二进制炸弹实验报告

时间:2022-08-02 20:59:46

接触《深入理解计算机系统》这本书很久了,但一直都处于看的阶段,但懂是一回事,做出来又是另一回事。

于是就试着做了几个实验,发现这个二进制炸弹特别好玩,于是乎就有了此文。


知识储备

调试:

停在地址处:   break  *0x8012345

打印寄存器:   print   $eax

显示所有寄存器值:I reg

检测内存值:x  /nfu addr  

                        x 检测内存值, n 查看几个内存单元 f进制显示 u显示单字节双字节等。


寄存器:

eiz: 伪寄存器,值始终为0

为什么要用:占位 NOP 可以用来占位,但只是一个字节,长指令比短指令执行快,所以选用其占位。同样的占位指令还有: lea 0x0(%edi) %edi

 

汇编:

lea 指令:LEA src, dst=>要求src必须为粗出去地址。而寻址方式多种多样。


重要知识点:

switch语句的实现:跳转表

 

工具:

gdb 调试

objdump –t 查看符号表,即所使用的所有符号。

objdump –d 反汇编

strings 显示程序所用的全部字符串


拆解过程

main函数:

 8048a52:	e8  a5 07 00 00       	call    80491fc <read_line>
8048a57: 83 c4 f4 add $0xfffffff4,%esp
8048a5a: 50 push %eax
8048a5b: e8 c0 00 00 00 call 8048b20 <phase_1>

通过调用库函数readline读入一行数据,字符串地址通过eax返回,然后将字符串传给phase_1函数。

同样,其后几个阶段遵循这种处理方式:读取一行字符串,调用phase_n,传入字符串参数。


阶段一:

阶段1比较简单,判断输入的字符串与程序内部某个字符串是否相当,不等即爆炸。

 8048b2c:	68 c0 97 04 08      push   $0x80497c0
8048b31: 50 push %eax
8048b32: e8 f9 04 00 00 call 8049030 <strings_not_equal>

所以解题的关键在于,找到程序中的字符串,使用gdb进行调试,打印地址0x80497c0的字符串。

二进制炸弹实验报告

答案显然:Public speaking is very easy.


阶段二:

phase2:
8048b56: 8d 45 e8 lea -0x18(%ebp),%eax
8048b59: 50 push %eax
8048b5a: 52 push %edx
8048b5b: e8 78 04 00 00 call 8048fd8 <read_six_numbers>
8048b60: 83 c4 10 add $0x10,%esp
8048b63: 83 7d e8 01 cmpl $0x1,-0x18(%ebp)
8048b67: 74 05 je 8048b6e <phase_2+0x26>
8048b69: e8 8e 09 00 00 call 80494fc <explode_bomb>
8048b6e: bb 01 00 00 00 mov $0x1,%ebx
8048b73: 8d 75 e8 lea -0x18(%ebp),%esi //esi = array
8048b76: 8d 43 01 lea 0x1(%ebx),%eax //eax = ebx + 1
8048b79: 0f af 44 9e fc imul -0x4(%esi,%ebx,4),%eax //eax = esi + ebx * 4 - 0x4
8048b7e: 39 04 9e cmp %eax,(%esi,%ebx,4) //eax == esi + ebx * 4
8048b81: 74 05 je 8048b88 <phase_2+0x40>
8048b83: e8 74 09 00 00 call 80494fc <explode_bomb>
8048b88: 43 inc %ebx
8048b89: 83 fb 05 cmp $0x5,%ebx
8048b8c: 7e e8 jle 8048b76 <phase_2+0x2e>

1. 输入六个数,通过read_six_sumbers将字符串拆分成6个数,存入 ebp -0x18的位置上。

2. cmpl  $0x1,-0x18(%ebp) 如果数组第一个元素不是1,则爆炸。即第一个数要为数字1.

3. 8048b76-8048b8c为一个循环。伪码如下:

  if (arr[0] != 1)  explode_bomb();
for (int i = 1; i <= 5; i++) {
if ( (i+1) * arr[i-1] != arr[i])
explode_bomb();
}

答案显然:1 26 24 120 720


阶段三:

第三阶段,有些繁琐。

内部含有sscanf函数,猜想应该是格式字符串。

sscanf传入的参数为:ebp-0x4,ebp-0x5, ebp-0xc地址;格式字符串地址;输入的字符参数地址。

ebp-0xc存第一个数, ebp-0x5存第二数,ebp-0x4第三个数。

二进制炸弹实验报告

因而,输入数据为:整数, 字符,整数。

 8048bc9:	83 7d f4 07          	cmpl   $0x7,-0xc(%ebp)
8048bcd: 0f 87 b5 00 00 00 ja 8048c88 <phase_3+0xf0>
8048bd3: 8b 45 f4 mov -0xc(%ebp),%eax
8048bd6: ff 24 85 e8 97 04 08 jmp *0x80497e8(,%eax,4)

输入的第一个数要小于等于7,而跳转到0x80497e8这个地址(汇编代码中没有显示这个地址),而同时,代码中又都可以跳转到<phase_3+0xf7>这个位置,猜想是不是switch语句,而要有switch,就需要查找跳转表。

二进制炸弹实验报告

可见,全部为phase_3内的地址,所以,验证为switch语句。

 既然要求第一个数要小于等于7,那么使用最简单0进行测试。

 8048bd3:	8b 45 f4             	mov    -0xc(%ebp),%eax
8048bd6: ff 24 85 e8 97 04 08 jmp *0x80497e8(,%eax,4)

第一个数为0,则eax 为0,则直接跳转到 (0x80497e8) 指向的位置。0x80497eb存储的内容为:

由上图可知:0x8048be0

8048be0:        b3 71                   mov    $0x71,%bl
8048be2: 81 7d fc 09 03 00 00 cmpl $0x309,-0x4(%ebp)
8048be9: 0f 84 a0 00 00 00 je 8048c8f <phase_3+0xf7>
8048bef: e8 08 09 00 00 call 80494fc <explode_bomb>

ebp-0x4存储的为第三个数,大小为0x309, 转10进制为:777。 同时,设置了bl 为 0x71。并跳转到0x8048c8f。

 8048c8f:	3a 5d fb             	cmp    -0x5(%ebp),%bl
8048c92: 74 05 je 8048c99 <phase_3+0x101>
8048c94: e8 63 08 00 00 call 80494fc <explode_bomb>

字符串值为0x71,换算成字符为 ‘q’。

所以答案为: 0 q 777。 当然,答案不为一,根据第一个参数的选择不同而不同。


阶段四:

 8048cec:	8d 45 fc            lea    -0x4(%ebp),%eax
8048cef: 50 push %eax
8048cf0: 68 08 98 04 08 push $0x8049808
8048cf5: 52 push %edx
8048cf6: e8 65 fb ff ff call 8048860 <sscanf@plt>
又是一个sscanf,查看0x8049808字符串内容。

二进制炸弹实验报告

可见,输入的是一个整数。

 8048d11:	8b 45 fc            mov    -0x4(%ebp),%eax
8048d14: 50 push %eax
8048d15: e8 86 ff ff ff call 8048ca0 <func4>
8048d1a: 83 c4 10 add $0x10,%esp
8048d1d: 83 f8 37 cmp $0x37,%eax
8048d20: 74 05 je 8048d27 <phase_4+0x47>

将输入的数据传入func4函数中,如果返回值为0x37, 则成功,所以,关键要看func4,推出输入的参数。

8048ca8:	8b 5d 08             mov    0x8(%ebp),%ebx
8048cab: 83 fb 01 cmp $0x1,%ebx //与1比较
8048cae: 7e 20 jle 8048cd0 <func4+0x30> //小于等于 1, 返回1
8048cb0: 83 c4 f4 add $0xfffffff4,%esp
8048cb3: 8d 43 ff lea -0x1(%ebx),%eax //参数-1, 调用func
8048cb6: 50 push %eax
8048cb7: e8 e4 ff ff ff call 8048ca0 <func4>
8048cbc: 89 c6 mov %eax,%esi //返回值保存在esi中
8048cbe: 83 c4 f4 add $0xfffffff4,%esp
8048cc1: 8d 43 fe lea -0x2(%ebx),%eax //参数-2, 调用func
8048cc4: 50 push %eax
8048cc5: e8 d6 ff ff ff call 8048ca0 <func4>
8048cca: 01 f0 add %esi,%eax //两次返回值相加,的返回结果
8048ccc: eb 07 jmp 8048cd5 <func4+0x35>
8048cce: 89 f6 mov %esi,%esi
8048cd0: b8 01 00 00 00 mov $0x1,%eax

存在自身调用自身的情况,应该是一个递归。代码应如下结构:

inf func4(int val) {
if (val <= 1)
return 1;
else
return func4(val-1) + func4(val-2);

}

val为何值,返回结果为0x37, 10进制的55. 最简单,写个程序暴力破解一下,值9.


阶段五:

 8048d3b:	e8 d8 02 00 00       	 call   8049018 <string_length>
8048d40: 83 c4 10 add $0x10,%esp
8048d43: 83 f8 06 cmp $0x6,%eax

可见输入的字符串,长度必须为6.

一个循环:

 8048d4d:	31 d2                xor    %edx,%edx
8048d4f: 8d 4d f8 lea -0x8(%ebp),%ecx
8048d52: be 20 b2 04 08 mov $0x804b220,%esi
8048d57: 8a 04 1a mov (%edx,%ebx,1),%al //依次取用户输入串中字符
8048d5a: 24 0f and $0xf,%al //只保留最后四位,其它位清零
8048d5c: 0f be c0 movsbl %al,%eax //带符号扩展送于eax中
8048d5f: 8a 04 30 mov (%eax,%esi,1),%al //取模式串第patt[eax]的字符
8048d62: 88 04 0a mov %al,(%edx,%ecx,1) //存入另一数组中,ebp-0x8
8048d65: 42 inc %edx
8048d66: 83 fa 05 cmp $0x5,%edx
8048d69: 7e ec jle 8048d57 <phase_5+0x2b>
ebx存放用户输入字符串首地址,而0x804b220肯定也为一个字符串,打印出来,将该字符串称为 模式串

二进制炸弹实验报告

整体思路就是,用户输入的字符串,取低四位,值作为索引,找到模式串中该索引对应的字符,放于一个新数组中。

8048d72:	68 0b 98 04 08       	push   $0x804980b
8048d77: 8d 45 f8 lea -0x8(%ebp),%eax
8048d7a: 50 push %eax
8048d7b: e8 b0 02 00 00 call 8049030 <strings_not_equal>

用新数组中字符与0x804980b字符比较,相等即破译。

二进制炸弹实验报告

所以,在模式串中的下标应为:15 0 5 11 13 1

而,要将这些字符换算成可打印的字符,然后输入。每个字符8位,最低4位不能动,所以,在高四位可以任意加1.

转变为:79, 80, 69, 75, 77, 65 对应字符:OPEKMA


阶段六:

首先,阶段六太不容易解决啦,整整花了7-8个小时。关键点:链表。


1. 读6个数

 8048dae:        8d45 e8             lea    -0x18(%ebp),%eax
8048db1: 50 push %eax
8048db2: 52 push %edx
8048db3: e820 02 00 00 call 8048fd8<read_six_numbers>
读取6个数,存放在ebp-18的地方,所以,ebp-18应该是一个数组首地址。

2. 一个大循环,检测输入数据是否满足要求

 8048db8:        31 ff               xor    %edi,%edi            //清零,控制变量i
8048dba: 83c4 10 add $0x10,%esp
8048dbd: 8d76 00 lea 0x0(%esi),%esi
8048dc0: 8d45 e8 lea -0x18(%ebp),%eax //取数组首地址
8048dc3: 8b04 b8 mov (%eax,%edi,4),%eax //取arr[i] –》 val
8048dc6: 48 dec %eax // val--
8048dc7: 83f8 05 cmp $0x5,%eax //必须小于等于5,即原小于等于6
8048dca: 7605 jbe 8048dd1 <phase_6+0x39>
8048dcc: e82b 07 00 00 call 80494fc<explode_bomb>
8048dd1: 8d5f 01 lea 0x1(%edi),%ebx // ebx = edi + 1, j = I + 1
8048dd4: 83fb 05 cmp $0x5,%ebx // 要小于等于5
8048dd7: 7f23 jg 8048dfc <phase_6+0x64>
8048dd9: 8d04 bd 00 00 00 00 lea 0x0(,%edi,4),%eax //eax = 4 * edi 用户获取元素地址
8048de0: 8945 c8 mov %eax,-0x38(%ebp) //暂存
8048de3: 8d75 e8 lea -0x18(%ebp),%esi //取arr首地址
8048de6: 8b55 c8 mov -0x38(%ebp),%edx
8048de9: 8b04 32 mov (%edx,%esi,1),%eax //获取元素arr[i]
8048dec: 3b04 9e cmp (%esi,%ebx,4),%eax //比较arr[j] arr[i]
8048def: 7505 jne 8048df6 <phase_6+0x5e> // 不能相同
8048df1: e806 07 00 00 call 80494fc<explode_bomb>
8048df6: 43 inc %ebx //j++
8048df7: 83fb 05 cmp $0x5,%ebx
8048dfa: 7eea jle 8048de6 <phase_6+0x4e>
8048dfc: 47 inc %edi //i++
8048dfd: 83ff 05 cmp $0x5,%edi
8048e00: 7ebe jle 8048dc0 <phase_6+0x28>

伪代码如下:

for (int i =0; i<=5; i++){
if (arr[i]>= 6)
explode_bomb();
for (int j = i+1; j<=5; j++){
if (arr[i]== arr[j])
explode_bomb();
}
}

检测元素,即每个元素必须不同。同时,必须都是大于等于0的,因为cmp    $0x5,%eax都是按照无符号数进行处理的,如果为负数,则换算后为一个大数,因此,不能出现这样的情况。所以,推断处理数据因为:1 2 3 4 5 6,至于排列顺序,目前未知。

 

3. 最关键的一段代码

 8048e02:        31 ff               xor    %edi,%edi                  // i清零
8048e04: 8d4d e8 lea -0x18(%ebp),%ecx //数组首地址arr
8048e07: 8d45 d0 lea -0x30(%ebp),%eax //ebp-0x30的地址
8048e0a: 8945 c4 mov %eax,-0x3c(%ebp) //存放于ebp-0x3c中
8048e0d: 8d76 00 lea 0x0(%esi),%esi //无用代码,对齐用
8048e10: 8b75 cc mov -0x34(%ebp),%esi //取ebp-34中值
8048e13: bb01 00 00 00 mov $0x1,%ebx // j = 1
8048e18: 8d04 bd 00 00 00 00 lea 0x0(,%edi,4),%eax //eax = edi * 4
8048e1f: 89c2 mov %eax,%edx
8048e21: 3b1c 08 cmp (%eax,%ecx,1),%ebx //arr[i] 与 j比较
8048e24: 7d12 jge 8048e38 <phase_6+0xa0> //小于的话,进入循环
8048e26: 8b04 0a mov (%edx,%ecx,1),%eax //eax = arr[i]
8048e29: 8db4 26 00 00 00 00 lea 0x0(%esi,%eiz,1),%esi //对齐,占位,无用
8048e30: 8b76 08 mov 0x8(%esi),%esi //esi+0x8的值给esi
8048e33: 43 inc %ebx //j++
8048e34: 39c3 cmp %eax,%ebx //arr[i] == j 停止循环
8048e36: 7cf8 jl 8048e30 <phase_6+0x98>
8048e38: 8b55 c4 mov -0x3c(%ebp),%edx //获得ebp-0x30地址
8048e3b: 8934 ba mov %esi,(%edx,%edi,4) //esi存入新数组中
8048e3e: 47 inc %edi
8048e3f: 83ff 05 cmp $0x5,%edi
8048e42: 7ecc jle 8048e10 <phase_6+0x78>

首先,在进入函数后,一句重要代码:

8048da4: c745 cc 6c b2 04 08        movl   $0x804b26c,-0x34(%ebp)

又是一个地址,就以为是一个字符串,但尝试打印的时候出现问题:

二进制炸弹实验报告

显示一个<node1>,难道是一个结构体。而8048e30:     8b 76 08   mov   0x8(%esi),%esi,esi值为0x804b26c,如果为结构体,

这明显是一个结构体的操作,相当于一个next指针。

打印出来试试:

二进制炸弹实验报告

可见,就是一个链表,首尾相接。

 伪代码如下:

for (int i =0; i<=5; i++){
for (int j =0; arr[i]> j; j++){
node = node.next;
}
ptr[i]= node;
}

比如输入 3 1 2 4,这样,就让链表第三个元素排到第一位置,链表第1位置元素排到第二,链表第二元素排到第三,……

 

4. 重新串接链表

 8048e44:        8b 75 d0            mov    -0x30(%ebp),%esi    //取ptr地址
8048e47: 8975 cc mov %esi,-0x34(%ebp) //存于ebp – 0x34中

8048e4a: bf01 00 00 00 mov $0x1,%edi // i = 1
8048e4f: 8d55 d0 lea -0x30(%ebp),%edx //ebp-0x30地址
8048e52: 8b04 ba mov (%edx,%edi,4),%eax //ptr[i] => eax
8048e55: 8946 08 mov %eax,0x8(%esi) //ptr[i-1]->next = ptr[i]
8048e58: 89c6 mov %eax,%esi //esi = ptr[i]
8048e5a: 47 inc %edi
8048e5b: 83ff 05 cmp $0x5,%edi
8048e5e: 7ef2 jle 8048e52 <phase_6+0xba>


8048e60: c746 08 00 00 00 00 movl $0x0,0x8(%esi)
8048e67: 8b75 cc mov -0x34(%ebp),%esi //最后一个结点next 赋0

5.检测链表值

 8048e6a:        31 ff               xor    %edi,%edi                   // i = 0
8048e6c: 8d74 26 00 lea 0x0(%esi,%eiz,1),%esi //无用代码
8048e70: 8b56 08 mov 0x8(%esi),%edx //esi+8 的值为下一个链表地址
8048e73: 8b06 mov (%esi),%eax //取当前链表第一个元素值
8048e75: 3b02 cmp (%edx),%eax //与下一个链表第一个元素值比较
8048e77: 7d05 jge 8048e7e <phase_6+0xe6> //当前大于下一个,则成功
8048e79: e87e 06 00 00 call 80494fc<explode_bomb>
8048e7e: 8b76 08 mov 0x8(%esi),%esi
8048e81: 47 inc %edi
8048e82: 83ff 04 cmp $0x4,%edi
8048e85: 7ee9 jle 8048e70 <phase_6+0xd8>
总结:也就是说,要让第一个元素的值从大到小排列。初始链表元素第一个值从第一到第五分别为:

fd 2d5  12d   3e5  d4   1b0

让其从大到小排列:3e5 2d5 1b0 12d fd d4

对应输入值:4 2 6 3 1 5


附上答案:

Public speaking is very easy.
1 2 6 24 120 720
0 q 777
9
OPEKMA
4 2 6 3 1 5


啊,终于成功拆解了!!!!!