type
status
date
slug
summary
tags
category
icon
password
Property
Jun 23, 2023 11:48 AM
实验 2:Bomb Lab
实验环境
- Ubuntu 20.04
实验要求
这个实验首先要求对汇编有一定的掌握,所以在此就不列举汇编的相关内容了。个人感觉用到最重要也是想要入手必须要具备的知识:一是学会使用反汇编及调试工具,二是了解函数调用的栈帧。
这次的任务是破解二进制炸弹,一共有七关,六个常规关卡和一个隐藏关卡,每次我们需要输入正确的『拆弹密码』才能进入下一关,而具体的『拆弹密码』藏在汇编代码中。进入隐藏关卡的方式也在其中!这就需要我们一点一点探索蛛丝马迹了。
栈帧
想要了解栈帧的结构?我们还是先来回顾(review)以下有哪些和函数栈相关的寄存器吧。(这儿并没有包含浮点寄存器)
- 所谓调用者保存,就是可以让被调用者(自身不作为另一个调用者)随意使用,也是为了自己用到的数据不被覆盖。
- 所谓被调用者保存,恰恰与调用者保存相反。
- 函数调用一般参数传递(非浮点)前 6 个参数存于寄存器,剩下的参数按照函数定义从右向左压栈。
- 栈指针指向函数栈栈顶。
- %rax 用于保存函数调用返回值。
了解了这些寄存器,我们再来看看栈帧的结构
就拿函数 P 的栈帧来说,从栈底到栈顶的方向分别存储以下内容:
- 被保存的寄存器
- 局部变量(
sub $0x18,%rsp
)
- 如果调用其他函数参数多于 6,便有参数构造区
- 调用其他函数时需要将返回地址压栈
汇编初探
想要完成拆弹任务,不但需要理解不同寄存器的常用方法,也要弄明白具体的操作符是什么意思:
类型 | 语法 | 例子 | 备注 |
常量 | 符号 $ 开头 | $-42 , $0x15213 | 一定要注意十进制还是十六进制 |
寄存器 | 符号 % 开头 | %esi , %rax | 可能存的是值或者地址 |
内存地址 | 括号括起来 | (%rbx) , 0x1c(%rax) , 0x4(%rcx, %rdi, 0x1) | 括号实际上是去寻址的意思 |
一些汇编语句与实际命令的转换:
指令 | 效果 |
mov %rbx, %rdx | rdx = rbx |
add (%rdx), %r8 | r8 += value at rdx |
mul $3, %r8 | r8 *= 3 |
sub $1, %r8 | r8-- |
lea (%rdx, %rbx, 2), %rdx | rdx = rdx + rbx*2 |
比较与跳转是拆弹的关键,基本所有的字符判断就是通过比较来实现的,比方说
cmp b,a
会计算 a-b
的值,test b, a
会计算 a&b
,注意运算符的顺序。例如等同于
if %r10 > %r9, jump to 8675309
各种不同的跳转:
指令 | 效果 |
jmp | Always jump |
je/jz | Jump if eq / zero |
jne/jnz | Jump if !eq / !zero |
jg | Jump if greater |
jge | Jump if greater / eq |
jl | Jump if less |
jle | Jump if less / eq |
ja | Jump if above(unsigned >) |
jae | Jump if above / equal |
jb | Jump if below(unsigned <) |
jbe | Jump if below / equal |
js | Jump if sign bits is 1(neg) |
jns | Jump if sign bit is 0 (pos) |
x | x |
举几个例子
若
%r12 >= 0x15213
,则跳转到 0xdeadeef
如果
%rdi
的无符号值大于等于 %rax
,则跳转到 0x15213b
如果
%r8 & %r8
不为零,那么跳转到 %rsi
存着的地址中。GDB 介绍
用 ctl+c 可以退出,每次进入都要设置断点(保险起见),炸弹会用
sscanf
来读取字符串,到底需要输入什么。GDB安装
方法一:
apt-get
打开终端,在终端里输入以下指令:
安装时需要选择 y 来确认安装 gdb。
方法二:
在网址:http://ftp.gnu.org/gnu/gdb下载gdb源码包或者直接用wget命令下载:
wget http://ftp.gnu.org/gnu/gdb/gdb-8.0.1.tar.gz
会下载到当前目录下。
使用
tar -zxvf
命令解压缩你下载的源码包安装完毕后,使用
gdb -v
查看是否安装完成GDB基础命令
命令 | 功能 |
gdb filename | 开始调试 |
run | 开始运行 |
run 1 2 3 | 开始运行,并且传入参数1,2,3 |
kill | 停止运行 |
quit | 退出gdb |
break sum | 在sum函数的开头设置断点 |
break *0x8048c3 | 在0x8048c3的地址处设置断点 |
delete 1 | 删除断点1 |
clear sum | 删除在sum函数入口的断点 |
stepi | 运行一条指令 |
stepi 4 | 运行4条指令 |
continue | 运行到下一个断点 |
disas sum | 反汇编sum函数 |
disas 0X12345 | 反汇编入口在0x12345的函数 |
print /x /d /t $rax | 将rax里的内容以16进制,10进制,2进制的形式输出 |
print 0x888 | 输出0x888的十进制形式 |
print (int)0x123456 | 将0x123456地址所存储的内容以数字形式输出 |
print (char*)0x123456 | 输出存储在0x123456的字符串 |
x/w $rsp | 解析在rsp所指向位置的word |
x/2w $rsp | 解析在rsp所指向位置的两个word |
x/2wd $rsp | 解析在rsp所指向位置的word,以十进制形式输出 |
info registers | 寄存器信息 |
info functions | 函数信息 |
info stack | 栈信息 |
实验过程
从main函数开始分析下反汇编。
大概分析了下主函数,主要还是传参和函数的调用,想要得出结果还是要看phase_1 ~ phase_6这些函数的反汇编。
Phase 1
查看
bomb.c
得到 Phase 1 相关的 C 代码。可以看出,程序先使用
read_line()
函数读取输入,并让 input 变量指向它。然后将其传给 phase_1
。此时 char
指针应该存储在 4(%esp)
中。然后查看 phase_1
的反汇编代码。包括 read_line
之类的函数并不需要详细了解,因为我们可以很容易看出来它干了什么。read_line函数会将读入字符串地址存放在rdi 和rsi中,strings_not_equal函数会使用edi和esi中的值当做两个字符址,并且判断他们是否相等,相等返回0
再看phase_1函数首先将0x402400这个赋值给esi,然后调用strings_not_equal, 刚才分析了,在每次调用phase_n之前都会先调用read_line读入一行并且放在edi和esi。显然这里是调用字符串比较函数比较我们输入的字符串和存放在0x402400地址的字符串是否相等,紧接着调用test指令,如果eax为0也就是两个字符串相等就跳转到函数结尾,否则调用explode_bomb函数,这个就是引爆炸弹的函数。到这里答案也就出来了,我们需要输入的就是存放在0x402400处的字符串。接下来用gdb开始调试
Phase 2
根据第五行,调用了
read_six_numbers
这个函数,显然这次要求的输入是6个数字,根据第八行和第九行,第一个数字必须是1,否则会爆炸,我们观察到如果第一个数字是1,跳转到+52,也就是lea 0x4(%rsp)
%rbx
,%rsp
是栈指针的地址,每个int的数据长度为4个bytes,这句话的意思就是说读取下一个数字的地址,存入%rbx
里。对于
read_six_numbers
可以将其反汇编:下一行有个奇怪的数值0x18,十进制为24,24/4=6正好是6个数字,这一行的目的就是设置一个结束点,放在%rbp中,然后回到+27.
仔细分析27行,不难发现,这段程序是在循环判断一个数组是否为公比为2的等比数列,如果不是则引爆炸弹,由于第一个数字是1,我们不难得出答案:
Phase 3
观察Phase 3函数
这段代码一上来调用了
sscan
f函数,通过查文档发现,这个函数是用来解析字符串里的数字,按照规定格式存到另一个字符串里,并返回所解析的数字的个数,第二个参数就是解析格式,print (char*)0x4025cf
,发现格式字符串为“%d %d”,也就是两个int数字.第7行,第8行说明输入的参数个数要大于1。第11行将第一个参数
0x8(%rsp)
和7比较,大于7则爆炸,说明输入的参数要小于等于7,同时ja为无符号跳转,则参数还有大于0,因此得出第一个参数的范围[0,7]。第14行为间接跳转,以 *0x402470
处的值为基地址,再加上8 * %rax
进行跳转,不同的 %rax
跳转到不同的位置。在内存中输入的六个数字分布如下:
分割出来的代码用类C语言可以表示为:
所以根据第一个值决定跳转位置的代码非常明显:
jmpq *0x402470(,%rax,8)
。我们可以使用 GDB 在查看跳转表的内容:这样我们就得到了跳转表:
值 | 目标值 |
0 | 207 |
1 | 311 |
2 | 707 |
3 | 256 |
4 | 389 |
5 | 206 |
6 | 682 |
7 | 327 |
也就是所,上表中的任意一对值都可以解除炸弹。
phase_4
从第5行看起,0x4025cf指向的地方存储的仍然是 两个int型整数。第8行和2比较,说明输入参数的个数为2。第10行和14比较,说明输入的第一个参数一定要小于14。第13,14,15行向func4()传递三个参数0x8(%rsp),0,14 。第17行测试函数返回值是否为0,要想不爆炸,函数返回值一定要为0。第19行说明输入的第二个参数一定要为0。
所以,我们要确定的是当输入的第一个参数为多少的时候,fun4()的返回值为0。下面看下fun4()的反汇编。
将汇编翻译为C如下所示:
当x == k时,返回值为0。所以第一个参数为7。
phase_5
关卡 5 的第7行 ~ 13行要求我们输入一个长度为 6 的字符串,否则就引爆。
接下来代码每次从字符串中取出一个字符,并做变换:
第15行 ~ 23行为一个循环。输入的字符串存储在%rbx中,第15行表示把输入字符串的第
%eax
个字符的ASCII码值给%ecx
,%cl
为%ecx
的低8位,所以第16行为取%ecx
的低八位。第18行表示再取低4位。
第19行的
0x4024b0
查看内容为maduiersnfotvbyl,这句话的意思是以0x4024b0
为基地址,以%rdx为偏移,从maduiersnfotvbyl字符串中取字符的低32位,结果放在%edx中。第20行,
%dl
中的值应为0x4024b0+%rdx
表示的字符,将其赋值给0x10(%rsp,%rax,1)
,最后计数器%rax+1
。第22行,表示是否循环够了6次。
第25行,
0x40245e
字符串为flyers
,比较两个字符串,如果%eax
为0(两个字符串相同),则解除炸弹,否则爆炸。所以,0x4024b0 + %rdx = {flyers的ASCII码}。
我们不知道变换方法是什么,但是代码中有一个突兀的地址,我们打印地址中的内容:
里面开头是一段奇怪的字符:”
maduiersnfotvbyl
”然后,将变换后的字符串和存储在 0x40245e 的字符串作比较,判断是否相当,如果相等,最解除成功。所以我们查看目标字符串:
在有了目标字符串后,我们现在知道了上述的字符串可以看做是一张查找表,可以得到对应关系:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | a | b | c | d | e | f |
m | a | d | u | i | e | r | s | n | f | o | t | v | b | y | l |
flyers对应的ascii值 0x66 0x6c 0x79 0x65 0x72 0x73。
- 与0x4024b0内存地址开始的查找表比较获得偏移量 0x9 0xF 0xE 0x5 0x6 0x72。
- 因此输入长度为6的字符串中每个字符的低4bit的值分别为0x9 0xF 0xE 0x5 0x6 0x72。
- 若输入为大写字母,将低4bit的值加上0x40,获得输入字符串IONEFG。
- 若输入为小写字母,将低4bit的值加上0x60,获得输入字符串ionefg。
所以,我们要得到 “flyers”,就应该输入“
9 f e 5 6 7
”,将数字对应到 ASCII 码中的字符,可以得到“IONEFG
”。phase_6
关卡 6 需要仔细分析,通过使用 GDB 单步运行的方法来分析了运行过程。
首先,函数要求读入 6 个数,并确认个数是否为 6。
然后,通过双重循环,判断 6 个数之间不存在重复
这里有一个细节,就是这段代码保存了和 7 的差:
通过分析这个结构,我们可以看到它是一个链表,定义类似于:
接下来,代码要求由大到小获取链表中的值,所以我们打印链表的节点值:
从大到小排列他们的索引分别是:3 4 5 6 1 2,考虑被 7 减的操作,所以答案是:4 3 2 1 6
总结
- 学会了 GDB 的使用方法,对调试又有了一定的认识
- 学会理解了栈帧的设计
- 熟悉了一些常用寄存器的用途
- 熟悉了 AT&T x86-64 汇编指令
- 作者:百川🌊
- 链接:https://www.baichuanweb.cn/article/example-35
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。