Origin
A certain App’s API includes a set of parameters that look very much like signatures (kps_wg, sign_wg, vcode — a style common to Alibaba-family clients). My initial instinct was:
These are all “client identity verification” — could I just spoof the User-Agent to pretend to be the official client and bypass rate limiting?
I spent two days tracing the entire chain. The conclusion was the complete opposite. This post only documents methodology and pitfalls — I won’t name the specific App, nor provide details that could be directly weaponized. This is purely a learning record.
1. Measure First, Reverse Second
Before diving in, I actually had a more practical question: a certain cloud drive’s download speed was only 300–400 KB/s, and I suspected “being identified as a third-party client and throttled.” So I first ran a round of controlled experiments:
| Variable | Result |
|---|---|
| Through local proxy | ~380 KB/s |
| Bypassing local proxy, direct to relay | Basically the same |
| Direct to cloud vendor’s CDN | Same |
| 4 files in parallel | Combined total still the same (not per-connection throttling) |
| 3 different UAs (including official client UA) | Absolutely no change |
| Different machine, same LAN, same account | 9 MB/s |
The conclusion was clear: the slowness has nothing to do with “whether the client is recognized” — it’s a problem with this particular machine’s network path. Measure the path first; don’t assume you’re being rate-limited — the time saved at this step exceeded all subsequent reverse-engineering work combined.
This is also the lesson I most want to record from this exercise: before reversing, first confirm whether it’s worth reversing.
2. Static Reconnaissance: What Can and Cannot Be Read
The target App’s request signing is handled by a hardened component. Statically, the structure looks like this:
- Plugin-based hardening:
lib<name>.sois merely a bootstrap shell; the real logic lives in a version-numbered plugin.so(e.g.,libxxxso-6.8.xxxxx.sonaming convention). The plugin also carries apkgInfodescriptor recording version and plugin class name. - Both
.sofiles are stripped, with empty dynamic symbol tables, and string-encrypted — keywords likesign,appkey,doCommandcan’t be found anywhere. - JNI uses
RegisterNativesdynamic registration inJNI_OnLoad, so there are noJava_com_xxx_yyyexport symbols in the files. - Anti-debugging traces like
Fptrace_scan be detected.
What’s readily visible also includes a “component number table” (signing, static data encryption, tokens, security bodies… dispatched by number), and the business-side code that assembles request parameters — the assembly logic is in plaintext; the algorithm is in native code.
3. unidbg Environment Setup (Pitfall Log)
To get it running, I used unidbg (Apache-2.0). Several pitfalls worth recording:
- Build: Source branch built with Maven (domestic mirrors are much faster). Compiling with JDK 17 hits ambiguity between
Moduleandjava.lang.Module(upstream was written for JDK 8) — adding two explicitimportstatements resolves it; theinstallphase gets blocked bymaven-gpg-plugin— add-Dgpg.skip=true. - Backend native library: For the 0.9.x unicorn2 backend, the JNI entry point is actually
libunicorn.soin the artifacts, while the legacy artifactlibunicorn_java.sois an outdated binding that throwsundefined symbol: helper_div_i32on load. Solution: putlibunicorn.sointojava.library.pathand explicitly calladdBackendFactory(new Unicorn2Factory(true)). - Creating the emulator: 64-bit +
AndroidResolver, and you must callvm.setJni(...)— otherwise it fails mid-execution withPlease vm.setJni(jni).
4. Let It Reveal Its Own Structure
The most reusable step in this entire exercise: don’t read assembly — just run JNI_OnLoad and dump the native startup protocol. The first run immediately reveals two critical things:
|
Three takeaways:
- The plugin reverse-requests a
ClassLoaderfrom the Java side — this means plugin class loading authority resides in native, and explains “why plugin classes aren’t in the APK’s dex.” RegisterNativesdirectly tells you what the master entry point looks like (adoCommandNative(int cmd, Object[] args)-style command dispatcher) — far more efficient than reverse-engineering assembly.- Combined with the component number table, you can confirm that “signing / encryption / tokens” are different branches of the same command dispatcher.
After providing a few JNI callbacks to native (ClassLoader handshake, static field reads, exception construction), JNI_OnLoad runs to completion.
The entire startup protocol looks like this:
|
5. Directly Calling Command Numbers: Error Codes Are Also Information
Directly invoking that command dispatcher and scanning a batch of candidate command numbers, the returns are all structured errors:
9901: uninitialized;9905: parameter error.
These error codes are themselves informative — they indicate that plugin initialization is driven by the App’s framework layer, and the framework layer is dex, which we have no ability to execute.
6. unidbg’s Boundary (The Biggest Insight This Time)
unidbg can only invoke native methods from the host side; the hardened component’s framework code (component implementations, initialization sequences) lives in dex, which the host cannot invoke.
This creates a deadlock:
To trigger the plugin to decrypt its bundled dex, you must first run the framework code; but the framework code IS the dex.
I tried two workarounds:
- Scanning emulator memory for
dex\n035/cdexheaders — afterJNI_OnLoadand during raw invocation of a dozen-plus command numbers: zero hits; - Hooking libc’s
open/openat/stat/mkdir— throughout the entire process: zero file access, indicating initialization never began.
Lesson: when emulating hardened plugins, first determine whether “initialization is in native or dex.” If it’s in dex, the pure-unidbg static emulation chain breaks and you need to switch to dynamic methods (root + hook, or running the real App in an emulator).
7. Conclusions
- This type of client signing is fundamentally device-bound keys + time-limited tokens: one token binds account information, another binds timestamp/ticket; keys are inside the hardened .so, bound to the device.
- Therefore changing UA, IP, or machine cannot “fool” it; conversely, what the server validates is that token set — not “whether you’re the official client.”
- The actual goal this time (slow downloads) didn’t require reverse-engineering at all — one path measurement solved it.
8. Compliance Statement
All observations above were conducted on my own account, my own devices, with the purpose of understanding how hardening solutions work. No server-side validation was bypassed, and no directly usable implementation details are provided externally. These notes only document methodology and pitfalls, for reference by fellow security learners.
Appendix: Tools Used
unidbg (Apache-2.0), jadx (Apache-2.0), and system utilities unzip / strings / readelf / nm. Sources and licenses were verified before installation.
评论
0 条评论