[DEF CON CTF 2025 Qualifiers] [reverse] tiniii write-up

tiniii

逆向题,ida打开后看到被混淆的汇编,仔细看汇编逻辑可以发现,每条汇编都被拆分成了两个block,第一个block包装了原本的汇编代码,第二个block实现分支跳转命令。

add rsp, 8
; real inst
jmp branch_block

branch_block:
jz branch_a
call branch_b

使用idapython解析这些block,还原为一个CFG,把没有分支跳转的block缩为一个点:

import idautils
import idc
import ida_bytes
import ida_kernwin
import ida_ua

from queue import Queue
import time

def is_instruction_head(ea):
    flags = ida_bytes.get_full_flags(ea)
    
    if ida_bytes.is_code(flags) and ea == idc.get_item_head(ea):
        return True
    else:
        return False

def ui_make_code(ea):
    # 选择目标地址
    ida_kernwin.jumpto(ea)
    
    # 发送 "Make Code" 命令(对应 C 键)
    ida_kernwin.process_ui_action("MakeCode")
    
    # 等待分析完成
    # ida_bytes.auto_wait()
    # time.sleep(0.1)

def make_code_at(ea):
    
    # 创建指令(类似按 C 键)
    success = idc.create_insn(ea)
    
    if not success:
        print(f"Failed to create code at {hex(ea)}")
        ui_make_code(ea)
        return
    
    # 生成反汇编文本
    disasm = idc.GetDisasm(ea)
    print(f"Disassembly at {hex(ea)}:\n{disasm}")

def undefine_code(ea):
    print("[+] undefine code...")
    idc.del_items(ea)

def resolve_call_target(ea):    
    # 获取操作数类型(重要)
    op_type = idc.get_operand_type(ea, 0)
    
    # 直接地址类型(如 call 0x219E3F3)
    if op_type == idc.o_near:
        return idc.get_operand_value(ea, 0)
    
    # 处理子函数符号(如 call sub_219E3F3)
    elif op_type == idc.o_mem:
        symbol_name = idc.print_operand(ea, 0)
        return idc.get_name_ea_simple(symbol_name)

def get_asm_details(ea):
    if not is_instruction_head(ea):
        print("[+] undefine & make")
        undefine_code(ea)            
        make_code_at(ea)

    # 获取助记符(如 mov, call)
    mnemonic = idc.print_insn_mnem(ea)
    
    # 获取完整汇编指令
    full_asm = idc.generate_disasm_line(ea, 0)
    
    # 获取操作数(最多处理3个操作数)
    operands = []
    for i in range(3):  # 遍历操作数索引
        op = idc.print_operand(ea, i)
        if not op:
            break
        operands.append(op)
    
    return {
        "address": ea,
        "mnemonic": mnemonic,
        "operands": operands,
        "full_instruction": full_asm.strip(),
        "hex_addr": hex(ea)
    }

cond_jmp = ['jz', 'jnz', 'jg', "jge", "jl", "jle", "ja", "jae", "jb", "jbe", "jc", "jnc"]

g_deob_insts = []
def parse_block(ea):
    print("[+] prase block", hex(ea))

    global g_deob_insts
    cur_ea = ea
    inst_list = []
    next_ea = 0
    real_inst = None
    
    for i in range(3):
        res = get_asm_details(cur_ea)
        # debug_inst(res)
        inst_list.append(res)
        
        instr_length = idc.get_item_size(cur_ea)  # 返回整型字节数
        cur_ea += instr_length
    

    if inst_list[0]['mnemonic'] == 'add':
        if inst_list[1]['mnemonic'] == 'jmp': 
            nop = {
                "address": ea,
                "mnemonic": "nop",
                "operands": [],
                "full_instruction": "nop",
                "hex_addr": hex(ea)
            }
            real_inst = nop
            next_ea = resolve_call_target(inst_list[1]['address'])
            real_inst['next_ea'] = next_ea
        elif inst_list[1]['mnemonic'] == 'retn':
            real_inst = inst_list[1]
            next_ea = 0x0
            real_inst['next_ea'] = next_ea
        elif inst_list[1]['mnemonic'] == 'leave':
            real_inst = inst_list[1]
            next_ea = 0x0
            real_inst['next_ea'] = next_ea
        elif inst_list[2]['mnemonic'] == 'jmp':
            # normal 
            real_inst = inst_list[1]
            next_ea = resolve_call_target(inst_list[2]['address'])        
            real_inst['next_ea'] = next_ea
        else:
            print('[!] wtf block?', hex(ea))
            exit(-1)            
    else:
        print('[!] wtf block?', hex(ea))
        exit(-1)

    print("[+] prase block end.", hex(next_ea), real_inst)
    
    return next_ea, real_inst

def parse_br(ea):
    print("[+] prase branch", hex(ea))
    global g_deob_insts
    cur_ea = ea
    inst_list = []
    next_lea = 0
    next_rea = 0
    
    # undefine_code(ea)
    for i in range(2):
        # undefine_code(cur_ea)
        res = get_asm_details(cur_ea)
        # debug_inst(res)
        inst_list.append(res)
        
        if res['mnemonic'] == 'call':
            break
        
        instr_length = idc.get_item_size(cur_ea)  # 返回整型字节数
        cur_ea += instr_length

    if inst_list[0]['mnemonic'] == 'call':
        call_target = resolve_call_target(inst_list[0]['address'])
        print("next lea:", hex(call_target))
        next_lea = call_target
        next_rea = call_target
    elif inst_list[0]['mnemonic'] in cond_jmp and inst_list[1]['mnemonic'] == 'call':
        call_target = resolve_call_target(inst_list[0]['address'])
        next_lea = call_target

        call_target = resolve_call_target(inst_list[1]['address'])        
        next_rea = call_target
    else:
        print("[!] wtf cmd?", inst_list[0]['mnemonic'], inst_list[0]['hex_addr'])
        exit(-1)

    print("next:", hex(next_lea), hex(next_rea))
    return next_lea, next_rea

def debug_inst(result):
    print(f"地址 {result['address']}:")
    print(f"指令: {result['full_instruction']}")
    print(f"助记符: {result['mnemonic']}")
    print(f"操作数: {result['operands']}")

def print_result():
    global g_deob_insts
    for inst in g_deob_insts:
        print(inst['full_instruction'])

g_res_fp = open("/tmp/result.s", "w")

def do_parse(start_ea):
    flag = True
    cur_ea = start_ea
    visit = {}
    nodes = {}
    
    ins_q = Queue()
    ins_q.put(cur_ea)
    counts = 0
    cur_node = {
        "asm" : "",
        "address" : cur_ea,
        "lch" : 0,
        "rch" : 0}
    nodes[cur_ea] = cur_node
    last_cur_node = cur_node
    flag_in_node = True
    root = cur_node

    def do_preorder(rt):
        travel_count = 0

        if rt["address"] in visit:
            # print("[+] loop finish")
            return
        visit[rt['address']] = 1
        travel_count += 1
        
        print("[+] bbl", hex(rt['address']))
        
        # 缩点
        cur = rt
        vi = {}
        while cur["lch"] != 0 and cur["rch"] == 0:
            
            if cur["lch"] not in nodes:
                print("[+] wtf not in nodes", hex(cur["lch"]))
                break            
            
            if cur['address'] in vi:
                break
            vi[cur['address']] = 1
            travel_count += 1
        
            if travel_count %10000 == 0:
                print("[+] travel graph...", travel_count)
                g_res_fp.write(rt["asm"])
                rt["asm"] = ""
        
            nxt = nodes[cur["lch"]]
            if 'nop' not in nxt["asm"]:
                rt["asm"] += nxt["asm"]
                # print(nxt['asm'])
                
            rt["lch"] = nxt["lch"]
            rt["rch"] = nxt["rch"]
            cur = nxt
        
        print(rt["asm"])
        print("-------------------")
        
        if rt["lch"] != 0:
            if rt["lch"] not in nodes:
                print("[+] wtf not in nodes", hex(cur["lch"]))
                return
            do_preorder(nodes[rt["lch"]])
            
        if rt["rch"] != 0:
            if rt["rch"] not in nodes:
                print("[+] wtf not in nodes", hex(cur["rch"]))
                return
            do_preorder(nodes[rt["rch"]])

    while not ins_q.empty():
        cur_ea = ins_q.get()
        print("[+] cur inst ea:", hex(cur_ea))

        if cur_ea in visit:
            print("[+] loop detect, continue!!!")
            continue

        counts += 1
        visit[cur_ea] = 1
                
        # pre parse
        cur_ins = get_asm_details(cur_ea)
        if cur_ins['mnemonic'] == 'call' or cur_ins['mnemonic'] in cond_jmp:
            n_lea, n_rea = parse_br(cur_ea)
            
            if cur_ins['mnemonic'] in cond_jmp:                
                ins_q.put(n_lea)
                ins_q.put(n_rea)

                node = {
                    "asm" : hex(cur_ea) + ":" + cur_ins['mnemonic'] + " " + hex(n_lea) + "," + hex(n_rea),
                    "address" : cur_ea,
                    "lch" : n_lea,
                    "rch" : n_rea
                }
                
                nodes[cur_ea] = node
            else:
                # direct no branch
                ins_q.put(n_lea)

                node = {
                    "asm" : hex(cur_ea) + ": nop\n",
                    "address" : cur_ea,
                    "lch" : n_lea,
                    "rch" : 0
                }
                nodes[cur_ea] = node

        elif cur_ins['mnemonic'] == 'add':
            next_ea, real_inst = parse_block(cur_ea)
            
            if next_ea != 0x0:
                ins_q.put(next_ea)
            
            node = {
                "asm" : real_inst['hex_addr'] + ": " + real_inst['full_instruction'] + "\n",
                "address" : cur_ea,
                "lch" : next_ea,
                "rch" : 0
            }
            nodes[cur_ea] = node
        
        if counts > 0xC3500 * 8 + 100:
            print("[+] debug exit")
            break
        
        print("[+] -------------------")

    visit = {}
    root = nodes[0x1BCC]
    print("[+] node count:", len(nodes), "preorder travel...")
    do_preorder(root)
    return root

target_ea = 0x1BCC
#target_ea = 0x21937E1
# result = parse_block(target_ea)
do_parse(target_ea)
# print_result()

之后反混淆的汇编代码,继续分析验证逻辑:
main函数如下:首先读文件,检查长度为8000byte,之后调用check_1函数,如果成功则再进行hash check,最后输出flag。
一般逆向题目的hash check不需要逆向,只是出题人没有把握不出非预期解。所以校验的关键在check_1函数

[+] bbl 0x219e3d6
0x219e3da: mov     rbp, rsp
0x219e3e6: lea     r11, [rsp-8+var_61DFF8]
0x219e3f7: sub     rsp, 1000h
0x219e407: or      [rsp-8+arg_0], 0
0x219e414: cmp     rsp, r11
0x33a198c: branch 0x33a1977,0x219e41c
-------------------
[+] bbl 0x33a1977
0x33a1977: nop
0x219e3f7: sub     rsp, 1000h
0x219e407: or      [rsp-8+arg_0], 0
0x219e414: cmp     rsp, r11
0x33a198c: branch 0x33a1977,0x219e41c
-------------------
[+] bbl 0x219e41c
0x219e420: sub     rsp, 6C0h
0x219e430: mov     [rbp-61E6B4h], edi; argc
0x219e43f: mov     [rbp-61E6C0h], rsi; argv
0x219e44f: mov     rax, fs:28h
0x219e461: mov     [rbp-8], rax
0x219e46e: xor     eax, eax
0x219e479: lea     rax, [rbp-61A820h]; memset 0
0x219e489: mov     edx, offset loc_61A800
0x219e497: mov     esi, 0
0x219e4a5: mov     rdi, rax
0x219e4b1: call    _memset
0x219e4bf: lea     rdx, [rbp-61E6A0h]; memset 0
0x219e4cf: mov     eax, 0
0x219e4dd: mov     ecx, 3E8h
0x219e4eb: mov     rdi, rdx
0x219e4f7: rep stosq
0x219e503: lea     rdx, [rbp-61C760h]; memset 0 input
0x219e513: mov     eax, 0
0x219e521: mov     ecx, 3E8h
0x219e52f: mov     rdi, rdx
0x219e53b: rep stosq
0x219e547: lea     rax, [rbp-61A820h]; array_1
0x219e557: mov     rdi, rax
0x219e563: call    loc_219F0C0; init_1
0x219e571: lea     rax, [rbp-61E6A0h]; array_2
0x219e581: mov     rdi, rax
0x219e58d: call    sub_339BD66; init_2
0x219e59b: cmp     dword ptr [rbp-61E6B4h], 1; argc > 1
0x33a1a69: branch 0x33a1ab9,0x219e5a7
-------------------
[+] bbl 0x33a1ab9
0x33a1ab9: nop
0x219e627: mov     rax, [rbp-61E6C0h]
0x219e637: add     rax, 8
0x219e644: mov     rax, [rax]
0x219e650: lea     rdx, aRb; "rb"
0x219e660: mov     rsi, rdx
0x219e66c: mov     rdi, rax
0x219e678: call    _fopen
0x219e686: mov     [rbp-61E6A8h], rax
0x219e696: cmp     qword ptr [rbp-61E6A8h], 0
0x33a1b07: branch 0x33a1b2d,0x219e6a3
-------------------
[+] bbl 0x33a1b2d
0x33a1b2d: nop
0x219e6ed: mov     rdx, [rbp-61E6A8h]; fp
0x219e6fd: lea     rax, [rbp-61C760h];input
0x219e70d: mov     rcx, rdx
0x219e719: mov     edx, 3E8h ; 1000 byte
0x219e727: mov     esi, 8
0x219e735: mov     rdi, rax
0x219e741: call    _fread
0x219e74f: mov     rax, [rbp-61E6A8h]
0x219e75f: mov     rdi, rax
0x219e76b: call    _fclose
0x219e779: mov     dword ptr [rbp-61E6B0h], 0
0x219e78c: mov     dword ptr [rbp-61E6ACh], 0
0x219e837: cmp     dword ptr [rbp-61E6ACh], 3E7h ; check read count
0x33a1bea: branch 0x33a1b98,0x219e846
-------------------
[+] bbl 0x33a1b98
0x33a1b98: nop
0x219e7ad: mov     eax, [rbp-61E6ACh]
0x219e7bc: cdqe
0x219e7c7: mov     eax, [rbp+rax*8-61C760h]
0x219e7d7: test    eax, eax
0x33a1bb2: branch 0x33a1bd1,0x219e7de
-------------------
[+] bbl 0x33a1bd1
0x33a1bd1: nop
0x219e817: add     dword ptr [rbp-61E6B0h], 1
0x219e827: add     dword ptr [rbp-61E6ACh], 1
0x219e837: cmp     dword ptr [rbp-61E6ACh], 3E7h
0x33a1bea: branch 0x33a1b98,0x219e846
-------------------
[+] bbl 0x219e846
0x219e84a: cmp     dword ptr [rbp-61E6B0h], 0
0x33a1bf6: branch 0x33a1c1d,0x219e856
-------------------
[+] bbl 0x33a1c1d
0x33a1c1d: nop
0x219e8a0: lea     rdx, [rbp-61C760h]; input
0x219e8b0: lea     rcx, [rbp-61E6A0h]; array_2
0x219e8c0: lea     rax, [rbp-61A820h]; array_1
0x219e8d0: mov     rsi, rcx
0x219e8dc: mov     rdi, rax
0x219e8e8: call    sub_219EE00(array_1, array_2, input) //check_1
0x219e8f6: test    eax, eax
0x33a1c5b: branch 0x33a1d3b(incorrect),0x219e8fd(check2 hash check)
-------------------
[+] bbl 0x33a1d3b
0x33a1d3b: nop
0x219ea86: lea     rax, aIncorrect; "Incorrect :("
0x219ea96: mov     rdi, rax
0x219eaa2: call    _puts
0x219eab0: mov     eax, 0
0x219eabe: mov     rdx, [rbp-8]
0x219eacb: sub     rdx, fs:28h
0x33a1d64: branch 0x33a1d70,0x219ead9
-------------------
[+] bbl 0x33a1d70
0x33a1d70: nop
0x219eaeb: leave

-------------------
[+] bbl 0x219ead9
0x219eadd: call    ___stack_chk_fail

-------------------
[+] bbl 0x219e8fd
0x219e901: mov     qword ptr [rbp-20h], 0
0x219e912: mov     qword ptr [rbp-18h], 0
0x219e923: lea     rax, [rbp-61C760h]; input
0x219e933: mov     rdi, rax
0x219e93f: call    sub_219EAFB ;calc hash 1
0x219e94d: lea     rax, [rbp-20h]
0x219e95a: mov     rsi, rax
0x219e966: lea     rax, unk_33A7280_flag
0x219e976: mov     rdi, rax
0x219e982: call    sub_33A4154 ;calc hash 2
0x219e990: lea     rax, [rbp-20h]
0x219e99d: mov     edx, 10h
0x219e9ab: lea     rcx, unk_33A506D ; EA 29 ED BF 55 33 D6 1F 36 A0 5C 80 5A 88 C9 18
0x219e9bb: mov     rsi, rcx
0x219e9c7: mov     rdi, rax
0x219e9d3: call    _memcmp ;memcpy hash
0x219e9e1: test    eax, eax
0x33a1ce7: branch 0x33a1d1f,0x219e9e8; fail, success
-------------------
[+] bbl 0x33a1d1f
0x33a1d1f: nop
0x219ea4e: lea     rax, aIncorrectHash; "Incorrect hash :("
0x219ea5e: mov     rdi, rax
0x219ea6a: call    _puts
0x219eab0: mov     eax, 0
0x219eabe: mov     rdx, [rbp-8]
0x219eacb: sub     rdx, fs:28h
0x33a1d64: branch 0x33a1d70,0x219ead9
-------------------
[+] bbl 0x219e9e8
0x219e9ec: lea     rax, unk_33A7280_flag
0x219e9fc: mov     rsi, rax
0x219ea08: lea     rax, aCongratulation; "Congratulations! The flag is %s.\n"
0x219ea18: mov     rdi, rax
0x219ea24: mov     eax, 0
0x219ea32: call    _printf
0x219eab0: mov     eax, 0
0x219eabe: mov     rdx, [rbp-8]
0x219eacb: sub     rdx, fs:28h
0x33a1d64: branch 0x33a1d70,0x219ead9
-------------------
[+] bbl 0x219e856
0x219e85a: lea     rax, aYourLicenseFil; "Your license file seems to be blank."
0x219e86a: mov     rdi, rax
0x219e876: call    _puts
0x219e884: mov     eax, 1
0x219eabe: mov     rdx, [rbp-8]
0x219eacb: sub     rdx, fs:28h
0x33a1d64: branch 0x33a1d70,0x219ead9
-------------------
[+] bbl 0x219e7de
0x219e7e2: mov     eax, [rbp-61E6ACh]
0x219e7f1: cdqe
0x219e7fc: mov     eax, [rbp+rax*8-61C75Ch]
0x219e80c: test    eax, eax
0x33a1bcf: branch 0x33a1bd8,0x219e813
-------------------
[+] bbl 0x33a1bd8
0x33a1bd8: nop
0x219e827: add     dword ptr [rbp-61E6ACh], 1
0x219e837: cmp     dword ptr [rbp-61E6ACh], 3E7h
0x33a1bea: branch 0x33a1b98,0x219e846
-------------------
[+] bbl 0x219e813
0x219e817: add     dword ptr [rbp-61E6B0h], 1

-------------------
[+] bbl 0x219e6a3
0x219e6a7: lea     rax, aFailedToOpenLi; "Failed to open license file."
0x219e6b7: mov     rdi, rax
0x219e6c3: call    _puts
0x219e6d1: mov     eax, 1
0x219eabe: mov     rdx, [rbp-8]
0x219eacb: sub     rdx, fs:28h
0x33a1d64: branch 0x33a1d70,0x219ead9
-------------------
[+] bbl 0x219e5a7
0x219e5ab: mov     rax, [rbp-61E6C0h]
0x219e5bb: mov     rax, [rax]
0x219e5c7: mov     rsi, rax
0x219e5d3: lea     rax, aUsageSLicenseF; "Usage: %s <license file>\n"
0x219e5e3: mov     rdi, rax
0x219e5ef: mov     eax, 0
0x219e5fd: call    _printf
0x219e60b: mov     eax, 1
0x219eabe: mov     rdx, [rbp-8]
0x219eacb: sub     rdx, fs:28h
0x33a1d64: branch 0x33a1d70,0x219ead9
-------------------

逆向check_1函数,结合deekseek-r1的结果,可以推测出c代码:
这里对于a+b = c,已知c求a和b的操作,编写代码,对于每个c,遍历a,使用map查找b,这样时间复杂度是nlogn,可以求解

void check_1(int* array_1,int*  array_2,int**  input) {
    
    for (int i = 0; i <= 19999; i++) {
        int idx = i % 1000;
    
        int val1 = array_2 [idx];
        u_int32_t k1 = input[idx][0];
        u_int32_t k2 = input[idx][1];

        if (k1 >= 0xc34ff) return 0;
        if (k2 >= 0xc34ff) return 0;

        // 计算前向累加和
        int sum1 = sum(array_1, 0, k1);

        // 计算后向累加和
        int sum2 = sum(array_1, 0xC3500*8 - k2, 0xC3500*8);

        if (sum1 == 0 || sum2 == 0) continue;
        if (val1 != sum1 + sum2) return 0;
    }

    return 1; // success
}

需要注意的是,这里的结果可能是无解,无解用0,0代替,会直接continue,在汇编中也可以看到这段逻辑

import sys
import struct

array1 = []
array2 = []

def load_array(asm):
    tmp = []
    for l in asm:
        t = l.split(" ")
        if t[1] == "mov" and "qword" in l:
            # print(t)
            val = int(t[9].replace("h", ""), 16)
            tmp.append(val)
    return tmp

def prefix_sum(arr):
    tmp = []
    tmp.append(0)
    for i in range(1, len(arr)+1, 1):
        tmp.append((tmp[i-1] + arr[i-1]) & 0xffffffffffffffff)

    return tmp

def write_license(arr):
    lic = b''
    for i in range(1000):
        lic += struct.pack("<I", arr[i*2])
        lic += struct.pack("<I", arr[i*2+1])
        
    with open("lic.bin", "wb") as fp:
        fp.write(lic)

with open(sys.argv[1], "r") as fp:
    asm = fp.read().strip().split("\n")
    array1 = load_array(asm)
    
    arrr1_pre_sum = prefix_sum(array1)
    arrr1_suf_sum = prefix_sum(array1[::-1])

    arrr1_suf_sum_mp = {}

    for k in range(len(arrr1_suf_sum)):
        arrr1_suf_sum_mp[ arrr1_suf_sum[k] ] = k

# print(arrr1_suf_sum)
with open(sys.argv[2], "r") as fp:
    asm = fp.read().strip().split("\n")
    array2 = load_array(asm)
    
def uint32_sub(a, b):
    return (a - b) % (1 << 64)  # 2^64取模

result = []
for i in range(len(array2)):
    ans = array2[i]
    value1 = 0
    value2 = 0
    # print(hex(ans))
    
    for t in range(len(arrr1_pre_sum)):        
        if ans >= arrr1_pre_sum[t]:
            res = ans - arrr1_pre_sum[t]
        else:
            continue

        # for j in arrr1_suf_sum:
        #     if (j + arrr1_pre_sum[t]) & 0xffffffff == ans:
        #         print("[+] fuck")
        #         break

        if res in arrr1_suf_sum_mp:
            value2 = arrr1_suf_sum_mp[res]
            value1 = t

    print("[!] ans:", value1, value2)
    result += [value1, value2]

print(result)

# test = [0, 0] + [1, 1] + [0]*(2000-4)
# print(test)
write_license(result)
# print(hex(arrr1_pre_sum[800000-1] + arrr1_suf_sum[800000-1]))
# print(hex(arrr1_pre_sum[800000-1]), hex(arrr1_suf_sum[800000-1]))
posted @ 2025-05-07 13:10  Helica  阅读(83)  评论(0)    收藏  举报