TL;DR

  • xix_iを与えるとgm(xi)mod  pg^{m(x_i)} \mod pをくれるクエリに17回問い合わせられる
  • そこから任意のxxに対してgm(x)mod  pg^{m(x)} \mod pを100回正確に求めるチャレンジがある
  • 線形代数と基底簡約を利用してppの倍数を複数用意し、そこから最大公約数を求めてppを導出する
  • 任意のxxに対して、m(x)m(x)が既知のm(xi)m(x_i)の線形結合で表現出来るので、gsim(xi)mod  p\prod g^{s_im(x_i)} \mod pを求めてチャレンジに提出する

Prerequisite

  • 線形代数
  • 基底簡約

Writeup

次のようなスクリプトが動いている

#!/usr/bin/python3

from sys import stdin, stdout, exit
from secrets import randbelow
from gmpy2 import next_prime

from flag import FLAG


class BabyZK:

    def __init__(self, degree, nbits):
        self.p = self.__safeprime(nbits)
        self.degree = degree
        self.m = [randbelow(self.p-1) for i in range(self.degree)]
        self.g = 2 + randbelow(self.p-3)
        self.ctr = 0

    def __safeprime(self, nbits):
        stdout.write("Generating safeprime...")
        p = -1
        while True:
            q = next_prime(randbelow(2 * 2**nbits))
            p = 2*q + 1
            if p.is_prime():
                break
        return p

    def __eval(self, x: int) -> int:
        y = 0
        for a in self.m:
            y += y * x + a
        return y % (self.p-1)

    def prover(self, x: int) -> int:
        if self.ctr > self.degree + 1:
            raise Exception("Sorry, you are out of queries...")
        self.ctr += 1
        return int(pow(self.g, self.__eval(x), self.p))

    def verify(self, x: int, u: int):
        if not u < self.p or u < 0:
            raise Exception("Oof, this is not mod p...")
        if int(pow(self.g, self.__eval(x), self.p)) != u:
            raise Exception("No can do...")


bzk = BabyZK(15, 1024)

def prove():
    stdout.write("> ")
    stdout.flush()
    challenge = int(stdin.readline())
    stdout.write("%d\n" % bzk.prover(challenge))
    stdout.flush()

def verify():
    for i in range(100):
        challenge = randbelow(bzk.p)
        stdout.write("%d\n" % challenge)
        stdout.flush()
        response = int(stdin.readline())
        bzk.verify(challenge, response)
    stdout.write("%s\n" % FLAG)
    stdout.flush()

banner = lambda: stdout.write("""
1) Query the prover oracle.
2) Prove to verifier that you know the secret.
3) Exit.
""")

choices = {
    1: prove,
    2: verify,
    3: exit
}

banner()
stdout.flush()

while True:
    try:
        choice = stdin.readline()
        choices.get(int(choice))()
    except Exception as e:
        stdout.write("%s\n" % e)
        stdout.flush()
        exit()

安全素数pppp未満の整数ggに対してFp\mathbb F_p上の14次多項式m(x)m(x)が定義されており、どれも未知である。これに対して次の2つのコマンドを実行出来る。

  1. 17回までxix_iを指定してgm(xi)mod  pg^{m(x_i)} \mod pを得る
  2. xix_iが指定され、gm(xi)mod  pg^{m(x_i)} \mod pを提出するチャレンジを100回行い、全部成功するとフラグが得られる。1回でも失敗すると終了する

ppが安全素数であることから、指数を直接求めることは難しく、このことから多項式mmを求めることが難しい。そもそもggすら分かっていないし求める方法も思い浮かばない。そこで、m,gm,gを求めるのは諦めてppを求めた上で2のチャレンジに成功する事を考える。

m(x)=i=014mixim(x) = \sum_{i=0}^{14} m_i x^iとおく。すると、1のクエリで指定したxix_iに対してm(xi)m(x_i)は次のような行列積で表される。17回クエリを発行出来るので1i171 \leq i\leq 17とする。

(1x1x12x1141x2x22x2141x17x172x1714)(m0m1m14)=(m(x1)m(x2)m(x17)) \begin{pmatrix} 1 & x_1 & x_1^2 & \dots & x_1^{14} \cr 1 & x_2 & x_2^2 & \dots & x_2^{14} \cr &&&\vdots \cr 1 & x_{17} & x_{17}^2 & \dots & x_{17}^{14} \cr \end{pmatrix} \begin{pmatrix} m_0 \cr m_1 \cr \vdots \cr m_{14} \end{pmatrix} = \begin{pmatrix} m(x_1)\cr m(x_2)\cr \vdots\cr m(x_{17}) \end{pmatrix}

わかりやすさの為に左辺の17x15行列をBB、(m0,m1,,m14)Tm_0, m_1, \dots, m_{14})^Tm\boldsymbol m、右辺をa\boldsymbol aとおいて、Bm=aB\boldsymbol m = \boldsymbol aとする。ここである整数xxに対してx=(1,x,x2,,x14)\boldsymbol x = (1, x, x^2,\dots,x^{14})を考えると、BBの次元が15次元なのでsB=x\boldsymbol sB = \boldsymbol xとなるs=(s1,s2,,s17)\boldsymbol s = (s_1, s_2, \dots, s_{17})が存在する。

よって、上の式に両辺からs\boldsymbol sを左から掛けると次が成り立つ。

m(x)=xm=sa=1i17sim(xi) m(x) = \boldsymbol x \boldsymbol m = \boldsymbol s \boldsymbol a = \sum_{1 \leq i \leq 17} s_im(x_i)

ggの指数にこれらを用いて次が成り立つ。

gm(x)1i17(gm(xi))simod  p g^{m(x)} \equiv \prod_{1 \leq i \leq 17} \left(g^{m(x_i)}\right)^{s_i} \mod p

17回のクエリによって、gm(xi)g^{m(x_i)}1i171 \leq i\leq 17の範囲なら既知であり、簡単な行列の計算によってs\boldsymbol sも既知である。したがって後はppを求める事さえできれば、2のチャレンジは突破出来る。

肝心のppを求める方法だが、ppを法として合同な2つの数を用意するとその差がppの倍数となるから、それを複数集めてGCDを求めればpp(と比較的小さい数の積)が手に入ることが期待出来る。

手元にあるのはgm(xi)mod  pg^{m(x_i)} \mod pが17個だけであるから、grim(xi)gsim(xi)mod  p\prod g^{r_im(x_i)} \equiv \prod g^{s_im(x_i)} \mod pとなるような、(r1,,r17),(s1,,s17)(r_1, \dots, r_{17}), (s_1, \dots, s_{17})を求めたい。これは結局指数法則によって次のような(r1,,r17),(s1,,s17)(r_1, \dots, r_{17}), (s_1, \dots, s_{17})を求めることになる。modp1\bmod p-1による合同ではなく、イコールなのはppが判明していないからである。

1i17rim(xi)=1i17sim(xi)1i17(risi)m(xi)=0 \begin{aligned} \sum_{1 \leq i \leq 17} r_im(x_i) = \sum_{1 \leq i \leq 17} s_im(x_i) \Leftrightarrow \sum_{1 \leq i \leq 17} (r_i - s_i)m(x_i) = 0 \end{aligned}

tirisit_i \coloneqq r_i - s_iとおいて、t=(t1,t2,,t17)\boldsymbol t =(t_1, t_2, \dots, t_{17})とすれば、上のBm=aB\boldsymbol m = \boldsymbol aの式より、tB=0\boldsymbol tB = 0となるt\boldsymbol tを求めれば良く、これはBBの核を求めることに同じである。このt\boldsymbol tに対してta=0\boldsymbol t\boldsymbol a = 0となるので、gtim(xi)1mod  p\prod g^{t_im(x_i)} \equiv 1 \mod pとなって、左辺がppで法を取らずに計算出来れば両辺から1を引くことでppの倍数を求められそうである。

ただし、ここで求めたt\boldsymbol tにはppが判明していない事に由来する次の2つの問題がある。

  1. ppが判明していないのに、負の数がt\boldsymbol tに含まれており、逆数が計算できない
  2. ppが判明していないため、gtim(xi)g^{t_im(x_i)}tit_iの大きさ次第で非常に大きくなり、時間空間計算量が爆発する

1については簡単に解決出来て、負であるようなtit_iは両辺にgtim(xi)g^{-t_im(x_i)}を掛けることで次のような式に変形出来る事を利用する。

ti0gtim(xi)ti<0gtim(xi)mod  p \prod_{t_i \geq 0} g^{t_im(x_i)} \equiv \prod_{t_i \lt 0} g^{-t_im(x_i)} \mod p

これで左辺も右辺も計算出来るので、この差をとればppの倍数が手に入る。

面倒なのは2の方でSageMathの.kernel()で求めた核は10510^5弱の大きな値が含まれており、各gm(xi)mod  pg^{m(x_i)} \mod pが1024bit程度と考えると、gtim(xi)g^{t_im(x_i)}ppで法をとらないため1億bitぐらいの巨大な数になってしまう。

(※ここでかなり詰まって恒例のWriteupカンニングタイムになった)

そこで登場するのが基底簡約である。kerB\ker Bの核(2次元)を基底とする格子を考え、これを簡約した基底もまた核となるので、簡約するとtit_iが大きくても10410^4未満の値に落ち着いてくれる。これで無事に計算量的に問題無いt\boldsymbol tが求まった事になるのでti0gtim(xi)ti<0gtim(xi)\prod_{t_i \geq 0} g^{t_im(x_i)} - \prod_{t_i \lt 0} g^{-t_im(x_i)}を2つ計算して最大公約数を取り、2や3といった小さい余分な数が含まれているからこれを除き、1つの大きな素数を得てppが求められる。

後は前半で述べたようにチャレンジで用いるxxに対してsB=x\boldsymbol sB = xちなるようなs\boldsymbol sを求めて、(gm(xi))simod  p\prod (g^{m(x_i)})^{s_i} \mod pを計算すれば、gm(x)g^{m(x)}が求められる。

Code

from pwn import process, remote


def choice(c):
    sc.sendline(str(c).encode())


def prove(x):
    choice(1)
    sc.recvuntil(b"> ")
    sc.sendline(str(x).encode())
    m_x = int(sc.recvline())

    return m_x


def verify(p, xs, l):
    chall = int(sc.recvline())
    rhs = [chall**i for i in range(15)]
    rhs = vector(rhs)
    exps = l.solve_left(rhs)
    ans = 1
    for x, e in zip(xs, exps):
        e %= (p-1)
        ans *= (F_p(x) ^ e)

    sc.sendline(str(int(ans)).encode())


l = [
    [x**i for i in range(15)] for x in range(17)
]

l = matrix(l)
ker_l = matrix(l.kernel().basis())
ker_l = ker_l.LLL()


print("[+] Init")
sc = process(["python3", "chall.py"])
sc.recvuntil(b"3) Exit.\n")
print("[+] Done")

g_pow_mx = []
for x in range(17):
    g_pow_mx.append(prove(x))

kps = []
for j in range(2):
    lhs = 1
    rhs = 1
    for x in range(17):
        e = ker_l[j][x]
        if e > 0:
            lhs *= (g_pow_mx[x] ^ e)
        else:
            rhs *= (g_pow_mx[x] ^ (-e))

    kps.append(lhs - rhs)

print("[+] two multiples of p are prepared")
p = gcd(kps)
factors = list(factor(p))
for _p, e in factors:
    if e == 1:
        p = _p

assert is_prime(p)
q = (p-1)//2
assert is_prime(q)
print("[+] Prime is calculated")
print(f"{p=}")
F_p = GF(p)

choice(2)

for i in range(100):
    print(i)
    verify(p, g_pow_mx, l)

sc.interactive()

Flag

ローカルでやっただけなので無し

Resources