ふるつき

v(*'='*)v かに

TSG Live CTF 6 writeup

先日行われたTSG Live! 6中の企画の一つ、TSG Live CTFに外部チームの一員として招待され、参加してきましたので記念writeupです。放送のアーカイブはこちら↓

チームは公式には kurenaifさんptr-yudai、私の3人でしたが、助っ人としてst98さんにもいくつかの問題を解いていただきました。正直私はいてもいなくても同じ結果になっただろうと思っていますが、このチームで参戦できて楽しかったです。私が解いた問題のうち、Fisherだけは正答数がすくなかったのでwriteupを書いておきます。


与えられるのは次のようなソースコードで、 string_to_signalsは文字列を[0, 1]からなるモールス符号に変換する関数です。素直に読むと、フラグ文字列をモールス符号に変換し、さらに01の符号データをsin波で表しています。それだけなら良いのですが、この波形データをランダムにシャッフルしたあと、wav形式で保存しています。

import soundfile as sf
import numpy as np
from utils import signals_to_string, string_to_signals

flag = open('flag.txt').read()
signals = string_to_signals(flag)

assert(len(signals) == 473)

wave = np.empty(len(signals) * 2000)
for i in range(len(wave)):
  if signals[i // 2000] == 0:
    wave[i] = 0.0
  else:
    # Generate 439.97Hz morse code
    wave[i] = np.sin(i * 439.97 / 44100 * (2 * np.pi))

# Fisher here :)
np.random.shuffle(wave)

sf.write('result.wav', wave, 44100, 'PCM_24')

結構困るように見え、また想定解法(賢い)では困った前提で xが小さいときに \sin(x) \approx x であることを利用しているのですが、実験をしてみると意外にもsin波を構成する点が、どのiに対応するかがほぼ一対一で対応付けられることに気が付きました。おそらく浮動小数点の演算誤差からくるものだとは思いますが、へーという感じです。

というわけで、自分で i を回しながら \sin(\frac{439.97i}{44100 * 2\pi}) に一致する点があるかないかで、その周辺が0/1のどちらを波形に変換したものかを判断することができます。一致の判定は浮動小数点でやると怖いので、多少の誤差を受け入れることにして小数を文字列に変換して、小数点以下n桁までが一致するかを見ました。

import soundfile as sf
import numpy as np
from utils import signals_to_string

filepath = 'result.wav'
data, _ = sf.read(filepath)

print(len(data))
size = len(data)
data = set(["{:.6f}".format(d) for d in data])

rev_data = []

for i in range(size):
  k = np.sin(i * 439.97 / 44100 * (2 * np.pi))
  ks = "{:.6f}".format(k)
  if ks in data:
    rev_data.append(k)
  else:
    rev_data.append("x")


signal = []
for i in range(0, len(rev_data), 2000):
  if rev_data[i:i+2000].count("x") > 500:
    signal.append(0)
  else:
    signal.append(1)

# print(len(rev_data))
# print(signal)
print(signals_to_string(signal))

重ねて申し上げますが、楽しい企画に誘ってくださり対戦どころか問題まで用意してくださったTSGの皆様、一緒にチームを組んでくれたkurenaifさん、ありがとうございました。楽しかったです

Midnightsun CTF 2021 writeup

Midnightsun CTFは毎年いくつか独自色の強い問題が出るな、という印象だったのですが、今年はBackupシリーズのcryptoがひどかった印象があります。その他AVRマイコンなどの組み込みに関連する問題が出るのはいつもどおりという感じ。

私は純粋cryptoを2問通しました。どちらもsolve数が二桁前半で、簡単というほどではないが難しい問題ではない、という感じでの面白い問題でした。なんとかチームメイトに怒られない程度の貢献はできたかな。

ocat_024

u = getrandbits(512)
p = next_prime(1337 * u + getrandbits(300))
q = next_prime(2021 * u + getrandbits(300))
n = p * q

sage: n                                                                                                                                                       
376347864369130929314918003073529176189619811132906053032580291332225522349124770927556541528827257242440708492473086949335591022991715583608318595770643139658398940858358366788472884935226392323475683663699450192657590965945792699658618476121298301964814904817629813971114882401961638885452623305901569855693667669
sage: e                                                                                                                                                       
310766221758298615381202293330340201483059349618898159596286032329366528507960364515724537815587119639645241427449290559721282531962504187828968370246921804939126848756614691757878117709116273156365166616707032588454229002048939764035908902821773289938766897121298435864096976179239412103595971947786698704390414999
sage: round(log(d, 2))                                                                                                                                        
376
sage: Zmod(n)(int.from_bytes(flag, 'big'))^e                                                                                                                  
303959676149267585825046248298946771045524761540948385119310381022731003152171453295355624619013005255259014408962058282203132797411290952180380162699236575669681289832696809724483816916203823843359489329046259936011464869397208487809073597880729992617105037777911285154649450892121090200338517719744860831555514222

 p = 1337u + a, q = 2021u + b となっているRSAの問題です。加えて、 dが比較的小さいことが分かっています。

まず第一に注目するのは素数が独特な構造になっていることです。これは過去にConfidence CTF 2019 TeaserでReally Suspicious Acronymとして出題されている形式で、 uが共通しており、 a, bのサイズが比較的小さいことから \frac{p}{q} = \frac{1337u + a}{2021u + b} \simeq \frac{1337}{2021}と近似ができるので、 n \times \frac{1337}{2021} \simeq n \times  \frac{p}{q} = p^2 という近似ができます。この近似により p, qの上位210ビット程度を求めることができます。

しかしこれだけの情報では、Coppersmith's methodを用いた素因数分解には不足です。そこで dが比較的小さいというヒントから、 ed = 1 \mod \phi(N)を使って立式していきます。

 \phi(N) = N - (p + q) + 1 ですから、  p + qを先程近似した値 p', q'と真の値との差 rを用いて p + q = p' + q' + rと表して以下の式が成り立ちます。

 ed = 1 + tN - t(p' + q' + r) + t

さらに \mod eをとります。

 0 \equiv 1 + tN - t(p' + q' + r) + t \mod e  \tag{△}

式(△)において、未知変数は r, t であり、 rは素因数 p, qの未知の部分なので300bit程度、また t = (ed - 1) / \phi(N) \simeq (2^{1045}\times 2^{376} - 1) / 2^{1045} \simeq 2^{376} であることから、 tの大きさは最大でも376bit程度になります。一方 eは1000bit以上ありますから、 r, tは剰余多項式(△)の比較的小さな整数根になっていると言えそうです。したがってCoppersmith's methodによって r, tの具体的な値を求められるかもしれません。多変数多項式の小さい根を求める実装としては、 defund/coppersmith が優れています。これを使って r, tを求めることができれば、 \phi(N)を計算しフラグを求められそうです。

# https://raw.githubusercontent.com/defund/coppersmith/master/coppersmith.sage
import itertools

from sage.rings.polynomial.multi_polynomial_sequence import PolynomialSequence

def small_roots(f, bounds, m=1, d=None):
    if not d:
        d = f.degree()

    R = f.base_ring()
    N = R.cardinality()
    
    f /= f.coefficients().pop(0)
    f = f.change_ring(ZZ)

    G = PolynomialSequence([], f.parent())
    for i in range(m+1):
        power = N^(m-i) * f^i
        for shifts in itertools.product(range(d), repeat=f.nvariables()):
            g = power
            for variable, shift in zip(f.variables(), shifts):
                g *= variable^shift
            G.append(g)

    B, monomials = G.coefficient_matrix()
    monomials = vector(monomials)

    factors = [monomial(*bounds) for monomial in monomials]
    for i, factor in enumerate(factors):
        B.rescale_col(i, factor)

    B = B.dense_matrix().LLL()

    B = B.change_ring(QQ)
    for i, factor in enumerate(factors):
        B.rescale_col(i, 1/factor)
    B = B.change_ring(ZZ)

    H = Sequence([], f.parent().change_ring(QQ))
    for h in B*monomials:
        if h.is_zero():
            continue
        H.append(h.change_ring(QQ))
        I = H.ideal()
        if I.dimension() == -1:
            H.pop()
        elif I.dimension() == 0:
            V = I.variety(ring=ZZ)
            if V:
                roots = []
                for root in V:
                    root = map(R, map(root.__getitem__, f.variables()))
                    roots.append(tuple(root))
                return roots

    return []


n = 376347864369130929314918003073529176189619811132906053032580291332225522349124770927556541528827257242440708492473086949335591022991715583608318595770643139658398940858358366788472884935226392323475683663699450192657590965945792699658618476121298301964814904817629813971114882401961638885452623305901569855693667669
e = 310766221758298615381202293330340201483059349618898159596286032329366528507960364515724537815587119639645241427449290559721282531962504187828968370246921804939126848756614691757878117709116273156365166616707032588454229002048939764035908902821773289938766897121298435864096976179239412103595971947786698704390414999
c =  303959676149267585825046248298946771045524761540948385119310381022731003152171453295355624619013005255259014408962058282203132797411290952180380162699236575669681289832696809724483816916203823843359489329046259936011464869397208487809073597880729992617105037777911285154649450892121090200338517719744860831555514222

p_ = int(sqrt(n * 1337/2021))
q_ = int(sqrt(n * 2021/1337))

PR.<r, t_> = PolynomialRing(Zmod(e))
f = 1 + k_*n - t_*(p_ + q_ + r) + t_
for ans in small_roots(f, [2^300, 2^377]):
    r = int(ans[0])
    phi = n - (p_ + q_ + r) + 1

    d = int(pow(e, -1, phi))

    m = pow(c, d, n)
    print(m)

求まりました。

dbcsig_64434

from hashlib import sha256


def keygen(password):
    while True:
        p = 2 * random_prime(2 ^ 521) + 1
        if p.is_prime(proof=False):
            break
    base, h = 3, password
    for i in range(256):
        h = sha256(h).digest()
    x = int.from_bytes(h * 2, "big")
    return base, p, pow(base, x, p), x


def sign(message, priv):
    h = int(sha256(message).hexdigest(), 16)
    k = next_prime(int.from_bytes(
        sha256(message + priv.to_bytes(128, "big")).digest() + sha256(message).digest(),
        "big"
    ))
    r = int(pow(g, (k - 1) / 2, p))
    s = int(Zmod((p - 1) / 2)(-r * priv + h) / k)
    return r, s


g, p, pub, priv = keygen(b"[redacted]")
r, s = sign(b"blockchain-ready deterministic signatures", priv)

(配布ソースコード中に存在した、問題と直接関係ないコメントを省略しています)

独自のデジタル署名アルゴリズムのようですが、プログラムはいわゆるDSAのそれと似通っています。目的は d = privを求めることで、 g, p, pub, r, sに加えて、平文から求められる hが分かっています。

このアルゴリズムを観察すると、2つ特徴的な部分が見つかります。まず1つ目は秘密鍵 dの構造で、int.from_bytes(h * 2, "big") のように、256bitの値を2回繰り返して並べることで512bitの値を作っています。値の大きさは512bitありますが、情報量としては256bitのものと等価と考えられます。2つめは kの値で、こちらも256bitの値を2つ並べて作られていますが、上位256bitが全くわからない一方、下位256bitは h + \alpha でありおよその値が分かっています。

式をたてて整理してみます。そもそものDSAアルゴリズムでは  ks \equiv -rd + h \mod q (ただしここで q = (p - 1) / 2です)がなりたち、それは今回も同様です。ここで k = 2^{256}k' + h + \alpha,  d = 2^{256}d' + d'と表しつつ、移項して式を整理すると次のような形になります。

 s(2^{256}k'  + h + \alpha) + r(2^{256}d' + d') - h \equiv 0 \mod q

ここで未知数は k', d', \alphaで、 k', d'はそれぞれ256bitの値です。 \alphaはどのくらいかわかりませんが、だいたい1000を超えることはないでしょう。他方、 qは521bit程度の値であり未知数と比較して大きいですから先ほどと同様にCoppersmith's methodが適用できそうです。

# https://raw.githubusercontent.com/defund/coppersmith/master/coppersmith.sage
...

from hashlib import sha256

g = 3
p = 403564885370838178925695432427367491470237155186244212153913898686763710896400971013343861778118177227348808022449550091155336980246939657874541422921996385839128510463
pub = 246412225456431779180824199385732957003440696667152337864522703662113001727131541828819072458270449510317065822513378769528087093456569455854781212817817126406744124198
r = 195569213557534062135883086442918136431967939088647809625293990874404630325238896363416607124844217333997865971186768485716700133773423095190740751263071126576205643521
s = 156909661984338007650026461825579179936003525790982707621071330974873615448305401425316804780001319386278769029432437834130771981383408535426433066382954348912235133967
message = b"blockchain-ready deterministic signatures"
h = int(sha256(message).hexdigest(), 16)

q = (p-1) // 2
# assert q.is_prime ????????

PR.<d_, k_, a> = PolynomialRing(Zmod(q))

f = 2^256 * s * k_ + s * (h + a) + r * (d_ + 2^256 * d_) - h
roots = small_roots(f, [2^256, 2^256, 1000], d=4, m=4)

for root in roots:
    dval = Integer(root[0])
    dval = dval * 2^256 + dval
    if pow(g, dval, p) == pub:
        print("[+] found: {}".format(dval))
        break
    else:
        print("[-] wrong: {}".format(dval))

できました。ところで、この問題のkeygenに従って値が作られていれば pは最大でも522bit程度の値で安全素数になると思ったのですが、実際には pは557bitあり、また安全素数ではありませんでした。これは一体どういうことでしょうか 🤔

Securinets CTF 2021 Quals writeup

楽しく解ける問題ばかりで評価が高い。BOSS問という感じの問題はなかったので割と解かれていた印象。唯一PoWが面倒な形式だったのでもうちょっと丁寧に作ってほしかった感じはある

MiTM

from Crypto.Util.number import long_to_bytes
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
from secret import flag
import hashlib
import random
import os
import signal

class DHx():
    def __init__(self):
        self.g = 2
        self.p = 0xf18d09115c60ea0e71137b1b35810d0c774f98faae5abcfa98d2e2924715278da4f2738fc5e3d077546373484585288f0637796f52b7584f9158e0f86557b320fe71558251c852e0992eb42028b9117adffa461d25c8ce5b949957abd2a217a011e2986f93e1aadb8c31e8fa787d2710683676f8be5eca76b1badba33f601f45
        self.private = random.randint(1, self.p - 1)
        self.secret = None

    def getPublicKey(self):
        return pow(self.g, self.private, self.p)

    def share(self, x : int):
        assert x > 1 and x < self.p
        return pow(x, self.private, self.p)

    def getSharedSecret(self, x : int):
        assert x > 1 and x < self.p
        self.secret = pow(x, self.private, self.p)

    def getFingerprint(self):
        return hashlib.sha256(long_to_bytes(self.secret)).hexdigest()

    def checkFingerprint(self, h1 : str, h2 : str):
        return h1 == h2 == self.getFingerprint()

    def encryptFlag(self):
        iv = os.urandom(16)
        key = hashlib.sha1(long_to_bytes(self.secret)).digest()[:16]
        return iv.hex() + AES.new(key, AES.MODE_CBC, iv).encrypt(pad(flag, 16)).hex()

signal.alarm(60)

Alice = DHx()
Bob = DHx()
Carol = DHx()

A = Alice.getPublicKey()
print("Alice sends to Bob: {}".format(A))
A = int(input("Forward to Bob: "))
B = Bob.share(A)
print("Bob sends to Carol: {}".format(B))
B = int(input("Forward to Carol: "))
Carol.getSharedSecret(B)

B = Bob.getPublicKey()
print("Bob sends to Carol: {}".format(B))
B = int(input("Forward to Carol: "))
C = Carol.share(B)
print("Carol sends to Alice: {}".format(C))
C = int(input("Forward to Alice: "))
Alice.getSharedSecret(C)

C = Carol.getPublicKey()
print("Carol sends to Alice: {}".format(C))
C = int(input("Forward to Alice: "))
A = Alice.share(C)
print("Alice sends to Bob: {}".format(A))
A = int(input("Forward to Bob: "))
Bob.getSharedSecret(A)

print("Alice says: ")
if (Alice.checkFingerprint(Carol.getFingerprint(), Bob.getFingerprint())):
    print(Alice.encryptFlag())
else:
    print("ABORT MISSION! Walls have ears; Be careful what you say as people may be eavesdropping.")

名前のとおりにAlice Bob Carol三者のDiffie Hellman鍵共有プロセスに介入して、任意のパラメータの交換を改竄できる。ただし三者は鍵共有のあと、互いの鍵が一致しているかを確かめるために、それぞれの鍵のfingerprintが一致することを確認している。したがって、三者の鍵を同じにしつつ、鍵の中身を知ることができるような改竄の仕方を考える必要がある。

一番簡単な方法は0や1を送って 0^a = 0 1^a = 1と計算してもらうことだが、三者は送られてきたパラメータ x 1 \lt x \lt p であることを確認しているのでこれはできない。それならば -1はどうか。三者秘密鍵 a,b,cが偶数の場合は -1^a \equiv 1 \mod pとなってしまうので途中でエラーになるが、すべて奇数ならば -1^a \equiv -1 \equiv p-1 \mod p p-1に落ち着く。

というわけで p-1を送りつつ a,b,cがそれぞれ奇数になる組み合わせを引くまでガチャする。実際はガチャ用のコードを整えている間にyoshikingがガチャを当てて解いていた。

from ptrlib import Socket
from Crypto.Util.number import long_to_bytes
from Crypto.Cipher import AES
import hashlib
import time

while True:
    time.sleep(1)
    sock = Socket("crypto1.q21.ctfsecurinets.com", 1337)
    p = 0xf18d09115c60ea0e71137b1b35810d0c774f98faae5abcfa98d2e2924715278da4f2738fc5e3d077546373484585288f0637796f52b7584f9158e0f86557b320fe71558251c852e0992eb42028b9117adffa461d25c8ce5b949957abd2a217a011e2986f93e1aadb8c31e8fa787d2710683676f8be5eca76b1badba33f601f45

    sock.sendlineafter("Forward to Bob: ", str(p - 1))
    AB = int(sock.recvlineafter("Carol: "))
    if AB == 1:
        sock.close()
        continue
    sock.sendlineafter("Forward to Carol: ", str(AB))
    print("[+] progress 1")

    sock.sendlineafter("Forward to Carol: ", str(p - 1))
    BC = int(sock.recvlineafter("Alice: "))
    if BC == 1:
        sock.close()
        continue
    sock.sendlineafter("Forward to Alice: ", str(BC))
    print("[+] progress 2")

    sock.sendlineafter("Forward to Alice: ", str(p - 1))
    CA = int(sock.recvlineafter("Bob: "))
    if CA == 1:
        sock.close()
        continue
    sock.sendlineafter("Forward to Bob: ", str(CA))
    print("[+] progress 3")

    print(sock.recvline())
    line = sock.recvline().decode()
    if "ABORT" in line:
        sock.close()
        continue

    key = hashlib.sha1(long_to_bytes(p - 1)).digest()[:16]
    iv, cipher = bytes.fromhex(line[:32]), bytes.fromhex(line[32:])
    aes = AES.new(key, AES.MODE_CBC, iv)
    print(aes.decrypt(cipher))

    break

あとでyoshikingに教えてもらったけど、 a,b,cがそれぞれ偶数の場合でも解けて、1度目の介入では適当な値を渡して、2度目でだけ p-1を送るようにすればshareが a,b,cの偶奇に応じて 1 p-1になるので、↑の方法より確率を上げることができる。

Shilaformi

import signal
from secret import flag
from Crypto.Random import random
from Crypto.Util.number import getPrime

#Odds and evens (hand game)
#https://en.wikipedia.org/wiki/Odds_and_evens_(hand_game)

def pad(choice, n):
    return 2*random.randint(1, n//2 - 1) + choice

def send(choice, n):
    choice = pad(choice, n)
    return pow(2, choice, n )

signal.alarm(360)
print ("Let's play odds and evens! If I win you loose 5 times the amount of money you have bet! I choose odd.\n")

WALLET = 10
while WALLET > 0:
    print("Your current wallet is {} $.\n".format(WALLET))

    if WALLET > 133337:
        print("Wow, You've got filthy rich. Here's your flag: {}".format(flag))
        exit()

    n = getPrime(1024)*getPrime(1024)
    print ("Public Modulus : {}\n".format(n))

    bet = int(input("Place your bet : "))
    assert bet <= WALLET and bet > 0

    choice = random.randint(0, 5)
    print ("Here's my choice : {}\n".format(send(choice, n)))

    player = int(input("Shilaaafooooooormi: "))
    assert player >= 0 and player < 6

    if (choice + player ) & 1:
        print("You lose.")
        WALLET -= 5*bet
    else:
        print("You win.")
        WALLET += bet

print("You got no money left in your wallet. Make sure to come back later!")

いわゆるOdds and evensというゲームらしく、相手が出してきた手と自分が出してきた手の和が偶数になるように値を選んで送れば勝ちとなる。相手は手 x 0 \le x \le 5の間から一つ選び、 n = pq と毎回ことなる乱数 rを用いて y = 2^{2r + x} \mod nとして渡してくれる。

このとき、 yから xの偶奇を判断する問題は、平方剰余問題(quadratic residuosity problem)として知られている 平方剰余問題と行った場合には aから a \equiv x^2 \mod nを満たす xを探す問題を指していそう。この問題は平方剰余の判定問題で、一般的な名前があるのかは知らない。ある値 aがある剰余の法 nの上で平方剰余か(= x^2 \equiv  a \mod nとなる xが存在するか)を調べるためにはルジャンドル記号、あるいはヤコビ記号を使うことができる。ルジャンドル記号は剰余の法が素数である場合に使える記号であり、ヤコビ記号はルジャンドル記号を拡張して合成数にも適用可能にしたものである。ただし、一部の正確性は失われている

大雑把に言えば、 Jacobi(a, n) = 1 のとき aは平方剰余かもしれないし、そうでないかもしれない。一方で Jacobi(a, n) = -1のとき、 aは必ず平方剰余ではない。したがって、与えられた手 y nについて Jacobi(y, n)をみて、その値が1の時はOdds and evensに勝てる確率は1/2であり、その値が1のときは必ず勝つことができる。

ところでここからがよくわかっていないんだけど、チームメイトのS3v3ru5に教えてもらい手元で実験した感じだと、 nの値によってヤコビ記号がうまく働いたり働かなかったりする。うまく働くケースに限って持金を全betし、勝てるかわからないときは1だけ掛けることで持金を増やした。

from ptrlib import Socket
import proofofwork
import gmpy2
import random

sock = Socket("crypto2.q21.ctfsecurinets.com", 1337)
pat = sock.recvregex(r"sha256\(([A-Za-z]+) \+ X\) = 000000")[0]
print(pat)

s = proofofwork.sha256(
        '000000??????????????????????????????????????????????????????????',
        text=pat + b'?????')
print(s)
sock.sendline(s[len(pat):])

# sock = Socket("localhost", 19999)

while True:
    current = int(sock.recvregex(r"wallet is ([0-9]+) \$.\n")[0])
    print(current)
    if current > 133337:
        sock.interactive()
    n = int(sock.recvlineafter("Modulus : "))
    print("[+] got modulus")

    # check n
    flag = True
    for choice in range(0, 6):
        x = 2*random.randint(1, n//2 - 1) + choice
        y = pow(2, x, n)
        if (x % 2 == 0) != (gmpy2.jacobi(y, n) == 1):
            flag = False
    print("[+] check: {}".format(flag))

    bet = 1
    if flag:
        # surely win
        bet = current
    sock.sendlineafter("bet : ", str(bet))

    y = int(sock.recvlineafter("choice : "))
    sock.sendlineafter("mi: ", str(1 if gmpy2.jacobi(y, n) == -1 else 0))
    if b"win" in sock.recvline():
        current += bet
        print("Win! current money: {}".format(current))
    else:
        current -= bet*5
        print("Loose... current money: {}".format(current))

        if current <= 0:
            print("LOOSE")
            break

Sign It!

from Crypto.Util.number import inverse
from Crypto.Random import random
from fastecdsa.curve import Curve
from fastecdsa.point import Point
import hashlib
import signal

class Server():
    def __init__(self, curve, G):
        self.G = G
        self.order = curve.q
        self.d = random.randrange(1, self.order - 1)
        self.P = (self.d * self.G)

    def sign(self, msg):
        z = int(hashlib.sha256(msg.encode()).hexdigest(), 16)
        k = random.randrange(1, self.order - 1)
        r = (k * self.G).x % self.order
        s = (inverse(k, self.order) * (z + r * self.d)) % self.order
        return (r, s)

    def verify(self, msg, sig):
        r, s = sig
        s %= self.order
        r %= self.order
        if s == 0 or r == 0:
            return False
        z = int(hashlib.sha256(msg.encode()).hexdigest(), 16)
        s_inv = inverse(s, self.order)
        u1 = (z * s_inv) % self.order
        u2 = (r * s_inv) % self.order
        W = u1 * self.G + u2 * self.P
        return W.x == r


signal.alarm(360)
# S256 curve params
p = 0x402969301d0ec23afaf7b6e98c8a6aebb286a58f525ec43b46752bfc466bc435
gx = 0x3aedc2917bdb427d67322a1daf1073df709a1e63ece00b01530511dcb1bae0d4
gy = 0x21cabf9609173616f5f50cb83e6a5673e4ea0facdc00d23572e5269632594f1d
a = 0x2ad2f52f18597706566e62f304ae1fa48e4062ee8b7d5627d6f41ed24dd68b97
b = 0x2c173bd8b2197e923097541427dda65c1c41ed5652cba93c86a7d0658070c707
q = 0x402969301d0ec23afaf7b6e98c8a6aeb2f4b05d0bbb538c027395fa703234883
S256 = Curve("S256", p, a, b, q, gx, gy)

PROOF = "Give me flag."
print("Welcome to the ECDSA testing of our probably secure S256 Curve. First choose your own generator then try to sign this message '{}' to prove us wrong.\n".format(PROOF))

print("Choose your point (x y) : ")

try:
    x = int(input("x : "))
    y = int(input("y : "))
    G = Point(x, y, curve=S256)
    S = Server(S256, G)

    sample = "No flags for you."
    print("Here's a sample signature: msg = '{}' , signature = {}".format(
        sample, S.sign(sample)))

    while True:
        msg = input("Message : ").strip()
        r = int(input("r : "))
        s = int(input("s : "))
        if S.verify(msg, (r, s)):
            print("Valid signature.")
            if msg == PROOF:
                from secret import flag
                print("Here you go : {}".format(flag))
                exit()
        else:
            print("Invalid signature.")

except Exception as e:
    print(e)
    exit()

ECDSAに関する問題。こちらからはいわゆるBase Point  Gを渡すことができ、かつ適当なメッセージに署名をしてもらえるので、向こうが指定したメッセージに署名するとフラグがもらえる。

楕円曲線上の離散対数問題が解けでもしない限りそんなことはできない。ところが与えられている楕円曲線のパラメータを見てみると、位数 qが小さい素因数として 157を持っているとS3v3ru5が教えてくれた。このとき、適当な楕円曲線上の点 Gに対して q/157をかけた G' = (q/157)G の位数は157になる。位数が小さければ離散対数問題は簡単にとける。

import hashlib
from ptrlib import Socket
import proofofwork

p = 0x402969301d0ec23afaf7b6e98c8a6aebb286a58f525ec43b46752bfc466bc435
gx = 0x3aedc2917bdb427d67322a1daf1073df709a1e63ece00b01530511dcb1bae0d4
gy = 0x21cabf9609173616f5f50cb83e6a5673e4ea0facdc00d23572e5269632594f1d
a = 0x2ad2f52f18597706566e62f304ae1fa48e4062ee8b7d5627d6f41ed24dd68b97
b = 0x2c173bd8b2197e923097541427dda65c1c41ed5652cba93c86a7d0658070c707
q = 0x402969301d0ec23afaf7b6e98c8a6aeb2f4b05d0bbb538c027395fa703234883

EC = EllipticCurve(GF(p), [a, b])
G = EC(gx, gy)
G2 = (q // 157) * G  # 157 is small factor of order

# sock = Socket("localhost", 19999)
sock = Socket("crypto2.q21.ctfsecurinets.com", 13337)
pat = sock.recvregex(r"sha256\(([A-Za-z]+) \+ X\) = 000000")[0]
print(pat)

s = proofofwork.sha256(
        '000000??????????????????????????????????????????????????????????',
        text=pat + b'?????')
print(s)
sock.sendline(s[len(pat):])

sock.sendlineafter("x : ", str(G2[0]))
sock.sendlineafter("y : ", str(G2[1]))

r, s = sock.recvregex(r"signature = \(([0-9]+), ([0-9]+)\)")
msg = "No flags for you."
z = int(hashlib.sha256(msg.encode()).hexdigest(), 16)
r, s = int(r), int(s)

for k in range(157):
    if (k*G2)[0] == r:
        print("[+] found k: {}".format(k))
        break
else:
    raise ValueError("[-] failed to calculate discrete log")

d = (s*k - z)*inverse_mod(r, 157) % 157
print("[+] found d: {}".format(d))


PROOF = "Give me flag."
z = int(hashlib.sha256(PROOF.encode()).hexdigest(), 16)
r = int((k * G2)[0])
s = (int(inverse_mod(k, 157)) * (z + r * d)) % 157


sock.sendlineafter("Message : ", PROOF)
sock.sendlineafter("r : ", str(r))
sock.sendlineafter("s : ", str(s))
sock.interactive()