CSAPP 实验II Bomb Lab

非常有趣的一个实验!需要拆除6个炸弹,输错密码炸弹就会爆炸!

更新历史:

  • 23.03.14:初稿
  • 23.03.18:新增secret_phase

系列

任务目标

Bomb Lab实验,使用GDB工具拆除六个阶段的炸弹,需要仔细输入,爆炸次数过多实验会失败。

相关内容

  1. 下载自己的炸弹,不能修改bomb.c代码。

  2. 每爆炸一次失去一半的分数。

  3. 测试命令

    ./bomb:运行炸弹程序, psol.txt:文件输入答案

  4. 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 :显示字符串str
    x/4dw arr :取4个整型数字以十进制格式显示
    display arr:每次断点都打印数据,参数/x /s /u /c
    disas:反汇编当前函数
    info registers:显示寄存器内容

  5. objdump命令 反汇编

    objdump -t:打印变量内容的存储位置
    objdump -d bomb > bomb.s:反汇编生成bomb.s文件。需要使用gdb查看函数

  6. 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:关闭视窗

  7. 函数参数传递相关寄存器

    %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,打印出寄存器里的字符串内容。
做法:

  1. break phase_1 :打断点

  2. disas:显示汇编指令

  3. break explode_bomb :在爆炸函数打断点,防止进入这里引爆炸弹

  4. 根据汇编语言可以看出,输入的字符串在调用字符串对比函数中进行对比,根据返回值进行选择是否爆炸。
    break strings_not_equal :打断点

  5. 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。
做法:

  1. break phase_2 :打断点

  2. disas:显示汇编指令

    发现函数调用了read_six_numbers函数,所以继续在这里打断点,看具体输入了什么。

  3. break read_six_numbers,查看这个函数的汇编指令

    从调用sscanf函数之前可以看出输入格式在第二个参数里 ,以在调用之前打断点,打印rsi的内容。从调用爆炸函数的条件也可以看出来,sscanf返回值大于等于6的时候才会正常跳转,输入的数太少会直接爆炸。

    这道题的密码应该是6位数字,输入的内容保存在后面的传参里,地址在rsi寄存器里,根据phase_2的调用之前,rsi寄存器里存的是rsp的地址,所以可以判断出,读取函数把输入的数字存到了rsp内的地址里。

  4. 根据返回到phase_2函数的下一条指令cmpl $0x1,(%rsp)
    cmpl:代表双字,四个字节,用x/6dw $rsp:打印输入的内容看一下是不是自己输入的;根据跳转条件可以看出,第一个密码是1,

  5. 第一个密码正确后,依次看后面的指令:
    lea 0x4(%rsp),%rbx:下四个字节,第二个密码地址放到rbx里。
    lea 0x18(%rsp),%rbp:第24字节地址放到rbx里。相当于for循环结束条件
    mov -0x4(%rbx),%eax:第一个密码1存到eax
    add %eax,%eaxeax+eax乘2
    cmp %eax,(%rbx):对比第二个密码是不是2。
    add $0x4,%rbxrbx指向下四个字节,第三个密码。
    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.
做法:

  1. break phase_3 :打断点

  2. disas:显示汇编指令

    找到sscanf函数,和之前一样打断点查看输入格式。

  3. x/1sb $rsi:这道题的密码应该是两个数

  4. 基本步骤和上面一样,看一下密码对比的循环指令:
    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。
做法:

  1. break phase_4 :打断点

  2. disas:显示汇编指令

    找到sscanf函数,和之前一样打断点查看输入格式。这道题和上一道的rsi内容一样,说明都是两个数。

  3. 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函数。

  4. disas:打断点反汇编,区间(a,b)

    mov %edx,%eax:第三个参数14存到rax
    sub %esi,%eaxrax-rsi
    mov %eax,%ecx:把两个参数的差存到rcx 记为m
    shr $0x1f,%ecxecx逻辑右移31位,ecx是32位的,移动31位,只剩下了符号位。
    add %ecx,%eaxeax等于0+差。如果b-a=-1,ecx=1,eax+1=0
    sar %eaxeax右移一位,算术右移,等于7,m
    lea (%rax,%rsi,1),%ecx:加载有效地址,效果是rcx里存入了a+m
    cmp %edi,%ecx比较中间值是否等于第一个数字。
    如果大于:lea -0x1(%rcx),%edxb等于m-1。递归函数,左边区间查找是结束递归后rax乘2。
    小于等于:
    mov $0x0,%eaxeax等于0
    cmp %edi,%ecx:对比中间值和第一个数字
    jge 0x401007 <func4+57>:大于等于跳转,本来是小于等于,相等于等于跳转,所以结束标志是第一个参数等于中间值m。返回值rax为0。
    0x1(%rcx),%esi:小于时,a = m+ 1,继续递归,小于时结束递归后执行的是0x1(%rax,%rax,1),%eax,不为0。

  5. 根据func4返回后test %eax,%eax,如果不相等就爆炸,所以eax只能等于0,所以第一个数字一定是在二分查找的左边区间,所以可能值为7,3,1,0。

  6. 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’中的答案。
做法:

  1. break phase_5 :打断点

  2. disas:显示汇编指令

    根据字符串对比函数的下一条cmp $0x6,%eax,可以看出这道题密码长度为6。

  3. 分析跳转后的代码

    movzbl (%rbx,%rax,1),%ecxecx内存的是rbx的第rax个字节做零扩展到双字,如图是0x69rax相当于是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:循环结束判断

  4. 后续指令把所有的输入字符改变之后保存在了rsp + 0x10指向的地址里,然后调用strings_not_equal:我们可以根据查看传参,看一下标准答案是什么。

    可以看到密码是字符串flyers,对应的字节是0x66 0x6c 0x79 0x65 0x72 0x73

  5. 我们在打印一下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
做法:

  1. break phase_6 :打断点

  2. disas:显示汇编指令

  3. 根据指令,这道题是读取了六个数字,我们直接看后面的处理。输入的数字保存在rsp指向的地址里。每个数字存储在四个字节里
    mov %rsp,%r14:把输入的数组起始地址送到r14
    mov $0x0,%r12dr12d赋值0,记为i
    mov %r13,%rbprbp保存栈顶指针,数组起始地址
    mov 0x0(%r13),%eaxrax等于输入的第一个值
    sub $0x1,%eax:自减1
    cmp $0x5,%eax:判断rax是不是等于5
    jbe 0x401128 <phase_6+52>:无符号小于等于才不会爆炸,所以输入的数字自减前必须是1-6
    add $0x1,%r12di+1
    cmp $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,%ebxj++
    cmp $0x5,%ebx:循环跳出条件,后面的每一个数不能和第一个数相等。
    add $0x4,%r13:循环重复,i指向下一个数
    所有的数范围是1-6,且不重复时。

  4. lea 0x18(%rsp),%rsi:把最后一个数的地址送到rsi
    mov %r14,%rax:数组的第一个地址送到rax
    mov $0x7,%ecxrcx等于7
    mov %ecx,%edxrdx = 7
    sub (%rax),%edxrdx = 7 - *rax,数组的第一个数
    mov %edx,(%rax):覆盖这个数
    add $0x4,%rax:数组下一个
    cmp %rsi,%rax:跳出条件
    这段代码把所有数组的数,全部用7减,保存差

  5. mov $0x0,%esirsi = 0,记为i
    mov (%rsp,%rsi,1),%ecxrcx保存rsp的第i个字节地址的数字,记为x,注意数字在4的倍数字节处
    cmp $0x1,%ecxx == 1,1进行这个if判断
    如果x==1:
    mov $0x6032d0,%edxrdx赋值,这里要注意应该就是密码所在地
    如果x>1:
    mov $0x1,%eaxrax = 1
    mov $0x6032d0,%edx:
    mov 0x8(%rdx),%rdxrdx = (rdx + 8地址保存的数据),就是rdx地址指向的节点的next。
    add $0x1,%eaxrax + 1
    cmp %ecx,%eax:对比xrax
    不相等时rax一直加1,最终rdx指向第x个节点地址
    if判断结束:
    mov %rdx,0x20(%rsp,%rsi,2):把rdx数送入rsp + 2 i + 0x20,这个地址,相当于把数组整体换位置,后面是偏移量。
    add $0x4,%rsii = i + 4
    cmp $0x18,%rsi:结束条件
    这段循环把数组里的数,全部向后偏移保存了一个数组,这个数组中0,1对应的是固定值$0x6032d0,大于1的值都是`$0x6032d0 + 8
    (x - 1)`。新数组的每一个数长度是8字节,数据是一串地址。

    可以看出在rsp后0x20地址处,是六个地址。

  6. mov 0x20(%rsp),%rbxrbx等于新建数组的第一个数,记为i
    lea 0x28(%rsp),%raxrax是新数组第二个数的地址,记为&j
    lea 0x50(%rsp),%rsirsi数组最大地址
    mov %rbx,%rcxrcx存第一个数,记为i
    循环:
    mov (%rax),%rdxrdx:存下个数,记为j
    mov %rdx,0x8(%rcx):把j放到i后8个字节处
    add $0x8,%rax&j = &j + 8,后移八个字节
    cmp %rsi,%rax:对比有没有到了数组末尾
    没有到最后一个地址:
    mov %rdx,%rcxrcx = rdxi后移8个字节
    继续上面的循环
    如果到了最后一个地址:
    movq $0x0,0x8(%rdx):把0存到最后一个节点的后8字节处,尾结点的next=0。
    上面把所有链表连接起来了。链接方式是按照输入的六个数字进行连接,按照1-6的顺序,数字越大,地址越大。每个数字代表一个地址,从1-6链接起来。

  7. 下面进行的是爆炸判断:
    mov $0x5,%ebprbp = 5
    循环:
    mov 0x8(%rbx),%raxrax=第一个节点的next
    mov (%rax),%eaxrax=地址保存的数
    cmp %eax,(%rbx)next对比*this
    小于就爆炸:
    大于等于:
    mov 0x8(%rbx),%rbx:this向后移动8位
    sub $0x1,%ebprbp减1
    不等于0时,循环继续
    上面这段循环如果链表的数据表示的地址内存的数是递增的就爆炸了。所以就是要把链表调整为递减的链表,通过输入的1-6。

  8. 打印看一下这个$0x6032d0地址处保存的数据是什么?
    链接之前的链表:

    重新链接之后的链表:

    根据爆炸判断,这道题就是让链表排序成递减。

答案:

4 3 2 1 6 5

secret_phase

重新开始写解题笔记的时候,按照小土刀的博客搭建了hexo博客,根据他的文章格式写出了这篇解题笔记,在他的文章里发现还有一道彩蛋题目,藏在了phase_defused函数里。这个不仔细读汇编代码很难发现,因为一般不会看拆弹成功的函数。现在自己把这个彩蛋也做一下。

思路:

做法:

  1. 在函数phase_defused处打断点。

    发现这里有一个secret_phase函数,对这个函数打断点发现不会进入。
    在汇编代码里找一下函数入口。

  2. cmpl $0x6,0x202181(%rip) # 0x603760 <num_input_strings>:对比当前的关卡数,不等于6时跳转,等于6时,可能会进入彩蛋。

  3. mov $0x402619,%esimov $0x603870,%edi,根据这两个参数,应该是调用的sscanf函数,看一下输入参数是什么?

    可以看出密码是两个数字一个字符串。

  4. mov $0x402622,%esi:打印看一下这里存的是什么?

    可以确定第三个密码是DrEvil

  5. mov $0x4024f8,%edi
    mov $0x402520,%edi:打印看一下这里是什么?

  6. 修改一下我们的psol.txt,最后一行加上0 0 DrEvil,重新调试进入secret_phase看一下汇编代码,代码没有进入,说明输入不在最后,继续看一下汇编代码。

  7. 根据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。把输入文件改一下。

  8. 成功进入彩蛋函数,现在开始解这个彩蛋。
    这里要把彩蛋的输入放在第六道题的下一行,彩蛋的入口在第四道题的输入后面。
    disas:先看一下汇编代码

    mov $0xa,%edxrdx=10
    mov $0x0,%esirsi=0
    mov %rax,%rdirdi = read_line的返回值。就是我们输入的字符串地址。
    callq 0x400bd0 <strtol@plt>:调用函数,找开头的十进制数转换为long int,存在rax里,结尾的下一个地址放在rsi里。
    mov %rax,%rbxrbx=数字
    lea -0x1(%rax),%eaxrax-1存到rax
    cmp $0x3e8,%eax:对比如果,0x3e8是1000
    如果大于,爆炸:
    如果小于等于:
    mov %ebx,%esirsi=rbx
    mov $0x6030f0,%edirdi赋值,这里应该是密码所在地
    callq 0x401204 <fun7>:跳转到fun7函数

  9. 在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:赋值0
    cmp %esi,%edx:继续对比
    如果rdx==rsi:返回0。根据彩蛋的汇编代码,可以看出返回值等于2才可以拆弹成功。
    如果rdx<rsi
    mov 0x10(%rdi),%rdi:把rdi指向的地址+0x10,取数据送到rdi
    递归fun7。
    lea 0x1(%rax,%rax,1),%eaxrax左移一位再加1。
    如果rdx > rsi
    mov 0x8(%rdi),%rdi:把rdi指向的地址+0x8,取数据送到rdi
    递归fun7。
    add %eax,%eaxrax左移一位

  10. 根据上面的分析。递归的返回值一共四种
    (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)

  11. 打印一下链表存的数据是哪些?

    根据搜索路径可以看出,需要输入的密码应该是22。

  12. 测试一下

    彩蛋拆除成功!!

答案:22+任意字符串