[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]))

浙公网安备 33010602011771号