CSAPP-BombLab 题解

前言

Bomb 是学校留的实验作业,但由于涉及汇编语言和相关的计算机底层原理,相较于 C++等作业显得尤为困难。(其实就是不好用 AI 直接出结果 XD)题目建议使用 gdb 等工具进行分析,但……有高级工具为啥不用呢?于是,IDA,启动!

但这里说一句,gdb 逆向出来的汇编代码和 IDA 逆向的代码在语法上有较大区别。本文仅作思路上的提示,不作为实验报告的标准答案。

那就直接进入正题吧!

背景知识

此部分采用 IDA 格式(Intel),与 gdb 格式(AT&T)略有出入,有兴趣的可以将这部分给 AI 并要求它转换成 gdb 格式以方便理解

硬件模型

CPU 是一个非常快速的处理器,但只能操作寄存器;寄存器是 CPU 内部的高速小存储;内存是 CPU 外的大型存储(当然也比硬盘小了)。

x86-64 寄存器​​:

一个寄存器有 64 位,但可能只用到其中最低的几位,那么他们就有不同的名字。例如下表中的rax,我如果使用eax来操作,那么最高的 32 位就会被自动全部设置为 0,但他们其实是完全相同的一个寄存器。

寄存器之间是相互等价的,没有任何区别,但我们通常约定好了很多寄存器的用途,方便统一标准。

下面是不同寄存器在不同操作长度下的名称和用途。

64 位 32 位 16 位 8 位 用途
rax eax ax al 返回值
rbx ebx bx bl 被调用者保存
rcx ecx cx cl 第 4 参数
rdx edx dx dl 第 3 参数
rsi esi si sil 第 2 参数
rdi edi di dil 第 1 参数
rbp ebp bp bpl 帧指针
rsp esp sp spl 栈指针
r8~r15 r8d~r15d r8w~r15w r8b~r15b r8/r9 是第 5/6 参数

调用约定​​:

  • 参数 1~6:rdi, rsi, rdx, rcx, r8, r9
  • 返回值:rax
  • 栈向低地址增长

栈​​:

栈其实是一块在内存中的地址,可以存相对大量的数据,而且遵循先进后出的原则。

常用指令

数据移动

mov  dst, src        ; dst = src
lea  dst, [expr]     ; dst = <result of expr>
push src             ; rsp -= 8; [rsp] = src
pop  dst             ; dst = [rsp]; rsp += 8

关键​​:lea rax, [rdi+rsi*2] 等价于 rax = rdi + 2*rsi,编译器常拿它做​​算术​​,不一定真是要取地址。

算术

add rax, rbx         ; rax += rbx
sub rax, 8           ; rax -= 8
imul eax, 3          ; eax *= 3
inc eax              ; eax++
dec eax              ; eax--
neg eax              ; eax = -eax

逻辑

and eax, 0Fh         ; 按位与
or  eax, ebx
xor eax, eax         ; 清零 eax 的惯用法
shl eax, 2           ; eax <<= 2
shr eax, 1           ; 无符号右移
sar eax, 1           ; 有符号右移

比较和跳转

cmp  a, b            ; 计算 a - b,只设标志位
test a, b            ; 计算 a & b,只设标志位
指令 跳转条件(cmp a, b 之后)
je/jz a == b
jne/jnz a != b
jg/jnle a > b (有符号)
jge a >= b
jl/jnge a < b
jle a <= b
ja a > b (无符号)
jb a < b (无符号)
js 结果为负
jmp 无条件

函数调用

call sub_400F0E      ; 压入返回地址,跳到函数
ret                  ; 从栈顶弹出地址并跳回

Bomb

小前言

汇编代码有很多细节处理,类似的逻辑也有不同表达方法,本文难以事无巨细的展开,需要读者自行理解。这里只做大体思路的分析。

整体分析

题目中给出了 bomb.c 文件,这只是一个框架,能够发现一共有 6 个 phase,每个 phase 都是单独的函数,但具体函数内容在引用的头文件中,并没有直接给出。

于是我们只能逐个读 phase 反汇编出来的代码了。

Phase 1

这里将地址为aBorderRelation的字符串(也就是后面提示的那一坨)移动到esi中(第二个参数的寄存器),然后调用了strings_not_equal。那此时的第一个参数是什么呢?就是phase_1接收的第一个参数,input

所以这就是一个最简单的字符串比较,如果输入等于那一坨,就通过jz跳转,否则就炸。

于是 Phase 1 的答案显然就是那一坨。找到对应地址得到“Border relations with Canada have never been better.”。

Phase 2

先来看整体逻辑,上面是调用了read_six_numbers,后续通过400F17400F25两段进行循环。

那么我们来看看read_six_numbers。如下:

其实前面一堆步骤就是在给sscanf传入各种地址,总之就是读取了 6 个用空格分开的数字,然后检查是否大于 5 个,如果正常就返回,否则直接炸(怎么一言不合就开炸 /doge)。

那么回到上面的循环。这里稍微展开分析一下。

lea     rbx, [rsp+38h+var_34]
lea     rbp, [rsp+38h+var_20]

这两行是在将那六个数字的首地址和尾地址(尾地址是最后一个数字的地址+1)存入rbxrbp。注意这里栈从高地址到低地址,但数组仍然是低地址到高地址。

紧接着进入400F17就开始了这样的过程:将地址为rbx+1数字放入eax,给eax乘二,检查eax和地址为rbx的数字是否相等。重复这个过程直到rbx+1rbq相等。 翻译成人话就是,反复验证nums[i] * 2 == nums[i + 1]是否成立,直到最后一个数字。这不就是最标准的循环嘛!

所以显然我们需要的是一个 6 位的公比为 2 的等比数列。于是我们取最简单的,得到答案:“1 2 4 8 16 32”。

Phase 3

看着长长一坨有点吓人。还好 IDA 神力已经帮我分析出了这是 switch 语句。

如果你弄懂了前面两个 Phase,那么你已经入门了,Phase 3 也是非常简单。

整体上就是读取两个数字,然后用第一个数字进行 switch,然后将第二个数字和 switch 中对应的结果进行比较判断。

显然这道题有多个答案,我们取屏幕中间的 Case 5,发现要求的是 0xCE,也就是 206。于是得出结果:“5 206”。

Phase 4

依旧读数字起手,只不过这次判断读入个数的地方略有区别,但大同小异。

读入后判断,要求第一个数据必须大于等于 0x0E,也就是 14。

而后在40103A中调用func4,三个传入的参数分别为:输入的第一个数字,0,14。

那么我们顺势进入func4看一看。

发现里面调用了func4本身,说明是一个递归函数。

特别值得说明的是,下面这段代码是对有符号数除以 2 并向 0 舍入。

mov     ecx, eax
shr     ecx, 1Fh
add     eax, ecx
sar     eax, 1

下面是 AI 给出的分析,借用一下。

块 1:计算中点 (Entry Block) 这是函数的入口,主要工作是计算二分查找的“中点”(mid) 并进行第一次判断。

块 3:左半边递归 (mid > x) 如果 mid > x,程序会顺着图中的蓝线(或向下)执行这个块。

块 2:第二次比较 (mid <= x) 如果 mid <= x,程序来到 loc_400FF2

块 4:右半边递归 (mid < x) 如果经过块 2 的判断,mid 不大于也不等于 x,那必然是 mid < x。程序顺着红线进入这个块。

块 5:函数返回 (Exit Block) 所有路径最终都会汇聚到这里。

C 语言伪代码还原

将上面的汇编逻辑翻译成高级语言,逻辑非常清晰:

int func4(int x, int low, int high) {
    // 1. 计算中点 mid
    int mid = low + (high - low) / 2;

    // 2. 如果中点值大于目标值,向左子树查找
    if (mid > x) {
        return 2 * func4(x, low, mid - 1);
    }
    // 3. 如果中点值小于目标值,向右子树查找
    else if (mid < x) {
        return 2 * func4(x, mid + 1, high) + 1;
    }
    // 4. 如果中点值等于目标值,找到节点,返回 0
    else {
        return 0;
    }
}

所以这个函数是在进行二分查找并返回查找路径。

回到phase_4,显然拆弹的条件是满足func4返回值为 0 且第二个输入为 0。

于是很容易构造出答案:“7 0”。

Phase 5

这次是读取字符串而不是数字了。上来就是一个验证,要求是长度为 6 的字符串。

读完之后的核心代码,实际上是在通过位操作,取每一个输入字符的最低四位作为索引查表,并要求查出的结果等于flyers

于是我们先来看一下这个表。

注意此处的6Dh也是一个字符,但由于对应了首地址,被单独分出来了。

所以表格为

索引 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
字符 m a d u i e r s n f o t v b y l

我们想要的flyers就对应了9 15 14 5 6 7。在 ASCII 中随意找到一组字符的最低四位对应这个值就好了。我们取:“ION567”。

Phase 6

!!!如此恶心的一大坨代码(不过没有混淆已经很不错了)

熟悉的读取六个数字。

左边一坨不做详细分析了,总之就是要求输入的六个数字不大于 6 且互不相同。

然后进入右面。首先是对每一个输入进行x = 7 - x的操作。

紧接着的部分不做详细分析了,总之就是将链表中next的值根据输入的数字进行重组(改变原有顺序)。最终要求重组后的链表从大到小排列。

找到 node 对应数据如下。

.data:00000000006032D0 node1           db  4Ch ; L             ; DATA XREF: phase_6:loc_401183↑o
.data:00000000006032D0                                         ; phase_6+B0↑o
.data:00000000006032D1                 db    1
.data:00000000006032D2                 db    0
.data:00000000006032D3                 db    0
.data:00000000006032D4                 db    1
.data:00000000006032D5                 db    0
.data:00000000006032D6                 db    0
.data:00000000006032D7                 db    0
.data:00000000006032D8                 db 0E0h
.data:00000000006032D9                 db  32h ; 2
.data:00000000006032DA                 db  60h ; `
.data:00000000006032DB                 db    0
.data:00000000006032DC                 db    0
.data:00000000006032DD                 db    0
.data:00000000006032DE                 db    0
.data:00000000006032DF                 db    0
.data:00000000006032E0                 public node2
.data:00000000006032E0 node2           db 0A8h
.data:00000000006032E1                 db    0
.data:00000000006032E2                 db    0
.data:00000000006032E3                 db    0
.data:00000000006032E4                 db    2
.data:00000000006032E5                 db    0
.data:00000000006032E6                 db    0
.data:00000000006032E7                 db    0
.data:00000000006032E8                 db 0F0h
.data:00000000006032E9                 db  32h ; 2
.data:00000000006032EA                 db  60h ; `
.data:00000000006032EB                 db    0
.data:00000000006032EC                 db    0
.data:00000000006032ED                 db    0
.data:00000000006032EE                 db    0
.data:00000000006032EF                 db    0
.data:00000000006032F0                 public node3
.data:00000000006032F0 node3           db  9Ch
.data:00000000006032F1                 db    3
.data:00000000006032F2                 db    0
.data:00000000006032F3                 db    0
.data:00000000006032F4                 db    3
.data:00000000006032F5                 db    0
.data:00000000006032F6                 db    0
.data:00000000006032F7                 db    0
.data:00000000006032F8                 db    0
.data:00000000006032F9                 db  33h ; 3
.data:00000000006032FA                 db  60h ; `
.data:00000000006032FB                 db    0
.data:00000000006032FC                 db    0
.data:00000000006032FD                 db    0
.data:00000000006032FE                 db    0
.data:00000000006032FF                 db    0
.data:0000000000603300                 public node4
.data:0000000000603300 node4           db 0B3h
.data:0000000000603301                 db    2
.data:0000000000603302                 db    0
.data:0000000000603303                 db    0
.data:0000000000603304                 db    4
.data:0000000000603305                 db    0
.data:0000000000603306                 db    0
.data:0000000000603307                 db    0
.data:0000000000603308                 db  10h
.data:0000000000603309                 db  33h ; 3
.data:000000000060330A                 db  60h ; `
.data:000000000060330B                 db    0
.data:000000000060330C                 db    0
.data:000000000060330D                 db    0
.data:000000000060330E                 db    0
.data:000000000060330F                 db    0
.data:0000000000603310                 public node5
.data:0000000000603310 node5           db 0DDh
.data:0000000000603311                 db    1
.data:0000000000603312                 db    0
.data:0000000000603313                 db    0
.data:0000000000603314                 db    5
.data:0000000000603315                 db    0
.data:0000000000603316                 db    0
.data:0000000000603317                 db    0
.data:0000000000603318                 db  20h
.data:0000000000603319                 db  33h ; 3
.data:000000000060331A                 db  60h ; `
.data:000000000060331B                 db    0
.data:000000000060331C                 db    0
.data:000000000060331D                 db    0
.data:000000000060331E                 db    0
.data:000000000060331F                 db    0
.data:0000000000603320                 public node6
.data:0000000000603320 node6           db 0BBh
.data:0000000000603321                 db    1
.data:0000000000603322                 db    0
.data:0000000000603323                 db    0
.data:0000000000603324                 db    6
.data:0000000000603325                 db    0
.data:0000000000603326                 db    0
.data:0000000000603327                 db    0
.data:0000000000603328                 db    0
.data:0000000000603329                 db    0
.data:000000000060332A                 db    0
.data:000000000060332B                 db    0
.data:000000000060332C                 db    0
.data:000000000060332D                 db    0
.data:000000000060332E                 db    0
.data:000000000060332F                 db    0
.data:0000000000603330                 db    0
.data:0000000000603331                 db    0
.data:0000000000603332                 db    0
.data:0000000000603333                 db    0
.data:0000000000603334                 db    0
.data:0000000000603335                 db    0
.data:0000000000603336                 db    0
.data:0000000000603337                 db    0
.data:0000000000603338                 db    0
.data:0000000000603339                 db    0
.data:000000000060333A                 db    0
.data:000000000060333B                 db    0
.data:000000000060333C                 db    0
.data:000000000060333D                 db    0
.data:000000000060333E                 db    0
.data:000000000060333F                 db    0
节点 地址 原始字节 (前 4 字节) Value (小端解析) Value (十进制)
node1 0x6032D0 4C 01 00 00 0x0000014C 332
node2 0x6032E0 A8 00 00 00 0x000000A8 168
node3 0x6032F0 9C 03 00 00 0x0000039C 924
node4 0x603300 B3 02 00 00 0x000002B3 691
node5 0x603310 DD 01 00 00 0x000001DD 477
node6 0x603320 BB 01 00 00 0x000001BB 443

得到这个表格。于是我们得到重排顺序为:“3 4 5 6 1 2”。

别忘了上面还有一个对 7 求差的过程,所以反过来得到答案:“4 3 2 1 6 5”。

Secret Phase

首先来看phase_defused的代码。

显然出现了一个特殊路径进入秘密关卡。我们来关注一下如何满足这个路径。

首先第一块是判断是否完成了所有 6 个 Phase,第二块是以%d %d %s的格式读取unk_603870地址上的内容并判断是否是三个。

那么这个奇怪的地址是什么呢?

这里有两种方法。一种是在此处打断点,通过 gdb 调试查看这个地址的实际内容,得到7 0,发现是 Phase 4 的密码。但我很好奇在代码中这个寻址是如何体现的呢?

这就要看read_line的函数了。其中有如下一段内容。

loc_40151F:
mov     edx, cs:num_input_strings
movsxd  rax, edx
lea     rsi, [rax+rax*4]
shl     rsi, 4
add     rsi, 603780h
mov     rdi, rsi
mov     eax, 0
mov     rcx, 0FFFFFFFFFFFFFFFFh
repne scasb
not     rcx
sub     rcx, 1
cmp     ecx, 4Eh ; 'N'
jle     short loc_40159A

这里的rax+rax*4以及下面的左移就凑出了0x50这个长度,也就能算出unk_603870是 Phase 4 的答案了。

好了回到正题,也就是说我们要将 Phase 4 的答案后面加上DrEvil这个单词。这样就可以成功跳转到秘密关卡了。

这里一上来是一个输入和转为十进制。接下来进入fun7函数。

我们先来分析fun7

这里和func4很相似,只不过是一个二叉树的查找,并返回二进制的路径。

回头看,上面要求fun7返回2,也就是10,我们可以知道是从二叉树的根节点通过先左后右的路线得到的值。

来看节点n1的数据。

.data:00000000006030F0                 public n1
.data:00000000006030F0 n1              db  24h ; $             ; DATA XREF: secret_phase+2C↑o
.data:00000000006030F1                 db    0
.data:00000000006030F2                 db    0
.data:00000000006030F3                 db    0
.data:00000000006030F4                 db    0
.data:00000000006030F5                 db    0
.data:00000000006030F6                 db    0
.data:00000000006030F7                 db    0
.data:00000000006030F8                 db  10h
.data:00000000006030F9                 db  31h ; 1
.data:00000000006030FA                 db  60h ; `
.data:00000000006030FB                 db    0
.data:00000000006030FC                 db    0
.data:00000000006030FD                 db    0
.data:00000000006030FE                 db    0
.data:00000000006030FF                 db    0
.data:0000000000603100                 db  30h ; 0
.data:0000000000603101                 db  31h ; 1
.data:0000000000603102                 db  60h ; `
.data:0000000000603103                 db    0
.data:0000000000603104                 db    0
.data:0000000000603105                 db    0
.data:0000000000603106                 db    0
.data:0000000000603107                 db    0
.data:0000000000603108                 db    0
.data:0000000000603109                 db    0
.data:000000000060310A                 db    0
.data:000000000060310B                 db    0
.data:000000000060310C                 db    0
.data:000000000060310D                 db    0
.data:000000000060310E                 db    0
.data:000000000060310F                 db    0

根据根左右的顺序,第一次向左就要跳转到603110的地址(注意此处是从高地址到低地址存放的)。

同理找到下一个右节点,得到数据16h,于是得到答案:“22”。

总结

芜湖!历时两天,终于搞定了这个烦人的可爱家伙。在不断与 AI 的搏斗中终于入门了汇编和逆向。爽了。

最后给一个答案吧。对了,如果是放在文件里的话,最后的换行是必要的,一开始还踩了这个坑。

Border relations with Canada have never been better.
1 2 4 8 16 32
5 206
7 0 DrEvil
ION567
4 3 2 1 6 5
22

AI 助手
AI
你好!我可以根据当前文章内容回答你的问题。