Why C-Based Code Cannot Be Used as Shellcode
Using C to Build a calc.exe
Executable
#include
#include
int main() {
WinExec("calc.exe", SW_SHOW);
return 0;
}
Then extract the .text section using objcopy:
x86_64-w64-mingw32-objcopy -O binary --only-section=.text exec_calc.exe exec_calc.bin
Disassemble with ndisasm
ndisasm -b 64 exec_calc.bin
From the disassembly, we can observe:
NOP Padding – Instructions such as nop word [cs:rax+rax+0x0] are inserted by the compiler for alignment or anti-disassembly purposes.
Strong Dependencies – Frequent usage of rel instructions (e.g., mov rax, [rel 0x34a0]) and API calls via the Import Address Table (IAT), such as jmp [rel 0x72ac] for WinExec.
Indirect Jumps – Instructions like FF25 (e.g., jmp [rel 0x72ac]) rely on fixed addresses in the import table, indicating tight coupling with the PE loader.
PE Format Reliance – The code uses system APIs through the IAT and expects the Windows PE loader to handle relocation, section initialization, and runtime setup.
Static API References – API calls like WinExec are accessed through statically defined entries in the import table.
Excessive Padding and Alignment – Compiler-inserted padding like
0F1F4000 nop dword [rax+0x0]
and section alignment at addresses like 0x1800.
Using msfvenom
to Generate Shellcode for calc.exe
payload:
$ msfvenom -p windows/x64/exec CMD=calc.exe -f raw -o calc.bin
Disassemble the Shellcode
$ ndisasm -b 64 calc.bin
Compare MSF shellcode and C
Msfvenom | C | |
---|---|---|
Generator Tool | Metaploit’s msfvenom
|
Extracted .text section from compiled PE |
Code Type | Pure position-independent code (PIC) | Compiler-generated, PE-dependent |
Dependency | No external dependencies, self-contained | Requires PE runtime, IAT, and relocation support |
Size | Compact (about 66 bytes) | Large (about 6KB) |
Feature | 1. Direct system calls, no standard library dependencies 2. Dynamically resolve WinExec address (via PEB traversal) 3. No redundant data, completely location independent |
1. Rely on PE Import Table (IAT) parsing API 2. Contains compiler-generated prologue/epilogue (such as stack frame adjustments) 3. Relocations and debugging information are present (even if the .text section is extracted) |
Execute Environmental | Can run from any memory address | Requires full PE loader support |
API call method | Dynamic resolution (gs:[0x60]→PEB→kernel32.dll) | Hard-coded address via IAT |
Code alignment | No padding bytes | Contains NOPs and alignment for structure |
Minimal, self-contained | Contains additional runtime/library code |
How to Improve C-Based Shellcode
To make your payload suitable for shellcode use, consider the following techniques:
PEB Traversal: Manually traverse the Process Environment Block (PEB) to locate loaded modules like kernel32.dll and resolve API addresses dynamically.
Dynamic API Resolution: Avoid using the Import Table; instead, resolve API addresses at runtime using hash-based or name-based lookup.
Avoid Compiler-Inserted Code: Use assembly or minimal C with inline assembly to bypass compiler-generated code (e.g., prologue/epilogue, SEH).
Position-Independent Code: Ensure your code can execute from any memory address without relying on relocations or fixed sections.