lab2操作系统和原理详解

CMU LAB2 解题详解

    1. phase_1

学生输入一个字符串,phase_1进行比较,如果与内部的字符串一致,则该阶段通过,否则炸弹爆炸。

phase_1比较简单,主要用于学生熟悉解题步骤、函数栈帧的构成以及汇编语言。

对于本文档分析的炸弹,字符串为:“Border relations with Canada have never been better.”。

直接根据xshell找到phase所在的位置

分析如下

1、第378行:sub $0x1c, %esp,将函数栈空间扩展了0x1c字节(28个字节)

2、第379行:将0x804a49c放置到了esp+4的地方。

3、第381/382行:将input的内容放置到了esp的地方。注:20(%esp)正好是栈中存放input的内容。

4、第383行:调用strings_not_equal函数。

5、显然,第379行以及第381/382行是在为调用strings_not_equal函数准备参数。在调用strings_not_equal函数之前(即382行执行之后,383行执行之前),函数栈帧变成如下:

6、第384行:test %eax %eax,是对eax寄存器里的内容(string_not_equal函数的返回内容)进行位与操作,如果为0,则置zf标志(零标志)为1,相同为0不同为1;

7、第385行:是一个je指令,je指令判断zf标志(零标志)为1时(也即strings_not_equal函数返回的是0的情况下),跳转到phase_2 + 0x20的地方,即0x8048c20的地方,说明炸弹拆除成功。否则,call 804944b <explode_bomb>,顾名思义,是爆炸炸弹,即拆除炸弹失败。

8、从上面的分析来看,上图中显示的栈帧中,esp的内容是输入的字符串的首地址,而esp + 4的内容是0x804a49c,应该是在程序中保存的被比较的字符串(即拆弹字符串)的首地址,而按照strings_not_equal的名字来看,如果是不等,则返回1,等则返回0。如果等,代表输入的拆弹字符串是正确的。

 

 

 

 

 

 

 

 

 

 

 

 

 

执行第801 - 804行之后,函数栈帧为:

 

 

1、第805行,将esp + 0x14的内容(input(输入字符串首地址))送入到了ebx寄存器,第806行,将esp + 0x18的内容(0x804a49c)送入到了esi寄存器。验证了我们前面所介绍的0x804a49c地址所在的地方应该是拆弹字符串所在的首地址。

2、807-809行:求input字符串的长度,结果送入到edi寄存器。

3、810-811行:求0x804a49c字符串的长度,结果保存在eax寄存器中。

4、812行:将1送入edx,通过后面的分析,可以知道edx存放的是返回结果,也即默认返回结果为1,即不等。

5、813-814行:比较edi和eax的内容,即input字符串与0x804a3fc为首地址的字符串长度进行比较,如果不等,则跳转到strings_not_equal + 0x63的地方:0x804916a + 0x63 = 0x80491cd(此地的指令是将edx的内容送入到eax,并返回,注意第812行,edx的内容被赋值为1),也即返回1,代表两个字符串不等

 

 

 

 

 

 

objdump --start-address=0x804a3fc -s bomb

得到所需要的字符串Border relations with Canada have never been better.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  1. phase_

phase_2要求输入包含6个整数的字符串。phase_2函数从中读取6个整数,并判断其正确性,如果不正确,则炸弹爆炸。phase_2主要考察学生对C语言循环的机器级表示的掌握程度。

观察框架源文件bomb.c:

从上可以看出:

1、首先调用了read_line()函数,用于输入炸弹秘钥,输入放置在char* input中。

2、调用phase_2函数,输入参数即为input,可以初步判断,phase_2函数将输入的input字符串作为参数。

因此下一步的主要任务是从asm.txt中查找在哪个地方调用了readline函数以及phase_2函数。

    1. 寻找并分析调用phase_2函数的代码

打开asm.txt,寻找phase_2函数。

 

 

 

 

分析上面的代码:

1、390 ~ 392行:进行一些压栈,并扩展了函数栈帧。

2、第394-395行:lea 0x18(%esp) %eax、mov %eax 4(%esp),将esp + 18指向的栈的内容的地址放置到esp+4指向的地方。简单的说,当前esp + 4指针指向的空间的内容为esp + 18。(实际上,根据后面的分析,可以知道esp + 4的内容,放的是num[0]的地址esp + 18

3、第396行:将0x40(%esp)的内容放置到esp指向的栈。0x40(%esp)里面的内容实际上就是input字符串首地址。

4、第397行:调用了read_six_numbers函数(顾名思义,从字符串中解析出六个整数),可以猜测实际上第394行到第396行,是在为read_six_numbers函数准备参数。

5、在调用read_six_numbers之前,函数栈帧为:

7、上图所示的函数栈帧中,从esp + 18 ~ esp+2c,共6个栈空间,标记为保存6个整数,实际上从当前的地方并不能完全看出来,可以有些猜测,到后来阅读read_six_numbers时,证实了当前的猜测是正确的。

8、依据以上的分析,read_six_numbers函数的定义:void read_six_numbers(char* input, num);其中第二个参数,是num数组的地址。在后面,会剖析read_six_numbers函数,来证实以上的猜测,下面的分析以以上的栈帧图为基础。

9、第399行:cmp $0x0, 0x18(%esp),0x18(%esp)中是num[0],该语句判断num[0]是否应等于0,如果等,则跳转到phase_2 + 0x27,如果不等,则call explode_bomb(第401行),从此处,可以猜测:num[0]  = 0

10、第401行:cmp $0x1, 0x1c(%esp),0x1c(%esp)中是num[1],该语句判断num[1]是否应等于1,如果等,则跳转到phase_2 + 0x46,如果不等,则call explode_bomb(第401行),从此处,可以猜测:num[0]  = 1

 

11、第412行(8048c62(phase_2 + 0x3e)),将0x20 + esp --> ebx寄存器,即将num[2]的地址送入到ebx寄存器,第413行,将0x30 + esp -->esi,0x30(%esp)是num[5]上面的栈空间,将该栈空间的地址送入到esi。

12、第415行:跳转到8048c4b(即第403行)。

13、第405行:将-0x8(%ebx)的内容送入到eax,-0x8(%ebx)的内容实际上指的是0x18(%esp),也即num[0]送入到eax。

14.第406行 add   -0x4(%ebx),%eax 也就是把ebx-4的位置也就是num[1]和num[2]相加

15 第407行 再把寄存器ebx和eax进行比较也就时num[2]和num[1]+num[0]作比较,如果num[2]!=num[1]+num[0] 则引爆炸弹,所以得到伪代码

1)num[0] = 0,num[1]=1;

2)num[i] = num[i-1]+num[i-2]。(i > 0)

因此,phase_2炸弹秘钥应该是:0 1 1 2 3 5

 

以上所有的分析是建立在六个输入数字是放置在esp + 0x18开始的地址中的前提下的。为确认这一个问题,下面对read_six_numbers函数进行详细分析。

1、第1035行:扩展栈帧,增加了44。

2、第1036行:将0x34(%esp)的内容送到eax,0x34(%esp)的内容正好是num[0]的地址,也即num的首地址,也即eax内容为num[0]的地址。(参见后面的栈帧图)

3、第1037行:将eax + 0x14的地址(即为eax + 0x14)送到edx,eax+0x14正好是num[5]的地址。(参见后面栈帧图)

4、第1038行:将edx的内容送到esp + 0x1c的地方,即将num[5]的地址送到esp+0x1c的地方;

5、第1039行 ~1047行:

1)num[4]地址,送到esp + 0x18

2)num[3]地址,送到esp + 0x14

3)num[2]地址,送到esp + 0x10

4)num[1]地址,送到esp + 0xc

6、第1047行:num[0]地址,送到esp + 8

7、第1048行:0x804a725送入到esp + 4的地方

8、第1050/1051行:0x30(%esp)内容送入到esp,0x30(%esp)内容为input输入首地址。

9、第1052行:调用scanf函数,用于从input中读入6个整数。可以认为前面都是在为scanf函数调用准备参数,包括第1048行,0x804a725实际上是指向一个字符串的首地址,这个字符串为“%d %d %d %d %d %d”(这点将在后面分析),因此,我们可以判断scanf的函数定义/使用为:scanf(input, "%d %d %d %d %d %d", &num[0],  &num[1], &num[2], &num[3], &num[4], &num[5],); 返回的是读取的整数的个数。

10、此时的栈帧为:

11、第1053行:将eax的值与5比较,eax应该是scanf函数返回的输入数字的个数;

12、第1054行:如果大于5,则函数正确返回;

13、第1055行:如果小于等于5,则引爆炸弹。

14.为了查看0x804a725地址的内容,可以使用objdump --start-address=0x804a725 -s bomb命令查看,如下图所示:

 

 

 

 

 

 

  1. phase_

phase_3要求输入包含1个小于10的整数,一个ASCII字符,以及一个整数的字符串。phase_2函数从中读取这些信息,并判断其正确性,如果不正确,则炸弹爆炸。

phase_3主要考察学生对C语言条件/分支的机器级表示的掌握程度。

观察框架源文件bomb.c:

从上可以看出:

1、首先调用了read_line()函数,用于输入炸弹秘钥,输入放置在char* input中。

2、调用phase_3函数,输入参数即为input,可以初步判断,phase_3函数将输入的input字符串作为参数。

因此下一步的主要任务是从asm.txt中查找在哪个地方调用了readline函数以及phase_3函数。

phase_3函数分析

寻找phase_3,可以寻找到phase_3函数,如下图所示:

 

1、第423~431行:初始化函数栈帧,同时为调用sscanf准备参数。之后,函数栈帧如下所示:

Input(输入字符串首地址)

 

 

 

D2

D1

 

 

Esp+0x2c

Esp+0x18

0x804a69d

Input(输入字符串首地址)

 

1)esp + c的地方,存放的是d2

2)esp + 8  的地方实际的内容是d1

第433 行 eax存放的是sscanf的返回值判断如果返回值小于等于1就引爆炸弹

所以所取到的有效数据大于1

所以就是判断有没有读入两个数

第436行 d1和7比较 如果d1大于7则引爆炸弹 所以d1的值一定小于7

  1. 第438-439行:将d1送给eax,跳转到0x804a464 + d1 * 4的内容所指示的地址。使用objdump --start-address=0x804a464 -s bomb查看0x804a4f8的内容,如下图所示:

 

显然,后面连续的8个32位的数值分别指向的地址是08048cc1、08048ce3、08048d05、08048d27、08048d43、08048d5e、08048d79、08048d94(注意:IA32为小端表示),分别对应于d1为0~7。显然这一块应该是一个swich-case的机器级表示。(参见袁春风老师《选择及循环语句的机器级表示》)

5.第441-443行(8048cc9)

 往寄存器eax赋为0x39

然后eax-0x8f

6.第444-446行(8048cd5)

 往寄存器eax赋为0

然后eax+0x1d4

7.第447-449行(8048ce1)

 往寄存器eax赋为0

然后eax-0x108

8.第450-452行(8048ced)

 往寄存器eax赋为0

然后eax+0x108

 

9.第453-455行(8048cf9)

 往寄存器eax赋为0

然后eax-0x108

10.第456-458行(8048d05)

 往寄存器eax赋为0

然后eax+0x108

11.第459-461行(8048d11)

 往寄存器eax赋为0

然后eax-0x108

  1. 第462-469

如果d1大于5或者寄存器eax存的数和d2不同的话

就引爆炸弹

所以d1<5 d2是switch函数所得到的结果

因此d1等于3 d2等于-264

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  1. phase_

phase_4要求输入2个整数,phase_4函数从中获取信息,并判断其正确性,如果不正确,则炸弹爆炸。

phase_4主要考察学生对递归的机器级表示的掌握程度。

观察框架源文件bomb.c:

从上可以看出:

1、首先调用了read_line()函数,用于输入炸弹秘钥,输入放置在char* input中。

2、调用phase_4函数,输入参数即为input,可以初步判断,phase_3函数将输入的input字符串作为参数。

因此下一步的主要任务是从asm.txt中查找在哪个地方调用了readline函数以及phase_4函数。

    1. phase_4函数分析

寻找8048ba1,或者继续寻找phase_4,可以寻找到phase_4函数,如下图所示:

 

 

1、511-519行:准备phase_4函数栈帧,并为调用sscanf函数准备参数。经过这些语句后,函数栈帧如下图所示。

 

注意:0x804a69d位置的内容为“%d %d”,可以判断是输入两个整数。其分析过程参见前面阶段分析,这里不再赘述。

2、520行:调用sscanf函数,读取两个整数,得到的两个数据d1和d2,放置在如上图中的栈中,函数返回结果在eax寄存器中。(根据前面的分析,应该知道返回结果代表读取的数据的数量)

3、521-522行:如果读取的数量不等于2,则跳转到8048dcc<phase_4 + 0x33>,引爆炸弹;

4、523-525行:d1与14(0xe)比较,大于,则引爆炸弹。显然,这里的判断是要求d1 <= 14。小于则跳转到8048dd1<phanse_4 + 0x38>,继续执行。

6、556行(8048e5d)- 561行,是在为调用8048dc8<func4>这个函数做准备。561行执行后,函数栈帧变为:

可以看出,为func4函数准备了3个参数,调用顺序为(d1,0,14)。

7、532行:调用func4(d1, 0, 14),返回结果在eax中。

8、533行:将func4的返回结果与0x05(10进制43)相比较,如果不等,则跳转到8048df9<phase_4+0x60>,引爆炸弹。也即func4的返回结果应该为5,否则引爆。

9、535-537行:将d2与43(0x05)相比较,如果不等,则引爆炸弹。如果相等,则过关

  1. 从上面分析来看,输入的两个数中,d1 <= 14,而d2=5。d1目前看来暂时不能确定,如果要确定d1的值,需分析func4函数

 

 

 

 

 

 

 

 

    1. func4函数分析

在asm.txt中寻找func4

 

 

1、根据前面分析,func4的定义应为:int func4(int x, int y, int z),后面的分析均以此为基础进行分析。

2、573-576行:准备函数栈帧。执行后,函数栈帧如下图所示:

3、477-479行:x --> edx,y --> eax,z --> ebx。

4、480-482行:ecx = esi= z - y;

5、483行:ebx逻辑右移0x1f位(shr:逻辑右移指令),即逻辑右移31位,即将z-y的最高位(符号位)移到了最低位。逻辑右移的结果送到esi,此时esi 为z - y的符号位。即如果z >= y,则ebx = 0,否则ebx = 1;(前面给的参数z = 14, y = 0, ebx = 0)

6、484-485行:ecx = esi + ecx = z - y + sign(z-y) ;然后ecx算术右移一位,即ecx = (z-y + sign(z-y))/2 = 7。(sar:算术右移指令,只有一个参数,意味着只右移1位,这里sign(z-y)代表取z-y的符号,当z>=y时,sign(z-y) = 0 否则sign(z-y) = 1)。(这里应该是求z和y的中间的值,分为负数和正数的处理方法不一样,为方便后面描述,这里设置mid = y + [(z-y) + sign(z-y)]/2)

 

7、486-487行:使ecx=ecx+eax,然后再将ecx和edx进行比较,

  1. Ecx和edx进行比较 ,也即mid与x相比较,如果mid <= x(jle为小于等于),则跳转到8048d73<func4 + 0x3b>。

 

9、489-494行:将esi的内容(z),送入到esp+8的位置,ebx + 1 -->esp + 4(ebx为mid), x-->esp,然后调用func4,显然这里是递归调用func4,此时调用func4(x, mid + 1, z)。如果该函数返回,将ebx(mid)*2,进行返回。

 

11、根据前面8~10分析:如果mid 等于x,则返回0,如果mid < x,则调用func4(x, mid+1, z)。

12、第496-500行:上面第8步,如果第518步没有跳转,则继续执行519行,此时mid > x。此时的过程:

1)将ebx -1 -->ecx,即ecx=mid-1,ecx->esp+8(esp+8为mid-1),eax->esp+4(eax为y),edx->esp(edx为x)。

2)调用func4:func4(x, y, mid-1)。

3)显然,以上代码含义是当mid > x时,调用func4(x, y, mid-1)。

4)如果func4返回,1与eax(func4的返回结果)相加(第524行),进行返回。

根据以上分析,func4显然是一个递归调用函数,其大致c语言代码为:

int func4(int x, int y, int z)

{

int mid = 0;

if(z >= y){

mid = y + (z - y)/2;

}else{

mid = y + (z-y + 1)/2;

}

if(x == mid){

return 0;

}else(if x < mid){

return 2* func4(x, y, mid -1);

}else(if x > mid){

Return 1 + func(x, mid + 1, z);

}

}

显然,上面代码为一个二进制搜索树,phase_4调用时的参数是func4(d1, 0, 14),最后的返回结果是43。当参数为0,14时,二进制搜索树为:

因此,phase_4的答案应该是:"10 5"

 

 

 

 

 

 

 

 

 

 

 

  1. phase_

phase_5要求输入一个包含6个字符的字符串。phase_5函数从中读取这些信息,并判断其正确性,如果不正确,则炸弹爆炸。

phase_5主要考察学生对指针(数组)机器级表示的掌握程度。

观察框架源文件bomb.c:

从上可以看出:

1、首先调用了read_line()函数,用于输入炸弹秘钥,输入放置在char* input中。

2、调用phase_5函数,输入参数即为input,可以初步判断,phase_5函数将输入的input字符串作为参数。

因此下一步的主要任务是从asm.txt中查找在哪个地方调用了readline函数以及phase_5函数。

    1. 寻找并分析调用phase_5函数的代码

打开asm.txt,寻找phase_2函数。

和phase_1类似分析:

1、当前栈的位置存放的是read_line函数读入的一串输入;

2、phase_5的函数入口地址为0x8048e8e

此时的函数栈为:

    1. phase_5函数分析

继续寻找phase_5,或搜索8048e8e,可以找到phase_5函数入口。如下图所示:

 

 

1、542-548行:初始化函数栈帧,并为调用string_length做准备(此时ebx的内容为input字符串首地址:574行)。函数栈帧如下图所示:

注:

 

1)545-546行:mov %gs:0x14, %eax   mov %eax, 0x1c(%esp),将gs(全局段寄存器)+0x14偏移位置的内容放置到eax,然后将其放置到esp + 0x1c的地方。从这里看不出这段代码什么含义,但据后面的分析,这里应该是起到一个“哨兵”的作用,防止数组访问越界。

 

2)547行:xor %eax, %eax,似乎没有什么用,得出来的结果是0,应该只是影响zf标志寄存器(zf为零标志寄存器,即zf=1)。

 

2、549行:判断input字符串的长度(esp指向的地方为input的首地址,参见上图),返回结果在eax寄存器中。

 

3、550-553行:判断input的长度是否为6,如果不是,则炸弹爆炸(582行),如果是,跳转到8048e68<phase_5+0x66>。也即输入的字符串长度应该是6。

 

4、571-573行(8048e68<phase_5+0x66>):将eax寄存器内容赋值为0,然后跳转到8048e2a<phase_5+0x28>。

 

 

5、554行(<8048eb6><phase_5+0x28>):将ebx + eax * 1地址的内容送入到edx。注意, ebx为input首地址,也即将input[%eax]的内容送入到edx。当eax = 0时,即为edx的内容为input[0]。

 

6、555行:将edx的内容(input[0])与0x0f位与,相当于取低4位(edx内容为input[eax]的低四位)。

 

7、将edx + 0x804a484指向的地址的内容送入到edx。0x804a484的内容(使用objdump --start-address=0x804a484 -s bomb,参见phase_1分析过程)为:

 

从上面来看,0x804a484应该是指向一个字符串,此时edx的内容应该是0x804a484加上input[eax]低4位的偏移的内容。

8、557行:将dl(edx的低8位,为(0x804a484+input[eax]) & 0xf)的内容送入到esp + eax * 1 + 0x15的地方。

9、558-560行:eax += 1,然后判断eax的内容是否等于6,如果不等,则跳转到8048eb6<phase_5+0x28>,重新回到第5步继续进行分析,直到eax=6(即循环6次)。

10、以上代码,以类c语言来简要说明:

for(int i = 0; i < 6; i++){

//将0x804a484 +  input[i] & 0x0f这个地址的内容送入到堆栈esp + i + 0x15地址中。

(0x804a484 +  input[i] & 0x0f)  --> (esp + i + 0x15)

}

经过6次循环后,函数栈帧如下:

显然,从esb + 0x15开始,是根据input的输入的每个字符的低四位,得出来的一个新的字符串。

11、561行:以上循环结束后,跳出循环,执行该语句:esp + 0x1b的内容改变为0;

12、562行:将0x804a45a送入到:esp+0x4。0x804a45a的内容为(objdump --start-address=0x804a45a -s bomb):

 

也即当前esp+0x4指向的是一个字符串首地址,字符串为devils

13、564-565行:eax的内容变为esp + 0x15,即通过上面循环形成的新的字符串的首地址,然后将其送入到esp。

14、调用strings_not_equal函数,显然,前面11~13均在为调用strings_not_equal做准备,调用strings_not_equal前,函数栈帧为:

15、显然,strings_not_equal函数判断以(esp + 0x15)为首地址的字符串与0x804a45a为首地址的字符串(“oilers”)相比较,如相等,eax返回0,如不相等eax返回1。(参见phase_1分析)

16、567行:判断eax是否为0(eax与eax位与),如果为0,0标志寄存器为1。

17、568-569行:如果eax=0,则跳转到804872<phase_5+0x6d>,后续直接退出phase_5了,说明输入的input字符串是正确的,否则引爆炸弹。(8048e72<phase_5+0x6d>代码后面分析

18、573-576行:将esp + 0x1c地址处的内容送入到eax(573行,esp+0x1c的内容应为%gs

:0x14的内容),然后与%gs:0x14的内容相异或,如果相等(为0),则跳转到0x8048e87,正常结束,否则调用__stack_chk_fail函数(应该是栈检查失败);

根据上面分析,%gs:0x14的值送入到esp+0x1c的地方(第575-576行),应该是起到一个“哨兵”的作用,防止数组的访问越界。

    1. phase_5结果分析

根据前面分析,显然phase_5函数的作用(以类C语言进行描述):

char array[] = {'m', 'a', 'd', 'u', 'i', 'e', 'r', 's', 'n', 'f', 'o', 't', 'v', 'b', 'y', 'l '};

char *str = "devils";

char  new_str[7];

//根据input的每个字符的低4位,以及array,形成新的字符串。

for(int i = 0; i < 6; i ++){

new_str[i] = array[input[i] &0xf]);

}

new_str[6] ='\0';

 

//如果new_str不等于str("devils"),则引爆炸弹。

if(strcmp(str, new_str) !=0){

explode_bomb();

}

根据以上分析,要形成"devils"字符串:

1)'d':对应于array第2个(从0开始),也即input[0]的字符的低4位应该为a,符合条件的可显示字符有:'2 ','B','R','b',’r'(参见附后的ASCII码表):

2)'e':对应于array第5个(从0开始),也即input[1]的低4位应该为4,符合条件的可显示字符有:'5 ','E','U','e','u'

3)'v':对应于array第c(0x1100)个(从0开始),也即input[2]的低4位应该为F,符合条件的可显示字符有:'<','L','\','l','|'

4)i':对应于array第4个(从0开始),也即input[3]的低4位应该为5,符合条件的可显示字符有:'$','4 ','D','T','d','t'

 

5)'l':对应于array第f个(从0开始),也即input[4]的低4位应该为6,符合条件的可显示字符有:'\','?','O','o','_'

6)'s':对应于array第7个(从0开始),也即input[5]的低4位应该为7,符合条件的可显示字符有:''','7 ','G','W','g','w'

 

 

因此,对于本题,答案不是唯一的,为6组可选字符的排列组合,如"beldog"等。

 

 

 

 

 

 

 

 

 

 

 

  1. phase_

phase_6要求输入6个1~6的数,这6个数不能重复。phase_6根据用户的输入,将某个链表按照用户的输入的值(进行某种计算后)进行排序,如果最终能排成升序,则解题成功。

phase_6主要考察学生对C语言指针、链表以及结构的机器级表示的掌握程度。

观察框架源文件bomb.c:

从上可以看出:

1、首先调用了read_line()函数,用于输入炸弹秘钥,输入放置在char* input中。

2、调用phase_6函数,输入参数即为input,可以初步判断,phase_6函数将输入的input字符串作为参数。

因此下一步的主要任务是从asm.txt中查找在哪个地方调用了readline函数以及phase_6函数。

    1. 寻找并分析调用phase_6函数的代码

打开asm.txt,寻找phase_2函数。

和phase_1类似分析:

1、当前栈的位置存放的是read_line函数读入的一串输入;

2、phase_2的函数入口地址为0x8048f12

此时的函数栈为:

    1. phase_6分析

在asm.txt中继续寻找phase_6,或者寻找8048f12,找到phase_6函数入口:

在asm.txt中继续寻找phase_6,或者寻找8048f12,找到phase_6函数入口:

 

 

 

 

 

 

1、583-591行:初始化函数栈帧,然后调用read_six_numbers函数。调用之后,从input中读取了6个数num[0]  ~  num[5](read_siz_numbers函数分析参见前面),位于esp+0x10  ~esp+0x24,此时函数栈帧如下图所示:

2、592行:0  --> esi

3、593-597行:判断(esp + esi*4 + 0x10)是否小于等于6以及大于等于1,如果不满足,则引爆炸弹(597行);也即输入的数(esi=0时,为num[0],esi=1时,为num[1]......)应该大于等于1,同时小于等于6。注意,比较时,先将该值减1(594行),然后与5进行比较(623行),比较时用的jbe(无符号整数比较),也即,如果该输入值小于1,减1之后变成一个很大的无符号数(负数),肯定是大于5的。因此这几行就实现了判断num[esi] >=1 && num[esi] <=6。(这应该是编译器做的优化

4、598行:esi += 1

5、599-600行:esi与6进行比较,如果等于,则意味着6个数已经比较完毕,跳转到0x8048ebb<phase_6 + 0x52>

6、601行:如果600行没有跳转,也即6个数还没有判断完毕,则继续执行,将0赋值给ebx

7、602-603行:判断num[ebx]是否与num[esi-1]相等,如果相等,则引爆炸弹;

8、608-610行:ebx+=1,然后判断ebx是否小于等于5,如果是,则跳转到0x8048ecc,即604行,也即跳转到第7步。

9、611行:跳转到8048eaa,进行num[esi]的比较(注意num[esi]在第626行加1)

10、综合以上分析,可以判断出以上代码的作用是:

1)判断每个输入的数应小于等于6,大于等于1;

2)num[i]不等于它的后续的每个数;

3)也即输入的6个数,应是1/2/3/4/5/6,但顺序不一定。

使用类c语言描述:

for (i = 0; i < 6; i++) {

if ((num[i] < 1) || (num[i] > 6))

explode_bomb();

for (j = i + 1; j < 6; j++) {

if (num[i] == num[j])

explode_bomb();

}

    }

14、624行(0x8048fa2<phase_6+0x90>)- 662行:ebx保存到esi(mov %ebx, %esi),将esp + ebx*4 + 0x10的内容(当ebx=0时,为num[0],当ebx=1时,为num[1]......)与1相比较,如果 esp + ebx*4 + 0x10 <= 1(661行),则跳转到0x8048f91<phase_6+0x7f>(662行),否则继续执行663行。(根据前面分析,ecx为输入的num的值,仅且仅当ecx=1时,执行这个662行跳转语句,跳转到0x8048f91<phase_6+0x7f>)。

15、618行(0x8048f91<phase_6+0x7f>):当ecx=1时,会执行该条语句,将0x804c174送入到edx。查看0x804c174地址的内容(objdump --start-address=0x804c174  -s bomb):

根据该图可以读得

1 0x0e0

2 0x200

3 0x323

4 0x268

5 0x0b7

6 0x1fc

16、619行:将edx内容送入到esp + esi*4 + 0x28。

17、620-622行:ebx += 1,然后与6相比较,如果等于6,则跳转到0x8048fla。

18、如果不等于6,则继续执行659行,对于本文,即跳转到第14步,前面分析了ecx=1的情况,如果ecx不等于1,则应继续执行663-665行

19、663-665行:eax赋值为1,edx赋值为0x804c174,跳转到8048f85 <phase_6+0x73>。(第649行)

20、649-652行:这是一个循环。判断ecx(num[ebx])是否等于eax,如果不是,则将edx + 8的内容送入到edx,然后继续判断, edx +8的内容应该是指向的是一个地址。如果相等,则跳转到8048f96 <phase_6+0x84>(从第655行继续执行)

21、根据前面的分析,13~20步的代码,是根据处理后的num值(参见第10步分析),将相关信息压栈(从esp+28开始压栈):(注意:IA32是小端方式)

1)当num[i] == 1时,将0x804c174压入到esp + 0x28 + i * 4;

2)当num[i] == 2时,将0x 804c180压入到esp + 0x28 + i * 4;

3)当num[i] == 3时,将0x 804c18c压入到esp + 0x28 + i * 4;

4)当num[i] == 4时,将0x804c198压入到esp + 0x28 + i * 4;

5)当num[i] == 5时,将0x 804c1a4压入到esp + 0x28 + i * 4;

6)当num[i] == 6时,将0x 804c1b0压入到esp + 0x28 + i * 4;

7)观察压入栈的内容,每个内容地址实际上是指向12字节的一段数据,该数据的末尾又是指向一个地址,因此,可以判断0x804c174开始的地方指向的是一个链表(但这些链表的存空间是连续分配的),每个节点包括12个字节,其中最后一个是指向下一个的指针,猜测每个节点的定义:

 

struct node {

int d1;//尚不清楚含义,以4个字节的int暂替

int d2;//尚不清楚含义,以4个字节的int暂替

struct node* next;

}

6个节点,分别为:

node1 = {0x216, 0x01}; (&node1 = 0x804c174)

node2 = {0x2a4, 0x02 }; (&node2 = 0x804c180)

node3 = {0x2f9, 0x03}; (&node3 = 0x804c18c)

node4 = {0x220, 0x04}; (&node4 = 0x804c198)

node5 = {0x22c, 0x05}; (&node5 = 0x804c1a4)

node6 = {0x10b, 0x06, 0}; (&node6 = 0x804c1b0)

链接关系为:

node1   -->   node2   -->   node3   -->   node4   -->   node5   -->   node6   -->   0

 

8)假设当前6个num的值为6/5/4/3/2/1,则经过6次循环后,函数栈帧如下图所示。

注:后面分析,均假设6个num的值为6/5/4/3/2/1。

22、以上操作结束,则跳转到0x8048fb9 <phase_6+0xa7>(第666行,参见第17步分析)。

23、666(8048fb9 <phase_6+0xa7>)- 676行:

1)666行:0x28(%esp)的内容(num[0]这个值指向的节点的地址)  --> ebx

2)667行:esp+0x2c  --> eax,esp+0x2c这个地址的内容为num[1]这个值对应的节点的地址

3)668行:esp + 0x40 --> esi,esp + 0x40,根据后面的分析,这个值是作为“哨兵”,防止访问越界

4)669行:ebx --> ecx(此时ebx以及ecx都是num[0]这个值指向的节点的地址)

5)670行:eax所指向的地址的内容(num[1]这个值对应的节点的地址)--> edx

6)671行:将edx的内容赋值给8(%ecx)的地址,注意,此时ecx为num[0]指向的节点的地址,8(%ecx)正好是num[0]这个值所对应的next,即node6.next = &node5

7)672行:eax += 4,即eax所指向的地址的内容变成了num[2]所指向的节点的地址;

8)673行:将eax与esi(哨兵)相比较,如果等于,则说明循环结束,跳转到8048fd7 <phase_6+0xc5>,如果不是,继续执行。

9)675行:edx --> ecx:根据前面分析,edx为num[1]值指向的节点的地址。

10)676行:跳转到8048fc7 <phase_6+0xb5>,即第670行,可以转到上面第5)步继续执行,注意,此时edx为num[1]值指向的节点地址,eax的内容为num[2]值指向的节点的地址,即node5.next = &node4。

11)如此循环,最后的结果是:

node6.next = &node5,node5.next = &node4,node4.next = &node3,node3.next = &node2,node2.next = &node1

12)如果以上都做完,跳转到8048fd7 <phase_6+0xc5>(677行)继续。

13)677行:将0赋值给8(%edx)指向的地址,此时edx为node1的地址,即将node1.next=0;

14)显然,以上步骤,根据num的值重新构成了一个链表,此时的链接关系变成了:

node6   -->   node5   -->   node4   -->   node3   -->   node2   -->   node1   -->   0。(注:以上分析均是基于6个num的值为6/5/4/3/2/1

24、678 - 行:

1)678行:5 --> esi

2)679行:将ebx+8这个地址的内容送给eax,注意, ebx为node6的地址,ebx+8为node6->next这个值的地址,这个地址的内容即为node5的地址。也即eax的内容为node5的地址。

3)680行:将eax指向的地址的内容赋值为eax,也即eax的内容为node5.d1

4)681行:将node5.d1与ebx指向的地址的内容相比较;(显然,此处是整数的比较,因此,也可以判断struct node中第一个元素应该是int),此时ebx的内容为node6的地址,node6的地址的内容为node6.d1,即node5.d1与node6.d1相比较。

5)682-683行:如果node6.d1 >= node5.d1,则跳转到8048ff1 <phase_6+0xdf>,否则引爆炸弹

6)684行:ebx的内容变为其指向的节点的next,即ebx=node6-next,指向了node5

7)685行:esi-= 1

8)686行:如果esi不为0,则跳转到679行,按以上的2)继续分析,应注意,此处ebx的值为node5的地址了。

9)显然,此时会判断node5.d1是否大于等于node4.d1,如果是,则继续,如果不是,则引爆炸弹

10)后续会依次判断node4.d1是否大于等于node3.d1,node3.d1是否大于等于node2.d1,......,综合起来,就是判断按照num值排序之后的节点是否降序排列,如果是,则解题成功,如果不是,则引爆炸弹。

    1. phase_6结果分析

根据以上分析,phase_6的功能:

1)phase_6定义了一个包含6个节点的链表,每个节点中包含两个整型(d1,d2),以及指向下一个节点的指针;6个节点依次的链接顺序为node1->node2->node3->node4->node5->node6

2)要求用户输入6个数,这6个数应为1~6,而且不能重;为便于以后说明,假设这6个数为1/2/3/4/5/6

3)phase_6对这6个数进行处理:num[i] = 7 -num[i];处理后的num变为6/5/4/3/2/1

4)按照num[i]的值重新排列链表,此时链表变为:

node6->node5->node4->node3->node2->node1

5)判断以上链表是否降序排列(按分量d1),如果是,则拆弹成功,否则,引爆炸弹。

也即phase_6会给出一个链表,链表中的节点的d1分量含有一个整数值,需要用户输入一个序列号,分别用7去减得到新的序列号,按照这个顺序重新排列链表中的节点,如果链表是按照降序排列,则输入的这个序列号是正确的。

对于前面的炸弹,其初始化的节点值为:

node1 = {0x0e0, 0x01, 0x804c180}; (&node1 = 0x804c174)

node2 = {0x200, 0x02, 0x804c18c }; (&node2 = 0x804c180)

node3 = {0x323, 0x03, 0x804c198}; (&node3 = 0x804c18c)

node4 = {0x268, 0x04, 0x804c1a4}; (&node4 = 0x804c198)

node5 = {0x0b7, 0x05, 0x804c1b0}; (&node5 = 0x804c1a4)

node6 = {0x1fc, 0x06, 0}; (&node6 = 0x804c1b0)

显然,使得这个链表按升序排列的序列是:5 1 6 2 4 3,,此即为本关答案。

    1. phase_6大致C语言代码

struct node{

int d1;

int d2;

int next;

};

 

struct node1 = {0x0e0, 0x01, &node2};

struct node2 = {0x200, 0x02, &node3 };

struct node3 = {0x323, 0x03, &node4};

struct node4 = {0x268, 0x04, &node5};

struct node5 = {0x0b7, 0x05, &node6};

struct node6 = {0x1fc, 0x06, 0};

 

void phase_6(char* input){

struct node*  pointer[6];

struct node* start = &node1;

struct node* p;

int num[6];

read_six_numbers(input, num);

 

for(int i = 0; i < 6; i ++ ){

if(num[i] < 1 || num[i] > 6){//每个数小于等于6,大于等于1

explode_bomb();

}

for(int   = i + 1; j < 6; j++){//每个数不能相同

if(num[i] == num[j]){

explode_bomb();

}

}

}

 

 

//按照num重新排列链表

for(int i = 0; i < 6; i ++){

p = start;

for(int j = 1; j < num[i]; j++){

p = p->next;

}

pointer[i] = p;

}

//重新构成链表

p = pointer[0];

for(int i = 1; i < 6; i ++){

p -> next = pointer[i];

p = p->next;

}

p = pointer[0];

for (int i = 0; i < 5; i++) {

if (p->d1> p->next->d1){//如果不是升序,则引爆炸弹

explode_bomb();

}

p = p->next;

    }

}

 

 

 

 

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值