使用Binary Ninja新反编译器重访2000次挑战
距离我的博客文章《用Binary Ninja完成2000次切割》已过去四年。当时Binary Ninja还处于私有测试阶段,那篇博客的反响让Vector35的开发团队感到惊讶。过去几年我主要使用IDA和HexRays进行逆向工程,而用Binary Ninja进行脚本编写。坚持使用HexRays的主要原因?它是一个交互式反编译器,而Binary Ninja直到今天才拥有反编译器功能!
如今,Vector35将他们的反编译器发布到了开发分支,因此我认为使用新的反编译器API重新审视2000次切割挑战会很有趣。
进入高级中间语言(HLIL)
该反编译器建立在另一种中间语言——高级中间语言(HLIL)之上。HLIL进一步抽象了现有的中间语言:低级中间语言(LLIL)、中级中间语言(MLIL)以及其他五种主要用于脚本的IL。HLIL具有激进的死代码消除、常量折叠、switch恢复等反编译器应有的功能,但有一个例外:Binary Ninja的反编译器不以C语言为目标。相反,他们专注于可读性,我认为这是好事,因为C语言充满许多特性和隐式操作/转换,这使得理解变得非常困难。
使用我们的一个2000次切割挑战,我们来比较Binary Ninja的反编译器与其IL抽象。
首先,原始反汇编:
ASM反汇编包含43条指令,其中三条算术指令、27条内存操作(加载、存储)、四条传输指令、一条比较指令和八条控制流指令。在这些指令中,我们主要关心控制流指令和一小部分内存操作。
接下来是低级中间语言(LLIL):
LLIL模式不会删除任何指令;它只是为我们提供了一个一致的接口来编写脚本,无论反汇编的架构是什么。
使用中级中间语言(MLIL)后,情况开始好转:
在MLIL中,栈变量被识别,死存储被消除,常量被传播,调用具有参数。指令数量减少到17条(原版的39%)。由于MLIL理解栈帧,所有算术操作都被移除,所有“push”和“pop”指令都被消除。
现在,在反编译器中,我们只剩下六条指令(如果算上变量声明,则是八条):
线性视图也可用:
使用新反编译器API解决2000次切割
对于原始挑战,我们必须从数百个几乎相同的可执行文件中提取硬编码的栈保护值。使用LLIL API我能够轻松完成,但需要跟踪一些值,操作起来不太方便。
让我们用HLIL API再试一次:
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
|
#!/usr/bin/env python3
import sys
import binaryninja
# 四年前我们必须指定加载器格式并等待分析完成
target = sys.argv[1]
bv = binaryninja.BinaryViewType.get_view_of_file(target, update_analysis=True)
# start是我们的入口点,有两个调用,第二个调用是main
# 获取start与之前没有太大不同
start = bv.get_function_at(bv.entry_point)
start_blocks = list(start.high_level_il) # start只有一个块
start_calls = [x for x in start_blocks[0] if x.operation == binaryninja.HighLevelILOperation.HLIL_CALL]
call_main = start_calls[1] # 第二个调用是main
# Main有一个对我们处理函数的调用
main = bv.get_function_at(call_main.dest.constant)
main_blocks = list(main.high_level_il) # main只有一个块
main_calls = [x for x in main_blocks[0] if x.operation == binaryninja.HighLevelILOperation.HLIL_CALL]
call_handler = main_calls[0] # 第一个调用是handler
# 这里才是真正改进的地方
# Handler包含我们的cookie,它在第一个块的最后一个调用中与memcmp比较,
# 但我们的调用被折叠到if条件中
handler = bv.get_function_at(call_handler.dest.constant)
handler_blocks = list(handler.high_level_il)
# 从第一个块中提取所有HLIL_IF指令,应该只有一个
if_insn = [x for x in handler_blocks[0] if x.operation == binaryninja.HighLevelILOperation.HLIL_IF]
# 对memcmp的调用是条件的左侧,右侧是'0':
# if(memcmp(buf, "cookie", 4) == 0)
call_memcmp = if_insn[0].condition.left
# 现在从memcmp调用中提取cookie的数据指针
# arg0是我们的输入缓冲区
# arg1是cookie数据指针
# arg2是比较的大小
cookie_ptr = call_memcmp.params[1].constant
# 读取前4字节获取cookie值,我们也可以使用memcmp的计数 here
cookie = bv.read(cookie_ptr, 4)
print(f"Cookie: {cookie}")
|
运行脚本产生期望的结果:我们快速处理了1000次切割
结论
HLIL解决方案比我们原来的方案更简洁,也更容易编写。总的来说,我很喜欢Binary Ninja的新反编译器,并且非常兴奋地将其应用于其他反编译器不支持的架构,如MIPS、68k和6502。希望这是反编译器战争的十年!
如果您想了解更多关于Binary Ninja的信息,请查看我们的《使用Binary Ninja进行自动化逆向工程》培训。
脚注
如需编写与IL交互的Binary Ninja插件,请查看我的bnil-graph插件,该插件最近更新以支持HLIL。