Introduction to CSAPP(十九):这可能是你能找到的分析最全的Bomblab了

Introduction to CSAPP(十九):这可能是你能找到的分析最全的Bomblab了

2023-06-18 updates

有朋友 @斯科达 报告在 mac apple silicon chip(M1, M2),不成功,调研了一下如何在M1上运行试验环境。

首先先说原因:在M1 M2 是 arm 架构,而实验相关的二进制文件是 x86-64 的架构,而 docker 作为用户态进程,通过 arm 指令集模拟 amd64(x86) 指令集的时候存在各种问题。

这里提一个解决方案,即使用 Linux on Mac via ROSETTA,在 mac 上运行一个通过 Rosetta 转译 x86 指令集 linux 虚拟机 + Qemu static user (用户态的 qemu 模拟) 实现对 gdb 的 ptrace 系统调用的支持。

以下是运行过程,请注意运行过程只对 mac apple silicon chip 有效

  • 首先下载一个 linux vm 的管理软件,这个软件提供了对 linux vm 和 docker runtime 的管理,使用它之后就可以告别 moby docker了
  • 其次切换 context 到 orb
  • 创建一个新 linux vm
    • 可以通过命令行新建,见下文
    • 也可以通过GUI创建,见下文图
  • 进入这个 vm
  • 执行脚本安装 bomb lab 相关的文件和依赖
  • 使用 qemu static 运行一个 full simulated 的 gdb remote stub
  • 开另外一个窗口 进入这个vm
  • 打断点进行调试
##### 在你的机器上运行
# 安装 orb
brew install orbstack
# 切换 docker container runtime to orb
docker context use orbstack
# 新建 linux
orb create ubuntu:bionic labs -a amd64
# 进入 linux vm
orb -m labs -u root

##### 在linux vm 中运行, 记这个窗口为 terminal0
# 安装并运行初始化脚本
# 这个脚本主要安装zsh, 一些必要的构建工具以及qemu
# 具体内容可以看这里 https://gist.github.com/Gnosnay/88d23cedea901b2ce478fdabb38f8b39
bash <(curl -s https://gist.githubusercontent.com/Gnosnay/88d23cedea901b2ce478fdabb38f8b39/raw/7ab538bc6eb1204c4b0a4d88035f5c2c7f019111/csapp-bomb-lab-linux-vm-init.sh)
# 进入 bomb lab 文件夹
cd /root/bomb
# 用 qemu 建立一个远程端口为 1234 的 gdb stub
qemu-x86_64-static -g 1234 ./bomb


##### 注意 另开一个命令行窗口,记为 terminal1
orb -m labs -u root
cd /root/bomb
gdb ./bomb
(gdb) target remote :1234
(gdb) b *0x400ee4
(gdb) continue

 
## 在 terminal0 输入任意字段
qeqemu-x86_64-static -g 1234 ./bomb
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
zxc # <--- 任意输入 


##### 在 terminal1 进行调试
(gdb) x/s 0x402400
0x402400:	"Border relations with Canada have never been better."
通过网站下载orb
通过GUI安装 linux vm
调试过程

如果你想知道这一切的原因是为什么,请参考以下链接:

---

虚拟环境

虚拟环境搭建面向纯小白用户。

首先,安装一下docker,不管你是Mac还是windows,它都有docker这个东西的。

Mac安装参考:

docs.docker.com/docker-

Windows安装参考:

docs.docker.com/docker-

更改配置进行加速:

Yannick:CSAPP LAB:DockerHub 镜像加速策略

然后,在命令行中运行:

 docker run --privileged -d -p 1221:22 --name bomb yansongsongsong/csapp:bomblab

这个时候你就有了一个可以操作的实验环境了。

如果你熟悉命令行更改文件,你可以执行

 docker exec -it bomb /bin/zsh

进入实验环境。

如果你不够熟悉命令行,希望在一个GUI的代码编辑器中方便地更改它,那么我推荐VSCode。

关于VSCode的安装,可以参考:

安装好之后,需要安装一个插件 remote ssh

在插件市场搜索这个插件并安装

然后按ctrl + shift + P 或者command + shift + P呼出 vscode 的命令洁面,敲remote ssh

选择 connect to host

之后选择 add host

选择新加ssh host

输入命令

ssh root@127.0.0.1 -p 1221

然后一路回车即可 最后会在右下角看到ssh已经添加的消息,点击建立连接即可

之后就可以连接到这个实验环境了

密码是THEPASSWORDYOUCREATED

随后一路回车即可。最后我们在左侧文件夹栏,选择打开root下的bomb文件夹,这样就可以直接通过vscode来修改与保存代码了。

手工搭建

如果你有一个linux的环境,那最好不过了。我使用的是64位的机器。这个程序在mac也是可以运行的。不需要进行额外的搭建操作,但是你可能需要下载一下gdb,如果你在mac上不会使用lldb的话。

如果你是windows,请使用上面的虚拟环境。

Lab分析

Bomblab主要是希望我们通过使用基本的逆向工程的手段,通过已有的bomb.c的主函数程序的提示,来获取每个phase的密钥,要注意的是,在CMU的课程中,炸弹被触发一次就会扣掉一半的分,在self-study版本中则没有这种记分机制,练习者也应该学会使用gdb,来让自己尽可能少的避免炸弹的爆炸。

Lab可以在csapp.cs.cmu.edu/3e/lab里下载,在虚拟实验环境中我已经为大家下载好了。如果你想要手动下载的话

lab代码

labs的托管网页中有着好多个综述性的介绍,比如:

课程目标与工具介绍

lab课程简介

另外代码中也有一个README,也是讲着差不多的综述性质的介绍。比较推荐看代码中的README。总的来说,你需要做的是:

  1. 阅读bomb.c的注释与代码 & 阅读bomb的反汇编代码;
  2. 分析程序运行的思路,并推测defuse bomb中当前的phase key是什么;
  3. 通过gdb等方式,验证并测试自己的猜测;
  4. 回到2,直到解决所有问题

GDB cheatsheet

启动和加载

设置断点

运行

显示栈帧

backtrace,别名whereinfo stack,后者简写可以是i s

显示值

  • print,可显示变量的值
  • info,可显示寄存器的值
  • x,可显示内存值,examining的简写

显示格式:p/格式 变量。显示寄存器或内存可使用的格式:

U单位:

例子:

题目解析

题目只给了一个main函数,我们可以大致看出来,它的模式是从某个地方读取字符串,然后作为参数输入每个关卡phase_,进行验证。具体的情况没有显示,说明我们需要通过某种手段去进行探查:

objdump -d bomb > bomb.asm

同时看到bomb.c中:

/* When run with no arguments, the bomb reads its input lines 
     * from standard input. */
if (argc == 1) {  
    infile = stdin;
}

说明可以通过文件读取的方式进行读取。

Phase_1

关键内容:

# 移除了机器指令
0000000000400ee0 <phase_1>:
400ee0: sub    $0x8,%rsp # 栈指针减8
400ee4: mov    $0x402400,%esi # 第二个参数传入0x402400
400ee9: callq  401338 <strings_not_equal> # 调用此方法
400eee: test   %eax,%eax    # 验证strings_not_equal的返回值
400ef0: je     400ef7 <phase_1+0x17> 
400ef2: callq  40143a <explode_bomb> # 如果返回值不为0,爆炸
400ef7: add    $0x8,%rsp
400efb: retq

仅从函数调用的角度来看,phase_1的参数存在1st argument寄存器中:%rdi,然后这个参数作为第一个参数,与0x402400作为第二个参数一起被传入到strings_not_equal中,进行一些判定操作。

0x402400看起来很像一个地址。因此我们可以在地址400ee4处打上断点来看一下这个地址里的值是什么:

(gdb) b *0x400ee4
Breakpoint 1 at 0x400ee4
(gdb) run 
Starting program: /root/bomb/bomb 
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
asd

Breakpoint 1, 0x0000000000400ee4 in phase_1 ()
(gdb) x/s 0x402400
0x402400:       "Border relations with Canada have never been better."

哦吼,发现了一个不得了的东西。其实从phase_1的函数名(strings_not_equal)中已经大致可以猜出来这个字符串可能就是我们要的值了。

为了以防万一,我们可以进一步看一下strings_not_equal这个函数:

0000000000401338 <strings_not_equal>:
401338: push   %r12 # 栈帧相关
40133a: push   %rbp # 栈帧相关
40133b: push   %rbx # 栈帧相关
40133c: mov    %rdi,%rbx # 第一个参数保存到%rbx
40133f: mov    %rsi,%rbp # 第二个参数保存到%rbp
401342: callq  40131b <string_length> # 以第一个参数作为入参,调用函数
401347: mov    %eax,%r12d # 返回值保存到%r12d
40134a: mov    %rbp,%rdi # 把第二个参数作为入参
40134d: callq  40131b <string_length> # 调用函数
401352: mov    $0x1,%edx # 将1赋值给%edx,作为strings_not_equal的返回结果预备值
401357: cmp    %eax,%r12d # 比较两个参数分别作为入参的调用结果
40135a: jne    40139b <strings_not_equal+0x63> # 不相等时跳转到40139b
40135c: movzbl (%rbx),%eax # 长度相等时,对(%rbx)内存的值付给%eax
40135f: test   %al,%al 
401361: je     401388 <strings_not_equal+0x50> # 如果这个值为0,即字符串的结束字符,则跳转到下面的位置;
401363: cmp    0x0(%rbp),%al # 对(%rbp)取值,和%al做比较;此时(%rbp)是内存的值,%al是我们输入的字符串的第一个字符
401366: je     401372 <strings_not_equal+0x3a># 相等跳转
401368: jmp    40138f <strings_not_equal+0x57># 不相等跳转
40136a: cmp    0x0(%rbp),%al
40136d: nopl   (%rax)
401370: jne    401396 <strings_not_equal+0x5e>
401372: add    $0x1,%rbx # 第一个参数指针+1
401376: add    $0x1,%rbp # 第二个参数指针+1
40137a: movzbl (%rbx),%eax
40137d: test   %al,%al
40137f: jne    40136a <strings_not_equal+0x32> # 重新进行字符比对
401381: mov    $0x0,%edx
401386: jmp    40139b <strings_not_equal+0x63>
401388: mov    $0x0,%edx
40138d: jmp    40139b <strings_not_equal+0x63>
40138f: mov    $0x1,%edx
401394: jmp    40139b <strings_not_equal+0x63>
401396: mov    $0x1,%edx
40139b: mov    %edx,%eax # 将返回结果预备值赋值(0或1)给%eax,准备返回
40139d: pop    %rbx
40139e: pop    %rbp
40139f: pop    %r12
4013a1: retq

其中

40136a: cmp    0x0(%rbp),%al
40136d: nopl   (%rax)
401370: jne    401396 <strings_not_equal+0x5e>
401372: add    $0x1,%rbx # 第一个参数指针+1
401376: add    $0x1,%rbp # 第二个参数指针+1
40137a: movzbl (%rbx),%eax
40137d: test   %al,%al
40137f: jne    40136a <strings_not_equal+0x32> # 重新进行字符比对

便是一个循环,对字符串进行一一比对。那到这里我们也明了,其实这个函数的实意就是比对两个入参的字符串是否相等,与函数名一致。

那么这个key就是Border relations with Canada have never been better.

Phase_2

0000000000400efc <phase_2>:
400efc: push   %rbp # 栈帧相关
400efd: push   %rbx # 栈帧相关
400efe: sub    $0x28,%rsp # 栈指针减40,说明有局部变量的分配
400f02: mov    %rsp,%rsi # 将栈指针作为第二个参数
400f05: callq  40145c <read_six_numbers> # 传入函数中
400f0a: cmpl   $0x1,(%rsp) # 查看栈指针的内存的值是否为1
400f0e: je     400f30 <phase_2+0x34> # 是 跳转400f30
400f10: callq  40143a <explode_bomb> # 否 爆炸
400f15: jmp    400f30 <phase_2+0x34>
400f17: mov    -0x4(%rbx),%eax # (%rbx-4)后,该内存地址的值赋值给%eax
400f1a: add    %eax,%eax # %eax+=%eax
400f1c: cmp    %eax,(%rbx) # 比较%eax和(%rbx)也就是下一个数的值
400f1e: je     400f25 <phase_2+0x29> # 相等跳转至400f25
400f20: callq  40143a <explode_bomb> # 不等爆炸
400f25: add    $0x4,%rbx # %rbx+=4
400f29: cmp    %rbp,%rbx # %rbp,%rbx比较看有没有比较完
400f2c: jne    400f17 <phase_2+0x1b># 不相等时400f17
400f2e: jmp    400f3c <phase_2+0x40># 相等时400f3c
400f30: lea    0x4(%rsp),%rbx # 栈指针+4后赋值给%rbx
400f35: lea    0x18(%rsp),%rbp # 栈指针+24后赋值给%rbp
400f3a: jmp    400f17 <phase_2+0x1b> # 跳至 400f17
400f3c: add    $0x28,%rsp
400f40: pop    %rbx
400f41: pop    %rbp
400f42: retq

根据解析,我们可以看到,第一个数一定是1,之后则是不断加上自身,也就是乘2(400f1a: add %eax,%eax # %eax+=%eax),又是6个数,那么这6个数则应该是一个初项为1,公比为2的等比数列:

1 2 4 8 16 32

Phase_3

我们看到phase3的第400f51400f51: mov $0x4025cf,%esi # 将一个地址作为第二个入餐准备进行函数调用,应该可以尝试去打印这个地址的值,发现:

Breakpoint 1, 0x0000000000400f51 in phase_3 ()
(gdb) x/s 0x4025cf
0x4025cf:       "%d %d"
(gdb)

联系上下文,说明phase_3的输入值应该有两个,都是数字。

0000000000400f43 <phase_3>:
400f43: sub    $0x18,%rsp   # 栈指针减24,说明有局部变量的分配
400f47: lea    0xc(%rsp),%rcx # 栈指针+12后赋值给%rcx
400f4c: lea    0x8(%rsp),%rdx # 栈指针+8后赋值给%rdx
400f51: mov    $0x4025cf,%esi # "%d %d"作为第二个入参准备进行函数调用
400f56: mov    $0x0,%eax # 为%eax赋值0
400f5b: callq  400bf0 <__isoc99_sscanf@plt>
400f60: cmp    $0x1,%eax # 如果输入的值的数量大于1
400f63: jg     400f6a <phase_3+0x27> # 跳转400f6a
400f65: callq  40143a <explode_bomb> # 否则爆炸
400f6a: cmpl   $0x7,0x8(%rsp) # %rsp+8的内存值与7比较
400f6f: ja     400fad <phase_3+0x6a># 大于,跳转400fad,也就是爆炸
400f71: mov    0x8(%rsp),%eax # 否则,%rsp+8的内存值赋值给%eax,也就是输入的第一个值,
400f75: jmpq   *0x402470(,%rax,8) # 并跳转至%rax*8+0x402470的内存保存的地址上;
400f7c: mov    $0xcf,%eax
400f81: jmp    400fbe <phase_3+0x7b>
400f83: mov    $0x2c3,%eax
400f88: jmp    400fbe <phase_3+0x7b>
400f8a: mov    $0x100,%eax
400f8f: jmp    400fbe <phase_3+0x7b>
400f91: mov    $0x185,%eax
400f96: jmp    400fbe <phase_3+0x7b>
400f98: mov    $0xce,%eax
400f9d: jmp    400fbe <phase_3+0x7b>
400f9f: mov    $0x2aa,%eax
400fa4: jmp    400fbe <phase_3+0x7b>
400fa6: mov    $0x147,%eax
400fab: jmp    400fbe <phase_3+0x7b>
400fad: callq  40143a <explode_bomb>
400fb2: mov    $0x0,%eax
400fb7: jmp    400fbe <phase_3+0x7b>
400fb9: mov    $0x137,%eax
400fbe: cmp    0xc(%rsp),%eax
400fc2: je     400fc9 <phase_3+0x86>
400fc4: callq  40143a <explode_bomb>
400fc9: add    $0x18,%rsp
400fcd: retq

我们可以看到,我们需要输入两个整数,其中第一个不能大于7,那么就有0~7共8种情况,然后,第二个数的判断依赖于第一个数的大小,因为这是一个间接跳转:

400f75: jmpq *0x402470(,%rax,8) # 并跳转至%rax*8+0x402470的内存保存的地址上;

因此,我们需要看一下可能的8种跳转:

(gdb) x/8a 0x402470
0x402470:       0x400f7c <phase_3+57>   0x400fb9 <phase_3+118>
0x402480:       0x400f83 <phase_3+64>   0x400f8a <phase_3+71>
0x402490:       0x400f91 <phase_3+78>   0x400f98 <phase_3+85>
0x4024a0:       0x400f9f <phase_3+92>   0x400fa6 <phase_3+99>

即:

任选一组

Phase_4

000000000040100c <phase_4>:
  40100c: sub    $0x18,%rsp
  401010: lea    0xc(%rsp),%rcx
  401015: lea    0x8(%rsp),%rdx
  40101a: mov    $0x4025cf,%esi # 0x4025cf是"%d %d"
  40101f: mov    $0x0,%eax
  401024: callq  400bf0 <__isoc99_sscanf@plt>
  401029: cmp    $0x2,%eax # 输入值的个数只能为2个
  40102c: jne    401035 <phase_4+0x29> # 否则爆炸
  40102e: cmpl   $0xe,0x8(%rsp) # 第一个数和14比较
  401033: jbe    40103a <phase_4+0x2e> # <= 14 跳转 
  401035: callq  40143a <explode_bomb> # 否则爆炸
  40103a: mov    $0xe,%edx # %edx = 14 第三个入参
  40103f: mov    $0x0,%esi # %esi = 0 第二个入参
  401044: mov    0x8(%rsp),%edi # 第一个数作为第一个入参
  401048: callq  400fce <func4> # 调用函数
  40104d: test   %eax,%eax # 返回值结果
  40104f: jne    401058 <phase_4+0x4c> # 不等于0时,爆炸
  401051: cmpl   $0x0,0xc(%rsp) # 第二个数和0比较
  401056: je     40105d <phase_4+0x51> # 相等,返回
  401058: callq  40143a <explode_bomb> # 不相等 爆炸
  40105d: add    $0x18,%rsp
  401061: retq

看完我们已经能知道,第二个数为0了。

我们看一下func4的情况:

0000000000400fce <func4>:
  400fce: sub    $0x8,%rsp
  400fd2: mov    %edx,%eax # %eax=14
  400fd4: sub    %esi,%eax # %eax=14-0=14
  400fd6: mov    %eax,%ecx # %ecx=14
  400fd8: shr    $0x1f,%ecx # 逻辑右移31位,%ecx=0
  400fdb: add    %ecx,%eax # %eax=14
  400fdd: sar    %eax # 算术右移 %eax=7
  400fdf: lea    (%rax,%rsi,1),%ecx# %ecx=7
  400fe2: cmp    %edi,%ecx # 第一个数和7比较
  400fe4: jle    400ff2 <func4+0x24> # <=7 时跳转
  400fe6: lea    -0x1(%rcx),%edx# 否则 %edx=6
  400fe9: callq  400fce <func4>
  400fee: add    %eax,%eax
  400ff0: jmp    401007 <func4+0x39>
  400ff2: mov    $0x0,%eax # %eax=0
  400ff7: cmp    %edi,%ecx
  400ff9: jge    401007 <func4+0x39>
  400ffb: lea    0x1(%rcx),%esi
  400ffe: callq  400fce <func4>
  401003: lea    0x1(%rax,%rax,1),%eax
  401007: add    $0x8,%rsp
  40100b: retq

其实从汇编中我们已经可以看出,当输入的值为7时,func4返回的结果为0,已经满足了我们的需求,因此我们应该输入:7 0

我们可以继续看看它的等价C语言:

// a:%rdi b:%rsi  c:%rdx
int func4(int a, int b, int c){
  int return_v = c - b; // %rax
  int t = ((unsigned)return_v) >> 31; // %rcx
  return_v = (t + return_v) >> 1;
  t = return_v + b;
  if (t - a <= 0){
    return_v = 0;
    if (t - a >= 0){
      return return_v;
    }else{
      b = t + 1;
      int r = func4(a,b,c);
      return 2 * r + 1;
    }
  } else {
    c = t - 1;
    int r = func4(a, b, c);
    return 2*r;
  }
}

// 修改后的代码
// x为输入的数
// y为0 z为14
int func4(int x, int y, int z){
    int k = z - y;
    k = ((int)(((unsigned)k>>31) + k) >> 1) + y;
    if(k < x)
        return 2*func4(x, k+1, z)+1;
    else if(k > x)
        return 2*func4(x, y, k-1);
    else
        return 0;
}

我们可以发现其实第一个数的取值范围是[0,0xe],我们可以对它进行一个穷举:

int main(){
    for(int i = 0; i <= 14; i++)
        if(!func4(i, 0, 14))
            printf("%d\n", i);
}
// ./func4 
// 0
// 1
// 3
// 7

所以最终答案是:

0 0 | 1 0 | 3 0 | 7 0

Phase_5

0000000000401062 <phase_5>:
  401062: push   %rbx
  401063: sub    $0x20,%rsp
  401067: mov    %rdi,%rbx
  40106a: mov    %fs:0x28,%rax
  401071: 
  401073: mov    %rax,0x18(%rsp)
  401078: xor    %eax,%eax
  40107a: callq  40131b <string_length>
  40107f: cmp    $0x6,%eax # 要求输入的字符序列长度为6
  401082: je     4010d2 <phase_5+0x70> 
  401084: callq  40143a <explode_bomb> # 不然爆炸
  401089: jmp    4010d2 <phase_5+0x70>
  # 关键循环开始
  40108b: movzbl (%rbx,%rax,1),%ecx # %rax开始时为0;这里其实是将输入的字符串保存至%ecx中
  40108f: mov    %cl,(%rsp) # 字符串的某一位保存至栈顶
  401092: mov    (%rsp),%rdx 
  401096: and    $0xf,%edx # 这一位与0xf做与运算
  401099: movzbl 0x4024b0(%rdx),%edx # 所得到的值作为指针访问0x4024b0地址下的某一位
  4010a0: mov    %dl,0x10(%rsp,%rax,1) # 将这位入栈
  4010a4: add    $0x1,%rax
  4010a8: cmp    $0x6,%rax
  4010ac: jne    40108b <phase_5+0x29>
  # 关键循环结束
  # 循环等价于:
  # for(int i=0;i<6;i++){
  #     narr[i] = array[input[i]&0xf]
  # }
  4010ae: movb   $0x0,0x16(%rsp)
  4010b3: mov    $0x40245e,%esi # 入参之一:0x40245e字符串,经检验得flyers
  4010b8: lea    0x10(%rsp),%rdi # 入参之二:上述循环压栈的字符
  4010bd: callq  401338 <strings_not_equal>
  4010c2: test   %eax,%eax
  4010c4: je     4010d9 <phase_5+0x77> # 字符串相等时才过了这一关
  4010c6: callq  40143a <explode_bomb>
  4010cb: nopl   0x0(%rax,%rax,1)
  4010d0: jmp    4010d9 <phase_5+0x77>
  4010d2: mov    $0x0,%eax
  4010d7: jmp    40108b <phase_5+0x29>
  4010d9: mov    0x18(%rsp),%rax
  4010de: xor    %fs:0x28,%rax
  4010e5: 
  4010e7: je     4010ee <phase_5+0x8c>
  4010e9: callq  400b30 <__stack_chk_fail@plt>
  4010ee: add    $0x20,%rsp
  4010f2: pop    %rbx
  4010f3: retq

从0x4024b0开始的16个数据

(gdb) x/16c 0x4024b0
0x4024b0 <array.3449>:  109 'm' 97 'a'  100 'd' 117 'u' 105 'i' 101 'e' 114 'r' 115 's'
0x4024b8 <array.3449+8>:        110 'n' 102 'f' 111 'o' 116 't' 118 'v' 98 'b'  121 'y' 108 'l'
(gdb) x/s 0x40245e
0x40245e:       "flyers"

也就是说,我们要求的是

// 已知有:
char array[16] = {'m','a','d','u','i','e','r','s','n','f','o','t','v','b','y','l'};
char target[6] = {'f','l','y','e','r','s'}
// 求input[i],i属于[0,5]使得
array[input[i]&0xf] = target[i];

随意选取上表"flyers"对应的任意字符组合都能过关。

Phase_6

00000000004010f4 <phase_6>:
  4010f4: push   %r14
  4010f6: push   %r13
  4010f8: push   %r12
  4010fa: push   %rbp
  4010fb: push   %rbx
  4010fc: sub    $0x50,%rsp
  401100: mov    %rsp,%r13
  401103: mov    %rsp,%rsi
  401106: callq  40145c <read_six_numbers>
  40110b: mov    %rsp,%r14
  40110e: mov    $0x0,%r12d
  401114: mov    %r13,%rbp
  401117: mov    0x0(%r13),%eax
  # 输入数字 1~6
  # 断点可以设置在这里简单看一下:
  # -----
  # (gdb) x/6d $rsp
    # 0x7fffffffe350: 1       2       3       4
    # 0x7fffffffe360: 5       6
    # -----
    # 可以发现输入的数字存放的位置
  40111b: sub    $0x1,%eax
  40111e: cmp    $0x5,%eax # 第一个数 - 1 <= 5
  401121: jbe    401128 <phase_6+0x34> # 时继续
  401123: callq  40143a <explode_bomb> # 否则爆炸
  401128: add    $0x1,%r12d # %r12d感觉想一个计数器
  40112c: cmp    $0x6,%r12d 
  401130: je     401153 <phase_6+0x5f> # 等于6时才跳转
  401132: mov    %r12d,%ebx # 否则继续
  401135: movslq %ebx,%rax
  401138: mov    (%rsp,%rax,4),%eax # 取下一个数到%eax
  40113b: cmp    %eax,0x0(%rbp) # 这个数和上一个数不能相等,否则爆炸
  40113e: jne    401145 <phase_6+0x51>
  401140: callq  40143a <explode_bomb>
  401145: add    $0x1,%ebx
  401148: cmp    $0x5,%ebx # ebx也是一个计数器
  40114b: jle    401135 <phase_6+0x41> # 继续循环
  40114d: add    $0x4,%r13 # 取下一个数
  401151: jmp    401114 <phase_6+0x20> # 到上面的进行循环
  # 这一段大致看下来给人的感觉是多重循环的嵌套
  # 尝试解这个多重循环
  # for (int i=0; i<6; i++){
  #     if (arr[i] - 1 > 5) bomb()
  #     for (int j=i+1; j<=5; j++) {
  #         if(arr[j] == arr[i]) bomb()
  #     }
  # }
  # 即 1. 验证每一个数字都必须<=6
  #    2. 每一个数字都不一样
  401153: lea    0x18(%rsp),%rsi # 第6个数给%rsi
  401158: mov    %r14,%rax # 第一个数的地址给%rax
  40115b: mov    $0x7,%ecx
  401160: mov    %ecx,%edx
  401162: sub    (%rax),%edx # %edx =7 - input[0]
  401164: mov    %edx,(%rax) # input[0] = 7 - input[0]
  401166: add    $0x4,%rax # 下一个数
  40116a: cmp    %rsi,%rax # 看看有没有取到最后一个数
  40116d: jne    401160 <phase_6+0x6c># 没有继续上面的操作
  40116f: mov    $0x0,%esi # 否则 %esi = 0 感觉又是一个计数器
  # 这一段循环的操作
  # 是将所有的input原地被7减并替换
  # 
  401174: jmp    401197 <phase_6+0xa3>
  401176: mov    0x8(%rdx),%rdx # %rdx=mem[%rdx+0x8]
  40117a: add    $0x1,%eax  # 计数器
  40117d: cmp    %ecx,%eax # 运行6次
  40117f: jne    401176 <phase_6+0x82>
  401181: jmp    401188 <phase_6+0x94> # 6次后跳出
  401183: mov    $0x6032d0,%edx
  401188: mov    %rdx,0x20(%rsp,%rsi,2)
  40118d: add    $0x4,%rsi # 另外一个循环,
  401191: cmp    $0x18,%rsi # 保证每个input值都运行过
  401195: je     4011ab <phase_6+0xb7>
  401197: mov    (%rsp,%rsi,1),%ecx # 指针偏移,依次获取6个数
  40119a: cmp    $0x1,%ecx # 比较数字 <= 1
  40119d: jle    401183 <phase_6+0x8f> # 是跳到上面
  40119f: mov    $0x1,%eax # 否 %eax = 1
  4011a4: mov    $0x6032d0,%edx # 这是一个magic number,跳转回到上面后,发现这个数字其实是地址
  4011a9: jmp    401176 <phase_6+0x82>
  # 这一块的逻辑很绕 我们可以从这个magic number入手,打印它前后的信息:
  #
  # (gdb) x 0x6032d0
    # 0x6032d0 <node1>:       0x0000014c
    #
    # 结合add = *(add+8);即mov    0x8(%rdx),%rdx,才想应该是一个数组的结构
    # 我们尝试打印出更多的信息,这个结构体的信息:
    #
    # (gdb) x/24w 0x6032d0
    # 0x6032d0 <node1>:       0x0000014c      0x00000001      0x006032e0      0x00000000
    # 0x6032e0 <node2>:       0x000000a8      0x00000002      0x006032f0      0x00000000
    # 0x6032f0 <node3>:       0x0000039c      0x00000003      0x00603300      0x00000000
    # 0x603300 <node4>:       0x000002b3      0x00000004      0x00603310      0x00000000
    # 0x603310 <node5>:       0x000001dd      0x00000005      0x00603320      0x00000000
    # 0x603320 <node6>:       0x000001bb      0x00000006      0x00000000      0x00000000
    # 那这个结构体是什么样子的呢
    # 在这里,我的输入是1 2 3 4 5 6
    # 我们看到打印出来的结果,每个node里第2个四字节的部分和我们的输入吻合;
    # 而第三个四字节的部分则是下一个node的起始地址,最后一个四字节的部分则为0,
    # 考虑到内存对齐,我们大概能推测出,这应该是一个链表,而我们的输入的数字与在第二个四字节的地方的数据有关,第一个四字节的内容表示的是什么待确定
    # 这个结构体类似:
    # struct {
  #  int sth; // 某四字节内容
  #  int input; // 与我们的输入有关
  #  node* next; // 下一个node地址
    # } node;
    # 回头再看,发现其实这一快的逻辑,是将内存中数组的指针,放到了
    # 首地址为rsp+0x20 尾地址为rsp+0x50的地方
    # 所以现在有了两个数组
    # oldArray -> {0x6032d0 0x6032e0 0x6032f0 0x603300 0x603310 0x603320}
    # 
    # 这个sth是什么我们继续看下面的操作
  4011ab: mov    0x20(%rsp),%rbx # %rbx = head
  4011b0: lea    0x28(%rsp),%rax # %rax = head.next
  4011b5: lea    0x50(%rsp),%rsi # %rsi = tail
  4011ba: mov    %rbx,%rcx # %rcx = head
  4011bd: mov    (%rax),%rdx # %rdx = head.next.value
  4011c0: mov    %rdx,0x8(%rcx) # head.next.value = head.next.value
  4011c4: add    $0x8,%rax # %rax = head.next.next
  4011c8: cmp    %rsi,%rax 
  4011cb: je     4011d2 <phase_6+0xde>
  4011cd: mov    %rdx,%rcx # head.value = head.next.value
  4011d0: jmp    4011bd <phase_6+0xc9>
  4011d2: movq   $0x0,0x8(%rdx)
  4011d9:
  # 我们将断点打到4011da
  # 再次检视链表数据
  # (gdb) x/24w 0x6032d0
  # 0x6032d0 <node1>:       0x0000014c      0x00000001      0x00000000      0x00000000
  # 0x6032e0 <node2>:       0x000000a8      0x00000002      0x006032d0      0x00000000
  # 0x6032f0 <node3>:       0x0000039c      0x00000003      0x006032e0      0x00000000
  # 0x603300 <node4>:       0x000002b3      0x00000004      0x006032f0      0x00000000
  # 0x603310 <node5>:       0x000001dd      0x00000005      0x00603300      0x00000000
  # 0x603320 <node6>:       0x000001bb      0x00000006      0x00603310      0x00000000
  # 发现地址发生了变化,看起来头节点在node6上
  # 结合input原地被7减去,再多试几组数据后发现
  # struct {
  #  int value; // 下一部分需要比对的值
  #  int order; // node序号
  #  node* next; // 下一个node地址,会因为我们的input而改变链接顺序
    # } node;
    # 我们结合以下代码检视%rbx的值
  # (gdb) p/a $rbx
    # $4 = 0x603320 <node6>
    # 发现$rbx上存的就是头指针
    # 那么下面的逻辑也就一目了然了
    #
  # 我们输入的input序列被7减去后得到的序列,是一个向量
  # 向量每个数字是node的序号,向量的顺序是node的链接顺序
  # 也就是说向量的第一个序号对应的node会变成头节点
  # 按照以下的逻辑
  # 我们要求这个链表按照降序排列
  4011da: mov    $0x5,%ebp
  4011df: mov    0x8(%rbx),%rax # %rax保存是%rbx的下一个节点的指针
  4011e3: mov    (%rax),%eax # 结构体中sth的值 保存在%rax中
  4011e5: cmp    %eax,(%rbx) # 比较两个node的sth值
  4011e7: jge    4011ee <phase_6+0xfa> # 如果靠前结点的sth < 靠后结点的sth
  4011e9: callq  40143a <explode_bomb> # 爆炸
  4011ee: mov    0x8(%rbx),%rbx # 移动指针
  4011f2: sub    $0x1,%ebp  
  4011f5: jne    4011df <phase_6+0xeb> # 循环
  4011f7: add    $0x50,%rsp
  4011fb: pop    %rbx
  4011fc: pop    %rbp
  4011fd: pop    %r12
  4011ff: pop    %r13
  401201: pop    %r14
  401203: retq

打印node中的数:

(gdb) p 0x0000014c  1
$15 = 332
(gdb) p 0x000000a8  2
$16 = 168
(gdb) p 0x0000039c  3
$17 = 924
(gdb) p 0x000002b3  4
$18 = 691
(gdb) p 0x000001dd  5
$19 = 477
(gdb) p 0x000001bb  6
$20 = 443

我们应该期望的排序是:

3 4 5 6 1 2 按照7取余后,得到4 3 2 1 6 5

Bonus

炸弹有奖励关卡,因为在查看反汇编结果时,发现有secret_phase的函数的存在。我们全局搜索这个函数名,在phase_defused中发现了调用。

00000000004015c4 <phase_defused>:
4015c4:  sub    $0x78,%rsp
4015c8:  mov    %fs:0x28,%rax
4015cf:  
4015d1:  mov    %rax,0x68(%rsp)
4015d6:  xor    %eax,%eax
4015d8:  cmpl   $0x6,0x202181(%rip)        # 603760 <num_input_strings>
4015df:  jne    40163f <phase_defused+0x7b>
4015e1:  lea    0x10(%rsp),%r8
4015e6:  lea    0xc(%rsp),%rcx
4015eb:  lea    0x8(%rsp),%rdx
4015f0:  mov    $0x402619,%esi # 这里包括下面发现了奇怪的地址,打印看看,发现是 "%d %d %s"
4015f5:  mov    $0x603870,%edi # 这里是 ""
4015fa:  callq  400bf0 <__isoc99_sscanf@plt>
4015ff:  cmp    $0x3,%eax # sscanf的返回值表示输入的参数个数,如果是3个,就到401604行,那么究竟什么时候会执行这段逻辑呢?
401602:  jne    401635 <phase_defused+0x71>
401604:  mov    $0x402622,%esi # "DrEvil"
401609:  lea    0x10(%rsp),%rdi # 比较 "DrEvil" 和某个值
40160e:  callq  401338 <strings_not_equal>
401613:  test   %eax,%eax
401615:  jne    401635 <phase_defused+0x71>
401617:  mov    $0x4024f8,%edi # "Curses, you've found the secret phase!"
40161c:  callq  400b10 <puts@plt> # 打印之
401621:  mov    $0x402520,%edi # "But finding it and solving it are quite different..."
401626:  callq  400b10 <puts@plt> # 打印之
40162b:  mov    $0x0,%eax
401630:  callq  401242 <secret_phase> # 调用了彩蛋关
401635:  mov    $0x402558,%edi # "Congratulations! You've defused the bomb!"
40163a:  callq  400b10 <puts@plt> # 打印之
40163f:  mov    0x68(%rsp),%rax
401644:  xor    %fs:0x28,%rax
40164b:  
40164d:  je     401654 <phase_defused+0x90>
40164f:  callq  400b30 <__stack_chk_fail@plt>
401654:  add    $0x78,%rsp
401658:  retq   
401659:  nop
40165a:  nop
40165b:  nop
40165c:  nop
40165d:  nop
40165e:  nop
40165f:  nop

以下是打印结果

# (gdb) x/s 0x402619
# 0x402619:       "%d %d %s"
# (gdb) x/s 0x603870
# 0x603870 <input_strings+240>:   ""
# (gdb) x/s 0x402622
# 0x402622:       "DrEvil"
# (gdb) x/s 0x4024f8
# 0x4024f8:       "Curses, you've found the secret phase!"
# (gdb) x/s 0x402520
# 0x402520:       "But finding it and solving it are quite different..."
# (gdb) x/s 0x402558
# 0x402558:       "Congratulations! You've defused the bomb!"

我们使用6关的密文,在phase_defused处打点观察,其中一个奇怪的却为空串的地址值:

(gdb) b *0x4015fa
Breakpoint 1 at 0x4015fa
(gdb) run defuse.txt 
Starting program: /root/bomb/bomb defuse.txt
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Phase 1 defused. How about the next one?
That's number 2.  Keep going!
Halfway there!
So you got that one.  Try this one.
Good work!  On to the next...

Breakpoint 1, 0x00000000004015fa in phase_defused ()
(gdb) x/s 0x603870
0x603870 <input_strings+240>:   "7 0"
(gdb) c
Continuing.
Congratulations! You've defused the bomb!
[Inferior 1 (process 1394) exited normally]
(gdb)

发现只有一次被断点,此时这个值为 7 0,也就是我们的第四关的答案,结合上面的汇编逻辑,我们可以猜测,当第四关的输入为数字 数字 字符串且第三个参数为DrEvil时,进入彩蛋关。

下面分析彩蛋关:

0000000000401242 <secret_phase>:
401242:  push   %rbx
401243:  callq  40149e <read_line>
401248:  mov    $0xa,%edx
40124d:  mov    $0x0,%esi
401252:  mov    %rax,%rdi
401255:  callq  400bd0 <strtol@plt>
40125a:  mov    %rax,%rbx # 断点可以设置在这里,打印后发现,rax中存的是我们输入的值
40125d:  lea    -0x1(%rax),%eax # eax = eax - 1
401260:  cmp    $0x3e8,%eax # eax - 0x3e8 <= 0 即 in - 1 <= 1000
401265:  jbe    40126c <secret_phase+0x2a>
401267:  callq  40143a <explode_bomb> # 不满足时 爆炸
40126c:  mov    %ebx,%esi # 这个是我们输入的值
40126e:  mov    $0x6030f0,%edi # 观察输入的参数:
# (gdb) x 0x6030f0
# 0x6030f0 <n1>:  0x00000024
401273:  callq  401204 <fun7>
401278:  cmp    $0x2,%eax
40127b:  je     401282 <secret_phase+0x40> # fun7返回值和2比,如果等于零,则成功
40127d:  callq  40143a <explode_bomb>
401282:  mov    $0x402438,%edi
401287:  callq  400b10 <puts@plt>
40128c:  callq  4015c4 <phase_defused>
401291:  pop    %rbx

关键点就在于func7这个函数了:

0000000000401204 <fun7>:
# %esi 存我们输入的值
# %edi 存某一地址
401204: sub    $0x8,%rsp
401208: test   %rdi,%rdi # 查看是否为null
40120b: je     401238 <fun7+0x34> # 满足跳转
40120d: mov    (%rdi),%edx 
40120f: cmp    %esi,%edx
401211: jle    401220 <fun7+0x1c>
401213: mov    0x8(%rdi),%rdi
401217: callq  401204 <fun7>
40121c: add    %eax,%eax
40121e: jmp    40123d <fun7+0x39>
401220: mov    $0x0,%eax
401225: cmp    %esi,%edx
401227: je     40123d <fun7+0x39>
401229: mov    0x10(%rdi),%rdi
40122d: callq  401204 <fun7>
401232: lea    0x1(%rax,%rax,1),%eax
401236: jmp    40123d <fun7+0x39>
401238: mov    $0xffffffff,%eax # 返回全1序列
40123d: add    $0x8,%rsp
401241: retq

等价C语言

int fun7(int cmp, Node* addr){
  if(addr == 0){
    return -1;
  }
  int v = addr->value;
  if (v == cmp){
    return 0;
  }else if( v < cmp){
        return 1 + 2*fun7(cmp, addr->right);
  }else{
    return 2*func7(cmp, addr->left);
  }
}

查看这个二叉树:

(gdb) x/144 0x6030f0
0x6030f0 <n1>:  0x00000024      0x00000000      0x00603110      0x00000000
0x603100 <n1+16>:       0x00603130      0x00000000      0x00000000      0x00000000
0x603110 <n21>: 0x00000008      0x00000000      0x00603190      0x00000000
0x603120 <n21+16>:      0x00603150      0x00000000      0x00000000      0x00000000
0x603130 <n22>: 0x00000032      0x00000000      0x00603170      0x00000000
0x603140 <n22+16>:      0x006031b0      0x00000000      0x00000000      0x00000000
0x603150 <n32>: 0x00000016      0x00000000      0x00603270      0x00000000
0x603160 <n32+16>:      0x00603230      0x00000000      0x00000000      0x00000000
0x603170 <n33>: 0x0000002d      0x00000000      0x006031d0      0x00000000
0x603180 <n33+16>:      0x00603290      0x00000000      0x00000000      0x00000000
0x603190 <n31>: 0x00000006      0x00000000      0x006031f0      0x00000000
0x6031a0 <n31+16>:      0x00603250      0x00000000      0x00000000      0x00000000
0x6031b0 <n34>: 0x0000006b      0x00000000      0x00603210      0x00000000
0x6031c0 <n34+16>:      0x006032b0      0x00000000      0x00000000      0x00000000
0x6031d0 <n45>: 0x00000028      0x00000000      0x00000000      0x00000000
0x6031e0 <n45+16>:      0x00000000      0x00000000      0x00000000      0x00000000
0x6031f0 <n41>: 0x00000001      0x00000000      0x00000000      0x00000000
0x603200 <n41+16>:      0x00000000      0x00000000      0x00000000      0x00000000
0x603210 <n47>: 0x00000063      0x00000000      0x00000000      0x00000000
0x603220 <n47+16>:      0x00000000      0x00000000      0x00000000      0x00000000
0x603230 <n44>: 0x00000023      0x00000000      0x00000000      0x00000000
0x603240 <n44+16>:      0x00000000      0x00000000      0x00000000      0x00000000
0x603250 <n42>: 0x00000007      0x00000000      0x00000000      0x00000000
0x603260 <n42+16>:      0x00000000      0x00000000      0x00000000      0x00000000
0x603270 <n43>: 0x00000014      0x00000000      0x00000000      0x00000000
0x603280 <n43+16>:      0x00000000      0x00000000      0x00000000      0x00000000
0x603290 <n46>: 0x0000002f      0x00000000      0x00000000      0x00000000
0x6032a0 <n46+16>:      0x00000000      0x00000000      0x00000000      0x00000000
0x6032b0 <n48>: 0x000003e9      0x00000000      0x00000000      0x00000000
0x6032c0 <n48+16>:      0x00000000      0x00000000      0x00000000      0x00000000
0x6032d0 <node1>:       0x0000014c      0x00000001      0x006032e0      0x00000000
0x6032e0 <node2>:       0x000000a8      0x00000002      0x00000000      0x00000000
0x6032f0 <node3>:       0x0000039c      0x00000003      0x00603300      0x00000000
0x603300 <node4>:       0x000002b3      0x00000004      0x00603310      0x00000000
0x603310 <node5>:       0x000001dd      0x00000005      0x00603320      0x00000000
0x603320 <node6>:       0x000001bb      0x00000006      0x006032d0      0x00000000

结果如下:

└─ 36
   ├─ 8
   │  ├─ 6
   │  │  ├─ left: 1
   │  │  └─ right: 7
   │  └─ 22
   │     ├─ left: 20
   │     └─ right: 35
   └─ 50
      ├─ 45
      │  ├─ left: 40
      │  └─ right: 47
      └─ 107
         ├─ left: 99
         └─ right: 1001

我们需要找最后返回结果为2的输入值。分析:

  • 输入值一定为节点值:否则一定会访问到一个空节点,最终结果一定是负数

穷举也好,继续分析层级和左右节点的返回值关系也好,发现有两个可能的值:20或22。

reference

编辑于 2023-11-16 22:52・IP 属地新加坡