在《Linux 上分析二进制文件的 10 种方法》中,我解释了如何使用 Linux 上丰富的原生工具集来分析二进制文件。但如果你想进一步探索你的二进制文件,你需要一个为二进制分析定制的工具。如果你是二进制分析的新手,并且大多使用的是脚本语言,这篇文章《GNU binutils 里的九种武器》可以帮助你开始学习编译过程和什么是二进制。
为什么我需要另一个工具?
如果现有的 Linux 原生工具也能做类似的事情,你自然会问为什么需要另一个工具。嗯,这和你用手机做闹钟、做笔记、做相机、听音乐、上网、偶尔打电话和接电话的原因是一样的。以前,使用单独的设备和工具处理这些功能 —— 比如拍照的实体相机,记笔记的小记事本,起床的床头闹钟等等。对用户来说,有一个设备来做多件(但相关的)事情是方便的。另外,杀手锏就是独立功能之间的互操作性。
同样,即使许多 Linux 工具都有特定的用途,但在一个工具中捆绑类似(和更好)的功能是非常有用的。这就是为什么我认为 Radare2 应该是你需要处理二进制文件时的首选工具。
$ r2 ./adder -- Sorry, radare2 has experienced an internal error. [0x004004b0]> [0x004004b0]> [0x004004b0]> aaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Check for vtables [x] Type matching analysis for all functions (aaft) [x] Propagate noreturn information [x] Use -AA or aaaa to perform additional experimental analysis. [0x004004b0]>
这意味着每次你选择一个二进制文件进行分析时,你必须在加载二进制文件后输入一个额外的命令 aaa。你可以绕过这一点,在命令后面跟上 -A 来调用 r2;这将告诉 r2 为你自动分析二进制:
1 2 3 4 5 6 7 8 9 10 11
$ r2 -A ./adder [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Check for vtables [x] Type matching analysis for all functions (aaft) [x] Propagate noreturn information [x] Use -AA or aaaa to perform additional experimental analysis. -- Already up-to-date. [0x004004b0]>
在 C 语言中,main 函数是一个程序开始执行的地方。理想情况下,其他函数都是从 main 函数调用的,在退出程序时,main 函数会向操作系统返回一个退出状态。这在源代码中是很明显的,然而,二进制程序呢?如何判断 adder 函数的调用位置呢?
你可以使用 axt 命令,后面加上函数名,看看 adder 函数是在哪里调用的;如下图所示,它是从 main 函数中调用的。这就是所谓的 交叉引用 。但什么调用 main 函数本身呢?从下面的 axt main 可以看出,它是由 entry0 调用的(关于 entry0 的学习我就不说了,留待读者练习)。
1 2 3 4 5 6 7
[0x004004b0]> axt sym.adder main 0x4005b9 [CALL] call sym.adder [0x004004b0]> [0x004004b0]> axt main entry0 0x4004d1 [DATA] mov rdi, main [0x004004b0]>
寻找定位
在处理文本文件时,你经常通过引用行号和行或列号在文件内移动;在二进制文件中,你需要使用地址。这些是以 0x 开头的十六进制数字,后面跟着一个地址。要找到你在二进制中的位置,运行 s 命令。要移动到不同的位置,使用 s 命令,后面跟上地址。
函数名就像标签一样,内部用地址表示。如果函数名在二进制中(未剥离的),可以使用函数名后面的 s 命令跳转到一个特定的函数地址。同样,如果你想跳转到二进制的开始,输入 s 0:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
[0x004004b0]> s 0x4004b0 [0x004004b0]> [0x004004b0]> s main [0x004005a5]> [0x004005a5]> s 0x4005a5 [0x004005a5]> [0x004005a5]> s sym.adder [0x00400596]> [0x00400596]> s 0x400596 [0x00400596]> [0x00400596]> s 0 [0x00000000]> [0x00000000]> s 0x0 [0x00000000]>
如果你使用的是编译后的二进制文件,则无法查看源代码。编译器将源代码转译成 CPU 可以理解和执行的机器语言指令;其结果就是二进制或可执行文件。然而,你可以查看汇编指令(的助记词)来理解程序正在做什么。例如,如果你想查看 main 函数在做什么,你可以使用 s main 寻找 main 函数的地址,然后运行 pdf 命令来查看反汇编的指令。
[0x004004b0]> s main [0x004005a5]> [0x004005a5]> s 0x4005a5 [0x004005a5]> [0x004005a5]> pdf ; DATA XREF from entry0 @ 0x4004d1 ┌ 55: int main (int argc, char **argv, char **envp); │ ; var int64_t var_8h @ rbp-0x8 │ ; var int64_t var_4h @ rbp-0x4 │ 0x004005a5 55 push rbp │ 0x004005a6 4889e5 mov rbp, rsp │ 0x004005a9 4883ec10 sub rsp, 0x10 │ 0x004005ad c745fc640000. mov dword [var_4h], 0x64 ; 'd' ; 100 │ 0x004005b4 8b45fc mov eax, dword [var_4h] │ 0x004005b7 89c7 mov edi, eax │ 0x004005b9 e8d8ffffff call sym.adder │ 0x004005be 8945f8 mov dword [var_8h], eax │ 0x004005c1 8b45f8 mov eax, dword [var_8h] │ 0x004005c4 89c6 mov esi, eax │ 0x004005c6 bf78064000 mov edi, str.Number_now_is__:__d ; 0x400678 ; "Number now is : %d\n" ; const char *format │ 0x004005cb b800000000 mov eax, 0 │ 0x004005d0 e8cbfeffff call sym.imp.printf ; int printf(const char *format) │ 0x004005d5 b800000000 mov eax, 0 │ 0x004005da c9 leave └ 0x004005db c3 ret [0x004005a5]>
这是 adder 函数的反汇编结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
[0x004005a5]> s sym.adder [0x00400596]> [0x00400596]> s 0x400596 [0x00400596]> [0x00400596]> pdf ; CALL XREF from main @ 0x4005b9 ┌ 15: sym.adder (int64_t arg1); │ ; var int64_t var_4h @ rbp-0x4 │ ; arg int64_t arg1 @ rdi │ 0x00400596 55 push rbp │ 0x00400597 4889e5 mov rbp, rsp │ 0x0040059a 897dfc mov dword [var_4h], edi ; arg1 │ 0x0040059d 8b45fc mov eax, dword [var_4h] │ 0x004005a0 83c001 add eax, 1 │ 0x004005a3 5d pop rbp └ 0x004005a4 c3 ret [0x00400596]>
字符串
查看二进制中存在哪些字符串可以作为二进制分析的起点。字符串是硬编码到二进制中的,通常会提供重要的提示,可以让你将重点转移到分析某些区域。在二进制中运行 iz 命令来列出所有的字符串。这个测试二进制中只有一个硬编码的字符串:
1 2 3 4 5 6 7 8
[0x004004b0]> iz [Strings] nth paddr vaddr len size section type string ――――――――――――――――――――――――――――――――――――――――――――――――――――――― 0 0x00000678 0x00400678 20 21 .rodata ascii Number now is : %d\n
[0x004004b0]>
交叉引用字符串
和函数一样,你可以交叉引用字符串,看看它们是从哪里被打印出来的,并理解它们周围的代码:
1 2 3 4 5 6 7 8
[0x004004b0]> ps @ 0x400678 Number now is : %d
[0x004004b0]> [0x004004b0]> axt 0x400678 main 0x4005c6 [DATA] mov edi, str.Number_now_is__:__d [0x004004b0]>
$ r2 -A ./adder [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze function calls (aac) [x] Analyze len bytes of instructions for references (aar) [x] Check for vtables [x] Type matching analysis for all functions (aaft) [x] Propagate noreturn information [x] Use -AA or aaaa to perform additional experimental analysis. -- What do you want to debug today? [0x004004b0]> [0x004004b0]> s sym.adder [0x00400596]> [0x00400596]> s 0x400596 [0x00400596]> [0x00400596]> pdda ; assembly | /* r2dec pseudo code output */ | /* ./adder @ 0x400596 */ | #include <stdint.h> | ; (fcn) sym.adder () | int32_t adder (int64_t arg1) { | int64_t var_4h; | rdi = arg1; 0x00400596 push rbp | 0x00400597 mov rbp, rsp | 0x0040059a mov dword [rbp - 4], edi | *((rbp - 4)) = edi; 0x0040059d mov eax, dword [rbp - 4] | eax = *((rbp - 4)); 0x004005a0 add eax, 1 | eax++; 0x004005a3 pop rbp | 0x004005a4 ret | return eax; | } [0x00400596]>
配置设置
随着你对 Radare2 的使用越来越熟悉,你会想改变它的配置,以适应你的工作方式。你可以使用 e 命令查看 r2 的默认配置。要设置一个特定的配置,在 e 命令后面添加 config = value:
1 2 3 4 5 6 7 8 9 10 11
[0x004005a5]> e | wc -l 593 [0x004005a5]> e | grep syntax asm.syntax = intel [0x004005a5]> [0x004005a5]> e asm.syntax = att [0x004005a5]> [0x004005a5]> e | grep syntax asm.syntax = att [0x004005a5]>