FerrugoCC
An experimental C compiler and obfuscating compiler written in Rust.
Compiles a practical subset of C to x86_64 assembly (System V ABI), with an optional 16-pass obfuscation pipeline. Developed following Writing a C Compiler by Nora Sandler, then extended with real-world corpus support and obfuscation.
Status: Preview (v0.1.0) — Not a production compiler. See Supported Scope and Known Limitations.
Requirements
- Rust 2024 edition (1.85+)
- gcc — used for preprocessing (
gcc -E -P) and assembling/linking - Target: x86_64 only (Linux and macOS)
- macOS on Apple Silicon: requires Rosetta 2 (
arch -x86_64)
Install
From crates.io:
From the repository checkout:
Supported Scope
Language features:
- Types:
int,long,unsigned,double,char,void, pointers, arrays, structs - Control flow:
if/else,while,do/while,for,switch/case,goto/label, ternary?: - Functions: declarations, definitions, variadic (
va_list/va_arg), function pointers (compositional declarator tree — arrays of fn ptrs, struct member fn ptrs, pointer-to-array) - Declarations:
typedef,enum,struct,static,extern, file-scope initializers - Operators: arithmetic, bitwise (
& | ^ << >>), logical, comparison, compound assignment - Preprocessor: delegated to
gcc -E(handles#include,#define,#ifdef, etc.)
Obfuscation (16 passes): constant encoding, arithmetic substitution, junk code, opaque predicates, control flow flattening, string encryption, VM virtualization, library function obfuscation, OPSEC sanitization, anti-disassembly, indirect calls, register shuffle, stack frame obfuscation, instruction substitution, function inlining, function outlining
Tested corpora: jsmn (JSON parser), inih (INI parser), sds (dynamic strings), kilo (text editor, smoke test)
Known Limitations
- No self-hosted preprocessor — requires
gccon PATH - No
union,float(parsed asdouble),_Bool, bitfields (parsed but ignored) - No multi-file compilation (single translation unit only)
- No
__attribute__semantics (syntax is skipped) __builtin_bswap*treated as identity (not lowered to byte-swap)- Function pointer types lose prototype information (stored as
Pointer(Void)) - Register allocation may produce non-deterministic output (HashMap iteration order)
Corpus Licensing
The corpus/ directory contains third-party C projects used for regression testing:
- jsmn: MIT — zserge/jsmn
- inih: BSD-3-Clause — benhoyt/inih
- sds: BSD-2-Clause — antirez/sds
- kilo: BSD-2-Clause — antirez/kilo
Each directory contains an ORIGIN file with provenance details.
Build & Run
# Full compilation (C source -> executable)
# Stop at each stage
# Obfuscation compilation (applies obfuscation passes instead of optimization)
# Obfuscation level (1=light, 2=standard, 3=full, 4=maximum)
# Per-pass control
# OPSEC policy control (only "warn" and "deny" are accepted; invalid values are rejected)
# Frequency parameters
Requires gcc on the system for assembling and linking.
Tests
Benchmark Suite
A benchmark suite of 20 C programs for quantitative evaluation of obfuscation effectiveness.
Quick Run (5 levels)
Generates 100 binaries (20 programs x 5 levels) + 100 assembly files, with automatic correctness verification via exit codes and size aggregation.
Full Evaluation (11 conditions + metrics + plots)
Runs all 20 benchmarks under 11 conditions (L0-L4 + 6 ablation), collecting
correctness, binary size, execution time, and reverse-engineering metrics.
Generates CSV files and visualization plots in results/YYYYMMDD/.
See Evaluation Infrastructure for details.
Benchmark Programs
| # | File | Category | Expected exit code |
|---|---|---|---|
| 01 | constant_return.c |
Constant return | 42 |
| 02 | arithmetic.c |
Arithmetic + type conversions | 30 |
| 03 | conditional.c |
if/else chains | 77 |
| 04 | loop_sum.c |
for loop summation | 55 |
| 05 | nested_loops.c |
Nested loops (bubble sort) | 101 |
| 06 | function_calls.c |
Multiple functions + recursion | 120 |
| 07 | pointers.c |
Pointer arithmetic + arrays | 90 |
| 08 | strings.c |
String literal operations | 44 |
| 09 | structs.c |
Structs + pointers | 46 |
| 10 | mixed_complex.c |
Combined features | 37 |
| 11 | deep_recursion.c |
Fibonacci + mutual recursion | 89 |
| 12 | branch_heavy.c |
Multi-stage if-else chains | 63 |
| 13 | switch_table.c |
switch/case + enum | 55 |
| 14 | matrix_ops.c |
3x3 matrix multiply + trace | 30 |
| 15 | linked_list.c |
Array-based linked list traversal | 15 |
| 16 | string_search.c |
Hand-written strstr + counting | 3 |
| 17 | bitwise_ops.c |
Hash, modular exponentiation, digit sum | 42 |
| 18 | multi_array.c |
Flattened 2D array transpose + sum | 45 |
| 19 | indirect_calls.c |
Switch-based function dispatch | 60 |
| 20 | struct_chain.c |
Nested structs + pointer access | 77 |
Output: benchmark/output/level_N/<name> (binaries), benchmark/output/level_N/<name>.s (assembly)
Intended for quantitative evaluation with deobfuscators (D-810, SATURN, etc.).
Evaluation Infrastructure
scripts/eval/ contains a complete evaluation pipeline for research papers.
Designed for x86_64 Linux; requires Bash 4+, gcc, GNU time, nm, strings, Python 3 + matplotlib.
See docs/evaluation-method.md for full methodology.
Scripts
| Script | Purpose | Output |
|---|---|---|
run_all.sh |
Main entry: build, compile all conditions, run sub-scripts, plot | results/YYYYMMDD/ |
collect_correctness.sh |
Run binaries, compare exit codes to expected | correctness.csv |
measure_size.sh |
Collect binary sizes | size.csv |
measure_perf.sh |
Wall-clock timing (N runs, default 10) | performance.csv |
collect_reverse_metrics.sh |
nm symbols, strings, .globl/label/call counts | reverse_metrics.csv |
plot.py |
Generate 4 figures from CSV data (matplotlib) | fig_*.png |
Evaluation Conditions (11)
| Condition | Flags |
|---|---|
| L0 | (no --fobfuscate) |
| L1 | --fobfuscate --obf-level=1 |
| L2 | --fobfuscate --obf-level=2 |
| L3 | --fobfuscate --obf-level=3 |
| L4 | --fobfuscate --obf-level=4 |
| L3-no-cff | --fobfuscate --obf-level=3 --obf-no-cff |
| L3-no-str | --fobfuscate --obf-level=3 --obf-no-strings |
| L3-no-arith | --fobfuscate --obf-level=3 --obf-no-arith-subst |
| L3-no-inl | --fobfuscate --obf-level=3 --obf-no-func-inline |
| L3-no-outl | --fobfuscate --obf-level=3 --obf-no-func-outline |
| L4-no-vm | --fobfuscate --obf-level=4 --obf-no-vm-virtualize |
Output Directory Structure
results/YYYYMMDD/
meta.json # Environment info (OS, kernel, rustc, gcc, commit)
correctness.csv # program,condition,expected,actual,pass
size.csv # program,condition,size_bytes
performance.csv # program,condition,run,time_sec
reverse_metrics.csv # program,condition,nm_symbols,strings_count,...
binaries/{cond}/{prog} # Compiled binaries
assembly/{cond}/{prog}.s
fig_size_overhead.png
fig_perf_overhead.png
fig_reverse_metrics.png
fig_ablation.png
Generated Figures
fig_size_overhead.png— Binary size ratio vs L0 (L0-L4, grouped bar)fig_perf_overhead.png— Execution time ratio vs L0 (L0-L4, bar + error bars)fig_reverse_metrics.png— Symbols, strings, labels normalized to L0fig_ablation.png— Ablation: L3 vs L3-no-X (size + reverse metrics)
Implementation Progress
| Chapter | Feature | Status |
|---|---|---|
| 1 | Constant return (return 42;) |
Done |
| 2 | Unary operators (-, ~, !) |
Done |
| 3 | Binary arithmetic (+, -, *, /, %) |
Done |
| 4 | Relational, equality & logical operators (<, <=, >, >=, ==, !=, &&, ||) |
Done |
| 5 | Local variables & assignment (int a = 5; a = 10;) |
Done |
| 6 | if/else, ternary, compound statements (if/else, ?:, {}) |
Done |
| 7 | Compound assignment, increment/decrement, comma operator | Done |
| 8 | Loop statements (while, do-while, for) with break/continue |
Done |
| 9 | Functions (declaration, definition, calls, parameters, variadic ...) |
Done |
| 10 | File-scope variables & storage classes (static, extern) |
Done |
| 11 | Long integers (long, type checking pass, implicit conversions) |
Done |
| 12 | Unsigned integers (unsigned int, unsigned long, usual arithmetic conversions) |
Done |
| 13 | Floating-point (double, SSE instructions, XMM registers) |
Done |
| 14 | Pointers (int *, &, *, pointer comparison, null, casts) |
Done |
| 15 | Arrays & pointer arithmetic (int arr[10], arr[i], ptr + n, sizeof) |
Done |
| 16 | Characters & strings (char, unsigned char, char/string literals) |
Done |
| 17 | void type & void pointers (void, void *, malloc/free) |
Done |
| 18 | Structs (struct, member access, pointer-to-struct access) |
Done |
| 19 | TACKY IR (three-address code intermediate representation, optimization pass infrastructure) | Done |
| 20 | Register allocation (graph coloring, liveness analysis, Chaitin-Briggs) | Done |
Code Obfuscation (--fobfuscate)
After completing all 20 chapters, code obfuscation passes were implemented as an additional feature.
The --fobfuscate flag applies obfuscation passes instead of optimization.
Consists of 11 TACKY IR-level passes and 5 ASM-level passes (16 total).
Obfuscation Levels
--obf-level=N controls obfuscation intensity:
| Level | Active Passes | VM | Use Case |
|---|---|---|---|
| 1 | Constant encoding, junk code, opaque predicates | No | Light: basic obfuscation |
| 2 | Level 1 + CFF, arithmetic substitution | No | Standard: adds control flow flattening |
| 3 | Level 2 + inlining, outlining, string encryption, anti-disasm, indirect calls, register shuffle, stack frame obf, instruction substitution, OPSEC (rename + strip) | No | Full: all passes except VM (default) |
| 4 | All 16 passes (+ VM virtualization), high frequency | Yes | Maximum: VM virtualization + all passes at high frequency |
TACKY IR Level (11 passes)
- Pass 12 -- Function Inlining: Embeds callee function bodies at call sites, destroying the call graph.
Renames variables/labels with
_inline_{N}_{name}, convertsReturntoCopy + Jump. Eligibility: body <= 50 instructions, non-recursive, non-main, non-Struct return, no GetAddress on parameters.--obf-inline-freq=Ncontrols frequency (default: every 3rd eligible call) - Pass 1 -- Constant Encoding: Replaces immediate values with runtime computations
// Before: x = 42; // After: tmp_a = 6; tmp_b = 7; x = tmp_a * tmp_b; // 6 * 7 = 42 // Zero: tmp = 7; x = tmp - tmp; // a - a = 0 - Pass 2 -- Arithmetic Substitution: Expands Add/Subtract into mathematically equivalent multi-step computations,
making expression recovery by decompilers (Hex-Rays, Ghidra) difficult. 4 patterns rotated:
# Target Transform Principle 0 Add a+b->(a+K)+(b-K)Affine transform 1 Add a+b->3(a+b)-2a-2bCoefficient expansion 2 Sub a-b->(a+K)-(b+K)Affine transform 3 Sub a-b->3a-3b-(2a-2b)Coefficient expansion - Pass 3 -- Junk Code Insertion: Inserts 3 dead computation instructions every N instructions (default 4)
- Pass 4 -- Opaque Predicates: Wraps value-producing instructions with always-true conditional branches every Nth time (default 5).
Rotates 4 mathematical identities to prevent pattern-matching removal:
# Identity Principle 0 x*(x+1) % 2 == 0Product of consecutive integers is even 1 !(x^2 + 1 > 0)-> 0x^2+1 is always positive 2 (x+1)^2 - x^2 - 1 - 2x == 0Algebraic identity 3 (x^3 - x) % 3 == 0Product of 3 consecutive integers divisible by 3 - Pass 13 -- Function Outlining: Extracts straight-line code blocks (Copy/Binary/Unary only)
into new functions
_obf_outlined_{N}, flooding the binary with decoy functions. Validates: inputs <= 6, no Double/Struct/Array I/O, intermediate variables unused outside the block (scans entire function body, including loop back-edges).--obf-outline-min-block=Nsets minimum block size (default 4) - Pass 14 -- VM Virtualization (VM-Based Code Virtualization): Converts each TACKY instruction of eligible functions
into individual handlers, with bytecode arrays and handler tables in
.datasection. Same category as VMProtect/Themida commercial protectors. Applied before CFF so the VM dispatch loop itself gets flattened, achieving double indirection. Eligibility: non-main, no Double types, no float conversions, no struct ops, body >= 2 instructions.--obf-no-vm-virtualizeto disable (enabled only at Level 4)// Before: Copy(a, dst); Binary(Add, dst, b, result); Return(result); // After: // .data: bytecode[] = {0,1,2,...} handler_table[] = {&h0, &h1, &h2,...} // dispatch: fetch bytecode[pc] -> load handler_table[idx] -> jmp *handler // handler_0: Copy(a, dst); jmp dispatch // handler_1: Binary(Add, dst, b, result); jmp dispatch // handler_2: Return(result) // direct return - Pass 15 -- Library Function Obfuscation: Replaces calls to known library functions (
strlen,strcmp,strcpy,memcpy,memset,memcmp,strncmp,strncpy,strchr,strcat) with equivalent custom TACKY IR implementations (_obf_strlen, etc.). Applied before all other passes so the custom implementations get fully obfuscated by the entire pipeline, defeating IDA Pro's FLIRT signature matching.--obf-no-lib-obfuscateto disable (enabled at all levels) - Pass 5 -- Control Flow Flattening (CFF): Transforms basic blocks into a jump-table + state-encoded
dispatch loop, destroying CFG recovery in IDA Pro etc.
- Jump table: Places block label array (
PointerArrayInit) in.data, dispatches viaJumpIndirect(jmp *%rax) - State encoding: Encodes state variable with affine transform (
encoded = index * A + B, default A=37, B=0xCAFE). Decodes at dispatch (index = (encoded - 0xCAFE) / 37) before indexing the jump table
# Generated dispatch loop subl $51966, %eax # decoded = (state - 0xCAFE) / 37 cdq movl $37, %r10d idivl %r10d leaq .Lobf_jt_N(%rip), %rbx # Jump table base address imulq $8, %rax addq %rbx, %rax movq (%rax), %rax # Load jump target address jmp *%rax # Indirect jump - Jump table: Places block label array (
- Pass 6 -- String Encryption: Encrypts string literals with additive cipher (key=0x5A) and stores as
ByteArrayInitin.data. Inserts unrolled decryption code (Load -> Subtract(key) -> Store) at the beginning of main() - Pass 16 -- OPSEC Sanitization: Operational security hardening applied as the final TACKY pass:
- String Leak Detection: Scans string literals for IP addresses, URLs, file paths, debug keywords, and credential keywords.
--opsec-policy=warn(default) emits[OPSEC WARNING]to stderr;--opsec-policy=denyemits[OPSEC ERROR]and fails compilation - Symbol Renaming: Renames all internal functions to
_f{N}, global variables to_v{N}, and static constants to_c{N}. Preservesmain, external functions (e.g.printf), and.Llabels - Symbol Strip: Suppresses
.globldirectives for all symbols exceptmain(internal linkage), and runsstripon the final binary to remove the symbol table entirely.--obf-no-stripto disable,--obf-no-opsecto disable all OPSEC features (including--opsec-policyand--opsec-audit) - Binary Audit (
--opsec-audit): Post-link audit usingstringsandnmto scan the final binary for leaked IP addresses, URLs, file paths, debug keywords, and credential keywords. Also flags user-defined symbols visible vianm(toolchain-derived symbols likeframe_dummyare filtered out). Respects--opsec-policyfor fail/warn behavior. When--opsec-policy=deny, thestringscommand must be available or compilation fails (fail-closed). Only"warn"and"deny"are accepted as policy values; invalid values are rejected at argument parsing
- String Leak Detection: Scans string literals for IP addresses, URLs, file paths, debug keywords, and credential keywords.
ASM Level (5 passes, applied after register allocation + fixup)
- Pass 7 -- Anti-Disassembly: Inserts
0xE8(x86call rel32opcode) as.byteafter unconditional jumps. Linear sweep disassemblers interpret this as a 5-byte instruction, corrupting instruction boundary detectionjmp .Lobf_6 .byte 0xe8 # <- Disassembler tries to interpret as call rel32 .Lobf_6: - Pass 8 -- Indirect Calls: Converts
call functolea func(%rip), %r10; call *%r10, hindering static call graph recovery - Pass 9 -- Register Shuffle: Inserts dead
movqinstructions every N instructions, creating false dependencies via R10/R11 scratch registers. 3 patterns rotated: Dead copy, Copy chain, Round-trip - Pass 10 -- Stack Frame Obfuscation: Extends the stack frame with fake slots and inserts fake store/load operations, causing decompilers to generate fake local variables and polluting data flow analysis
- Pass 11 -- Instruction Substitution: Replaces x86-64 instructions with semantically equivalent but pattern-different sequences.
4 patterns: Add<->Sub immediate swap, Neg expansion (
not+add $1), Mov immediate split (mov (N+K); sub K)
Pass Ordering
TACKY IR pass ordering is intentionally designed:
- Library Function Obfuscation first -- custom implementations get all subsequent passes applied
- Function Inlining -- inlined code gets all subsequent passes applied
- Constant Encoding -- constants added by later passes need not be encoded
- Arithmetic Substitution -- further complicates expressions from constant encoding
- Junk Code -- doesn't alter control flow, safe before CFF
- Opaque Predicates -- adds branches that CFF will flatten
- Function Outlining -- extracts already-obfuscated code into decoy functions
- VM Virtualization -- converts functions to bytecode+VM interpreter; before CFF for double indirection
- CFF -- flattens all functions including VM dispatch loops
- String Encryption -- applied late so decryption code isn't destroyed by other passes
- OPSEC Sanitization -- applied last: renames symbols after all passes complete, then strips
.globl
ASM-level passes are applied after register allocation (order: Stack Frame Obf -> Register Shuffle -> Instruction Substitution -> Anti-Disassembly -> Indirect Calls).
Architecture
Source Code (.c)
|
v
+----------+
| Lexer | src/lex/ Tokenize
+----+-----+
v
+----------+
| Parser | src/parse/ Build AST
+----+-----+
v
+----------+
| Validate | src/typecheck/ Type checking & implicit cast insertion
+----+-----+
v
+----------+
| TACKY Gen | src/tacky/ C AST -> TACKY IR (three-address code)
+----+-----+
v
+----------+
| Optimize | src/tacky/ TACKY IR optimization passes (default)
| or | optimize.rs Algebraic simplification, constant folding, unreachable code elimination,
| | copy propagation, CSE, liveness-based DCE
| Obfuscate | obfuscate.rs TACKY obfuscation passes (--fobfuscate)
+----+-----+ Inlining, constant encoding, arith subst, junk code, opaque predicates,
v outlining, VM virtualization, CFF, string encryption
+----------+
| Codegen | src/codegen/ TACKY IR -> Asm(Pseudo)
| | generator.rs
+----+-----+
v
+----------+
| RegAlloc | src/codegen/ Liveness analysis -> interference graph -> coalescing -> graph coloring
| | regalloc.rs Pseudo -> Register/Stack(spill)
+----+-----+
v
+----------+
| Fixup | src/codegen/ Fix invalid operand combinations + prologue/epilogue generation
| | regalloc.rs
+----+-----+
v
+----------+
| ASM Obf | src/codegen/ ASM-level obfuscation (--fobfuscate)
| | mod.rs Stack frame obf, register shuffle, instruction substitution, anti-disasm, indirect calls
+----+-----+
v
+----------+
| Emitter | src/emit/ Assembly AST -> .s text output
+----+-----+
v
+----------+
| Driver | src/driver.rs Invoke gcc: .s -> executable
+----------+
Primary target: x86-64 Linux (AT&T syntax). macOS (x86_64/Rosetta 2) is best-effort and not guaranteed.
詳細な実装ノート / Detailed Implementation Notes
以下は各章の詳細な実装ノート(日本語)。
Chapter 20 の詳細
Chapter 20 ではグラフ彩色によるレジスタ割り当てを実装した。 それまで全変数をスタックに配置し、毎回 load→operate→store の3命令パターンだったのを、 変数をレジスタに直接割り当てることで効率的なコードを生成する。
- Pseudo レジスタ: コード生成で変数を
Operand::Pseudo(name)として出力し、後段で解決する// 旧: 3命令パターン(全変数スタック経由) movl -4(%rbp), %eax // load left addl -8(%rbp), %eax // operate movl %eax, -12(%rbp) // store result // 新: 2命令パターン(変数がレジスタに割り当てられる) movl %ebx, %ecx // left → dst addl %edx, %ecx // right + dst - 生存解析(Liveness Analysis): 後方データフロー解析で各命令時点の生存変数集合を計算
- ラベル/ジャンプから CFG を構築し、不動点反復で
live_in/live_afterを求める - 暗黙的な use/def を追跡(
idiv→ AX,DX、call→ 全 caller-saved レジスタ等)
- ラベル/ジャンプから CFG を構築し、不動点反復で
- 干渉グラフ: 同時に生存する変数間に辺を張る。整数グラフと XMM グラフを分離して独立に彩色
- Mov 命令の src-dst 間には辺を張らない(coalescing で活用)
- 保守的 Coalescing: Mov 辺で結ばれた非干渉ノードを合体し、冗長な Mov を除去
- Briggs 基準: Pseudo-Pseudo 合体(合体後の高次隣接数 < k で安全判定)
- George 基準: Pseudo-HardReg 合体(Pseudo の全隣接が HardReg とも干渉 or 低次で安全判定)
- Chaitin-Briggs グラフ彩色:
- Simplify: degree < k のノードをスタックに push して除去
- Potential Spill: degree が最大のノードを spill 候補としてマーク
- Select: スタックから pop して、隣接ノードが使っていない色(レジスタ)を割り当て
- 楽観的彩色: spill 候補でも色が見つかれば割り当て、見つからなければ実際に spill
- レジスタ分類(System V AMD64 ABI):
- 整数割り当て候補(12個): AX, BX, CX, DX, SI, DI, R8, R9, R12, R13, R14, R15
- Callee-saved(5個): BX, R12, R13, R14, R15(関数呼び出しをまたいで保存が必要)
- Scratch レジスタ: R10, R11(fixup 用)、XMM15(XMM の fixup 用)
- XMM 割り当て候補(15個): XMM0〜XMM14
- Fixup パス: レジスタ割り当て後に生じる無効なオペランド組み合わせを修正
movl Stack, Stack→ scratch レジスタ(R10 or XMM15)経由の2命令に展開imulのメモリ dst → R11 経由に展開Truncateのメモリ-メモリ → R10 経由に展開(難読化で spill 増加時に発生)movslq $imm, reg→movq $imm, regに変換(即値の符号拡張は不要)- 同一レジスタ間の
movを除去(noop 最適化)
- プロローグ/エピローグ自動生成: 使用した callee-saved レジスタのみ push/pop する
push %rbp # フレームポインタ保存 movq %rsp, %rbp push %rbx # callee-saved(使用した場合のみ) push %r12 # callee-saved(使用した場合のみ) subq $N, %rsp # spill スロット確保(16バイトアラインメント調整済み) - スタックオフセット補正: callee-saved レジスタの push が
%rbp直下のスタック領域を使用するため、 spill/ローカル変数のオフセットを callee-saved push 分だけ下方にシフトし、重複を防ぐ - スタックアラインメント:
callee_saved_count * 8 + alloc_size ≡ 0 (mod 16)を保証let callee_bytes = callee_count * 8; let total = callee_bytes + spill_bytes; let aligned_total = & !15; let alloc_size = aligned_total - callee_bytes; - アドレス取得対象の例外処理:
&xでアドレスを取られる変数、構造体、配列は レジスタ割り当ての対象外とし、従来通りスタックに配置する
コード難読化の詳細
TACKY IR レベル(11パス)
- Pass 12 — 関数インライン展開(Function Inlining): 呼び出し先の関数本体を呼び出し元に埋め込み、コールグラフを破壊する。
変数・ラベルを
_inline_{N}_{name}でリネームし、ReturnをCopy + Jumpに変換。 適格条件: 本体 ≤ 50 命令、非再帰、非 main、非 Struct 戻り値、パラメータの GetAddress なし。--obf-inline-freq=Nで N 回の適格呼び出しごとにインライン化(デフォルト 3)// 元: result = add(a, b); // 変換後: _inline_0_x = a; _inline_0_y = b; // _inline_0_tmp = _inline_0_x + _inline_0_y; // result = _inline_0_tmp; - Pass 1 — 定数の間接化(Constant Encoding): 即値をランタイム計算に置換
// 元: x = 42; // 変換後: tmp_a = 6; tmp_b = 7; x = tmp_a * tmp_b; // 6 * 7 = 42 // ゼロの場合: tmp = 7; x = tmp - tmp; // a - a = 0 - Pass 2 — 算術置換(Arithmetic Substitution): Add/Subtract を数学的に等価な多段計算に展開し、
デコンパイラ(Hex-Rays, Ghidra)での式復元を困難にする。4パターンをローテーション:
# 対象 変換 原理 0 Add a+b→(a+K)+(b-K)アフィン変換 1 Add a+b→3(a+b)-2a-2b係数展開 2 Sub a-b→(a+K)-(b+K)アフィン変換 3 Sub a-b→3a-3b-(2a-2b)係数展開 - Pass 3 — ジャンクコード挿入: N命令ごと(デフォルト4)に結果が使われない dead computation を3命令挿入
- Pass 4 — 不透明述語(Opaque Predicates): N回に1回(デフォルト5)、値生成命令を常に真の条件分岐で囲む。
パターンマッチによる自動除去を防ぐため4種類の数学的恒等式をローテーション:
# 恒等式 原理 0 x*(x+1) % 2 == 0連続整数の積は偶数 1 !(x² + 1 > 0)→ 0x²+1 は常に正 2 (x+1)² - x² - 1 - 2x == 0展開すると恒等式 3 (x³ - x) % 3 == 0連続3整数の積は3の倍数 - Pass 13 — 関数アウトライン化(Function Outlining): 関数内の直線コードブロック(Copy/Binary/Unary のみで構成)を
新しい関数
_obf_outlined_{N}に切り出し、偽の関数を大量に出現させる。解析者が見る関数は意味不明な断片となる。 入力変数 ≤ 6、Double/Struct/Array 型の入出力なし、中間変数がブロック外(関数全体)で未使用であることを検証。--obf-outline-min-block=Nで最小ブロックサイズを設定(デフォルト 4)// 元: tmp = a + b; result = tmp * c; // 変換後: result = _obf_outlined_0(a, b, c); // 新関数: int _obf_outlined_0(int p0, int p1, int p2) { // t0 = p0 + p1; t1 = t0 * p2; return t1; } - Pass 14 — VM仮想化(VM-Based Code Virtualization): 適格な関数の各TACKY命令を個別の
ハンドラに配置し、
.dataセクションにバイトコード配列とハンドラテーブルを生成する。 VMProtect/Themida 等の商用プロテクタと同カテゴリの技術で、静的解析でのCFG復元を極めて困難にする。 CFF の前に適用することで、VMディスパッチループ自体が CFF で平坦化され二重の間接化が実現される。 適格条件: 非 main、Double 型なし、浮動小数点変換なし、構造体操作なし、本体 ≥ 2 命令。--obf-no-vm-virtualizeで無効化可能(Level 4 のみデフォルト有効)// 変換前: Copy(a, dst); Binary(Add, dst, b, result); Return(result); // 変換後: // .data: bytecode[] = {0,1,2,...} handler_table[] = {&h0, &h1, &h2,...} // dispatch: fetch bytecode[pc] → load handler_table[idx] → jmp *handler // handler_0: Copy(a, dst); jmp dispatch // handler_1: Binary(Add, dst, b, result); jmp dispatch // handler_2: Return(result) // 直接リターン - Pass 15 — ライブラリ関数難読化(Library Function Obfuscation):
strlen,strcmp,strcpy,memcpy,memset,memcmp,strncmp,strncpy,strchr,strcatの既知ライブラリ関数の 呼び出しを等価な自前実装(_obf_strlen等)に差し替える。 全パスの最初に適用することで、自前実装が後続の全パイプラインで難読化され、 IDA Pro の FLIRT シグネチャマッチングを無効化する。--obf-no-lib-obfuscateで無効化可能(全レベルでデフォルト有効) - Pass 5 — 制御フロー平坦化(CFF): 関数内の基本ブロックをジャンプテーブル + 状態エンコードの
dispatch ループに変換し、IDA Pro 等の CFG 復元を破壊する
- ジャンプテーブル:
.dataセクションにブロックラベルの配列(PointerArrayInit)を配置し、JumpIndirect(jmp *%rax)で分岐。連続比較のif (state == i) goto block_iよりも 静的解析での復元が困難 - 状態エンコード: state 変数をアフィン変換(
encoded = index * A + B、デフォルト A=37, B=0xCAFE)で符号化。 dispatch でデコード(index = (encoded - 0xCAFE) / 37)してからジャンプテーブルを索引。 自動的なステートマシン復元を妨害する JumpIndirectのpossible_targets: 間接ジャンプは動的ターゲットだが、レジスタ割り当ての 生存解析で正しい CFG 後続ブロック情報が必要。ジャンプテーブルの全エントリラベルをpossible_targetsとして保持し、jump_targets()で返すことで解決
# dispatch ループ(生成されるアセンブリ) subl $51966, %eax # decoded = (state - 0xCAFE) / 37 cdq movl $37, %r10d idivl %r10d leaq .Lobf_jt_N(%rip), %rbx # ジャンプテーブルのベースアドレス imulq $8, %rax addq %rbx, %rax movq (%rax), %rax # ジャンプ先アドレスをロード jmp *%rax # 間接ジャンプ - ジャンプテーブル:
- Pass 6 — 文字列暗号化: 文字列リテラルを加算暗号化(key=0x5A)して
.dataにByteArrayInitとして配置。 main() の先頭にアンロール復号コード(Load → Subtract(key) → Store)を挿入- Pass 6 は Pass 1〜5 の後に適用する。復号コードが CFF 等で破壊されるのを防ぐため
- Pass 16 — OPSEC 衛生化(OPSEC Sanitization): 全パスの最後に適用する運用セキュリティ強化:
- 文字列リーク検出: 文字列リテラル中の IP アドレス、URL、ファイルパス、デバッグキーワード、資格情報キーワードを検出。
--opsec-policy=warn(デフォルト)で[OPSEC WARNING]を stderr に出力、--opsec-policy=denyで[OPSEC ERROR]を出力しコンパイルを失敗させる(fail-closed)。--obf-no-opsec-warnで検出自体を無効化可能(deny ポリシーより優先) - シンボルリネーム: 内部関数を
_f{N}、グローバル変数を_v{N}、静的定数を_c{N}にリネーム。main、外部関数(printf等)、.Lラベルは保持。nmやstringsでの関数名特定を防止 - シンボル Strip:
main以外の全関数・変数の.globlディレクティブを抑制(internal linkage 化)し、 フルコンパイル時にstripコマンドを自動実行してバイナリからシンボルテーブルを完全除去。--obf-no-stripで無効化可能(stripコマンドが未インストールでも警告のみでコンパイルは成功) - バイナリ監査(
--opsec-audit): リンク後のバイナリに対してstringsとnmを実行し、 IP アドレス・URL・ファイルパス・デバッグキーワード・資格情報キーワードの漏洩を検出。nmでユーザー定義シンボルの残存もフラグする(informational、frame_dummy等のツールチェイン由来シンボルは除外)。--opsec-policyに従い warn/deny を切り替え。--opsec-policy=deny時にstringsコマンドが未インストールの場合はコンパイル失敗(fail-closed)。warn時はstrings/nm未インストールでも警告のみでスキップ。--opsec-policyにはwarnとdenyのみ指定可能(不正値は引数パース時にリジェクト)
- Level 3/4 でデフォルト有効、Level 1/2 ではリネーム無効(警告のみ Level 2 で有効)
--obf-no-opsecで OPSEC 衛生化全体を無効化(--opsec-policy/--opsec-audit含む全 OPSEC 機能を上書き)
- 文字列リーク検出: 文字列リテラル中の IP アドレス、URL、ファイルパス、デバッグキーワード、資格情報キーワードを検出。
ASM レベル(5パス、レジスタ割り当て+fixup 後に適用)
- Pass 7 — 反逆アセンブリ(Anti-Disassembly): 無条件ジャンプ(
Jmp,JmpIndirect)の直後に0xE8(x86 のcall rel32オペコード)を.byteとして挿入。リニアスイープ型逆アセンブラが 5バイト命令として解釈しようとするため、後続命令の命令境界認識が破壊されるjmp .Lobf_6 .byte 0xe8 # ← 逆アセンブラはここから call rel32 として解釈を試みる .Lobf_6: - Pass 8 — 関数呼び出しの間接化(Indirect Calls):
call funcをlea func(%rip), %r10; call *%r10に変換。静的解析でのコールグラフ復元を妨害する - Pass 9 — レジスタシャッフル(Register Shuffle): dead な
movq命令を N 命令ごとに挿入し、 R10/R11 scratch レジスタへの偽コピーでデータフローグラフに偽の依存関係を生成する。 3パターンをローテーション: Dead copy(1命令)、Copy chain(2命令)、Round-trip(2命令)movq %rcx, %r10 # Dead copy: ライブなレジスタの値を scratch にコピー movq %rax, %r10 # Copy chain: レジスタ値を scratch1 に movq %r10, %r11 # scratch1 → scratch2 に伝播 movq %rdx, %r10 # Round-trip: scratch に退避 movq %r10, %rdx # scratch から復元(noop) - Pass 10 — スタックフレーム難読化(Stack Frame Obfuscation): スタックフレームを拡張して偽のスタックスロットを追加し、
偽の store/load 操作を N 命令ごとに挿入する。デコンパイラが偽スロットを「ローカル変数」として認識し、
復元される変数数が増加する。偽の store/load パターンが実際の変数アクセスに紛れ、データフロー解析を妨害する
subq $64, %rsp # AllocateStack が拡張される(偽スロット分追加) movl %ecx, -48(%rbp) # 偽の int store → デコンパイラが int 型の偽変数を生成 movq %rax, -56(%rbp) # 偽の quad store → long/pointer 型の偽変数を生成 movl -48(%rbp), %r10d # 偽の load → 偽変数の「使用」を生成 - Pass 11 — 命令置換(Instruction Substitution): x86-64 命令を意味的に等価だがパターンの異なる命令列に置換し、
デコンパイラ・逆アセンブラのパターンマッチングを妨害する。4パターンをローテーション:
Add→Sub 即値スワップ、Sub→Add 即値スワップ、Neg 展開(
not+add $1)、Mov 即値分割(mov (N+K); sub K)# Add → Sub 即値スワップ addl $42, %eax → subl $-42, %eax # Neg 展開(二の補数: -x = ~x + 1) negl %edx → notl %edx; addl $1, %edx # Mov 即値分割 movl $100, %eax → movl $142, %eax; subl $42, %eax
パス適用順序の設計
TACKY IR パスの適用順序は意図的に設計されている:
- ライブラリ関数難読化が最初 → 自前実装が後続の全パスで難読化される(FLIRT 対策)
- 関数インライン展開 → インラインされたコードに後続の全パスが適用される
- 定数の間接化 → 後続パスが追加する定数はエンコード不要
- 算術置換 → 定数間接化で展開された式をさらに複雑にしつつ、後続パスでさらにノイズを加える
- ジャンクコード → 制御フローを変えないので CFF の解析に影響しない
- 不透明述語 → 分岐を追加。CFF がこれも含めて平坦化する
- 関数アウトライン化 → Pass 1-4 で難読化済みのコードが切り出され、解析者が見る関数は意味不明な断片
- VM仮想化 → 適格な関数をバイトコード+VMインタプリタに変換。CFF の前に適用することで二重間接化
- CFF → VMディスパッチループを含む全関数に適用。コード+データの相関解析が必要な二重の間接化
- 文字列暗号化 → 復号コードが他のパスで壊されないよう終盤に適用
- OPSEC 衛生化 → 全パスの最後に適用: リネーム・
.globl抑制は他のパスが完了してから実行
ASM レベルパスはレジスタ割り当て後に適用する(適用順: スタックフレーム難読化 → レジスタシャッフル → 命令置換 → 反逆アセンブリ → 間接コール):
- スタックフレーム難読化はフレーム構造を変更するため最初に適用。後続のレジスタシャッフルが挿入する dead mov が偽スタック操作の近傍に散在することで解析をさらに困難にする
- レジスタシャッフルは dead mov を挿入し R10/R11 scratch を使用。fixup シーケンスの中間を避けるため安全
- 命令置換はシャッフル後に適用。シャッフルで挿入された dead mov の近傍で命令置換が行われることでデータフロー解析をさらに困難にする
- 反逆アセンブリはジャンプ命令の位置を変えないため安全
- 間接コール変換は R10(caller-saved scratch)を使用し、Call の直前に挿入するため安全
可変長引数関数の定義サポートの詳細
va_list/va_start/va_arg/va_end によるユーザー定義可変長引数関数を実装した。
System V AMD64 ABI に完全準拠し、#include <stdarg.h> なしで使用可能(コンパイラ組み込み)。
対応する引数型
- 整数型:
int,long,unsigned int,unsigned long(GP レジスタ経由) - ポインタ型:
int *,char *等(GP レジスタ経由) - 浮動小数点型:
double(XMM レジスタ経由) - 7引数以上のスタック渡し: レジスタを超過した引数は
overflow_arg_areaから取得
System V AMD64 ABI の va_list 構造体
va_list (24 bytes, align 8):
[0..4] gp_offset 次の GP レジスタ引数のオフセット (0〜48)
[4..8] fp_offset 次の XMM レジスタ引数のオフセット (48〜176)
[8..16] overflow_arg_area スタック渡し引数へのポインタ
[16..24] reg_save_area レジスタ保存領域へのポインタ
レジスタ保存領域 (176 bytes)
可変長関数の入口で、パラメータ受け取り前に全引数レジスタを保存:
offset 0-47: GP レジスタ (rdi, rsi, rdx, rcx, r8, r9) × 8B = 48B
offset 48-175: XMM レジスタ (xmm0-xmm7) × 16B = 128B
va_arg の分岐ロジック
整数型:
if gp_offset < 48:
val = reg_save_area[gp_offset] // レジスタ保存領域から取得
gp_offset += 8
else:
val = *overflow_arg_area // スタックから取得
overflow_arg_area += 8
浮動小数点型:
if fp_offset < 176:
val = reg_save_area[fp_offset] // XMM 保存領域から取得
fp_offset += 16
else:
val = *overflow_arg_area // スタックから取得
overflow_arg_area += 8
実装範囲
- AST:
Type::VaList(24B, align 8)、Expr::VaStart/VaArg/VaEnd - パーサー:
va_listを型トークンとして認識(ブロック内宣言・for 初期化部・キャスト・sizeof で使用可能)。va_start/va_arg/va_endを特殊識別子としてパース - 型チェッカー: 引数が
va_list型であることを検証 - TACKY IR:
VaStart { gp_offset_init, fp_offset_init }、VaArg { arg_type }、VaEnd(no-op) - コード生成: va_arg の分岐ロジックをインライン展開(Cmp + JmpCC + Mov + Lea + Binary)
- レジスタ割り当て:
VaList型変数を強制スタック配置。__va_reg_saveはArray(UChar, 176)として管理 - スタックフレーム修正:
insert_prologue_epilogueで全Stack()オペランドをスキャンし、 強制スタック変数を含む正確なフレームサイズを計算(scan_min_stack_offset)
typedef サポートの詳細
typedef による型エイリアス定義を実装した:
- 基本型エイリアス:
typedef int myint;—myintをintの別名として使用可能 - ポインタ typedef:
typedef int *pint;—pintはint *のエイリアス - 構造体 typedef:
typedef struct point { int x; int y; } Point;— 構造体タグと型名を同時に定義 - 配列 typedef:
typedef int arr3[3];— 固定長配列の型名を定義 - ネスト typedef:
typedef pint *ppint;— typedef 名を使った型を再度 typedef 可能 - unsigned 型:
typedef unsigned long usize_t;— 複合型指定子のエイリアス - グローバル/ブロックスコープ: トップレベルおよび関数本体内の両方で typedef 宣言可能
- 関数パラメータ/戻り値: typedef 名を関数シグネチャで使用可能
- キャスト/sizeof:
(myint)expr,sizeof(myint)で typedef 名を認識 - カンマ区切り:
typedef int myint, *pint;で複数の型名を一度に定義
実装方式:
- パーサーに
typedef_names: HashMap<String, Type>テーブルを追加(struct_tagsと同様のフラット方式) parse_typedef()がtypedefキーワードを消費し、ベース型 + 宣言子をパースして型名テーブルに登録parse_type_specifier()で識別子が typedef 名テーブルに存在すればその型として解決- AST には
TopLevelDecl::Typedef/BlockItem::Typedefノードを追加(型チェッカー・コード生成ではスキップ) - キャスト式・sizeof 式の先読みで typedef 名を型キーワードとして判定
Chapter 19 の詳細
Chapter 19 では TACKY IR(三番地コード中間表現)を導入した:
- TACKY IR: C の AST と x86-64 アセンブリの中間に位置する三番地コード形式の IR
// C: int r = a + b * c; // TACKY: tmp.0 = b * c r = a + tmp.0 - コンパイルパイプライン変更:
C AST → TACKY IR → Assembly ASTの2段階に分離 - 最適化パス基盤: TACKY IR 上で6パス構成の最適化パイプライン(代数的簡略化・定数畳み込み・到達不能コード除去・コピー伝播・共通部分式除去・生存解析ベース死コード除去)を実行可能にする
Chapter 18 の詳細
Chapter 18 では構造体を実装した:
- 構造体型:
structの定義とメンバアクセス; struct Point p; p.x = 10; p.y = 20; return p.x + p.y; // 30 - ポインタ経由のメンバアクセス:
ptr->member((*ptr).memberの糖衣構文) MemoryOffset(Reg, i32)オペランド: レジスタ+オフセット間接アドレッシングで構造体メンバにアクセスCopyToOffset/CopyFromOffset: 構造体コピーのための TACKY 命令
Chapter 17 の詳細
Chapter 17 では以下の機能を追加した:
void型: 関数の戻り値型・キャスト先として使用可能な不完全型void intvoid *(void ポインタ): 任意のポインタ型と暗黙的に相互変換可能な汎用ポインタvoid *; void ; int(void)exprキャスト: 式の値を捨てる(副作用のみ実行)- void ポインタの暗黙変換: 代入・関数引数・return・三項演算子で
void *↔ 任意のポインタ型 - 不完全型チェック:
void x;,sizeof(void), void ポインタ算術等をコンパイルエラーとして検出
Chapter 15 の詳細
Chapter 15 では以下の機能を追加した:
- 配列型:
int arr[10],long arr[5]等の固定長配列宣言 - 配列添字:
arr[i]はパーサーで*(arr + i)に脱糖(desugaring) - 配列→ポインタ減衰(decay): 式中の配列は自動的にポインタに変換
- ポインタ算術: ポインタ加減算は要素サイズ分スケーリングされる
- ポインタ減算: 同じ型のポインタ同士の差分は要素数で返される
- ポインタ比較の拡張:
<,<=,>,>=を同じポインタ型同士で許可 - ポインタ増分/減分:
++,--,+=,-=が要素サイズ分移動 sizeof演算子: 型チェック時に定数(unsigned long)に解決- 配列パラメータ: 関数パラメータの
int arr[]はint *arrに変換 - グローバル配列:
.bssセクションにゼロ初期化で配置 - 配列初期化子リスト:
int arr[3] = {1, 2, 3};形式の初期化に対応- ローカル配列:
CopyToOffset命令列で各要素を書き込み - グローバル配列・
staticローカル配列:.dataセクションに静的初期値を出力 - 部分初期化: 明示要素数 < 配列サイズの場合、残りをゼロ初期化
- 式初期化子: ローカル配列では
int x = 5; int arr[2] = {x, x+1};が可能
- ローカル配列:
- カンマ区切り複数宣言:
int a = 1, b = 2, *p = &a;形式のカンマ区切り宣言に対応- ローカル変数・グローバル変数・for 初期化部すべてで使用可能
- ポインタ・配列混在:
int x = 1, *p, arr[3] = {2, 3, 4};— 各宣言子に同じベース型を渡し、parse_declarator()がポインタ/配列を個別に付加 parse_declaration()がVec<Declaration>を返し、parse_block_item()がVec<BlockItem>に展開
Chapter 14 の詳細
Chapter 14 では以下の機能を追加した:
- ポインタ型:
int *,double *,int **等の多段ポインタに対応 - アドレス演算子 (
&): 変数のアドレスを取得 - 間接参照演算子 (
*): ポインタ経由の読み書き - ポインタの関数引数・戻り値: ポインタを関数間で受け渡し
- ポインタ比較:
==/!=で同じポインタ型同士を比較 - null ポインタ: 整数定数 0 をポインタに代入可能。条件式で真偽判定
- ポインタ ↔ 整数キャスト: 明示的キャストで相互変換
- 宣言子パーサー:
int *x,int **pp等のポインタ宣言構文を解析 - 左辺値の一般化:
*ptr = val形式の代入に対応
Chapter 13 の詳細
Chapter 13 では以下の機能を追加した:
double型: IEEE 754 倍精度浮動小数点数に対応- 浮動小数点リテラル: 小数点・指数表記に対応
- SSE 命令による浮動小数点演算:
addsd,subsd,mulsd,divsd - 単項演算子: 符号反転(
xorpd)、論理否定(comisd) - 比較演算:
comisd命令 + unsigned 条件コード - 型変換: 整数 ↔
doubleの暗黙変換(cvtsi2sd,cvttsd2si) - 混合引数の関数呼出規約: 整数レジスタと XMM レジスタを独立カウント
double定数プール:.rodataセクションにビットパターンを配置
Chapter 12 の詳細
Chapter 12 では以下の機能を追加した:
unsigned int/unsigned long型: 符号なし整数型に対応- 符号なしリテラル:
U/u,UL/ul/LU/luサフィックス - 通常算術変換: 4 型の暗黙変換
- 符号なし除算/剰余:
div命令(idivと区別) - 符号なし比較:
seta/setae/setb/setbe条件コード - ゼロ拡張:
unsigned int→unsigned longにmovl(上位32ビット自動クリア)
Chapter 11 の詳細
long型: 64ビット符号付き整数に対応- 型検査パス(Validate)の導入: パースとコード生成の間に型検査を挿入
- 暗黙的型変換:
int↔longの自動変換(Castノード挿入) - 型に応じたコード生成:
movl/addl(32bit) vsmovq/addq(64bit) の選択 - 符号拡張/切り詰め命令:
movslq(int→long),movl(long→int truncate)
Chapter 10 の詳細
- グローバル変数: ファイルスコープの変数宣言に対応(
.data/.bssセクションに配置) staticローカル変数: 関数呼び出し間で値を保持するstatic関数: 内部リンケージ(.globlを出力しない)extern宣言: 外部リンケージの変数参照
Chapter 9 の詳細
- 関数定義・呼び出し: 複数関数のプログラムに対応
- 関数プロトタイプ(前方宣言): 定義前に宣言できる
- 再帰: 関数の再帰呼び出しに対応
- 最大6引数のレジスタ渡し: System V AMD64 ABI 準拠
- 7引数以上のスタック渡し: 16バイトアラインメント対応
- 可変長引数関数の呼び出し:
printf,sprintf等の可変長引数関数を呼び出せる...トークンのレキシング、パラメータリスト末尾の, ...パース- 可変長関数呼び出し時に
%alに XMM レジスタ引数数をセット(System V ABI 準拠) - デフォルト引数昇格(
char→int)
int ; int - 可変長引数関数の定義:
va_list/va_start/va_arg/va_endによるユーザー定義の可変長引数関数- System V AMD64 ABI 準拠の
va_list構造体(24バイト:gp_offset,fp_offset,overflow_arg_area,reg_save_area) - 関数入口で全引数レジスタ(GP 6個 + XMM 8個)を 176バイトのレジスタ保存領域に退避
va_argで整数型(int,long, ポインタ)と浮動小数点型(double)の両方に対応- レジスタ渡し引数(GP:
gp_offset < 48、XMM:fp_offset < 176)とスタック渡し引数(overflow_arg_area)の自動切り替え #include <stdarg.h>不要(コンパイラ組み込み)
int int // 42 - System V AMD64 ABI 準拠の
Chapter 8 の詳細
- while / do-while / for ループ
- break / continue
Chapter 7 の詳細
- 複合代入演算子:
+=,-=,*=,/=,%= - 前置/後置インクリメント/デクリメント:
++a,a++,--a,a-- - カンマ演算子:
expr1, expr2
Future Work
書籍 "Writing a C Compiler" の全20章を完了。今後の改善候補:
最適化
- Coalescing(コピー合体):
Mov a, bで a と b が干渉しなければ合体し、Mov を除去する。Briggs 基準(Pseudo-Pseudo)と George 基準(Pseudo-HardReg)で安全性を判定 - TACKY IR 上の最適化パス強化: 6パス構成の最適化パイプライン(収束まで最大10回反復)
- 代数的簡略化(Algebraic Simplifications):
x+0→x,x*1→x,x*0→0,x-x→0,x/1→x,x%1→0等の恒等式を簡略化 - 定数畳み込み(Constant Folding): コンパイル時に定数式を評価
- 到達不能コード除去(Unreachable Code Elimination): CFG の BFS で到達不能な基本ブロックを除去
- コピー伝播(Copy Propagation):
dst = srcのコピーを追跡し、後続のdstの使用をsrcに置換 - 共通部分式除去(CSE): 基本ブロック内で同一計算を検出し、Copy に置換
- 生存解析ベース死コード除去(Liveness DCE): 逆方向データフロー解析で生存していない変数への書き込みを除去
- 代数的簡略化(Algebraic Simplifications):
- ポインタ経由の書き込みを考慮したコピー伝播: Store 時にアドレス取得変数のコピーを無効化
言語機能の拡充
- 可変長引数関数の呼び出し:
printf(fmt, ...)等の外部可変長引数関数を呼び出し可能に - 可変長引数関数の定義:
va_list/va_start/va_arg/va_end(System V AMD64 ABI 準拠、int/long/double/ポインタ対応) - 配列初期化子リスト:
int arr[3] = {1, 2, 3}; - カンマ区切り複数宣言:
int a = 1, b = 2, c = 3; - switch 文:
switch/case/default(fall-through、ネスト、enum 定数 case ラベル対応) - enum 型:
enum Color { RED, GREEN, BLUE };(明示値、負値、typedef enum、無名 enum 対応) - typedef: 型エイリアスの定義(
typedef int myint;,typedef struct { ... } Point;, ポインタ・配列・ネスト対応) - プリプロセッサ:
#include,#define,#ifdef等
コード難読化(Anti-Reverse Engineering)
コンパイラレベルでの難読化変換。--fobfuscate フラグで有効化し、TACKY IR + ASM レベルの計16パスを適用する。
TACKY IR レベル(11パス):
- 定数の間接化(Constant Encoding)
- 算術置換(Arithmetic Substitution)
- ジャンクコード挿入
- 不透明述語(Opaque Predicates)多様化
- 制御フロー平坦化(CFF)+ ジャンプテーブル + 状態エンコード
- 文字列暗号化
- VM仮想化(VM-Based Code Virtualization)
- ライブラリ関数難読化(Library Function Obfuscation)
- OPSEC 衛生化(シンボルリネーム + 文字列リーク警告 + シンボル Strip + fail-closed ポリシー + バイナリ監査)
ASM レベル(5パス):
- 反逆アセンブリ(Anti-Disassembly)
- 関数呼び出しの間接化(Indirect Calls)
- レジスタシャッフル(Register Shuffle)
- スタックフレーム難読化(Stack Frame Obfuscation)
- 命令置換(Instruction Substitution)
関数境界攪乱(2パス):
- 関数インライン展開(Function Inlining)
- 関数アウトライン化(Function Outlining)
パラメータ化:
- 難易度レベル制御(
--obf-level=1..4) - 個別パス制御:
--obf-no-cff,--obf-no-strings,--obf-no-vm-virtualize,--obf-no-opsec,--obf-no-strip等で各パスを個別に無効化 - 頻度パラメータ:
--obf-junk-freq=N,--obf-pred-freq=N等で頻度を調整 - OPSEC ポリシー制御:
--opsec-policy=warn|denyで違反時の動作を制御、--opsec-auditでリンク後バイナリの監査
ベンチマーク・評価
- 難読化ベンチマークスイート: 20本のCプログラム × 5難読化レベル = 100バイナリの自動生成・検証
- 評価インフラ: 11条件(L0-L4 + 6アブレーション)× 20ベンチの自動評価パイプライン(正しさ・サイズ・実行時間・逆解析指標・可視化)
- デオブフスケーター定量評価: D-810, SATURN 等での復元成功率測定
コード品質
- コンパイラ警告の解消: 未使用変数・インポートを整理し0件に(19件 → 0件)
- E2E テストスイートの充実: 各章の機能を網羅する統合テストの追加
License
MIT License. See LICENSE.