A terminal UI (TUI) port of ArgFuscator.net - a tool for obfuscating command-line arguments to evade signature-based detection.
Built in Go as an educational security research project.
Source repo being ported: https://github.com/wietze/ArgFuscator.net
cmdFuscator reads ArgFuscator-compatible JSON profile files to learn which obfuscation techniques apply to a given executable, then presents an interactive terminal interface for applying those techniques to a command you type.
A de-obfuscator is planned as a future addition. Both the obfuscator and de-obfuscator are designed to be importable as Go module packages as well as usable via the CLI.
The module root exposes public packages (models, loader, engine) that can be
imported by external tools. The TUI lives entirely under cmd/cmdfuscator/ and is
not part of the public API. A future de-obfuscator will live alongside engine/ as a peer package at the module root.
cmdFuscator/
├── main.go # Module doc file (package cmdfuscator)
├── go.mod / go.sum
├── cmd/
│ └── cmdfuscator/
│ ├── main.go # TUI entry point
│ └── tui/
│ ├── app.go # Bubbletea model (View / Update / Init)
│ ├── styles.go # Lipgloss style definitions
│ └── keys.go # Key binding definitions
├── data/
│ ├── data.go # go:embed declaration (exports ModelFS)
│ └── models/
│ ├── bash.json
│ ├── certutil.json
│ └── powershell.json # add more from ArgFuscator repo here
├── models/
│ └── models.go # Token, Profile, ProfileFile, etc.
├── loader/
│ └── loader.go # LoadFS, IndexByName, GroupByPlatform
└── engine/
├── engine.go # Obfuscate(); Tokenize + Render stubs
└── modifiers/
├── modifier.go # Modifier interface + registry
├── all/
│ └── all.go # Blank imports to register all modifiers
├── charinsert/
│ └── char_insertion.go # STUB – TODO
├── filepath/
│ └── file_path.go # STUB – TODO
├── optionchar/
│ └── option_char_sub.go # STUB – TODO
├── quoteinsert/
│ └── quote_insertion.go # STUB – TODO
├── randomcase/
│ └── random_case.go # STUB – TODO
├── regex/
│ └── regex.go # STUB – TODO
├── reorderargs/
│ └── reorder_args.go # STUB – TODO
├── sed/
│ └── sed.go # STUB – TODO
├── shorthands/
│ └── shorthands.go # STUB – TODO
└── urltransform/
└── url_transformer.go # STUB – TODO
| Package | Import path |
|---|---|
| Module doc / root | cmdFuscator |
| Embedded profile data | cmdFuscator/data |
| Data types | cmdFuscator/models |
| Profile loader | cmdFuscator/loader |
| Obfuscation engine | cmdFuscator/engine |
| Modifier interface + registry | cmdFuscator/engine/modifiers |
| TUI (CLI only, not a library export) | cmdFuscator/cmd/cmdfuscator/tui |
Each file in data/models/ follows this schema:
{
"versions": { "argfuscator": "2.0", "format": "2.0" },
"profiles": [{
"executableVersion": "...",
"platform": "windows|linux|macos",
"operatingSystem": "Windows|Ubuntu|macOS",
"operatingSystemVersion": "...",
"alias": ["alt-name"],
"parameters": {
"command": [
{"command": "certutil.exe"},
{"argument": "-urlcache"},
{"url": "https://example.com"},
{"path": "output.txt"}
],
"arguments": [],
"modifiers": {
"RandomCase": { "AppliesTo": ["argument","value"], "Probability": "0.5" },
"QuoteInsertion": { "AppliesTo": ["path","url"], "Probability": "0.5" },
"OptionCharSubstitution": { "AppliesTo": ["argument"], "Probability": "0.5",
"OutputOptionChars": ["/","-","–"] },
"Sed": { "AppliesTo": ["argument"], "Probability": "0.5",
"SedStatements": "s/a/ᵃ/i\ns/e/ᵉ/i" },
"FilePathTransformer": { "AppliesTo": ["path"], "Probability": "0.5",
"PathTraversal": true, "SubstituteSlashes": true },
"CharacterInsertion": { "AppliesTo": ["argument"], "Probability": "0.5",
"Characters": ["…"], "Offset": "2" }
}
}
}]
}| Token Type | Meaning |
|---|---|
command |
The executable name (e.g. certutil, curl, etc) |
argument |
A flag/switch (e.g. -urlcache, --log-level) |
value |
A value for a preceding argument |
path |
A file-system path argument |
url |
A URL argument |
Each modifier stub in engine/modifiers/<name>/ has:
- A struct that implements the
Modifierinterface Name() string– must match the JSON key exactly (e.g."RandomCase")Description() string– shown in the TUI options panelApply(tokens []models.Token, cfg json.RawMessage) ([]models.Token, error)– the function you implement;cfgis the raw modifier config from the JSON profile
The engine calls Apply() on each enabled modifier in sequence. Stubs return
modifiers.ErrNotImplemented; the engine skips them gracefully and reports them
in the TUI status bar.
New modifiers self-register via init():
func init() { modifiers.Register(&MyModifier{}) }Then add a blank import to engine/modifiers/all/all.go.
- Use
errors.New/fmt.Errorf("...: %w", err)for error wrapping - Prefer value receivers for small structs, pointer receivers when mutating
- Use
strings.Builderfor efficient string construction - Use
math/randwith a seeded source for randomness - Write table-driven tests in
_test.gofiles alongside each modifier
This section is a curriculum for working through the stubs in a logical order. Each phase builds on the last and introduces progressively more interesting Go concepts.
| File | What to implement |
|---|---|
engine/engine.go |
Tokenize() — parse command string into typed tokens |
engine/engine.go |
Render() — join tokens back into a command string |
engine/modifiers/randomcase/ |
Probabilistic per-character case flip (implemented) |
engine/modifiers/quoteinsert/ |
Insert empty "" or '' inside tokens |
engine/modifiers/optionchar/ |
Replace - with –, /, —, etc. |
engine/modifiers/sed/ |
Parse s/a/ᵃ/i rules and apply per-char substitution |
engine/modifiers/filepath/ |
Path traversal, slash substitution, extra separators |
engine/modifiers/charinsert/ |
Insert invisible Unicode codepoints at a fixed offset (implemented) |
engine/modifiers/shorthands/ |
Abbreviate flags to shortest unambiguous prefix |
engine/modifiers/urltransform/ |
Hex/octal IP encoding, URL path traversal |
engine/modifiers/reorderargs/ |
Shuffle flag–value pairs while keeping them grouped |
engine/modifiers/regex/ |
Regex find-and-replace substitutions |
Each stub has detailed guidance comments. The TUI gracefully labels unimplemented modifiers as "not implemented" in the status bar without crashing.
Files: engine/engine.go
Start here. Everything else depends on the token representation being correct. No randomness, no config parsing — just pure string → struct → string.
Tokenize(command string, profile models.Profile) ([]models.Token, error)
- Split the input on whitespace (but respect quoted strings — a value like
"hello world"is one token). - The first token is always
TokenTypeCommand. - Walk
profile.Parameters.Argumentsto build a map of known flags →ValueCount. - For each remaining token:
- If it matches a known flag →
TokenTypeArgument; consume the next N tokens asTokenTypeValue. - If it starts with
http://orhttps://→TokenTypeURL. - If it contains
/or\(and isn't a flag) →TokenTypePath. - Otherwise →
TokenTypeValue.
- If it matches a known flag →
Render(tokens []models.Token) string
Join tokens with spaces. Re-quote any value that contains a space.
The invariant Render(Tokenize(cmd)) == cmd should hold for unmodified input.
Go concepts introduced: strings.Fields, strings.Builder, slice operations, map lookups, strconv.
Tests to write (engine/engine_test.go):
// Table-driven round-trip test
var cases = []struct{
input string
wantTokens []models.Token
}{
{"certutil.exe -urlcache -f https://x.com out.bin", [...]},
{"bash -c id", [...]},
}These modifiers touch individual characters or token boundaries. They are the easiest to reason about because the output is deterministic once you fix the random seed.
- Iterate over the rune slice of each eligible token value.
- For each rune, roll
rand.Float64(). If< probability, flip case withunicode.ToUpper/unicode.ToLower. - Rebuild the string with
strings.Builder.
Go concepts introduced: []rune vs []byte, unicode package, math/rand.
- Pick a random insertion position between index 1 and
len(runes)-1. - Insert
""or''(chosen randomly) at that position.
Go concepts introduced: Slice insertion (append(s[:i], append([]T{x}, s[i:]...)...)).
- Check whether
runes[0]is-or/. - If so, pick a random entry from
cfg.OutputOptionCharsand replace the first rune.
Go concepts introduced: json.Unmarshal into a typed config struct, multi-byte UTF-8 rune indexing.
Tests to write (per-modifier _test.go):
// Seed rand so output is deterministic, then assert exact output.
// Also test that tokens NOT in AppliesTo are never modified.
// Also test that Probability=0.0 always returns input unchanged.
// Also test that Probability=1.0 always transforms every eligible token.Parse SedStatements (newline-delimited s/<from>/<to>/i rules) into a
map[rune]string substitution table, then apply it per-character with probability.
- Split on
\nto get individual rules. - For each rule, the character after
sis the delimiter. Split on it:[from, to]. - The
/iflag means bothunicode.ToUpper(from)andunicode.ToLower(from)map toto. - Apply the table: for each eligible rune, if it exists in the map and probability fires, replace it.
Go concepts introduced: String parsing without regexp, rune → string maps.
Compile each rule's Pattern with regexp.Compile, then call re.ReplaceAllString on each eligible token value.
Go concepts introduced: regexp package, error handling for user-supplied patterns.
These modifiers require parsing structured values (file paths, URLs) rather than treating tokens as opaque strings.
Use strings.Split on / and \ to get path components, then:
- SubstituteSlashes: randomly swap
/for\and vice versa. - PathTraversal: insert
./or.\between two random adjacent components. - ExtraSlashes: double one random separator.
Go concepts introduced: path/filepath, platform-aware separator handling.
Parse with net/url.Parse, then inspect u.Hostname():
- If
net.ParseIP(host)succeeds, encode the IP in one of three alternate forms:- Integer: pack four octets into
uint32withbinary.BigEndian, format as%d. - Hex: same
uint32, format as0x%08x. - Octal: format each octet as
0%oand rejoin with..
- Integer: pack four octets into
- Reconstruct the URL string with the modified host.
Go concepts introduced: net/url, net.IP, encoding/binary, format verbs.
These modifiers need to understand the relationship between flags and their values, making them the most structurally complex.
- Build an index of all known flags from
profile.Parameters.Arguments. - For each argument token, strip its leading option char and find the matching flag entry.
- Find the shortest prefix of that flag's canonical form that is unambiguous (no other known flag shares it).
- Replace the token's value with
<option-char> + shortest-prefix.
Go concepts introduced: Prefix matching, data-driven lookup tables, handling ambiguity.
- Separate the command token (index 0) from the rest.
- Group remaining tokens into
(flag, value...)pairs using theArgumentDefinitionsValueCount. - Shuffle the pairs with
rand.Shuffle. - Flatten:
[command] + [group1 tokens...] + [group2 tokens...] + ....
Go concepts introduced: rand.Shuffle, grouping slices by a data-driven rule.
Write table-driven tests. Every test case should specify:
| Field | Purpose |
|---|---|
name |
Description shown on failure |
input |
[]models.Token before the modifier runs |
cfg |
Raw JSON config (use json.RawMessage(...) literals) |
want |
Expected []models.Token after the modifier runs |
wantErr |
Whether an error is expected |
Seed math/rand in test setup so randomized modifiers produce deterministic output:
// In TestMain or individual tests:
rand.Seed(42)Even without a fixed seed you can assert structural invariants:
- The token count never changes (modifiers only mutate values, not add/remove tokens).
TokenTypeCommandtokens are never modified unless"command"is inAppliesTo.- The original string is recoverable when
Probability = "0.0". - With
Probability = "1.0", every eligible token is different from the input (for case-flipping modifiers).
Test the full pipeline end to end:
result, err := eng.Obfuscate("certutil.exe -urlcache -f https://x.com out.bin", profile, enabled)
// assert no error, output is non-empty, output differs from inputLoad every file in data/models/ and assert no parse errors. This catches JSON
schema drift early:
profiles, err := loader.LoadFS(data.ModelFS)
assert.NoError(t, err)
assert.NotEmpty(t, profiles)Download JSON files from: https://github.com/wietze/ArgFuscator.net/tree/main/models
Place them in data/models/. They are embedded at compile time via go:embed
in data/data.go.
go mod tidy # fetch dependencies
go run ./cmd/cmdfuscator # launch TUI
# or build a binary
go build -o cmdfuscator ./cmd/cmdfuscator && ./cmdfuscator| Package | Role |
|---|---|
github.com/charmbracelet/bubbletea |
TUI event loop |
github.com/charmbracelet/lipgloss |
Terminal styling and layout |
github.com/charmbracelet/bubbles |
textinput and viewport widgets |
| Key | Action |
|---|---|
Tab |
Cycle focus between panels |
Up / Down |
Navigate list / options |
Space |
Toggle modifier on/off |
Enter |
Apply obfuscation |
c |
Copy output to clipboard |
r |
Reset / clear output |
/ |
Focus search bar in sidebar |
Esc |
Cancel search |
q / ^C |
Quit |
This project is intended for educational and authorized security research purposes only. The obfuscation profiles are derived from the ArgFuscator.net project (GPL-3.0).