csapp实验bomb lab(反汇编技术与gdb调试)

一.实验目的

该实验要在linux环境下做,因为我的虚拟机VMware不能与宿主机之间传输文件、复制黏贴(弄了好长时间也没弄成),所以实验包我是在虚拟机中登录官网下载的,下载地址为:

CS:APP3e, Bryant and O'Hallaron (cmu.edu)

下载Bomb Lab后面的 Self-Study Handout.

里面包含三个文件:

图1 实验包中的文件

bomb是可执行文件,bomb.c是C文件,里面是C语言代码,告诉了我们如何使用这个实验,大致意思是一共有六个关卡,一个关卡就是一个函数(phase_1~phase_6),每个关卡都有密码,是字符串形式的密码,如果你输入的密码正确,会返回Phase defused,如果输入错误,那么炸弹就会爆炸(返回错误)。

二.实验知识预备

因为我们不知道六个关卡函数具体的代码都是什么,我们只有可执行程序,但是我们可以应用反汇编技术把可执行程序转化为汇编程序。通过阅读汇编程序,我们就可以推断出程序的具体作用,从而给出正确的密码输入。

1.反汇编技术

(1)objdump

linux下自带的一种工具,一般在gcc包中,可以把linux下的可执行文件反汇编,用法示例:

-a 文件名 显示当前文件的格式

-d 文件名 反汇编

-f 文件名 显示文件头信息

扫描二维码关注公众号,回复: 14677968 查看本文章

-h 文件名 显示各section的头信息

-x 文件名 显示全部头文件信息

-s 文件名 显示头文件信息及所对十六进制信息

-S 目标文件 显示反汇编代码,将反汇编代码与源代码交替显示,编译时需要使用-g参数,即需要调试信息;

-C 目标文件 将C++符号名逆向解析

-l 文件名 反汇编代码中插入文件名和行号

-j文件名 仅反汇编指定的section

更多可参看参考文献[1]

(2)gdb反汇编

gdb调试工具的disas命令可以对局部代码反汇编,使用格式一般是:disas 函数名

具体可参看文献[2]和文献[3]

2.GDB调试工具

程序中出现的语法错误可以借助编译器解决;但逻辑错误则只能靠自己解决。实际场景中解决逻辑错误最高效的方法,就是借助调试工具对程序进行调试。

调试(Debug),就是让代码一步一步慢慢执行,跟踪程序的运行过程。比如,可以让程序停在某个地方,查看当前所有变量的值,或者内存中的数据;也可以让程序一次只执行一条或者几条语句,看看程序到底执行了哪些代码。

也就是说,通过调试程序,我们可以监控程序执行的每一个细节,包括变量的值、函数的调用过程、内存中数据、线程的调度等,从而发现隐藏的错误或者低效的代码。

GDB 全称“GNU symbolic debugger”,从名称上不难看出,它诞生于 GNU 计划(同时诞生的还有 GCC、Emacs 等),是 Linux 下常用的程序调试器。发展至今,GDB 已经迭代了诸多个版本,当下的 GDB 支持调试多种编程语言编写的程序,包括 C、C++、Go、Objective-C、OpenCL、Ada等。实际场景中,GDB 更常用来调试 C 和 C++程序。

GDB的命令详解可以参看参考文献[4]。

3.实验所用汇编知识

注意:本实验反汇编出的汇编语言是X86-64汇编,采用的是ATT语法,而不是dos和windows系统上通常使用的Intel语法,因为反汇编指令objdump输出的默认格式就是ATT,比如:

move a,b

在Intel语法中,作用是b的值传递给a,第一个操作数始终是目的,第二个是源。

但是在ATT语法中,正好相反,作用是a的值传递给b,第一个是源,第二个是目的。

具体区别在文献[5]中给出,如下:

图2 ATT与Intel区别

(1)整数寄存器

一个x86-64的CPU包含16个存储64位值的通用目的寄存器,用来存储整数数据和指针。

图3 整数寄存器

最初的8086只有8个16位的寄存器,即图中的%ax到%bp。每个寄存器都有特殊的用途。扩展到IA32 架构时,这些寄存器也扩展成32位寄存器,标号从%eax到%ebp。扩展到x86-64后,原来的8个寄存器扩展成64位,标号从%rax到%rbp。除此之外还增加了8个新的寄存器,从%r8到%r15。[5]

对于这些寄存器,用途大致如下:

eax(32位)/rax(64位):
通常用来执行加法,函数调用的返回值一般也放在这里面
ebx(32位)/rbx:
通常用来数据存取
ecx(32位)/rcx:
通常用作for循环的计数器
edx(32位)/rdx(64位):
读取I/O端口时,存放端口号
esp(32位)/rsp(64位):
栈顶指针,指向栈的顶部
ebp(32位)/rbp(64位):
栈底指针,指向栈的底部,用ebp+偏移量的形式来定位函数存放在栈中的局部变量
esi(32位)/rsi(64位):
字符串操作时,用于存放数据源的地址
edi(32位)/rdi(64位):
字符串操作时,用于存放目的地址的,和esi两个经常搭配一起使用,执行字符串的复制等操作

详细可参看参考文献[8]。

实验中用到的是32位和64位的前8个寄存器。

(2)条件码寄存器

是CPU维护的一组单个位的用于描述最近的算术或逻辑操作的状态属性,条件分支指令可以检测这些寄存器的值来确定转移方向。

  •  CF:进位标志。最近的操作使最高位产生了进位或借位,可用来检查无符号操作的溢出。

  •  ZF:零标志。最近的操作得出的结果为0。

  •  SF:符号标志。最近的操作得到的结果为负数。

  •  OF:溢出标志。最近的操作导致一个补码溢出正溢出或负溢出。

(3)实验常用汇编指令

参看文献[7]、[10]。

4.内存原理

图4 linux x86-64内存映像

内存一般分五大区,有“栈区”、“堆区”、“全局/静态区”、“常量区”、“代码区”。关于这五大区分别存放什么,有什么作用,可以参看参考文献[6]。

在本实验中,我们只关注栈区。

图5 栈区示意图

内存的栈区从上向下“生长”,但是地址是从下向上增大。该区由编译器自动分配、释放,数据先进栈的后出栈。栈区存放局部变量(包括const修饰的)、函数的参数和其返回值

当某个函数运行时,机器需要分配一定的内存去进行函数内的各种操作,这个过程中分配的那部分栈称为栈帧

栈帧是一段有界限的内存区间,由最顶端的两个指针界定,寄存器 %ebp 为帧指针(栈底),也叫“基指针”。而寄存器 %esp 为栈指针,也就是说寄存器%ebp保存了所分配内存的最高地址,寄存器%esp保存了所分配内存的最低地址。

三.实验过程

本文实验是在openEuler 22.03上进行的。

首先我们打开实验包,来到bomb的目录下,右键选择“打开终端”。

图6 在bomb目录下打开终端

1.反汇编整个文件

在终端输入下面语句:

objdump -d bomb > bomb.txt

该命令把bomb可执行文件反汇编后输出到一个文本文件上。方便我们阅读代码。

执行后,可以发现bomb目录下多了一个bomb.txt文件。

图7 生成bomb.txt文件

打开文件可以发现里面是本程序的汇编代码:

图8 文件汇编代码

里面也包括了六个关卡函数“phase_1~phase_6"。

下面我们启动gdb,对每个函数设置断点分步调试,逐个攻克。

2.关卡1——phase_1

启动GDB:

在终端输入语句:

gdb bomb

图9 启动GDB调试

终端会打印出gdb的版本等信息,不用理会。

下面我们在函数1——phase_1处设置断点,然后调试运行。

图10 设置断点

输入语句:

break phase_1

可以看到断点设置在地址0x400ee0处,我们打开刚才反汇编得到的bomb.txt文件,查看phase_1,起始地址就是0x400ee0。

图11 函数phase_1的汇编代码

然后在终端输入run,开始调试

图12 run调试程序

此处运行的语句在bomb.c文件中可以查到:

   printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
   printf("which to blow yourself up. Have a nice day!\n");

我们可以在bomb.c文件中看一下详细代码

   printf("Welcome to my fiendish little bomb. You have 6 phases with\n");
   printf("which to blow yourself up. Have a nice day!\n");

    /* Hmm...  Six phases must be more secure than one phase! */
    input = read_line();             /* Get input                   */
    phase_1(input);                  /* Run the phase               */
    phase_defused();                 /* Drat!  They figured it out!
                      * Let me know how they did it. */

运行完两个输出语句后,要读入一个字符串输入,然后传递给phase_1()函数判别是否是正确密码,我们此时还不知道正确密码是什么,但是因为设置了断点卡在phase_1前,其实也就是卡在了phase_1(input)这条语句之前,那么我们即使乱输入一个密码,也不会触发程序马上爆炸。

所以此处我们胡乱输入一个密码,比如:

图13输入一个字符串tangsun

可以看到已经到达了断点,下一步我们对该函数进行反汇编,看一看这个程序究竟是如何运作的

输入语句:

disas phase_1

终端输出phase_1的汇编代码:

图14 函数phase_1汇编代码

rsp为栈顶指针,第一句:

sub $0x8,%rsp

操作是rsp=rsp-$0x8,这里的$0x8是一个16进制常量,8个字节,%rsp存的是栈顶指针的地址,该操作可以把栈顶指针向栈顶移动8个字节的空间,支持后面函数的各种操作。

下一句move $0x402400,%esi,将地址为$0x402400处的值传递给32位寄存器esi。

call 0x401338 <strings_not_equal>

调用地址为0x401338处的函数strings_not_equal,根据函数名我们可以大致判断出该函数的作用应该是比较两个字符串,如果不等,那么返回xxx。我们打开bomb.txt,在地址0x401338处找到该函数:

0000000000401338 <strings_not_equal>:
  401338:       41 54                   push   %r12
  40133a:       55                      push   %rbp
  40133b:       53                      push   %rbx
  40133c:       48 89 fb                mov    %rdi,%rbx
  40133f:       48 89 f5                mov    %rsi,%rbp
  401342:       e8 d4 ff ff ff          call   40131b <string_length>
  401347:       41 89 c4                mov    %eax,%r12d
  40134a:       48 89 ef                mov    %rbp,%rdi
  40134d:       e8 c9 ff ff ff          call   40131b <string_length>
  401352:       ba 01 00 00 00          mov    $0x1,%edx
  401357:       41 39 c4                cmp    %eax,%r12d
  40135a:       75 3f                   jne    40139b <strings_not_equal+0x63>
  40135c:       0f b6 03                movzbl (%rbx),%eax
  40135f:       84 c0                   test   %al,%al
  401361:       74 25                   je     401388 <strings_not_equal+0x50>
  401363:       3a 45 00                cmp    0x0(%rbp),%al
  401366:       74 0a                   je     401372 <strings_not_equal+0x3a>
  401368:       eb 25                   jmp    40138f <strings_not_equal+0x57>
  40136a:       3a 45 00                cmp    0x0(%rbp),%al
  40136d:       0f 1f 00                nopl   (%rax)
  401370:       75 24                   jne    401396 <strings_not_equal+0x5e>
  401372:       48 83 c3 01             add    $0x1,%rbx
  401376:       48 83 c5 01             add    $0x1,%rbp
  40137a:       0f b6 03                movzbl (%rbx),%eax
  40137d:       84 c0                   test   %al,%al
  40137f:       75 e9                   jne    40136a <strings_not_equal+0x32>
  401381:       ba 00 00 00 00          mov    $0x0,%edx
  401386:       eb 13                   jmp    40139b <strings_not_equal+0x63>
  401388:       ba 00 00 00 00          mov    $0x0,%edx
  40138d:       eb 0c                   jmp    40139b <strings_not_equal+0x63>
  40138f:       ba 01 00 00 00          mov    $0x1,%edx
  401394:       eb 05                   jmp    40139b <strings_not_equal+0x63>
  401396:       ba 01 00 00 00          mov    $0x1,%edx
  40139b:       89 d0                   mov    %edx,%eax
  40139d:       5b                      pop    %rbx
  40139e:       5d                      pop    %rbp
  40139f:       41 5c                   pop    %r12
  4013a1:       c3                      ret

根据汇编代码,我们可看出该函数的作用是传进去两个参数,根据前面寄存器用途的介绍,%esi一般作为源,%edi一般作为目的,那么函数的传参形式应该是strings_not_equal(%edi,%esi)。

%eax和%r12d分别存储%esi和%edi,在第12行比较二者大小,实际上原理是这样的:

cmp  %eax,%r12d

CMP结果

ZF

CF

目的操作数 < 源操作数

0

1

目的操作数 > 源操作数

0

0

目的操作数 = 源操作数

1

0

第13行的jne指令,当ZF=0时跳转,ZF=1时不跳转。

也就是说12、13两行的作用是:

if( %eax == %r12d)
继续执行
else
跳转到40139b处

40139b行是mov %edx,%eax,直接把edx传给eax然后返回结果,在第11行我们知道edx已经是1了,那么最后返回的结果(%eax)就是1。

也就是说该函数当两个参数不相等,则返回1。根据后面的语句可看出若两个参数相等则返回0


我们回到phase_1上来,后面就很好理解了。

 400eee:       85 c0             test   %eax,%eax                //判断%eax是否为0
 400ef0:       74 05             je     400ef7 <phase_1+0x17>   //%eax为0则跳转到400ef7
 400ef2:       e8 43 05 00 00    call   40143a <explode_bomb>   //不为0则炸弹爆炸,失败
 400ef7:       48 83 c4 08       add    $0x8,%rsp              //栈顶指针向栈底移动8个字节
 400efb:       c3                ret                       //程序结束

%eax为0也就是两个字符串相等,也就是你输入的密码正确,我们前面知道,输入到strings_not_equal()函数的两个参数分别保存在%esi和%edi中,我们可以推测需要我们手动输入的密码存放在寄存器%edi中,为什么呢?

因为寄存器的使用是有顺序的,使用顺序是%edi, %esi, %edx, %ecx,在图3可以看到,反汇编出的代码中第2行使用了寄存器%esi,那么说明我们之前已经使用过了%edi,我们之前输入了密码,那么密码就一定保存在%edi中,而%esi自然保存的就是需要匹配的正确答案!

mov $0x402400 %esi

找到了!,答案就存放在地址0x402400处,如何查看该地址处存放了什么呢?

gdb中有指令x,可以查看内存地址中的值,x/s可以查看字符串变量的值。 该指令的具体使用可以参看文献[9]。

图15 查看0x402400处的内容

正确答案就是“Border relations with Canada have never been better"。

下次进入时,输入答案后:

图16 通过第一关

3.关卡2——phase_2

套路与关卡1相同。

直接上反汇编后得到的代码:

图17 phase_2汇编代码

sub $0x28,%rsp
move %rsp,%rsi

首先还是熟悉的语句:sub $0x28 , %rsp , 作用是开辟出40个字节的空间留给程序使用。把栈指针存放在寄存器%rsi中。

call 0x40145c<read_six_numbers>

调用函数read_six_numbers,从函数名可以猜测出此时读入6个数字。

cmpl   $0x1,(%rsp)
je     400f30 <phase_2+52>
call   40143a <explode_bomb>

判断如果栈指针%rsp指向的位置值是1,就跳转到52行,否则就爆炸。我们跳转到52行,看不爆炸会执行什么。

lea    0x4(%rsp),%rbx
lea    0x18(%rsp),%rbp
jmp    400f17 <phase_2+27>

%rsp+0x4——>%rbx,此时%rbx指向的是第2个值的地址,第一个值是输入到%edi中。

%rsp+0x18——>%rbp,此时%rbp指向的是最后一个值(第六个)的地址。

然后跳转到27行:

mov    -0x4(%rbx),%eax
add    %eax,%eax
cmp    %eax,(%rbx)
je     400f25 <phase_2+41>
call   40143a <explode_bomb>

%rbx指针向栈顶移动0x4,然后把该处的值(第一个数)传递给%eax。%eax=%eax+%eax,让该处的值乘2。

比较%eax和%rbx值的大小,其实也就是比较第一个值的2倍与第二个值的大小。如果相等,跳转到41行。如果不等,则爆炸。我们跳转到41行:

add    $0x4,%rbx
cmp    %rbp,%rbx
jne    400f17 <phase_2+27>
jmp    400f3c <phase_2+64>

把%rbx增加4个字节(向栈底移动4个字节),指向下一个值,也就是第三个值。比较第三个值与栈底指针指向的值(第六个值),如果不相等,跳转到27行,相等跳转64行,其实也就是验证下一个值是不是最后一个,如果是,那就比较结束,跳出整个程序,如果不是,那就继续比。

跳到27行依然是同样的套路,如果第三个值是第二个值的二倍,那就继续运行,如果不是就爆炸。

跳到64行:

add    $0x28,%rsp
pop    %rbx
pop    %rbp
ret

%rsp向栈底移动40个字节,弹出%rbx和%rbp,收场。

总结下来就是:

输入的第一个值必须是1,让第一个炸弹不爆炸,后面就是等比数列,依次是前一个的二倍。

输入:1 2 4 8 16 31

图18 第二关通过

4.关卡3——phase_3

图19 第三关汇编

sub    $0x18,%rsp
lea    0xc(%rsp),%rcx
lea    0x8(%rsp),%rdx
mov    $0x4025cf,%esi
mov    $0x0,%eax

第一步栈指针开辟空间。 然后让%rcx指向%rsp+0xc,让%rdx指向%rsp+0x8,由寄存器使用规律可知,%edi和esi已经使用过,下面的使用顺序就是%rdx、%rcx。然后把$0x4025cf处的值赋给%esi,此时我们知道才使用%esi,说明%rdx、%rcx还没有被赋值。由于给出地址,可以查看一下该地址处究竟是什么:

图20 查看存放值

存放着"%d %d",先不用管,往下看,mov $0x0,%eax,把%eax赋值为0。

call   400bf0 <__isoc99_sscanf@plt>
cmp    $0x1,%eax
jg     400f6a <phase_3+39>
call   40143a <explode_bomb>

调用了函数,根据函数名,我们大概可以猜出使用了sscanf输入值,因为%esi已经使用过,所以此时输入的值保存在%rdx、%rcx中,根据前面%esi中保存的是"%d,%d",所以%rdx、%rcx存储的都是整数,也就是我们输入的值必须是整数。

此外,sscanf函数原型为:

int sscanf(const char *str, const char *format,......);

sscanf()会将参数str的字符串根据参数format字符串来转换格式并格式化数据。转换后的结果存于对应的参数内。成功则返回参数数目,失败则返回0。返回值保存在%eax中。

然后比较%eax保存的值和1,如果大于1,也就是两个参数都输入成功,那就跳转到39行,否则爆炸。(jg:如果目的大于源,跳转)

我们来看39行。

cmpl   $0x7,0x8(%rsp)
ja     400fad <phase_3+106>
mov    0x8(%rsp),%eax
jmp    *0x402470(,%rax,8)
mov    $0xcf,%eax
jmp    400fbe <phase_3+123>
mov    $0x2c3,%eax
jmp    400fbe <phase_3+123>
mov    $0x100,%eax
jmp    400fbe <phase_3+123>
mov    $0x185,%eax
jmp    400fbe <phase_3+123>
mov    $0xce,%eax
jmp    400fbe <phase_3+123>
mov    $0x2aa,%eax
jmp    400fbe <phase_3+123>
mov    $0x147,%eax
jmp    400fbe <phase_3+123>
call   40143a <explode_bomb>

如果%rsp+0x8处的值大于7,跳转106行(ja与jg作用类似),我们之前知道,%rsp+0x8就是%rdx的地址,106行是爆炸,说明%rdx处的值不能大于7,也就是说我们输入的第一个数要小于7

下面就是将%rsp+0x8处的值赋值给%eax,将我们输入的第一个数存到%eax,然后跳转到地址*0x402470,*0x402470(,%rax,8)就是从*0x402470开始,以8字节的%rax倍增长地址,%rax就是倍数,也就是我们输入的第一个数(%rdx),因为这个数要<=7,否则爆炸,可以看一下0,1,2,3,4,5,6,7倍相应的值:

图21 查看各地址值

一目了然,这就是一个跳转表,输入不同的值可以跳转到不同行,比如我们输入1,跳转到118行:

mov    $0x137,%eax

把%eax赋值为$0x137,然后比较%rsp+0xc处的值,也就是输入的第二个值,是否等于%eax处的0x137,如果相等,跳转134行,否则爆炸。

134行就收场结束了,所以1 $0x137就是一个正确答案。

注意输入答案时,十六进制要转换为十进制,最后输入:1 311

图22 第三关通关

此题也可以跳转其他行,第一个数可以是0到7的任一个数,随之第二个数也跟随着变化。

5.关卡4——phase_4

图23 第四关汇编

sub    $0x18,%rsp
lea    0xc(%rsp),%rcx
lea    0x8(%rsp),%rdx
mov    $0x4025cf,%esi
mov    $0x0,%eax
call   400bf0 <__isoc99_sscanf@plt>

栈指针开辟空间,接下来的几条指令与phase_3相同,不再赘述,只需要知道:

%rcx指向%rsp+0xc,%rdx指向%rsp+0x8。

地址$0x4025cf处存放的也是"%d %d"。 将%eax赋值为0,然后输入两个参数,存放在%edx和%ecx中。

cmp    $0x2,%eax
jne    401035 <phase_4+41>

如果输入的参数不是两个,就跳转41行,一看是爆炸函数,我们看不爆炸的情况。

cmpl   $0xe,0x8(%rsp)
jbe    40103a <phase_4+46>
call   40143a <explode_bomb>

如果%rsp+0x8处的值(%rdx)小于等于0xe(14),跳转46行,否则爆炸,来看46行:

mov    $0xe,%edx
mov    $0x0,%esi
mov    0x8(%rsp),%edi

edx=14 , esi=0 , edi=rdx

call   400fce <func4>
test   %eax,%eax
jne    401058 <phase_4+76>
cmpl   $0x0,0xc(%rsp)
je     40105d <phase_4+81>
call   40143a <explode_bomb>
add    $0x18,%rsp
ret

然后调用函数func4,然后测试eax是否为0,如果不是就跳转到76行,爆炸。如果是就接着执行,下面比较%rsp+0cx(%rcx)是否为0,是就跳转81行,81行收场结束,所以我们知道输入的第二个参数必须是0

那第一个参数呢,我们只知道必须要小于等于14,还有一个点,就是执行完func4之后,eax必须是0,否则也爆炸,我们来看一下func4的汇编代码:

0000000000400fce <func4>:
  400fce:       48 83 ec 08             sub    $0x8,%rsp
  400fd2:       89 d0                   mov    %edx,%eax
  400fd4:       29 f0                   sub    %esi,%eax
  400fd6:       89 c1                   mov    %eax,%ecx
  400fd8:       c1 e9 1f                shr    $0x1f,%ecx
  400fdb:       01 c8                   add    %ecx,%eax
  400fdd:       d1 f8                   sar    %eax
  400fdf:       8d 0c 30                lea    (%rax,%rsi,1),%ecx
  400fe2:       39 f9                   cmp    %edi,%ecx
  400fe4:       7e 0c                   jle    400ff2 <func4+0x24>
  400fe6:       8d 51 ff                lea    -0x1(%rcx),%edx
  400fe9:       e8 e0 ff ff ff          call   400fce <func4>
  400fee:       01 c0                   add    %eax,%eax
  400ff0:       eb 15                   jmp    401007 <func4+0x39>
  400ff2:       b8 00 00 00 00          mov    $0x0,%eax
  400ff7:       39 f9                   cmp    %edi,%ecx
  400ff9:       7d 0c                   jge    401007 <func4+0x39>
  400ffb:       8d 71 01                lea    0x1(%rcx),%esi
  400ffe:       e8 cb ff ff ff          call   400fce <func4>
  401003:       8d 44 00 01             lea    0x1(%rax,%rax,1),%eax
  401007:       48 83 c4 08             add    $0x8,%rsp
  40100b:       c3                      ret

转化为类c语言,大概是:

eax=edx;         //第一次执行时为0xe
eax=eax-esi;
ecx=eax;
ecx=0;
eax=eax+ecx;
eax=eax/2;             //第一次执行时为0x7
ecx=eax+esi*1;
if(ecx<=edi)
{
  eax=0;
  if(ecx>=edi)
  return;
}
else
{   
    edx = rcx - 1;
    func4();
    eax = eax + eax;
    return;
}

我们知道,执行完后必须要让eax为0,根据第8行和第10行,可以让ecx小于等于edi,前面我们知道,edi被赋值为edx,也就是我们输入的第一个参数,ecx根据func4前面几行的执行可以知道等于0x7,那么edx必须小于等于7,如果不进入if分支是否就一定就不能让%eax等于0呢,也不是,进入else中,会发生递归,经过模拟,最后可以得出edx可以为7,3,1,0

所以最终密码就是

0 0/1 0/3 0/7 0

图24 第四关通关

6.关卡5——phase_5

图25 第五关汇编

push   %rbx
sub    $0x20,%rsp
mov    %rdi,%rbx
mov    %fs:0x28,%rax
mov    %rax,0x18(%rsp)   //此时rsp+0x18中存放的是0x28
xor    %eax,%eax
call   40131b <string_length>
cmp    $0x6,%eax
je     4010d2 <phase_5+112>
call   40143a <explode_bomb>

首先将输入值赋值给%rbx,然后开辟32字节的栈空间,接下来的四行代码可以翻译为:

rbx=rdi       //第一个输入值存入rbx
rax=%fs:0x28
rsp+0x18=rax    //将%fs:0x28放入栈中
eax=0    //与自身做异或操作,结果为0

下面调用函数:string_length,这个函数在第一关phase_1中的strings_not_equal出现过,从函数名猜测作用应该是返回字符串长度。接下来是比较%eax的值是否等于6,是就跳转112行,不是就爆炸。

因为调用过string_length函数,所以返回值%eax中存放的是字符串长度,所以我们可以得出输入的字符串长度必须是6

如果不爆炸跳转112行,我们来分析一下112行:

mov    $0x0,%eax
jmp    40108b <phase_5+41>

把%eax赋值为0,然后跳转41行,看41行:

movzbl (%rbx,%rax,1),%ecx
mov    %cl,(%rsp)
mov    (%rsp),%rdx
and    $0xf,%edx
movzbl 0x4024b0(%rdx),%edx
mov    %dl,0x10(%rsp,%rax,1)
add    $0x1,%rax
cmp    $0x6,%rax
jne    40108b <phase_5+41>

是一堆赋值语句,翻译成易懂语句:

if(rax=0;rax!=6;rax++)
{
   ecx=rbx+rax*1     //把输入的字符串每个字节的值依次传给ecx,其余位置0
   取出ecx低8位装入栈
   rdx=ecx低8位
   edx=edx与0xf      //0xf就是低4位都是1,前面都是0,操作之后作用相当于取出edx的低4位,也就是ecx的低4位
   edx=rdx+0x4024b0      将地址0x4024b0加上偏移量rdx组成一个新地址
   0x10+rsp+rax*1=dl     //将寄存器%edx的低8位存放到栈上
}

这里给了一个地址:0x4024b0,我们可以看一下存放了什么:

图26 0x4024b0存放的值

该部分代码的作用可以表示为:

图27 程序示意图

最后得到的就是地址0x4024b0处的6个字符这6个字符的16进制ASCII码值的低8位分别存放在:

rsp+0x10+rax(rax=0,1,2,3,4,5,6)。有什么别的目的,还要往下看:

movb   $0x0,0x16(%rsp)
mov    $0x40245e,%esi
lea    0x10(%rsp),%rdi
call   401338 <strings_not_equal>

翻译成易懂代码:

rsp+0x16=0
esi=0x40245e
rdi=rsp+0x10
调用string_not_equal

很明显,string_not_equal需要输入两个参数,一个是rdi,一个是esi,rdi处是rsp+0x10,这个地址我们前面知道存放的是之前提取的6个字符,esi中存放的是0x40245e处的值,我们查看一下此处放了什么:

图28 0x40245e处的值

那么输入的两个参数就分别是:之前提取的6个字符组成的字符串,"flyers"

然后比较这两个字符是否相同,有什么用呢,接着看:

test   %eax,%eax
je     4010d9 <phase_5+119>
call   40143a <explode_bomb>

判断返回值%eax是否为0,不是爆炸,是跳转119行,根据phase_1,我们知道当两个输入的参数不相等时返回1,相等时返回0,那么这两个参数必须相等才能不爆炸,我们看一下119行:

mov    0x18(%rsp),%rax
xor    %fs:0x28,%rax
je     4010ee <phase_5+140>

rax=rsp+0x18,然后rax与0x28异或,我们前面知道rsp+0x18处存放的就是0x28,此时赋给rax再与0x28异或,相当于把rax清0。

然后跳转140行:

add    $0x20,%rsp
pop    %rbx
ret

清场结束。

现在很明白了,要想过关,就必须让提取的6个字符的ASCII码值等于"flyers"的ASCII码值,那么我们根据什么提取的6个字符呢,是根据我们输入的字符串的16位ASCII值分别与0xf相与得出的值确定0x4024b0处字符串的下标。

那么我们在0x4024b0处的字符串中找到字符"f"、"l"、"y"、"e"、"r"、"s"的下标,分别是:

9、15、14、5、6、7 , 去查ASCII码表,找到6个 与 0xf相与分别为 9、15、14、5、6、7 的字符:

ionefg

正确答案得出!

因为有其他任务要做,暂时不写phase_6和隐藏关卡,有时间会补上!

参考文献:

[1]linux反汇编简单示例_锅锅是锅锅的博客-CSDN博客_linux 反汇编

[2]gdb反汇编详解C函数底层实现笔记_bindingfly的博客-CSDN博客

[3]使用gdb反汇编的方法_孟建行的博客-CSDN博客_gdb 反汇编

[4]GDB调试命令详解_小桥流水人家_的博客-CSDN博客_gdb调试

[5]《深入理解计算机系统》第三版

[6]C语言内存分区_wz_love999的博客-CSDN博客_c语言内存空间图

[7]x86-64常用指令总结_高阶近似的博客-CSDN博客

[8]CPU通用寄存器 eax ebx ecx edx esp ebp esi edi_015646的博客-CSDN博客_eax寄存器

[9]gdb x命令_南洋读书人的博客-CSDN博客_gdb x命令

[10]学 Win32 汇编[28] - 跳转指令: JMP、JECXZ、JA、JB、JG、JL、JE、JZ、JS、JC、JO、JP 等..._weixin_34329187的博客-CSDN博客

猜你喜欢

转载自blog.csdn.net/qq_53162179/article/details/128622580
今日推荐