CRACK - Compiler Hardening Checker
Note: This is a v0 release, API may change.
A tool to analyze ELF binaries for security hardening features.
Supports binaries compiled with gcc, clang, and rustc (stable).
Based on recommendations from:
Installation
go install github.com/mkacmar/crack/cmd/crack@latest
Or download pre-built binaries from releases.
Usage
crack analyze [options] [<path>...]
<path>... - Files or directories to analyze (supports glob patterns)
--recursive - Recursively scan directories
--input <file> - Read paths from file, one per line (use - for stdin)
--parallel <n> - Number of files to analyze in parallel
Rule Selection
See rules reference for available rules.
--rules <ids> - Comma-separated list of rule IDs to run
--target-compiler <spec> - Only run rules available for these compilers (e.g., gcc, clang:15)
--target-platform <spec> - Only run rules available for these platforms (e.g., arm64, amd64)
The --target-compiler and --target-platform flags filter which rules are loaded based on their applicability.
At runtime, the tool also detects the actual compiler from binary metadata and skips rules that don't apply to the detected compiler.
For stripped binaries where detection fails, all loaded rules run.
Output Options
--include-passed - Include passing checks in output
--include-skipped - Include skipped checks in output
--sarif <file> - Save detailed SARIF report to file
--aggregate - Aggregate findings into actionable recommendations
--exit-zero - Exit with 0 even when findings are detected
The --include-passed and --include-skipped flags affect both text and SARIF output.
For programmatic access to results, use SARIF output (--sarif). SARIF (Static Analysis Results Interchange Format) is a standardized JSON format. We support SARIF version 2.1.0.
Logging Options
--log <file> - Write logs to file
--log-level <level> - Log level: none, debug, info, warn, error
Debuginfod Options
Fetch debug symbols from debuginfod servers.
--debuginfod - Enable debuginfod integration
--debuginfod-servers <urls> - Comma-separated server URLs
--debuginfod-cache <dir> - Cache directory for downloaded symbols
--debuginfod-timeout <duration> - HTTP timeout
--debuginfod-retries <n> - Max retries per server
Exit Codes
0 - Success (no findings, or --exit-zero specified)
1 - Error (invalid arguments, file errors, etc.)
2 - Findings detected
Programmatic Usage
The public packages can be used as a library to integrate binary analysis into your own tools, write custom rules, or build an alternative frontend.
Import the relevant packages:
import (
"github.com/mkacmar/crack/analyzer"
"github.com/mkacmar/crack/binary"
"github.com/mkacmar/crack/rule"
"github.com/mkacmar/crack/rule/elf"
"github.com/mkacmar/crack/toolchain"
)
Parse a binary using binary.ParseELF:
f, err := os.Open("/usr/bin/ls")
if err != nil {
log.Fatal(err)
}
defer f.Close()
bin, err := binary.ParseELF(f)
if err != nil {
log.Fatal(err)
}
The ELFBinary struct provides access to ELF metadata (see debug/elf for types), symbol tables, and detected toolchain info. Helper methods simplify common checks:
Create an analyzer with the rules you want to run (see rules reference for available rules):
rules := []rule.ELFRule{
elf.PIERule{},
elf.StackCanaryRule{},
elf.FullRELRORule{},
}
a := analyzer.NewAnalyzer(rules, opts)
findings := a.Analyze(bin)
for _, f := range findings {
fmt.Printf("%s: %s - %s\n", f.RuleID, f.Status, f.Message)
}
The Options struct controls which findings are returned:
IncludePassed - include rules that passed
IncludeSkipped - include rules that were skipped
By default, only failed rules are returned. Pass nil for options to use defaults.
Applicability
Each rule declares its applicability - which platforms and compilers it supports. For example, a rule might require GCC 10+ or only apply to ARM64 architecture. When analyzing a binary, rules that don't apply are automatically skipped.
Rules specify a MinVersion - the minimum compiler version required for the feature. The Platform field specifies which architectures the rule applies to. For example, the ARM PAC rule:
rule.Applicability{
Platform: binary.PlatformARM64v83,
Compilers: map[toolchain.Compiler]rule.CompilerRequirement{
toolchain.GCC: {MinVersion: toolchain.Version{Major: 10, Minor: 1}, Flag: "-mbranch-protection=pac-ret"},
toolchain.Clang: {MinVersion: toolchain.Version{Major: 12}, Flag: "-mbranch-protection=pac-ret"},
},
}
If your binaries are built with an internal compiler, register it via a custom detector so rules can determine whether they apply.
Use analyzer.CheckApplicability() to manually check if a rule applies:
result := analyzer.CheckApplicability(myRule.Applicability(), bin)
if result == analyzer.Applicable {
// ...
}
Use rule.FilterRules() to pre-filter rules based on your target environment. The filter uses MaxVersion to exclude rules that require a newer compiler than you have:
filter := &rule.TargetFilter{
Compilers: []rule.CompilerTarget{
{Compiler: toolchain.GCC, MaxVersion: &toolchain.Version{Major: 12}},
},
}
filtered := rule.FilterRules(rules, filter)
Custom Rules
To create a custom rule, implement the rule.ELFRule interface. For example, a rule that checks for a minimum stack size:
type MinStackSizeRule struct {
MinBytes uint64
}
func (r MinStackSizeRule) ID() string { return "min-stack-size" }
func (r MinStackSizeRule) Name() string { return "Minimum Stack Size" }
func (r MinStackSizeRule) Description() string { return "Ensures stack size meets minimum requirements" }
func (r MinStackSizeRule) Applicability() rule.Applicability {
return rule.Applicability{
Platform: binary.PlatformAll,
}
}
func (r MinStackSizeRule) Execute(bin *binary.ELFBinary) rule.Result {
for _, prog := range bin.Progs {
if prog.Type == elf.PT_GNU_STACK && prog.Memsz >= r.MinBytes {
return rule.Result{Status: rule.StatusPassed, Message: fmt.Sprintf("Stack size %d bytes", prog.Memsz)}
}
}
return rule.Result{Status: rule.StatusFailed, Message: "Stack size below minimum or not set"}
}
Custom Compiler Detection
To detect custom compilers, implement toolchain.ELFDetector and pass it to binary.ParseELFWithDetector(). This enables applicability checks for binaries built with internal or proprietary compilers:
// AcmeDetector detects Acme Corp's internal compiler, falling back to standard detection.
type AcmeDetector struct {
fallback toolchain.ELFCommentDetector
}
func (d AcmeDetector) Detect(comment string) (toolchain.Compiler, toolchain.Version) {
// Acme compiler writes "ACME C Compiler 2.3.1" in .comment section
if strings.Contains(comment, "ACME C Compiler") {
parts := strings.Fields(comment)
if len(parts) >= 4 {
if v, err := toolchain.ParseVersion(parts[3]); err == nil {
return toolchain.Compiler("acme-cc"), v
}
}
return toolchain.Compiler("acme-cc"), toolchain.Version{}
}
return d.fallback.Detect(comment)
}
bin, err := binary.ParseELFWithDetector(f, AcmeDetector{})
For complete API documentation, see pkg.go.dev.
License
MIT License - see LICENSE for details.