Bomblab(ICS课程回课pku)

💬全剧情

充斥邪魅气息の欢迎🤗
Welcome to Dr. Evil’s little bomb. You have 6 phases with which to blow yourself up. Have a nice day! Mua ha ha ha!

命中注定的逐步溃败👼
Phase 1 defused. How about the next one?
That’s number 2. Keep going!
Halfway there!
So you got that one. Try this one.
Good work! On to the next…

来自博士的赞赏👍
Congratulations! You’ve defused the bomb!
Your instructor has been notified and will verify your solution.


隐藏剧情🎭

邪恶的内心独白🧛‍♂️
Wow, they got it! But isn’t something… missing? Perhaps something they overlooked? Mua ha ha ha ha!

面对强敌的惊愕💦
Curses, you’ve found the secret phase!
But finding it and solving it are quite different…

PKUer不好惹,逃💨
Bravo! You’ve defused the secret stage!
Dr. Evil finally knew that the students at PKU are not easy to defeat.
He fled in haste, but you know it is not over…

但邪恶永存,He will be back again!🎃
​ “A sleepless malice”, uttered you as you caught a whiff of the next trouble…


🚗Tips

一些好用的gdb命令:

  1. 更改字符串:set {char[20]} $rdi = “new string.”
  2. 改变条件码,控制跳转方向:set $eflags = ($eflags & ~0b1000000),即设置ZF为0
  3. 设置不同输入,计算函数返回值:print (int) fun7(0x55fa6e688150, 7)
  4. 显示内存中一段区块的内容:x/32gx 0x55fa6e688150

eflags中一些比较重要的标志位:

11109876543210
OF---SFZF---PF-CF

阅读汇编代码的技巧:

  1. 根据汇编,写出伪代码,进一步写出对应的C语言代码。
  2. 根据循环结构和jump的终点,将代码分出模块,分而治之。
  3. 可以边在gdb中运行,边确定函数的功能(避免头晕眼花)。
  4. 也可以把汇编代码内容复制到有着色功能的编辑器中。

从一堆限制条件中确定答案的技巧:

  1. 当然可以试图去理解函数的功能,从而确定答案。不过在已知很多限定条件时,也可以利用枚举。
  2. 可以利用gdb自带的call命令,比如print (int) fun7(0x55fa6e688150, 7),就可以输出fun7以给定值作为参数的返回值(要先确保目标函数是否会有side effects)。
  3. 可以利用C语言的asm关键字或者直接翻译出C代码,直接在本地上循环运行相应代码段。

⛔Cautions

  1. 不可以修改程序计数器(包括在set $pc, set $rip, jump *),此类操作会直接触发炸弹。
  2. 不要以为在explode_bomb上设置断点后炸弹就不会爆炸,只要explode_bomb被触发,处罚会立刻上报。正确做法是在所有call explode_bomb的指令上设置断点。

🛵开始闯关!

Ready

gdb的一些常用操作:

  1. 召出图形化界面,gdb -tui
  2. 打开文件,file <executable file name>
  3. start, run, continue, break, si, ni, display, info registers, kill, quit…
  4. 刷新界面,键盘按ctrl+L

phase_1

最开始在无法确定密码是什么时,可以先提前输入很长一段字符串作为密码,预留好空间,之后再改。

0x00000000000027cd <+0>:     endbr64 
0x00000000000027d1 <+4>:     sub    $0x8,%rsp
0x00000000000027d5 <+8>:     lea    0x2a24(%rip),%rsi        # 0x5200
0x00000000000027dc <+15>:    callq  0x2d2e <strings_not_equal>
0x00000000000027e1 <+20>:    test   %eax,%eax
0x00000000000027e3 <+22>:    jne    0x27ea <phase_1+29>
0x00000000000027e5 <+24>:    add    $0x8,%rsp
0x00000000000027e9 <+28>:    retq   
0x00000000000027ea <+29>:    callq  0x3105 <explode_bomb>
0x00000000000027ef <+34>:    jmp    0x27e5 <phase_1+24>

根据strings_not_equal函数名可以推断,此段代码的功能就是比较%rdi和%rsi(0x5200)所指向的字符串是否相同。

(gdb) x/s 0x5200
0x5200: “All those moments will be lost in time, like tears in rain. Time to die.

(gdb) set {char[100]} $rdi = “All those moments will be lost in time, like tears in rain. Time to die.”

我所见过的事物,你们人类绝对无法置信,我目睹了战舰在猎户星座的端沿起火燃烧,我看着C射线在唐怀瑟之门附近的黑暗中闪耀,所有这些时刻,终将随时间消逝,一如眼泪消失在雨中。 --《银翼杀手》


phase_2

观察头部和尾部的功能:开栈,设置金丝雀值以及检测栈是否被破坏。

0x00000000000027f1 <+0>:     endbr64 
0x00000000000027f5 <+4>:     push   %rbx
0x00000000000027f6 <+5>:     sub    $0x20,%rsp
0x00000000000027fa <+9>:     mov    %fs:0x28,%rax
0x0000000000002803 <+18>:    mov    %rax,0x18(%rsp)
0x0000000000002808 <+23>:    xor    %eax,%eax
...
0x0000000000002848 <+87>:    mov    0x18(%rsp),%rax
0x000000000000284d <+92>:    xor    %fs:0x28,%rax
0x0000000000002856 <+101>:   jne    0x285e <phase_2+109>
0x0000000000002858 <+103>:   add    $0x20,%rsp
0x000000000000285c <+107>:   pop    %rbx
0x000000000000285d <+108>:   retq   
0x000000000000285e <+109>:   callq  0x2290 <__stack_chk_fail@plt>

主干部分,首先读取六个数字,并将结果存储在%rsp所指向的数组中。

0x000000000000280a <+25>:    mov    %rsp,%rsi
0x000000000000280d <+28>:    callq  0x31f3 <read_six_numbers>

之后的内容无非是一连串的比对,只要细心就可以分析好的,C版本代码如下:

// int rsp[6];
assert(rsp[0] >= 0);
for (int ebx = 1; ebx <= 5; ++ebx) {
    assert(rsp[ebx] == ebx + rsp[ebx-1])
}

自然可以得到一种答案:0 1 3 6 10 15


phase_3

很长很长,采用逐段分析&翻译的方式:

# 设置金丝雀值
0x0000000000002863 <+0>:     endbr64 
0x0000000000002867 <+4>:     sub    $0x18,%rsp
0x000000000000286b <+8>:     mov    %fs:0x28,%rax
0x0000000000002874 <+17>:    mov    %rax,0x8(%rsp)
0x0000000000002879 <+22>:    xor    %eax,%eax
# 读取两个int,存储在%rsp所指向的数组中
0x000000000000287b <+24>:    lea    0x4(%rsp),%rcx
0x0000000000002880 <+29>:    mov    %rsp,%rdx
0x0000000000002883 <+32>:    lea    0x2e36(%rip),%rsi        # 0x56c0
0x000000000000288a <+39>:    callq  0x2350 <__isoc99_sscanf@plt>
# 确保读入的是两个数
0x000000000000288f <+44>:    cmp    $0x1,%eax
0x0000000000002892 <+47>:    jle    0x28af <phase_3+76>
# 确保0<=rsp[0]<=7
0x0000000000002894 <+49>:    mov    (%rsp),%eax
0x0000000000002897 <+52>:    cmp    $0x7,%eax
0x000000000000289a <+55>:    ja     0x2900 <phase_3+157>	 # explode_bomb

之后的代码,其实就是一段跳转表实现的switch结构。

0x000000000000289c <+57>:    mov    %eax,%eax
0x000000000000289e <+59>:    lea    0x2abb(%rip),%rdx        # 0x5360
0x00000000000028a5 <+66>:    movslq (%rdx,%rax,4),%rax
0x00000000000028a9 <+70>:    add    %rdx,%rax
0x00000000000028ac <+73>:    notrack jmpq *%rax

(gdb) x/8wx 0x5360
0x5360: 0xffffd556 0xffffd5ac 0xffffd576 0xffffd57d
0x5370: 0xffffd584 0xffffd58b 0xffffd592 0xffffd599

不同的rsp[0]值所对应的跳转标号一目了然,不再赘述。

每一种跳转都对应一种答案,可自行分析。


phase_4

# some initialization
0x0000000000002955 <+0>:     endbr64 
0x0000000000002959 <+4>:     sub    $0x18,%rsp
0x000000000000295d <+8>:     mov    %fs:0x28,%rax
0x0000000000002966 <+17>:    mov    %rax,0x8(%rsp)
0x000000000000296b <+22>:    xor    %eax,%eax
# read two nums from %rdi and 
# save them in an array designated by %rsp
0x000000000000296d <+24>:    lea    0x4(%rsp),%rcx
0x0000000000002972 <+29>:    mov    %rsp,%rdx
0x0000000000002975 <+32>:    lea    0x2d44(%rip),%rsi        # 0x56c0
0x000000000000297c <+39>:    callq  0x2350 <__isoc99_sscanf@plt>
# assert two nums in total
0x0000000000002981 <+44>:    cmp    $0x2,%eax
0x0000000000002984 <+47>:    jne    0x2992 <phase_4+61>      # explode_bomb

# assert rsp[0]>=0
0x0000000000002986 <+49>:    mov    (%rsp),%eax
0x0000000000002989 <+52>:    test   %eax,%eax
0x000000000000298b <+54>:    js     0x2992 <phase_4+61>      # explode_bomb
# assert rsp[0] <= 14
0x000000000000298d <+56>:    cmp    $0xe,%eax
0x0000000000002990 <+59>:    jle    0x2997 <phase_4+66>
0x0000000000002992 <+61>:    callq  0x3105 <explode_bomb>
# call func4(rsp[0], 0, 14)
0x0000000000002997 <+66>:    mov    $0xe,%edx
0x000000000000299c <+71>:    mov    $0x0,%esi
0x00000000000029a1 <+76>:    mov    (%rsp),%edi
0x00000000000029a4 <+79>:    callq  0x291f <func4>
# assert func4(rsp[0], 0, 14) == 0x1b
0x00000000000029a9 <+84>:    cmp    $0x1b,%eax
0x00000000000029ac <+87>:    jne    0x29b5 <phase_4+96>

# assert rsp[1] == 0x1b
0x00000000000029ae <+89>:    cmpl   $0x1b,0x4(%rsp)
0x00000000000029b3 <+94>:    je     0x29ba <phase_4+101>
0x00000000000029b5 <+96>:    callq  0x3105 <explode_bomb>
# check stack protector and exit
0x00000000000029ba <+101>:   mov    0x8(%rsp),%rax
0x00000000000029bf <+106>:   xor    %fs:0x28,%rax
0x00000000000029c8 <+115>:   jne    0x29cf <phase_4+122>
0x00000000000029ca <+117>:   add    $0x18,%rsp
0x00000000000029ce <+121>:   retq   
0x00000000000029cf <+122>:   callq  0x2290 <__stack_chk_fail@plt>

关键在于使得func4(rsp[0], 0, 14) == 0x1b的数字rsp[0],直接通过遍历[0,14]并执行print (int) func4(x, 0, 14)试出来。

(gdb) print (int) func4(9, 0, 14)
27

故答案为:9 27


phase_5

# some initialization
0x00000000000029d4 <+0>:     endbr64 
0x00000000000029d8 <+4>:     sub    $0x18,%rsp
0x00000000000029dc <+8>:     mov    %fs:0x28,%rax
0x00000000000029e5 <+17>:    mov    %rax,0x8(%rsp)
0x00000000000029ea <+22>:    xor    %eax,%eax
# read two nums from %rdi and 
# save them in an array designated by %rsp
0x00000000000029ec <+24>:    lea    0x4(%rsp),%rcx
0x00000000000029f1 <+29>:    mov    %rsp,%rdx
0x00000000000029f4 <+32>:    lea    0x2cc5(%rip),%rsi        # 0x56c0
0x00000000000029fb <+39>:    callq  0x2350 <__isoc99_sscanf@plt>
# assert two nums in total
0x0000000000002a00 <+44>:    cmp    $0x1,%eax
0x0000000000002a03 <+47>:    jle    0x2a36 <phase_5+98>
# rsp[0] = rsp[0] & 0xf (rsp in [0, 15])
0x0000000000002a05 <+49>:    mov    (%rsp),%eax
0x0000000000002a08 <+52>:    and    $0xf,%eax
0x0000000000002a0b <+55>:    mov    %eax,(%rsp)

# ecx = 0, edx = 0
0x0000000000002a0e <+58>:    mov    $0x0,%ecx
0x0000000000002a13 <+63>:    mov    $0x0,%edx
# LOOP BEGIN
# eax = rsp[0]
0x0000000000002a18 <+68>:    mov    (%rsp),%eax
# if eax == 0xf, goto +105
0x0000000000002a1b <+71>:    cmp    $0xf,%eax
0x0000000000002a1e <+74>:    je     0x2a3d <phase_5+105>
# edx += 1
0x0000000000002a20 <+76>:    add    $0x1,%edx
0x0000000000002a23 <+79>:    cltq   
# eax = array[eax]
0x0000000000002a25 <+81>:    lea    0x2954(%rip),%rsi        # 0x5380 <array.3500>
0x0000000000002a2c <+88>:    mov    (%rsi,%rax,4),%eax
# rsp[0] = eax, ecx += eax
0x0000000000002a2f <+91>:    mov    %eax,(%rsp)
0x0000000000002a32 <+94>:    add    %eax,%ecx
0x0000000000002a34 <+96>:    jmp    0x2a18 <phase_5+68>
# LOOP END

0x0000000000002a36 <+98>:    callq  0x3105 <explode_bomb>
0x0000000000002a3b <+103>:   jmp    0x2a05 <phase_5+49>
# assert edx == 0xf
0x0000000000002a3d <+105>:   cmp    $0xf,%edx
0x0000000000002a40 <+108>:   jne    0x2a48 <phase_5+116>
# assert rsp[1] == ecx
0x0000000000002a42 <+110>:   cmp    %ecx,0x4(%rsp)
0x0000000000002a46 <+114>:   je     0x2a4d <phase_5+121>
0x0000000000002a48 <+116>:   callq  0x3105 <explode_bomb>
# check stack protector
0x0000000000002a4d <+121>:   mov    0x8(%rsp),%rax
0x0000000000002a52 <+126>:   xor    %fs:0x28,%rax
0x0000000000002a5b <+135>:   jne    0x2a62 <phase_5+142>
0x0000000000002a5d <+137>:   add    $0x18,%rsp
0x0000000000002a61 <+141>:   retq   
0x0000000000002a62 <+142>:   callq  0x2290 <__stack_chk_fail@plt>

简单理解一下,就是rsp[0]可以保证循环15次,且rsp[1]刚好等于循环结束生成的ecx的值。由于rsp[0] ∈ \in [0,15],我们可以通过遍历来寻找答案。


先把循环的一段翻译成C语言:

rsp[0] = rsp[0] & 0xf;
int ecx = 0, edx = 0;
for (int eax = rsp[0]; eax != 0xf; ++edx) {
    eax = array[eax];
	ecx += eax;
}
assert(edx == 0xf);
assert(ecx == rsp[1]);

然后,我们再读取array数组的内容:

(gdb) x/16wd 0x5380
0x5380 <array.3500>: 10 2 14 7
0x5390 <array.3500+16>: 8 12 15 11
0x53a0 <array.3500+32>: 0 4 1 13
0x53b0 <array.3500+48>: 3 9 6 5

这样,我们就可以在本地上进行通过测试来确定输入的两个数字的值。


#include <stdio.h>

int main()
{
    int array[] = {10, 2, 14, 7,  8, 12, 15, 11, 0, 4, 1, 13, 3, 9, 6, 5};
    for (int rsp0 = 0; rsp0 < 15; ++rsp0) {
        int ecx = 0, edx = 0;
        for (int eax = rsp0; eax != 0xf; ++edx) {
            eax = array[eax];
            ecx += eax;
        }
        if (edx == 15) {
            printf("rsp[0] & 0xf = %x, rsp[1] = %d\n", rsp0, ecx);
        }
    }
}

输出的结果为:“rsp[0] & 0xf = 5, rsp[1] = 115”

所以5 115即为一个答案。


phase_6

查看汇编代码,整整91行😟,跳转了21次(对不起,我再也不会用goto语句了)。

把所有跳转语句指向的地方进行断块处理,一步步分析每一个模块的功能。

第一个检测模块

通过追踪,发现这一部分结构为大循环套小循环,功能为保证每一个数字都属于[1,6]且两两不等。

相应C代码:

int rsp[6];
for (int ebp = 0; ebp < 6; ++ebp) {
    assert(1 <= rsp[ebp] && rsp[ebp] <= 6);
    for (int ebx = ebp + 1; ebx < 6; ++ebx) {
        assert(rsp[ebp] != rsp[ebx]);
    }
}

处理模块

总共有三个部分。

part1
for (int eax = 0; eax < 6; ++eax) {
    rsp[eax] = 7 - rsp[eax];
}

功能:将读取的六个数字变换大小。


part2

再往后将遇到node(链表节点),故先判断node的结构:

(gdb) x/10gx 0x9230
0x9230 <node1>: 0x000000010000021e 0x0000000000009240
0x9240 <node2>: 0x000000020000010e 0x0000000000009250
0x9250 <node3>: 0x00000003000003e2 0x0000000000009260
0x9260 <node4>: 0x000000040000020a 0x0000000000009270
0x9270 <node5>: 0x00000005000003b5 0x0000000000008120
(gdb) x/2gx 0x8120
0x8120 <node6>: 0x000000060000035b 0x0000000000000000

根据名字和内容,推断node的结构为:class node: int value, node *next


node *ptrs[6];
for (int esi = 0; esi < 6; ++esi) {
    node *rdx = &node1;
    for (int eax = 1; eax < rsp[esi]; ++eax) {
        rdx = rdx->next;
    }
    ptrs[esi] = rdx;
}

功能:将ptrs[i]指向node(rsp[i]),举个例子,如果rsp[1]为3,那么ptrs[1]=&node3。


part3
node *rcx = ptrs[0];
for (int eax = 1; eax < 6; ++eax) {
    rcx = (rcx->next = ptrs[eax]);
}
rcx->next = NULL;

功能:结合part2,将node(rsp[i])指向node(rsp[i+1]),node(rsp[5])指向空地址。举个例子,如果读取的六个数字第一个是3,第二个是4,那么rsp[0]=4,rsp[1]=3,则node4将指向node3。


第二个检测模块
for (int ebp = 0; ebp < 5; ++ebp, rbx = rbx->next) {
    assert(rbx->value < rbx->next->value);
}

功能:保证链表值从前往后递增。

由于节点值:3>5>6>1>4>2,所以链表的结构应为:2->4->1->6->5->3->null,所以int rsp[6]={2, 4, 1, 6, 5, 3},则在调整之前int rsp[6]={5, 3, 6, 1, 2, 4}

故答案为:5 3 6 1 2 4


👨‍💻secret_phase

进入

Never could a muggle senses the magic. It’s purely beyond their ken. Do they really think alohomora could open all the doors in the world? No way! Neither would they have any idea that I further secured it with some abracadabra. Mua ha ha ha! Just bother with these most impregnable spells ever!

根据提示以及汇编代码中遇到的奇怪的函数名,可以判断汇编代码中abracadabra(a magical charm or incantation)和alohomora(阿拉霍洞开,一个用于开锁的咒语)为进入的关键。


main函数的结尾部分:

   0x000000000000265b <+370>:   mov    %rax,%rdi
   0x000000000000265e <+373>:   callq  0x2a67 <phase_6>
   0x0000000000002663 <+378>:   mov    %rbx,%rdi
   0x0000000000002666 <+381>:   callq  0x3383 <phase_defused>
   0x000000000000266b <+386>:   mov    %rbx,%rdi
   0x000000000000266e <+389>:   callq  0x2230 <free@plt>
   0x0000000000002673 <+394>:   jmpq   0x253d <main+84>

由于free为库函数,phase_6也已经全部分析过了,二者都不含有特殊入口,因此首要怀疑对象确定为phase_defused函数。


disas phase_defused之后,果然发现了这些函数名:

0x55ac4dff2468 <phase_defused+229>      callq  0x55ac4dff1678 <abracadabra>
0x55ac4dff248b <phase_defused+264>      callq  0x55ac4dff1729 <alohomora>
0x55ac4dff24b1 <phase_defused+302>      callq  0x55ac4dff1bd4 <secret_phase>

而后,通过si, ni大法,可以发现abracadabra是必然会执行的。

观察abracadabra内容,要点也就是:从第phase_2的输入中按照"%d %d %d %d %d %d %s"读取内容,如果发现读取到了最后的字符串,就将读取到的字符串与"NothingThatHasMeaning1sEasy…"(凡是有意义的事都不会容易…)进行比对,如果比对成功,就返回1。(也就是说正常情况下这个函数都是返回0的。)

得到返回值为1的凭证之后,我们就能进入到alohomora中。


alohomora的功能为:从phase_4的输入中按照"%d %d %s"的格式读取内容,如果发现读取到了最后的字符串,就把字符串中每一个char都加2,最后与"000Gcu{FqgupvGpvgt3pvqItqypWrNkhg0"进行比对,如果相同,就返回1。

对于"000Gcu{FqgupvGpvgt3pvqItqypWrNkhg0",将其每一个字符都减2,便得到第二处密文:“…EasyDoesntEnter1ntoGrownUpLife.”(成年人的生活里没有容易二字。)

因此,我们分别在第二处和第四处补加上这两处密码,最后就能进入到secret_phase中。


破译

主体内容就是三部分:读取字符串并转为整数,确保1<=x<=1001且fun7(n1, x) == 4,以及最后的恭喜。

在fun7中我们遇到了新的数据类型:二叉树。

(gdb) x/28gx 0x9150
0x9150 <n1>: 0x0000000000000024 0x0000000000009170
0x9160 <n1+16>: 0x0000000000009190 0x0000000000000000

0x9210 <n34>: 0x000000000000006b 0x0000000000008060
0x9220 <n34+16>: 0x0000000000008100 0x0000000000000000

观察其中每一个元素都占用了32byte空间,且最后8个byte均为0。猜测是alignment。

第二第三个8-byte很明显是指针,第一个8-byte包含了一个int或long(short,char…)。


发现部分指针(0x8080等)指向的节点未知,果然漏掉了部分节点:

(gdb) x/20gx 0x8080
0x8080 <n44>: 0x0000000000000023 0x0000000000000000
0x8090 <n44+16>: 0x0000000000000000 0x0000000000000000

0x8100 <n48>: 0x00000000000003e9 0x0000000000000000
0x8110 <n48+16>: 0x0000000000000000 0x0000000000000000

推断:class node: int val, node *llink, *rlink


fun7是递归函数,内容很少,可以直接写出C代码:

int fun7(node *n, int x)
{
    if (n == NULL) {
        return -1;
    }
    if (n->val > x) {
        return 2 * fun7(n->llink, x);
    } else if (tree[id] < x) {
        return 2 * fun7(n->rlink, x) + 1;
    } else {
        return 0;
    }
}

因此我们直接本地遍历:

int tree[] = { 0x024, 0x008, 0x032, 0x006, 0x016, 0x02d, 0x06b, 0x001, 0x007,
0x014, 0x023, 0x028, 0x02f, 0x063, 0x3e9 };
int NUM = 15;

int fun7(int id, int x)
{
    if (id >= NUM) {
        return -1;
    }
    if (tree[id] > x) {
        return 2 * fun7(2 * id + 1, x);
    } else if (tree[id] < x) {
        return 2 * fun7(2 * id + 2, x) + 1;
    } else {
        return 0;
    }
}

int main()
{
    for (int x = 1; x <= 1001; ++x) {
        if (fun7(0, x) == 4) {
            printf("%d\n", x);
        }
    }
}

输出的结果只有:7,即为最终密码。


完结撒花🎉🎉🎉

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值