CSAPP lab2 二进制拆弹 binary bombs secret_phase(完结撒花?)
给出对应于7个阶段的7篇博客
phase_1 https://www.cnblogs.com/wkfvawl/p/10632044.html
phase_2 https://www.cnblogs.com/wkfvawl/p/10636214.html
phase_3 https://www.cnblogs.com/wkfvawl/p/10651205.html
phase_4 https://www.cnblogs.com/wkfvawl/p/10672680.html
phase_5 https://www.cnblogs.com/wkfvawl/p/10703941.html
phase_6 https://www.cnblogs.com/wkfvawl/p/10742405.html
secret_phase https://www.cnblogs.com/wkfvawl/p/10745307.html
1.1 打开隐藏关
根据前面的提示,在第四阶段输入特殊字符,会引发隐藏关,但是我们在第四阶段的分析中,并没有找到相关代码。
实际上,我们每个阶段通过之后,都调用了phase_defused函数,可以分析里面代码,看看是否在里面。(分析证明,确实在里面)。
1.2 phase_defused分析
在asm.txt中寻找phase_defused,可以找到该函数的入口地址0x80495d4。
1、1127-1132行:准备函数栈帧,并调用send_msg函数(send_msg是向服务器发送信息,也就是是否通过某一阶段,会向服务器发送信息,send_msg不做进一步分析)。此时函数栈帧如下:
2、1132-1133行:将0x804c808地址的内容与6比较,如果不等,则跳转到8049679 <phase_defused+0xa5>(1163行(8049679 <phase_defused+0xa5>):判断栈是否溢出,如果没有,正常返回。(注意:esp+0x7c处存放的“哨兵”))(问题:0x804c808地址内容是什么?通过objdump --start-address=0x804c808 -s bomb找不到该地址内容)
3、1135-1143行:如果相等,则继续从1135行执行。这些代码实际上是在为调用sscanf函数准备参数:
1)1141行:0x804a78b放入到esp + 4。使用objdump --start-address=0x804a78b -s bomb,可以查看该地址内容(这是个字符串常量,在只读数据段中):"%d %d %s",可以判断,要读入的数据应该包含两个整数,一个字符串。
2)1143行:0x804c910放入到esp。根据前面各个阶段的分析,0x804c910应该存放的是要读入的字符串。(0x804c910是什么?使用objdump --start-address=0x804c910 -s bomb找不到该地址内容)
3)1135-1140行:准备返回读出的数据:两个整数,一个字符串。
4)函数栈帧变为:
5)从以上函数栈帧可以看出,读出的数据最终放置在esp+24(d1)、esp+28(d2)、esp+2c(str)的地方。sscanf函数返回结果存储在eax中(读出的数据个数)。
4、1145 - 1146行:如果读出的数据不是3个,则跳转到8049661 <phase_defused+0x8d>(第1159行)
5、1159-1168行:显示了0x804a640以及0x804a66c地址的内容,然后退出了(puts函数:在屏幕上打印)。使用objdump --start-address=0x804a640 -s bomb可以查看这两个地址的内容,如下图所示:
显然,这就是我们看到的通过某个阶段的信息,但这里我们仍然没有打开隐藏关。
6、如果读出的数据是3个,继续从1147行开始执行。
7、1147-1150行:为调用strings_not_equal做准备:
1)1147行:0x804a794 --> esp + 4
2)1149-1150行:sscanf函数读出的str -->esp
3)显然,strings_not_equal是要比较前述两个字符串,0x804a794的内容(objdump --start-address=0x804a794 -s bomb):
显然,这个要比较的字符串是:“DrEvil”
8、1152-1153行:判断以上两个字符串是否相等(相等,eax返回为0),如果不等,跳转到8049661 <phase_defused+0x8d>(第1159行,显示一些信息后,退出函数,参见前面分析),如果相等,继续执行1154行。
9、1154行:显示一些信息后,调用secret_path函数。
1)显示0x804a5e0/0x804a608地址显示的内容,使用objdump --start-address=0x804a5e0 -s bomb查看这些地址内容:
显然,这些信息是说找到了隐藏的关。
2)1158行:调用secret_phase函数,显然,这个函数就是隐藏关!!
10、现在的问题是,第1144行输入的字符串0x804c910是什么(根据前面的分析,这个字符串中应该包含"%d %d %s",其中第三个字符串应该是" DrEvil ",才能打开隐藏关)?第1133行和6进行比较的0x804c808又是什么?这两个地址的内容通过objdump --start-address=0x804c910 -s bomb并不能查看出来。可以使用objdump -D bomb将所有的section全部打印出来进行分析:
从上图可以看出,这两个地址的内容,是放置在bss段中(未初始化的全局变量以及静态变量):
1)0x804c808:对应的全局变量名称为num_input_strings,顾名思义,这个名字是用户输入的字符串的个数,对于bomb来说,每一关需要输入一个一个字符串(read_line函数),可以猜测这个变量保存了输入的字符串的个数,因为每通一关,输入一个字符串,该变量也可以说明是通过了几关。(实际上,可以继续分析read_line函数,在该函数中,每执行一次,会将0x804c808处的内容加1,这里不展开分析了)。
因此,我们可以判断:将0x804c808处的内容与6相比较,应是判断是否已经通过了第6关,如果已经通过了第6关,才判断是否打开隐藏关。
2)0x804c910:这个是在0x804c820 + 0xf0的地址,也即(input_strings+0xf0),从这里大致可以判断,input_strings应该是保存每次输入的数组。根据解题提示,在第四关输入一个特殊的字符串,可以打开隐藏关,而第四关要求我们输入的是“%d %d”(根据第四关解题,应输入"12 43"),为了打开隐藏关,应该输入"12 43 DrEvil"。(DrEvil为作者名字,太自恋了)
为验证以上猜测,可以使用gdb bomb跟踪bomb的执行:
从上面可以看出:
1)0x804c808:每输入一次,该地方的值加1;
2)0x804c910:正是第四次输入之后,该字符串的首地址。
以上分析,可以说明我们的判断是正确的。
1.3 secret_phase分析
在asm.txt中寻找secret_phase,可以找到secret_phase函数入口地址。
1、723-731行:准备函数栈帧,调用read_line函数,读取用户输入,用户输入放在ax中。然后调用strtol将用户输入转换成十进制数,此时函数栈帧为:
strtol(input, 0, 10)将input转换成十进制整数。strtol的原型参见相关c语言资料。
2、732-736行:将strtol函数传递给ebx,将返回结果减一,然后与0x3e8(十进制1000)进行比较,如果小于则跳转到8049082 <secret_phase+0x32>(第737行),否则引爆炸弹。
注意,这里使用的是jbe,是无符号整数跳转指令。如果输入的数小于0,减1之后是一个很大的无符号数,因此也会大于1000,因此此段代码用于判断输入的整数是否大于等于1,小于等于1001。
3、737-739行:为调用8048fff <fun7>函数准备函数栈帧。显然,fun7函数包括两个参数:
1)用户输入的整数放置到esp+4(注意:第732行,将用户输入的整数传递给了ebx)
2)0x804c0c0放置到esp。(0x804c0c0应该是某个地址,但0x804c0c0放置的是什么,将在后文分析)
此时的函数栈帧是:
fun7函数返回结果放置在eax中。
4、740-742行:判断fun7返回的结果是否是3,如果是,则跳转到804909c <secret_phase+0x4c>(第743行),否则引爆炸弹;
5、743-747行:应该是显示某些信息,然后退出函数,这不是我们关心的内容。
目前的关键是fun7函数。根据前面分析,函数fun7的原型应该是:
int fun7(void* ptr, int num)
注:因尚不清楚0x804c0c0指向的内容是什么,以一个指向void的指针代替。
1.4 fun7函数分析
在asm.txt中继续寻找fun7,可以找到fun7的入口。
1、693-696行:初始化函数栈帧,此时函数栈帧为:
此时,edx的内容为ptr = 0x804c0c0,ecx为用户输入的整数num(后文均以num代替)。
2、697-698行:判断edx是否为0,即0x804c0c0(ptr指针)是否为0,如是,则跳转到8049046 <fun7+0x47>(717行,实际上就是返回了0xffffffff,即-1),否则继续执行。
3、699-700行:将edx指向的地址的内容放置到ebx(即0x804c0c0指向的地址的内容),比较ecx是否与ebx,即num与0x804c0c0地址的内容相比较。
4、701行:如0x804c0c0地址的内容(*ptr)小于等于num,则跳转到8049028 <fun7+0x29>(第708行)
5、702-707行:如果0x804c0c0地址的内容(*ptr)大于num,则:
1)702行:将ecx放置到esp+4,即将num放置到esp+4
2)703-704行:将edx + 4地址的内容,放置到esp(edx此时为0x804c0c0)
3)705行:调用fun7:fun7(*(ptr + 4), num),显然这是一个递归调用。
4)706行:将3)的返回结果乘以2。
6、708-716行:当(*ptr <= num)时,运行这段代码。
1)708行:0 --> eax
2)709行:比较ecx与ebx,其中ecx为num,ebx为0x804c0c0地址的内容(*ptr)
3)710行:如果ebx==ecx,则跳转到804904b <fun7+0x4c>(即结束,返回eax的值,此时eax的值为0,即返回0)
4)711-716行:如果ebx != ecx,即*ptr < num时:
a:711行:ecx --> esp +4,即esp+4为num
b:712-713行:将edx + 8的内容放置到esp,此时edx的内容为0x804c0c0
c:714行:调用fun7:fun7(*(ptr + 8), num),显然这是一个递归调用。
d:715行:eax = eax + eax * 1 + 1;eax为fun7返回结果,即将上一步的返回结果乘以2加1。
7、综合以上分析,可以初步写出fun7的C语言代码:
int fun7(void* ptr, int num)
{
if(ptr == null){
return -1;
}
if(*ptr > num){
return 2 * fun7(*(ptr + 4), num);
}else if(*ptr == num){
return 0;
}else{
return 2 * fun7(*(ptr + 8), num) + 1;
}
}
注:
1、ptr的类型尚未知,需要进一步分析,这里暂时以void代替。但根据代码的分析:
a:*(ptr + 4),即ptr +4 的地址的内容,也应该是和ptr同类型的指针;
b:*(ptr + 8),即ptr +8的地址的内容,也应该是和ptr同类型的指针;
c:*(ptr),这应该是一个整型。
因此,ptr指向的类型应该是一个构成二叉树的一个节点。
1.5 二叉树分析
1.5.1 初步分析
根据前面分析,ptr应该是一个指向二叉树的一个节点,其定义类似:
struct nodeTree{
int val;//用于与num比较,应该是一个整数
struct node* left;
struct node* right;
}
如果:struct nodeTree *ptr;
则:*(ptr+4) = ptr->left,*(ptr+8) = ptr->right。
1.5.2 0x804c0c0地址内容分析
证实以上猜测的最好方法是分析0x804c0c0地址的内容。可以通过两种方法单独分析或者二者结合分析。
方式1:objdump --start-address=0x804c0c0 -s bomb > s0x804c0c0.txt,然后查看该txt文件。
从上面可以看出:
1)0x804c0c0开始的12个字节:0x24、804c0cc、804c0d8
2)0x804c0cc开始的12个字节:0x08、804c0fc、804c0e4
3)0x804c0d8开始的12个字节:0x32、804c0f0、804c108
4)0x804c0e4开始的12个字节:0x16、804c150、804c138
5)0x804c0f0开始的12个字节:0x2d、804c114、804c15c
6)0x804c0fc开始的12个字节:0x06、804c120、804c144
7)0x804c108开始的12个字节:0x6b、804c12c、804c168
8)0x804c114开始的12个字节:0x28、0000000、0000000
9)0x804c120开始的12个字节:0x01、0000000、0000000
10)0x804c12c开始的12个字节:0x63、000000、0000000
11)0x804c138开始的12个字节:0x23、0000000、0000000
12)0x804c144开始的12个字节:0x07、0000000、0000000
13)0x8c150开始的12个字节:0x14、0000000、0000000
14)0x8c15c开始的12个字节:0x2f、0000000、0000000
15)0x8c168开始的12个字节:0x3e9、0000000、0000000
通过以上分析,显然这是一棵二叉树,后面的left和right都为null的情况,代表他们都是叶子节点。
方式2:objdump --start-address=0x804c0c0 -D bomb > D0x804c0c0.txt,然后查看该txt文件。
显然,这种方式看到的结果和第一种方式是一样的,但这种方式比第一种方式更容易理解。我们可以看出来一个一个节点,以及它们在程序中定义的名称。
1.5.3 二叉树
通过以上的分析,我们可以得出一个二叉树,如下图所示(上面的16进制均转换成了十进制)。
显然,这还是一棵平衡二叉树,而且左子树的值比右子树的值要小,可以用于搜索某个值。
1.5 secret_phase结果分析
根据前面分析,secret_phase要求用户输入一个1 ~ 1001的数,fun7搜索这个二叉树,要求fun7返回3,下面来分析fun7返回值的含义。根据以上分析,重写fun7的C语言代码。
struct nodeTree{
int val;//用于与num比较,应该是一个整数
struct node* left;
struct node* right;
}
int fun7(struct nodeTree* ptr, int num)
{
if(ptr == null){
return -1;
}
if(ptr->val > num){
return fun7(ptr->left, num) * 2;
}else if(ptr->val == num){
return 0;
}else{
return fun7(ptr->right, num) * 2 + 1;
}
}
从代码可以看出,给定一个值后,fun7对这个二叉树进行搜索,在某一层上搜索到这个值,返回0,回退到上一层,如果该层是上一层的右子树,则将返回值*2加1(也相当于左移一位,然后加1),如果该层是上一层的左子树,则将返回值*2(左移一位)。
因此,对于本题来讲,相当于是对二叉树的每个节点进行了编号,左子树为0,右子树为1,如下图所示:
例如:
1)搜索1,fun7 = 000b = 0,搜索7,fun7 = 100b = 4
2)搜索20,fun7 = 010b = 2,搜索35,fun7= 110b = 6
3)搜索40,fun7 = 001b = 1,搜索47,fun7 = 101b = 5
4)搜索99,fun7= 011b = 3,搜索1001,fun7= 111b = 7
因此,对于本题来说,要使fun7 = 3,则输入的数应该是99。这就是答案!!