Post

[Blog] Stack Pivot - Overflow in libc function

Stack pivot technique

[Blog] Stack Pivot - Overflow in libc function

Preface

Hello, this is my first blog about a pwnable technique. In this blog, I will explain the attack vector in stack pivot that I usually use in pwnable challenges specifically in this challenge.

Background - Stack Pivot

When calling a function, compiler will create a stack frame for it. A frame typically contains:

  • Local variables
  • Saved registers
  • Return address

On x86-64, two registers are especially important for stack-related exploitation:

  • rsp (Stack Pointer)
    • Always points to the top of the stack.
    • All push/pop/call/ret operations use rsp.
        push rax  ; rsp -= 8
                  ; [rsp] = rax
        pop rax   ; rax = [rsp]
                  ; rsp += 8
        call func ; rsp -= 8
                  ; [rsp] = rip
        ret       ; rip = [rsp]
                  ; rsp += 8
      
  • rbp (Base Pointer)
    • Points to the base of the current stack frame.
    • Local variables are accessed with negative offsets like [rbp - offset]

Function prologue:

push rbp        ; rsp -= 8
                ; [rsp] = rbp
mov rbp, rsp    ; rbp = rsp
sub rsp, offset ; allocate local variables

Function epilogue:

leave   ; mov rsp, rbp
        ; pop rbp
ret     ; rip = [rsp]
        ; rsp += 8

Stack frame layout:

1
2
3
4
5
6
7
8
9
10
11
[ high addresses ]
... previous frames ...
+------------------------+
| return address         |  <-- [rbp+8]
+------------------------+
| saved RBP (caller)     |  <-- [rbp]
+------------------------+
| local variables        |  <-- [rbp-0x8], [rbp-0x10], ...
| (buffers, ints, ...)   |
+------------------------+
[ low addresses ]           <-- rsp after sub

So what is stack pivot? A stack pivot is a technique that allows us to change the stack pointer (rsp) to a memory region we control (for example, a buffer on the heap, .bss, or another stack area). This is useful when we don’t enough gadgets or libc leaks to perform a ROP chain on the original stack.

Some common ways to achieve stack pivot:

  • Overwriting saved rbp and then after the second leave; ret, rsp will be set to our controlled memory.
  • Using some gadgets such as pop rbp; ret or leave; ret to set rbp and rsp.

Overflow in libc function

As I mentioned above, every C function compiled with the System V ABI gets a prologue and an epilogue that set up and tear down its stack frame. From the CPU’s point of view, calling scanf is no different from calling a user-defined foo() — it’s still a call instruction that creates a new stack frame, and that function is free to call other functions inside libc.

Another detail that is easy to overlook is that many libc functions are actually thin wrappers around more complex internal helpers. For example, when you call:

1
__isoc99_scanf("%s", buf);

the control flow usually looks like:

1
2
3
4
main
  └─ __isoc99_scanf           // public wrapper
       └─ __vfscanf_internal  // real parser/worker
            └─ (other internal helpers, locale helpers, …)

Every each of these functions has its own prologue, epilogue and thus its own stack frame: local variables at [rbp - offset], a saved frame pointer at [rbp], and a return address at [rbp + 8].

So what happens if we can make an overflow inside one of these internal libc functions, specifically in libc input function such as scanf, read, fgets? The attack vector relies on Frame Overlap.

We know that in a standard function (like main), a local buffer is located at a fixed offset from the Base Pointer:

lea rax, [rbp - 0x100]  ; load address of buffer
mov rsi, rax            ; argument for read/scanf

If we can overwrite the saved RBP of the current function and return, the caller (e.g., main) will think its stack frame is somewhere else.

Set up

We use a small primitive to overwrite the Saved RBP on the stack to a location called fake_rbp and when the function returns, it executes leave; ret:

  • mov rsp, rbp cleaning up the stack.
  • pop rbp pop our fake_rbp into rbp.
  • ret execution returns to the caller (e.g., main).

Now, we’re back in main, but the rbp is pointing to our fake_rbp location.

Trigger

main then continues execute and calls an input function like read(0, buf, 0x100) to read data. To prepare for this call, main calculates the address of the buf related to the current rbp:

lea rax, [rbp - 0x100] ; buffer = fake_rbp - 0x100

At this point, if we carefully choose fake_rbp such that buf overlaps with the saved rbp and return address of the input function (read), we can exploit this overlap to overflow into those critical fields.

Example

Let’s take an example with read(0, buf, 0x100): Suppose we are in a function vuln() with a primitive to overflow to return address:

  • Original rbp : 0x7fffffffe100
  • Fake rbp : 0x7fffffffe200

When vuln() executes leave; ret and return to lea rax, [rbp - 0x100]:

1
2
3
4
5
6
leave       ; mov rsp, rbp  ; rsp = 0x7fffffffe100
            ; pop rbp       ; rsp = 0x7fffffffe108, rbp = 0x7fffffffe200
ret         ; rip = [rsp]   ; return address
            ; rsp += 8      ; rsp = 0x7fffffffe110

lea rax, [rbp - 0x100] ; buf = 0x7fffffffe200 - 0x100 = 0x7fffffffe100

Next vuln() executes read again:

1
2
call read   ; rsp -=8        ; rsp = 0x7fffffffe108
            ; [rsp] = rip

The return address will be at 0x7fffffffe108. As you can see, the buffer now is 0x7fffffffe100 which is just above the return address 8 bytes. Inside of read, it will have a syscall read to actually read our input and we can easily get an overflow to overwrite the save rbp and return address of read.

Keep in mind: some libc functions have their own stack canaries!!!

Application

In the challenge, I only have a very weak primitive: I can arbitrarily overwrite 2 bytes at a chosen address. The execution then goes into an encode function that I can’t really control, so at first glance it looks like there’s no way to build an ORW ROP chain.

The key idea is to stop targeting the return address and instead focus on the saved rbp. By carefully computing offsets, I find a pointer that lets me turn a later read into a stack overflow. Once I overwrite rbp, the next read call will use this region as part of its stack frame. That finally gives me RIP control and lets me jump to my ROP chain, even though I only started with a 2-byte write primitive.

This leads to an unintended solution for the challenge, but more importantly it shows a useful pattern: you don’t always need to touch the original return address. You can first misalign the stack via rbp or rsp, then abuse a later function (like read, scanf) to do the actual overwrite.

In addition, internal libc helpers themselves are packed with handy gadgets, such as sequences that pop registers and then return. When the binary has very few useful instructions, these internal functions effectively act as an extra gadget pool, giving you the missing pieces you need to build a working payload even in a very constrained environment.

In __vfscanf_internal:

Desktop View

fgets:

Desktop View

getchar:

Desktop View

Furthermore, this technique is not tied to this specific setup. In general, it’s often enough to tamper with the saved return address of a function or, more subtly, just the saved rbp. If you can redirect rbp into attacker-controlled memory, the normal epilogue (leave; ret) will pivot the stack for you. From there, any function that reads or writes a large chunk of data on that fake frame becomes a powerful primitive to overwrite the new return address. Simply controlling rbp/rsp can be enough to reach full ROP in cases where a direct overwrite of the original return address seems impossible.

This post is licensed under CC BY 4.0 by the author.