Safe-Linkingのリークについて(と、AeroCTF2022 writeup)
小さな便利テクを思いついたのでメモしておきます。 Safe-Linking有効で、free後のfdの読み出し時、freeリストの先頭チャンクでない場合でも、以下の式でheapの先頭が求められます。
(leak ^ (leak >> 12) ^ (leak >> 24)) & 0xfffffffff000
制約は、fdで示されたチャンクがheapページの先頭から 0x1000までのところに存在することだけです。
これの強みは、freeリストの先頭であるheapの先頭 >> 12
の値を見ずとも計算できること、fdの先頭1バイトを破壊せざるを得ない状況(正確には先頭12ビットまで)でも良いということの2つです。
以上です。
以下はこれを使って解いたAeroCTF2022よりheap-2022と、全く関係ないone_bulletの2問のwriteupです。exploitはここです。
heap-2022
free後に消去していないポインタを使って、freeが何回もできるようになるタイプのヒープ問です。glibc2.35です。
freeする先を差し替えられるので、これを使って任意アドレスfreeになります。
mainから抜けられるので最後はスタックを狙えばよいでしょう。ということで、必要なのはheap、libc、stackアドレスのリークと、任意アドレスallocです。任意アドレスallocはオーバーラップからtcacheを編集して書いていきます。allocに回数制限があるのでそれだけ気を付けたら、そんなにかからずに書きれます。
ところがタイムアウトがかなり厳しいです。exploitを何度か投げてみますが、だいたい6割くらいで止まります。
一般的に?こういうタイムアウトではまじめにインタラクティブにやらず、以下のようにプロンプトを多少無視して投げると上手くいくことが知られています。
r.sendlineafter('> ', '1')
if ENABLE_BULK_SEND:
r.sendline(data)
else:
r.sendlineafter(': ', data)
しかし、おそらく受け取り側の関数に依存して成否が決まり、今回はまともに動かなかったので不採用です。(完全に体感ですが、readが入っていると上手くいかないことが多いです。)
ということで以降数時間で謎のheap問ゴルフ?をやり、ぽちぽち計算していると、上のheapリークを思いついたという話でした。この発想によってheapリークを一回分減らし、さらにlibcリークで使ったパディング用のチャンクを任意アドレスallocでも利用することでさらにallocを減らし、ようやく通りました。
フラグを取ってしばらく後、終了間際に運営に文句を言った参加者がいて、タイムアウトがあっさり延長されていました。悲しいね。
one_bullet
static, malloc_hookが使えるlibc、no-pieです。ライブラリはsyscallラッパー、execXX系が全部ないものが用意されています。
バグがよくわからずとりあえずガチャガチャしていると、scanfの格納先がバグってセグります。
00401f44 f3 0f 1e fa ENDBR64
00401f48 55 PUSH RBP
00401f49 48 89 e5 MOV RBP,RSP
00401f4c 48 83 ec 10 SUB RSP,0x10
00401f50 48 c7 45 MOV qword ptr [RBP + local_10],0x0
f8 00 00
00 00
00401f58 48 8d 3d LEA RDI,[s_{!}_Please_leave_a_comment_about_004b30 = "{!} Please leave a comment ab
f1 10 0b 00
00401f5f e8 2c f9 CALL puts int puts(char * __s)
01 00
00401f64 48 8d 3d LEA RDI,[s_{?}_Comment_size:_004b307b] = "{?} Comment size: "
10 11 0b 00
00401f6b b8 00 00 MOV EAX,0x0
00 00
00401f70 e8 fb f9 CALL printf int printf(char * __format, ...)
00 00
00401f75 48 8b 45 f0 MOV RAX,qword ptr [RBP + local_18]
00401f79 48 89 c6 MOV RSI,RAX
00401f7c 48 8d 3d LEA RDI,[DAT_004b308e] = 25h %
0b 11 0b 00
00401f83 b8 00 00 MOV EAX,0x0
00 00
00401f88 e8 73 fb CALL __isoc99_scanf undefined __isoc99_scanf(undefin
00 00
int a; scanf("%lld", a);
のような実装になっているようです。a
が未初期化なので、その前に入力した数値のアドレスに好きな数字を入れられます。AAWになります。
libcもstaticで固定なので、__malloc_hook
に好きなガジェットを入れて終わりのはずですが、特に入れてほしそうな顔のガジェットがいません。/bin/sh
もいないのでそこそこきれいに掃除されています。
syscall
やpop rax
, pop rdx
はあるので、スタックをどこかに移してropしたいです。
こういう時に役立つのはlibc gotか適当なvtableです。どちらも今回はアドレス固定かつwritableですので、適当にセグるまで書き換えて、レジスタの状況を見てスタックを動かせるかを確認します。mov rsp
系のガジェットはrbx
, rcx
からいただくことができますが、良い値が入っていなそうです。ここに入るガジェットをつないでも良さそうですが、これもいまいちでした。作業を続けていると、rbp
にvtableを放り込んでくれるケースに遭遇しました。
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x6a8
$rbx : 0x00000000004de760 → 0x00000000fbad208b
$rcx : 0x480
$rdx : 0x00000000004dfdc0 → 0x0000000000000000
$rsp : 0x00007ffc7e673c98 → 0x0000000000427ee6 → <_IO_default_uflow+54> cmp eax, 0xffffffff
$rbp : 0x00000000004e0240 → 0x0000000000000000
$rsi : 0x00000000004de7e4 → 0x004e0ee000000000
$rdi : 0x00000000004de760 → 0x00000000fbad208b
$rip : 0xdeaddead
$r8 : 0x00000000004baf20 → 0x0002000200020002
$r9 : 0x12
$r10 : 0x00000000004b308e → 0x7d3f7b00646c6c25 ("%lld"?)
$r11 : 0x246
$r12 : 0x00000000004dfbe0 → 0x00000000004db940 → 0x00000000004b7fa8 → 0x61465f5856410043 ("C"?)
$r13 : 0x00000000004de760 → 0x00000000fbad208b
$r14 : 0x1
$r15 : 0xffffffffffffffc0
$eflags: [zero carry PARITY adjust sign trap INTERRUPT direction overflow RESUME virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007ffc7e673c98│+0x0000: 0x0000000000427ee6 → <_IO_default_uflow+54> cmp eax, 0xffffffff ← $rsp
0x00007ffc7e673ca0│+0x0008: 0x0000000000000000
0x00007ffc7e673ca8│+0x0010: 0x0000000000000000
0x00007ffc7e673cb0│+0x0018: 0x00007ffc7e6743d0 → 0x00007ffc7e6744d0 → 0x00007ffc7e6744f0 → 0x00007ffc7e674510 → 0x00007ffc7e674530 → 0x00007ffc7e674550 → 0x00007ffc7e674570
0x00007ffc7e673cb8│+0x0020: 0x0000000000412380 → <__vfscanf_internal+1856> cmp eax, 0xffffffff
0x00007ffc7e673cc0│+0x0028: 0x00000000004de760 → 0x00000000fbad208b
0x00007ffc7e673cc8│+0x0030: 0x0000000000000040 ("@"?)
0x00007ffc7e673cd0│+0x0038: 0x00007ffc7e674410 → 0x0000000000000000
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0xdeaddead
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "one_bullet", stopped 0xdeaddead in ?? (), reason: SIGSEGV
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ vm
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- one_bullet
0x0000000000401000 0x00000000004b3000 0x0000000000001000 r-x one_bullet
0x00000000004b3000 0x00000000004da000 0x00000000000b3000 r-- one_bullet
0x00000000004db000 0x00000000004e1000 0x00000000000da000 rw- one_bullet
0x00000000004e1000 0x00000000004e2000 0x0000000000000000 rw-
0x0000000001725000 0x0000000001748000 0x0000000000000000 rw- [heap]
0x00007ffc7e655000 0x00007ffc7e676000 0x0000000000000000 rw- [stack]
0x00007ffc7e712000 0x00007ffc7e716000 0x0000000000000000 r-- [vvar]
0x00007ffc7e716000 0x00007ffc7e718000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 --x [vsyscall]
バックトレース
__isoc99_scanf
__vfscanf_internal
_IO_default_uflow
__uflow <-- これが変わっている
vtable
はrdi
からrbp
に持ってきているようです。rdi
に入っているstdin
へのポインタなので、+0xd8
からvtable
を持ってきて、その中の+0x20
の関数を呼んでいます。あまり理由がわからなかったですが、2引数を受けて、さらに2引数を加えてvtableの関数を呼ぶため、rbp
を使う必要があったのでしょうか。
dump of assembler code for function _IO_default_uflow:
0x0000000000427eb0 <+0>: endbr64
0x0000000000427eb4 <+4>: push rbp
0x0000000000427eb5 <+5>: push rbx
0x0000000000427eb6 <+6>: mov rbx,rdi
0x0000000000427eb9 <+9>: sub rsp,0x8
0x0000000000427ebd <+13>: mov rbp,QWORD PTR [rdi+0xd8]
0x0000000000427ec4 <+20>: mov rdx,0x4dfdc0
0x0000000000427ecb <+27>: mov rax,0x4e0468
0x0000000000427ed2 <+34>: mov rcx,rbp
0x0000000000427ed5 <+37>: sub rax,rdx
0x0000000000427ed8 <+40>: sub rcx,rdx
0x0000000000427edb <+43>: cmp rax,rcx
0x0000000000427ede <+46>: jbe 0x427f08 <_IO_default_uflow+88>
0x0000000000427ee0 <+48>: mov rdi,rbx
0x0000000000427ee3 <+51>: call QWORD PTR [rbp+0x20]
とにかく、rbp
が都合のいいところを向いているので、ここにleave
を置けばvtable
のアドレスがスタックになります。あとはその下方にropペイロードが置いてあればよいです。vtable
の+0x20
直下などは別のところで別の関数が呼ばれて具合が悪いので、pivotを噛ませて一気に下の方に動かして解決しました。
leave
は、スタックがコントロールできる状況では良く利用していますが、今回のように一発で何とかする必要がある場合に出番があった記憶がないので、上手くガジェットがつながっておもしろかったです。