Hgame CTF 2025题目复现

REverse

Turtle

  1. Exeinfo PE打开发现是upx壳,而且题目也提示了需要脱壳,upx壳被魔改过了,用普通脱壳工具无效,于是手动脱壳。
    Exeinfo PE

    ps:有的魔改后的upx壳可以在010editor中改(把add0->upx0,add1->upx1……,本题为只读文件改不了)

  2. 用xdbg64打开,oep脱壳定律找到大跳转,打断点
    xdbg64
  3. F9运行,接着用ScyllaDump,oep为00000000004014E0,生成Turtle_dump.exe
    Scylla Dump
    Turtle_dump.exe
    接着先后点击IAT Autosearch,Get Imports,Fix Dump,这里打开Turtle_dump.exe,之后打开文件夹就会看到新生成的Turtle_dump_SCY.exe文件了。
    IAT Autosearch
    Get Imports
  4. 将Turtle_dump_SCY.exe用IDA(64)打开,发现可以正常地看到函数了,找到main函数,F5反汇编一下
    main
  5. 点开函数后可以看出sub_401550是rc4 init,而sub_40163E(标准)和sub_40175A(魔改)是两个rc4
  6. 解出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
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
from Crypto.Cipher.ARC4 import *  
enc_key = bytes([
0xCD,
0x8F,
0x25,
0x3D,
0xE1,
])

enc_key += b'QJ'

cipher = new(b'yekyek')
dec_key = cipher.decrypt(enc_key)
print(dec_key)
#ecg4ab6

enc_flag = [0 for i in range(40)]
v5 = enc_flag
v5[0] = -8
v5[1] = -43
v5[2] = 98
v5[3] = -49
v5[4] = 67
v5[5] = -70
v5[6] = -62
v5[7] = 35
v5[8] = 21
v5[9] = 74
v5[10] = 81
v5[11] = 16
v5[12] = 39
v5[13] = 16
v5[14] = -79
v5[15] = -49
v5[16] = -60
v5[17] = 9
v5[18] = -2
v5[19] = -29
v5[20] = -97
v5[21] = 73
v5[22] = -121
v5[23] = -22
v5[24] = 89
v5[25] = -62
v5[26] = 7
v5[27] = 59
v5[28] = -87
v5[29] = 17
v5[30] = -63
v5[31] = -68
v5[32] = -3
v5[33] = 75
v5[34] = 87
v5[35] = -60
v5[36] = 126
v5[37] = -48
v5[38] = -86
v5[39] = 10

add_bytes = new(b'ecg4ab6').encrypt(b'\x00' * 40)

flag = ''
for i in range(40):
flag += chr((enc_flag[i] + add_bytes[i]) & 0xff)
print(flag)

#output:
#b'ecg4ab6'
#hgame{Y0u'r3_re4l1y_g3t_0Ut_of_th3_upX!}

flag: hgame{Y0u’r3_re4l1y_g3t_0Ut_of_th3_upX!}

尊嘟假嘟

  1. 在 MuMu模拟器 打开 apk,点击下方 尊嘟 假嘟 按钮的话就是让图片跳转,但其实你分别在两个界面点图片的能发现出现了”0.o”和”o.0” 的字符,并且是逐渐加长的
  2. 用 jadx 打开 apk,先看到有一个DexCall

    静态加载了两个本地库“zunjia”和“check”,注意到核心方法callDexMethod,动态加载.dex文件
1
public static Object callDexMethod(Context context, String dexFileName, String className, String methodName, Object input) 

最后又将 dex 文件删掉,这让我想到之前做的一道 Find My Dex 的题目,于是准备先用 frida hook 拿出这个被删掉的 dex 文件
frida-server连一下

hook 出 dex 文件的地址

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
Java.perform(function() {
// 确保 File 类正确加载
var File = Java.use("java.io.File");

// 获取 DexCall 类
var DexCall = Java.use("com.nobody.zunjia.DexCall");

// Hook callDexMethod 方法
DexCall.callDexMethod.implementation = function(context, dexFileName, className, methodName, input) {
// 获取 dexDir 的路径信息
var dexDir = File.$new(context.getCacheDir(), "dex");
console.log("dexDir path: " + dexDir.getAbsolutePath());

// 打印 dexFileName 和其他参数信息
console.log("dexFileName: " + dexFileName);
console.log("className: " + className);
console.log("methodName: " + methodName);

// 调用原始的 callDexMethod 方法
var result = this.callDexMethod(context, dexFileName, className, methodName, input);

// 返回结果
return result;
};
});


注意这里需要点击图片才能触发DexCall.callDexMethod,点callDexMethod x 交叉引用跳转

setText x 交叉引用跳转

而这个 JiaDu 是 ImageView

触发后

可以知道这个 dex 文件的地址是在/data/user/0/com.nobody.zunjia/cache/dex
再用一个脚本把它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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Java.perform(function() {
// 获取 File 类
var File = Java.use("java.io.File");

// 获取 DexCall 类
var DexCall = Java.use("com.nobody.zunjia.DexCall");

// Hook copyDexFromAssets 方法
DexCall.copyDexFromAssets.implementation = function(context, dexFileName, dexDir) {
console.log("[*] copyDexFromAssets called with dexFileName: " + dexFileName);

// 调用原始方法获取 dex 文件
var dexFile = this.copyDexFromAssets(context, dexFileName, dexDir);

// 获取 dex 文件的路径
var dexFilePath = dexFile.getAbsolutePath();
console.log("[*] dex file saved to: " + dexFilePath);

// 获取目标路径以保存 dex 文件(例如 `/data/data/com.nobody.zunjia/files/dex_copy`)
var savePath = "/data/user/0/com.nobody.zunjia/cache/dex/dex_copy";
var targetFile = File.$new(savePath);

// 使用 Java IO API 来拷贝文件
var inputStream = dexFile.$new(dexFilePath).getInputStream();
var outputStream = targetFile.getOutputStream();

var buffer = Java.array('byte', [1024]);
var bytesRead;

while ((bytesRead = inputStream.read(buffer)) !== -1) {
outputStream.write(buffer, 0, bytesRead);
}

// 关闭流
inputStream.close();
outputStream.close();

console.log("[*] dex file copied to: " + savePath);

// 返回 dex 文件
return dexFile;
};
});


这个路径下没有权限不能直接进行提取,先把它转移到另外的文件夹,再用 adb pull 提取出来


可以看到这个路径下多了一个zunjia.dex文件,然后执行

1
mv zunjia.dex /sdcard/Download

1
adb pull /sdcard/Download/zunjia.dex D:\aaa\zunjia


这样就把 dex 文件提取到本地了,然后在 jadx 中把这个文件也添加进去,可以看到多了的内容

base64换表

有个 toast 类,里面有一个 native 的check 函数

1
check(this.mycontext, (String) DexCall.callDexMethod(this.mycontext, this.mycontext.getString(R.string.dex), this.mycontext.getString(R.string.classname), this.mycontext.getString(R.string.func1), s));

resources.arsc的 res/values/strings.xml 中可以看到对应的字符串

所以这个其实是

1
(String) DexCall.callDexMethod("zunjia.dex""com.nobody.zundujiadu", "encode", s);

分析 check.so 文件

获取密文数据,ida 脚本

1
2
3
4
5
from idaapi import *
for i in range(43):
print(hex(get_bytes(0x3958 + i, 1)[0]), end=",")

#output:0x7a,0xc7,0xc7,0x94,0x51,0x82,0xf5,0x99,0xc,0x30,0xc8,0xcd,0x97,0xfe,0x3d,0xd2,0xae,0xe,0xba,0x83,0x59,0x87,0xbb,0xc6,0x35,0xe1,0x8c,0x59,0xef,0xad,0xfa,0x94,0x74,0xd3,0x42,0x27,0x98,0x77,0x54,0x3b,0x46,0x5e,0x95,

加密函数点进去看是rc4

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
import base64
from itertools import product

def rc4(key, data):
"""RC4算法实现"""
S = list(range(256))
j = 0
# 密钥调度算法(KSA)
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
# 伪随机生成算法(PRGA)
i = j = 0
result = bytearray()
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
k = S[(S[i] + S[j]) % 256]
result.append(byte ^ k &0xff)
return bytes(result)


# 定义标准 Base64 字符表
standard_base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="

# 定义自定义 Base64 字符表(换表)
custom_base64 = "3GHIJKLMNOPQRSTUb=cdefghijklmnopWXYZ/12+406789VaqrstuvwxyzABCDEF5"

# 构建换表字典
encode_dict = {standard_base64[i]: custom_base64[i] for i in range(len(standard_base64))}

# 简化的加密函数
def custom_base64_encode(data):
result = []
# 遍历字符串中的每个字符
for i in range(len(data)):
# 将字符转换为 ASCII 值并与索引 i 进行异或
result.append(chr(ord(data[i]) ^ i))
# 将结果列表转回字符串
data = ''.join(result)
base64_encoded = base64.b64encode(data.encode('utf-8')).decode('utf-8')
return ''.join(encode_dict.get(c, c) for c in base64_encoded)


# 密文数据
encrypted_data = bytes([
0x7a, 0xc7, 0xc7, 0x94, 0x51, 0x82, 0xf5, 0x99, 0x0c, 0x30, 0xc8, 0xcd,
0x97, 0xfe, 0x3d, 0xd2, 0xae, 0x0e, 0xba, 0x83, 0x59, 0x87, 0xbb, 0xc6,
0x35, 0xe1, 0x8c, 0x59, 0xef, 0xad, 0xfa, 0x94, 0x74, 0xd3, 0x42, 0x27,
0x98, 0x77, 0x54, 0x3b, 0x46, 0x5e, 0x95
])

# 生成所有可能的密码组合
segments = ['o.0', '0.o']

for bits in product([0, 1], repeat=12):
# 构造密码
password = ''.join([segments[b] for b in bits])

# 转换到标准Base64
key=custom_base64_encode(password).encode()
# 使用密钥解密
decrypted = rc4(key, encrypted_data)
if b'hgame' in decrypted:
print(decrypted)

b’hgame{4af153b9-ed3e-420b-978c-eeff72318b49}’


Hgame CTF 2025题目复现
http://example.com/2025/02/23/2/
作者
Eleven
发布于
2025年2月23日
许可协议