小さな便利テクを思いついたのでメモしておきます。 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もいないのでそこそこきれいに掃除されています。

syscallpop 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 <-- これが変わっている

vtablerdiから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は、スタックがコントロールできる状況では良く利用していますが、今回のように一発で何とかする必要がある場合に出番があった記憶がないので、上手くガジェットがつながっておもしろかったです。