360, Ijiami, NetEase Yidun, Naijia, Tencent Legu — an AI + garlic capability test

Translated by DeepSeek Flash.

Originally published in Chinese on Kanxue Forum.

1. Basic test situation

I’m a beginner at reverse engineering; to test how well garlic analyzes android native code, I designed the following test:
Test requirements:

  • Find some apks from packer vendors on the market
  • Including: 360, Ijiami, NetEase Yidun, Naijia, Tencent Legu
  • The versions of the packer products are unknown
  • Use an AI loop
  • Static analysis uses only garlic, dynamic emulation uses unicorn, dynamic analysis uses frida
  • Emulators are fine, real devices are not
  • Test system: macmin m4
  • AI model: deepseek-v4-pro
  • Client: cc switch
  • garlic version: 1.8 (unreleased)
  • Not allowed to use objdump/capstone in a way that affects the control flow

Acceptance criteria:

  • If the dex is hidden, obtain the dex -> done
  • If the dex isn’t hidden, obtain the request signature -> done
  • If there’s a VMP, work out the VM’s instruction set -> done

Comparison test:

  • Drop garlic, use toolchains like jadx/r2/unicorn/ghidra
  • Compare token usage
  • Compare analysis efficiency
  • Compare the various AIs

2. Basic principles of the software

When garlic’s mcp-server receives analyze xxx.apk it does the following:

  • Decompile all the dex
  • Generate the call graph for the whole apk
  • Generate the apk’s string references
  • Import the call graph into a database for the AI to query
  • garlic’s built-in rosemary engine generates the following for the AI to use
    1. Analyze the so’s cfg
    2. Disassembly
    3. exports/imports
    4. Call graph of known functions
    5. pc xref export
    6. strings export

Example:

.
├── 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


3. 360 packing

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

This is two layers of packing. I don’t know whether it’s a normal or an abnormal package — it wasn’t downloaded from the official site;

Loading flow:

StubApp.attachBaseContext()
  ├─ DtcLoader.<clinit> → System.loadLibrary("jgdtc")   (DTC engine, decrypted and generated at runtime)
  ├─ interface5(app)                                     (native, shell initialization)
  ├─ reflection loads the real Application "entryRunApplication"
  ├─ interface8(realApp, ctx)                            (native, decrypt DEX)
  └─ System.loadLibrary("jiagu") → releases jiagu.so into /.jiagu/ then loads it

AI output:


- Entry chain: `JNI_OnLoad → interpreter_wrap_int64_t(0x46af0) → sub_44eb4(0x44eb4)`
- 15-opcode typed stack machine, jump table @0x53968
- JNI_OnLoad real logic: caches Context + ActivityThread


- `classes.dex` tail (from offset 18156, 23MB) = encrypted payload (`d6edc6c6` directory structure + encrypted blob)
- `assets/goodgoodstudy.zip` = decoy;`assets/data.bin` = PNG atlas;`.jgapp` = 16-byte marker



The real DEX decryption is in the **second-stage encrypted .so** (`libdjiagu.so`/`libijmDataEncryption.so`/`libjgdtc.so`, generated by decrypting the ciphertext at runtime), or triggered by the `__arm_a_1`  routine (requires fixing its funcptr/arguments before emulation). Key point: find the `interface5`/`interface8` native methods (the RegisterNatives table is in the encrypted code; libjiagu.so's plaintext has no "interface5" string).

Conclusion: this apk is some 360 wrapped around an encryption product — feels like a pirated build; the dex you unzip is a shell when android runs it, and once the shell comes up it decrypts the ciphertext after the dex and releases the real business logic;


4. NetEase Yidun

  • apk: com.yiyou.ga
  • md5: 504ca64deaa7ea7c44ca58be3664f406
  • so signature: libvlplg.so
  • MyJni.load(Application, String) → returns true

Loading flow:

System.load(libvlplg.so)
  → Dynamic Linker:
    → INIT[0] (0xFD504): code/data decryption
    → INIT[1] (0xFD5B4): VMP initialization
    → JNI_OnLoad (0xFD9D8): register native methods
        
MyApplication.attachBaseContext()
  → MyJni.load(Application, String) [native 0xFD700]:
    → reads 37 chunks
    → decrypts the first 284 bytes of each chunk → stub DEX
    → 37 x InMemoryDexClassLoader(stub)
    → returns true
    
App runs afterwards:
  → when a class is needed → custom ClassLoader.findClass()
  → VMP decrypts the class data in the corresponding chunk
  → returns the full class definition

AI output:

DEX Decryption Handler (0xFDCF4)
  ├── LDR W2,[X3,#0x26C] W4,[X3,#0x27C] W5,[X3,#0x24C] W1,[X3,#0x25C]
  │   └── loads parameters from the struct parsed out of the chunk sub-header
  ├── LDR X0,[X20,#0x12]
  │   └── VM state offset 0x12 (keystream pointer?)
  ├── BL sub_101ED8 (0x101ED8)  ← VM interpreter core
  │   ├── PRNG chain (mod_mul/mod_add/mod_sub × N)
  │   ├── TBZ W7,#0 → verify PRNG output
  │   ├── LDR W1,[X1,#0x2910] → check data size
  │   ├── BL sub_10719C (0x10719C) ← data processing
  │   │   ├── BL sub_FD440 (memcpy/setup)
  │   │   ├── BL sub_107FC4 (0x107FC4) ← function table dispatch
  │   │   │   └── BLR [X20,#0x4A*8] ← dynamic dispatch
  │   │   └── STR W3,[X19,#0x23] ← writes back the result
  │   ├── BL sub_10721C (0x10721C) ← JNI wrapper
  │   │   └── BLR X8 → NewDirectByteBuffer?
  │   └── RET → returns DEX Decryption Handler
  ├── BLR [X23,#0x6E*8] ← release/cleanup
  └── TBNZ W1,#0 → next round or finish
  
  
  | Address                    | Description                         |
| -------------------------- | ----------------------------------- |
| 0xFD504                    | INIT[0] - code decryption           |
| 0xFD5B4                    | INIT[1] - VMP initialization        |
| 0xFD640                    | Key Generation function             |
| 0xFD700                    | MyJni.load native entry             |
| 0xFD9D8                    | JNI_OnLoad                          |
| 0x100088                   | string decryption (reads 0x10D320)  |
| 0x107808/0x107818/0x107828 | PRNG mod_sub/mod_add/mod_mul        |
| 0x10D320                   | key table (8×uint32, placeholders, never updated) |
| 0x113568                   | seed struct (BSS)                   |


- `/xxx/apks/tt/xor_key_284.bin` — 256-byte XOR keystream
- `/xxx/apks/tt/unpacked_dex2/stub_0.dex` — the decrypted 284-byte stub DEX
- `/xxx/apks/tt/frida_readbuf.js` — Frida script that captures the stub DEX
- `/xxx/apks/tt/analyze_vm.py` — VM bytecode analysis script

Conclusion: like the previous apk, this one also stores the ciphertext after the dex. Once the shell comes up it runs a pile of encryption/decryption, unpacks a new dex, and then uses InMemoryDexLoader to load the real dex;


5. Ijiami

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

Loading flow:

APK assets/ijiami.dat (17MB, encrypted ZIP)
   │
   ├─ ① XOR-decrypt the ZIP head/tail (hides the "PK\x03\x04" magic)
   │
   ├─ ② get a standard ZIP → decompress the "classes.dex" entry (raw DEFLATE)
   │
   ├─ ③ get a 57MB ifd (= byte-substitution-encrypted DEX payload, with a 40-byte header)
   │
   └─ ④ byte substitution + K0 head XOR + trailing map_list K0 XOR
        → 9 real App DEX files (dex_3~dex_11, 57,225,632 bytes total)

AI output:

This App's DEX protection, once taken apart, is actually **three layers**:

1. **Container layer**: ijiami.dat is a ZIP (head/tail XOR hides the magic).
2. **Compression layer**: the classes.dex entry is compressed with raw DEFLATE.
3. **Encryption layer**: the 57MB ifd uses byte substitution + K0 head XOR + a map_list special case.

All three layers are reversible offline, so in the end a single pure Python script can restore the 9 real App DEX files straight from the APK, **no need to run the App, no frida, no native emulation**.

**Key finding**: the `com.ijm.dataencryption.DETool` class is a stub in the dex; the code that actually assembles these parameters and calls dowork is **swapped in by the native shell at runtime** (Ijiami's "stub + runtime replacement" trick). dowork's 256-bit key derivation (`6cf140de...`) is a custom native-side derivation, but it's completely useless for offline DEX recovery — `dex_decrypt.py` with hardcoded K0 + data-dependent substitution already bypasses it.


| Address | Function  | Purpose                                     |
| ------- | --------- | ------------------------------------------- |
| 0x572a8 | al()      | JNI entry, DEX decrypt + build ClassLoader  |
| 0x5bd80 | sub_5bd80 | write ifd file (fopen "wb+" + fwrite + flock) |
| 0x34bac | Unpack    | calls 0x65008/0x652a0/0x65468               |
| 0x65008 | Parse ZIP | Parse the container                         |
| 0x652a0 | Locate    | Find "classes.dex" by name                  |
| 0x65468 | Unpacked  | raw DEFLATE (fcn.0006cbf4 windowBits=-15)   |

Conclusion: this apk took the longest. It involves rebuilding several so files, then using garlic to analyze the rebuilt elf. The dex is hidden, and the hidden content has to be decrypted, decompressed and loaded via InMemoryDexLoader — no vmp;


6. Naijia

  • apk: com.sf.activity
  • md5: 8a5ff65cf4e0f48b793259091b2e58e2
  • so signature: libxloader.so, libnllvm.so

Loading flow:

ShellApplication.attachBaseContext
  → Helper.a(context): creates the .cache/ .local/ .meta-inf/ .shell/ .jiagu/ directories
  → extracts the encrypted payload from assets:
       maindata/res.data, meta-inf/enc.mf, maindata/xXxX.enc, cert0_enc
  → System.loadLibrary("xloader")  → the native layer decrypts the real DEX
  → DexInstall reflectively injects into the ClassLoader
       (makeDexElements / makePathElements / makeInMemoryDexElements)
  → starts com.sf.base.MainApplication via an Instrumentation / Application hook

AI output:

Complete chain of evidence:
1. `com/Proxy/Helper.java` hardcodes `"d3d3Lm5hZ2Fpbi5jb20="` → base64-decoded = **`www.nagain.com`**
2. creates the **`.jiagu/`** (packing) directory at runtime
3. shell code structure:
   - `com.Proxy.ShellApplication` / `Helper` / `VLibrary` / `H` / `Defines`
   - `com.Install.DexInstall`
   - `com.appsec.*` (a ~ k)
4. dedicated native libraries:
   - `libxloader.so` (loader)
   - `libnllvm.so`
   - `libKeyProvider.so`, `libMyKey.so` (contains `"Example SO content for com.sf.activity"`)
   - `libentryexpro.so`, `libnMg.so`, `libdu.so`, `libwind.so`

Security protections

  • Integrity check (tamper detection): Helper.b() runs a CRC check on the APK’s sourceDir and compares it against a pre-stored value (XOR-inverted); tampering is detected
  • Anti-debug / anti-emulator: Helper.a() reads /proc/self/exe to check for ELF traits (bytes 3 / 62); the com.appsec.* package handles the anti-debug logic
  • String obfuscation: DexInstall.a() decrypts class names with a +3 offset (e.g. android.app.Application, android.app.Instrumentation), together with lots of reflection

Conclusion: this apk has no hidden business logic; the tricky part is the j2c-protected HTTP signature code, which pure static analysis can’t handle (as the AI said) — dynamic packet capture and emulation are enough.


7. Tencent Legu

  • apk: com.ifeng.newvideo
  • md5: 58bd5f262c3634addb533cb171b91971
  • so signature: libshell-super.com.ifeng.newvideo.so

Loading flow:

Original APK
  │
  │ ① pull out all the dex files
  ├──────────────────────────────────────────────┐
  │                                              │
  │  classes.dex    classes2.dex    classes3.dex  │
  │        │              │              │        │
  │        │ ② NRV2D compression (UCL nrv2d, no encryption) │
  │        ▼              ▼              ▼        │
  │  packed_dex[0]  packed_dex[1]  packed_dex[2]  │
  │                                              │
  │ ③ optional method/resource data (unused in this sample) │
  │    hashmap[0..2]  = NRV2D compression + XTEA encryption │
  │    bytecode[0..2] = NRV2D compression + XTEA encryption │
  │                                              │
  │ ④ everything packed into one file assets/0OO00l111l1l │
  └──────────────────────────────────────────────┘
  │
  │ ⑤ inject the shell so (contains the decryption key + the NRV2D decompression function + anti-debug)
  │    libshell-super.com.ifeng.newvideo.so
  │
  │ ⑥ replace the Application with the shell entry point
  │    com.ifeng.newvideo.StubWrapperProxyApplication
  ▼
Hardened APK

AI output:

Offset      Field             Size     Meaning
0x00        nb_dex            u4       dex file count (this sample = 3)
0x04        packed_dex[0]     var      compressed data of the 1st dex
             ├ unknown1       u8      8 bytes (purpose unknown, maybe a random number/checksum)
             ├ uncomp_size    u4      NRV2D decompressed byte count
             ├ comp_size      u4      compressed byte count
             ├ unknown2       u4      always 0x457 in this sample (=1111)
             └ data           comp_size  compressed data (NRV2D format)
...         packed_dex[1]    var       the 2nd dex
...         packed_dex[2]    var       the 3rd dex
            hashmap[0..2]    var       each = uncomp_size(u4) + comp_size(u4) + data
            bytecode[0..2]   var       each = uncomp_size(u4) + comp_size(u4) + data
            
            
Variables: src = source, dst = destination, bit buffer bitbuf, last offset last_off (initial=1)

main loop:
  if read1bit == 1:
      literal: dst += src[read1byte]
  else:
      # ---- read offset (varlen encoding) ----
      w16 = 1
      loop:
          w16 = (w16 << 1) | read1bit        # accumulate
          if read1bit == 1: break            # check the bit, 1 to stop
          w16 = (w16 << 1) - 2               # -2 adjustment when continuing
          w16 = w16 | read1bit               # extra accumulation
      if w16 == 2:
          offset = last_off                  # reuse the last offset
      else:
          low = src[read1byte]                # low byte of the offset
          offset = ((w16 << 8) - 768 + low) >> 1 + 1
          last_off = offset

      # ---- read length ----
      if w16 == 2:                           # reuse branch
          w16_prev = read1bit << 1           # read 1 extra bit
      else:                                  # normal branch
          w16_prev = ((~low) & 1) << 1       # decided by low's parity
      b0 = read1bit                          # length pre-check
      w1 = b0 | w16_prev
      if w1 != 0:
          length = w1                        # short length (1/2/3)
      else:
          # varlen accumulation
          w16 = 1
          loop:
              w16 = (w16 << 1) | read1bit
              if read1bit == 1: break
          length = w16 + 2

      # ---- copy ----
      copylen = length + 1 + (1 if offset > 1280 else 0)
      copy copylen bytes from dst[len-offset] to the end of dst
      
      
① extract 0OO00l111l1l / tosversion / libshell-super.*.so from the APK
② parse 0OO00l111l1l → nb_dex + 3 packed_dex entries
③ for each packed_dex.data, use unicorn to emulate the NRV2D function in the so to decompress
④ skip the 16-byte header of the decompressed result → truncate by the dex header's file_size → complete dex
⑤ verify integrity with an adler32 checksum    


Result:
python legu_unpack.py com.ifeng.newvideo.apk [output dir]
# default output ./unpacked/, producing classes.dex / classes2.dex / classes3.dex

Conclusion: this apk also hides the dex, then through the shell’s initialization it loads native libs, decrypts the dex natively, and launches the app via InMemoryDexLoader;


8. Test results

  1. I designed 6 test cases and all of them succeeded. The AI’s assessment of garlic: an apk map generator — the AI is quite good at sucking up, and I rather enjoyed it.
  2. I’m publishing 5 of them; I’m not publishing the one with a vmp. The vmp’s virtual machine bytecode can be fully inferred by the AI, and it’s a banking app
  3. Comparison test: swapping garlic for tools like jadx/ghidra/r2 makes things slower and consumes more tokens
  4. Especially ghidra — the AI uses java scripts, which burns a lot of tokens
  5. I didn’t test IDA. IDA would surely succeed; personally I feel that handing IDA’s exported content to the AI to query consumes a lot of tokens

If anyone needs the reverse engineering process, email me and I’ll zip up claude’s projects for you.

Using garlic -n requires a license. If you need one, you can reach me by email or WeChat. The email and WeChat are on the github project’s homepage.

garlic’s github: https://github.com/neocanable/garlic, welcome to use it, and PRs are especially welcome

← All posts