Install
$ agentstack add skill-as0ler-skills-deobfuscation ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Binary Deobfuscation & Unpacking
Framework for identifying, analyzing, and defeating software protection schemes on native binaries. This skill provides the generic methodology; protector-specific techniques are in sub-skills.
Sub-Skills
| Protector | Reference | |-----------|-----------| | VMProtect | [vmprotect/SKILL.md](./vmprotect/SKILL.md) — VMP 2.x/3.x detection, section dumping, string deobfuscation, devirtualization, anti-debug bypass |
1. General Workflow
1. Detect → identify the protector and features used
2. Bypass → neutralize anti-debug / anti-tamper
3. Unpack → dump decrypted code sections at runtime
4. Recover → extract strings, reconstruct imports
5. Analyze → decompile / devirtualize recovered code
2. Protector Identification
2.1 Quick Triage (radare2)
iS # unusual section names (.vmp0, .themida, .enigma, etc.)
iS~rwx # RWX sections — strong packing indicator
ii | wc -l # import count — very few = likely packed
iz | wc -l # string count — very few = string encryption
ie # entry point — does it land in a non-.text section?
s entry0; pd 20 # entry point instructions — push/call/jmp patterns
2.2 Protector Signatures
| Protector | Section Names | Entry Pattern | Key Imports | Other Indicators | |-----------|--------------|---------------|-------------|------------------| | VMProtect | .vmp0, .vmp1, .VMP | Jump into VMP section | dlsym/GetProcAddress only | VM dispatch loops, handler tables | | Themida/WL | .themida, .winlice | pushad; call $+5 | Kernel-level anti-debug | Driver-based protection | | Code Virtualizer | .cv0, .cv1 | Similar to VMP | Few imports | Oreans family (same vendor as Themida) | | Arxan/Irdeto | Normal names | Guard pages, integrity checks | Normal imports | Checksum functions, code guards | | UPX | UPX0, UPX1 | Decompression stub | Normal (after unpack) | upx -d works for unmodified UPX | | Custom | Varies | Varies | Often dlsym/mmap/mprotect | Manual analysis required |
2.3 Android .so Specific Detection
On Android, native libraries protected with VMP or similar tools show:
readelf -S target.so | grep -iE 'vmp|themida|cv[0-9]' # named sections
readelf -l target.so | grep RWE # RWX segments
readelf -d target.so | grep NEEDED # minimal dependencies
- RWX segments (
PF_R | PF_W | PF_X) — legitimate .so files almost never need this - JNIOnLoad doing heavy work — protectors use JNIOnLoad as the unpacking entry point
- mprotect storms at startup — the library calls mprotect repeatedly to make sections writable, unpack, then re-protect
3. Generic Dynamic Techniques
These techniques work regardless of the protector. They hook at the API boundary rather than trying to reverse the obfuscation internals.
3.1 Module Fingerprinting (Android)
Identify the target .so by matching a known export at an expected offset:
function findTargetModule(exportName, expectedOffset) {
for (const mod of Process.enumerateModules()) {
const exp = mod.findExportByName(exportName);
if (exp && exp.equals(mod.base.add(expectedOffset))) {
return mod;
}
}
return null;
}
This avoids name-based matching (which breaks when the library is renamed) and works even when the library hasn't been loaded yet when combined with a dlopen hook.
3.2 Library Load Detection
Wait for a target library to load at runtime:
function waitForLibrary(identifyFn, callback) {
var found = identifyFn();
if (found) { callback(found); return; }
var dlopen = Module.getGlobalExportByName('android_dlopen_ext')
?? Module.getGlobalExportByName('dlopen');
Interceptor.attach(dlopen, {
onLeave() {
if (found) return;
found = identifyFn();
if (found) callback(found);
}
});
}
3.3 mprotect Monitoring
Track memory permission changes to understand the unpacking sequence:
function hookMprotect(targetModule) {
var log = [];
Interceptor.attach(Module.getGlobalExportByName('mprotect'), {
onEnter(args) {
this.addr = args[0];
this.len = args[1].toUInt32();
this.prot = args[2].toInt32();
},
onLeave(retval) {
if (retval.toInt32() !== 0) return;
var start = this.addr;
var end = start.add(this.len);
var modStart = targetModule.base;
var modEnd = modStart.add(targetModule.size);
if (start.compare(modEnd) >= 0 || end.compare(modStart) = 0 &&
returnAddr.compare(targetEnd) 4096) return;
if (isDuplicate(source, str)) return;
send({ type: 'string', source: source, value: str, extra: extra || '' });
}
3.7 Python Host Script Pattern
Capture agent output and write categorized results:
import frida, sys, signal, time
strings_by_source = {}
def on_message(message, data):
if message["type"] == "send":
payload = message["payload"]
if isinstance(payload, dict) and payload.get("type") == "string":
source = payload.get("source", "unknown")
value = payload.get("value", "")
strings_by_source.setdefault(source, []).append(value)
print(f"[{source}] {value}")
elif isinstance(payload, dict) and payload.get("type") == "dump":
with open(f"dump_{payload['reason']}.bin", "wb") as f:
f.write(data)
print(f"[dump] {payload['size']} bytes saved")
device = frida.get_device_manager().add_remote_device("127.0.0.1:27042")
pid = device.spawn(["com.example.app"])
session = device.attach(pid)
script = session.create_script(open("agent.js").read())
script.on("message", on_message)
script.load()
device.resume(pid)
signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
while True: time.sleep(1)
3.8 uncompress / zlib Monitoring
Many protectors (including VMProtect) use zlib to decompress packed code at runtime:
var uncompress = Module.getGlobalExportByName('uncompress');
if (uncompress) {
Interceptor.attach(uncompress, {
onEnter(args) {
this.dest = args[0];
this.destLen = args[1];
this.srcLen = args[3].toUInt32();
},
onLeave(retval) {
if (retval.toInt32() !== 0) return;
var outLen = this.destLen.readU32();
console.log('[uncompress] ' + this.srcLen + ' -> ' + outLen + ' bytes @ ' + this.dest);
}
});
}
4. Tips
- Hook consumers, not decryptors. Reversing the decryption algorithm is hard and protector-specific. Hooking the APIs that receive decrypted data (strcmp, JNI, dlsym) is protector-agnostic and much faster.
- Filter by return address. Without filtering, libc hooks produce thousands of irrelevant hits from system libraries. Check
this.returnAddressagainst the target module range. - Dump after JNI_OnLoad (Android). For protected Android .so files, the unpacking is typically complete by the time
JNI_OnLoadis called. Hook it and dump sections fromonEnter. - Track mprotect to understand unpacking. The sequence of mprotect calls reveals which sections are being unpacked and when. This tells you the right moment to dump.
- Use fingerprinting, not names. Identify target modules by matching exports at known offsets, not by library name (which can change between builds).
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: as0ler
- Source: as0ler/skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.