本实验通过要求你使用课程所学知识拆除一个“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) /比较x和0/
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 /比较x和1/
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”为开头且后续字符串以非数字字符的字符串。
综上所述,解除各阶段炸弹的一种输入为:
运行结果为:
原文:http://blog.csdn.net/u010327109/article/details/51241368