memo46

競プロの精進記録その他。

WaniCTF'21-spring writeup

waniCTFと呼ばれる、大阪大学 CTF サークル Wani Hackase が開催する初心者向けの CTF大会に参加しました。

全体60位/433人でした。

wanictf.org

結果は↓な感じで、ほとんどCTFしたことない自分からしたら割と頑張れたのでは、と思います。

ほとんどの分野でEasyぐらいまでしか解けなかったのですが、唯一cryptoは全完出来たので嬉しかったです!(競プロで殴れただけ)

f:id:bakamono1357:20210502200120p:plain f:id:bakamono1357:20210502200202p:plain

以下、writeupです

Crypto

[Begginer] Simple Conversion

convert.pyでフラグを整数に変換した後のoutput.txtが与えられるので、それを逆変換してくださいという問題でした。

from const import flag


def bytes_to_integer(x: bytes) -> int:
    x = int.from_bytes(x, byteorder="big")
    return x


print(bytes_to_integer(flag))
709088550902439876921359662969011490817828244100611994507393920171782905026859712405088781429996152122943882490614543229

int.to_bytes()を使って元に戻せばよいです。引数のlengthは適当にガチャして64を入れたらフラグが出てきました。

with open('output.txt') as f:
    n = int(f.read())
print(n.to_bytes(64, byteorder="big"))

フラグは、FLAG{7h1s_i5_h0w_we_c0nvert_m3ss@ges_1nt0_num63rs}

[Easy] Easy

前問と同じ、encrypt.pyで変換後のoutput.txtが与えられるから逆変換しろという問題。

with open("flag.txt") as f:
    flag = f.read().strip()


A = REDACTED
B = REDACTED

plaintext_space = "ABCDEFGHIJKLMNOPQRSTUVWXYZ_{}"

assert all(x in plaintext_space for x in flag)


def encrypt(plaintext: str, a: int, b: int) -> str:
    ciphertext = ""
    for x in plaintext:
        if "A" <= x <= "Z":
            x = ord(x) - ord("A")
            x = (a * x + b) % 26
            x = chr(x + ord("A"))
        ciphertext += x

    return ciphertext


if __name__ == "__main__":
    ciphertext = encrypt(flag, a=A, b=B)
    print(ciphertext)

読むと、文字'A'~'Z'を0~25に直して、その数字を$x$として$Ax+B (mod \space 26)$に変換していることが分かります。 しかし変換するのに使っている1次式の係数は隠されてしまっています。

そこで、output.txtを見ます。

HLIM{OCLSAQCZASPYFZASRILLCVMC}

最初の4文字はFLAGとなっているはずなので、ここから係数$A, B$がある程度分かるはずです。 実際は2つ分かればよくて、連立合同式を立てて計算してみると、$A=5, B=8$になることが分かります。 よってそれを元に適当に変換するコードを書けばよいです。

with open("output.txt") as f:
    S = f.read().strip()

A = 5
B = 8

def decrypt(s: str, a: int, b: int) -> str:
    res = ""
    for x in s:
        if "A" <= x <= "Z":
            x = ord(x) - ord("A")
            x = (x - b + 26) % 26
            for i in range(26):
                if a * i % 26 == x:
                    x = i
                    break
            x = chr(x + ord("A"))
        res += x

    return res

if __name__ == '__main__':
    res = decrypt(S, A, B)
    print(res)

フラグは、FLAG{WELCOMETOCRYPTOCHALLENGE}

[Hard] Can't restore the flag?

与えられたコマンドnc crt.cry.wanictf.org 50000でサーバーにアクセスすると、Modを入力するように要求されます。

siro53@MyComputer:~/CTF/waniCTF2021/crypto$ nc crt.cry.wanictf.org 50000
Mod > 

サーバーのコードは次のようになっています。(適宜コメントを入れています)

from Crypto.Util.number import bytes_to_long

with open("flag.txt", "rb") as f:
    flag = f.read()
flag = bytes_to_long(flag)

assert flag <= 10 ** 103

upper_bound = 300
while True:
    try:
        mod = int(input("Mod > ")) # modを入力する
        if mod > upper_bound: # mod は 300以下じゃないとだめ
            print("Don't cheat 🤪")
            continue

        result = flag % mod
        print(result) # flagをbytesから整数に直したものを、入力したmodであまりを計算し出力
    except Exception:
        print("Bye 👋")
        break

要するにフラグの300以下のmodを取った値なら教えてあげるから、そこからフラグの値を復元してねということです。

こんなのCRT(中国剰余定理)じゃん...ということで、手打ちでmodを埋め込んでCRTをすればフラグが分かります。完全に競プロ

コードは長いので省略します。実装はここを参考にしました

フラグはFLAG{Ch1n3s3_r3m@1nd3r_7h30r3m__so_u5eful}

[Normal] Extra

encrypt.pyとoutput.txtが与えられます。そこからフラグを復元しろという問題です。

from Crypto.Util.number import getPrime, bytes_to_long

with open("flag.txt", "rb") as f:
    flag = f.read()

p, q = getPrime(1024), getPrime(1024)
N = p * q
M = 2 * p + q
e = 0x10001

def encrypt(plaintext: bytes) -> int:
    plaintext = bytes_to_long(plaintext)
    c = pow(plaintext, e, N)

    return c


if __name__ == "__main__":
    c = encrypt(flag)

    print(f"{N = }")
    print(f"{M = }")
    print(f"{e = }")
    print(f"{c = }")
N = 22255382023772668851018179427844169178508638456713544208965498667359965716247243217931028270320680101854437928939452335472153643094266035953797432826168426002458800906764442624308120284177094975740468163835305872963635678413995878812492729432260346481442092245748885202467992527408086207041964831622724073720751839241897580988210971776031098476500998975223039782371635291859483569580516707907602619018780393060215756966917504096971372578145138070121288608502379649804953835336933545368863853793291348412017384228807171466141787383764812064465152885522264261710104646819565161405416285530129398700414912821358924882993
M = 455054308184393892678058040417894434538147052966484655368629806848690951585316383741818991249942897131402174931069148907410409095241197004639436085265522674198117934494409967755516107042868190564732371162423204135770802585390754508661199283919569348449653439331457503898545517122035939648918370853985174413495
e = 65537
c = 17228720052381175899005296327529228647857019551986416863927209013417483505116054978735086007753554984554590706212543316457002993598203960172630351581308428981923248377333772786232057445880572046104706039330059467410587857287022959518047526287362946817619717880614820138792149370198936936857422116461146587380005750298216662907558653796277806259062461884502203484610534512552197338982682870358910558302016481352035443274153409114492025483995668048818103066011831955626539382173160900595378864729936791103356604330731386911513668727994911216530875480647283550078311836214338646991447576725034118526046292574067040720093

問題文は「いつものRSA?」と書いていて、実際encrypt.pyを眺めるとそんな感じのことをしています。

しかし少し異なるのは$M=2p+q$が与えられている点です。これと$N=pq$を連立して2次方程式を解けば、解の公式から$p$の候補が2つに絞れます。

よって$p$を2通り試してwikiを参考にそこから復号すればよいです。

実装にあたり先程のPythonACL記事を参考にしています。

from Crypto.Util.number import long_to_bytes
from math_lib import *

N, M, e, c = map(int, input().split())

def isqrt(n: int) -> int:
    ok = 0
    ng = n
    while ng - ok > 1:
        mid = (ok + ng) // 2
        if mid * mid <= n:
            ok = mid
        else:
            ng = mid
    
    return ok

s = isqrt(M*M-8*N)
assert (s * s == M*M-8*N)

p = (M + isqrt(M*M - 8*N)) // 4
# p = (M - isqrt(M*M - 8*N)) // 4
q = N // p

assert (p * q == N)

phi = lcm(p-1, q-1)
d = inv_mod(e, phi)
m = pow(c, d, N)

print(long_to_bytes(m))

フラグは、FLAG{@n_ex7ra_param3ter_ru1n5_3very7h1n9}

[Very Hard] OUCS

与えられたコマンドnc oucs.cry.wanictf.org 50010でサーバーにアクセスすると、次の5つのコマンドを使えます。 f:id:bakamono1357:20210502173524p:plain

サーバーのコードも与えられています。(適宜コメントを入れています)

import random

from Crypto.Util.number import bytes_to_long, getPrime, long_to_bytes

from const import description, flag, logo

# nc oucs.cry.wanictf.org 50010

class OkamotoUchiyamaCryptoSystem:
    def __init__(self, bits: int):
        p, q = getPrime(bits), getPrime(bits)
        n = p * p * q

        while g := random.randrange(2, n):
            if pow(g, p - 1, p * p) != 1:
                break
        h = pow(g, n, n)

        self.p = p
        self.n = n
        self.g = g
        self.h = h

    def encrypt(self, plaintext: bytes) -> bytes:
        plaintext = bytes_to_long(plaintext)
        n, g, h = self.n, self.g, self.h
        r = random.randrange(2, n)

        ciphertext = pow(g, plaintext, n) * pow(h, r, n) % n
        ciphertext = long_to_bytes(ciphertext)

        return ciphertext

    def decrypt(self, ciphertext: bytes) -> bytes:
        ciphertext = bytes_to_long(ciphertext)
        p, g = self.p, self.g

        a = (pow(ciphertext, p - 1, p ** 2) - 1) // p
        b = (pow(g, p - 1, p * p) - 1) // p
        b_ = pow(b, -1, p)
        plaintext = a * b_ % p
        plaintext = long_to_bytes(plaintext)

        return plaintext

    def get_publickey(self) -> tuple[int, int, int]:
        return self.n, self.g, self.h

# フラグは毎回異なる

if __name__ == "__main__":
    print(logo)
    cipher = OkamotoUchiyamaCryptoSystem(bits=1024)

    while True:
        print()
        print(description)
        while not (choice := input("> ")) in "12345":
            print("Invalid choice.")

        choice = int(choice)

        # 1. Encrypt the flag
        # フラグを暗号化して、暗号文(int)を返す
        if choice == 1:
            ciphertext = cipher.encrypt(flag)
            ciphertext = bytes_to_long(ciphertext)
            print(f"{ciphertext = :#x}")

        # 2. Encrypt
        # 平文(int)を入れると、暗号文(int)が出てくる
        elif choice == 2: 
            print("Enter your plaintext")
            plaintext = int(input("> "), 0)
            plaintext = long_to_bytes(plaintext)

            ciphertext = cipher.encrypt(plaintext)
            ciphertext = bytes_to_long(ciphertext)
            print(f"{ciphertext = :#x}")

        # 3. Decrypt
        # 暗号文(int)を入れると、平文(int)が出てくる
        # ただし平文がフラグと一致した場合、何も表示されない
        elif choice == 3:
            print("Enter your ciphertext")
            ciphertext = int(input("> "), 0)
            ciphertext = long_to_bytes(ciphertext)

            # ... except for the flag
            plaintext = cipher.decrypt(ciphertext)
            if flag == plaintext:
                print("Decryption succeeded, but we won't tell you the result :P")
                continue
            plaintext = bytes_to_long(plaintext)
            print(f"{plaintext = :#x}")

        # 4. Show public key
        elif choice == 4:
            n, g, h = cipher.get_publickey()
            print(f"{n = :#x}")
            print(f"{g = :#x}")
            print(f"{h = :#x}")

        # 5. Exit
        else:
            print("Bye :)")
            break

これを用いてフラグを得る問題です。

とりあえず見たことない計算をしているので、クラス名を頼りに岡本 内山 暗号とかで検索してみます。するとWikipediaのページがヒットします。

EPOC (暗号) - Wikipedia

wikiの式とserver.pyをにらめっこして比べると、どうやらこの暗号で間違いないようです。秘密鍵素数$p, q$なので、これらが分からないことには復号できません。

しばらく考えましたが素数$p, q$が分からないことにはどうしようもないので、色々writeupを漁ってみます。

すると、どうやら岡本・内山暗号は準同型暗号であることを知りました。(そういえば問題文にも「OUによるHomomorphicなCryptoSystemです」と書いてあったな...とwriteupを書きながら気づきました)

この暗号は特に、暗号化する関数を$E$とすると、平文$a, b$に対し$E(a)E(b) = E(a+b)$が成り立つようです。これはめちゃくちゃ使えそうです。

実際、次のような手順で解けます。

  1. コマンド1で、フラグを暗号化した値$C$を教えてもらう。
  2. コマンド2で、適当に決めた定数$T$(私は1にしました)を暗号化した値$E_T$を教えてもらう。
  3. コマンド4で、公開鍵$n, g, h$を教えてもらう。
  4. $C * E_T (mod \space n)$を計算し、それをコマンド3でdecryptしてもらう。
  5. 4で得た値から定数$T$を引いた値がフラグ!

よって、5で得た値をbytes型に直すことでフラグが得られました。

フラグは、FLAG{OU_d0es_n0t_repre53nt_Osaka_University_but_Okamoto-Uchiyama}

Forensics

[Beginner] presentation

zipを解凍すると中にはpowerpointファイルが入っています。開いてみるとこうなっています。 f:id:bakamono1357:20210502175933p:plain よってこの四角い図形をどければフラグが分かります。 f:id:bakamono1357:20210502180115p:plain

[Very Hard] MixedUSB

与えられたMixedUSB.imgというイメージファイルをダウンロードします。とりあえずマウントしようとしてみると失敗します。

このイメージファイル壊れてそう...という気持ちになるのでなんとか中身を見る方法はないか調べると、TestDiskというツールがあることを知りました。

qiita.com

より詳しい使い方はこのサイトがわかりやすかったです。rootまでイメージファイルを移動させてからやらないといけなくて面倒だった。

blog.goo.ne.jp

f:id:bakamono1357:20210502181057p:plain
こんな感じで起動
f:id:bakamono1357:20210502181129p:plain
こんな画面になる

すると、dummyファイルが沢山ある中にflag.txtを発見することが出来ました。これを適当なディレクトリにコピーして中身を見ればフラグゲットです。 f:id:bakamono1357:20210502181343p:plain

フラグはFLAG{mixed_file_allocation_table}

[Easy] secure document

zipファイルにはパスワード付きのzipファイルとpassword-generatorというテキストファイルが入っていました。

::the::
Send, +wani
return

::password:: Send, +c+t+f return

::for:: Send, {home}{right 3}{del}1{end}{left 2}{del}7 return

::today:: FormatTime , x,, yyyyMMdd SendInput, {home}{right 4}_%x% return

::is:: Send, _{end}{!}{!}{left} return

:*?:ni:: Send, ^ac{Esc}{home}password{:} {end} return

zipファイルの中にはflag.jpgが入っており、パスワードを得ることが課題のようです。

とりあえずpassword-generatorが何なのか理解する必要があるので、必死にググると(自分はFormatTimeが関数っぽいなと思ってこれでググりました。)これはAutoHotKeyというキーボードの割当をカスタマイズするための言語で書かれたスクリプトであることが分かります。

問題には次のような説明文があったので、このスクリプトを動かしてメモ帳などに「the password for today is nani」とタイプすると、password: Wan1_20210502_C7F!na!と変換されます。

日付の部分はzipファイル名と同じ20210428に直してセミコロン以降をパスワードに入力してみると、zipファイルが空きました。

フラグは↓

f:id:bakamono1357:20210502182338p:plain

ブラウザのURL入力するところで試していたらスクリプトが変な挙動をして一生パスワードが出てこず、大量に時間を溶かしてしまった...

[Normal] slow

zipファイルには.wavファイルが入っています。audacityとかで波形を見てみても何もわかりませんでした。

色々writeupを漁ってみると、どうやらSSTV(低速度走査テレビ)ってやつに聞かせてみる方法もあるらしい

実際RX-SSTVをインストールして試してみると...

f:id:bakamono1357:20210502183144p:plain

...うわあああなんか浮かび上がってきたあああああああああああ

というわけでフラグは、FLAG{encoded_by_slow-scan_television}でした。

(ちなみに音は結構不快なので注意)

Misc

[Beginner] binary

sample.pyと01だけ書かれたbinary.csvが与えられるので複号せよという問題。

s = "WANI"
bits = []
for i in range(len(s)):
    val = s[i]
    for j in range(8):
        b = (ord(val) >> (7 - j)) & 0x01
        bits.append(b)

print(bits)

s = "" c = 0 for i in range(len(bits)): val = int(bits[i]) c = (c << 1) | val if i % 8 == 7: s = s + chr(c) c = 0

print(s)

適当に復元する。
bits = []

with open('binary.csv') as f: for line in f: line = line.rstrip('\r\n') bits.append(int(line))

s = "" c = 0 for i in range(len(bits)): val = int(bits[i]) c = (c << 1) | val if i % 8 == 7: s = s + chr(c) c = 0

print(s)

フラグは、FLAG{the_basic_knowledge_of_communication}

Pwn

[Beginner] 01 netcat

与えられたコマンドnc netcat.pwn.wanictf.org 9001で問題サーバにアクセスするとlsコマンドを打てと言われます。

仰せのままに実行すると当該ディレクトリにflag.txtがあることがわかるので、cat flag.txtを実行することでフラグを得ます。 f:id:bakamono1357:20210502190702p:plain

[Easy] 03 rop machine easy

nc rop-easy.pwn.wanictf.org 9003で問題サーバにアクセスします。 f:id:bakamono1357:20210502191058p:plain rop machineという予め用意されたプログラムを使ってROP (Return Oriented Programming)を行い、system("/bin/sh")を実行する問題のようです。

ROPを僕は知らなかったので色々調べました。↓のwriteupがわかりやすかったです。

blog.8f-nai.net

要するにこのrop machineというやつは命令列を上から順に実行します。pop rdi; retという命令は、stackのtopにある値をpopしてrdiレジスタにセットし、次にstackのtopにある値をリターンアドレスとみなして飛びます。

よってstackに"/bin/sh"のアドレス、system関数のアドレスを順にpushしておき、pop rdi; retを実行することでsystem("/bin/sh")が実行できます。

f:id:bakamono1357:20210502192029p:plain
フラグは↑

[Easy] 04 rop machine normal

全問に加えて実行できる命令が増えています。

目標はexecve("/bin/sh", 0, 0)を実行することであり、execveのsyscall番号が0x3bであることがわかっています。

f:id:bakamono1357:20210502192245p:plain

やることは前問とほぼ同じで、第1引数に渡す値をrdiレジスタにセット、第2引数に渡す値をrsiレジスタにセット、第3引数に渡す値をrdxレジスタにセットし、syscall命令に渡すsyscall番号をraxレジスタにセットし、実行してあげればよいです。

f:id:bakamono1357:20210502192728p:plain
渡す命令順はこんな感じ

フラグは、FLAG{now-you-can-call-any-system-calls-with-syscall}

reversing

[Beginner] secret

zipファイルには実行ファイルのみが入っています。

実行権限を与えて実行してみると、パスワードを要求されます。 f:id:bakamono1357:20210502193411p:plain

そこでヒントにある表層解析というのを試してみると、

$ strings -a secret > out
f:id:bakamono1357:20210502193518p:plain

なんかwani_is_the_coolest_animals_in_the_world!というのがパスワードっぽく見えます。実際正しくて、これを入れるとフラグが出てきました。 f:id:bakamono1357:20210502193632p:plain

[Easy] execute

zipを解凍するとファイル構成は次のようになっています。 f:id:bakamono1357:20210502193803p:plain 説明文に「コマンドを間違えてソースコードを消しちゃった!」とある通り、Makefileを見てみると

all:
        gcc --version > version.txt
        gcc -S main.c
        gcc -shared -o libprint.so print.c
        rm main.c
        rm print.c

ガッツリ消してて笑う

main.sとlibprint.soというやつがあるなら普通になんらかの方法でコンパイルできるんじゃないか?と思い必死にググると、ありました。

sleepy-yoshi.hatenablog.com

これを参考に次のようにコンパイルして実行すると、フラグを得ることが出来ました。

$ export LD_LIBRARY_PATH=.
$ gcc -I./ -L./ main.s -o main -lprint
$ ./main
Flag is "FLAG{c4n_y0u_execu4e_th1s_fi1e}"

Web

[Beginner] fake

https://fake.web.wanictf.org/ というサイトからフラグを得る問題です。デベロッパーツール開けばなんかあるでしょと思い開くと、謎のhtmlファイルがありました。 f:id:bakamono1357:20210502194710p:plain これを開くとフラグを得ます。 f:id:bakamono1357:20210502194748p:plain

[Easy] Wani Request 1

https://request1.web.wanictf.org/

↑のwebサイトが与えられています。

自前のサーバを用意もしくはRequestBinなどのサービスを使えと言われているので、RequestBinを使ってみます。

与えられたサイトに発行されたURLを入れて送信し、httpヘッダーを見るとそれっぽいリンクがあります。 f:id:bakamono1357:20210502195418p:plain 実際アクセスするとフラグをゲットできます。 f:id:bakamono1357:20210502195445p:plain