Home mocactf crypto ML
Post
Cancel

mocactf crypto ML

mocactf

初のキャンプしながらCTFです。

二度と同じような経験ができないようなものを体験できたのでよかったです。とりあえず、日本で寝袋買います…

ML

状況

始めに3つのファイル(ciphertext, test.py, model.onnx)が渡されます。

test.pyはmodel.onnxの起動法があるので省略で、目標はflagがmodel.onnxに入力され、出力がciphertextにあるのでflagをcrypto的に求めろということらしいです。

initial alanysisとして、model.onnxに色々入力を渡した結果は

  • 16bytesごとに暗号化される
  • 出力は符号によってbitが決定される
  • 決定的(tweakが存在しない)

であることがまずわかります。

ここで、rev, miscではなくcryptoのチャレンジであることから何かしらの暗号が実装されていそうです。

今思いつくものとしては16bytesの暗号のAESや独自暗号の線がありそうです。

解析

なので、TSG CTF 2020 onnxrev write-up (github.com)を参考にonnxをパースしていきます。

![image-20240918194209927](assets/img/ctf/2024-09-18-moca/image-20240918194209927.pngimage-20240918194230902約1000行の処理からなるものが確認でき、更に似たような処理が13 or 14回繰り返されていることからブロック暗号系統であることが推測できます。

image-20240918194334937

ただ、この時点で、TSGCTF2024みたいに各処理結果をパースするコードを書くには膨大すぎるので、他の問題に手を付けたり、moca2024を探索していました。

ヒント

さて、途中で与えられたヒントとしてremember, this is crypto! We never design our own cryptoとあるのでほぼAES256で確定な状況です。

なので、処理の概要をつかむためにsboxがハードコードされている部分を探すことにしましたが、すべての定数を見てみたところSboxらしきものはなくどうしようもなく終わりました。

帰りの飛行機で暇だったので適当に検索しているとONNXモデルに出力ノードを追加する方法 #Python3 - Qiitaという変数を書くだけで処理を追加してくれる神機能がありこれ使えるやんけ…ってなりました

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import onnx
from util import *
from onnxruntime import InferenceSession
import numpy as np
from Crypto.Cipher import AES


def seach():
    model = onnx.load("../model/model.onnx")

    for node in model.graph.node:
        if node.op_type == "Identity" or node.op_type == "Constant":
            continue
        yield node.output[0]

for out in seach():
    input_path = '../model/model.onnx'
    output_path = 'model.onnx'
    input_names = ['input']
    output_names = [out]
    onnx.utils.extract_model(input_path, output_path, input_names, output_names)
    
    sess = InferenceSession("./model.onnx")
    inp = b"\x00"*16
    inps = [inp[i:i+16] for i in range(0, len(inp), 16)]

    inp = []
    for i in inps:
        inp += bin_list(i)

    x = np.concatenate(inp).reshape(-1, 16*8)

    outs = sess.run(None, {"input": x})
    print(out, outs)
    input()

このような感じで定数と初期化を除くノードを見てみると、いきなりb"\x00"*16でない部分

image-20240918200117082

が見つかり、これはAESのはじめのAdd Roundkeyに該当することがわかります。

ということでAES256はroundkey[0]とroundkey[1]が分かれば、masterkeyもわかるのですべてがうまくいきます。

続けて処理を見ていくと以下のようになり、AESの1roundが見て取れます。ここからroundkey[1]を求めていきます。

image-20240918200300680

1
2
roundkey0 = b'p{ylwt_dnht_xeom'
roundkey1 = b'ecdt_o__1und_loo'

ただ、このAESの処理は転置されていることに気を付ければ、b"pwnx{they_told_me_1_could_not_do"という文字がAESのmasterkeyになります。

1
2
3
4
from Crypto.Cipher import AES
aes = AES.new(b"pwnx{they_told_me_1_could_not_do",AES.MODE_ECB)
print(aes.decrypt(open("ciphertext","rb").read()))
# b'here the last piece of the flag and the rest is in the key :) _aes_with_ml_so_1_did_it_anyway}\x02\x02'

よってplaintextとkeyの情報を合わせるとpwnx{they_told_me_1_could_not_do_aes_with_ml_so_1_did_it_anyway}がflagになります。

This post is licensed under CC BY 4.0 by the author.