Manual De-obfuscation Techniques
Manual De-obfuscation Techniques
In the realm of software security, the process of making obscured code understandable, known as de-obfuscation, is crucial. This guide delves into various strategies for de-obfuscation, focusing on static analysis techniques and recognizing obfuscation patterns. Additionally, it introduces an exercise for practical application and suggests further resources for those interested in exploring more advanced topics.
Strategies for Static De-obfuscation
When dealing with obfuscated code, several strategies can be employed depending on the nature of the obfuscation:[2]
- DEX bytecode (Java): One effective approach involves identifying the application’s de-obfuscation methods, then replicating these methods in a Java file. This file is executed to reverse the obfuscation on the targeted elements.
- Java and Native Code: Another method is to translate the de-obfuscation algorithm into a scripting language like Python. This strategy highlights that the primary goal is not to fully understand the algorithm but to execute it effectively.
Identifying Obfuscation
Recognizing obfuscated code is the first step in the de-obfuscation process. Key indicators include:[2]
- The absence or scrambling of strings in Java and Android, which may suggest string obfuscation.
- The presence of binary files in the assets directory or calls to
DexClassLoader, hinting at code unpacking and dynamic loading. - The use of native libraries alongside unidentifiable JNI functions, indicating potential obfuscation of native methods.
Dynamic Analysis in De-obfuscation
By executing the code in a controlled environment, dynamic analysis allows for the observation of how the obfuscated code behaves in real time. This method is particularly effective in uncovering the inner workings of complex obfuscation patterns that are designed to hide the true intent of the code.[3][4]
Applications of Dynamic Analysis
- Runtime Decryption: Many obfuscation techniques involve encrypting strings or code segments that only get decrypted at runtime. Through dynamic analysis, these encrypted elements can be captured at the moment of decryption, revealing their true form.
- Identifying Obfuscation Techniques: By monitoring the application’s behavior, dynamic analysis can help identify specific obfuscation techniques being used, such as code virtualization, packers, or dynamic code generation.
- Uncovering Hidden Functionality: Obfuscated code may contain hidden functionalities that are not apparent through static analysis alone. Dynamic analysis allows for the observation of all code paths, including those conditionally executed, to uncover such hidden functionalities.
Automated De-obfuscation with LLMs (Androidmeda)
While the previous sections focus on fully manual strategies, in 2025 a new class of Large-Language-Model (LLM) powered tooling emerged that can automate most of the tedious renaming and control-flow recovery work.
One representative project is Androidmeda – a Python utility that takes decompiled Java sources (e.g. produced by jadx) and returns a greatly cleaned-up, commented and security-annotated version of the code.[5][6]
Key capabilities
- Renames meaningless identifiers generated by ProGuard / DexGuard / DashO / Allatori / … to semantic names.
- Detects and restructures control-flow flattening, replacing opaque switch-case state machines with normal loops / if-else constructs.
- Decrypts common string encryption patterns when possible.
- Injects inline comments that explain the purpose of complex blocks.
- Performs a lightweight static security scan and writes the findings to
vuln_report.jsonwith severity levels (informational → critical).
Installation
git clone https://github.com/In3tinct/Androidmeda
cd Androidmeda
pip3 install -r requirements.txt
Preparing the inputs
- Decompile the target APK with
jadx(or any other decompiler) and keep only the source directory that contains the.javafiles:jadx -d input_dir/ target.apk - (Optional) Trim
input_dir/so that it only contains the application packages you want to analyse – this massively speeds-up processing and LLM costs.
Usage examples
Remote provider (Gemini-1.5-flash):
export OPENAI_API_KEY=<your_key>
python3 androidmeda.py \
--llm_provider google \
--llm_model gemini-1.5-flash \
--source_dir input_dir/ \
--output_dir out/ \
--save_code true
Offline (local ollama backend with llama3.2):
python3 androidmeda.py \
--llm_provider ollama \
--llm_model llama3.2 \
--source_dir input_dir/ \
--output_dir out/ \
--save_code true
Output
out/vuln_report.json– JSON array withfile,line,issue,severity.- A mirrored package tree with de-obfuscated
.javafiles (only if--save_code true).
Tips & troubleshooting
- Skipped class ⇒ usually caused by an unparsable method; isolate the package or update the parser regex.
- Slow run-time / high token usage ⇒ point
--source_dirto specific app packages instead of the entire decompile. - Always manually review the vulnerability report – LLM hallucinations can lead to false positives / negatives.
Practical value – Crocodilus malware case study
Feeding a heavily obfuscated sample from the 2025 Crocodilus banking trojan through Androidmeda reduced analysis time from hours to minutes: the tool recovered call-graph semantics, revealed calls to accessibility APIs and hard-coded C2 URLs, and produced a concise report that could be imported into analysts’ dashboards.[5]
Targeted Dalvik string decryption with DaliVM
DaliVM is a Python Dalvik bytecode emulator aimed at statically recovering runtime-only values (especially decrypted strings) without spinning up Android. It executes a specific method inside an APK by emulating Dalvik opcodes and mocking Android/Java APIs.[1]
Workflow
- Select target method by Dalvik signature (
Lpkg/Class;->method(Args)Ret). Examples:Lutil/Crypto;->decrypt(Ljava/lang/String;)Ljava/lang/String;,LMyClass;->compute(II)I. - Enumerate call sites across multi-DEX (
classes*.dex) and reconstruct arguments via backward data-flow tracing, forward lookup, and partial execution when needed. - Emulate the method inside the Dalvik VM (covers 120+ opcodes across const/array/control/field/invoke, handles class init via
<clinit>) and collect return values (e.g., decrypted strings). - Bypass runtime dependencies using built-in mocks for common Android APIs (Context, PackageManager, Signature, reflection, system services) and hooks for Java stdlib (String/StringBuilder/Integer/Math/Arrays/List/Iterator).
- If execution stalls, enable opcode-level tracing to see PC/register changes and extend opcode handlers.
CLI usage
# Emulate a decryptor and dump all returns
python emulate.py app.apk "Lcom/example/Decryptor;->decrypt"
# Verbose, debug trace, and limit outputs
python emulate.py app.apk "Lcom/example/Decryptor;->decrypt" -v --debug --limit 10
Outputs are the collected return values per invocation; useful for bulk string/config extraction during malware triage or heavily obfuscated apps.
Offline recovery of staged Android malware payloads
A recurring Android malware pattern is a small Java stub + stripped JNI loader + high-entropy asset. If the APK contains a native library with one abnormally large JNI export, encrypted strings, and an assets/ blob that doesn’t match its file extension, you can usually recover the next stage without executing the sample.[7]
Repair hostile APK and DEX metadata before decompiling
Treat an APK as an adversarial ZIP, not as a trustworthy filesystem tree. List members before extraction and reject absolute paths, normalized paths that escape the output directory, and file/directory collisions. A sample can remain installable while crafted names make extractors omit entries, crash, or write outside the analysis directory.[8][11]
A DEX parser error also does not prove that the bytecode is encrypted. Compare the header and map_list against the actual layout: section sizes must fit inside file_size, fixed-width tables must be aligned, indexes must stay within their target tables, and string_data, class_data, and code_item references must decode consistently. A packer can swap map type labels or point table entries at valid-but-wrong data so a disassembler follows the attacker’s metadata instead of the real structures.[9][11]
Useful repairs on a disposable copy are:[9][11]
- Rebuild incorrect map entries from structurally valid candidate sections instead of trusting the declared type/offset pair.
- Replace a junk
code_item.debug_info_offwith0when debugging data is nonessential;0explicitly means that no debug information exists. - Parse only through the DEX header’s declared
file_size, but carve any trailing overlay for separate analysis; triage invalid references in unreachable methods separately from reachable code. - After patching offsets or instructions, update
file_size/map values as needed and recompute the DEX SHA-1 signature and Adler-32 checksum before reopening it in strict tools.
Recover indexed native string oracles and encrypted assets
When most Java strings are calls such as nativeGetStr(int), enumerate the integer call sites and reverse the single JNI routine as a string oracle.[8][11] In a stripped native library, the fixed AES S-box and Rcon tables identify AES; a nonce/counter block plus an incrementing counter distinguishes CTR-like use from ECB/CBC.[11] Preserve the exact counter layout and endianness when reimplementing it, then iterate all valid indexes to recover configuration, asset names, permission strings, and payload parameters in bulk.[11]
Apply recovered cipher parameters to high-entropy, extensionless assets offline and validate the plaintext independently rather than trusting a successful decrypt. For an embedded APK, check ZIP integrity and its signing metadata:[8][11]
file stage2.bin
unzip -t stage2.bin
apksigner verify --verbose --print-certs stage2.bin
Reconstruct DPT-Shell method bodies
DPT-Shell hollows DEX method implementations and reconstructs them at runtime.[10] Strong fingerprints are a small ProxyApplication/JniBridge stub, assets/OoooooOooo, and a native loader below assets/vwwwwwvwww/ for each ABI.[10][11] Repair deceptive DEX table offsets before resolving method indexes; otherwise valid code-store records will be mapped to the wrong methods.[11]
The upstream writer and runtime parser define the code store as little-endian records:[10]
u16 version
u16 dex_count
u32 dex_section_offset[dex_count]
for each DEX section:
u16 method_count
repeat method_count times:
u32 method_idx
u32 instruction_size_bytes
u8 instructions[instruction_size_bytes]
For each DEX section, use method_idx as an index into that file’s method_ids, locate the method’s code_item, and restore its insns[] bytes.[10][11] Keep DEX instruction units in mind: code_item.insns_size counts 16-bit code units whereas the external store records a byte length. Validate every offset/length against the store boundary, then regenerate the DEX signature/checksum after patching.[9][10]
Also inspect nominal image resources instead of assuming they are decoration. For PNGs, concatenate IDAT chunks in file order, decompress the zlib stream, and compare its expected scanline length with the actual output; unexplained trailing data or additional streams can be another code/payload carrier.[11]
OLLVM-style native XOR string recovery
A common native pattern is a one-time init block that decrypts strings in place byte-by-byte:
if (init_done == 0) {
DAT_00142f50 ^= 0xd7;
DAT_00142f51 ^= 0xb4;
DAT_00142f52 ^= 0xa6;
init_done = 1;
}
Practical workflow:
- In Ghidra, increase the decompiler timeout if one JNI function is tens of kilobytes long and initially fails to decompile.
- Parse assignments like
DAT_xxxx ^= 0xNNfrom the decompiler output to build an address → XOR key map. - Apply that map to the corresponding bytes from the ELF
.data/.rodatasection using Python, LIEF, or rawreadelfoffsets. - Recovered strings often expose asset names, class names, JNI signatures, crypto primitives, and backend URLs needed to unpack the next stage.
This is especially useful when the native loader decrypts its strings only on the first invocation, leaving plaintext only in process memory but never on disk.
Rebuilding filename-derived AES asset decryptors
Once native strings reveal constants such as an asset name, SHA-1, SHA-256, and AES/CBC/PKCS5Padding, recreate the decryptor offline and validate the result with file magic:
from Crypto.Cipher import AES
import hashlib
ct = open('asset.bin','rb').read()
seed = b'asset_name2'
key = hashlib.sha1(seed).digest()[:16]
iv = hashlib.sha256(seed).digest()[:16]
pt = AES.new(key, AES.MODE_CBC, iv).decrypt(ct)
pt = pt[:-pt[-1]]
open('stage2.bin','wb').write(pt)
Triage hints:
- Test seeds derived from asset names, parent folder names, or adjacent literals.
- If plaintext starts with
PK\x03\x04, treat it as a ZIP container and inspect every entry before focusing on a singleclasses.dex. - Reuse the same derivation pattern against nested assets; packers frequently keep the same KDF across stages.
StringFog and similar DEX string obfuscators
If JADX shows many Base64-looking constants and a helper like StringFogImpl.decrypt(String), extract the key and replay the transform over all candidate strings. One common variant is Base64 decode + repeating-key XOR:
import base64
def sf(s):
key = b'UTF-8'
ct = base64.b64decode(s)
return bytes(c ^ key[i % len(key)] for i, c in enumerate(ct)).decode()
Recovered plaintext commonly reveals Firebase paths, Telegram bot logic, phishing URLs, WebView resources, and operator config that do not appear anywhere else in the APK.
Treat split APKs, loaders, and web assets as one payload chain
After decrypting a staged container:
- Inspect
bootstrap.dex/installer.dex/payload_split*.apk/ HTML assets together instead of analyzing each file in isolation. - Expect
DexClassLoaderor equivalent runtime assembly logic even if each split APK looks incomplete on its own. - Check both native code and phishing WebView assets for separate backend URLs; the payment-theft infrastructure may be distinct from the RAT C2.
- Search config files for subscription timestamps, HMAC/signature fields, wallet addresses, or miner toggles because MaaS builders often hide monetization logic in JSON rather than code.
References
- [1] DaliVM: Python Dalvik emulator for static string decryption
- [2] 6. Reverse Engineering Android Apps - Obfuscation
- [3] BlackHat USA 2018: “Unpacking the Packed Unpacker: Reverse Engineering an Android Anti-Analysis Library” (video)
- This talk goes over reverse engineering one of the most complex anti-analysis native libraries I’ve seen used by an Android application. It covers mostly obfuscation techniques in native code.
- [4] REcon 2019: “The Path to the Payload: Android Edition” (video)
- This talk discusses a series of obfuscation techniques, solely in Java code, that an Android botnet was using to hide its behavior.
- [5] Deobfuscating Android Apps with Androidmeda: A Smarter Way to Read Obfuscated Code
- [6] Androidmeda source code
- [7] Fake RTO Challan Checker Part 2: Cracking the Payload, Mapping the Operator, and Why This Is Worse Than I Thought
- [8] Fake mParivahan APK — original malware analysis and sample research
- [9] Android Open Source Project — Dalvik executable format
- [10] dpt-shell — Android DEX protection shell source
- [11] 78 Victims, One Lazy Key, and a Firebase Named After India’s Ruling Party