前言
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,后续通过400F17和400F25两段进行循环。
那么我们来看看read_six_numbers。如下:

其实前面一堆步骤就是在给sscanf传入各种地址,总之就是读取了 6 个用空格分开的数字,然后检查是否大于 5 个,如果正常就返回,否则直接炸(怎么一言不合就开炸 /doge)。
那么回到上面的循环。这里稍微展开分析一下。
lea rbx, [rsp+38h+var_34]
lea rbp, [rsp+38h+var_20]
这两行是在将那六个数字的首地址和尾地址(尾地址是最后一个数字的地址+1)存入rbx和rbp。注意这里栈从高地址到低地址,但数组仍然是低地址到高地址。
紧接着进入400F17就开始了这样的过程:将地址为rbx+1数字放入eax,给eax乘二,检查eax和地址为rbx的数字是否相等。重复这个过程直到rbx+1和rbq相等。
翻译成人话就是,反复验证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