SECCON 2023 Finals writeup

~~kurenaifさんにmuck-a-muckのwriteupを書いてもらうために自分もwriteupを書くことにしました.~~

# DLP 4.0
``` python:problem.sage
import os
import secrets
import signal

FLAG = os.getenv("FLAG", "FAKEFLAG{THIS_IS_FAKE}")


if __name__ == "__main__":
    signal.alarm(333)
    p = int(input("What's your favorite 333-bit p: "))
    if not is_prime(p) or p.bit_length() != 333:
        print("Invalid p")
        exit()
    order = p**2 - 1
    x = secrets.randbelow(order)
    Q = QuaternionAlgebra(Zmod(p), -1, -1)
    g = Q.random_element()
    h = g**x
    print(f"{g = }")
    print(f"{h = }")
    _x = int(input("Guess x: "))
    if g**_x == h:
        print(FLAG)
    else:
        print("NO FLAG")

```

四元数の離散対数問題です. $p$をこちらから指定できるので$\varphi(p)
$の素因数 Pohlig–Hellman algorithm を用いて離散対数問題を解くことができます.  
というわけでまずそのような$333$bit素数を探してこないといけません

```  python:solve1.sage
from Crypto.Util.number import getPrime, isPrime

t = 1
for i in range(10000):
    p = Primes()
    t *= p.unrank(i)
    if len(bin(int(t + 1))[2:]) == 333 and isPrime(int(t + 1)):
        print(t + 1)
        print(len(bin(int(t + 1))))
        break
    elif len(bin(int(t))) > 333:
        while len(bin(int(t))) > 333:
            t //= getPrime(len(bin(int(t))[2:]) - 100)
    else:
        continue

print(factor(t))

```

`solve1.sage`で$p=16033419932502986469037291427850365121716627585835910890032900960816840045300923995323317366410694661$を得ました.

$\mod p$上の4元数に対して Pohlig–Hellman algorithm を適用せずにその絶対値の2乗に対して適用しました.

``` python:solve2.sage
def fast_dlp_modn(H, G, n):
    return int(pari(f"znlog({H}, Mod({G}, {n}))"))


g = [
    10574785669437053346460855274101765079926527156435114060684936013444693474513418665386014039279892360,
    11804743590594086793205518143144243933915696733500960553102945550815013856707501681266383662910190466,
    7702563307652547444139815157687071577685298555987273908224271224559325002791811117802596224796655243,
    13131661278034532409159200125836863854067784171016940507434161024540649930096249081912606216978083103,
]
h = [
    12582425849478489067703813004513843030298821460495120678211987089170893681521733732769395491181812508,
    71140667351304439771702228354606580027717957703387229390348649244481789665458974114357620201525437,
    727887411434074784475642696372521753765894615469985853922104017787522324571701971915021832299619105,
    11307198890461502314840521692749101049689436507205606042087217255209853340300627394877294102776519227,
]
# assert Q.order() == p**4
# assert g == g ** Q.order()
p = 16033419932502986469037291427850365121716627585835910890032900960816840045300923995323317366410694661
absg = int(g[0]) ** 2 + int(g[1]) ** 2 + int(g[2]) ** 2 + int(g[3]) ** 2
absh = int(h[0]) ** 2 + int(h[1]) ** 2 + int(h[2]) ** 2 + int(h[3]) ** 2
# assert Mod(absg, p) ** x == Mod(absh, p)
g = Mod(absg, p)
y = Mod(absh, p)

phi_p = "2 * 3 * 7 * 281 * 585131 * 2283091 * 66558319 * 38812459031 * 8407411055293 * 8899182573469"

x = fast_dlp_modn(y, g, p)
print("[+] x:", x)

```

# KEX 4.0

```python:problem.sage
import os
from hashlib import sha256
from secrets import randbelow

from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes


FLAG = os.getenvb(b"FLAG", b"FAKEFLAG{THIS_IS_FAKE}")

p = 0xC20C8EDB31BFFA707DC377C2A22BE4492D1F8399FFFD388051EC5E4B68B4598B
order = p**2 - 1
Q = QuaternionAlgebra(Zmod(p), -1, -1)
i, j, k = Q.gens()
pub_A = (
    71415146914196662946266805639224515745292845736145778437699059682221311130458
    + 62701913347890051538907814870965077916111721435130899071333272292377551546304 * i
    + 60374783698776725786512193196748274323404201992828981498782975421278885827246 * j
    + 60410367194208847852312272987063897634106232443697621355781061985831882747944 * k
)
pub_B = (
    57454549555647442495706111545554537469908616677114191664810647665039190180615
    + 8463288093684346104394651092611097313600237307653573145032139257020916133199 * i
    + 38959331790836590587805534615513493167925052251948090437650728000899924590900 * j
    + 62208987621778633113508589266272290155044608391260407785963749700479202930623 * k
)


def hash_Q(x):
    return sha256(
        long_to_bytes(int(x[0]))
        + long_to_bytes(int(x[1]))
        + long_to_bytes(int(x[2]))
        + long_to_bytes(int(x[3]))
    ).digest()


if __name__ == "__main__":
    # Alice sends share_A to Bob
    priv_A = randbelow(order)
    A = pub_A**priv_A
    share_A = A**-1 * pub_B * A
    print(f"{share_A = }")
    # Bob sends share_B to Alice
    priv_B = randbelow(order)
    B = pub_B**priv_B
    share_B = B**-1 * pub_A * B
    print(f"{share_B = }")
    # Alice computes the shared key
    Ka = A**-1 * share_B**priv_A
    # Bob computes the shared key
    Kb = share_A**-priv_B * B
    assert Ka == Kb

    # Encrypt FLAG with the shared key
    key = hash_Q(Ka)
    cipher = AES.new(key, mode=AES.MODE_CTR)
    nonce = cipher.nonce.hex()
    enc = cipher.encrypt(FLAG).hex()
    print(f"{nonce = }")
    print(f"{enc = }")

```

四元数上の Diffie–Hellman 鍵交換です.　コードからわかる情報を羅列していきます.

- $\mathrm{Ka}=\mathrm{Kb}=A^{-1}B^{-1}AB$ 
- $A=A_{pub}^a$, $B=B_{pub}^b$
- $A_{share}=A^{-1}B_{pub}A$, $B_{share}=B^{-1}A_{pub}B$

四元数の行列は対角化できます. (SECCON2023 QualsのRSA4.0の[作問者writeup](https://github.com/y011d4/my-ctf-challenges/tree/main/2023-SECCONCTF-Quals/crypto/rsa-4.0/solution)の想定解法でも用いられています).
私の場合は$4\times4$の行列でやりました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109452/097935dc-331b-15c8-cf4a-f6e53471a5b5.png)

```math
\begin{aligned}
A_{pub}&= P_AD_AP_A^{-1}\\
B_{pub}&=P_BD_BP_B^{-1}\\
A&=P_AD_A^aP_A^{-1}\\
B&=P_BD_B^bP_B^{-1}
\end{aligned}
```
より

```math
\begin{aligned}
P_A^{-1}A_{share}P_A&=D_A^{-a}(P_A^{-1}B_{pub}P_A)D_A^a\\
P_B^{-1}B_{share}P_B&=D_B^{-b}(P_B^{-1}B_{pub}P_B)D_B^b
\end{aligned}
```

$P_A^{-1}B_{pub}P_A$, $P_B^{-1}B_{pub}P_B$は既知の情報から求めることが可能です.
$D_A^a, D_B^b$は対角行列です.

```math
D^{-1}TD=
\begin{pmatrix}
\frac{1}{x} & 0 & 0 & 0\\
0 & \frac{1}{x} & 0 & 0\\
0 & 0& \frac{1}{y} & 0 \\
0 & 0 & 0 & \frac{1}{y}\\
\end{pmatrix}
\begin{pmatrix}
a & b & c & d\\
e & f & g & h\\
i & j& k & l \\
m & n & o & p\\
\end{pmatrix}
\begin{pmatrix}
x & 0 & 0 & 0\\
0 & x & 0 & 0\\
0 & 0& y & 0 \\
0 & 0 & 0 & y\\
\end{pmatrix}
=
\begin{pmatrix}
a & \frac{by}{x} &\frac{cz}{x} & \frac{dw}{x}\\
\frac{ex}{y} & f & \frac{gz}{y} & \frac{hw}{y}\\
\frac{ix}{z} & \frac{jy}{z}& k & \frac{lw}{z} \\
\frac{mx}{w} & \frac{ny}{w} & \frac{oz}{w} & p\\
\end{pmatrix}
```
$D$には$D_A^a$又は$D_B^b$, $T$には$P_A^{-1}B_{pub}P_A$または$P_B^{-1}B_{pub}P_B$が入ります.
よって$x:y:w:z$を求めることができます. ($x:y=1:1$は求めまでもありませんがそれに気づいたのはコードを書き終わってからでした...)

そうして求めたものを$D_A'^{a}$と$D_B'^b$としてます. $D_A'^{a}$と$D_B'^b$はそれぞれある定数$k_A,k_B\in \mathbb{Z}/p\mathbb{Z}$を用いて

```math
\begin{aligned}
D_A'^a&=k_AD_A^a\\
D_B'^b&=k_BD_B^b
\end{aligned}
```
の関係が成り立ちます. 一見この$k_A,k_B$を求める必要がありそうですがほしいものは $A^{-1}B^{-1}AB$なのでこのような定数は打ち消しあって影響を及ぼしません. 

以下がそれらをもとに書かれたコードです. この問題は徹夜して解いたので`problem.sage`の上から追記する限界コーディングをしてます. またsageで有限体の拡大をどうやるかわからず行列計算を分割してやる羽目になってます...(わかる方, 教えていただけると幸いです.)

```python:solve.sage
import os
from hashlib import sha256
from secrets import randbelow


from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes


FLAG = os.getenvb(b"FLAG", b"FAKEFLAG{THIS_IS_FAKE}")

p = 0xC20C8EDB31BFFA707DC377C2A22BE4492D1F8399FFFD388051EC5E4B68B4598B
order = p**2 - 1
Q = QuaternionAlgebra(Zmod(p), -1, -1)
i, j, k = Q.gens()
pub_A = (
    71415146914196662946266805639224515745292845736145778437699059682221311130458
    + 62701913347890051538907814870965077916111721435130899071333272292377551546304 * i
    + 60374783698776725786512193196748274323404201992828981498782975421278885827246 * j
    + 60410367194208847852312272987063897634106232443697621355781061985831882747944 * k
)
pub_B = (
    57454549555647442495706111545554537469908616677114191664810647665039190180615
    + 8463288093684346104394651092611097313600237307653573145032139257020916133199 * i
    + 38959331790836590587805534615513493167925052251948090437650728000899924590900 * j
    + 62208987621778633113508589266272290155044608391260407785963749700479202930623 * k
)


def hash_Q(x):
    return sha256(long_to_bytes(int(x[0])) + long_to_bytes(int(x[1])) + long_to_bytes(int(x[2])) + long_to_bytes(int(x[3]))).digest()


# diagonal matrix
def diag(m, p):
    a = Mod(m[0][0], p)
    b = Mod(m[0][1], p)
    c = Mod(m[0][2], p)
    d = Mod(m[0][3], p)
    r = (-b ^ 2 - c ^ 2 - d ^ 2).square_root()
    half = Mod(1, p) / Mod(2, p)
    J = [
        [a - r, 0, 0, 0],
        [0, a - r, 0, 0],
        [0, 0, a + r, 0],
        [0, 0, 0, a + r],
    ]
    S = [
        [
            (d * r - b * c) / (c ^ 2 + d ^ 2),
            (c * r + b * d) / (c ^ 2 + d ^ 2),
            -(d * r + b * c) / (c ^ 2 + d ^ 2),
            (-c * r + b * d) / (c ^ 2 + d ^ 2),
        ],
        [
            (c * r + b * d) / (c ^ 2 + d ^ 2),
            (-d * r + b * c) / (c ^ 2 + d ^ 2),
            (-c * r + b * d) / (c ^ 2 + d ^ 2),
            (d * r + b * c) / (c ^ 2 + d ^ 2),
        ],
        [0, 1, 0, 1],
        [1, 0, 1, 0],
    ]
    Sinv = [
        [
            d / (2 * r),
            c / (2 * r),
            -b / (2 * r),
            half,
        ],
        [
            c / (2 * r),
            -d / (2 * r),
            half,
            b / (2 * r),
        ],
        [
            -d / (2 * r),
            -c / (2 * r),
            b / (2 * r),
            half,
        ],
        [
            -c / (2 * r),
            d / (2 * r),
            half,
            -b / (2 * r),
        ],
    ]
    S = Matrix(S)
    Sinv = Matrix(Sinv)
    J = Matrix(J)
    # print(Sinv * S)
    # print(J)
    # print(m.eigenmatrix_right())
    assert S**-1 == Sinv
    assert S * J * S**-1 == m
    return S, J, Sinv


def divide_expression(expression):
    # Find the position of the '+'
    plus_index = expression.find("+")

    # Extract the two parts of the expression
    part1 = expression[:plus_index].strip()
    part2 = expression[plus_index + 1 :].strip()

    return part1, part2


def get_sqrt(expression):
    e = divide_expression(expression)
    t_index = e[0].find("t")
    return e[0][t_index + 1 :].strip()


def to_mod(m):
    new_m = [[0 for _ in range(4)] for _ in range(4)]
    for i in range(4):
        for j in range(4):
            new_m[i][j] = Mod(int(m[i][j]), p)
    new_m = Matrix(new_m)
    return new_m


if __name__ == "__main__":
    share_A = (
        57454549555647442495706111545554537469908616677114191664810647665039190180615
        + 29676674584636622512615278554619783662266316745243745754583020553342447549066 * i
        + 13738434026348321269316223833101191512670504554293346482813342673413295266974 * j
        + 23943604179074440949144139144245518129342426692024663551007842394683089455212 * k
    )
    share_B = (
        71415146914196662946266805639224515745292845736145778437699059682221311130458
        + 65071948237600563018819399020079518439338035815171479183947570522190990857574 * i
        + 52272525531848677372993318721896591307730532037121185733047803928301284987593 * j
        + 68406537373378314867132842983264676792172029888604057526079501977599097329576 * k
    )

    Pb, Tb, Pbinv = diag(pub_B.matrix(), p)
    Pa, Ta, Painv = diag(pub_A.matrix(), p)
    tmp = Painv * share_A.matrix() * Pa
    assert Pb * Tb * Pbinv == pub_B.matrix()
    mid = Painv * pub_B.matrix() * Pa

    z_coff = tmp[0][2] / mid[0][2]
    w_coff = tmp[0][3] / mid[0][3]
    y_coff = (tmp[1][2] / z_coff / mid[1][2]) ** -1

    PA_coff = (
        Pa
        * Matrix(
            [
                [1, 0, 0, 0],
                [0, y_coff, 0, 0],
                [0, 0, z_coff, 0],
                [0, 0, 0, w_coff],
            ]
        )
        * Painv
    )

    tmpB = Pbinv * share_B.matrix() * Pb
    midB = Pbinv * pub_A.matrix() * Pb

    z_coffB = tmpB[0][2] / midB[0][2]
    w_coffB = tmpB[0][3] / midB[0][3]
    y_coffB = (tmpB[1][2] / z_coffB / midB[1][2]) ** -1

    PB_coff = (
        Pb
        * Matrix(
            [
                [1 / w_coffB, 0, 0, 0],
                [0, y_coffB / w_coffB, 0, 0],
                [0, 0, z_coffB / w_coffB, 0],
                [0, 0, 0, w_coffB / w_coffB],
            ]
        )
        * Pbinv
    )
    real_PA_coff = [[0 for _ in range(4)] for _ in range(4)]
    real_PB_coff = [[0 for _ in range(4)] for _ in range(4)]
    real_PAinv_coff = [[0 for _ in range(4)] for _ in range(4)]
    real_PBinv_coff = [[0 for _ in range(4)] for _ in range(4)]
    for i in range(4):
        for j in range(4):
            real_PA_coff[i][j] = Mod(int(divide_expression(str(PA_coff[i][j]))[1]), p)
            real_PB_coff[i][j] = Mod(int(divide_expression(str(PB_coff[i][j]))[1]), p)
            real_PAinv_coff[i][j] = Mod(int(divide_expression(str((PA_coff**-1)[i][j]))[1]), p)
            real_PBinv_coff[i][j] = Mod(int(divide_expression(str((PB_coff**-1)[i][j]))[1]), p)
    real_PA_coff = Matrix(real_PA_coff)
    real_PB_coff = Matrix(real_PB_coff)
    real_PAinv_coff = Matrix(real_PAinv_coff)
    real_PBinv_coff = Matrix(real_PBinv_coff)

    sa = Mod(int(get_sqrt(str(PA_coff[0][0]))), p)
    sb = Mod(int(get_sqrt(str(PB_coff[0][0]))), p)

    both_sqrt = (sa * sb).sqrt()
    real_AB = (
        to_mod(real_PA_coff) * to_mod(real_PB_coff)
        + to_mod((PA_coff - real_PA_coff) / sa.sqrt()) * to_mod((PB_coff - real_PB_coff) / sb.sqrt()) * both_sqrt
    )
    im_AB = []
    im_AB.append(to_mod((PA_coff - real_PA_coff) * (real_PB_coff) / sa.sqrt()))
    im_AB.append(to_mod((real_PA_coff * (PB_coff - real_PB_coff)) / sb.sqrt()))

    real_ABinv = (
        real_PAinv_coff * real_PBinv_coff
        + to_mod((PA_coff**-1 - real_PAinv_coff) / sa.sqrt()) * to_mod((PB_coff**-1 - real_PBinv_coff) / sb.sqrt()) * both_sqrt
    )
    im_AB_inv = []
    im_AB_inv.append(to_mod(((PA_coff**-1 - real_PAinv_coff) * real_PBinv_coff) / sa.sqrt()))
    im_AB_inv.append(to_mod((real_PAinv_coff * (PB_coff**-1 - real_PBinv_coff)) / sb.sqrt()))
    real = (
        real_ABinv * real_AB
        + im_AB_inv[0] * im_AB[0] * sa
        + im_AB_inv[1] * im_AB[1] * sb
        + (im_AB_inv[1] * im_AB[0] + im_AB_inv[0] * im_AB[1]) * both_sqrt
    )

    key = hash_Q(real[0])
    nonce = long_to_bytes(0x6CED0927695BC45E)
    cipher = AES.new(key, AES.MODE_CTR, nonce=nonce)
    nonce = cipher.nonce.hex()
    enc = long_to_bytes(
        0x6A59A899FED260513CD4AD037BB3D8681AE47E4D5C13139AEBDE981C01F93AAC63D6A39C04E4DFA3FD05FA41C1BCDA8B39C660AFF5673458D5324EAC738D1BD0A255
    )
    plain = cipher.decrypt(enc)
    print(plain)

```

# muck-a-muck

何もわからず会場でチャチャチャを踊りながら椅子を暖めてました.
kurenaifさんWriteup待ってます()

# ハードウェア問

戦犯をしました. 問題名は分からないです.
正しい解法は水色の部分を切断することです.
トランジスタがGNDに張り付いているのでU_4のD_FFの出力が0なのでタイミングを計って切断することと, DATA1も分圧のせいでU_3のD_FFの出力が0なので切断して1が出力されるようにします. R14, R17 だけをきればよいらしいですが$3.3/34\sim0.097\mathrm{V}$だとやはり電圧が低すぎる気がする...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109452/9dd80b8c-d56f-36b3-4aa0-d4aca86c70bb.png)

upsolveしたいですがダウンロードしていなくてコードをコードが手元にないので（は？）今すべてを保存していたチームメンバに送ってもらってます. ~~届いたらやります.~~ 届きましたが眠いので明日やります...