VNCTF2025-Writeup-Reverse

本文最后更新于 2025年2月10日 上午

VNCTF2025 Writeup Reverse

写在最前面

“但是太阳,它每时每刻都是夕阳也都是旭日。当它熄灭着走下山去收尽苍凉残照之际,正是它在另一面燃烧着爬上山巅布散烈烈朝辉之时。”

好久没有更新博客了,在不断的被否定与碰壁中艰难行走,VNCTF2025交出了自己接触CTF以来的最完美的答卷,虽然没有AK,但赛后也努力解出了。感谢一路上给予我指点的师傅和陪伴我的朋友们,我相信我会越来越好。

Just reverse it!

fish_hook

jadx看逻辑

image-20250208234806158

提示联网再钓鱼,又有个download hook_fish.dex,字符串搜一下

image-20250208234853691

搜到一个地址http://47.121.211.23/hook_fish.dex

下载hook_fish.dex

继续审main的逻辑

image-20250208235030116

先对每个字符+68,再转成hex, 再进行递归的相邻换位(index:0<——>1,2<——>3这样换),

异或换位相当于:

1
2
3
4
5
6
7
8
9
10
11
void code(char *a, int index, int len) 
{
if (index >= len - 1)
{
return;
}
char temp = a[index];
a[index] = a[index + 1];
a[index + 1] = temp;
code(a, index + 2, len);
}

再进行一个加密

1
2
3
4
5
6
7
8
9
10
11
12
for (int i2 = 0; i2 < str3.length; i2++)
{
if (str3[i2] >= 'a' && str3[i2] <= 'f')
{
str3[i2] = (char) ((str3[i2] - '1') + (i2 % 4));
}
else
{
str3[i2] = (char) (str3[i2] + '7' + (i2 % 10));
}
}

最后进了fish_hook.hex

jadx分析fish_hook.hex

image-20250208235345483

给了一个码表然后对应转化成

1
jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji

思路是先查表还原对应回去,然后爆破一下加密,然后换位回去之后转回字符再-68即可

exp如下:

fish1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class FishDecoder:
def __init__(self):
self.fish_dcode = {
"iiijj": 'a', "jjjii": 'b', "jijij": 'c', "jjijj": 'd', "jjjjj": 'e',
"ijjjj": 'f', "jjjji": 'g', "iijii": 'h', "ijiji": 'i', "iiiji": 'j',
"jjjij": 'k', "jijji": 'l', "ijiij": 'm', "iijji": 'n', "ijjij": 'o',
"jiiji": 'p', "ijijj": 'q', "jijii": 'r', "iiiii": 's', "jjiij": 't',
"ijjji": 'u', "jiiij": 'v', "iiiij": 'w', "iijij": 'x', "jjiji": 'y',
"jijjj": 'z', "iijjl": '1', "iiilj": '2', "iliii": '3', "jiili": '4',
"jilji": '5', "iliji": '6', "jjjlj": '7', "ijljj": '8', "iljji": '9',
"jjjli": '0'
}

def decode(self, cipher_text):
chunks = [cipher_text[i:i+5] for i in range(0, len(cipher_text), 5)]
return ''.join(self.fish_dcode.get(chunk, '?') for chunk in chunks)

decoder = FishDecoder()
cipher_text = "jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji"
plain_text = decoder.decode(cipher_text)
print(plain_text)

得到:0qksrtuw0x74r2n3s2x3ooi4ps54r173k2os12r32pmqnu73r1h432n301twnq43prruo2h5

fish2.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
#include<stdio.h>
int main()
{
char s[72];
char t[]="0qksrtuw0x74r2n3s2x3ooi4ps54r173k2os12r32pmqnu73r1h432n301twnq43prruo2h5";
int p;
for(int i=0;i<72;i++)
{
for(int b=40;b<=128;b++)
{
if(b >= 'a' && b <= 'f')
p= (b-'1') + (i% 4);
else
p= (b+'7')+ (i% 10);
if(p==t[i])
{
s[i]=b;
break;
}
}
}
for (int i =71; i > 0; i -= 2)
{
char temp = s[i];
s[i] = s[i-1];
s[i-1]=temp;
}
for (int i =0;i<72;i++)
{
printf("%c", s[i]);
}
return 0;
}

得到:9a9287988abfb9a3b6a978b075bda3afb274bba38c7493afa3b1bda3aa7597ac6575b0c1

fish3.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
int main()
{
int d[]={
0x9a, 0x92, 0x87, 0x98, 0x8a, 0xbf, 0xb9, 0xa3, 0xb6, 0xa9,
0x78, 0xb0, 0x75, 0xbd, 0xa3, 0xaf, 0xb2, 0x74, 0xbb, 0xa3,
0x8c, 0x74, 0x93, 0xaf, 0xa3, 0xb1, 0xbd, 0xa3, 0xaa, 0x75,
0x97, 0xac, 0x65, 0x75, 0xb0, 0xc1};
for(int i=0;i<36;i++)
{
d[i]-=68;
printf("%c",d[i]);
}
return 0;
}

拆开之后-68运行即可

得到flag:

1
VNCTF{u_re4l1y_kn0w_H0Ok_my_f1Sh!1l}

Fuko’s starfish

一堆反调试去不干净,没办法进行动调,硬着头皮运行程序

image-20250209105435245

先猜数,二分直接过掉

2222222

然后是个贪吃蛇,玩不过去

image-20250209105738562

ida找到贪吃蛇的逻辑发现是score达到23过关

image-20250209110113298

直接patch一下改到0

image-20250209110317426

image-20250209110336842

这样就直接过了第二关,看到如下提示

image-2025020911044442

去审贪吃蛇胜利之后的逻辑看到函数sub_1800025F0

直接反编译会出问题,有call $5指令,全部nop

image-20250209110940585

前面有字符串输出,是加密字符串后(xor 0x17)再输出的

image-20250209111039936

image-20250209111128334

赛博厨子证实了猜测

审后面的加密逻辑,发现是AES

image-20250209111252589

但是这里有个反调试,检测到调试器的时候直接用key,没检测到的时候异或0x17

动调不起来,但是key的值又缺四个字节的,查看交叉引用发现一个可疑的rand函数,但是显示出的几个不是密钥

image-20250209111509258

往下追nop掉一个retn,发现了srand和真正的rand

image-20250209111847084

image-20250209111900240

直接抄出来跑一下异或0x17得到密钥

exp如下

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

int main()
{
srand(0x1BF52u);
uint8_t a[16];
for (int i=0;i<16;i++)
{
int r=rand();
a[i]=(r+r/255)^0x17;
}

for(int i=0;i<16;i++)
printf("%02X",a[i]);

return 0;
}

运行得到key:09E5FDEB683175B6B13B840891EB78D2

算法没有魔改,赛博厨子一把梭

image-20250209113037915

得到flag:

1
VNCTF{W0w_u_g0t_Fuk0's_st4rf1sh}

抽奖转盘

人生第一次完成鸿蒙逆向,略显兴奋的同时记录一下解题过程

先去看了so

image-20250209114513028

一个base64,一个魔改的rc4,最后多xor了40,key是Take_it_easy

jadx-dev-all.jar反编译modules.abc,看到了承接MyCry的地方

image-20250209114304949

加密过程有了开始翻密文(翻了很久)

3333333

找到了密文,发现密文里明显不是base64的可见字符,推测java层有加密

继续审java层逻辑发现了一个对于密文的小加密,+1再xor7

image-20250209115134450

解密流程是先解Java层,然后解base64,然后解魔改rc4

homo.c

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
int main()
{
int a[]={101, 74, 76, 49, 101, 76, 117, 87, 55, 69, 118, 68, 118, 69, 55, 67, 61, 83, 62, 111, 81, 77, 115, 101, 53, 73, 83, 66, 68, 114, 109, 108, 75, 66, 97, 117, 93, 127, 115, 124, 109, 82, 93, 115};
for(int i=0;i<44;i++)
{
a[i]^=7;
a[i]-=1;
printf("%c",a[i]);
}
return 0;
}

解得:aLJ5aJqO/ApBpA/C9S8gUIsa1MSDBtijKDeqYwsziTYs

解base64有:68b279689a8efc0a41a40fc2f52f20508b1ad4c48306d8a32837aa630b3389362c

最后解rc4

homo.py

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
def initialize_key_schedule(key):
key_length = len(key)
sbox = list(range(256))
j = 0
for i in range(256):
j = (j + sbox[i] + key[i % key_length]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]

return sbox

def generate_keystream(sbox):
i = 0
j = 0
while True:
i = (i + 1) % 256
j = (j + sbox[i]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]

keystream_value = sbox[(sbox[i] + sbox[j]) % 256]
yield keystream_value


key = b'Take_it_easy'

ciphertext = bytes.fromhex('68b279689a8efc0a41a40fc2f52f20508b1ad4c48306d8a32837aa630b3389362c')

sbox = initialize_key_schedule(key)
keystream_generator = generate_keystream(sbox)

plaintext = "".join(chr(byte ^0x28^next(keystream_generator)) for byte in ciphertext)

print(plaintext)

解得YQFWI~MXvwb’qhbLdvwbgdqfhb5358$,不太对的样子,猜测还有没看到的加密,由flag头VNCTF猜要减3,尝试一下

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main()
{
char a[]="YQFWI~MXvwb'qhbLdvwbgdqfhb5358$";
for(int i=0;i<strlen(a);i++)
{
a[i]-=3;
printf("%c",a[i]);
}
return 0;
}

解得:VNCTF{JUst_$ne_Iast_dance_2025!

故最后得到flag:

1
VNCTF{JUst_$ne_Iast_dance_2025!}

kotlindroid

很奇怪的非常规题,最后才出的

jadx看逻辑

image-20250209125310512

翻了很久不断交叉引用找到了加密逻辑:AES/GCM/NoPadding

iv是114514,密文是MTE0NTE0HMuJKLOW1BqCAi2MxpHYjGjpPq82XXQ/jgx5WYrZ2MV53a9xjQVbRaVdRiXFrSn6EcQPzA==,缺少add和key

然后尝试frida hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Java.perform(() => {
// Hook JNI 类的方法
const JNI = Java.use("com.atri.ezcompose.JNI");
JNI.getAt.implementation = function() {
console.log('[+] JNI.getAt() called');
const result = this.getAt();
console.log(`[*] JNI.getAt() returned: ${result}`);
return result;
};

// Hook SearchActivityKt 类的方法
const SearchActivityKt = Java.use("com.atri.ezcompose.SearchActivityKt");

SearchActivityKt.check.implementation = function(text, context, key) {
console.log('[+] check() called');
console.log(` Text: ${text}`);
console.log(` Context: ${context}`);
console.log(` Key: ${key}`);

const isValid = this.check(text, context, key);
console.log(`[*] check() result: ${isValid}`);
return isValid;
};
});

hook结果

1
2
3
4
5
6
7
8
9
[+] check() called
Text: hhh
Context: com.atri.ezcompose.SearchActivity@af9c1c8
Key: 97,116,114,105,107,101,121,115,115,121,101,107,105,114,116,97
[*] check() result: undefined
[+] generateIV() called
[*] generateIV() returned: 49,49,52,53,49,52
[+] JNI.getAt() called
[*] JNI.getAt() returned: mysecretadd

hook得到key是atrikeyssyekirta

add是mysecretadd

这里有点小坑,密文解完base64前6位是iv,后面才是真正的密文

image-20250209170228965

真正的密文是1ccb8928b396d41a82022d8cc691d88c68e93eaf365d743f8e0c79598ad9d8c579ddaf718d055b45a55d4625c5ad29fa11c40fcc(16进制)

赛博厨子梭了

image-20250209135614226

得到flag:

1
VNCTF{Y0U_@re_th3_Ma5t3r_0f_C0mp0s3}

AndroidLux(赛后做出)

jadx审逻辑

flag验证

image-20250209201918652

起了一个socket环境

image-20250209201707600

把/assets下的busybox和env复制到指定路径,env其实是个tar.xz,并解压

image-20250209201746813

没有看到有加载native so,所以去/assets目录下复制出来env并解压

1
2
mv env env.tar.xz
tar -xJvf env.tar.xz

然后得到一大堆文件

image-20250209202249475

翻了半天无果,然后有小提示,用Everything去看文件的修改时间,有

image-20250209202349910

有一个env的elf文件,然后还有一个libexec.so比较可疑

先分析env

image-20250209202457766

有个魔改且变表的base64

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
v16 = 0;
if ( len % 3 )
v3 = 4;
else
v3 = 0;
v13 = malloc(4 * (len / 3) + 1 + v3);
for ( i1 = 0; i1 < len; ++i1 )
{
if ( len - i1 <= 2 )
{
v13[v16] = base64[input[i1] >> 2];
if ( len - i1 == 2 )
{
v7 = input[i1++] & 3;
v13[v16 + 1] = base64[v7 | (input[i1] >> 2) & 0x3C];
v13[v16 + 2] = base64[input[i1] & 0xF];
}
else
{
v13[v16 + 1] = base64[input[i1] & 3];
v13[v16 + 2] = 61;
}
v8 = v16 + 3;
v16 += 4;
v13[v8] = 61;
}
else
{
v13[v16] = base64[input[i1] >> 2];
v4 = input[i1] & 3;
i2 = i1 + 1;
v13[v16 + 1] = base64[v4 | (input[i2] >> 2) & 0x3C];
v5 = input[i2] & 0xF;
i1 = i2 + 1;
v13[v16 + 2] = base64[v5 | (16 * (input[i1] >> 6))];
v6 = v16 + 3;
v16 += 4;
v13[v6] = base64[input[i1] & 0x3F];
}
}
v13[v16] = 0;
result = a3;
*a3 = v13;
return result;

然后去浏览libexec.so

发现hook了read和strncmp

read里面多了一个xor 1

image-20250209202740996

strncmp里面多了一个ROT13

image-20250209202729976

所以最后解密的顺序是先ROT13,再解魔改base64,最后xor 1

rot13.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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *rot13_decrypt(const char *input)
{
int len = strlen(input);
char *output = malloc(len + 1);
if (output == NULL) {
return NULL;
}

for (int i = 0; i < len; i++) {
char c = input[i];

if (c >= 'A' && c <= 'M') {
output[i] = c + 13;
} else if (c >= 'N' && c <= 'Z') {
output[i] = c - 13;
}
else if (c >= 'a' && c <= 'm') {
output[i] = c + 13;
} else if (c >= 'n' && c <= 'z') {
output[i] = c - 13;
}
else {
output[i] = c;
}
}
output[len] = '\0';
return output;
}

int main(void)
{
const char *encrypted = "RPVIRN40R9PU67ue6RUH88Rgs65Bp8td8VQm4SPAT8Kj97QgVG==";
char *decrypted = rot13_decrypt(encrypted);

if (decrypted) {
printf("%s\n", decrypted);
free(decrypted);
} else {
fprintf(stderr, "error\n");
}
return 0;
}

得到:ECIVEA40E9CH67hr6EHU88Etf65Oc8gq8IDz4FCNG8Xw97DtIT==

b64.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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>


static int base64_rev[256] = {0};

void init_base64_rev(void) {
int i;
/* 先全部置为 -1 */
for (i = 0; i < 256; i++) {
base64_rev[i] = -1;
}
const char *alphabet = "TUVWXYZabcdefghijABCDEF456789GHIJKLMNOPQRSklmnopqrstuvwxyz0123+/";
for (i = 0; i < 64; i++) {
base64_rev[(unsigned char)alphabet[i]] = i;
}
}

unsigned char *decode_custom_base64(const char *encoded, int *out_len) {
int encoded_len, i, pad = 0;
unsigned char *decoded;
int decoded_len;

if (!encoded) return NULL;

encoded_len = strlen(encoded);
if (encoded_len % 4 != 0) {
fprintf(stderr, "length error\n");
return NULL;
}

/* 统计尾部的 '=' 填充字符个数 */
if (encoded_len >= 1 && encoded[encoded_len-1] == '=')
pad++;
if (encoded_len >= 2 && encoded[encoded_len-2] == '=')
pad++;

/* 每 4 个编码字符通常还原 3 个字节,但有填充时少还原 */
decoded_len = (encoded_len / 4) * 3 - pad;
decoded = malloc(decoded_len);
if (!decoded) return NULL;

int decoded_index = 0;
for (i = 0; i < encoded_len; i += 4) {
int d1, d2, d3, d4;
char ch1 = encoded[i], ch2 = encoded[i+1], ch3 = encoded[i+2], ch4 = encoded[i+3];

/* 如果字符为 '=' 则对应值置 0;否则通过反查表获得其 6 位值 */
d1 = (ch1 == '=') ? 0 : base64_rev[(unsigned char)ch1];
d2 = (ch2 == '=') ? 0 : base64_rev[(unsigned char)ch2];
d3 = (ch3 == '=') ? 0 : base64_rev[(unsigned char)ch3];
d4 = (ch4 == '=') ? 0 : base64_rev[(unsigned char)ch4];

/* 检查字符合法性 */
if ((ch1 != '=' && d1 < 0) || (ch2 != '=' && d2 < 0) ||
(ch3 != '=' && d3 < 0) || (ch4 != '=' && d4 < 0)) {
fprintf(stderr, "NO!\n");
free(decoded);
return NULL;
}

/* 根据填充情况决定还原多少字节 */
if (ch3 == '=') {
// 仅 1 字节有效
unsigned char a = (unsigned char)((d1 << 2) | d2);
decoded[decoded_index++] = a;
} else if (ch4 == '=') {
// 2 字节有效
unsigned char a = (unsigned char)((d1 << 2) | (d2 & 3));
unsigned char b = (unsigned char)(((d2 >> 2) << 4) | d3);
decoded[decoded_index++] = a;
if (decoded_index < decoded_len)
decoded[decoded_index++] = b;
} else {
// 3 字节完整块
unsigned char a = (unsigned char)((d1 << 2) | (d2 & 3));
unsigned char b = (unsigned char)(((d2 >> 2) << 4) | (d3 & 0xF));
unsigned char c = (unsigned char)(((d3 >> 4) << 6) | d4);
decoded[decoded_index++] = a;
if (decoded_index < decoded_len)
decoded[decoded_index++] = b;
if (decoded_index < decoded_len)
decoded[decoded_index++] = c;
}
}

if (out_len)
*out_len = decoded_len;
return decoded;
}

/* 测试示例 */
int main(void) {
/* 初始化反查表 */
init_base64_rev();

/* 示例:设 encoded_str 为用上述编码算法加密后的字符串 */
const char *encoded_str = "ECIVEA40E9CH67hr6EHU88Etf65Oc8gq8IDz4FCNG8Xw97DtIT=="; // 请替换为实际的编码字符串
int decoded_len;
unsigned char *decoded = decode_custom_base64(encoded_str, &decoded_len);
if (!decoded) {
fprintf(stderr, "error\n");
return 1;
}

for (int i = 0; i < decoded_len; i++) {
printf("%c", decoded[i]^1);
}
printf("\n");

free(decoded);
return 0;
}

得到flag:

1
VNCTF{Ur_go0d_@ndr0id&l1nux_Reve7ser}

VNCTF2025-Writeup-Reverse
http://example.com/2025/02/10/VNCTF2025 Writeup Reverse/
作者
peace dawn
发布于
2025年2月10日
许可协议