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
- Analyze the so’s cfg
- Disassembly
- exports/imports
- Call graph of known functions
- pc xref export
- 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’ssourceDirand compares it against a pre-stored value (XOR-inverted); tampering is detected - Anti-debug / anti-emulator:
Helper.a()reads/proc/self/exeto check for ELF traits (bytes 3 / 62); thecom.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
- 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.
- 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
- Comparison test: swapping garlic for tools like jadx/ghidra/r2 makes things slower and consumes more tokens
- Especially ghidra — the AI uses java scripts, which burns a lot of tokens
- 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