tags: crypto,ctf,writeup

VolgaCTF2020Qualifier Crypto Guess writeup

上周末玩了下VolgaCTF 2020 Qualifier(https://ctftime.org/event/933), 只做了几道密码题,因为其他题都没有思路T_T 。整体感觉俄罗斯人的脑洞很大,数学功底很好。其中有一道ElGamal相关的密码题,虽然没感觉在现实中有什么实际用途,但是学到了一些重要的性质。

我的解法比较简单,但是好像是非预期解。赛后,在这篇wp里发现了一个更完美的解法:标准解法的wp(https://www.josephsurin.me/posts/2020-03-30-volgactf-2020-qualifier-writeups#guess) 。我会在第三节记录下我的解法,在第四节简单翻译下标准解法。

一、题目信息

Guess

Try to guess all encrypted bits and get your reward!

server.py(https://q.2020.volgactf.ru/files/541eeaf9fdf2d86fbe87def09be7e560/server.py)

nc guess.q.2020.volgactf.ru 7777

Your team has solved this task on Mar 28 at 21:20 UTC.

179 other teams have managed to do the same.

This task has been rated by 73 teams. The average rating is 3.99 (5.00 max).

server.py如下:

#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import print_function
from Crypto.PublicKey import ElGamal
from Crypto import Random
from flag_file import flag
import Crypto.Random.random
import time
import sys


"""
    Communication utils
"""

def read_message():
    return sys.stdin.readline()


def send_message(message):
    sys.stdout.write('{0}\r\n'.format(message))
    sys.stdout.flush()


def eprint(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)


"""
    Algebra
"""

def kronecker(x, p):
    q = (p - 1) / 2
    return pow(x, q, p)


def findQNR(p):
    r = Crypto.Random.random.randrange(2, p - 1)
    while kronecker(r, p) == 1:
        r = Crypto.Random.random.randrange(2, p-1)
    return r


def findQR(p):
    r = Crypto.Random.random.randrange(2, p - 1) 
    return pow(r, 2, p)


"""
    Main
"""

if __name__ == '__main__':
    try:
        while True:
            key = ElGamal.generate(512, Random.new().read)
            runs = 1000
            successful_tries = 0

            send_message('(y, p) = ({0}, {1})'.format(key.y, key.p))

            for i in xrange(runs):
                plaintexts = dict()
                plaintexts[0] = findQNR(key.p)
                plaintexts[1] = findQR(key.p)

                challenge_bit = Crypto.Random.random.randrange(0,2)
                eprint('[{0}][INFO] Bit {1} was generated.'.format(time.strftime("%Y-%m-%d. %H:%M:%S"), challenge_bit))
                r = Crypto.Random.random.randrange(1,key.p-1) 
                challenge = key.encrypt(plaintexts[challenge_bit], r)

                # Send challenge
                send_message(challenge)

                # Receive challenge_bit
                received_bit = read_message()
                eprint('[{0}][INFO] Bit {1} was received.'.format(time.strftime("%Y-%m-%d. %H:%M:%S"), received_bit))
                if int(received_bit) == challenge_bit:
                    successful_tries += 1
                    eprint(successful_tries)
                    
            if successful_tries == runs:
                send_message(flag)

    except Exception as ex:
        send_message('Something must have gone very, very wrong...')
        eprint('[{0}][ERROR] {1}'.format(time.strftime("%Y-%m-%d. %H:%M:%S"), ex))

    finally:
        pass

这个题目执行了1000次ElGamal加密算法,告知了我们ElGamal密码体制中的y, p, 及每次加密的结果y1, y2。每次加密时使用两种明文,第一种是根据findQNR函数生成,第二种是根据findQR函数生成。选手如果可以正确猜测每次使用的是哪一种明文,则可以获得flag。

二、ElGamal

在ElGamal密码体制中,$g^k \equiv y \bmod p$, 其中私钥为 {k},公钥为 {p,g,y} 。

三、我的解法: 基于欧拉定理(或称费马小定理)及Crypto.PublicKey.ElGamal的特性

我们先用数学语言描述一下我们要解决的问题:

已知y, p, y1, y2及以下关系 $$ \begin{align} & y \equiv g^k \bmod p
\newline & y1 \equiv g^r \bmod p \newline & y2 \equiv my^r \bmod p \end{align} $$

其中m有两种生成方式

  1. findQNR: 生成一个随机数r1, 如果生成了一个随机数r1满足$r_1^{\frac{p-1}{2}}\neq 1 \bmod p$ ,则可以取r1: $m_1=r_1$
  2. findQR: 生成一个随机数r2, $m_2=r_2^2$

我们需要根据已知信息,判断出来m是哪种生成方式。

其实我不理解为什么两个函数名,一个叫findQNR, 一个叫findQR。但是我发现了一个性质

$$ \begin{align} & m_1 ^ {\frac{p-1}{2}}\neq 1 \bmod p \newline & m_2 ^ {\frac{p-1}{2}}\equiv (r_2^2)^{\frac{p-1}{2}} \equiv r_2^{p-1} \equiv r_2^{\phi(p)} \equiv 1 \bmod p \tag{1} \end{align} $$

根据欧拉定理(https://baike.baidu.com/item/%E6%AC%A7%E6%8B%89%E5%AE%9A%E7%90%86),(1)式是一直成立的。

所以我们可以尝试将这个性质推广到y2上。

对 m1: $$ \begin{align} & y_2 ^ {\frac{p-1}{2}} \equiv (m_1y^r )^ {\frac{p-1}{2}} \equiv (m_1)^ {\frac{p-1}{2}} (y^r) ^ {\frac{p-1}{2}} \equiv 1 \bmod p \tag{2}
\newline & \iff \newline & ay^{r\frac{p-1}{2}} \equiv ag^{kr\frac{p-1}{2}} \equiv 1 \bmod p (a=m_1^{\frac{p-1}{2}}\bmod, a \neq 1) \newline & \iff \newline & g^{kr\frac{p-1}{2}} \equiv a^{-1} \bmod p \end{align} $$

因为p是一个很大的素数,所以由$Z_p^*$的生成元g的$kr\frac{p-1}{2}$次方,与a关于p的逆 同模,即使计算1000次,也是小概率事件的。所以我们可以大胆假设(2)式成立的概率是非常低的。

对 m2: $$ \begin{align} & y_2 ^ {\frac{p-1}{2}} \equiv (m_2y^r )^ {\frac{p-1}{2}} \equiv (m_2)^ {\frac{p-1}{2}} (y^r) ^ {\frac{p-1}{2}} \equiv 1 \bmod p \newline & \iff \newline & (y^r) ^ {\frac{p-1}{2}} \equiv 1 \bmod p \tag{3} \end{align} $$

在查看python的Crypto.PublicKey.ElGamal库时,我发现ElGamal的p选取规则是p=2q+1, 其中q是一个大素数。(这是为了防止p-1完全由小素数因子构成,受到另一种攻击)

这个特性给我们的计算带来了一些便利。我们可以基于这个特性,继续推理(3)式 $$ \begin{align} & (3)式 \iff (y^r) ^ q \equiv g^{rqk} \equiv 1 \bmod p \tag{4} \iff k或r是偶数,或g是某数的平方 \end{align} $$

因为g和k在1000次循环中都是固定的,而且k的生成方式是完全随机的, 因此可以认为大约有50%的概率(4)式成立

obj.x=number.getRandomRange(2, obj.p-1, randfunc)

至此我们获得了一种方法,有50%的概率可以区分明文是使用哪种方式生成的。

即如果k是偶数,则接下来的1000次计算中,$y_2^{\frac{p-1}{2}} \equiv 1 \bmod p$成立意味着明文由findQR生成,反之,明文由findQNR函数生成。

代码如下:

from pwn import *


def solve():
    pattern_y_p = re.compile("\(y, p\) = \(([0-9]+), ([0-9]+)\)")
    pattern_y1_y2 = re.compile("\(([0-9]+)L, ([0-9]+)L\)")

    r = remote("guess.q.2020.volgactf.ru", 7777)
    # r = process(["python", "server.py"])
    match = pattern_y_p.search(r.recvline().strip())
    y, p = int(match.group(1)), int(match.group(2))
    for i in range(1000):
        print(i)
        recv = r.recvline()
        # print(recv)
        match = pattern_y1_y2.search(recv.strip())
        y1, y2 = int(match.group(1)), int(match.group(2))
        if pow(y2, (p - 1) / 2, p) == 1:
            r.sendline("1")
        else:
            r.sendline("0")

    print(r.recvline())
    print(r.recvline())
    print(r.recvline())

总结这种方法,想起来比较简单,缺点是

  1. 只有50%的概率成立
  2. 完全没有利用y1

四、准确解法: 基于欧拉判别法(Euler’s Criterion)或二次剩余特性

这一节基本是这篇文章的翻译:标准解法的wp(https://www.josephsurin.me/posts/2020-03-30-volgactf-2020-qualifier-writeups#guess)

吃了没文化的亏,作者认识到findQNR函数中使用了欧拉判别法(https://zh.wikipedia.org/wiki/%E6%AC%A7%E6%8B%89%E5%87%86%E5%88%99), 来寻找一个随机数,使这个随机数是模p的二次剩余。

二次剩余(https://zh.wikipedia.org/wiki/%E4%BA%8C%E6%AC%A1%E5%89%A9%E4%BD%99)是指X的平方除以p得到的余数。

当存在某个${\displaystyle X},{\displaystyle X^{2}\equiv d{\pmod {p}}}$ 成立时,称“ d是模p的二次剩余”

当对任意 ${\displaystyle X}, {\displaystyle X^{2}\equiv d{\pmod {p}}}$ 不成立时,称“d是模 p的二次非剩余”

欧拉判别法描述了这样一个性质, 若p是奇质数且p不能整除d,则:

d是模p的二次剩余当且仅当: $$ {\displaystyle d^{\frac {p-1}{2}}\equiv 1{\pmod {p}}} $$ d是模p的非二次剩余当且仅当: $$ {\displaystyle d^{\frac {p-1}{2}}\equiv -1{\pmod {p}}} $$

因此在这个题目中,findQNR函数是寻找一个模p的二次非剩余m1, 而findQR是生成一个模p的二次剩余m2。

我们可以根据二次剩余的性质来解这个题: 已知y, p, y1, y2及以下关系 $$ \begin{align} & y \equiv g^k \bmod p
\newline & y1 \equiv g^r \bmod p \newline & y2 \equiv my^r \bmod p \end{align} $$

  • 如果y是二次剩余,则y2是否是二次剩余 等价于 m是否是二次剩余
  • 如果y不是二次剩余, 因为$y^r\equiv(g^k)^r\equiv(g^r)^k\equiv y_1^k \bmod p$, 所以
    • 如果y1是二次剩余,则y2是否是二次剩余 等价于 m是否是二次剩余
    • 如果y1是二次非剩余,则y2是否是二次剩余 等价于 m是否是二次非剩余

amazing!

作者的代码如下:

from pwn import remote

def ec(x, p):
    q = (p - 1) / 2
    return pow(x, q, p)

def qr(x, p):
    return ec(x, p) == 1

conn = remote('guess.q.2020.volgactf.ru', 7777)

y, p = map(int, conn.recvline().split('= (')[1][:-3].split(', '))

for i in range(1000):
    c1, c2 = eval(conn.recvline())

    b = ''
    if qr(y, p):
        if qr(c2, p):
            b = '1'
        else:
            b = '0'
    else:
        if qr(c1, p):
            if qr(c2, p):
                b = '1'
            else:
                b = '0'
        else:
            if qr(c2, p):
                b = '0'
            else:
                b = '1'
    conn.sendline(b)
    print('challenge', i)
print(conn.recvline())

五、扩展

1. 基于Crypto.PublicKey.ElGamal库的优化

在写本文时,我发现了一个新的性质。

翻阅Crypto.PublicKey.ElGamal源码,我发现g就是关于p的二次剩余

obj.g = pow(number.getRandomRange(2, obj.p, randfunc), 2, obj.p)

这个发现无论是对我的解法,还是第二种解法,都有很大帮助。

  • 对我的解法来说,会将解题失败的概率限制在$1-(1-P_r(m_1^\frac{1-p}{2}\equiv 1\bmod p))^{1000}$
  • 对于二次剩余解法,会将问题直接简化成:y2是否是二次剩余 等价于 m是否是二次剩余

有兴趣的读者可以继续推理下

2. 其他性质

Crypto.PublicKey.ElGamal库中,g可以生成一个q阶的循环群

同样是刚刚那段代码,有一个注释称,g可以生成一个q阶的循环群

# Choose a square residue; it will generate a cyclic group of order q.
obj.g = pow(number.getRandomRange(2, obj.p, randfunc), 2, obj.p)

另外,该库还限制了g与p-1的一些关系。

while 1:
    # Choose a square residue; it will generate a cyclic group of order q.
    obj.g = pow(number.getRandomRange(2, obj.p, randfunc), 2, obj.p)

    # We must avoid g=2 because of Bleichenbacher's attack described
    # in "Generating ElGamal signatures without knowning the secret key",
    # 1996
    if obj.g in (1, 2):
        continue

    # Discard g if it divides p-1 because of the attack described
    # in Note 11.67 (iii) in HAC
    if (obj.p - 1) % obj.g == 0:
        continue

    # g^{-1} must not divide p-1 because of Khadir's attack
    # described in "Conditions of the generator for forging ElGamal
    # signature", 2011
    ginv = number.inverse(obj.g, obj.p)
    if (obj.p - 1) % ginv == 0:
        continue

    # Found
    break

g可以生成关于p的循环群好理解,我们可以简单证明这个循环群的阶数是q如下:

$$ \begin{align} & g^1 \equiv g \bmod p \\ & g^q \equiv r^{2q} \equiv 1 \bmod p \end{align} $$

六、参考资料