VolgaCTF2020Qualifier Crypto Guess writeup
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
-
CTF wiki描述的很清楚了, ctfwiki(https://ctf-wiki.github.io/ctf-wiki/crypto/asymmetric/discrete-log/elgamal-zh/)
-
不过可能会有一些背景知识忘记了,我在b站突击补习了一下。
在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有两种生成方式
- findQNR: 生成一个随机数r1, 如果生成了一个随机数r1满足$r_1^{\frac{p-1}{2}}\neq 1 \bmod p$ ,则可以取r1: $m_1=r_1$
- 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())
总结这种方法,想起来比较简单,缺点是
- 只有50%的概率成立
- 完全没有利用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} $$
六、参考资料
- 标准解法的wp(https://www.josephsurin.me/posts/2020-03-30-volgactf-2020-qualifier-writeups#guess)
- VolgaCTF 2020 Qualifier(https://ctftime.org/event/933)
- 离散数学(全)-北京大学(https://www.bilibili.com/video/BV1BW411n7gw?p=56)
- 【国家级精品课】—-密码学( 解放军信息工程大学)(https://www.bilibili.com/video/BV1MW411t7gZ?p=19)
- 欧拉判别法(https://zh.wikipedia.org/wiki/%E6%AC%A7%E6%8B%89%E5%87%86%E5%88%99)
- 二次剩余(https://zh.wikipedia.org/wiki/%E4%BA%8C%E6%AC%A1%E5%89%A9%E4%BD%99)
- 欧拉定理(https://baike.baidu.com/item/%E6%AC%A7%E6%8B%89%E5%AE%9A%E7%90%86)
- ctfwiki(https://ctf-wiki.github.io/ctf-wiki/crypto/asymmetric/discrete-log/elgamal-zh/)