bomblab
复习的时候一定先读上面这篇Blog,关于gdb指令的内容很详尽
具体的过程在这篇Blog里非常详尽,且大一下学期在学校的计算机系统基础课上已经做过BombLab,本篇只记录一些需要注意的细节
Phase_1
1
2
3
4
5
6
7
8
9
10
11
0000000000400ee0 <phase_1>:
400ee0: 48 83 ec 08 subq $0x8, %rsp
400ee4: be 00 24 40 00 movl $0x402400, %esi # imm = 0x402400
400ee9: e8 4a 04 00 00 callq 0x401338 <strings_not_equal>
400eee: 85 c0 testl %eax, %eax
400ef0: 74 05 je 0x400ef7 <phase_1+0x17>
400ef2: e8 43 05 00 00 callq 0x40143a <explode_bomb>
400ef7: 48 83 c4 08 addq $0x8, %rsp
400efb: c3 retq
1. 寄存器的意义
一年前上csapp的时候,最头疼的就是x86中各种各样的寄存器,记不清它们的意义,这里只列出一些lab中用到的寄存器的意义:
1)R与E
- R开头的寄存器如RAX(64位),包含了E开头的寄存器如EAX(低32位),而EAX又包含了AX(低16位)
- 比如对RDX寄存器,可以通过以下方式访问其部分位:
- %rbp: 基址指针寄存器,通常用于标记函数栈帧的底部
- %rbx: 基址寄存器,在这段代码中作为数组指针使用
- %rsp: 栈指针寄存器,指向栈顶
- %rsi: 第二参数寄存器,用于传递函数参数
- %eax: 累加器寄存器的低32位,用于计算和比较
2.Phase_1
1)%esi作为函数调用的第2个参数寄存器,存储了0x402400这个值,一看就是个地址,所以要用x/s指令查看这个地址所存放的内容是什么(就是phase1的答案)
2)而string_not_equal
函数的第一个参数,自然就是我们输入的字符串,通过testl %eax, %eax
和je
这两个指令来判断我们输入的字符串是否等于0x402400这个地址里存放的字符串
Phase_2
第二关比较复杂:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
0x0000000000400efc <+0>: push %rbp ; #保存基址指针寄存器rbp的值到栈上
0x0000000000400efd <+1>: push %rbx ; #保存基址寄存器rbx的值到栈上
0x0000000000400efe <+2>: sub $0x28,%rsp ; #在栈上分配40字节(0x28)的空间
0x0000000000400f02 <+6>: mov %rsp,%rsi ; #将栈顶指针复制给第二参数寄存器rsi
0x0000000000400f05 <+9>: call 0x40145c <read_six_numbers> ; #调用函数读取6个数字到栈上
0x0000000000400f0a <+14>: cmpl $0x1,(%rsp) ; #比较栈顶位置的值是否等于1
0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52> ; #如果等于1,跳转到+52处
0x0000000000400f10 <+20>: call 0x40143a <explode_bomb> ; #否则,调用炸弹函数(游戏失败)
0x0000000000400f15 <+25>: jmp 0x400f30 <phase_2+52> ; #跳转到+52处(实际上这行代码不会执行)
#检查当前元素是否等于前一个元素的2倍
0x0000000000400f17 <+27>: mov -0x4(%rbx),%eax ; #将rbx指针前4字节的值(前一个数字)存入eax
0x0000000000400f1a <+30>: add %eax,%eax ; #将eax的值翻倍
0x0000000000400f1c <+32>: cmp %eax,(%rbx) ; #比较当前rbx指向的值是否等于eax(翻倍的前一个数)
0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41> ; #如果相等,跳转到+41处
0x0000000000400f20 <+36>: call 0x40143a <explode_bomb> ; #否则,调用炸弹函数(游戏失败)
#这部分检查是否遍历完了输入的6个数字
0x0000000000400f25 <+41>: add $0x4,%rbx ; #rbx指针增加4字节(指向下一个数字)
0x0000000000400f29 <+45>: cmp %rbp,%rbx ; #比较rbx是否等于rbp(是否达到数组末尾)
0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27> ; #如果不等于,跳回+27处继续循环
0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64> ; #跳转到+64处(函数结尾)
#从这里进入循环
0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx ; #将第二个数字的地址加载到rbx
0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp ; #将数组结束后的地址加载到rbp(栈顶+24字节)
0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27> ; #跳转到+27处开始循环
#结束
0x0000000000400f3c <+64>: add $0x28,%rsp ; #恢复栈指针(释放之前分配的空间)
0x0000000000400f40 <+68>: pop %rbx ; #恢复rbx的值
0x0000000000400f41 <+69>: pop %rbp ; #恢复rbp的值
0x0000000000400f42 <+70>: ret ; #返回,函数结束
这一关的逻辑是:
- 读取6个数字到栈上
- 检查第一个数字是否为1,不是则引爆炸弹
- 检查后续每个数字是否是前一个数字的2倍,不是则引爆炸弹
寄存器
1. %rsp
: 在这一关里保存了我们输入的整数数组的起始地址,这一点可以由以下地方看出
- 在函数开始时,执行了 sub $0x28,%rsp 指令,在栈上分配了空间
- 然后执行 mov %rsp,%rsi 指令,将栈指针(指向栈顶,也就是分配空间后的栈顶)复制给了 %rsi 寄存器
- 接着调用 read_six_numbers 函数,并将 %rsi 作为参数传递,这个函数会将读取的6个数字存储到 %rsi 指向的地址,即 %rsp 指向的位置
- 在后续的代码中,通过 cmpl $0x1,(%rsp) 指令直接比较栈顶位置的值,证明第一个元素确实存储在 %rsp 指向的位置
2. 0x4(%rsp)
这个地址指向的就是数组的第二个元素,而0x18(%rsp)
就是数组的结束地址(因为int类型的字节数是4bytes)
Answer
于是答案就是1 2 4 8 16 32这个等比数列
Phase_3
1)汇编代码解释
1. 函数准备和输入处理部分
1
2
3
4
5
6
0x0000000000400f43 <+0>: sub $0x18,%rsp ; #分配24字节的栈空间
0x0000000000400f47 <+4>: lea 0xc(%rsp),%rcx ; #将栈上偏移12字节的地址加载到rcx (第2个变量的地址)
0x0000000000400f4c <+9>: lea 0x8(%rsp),%rdx ; #将栈上偏移8字节的地址加载到rdx (第1个变量的地址)
0x0000000000400f51 <+14>: mov $0x4025cf,%esi ; #加载格式字符串的地址到esi (可能是"%d %d"格式)
0x0000000000400f56 <+19>: mov $0x0,%eax ; #eax置0 (sscanf调用前的常规设置,表示无向量寄存器使用)
0x0000000000400f5b <+24>: call 0x400bf0 <__isoc99_sscanf@plt> ; #调用sscanf读取输入
2. 输入验证部分
1
2
3
0x0000000000400f60 <+29>: cmp $0x1,%eax ; #比较eax(sscanf返回值,其返回值是读取的输入变量的个数)和1
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39> ; #如果sscanf读取了多于1个值,跳转到+39处继续执行
0x0000000000400f65 <+34>: call 0x40143a <explode_bomb> ; #否则炸弹爆炸(读取不足2个值)
3. 第一个输入值检查和跳转表
1
2
3
4
0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp) ; #比较第一个输入值和7
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106> ; #如果大于7,跳转到+106处(爆炸)
0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax ; #将第一个输入值加载到eax
0x0000000000400f75 <+50>: jmp *0x402470(,%rax,8) ; #使用第一个输入值作为跳转表的索引,跳转到对应位置
4. 根据第一个输入值设置目标值的不同情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
0x0000000000400f7c <+57>: mov $0xcf,%eax ; #输入0时:设置eax为0xcf(207)
0x0000000000400f81 <+62>: jmp 0x400fbe <phase_3+123> ; #跳到比较部分
0x0000000000400f83 <+64>: mov $0x2c3,%eax ; #输入1时:设置eax为0x2c3(707)
0x0000000000400f88 <+69>: jmp 0x400fbe <phase_3+123> ; #跳到比较部分
0x0000000000400f8a <+71>: mov $0x100,%eax ; #输入2时:设置eax为0x100(256)
0x0000000000400f8f <+76>: jmp 0x400fbe <phase_3+123> ; #跳到比较部分
0x0000000000400f91 <+78>: mov $0x185,%eax ; #输入3时:设置eax为0x185(389)
0x0000000000400f96 <+83>: jmp 0x400fbe <phase_3+123> ; #跳到比较部分
0x0000000000400f98 <+85>: mov $0xce,%eax ; #输入4时:设置eax为0xce(206)
0x0000000000400f9d <+90>: jmp 0x400fbe <phase_3+123> ; #跳到比较部分
0x0000000000400f9f <+92>: mov $0x2aa,%eax ; #输入5时:设置eax为0x2aa(682)
0x0000000000400fa4 <+97>: jmp 0x400fbe <phase_3+123> ; #跳到比较部分
0x0000000000400fa6 <+99>: mov $0x147,%eax ; #输入6时:设置eax为0x147(327)
0x0000000000400fab <+104>: jmp 0x400fbe <phase_3+123> ; #跳到比较部分
0x0000000000400fad <+106>: call 0x40143a <explode_bomb> ; #输入超过7时爆炸(前面的ja指令跳转到这里)
0x0000000000400fb2 <+111>: mov $0x0,%eax ; #爆炸后不会到这里,但逻辑上是设置eax为0
0x0000000000400fb7 <+116>: jmp 0x400fbe <phase_3+123> ; #跳到比较部分
0x0000000000400fb9 <+118>: mov $0x137,%eax ; #输入7时:设置eax为0x137(311)
5. 比较第二个输入值和最终验证
1
2
3
0x0000000000400fbe <+123>: cmp 0xc(%rsp),%eax ; #比较第二个输入值和eax中的目标值
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134> ; #如果相等,跳到最后(成功)
0x0000000000400fc4 <+129>: call 0x40143a <explode_bomb> ; #否则炸弹爆炸
6. 函数完成部分
1
2
0x0000000000400fc9 <+134>: add $0x18,%rsp ; #恢复栈指针
0x0000000000400fcd <+138>: ret ; #返回
2)总体功能分析:
这个函数phase_3
的功能是检查用户输入的两个整数值是否符合特定条件:
- 必须输入两个值
- 第一个值必须是0-7之间的数字
- 第二个值必须等于一个与第一个值对应的特定数字:
- 如果第一个值是0,第二个值必须是207
- 如果第一个值是1,第二个值必须是707
- 如果第一个值是2,第二个值必须是256
- 如果第一个值是3,第二个值必须是389
- 如果第一个值是4,第二个值必须是206
- 如果第一个值是5,第二个值必须是682
- 如果第一个值是6,第二个值必须是327
- 如果第一个值是7,第二个值必须是311
所以有8种可能的正确输入组合来”拆除”这个炸弹。
3)细节解释
1. 为什么栈上偏移8字节的地址是第1个变量的地址?
答:在这个函数中,栈的使用方式如下:
首先,函数一开始执行 sub $0x18,%rsp
,分配了24字节的栈空间。这意味着栈指针(rsp)向下移动了24字节。
在x86-64架构中,栈是向低地址方向增长的。当我们分配栈空间后,rsp指向这个新分配空间的底部。
当函数调用sscanf
时,它需要为两个输入值准备内存位置:
- 第一个变量存储在 8(%rsp) 位置(即rsp+8的地址)
- 第二个变量存储在 12(%rsp) 位置(即rsp+12的地址)
2)汇编中的switch
语句——jmp *0x402470(,%rax,8)
这条指令执行了一个”计算跳转”(computed jump)或”跳转表”(jump table)操作,具体步骤是:
- 计算地址:0x402470 + (rax * 8)
- 如果rax = 0,地址为:0x402470
- 如果rax = 1,地址为:0x402478
- 如果rax = 2,地址为:0x402480
…以此类推
- 从计算出的地址加载64位值(一个内存地址)
- 跳转到加载的地址
Phase_4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
0x000000000040100c <+0>: sub $0x18,%rsp
0x0000000000401010 <+4>: lea 0xc(%rsp),%rcx
0x0000000000401015 <+9>: lea 0x8(%rsp),%rdx
0x000000000040101a <+14>: mov $0x4025cf,%esi
#x/s 0x4025cf,显示0x4025cf:"%d %d",说明输入两个整数
0x000000000040101f <+19>: mov $0x0,%eax
0x0000000000401024 <+24>: call 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000401029 <+29>: cmp $0x2,%eax
0x000000000040102c <+32>: jne 0x401035 <phase_4+41>
#第一个输入不能等于14
0x000000000040102e <+34>: cmpl $0xe,0x8(%rsp)
0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46>
0x0000000000401035 <+41>: call 0x40143a <explode_bomb>
0x000000000040103a <+46>: mov $0xe,%edx # 将立即数0xe(十进制14)放入edx寄存器
0x000000000040103f <+51>: mov $0x0,%esi # 将立即数0x0(0)放入esi寄存器
0x0000000000401044 <+56>: mov 0x8(%rsp),%edi # 将栈指针偏移8字节的内存位置的值加载到edi寄存器
0x0000000000401048 <+60>: call 0x400fce <func4> # 调用func4函数
0x000000000040104d <+65>: test %eax,%eax # 测试eax寄存器的值与自身进行AND操作,检查结果是否为零
0x000000000040104f <+67>: jne 0x401058 <phase_4+76> # 如果结果不为零,跳转到phase_4+76位置
0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp) # 比较栈指针偏移12字节的内存位置的值与0
0x0000000000401056 <+74>: je 0x40105d <phase_4+81> # 如果相等,跳转到phase_4+81位置
0x0000000000401058 <+76>: call 0x40143a <explode_bomb> # 调用explode_bomb函数(炸弹爆炸)
0x000000000040105d <+81>: add $0x18,%rsp # 栈指针增加0x18(24)字节
0x0000000000401061 <+85>: ret # 函数返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
0x0000000000400fce <+0>: sub $0x8,%rsp # 栈指针减8,为函数调用保留空间
0x0000000000400fd2 <+4>: mov %edx,%eax # 将edx(第三个参数)复制到eax
0x0000000000400fd4 <+6>: sub %esi,%eax # eax = eax - esi (即 eax = edx - esi)
0x0000000000400fd6 <+8>: mov %eax,%ecx # 将eax的值复制到ecx
0x0000000000400fd8 <+10>: shr $0x1f,%ecx # 将ecx右移31位(获取符号位)
0x0000000000400fdb <+13>: add %ecx,%eax # eax = eax + ecx (处理负数情况)
0x0000000000400fdd <+15>: sar %eax # 算术右移eax(相当于除以2)
0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx # ecx = rax + rsi (计算中间值)
0x0000000000400fe2 <+20>: cmp %edi,%ecx # 比较ecx和edi(第一个参数)
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> # 如果ecx <= edi,跳转到+36位置
0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx # 如果ecx > edi,设置edx = rcx - 1
0x0000000000400fe9 <+27>: call 0x400fce <func4> # 递归调用func4(edi, esi, rcx-1)
0x0000000000400fee <+32>: add %eax,%eax # eax = eax + eax (返回值乘以2)
0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57> # 跳转到函数结尾
0x0000000000400ff2 <+36>: mov $0x0,%eax # 设置eax=0(返回值)
0x0000000000400ff7 <+41>: cmp %edi,%ecx # 比较ecx和edi
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> # 如果ecx >= edi,跳到函数结尾
0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi # 如果ecx < edi,设置esi = rcx + 1
0x0000000000400ffe <+48>: call 0x400fce <func4> # 递归调用func4(edi, rcx+1, edx)
0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax # eax = 2*rax + 1
0x0000000000401007 <+57>: add $0x8,%rsp # 恢复栈指针
0x000000000040100b <+61>: ret # 函数返回
func4
实现了一个二分搜索算法,它接收三个参数:
- edi:目标值(我们要查找的值)
- esi:搜索范围的下限
- edx:搜索范围的上限
函数的逻辑是:
- 计算中间值 mid = (上限 - 下限) / 2 + 下限
- 如果 mid == 目标值,返回0
- 如果 mid > 目标值,递归搜索左半部分,返回值乘以2
- 如果 mid < 目标值,递归搜索右半部分,返回值乘以2再加1
结合前面的phase_4,我们知道:
函数调用参数是func4(第一个输入值, 0, 14)
要通过炸弹,函数必须返回0
因此,输入值必须使func4返回0,这意味着在某个递归层次上,计算出的中间值必须等于输入值,根据二分搜索的性质,输入值可能是0、7或14,但根据phase_4的反汇编代码,我们知道不能取14(0xe),所以我们输入7,0即可
Phase_5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
0x0000000000401062 <+0>: push %rbx #保存 rbx 寄存器
0x0000000000401063 <+1>: sub $0x20,%rsp #分配 32 字节栈空间
0x0000000000401067 <+5>: mov %rdi,%rbx #将第一个参数(输入字符串)保存到 rbx
0x000000000040106a <+8>: mov %fs:0x28,%rax #读取栈保护金丝雀值
0x0000000000401073 <+17>: mov %rax,0x18(%rsp) #将金丝雀值存入栈中
0x0000000000401078 <+22>: xor %eax,%eax #清零 eax (同时设置 rax=0)
0x000000000040107a <+24>: call 0x40131b <string_length> #调用 string_length(输入)
0x000000000040107f <+29>: cmp $0x6,%eax #比较长度是否为 6
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112> #如果是6则跳转
0x0000000000401084 <+34>: call 0x40143a <explode_bomb> #否则引爆炸弹
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112> #跳转到循环初始化
#循环体
0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx #循环开始: 读取输入字符[rbx+rax]到 ecx
0x000000000040108f <+45>: mov %cl,(%rsp) #将字符存入栈顶
0x0000000000401092 <+48>: mov (%rsp),%rdx #重新读取到 rdx (可能为了扩展字节)
0x0000000000401096 <+52>: and $0xf,%edx #取字符的低4位
0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx #用低4位作为偏移,从0x4024b0查找字符
0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) #将找到的字符存入[rsp+0x10+rax]
0x00000000004010a4 <+66>: add $0x1,%rax #增加计数器
0x00000000004010a8 <+70>: cmp $0x6,%rax #比较是否处理完6个字符
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41> #未完成则继续循环
0x00000000004010ae <+76>: movb $0x0,0x16(%rsp) #在生成的字符串末尾添加null终止符
0x00000000004010b3 <+81>: mov $0x40245e,%esi #目标字符串地址作为第二个参数
0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi #生成的字符串地址作为第一个参数
0x00000000004010bd <+91>: call 0x401338 <strings_not_equal> #比较字符串
0x00000000004010c2 <+96>: test %eax,%eax #测试返回值
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119> #相等则跳转(成功)
0x00000000004010c6 <+100>: call 0x40143a <explode_bomb> #否则引爆炸弹
0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1) #对齐填充
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119> #跳转到清理代码
#循环初始化
0x00000000004010d2 <+112>: mov $0x0,%eax #初始化循环计数器(eax=0)
0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41> #跳转到循环开始
0x00000000004010d9 <+119>: mov 0x18(%rsp),%rax #恢复栈保护金丝雀值
0x00000000004010de <+124>: xor %fs:0x28,%rax #验证金丝雀值是否被修改
0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140> #未修改则跳转
0x00000000004010e9 <+135>: call 0x400b30 <__stack_chk_fail@plt> #否则报栈溢出错误
0x00000000004010ee <+140>: add $0x20,%rsp #恢复栈指针
0x00000000004010f2 <+144>: pop %rbx #恢复 rbx 寄存器
0x00000000004010f3 <+145>: ret #返回
要找到正确的输入,我们需要:
- 查看
x/s 0x40245e
获取目标字符串 - 查看
x/16b 0x4024b0
获取16字符的查找表 - 对于目标字符串的每个字符,反向查找它在查找表中的位置(索引)
- 这些索引值(0-15)就是输入字符需要的低4位值
1
2
(gdb) x/s 0x40245e
0x40245e: "flyers"
1. 已知信息
- 目标字符串:
"flyers"
(位于0x40245e
) - 查找表:前16字节为
"maduiersnfotvbyl"
(位于0x4024b0
)
2. 查找表索引分析
让我们给查找表的每个字符编号(索引从0开始):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0: 'm'
1: 'a'
2: 'd'
3: 'u'
4: 'i'
5: 'e'
6: 'r'
7: 's'
8: 'n'
9: 'f'
10: 'o'
11: 't'
12: 'v'
13: 'b'
14: 'y'
15: 'l'
3. 反向查找需要的索引
我们需要找到哪些索引对应目标字符串 “flyers” 的每个字母:
- ‘f’ → 查找表中位于索引 9
- ‘l’ → 查找表中位于索引 15
- ‘y’ → 查找表中位于索引 14
- ‘e’ → 查找表中位于索引 5
- ‘r’ → 查找表中位于索引 6
- ’s’ → 查找表中位于索引 7
4. 构造输入字符
输入字符的低4位需要等于上述索引值。因此我们需要找到ASCII字符,其低4位分别是:
- 第1字符:低4位 = 9 (0x9)
- 第2字符:低4位 = 15 (0xF)
- 第3字符:低4位 = 14 (0xE)
- 第4字符:低4位 = 5 (0x5)
- 第5字符:低4位 = 6 (0x6)
- 第6字符:低4位 = 7 (0x7)
5. 可能的输入字符
任何ASCII字符,只要其低4位匹配即可。例如:
- 第1字符:可以是 ‘I’(0x49), ‘Y’(0x59), ‘i’(0x69), ‘y’(0x79) 等(因为 0xX9)
- 第2字符:可以是 ‘O’(0x4F), ‘_‘(0x5F), ‘o’(0x6F) 等(因为 0xXF)
- 第3字符:可以是 ‘N’(0x4E), ‘^’(0x5E), ‘n’(0x6E) 等(因为 0xXE)
- 第4字符:可以是 ‘E’(0x45), ‘U’(0x55), ‘e’(0x65), ‘u’(0x75) 等(因为 0xX5)
- 第5字符:可以是 ‘F’(0x46), ‘V’(0x56), ‘f’(0x66), ‘v’(0x76) 等(因为 0xX6)
- 第6字符:可以是 ‘G’(0x47), ‘W’(0x57), ‘g’(0x67), ‘w’(0x77) 等(因为 0xX7)
6. 示例解决方案
一个简单的解决方案是选择可打印字符,其低4位正好匹配:
- ‘i’ (0x69 = 0110 1001 → 低4位=9)
- ‘o’ (0x6F = 0110 1111 → 低4位=15)
- ‘n’ (0x6E = 0110 1110 → 低4位=14)
- ‘e’ (0x65 = 0110 0101 → 低4位=5)
- ‘f’ (0x66 = 0110 0110 → 低4位=6)
- ‘g’ (0x67 = 0110 0111 → 低4位=7)
因此,输入字符串 "ionefg"
即可。
Phase_6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
0x00000000004010f4 <+0>: push %r14 # 将寄存器压入栈中,保存其当前的值以待后续恢复
# 具体行为:将栈指针 RSP 减少 8 字节(因为 RBX 是 64 位寄存器),然后把 RBX 的值存入栈顶
0x00000000004010f6 <+2>: push %r13
0x00000000004010f8 <+4>: push %r12
0x00000000004010fa <+6>: push %rbp
0x00000000004010fb <+7>: push %rbx
0x00000000004010fc <+8>: sub $0x50,%rsp # 分配80字节栈空间
0x0000000000401100 <+12>: mov %rsp,%r13 # r13 = 栈顶指针(用于遍历输入数组)
0x0000000000401103 <+15>: mov %rsp,%rsi # rsi = 栈顶指针(作为参数传递给read_six_numbers)
0x0000000000401106 <+18>: call 0x40145c <read_six_numbers> # 调用函数读取6个整数到栈顶
0x000000000040110b <+23>: mov %rsp,%r14 # r14 = 栈顶指针(保存输入数组起始地址)
# 第一层循环:验证每个输入数字 <=6 且互不相同
0x000000000040110e <+26>: mov $0x0,%r12d # r12d = 0(外层循环计数器)
0x0000000000401114 <+32>: mov %r13,%rbp # rbp = 当前处理的数字地址(初始为栈顶)
0x0000000000401117 <+35>: mov 0x0(%r13),%eax # eax = 当前数字的值
0x000000000040111b <+39>: sub $0x1,%eax # eax = num - 1
0x000000000040111e <+42>: cmp $0x5,%eax # 检查 num-1 <= 5(即 num <= 6)
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52> # 无符号比较,合法则跳转
0x0000000000401123 <+47>: call 0x40143a <explode_bomb> # 否则引爆炸弹
0x0000000000401128 <+52>: add $0x1,%r12d # 外层循环计数器+1
0x000000000040112c <+56>: cmp $0x6,%r12d # 检查是否处理完6个数字
0x0000000000401130 <+60>: je 0x401153 <phase_6+95> # 完成则跳转到后续处理
0x0000000000401132 <+62>: mov %r12d,%ebx # ebx = 当前外层循环计数器(内层循环起始索引)
0x0000000000401135 <+65>: movslq %ebx,%rax # 将ebx符号扩展为rax(用于数组索引)
0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax # eax = 输入数组[ebx]的值
0x000000000040113b <+71>: cmp %eax,0x0(%rbp) # 比较当前数字与后续数字
0x000000000040113e <+74>: jne 0x401145 <phase_6+81> # 不相等则继续
0x0000000000401140 <+76>: call 0x40143a <explode_bomb> # 相等则爆炸(禁止重复)
0x0000000000401145 <+81>: add $0x1,%ebx # 内层循环计数器+1
0x0000000000401148 <+84>: cmp $0x5,%ebx # 检查是否比较到第5个数字
0x000000000040114b <+87>: jle 0x401135 <phase_6+65> # 继续内层循环
0x000000000040114d <+89>: add $0x4,%r13 # 移动到下一个数字地址(+4字节)
0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32> # 跳回外层循环
# 转换每个数字为7 - num
0x401153 <+95>: lea 0x18(%rsp),%rsi #rsi = 输入数组末尾地址(rsp + 24,6个int占24字节)
0x401158 <+100>: mov %r14,%rax #rax = 输入数组起始地址(r14 = rsp)
0x40115b <+103>: mov $0x7,%ecx #ecx = 7
0x401160 <+108>: mov %ecx,%edx #edx = 7
0x401162 <+110>: sub (%rax),%edx #edx = 7 - 当前数字
0x401164 <+112>: mov %edx,(%rax) #将结果写回数组(转换后的值)
0x401166 <+114>: add $0x4,%rax #rax += 4(移动到下一个数字)
0x40116a <+118>: cmp %rsi,%rax #检查是否到达数组末尾
0x40116d <+121>: jne 0x401160 <+108> #未完成则继续循环
0x40116f <+123>: mov $0x0,%esi #esi = 0(循环计数器,初始为0)
0x401174 <+128>: jmp 0x401197 <+163> #跳转到主循环
# --- 遍历链表找到第 ecx 个节点 ---
0x401176 <+130>: mov 0x8(%rdx),%rdx #rdx = rdx->next(移动到下一个节点)
0x40117a <+134>: add $0x1,%eax #eax++(计数器+1)
0x40117d <+137>: cmp %ecx,%eax #检查是否到达目标索引(ecx = 转换后的数字)
0x40117f <+139>: jne 0x401176 <+130> #未到达则继续遍历
0x401181 <+141>: jmp 0x401188 <+148> #找到后跳转到存储节点
# --- 处理数字 <=1 的情况(直接取链表头) ---
0x401183 <+143>: mov $0x6032d0,%edx #edx = 链表头节点地址(node1)
0x401188 <+148>: mov %rdx,0x20(%rsp,%rsi,2) #存储节点指针到栈 [rsp+0x20+esi*2]
0x40118d <+153>: add $0x4,%rsi #esi += 4(移动到下一个输入数字)
0x401191 <+157>: cmp $0x18,%rsi #检查是否处理完6个数字(0x18=24)
0x401195 <+161>: je 0x4011ab <+183> #完成则跳转到链表重组
# --- 主循环:读取当前数字并检查 ---
0x401197 <+163>: mov (%rsp,%rsi,1),%ecx #ecx = 转换后的输入数字
0x40119a <+166>: cmp $0x1,%ecx #检查数字是否 <=1
0x40119d <+169>: jle 0x401183 <+143> #是则直接取链表头
0x40119f <+171>: mov $0x1,%eax #eax = 1(初始化计数器)
0x4011a4 <+176>: mov $0x6032d0,%edx #edx = 链表头节点地址
0x4011a9 <+181>: jmp 0x401176 <+130> #跳转到链表遍历
0x4011ab <+183>: mov 0x20(%rsp),%rbx #rbx = 第一个节点指针(新链表头)
0x4011b0 <+188>: lea 0x28(%rsp),%rax #rax = 第二个节点指针的地址(rsp+0x28)
0x4011b5 <+193>: lea 0x50(%rsp),%rsi #rsi = 结束地址(rsp+0x50)
0x4011ba <+198>: mov %rbx,%rcx #rcx = 当前节点指针(初始为rbx)
0x4011bd <+201>: mov (%rax),%rdx #rdx = 下一个节点指针
0x4011c0 <+204>: mov %rdx,0x8(%rcx) #rcx->next = rdx(链接节点)
0x4011c4 <+208>: add $0x8,%rax #rax += 8(移动到下下个节点指针)
0x4011c8 <+212>: cmp %rsi,%rax #检查是否到达结束地址
0x4011cb <+215>: je 0x4011d2 <+222> #完成则跳转
0x4011cd <+217>: mov %rdx,%rcx #rcx = rdx(更新当前节点)
0x4011d0 <+220>: jmp 0x4011bd <+201> #继续循环
0x4011d2 <+222>: movq $0x0,0x8(%rdx) #链表末尾的 next = NULL
0x4011da <+230>: mov $0x5,%ebp #ebp = 5(循环5次,共6个节点)
0x4011df <+235>: mov 0x8(%rbx),%rax #rax = rbx->next
0x4011e3 <+239>: mov (%rax),%eax #eax = rax->value
0x4011e5 <+241>: cmp %eax,(%rbx) #比较 rbx->value >= rax->value
0x4011e7 <+243>: jge 0x4011ee <+250> #若降序则继续
0x4011e9 <+245>: call 0x40143a <explode_bomb> #否则爆炸
0x4011ee <+250>: mov 0x8(%rbx),%rbx #rbx = rbx->next(移动到下一节点)
0x4011f2 <+254>: sub $0x1,%ebp #ebp--
0x4011f5 <+257>: jne 0x4011df <+235> #循环直到 ebp=0
0x4011f7 <+259>: add $0x50,%rsp #释放栈空间
0x4011fb <+263>: pop %rbx #恢复被调用者保存的寄存器
0x4011fc <+264>: pop %rbp
0x4011fd <+265>: pop %r12
0x4011ff <+267>: pop %r13
0x401201 <+269>: pop %r14
0x401203 <+271>: ret
我们查看这个很特殊的地址的内容
x/24 会从当前内存地址开始,显示 24个连续的内存单元
默认情况下,每个内存单元的大小取决于GDB的当前设置(通常为4字节,即1个字)
1
2
3
4
5
6
7
(gdb) x/24 0x6032d0
0x6032d0 <node1>: 332 1 6304480 0
0x6032e0 <node2>: 168 2 6304496 0
0x6032f0 <node3>: 924 3 6304512 0
0x603300 <node4>: 691 4 6304528 0
0x603310 <node5>: 477 5 6304544 0
0x603320 <node6>: 443 6 0 0
按第一列的值大小降序排列得到:3 4 5 6 1 2
又由反汇编代码中的7-num部分有:4 3 2 1 6 5
(最近任务比较多,隐藏关卡懒得写了)
至此bomblab完成!