前言
前几天参加了网鼎杯的比赛,第一道题就是简单粗暴的离散对数问题(DLP)。当时自己是用sagemath的discrete_log()函数的直接秒解的(另外第三题必须要用cmd打开flag才出来就很坑)。赛后发现这道离散对数题应该是用Pohlig–Hellman algorithm 做的,所以针对这次的题目学习了一遍该算法。
群
- 定义6.7:设 H H H是群 G G G的一个子集合,如果对于群 G G G的二元运算, H H H也构成一个群,就称 H H H为群 G G G的子群,记作 H ≤ G H ≤ G H≤G。
- 定义6.8: 若群 G G G包含的元素个数有限,则称 G G G为有限群,否则称为无限群。有限群 G G G所包含的元素个数称为 G G G的阶,记为 ∣ G ∣ | G | ∣G∣。
- 定义6.9 :设 a a a是群 G G G中的一个元素,若存在正整数 n n n使得 a n = e a^n = e an=e,则称 a a a为有限阶元素,满足 a n = e a^n = e an=e的最小正整数 n n n叫做 a a a的阶,记为 ∣ a ∣ | a | ∣a∣。若不存在正整数 n n n使得 a n = e a^n = e an=e,则称 a a a为无限阶元素。
- 定义6.10 设
G
G
G为群,若存在
G
G
G的一个元素
a
a
a,使得
G
G
G中的任意元素均由
a
a
a的幂组成,即
G = { a n ∣ n ∈ Z } G = \{ a^n | n ∈ Z\} G={an∣n∈Z}
则称群 G G G为循环群,元素 a a a为循环群 G G G的生成元,记为 G = < a > G = < a > G=<a>。
后面的内容都是围绕循环群的子群以及群和元素的阶展开的。
Pohlig–Hellman algorithm
维基百科解释如下:
是一种特殊算法,在阶为光滑数的有限阿贝尔群上解决离散对数问题。
同时
先看普遍情况下的算法:
大致流程如下:
(假设阶为
ϕ
(
m
)
\phi(m)
ϕ(m))
首先,要解决的问题是
g
x
≡
h
m
o
d
m
a
n
d
n
=
ϕ
(
m
)
=
∏
i
=
1
r
p
i
e
i
g^x\equiv h \ mod\ m\ and\ n=\phi(m)=\prod_{i=1}^r p_i ^ {e_i}
gx≡h mod m and n=ϕ(m)=i=1∏rpiei
等式两边同乘上一个指数
n
p
i
e
i
\frac{n}{p_i ^ {e_i}}
piein,可以得到:
{
(
g
x
1
)
n
p
1
e
1
≡
h
n
p
1
e
1
m
o
d
m
(
g
x
2
)
n
p
2
e
2
≡
h
n
p
2
e
2
m
o
d
m
(
g
x
3
)
n
p
3
e
3
≡
h
n
p
3
e
3
m
o
d
m
.
.
.
(
g
x
r
)
n
p
r
e
r
≡
h
n
p
r
e
r
m
o
d
m
\begin{cases} (g^{x_1})^\frac{n}{p_1 ^ {e_1}}\equiv h^\frac{n}{p_1 ^ {e_1}} \ mod\ m\\ (g^{x_2})^\frac{n}{p_2 ^ {e_2}}\equiv h^\frac{n}{p_2 ^ {e_2}} \ mod\ m\\ (g^{x_3})^\frac{n}{p_3 ^ {e_3}}\equiv h^\frac{n}{p_3 ^ {e_3}} \ mod\ m\\ ...\\ (g^{x_r})^\frac{n}{p_r ^ {e_r}}\equiv h^\frac{n}{p_r ^ {e_r}} \ mod\ m\\ \end{cases}
⎩
⎨
⎧(gx1)p1e1n≡hp1e1n mod m(gx2)p2e2n≡hp2e2n mod m(gx3)p3e3n≡hp3e3n mod m...(gxr)prern≡hprern mod m
对于每一条式子来说,通过遍历 x i ∈ { 0 , 1 , . . . , p i e i } x_i\in\{0,1,...,p_i ^ {e_i}\} xi∈{0,1,...,piei}一定可以计算出一个 x i x_i xi满足: ( g x i ) n p i e i ≡ h n p i e i m o d m (g^{x_i})^\frac{n}{p_i ^ {e_i}}\equiv h^\frac{n}{p_i ^ {e_i}} \ mod\ m (gxi)piein≡hpiein mod m
所以我们可以得到:
{ x ≡ x 1 m o d p 1 e 1 x ≡ x 2 m o d p 2 e 2 x ≡ x 3 m o d p 3 e 3 . . . x ≡ x r m o d p r e r \begin{cases} x\equiv x_1 \ \mod p_1 ^ {e_1}\\ x\equiv x_2 \ \mod p_2 ^ {e_2}\\ x\equiv x_3 \ \mod p_3 ^ {e_3}\\ ...\\ x\equiv x_r \ \mod p_r ^ {e_r}\\ \end{cases} ⎩ ⎨ ⎧x≡x1 modp1e1x≡x2 modp2e2x≡x3 modp3e3...x≡xr modprer
最后用中国剩余定理可以算出最后的
x
x
x:
x
≡
x
′
m
o
d
∏
i
=
1
r
p
i
e
i
x \equiv x' \mod \prod_{i=1}^r p_i ^ {e_i}
x≡x′modi=1∏rpiei
理论依据
(为什么能算出这个 x i x_i xi?)
我们用群理论重新梳理一遍整个算法。
根据题目内容,我们可以构建生成元为
g
g
g ,阶为
ϕ
(
m
)
\phi(m)
ϕ(m)(用n表示),模
m
m
m的有限循环群
群内元素为:
G
=
{
1
,
g
1
,
g
2
,
.
.
.
,
g
n
}
G=\{1,g^1,g^2,...,g^n\}
G={1,g1,g2,...,gn}
(这里先讲一下,循环群的阶与生成元的阶是一样的,暂且认为阶为
n
n
n,因为根据欧拉定理
g
ϕ
(
m
)
≡
1
m
o
d
m
g^{\phi(m)}\equiv1 \mod m
gϕ(m)≡1modm,即使存在元素的阶t小于
n
n
n(即
g
t
≡
1
m
o
d
m
g^{t}\equiv1 \mod m
gt≡1modm),那也是
n
n
n的因子,可以通过遍历因子的方式得到
t
t
t)
通过这个循环群我们可以知道 h h h是在该群中的,而且 x ∈ { 0 , 1 , . . , n − 1 } x\in\{0,1,..,n-1\} x∈{0,1,..,n−1}
那为什么要乘上指数呢?
其实对每个式子乘上指数是为了构造多个不同的该循环群的子群。
令
g
i
=
g
n
p
i
e
i
g_i=g^\frac{n}{pi^{e_i}}
gi=gpiein,可以构造一个以
g
i
g_i
gi为生成元,阶为
p
i
e
i
p_i^{e_i}
piei的循环群子群
元素为
<
g
i
>
=
{
1
,
g
n
p
i
e
i
,
g
n
p
i
e
i
∗
2
,
g
n
p
i
e
i
∗
3
,
.
.
.
,
g
n
p
i
e
i
∗
(
p
i
e
i
−
1
)
}
<g_i>=\{1,g^\frac{n}{pi^{e_i}},g^{\frac{n}{pi^{e_i}}*2},g^{\frac{n}{pi^{e_i}}*3},...,g^{\frac{n}{pi^{e_i}}*(p_i^{e_i}-1)}\}
<gi>={1,gpiein,gpiein∗2,gpiein∗3,...,gpiein∗(piei−1)}
同时我们也构造
h
i
=
h
n
p
i
e
i
=
(
g
x
)
n
p
i
e
i
m
o
d
m
h_i=h^{\frac{n}{pi^{e_i}}}=(g^{x})^{\frac{n}{pi^{e_i}}} \mod m
hi=hpiein=(gx)pieinmodm(取模后
x
x
x变成了
x
i
x_i
xi)
因此
h
i
∈
<
g
i
>
h_i\in <g_i>
hi∈<gi>,
x
i
x_i
xi必定存在,我们可以通过遍历
x
i
∈
[
0
,
p
i
e
i
−
1
]
x_i\in [0,p_i^{e_i}-1]
xi∈[0,piei−1]找到它。
同时我们要关注
g
x
≡
h
m
o
d
m
→
(
g
n
p
i
e
i
)
x
i
≡
h
n
p
i
e
i
m
o
d
m
g^x\equiv h \ mod\ m\rightarrow (g^{\frac{n}{p_i^{e_i}}})^{x_i}\equiv h^\frac{n}{p_i ^ {e_i}} \ mod\ m
gx≡h mod m→(gpiein)xi≡hpiein mod m
实际上是
x
∈
[
0
,
n
−
1
]
→
x
i
∈
[
0
,
p
i
e
i
−
1
]
x\in[0,n-1]\rightarrow x_i\in [0,p_i^{e_i}-1]
x∈[0,n−1]→xi∈[0,piei−1]
也就是说,每个等式计算出来的
x
i
x_i
xi是原来的
x
x
x在不同模下的结果。所以我们才能用CRT把原来的
x
x
x算出来。
特殊算法(当阶为素数幂prime power):
其实就是普遍算法,为了美观代码简化了一点,差异不大。但可以与
B
S
G
S
BSGS
BSGS算法并用(其实我并不太清楚怎么并用), 时间复杂度降为
O
(
e
p
)
O(e\sqrt{p})
O(ep)。
实战演练
you_rise_me_up.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from Crypto.Util.number import *
import random
n = 2 ** 512
m = random.randint(2, n-1) | 1
c = pow(m, bytes_to_long(flag), n)
print 'm = ' + str(m)
print 'c = ' + str(c)
# m = 391190709124527428959489662565274039318305952172936859403855079581402770986890308469084735451207885386318986881041563704825943945069343345307381099559075
# c = 6665851394203214245856789450723658632520816791621796775909766895233000234023642878786025644953797995373211308485605397024123180085924117610802485972584499
离散对数问题, n n n为 2 512 2^{512} 2512,素数幂,可以使用Pohlig–Hellman algorithm。
首先找阶,因为生成元m的阶整除
ϕ
(
n
)
\phi(n)
ϕ(n),所以我们可以通过遍历
2
2
2的幂来找到这个阶。
遍历之后发现阶为
2
510
2^{510}
2510,直接使用上述特殊算法就可得到结果。
(自己写的辣鸡代码)
def Prime_power_Pohlig_Hellman(g,h,n,p,e):
'''
phi(n)=p^e
g^(p^e)=1 mod n
g^x=h mod n
'''
for a in range(e):
if(pow(g,2**a,n)==1):
break
x_k=0
e=a
gm=pow(g,pow(p,e-1,n),n)
print gm
if(pow(g,p**e,n)!=1):
print 'error'
for k in range(e):
_x=pow(p,e,n)-x_k
if(pow(g,x_k,n)*pow(g,_x,n)%n !=1 ):
print 'error'
h_k=pow(pow(g,_x,n)*h,pow(p,e-1-k,n),n)
for d_k in range(p):
if(pow(gm,d_k,n)==h_k):
print k,d_k
break
x_k=x_k+d_k*pow(p,k,n)
return x_k
if __name__ =='__main__':
n=2**512
m = 391190709124527428959489662565274039318305952172936859403855079581402770986890308469084735451207885386318986881041563704825943945069343345307381099559075
c = 6665851394203214245856789450723658632520816791621796775909766895233000234023642878786025644953797995373211308485605397024123180085924117610802485972584499
print libnum.n2s(Prime_power_Pohlig_Hellman(m, c, 2**512, 2, 511) )
最后得到
后记
这篇博客的内容是我第一次使用新学的群理论来分析(尽力了),可能有讲得不好的地方,请多多包涵。
个人体会有两个,一是其实算法并没有那么难看懂,英语也没有那么难读懂,莫名的恐惧只是因为不了解。所以多看几遍就好了。
二是我觉得了解算法就是一种说服自己的过程,这个算法为什么要这样做?理论依据又是什么?学习算法的过程中,我一直在寻找理由说服自己,这个算法这样做肯定是有它的理由的。可能有点吹毛求疵的感觉,但我只是想纯粹的去理解算法。
三是自己还有很大的进步空间。