DevelBranch
Software Research and Development

[Meepwn CTF] WhoAreYou write up

Posted in CTF with Capstone, keystone, meepwn, Reversing, unicorn, writeup

Phân tích

Thử thách này được pack bằng UPX, rất nhanh chóng, tôi dùng lệnh upx -d để unpack. Sau khi giải nén ta có: Thử thách này sẽ nhận một chuỗi đầu vào, tính toán ra một giá trị 64bit và kiểm tra với giá trị có sẵn. Nếu đúng, thì đó chính là cờ. Giả mã của hàm main khá ngắn:

int sub_140001230()
{
__int64 v1; // [sp+0h] [bp-48h]@4
char name[9]; // [sp+20h] [bp-28h]@1
__int64 v3; // [sp+30h] [bp-18h]@4

printf("Who Are You? ");
scanf("%8s", name);
if ( qword_1400067B0(name) )
printf("nope! Go and find yourself :(\n");
else
printf("Yeah! Its truly you :) here is ur flag: MeePwnCTF{%s}\n");
return 0;
}

Vấn đề nằm ở đoạn mã mà qword_1400067B0 đang trỏ tới. Nếu theo phân tích bình thường, chúng ta sẽ bị nhầm với đoạn mã ở đây:

__int64 sub_140001030()
{
  __int64 result; // [email protected]

  qmemcpy((void *)qword_1400067B8, sub_140004040, 0x217Fui64);
  result = qword_1400067B8;
  qword_1400067A8 = qword_1400067B8;
  return result;
}
.text:0000000140001060                 mov     rax, cs:qword_1400067B8
.text:0000000140001067                 mov     cs:qword_1400067B0, rax
.text:000000014000106E                 retn

Nếu ta tìm key cho hàm sub_140004040, thì kết quả sẽ được chuỗi f4k3f4k3, hoàn toàn không phải chuỗi cần tìm. Tuy nhiên, tôi đã tìm lại toàn bộ binary nhưng không tìm thấy một hàm nào phù hợp khác.

Tôi nhận ra vấn đề: Có lẽ trình nén là upx đã bị sửa, phần code giải mã thật chắc chắn nằm trong đó. Tôi không có nhiều thời gian để xem lại cả quá trình unpack upx. Để đơn giản, tôi chạy chương trình, dùng x64dbg để attach vào chương trình đang chạy và đặt breakpoint on execute ngay tại địa chỉ qword_1400067B0. Khi chương trình dừng tại đó, tôi dump toàn bộ phần code kiểm tra ra file (online_mem.bin).

Khi có đoạn code kiểm tra đúng rồi, chúng ta có một vài lựa chọn:

Phân tích thấy cả đoạn code chỉ dùng 2 thanh ghi rax và rbx để tính toán. Các lệnh tính toán chỉ bao gồm các lệnh add, sub, xor, rol, ror. Ngoài ra, chỉ có lệnh jmp trong toàn bộ đoạn code.

Phương pháp làm của tôi như sau:

Tôi là một big fan của Capstone/Keystone/Unicorn, trong lời giải này, tôi có cơ hội được sử dụng cả 3 framework cùng nhau :)

Key: uNp4ck3r

who_are_you.py

from capstone import *
from capstone.x86 import *
from keystone import *
from unicorn import *
from unicorn.x86_const import *
import struct


def assembler(address, assembly):
    ks = Ks(KS_ARCH_X86, KS_MODE_64)
    encoding, _ = ks.asm(assembly, address)
    patch_data = ''.join(chr(e) for e in encoding)
    return patch_data


def reverse_code(input):
    output_code = []
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    md.detail = True

    base_addr = 0
    rip = 0
    while True:
        try:
            code = input[rip : rip + 32] # 2 instructions
            insns = md.disasm(code, rip, 2)# try to disasm 2 instructions
            insn1 = insns.next()
            if insn1.mnemonic != 'jmp':
                insn2 = insns.next()
            insns.close()
        except StopIteration:
            break
        except CsError as ex:
            print("ERROR: %s" % ex)
            break
        if rip == 0:
            rip += insn1.size  # skip the first instruction
            continue

        if rip == 0x2171: # some last instructions
            output_code.append(assembler(rip, 'mov rbx, %d' % insn1.operands[1].imm))
            break

        elif insn1.mnemonic == 'jmp' and insn1.operands[0].type == X86_OP_IMM and (insn1.size == 5 or insn1.size == 2):
            rip = insn1.operands[0].imm
            continue
        elif 'mov' in insn1.mnemonic and insn1.operands[1].type == X86_OP_IMM:
            if insn2.mnemonic == 'add':
                output_code.append(str(code[0:insn1.size]) + assembler(rip, 'sub ' + insn2.op_str))
                rip += insn1.size + insn2.size
                continue
            if insn2.mnemonic == 'sub':
                output_code.append(str(code[0:insn1.size]) + assembler(rip, 'add ' + insn2.op_str))
                rip += insn1.size + insn2.size
                continue
            if insn2.mnemonic == 'xor':
                output_code.append(str(code[0:insn1.size + insn2.size]))
                rip += insn1.size + insn2.size
                continue

            output_code.append(str(code[0:insn1.size]))
            rip += insn1.size
            continue

        if insn1.mnemonic == 'rol':
            output_code.append(str(assembler(rip, 'ror ' + insn1.op_str)))
            rip += insn1.size
        elif insn1.mnemonic == 'ror':
            output_code.append(str(assembler(rip, 'rol ' + insn1.op_str)))
            rip += insn1.size
        else:
            raise Exception('should not be here:', hex(rip))


    return output_code[::-1]


def main():
    try:
        code = open("online_mem.bin", 'rb').read()
        out = reverse_code(code)
        code = ''.join(out)
    except Exception as e:
        print e
        return

    # emumator
    mu = Uc(UC_ARCH_X86, UC_MODE_64)
    sc_addr = 0x1c0000
    # map 8KB memory for this emulation
    mu.mem_map(sc_addr, 8 * 1024)
    # write machine code to be emulated to memory
    mu.mem_write(sc_addr, code)
    # emulate machine code in infinite time
    mu.emu_start(sc_addr, sc_addr + len(code))
    rbx = mu.reg_read(UC_X86_REG_RBX)
    print 'Key:', struct.pack('<Q', rbx)


if __name__ == '__main__':
    main()