ferrugocc 0.4.0

An experimental C compiler and obfuscating compiler written in Rust, targeting x86_64 SysV ABI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
//! コンパイラドライバー
//!
//! コンパイルパイプラインの各ステージを順に実行し、
//! 最終的に gcc を呼び出して実行可能バイナリを生成する。
//!
//! # パイプライン
//! ```text
//! source.c → [Preprocess] → [Lex] → [Parse] → [Validate] → [TackyGen]
//!          → [Optimize or Obfuscate] → [Codegen] → [Emit] → source.s → [gcc] → binary
//! ```
//!
//! - デフォルト: TACKY IR 最適化パス(定数畳み込み・コピー伝播・不要コード除去)
//! - `--fobfuscate`: 難読化パス(TACKY: 関数インライン展開・定数間接化・算術置換・ジャンクコード・不透明述語・関数アウトライン化・CFF・文字列暗号化・OPSEC衛生化、
//!   ASM: スタックフレーム難読化・レジスタシャッフル・命令置換・反逆アセンブリ・間接呼出)
//! - `--obf-level=N`: 難読化強度レベル(1=軽量, 2=標準, 3=全パス有効, 4=最大)
//!
//! `--lex`, `--parse`, `--validate`, `--tacky`, `--codegen`, `-S` フラグで途中のステージで停止できる。
//! これは本のテストスイートとの互換性のために必要。

use std::path::{Path, PathBuf};
use std::process::Command;

use crate::codegen;
use crate::emit;
use crate::error::{CompileError, Result};
use crate::lex;
use crate::obfuscation::{ObfuscationConfig, OpsecPolicy};
use crate::parse;
use crate::tacky;
use crate::typecheck;

/// 前処理モード。
///
/// ソースファイルを字句解析に渡す前にどのように前処理するかを指定する。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum PreprocessMode {
    /// 前処理をスキップ(ソースをそのまま字句解析に渡す)
    None,
    /// 外部プリプロセッサ(gcc -E -P)を使用
    External,
}

/// コンパイルをどのステージまで実行するかを指定する列挙型。
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Stage {
    /// 字句解析のみ(トークン化が成功すれば OK)
    Lex,
    /// 構文解析まで(AST 構築が成功すれば OK)
    Parse,
    /// 型検査まで
    Validate,
    /// TACKY IR 生成まで
    Tacky,
    /// コード生成まで(アセンブリ AST 構築が成功すれば OK)
    Codegen,
    /// アセンブリ出力まで(.s ファイルを書き出す)
    EmitAsm,
    /// フルコンパイル(gcc でバイナリまで生成)
    Full,
}

/// 外部プリプロセッサ(gcc -E -P)を実行する。
///
/// ソースファイルのディレクトリを `-I` に含めることで、
/// ローカルの `#include` を解決できるようにする。
fn preprocess_external(
    source_path: &Path,
    pp_defines: &[String],
    pp_undefs: &[String],
) -> Result<String> {
    let source_dir = source_path.parent().unwrap_or(Path::new("."));
    let mut cmd = Command::new("gcc");
    cmd.arg("-E").arg("-P").arg("-I").arg(source_dir);
    for undef in pp_undefs {
        cmd.arg(format!("-U{undef}"));
    }
    for def in pp_defines {
        cmd.arg(format!("-D{def}"));
    }
    let output = cmd.arg(source_path).output().map_err(|e| {
        CompileError::ExternalToolError(format!("failed to run preprocessor (gcc -E): {e}"))
    })?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(CompileError::ExternalToolError(format!(
            "preprocessing failed:\n{stderr}"
        )));
    }

    String::from_utf8(output.stdout).map_err(|e| {
        CompileError::ExternalToolError(format!("preprocessor output is not valid UTF-8: {e}"))
    })
}

fn asm_output_path(source_path: &Path) -> PathBuf {
    std::env::var_os("FERRUGOCC_ASM_OUTPUT")
        .map(PathBuf::from)
        .unwrap_or_else(|| source_path.with_extension("s"))
}

/// 単一の .c ファイルをコンパイルパイプラインで処理する。
/// `stage` で停止。EmitAsm/Full まで進んだ場合は .s ファイルのパスを返す。
fn compile_one(
    source_path: &Path,
    stage: Stage,
    obf_config: &Option<ObfuscationConfig>,
    preprocess: PreprocessMode,
    pp_defines: &[String],
    pp_undefs: &[String],
) -> Result<Option<PathBuf>> {
    let source = match preprocess {
        PreprocessMode::None => std::fs::read_to_string(source_path)?,
        PreprocessMode::External => preprocess_external(source_path, pp_defines, pp_undefs)?,
    };

    let tokens = lex::lex(&source)?;
    if stage == Stage::Lex {
        return Ok(None);
    }

    let mut program = parse::parse(&tokens)?;
    if stage == Stage::Parse {
        return Ok(None);
    }

    typecheck::typecheck(&mut program)?;
    if stage == Stage::Validate {
        return Ok(None);
    }

    let tacky_program = tacky::generate_tacky(&program)?;
    if stage == Stage::Tacky {
        return Ok(None);
    }

    let tacky_program = if let Some(config) = obf_config.as_ref() {
        tacky::obfuscate(tacky_program, config)?
    } else {
        tacky::optimize(tacky_program)
    };

    let asm_program = codegen::generate(&tacky_program, obf_config.as_ref())?;
    if stage == Stage::Codegen {
        return Ok(None);
    }

    let asm_text = emit::emit(&asm_program)?;
    let asm_path = asm_output_path(source_path);
    std::fs::write(&asm_path, &asm_text)?;
    Ok(Some(asm_path))
}

/// .s → .o にアセンブルする(gcc -c)
fn assemble_to_object(asm_path: &Path, obj_path: &Path) -> Result<()> {
    let mut cmd = Command::new("gcc");
    cmd.arg("-c").arg(asm_path).arg("-o").arg(obj_path);
    if cfg!(target_os = "linux") {
        cmd.arg("-no-pie");
    }
    let status = cmd.status()?;
    if !status.success() {
        return Err(CompileError::ExternalToolError(format!(
            "gcc -c failed for {}",
            asm_path.display()
        )));
    }
    Ok(())
}

/// 複数の .o をリンクして実行ファイルを生成する
fn link_objects(objects: &[PathBuf], output_path: &Path) -> Result<()> {
    let mut cmd = Command::new("gcc");
    for obj in objects {
        cmd.arg(obj);
    }
    cmd.arg("-o").arg(output_path);
    if cfg!(target_os = "linux") {
        cmd.arg("-no-pie");
    }
    let status = cmd.status()?;
    if !status.success() {
        return Err(CompileError::ExternalToolError(format!(
            "gcc link failed (exit {})",
            status
        )));
    }
    Ok(())
}

/// 複数ファイル対応のコンパイルパイプライン。
///
/// 各 .c ファイルを独立にコンパイルし、最後に gcc でリンクする。
/// .o ファイルはそのままリンカに渡す。
#[allow(clippy::too_many_arguments)]
pub fn run_multi(
    sources: &[PathBuf],
    stage: Stage,
    obf_config: Option<ObfuscationConfig>,
    preprocess: PreprocessMode,
    pp_defines: &[String],
    pp_undefs: &[String],
    compile_only: bool,
    output: Option<&Path>,
) -> Result<()> {
    // 入力ファイルの分類
    let mut c_files: Vec<&Path> = Vec::new();
    let mut o_files: Vec<PathBuf> = Vec::new();
    for source in sources {
        match source.extension().and_then(|e| e.to_str()) {
            Some("c") | Some("h") => c_files.push(source),
            Some("o") => o_files.push(source.clone()),
            _ => {
                return Err(CompileError::ExternalToolError(format!(
                    "unrecognized file type: {}",
                    source.display()
                )));
            }
        }
    }

    if compile_only && output.is_some() && c_files.len() > 1 {
        return Err(CompileError::ExternalToolError(
            "-o cannot be used with -c and multiple source files".to_string(),
        ));
    }

    // Multi-file または -c(リンク用 .o 生成)の場合、global シンボルの OPSEC リネームを抑制
    let mut obf_config = obf_config;
    if ((c_files.len() + o_files.len()) > 1 || compile_only)
        && let Some(ref mut config) = obf_config
    {
        config.preserve_globals = true;
    }

    // 各 .c ファイルをコンパイル
    let mut asm_paths: Vec<PathBuf> = Vec::new();
    for c_file in &c_files {
        if let Some(asm_path) = compile_one(
            c_file,
            stage,
            &obf_config,
            preprocess,
            pp_defines,
            pp_undefs,
        )? {
            asm_paths.push(asm_path);
        }
    }

    // EmitAsm 以前のステージでは .s も生成しない
    if stage < Stage::EmitAsm {
        return Ok(());
    }
    // -S: .s ファイル出力で停止
    if stage == Stage::EmitAsm {
        return Ok(());
    }

    // -c: .s → .o にアセンブルして停止(リンクしない)
    if compile_only {
        for asm_path in &asm_paths {
            let obj_path = if let Some(out) = output {
                out.to_path_buf()
            } else {
                asm_path.with_extension("o")
            };
            assemble_to_object(asm_path, &obj_path)?;
            let _ = std::fs::remove_file(asm_path);
        }
        return Ok(());
    }

    // Full: .s → .o → リンク
    let mut all_objects: Vec<PathBuf> = Vec::new();
    for asm_path in &asm_paths {
        let obj_path = asm_path.with_extension("o");
        assemble_to_object(asm_path, &obj_path)?;
        let _ = std::fs::remove_file(asm_path);
        all_objects.push(obj_path);
    }
    all_objects.extend(o_files);

    let output_path = if let Some(out) = output {
        out.to_path_buf()
    } else if c_files.len() == 1 && all_objects.len() == 1 {
        // 後方互換: 単一 .c → 拡張子なしバイナリ
        c_files[0].with_extension("")
    } else {
        PathBuf::from("a.out")
    };

    link_objects(&all_objects, &output_path)?;

    // 生成した .o を掃除(引数で渡された .o は残す)
    for asm_path in &asm_paths {
        let obj_path = asm_path.with_extension("o");
        let _ = std::fs::remove_file(&obj_path);
    }

    // OPSEC strip
    if let Some(config) = &obf_config
        && config.opsec_strip
    {
        let strip_status = Command::new("strip").arg(&output_path).status();
        match strip_status {
            Ok(s) if s.success() => {}
            _ => {
                eprintln!("[OPSEC] warning: strip command failed or not found");
            }
        }
    }

    // ELF ウォーターマーク埋め込み(strip の後に実行 — strip が e_ident padding をクリアするため)
    apply_elf_watermark(&output_path)?;

    // OPSEC バイナリ監査
    if let Some(config) = &obf_config
        && config.opsec_audit
    {
        opsec_audit_binary(&output_path, config.opsec_policy)?;
    }

    Ok(())
}

/// 単一ファイルコンパイル(後方互換ラッパー)
#[allow(dead_code)]
pub fn run(
    source_path: &Path,
    stage: Stage,
    obf_config: Option<ObfuscationConfig>,
    preprocess: PreprocessMode,
    pp_defines: &[String],
    pp_undefs: &[String],
) -> Result<()> {
    run_multi(
        &[source_path.to_path_buf()],
        stage,
        obf_config,
        preprocess,
        pp_defines,
        pp_undefs,
        false,
        None,
    )
}

/// リンク後バイナリの OPSEC 監査
///
/// `strings` コマンドでバイナリ内の文字列をスキャンし、
/// ELF バイナリに LSB ステガノグラフィでウォーターマークを埋め込む。
/// e_ident[9..16] (EI_PAD, 7 bytes) に "FERRUGO" の各ビットを LSB エンコードし、
/// e_flags (offset 48..52, 4 bytes) にバージョン情報を LSB エンコードする。
/// ELF 以外(macOS Mach-O 等)の場合はサイレントにスキップする。
fn apply_elf_watermark(binary_path: &Path) -> Result<()> {
    let mut data = std::fs::read(binary_path)
        .map_err(|e| CompileError::ExternalToolError(format!("read binary: {e}")))?;

    // ELF magic check: 0x7F 'E' 'L' 'F'
    if data.len() < 64 || data[0..4] != [0x7F, b'E', b'L', b'F'] {
        return Ok(()); // Not ELF, skip silently
    }

    // Verify 64-bit ELF (ELFCLASS64)
    if data[4] != 2 {
        return Ok(());
    }

    // LSB encode "FERRUGO" (7 bytes) into e_ident[9..16] (EI_PAD)
    let magic = b"FERRUGO";
    for (i, &byte) in magic.iter().enumerate() {
        // Spread 8 bits of magic[i] across byte at position 9+i using LSB
        // Since EI_PAD is normally all zeros, we write the LSB directly
        data[9 + i] = (data[9 + i] & 0xFE) | (byte & 0x01);
    }

    // LSB encode version number bits into e_flags (offset 48..52)
    // Spread the 4 low bits of the version across 4 carrier bytes' LSBs.
    // v0.4.0 = 4 = 0b0100 → LSBs [0, 0, 1, 0] at e_flags[48..52].
    let ver_num: u8 = 4; // v0.4.0
    for i in 0..4u8 {
        let bit = (ver_num >> i) & 0x01;
        data[48 + i as usize] = (data[48 + i as usize] & 0xFE) | bit;
    }

    std::fs::write(binary_path, &data)
        .map_err(|e| CompileError::ExternalToolError(format!("write binary: {e}")))?;

    Ok(())
}

/// IP アドレス・ファイルパス・URL・デバッグキーワード・資格情報キーワードを検出する。
/// `nm` コマンドで main/_ 以外のユーザー定義シンボルをフラグする(informational)。
fn opsec_audit_binary(binary_path: &Path, policy: OpsecPolicy) -> Result<()> {
    let tag = match policy {
        OpsecPolicy::Warn => "OPSEC AUDIT WARNING",
        OpsecPolicy::Deny => "OPSEC AUDIT ERROR",
    };

    let mut violations: Vec<String> = Vec::new();
    let mut strings_ran = false;

    // strings コマンドでバイナリ内の文字列をスキャン
    if let Ok(output) = Command::new("strings").arg(binary_path).output()
        && output.status.success()
    {
        strings_ran = true;
        let stdout = String::from_utf8_lossy(&output.stdout);
        for line in stdout.lines() {
            let lower = line.to_lowercase();

            // IP アドレスパターン
            if contains_ip_pattern(line) {
                violations.push(format!(
                    "[{tag}] Binary string may contain IP address: \"{}\"",
                    truncate_str(line, 60)
                ));
            }

            // ファイルパス
            if line.contains("/home/")
                || line.contains("/tmp/")
                || line.contains("/etc/")
                || line.contains("C:\\")
                || line.contains("\\\\")
            {
                violations.push(format!(
                    "[{tag}] Binary string may contain file path: \"{}\"",
                    truncate_str(line, 60)
                ));
            }

            // URL
            if lower.contains("http://") || lower.contains("https://") || lower.contains("ftp://") {
                violations.push(format!(
                    "[{tag}] Binary string may contain URL: \"{}\"",
                    truncate_str(line, 60)
                ));
            }

            // デバッグ系キーワード
            for keyword in &["debug", "todo", "fixme"] {
                if lower.contains(keyword) {
                    violations.push(format!(
                        "[{tag}] Binary string contains debug keyword \"{keyword}\": \"{}\"",
                        truncate_str(line, 60)
                    ));
                    break;
                }
            }

            // 資格情報関連キーワード
            for keyword in &[
                "password",
                "passwd",
                "secret",
                "api_key",
                "token",
                "credential",
            ] {
                if lower.contains(keyword) {
                    violations.push(format!(
                        "[{tag}] Binary string contains sensitive keyword \"{keyword}\": \"{}\"",
                        truncate_str(line, 60)
                    ));
                    break;
                }
            }
        }
    }
    // strings が実行できなかった場合: deny ポリシーでは fail-closed
    if !strings_ran {
        if policy == OpsecPolicy::Deny {
            return Err(CompileError::OpsecViolation(
                "binary audit: 'strings' command not available (required for deny policy)"
                    .to_string(),
            ));
        }
        eprintln!("[OPSEC] warning: 'strings' command not available, binary audit skipped");
        return Ok(());
    }

    // nm コマンドでユーザー定義シンボルをフラグ(informational)
    // ツールチェイン由来の既知シンボルは除外
    const TOOLCHAIN_SYMBOLS: &[&str] = &[
        "deregister_tm_clones",
        "register_tm_clones",
        "frame_dummy",
        "__do_global_dtors_aux",
        "__libc_csu_init",
        "__libc_csu_fini",
        "__libc_start_main",
        "_dl_relocate_static_pie",
        "_fini",
        "_init",
        "_start",
    ];
    if let Ok(output) = Command::new("nm").arg(binary_path).output()
        && output.status.success()
    {
        let stdout = String::from_utf8_lossy(&output.stdout);
        for line in stdout.lines() {
            // nm 出力形式: "addr T symbol_name"
            let parts: Vec<&str> = line.split_whitespace().collect();
            if parts.len() >= 3 && (parts[1] == "T" || parts[1] == "t") {
                let sym = parts[2];
                // main, _ プレフィックス, . プレフィックス, ツールチェイン由来シンボルをスキップ
                if sym != "main"
                    && !sym.starts_with('_')
                    && !sym.starts_with('.')
                    && !TOOLCHAIN_SYMBOLS.contains(&sym)
                {
                    eprintln!("[OPSEC AUDIT INFO] User-defined symbol in binary: {sym}");
                }
            }
        }
    }
    // nm が存在しない場合は黙ってスキップ(informational のみなので非致命的)

    // 違反を一括出力
    for v in &violations {
        eprintln!("{v}");
    }

    if policy == OpsecPolicy::Deny && !violations.is_empty() {
        return Err(CompileError::OpsecViolation(format!(
            "binary audit: {} violation(s) detected",
            violations.len()
        )));
    }

    if violations.is_empty() {
        eprintln!("[OPSEC] binary audit passed");
    }

    Ok(())
}

/// 文字列中に IP アドレスパターン(N.N.N.N)が含まれるか簡易判定
fn contains_ip_pattern(s: &str) -> bool {
    let bytes = s.as_bytes();
    let len = bytes.len();
    let mut i = 0;
    while i < len {
        if bytes[i].is_ascii_digit() {
            let mut dots = 0;
            let mut j = i;
            let mut valid = true;
            for _ in 0..4 {
                if j >= len || !bytes[j].is_ascii_digit() {
                    valid = false;
                    break;
                }
                let start = j;
                while j < len && bytes[j].is_ascii_digit() {
                    j += 1;
                }
                if j - start > 3 {
                    valid = false;
                    break;
                }
                dots += 1;
                if dots < 4 {
                    if j >= len || bytes[j] != b'.' {
                        valid = false;
                        break;
                    }
                    j += 1;
                }
            }
            if valid && dots == 4 {
                return true;
            }
        }
        i += 1;
    }
    false
}

/// 文字列を最大 max_len 文字(char 単位)に切り詰める
fn truncate_str(s: &str, max_len: usize) -> String {
    if s.chars().count() <= max_len {
        s.to_string()
    } else {
        let truncated: String = s.chars().take(max_len).collect();
        format!("{truncated}...")
    }
}