学习思考
实验 2:Bomb Lab
00 分钟
2023-4-1
2023-6-23
type
status
date
slug
summary
tags
category
icon
password
Property
Jun 23, 2023 11:48 AM

实验 2:Bomb Lab

实验环境

  • Ubuntu 20.04

实验要求

这个实验首先要求对汇编有一定的掌握,所以在此就不列举汇编的相关内容了。个人感觉用到最重要也是想要入手必须要具备的知识:一是学会使用反汇编及调试工具,二是了解函数调用的栈帧
这次的任务是破解二进制炸弹,一共有七关,六个常规关卡和一个隐藏关卡,每次我们需要输入正确的『拆弹密码』才能进入下一关,而具体的『拆弹密码』藏在汇编代码中。进入隐藏关卡的方式也在其中!这就需要我们一点一点探索蛛丝马迹了。

栈帧

想要了解栈帧的结构?我们还是先来回顾(review)以下有哪些和函数栈相关的寄存器吧。(这儿并没有包含浮点寄存器)
notion image
  • 所谓调用者保存,就是可以让被调用者(自身不作为另一个调用者)随意使用,也是为了自己用到的数据不被覆盖。
  • 所谓被调用者保存,恰恰与调用者保存相反。
  • 函数调用一般参数传递(非浮点)前 6 个参数存于寄存器,剩下的参数按照函数定义从右向左压栈
  • 栈指针指向函数栈栈顶。
  • %rax 用于保存函数调用返回值。
了解了这些寄存器,我们再来看看栈帧的结构
notion image
就拿函数 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查看是否安装完成
notion image

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开始调试
notion image

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函数
这段代码一上来调用了sscanf函数,通过查文档发现,这个函数是用来解析字符串里的数字,按照规定格式存到另一个字符串里,并返回所解析的数字的个数,第二个参数就是解析格式,print (char*)0x4025cf,发现格式字符串为“%d %d”,也就是两个int数字.
notion image
第7行,第8行说明输入的参数个数要大于1。第11行将第一个参数0x8(%rsp) 和7比较,大于7则爆炸,说明输入的参数要小于等于7,同时ja为无符号跳转,则参数还有大于0,因此得出第一个参数的范围[0,7]。第14行为间接跳转,以 *0x402470 处的值为基地址,再加上8 * %rax 进行跳转,不同的 %rax 跳转到不同的位置。
在内存中输入的六个数字分布如下:
notion image
分割出来的代码用类C语言可以表示为:
所以根据第一个值决定跳转位置的代码非常明显:jmpq *0x402470(,%rax,8)。我们可以使用 GDB 在查看跳转表的内容:
notion image
这样我们就得到了跳转表:
目标值
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。
notion image

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码}。
我们不知道变换方法是什么,但是代码中有一个突兀的地址,我们打印地址中的内容:
notion image
里面开头是一段奇怪的字符:”maduiersnfotvbyl
然后,将变换后的字符串和存储在 0x40245e 的字符串作比较,判断是否相当,如果相等,最解除成功。所以我们查看目标字符串:
notion image
在有了目标字符串后,我们现在知道了上述的字符串可以看做是一张查找表,可以得到对应关系:
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”。
notion image

phase_6

关卡 6 需要仔细分析,通过使用 GDB 单步运行的方法来分析了运行过程。
首先,函数要求读入 6 个数,并确认个数是否为 6。
然后,通过双重循环,判断 6 个数之间不存在重复
这里有一个细节,就是这段代码保存了和 7 的差:
通过分析这个结构,我们可以看到它是一个链表,定义类似于:
接下来,代码要求由大到小获取链表中的值,所以我们打印链表的节点值:
notion image
从大到小排列他们的索引分别是:3 4 5 6 1 2,考虑被 7 减的操作,所以答案是:4 3 2 1 6
notion image

总结

  • 学会了 GDB 的使用方法,对调试又有了一定的认识
  • 学会理解了栈帧的设计
  • 熟悉了一些常用寄存器的用途
  • 熟悉了 AT&T x86-64 汇编指令

评论
  • Twikoo