Module 4: Position-Independent Code
RipStart, RipData, and the call/pop trick on two architectures.
The Core Challenge
Shellcode gets injected at an unknown address. It can't use absolute addresses for data or functions. Stardust solves this with two assembly functions: RipStart() (where does my code begin?) and RipData() (where is my data section?). Everything else is built on top of these two primitives.
x64: RipStart
ASM - entry.x64.asm[BITS 64]
DEFAULT REL
EXTERN entry
GLOBAL stardust
GLOBAL RipStart
[SECTION .text$A]
stardust: ; Shellcode entry point
push rsi ; Save RSI
mov rsi, rsp ; Save original stack pointer
and rsp, 0FFFFFFFFFFFFFFF0h ; Align stack to 16 bytes (required by ABI)
sub rsp, 020h ; 32-byte shadow space for Windows x64 ABI
call entry ; Call our C++ entry() function
mov rsp, rsi ; Restore stack
pop rsi ; Restore RSI
ret
RipStart:
call RipPtr ; CALL pushes address of next instruction
ret ; This ret returns RAX to caller
RipPtr:
mov rax, [rsp] ; RAX = return address (pushed by CALL)
sub rax, 0x1b ; Subtract offset back to stardust label
ret ; RAX now = address of stardust (shellcode start)
Breaking Down the 0x1b Offset
When RipStart calls RipPtr, the return address on the stack points to the ret after call RipPtr. The magic number 0x1b (27 decimal) is the distance in bytes from stardust: to that return address. This is known at compile time because all the instructions between them have fixed sizes.
x86: Same Concept, Different ABI
ASM - entry.x86.asm[BITS 32]
DEFAULT REL
EXTERN _entry
GLOBAL _stardust
GLOBAL _RipStart
[SECTION .text$A]
_stardust: ; x86 entry point (note underscore prefix)
push ebp ; Standard x86 prologue
mov ebp, esp
call _entry ; Call C++ entry function
mov esp, ebp
pop ebp
ret
_RipStart:
call _RipPtr
ret
_RipPtr:
mov eax, [esp] ; EAX = return address
sub eax, 0x11 ; Different offset (x86 instructions are shorter)
ret
x86 Underscore Prefix Convention
Notice the underscore prefix on x86 symbols (_stardust, _RipStart, _entry). This is the cdecl name decoration convention used by 32-bit Windows compilers. The x64 ABI does not use this prefix, which is why the x64 version has bare names. The linker expects this convention to match symbols between assembly and C++ object files.
RipData: Finding the Data Section
RipData() sits in .text$C — the LAST code section, right after .rdata. This strategic placement means it can calculate where .rdata begins:
ASM - utils.x64.asm[BITS 64]
DEFAULT REL
GLOBAL RipData
[SECTION .text$C] ; Placed AFTER .rdata in the linker script
RipData:
call RetPtrData ; Push return address
ret
RetPtrData:
mov rax, [rsp] ; RAX = address of "ret" after call
sub rax, 0x5 ; Back up to start of RipData
ret ; RAX = address of RipData function
Memory Layout and RIP-Relative Addressing
RipData() returns: start of .text$C (right after .rdata)
symbol<T>(s) uses RipData() to calculate actual string address
How RipStart and RipData Work Together
C++ - from main.cc constructor// In the instance constructor:
base.address = RipStart(); // Where does our shellcode start?
base.length = ( RipData() - base.address ) + END_OFFSET; // How big is it?
// RipData is at the END of the shellcode, RipStart at the BEGINNING
// So the difference = total shellcode size
Why This Design is Elegant
The call/ret trick is a classic technique, but Stardust's use of two functions at opposite ends of the binary (RipStart in .text$A, RipData in .text$C) gives it two reference points. From those two points, it can calculate any address within the shellcode. The symbol<T> template (Module 7) builds on this to give position-independent string access.
Pop Quiz: PIC Techniques
Q1: Why does Stardust need BOTH RipStart() and RipData()?
Q2: The magic number 0x1b in RipPtr (x64) represents what?