实验目的
本实验通过要求你使用课程所学知识拆除一个“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