接触《深入理解计算机系统》这本书很久了,但一直都处于看的阶段,但懂是一回事,做出来又是另一回事。
于是就试着做了几个实验,发现这个二进制炸弹特别好玩,于是乎就有了此文。
知识储备
调试:
停在地址处: break *0x8012345
打印寄存器: print $eax
显示所有寄存器值:I reg
检测内存值:x /nfu addr
x 检测内存值, n 查看几个内存单元 f进制显示 u显示单字节双字节等。
寄存器:
eiz: 伪寄存器,值始终为0
为什么要用:占位 NOP 可以用来占位,但只是一个字节,长指令比短指令执行快,所以选用其占位。同样的占位指令还有: lea 0x0(%edi) %edi
汇编:
lea 指令:LEA src, dst=>要求src必须为粗出去地址。而寻址方式多种多样。
重要知识点:
switch语句的实现:跳转表
工具:
gdb 调试
objdump –t 查看符号表,即所使用的所有符号。
objdump –d 反汇编
strings 显示程序所用的全部字符串
拆解过程
main函数:
8048a52: e8 a5 07 00 00 call 80491fc <read_line>
8048a57: 83 c4 f4 add $0xfffffff4,%esp
8048a5a: 50 push %eax
8048a5b: e8 c0 00 00 00 call 8048b20 <phase_1>
通过调用库函数readline读入一行数据,字符串地址通过eax返回,然后将字符串传给phase_1函数。
同样,其后几个阶段遵循这种处理方式:读取一行字符串,调用phase_n,传入字符串参数。
阶段一:
阶段1比较简单,判断输入的字符串与程序内部某个字符串是否相当,不等即爆炸。
8048b2c: 68 c0 97 04 08 push $0x80497c0
8048b31: 50 push %eax
8048b32: e8 f9 04 00 00 call 8049030 <strings_not_equal>
所以解题的关键在于,找到程序中的字符串,使用gdb进行调试,打印地址0x80497c0的字符串。
答案显然:Public speaking is very easy.
阶段二:
phase2:
8048b56: 8d 45 e8 lea -0x18(%ebp),%eax
8048b59: 50 push %eax
8048b5a: 52 push %edx
8048b5b: e8 78 04 00 00 call 8048fd8 <read_six_numbers>
8048b60: 83 c4 10 add $0x10,%esp
8048b63: 83 7d e8 01 cmpl $0x1,-0x18(%ebp)
8048b67: 74 05 je 8048b6e <phase_2+0x26>
8048b69: e8 8e 09 00 00 call 80494fc <explode_bomb>
8048b6e: bb 01 00 00 00 mov $0x1,%ebx
8048b73: 8d 75 e8 lea -0x18(%ebp),%esi //esi = array
8048b76: 8d 43 01 lea 0x1(%ebx),%eax //eax = ebx + 1
8048b79: 0f af 44 9e fc imul -0x4(%esi,%ebx,4),%eax //eax = esi + ebx * 4 - 0x4
8048b7e: 39 04 9e cmp %eax,(%esi,%ebx,4) //eax == esi + ebx * 4
8048b81: 74 05 je 8048b88 <phase_2+0x40>
8048b83: e8 74 09 00 00 call 80494fc <explode_bomb>
8048b88: 43 inc %ebx
8048b89: 83 fb 05 cmp $0x5,%ebx
8048b8c: 7e e8 jle 8048b76 <phase_2+0x2e>
1. 输入六个数,通过read_six_sumbers将字符串拆分成6个数,存入 ebp -0x18的位置上。
2. cmpl $0x1,-0x18(%ebp) 如果数组第一个元素不是1,则爆炸。即第一个数要为数字1.
3. 8048b76-8048b8c为一个循环。伪码如下:
if (arr[0] != 1) explode_bomb();
for (int i = 1; i <= 5; i++) {
if ( (i+1) * arr[i-1] != arr[i])
explode_bomb();
}
答案显然:1 26 24 120 720
阶段三:
第三阶段,有些繁琐。
内部含有sscanf函数,猜想应该是格式字符串。
sscanf传入的参数为:ebp-0x4,ebp-0x5, ebp-0xc地址;格式字符串地址;输入的字符参数地址。
ebp-0xc存第一个数, ebp-0x5存第二数,ebp-0x4第三个数。
因而,输入数据为:整数, 字符,整数。
8048bc9: 83 7d f4 07 cmpl $0x7,-0xc(%ebp)
8048bcd: 0f 87 b5 00 00 00 ja 8048c88 <phase_3+0xf0>
8048bd3: 8b 45 f4 mov -0xc(%ebp),%eax
8048bd6: ff 24 85 e8 97 04 08 jmp *0x80497e8(,%eax,4)
输入的第一个数要小于等于7,而跳转到0x80497e8这个地址(汇编代码中没有显示这个地址),而同时,代码中又都可以跳转到<phase_3+0xf7>这个位置,猜想是不是switch语句,而要有switch,就需要查找跳转表。
可见,全部为phase_3内的地址,所以,验证为switch语句。
既然要求第一个数要小于等于7,那么使用最简单0进行测试。
8048bd3: 8b 45 f4 mov -0xc(%ebp),%eax
8048bd6: ff 24 85 e8 97 04 08 jmp *0x80497e8(,%eax,4)
第一个数为0,则eax 为0,则直接跳转到 (0x80497e8) 指向的位置。0x80497eb存储的内容为:
由上图可知:0x8048be0
8048be0: b3 71 mov $0x71,%bl
8048be2: 81 7d fc 09 03 00 00 cmpl $0x309,-0x4(%ebp)
8048be9: 0f 84 a0 00 00 00 je 8048c8f <phase_3+0xf7>
8048bef: e8 08 09 00 00 call 80494fc <explode_bomb>
ebp-0x4存储的为第三个数,大小为0x309, 转10进制为:777。 同时,设置了bl 为 0x71。并跳转到0x8048c8f。
8048c8f: 3a 5d fb cmp -0x5(%ebp),%bl
8048c92: 74 05 je 8048c99 <phase_3+0x101>
8048c94: e8 63 08 00 00 call 80494fc <explode_bomb>
字符串值为0x71,换算成字符为 ‘q’。
所以答案为: 0 q 777。 当然,答案不为一,根据第一个参数的选择不同而不同。
阶段四:
8048cec: 8d 45 fc lea -0x4(%ebp),%eax又是一个sscanf,查看0x8049808字符串内容。
8048cef: 50 push %eax
8048cf0: 68 08 98 04 08 push $0x8049808
8048cf5: 52 push %edx
8048cf6: e8 65 fb ff ff call 8048860 <sscanf@plt>
可见,输入的是一个整数。
8048d11: 8b 45 fc mov -0x4(%ebp),%eax
8048d14: 50 push %eax
8048d15: e8 86 ff ff ff call 8048ca0 <func4>
8048d1a: 83 c4 10 add $0x10,%esp
8048d1d: 83 f8 37 cmp $0x37,%eax
8048d20: 74 05 je 8048d27 <phase_4+0x47>
将输入的数据传入func4函数中,如果返回值为0x37, 则成功,所以,关键要看func4,推出输入的参数。
8048ca8: 8b 5d 08 mov 0x8(%ebp),%ebx
8048cab: 83 fb 01 cmp $0x1,%ebx //与1比较
8048cae: 7e 20 jle 8048cd0 <func4+0x30> //小于等于 1, 返回1
8048cb0: 83 c4 f4 add $0xfffffff4,%esp
8048cb3: 8d 43 ff lea -0x1(%ebx),%eax //参数-1, 调用func
8048cb6: 50 push %eax
8048cb7: e8 e4 ff ff ff call 8048ca0 <func4>
8048cbc: 89 c6 mov %eax,%esi //返回值保存在esi中
8048cbe: 83 c4 f4 add $0xfffffff4,%esp
8048cc1: 8d 43 fe lea -0x2(%ebx),%eax //参数-2, 调用func
8048cc4: 50 push %eax
8048cc5: e8 d6 ff ff ff call 8048ca0 <func4>
8048cca: 01 f0 add %esi,%eax //两次返回值相加,的返回结果
8048ccc: eb 07 jmp 8048cd5 <func4+0x35>
8048cce: 89 f6 mov %esi,%esi
8048cd0: b8 01 00 00 00 mov $0x1,%eax
存在自身调用自身的情况,应该是一个递归。代码应如下结构:
inf func4(int val) {
if (val <= 1)
return 1;
else
return func4(val-1) + func4(val-2);
}
val为何值,返回结果为0x37, 10进制的55. 最简单,写个程序暴力破解一下,值9.
阶段五:
8048d3b: e8 d8 02 00 00 call 8049018 <string_length>
8048d40: 83 c4 10 add $0x10,%esp
8048d43: 83 f8 06 cmp $0x6,%eax
可见输入的字符串,长度必须为6.
一个循环:
8048d4d: 31 d2 xor %edx,%edxebx存放用户输入字符串首地址,而0x804b220肯定也为一个字符串,打印出来,将该字符串称为 模式串。
8048d4f: 8d 4d f8 lea -0x8(%ebp),%ecx
8048d52: be 20 b2 04 08 mov $0x804b220,%esi
8048d57: 8a 04 1a mov (%edx,%ebx,1),%al //依次取用户输入串中字符
8048d5a: 24 0f and $0xf,%al //只保留最后四位,其它位清零
8048d5c: 0f be c0 movsbl %al,%eax //带符号扩展送于eax中
8048d5f: 8a 04 30 mov (%eax,%esi,1),%al //取模式串第patt[eax]的字符
8048d62: 88 04 0a mov %al,(%edx,%ecx,1) //存入另一数组中,ebp-0x8
8048d65: 42 inc %edx
8048d66: 83 fa 05 cmp $0x5,%edx
8048d69: 7e ec jle 8048d57 <phase_5+0x2b>
整体思路就是,用户输入的字符串,取低四位,值作为索引,找到模式串中该索引对应的字符,放于一个新数组中。
8048d72: 68 0b 98 04 08 push $0x804980b
8048d77: 8d 45 f8 lea -0x8(%ebp),%eax
8048d7a: 50 push %eax
8048d7b: e8 b0 02 00 00 call 8049030 <strings_not_equal>
用新数组中字符与0x804980b字符比较,相等即破译。
所以,在模式串中的下标应为:15 0 5 11 13 1
而,要将这些字符换算成可打印的字符,然后输入。每个字符8位,最低4位不能动,所以,在高四位可以任意加1.
转变为:79, 80, 69, 75, 77, 65 对应字符:OPEKMA
阶段六:
首先,阶段六太不容易解决啦,整整花了7-8个小时。关键点:链表。
1. 读6个数
8048dae: 8d45 e8 lea -0x18(%ebp),%eax读取6个数,存放在ebp-18的地方,所以,ebp-18应该是一个数组首地址。
8048db1: 50 push %eax
8048db2: 52 push %edx
8048db3: e820 02 00 00 call 8048fd8<read_six_numbers>
2. 一个大循环,检测输入数据是否满足要求
8048db8: 31 ff xor %edi,%edi //清零,控制变量i
8048dba: 83c4 10 add $0x10,%esp
8048dbd: 8d76 00 lea 0x0(%esi),%esi
8048dc0: 8d45 e8 lea -0x18(%ebp),%eax //取数组首地址
8048dc3: 8b04 b8 mov (%eax,%edi,4),%eax //取arr[i] –》 val
8048dc6: 48 dec %eax // val--
8048dc7: 83f8 05 cmp $0x5,%eax //必须小于等于5,即原小于等于6
8048dca: 7605 jbe 8048dd1 <phase_6+0x39>
8048dcc: e82b 07 00 00 call 80494fc<explode_bomb>
8048dd1: 8d5f 01 lea 0x1(%edi),%ebx // ebx = edi + 1, j = I + 1
8048dd4: 83fb 05 cmp $0x5,%ebx // 要小于等于5
8048dd7: 7f23 jg 8048dfc <phase_6+0x64>
8048dd9: 8d04 bd 00 00 00 00 lea 0x0(,%edi,4),%eax //eax = 4 * edi 用户获取元素地址
8048de0: 8945 c8 mov %eax,-0x38(%ebp) //暂存
8048de3: 8d75 e8 lea -0x18(%ebp),%esi //取arr首地址
8048de6: 8b55 c8 mov -0x38(%ebp),%edx
8048de9: 8b04 32 mov (%edx,%esi,1),%eax //获取元素arr[i]
8048dec: 3b04 9e cmp (%esi,%ebx,4),%eax //比较arr[j] arr[i]
8048def: 7505 jne 8048df6 <phase_6+0x5e> // 不能相同
8048df1: e806 07 00 00 call 80494fc<explode_bomb>
8048df6: 43 inc %ebx //j++
8048df7: 83fb 05 cmp $0x5,%ebx
8048dfa: 7eea jle 8048de6 <phase_6+0x4e>
8048dfc: 47 inc %edi //i++
8048dfd: 83ff 05 cmp $0x5,%edi
8048e00: 7ebe jle 8048dc0 <phase_6+0x28>
伪代码如下:
for (int i =0; i<=5; i++){
if (arr[i]>= 6)
explode_bomb();
for (int j = i+1; j<=5; j++){
if (arr[i]== arr[j])
explode_bomb();
}
}
检测元素,即每个元素必须不同。同时,必须都是大于等于0的,因为cmp $0x5,%eax都是按照无符号数进行处理的,如果为负数,则换算后为一个大数,因此,不能出现这样的情况。所以,推断处理数据因为:1 2 3 4 5 6,至于排列顺序,目前未知。
3. 最关键的一段代码
8048e02: 31 ff xor %edi,%edi // i清零
8048e04: 8d4d e8 lea -0x18(%ebp),%ecx //数组首地址arr
8048e07: 8d45 d0 lea -0x30(%ebp),%eax //ebp-0x30的地址
8048e0a: 8945 c4 mov %eax,-0x3c(%ebp) //存放于ebp-0x3c中
8048e0d: 8d76 00 lea 0x0(%esi),%esi //无用代码,对齐用
8048e10: 8b75 cc mov -0x34(%ebp),%esi //取ebp-34中值
8048e13: bb01 00 00 00 mov $0x1,%ebx // j = 1
8048e18: 8d04 bd 00 00 00 00 lea 0x0(,%edi,4),%eax //eax = edi * 4
8048e1f: 89c2 mov %eax,%edx
8048e21: 3b1c 08 cmp (%eax,%ecx,1),%ebx //arr[i] 与 j比较
8048e24: 7d12 jge 8048e38 <phase_6+0xa0> //小于的话,进入循环
8048e26: 8b04 0a mov (%edx,%ecx,1),%eax //eax = arr[i]
8048e29: 8db4 26 00 00 00 00 lea 0x0(%esi,%eiz,1),%esi //对齐,占位,无用
8048e30: 8b76 08 mov 0x8(%esi),%esi //esi+0x8的值给esi
8048e33: 43 inc %ebx //j++
8048e34: 39c3 cmp %eax,%ebx //arr[i] == j 停止循环
8048e36: 7cf8 jl 8048e30 <phase_6+0x98>
8048e38: 8b55 c4 mov -0x3c(%ebp),%edx //获得ebp-0x30地址
8048e3b: 8934 ba mov %esi,(%edx,%edi,4) //esi存入新数组中
8048e3e: 47 inc %edi
8048e3f: 83ff 05 cmp $0x5,%edi
8048e42: 7ecc jle 8048e10 <phase_6+0x78>
首先,在进入函数后,一句重要代码:
8048da4: c745 cc 6c b2 04 08 movl $0x804b26c,-0x34(%ebp)
又是一个地址,就以为是一个字符串,但尝试打印的时候出现问题:
显示一个<node1>,难道是一个结构体。而8048e30: 8b 76 08 mov 0x8(%esi),%esi,esi值为0x804b26c,如果为结构体,
这明显是一个结构体的操作,相当于一个next指针。
打印出来试试:
可见,就是一个链表,首尾相接。
伪代码如下:
for (int i =0; i<=5; i++){
for (int j =0; arr[i]> j; j++){
node = node.next;
}
ptr[i]= node;
}
比如输入 3 1 2 4,这样,就让链表第三个元素排到第一位置,链表第1位置元素排到第二,链表第二元素排到第三,……
4. 重新串接链表
8048e44: 8b 75 d0 mov -0x30(%ebp),%esi //取ptr地址
8048e47: 8975 cc mov %esi,-0x34(%ebp) //存于ebp – 0x34中
8048e4a: bf01 00 00 00 mov $0x1,%edi // i = 1
8048e4f: 8d55 d0 lea -0x30(%ebp),%edx //ebp-0x30地址
8048e52: 8b04 ba mov (%edx,%edi,4),%eax //ptr[i] => eax
8048e55: 8946 08 mov %eax,0x8(%esi) //ptr[i-1]->next = ptr[i]
8048e58: 89c6 mov %eax,%esi //esi = ptr[i]
8048e5a: 47 inc %edi
8048e5b: 83ff 05 cmp $0x5,%edi
8048e5e: 7ef2 jle 8048e52 <phase_6+0xba>
8048e60: c746 08 00 00 00 00 movl $0x0,0x8(%esi)
8048e67: 8b75 cc mov -0x34(%ebp),%esi //最后一个结点next 赋0
5.检测链表值
8048e6a: 31 ff xor %edi,%edi // i = 0总结:也就是说,要让第一个元素的值从大到小排列。初始链表元素第一个值从第一到第五分别为:
8048e6c: 8d74 26 00 lea 0x0(%esi,%eiz,1),%esi //无用代码
8048e70: 8b56 08 mov 0x8(%esi),%edx //esi+8 的值为下一个链表地址
8048e73: 8b06 mov (%esi),%eax //取当前链表第一个元素值
8048e75: 3b02 cmp (%edx),%eax //与下一个链表第一个元素值比较
8048e77: 7d05 jge 8048e7e <phase_6+0xe6> //当前大于下一个,则成功
8048e79: e87e 06 00 00 call 80494fc<explode_bomb>
8048e7e: 8b76 08 mov 0x8(%esi),%esi
8048e81: 47 inc %edi
8048e82: 83ff 04 cmp $0x4,%edi
8048e85: 7ee9 jle 8048e70 <phase_6+0xd8>
fd 2d5 12d 3e5 d4 1b0
让其从大到小排列:3e5 2d5 1b0 12d fd d4
对应输入值:4 2 6 3 1 5
附上答案:
Public speaking is very easy.
1 2 6 24 120 720
0 q 777
9
OPEKMA
4 2 6 3 1 5
啊,终于成功拆解了!!!!!