CSAPP Bomb Lab

看完了 csapp 的第三章关于汇编的内容,就做了下对应的 lab 练习一下,能把汇编练习设计成诸多拆弹关卡而且还有彩蛋关卡还是挺有意思的。

首先准备好环境及工具。

环境及工具

ubuntu 14.04.1 x86_64
gdb 7.7.1
objdump

准备

通过 wget 下载 bomb 压缩包

1
wget http://csapp.cs.cmu.edu/im/labs/bomblab.tar

解压缩

1
tar -xvf bomb.tar

反汇编执行文件

1
objdump -d bomb > bomb.txt

发现在main函数下面有6个以 phase 开头的函数,说明有6个关卡,搜索 phase 发现还有个隐藏关卡,估计是个彩蛋。

那先从第一个关卡开始吧!

第一关

1
2
3
4
gdb bomb
b phase_1
run
disas

发现它调用了一个 strings_not_equal 函数,把传进去的参数打印出来。

1
x/s 0x402400

得到个字符串,这就是第一关答案。

第二关

接着来看第二关,先b phase_2打个断点然后运行。

phase_2 这个函数先是栈指针下移分配一段内存空间,然后把 rsp 所指向的地址赋给 rsi ,接着调用读取六个数字的方法,可以知道这一关需要输入6个数字,根据 cmpl $0x1,(%rsp) 可以知道第一个数为1。接着做的是把第二个数的地址存到 rbx 中,把最后一个数的地址存到 rbp 中,然后是把前一个数赋给 eax ,当前数要是前一个数的两倍,否则炸弹会爆炸,接着 rbp 后移4个字节指向下一个数,循环上述步骤直到遍历检查所有数字,所以这一关应该输入 1 2 4 8 16 32

第三关

看到第三关先是对输入的数做了一个检查,那就先打印出要求输入的格式。

发现下面是 switch 语句,跳转到的地址间的间隔为8个字节,rax 取值为0-7,打印出这个数组可得

gdb 打印以4个字节为一个单位所以可以得到0-7对应的跳转地址为

  • 0 -> 0x400f7c
  • 1 -> 0x400fb9
  • 2 -> 0x400f83
  • 3 -> 0x400f8a
  • 4 -> 0x400f91
  • 5 -> 0x400f98
  • 6 -> 0x400f9f
  • 7 -> 0x400fa6

跳转完后第二个数有对应的赋值,把16进制的数转化为10进制的数后可以得到8组答案:

  • 0 207
  • 1 311
  • 2 707
  • 3 256
  • 4 389
  • 5 206
  • 6 682
  • 7 327

第四关

首先调用 “x/s 0x4025cf” 打印出输入格式

接着往下看,

表明第一个数要小于等于14,接着<+46>,<+51>,<+56>在赋值要传入的参数,调用 func4 函数,stepi 进入此函数

这个函数返回会对返回结果做一个与运算,如果结果不为0仍然会爆炸,所以必须确保 func4 调用完成后返回0,根据传进来的参数为func4(int x,int y,int z),y = 0,z = 14; 执行指令下去可得x>=7时会跳转到处执行,接着x<=7会跳转到增加栈指针返回,此时%rax存放的是0,如果不满足上面两个条件则会进入递归调用,递归调用后%rax的值就不为0了,所以第一个值为7。

第二个值返回到phase_4的调用就可以得出为0,很好判断,所以这个关卡输入为 7 0

第五关

这一关在有个指令调用了string_length方法,然后把返回值与6比较,如果相等的话会调转到的位置,不相等的话会爆炸,推测出输入的字符串长度为6,接着先把 %eax 赋为 0,跳转到, 是个 dowhile 循环,当 %rax 的值等于 6 时退出,在循环开始时 %rbx 的值存的是 %rdi ,也就是传进去的字符串地址,这个循环内部顺序取出字符串的每个字节,把每个字节与 0xf 做相与运算,把运算结果作为偏移量取出地址为 0x4024b0 的字符串中对应的字符,之后把这个字符存到 0x10(%rsp,%rax,1) 的内存地址上。循环结束后,会把这个字符串与地址 0x40245e 上的做比较,不相等则会爆炸。

打印 0x4024b0 处的字符串

0x40245e 上的字符串,为”flyers”

现在要跟1111做相与运算得到各字符的偏移量,比如要得到结果为 ‘m’ 的字符,字符后四位必须为 0000 查看 ascii 对照码可以得到很多种答案,现在快速求出一种结果直接使用从字符 ‘a’ 开始方便运算。

根据 maduiersnfotvbyl 得出各字符转换前的字符

  • m <- p
  • a <- a
  • d <- b
  • u <- c
  • i <- d
  • e <- e
  • r <- f
  • s <- g
  • n <- h
  • f <- i
  • o <- j
  • t <- k
  • v <- l
  • b <- m
  • y <- n
  • l <- o

所以要使结果为 “flyers” ,输入的字符可以是 “ionefg”

第六关

汇编代码比较长,分开来一步步看,首先调用一个 read_six_numbers 的函数,可以知道输入的字符串为6个数字,接着一步是循环检验所有的数是否都小于等于6,接着循环把栈指针指向的内存段上的数组赋值为7-对应下标的值的数组。

开始,它得出了对应1,2,3,4,5,6的数值,并且打印0x6032d0,0x6032d8发现他是一个链表,不过在这个阶段还没链接好,等下一个阶段再进行链接,对应的结构体是一个数值,被7减后的输入值,结构体地址。

1
2
3
4
5
struct Node{
int num;
int seven_sub_input;
Node *next;
}

打印出地址上所对应的值以及与被7减后输入值的对应关系,具体如下所示

  • 332 -> 1
  • 168 -> 2
  • 924 -> 3
  • 691 -> 4
  • 477 -> 5
  • 443 -> 6

再接着就是链接各结点,然后判断 num 是否是从大到小排列,可得出 seven_sub_input 的顺序为 3,4,5,6,1,2 ,被7减之前的输入数为 4 3 2 1 6 5。

隐藏关卡

之前看到还有隐藏关卡在里面,那就先来看下如何进入隐藏关卡,先找到它在哪个位置被调用。

1
vi bomb.txt

它是在phase_defused函数中被调用的,可以用 gdb 来打断点调试下。

1
2
3
4
gdb bomb
set args sol.txt
b phase_defused
run

大致看下这段反汇编代码,首先是要有6个字符串输入,再打个断电在的地方,看它是在哪一关卡可以开启隐藏关卡

1
2
3
4
clear phase_defused
b *0x00000000004015f5
run
disas

再打印几个内存地址上的字符串

0x603870 地址上是字符串 “7 0”0x402619 地址上是字符串 “%d %d %s” 可以知道是第四个关卡开启隐藏关卡,最后一个字符串要输对才能进入,至于对应的字符串就存在地址 0x402622 上面,为 “DrEvil” ,加到sol.txt里面开启隐藏关卡!

经历了前面6关特别是第6关的拆弹,继续拆隐藏关卡的炸弹就轻松不少。这段汇编代码首先是读入输入的字符串,对输入的数字要求小于等于1001,然后把对应的数值赋到 %esi ,第一个参数赋值为 0x6030f0 这个内存地址,首先打印这个内存地址存的数值。

接着 stepi 进入 fun7 函数。

这个函数涉及到递归调用,为了更好地分析可以把对应的 C 代码写一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int fun7(Type* x,int n){
if(x == null){
return 0xffffffff;
}
int result;
if((*x).value <= n){
result = 0;
if((*x).value == n){
return result; //1
}else{
x = (*x).second_node;
return 2 * fun7(x,n) + 1; //2
}
}else{
x = (*x).first_node;
return 2 * fun7(x,n); //3
}
}

因为打印出 rdi 后8个字节和后16个字节发现他们是一个地址,可以想到的结构是二叉树,所以 x 是个结构体指针,结合之前 secret_phase 的汇编代码可以知道只有返回值为 2 是才不会引爆炸弹,所以为了让 %rax 的值为2,需要递归调用顺序为 3,2,1

按照汇编代码中取地址位数的方式打印出对应地址所指向的地址和值可得

所以答案是22!

至此所有的炸弹拆除完毕。