rajyanのメモ帳

競技プログラミング、CTFなどに関することを適当にまとめます

SECCON Beginners CTF 2020 writeup

目次

はじめに

SECCON Beginners CTF 2020にふ゛ち (@betit0919) | Twitter, まいこー (@micheeeeell1001) | Twitterと3人で出ました。久しぶりのCTFとても楽しかったです。

f:id:latikuchi:20200524144215p:plain f:id:latikuchi:20200524144155p:plain 解けた問題はこんな感じで異常に偏っています。自分はCryptoとRev解いて余った時間でPwnやる予定だったのですが、Revが久しぶりでなんも覚えておらず、Pwnには手をつけられず……。 Rev, Pwnを練習しなくてはいけないなあと感じました。あと、久しぶりにPython 使ってそこにもめちゃくちゃ苦労したので、Pythonにももっと慣れていきたいです。

解いた問題

2020/05/26 追記 チームメイトが解いた問題の解法はこちらです betit0919.hatenablog.com

Noisy equations

概要

  • coeffs (接続するたびにランダム)とanswersが与えられる
  • flag長Nとすれば、coeffs はN×Nの行列answersはN×1のベクトル
  • これ以外にN×1のランダムなベクトルが設定されているが、seedは固定
  • A b + r = x (A: coeffs, b: flag, r: randbits, x: answers) という式が成り立つ

解法

  • seed固定なので二回取ってきて差分を取ると(A_1-A_2)b = x_1-x_2 という感じでランダム部分が消える
  • 逆行列をかけると求めたいbが求まる
from pwn import *
from Crypto.Util.number import *
import numpy as np
import json

io1 = remote("noisy-equations.quals.beginners.seccon.jp", 3000)
coeff1 = json.loads(io1.recvline())
ans1 = json.loads(io1.recvline())
io2 = remote("noisy-equations.quals.beginners.seccon.jp", 3000)
coeff2 = json.loads(io2.recvline())
ans2 = json.loads(io2.recvline())

N = len(ans2)
subcoeff = [[0 for i in range(N)] for j in range(N)]
subans = [0] * N
for i in range(N):
    for j in range(N):
        subcoeff[i][j] = int(coeff1[i][j]) - int(coeff2[i][j])
for i in range(N):
    subans[i] = int(ans1[i]) - int(ans2[i])

A = np.array(subcoeff).astype(np.float64)
b = np.array(subans).astype(np.float64)
flag_ascii = np.linalg.inv(A).dot(b)
flag = ''
for c in ans_ascii:
    flag += chr(round(c))
print(flag)

ctf4b{r4nd0m_533d_15_n3c3554ry_f0r_53cur17y}

入力をうまく配列に入れたり、逆行列をかけたり……Python力がなさすぎて、解法思いついてから実装するまでにめっちゃ時間かかった。

RSA Calc

概要

  • nc で指定されたポートに繋ぐと、Sign, Exec, Exit を選ぶインターフェイスが与えられる
  • Signを選択すると入力したdataに対するSignatureが返される
  • Execを選択するとdataと signatureを入力させられ、dataに対するsignatureが正しい時のみその後の処理が実行される
  • SignatureはRSA署名でN, eも普通でp,qが求められる感じではない
  • dataはExecできると','で分解され、'+-*/'などの演算子と数字による演算が行われる
  • 'F'の入力があるとval==1337が実行されtrueならフラグが出力される
  • Sign では'F', '1337'を含む文字列を入力するとsignatureを返さないようになってる

解法

  • いかにして'1000,337,+,F'とか'1337,F'みたいな実行結果が1337になるような実行文のRSA署名を得るかが問題
  • m='1337,F'としたとき、例えば2mとかの署名は返してくれるので、これをうまく使う
  • sign_m=md(mod N) = (2m)d/2d (mod N)みたいな感じ
  • 実際には'a'でやった
from Crypto.Util.number import *
import binascii as ba
from pwn import *

io = remote('rsacalc.quals.beginners.seccon.jp', 10001)
io.recvuntil('N: ')
N = int(io.recvline())

m = b'1337,F'
ma = long_to_bytes(bytes_to_long(m)*bytes_to_long(b'a'))

io.sendlineafter('> ', '1')
io.sendlineafter('data> ', ma)

io.recvuntil('Signature: ')
sig_ma = io.recvline()[0:-1]
print(sig_ma)

io.sendlineafter('> ', '1')
io.sendlineafter('data> ', 'a')

io.recvuntil('Signature: ')
sig_a = io.recvline()[0:-1]
print(sig_a)

sig_m = hex(bytes_to_long(ba.unhexlify(sig_ma)) * inverse(bytes_to_long(ba.unhexlify(sig_a)),N) % N)

io.sendlineafter('> ', '2')
io.sendlineafter('data> ', m)
io.sendlineafter('signature> ', sig_m)
print(io.recvline())

ctf4b{SIgn_n33ds_P4d&H4sh}

mask

概要

  • 実行すると引数にflagを入力してくれと言われる
  • flag を入れて実行すると謎の文字列が2つ返り入力が正しくないと言われる

解法

  • radare2+r2dec でCの疑似コードを出してみる
while (eax < var_d4h) {
        eax = var_d8h;
        rax = (int64_t) eax;
        eax = *((rbp + rax - 0xd0));
        eax &= 0x75;
        edx = eax;
        eax = var_d8h;
        rax = (int64_t) eax;
        *((rbp + rax - 0x90)) = dl;
        eax = var_d8h;
        rax = (int64_t) eax;
        eax = *((rbp + rax - 0xd0));
        eax &= 0xffffffeb;
        edx = eax;
        eax = var_d8h;
        rax = (int64_t) eax;
        *((rbp + rax - 0x50)) = dl;
        var_d8h++;
        eax = var_d8h;
    }

片方は&= 0x75, もう一方は&= 0xffffffebで入力の各文字mask処理が行われて、それが出力されている

  eax = var_d4h;
    rax = (int64_t) eax;
    *((rbp + rax - 0x90)) = 0;
    eax = var_d4h;
    rax = (int64_t) eax;
    *((rbp + rax - 0x50)) = 0;
    rax = &s1;
    puts (rax);
    rax = &var_50h;
    puts (rax);
    rax = &s1;
    eax = strcmp (rax, "atd4`qdedtUpetepqeUdaaeUeaqau");
    if (eax == 0) {
        rax = &var_50h;
        eax = strcmp (rax, "c`b bk`kj`KbababcaKbacaKiacki");
        if (eax == 0) {
            puts ("Correct! Submit your FLAG.");
        }

それぞれ"atd4`qdedtUpetepqeUdaaeUeaqau"と"c`b bk`kj`KbababcaKbacaKiacki")との比較が行われる。

ということで0x75, 0xffffffebのmaskのビットが立っているところはflagのビットの状態がわかるので、2つのorをとってflagを逆算します。

a = 'atd4`qdedtUpetepqeUdaaeUeaqau'
b = 'c`b bk`kj`KbababcaKbacaKiacki'
mask1 = 0x75
mask2 = 0xffffffeb
ans = ""
for i in range(len(a)):
    ans += chr(ord(a[i])&mask1 | ord(b[i])&mask2)
print(ans)

ctf4b{dont_reverse_face_mask}

dont_reverse_face_mask

とか、問題文のところの

The price of mask goes down. So does the point (it's easy)!

とか、ユーモアが効いてますね。

考察した問題

yakisoba

  • revしてみると、一文字ずつ判定しているところがあってそこがカオス
  • ヒントにもある通り自動化しないとやってられなそう
  • 1文字ずつ正しいかどうか判別がつくのでbruteforceできそうだが、実装できなかった

ghost

  • ghostが書いたscriptということで、ghostscript->PostScriptで書かれている
  • interpreter使ったり、実行したりしながら試したが間に合わず

2020/05/26 追記 コンテスト後に解きました

概要

  • postscriptで書かれている
  • ghostscriptを入れて実行すると入力を求められる
  • 文字列と同じ長さの数列が返る
  • 後ろの文字を変えてもそれより前は変わらない

解法

  • 1文字ずつ決まるのでbruteforce
from pwn import *

output = [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]
s = 'ctf4b{'

len_o = len(output)

while len(s) != len_o:
    for c in string.printable:
        p = process('gs chall.gs', shell=True)
        p.sendlinethen('details.\n', s+c)
        res = p.recvline(keepends=False).split(b' ')[0:-1]
        p.close()

        if list(map(int, res)) == output[:len(s)+1]:
            s += c
            print(s)
            break

ctf4b{st4ck_m4ch1n3_1s_4_l0t_0f_fun!}

おわりに

時間かけた割に全然解けてなくないですか??? もっとrev, pwn問解けるようになりたい!!!