CS:APP-Bomb Lab
前言
该实验主要是考察我们的阅读汇编代码的能力,会有几个小的关卡,通过了第一个关卡后才可以进入下一个关卡。
推荐工具:pwbdbg
,便于动态调试查看源码和寄存器等变量.
获取汇编代码:
objdump -d bomb > bomb.s
通关观察bomb.c
文件,我们可以知道,每一个阶段都会获取我们的输入,然后作为参数调用phase_x
函数调用过程,参数传递表:
机器位数 | 1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|---|
64 | %rdi | %rsi | %rdx | %rcx | %r8 | %r9 |
如果参数超过7,则第七个开始,参数是放到栈上,且是逆序压入栈中(第7个参数是在栈顶)
返回值是放到了rax
中
此外,rbp
寄存器通常被用作栈帧指针(Frame Pointer)或基址指针(Base Pointer),一般用于定位局部变量
rsp用于指向当前栈顶的地址,二者动态共同维护函数栈帧
phase_1
使用pwndbg打开可执行文件
gdb bomb
b main
run
然后不断单步运行,输入测试aaaabbbb
,然后使用命令s
步入phase_1
方法,该函数内容如下:
► 0x400ee0 <phase_1> sub rsp, 8
0x400ee4 <phase_1+4> mov esi, 0x402400
0x400ee9 <phase_1+9> call strings_not_equal <strings_not_equal>
0x400eee <phase_1+14> test eax, eax
0x400ef0 <phase_1+16> je phase_1+23 <phase_1+23>
0x400ef2 <phase_1+18> call explode_bomb <explode_bomb>
0x400ef7 <phase_1+23> add rsp, 8
0x400efb <phase_1+27> ret
我们可以看到,函数将0x402400
作为参数,给到了esi
,然后是调用函数strings_not_equal
,该函数用于判断两个字符串是否相等,而esi
是调用函数中,充当保存第二个参数的角色,盲猜0x402400
中存放的是一个字符串,我们来看看这个字符串是什么:
pwndbg> print (char*)0x402400
$3 = 0x402400 "Border relations with Canada have never been better."
把它作为输入参数,先重新运行
gdb bomb
b main
run
然后输入Border relations with Canada have never been better.
,程序果然进入到了下一个阶段函数中了。
In file: /home/kali/Desktop/bomb/bomb.c
76 * Let me know how they did it. */
77 printf("Phase 1 defused. How about the next one?\n");
78
79 /* The second phase is harder. No one will ever figure out
80 * how to defuse this... */
► 81 input = read_line();
82 phase_2(input);
83 phase_defused();
84 printf("That's number 2. Keep going!\n");
85
86 /* I guess this is too easy so far. Some more complex code will
因此本次通关payload
:Border relations with Canada have never been better.
phase_2
经过长时间的动态调试后,该函数主要的功能是:
先调用sscanf()
函数将我们输入的字符串解析成若干个数字,数字个数必须是6个,然后第一个数字必须是1,接着用循环判断这些数字,要满足后面的数字是前面一个上数字的两倍,因此我们可以直接输入1 2 4 8 16 32
下面是phase_2
函数的解析:
; bobm.s
0000000000400efc <phase_2>:
400efc: 55 push %rbp
400efd: 53 push %rbx
400efe: 48 83 ec 28 sub $0x28,%rsp
; 将rsp的内容给rsi,rsi作为函数调用过程中的第二个参数,说明read_six_numbers函数的入参参数至少有两个
400f02: 48 89 e6 mov %rsp,%rsi
400f05: e8 52 05 00 00 call 40145c <read_six_numbers>
; 经过上面的函数调用,rsp指向的地址中,保存的是若干个整数
400f0a: 83 3c 24 01 cmpl $0x1,(%rsp)
; 输入的第一个参数必须是 1
400f0e: 74 20 je 400f30 <phase_2+0x34>
400f10: e8 25 05 00 00 call 40143a <explode_bomb>
; 直接跳 400f30
400f15: eb 19 jmp 400f30 <phase_2+0x34>
; 获取上一个位置的值
400f17: 8b 43 fc mov -0x4(%rbx),%eax
; 上一个值加倍
400f1a: 01 c0 add %eax,%eax
; 和当前的相比,是否相等
400f1c: 39 03 cmp %eax,(%rbx)
; 相等直接跳 400f25
400f1e: 74 05 je 400f25 <phase_2+0x29>
; 不等就调用 explode_bomb
400f20: e8 15 05 00 00 call 40143a <explode_bomb>
; rbx往后移动
400f25: 48 83 c3 04 add $0x4,%rbx
; 判断 rbp 是否和 rbx 相等,不等的时候跳 400f17,即继续循环
400f29: 48 39 eb cmp %rbp,%rbx
400f2c: 75 e9 jne 400f17 <phase_2+0x1b>
; 否则跳400f3c
400f2e: eb 0c jmp 400f3c <phase_2+0x40>
; rbx 指向 rsp 的下一个位置
400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx
400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp
; 直接跳 400f17
400f3a: eb db jmp 400f17 <phase_2+0x1b>
400f3c: 48 83 c4 28 add $0x28,%rsp
400f40: 5b pop %rbx
400f41: 5d pop %rbp
400f42: c3 ret
read_six_numbers
函数内容:
; bobm.s
000000000040145c <read_six_numbers>:
40145c: 48 83 ec 18 sub $0x18,%rsp
; 将rsi的内容给rdx,而rdx是函数调用过程中第三个参数
; 后面调用了 sscanf ,而 sscanf 函数的第三个参数是解析字符串后的第一个值,
; 因此我们可以大胆猜测rdx指向的内存,是保存解析我们输入的字符串所代表的数字
401460: 48 89 f2 mov %rsi,%rdx
401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx
401467: 48 8d 46 14 lea 0x14(%rsi),%rax
40146b: 48 89 44 24 08 mov %rax,0x8(%rsp)
401470: 48 8d 46 10 lea 0x10(%rsi),%rax
401474: 48 89 04 24 mov %rax,(%rsp)
401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9
40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8
401480: be c3 25 40 00 mov $0x4025c3,%esi
401485: b8 00 00 00 00 mov $0x0,%eax
; 调试过程中我们发现第二个参数是 %d %d %d %d %d %d
40148a: e8 61 f7 ff ff call 400bf0 <__isoc99_sscanf@plt>
40148f: 83 f8 05 cmp $0x5,%eax
; sscanf函数返回值与0比较,小于等于5则调用explode_bomb
; 因此我们要输入6个数字,并以空格相隔
401492: 7f 05 jg 401499 <read_six_numbers+0x3d>
401494: e8 a1 ff ff ff call 40143a <explode_bomb>
401499: 48 83 c4 18 add $0x18,%rsp
40149d: c3 ret
因此通关payload
:1 2 4 8 16 32
phase_3
0000000000400f43 <phase_3>:
; 栈帧有24字节大小
400f43: 48 83 ec 18 sub $0x18,%rsp
; 函数调用的第4个参数
400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
; 函数调用的第3个参数
400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
; 动态调试过程发现 0x4025cf 执行的内容是 %d %d
400f51: be cf 25 40 00 mov $0x4025cf,%esi
400f56: b8 00 00 00 00 mov $0x0,%eax
400f5b: e8 90 fc ff ff call 400bf0 <__isoc99_sscanf@plt>
; 返回值要大于1.因此我们可以得出结论,程序将我们输入的两个数字保存到了栈上,
; 第一个数字距离栈顶8字节,第二个数字距离栈顶12字节
400f60: 83 f8 01 cmp $0x1,%eax
400f63: 7f 05 jg 400f6a <phase_3+0x27>
400f65: e8 d0 04 00 00 call 40143a <explode_bomb>
; 无符号比较,第一个数字与7比较,如果大于7,则跳 400fad,即调用explode_bomb
; 因此第一个数字要小于等于7
400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp)
400f6f: 77 3c ja 400fad <phase_3+0x6a>
; 第一个数字给eax
400f71: 8b 44 24 08 mov 0x8(%rsp),%eax
; 跳到 0x402470 + rax*8 的位置
; 理想状况下,最好能够直接跳到 400fc9,但是如果想要跳到这个位置,那么需要我们输入的是一个负数
; 可是输入负数后,上面的cmpl又过不了
; 我输入的是0,然后就跳过了这里
400f75: ff 24 c5 70 24 40 00 jmp *0x402470(,%rax,8)
; 将0xcf给eax
400f7c: b8 cf 00 00 00 mov $0xcf,%eax
; 跳 400fbe
400f81: eb 3b jmp 400fbe <phase_3+0x7b>
400f83: b8 c3 02 00 00 mov $0x2c3,%eax
400f88: eb 34 jmp 400fbe <phase_3+0x7b>
400f8a: b8 00 01 00 00 mov $0x100,%eax
400f8f: eb 2d jmp 400fbe <phase_3+0x7b>
400f91: b8 85 01 00 00 mov $0x185,%eax
400f96: eb 26 jmp 400fbe <phase_3+0x7b>
400f98: b8 ce 00 00 00 mov $0xce,%eax
400f9d: eb 1f jmp 400fbe <phase_3+0x7b>
400f9f: b8 aa 02 00 00 mov $0x2aa,%eax
400fa4: eb 18 jmp 400fbe <phase_3+0x7b>
400fa6: b8 47 01 00 00 mov $0x147,%eax
400fab: eb 11 jmp 400fbe <phase_3+0x7b>
400fad: e8 88 04 00 00 call 40143a <explode_bomb>
400fb2: b8 00 00 00 00 mov $0x0,%eax
400fb7: eb 05 jmp 400fbe <phase_3+0x7b>
400fb9: b8 37 01 00 00 mov $0x137,%eax
; 将我们输入的第二个数字与eax比较
400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax
; 相等就跳 400fc9 因此第二个数字必须是0xcf=207
400fc2: 74 05 je 400fc9 <phase_3+0x86>
400fc4: e8 71 04 00 00 call 40143a <explode_bomb>
400fc9: 48 83 c4 18 add $0x18,%rsp
400fcd: c3 ret
因此最终我们要先输入一个0~7的数字,再输入207
因此通关payload
:0 207
phase_4
000000000040100c <phase_4>:
; 栈帧有24字节大小
40100c: 48 83 ec 18 sub $0x18,%rsp
; 函数调用的第4个参数,实际上该地址对应我们输入的第二个数字
401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx
; 函数调用的第3个参数,实际上该地址对应我们输入的第一个数字
401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx
; 动态调试过程发现 0x4025cf 执行的内容是 %d %d
40101a: be cf 25 40 00 mov $0x4025cf,%esi
40101f: b8 00 00 00 00 mov $0x0,%eax
401024: e8 c7 fb ff ff call 400bf0 <__isoc99_sscanf@plt>
; 返回值要等于2,即我们输入的字符串中,必须包含且仅包含两个数字
; 第一个数字距离栈顶8字节,第二个数字距离栈顶12字节
401029: 83 f8 02 cmp $0x2,%eax
40102c: 75 07 jne 401035 <phase_4+0x29>
; 取第一个数字进行无符号比较
40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp)
; 小于等于 0xe = 14 则 跳40103a
401033: 76 05 jbe 40103a <phase_4+0x2e>
; 否则调用 explode_bomb,因此我们输入的第一个数字必须小于等于14
401035: e8 00 04 00 00 call 40143a <explode_bomb>
; 函数调用的第3个参数,值为0xe
40103a: ba 0e 00 00 00 mov $0xe,%edx
; 函数调用的第2个参数,值为0x0
40103f: be 00 00 00 00 mov $0x0,%esi
; 函数调用的第1个参数,值为我们输入的第一个数字
401044: 8b 7c 24 08 mov 0x8(%rsp),%edi
401048: e8 81 ff ff ff call 400fce <func4>
; 检查返回值
40104d: 85 c0 test %eax,%eax
; 如果不等于0,则跳401058
; 因此我们要使得func4函数返回值为0
40104f: 75 07 jne 401058 <phase_4+0x4c>
; 将0xc(%rsp)与0x0相比
401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
; 相等则跳40105d,实际上我们需要使得该位置变为0
; 该位置实际上是我们输入的第二个数字
401056: 74 05 je 40105d <phase_4+0x51>
401058: e8 dd 03 00 00 call 40143a <explode_bomb>
40105d: 48 83 c4 18 add $0x18,%rsp
401061: c3 ret
查看func4()
内容:
0000000000400fce <func4>:
; 栈帧有8字节大小
400fce: 48 83 ec 08 sub $0x8,%rsp
; 将传进来的第三个参数给eax
400fd2: 89 d0 mov %edx,%eax
; eax = eax - esi ,即 eax减去传过来的第二个参数
400fd4: 29 f0 sub %esi,%eax
; eax = ecx
400fd6: 89 c1 mov %eax,%ecx
; 逻辑右移31位后,ecx最低位是原先的最高位
400fd8: c1 e9 1f shr $0x1f,%ecx
400fdb: 01 c8 add %ecx,%eax
; 算术右移一位(除以2)
400fdd: d1 f8 sar %eax
; ecx = rax + rsi * 1
400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx
400fe2: 39 f9 cmp %edi,%ecx
; 如果 ecx 小于等于 edi,则跳400ff2(mov $0x0,%eax)
400fe4: 7e 0c jle 400ff2 <func4+0x24>
; 否则,edx = rcx -1
400fe6: 8d 51 ff lea -0x1(%rcx),%edx
; 递归调用
400fe9: e8 e0 ff ff ff call 400fce <func4>
; eax = eax + eax;
400fee: 01 c0 add %eax,%eax
; 跳401007,即函数返回
400ff0: eb 15 jmp 401007 <func4+0x39>
400ff2: b8 00 00 00 00 mov $0x0,%eax
400ff7: 39 f9 cmp %edi,%ecx
; 如果 edi 大于等于 ecx,则跳 401007(add $0x8,%rsp)
400ff9: 7d 0c jge 401007 <func4+0x39>
; 否则,esi = rcx + 1
400ffb: 8d 71 01 lea 0x1(%rcx),%esi
; 递归调用
400ffe: e8 cb ff ff ff call 400fce <func4>
; eax = rax + rax * 1 + 1
401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401007: 48 83 c4 08 add $0x8,%rsp
40100b: c3 ret
对应的C代码大致如下:
unsigned func4(unsigned di, unsigned si, unsigned dx){
ax = dx - si;
cx = ax;
// 对cx无符号右移31位
// 即 如果dx小于si,则cx=1
if(cx & 0x80000000){
cx = 1;
} else{
cx = 0;
}
ax = cx + ax;
// 算数右移
ax = ax >> 1;
cx = ax + si;
// 2~14行,等价于 cx=(|dx-si|+dx-si)/2+si
// |dx-si| 要么是0,要么是1
if(cx == di)return 0;
if(cx<di){
si = cx + 1;
return 2 * func(di,si,dx) + 1;
}else{
dx = cx - 1;
ax = return 2 * func(di,si,dx);
}
}
假设一开始我们输入的数字是x,我们来看一下调用栈
f(x,0,14)
|-- 0 : if x == 7
|-- 2 * f(x,8,14) + 1: if x > 7
|-- 2 * f(x,0,6) : if x < 7
再通关条件是:
-
输入的第一个数字是小于等于14
-
输入的第二个数字是0
-
func4函数返回值是0
因此满足条件的一个payload
是7 0
(当然不止这一个,满足func4
返回值是0的都可以)
因此通关payload
:7 0
phase_5
0000000000401062 <phase_5>:
401062: 53 push %rbx
401063: 48 83 ec 20 sub $0x20,%rsp
401067: 48 89 fb mov %rdi,%rbx
40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
401071: 00 00
401073: 48 89 44 24 18 mov %rax,0x18(%rsp)
401078: 31 c0 xor %eax,%eax
40107a: e8 9c 02 00 00 call 40131b <string_length>
; 输入的长度必须是6
40107f: 83 f8 06 cmp $0x6,%eax
401082: 74 4e je 4010d2 <phase_5+0x70>
401084: e8 b1 03 00 00 call 40143a <explode_bomb>
401089: eb 47 jmp 4010d2 <phase_5+0x70>
; ecx = rbx + rax * 1
; 循环开始 rax 从0~5变化,实际上相当于循环获取数组中的内容,然后每个元素和0xf进行与操作
; 而该数组用rbx地址指定,rbx又由rdi指定,rdi指向我们输入的字符串
; 因此实际上是遍历我们的字符串,依次把每个元素与0xf进行与操作,然后作为偏移地址
40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx
40108f: 88 0c 24 mov %cl,(%rsp)
401092: 48 8b 14 24 mov (%rsp),%rdx
401096: 83 e2 0f and $0xf,%edx
;rdx作为偏移地址,获取 0x4024b0 开始偏移rdx的内容,写入到edx中
;0x4024b0的内容为maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?
401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx
; 然后再写入到rsp+rax+0x10中
4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1)
4010a4: 48 83 c0 01 add $0x1,%rax
4010a8: 48 83 f8 06 cmp $0x6,%rax
; 如果rax-6不等于0,则跳至40108b(循环结束)
4010ac: 75 dd jne 40108b <phase_5+0x29>
; 将距离栈顶0x16个字节的内容赋值为0
4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp)
; 将$0x40245e给esi
4010b3: be 5e 24 40 00 mov $0x40245e,%esi
; 将距离栈顶0x10个字节的字符串地址给rdi
4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi
; 调用字符串是否相等,实际上是判断距离栈顶0x10个字节的字符串和$0x40245e所在的字符串是否相等
; 动态调试过程中,该值我们可以查到为:flyers
4010bd: e8 76 02 00 00 call 401338 <strings_not_equal>
4010c2: 85 c0 test %eax,%eax
4010c4: 74 13 je 4010d9 <phase_5+0x77>
4010c6: e8 6f 03 00 00 call 40143a <explode_bomb>
4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
4010d0: eb 07 jmp 4010d9 <phase_5+0x77>
4010d2: b8 00 00 00 00 mov $0x0,%eax
4010d7: eb b2 jmp 40108b <phase_5+0x29>
4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax
4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax
4010e5: 00 00
4010e7: 74 05 je 4010ee <phase_5+0x8c>
4010e9: e8 42 fa ff ff call 400b30 <__stack_chk_fail@plt>
4010ee: 48 83 c4 20 add $0x20,%rsp
4010f2: 5b pop %rbx
4010f3: c3 ret
最主要的是循环体中的内容。
为了从maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?
中获得flyers
字符串,我们要根据下表依次构建索引
m a d u i e r s n f o t v b y l
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
需要构建的索引是
9 15 14 5 6 7
也就是说,我们输入的字符串中,每次和0xf
相与后,得到的是上述的值,观察ascii表,我们可以发现很多符合条件的,一个符合条件的是
9?>567
他们的ascii码16进制表示分别为
0x39 0x3F 0x3E 0x35 0x36 0x37
因此通关payload
:9?>567
phase_6
这一关卡源码太多了,我实在没有完全看明白,就算借助了IDA,也只是阅读了前半部分(步骤5和步骤6看网上别人分析的):
- 将输入的字符串解析成6个数字
- 判断每个数字是否都小于等于6
- 判断6个数字中是否有重复值
- 将每个数字都变成“对应的相反数加上7”
- 遍历每个数字,根据每个数字的值作为偏移量,遍历0x6032d0指向的链表,获取指定偏移量后对应的链表节点的地址,将该地址写入到栈上某个地方(可以理解成一个数组)
- 遍历上个步骤得到的数组,其元素是若干个地址,要求当前元素指向的内容小于上个元素指向的内容,即我们要想办法构造一个逆序序列
而链表0x6032d0
内容如下:
+----------|---------+
0x6032d0 | 332 |0x6032e0 |
+----------|---------+
+----------|---------+
0x6032e0 | 168 |0x6032f0 |
+----------|---------+
+----------|---------+
0x6032f0 | 924 |0x603300 |
+----------|---------+
+----------|---------+
0x603300 | 691 |0x603310 |
+----------|---------+
+----------|---------+
0x603310 | 477 |0x603320 |
+----------|---------+
+----------|---------+
0x603320 | 443 | 0x0 |
+----------|---------+
为了使得第一个元素指向的值是最大的,我们应该将第一个元素的值设置为0x6032f0
,而该地址相对链首而言是第3个。第二个元素的值为0x603300,相对链首而言是第4个,即
3 4 5 6 1 2
上述再取相反数,加上7,得到我们原始输入:
4 3 2 1 6 5
下面是一些注释
00000000004010f4 <phase_6>:
4010f4: 41 56 push %r14
4010f6: 41 55 push %r13
4010f8: 41 54 push %r12
4010fa: 55 push %rbp
4010fb: 53 push %rbx
; 开辟80字节空间
4010fc: 48 83 ec 50 sub $0x50,%rsp
401100: 49 89 e5 mov %rsp,%r13
401103: 48 89 e6 mov %rsp,%rsi
; 将我们输入的字符串解析为6个数
401106: e8 51 03 00 00 call 40145c <read_six_numbers>
; 经过上面的函数调用,rsp指向的地址中,保存的是若干个整数
40110b: 49 89 e6 mov %rsp,%r14
40110e: 41 bc 00 00 00 00 mov $0x0,%r12d
401114: 4c 89 ed mov %r13,%rbp
401117: 41 8b 45 00 mov 0x0(%r13),%eax
40111b: 83 e8 01 sub $0x1,%eax
; 如果eax-1后,小于等于5,则跳转401128,否则调用explode_bomb
; 因此输入的数字要小于等于6
40111e: 83 f8 05 cmp $0x5,%eax
401121: 76 05 jbe 401128 <phase_6+0x34>
401123: e8 12 03 00 00 call 40143a <explode_bomb>
401128: 41 83 c4 01 add $0x1,%r12d
40112c: 41 83 fc 06 cmp $0x6,%r12d
; 如果r12加一后是6,则跳401153
401130: 74 21 je 401153 <phase_6+0x5f>
401132: 44 89 e3 mov %r12d,%ebx
401135: 48 63 c3 movslq %ebx,%rax
401138: 8b 04 84 mov (%rsp,%rax,4),%eax
; 实际上这一块完成的是:要输入的6个数字各不相同
40113b: 39 45 00 cmp %eax,0x0(%rbp)
40113e: 75 05 jne 401145 <phase_6+0x51>
401140: e8 f5 02 00 00 call 40143a <explode_bomb>
401145: 83 c3 01 add $0x1,%ebx
401148: 83 fb 05 cmp $0x5,%ebx
40114b: 7e e8 jle 401135 <phase_6+0x41>
40114d: 49 83 c5 04 add $0x4,%r13
401151: eb c1 jmp 401114 <phase_6+0x20>
401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi
401158: 4c 89 f0 mov %r14,%rax
40115b: b9 07 00 00 00 mov $0x7,%ecx
401160: 89 ca mov %ecx,%edx
; 实际上这一段完成的是将输入的每个数字变为 7 - 数字
401162: 2b 10 sub (%rax),%edx
401164: 89 10 mov %edx,(%rax)
401166: 48 83 c0 04 add $0x4,%rax
40116a: 48 39 f0 cmp %rsi,%rax
40116d: 75 f1 jne 401160 <phase_6+0x6c>
40116f: be 00 00 00 00 mov $0x0,%esi
401174: eb 21 jmp 401197 <phase_6+0xa3>
401176: 48 8b 52 08 mov 0x8(%rdx),%rdx
40117a: 83 c0 01 add $0x1,%eax
40117d: 39 c8 cmp %ecx,%eax
40117f: 75 f5 jne 401176 <phase_6+0x82>
401181: eb 05 jmp 401188 <phase_6+0x94>
401183: ba d0 32 60 00 mov $0x6032d0,%edx
401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2)
40118d: 48 83 c6 04 add $0x4,%rsi
401191: 48 83 fe 18 cmp $0x18,%rsi
401195: 74 14 je 4011ab <phase_6+0xb7>
401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx
; 判断是不是小于等于1,是的话就跳转到401183
40119a: 83 f9 01 cmp $0x1,%ecx
40119d: 7e e4 jle 401183 <phase_6+0x8f>
40119f: b8 01 00 00 00 mov $0x1,%eax
4011a4: ba d0 32 60 00 mov $0x6032d0,%edx
4011a9: eb cb jmp 401176 <phase_6+0x82>
4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx
4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax
4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi
4011ba: 48 89 d9 mov %rbx,%rcx
4011bd: 48 8b 10 mov (%rax),%rdx
4011c0: 48 89 51 08 mov %rdx,0x8(%rcx)
4011c4: 48 83 c0 08 add $0x8,%rax
4011c8: 48 39 f0 cmp %rsi,%rax
4011cb: 74 05 je 4011d2 <phase_6+0xde>
4011cd: 48 89 d1 mov %rdx,%rcx
; 上述代码相当于把数字作为偏移量,距离链首指定偏移量后的节点的地址, copy 到另外一个地方
4011d0: eb eb jmp 4011bd <phase_6+0xc9>
4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx)
4011d9: 00
4011da: bd 05 00 00 00 mov $0x5,%ebp
4011df: 48 8b 43 08 mov 0x8(%rbx),%rax
4011e3: 8b 00 mov (%rax),%eax
; 将当前指针指向的元素和上一个指针指向的元素相比
4011e5: 39 03 cmp %eax,(%rbx)
4011e7: 7d 05 jge 4011ee <phase_6+0xfa>
4011e9: e8 4c 02 00 00 call 40143a <explode_bomb>
4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx
4011f2: 83 ed 01 sub $0x1,%ebp
4011f5: 75 e8 jne 4011df <phase_6+0xeb>
4011f7: 48 83 c4 50 add $0x50,%rsp
4011fb: 5b pop %rbx
4011fc: 5d pop %rbp
4011fd: 41 5c pop %r12
4011ff: 41 5d pop %r13
401201: 41 5e pop %r14
401203: c3 ret
因此通关payload
:4 3 2 1 6 5
最终完整通关payload
:
Border relations with Canada have never been better.
1 2 4 8 16 32
0 207
7 0
9?>567
4 3 2 1 6 5
隐藏关卡
竟然还有隐藏关卡,大意了大意了,当时压根没想到。查看bomb.c
文件,我们发现每个phase
结束后,都会调用phase_defused
这个函数,结合bomb.s
文件,在这个函数中先是判断输入的字符串个数是不是6,是的话再继续判断0x603870
指向的内容中,前两个是否是数字且第三个字符串是不是DrEvil
,是的话即可进入隐藏关卡,因此我们要在第四关输入的字符串后面加上DrEvil
,然后第6关结束后,自动进入隐藏关卡。
secret_phase
函数如下
0000000000401242 <secret_phase>:
401242: 53 push %rbx
401243: e8 56 02 00 00 call 40149e <read_line>
; 将字符串解析成10进制数字
401248: ba 0a 00 00 00 mov $0xa,%edx
40124d: be 00 00 00 00 mov $0x0,%esi
401252: 48 89 c7 mov %rax,%rdi
401255: e8 76 f9 ff ff call 400bd0 <strtol@plt>
; 返回值给rbx
40125a: 48 89 c3 mov %rax,%rbx
; 我们输入的数字减一后如果大于0x3e8,就explode_bomb
40125d: 8d 40 ff lea -0x1(%rax),%eax
401260: 3d e8 03 00 00 cmp $0x3e8,%eax
401265: 76 05 jbe 40126c <secret_phase+0x2a>
401267: e8 ce 01 00 00 call 40143a <explode_bomb>
; 实际上输入的第一个数字作为fun7的第二个参数
40126c: 89 de mov %ebx,%esi
; 将0x6030f0作为fun7的第一个参数,该地址对应的内容是0x24h
40126e: bf f0 30 60 00 mov $0x6030f0,%edi
401273: e8 8c ff ff ff call 401204 <fun7>
; 返回值得是2
401278: 83 f8 02 cmp $0x2,%eax
40127b: 74 05 je 401282 <secret_phase+0x40>
40127d: e8 b8 01 00 00 call 40143a <explode_bomb>
401282: bf 38 24 40 00 mov $0x402438,%edi
401287: e8 84 f8 ff ff call 400b10 <puts@plt>
40128c: e8 33 03 00 00 call 4015c4 <phase_defused>
401291: 5b pop %rbx
401292: c3 ret
我们来看fun7函数
0000000000401204 <fun7>:
401204: 48 83 ec 08 sub $0x8,%rsp
401208: 48 85 ff test %rdi,%rdi
; 如果第一个参数是0,则跳转401238,实际上是返回0xffffffff
40120b: 74 2b je 401238 <fun7+0x34>
; 获取第一个参数指向的内容(最开始调用的时候,该值是0x24h)
40120d: 8b 17 mov (%rdi),%edx
; 上面获取到的与传进来的第二个参数(最开始调用时候,该值是我们输入的第一个数字)相比,
; 如果大于等于第二个参数,则跳转401220
40120f: 39 f2 cmp %esi,%edx
401211: 7e 0d jle 401220 <fun7+0x1c>
401213: 48 8b 7f 08 mov 0x8(%rdi),%rdi
401217: e8 e8 ff ff ff call 401204 <fun7>
40121c: 01 c0 add %eax,%eax
40121e: eb 1d jmp 40123d <fun7+0x39>
401220: b8 00 00 00 00 mov $0x0,%eax
401225: 39 f2 cmp %esi,%edx
401227: 74 14 je 40123d <fun7+0x39>
401229: 48 8b 7f 10 mov 0x10(%rdi),%rdi
40122d: e8 d2 ff ff ff call 401204 <fun7>
401232: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401236: eb 05 jmp 40123d <fun7+0x39>
; 返回0xffffffff
401238: b8 ff ff ff ff mov $0xffffffff,%eax
40123d: 48 83 c4 08 add $0x8,%rsp
401241: c3 ret
上面的代码大致等效于
fun7(int *rdi, int rsi){
if(rdi == 0)return 0xffffffff;
if(*rdi <= rsi){
if(*rdi == rsi){
return 0;
}else{
rdi = *(rdi + 16);
return 2 * fun7(rdi, rsi) + 1;
}
}else{
rdi = *(rdi + 8);
return 2 * fun7(rdi, rsi);
}
}
简单分析一下,如果我们不理会return 0xffffffff;
这个分支,我们很快就会发现,函数只会有三种分支,一个是0,另一个是递归后乘以2再加1,最后一个是递归后乘以2.
为了使得最终返回2,我们可以使得上一次递归返回值是1,以2*f
的方式返回。
为了使得上一次递归返回值是1,我们可以使得上上一次递归返回值是0.以2*f+1
的方式返回。
关键是如何使得上上一次递归返回值是0.
我们先看一下0x6030f0
开始的内容
0x6030f0 <n1>: 0x0024 0x0000 0x0000 0x0000 0x3110 0x0060 0x0000 0x0000
0x603100 <n1+16>: 0x3130 0x0060 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x603110 <n21>: 0x0008 0x0000 0x0000 0x0000 0x3190 0x0060 0x0000 0x0000
0x603120 <n21+16>: 0x3150 0x0060 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x603130 <n22>: 0x0032 0x0000 0x0000 0x0000 0x3170 0x0060 0x0000 0x0000
0x603140 <n22+16>: 0x31b0 0x0060 0x0000 0x0000 0x0000 0x0000 0x0000 0x0000
0x603150 <n32>: 0x0016 0x0000 0x0000 0x0000 0x3270 0x0060 0x0000 0x0000
假设一开始我们输入的数字是x,我们来看一下调用栈
f(0x6030f0,x)
|-- 0 : if x == 0x24h
|-- 2 * f(0x603130,x) + 1: if x > 0x24h
|-- 2 * f(0x603110,x) : if x < 0x24h
我们要走f(0x603110,x)这个分支
f(0x603110,x)
|-- 0 : if x == 0x8h
|-- 2 * f(0x603150,x) + 1: if x > 0x8h
|-- 2 * f(0x603190,x) : if x < 0x8h
我们要走f(0x603150,x)这个分支
f(0x603150,x)
|-- 0 : if x == 0x16h
|-- 2 * f(0x603230,x) + 1: if x > 0x16h
|-- 2 * f(0x603270,x) : if x < 0x16h
因此我们只要满足x == 0x16h即可
最终完整payload:
Border relations with Canada have never been better.
1 2 4 8 16 32
0 207
7 0 DrEvil
9?>567
4 3 2 1 6 5
22
总结
阅读汇编实际上难度不大,难的是如何将汇编代码反推其逻辑功能,如果是C语言代码,一般可以通过变量名称或者代码结构反推逻辑功能,但是来到了汇编代码层,就困难得多了。
分析汇编过程动静结合的方式尤为重要,对于新手,不建议直接用IDA,因为实际上这些可视化反汇编软件存在一些错误,这些可能给新手带来很多困惑。
强烈推荐安装pwbdbg
本文由「黄阿信」创作,创作不易,请多支持。
如果您觉得本文写得不错,那就点一下「赞赏」请我喝杯咖啡~
商业转载请联系作者获得授权,非商业转载请附上原文出处及本链接。
关注公众号,获取最新动态!