Last updated on January 30, 2024 am
记录一道十分nice的题目
我觉得这道题出得十分得好,它采取了较为简单的控制流混淆,可以使用angr恢复出原本的逻辑,也可以用idapython直接patch去混淆,当然也可以直接使用angr跑出flag,但这样就失去了这题本身的意义了。
混淆
这题采取的是控制流混淆,也就是增加一些无关紧要的变量,以这些变量之间的稀奇古怪的运算作为控制条件,再在各个块中跳来跳去。
以这题中一个关键函数start_quest
来看,可以发现里面有很多y26 >= 10 && (x25-1) * (x25 & 1) !=0
等诸如此类的控制条件,并且反复出现相同的语句去控制。
再看看它的CFG
给人的感觉就是冗长、繁琐。然而相比于它的大哥控制流平坦化,这还是比较友好的。
去混淆
思路一 IDApython
我们知道了这道题是以一些无关变量来操控控制流,以达到混淆目的。很自然的一个想法就是,如果这些变量的值都不会变,那么控制语句的结果不就是唯一确定的吗。于是乎,我们想到去查看这些变量的定义位置,会惊奇的发现,它们全部都在bss段定义!
我们再交叉引用几个变量,又会惊奇地发现,它们的值全都没有发生改变。
也就是说,那些控制语句的结果就是唯一的!可是这样子的话,那么IDA就应该能分析出来并优化掉那些不可达语句。然而事实很明朗,并没有。原因在于,在IDA眼中,这些仍然是变量,只不过是不会变的变量。
那么这就有了第一个patch思路,也就是我们手动帮助IDA识别,即把所有的这些变量全部patch成0。比如说原本是mov eax,y26
,那么我们就patch成mov eax,0
。可能会想到,那这么多的变量,我得patch到啥时候呀?这个情况下,我们就应该想到可以借助IDApython来帮我们完成,毕竟 重复性 的工作是计算机最擅长的。
直接上脚本,脚本中会有详细注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import ida_xref
ea = 0x610318 end = 0x6105A8 regs = set()
pad = b'\x90\x90' patch = {'eax':b'\xB8\x00\x00\x00\x00' + pad, 'ebx':b'\xBB\x00\x00\x00\x00' + pad, 'ecx':b'\xB9\x00\x00\x00\x00' + pad, 'edx':b'\xBA\x00\x00\x00\x00' + pad, 'esi':b'\xBE\x00\x00\x00\x00' + pad, 'edi':b'\xBF\x00\x00\x00\x00' + pad, 'r8d':b'\x41\xB8\x00\x00\x00\x00' + pad, 'r9d':b'\x41\xB9\x00\x00\x00\x00' + pad, 'r10d':b'\x41\xBA\x00\x00\x00\x00' + pad, 'r11d':b'\x41\xBB\x00\x00\x00\x00' + pad, 'r12d':b'\x41\xBC\x00\x00\x00\x00' + pad, 'r13d':b'\x41\xBD\x00\x00\x00\x00' + pad, 'r14d':b'\x41\xBE\x00\x00\x00\x00' + pad, 'r15d':b'\x41\xBF\x00\x00\x00\x00' + pad} ''' 如何确定只有这些寄存器? 当然也是使用IDApython 可以先在下面的reg =idc.print_operand(ref,0)后追加 regs.add(reg) 最后再print(regs)就可以得到了
为什么要加一个pad? 可以自己手动patch一两个,看看patch后和patch前长度有啥区别就知道了
确实 按道理来说 这些应该也有规律的 可以使用脚本来表示的,而非字典 但是 我不会:( '''
for addr in range(ea,end,4): ref = ida_xref.get_first_dref_to(addr) while ref != idaapi.BADADDR: if 'mov' in GetDisasm(ref): reg =idc.print_operand(ref,0) print("before:",GetDisasm(ref)) idaapi.patch_bytes(ref,patch[reg]) print('after:',GetDisasm(ref)) ref = ida_xref.get_next_dref_to(addr,ref)
|
patch完成之后,CFG还是那个CFG,但是反编译已经成功去混淆了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| __int64 __fastcall start_quest(std::string *a1) { _BYTE v2[4]; int v3; std::string *v5; int *v6; _BYTE *v7; _BYTE *v8; std::string *v9;
v9 = a1; v8 = &v2[-16]; v7 = &v2[-16]; v6 = &v2[-16]; v5 = &v2[-16]; std::vector<int>::push_back(&hero, &secret_100); std::vector<int>::push_back(&hero, &secret_214); std::vector<int>::push_back(&hero, &secret_266); std::vector<int>::push_back(&hero, &secret_369); std::vector<int>::push_back(&hero, &secret_417); std::vector<int>::push_back(&hero, &secret_527); std::vector<int>::push_back(&hero, &secret_622); std::vector<int>::push_back(&hero, &secret_733); std::vector<int>::push_back(&hero, &secret_847); std::vector<int>::push_back(&hero, &secret_942); std::vector<int>::push_back(&hero, &secret_1054); std::vector<int>::push_back(&hero, &secret_1106); std::vector<int>::push_back(&hero, &secret_1222); std::vector<int>::push_back(&hero, &secret_1336); std::vector<int>::push_back(&hero, &secret_1441); std::vector<int>::push_back(&hero, &secret_1540); std::vector<int>::push_back(&hero, &secret_1589); std::vector<int>::push_back(&hero, &secret_1686); std::vector<int>::push_back(&hero, &secret_1796); std::vector<int>::push_back(&hero, &secret_1891); std::vector<int>::push_back(&hero, &secret_1996); std::vector<int>::push_back(&hero, &secret_2112); std::vector<int>::push_back(&hero, &secret_2165); std::vector<int>::push_back(&hero, &secret_2260); std::vector<int>::push_back(&hero, &secret_2336); std::vector<int>::push_back(&hero, &secret_2412); std::vector<int>::push_back(&hero, &secret_2498); std::vector<int>::push_back(&hero, &secret_2575); if ( std::string::length(v9) - 1LL != legend >> 2 ) { *v6 = legend >> 2; } else { std::string::string(v5, v9); v3 = sanitize_input(v5); *v6 = v3; std::string::~string(v5); } return *v6; }
|
会发现,代码逻辑已经十分清晰了。
思路二 Angr符号执行
前面提即,那些控制变量的条件是确定的,也就是哪些走、哪些地方不走是确定,如果我们可以直接把不走的地方给nop掉,不也可达到去混淆的目的吗?
那问题在于,我们如何确定哪些块该走,哪些块不该走呢? IDApython或许也可以一试,毕竟这也是重复性的活,不过这时候规律性就没那么明显了,脚本就不是很好写了。但是我们还有Angr这一个帮手呀,它不就是一步一步帮我们探索嘛,那只要把它没有走到的块给nop掉不就好了嘛
直接看脚本吧,脚本附有详细注释
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| import angr import argparse import os
Blocks = set()
def GetCFG(func_addr): print("[*] Getting CFG") cfg = proj.analyses.CFGFast(normalize = True,force_complete_scan = False) function_cfg = cfg.functions.get(func_addr).transition_graph print("[+] Getting CFG Done") return function_cfg
def GetBlock(cfg): print("[*] Getting Block ") for node in cfg.nodes: Blocks.add(node.addr) print("[+] Getting Block Done")
def Hook(block): for insn in block.capstone.insns: if insn.mnemonic == 'call': src_Addr = int(insn.op_str,16) proj.hook(src_Addr,angr.SIM_PROCEDURES["stubs"]["ReturnUnconstrained"](), replace=True)
def Patch(block): start = block.addr - proj.loader.main_object.mapped_base size = block.size oridata[start : start + size] = b'\x90' * size
def Write(oridata): print("[*] Wtring File") fname, suffix = os.path.splitext(file) newname = fname +'_patched' + suffix with open(newname,'wb') as newfile: newfile.write(oridata) print("[+] Wtring Done")
def deobfu_func(func_addr): state = proj.factory.blank_state(addr = func_addr) simgr = proj.factory.simgr(state) while len(simgr.active): for active in simgr.active: Blocks.discard(active.addr) block = proj.factory.block(active.addr) Hook(block) simgr.step()
print("[*] Patching") for block_addr in Blocks: Patch(proj.factory.block(block_addr)) print("[+] Patching Done")
if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('-f', '--file', required=True, help='File to deobfuscate') parser.add_argument('-s', '--start', type=lambda x : int(x, 0), help='Starting address of target function') args = parser.parse_args()
file = args.file start = args.start proj = angr.Project(file,auto_load_libs=False)
with open(file,'rb') as orifile: oridata = bytearray(orifile.read())
func_cfg = GetCFG(start) GetBlock(func_cfg) deobfu_func(start)
Write(oridata)
|
值得注意的是,每符号执行一次,只能清除一个函数的混淆,如果有多个函数需要清楚混淆,就要重复执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| __int64 __fastcall start_quest(std::string *a1) { _BYTE v2[4]; int v3; std::string *v5; int *v6; _BYTE *v7; _BYTE *v8; std::string *v9;
v9 = a1; v8 = &v2[-16]; v7 = &v2[-16]; v6 = &v2[-16]; v5 = &v2[-16]; std::vector<int>::push_back(&hero, &secret_100); std::vector<int>::push_back(&hero, &secret_214); std::vector<int>::push_back(&hero, &secret_266); std::vector<int>::push_back(&hero, &secret_369); std::vector<int>::push_back(&hero, &secret_417); std::vector<int>::push_back(&hero, &secret_527); std::vector<int>::push_back(&hero, &secret_622); std::vector<int>::push_back(&hero, &secret_733); std::vector<int>::push_back(&hero, &secret_847); std::vector<int>::push_back(&hero, &secret_942); std::vector<int>::push_back(&hero, &secret_1054); std::vector<int>::push_back(&hero, &secret_1106); std::vector<int>::push_back(&hero, &secret_1222); std::vector<int>::push_back(&hero, &secret_1336); std::vector<int>::push_back(&hero, &secret_1441); std::vector<int>::push_back(&hero, &secret_1540); std::vector<int>::push_back(&hero, &secret_1589); std::vector<int>::push_back(&hero, &secret_1686); std::vector<int>::push_back(&hero, &secret_1796); std::vector<int>::push_back(&hero, &secret_1891); std::vector<int>::push_back(&hero, &secret_1996); std::vector<int>::push_back(&hero, &secret_2112); std::vector<int>::push_back(&hero, &secret_2165); std::vector<int>::push_back(&hero, &secret_2260); std::vector<int>::push_back(&hero, &secret_2336); std::vector<int>::push_back(&hero, &secret_2412); std::vector<int>::push_back(&hero, &secret_2498); std::vector<int>::push_back(&hero, &secret_2575); if ( std::string::length(v9) - 1LL != legend >> 2 ) { *v6 = legend >> 2; } else { std::string::string(v5, v9); v3 = sanitize_input(v5); *v6 = v3; std::string::~string(v5); } return *v6; }
|
这是运行完,清除start_quest
混淆后的效果,可以发现效果不错,而且与之前IDApython的结果不能说差不多吧,只能说一模一样。因为这二者的思路本质上都是一样的,殊途同归了属于是
End
清除混淆后的步骤就比较简单了,虽然它还有一个变量名混淆,就是一个变量赋值来赋值去的,不过这个直接使用IDA的变量重命名就能轻松搞定了。
对比一下可以发现,IDApython的脚本简洁明了,运行速度也快于Angr,但它的缺点就是在面对复杂情况下的混淆可能就有点乏力,比如这几个变量不再初始为0,而是随机的初始值,可能就得费一番功夫去修改脚本了。而Angr的优势在于它的准确性,因为它就是实打实的去跑,去探索究竟哪一个块是可达的,哪一个块是不可达的,即使面对复杂的控制流混淆,这一本质是仍是通用的。
二者各有优劣,但都很精彩。
(当然这题也可以直接符号执行出flag,但是这显然就学不到什么了)