Post

bomblab

bomblab

小土刀’s Blog

复习的时候一定先读上面这篇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寄存器,可以通过以下方式访问其部分位:
    • RDX:完整64位
    • EDX:低32位
    • DX:低16位
    • DL:低8位

      2)常见寄存器

  • %rbp: 基址指针寄存器,通常用于标记函数栈帧的底部
  • %rbx: 基址寄存器,在这段代码中作为数组指针使用
  • %rsp: 栈指针寄存器,指向栈顶
  • %rsi: 第二参数寄存器,用于传递函数参数
  • %eax: 累加器寄存器的低32位,用于计算和比较

2.Phase_1

1)%esi作为函数调用的第2个参数寄存器,存储了0x402400这个值,一看就是个地址,所以要用x/s指令查看这个地址所存放的内容是什么(就是phase1的答案)

2)而string_not_equal函数的第一个参数,自然就是我们输入的字符串,通过testl %eax, %eaxje这两个指令来判断我们输入的字符串是否等于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                    ; #返回,函数结束

这一关的逻辑是:

  1. 读取6个数字到栈上
  2. 检查第一个数字是否为1,不是则引爆炸弹
  3. 检查后续每个数字是否是前一个数字的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这个等比数列

贴一张拆弹结果(很好看的termius终端)
description

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的功能是检查用户输入的两个整数值是否符合特定条件:

  1. 必须输入两个值
  2. 第一个值必须是0-7之间的数字
  3. 第二个值必须等于一个与第一个值对应的特定数字:
    • 如果第一个值是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)操作,具体步骤是:

  1. 计算地址:0x402470 + (rax * 8)
  • 如果rax = 0,地址为:0x402470
  • 如果rax = 1,地址为:0x402478
  • 如果rax = 2,地址为:0x402480
    …以此类推
  1. 从计算出的地址加载64位值(一个内存地址)
  2. 跳转到加载的地址

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:搜索范围的上限

函数的逻辑是:

  1. 计算中间值 mid = (上限 - 下限) / 2 + 下限
  2. 如果 mid == 目标值,返回0
  3. 如果 mid > 目标值,递归搜索左半部分,返回值乘以2
  4. 如果 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                       #返回

要找到正确的输入,我们需要:

  1. 查看 x/s 0x40245e 获取目标字符串
  2. 查看 x/16b 0x4024b0 获取16字符的查找表
  3. 对于目标字符串的每个字符,反向查找它在查找表中的位置(索引)
  4. 这些索引值(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” 的每个字母:

  1. ‘f’ → 查找表中位于索引 9
  2. ‘l’ → 查找表中位于索引 15
  3. ‘y’ → 查找表中位于索引 14
  4. ‘e’ → 查找表中位于索引 5
  5. ‘r’ → 查找表中位于索引 6
  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. 第1字符:可以是 ‘I’(0x49), ‘Y’(0x59), ‘i’(0x69), ‘y’(0x79) 等(因为 0xX9)
  2. 第2字符:可以是 ‘O’(0x4F), ‘_‘(0x5F), ‘o’(0x6F) 等(因为 0xXF)
  3. 第3字符:可以是 ‘N’(0x4E), ‘^’(0x5E), ‘n’(0x6E) 等(因为 0xXE)
  4. 第4字符:可以是 ‘E’(0x45), ‘U’(0x55), ‘e’(0x65), ‘u’(0x75) 等(因为 0xX5)
  5. 第5字符:可以是 ‘F’(0x46), ‘V’(0x56), ‘f’(0x66), ‘v’(0x76) 等(因为 0xX6)
  6. 第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

(最近任务比较多,隐藏关卡懒得写了)

description

至此bomblab完成!

This post is licensed under CC BY 4.0 by the author.