难以理解反汇编二进制炸弹阶段 3 中的逻辑
Difficulty understanding logic in disassembled binary bomb phase 3
我有来自二进制炸弹实验室的以下汇编程序。目标是在不触发 explode_bomb
函数的情况下确定 运行 二进制文件所需的关键字。我评论了我对该程序的程序集分析,但我无法将所有内容拼凑在一起。
我相信我拥有我需要的所有信息,但我仍然无法看到实际的底层逻辑,因此我被卡住了。如果有任何帮助,我将不胜感激!
以下为反汇编程序本身:
0x08048c3c <+0>: push %edi
0x08048c3d <+1>: push %esi
0x08048c3e <+2>: sub [=10=]x14,%esp
0x08048c41 <+5>: movl [=10=]x804a388,(%esp)
0x08048c48 <+12>: call 0x80490ab <string_length>
0x08048c4d <+17>: add [=10=]x1,%eax
0x08048c50 <+20>: mov %eax,(%esp)
0x08048c53 <+23>: call 0x8048800 <malloc@plt>
0x08048c58 <+28>: mov [=10=]x804a388,%esi
0x08048c5d <+33>: mov [=10=]x13,%ecx
0x08048c62 <+38>: mov %eax,%edi
0x08048c64 <+40>: rep movsl %ds:(%esi),%es:(%edi)
0x08048c66 <+42>: movzwl (%esi),%edx
0x08048c69 <+45>: mov %dx,(%edi)
0x08048c6c <+48>: movzbl 0x11(%eax),%edx
0x08048c70 <+52>: mov %dl,0x10(%eax)
0x08048c73 <+55>: mov %eax,0x4(%esp)
0x08048c77 <+59>: mov 0x20(%esp),%eax
0x08048c7b <+63>: mov %eax,(%esp)
0x08048c7e <+66>: call 0x80490ca <strings_not_equal>
0x08048c83 <+71>: test %eax,%eax
0x08048c85 <+73>: je 0x8048c8c <phase_3+80>
0x08048c87 <+75>: call 0x8049363 <explode_bomb>
0x08048c8c <+80>: add [=10=]x14,%esp
0x08048c8f <+83>: pop %esi
0x08048c90 <+84>: pop %edi
0x08048c91 <+85>: ret
以下是我的分析
5 <phase_3>
6 0x08048c3c <+0>: push %edi // push value in edi to stack
7 0x08048c3d <+1>: push %esi // push value of esi to stack
8 0x08048c3e <+2>: sub [=11=]x14,%esp // grow stack by 0x14 (move stack ptr -0x14 bytes)
9
10 0x08048c41 <+5>: movl [=11=]x804a388,(%esp) // put 0x804a388 into loc esp points to
11
12 0x08048c48 <+12>: call 0x80490ab <string_length> // check string length, store in eax
13 0x08048c4d <+17>: add [=11=]x1,%eax // increment val in eax by 0x1 (str len + 1)
14 // at this point, eax = str_len + 1 = 77 + 1 = 78
15
16 0x08048c50 <+20>: mov %eax,(%esp) // get val in eax and put in loc on stack
17 //**** at this point, 0x804a388 should have a value of 78? ****
18
19 0x08048c53 <+23>: call 0x8048800 <malloc@plt> // malloc --> base ptr in eax
20
21 0x08048c58 <+28>: mov [=11=]x804a388,%esi // 0x804a388 in esi
22 0x08048c5d <+33>: mov [=11=]x13,%ecx // put 0x13 in ecx (counter register)
23 0x08048c62 <+38>: mov %eax,%edi // put val in eax into edi
24 0x08048c64 <+40>: rep movsl %ds:(%esi),%es:(%edi) // repeat 0x13 (19) times
25 // **** populate malloced memory with first 19 (edit: 76) chars of string at 0x804a388 (this string is 77 characters long)? ****
26
27 0x08048c66 <+42>: movzwl (%esi),%edx // put val in loc esi points to into edx
***** // at this point, edx should contain the string at 0x804a388?
28
29 0x08048c69 <+45>: mov %dx,(%edi) // put val in dx to loc edi points to
***** // not sure what effect this has or what is in edi at this point
30 0x08048c6c <+48>: movzbl 0x11(%eax),%edx // edx = [eax + 0x11]
31 0x08048c70 <+52>: mov %dl,0x10(%eax) // [eax + 0x10] = dl
32 0x08048c73 <+55>: mov %eax,0x4(%esp) // [esp + 0x4] = eax
33 0x08048c77 <+59>: mov 0x20(%esp),%eax // eax = [esp + 0x20]
34 0x08048c7b <+63>: mov %eax,(%esp) // put val in eax into loc esp points to
***** // not sure what effect these movs have
35
36 // edi --> first arg
37 // esi --> second arg
38 // compare value in esi to edi
39 0x08048c7e <+66>: call 0x80490ca <strings_not_equal> // store result in eax
40 0x08048c83 <+71>: test %eax,%eax
41 0x08048c85 <+73>: je 0x8048c8c <phase_3+80>
42 0x08048c87 <+75>: call 0x8049363 <explode_bomb>
43 0x08048c8c <+80>: add [=11=]x14,%esp
44 0x08048c8f <+83>: pop %esi
45 0x08048c90 <+84>: pop %edi
46 0x08048c91 <+85>: ret
更新:
在调用 strings_not_equal 之前检查寄存器后,我得到以下信息:
eax 0x804d8aa 134535338
ecx 0x0 0
edx 0x76 118
ebx 0xffffd354 -11436
esp 0xffffd280 0xffffd280
ebp 0xffffd2b8 0xffffd2b8
esi 0x804a3d4 134521812
edi 0x804f744 134543172
eip 0x8048c7b 0x8048c7b <phase_3+63>
eflags 0x282 [ SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
我使用 Hopper 得到以下反汇编伪代码:
我什至尝试使用在 eax 中找到的数字和之前看到的字符串作为我的关键字,但它们都不起作用。
rep movsl
将 32 位长字从地址 %esi
复制到地址 %edi
,每次都递增 4,次数等于 %ecx
。将其视为 memcpy(edi, esi, ecx*4)
.
参见 https://felixcloutier.com/x86/movs:movsb:movsw:movsd:movsq(在 Intel 符号中为 movsd)。
所以这是复制 19*4=76
字节。
该函数将静态存储中的字符串修改副本复制到 malloced 缓冲区中。
这看起来很奇怪。 malloc
大小取决于 strlen
+1,但 memcpy
大小是编译时常量?您的反编译显然表明 address 是一个字符串文字,所以看起来没问题。
可能错过优化是因为自定义 string_length()
函数可能只在另一个 .c
中定义(并且炸弹是在没有 link 时间优化的情况下编译的文件内联)。所以 size_t len = string_length("some string literal");
不是编译时常量,编译器发出对它的调用,而不是能够使用字符串的已知常量长度。
但他们可能在源代码中使用了 strcpy
,编译器将其内联为 rep movs
。由于它显然是从字符串文字复制的,因此长度是一个编译时常量,它可以优化 strcpy
通常必须完成的那部分工作。通常,如果您已经计算出长度,最好使用 memcpy
而不是让 strcpy
动态地再次计算它,但在这种情况下,它实际上帮助编译器为该部分制作了更好的代码,而不是如果他们再次将 string_length
的 return 值传递给 memcpy
,因为 string_length
无法内联和优化。
<+0>: push %edi // push value in edi to stack
<+1>: push %esi // push value of esi to stack
<+2>: sub [=10=]x14,%esp // grow stack by 0x14 (move stack ptr -0x14 bytes)
这样的评论是多余的;指令本身已经说明了这一点。这将保存两个调用保留寄存器,以便该函数可以在内部使用它们并在以后恢复它们。
您对 sub
的评论更好;是的,grow the stack 是这里更高层次的语义。此函数为局部变量保留了一些 space(以及函数参数以 mov
而不是 push
ed 存储)。
rep movsd
复制 0x13 * 4 个字节,递增 ESI 和 EDI 以指向复制区域的末尾。因此,另一个 movsd
指令将复制另外 4 个与前一个副本连续的字节。
该代码实际上复制了另外 2 个,但它没有使用 movsw
,而是使用 movzw
字加载和 mov
存储。 这样一共复制了 78 个字节。
...
# at this point EAX = malloc return value which I'll call buf
<+28>: mov [=11=]x804a388,%esi # copy src = a string literal in .rodata?
<+33>: mov [=11=]x13,%ecx
<+38>: mov %eax,%edi # copy dst = buf
<+40>: rep movsl %ds:(%esi),%es:(%edi) # memcpy 76 bytes and advance ESI, EDI
<+42>: movzwl (%esi),%edx
<+45>: mov %dx,(%edi) # copy another 2 bytes (not moving ESI or EDI)
# final effect: 78-byte memcpy
在某些(但不是全部)CPU 上,仅使用具有适当计数的 rep movsb
或 rep movsw
会很有效,但这不是编译器在这种情况下选择的。 movzx
又名 AT&T movz
是一种很好的方式来进行窄负载而不会受到部分寄存器的惩罚。这就是编译器这样做的原因,这样他们就可以写入一个完整的寄存器,即使他们只打算使用存储指令读取该寄存器的低 8 位或 16 位。
在将字符串文字复制到 buf 之后,我们有一个字节 load/store 复制一个带有 buf
的字符。 记住这一点 EAX仍然指向 buf
,malloc
return 值。 所以它正在制作字符串文字的修改副本。
<+48>: movzbl 0x11(%eax),%edx
<+52>: mov %dl,0x10(%eax) # buf[16] = buf[17]
也许如果源代码没有击败常量传播,具有足够高的优化级别,编译器可能只是将最终字符串放入 .rodata
您可以找到它的地方,从而使这个炸弹阶段变得微不足道。 :P
然后它将指针存储为字符串比较的堆栈参数。
<+55>: mov %eax,0x4(%esp) # 2nd arg slot = EAX = buf
<+59>: mov 0x20(%esp),%eax # function arg = user input?
<+63>: mov %eax,(%esp) # first arg slot = our incoming stack arg
<+66>: call 0x80490ca <strings_not_equal>
如何 "cheat":使用 GDB 查看 运行time 结果
一些炸弹实验室只允许您 运行 在线炸弹,在测试服务器上,它会记录爆炸。你不能在 GDB 下 运行 它,只能使用静态反汇编(如 objdump -drwC -Mintel
)。所以测试服务器可以记录你有多少次失败的尝试。例如就像我在 google 中发现的 CS 3330 at cs.virginia.edu,其中满分需要少于 20 次爆炸。
使用 GDB 在函数的一部分检查内存/寄存器使得这比仅从静态分析工作要容易得多,实际上简化了这个函数,其中只在最后检查单个输入。例如只需看看传递给 strings_not_equal
的其他 arg 是什么。 (特别是如果您使用 GDB 的 jump
或 set $pc = ...
命令跳过炸弹爆炸检查。)
在调用 strings_not_equal
之前设置断点或单步执行。 使用 p (char*)$eax
将 EAX 视为 char*
并向您显示从该地址开始的(以 0 结尾的)C 字符串。那时 EAX 保存缓冲区的地址,正如您从存储到堆栈所看到的那样。
Copy/paste 那个字符串结果,你就完成了。
具有多个数字输入的其他阶段通常不容易用调试器来解决,并且至少需要一些数学知识,但是 linked-list 阶段要求您在其中输入一系列数字如果您知道如何使用调试器设置寄存器以使比较成功,那么列表遍历的正确顺序也会变得微不足道。
我有来自二进制炸弹实验室的以下汇编程序。目标是在不触发 explode_bomb
函数的情况下确定 运行 二进制文件所需的关键字。我评论了我对该程序的程序集分析,但我无法将所有内容拼凑在一起。
我相信我拥有我需要的所有信息,但我仍然无法看到实际的底层逻辑,因此我被卡住了。如果有任何帮助,我将不胜感激!
以下为反汇编程序本身:
0x08048c3c <+0>: push %edi
0x08048c3d <+1>: push %esi
0x08048c3e <+2>: sub [=10=]x14,%esp
0x08048c41 <+5>: movl [=10=]x804a388,(%esp)
0x08048c48 <+12>: call 0x80490ab <string_length>
0x08048c4d <+17>: add [=10=]x1,%eax
0x08048c50 <+20>: mov %eax,(%esp)
0x08048c53 <+23>: call 0x8048800 <malloc@plt>
0x08048c58 <+28>: mov [=10=]x804a388,%esi
0x08048c5d <+33>: mov [=10=]x13,%ecx
0x08048c62 <+38>: mov %eax,%edi
0x08048c64 <+40>: rep movsl %ds:(%esi),%es:(%edi)
0x08048c66 <+42>: movzwl (%esi),%edx
0x08048c69 <+45>: mov %dx,(%edi)
0x08048c6c <+48>: movzbl 0x11(%eax),%edx
0x08048c70 <+52>: mov %dl,0x10(%eax)
0x08048c73 <+55>: mov %eax,0x4(%esp)
0x08048c77 <+59>: mov 0x20(%esp),%eax
0x08048c7b <+63>: mov %eax,(%esp)
0x08048c7e <+66>: call 0x80490ca <strings_not_equal>
0x08048c83 <+71>: test %eax,%eax
0x08048c85 <+73>: je 0x8048c8c <phase_3+80>
0x08048c87 <+75>: call 0x8049363 <explode_bomb>
0x08048c8c <+80>: add [=10=]x14,%esp
0x08048c8f <+83>: pop %esi
0x08048c90 <+84>: pop %edi
0x08048c91 <+85>: ret
以下是我的分析
5 <phase_3>
6 0x08048c3c <+0>: push %edi // push value in edi to stack
7 0x08048c3d <+1>: push %esi // push value of esi to stack
8 0x08048c3e <+2>: sub [=11=]x14,%esp // grow stack by 0x14 (move stack ptr -0x14 bytes)
9
10 0x08048c41 <+5>: movl [=11=]x804a388,(%esp) // put 0x804a388 into loc esp points to
11
12 0x08048c48 <+12>: call 0x80490ab <string_length> // check string length, store in eax
13 0x08048c4d <+17>: add [=11=]x1,%eax // increment val in eax by 0x1 (str len + 1)
14 // at this point, eax = str_len + 1 = 77 + 1 = 78
15
16 0x08048c50 <+20>: mov %eax,(%esp) // get val in eax and put in loc on stack
17 //**** at this point, 0x804a388 should have a value of 78? ****
18
19 0x08048c53 <+23>: call 0x8048800 <malloc@plt> // malloc --> base ptr in eax
20
21 0x08048c58 <+28>: mov [=11=]x804a388,%esi // 0x804a388 in esi
22 0x08048c5d <+33>: mov [=11=]x13,%ecx // put 0x13 in ecx (counter register)
23 0x08048c62 <+38>: mov %eax,%edi // put val in eax into edi
24 0x08048c64 <+40>: rep movsl %ds:(%esi),%es:(%edi) // repeat 0x13 (19) times
25 // **** populate malloced memory with first 19 (edit: 76) chars of string at 0x804a388 (this string is 77 characters long)? ****
26
27 0x08048c66 <+42>: movzwl (%esi),%edx // put val in loc esi points to into edx
***** // at this point, edx should contain the string at 0x804a388?
28
29 0x08048c69 <+45>: mov %dx,(%edi) // put val in dx to loc edi points to
***** // not sure what effect this has or what is in edi at this point
30 0x08048c6c <+48>: movzbl 0x11(%eax),%edx // edx = [eax + 0x11]
31 0x08048c70 <+52>: mov %dl,0x10(%eax) // [eax + 0x10] = dl
32 0x08048c73 <+55>: mov %eax,0x4(%esp) // [esp + 0x4] = eax
33 0x08048c77 <+59>: mov 0x20(%esp),%eax // eax = [esp + 0x20]
34 0x08048c7b <+63>: mov %eax,(%esp) // put val in eax into loc esp points to
***** // not sure what effect these movs have
35
36 // edi --> first arg
37 // esi --> second arg
38 // compare value in esi to edi
39 0x08048c7e <+66>: call 0x80490ca <strings_not_equal> // store result in eax
40 0x08048c83 <+71>: test %eax,%eax
41 0x08048c85 <+73>: je 0x8048c8c <phase_3+80>
42 0x08048c87 <+75>: call 0x8049363 <explode_bomb>
43 0x08048c8c <+80>: add [=11=]x14,%esp
44 0x08048c8f <+83>: pop %esi
45 0x08048c90 <+84>: pop %edi
46 0x08048c91 <+85>: ret
更新:
在调用 strings_not_equal 之前检查寄存器后,我得到以下信息:
eax 0x804d8aa 134535338
ecx 0x0 0
edx 0x76 118
ebx 0xffffd354 -11436
esp 0xffffd280 0xffffd280
ebp 0xffffd2b8 0xffffd2b8
esi 0x804a3d4 134521812
edi 0x804f744 134543172
eip 0x8048c7b 0x8048c7b <phase_3+63>
eflags 0x282 [ SF IF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
我使用 Hopper 得到以下反汇编伪代码:
我什至尝试使用在 eax 中找到的数字和之前看到的字符串作为我的关键字,但它们都不起作用。
rep movsl
将 32 位长字从地址 %esi
复制到地址 %edi
,每次都递增 4,次数等于 %ecx
。将其视为 memcpy(edi, esi, ecx*4)
.
参见 https://felixcloutier.com/x86/movs:movsb:movsw:movsd:movsq(在 Intel 符号中为 movsd)。
所以这是复制 19*4=76
字节。
该函数将静态存储中的字符串修改副本复制到 malloced 缓冲区中。
这看起来很奇怪。 malloc
大小取决于 strlen
+1,但 memcpy
大小是编译时常量?您的反编译显然表明 address 是一个字符串文字,所以看起来没问题。
可能错过优化是因为自定义 string_length()
函数可能只在另一个 .c
中定义(并且炸弹是在没有 link 时间优化的情况下编译的文件内联)。所以 size_t len = string_length("some string literal");
不是编译时常量,编译器发出对它的调用,而不是能够使用字符串的已知常量长度。
但他们可能在源代码中使用了 strcpy
,编译器将其内联为 rep movs
。由于它显然是从字符串文字复制的,因此长度是一个编译时常量,它可以优化 strcpy
通常必须完成的那部分工作。通常,如果您已经计算出长度,最好使用 memcpy
而不是让 strcpy
动态地再次计算它,但在这种情况下,它实际上帮助编译器为该部分制作了更好的代码,而不是如果他们再次将 string_length
的 return 值传递给 memcpy
,因为 string_length
无法内联和优化。
<+0>: push %edi // push value in edi to stack
<+1>: push %esi // push value of esi to stack
<+2>: sub [=10=]x14,%esp // grow stack by 0x14 (move stack ptr -0x14 bytes)
这样的评论是多余的;指令本身已经说明了这一点。这将保存两个调用保留寄存器,以便该函数可以在内部使用它们并在以后恢复它们。
您对 sub
的评论更好;是的,grow the stack 是这里更高层次的语义。此函数为局部变量保留了一些 space(以及函数参数以 mov
而不是 push
ed 存储)。
rep movsd
复制 0x13 * 4 个字节,递增 ESI 和 EDI 以指向复制区域的末尾。因此,另一个 movsd
指令将复制另外 4 个与前一个副本连续的字节。
该代码实际上复制了另外 2 个,但它没有使用 movsw
,而是使用 movzw
字加载和 mov
存储。 这样一共复制了 78 个字节。
...
# at this point EAX = malloc return value which I'll call buf
<+28>: mov [=11=]x804a388,%esi # copy src = a string literal in .rodata?
<+33>: mov [=11=]x13,%ecx
<+38>: mov %eax,%edi # copy dst = buf
<+40>: rep movsl %ds:(%esi),%es:(%edi) # memcpy 76 bytes and advance ESI, EDI
<+42>: movzwl (%esi),%edx
<+45>: mov %dx,(%edi) # copy another 2 bytes (not moving ESI or EDI)
# final effect: 78-byte memcpy
在某些(但不是全部)CPU 上,仅使用具有适当计数的 rep movsb
或 rep movsw
会很有效,但这不是编译器在这种情况下选择的。 movzx
又名 AT&T movz
是一种很好的方式来进行窄负载而不会受到部分寄存器的惩罚。这就是编译器这样做的原因,这样他们就可以写入一个完整的寄存器,即使他们只打算使用存储指令读取该寄存器的低 8 位或 16 位。
在将字符串文字复制到 buf 之后,我们有一个字节 load/store 复制一个带有 buf
的字符。 记住这一点 EAX仍然指向 buf
,malloc
return 值。 所以它正在制作字符串文字的修改副本。
<+48>: movzbl 0x11(%eax),%edx
<+52>: mov %dl,0x10(%eax) # buf[16] = buf[17]
也许如果源代码没有击败常量传播,具有足够高的优化级别,编译器可能只是将最终字符串放入 .rodata
您可以找到它的地方,从而使这个炸弹阶段变得微不足道。 :P
然后它将指针存储为字符串比较的堆栈参数。
<+55>: mov %eax,0x4(%esp) # 2nd arg slot = EAX = buf
<+59>: mov 0x20(%esp),%eax # function arg = user input?
<+63>: mov %eax,(%esp) # first arg slot = our incoming stack arg
<+66>: call 0x80490ca <strings_not_equal>
如何 "cheat":使用 GDB 查看 运行time 结果
一些炸弹实验室只允许您 运行 在线炸弹,在测试服务器上,它会记录爆炸。你不能在 GDB 下 运行 它,只能使用静态反汇编(如 objdump -drwC -Mintel
)。所以测试服务器可以记录你有多少次失败的尝试。例如就像我在 google 中发现的 CS 3330 at cs.virginia.edu,其中满分需要少于 20 次爆炸。
使用 GDB 在函数的一部分检查内存/寄存器使得这比仅从静态分析工作要容易得多,实际上简化了这个函数,其中只在最后检查单个输入。例如只需看看传递给 strings_not_equal
的其他 arg 是什么。 (特别是如果您使用 GDB 的 jump
或 set $pc = ...
命令跳过炸弹爆炸检查。)
在调用 strings_not_equal
之前设置断点或单步执行。 使用 p (char*)$eax
将 EAX 视为 char*
并向您显示从该地址开始的(以 0 结尾的)C 字符串。那时 EAX 保存缓冲区的地址,正如您从存储到堆栈所看到的那样。
Copy/paste 那个字符串结果,你就完成了。
具有多个数字输入的其他阶段通常不容易用调试器来解决,并且至少需要一些数学知识,但是 linked-list 阶段要求您在其中输入一系列数字如果您知道如何使用调试器设置寄存器以使比较成功,那么列表遍历的正确顺序也会变得微不足道。