Binary Bomb,二进制炸弹。
偶然在网上看到这么一个玩意,逆向工程,一共有6关,每一关需要玩家输入一个字符串,如成功则进入下一关,失败则exit。
因为以前也学习过crack方面的东西,所以非常有亲切感,就下下来玩一玩。
没想到太久没搞,花了一个通宵才把6关通过。久违的兴奋感。。
这篇文章就不给出答案了,只是给一个足够任何人动手找到答案的指导。
通过这个逆向工程游戏,我们主要可以学到:
1. 使用objdump,gdb等工具进行程序分析的能力。
2. 了解编译器是如何调用函数,如何传入参数,保存返回值,如何实现高级语言中的循环等等,加深对堆栈的理解。
下面开始简略的分析。
当然要先分析main函数了。
8048a68: call 80490f0 <read_line> ;从read_line字面就可以看出,读取用户出入的字符串,把地址放入$eax中
8048a6d: mov %eax,(%esp) ;把字符串地址压栈,作为参数传给函数phase_1
8048a70: call 8048b1c <phase_1> ;第一段
分析main函数的反汇编代码可以看出来基本上每一关在main中都是上面3个语句。
然后就可以进入phase_1进行分析了。
08048b1c <phase_1>:
8048b1c: 55 push %ebp
8048b1d: 89 e5 mov %esp,%ebp
8048b1f: 83 ec 10 sub $0x10,%esp
8048b22: 68 78 96 04 08 push $0x8049678 ;传给strings_not_equal的第二个参数
8048b27: ff 75 08 pushl 0x8(%ebp) ;传给strings_not_equal的第一个参数
8048b2a: e8 00 04 00 00 call 8048f2f <strings_not_equal> ;函数声明应该是int strings_not_equal(char *s1, char *s2),相等则返回0
8048b2f: 83 c4 10 add $0x10,%esp
8048b32: 85 c0 test %eax,%eax ;$eax中保存了strings_not_euqal的返回结果,测试是否为0
8048b34: 74 05 je 8048b3b <phase_1+0x1f> ;如果相等则跳转到最后,成功返回
8048b36: e8 b1 08 00 00 call 80493ec <explode_bomb> ; 否则调用 explode_bomb,从名字就可以看出来 炸弹爆炸了。
8048b3b: c9 leave
8048b3c: c3 ret
注释解释的很清楚了。我们需要知道两点:
1:通常函数返回结果保存在$eax寄存器中。
2:进入函数保存并设置完ebp后,$ebp+4保存了返回地址,$ebp+8保存第一个参数,$ebp+12保存第二个参数,以此类推。
phase_1做的事情就是把用户输入的字符串和位于内存0x8049678起始的一个字符串进行比较,如果相同则通关。
所以我们可以用objdump查看0x8049678处的ascii值,可以用命令objdump --start-address=0x8049678 -s bomb | more 查看。
这个字符串就是第一关的答案。
phase_2:
08048b3d <phase_2>:
8048b3d: 55 push %ebp
8048b3e: 89 e5 mov %esp,%ebp
8048b40: 56 push %esi
8048b41: 53 push %ebx
8048b42: 83 ec 28 sub $0x28,%esp ;分配栈空间
8048b45: 8d 45 e0 lea -0x20(%ebp),%eax ;这个地址属于phase_2的栈空间里
8048b48: 50 push %eax ;read_six_numbers的第二个参数压栈,这是个地址,后面知道是用来保存6个数字用的
8048b49: ff 75 08 pushl 0x8(%ebp) ;把用户输入的字符串地址压栈,作为调用read_six_numbers第一个参数
8048b4c: e8 84 03 00 00 call 8048ed5 <read_six_numbers> ;调用read_six_numbers
8048b51: 83 c4 10 add $0x10,%esp
8048b54: 83 7d e0 01 cmpl $0x1,-0x20(%ebp) ;$ebp-0x20保存了第一个数字,$ebp-0x20+4保存了第二个数字,以此类推。
;这个cmpl语句检查$ebp-0x20中的int型整数是否等于1.
8048b58: 74 05 je 8048b5f <phase_2+0x22> ;如果等于1则继续
8048b5a: e8 8d 08 00 00 call 80493ec <explode_bomb> ;否则炸弹爆炸
8048b5f: bb 02 00 00 00 mov $0x2,%ebx ;$ebp初始化为2
8048b64: 8d 75 e0 lea -0x20(%ebp),%esi ;下面开始一段循环,这段循环用c伪代码解释比较清楚
8048b67: 89 d8 mov %ebx,%eax
8048b69: 0f af 44 9e f8 imul -0x8(%esi,%ebx,4),%eax
8048b6e: 3b 44 9e fc cmp -0x4(%esi,%ebx,4),%eax
8048b72: 74 05 je 8048b79 <phase_2+0x3c>
8048b74: e8 73 08 00 00 call 80493ec <explode_bomb>
8048b79: 43 inc %ebx
8048b7a: 83 fb 07 cmp $0x7,%ebx
8048b7d: 75 e8 jne 8048b67 <phase_2+0x2a>
8048b7f: 8d 65 f8 lea -0x8(%ebp),%esp
8048b82: 5b pop %ebx
8048b83: 5e pop %esi
8048b84: c9 leave
8048b85: c3 ret
所以我们输入的字符串必须是"num1 num2 num3 num4 num5 num6"。当然不是随便输入6个数字,在得到了6个数字后,要进行一段循环验证,用c伪代码解释一下:
int A[6];
if(A[0]!=1)
call explode_bomb
int ebx = 2;
int i=1;
do{
int eax = ebx;
eax *= A[i-1];
if(A[i]!= eax)
call explode_bomb
++i;
++ebx;
}
while(ebx!=7)
return success
也就是6个数分别有一个乘法因子为factor[] = {1,2,3,4,5,6}。
必须满足A[i] = A[i-1]*factor[i]; A[0] = 1。 这样很容易推导出6个数字。这关也就解决了。下面是read_six_numbers的分析。
08048ed5 <read_six_numbers>:
8048ed5: 55 push %ebp
8048ed6: 89 e5 mov %esp,%ebp
8048ed8: 83 ec 08 sub $0x8,%esp
8048edb: 8b 55 0c mov 0xc(%ebp),%edx ;提出传入的第二个参数,存放6个数字的首地址。
8048ede: 8d 42 14 lea 0x14(%edx),%eax
8048ee1: 50 push %eax ;压入第8个参数 存放第6个数字的地址
8048ee2: 8d 42 10 lea 0x10(%edx),%eax
8048ee5: 50 push %eax ;压入第7个参数 存放第5个数字的地址
8048ee6: 8d 42 0c lea 0xc(%edx),%eax
8048ee9: 50 push %eax ;压入第6个参数 存放第4个数字的地址
8048eea: 8d 42 08 lea 0x8(%edx),%eax
8048eed: 50 push %eax ;压入第5个参数 存放第3个数字的地址
8048eee: 8d 42 04 lea 0x4(%edx),%eax
8048ef1: 50 push %eax ;压入第4个参数 存放第2个数字的地址
8048ef2: 52 push %edx ;压入第3个参数(放置第一个数字的地址,见sscanf的声明)
8048ef3: 68 5c 99 04 08 push $0x804995c ;压入第二个参数:通过objdump可以看到是"%d %d %d %d %d %d"。
8048ef8: ff 75 08 pushl 0x8(%ebp) ;压入第一个参数:我们输入的字符串。
8048efb: e8 78 f9 ff ff call 8048878 <sscanf@plt> ;调用sscanf, int sscanf(const char *buffer,const char *format,[argument ]...);
8048f00: 83 c4 20 add $0x20,%esp
8048f03: 83 f8 05 cmp $0x5,%eax
8048f06: 7f 05 jg 8048f0d <read_six_numbers+0x38>
8048f08: e8 df 04 00 00 call 80493ec <explode_bomb>
8048f0d: c9 leave
8048f0e: c3 ret
phase_3:
注释已经解释的很清楚了。
要求输入的字符串格式是"num1 num2"。
08048b86 <phase_3>:
8048b86: 55 push %ebp
8048b87: 89 e5 mov %esp,%ebp
8048b89: 83 ec 18 sub $0x18,%esp
8048b8c: 8d 45 f8 lea -0x8(%ebp),%eax
8048b8f: 50 push %eax ;存放第二个数字地址 sscanf第四个参数
8048b90: 8d 45 fc lea -0x4(%ebp),%eax
8048b93: 50 push %eax ;存放第一个数字地址 sscanf的第三个参数
8048b94: 68 68 99 04 08 push $0x8049968 ;格式化字符串 sscanf 第二个参数 该地址的内容为"%d %d"
8048b99: ff 75 08 pushl 0x8(%ebp) ;输入的字符串 sscanf 第一个参数
8048b9c: e8 d7 fc ff ff call 8048878 <sscanf@plt> ;从输入的字符串中读取两个数字
8048ba1: 83 c4 10 add $0x10,%esp
8048ba4: 83 f8 01 cmp $0x1,%eax ;sscanf的返回结果,为成功读取的数字个数,如果<=1则肯定失败
8048ba7: 7f 05 jg 8048bae <phase_3+0x28>
8048ba9: e8 3e 08 00 00 call 80493ec <explode_bomb>
8048bae: 83 7d fc 07 cmpl $0x7,-0x4(%ebp)
8048bb2: 77 65 ja 8048c19 <phase_3+0x93> ;如果第一个数字>7,则跳转到后面的explode_bomb
8048bb4: 8b 45 fc mov -0x4(%ebp),%eax
8048bb7: ff 24 85 cc 96 04 08 jmp *0x80496cc(,%eax,4) ;这个间接跳转的意思是 跳转到 0x80496cc+4$eax 地址中的数字 所以用objdump查看0x80496cc中的内容,
;因为$eax也就是第一个数字要>1所以有一些可能的跳转,很大的可能是有几个开放的答案,这里我就
;选择了$eax=2来做,结果也是通过的。
8048bbe: b8 00 00 00 00 mov $0x0,%eax
8048bc3: eb 4d jmp 8048c12 <phase_3+0x8c>
8048bc5: b8 00 00 00 00 mov $0x0,%eax
8048bca: eb 41 jmp 8048c0d <phase_3+0x87>
8048bcc: b8 00 00 00 00 mov $0x0,%eax
8048bd1: eb 35 jmp 8048c08 <phase_3+0x82>
8048bd3: b8 00 00 00 00 mov $0x0,%eax
8048bd8: eb 29 jmp 8048c03 <phase_3+0x7d>
8048bda: b8 00 00 00 00 mov $0x0,%eax
8048bdf: eb 1d jmp 8048bfe <phase_3+0x78>
8048be1: b8 00 00 00 00 mov $0x0,%eax
8048be6: eb 11 jmp 8048bf9 <phase_3+0x73>
8048be8: b8 59 03 00 00 mov $0x359,%eax
8048bed: eb 05 jmp 8048bf4 <phase_3+0x6e>
8048bef: b8 00 00 00 00 mov $0x0,%eax
8048bf4: 2d df 01 00 00 sub $0x1df,%eax
8048bf9: 05 bd 02 00 00 add $0x2bd,%eax
8048bfe: 2d db 02 00 00 sub $0x2db,%eax
8048c03: 05 f2 00 00 00 add $0xf2,%eax
8048c08: 2d 86 00 00 00 sub $0x86,%eax
8048c0d: 05 86 00 00 00 add $0x86,%eax
8048c12: 2d 9b 01 00 00 sub $0x19b,%eax
8048c17: eb 0a jmp 8048c23 <phase_3+0x9d>
8048c19: e8 ce 07 00 00 call 80493ec <explode_bomb>
8048c1e: b8 00 00 00 00 mov $0x0,%eax
8048c23: 83 7d fc 05 cmpl $0x5,-0x4(%ebp) ;要求第一个数字<=5
8048c27: 7f 05 jg 8048c2e <phase_3+0xa8>
8048c29: 3b 45 f8 cmp -0x8(%ebp),%eax ;经过一系列对eax的加加减减,最后要求我们输入的第二个数字要和eax相等。
;我们根据之前选择的第一个数字,查看此时$eax的值作为第二个数字即可。
8048c2c: 74 05 je 8048c33 <phase_3+0xad>
8048c2e: e8 b9 07 00 00 call 80493ec <explode_bomb>
8048c33: c9 leave
8048c34: c3 ret
phase_4
这一关感觉比较简单,我们输入一个数字当做参数调用func4,如果返回值等于0x90(144)则通关。
只要分析一下func4函数即可。
这个func4函数其实就是计算斐波纳契数列的函数。
08048c71 <phase_4>:
8048c71: 55 push %ebp
8048c72: 89 e5 mov %esp,%ebp
8048c74: 83 ec 1c sub $0x1c,%esp
8048c77: 8d 45 fc lea -0x4(%ebp),%eax
8048c7a: 50 push %eax ;存放数字的地址
8048c7b: 68 6b 99 04 08 push $0x804996b ;参数压栈 格式化字符串 ("%d")
8048c80: ff 75 08 pushl 0x8(%ebp) ;参数压栈 输入的字符串
8048c83: e8 f0 fb ff ff call 8048878 <sscanf@plt> ;经过sscanf 把输入的数字转换成int存入$ebp-4地址中。
8048c88: 83 c4 10 add $0x10,%esp
8048c8b: 83 f8 01 cmp $0x1,%eax
8048c8e: 75 06 jne 8048c96 <phase_4+0x25>
8048c90: 83 7d fc 00 cmpl $0x0,-0x4(%ebp) ;如果该数字<=0 则失败
8048c94: 7f 05 jg 8048c9b <phase_4+0x2a>
8048c96: e8 51 07 00 00 call 80493ec <explode_bomb>
8048c9b: ff 75 fc pushl -0x4(%ebp) ;参数压栈
8048c9e: e8 92 ff ff ff call 8048c35 <func4> ;调用func4(int i)函数
8048ca3: 83 c4 04 add $0x4,%esp
8048ca6: 3d 90 00 00 00 cmp $0x90,%eax ;判断结果是否等于0x90
8048cab: 74 05 je 8048cb2 <phase_4+0x41>
8048cad: e8 3a 07 00 00 call 80493ec <explode_bomb>
8048cb2: c9 leave
8048cb3: c3 ret
08048c35 <func4>:
8048c35: 55 push %ebp
8048c36: 89 e5 mov %esp,%ebp
8048c38: 56 push %esi
8048c39: 53 push %ebx
8048c3a: 8b 5d 08 mov 0x8(%ebp),%ebx ;$ebx为传入的参数i
8048c3d: 83 fb 01 cmp $0x1,%ebx ;如果ebx<=1 则返回1
8048c40: 7f 07 jg 8048c49 <func4+0x14>
8048c42: be 00 00 00 00 mov $0x0,%esi
8048c47: eb 1e jmp 8048c67 <func4+0x32>
8048c49: be 00 00 00 00 mov $0x0,%esi
8048c4e: 83 ec 0c sub $0xc,%esp
8048c51: 8d 43 ff lea -0x1(%ebx),%eax
8048c54: 50 push %eax ;ebx-1压栈
8048c55: e8 db ff ff ff call 8048c35 <func4> ;调用func4(i-1)
8048c5a: 83 eb 02 sub $0x2,%ebx ;ebx = ebx-2
8048c5d: 01 c6 add %eax,%esi
8048c5f: 83 c4 10 add $0x10,%esp
8048c62: 83 fb 01 cmp $0x1,%ebx
8048c65: 7f e7 jg 8048c4e <func4+0x19> ;调回继续计算func4(ebx-2)
8048c67: 8d 46 01 lea 0x1(%esi),%eax
8048c6a: 8d 65 f8 lea -0x8(%ebp),%esp
8048c6d: 5b pop %ebx
8048c6e: 5e pop %esi
8048c6f: c9 leave
8048c70: c3 ret
func4(i) = func4(i-1)+func4(i-2)。
答案就是使得func4(i) = 144的i。
phase_5:
这一关需要输入长度为6的字符串。
08048cb4 <phase_5>:
8048cb4: 55 push %ebp
8048cb5: 89 e5 mov %esp,%ebp
8048cb7: 53 push %ebx
8048cb8: 83 ec 20 sub $0x20,%esp
8048cbb: 8b 5d 08 mov 0x8(%ebp),%ebx ;$ebx中存放输入的字符串首地址
8048cbe: 53 push %ebx ;参数压栈,字符串地址
8048cbf: e8 4b 02 00 00 call 8048f0f <string_length> ;计算字符串长度
8048cc4: 83 c4 10 add $0x10,%esp
8048cc7: 83 f8 06 cmp $0x6,%eax ;比较是否长度为6,不为6则explode_bomb
8048cca: 74 05 je 8048cd1 <phase_5+0x1d>
8048ccc: e8 1b 07 00 00 call 80493ec <explode_bomb>
8048cd1: ba 01 00 00 00 mov $0x1,%edx ;下面一段是循环,0x8048cd9 到 0x8048cef ;$edx是计数器从1到6,循环体执行6次
8048cd6: 8d 4d f5 lea -0xb(%ebp),%ecx
8048cd9: 0f be 44 13 ff movsbl -0x1(%ebx,%edx,1),%eax ;$eax赋值为 内存$ebx+$edx-1 中的内容 即输入的字符串某字节开始的4个字节(作为一个整型)
8048cde: 83 e0 0f and $0xf,%eax ;$eax只保留低4位,所以此时$eax是对应的输入字符串中某字符的低4位。
8048ce1: 8a 80 c0 a5 04 08 mov 0x804a5c0(%eax),%al ;把$al设置为0x804a5c0+$eax地址的那个字节,由此可见0x804a5c0地址是一个字符串的首地址,此时$eax取值范围;为0-15,因此0x804a5c0处应该存在一个长度为16的字符串,经过objdump查看可以发现正是如此。;这一语句实际上是以输入的字符串中每个字符的低4位为索引,找到0x804a5c0中对应的字符,将其存入al
8048ce7: 88 44 0a ff mov %al,-0x1(%edx,%ecx,1) ;此时把al存入最终栈空间 $ecx+$edx-1中,其中$ecx = $esp-0xb
8048ceb: 42 inc %edx ;计数的同时可以让输入字符串后移一个字节。
8048cec: 83 fa 07 cmp $0x7,%edx
8048cef: 75 e8 jne 8048cd9 <phase_5+0x25>
8048cf1: c6 45 fb 00 movb $0x0,-0x5(%ebp) ;将6个字符存入$ecx为首的地址后,需要将第7个字符设置为NULL作为结束符。;可以分析得到$ecx到$ebp-5之间正好是7个字节(6个字符+1个结束符)
8048cf5: 83 ec 08 sub $0x8,%esp
8048cf8: 68 c2 96 04 08 push $0x80496c2 ;参数2,比较字符串。
8048cfd: 51 push %ecx ;参数1 ,根据我们输入字符串为索引得到的字符串
8048cfe: e8 2c 02 00 00 call 8048f2f <strings_not_equal> ;比较是否相同,相同则通关。
8048d03: 83 c4 10 add $0x10,%esp
8048d06: 85 c0 test %eax,%eax
8048d08: 74 05 je 8048d0f <phase_5+0x5b>
8048d0a: e8 dd 06 00 00 call 80493ec <explode_bomb>
8048d0f: 8b 5d fc mov -0x4(%ebp),%ebx
8048d12: c9 leave
8048d13: c3 ret
通过分析发现,需要索引的字符串为0x804a5c0起始的16个字符,比如是char B[16];
若输入字符串为char A[6];
则最终用于比较的字符串char C[6]满足:C[i] = B[ A[i]&0xF ];
若C = "abcdef" , B="qberasdfzxcvbgty"
我们需要的索引则为 4,1,10,6,2,7。要求我们输入的6个字符低四位分别为 4,1,10,6,2,7.
我们可以查ascii表,看看哪些可以输入的字符其低四位满足以上要求即可。
phase_6:
文档中说第六关很有挑战,但是我发现这一关很简单,加上整个程序有没有使用过的fun7函数和几个其它函数,我怀疑这个程序是否不完整。
这一关要求输入一个数字,会用strtol转换成long int,放入ebx寄存器。
然后调用了fun6函数,这个函数并没有用到我们的输入,而且其内容很复杂,有很多跳转。但这跟我们通关没什么关系。
直接看最后的比较,比较$ebx和($eax)的值,那我们就可以直接查看($eax)中的数值,这个就是我们要输入的答案了。
所以我怀疑最后一关原来是否是这个代码。
08048d72 <phase_6>:
8048d72: 55 push %ebp
8048d73: 89 e5 mov %esp,%ebp
8048d75: 53 push %ebx
8048d76: 83 ec 04 sub $0x4,%esp
8048d79: 6a 00 push $0x0
8048d7b: 6a 0a push $0xa
8048d7d: 6a 00 push $0x0
8048d7f: ff 75 08 pushl 0x8(%ebp)
8048d82: e8 81 fa ff ff call 8048808 <__strtol_internal@plt> ;strtol函数是把字符串转换成long int。前面的参数压栈就不详述了。
8048d87: 89 c3 mov %eax,%ebx ;$ebx存放我们输入的字符串转换成long int后的值。
8048d89: 68 30 a6 04 08 push $0x804a630 ;fun6参数压栈
8048d8e: e8 81 ff ff ff call 8048d14 <fun6> ;fun6只有一个参数,跟我们输入的字符串没有任何关系。
8048d93: ba 08 00 00 00 mov $0x8,%edx
8048d98: 83 c4 14 add $0x14,%esp
8048d9b: 8b 40 08 mov 0x8(%eax),%eax
8048d9e: 4a dec %edx
8048d9f: 75 fa jne 8048d9b <phase_6+0x29>
8048da1: 3b 18 cmp (%eax),%ebx ;比较$ebx和($eax)的值,如果相同,就通关了!!!
8048da3: 74 05 je 8048daa <phase_6+0x38>
8048da5: e8 42 06 00 00 call 80493ec <explode_bomb>
8048daa: 8b 5d fc mov -0x4(%ebp),%ebx
8048dad: c9 leave
8048dae: c3 ret