一、实验目的
1.理解C语言程序的机器级表示。
2.初步掌握GDB调试器的用法。
3.阅读C编译器生成的x86-64机器代码,理解不同控制结构生成的基本指令模式,过程的实现。
二、实验环境
1.SecureCRT(10.120.11.12)
2.Linux
3.Objdump命令反汇编
4.GDB调试工具
5.Xshell 软件
三、实验内容
登录bupt1服务器,在home目录下可以找到Evil博士专门为你量身定制的一个bomb,当运行时,它会要求你输入一个字符串,如果正确,则进入下一关,继续要求你输入下一个字符串;否则,炸弹就会爆炸,输出一行提示信息并向计分服务器提交扣分信息。因此,本实验要求你必须通过反汇编和逆向工程对bomb执行文件进行分析,找到正确的字符串来解除这个的炸弹。
本实验通过要求使用课程所学知识拆除一个“binary bombs”来增强对程序的机器级表示、汇编语言、调试器和逆向工程等方面原理与技能的掌握。 “binary bombs”是一个Linux可执行程序,包含了5个阶段(或关卡)。炸弹运行的每个阶段要求你输入一个特定字符串,你的输入符合程序预期的输入,该阶段的炸弹就被拆除引信;否则炸弹“爆炸”,打印输出 “BOOM!!!”。炸弹的每个阶段考察了机器级程序语言的一个不同方面,难度逐级递增。
为完成二进制炸弹拆除任务,需要使用gdb调试器和objdump来反汇编bomb文件,可以单步跟踪调试每一阶段的机器代码,也可以阅读反汇编代码,从中理解每一汇编语言代码的行为或作用,进而设法推断拆除炸弹所需的目标字符串。
四。实验步骤及实验分析
1.准备工作:
登录bupt1服务器,使用ls命令,发现炸弹文件压缩包bomb322.tar。使用tar -xvf bomb322.tar解压文件压缩包,得到一个目录bomb322。使用cd bomb322命令进入目录,使用ls命令,可看到三个文件bomb,bomb.c,README 。(answer文件为我自己建立的文本文件,用于存储答案),我的压缩包已经解压过了,所以没有演示该步骤
输入cat README命令,阅读README文件内容,确认炸弹bomb322属于学生2022211270。
2.观察bomb.c文件
发现实验分为六步,分别位于phase_1,phase_2,phase_3,phase_4,phase_5,phase_6六个函数中。其中还有一个隐藏关卡7位于sectet_phase
2.开始拆弹
注:如果不想在试错过程中错误的引爆炸弹,可以在每一次run程序前键入break explode_bomb,在爆炸函数这里设置断点,这样的话一旦输错即跳转到该断点处,此时再键入kill 终止程序,这样的话,就可以既知道输入的测试样例为错误答案又能不引爆炸弹
示例:
如果出现BOOM!想要及时退出程序,按住ctrl c 即可退出!
实验一
键入gdb bomb 进入调试窗口
键入b phase_1在phase_1函数处设置断点。
键入命令disas phase_1,观察phase_1的代码,根据<phase_1+9>处的代码,发现该关卡应输入一字符串,并与对应的字符串相互比较,二者一致则拆除炸弹通过关。
同时观察代码,根据<phasse_1+4>处的代码,可发现从键盘输入的字符串应与存储在0x402740处的字符串相互比较。故键入x /s 0x402670,以字符串形式显示该地址处的数据。得到一串字符串
因此第一关的答案为字符串:I am the mayor. I can do anything I want.
键入命令r运行程序,输入该字符串
随后使用crtl+C命令和kill命令安全退出调试程序。
打开answer文件,存入第一关答案I am the mayor. I can do anything I want.保存并退出。键入cat answer,检查字符串是否已存入文件answer。
重新进入gdb调试工具,键入r answer ,关卡一通过,说明正确答案已正确存入文件answer。
关卡一通过。
实验二
键入b phase_2在phase_2函数处设置断点
键入命令disas phase_2,观察phase_2的代码,根据<phase_2+25>处的代码,可知我们应输入六个数字。
键入disas read_six_numbers,通过观察<__isoc99_sscanf@plt>上边寄存器%esi的内容发现输入的必须是6个整数
并且如果输入的数字大于5,才不会引爆炸弹。
继续观察phase_2函数,汇编代码分析:
Dump of assembler code for function phase_2:
0x0000000000400f49 <+0>: push %rbp
0x0000000000400f4a <+1>: push %rbx
0x0000000000400f4b <+2>: sub $0x28,%rsp
0x0000000000400f4f <+6>: mov %fs:0x28,%rax
0x0000000000400f58 <+15>: mov %rax,0x18(%rsp)
0x0000000000400f5d <+20>: xor %eax,%eax
0x0000000000400f5f <+22>: mov %rsp,%rsi
0x0000000000400f62 <+25>: callq 0x40179f <read_six_numbers>
0x0000000000400f67 <+30>: cmpl $0x1,(%rsp)
0x0000000000400f6b <+34>: je 0x400f72 <phase_2+41>
//第一个数等于一,跳过爆炸函数,不会引爆炸弹
0x0000000000400f6d <+36>: callq 0x401769 <explode_bomb>
0x0000000000400f72 <+41>: mov %rsp,%rbx
//bx存放第一个数,bx==1
0x0000000000400f75 <+44>: lea 0x14(%rsp),%rbp
//bp存放(%rsp+0x14),转化为十进制是%rsp+20,(20/4==5)即bp存放输入的第6个整数
0x0000000000400f7a <+49>: mov (%rbx),%eax//ax==bx==1
0x0000000000400f7c <+51>: add %eax,%eax
//关键语句:ax==ax+ax==2*ax
0x0000000000400f7e <+53>: cmp %eax,0x4(%rbx)
//后一个数与前一个数的2倍进行比较,如果相同则不引爆,不相同则引爆,
说明后一个整数是前一个整数的二倍,
说明输入的6个整数应该是以 1开头的以2为公比的等比数列,即1 2 4 8 16 32
0x0000000000400f81 <+56>: je 0x400f88 <phase_2+63>
0x0000000000400f83 <+58>: callq 0x401769 <explode_bomb>
0x0000000000400f88 <+63>: add $0x4,%rbx
//第一个整数移动到第二个整数,以此类推
0x0000000000400f8c <+67>: cmp %rbp,%rbx
/循环结束的条件是(%rbx+0x4)==%rbp,前一个数的2倍==第6个数时,循环结束。
0x0000000000400f8f <+70>: jne 0x400f7a <phase_2+49>
//若不相等则进入循环,跳转到<+49>步
0x0000000000400f91 <+72>: mov 0x18(%rsp),%rax
//与栈保护相关
0x0000000000400f96 <+77>: xor %fs:0x28,%rax
0x0000000000400f9f <+86>: je 0x400fa6 <phase_2+93>
0x0000000000400fa1 <+88>: callq 0x400b90 <__stack_chk_fail@plt>
0x0000000000400fa6 <+93>: add $0x28,%rsp
0x0000000000400faa <+97>: pop %rbx
0x0000000000400fab <+98>: pop %rbp
--Type <RET> for more, q to quit, c to continue without paging--c
0x0000000000400fac <+99>: retq
因此,这道题的答案是1 2 4 8 16 32
键入命令r answer运行程序,输入1 2 4 8 16 32
随后使用crtl+C命令和kill命令安全退出调试程序。
打开answer文件,存入第二关答案0 1 1 2 3 5.保存并退出。键入cat answer,检查字符串是否已存入文件answer。
重新进入gdb调试工具,键入r answer ,关卡通过,说明正确答案已正确存入文件answer。
实验三
键入b phase_3在phase_3函数处设置断点。
键入命令disas phase_3,观察phase_3的代码,发现,输入函数的参数与0x402796有关,
使用x /s 0x402796函数进行观察,发现本次需要输入两个整数与一个字符。
同时发现输入的内容>2个才不会引爆炸弹。
继续向后观察,发现对第一个输入数字的判断,第一个数字以无符号比较不可大于7
<+318>为炸弹函数
继续向后观察,发现后续代码中存在一个switch语句,跳转位置取决于输入的第一个数字。
%eax中放第一个输入的数字
jmpq *0x4027b0(,%rax,8):*表示间接引用,8是指针存储为8字节的四字,用%rax*8才能到对应的跳转位置。
查看switch各分支的位置
由于第一个输入的数字<=7,且跳转表有8个跳转位置,因此有case0 case1.... case7
对应关系为:
(gdb) x /20xg 0x4027b0
0x4027b0: case0: 0x0000000000400ffa case1:0x000000000040101c
0x4027c0: case2: 0x000000000040103e case3: 0x0000000000401060
0x4027d0: case4: 0x000000000040107f case5: 0x000000000040109a
0x4027e0: case6: 0x00000000004010b5 case7: 0x00000000004010d0
故,以第一个输入数字为1为例:
查看对应的汇编代码:
0x0000000000401017 <+106>: jmpq 0x4010f5 <phase_3+328>
0x000000000040101c <+111>: mov $0x68,%eax
//101c对应的跳转位置,ax==0x68,第一个整数为1
0x0000000000401021 <+116>: cmpl $0x201,0x14(%rsp)
//第二个整数为0x201,即256*2+1=513,不相等则直接bomb
0x0000000000401029 <+124>: je 0x4010f5 <phase_3+328>
//跳转到<+328>位置
0x000000000040102f <+130>: callq 0x401769 <explode_bomb>
0x0000000000401034 <+135>: mov $0x68,%eax
0x00000000004010f5 <+328>: cmp 0xf(%rsp),%al
//al与ax寄存器的值相同,说明要找0x68对应的字符,即字符h
0x00000000004010f9 <+332>: je 0x401100 <phase_3+339>
0x00000000004010fb <+334>: callq 0x401769 <explode_bomb>
//下面与栈保护有关
0x0000000000401100 <+339>: mov 0x18(%rsp),%rax
0x0000000000401105 <+344>: xor %fs:0x28,%rax
0x000000000040110e <+353>: je 0x401115 <phase_3+360>
0x0000000000401110 <+355>: callq 0x400b90 <__stack_chk_fail@plt>
0x0000000000401115 <+360>: add $0x28,%rsp
0x0000000000401119 <+364>: retq
由分析可知,其中的一组答案为1 h 513,其他跳转过程同理。
随后使用crtl+C命令和kill命令安全退出调试程序。
打开answer文件,存入第三关答案1 h 513保存并退出。键入cat answer,检查字符串是否已存入文件answer。
重新进入gdb调试工具,键入r answer ,关卡通过,说明正确答案已正确存入文件answer。
至此,关卡三已通过。
实验四
键入b phase_4在phase_4函数处设置断点。
键入命令disas phase_4,观察phase_4的代码,同关卡三,我们可发现本题也是输入两个整数
%eax-0x2>=0,并且%eax-2<=2,由此可知
输入的数字n1 n2,有一个数字的范围在2-4(初步理解为n1>=2&&n1<=4)
观察phase_4代码可知,首先,%edi的值赋为6进入func4递归函数,%eax保存着递归函数返回值,与你输入的第二个数进行比对,如果%eax的值与你输入的第二个值相同,则不引爆炸弹,否则就会bomb。
因此,我们要输入的值:一个在2-4范围中,一个是初始值为6且经过func4函数的返回值,记为n1 n2
下面输入disas func4,观察其汇编代码
(gdb) disas func4
Dump of assembler code for function func4:
0x000000000040111a <+0>: test %edi,%edi
//判断%edi的值与0的关系
0x000000000040111c <+2>: jle 0x401149 <func4+47>
//如果<=0,则跳转到<+47>
0x000000000040111e <+4>: mov %esi,%eax
//如果%edi>0,则ax=si,初始时si==n1
0x0000000000401120 <+6>: cmp $0x1,%edi//di与1比较
0x0000000000401123 <+9>: je 0x401153 <func4+57>
//di==1,跳转到<+57>
0x0000000000401125 <+11>: push %r12
0x0000000000401127 <+13>: push %rbp
0x0000000000401128 <+14>: push %rbx
//入栈,引入参数,同时说明递归时用的是另外的寄存器,递归的参数为func4(edi,esi,eax)
0x0000000000401129 <+15>: mov %esi,%ebp
//bp=si
0x000000000040112b <+17>: mov %edi,%ebx
//bx=di
0x000000000040112d <+19>: lea -0x1(%rdi),%edi
//di--
0x0000000000401130 <+22>: callq 0x40111a <func4>
//递归回到开头
0x0000000000401135 <+27>: lea 0x0(%rbp,%rax,1),%r12d
//r12d=ax+bp
0x000000000040113a <+32>: lea -0x2(%rbx),%edi
//di=bx-2==di-2
0x000000000040113d <+35>: mov %ebp,%esi
//si=bp
0x000000000040113f <+37>: callq 0x40111a <func4>
//递归回到开头
0x0000000000401144 <+42>: add %r12d,%eax
//核心部分:ax=r12d+ax,又r12d=ax+bp,bp=si,ax=ax+si+ax==2*ax+n1;
0x0000000000401147 <+45>: jmp 0x40114f <func4+53>
//跳转至<+53>,出栈
0x0000000000401149 <+47>: mov $0x0,%eax
0x000000000040114e <+52>: retq
//return 0
0x000000000040114f <+53>: pop %rbx
0x0000000000401150 <+54>: pop %rbp
0x0000000000401151 <+55>: pop %r12
0x0000000000401153 <+57>: repz retq
End of assembler dump.
看着有些乱,整合一下:
(gdb) disas func4
Dump of assembler code for function func4:
0x000000000040111a <+0>: test %edi,%edi
0x000000000040111c <+2>: jle 0x401149 <func4+47>
//if(edi<=0) return 0
0x0000000000401149 <+47>: mov $0x0,%eax
0x000000000040114e <+52>: retq
0x000000000040114f <+53>: pop %rbx
0x0000000000401150 <+54>: pop %rbp
0x0000000000401151 <+55>: pop %r12
0x0000000000401153 <+57>: repz retq
//else if(edi>0)
0x000000000040111e <+4>: mov %esi,%eax
0x0000000000401120 <+6>: cmp $0x1,%edi
//if(edi>0&&edi==1) return n1,结束递归的条件。
0x0000000000401123 <+9>: je 0x401153 <func4+57>
0x0000000000401153 <+57>: repz retq
//if(edi>0&&edi!=1)
0x0000000000401125 <+11>: push %r12
0x0000000000401127 <+13>: push %rbp
0x0000000000401128 <+14>: push %rbx
0x0000000000401129 <+15>: mov %esi,%ebp//ebp=esi=n1
0x000000000040112b <+17>: mov %edi,%ebx
0x000000000040112d <+19>: lea -0x1(%rdi),%edi
0x0000000000401130 <+22>: callq 0x40111a <func4>
//func4(edi--,esi,eax),esi=n1,并且%eax中存储func4函数的返回值
0x0000000000401135 <+27>: lea 0x0(%rbp,%rax,1),%r12d
//%r12d=func4(edi--,esi,eax)+n1
0x000000000040113a <+32>: lea -0x2(%rbx),%edi//edi-2
0x000000000040113d <+35>: mov %ebp,%esi
0x000000000040113f <+37>: callq 0x40111a <func4>
//%eax=func4(edi-2,esi,eax),esi=n1
0x0000000000401144 <+42>: add %r12d,%eax
//(计算前)eax==func4(edi-2,esi,eax)
//r12d==func4(edi--,esi,eax)+n1
//(计算后)eax==func4(edi--,esi,eax)+func4(edi-2,esi,eax)+n1
//核心:func4(edi,esi,eax)func4(edi--,esi,eax)+func4(edi-2,esi,eax)+n1
0x0000000000401147 <+45>: jmp 0x40114f <func4+53>
End of assembler dump.
由此,若n1==2时
func4(1)==func4(0)+func4(-1)+n1==0+0+2==2
func4(2)==func4(1)+func4(0)+n1==2+0+2==4
func4(3)==func4(2)+func4(1)+n1==4+2+2==8
func4(4)==func4(3)+func4(2)+n1==8+4+2==14
func4(5)==func4(4)+func4(3)+n1==14+8+2==24
func4(6)==func4(5)+func4(4)+n1==24+14+2==40
因此 n1==2,n2==40为一组答案,(但不知道为啥,这里输入40 2才算过,输入2 40反而不行)
将答案输入程序进行测试,成功拆弹,后将答案存入answer文件。
实验五
键入b phase_5在phase_5函数处设置断点。
键入命令disas phase_5,观察phase_5的代码
观察代码,发现本次需要输入一个长度为6的字符串
(gdb) disas phase_5
Dump of assembler code for function phase_5:
0x00000000004011c2 <+0>: push %rbx
0x00000000004011c3 <+1>: sub $0x10,%rsp
0x00000000004011c7 <+5>: mov %rdi,%rbx
0x00000000004011ca <+8>: mov %fs:0x28,%rax
0x00000000004011d3 <+17>: mov %rax,0x8(%rsp)
0x00000000004011d8 <+22>: xor %eax,%eax
0x00000000004011da <+24>: callq 0x401477 <string_length>
//输入字符串,6个字符
0x00000000004011df <+29>: cmp $0x6,%eax
0x00000000004011e2 <+32>: je 0x4011e9 <phase_5+39>
0x00000000004011e4 <+34>: callq 0x401769 <explode_bomb>
0x00000000004011e9 <+39>: mov $0x0,%eax
//eax=0
0x00000000004011ee <+44>: movzbl (%rbx,%rax,1),%edx
//dx=bx+ax
0x00000000004011f2 <+48>: and $0xf,%edx
//edx=edx&0xf
0x00000000004011f5 <+51>: movzbl 0x4027f0(%rdx),%edx
//dx=dx+0x4027f0
0x00000000004011fc <+58>: mov %dl,(%rsp,%rax,1)
0x00000000004011ff <+61>: add $0x1,%rax
//ax=ax+1
0x0000000000401203 <+65>: cmp $0x6,%rax
0x0000000000401207 <+69>: jne 0x4011ee <phase_5+44>
//存在循环,rax记录当前的字符的位置,如果rax!=6,则继续循环
0x0000000000401209 <+71>: movb $0x0,0x6(%rsp)
//6个字符后面赋值为0
0x000000000040120e <+76>: mov $0x40279f,%esi
//esi=0x40279f
0x0000000000401213 <+81>: mov %rsp,%rdi
//rdi放首字符地址
0x0000000000401216 <+84>: callq 0x401495 <strings_not_equal>
0x000000000040121b <+89>: test %eax,%eax
0x000000000040121d <+91>: je 0x401224 <phase_5+98>
//判断字符串是否匹配
0x000000000040121f <+93>: callq 0x401769 <explode_bomb>
0x0000000000401224 <+98>: mov 0x8(%rsp),%rax
0x0000000000401229 <+103>: xor %fs:0x28,%rax
0x0000000000401232 <+112>: je 0x401239 <phase_5+119>
--Type <RET> for more, q to quit, c to continue without paging--c
0x0000000000401234 <+114>: callq 0x400b90 <__stack_chk_fail@plt>
0x0000000000401239 <+119>: add $0x10,%rsp
0x000000000040123d <+123>: pop %rbx
0x000000000040123e <+124>: retq
End of assembler dump.
观察到两个奇怪的地址,用x /s 指令查看内容,发现是两个字符串,而且0x40279f对应的刚好是6个字符,再返回去观察汇编代码
可以推理出:edx=edx$0xf,存放的是每一次输入的字符的十六进制值与0xf按位与的结果
按照这个结果作为位置索引,在maduiersnfotvbylSo字符串中找到对应的字符,最终组成的字符串按顺序与bruins作比较,相同则通过,不相同则引爆炸弹。
示例 设未知数x,它的低四位与1111按位与后结果是它本身,这个结果如果算出13,根据13这个下标在maduiersnfotvbylSo找到“b”,构成“bruins”的第一个字符,而x本身的低四位是D,根据ASCII表找出m符合条件。
b在“maduiersnfotvbylSo”中为第14个,对应的下标为13,ASCII中低四位二进制为13的字符有m
r在“maduiersnfotvbylSo”中为第7个,对应的下标为6,ASCII中低四位二进制为6的字符有f
u在“maduiersnfotvbylSo”中为第4个,对应的下标为3,ASCII中低四位二进制为3的字符有c
i在“maduiersnfotvbylSo”中为第5个,对应的下标为4,ASCII中低四位二进制为4的字符有d,t
n在“maduiersnfotvbylSo”中为第9个,对应的下标为8,ASCII中低四位二进制为8的字符有h,x
s在“maduiersnfotvbylSo”中为第8个,对应的下标为7,ASCII中低四位二进制为7的字符有g,w
因此,一组结果为mfcdhg。
将答案输入程序进行测试,成功拆弹,后将答案存入answer文件。
心得体会
1.汇编语言中$加操作数表示立即数
2.分析函数时不要只看某几条语句,看到不懂的地方就先向下看,根据后面的逻辑反推前面的逻辑。
3、 大胆的猜测也是一个很重要的策略。
4、递归函数和循环往往很复杂,这种时候好的转换为C语言伪代码的能力就变得很重要
5.有时候没必要分析每一句的含义,学会抓大放小。