ふるつき

v(*'='*)v かに

CPCTF 2024 writeup

久しぶりにCTFに参加しました。cryptoのある程度以上の難易度の問題を解いたら満足してしまって、昔CTFを開催していたときに「この人いつも特定のジャンルの問題だけ解いて帰るな~」と思って見ていた人の気持ちってこういうことだったんだろうな、と思いました。

取り組んだ問題は面白かったし、解いていて楽しかったです。

[Crypto] AAAA

ほぼ全てがマスクされたOpenSSH形式のRSA秘密鍵を見せてもらえます。わかるのは鍵の一部のBASE64TOKYO+INSTITUTE+OF+TECHNOLOGY+DIGITAL+CREATORS+CLUB+TRAPAAAA を含むということだけ。

しかしそれ以外にも、公開鍵と暗号文、それから ed / \phi(n) を貰えます。

RSA e, d には  ed \equiv 1 \mod \phi(n)という関係があり、適当な整数 kを用いて ed = 1 + k\phi(n)と書いて両辺 \phi(n)で割ると、与えられている ed / \phi(n)はほとんど kと一致する事がわかります。

今回の問題設定では eが大きく、ほとんど  nくらいの大きさがあることと合わせて考えると

 ed = 1 + k\phi(n) = 1 + k(n - s + 1)

について \mod eを取ると 1 + k(n - s + 1) \equiv 0 \mod eという式が立ち*1、未知数は sのみで、 eの半分程度の大きさになるはずなので、この剰余多項式の小さい根を求めれば sが求まります。 sがわかれば \phi(n)が求められるのであとは dを求めて暗号文を復号すればよいです。

問題に与えられていた文字列は dbase64結果のようでした

from base64 import b64decode, b64encode

e = 506184338...
n = 12339326...
k = 6649705358687...

PR.<x> = PolynomialRing(Zmod(e))
f = 1 + k*n - k*x + k
f = f.monic()

s = int(f.small_roots(X=floor(3 * n**0.5), beta=0.5)[0])
phi = n - s + 1

d = int(pow(e, -1, phi))
c = 91909831...

m = pow(c, d, n)
print(bytes.fromhex(hex(m)[2:]))

[Crypto] CES

以下のいずれかを合計2回だけ行えるサーバに接続できます。

  1. 任意の平文をCES-CBCで暗号化する
  2. フラグをCES-CBCで暗号化する

CESはAESに手を加えたブロック暗号で、subbytesの実装が次のようになっていること以外はオリジナルのAESの実装と同じです。

def concat(a, b):
    tmp = int("{:064b}".format(a) + "{:064b}".format(b), 2)
    tmp_bin_list = [int(n) for n in bin(tmp)[2:]]
    irreducible_poly = [1, 0, 0, 0, 1, 1, 0, 1, 1]
    res = np.polydiv(tmp_bin_list, irreducible_poly)[1]
    res = list(map(int, res))
    res = [x % 2 for x in res]
    res = "".join(map(str, res))
    res = int(res, 2)
    return res

def sub_bytes(s, round_key):
    for i, line in enumerate(s):
        for j, x in enumerate(line):
            k = bytes_to_long(matrix2bytes(round_key))
            res = concat(k, s[i][j])
            assert concat(k, res) == s[i][j]
            s[i][j] = res

    return s

このconcat, subbytes がどういう実装になっているのかはわかりませんでしたが、assert の条件をながめていると「このsubbytesの実装は線形な変換になっているのでは」と思いました。

AESの各種ステップのうち非線形な変換はsubbytesだけですから、この実装が線形になればCESによる暗号化は線形な変換になります。つまり、平文 mを鍵 kで暗号化したとき、暗号文 cの各ビットが m kの何らかのビットのXORで表せます。鍵の足し方は平文によりませんから、異なる平文 m, m'を同一の鍵 kで暗号化した c, c'について m \oplus m' = c \oplus c'となるはずです。

ということを実験したらまさにそうなったので、サーバの実装では1, 2で同じ鍵が使われることを利用して、2で得た暗号化された平文を1で作った適当な平文の暗号文を用いて復号するコードを書きました

from ptrlib import Socket, chunks, xor
import ast
from problem import bytes2matrix, matrix2bytes, N_ROUNDS, inv_shift_rows, inv_mix_columns

def my_decrypt(ciphertext):
    state = bytes2matrix(ciphertext)

    for i in range(N_ROUNDS - 1, -1, -1):
        inv_shift_rows(state)
        if i > 0:
            inv_mix_columns(state)

    plaintext = matrix2bytes(state)
    return plaintext


sock = Socket("ces.web.cpctf.space 30008")
sock.sendlineafter("option: ", "2")
f_enc = ast.literal_eval(sock.recvlineafter("flag: ").decode()) # type: bytes
iv = ast.literal_eval(sock.recvlineafter("iv: ").decode())

sock.sendlineafter("option: ", "1")
payload = "A" * len(f_enc)
sock.sendlineafter("encrypt: ", payload)
enc = ast.literal_eval(sock.recvlineafter("message: ").decode()) # type: bytes
iv2 = ast.literal_eval(sock.recvlineafter("iv: ").decode())

flag = b""

for c1, c2 in zip(chunks(f_enc, 16), chunks(enc, 16)):
    c_wo_k = my_decrypt(xor(c1, c2))
    # c_wo_k = (f_i ^ iv1) ^ (m_i ^ iv2)

    flag_i = xor(xor(c_wo_k, xor(b"A" * 16, iv2)), iv)
    flag += flag_i
    print(flag)

    iv = c1
    iv2 = c2

AESがそれっぽく改造されており一見難問にみえるものの、assertで性質をさりげなく表現することで難易度が調節されており、良い問題だったと思います。

*1:s = p + qとおいてます

2023年に読み始めて面白かったWeb小説

kotatsugame リスペクトです。2022年版もある

furutsuki.hatenablog.com

途中までしか読んでなくても、その途中までが面白かったら載せています。2022年に読み始めてても↑に書き忘れてたら書いてるかも

こうして見返すと新規に読み始めているのハーメルンばかりですね

Terrapin-Attack メモ

qiita.com

の記事とコメントのほうが詳しく解説してた


ちょっと興味があって、先日公開されたSSHのIntegrityを破る攻撃であるところのTerrapin-Attack (https://terrapin-attack.com) について調べて社内勉強会で発表したりしていました。せっかくなので日本語のメモを残しておきます。

概要

中間者がパケットをDROPできるというのが攻撃。サーバ・クライアントはそのパケットがDROPされたことに気が付けないので、Integrityが破られたということになる。1つパケットをDROPするので辻褄合わせのために1つ偽造しておくことになる。

原理

前提: サーバ・クライアントはメッセージ番号のカウンタを持っていて、MACの計算に使われたりする

これは論文の図がわかりやすいと思う。サーバ・クライアントはそれぞれ今いくつメッセージを送っていて、いくつのメッセージを受け取っているかをカウンタとして持っている*1。論文中ではそれぞれSnd, Rcvという名前になっていて、基本的にはサーバのSnd = クライアントRcvで逆も然り。

暗号方式によっては、このカウンタがMACの計算に使われる*2。Terrapin-Attackでは、このカウンタの値をサーバ・クライアント間でずらしておくことで、一個メッセージをDROPしたときにSnd, Rcvの値がずれないようにする=MACの計算が正常に行われるようにすることで、メッセージのDROPに気が付かせない

攻撃: IGNOREパケットを偽造する

攻撃者はSecure Connectionの確立前(=鍵共有の完了前)にIGNOREパケット*3を偽造する。図ではクライアントに対して送り付けているけど逆でもよくて、どのパケットをDROPしたいかによる。

Secure Connectionの確立前なので通信はTCP平文で行われていて、中間者がパケットの盗聴・偽造が可能。これにより、サーバのSndに対してクライアントのRcvが1多い状態になる。

攻撃: パケットをDROPする

攻撃者はSecure Connectionの確立後の最初のパケットをDROPする。図ではサーバ→クライアントのパケットをDROPしている。これにより、クライアントのRcvは上がらないがサーバのSndが1上がる。先にIGNOREパケットを偽造しておいたお陰で、ここでサーバのSndとクライアントのRcvの値が揃う。

普通はパケットが1つ消えると、次のパケットの送信でSnd, Rcvの値がずれてMACの検証に失敗して気がつけるが、次のパケットのMACの検証は成功するのでサーバ・クライアントはパケットの消失に気がつけず、攻撃が成立した。

脅威

で、Secure Connection の確立後のパケットがDROPできるのがどれだけ脅威なのか。論文によると、Secure Connection確立後の最初のパケットはEXT_INFOってやつでSSHの拡張プロトコル的なやつらしい。これでkeystroke timing countermeasureみたいなセキュア機能を有効にするんだが、それを阻止できる(クライアントからみると、サーバはEXT_INFOを送ってこないので拡張プロトコルに対応してないんだなと思わせられる)。

したがって他の攻撃を通しやすくなる、ということっぽい

対策

OpenSSH 9.6にはこの攻撃への対策が導入されている。strict KEXと命名された。

SSHハンドシェイクの最初のパケットであるKEX_INITパケットには、そのサーバ/クライアントが対応する暗号方式がalgorithm_listとして含まれるが、algorithm_listにstrictモード用の識別子 kex-strict-c-v00@openssh.com / kex-strict-s-v00@openssh.com が追加される。これは暗号方式ではなく、自分がこれを送っていて相手がこれを送ってきたらstrict KEXをやりますという値。

strict KEXに入ると、SSHハンドシェイク中のIGNOREパケットとDEBUGパケットは無視されるようになり、このパケットを受け取ってもRcvカウンタがインクリメントされなくなる。

まとめ

……という話をしたら、じゃあKEX_INITを改竄すればいいじゃんという指摘をもらって、たしかにと思ったけど、KEX_INITの値はこのあとのKEX_DHREPLYで計算されるハッシュに利用されるので改竄されると気が付かれるようだった。逆にIGNOREはそういうハッシュに含まれないので気がつけない。このあたりTLSはTranscript Hashなるやつを採用していて対策できていると論文にあった気がするので、次はそのあたりが気になる*4

なんにせよこういう面白い話を知れるのは楽しいですね

*1:SSHは双方向にメッセージを送りあうのでSnd, Rcvの2つが必要

*2:逆にこのカウンタをMACに使わない方式ではこの攻撃は成立しない

*3:https://tex2e.github.io/rfc-translater/html/rfc4253.html#11-2--Ignored-Data-Message

*4:このあたりは関係ない話を繋げている気がするので相当自信ない