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了
- 可以通过命令行安装,见下文
- 可以手动安装:Download OrbStack · Fast, light, simple Docker & Linux on macOS
- 其次切换 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."
如果你想知道这一切的原因是为什么,请参考以下链接:
- 另一个 Rosetta 转译的思路,但是对我们这个情况不适用,可以留作参考
- 从下面 orb 社区维护者的方案中可以看到,这个转译方案会导致 cpu 100% 空转而且解决不了问题。测试发现确实是这样的。不过可以留作其他类似问题的一个修复参考
- Debugging an x86 application in Rosetta for Linux - the sporks space --- 在 Rosetta for Linux 中调试 x86 应用程序 - the sporks space
- 本文采用的方案:orb community 的方案
- GDB - PTRACE_GETREGS: Input/output error (Qemu ORB Machine) · Issue #113 · orbstack/orbstack (github.com)
- Problem with gdb · orbstack · Discussion #27 (github.com)
- 题外话,另外一些基于 qemu 的 gdb server 调试的教程
---
虚拟环境
虚拟环境搭建面向纯小白用户。
首先,安装一下docker,不管你是Mac还是windows,它都有docker这个东西的。
Mac安装参考:
https://docs.docker.com/docker-for-mac/install/
Windows安装参考:
https://docs.docker.com/docker-for-windows/install/
更改配置进行加速:
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
之后选择 add 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可以在http://csapp.cs.cmu.edu/3e/labs.html里下载,在虚拟实验环境中我已经为大家下载好了。如果你想要手动下载的话
labs的托管网页中有着好多个综述性的介绍,比如:
另外代码中也有一个README,也是讲着差不多的综述性质的介绍。比较推荐看代码中的README。总的来说,你需要做的是:
- 阅读
bomb.c
的注释与代码 & 阅读bomb
的反汇编代码; - 分析程序运行的思路,并推测defuse bomb中当前的phase key是什么;
- 通过gdb等方式,验证并测试自己的猜测;
- 回到2,直到解决所有问题
GDB cheatsheet
启动和加载
设置断点
运行
显示栈帧
backtrace
,别名where
、info 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的第400f51
行400f51: 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。