Bomb Lab

实验说明:

A “binary bomb” is a program provided to students as an object code file. When run, it prompts the user to type in 6 different strings. If any of these is incorrect, the bomb “explodes,” printing an error message and logging the event on a grading server. Students must “defuse” their own unique bomb by disassembling and reverse engineering the program to determine what the 6 strings should be. The lab teaches students to understand assembly language, and also forces them to learn how to use a debugger. It’s also great fun. A legendary lab among the CMU undergrads.

是个逆向的小游戏,我们使用gdb来做

反汇编一下main函数,可以大概看到有很多的phase,我们需要逐个拆除

phase_1


反汇编可以看到在phase_1里面调用了一个strings_not_equal的函数,猜测如果两个字符串不相等就会爆炸,x64下程序通过rsi传入第二个参数,我们看一下具体的内存值即可。

运行程序,发现可以不被引爆

phase_2

反汇编phase_2

可以看到后面有个jne指令向上跳转,所以肯定是个循环结构,在上面是个read_six_numbers,读入6个数字,所以猜测对每个数字都有要求,并且通过动调可以发现cmp那条指令要求第一个数字为1


读入六个数字,否则就退出程序

0x0000000000400f1a <+30>:	add    eax,eax
0x0000000000400f1c <+32>: cmp DWORD PTR [rbx],eax

这个位置动态调试可以发现是后面的数据是否等于前面数字的二倍

那么payload应该就是1 2 4 8 16 32了
可以通过验证

phase_3

0x0000000000400f43 <+0>:	sub    rsp,0x18
0x0000000000400f47 <+4>: lea rcx,[rsp+0xc]
0x0000000000400f4c <+9>: lea rdx,[rsp+0x8]
0x0000000000400f51 <+14>: mov esi,0x4025cf
0x0000000000400f56 <+19>: mov eax,0x0
0x0000000000400f5b <+24>: call 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000400f60 <+29>: cmp eax,0x1
# 读入数据要是2
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>
0x0000000000400f65 <+34>: call 0x40143a <explode_bomb>
0x0000000000400f6a <+39>: cmp DWORD PTR [rsp+0x8],0x7
# 第一个读入的数据不能超过7
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106>
0x0000000000400f71 <+46>: mov eax,DWORD PTR [rsp+0x8]
0x0000000000400f75 <+50>: jmp QWORD PTR [rax*8
+0x402470]
# 寻址到不同的分支
0x0000000000400f7c <+57>: mov eax,0xcf
0x0000000000400f81 <+62>: jmp 0x400fbe <phase_3+123>
0x0000000000400f83 <+64>: mov eax,0x2c3
0x0000000000400f88 <+69>: jmp 0x400fbe <phase_3+123>
0x0000000000400f8a <+71>: mov eax,0x100
0x0000000000400f8f <+76>: jmp 0x400fbe <phase_3+123>
0x0000000000400f91 <+78>: mov eax,0x185
0x0000000000400f96 <+83>: jmp 0x400fbe <phase_3+123>
0x0000000000400f98 <+85>: mov eax,0xce
0x0000000000400f9d <+90>: jmp 0x400fbe <phase_3+123>
0x0000000000400f9f <+92>: mov eax,0x2aa
0x0000000000400fa4 <+97>: jmp 0x400fbe <phase_3+123>
0x0000000000400fa6 <+99>: mov eax,0x147
0x0000000000400fab <+104>: jmp 0x400fbe <phase_3+123>
0x0000000000400fad <+106>: call 0x40143a <explode_bomb>
0x0000000000400fb2 <+111>: mov eax,0x0
0x0000000000400fb7 <+116>: jmp 0x400fbe <phase_3+123>
0x0000000000400fb9 <+118>: mov eax,0x137
0x0000000000400fbe <+123>: cmp eax,DWORD PTR [rsp+0xc]
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134>
0x0000000000400fc4 <+129>: call 0x40143a <explode_bomb>
0x0000000000400fc9 <+134>: add rsp,0x18
0x0000000000400fcd <+138>: ret

目测看起来像个Switch结构,关键点给出注释,中间通过读入的数字到eax里面进行寻址
比如我要输入的第一个数字是7,那么查看地址如下

pwndbg> x/wx 0x4024a8
0x4024a8: 0x00400fa6

可以发现会执行mov eax,0x147,然后jmp到cmp指令,将sscanf读入的第二个参数和该参数相比较,那么第二个我们输入0x147即可通过验证

Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.
Phase 1 defused. How about the next one?
1 2 4 8 16 32
That's number 2. Keep going!
7 327
Halfway there!

这是截止到目前的程序输出

phase_4

Dump of assembler code for function phase_4:
0x000000000040100c <+0>: sub rsp,0x18
0x0000000000401010 <+4>: lea rcx,[rsp+0xc]
0x0000000000401015 <+9>: lea rdx,[rsp+0x8]
0x000000000040101a <+14>: mov esi,0x4025cf
0x000000000040101f <+19>: mov eax,0x0
0x0000000000401024 <+24>: call 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000401029 <+29>: cmp eax,0x2
0x000000000040102c <+32>: jne 0x401035 <phase_4+41>
0x000000000040102e <+34>: cmp DWORD PTR [rsp+0x8],0xe
# 第一个参数需要小于0xe
0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46>
0x0000000000401035 <+41>: call 0x40143a <explode_bomb>
0x000000000040103a <+46>: mov edx,0xe
0x000000000040103f <+51>: mov esi,0x0
0x0000000000401044 <+56>: mov edi,DWORD PTR [rsp+0x8]
0x0000000000401048 <+60>: call 0x400fce <func4>
# 调用func4函数继续处理,第一个参数为我们输入的第一个值,第二个参数是0,第三个参数是0xe
0x000000000040104d <+65>: test eax,eax
0x000000000040104f <+67>: jne 0x401058 <phase_4+76>
0x0000000000401051 <+69>: cmp DWORD PTR [rsp+0xc],0x0
0x0000000000401056 <+74>: je 0x40105d <phase_4+81>
# 返回值和第二个参数都必须为0
0x0000000000401058 <+76>: call 0x40143a <explode_bomb>
0x000000000040105d <+81>: add rsp,0x18
0x0000000000401061 <+85>: ret

可以看到还是常规的首先比较参数个数,需要两个,并且第一个参数需要不能大于0xe

func4函数如下

0x0000000000400fce <+0>:	sub    rsp,0x8
0x0000000000400fd2 <+4>: mov eax,edx
# 0xe
0x0000000000400fd4 <+6>: sub eax,esi
0x0000000000400fd6 <+8>: mov ecx,eax
0x0000000000400fd8 <+10>: shr ecx,0x1f
0x0000000000400fdb <+13>: add eax,ecx
0x0000000000400fdd <+15>: sar eax,1
0x0000000000400fdf <+17>: lea ecx,[rax+rsi*1]
0x0000000000400fe2 <+20>: cmp ecx,edi
# 第一轮循环:ecx = (0xe >> 0x1f + 0xe) >> 1,然后和我们输入的第二个参数比较
0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36>
0x0000000000400fe6 <+24>: lea edx,[rcx-0x1]
# 第三个参数:[rcx-1]
0x0000000000400fe9 <+27>: call 0x400fce <func4>
0x0000000000400fee <+32>: add eax,eax
0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57>
0x0000000000400ff2 <+36>: mov eax,0x0
0x0000000000400ff7 <+41>: cmp ecx,edi
0x0000000000400ff9 <+43>: jge 0x401007 <func4+57>
0x0000000000400ffb <+45>: lea esi,[rcx+0x1]
0x0000000000400ffe <+48>: call 0x400fce <func4>
0x0000000000401003 <+53>: lea eax,[rax+rax*1+0x1]
0x0000000000401007 <+57>: add rsp,0x8
0x000000000040100b <+61>: ret

一个递归函数,有点绕。。。
借助一下ida - - (我好菜

__int64 __fastcall func4(__int64 a1, __int64 a2, __int64 a3)
{
signed int v3; // ecx
__int64 result; // rax

v3 = (a3 - a2) / 2 + a2;
if ( v3 > a1 )
return 2 * func4(a1, a2, (v3 - 1));
result = 0LL;
if ( v3 < a1 )
result = 2 * func4(a1, (v3 + 1), a3) + 1;
return result;
}

很清晰,不能大于不能小于,必须等于(当然在递归中等于也可以
仔细想想,这不就是二分查找吗(x
所以只要能被二分查找发现的都可以

phase_5

汇编代码如下

0x0000000000401062 <+0>:	push   rbx
0x0000000000401063 <+1>: sub rsp,0x20
0x0000000000401067 <+5>: mov rbx,rdi
0x000000000040106a <+8>: mov rax,QWORD PTR fs:0x28
0x0000000000401073 <+17>: mov QWORD PTR [rsp+0x18],rax
0x0000000000401078 <+22>: xor eax,eax
0x000000000040107a <+24>: call 0x40131b <string_length>
0x000000000040107f <+29>: cmp eax,0x6
# 比较字符串长度
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112>
0x0000000000401084 <+34>: call 0x40143a <explode_bomb>
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
0x000000000040108b <+41>: movzx ecx,BYTE PTR [rbx+rax*1]
0x000000000040108f <+45>: mov BYTE PTR [rsp],cl
0x0000000000401092 <+48>: mov rdx,QWORD PTR [rsp]
0x0000000000401096 <+52>: and edx,0xf
0x0000000000401099 <+55>: movzx edx,BYTE PTR [rdx+0x4024b0]
0x00000000004010a0 <+62>: mov BYTE PTR [rsp+rax*1+0x10],dl
0x00000000004010a4 <+66>: add rax,0x1
# 每次+1
0x00000000004010a8 <+70>: cmp rax,0x6
# 循环6
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>
0x00000000004010ae <+76>: mov BYTE PTR [rsp+0x16],0x0
0x00000000004010b3 <+81>: mov esi,0x40245e
0x00000000004010b8 <+86>: lea rdi,[rsp+0x10]
0x00000000004010bd <+91>: call 0x401338 <strings_not_equal>
0x00000000004010c2 <+96>: test eax,eax
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119>
0x00000000004010c6 <+100>: call 0x40143a <explode_bomb>
0x00000000004010cb <+105>: nop DWORD PTR [rax+rax*1+0x0]
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119>
0x00000000004010d2 <+112>: mov eax,0x0
0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41>
0x00000000004010d9 <+119>: mov rax,QWORD PTR [rsp+0x18]
0x00000000004010de <+124>: xor rax,QWORD PTR fs:0x28
0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140>
0x00000000004010e9 <+135>: call 0x400b30 <__stack_chk_fail@plt>
0x00000000004010ee <+140>: add rsp,0x20
0x00000000004010f2 <+144>: pop rbx
0x00000000004010f3 <+145>: ret

很显然是一个大循环
动调发现有一个类似于代换表的东西,需要在循环中给出索引,然后查表,最后和字符串对比

pwndbg> x/s 0x4024B0
0x4024b0 <array>: "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?"
pwndbg> x/s 0x40245e
0x40245e: "flyers"

应该是取前面的maduiersnfotvbyl
索引值为[9, 15, 14, 5, 6, 7]
取ascii码值和0xf与后等于索引值即可, “yonefg“

phase_6

部分汇编代码如下

0x0000000000401106 <+18>:	call   0x40145c <read_six_numbers>
0x000000000040110b <+23>: mov r14,rsp
0x000000000040110e <+26>: mov r12d,0x0
0x0000000000401114 <+32>: mov rbp,r13
# rsp = rbp 都指向我们输入的6个数字,通过栈传递参数,返回值存入rax
0x0000000000401117 <+35>: mov eax,DWORD PTR [r13+0x0]
# 输入的第一个数字进入eax
0x000000000040111b <+39>: sub eax,0x1
0x000000000040111e <+42>: cmp eax,0x5
0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52>
# -1 后和5进行比较, 小于等于则跳转,即第一位数字必须小于0x5
0x0000000000401123 <+47>: call 0x40143a <explode_bomb>
0x0000000000401128 <+52>: add r12d,0x1

0x000000000040112c <+56>: cmp r12d,0x6
0x0000000000401130 <+60>: je 0x401153 <phase_6+95>
loop1:
# 跳出循环
0x0000000000401132 <+62>: mov ebx,r12d
# 循环中的第二层循环
0x0000000000401135 <+65>: movsxd rax,ebx
0x0000000000401138 <+68>: mov eax,DWORD PTR [rsp+rax*4]
# 索引取出数字
0x000000000040113b <+71>: cmp DWORD PTR [rbp+0x0],eax
# 比较取出的数字是否和第一个数字相同,相同则爆炸
0x000000000040113e <+74>: jne 0x401145 <phase_6+81>
0x0000000000401140 <+76>: call 0x40143a <explode_bomb>
0x0000000000401145 <+81>: add ebx,0x1
0x0000000000401148 <+84>: cmp ebx,0x5
# ebx = 1-6,比较所有的数字
0x000000000040114b <+87>: jle 0x401135 <phase_6+65>
# 跳出第二层循环

0x000000000040114d <+89>: add r13,0x4
# 动调发现r13指向第一个数字,+4后指向第二个数字
0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32>
# 循环,退出条件为r12d = 6

loop2:
0x0000000000401153 <+95>: lea rsi,[rsp+0x18]
0x0000000000401158 <+100>: mov rax,r14
0x000000000040115b <+103>: mov ecx,0x7
# 循环开始
0x0000000000401160 <+108>: mov edx,ecx
0x0000000000401162 <+110>: sub edx,DWORD PTR [rax]
0x0000000000401164 <+112>: mov DWORD PTR [rax],edx
# 数字 = edx( 0x7 - 数字)
0x0000000000401166 <+114>: add rax,0x4
0x000000000040116a <+118>: cmp rax,rsi
0x000000000040116d <+121>: jne 0x401160 <phase_6+108>
# 退出条件:当前数字为0

loop1中主要是比较六个数字中是否有相同的数字

0x000000000040116f <+123>:	mov    esi,0x0

0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163>

loop3:
0x0000000000401176 <+130>: mov rdx,QWORD PTR [rdx+0x8]
# node节点
0x000000000040117a <+134>: add eax,0x1
0x000000000040117d <+137>: cmp eax,ecx
0x000000000040117f <+139>: jne 0x401176 <phase_6+130>
# 循环,一直到eax和数字相等

0x0000000000401181 <+141>: jmp 0x401188 <phase_6+148>

0x0000000000401183 <+143>: mov edx,0x6032d0
0x0000000000401188 <+148>: mov QWORD PTR [rsp+rsi*2+0x20],rdx
# 将node节点存入到栈空间中

0x000000000040118d <+153>: add rsi,0x4
0x0000000000401191 <+157>: cmp rsi,0x18
# 一直到rsi=0x18,即遍历完全6个数字
0x0000000000401195 <+161>: je 0x4011ab <phase_6+183>

0x0000000000401197 <+163>: mov ecx,DWORD PTR [rsp+rsi*1]
0x000000000040119a <+166>: cmp ecx,0x1
# 第一个数字
0x000000000040119d <+169>: jle 0x401183 <phase_6+143>
# 如果小于等于则向上跳
0x000000000040119f <+171>: mov eax,0x1
0x00000000004011a4 <+176>: mov edx,0x6032d0
# 0x6032d0 -> 0x000000010000014c
0x00000000004011a9 <+181>: jmp 0x401176 <phase_6+130>
# 向上跳

这一部分也详细写入注释中
经过动调发现实现了将输入的数字减去7,并且将对应的node节点都移动到栈中

0x00000000004011ab <+183>:	mov    rbx,QWORD PTR [rsp+0x20]
0x00000000004011b0 <+188>: lea rax,[rsp+0x28]
0x00000000004011b5 <+193>: lea rsi,[rsp+0x50]
0x00000000004011ba <+198>: mov rcx,rbx
# start
0x00000000004011bd <+201>: mov rdx,QWORD PTR [rax]
0x00000000004011c0 <+204>: mov QWORD PTR [rcx+0x8],rdx
0x00000000004011c4 <+208>: add rax,0x8
0x00000000004011c8 <+212>: cmp rax,rsi
0x00000000004011cb <+215>: je 0x4011d2 <phase_6+222>

0x00000000004011cd <+217>: mov rcx,rdx
0x00000000004011d0 <+220>: jmp 0x4011bd <phase_6+201>
# end 遍历六个node
0x00000000004011d2 <+222>: mov QWORD PTR [rdx+0x8],0x0
0x00000000004011da <+230>: mov ebp,0x5
0x00000000004011df <+235>: mov rax,QWORD PTR [rbx+0x8]
0x00000000004011e3 <+239>: mov eax,DWORD PTR [rax]
0x00000000004011e5 <+241>: cmp DWORD PTR [rbx],eax
0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250>
# 如果前一个节点大于后一个节点则不会跳转

0x00000000004011e9 <+245>: call 0x40143a <explode_bomb>
0x00000000004011ee <+250>: mov rbx,QWORD PTR [rbx+0x8]
0x00000000004011f2 <+254>: sub ebp,0x1
0x00000000004011f5 <+257>: jne 0x4011df <phase_6+235>
0x00000000004011f7 <+259>: add rsp,0x50
0x00000000004011fb <+263>: pop rbx
0x00000000004011fc <+264>: pop rbp
0x00000000004011fd <+265>: pop r12
0x00000000004011ff <+267>: pop r13
0x0000000000401201 <+269>: pop r14
0x0000000000401203 <+271>: ret

对比了node节点中的数据的大小,我们重启程序,查看原生的node节点情况如下

pwndbg> x/32gx 0x6032d0
0x6032d0 <node1>: 0x000000010000014c 0x00000000006032e0
0x6032e0 <node2>: 0x00000002000000a8 0x00000000006032f0
0x6032f0 <node3>: 0x000000030000039c 0x0000000000603300
0x603300 <node4>: 0x00000004000002b3 0x0000000000603310
0x603310 <node5>: 0x00000005000001dd 0x0000000000603320
0x603320 <node6>: 0x00000006000001bb 0x0000000000000000

大小顺序为 3 4 5 6 1 2
那么变换后是这些,变换前就是 4 3 2 1 6 5

反馈与总结

通过这个实验复习了一些汇编的知识,很大收获,之前习惯于看c代码,才发现读汇编的能力是如此之弱

  1. objdump -d -m 指令格式 ./binary_file > xxx.asm 反汇编出文件
  2. 函数的连续调用的寄存器参数变化,依然是rdi等寄存器传进去
  3. 毅力,自制力和专注力
文章作者: Alex
文章链接: http://example.com/2021/06/15/BombLab/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Alex's blog~