Published on

Pwn Learning Path #1: Reading Stack Frames in GDB

Authors
  • avatar
    Name
    Muhammad Huzaifa
    Twitter

Before overwriting a return address, I needed to know where that address lives and how it gets there. My first lab was therefore not an exploit. It was a GDB fluency drill.

The program

The important function accepts three integers, creates local values, and returns their sum:

static long inspect_me(long a, long b, long c) {
    volatile long local_a = a + 1;
    volatile long local_b = b + 2;
    volatile long local_c = c + 3;
    volatile long total = local_a + local_b + local_c;

    printf("local_a=%ld local_b=%ld local_c=%ld total=%ld\n",
           local_a, local_b, local_c, total);
    return total;
}

The volatile locals make the values easier to observe in a simple debug build.

What I inspected

I built the program, opened it in GDB, and stopped at both main and inspect_me:

cd labs/phase1-stack-basics
make
gdb ./stack_frames
set disassembly-flavor intel
break main
run
disassemble main
break inspect_me
continue
info registers
x/40gx $rsp
disassemble inspect_me

On Linux x86_64, the first integer or pointer arguments arrive in these registers:

1: rdi
2: rsi
3: rdx
4: rcx
5: r8
6: r9

That meant the values 10, 20, and 30 could be checked in rdi, rsi, and rdx as the function began.

The frame model that clicked

For this simple unoptimized build, I could reason about the function frame like this:

higher addresses

[rbp + 8]  saved return address
[rbp]      saved caller RBP
[rbp - ...] local variables

lower addresses

Each register had a different job:

  • rip identifies the current or next instruction;
  • rsp points at the current top of the stack;
  • rbp acts as a stable frame reference in this build;
  • rax carries the function's return value.

The function epilogue made more sense once I stopped treating it as magic:

leave  -> restore this function's stack frame
ret    -> take an address from [rsp], advance rsp by 8, and jump there

The saved return address is therefore data until ret consumes it. If a memory bug lets me change that data, I can change the next value loaded into rip.

My main takeaway

The useful skill was not memorizing [rbp+8]. Compiler options can change the exact layout, and optimized code may not use rbp as a frame pointer at all.

The real skill was learning how to verify the layout:

  1. stop before and after call;
  2. inspect rsp, rbp, and the stack;
  3. step through the prologue and epilogue;
  4. connect each instruction to a concrete memory change.

That model became the foundation for the first control-flow exploit.

Series navigation

← Part 0: roadmap · Part 2: basic ret2win →