深夜提醒

现在是深夜,建议您注意休息,不要熬夜哦~

🏮 🏮 🏮

新年快乐

祝君万事如意心想事成!

share-image
中
ESC

Reverse-Engineering a Hardened Client's Request Signing with unidbg (Study Notes)

English 阅读中文版

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>.so is merely a bootstrap shell; the real logic lives in a version-numbered plugin .so (e.g., libxxxso-6.8.xxxxx.so naming convention). The plugin also carries a pkgInfo descriptor recording version and plugin class name.
  • Both .so files are stripped, with empty dynamic symbol tables, and string-encrypted — keywords like sign, appkey, doCommand can’t be found anywhere.
  • JNI uses RegisterNatives dynamic registration in JNI_OnLoad, so there are no Java_com_xxx_yyy export symbols in the files.
  • Anti-debugging traces like Fptrace_s can 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:

  1. Build: Source branch built with Maven (domestic mirrors are much faster). Compiling with JDK 17 hits ambiguity between Module and java.lang.Module (upstream was written for JDK 8) — adding two explicit import statements resolves it; the install phase gets blocked by maven-gpg-plugin — add -Dgpg.skip=true.
  2. Backend native library: For the 0.9.x unicorn2 backend, the JNI entry point is actually libunicorn.so in the artifacts, while the legacy artifact libunicorn_java.so is an outdated binding that throws undefined symbol: helper_div_i32 on load. Solution: put libunicorn.so into java.library.path and explicitly call addBackendFactory(new Unicorn2Factory(true)).
  3. Creating the emulator: 64-bit + AndroidResolver, and you must call vm.setJni(...) — otherwise it fails mid-execution with Please 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:

JNIEnv->FindClass(.../SecException)
JNIEnv->FindClass(.../adapter/common/HttpUtil)
CallStaticObjectMethod(...MainPlugin, getMainPluginClassLoader() => java.lang.ClassLoader)
JNIEnv->RegisterNatives(<bridge class>, ..., 1)
RegisterNative(<bridge class>, doCommandNative(I[Ljava/lang/Object;)Ljava/lang/Object;, 0x…)

Three takeaways:

  • The plugin reverse-requests a ClassLoader from the Java side — this means plugin class loading authority resides in native, and explains “why plugin classes aren’t in the APK’s dex.”
  • RegisterNatives directly tells you what the master entry point looks like (a doCommandNative(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:

graph TD
A["AndroidEmulator<br/>(aarch64 + Unicorn2 backend + AndroidResolver)"] --> B["loadLibrary: load bootstrap .so"]
B --> C["callJNI_OnLoad"]
C --> D{"native reverse-requests<br/>ClassLoader from Java"}
D -- "getMainPluginClassLoader" --> E["Java side provides a shell<br/>mapping loadClass to dex"]
E --> F["RegisterNatives<br/>registers doCommandNative(int, Object[])"]
F --> G["Command dispatch<br/>signing / static encryption / tokens / security body"]
G --> H["Invoked by cmd number"]
H --> I["Structured errors: 9901 uninitialized / 9905 parameter error"]
I --> J["Initialization happens at framework layer (dex)<br/>pure unidbg reaches its limit here"]

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 / cdex headers — after JNI_OnLoad and 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.

文章作者:阿文
文章链接: https://www.awen.me/post/5f28d615.html
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 阿文的博客!

评论

0 条评论
😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️
加载中...

留言反馈

😀😃😄 😁😅😂 🤣😊😇 🙂🙃😉 😌😍🥰 😘😗😙 😚😋😛 😝😜🤪 🤨🧐🤓 😎🥸🤩 🥳😏😒 😞😔😟 😕🙁☹️ 😣😖😫 😩🥺😢 😭😤😠 😡🤬🤯 😳🥵🥶 😱😨😰 😥😓🤗 🤔🤭🤫 🤥😶😐 😑😬🙄 😯😦😧 😮😲🥱 😴🤤😪 😵🤐🥴 🤢🤮🤧 😷🤒🤕 🤑🤠😈 👿👹👺 🤡💩👻 💀☠️👽 👾🤖🎃 😺😸😹 😻😼😽 🙀😿😾 👍👎👏 🙌👐🤲 🤝🤜🤛 ✌️🤞🤟 🤘👌🤏 👈👉👆 👇☝️✋ 🤚🖐️🖖 👋🤙💪 🦾🖕✍️ 🙏💅🤳 💯💢💥 💫💦💨 🕳️💣💬 👁️‍🗨️🗨️🗯️ 💭💤❤️ 🧡💛💚 💙💜🖤 🤍🤎💔 ❣️💕💞 💓💗💖 💘💝💟 ☮️✝️☪️ 🕉️☸️✡️ 🔯🕎☯️ ☦️🛐⛎ ♈♉♊ ♋♌♍ ♎♏♐ ♑♒♓ 🆔⚛️🉑 ☢️☣️📴 📳🈶🈚 🈸🈺🈷️ ✴️🆚💮 🉐㊙️㊗️ 🈴🈵🈹 🈲🅰️🅱️ 🆎🆑🅾️ 🆘❌⭕ 🛑⛔📛 🚫💯💢 ♨️🚷🚯 🚳🚱🔞 📵🚭❗ ❕❓❔ ‼️⁉️🔅 🔆〽️⚠️ 🚸🔱⚜️ 🔰♻️✅ 🈯💹❇️ ✳️❎🌐 💠Ⓜ️🌀 💤🏧🚾 ♿🅿️🈳 🈂🛂🛃 🛄🛅🛗 🚀🛸🚁 🚉🚆🚅 ✈️🛫🛬 🛩️💺🛰️