Difficulty: Intermediate

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

.text$A — stardust(), RipStart(), RipPtr()
.text$B — entry(), instance(), resolve::module(), resolve::_api()
.rdata — "user32.dll", "Hello world", "caption" (strings)
.text$C — RipData(), RetPtrData()
RipStart() returns: start of .text$A (code base)
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()?

RipStart() returns the very beginning of the shellcode (useful for calculating total size and knowing where code starts). RipData() returns the position of the data section boundary, which the symbol<T> template uses to translate compile-time string pointers into runtime addresses.

Q2: The magic number 0x1b in RipPtr (x64) represents what?

0x1b (27 bytes) is the sum of all instruction sizes from the stardust: label to the point where RipPtr reads [rsp]. It's a hardcoded compile-time constant. On x86 the offset is 0x11 (17 bytes) because x86 instructions are shorter.