二进制炸弹(第二次实验)

实验目的

本实验通过要求你使用课程所学知识拆除一个“binary bombs”来增强对程序的机器级表示、汇编语言、调试器和逆向工程等方面原理与技能的掌握。 一个“binary bombs”(二进制炸弹,下文将简称为炸弹)是一个Linux可执行程序,包含了6个阶段(或层次、关卡)。炸弹运行的每个阶段要求你输入一个特定字符串,你的输入符合程序预期的输入,该阶段的炸弹就被拆除引信即解除了,否则炸弹“爆炸”打印输出 “BOOM!!!”。
实验的目标是拆除尽可能多的炸弹层次。
每个炸弹阶段考察了机器级程序语言的一个不同方面,难度逐级递增:
阶段1:字符串比较
阶段2:循环
阶段3:条件/分支
阶段4:递归调用和栈
阶段5:指针
阶段6:链表/指针/结构
另外还有一个隐藏阶段,只有当你在第4阶段的解后附加一特定字符串后才会出现。
为完成二进制炸弹拆除任务,你需要使用gdb调试器和objdump来反汇编炸弹的可执行文件并单步跟踪调试每一阶段的机器代码,从中理解每一汇编语言代码的行为或作用,进而设法推断拆除炸弹所需的目标字符串。比如在每一阶段的开始代码前和引爆炸弹的函数前设置断点。

实验分析

为了使分析清晰明了,每个阶段都有等价的c语言代码,隐藏关留到最后分析。
在开始之前,首先介绍两个库函数:
1.int sscanf(char *input,char *format,arg3,arg4….);这个函数和scanf功能相似,只是从input里读数据,而不是从stdin里读数据,返回成功读取数据的个数
2.long int strtol(const char *nptr,char **endptr,int base,int group);这个函数以base为进制将nptr字符串转化为对应的数值并返回

阶段一

08048b20 <phase_1>:
...
 8048b2c:       push   $0x80497c0                  /pattern/
 8048b31:       push   %eax                         /input/
 8048b32:       call   8049030 <strings_not_equal>
...

由上面的代码可以发现,调用了strings_not_equal比较input和pattern,用gdb查看0x80497c处的值:

(gdb) x/20x 0x80497c0
0x80497c0:  0x6c627550  0x73206369  0x6b616570  0x20676e69
0x80497d0:0x76207369    0x20797265  0x79736165  0x6425002e
...(省略)

根据字节顺序对照ASCII码表可知pattern为”Public speaking is easy.”,故阶段一的正确输入就是pattern。

阶段二

08048b48 <phase_2>:
 8048b4b:               sub    $0x20,%esp
 8048b4e:               push   %esi
 8048b4f:               push   %ebx
 8048b50:               mov    0x8(%ebp),%edx               /edx=input/  
 8048b53:               add    $0xfffffff8,%esp
 8048b56:               lea    -0x18(%ebp),%eax             /&a[0]/
 8048b59:               push   %eax
 8048b5a:               push   %edx                         /input/
 8048b5b:               call   8048fd8 <read_six_numbers>

这里我们可以看到,代码分配了int a[6]的存储空间,并调用了

read_six_numbers(input,a):
08048fd8 <read_six_numbers>:
 8048fdb:       sub    $0x8,%esp
 8048fde:    08048b48 <phase_2>:
   mov    0x8(%ebp),%ecx                                    /input/
 8048fe1:       mov    0xc(%ebp),%edx                       /a/
 8048fe4:       lea    0x14(%edx),%eax                      /a+5/
 8048fe7:       push   %eax
 8048fe8:       lea    0x10(%edx),%eax                      /a+4/
 8048feb:       push   %eax
 8048fec:       lea    0xc(%edx),%eax                           /a+3/
 8048fef:       push   %eax
 8048ff0:       lea    0x8(%edx),%eax                           /a+2/
 8048ff3:       push   %eax
 8048ff4:       lea    0x4(%edx),%eax                           /a+1/
 8048ff7:       push   %eax
 8048ff8:       push   %edx                                 /a/
 8048ff9:       push   $0x8049b1b                              /format/
 8048ffe:       push   %ecx                                 /input/
 8048fff:       call   8048860 <sscanf@plt>

这里可以清楚地看到read_six_numbers将input,format,&a[0],&a[1]…&a[5]保存在esp指向的栈里面,接着调用sscanf,从input里读入数据存入数组a里。使用gdb查看format值:

(gdb) x/20x 0x8049b1b
0x8049b1b:  0x25206425  0x64252064  0x20642520  0x25206425
0x8049b2b:  0x61420064  0x6f682064  0x28207473
...(省略)

通过ASCII码表得知,format确实是”%d %d %d %d %d %d”,接着:

08048fd8 <read_six_numbers>:
 8049004:       add    $0x20,%esp
 8049007:       cmp    $0x5,%eax               /sscanf从input中读取数字个数/ 
 804900a:       jg     8049011 <read_six_numbers+0x39>
 804900c:       call   80494fc <explode_bomb>       /if eax<=5,explode/
 8049011:       mov    %ebp,%esp
 8049013:       pop    %ebp    
 8049014:       ret

当sscanf返回值<=5时,引爆,否则返回phase_2。继续分析phase_2:

08048b48 <phase_2>:
8048b63:               cmpl   $0x1,-0x18(%ebp)             /a[0]/
 8048b67:               je     8048b6e <phase_2+0x26>
 8048b69:               call   80494fc <explode_bomb>           /if (a[0]!=1/
 8048b6e:               mov    $0x1,%ebx                   /if (a[0]==1)/
 8048b73:               lea    -0x18(%ebp),%esi             /esi=&a[0]/
 .loop
 8048b76:               lea    0x1(%ebx),%eax               /eax=ebx-1/ 
 8048b79:               imul   -0x4(%esi,%ebx,4),%eax           /eax*=a[ebx]/
 8048b7e:               cmp    %eax,(%esi,%ebx,4)
 8048b81:               je     8048b88<phase_2+0x40>
 8048b83:               call   80494fc <explode_bomb>           /eax!=a[ebx]/
 8048b88:               inc    %ebx
 8048b89:               cmp    $0x5,%ebx
 8048b8c:               jle    8048b76 <phase_2+0x2e>
 goto .loop

这段代码首先比较a[0]和1,如果不相等,爆炸,否则进入for循环,比较每个数组元素的值,整个阶段二的等价C语言代码为:

void read_six_numbers(char *input,int a[])
{
    Int eax=sscanf(input,"%d %d %d %d %d %d",
a,a+1,a+2,a+3,a+4,a+5);
    if (eax<=5)
        explode_bomb();
}
void phase_2(char *input)
{
    int a[6];
    read_six_numbers(input,a);
    if (a[0]!=1)
        explode_bomb();
    for (int ebx=1;ebx<=5;ebx++)
        if (a[ebx]!=(ebx+1)*a[ebx-1])
            explode_bomb();
}

由此可以知道阶段二的字符串必须以”1 2 6 24 120 720 ”为开头,由于read_six_numbers仅限定了读入数据个数不小于6,因此正确答案不唯一

阶段三

08048b98 <phase_3>:
  8048ba5:       lea    -0x4(%ebp),%eax
 8048ba8:       push   %eax                         /&c/
 8048ba9:       lea    -0x5(%ebp),%eax
 8048bac:       push   %eax                         /&b/
 8048bad:       lea    -0xc(%ebp),%eax
 8048bb0:       push   %eax                         /&a/
 8048bb1:       push   $0x80497de                  /format/
 8048bb6:       push   %edx                         /input/
 8048bb7:       call   8048860 <sscanf@plt>

同样用gdb可以知道format的值是”%d %c %d”,因此调用了sscanf(input,”%d %c %d”,&a,&b,&c),之后同样比较其返回值,如果小于3就引爆,接着:

08048b98 <phase_3>:
 8048bc9:       cmpl   $0x7,-0xc(%ebp)
 8048bcd:       ja     8048c88 <phase_3+0xf0>  
 8048bd3:       mov    -0xc(%ebp),%eax              /c/
 8048bd6:       jmp    *0x80497e8(,%eax,4)          /case 0,1,2,3,4,5,6,7/
 ...
 8048c88:       mov    $0x78,%bl
 8048c8a:       call   80494fc <explode_bomb>           /default/

这里可以看到这是一个switch语句,用gdb查看跳转表:

(gdb) x/8x 0x80497e8
0x80497e8:  0x08048be0  0x08048c00  0x08048c16  0x08048c28
0x80497f8:  0x08048c40  0x08048c52  0x08048c64  0x08048c76

其中,从上到下,从左到右依次为a=0,1,..7时跳转地址,以a=0时为例:

08048b98 <phase_3>:
 8048be0:       mov    $0x71,%bl                   /bl=”q”/
 8048be2:       cmpl   $0x309,-0x4(%ebp)           /比较777和c/
 8048be9:       je     8048c8f <phase_3+0xf7>       /c==777/
 8048bef:       call   80494fc <explode_bomb>       /c!=777/
 ...
 8048c8f:       cmp    -0x5(%ebp),%bl           /比较b和”q”/
 8048c92:       je     8048c99 <phase_3+0x101>      /b==”q”/
 8048c94:       call   80494fc <explode_bomb>       /b!=”q”/
 8048c99:       mov    -0x18(%ebp),%ebx             /return/
 8048c9c:       mov    %ebp,%esp
 8048c9e:       pop    %ebp
 8048c9f:       ret

由此可知,当a=0时,b=”q”且c=777才不会引爆,其余情况类似。
C语言等价代码为:

void phase_3(char *input)
{
    int a,c;
    char b;
    int eax=sscanf(input,"%d %c %d",&a,&b,&c);
    if (eax<=2)
        explode_bomb();
    switch (a){
    case 0:
        if (c!=777||b!="q")
            explode_bomb();
    case 1:
        if (c!=214||b!="b")
            explode_bomb();
    case 2:
        if (c!=755||b!="b")
            explode_bomb();
    case 3:
        if (c!=251||b!="k")
            explode_bomb();
    case 4:
        if (c!=160||b!="o")
            explode_bomb();
    case 5:
        if (c!=458||b!="t")
            explode_bomb();
    case 6:
        if (c!=780||b!="v")
            explode_bomb();
    case 7:
        if (c!=524||b!="b")
            explode_bomb();
    default:explode_bomb();
    }
}

由此可知,阶段三字符串必须是以0q777,1b214,2b755,3k251,4o111,5t458,6v780,7b524中的一个为开头且后续字符串不能以数字为开头的字符串。

阶段四

08048ce0 <phase_4>:
 8048ce3:       sub    $0x18,%esp
 8048ce6:       mov    0x8(%ebp),%edx           /input/
 8048ce9:       add    $0xfffffffc,%esp
 8048cec:       lea    -0x4(%ebp),%eax          /&x/
 8048cef:       push   %eax                     /&x/
 8048cf0:       push   $0x8049808                  /format/
 8048cf5:       push   %edx                     /input/
 8048cf6:       call   8048860 <sscanf@plt>
     ...
     8048cfe:       cmp    $0x1,%eax
     8048d01:       jne    8048d09 <phase_4+0x29>
     ...
 8048d09:       call   80494fc <explode_bomb> 

同前分析可知,这里调用了sscanf(input,”%d”,&x),并且读到的数据个数不为1时,引爆,如果读入了一个数据:

08048ce0 <phase_4>:
 ...
 8048d03:       cmpl   $0x0,-0x4(%ebp)             /比较x0/
 8048d07:       jg     8048d0e <phase_4+0x2e>           /x>0/
 8048d09:       call   80494fc <explode_bomb>           /x<=0/
 8048d0e:       add    $0xfffffff4,%esp
 8048d11:       mov    -0x4(%ebp),%eax              /x/
 8048d14:       push   %eax/x/
 8048d15:       call   8048ca0 <func4> 
 8048d1a:       add    $0x10,%esp
 8048d1d:       cmp    $0x37,%eax          /比较func4(x)和55/
 8048d20:       je     8048d27 <phase_4+0x47>       /func4(x)==55,return/
 8048d22:       call   80494fc <explode_bomb>

可以看出,如果读入值x<=0,引爆,如果读入值>0,比较func4(x)和55的大小,如果不相等,引爆,接下来分析func4:

08048ca0 <func4>:
...
 8048cab:       cmp    $0x1,%ebx                   /比较x1/
 8048cae:       jle    8048cd0 <func4+0x30>         /x<=1,return 1/
 8048cb0:       add    $0xfffffff4,%esp
 8048cb3:       lea    -0x1(%ebx),%eax
 8048cb6:       push   %eax                         /push x-1/
 8048cb7:       call   8048ca0 <func4>                  /func4(x-1)/
 8048cbc:       mov    %eax,%esi                    /esi=func4(x-1)/
 8048cbe:       add    $0xfffffff4,%esp
 8048cc1:       lea    -0x2(%ebx),%eax
 8048cc4:       push   %eax                         /push x-2/
 8048cc5:       call   8048ca0 <func4>                  /func4(x-2)/ 
 8048cca:       add    %esi,%eax                    /return func4(x-1)+func4(x-2)/
 8048ccc:       jmp    8048cd5 <func4+0x35>
 8048cce:       mov    %esi,%esi
 8048cd0:       mov    $0x1,%eax
...

可以发现func4首先比较x和1,如果x<=1返回1,否则返回func4(x-1)+func4(x-2;
C语言等价代码为:

int func4(int x)
{
    if (x<=1)
        return 1;
    else
        return func4(x-1)+func4(x-2);
}
void phase_4(char *input)
{
    int x;
    int eax=sscanf(input,"%d",&x);
    if (eax!=1)
        explode_bomb();
    if (x<=0)
        explode_bomb();
    if (func4(x)!=55)
        explode_bomb();
}

可知这是一个斐波拉契数列:
x 0 1 2 3 4 5 6 7 8 9
func4 1 1 2 3 5 8 13 21 34 55
由此可知,阶段4的正确输入是以9为开头,后续字符串以非数字开头的字符串,关于隐藏关是什么,我们留到最后分析。

阶段五

08048d2c <phase_5>:  
...
 8048d3a:       push   %ebx                         /input/                                   
 8048d3b:       call   8049018 <string_length>                
 8048d40:       add    $0x10,%esp
 8048d43:       cmp    $0x6,%eax
 8048d46:       je     8048d4d <phase_5+0x21>
 8048d48:       call   80494fc <explode_bomb>
...

可以知道这里比较input的长度,如果不是6,引爆,接着:

08048d2c <phase_5>: 
 8048d4d:       xor    %edx,%edx                    /edx=0/
 8048d4f:       lea    -0x8(%ebp),%ecx          /ecx=&s[0]/
 8048d52:       mov    $0x804b220,%esi             /esi=&p[0]/
 .loop
 8048d57:       mov    (%edx,%ebx,1),%al       /al=input[edx]/
 8048d5a:       and    $0xf,%al                     /al&=0xf/
 8048d5c:       movsbl %al,%eax                 /eax=(int)al/
 8048d5f:       mov    (%eax,%esi,1),%al    /al=p[eax]/
 8048d62:       mov    %al,(%edx,%ecx,1)    /s[edx]=al/
 8048d65:       inc    %edx    
 8048d66:       cmp    $0x5,%edx
 8048d69:       jle    8048d57 <phase_5+0x2b>
 goto .loop

同样用gdb可以知道p的值是”isrveawhobpnutfg”,上述循环的功能是令
s[edx]=p[(int)(input[edx]&0xf)],接着:

08048d2c <phase_5>: 
...
8048d6b:       movb   $0x0,-0x2(%ebp)              /s[7]=0/
...
 8048d72:       push   $0x804980b                  /pattern/
 8048d77:       lea    -0x8(%ebp),%eax
 8048d7a:       push   %eax                         /s/
 8048d7b:       call   8049030 <strings_not_equal>
 8048d80:       add    $0x10,%esp
 8048d83:       test   %eax,%eax
 8048d85:       je     8048d8c <phase_5+0x60>   /字符串相等,return/
 8048d87:       call   80494fc <explode_bomb>
...

上面的代码是比较s和pattern是否相同,如果不相同,引爆;同样利用gdb可以知道pattern的值是”giant”。
等价c代码为:

char p[17]="isrveawhobpnutfg";
void phase_5(char *input)
{
    char s[7];
    if (string_length(input)!=6)
        explode_bomb();
    for (int edx=0;edx<=5;edx++)
    {
        int eax=(int)(input[edx]&0xf);
        s[edx]=p[eax];
    }
    s[7]='\0';
    if (strings_not_equal(s,"giants"))
        explode_bomb();
}

由此我们知道:input[edx]必须从下面取值,最后的,最后取任意组合即可:

edx 0 1 2 3 4 5
ASCII 0x_f 0x_0 0x_5 0x_b 0x_d 0x_1
字符 /,?,O,_,o 0, ,@,P,`,p %,5,E,U,e,u +,;,K,[,k,{ -,=,M,],m,} !,1,A,Q,a,q

阶段六

用gdb查看0x804b26c附近的内容:

(gdb) x/20x 0x804b26c
0x804b26c <node1>:  0x000000fd  0x00000001  0x0804b260  0x000003e9
...(省略)

根据之后的代码和第3个值,推测node1的类型是结构,其中有一个指针,顺着这个指针继续查找:

(gdb) x/20x 0x804b260
0x804b260 <node2>:  0x000002d5  0x00000002  0x0804b254  0x000000fd
...(省略)

可以确定结构由三个元素组成,两个整形数据,一个结构类型的指针,重复操作可以发现这是一个有6个元素的链表,称此结构为node,此时的链表为:

node(1)->node(2)->node(3)->node(4)->node(5)->node(6)->NULL

下面分析代码:

08048d98 <phase_6>:
...
 8048db1:       push   %eax                         /num/
 8048db2:       push   %edx                         /input/
 8048db3:       call   8048fd8 <read_six_numbers>
...

从input中读6个数到num[]中,接着:

08048d98 <phase_6>:
...
 8048db8:        xor    %edi,%edi                  /edi=0/
 8048dba:        add    $0x10,%esp
 8048dbd:        lea    0x0(%esi),%esi
 .loop
 8048dc0:        lea    -0x18(%ebp),%eax           /eax=num/
 8048dc3:        mov    (%eax,%edi,4),%eax
 8048dc6:        dec    %eax             /eax=num[edi]-1/
 8048dc7:        cmp    $0x5,%eax
 8048dca:        jbe    8048dd1 <phase_6+0x39>
 8048dcc:        call   80494fc <explode_bomb>          /确保num[edi]<=6/
 8048dd1:        lea    0x1(%edi),%ebx
 8048dd4:        cmp    $0x5,%ebx       /保证第一次内循环ebx<=5/
 8048dd7:        jg     8048dfc <phase_6+0x64>
 8048dd9:        lea    0x0(,%edi,4),%eax     /eax=4*edi/
 8048de0:        mov    %eax,-0x38(%ebp)            /保存eax/
 8048de3:        lea    -0x18(%ebp),%esi            /esi=num/
 .loop1
 8048de6:        mov    -0x38(%ebp),%edx        /edx=4*edi/
 8048de9:        mov    (%edx,%esi,1),%eax  /eax=num[edi]/
 8048dec:        cmp    (%esi,%ebx,4),%eax          /比较num[edi]和num[edx]/
 8048def:        jne    8048df6 <phase_6+0x5e>
 8048df1:        call   80494fc <explode_bomb>          /确保两者不相等/
 8048df6:        inc    %ebx
 8048df7:        cmp    $0x5,%ebx
 8048dfa:        jle    8048de6 <phase_6+0x4e>
 goto .loop1
 8048dfc:        inc    %edi
 8048dfd:        cmp    $0x5,%edi
 8048e00:        jle    8048dc0 <phase_6+0x28>
 goto .loop
...

这个二重循环的作用一是保证(unsigned)(num[edi]-1)<6,由补码数和无符号数转换关系知这样可以确保num[edi]为正且<7;另一作用是保证num中元素两两互异,接着:

08048d98 <phase_6>:
...
 8048e02:       xor    %edi,%edi                    /edi=0/
 8048e04:       lea    -0x18(%ebp),%ecx             /ecx=num/
 8048e07:       lea    -0x30(%ebp),%eax             /eax=p/
 8048e0a:       mov    %eax,-0x3c(%ebp)             /保存eax/
 8048e0d:       lea    0x0(%esi),%esi   
 .loop
 8048e10:       mov    -0x34(%ebp),%esi /esi=&node1/
 8048e13:       mov    $0x1,%ebx                   /ebx=1/
 8048e18:       lea    0x0(,%edi,4),%eax            /eax=4edi/
 8048e1f:       mov    %eax,%edx                    /edx=4edi/
 8048e21:       cmp    (%eax,%ecx,1),%ebx       /判断第一次内循环是否执行/      
 8048e24:       jge    8048e38 <phase_6+0xa0>   /ebx>=num[edi]/
 8048e26:       mov    (%edx,%ecx,1),%eax           /ebx<num[edi]/
 8048e29:       lea    0x0(%esi,%eiz,1),%esi        /eiz=0/
 .loop1
 8048e30:       mov    0x8(%esi),%esi       /esi=esi->next/
 8048e33:       inc    %ebx
 8048e34:       cmp    %eax,%ebx
 8048e36:       jl     8048e30 <phase_6+0x98>   /ebx<num[edi],继续/
 goto .loop1
 8048e38:       mov    -0x3c(%ebp),%edx             /edx=p/
 8048e3b:       mov    %esi,(%edx,%edi,4)       /p[edi]=esi/    
 8048e3e:       inc    %edi  
 8048e3f:       cmp    $0x5,%edi
 8048e42:       jle    8048e10 <phase_6+0x78>
 goto .loop
...

上述二重循环的功能是使p[edi]为指向node(num[edi])的指针,接着:

08048d98 <phase_6>:
...
 8048e44:       mov    -0x30(%ebp),%esi             /esi=p[0]/
 8048e47:       mov    %esi,-0x34(%ebp)             /保存esi/
 8048e4a:       mov    $0x1,%edi                       /edi=1/
 8048e4f:       lea    -0x30(%ebp),%edx             /edx=p/ 
 .loop
 8048e52:       mov    (%edx,%edi,4),%eax       /eax=p[edi]/
 8048e55:       mov    %eax,0x8(%esi)       /esi->next=p[edi]/
 8048e58:       mov    %eax,%esi                /esi=p[edi]/
 8048e5a:       inc    %edi    
 8048e5b:       cmp    $0x5,%edi
 8048e5e:       jle    8048e52 <phase_6+0xba>
 goto .loop
 8048e60:       movl   $0x0,0x8(%esi)  /node(num[5]).next=NULL/
...

上述循环的功能是使node(num[edi]).next=&node(num[edi+1]),此时的链表为:

node(num[0])->node(num[1])->node(num[2])->node(num[3])->node(num[4])->node(num[5])->NULL

接着进入下一个循环:

08048d98 <phase_6>:
...
 8048e67:       mov    -0x34(%ebp),%esi         /esi=p[0]=&node(num[0])/
 8048e6a:       xor    %edi,%edi                    /edi=0/
 8048e6c:       lea    0x0(%esi,%eiz,1),%esi        /esi=esi/
 .loop
 8048e70:       mov    0x8(%esi),%edx       /edx=esi->next/
 8048e73:       mov    (%esi),%eax          /eax=esi->value/
 8048e75:       cmp    (%edx),%eax      /比较eax和edx->value/
 8048e77:       jge    8048e7e <phase_6+0xe6>  
 8048e79:       call   80494fc <explode_bomb>       /链表元素出现升序,引爆/  
 8048e7e:       mov    0x8(%esi),%esi       /esi=esi->next/
 8048e81:       inc    %edi
 8048e82:       cmp    $0x4,%edi
 8048e85:       jle    8048e70 <phase_6+0xd8>  
     goto .loop 
...

上述循环检查是否经上一步操作后的链表是否为非增的(从链表头开始)。
等价c代码如下:

typedef struct node{
    int value;
    int idx;
    struct node *next;
}node;
node node1={0xfd,1,&node2};
node node2={0x2d5,2,&node3};
node node3={0x12d,3,&node4};
node node4={0x3e5,4,&node5};
node node5={0xd4,5,&node6};
node node6={0x1b0,6,NULL};
void phase_6(input)
{
    int num[6];
    read_six_numbers(input,num);
    for (int edi=0;edi<=5;edi++)
    {
        if ((unsigned)(num[edi]-1)>5)
            explode_bomb();
        for (int ebx=edi+1;ebx<=5;ebx++)
            if (num[edi]==num[ebx])
                explode_bomb();
    }/*确保元素>0且<7*/
    node *p[6];
    for (int edi=1;edi<=5;edi++)
    {
        node *esi=&node1;
        for (int ebx=1;ebx<num[edi];ebx++)
            esi=esi->next;
        p[edi]=esi;
    }/*令p[edi]为指向第num[edi]个节点的指针*/
    node *esi=p[0];
    for (int edi=1;edi<=5;edi++)
    {
        esi->next=p[edi];
        esi=p[edi];
    }/*令node(num[edi]).next=&node(num[edi+1])*/
    esi=p[0];
    for (int edi=0;edi<=4;edi++)
    {
        if (esi->value < esi->next->p->value)
            explode_bomb();
        esi=esi->next;
    }/*确保重新排序的链表是不增序列*/
}

将链表元素按value值由大到小排序,得到链表如下:

node(4)->node(2)->node(6)->node(3)->node(1)->node(5)->NULL

故num={4,2,6,3,1,5},对应的,input值是以”4 2 6 3 1 5”为开头且后续字符串以非数字字符开头的字符串。

隐藏关

首先来分析phase_defused:

0804952c <phase_defused>:
...
 8049533:       cmpl   $0x6,0x804b480
 804953a:       jne    804959f <phase_defused+0x73>
...

通过gdb 查看0x804b480处的值:

(gdb) x/20x 0x804b480
0x804b480 <num_input_strings>:  0x00000000  0x00000010  0x00000000 ...(省略)

阶段1通过后,再次查看此处的值:

0x804b480 <num_input_strings>:  0x00000001  0x00000010  0x00000000  0x7c010001...(省略)

重复上述操作,发现此处的值依次为2,3,4,5,6,因此这是输入字符串的个数,如果num_input_strings!=6,退出,反之继续分析:

0804952c <phase_defused>:
...
 804953c:       lea    -0x50(%ebp),%ebx 
 804953f:       push   %ebx                 /push s/
 8049540:       lea    -0x54(%ebp),%eax
 8049543:       push   %eax                 /push &n/
 8049544:       push   $0x8049d03
 8049549:       push   $0x804b770
 804954e:       call   8048860 <sscanf@plt>
...

用gdb查看0x8049d03,发现其值为”%d %s”再查看$0x804b770,其值如下:

(gdb) x/20x 0x804b770
0x804b770 <input_strings+240>:  0x00000039  0x00000000  0x00000000  0x00000000...(省略)

此时如果改变阶段4的输入,同样可以发现此处的值总是等于阶段4的输入字符串,说明read_line在这里存储了输入字符串的副本!相当于调用了sscanf(input,”%d %s”,&n,s),并且如果读入数据个数不是2就退出,接下来继续分析:

0804952c <phase_defused>:
...
 804955e:       push   $0x8049d09      /pattern/
 8049563:       push   %ebx             /s/
 8049564:       call   8049030 <strings_not_equal>
...

利用gdb可以判断此处将s和”austinpowers”比较,如果不同就退出,相同就调用secret_phase,下面分析secret_phase:

08048ee8 <secret_phase>:
...
 8048eef:       call   80491fc <read_line>
 8048ef4:       push   $0x0                                            
 8048ef6:       push   $0xa                            /十进制/   
 8048ef8:       push   $0x0
 8048efa:       push   %eax                         /input/
 8048efb:       call   80487f0 <__strtol_internal@plt> 
...

上面的代码是将input以十进制转换为对应的数,之后截断为int类型,判断其返回值x是否在1~1001(包括边界值),是则继续,反之引爆,继续分析:

08048ee8 <secret_phase>:
...
 8048f17:       push   %ebx             /x/
 8048f18:       push   $0x804b320      /&n1/
 8048f1d:       call   8048e94 <fun7>   
...

首先我们用gdb查看0x804b320处的值:

(gdb) x/3x 0x804b320
0x804b320 <n1>: 0x00000024  0x0804b314  0x0804b308
(gdb) x/3x 0x804b314
0x804b314 <n21>:    0x00000008  0x0804b2e4  0x0804b2fc  
(gdb) x/3x 0x804b308
0x804b308 <n22>:    0x00000032  0x0804b2f0  0x0804b2d8

可以推测n1,n21,n22以及他们指向的节点都是树的节点,其结构如图所示:
这里写图片描述
因此可以推断,该处调用了fun7(&n1,x),fun7的原型为

int fun7(tnode *pnode,int x);

下面分析fun7:

08048e94 <fun7>:
...
 8048ea0:       test   %edx,%edx          /pnode是否为NULL/
 8048ea2:       jne    8048eb0 <fun7+0x1c>  /不是,继续/
 8048ea4:       mov    $0xffffffff,%eax        /是的,返回-1/
 8048ea9:       jmp    8048ee2 <fun7+0x4e>
...

上面的代码保证递归到树的底层时,返回-1,继续分析:

08048e94 <fun7>:
...
(pnode in %edx)
 8048eb0:       cmp    (%edx),%eax      /比较x,pnode->value/
 8048eb2:       jge    8048ec5 <fun7+0x31>
 8048eb4:       add    $0xfffffff8,%esp    /x<pnode->value/
 8048eb7:       push   %eax                     /push x/
 8048eb8:       mov    0x4(%edx),%eax      /eax= pnode->l/
 8048ebb:       push   %eax                 /push pnode->l/
 8048ebc:       call   8048e94 <fun7>
 8048ec1:       add    %eax,%eax  /return 2*fun7(pnode->l,x)/
 8048ec3:       jmp    8048ee2 <fun7+0x4e>
 8048ec5:       cmp    (%edx),%eax          /x>=pnode->value,继续比较/
 8048ec7:       je     8048ee0 <fun7+0x4c>     
 8048ec9:       add    $0xfffffff8,%esp                /x>pnode->value/
 8048ecc:       push   %eax                     /push x/
 8048ecd:       mov    0x8(%edx),%eax           /eax=pnode->r/
 8048ed0:       push   %eax                 /push pnode->r/
 8048ed1:       call   8048e94 <fun7>       
 8048ed6:       add    %eax,%eax
 8048ed8:       inc    %eax     /return 2*fun7(pnode->r,x)+1/
 8048ed9:       jmp    8048ee2 <fun7+0x4e>
 8048edb:       nop
 8048edc:       lea    0x0(%esi,%eiz,1),%esi        /esi=esi/   
 8048ee0:       xor    %eax,%eax    /x==pnode->value,return 0/
...

从上面可以看出有两种可能的递归调用路径,下面是整个隐藏关等价c语言代码:

char *input_strings_4;
int  num_input_strings;
typedef struct tnode{
    int value;
    struct tnode *l,*r;
}tnode;
void secret_phase();
void phase_defused()
{
    if (num_input_strings!=6)
        return ;
    int n;
    char s[80];
    if (sscanf(input_strings_4,"%d %s",&n,s)!=2)
    {
        printf("Congratulations! You've defused the bomb!\n");
        return 0;
    }
    if (strings_not_equal(s,"austinpowers"))
    {
        printf("Congratulations! You've defused the bomb!\n");
        return 0;
    }
    printf("Curses, you've found the secret phase!\n");
    printf("But finding it and solving it are quite different...\n");
    secret_phase();
    printf("Congratulations! You've defused the bomb!\n");
}
int fun7(tnode *pnode,int x)
{
    if (pnode==NULL)
        return -1;
    if (x<s->value)
        return 2*fun7(pnode->l,x);
    if (x!=s->value)
        return 2*fun7(pnode->r,x)+1;
    return 0;
}
void secret_phase()
{
    input=read_line();
    int x=(int)__strtol_internal(input,0,10,0);
    if ((unsigned)(x-1)>1000)
        explode_bomb();
    if (fun7(&n1,x)!=7)
        explode_bomb();
    printf("Wow! You've defused the secret stage!\n");
}

fun7的返回值是7,由此可以推知,递归产生的返回值依次是0,1,3,7,即递归是沿着最右边的路径下降的,并且在最后一层x=pnode->value=1001,故可推知正确的字符串是以”1001”为开头且后续字符串以非数字字符的字符串。
综上所述,解除各阶段炸弹的一种输入为:
这里写图片描述
运行结果为:
这里写图片描述

展开阅读全文
©️2020 CSDN 皮肤主题: 大白 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值