二进制炸弹bomblab-第二关-详细解析

观前提示:本文并非速通指南,偏向于如何一步步思考与解题,也许会分析得有些拖沓。且本文为系列文章,一篇只分析一关的破解思路,前文提到的知识点后面不会赘述,分析过的类似的汇编代码段也不会再分析,除非是没见过的结构,所以越往后分析得越少。

本文共四千五百余字。

前言

​ 如果这些解析对你有帮助,那么我会非常荣幸和开心!

​ 如果我的解析存在错误,非常抱歉误导了你!请在评论区提出!我也是一个正在不断学习的学生。

​ 谢谢你!

正式破解

概览全局

反正就是设断点,运行,输入字符串这套丝滑连招。

来到这里:

先看全局,扫视一下汇编代码:

可以看到差不多都是老熟人,被操作的寄存器几乎还是那么几个,eax,ebx,esp……

只多了一点上一篇没介绍过的新命令和新寄存器,因为很少,所以干脆在这里介绍一下了,就不放在下面汇编代码分析的里面了。

新命令

xor 指令(exclusive OR):

  • 用于将一个寄存器或内存位置中的值与另一个寄存器或常数进行异或运算,并将结果存储回目标操作数中。

  • 异或操作的规则是:如果两个操作数的对应位不同,则结果为1,否则为0。

lea 指令(Load Effective Address):

  • 用于将一个内存地址计算的有效地址加载到指定的寄存器中,而不涉及实际的数据传输。虽然 LEA 指令的名称中包含了 “Load” 这个词,但它实际上并不执行数据加载操作,而只是执行地址计算

  • 它的使用场景之一是在进行一些复杂的内存寻址计算时,例如数组访问或数据结构的处理。它可以用来计算数组元素的地址,而不必实际加载数组元素的内容。

    • 例如:

    • ; 计算数组元素的地址并存储到寄存器 %eax
          leal array+2, %eax
      ; 此时 %eax 中存储的是数组元素 array[2] 的地址
      ; 可以继续在后续的代码中使用 %eax 来处理数组元素
      

cmpl指令(Compare Long):

  • “Long” 通常指的是双字(32位)整数类型。因此,cmpl 指令用于比较两个双字整数(32位整数)操作数的值。它执行源操作数减去目的操作数的减法,并不修改操作数的值只是影响标志位的状态。

  • 该指令执行完毕后,会根据比较结果设置以下标志位中的一些或全部:

    • Zero Flag (ZF)零标志位:如果两个操作数相等,ZF 被设置为 1,否则为 0。

    • Sign Flag (SF)符号标志位:如果结果为负数,SF 被设置为 1,否则为 0。

    • Overflow Flag (OF)溢出标志位:如果操作产生溢出,OF 被设置为 1,否则为 0。

    • 例如:

    • 	movl num1, %eax
          movl num2, %ebx
      
          cmpl %ebx, %eax    # 比较 num1 和 num2
      
          jg greater         # 如果 num1 大于 num2,跳转到 greater 标签
          jl lesser          # 如果 num1 小于 num2,跳转到 lesser 标签
          je equal           # 如果 num1 等于 num2,跳转到 equal 标签
      

jns指令(Jump if Not Sign):

  • 如果符号标志位(SF)为0(非负数),则执行跳转操作。

新寄存器

gs(Global Segment)全局段:

  • 是一个段寄存器,在x86体系结构中,用于指向全局段。全局段可以用于多任务环境中的任务隔离,每个任务有自己独立的内存空间。通过使用 %gs 寄存器,可以在不同的任务之间切换并访问各自的内存空间,从而实现任务间的隔离和保护。

  • 可以用于访问数组和其他数据结构。在一些特定的编程环境中,特别是多线程或多任务环境下,它可以被用来实现线程局部存储(Thread Local Storage,TLS),其中每个线程有自己的数据副本,比如线程私有的数组。

  • 函数开头的几句和结尾的几句有关栈保护,和解题无关,所以现在这里就不讲了,也许后期会讲一下?

新命令和新寄存器就是这些了。

了解了这些,再大概扫视一遍,可以发现又是熟悉的比较+跳转,中间调用了一次读取6个数字的函数,可以猜测又是遍历循环,再结合上面的一些指令,比如lea,大概能猜到是遍历6个数字组成的数组的同时进行一些比较操作

新的寻址方法

上一篇系列文章里说过:

  • 所以0x1c(%esp)表示以esp为基地址,加上偏移0x1c字节的内存单元
  • 这个叫基址+偏移寻址。比如从庄重文操场去西苑食堂的路有好多条,有近路有远路,你可以对别人说,先去光前体育馆大门口然后往东再走50米,就到西苑了。
  • 这种小括号写法是AT&T风格的,括号中可以有3个数据,基址寄存器base、索引寄存器index和比例因子scale。在此不做展开解释,如果后期有用到我们再来解释,减少负担。

现在我们就来解释,就直接以phase_2里的语句为例:

add    (%esp,%ebx,4),%eax

基址寄存器是esp,索引寄存器是ebx,比例因子是4,实际操作是:将寄存器 %ebx 乘以 4(即位运算中左移两位)得到偏移量,然后将 %esp 和偏移量相加,得到内存地址。然后从该内存地址读取值,将其与 %eax 中的值相加,并将结果存储回 %eax 寄存器。

最后,我们再看一下初始的寄存器里存了些什么:

按上一篇文章对main函数反汇编得到的代码的分析,eax应该还是我们这轮输进去的字符串(我验证过,懒得截图了,你可以自己试试)。暂时还不知道其他寄存器数据的意义。接下来就是一步步运行代码,查看堆栈里的数据内容,分析逻辑的事了。

分析汇编代码

以后都直接删去机器码了,也看不懂,还占注释空间。

08048b54 <phase_2>:
 8048b54:	push   %ebx
 8048b55:	sub    $0x30,%esp 				
 8048b58:	mov    %gs:0x14,%eax			
 8048b5e:	mov    %eax,0x24(%esp)			
 8048b62:	xor    %eax,%eax				
 8048b64:	lea    0xc(%esp),%eax
 8048b68:	push   %eax
 8048b69:	pushl  0x3c(%esp)			;以上是一些开辟空间和赋值的操作
 8048b6d:	call   80490a2 <read_six_numbers>

我随便输入的字符串仍然是“random”,然后想要运行到下面一步炸弹就炸了。问题出在“read_six_numbers”这个函数里面。带着找出为什么爆炸的目的,我们扫视一下这个函数的代码:

blob

可以看到:

 80490cf:	   cmp    $0x5,%eax
 80490d2:	   jg     80490d9 <read_six_numbers+0x37>
 80490d4:	   call   804907d <explode_bomb>

如果执行倒数第三行就会引爆炸弹 —> 倒推 —>当eax里存放的值不大于(greater)5就不跳过炸弹引爆函数,直接引爆 —> 倒推 —> 发现eax只有一开始的赋值,后来就没改变过 —> 能改变eax的只有一个可能:

 80490c7:	e8 44 f7 ff ff       	call   8048810 <__isoc99_sscanf@plt>

是这个函数里面改变了eax的值。难道又要再深入一层去查看这个函数的代码吗?

不,不需要。

因为sscanf是 C语言标准库中的一个函数,不是设炸弹的人自己编写的函数,它类似于 scanf。而isoc99是指 ISO/IEC 9899:1999,它代表了 C 编程语言的第 2 个国际标准,通常称为 C99。ptlProcedure Linkage Table(过程链接表)的缩写,它是在共享库中用于实现函数调用的一种机制,它允许函数调用的地址解析延迟进行,以提高程序的性能和资源利用率。

我们可以直接去网上找说明文档(下面随便找了一个):

blob

参数:

  • 数据源 参数:C 字符串,函数以它作为源来提取数据。

  • 格式 参数:C 字符串,包含一个格式字符串,遵循与 scanf 中的格式相同的规范。

  • 附加 参数:根据格式字符串的不同,函数可能期望附加参数序列,每个参数都包含一个指针,指向已分配存储空间,用于存储所提取字符的解释和相应类型。

    (这些参数的数量至少应与格式说明符存储的值的数量相同。函数会忽略额外的参数)

返回值:

  • 成功时,函数返回成功填充的参数列表中的参数个数。这个计数可能与期望的项目数匹配,或者在匹配失败的情况下少于(甚至为零)。
  • 在尚未成功解析任何数据之前出现输入失败的情况下,返回 EOF

让我们来看一个具体的例子:

#include <stdio.h>

int main() {
    char input[] = "2023-08-21 15:30"; // 包含日期和时间信息的字符串
    int year, month, day, hour, minute;

    // 使用 sscanf 从字符串中提取数据
    sscanf(input, "%d-%d-%d %d:%d", &year, &month, &day, &hour, &minute);

    // 输出提取出的数据
    printf("Year: %d\n", year);
    printf("Month: %d\n", month);
    printf("Day: %d\n", day);
    printf("Hour: %d\n", hour);
    printf("Minute: %d\n", minute);

    return 0;
}

在这个例子中,我们使用了 sscanf 函数:

  • 参数:input 是包含日期和时间信息的字符串,作为 sscanf 的输入源。
  • 格式 参数:"%d-%d-%d %d:%d" 是格式字符串,告诉 sscanf 如何解析输入数据。%d 表示解析整数。
  • 附加 参数:在这里,我们有五个参数,分别是 &year&month&day&hour&minute。这些参数都是整数变量的地址,用于存储解析出的数据。
  • 返回值:与前面的例子类似,sscanf 函数返回成功解析的项目数。在这里,它应该返回 5,因为我们解析了年、月、日、小时和分钟五个项目。

所以,当我们运行这个程序时,它会从字符串 “2023-08-21 15:30” 中提取出年、月、日、小时和分钟,并将这些数据输出到屏幕上。

那说回我们的程序,编写炸弹程序的人在read_six_numbers函数调用__isoc99_sscanf函数时,会设定什么样的参数呢?我们看一下内存里存的东西就可以知道了。

看哪里的数据?这些参数会存在哪里?非常明显,一定是靠近sscanf函数的地方,就像sscanf(input, "%d-%d-%d %d:%d", &year, &month, &day, &hour, &minute);这个C语言例子,毕竟我们的汇编代码是从C语言对应的可执行文件反汇编来的。

果然:

blob

到这里就可以推断出read_six_numbers函数的意图了:从我们输入的字符串里读取不少于5个int数字,如果少于/等于5,直接引爆炸弹。其实结合函数名,可以知道应该是需要恰好6个数字,不过这个条件没有体现在这个函数内部,可能是在phase_2函数里进行限制。

到这里可以还原出这个c程序的一种可能的形式(不太可能做到从反汇编代码百分百还原出C源代码):

int read_six_numbers(char *input) {
    int numbers[6];
	int num = sscanf(input, "%d %d %d %d %d %d", &numbers[0], &numbers[1], &numbers[2], &numbers[3], &numbers[4], &numbers[5]);
    if (num <= 5) {
        explode_bomb();
    }
    return num;
}

read_six_numbers函数到这里已经分析得很清楚了,不过现在还不知道需要什么规律的数字。为了避免爆炸,我直接在即将执行cmp $0x5,%eax的时候修改eax的值为6,我的输入还是保持“random”,确实成功了,没有触发引爆炸弹的函数。

但是接下来我们回到phase_2函数,查看内存会发现是乱七八糟的东西,毕竟我刚刚是使用歪门邪道直接修改eax的值,其实sscanf没有解析出数据,更不可能在内存里找到相关数据了:

blob

于是还是选择了重新输入字符串,把我的“random”随便改成“1 2 3 4 5 6”。然后就正常了:

blob

接下来的代码我不再分析。因为其实和上一篇文章的string_not_equal函数原理几乎一模一样,我已经授之以渔了。

我就直接来还原整体的C代码了。

还原成C代码

int read_six_numbers(char *input,int *numbers) {
	int num = sscanf(input, "%d %d %d %d %d %d", &numbers[0], &numbers[1], &numbers[2], &numbers[3], &numbers[4], &numbers[5]);
    if (num <= 5) {
        explode_bomb();
    }
    return num;
}

void phase_2(char *input){
    int numbers[6];
    int result;
    int num = read_six_numbers(input,numbers);
    if(numbers[0] <= 0){
        explode_bomb();
    }
    else{
        for(int i = 1;i < 6;i++){
            result = i ;
            result += numbers[i-1];
            if(numbers[i] != result){
                explode_bomb();
            }
        }
    }
    return;
}

关于答案的讨论

正常通关的情况下,这个关卡的答案范围是有上限、有下限的。但是却有无数种答案。

前6个答案数字的上限不得超过int整数的范围,下限是第一个数字必须是非负数。

我们分析了整个代码并没有看到对输入参数个数的限制,且多于6个的参数会被sscanf函数直接忽略,也就是说撇开前六个作为正确答案的部分,后面还可以随便输入东西,不会造成影响。验证一下:

blob

的确如此。

都说了正常情况,那肯定有不正常情况。如果第一个答案数字大于等于int正整数的最大值2147483647,eax是32位的寄存器,读取这个地址的数值之后,eax最大只会截取到2147483647[1],然后+1后就会溢出成最小值-2147483648。经过设计,答案可以是这样的(提一嘴,如果数据超过10位数程序会报错):

blob

[1]

在32位系统中,有符号整数使用补码表示,其中最高位为符号位。

最大的32位有符号整数是2147483647,二进制表示为:

0111 1111    1111 1111    1111 1111    1111 1111

如果你在这个基础上再加1,就会发生溢出。在补码表示中,负数的二进制数值是通过将其对应正数的二进制按位取反再加1得到的。当最大的32位有符号整数加1时,其二进制表示将变为:

1000 0000    0000 0000    0000 0000    0000 0000

这代表的是-2147483648,即负的int的最小值。在32位有符号整数的表示范围内,从最大正数加1变为最小负数就是一个溢出。

后记

感谢你看到这里!

一些参考过的文章(这些文章写得很好,虽然这个知识没有写在文章里,也没有用到,但是还是想提一下):

  • 为什么需要对齐?
    • 因为如果按数据本身占多少内存就直接存下来而没有对齐,32位系统一次读32位,可能就会读取到某个数据中间,读不下了就断了,然后需要再读32位,再把2次读取的数据拼在一起,过滤掉没有用的信息,才能得到你想读取的准确数据。效率很慢。如果对齐了,就可以保证读取的数据是一份一份的。
  • 32位linux系统中对齐策略是怎么样的?
    • 字节对齐: 这是最基本的对齐策略,确保每个数据项的起始地址都是其大小的整数倍。例如,一个 4 字节的整数会在地址上对齐到 4 的倍数。
    • 数据类型对齐: 不同数据类型可能有不同的对齐要求。例如,在 x86 架构下,32 位整数需要 4 字节对齐,16 位的短整数需要 2 字节对齐。结构体的对齐通常是其成员中最大对齐要求的倍数。
    • 结构体对齐: 结构体的对齐是根据结构体的成员中最大对齐要求的倍数来进行的。这确保了结构体中的每个成员都能够正确对齐。
    • 伪对齐: 在某些情况下,可以通过取消对齐来节省内存,但会牺牲访问速度。这种方式可能会使得数据成员在内存中不会进行任何填充或对齐。

Purpose of memory alignment(内存对齐的目的)

linux中结构体对齐

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值