π Language: English | δΈζ
A static + dynamic deobfuscation tool for Mach-O binaries, suitable for security auditing and reverse-engineering research.
π License: MIT (see LICENSE)
- β
Supports
x86_64andarm64 - β Supports thin Mach-O 64-bit
- β Supports fat Mach-O (with architecture filtering)
- β Static XOR range deobfuscation with boundary checks
- β Dynamic Unicorn-based emulation + memory dump
- β
Symbol string restoration (
lazy bind + symtab) - β Common section/segment name repair
Install from GitHub ZIP:
pip install https://github.com/fjh658/tnt_deobfuscator/archive/refs/heads/main.zipOr local editable install:
pip install -e .During install, the IDA plugin is deployed automatically:
- macOS / Linux:
~/.idapro/plugins/tnt_deobfuscator_ida.py - Windows:
%APPDATA%\\Hex-Rays\\IDA Pro\\plugins\\tnt_deobfuscator_ida.py - Windows fallback:
%APPDATA%\\IDA Pro\\plugins\\tnt_deobfuscator_ida.py - Final fallback (all platforms):
~/.idapro/plugins/tnt_deobfuscator_ida.py - Supported IDA version: 8.3+
- Auto-deployed on install; manual reinstall/remove is available via
--install/--uninstall. - Install tries a symlink first (for easier cleanup), then falls back to file copy.
In IDA, the plugin now supports actions:
repair: repair current IDB (recommended after loading*.deobf)deobfuscate: run external CLI onlyboth: run CLI, then repair current IDB
Default behavior: static can be omitted.
tnt-deobfuscator -i <input_binary> is equivalent to tnt-deobfuscator static -i <input_binary>.
tnt-deobfuscator -i <input_binary> -o <output_binary>Underscore alias is also available:
tnt_deobfuscator -i <input_binary> -o <output_binary>Examples:
tnt-deobfuscator static -i <input_binary> -o <output_binary> --arch all
tnt-deobfuscator static -i <input_binary> -o <output_binary> --arch x86_64
tnt-deobfuscator static -i <input_binary> -o <output_binary> --arch arm64
tnt-deobfuscator dynamic -i <input_binary> -o <output_binary>
tnt-deobfuscator dynamic -i <input_binary> -o <output_binary> --emu-timeout-ms 30000 --emu-max-insn 2000000
tnt-deobfuscator dynamic -i <input_binary> -o <output_binary> --dynamic-string-layer none
tnt-deobfuscator dynamic -i <input_binary> -o <output_binary> --dynamic-string-layer analysis
tnt-deobfuscator dynamic -i <input_binary> -o <output_binary> --dynamic-string-layer runnable
tnt-deobfuscator dynamic -i <input_binary> -o <output_binary> --arch arm64 --dynamic-string-layer analysis --arm64-disable-early-stop --verbose
tnt-deobfuscator -i <input_binary> -o <output_binary> # default static
tnt-deobfuscator -i <input_binary> --verbose
tnt-deobfuscator -i <input_binary> --force-reprocess
tnt-deobfuscator --install
tnt-deobfuscator --uninstall
tnt-deobfuscator --install-ida-plugin
tnt-deobfuscator --uninstall-ida-pluginIf -o is omitted, output defaults to <input>.deobf.
Without subcommand, tnt-deobfuscator -i ... runs stage-1 static mode by default.
--install / --uninstall are future-proof integration entrypoints; currently they manage IDA plugin install/uninstall.
--install-ida-plugin / --uninstall-ida-plugin are explicit plugin-only forms.
Common options (static and dynamic):
-i/--input(required)
tnt-deobfuscator static -i app.dylib-o/--output(optional, default<input>.deobf)
tnt-deobfuscator static -i app.dylib -o app.clean.dylib--arch all(default; patch all supported slices)
tnt-deobfuscator static -i app.dylib --arch all--arch x86_64(patch x86_64 slice only)
tnt-deobfuscator static -i app.dylib --arch x86_64--arch arm64(patch arm64 slice only)
tnt-deobfuscator static -i app.dylib --arch arm64--verbose(show diagnostics and candidate rejection reasons)
tnt-deobfuscator static -i app.dylib --verbose--force-reprocess(allow running stage-1 again on files that look already processed)
tnt-deobfuscator static -i app.dylib.deobf --force-reprocessDynamic-only options (dynamic subcommand):
--emu-timeout-ms(default30000;0means unlimited)
tnt-deobfuscator dynamic -i app.dylib --emu-timeout-ms 60000
tnt-deobfuscator dynamic -i app.dylib --emu-timeout-ms 0--emu-max-insn(default2000000;0means unlimited)
tnt-deobfuscator dynamic -i app.dylib --emu-max-insn 4000000
tnt-deobfuscator dynamic -i app.dylib --emu-max-insn 0--dynamic-string-layer none(disable runtime string extraction/reporting; keeps dynamic code overlay behavior)
tnt-deobfuscator dynamic -i app.dylib --dynamic-string-layer none--dynamic-string-layer analysis(default; extract/report runtime decoded strings without string-byte patching)
tnt-deobfuscator dynamic -i app.dylib --dynamic-string-layer analysis--dynamic-string-layer runnable(patch decoded string bytes + matching key sites)
tnt-deobfuscator dynamic -i app.dylib --dynamic-string-layer runnable--arm64-disable-early-stop(arm64-only; disable arm64 early-stop heuristic for deeper runtime coverage; may run significantly longer)
tnt-deobfuscator dynamic -i app.dylib --arch arm64 --dynamic-string-layer analysis --arm64-disable-early-stop --verbose- Full dynamic run example
tnt-deobfuscator dynamic -i app.dylib -o app.dynamic.deobf --arch arm64 --emu-timeout-ms 45000 --emu-max-insn 3000000 --dynamic-string-layer runnable --verboseIntegration management options:
--install(install all integrations; currently IDA plugin)
tnt-deobfuscator --install--uninstall(remove all integrations; currently IDA plugin)
tnt-deobfuscator --uninstall--install-ida-plugin(install plugin only)
tnt-deobfuscator --install-ida-plugin--uninstall-ida-plugin(remove plugin only)
tnt-deobfuscator --uninstall-ida-pluginstatic: performs metadata-driven static recovery (XOR chunk decryption), then repairs symbol strings and section/segment names.dynamic: first tries static prime, then emulates runtime entry with Unicorn and applies observed memory modifications.--dynamic-string-layeris only meaningful indynamicmode.--arm64-disable-early-stopis only meaningful indynamic+arm64; it disables arm64 early-stop and favors deeper coverage over speed.- In
dynamic+arm64, when default limits are used, runtime budget is auto-tuned to180000msand50000000instructions.
flowchart TD
A["Input binary (x86_64 slice)"] --> B["Parse slice and validate arch"]
B --> C["Static: scan metadata table and select candidate"]
C --> D["XOR chunk decryption + symfix/secfix"]
D --> E{"Mode"}
E -->|"static"| F["Write .deobf and suggest IDA repair"]
E -->|"dynamic"| G["Dynamic prime (run static patch first)"]
G --> H["Locate stubs + runtime entry"]
H --> I["Unicorn x86_64 emulation (main thread path)"]
I --> J["Collect runtime string candidates"]
J --> K{"dynamic-string-layer"}
K -->|"analysis"| L["Report runtime decoded strings"]
K -->|"runnable"| M["Patch decoded strings + x86 key sites"]
L --> N["Write output + NEXT guidance"]
M --> N
flowchart TD
A["Input binary (arm64 slice)"] --> B["Parse slice and validate arch"]
B --> C["Static: scan metadata table (reject -> continue -> selected)"]
C --> D["XOR chunk decryption + symfix/secfix"]
D --> E["arm64 static string pass: extract/apply/key-patch"]
E --> F{"Mode"}
F -->|"static"| G["Write .deobf and suggest IDA repair"]
F -->|"dynamic"| H["Dynamic prime (run static patch first)"]
H --> I["Locate stubs + runtime entry (prefer Objective-C load)"]
I --> J["Unicorn arm64 emulation (main thread + pthread sync)"]
J --> K["Observe runtime strings/helper literals/stub hits"]
K --> L{"Early-stop"}
L -->|"on"| M["STOP_EARLY when runtime becomes stable"]
L -->|"off"| N["Run until emu stop condition"]
M --> O{"dynamic-string-layer"}
N --> O
O -->|"analysis"| P["Report runtime observations"]
O -->|"runnable"| Q["Patch strings + arm64 key sites + helper layer"]
P --> R["Write output + NEXT guidance"]
Q --> R
- Stage 1 (
staticordynamic):tnt-deobfuscator static -i <input_binary> -o <input_binary>.deobf - Stage 2 (
repair): open*.deobfin IDA and run plugin actionrepair
Safety policy:
- Stage 1 never overwrites the input file (output must differ from input).
- Stage 2 modifies IDB metadata only; on-disk binary bytes are not patched.
TNT_IDA_PLUGIN_DIR: force plugin install directory (custom/testing)TNT_IDA_PLUGIN_LINK_MODE: plugin install strategy (auto/symlink/copy, default:auto)TNT_DEOBF_SKIP_IDA_PLUGIN_INSTALL=1: skip plugin auto-installTNT_DEOBF_ACTION: default plugin action (repair/deobfuscate/both)TNT_DEOBF_MODE: default plugin mode (static/dynamic)TNT_DEOBF_ARCH: default plugin arch (all/x86_64/arm64)TNT_DEOBF_DYNAMIC_ARGS: dynamic-mode extra args (e.g.--emu-timeout-ms 30000 --emu-max-insn 2000000)TNT_DEOBF_FORCE_REPROCESS=1: bypass plugin first-stage reprocess warningTNT_DEOBF_FORCE_REPAIR=1: bypass plugin second-stage (repair) repeat warningTNT_DEOBF_REPAIR_PROFILE: repair behavior profile (auto/analysis/runnable)analysis: comment-based string restoration only (no string type creation)runnable: keep existing repair behavior (including string type creation when matched)auto(default): infer from filename (*.analysis*-> analysis,*.runnable*-> runnable)__*literals (for example__TEXT,__DATA,__LINKEDIT) are treated as Mach-O style literals in naming/annotation (machogroup), not generic env-style literals.
TNT_DEOBF_NO_ACTION_PROMPT=1: skip action prompt and useTNT_DEOBF_ACTIONTNT_DEOBF_NO_MODE_PROMPT=1: skip mode prompt and useTNT_DEOBF_MODETNT_DEOBF_NO_ARCH_PROMPT=1: skip arch prompt and useTNT_DEOBF_ARCH(or IDA processor auto-detection)TNT_DEOBF_NO_DYNAMIC_PROMPT=1: skip dynamic args prompt- By default, plugin prompts for
action/mode/archuse readonly dropdowns (non-editable). TNT_DEOBF_CLI: override CLI command/path used by the pluginTNT_DEOBF_ARGS: append global extra CLI argsTNT_DEOBF_TIMEOUT_SEC: optional plugin-side timeout (seconds) for external CLI invocation
Environment variable examples:
- Install plugin to a custom path
TNT_IDA_PLUGIN_DIR=/tmp/ida_plugins tnt-deobfuscator --install-ida-plugin- Force plugin install style to symlink/copy
TNT_IDA_PLUGIN_LINK_MODE=symlink tnt-deobfuscator --install-ida-plugin
TNT_IDA_PLUGIN_LINK_MODE=copy tnt-deobfuscator --install-ida-plugin- Skip auto plugin deployment during pip install
TNT_DEOBF_SKIP_IDA_PLUGIN_INSTALL=1 pip install -e .- Make plugin run non-interactive dynamic deobfuscation by default
export TNT_DEOBF_ACTION=deobfuscate
export TNT_DEOBF_MODE=dynamic
export TNT_DEOBF_ARCH=arm64
export TNT_DEOBF_DYNAMIC_ARGS="--emu-timeout-ms 60000 --emu-max-insn 3000000 --dynamic-string-layer runnable"
export TNT_DEOBF_NO_ACTION_PROMPT=1
export TNT_DEOBF_NO_MODE_PROMPT=1
export TNT_DEOBF_NO_ARCH_PROMPT=1
export TNT_DEOBF_NO_DYNAMIC_PROMPT=1- The tool scans obfuscation metadata after
mach_header_64 + sizeofcmds. - Each
(start, size)chunk is XOR-restored using a computed key. - Symbol restoration uses a safe replacement policy to avoid overwriting adjacent string slots.
- Dynamic mode requires Unicorn (included in normal install dependencies). If installed with
--no-deps, runpip install unicorn. - In verbose static logs,
reject table@...lines mean invalid candidates were skipped; scanning continues until a valid table is selected. - Dynamic string layer options (default:
analysis): none: disable dynamic string handling.analysis: extract/report runtime-decoded strings without patching file semantics.runnable: decode string bytes and patch matching decode-key sites together.noneandanalysiskeep the same output-file semantics for string bytes (no string-byte patching). The practical difference is reporting/extraction visibility and runtime cost.- Runtime string handling no longer skips
__*literals by default (applies to bothanalysisreporting andrunnablepatch path). - Protection-hook stub selection is arch-aware:
x86_64prefers_mprotect(fallback_vm_protect),arm64prefers_vm_protect(fallback_mprotect). - Dynamic summary prints resolved stub symbols, e.g.
mprotect_stub=0x...(_mprotect)anddyld_stub=0x...(__dyld_get_image_vmaddr_slide). - In IDA plugin, canceling the
Output filedialog aborts the fulldeobfuscate/bothaction (no fallback write path).
Licensed under the MIT License. See LICENSE.
