CSAPP 实验II Bomb Lab
更新历史:
- 23.03.14:初稿
- 23.03.18:新增secret_phase
系列
- CSAPP - 笔记汇总
- I Data Lab - 位操作,数据表示
- II Bomb Lab - 汇编,栈帧与 gdb
- III Attack Lab - 漏洞是如何被攻击的
- IV Cache Lab - 实现一个缓存系统
- V Shell Lab - 实现一个Shell
任务目标
Bomb Lab实验,使用GDB工具拆除六个阶段的炸弹,需要仔细输入,爆炸次数过多实验会失败。
相关内容
下载自己的炸弹,不能修改bomb.c代码。
每爆炸一次失去一半的分数。
测试命令
./bomb
:运行炸弹程序,psol.txt
:文件输入答案GDB命令
gdb boom
:调试命令。参数:-e
:置顶可执行文件名quit
:退出调试run [参数]
:运行程序;< psol.txt
:文件输入continue
:断点继续print expr
:打印表达式值,区别:x打印的内存的值nexti
:单步执行,不进入函数调用stepi
:单步执行进入函数调用break function
:函数处断点break *address
:内存地址处断点delete n
:删除n号断点delete
:删除所有断点finish
:运行的当前函数返回停止x/5cb str
:显示5个单字节;h
:两个;w
:四个;g
:八个x/1sb str
:显示字符串strx/4dw arr
:取4个整型数字以十进制格式显示display arr
:每次断点都打印数据,参数/x /s /u /c
。disas
:反汇编当前函数info registers
:显示寄存器内容objdump命令
反汇编
objdump -t
:打印变量内容的存储位置objdump -d bomb > bomb.s
:反汇编生成bomb.s文件。需要使用gdb查看函数
screen 命令
分屏看汇编文件,也可以不分屏在GDB中使用disas命令
screen -ls
:查看所有screen终端screen -S name
:创建终端screen -r name or id
:连接终端。连接失败用screen -d **
:连接exit
:退出终端Ctrl + a,d
:暂离;c
:创建子会话;w
:子会话列表;p
:上一个;n
:下一个;0-9
:切换;:
:命令行模式(resize 80
)调整窗口大小。screen -S id -X quit
:关闭视窗函数参数传递相关寄存器
%rax
:返回值%rdi
:第一个参数%rsi
:第二个参数%rdx
:第三个参数%rcx
:第四个参数%d8
,%d9
,%d10
依次是后面的参数
题目及解法
思路
先从源码bomb.c
可以看出调用顺序,通过函数explode_bomb
让炸弹爆炸,所有我们在调试过程中,在这个函数处打断点,每次只要输入错误密码,炸弹爆炸的时候程序都会进入断点,这样就不会扣分了。从源码里还能看出每一个关卡的函数都是phase_x
,所以每次调试之前,在爆炸处打断点,在phase_x
处打断点,进入这关之后,用反汇编命令disas
,显示函数的汇编代码,然后进行解题。
phase_1
思路:
第一个比较简单,找到字符串对比函数,参数1放在寄存器rdi里,参数2放在寄存器rsi里。
在字符串对比函数内打断点,命令x/1sb $rsi
,打印出寄存器里的字符串内容。
做法:
break phase_1
:打断点disas
:显示汇编指令break explode_bomb
:在爆炸函数打断点,防止进入这里引爆炸弹根据汇编语言可以看出,输入的字符串在调用字符串对比函数中进行对比,根据返回值进行选择是否爆炸。
break strings_not_equal
:打断点x/1sb $rsi
:打印$rsi
内容,根据phase_1函数的汇编指令中,改变了rsi内容,说明我们输入的密码在$rdi
中,解密的密码在$rsi
中。
答案:
Border relations with Canada have never been better.
phase_2
思路:
第二个主要是是在函数分配的栈里面进行操作,通过sscanf函数的第二个参数,format,”%d %d %d %d %d %d”,可以看出密码应该是六个数,通过记录的%rsi
是phase2函数的%rsp
,可以知道最终密码应该是保存在主函数的栈区,第一次和1比较,相同才能继续,第一个数字是1, 后面每一个数字都是和前一个数字*2相比较,那么就是一个等比数列,公比2。
做法:
break phase_2
:打断点disas
:显示汇编指令发现函数调用了read_six_numbers函数,所以继续在这里打断点,看具体输入了什么。
break read_six_numbers
,查看这个函数的汇编指令从调用sscanf函数之前可以看出输入格式在第二个参数里 ,以在调用之前打断点,打印
rsi
的内容。从调用爆炸函数的条件也可以看出来,sscanf返回值大于等于6的时候才会正常跳转,输入的数太少会直接爆炸。这道题的密码应该是6位数字,输入的内容保存在后面的传参里,地址在
rsi
寄存器里,根据phase_2的调用之前,rsi
寄存器里存的是rsp
的地址,所以可以判断出,读取函数把输入的数字存到了rsp
内的地址里。根据返回到phase_2函数的下一条指令
cmpl $0x1,(%rsp)
cmpl
:代表双字,四个字节,用x/6dw $rsp
:打印输入的内容看一下是不是自己输入的;根据跳转条件可以看出,第一个密码是1,第一个密码正确后,依次看后面的指令:
lea 0x4(%rsp),%rbx
:下四个字节,第二个密码地址放到rbx
里。lea 0x18(%rsp),%rbp
:第24字节地址放到rbx
里。相当于for循环结束条件
mov -0x4(%rbx),%eax
:第一个密码1存到eax
。add %eax,%eax
:eax
+eax
。乘2
cmp %eax,(%rbx)
:对比第二个密码是不是2。add $0x4,%rbx
:rbx
指向下四个字节,第三个密码。cmp %rbp,%rbx
:对比是不是到了结束末尾,否则继续进行循环。
所以这六个数字密码是一个1开头的等比数列,公比为2。
答案:
1 2 4 8 16 32
phase_3
思路:
第三个和第二个相同,查看输入的是两个%d,存在phase3的栈里面,不一样的地方是,这道题目的答案有7种,因为第一个密码小于7,所有一共有8组答案,分别是0,207, 1-0x137,2-0x2c3,3-0x100,4-0x185,5-0xce, 6-0x2aa, 7-0x147.
做法:
break phase_3
:打断点disas
:显示汇编指令找到
sscanf
函数,和之前一样打断点查看输入格式。x/1sb $rsi
:这道题的密码应该是两个数基本步骤和上面一样,看一下密码对比的循环指令:
cmpl $0x7,0x8(%rsp)
:对比第一个数字和7的大小ja 0x400fad <phase_3+106>
:无符号大于就跳转,跳转之后调用了爆炸函数,说明第一个数字小于等于7并且大于等于0负数当无符号数时第一位总是1,一定比8大
。mov 0x8(%rsp),%eax
:把第一个数字放到eax
里。jmpq *0x402470(,%rax,8)
:跳转到*(0x402470 + (%rax * 8))
打印一下从0x402470
这里开始存的是什么:x/8xg 0x402470
或者x/64xb 0x402470
:这里要注意数据是按照大端法存储的。可以看出对应跳转关系,跳转后执行的指令为:
mov $0xcf,%eax
,根据不同的第一位数字得到的为:0--0xcf
1--0x137
2--0x2c3
3--0x100
4--0x185
5--0xce
6--0x2aa
7--0x147
答案:
0 207
phase_4
思路:
第四个主要难点在func4函数里有一个单操作数的sar指令,之前一直以为移位的数存在寄存器cl里,后来才发现这是默认移位1,所有这个题的密码第一位是在用二分法在(0-14)中找值,但是由于函数返回值是0才不会爆炸,往右半部分查找时,返回值不为0,所以密码只能在左半部分,分别是7,3,1,0,四种答案,第二个密码是在phase_4函数里,对比的数字是0,所以第二个密码是0。
做法:
break phase_4
:打断点disas
:显示汇编指令找到
sscanf
函数,和之前一样打断点查看输入格式。这道题和上一道的rsi
内容一样,说明都是两个数。cmpl $0xe,0x8(%rsp)
:jbe 0x40103a <phase_4+46>
:小于等于跳转,说明第一个密码小于等于14。mov $0xe,%edx
:14存到rdx
第三个参数。mov $0x0,%esi
:0存到rsi
第二个参数。mov 0x8(%rsp),%edi
:第一个密码存到rdi
第一个参数。callq 0x400fce <func4>
:调用func4函数。disas
:打断点反汇编,区间(a,b)mov %edx,%eax
:第三个参数14存到rax
sub %esi,%eax
:rax
-rsi
mov %eax,%ecx
:把两个参数的差存到rcx
记为mshr $0x1f,%ecx
:ecx
逻辑右移31位,ecx
是32位的,移动31位,只剩下了符号位。add %ecx,%eax
:eax
等于0+差。如果b-a=-1,ecx=1,eax+1=0
sar %eax
:eax
右移一位,算术右移,等于7,mlea (%rax,%rsi,1),%ecx
:加载有效地址,效果是rcx
里存入了a+mcmp %edi,%ecx
比较中间值是否等于第一个数字。
如果大于:lea -0x1(%rcx),%edx
b等于m-1。递归函数,左边区间查找是结束递归后rax
乘2。
小于等于:mov $0x0,%eax
:eax
等于0cmp %edi,%ecx
:对比中间值和第一个数字jge 0x401007 <func4+57>
:大于等于跳转,本来是小于等于,相等于等于跳转,所以结束标志是第一个参数等于中间值m。返回值rax
为0。0x1(%rcx),%esi
:小于时,a = m+ 1,继续递归,小于时结束递归后执行的是0x1(%rax,%rax,1),%eax
,不为0。根据
func4
返回后test %eax,%eax
,如果不相等就爆炸,所以eax只能等于0,所以第一个数字一定是在二分查找的左边区间,所以可能值为7,3,1,0。cmpl $0x0,0xc(%rsp)
:相等时函数结束,说明第二个密码是固定值0。
答案:
0 0
phase_5
思路:
第五个比较有趣,还是和12一样计算字符串长度,知道密码应该是一个6位长度字符串,计算字符串是不是相等函数的参数显示待匹配的字符串是”flyers”,编译的时候用了canary,栈指针每次都不同,这个对解密应该是没影响,主要是输入的字符串,每一个字节只保留了低四位,然后加上一个地址,将内存中的值送到了rsp里,那么这个地址应该是一个数组,打印一下这个地址开始的15个字节,是一串字符maduiersnfotvbyl,需要我们从里面找出密码,位置分别是9,15,14,5,6,7。所以输入的字符串的ASCII编码后四位应该就是这个值,答案比较多,找
了一个’a’-‘z’中的答案。
做法:
break phase_5
:打断点disas
:显示汇编指令根据字符串对比函数的下一条
cmp $0x6,%eax
,可以看出这道题密码长度为6。分析跳转后的代码
movzbl (%rbx,%rax,1),%ecx
:ecx
内存的是rbx
的第rax
个字节做零扩展到双字,如图是0x69
;rax
相当于是for循环里的i。mov %cl,(%rsp)
:把rcx
的低八位送入栈顶指针指向的地址。mov (%rsp),%rdx
:把值继续送到rdx
。and $0xf,%edx
:只保留rdx
的低四位。movzbl 0x4024b0(%rdx),%edx
:把rdx
的值加上0x4024b0
,新地址的内容存到edx
里。mov %dl,0x10(%rsp,%rax,1)
:把rdx
的低八位存入rax+rsp+0x10
的位置。add $0x1,%rax
:i自加cmp $0x6,%rax
:循环结束判断后续指令把所有的输入字符改变之后保存在了
rsp + 0x10
指向的地址里,然后调用strings_not_equal
:我们可以根据查看传参,看一下标准答案是什么。可以看到密码是字符串
flyers
,对应的字节是0x66 0x6c 0x79 0x65 0x72 0x73
。我们在打印一下for循环里跳转的地址
0x4024b0
保存的是什么?由于偏移量总是低四位,范围是0-15,所以字符串只需要关注
maduiersnfotvbyl
前面这些,需要我们从里面找出密码,位置分别是9,15,14,5,6,7。所以输入的字符串的ASCII编码后四位应该就是这个值,答案比较多,找了一个’a’-‘z’中的答案。
答案:
ionefg
phase_6
思路:
第六个是一个链表,节点的前八个字节中低位是数据,高位是输入的密码,高位的8个字节是下一跳地址,根据输入的6个数必须是1-6,切两两不相同,还进行了一次处理,每一位密码x,变成了y = 7-x。
然后是链表的操作,每一个密码的节点对应的是(y - 1) * 8 + 结点首地址, 分配好结点后进行结点连接。
每一位密码的下一跳就是下一位密码对应的结点地址,最后一位结点的下一跳是0;要求是必要让结点中的数据,满足前一个结点大于等于后一个结点,
而所有结点的数据分别是:
(1):332
(2):168
(3):924
(4):691
(5):477
(6):443
所以数据最大的结点应该在最前面,序号应该是3 4 5 6 1 2。
而输入的密码应该是7 - y: 4 3 2 1 6 5
做法:
break phase_6
:打断点disas
:显示汇编指令根据指令,这道题是读取了六个数字,我们直接看后面的处理。输入的数字保存在
rsp
指向的地址里。每个数字存储在四个字节里mov %rsp,%r14
:把输入的数组起始地址送到r14
mov $0x0,%r12d
:r12d
赋值0,记为i
mov %r13,%rbp
:rbp
保存栈顶指针,数组起始地址mov 0x0(%r13),%eax
:rax
等于输入的第一个值sub $0x1,%eax
:自减1cmp $0x5,%eax
:判断rax
是不是等于5jbe 0x401128 <phase_6+52>
:无符号小于等于才不会爆炸,所以输入的数字自减前必须是1-6
。add $0x1,%r12d
:i
+1cmp $0x6,%r12d
:循环结束标志,相等跳转
不相等:mov %r12d,%ebx
:把i
送到rbx
, 记为j = i + 1
。movslq %ebx,%rax
:符号扩展送到rax
,等于j
mov (%rsp,%rax,4),%eax
:输入的第j
个数 送到rax
cmp %eax,0x0(%rbp)
:对比rax
和栈顶指针指向的数是否相同,相等就爆炸,数字不能重复。add $0x1,%ebx
:j++
cmp $0x5,%ebx
:循环跳出条件,后面的每一个数不能和第一个数相等。add $0x4,%r13
:循环重复,i
指向下一个数
所有的数范围是1-6
,且不重复时。lea 0x18(%rsp),%rsi
:把最后一个数的地址送到rsi
mov %r14,%rax
:数组的第一个地址送到rax
mov $0x7,%ecx
:rcx
等于7mov %ecx,%edx
:rdx
= 7sub (%rax),%edx
:rdx
= 7 -*rax
,数组的第一个数mov %edx,(%rax)
:覆盖这个数add $0x4,%rax
:数组下一个cmp %rsi,%rax
:跳出条件
这段代码把所有数组的数,全部用7减,保存差mov $0x0,%esi
:rsi
= 0,记为i
mov (%rsp,%rsi,1),%ecx
:rcx
保存rsp
的第i
个字节地址的数字,记为x
,注意数字在4的倍数字节处cmp $0x1,%ecx
:x
== 1,1进行这个if判断
如果x==1:mov $0x6032d0,%edx
:rdx
赋值,这里要注意应该就是密码所在地
如果x>1:mov $0x1,%eax
:rax
= 1mov $0x6032d0,%edx
:mov 0x8(%rdx),%rdx
:rdx
= (rdx
+ 8地址保存的数据),就是rdx
地址指向的节点的next。add $0x1,%eax
:rax
+ 1cmp %ecx,%eax
:对比x
和rax
不相等时rax
一直加1,最终rdx
指向第x个节点地址
if判断结束:mov %rdx,0x20(%rsp,%rsi,2)
:把rdx
数送入rsp
+ 2 i + 0x20,这个地址,相当于把数组整体换位置,后面是偏移量。add $0x4,%rsi
:i
=i
+ 4cmp $0x18,%rsi
:结束条件
这段循环把数组里的数,全部向后偏移保存了一个数组,这个数组中0,1对应的是固定值$0x6032d0
,大于1的值都是`$0x6032d0 + 8 (x - 1)`。新数组的每一个数长度是8字节,数据是一串地址。可以看出在
rsp
后0x20地址处,是六个地址。mov 0x20(%rsp),%rbx
:rbx
等于新建数组的第一个数,记为i
lea 0x28(%rsp),%rax
:rax
是新数组第二个数的地址,记为&j
lea 0x50(%rsp),%rsi
:rsi
数组最大地址mov %rbx,%rcx
:rcx
存第一个数,记为i
循环:mov (%rax),%rdx
:rdx
:存下个数,记为j
mov %rdx,0x8(%rcx)
:把j
放到i
后8个字节处add $0x8,%rax
:&j = &j + 8
,后移八个字节cmp %rsi,%rax
:对比有没有到了数组末尾
没有到最后一个地址:mov %rdx,%rcx
:rcx
=rdx
,i
后移8个字节
继续上面的循环
如果到了最后一个地址:movq $0x0,0x8(%rdx)
:把0存到最后一个节点的后8字节处,尾结点的next=0。
上面把所有链表连接起来了。链接方式是按照输入的六个数字进行连接,按照1-6的顺序,数字越大,地址越大。每个数字代表一个地址,从1-6链接起来。下面进行的是爆炸判断:
mov $0x5,%ebp
:rbp
= 5
循环:mov 0x8(%rbx),%rax
:rax
=第一个节点的nextmov (%rax),%eax
:rax
=地址保存的数cmp %eax,(%rbx)
:next对比*this
小于就爆炸:
大于等于:mov 0x8(%rbx),%rbx
:this向后移动8位sub $0x1,%ebp
:rbp
减1
不等于0时,循环继续
上面这段循环如果链表的数据表示的地址内存的数是递增的就爆炸了。所以就是要把链表调整为递减的链表,通过输入的1-6。打印看一下这个
$0x6032d0
地址处保存的数据是什么?
链接之前的链表:重新链接之后的链表:
根据爆炸判断,这道题就是让链表排序成递减。
答案:
4 3 2 1 6 5
secret_phase
重新开始写解题笔记的时候,按照小土刀的博客搭建了hexo博客,根据他的文章格式写出了这篇解题笔记,在他的文章里发现还有一道彩蛋题目,藏在了phase_defused
函数里。这个不仔细读汇编代码很难发现,因为一般不会看拆弹成功的函数。现在自己把这个彩蛋也做一下。
思路:
做法:
在函数
phase_defused
处打断点。发现这里有一个
secret_phase
函数,对这个函数打断点发现不会进入。
在汇编代码里找一下函数入口。cmpl $0x6,0x202181(%rip) # 0x603760 <num_input_strings>
:对比当前的关卡数,不等于6时跳转,等于6时,可能会进入彩蛋。mov $0x402619,%esi
和mov $0x603870,%edi
,根据这两个参数,应该是调用的sscanf函数,看一下输入参数是什么?可以看出密码是两个数字一个字符串。
mov $0x402622,%esi
:打印看一下这里存的是什么?可以确定第三个密码是
DrEvil
mov $0x4024f8,%edi
:mov $0x402520,%edi
:打印看一下这里是什么?修改一下我们的
psol.txt
,最后一行加上0 0 DrEvil
,重新调试进入secret_phase
看一下汇编代码,代码没有进入,说明输入不在最后,继续看一下汇编代码。根据sscanf的第一个参数地址在
0x603870
,我们就找一下之前输入的数据哪一个存放在这周围,在bomb.s
文件里搜一下最近的是read_line
函数里的0x603780
。
看一下附件的代码:lea (%rax,%rax,4),%rsi
:shl $0x4,%rsi
:add $0x603780,%rsi
:rax
放的是题目序号,乘5,左移4位,加上0x603780
后等于我要的值,rax
应该等于3。也就是说第三道题目解出来之后,第四道题目的输入缓冲区在0x603870
。把输入文件改一下。成功进入彩蛋函数,现在开始解这个彩蛋。
这里要把彩蛋的输入放在第六道题的下一行,彩蛋的入口在第四道题的输入后面。disas
:先看一下汇编代码mov $0xa,%edx
:rdx
=10mov $0x0,%esi
:rsi
=0mov %rax,%rdi
:rdi
= read_line的返回值。就是我们输入的字符串地址。callq 0x400bd0 <strtol@plt>
:调用函数,找开头的十进制数转换为long int,存在rax
里,结尾的下一个地址放在rsi
里。mov %rax,%rbx
:rbx
=数字lea -0x1(%rax),%eax
:rax
-1存到rax
里cmp $0x3e8,%eax
:对比如果,0x3e8是1000
如果大于,爆炸:
如果小于等于:mov %ebx,%esi
:rsi
=rbx
mov $0x6030f0,%edi
:rdi
赋值,这里应该是密码所在地callq 0x401204 <fun7>
:跳转到fun7函数在fun7函数里打断点,看下汇编代码
test %rdi,%rdi
:testrdi
如果等于0就跳转:mov $0xffffffff,%eax
:赋值32位全1,返回
不等于0:mov (%rdi),%edx
:把rdi
地址保存的数据$
给rdx
cmp %esi,%edx
:对比rdx
和我们输入的密码jle 0x401220 <fun7+28>
:
如果rdx
<=rsi
:mov $0x0,%eax
:赋值0cmp %esi,%edx
:继续对比
如果rdx
==rsi
:返回0。根据彩蛋的汇编代码,可以看出返回值等于2才可以拆弹成功。
如果rdx
<rsi
:mov 0x10(%rdi),%rdi
:把rdi
指向的地址+0x10,取数据送到rdi
递归fun7。lea 0x1(%rax,%rax,1),%eax
:rax
左移一位再加1。
如果rdx
>rsi
:mov 0x8(%rdi),%rdi
:把rdi
指向的地址+0x8,取数据送到rdi
递归fun7。add %eax,%eax
:rax
左移一位根据上面的分析。递归的返回值一共四种
(1):返回32位1
(2):左移一位
(3):0
(4):左移一位加1
要想拆掉炸弹,需要返回的是2。
返回值顺序是:左移一位<左移一位加1<返回0
先从内层递归找条件:fun(x,y)
返回0:rdi
指向的数 == y
左移一位加1:rdi
指向的数 < y,这里x+16字节
左移一位:rdi
指向的数 > y,这里x+8字节
递归顺序为:
fun(x,y) -> fun(x + 8,y) -> fun(x+16,y)打印一下链表存的数据是哪些?
根据搜索路径可以看出,需要输入的密码应该是22。
测试一下
彩蛋拆除成功!!
答案:22
+任意字符串