几乎是大学上到现在最难的一次实验了吧,发出来供大家参考。
注意每个人拿到的bomb文件都不一样,只能参考一下思路哦...相信我写的很详细了,大家应该都能在其中获得一些新思路吧
BOMBLAB
计科23xx xxx 2023xxxxxxxx
【实验目的】
通过反汇编和调试可执行程序来理解程序的机器级表示。
【实验环境】
远程连接到linux服务器下进行试验:已安装gcc,gdb等基础工具
内核版本和系统架构:
Linux slave2 3.10.0-1160.102.1.el7.x86_64
CPU信息:Intel(R) Xeon(R) Bronze 3206R CPU @ 1.90GHz
【实验过程】
ssh连接到服务器,进入linux环境:
拿到文件夹后,里面的内容:
Bomb是可执行文件,我们先看一下bomb.c
发现关键的函数都被隐藏了,主函数大致的意思是:有六个关卡,每个关卡要输入一个特定的东西才能解除bomb,否则就会爆炸。
最后,提示了something missing?可能存在隐藏关卡!
执行一下bomb文件:
确实是这样,我们要输入六个phases以拆除炸弹。
反汇编bomb文件得到汇编代码,把它重定向到1.txt,易于分析汇编代码。
开始分析汇编代码:
Phase_1:函数调用,简单的字符串比较
8048b33: 83 ec 14 sub $0x14,%esp
8048b36: 68 bc 9f 04 08 push $0x8049fbc
8048b3b: ff 74 24 1c pushl 0x1c(%esp)
8048b3f: e8 84 04 00 00 call 8048fc8 <strings_not_equal>
string1是输入的字符串,string2是内存中的字符串,gdb查看内存中的字符串内容即可。
答案为I am not part of the problem. I am a Republican.
*调用stringnotequal
08048fc8 <strings_not_equal>:
8048fc8: 57 push %edi
8048fc9: 56 push %esi
8048fca: 53 push %ebx
8048fcb: 8b 5c 24 10 mov 0x10(%esp),%ebx
8048fcf: 8b 74 24 14 mov 0x14(%esp),%esi
mov 0x10(%esp),%ebx 和 mov 0x14(%esp),%esi 分别将第一个和第二个字符串指针加载到ebx和esi寄存器中。
调用string_length函数两次,分别计算两个字符串的长度,结果存入edi(第一个字符串长度)和eax(第二个字符串长度)。比较两长度,若不等则直接返回1(字符串不等)。后面再逐字比较。
Res:返回1:两字符串长度不同,或长度相同但内容不同。
返回0:两字符串完全一致(长度和内容均相同)。
8048b44: 83 c4 10 add $0x10,%esp
8048b47: 85 c0 test %eax,%eax
//检测eax中(函数的返回值)是否为0,若为0则设置ZF=1
8048b49: 74 05 je 8048b50 <phase_1+0x1d>
//ZF=1,跳转,不执行explode_bomb,否则引爆。
8048b4b: e8 6f 05 00 00 call 80490bf <explode_bomb>
8048b50: 83 c4 0c add $0xc,%esp
8048b53: c3 ret
08048b54 <phase_2>:数组+循环结构
8048b54: 56 push %esi
8048b55: 53 push %ebx
8048b56: 83 ec 2c sub $0x2c,%esp
Esi和ebx入栈,esp-2c 下移,为存储临时变量腾出空间。
8048b59: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048b5f: 89 44 24 24 mov %eax,0x24(%esp)
8048b63: 31 c0 xor %eax,%eax 清零eax,防止对后面有影响。
一种保护机制,从线程局部存储(TLS)中读取一个随机值(称为 "Canary" 或 "金丝雀值")。
函数返回前:检查栈中的 Canary 值是否被篡改,若被篡改则触发崩溃(防止攻击者利用栈溢出)。
8048b65: 8d 44 24 0c lea 0xc(%esp),%eax //eax=esp-28
8048b69: 50 push %eax //esp-28=0xbffff1a4入栈。
8048b6a: ff 74 24 3c pushl 0x3c(%esp)//esp+4中的值入栈
8048b6e: e8 71 05 00 00 call 80490e4 <read_six_numbers>
//函数调用过程见后面两页
//函数返回,esp恢复为esp-3c=0xffff190 函数中的操作让esp-14到esp-28存入了数组的六个int。
add:esp-28 esp-24 esp-20 esp-1c esp-18 esp-14
a[0] | a[1] | a[2] | a[3] | a[4] | a[5] |
8048b73: 83 c4 10 add $0x10,%esp
//esp-3c+10=esp-2c 更新esp的值,为后面访问数组值准备。
8048b76: 83 7c 24 04 00 cmpl $0x0,0x4(%esp)
//比较esp-2c+4=esp-28即a[0]与0的大小
8048b7b: 75 07 jne 8048b84 <phase_2+0x30>
//不一样则跳转到爆炸函数,所以a[0]=0
8048b7d: 83 7c 24 08 01 cmpl $0x1,0x8(%esp)
8048b82: 74 05 je 8048b89 <phase_2+0x35>
//比较esp-24即a[1]和1,一样则跳过爆炸函数,所以a[1]=1
8048b84: e8 36 05 00 00 call 80490bf <explode_bomb>
8048b89: 8d 5c 24 04 lea 0x4(%esp),%ebx
8048b8d: 8d 74 24 14 lea 0x14(%esp),%esi
//设置ebx的值为esp-28,esi的值为esp-18
8048b91: 8b 43 04 mov 0x4(%ebx),%eax
8048b94: 03 03 add (%ebx),%eax
//将ebx的值加4移到eax,并将原来的ebx内存中对应的数和现在内存中对应的数相加。相当于a[0]+a[1]
8048b96: 39 43 08 cmp %eax,0x8(%ebx)
//比较a[0]+a[1]的值和a[2]的值,不相等则爆炸。
8048b99: 74 05 je 8048ba0 <phase_2+0x4c>
8048b9b: e8 1f 05 00 00 call 80490bf <explode_bomb>
8048ba0: 83 c3 04 add $0x4,%ebx
8048ba3: 39 f3 cmp %esi,%ebx
8048ba5: 75 ea jne 8048b91 <phase_2+0x3d>
//直到ebx=esi才停止循环,
相当于对整个数组进行a[i]+a[i+1]=a[i+2]的操作(i=0-3)
对应的c伪代码大致如下:
最终的结果为0 1 1 2 3 5,(其实更多的数也可以,只需要前面六个是011235即可)
8048ba7: 8b 44 24 1c mov 0x1c(%esp),%eax
8048bab: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048bb2: 74 05 je 8048bb9 <phase_2+0x65>
8048bb4: e8 d7 fb ff ff call 8048790 <__stack_chk_fail@plt>
Canary值的校验,如果存储的值与gs+14中的值不等,说明发生了缓冲区溢出,程序终止。
8048bb9: 83 c4 24 add $0x24,%esp
8048bbc: 5b pop %ebx
8048bbd: 5e pop %esi
8048bbe: c3 ret
上面四句是函数返回操作。
调用的函数
080490e4 <read_six_numbers>:
80490e4: 83 ec 0c sub $0xc,%esp
80490e7: 8b 44 24 14 mov 0x14(%esp),%eax
80490eb: 8d 50 14 lea 0x14(%eax),%edx
//此时eax的值为esp-38中放的值,也就是esp-28,加上14变为esp-14,移到edx内,再压栈
80490ee: 52 push %edx
80490ef: 8d 50 10 lea 0x10(%eax),%edx
80490f2: 52 push %edx
80490f3: 8d 50 0c lea 0xc(%eax),%edx
80490f6: 52 push %edx
80490f7: 8d 50 08 lea 0x8(%eax),%edx
80490fa: 52 push %edx
80490fb: 8d 50 04 lea 0x4(%eax),%edx
80490fe: 52 push %edx
80490ff: 50 push %eax
在栈空间内,分配数组中六个数字的地址。随后用scanf将六个数读入相对应的内存(较高地址的栈)中。
8049100: 68 53 a1 04 08 push $0x804a153
8049105: ff 74 24 2c pushl 0x2c(%esp)
而esp-64+2c=esp+38 其中存储了esp-28的值,也就是数组的首地址
8049109: e8 02 f7 ff ff call 8048810 <__isoc99_sscanf@plt>
//调用sscanf 两个参数分别为数组首地址和 %d,%d,%d,%d,%d,%d
804910e: 83 c4 20 add $0x20,%esp
8049111: 83 f8 05 cmp $0x5,%eax
//如果输入的数字个数小于等于5 则引爆炸弹。
8049114: 7f 05 jg 804911b <read_six_numbers+0x37>
8049116: e8 a4 ff ff ff call 80490bf <explode_bomb>
804911b: 83 c4 0c add $0xc,%esp
804911e: c3 ret
08048bbf <phase_3>:条件分支,跳转表
8048bbf: 83 ec 1c sub $0x1c,%esp //esp-1c腾出空间给临时变量
8048bc2: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048bc8: 89 44 24 0c mov %eax,0xc(%esp)
8048bcc: 31 c0 xor %eax,%eax
//Canary 值初始化
8048bce: 8d 44 24 08 lea 0x8(%esp),%eax
8048bd2: 50 push %eax
8048bd3: 8d 44 24 08 lea 0x8(%esp),%eax
8048bd7: 50 push %eax
8048bd8: 68 5f a1 04 08 push $0x804a15f
8048bdd: ff 74 24 2c pushl 0x2c(%esp)
8048be1: e8 2a fc ff ff call 8048810 <__isoc99_sscanf@plt>
//读取两个数。栈空间如图所示:
esp+8中存储的是0x0804c480,即为这两个输入数字存储的地方,我们可以进行gdb调试,断点设置在*8048be6处,输入2,3后查看对应的内存空间可以发现
在内存中,2,3以int存储在esp-18和esp-14的地址空间中。如图:
8048be6: 83 c4 10 add $0x10,%esp
8048be9: 83 f8 01 cmp $0x1,%eax
8048bec: 7f 05 jg 8048bf3 <phase_3+0x34>
//对scanf的返回值和1作比较,若输入的个数小于等于1,则爆炸。
8048bee: e8 cc 04 00 00 call 80490bf <explode_bomb>
8048bf3: 83 7c 24 04 07 cmpl $0x7,0x4(%esp)
8048bf8: 77 3c ja 8048c36 <phase_3+0x77>
8048bfa: 8b 44 24 04 mov 0x4(%esp),%eax
//将7与a[0]作比较,(无符号数)若a[0]>7则爆炸,在有符号数中
:>7或<0则爆炸。
8048bfe: ff 24 85 20 a0 04 08 jmp *0x804a020(,%eax,4)
初始化跳转表,跳到0x804a020+4*a[0]的位置 通过gdb查看对应的内存可得:
跳转表如下:
a[0]的值 | 跳转的地址 | 跳转执行的操作 |
0 | 0x08048c42 | 比较a[1]与0x329(809) |
1 | 0x08048c50 | 比较a[1]与0x27f(639) |
2 | 0x08048c0c | 比较a[1]与0x336(822) |
3 | 0x08048c13 | 比较a[1]与0x173(371) |
4 | 0x08048c1a | 比较a[1]与0x2f2(754) |
5 | 0x08048c21 | 比较a[1]与0x275(629) |
6 | 0x08048c28 | 比较a[1]与0x7a(122) |
7 | 0x08048c2f | 比较a[1]与0x370(880) |
>7或<0 | 0x08048c36 | Bomb! |
以下均为跳转表的具体实现部分:
8048c05: b8 7f 02 00 00 mov $0x27f,%eax
8048c0a: eb 3b jmp 8048c47 <phase_3+0x88>
8048c0c: b8 36 03 00 00 mov $0x336,%eax
8048c11: eb 34 jmp 8048c47 <phase_3+0x88>
8048c13: b8 73 01 00 00 mov $0x173,%eax
8048c18: eb 2d jmp 8048c47 <phase_3+0x88>
8048c1a: b8 f2 02 00 00 mov $0x2f2,%eax
8048c1f: eb 26 jmp 8048c47 <phase_3+0x88>
8048c21: b8 75 02 00 00 mov $0x275,%eax
8048c26: eb 1f jmp 8048c47 <phase_3+0x88>
8048c28: b8 7a 00 00 00 mov $0x7a,%eax
8048c2d: eb 18 jmp 8048c47 <phase_3+0x88>
8048c2f: b8 70 03 00 00 mov $0x370,%eax
8048c34: eb 11 jmp 8048c47 <phase_3+0x88>
8048c36: e8 84 04 00 00 call 80490bf <explode_bomb>
8048c3b: b8 00 00 00 00 mov $0x0,%eax
8048c40: eb 05 jmp 8048c47 <phase_3+0x88>
8048c42: b8 29 03 00 00 mov $0x329,%eax
8048c47: 3b 44 24 08 cmp 0x8(%esp),%eax
8048c4b: 74 05 je 8048c52 <phase_3+0x93>
8048c4d: e8 6d 04 00 00 call 80490bf <explode_bomb>
由跳转表,可知答案如下:(不唯一)
8048c52: 8b 44 24 0c mov 0xc(%esp),%eax
8048c56: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048c5d: 74 05 je 8048c64 <phase_3+0xa5>
8048c5f: e8 2c fb ff ff call 8048790 <__stack_chk_fail@plt>
//Canary 值校验,不满足则退出函数。
8048c64: 83 c4 1c add $0x1c,%esp
8048c67: c3 ret
//函数返回
08048cab <phase_4>:递归
8048cab: 83 ec 1c sub $0x1c,%esp
8048cae: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048cb4: 89 44 24 0c mov %eax,0xc(%esp)
8048cb8: 31 c0 xor %eax,%eax
8048cba: 8d 44 24 04 lea 0x4(%esp),%eax
8048cbe: 50 push %eax
8048cbf: 8d 44 24 0c lea 0xc(%esp),%eax
8048cc3: 50 push %eax
8048cc4: 68 5f a1 04 08 push $0x804a15f
8048cc9: ff 74 24 2c pushl 0x2c(%esp)
8048ccd: e8 3e fb ff ff call 8048810 <__isoc99_sscanf@plt>
前面和上一问一致,都是输入两个数。(顺序交换)
8048cd2: 83 c4 10 add $0x10,%esp
8048cd5: 83 f8 02 cmp $0x2,%eax
8048cd8: 75 0c jne 8048ce6 <phase_4+0x3b>
//输入的不是两个数 则爆炸
8048cda: 8b 44 24 04 mov 0x4(%esp),%eax
8048cde: 83 e8 02 sub $0x2,%eax
8048ce1: 83 f8 02 cmp $0x2,%eax
//2<=a[1]<=4 否则爆炸。
8048ce4: 76 05 jbe 8048ceb <phase_4+0x40>
8048ce6: e8 d4 03 00 00 call 80490bf <explode_bomb>
8048ceb: 83 ec 08 sub $0x8,%esp
8048cee: ff 74 24 0c pushl 0xc(%esp)
8048cf2: 6a 08 push $0x8
//原来放scanf参数的地方压入a[1]和立即数8,作为func4的两个参数。调用func4.
8048cf4: e8 6f ff ff ff call 8048c68 <func4>
//调用func4过程见后面
8048cf9: 83 c4 10 add $0x10,%esp
8048cfc: 3b 44 24 08 cmp 0x8(%esp),%eax
//func4(8,a[1]),return的值和a[0]相等即结束,否则爆炸。
//这边我们先用取巧的方法:用gdb调试 输入a[1]=2,a[0]随便输入一个。在函数前后分别打两个断点,我们观察一下func4运行结束后eax中的值。
用c指令跳过函数func4内部。
查看寄存器:
Eax=108,说明返回值为108,有一组正确答案为108 2.
a[1]=3时,类似方法 eax=162
a[1]=4时,类似方法 eax=216
所有的答案:
2 108
3 162
4 216
8048d00: 74 05 je 8048d07 <phase_4+0x5c>
8048d02: e8 b8 03 00 00 call 80490bf <explode_bomb>、
8048d07: 8b 44 24 0c mov 0xc(%esp),%eax
8048d0b: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048d12: 74 05 je 8048d19 <phase_4+0x6e>
8048d14: e8 77 fa ff ff call 8048790 <__stack_chk_fail@plt>
8048d19: 83 c4 1c add $0x1c,%esp
8048d1c: c3 ret
校验canary值和返回
Func4分析
08048c68 <func4>:
原先的栈结构,调用的参数分别为8,a[1]
函数的流程图 递归调用的大致过程
函数递归调用,栈不断向下增长。函数返回,依次弹栈,直到返回入口处的函数。 |
当val<=0 返回0;val=1,返回n
Else:return func(val-1,n)+func(val-2,n)+n
8048c68: 57 push %edi
8048c69: 56 push %esi
8048c6a: 53 push %ebx
8048c6b: 8b 5c 24 10 mov 0x10(%esp),%ebx
8048c6f: 8b 7c 24 14 mov 0x14(%esp),%edi
8048c73: 85 db test %ebx,%ebx
8048c75: 7e 2b jle 8048ca2 <func4+0x3a>
8048c77: 89 f8 mov %edi,%eax
8048c79: 83 fb 01 cmp $0x1,%ebx
8048c7c: 74 29 je 8048ca7 <func4+0x3f>
8048c7e: 83 ec 08 sub $0x8,%esp
8048c81: 57 push %edi
8048c82: 8d 43 ff lea -0x1(%ebx),%eax
8048c85: 50 push %eax
8048c86: e8 dd ff ff ff call 8048c68 <func4>
8048c8b: 83 c4 08 add $0x8,%esp
8048c8e: 8d 34 07 lea (%edi,%eax,1),%esi
8048c91: 57 push %edi
8048c92: 83 eb 02 sub $0x2,%ebx
8048c95: 53 push %ebx
8048c96: e8 cd ff ff ff call 8048c68 <func4>
8048c9b: 83 c4 10 add $0x10,%esp
8048c9e: 01 f0 add %esi,%eax
8048ca0: eb 05 jmp 8048ca7 <func4+0x3f>
8048ca2: b8 00 00 00 00 mov $0x0,%eax
8048ca7: 5b pop %ebx
8048ca8: 5e pop %esi
8048ca9: 5f pop %edi
8048caa: c3 ret
相当于
08048d1d <phase_5>:ASCII密码
8048d1d: 53 push %ebx
8048d1e: 83 ec 24 sub $0x24,%esp
8048d21: 8b 5c 24 2c mov 0x2c(%esp),%ebx
//将输入字符串的地址移到ebx
8048d25: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048d2b: 89 44 24 18 mov %eax,0x18(%esp)
8048d2f: 31 c0 xor %eax,%eax
//保存ebx canary值初始化
8048d31: 53 push %ebx
用gdb输入为abcdef
查看此时ebx中内存对应的string 为“abcdef” 对此调用string_length,返回eax=6。
8048d32: e8 72 02 00 00 call 8048fa9 <string_length>
8048d37: 83 c4 10 add $0x10,%esp
8048d3a: 83 f8 06 cmp $0x6,%eax
8048d3d: 74 05 je 8048d44 <phase_5+0x27>
8048d3f: e8 7b 03 00 00 call 80490bf <explode_bomb>
//字符串的长度不等于6,则爆炸
8048d44: b8 00 00 00 00 mov $0x0,%eax
8048d49: 0f b6 14 03 movzbl (%ebx,%eax,1),%edx
//Ebx对应的内存中存储abcdef的ascii码,movzbl指令获取第一个字符的ascii码值,前面补满0,填到edx中。
8048d4d: 83 e2 0f and $0xf,%edx
//edx和0xf按位与,0xf是00000000000000000000000000001111,也就是获取edx最后4bit的值,也就是第一个字符ascii码的后面一位。
8048d50: 0f b6 92 40 a0 04 08 movzbl 0x804a040(%edx),%edx
以上是0x804a040附近的内存。加上edx中的偏移量,获取内存移到edx。
8048d57: 88 54 04 05 mov %dl,0x5(%esp,%eax,1)
Edx中的低16位移动到esp+5(0xffffd270+5)
8048d5b: 83 c0 01 add $0x1,%eax
8048d5e: 83 f8 06 cmp $0x6,%eax
8048d61: 75 e6 jne 8048d49 <phase_5+0x2c>
//循环结构,结果:把六个字符的ascii码的后面一位作为偏移量,加上0x804a040,得到的内存中的值放在esp+5(0xffffd270+5)到esp+10(0xffffd270+10)
8048d63: c6 44 24 0b 00 movb $0x0,0xb(%esp)
8048d68: 83 ec 08 sub $0x8,%esp
8048d6b: 68 16 a0 04 08 push $0x804a016
8048d70: 8d 44 24 11 lea 0x11(%esp),%eax
8048d74: 50 push %eax
8048d75: e8 4e 02 00 00 call 8048fc8 <strings_not_equal>
8048d7a: 83 c4 10 add $0x10,%esp
8048d7d: 85 c0 test %eax,%eax
8048d7f: 74 05 je 8048d86 <phase_5+0x69>
8048d81: e8 39 03 00 00 call 80490bf <explode_bomb>
观察以上汇编,与第一题类似,调用了strings_not_equal,不一样则爆炸。两个参数:一个是前面存储在esp+5(0xffffd270+5)到esp+10(0xffffd270+10)中的值,一个是存储在0x804a016中的字符串,用gdb查看。
每个数对应的ascii码如下:
每个ASCII码对应一个0x804a040+偏移量处的内存,这个偏移量是输入字符串的ASCII码值的后面一位。
偏移量对应:
也就是说,输入的六个字符的ASCII码值应该为:
0x*A 0x*4 0x*f 0x*5 0x*6 0x*7(*可以为任何值)
答案是六个任意组合的字符:
例如
jdoefg JDOEFG jdo567
*$/%&' J4o567 *$/567
8048d86: 8b 44 24 0c mov 0xc(%esp),%eax
8048d8a: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048d91: 74 05 je 8048d98 <phase_5+0x7b>
8048d93: e8 f8 f9 ff ff call 8048790 <__stack_chk_fail@plt>
8048d98: 83 c4 18 add $0x18,%esp
8048d9b: 5b pop %ebx
8048d9c: c3 ret
校验canary值和返回
08048d9d <phase_6>:链表
8048d9d: 56 push %esi
8048d9e: 53 push %ebx
8048d9f: 83 ec 4c sub $0x4c,%esp
8048da2: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048da8: 89 44 24 44 mov %eax,0x44(%esp)
8048dac: 31 c0 xor %eax,%eax
//函数入口,canary值初始化
8048dae: 8d 44 24 14 lea 0x14(%esp),%eax
8048db2: 50 push %eax
8048db3: ff 74 24 5c pushl 0x5c(%esp)
8048db7: e8 28 03 00 00 call 80490e4 <read_six_numbers>
将六个数读取到相应内存,栈结构如图所示。
8048dbc: 83 c4 10 add $0x10,%esp
8048dbf: mov $0x0,%esi esi = 0(循环计数器)
8048dc4: mov 0xc(%esp,%esi,4),%eax eax = 第esi个数(esp+0xc + esi*4)
8048dc8: sub $0x1,%eax eax = num-1
8048dcb: cmp $0x5,%eax 检查 num-1 ∈ [0,5](即 num ∈ [1,6])
8048dce: 76 05 jbe 8048dd5 <phase_6+0x38>
不∈ [1,6]就爆炸
8048dd0: e8 ea 02 00 00 call 80490bf <explode_bomb>
8048dd5: 83 c6 01 add $0x1,%esi 循环。
8048dd8: 83 fe 06 cmp $0x6,%esi
8048ddb: 74 33 je 8048e10 <phase_6+0x73>
上面汇编是判断输入的6个数字都要在1到6之间。循环结束,跳转到地址8048e10处继续执行
8048ddd: mov %esi,%ebx ebx = esi(内部循环起始索引)
8048ddf: mov 0xc(%esp,%ebx,4),%eax eax = 当前数(第ebx个,ebx >= esi+1)
8048de3: cmp %eax,0x8(%esp,%esi,4) 比较第esi个数是否与第ebx个数相等
8048de7: 75 05 jne 8048dee <phase_6+0x51>
8048de9: e8 d1 02 00 00 call 80490bf <explode_bomb> 相等就爆炸
上面是验证数值是否唯一的过程,
比如先验证完a[0]是不是∈ [1,6],接着访问a[1]~a[5],如果与a[0]相等就爆炸
以上的代码要求输入 6 个 1~6 之间的不重复整数。
8048dee: 83 c3 01 add $0x1,%ebx
8048df1: 83 fb 05 cmp $0x5,%ebx
8048df4: 7e e9 jle 8048ddf <phase_6+0x42>
8048df6: eb cc jmp 8048dc4 <phase_6+0x27>
8048df8: 8b 52 08 mov 0x8(%edx),%edx
8048dfb: 83 c0 01 add $0x1,%eax
8048dfe: 39 c8 cmp %ecx,%eax
8048e00: 75 f6 jne 8048df8 <phase_6+0x5b>
8048e02: 89 54 b4 24 mov %edx,0x24(%esp,%esi,4)
8048e06: 83 c3 01 add $0x1,%ebx
8048e09: 83 fb 06 cmp $0x6,%ebx
8048e0c: 75 07 jne 8048e15 <phase_6+0x78>
8048e0e: eb 1c jmp 8048e2c <phase_6+0x8f>
8048e10: bb 00 00 00 00 mov $0x0,%ebx
8048e15: 89 de mov %ebx,%esi
8048e17: 8b 4c 9c 0c mov 0xc(%esp,%ebx,4),%ecx
8048e1b: b8 01 00 00 00 mov $0x1,%eax
8048e20: ba 3c c1 04 08 mov $0x804c13c,%edx
8048e25: 83 f9 01 cmp $0x1,%ecx
8048e28: 7f ce jg 8048df8 <phase_6+0x5b>
8048e2a: eb d6 jmp 8048e02 <phase_6+0x65>
//以上代码,将链表节点的地址存入栈中。
分析时发现一个立即数0x804c13c,存入edx中后有这样的寻址:8048df8:mov 0x8(%edx),%edx。我们查看此处附近的内存:
自然联想到node是链表的节点,进一步观察可得:每个节点占12个字节,前四个字节是节点的值,中间四个是节点的编号,最后四个存储下一个节点的地址。
代码做了些什么呢?我们逐行分析,8048e10 – 8048e25。
这段取出了a[0],若<1则跳转到8048df8,>=1跳转到8048e02
而8048df8~8048e02中的内容是通过a[0]的值来确定需要访问的链表节点地址。
mov %edx,0x24(%esp,%esi,4)这句将节点地址移到以esp-20开头的栈空间中(位于存储数组的上方)
然后,通过跳转实现循环,对数组中的每个元素实行一样的操作。
分析完成后,我们用gdb查看内存验证一下:(输入的是2 3 1 5 6 4)
可见24c~260中存储了2 3 1 5 6 4。而264开始,存储了6个链表节点的地址,分别是2,3,1,5,6,4号节点的地址!我们的分析是正确的。
8048e2c: mov 0x24(%esp),%ebx ebx = 第一个节点地址(新链表头)
8048e30: lea 0x24(%esp),%eax eax = 节点地址数组起始地址
8048e34: lea 0x38(%esp),%esi esi = 节点地址数组结束地址
8048e38: mov %ebx,%ecx ecx = 当前节点
8048e3a: mov 0x4(%eax),%edx edx = 下一个节点地址(eax+4)
8048e3d: mov %edx,0x8(%ecx) 当前节点->next = edx
8048e40: add $0x4,%eax eax +=4(移动到下个节点地址)
8048e43: mov %edx,%ecx ecx = edx(更新当前节点)
8048e45: cmp %esi,%eax 检查是否处理完所有节点
8048e47: jne 8048e3a 未完成则继续循环
8048e49: movl $0x0,0x8(%edx) 最后一个节点->next = NULL
以上,将节点地址刚才存储的数组顺序连接成新链表。
用gdb查看内存
改变链表顺序实则就是在改变最后4个字节中的地址值!
8048e50: be 05 00 00 00 mov $0x5,%esi 初始化循环的变量
8048e55: 8b 43 08 mov 0x8(%ebx),%eax
//eax中存储第二个节点的地址
8048e58: 8b 00 mov (%eax),%eax
//eax中存储第二个地址的数值
8048e5a: 39 03 cmp %eax,(%ebx)
//比较第一个节点的值和第二个节点的值,node1.val<=node2.val则不爆炸
然后循环访问每个元素,其实就是对原链表值做个降序排序。
8048e5c: 7e 05 jle 8048e63 <phase_6+0xc6>
8048e5e: e8 5c 02 00 00 call 80490bf <explode_bomb>
8048e63: 8b 5b 08 mov 0x8(%ebx),%ebx
8048e66: 83 ee 01 sub $0x1,%esi
8048e69: 75 ea jne 8048e55 <phase_6+0xb8>
以上代码要求我们输入的值满足链表值降序的要求。
降序 输入3 1 6 4 2 5
8048e6b: 8b 44 24 3c mov 0x3c(%esp),%eax
8048e6f: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048e76: 74 05 je 8048e7d <phase_6+0xe0>
8048e78: e8 13 f9 ff ff call 8048790 <__stack_chk_fail@plt>
8048e7d: 83 c4 44 add $0x44,%esp
8048e80: 5b pop %ebx
8048e81: 5e pop %esi
8048e82: c3 ret
校验canary值并返回
隐藏关卡:二叉树
如何进入?
只有phase_defused函数调用了secret_phase,我们来分析下phase_defused函数
08049218 <phase_defused>:
8049218: 83 ec 6c sub $0x6c,%esp
804921b: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8049221: 89 44 24 5c mov %eax,0x5c(%esp)
8049225: 31 c0 xor %eax,%eax
//函数入口,设置canary值
8049227: 83 3d cc c3 04 08 06 cmpl $0x6,0x804c3cc
在phase_defused处打断点 查看0x804c3cc处内存,解开一关后:
解开两关后:
可知0x804c3cc处存储的是解开的关卡数,与6比较,直接跳到结束处,开启不了隐藏关。
804922e: 75 73 jne 80492a3 <phase_defused+0x8b>
8049230: 83 ec 0c sub $0xc,%esp
8049233: 8d 44 24 18 lea 0x18(%esp),%eax
8049237: 50 push %eax
8049238: 8d 44 24 18 lea 0x18(%esp),%eax
804923c: 50 push %eax
804923d: 8d 44 24 18 lea 0x18(%esp),%eax
8049241: 50 push %eax
//在栈上分配三个连续的空间
8049242: 68 b9 a1 04 08 push $0x804a1b9
8049247: 68 d0 c4 04 08 push $0x804c4d0
804924c: e8 bf f5 ff ff call 8048810 <__isoc99_sscanf@plt>
//查看传入的两个参数,一个是d d s,说明开启隐藏关需要在某一关输出两个整数和一个字符串,答案是两个整数的有关卡3,4,而第二个参数存储的是108 2(调试中我们输入的),是4的一个答案,说明开启隐藏关的条件是在第四关的答案后面加上一个字符串,这个字符串是什么呢?我们继续往下看。
8049251: 83 c4 20 add $0x20,%esp
8049254: 83 f8 03 cmp $0x3,%eax
8049257: 75 3a jne 8049293 <phase_defused+0x7b>
//输入的参数必须为3个
8049259: 83 ec 08 sub $0x8,%esp
804925c: 68 c2 a1 04 08 push $0x804a1c2
8049261: 8d 44 24 18 lea 0x18(%esp),%eax
8049265: 50 push %eax
8049266: e8 5d fd ff ff call 8048fc8 <strings_not_equal>
804926b: 83 c4 10 add $0x10,%esp
804926e: 85 c0 test %eax,%eax
8049270: 75 21 jne 8049293 <phase_defused+0x7b>
比较局部变量中的字符串(栈地址 0x18(%esp))与预设字符串 0x804a1c2(反编译后内容如 "DrEvil")。
第三个参数要是DrEvil!
8049272: 83 ec 0c sub $0xc,%esp
8049275: 68 88 a0 04 08 push $0x804a088
804927a: e8 41 f5 ff ff call 80487c0 <puts@plt>
804927f: c7 04 24 b0 a0 04 08 movl $0x804a0b0,(%esp)
8049286: e8 35 f5 ff ff call 80487c0 <puts@plt>
804928b: e8 44 fc ff ff call 8048ed4 <secret_phase>
8049290: 83 c4 10 add $0x10,%esp
8049293: 83 ec 0c sub $0xc,%esp
8049296: 68 e8 a0 04 08 push $0x804a0e8
804929b: e8 20 f5 ff ff call 80487c0 <puts@plt>
一些提示信息,对解题无帮助……
80492a0: 83 c4 10 add $0x10,%esp
80492a3: 8b 44 24 5c mov 0x5c(%esp),%eax
80492a7: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
80492ae: 74 05 je 80492b5 <phase_defused+0x9d>
80492b0: e8 db f4 ff ff call 8048790 <__stack_chk_fail@plt>
80492b5: 83 c4 6c add $0x6c,%esp
80492b8: c3 ret
//函数返回,校验canary值。
于是,我们到了隐藏关…
隐藏关的代码位于
08048ed4 <secret_phase>:
8048ed4: 53 push %ebx
8048ed5: 83 ec 08 sub $0x8,%esp
8048ed8: e8 42 02 00 00 call 804911f <read_line>
//读取输入值 存的地址返回到eax 这边输入123456用于调试。
8048edd: 83 ec 04 sub $0x4,%esp
8048ee0: 6a 0a push $0xa
8048ee2: 6a 00 push $0x0
8048ee4: 50 push %eax
8048ee5: e8 96 f9 ff ff call 8048880 <strtol@plt>
使用strtol将字符串转换为长整数(n),返回值在eax中
8048eea: 89 c3 mov %eax,%ebx
8048eec: 8d 40 ff lea -0x1(%eax),%eax
8048eef: 83 c4 10 add $0x10,%esp
8048ef2: 3d e8 03 00 00 cmp $0x3e8,%eax
8048ef7: 76 05 jbe 8048efe <secret_phase+0x2a>
8048ef9: e8 c1 01 00 00 call 80490bf <explode_bomb>
//验证n-1是否小于等于0x3e8(1000)(即1 ≤ n ≤ 1001),否则引爆炸弹。
8048efe: 83 ec 08 sub $0x8,%esp
8048f01: 53 push %ebx
8048f02: 68 88 c0 04 08 push $0x804c088
8048f07: e8 77 ff ff ff call 8048e83 <fun7>
//Fun7的两个参数分别是:n和内存地址0x804c088
查看0x804c088附近的内存
//这是什么数据结构?根据之前链表的经验,关键是看每个结构的数据域和地址域。
一个节点12个字节 前四个没什么规律 其它8个字节似乎存储了其它两个节点的内存……?这不是树结构吗?节点值和左右子节点啊!画出示意图看看:
一个标准的完全二叉树(一共四层)。Fun7函数接收的参数也就是根节点!
我们往下看fun7函数。
8048f0c: 83 c4 10 add $0x10,%esp
8048f0f: 83 f8 02 cmp $0x2,%eax
8048f12: 74 05 je 8048f19 <secret_phase+0x45>
8048f14: e8 a6 01 00 00 call 80490bf <explode_bomb>
//检查fun7返回值(eax)是否为2,否则引爆炸弹。
8048f19: 83 ec 0c sub $0xc,%esp
8048f1c: 68 f0 9f 04 08 push $0x8049ff0
8048f21: e8 9a f8 ff ff call 80487c0 <puts@plt>
8048f26: e8 ed 02 00 00 call 8049218 <phase_defused>
8048f2b: 83 c4 18 add $0x18,%esp
8048f2e: 5b pop %ebx
8048f2f: c3 ret
Fun7函数,接收根节点地址和输入字符串的强转长整数。
08048e83 <fun7>:
8048e83: 53 push %ebx
8048e84: 83 ec 08 sub $0x8,%esp
8048e87: 8b 54 24 10 mov 0x10(%esp),%edx
8048e8b: 8b 4c 24 14 mov 0x14(%esp),%ecx
//edx=node地址(第一次是根节点)
ecx=n
8048e8f: 85 d2 test %edx,%edx
8048e91: 74 37 je 8048eca <fun7+0x47>
//若node=NULL,返回-1(递归终止条件!)
if (node=NULL) return -1;
8048e93: 8b 1a mov (%edx),%ebx
//ebx=node->value
8048e95: 39 cb cmp %ecx,%ebx
8048e97: 7e 13 jle 8048eac <fun7+0x29>
//比较 node->value 和n
若 node->value <=n,跳转8048eac。if (node->value <=n) return…;
8048e99: 83 ec 08 sub $0x8,%esp
8048e9c: 51 push %ecx
8048e9d: ff 72 04 pushl 0x4(%edx)
8048ea0: e8 de ff ff ff call 8048e83 <fun7>
//调用左子树,两个参数分别为ecx(n), 0x4(%edx)(node->left)
8048ea5: 83 c4 10 add $0x10,%esp
8048ea8: 01 c0 add %eax,%eax
8048eaa: eb 23 jmp 8048ecf <fun7+0x4c>
//将返回值翻倍后直接跳转8048ecf(返回eax,即为2倍fun(node->left,n))
if (node->value >n) return 2fun(node->left,n);
(若 node->value <=n跳转到这边)
8048eac: b8 00 00 00 00 mov $0x0,%eax
8048eb1: 39 cb cmp %ecx,%ebx
8048eb3: 74 1a je 8048ecf <fun7+0x4c>
//eax=0 ecx=n ebx=node->val 若两者相等则返回0(eax)
if (node->value ==n) return 0;
8048eb5: 83 ec 08 sub $0x8,%esp
8048eb8: 51 push %ecx
8048eb9: ff 72 08 pushl 0x8(%edx)
8048ebc: e8 c2 ff ff ff call 8048e83 <fun7>、
//ecx=n, 0x8(%edx)是右子节点,调用func(node->right,n)
8048ec1: 83 c4 10 add $0x10,%esp
8048ec4: 8d 44 00 01 lea 0x1(%eax,%eax,1),%eax
8048ec8: eb 05 jmp 8048ecf <fun7+0x4c>
//返回的值*2再加1
if (node->value <n) return 2func(node->right,n)+1;
8048eca: b8 ff ff ff ff mov $0xffffffff,%eax
8048ecf: 83 c4 08 add $0x8,%esp
8048ed2: 5b pop %ebx
8048ed3: c3 ret
有伪代码:
我们的树结构(附上节点值
)
func(root,n)=2,确定n?
最终希望返回值是 2,所以我们输入的数一定在树中,否则就返回负值。
经过这个节点先要0*2+1=1,也就是其父节点的右子节点,然后他的父节点是祖父节点的子节点(1*2 = 2),此时返回值恰好是 2,故祖父节点就是根节点。所以目标节点(36)是根节点的左子节点(8)的右子节点(22),所以最终期待用户输入值是 22,即 0x16.
什么字符串经过strtol后是0x16?其实也就是字符串”22”即可。
Ans:(an example)
I am not part of the problem. I am a Republican.
0 1 1 2 3 5
0 809
108 2 DrEvil
*$/%&'
3 1 6 4 2 5
22
Bomb has been defused!
【实验总结】
这次试验个人觉得难度非常大,简单易懂的c代码想从汇编级读懂,难度不知道翻了多少倍。但是,从困难的解题过程中,我知道了计算机到底是怎么执行程序的,寄存器,内存,指令……到底他们之间怎么配合才能让一个程序得到应有的结果,分支,循环这种东西在我脑中不再是简单的ifelse,while这样的英文单词,而是汇编级的“跳转”。数组,链表,二叉树这些数据结构也不是晦涩难懂的东西,只是内存中的一些按规律排列的字节罢了。程序的运行不再是轻点一下“开始运行”,而是ebp,esp不断变化,栈帧不断上下增长的过程。从上学期电子电路学的制作cpu,到现在对汇编代码有了充分的研究,可以说,计算机这个神奇的东西在我看来已经不那么神秘了。