爱意满满的作品展示区。
neocanable

360,爱加密,网易易盾,娜迦,腾讯乐固 AI 破解

  •  
  •   neocanable ·
    neocanable · 1h 7m ago · 213 views

    一、测试基本情况

    garlic的 arm64 的 native 分析我写完了一段时间了,还在看雪发了三篇文章:

    [原创] Arm 静态分析引擎原理一 - 反汇编引擎码表设计

    [原创] Arm 静态分析引擎原理二 - 反汇编引擎实现

    [原创] Arm 静态分析引擎原理三 - 控制流构建基础

    我是一个逆向分析的小白,为了测试 garlic 对 android native 的分析能力,我设计了下面的测试: 测试要求:

    • 在市场上找到一些加固厂商的 apk
    • 包括:某 60 、某加密、某盾,某迦、某乐固
    • 加固产品的版本未知
    • 使用 AI loop
    • 静态分析只用 garlic ,动态模拟用 unicorn ,动态分析用 frida
    • 可以用模拟器,不能用真机
    • 测试系统:macmin m4
    • ai 模型:deepseek-v4-pro
    • 客户端:cc switch
    • garlic version: 1.8 (未发布)
    • 禁止用 objdump/capstone 影响控制流

    测试验收:

    • 如果有 dex 隐藏的,拿到 dex -> 结束
    • 没有 dex 隐藏的,拿到请求签名 -> 结束
    • 有 vmp 的,分析出 vm 的指令集 -> 结束

    对比测试:

    • 抛弃 garlic ,用 jadx/r2/unicorn/ghidra 等工具链
    • 对比 token 使用情况
    • 对比分析效率
    • 对比各个 AI

    有朋友发邮件问我怎么用的 AI ,我传了个附件,我明确我是逆向小白,所有抽取 dex/解密的工作完全由 AI 来完成,我大部分时间就说:好的/继续/选择 A/A 和 B 同时/先 A 后 B 这些。

    所以我大部分时间是坐在电脑旁边刷视频,它弹出一个来我就继续,离开就用 loop 模式;

    附件是 AI 逆向凤凰网 APK 的过程,加固应该是腾讯; AI 的分析成果应该也在里面,请大佬过目。


    二、软件基础原理

    garlic 的 mcp-server 在接受到分析 xxx.apk的时候会做如下操作:

    • 反编译所有的 dex
    • 生成 apk 整体的 call graph
    • 生成 apk 的 string 引用
    • 导入 call graph 到数据库,供 AI 查询
    • garlic 内置的 rosemary 引擎会生成如下内容供 ai 使用
      1. 分析 so 的 cfg
      2. 反汇编
      3. exports/imports
      4. 已知函数 call graph
      5. pc xref 导出
      6. strings 导出

    样例:

    .
    ├── analysis.duckdb
    ├── cg
    │   ├── call_graph_edge.csv
    │   ├── call_graph_node.csv
    │   ├── string_edge.csv
    │   └── string_node.csv
    ├── decompiled
    │   ├── AndroidManifest.xml
    │   └── com
    │       ├── example
    │       │   └── helloworld
    │       │       ├── BuildConfig.java
    │       │       └── R.java
    │       ├── ifeng
    │       │   └── newvideo
    │       │       └── StubWrapperProxyApplication.java
    │       └── wrapper
    │           └── proxyapplication
    │               ├── AndroidNClassLoader.java
    │               ├── CustomerClassLoader.java
    │               ├── MultiDex.java
    │               ├── MultiDexForMemoryDex.java
    │               ├── MultiDexForTinker.java
    │               ├── MultiDexForTinkerForMemoryDex.java
    │               ├── ShareReflectUtil.java
    │               ├── Util.java
    │               └── WrapperProxyApplication.java
    ├── native_libs
    │   └── arm64-v8a
    │       ├── libapp.so
    │       ├── libapp.so.cfg_edges
    │       ├── libapp.so.cfg_nodes
    │       ├── libapp.so.dissembly
    │       ├── libapp.so.entries
    │       ├── libapp.so.exports
    │       ├── libapp.so.func_xref
    │       ├── libapp.so.imports
    │       ├── libapp.so.pc_xrefs
    │       ├── libapp.so.strings
    │       ├── libBugly_Native.so
    │       ├── libBugly_Native.so.cfg_edges
    │       ├── libBugly_Native.so.cfg_nodes
    │       ├── libBugly_Native.so.dissembly
    │       ├── libBugly_Native.so.entries
    │       ├── libBugly_Native.so.exports
    │       ├── libBugly_Native.so.func_xref
    │       ├── libBugly_Native.so.imports
    │       ├── libBugly_Native.so.pc_xrefs
    │       ├── libBugly_Native.so.strings
    │       ├── libc++_shared.so
    │       ├── libc++_shared.so.cfg_edges
    │       ├── libc++_shared.so.cfg_nodes
    │       ├── libc++_shared.so.dissembly
    │       ├── libc++_shared.so.entries
    ......
    │       ├── libflutter.so.pc_xrefs
    │       ├── libflutter.so.strings
    │       ├── libgifimage.so
    │       ├── libgifimage.so.cfg_edges
    │       ├── libgifimage.so.cfg_nodes
    │       ├── libgifimage.so.dissembly
    │       ├── libgifimage.so.entries
    │       ├── libgifimage.so.exports
    │       ├── libgifimage.so.func_xref
    │       ├── libgifimage.so.imports
    │       ├── libgifimage.so.pc_xrefs
    │       ├── libgifimage.so.strings
    │       ├── libifeng_secure.so
    │       ├── libifeng_secure.so.cfg_edges
    │       ├── libifeng_secure.so.cfg_nodes
    │       ├── libifeng_secure.so.dissembly
    │       ├── libifeng_secure.so.entries
    │       ├── libifeng_secure.so.exports
    │       ├── libifeng_secure.so.func_xref
    │       ├── libifeng_secure.so.imports
    │       ├── libifeng_secure.so.pc_xrefs
    │       ├── libifeng_secure.so.strings
    │       ├── libijkffmpeg.so
    │       ├── libijkffmpeg.so.cfg_edges
    │       ├── libijkffmpeg.so.cfg_nodes
    │       ├── libijkffmpeg.so.dissembly
    │       ├── libijkffmpeg.so.entries
    │       ├── libijkffmpeg.so.exports
    │       ├── libijkffmpeg.so.func_xref
    │       ├── libijkffmpeg.so.imports
    │       ├── libijkffmpeg.so.pc_xrefs
    │       ├── libijkffmpeg.so.strings
    │       ├── libijkplayer.so
    │       ├── libijkplayer.so.cfg_edges
    │       ├── libijkplayer.so.cfg_nodes
    │       ├── libijkplayer.so.dissembly
    │       ├── libijkplayer.so.entries
    │       ├── libijkplayer.so.exports
    │       ├── libijkplayer.so.func_xref
    │       ├── libijkplayer.so.imports
    │       ├── libijkplayer.so.pc_xrefs
    │       ├── libijkplayer.so.strings
    │       ├── libijksdl.so
    │       ├── libijksdl.so.cfg_edges
    │       ├── libijksdl.so.cfg_nodes
    │       ├── libijksdl.so.dissembly
    │       ├── libijksdl.so.entries
    ......
    │       ├── libimagepipeline.so.entries
    │       ├── libimagepipeline.so.exports
    │       ├── libimagepipeline.so.func_xref
    │       ├── libimagepipeline.so.imports
    │       ├── libimagepipeline.so.pc_xrefs
    │       ├── libimagepipeline.so.strings
    │       ├── libjcore300.so
    │       ├── libjcore300.so.cfg_edges
    │       ├── libjcore300.so.cfg_nodes
    │       ├── libjcore300.so.dissembly
    │       ├── libjcore300.so.entries
    ......
    │       ├── libweibosdkcore.so.entries
    │       ├── libweibosdkcore.so.exports
    │       ├── libweibosdkcore.so.func_xref
    │       ├── libweibosdkcore.so.imports
    │       ├── libweibosdkcore.so.pc_xrefs
    ......
    │       ├── libwind.so.func_xref
    │       ├── libwind.so.imports
    │       ├── libwind.so.pc_xrefs
    │       └── libwind.so.strings
    └── report
        ├── report.html
        └── report.md
    
    13 directories, 311 files
    
    

    三、某 60 加固

    • apk: com.fhyx.gamesstore.apk
    • md5: 4342e69622e37fce5b904c2a476bd303
    • so 特征:libjiagu.so

    这个是两层加固,不知道是正常的包还是异常的包,不是在官网的 apk 下载下来的;

    加载流程:

    StubApp.attachBaseContext()
      ├─ DtcLoader.<clinit> → System.loadLibrary("jgdtc")   (DTC 引擎,运行时解密生成)
      ├─ interface5(app)                                     (native,壳初始化)
      ├─ 反射加载真实 Application "entryRunApplication"
      ├─ interface8(realApp, ctx)                            (native,解密 DEX)
      └─ System.loadLibrary("jiagu") → 释放 jiagu.so 到 /.jiagu/ 再加载
    

    AI 输出:

    
    - 入口链:`JNI_OnLoad → interpreter_wrap_int64_t(0x46af0) → sub_44eb4(0x44eb4)`
    - 15-opcode 类型化栈机,跳转表 @0x53968
    - JNI_OnLoad 真实逻辑:缓存 Context + ActivityThread
    
    
    - `classes.dex` 尾部(offset 18156 起,23MB)= 加密 payload(目录 `d6edc6c6` 结构 + 加密 blob)
    - `assets/goodgoodstudy.zip` = 诱饵;`assets/data.bin` = PNG 图集;`.jgapp` = 16 字节标记
    
    
    
    真正 DEX 解密在**第二阶段加密 .so**(`libdjiagu.so`/`libijmDataEncryption.so`/`libjgdtc.so`,运行时从密文解密生成)里,或由 `__arm_a_1`  程序触发(需修正其 funcptr/参数后仿真)。关键点:找到 `interface5`/`interface8` native 方法(RegisterNatives 注册表在加密代码中,libjiagu.so 明文里无 "interface5" 字符串)。
    

    结论:这个是一个某 60 套着某加密的 apk ,感觉是个盗版;解压出来的 dex 在 android 运行的时候是一个壳,壳起来后解密 dex 后面的密文,释放出真实的业务逻辑;


    四、某盾

    • apk: com.yiyou.ga
    • md5: 504ca64deaa7ea7c44ca58be3664f406
    • so 特征:libvlplg.so
    • MyJni.load(Application, String) → 返回 true

    加载流程:

    System.load(libvlplg.so)
      → Dynamic Linker:
        → INIT[0] (0xFD504): 代码/数据解密
        → INIT[1] (0xFD5B4): VMP 初始化
        → JNI_OnLoad (0xFD9D8): 注册 native methods
            
    MyApplication.attachBaseContext()
      → MyJni.load(Application, String) [native 0xFD700]:
        → 读取 37 个 chunk
        → 为每个 chunk 解密前 284 字节 → stub DEX
        → 37 x InMemoryDexClassLoader(stub)
        → 返回 true
        
    App 后续运行:
      → 需要类时 → 自定义 ClassLoader.findClass()
      → VMP 解密对应 chunk 中的类数据
      → 返回完整类定义
    

    AI 输出:

    DEX Decryption Handler (0xFDCF4)
      ├── LDR W2,[X3,#0x26C] W4,[X3,#0x27C] W5,[X3,#0x24C] W1,[X3,#0x25C]
      │   └── 从 chunk 子头解析后的结构体加载参数
      ├── LDR X0,[X20,#0x12]
      │   └── VM 状态偏移 0x12 (密钥流指针?)
      ├── BL sub_101ED8 (0x101ED8)  ← VM 解释器核心
      │   ├── PRNG 链 (mod_mul/mod_add/mod_sub × N)
      │   ├── TBZ W7,#0 → 验证 PRNG 输出
      │   ├── LDR W1,[X1,#0x2910] → 检查数据大小
      │   ├── BL sub_10719C (0x10719C) ← 数据处理
      │   │   ├── BL sub_FD440 (memcpy/setup)
      │   │   ├── BL sub_107FC4 (0x107FC4) ← 函数表分发
      │   │   │   └── BLR [X20,#0x4A*8] ← 动态分发
      │   │   └── STR W3,[X19,#0x23] ← 写回结果
      │   ├── BL sub_10721C (0x10721C) ← JNI 包装器
      │   │   └── BLR X8 → NewDirectByteBuffer?
      │   └── RET → 返回 DEX Decryption Handler
      ├── BLR [X23,#0x6E*8] ← 释放/清理
      └── TBNZ W1,#0 → 下一轮或结束
      
      
      | 地址                       | 描述                                |
    | -------------------------- | ----------------------------------- |
    | 0xFD504                    | INIT[0] - 代码解密                  |
    | 0xFD5B4                    | INIT[1] - VMP 初始化                |
    | 0xFD640                    | Key Generation 函数                 |
    | 0xFD700                    | MyJni.load native 入口              |
    | 0xFD9D8                    | JNI_OnLoad                          |
    | 0x100088                   | 字符串解密 (读取 0x10D320)          |
    | 0x107808/0x107818/0x107828 | PRNG mod_sub/mod_add/mod_mul        |
    | 0x10D320                   | 密钥表 (8×uint32, 占位值, 永不更新) |
    | 0x113568                   | 种子结构体 (BSS)                    |
    
    
    - `/xxx/apks/tt/xor_key_284.bin` — 256 字节 XOR 密钥流
    - `/xxx/apks/tt/unpacked_dex2/stub_0.dex` — 解密后的 284 字节 stub DEX
    - `/xxx/apks/tt/frida_readbuf.js` — 捕获 stub DEX 的 Frida 脚本
    - `/xxx/apks/tt/analyze_vm.py` — VM 字节码分析脚本
    

    结论:这个 apk 和上个 apk 一样,也是把密文存在了 dex 的后面,壳起来以后,跑了一堆加解密,解压出新的 dex ,然后用 InMemoryDexLoader 来加载真实的 dex ;


    五、某加密

    • apk: com.hexin.plat.android.ZheshangSecurity
    • md5: 1acc460d513540d9b0b6bf32c030b69f
    • so 特征:ijiami.dat, libexec.so

    加载流程:

    APK assets/ijiami.dat (17MB, 加密的 ZIP)
       │
       ├─ ① XOR 解密 ZIP 头/尾(隐藏 "PK\x03\x04" 魔术)
       │
       ├─ ② 得到标准 ZIP → 解压 "classes.dex" 条目( raw DEFLATE )
       │
       ├─ ③ 得到 57MB 的 ifd (= 字节替换加密的 DEX payload ,含 40 字节头)
       │
       └─ ④ 字节替换 + K0 头部 XOR + 末尾 map_list K0 XOR
            → 9 个 App 真身 DEX ( dex_3~dex_11 ,共 57,225,632 字节)
    

    AI 输出:

    对本 App 的 DEX 保护,拆解后其实是**三层**:
    
    1. **容器层**:ijiami.dat 是 ZIP (头尾 XOR 隐藏魔术)。
    2. **压缩层**:classes.dex 条目用 raw DEFLATE 压缩。
    3. **加密层**:57MB ifd 用字节替换 + K0 头部 XOR + map_list 特例。
    
    三层全部离线可逆,最终一个纯 Python 脚本就能从 APK 直接还原 9 个 App 真身 DEX ,**无需运行 App 、无需 frida 、无需 native 模拟**。
    
    **重要结论**:`com.ijm.dataencryption.DETool` 类在 dex 里是 stub ,真正组装这些参数、调用 dowork 的代码在**运行时被 native 壳替换**(爱加密的"stub + 运行时替换"手法)。dowork 的 256-bit key 派生(`6cf140de...`)是 native 侧的自定义派生,但对离线还原 DEX 完全无用——`dex_decrypt.py` 用硬编码 K0 + 数据相关替换已经绕过了它。
    
    
    | 地址    | 函数      | 作用                                        |
    | ------- | --------- | ------------------------------------------- |
    | 0x572a8 | al()      | JNI 入口,DEX 解密 + 建 ClassLoader         |
    | 0x5bd80 | sub_5bd80 | 写 ifd 文件( fopen "wb+" + fwrite + flock ) |
    | 0x34bac | 解压包装  | 调 0x65008/0x652a0/0x65468                  |
    | 0x65008 | 解析 ZIP  | 解析容器                                    |
    | 0x652a0 | 找条目    | 按名字找 "classes.dex"                      |
    | 0x65468 | 解压条目  | raw DEFLATE ( fcn.0006cbf4 windowBits=-15 )  |
    

    结论:这个 apk 是耗时最长的,涉及的重建几个 so ,然后用 garlic 分析重建的 elf 的工作,是隐藏的 dex ,隐藏的内容需要解密,解压,InMemoryDexLoader 加载,没有 vmp ;


    六、某迦

    加载流程:

    ShellApplication.attachBaseContext
      → Helper.a(context):创建 .cache/ .local/ .meta-inf/ .shell/ .jiagu/ 目录
      → 从 assets 提取加密载荷:
           maindata/res.data 、meta-inf/enc.mf 、maindata/xXxX.enc 、cert0_enc
      → System.loadLibrary("xloader")  → native 层解密出真实 DEX
      → DexInstall 反射注入 ClassLoader
           ( makeDexElements / makePathElements / makeInMemoryDexElements )
      → 通过 Instrumentation / Application 钩子启动 com.sf.base.MainApplication
    

    AI 输出:

    完整证据链:
    1. `com/Proxy/Helper.java` 中硬编码 `"d3d3Lm5hZ2Fpbi5jb20="` → base64 解码 = **`www.nagain.com`**
    2. 运行时创建 **`.jiagu/`**(加固)目录
    3. 壳代码结构:
       - `com.Proxy.ShellApplication` / `Helper` / `VLibrary` / `H` / `Defines`
       - `com.Install.DexInstall`
       - `com.appsec.*`( a ~ k )
    4. 专属 native 库:
       - `libxloader.so`(加载器)
       - `libnllvm.so`
       - `libKeyProvider.so`、`libMyKey.so`(内含 `"Example SO content for com.sf.activity"`)
       - `libentryexpro.so`、`libnMg.so`、`libdu.so`、`libwind.so`
       
       
    ## 三、安全防护机制
    
    - **完整性校验(防篡改)**:`Helper.b()` 对 APK 的 `sourceDir` 做 CRC 校验,与预存值比对( XOR 取反),篡改即检测
    - **反调试 / 反模拟器**:`Helper.a()` 读取 `/proc/self/exe` 检测 ELF 特征(字节 3 / 62 );`com.appsec.*` 包负责反调试逻辑
    - **字符串混淆**:`DexInstall.a()` 用 +3 偏移解密类名(如 `android.app.Application`、`android.app.Instrumentation`),配合大量反射调用   
    

    结论:这个 apk 没有隐藏的业务逻辑,复杂的地方在 j2c 保护的 http 签名的代码,纯静态没有办法分析( AI 说的),动态抓包和模拟就可以了。


    七、某固

    加载流程:

    原始 APK
      │
      │ ① 取出所有 dex 文件
      ├──────────────────────────────────────────────┐
      │                                              │
      │  classes.dex    classes2.dex    classes3.dex  │
      │        │              │              │        │
      │        │ ② NRV2D 压缩( UCL nrv2d ,不加密)     │
      │        ▼              ▼              ▼        │
      │  packed_dex[0]  packed_dex[1]  packed_dex[2]  │
      │                                              │
      │ ③ 可选的方法/资源数据(本样本未用)             │
      │    hashmap[0..2]  = NRV2D 压缩 + XTEA 加密     │
      │    bytecode[0..2] = NRV2D 压缩 + XTEA 加密     │
      │                                              │
      │ ④ 全部打包进一个文件 assets/0OO00l111l1l      │
      └──────────────────────────────────────────────┘
      │
      │ ⑤ 注入壳 so (含解密密钥 + NRV2D 解压函数 + 反调试)
      │    libshell-super.com.ifeng.newvideo.so
      │
      │ ⑥ 替换 Application 为壳入口
      │    com.ifeng.newvideo.StubWrapperProxyApplication
      ▼
    加固后的 APK
    

    AI 输出:

    偏移        字段              大小     含义
    0x00        nb_dex            u4       dex 文件个数(本样本 = 3 )
    0x04        packed_dex[0]     变长    第 1 个 dex 的压缩数据
                 ├ unknown1       u8      8 字节(用途不明,可能是随机数/校验)
                 ├ uncomp_size    u4      NRV2D 解压后的字节数
                 ├ comp_size      u4      压缩后的字节数
                 ├ unknown2       u4      本样本恒为 0x457 (=1111 )
                 └ data           comp_size  压缩数据( NRV2D 格式)
    ...         packed_dex[1]    变长    第 2 个 dex
    ...         packed_dex[2]    变长    第 3 个 dex
                hashmap[0..2]    变长    每个 = uncomp_size(u4) + comp_size(u4) + data
                bytecode[0..2]   变长    每个 = uncomp_size(u4) + comp_size(u4) + data
                
                
    变量: src 源, dst 目标, 位缓冲 bitbuf, 上次偏移 last_off(初始=1)
    
    主循环:
      if 读 1bit == 1:
          literal: dst += src[读 1 字节]
      else:
          # ---- 读 offset (varlen 变长编码) ----
          w16 = 1
          loop:
              w16 = (w16 << 1) | 读 1bit          # 累积
              if 读 1bit == 1: break              # 判断 bit ,1 终止
              w16 = (w16 << 1) - 2               # 继续时的 -2 调整
              w16 = w16 | 读 1bit                 # 额外累积
          if w16 == 2:
              offset = last_off                  # 复用上次偏移
          else:
              low = src[读 1 字节]                  # 偏移低位字节
              offset = ((w16 << 8) - 768 + low) >> 1 + 1
              last_off = offset
    
          # ---- 读 length ----
          if w16 == 2:                           # 复用分支
              w16_prev = 读 1bit << 1             # 额外读 1 bit
          else:                                  # 常规分支
              w16_prev = ((~low) & 1) << 1       # 由 low 的奇偶决定
          b0 = 读 1bit                            # length 前置检查
          w1 = b0 | w16_prev
          if w1 != 0:
              length = w1                        # 短长度(1/2/3)
          else:
              # varlen 累积
              w16 = 1
              loop:
                  w16 = (w16 << 1) | 读 1bit
                  if 读 1bit == 1: break
              length = w16 + 2
    
          # ---- 拷贝 ----
          copylen = length + 1 + (1 if offset > 1280 else 0)
          从 dst[len-offset] 拷贝 copylen 字节到 dst 末尾
          
          
    ① 从 APK 提取 0OO00l111l1l / tosversion / libshell-super.*.so
    ② 解析 0OO00l111l1l → nb_dex + 3 个 packed_dex
    ③ 对每个 packed_dex.data 用 unicorn 模拟 so 里的 NRV2D 函数解压
    ④ 解压结果跳 16 字节头 → 按 dex 头 file_size 截断 → 完整 dex
    ⑤ adler32 校验 checksum 确认完整    
    
    
    结果:
    python legu_unpack.py com.ifeng.newvideo.apk [输出目录]
    # 默认输出 ./unpacked/,得到 classes.dex / classes2.dex / classes3.dex
    

    结论:这个 apk 也是将 dex 藏起来了,然后通过壳的初始化,加载 native libs ,native 解密 dex ,通过 InMemoryDexLoader 启动 app ;


    八、测试效果

    1. 本身设计了 6 个测试案例全部成功,AI 对 garlic 的评价:apk 的地图生成器,AI 比较会舔,我挺受用。
    2. 发出来 5 个,有一个 vmp 的不发了,vmp 的虚拟机的字节码可以完全被 AI 推理出来
    3. 对比测试:将 garlic 替换成 jadx/ghidra/r2 等工具,效率会慢,token 消耗会变多
    4. 特别是 ghidra ,AI 用 java 的脚本,非常消耗 token
    5. 没有测试 IDA ,IDA 肯定会成功,个人感觉 IDA 导出的内容给 AI 查询的时候会消耗很多 token
    6. 在 deepseek 涨价之前,薅了一波

    如果有人需要逆向过程,可以发我邮件,我打包 calude 的 projects 给你。

    garlic 的 github: https://github.com/neocanable/garlic, 欢迎使用,特别欢迎 PR

    No Comments Yet
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   5747 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 07:01 · PVG 15:01 · LAX 00:01 · JFK 03:01
    ♥ Do have faith in what you're doing.