What is Position-Independent Code?
Position-independent code (PIC) is code that executes correctly regardless of where it’s loaded in memory. This is critical for shellcode because:- No fixed addresses: You don’t know where your code will be loaded
- No relocations: Traditional PE files have relocation tables - shellcode doesn’t
- Data access: You need to access strings and constants without hardcoded addresses
- Self-awareness: The code must discover its own location at runtime
Why PIC Matters for Shellcode
When injected into a process, shellcode can land anywhere in memory. Consider this scenario:0x180000000 instead of 0x140000000, it will read garbage or crash.
RipStart() - Finding Your Base
TheRipStart() function calculates the shellcode’s base address using a clever call-stack trick.
x64 Implementation
Fromsrc/asm/entry.x64.asm:20-27:
call RipPtrpushes the return address (theretaftercall) onto the stack[rsp]contains the address of the instruction immediately aftercall- Subtract the known offset from start of shellcode to this instruction
- Result: absolute base address of the shellcode
x86 Implementation
Fromsrc/asm/entry.x86.asm:18-25:
Usage in Code
The base address is calculated in the instance constructor (src/main.cc:19):
base.address contains the absolute memory address where your shellcode is loaded, regardless of where that is.
RipData() - Locating Your Data Section
TheRipData() function returns the address where the data section (strings, constants) begins.
Implementation
Fromsrc/asm/utils.x64.asm:7-15:
RipData() in .text$C, which is positioned after all code (.text$A and .text$B) and right before .rdata, it provides a reliable marker for where the data section starts.
Section Layout
The linker script (scripts/linker.ld) enforces this order:
Size Calculation
Using both functions together (src/main.cc:19-20):
symbol() - Accessing Raw Strings
Thesymbol<T>() template function provides position-independent access to string literals and data.
Implementation
Frominclude/common.h:35-38:
How It Works
This function calculates the runtime address of a compile-time constant using pointer arithmetic:RipData()returns the runtime address of the data section&RipDatais the compile-time address where the compiler placedRipDatafunctionsis the compile-time address of the string literal- The difference
(&RipData - s)is a compile-time constant offset - Subtracting this offset from the runtime
RipData()gives the runtime address ofs
Usage Examples
Loading a DLL:"user32.dll"is stored in.rdataat offset, say,0x1000- Compiler uses this temporary address
symbol()calculates: “Where is the data section now?” + “offset to this string”- Returns the correct runtime address
- LoadLibraryA receives valid pointer to “user32.dll”
Type Safety
The template parameter enforces type safety:Linker Script and Section Ordering
The linker script is the foundation of position independence in Stardust.Complete Script
Fromscripts/linker.ld:
Why This Order Matters
1. .text$A First Contains the entry point. Must be at the beginning so when the shellcode is called, execution starts here. 2. .text$B Second All functions marked withdeclfn go here:
include/macros.h:6. This groups all your main code together.
3. .rdata Third
String literals and constants. By placing this after all code, we can:
- Use
RipData()to find it - Calculate shellcode size (code ends where data begins)
- Access strings with
symbol<T>()
RipData() function itself. Acts as a marker for the end of the shellcode.
Memory Layout Example
If shellcode loads at0x180000000:
0x180000526 at runtime, even though the compiler used a different address.
Practical Example: Position-Independent String Access
Let’s trace a complete example of accessing a string:Compile Time
Source code:- Stores
"user32.dll\0"in.rdatasection - Assigns temporary address, e.g.,
0x00001000 - Generates call to
symbol()with0x00001000as argument
Link Time
Linker:- Places
.text$Aat offset0x0000 - Places
.text$Bat offset0x0200 - Places
.rdataat offset0x0500(“user32.dll” at0x051A) - Places
.text$Cat offset0x053B
Runtime (Shellcode at 0x180000000)
Execution:-
RipStart() called
- Returns
0x180000000
- Returns
-
RipData() called within symbol()
- Returns
0x18000053B(runtime address of.text$C)
- Returns
-
Pointer arithmetic in symbol()
-
LoadLibraryA receives 0x18000051A
- Reads “user32.dll” correctly
- Loads the library
Benefits and Trade-offs
Benefits
✅ Works anywhere: No matter where the shellcode is injected, it runs correctly ✅ No relocations: The PE relocation section is not needed ✅ Self-contained: All data is embedded and accessible ✅ Compact: Small code size, no external dependenciesTrade-offs
⚠️ Initial setup: Must callRipStart()/RipData() before using strings
⚠️ Wrapper functions: Need symbol<T>() wrapper for every data access
⚠️ Architecture-specific: Different offsets for x86 vs x64
⚠️ No global data: Can’t use global variables with initializers
Best Practices
Always Use symbol() for Data
Cache RipStart Results
Don’t callRipStart() repeatedly:
Mark Functions with declfn
All your code should be in.text$B:
