2025VNCTF-pwn&re

D0wnBe@t Lv4

RE

好多的安卓逆向啊,但是都差一点😫😫😫

hook_fish

算是签到题目来着

拿到雷电模拟器里面直接启动,会出现什么什么 ,jadx启动!!搜索关键字 ,然后就有下面的函数了:

image-20250210161438381

但这些不重要,注意中间有一个encrypt的调用,

image-20250210161936226

返回值就是加密之后的函数,但是有什么用呢?往下看

image-20250210161558551

这里调用了hoo_fish.dex的check方法,但这个文件哪来的呢?我们全局搜索字符串可以看到:

image-20250210162049999

访问这个网站就可以下载到了,然后用dex2jar将dex转为apk文件,继续 jadx启动!!!

image-20250210162553464

全部代码就不展示了,里面是一个自定义的hashmap映射表的加密函数,并将加密之后的函数与strr比较,相同就check过了,大致就是这样,然后直接写脚本模拟即可:

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
def decode_fish(encoded_str):
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'
}

decoded_str = ''.join(fish_dcode[encoded_str[i:i+5]] for i in range(0, len(encoded_str), 5))
return decoded_str

strr = "jjjliijijjjjjijiiiiijijiijjiijijjjiiiiijjjjliiijijjjjljjiilijijiiiiiljiijjiiliiiiiiiiiiiljiijijiliiiijjijijjijijijijiilijiijiiiiiijiljijiilijijiiiijjljjjljiliiijjjijiiiljijjijiiiiiiijjliiiljjijiiiliiiiiiljjiijiijiijijijjiijjiijjjijjjljiliiijijiiiijjliijiijiiliiliiiiiiljiijjiiliiijjjliiijjljjiijiiiijiijjiijijjjiiliiliiijiijijijiijijiiijjjiijjijiiiljiijiijilji"

def decode_swap(a, index=0):
if index >= len(a) - 1:
return
a[index], a[index + 1] = a[index + 1], a[index] # 交换
decode_swap(a, index + 2)

def decrypt(encrypted_str):
encrypted_chars = list(encrypted_str)

# 逆向字符变换
for i in range(len(encrypted_chars)):
if '0' <= encrypted_chars[i] <= '9':
encrypted_chars[i] = chr(ord(encrypted_chars[i]) + ord('1') - (i % 4))
else:
encrypted_chars[i] = chr(ord(encrypted_chars[i]) - ord('7') - (i % 10))

# 逆向递归交换
decode_swap(encrypted_chars, 0)

# 还原十六进制字符串
hex_str = ''.join(encrypted_chars)
byte_data = bytes.fromhex(hex_str)

# 逆向字节偏移
original_bytes = bytes([b - 68 for b in byte_data])

return original_bytes.decode('utf-8')

encrypted_text = decode_fish(strr)
decrypted_text = decrypt(encrypted_text)
print(decrypted_text) # VNCTF{u_re4l1y_kn0w_H0Ok_my_f1Sh!1l}

Fuko’s_starfish

常规的windows exe逆向,但是关键操作都在dll里面,和软件系统安全赛初赛的花指令一样的,难崩

  • 主程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
hLibModule = LoadLibraryW(L"starfish.dll");
if ( hLibModule )
{
Sleep(0x7D0u);
sub_140001170("\nhello ctfer\n");
Sleep(0x7D0u);
sub_140001170("I am Ibuki Fuko!\n");
Sleep(0x7D0u);
sub_140001170("If you want to get my starfish\n");
Sleep(0x7D0u);
sub_140001170("You need to complete my three games!\n");
Sleep(0x7D0u);
sub_140001170("Are you ready?\n");
Sleep(0x7D0u);
sub_140001170("Let's start!\n");
Sleep(0x7D0u);
system("cls");
sub_140001490();
sub_140001170("Goodbye\n");
FreeLibrary(hLibModule);
return 0;
}

加载dll,然后进行下面的操作,操作是在 sub_140001490();,就是个小游戏的代码,但是不重要,因为只有一个游戏在这里面,还有另外两个小游戏在dll里面,下面就直接看dll代码:

  • dll

image-20250210175929001

fdwReason:

宏定义 说明
1 DLL_PROCESS_ATTACH 进程加载 DLL
2 DLL_THREAD_ATTACH 进程创建新线程时调用
3 DLL_THREAD_DETACH 线程退出时调用
0 DLL_PROCESS_DETACH 进程卸载 DLL

所以exe文件刚开始加载dll的时候会进入这里,然后创建了一个线程,但是重点也不在这里,而是在后面的 ThreadProc里面,这个留在后面说,然后直接查看字符串,进入第二个小游戏的地方:

image-20250210180554979

查看win之后程序在哪:

image-20250210180617212

就是中间这个 sub_1800025F0函数,这里要去掉花指令:

image-20250210180656364

ret给nop掉,因为这个改变了程序正常运行的分支,再 f5就可以看正常的程序了,代码不展示了,简单来说就是 AES/ECB加密然后与给定字符串比较,相同就对了,但是在生成密钥的地方有问题(函数sub_180001650):

获取密钥,刚好16字节:

image-20250210181236752

继续往下:

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
CurrentProcess = GetCurrentProcess();
CheckRemoteDebuggerPresent(CurrentProcess, &pbDebuggerPresent);
if ( pbDebuggerPresent )
{
v131 = v5;
v132 = v4;
v133 = v3;
v134 = v9;
v124 = v2;
v11 = v8;
v135 = v7;
v136 = v6;
v12 = v126;
v13 = v125;
v14 = v127;
LOWORD(v147) = 7481;
*(_DWORD *)Str = 964328031;
for ( i = 0i64; strlen(Str) > i; ++i )
Str[i] ^= 0x17u;
sub_1800010B0((char *)"%s");
v16 = v14;
v17 = v138;
v18 = v12;
v19 = v136;
v20 = v128;
v21 = v135;
v22 = v11;
v23 = v124;
v24 = v134;
v25 = v133;
v26 = v132;
v27 = v131;
}
else
{
v124 = v2 ^ 0x17;
v28 = v3 ^ 0x17;
v29 = v4 ^ 0x17;
v27 = v5 ^ 0x17;
v21 = v7 ^ 0x17;
v22 = v8 ^ 0x17;
v24 = v9 ^ 0x17;
v17 = v138 ^ 0x17;
v13 = v125 ^ 0x17;
v19 = v6 ^ 0x17;
v18 = v126 ^ 0x17;
v20 = v128 ^ 0x17;
LOBYTE(v137) = v137 ^ 0x17;
LOBYTE(v130) = v130 ^ 0x17;
v16 = v127 ^ 0x17;
LOBYTE(v129) = v129 ^ 0x17;
v23 = v124;
v25 = v28;
v26 = v29;
}

检测是否在调试状态,是的话就输出 Hmm.,不是的话,就将密钥 ^0x17,然后再加密,但是密钥其实在其他地方被改变了,而不是静态写在data段的数据,所以直接看 byte_18000E1E0…………^0x17是不对的,我们看到数据,然后 x查看被谁引用了

image-20250210181705083

发现就是开头说过的 ThreadProc函数,跟进去看,是一堆随机数生成,然后得到的数据,但是我们并不知道随机数种子是什么,dll动调也不太会,但其实往函数最下面看,还是有个一样的花指令

image-20250210182407660

同样的看汇编 nop掉retn,再看代码就知道了:

image-20250210183308175

那我们就可以模拟随机数生成,然后就可以知道密钥是多少了

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

int main() {
unsigned char key[16];
srand(114514u);
for (int i = 0; i < 16; i++) {
int v = rand();
key[i] = (v + v / 0xff) ^ 0x17;
printf("%02x", key[i]); // 09e5fdeb683175b6b13b840891eb78d2
}
}

然后直接cyberchelf一把梭

image-20250210183451712

写个脚本也行

1
2
3
4
5
6
7
8
9
10
11
12
from Crypto.Cipher import AES
import binascii

hex_key = "09e5fdeb683175b6b13b840891eb78d2"
hex_ciphertext = "3d011c190ba090815f672731a89aa47497362167ab2eb4a09418d37d93e646e7"

key = binascii.unhexlify(hex_key)
ciphertext = binascii.unhexlify(hex_ciphertext)

cipher = AES.new(key,AES.MODE_ECB)
decrypted = cipher.decrypt(ciphertext)
print(decrypted) # b"VNCTF{W0w_u_g0t_Fuk0's_st4rf1sh}"

kotlindroid

主要部分如下:

image-20250210200615961

前面的key很好获得,key1^23,key2^8拼接起来就是了:atrikeyssyekirta

跟进check继续看看:

image-20250210201021225

可以发现是 AES/GCM加密,密文是: MTE0NTE0HMuJKLOW1BqCAi2MxpHYjGjpPq82XXQ/jgx5WYrZ2MV53a9xjQVbRaVdRiXFrSn6EcQPzA==,iv是114514,key就是上面得到的,其中函数GCMParameterSpec设置了tag为128位,其实就是密文最后16字节,但是GCM模式还需要一个aad才可以解密,下面找一找aad:

先跟进sec后面有个 SearchActivityKt$sec$1函数

image-20250210213838529

发现从JNI类获得了bytes数据然后更新为AAD

image-20250210223102503

image-20250210223442143

调用了so文件中的 native_natget方法,从里面传入了上面的数组

由于每次的AAD都是一样的,可以直接frida或者动调获得。这里简单介绍一下动调出AAD

  • 动调

用JEB的动调,关于下载安装可以看这篇文章

https://plastic-tire-e58.notion.site/JEB-1a2ca45ea8a480a0b576f249b42306bc?pvs=73

进入之后,我们找到核心的函数:

image-20250222171733987

也就是这里会添加AAD,我们在开头处下断点,注意要先tap到字节码的页面,然后才能下断点(ctrl+b下断点)

image-20250222172112212

在这里下一个断点,然后步过到这里的时候转为步入,因为该处是native层的处理,直接步过我们无法看到具体的数据

image-20250222172357292

然后就可以看到数据了 mysecretadd

直接模拟该逻辑用java写个代码直接输出就行:

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
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.util.Base64;
import java.nio.charset.StandardCharsets;

public class AESGCMExample {

// AES GCM 解密方法,使用 AAD
public static String aesGcmDecryptWithAAD(byte[] cipherTextWithTag, byte[] key, byte[] iv, byte[] aad) throws Exception {
// 从密文中分离出 tag (最后 16 字节) 和密文部分
byte[] tag = new byte[16];

// 初始化解密器
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(128, iv); // 128-bit tag
cipher.init(Cipher.DECRYPT_MODE, new javax.crypto.spec.SecretKeySpec(key, "AES"), spec);

// 设置 AAD
cipher.updateAAD(aad); // 添加 AAD 数据

// 执行解密
byte[] decryptedData = cipher.doFinal(cipherTextWithTag);

// 返回解密后的明文
return new String(decryptedData, StandardCharsets.UTF_8);
}

public static void main(String[] args) {
try {
// 假设的密文,密钥和 IV
String cipherTextBase64 = "HMuJKLOW1BqCAi2MxpHYjGjpPq82XXQ/jgx5WYrZ2MV53a9xjQVbRaVdRiXFrSn6EcQPzA==";
byte[] key = "atrikeyssyekirta".getBytes(); // AES 密钥
byte[] iv = "114514".getBytes(); // IV
byte[] cipherTextWithTag = Base64.getDecoder().decode(cipherTextBase64);

// 假设的 AAD 数据
String aad = "mysecretadd";
byte[] aadBytes = aad.getBytes(StandardCharsets.UTF_8);

// 解密过程
String decryptedText = aesGcmDecryptWithAAD(cipherTextWithTag, key, iv, aadBytes);

System.out.println("解密后的文本: " + decryptedText);

} catch (Exception e) {
e.printStackTrace();
}

}
// VNCTF{Y0U_@re_th3_Ma5t3r_0f_C0mp0s3}
  • 标题: 2025VNCTF-pwn&re
  • 作者: D0wnBe@t
  • 创建于 : 2025-02-10 16:04:41
  • 更新于 : 2025-02-22 18:09:07
  • 链接: http://downbeat.top/2025/02/10/2025VNCTF-pwn-re/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论
目录
2025VNCTF-pwn&re