Zj_W1nd's BLOG

CSAPP Labs——Bomb Lab

2024/06/06

介绍

https://hansimov.gitbook.io/csapp/labs/bomb-lab

年轻人的第一道逆向题目,目标是通过逆向分析出正确的输入从而让程序正确运行,拆除炸弹!
由于已经有过简单的linux逆向经验,这里我们不再过多强调要求,直接扔进IDA开搞。其实实验给出了main函数的源码文件作为提示,但是我们其实并不需要(本身main函数也没有什么复杂的逻辑,是一个线性的)

内容:

运行程序,随便输入点什么直接爆了,扔进IDA进行分析。给出的程序把符号表留下了,函数名都留着就很方便。能看到程序允许加一个文件参数然后依据文件内容自动执行,我们有pwntools所以不用管。程序每次调用一个readline然后有六个关卡(隐藏关最后再说)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
initialize_bomb();
puts("Welcome to my fiendish little bomb. You have 6 phases with");
puts("which to blow yourself up. Have a nice day!");
line = (char *)read_line();
phase_1(line);
phase_defused();
puts("Phase 1 defused. How about the next one?");
v4 = read_line();
phase_2((__int64)v4);
phase_defused();
puts("That's number 2. Keep going!");
v5 = read_line();
phase_3((__int64)v5);
phase_defused();
puts("Halfway there!");
v6 = read_line();
phase_4((__int64)v6);
phase_defused();
puts("So you got that one. Try this one.");
v7 = read_line();
phase_5((__int64)v7);
phase_defused();
puts("Good work! On to the next...");
v8 = read_line();
phase_6((__int64)v8);
phase_defused();
return 0;

phase1

跟进phase1,发现就是一个字符串比较。由于函数命名保留了直接这关就过了。输入就是这个串。

1
2
3
4
result = strings_not_equal(a1, "Border relations with Canada have never been better.");
if ( (_DWORD)result )
explode_bomb();
return result;

我们也可以跟进一下strings_not_equal(),确实是一个字符串比较函数。这里也附上手动实现的stringlength。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
__int64 __fastcall string_length(_BYTE *a1)
{
_BYTE *v1; // rdx
__int64 result; // rax

if ( !*a1 )
return 0LL;
v1 = a1;
do
result = (unsigned int)((_DWORD)++v1 - (_DWORD)a1);
while ( *v1 );
return result;
}

__int64 __fastcall strings_not_equal(_BYTE *a1, _BYTE *a2)
{
_BYTE *char_ptr_input; // rbx
_BYTE *char_ptr_key; // rbp
int input_len; // r12d
int key_len; // eax
unsigned int v6; // edx

char_ptr_input = a1;
char_ptr_key = a2;
input_len = string_length(a1);
key_len = string_length(a2);
v6 = 1;
if ( input_len == key_len )
{
if ( *a1 )
{
if ( *a1 == *a2 )
{
do
{
++char_ptr_input;
++char_ptr_key;
if ( !*char_ptr_input )
return 0; // 正确出口,比较的两字符串都相同返回0
}
while ( *char_ptr_input == *char_ptr_key );
return 1;
}
else
return 1;
}
else
return 0; // 如果输入\0也能return0?
}
return v6;
}

phase2

phase2是读六个数然后比较,IDA优化下数据类型后就是下面这样。要求读入六个数以空格分割,然后第一个数是1,并且后一个数是前一个的两倍,即输入1 2 4 8 16 32.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall phase_2(__int64 src)
{
__int64 result; // rax
int *ptr; // rbx
int arr[6]; // [rsp+0h] [rbp-38h] BYREF
char end; // [rsp+18h] [rbp-20h] BYREF

read_six_numbers(src, arr);
if ( arr[0] != 1 )
((void (__fastcall __noreturn *)(__int64, int *))explode_bomb)(src, arr);
ptr = &arr[1]; // 分配栈上数组,读入6个数,满足依次*2并且第一个是1就通过。也就是1 2 4 8 16 32
do
{
result = (unsigned int)(2 * *(ptr - 1)); // 这里不如直接看汇编,很清楚
if ( *ptr != (_DWORD)result )
((void (__fastcall __noreturn *)(__int64, int *))explode_bomb)(src, arr);
++ptr; // ++ next one
}
while ( ptr != (int *)&end );
return result;
}

直接看汇编也可以:

1
2
3
4
5
6
7
...
mov eax, [rbx-4]
add eax, eax ;二倍后比较
cmp [rbx], eax
jz short loc_400F25
call explode_bomb
...

其实我们也可以看下read_six_numbers,这玩意是用sscanf实现的绝对安全输入:

1
2
3
4
5
6
7
8
__int64 __fastcall read_six_numbers(__int64 src, int *a2)
{
__int64 result; // rax
result = __isoc99_sscanf(src, "%d %d %d %d %d %d", a2, a2 + 1, a2 + 2, a2 + 3, a2 + 4, a2 + 5);
if ( (int)result <= 5 )
explode_bomb();
return result;
}

phase3

强大的IDA!

这关是一个switch跳表,但是IDA直接识别了switch结构直接也是一眼顶针了。输入第一个数选择switch分支,然后只要第二个数和分支的result匹配即可。我选择输入0 207

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
if ( (int)__isoc99_sscanf(a1, "%d %d", &v2, &v3) <= 1 )
explode_bomb();
switch ( v2 ) // IDA伟大,一个简单的switch跳表结构
{
case 0:
result = 207LL;
break;
case 1:
result = 311LL;
break;
case 2:
result = 707LL;
break;
case 3:
result = 256LL;
break;
case 4:
result = 389LL;
break;
case 5:
result = 206LL;
break;
case 6:
result = 682LL;
break;
case 7:
result = 327LL;
break;
default:
explode_bomb();
}
if ( (_DWORD)result != v3 )
explode_bomb();

phase4

这里开始就没有那么简单了。看看phase4的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall phase_4(__int64 a1)
{
__int64 result; // rax
unsigned int v2; // [rsp+8h] [rbp-10h] BYREF
int v3; // [rsp+Ch] [rbp-Ch] BYREF

if ( (unsigned int)__isoc99_sscanf(a1, "%d %d", &v2, &v3) != 2 || v2 > 0xE )// V2<14
explode_bomb();
result = func4(v2, 0LL, 14LL);
if ( (_DWORD)result || v3 ) // 同时取0
explode_bomb();
return result;
}

传入两个数,第二个要求是0,第一个作为参数传入func4并要求func4最终返回0。我们看看func4是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall func4(int a1, int a2, int a3)
{
int v3; // ecx
__int64 result; // rax

v3 = (a3 - a2) / 2 + a2; // 每次递归逼近a1
if ( v3 > a1 )
return 2 * (unsigned int)func4(a1, a2, v3 - 1);
result = 0LL;
if ( v3 < a1 )
return 2 * (unsigned int)func4(a1, v3 + 1, a3) + 1;
return result; // 注意到没有给出v3=a1的逻辑分支,也就是说递归的终点会是v3=a1
// 然后逐步返回2的幂次。传0试试过了
}

func4是一个递归函数,三个参数。我们自己传入的参数1会在每次递归时保留不变,a2和a3分别初值传入0和14.然后再每次递归的时候,会以当前的a2和a3取一个“中间值”,然后将中间值和a1比较,根据比较结果继续递归。我们发现每次判断v3大于a1继续递归,v3小于a1也继续递归,但是没有给出v3=a1的逻辑分支,也就是说递归终点一定是v3=a1,然后从0返回。
俗话说三分逆向七分猜,直接看肯定是看不出正确输入的。这里大概的感觉是a2和a3不断的向a1夹逼。这里其实就可以猜测了,我们拿python写个一样的脚本进行尝试,结果输入0直接返回就是0,因此这关输入0 0即可。

其实仔细分析,我们只要保证一直维持在v3>a1的分支,最后返回的结果一定是0(因为没有任何从0到1的步骤)。因此0并不是唯一解。尝试了下0,1,3都可以。

phase5

代换密码。输入长度要求是6,最后代换过的字符串是"flyers"即可。对数据结构和数据类型修复之后得到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 __fastcall phase_5(char *a1)
{
__int64 i; // rax
char v3[8]; // [rsp+10h] [rbp-18h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-10h]

v4 = __readfsqword(0x28u);
if ( (unsigned int)string_length(a1) != 6 )
explode_bomb();
for ( i = 0LL; i != 6; ++i )
v3[i] = sbox[a1[i] & 0xF]; // 代换(S盒?)
v3[6] = 0;
if ( (unsigned int)strings_not_equal(v3, "flyers") )// 偏移:9 15(F) 14(E) 5 6 7
explode_bomb(); // ASCII: IONEFG
return __readfsqword(0x28u) ^ v4;
}

对于输入的字符,我们只取低4字节0-15用来在sbox中索引代换目标。数据段的sbox如下(非数组形式会显示对应的ascii字符):

1
2
3
4
.rodata:00000000004024B0 ; _BYTE sbox[16]
.rodata:00000000004024B0 sbox db 6Dh, 61h, 64h, 75h, 69h, 65h, 72h, 73h, 6Eh, 66h, 6Fh
.rodata:00000000004024B0 ; DATA XREF: phase_5+37↑r
.rodata:00000000004024BB db 74h, 76h, 62h, 79h, 6Ch

查表可得我们需要的flyers是 9 15 14 5 6 7,因此看ASCII表,找低4位对应的字母输入即可。这里选择输入IONEFG(0x49,0x4F,0x4E,0x45,0x46,0x47)。这里用字母可以确定是ASCII,用标点符号的话可能会由于编码问题导致失败。

phase6

最难的一集。1打5说是。下面的IDA代码都是手动修复过变量类型、名字和结构体的,可放心食用。

输入检查

打开函数,逻辑稍有一点复杂,一点点看。首先还是读6个数到栈上,第一个循环内检查所有的输入,每个数都要小于等于6并且通过嵌套循环检查是不是六个不同的数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
nums_ptr = input_nums;
read_six_numbers(input_line, input_nums);
idx = 0;
while ( 1 )
{
if ( (unsigned int)(*nums_ptr - 1) > 5 ) // 两层循环,外层循环推进指针,这里要求每个数都小于等于6
explode_bomb();
if ( ++idx == 6 )
break;
idx_cpy = idx;
do // 内层循环,检查后面所有的数是不是和标记的数一样,有一样的直接爆
{ // 我们反正输入6个不同的数就行
if ( *nums_ptr == input_nums[idx_cpy] )
explode_bomb();
++idx_cpy;
}
while ( idx_cpy <= 5 );
++nums_ptr;
}

输入处理

接下来,将所有的输入对7取补

1
2
3
4
5
6
7
arr_ptr = input_nums;
do
{
*arr_ptr = 7 - *arr_ptr; // 对每个数对7取补
++arr_ptr;
}
while ( arr_ptr != (int *)&arr_end );

链表指针数组初始化

下一块注意到从数据段读东西了,名字还叫node,跟进看下结果好家伙,数据段存了六个链表的节点。(node1->node2->node3->node4->node5->node6->0x0)

IDA数据类型改qword自动识别出偏移了,而且这六个是连续存储的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.data:00000000006032D0                 public node1
.data:00000000006032D0 node1 dd 332 ; data
.data:00000000006032D0 ; DATA XREF: phase_6:loc_401183↑o
.data:00000000006032D0 ; phase_6+B0↑o
.data:00000000006032D4 db 1, 3 dup(0)
.data:00000000006032D8 dq 6032E0h ; next
.data:00000000006032E0 public node2
.data:00000000006032E0 node2 dd 168 ; data
.data:00000000006032E4 db 2, 3 dup(0)
.data:00000000006032E8 dq 6032F0h ; next
.data:00000000006032F0 public node3
.data:00000000006032F0 node3 dd 924 ; data
.data:00000000006032F4 db 3, 3 dup(0)
.data:00000000006032F8 dq 603300h ; next
.data:0000000000603300 public node4
.data:0000000000603300 node4 dd 691 ; data
.data:0000000000603304 db 4, 3 dup(0)
.data:0000000000603308 dq 603310h ; next
.data:0000000000603310 public node5
.data:0000000000603310 node5 dd 477 ; data
.data:0000000000603314 db 5, 3 dup(0)
.data:0000000000603318 dq 603320h ; next
.data:0000000000603320 public node6
.data:0000000000603320 node6 dd 443 ; data
.data:0000000000603324 db 6, 3 dup(0)
.data:0000000000603328 dq 0 ; next

然后看代码,这里是对结构体和指针做了优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for ( i = 0LL; i != 6; ++i )
{
num = input_nums[i]; // 根据输入的6个数按对应的node初始化链表
if ( num <= 1 )
{
cur1 = &node1;
}
else // 进入else是根据输入数(7-input)取对应node
{
idx1 = 1;
cur1 = &node1;
do
{
cur1 = (node *)cur1->next; // v6=v6->next
++idx1;
}
while ( idx1 != num );//该循环用于取到对应node,单向链表
}
nodeptr_arr[i] = cur1; // 写入栈上的结构体指针数组,最后一个元素是0x0
}

对7取补后根据输入的数,从data段选对应的node放在栈上的结构体指针数组里面

链表连接

下面这段由于反汇编f5的误差问题看了好久,它在一个关键的下标位置出错了,动调辅助理解的。不能全信IDA。

这段开始由于IDA加自己的问题没看懂,于是起gdb动调了一下。结果发现这里是按照我们栈上的顺序,将链表结点的next域刷新一遍。

1
2
3
4
5
6
7
8
9
10
cur = nodeptr_arr[0];
next2 = &nodeptr_arr[1];
for ( node2 = nodeptr_arr[0]; ; node2 = tmp_node )
{
tmp_node = *next2;
node2->next = (__int64)*next2++; //唯一的写,刷一遍next域,按照栈上的顺序
if ( next2 == (node **)&endarr )
break;
}
tmp_node->next = 0LL; // 最后一项next置零

gdb动调输入123456后,发现data段原本的链接1->2->3…->6被改成了6->…->1,结合这段代码可以知道,这里是按我们输入的顺序将next域刷了一遍。

最后判断:

1
2
3
4
5
6
7
8
9
do
{
next_node = *(unsigned int *)cur->next;
if ( cur->data < (int)next_node ) // 四字节小端数据
explode_bomb();
cur = (node *)cur->next;
--cnt_to_5;
}
while ( cnt_to_5 );

对我们搭建的链表进行判断,我们需要这个链表前一项的data一直比后一项大(注意这里的(int)next_node实质上是取了下一个node的data域)。而且这里有一个小陷阱,比较的是int类型,是四字节的数据,而data段的node中存的是qword,按4字节小端读一下之后node1~6依次是332,168,924,691,477,443。也就是说我们要从大到小的话需要按照3->4->5->6->1->2的顺序连接,结合前面的对7取补,因此我们最后要输入4 3 2 1 6 5。

secret phase

触发条件

每次拆除结束后,都会调用一个phase_defused()函数,跟进发现有一个secret phase。看一下触发条件,需要input_strings这个量是6且用sscanf对一个莫名其妙的地址读两个数字加一个字符串,然后字符串要是"DrEvil"。

1
2
3
4
5
6
7
8
9
10
11
if ( num_input_strings == 6 )
{
if ( (unsigned int)__isoc99_sscanf(&file_input_603870, "%d %d %s", &v1, &v2, v3) == 3
&& !(unsigned int)strings_not_equal(v3, "DrEvil") )
{
puts("Curses, you've found the secret phase!");
puts("But finding it and solving it are quite different...");
secret_phase();
}
puts("Congratulations! You've defused the bomb!");
}

先交叉引用num_input_strings,定位read_line函数。仔细检查发现了真正的读取函数skip(),这个函数内部会从预定的输入流(stdin或者文件)循环读取80字节放入bss。而0x603780这个地址在加240后恰好就是上面secret_phase要取内容的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 从infile读80个字符到0x603870
char *skip()
{
char *v0; // rax
char *v1; // rbx
do
{
v0 = fgets((char *)(80LL * num_input_strings + 0x603780), 80, infile);// 恰好在3次输入后指向0x603870
v1 = v0;
}
while ( v0 && (unsigned int)blank_line(v0) );
return v1;
}

const char *read_line()
{
int v0; // edx
const char *file_content; // rsi
unsigned __int64 content_len; // kr08_8
int v3; // eax
__int64 v4; // rax

if ( !(__int64)skip() )
{
if ( infile == (FILE *)stdin )
{
puts("Error: Premature EOF on stdin");
exit(8);
}
if ( getenv("GRADE_BOMB") )
exit(0);
infile = (FILE *)stdin;
if ( !(__int64)skip() )
{
puts("Error: Premature EOF on stdin");
exit(0);
}
}
v0 = num_input_strings;
file_content = (const char *)(80LL * num_input_strings + 0x603780);
content_len = strlen(file_content) + 1;
if ( (int)content_len - 1 > 78 ) // 80个或以上的字符
{
puts("Error: Input line too long");
v3 = num_input_strings++;
v4 = 10LL * v3;
input_strings[v4] = 0x636E7572742A2A2ALL;
qword_603788[v4] = 0x2A2A2A64657461LL;
explode_bomb();
}
*((_BYTE *)&input_strings[10 * num_input_strings - 1] + (int)content_len + 6) = 0;
num_input_strings = v0 + 1;
return file_content;
}

gdb动调一下确定num_input_strings代表输入的次数(动态确定会比较方便),并且正常跑一趟流程发现0x603870存的是0 0,也就是我们phase4的输入。因此确定触发secret_phase的条件是在phase4输入"0 0 DrEvil"

解决

看看是个什么问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned __int64 secret_phase()
{
const char *line; // rax
int v1; // ebx

line = read_line();
v1 = strtol(line, 0LL, 10);
if ( (unsigned int)(v1 - 1) > 1000 )
explode_bomb();
if ( (unsigned int)fun7((struct jmp_num *)&n1, v1) != 2 )
explode_bomb();
puts("Wow! You've defused the secret stage!");
return phase_defused();
}

接着读一次输入,我们要输入一个1000以内的十进制数,然后传入func7并且要求func7返回2。func7又有一个设定好的参数。都跟进看看。

首先看这个n1,取qword一看,又是结构体。从连续存储的数据段看,这是一个二叉树:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
.data:00000000006030F0                 public n1
.data:00000000006030F0 n1 dd 36 ; DATA XREF: secret_phase+2C↑o
.data:00000000006030F4 db 0
.data:00000000006030F5 db 0
.data:00000000006030F6 db 0
.data:00000000006030F7 db 0
.data:00000000006030F8 dq offset n21
.data:0000000000603100 dq offset n22
.data:0000000000603108 dq 0
.data:0000000000603110 public n21
.data:0000000000603110 n21 dd 8 ; DATA XREF: .data:00000000006030F8↑o
.data:0000000000603114 db 0
.data:0000000000603115 db 0
.data:0000000000603116 db 0
.data:0000000000603117 db 0
.data:0000000000603118 dq offset n31
.data:0000000000603120 dq offset n32
.data:0000000000603128 dq 0
.data:0000000000603130 public n22
.data:0000000000603130 n22 dd 50 ; DATA XREF: .data:0000000000603100↑o
.data:0000000000603134 db 0
.data:0000000000603135 db 0
.data:0000000000603136 db 0
.data:0000000000603137 db 0
.data:0000000000603138 dq offset n33
.data:0000000000603140 dq offset n34
.data:0000000000603148 dq 0
.data:0000000000603150 public n32
.data:0000000000603150 n32 dd 22 ; DATA XREF: .data:0000000000603120↑o
.data:0000000000603154 db 0
.data:0000000000603155 db 0
.data:0000000000603156 db 0
.data:0000000000603157 db 0
.data:0000000000603158 dq offset n43
.data:0000000000603160 dq offset n44
.data:0000000000603168 dq 0
.data:0000000000603170 public n33
.data:0000000000603170 n33 dd 45 ; DATA XREF: .data:0000000000603138↑o
.data:0000000000603174 db 0
.data:0000000000603175 db 0
.data:0000000000603176 db 0
.data:0000000000603177 db 0
.data:0000000000603178 dq offset n45
.data:0000000000603180 dq offset n46
.data:0000000000603188 dq 0
.data:0000000000603190 public n31
.data:0000000000603190 n31 dd 6 ; DATA XREF: .data:0000000000603118↑o
.data:0000000000603194 db 0
.data:0000000000603195 db 0
.data:0000000000603196 db 0
.data:0000000000603197 db 0
.data:0000000000603198 dq offset n41
.data:00000000006031A0 dq offset n42
.data:00000000006031A8 dq 0
.data:00000000006031B0 public n34
.data:00000000006031B0 n34 dd 107 ; DATA XREF: .data:0000000000603140↑o
.data:00000000006031B4 db 0
.data:00000000006031B5 db 0
.data:00000000006031B6 db 0
.data:00000000006031B7 db 0
.data:00000000006031B8 dq offset n47
.data:00000000006031C0 dq offset n48
.data:00000000006031C8 dq 0
...

后面的就不放了,nxy似乎能理解成第几层的第几个节点。然后看func7:

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall fun7(struct jmp_num *a1, int a2)
{
__int64 result; // rax
// 爆破!
if ( !a1 )
return 0xFFFFFFFFLL;
if ( a1->data > a2 )
return 2 * (unsigned int)fun7(a1->next1, a2);
result = 0LL;
if ( a1->data != a2 )
return 2 * (unsigned int)fun7(a1->next2, a2) + 1;
return result;
}

又是递归。从根节点开始,如果我们输的数比节点存的数小就以2*func的形式递归,然后如果不等于的话就以2*func+1的形式递归。同样找递归出口,最后是要通过两个if判断然后返回0.

关键来了,要想pass第二个if,这就意味着我们的输入在最后一次递归一定等于某个节点存储的数据。因此正确结果一定就是二叉树中的某一个节点的data。一共也就15个数,排除最后一个1001,我们直接进行爆破拆弹()最后得到正确结果是22.

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# b'BOOM!!!\n'
# 36
# [*] Stopped process './bomb' (pid 166)
# [+] Starting local process './bomb': pid 168
# b'BOOM!!!\n'
# 8
# [*] Stopped process './bomb' (pid 168)
# [+] Starting local process './bomb': pid 170
# b'BOOM!!!\n'
# 50
# [*] Stopped process './bomb' (pid 170)
# [+] Starting local process './bomb': pid 172
# b"Congratulations! You've defused the bomb!\n"
# 22
# [*] Stopped process './bomb' (pid 172)
# [+] Starting local process './bomb': pid 174
# b'BOOM!!!\n'
# 45
# [*] Stopped process './bomb' (pid 174)
# [+] Starting local process './bomb': pid 176
# b'BOOM!!!\n'
# 6
# [*] Stopped process './bomb' (pid 176)
# [+] Starting local process './bomb': pid 178
# b'BOOM!!!\n'
# 107
# [*] Stopped process './bomb' (pid 178)
# [+] Starting local process './bomb': pid 180
# b'BOOM!!!\n'
# 40
# [*] Stopped process './bomb' (pid 180)
# [+] Starting local process './bomb': pid 182
# b'BOOM!!!\n'
# 1
# [*] Stopped process './bomb' (pid 182)
# [+] Starting local process './bomb': pid 184
# b'BOOM!!!\n'
# 99
# [*] Stopped process './bomb' (pid 184)
# [+] Starting local process './bomb': pid 186
# b'BOOM!!!\n'
# 23
# [*] Stopped process './bomb' (pid 186)
# [+] Starting local process './bomb': pid 188
# b'BOOM!!!\n'
# 7
# [*] Stopped process './bomb' (pid 188)
# [+] Starting local process './bomb': pid 190
# b'BOOM!!!\n'
# 14
# [*] Stopped process './bomb' (pid 190)
# [+] Starting local process './bomb': pid 192
# b'BOOM!!!\n'
# 47
# [*] Stopped process './bomb' (pid 192)

总结

年轻人的第一道逆向题目。这道题虽然有linux逆向经验的话并不困难,但是通过这个lab我使用IDA分析结构体的水平极大提升了。
success_bomb_lab.png

附EXP和i64文件

反汇编结果
bomb.i64
secret.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
from pwn import *

context.log_level = 'info'
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"]
# 没有删除符号表,函数功能命名清晰直接看就行

def phase_1():
# phase_1是简单的字符串比较,key是明文存储,反汇编直接输入就行
# 疑似传入\0也会return 0
p.recv()
p.sendline("Border relations with Canada have never been better.")

def phase_2():
# phase_2是在栈上分配一个数组读6个数,满足依次*2并且第一个是1就通过
# 看汇编比F5清楚(有一步add eax, eax)
p.recv()
p.sendline("1 2 4 8 16 32")

def phase_3():
# phase_3是一个switch跳表,直接IDA F5,IDA伟大
p.recv()
p.sendline("0 207")

def phase_4():
# phase_4里面有一个func4,是递归的。出口要求func4返回0
# 每次递归逼近a1,注意到没有给出v3=a1的逻辑分支,也就是说递归的终点会是v3=a1
# 然后逐步返回2的幂次。传0试试过了
p.recv()
p.sendline("0 0 DrEvil")

def phase_5():
# S盒代换,比较输入和“flyers"这个串
# 看数据确定偏移,它只取了低4位(0xF与了)在ASCII查字母输入即可
p.recv()
p.sendline("IONEFG")

def phase_6():
# 有些抽象,这里是对一个预定义在data段的链表进行操作,输入6个数但是不能相同
# 首先,预存了一个结构体指针数组。按照输入的6个数对7的补(7-x),从预定义的7个结构体进行初始化(1->2->3->4->5->6->0x0)
# 假如输入(1 2 3 4 5 6),栈上的结构体数组就对应node(6 5 4 3 2 1)
# 1-6:332,168,924,691,477,443,要想每一个都比下一个大,需要顺序345612,输入432165
p.recv()
#gdb.attach(p)
p.sendline("4 3 2 1 6 5")

def secret_phase(num):
# answer: 22
num=str(num)
p.recv()
p.sendline(num)

num_list=[36,8,50,22,45,6,107,40,1,99,23,7,14,47]
# result: 22
for i in range(14):
p=process("./bomb")
phase_1()
phase_2()
phase_3()
phase_4()
phase_5()
phase_6()
secret_phase(num_list[i])
p.recvline()
result=p.recvline()
print(result)
print(f"{num_list[i]}")
p.close()

# b'BOOM!!!\n'
# 36
# [*] Stopped process './bomb' (pid 166)
# [+] Starting local process './bomb': pid 168
# b'BOOM!!!\n'
# 8
# [*] Stopped process './bomb' (pid 168)
# [+] Starting local process './bomb': pid 170
# b'BOOM!!!\n'
# 50
# [*] Stopped process './bomb' (pid 170)
# [+] Starting local process './bomb': pid 172
# b"Congratulations! You've defused the bomb!\n"
# 22
# [*] Stopped process './bomb' (pid 172)
# [+] Starting local process './bomb': pid 174
# b'BOOM!!!\n'
# 45
# [*] Stopped process './bomb' (pid 174)
# [+] Starting local process './bomb': pid 176
# b'BOOM!!!\n'
# 6
# [*] Stopped process './bomb' (pid 176)
# [+] Starting local process './bomb': pid 178
# b'BOOM!!!\n'
# 107
# [*] Stopped process './bomb' (pid 178)
# [+] Starting local process './bomb': pid 180
# b'BOOM!!!\n'
# 40
# [*] Stopped process './bomb' (pid 180)
# [+] Starting local process './bomb': pid 182
# b'BOOM!!!\n'
# 1
# [*] Stopped process './bomb' (pid 182)
# [+] Starting local process './bomb': pid 184
# b'BOOM!!!\n'
# 99
# [*] Stopped process './bomb' (pid 184)
# [+] Starting local process './bomb': pid 186
# b'BOOM!!!\n'
# 23
# [*] Stopped process './bomb' (pid 186)
# [+] Starting local process './bomb': pid 188
# b'BOOM!!!\n'
# 7
# [*] Stopped process './bomb' (pid 188)
# [+] Starting local process './bomb': pid 190
# b'BOOM!!!\n'
# 14
# [*] Stopped process './bomb' (pid 190)
# [+] Starting local process './bomb': pid 192
# b'BOOM!!!\n'
# 47
# [*] Stopped process './bomb' (pid 192)

CATALOG
  1. 1. 介绍
  2. 2. 内容:
    1. 2.1. phase1
    2. 2.2. phase2
    3. 2.3. phase3
    4. 2.4. phase4
    5. 2.5. phase5
    6. 2.6. phase6
      1. 2.6.1. 输入检查
      2. 2.6.2. 输入处理
      3. 2.6.3. 链表指针数组初始化
      4. 2.6.4. 链表连接
      5. 2.6.5. 最后判断:
    7. 2.7. secret phase
      1. 2.7.1. 触发条件
      2. 2.7.2. 解决
  3. 3. 总结
  4. 4. 附EXP和i64文件