Tencent Legu¶
The most widely used Chinese packer. Free protection service integrated with Tencent's app distribution ecosystem. Frequently found on both legitimate Chinese apps and malware. The protection is adequate against automated AV scanning but yields to manual analysis with Frida-based unpacking.
Overview¶
| Property | Value |
|---|---|
| Vendor | Tencent |
| Free Tier | Yes |
| APKiD Signature | packer : Tencent Legu |
| Unpacking Difficulty | Medium |
Identification¶
| Artifact | Location | Description |
|---|---|---|
| Application class | AndroidManifest.xml |
com.tencent.StubShell.TxAppEntry replaces the real Application class |
| Meta-data | AndroidManifest.xml |
<meta-data android:name="TxAppEntry" android:value="<real_application_class>"/> stores the original Application class name |
| Native libraries | lib/armeabi/ |
libshella-<version>.so + libshellx-<version>.so (versioned by Legu release) |
| DEX stubs | lib/armeabi/ |
mix.dex + mixz.dex containing a single empty com.mixClass{} |
| Older native names | lib/ |
libshell-super.2019.so, libtxoprot.so in earlier versions |
| Runtime directory | /data/data/<pkg>/ |
tx_shell/ directory created at runtime containing libshella.so, libshellb.so, libshellc.so |
| Crash reporting | DEX | com.tencent.bugly.legu.crashreport.CrashReport with app ID 900015015 |
| Version string | DEX | Static method c() on the shell class returns version (e.g., "2.10.7.1"); static field version holds a hash |
Version Detection¶
The Legu version can be determined from the native library filename suffix (libshella-2.10.7.1.so) or by calling the version method at runtime:
Java.perform(function() {
var TxAppEntry = Java.use("com.tencent.StubShell.TxAppEntry");
console.log("Legu version: " + TxAppEntry.c());
});
Protection¶
Runtime Loading¶
TxAppEntry.attachBaseContext() loads the native shell library, which decrypts the real DEX from within the outer classes.dex using mmap/mprotect, then calls load() to inject the decrypted classes into the running process. runCreate() delegates to the real Application's onCreate(). The original code exists only in memory and is not extractable statically.
getPackageName() Override¶
Legu overrides getPackageName() with stack trace inspection to manipulate the ContentProvider installation order. This ensures the shell's providers initialize before the real app's providers, which is required for the decryption chain to complete before any app component tries to access protected classes.
Anti-Analysis¶
- DEX encryption with AES (decrypted via native library at runtime)
- Native library anti-debugging (ptrace self-attach)
- Emulator detection via hardware properties
- Anti-Frida checks (port scanning,
/proc/mapsinspection, named pipe detection) - String encryption in native layer
- Code segment checksumming
Unpacking¶
Static unpacking is not feasible since the DEX is decrypted in memory by native code. Dynamic analysis is required.
Standard Approach¶
- Hook
DexClassLoaderorInMemoryDexClassLoaderto intercept DEX loading - Dump DEX bytes from memory after native loader decrypts
- Alternative: use frida-dexdump which scans process memory for DEX headers
- Memory dump from
/proc/<pid>/mapsto locate decrypted DEX regions
Anti-Frida Bypass¶
Tencent Legu checks for Frida by scanning /proc/self/maps for frida-agent strings, probing port 27042, and checking named pipes in /proc/self/fd/. A combined bypass hooks these checks at the native level:
var openPtr = Module.findExportByName("libc.so", "open");
Interceptor.attach(openPtr, {
onEnter: function(args) {
this.path = args[0].readUtf8String();
},
onLeave: function(retval) {
if (this.path && this.path.indexOf("/proc") !== -1 &&
this.path.indexOf("/maps") !== -1) {
this.isMaps = true;
}
}
});
var readPtr = Module.findExportByName("libc.so", "read");
Interceptor.attach(readPtr, {
onLeave: function(retval) {
if (this.isMaps) {
var buf = this.context.x1;
var content = buf.readUtf8String();
if (content && content.indexOf("frida") !== -1) {
buf.writeUtf8String(content.replace(/frida/g, "aaaaa"));
}
}
}
});
var connectPtr = Module.findExportByName("libc.so", "connect");
Interceptor.attach(connectPtr, {
onEnter: function(args) {
var sockaddr = args[1];
var port = (sockaddr.add(2).readU8() << 8) | sockaddr.add(3).readU8();
if (port === 27042) {
args[1] = ptr(0);
}
}
});
For Legu versions after 2023, the packer also scans for frida-gadget in loaded modules. The Frida naming convention for renamed gadgets can bypass the string-based check. Using frida-server with --listen 0.0.0.0:1337 on a non-standard port avoids port scanning detection.
Malware Usage¶
| Family | Notes |
|---|---|
| Triada | Firmware variants |
| Chinese adware | Most common protection on Chinese-origin adware |