解压专家 (apk)
时间不是很充裕,有一些细节没有完善,有兴趣的可以深入分析、比如后续 GOT 链接这些的...本人也是刚学。
分析一款可以共享密码的解压缩 app,其中用到了 native 的知识点,本文将介绍两种 native 解密方法。
java 层

publicstaticvoidcheckPasswordFromServer(String s, Context context0, FindPasswordCallback zipUtil$FindPasswordCallback0) {newThread(newRunnable() {@Overridepublicvoidrun() {varhashMap0=newHashMap();DocumentFileBeandocumentFileBean0= DocumentFileManager.getFileBean(context0, s);if(documentFileBean0 != null) {Strings= documentFileBean0.getFileName();longv= documentFileBean0.getFileSize(); hashMap0.put("fileName", s); hashMap0.put("fileSize", v);Strings1= ZMCoreUnZipWrapper.getQPWAT(context0, s + v);try { hashMap0.put("md5", FileUtil.getFileMd5(s)); }catch(Exception exception0) { exception0.printStackTrace(); }vargson0=newGson();Strings2= gson0.toJson(hashMap0);varokHttpClient0=newOkHttpClient();try {varsecureRandom0=newSecureRandom();byte[] arr_b = newbyte[16]; secureRandom0.nextBytes(arr_b);varivParameterSpec0=newIvParameterSpec(arr_b);varsecretKeySpec0=newSecretKeySpec("EyR2JvBXJXaUdY9auxetvhpEeQ8DmC6L".getBytes("UTF-8"), "AES");Strings3= gson0.toJson(newZipCheckInfo(Base64.encodeToString(ZipUtil.GeneralEncrypt(s2.getBytes("UTF-8"), secretKeySpec0, ivParameterSpec0), 2), Base64.encodeToString(arr_b, 2)));RequestBodyrequestBody0= RequestBody.create(ZipUtil.JSON, s3); okHttpClient0.newCall(newBuilder().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(newCallback() {@Override// okhttp3.CallbackpublicvoidonFailure(Call call0, IOException iOException0) { Log.d("onFailure", "onFailure---" + iOException0.getMessage()); com.fileunzip.zxwknight.utils.ZipUtil.8.this.val$callback.findPassword(""); }@Override// okhttp3.CallbackpublicvoidonResponse(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 {Strings= response0.body().string();Classclass0=newHashMap().getClass();varmap0= (Map)gson0.fromJson(s, class0);byte[] arr_b = Base64.decode(((String)map0.get("data")), 2);varivParameterSpec0=newIvParameterSpec(Base64.decode(((String)map0.get("iv")), 2));vars1=newString(ZipUtil.GeneralDecrypt(arr_b, secretKeySpec0, ivParameterSpec0), "UTF-8");newHashMap();Classclass1= map0.getClass();vars2= (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
publicstaticStringgetVersionName(Context context0) {try {return context0.getApplicationContext().getPackageManager().getPackageInfo("com.fileunzip.zxwknight", 0).versionName; }catch(PackageManager.NameNotFoundException packageManager$NameNotFoundException0) { packageManager$NameNotFoundException0.printStackTrace();return""; }}
User-Client-Access
Strings= documentFileBean0.getFileName();longv= documentFileBean0.getFileSize();hashMap0.put("fileName", s);hashMap0.put("fileSize", v);Strings1= ZMCoreUnZipWrapper.getQPWAT(context0, s + v);其中 getQPWAT 调用 libZMCoreUnZip.so 的 executeGBS native 方法
publicstaticStringgetQPWAT(Context context0, String s) {returnZMCoreUnZipZipApi.executeGBS(s, context0);}publicclassZMCoreUnZipZipApi {static {System.loadLibrary("ZMCoreUnZip"); }publicstatic native int executeConfig(String arg0, ZMCoreUnZipZipExtractCallback arg1, String arg2, Context arg3, int arg4) { }publicstatic native StringexecuteGBS(String arg0, Context arg1) { }publicstatic native StringgetVersionInfo() { }}native 层
静态分析+vibe
发现能识别 elf,但是 export 符号很奇怪,不是可见字符,且指示条不正常。

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

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


随后发现 init_array 均为空值 0,说明需要重定向。
查看 DT_RELA 元素为 Elf64_Rela
structElf64_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)。

所以 init_array 为
0x549504// init10x5495B4// init20x549D08// init30可以通过编写以下脚本来应用重定向
# from `ELF RELA Relocation Table`base =0table= [ (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_idaforold, newintable: 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 分析,能够找到关键全局变量并创建结构体。
#pragma pack(push, 2)structloader_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;unsignedint phnum;unsignedint unknown_6C;unsignedint rela_count;unsignedint jmprel_count;unsignedint android_reloc_size;unsignedint payload_seed0;unsignedint flags;unsignedint packed_size;unsignedint aux_size;unsignedint init_stage;unsignedint dynsym_seed0;unsignedint sym_index_begin;unsignedint sym_index_end;unsignedint dynsym_seed3;unsignedint resource_flags;unsignedint loader_word_A4;int gate_beg;int gate_end;unsignedint split_begin;unsignedint split_end;};#pragma pack(pop).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 负责环境探测

init2 负责恢复 dynsym、dynstr
init2-> 解析 DT_SYMTAB / DT_STRTAB / DT_HASH-> init2_run_symbol_decode_context-> gp_decode_D1 = dynstr_dynsym_decode_Impl->DispatchToSymbolDecode(...)-> dynstr_dynsym_decode_Impl注意到:

dynstr_dynsym_decode_Impl 的解密过程
dynstr_dynsym_decode_Impl @ 0x54D6B8 做两件事:先解 .dynstr,再修 .dynsym
// 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,unsignedint i_1,unsignedint i_2, _DWORD *pDynStr,unsignedint dynstrLen,int a6){ __int64 i; // x4int key; // w6int v9; // w5 Elf64_Sym *sym; // x0 __int64 v11; // x4 __int64 v12; // x8 Elf64_Sym *endSym; // x2unsigned __int64 st_value; // x1unsigned __int64 st_value_1; // x3unsigned __int64 st_size; // x1if ( a6 == 0 ) {if ( dynstrLen != 0 ) { i = 0; key = 0x56312342;do { v9 = pDynStr[i] ^ key; key += i * 4; pDynStr[i++] = v9; }while ( dynstrLen > (unsignedint)(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 ); } }return1;}给出修复脚本
deffix_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)}")assertlen(b) % 4 == 0, "length must be multiple of 4" new_b = bytearray()for i inrange(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)deffix_dynsym(): start = int('000000000055FD38', 16) end = int('00000000005A68F8', 16) # dynsym 结束与 HASH 表之前 b = utils_ida.GetBytesFromEA(start, end - start)assertlen(b) % 0x18 == 0, "length must be multiple of 0x18" new_b = bytearray() the_start = 0for i inrange(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 symbolif st_shndx != 0:if the_start == 0: the_start = iassert 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 负责还原代码
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_decodeload2_load_packed_segments
函数概括为六个阶段:
从 ELF header 后定位主 packed payload,并定位 payload 尾部的 trailer 根据 flags获取可选字符串或构造外部 key第一次调用 core_decode,恢复 metadata 后方的 ELF 链接信息块以链接信息块为基址回填 Program Header、 .dynsym、.dynamic和 relocation 地址第二次调用 core_decode,把主 packed payload 还原为带 8 字节长度头的 zlib 包分配独立输出缓冲,调用 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 --> IRuntimeMalloc、RuntimeFree、RuntimeMemcpy 等名称对应 API 表里的 libc 函数;AcquireFlag4String/AcquireFlag8String/AcquireFlag10String 只是按控制它们的 flag 命名,具体业务语义仍未确认。
// load2_load_packed_segments @ 0x54C944uint32_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;constchar *descriptor = descriptor_rel != 0 ? (constchar *)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。constchar *key_source_bit2 = NULL; // X21constchar *key_source_bit3 = NULL; // X26constchar *key_source_bit4 = NULL; // X28if ((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); } elseif (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);return0; } 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 ); } elseif ((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);return0; }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 ); } elseif ((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 = (constchar *)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);return0; } 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);return1;}该样本将会执行两次 core_decode 而不执行 xor_decode_fixed_key_blocks

core_decode 伪代码
// core_decode @ 0x54B7ECuint32_tcore_decode(uint8_t *src_ptr, // X0int src_copy_len, // W1uint8_t *decode_buf, // X2uint32_t decode_len, // W3int gate_beg, // W4int gate_end, // W5uint8_t *ext_key // X6){uint8_t head_mask[0x40];uint8_t saved_0x80[0x80];if (src_ptr == NULL) return0; // 0x54B808if (src_copy_len <= 0x3F) return0; // 0x54B80C..0x54B814memcpy(decode_buf, src_ptr, src_copy_len); // 0x54B840..0x54B844if (decode_len <= 0x3F) return0; // 0x54B848..0x54B850uint32_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); // 0x54B8A8memset(head_mask, 0xD7, 0x40); // 0x54B8AC..0x54B8C0BuildHeadMask(ext_key, head_mask); // 0x54B8C4..0x54B8CCuint32_t body_len = decode_len - 0x40;if (body_len <= 0x7F) { // 0x54B8D4..0x54B8D8Rc4VariantXor(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..0x54B978Rc4VariantXor(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); // 0x54BA40uint32_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) { // 0x54BA8Cint quarter = block_count >> 2; // 0x54BA90int 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..0x54BB24switch (mode) { // 0x54BB28..0x54BB3Cdefault:// mode 0/default: 0x54BB40 / 0x54BB58..0x54BB80if (block_index >= range0_beg && block_index <= range0_end) {// 0x54BDC4..0x54BE24for (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;case1:// mode 1: loc_54BC70 / 0x54BC84..0x54BCCCif (block_index >= range1_beg && block_index <= range1_end) {// 0x54BE28..0x54BE88for (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;case2:// mode 2: loc_54BD10 / 0x54BD10..0x54BD5Cfor (int i = 0; i < 32; ++i) {uint32_t rolling = key4[sched[i % 9] & 3]; block32[i] ^= saved32[i] ^ i ^ rolling; }break;case3:// mode 3: loc_54BCD0 / 0x54BCDC..0x54BD0Cif (block_index >= range3_beg) {// 0x54BD60..0x54BDC0for (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..0x54BBBCif (tail == 0) {return decode_len; }uint32_t tail_pos = decode_len - tail;for (uint32_t i = 0; i < tail; ++i) { // 0x54BBD4..0x54BC64uint32_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() 基本一致:
intzlib_uncompress2_wrapper(void *dst,uint64_t *dst_len,constvoid *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 |
完整运行时映射为:
main_payload[0x000000:0x5062E0] -> VA 0x000000main_payload[0x5062E0:0x52FFD0] -> VA 0x50A2E0main_payload[0x52FFD0:0x530268] -> VA 0x537FD0第二段的 p_memsz - p_filesz = 0x30,第三段为 0xB848;这两块段尾内存必须清零。
当前 IDB 的 VA 0..0x18F 已经保存原始 ELF header 和 Program Header Table,为了避免静态 patch 时覆盖这些结构,可以仅对首段执行:
main_payload[0x190:0x5062E0] -> VA 0x190解密脚本
"""Decode and patch the packed ELF for the current flags=3 sample."""import structimport zlibimport ida_bytesimport ida_kernwinMAIN_VA = 0x190MAIN_SIZE = 0x5062E0META_VA = 0x63C020META_SIZE = 0x6CC18PHNUM = 6PHENTSIZE = 0x38PT_LOAD = 1def u32(data: bytes | bytearray, offset: int) -> int:returnstruct.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 = 0x53B20C96forindexinrange(256): value = indexfor_inrange(8): value = ((value >> 1) ^ polynomial) if value & 1else value >> 1 value &= 0xFFFFFFFF table.append(value) current = 0xFFFFFFFFforvaluein data: current = table[(value ^ (current & 0xFF)) & 0xFF] ^ (current >> 8) current = (current + 16) & 0xFFFFFFFFreturn (-current - 516327184) & 0xFFFFFFFFdef rc4_variant_xor(data: bytearray, key_u32: int) ->None: state = list(range(256)) key = struct.pack("<I", key_u32 & 0xFFFFFFFF) state_index = 0forindexinrange(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 = 0forpositioninrange(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] ^= maskdef 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 - 0x40if body_length <= 0x7F: body = bytearray(output[0x40:decode_len])rc4_variant_xor(body, seed_main) output[0x40:decode_len] = bodyforindexinrange(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]forindexinrange(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) forindexinrange(32)]forblock_indexinrange(block_count): block_offset = 0xC0 + block_index * 0x80 words = [u32(output, block_offset + index * 4) forindexinrange(32)] mode = schedule[block_index % 9] & 3if mode == 0:if range1_end <= block_index <= range3_begin: words = [word ^ 0xB2forwordin words]else: words = [word ^ saved_words[i] ^ key4[saved_words[i] & 3] fori, word inenumerate(words)] elif mode == 1:if range1_begin <= block_index <= range1_end: words = [word ^ 0xA1forwordin words]else: words = [word ^ saved_words[i] ^ (32 - i) ^ schedule[i % 9] fori, word inenumerate(words)] elif mode == 2: words = [word ^ saved_words[i] ^ i ^ key4[schedule[i % 9] & 3] fori, word inenumerate(words)]else:if block_index >= range3_begin: words = [word ^ 0xC3forwordin words]else: words = [word ^ saved_words[i] ^ i ^ key4[saved_words[i] & 3] fori, word inenumerate(words)]forindex, word inenumerate(words):put_u32(output, block_offset + index * 4, word) tail_length = (decode_len + 0x40) & 0x7Fif tail_length: tail_offset = decode_len - tail_lengthforindexinrange(tail_length): position = tail_offset + index schedule_value = schedule[key4[index & 3] % 9] output[position] ^= ((schedule_value % 0xFF) ^ saved_block[index] ^ index) & 0xFFreturn outputdef parse_load_segments(decoded_meta: bytes):forindexinrange(PHNUM): phdr = struct.unpack_from("<IIQQQQQQ", decoded_meta, index * PHENTSIZE) p_type, _, p_offset, p_vaddr, _, p_filesz, p_memsz, _ = phdrif p_type == PT_LOAD:yield p_offset, p_vaddr, p_filesz, p_memszdef patch_load_segments(final_image: bytes, decoded_meta: bytes) ->None:forp_offset, p_vaddr, p_filesz, p_memsz inparse_load_segments(decoded_meta): skip = 0x190if p_offset == 0else0 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 脚本
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;publicclassZMCoreInitDumpTestextendsTestCase {private AndroidEmulator emulator;publicvoidtestInitArrayAndRebuildSo()throws Exception {Pathworkspace= Paths.get(System.getProperty("zmcore.workspace")).toAbsolutePath().normalize();Filelibrary= workspace.resolve("libZMCoreUnZip.so").toFile();Pathoutput= workspace.resolve("unidbg-dump/libZMCoreUnZip.init-rebuilt.so"); Files.createDirectories(output.getParent()); emulator = AndroidEmulatorBuilder.for64Bit() .setProcessName("com.fileunzip.zxwknight") .addBackendFactory(newUnicorn2Factory(true)) .build();Memorymemory= emulator.getMemory(); memory.setLibraryResolver(newAndroidResolver(23));VMvm= emulator.createDalvikVM();// forceCallInit=true: execute DT_INIT/DT_INIT_ARRAY before rebuilding the file.DalvikModuledalvikModule= vm.loadLibrary(library, true);Modulemodule= dalvikModule.getModule(); rebuildLoadSegments(library.toPath(), output, module); }// 以原始 ELF 为文件结构模板,用 init_array 执行后的运行时内存重建各个 PT_LOAD 段privatevoidrebuildLoadSegments(Path input, Path output, Module module)throws IOException {byte[] original = Files.readAllBytes(input);byte[] rebuilt = original.clone();ByteBufferelf= ByteBuffer.wrap(original).order(ByteOrder.LITTLE_ENDIAN);longphoff= elf.getLong(0x20); // Elf64_Ehdr.e_phoffintphentsize= Short.toUnsignedInt(elf.getShort(0x36)); // Elf64_Ehdr.e_phentsizeintphnum= Short.toUnsignedInt(elf.getShort(0x38)); // Elf64_Ehdr.e_phnum// 将解密后的内存段写回原始 ELF 文件的对应位置for (intindex=0; index < phnum; index++) {intphdr= Math.toIntExact(phoff + (long) index * phentsize);if (elf.getInt(phdr) != 1) { // PT_LOADcontinue; }longfileOffset= elf.getLong(phdr + 0x08); // Elf64_Phdr.p_offsetlongvirtualAddress= elf.getLong(phdr + 0x10); // Elf64_Phdr.p_vaddrlongfileSize= elf.getLong(phdr + 0x20); // Elf64_Phdr.p_fileszbyte[] 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); }@OverrideprotectedvoidtearDown()throws IOException {if (emulator != null) { emulator.close(); } }}将原来的前 0x190 字节复制到 dump 的即可分析
其他
其实还应该解密 elf 最后的 .data 段用于实现 GOT、LPT 定位的,但是比较麻烦就先不做了(分配到动态内存中了),而上述静态分析方法可以直接解密(或者这里结合一下静态的脚本解密一下也可以),下面给出一个工具脚本,用于 patch GOT,当然不使用也可以。
import jsonimport structfrom pathlib import Pathimport ida_bytesimport ida_nameimport ida_naltimport ida_segmentDYNSYM_START = 0x63C170DYNSTR_START = 0x5BA5E8RELA_START = 0x63CE90RELA_END = 0x6A8B78ELF64_SYM_SIZE = 0x18ELF64_RELA_SIZE = 0x18R_AARCH64_ABS64 = 1025R_AARCH64_JUMP_SLOT = 1026R_AARCH64_RELATIVE = 1027VALID_RELOCATION_TYPES = {R_AARCH64_ABS64, R_AARCH64_JUMP_SLOT, R_AARCH64_RELATIVE}MAX_SYMBOL_COUNT = 0x1000MAX_RELOCATION_OFFSET = 0x700000RELOCATION_NAMES = { R_AARCH64_ABS64: "R_AARCH64_ABS64", R_AARCH64_JUMP_SLOT: "R_AARCH64_JUMP_SLOT", R_AARCH64_RELATIVE: "R_AARCH64_RELATIVE",}defread_bytes(address, size): data = ida_bytes.get_bytes(address, size)if data isNoneorlen(data) != size:raise RuntimeError(f"无法读取 IDA 地址 0x{address:X} 的 0x{size:X} 字节")return datadefread_c_string(address): data = ida_bytes.get_strlit_contents(address, -1, 0)if data isNone:return""return data.decode("utf-8", errors="replace")defparse_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, }defvalidate_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] >> 32if ( first_type notin VALID_RELOCATION_TYPESor first_symbol >= MAX_SYMBOL_COUNTor first_relocation[0] >= MAX_RELOCATION_OFFSET ):raise RuntimeError("0x63CE90 ?????? Elf64_Rela???? IDA ????????""????????? unidbg dump ?????" )defparse_relocations(): validate_decrypted_tables() count = (RELA_END - RELA_START) // ELF64_RELA_SIZE raw_relocations = [] maximum_symbol_index = 0for index inrange(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 & 0xFFFFFFFFif relocation_type notin 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 inrange(maximum_symbol_index + 1)]for relocation in raw_relocations: symbol_index = relocation["symbol_index"] symbol = symbols[symbol_index] if symbol_index < len(symbols) elseNone relocation["symbol_name"] = symbol["name"] if symbol else"" relocation["symbol_value"] = symbol["value"] if symbol else0if 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 isnotNoneelse"" )return symbols, raw_relocationsdefsanitize_name(name): result = []for character in name:if character.isalnum() or character == "_": result.append(character)else: result.append("_")return"".join(result).strip("_")defapply_relocations(relocations): patched = 0 unchanged = 0 named_jump_slots = 0 skipped = 0for 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 isNoneor ida_segment.getseg(target_address) isNone: skipped += 1continue current_value = struct.unpack("<Q", read_bytes(target_address, 8))[0] patch_value = resolved_value & 0xFFFFFFFFFFFFFFFFif current_value == patch_value: unchanged += 1elif ida_bytes.patch_qword(target_address, patch_value): patched += 1else: skipped += 1continueif relocation_type == R_AARCH64_JUMP_SLOT: symbol_name = relocation["symbol_name"]ifnot symbol_name or ida_segment.getseg(target_address) isNone: skipped += 1continue got_name = "got_" + sanitize_name(symbol_name)if ida_name.set_name(target_address, got_name, ida_name.SN_FORCE): named_jump_slots += 1else: skipped += 1continue skipped += 1return {"patched": patched,"unchanged": unchanged,"named_jump_slots": named_jump_slots,"skipped": skipped, }defformat_hex(value):if value isNone:return"-"if value < 0:returnf"-0x{-value:X}"returnf"0x{value:X}"defwrite_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_pathdefmain(): 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) + 1print(f"解析符号数量: {len(symbols)}")print(f"解析重定位数量: {len(relocations)}")for type_name, count insorted(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)breakprint(f"JSON: {json_path}")print(f"文本: {text_path}")if __name__ == "__main__": main()分析 executeGBS
__int64 __fastcall Java_com_jindk_zmcoreunzipwrapper_ZMCoreUnZipZipApi_executeGBS( _JNIEnv *a1, __int64 a2, void *a3, void *a4){struct_jmethodID *StaticMethodID_1; // x0constchar *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]constchar *v27; // [xsp+E0h] [xbp-180h]constchar *v28; // [xsp+E8h] [xbp-178h]constchar *v29; // [xsp+F0h] [xbp-170h]constchar *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]constchar *v45; // [xsp+218h] [xbp-48h]constchar *v46; // [xsp+238h] [xbp-28h]constchar *v47; // [xsp+258h] [xbp-8h] Class = _JNIEnv::FindClass(a1, "android/content/Context");if ( Class == nullptr )return0; 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 )return0; v36 = _JNIEnv::CallObjectMethod(&a1->functions, a4, v37);if ( v36 == nullptr )return0; 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 )return0; 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 )return0; 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 = (constchar *)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 )return0; 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 )return0; 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 )return0; 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 )return0; _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);}伪代码
intZMCoreUnzip_executeCVS(Context context) {StringpackageName= context.getPackageName();if (!packageName.equals("com.fileunzip.zxwknight")) {return -300; }PackageManagerpm= context.getPackageManager();intflags= (SDK_INT >= 28) ? 0x08000000 : 0x40;PackageInfopi= 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) {StringpackageName= context.getPackageName(); // 当前样本 = "com.fileunzip.zxwknight"intctxValue= ZMCoreUnzip_executeCVS(context); // 当前样本静态复现 = 516622220inttail=2 * ((ctxValue >> 2) - 169) + 127; // 见 0x4B9008-0x4B9030Stringdata= 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");Macmac= Mac.getInstance("HmacSHA1"); // 算法固定为 HMAC-SHA1 mac.init(newSecretKeySpec(key, "HmacSHA1"));byte[] digest = mac.doFinal(body);return ZMCoreUnZip_BXF_encode(digest); // Base64 编码别名,编码表为 A-Z a-z 0-9 + / =}POC
#!/usr/bin/env python3"""Minimal fixed-value PoC for:1. submit zip password -> /api/v3/sync/ads2. query zip password -> /api/v3/search/infoUsage: python min_zip_password_poc.py submit "a.zip" "123456" python min_zip_password_poc.py query "a.zip""""import argparseimport base64import hashlibimport hmacimport jsonimport osfrom pathlib import Pathimport requestsfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import pad, unpadREGION = "CN"PACKAGE_NAME = "com.fileunzip.zxwknight"VERSION_NAME = "3.2.23"APP_ID = "zipa"PLATFORM = "android"CTX_VALUE = 516622220QUERY_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 Nonereturn 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 argsdef 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()
看雪ID:mb_jepgtozh
https://bbs.kanxue.com/user-home-1022681.htm

# 往期推荐
从时间片轮转到调用级并发:unidbg 单后端多线程架构重构
基于 GLM-5.2 的 X-Gnarly (332位) 纯算还原分析


球分享

球点赞

球在看

点击阅读原文查看更多
夜雨聆风