CSAPP实验-二进制炸弹writeup

0x01 题目概述

二进制炸弹是《深入理解计算机系统》的一个课程实验。

给定一个二进制文件bomb,及其主程序bomb.c文件,运行二进制文件bomb,一共有6关,用户需要通过6个输入来避免炸弹的爆炸。

我们需要通过对二进制文件进行逆向分析,得到能避开炸弹爆炸的合理的输入。

0x02 涉及知识点

汇编语言的基础,逆向分析工具的使用。

0x03 实验环境

Ubuntu16.04LTS,IDA Pro 7.0,gdb

0x04 解题思路

phase_1

  1. 首先读取了用户输入的第一个字符串,保存到rax寄存器中,并通过”mov rdi, rax”将字符串的值赋值给rdi寄存器,作为后面调用函数phase_1()的第一个参数.

在这里插入图片描述

  1. 用gdb运行bomb: gdb bomb

  2. 为了解出第一个字符串,在phase_1()函数处打断点: b phase_1, 然后运行程序 r. 接下来随便输入一个字符串用于调试, 比如字符串”da”;然后此时进入了phase_1()函数,输入disass查看其汇编代码:

在这里插入图片描述

  1. 从<strings_not_equal>的名字可知,该函数用于比较两字符串的值,需要两个字符串作为输入. 两个字符串不相等的话则返回1,相等返回0,结果保存在eax中. “test eax,eax”用于检查eax寄存器的值是否为0:如果eax为0,则由于zf标志位为0,因此执行 “je 0x400ef7 <phase_1+23>”,跳过了调用<explode_bomb>的指令代码.

因此, 在这里需要输入的字符串需要与代码中用于比较的字符串相同.

  1. 观察phase_1()函数的汇编代码,在调用<strings_not_equal>之前事先通过指令”mov esi,0x402400”, 将内存地址为”0x402400”的值赋值给了esi寄存器,作为函数<strings_not_equal>的参数.

  2. 查看内存地址为”0x402400”的值:

在这里插入图片描述

  1. 进行测试, 解决phase_1:

在这里插入图片描述

Answer1: Border relations with Canada have never been better.


string_length解析

  1. 比较rdi存放的字符串地址的内容是不是’\0’, 是的话则跳转到0x401332: 将eax赋值为0; 否则的话:

  2. 把rdi 也就是用户的输入字符串赋值给rdx, 然后将寄存器rdx中存放的字符串地址+1, 原来是0x6037800,对应的字符串内容为”da”(也就是我一开始输入的测试值),现在变成了0x6037801,对应的字符串内容为”a”.

  3. “mov eax, edx” 和 “mov eax, rdx”一样,不过rdx是64位的寄存器

  4. “sub eax, edi” 也就是将eax(地址加了1之后的rdx, 即地址加了1之后的rdi)减去edi, 结果为1并赋值给eax

  5. 比较此时rdx对应的是不是’\0’,是的话结束循环,否则继续循环; 下一次循环中eax就会为2,以此类推

phase_2

  1. 使用gdb进行动态调试, 由phase_2调用的函数<read_six_numbers>可知需要用户输入6个数字. 因此, 先随便输入6个数字, 比如:”1 2 3 4 5 6”.

执行到下图的位置时发现有个内存地址, 打印出其中的内容发现是scanf的格式字符串.

可以知道, 需要输入6个数字, 并且是以空格分隔开的:

在这里插入图片描述

  1. 执行完scanf函数以后会有一个判断, 只要输入按要求来就不会引爆炸弹. 这里eax是读取的数据个数, 如果比5大,则跳转到地址0x401499, 即不执行<explode_bomb>函数.

在这里插入图片描述

  1. 继续执行phase_2()函数. rsp为phase_2() 函数的栈顶指针, 由函数调用栈可知, 指向的是第一个参数.

在这里插入图片描述

在这里插入图片描述

因此, [rsp] 至 [rsp+0x14] 就是用户传入的数字参数.

一个数字为int=4字节. 0-3为第一个参数, 4-7为第二个参数, 8-11为第三个参数,12-15为第四个参数, 16-20为第五个参数,21-23为第六个参数

由于我输入的是”1 2 3 4 5 6”, 画出来的参数在函数栈中的表示如下:

地址以及存储的值
rbp rsp+24
rsp+20 6
rsp+16 5
rsp+12 4
rsp+8 3
rbx rsp+4 2
rsp 1
  1. 接下来在IDA中查看会比较清晰.

rbx-0x4的位置就是rsp的位置,也就是第一个参数的位置; 将[rbx-0x4]指向的数值赋值给了eax后另eax乘以2, 再与第二个参数的值,也就是[rbx]进行比较, 只有相等才不会触发函数<explode_bomb>. 因此, 第二个参数的值是第一个的两倍.

在这里插入图片描述

在该次判断中, 由于rbx+4以后变成rsp+8, 显然不等于rbp=rsp+24,因此跳到loc_400F17处. 由于此时rbx为rsp+8, rbx-4为rsp+4, 因此第三个数为第二个数的两倍.

以此类推即可知,每个数都是前一个数的两倍,这是一个等比数列.

随便输入一个等比数列, phase_2解决:

在这里插入图片描述

Answer2(答案不唯一): 1 2 4 8 16 32

phase_3

  1. 同样的方法打断点, 进行调试.
    可以看到, 输入应该为两个数字, 并且以空格隔开:

在这里插入图片描述

  1. 在phase_3()函数中, 先将两个参数的值分别赋值给寄存器rdx和rcx, 然后调用scanf函数.

  2. 接下来需要注意的地方是, 在箭头处, 先将 [rsp+18h+var_10] 处的值, 也就是rdx处的值, 即参数1与7比较. 如果比7大, 则跳转到地址0x400FAD处, 从地址0x400FAD处的代码可以看到走这个分支的话必然引起炸弹爆炸, 因此第一个参数的值必然小于或等于7.

在这里插入图片描述

地址0x400FAD处的代码:

在这里插入图片描述

  1. 接下来, 将[rsp+18h+var_10] 处的值, 即参数1的值赋值给eax, 并跳转到 “jmp QWORD PTR [rax*8+0x402470] ”, 先假设输入的参数1值为2, 那么跳转的地址就是0x402480, 接下来是跳转到0x400F83处:

在这里插入图片描述

对eax赋值为 0x2C3, 跳转到0x400FBE处

在这里插入图片描述

可以看出,参数2的值必须等于eax的值

在这里插入图片描述

可以观察到, 对于参数1的不同取值, 参数2的值也应该与程序中最终对eax赋的值相同才行.

因此, phase_3 的答案可以测试小于7非负数作为参数1, 并走通程序来判断参数2的值.

这里选择输入数据为: 2 707 , 其中 707 即 0x2C3 可以看到测试通过

在这里插入图片描述

phase_4

  1. 同样的方法打断点, 进行调试. 和phase_3一样, 输入也为两个数字, 空格隔开

在这里插入图片描述

  1. 可以看到, 参数1必须 ≤ 14才不会引爆炸弹

在这里插入图片描述

  1. 再往下看, 先将参数1的值保存在寄存器edi中.

然后phase_4()函数先调用了func4()函数, 然后判断func4()函数的返回值(保存在寄存器eax中), 如果eax不是0的话, 则会跳转到引爆炸弹的地方. 因此函数func4()返回值eax必须是0.

接下来则判断第二个参数是否为0, 如果不是0的话则引爆炸弹, 因此第二个参数已经可以确定为0.

在这里插入图片描述

  1. 接下来要做的就是分析func4()函数

在这里插入图片描述

框框框住的为两个关键的条件判断. ecx<=edi则跳转, 以及ecx>=edi则跳转.
接下来有两种方法

a) 第一种是将汇编代码写成高级语言,直接执行. 因为参数1的范围已知. 最终结果为, 参数1的取值可以有:0, 1, 3, 7


# -*- coding:utf-8 -*-

edx = 14
esi = 0
edi = 8  # param 1
eax = 0
ecx = 0


def func4():
    global edx, esi, edi, eax, ecx
    eax = edx
    eax = eax - esi
    ecx = eax
    ecx = ecx >> 31
    eax = eax + ecx
    eax = eax >> 1
    ecx = eax + esi 

    if ecx <= edi:
        eax = 0
        if ecx >= edi:  # ecx == edi
            return eax
        else:           # ecx < edi
            esi = ecx + 1
            func4()
            eax = eax*2 + 1
            return eax
    else:               # ecx > edi
        edx = ecx - 1
        func4()
        eax = eax*2
        return eax


if __name__ == "__main__":
    # for edi in range(7, 15):
    edi = 7
    res = func4()
    print(edi, ": ", res)


b) 第二种方法, 分析程序的逻辑.

可以看到, func4()中有三个条件判断很关键:

从地址0x400fd2到0x 400fdf:

从高地址往低地址逆着推:

ecx = eax + esi
= eax>>1 + esi = eax / 2 + esi
= (eax + ecx) / 2 + esi = eax / 2 + esi # ecx是eax的符号, 这里都是正数ecx为0
= (eax - esi) / 2 + esi = (eax + esi) / 2
= (edx + esi) / 2

a) 情况1: 如果ecx > edi,则 400fe6 处代码将 ecx-1 赋给edx,接着递归调用func4函数。eax = eax + eax

b) 情况2: 如果ecx == edi,则将eax赋值为0并返回。

c) 情况3: 如果ecx < edi,则 400ffb 处代码将 ecx+1 赋给esi,接着递归调用func4函数。 eax = eax + eax + 1

这样一分析的话, 可以看到这个过程像二分查找. esi初始为0, 为左边界, edx为右边界, ecx为区间的中间值, edi为参数1.

情况2能保证最后eax为0;

情况1中会将eax= eax+ eax. 在情况3中, 会将eax = eax + eax + 1. 因此在递归过程中不能出现ecx<edi的情况, 如果出现了, 那么eax =eax * 2 + 1, eax必不等于0.

由于ecx为区间的中间值, 那么, 为了能到达情况2另eax=0, 则参数1也就是edi的值必须是ecx在区间变化过程中的值.

按照程序的逻辑来走的话, 区间变化如下:

[esi, edx] = [0, 14], ecx=7; edx = ecx-1=6; 如果edi==7, 则此时已经满足情况2的条件;

[esi, edx] = [0, 6], ecx=3; edx = ecx-1=2; 如果edi==3, 则此时已经满足情况2的条件;

[esi, edx] = [0, 2], ecx=1; edx = ecx-1=0; 如果edi==1, 则此时已经满足情况2的条件;

[esi, edx] = [0, 0], ecx=0; edx = ecx-1=-1; 如果edi==0, 则此时已经满足情况2的条件;

因此, 参数1的取值为: 0, 1, 3, 7, 参数2的取值为0. 测试通过

在这里插入图片描述

phase_5

  1. 由string_length()函数对输入判断的返回值可知, 应该输入长度为6的字符串

在这里插入图片描述

  1. 输入数据”abcdef”进行测试. 此时地址0x61处也就是寄存器ecx存储的是97, 也就是”a”, 依次打印0x62为”b”, 0x63为”c”.

在这里插入图片描述

  1. 从地址0x40108B开始到0x4010AC是一个循环, 当寄存器rax的值不等于6 的时候, 会在地址0x4010AC处跳转到0x40108B.

  2. 执行完循环以后, 将0x40245e地址处的内容放入esi寄存器中, 打印出来发现是字符串”flyers”. 然后将用户输入的字符串放入rdi寄存器, 打印发现此时已经跟我一开始输入的”abcdef”不是同一个了. 因此, phase_5应该是要构造一个字符串, 使得经过循环以后rdi的值为”flyers”. 最终, 在函数strings_not_equal()中对esi的内容和rdi的内容进行比较

在这里插入图片描述

  1. 接下来来具体看循环里面的内容. 地址0x4024b0打印出来, 发现是一个字符串, 通过rdx的低四位值来对字符串中的数据进行读取, 最后存放到edx寄存器中.

那么rdx值怎么来的? 溯源上去可以看到是用户输入的字符

在这里插入图片描述

  1. 然后再保存到[rsp+rax*1+0x10], 因为rax是从0–5, 因此存放地址就是[rsp+0x10]–[rsp+0x15]

  2. 地址0x4010ae处: 退出循环以后,将[rsp+0x16] 置为0, 作为循环生成后字符串的结束标志:’\0’.

然后地址0x4010b3处将字符串”flyers”放入esi寄存器中,用于后续strings_not_equal()函数的字符串比较

  1. 然后地址0x4010b8处将[rsp+0x10]放入寄存器rdi中, 用于后续strings_not_equal()函数的字符串比较

所以phase_5的解决方案就是:

从字符串”maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?”中, 根据ASCII码与0xF逻辑与操作得到的后四位, 取出”flayers”.

参考: http://ascii.911cha.com/

目标字符字符串中的索引索引对应的二进制可能的取值取值对应的二进制
f91001i0110
l151111o0110
y141110n0110
e50101e0110
r60110f0110
s70111g0110

所以, 最终可能的一种答案为: ionefg

phase_6

  1. 从地址0x401106可以看到, phase_6的输入为6个数字, 输入”1 2 3 4 5 6” 进行测试

在这里插入图片描述

在这里插入图片描述

则数据从rsp开始存放

地址以及存储的值
rsp+0x14 6
rsp+0x10 5
rsp+0xc 4
rsp+0x8 3
rsp+0x4 2
rsp 1
  1. 走完第一遍循环, 发现输入的数字满足两个条件: a. 参数1≤6; b.参数1和参数2,3,4,5,6都不相等

  2. 在该循环结束之后, 地址0x40114D处, 对r13中保存的地址+4, 即此时r13保存的为参数2的地址, 在地址0x401151处跳回地址0x401114.

则可以推断出:

a) 输入的6个数字都要≤6

b) 每个数字和后面的数字均不相等

在这里插入图片描述

r12寄存器用于控制循环, 当r12寄存器中的值为6时,跳转到地址0x401153处

  1. 地址0x40115-0x401174

在这里插入图片描述

0x401153处将rsp+18h的地址给rsi, 也就是参数6的地址再加4.

0x401158处将r14中存放的地址, 也就是参数1的地址(往上溯源发现是rsp中存放的值)赋值给rax寄存器. rax存放的地址在0x401166处自增4,也就是存放下一个参数的地址. 然后rsi寄存器在地址0x40116A处与rax进行比较, 控制循环的次数.

接下来, 在0x40115B处将7赋值给ecx寄存器, 在每一层循环中都用ecx寄存器对edx寄存器重新赋值. 接下来用edx减去当前参数, 并对当前参数重新覆盖.

参数1 = 7 - 参数1; 参数2=7-参数2; 参数3=7-参数3… 以此类推

再令esi为0, 跳转到0x401197.

  1. 接下来, 注意到gdb调试中的0x401183和0x4011A4的指令中, 都有个地址: 0x6032d0. 在IDA中, 显示为node1. 猜测为链表.

可以看到是在数据段, 因此这个链表应该是个全局变量. 注意到node1中, 0x6032D8-0x6032DF为0x6032E0(高位放在高地址,低位放在低地址), 也就是node2的地址. 因此, 该节点应该是个结构体, 最后一个成员为指针, 指向下一个节点, 并且占了八个字节. 一个节点占了16个字节.

因此可以猜测结构体为:

struct node{

          …,

          node *next;

};

在这里插入图片描述

  1. 地址0x401197开始, 有两个循环, 外层循环由rsi寄存器控制, 当rsi寄存器值为0x18时退出, 内层循环由eax控制, 当eax值和ecx寄存器值一样时退出.

a) 如果当前的参数值(也就是被7减过的)大于1, 则进入一层子循环0x401176-0x40117F, 用寄存器eax的值来控制, 只有当当前的参数与eax的值相同时才会退出.

并且将rdx寄存器中的值赋值为每个节点的起始地址 :

0x401176处: mov rdx, QWORD ptr [rdx+8] 这里比较奇怪的是IDA中没有显示QWORD ptr

从rdx+8的地址开始, 取8个字节放进rdx寄存器, rdx是node1的 起始地址, +8是刚好到指向下个节点的指针的起始地址, 再取8字节,刚好就是下一个节点的起始地址

当eax的值与当前参数相同时, 跳出循环,并且跳转至0x401188处, 把node1的地址赋值给[rsp+rsi*2+20h], 然后rsi自增4, 当rsi值为24时退出循环, 刚好赋值了6次. 当rsi值不为24时, 继续回到0x401197, 处理下一个参数.

在这里插入图片描述

b) 如果当前参数≤1, 则跳转到0x401183处, 先将node1地址给edx寄存器, 然后在0x401188处对地址[rsp+rsi*2+20h]进行赋值.

c) 因此, 从0x401176-0x401197的意义是, 根据被7减过的参数, 对地址[rsp+rsi2+0x20]进行赋值.
比如当前参数1的值为2, 此时rsi值为0, 那么会将地址[rsp+0
2+0x20]的内容赋值为 node参数1 也就是node2 的地址. 可以猜想[rsp+0+20h]开始分配了一个数组, 数组起始地址从[rsp+0x20]开始, 最后一个元素起始地址是[rsp+0x48]:

struct node *Array[6];

其中, Arrayi是一个指针, 指向了节点p(nodep, p=1,2…,6)的地址, p为参数的值. 并且, 由于指针是八个字节, 因此地址[rsp+rsi*2+20h]中rsi需要乘以2.

哭了,这部分终于理清楚了.

  1. 从地址0x4011AB开始, 这里需要注意的是, 数组本身的地址以及数组中元素存储的节点的起始地址的区别:

在这里插入图片描述

分成两部分来看:

a) 首先是初始化的部分: 0x4011AB至0x4011BA

0x4011AB mov rbx, [rsp+78h+var_58]

[rsp+0x20] 是Array[0]的起始地址, mov指令将该起始地址保存的地址, 也就是 node参数1 的地址赋值给了rbx寄存器.


0x4011B0 lea rax, [rsp+78h+var_50]

[rsp+0x28] 是Array[1]的起始地址, lea指令将Array[1]的起始地址赋值给rax寄存器.


4011B5 lea rsi, [rsp+78h+var_28]

[rsp+0x50]可以看作是Array[6]的起始地址, 把该地址直接赋值给rsi寄存器. 但是Array数组元素下标从0到5, 所以猜测这个地址应该是后续用来判断循环边界的. 这点在地址0x4011C8处得到验证.


4011BA mov rcx, rbx

这里将rbx保存的值, 也就是 node参数1 的起始地址赋值给rcx寄存器. rcx寄存器在这里的作用是保存当前的 node的起始地址.

b) 地址0x4011BD至0x4011D0 为循环, 由rax寄存器控制, rsi寄存器为边界:

这里就不在一行一行解读, 详细步骤看图片中的注释即可.

rcx寄存器保存了当前遍历到的Array元素Array[i] 保存的地址, 也就是node参数i+1 的地址, 其中i∈[0,5] . 因为数组元素Array[i]保存的是节点 nodei+1 的起始地址.

rax寄存器保存了当前Array元素的下一个元素 Array[i+1] 的地址, 再通过 [rax]就能得到该元素保存的 node参数i+2 的地址.


0x4011C0 mov [rcx+8], rdx

rcx为 node参数i+1 的地址 rcx+8 为node参数i 节点的结构体中, 指向下个节点的指针的起始地址. 因此,这里是将 node参数i 节点指向了node参数i+1.

然后在0x4011C4处让rax寄存器的值自增8, 也就是将Array[i+1] 的地址自增8, 来到Array[i+2] 的地址. 依次类推.

当rax地址与rsi相同, 即rax从[rsp+0x28]变成[rsp+0x50]时, 一共经过5个循环, 修改了5个节点指向的节点地址. 然后跳转到0x4011D2.

总结一下, 6)中是将 Array[i]保存了 node参数i+1 的地址, i∈[0, 5].

                7)中是遍历数组Array, 对于每个元素Array[i]保存的节点地址, 让保存的节点地址 node参数i+1 指向 node参数i+2.

比如,

Array012345
node135264

在这里插入图片描述

  1. 地址0x4011D2:

地址0x4011D2处, rdx此时保存的值为 node参数6 的地址.

地址0x4011DF处, 由0x4011AB处可知, rbx为[rsp+0x20], 是 node参数1 的地址. 则这里是将[rbx+8]的值, 也就是node1结构体中指向下个节点的指针, 即 node参数2 的地址给了rax寄存器.

地址0x4011E3处将[rax]的值, 也就是node参数2的值赋值给eax寄存器.

结合地址0x4011E5和0x4011E7可知, [rbx]必须大于 eax寄存器的值, 也就是node参数1 的值 必须≥node参数2的值. 不满足该条件就会引爆炸弹! 也就是说, 当前节点的值必须 > 它指向的节点的值. 也就是说, 用户的输入能对这个链表进行从大到小的排序.

同时注意到0x4011E5是将eax的值赋值给dword大小的内存空间[rbx], 也就是4个字节. 因此, 可以判断结构体第一个成员是int类型. 还记得之前分析的0x4011C0(第7)点分析)中, 为了将地址偏移到当前节点的结构体中指向下个节点的指针, 需要将rcx+8.回想起之前老师说的对齐机制, 这里应该是在节点的值–4字节 与 指针–8字节之间填充了4个字节. 结构体可以初步判断为:

struct node{

          int val;

          int padding;

          node* next;

};

满足这个条件判断, 跳转到0x4011EE.

0x4011EE mov rbx, [rbx+8]

[rbx+8] 为node1结构体中指向下个节点的指针, 即 node参数2 的地址, 将该地址赋值给rbx. 也就是遍历到下一个节点. 然后跳回地址0x4011DF.

  1. 那么, 现在的问题就变成了需要知道链表中节点存储的值. 然后对其从大到小排序, 再反推出输入的数值.

在IDA中查看或是在gdb中打印, 这里我直接在gdb中打印, 以16进制打印12个 8个字节的值:

在这里插入图片描述

根据高位放在高地址, 低位放在低地址的原则, 结合结构体的结构:

struct node{

          int val; // 低4字节

          int padding; // 高4字节

          node* next; // 高8字节

};

画出节点的初始关系图:

在这里插入图片描述

用python将其转为10进制:

在这里插入图片描述

对节点的值进行从大到小排序:

节点val0x39c0x2b30x1dd0x1bb0x14c0xa8
节点编号node 3node 4node 5node 6node 1node 2

也就是说, 用户的输入在经由 7 减去每个数之后得到的是 3, 4, 5, 6, 1, 2. 这样一来才能保证重新链接后的节点的值是从大到小排序的.

因此, 用户的输入为4 3 2 1 6 5

测试成功!~

在这里插入图片描述

0x05 总结收获

通过这次二进制炸弹的实验,算是对汇编的基础有了一点了解。并且对IDA的使用和gdb的使用也有了初步的了解。

不过这篇被diss惹,不能写得太细,应该把整个题目的框架揪出来就好。 下次好好照着师兄的要求来!

0x06 参考资料

1.关于汇编跳转指令的说明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值