Skip to content

Preserve clap CLI names in obfuscated output #1

@dmriding

Description

@dmriding

The Problem

When Apate obfuscates a crate that uses clap derives for CLI parsing, the obfuscated binary's CLI becomes unusable:

// Before obfuscation — clean CLI
#[derive(Parser)]
struct Cli {
    #[arg(long)]
    input: PathBuf,    // generates --input flag
    #[command(subcommand)]
    command: Commands,
}

enum Commands {
    Encrypt { ... },   // generates `encrypt` subcommand
    Decrypt { ... },   // generates `decrypt` subcommand
}

After obfuscation, --input becomes --__a3b2 and encrypt becomes b9c5. The binary compiles and runs, but the CLI is gibberish. You can see this in action by running --help on Apate's own Level 1 output.

The Fix

Detect structs/enums annotated with #[derive(Parser)], #[derive(Subcommand)], #[derive(Args)], or #[derive(ValueEnum)], and preserve their field names, variant names, and doc comments — since clap uses all of these at runtime.

Important: This is source code obfuscation, not binary obfuscation. Apate works at the AST level using syn. No compiler internals, no LLVM, no binary manipulation.

Scope

What to preserve:

  • Field idents on clap-derived structs (they become --long flag names)
  • Enum variant idents on #[derive(Subcommand)] enums (they become subcommand names)
  • Doc comments (///) on clap-annotated items (clap uses them as --help text)

What to still obfuscate:

  • All internal logic, local variables, non-clap types
  • Everything that isn't part of the external CLI interface

Implementation

Step 1: Detection helper (beginner-friendly)

Add a has_clap_derive() function that checks if an item has a clap derive attribute:

fn has_clap_derive(attrs: &[syn::Attribute]) -> bool {
    attrs.iter().any(|attr| {
        if attr.path().is_ident("derive") {
            if let Ok(nested) = attr.parse_args_with(
                syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
            ) {
                return nested.iter().any(|path| {
                    path.segments.last().map_or(false, |seg| {
                        matches!(
                            seg.ident.to_string().as_str(),
                            "Parser" | "Subcommand" | "Args" | "ValueEnum"
                        )
                    })
                });
            }
        }
        false
    })
}

Step 2: Collect preserved names

Walk the AST pre-rename, find clap-derived structs/enums, and collect their field/variant names into a HashSet<String> of names to skip.

Step 3: Wire into rename pass

Add the preserved names to the blocklist in both the heuristic renamer and the SemanticRenamer path.

Step 4: Preserve doc comments in strip pass

Skip stripping doc attributes on items that have clap derives.

Verification

  • Self-hosting all 3 levels: cargo build succeeds
  • Obfuscated binary: apate --help shows correct flag names and subcommands
  • Roundtrip: apate decrypt still restores byte-for-byte

Files involved

File Change
src/passes/strip.rs Skip doc-stripping on clap items
src/passes/rename.rs Skip renaming clap field/variant names
src/passes/homoglyph.rs No change needed (inherits from rename)
src/passes/strings.rs No change needed (already skips attributes)

Metadata

Metadata

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions