[Meepwn CTF] WhoAreYou write up

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; // rax@1

  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:

  • Patch lại vào file đã unpack, tại địa chỉ 0x140004040, rồi dùng IDA phân tích thuật toán, ... --> cách này quá dài, tôi thì lười
  • Patch lại vào file đã unpack, tại địa chỉ 0x140004040, rồi dùng angr framework để tìm 8 kí tự phù hợp --> Cách này chắc là dùng được.
  • Dựng một đoạn code chạy theo chiều ngược lại từ online_mem.bin, giá trị đầu ra của đoạn code này chính là 8 kí tự cần tìm  --> Tôi dùng cách này

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:

  • disassembly toàn bộ đoạn code:
    • nếu gặp jmp --> lấy rip mới và bỏ qua lệnh này
    • nếu gặp rol--> chuyển thành ror
    • nếu gặp ror--> chuyển thành rol
    • nếu gặp mov --> kiểm tra lệnh tiếp theo và đổi add thành sub, sub thành add
  • bỏ qua lệnh đầu tiên do nó không có tác dụng trong quá trình đảo ngược
  • gặp lệnh cuối cùng (mov rax, value; sub rax, rbx; ret), đây là lệnh kiểm tra nên sẽ chuyển thành mov rbx, value
  • đảo ngược thứ tự toàn bộ các lệnh, tạo nó thành một đoạn code mới
  • dùng emulator để thực thi toàn bộ đoạn code mới, giá trị đầu ra của rbx sẽ mô tả chính xác giá trị đúng của đầu vào

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 :)

  • Capstone is a multi-architecture disassembly framework : http://www.capstone-engine.org/
  • Keystone is a multi-architecture assembler framework : http://www.keystone-engine.org/
  • Unicorn Engine is a multi-architecture CPU emulator framework: http://www.unicorn-engine.org/

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()