烧包包儿的博客


  • Home

  • Archives

ECC 椭圆曲线加密算法学习————从实数域到有限域的椭圆曲线

Posted on 2020-05-22

ECC 椭圆曲线加密算法学习————从实数域到有限域的椭圆曲线

标签(空格分隔): ecc Crypto


****
> File Name: ECC 椭圆曲线加密算法学习————从实数域到有限域的椭圆曲线
> Author: shaobaobaoer
> Mail: shaobaobaoer@126.com
> WebSite: shaobaobaoer.cn
> Time: Sunday, 24. March 2019 11:53AM
****

0x00 前言

我好像很久没有自己学点东西了,从寒假上来就一直在做期末大作业然后考试。现在算是考完了,但是也要意味着考研复习要彻底拉开帷幕了。很多想去复现的CVE只能在计划本上越排越后面。有个东西一直想去看看,就是ECC。上学期学了数论的选修课,

参考的网站

  • https://andrea.corbellini.name/2015/05/17/elliptic-curve-cryptography-a-gentle-introduction
  • https://andrea.corbellini.name/2015/05/23/elliptic-curve-cryptography-finite-fields-and-discrete-logarithms/

相关代码

  • 本文的完整代码
    • https://github.com/ninthDevilHAUNSTER/ecc_learning
  • Schoof 算法Python版本
    • https://github.com/pdinges/python-schoof

预先准备的数学概念

群 Group

这个概念来自于离散数学,有所困惑还请多翻翻书。对于一个阿贝尔群(有些书上管它叫交换群或者加群,相信学过离散数学的你对它一定记忆犹新)它的性质包括

  • 具有封闭性
  • 满足结合律
  • 存在单位元
  • 每个数存在逆元(备注,知乎上的翻译翻错了)
  • 满足交换律

整数域和有限域

首先,有限域是一个带有有限元素的集合。比如,有一个有限域是整数模p的集合(integers mod p,p是素数),可表示为 $\mathbb {Z}/p,GF(p)$ 或 $\mathbb {F}_{p}$ ,一般用后者。

模运算 mod

这个概念来自于数论,有所困惑的还请找潘兄弟俩的书看看。
需要用到的知识在下面罗列

  • 模运算的加法
  • 模运算的乘法
  • 乘法逆元,简称逆元
    • $ 45 \times x \equiv 1 (mod ;23) $
    • 得出 $ x = -1 $所以,-1是45对模23的逆元
    • 关于乘法逆元可以用辗转欧几里得算法来快速求出。代码在misc/shaobaobaoer_math_lab.py下。

形如$ y^2 ;\equiv ;A;(;mod;p;)$的同余方程求解问题

参考网站
https://www.johndcook.com/blog/quadratic_congruences/?tdsourcetag=s_pctim_aiomsg
https://blog.csdn.net/ACdreamers/article/details/10182281
C代码模板:
https://blog.csdn.net/Quack_quack/article/details/50189111

其中p是一个奇素数。其中需要用到一些二次剩余得知识证明。在两篇文章中都有说,这里就记录一些结论。

确定方程是否有解?

方程有解当且仅当 $A^{\frac{p-1}{2}}\equiv 1 (mod p)$

确定方程的解

如何计算方程的解呢?
网上搜下来都是ACM的题目了。顿时感觉自己血菜。在ACMer的博客里看到了 cipolla算法。代码量用python写起来不是很复杂,但是这个东西暂时还看不懂,魔改了一下 cipolla 的模板。之后也许会填坑

三次一元方程解的形式

对于如下形式的方程
$$ ax^3 + bx^2 + cx + d = 0$$
存在如下结论
$x1+x2+x3=-b/a$
$x1x2+x2x3+x3x1=c/a$
$x1
x2*x3=-d/a$

现在,方程变为如下形式
$$ (kx + c)^2 = x^3 + ax +b ;;; \Longleftrightarrow ;;;x^3 - k^2x^2 + (a-2kc)x + b - c^2 = 0$$

可以得到如下结论,该结论可以节省很多解方程的时间

$$x3= k^2 - x1 - x2$$

0x01 椭圆曲线定义

首先来看下椭圆曲线的标准形式公式:

$$y^2 = x^3 + ax +b$$

其中 $4a^3 + 27b^2 \ne 0 $ 这个限定条件保证曲线不包含奇点。
这样的椭圆曲线被称为 Weierstrass 标准形式。

当然,为了定义椭圆曲线,还需要一个无穷远的点作为曲线的一部分,在这里用符号 $\odot$ 表示。

最终,将表达式可以精炼为
$$\left{ (x, y) \in \mathbb{R}^2\ |\ y^2 = x^3 + ax + b,\ 4 a^3 + 27 b^2 \ne 0 \right}\ \cup\ \left{ \odot \right}$$

根据解析式,不难发现该曲线关于x轴对称,之后会用到其性质

1.1 标准形式的椭圆曲线绘制

执行如下代码可以画出来一个椭圆曲线。

1
2
3
4
5
6
7
8
9
10
# draw_curve/draw.py
from matplotlib import pyplot as plt
import numpy as np
a = -2
b = 3
y, x = np.ogrid[-5:5:100j, -5:5:100j]
plt.contour(x.ravel(), y.ravel(), pow(y, 2) - pow(x, 3) - x * a - b, [0])
plt.grid()
plt.legend()
plt.show()

image_1d6kcqrgb13j7sbbqsl5pq1t449.png-24kB

1.2 椭圆曲线上的群G

之后,有了这个可爱的曲线之后,我们定义一个群G

  • 群众元素满足上头的表达式,也就是说,群中元素是椭圆曲线上的点
  • 单位元$e$是无穷远处的点$\odot$
  • 逆元$p$是原坐标点关于x轴对称的那一点。
  • 定义一种在该群上的加法,取一条与曲线相交于三点的直线,记交点为$P,Q,R$,他们的总和等于0,就是说$P+Q+R=0$。这三点没有顺序,所以可以证明该椭圆曲线满足交换律。

之后,可以证明这个群是一个典型的阿贝尔群。

1.3 群G上加法运算的各种情况

为了方便表示,可以把该加法写成$P+Q=-R$
这样写,其中的-R有几何含义,也就是R关于X轴对称的一点,如图

image_1d6ke29sgttoe4f1jmf1j7e18cnm.png-60.9kB

之后,讨论三种情况。

CASE 1 P = Q :

$k = \cfrac{3x^2_0 + a }{2y_0}$

得知直线方程后,计算$R_x= k^2 - x1 - x2$

CASE 2 P != Q 并且 P + Q = -R:

$k = \cfrac{y_P - y_Q}{ x_P - x_Q}$

得知直线方程后,计算$R_x= k^2 - x1 - x2$

CASE 3 P != Q 并且 P + Q = -P OR P + Q = -Q:

这就是之前说到的第三种情况,在这种情况下,只需要计算

$ \cfrac{3x^2_0 + a }{2y_0} - \cfrac{y_P - y_Q}{ x_P - x_Q} $是否等于零

这种情况下,$P,Q$必有一点满足上式。将该点坐标的纵坐标求反即得到 -R。实际运算的时候可以和CASE2合并。

之后,就可以写一个函数来计算-R的值了,在此就罗列一些关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def get_three_pionts(self, P, Q):
if P == (0, 0) or Q == (0, 0):
return Q if P == (0, 0) else P

if self.__check_on_curve(P) and self.__check_on_curve(Q):
if P == Q:
# CASE 1
k = (3 * P[0] ** 2 + self.a) / (2 * P[1])
b = P[1] - k * P[0]
x = k ** 2 - P[0] - Q[0]
y = k * x + b
R = (x, -y)
else:
# CASE 2
k = (P[1] - Q[1]) / (P[0] - Q[0])
b = P[1] - k * P[0]
x = k ** 2 - P[0] - Q[0]
y = k * x + b
R = (x, -y)
return R
else:
raise ValueError

1.4 标量积与对数问题

定义一种变量积如下所示,实际上可以用快速幂来解决这个数乘问题 $$nP = \underbrace{P + P + \cdots + P}_{n\ \text{times}}$$

OK,对于给定的$Q$($Q=nP$)和$P$是否能够找到这个n呢?当然,这玩意儿不叫除,而是叫对数。(为了和其他密码中“幂”的说法保持一致,从某种程度上而言,这个也算是幂而非数乘)

刚才说了,加法是有特殊的定义的。然而一个个加P又非常蠢,有什么好的方法呢?其中一个比较好的算法是倍加算法。原理很简单,可以看如下公式

n = 151, 二进制表示: $10010111_2$ ,这个二进制也可以表示成幂次加和:

$\begin{array}{rcl} 151 & = & 1 \cdot 2^7 + 0 \cdot 2^6 + 0 \cdot 2^5 + 1 \cdot 2^4 + 0 \cdot 2^3 + 1 \cdot 2^2 + 1 \cdot 2^1 + 1 \cdot 2^0 \ & = & 2^7 + 2^4 + 2^2 + 2^1 + 2^0 \end{array}$

简化一下:

$151 \cdot P = 2^7 P + 2^4 P + 2^2 P + 2^1 P + 2^0 P$

在之后,可以写一下小把戏来达成这个加法,bits函数其实就是一个迭代器。而每次更新P和R的值。R是最后的结果,而P则是每次自加,形如$2^n P$,n为迭代次数。

1
2
3
4
5
6
7
8
9
10
11
12
def get_scalar_multiplication(self, n, P):
"""
Returns the result of n * x, computed using
the double and add algorithm.
"""
self.R = (0, 0)
self.P = P
for bit in bits(n):
if bit == 1:
self.R = self.get_three_pionts(self.P, self.R)
self.P = self.get_three_pionts(self.P, self.P)
return self.R

在连续曲线上,这个n是非常好找的,(当然你要算很多次)但是如果在离散领域呢?就会变得更加复杂。这是椭圆曲线加密算法的核心所在。

0x02 有限域和离散对数领域的椭圆曲线

之前说了关于标量积的相关知识,在看这节之前,还需要知道数论的相关知识

之后,将椭圆曲线和有限域 $\mathbb {F}{p}$结合起来,在$\mathbb {F}{p}$上定义椭圆曲线,形式是点集。于是它变成了这样子,注意,在计算 $y^2 \equiv A (mod ;p)$的时候推荐用前文说到的cipolla算法

image_1d6kgl87h19krnf911311hvhej613.png-8.7kB

参考一些图片如下所示:这就是画出来的图
a=-7;b=10;p=19
discreteEC_-7_10_19_12_48AM.png-13.2kB
a=-7;b=10;p=97
discreteEC_-7_10_97_12_48AM.png-13.7kB

$\mathbb {F}_{p}$本身是一个阿贝尔群。不难证明对于在离散对数域内的椭圆曲线,仍然是一个阿贝尔群。【证明在最后】

另外,回到之前刚刚说的奇点,所谓奇点,就是说在曲线中会算出来在(0,0)处的点。注意我们定义的单位元$\odot $ 的位置就是(0,0)。为了不让曲线上的点与单位元重合,所以要排除掉含有奇点的曲线。比如说曲线线$$y^2 \equiv x^3 (mod;29)$$,令$x=0$,$y=\pm 0$,可以发现在(0,0)处有三个点(别忘了还有一个单位元)。所以是一个无效的椭圆曲线

2.1 离散域上点的加法

之前谈及过,$P + Q =-R$这玩意儿在实数域自然没有问题。在有限域之内也可以参照相同的定义

对此,设$L : ax + by + c \equiv 0 (mod ; p)$为$\mathbb {F}_{p}$ 上一条线。同样可以通过两点确定一条直线。

和之前证明的东西类似,只不过都需要加上$mod;p$。

对此,主要讨论之前出现的两种情况,大胆利用实数域的做法,实际上方法一样。

CASE 1 P = Q :

$k \equiv \cfrac{3x^2_0 + a }{2y_0} (mod ; p)$

得知直线方程后,计算$R_x \equiv k^2 - x1 - x2 (mod ;p )$

CASE 2 P != Q 并且 P + Q = -R:

$k \equiv \cfrac{y_P - y_Q}{ x_P - x_Q} (mod ; p)$

得知直线方程后,计算$R_x \equiv k^2 - x1 - x2 (mod ;p )$

CASE 3 P != Q 并且 P + Q = -P OR P + Q = -Q:

这就是之前说到的第三种情况,在这种情况下,只需要计算

$ \cfrac{3x^2_0 + a }{2y_0} (mod ; p) - \cfrac{y_P - y_Q}{ x_P - x_Q} (mod ; p) \equiv 0 (mod ; p)$

这种情况下,$P,Q$必有一点满足上式。将该点坐标的纵坐标求加法逆元即得到 -R。实际运算的时候可以和CASE2合并。

之后,就可以写一个函数来计算-R的值了,在此就罗列一些关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def get_three_pionts(self, P, Q):
R = (0, 0)
if P == (0, 0) or Q == (0, 0):
return Q if P == (0, 0) else P
if self.__check_on_curve(P) and self.__check_on_curve(Q):
# CASE 1 P == Q
if P == Q:
k = (3 * P[0] ** 2 + self.a) * egcd(2 * P[1], self.p)
k %= self.p
b = P[1] - k * P[0]
b %= self.p
x = k ** 2 - P[0] - Q[0]
y = k * x + b
R = (positive_mod(x, self.p), positive_mod(self.p - y, self.p))
else:
# CASE 2 P != Q
k = (P[1] - Q[1]) * egcd(P[0] - Q[0], self.p)
k %= self.p
b = P[1] - k * P[0]
b %= self.p
x = k ** 2 - P[0] - Q[0]
y = k * x + b
R = (positive_mod(x, self.p), positive_mod(self.p - y, self.p))
return R
else:
raise ValueError

2.2 椭圆曲线阶

既然是在有限域$\mathbb {F}_{p}$内,那么椭圆曲线应该是有限个点构成的,所以,到底有多少个点呢?

对于一个群而言,有多少点就叫做这个群的阶。比如模13群的阶为 13。同样,对于有限域$\mathbb {F}_{p}$上的椭圆曲线而言,也有对应的阶。枚举当然是很蠢的,有种算法叫做Schoof算法,它的复杂度是多项式时间,算法比较复杂。可以在github上找到其代码https://github.com/pdinges/python-schoof

注意需要用 Linux 和 python3 运行,大概的用法如下所示:

1
2
➜  python-schoof git:(master) ✗ python3  naive_schoof.py 23 4 2
Counting points on y^2 = x^3 + 4x + 2 over GF<23>: 21

2.3 从数乘到循环子群

之前实现了连续域内的数乘,离散领域内容也是类似,利用相同的幂次加和来计算nP。唯一需要注意的就是对于n=0而言,直接返回单位元$\odot$(0,0)即可。主要代码如下:

1
2
3
4
5
6
7
8
9
def get_scalar_multiplication(self, n, P):
if n == 0: return 0, 0
self.R = (0, 0)
self.P = P
for bit in bits(n):
if bit == 1:
self.R = self.get_three_pionts(self.P, self.R)
self.P = self.get_three_pionts(self.P, self.P)
return self.R

实际上还有一些优化策略,会在之后讨论。

当写完这个函数后,会发现椭圆曲线上有个很有意思特点,任取一条曲线如:
$$ y^2 \equiv x^3 + 2x + 3 (mod ;97)$$
已知点P(3,6)在该曲线上。

我们来算算 $nP$是多少。

  • $0P \rightarrow \odot$
  • $1P \rightarrow (3,6)$
  • $2P\rightarrow (80,10)$
  • $3P\rightarrow (80,87)$
  • $4P\rightarrow (3,91)$
  • $5P\rightarrow \odot$ 备注:计算斜率的会出现(n * x + p * y) % p != gcd的情况,这也就意味着该点位于无穷远处
  • $1P \rightarrow (3,6)$
  • $2P\rightarrow (80,10)$
  • $3P\rightarrow (80,87)$
  • $9P\rightarrow (3,91)$
  • $10P \rightarrow \odot$

不难发现P的倍乘是一个5阶循环群,生成元是$\langle \odot \rangle$,$\langle P \rangle$,$\langle 2P \rangle$,$\langle 3P \rangle$,$\langle 4P \rangle$

同时可以发现,P的加法是一个闭环。也就意味着数乘的数字是可以提取的,如下面的公式所示:
$$mP + nP = \underbrace{P + P + \cdots + P}{m\ \text{times}} + \underbrace{P + P + \cdots + P}{n\ \text{times}} = (m + n)P $$

于是,可以证明 nP的集合是椭圆曲线形成的群里的一个具有循环性质的子群(the set of the multiples of P is a cyclic subgroup of the group formed by the elliptic curve.) 这里的P是生成元,或者叫基点。

也就是说,如果我们知道它是多少阶的循环群的话,计算 nP的代码就可以更加精简。

2.4 循环子群的阶讨论

之前不难发现,P生成子群地阶是多少。之前那个P的倍乘是一个5阶循环子群,对于其他的P和曲线而言不一定适用。那么,如何去计算阶数n呢?显然用 Schoof算法不可行,毕竟它是对于一整个椭圆曲线适用,对于其子群无效。

  • 设一个群G的阶为N。任取一个生成自群G的循环子群,设它的阶数是n,生成元为P, n,P 满足的条件是 nP=0 。就好像之前的 5P=0 。(这里的群说的是有限域内的椭圆曲线,只说思路,没加证明)
  • P 的阶和椭圆曲线是有联系的,根据拉格朗日定理,子群的阶是父群的阶的因子。换句话说,如果一个椭圆曲线包含 N 个点,它的一个子群包含 n 个点,那么 n 是 N 的因子。

结合上述两个定律,那么n的计算可以通过如下步骤

  • 用 Schoof 计算 椭圆曲线的阶 $N$
  • 从小到大遍历 $N$ 的所有因子
  • 找到最小的$n$ 使得 $nP = 0$

2.5 寻找子群与离散对数问题

之前讨论了很多椭圆曲线上的东西。那么,如何利用ECC去加密解密呢?

首先补充一点拉格朗日定理的东西。拉格朗日定理证明$h = N/n$永远是一个整数,称 $h$为辅因子。引入$h$。那么可以得到$n(hP)=0$

由此,总结一下ECC的算法步骤

  • 计算椭圆曲线的阶 $N$
  • 选择一个阶为 $n$ 的子群,n为素数且为$N$的因子。
  • 计算辅因子 $h = N/n$
  • 在曲线上选择一个基点 P
  • 计算 $G = hP$
  • 如果G是0,那么重新选择基点否则找到了阶为$n$,生成元为$P$的子群
。

现在,假设有一条的在有限域$\mathbb {F}_{p}$上的椭圆曲线,要解决的问题是:
如果我们已知 P和Q ,对于 Q = kP ,我们应该怎么去计算这个 k ?
这个是椭圆曲线中大名鼎鼎的离散对数问题。也是其最为核心之处。

ECC有趣的地方在于,它的离散问题看上去比其他密码学中的离散问题难多了。这就说明我们可以用更少的位数的整数 k 做到和其他加密算法一样安全级别的加密效果。

ECC 椭圆曲线加密算法学习————安全性问题与实战

Posted on 2020-05-22

ECC 椭圆曲线加密算法学习————安全性问题与实战

标签(空格分隔): ecc Crypto


****
> File Name: ECC 椭圆曲线加密算法学习————安全性问题与实战
> Author: shaobaobaoer
> Mail: shaobaobaoer@126.com
> WebSite: shaobaobaoer.cn
> Time: Sunday, 24. March 2019 11:05PM
****

0x00 前言

之前学习了ECDSA 和 ECDH 算法。不难发现椭圆曲线的离散对数难题对该密码的安全性有着多么重要的作用。之前谈及,椭圆曲线的离散对数难题非常难,尽管如此,也应该有些方法可以解开这个问题。就好像对于模运算的密码系统,比如RSA而言,可以用yafu工具来强解,也可以上某网站查表,也包括一些共模攻击,小指数攻击等方法。

参考的网站

  • https://andrea.corbellini.name/2015/06/08/elliptic-curve-cryptography-breaking-security-and-a-comparison-with-rsa/
  • https://github.com/p4-team

相关代码

  • 本文的完整代码
    • https://github.com/ninthDevilHAUNSTER/ecc_learning

0x01 BSGS 小步大步法

方法的英文名是Baby-step, giant-step,这个算法基于一个非常简单的道理

$$Q = xP = (am +b)P \Longrightarrow Q -amP = bP$$

1.1 算法简介

BSGS方法的步骤就是中间相遇。算是一种比较机智的暴力搜索法。该算法的工作步骤如下。

对于知道公钥和域参数六元组的情况下

  • 计算 $m = \sqrt{n}$
  • 对于 $b={0..m}$,计算$bP$并打表
  • 对于 $a={0..m}$
    • 计算 $Q - amP$
    • 寻找与 上式子结果相同的 $bP$
    • 如果找到,那么$k=b + am$

那么所谓的baby其实就是b。而gaint 就是 am。参考某作者的一张图片,可以很清晰的阐明这个思路。
baby-step-giant-step.gif-154.8kB

之所以取 $m = \sqrt{n}$ 是因为这样可以取遍所有 n的情况

  • 对于 $a = 0 ; Q = (0m + b)P$ 这样就去遍历了所有 $Q = (1..m)P$ 的情况
  • 对于 $a = 1 ; Q = (m + b)P$ 这样就去遍历了所有 $Q = m + (1..m)P$ 的情况
  • …
  • 对于 $a = m-1 ; Q = (m-1 + b)P$ 这样就去遍历了所有 $Q = (m)m + (1..m)P$ 的情况

之后来看下其复杂度,如果认为查表的速度为O(1),那么该算法的时间复杂度和空间复杂度是$O(\sqrt{n})$
实际上这还是一个非常大的数字。

对于prime256v1而言,$\sqrt{n}$大概为$3.402823669209385 \times 10^{38}$。即使是哈希表的每个节点占1B,那也必然是Memory Error。

1.2 算法实践

之后可以简单实践以下这个算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def BSGS(E, p, q):
# B
step = 0
hash_table = {}
m = math.floor(math.sqrt(E.GF))
for i in range(m):
tmp = E.get_scalar_multiplication(i, p)
hash_table[tmp] = i
hash_list = hash_table.keys()
# G
for a in range(m):
amP = E.get_scalar_multiplication(-a * m, p)
Q_amP = E.get_three_pionts(amP, q)
if Q_amP in hash_list:
return hash_table[Q_amP] + a * m, step
else:
step += 1
return 0, 0

对于这样的测试样例,很快就可以算出来了。(我算了102步)

1
2
3
4
5
E = EllipticCurve(a=1, b=-1, p=10177, GF=10331)
p = (0x1, 0x1)
q = (0x1a28, 0x8fb)
k = 325
k0, step = BSGS(E, p, q)

0x02 Pollard rho 算法

刚才谈及,小步大步算法的空间复杂度太高,对于大参数曲线无法使用。因此得想些别的办法。pallard rho算法就是解决了这样的问题,时间复杂度与BSGS一样(但是理论上BSGS更快),空间复杂度只有$O(1)$

其核心思想是找到满足 $aP+bQ=AP+BQ$的参数$a,b,A,B$

对于 $Q = xP$ 有如下等式。

$$aP+bQ=AP+BQ\
aP+bQ=AP+BxP\
(a+bx)P=(A+Bx)P\
(a−A)P=(B−b)xP$$

如果要约去 $P$,需要加上取余符号。这步骤有点类似与同k的x计算方法。将x独立出来,可以得到

$$x \equiv (a-A) \times (B-b)^{-1}; mod; n $$

这样就可以算出$x$了

那么如何取确定$a,b,A,B$呢?之前说过,椭圆曲线的标量积满足分配律。这就意味着,如果定义一串伪随机数序列$(a_n,b_n)$,因为$P$和$Q$的标量积是有循环性质的,所以$aP+bQ$也是循环的。

当我们产生出这样一个循环序列后,从该序列中任取一组$(a,b)$随后更具循环序列的性质,找到满足$aP+bQ=AP+BQ$的$A$与$B$即可。

2.1 计算循环序列

非常糟糕的是,如果对所有a,b进行遍历,所需要的复杂度是$O(n^2)$显然这不能满足需求。而实际上有一种算法能够很好的解决这样的问题。该算法称为乌龟野兔算法,也称为弗洛伊德发现算法。

其算法大意就是定义两个点,分别对应(a,b)和(A,b),一个点(乌龟)每个时间步前进一格,也就是逐一读取伪随机序列中的点。另一个(兔子)每个时间步前进两格,也就是一个跳一个读取伪随机序列中的点。如果点的范围超出了循环子群的阶n。那么对其进行取余操作。

tortoise-hare.gif-48.5kB

如图所示,对于曲线$y^2≡x^3+2x+3;(mod;97)$ 首先它的循环子群阶为5。定义两个点 (a,b)。 和 (A,B) 绿色的代表乌龟,红色的代表兔子。很快就能找到两个相同的点。

如果随机序列是静态存储的话。那么空间复杂度就是 $O(1)$。如果随机序列是动态生成的话,那么空间复杂度大概是是$O(logn)$。计算渐进随机序列的时间复杂度非常困难,利用概率证明来证明时间复杂度的话,可以得到$O(\sqrt(n))$。

2.2 算法实践

修了修原文作者造的轮子,于是就能用了。他随机序列方法和图中有些不太一样,大概就是采用+;*;+的顺序。感觉挺有意思的,有兴趣的话可以去我的github底下看看。最终的结果为

1
2
3
4
5
6
    E = EllipticCurve(a=1, b=-1, p=10177, GF=10331)
p = (0x1, 0x1)
q = (0x1a28, 0x8fb)
k = 325
print(log(p, q, E))
(325, 221)

0x03 量子算法 Shor

很显然,哪怕把复杂度降低到$O(\sqrt n)$,仍然也解决不了数学难题,那未来的技术呢?确实存在一种能够在多项式时间内计算离散对数的量子算法:Shor算法,具有时间复杂度$O((logn)^3)$ 和空间复杂性 $O(logn)$。

量子计算机时至今日,仍然远未变得足够复杂来运行诸如Shor的算法,需要量子抗性算法的深入得研究

0x04 SageMath 工具

后来我从P4队的WP里看到了这个很牛逼的工具。里面的椭圆曲线求对数函数的速度非常快。我不太知道他用了什么方法,应该是改进的 rho 吧。

下载地址
http://www.sagemath.org/download.html

实际上这是一个用python2.7写的数学计算库。椭圆曲线的东西用起来非常舒服

大概的步骤如下所示,比我造的轮子好用多了。

1
2
3
4
5
6
sage: E =EllipticCurve(GF(10177),[1,-1])
sage: P = E([1,1])
sage: Q = E([0x1a28,0x8fb])
sage: P.discrete_log(Q)
325
# 这算的速度,绝对是秒级别的

0x05 ECC VS RSA

首先抛弃一下量子计算,为什么ECC比RSA优秀呢?NIST给了个表,告诉我们实现相同的安全级别,所需要的RSA与ECC的比特数

RSA key size (bits) ECC key size (bits)
1024 160
2048 224
3072 256
7680 384
15360 521

请注意,RSA密钥大小与ECC密钥大小之间没有线性关系(也就意味着:如果我们将RSA密钥大小加倍,并不需要将ECC密钥加倍)。该表不仅告诉我们ECC使用更少的内存,而且密钥生成和签名也要快得多。

但为什么会这样呢?答案是,用于计算椭圆曲线上离散对数的更快算法是Pollard rho和BSGS,而在RSA的情况下,有着特殊的对抗算法,比如通用数字域筛,它用于整数因子分解的算法(yafu就是用的这个)。一般数字域筛是迄今为止最快的整数分解算法。该算法正对所有适用于基于模运算的其他密码系统,包括DSA,DH和ElGamal。

0x06 CTF题目实战

XUSTCTF 2016

1
2
3
4
5
6
7
8
9
10
11
12
已知椭圆曲线加密Ep(a,b)参数为

p = 15424654874903
a = 16546484
b = 4548674875
G(6478678675,5636379357093)

私钥为
k = 546768
求公钥K(x,y)
提示:K=kG
提交格式XUSTCTF{x+y}(注意,大括号里面是x和y加起来求和,不是用加号连接)

算是一个非常常规的题目了。直接用自己写的轮子就可出了

1
2
3
4
from curve_base_class import EllipticCurve
E = EllipticCurve(p=15424654874903, a=16546484, b=4548674875)
K = E.get_scalar_multiplication(546768,(6478678675,5636379357093))
print("XUSTCTF{%s}"%(K[0]+K[1]))

HXP 2018 Curve12833227

WP:https://github.com/p4-team/ctf/blob/master/2018-12-08-hxp/crypto_curve12833227/vuln.py

关键代码如下,省略了mul和add两个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
from random import randrange
from Crypto.Cipher import AES

p = 2**128 - 33227

i = lambda x: pow(x, p-2, p)


x = randrange(p)
aes = AES.new(x.to_bytes(16, 'big'), AES.MODE_CBC, bytes(16))
flag = open('flag.txt').read().strip()
cipher = aes.encrypt(flag.ljust((len(flag)+15)//16*16).encode())
print(*mul(x, (4, 10)), cipher.hex(), file=open('flag.enc', 'w'))

Securinets CTF Quals 2018 Improve the quality

这个题目一开始给了个很长很长的描述。简单概括一下就是

  • 已知曲线 $ y^2 = x^3 + A*x + B$
  • 已知 A = 658974
  • 不知道 B 但是它最重要的数字是6
  • p = 962280654317
  • 生成元 P = (518459267012, 339109212996)
  • 私钥位 k。将k 切成若干份
  • Qi = ki * P
  • 以及一大堆的$Q_i$

首先先把B算出来

1
2
B = (P[0] ** 3 + A * P[0] - P[1] ** 2) % p
print(B)

这样算B会很大,加法逆元一下,得到B=618 满足条件。

这里需要用到之前介绍的sagamath工具。把Q中的点遍历一遍,就可以很快求出结果

1
2
3
4
5
6
7
8
9
10
K.<z> = GF(prime)
E = EllipticCurve(K,[A,B])
P = E([x,y])

solutions = []
for px,py in data:
Q = E([px,py])
solution = P.discrete_log(Q)
solutions.append(solution)
print(solutions)

之后把数字转chr即可获得一段提示,根据提示找到一张图片,解隐写可得FLAG。

1
2
3
for i in solutions:
for j in range(0, 10, 2):
print(chr(int(str(i)[j:j + 2])), end="")

ASIS Final 2016 RACES

这个加密算法,看上去没有什么大问题,这么多密钥参数,很容易让人想到是共模攻击。
突破点在于其素数生成函数

1
2
3
prime = getPrime(nbit)
if prime % 3 == 2:
return prime

这也就意味着,这里面所有的素数都是被3模余2的。我们知道素数只有$6k+5$和$6k+1$的形式(这是个伪命题),这也就意味着该脚本中的素数都是$6k+5$的形式。

在公钥文件里面,包含了非常多的(n, e),其n来自于利用该方法生成的两个素数之积。对于这种题目的一般做法是查看两个公钥是否有相同的参数,这个题目也不例外。

1
2
3
for pair in tqdm.tqdm(itertools.combinations(pairs, 2)):
if gmpy2.gcd(gmpy2.mpz(pair[0][0]), gmpy2.mpz(pair[1][0])) != 1:
print(pair)

这样,就能够找到p,q的值,并且迅速定位到c的值。

脚本中的加密过程非常简单,只不过是加了个ECC的壳子而已
∵$d = e^{-1} ;mod ;lcm$
∴$C = eM \Longrightarrow M = dC$

所以只需要求出e的逆元就可以了。

1
2
3
4
5
6
lcm = gmpy2.lcm((p + 1), (q + 1))
d = gmpy2.invert(e, lcm)

p0, p1 = multiply(c, d, 0, n)
print(hex(p1 - p0))
# ASIS{58cf105e8993ff852a7ea69c3f6464458a87c69f89ef3dfd749da4e2d3982de34832e38cab1baf8d1cd3ce0f73251629}

最终一波解密可以得到FLAG。

TUM CTF 2016 Heicss

我改编了一些东西
虽然归属了椭圆曲线的tag。但是只是涉及了一些基本的操作
首先那个order有点脑洞。我自己爆爆了半个小时也没有个所以然来,所以直接给正确的order了。感觉当初官方应该给了个hint,说实际的order和脚本的order差了在末尾的若干个空格,正确的order就是原本脚本里的order加四个空格

主要代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Give me the flag. This is an order!
def decode(bs):
if len(bs) < 0x40:
return None
s, m = int(bs[:0x40], 16), bs[0x40:]
if s >= q:
print('\x1b[31mbad signature\x1b[0m')
return None
S = s, sqrt(pow(s, 3, q) + a * s + b, q)
if S[1] is None:
print('\x1b[31mbad signature:\x1b[0m {:#x}'.format(S[0]))
return None
h = int(SHA256.new(m.encode()).hexdigest(), 16)
if mul(q, a, b, e, S)[0] == h:
return m
else:
print('\x1b[31mbad signature:\x1b[0m ({:#x}, {:#x})'.format(*S))

不难发现,输入的被拆为 前64和后64到最后的两个部分,第二部分通过了 SHA256 加密生成了 h。

首先来跟踪前40位的部分

  • 再输入长度超过64位,并且签名错误的情况下
  • 如果 s >= q 那么会打印bad signature
  • 如果 s < q 那么会打印bad signature 以及 前半部分还有后半部分经过一个函数变换后的值。

就好像盲注一样,s已知可以通过二分盲注来爆破出q的值
这个不会非常困难。由于我没有环境部署这个题目,就暂且跳过了。爆出来的q是这样的

1
q = 0x247ce416cf31bae96a1c548ef57b012a645b8bff68d3979e26aa54fc49a2c297

随后,记 s 为 S 的横坐标,随后会计算S的纵坐标(我姑且这么解释),纵坐标就是以下方程的一个解
$$y ^ 2 \equiv x^3 + ax +b ; mod ; q $$

当然,由于横坐标可以任意定。如果取0的话,那么y就变成了
$$ y^2 \equiv b; mod ; q $$
这个y是可以打印出来的

1
2
3
4
>> 00000000000000000000000000000000000000000000000000000000000000001
>> bad signature: (0x0, 0x18aae6ca595e2b030870f49d1aa143f4b46864eceab492f6f5a0f0efc9c90e51)

b = pow(0x18aae6ca595e2b030870f49d1aa143f4b46864eceab492f6f5a0f0efc9c90e51, 2, q)

这样的话,就可以计算出b的值

1
b = 8575167449093451733644615491327478728087226005203626331099704278682109092640

如果尝试输入1,那么会使得y无法算出来,应该是 GCD == 1 了。

那么,如果输入的横坐标是2的话,那么y就变成了
$$ y^2 \equiv (8 + 2a + b); mod ; q $$
这样的话,就可以计算出a的值

1
2
3
4
5
>> 00000000000000000000000000000000000000000000000000000000000000021
>> bad signature: (0x2, 0x20d599b9106e16f43d0c0a54e78517f5834bf15ef0206a5ce37080e4cad4f359)

a = (((pow(0x20d599b9106e16f43d0c0a54e78517f5834bf15ef0206a5ce37080e4cad4f359, 2, q) - b - 8) % q) / 2)
# a = 5079713781418039671549386476218981709382212150018593601284925328028384622133

目前得出了椭圆曲线的几个参数,分别是$ a,b,p,$还有生成元$S$。

  • S的横坐标是输入的x,纵坐标是将x带入曲线方程中得到的y

那么就要来观察一下签名部分了。
已知签名的内容来自于输入的字符串从第64位开始到最后,采用了 SHA256的加密得到 $h$

如果 $(e S )x = h$ 那么会返回m。我们需要的是让m为order,也就是最顶上的那个字符串。

很显然,只有S是可控的。并且h是已知的,为了方便,我们需要让两个坐标点相等,而不是傻乎乎得去把S的横坐标取出来和h比较。(这也是为啥我说官方应该给了hint,wp里说最后的msg应该加上四个空格,我看了好久都没看懂…)非常巧合的是可以利用模的逆元来算出S
$$ S = e^{-1}*h$$

如果要算e的逆元,那么就需要知道椭圆曲线的阶是多少。令我非常费解的是,椭圆曲线循环子群的阶是如何算出来的。这个数字太大了导致我用schoof算法直接内存爆了。
这边非常无奈的抄一下答案

1
feild_order = 16503925798136106726026894143294039201930439456987742756395524593191976084900

于是,一串小脚本就可以算出 S 了。

1
2
3
4
5
6
7
e = 65537
hx = int(SHA256.new(msg.encode()).hexdigest(), 16)
hy = sqrt(pow(hx, 3, q) + a * hx + b, q)
e_inv = gmpy2.invert(e, field_order)
S = mul(q, a, b, e_inv, (hx, hy))
check = mul(q, a, b, e, S)
assert check[0] == hx

取出横坐标,非常巧合得发现其横坐标刚好64位也不用补0啥得。

1
2
"10feab68fea4ecbc95e2f7c67ebcf83e75fc0e0357006ca2429559f4aa83fce8".__len__()
Out[11]: 64

把它和最前面那段加起来,即可得到结果

1
2
10feab68fea4ecbc95e2f7c67ebcf83e75fc0e0357006ca2429559f4aa83fce8Give me the flag. This is an order!    
happy shaobaobaoer

ECC 椭圆曲线加密算法学习————ECDH与ECDSA

Posted on 2020-05-22

ECC 椭圆曲线加密算法学习————ECDH与ECDSA

标签(空格分隔): ecc Crypto


****
> File Name: ECC 椭圆曲线加密算法学习————ECDH与ECDSA
> Author: shaobaobaoer
> Mail: shaobaobaoer@126.com
> WebSite: shaobaobaoer.cn
> Time: Sunday, 25. March 2019 22:42PM
****

<!–查看注释有惊喜–>

0x00 前言

之前学习了实数域上的椭圆曲线与有限域$\mathbb {F}_{p}$上的椭圆曲线。详细可以参考ECC椭圆加密算法学习————从实数域到有限域的椭圆曲线。

不难发现,在实数域的标量乘法看上去是一个“简单”的问题,但是在有限域$\mathbb {F}_{p}$就显得非常困难。本文主要讨论如何将之前所学的运用于加密问题中。

相关代码

  • 本文的完整代码
    • https://github.com/ninthDevilHAUNSTER/ecc_learning

一些重要的域参数

  • 素数 $p$
  • 椭圆曲线系数 $a$ 与 $b$
  • 基点(生成元) $G$
  • 子群的阶$n$
  • 辅因子$h$

之后,将这六个域参数组合成一个一个六元组$(p,a,b,G,n,h)$

在后文中会经常用到

本文代码

0x01 椭圆曲线加密算法

尽管之前花了很长时间的铺垫,但是椭圆曲线的密码学策略,简单而纯粹

在选取完毕一个六元组后,定义如下内容

  • 私钥: 一个随机的整数$d$,选取自${1,…,n-1}$
  • 公钥:$H = dG$ ($G$是循环子群的生成元)

至此,来考虑一个问题
如果我们知道 $d$与六元组,那么很简单就可以算出$H$了。但是,如果我们只知道$H$和$G$,去寻找一个$d$是非常困难的,因为这需要解决离散对数问题。

通过这一组密钥,可以衍生出两种经典的椭圆曲线加密算法,分别是 ECDH,可以用来加密。以及 ECDSA ,可以用来数字签名

0x02 ECDH 加密算法

ECDH是Diffie—Hellman算法的衍生。关于DH算法这里不展开,如果能学到这里想必对DSA与DH都有一定程度的了解。

2.1 ECDH 的工作过程

ECDH的工作原理如下所示,实际上和纯粹的DH算法有着非常相似的地方

  • Alice 和 Bob 首先约定一个六元组,生成自己的私钥和公钥。定义数据结构如下
    • Alice Private Key : $d_A$
    • Alice Public Key : $H_A = d_AG$
    • Bob Private Key : $d_B$
    • Bob Public Key : $H_B = d_BG$
  • Alice 和 Bob 公开得交换公钥。第三方可以窃听到 公钥$H_A$和$H_B$,但是他无法解出$d_A$和$d_B$
  • Alice 用自己的私钥计算 $S_A = d_A H_B$;Bob 用自己的私钥计算 $S_B = d_B H_A$

很显然 $S_A = S_B$ 因为以下算式

$$S = d_AH_B = d_A(d_B G) = d_B(d_A G)$$

对此,可以将上述过程总结为一个数学问题。

给定三个点 P,aP,bP,那么abP的结果是什么?

总之,ECDH的问题也被归结为非常困难的那一类。尽管无法证明有多困难,但是这个难度应该和在上一篇文章最后提及的椭圆曲线难题的难度相当。

  • 此时,Alice和Bob已经获得了共享密钥,可以通过对称加密来交换数据

比如说,他们可以使用S的x坐标密钥,使用AES或者3DES加密消息。
实际上在TLS的过程中中,TLS将x坐标与其他相关的数字关联起来,从而计算得到字节型字符串的散列。

2.2 ECDH 加密解密实践

之前为了解释算法的过程,所以采用了非常简单的椭圆曲线。实际上,运用在实际中的曲线非常复杂。有一个组织叫高效密码学标准协会SECG。制定了一系列的高强度的密码学标准。可以在相关文档中查到高强度的密码学参数。

在此,选取了Secp256k1作为之后演示的例子。同时secp256k1也是比特币公钥加密中的椭圆曲线参数。
其六元组的构成如下所示:

1
2
3
4
5
6
7
p  = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
a = 0
b = 7
xG = 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
yG = 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
n = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
h = 1

通过一个简单的脚本,可以还原上述过程,如果一切顺利,S_A 与 S_B 无论如何都是相等的

1
2
3
4
5
6
7
Curve: secp256k1
Alice's private key: 103578888203905683147310628925141324706004210586941582062255108435073330635559
Alice's public key: (72205313168121137722470139642970359323314163425341806818266474706052417395420, 42908651374588414209406038144703625728727324613824233811609705542553632995837)
Bob's private key: 97727733532815547475289836254065376178409286030674739848985976984975775000167
Bob's public key: (5226211608039988834249916327528388136314009566006478722042576040985411258297, 76867928087387169315809382707245048142671884194759753857442364958995784506075)
Shared secret calculate by Alice : (90402392865121638378190357760493775057772079441731676814490847255017097141119, 29964306249717728516272468678674342055122755840848542530168745390980111134510)
Shared secret calculate by Bob : (90402392865121638378190357760493775057772079441731676814490847255017097141119, 29964306249717728516272468678674342055122755840848542530168745390980111134510)

2.3 临时(Ephermeral) ECDH

ECDHE 也是一种常见的ECDH算法。其中的E表示短暂的。指交换的密钥是临时的,而不是静态的。在TLS中使用ECDHE。那么客户端和服务端在建立连接时即时生成其公钥私钥对,然后使用TLS证书对密钥进行签名,然后在各方之间交换

0x03 ECDSA 加密算法

ECDSA 的使用场景如下:
Alice想要用她的私钥$d_A$对一个消息进行签名。Bob希望通过Alice的公钥$H_A$来验证对消息的签名。但是只有Alice能够签名,其他人只能验证她的签名。

同样Alice和Bob使用相同的六元组域参数。所谓ECDSA是应用于椭圆曲线的DSA数字签名算法的变体。
关于DSA的具体细节,可以看看我之前写的文章(PS: 代码有些问题,当初连模逆运算都不是很懂)

ECDSA处理的是消息的哈希值,而不是消息本身,散列函数选择取决于双方,但是应当选用安全的散列函数。在此,我们定义消息的散列值为$z$

3.1 ECDSA 的签名过程

ECDSA 的流程如下所示:

预定完毕一个六元组后,Alice首先定义公钥和私钥

  • 私钥: 一个随机的整数$d_A$,选取自${1,…,n-1}$
  • 公钥: $H_A = d_AG$ ($G$是循环子群的生成元)

随后,Alice 定义如下内容

  • 选取一个随机的整数$k$,选取自${1,…,n-1}$
  • 计算 $P = kG$ ($G$是循环子群的生成元)
  • 计算 $r \equiv x_p ;(mod ;n );$ ($x_p$ 是P的横坐标)
  • 如果 $r = 0 $ 则重新选取k
  • 计算 $s \equiv k^{-1} ( z + rd_A); (mod;n)$ ($d_A$是Alice的私钥,$k^{-1}$是$k$的逆元)
  • 如果 $s = 0 $ 则重新选取k
  • 将 $(r,s)$ 封装为一个签名

简单来说,该算法生成了一个密钥k。该密钥通过标量积隐藏在签名的$r$中。之前也说过,通过$P$和$G$计算$k$,是有限域椭圆曲线的数学难题。
随后$r$通过算式 $s \equiv k^{-1} ( z + rd_A); (mod;n)$与消息散列$z$建立起联系。并将$r$和$s$封装为签名。

为了计算$s$,需要知道$k$的对于$n$的逆元。在之前说到过,如果$n$是个合数,那么在计算模逆的时候会出些一些非常棘手的状况。所以如果私钥不是素数,那么ECDSA就不能够被使用。当然,在制定高强度密码学标准的时候,自然是考虑到了这种情况

3.2 ESDSA 的验证过程

对于Bob而言,他知道$z$和$(r,s)$,可以这样来验证签名

  • 计算 $u_1 \equiv s^{-1} z ;(mod ;n)$
  • 计算 $u_2 \equiv s^{-1} r ;(mod ;n)$
  • 计算 $P_0 = u_1G + u_2H_A$
  • 当且仅当 $ r=xP;mod;n $的时候 $P_0 = H_A$

3.3 ESDSA 的正确性验证

首先从最后一步开始:(严谨而言下面每个式子都需要模n)
$$
\begin{eqnarray}
P =& u_1G + u_2H_A \
=& u_1G + u_2d_AG\
=& (;u_1 + u_2d_A; ); G
\end{eqnarray}
$$

之后回带 $u_1$,$u_2$
$$
\begin{eqnarray}
P =& (;u_1 + u_2d_A; ); G \
=& (;s^{-1} z + s^{-1} rd_A;) ;G\
=& s^{-1}(;z + rd_A;); G
\end{eqnarray}
$$

∵ $s = k^{-1}(;z + rd_A;); mod ; n $
∴ $ k = s^{-1}(;z + rd_A;); mod ; n$
因此的得证 $ P = kG$

综上所述,可以将ECDSA的过程用下图来概括

image_1d6nq8u9u1vd1lro1aqn10bh1ugug.png-19.8kB

3.4 ESDSA 签名与验证演示

我写了一个ESDSA签名与验证的脚本,其输出内容如下所示。详细代码可以再github里查看

1
2
3
4
5
6
7
Curve: secp256k1
Private key: 104402155858234843462820049838931863424340700345639408070129091750274190501639
Public key: (26871662523956236501334015334554552355718345422423650192015494443518483271191, 78585437455917839772708635641693122787004414694759872862060571807949572029114)

Message: 79600447942433
Signature: (90544279873496095960562757005038315753760612769739282803554290388042828042546, mpz(81447758399901905504654585382621756821500911503165874513424939904998314198491))
Verification: signature matches

3.5 ESDSA 简单攻击演示

生成ECDSA签名时,保守秘密非常重要 ķ真的很秘密。如果我们使用相同的ķ来签名,或者如果随机数生成器可预测,攻击者就能找到私钥

如果使用相同的k进行签名,将会发生一些有趣的事情。也是索尼在11年前犯的错误

在计算开始之前,首先我们应当知道域参数的六元组(或者曲线名称),两组签名 $(r_1,s_1),(r_2,s_2)$与哈希值$z1$,$z2$。以及公钥 $S_A$

对于相同的k生成的两组签名 $(r_1,s_1),(r_2,s_2)$而言。

  • 最先确定的是$r1=r2$
  • 两式相减可得 $$s_1 - s_2 \equiv k^{-1} \times (z_1 -z_2) ; mod ;n$$
  • 两边同乘k后将k的系数除过去 可得 $$k \equiv (s_1 - s_2)^{-1} \times (z_1 -z_2) ; mod ;n$$
  • 此时,就可以计算出$k$,再利用 $k$ 可以计算出私钥$d_A$
    $$s \equiv k^{-1} (z _ rd_A) ; mod ; n \Longrightarrow d_A \equiv r{-1} (sk -z ) ; mod ; n$$

test

Posted on 2020-05-22

Hello World

Posted on 2020-05-22

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

烧包

大梦将荒,有客惶惶,举酒盈樽,侃谈千魂

5 posts
© 2020 烧包
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4