The Common Objection
A common claim in security circles goes something like this:
“The language doesn’t really matter. If a program makes suspicious syscalls, it gets detected. If it doesn’t, it won’t. Writing in an obscure language is irrelevant.”
On the surface, this sounds reasonable. But it collapses under scrutiny — and empirical testing makes the collapse very visible.
What Compilers Actually Leave Behind
Every compiler has a personality. It leaves traces.
Simple version: Every language has its own handwriting. When you compile C or Rust, the binary looks like it was written by that compiler — because it was. Assembly has no handwriting.
C / GCC Artifacts:
- Compiler version strings embedded in binary
- Standard function prologue/epilogue patterns (
push rbp,mov rbp, rsp,sub rsp, 0x...) - libc dependency chain (even with
-nostdlib, CRT startup code remains) - Exception handling tables (
.eh_framesection) - Stack canary patterns (
__stack_chk_fail) - DWARF debug artifacts (even after
strip)
Rust / LLVM Artifacts:
- Panic handler stubs (even with
no_std) - Unwind tables
- LLVM IR-specific optimization patterns
- Rust ABI function signatures
__rust_alloc,__rust_deallocsymbols (even unused)
Pure Assembly:
- Raw opcodes
- Whatever strings you explicitly put there
- Nothing else
Simple version: C and Rust binaries come with luggage they can’t leave behind. Assembly travels light — only what you pack.
The Calling Convention Problem
This is where the argument usually gets interesting.
C/Rust compilers enforce System V AMD64 ABI on every function call:
; Every C function call looks like this
push rbp
mov rbp, rsp
sub rsp, 0x20
; ... function body ...
leave
ret
This is a detectable pattern. Detection engines can fingerprint it. Every. Single. Function.
In pure assembly, you have no obligations:
; No prologue
; No standard frame
; Registers used in any order
; Stack manipulated freely
; Can use SROP instead of call/ret entirely
Simple version: When C calls a function, it always does it the same way — like a habit you can’t break. In assembly, you can call functions differently every time, in ways a compiler would never produce.
The CFG Argument: Compilers Are Predictable by Design
Modern detection systems rely heavily on Control Flow Graph (CFG) analysis — mapping how execution moves through a binary.
Compilers produce deterministic CFGs. Given the same source code and compiler version, you get the same output. Detection tools learn these patterns.
Pure assembly allows:
CMOV obfuscation — branch-free execution:
mov rbx, 35
cmovz rax, rbx ; No branch, no predictable CFG edge
cmovs rax, rbx ; Looks like data movement, behaves like control flow
SROP — kernel-mediated execution:
mov rax, 15 ; rt_sigreturn
syscall ; Execution jumps via kernel signal frame
; CFG tools see a dead end here
Simple version: A compiler always takes the same roads. In assembly, you can cut through fields, jump fences, and take paths that don’t appear on any map.
The “Rewrite From Scratch” Fallacy
Another common argument:
“The exact same logic applies to just rewriting malware from scratch in C. How is the antivirus supposed to find a signature it doesn’t know exists?”
This concedes more than it intends to. Yes, rewriting from scratch removes known signatures. But:
- A fresh C binary still has compiler artifacts
- A fresh Rust binary still has LLVM patterns
- A fresh assembly binary has nothing recognizable by definition
You can’t signature something with no fingerprints. You can flag it as unknown — but “unknown” and “malicious” are different verdicts.
Simple version: Even a brand new C program wears a name tag that says “made with GCC.” Assembly wears no name tag at all.
Empirical Results
Theory is useful. Test results are better.
Test setup: Pure x64 Linux Assembly C2 implant with live XOR-encrypted shellcode embedded. Tested against 65 antivirus engines and enterprise sandbox analysis.
| Platform | Result |
|---|---|
| VirusTotal (65 engines) | 0/65 — no detections |
| Hatching Triage (behavioral) | Score 8/10 — detected process enumeration and ptrace |
| Triage — SROP injection | Not detected |
| Triage — process_vm_writev | Not detected |
| Triage — post-injection C2 | Not detected |
The sandbox saw the entry phase (process discovery, ptrace syscall). It completely missed the injection mechanism and everything that happened after.
Simple version: The security system saw someone walk up to the door. It had no idea they picked the lock, walked in, and made themselves at home.
Addressing the Syscall Argument
“If it makes suspicious syscalls, it gets detected. Language doesn’t matter.”
Let’s test this directly.
The implant made the following syscalls during injection:
ptrace(PTRACE_ATTACH) → Detected by sandboxrt_sigreturn(SROP) → Not detectedprocess_vm_writev(syscall 311) → Not detected
Same binary. Same execution. Some syscalls detected, critical ones missed entirely.
Why? Because rt_sigreturn in the context of SROP looks like a legitimate signal return. process_vm_writev is a modern API with legitimate uses. The sandbox has no behavioral model for the combination — because the combination is rare, undocumented in the wild, and produces no compiler-recognizable patterns.
Simple version: “Suspicious syscalls get detected” only works if the detection system knows the context. Change the context in ways a compiler never would, and the rule stops applying.
What “Compiler-Enforced” vs “Optional” Actually Means
One more common pushback:
“Assembly has calling conventions too.”
Technically true. System V AMD64 ABI exists. But:
- In C/Rust: the compiler enforces it on every function call. No exceptions.
- In assembly: it’s optional. You can follow it, ignore it, or invent your own.
In the implant analyzed for this post, there are:
- No standard function prologues
- No predictable register assignments
- No consistent stack frame structure
- SROP frames instead of standard call/ret
A detection rule written for “standard calling convention + ptrace” catches the ptrace. It has nothing to match for the rest.
The Detection Surface Formula
C binary detection surface:
= compiler artifacts
+ library patterns
+ calling convention fingerprints
+ CFG predictability
+ known syscall sequences
Pure assembly detection surface:
= only what you explicitly include
This is not a marginal difference. It’s structural.
Simple version: Detection systems are pattern matchers. The fewer patterns you produce, the less they can match. Assembly lets you produce fewer patterns than any compiled language — not because of magic, but because there’s no compiler adding patterns you didn’t ask for.
What This Means for Defense
If pure assembly malware became common:
- Signature-based detection — effectively blind. No patterns to sign.
- CFG analysis — partially blind. SROP and CMOV break graph assumptions.
- Behavioral detection — still works, but only for high-level behaviors (file access patterns, process enumeration). Injection mechanism and C2 remain invisible.
- eBPF/kernel monitoring — most effective layer. Syscall sequences captured at kernel level regardless of language.
The practical implication: network and static defenses are insufficient. Endpoint behavioral detection at the kernel level is the only reliable layer.
Academic Support
This isn’t just empirical observation. The academic literature supports it.
“Exotic Compilers as a Malware Evasion Technique” (SEBD 2019, Calabria University) directly addresses this:
“Since in single instruction compiled programs comparisons, jumps, function calls are all implemented with a single instruction, the resulting CFG is a single (usually long) basic block. This result is very interesting, because having a CFG composed by a single basic block makes ineffective all detection.”
The paper’s conclusion: unusual compilation approaches — including approaches that eliminate compiler fingerprints — measurably defeat CFG-based detection. Pure assembly takes this further than any compiler-based approach can.
Conclusion
The claim that “language doesn’t matter, only behavior does” conflates two different things: capability and detection surface.
Yes, C and Rust can make the same syscalls. No, they cannot eliminate compiler artifacts, enforce non-standard execution paths, or produce SROP-style CFG breaks the way hand-written assembly can.
The test results are unambiguous: 0/65 on static analysis, critical injection techniques invisible to dynamic analysis, post-exploitation C2 activity undetected.
Language matters. Not because of what it can do — but because of what it doesn’t leave behind.
Further Reading
- ICMP-Ghost C2 — Pure Assembly C2 Framework
- Phantom Evasion Loader — SROP + process_vm_writev
- Dissecting LockBit 5.0 Linux
- Phantom Evasion Loader Blog
- Volatile Storage Mechanisms
- Ianni et al., “Exotic Compilers as a Malware Evasion Technique”, SEBD 2019