YuHuanTin's Blog

一款可分享密码的解压缩 app 分析学习

2026/07/12

解压专家 (apk)

依据 ELF 结构

分析一款可以共享密码的解压缩 app,其中用到了 native 的知识点

本文将介绍两种 native 解密方法

java 层

image.png

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
public static void checkPasswordFromServer(String s, Context context0, FindPasswordCallback zipUtil$FindPasswordCallback0) {
new Thread(new Runnable() {
@Override
public void run() {
var hashMap0 = new HashMap();
DocumentFileBean documentFileBean0 = DocumentFileManager.getFileBean(context0, s);
if(documentFileBean0 != null) {
String s = documentFileBean0.getFileName();
long v = documentFileBean0.getFileSize();
hashMap0.put("fileName", s);
hashMap0.put("fileSize", v);
String s1 = ZMCoreUnZipWrapper.getQPWAT(context0, s + v);
try {
hashMap0.put("md5", FileUtil.getFileMd5(s));
}
catch(Exception exception0) {
exception0.printStackTrace();
}

var gson0 = new Gson();
String s2 = gson0.toJson(hashMap0);
var okHttpClient0 = new OkHttpClient();
try {
var secureRandom0 = new SecureRandom();
byte[] arr_b = new byte[16];
secureRandom0.nextBytes(arr_b);
var ivParameterSpec0 = new IvParameterSpec(arr_b);
var secretKeySpec0 = new SecretKeySpec("EyR2JvBXJXaUdY9auxetvhpEeQ8DmC6L".getBytes("UTF-8"), "AES");
String s3 = gson0.toJson(new ZipCheckInfo(Base64.encodeToString(ZipUtil.GeneralEncrypt(s2.getBytes("UTF-8"), secretKeySpec0, ivParameterSpec0), 2), Base64.encodeToString(arr_b, 2)));
RequestBody requestBody0 = RequestBody.create(ZipUtil.JSON, s3);
okHttpClient0.newCall(new Builder().url("https://file.unisapps.com/api/v3/search/info").addHeader("User-Client-Access", s1).addHeader("appid", "zipa").addHeader("version", CommonUtil.getVersionName(context0)).addHeader("platform", "android").post(requestBody0).build()).enqueue(new Callback() {
@Override // okhttp3.Callback
public void onFailure(Call call0, IOException iOException0) {
Log.d("onFailure", "onFailure---" + iOException0.getMessage());
com.fileunzip.zxwknight.utils.ZipUtil.8.this.val$callback.findPassword("");
}

@Override // okhttp3.Callback
public void onResponse(Call call0, Response response0) throws IOException {
if(response0.isSuccessful()) {
if(response0.code() != 200) {
com.fileunzip.zxwknight.utils.ZipUtil.8.this.val$callback.findPassword("");
return;
}

try {
String s = response0.body().string();
Class class0 = new HashMap().getClass();
var map0 = (Map)gson0.fromJson(s, class0);
byte[] arr_b = Base64.decode(((String)map0.get("data")), 2);
var ivParameterSpec0 = new IvParameterSpec(Base64.decode(((String)map0.get("iv")), 2));
var s1 = new String(ZipUtil.GeneralDecrypt(arr_b, secretKeySpec0, ivParameterSpec0), "UTF-8");
new HashMap();
Class class1 = map0.getClass();
var s2 = (String)((Map)gson0.fromJson(s1, class1)).get("pd");
com.fileunzip.zxwknight.utils.ZipUtil.8.this.val$callback.findPassword(s2);
}
catch(Exception unused_ex) {
com.fileunzip.zxwknight.utils.ZipUtil.8.this.val$callback.findPassword("");
}

Log.d("map", "map");
return;
}

com.fileunzip.zxwknight.utils.ZipUtil.8.this.val$callback.findPassword("");
}
});
}
catch(Exception exception1) {
exception1.printStackTrace();
}

return;
}

zipUtil$FindPasswordCallback0.findPassword("");
}
}).start();
}

getVersionName

1
2
3
4
5
6
7
8
9
public static String getVersionName(Context context0) {
try {
return context0.getApplicationContext().getPackageManager().getPackageInfo("com.fileunzip.zxwknight", 0).versionName;
}
catch(PackageManager.NameNotFoundException packageManager$NameNotFoundException0) {
packageManager$NameNotFoundException0.printStackTrace();
return "";
}
}

image.png

User-Client-Access

1
2
3
4
5
String s = documentFileBean0.getFileName();
long v = documentFileBean0.getFileSize();
hashMap0.put("fileName", s);
hashMap0.put("fileSize", v);
String s1 = ZMCoreUnZipWrapper.getQPWAT(context0, s + v);

其中 getQPWAT 调用 libZMCoreUnZip.so 的 executeGBS native 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static String getQPWAT(Context context0, String s) {
return ZMCoreUnZipZipApi.executeGBS(s, context0);
}

public class ZMCoreUnZipZipApi {
static {
System.loadLibrary("ZMCoreUnZip");
}

public static native int executeConfig(String arg0, ZMCoreUnZipZipExtractCallback arg1, String arg2, Context arg3, int arg4) {
}

public static native String executeGBS(String arg0, Context arg1) {
}

public static native String getVersionInfo() {
}
}

native 层

静态分析+vibe

发现能识别 elf,但是 export 符号很奇怪,不是可见字符,且指示条不正常

image.png

发现 JNI_OnLoad export 不存在,所以看 init_array

image.png

发现不是很好找,这里根据 elf 结构我们可以直接根据 Elf64_Dyn 类型引用定位到 ELF Dynamic Information 然后找到 DT_INIT_ARRAY

image.png

image.png

随后发现 init_array 均为空值 0,说明需要重定向。

查看 DT_RELA 元素为 Elf64_Rela

1
2
3
4
5
6
struct Elf64_Rela
{
unsigned __int64 r_offset;
unsigned __int64 r_info;
__int64 r_addend;
};

注意我们需要 r_info == 0x403 的项目,且 r_offset 需要位于 [DT_INIT_ARRAY, DT_INIT_ARRAY + DT_INIT_ARRAYSZ) 之中,即 [0x55DB00, 0x55DB00+0x20)

image.png

所以 init_array 为

1
2
3
4
0x549504 // init1
0x5495B4 // init2
0x549D08 // init3
0

可以通过编写以下脚本来应用重定向

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
# from `ELF RELA Relocation Table`
base = 0
table = [
(0x55DB00, 0x549504),
(0x55DB08, 0x5495B4),
(0x55DB10, 0x549D08),
(0x55DB20, 0x549490),
(0x55DB28, 0x54949C),
(0x55DE30, 0x55F5B0),
(0x55DE68, 0x55F5A8),
(0x55DE70, 0x55E008),
(0x55DEA0, 0x553A88),
(0x55DEB0, 0x557268),
(0x55DEC0, 0x55F550),
(0x55DED0, 0x54B288),
(0x55DEE0, 0x55F5C0),
(0x55DF10, 0x55E130),
(0x55DF60, 0x55723C),
(0x55DF78, 0x55FA30),
(0x55DF90, 0x55FB88),
(0x55DFB0, 0x55FB50),
(0x55DFC0, 0x54AFE0),
(0x55DFD0, 0x55F5B8),
(0x55DFD8, 0x54B070),
(0x55DFE0, 0x55F9E0),
(0x55F5B0, 0x63BF68),
(0x55F5B8, 0),
]

def main():
import utils_ida
for old, new in table:
print(f"patch {hex(old)} -> {hex(base + new)}")
utils_ida.DelItem(old, 8)
utils_ida.CreateQword(old)
utils_ida.Patch(old, (base + new).to_bytes(8, 'little'))
utils_ida.SetType(old, "void (*)(void)")

if __name__ == "__main__":
main()

借助 ai 分析,能够找到关键全局变量并创建结构体

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
#pragma pack(push, 2)
struct loader_meta_t
{
unsigned __int64 image_header_off;
unsigned __int64 program_headers_off;
unsigned __int64 dynsym_off;
unsigned __int64 dynstr_off;
unsigned __int64 unknown_table_20_off;
unsigned __int64 rela_off;
unsigned __int64 jmprel_off;
unsigned __int64 optional_table_off;
unsigned __int64 dynamic_off;
unsigned __int64 aux_src_off;
unsigned __int64 callback_or_code_off;
unsigned __int64 loader_entry_off;
unsigned __int64 android_reloc_off;
unsigned int phnum;
unsigned int unknown_6C;
unsigned int rela_count;
unsigned int jmprel_count;
unsigned int android_reloc_size;
unsigned int payload_seed0;
unsigned int flags;
unsigned int packed_size;
unsigned int aux_size;
unsigned int init_stage;
unsigned int dynsym_seed0;
unsigned int sym_index_begin;
unsigned int sym_index_end;
unsigned int dynsym_seed3;
unsigned int resource_flags;
unsigned int loader_word_A4;
int gate_beg;
int gate_end;
unsigned int split_begin;
unsigned int split_end;
};
#pragma pack(pop)
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
.data:000000000063BF68 ; loader_meta_t loader_meta
.data:000000000063BF68 loader_meta DCQ 0 ; image_header_off
.data:000000000063BF70 DCQ 0 ; program_headers_off
.data:000000000063BF78 DCQ 0x150 ; dynsym_off
.data:000000000063BF80 DCQ 0x5BA5E8 ; dynstr_off
.data:000000000063BF88 DCQ 0xE70 ; unknown_table_20_off
.data:000000000063BF90 DCQ 0xE70 ; rela_off
.data:000000000063BF98 DCQ 0x6CB58 ; jmprel_off
.data:000000000063BFA0 DCQ 0 ; optional_table_off
.data:000000000063BFA8 DCQ 0x6CB58 ; dynamic_off
.data:000000000063BFB0 DCQ 0xB8 ; aux_src_off
.data:000000000063BFB8 DCQ 0x4B8290 ; callback_or_code_off
.data:000000000063BFC0 DCQ 0x5A68F8 ; loader_entry_off
.data:000000000063BFC8 DCQ 0 ; android_reloc_off
.data:000000000063BFD0 DCD 6 ; phnum
.data:000000000063BFD4 DCD 0 ; unknown_6C
.data:000000000063BFD8 DCD 0x47DF ; rela_count
.data:000000000063BFDC DCD 0 ; jmprel_count
.data:000000000063BFE0 DCD 0 ; android_reloc_size
.data:000000000063BFE4 DCD 0x80B35FC2 ; payload_seed0
.data:000000000063BFE8 DCD 3 ; flags
.data:000000000063BFEC DCD 0x5062E0 ; packed_size
.data:000000000063BFF0 DCD 0x6CC18 ; aux_size
.data:000000000063BFF4 DCD 5 ; init_stage
.data:000000000063BFF8 DCD 0 ; dynsym_seed0
.data:000000000063BFFC DCD 0x3A ; sym_index_begin
.data:000000000063C000 DCD 0x2F28 ; sym_index_end
.data:000000000063C004 DCD 0x815C0 ; dynsym_seed3
.data:000000000063C008 DCD 0 ; resource_flags
.data:000000000063C00C DCD 0 ; loader_word_A4
.data:000000000063C010 DCD 0xFFFFFFFF ; gate_beg
.data:000000000063C014 DCD 0xFFFFFFFF ; gate_end
.data:000000000063C018 DCD 0 ; split_begin
.data:000000000063C01C DCD 0 ; split_end

三个 init 函数分别有以下功能

init1 负责环境探测

顺序 动作 结果
1 dlopen("libc.so") 获得 libc 句柄
2 dlsym("__system_property_get") 获得系统属性读取函数
3 读取 ro.build.version.sdk 记录 Android SDK 版本
4 打开 /proc/self/maps 枚举当前进程映射
5 "%lx-%lx %4s %*x %*x:%*x %*d%n" 解析映射行 获得区间和页权限
6 结合 SDK、模块位置和权限写入全局状态 init3load2_load_packed_segments 使用

init2 负责恢复 dynsym、dynstr

1
2
3
4
5
6
init2
-> 解析 DT_SYMTAB / DT_STRTAB / DT_HASH
-> init2_run_symbol_decode_context
-> gp_decode_D1 = dynstr_dynsym_decode_Impl
-> DispatchToSymbolDecode(...)
-> dynstr_dynsym_decode_Impl

注意到:

item value
DT_SYMTAB 0x55FD38
DT_STRTAB 0x5BA5E8
DT_STRSZ 0x817E0
DT_HASH.nchain 0x2F28
loader_meta.sym_index_begin 0x3A
loader_meta.sym_index_end 0x2F28
loader_meta.dynsym_seed3 0x815C0

dynstr_dynsym_decode_Impl 的解密过程

dynstr_dynsym_decode_Impl @ 0x54D6B8 做两件事:先解 .dynstr,再修 .dynsym

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
// Decodes runtime .dynstr (dword-XOR stream) and patches Elf64_Sym entries by XOR-adjusting st_value/st_size for a selected symbol range.
__int64 __fastcall dynstr_dynsym_decode_Impl(
_BYTE *dynsym_base,
unsigned int i_1,
unsigned int i_2,
_DWORD *pDynStr,
unsigned int dynstrLen,
int a6)
{
__int64 i; // x4
int key; // w6
int v9; // w5
Elf64_Sym *sym; // x0
__int64 v11; // x4
__int64 v12; // x8
Elf64_Sym *endSym; // x2
unsigned __int64 st_value; // x1
unsigned __int64 st_value_1; // x3
unsigned __int64 st_size; // x1

if ( a6 == 0 )
{
if ( dynstrLen != 0 )
{
i = 0;
key = 0x56312342;
do
{
v9 = pDynStr[i] ^ key;
key += i * 4;
pDynStr[i++] = v9;
}
while ( dynstrLen > (unsigned int)(i * 4) );
}
sym = (Elf64_Sym *)&dynsym_base[24 * i_1];
if ( i_1 < i_2 )
{
v11 = i_1 + 0x151;
v12 = i_1 + 0x15B;
endSym = &sym[i_2 - 1 - i_1 + 1];
do
{
st_value = sym->st_value;
st_value_1 = st_value ^ v11;
if ( st_value != 0 )
{
st_size = sym->st_size;
sym->st_value = st_value_1;
sym->st_size = st_size ^ v12;
}
++sym;
}
while ( sym != endSym );
}
}
return 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
def fix_dynstr():
key = 0x56312342
start = int('00000000005BA5E8', 16)
end = int('000000000063BBBC', 16)

utils_ida.DelItem(start, end - start)
b = utils_ida.GetBytesFromEA(start, end - start)
print(f"patch {hex(start)} -> {hex(end)}, length: {len(b)}")
assert len(b) % 4 == 0, "length must be multiple of 4"

new_b = bytearray()
for i in range(0, len(b) // 4):
v = int.from_bytes(b[i*4:i*4+4], 'little')
new_v = v ^ (key & 0xFFFFFFFF)
key = (key + i * 4) & 0xFFFFFFFF
new_b += new_v.to_bytes(4, 'little')
utils_ida.Patch(start, new_b)

def fix_dynsym():
start = int('000000000055FD38', 16)
end = int('00000000005A68F8', 16) # dynsym 结束与 HASH 表之前

b = utils_ida.GetBytesFromEA(start, end - start)

assert len(b) % 0x18 == 0, "length must be multiple of 0x18"

new_b = bytearray()
the_start = 0
for i in range(0, len(b) // 0x18):
st_name, st_info, st_other, st_shndx, st_value, st_size = struct.unpack('<IBBHQQ', b[i * 0x18: (i + 1) * 0x18])

# skip external symbol
if st_shndx != 0:
if the_start == 0:
the_start = i

assert the_start == 58, "the_start must be 58"
st_value ^= (the_start + 0x151) & 0xFFFFFFFFFFFFFFFF
st_size ^= (the_start + 0x15B) & 0xFFFFFFFFFFFFFFFF
new_b += struct.pack('<IBBHQQ', st_name, st_info, st_other, st_shndx, st_value, st_size)
utils_ida.Patch(start, new_b)

if __name__ == "__main__":
fix_dynstr()
fix_dynsym()

init3 负责还原代码

1
2
3
4
5
6
7
init3
-> decode_dyn_decode_api_table // 解密 dyn_decode_api_table 函数
-> dyn_decode_api_table
-> load1_build_runtime_context
-> load2_load_packed_segments
-> DispatchTo(core_decode) // flags bit1
-> core_decode

load2_load_packed_segments

函数概括为六个阶段:

  1. 从 ELF header 后定位主 packed payload,并定位 payload 尾部的 trailer
  2. 根据 flags 获取可选字符串或构造外部 key
  3. 第一次调用 core_decode,恢复 metadata 后方的 ELF 链接信息块
  4. 以链接信息块为基址回填 Program Header、.dynsym.dynamic 和 relocation 地址
  5. 第二次调用 core_decode,把主 packed payload 还原为带 8 字节长度头的 zlib 包
  6. 分配独立输出缓冲,调用 zlib_uncompress2_wrapper,再按 PT_LOAD 解释解压结果
flowchart TD
    A["从 image_base 定位 packed_src"]
    B["准备 packed_dst 与 ext_key"]
    C["解码 `meta+0x48` 数据块"]
    D{"flags"}
    E["bit5: fixed XOR"]
    F["bit1: core_decode"]
    G["读取 unpacked_size / compressed_size"]
    H["解压为最终 ELF 内存镜像"]
    I["回填 elf_parse_ctx_t"]
    A --> B --> C --> D
    D --> E --> G
    D --> F --> G
    D -->|无解码标志| G
    G --> H --> I

RuntimeMallocRuntimeFreeRuntimeMemcpy 等名称对应 API 表里的 libc 函数;AcquireFlag4String/AcquireFlag8String/AcquireFlag10String 只是按控制它们的 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
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
// load2_load_packed_segments @ 0x54C944
uint32_t load2_load_packed_segments(
elf_parse_ctx_t *ctx,
loader_meta_t *meta,
uint8_t *image_base
) {
uint32_t flags = meta->flags;
uint32_t packed_size = meta->packed_size;
uint32_t meta_block_size = meta->meta_block_size;

ctx->image_header = image_base + meta->image_header_off;
ctx->phnum = meta->phnum;
ctx->dynsym_seed0 = meta->dynsym_seed0;

const uint8_t *meta_block_src = (const uint8_t *)meta + meta->meta_block_off;
uint8_t *meta_block_dst = RuntimeMalloc(meta_block_size);
PrepareResourceState(meta->resource_flags);

uint64_t payload_off = sizeof(Elf64_Ehdr) +
image_ehdr(image_base)->e_phnum * sizeof(Elf64_Phdr);
uint8_t *packed_src = image_base + payload_off;
uint64_t *trailer = (uint64_t *)align_up(
(uintptr_t)packed_src + packed_size,
8
);

uint8_t *packed_dst = packed_src;
if ((flags & 0x01) != 0) {
packed_dst = RuntimeMalloc(packed_size);
ctx->owns_packed_buffer = true;
}

// X23 = align_up(packed_src + packed_size, 8)。
// 当前样本 X23 = 0x506470,且 *(uint64_t *)X23 == 0。
uint64_t descriptor_rel = *(uint64_t *)trailer;
const char *descriptor = descriptor_rel != 0
? (const char *)trailer + descriptor_rel
: NULL;

ReleaseOldDescriptorCache(ctx);
if (descriptor != NULL) {
ctx->descriptor_len = RuntimeStrlen(descriptor);
ctx->descriptor_copy = RuntimeMalloc(ctx->descriptor_len + 1);
RuntimeMemcpy(ctx->descriptor_copy, descriptor, ctx->descriptor_len + 1);
}

// X21/X26/X28 是三个受 flags bit2/bit3/bit4 控制的可选字符串。
// 当前 flags=3,三个分支均不执行,因此三者静态结果都为 NULL。
const char *key_source_bit2 = NULL; // X21
const char *key_source_bit3 = NULL; // X26
const char *key_source_bit4 = NULL; // X28

if ((flags & 0x04) != 0) {
key_source_bit2 = AcquireFlag4String();
if (!ValidateFlag4String(key_source_bit2)) {
key_source_bit2 = NULL;
}
}

if ((flags & 0x08) != 0) {
key_source_bit3 = AcquireFlag8String();
}

if ((flags & 0x10) != 0) {
key_source_bit4 = AcquireFlag10String(ctx->descriptor_copy);
if (!ValidateFlag10String(key_source_bit4)) {
key_source_bit4 = NULL;
}
}

size_t ext_key_len =
SafeStrlen(key_source_bit2) +
SafeStrlen(key_source_bit3) +
SafeStrlen(key_source_bit4);

if (RuntimeFallbackKeyEnabled()) {
ext_key = RuntimeMalloc(11);
RuntimeMemset(ext_key, 0, 11);
RuntimeMemset(ext_key, 0x5C, 8);
} else if (ext_key_len != 0) {
ext_key = RuntimeMalloc(ext_key_len + 1);
RuntimeMemset(ext_key, 0, ext_key_len + 1);
AppendString(ext_key, key_source_bit2);
AppendString(ext_key, key_source_bit3);
AppendString(ext_key, key_source_bit4);
}

// meta+0x48 指向的数据块始终先通过 core_decode。
// 这是 load3 的第一个 decode callsite,不受主 packed payload 的 bit1/bit5 分流控制。
if (core_decode_dispatcher(
meta_block_src,
meta_block_size,
meta_block_dst,
meta_block_size,
-1,
-1,
ext_key
) != meta_block_size) {
RuntimeFree(ext_key);
return 0;
}

ctx->decoded_meta_block = meta_block_dst;
ctx->decoded_meta_block_size = meta_block_size;

if ((flags & 0x01) != 0) {
// 独立目标缓冲路径:packed_src -> packed_dst。
if ((flags & 0x20) != 0) {
xor_decode_fixed_key_blocks(
packed_dst,
packed_src,
packed_size
);
} else if ((flags & 0x02) != 0) {
if (NeedsSplitSourceRebuild(meta, payload_off, packed_size)) {
uint8_t *joined_src = RuntimeMalloc(packed_size);
uint32_t joined_len = RebuildSplitSource(
joined_src,
image_base,
meta,
payload_off,
packed_size
);

core_decode_dispatcher(
joined_src,
joined_len,
packed_dst,
packed_size,
-1,
-1,
ext_key
);
RuntimeFree(joined_src);
} else {
core_decode_dispatcher(
packed_src,
packed_size,
packed_dst,
packed_size,
meta->gate_beg,
meta->gate_end,
ext_key
);
}
} else {
// 未设置解码标志,不保留刚分配的目标,直接使用原始 packed 段。
RuntimeFree(packed_dst);
packed_dst = packed_src;
ctx->owns_packed_buffer = false;
}

// packed payload 前 8 字节是后续解压/加载需要的头字段。
uint32_t unpacked_size = *(uint32_t *)(packed_dst + 0x00);
uint32_t compressed_size = *(uint32_t *)(packed_dst + 0x04);
uint64_t adjusted_size = unpacked_size;

ctx->decompressed_buffer = AllocatePayloadOutput(meta, unpacked_size);
CalcPayloadSeed(packed_dst + 8, compressed_size);

if (zlib_uncompress2_wrapper(
ctx->decompressed_buffer,
&adjusted_size,
packed_dst + 8,
compressed_size
) != 0) {
RuntimeFree(ctx->decompressed_buffer);
RuntimeFree(packed_dst);
RuntimeFree(ext_key);
return 0;
}

RuntimeFree(packed_dst);
ctx->loaded_image_size = AdjustOutputSize(meta, adjusted_size);
} else {
// 原映射路径:先将覆盖范围改为 RWX,再原地处理 packed_src。
RuntimeMprotect(
image_base,
PageAlign(payload_off + packed_size),
RuntimeExecMapFlags() | PROT_READ | PROT_WRITE
);

if ((flags & 0x20) != 0) {
xor_decode_fixed_key_blocks(
packed_src,
packed_src,
packed_size
);
} else if ((flags & 0x02) != 0) {
core_decode_dispatcher(
packed_src,
packed_size,
packed_src,
packed_size,
meta->gate_beg,
meta->gate_end,
ext_key
);
}

ctx->parsed_dynstr = (const char *)image_base;
ctx->loaded_image_size = packed_size;
}

// 解码完成后,按恢复出的 image base 回填 ELF 运行时视图。
uint8_t *loaded_base = ctx->loaded_base;
ctx->parsed_dynsym = loaded_base + meta->dynsym_off;
ctx->load_bias = loaded_base + meta->load_bias_off;
ctx->rela = loaded_base + meta->rela_off;
ctx->jmprel = loaded_base + meta->jmprel_off;
ctx->rela_count = meta->rela_size / sizeof(Elf64_Rela);

if (meta->android_reloc_off != 0) {
ctx->android_reloc = loaded_base + meta->android_reloc_off;
}

if (meta->dynamic_off == 0) {
RuntimeFree(ext_key);
return 0;
}

ctx->dynamic = loaded_base + meta->dynamic_off;
elf_compute_load_bias(ctx);
ctx->image_base = ctx->image_end;
ctx->sym_index_begin = meta->sym_index_begin;
ctx->sym_index_end = meta->sym_index_end;
ctx->dynsym_seed3 = meta->dynsym_seed3;

Elf64_Dyn *dynamic = FindFirstDynamicTag(trailer, 1);
FinalizeLoadedImage(ctx, trailer, dynamic);

RuntimeFree(ext_key);
return 1;
}

该样本将会执行两次 core_decode 而不执行 xor_decode_fixed_key_blocks

调用点 输入范围 解密内容 解密后的用途
0x54CB7C 0x63C020 开始,长度 0x6CC18 壳放在 loader_meta 后方的 ELF 链接信息块 结果保存到 ctx+0x90;后续以它为基址计算 .dynsym.dynamic、RELA、JMPREL 和 Android relocation 地址
0x54CFE8 image_base+0x190 开始,长度 0x5062E0 主 packed payload,即压缩的最终代码/数据镜像外层 结果前 8 字节给出解压长度,packed_dst+8 再送入解压函数生成最终内存镜像

core_decode 伪代码

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
// core_decode @ 0x54B7EC
uint32_t core_decode(
uint8_t *src_ptr, // X0
int src_copy_len, // W1
uint8_t *decode_buf, // X2
uint32_t decode_len, // W3
int gate_beg, // W4
int gate_end, // W5
uint8_t *ext_key // X6
) {
uint8_t head_mask[0x40];
uint8_t saved_0x80[0x80];

if (src_ptr == NULL) return 0; // 0x54B808
if (src_copy_len <= 0x3F) return 0; // 0x54B80C..0x54B814

memcpy(decode_buf, src_ptr, src_copy_len); // 0x54B840..0x54B844
if (decode_len <= 0x3F) return 0; // 0x54B848..0x54B850

uint32_t seed_input[5];
seed_input[0] = *(uint32_t *)(decode_buf + 0x0C);
seed_input[1] = *(uint32_t *)(decode_buf + 0x04);
seed_input[2] = *(uint32_t *)(decode_buf + 0x30);
seed_input[3] = decode_len;
seed_input[4] = *(uint32_t *)(decode_buf + 0x08);

uint32_t seed_main = CalcPayloadSeed(seed_input, 0x14); // 0x54B8A8

memset(head_mask, 0xD7, 0x40); // 0x54B8AC..0x54B8C0
BuildHeadMask(ext_key, head_mask); // 0x54B8C4..0x54B8CC

uint32_t body_len = decode_len - 0x40;

if (body_len <= 0x7F) { // 0x54B8D4..0x54B8D8
Rc4VariantXor(decode_buf + 0x40, body_len, seed_main);

for (int i = 0; i < 0x40; ++i) {
decode_buf[i] ^= head_mask[i];
}

return decode_len;
}

memcpy(saved_0x80, decode_buf + 0x40, 0x80); // 0x54B960..0x54B978
Rc4VariantXor(decode_buf + 0x40, 0x80, seed_main);

uint32_t k0 = (seed_main ^ *(uint32_t *)(decode_buf + 0x34)) + 8217;
uint32_t k1 = (seed_main ^ *(uint32_t *)(decode_buf + 0x3C)) + 6502;
uint32_t k2 = (seed_main ^ decode_len) + 8213;
uint32_t k3 = (seed_main ^ *(uint32_t *)(decode_buf + 0x38)) + 6534;
uint32_t key4[4] = { k0, k1, k2, k3 };

for (int i = 0; i < 0x40; ++i) { // 0x54B9E4..0x54BA3C
decode_buf[i] ^= head_mask[i];
}

Rc4VariantXor(saved_0x80, 0x80, k2); // 0x54BA40

uint32_t sched[9] = {
21605, k3, 17477, 1383, k0, k1, 309, k2, 26740
};

uint32_t *saved32 = (uint32_t *)saved_0x80;
uint32_t block_count = (decode_len - 0xC0) >> 7;

if (block_count != 0) { // 0x54BA8C
int quarter = block_count >> 2; // 0x54BA90
int range1_beg = quarter;
int range1_end = quarter * 2;
int range0_beg = quarter * 2;
int range0_end = quarter * 3;
int range3_beg = quarter * 3;

uint8_t *block = decode_buf + 0xC0;
int block_pos = 0x120; // 注意:gate 比较用 0x120 起步

for (uint32_t block_index = 0;
block_index < block_count;
++block_index, block += 0x80, block_pos += 0x80) {

// 0x54BAF4..0x54BB0C:gate 判断直接内联在 core_decode 里
if (!(block_pos < gate_beg ||
gate_beg <= 0x7F ||
(gate_end != -1 && block_pos >= gate_end))) {
continue;
}

uint32_t *block32 = (uint32_t *)block;
uint32_t mode = sched[block_index % 9] & 3; // 0x54BB10..0x54BB24

switch (mode) { // 0x54BB28..0x54BB3C
default:
// mode 0/default: 0x54BB40 / 0x54BB58..0x54BB80
if (block_index >= range0_beg && block_index <= range0_end) {
// 0x54BDC4..0x54BE24
for (int i = 0; i < 0x80; ++i) {
block[i] ^= 0xB2;
}
} else {
for (int i = 0; i < 32; ++i) {
uint32_t x = saved32[i];
block32[i] ^= x ^ key4[x & 3];
}
}
break;

case 1:
// mode 1: loc_54BC70 / 0x54BC84..0x54BCCC
if (block_index >= range1_beg && block_index <= range1_end) {
// 0x54BE28..0x54BE88
for (int i = 0; i < 0x80; ++i) {
block[i] ^= 0xA1;
}
} else {
for (int i = 0; i < 32; ++i) {
uint32_t counter = 32 - i;
uint32_t rotating = sched[i % 9];
block32[i] ^= saved32[i] ^ counter ^ rotating;
}
}
break;

case 2:
// mode 2: loc_54BD10 / 0x54BD10..0x54BD5C
for (int i = 0; i < 32; ++i) {
uint32_t rolling = key4[sched[i % 9] & 3];
block32[i] ^= saved32[i] ^ i ^ rolling;
}
break;

case 3:
// mode 3: loc_54BCD0 / 0x54BCDC..0x54BD0C
if (block_index >= range3_beg) {
// 0x54BD60..0x54BDC0
for (int i = 0; i < 0x80; ++i) {
block[i] ^= 0xC3;
}
} else {
for (int i = 0; i < 32; ++i) {
uint32_t x = saved32[i];
block32[i] ^= x ^ i ^ key4[x & 3];
}
}
break;
}
}
}

uint32_t tail = (decode_len + 0x40) & 0x7F; // 0x54BBB4..0x54BBBC
if (tail == 0) {
return decode_len;
}

uint32_t tail_pos = decode_len - tail;

for (uint32_t i = 0; i < tail; ++i) { // 0x54BBD4..0x54BC64
uint32_t pos = tail_pos + i;

if (gate_beg > (int)pos ||
gate_beg <= 0x7F ||
(gate_end != -1 && gate_end <= (int)pos)) {
uint32_t v = sched[key4[i & 3] % 9];
decode_buf[pos] ^= (v + v / 0xFF) ^ saved_0x80[i] ^ i;
}
}

return decode_len;
}

zlib_uncompress2_wrapper

sub_557170 @ 0x557170 是壳内置 zlib 的一次性解压包装器,参数语义与 uncompress2() 基本一致:

1
2
3
4
5
6
int zlib_uncompress2_wrapper(
void *dst,
uint64_t *dst_len,
const void *src,
uint64_t src_len
);

解压后需要按 PT_LOAD 分段映射

zlib 解压后的 main_payload 输出长度为 0x530268

以下 Elf64_Phdr 来自于第一次 core_decode 解出的 metadata 开头

metadata 开头连续保存 6 个 Elf64_Phdr;筛选 p_type == PT_LOAD 后得到:

p_offset p_vaddr p_filesz p_memsz
0x0 0x0 0x5062E0 0x5062E0
0x5062E0 0x50A2E0 0x29CF0 0x29D20
0x52FFD0 0x537FD0 0x298 0xBAE0

完整运行时映射为:

1
2
3
main_payload[0x000000:0x5062E0] -> VA 0x000000
main_payload[0x5062E0:0x52FFD0] -> VA 0x50A2E0
main_payload[0x52FFD0:0x530268] -> VA 0x537FD0

第二段的 p_memsz - p_filesz = 0x30,第三段为 0xB848;这两块段尾内存必须清零。

当前 IDB 的 VA 0..0x18F 已经保存原始 ELF header 和 Program Header Table,为了避免静态 patch 时覆盖这些结构,可以仅对首段执行:

1
main_payload[0x190:0x5062E0] -> VA 0x190

解密脚本

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
"""Decode and patch the packed ELF for the current flags=3 sample."""

import struct
import zlib

import ida_bytes
import ida_kernwin

MAIN_VA = 0x190
MAIN_SIZE = 0x5062E0
META_VA = 0x63C020
META_SIZE = 0x6CC18
PHNUM = 6
PHENTSIZE = 0x38
PT_LOAD = 1

def u32(data: bytes | bytearray, offset: int) -> int:
return struct.unpack_from("<I", data, offset)[0]

def put_u32(data: bytearray, offset: int, value: int) -> None:
struct.pack_into("<I", data, offset, value & 0xFFFFFFFF)

def calc_payload_seed(data: bytes) -> int:
table = []
polynomial = 0x53B20C96
for index in range(256):
value = index
for _ in range(8):
value = ((value >> 1) ^ polynomial) if value & 1 else value >> 1
value &= 0xFFFFFFFF
table.append(value)

current = 0xFFFFFFFF
for value in data:
current = table[(value ^ (current & 0xFF)) & 0xFF] ^ (current >> 8)
current = (current + 16) & 0xFFFFFFFF
return (-current - 516327184) & 0xFFFFFFFF

def rc4_variant_xor(data: bytearray, key_u32: int) -> None:
state = list(range(256))
key = struct.pack("<I", key_u32 & 0xFFFFFFFF)
state_index = 0
for index in range(256):
old_value = state[index]
state_index = (key[index & 3] + old_value + state_index) & 0xFF
state[index] = state[state_index]
state[state_index] = old_value

index = 0
state_index = 0
for position in range(len(data)):
index = (index + 1) & 0xFF
old_value = state[index]
state_index = (old_value + state_index) & 0xFF
state[index] = state[state_index]
state[state_index] = old_value
stream_value = state[(old_value + state[index]) & 0xFF]
mask = (((stream_value >> 2) | (stream_value << 6)) + 0x3A) & 0xFF
data[position] ^= mask

def core_decode(src: bytes) -> bytearray:
decode_len = len(src)
output = bytearray(src)
seed_input = struct.pack(
"<IIIII",
u32(output, 0x0C),
u32(output, 0x04),
u32(output, 0x30),
decode_len,
u32(output, 0x08),
)
seed_main = calc_payload_seed(seed_input)
head_mask = bytes([0xD7]) * 0x40
body_length = decode_len - 0x40

if body_length <= 0x7F:
body = bytearray(output[0x40:decode_len])
rc4_variant_xor(body, seed_main)
output[0x40:decode_len] = body
for index in range(0x40):
output[index] ^= head_mask[index]
return output

saved_block = bytearray(output[0x40:0xC0])
first_block = bytearray(saved_block)
rc4_variant_xor(first_block, seed_main)
output[0x40:0xC0] = first_block

key0 = ((seed_main ^ u32(output, 0x34)) + 8217) & 0xFFFFFFFF
key2 = ((seed_main ^ decode_len) + 8213) & 0xFFFFFFFF
key1 = ((seed_main ^ u32(output, 0x3C)) + 6502) & 0xFFFFFFFF
key3 = ((seed_main ^ u32(output, 0x38)) + 6534) & 0xFFFFFFFF
key4 = [key0, key1, key2, key3]

for index in range(0x40):
output[index] ^= head_mask[index]
rc4_variant_xor(saved_block, key2)

block_count = (decode_len - 0xC0) >> 7
range1_begin = (decode_len - 0xC0) >> 9
range1_end = range1_begin << 1
range3_begin = range1_end + range1_begin
schedule = [21605, key3, 17477, 1383, key0, key1, 309, key2, 26740]
saved_words = [u32(saved_block, index * 4) for index in range(32)]

for block_index in range(block_count):
block_offset = 0xC0 + block_index * 0x80
words = [u32(output, block_offset + index * 4) for index in range(32)]
mode = schedule[block_index % 9] & 3
if mode == 0:
if range1_end <= block_index <= range3_begin:
words = [word ^ 0xB2 for word in words]
else:
words = [word ^ saved_words[i] ^ key4[saved_words[i] & 3] for i, word in enumerate(words)]
elif mode == 1:
if range1_begin <= block_index <= range1_end:
words = [word ^ 0xA1 for word in words]
else:
words = [word ^ saved_words[i] ^ (32 - i) ^ schedule[i % 9] for i, word in enumerate(words)]
elif mode == 2:
words = [word ^ saved_words[i] ^ i ^ key4[schedule[i % 9] & 3] for i, word in enumerate(words)]
else:
if block_index >= range3_begin:
words = [word ^ 0xC3 for word in words]
else:
words = [word ^ saved_words[i] ^ i ^ key4[saved_words[i] & 3] for i, word in enumerate(words)]

for index, word in enumerate(words):
put_u32(output, block_offset + index * 4, word)

tail_length = (decode_len + 0x40) & 0x7F
if tail_length:
tail_offset = decode_len - tail_length
for index in range(tail_length):
position = tail_offset + index
schedule_value = schedule[key4[index & 3] % 9]
output[position] ^= ((schedule_value % 0xFF) ^ saved_block[index] ^ index) & 0xFF
return output

def parse_load_segments(decoded_meta: bytes):
for index in range(PHNUM):
phdr = struct.unpack_from("<IIQQQQQQ", decoded_meta, index * PHENTSIZE)
p_type, _, p_offset, p_vaddr, _, p_filesz, p_memsz, _ = phdr
if p_type == PT_LOAD:
yield p_offset, p_vaddr, p_filesz, p_memsz

def patch_load_segments(final_image: bytes, decoded_meta: bytes) -> None:
for p_offset, p_vaddr, p_filesz, p_memsz in parse_load_segments(decoded_meta):
skip = 0x190 if p_offset == 0 else 0
source_begin = p_offset + skip
source_end = p_offset + p_filesz
ida_bytes.patch_bytes(
p_vaddr + skip,
final_image[source_begin:source_end],
)
if p_memsz > p_filesz:
ida_bytes.patch_bytes(
p_vaddr + p_filesz,
bytes(p_memsz - p_filesz),
)
print(
f"PT_LOAD final[0x{source_begin:X}:0x{source_end:X}] "
f"-> VA 0x{p_vaddr + skip:X}"
)

def main() -> None:
decoded_meta = core_decode(ida_bytes.get_bytes(META_VA, META_SIZE))
decoded_main = core_decode(ida_bytes.get_bytes(MAIN_VA, MAIN_SIZE))

unpacked_size, compressed_size = struct.unpack_from("<II", decoded_main)
final_image = zlib.decompress(decoded_main[8:8 + compressed_size])
assert len(final_image) == unpacked_size

# The first six decoded metadata records are Elf64_Phdr entries.
patch_load_segments(final_image, decoded_meta)
ida_kernwin.refresh_idaview_anyway()
print(f"unpacked 0x{len(final_image):X} bytes")

if __name__ == "__main__":
main()

unidbg

编写 test 脚本

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
package com.github.unidbg.zmcore;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.Symbol;
import com.github.unidbg.arm.backend.Unicorn2Factory;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DalvikModule;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.memory.Memory;
import junit.framework.TestCase;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class ZMCoreInitDumpTest extends TestCase {

private AndroidEmulator emulator;

public void testInitArrayAndRebuildSo() throws Exception {
Path workspace = Paths.get(System.getProperty("zmcore.workspace")).toAbsolutePath().normalize();
File library = workspace.resolve("libZMCoreUnZip.so").toFile();
Path output = workspace.resolve("unidbg-dump/libZMCoreUnZip.init-rebuilt.so");
Files.createDirectories(output.getParent());

emulator = AndroidEmulatorBuilder.for64Bit()
.setProcessName("com.fileunzip.zxwknight")
.addBackendFactory(new Unicorn2Factory(true))
.build();
Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
VM vm = emulator.createDalvikVM();

// forceCallInit=true: execute DT_INIT/DT_INIT_ARRAY before rebuilding the file.
DalvikModule dalvikModule = vm.loadLibrary(library, true);
Module module = dalvikModule.getModule();
rebuildLoadSegments(library.toPath(), output, module);
}

// 以原始 ELF 为文件结构模板,用 init_array 执行后的运行时内存重建各个 PT_LOAD 段
private void rebuildLoadSegments(Path input, Path output, Module module) throws IOException {
byte[] original = Files.readAllBytes(input);
byte[] rebuilt = original.clone();

ByteBuffer elf = ByteBuffer.wrap(original).order(ByteOrder.LITTLE_ENDIAN);
long phoff = elf.getLong(0x20); // Elf64_Ehdr.e_phoff
int phentsize = Short.toUnsignedInt(elf.getShort(0x36)); // Elf64_Ehdr.e_phentsize
int phnum = Short.toUnsignedInt(elf.getShort(0x38)); // Elf64_Ehdr.e_phnum

// 将解密后的内存段写回原始 ELF 文件的对应位置
for (int index = 0; index < phnum; index++) {
int phdr = Math.toIntExact(phoff + (long) index * phentsize);
if (elf.getInt(phdr) != 1) { // PT_LOAD
continue;
}
long fileOffset = elf.getLong(phdr + 0x08); // Elf64_Phdr.p_offset
long virtualAddress = elf.getLong(phdr + 0x10); // Elf64_Phdr.p_vaddr
long fileSize = elf.getLong(phdr + 0x20); // Elf64_Phdr.p_filesz
byte[] segment = emulator.getBackend().mem_read(module.base + virtualAddress, fileSize);
System.arraycopy(segment, 0, rebuilt, Math.toIntExact(fileOffset), segment.length);
}

// 运行时内存可能覆盖 ELF Header,最终恢复原 SO 的前 0x190 字节
System.arraycopy(original, 0, rebuilt, 0, 0x190);

Files.write(output, rebuilt);
System.out.println("rebuilt SO: " + output);
}

@Override
protected void tearDown() throws IOException {
if (emulator != null) {
emulator.close();
}
}
}

将原来的前 0x190 字节复制到 dump 的即可分析

其他

其实还应该解密 elf 最后的 .data 段用于实现 GOT、LPT 定位的,但是比较麻烦就先不做了(分配到动态内存中了),而上述静态分析方法可以直接解密(或者这里结合一下静态的脚本解密一下也可以),下面给出一个工具脚本,用于 patch GOT,当然不使用也可以

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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
import json
import struct
from pathlib import Path

import ida_bytes
import ida_name
import ida_nalt
import ida_segment

DYNSYM_START = 0x63C170
DYNSTR_START = 0x5BA5E8
RELA_START = 0x63CE90
RELA_END = 0x6A8B78

ELF64_SYM_SIZE = 0x18
ELF64_RELA_SIZE = 0x18

R_AARCH64_ABS64 = 1025
R_AARCH64_JUMP_SLOT = 1026
R_AARCH64_RELATIVE = 1027

VALID_RELOCATION_TYPES = {R_AARCH64_ABS64, R_AARCH64_JUMP_SLOT, R_AARCH64_RELATIVE}
MAX_SYMBOL_COUNT = 0x1000
MAX_RELOCATION_OFFSET = 0x700000

RELOCATION_NAMES = {
R_AARCH64_ABS64: "R_AARCH64_ABS64",
R_AARCH64_JUMP_SLOT: "R_AARCH64_JUMP_SLOT",
R_AARCH64_RELATIVE: "R_AARCH64_RELATIVE",
}

def read_bytes(address, size):
data = ida_bytes.get_bytes(address, size)
if data is None or len(data) != size:
raise RuntimeError(f"无法读取 IDA 地址 0x{address:X} 的 0x{size:X} 字节")
return data

def read_c_string(address):
data = ida_bytes.get_strlit_contents(address, -1, 0)
if data is None:
return ""
return data.decode("utf-8", errors="replace")

def parse_symbol(index):
address = DYNSYM_START + index * ELF64_SYM_SIZE
st_name, st_info, st_other, st_shndx, st_value, st_size = struct.unpack(
"<IBBHQQ", read_bytes(address, ELF64_SYM_SIZE)
)
return {
"index": index,
"address": address,
"name_offset": st_name,
"name": read_c_string(DYNSTR_START + st_name) if st_name else "",
"info": st_info,
"other": st_other,
"section_index": st_shndx,
"value": st_value,
"size": st_size,
}

def validate_decrypted_tables():
symbol_zero = read_bytes(DYNSYM_START, ELF64_SYM_SIZE)
if symbol_zero != b"\x00" * ELF64_SYM_SIZE:
raise RuntimeError(
"0x63C170 处不是有效的 Elf64_Sym[0],动态符号表可能尚未解密;"
"请先完成 private relocation 解密"
)

first_relocation = struct.unpack("<QQq", read_bytes(RELA_START, ELF64_RELA_SIZE))
first_type = first_relocation[1] & 0xFFFFFFFF
first_symbol = first_relocation[1] >> 32
if (
first_type not in VALID_RELOCATION_TYPES
or first_symbol >= MAX_SYMBOL_COUNT
or first_relocation[0] >= MAX_RELOCATION_OFFSET
):
raise RuntimeError(
"0x63CE90 处不是有效的 Elf64_Rela;请确认 IDA 中的地址是否正确,"
"并确保已经加载解密后的 unidbg dump 文件"
)

def parse_relocations():
validate_decrypted_tables()
count = (RELA_END - RELA_START) // ELF64_RELA_SIZE
raw_relocations = []
maximum_symbol_index = 0

for index in range(count):
address = RELA_START + index * ELF64_RELA_SIZE
offset, info, addend = struct.unpack(
"<QQq", read_bytes(address, ELF64_RELA_SIZE)
)
symbol_index = info >> 32
relocation_type = info & 0xFFFFFFFF
if relocation_type not in VALID_RELOCATION_TYPES:
raise RuntimeError(
f"重定位项 #{index} @ 0x{address:X} 类型无效: {relocation_type}"
)
if symbol_index >= MAX_SYMBOL_COUNT:
raise RuntimeError(
f"重定位项 #{index} @ 0x{address:X} 符号索引越界: {symbol_index}"
)
if offset >= MAX_RELOCATION_OFFSET:
raise RuntimeError(
f"重定位项 #{index} @ 0x{address:X} 偏移地址越界: 0x{offset:X}"
)

maximum_symbol_index = max(maximum_symbol_index, symbol_index)
raw_relocations.append(
{
"index": index,
"address": address,
"offset": offset,
"info": info,
"symbol_index": symbol_index,
"type": relocation_type,
"type_name": RELOCATION_NAMES.get(
relocation_type, f"UNKNOWN_{relocation_type}"
),
"addend": addend,
}
)

symbols = [parse_symbol(index) for index in range(maximum_symbol_index + 1)]

for relocation in raw_relocations:
symbol_index = relocation["symbol_index"]
symbol = symbols[symbol_index] if symbol_index < len(symbols) else None
relocation["symbol_name"] = symbol["name"] if symbol else ""
relocation["symbol_value"] = symbol["value"] if symbol else 0

if relocation["type"] == R_AARCH64_RELATIVE:
relocation["resolved_value"] = relocation["addend"]
elif relocation["type"] == R_AARCH64_ABS64 and symbol:
relocation["resolved_value"] = symbol["value"] + relocation["addend"]
else:
relocation["resolved_value"] = None

resolved_value = relocation["resolved_value"]
relocation["resolved_name"] = (
ida_name.get_name(resolved_value) if resolved_value is not None else ""
)

return symbols, raw_relocations

def sanitize_name(name):
result = []
for character in name:
if character.isalnum() or character == "_":
result.append(character)
else:
result.append("_")
return "".join(result).strip("_")

def apply_relocations(relocations):
patched = 0
unchanged = 0
named_jump_slots = 0
skipped = 0

for relocation in relocations:
target_address = relocation["offset"]
resolved_value = relocation["resolved_value"]
relocation_type = relocation["type"]

if relocation_type in (R_AARCH64_RELATIVE, R_AARCH64_ABS64):
if resolved_value is None or ida_segment.getseg(target_address) is None:
skipped += 1
continue

current_value = struct.unpack("<Q", read_bytes(target_address, 8))[0]
patch_value = resolved_value & 0xFFFFFFFFFFFFFFFF
if current_value == patch_value:
unchanged += 1
elif ida_bytes.patch_qword(target_address, patch_value):
patched += 1
else:
skipped += 1
continue

if relocation_type == R_AARCH64_JUMP_SLOT:
symbol_name = relocation["symbol_name"]
if not symbol_name or ida_segment.getseg(target_address) is None:
skipped += 1
continue
got_name = "got_" + sanitize_name(symbol_name)
if ida_name.set_name(target_address, got_name, ida_name.SN_FORCE):
named_jump_slots += 1
else:
skipped += 1
continue

skipped += 1

return {
"patched": patched,
"unchanged": unchanged,
"named_jump_slots": named_jump_slots,
"skipped": skipped,
}

def format_hex(value):
if value is None:
return "-"
if value < 0:
return f"-0x{-value:X}"
return f"0x{value:X}"

def write_outputs(symbols, relocations):
idb_path = Path(ida_nalt.get_input_file_path())
output_directory = idb_path.parent
json_path = output_directory / "private-relocations.json"
text_path = output_directory / "private-relocations.txt"

result = {
"dynsym_start": DYNSYM_START,
"dynstr_start": DYNSTR_START,
"rela_start": RELA_START,
"rela_end": RELA_END,
"symbol_count": len(symbols),
"relocation_count": len(relocations),
"symbols": symbols,
"relocations": relocations,
}
json_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")

lines = [
f"dynsym: 0x{DYNSYM_START:X}",
f"dynstr: 0x{DYNSTR_START:X}",
f"rela: [0x{RELA_START:X}, 0x{RELA_END:X})",
f"symbols: {len(symbols)}",
f"relocations: {len(relocations)}",
"",
"index relocation offset type symbol addend resolved",
]
for relocation in relocations:
symbol_name = relocation["symbol_name"] or "-"
resolved = format_hex(relocation["resolved_value"])
if relocation["resolved_name"]:
resolved += f" ({relocation['resolved_name']})"
lines.append(
f"{relocation['index']:5d} "
f"0x{relocation['address']:08X} "
f"0x{relocation['offset']:08X} "
f"{relocation['type_name']:<24} "
f"{symbol_name:<30} "
f"{format_hex(relocation['addend']):<12} "
f"{resolved}"
)
text_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
return json_path, text_path

def main():
symbols, relocations = parse_relocations()
patch_result = apply_relocations(relocations)
json_path, text_path = write_outputs(symbols, relocations)

type_counts = {}
for relocation in relocations:
type_name = relocation["type_name"]
type_counts[type_name] = type_counts.get(type_name, 0) + 1

print(f"解析符号数量: {len(symbols)}")
print(f"解析重定位数量: {len(relocations)}")
for type_name, count in sorted(type_counts.items()):
print(f"{type_name}: {count}")

print(f"实际写入补丁: {patch_result['patched']}")
print(f"原值已经正确: {patch_result['unchanged']}")
print(f"命名外部 GOT: {patch_result['named_jump_slots']}")
print(f"跳过项目: {patch_result['skipped']}")

for relocation in relocations:
if relocation["offset"] == 0x533D50:
print("0x533D50:", relocation)
break

print(f"JSON: {json_path}")
print(f"文本: {text_path}")

if __name__ == "__main__":
main()

分析 executeGBS

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
__int64 __fastcall Java_com_jindk_zmcoreunzipwrapper_ZMCoreUnZipZipApi_executeGBS(
_JNIEnv *a1,
__int64 a2,
void *a3,
void *a4)
{
struct _jmethodID *StaticMethodID_1; // x0
const char *v6; // [xsp+10h] [xbp-250h]
char *v7; // [xsp+18h] [xbp-248h]
jbyte *ByteArrayElements; // [xsp+20h] [xbp-240h]
int ArrayLength; // [xsp+2Ch] [xbp-234h]
struct _jmethodID *ObjectField; // [xsp+30h] [xbp-230h]
struct _jmethodID *v11; // [xsp+38h] [xbp-228h]
jmethodID v12; // [xsp+40h] [xbp-220h]
void *v13; // [xsp+48h] [xbp-218h]
jclass v14; // [xsp+58h] [xbp-208h]
__int64 v15; // [xsp+68h] [xbp-1F8h]
jmethodID v16; // [xsp+70h] [xbp-1F0h]
jclass v17; // [xsp+78h] [xbp-1E8h]
struct _jmethodID *v18; // [xsp+90h] [xbp-1D0h]
__int64 v19; // [xsp+98h] [xbp-1C8h]
jclass v20; // [xsp+A0h] [xbp-1C0h]
void *v21; // [xsp+A8h] [xbp-1B8h]
void *v22; // [xsp+B0h] [xbp-1B0h]
int v23; // [xsp+D0h] [xbp-190h]
int v24; // [xsp+D4h] [xbp-18Ch]
int v25; // [xsp+D8h] [xbp-188h]
int v26; // [xsp+DCh] [xbp-184h]
const char *v27; // [xsp+E0h] [xbp-180h]
const char *v28; // [xsp+E8h] [xbp-178h]
const char *v29; // [xsp+F0h] [xbp-170h]
const char *StringUTFChars; // [xsp+F8h] [xbp-168h]
void *v31; // [xsp+100h] [xbp-160h]
void *v32; // [xsp+108h] [xbp-158h]
struct _jmethodID *StaticMethodID; // [xsp+110h] [xbp-150h]
jclass a2a; // [xsp+118h] [xbp-148h]
int v35; // [xsp+12Ch] [xbp-134h]
jmethodID v36; // [xsp+130h] [xbp-130h]
struct _jmethodID *v37; // [xsp+138h] [xbp-128h]
jmethodID MethodID; // [xsp+140h] [xbp-120h]
jclass Class; // [xsp+148h] [xbp-118h]
const char *v45; // [xsp+218h] [xbp-48h]
const char *v46; // [xsp+238h] [xbp-28h]
const char *v47; // [xsp+258h] [xbp-8h]

Class = _JNIEnv::FindClass(a1, "android/content/Context");
if ( Class == nullptr )
return 0;
MethodID = _JNIEnv::GetMethodID(&a1->functions, Class, "getPackageManager", "()Landroid/content/pm/PackageManager;");
v37 = _JNIEnv::GetMethodID(&a1->functions, Class, "getPackageName", "()Ljava/lang/String;");
if ( a4 == nullptr || MethodID == nullptr || v37 == nullptr )
return 0;
v36 = _JNIEnv::CallObjectMethod(&a1->functions, a4, v37);
if ( v36 == nullptr )
return 0;
v35 = ZMCoreUnzip_executeCVS(a1, a4);
a2a = _JNIEnv::FindClass(a1, "java/lang/String");
StaticMethodID = _JNIEnv::GetStaticMethodID(&a1->functions, a2a, "valueOf", "(I)Ljava/lang/String;");
v32 = (void *)_JNIEnv::CallStaticObjectMethod(
&a1->functions,
a2a,
StaticMethodID,
(unsigned int)(2 * ((v35 >> 2) - 169) + 127));
v31 = (void *)_JNIEnv::CallStaticObjectMethod(&a1->functions, a2a, StaticMethodID, (unsigned int)v35);
if ( v32 == nullptr || v31 == nullptr )
return 0;
StringUTFChars = _JNIEnv::GetStringUTFChars(&a1->functions, v31, nullptr);
v29 = _JNIEnv::GetStringUTFChars(&a1->functions, v36, nullptr);
v28 = _JNIEnv::GetStringUTFChars(&a1->functions, a3, nullptr);
v27 = _JNIEnv::GetStringUTFChars(&a1->functions, v32, nullptr);
if ( StringUTFChars == nullptr || v29 == nullptr || v28 == nullptr || v27 == nullptr )
return 0;
v26 = j_got_strlen_chk(StringUTFChars, -1);
v25 = j_got_strlen_chk(v29, -1);
v24 = j_got_strlen_chk(v28, -1);
v23 = j_got_strlen_chk(v27, -1);
v47 = (const char *)j_got_malloc(v26 + v25 + v24 + v23 + 1);
j_got_memcpy_chk(v47, StringUTFChars, v26, -1);
v46 = &v47[v26];
j_got_memcpy_chk(v46, v29, v25, -1);
v45 = &v46[v25];
j_got_memcpy_chk(v45, v28, v24, -1);
j_got_memcpy_chk(&v45[v24], v27, v23, -1);
v47[v26 + v25 + v24 + v23] = 0;
v22 = (void *)_JNIEnv::NewStringUTF(a1, v47);
j_got_free(v47);
v21 = (void *)_JNIEnv::NewStringUTF(a1, "gwFG#9P!ad+*PMUnisapps$Rs#L!_1G9");
v20 = _JNIEnv::FindClass(a1, "java/lang/String");
v19 = _JNIEnv::NewStringUTF(a1, "utf-8");
v18 = _JNIEnv::GetMethodID(&a1->functions, v20, "getBytes", "(Ljava/lang/String;)[B");
_JNIEnv::CallObjectMethod(&a1->functions, v21, v18);
_JNIEnv::CallObjectMethod(&a1->functions, v22, v18);
v17 = _JNIEnv::FindClass(a1, "javax/crypto/spec/SecretKeySpec");
if ( v17 == nullptr )
return 0;
v16 = _JNIEnv::GetMethodID(&a1->functions, v17, "<init>", "([BLjava/lang/String;)V");
v15 = _JNIEnv::NewStringUTF(a1, "HmacSHA1");
_JNIEnv::NewObject(&a1->functions, v17, v16);
v14 = _JNIEnv::FindClass(a1, "javax/crypto/Mac");
if ( v14 == nullptr )
return 0;
StaticMethodID_1 = _JNIEnv::GetStaticMethodID(
&a1->functions,
v14,
"getInstance",
"(Ljava/lang/String;)Ljavax/crypto/Mac;");
v13 = (void *)_JNIEnv::CallStaticObjectMethod(&a1->functions, v14, StaticMethodID_1, v15);
v12 = _JNIEnv::GetMethodID(&a1->functions, v14, "init", "(Ljava/security/Key;)V");
v11 = _JNIEnv::GetMethodID(&a1->functions, v14, "doFinal", "([B)[B");
_JNIEnv::CallVoidMethod(&a1->functions, (__int64)v13, (__int64)v12);
ObjectField = _JNIEnv::CallObjectMethod(&a1->functions, v13, v11);
ArrayLength = _JNIEnv::GetArrayLength(&a1->functions, (__int64)ObjectField);
if ( ArrayLength == 0 )
return 0;
ByteArrayElements = _JNIEnv::GetByteArrayElements(&a1->functions, ObjectField, nullptr);
v7 = (char *)j_got_malloc(ArrayLength + 1);
j_got_memcpy_chk(v7, ByteArrayElements, ArrayLength, -1);
v7[ArrayLength] = 0;
v6 = ZMCoreUnZip_BXF_encode(v7, ArrayLength);
if ( v6 == nullptr )
return 0;
_JNIEnv::DeleteLocalRef(&a1->functions, (__int64)v22);
_JNIEnv::DeleteLocalRef(&a1->functions, v15);
_JNIEnv::DeleteLocalRef(&a1->functions, (__int64)v21);
_JNIEnv::DeleteLocalRef(&a1->functions, v19);
j_got_free(v7);
return _JNIEnv::NewStringUTF(a1, v6);
}

伪代码

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
int ZMCoreUnzip_executeCVS(Context context) {
String packageName = context.getPackageName();
if (!packageName.equals("com.fileunzip.zxwknight")) {
return -300;
}

PackageManager pm = context.getPackageManager();
int flags = (SDK_INT >= 28) ? 0x08000000 : 0x40;

PackageInfo pi = pm.getPackageInfo(packageName, flags);

// Android 9+ 走 signingInfo,旧版本走 PackageInfo.signatures
Signature[] signatures = GetApkSignatures(pi);

// 当前样本最终取第一个签名对象的 hashCode()
return signatures[0].hashCode();
}

jstring NativeGetQPWATImpl(JNIEnv *env, jclass clazz, jstring input, jobject context) {
String packageName = context.getPackageName(); // 当前样本 = "com.fileunzip.zxwknight"
int ctxValue = ZMCoreUnzip_executeCVS(context); // 当前样本静态复现 = 516622220

int tail = 2 * ((ctxValue >> 2) - 169) + 127; // 见 0x4B9008-0x4B9030

String data =
String.valueOf(ctxValue) + // 前缀1:签名 hashCode
packageName + // 前缀2:包名
input + // 中间:Java 侧传来的 fileName + fileSize
String.valueOf(tail); // 后缀:由 ctxValue 推导

byte[] key = "gwFG#9P!ad+*PMUnisapps$Rs#L!_1G9".getBytes("utf-8");
byte[] body = data.getBytes("utf-8");

Mac mac = Mac.getInstance("HmacSHA1"); // 算法固定为 HMAC-SHA1
mac.init(new SecretKeySpec(key, "HmacSHA1"));
byte[] digest = mac.doFinal(body);

return ZMCoreUnZip_BXF_encode(digest); // Base64 编码别名,编码表为 A-Z a-z 0-9 + / =
}

POC

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
#!/usr/bin/env python3
"""
Minimal fixed-value PoC for:
1. submit zip password -> /api/v3/sync/ads
2. query zip password -> /api/v3/search/info

Usage:
python min_zip_password_poc.py submit "a.zip" "123456"
python min_zip_password_poc.py query "a.zip"
"""

import argparse
import base64
import hashlib
import hmac
import json
import os
from pathlib import Path

import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad

REGION = "CN"

PACKAGE_NAME = "com.fileunzip.zxwknight"
VERSION_NAME = "3.2.23"
APP_ID = "zipa"
PLATFORM = "android"
CTX_VALUE = 516622220

QUERY_URL = "https://file.unisapps.com/api/v3/search/info"
SUBMIT_URL = "https://file.unisapps.com/api/v3/sync/ads"

ACCESS_KEY = b"gwFG#9P!ad+*PMUnisapps$Rs#L!_1G9"
AES_KEY = b"EyR2JvBXJXaUdY9auxetvhpEeQ8DmC6L"

def file_md5(path: Path) -> str:
md5 = hashlib.md5()
with path.open("rb") as fp:
for chunk in iter(lambda: fp.read(1024 * 1024), b""):
md5.update(chunk)
return md5.hexdigest()

def build_user_client_access(file_name: str, file_size: int) -> str:
tail = 2 * ((CTX_VALUE >> 2) - 169) + 127
text = f"{CTX_VALUE}{PACKAGE_NAME}{file_name}{file_size}{tail}".encode("utf-8")
digest = hmac.new(ACCESS_KEY, text, hashlib.sha1).digest()
return base64.b64encode(digest).decode("ascii")

def encrypt_json(obj: dict) -> str:
plain = json.dumps(obj, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
iv = os.urandom(16)
cipher = AES.new(AES_KEY, AES.MODE_CBC, iv)
data = cipher.encrypt(pad(plain, AES.block_size))
body = {
"data": base64.b64encode(data).decode("ascii"),
"iv": base64.b64encode(iv).decode("ascii"),
}
return json.dumps(body, ensure_ascii=False, separators=(",", ":"))

def try_decrypt_response(text: str) -> str | None:
try:
obj = json.loads(text)
data = base64.b64decode(obj["data"])
iv = base64.b64decode(obj["iv"])
except Exception:
return None

cipher = AES.new(AES_KEY, AES.MODE_CBC, iv)
try:
plain = unpad(cipher.decrypt(data), AES.block_size)
except Exception:
return None
return plain.decode("utf-8", errors="replace")

def submit_password(file_name: str, file_size: int, md5_value: str, password: str) -> None:
body = encrypt_json(
{
"fileName": file_name,
"fileSize": file_size,
"pd": password,
"region": REGION,
"md5": md5_value,
}
)
headers = {
"Content-Type": "application/json; charset=utf-8",
"appid": APP_ID,
"version": VERSION_NAME,
"platform": PLATFORM,
}
resp = requests.post(SUBMIT_URL, data=body.encode("utf-8"), headers=headers, timeout=20)
print("[submit] status =", resp.status_code)
print("[submit] body =", resp.text)

def query_password(file_name: str, file_size: int, md5_value: str) -> None:
body = encrypt_json(
{
"fileName": file_name,
"fileSize": file_size,
"md5": md5_value,
}
)
headers = {
"Content-Type": "application/json; charset=utf-8",
"User-Client-Access": build_user_client_access(file_name, file_size),
"appid": APP_ID,
"version": VERSION_NAME,
"platform": PLATFORM,
}
resp = requests.post(QUERY_URL, data=body.encode("utf-8"), headers=headers, timeout=20)
print("[query] status =", resp.status_code)
print("[query] body =", resp.text)

plain = try_decrypt_response(resp.text)
if plain is not None:
print("[query] decrypted =", plain)

def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Minimal zip password submit/query PoC")
parser.add_argument("action", choices=["submit", "query"])
parser.add_argument("path", help="local archive path")
parser.add_argument("password", nargs="?", help="required for submit")
args = parser.parse_args()
if args.action == "submit" and not args.password:
parser.error("password is required for submit")
return args

def main() -> None:
args = parse_args()
file_path = Path(args.path)
if not file_path.is_file():
raise SystemExit(f"file not found: {file_path}")

file_name = file_path.name
file_size = file_path.stat().st_size
md5_value = file_md5(file_path)

print("fileName =", file_name)
print("fileSize =", file_size)
print("md5 =", md5_value)

if args.action == "submit":
submit_password(file_name, file_size, md5_value, args.password)
if args.action == "query":
query_password(file_name, file_size, md5_value)

if __name__ == "__main__":
main()
阅读更多

Wechat 开启 H5 DevTools

2026/07/03

环境:Android 微信 8.0.69
目标:让 chrome://inspect 能看到并调试微信内打开的 H5 网页 (真实 DOM / Console / Network / 断点), 不包括小程序

背景

微信有两套浏览器内核

微信打开网页不是用一个固定的浏览器,而是在 XWeb 框架下可选多个内核。本文只关心两个:

Pinus 内核(默认)

Pinus 是腾讯基于 Chromium 自研裁剪的浏览器内核(源码路径里叫 weblayer,基于 Chrome 116),
libxwebcore.so(135MB)的形式打包在微信里,类名 com.tencent.xweb.pinus.sdk.WebView,
内核类型枚举值 WV_KIND_PINUS

它的调试不走标准通道:Pinus 不开标准的 webview_devtools_remote 调试端口,而是走微信
自己的私有远程调试(往 xweb.weixin.qq.com 拨 WebSocket 隧道),chrome 连不上

系统 WebView 内核

就是 Android 系统自带的 WebView(本机是 libmonochrome_64.so,171MB,Chrome 130),
内核类型 WV_KIND_SYS。它是标准 Chromium,开启调试后会 bind 标准的
webview_devtools_remote_<pid> 端口,chrome://inspect 直接就能连

开启 webview 调试为什么无法连接上

调用 android.webkit.WebView.setWebContentsDebuggingEnabled(true) 想开调试但因为渲染页面的是 Pinus,开调试端口的却是系统 WebView。所以 chrome://inspect 里能看到”远程浏览器”,却没有可点的 page。

1
2
3
4
5
com.tencent.mm (UI 进程)
├── Pinus 内核 (libxwebcore, Chrome116)
│ 渲染 H5 页面 ✓ 不开标准调试端口 ✗ -> 走私有隧道 → xweb.weixin.qq.com
└── 系统 WebView 内核 (libmonochrome, Chrome130)
开调试端口 ✓ 默认不渲染页面 ✗ -> /json 恒为 [],无法 inspect

一、系统 WebView 内核方案

1. 做法总览

强制微信用系统 WebView 渲染 H5 + 开启系统 WebView 调试。 setWebContentsDebuggingEnabled(true) 只对 系统 WebView 那套生效

2. 选择内核

1
2
3
4
5
6
WebView.I0(ctx, kind, module, cb)       initWebviewCore — 真正的初始化入口
├─ WebView.H0(kind, module) getPreferedWebviewType — 解析用哪个内核
│ └─ t0.a.c(module) t0.a = new u0() (u0 的静态单例)
│ └─ u0.c(module) 读 HardCodeWebView<module> → ABTestWebView<module>
│ └─ z3.f() 打开 SharedPreferences "xweb_debug"
└─ WebView.J0(ctx, H0 返回值, …) 成功则 WebView.m = kind, 进程内只决策一次

2.1 枚举 f1

1
2
3
4
5
6
7
8
// com.tencent.xweb.f1  (X5 字段已移除, 仅留在 values 数组)
public enum f1 {
WV_KIND_NONE, // ordinal 0 → 字段 d
WV_KIND_CW, // ordinal 1 → 字段 e
WV_KIND_X5, // ordinal 2 (无字段, 已移除)
WV_KIND_SYS, // ordinal 3 → 字段 f
WV_KIND_PINUS // ordinal 4 → 字段 g
}

2.2 WebView.H0 — 决策入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// com.tencent.xweb.WebView.H0(f1 kind, String module) 原名 getPreferedWebviewType
public static f1 H0(f1 f10, String s) {
f1 f11 = f1.d; // d = NONE
if (WebView.m != f11) return WebView.m; // ★ 已决策过 → 直接用缓存 (进程内只一次)
if (br5.a.c()) { br5.s0.d(69L, 1); return f1.f; } // x86 → f = SYS

f1 f13 = t0.a.c(s); // 读 HardCodeWebView<module> (见 2.3)
if (f13 == f11) { // 没配 HardCode
String s1 = b.m().g("setwebtype", s); // 退而读服务器 cmd 配置
if (s1 != null && !s1.isEmpty()) f10 = f1.valueOf(s1);
} else {
f10 = f13; // 用 HardCode (优先级最高, 跳过 cmd 配置)
}

f1 f12 = f1.f; // 默认 = SYS
if (f10 != f1.e && f10 != f1.g) { // 非 CW(e) 且 非 PINUS(g)
f12 = f10; // 采用 f10
} else {
WebView.p = true; // 标记 xweb 不可用, 兜底 SYS
}
return f12; // 交给 I0→J0 写进 WebView.m
}

代码里直接读出三点:

  • 只决策一次:WebView.m != NONE 就直接返回。WebView.mI0(initWebviewCore)→ J0 在第一个 WebView 创建时写一次(WebView.m = f10),之后全进程复用 —— 运行时改 WebView.m 或刷页面没用,旧 WebView 早用 Pinus 建好了。
  • HardCode 优先级最高:t0.a.c(module) 返回非 NONE 就直接采用,根本不读服务器 setwebtype
  • CW / PINUS 兜底成 SYS:落 else 分支 WebView.p=true。我们写 WV_KIND_SYS 不触发兜底,稳走 SYS。

2.3 u0.c — 读 HardCodeWebView

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// com.tencent.xweb.u0.c(String module)
public f1 c(String s) {
f1 f10 = f1.d; // NONE
if (s == null || s.isEmpty() || this.a == null) return f10;
SharedPreferences sp = z3.f(); // 打开 "xweb_debug" (见 2.4)
if (sp == null) return f10;
String s1 = sp.getString("HardCodeWebView" + s, ""); // ★ 先 HardCode
if (s1 == null || s1.isEmpty() || s1.equals("WV_KIND_NONE")) {
s1 = sp.getString("ABTestWebView" + s, ""); // 再 ABTest 兜底
}
if (s1 == null || s1.isEmpty()) return f10;
try { this.b = f1.valueOf(s1); } catch (Throwable t) { this.b = f10; }
return this.b;
}

2.4 z3.f — 打开 xweb_debug

1
2
3
4
5
6
7
8
// br5.z3
public static SharedPreferences f() { return z3.h("xweb_debug"); }

public static SharedPreferences h(String s) { // z3.h = 经典 SP (非 MMKV)
Context context0 = z3.b;
if (context0 == null) return null;
return context0.getSharedPreferences(s, 4); // MODE_MULTI_PROCESS
}

串起来:往 xweb_debugHardCodeWebViewmm = WV_KIND_SYSu0.c("mm") 返回 SYS → H0 采用、返回 SYS → I0/J0 写进 WebView.m → 系统内核渲染、标准调试端口有页面。

3. 落地代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookDevTools() {
// 选择内核 H0 恒返回 SYS
const W = Java.use('com.tencent.xweb.WebView')
const F: any = Java.use('com.tencent.xweb.f1')
const SYS = F.valueOf('WV_KIND_SYS')
W.H0.implementation = function (kind: any, m: any) {
W.m.value = SYS
log('call com.tencent.xweb.WebView.H0', kind, m, '->', SYS)
return SYS
}
// 启用调试
Java.perform(() => {
Java.scheduleOnMainThread(() => {
Java.use('android.webkit.WebView').setWebContentsDebuggingEnabled(true);
utils.log('call WebView.setWebContentsDebuggingEnabled(true)')
})
})
}

4. 注入哪个进程、具体怎么做

核心前提:hookDevTools 只要保证 WebView.H0首次点开 H5 网页之前被 hook 住即可。

注入进程:UI 进程 com.tencent.mm(无后缀)。它是最终渲染 H5、H0 选择内核的进程。

完整流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 完全关闭微信
adb shell am force-stop com.tencent.mm

# 2. 手动打开微信 App 先别点任何网页

# 3. frida attach UI 进程, 加载脚本

# 4. 在微信里点开目标 H5 网页
# → 首次点击触发 H0 → 被 hook 强制成系统 WebView 内核渲染
# → setWebContentsDebuggingEnabled(true) 已开, 页面注册进标准调试端口

# 5. 电脑 chrome 打开 chrome://inspect
# → Devices 里直接出现该页面 → 点 inspect (真 DOM/Console/Network/断点)

二、Pinus 内核自己的私有调试(存档)

「不切系统内核、直接在 Pinus 上调试」的可行性分析
结论:技术可行,但要自己搭中转 + 破触发锁,投入产出比低
基于 libxwebcore.so(Chrome116)逆向

6. Pinus 调试是「往外拨」,不是「等你连」

系统 WebView 是开个本地端口等 chrome 来连(server 模型)。Pinus 相反 —— 它是客户端,
主动拨号连微信服务器,把 CDP 流量通过 WebSocket 隧道转出去:

1
2
3
4
5
6
7
8
9
10
11
12
13
页面 JS 调 window.xweb_remote_debug.RemoteDebugStart(token)
│ (该 JS 对象由 render frame observer 注入)

① 注册设备 POST https://xweb.weixin.qq.com/api/rd/create_device

② 设备隧道 连 wss://xweb.weixin.qq.com/rd/device/<deviceId>?role=user
▼ (等服务器下发 JSON 命令)
③ 服务器下发 {"method":"XWeb.inspect","params":{"targetId":..,"deviceToken":..}}

④ Pinus 用 targetId 找到真实 DevToolsAgentHost (和系统内核同一套 host 查找)

⑤ 目标隧道 连 wss://xweb.weixin.qq.com/rd/target/<targetId>?role=user&token=<token>
这条隧道里跑的就是【真实原生 CDP】

设备隧道命令(XWebDevTools_OnTunnelData):XWeb.targets / XWeb.version /
XWeb.inspect / XWeb.close / XWeb.NetLog*CDP 会话本身是完整原生的,只是被套进
隧道发去了微信服务器。

7. 帧格式与地址(静态读出)

帧格式:XWebWebSocketAdapter::Write(0x270A3D4)拿一段裸字节直接塞进 WebSocket,
无自定义封包,就是「JSON 文本 over WebSocket」:

  • 设备隧道:收 {"method":"XWeb.inspect","params":{...}},发 {"event":"...","data":{...}}
  • 目标隧道:收发就是裸 CDP JSON({"id":1,"method":"Runtime.evaluate",...} 及其 response),
    进出点 sub_270CF24(server→CDP)/ sub_3627EEC(CDP→server)

三个地址全写死 host = xweb.weixin.qq.com,只有 id/token 是变量:
api/rd/create_devicerd/device/%srd/target/%s?token=%s

8. 触发被三重锁死(私有化的真正门槛)

想让 Pinus 自己发起,入口是页面 JS window.xweb_remote_debug.RemoteDebugStart(token),但:

  1. JS 注册域名锁:RemoteDebugStart 只在页面域名 = xweb.weixin.qq.com 时才挂到
    window.xweb_remote_debug(sub_2715ABC)。普通 H5 页面只有 RemoteDebugStatus/Stop
  2. 二次域名校验:HandleRemoteDebugStart(0x2716500)里又查一遍,非该域名返回 invalid domain
  3. 需要 token + renderer 线程:token 来自微信调试门户,create_device 也要联微信服务器。

9. 私有化路线(若非做不可)

  1. spawn 时(页面加载前)hook std::string == "xweb.weixin.qq.com"(0x26D8860,窄化到只对
    该域名返回真)绕过两处域名锁。
  2. hook XWeb_RD_CreateDevice(0x2711A18)、XWebTarget_StartClientProxy(0x270D194),把
    xweb.weixin.qq.com 改写成 ws://localhost:<port>
  3. 本地中转扮演三个角色:create_device(返 deviceId)+ 设备隧道(下发
    XWeb.inspect{targetId, deviceToken},targetId 从 XWeb.targets 返回里拿)+ 目标隧道
    (裸 CDP 双向桥给 chrome 前端)。
  4. 注入帧要打进真正渲染内容的 render frame,不是 appbrand 的 about:blank 容器帧。

实测尝试运行时抓帧,卡在:触发锁 + 多进程漂移(H5 落 :appbrand0)+ 注入到空容器帧。
比板块一重得多,除非必须用 Pinus 内核渲染又要调试,否则不建议


附:关键符号

符号 作用
com.tencent.xweb.f1 内核类型枚举 (WV_KIND_*)
com.tencent.xweb.WebView.H0(f1, String) 内核决策入口 (getPreferedWebviewType)
com.tencent.xweb.WebView.m 缓存已决策内核的静态字段
com.tencent.xweb.u0.c(String) 从 xweb_debug SP 读 HardCodeWebView
br5.z3.f() 打开 SharedPreferences “xweb_debug”
com.tencent.xweb.pinus.sdk.WebView Pinus 内核 WebView

Native 模块:

模块 说明
libxwebcore.so Pinus 内核 (Chrome 116),渲染页面
libmonochrome_64.so 系统 WebView (Chrome 130),标准调试端口
socket webview_devtools_remote_<pid> 系统 WebView 的调试端口

libxwebcore.so Pinus 私有调试函数(libxwebcore.so.i64):

RVA 名字 作用
0x2715658 XWebFrameHelper_CreateXWebExtend 注入 window.xweb_remote_debug
0x2715ABC (AddRemoteDebugMethods) 注册 JS 方法;RemoteDebugStart 有域名锁
0x2716500 (HandleRemoteDebugStart) Start 处理,二次域名校验 + 取 token
0x2711A18 XWeb_RD_CreateDevice 注册设备,硬编码 create_device URL
0x2711F64 (device tunnel WS) 开设备隧道 rd/device
0x270EB0C XWebDevTools_OnTunnelData 设备隧道命令分发
0x270D194 XWebTarget_StartClientProxy 开目标隧道,硬编码 rd/target URL
0x270CF24 (target onMessage) 目标隧道 server→CDP 入口
0x3627EEC (session dispatch) CDP→server 出口
0x270A3D4 XWebWebSocketAdapter::Write 裸字节写 WS(无自定义封包)
0x26D8860 std::string::operator==(char*) 域名锁字符串比较(绕过点)
0x3615C5C XWeb_GetTabHostById 按 targetId 查真实 DevToolsAgentHost

提取 libxwebcore.so:解 split_delivery.config.arm64_v8a.apk → 取 libxwebfullpack.so
(是个 zip)→ 再解出 libxwebcore.so
提取 libmonochrome_64.so:从 com.google.android.trichromelibrary_*base.apk
lib/arm64-v8a/libmonochrome_64.so

阅读更多

2026 腾讯游戏安全 PC 初赛 WP

2026/04/30

驱动加载

使用 EfiGuard 进行 PG 和 DSE 修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 挂载 EFI 到 M:,注意在 explorer 中是看不到的
mountvol M: /S
M:
mkdir \EFI\EfiGuard

copy C:\EfiGuard\EFI\Boot\Loader.efi M:\EFI\EfiGuard\
copy C:\EfiGuard\EFI\Boot\EfiGuardDxe.efi M:\EFI\EfiGuard\

// 创建启动项,生成一个 GUID,下面要用
bcdedit /copy {bootmgr} /d "EfiGuard"
bcdedit /set {上面的GUID} path \EFI\EfiGuard\Loader.efi
bcdedit /set {fwbootmgr} displayorder {上面的GUID} /addfirst

// 取消第一启动,恢复
bcdedit /set {fwbootmgr} displayorder {bootmgr} /addfirst

绿字进系统后关闭 DSE 就可以加载驱动了

1
2
3
4
5
EfiDSEFix.exe -d

Disabling DSE...
CI!g_CiOptions at 0xFFFFF800648391B0.
Successfully disabled DSE. Original g_CiOptions value: 0xE

启动驱动后运行程序

image.png

exe 分析

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
int __fastcall main_0(int argc, const char **argv, const char **envp)
{
__int64 v4; // rcx
int inputPos; // ebx
__int64 wirtePos; // rsi
unsigned int inputChar; // edi
__int64 code; // rdx
char _AL; // al
__int64 _RCX; // rcx
char theSend; // al
int v13; // r8d
int status; // eax
DWORD LastError; // edx
char *seq; // rdx
_DWORD moveBufOut[2]; // [rsp+20h] [rbp-188h] BYREF
ioctl_8001200c_out map; // [rsp+28h] [rbp-180h] BYREF
_OWORD buf[4]; // [rsp+40h] [rbp-168h] BYREF
int (__stdcall *send)(SOCKET, const char *, int, int); // [rsp+80h] [rbp-128h] BYREF
_BYTE Buf[176]; // [rsp+E0h] [rbp-C8h] BYREF

InitTips();
printf("[*] Connecting to Shadow gate driver...\n");
hDevice = (HANDLE)OpenDriver();
if ( hDevice == (HANDLE)-1LL )
{
printf("[!] Cannot continue without driver.\n");
printf("[*] Press any key to exit...\n");
getch();
return 1;
}
printf("[+] Gate module online.\n\n");
sub_140001670();
memset(&map, 0, sizeof(map));
if ( IO_8001200C(v4, &map) )
printf("[*] Maze grid: %ux%u, Entry=(%u,%u), Exit=(%u,%u)\n", map.x, map.y, map.ex, map.ey, map.exX, map.exY);
PrintOperatorTips();
memset(&send, 0, 0x100u);
inputPos = 0;
wirtePos = 0;
while ( 2 )
{
printf("[op #%d] > ", inputPos);
inputChar = getch();
printf("%c\n", inputChar);
inputChar -= 0x1B;
_AL = inputChar;
_RCX = (unsigned __int16)Buf;
LOBYTE(_RCX) = (unsigned __int8)Buf + 11;
switch ( inputChar )
{
case '\0':
case '6': // Q
case 'V': // q
goto Exit;
case '$': // ?Hh
case '-':
case 'M':
PrintOperatorTips();
continue;
case '&': // AJaj left
case '/':
case 'F':
case 'O':
LOBYTE(code) = 0x30;
theSend = 'L';
goto Send;
case ')': // DLdl right
case '1':
case 'I':
case 'Q':
LOBYTE(code) = 0x40;
__asm { rcl al, cl; Rotate Through Carry Left }
theSend = 'R';
goto Send;
case '.':
case '<':
case 'N':
case '\\':
LOBYTE(code) = 0x10;
theSend = 'U';
goto Send;
case '0':
case '8':
case 'P':
case 'X':
LOBYTE(code) = 0x20;
theSend = 'D';
Send:
if ( inputPos < 255 )
{
*((_BYTE *)Buf + wirtePos + 80) = theSend;
++inputPos;
++wirtePos;
if ( (unsigned int)inputPos >= 0x100 )
_report_securityfailure_w(_RCX);
*((_BYTE *)Buf + wirtePos + 80) = 0;
}
v13 = dword_140005668;
memset(buf, 0, sizeof(buf));
moveBufOut[0] = 0;
++dword_140005668;
status = IO_80012004(hDevice, code, v13, buf, moveBufOut);
if ( status != 1 )
{
if ( status == -1 )
{
LastError = GetLastError();
printf("[ERROR] DeviceIoControl failed: %lu\n", LastError);
}
continue;
}
printf(L"\n");
printf("=============================================\n");
printf(" ACCESS GRANTED - Credential extracted!\n");
printf("=============================================\n");
if ( moveBufOut[0] )
{
printf("[CREDENTIAL] %s\n", (const char *)buf);
}
else
{
printf("[*] You reached the exit, but the credential could not be decrypted.\n");
printf("[*] Hint: Only the shortest path unlocks the credential.\n");
}
printf("[*] Your input (%d ops sent): %s\n", inputPos, (const char *)&send);
printf("[*] Note: blocked ops are included above; the driver only counts successful ones.\n");
printf(L"\n");
memset(buf, 0, sizeof(buf));
printf("[*] Press any key to exit...\n");
getch();
Exit:
Close3Func(_RCX);
if ( hDevice != (HANDLE)-1LL )
CloseHandle(hDevice);
printf("[*] Mission aborted. ACE HQ standing by.\n");
return 0;
case '7':
case 'W':
IO_80012008_Reset();
inputPos = 0;
dword_140005668 = 0;
LOBYTE(send) = 0;
wirtePos = 0;
printf("[*] Reset to entry point.\n");
continue;
case '9':
case 'Y':
printf("[*] Operations sent: %d\n", inputPos);
seq = (char *)&send;
if ( inputPos <= 0 )
seq = "(none)";
printf("[*] Sequence: %s\n", seq);
continue;
default:
printf("[?] Unknown command. 'h' for help.\n");
continue;
}
}
}

重点在驱动交互,其余均为负责输入的逻辑

IO_8001200C

初始化

1
2
3
4
5
6
7
BOOL __fastcall IO_8001200C(__int64 a1, void *lpOutBuffer)
{
DWORD BytesReturned; // [rsp+40h] [rbp-18h] BYREF

BytesReturned = 0;
return DeviceIoControl(hDevice, 0x8001200C, 0, 0, lpOutBuffer, 0x18u, &BytesReturned, 0);
}
1
2
3
4
5
6
7
8
9
struct __unaligned ioctl_8001200c_out
{
DWORD x;
DWORD y;
DWORD ex;
DWORD ey;
DWORD exX;
DWORD exY;
};

IO_80012004

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
__int64 __fastcall IO_80012004(void *hDevice, __int64 _RDX, int a3, _BYTE *buf, DWORD *out)
{
__int64 v5; // rdi
char v6; // al
unsigned int rest; // esi
int lpInBuffer__1; // eax
DWORD Size; // ecx
__int64 Size_1; // rdi
MoveOutBuf IOOutBuf; // [rsp+20h] [rbp-C8h] BYREF
DWORD lpBytesReturned; // [rsp+B0h] [rbp-38h] BYREF
ioctl_80012004_in lpInBuffer_; // [rsp+B8h] [rbp-30h] BYREF
__int64 v17; // [rsp+D0h] [rbp-18h]

v17 = v5;
v6 = _RDX ^ 0xFA;
rest = 0;
memset(&IOOutBuf, 0, sizeof(IOOutBuf));
LOBYTE(_RDX) = (unsigned __int8)(_RDX ^ 0x40) >> 5;
lpInBuffer__1 = (unsigned __int8)(_RDX | (8 * v6));
__asm { rcr rdx, 8Bh; Rotate Through Carry Right }
*(_QWORD *)&lpInBuffer_.code = (unsigned __int8)lpInBuffer__1;
lpBytesReturned = 0;
lpInBuffer_.check = a3 ^ lpInBuffer__1 ^ 0xDEAD1337;
if ( DeviceIoControl(hDevice, 0x80012004, &lpInBuffer_, 0xCu, &IOOutBuf, 0x84u, &lpBytesReturned, 0) )
{
if ( *(_DWORD *)IOOutBuf.msg == 'WIN!' )
{
rest = 1;
if ( buf )
{
if ( out )
{
Size = IOOutBuf.size;
*out = IOOutBuf.size;
if ( Size - 1 > 0x3E )
{
*buf = 0;
}
else
{
Size_1 = Size;
memcpy(buf, IOOutBuf.p2, Size);
buf[Size_1] = 0;
}
}
}
}
}
else
{
rest = -1;
}
memset(&IOOutBuf, 0, sizeof(IOOutBuf));
return rest;
}

负责移动,发送给驱动的数据结构见下

1
2
3
4
5
6
struct ioctl_80012004_in
{
DWORD code;
DWORD pos;
DWORD 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
from struct import pack

def calcMoveCode(_RDX):
'''
计算键盘映射到驱动键盘映射
0x10 up
0x20 down
0x30 left
0x40 right
:param _RDX:
:return:
'''
v6 = (_RDX ^ 0xFA) & 0xff
_RDX = ((_RDX ^ 0x40) >> 5) & 0xff
res = (_RDX | (8 * v6)) & 0xff
print(hex(res))
return res

def IO_MOVE_GenBuf(code, index):
code = calcMoveCode(code)
check = (code ^ index ^ 0xDEAD1337) & 0xffffffff
print(hex(check))

b = pack('<III', code, index, check)
print(f'{b.hex()}')

IO_MOVE_GenBuf(0x30, 0)
IO_MOVE_GenBuf(0x20, 1)
IO_MOVE_GenBuf(0x10, 2)
IO_MOVE_GenBuf(0x40, 3)

IO_80012008_Reset

重置迷宫

1
2
3
4
5
6
7
BOOL IO_80012008_Reset()
{
DWORD BytesReturned; // [rsp+40h] [rbp-18h] BYREF

BytesReturned = 0;
return DeviceIoControl(hDevice, 0x80012008, 0, 0, 0, 0, &BytesReturned, 0);
}

sys 分析

有一些难以分析的 call,nop 掉看整体流程

image.png

载入点

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
__int64 __fastcall sub_140003208(struct _DRIVER_OBJECT *DriverObject)
{
void *Pool2; // rax
NTSTATUS Pool2_1; // edi
struct _UNICODE_STRING DestinationString; // [rsp+40h] [rbp-18h] BYREF

Pool2 = (void *)ExAllocatePool2(64, 472, 0x657A614D);
MemoryPool = Pool2;
if ( !Pool2 )
return 0xC000009ALL;
Pool2_1 = (int)Pool2; // 混淆
if ( (int)Pool2 < 0
|| (KeInitializeSpinLock(&SpinLock),
KeInitializeSpinLock(&SpinLock_),
((void (*)(void))loc_140001E60)(),
DestinationString = 0,
RtlInitUnicodeString(&DestinationString, L"\\Device\\ShadowGate"),
Pool2_1 = IoCreateDevice(DriverObject, 0, &DestinationString, 0x22u, 0x100u, 0, &DeviceObject),
Pool2_1 < 0) )
{
_mm_lfence();
ExFreePoolWithTag(MemoryPool, 0x657A614Du);
LABEL_5:
MemoryPool = 0;
return (unsigned int)Pool2_1;
}
RtlInitUnicodeString(&SymbolicLinkName, L"\\??\\ShadowGate");
Pool2_1 = IoCreateSymbolicLink(&SymbolicLinkName, &DestinationString);
if ( Pool2_1 < 0 )
{
_mm_lfence();
IoDeleteDevice(DeviceObject);
ExFreePoolWithTag(MemoryPool, 0x657A614Du);
DeviceObject = 0;
goto LABEL_5;
}
DriverObject->DriverUnload = (PDRIVER_UNLOAD)sub_140001840;
DriverObject->MajorFunction[0] = (PDRIVER_DISPATCH)sub_1400014B0;
DriverObject->MajorFunction[2] = (PDRIVER_DISPATCH)sub_140001410;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = (PDRIVER_DISPATCH)j_IRP_Control_Func;
DeviceObject->Flags |= 4u;
DeviceObject->Flags &= ~0x80u;
return 0;
}

Contorl 里面也有两处难以分析 call,nop 掉看整体逻辑

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
__int64 __fastcall IRP_Control_Func(__int64 a1, IRP *IRP, __int64 a3, char a4)
{
__int16 v4; // r11
__int16 v5; // r13
__int16 v6; // di
struct _IO_STACK_LOCATION *CurrentStackLocation; // rax
unsigned int Status_1; // ebp
ULONG_PTR info; // r8
ULONG IoControlCode; // edx
unsigned int inputLen; // edi
unsigned int outLen; // eax
buf_u *SystemBuffer; // r14
unsigned int Status; // edx
KIRQL NewIrql_1; // al
KSPIN_LOCK *SpinLock; // rcx
KIRQL NewIrql; // al
struct_MemoryPool *P; // rcx
unsigned int _____1; // eax
unsigned int ____; // esi
KIRQL NewIrql_4; // al
struct_MemoryPool *MemoryPool; // rdi
KIRQL NewIrql_2; // bl
HANDLE CurrentThreadId; // rax
KSPIN_LOCK *p_kspin_lock1C0; // rcx
KSPIN_LOCK *p_kspin_lock1C0_1; // rcx
KIRQL NewIrql_5; // al
char _DI_1; // cl
KIRQL NewIrql_3; // si
__int64 n255; // rdi
KSPIN_LOCK *p_kspin_lock1C0_2; // rcx
DWORD v37; // [rsp+0h] [rbp-158h] BYREF
__int64 v38; // [rsp+8h] [rbp-150h]
__m128 buf[16]; // [rsp+20h] [rbp-138h] BYREF

v6 = __ROR2__(v5, 1);
LOBYTE(v6) = a4 - v6;
CurrentStackLocation = IRP->Tail.Overlay.CurrentStackLocation;
_DI = v6 - v4;
__asm { rcr di, 0AFh }
Status_1 = 0;
info = 0;
IoControlCode = CurrentStackLocation->Parameters.DeviceIoControl.IoControlCode;
inputLen = CurrentStackLocation->Parameters.DeviceIoControl.InputBufferLength;
outLen = CurrentStackLocation->Parameters.DeviceIoControl.OutputBufferLength;
SystemBuffer = (buf_u *)IRP->AssociatedIrp.SystemBuffer;
if ( ::MemoryPool )
{
switch ( IoControlCode )
{
case 0x80012004:
if ( SystemBuffer && inputLen >= 0xC && outLen >= 0x84 )// move?
{
v38 = *(_QWORD *)&SystemBuffer->_12004.code;
if ( SystemBuffer->_12004.check == ((unsigned __int8)v38 ^ HIDWORD(v38) ^ 0xDEAD1337) )
{
NewIrql = KeAcquireSpinLockRaiseToDpc(&::MemoryPool->kspin_lock1C0);
P = ::MemoryPool;
++::MemoryPool->dwordBC;
KeReleaseSpinLock(&P->kspin_lock1C0, NewIrql);
____ = _____1; // 可能混淆
customMemset(SystemBuffer, 0, 0x84u);
NewIrql_4 = KeAcquireSpinLockRaiseToDpc(&::MemoryPool->kspin_lock1C0);
MemoryPool = ::MemoryPool;
NewIrql_2 = NewIrql_4;
CurrentThreadId = PsGetCurrentThreadId();
p_kspin_lock1C0 = &::MemoryPool->kspin_lock1C0;
*(_QWORD *)MemoryPool[1].gap8 = CurrentThreadId;
KeReleaseSpinLock(p_kspin_lock1C0, NewIrql_2);
((void (__fastcall *)(struct_MemoryPool *, _QWORD))loc_14040305A)(::MemoryPool, ____);
if ( ____ == 2 )
{
p_kspin_lock1C0_1 = &::MemoryPool->kspin_lock1C0;
SystemBuffer[2]._1200c.enY = 'WIN!';
NewIrql_5 = KeAcquireSpinLockRaiseToDpc(p_kspin_lock1C0_1);
_DI = _DI_1;
__asm { rcl dil, 0BEh }
NewIrql_3 = NewIrql_5;
LOBYTE(_R8D) = _DI;
__asm { rcr r8d, 22h }
n255 = *(unsigned int *)&::MemoryPool->gap8[172];
if ( (unsigned int)n255 > 0xFF )
n255 = 255;
sub_140003600(buf, (__m128 *)::MemoryPool->buf, (unsigned int)n255);
_mm_lfence();
p_kspin_lock1C0_2 = &::MemoryPool->kspin_lock1C0;
buf[0].m128_i8[n255] = 0;
KeReleaseSpinLock(p_kspin_lock1C0_2, NewIrql_3);
v37 = 0;
sub_1403F7C6D(buf, (unsigned int)n255, (__int64)&SystemBuffer[2]._12004 + 16, (__int64)&v37);
SystemBuffer[5]._1200c.enX = v37;
memset(buf, 0, sizeof(buf));
}
sub_1400026B4(SystemBuffer);
}
else
{
customMemset(SystemBuffer, 0, 0x84u);
sub_140002038(SystemBuffer);
}
info = 132;
goto LABEL_23;
}
break;
case 0x80012008:
sub_140001A7C((__int64)::MemoryPool); // reset
sub_14031A53E();
info = 0;
goto LABEL_23;
case 0x8001200C:
if ( SystemBuffer && outLen >= 0x18 ) // init
{
NewIrql_1 = KeAcquireSpinLockRaiseToDpc(&::MemoryPool->kspin_lock1C0);
SystemBuffer->_1200c.enY = 0;
SystemBuffer->_1200c.x = 13;
*(_QWORD *)&SystemBuffer->_1200c.y = 13;
SpinLock = &::MemoryPool->kspin_lock1C0;
SystemBuffer->_1200c.exitX = 12;
SystemBuffer->_1200c.exitY = 12;
KeReleaseSpinLock(SpinLock, NewIrql_1);
info = 0x18;
goto LABEL_23;
}
break;
default:
Status_1 = 0xC0000010;
LABEL_23:
Status = Status_1; // 可能混淆
return IO_CompleteReq(IRP, Status, info);
}
Status_1 = 0xC0000023;
goto LABEL_23;
}
Status = 0xC00000A3;
// 可能混淆
return IO_CompleteReq(IRP, Status, info);
}
/* Orphan comments:
可能混淆
*/

CODE 0x8001200C

传递迷宫大小,位置信息

CODE 0x80012008

重置

CODE 0x80012004

移动,主要逻辑

移动延迟

KeDelayExecutionThread 对每步移动都进行一点延迟,所以算法爆破特别慢,需要 patch

image.png

VA 0x1400026BD,RVA 0x26BD 全 nop (5字节)

1
eq ShadowGateSys+0x26bd c483489090909090

五个泄漏点

sys data 段起始有一个花指令魔数,通过该魔数可以定位四个泄漏点

image.png

1. Event 泄漏

exe 0x140001340 创建 Global Event

image.png

sys 0x1400022B0 中 event 泄漏:n2==0/2 置 MazeMoveOK,否则置 MazeMoveWall

image.png

2. 信号量 泄漏

exe 0x14021B91F 创建信号量,异或 0x4B 解密字符串

image.png

image.png

得到

1
2
Global\{A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D}
Global\{B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}

sys 0x140319A37 中 semaphore 泄漏,_EDX ==0/2 释放 {A7F3B2C1-9E4D-4C8A-B5D6-1F2E3A4B5C6D} ,否则释放 {B8E2C3D0-0F5A-5D9B-C6E7-2A3F4B5C6D7E}

image.png

3. TEB.LastError 泄漏

sys 0x140316ADF 中修改 exe LastErrorValue ,n2 为 0 时设置 0xC0DE0001, 2 时设置 0xC0DE0002, 其他时设置 0xC0DE0000

image.png

4. TEB + 0x1748 泄漏

exe 0x14021BC88 在释放时有部分提示

image.png

sys 0x14031857E 中,使用 PsGetThreadTeb 获取 TEB 后使用 ZwSetInfomationObjectteb + 0x1748 处进行设置

在 icall 分发处能够看到实际调用的 call 为 PsGetThreadTebZwSetInfomationObject

image.png

两个 icall 都清理为 call rax 不然反编译有问题

image.png

n2 = 0/2,设置 1,否则设置 0

image.png

5. 内存属性 泄漏

_guard_dispatch_icall_nop 下断点,当输入断在 rax=ZwProtectVirtualMemory 时就是触发第五个漏洞

调用参数为

rdx = ShadowGateApp.exe.data

rcx = ShadowGateApp.exe.data 页长度

.data 页的属性修改为 RWERW

1
2
3
4
3: kd> dq rdx L1
ffffab06`6e6a75a0 00007ff6`0eb55000
3: kd> dq r8 L1
ffffab06`6e6a75a8 00000000`00001000

image.png

找了下没找到,看堆栈这个是真的在 vm 里面了,该泄漏通过观察页得到,猜测 sys 会修改名为 .data 的页

image.png

image.png

根据上面规则猜测 n2 = 0/2 时,页属性变成 PAGE_EXECUTE_READWRITE 其他时,保持 PAGE_READWRITE

查询迷宫 & 探索迷宫

方法一:r3 下通过泄漏探索迷宫

伪代码,完整见附件

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
Init():
打开驱动设备
创建 2 个事件对象
创建 2 个命名信号量
创建 1 个 probe handle
找到当前 exe 的 .data 首页
把 probe handle 写入 TEB+0x1748
发送 QUERY,得到迷宫大小、起点、终点

EncodeMove(dir):
将 U/D/L/R 编码成驱动需要的 dir 值

DoMove(dir):
packet.dir = EncodeMove(dir)
packet.counter = global_counter
packet.check = dir ^ counter ^ 0xDEAD1337
global_counter++
发送 IOCTL_MOVE
返回 reply

PrepareDetectors():
ResetEvent(ok)
ResetEvent(wall)
DrainSemaphore(A)
DrainSemaphore(B)
SetLastError(哨兵值)
将 .data 首页改回 RW
将 probe handle flags 改回基线
重新确保 TEB+0x1748 = probe handle

ReadVerdict(reply):
同时读取:
Event 状态
Semaphore 状态
LastError
HandleFlags
.data 页保护
if Event 有效:
返回 open / wall / win
else if Semaphore 有效:
返回 open / wall / win
else if LastError 有效:
返回 open / wall / win
else if HandleFlags 相对基线发生变化:
返回 open / wall / win
else:
根据 .data 页保护返回 open / wall / win

ReplayPath(path):
RESET 驱动
for step in path:
PrepareDetectors()
verdict = ReadVerdict(DoMove(step))
若 replay 过程中出现 wall,则说明路径失效,直接报错

ProbeOneStep(path_to_cur, dir):
ReplayPath(path_to_cur)
PrepareDetectors()
reply = DoMove(dir)
return ReadVerdict(reply)

DFS(x, y):
标记 (x, y) 已访问
for dir in [R, D, L, U]:
nx, ny = 邻格
若越界或该格已判定,continue

verdict = ProbeOneStep(path[x][y], dir)

if verdict == wall:
maze[ny][nx] = '#'
else:
maze[ny][nx] = '.'
path[nx][ny] = path[x][y] + dir
if 未访问:
DFS(nx, ny)

BuildShortestPath():
对已恢复出的 '.' 地图做 BFS
从起点求到终点的最短路径

得到以下迷宫图

1
2
3
4
5
6
7
8
9
10
11
12
13
.......#.....
######.###.#.
.....#.....#.
.###.#######.
.#.........#.
.#.#.#####.#.
.#.#.#...#.#.
.#.###.#.###.
.#.....#.#...
.#######.#.##
...#...#.#.#.
##.#.#.#.#.#.
.....#.#.....

方法二:可以通过 sys 直接读取分配内存读取到迷宫

image.png

image.png

最短路径求解

bfs

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
from __future__ import annotations

from collections import deque
import re
from pathlib import Path

INPUT_PATH = Path("Input.txt")
OPEN_TILES = {".", "*"}

def parse_point(text: str) -> tuple[int, int] | None:
match = re.search(r"\((\d+),\s*(\d+)\)", text)
if not match:
return None
return int(match.group(1)), int(match.group(2))

def load_maze(path: Path) -> tuple[list[str], tuple[int, int], tuple[int, int]]:
lines = [line.strip() for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
start: tuple[int, int] | None = None
goal: tuple[int, int] | None = None
maze: list[str] = []

for line in lines:
if line.startswith("start="):
left, _, right = line.partition("goal=")
start = parse_point(left)
goal = parse_point(right)
continue
if set(line) <= OPEN_TILES | {"#"}:
maze.append(line)

if not maze:
raise ValueError("Input.txt 中没有找到迷宫数据")

width = len(maze[0])
if any(len(row) != width for row in maze):
raise ValueError("迷宫每一行长度必须一致")

if start is None:
start = (0, 0)
if goal is None:
goal = (width - 1, len(maze) - 1)

return maze, start, goal

def is_open(maze: list[str], x: int, y: int) -> bool:
return maze[y][x] in OPEN_TILES

def solve(maze: list[str], start: tuple[int, int], goal: tuple[int, int]) -> str:
width = len(maze[0])
height = len(maze)
sx, sy = start
gx, gy = goal

if not (0 <= sx < width and 0 <= sy < height and is_open(maze, sx, sy)):
raise ValueError("起点不可达或越界")
if not (0 <= gx < width and 0 <= gy < height and is_open(maze, gx, gy)):
raise ValueError("终点不可达或越界")

moves = [
(1, 0, "R"),
(0, 1, "D"),
(-1, 0, "L"),
(0, -1, "U"),
]

queue = deque([start])
prev: dict[tuple[int, int], tuple[tuple[int, int], str] | None] = {start: None}

while queue:
x, y = queue.popleft()
if (x, y) == goal:
break

for dx, dy, step in moves:
nx = x + dx
ny = y + dy
if not (0 <= nx < width and 0 <= ny < height):
continue
if not is_open(maze, nx, ny):
continue
if (nx, ny) in prev:
continue
prev[(nx, ny)] = ((x, y), step)
queue.append((nx, ny))

if goal not in prev:
raise ValueError("找不到从起点到终点的路径")

path: list[str] = []
cur = goal
while cur != start:
parent, step = prev[cur] # type: ignore[misc]
path.append(step)
cur = parent
path.reverse()
return "".join(path)

def main() -> None:
maze, start, goal = load_maze(INPUT_PATH)
path = solve(maze, start, goal)
print(f"start={start} goal={goal}")
print(f"length={len(path)}")
print(path)

if __name__ == "__main__":
main()

Flag

flag{SHAD0WNT_HYPERVMX}

1
2
3
4
5
6
7
8
9
=============================================
ACCESS GRANTED - Credential extracted!
=============================================
[CREDENTIAL] flag{SHAD0WNT_HYPERVMX}
[*] Your input (32 ops sent): RRRRRRDDRRRRUURRDDDDDDDDLLDDDDRR
[*] Note: blocked ops are included above; the driver only counts successful ones.

[*] Press any key to exit...
[*] Mission aborted. ACE HQ standing by.
阅读更多

内网 ss 服务端搭建

2026/04/08

在某某内网环境下,手机需要通过某内网设备来连接到互联网(内网设备已连接外网),且为了安全考虑,所以采用 ss 的加密,故有此篇记录

下载&启动 ss 服务端

选取 https://github.com/shadowsocks/shadowsocks-rust

笔者使用 shadowsocks-v1.24.0.x86_64-pc-windows-gnu.zip 进行测试

1
2
3
4
5
6
7
8
9
10
\shadowsocks-v1.24.0.x86_64-pc-windows-gnu 的目录

2026/04/08 05:56 <DIR> .
2026/04/08 05:56 <DIR> ..
2026/04/08 04:51 9,754,112 sslocal.exe
2025/12/11 08:22 7,732,224 ssmanager.exe
2025/12/11 08:21 7,635,456 ssserver.exe
2026/04/08 04:51 10,604,032 ssservice.exe
2025/12/11 08:20 2,496,000 ssurl.exe
2026/04/08 04:51 10,598,912 sswinservice.exe

生成密钥

1
ssservice.exe genkey -m "aes-256-gcm"

编写 config.json

1
2
3
4
5
6
7
8
9
{
"server": "0.0.0.0",
"server_port": 8388,
"password": "你生成的密钥或自定义密码",
"method": "aes-256-gcm",
"timeout": 300,
"fast_open": true,
"mode": "tcp_and_udp"
}

启动服务端(这里不做服务安装,仅作为 cli server)

1
ssserver.exe -c config.json

导出 ss 链接

1
ssurl.exe -e config.json

注意

由于服务端接收 0.0.0.0 也就是所有 ip 请求,所以生成的 ss 链接也是 0.0.0.0 所以需要改成目标设备 ip,例如 192.168.1.2@8388

注意最后要开启对应端口的防火墙,例如 8388

阅读更多

1+13T Root 记录

2026/03/23

1+13T 刷机记录

目标:SukiSU-Ultra


准备

https://yun.daxiaamu.com/OnePlus_Roms/一加OnePlus 13T/

新机搭载 ColorOS PKX110_15.0.2.602(CN01) ,上面没有

1
2
3
4
5
> getprop | grep ro.build.display
[ro.build.display.full_id]: [PKX110domestic_11_15.0.2.602(CN01)_2025090420480208]
[ro.build.display.id]: [PKX110_15.0.2.602(CN01)]
[ro.build.display.id.show]: [PKX110_15.0.2.602(CN01)]
[ro.build.display.ota]: [PKX110_11_A.53]

https://optool.daxiaamu.com/install_adb_drivers?src=pctool 下载并安装 fastboot 驱动

解锁BL

开发者 → OEM 解锁 开启

1
2
3
> adb devices
List of devices attached
<hide> device
1
adb reboot bootloader
1
2
> fastboot devices
<hide> fastboot
1
2
3
> fastboot flashing unlock
OKAY [ 0.016s]
Finished. Total time: 0.016s

按音量下键并确认,出现黄字即成功

Root

本来想要采用 DSU + GSI Sideload 的方式启动下载本机的 init_boot.img 的,但是没找到编译好的,要不就是版本不对,所以不采用 DSU + GSI Sideload 的方式

更新系统到 603

https://yun.daxiaamu.com/OnePlus_Roms/一加OnePlus 13T/ColorOS PKX110_15.0.2.603(CN01) A.54/

系统 → 软件更新 → 本地更新,使用全量包安装

提取 img

https://yun.daxiaamu.com/files/tool/Fastboot Enhance/

加载全量包的 payload.bin 使用工具提取 init_boot.imgboot.img

image.png

刷入 img

首先安装 SukiSu.apk,将刚才提取的 init_boot.img 进行修补,下载修补完成的 img

eg. kernelsu_patched_20241231_195134.img

进入 bootloader

1
adb reboot bootloader

注意这是持久化刷入

1
fastboot flash init_boot path\to\kernelsu_patched_20241231_195134.img
1
fastboot reboot

刷入 TWRP(可不刷)

https://yun.daxiaamu.com/files/TWRP/OnePlus 13T/kmiit/

1
adb reboot bootloader

fastboot 下

1
fastboot getvar all

slot-count: 如果显示 2,说明是 A/B 分区;如果显示 1 或找不到,则是传统的 A-only 分区

current-slot: 显示当前活跃的分区(如 a 或 b)

has-slot:recovery: 如果显示 yes,说明你有独立的 recovery 分区;如果显示 no,说明你的 Recovery 是集成在 boot 分区(或 Android 13+ 的 init_boot / vendor_boot)里的

1
fastboot flash recovery_? TWRP.img

在拥有全量包的情况下其实无需安装 TWRP,如果出现故障,fastboot boot 到 TWRP 临时用即可

备份

字库备份

吐槽:这是哪个大聪明发明的词,就是备份分区 :(

使用多系统工具箱 / TWRP 直接备份均可,super 可不用备份

保留 root 升级

见视频

不可禁用/删除 app

名称 解决方案
软件包安装程序 使用 CrossProfileTestApp.apk 禁用 or CtsPermissionApp(未测试) 应该卸载 oplus.appdetail

需要删除的

com.coloros.phonemanager

com.oplus.appdetail

基本模块

名称 功能
https://github.com/Dr-TSNG/ZygiskNext
https://t.me/lsposed_irena
https://github.com/re-zero001/LSPosed-Irena
https://github.com/KOWX712/PlayIntegrityFix https://github.com/osm0sis/PlayIntegrityFork 谷歌商店修复
https://github.com/5ec1cff/TrickyStore 隐藏 tee 损坏
https://github.com/KOWX712/Tricky-Addon-Update-Target-List 同上,配置
shamiko? 隐藏 root?

ref

https://optool.daxiaamu.com/

https://github.com/eritpchy/FingerprintPay/releases(指纹)

https://www.bilibili.com/video/av115547337459255(晨钟酱)

https://www.bilibili.com/video/av115042041206760

https://www.bilibili.com/video/av616455200/ or https://www.bilibili.com/video/av616455200/(CrossProfileTestApp)

阅读更多