CTF-Crypto要点


密码学简介

密码学(Cryptography)一般可分为古典密码学和现代密码学。

其中,古典密码学,作为一种实用性艺术存在,其编码和破译通常依赖于设计者和敌手的创造力与技巧,并没有对密码学原件进行清晰的定义。古典密码学主要包含以下几个方面:

  • 单表替换加密(Monoalphabetic Cipher)
  • 多表替换加密(Polyalphabetic Cipher)
  • 奇奇怪怪的加密方式

而现代密码学则起源于 20 世纪中后期出现的大量相关理论,1949 年香农(C. E. Shannon)发表了题为《保密系统的通信理论》的经典论文标志着现代密码学的开始。现代密码学主要包含以下几个方面:

  • 对称加密(Symmetric Cryptography),以 DES,AES,RC4 为代表。
  • 非对称加密(Asymmetric Cryptography),以 RSA,ElGamal,椭圆曲线加密为代表。
  • 哈希函数(Hash Function),以 MD5,SHA-1,SHA-512 等为代表。
  • 数字签名(Digital Signature),以 RSA 签名,ElGamal 签名,DSA 签名为代表。

其中,对称加密体制主要分为两种方式:

  • 分组密码(Block Cipher),又称为块密码。
  • 序列密码(Stream Cipher),又称为流密码。

一般来说,密码设计者的根本目标是保障信息及信息系统的

  • 机密性(Confidentiality)
  • 完整性(Integrity)
  • 可用性(Availability)
  • 认证性(Authentication)
  • 不可否认性(Non-repudiation)

其中,前三者被称为信息安全的 CIA 三要素 。

而对于密码破解者来说,一般是要想办法识别出密码算法,然后进行暴力破解,或者利用密码体制的漏洞进行破解。当然,也有可能通过构造虚假的哈希值或者数字签名来绕过相应的检测。

一般来说,我们都会假设攻击者已知待破解的密码体制,而攻击类型通常分为以下四种:

攻击类型 说明
唯密文攻击 只拥有密文
已知明文攻击 拥有密文与对应的明文
选择明文攻击 拥有加密权限,能够对明文加密后获得相应密文
选择密文攻击 拥有解密权限,能够对密文解密后获得相应明文

Note

注:之前在这里曾写过这些攻击常见的场景,随着不断地学习,渐渐意识到这些攻击类型侧重描述攻击者的能力,有可能适用于各种各样的场景。故进行修正。

这里推荐一些资料

古典密码简介

在古典密码学中,我们主要介绍单表替代密码,多表替代密码,以及一些其它比较有意思的密码。

值得一提的是,在古典密码学中,设计者主要考虑消息的保密性,使得只有相关密钥的人才可以解密密文获得消息的内容,对于消息的完整性和不可否认性则并没有进行太多的考虑。

单表代换加密

通用特点

在单表替换加密中,所有的加密方式几乎都有一个共性,那就是明密文一一对应。所以说,一般有以下两种方式来进行破解

  • 在密钥空间较小的情况下,采用暴力破解方式
  • 在密文长度足够长的时候,使用词频分析,http://quipqiup.com/

当密钥空间足够大,而密文长度足够短的情况下,破解较为困难。

凯撒密码

原理

凯撒密码(Caesar)加密时会将明文中的 每个字母 都按照其在字母表中的顺序向后(或向前)移动固定数目(循环移动)作为密文。例如,当偏移量是左移 3 的时候(解密时的密钥就是 3):

明文字母表:ABCDEFGHIJKLMNOPQRSTUVWXYZ
密文字母表:DEFGHIJKLMNOPQRSTUVWXYZABC

使用时,加密者查找明文字母表中需要加密的消息中的每一个字母所在位置,并且写下密文字母表中对应的字母。需要解密的人则根据事先已知的密钥反过来操作,得到原来的明文。例如:

明文:THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG
密文:WKH TXLFN EURZQ IRA MXPSV RYHU WKH ODCB GRJ

根据偏移量的不同,还存在若干特定的恺撒密码名称

  • 偏移量为 10:Avocat (A→K)
  • 偏移量为 13:ROT13
  • 偏移量为 -5:Cassis (K 6)
  • 偏移量为 -6:Cassette (K 7)

此外,还有还有一种基于密钥的凯撒密码 Keyed Caesar。其基本原理是 利用一个密钥,将密钥的每一位转换为数字(一般转化为字母表对应顺序的数字),分别以这一数字为密钥加密明文的每一位字母。

这里以 XMan 一期夏令营分享赛宫保鸡丁队 Crypto 100 为例进行介绍。

密文:s0a6u3u1s0bv1a
密钥:guangtou
偏移:6,20,0,13,6,19,14,20
明文:y0u6u3h1y0uj1u

破解

  1. 遍历 26 个偏移量,适用于普遍情况
  2. 利用词频分析,适用于密文较长的情况。

其中,第一种方式肯定可以得到明文,而第二种方式则不一定可以得到正确的明文。

而对于基于密钥的凯撒密码来说,一般来说必须知道对应的密钥。

工具

一般我们有如下的工具,其中 JPK 比较通用。

移位密码

与凯撒密码类似,区别在于移位密码不仅会处理字母,还会处理数字和特殊字符,常用 ASCII 码表进行移位。其破解方法也是遍历所有的可能性来得到可能的结果。

Atbash Cipher

原理

埃特巴什码(Atbash Cipher)其实可以视为下面要介绍的简单替换密码的特例,它使用字母表中的最后一个字母代表第一个字母,倒数第二个字母代表第二个字母。在罗马字母表中,它是这样出现的:

明文:A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
密文:Z Y X W V U T S R Q P O N M L K J I H G F E D C B A

下面给出一个例子:

明文:the quick brown fox jumps over the lazy dog
密文:gsv jfrxp yildm ulc qfnkh levi gsv ozab wlt

破解

可以看出其密钥空间足够短,同时当密文足够长时,仍然可以采用词频分析的方法解决。

工具

简单替换密码

原理

简单替换密码(Simple Substitution Cipher)加密时,将每个明文字母替换为与之唯一对应且不同的字母。它与恺撒密码之间的区别是其密码字母表的字母不是简单的移位,而是完全是混乱的,这也使得其破解难度要高于凯撒密码。 比如:

明文字母 : abcdefghijklmnopqrstuvwxyz
密钥字母 : phqgiumeaylnofdxjkrcvstzwb

a 对应 p,b 对应 h,以此类推。

明文:the quick brown fox jumps over the lazy dog
密文:cei jvaql hkdtf udz yvoxr dsik cei npbw gdm

而解密时,我们一般是知道了每一个字母的对应规则,才可以正常解密。

破解

由于这种加密方式导致其所有的密钥个数是26!26! ,所以几乎上不可能使用暴力的解决方式。所以我们 一般采用词频分析。

工具

仿射密码

原理

  • 仿射密码的加密函数是 E(x)=(ax+b)(mod m),其中
    • x 表示明文按照某种编码得到的数字
    • a 和 m 互质
    • m 是编码系统中字母的数目

解密函数是 D(x)=a^−1(x−b)(mod m),其中 a^−1 是 a 在 Zm群的乘法逆元。

下面我们以 E(x)=(5x+8) mod 26函数为例子进行介绍,加密字符串为 AFFINE CIPHER,这里我们直接采用字母表 26 个字母作为编码系统

明文 A F F I N E C I P H E R
x 0 5 5 8 13 4 2 8 15 7 4 17
y=5x+8 8 33 33 48 73 28 18 48 83 43 28 93
y mod 26 8 7 7 22 21 2 18 22 5 17 2 15
密文 I H H W V C S W F R C P

其对应的加密结果是 IHHWVCSWFRCP

对于解密过程,正常解密者具有 a 与 b,可以计算得到 a−1 为 21,所以其解密函数是D(x)=21(x−8)(mod 26) ,解密如下

密文 I H H W V C S W F R C P
y 8 7 7 22 21 2 18 22 5 17 2 15
x=21(y−8) 0 -21 -21 294 273 -126 210 294 -63 189 -126 147
xmod26 0 5 5 8 13 4 2 8 15 7 4 17
明文 A F F I N E C I P H E R

可以看出其特点在于只有 26 个英文字母。

破解

首先,我们可以看到的是,仿射密码对于任意两个不同的字母,其最后得到的密文必然不一样,所以其也具有最通用的特点。当密文长度足够长时,我们可以使用频率分析的方法来解决。

其次,我们可以考虑如何攻击该密码。可以看出当a=1时,仿射加密是凯撒加密。而一般来说,我们利用仿射密码时,其字符集都用的是字母表,一般只有 26 个字母,而不大于 26 的与 26 互素的个数一共有

ϕ(26)=ϕ(2)×ϕ(13)=12

算上 b 的偏移可能,一共有可能的密钥空间大小也就是

12×26=312

一般来说,对于该种密码,我们至少得是在已知部分明文的情况下才可以攻击。下面进行简单的分析。

这种密码由两种参数来控制,如果我们知道其中任意一个参数,那我们便可以很容易地快速枚举另外一个参数得到答案。

但是,假设我们已经知道采用的字母集,这里假设为 26 个字母,我们还有另外一种解密方式,我们只需要知道两个加密后的字母 y1,y2 即可进行解密。那么我们还可以知道

y1=(ax1+b)(mod 26)

y2=(ax2+b)(mod 26)

两式相减,可得

y1−y2=a(x1−x2)(mod 26)

这里 y1,y2已知,如果我们知道密文对应的两个不一样的字符 x1与 x2 ,那么我们就可以很容易得到 a ,进而就可以得到 b 了。

例子

这里我们以 TWCTF 2016 的 super_express 为例进行介绍。简单看一下给的源码

import sys
key = '****CENSORED***************'
flag = 'TWCTF{*******CENSORED********}'

if len(key) % 2 == 1:
    print("Key Length Error")
    sys.exit(1)

n = len(key) / 2
encrypted = ''
for c in flag:
    c = ord(c)
    for a, b in zip(key[0:n], key[n:2*n]):
        c = (ord(a) * c + ord(b)) % 251
    encrypted += '%02x' % c

print encrypted

可以发现,虽然对于 flag 中的每个字母都加密了 n 次,如果我们仔细分析的话,我们可以发现

c1=a1c+b1

c2=a2c1+b2

=a1a2c+a2b1+b2

=kc+d

根据第二行的推导,我们可以得到其实 cn也是这样的形式,可以看成 cn=xc+y ,并且,我们可以知道的是,key 是始终不变化的,所以说,其实这个就是仿射密码。

此外,题目中还给出了密文以及部分部分密文对应的明文,那么我们就很容易利用已知明文攻击的方法来攻击了,利用代码如下

import gmpy

key = '****CENSORED****************'
flag = 'TWCTF{*******CENSORED********}'

f = open('encrypted', 'r')
data = f.read().strip('\n')
encrypted = [int(data[i:i + 2], 16) for i in range(0, len(data), 2)]
plaindelta = ord(flag[1]) - ord(flag[0])
cipherdalte = encrypted[1] - encrypted[0]
a = gmpy.invert(plaindelta, 251) * cipherdalte % 251
b = (encrypted[0] - a * ord(flag[0])) % 251
a_inv = gmpy.invert(a, 251)
result = ""
for c in encrypted:
    result += chr((c - b) * a_inv % 251)
print result

结果如下

➜  TWCTF2016-super_express git:(master) ✗ python exploit.py
TWCTF{Faster_Than_Shinkansen!}

多表代换加密

对于多表替换加密来说,加密后的字母几乎不再保持原来的频率,所以我们一般只能通过寻找算法实现对应的弱点进行破解。

Playfair

原理

Playfair 密码(Playfair cipher or Playfair square)是一种替换密码,1854 年由英国人查尔斯 · 惠斯通(Charles Wheatstone)发明,基本算法如下:

  1. 选取一串英文字母,除去重复出现的字母,将剩下的字母逐个逐个加入 5 × 5 的矩阵内,剩下的空间由未加入的英文字母依 a-z 的顺序加入。注意,将 q 去除,或将 i 和 j 视作同一字。
  2. 将要加密的明文分成两个一组。若组内的字母相同,将 X(或 Q)加到该组的第一个字母后,重新分组。若剩下一个字,也加入 X 。
  3. 在每组中,找出两个字母在矩阵中的地方。
    • 若两个字母不同行也不同列,在矩阵中找出另外两个字母(第一个字母对应行优先),使这四个字母成为一个长方形的四个角。
    • 若两个字母同行,取这两个字母右方的字母(若字母在最右方则取最左方的字母)。
    • 若两个字母同列,取这两个字母下方的字母(若字母在最下方则取最上方的字母)。

新找到的两个字母就是原本的两个字母加密的结果。

以 playfair example 为密匙,得

P L A Y F
I R E X M
B C D G H
K N O Q S
T U V W Z

要加密的讯息为 Hide the gold in the tree stump

HI DE TH EG OL DI NT HE TR EX ES TU MP

就会得到

BM OD ZB XD NA BE KU DM UI XM MO UV IF

工具

  • CAP4

Polybius

原理

Polybius 密码又称为棋盘密码,其一般是将给定的明文加密为两两组合的数字,其常用密码表

1 2 3 4 5
1 A B C D E
2 F G H I/J K
3 L M N O P
4 Q R S T U
5 V W X Y Z

举个例子,明文 HELLO,加密后就是 23 15 31 31 34。

另一种密码表

A D F G X
A b t a l p
D d h o z k
F q f v s n
G g j c u x
X m r e w y

注意,这里字母的顺序被打乱了。

A D F G X 的由来:

1918 年,第一次世界大战将要结束时,法军截获了一份德军电报,电文中的所有单词都由 A、D、F、G、X 五个字母拼成,因此被称为 ADFGX 密码。ADFGX 密码是 1918 年 3 月由德军上校 Fritz Nebel 发明的,是结合了 Polybius 密码和置换密码的双重加密方案。

举个例子,HELLO,使用这个表格加密,就是 DD XF AG AG DF。

工具

  • CrypTool

例子

这里以安恒杯 9 月 Crypto 赛题 Go 为例,题目为:

密文:ilnllliiikkninlekile

压缩包给了一行十六进制:546865206c656e677468206f66207468697320706c61696e746578743a203130

请对密文解密

首先对十六进制进行 hex 解码,得到字符串:”The length of this plaintext: 10”

密文长度为 20 ,而明文长度为 10 ,密文只有 “l”,”i”,”n”,”k”,”e” 这五个字符,联想到棋盘密码。

首先试一下五个字符按字母表顺序排列:

e i k l n
e A B C D E
i F G H I/J K
k L M N O P
l Q R S T U
n V W X Y Z

根据密文解密得:iytghpkqmq。

这应该不是我们想要的 flag 答案。

看来这五个字符排列不是这么排列的,一共有 5! 种情况,写脚本爆破:

import itertools

key = []
cipher = "ilnllliiikkninlekile"

for i in itertools.permutations('ilnke', 5):
    key.append(''.join(i))

for now_key in key:
    solve_c = ""
    res = ""
    for now_c in cipher:
        solve_c += str(now_key.index(now_c))
    for i in range(0,len(solve_c),2):
        now_ascii = int(solve_c[i])*5+int(solve_c[i+1])+97
        if now_ascii>ord('i'):
            now_ascii+=1
        res += chr(now_ascii)
    if "flag" in res:
        print now_key,res

脚本其实就是实现棋盘密码这个算法,只是这五个字符的顺序不定。

跑出下面两个结果:

linke flagishere

linek flagkxhdwd

显然第一个是我们想要的答案。

附上正确的密码表:

l i n k e
l A B C D E
i F G H I/J K
n L M N O P
k Q R S T U
e V W X Y Z

Vigenere 维吉尼亚密码

原理

维吉尼亚密码(Vigenere)是使用一系列凯撒密码组成密码字母表的加密算法,属于多表密码的一种简单形式。

下面给出一个例子

明文:come greatwall
密钥:crypto

首先,对密钥进行填充使其长度与明文长度一样。

明文 c o m e g r e a t w a l l
密钥 c r y p t o c r y p t o c

其次,查表得密文

明文:come greatwall
密钥:crypto
密文:efkt zferrltzn

破解

对包括维吉尼亚密码在内的所有多表密码的破译都是以字母频率为基础的,但直接的频率分析却并不适用,这是因为在维吉尼亚密码中,一个字母可以被加密成不同的密文,因而简单的频率分析在这里并没有用。

破译维吉尼亚密码的关键在于它的密钥是循环重复的。 如果我们知道了密钥的长度,那密文就可以被看作是交织在一起的凯撒密码,而其中每一个都可以单独破解。关于密码的长度,我们可以 使用卡西斯基试验和弗里德曼试验来获取。

卡西斯基试验是基于类似 the 这样的常用单词有可能被同样的密钥字母进行加密,从而在密文中重复出现。例如,明文中不同的 CRYPTO 可能被密钥 ABCDEF 加密成不同的密文:

密钥:ABCDEF AB CDEFA BCD EFABCDEFABCD
明文:CRYPTO IS SHORT FOR CRYPTOGRAPHY
密文:CSASXT IT UKSWT GQU GWYQVRKWAQJB

此时明文中重复的元素在密文中并不重复。然而,如果密钥相同的话,结果可能便为(使用密钥 ABCD):

密钥:ABCDAB CD ABCDA BCD ABCDABCDABCD
明文:CRYPTO IS SHORT FOR CRYPTOGRAPHY
密文:CSASTP KV SIQUT GQU CSASTPIUAQJB

此时卡西斯基试验就能产生效果。对于更长的段落此方法更为有效,因为通常密文中重复的片段会更多。如通过下面的密文就能破译出密钥的长度:

密文:DYDUXRMHTVDVNQDQNWDYDUXRMHARTJGWNQD

其中,两个 DYDUXRMH 的出现相隔了 18 个字母。因此,可以假定密钥的长度是 18 的约数,即长度为 18、9、6、3 或 2。而两个 NQD 则相距 20 个字母,意味着密钥长度应为 20、10、5、4 或 2。取两者的交集,则可以基本确定密钥长度为 2。接下来就是进行进一步的操作了。

关于更加详细的破解原理,这里暂时不做过多的介绍。可以参考 http://www.practicalcryptography.com/cryptanalysis/stochastic-searching/cryptanalysis-vigenere-cipher/

工具

Nihilist

原理

Nihilist 密码又称关键字密码:明文 + 关键字 = 密文。以关键字 helloworld 为例。

首先利用密钥构造棋盘矩阵(类似 Polybius 密码) - 新建一个 5 × 5 矩阵 - 将字符不重复地依次填入矩阵 - 剩下部分按字母顺序填入 - 字母 i 和 j 等价

1 2 3 4 5
1 h e l o w
2 r d a b c
3 f g i / j k m
4 n p q s t
5 u v x y z

对于加密过程参照矩阵 M 进行加密:

a -> M[2,3] -> 23
t -> M[4,5] -> 45

对于解密过程

参照矩阵 M 进行解密:

23 -> M[2,3] -> a
45 -> M[4,5] -> t

可以看出,密文的特征有如下几点

  • 纯数字
  • 只包含 1 到 5
  • 密文长度偶数。

Hill

原理

希尔密码(Hill)使用每个字母在字母表中的顺序作为其对应的数字,即 A=0,B=1,C=2 等,然后将明文转化为 n 维向量,跟一个 n × n 的矩阵相乘,再将得出的结果模 26。注意用作加密的矩阵(即密匙)在 Zn26必须是可逆的,否则就不可能解码。只有矩阵的行列式和 26 互质,才是可逆的。下面举一个例子

明文:ACT

密文即为

密文:POH

工具

例子

这里我们以 ISCC 2015 base decrypt 150 为例进行介绍,题目为

密文: 22,09,00,12,03,01,10,03,04,08,01,17 (wjamdbkdeibr)

使用的矩阵是 1 2 3 4 5 6 7 8 10

请对密文解密.

首先,矩阵是 3 × 3 的。说明每次加密 3 个字符。我们直接使用 Cryptool,需要注意的是,这个矩阵是按照列来排布的。即如下

1 4 7
2 5 8
3 6 10

最后的结果为 overthehillx

AutokeyCipher

原理

自动密钥密码(Autokey Cipher)也是多表替换密码,与维吉尼亚密码密码类似,但使用不同的方法生成密钥。通常来说它要比维吉尼亚密码更安全。自动密钥密码主要有两种,关键词自动密钥密码和原文自动密钥密码。下面我们以关键词自动密钥为例:

明文:THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG
关键词:CULTURE

自动生成密钥:

CULTURE THE QUICK BROWN FOX JUMPS OVER THE

接下来的加密过程和维吉尼亚密码类似,从相应的表格可得:

密文

VBP JOZGD IVEQV HYY AIICX CSNL FWW ZVDP WVK

工具

其它类型加密

培根密码

原理

培根密码使用两种不同的字体,代表 A 和 B,结合加密表进行加解密。

a AAAAA g AABBA n ABBAA t BAABA
b AAAAB h AABBB o ABBAB u-v BAABB
c AAABA i-j ABAAA p ABBBA w BABAA
d AAABB k ABAAB q ABBBB x BABAB
e AABAA l ABABA r BAAAA y BABBA
f AABAB m ABABB s BAAAB z BABBB

上面的是常用的加密表。还有另外的一种加密表,可认为是将 26 个字母从 0 到 25 排序,以二进制表示,A 代表 0,B 代表 1。

下面这一段内容就是明文 steganography 加密后的内容,正常字体是 A,粗体是 B:

To encode a message each letter of the plaintext is replaced by a group of five of the letters ‘A’ or ‘B’.

可以看到,培根密码主要有以下特点

  • 只有两种字符
  • 每一段的长度为 5
  • 加密内容会有特殊的字体之分,亦或者大小写之分。

工具

栅栏密码

原理

栅栏密码把要加密的明文分成 N 个一组,然后把每组的第 1 个字连起来,形成一段无规律的话。这里给出一个例子

明文:THERE IS A CIPHER

去掉空格后变为

THEREISACIPHER

分成两栏,两个一组得到

TH ER EI SA CI PH ER

先取出第一个字母,再取出第二个字母

TEESCPE
HRIAIHR

连在一起就是

TEESCPEHRIAIHR

上述明文也可以分为 2 栏。

THEREIS ACIPHER

组合得到密文

TAHCEIRPEHIESR

工具

曲路密码

原理

曲路密码(Curve Cipher)是一种换位密码,需要事先双方约定密钥(也就是曲路路径)。下面给出一个例子

明文:The quick brown fox jumps over the lazy dog

填入 5 行 7 列表(事先约定填充的行列数)

加密的回路线(事先约定填充的行列数)

密文:gesfc inpho dtmwu qoury zejre hbxva lookT

列移位加密

原理

列移位密码(Columnar Transposition Cipher)是一种比较简单,易于实现的换位密码,通过一个简单的规则将明文打乱混合成密文。下面给出一个例子。

我们以明文 The quick brown fox jumps over the lazy dog,密钥 how are u 为例:

将明文填入 5 行 7 列表(事先约定填充的行列数,如果明文不能填充完表格可以约定使用某个字母进行填充)

密钥: how are u,按 how are u 在字母表中的出现的先后顺序进行编号,我们就有 a 为 1,e 为 2,h 为 3,o 为 4,r 为 5,u 为 6,w 为 7,所以先写出 a 列,其次 e 列,以此类推写出的结果便是密文:

密文: qoury inpho Tkool hbxva uwmtd cfseg erjez

工具

01248 密码

原理

该密码又称为云影密码,使用 0,1,2,4,8 四个数字,其中 0 用来表示间隔,其他数字以加法可以表示出 如:28=10,124=7,18=9,再用 1->26 表示 A->Z。

可以看出该密码有以下特点

  • 只有 0,1,2,4,8

例子

这里我们以 CFF 2016 影之密码为例进行介绍,题目

8842101220480224404014224202480122

我们按照 0 来进行分割,如下

内容 数字 字符
88421 8+8+4+2+1=23 W
122 1+2+2=5 E
48 4+8=12 L
2244 2+2+4+4=12 L
4 4 D
142242 1+4+2+2+4+2=15 O
248 2+4+8=14 N
122 1+2+2=5 E

所以最后的 flag 为 WELLDONE。

JSFuck

原理

JSFuck 可以只用 6 个字符 []()!+ 来编写 JavaScript 程序。比如我们想用 JSFuck 来实现 alert(1) 代码如下

[][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]][([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[!+[]+!+[]+!+[]]]]+([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]]]+([][[]]+[])[+[[+!+[]]]]+(![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[+!+[]]]]+([][[]]+[])[+[[+[]]]]+([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]((![]+[])[+[[+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]+(!![]+[])[+[[+[]]]]+([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[+!+[]]]+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+[+!+[]]+([][(![]+[])[+[[+[]]]]+([][[]]+[])[+[[!+[]+!+[]+!+[]+!+[]+!+[]]]]+(![]+[])[+[[!+[]+!+[]]]]+(!![]+[])[+[[+[]]]]+(!![]+[])[+[[!+[]+!+[]+!+[]]]]+(!![]+[])[+[[+!+[]]]]]+[])[+[[+!+[]]]+[[!+[]+!+[]+!+[]+!+[]+!+[]+!+[]]]])()

其他一些基本的表达:

false       =>  ![]
true        =>  !![]
undefined   =>  [][[]]
NaN         =>  +[![]]
0           =>  +[]
1           =>  +!+[]
2           =>  !+[]+!+[]
10          =>  [+!+[]]+[+[]]
Array       =>  []
Number      =>  +[]
String      =>  []+[]
Boolean     =>  ![]
Function    =>  []["filter"]
eval        =>  []["filter"]["constructor"]( CODE )()
window      =>  []["filter"]["constructor"]("return this")()

工具

BrainFuck

原理

Brainfuck,是一种极小化的计算机语言,它是由 Urban Müller 在 1993 年创建的。我们举一个例子,如果我们想要一个在屏幕上打印 Hello World!,那么对应的程序如下。对于其中的原理,感兴趣的可以自行网上搜索。

++++++++++[>+++++++>++++++++++>+++>+<<<<-]
>++.>+.+++++++..+++.>++.<<+++++++++++++++.
>.+++.------.--------.>+.>.

与其对应的还有 ook。

工具

猪圈密码

原理

猪圈密码是一种以格子为基础的简单替代式密码,格子如下

我们举一个例子,如明文为 X marks the spot ,那么密文如下

工具

舞动的小人密码

原理

这种密码出自于福尔摩斯探案集。每一个跳舞的小人实际上对应的是英文二十六个字母中的一个,而小人手中的旗子则表明该字母是单词的最后一个字母,如果仅仅是一个单词而不是句子,或者是句子中最后的一个单词,则单词中最后一个字母不必举旗。

键盘密码

所谓键盘密码,就是采用手机键盘或者电脑键盘进行加密。

手机键盘密码

手机键盘加密方式,是每个数字键上有 3-4 个字母,用两位数字来表示字母,例如:ru 用手机键盘表示就是:7382,那么这里就可以知道了,手机键盘加密方式不可能用 1 开头,第二位数字不可能超过 4,解密的时候参考此

关于手机键盘加密还有另一种方式,就是「音的」式(这一点可能根据手机的不同会有所不同),具体参照手机键盘来打,例如:「数字」表示出来就是:748 94。在手机键盘上面按下这几个数,就会出:「数字」的拼音。

电脑键盘棋盘

电脑键盘棋盘加密,利用了电脑的棋盘方阵。

电脑键盘坐标

电脑键盘坐标加密,利用键盘上面的字母行和数字行来加密,例:bye 用电脑键盘 XY 表示就是:351613

电脑键盘 QWE

电脑键盘 QWE 加密法,就是用字母表替换键盘上面的排列顺序。

键盘布局加密

简单地说就是根据给定的字符在键盘上的样子来进行加密。

0CTF 2014 classic

小丁丁发现自己置身于一个诡异的房间,面前只有一扇刻着奇怪字符的门。 他发现门边上还有一道密码锁,似乎要输入密码才能开门。。4esxcft5 rdcvgt 6tfc78uhg 098ukmnb

发现这么乱,还同时包括数字和字母猜想可能是键盘密码,试着在键盘上按照字母顺序描绘一下,可得到 0ops 字样,猜测就是 flag 了。

2017 年 xman 选拔赛——一二三,木头人

我数 123 木头人,再不行动就要被扣分。

23731263111628163518122316391715262121

密码格式 xman{flag}

题目中有很明显的提示 123,那么就自然需要联想到键盘密码中电脑键盘坐标密码,可以发现前几个数字第二个数字都是 1-3 范围内的,也验证了我们的猜测。于是

23-x

73-m

12-a

63-n

11-q

不对呀,密码格式是 xman{,第四个字符是 {,于是看了看 { 的位置,其并没有对应的横坐标,但是如果我们手动把它视为 11 的话,那么 111 就是 {。然后依次往后推,发现确实可行,,最后再把 121 视为 } 即可得到 flag。

xman{hintisenough}

从这里我们可以看出,我们还是要注意迁移性,不能单纯地照搬一些已有的知识。

总结

古典密码分析思路

CTF 中有关古典密码的题目,通常是根据密文求出明文,因此采用唯密文攻击居多,基本分析思路总结如下:

  1. 确定密码类型:根据题目提示、加密方式、密文字符集、密文展现形式等信息。
  2. 确定攻击方法:包括直接分析、蛮力攻击、统计分析等方法。对于无法确定类型的特殊密码,应根据其密码特性选用合适的攻击方法。
  3. 确定分析工具:以在线密码分析工具与 Python 脚本工具包为主,以离线密码分析工具与手工分析为辅。

以上唯密文攻击方法的适用场景与举例如下:

攻击方法 适用场景 举例
直接分析法 由密码类型可确定映射关系的代换密码 凯撒密码、猪圈密码、键盘密码等
蛮力攻击法 密钥空间较小的代换密码或置换密码 移位密码、栅栏密码等
统计分析法 密钥空间较大的代换密码 简单替换密码、仿射密码、维吉尼亚密码等

实验吧 围在栅栏里的爱

题目描述

最近一直在好奇一个问题,QWE 到底等不等于 ABC?

-.- .. –.- .-.. .– - ..-. -.-. –.- –. -. … — —

flag 格式:CTF{xxx}

首先,根据密码样式判断是摩斯电码,解密后得到 KIQLWTFCQGNSOO,看着也不像 flag,题目中还有还有栅栏与 QWE到底等不等于ABC,两个都试了试之后,发现是先 QWE 然后栅栏可得到结果。

首先键盘 QWE 解密,试着解密得到 IILYOAVNEBSAHR。继而栅栏解密得到 ILOVESHIYANBAR

2017 SECCON Vigenere3d

程序如下

# Vigenere3d.py
import sys
def _l(idx, s):
    return s[idx:] + s[:idx]
def main(p, k1, k2):
    s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
    t = [[_l((i+j) % len(s), s) for j in range(len(s))] for i in range(len(s))]
    i1 = 0
    i2 = 0
    c = ""
    for a in p:
        c += t[s.find(a)][s.find(k1[i1])][s.find(k2[i2])]
        i1 = (i1 + 1) % len(k1)
        i2 = (i2 + 1) % len(k2)
    return c
print main(sys.argv[1], sys.argv[2], sys.argv[2][::-1])

$ python Vigenere3d.py SECCON{**************************} **************
POR4dnyTLHBfwbxAAZhe}}ocZR3Cxcftw9

解法一

首先,我们先来分析一下 t 的构成 $$ t[i][j]=s[i+j:]+s[:i+j] \ t[i][k]=s[i+k:]+s[:i+k] $$

t[i][j][k]t[i][j][k] 为 t[i][j]t[i][j] 中的第 k 个字符,t[i][k][j]t[i][k][j] 为 t[i][k]t[i][k] 中的第 j 个字符。无论是 i+j+ki+j+k 是否超过 len(s) 两者都始终保持一致,即 t[i][j][k]=t[i][k][j]t[i][j][k]=t[i][k][j] 。

故而,其实对于相同的明文来说,可能有多个密钥使其生成相同的密文。

然而上面分析就是单纯地分析而已,,下面开始正题。

不难看出,密文的每一位只与明文的相应位相关,而且,密钥的每一位的空间最大也就是 s 的大小,所以我们可以使用爆破来获取密钥。这里根据上述命令行提示,可以知道密钥长度为 14,恰好明文前面 7 个字节已知。恢复密钥的 exp 如下

def get_key(plain, cipher):
    s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
    t = [[_l((i + j) % len(s), s) for j in range(len(s))]
         for i in range(len(s))]
    i1 = 0
    i2 = 0
    key = ['*'] * 14
    for i in range(len(plain)):
        for i1 in range(len(s)):
            for i2 in range(len(s)):
                if t[s.find(plain[i])][s.find(s[i1])][s.find(s[i2])] == cipher[
                        i]:
                    key[i] = s[i1]
                    key[13 - i] = s[i2]
    return ''.join(key)

恢复明文的脚本如下

def decrypt(cipher, k1, k2):
    s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
    t = [[_l((i + j) % len(s), s) for j in range(len(s))]
         for i in range(len(s))]
    i1 = 0
    i2 = 0
    plain = ""
    for a in cipher:
        for i in range(len(s)):
            if t[i][s.find(k1[i1])][s.find(k2[i2])] == a:
                plain += s[i]
                break
        i1 = (i1 + 1) % len(k1)
        i2 = (i2 + 1) % len(k2)
    return plain

得到明文如下

➜  2017_seccon_vigenere3d git:(master) python exp.py
SECCON{Welc0me_to_SECCON_CTF_2017}

解法二

关于此题的分析:

  1. 考虑到在程序正常运行下,数组访问不会越界,我们在讨论时做以下约定:arr[index]⇔arr[index%len(arr)]arr[index]⇔arr[index%len(arr)]
  2. 关于 python 程序中定义的 _l 函数,发现以下等价关系:_l(offset,arr)[index]⇔arr[index+offset]_l(offset,arr)[index]⇔arr[index+offset]
  3. 关于 python 的 main 函数中三维矩阵 t 的定义,发现以下等价关系:t[a][b][c]⇔_l(a+b,s)[c]t[a][b][c]⇔_l(a+b,s)[c]
  4. 综合第 2 第 3 点的观察,有如下等价关系:t[a][b][c]⇔s[a+b+c]t[a][b][c]⇔s[a+b+c]
  5. 我们将 s 视为一种编码格式,即:编码过程 s.find(x),解码过程 s[x]。并直接使用其编码结果的数字替代其所代指的字符串,那么加密过程可以用以下公式表示:
  6. e=f+k1+k2e=f+k1+k2
  7. 其中,e 是密文,f 是明文,k1 与 k2 是通过复制方法得到、与 f 长度一样的密钥,加法是向量加

所以我们只需要通过计算 k1+k2 ,模拟密钥,即可解密。关于此题的解密 python 脚本:

# exp2.py
enc_str = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}'
dec_dic = {k:v for v,k in enumerate(enc_str)}
encrypt = 'POR4dnyTLHBfwbxAAZhe}}ocZR3Cxcftw9'
flag_bg = 'SECCON{**************************}'

sim_key = [dec_dic[encrypt[i]]-dec_dic[flag_bg[i]] for i in range(7)] # 破解模拟密钥
sim_key = sim_key + sim_key[::-1]

flag_ed = [dec_dic[v]-sim_key[k%14] for k,v in enumerate(encrypt)] # 模拟密钥解密
flag_ed = ''.join([enc_str[i%len(enc_str)] for i in flag_ed]) # 解码
print(flag_ed)

得到明文如下:

$ python exp2.py
SECCON{Welc0me_to_SECCON_CTF_2017}

消失的三重密码

密文

of zit kggd zitkt qkt ygxk ortfzoeqs wqlatzwqssl qfr zvg ortfzoeqs yggzwqssl. fgv oy ngx vqfz zg hxz zitd of gft soft.piv dgfn lgsxzogfl qkt zitkt? zohl:hstqlt eiqfut zit ygkd gy zit fxdwtk ngx utz.zit hkgukqddtkl!

使用 quipquip 直接解密。

流密码

流密码一般逐字节或者逐比特处理信息。一般来说

  • 流密码的密钥长度会与明文的长度相同。
  • 流密码的密钥派生自一个较短的密钥,派生算法通常为一个伪随机数生成算法。

需要注意的是,流加密目前来说都是对称加密。

伪随机数生成算法生成的序列的随机性越强,明文中的统计特征被覆盖的更好。

流密码加解密非常简单,在已知明文的情况下,可以非常容易地获取密钥流。

流密码的关键在于设计好的伪随机数生成器。一般来说,伪随机数生成器的基本构造模块为反馈移位寄存器。当然,也有一些特殊设计的流密码,比如 RC4。

伪随机数生成器

伪随机数生成器介绍

概述

伪随机数生成器(pseudorandom number generator,PRNG),又称为确定性随机位生成器(deterministic random bit generator,DRBG),是用来生成接近于绝对随机数序列的数字序列的算法。一般来说,PRNG 会依赖于一个初始值,也称为种子,来生成对应的伪随机数序列。只要种子确定了,PRNG 所生成的随机数就是完全确定的,因此其生成的随机数序列并不是真正随机的。

就目前而言,PRNG 在众多应用都发挥着重要的作用,比如模拟(蒙特卡洛方法),电子竞技,密码应用。

随机性的严格性

  • 随机性:随机数应该不存在统计学偏差,是完全杂乱的数列。
  • 不可预测性:不能从过去的序列推测出下一个出现的数。
  • 不可重现性:除非数列保存下来,否则不能重现相同的数列。

这三个性质的严格性依次递增。

一般来说,随机数可以分为三类

类别 随机性 不可预测性 不可重现性
弱伪随机数
强伪随机数
真随机数

一般来说,密码学中使用的随机数是第二种。

周期

正如我们之前所说,一旦 PRNG 所依赖的种子确定了,那么 PRNG 生成的随机数序列基本也就确定了。这里定义 PRNG 的周期如下:对于一个 PRNG 的所有可能起始状态,不重复序列的最长长度。显然,对于一个 PRNG 来说,其周期不会大于其所有可能的状态。但是,需要注意的是,并不是当我们遇到重复的输出时,就可以认为是 PRNG 的周期,因为 PRNG 的状态一般都是大于输出的位数的。

评价标准

参见维基百科,https://en.wikipedia.org/wiki/Pseudorandom_number_generator。

分类

目前通用的伪随机数生成器主要有

  • 线性同余生成器,LCG
  • 线性回归发生器
  • Mersenne Twister
  • xorshift generators
  • WELL family of generators
  • Linear feedback shift register,LFSR,线性反馈移位寄存器

问题

通常来说,伪随机数生成器可能会有以下问题

  • 在某些种子的情况下,其生成的随机数序列的周期会比较小。
  • 生成大数时,分配的不均匀。
  • 连续值之间关联密切,知道后续值,可以知道之前的值。
  • 输出序列的值的大小很不均匀。

密码安全伪随机数生成器

介绍

密码学安全伪随机数生成器(cryptographically secure pseudo-random number generator,CSPRNG),也称为密码学伪随机数生成器(cryptographic pseudo-random number generator,CPRNG),是一种特殊的伪随机数生成器。它需要满足满足一些必要的特性,以便于适合于密码学应用。

密码学的很多方面都需要随机数

  • 密钥生成
  • 生成初始化向量,IV,用于分组密码的 CBC,CFB,OFB 模式
  • nounce,用于防止重放攻击以及分组密码的 CTR 模式等、
  • one-time pads
  • 某些签名方案中的盐,如 ECDSARSASSA-PSS

需求

毫无疑问,密码学安全伪随机数生成器的要求肯定比一般的伪随机数生成器要高。一般而言,CSPRNG 的要求可以分为两类

  • 通过统计随机性测试。CSPRNG 必须通过 next-bit test,也就是说,知道了一个序列的前 k 个比特,攻击者不可能在多项式时间内以大于 50% 的概率预测出来下一个比特位。这里特别提及一点,姚期智曾在 1982 年证明,如果一个生成器可以通过 next-bit test,那么它也可以通过所有其他的多项式时间统计测试。
  • 必须能够抵抗足够强的攻击,比如当生成器的部分初始状态或者运行时的状态被攻击者获知时,攻击者仍然不能够获取泄漏状态之前的生成的随机数。

分类

就目前而看, CSPRNG 的设计可以分为以下三类

  • 基于密码学算法,如密文或者哈希值。
  • 基于数学难题
  • 某些特殊目的的设计

2017 Tokyo Westerns CTF 3rd Backpacker’s Problem

题目中给了一个 cpp 文件,大概意思如下

Given the integers a_1, a_2, ..., a_N, your task is to find a subsequence b of a
where b_1 + b_2 + ... + b_K = 0.

Input Format: N a_1 a_2 ... a_N
Answer Format: K b_1 b_2 ... b_K

Example Input:
4 -8 -2 3 5
Example Answer:
3 -8 3 5

即是一个背包问题。其中,在本题中,我们需要解决 20 个这样的背包问题,背包大小依次是 1 * 10~20 * 10。而子集求和的背包问题是一个 NPC 问题,问题的时间复杂度随着随着背包大小而指数增长。这里背包的大小最大是 200,显然不可能使用暴力破解的方式。

线性同余生成器

2016 Google CTF woodman

程序的大概意思就是一个猜数游戏,如果连续猜中若干次,就算会拿到 flag,背后的生成相应数的核心代码如下

class SecurePrng(object):
    def __init__(self):
        # generate seed with 64 bits of entropy
        self.p = 4646704883L
        self.x = random.randint(0, self.p)
        self.y = random.randint(0, self.p)

    def next(self):
        self.x = (2 * self.x + 3) % self.p
        self.y = (3 * self.y + 9) % self.p
        return (self.x ^ self.y)

这里我们显然,我们猜出前两轮还是比较容易的,毕竟概率也有 0.25。这里当我们猜出前两轮后,使用 Z3 来求解出初始的 x 和 y,那么我们就可以顺利的猜出剩下的值了。

具体的脚本如下,然而 Z3 在解决这样的问题时似乎是有问题的。。。

这里我们考虑另外一种方法,依次从低比特位枚举到高比特位获取 x 的值,之所以能够这样做,是依赖于这样的观察

  • a + b = c,c 的第 i 比特位的值只受 a 和 b 该比特位以及更低比特位的影响。因为第 i 比特位进行运算时,只有可能收到低比特位的进位数值。
  • a - b = c,c 的第 i 比特位的值只受 a 和 b 该比特位以及更低比特位的影响。因为第 i 比特位进行运算时,只有可能向低比特位的借位。
  • a * b = c,c 的第 i 比特位的值只受 a 和 b 该比特位以及更低比特位的影响。因为这可以视作多次加法。
  • a % b = c,c 的第 i 比特位的值只受 a 和 b 该比特位以及更低比特位的影响。因为这可视为多次进行减法。
  • a ^ b = c,c 的第 i 比特位的值只受 a 和 b 该比特位的影响。这一点是显而易见的。

注:个人感觉这个技巧非常有用。

此外,我们不难得知 p 的比特位为 33 比特位。具体利用思路如下

  1. 首先获取两次猜到的值,这个概率有 0.25。
  2. 依次从低比特位到高比特位依次枚举第一次迭代后的 x 的相应比特位
  3. 根据自己枚举的值分别计算出第二次的值,只有当对应比特位正确,可以将其加入候选正确值。需要注意的是,这里由于取模,所以我们需要枚举到底减了多少次。
  4. 此外,在最终判断时,仍然需要确保对应的值满足一定要求,因为之前对减了多少次进行了枚举。

具体利用代码如下

import os
import random
from itertools import product


class SecurePrng(object):
    def __init__(self, x=-1, y=-1):
        # generate seed with 64 bits of entropy
        self.p = 4646704883L  # 33bit
        if x == -1:
            self.x = random.randint(0, self.p)
        else:
            self.x = x
        if y == -1:
            self.y = random.randint(0, self.p)
        else:
            self.y = y

    def next(self):
        self.x = (2 * self.x + 3) % self.p
        self.y = (3 * self.y + 9) % self.p
        return (self.x ^ self.y)


def getbiti(num, idx):
    return bin(num)[-idx - 1:]


def main():
    sp = SecurePrng()
    targetx = sp.x
    targety = sp.y
    print "we would like to get x ", targetx
    print "we would like to get y ", targety

    # suppose we have already guess two number
    guess1 = sp.next()
    guess2 = sp.next()

    p = 4646704883

    # newx = tmpx*2+3-kx*p
    for kx, ky in product(range(3), range(4)):
        candidate = [[0]]
        # only 33 bit
        for i in range(33):
            #print 'idx ', i
            new_candidate = []
            for old, bit in product(candidate, range(2)):
                #print old, bit
                oldx = old[0]
                #oldy = old[1]
                tmpx = oldx | ((bit & 1) << i)
                #tmpy = oldy | ((bit / 2) << i)
                tmpy = tmpx ^ guess1
                newx = tmpx * 2 + 3 - kx * p + (1 << 40)
                newy = tmpy * 3 + 9 - ky * p + (1 << 40)
                tmp1 = newx ^ newy
                #print "tmpx:    ", bin(tmpx)
                #print "targetx: ", bin(targetx)
                #print "calculate:     ", bin(tmp1 + (1 << 40))
                #print "target guess2: ", bin(guess1 + (1 << 40))
                if getbiti(guess2 + (1 << 40), i) == getbiti(
                        tmp1 + (1 << 40), i):
                    if [tmpx] not in new_candidate:
                        #print "got one"
                        #print bin(tmpx)
                        #print bin(targetx)
                        #print bin(tmpy)
                        new_candidate.append([tmpx])
            candidate = new_candidate
            #print len(candidate)
            #print candidate
        print "candidate x for kx: ", kx, " ky ", ky
        for item in candidate:
            tmpx = candidate[0][0]
            tmpy = tmpx ^ guess1
            if tmpx >= p or tmpx >= p:
                continue
            mysp = SecurePrng(tmpx, tmpy)
            tmp1 = mysp.next()
            if tmp1 != guess2:
                continue
            print tmpx, tmpy
            print(targetx * 2 + 3) % p, (targety * 3 + 9) % p


if __name__ == "__main__":
    main()

反馈移位寄存器

一般的,一个 n 级反馈移位寄存器如下图所示

线性反馈移位寄存器 - LFSR

介绍

线性反馈移位寄存器的反馈函数一般如下

$$
a_{i+n}=\sum\limits_{j=1}^{n}c_ja_{i+n-j}
$$
其中,
$$
c_j
$$
均在某个有限域 $F_q$ 中。

既然线性空间是一个线性变换,我们可以得知这个线性变换为

$$
\begin{align}
&\left[
a_{i+1},a_{i+2},a_{i+3}, …,a_{i+n}
\right]\\=&\left[
a_{i},a_{i+1},a_{i+2}, …,a_{i+n-1}
\right]\left[ \begin{matrix} 0 & 0 & \cdots & 0 & c_n \ 1 & 0 & \cdots & 0 & c_{n-1} \ 0 & 1 & \cdots & 0 & c_{n-2}\\vdots & \vdots & \ddots & \vdots \ 0 & 0 & \cdots & 1 & c_1 \ \end{matrix} \right]\\=&\left[
a_{0},a_{1},a_{2}, …,a_{n-1}
\right]\left[ \begin{matrix} 0 & 0 & \cdots & 0 & c_n \ 1 & 0 & \cdots & 0 & c_{n-1} \ 0 & 1 & \cdots & 0 & c_{n-2}\\vdots & \vdots & \ddots & \vdots \ 0 & 0 & \cdots & 1 & c_1 \ \end{matrix} \right]^{i+1}
\end{align
}
$$
进而,我们可以求得其特征多项式为

$$
f(x)=x^n-\sum\limits_{i=1}^{n}c_ix^{n-i}
$$
同时,我们定义其互反多项式为

$$
\overline f(x)=x^nf(\frac{1}{x})=1-\sum\limits_{i=1}^{n}c_ix^{i}
$$
我们也称互反多项式为线性反馈移位寄存器的联结多项式。

这里有一些定理需要我们记一下,感兴趣的可以自行推导。

特征多项式与生成函数

已知某个 n 级线性反馈移位寄存器的特征多项式,那么该序列对应的生成函数为

$$
A(x)=\frac{p(x)}{\overline f(x)}
$$

其中,$p(x)=\sum\limits_{i=1}^{n}(c_{n-i}x^{n-i}\sum\limits_{j=1}^{i}a_jx^{j-1})$。可以看出 p(x) 完全由初始状态和反馈函数的系数决定。

序列周期与生成函数

序列的的周期为其生成函数的既约真分式的分母的周期。

对于 n 级线性反馈移位寄存器,最长周期为 $2^{n}-1$(排除全零)。达到最长周期的序列一般称为 m 序列。

特殊性质

  • 将两个序列累加得到新的序列的周期为这两个序列的周期的和。
  • 序列是 n 级 m 序列,当且仅当序列的极小多项式是 n 次本原多项式。

B-M 算法

一般来说,我们可以从两种角度来考虑 LFSR

  • 密钥生成角度,一般我们希望使用级数尽可能低的 LFSR 来生成周期大,随机性好的序列。
  • 密码分析角度,给定一个长度为 n 的序列 a,如何构造一个级数尽可能小的 LFSR 来生成它。其实这就是 B-M 算法的来源。

一般来说,我们定义一个序列的线性复杂度如下

  • 若 s 为一个全零序列,则线性复杂度为0。
  • 若没有 LFSR 能生成 s,则线性复杂度为无穷。
  • 否则,s 的线性复杂度为生成 L(s) 的最小级的 LFSR。

BM 算法的要求我们需要知道长度为 2n 的序列。其复杂度

  • 时间复杂度:O(n^2) 次比特操作
  • 空间复杂度:O(n) 比特。

关于 BM 算法的细节,后续添加,目前处于学习过程中。

但是其实如果我们知道了长度为 2n 的序列,我们也可以一种比较笨的方法来获取原先的序列。不妨假设已知的序列为$a_1,…,a_{2n}$,我们可以令

$S_1=(a_1,…,a_n)$

$S_2=(a_2,…,a_{n+1})$

….

$S_{n+1}=(a_{n+1},…,a_{2n})$

那么我们可以构造矩阵 $X=(S_1,…,S_n)$,那么

$S_{n+1}=(c_n,…,c_1)X$

所以

$(c_n,…,c_1)=S_{n+1}X^{-1}$

进而我们也就知道了 LFSR 的反馈表达式,进而我们就可以推出初始化种子。

2018 强网杯 streamgame1

简单看一下题目

from flag import flag
assert flag.startswith("flag{")
assert flag.endswith("}")
assert len(flag)==25

def lfsr(R,mask):
    output = (R << 1) & 0xffffff
    i=(R&mask)&0xffffff
    lastbit=0
    while i!=0:
        lastbit^=(i&1)
        i=i>>1
    output^=lastbit
    return (output,lastbit)



R=int(flag[5:-1],2)
mask    =   0b1010011000100011100

f=open("key","ab")
for i in range(12):
    tmp=0
    for j in range(8):
        (R,out)=lfsr(R,mask)
        tmp=(tmp << 1)^out
    f.write(chr(tmp))
f.close()

可以发现,flag 的长度为25-5-1=19,所以可以暴力枚举。结果

➜  2018-强网杯-streamgame1 git:(master) ✗ python exp.py
12
0b1110101100001101011

因此 flag 为 flag{1110101100001101011}。

2018 CISCN 初赛 oldstreamgame

简单看一下题目

flag = "flag{xxxxxxxxxxxxxxxx}"
assert flag.startswith("flag{")
assert flag.endswith("}")
assert len(flag)==14

def lfsr(R,mask):
    output = (R << 1) & 0xffffffff
    i=(R&mask)&0xffffffff
    lastbit=0
    while i!=0:
        lastbit^=(i&1)
        i=i>>1
    output^=lastbit
    return (output,lastbit)

R=int(flag[5:-1],16)
mask = 0b10100100000010000000100010010100

f=open("key","w")
for i in range(100):
    tmp=0
    for j in range(8):
        (R,out)=lfsr(R,mask)
        tmp=(tmp << 1)^out
    f.write(chr(tmp))
f.close()

程序很简单,仍然是一个 LFSR,但是初态是 32 比特位,当然,我们也可以选择爆破,但是这里不选择爆破。

这里给出两种做法。

第一种做法,程序输出的第 32 个比特是由程序输出的前 31 个比特和初始种子的第 1 个比特来决定的,因此我们可以知道初始种子的第一个比特,进而可以知道初始种子的第 2 个比特,依次类推。代码如下

mask = 0b10100100000010000000100010010100
b = ''
N = 32
with open('key', 'rb') as f:
    b = f.read()
key = ''
for i in range(N / 8):
    t = ord(b[i])
    for j in xrange(7, -1, -1):
        key += str(t >> j & 1)
idx = 0
ans = ""
key = key[31] + key[:32]
while idx < 32:
    tmp = 0
    for i in range(32):
        if mask >> i & 1:
            tmp ^= int(key[31 - i])
    ans = str(tmp) + ans
    idx += 1
    key = key[31] + str(tmp) + key[1:31]
num = int(ans, 2)
print hex(num)

运行

➜  2018-CISCN-start-oldstreamgame git:(master) ✗ python exp1.py
0x926201d7

第二种做法,我们可以考虑一下矩阵转换的过程,如果进行了 32 次线性变换,那么就可以得到输出流前 32 个比特。而其实,我们只需要前 32 个比特就可以恢复初始状态了。

mask = 0b10100100000010000000100010010100

N = 32
F = GF(2)

b = ''
with open('key', 'rb') as f:
    b = f.read()

R = [vector(F, N) for i in range(N)]
for i in range(N):
    R[i][N - 1] = mask >> (31 - i) & 1
for i in range(N - 1):
    R[i + 1][i] = 1
M = Matrix(F, R)
M = M ^ N

vec = vector(F, N)
row = 0
for i in range(N / 8):
    t = ord(b[i])
    for j in xrange(7, -1, -1):
        vec[row] = t >> j & 1
        row += 1
print rank(M)
num = int(''.join(map(str, list(M.solve_left(vec)))), 2)
print hex(num)

运行脚本

➜  2018-CISCN-start-oldstreamgame git:(master) ✗ sage exp.sage
32
0x926201d7

从而 flag 为 flag{926201d7}。

还有一种做法是 TokyoWesterns 的,可以参考对应的文件夹的文件。

非线性反馈移位寄存器

介绍

为了使得密钥流输出的序列尽可能复杂,会使用非线性反馈移位寄存器,常见的有三种

  • 非线性组合生成器,对多个 LFSR 的输出使用一个非线性组合函数
  • 非线性滤波生成器,对一个 LFSR 的内容使用一个非线性组合函数
  • 钟控生成器,使用一个(或多个)LFSR 的输出来控制另一个(或多个)LFSR 的时钟

非线性组合生成器

简介

组合生成器一般如下图所示。

Geffe

这里我们以 Geffe 为例进行介绍。Geffe 包含 3 个线性反馈移位寄存器,非线性组合函数为
$$
F(x1,x2,x3)=(x1\andx2)⊕(┐x1\andx3)=(x1\andx2)⊕(x1\andx3)⊕x3F(x1,x2,x3)=(x1\andx2)⊕(⌝x1\andx3)=(x1\andx2)⊕(x1\andx3)⊕x3
$$

2018 强网杯 streamgame3

简单看一下题目

from flag import flag
assert flag.startswith("flag{")
assert flag.endswith("}")
assert len(flag)==24

def lfsr(R,mask):
    output = (R << 1) & 0xffffff
    i=(R&mask)&0xffffff
    lastbit=0
    while i!=0:
        lastbit^=(i&1)
        i=i>>1
    output^=lastbit
    return (output,lastbit)


def single_round(R1,R1_mask,R2,R2_mask,R3,R3_mask):
    (R1_NEW,x1)=lfsr(R1,R1_mask)
    (R2_NEW,x2)=lfsr(R2,R2_mask)
    (R3_NEW,x3)=lfsr(R3,R3_mask)
    return (R1_NEW,R2_NEW,R3_NEW,(x1*x2)^((x2^1)*x3))

R1=int(flag[5:11],16)
R2=int(flag[11:17],16)
R3=int(flag[17:23],16)
assert len(bin(R1)[2:])==17
assert len(bin(R2)[2:])==19
assert len(bin(R3)[2:])==21
R1_mask=0x10020
R2_mask=0x4100c
R3_mask=0x100002


for fi in range(1024):
    print fi
    tmp1mb=""
    for i in range(1024):
        tmp1kb=""
        for j in range(1024):
            tmp=0
            for k in range(8):
                (R1,R2,R3,out)=single_round(R1,R1_mask,R2,R2_mask,R3,R3_mask)
                tmp = (tmp << 1) ^ out
            tmp1kb+=chr(tmp)
        tmp1mb+=tmp1kb
    f = open("./output/" + str(fi), "ab")
    f.write(tmp1mb)
    f.close()

可以看出,该程序与 Geffe 生成器非常类似,这里我们使用相关攻击方法进行攻击,我们可以统计一下在三个 LFSR 输出不同的情况下,最后类 Geffe 生成器的输出,如下

x1x1 x2x2 x3x3 F(x1,x2,x3)F(x1,x2,x3)
0 0 0 0
0 0 1 1
0 1 0 0
0 1 1 0
1 0 0 0
1 0 1 1
1 1 0 1
1 1 1 1

可以发现

  • Geffe 的输出与 x1x1 相同的概率为 0.75
  • Geffe 的输出与 x2x2 相同的概率为 0.5
  • Geffe 的输出与 x3x3 相同的概率为 0.75

这说明输出与第一个和第三个的关联性非常大。 因此,我们可以暴力去枚举第一个和第三个 LFSR 的输出判断其与 类 Geffe 的输出相等的个数,如果大约在 75% 的话,就可以认为是正确的。第二个就直接暴力枚举了。

脚本如下

#for x1 in range(2):
#    for x2 in range(2):
#        for x3 in range(2):
#            print x1,x2,x3,(x1*x2)^((x2^1)*x3)
#n = [17,19,21]

#cycle = 1
#for i in n:
#    cycle = cycle*(pow(2,i)-1)
#print cycle


def lfsr(R, mask):
    output = (R << 1) & 0xffffff
    i = (R & mask) & 0xffffff
    lastbit = 0
    while i != 0:
        lastbit ^= (i & 1)
        i = i >> 1
    output ^= lastbit
    return (output, lastbit)


def single_round(R1, R1_mask, R2, R2_mask, R3, R3_mask):
    (R1_NEW, x1) = lfsr(R1, R1_mask)
    (R2_NEW, x2) = lfsr(R2, R2_mask)
    (R3_NEW, x3) = lfsr(R3, R3_mask)
    return (R1_NEW, R2_NEW, R3_NEW, (x1 * x2) ^ ((x2 ^ 1) * x3))


R1_mask = 0x10020
R2_mask = 0x4100c
R3_mask = 0x100002
n3 = 21
n2 = 19
n1 = 17


def guess(beg, end, num, mask):
    ansn = range(beg, end)
    data = open('./output/0').read(num)
    data = ''.join(bin(256 + ord(c))[3:] for c in data)
    now = 0
    res = 0
    for i in ansn:
        r = i
        cnt = 0
        for j in range(num * 8):
            r, lastbit = lfsr(r, mask)
            lastbit = str(lastbit)
            cnt += (lastbit == data[j])
        if cnt > now:
            now = cnt
            res = i
            print now, res
    return res


def bruteforce2(x, z):
    data = open('./output/0').read(50)
    data = ''.join(bin(256 + ord(c))[3:] for c in data)
    for y in range(pow(2, n2 - 1), pow(2, n2)):
        R1, R2, R3 = x, y, z
        flag = True
        for i in range(len(data)):
            (R1, R2, R3,
             out) = single_round(R1, R1_mask, R2, R2_mask, R3, R3_mask)
            if str(out) != data[i]:
                flag = False
                break
        if y % 10000 == 0:
            print 'now: ', x, y, z
        if flag:
            print 'ans: ', hex(x)[2:], hex(y)[2:], hex(z)[2:]
            break


R1 = guess(pow(2, n1 - 1), pow(2, n1), 40, R1_mask)
print R1
R3 = guess(pow(2, n3 - 1), pow(2, n3), 40, R3_mask)
print R3
R1 = 113099
R3 = 1487603

bruteforce2(R1, R3)

运行结果如下

➜  2018-CISCN-start-streamgame3 git:(master) ✗ python exp.py
161 65536
172 65538
189 65545
203 65661
210 109191
242 113099
113099
157 1048576
165 1048578
183 1048580
184 1049136
186 1049436
187 1049964
189 1050869
190 1051389
192 1051836
194 1053573
195 1055799
203 1060961
205 1195773
212 1226461
213 1317459
219 1481465
239 1487603
1487603
now:  113099 270000 1487603
now:  113099 280000 1487603
now:  113099 290000 1487603
now:  113099 300000 1487603
now:  113099 310000 1487603
now:  113099 320000 1487603
now:  113099 330000 1487603
now:  113099 340000 1487603
now:  113099 350000 1487603
now:  113099 360000 1487603
ans:  1b9cb 5979c 16b2f3

从而 flag 为 flag{01b9cb05979c16b2f3}。

题目

  • 2017 WHCTF Bornpig
  • 2018 Google CTF 2018 Betterzip

RC4

基本介绍

RSA 由 Ron Rivest 设计,最初隶属于 RSA 安全公司,是一个专利密码产品。它是面向字节的流密码,密钥长度可变,非常简单,但也很有效果。RC4 算法广泛应用于 SSL/TLS 协议和 WEP/WPA 协议。

基本流程

RC4 主要包含三个流程

  • 初始化 S 和 T 数组。
  • 初始化置换 S。
  • 生成密钥流。

初始化 S 和 T 数组

初始化 S 和 T 的代码如下

for i = 0 to 255 do
    S[i] = i
    T[i] = K[i mod keylen])

初始化置换 S

j = 0
for i = 0 to 255 do 
    j = (j + S[i] + T[i]) (mod 256) 
    swap (S[i], S[j])

生成流密钥

i = j = 0 
for each message byte b
    i = (i + 1) (mod 256)
    j = (j + S[i]) (mod 256)
    swap(S[i], S[j])
    t = (S[i] + S[j]) (mod 256) 
    print S[t]

我们一般称前两部分为 KSA ,最后一部分是 PRGA。

块加密

概述

所谓块加密就是每次加密一块明文,常见的加密算法有

  • IDEA 加密
  • DES 加密
  • AES 加密

块加密也是对称加密。

其实,我们也可以把块加密理解一种特殊的替代密码,但是其每次替代的是一大块。而正是由于一大块,明文空间巨大,而且对于不同的密钥,我们无法做一个表进行对应相应的密文,因此必须得有 复杂 的加解密算法来加解密明密文。

而与此同时,明文往往可能很长也可能很短,因此在块加密时往往需要两个辅助

  • padding,即 padding 到指定分组长度
  • 分组加密模式,即明文分组加密的方式。

基本策略

在分组密码设计时,充分使用了 Shannon 提出的两大策略:混淆与扩散两大策略。

混淆

混淆,Confusion,将密文与密钥之间的统计关系变得尽可能复杂,使得攻击者即使获取了密文的一些统计特性,也无法推测密钥。一般使用复杂的非线性变换可以得到很好的混淆效果,常见的方法如下

  • S 盒
  • 乘法

扩散

扩散,Diffusion,使得明文中的每一位影响密文中的许多位。常见的方法有

  • 线性变换
  • 置换
  • 移位,循环移位

常见加解密结构

目前块加密中主要使用的是结构是

  • 迭代结构,这是因为迭代结构便于设计与实现,同时方便安全性评估。

迭代结构

概述

迭代结构基本如下,一般包括三个部分

  • 密钥置换
  • 轮加密函数
  • 轮解密函数

轮函数

目前来说,轮函数主要有主要有以下设计方法

  • Feistel Network,由 Horst Feistel 发明,DES 设计者之一。
    • DES
  • Substitution-Permutation Network(SPN)
    • AES
  • 其他方案

密钥扩展

目前,密钥扩展的方法有很多,没有见到什么完美的密钥扩展方法,基本原则是使得密钥的每一个比特尽可能影响多轮的轮密钥。

ARX: Add-Rotate-Xor

概述

ARX 运算是如下 3 种基本运算的统称 - Add 有限域上的模加 - Rotate 循环移位 - Xor 异或

有许多常见的块加密算法在轮函数中只用到了这 3 种基本运算,典型例子如 Salsa20、Speck 等。另外 IDEA 也采用了类似的基本运算来构建加解密操作,不过以乘法代替了移位。

优缺点

优点

  • 操作简单,运算速度快
  • 执行时间为常数,可以避免基于时间的测信道攻击
  • 组合后的函数表达能力足够强(参见下方例题)

缺点

  • 在三种基本运算当中,Rotate、Xor 对于单个 bit 来说均是完全线性的运算,可能会带来一定的脆弱性 (参见 Rotational cryptanalysis)

题目

2018 *ctf primitive

分析

本题要求我们组合一定数目以内的 Add-Rotate-Xor 运算,使得获得的加密算法能够将固定明文加密成指定的随机密文,即通过基础运算来构建任意置换函数。成功构建 3 次之后即可获得 flag。

解题思路

对于模 256 下的运算,一种典型的基于 ARX 的换位操作可以表示为如下组合

RotateLeft_1(Add_255(RotateLeft_7(Add_2(x))))

上述函数对应了一个将 254 和 255 进行交换,同时保持其它数字不变的置换运算。

直觉上来说,由于在第一步的模加 2 运算中,仅有输入为 254、255 时会发生进位,该组合函数得以区别对待这一情况。

利用上述原子操作,我们可以构造出任意两个数字 a,b 的置换,结合 Xor 操作,我们可以减少所需的基本操作数目,使其满足题目给出的限制。一种可能的操作步骤如下:

  1. 对于 a,b,通过模加操作使得 a 为 0
  2. 通过右移使得 b 的最低位为 1
  3. b 不为 1,进行 Xor 1, Add 255 操作,保持 a 仍然为 0,同时 b 的数值减小
  4. 重复操作 2-3 直至 b 为 1
  5. 进行 Add 254 及换位操作,交换 a,b
  6. 对于换位以外的所有操作,加入对应的逆运算,确保 a,b 以外的数值不变

完整的解题脚本如下:

from pwn import *
import string
from hashlib import sha256

#context.log_level='debug'
def dopow():
    chal = c.recvline()
    post = chal[12:28]
    tar = chal[33:-1]
    c.recvuntil(':')
    found = iters.bruteforce(lambda x:sha256(x+post).hexdigest()==tar, string.ascii_letters+string.digits, 4)
    c.sendline(found)

#c = remote('127.0.0.1',10001)
c = remote('47.75.4.252',10001)
dopow()
pt='GoodCipher'

def doswap(a,b):
    if a==b:
        return
    if a>b:
        tmp=b
        b=a
        a=tmp
    ans=[]
    ans.append((0,256-a))
    b-=a
    a=0
    while b!=1:
        tmp=0
        lo=1
        while b&lo==0:
            lo<<=1
            tmp+=1
        if b==lo:
            ans.append((1,8-tmp))
            break
        if tmp!=0:
            ans.append((1,8-tmp))
        b>>=tmp
        ans.append((2,1))
        b^=1
        ans.append((0,255))
        b-=1
    ans.append((0,254))

    for a,b in ans:
        c.sendline('%d %d'%(a,b))
        c.recvline()
    for a,b in [(0,2),(1,7),(0,255),(1,1)]:
        c.sendline('%d %d'%(a,b))
        c.recvline()
    for a,b in ans[::-1]:
        if a==0:
            c.sendline('%d %d'%(a,256-b))
        elif a==1:
            c.sendline('%d %d'%(a,8-b))
        elif a==2:
            c.sendline('%d %d'%(a,b))
        c.recvline()

for i in range(3):
    print i
    m=range(256)
    c.recvuntil('ciphertext is ')
    ct=c.recvline().strip()
    ct=ct.decode('hex')
    assert len(ct)==10
    for i in range(10):
        a=ord(ct[i])
        b=ord(pt[i])
        #print m[a],b
        doswap(m[a],b)
        for j in range(256):
            if m[j]==b:
                m[j]=m[a]
                m[a]=b
                break
    c.sendline('-1')

c.recvuntil('Your flag here.\n')
print c.recvline()

DES

基本介绍

Data Encryption Standard(DES),数据加密标准,是典型的块加密,其基本信息如下

  • 输入 64 位。
  • 输出 64 位。
  • 密钥 64 位,使用 64 位密钥中的 56 位,剩余的 8 位要么丢弃,要么作为奇偶校验位。
  • Feistel 迭代结构
    • 明文经过 16 轮迭代得到密文。
    • 密文经过类似的 16 轮迭代得到明文。

基本流程

给出一张简单的 DES 流程图 。

加密

我们可以考虑一下每一轮的加密过程

$$
L_{i+1}=R_i
$$

$$
R_{i+1}=L_i\oplus F(R_i,K_i)
$$

那么在最后的 Permutation 之前,对应的密文为
$$
(R_{n+1},L_{n+1})
$$

解密

那么解密如何解密呢?首先我们可以把密文先进行逆置换,那么就可以得到最后一轮的输出。我们这时考虑每一轮

$$
R_i=L_{i+1}
$$

$$
L_i=R_{i+1}\oplus F(L_{i+1},K_i)
$$

因此,
$$
(L_0,R_0)
$$
就是加密时第一次置换后的明文。我们只需要再执行逆置换就可以获得明文了。

可以看出,DES 加解密使用同一套逻辑,只是密钥使用的顺序不一致。

核心部件

DES 中的核心部件主要包括(这里只给出加密过程的)

  • 初始置换
  • F 函数
    • E 扩展函数
    • S 盒,设计标准未给出。
    • P 置换
  • 最后置换

其中 F 函数如下

如果对 DES 更加感兴趣,可以进行更加仔细地研究。欢迎提供 PR。

衍生

在 DES 的基础上,衍生了以下两种加密方式

  • 双重 DES
  • 三种 DES

双重 DES

双重 DES 使用两个密钥,长度为 112 比特。加密方式如下

$$
C=E_{k2}(E_{k1}(P))
$$
但是双重 DES 不能抵抗中间相遇攻击,我们可以构造如下两个集合

$$
I={E_{k1}(P)}
$$

$$
J=D_{k2}(C)
$$

即分别枚举 K1 和 K2 分别对 P 进行加密和对 C 进行解密。

在我们对 P 进行加密完毕后,可以对加密结果进行排序,这样的复杂度为
$$
2^nlog(2^n)=O(n2^n)
$$
当我们对 C 进行解密时,可以每解密一个,就去对应的表中查询。

总的复杂度为还是
$$
O(n2^n)
$$

三重 DES

三重 DES 的加解密方式如下

$$
C=E_{k3}(D_{k2}(E_{k1}(P)))
$$

$$
P=D_{k1}(E_{k2}(D_{k3}(C)))
$$

在选择密钥时,可以有两种方法

  • 3 个不同的密钥,k1,k2,k3 互相独立,一共 168 比特。
  • 2 个不同的密钥,k1 与 k2 独立,k3=k1,112 比特。

攻击方法

  • 差分攻击
  • 线性攻击

2018 N1CTF N1ES

基本代码如下

# -*- coding: utf-8 -*-
def round_add(a, b):
    f = lambda x, y: x + y - 2 * (x & y)
    res = ''
    for i in range(len(a)):
        res += chr(f(ord(a[i]), ord(b[i])))
    return res

def permutate(table, block):
    return list(map(lambda x: block[x], table))

def string_to_bits(data):
    data = [ord(c) for c in data]
    l = len(data) * 8
    result = [0] * l
    pos = 0
    for ch in data:
        for i in range(0,8):
            result[(pos<<3)+i] = (ch>>i) & 1
        pos += 1
    return result

s_box = [54, 132, 138, 83, 16, 73, 187, 84, 146, 30, 95, 21, 148, 63, 65, 189, 188, 151, 72, 161, 116, 63, 161, 91, 37, 24, 126, 107, 87, 30, 117, 185, 98, 90, 0, 42, 140, 70, 86, 0, 42, 150, 54, 22, 144, 153, 36, 90, 149, 54, 156, 8, 59, 40, 110, 56,1, 84, 103, 22, 65, 17, 190, 41, 99, 151, 119, 124, 68, 17, 166, 125, 95, 65, 105, 133, 49, 19, 138, 29, 110, 7, 81, 134, 70, 87, 180, 78, 175, 108, 26, 121, 74, 29, 68, 162, 142, 177, 143, 86, 129, 101, 117, 41, 57, 34, 177, 103, 61, 135, 191, 74, 69, 147, 90, 49, 135, 124, 106, 19, 8
9, 38, 21, 41, 17, 155, 83, 38, 159, 179, 19, 157, 68, 105, 151, 166, 171, 122, 179, 114, 52, 183, 89, 107, 113, 65, 161, 141, 18, 121, 95, 4, 95, 101, 81, 156,
 17, 190, 38, 84, 9, 171, 180, 59, 45, 15, 34, 89, 75, 164, 190, 140, 6, 41, 188, 77, 165, 105, 5, 107, 31, 183, 107, 141, 66, 63, 10, 9, 125, 50, 2, 153, 156, 162, 186, 76, 158, 153, 117, 9, 77, 156, 11, 145, 12, 169, 52, 57, 161, 7, 158, 110, 191, 43, 82, 186, 49, 102, 166, 31, 41, 5, 189, 27]

def generate(o):
    k = permutate(s_box,o)
    b = []
    for i in range(0, len(k), 7):
        b.append(k[i:i+7] + [1])
    c = []
    for i in range(32):
        pos = 0
        x = 0
        for j in b[i]:
            x += (j<<pos)
            pos += 1
        c.append((0x10001**x) % (0x7f))
    return c



class N1ES:
    def __init__(self, key):
        if (len(key) != 24 or isinstance(key, bytes) == False ):
            raise Exception("key must be 24 bytes long")
        self.key = key
        self.gen_subkey()

    def gen_subkey(self):
        o = string_to_bits(self.key)
        k = []
        for i in range(8):
            o = generate(o)
            k.extend(o)
            o = string_to_bits([chr(c) for c in o[0:24]])
        self.Kn = []
        for i in range(32):
            self.Kn.append(map(chr, k[i * 8: i * 8 + 8]))
        return

    def encrypt(self, plaintext):
        if (len(plaintext) % 16 != 0 or isinstance(plaintext, bytes) == False):
            raise Exception("plaintext must be a multiple of 16 in length")
        res = ''
        for i in range(len(plaintext) / 16):
            block = plaintext[i * 16:(i + 1) * 16]
            L = block[:8]
            R = block[8:]
            for round_cnt in range(32):
                L, R = R, (round_add(L, self.Kn[round_cnt]))
            L, R = R, L
            res += L + R
        return res

显然,我们可以将其视为一个 Feistel 加密的方式,解密函数如下

    def decrypt(self,ciphertext):
        res = ''
        for i in range(len(ciphertext) / 16):
            block = ciphertext[i * 16:(i + 1) * 16]
            L = block[:8]
            R = block[8:]
            for round_cnt in range(32):
                L, R =R, (round_add(L, self.Kn[31-round_cnt]))
            L,R=R,L
            res += L + R
        return res

最后结果为

➜  baby_N1ES cat challenge.py
from N1ES import N1ES
import base64
key = "wxy191iss00000000000cute"
n1es = N1ES(key)
flag = "N1CTF{*****************************************}"
cipher = n1es.encrypt(flag)
#print base64.b64encode(cipher)  # HRlgC2ReHW1/WRk2DikfNBo1dl1XZBJrRR9qECMNOjNHDktBJSxcI1hZIz07YjVx
cipher = 'HRlgC2ReHW1/WRk2DikfNBo1dl1XZBJrRR9qECMNOjNHDktBJSxcI1hZIz07YjVx'
cipher = base64.b64decode(cipher)
print n1es.decrypt(cipher)
➜  baby_N1ES python challenge.py
N1CTF{F3istel_n3tw0rk_c4n_b3_ea5i1y_s0lv3d_/--/}

2019 CISCN part_des

题目只给了一个文件:

Round n part_encode-> 0x92d915250119e12b
Key map -> 0xe0be661032d5f0b676f82095e4d67623628fe6d376363183aed373a60167af537b46abc2af53d97485591f5bd94b944a3f49d94897ea1f699d1cdc291f2d9d4a5c705f2cad89e938dbacaca15e10d8aeaed90236f0be2e954a8cf0bea6112e84

考虑到题目名以及数据特征,Round n part_encode 为执行n轮des的中间结果,Key map 应为des的子密钥,要还原出明文只需进行n轮des加密的逆过程即可,解密时注意以下三点。

  • 子密钥的选取,对于只进行了n轮的加密结果,解密时应依次使用密钥 n, n-1…, 1。
  • des 最后一轮后的操作,未完成的 des 没有交换左右两部分和逆初始置换,因此解密时我们应先对密文进行这两步操作。
  • n 的选择,在本题中,我们并不知道 n,但这无关紧要,我们可以尝试所有可能的取值(0-15)flag应为ascii字符串。

代码:

kkk = 16
def bit_rot_left(lst, pos):
    return lst[pos:] + lst[:pos]

class DES:
    IP = [
            58,50,42,34,26,18,10,2,60,52,44,36,28,20,12,4,
            62,54,46,38,30,22,14,6,64,56,48,40,32,24,16,8,
            57,49,41,33,25,17,9,1,59,51,43,35,27,19,11,3,
            61,53,45,37,29,21,13,5,63,55,47,39,31,23,15,7
        ]
    IP_re = [
            40,8,48,16,56,24,64,32,39,7,47,15,55,23,63,31,
            38,6,46,14,54,22,62,30,37,5,45,13,53,21,61,29,
            36,4,44,12,52,20,60,28,35,3,43,11,51,19,59,27,
            34,2,42,10,50,18,58,26,33,1,41,9,49,17,57,25
        ]
    Pbox = [
            16,7,20,21,29,12,28,17,1,15,23,26,5,18,31,10,
            2,8,24,14,32,27,3,9,19,13,30,6,22,11,4,25
        ]
    E = [
            32,1,2,3,4,5,4,5,6,7,8,9,
            8,9,10,11,12,13,12,13,14,15,16,17,
            16,17,18,19,20,21,20,21,22,23,24,25,
            24,25,26,27,28,29,28,29,30,31,32,1
        ]
    PC1 = [
                57,49,41,33,25,17,9,1,58,50,42,34,26,18,
                10,2,59,51,43,35,27,19,11,3,60,52,44,36,
                63,55,47,39,31,23,15,7,62,54,46,38,30,22,
                14,6,61,53,45,37,29,21,13,5,28,20,12,4
        ]
    PC2 = [
            14,17,11,24,1,5,3,28,15,6,21,10,
            23,19,12,4,26,8,16,7,27,20,13,2,
            41,52,31,37,47,55,30,40,51,45,33,48,
            44,49,39,56,34,53,46,42,50,36,29,32
        ]
    Sbox = [
            [
                [14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7],
                [0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8],
                [4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0],
                [15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13],
            ],
            [
                [15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10],
                [3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5],
                [0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15],
                [13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9],
            ],
            [
                [10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8],
                [13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1],
                [13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7],
                [1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12],
            ],
            [
                [7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15],
                [13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9],
                [10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4],
                [3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14],
            ],
            [
                [2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9],
                [14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6],
                [4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14],
                [11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3],
            ],
            [
                [12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11],
                [10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8],
                [9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6],
                [4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13],
            ],
            [
                [4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1],
                [13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6],
                [1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2],
                [6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12],
            ],
            [
                [13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7],
                [1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2],
                [7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8],
                [2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11],
            ]
        ]
    rout = [1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1]
    def __init__(self):
        self.subkey = [[[1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1], [1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1], [1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1], [1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1], [1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1], [1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0], [1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1], [0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0], [0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0], [0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1], [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0], [0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0], [1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0], [1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0], [1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0], [1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0]], [[1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0], [1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0], [1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0], [1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0], [0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1], [0, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0], [0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0], [1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1], [1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 0], [1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1], [1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1], [1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1], [1, 1, 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1], [1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1]]]

    def permute(self, lst, tb):
        return [lst[i-1] for i in tb]

    def f(self,riti,subkeyi):
        tmp = [i^j for i,j in zip(subkeyi,self.permute(riti,DES.E))]
        return  self.permute(sum([[int(l) for l in str(bin(DES.Sbox[i][int(str(tmp[6*i])+str(tmp[6*i+5]),2)][int("".join(str(j) for j in tmp[6*i+1:6*i+5]),2)])[2:].zfill(4))] for i in range(8)],[]),DES.Pbox)

    def des_main(self,m,mark):
        sbkey = self.subkey[0]
        #if mark == 'e' else self.subkey[1]
        # tmp =  self.permute([int(i) for i in list((m).ljust(64,"0"))],self.IP)
        tmp =  [int(i) for i in list((m).ljust(64,"0"))]
        global kkk
        print(kkk)
        for i in range(kkk):
            tmp = tmp[32:] + [j^k for j,k in zip(tmp[:32],self.f(tmp[32:],sbkey[i if mark != 'd' else kkk-1-i]))]
        return "".join([str(i) for i in self.permute(tmp[32:]+tmp[:32],self.IP_re)])

    def des_encipher(self,m):
        m = "".join([bin(ord(i))[2:].zfill(8) for i in m])
        des_en = self.des_main(m,'e')
        return "".join([chr(int(des_en[i*8:i*8+8],2)) for i in range(8)])

    def des_decipher(self,c):
        c = "".join([bin(ord(i))[2:].zfill(8) for i in c])
        des_de = self.des_main(c,'d')
        return "".join([chr(int(des_de[i*8:i*8+8],2)) for i in range(8)])

def test():
    import base64
    global kkk
    while kkk >=0:
        desobj = DES()
        # cipher = desobj.des_encipher("12345678")
        cipher = '\x01\x19\xe1+\x92\xd9\x15%'
        message1 = desobj.des_decipher(cipher)
        print(message1)
        kkk -= 1
if __name__=='__main__':
    test()

```

解密结果(部分):

14
t-ÏEÏx§
13
y0ur9Ood
12
µp^Ûé=¹ˆ
11
)Á`rûÕû

可以看出n为13,flag为flag{y0ur9Ood}

IDEA

概述

国际数据加密算法(International Data Encryption Algorithm,IDEA),最早称为改良建议加密标准(Improved Proposed Encryption Standard,IPES),是密码学上一种对称密钥分组密码,由 James Massey 与来学嘉设计,在 1991 年首次提出。这个算法的提出,是为了取代旧有的数据加密标准 DES。

基本流程

密钥生成

IDEA 在加密的每轮中使用 6 个密钥,然后最后输出轮使用 4 个密钥。所以一共有 52 个。

  1. 前 8 个密钥来自与该算法最初的密钥,K1 取自密钥的高 16 比特,K8 取自密钥的低 16 比特。
  2. 将密钥循环左移 25 位获取下一轮密钥,然后再次分为 8 组。

加密流程

IDEA 加密的数据块的大小为 64 比特,其使用的密钥长度为 128 比特。该算法会对输入的数据块进行 8 次相同的变换,只是每次使用的密钥不同,最后会进行一次输出变换。每一轮的操作

可以输入和输出都是 16 比特位一组。每一轮的主要执行的运算有

  • 按位异或,⊕
  • 模加,模数为 2^16 ,⊞
  • 模乘,模数为 2^16+1,⊙。但是需要注意的是 0x0000 的输入会被修改为 2^16 ,2^16 的输出结果会被修改为 0x0000。

这里我们称由 K5,K6 构成的中间那个方格的加密方式为 MA。这也是 IDEA 算法中重要的一部分,此外,我们称 MA_L 为该部分加密后的左侧结果,其最后会和最左边的 16 比特操作;MA_R 为该部分加密后的右半部分的结果,其最后会和第三个 16 比特操作。

在最后输出轮的操作如下

解密流程

解密流程与加密流程相似,主要在于其密钥的选取

  • 第 i(1-9) 轮的解密的密钥的前 4 个子密钥由加密过程中第 10-i 轮的前 4 个子密钥得出
  • 其中第 1 个和第 4 个解密子密钥为相应的子密钥关于 2^16+1的乘法逆元。
  • 第 2 个和第 3 个子密钥的取法为
    • 当轮数为 2,…,8 时,取相应的第 3 个和第 2 个的子密钥的2^16的加密逆元。
    • 当轮数为 1 或 9 时,取相应的第 2 个和第 3 个子密钥对应的2^16 的加密逆元。
  • 第 5 和第 6 个密钥不变。

总体流程

我们来证明一下算法的正确性,这里我们关注于解密算法的第一轮,首先我们先看一下Yi是如何得到的

Y1=W81⊙Z49

Y2=W83⊞Z50Y

Y3=W82⊞Z51

Y4=W83⊙Z52

解密时,第一轮直接进行的变换为

J11=Y1⊙U1=Y1⊙Z−149=W81

J12=Y2⊞U2=Y2⊞Z−150=W83

J13=Y3⊞U3=Y3⊞Z−151=W82

J14=Y4⊙U4=Y4⊙Z−152=W84

可以看出得到的结果只有中间的两个 16 位加密结果恰好相反。我们进一步看一下W8i是如何得到的。

W81=I81⊕MAR(I81⊕I83,I82⊕I84)

W82=I83⊕MAR(I81⊕I83,I82⊕I84)

W83=I82⊕MAL(I81⊕I83,I82⊕I84)

W84=I84⊕MAL(I81⊕I83,I82⊕I84)

那么对于 V11 来说

V11=J11⊕MAR(J11⊕J13,J12⊕J14)

通过简单带入已有的值,显然

V11=W81⊕MAR(I81⊕I83,I82⊕I84)=I81

对于其他的元素也类似,那么其实我们会发现第一轮解密后的结果恰好是I81,I83,I82,I84。

类似地,这个关系可以一直满足直到

V81=I11,V82=I13,V83=I12,V84=I14

那么最后再经过一次简单的输出变换,恰好得到最初加密的数值。

AES

基本介绍

Advanced Encryption Standard(AES),高级加密标准,是典型的块加密,被设计来取代 DES,由 Joan Daemen 和 Vincent Rijmen 所设计。其基本信息如下

  • 输入:128 比特。
  • 输出:128 比特。
  • SPN 网络结构。

其迭代轮数与密钥长度有关系,如下

密钥长度(比特) 迭代轮数
128 10
192 12
256 14

基本流程

基本概念

在 AES 加解密过程中,每一块都是 128 比特,所以我们这里明确一些基本概念。

在 AES 中,块与 State 之间的转换过程如下

所以,可以看出,每一个 block 中的字节是按照列排列进入到状态数组的。

而对于明文来说,一般我们会选择使用其十六进制进行编码。

加解密过程

基本的流程,每一轮主要包括

  • 轮密钥加,AddRoundKey
  • 字节替换,SubBytes
  • 行移位,ShiftRows
  • 列混淆,MixColumns

上面的列混淆的矩阵乘法等号左边的列向量应该在右边。

这里再给一张其加解密的全图,其解密算法的正确性很显然。

我们这里重点关注一下以下。

字节替换

在字节替换的背后,其实是有对应的数学规则来定义对应的替换表的,如下

这里的运算均定义在 GF(28) 内。

列混淆

这里的运算也是定义在 GF(28)上,使用的模多项式为 x8+x4+x3+1。

密钥扩展

等价解密算法

简单分析一下,我们可以发现

  • 交换逆向行移位和逆向字节代替并不影响结果。
  • 交换轮密钥加和逆向列混淆并不影响结果,关键在于
  • 首先可以把异或看成域上的多项式加法
  • 然后多项式中乘法对加法具有分配率。

攻击方法

  • 积分攻击

2018 国赛 Crackmec

通过简单分析这个算法,我们可以发现这个算法是一个简化版的 AES,其基本操作为

  • 9 轮迭代
    • 行移位
    • 变种字节替换

如下

  memcpy(cipher, plain, 0x10uLL);
  for ( i = 0LL; i <= 8; ++i )
  {
    shift_row(cipher);
    for ( j = 0LL; j <= 3; ++j )
      *(_DWORD *)&cipher[4 * j] =
        box[((4 * j + 3 + 16 * i) << 8) + (unsigned __int8)cipher[4 * j + 3]] ^
        box[((4 * j + 2 + 16 * i) << 8) + (unsigned __int8)cipher[4 * j + 2]] ^
        box[((4 * j + 1 + 16 * i) << 8) + (unsigned __int8)cipher[4 * j + 1]] ^
        box[((4 * j + 16 * i) << 8) + (unsigned __int8)cipher[4 * j]];
  }
  result = shift_row(cipher);
  for ( k = 0LL; k <= 0xF; ++k )
  {
    result = subbytes[256 * k + (unsigned __int8)cipher[k]];
    cipher[k] = result;
  }
  return result;

根据程序流程,我们已知程序加密的结果,而 subbytes 和 shift_row 又是可逆的,所以我们可以获取最后一轮加密后的结果。此时,我们还知道 box 对应的常数,我们只是不知道上一轮中 cipher[4*j] 对应的值,一共 32 位,如果我们直接爆破的话,显然不可取,因为每一轮都需要这么爆破,时间不可接受。那么有没有其它办法呢?其实有的,我们可以考虑中间相遇攻击,即首先枚举所有的 cipher[4*j]cipher[4*j+1] 的字节组合,一共 256*256 种。在枚举剩下两个字节时,我们可以先计算出其与密文的异或值,然后去之前的组合中找,如果找到的话,我们就认为是正确的。这样复杂度瞬间降到 O(216)。

代码如下

encflag = [
    0x16, 0xEA, 0xCA, 0xCC, 0xDA, 0xC8, 0xDE, 0x1B, 0x16, 0x03, 0xF8, 0x84,
    0x69, 0x23, 0xB2, 0x25
]
subbytebox = eval(open('./subbytes').read())
box = eval(open('./box').read())
print subbytebox[-1], box[-1]


def inv_shift_row(now):
    tmp = now[13]
    now[13] = now[9]
    now[9] = now[5]
    now[5] = now[1]
    now[1] = tmp

    tmp = now[10]
    now[10] = now[2]
    now[2] = tmp
    tmp = now[14]
    now[14] = now[6]
    now[6] = tmp

    tmp = now[15]
    now[15] = now[3]
    now[3] = now[7]
    now[7] = now[11]
    now[11] = tmp

    return now


def byte2num(a):
    num = 0
    for i in range(3, -1, -1):
        num = num * 256
        num += a[i]
    return num


def getbytes(i, j, target):
    """
    box[((4 * j + 3 + 16 * i) << 8) + a2[4 * j + 3]]
    box[((4 * j + 2 + 16 * i) << 8 )+ a2[4 * j + 2]]
    box[((4 * j + 1 + 16 * i) << 8) + a2[4 * j + 1]]
    box[((4 * j + 16 * i) << 8) + a2[4 * j]];
    """
    box01 = dict()
    for c0 in range(256):
        for c1 in range(256):
            num0 = ((4 * j + 16 * i) << 8) + c0
            num1 = ((4 * j + 1 + 16 * i) << 8) + c1
            num = box[num0] ^ box[num1]
            box01[num] = (c0, c1)
    for c2 in range(256):
        for c3 in range(256):
            num2 = ((4 * j + 2 + 16 * i) << 8) + c2
            num3 = ((4 * j + 3 + 16 * i) << 8) + c3
            num = box[num2] ^ box[num3]
            calc = num ^ target
            if calc in box01:
                c0, c1 = box01[calc]
                return c0, c1, c2, c3
    print 'not found'
    print i, j, target, calc
    exit(0)


def solve():
    a2 = [0] * 16
    """
      for ( k = 0LL; k <= 0xF; ++k )
      {
        result = subbytesbox[256 * k + a2[k]];
        a2[k] = result;
      }
    """
    for i in range(15, -1, -1):
        tag = 0
        for j in range(256):
            if subbytebox[256 * i + j] == encflag[i]:
                # j = a2[k]
                tag += 1
                a2[i] = j
                if tag == 2:
                    print 'two number', i
                    exit(0)
    """
      result = shift_row(a2);
    """
    a2 = inv_shift_row(a2)
    """
      for ( i = 0LL; i <= 8; ++i )
      {
        shift_row(a2);
        for ( j = 0LL; j <= 3; ++j )
          *(_DWORD *)&a2[4 * j] = box[((4 * j + 3 + 16 * i) << 8) + a2[4 * j + 3]] ^ box[((4 * j + 2 + 16 * i) << 8)
                                                                                       + a2[4 * j + 2]] ^ box[((4 * j + 1 + 16 * i) << 8) + a2[4 * j + 1]] ^ box[((4 * j + 16 * i) << 8) + a2[4 * j]];
      }
    """
    for i in range(8, -1, -1):
        tmp = [0] * 16
        print 'round ', i
        for j in range(0, 4):
            num = byte2num(a2[4 * j:4 * j + 4])
            #print num, a2[4 * j:4 * j + 4]
            tmp[4 * j
               ], tmp[4 * j + 1], tmp[4 * j + 2], tmp[4 * j + 3] = getbytes(
                   i, j, num
               )
        a2 = inv_shift_row(tmp)
    print a2
    print ''.join(chr(c) for c in a2)


if __name__ == "__main__":
    solve()

运行结果

➜  cracemec git:(master) ✗ python exp.py
211 3549048324
round  8
round  7
round  6
round  5
round  4
round  3
round  2
round  1
round  0
[67, 73, 83, 67, 78, 98, 35, 97, 100, 102, 115, 64, 70, 122, 57, 51]
CISCNb#adfs@Fz93

Simon and Speck Block Ciphers

这是一组姐妹轻量级加密。

Simon Block Cipher

基本介绍

Simon 块加密算法由 NSA 2013 年 6 月公布,主要在硬件实现上进行了优化。

Simon Block Cipher 是平衡的 Feistel cipher 加密,一共有两块,若每块加密的大小为 n bits,那么明文的大小就是 2n bits。此外,一般来说,该加密中所使用的密钥长度是块长度的整数倍,比如 2n,4n 等。常见的 Simon 加密算法有

一般来说,Simon 算法称之为 Simon 2n/nm,n 为块大小,m 是块大小与密钥之间的倍数。比如说 Simon 48/96 就是指明文是 48 比特,密钥是 96 比特的加密算法。

此外,对于 Simon 块加密算法来说,每轮的加密过程一样,如下

当然,对于每一轮以及不同的 m 来说,密钥也会有所不同

其中, zj 是由 Linear Feedback Shift Register (LFSR) 生成的,虽然对于不同的 zj的逻辑不同,但是初始向量是固定的。

Constant
z0z0=11111010001001010110000111001101111101000100101011000011100110
z1z1=10001110111110010011000010110101000111011111001001100001011010
z2z2=10101111011100000011010010011000101000010001111110010110110011
z3z3=11011011101011000110010111100000010010001010011100110100001111
z4z4=11010001111001101011011000100000010111000011001010010011101111

2017 SECCON Simon and Speck Block Ciphers

题目描述如下

Simon and Speck Block Ciphers

https://eprint.iacr.org/2013/404.pdf Simon_96_64, ECB, key="SECCON{xxxx}", plain=0x6d564d37426e6e71, cipher=0xbb5d12ba422834b5

从名字中可以看出密钥是 96 比特(12 byte),明文是 64 比特(8 字节),而密钥已经给出了 8 个字节,只剩下四个字节未知。那我们可以使用暴力破解的方法。这里从 https://github.com/bozhu/NSA-ciphers/blob/master/simon.py 获取了一份 simon 加密算法。

具体如下

from pwn import *
from simon import SIMON

plain = 0x6d564d37426e6e71
cipher = 0xbb5d12ba422834b5


def compare(key):
    key = "SECCON{" + key + "}"
    key = key.encode('hex')
    key = int(key, 16)
    my_simon = SIMON(64, 96, key)
    test = my_simon.encrypt(plain)
    if test == cipher:
        return True
    else:
        return False


def solve():
    visible = string.uppercase + string.lowercase + string.digits + string.punctuation + " "
    key = pwnlib.util.iters.mbruteforce(compare, visible, 4, method="fixed")
    print key


if __name__ == "__main__":
    solve()

结果如下

➜  2017_seccon_simon_and_speck_block_ciphers git:(master) python exp.py
[+] MBruteforcing: Found key: "6Pz0"

分组模式

分组加密会将明文消息划分为固定大小的块,每块明文分别在密钥控制下加密为密文。当然并不是每个消息都是相应块大小的整数倍,所以我们可能需要进行填充。

填充方式

正如我们之前所说,在分组加密中,明文的长度往往并不满足要求,需要进行 padding,而如何 padding 目前也已经有了不少的规定。

常见的 填充规则 如下。需要注意的是,即使消息的长度是块大小的整数倍,仍然需要填充。

一般来说,如果在解密之后发现 Padding 不正确,则往往会抛出异常。我们也因此可以知道 Paddig 是否正确。

Pad with bytes all of the same value as the number of padding bytes (PKCS5 padding)

举例子如下

DES INPUT BLOCK  = f  o  r  _  _  _  _  _
(IN HEX)           66 6F 72 05 05 05 05 05
KEY              = 01 23 45 67 89 AB CD EF
DES OUTPUT BLOCK = FD 29 85 C9 E8 DF 41 40

Pad with 0x80 followed by zero bytes (OneAndZeroes Padding)

举例子如下

DES INPUT BLOCK  = f  o  r  _  _  _  _  _
(IN HEX)           66 6F 72 80 00 00 00 00
KEY              = 01 23 45 67 89 AB CD EF
DES OUTPUT BLOCK = BE 62 5D 9F F3 C6 C8 40

这里其实就是和 md5 和 sha1 的 padding 差不多。

Pad with zeroes except make the last byte equal to the number of padding bytes

举例子如下

DES INPUT BLOCK  = f  o  r  _  _  _  _  _
(IN HEX)           66 6f 72 00 00 00 00 05
KEY              = 01 23 45 67 89 AB CD EF
DES OUTPUT BLOCK = 91 19 2C 64 B5 5C 5D B8

Pad with zero (null) characters

举例子如下

DES INPUT BLOCK  = f  o  r  _  _  _  _  _
(IN HEX)           66 6f 72 00 00 00 00 00
KEY              = 01 23 45 67 89 AB CD EF
DES OUTPUT BLOCK = 9E 14 FB 96 C5 FE EB 75

Pad with spaces

举例子如下

DES INPUT BLOCK  = f  o  r  _  _  _  _  _
(IN HEX)           66 6f 72 20 20 20 20 20
KEY              = 01 23 45 67 89 AB CD EF
DES OUTPUT BLOCK = E3 FF EC E5 21 1F 35 25

2018 上海市大学生网络安全大赛 aessss

有时候可以针对一些使用不当的 Padding 进行攻击。这里以 2018 上海市大学生网络安全大赛的一道题目为例:

题目脚本如下:

import random
import sys
import string
from hashlib import sha256
import SocketServer
from Crypto.Cipher import AES
from secret import FLAG, IV, KEY


class Task(SocketServer.BaseRequestHandler):
    def proof_of_work(self):
        proof = ''.join(
            [random.choice(string.ascii_letters+string.digits) for _ in xrange(20)])
        # print proof
        digest = sha256(proof).hexdigest()
        self.request.send("sha256(XXXX+%s) == %s\n" % (proof[4:], digest))
        self.request.send('Give me XXXX:')
        x = self.request.recv(10)
        x = x.strip()
        if len(x) != 4 or sha256(x+proof[4:]).hexdigest() != digest:
            return False
        return True

    def pad(self, s):
        s += (256 - len(s)) * chr(256 - len(s))
        ret = ['\x00' for _ in range(256)]
        for index, pos in enumerate(self.s_box):
            ret[pos] = s[index]
        return ''.join(ret)

    def unpad(self, s):
        ret = ['\x00' for _ in range(256)]
        for index, pos in enumerate(self.invs_box):
            ret[pos] = s[index]
        return ''.join(ret[0:-ord(ret[-1])])

    s_box = [
        0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76,
        0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0,
        0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15,
        0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75,
        0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84,
        0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF,
        0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8,
        0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2,
        0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73,
        0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB,
        0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79,
        0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08,
        0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A,
        0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E,
        0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF,
        0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16
    ]

    invs_box = [
        0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB,
        0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB,
        0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E,
        0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25,
        0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92,
        0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84,
        0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06,
        0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B,
        0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73,
        0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E,
        0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B,
        0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4,
        0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F,
        0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF,
        0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61,
        0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D
    ]

    def encrypt(self, msg):
        cipher = AES.new(KEY, AES.MODE_CBC, IV)
        return cipher.encrypt(msg).encode('hex')

    def handle(self):
        if not self.proof_of_work():
            return
        self.request.settimeout(15)
        req = self.request
        flag_len = len(FLAG)
        assert(flag_len == 33)
        self.flag = self.pad(FLAG)
        assert(len(self.flag) == 256)

        while True:
            req.sendall(
                'Welcome to AES(WXH) encrypt system.\n1. get encrypted flag.\n2. pad flag.\n3.Do some encrypt.\nYour choice:')
            cmd = req.recv(2).strip()
            try:
                cmd = int(cmd)
            except ValueError:
                cmd = 0
            if cmd == 1:
                enc = self.encrypt(self.flag)
                req.sendall('Here is the encrypted flag: 0x%s\n' % enc)
            elif cmd == 2:
                req.sendall('Pad me something:')
                self.flag = self.unpad(self.flag)[
                    :flag_len] + req.recv(1024).strip()
                assert(len(self.flag) <= 256)
                self.flag = self.pad(self.flag)
                req.sendall('Done.\n')
            elif cmd == 3:
                req.sendall('What do you want to encrypt:')
                msg = self.pad(req.recv(1024).strip())
                assert(len(msg) <= 256)
                enc = self.encrypt(msg)
                req.sendall('Here is the encrypted message: 0x%s\n' % enc)
            else:
                req.sendall('Do not lose heart~ !% Once WXH AK IOI 2019 can Solved! WXH is the first in the tianxia!')
                req.close()
                return


class ThreadedServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    pass


if __name__ == "__main__":
    HOST, PORT = '0.0.0.0', 23333
    print 'Run in port:23333'
    server = ThreadedServer((HOST, PORT), Task)
    server.allow_reuse_address = True
    server.serve_forever()

分析

这个题目问题出在 padding 的时候,由于不足 256 位要进行 padding,padding 的字节也就是缺的字节数,但是如果明文够 256 字节,那么按照代码逻辑就不进行 padding:

def pad(self, s):
        s += (256 - len(s)) * chr(256 - len(s))
        ret = ['\x00' for _ in range(256)]
        for index, pos in enumerate(self.s_box):
            ret[pos] = s[index]
        return ''.join(ret)

最大的问题出在 unpad 上,unpad 没有进行检查,仅仅通过最后一个字节来判断填充的字节数。

 def unpad(self, s):
        ret = ['\x00' for _ in range(256)]
        for index, pos in enumerate(self.invs_box):
            ret[pos] = s[index]
        return ''.join(ret[0:-ord(ret[-1])])

我们可以通过篡改最后一个字节来控制去掉的 padding 字节数。

利用

  1. 选择 choice2,追加 256-33 =223字节,使当前 flag 不需要填充,追加的最后一个字节设置成 chr(256-32)
  2. 服务器对 flag 追加我们的信息,并进行 s 盒替换,结果赋给类中的 flag 变量。
  3. 我们再次选择 choice2,这里由于我们需要追加,服务器会将类中的 flag 变量取出进行逆 S 盒替换和 unpad,这样按照这个 unpad 算法会把后面 224 字节的全部当成 padding 去掉,明文剩下了真正 flag 的前 32 位。
  4. 我们此时输入一个字符 i, 那么此时加密的对象就是 flag[:32]+i
  5. 选择 choice1 对当前 flag 加密,控制 i 进行爆破,如果得到的密文和最初的 flag 加密的密文一样,就得到了 flag 的最后一个字节。
  6. 逐字节爆破,直至获取全部的 flag。

exp 如下:

# -*- coding: utf-8 -*-
from hashlib import sha256
import socket
import string
import itertools
HOST='106.75.13.64'
PORT=54321
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))
def brute_force(pad, shavalue):
    for str in itertools.product(string.ascii_letters + string.digits, repeat=4):
        str=''.join(str)
        if sha256(str + pad).hexdigest() == shavalue:
            print str
            return str
def choice1():
    sock.send("1\n")
    result=sock.recv(1024).strip()[30:]
    sock.recv(1024).strip()
    return result
def choice2(pad):
    sock.send("2\n")
    sock.recv(1024).strip()
    sock.send(pad+"\n")
    sock.recv(1024).strip()
    sock.recv(1024).strip()
def choice3(str):
    sock.send("3\n")
    sock.recv(1024).strip()
    sock.send(str+"\n")
    result=sock.recv(1024).strip()[33:]
    sock.recv(1024).strip()
    return result
content = sock.recv(1024).strip()
pad=content[12:12+16]
hash=content[33:33+64]
sock.recv(1024).strip()
sock.send(str(brute_force(pad,hash))+"\n")
print sock.recv(1024).strip()
flag_enc=choice1()
flag=""
for i in range(33):
    a = ''.join(['a' for _ in range(223)])
    a = a[:-1] + chr(224+i)
    for c in string.printable:
        print c+flag
        choice2(a)
        choice2(c+flag)
        if choice1() == flag_enc:
            flag=c+flag
            print "success:",flag
            break

flag{H4ve_fun_w1th_p4d_and_unp4d}

ECB

ECB 模式全称为电子密码本模式(Electronic codebook)。

加密

解密

优缺点

优点

  1. 实现简单。
  2. 不同明文分组的加密可以并行计算,速度很快。

缺点

  1. 同样的明文块会被加密成相同的密文块,不会隐藏明文分组的统计规律。正如下图所示

image-20180716215135907

为了解决统一明文产生相同密文的问题,提出了其它的加密模式。

典型应用

  1. 用于随机数的加密保护。
  2. 用于单分组明文的加密。

2016 ABCTF aes-mess-75

题目描述如下

We encrypted a flag with AES-ECB encryption using a secret key, and got the hash: e220eb994c8fc16388dbd60a969d4953f042fc0bce25dbef573cf522636a1ba3fafa1a7c21ff824a5824c5dc4a376e75 However, we lost our plaintext flag and also lost our key and we can't seem to decrypt the hash back :(. Luckily we encrypted a bunch of other flags with the same key. Can you recover the lost flag using this?

[HINT] There has to be some way to work backwards, right?

可以看出,这个加密是一个 ECB 加密,然后 AES 是 16 个字节一组,每个字节可以使用两个 16 进制字符表示,因此,我们每 32 个字符一组进行分组,然后去对应的 txt 文件中搜索即可。

对应 flag

e220eb994c8fc16388dbd60a969d4953 abctf{looks_like
f042fc0bce25dbef573cf522636a1ba3 _you_can_break_a
fafa1a7c21ff824a5824c5dc4a376e75 es}

最后一个显然在加密时进行了 padding。

CBC

CBC 全称为密码分组链接(Cipher-block chaining) 模式,这里

  • IV 不要求保密
  • IV 必须是不可预测的,而且要保证完整性。

加密

解密

优缺点

优点

  1. 密文块不仅和当前密文块相关,而且和前一个密文块或 IV 相关,隐藏了明文的统计特性。
  2. 具有有限的两步错误传播特性,即密文块中的一位变化只会影响当前密文块和下一密文块。
  3. 具有自同步特性,即第 k 块起密文正确,则第 k+1 块就能正常解密。

缺点

  1. 加密不能并行,解密可以并行。

应用

CBC 应用十分广泛

  • 常见的数据加密和 TLS 加密。
  • 完整性认证和身份认证。

攻击

字节反转攻击

原理

字节反转的原理十分简单,我们观察解密过程可以发现如下特性:

  • IV 向量影响第一个明文分组
  • 第 n 个密文分组可以影响第 n + 1 个明文分组

假设第nn 个密文分组为CnCn,解密后的第nn 个明文分组为为PnPn。

然后Pn+1=Cn xor f(Cn+1)Pn+1=Cn xor f(Cn+1)。

其中ff 函数为图中的Block Cipher DecryptionBlock Cipher Decryption。

对于某个信息已知的原文和密文,然后我们可以修改第nn 个密文块CnCn 为Cn xor Pn+1 xor ACn xor Pn+1 xor A。然后再对这条密文进行解密,那么解密后的第nn 个明文快将会变成AA。

例题
from flag import FLAG
from Crypto.Cipher import AES
from Crypto import Random
import base64

BLOCK_SIZE=16
IV = Random.new().read(BLOCK_SIZE)
passphrase = Random.new().read(BLOCK_SIZE)

pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]

prefix = "flag="+FLAG+"&userdata="
suffix = "&user=guest"
def menu():
    print "1. encrypt"
    print "2. decrypt"
    return raw_input("> ")

def encrypt():
    data = raw_input("your data: ")
    plain = prefix+data+suffix
    aes = AES.new(passphrase, AES.MODE_CBC, IV)
    print base64.b64encode(aes.encrypt(pad(plain)))


def decrypt():
    data = raw_input("input data: ")
    aes = AES.new(passphrase, AES.MODE_CBC, IV)
    plain = unpad(aes.decrypt(base64.b64decode(data)))
    print 'DEBUG ====> ' + plain
    if plain[-5:]=="admin":
        print plain
    else:
        print "you are not admin"

def main():
    for _ in range(10):
        cmd = menu()
        if cmd=="1":
            encrypt()
        elif cmd=="2":
            decrypt()
        else:
            exit()

if __name__=="__main__":
    main()

可见题目希望我们提供一个加密的字符串,如果这个字符串解密后最后的内容为 admin。程序将会输出明文。所以题目流程为先随便提供一个明文,然后将密文进行修改,使得解密后的字符串最后的内容为 admin, 我们可以枚举 flag 的长度来确定我们需要在什么位置进行修改。

以下是 exp.py

from pwn import *
import base64

pad = 16
data = 'a' * pad
for x in range(10, 100):
    r = remote('xxx.xxx.xxx.xxx', 10004)
    #r = process('./chall.sh')

    r.sendlineafter('> ', '1')
    r.sendlineafter('your data: ', data)
    cipher = list(base64.b64decode(r.recv()))
    #print 'cipher ===>', ''.join(cipher)

    BLOCK_SIZE = 16
    prefix = "flag=" + 'a' * x + "&userdata="
    suffix = "&user=guest"
    plain = prefix + data + suffix

    idx = (22 + x + pad) % BLOCK_SIZE + ((22 + x + pad) / BLOCK_SIZE - 1) * BLOCK_SIZE
    cipher[idx + 0] = chr(ord(cipher[idx + 0]) ^ ord('g') ^ ord('a'))
    cipher[idx + 1] = chr(ord(cipher[idx + 1]) ^ ord('u') ^ ord('d'))
    cipher[idx + 2] = chr(ord(cipher[idx + 2]) ^ ord('e') ^ ord('m'))
    cipher[idx + 3] = chr(ord(cipher[idx + 3]) ^ ord('s') ^ ord('i'))
    cipher[idx + 4] = chr(ord(cipher[idx + 4]) ^ ord('t') ^ ord('n'))

    r.sendlineafter('> ', '2')
    r.sendlineafter('input data: ', base64.b64encode(''.join(cipher)))

    msg = r.recvline()
    if 'you are not admin' not in msg:
        print msg
        break
    r.close()  

PCBC

PCBC 的全称为明文密码块链接(Plaintext cipher-block chaining)。也称为填充密码块链接(Propagating cipher-block chaining)。

加密

解密

特点

  • 解密过程难以并行化
  • 互换邻接的密文块不会对后面的密文块造成影响

CFB

CFB 全称为密文反馈模式(Cipher feedback)。

加密

解密

优缺点

优点

  • 适应于不同数据格式的要求
  • 有限错误传播
  • 自同步

缺点

  • 加密不能并行化,解密不能并行

应用场景

该模式适应于数据库加密,无线通信加密等对数据格式有特殊要求的加密环境。

OFB

OFB 全称为输出反馈模式(Output feedback),其反馈内容是分组加密后的内容而不是密文。

加密

解密

优缺点

优点

  1. 不具有错误传播特性。

缺点

  1. IV 无需保密,但是对每个消息必须选择不同的 IV。
  2. 不具有自同步能力。

适用场景

适用于一些明文冗余度比较大的场景,如图像加密和语音加密。

CTR

CTR 全称为计数器模式(Counter mode),该模式由 Diffe 和 Hellman 设计。

加密

解密

Padding Oracle Attack

介绍

Padding Oracle Attack 攻击一般需要满足以下几个条件

  • 加密算法
    • 采用 PKCS5 Padding 的加密算法。 当然,非对称加密中 OAEP 的填充方式也有可能会受到影响。
    • 分组模式为 CBC 模式。
  • 攻击者能力
    • 攻击者可以拦截上述加密算法加密的消息。
    • 攻击者可以和 padding oracle(即服务器) 进行交互:客户端向服务器端发送密文,服务器端会以某种返回信息告知客户端 padding 是否正常。

Padding Oracle Attack 攻击可以达到的效果如下

  • 在不清楚 key 和 IV 的前提下解密任意给定的密文。

原理

Padding Oracle Attack 攻击的基本原理如下

  • 对于很长的消息一块一块解密。
  • 对于每一块消息,先解密消息的最后一个字节,然后解密倒数第二个字节,依次类推。

这里我们回顾一下 CBC 的

  • 加密

$$
C_i=E_K(P_i \oplus C_{i-1})\
C_0=IV
$$

  • 解密

$$
P_{i}=D_{K}(C_{i})\oplus C_{i-1}\ C_{0}=IV
$$

我们主要关注于解密,这里我们并不知道 IV 和 key。这里我们假设密文块的长度为 n 个字节。

假设我们截获了密文 Y,以获取密文 Y 的最后一个字节为例子进行分析。为了获取 Y 的内容,我们首先需要伪造一块密文 F 以便于可以修改 Y 对应明文的最后一个字节。这是因为若我们构造密文 F|Y ,那么解密 Y 时具体为 $P=D_K(Y)\oplus F$ ,所以修改密文 F 的最后一个字节 $F_{n}$ 可以修改 Y 对应的明文的最后一个字节。下面给出获取 P 最后一个字节的过程

  1. i=0,设置 F 的每个字节为随机字节
  2. 设置 $F_n=i \oplus 0x01$
  3. 将 F|Y 发送给服务器,如果 P 的最后一个字节是 i 的话,那么最后的 padding 就是 0x01,不会出现错误。否则,只有 P 的最后 $P_n \oplus i \oplus 0x01$ 字节都是 $P_n \oplus i \oplus 0x01$ 才不会报错。而且,需要注意的是 padding 的字节只能是 0 到 n。 因此,若想要使得在 F 随机地情况下,并且满足padding 字节大小的约束情况下还不报错概率很小。所以在服务器端不报错的情况下,我们可以认为我们确实获取了正确的字节。
  4. 在出现错误的情况下,i=i+1,跳转到2。

当获取了 P 的最后一个字节后,我们可以继续获取 P 的倒数第二个字节,此时需要设置 $F_n=P_n\oplus 0x02$ ,同时设置 $F_{n-1}=i \oplus 0x02$ 去枚举 i。

所以,综上所示,Padding Oracle Attack 其实在一定程度上是一种具有很大概率成功的攻击方法。

然而,需要注意的是,往往遇到的一些现实问题并不是标准的 Padding Oracle Attack 模式,我们往往需要进行一些变形。

2017 HITCON Secret Server

分析

程序中采用的加密是 AES CBC,其中采用的 padding 与 PKCS5 类似

def pad(msg):
    pad_length = 16-len(msg)%16
    return msg+chr(pad_length)*pad_length

def unpad(msg):
    return msg[:-ord(msg[-1])]

但是,在每次 unpad 时并没有进行检测,而是直接进行 unpad。

其中,需要注意的是,每次和用户交互的函数是

  • send_msg ,接受用户的明文,使用固定的 2jpmLoSsOlQrqyqE 作为 IV,进行加密,并将加密结果输出。
  • recv_msg ,接受用户的 IV 和密文,对密文进行解密,并返回。根据返回的结果会有不同的操作
            msg = recv_msg().strip()
            if msg.startswith('exit-here'):
                exit(0)
            elif msg.startswith('get-flag'):
                send_msg(flag)
            elif msg.startswith('get-md5'):
                send_msg(MD5.new(msg[7:]).digest())
            elif msg.startswith('get-time'):
                send_msg(str(time.time()))
            elif msg.startswith('get-sha1'):
                send_msg(SHA.new(msg[8:]).digest())
            elif msg.startswith('get-sha256'):
                send_msg(SHA256.new(msg[10:]).digest())
            elif msg.startswith('get-hmac'):
                send_msg(HMAC.new(msg[8:]).digest())
            else:
                send_msg('command not found')

主要漏洞

这里我们再简单总结一下我们已有的部分

  • 加密
    • 加密时的 IV 是固定的而且已知。
    • Welcome!! 加密后的结果。
  • 解密
    • 我们可以控制 IV。

首先,既然我们知道 Welcome!! 加密后的结果,还可以控制 recv_msg 中的 IV,那么根据解密过程

$$
P_{i}=D_{K}(C_{i})\oplus C_{i-1}\ C_{0}=IV
$$

如果我们将 Welcome!! 加密后的结果输入给 recv_msg,那么直接解密后的结果便是 (Welcome!!+'\x07'*7) xor iv,如果我们恰当的控制解密过程中传递的 iv,那么我们就可以控制解密后的结果。也就是说我们可以执行上述所说的任意命令。从而,我们也就可以知道 flag 解密后的结果。

其次,在上面的基础之上,如果我们在任何密文 C 后面添加自定义的 IV 和 Welcome 加密后的结果,作为输入传递给 recv_msg,那么我们便可以控制解密之后的消息的最后一个字节,那么由于 unpad 操作,我们便可以控制解密后的消息的长度减小 0 到 255

利用思路

基本利用思路如下

  1. 绕过 proof of work
  2. 根据执行任意命令的方式获取加密后的 flag。
  3. 由于 flag 的开头是 hitcon{,一共有7个字节,所以我们任然可以通过控制 iv 来使得解密后的前 7 个字节为指定字节。这使得我们可以对于解密后的消息执行 get-md5 命令。而根据 unpad 操作,我们可以控制解密后的消息恰好在消息的第几个字节处。所以我们可以开始时将控制解密后的消息为 hitcon{x,即只保留hitcon{ 后的一个字节。这样便可以获得带一个字节哈希后的加密结果。类似地,我们也可以获得带制定个字节哈希后的加密结果。
  4. 这样的话,我们可以在本地逐字节爆破,计算对应 md5,然后再次利用任意命令执行的方式,控制解密后的明文为任意指定命令,如果控制不成功,那说明该字节不对,需要再次爆破;如果正确,那么就可以直接执行对应的命令。

具体代码如下

#coding=utf-8
from pwn import *
import base64, time, random, string
from Crypto.Cipher import AES
from Crypto.Hash import SHA256, MD5
#context.log_level = 'debug'
if args['REMOTE']:
    p = remote('52.193.157.19', 9999)
else:
    p = remote('127.0.0.1', 7777)


def strxor(str1, str2):
    return ''.join([chr(ord(c1) ^ ord(c2)) for c1, c2 in zip(str1, str2)])


def pad(msg):
    pad_length = 16 - len(msg) % 16
    return msg + chr(pad_length) * pad_length


def unpad(msg):
    return msg[:-ord(msg[-1])]  # 去掉pad


def flipplain(oldplain, newplain, iv):
    """flip oldplain to new plain, return proper iv"""
    return strxor(strxor(oldplain, newplain), iv)


def bypassproof():
    p.recvuntil('SHA256(XXXX+')
    lastdata = p.recvuntil(')', drop=True)
    p.recvuntil(' == ')
    digest = p.recvuntil('\nGive me XXXX:', drop=True)

    def proof(s):
        return SHA256.new(s + lastdata).hexdigest() == digest

    data = pwnlib.util.iters.mbruteforce(
        proof, string.ascii_letters + string.digits, 4, method='fixed')
    p.sendline(data)
    p.recvuntil('Done!\n')


iv_encrypt = '2jpmLoSsOlQrqyqE'


def getmd5enc(i, cipher_flag, cipher_welcome):
    """return encrypt( md5( flag[7:7+i] ) )"""
    ## keep iv[7:] do not change, so decrypt won't change
    new_iv = flipplain("hitcon{".ljust(16, '\x00'), "get-md5".ljust(
        16, '\x00'), iv_encrypt)
    payload = new_iv + cipher_flag
    ## calculate the proper last byte number
    last_byte_iv = flipplain(
        pad("Welcome!!"),
        "a" * 15 + chr(len(cipher_flag) + 16 + 16 - (7 + i + 1)), iv_encrypt)
    payload += last_byte_iv + cipher_welcome
    p.sendline(base64.b64encode(payload))
    return p.recvuntil("\n", drop=True)


def main():
    bypassproof()

    # result of encrypted Welcome!!
    cipher = p.recvuntil('\n', drop=True)
    cipher_welcome = base64.b64decode(cipher)[16:]
    log.info("cipher welcome is : " + cipher_welcome)

    # execute get-flag
    get_flag_iv = flipplain(pad("Welcome!!"), pad("get-flag"), iv_encrypt)
    payload = base64.b64encode(get_flag_iv + cipher_welcome)
    p.sendline(payload)
    cipher = p.recvuntil('\n', drop=True)
    cipher_flag = base64.b64decode(cipher)[16:]
    flaglen = len(cipher_flag)
    log.info("cipher flag is : " + cipher_flag)

    # get command not found cipher
    p.sendline(base64.b64encode(iv_encrypt + cipher_welcome))
    cipher_notfound = p.recvuntil('\n', drop=True)

    flag = ""
    # brute force for every byte of flag
    for i in range(flaglen - 7):
        md5_indexi = getmd5enc(i, cipher_flag, cipher_welcome)
        md5_indexi = base64.b64decode(md5_indexi)[16:]
        log.info("get encrypt(md5(flag[7:7+i])): " + md5_indexi)
        for guess in range(256):
            # locally compute md5 hash
            guess_md5 = MD5.new(flag + chr(guess)).digest()
            # try to null out the md5 plaintext and execute a command
            payload = flipplain(guess_md5, 'get-time'.ljust(16, '\x01'),
                                iv_encrypt)
            payload += md5_indexi
            p.sendline(base64.b64encode(payload))
            res = p.recvuntil("\n", drop=True)
            # if we receive the block for 'command not found', the hash was wrong
            if res == cipher_notfound:
                print 'Guess {} is wrong.'.format(guess)
            # otherwise we correctly guessed the hash and the command was executed
            else:
                print 'Found!'
                flag += chr(guess)
                print 'Flag so far:', flag
                break


if __name__ == "__main__":
    main()

最后结果如下

Flag so far: Paddin9_15_ve3y_h4rd__!!}\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10

2017 HITCON Secret Server Revenge

描述

The password of zip is the flag of "Secret Server"

分析

这个程序时接着上面的程序继续搞的,不过这次进行的简单的修改

  • 加密算法的 iv 未知,不过可以根据 Welcome 加密后的消息推算出来。
  • 程序多了一个 56 字节的 token。
  • 程序最多能进行 340 操作,因此上述的爆破自然不可行

程序的大概流程如下

  1. 经过 proof of work
  2. 发送 “Welcome!!” 加密后的消息
  3. 在 340 次操作中,需要猜中 token 的值,然后会自动将 flag 输出。

漏洞

当然,在上个题目中存在的漏洞,在这个题目中仍然存在,即

  1. 任意执行给定命令
  2. 长度截断

利用思路

由于 340 的次数限制,虽然我们仍然可以获得 md5(token[:i]) 加密后的值(这里需要注意的是这部分加密后恰好是 32 个字节,前 16 个字节是 md5 后加密的值,后面的 16 个字节完全是填充的加密后的字节。这里md5(token[:i]) 特指前16个字节。)。但是,我们不能再次为了获得一个字符去爆破 256 次了。

既然不能够爆破,那么我们有没有可能一次获取一个字节的大小呢?这里,我们再来梳理一下该程序可能可以泄漏的信息

  1. 某些消息的 md5 值加密后的值,这里我们可以获取 md5(token[:i]) 加密后的值。
  2. unpad 每次会对解密后的消息进行 unpad,这个字节是根据解密后的消息的最后一个字节来决定的。如果我们可以计算出这个字节的大小,那么我们就可能可以知道一个字节的值。

这里我们深入分析一下 unpad 的信息泄漏。如果我们将加密 IV 和 encrypt(md5(token[:i])) 放在某个密文 C 的后面,构成 C|IV|encrypt(md5(token[:i])),那么解密出来的消息的最后一个明文块就是 md5(token[:i])。进而,在 unpad 的时候就是利用 md5(token[:i]) 的最后一个字节( 0-255)进行 unpad,之后对 unpad 后的字符串执行指定的命令(比如md5)。那么,如果我们事先构造一些消息哈希后加密的样本,然后将上述执行后的结果与样本比较,如果相同,那么我们基本可以确定 md5(token[:i])最后一个字节。然而,如果 md5(token[:i]) 的最后一个字节小于16,那么在 unpad 时就会利用一些 md5 中的值,而这部分值,由于对于不同长度的 token[:i] 几乎都不会相同。所以可能需要特殊处理。

我们已经知道了这个问题的关键,即生成与 unpad 字节大小对应的加密结果样本,以便于查表。

具体利用思路如下

  1. 绕过 proof of work。
  2. 获取 token 加密后的结果 token_enc ,这里会在 token 前面添加 7 个字节 "token: " 。 因此加密后的长度为 64。
  3. 依次获取 encrypt(md5(token[:i])) 的结果,一共是 57 个,包括最后一个 token 的 padding。
  4. 构造与 unpad 大小对应的样本。这里我们构造密文 token_enc|padding|IV_indexi|welcome_enc。由于 IV_indexi 是为了修改最后一个明文块的最后一个字节,所以该字节处于变化之中。我们若想获取一些固定字节的哈希值,这部分自然不能添加。因此这里产生样本时 unpad 的大小范围为 17 ~ 255。如果最后测试时 md5(token[:i]) 的最后一个字节小于17的话,基本就会出现一些未知的样本。很自然的一个想法是我们直接获取 255-17+1个这么多个样本,然而,如果这样做的话,根据上面 340 的次数(255-17+1+57+56>340)限制,我们显然不能获取到 token 的所有字节。所以这里我们需要想办法复用一些内容,这里我们选择复用 encrypt(md5(token[:i])) 的结果。那么我们在补充 padding 时需要确保一方面次数够用,另一方面可以复用之前的结果。这里我们设置 unpad 的循环为 17 到 208,并使得 unpad 大于 208 时恰好 unpad 到我们可以复用的地方。这里需要注意的是,当 md5(token[:i]) 的最后一个字节为 0 时,会将所有解密后的明文 unpad 掉,因此会出现 command not found 的密文。
  5. 再次构造密文 token_enc|padding|IV|encrypt(md5(token[:i])) ,那么,解密时即使用 md5(token[:i]) 的最后一个字节进行 unpad。如果这个字节不小于17或者为0,则可以处理。如果这个字节小于17,那么显然,最后返回给用户的 md5 的结果并不在样本范围内,那么我们修改其最后一个字节的最高比特位,使其 unpad 后可以落在样本范围内。这样,我们就可以猜出 md5(token[:i]) 的最后一个字节。
  6. 在猜出 md5(token[:i]) 的最后一个字节后,我们可以在本地暴力破解 256 次,找出所有哈希值末尾为 md5(token[:i]) 的最后一个字节的字符。
  7. 但是,在第六步中,对于一个 md5(token[:i]) 可能会找出多个备选字符,因为我们只需要使得其末尾字节是给定字节即可。
  8. 那么,问题来了,如何删除一些多余的备选字符串呢?这里我就选择了一个小 trick,即在逐字节枚举时,同时枚举出 token 的 padding。由于 padding 是 0x01 是固定的,所以我们只需要过滤出所有结尾不是 0x01 的token 即可。

这里,在测试时,将代码中 sleep 注释掉了。以便于加快交互速度。利用代码如下

from pwn import *
import base64, time, random, string
from Crypto.Cipher import AES
from Crypto.Hash import SHA256, MD5
#context.log_level = 'debug'

p = remote('127.0.0.1', 7777)


def strxor(str1, str2):
    return ''.join([chr(ord(c1) ^ ord(c2)) for c1, c2 in zip(str1, str2)])


def pad(msg):
    pad_length = 16 - len(msg) % 16
    return msg + chr(pad_length) * pad_length


def unpad(msg):
    return msg[:-ord(msg[-1])]  # remove pad


def flipplain(oldplain, newplain, iv):
    """flip oldplain to new plain, return proper iv"""
    return strxor(strxor(oldplain, newplain), iv)


def bypassproof():
    p.recvuntil('SHA256(XXXX+')
    lastdata = p.recvuntil(')', drop=True)
    p.recvuntil(' == ')
    digest = p.recvuntil('\nGive me XXXX:', drop=True)

    def proof(s):
        return SHA256.new(s + lastdata).hexdigest() == digest

    data = pwnlib.util.iters.mbruteforce(
        proof, string.ascii_letters + string.digits, 4, method='fixed')
    p.sendline(data)


def sendmsg(iv, cipher):
    payload = iv + cipher
    payload = base64.b64encode(payload)
    p.sendline(payload)


def recvmsg():
    data = p.recvuntil("\n", drop=True)
    data = base64.b64decode(data)
    return data[:16], data[16:]


def getmd5enc(i, cipher_token, cipher_welcome, iv):
    """return encrypt( md5( token[:i+1] ) )"""
    ## keep iv[7:] do not change, so decrypt msg[7:] won't change
    get_md5_iv = flipplain("token: ".ljust(16, '\x00'), "get-md5".ljust(
        16, '\x00'), iv)
    payload = cipher_token
    ## calculate the proper last byte number
    last_byte_iv = flipplain(
        pad("Welcome!!"),
        "a" * 15 + chr(len(cipher_token) + 16 + 16 - (7 + i + 1)), iv)
    payload += last_byte_iv + cipher_welcome
    sendmsg(get_md5_iv, payload)
    return recvmsg()


def get_md5_token_indexi(iv_encrypt, cipher_welcome, cipher_token):
    md5_token_idxi = []
    for i in range(len(cipher_token) - 7):
        log.info("idx i: {}".format(i))
        _, md5_indexi = getmd5enc(i, cipher_token, cipher_welcome, iv_encrypt)
        assert (len(md5_indexi) == 32)
        # remove the last 16 byte for padding
        md5_token_idxi.append(md5_indexi[:16])
    return md5_token_idxi


def doin(unpadcipher, md5map, candidates, flag):
    if unpadcipher in md5map:
        lastbyte = md5map[unpadcipher]
    else:
        lastbyte = 0
    if flag == 0:
        lastbyte ^= 0x80
    newcandidates = []
    for x in candidates:
        for c in range(256):
            if MD5.new(x + chr(c)).digest()[-1] == chr(lastbyte):
                newcandidates.append(x + chr(c))
    candidates = newcandidates
    print candidates
    return candidates


def main():
    bypassproof()

    # result of encrypted Welcome!!
    iv_encrypt, cipher_welcome = recvmsg()
    log.info("cipher welcome is : " + cipher_welcome)

    # execute get-token
    get_token_iv = flipplain(pad("Welcome!!"), pad("get-token"), iv_encrypt)
    sendmsg(get_token_iv, cipher_welcome)
    _, cipher_token = recvmsg()
    token_len = len(cipher_token)
    log.info("cipher token is : " + cipher_token)

    # get command not found cipher
    sendmsg(iv_encrypt, cipher_welcome)
    _, cipher_notfound = recvmsg()

    # get encrypted(token[:i+1]),57 times
    md5_token_idx_list = get_md5_token_indexi(iv_encrypt, cipher_welcome,
                                              cipher_token)
    # get md5map for each unpadsize, 209-17 times
    # when upadsize>208, it will unpad ciphertoken
    # then we can reuse
    md5map = dict()
    for unpadsize in range(17, 209):
        log.info("get unpad size {} cipher".format(unpadsize))
        get_md5_iv = flipplain("token: ".ljust(16, '\x00'), "get-md5".ljust(
            16, '\x00'), iv_encrypt)
        ## padding 16*11 bytes
        padding = 16 * 11 * "a"
        ## calculate the proper last byte number, only change the last byte
        ## set last_byte_iv = iv_encrypted[:15] | proper byte
        last_byte_iv = flipplain(
            pad("Welcome!!"),
            pad("Welcome!!")[:15] + chr(unpadsize), iv_encrypt)
        cipher = cipher_token + padding + last_byte_iv + cipher_welcome
        sendmsg(get_md5_iv, cipher)
        _, unpadcipher = recvmsg()
        md5map[unpadcipher] = unpadsize

    # reuse encrypted(token[:i+1])
    for i in range(209, 256):
        target = md5_token_idx_list[56 - (i - 209)]
        md5map[target] = i

    candidates = [""]
    # get the byte token[i], only 56 byte
    for i in range(token_len - 7):
        log.info("get token[{}]".format(i))
        get_md5_iv = flipplain("token: ".ljust(16, '\x00'), "get-md5".ljust(
            16, '\x00'), iv_encrypt)
        ## padding 16*11 bytes
        padding = 16 * 11 * "a"
        cipher = cipher_token + padding + iv_encrypt + md5_token_idx_list[i]
        sendmsg(get_md5_iv, cipher)
        _, unpadcipher = recvmsg()
        # already in or md5[token[:i]][-1]='\x00'
        if unpadcipher in md5map or unpadcipher == cipher_notfound:
            candidates = doin(unpadcipher, md5map, candidates, 1)
        else:
            log.info("unpad size 1-16")
            # flip most significant bit of last byte to move it in a good range
            cipher = cipher[:-17] + strxor(cipher[-17], '\x80') + cipher[-16:]
            sendmsg(get_md5_iv, cipher)
            _, unpadcipher = recvmsg()
            if unpadcipher in md5map or unpadcipher == cipher_notfound:
                candidates = doin(unpadcipher, md5map, candidates, 0)
            else:
                log.info('oh my god,,,, it must be in...')
                exit()
    print len(candidates)
    # padding 0x01
    candidates = filter(lambda x: x[-1] == chr(0x01), candidates)
    # only 56 bytes
    candidates = [x[:-1] for x in candidates]
    print len(candidates)
    assert (len(candidates[0]) == 56)

    # check-token
    check_token_iv = flipplain(
        pad("Welcome!!"), pad("check-token"), iv_encrypt)
    sendmsg(check_token_iv, cipher_welcome)
    p.recvuntil("Give me the token!\n")
    p.sendline(base64.b64encode(candidates[0]))
    print p.recv()

    p.interactive()


if __name__ == "__main__":
    main()

效果如下

...
79
1
hitcon{uNp@d_M3th0D_i5_am4Z1n9!}

Teaser Dragon CTF 2018 AES-128-TSB

这个题目还是蛮有意思的,题目描述如下

Haven't you ever thought that GCM mode is overcomplicated and there must be a simpler way to achieve Authenticated Encryption? Here it is!

Server: aes-128-tsb.hackable.software 1337

server.py

附件以及最后的 exp 自行到 ctf-challenge 仓库下寻找。

题目的基本流程为

  • 不断接收 a 和 b 两个字符串,其中 a 为明文,b 为密文,注意
    • b 在解密后需要满足尾部恰好等于 iv。
  • 如果 a 和 b 相等,那么根据
    • a 为 gimme_flag ,输出加密后的 flag。
    • 否则,输出一串随机加密的字符串。
  • 否则输出一串明文的字符串。

此外,我们还可以发现题目中的 unpad 存在问题,可以截断指定长度。

def unpad(msg):
    if not msg:
        return ''
    return msg[:-ord(msg[-1])]

一开始,很直接的思路是 a 和 b 的长度都输入 0 ,那么可以直接绕过 a==b 检查,获取一串随机密文加密的字符串。然而似乎并没有什么作用,我们来分析一下加密的流程

def tsb_encrypt(aes, msg):
    msg = pad(msg)
    iv = get_random_bytes(16)
    prev_pt = iv
    prev_ct = iv
    ct = ''
    for block in split_by(msg, 16) + [iv]:
        ct_block = xor(block, prev_pt)
        ct_block = aes.encrypt(ct_block)
        ct_block = xor(ct_block, prev_ct)
        ct += ct_block
        prev_pt = block
        prev_ct = ct_block
    return iv + ct

不妨假设 $P_0=iv,C_0=iv$,则

$C_i=C_{i-1}\oplus E(P_{i-1} \oplus P_i)$

那么,假设消息长度为 16,与我们想要得到的gimme_flag padding 后长度类似,则

$C_1=IV\oplus E( IV \oplus P_1)$

$C_2=C_1 \oplus E(P_1 \oplus IV)$

可以很容易的发现 $C_2=IV$。

反过来想,如果我们向服务器发送 iv+c+iv,那么总能绕过 tsb_decrypt 的 mac 检查

def tsb_decrypt(aes, msg):
    iv, msg = msg[:16], msg[16:]
    prev_pt = iv
    prev_ct = iv
    pt = ''
    for block in split_by(msg, 16):
        pt_block = xor(block, prev_ct)
        pt_block = aes.decrypt(pt_block)
        pt_block = xor(pt_block, prev_pt)
        pt += pt_block
        prev_pt = pt_block
        prev_ct = block
    pt, mac = pt[:-16], pt[-16:]
    if mac != iv:
        raise CryptoError()
    return unpad(pt)

那么此时,服务器解密后的消息则是

$unpad(IV \oplus D(C_1 \oplus IV))$

获取明文最后一个字节

我们可以考虑控制 D 解密的消息为常数值,比如全零,即C1=IV,那么我们就可以从 0 到 255 枚举 IV 的最后一个字节,得到 $IV \oplus D(C_1 \oplus IV)$ 的最后一个字节也是 0255。而只有是 115 的时候,unpad 操作过后,消息长度不为 0。因此,我们可以在枚举时统计究竟哪些数字导致了长度不为零,并标记为 1,其余标记为 0。

def getlast_byte(iv, block):
    iv_pre = iv[:15]
    iv_last = ord(iv[-1])
    tmp = []
    print('get last byte')
    for i in range(256):
        send_data('')
        iv = iv_pre + chr(i)
        tmpblock = block[:15] + chr(i ^ ord(block[-1]) ^ iv_last)
        payload = iv + tmpblock + iv
        send_data(payload)
        length, data = recv_data()
        if 'Looks' in data:
            tmp.append(1)
        else:
            tmp.append(0)
    last_bytes = []
    for i in range(256):
        if tmp == xor_byte_map[i][0]:
            last_bytes.append(xor_byte_map[i][1])
    print('possible last byte is ' + str(last_bytes))
    return last_bytes

此外,我们可以在最初的时候打表获取最后一个字节所有的可能情况,记录在 xor_byte_map 中。

"""
every item is a pair [a,b]
a is the xor list
b is the idx which is zero when xored
"""
xor_byte_map = []
for i in range(256):
    a = []
    b = 0
    for j in range(256):
        tmp = i ^ j
        if tmp > 0 and tmp <= 15:
            a.append(1)
        else:
            a.append(0)
        if tmp == 0:
            b = j
    xor_byte_map.append([a, b])

通过与这个表进行对比,我们就可以知道最后一个字节可能的情况。

解密任意加密块

在获取了明文最后一个字节后,我们就可以利用 unpad 的漏洞,从长度 1 枚举到长度 15 来获得对应的明文内容。

def dec_block(iv, block):
    last_bytes = getlast_byte(iv, block)

    iv_pre = iv[:15]
    iv_last = ord(iv[-1])
    print('try to get plain')
    plain0 = ''
    for last_byte in last_bytes:
        plain0 = ''
        for i in range(15):
            print 'idx:', i
            tag = False
            for j in range(256):
                send_data(plain0 + chr(j))
                pad_size = 15 - i
                iv = iv_pre + chr(pad_size ^ last_byte)
                tmpblock = block[:15] + chr(
                    pad_size ^ last_byte ^ ord(block[-1]) ^ iv_last
                )
                payload = iv + tmpblock + iv
                send_data(payload)
                length, data = recv_data()
                if 'Looks' not in data:
                    # success
                    plain0 += chr(j)
                    tag = True
                    break
            if not tag:
                break
        # means the last byte is ok
        if plain0 != '':
            break
    plain0 += chr(iv_last ^ last_byte)
    return plain0

解密出指定明文

这一点比较简单,我们希望利用这一点来获取 gimme_flag 的密文

    print('get the cipher of flag')
    gemmi_iv1 = xor(pad('gimme_flag'), plain0)
    gemmi_c1 = xor(gemmi_iv1, cipher0)
    payload = gemmi_iv1 + gemmi_c1 + gemmi_iv1
    send_data('gimme_flag')
    send_data(payload)
    flag_len, flag_cipher = recv_data()

其中 plain0 和 cipher0 是我们获取的 AES 加密的明密文对,不包括之前和之后的两个异或。

解密 flag

这一点,其实就是利用解密任意加密块的功能实现的,如下

    print('the flag cipher is ' + flag_cipher.encode('hex'))
    flag_cipher = split_by(flag_cipher, 16)

    print('decrypt the blocks one by one')
    plain = ''
    for i in range(len(flag_cipher) - 1):
        print('block: ' + str(i))
        if i == 0:
            plain += dec_block(flag_cipher[i], flag_cipher[i + 1])
        else:
            iv = plain[-16:]
            cipher = xor(xor(iv, flag_cipher[i + 1]), flag_cipher[i])
            plain += dec_block(iv, cipher)
            pass
        print('now plain: ' + plain)
    print plain

可以思考一下为什么第二块之后的密文操作会有所不同。

完整的代码参考 ctf-challenge 仓库。

参考资料

非对称加密

在非对称密码中,加密者与解密者所使用的密钥并不一样,典型的有 RSA 加密,背包加密,椭圆曲线加密。

RSA 介绍

RSA 加密算法是一种非对称加密算法。在公开密钥加密和电子商业中 RSA 被广泛使用。RSA 是 1977 年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。RSA 就是他们三人姓氏开头字母拼在一起组成的。

RSA 算法的可靠性由极大整数因数分解的难度决定。换言之,对一极大整数做因数分解愈困难,RSA 算法愈可靠。假如有人找到一种快速因数分解的算法的话,那么用 RSA 加密的信息的可靠性就肯定会极度下降。但找到这样的算法的可能性是非常小的。如今,只有短的 RSA 密钥才可能被强力方式解破。到 2017 年为止,还没有任何可靠的攻击 RSA 算法的方式。

基本原理

公钥与私钥的产生

  1. 随机选择两个不同大质数 $p$ 和 $q$,计算 $N = p \times q$
  2. 根据欧拉函数,求得 $\varphi (N)=\varphi (p)\varphi (q)=(p-1)(q-1)$
  3. 选择一个小于 $\varphi (N)$ 的整数 $e$,使 $e$ 和 $\varphi (N)$ 互质。并求得 $e$ 关于 $\varphi (N)$ 的模反元素,命名为 $d$,有 $ed\equiv 1 \pmod {\varphi (N)}$
  4. 将 $p$ 和 $q$ 的记录销毁

此时,$(N,e)$ 是公钥,$(N,d)$ 是私钥。

消息加密

首先需要将消息 以一个双方约定好的格式转化为一个小于 $N$,且与 $N$ 互质的整数 $m$。如果消息太长,可以将消息分为几段,这也就是我们所说的块加密,后对于每一部分利用如下公式加密:

$$
m^{e}\equiv c\pmod N
$$

消息解密

利用密钥 $d$ 进行解密。

$$
c^{d}\equiv m\pmod N
$$

正确性证明

即我们要证$m^{ed} \equiv m \bmod N$,已知$ed \equiv 1 \bmod \phi(N)$,那么 $ed=k\phi(N)+1$,即需要证明

$$
m^{k\phi(N)+1} \equiv m \bmod N
$$

这里我们分两种情况证明

第一种情况 $gcd(m,N)=1$,那么 $m^{\phi(N)} \equiv 1 \bmod N$,因此原式成立。

第二种情况 $gcd(m,N)\neq 1$,那么 $m$ 必然是 $p$ 或者 $q$ 的倍数,并且 $n=m$ 小于 $N$。我们假设

$$
m=xp
$$

那么 $x$ 必然小于 $q$,又由于 $q$ 是素数。那么

$$
m^{\phi(q)} \equiv 1 \bmod q
$$

进而

$$
m^{k\phi(N)}=m^{k(p-1)(q-1)}=(m^{\phi(q)})^{k(p-1)} \equiv 1 \bmod q
$$

那么

$$
m^{k\phi(N)+1}=m+uqm
$$

进而

$$
m^{k\phi(N)+1}=m+uqxp=m+uxN
$$

所以原式成立。

基本工具

RSAtool

  • 安装

    git clone https://github.com/ius/rsatool.git
    cd rsatool
    python rsatool.py -h
  • 生成私钥

    python rsatool.py -f PEM -o private.pem -p 1234567 -q 7654321

RSA Converter

  • 根据给定密钥对,生成 pem 文件
  • 根据 $n$,$e$,$d$ 得出 $p$,$q$

openssl

  • 查看公钥文件

    openssl rsa -pubin -in pubkey.pem -text -modulus
  • 解密

    rsautl -decrypt -inkey private.pem -in flag.enc -out flag

更加具体的细节请参考 openssl --help

分解整数工具

python 库

primefac

整数分解库,包含了很多整数分解的算法。

gmpy

  • gmpy.root(a, b),返回一个元组 (x, y),其中 xab 次方的值,y 是判断 x 是否为整数的布尔型变量

gmpy2

安装时,可能会需要自己另行安装 mpfr 与 mpc 库。

  • gmpy2.iroot(a, b),类似于 gmpy.root(a,b)

pycrypto

  • 安装

    sudo pip install pycrypto
  • 使用

    import gmpy
    from Crypto.Util.number import *
    from Crypto.PublicKey import RSA
    from Crypto.Cipher import PKCS1_v1_5
    
    msg = 'crypto here'
    p = getPrime(128)
    q = getPrime(128)
    n = p*q
    e = getPrime(64)
    pubkey = RSA.construct((long(n), long(e)))
    privatekey = RSA.construct((long(n), long(e), long(d), long(p), long(q)))
    key = PKCS1_v1_5.new(pubkey)
    enc = key.encrypt(msg).encode('base64')
    key = PKCS1_v1_5.new(privatekey)
    msg = key.decrypt(enc.decode('base64'), e)

Jarvis OJ - Basic - veryeasyRSA

p = 3487583947589437589237958723892346254777 q = 8767867843568934765983476584376578389

e = 65537

求 d =

请提交 PCTF{d}

直接根据 $ed\equiv 1 \pmod{\varphi (N)}$,其中 $\varphi (N)=\varphi (p)\varphi (q)=(p-1)(q-1)$,可得 $d$。

import gmpy2
p = 3487583947589437589237958723892346254777
q = 8767867843568934765983476584376578389
e = 65537
phin = (p - 1) * (q - 1)
print gmpy2.invert(e, phin)
➜  Jarvis OJ-Basic-veryeasyRSA git:(master) ✗ python exp.py       
19178568796155560423675975774142829153827883709027717723363077606260717434369

2018 CodeGate CTF Rsababy

程序就是一个简单的 RSA,不过程序还生成了两个奇怪的数

e = 65537
n = p * q
pi_n = (p-1)*(q-1)
d = mulinv(e, pi_n)
h = (d+p)^(d-p)
g = d*(p-0xdeadbeef)

所以,问题应该出自这里,所以我们就从此下手,不放这里先假设 const = 0xdeadbeef。那么

$$
eg = ed * (p-const)
$$

进而,根据 RSA 可知

$$
2^{eg}=2^{ed * (p-const)}=2^{p-const} \pmod n
$$

$$
2^{p-const} * 2^{const-1} = 2^{p-1} \pmod n
$$

所以

$$
2^{p-1} = 2^{eg} * 2^{const-1}+kn
$$

而与此同时根据费马小定理,我们知道

$$
2^{p-1} \equiv 1 \pmod p
$$

所以

$$
p|2^{p-1}-1 | 2^{eg+const-1}-1+kn
$$

进而

$$
p|2^{eg+const-1}-1
$$

所以

$$
p|gcd(2^{eg+const-1}-1,n)
$$

因此,代码如下

tmp = gmpy2.powmod(2,e*g+const-1,n)-1
p = gmpy2.gcd(tmp,n)
q = n/p
phin = (p-1)*(q-1)
d =gmpy2.invert(e,phin)
plain = gmpy2.powmod(data,d,n)
print hex(plain)[2:].decode('hex')

2018 国家安全周 pure math

题目的基本描述是这个样子的

1) p ** p % q = 1137973316343089029387365135250835133803975869258714714790597743585251681751361684698632609164883988455302237641489036138661596754239799122081528662395492
2) q ** q % p = 6901383184477756324584651464895743132603115552606852729050186289748558760692261058141015199261946483809004373728135568483701274908717004197776113227815323
3) (p ** q + q ** p) % (p*q) = 16791287391494893024031688699360885996180880807427715700800644759680986120242383930558410147341340225420991368114858791447699399702390358184412301644459406
4) (p+q) ** (p+q) % (p*q) = 63112211860889153729003401381621068190906433969243079543438386686621389392583849748240273643614258173423474299387234175508649197780206757067354426424570586101908571600743792328163163458500138799976944702155779196849585083397395750018148652864158388247163109077215394538930498877175474225571393901460434679279
5) FLAG ** 31337 % (p*q) = 6931243291746179589612148118911670244427928875888377273917973305632621316868302667641610838193899081089153471883271406133321321416064760200919958612671379845738048938060512995550639898688604592620908415248701721672948126507753670027043162669545932921683579001870526727737212722417683610956855529996310258030
Now, what’s the FLAG???

我们的目的基本上就是求得 FLAG,那么怎么做呢?这个题目需要我们具有较好的数论功底。

根据题目中这样的内容,我们可以假设 $p$,$q$ 都是大素数,那么

$p^{q-1} \equiv 1\bmod q$

那么

$p^{q} \equiv p \bmod pq$

那么我们可以根据 3)知道

$p^q+q^p \equiv p+q \bmod pq$

而 $p+q$ 又显然小于 $pq$,所以我们就知道 $p+q$ 的数值。

进一步,我们假设1),2),3),4),5)对应的值分别为 $x_1$, $x_2$, $x_3$, $x_4$, $x_5$ 则

根据4),我们可以知道

$(p+q)^{p+q} \equiv p^{p+q}+q^{p+q} \bmod pq$

又因为1)和 2),则

$p^pp \equiv px_1\bmod pq$

$q^qq \equiv qx_2 \bmod pq$

因此

$px_1+qx_2 \equiv x_4 \bmod pq$

根据 $x_1$ 和 $x_2$ 的求得方式,我们可以知道这里也是等号,因此我们得到了一个二元一次方程组,直接求解即可。

import gmpy2
x1 = 1137973316343089029387365135250835133803975869258714714790597743585251681751361684698632609164883988455302237641489036138661596754239799122081528662395492
x2 = 6901383184477756324584651464895743132603115552606852729050186289748558760692261058141015199261946483809004373728135568483701274908717004197776113227815323
p_q = 16791287391494893024031688699360885996180880807427715700800644759680986120242383930558410147341340225420991368114858791447699399702390358184412301644459406
x4 = 63112211860889153729003401381621068190906433969243079543438386686621389392583849748240273643614258173423474299387234175508649197780206757067354426424570586101908571600743792328163163458500138799976944702155779196849585083397395750018148652864158388247163109077215394538930498877175474225571393901460434679279

if (x4 - x1 * p_q) % (x2 - x1) == 0:
    print 'True'
q = (x4 - x1 * p_q) / (x2 - x1)
print q
p = p_q - q

c = 6931243291746179589612148118911670244427928875888377273917973305632621316868302667641610838193899081089153471883271406133321321416064760200919958612671379845738048938060512995550639898688604592620908415248701721672948126507753670027043162669545932921683579001870526727737212722417683610956855529996310258030

phin = (p - 1) * (q - 1)
d = gmpy2.invert(31337, phin)
flag = gmpy2.powmod(c, d, p * q)
flag = hex(flag)[2:]
print flag.decode('hex')

flag 如下

➜  2018-国家安全周第一场-puremath git:(master) ✗ python exp.py
True
7635093784603905632817000902311635311970645531806863592697496927519352405158721310359124595712780726701027634372170535318453656286180828724079479352052417
flag{6a66b8d5-6047-4299-a48e-4c4d1f874d12}

2018 Pwnhub LHY

首先分析这段代码

assert gmpy.is_prime(y)**2016 + gmpy.is_prime(x + 1)**2017 + (
    (x**2 - 1)**2 % (2 * x * y - 1) + 2
)**2018 == 30097557298197417800049182668952226601954645169633891463401117760245367082644152355564014438095421962150109895432272944128252155287648477680131934943095113263121691874508742328500559321036238322775864636883202538152031804102118831278605474474352011895348919417742923873371980983336517409056008233804190890418285814476821890492630167665485823056526646050928460488168341721716361299816947722947465808004305806687049198633489997459201469227952552870291934919760829984421958853221330987033580524592596407485826446284220272614663464267135596497185086055090126893989371261962903295313304735911034185619611156742146

由于 gmpy.is_prime 要么返回1,要么返回 0,所以我们可以很容易地试出来 y 是素数,x+1 也是素数,并且

$(x^2-1)^2\equiv 0 \bmod (2xy-1)$

为了式子能够整除,猜测 $x=2y$ 。

于是,对于下面的内容

p = gmpy.next_prime(x**3 + y**3)
q = gmpy.next_prime(x**2 * y + y**2 * x)
n = p * q
phi = (p - 1) * (q - 1)
d = gmpy.invert(0x10001, phi)
enc = pow(bytes_to_long(flag), 0x10001, n)
print 'n =', n
print 'enc =', enc

$p$ 和 $q$ 自然为

$p=next_prime(9y^3)$

$q=next_prime(6y^3)$

根据素数的间隔,可以知道 $p$ 和 $q$ 最多比括号里的数字大一点,这里一般不会超过 $1000$。

那么

$n \geq 54y^6$

所以我们知道了 $y$ 的上界,而对于 $y$ 的下界其实也不会离上界太远,我们大概减个几十万。进而,我们利用二分查找的方式来寻找 $p$ 和 $q$,如下

import gmpy2
tmp = 30097557298197417800049182668952226601954645169633891463401117760245367082644152355564014438095421962150109895432272944128252155287648477680131934943095113263121691874508742328500559321036238322775864636883202538152031804102118831278605474474352011895348919417742923873371980983336517409056008233804190890418285814476821890492630167665485823056526646050928460488168341721716361299816947722947465808004305806687049198633489997459201469227952552870291934919760829984421958853221330987033580524592596407485826446284220272614663464267135596497185086055090126893989371261962903295313304735911034185619611156742146

print gmpy2.iroot(tmp, 2018)
print gmpy2.iroot(tmp - 1, 2018)

print gmpy2.iroot(tmp - 2, 2018)

n = 260272753019642842691231717156206014402348296256668058656902033827190888150939144319270903947159599144884859205368557385941127216969379550487700198771513118894125094678559478972591331182960004648132846372455712958337042783083099376871113795475285658106058675217077803768944674144803250791799957440111855021945690877200606577646234107957498370758707097662736662439460472126493593605957225541979181422479704018055731221681621886820626215670393536343427267329350730257979042198593215747542270975288047196483958369426727778580292311145109908665004662296440533724591193527886702374790526322791818523938910660223971454070731594803459613066617828657725704376475527288174777197739360634209448477565044519733575375490101670974499385760735451471034271880800081246883157088501597655371430353965493264345172541221268942926210055390568364981514774743693528424196241142665685211916330254113610598390909248626686397970038848966187547231199741

y = 191904757378974300059526915134037747982760255307942501070454569331878491189601823952845623286161325306079772871025816081849039036850918375408172174102720702781463514549851887084613000000L
y = gmpy2.next_prime(y)

enc = 73933313646416156737449236838459526871566017180178176765840447023088664788672323530940171469589918772272559607026808711216932468486201094786991159096267208480969757088208089800600731106685561375522764783335332964711981392251568543122418192877756299395774738176188452197889668610818741062203831272066261677731889616150485770623945568369493256759711422067551058418926344060504112146971937651406886327429318390247733970549845424064244469193626197360072341969574784310397213033860597822010667926563087858301337091484951760613299203587677078666096526093414014637559237148644939541419075479462431789925219269815364529507771308181435591670281081465439913711912925412078002618729159141400730636976744132429329651487292506365655834202469178066850282850374067239317928012461993443785247524500680257923687511378073703423047348824611101206633407452837948194591695712958510124436821151767823443033286425729473563002691262316964646014201612

end = gmpy2.iroot(n / 54, 6)[0]
beg = end - 2000000

mid = 1
while beg < end:
    mid = (beg + end) / 2
    if gmpy2.is_prime(mid) != 1:
        mid = gmpy2.next_prime(mid)
    p = gmpy2.next_prime(9 * mid**3)
    q = gmpy2.next_prime(6 * mid**3)
    n1 = p * q
    if n1 == n:
        print p, q
        phin = (p - 1) * (q - 1)
        d = gmpy2.invert(0x10001, phin)
        m = gmpy2.powmod(enc, d, n)
        print hex(m)[2:].strip('L').decode('hex')
        print 'ok'
        exit(0)
    elif n1 < n:
        beg = mid
    else:
        end = mid
    print beg, end

模数相关攻击

暴力分解 N

攻击条件

在 N 的比特位数小于 512 的时候,可以采用大整数分解的策略获取 p 和 q。

JarvisOJ - Easy RSA

这里我们以 “JarvisOJ - Easy RSA” 为例进行介绍,题目如下

还记得 veryeasy RSA 吗?是不是不难?那继续来看看这题吧,这题也不难。
已知一段 RSA 加密的信息为:0xdc2eeeb2782c 且已知加密所用的公钥:
N=322831561921859 e = 23
请解密出明文,提交时请将数字转化为 ascii 码提交
比如你解出的明文是 0x6162,那么请提交字符串 ab
提交格式:PCTF{明文字符串}

可以看出,我们的 N 比较小,这里我们直接使用 factordb 进行分解,可以得到

$$
322831561921859 = 13574881 \times 23781539
$$

进而我们简单编写程序如下

import gmpy2
p = 13574881
q = 23781539
n = p * q
e = 23
c = 0xdc2eeeb2782c
phin = (p - 1) * (q - 1)
d = gmpy2.invert(e, phin)
p = gmpy2.powmod(c, d, n)
tmp = hex(p)
print tmp, tmp[2:].decode('hex')

结果如下

➜  Jarvis OJ-Basic-easyRSA git:(master) ✗ python exp.py
0x33613559 3a5Y

p & q 不当分解 N

攻击条件

当 RSA 中 p 和 q 选取不当时,我们也可以进行攻击。

|p-q| 很大

当 p-q 很大时,一定存在某一个参数较小,这里我们假设为 p,那么我们可以通过穷举的方法对模数进行试除,从而分解模数,得到保密参数与明文信息。基本来说,不怎么可行。

|p-q| 较小

首先

$$
\frac{(p+q)^2}{4}-n=\frac{(p+q)^2}{4}-pq=\frac{(p-q)^2}{4}
$$

既然 |p-q| 较小,那么 $\frac{(p-q)^2}{4}$ 自然也比较小,进而 $\frac{(p+q)^2}{4}$ 只是比 N 稍微大一点,所以 $\frac{p+q}{2}$ 与 $\sqrt{n}$ 相近。那么我们可以按照如下方法来分解

  • 顺序检查 $\sqrt{n}$ 的每一个整数 x,直到找到一个 x 使得 $x^2-n$ 是平方数,记为 $y^2$
  • 那么 $x^2-n=y^2$,进而根据平方差公式即可分解 N

p - 1 光滑

  • 光滑数(Smooth number):指可以分解为小素数乘积的正整数

  • 当$p$是$N$的因数,并且$p-1$是光滑数,可以考虑使用Pollard's p-1算法来分解$N$

  • 根据费马小定理有

    $$若p\nmid a,\ 则a^{p-1}\equiv 1\pmod{p}$$

    则有

    $$a^{t(p-1)}\equiv 1^t \equiv 1\pmod{p}$$

    $$a^{t(p-1)} - 1 = k*p$$

  • 根据Pollard's p-1算法:

    如果$p$是一个$B-smooth\ number$,那么则存在

    $$M = \prod_{q\le{B}}{q^{\lfloor\log_q{B}\rfloor}}$$

    使得

    $$(p-1)\mid M$$

    成立,则有

    $$\gcd{(a^{M}-1, N)}$$

    如果结果不为$1$或$N$,那么就已成功分解$N$。

    因为我们只关心最后的gcd结果,同时N只包含两个素因子,则我们不需要计算$M$,考虑$n=2,3,\dots$,令$M = n!$即可覆盖正确的$M$同时方便计算。

  • 在具体计算中,可以代入降幂进行计算

    $$
    a^{n!}\bmod{N}=\begin{cases}
    (a\bmod{N})^2\mod{N}&n=2\
    (a^{(n-1)!}\bmod{N})^n\mod{N}&n\ge{3}
    \end{cases}
    $$

  • Python代码实现

      from gmpy2 import *
      a = 2
      n = 2
      while True:
          a = powmod(a, n, N)
          res = gcd(a-1, N)
          if res != 1 and res != N:
              q = n // res
              d = invert(e, (res-1)*(q-1))
              m = powmod(c, d, N)
              print(m)
              break
          n += 1

p + 1 光滑

  • 当$p$是$N$的因数,并且$p+1$是光滑数,可以考虑使用Williams's p+1算法来分解$N$

  • 已知$N$的因数$p$,且$p+1$是一个光滑数

    $$
    p = \left(\prod_{i=1}^k{q_i^{\alpha_i}}\right)+1
    $$

    $q_i$即第$i$个素因数且有$q_i^{\alpha_i}\le B_1$, 找到$\beta_i$使得让$q_i^{\beta_i}\le B_1$且$q_i^{\beta_i+1}> B_1$,然后令

    $$
    R = \prod_{i=1}^k{q_i^{\beta_i}}
    $$

    显然有$p-1\mid R$且当$(N, a) = 1$时有$a^{p-1}\equiv 1 \pmod{p}$,所以有$a^R\equiv 1\pmod{p}$,即

    $$
    p\mid(N, a^R-1)
    $$

  • 令$P,Q$为整数,$\alpha,\beta$为方程$x^2-Px+Q=0$的根,定义如下类卢卡斯序列

    $$
    \begin{aligned}
    U_n(P, Q) &= (\alpha^n -\beta^n)/(\alpha - \beta)\
    V_n(P, Q) &= \alpha^n + \beta^n
    \end{aligned}
    $$

    同样有$\Delta = (\alpha - \beta)^2 = P^2-4Q$,则有

    $$
    \begin{cases}
    U_{n+1} &= PU_n - QU_{n-1}\
    V_{n+1} &= PV_n - QV_{n-1}
    \end{cases}\tag{2.2}
    $$

    $$
    \begin{cases}
    U_{2n} &= V_nU_n\
    V_{2n} &= V_n^2 - 2Q^n
    \end{cases}\tag{2.3}
    $$

    $$
    \begin{cases}
    U_{2n-1} &= U_n^2 - QU_{n-1}^2\
    V_{2n-1} &= V_nV_{n-1} - PQ^{n-1}
    \end{cases}\tag{2.4}
    $$

    $$
    \begin{cases}
    \Delta U_{n} &= PV_n - 2QV_{n-1}\
    V_{n} &= PU_n - 2QU_{n-1}
    \end{cases}\tag{2.5}
    $$

    $$
    \begin{cases}
    U_{m+n} &= U_mU_{n+1} - QU_{m-1}U_n\
    \Delta U_{m+n} &= V_mV_{n+1} - QV_{m-1}V_n
    \end{cases}\tag{2.6}
    $$

    $$
    \begin{cases}
    U_{n}(V_k(P, Q), Q^k) &= U_{nk}(P, Q)/U_k(P, Q)\
    V_{n}(V_k(P, Q), Q^k) &= V_n(P, Q)
    \end{cases}\tag{2.7}
    $$

    同时我们有如果$(N, Q) = 1$且$P^{‘}Q\equiv P^2-2Q\pmod{N}$,则有$P^{‘}\equiv \alpha/\beta + \beta/\alpha$以及$Q^{‘}\equiv \alpha/\beta + \beta/\alpha = 1$,即

    $$
    U_{2m}(P, Q)\equiv PQ^{m-1}U_m(P^{‘}, 1)\pmod{N}\tag{2.8}
    $$

    根据扩展卢卡斯定理

    如果p是奇素数,$p\nmid Q$且勒让德符号$(\Delta/p) = \epsilon$,则

    $$
    \begin{aligned}
    U_{(p-\epsilon)m}(P, Q) &\equiv 0\pmod{p}\
    V_{(p-\epsilon)m}(P, Q) &\equiv 2Q^{m(1-\epsilon)/2}\pmod{p}
    \end{aligned}
    $$

  • 第一种情况:已知N的因数p,且p+1是一个光滑数

    $$
    p = \left(\prod_{i=1}^k{q_i^{\alpha_i}}\right)-1
    $$

    有$p+1\mid R$,当$(Q, N)=1$且$(\Delta/p) = -1$时有$p\mid U_R(P, Q)$,即$p\mid (U_R(P, Q), N)$

    为了找到$U_R(P, Q)$,GuyConway提出可以使用如下公式

    $$
    \begin{aligned}
    U_{2n-1} &= U_n^2 - QU_n^2 - 1\
    U_{2n} &= U_n(PU_n - 2QU_{n-1})\
    U_{2n+1} &= PU_{2n} - QU_{2n-1}
    \end{aligned}
    $$

    但是上述公式值太大了,不便运算,我们可以考虑如下方法

    如果$p \mid U_R(P, 1)$,根据公式2.3有$p\mid U_{2R}(P, Q)$,所以根据公式2.8有$p \mid U_R(P^{‘}, 1)$,设$Q=1$,则有

    $$
    V_{(p-\epsilon)m}(P, 1) \equiv 2\pmod{p}
    $$

    即,如果$p\mid U_R(P, 1)$,则$p\mid(V_R(P, 1) -2)$.

    第一种情况可以归纳为:

    让$R = r_1r_2r_3\cdots r_m$,同时找到$P_0$使得$(P_0^2-4, N) = 1$,定义$V_n(P) = V_n(P, 1), U_n(P) = U_n(P, 1)$且

    $$
    P_j \equiv V_{r_j}(P_{j-1})\pmod{N}(j = 1,2,3,\dots,m)
    $$

    根据公式2.7,有

    $$
    P_m \equiv V_R(P_0)\pmod{N}\tag{3.1}
    $$

    要计算$V_r = V_r(P)$可以用如下公式

    根据公式2.2公式2.3公式2.4

    $$
    \begin{cases}
    V_{2f-1}&\equiv V_fV_{f-1}-P\
    V_{2f}&\equiv V_f^2 - 2\
    V_{2f+1}&\equiv PV_f^2-V_fV_{f-1}-P\pmod(N)
    \end{cases}
    $$

    $$
    r = \sum_{i=0}^t{b_t2^{t-i}}\ \ \ \ (b_i=0,1)
    $$

    $f_0=1, f_{k+1}=2f_k+b_{k+1}$,则$f_t=r$,同样$V_0(P) = 2, V_1(P) = P$,则最终公式为

    $$
    (V_{f_{k+1}}, V_{f_{k+1}-1}) = \begin{cases}
    (V_{2f_k}, V_{2f_k-1})\ \ \ \ if\ b_{k+1}=0\
    (V_{2f_k+1}, V_{2f_k})\ \ \ \ if\ b_{k+1}=1
    \end{cases}
    $$

  • 第二种情况:已知p+1是一个光滑数

    $$
    p = s\left(\prod_{i=1}^k{q_i^{\alpha_i}}\right)-1
    $$

    当$s$是素数,且$B_1<s\le B_2$,有$p\mid(a_m^s-1, N),$定义$s_j$和$2d_j$

    $$
    2d_j = s_j+1-s_j
    $$

    如果$(\Delta/p) = -1$且$p\nmid P_m-2$,则根据公式2.7公式3.1有$p\mid(U_s(P_m), N)$。

    令$U[n] \equiv U_n(P_m), V[n]\equiv V_n(P_m)\pmod{N}$,计算$U[2d_j-1], U[2d_j], U[2d_j+1]$通过

    $$U[0] = 0, U[1] = 1, U[n+1] = P_mU[n] - U[n-1]$$

    计算

    $$
    T[s_i] \equiv \Delta U_{s_i}(P_m) = \Delta U_{s_iR}(P_0)/U_R(P_0)\pmod{N}
    $$

    通过公式2.6公式2.7公式3.1

    $$
    \begin{cases}
    T[s_1]&\equiv P_mV[s_1]-2V[s_1-1]\
    T[s_1-1]&\equiv 2V[s_1]-P_mV[s_1-1]\pmod{N}
    \end{cases}
    $$

    $$
    \begin{cases}
    T[s_{i+1}]&\equiv T[s_i]U[2d_i+1]-T[s_i-1]U[2d_i]\
    T[s_{i+1}-1]&\equiv T[s_i]U[2d_i]-T[s_i-1]U[2d_i-1]\pmod{N}
    \end{cases}
    $$

    计算$T[s_i], i=1,2,3\dots$,然后计算

    $$
    H_t = (\prod_{i=0}^c{T[s_{i+t}], N})
    $$

    其中$t = 1, c+1, 2c+1, \dots, c[B_2/c]+1$,我们有$p\mid H_i$当$(\Delta/p)=-1$

  • python代码实现

      def mlucas(v, a, n):
          """ Helper function for williams_pp1().  Multiplies along a Lucas sequence modulo n. """
          v1, v2 = v, (v**2 - 2) % n
          for bit in bin(a)[3:]: v1, v2 = ((v1**2 - 2) % n, (v1*v2 - v) % n) if bit == "0" else ((v1*v2 - v) % n, (v2**2 - 2) % n)
          return v1
    
      for v in count(1):
          for p in primegen():
              e = ilog(isqrt(n), p)
              if e == 0: break
              for _ in xrange(e): v = mlucas(v, p, n)
              g = gcd(v-2, n)
              if 1 < g < n: return g # g|n
              if g == n: break

2017 SECCON very smooth

该程序给了一个 HTTPS 加密的流量包,首先从其中拿到证书

➜  2017_SECCON_verysmooth git:(master) binwalk -e s.pcap      

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
2292          0x8F4           Certificate in DER format (x509 v3), header length: 4, sequence length: 467
4038          0xFC6           Certificate in DER format (x509 v3), header length: 4, sequence length: 467
5541          0x15A5          Certificate in DER format (x509 v3), header length: 4, sequence length: 467

➜  2017_SECCON_verysmooth git:(master) ls
s.pcap  _s.pcap.extracted  very_smooth.zip

这里分别查看三个证书,三个模数都一样,这里只给一个例子

➜  _s.pcap.extracted git:(master) openssl x509 -inform DER -in FC6.crt  -pubkey -text -modulus -noout 
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDVRqqCXPYd6Xdl9GT7/kiJrYvy
8lohddAsi28qwMXCe2cDWuwZKzdB3R9NEnUxsHqwEuuGJBwJwIFJnmnvWurHjcYj
DUddp+4X8C9jtvCaLTgd+baSjo2eB0f+uiSL/9/4nN+vR3FliRm2mByeFCjppTQl
yioxCqbXYIMxGO4NcQIDAQAB
-----END PUBLIC KEY-----
Certificate:
    Data:
        Version: 1 (0x0)
        Serial Number: 11640506567126718943 (0xa18b630c7b3099df)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=JP, ST=Kawasaki, O=SRL
        Validity
            Not Before: Oct  8 02:47:17 2017 GMT
            Not After : Oct  8 02:47:17 2018 GMT
        Subject: C=JP, ST=Kawasaki, O=SRL
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (1024 bit)
                Modulus:
                    00:d5:46:aa:82:5c:f6:1d:e9:77:65:f4:64:fb:fe:
                    48:89:ad:8b:f2:f2:5a:21:75:d0:2c:8b:6f:2a:c0:
                    c5:c2:7b:67:03:5a:ec:19:2b:37:41:dd:1f:4d:12:
                    75:31:b0:7a:b0:12:eb:86:24:1c:09:c0:81:49:9e:
                    69:ef:5a:ea:c7:8d:c6:23:0d:47:5d:a7:ee:17:f0:
                    2f:63:b6:f0:9a:2d:38:1d:f9:b6:92:8e:8d:9e:07:
                    47:fe:ba:24:8b:ff:df:f8:9c:df:af:47:71:65:89:
                    19:b6:98:1c:9e:14:28:e9:a5:34:25:ca:2a:31:0a:
                    a6:d7:60:83:31:18:ee:0d:71
                Exponent: 65537 (0x10001)
    Signature Algorithm: sha256WithRSAEncryption
         78:92:11:fb:6c:e1:7a:f7:2a:33:b8:8b:08:a7:f7:5b:de:cf:
         62:0b:a0:ed:be:d0:69:88:38:93:94:9d:05:41:73:bd:7e:b3:
         32:ec:8e:10:bc:3a:62:b0:56:c7:c1:3f:60:66:a7:be:b9:46:
         f7:46:22:6a:f3:5a:25:d5:66:94:57:0e:fc:b5:16:33:05:1c:
         6f:f5:85:74:57:a4:a0:c6:ce:4f:fd:64:53:94:a9:83:b8:96:
         bf:5b:a7:ee:8b:1e:48:a7:d2:43:06:0e:4f:5a:86:62:69:05:
         e2:c0:bd:4e:89:c9:af:04:4a:77:a2:34:86:6a:b8:d2:3b:32:
         b7:39
Modulus=D546AA825CF61DE97765F464FBFE4889AD8BF2F25A2175D02C8B6F2AC0C5C27B67035AEC192B3741DD1F4D127531B07AB012EB86241C09C081499E69EF5AEAC78DC6230D475DA7EE17F02F63B6F09A2D381DF9B6928E8D9E0747FEBA248BFFDFF89CDFAF4771658919B6981C9E1428E9A53425CA2A310AA6D760833118EE0D71

可以看出模数只有 1024 比特。而且,根据题目名 very smooth,应该是其中一个因子比较 smooth,这里我们利用 primefac 分别尝试 Pollard’s p − 1 与 Williams’s p + 1 算法,如下

➜  _s.pcap.extracted git:(master) python -m primefac -vs -m=p+1  149767527975084886970446073530848114556615616489502613024958495602726912268566044330103850191720149622479290535294679429142532379851252608925587476670908668848275349192719279981470382501117310509432417895412013324758865071052169170753552224766744798369054498758364258656141800253652826603727552918575175830897

149767527975084886970446073530848114556615616489502613024958495602726912268566044330103850191720149622479290535294679429142532379851252608925587476670908668848275349192719279981470382501117310509432417895412013324758865071052169170753552224766744798369054498758364258656141800253652826603727552918575175830897: p+1 11807485231629132025602991324007150366908229752508016230400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 12684117323636134264468162714319298445454220244413621344524758865071052169170753552224766744798369054498758364258656141800253652826603727552918575175830897
Z309  =  P155 x P155  =  11807485231629132025602991324007150366908229752508016230400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001 x 12684117323636134264468162714319298445454220244413621344524758865071052169170753552224766744798369054498758364258656141800253652826603727552918575175830897

可以发现当使用 Williams’s p + 1 算法时,就直接分解出来了。按道理这个因子是 p-1 似乎更光滑,但是却并不能使用 Pollard’s p − 1 算法分解,这里进行进一步的测试

➜  _s.pcap.extracted git:(master) python -m primefac -vs 1180748523162913202560299132400715036690822975250801623040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002

1180748523162913202560299132400715036690822975250801623040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002: 2 7 43 503 761429 5121103123294685745276806480148867612214394022184063853387799606010231770631857868979139305712805242051823263337587909550709296150544706624823
Z154  =  P1 x P1 x P2 x P3 x P6 x P142  =  2 x 7 x 43 x 503 x 761429 x 5121103123294685745276806480148867612214394022184063853387799606010231770631857868979139305712805242051823263337587909550709296150544706624823

➜  _s.pcap.extracted git:(master) python -m primefac -vs 1180748523162913202560299132400715036690822975250801623040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 

1180748523162913202560299132400715036690822975250801623040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000: 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5 5
Z154  =  P1^185 x P1^62 x P1^97  =  2^185 x 3^62 x 5^97

可以看出,对于 p-1 确实有很多小因子,但是个数太多,这就会使得进行枚举的时候出现指数爆炸的情况,因此没有分解出来。

进而根据分解出来的数构造私钥

from Crypto.PublicKey import RSA
import gmpy2


def main():
    n = 149767527975084886970446073530848114556615616489502613024958495602726912268566044330103850191720149622479290535294679429142532379851252608925587476670908668848275349192719279981470382501117310509432417895412013324758865071052169170753552224766744798369054498758364258656141800253652826603727552918575175830897L
    p = 11807485231629132025602991324007150366908229752508016230400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001L
    q = 12684117323636134264468162714319298445454220244413621344524758865071052169170753552224766744798369054498758364258656141800253652826603727552918575175830897L
    e = 65537L
    priv = RSA.construct((n, e, long(gmpy2.invert(e, (p - 1) * (q - 1)))))
    open('private.pem', 'w').write(priv.exportKey('PEM'))


main()

最后,将私钥导入到 wireshark 中即可得到明文(Edit -> Preferences -> Protocols -> SSL -> RSA Key List)。

<html>
<head><title>Very smooth</title></head>
<body>
<h1>
Answer: One of these primes is very smooth.
</h1>
</body>
</html>

扩展

关于更多的一些分解模数 N 的方法可以参考 https://en.wikipedia.org/wiki/Integer_factorization。

模不互素

攻击原理

当存在两个公钥的 N 不互素时,我们显然可以直接对这两个数求最大公因数,然后直接获得 p,q,进而获得相应的私钥。

SCTF RSA2

这里我们以 SCTF rsa2 为例进行介绍。直接打开 pcap 包,发现有一堆的消息,包含 N 和 e,然后试了试不同的 N 是否互素,我试了前两个

import gmpy2
n1 = 20823369114556260762913588844471869725762985812215987993867783630051420241057912385055482788016327978468318067078233844052599750813155644341123314882762057524098732961382833215291266591824632392867716174967906544356144072051132659339140155889569810885013851467056048003672165059640408394953573072431523556848077958005971533618912219793914524077919058591586451716113637770245067687598931071827344740936982776112986104051191922613616045102859044234789636058568396611030966639561922036712001911238552391625658741659644888069244729729297927279384318252191421446283531524990762609975988147922688946591302181753813360518031
n2 = 19083821613736429958432024980074405375408953269276839696319265596855426189256865650651460460079819368923576109723079906759410116999053050999183058013281152153221170931725172009360565530214701693693990313074253430870625982998637645030077199119183041314493288940590060575521928665131467548955951797198132001987298869492894105525970519287000775477095816742582753228905458466705932162641076343490086247969277673809512472546919489077884464190676638450684714880196854445469562733561723325588433285405495368807600668761929378526978417102735864613562148766250350460118131749533517869691858933617013731291337496943174343464943
print gmpy2.gcd(n1, n2)

结果发现竟然不互素。

➜  scaf-rsa2 git:(master) ✗ python exp.py
122281872221091773923842091258531471948886120336284482555605167683829690073110898673260712865021244633908982705290201598907538975692920305239961645109897081011524485706755794882283892011824006117276162119331970728229108731696164377808170099285659797066904706924125871571157672409051718751812724929680249712137

那么我们就可以直接来解密了,这里我们利用第一对公钥密码。代码如下

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5, PKCS1_OAEP
import gmpy2
from base64 import b64decode
n1 = 20823369114556260762913588844471869725762985812215987993867783630051420241057912385055482788016327978468318067078233844052599750813155644341123314882762057524098732961382833215291266591824632392867716174967906544356144072051132659339140155889569810885013851467056048003672165059640408394953573072431523556848077958005971533618912219793914524077919058591586451716113637770245067687598931071827344740936982776112986104051191922613616045102859044234789636058568396611030966639561922036712001911238552391625658741659644888069244729729297927279384318252191421446283531524990762609975988147922688946591302181753813360518031
n2 = 19083821613736429958432024980074405375408953269276839696319265596855426189256865650651460460079819368923576109723079906759410116999053050999183058013281152153221170931725172009360565530214701693693990313074253430870625982998637645030077199119183041314493288940590060575521928665131467548955951797198132001987298869492894105525970519287000775477095816742582753228905458466705932162641076343490086247969277673809512472546919489077884464190676638450684714880196854445469562733561723325588433285405495368807600668761929378526978417102735864613562148766250350460118131749533517869691858933617013731291337496943174343464943
p1 = gmpy2.gcd(n1, n2)
q1 = n1 / p1
e = 65537
phin = (p1 - 1) * (q1 - 1)
d = gmpy2.invert(e, phin)
cipher = 0x68d5702b70d18238f9d4a3ac355b2a8934328250efd4efda39a4d750d80818e6fe228ba3af471b27cc529a4b0bef70a2598b80dd251b15952e6a6849d366633ed7bb716ed63c6febd4cd0621b0c4ebfe5235de03d4ee016448de1afbbe61144845b580eed8be8127a8d92b37f9ef670b3cdd5af613c76f58ca1a9f6f03f1bc11addba30b61bb191efe0015e971b8f78375faa257a60b355050f6435d94b49eab07075f40cb20bb8723d02f5998d5538e8dafc80cc58643c91f6c0868a7a7bf3bf6a9b4b6e79e0a80e89d430f0c049e1db4883c50db066a709b89d74038c34764aac286c36907b392bc299ab8288f9d7e372868954a92cdbf634678f7294096c7
plain = gmpy2.powmod(cipher, d, n1)
plain = hex(plain)[2:]
if len(plain) % 2 != 0:
    plain = '0' + plain
print plain.decode('hex')

最后解密如下

➜  scaf-rsa2 git:(master) ✗ python exp.py       
sH1R3_PRlME_1N_rsA_iS_4ulnEra5le

解压压缩包即可。

共模攻击

攻击条件

当两个用户使用相同的模数 N、不同的私钥时,加密同一明文消息时即存在共模攻击。

攻击原理

设两个用户的公钥分别为 $e_1$ 和 $e_2$,且两者互质。明文消息为 $m$,密文分别为:

$$
c_1 = m^{e_1}\bmod N \
c_2 = m^{e_2}\bmod N
$$

当攻击者截获 $c_1$ 和 $c_2$ 后,就可以恢复出明文。用扩展欧几里得算法求出 $re_1+se_2=1\bmod n$ 的两个整数 $r$ 和 $s$,由此可得:

$$
\begin{align}
c_{1}^{r}c_{2}^{s} &\equiv m^{re_1}m^{se_2}\bmod n\
&\equiv m^{(re_1+se_2)} \bmod n\
&\equiv m\bmod n
\end{align}
$$

XMan 一期夏令营课堂练习

题目描述:

{6266565720726907265997241358331585417095726146341989755538017122981360742813498401533594757088796536341941659691259323065631249,773}

{6266565720726907265997241358331585417095726146341989755538017122981360742813498401533594757088796536341941659691259323065631249,839}

message1=3453520592723443935451151545245025864232388871721682326408915024349804062041976702364728660682912396903968193981131553111537349

message2=5672818026816293344070119332536629619457163570036305296869053532293105379690793386019065754465292867769521736414170803238309535

题目来源:XMan 一期夏令营课堂练习

可以看出两个公钥的 N 是一样的,并且两者的 e 互素。写一个脚本跑一下:

import gmpy2
n = 6266565720726907265997241358331585417095726146341989755538017122981360742813498401533594757088796536341941659691259323065631249
e1 = 773

e2 = 839

message1 = 3453520592723443935451151545245025864232388871721682326408915024349804062041976702364728660682912396903968193981131553111537349

message2 = 5672818026816293344070119332536629619457163570036305296869053532293105379690793386019065754465292867769521736414170803238309535
# s & t
gcd, s, t = gmpy2.gcdext(e1, e2)
if s < 0:
    s = -s
    message1 = gmpy2.invert(message1, n)
if t < 0:
    t = -t
    message2 = gmpy2.invert(message2, n)
plain = gmpy2.powmod(message1, s, n) * gmpy2.powmod(message2, t, n) % n
print plain

得到

➜  Xman-1-class-exercise git:(master) ✗ python exp.py
1021089710312311910410111011910111610410511010710511610511511211111511510598108101125

这时候需要考虑当时明文是如何转化为这个数字了,一般来说是 16 进制转换,ASCII 字符转换,或者 Base64 解密。这个应该是 ASCII 字符转换,进而我们使用如下代码得到 flag

i = 0
flag = ""
plain = str(plain)
while i < len(plain):
    if plain[i] == '1':
        flag += chr(int(plain[i:i + 3]))
        i += 3
    else:
        flag += chr(int(plain[i:i + 2]))
        i += 2
print flag

这里之所以使用 1 来判断是否为三位长度,是因为 flag 一般都是明文字符,而 1 开头的长度为 1 或者 2 的数字,一般都是不可见字符。

flag

➜  Xman-1-class-exercise git:(master) ✗ python exp.py
flag{whenwethinkitispossible}

公钥指数相关攻击

小公钥指数攻击

攻击条件

e 特别小,比如 e 为 3。

攻击原理

假设用户使用的密钥 $e=3$。考虑到加密关系满足:

$$
c\equiv m^3 \bmod N
$$

则:

$$
\begin{align}
m^3 &= c+k\times N\
m &= \sqrt[3]{c+k\times n}
\end{align}
$$

攻击者可以从小到大枚举 $k$,依次开三次根,直到开出整数为止。

范例

这里我们以 XMan 一期夏令营课堂练习为例进行介绍(Jarvis OJ 有复现),附件中有一个 flag.encpubkey.pem,很明显是密文和公钥了,先用 openssl 读一下公钥。

➜  Jarvis OJ-Extremely hard RSA git:(master) ✗ openssl rsa -pubin -in pubkey.pem -text -modulus       
Public-Key: (4096 bit)
Modulus:
    00:b0:be:e5:e3:e9:e5:a7:e8:d0:0b:49:33:55:c6:
    18:fc:8c:7d:7d:03:b8:2e:40:99:51:c1:82:f3:98:
    de:e3:10:45:80:e7:ba:70:d3:83:ae:53:11:47:56:
    56:e8:a9:64:d3:80:cb:15:7f:48:c9:51:ad:fa:65:
    db:0b:12:2c:a4:0e:42:fa:70:91:89:b7:19:a4:f0:
    d7:46:e2:f6:06:9b:af:11:ce:bd:65:0f:14:b9:3c:
    97:73:52:fd:13:b1:ee:a6:d6:e1:da:77:55:02:ab:
    ff:89:d3:a8:b3:61:5f:d0:db:49:b8:8a:97:6b:c2:
    05:68:48:92:84:e1:81:f6:f1:1e:27:08:91:c8:ef:
    80:01:7b:ad:23:8e:36:30:39:a4:58:47:0f:17:49:
    10:1b:c2:99:49:d3:a4:f4:03:8d:46:39:38:85:15:
    79:c7:52:5a:69:98:4f:15:b5:66:7f:34:20:9b:70:
    eb:26:11:36:94:7f:a1:23:e5:49:df:ff:00:60:18:
    83:af:d9:36:fe:41:1e:00:6e:4e:93:d1:a0:0b:0f:
    ea:54:1b:bf:c8:c5:18:6c:b6:22:05:03:a9:4b:24:
    13:11:0d:64:0c:77:ea:54:ba:32:20:fc:8f:4c:c6:
    ce:77:15:1e:29:b3:e0:65:78:c4:78:bd:1b:eb:e0:
    45:89:ef:9a:19:7f:6f:80:6d:b8:b3:ec:d8:26:ca:
    d2:4f:53:24:cc:de:c6:e8:fe:ad:2c:21:50:06:86:
    02:c8:dc:dc:59:40:2c:ca:c9:42:4b:79:00:48:cc:
    dd:93:27:06:80:95:ef:a0:10:b7:f1:96:c7:4b:a8:
    c3:7b:12:8f:9e:14:11:75:16:33:f7:8b:7b:9e:56:
    f7:1f:77:a1:b4:da:ad:3f:c5:4b:5e:7e:f9:35:d9:
    a7:2f:b1:76:75:97:65:52:2b:4b:bc:02:e3:14:d5:
    c0:6b:64:d5:05:4b:7b:09:6c:60:12:36:e6:cc:f4:
    5b:5e:61:1c:80:5d:33:5d:ba:b0:c3:5d:22:6c:c2:
    08:d8:ce:47:36:ba:39:a0:35:44:26:fa:e0:06:c7:
    fe:52:d5:26:7d:cf:b9:c3:88:4f:51:fd:df:df:4a:
    97:94:bc:fe:0e:15:57:11:37:49:e6:c8:ef:42:1d:
    ba:26:3a:ff:68:73:9c:e0:0e:d8:0f:d0:02:2e:f9:
    2d:34:88:f7:6d:eb:62:bd:ef:7b:ea:60:26:f2:2a:
    1d:25:aa:2a:92:d1:24:41:4a:80:21:fe:0c:17:4b:
    98:03:e6:bb:5f:ad:75:e1:86:a9:46:a1:72:80:77:
    0f:12:43:f4:38:74:46:cc:ce:b2:22:2a:96:5c:c3:
    0b:39:29
Exponent: 3 (0x3)
Modulus=B0BEE5E3E9E5A7E8D00B493355C618FC8C7D7D03B82E409951C182F398DEE3104580E7BA70D383AE5311475656E8A964D380CB157F48C951ADFA65DB0B122CA40E42FA709189B719A4F0D746E2F6069BAF11CEBD650F14B93C977352FD13B1EEA6D6E1DA775502ABFF89D3A8B3615FD0DB49B88A976BC20568489284E181F6F11E270891C8EF80017BAD238E363039A458470F1749101BC29949D3A4F4038D463938851579C7525A69984F15B5667F34209B70EB261136947FA123E549DFFF00601883AFD936FE411E006E4E93D1A00B0FEA541BBFC8C5186CB6220503A94B2413110D640C77EA54BA3220FC8F4CC6CE77151E29B3E06578C478BD1BEBE04589EF9A197F6F806DB8B3ECD826CAD24F5324CCDEC6E8FEAD2C2150068602C8DCDC59402CCAC9424B790048CCDD9327068095EFA010B7F196C74BA8C37B128F9E1411751633F78B7B9E56F71F77A1B4DAAD3FC54B5E7EF935D9A72FB176759765522B4BBC02E314D5C06B64D5054B7B096C601236E6CCF45B5E611C805D335DBAB0C35D226CC208D8CE4736BA39A0354426FAE006C7FE52D5267DCFB9C3884F51FDDFDF4A9794BCFE0E1557113749E6C8EF421DBA263AFF68739CE00ED80FD0022EF92D3488F76DEB62BDEF7BEA6026F22A1D25AA2A92D124414A8021FE0C174B9803E6BB5FAD75E186A946A17280770F1243F4387446CCCEB2222A965CC30B3929
writing RSA key
-----BEGIN PUBLIC KEY-----
MIICIDANBgkqhkiG9w0BAQEFAAOCAg0AMIICCAKCAgEAsL7l4+nlp+jQC0kzVcYY
/Ix9fQO4LkCZUcGC85je4xBFgOe6cNODrlMRR1ZW6Klk04DLFX9IyVGt+mXbCxIs
pA5C+nCRibcZpPDXRuL2BpuvEc69ZQ8UuTyXc1L9E7Huptbh2ndVAqv/idOos2Ff
0NtJuIqXa8IFaEiShOGB9vEeJwiRyO+AAXutI442MDmkWEcPF0kQG8KZSdOk9AON
Rjk4hRV5x1JaaZhPFbVmfzQgm3DrJhE2lH+hI+VJ3/8AYBiDr9k2/kEeAG5Ok9Gg
Cw/qVBu/yMUYbLYiBQOpSyQTEQ1kDHfqVLoyIPyPTMbOdxUeKbPgZXjEeL0b6+BF
ie+aGX9vgG24s+zYJsrST1MkzN7G6P6tLCFQBoYCyNzcWUAsyslCS3kASMzdkycG
gJXvoBC38ZbHS6jDexKPnhQRdRYz94t7nlb3H3ehtNqtP8VLXn75NdmnL7F2dZdl
UitLvALjFNXAa2TVBUt7CWxgEjbmzPRbXmEcgF0zXbqww10ibMII2M5HNro5oDVE
JvrgBsf+UtUmfc+5w4hPUf3f30qXlLz+DhVXETdJ5sjvQh26Jjr/aHOc4A7YD9AC
LvktNIj3betive976mAm8iodJaoqktEkQUqAIf4MF0uYA+a7X6114YapRqFygHcP
EkP0OHRGzM6yIiqWXMMLOSkCAQM=
-----END PUBLIC KEY-----

看到 $e=3$,很明显是小公钥指数攻击了。这里我们使用 Crypto 库来读取公钥,使用 multiprocessing 来加快破解速度。

#/usr/bin/python
# coding=utf-8
import gmpy2
from Crypto.PublicKey import RSA
from multiprocessing import Pool
pool = Pool(4)

with open('./pubkey.pem', 'r') as f:
    key = RSA.importKey(f)
    N = key.n
    e = key.e
with open('flag.enc', 'r') as f:
    cipher = f.read().encode('hex')
    cipher = int(cipher, 16)


def calc(j):
    print j
    a, b = gmpy2.iroot(cipher + j * N, 3)
    if b == 1:
        m = a
        print '{:x}'.format(int(m)).decode('hex')
        pool.terminate()
        exit()


def SmallE():
    inputs = range(0, 130000000)
    pool.map(calc, inputs)
    pool.close()
    pool.join()


if __name__ == '__main__':
    print 'start'
    SmallE()

爆破时间有点长,,拿到 flag

Didn't you know RSA padding is really important? Now you see a non-padding message is so dangerous. And you should notice this in future.Fl4g: flag{Sm4ll_3xpon3nt_i5_W3ak}

题目

RSA 衍生算法——Rabin 算法

攻击条件

Rabin 算法的特征在于 $e=2$。

攻击原理

密文:

$$
c = m^2\bmod n
$$

解密:

  • 计算出 $m_p$ 和 $m_q$:

$$
\begin{align}
m_p &= \sqrt{c} \bmod p\
m_q &= \sqrt{c} \bmod q
\end{align}
$$

  • 用扩展欧几里得计算出 $y_p$ 和 $y_q$:

$$
y_p \cdot p + y_q \cdot q = 1
$$

  • 解出四个明文:

$$
\begin{align}
a &= (y_p \cdot p \cdot m_q + y_q \cdot q \cdot m_p) \bmod n\
b &= n - a\
c &= (y_p \cdot p \cdot m_q - y_q \cdot q \cdot m_p) \bmod n\
d &= n - c
\end{align}
$$

注意:如果 $p \equiv q \equiv 3 \pmod 4$,则

$$
\begin{align}
m_p &= c^{\frac{1}{4}(p + 1)} \bmod p\
m_q &= c^{\frac{1}{4}(q + 1)} \bmod q
\end{align}
$$

而一般情况下,$p \equiv q \equiv 3 \pmod 4$ 是满足的,对于不满足的情况下,请参考相应的算法解决。

例子

这里我们以 XMan 一期夏令营课堂练习(Jarvis OJ 有复现)为例,读一下公钥。

➜  Jarvis OJ-hard RSA git:(master) ✗ openssl rsa -pubin -in pubkey.pem -text -modulus 
Public-Key: (256 bit)
Modulus:
    00:c2:63:6a:e5:c3:d8:e4:3f:fb:97:ab:09:02:8f:
    1a:ac:6c:0b:f6:cd:3d:70:eb:ca:28:1b:ff:e9:7f:
    be:30:dd
Exponent: 2 (0x2)
Modulus=C2636AE5C3D8E43FFB97AB09028F1AAC6C0BF6CD3D70EBCA281BFFE97FBE30DD
writing RSA key
-----BEGIN PUBLIC KEY-----
MDowDQYJKoZIhvcNAQEBBQADKQAwJgIhAMJjauXD2OQ/+5erCQKPGqxsC/bNPXDr
yigb/+l/vjDdAgEC
-----END PUBLIC KEY-----

$e=2$,考虑 Rabin 算法。首先我们先分解一下 p 和 q,得到

p=275127860351348928173285174381581152299
q=319576316814478949870590164193048041239

编写代码

#!/usr/bin/python
# coding=utf-8
import gmpy2
import string
from Crypto.PublicKey import RSA

# 读取公钥参数
with open('pubkey.pem', 'r') as f:
    key = RSA.importKey(f)
    N = key.n
    e = key.e
with open('flag.enc', 'r') as f:
    cipher = f.read().encode('hex')
    cipher = string.atoi(cipher, base=16)
    # print cipher
print "please input p"
p = int(raw_input(), 10)
print 'please input q'
q = int(raw_input(), 10)
# 计算yp和yq
inv_p = gmpy2.invert(p, q)
inv_q = gmpy2.invert(q, p)

# 计算mp和mq
mp = pow(cipher, (p + 1) / 4, p)
mq = pow(cipher, (q + 1) / 4, q)

# 计算a,b,c,d
a = (inv_p * p * mq + inv_q * q * mp) % N
b = N - int(a)
c = (inv_p * p * mq - inv_q * q * mp) % N
d = N - int(c)

for i in (a, b, c, d):
    s = '%x' % i
    if len(s) % 2 != 0:
        s = '0' + s
    print s.decode('hex')

拿到 flag,PCTF{sp3ci4l_rsa}

私钥 d 相关攻击

d 泄露攻击

攻击原理

首先当 $d$ 泄露之后,我们自然可以解密所有加密的消息。我们甚至还可以对模数 N 进行分解。其基本原理如下

我们知道 $ed \equiv 1 \bmod \varphi(n)$,那么存在一个 $k$ 使得

$$
ed-1=k\varphi(n)
$$

又 $\forall a\in {Z}_n^*$,满足$a^{ed-1}\equiv1(\bmod n)$。令

$$
ed-1=2^st
$$

其中,$t$ 是一个奇数。然后可以证明对于至少一半的 $a\in {Z}_n^*$,存在一个 $i\in[1,s]$,使得

$$
a^{2^{i-1}t}\not\equiv\pm1(\bmod n),a^{2^{i}t}\equiv1(\bmod n)
$$

成立。如果 $a,i$ 满足上述条件,$gcd(a^{2^{i-1}t}-1,n)$是 $n$ 的一个非平凡因子,所以可以对 $n$ 进行暴力分解。

工具

利用以下工具可以直接进行计算

2017 HITB - hack in the card II

The second smart card sent to us has been added some countermeasures by that evil company. They also changed the public key(attachments -> publickey.pem). However it seems that they missed something……
Can you decrypt the following hex-encoded ciphertext this time?

016d1d26a470fad51d52e5f3e90075ab77df69d2fb39905fe634ded81d10a5fd10c35e1277035a9efabb66e4d52fd2d1eaa845a93a4e0f1c4a4b70a0509342053728e89e977cfb9920d5150393fe9dcbf86bc63914166546d5ae04d83631594703db59a628de3b945f566bdc5f0ca7bdfa819a0a3d7248286154a6cc5199b99708423d0749d4e67801dff2378561dd3b0f10c8269dbef2630819236e9b0b3d3d8910f7f7afbbed29788e965a732efc05aef3194cd1f1cff97381107f2950c935980e8954f91ed2a653c91015abea2447ee2a3488a49cc9181a3b1d44f198ff9f0141badcae6a9ae45c6c75816836fb5f331c7f2eb784129a142f88b4dc22a0a977

这题是接续 2017 HITB - hack in the card I 的一道题,我们直接使用 openssl 查看 publickey.pem 的公钥,发现它的 N 与上一道题的 N 相同,并且上题的 N,e,d 已知。由此可直接使用上面的 rsatool.py 得到 p,q,并通过本题的 e 计算出 e 得到明文。

Wiener’s Attack

攻击条件

在 d 比较小($d<\frac{1}{3}N^{\frac{1}{4}}$)时,攻击者可以使用 Wiener’s Attack 来获得私钥。

攻击原理

工具

综合例子

2016 HCTF RSA1

这里我们以 2016 年 HCTF 中 RSA 1 - Crypto So Interesting 为例进行分析,源代码链接

首先先绕过程序的 proof 部分,差不多使用一些随机的数据就可以绕过。

其次,我们来分析一下具体的代码部分,程序是根据我们的 token 来获取 flag 的,这里我们就直接利用源代码中提供的 token。

    print "This is a RSA Decryption System"
    print "Please enter Your team token: "
    token = raw_input()
    try:
        flag = get_flag(token)
        assert len(flag) == 38
    except:
        print "Token error!"
        m_exit(-1)

接下来我们首先知道 $n=pq$,我们再来你仔细分析一下这个 e,d 是如何得到的。

    p=getPrime(2048)
    q=getPrime(2048)
    n = p * q
    e, d = get_ed(p, q)
    print "n: ", hex(n)
    print "e: ", hex(e)

get_ed 函数如下

def get_ed(p, q):
    k = cal_bit(q*p)
    phi_n = (p-1)*(q-1)
    r = random.randint(10, 99)
    while True:
        u = getPrime(k/4 - r)
        if gcd(u, phi_n) != 1:
            continue
        t = invmod(u, phi_n)
        e = pi_b(t)
        if gcd(e, phi_n) == 1:
            break
    d = invmod(e, phi_n)
    return (e, d)

可以看出,我们得到的 u 的位数比 n 的位数的四分之一还要少,这里其实就差不多满足了 Wiener’s Attack 了。而且我们计算出来的 u,t,e,d 还满足以下条件

$$
\begin{align}
ut &\equiv 1 \bmod \varphi(n) \
et &\equiv 1 \bmod bt \
ed &\equiv 1 \bmod \varphi(n)
\end{align}
$$

根据题中给出的条件,我们已经知道了 n,e,bt。

所以首先我们可以根据上面的第二个式子知道 e。这时候,可以利用第一个式子进行 Wiener’s Attack,获取 u。进而这时我们可以利用私钥指数泄露攻击的方法来分解 N 从而得到 p,q。进而我们就可以得到 d 了。

首先我们绕过 proof 得到了 N,e,加密后的 flag 如下

n:  0x4b4403cd5ac8bdfaa3bbf83decdc97db1fbc7615fd52f67a8acf7588945cd8c3627211ffd3964d979cb1ab3850348a453153710337c6fe3baa15d986c87fca1c97c6d270335b8a7ecae81ae0ebde48aa957e7102ce3e679423f29775eef5935006e8bc4098a52a168e07b75e431a796e3dcd29c98dab6971d3eac5b5b19fb4d2b32f8702ef97d92da547da2e22387f7555531af4327392ef9c82227c5a2479623dde06b525969e9480a39015a3ed57828162ca67e6d41fb7e79e1b25e56f1cff487c1d0e0363dc105512d75c83ad0085b75ede688611d489c1c2ea003c3b2f81722cdb307a3647f2da01fb3ba0918cc1ab88c67e1b6467775fa412de7be0b44f2e19036471b618db1415f6b656701f692c5e841d2f58da7fd2bc33e7c3c55fcb8fd980c9e459a6df44b0ef70b4b1d813a57530446aa054cbfb9d1a86ffb6074b6b7398a83b5f0543b910dcb9f111096b07a98830a3ce6da47cd36b7c1ac1b2104ea60dc198c34f1c50faa5b697f2f195afe8af5d455e8ac7ca6eda669a5a1e3bfbd290a4480376abd1ff21298d529b26a4e614ab24c776a10f5f5d8e8809467a3e81f04cf5d5b23eb4a3412886797cab4b3c5724c077354b2d11d19ae4e301cd2ca743e56456d2a785b650c7e1a727b1bd881ee85c8d109792393cc1a92a66b0bc23b164146548f4e184b10c80ec458b776df10405b65399e32d657bc83e1451
e:  0x10194521505692a64d043daaef7647e0efb1503ec89220a0e4148ab53ecf708146a8893a2e700e4f2f062be14a3ab4e46339a939d5c7289904cc0ab043320d3a4d7da868bf5736ae5f787d6c0e3d9b8cc4b81314ad6c5ff643bc0d8946fea7eb09bf707a54747a39df1cfc0c30849770578cb63de86621001ce86a11874c91419a4d07373e66e94f31b988cac3aeaff88c7abaf3b78468a434990f7854e734208a7461f8245660fa8301f979e85517d705302c797dbdf2938cc442b01c228939eb73aa29651a198a332af2bb982310699684e5a0595c7413ec01eefb3613a9ea4b59f1de984ad4bf6654960613c0f8104b4e41fb33384e07f715176d68f4bb7613b1258675e70dc774f701aee053830f0be28ba9f308c9fe1707a5ba07a2027d74144b8aeb4042df3c1d73d9c38c2d7d1a890fd70d6e38c72da5d075f3811c0354dcecdd836a59112a70be22757278c5e4973906aaeeadd6f61d0845d6f9761df191b0b2527d122dd07f8bd07f5cd14268246ac2b93b778c84b5157f7eb23a8eaa9f0f885f2a38e3fb8fd1012d9b6c841cea8d9d73b232bef298afd086c1063bdd11e0777c8d2ec91ae843a67a98039cb53fad0ee25040176841a017fabf79b98de21d40bc6985f82dd84406aad26e9ac9bc5f6e12385230d9620b888c201ca9c413cbf0f36b100a6c62c5c8f065934fcf9f9f0179eea35888cb357b704441c1
flag:  0x2517d1866acc5b7b802a51d6251673262e9e6b2d0e0e14a87b838c2751dee91e4ea29019b0a7877b849fddf9e08580d810622db538462b529412eba9d0f8a450fe1889021c0bbd12a62ccc3fff4627b1dbdebec3a356a066adc03f7650722a34fe41ea0a247cb480a12286fffc799d66b6631a220b8401f5f50daa12943856b35e59abf8457b2269efea14f1535fb95e56398fd5f3ac153e3ea1afd7b0bb5f02832883da46343404eb44594d04bbd254a9a35749af84eaf4e35ba1c5571d41cab4d58befa79b6745d8ecf93b64dd26056a6d1e82430afbff3dbc08d6c974364b57b30c8a8230c99f0ec3168ac4813c4205d9190481282ae14f7b94400caff3786ed35863b66fefcffbef1ad1652221746a5c8da083987b2b69689cf43e86a05ce4cf059934716c455a6410560e41149fbcf5fcea3c210120f106b8f6269b9a954139350626cf4dcb497ce86264e05565ec6c6581bf28c643bb4fab8677148c8034833cedacb32172b0ff21f363ca07de0fa2882ac896954251277adc0cdd0c3bd5a3f107dbebf5f4d884e43fe9b118bdd51dc80607608670507388ae129a71e0005826c7c82efccf9c86c96777d7d3b9b5cce425e3dcf9aec0643f003c851353e36809b9202ff3b79e8f33d40967c1d36f5d585ac9eba73611152fc6d3cf36fd9a60b4c621858ed1f6d4db86054c27828e22357fa3d7c71559d175ff8e8987df

其次使用如下方法进行 Wiener’s Attack 得到 u,如下

if __name__ == "__main__":
    bt = 536380958350616057242691418634880594502192106332317228051967064327642091297687630174183636288378234177476435270519631690543765125295554448698898712393467267006465045949611180821007306678935181142803069337672948471202242891010188677287454504933695082327796243976863378333980923047411230913909715527759877351702062345876337256220760223926254773346698839492268265110546383782370744599490250832085044856878026833181982756791595730336514399767134613980006467147592898197961789187070786602534602178082726728869941829230655559180178594489856595304902790182697751195581218334712892008282605180395912026326384913562290014629187579128041030500771670510157597682826798117937852656884106597180126028398398087318119586692935386069677459788971114075941533740462978961436933215446347246886948166247617422293043364968298176007659058279518552847235689217185712791081965260495815179909242072310545078116020998113413517429654328367707069941427368374644442366092232916196726067387582032505389946398237261580350780769275427857010543262176468343294217258086275244086292475394366278211528621216522312552812343261375050388129743012932727654986046774759567950981007877856194574274373776538888953502272879816420369255752871177234736347325263320696917012616273L
    e = 0x10194521505692a64d043daaef7647e0efb1503ec89220a0e4148ab53ecf708146a8893a2e700e4f2f062be14a3ab4e46339a939d5c7289904cc0ab043320d3a4d7da868bf5736ae5f787d6c0e3d9b8cc4b81314ad6c5ff643bc0d8946fea7eb09bf707a54747a39df1cfc0c30849770578cb63de86621001ce86a11874c91419a4d07373e66e94f31b988cac3aeaff88c7abaf3b78468a434990f7854e734208a7461f8245660fa8301f979e85517d705302c797dbdf2938cc442b01c228939eb73aa29651a198a332af2bb982310699684e5a0595c7413ec01eefb3613a9ea4b59f1de984ad4bf6654960613c0f8104b4e41fb33384e07f715176d68f4bb7613b1258675e70dc774f701aee053830f0be28ba9f308c9fe1707a5ba07a2027d74144b8aeb4042df3c1d73d9c38c2d7d1a890fd70d6e38c72da5d075f3811c0354dcecdd836a59112a70be22757278c5e4973906aaeeadd6f61d0845d6f9761df191b0b2527d122dd07f8bd07f5cd14268246ac2b93b778c84b5157f7eb23a8eaa9f0f885f2a38e3fb8fd1012d9b6c841cea8d9d73b232bef298afd086c1063bdd11e0777c8d2ec91ae843a67a98039cb53fad0ee25040176841a017fabf79b98de21d40bc6985f82dd84406aad26e9ac9bc5f6e12385230d9620b888c201ca9c413cbf0f36b100a6c62c5c8f065934fcf9f9f0179eea35888cb357b704441c1
    t = gmpy2.invert(e, bt)
    n = 0x4b4403cd5ac8bdfaa3bbf83decdc97db1fbc7615fd52f67a8acf7588945cd8c3627211ffd3964d979cb1ab3850348a453153710337c6fe3baa15d986c87fca1c97c6d270335b8a7ecae81ae0ebde48aa957e7102ce3e679423f29775eef5935006e8bc4098a52a168e07b75e431a796e3dcd29c98dab6971d3eac5b5b19fb4d2b32f8702ef97d92da547da2e22387f7555531af4327392ef9c82227c5a2479623dde06b525969e9480a39015a3ed57828162ca67e6d41fb7e79e1b25e56f1cff487c1d0e0363dc105512d75c83ad0085b75ede688611d489c1c2ea003c3b2f81722cdb307a3647f2da01fb3ba0918cc1ab88c67e1b6467775fa412de7be0b44f2e19036471b618db1415f6b656701f692c5e841d2f58da7fd2bc33e7c3c55fcb8fd980c9e459a6df44b0ef70b4b1d813a57530446aa054cbfb9d1a86ffb6074b6b7398a83b5f0543b910dcb9f111096b07a98830a3ce6da47cd36b7c1ac1b2104ea60dc198c34f1c50faa5b697f2f195afe8af5d455e8ac7ca6eda669a5a1e3bfbd290a4480376abd1ff21298d529b26a4e614ab24c776a10f5f5d8e8809467a3e81f04cf5d5b23eb4a3412886797cab4b3c5724c077354b2d11d19ae4e301cd2ca743e56456d2a785b650c7e1a727b1bd881ee85c8d109792393cc1a92a66b0bc23b164146548f4e184b10c80ec458b776df10405b65399e32d657bc83e1451
    solve(n, t)

其中 solve 函数就是对应的 Wiener’s Attack 的函数。

我们得到了 u,如下

➜  rsa-wiener-attack git:(master) ✗ python RSAwienerHacker.py
Testing Wiener Attack
Hacked!
('hacked_d = ', mpz(404713159471231711408151571380906751680333129144247165378555186876078301457022630947986647887431519481527070603810696638453560506186951324208972060991323925955752760273325044674073649258563488270334557390141102174681693044992933206572452629140703447755138963985034199697200260653L))
-------------------------
Hacked!
('hacked_d = ', mpz(404713159471231711408151571380906751680333129144247165378555186876078301457022630947986647887431519481527070603810696638453560506186951324208972060991323925955752760273325044674073649258563488270334557390141102174681693044992933206572452629140703447755138963985034199697200260653L))
-------------------------
Hacked!
('hacked_d = ', mpz(404713159471231711408151571380906751680333129144247165378555186876078301457022630947986647887431519481527070603810696638453560506186951324208972060991323925955752760273325044674073649258563488270334557390141102174681693044992933206572452629140703447755138963985034199697200260653L))
-------------------------
Hacked!
('hacked_d = ', mpz(404713159471231711408151571380906751680333129144247165378555186876078301457022630947986647887431519481527070603810696638453560506186951324208972060991323925955752760273325044674073649258563488270334557390141102174681693044992933206572452629140703447755138963985034199697200260653L))
-------------------------
Hacked!
('hacked_d = ', mpz(404713159471231711408151571380906751680333129144247165378555186876078301457022630947986647887431519481527070603810696638453560506186951324208972060991323925955752760273325044674073649258563488270334557390141102174681693044992933206572452629140703447755138963985034199697200260653L))

接着利用 RsaConverter 以及 u,t,n 获取对应的 p 和 q。如下

94121F49C0E7A37A60FDE4D13F021675ED91032EB16CB070975A3EECECE8697ED161A27D86BCBC4F45AA6CDC128EB878802E0AD3B95B2961138C8CD04D28471B558CD816279BDCCF8FA1513A444AF364D8FDA8176A4E459B1B939EBEC6BB164F06CDDE9C203C612541E79E8B6C266436AB903209F5C63C8F0DA192F129F0272090CBE1A37E2615EF7DFBB05D8D88B9C964D5A42A7E0D6D0FF344303C4364C894AB7D912065ABC30815A3B8E0232D1B3D7F6B80ED7FE4B71C3477E4D6C2C78D733CF23C694C535DB172D2968483E63CC031DFC5B27792E2235C625EC0CFDE33FD3E53915357772975D264D24A7F31308D72E1BD7656B1C16F58372E7682660381
8220863F1CFDA6EDE52C56B4036485DB53F57A4629F5727EDC4C5637603FE059EB44751FC49EC846C0B8B50966678DFFB1CFEB350EC44B57586A81D35E4887F1722367CE99116092463079A63E3F29D4F4BC416E7728B26248EE8CD2EFEA6925EC6F455DF966CEE13C808BC15CA2A6AAC7FEA69DB7C9EB9786B50EBD437D38B73D44F3687AEB5DF03B6F425CF3171B098AAC6708D534F4D3A9B3D43BAF70316812EF95FC7EBB7E224A7016D7692B52CB0958951BAB4FB5CB1ABB4DAC606F03FA15697CC3E9DF26DE5F6D6EC45A683CD5AAFD58D416969695067795A2CF7899F61669BC7543151AB700A593BF5A1E5C2AFBCE45A08A2A9CC1685FAF1F96B138D1

然后我们直接去获得 d,进而就可以恢复明文

    p = 0x94121F49C0E7A37A60FDE4D13F021675ED91032EB16CB070975A3EECECE8697ED161A27D86BCBC4F45AA6CDC128EB878802E0AD3B95B2961138C8CD04D28471B558CD816279BDCCF8FA1513A444AF364D8FDA8176A4E459B1B939EBEC6BB164F06CDDE9C203C612541E79E8B6C266436AB903209F5C63C8F0DA192F129F0272090CBE1A37E2615EF7DFBB05D8D88B9C964D5A42A7E0D6D0FF344303C4364C894AB7D912065ABC30815A3B8E0232D1B3D7F6B80ED7FE4B71C3477E4D6C2C78D733CF23C694C535DB172D2968483E63CC031DFC5B27792E2235C625EC0CFDE33FD3E53915357772975D264D24A7F31308D72E1BD7656B1C16F58372E7682660381
    q = 0x8220863F1CFDA6EDE52C56B4036485DB53F57A4629F5727EDC4C5637603FE059EB44751FC49EC846C0B8B50966678DFFB1CFEB350EC44B57586A81D35E4887F1722367CE99116092463079A63E3F29D4F4BC416E7728B26248EE8CD2EFEA6925EC6F455DF966CEE13C808BC15CA2A6AAC7FEA69DB7C9EB9786B50EBD437D38B73D44F3687AEB5DF03B6F425CF3171B098AAC6708D534F4D3A9B3D43BAF70316812EF95FC7EBB7E224A7016D7692B52CB0958951BAB4FB5CB1ABB4DAC606F03FA15697CC3E9DF26DE5F6D6EC45A683CD5AAFD58D416969695067795A2CF7899F61669BC7543151AB700A593BF5A1E5C2AFBCE45A08A2A9CC1685FAF1F96B138D1
    if p * q == n:
        print 'true'
    phin = (p - 1) * (q - 1)
    d = gmpy2.invert(e, phin)
    cipher = 0x2517d1866acc5b7b802a51d6251673262e9e6b2d0e0e14a87b838c2751dee91e4ea29019b0a7877b849fddf9e08580d810622db538462b529412eba9d0f8a450fe1889021c0bbd12a62ccc3fff4627b1dbdebec3a356a066adc03f7650722a34fe41ea0a247cb480a12286fffc799d66b6631a220b8401f5f50daa12943856b35e59abf8457b2269efea14f1535fb95e56398fd5f3ac153e3ea1afd7b0bb5f02832883da46343404eb44594d04bbd254a9a35749af84eaf4e35ba1c5571d41cab4d58befa79b6745d8ecf93b64dd26056a6d1e82430afbff3dbc08d6c974364b57b30c8a8230c99f0ec3168ac4813c4205d9190481282ae14f7b94400caff3786ed35863b66fefcffbef1ad1652221746a5c8da083987b2b69689cf43e86a05ce4cf059934716c455a6410560e41149fbcf5fcea3c210120f106b8f6269b9a954139350626cf4dcb497ce86264e05565ec6c6581bf28c643bb4fab8677148c8034833cedacb32172b0ff21f363ca07de0fa2882ac896954251277adc0cdd0c3bd5a3f107dbebf5f4d884e43fe9b118bdd51dc80607608670507388ae129a71e0005826c7c82efccf9c86c96777d7d3b9b5cce425e3dcf9aec0643f003c851353e36809b9202ff3b79e8f33d40967c1d36f5d585ac9eba73611152fc6d3cf36fd9a60b4c621858ed1f6d4db86054c27828e22357fa3d7c71559d175ff8e8987df
    flag = gmpy2.powmod(cipher, d, n)
    print long_to_bytes(flag)

得到 flag

true
hctf{d8e8fca2dc0f896fd7cb4cb0031ba249}

Coppersmith 相关攻击

基本原理

Coppersmith 相关攻击与Don Coppersmith 紧密相关,他提出了一种针对于模多项式(单变量,二元变量,甚至多元变量)找所有小整数根的多项式时间的方法。

这里我们以单变量为主进行介绍,假设

  • 模数为 N ,N 具有一个因子 $b\geq N^{\beta},0< \beta \leq 1$
  • 多项式 F 的次数为 $\delta$

那么该方法可以在$O(c\delta^5log^9(N))$ 的复杂度内找到该多项式所有的根$x_0$,这里我们要求 $|x_0|<cN^{\frac{\beta^2}{\delta}}$ 。

在这个问题中,我们的目标是找到在模 N 意义下多项式所有的根,这一问题被认为是复杂的。Coppersmith method 主要是通过 Lenstra–Lenstra–Lovász lattice basis reduction algorithm(LLL)方法找到

  • 与该多项式具有相同根 $x_0$
  • 更小系数
  • 定义域为整数域

的多项式 g,由于在整数域上找多项式的根是简单的(Berlekamp–Zassenhaus),从而我们就得到了原多项式在模意义下的整数根。

那么问题的关键就是如何将 f 转换到 g 呢?Howgrave-Graham 给出了一种思路

也就是说我们需要找到一个具有“更小系数”的多项式 g,也就是下面的转换方式

在 LLL 算法中,有两点是非常有用的

  • 只对原来的基向量进行整数线性变换,这可以使得我们在得到 g 时,仍然以原来的 $x_0$ 为根。
  • 生成的新的基向量的模长是有界的,这可以使得我们利用 Howgrave-Graham 定理。

在这样的基础之上,我们再构造出多项式族 g 就可以了。

关于更加细节的内容,请自行搜索。同时这部分内容也会不断更新。

需要注意的是,由于 Coppersmith 根的约束,在 RSA 中的应用时,往往只适用于 e 较小的情况。

Basic Broadcast Attack

攻击条件

如果一个用户使用同一个加密指数 e 加密了同一个密文,并发送给了其他 e 个用户。那么就会产生广播攻击。这一攻击由 Håstad 提出。

攻击原理

这里我们假设 e 为 3,并且加密者使用了三个不同的模数 $n_1,n_2,n_3$ 给三个不同的用户发送了加密后的消息 m,如下

$$
\begin{align}
c_1&=m^3\bmod n_1 \
c_2&=m^3\bmod n_2 \
c_3&=m^3\bmod n_3
\end{align}
$$

这里我们假设 $n_1,n_2,n_3$ 互素,不然,我们就可以直接进行分解,然后得到 d,进而然后直接解密。

同时,我们假设 $m<n_i, 1\leq i \leq 3$。如果这个条件不满足的话,就会使得情况变得比较复杂,这里我们暂不讨论。

既然他们互素,那么我们可以根据中国剩余定理,可得$m^3 \equiv C \bmod n_1n_2n_3$。

此外,既然 $m<n_i, 1\leq i \leq 3$,那么我们知道 $m^3 < n_1n_2n_3$ 并且 $C<m^3 < n_1n_2n_3$,那么 $m^3 = C$,我们对 C 开三次根即可得到 m 的值。

对于较大的 e 来说,我们只是需要更多的明密文对。

SCTF RSA3 LEVEL4

参考 http://ohroot.com/2016/07/11/rsa-in-ctf。

这里我们以 SCTF RSA3 中的 level4 为例进行介绍,首先编写代码提取 cap 包中的数据,如下

#!/usr/bin/env python

from scapy.all import *
import zlib
import struct

PA = 24
packets = rdpcap('./syc_security_system_traffic3.pcap')
client = '192.168.1.180'
list_n = []
list_m = []
list_id = []
data = []
for packet in packets:
    # TCP Flag PA 24 means carry data
    if packet[TCP].flags == PA or packet[TCP].flags == PA + 1:
        src = packet[IP].src
        raw_data = packet[TCP].load
        head = raw_data.strip()[:7]
        if head == "We have":
            n, e = raw_data.strip().replace("We have got N is ",
                                            "").split('\ne is ')
            data.append(n.strip())
        if head == "encrypt":
            m = raw_data.replace('encrypted messages is 0x', '').strip()
            data.append(str(int(m, 16)))

with open('./data.txt', 'w') as f:
    for i in range(0, len(data), 2):
        tmp = ','.join(s for s in data[i:i + 2])
        f.write(tmp + '\n')

其次,利用得到的数据直接使用中国剩余定理求解。

from functools import reduce
import gmpy
import json, binascii


def modinv(a, m):
    return int(gmpy.invert(gmpy.mpz(a), gmpy.mpz(m)))


def chinese_remainder(n, a):
    sum = 0
    prod = reduce(lambda a, b: a * b, n)
    # 并行运算
    for n_i, a_i in zip(n, a):
        p = prod // n_i
        sum += a_i * modinv(p, n_i) * p
    return int(sum % prod)


nset = []
cset = []
with open("data.txt") as f:
    now = f.read().strip('\n').split('\n')
    for item in now:
        item = item.split(',')
        nset.append(int(item[0]))
        cset.append(int(item[1]))

m = chinese_remainder(nset, cset)
m = int(gmpy.mpz(m).root(19)[0])
print binascii.unhexlify(hex(m)[2:-1])

得到密文,然后再次解密即可得到 flag。

H1sTaDs_B40aDcadt_attaCk_e_are_same_and_smA9l

题目

  • 2017 WHCTF OldDriver
  • 2018 N1CTF easy_fs

Broadcast Attack with Linear Padding

对于具有线性填充的情况下,仍然可以攻击,这时候就会使用 Coppersmith method 的方法了,这里暂不介绍。可以参考

攻击条件

当 Alice 使用同一公钥对两个具有某种线性关系的消息 M1 与 M2 进行加密,并将加密后的消息 C1,C2 发送给了 Bob 时,我们就可能可以获得对应的消息 M1 与 M2。这里我们假设模数为 N,两者之间的线性关系如下

$$
M_1 \equiv f(M_2) \bmod N
$$

其中 f 为一个线性函数,比如说 $f=ax+b$。

在具有较小错误概率下的情况下,其复杂度为 $O(elog^2N)$。

这一攻击由 Franklin,Reiter 提出。

攻击原理

首先,我们知道 $C_1 \equiv M_1 ^e \bmod N$,并且 $M_1 \equiv f(M_2) \bmod N$,那么我们可以知道 $M_2$ 是 $f(x)^e \equiv C_1 \bmod N$ 的一个解,即它是方程 $f(x)^e-C_1$ 在模 N 意义下的一个根。同样的,$M_2$ 是 $x^e - C_2$ 在模 N 意义下的一个根。所以说 $x-M_2$ 同时整除以上两个多项式。因此,我们可以求得两个多项式的最大公因子,如果最大公因子恰好是线性的话,那么我们就求得了 $M_2$。需要注意的是,在 $e=3$ 的情况下,最大公因子一定是线性的。

这里我们关注一下 $e=3$,且 $f(x)=ax+b$ 的情况。首先我们有

$$
C_1 \equiv M_1 ^3 \bmod N,M_1 \equiv aM_2+b \bmod N
$$

那么我们有

$$
C_1 \equiv (aM_2+b)^3 \bmod N,C_2 \equiv M_2^3 \bmod N
$$

我们需要明确一下我们想要得到的是消息 m,所以需要将其单独构造出来。

首先,我们有式 1

$$
(aM_2+b)^3=a^3M_2^3+3a^2M^2b+3aM_2b^2+b^3
$$

再者我们构造如下式 2

$$
(aM_2)^3-b^3 \equiv (aM_2-b)(a^2M_2^2+aM_2b+b^2) \bmod N
$$

根据式 1 我们有

$$
a^3M_2^3-2b^3+3b(a^2M_2^2+aM_2b+b^2) \equiv C_1 \bmod N
$$

继而我们有式 3

$$
3b(a^2M_2^2+aM_2b+b^2) \equiv C_1-a^3C_2+2b^3 \bmod N
$$

那么我们根据式 2 与式 3 可得

$$
(a^3C_2-b^3)*3b \equiv (aM_2-b)( C_1-a^3C_2+2b^3 ) \bmod N
$$

进而我们有

$$
aM_2-b=\frac{3a^3bC_2-3b^4}{C_1-a^3C_2+2b^3}
$$

进而

$$
aM_2\equiv \frac{2a^3bC_2-b^4+C_1b}{C_1-a^3C_2+2b^3}
$$

进而

$$
M_2 \equiv\frac{2a^3bC_2-b^4+C_1b}{aC_1-a^4C_2+2ab^3}=\frac{b}{a}\frac{C_1+2a^3C_2-b^3}{C_1-a^3C_2+2b^3}
$$

上面的式子中右边所有的内容都是已知的内容,所以我们可以直接获取对应的消息。

有兴趣的可以进一步阅读 A New Related Message Attack on RSA 以及 paper 这里暂不做过多的讲解。

SCTF RSA3

这里我们以 SCTF RSA3 中的 level3 为例进行介绍。首先,跟踪 TCP 流可以知道,加密方式是将明文加上用户的 user id 进行加密,而且还存在多组。这里我们选择第 0 组和第 9 组,他们的模数一样,解密脚本如下

import gmpy2
id1 = 1002
id2 = 2614

c1 = 0x547995f4e2f4c007e6bb2a6913a3d685974a72b05bec02e8c03ba64278c9347d8aaaff672ad8460a8cf5bffa5d787c5bb724d1cee07e221e028d9b8bc24360208840fbdfd4794733adcac45c38ad0225fde19a6a4c38e4207368f5902c871efdf1bdf4760b1a98ec1417893c8fce8389b6434c0fee73b13c284e8c9fb5c77e420a2b5b1a1c10b2a7a3545e95c1d47835c2718L
c2 = 0x547995f4e2f4c007e6bb2a6913a3d685974a72b05bec02e8c03ba64278c9347d8aaaff672ad8460a8cf5bffa5d787c72722fe4fe5a901e2531b3dbcb87e5aa19bbceecbf9f32eacefe81777d9bdca781b1ec8f8b68799b4aa4c6ad120506222c7f0c3e11b37dd0ce08381fabf9c14bc74929bf524645989ae2df77c8608d0512c1cc4150765ab8350843b57a2464f848d8e08L
n = 25357901189172733149625332391537064578265003249917817682864120663898336510922113258397441378239342349767317285221295832462413300376704507936359046120943334215078540903962128719706077067557948218308700143138420408053500628616299338204718213283481833513373696170774425619886049408103217179262264003765695390547355624867951379789924247597370496546249898924648274419164899831191925127182066301237673243423539604219274397539786859420866329885285232179983055763704201023213087119895321260046617760702320473069743688778438854899409292527695993045482549594428191729963645157765855337481923730481041849389812984896044723939553
a = 1
b = id1 - id2


def getmessage(a, b, c1, c2, n):
    b3 = gmpy2.powmod(b, 3, n)
    part1 = b * (c1 + 2 * c2 - b3) % n
    part2 = a * (c1 - c2 + 2 * b3) % n
    part2 = gmpy2.invert(part2, n)
    return part1 * part2 % n


message = getmessage(a, b, c1, c2, n) - id2
message = hex(message)[2:]
if len(message) % 2 != 0:
    message = '0' + message

print message.decode('hex')

得到明文

➜  sctf-rsa3-level3 git:(master) ✗ python exp.py
F4An8LIn_rElT3r_rELa53d_Me33Age_aTtaCk_e_I2_s7aLL

当然,我们也可以直接使用 sage 来做,会更加简单一点。

import binascii

def attack(c1, c2, b, e, n):
    PR.<x>=PolynomialRing(Zmod(n))
    g1 = x^e - c1
    g2 = (x+b)^e - c2

    def gcd(g1, g2):
        while g2:
            g1, g2 = g2, g1 % g2
        return g1.monic()
    return -gcd(g1, g2)[0]

c1 = 0x547995f4e2f4c007e6bb2a6913a3d685974a72b05bec02e8c03ba64278c9347d8aaaff672ad8460a8cf5bffa5d787c5bb724d1cee07e221e028d9b8bc24360208840fbdfd4794733adcac45c38ad0225fde19a6a4c38e4207368f5902c871efdf1bdf4760b1a98ec1417893c8fce8389b6434c0fee73b13c284e8c9fb5c77e420a2b5b1a1c10b2a7a3545e95c1d47835c2718L
c2 = 0x547995f4e2f4c007e6bb2a6913a3d685974a72b05bec02e8c03ba64278c9347d8aaaff672ad8460a8cf5bffa5d787c72722fe4fe5a901e2531b3dbcb87e5aa19bbceecbf9f32eacefe81777d9bdca781b1ec8f8b68799b4aa4c6ad120506222c7f0c3e11b37dd0ce08381fabf9c14bc74929bf524645989ae2df77c8608d0512c1cc4150765ab8350843b57a2464f848d8e08L
n = 25357901189172733149625332391537064578265003249917817682864120663898336510922113258397441378239342349767317285221295832462413300376704507936359046120943334215078540903962128719706077067557948218308700143138420408053500628616299338204718213283481833513373696170774425619886049408103217179262264003765695390547355624867951379789924247597370496546249898924648274419164899831191925127182066301237673243423539604219274397539786859420866329885285232179983055763704201023213087119895321260046617760702320473069743688778438854899409292527695993045482549594428191729963645157765855337481923730481041849389812984896044723939553
e=3
a = 1
id1 = 1002
id2 = 2614
b = id2 - id1
m1 = attack(c1,c2, b,e,n)
print binascii.unhexlify("%x" % int(m1 - id1))

结果如下

➜  sctf-rsa3-level3 git:(master) ✗ sage exp.sage
sys:1: RuntimeWarning: not adding directory '' to sys.path since everybody can write to it.
Untrusted users could put files in this directory which might then be imported by your Python code. As a general precaution from similar exploits, you should not execute Python code from this directory
F4An8LIn_rElT3r_rELa53d_Me33Age_aTtaCk_e_I2_s7aLL

题目

  • hitcon 2014 rsaha
  • N1CTF 2018 rsa_padding

Coppersmith’s short-pad attack

攻击条件

目前在大部分消息加密之前都会进行 padding,但是如果 padding 的长度过短,也有可能被很容易地攻击。

这里所谓 padding 过短,其实就是对应的多项式的根会过小。

攻击原理

我们假设爱丽丝要给鲍勃发送消息,首先爱丽丝对要加密的消息 M 进行随机 padding,然后加密得到密文 C1,发送给鲍勃。这时,中间人皮特截获了密文。一段时间后,爱丽丝没有收到鲍勃的回复,再次对要加密的消息 M 进行随机 padding,然后加密得到密文 C2,发送给 Bob。皮特再一次截获。这时,皮特就可能可以利用如下原理解密。

这里我们假设模数 N 的长度为 k,并且 padding 的长度为 $m=\lfloor \frac{k}{e^2} \rfloor$。此外,假设要加密的消息的长度最多为 k-m 比特,padding 的方式如下

$$
M_1=2^mM+r_1, 0\leq r_1\leq 2^m
$$

消息 M2 的 padding 方式类似。

那么我们可以利用如下的方式来解密。

首先定义

$$
g_1(x,y)=x^e-C_1
g_2(x,y)=(x+y)^e-C_2
$$

其中 $y=r_2-r_1$。显然这两个方程具有相同的根 M1。然后还有一系列的推导。

Known High Bits Message Attack

攻击条件

这里我们假设我们首先加密了消息 m,如下

$$
C\equiv m^d \bmod N
$$

并且我们假设我们知道消息 m 的很大的一部分 $m_0$,即 $m=m_0+x$,但是我们不知道 $x$。那么我们就有可能通过该方法进行恢复消息。这里我们不知道的 x 其实就是多项式的根,需要满足 Coppersmith 的约束。

可以参考 https://github.com/mimoo/RSA-and-LLL-attacks。

Factoring with High Bits Known

攻击条件

当我们知道一个公钥中模数 N 的一个因子的较高位时,我们就有一定几率来分解 N。

攻击工具

请参考 https://github.com/mimoo/RSA-and-LLL-attacks。上面有使用教程。关注下面的代码

beta = 0.5
dd = f.degree()
epsilon = beta / 7
mm = ceil(beta**2 / (dd * epsilon))
tt = floor(dd * mm * ((1/beta) - 1))
XX = ceil(N**((beta**2/dd) - epsilon)) + 1000000000000000000000000000000000
roots = coppersmith_howgrave_univariate(f, N, beta, mm, tt, XX)

其中,

  • 必须满足 $q\geq N^{beta}$,所以这里给出了$beta=0.5$,显然两个因数中必然有一个是大于的。
  • XX 是 $f(x)=q’+x$ 在模 q 意义下的根的上界,自然我们可以选择调整它,这里其实也表明了我们已知的 $q’$ 与因数 q 之间可能的差距。

2016 HCTF RSA2

这里我们以 2016 年 HCTF 中的 RSA2 为例进行介绍。

首先程序的开头是一个绕过验证的,绕过即可,代码如下

from pwn import *
from hashlib import sha512
sh = remote('127.0.0.1', 9999)
context.log_level = 'debug'
def sha512_proof(prefix, verify):
    i = 0
    pading = ""
    while True:
        try:
            i = randint(0, 1000)
            pading += str(i)
            if len(pading) > 200:
                pading = pading[200:]
            #print pading
        except StopIteration:
            break
        r = sha512(prefix + pading).hexdigest()
        if verify in r:
            return pading


def verify():
    sh.recvuntil("Prefix: ")
    prefix = sh.recvline()
    print len(prefix)
    prefix = prefix[:-1]
    prefix = prefix.decode('base64')
    proof = sha512_proof(prefix, "fffffff")
    sh.send(proof.encode('base64'))
if __name__ == '__main__':
    verify()
    print 'verify success'
    sh.recvuntil("token: ")
    token = "5c9597f3c8245907ea71a89d9d39d08e"
    sh.sendline(token)

    sh.recvuntil("n: ")
    n = sh.readline().strip()
    n = int(n[2:], 16)

    sh.recvuntil("e: ")
    e = sh.readline().strip()
    e = int(e[2:], 16)

    sh.recvuntil("e2: ")
    e2 = sh.readline().strip()
    e2 = int(e2[2:], 16)

    sh.recvuntil("is: ")
    enc_flag = sh.readline().strip()
    enc_flag = int(enc_flag[2:-1], 16)
    print "n: ", hex(n)
    print "e: ", hex(e)
    print "e2: ", hex(e2)
    print "flag: ", hex(enc_flag)

这里我们也已经得到 n,e,e2,加密后的 flag 了,如下

n:  0x724d41149e1bd9d2aa9b333d467f2dfa399049a5d0b4ee770c9d4883123be11a52ff1bd382ad37d0ff8d58c8224529ca21c86e8a97799a31ddebd246aeeaf0788099b9c9c718713561329a8e529dfeae993036921f036caa4bdba94843e0a2e1254c626abe54dc3129e2f6e6e73bbbd05e7c6c6e9f44fcd0a496f38218ab9d52bf1f266004180b6f5b9bee7988c4fe5ab85b664280c3cfe6b80ae67ed8ba37825758b24feb689ff247ee699ebcc4232b4495782596cd3f29a8ca9e0c2d86ea69372944d027a0f485cea42b74dfd74ec06f93b997a111c7e18017523baf0f57ae28126c8824bd962052623eb565cee0ceee97a35fd8815d2c5c97ab9653c4553f
e:  0x10001
e2:  0xf93b
flag:  0xf11e932fa420790ca3976468dc4df1e6b20519ebfdc427c09e06940e1ef0ca566d41714dc1545ddbdcae626eb51c7fa52608384a36a2a021960d71023b5d0f63e6b38b46ac945ddafea42f01d24cc33ce16825df7aa61395d13617ae619dca2df15b5963c77d6ededf2fe06fd36ae8c5ce0e3c21d72f2d7f20cd9a8696fbb628df29299a6b836c418cbfe91e2b5be74bdfdb4efdd1b33f57ebb72c5246d5dce635529f1f69634d565a631e950d4a34a02281cbed177b5a624932c2bc02f0c8fd9afd332ccf93af5048f02b8bd72213d6a52930b0faa0926973883136d8530b8acf732aede8bb71cb187691ebd93a0ea8aeec7f82d0b8b74bcf010c8a38a1fa8

接下来我们来分析主程序。可以看出

    p, q, e = gen_key()
    n = p * q
    phi_n = (p-1)*(q-1)
    d = invmod(e, phi_n)
    while True:
        e2 = random.randint(0x1000, 0x10000)
        if gcd(e2, phi_n) == 1:
            break

我们得到的 $n=p \times q$。而 p,q 以及我们已知的 e 都在 gen_key 函数中生成。看一看 gen_key 函数

def gen_key():
    while True:
        p = getPrime(k/2)
        if gcd(e, p-1) == 1:
            break
    q_t = getPrime(k/2)
    n_t = p * q_t
    t = get_bit(n_t, k/16, 1)
    y = get_bit(n_t, 5*k/8, 0)
    p4 = get_bit(p, 5*k/16, 1)
    u = pi_b(p4, 1)
    n = bytes_to_long(long_to_bytes(t) + long_to_bytes(u) + long_to_bytes(y))
    q = n / p
    if q % 2 == 0:
        q += 1
    while True:
        if isPrime(q) and gcd(e, q-1) == 1:
            break
        m = getPrime(k/16) + 1
        q ^= m
    return (p, q, e)

其中我们已知如下参数

$$
k=2048
e=0x10001
$$

首先,程序先得到了 1024 比特位的素数 p,并且 gcd(2,p-1)=1

然后,程序又得到了一个 1024 比特位的素数 $q_t$,并且计算 $n_t=p \times q_t$。

下面多次调用了 get_bit 函数,我们来简单分析一下

def get_bit(number, n_bit, dire):
    '''
    dire:
        1: left
        0: right
    '''

    if dire:
        sn = size(number)
        if sn % 8 != 0:
            sn += (8 - sn % 8)
        return number >> (sn-n_bit)
    else:
        return number & (pow(2, n_bit) - 1)

可以看出根据 dire(ction) 的不同,会得到不同的数

  • dire=1 时,程序首先计算 number 的二进制位数 sn,如果不是 8 的整数倍的话,就将 sn 增大为 8 的整数倍,然后返回 number 右移 sn-n_bit 的数字。其实 就是最多保留 numbern_bit 位。
  • dire=0 时,程序直接获取 number 的低 n_bit 位。

然后我们再来看程序

    t = get_bit(n_t, k/16, 1)
    y = get_bit(n_t, 5*k/8, 0)
    p4 = get_bit(p, 5*k/16, 1)

这三个操作分别做了如下的事情

  • tn_t 的最多高 k/16 位,即 128 位,位数不固定。
  • yn_t 的低 5*k/8 位,即 1280 位,位数固定。
  • p4 为 p 的最多高 5*k/16 位,即 640 位,位数不固定。

此后,程序有如下操作

    u = pi_b(p4, 1)

利用 pi_bp4 进行了加密

def pi_b(x, m):
    '''
    m:
        1: encrypt
        0: decrypt
    '''
    enc = DES.new(key)
    if m:
        method = enc.encrypt
    else:
        method = enc.decrypt
    s = long_to_bytes(x)
    sp = [s[a:a+8] for a in xrange(0, len(s), 8)]
    r = ""
    for a in sp:
        r += method(a)
    return bytes_to_long(r)

其中,我们已知了密钥 key,所以只要我们有密文就可以解密。此外,可以看到的是程序是对传入的消息进行 8 字节分组,采用密码本方式加密,所以密文之间互不影响。

下面

    n = bytes_to_long(long_to_bytes(t) + long_to_bytes(u) + long_to_bytes(y))
    q = n / p
    if q % 2 == 0:
        q += 1
    while True:
        if isPrime(q) and gcd(e, q-1) == 1:
            break
        m = getPrime(k/16) + 1
        q ^= m
    return (p, q, e)

程序将 t,u,y 拼接在一起得到 n,进而,程序得到了 q,并对 q 的低 k/16 位做了抑或,然后返回 q'

在主程序里,再一次得到了 n'=p*q'。这里我们仔细分析一下

n'=p * ( q + random(2^{k/16}))

而 p 是 k/2 位的,所以说,random 的部分最多可以影响原来的 n 的最低的 $k/2+k/16=9k/16$ 比特位。

而,我们还知道 n 的最低的 5k/8=10k/16 比特为其实就是 y,所以其并没有影响到 u,即使影响到也就最多影响到一位。

所以我们首先可以利用我们得到的 n 来获取 u,如下

u=hex(n)[2:-1][-480:-320]

虽然,这样可能会获得较多位数的 u,但是这样并不影响,我们对 u 解密的时候每一分组都互不影响,所以我们只可能影响最高位数的 p4。而 p4 的的高 8 位也有可能是填充的。但这也并不影响,我们已经得到了因子 p 的的很多部分了,我们可以去尝试着解密了。如下

if __name__=="__main__":
    n = 0x724d41149e1bd9d2aa9b333d467f2dfa399049a5d0b4ee770c9d4883123be11a52ff1bd382ad37d0ff8d58c8224529ca21c86e8a97799a31ddebd246aeeaf0788099b9c9c718713561329a8e529dfeae993036921f036caa4bdba94843e0a2e1254c626abe54dc3129e2f6e6e73bbbd05e7c6c6e9f44fcd0a496f38218ab9d52bf1f266004180b6f5b9bee7988c4fe5ab85b664280c3cfe6b80ae67ed8ba37825758b24feb689ff247ee699ebcc4232b4495782596cd3f29a8ca9e0c2d86ea69372944d027a0f485cea42b74dfd74ec06f93b997a111c7e18017523baf0f57ae28126c8824bd962052623eb565cee0ceee97a35fd8815d2c5c97ab9653c4553f
    u = hex(n)[2:-1][-480:-320]
    u = int(u,16)
    p4 = pi_b(u,0)
    print hex(p4)

解密结果如下

2016-HCTF-RSA2 git:(master) ✗ python exp_p4.py
0xa37302107c17fb4ef5c3443f4ef9e220ac659670077b9aa9ff7381d11073affe9183e88acae0ab61fb75a3c7815ffcb1b756b27c4d90b2e0ada753fa17cc108c1d0de82c747db81b9e6f49bde1362693L

下面,我们直接使用 sage 来解密,这里 sage 里面已经实现了这个攻击,我们直接拿来用就好

from sage.all import *
import binascii
n = 0x724d41149e1bd9d2aa9b333d467f2dfa399049a5d0b4ee770c9d4883123be11a52ff1bd382ad37d0ff8d58c8224529ca21c86e8a97799a31ddebd246aeeaf0788099b9c9c718713561329a8e529dfeae993036921f036caa4bdba94843e0a2e1254c626abe54dc3129e2f6e6e73bbbd05e7c6c6e9f44fcd0a496f38218ab9d52bf1f266004180b6f5b9bee7988c4fe5ab85b664280c3cfe6b80ae67ed8ba37825758b24feb689ff247ee699ebcc4232b4495782596cd3f29a8ca9e0c2d86ea69372944d027a0f485cea42b74dfd74ec06f93b997a111c7e18017523baf0f57ae28126c8824bd962052623eb565cee0ceee97a35fd8815d2c5c97ab9653c4553f
p4 =0xa37302107c17fb4ef5c3443f4ef9e220ac659670077b9aa9ff7381d11073affe9183e88acae0ab61fb75a3c7815ffcb1b756b27c4d90b2e0ada753fa17cc108c1d0de82c747db81b9e6f49bde1362693
cipher = 0xf11e932fa420790ca3976468dc4df1e6b20519ebfdc427c09e06940e1ef0ca566d41714dc1545ddbdcae626eb51c7fa52608384a36a2a021960d71023b5d0f63e6b38b46ac945ddafea42f01d24cc33ce16825df7aa61395d13617ae619dca2df15b5963c77d6ededf2fe06fd36ae8c5ce0e3c21d72f2d7f20cd9a8696fbb628df29299a6b836c418cbfe91e2b5be74bdfdb4efdd1b33f57ebb72c5246d5dce635529f1f69634d565a631e950d4a34a02281cbed177b5a624932c2bc02f0c8fd9afd332ccf93af5048f02b8bd72213d6a52930b0faa0926973883136d8530b8acf732aede8bb71cb187691ebd93a0ea8aeec7f82d0b8b74bcf010c8a38a1fa8
e2 = 0xf93b
pbits = 1024
kbits = pbits - p4.nbits()
print p4.nbits()
p4 = p4 << kbits
PR.<x> = PolynomialRing(Zmod(n))
f = x + p4
roots = f.small_roots(X=2^kbits, beta=0.4)
if roots:
    p = p4+int(roots[0])
    print "p: ", hex(int(p))
    assert n % p == 0
    q = n/int(p)
    print "q: ", hex(int(q))
    print gcd(p,q)
    phin = (p-1)*(q-1)
    print gcd(e2,phin)
    d = inverse_mod(e2,phin)
    flag = pow(cipher,d,n)
    flag = hex(int(flag))[2:-1]
    print binascii.unhexlify(flag)

关于 small_roots 的使用,可以参考 SAGE 说明

结果如下

➜  2016-HCTF-RSA2 git:(master) ✗ sage payload.sage
sys:1: RuntimeWarning: not adding directory '' to sys.path since everybody can write to it.
Untrusted users could put files in this directory which might then be imported by your Python code. As a general precaution from similar exploits, you should not execute Python code from this directory
640
p:  0xa37302107c17fb4ef5c3443f4ef9e220ac659670077b9aa9ff7381d11073affe9183e88acae0ab61fb75a3c7815ffcb1b756b27c4d90b2e0ada753fa17cc108c1d0de82c747db81b9e6f49bde13626933aa6762057e1df53d27356ee6a09b17ef4f4986d862e3bb24f99446a0ab2385228295f4b776c1f391ab2a0d8c0dec1e5L
q:  0xb306030a7c6ace771db8adb45fae597f3c1be739d79fd39dfa6fd7f8c177e99eb29f0462c3f023e0530b545df6e656dadb984953c265b26f860b68aa6d304fa403b0b0e37183008592ec2a333c431e2906c9859d7cbc4386ef4c4407ead946d855ecd6a8b2067ad8a99b21111b26905fcf0d53a1b893547b46c3142b06061853L
1
1
hctf{d8e8fca2dc0f896fd7cb4cb0031ba249}

题目

  • 2016 湖湘杯 简单的 RSA
  • 2017 WHCTF Untitled

Boneh and Durfee attack

攻击条件

当 d 较小时,满足 $d < N^{0.292}$ 时,我们可以利用该攻击,比 Wiener’s Attack 要强一些。

攻击原理

这里简单说一下原理。

首先

$$
ed \equiv 1 \bmod \varphi(N)/2
$$

进而有

$$
ed +k\varphi(N)/2=1
$$

$$
k \varphi(N)/2 \equiv 1 \bmod e
$$

$$
\varphi(N)=(p-1)(q-1)=qp-p-q+1=N-p-q+1
$$

所以

$$
k(N-p-q+1)/2 \equiv 1 \bmod e
$$

假设 $A=\frac{N+1}{2}$,$y=\frac{-p-q}{2}$ ,原式可化为

$$
f(k,y)=k(A+y) \equiv 1 \bmod e
$$

其中

$|k|<\frac{2ed}{\varphi(N)}<\frac{3ed}{N}=3\frac{e}{N}d<3\frac{e}{N}N^{delta}$

$|y|<2*N^{0.5}$

y 的估计用到了 p、q 比较均匀的假设。这里 delta 为预估的小于 0.292 的值。

如果我们求得了该二元方程的根,那么我们自然也就可以解一元二次方程 $N=pq,p+q=-2y$ 来得到 p 与 q。

更加具体的推导,参考 New Results on the Cryptanalysis of Low Exponent RSA.

攻击工具

请参考 https://github.com/mimoo/RSA-and-LLL-attacks 。上面有使用教程。

2015 PlaidCTF Curious

这里我们以 2015 年 PlaidCTF Curious 为例进行介绍。

首先题目给了一堆 N,e,c。简单看一下可以发现该 e 比较大。这时候我们可以考虑使用 Wiener’s Attack,这里我们使用更强的目前介绍的攻击。

核心代码如下

    nlist = list()
    elist = list()
    clist = list()
    with open('captured') as f:
        # read the line {N : e : c} and do nothing with it
        f.readline()
        for i in f.readlines():
            (N, e, c) = i[1:-2].split(" : ")
            nlist.append(long(N,16))
            elist.append(long(e,16))
            clist.append(long(c,16))

    for i in range(len(nlist)):
        print 'index i'
        n = nlist[i]
        e = elist[i]
        c = clist[i]
        d = solve(n,e)
        if d==0:
            continue
        else:
            m = power_mod(c, d, n)
            hex_string = "%x" % m
            import binascii
            print "the plaintext:", binascii.unhexlify(hex_string)
            return

结果如下

=== solution found ===
private key found: 23974584842546960047080386914966001070087596246662608796022581200084145416583
the plaintext: flag_S0Y0UKN0WW13N3R$4TT4CK!

2019 Defcon Quals ASRybaB

题目大概意思是,我们接收三对 RSA ,然后需要求出 d,然后对给定的数字 v[i] 加密,发送给服务器,只要时间在一定范围内,940s,即可。那难点自然在 create_key 函数了。

def send_challenges():

    code = marshal.loads("63000000000d000000070000004300000073df010000740000721d0064010064020015000000000100640200157d00006e00007401007d01007c0100640300157d02006402007d0300786f007c03006a02008300007c01006b030072a400784c007403007296007404006a05007c02008301007d04007404006a05007c02008301007d05007406007c04007c0500188301006a02008300007c0100640400146b0400724b0050714b00714b00577c04007c0500147d0300713600577c0400640500187c050064050018147d06006406007d07006407007d080078090174030072ce017404006a07007408006403007409007c01007c0700148301008302007408006403007409007c01007c070014830100640500178302008302007d09007871007c09006a02008300007c01007c0800146b0000727b016402007d0a007844007404006a0a007c0a00830100736d017404006a0700740800640300640800830200740800640300640800830200740800640300640900830200178302007d0a00712a01577c09007c0a00397d0900710b01577404006a0b007c09007c06008302006405006b0300729a0171c6006e00007404006a0c007c09007c06008302007d0b007404006a0b007c0b007c06008302006405006b030072ca0171c6006e00005071c60057640a007d0c007c03007c0b0066020053280b0000004e690700000069000000006902000000675839b4c876bedf3f6901000000674e62105839b4d03f678d976e1283c0d23f692d000000690c0000006903000000280d000000740500000046616c736574050000004e53495a45740a0000006269745f6c656e67746874040000005472756574060000006e756d626572740e0000006765745374726f6e675072696d657403000000616273740e00000067657452616e646f6d52616e67657403000000706f777403000000696e74740700000069735072696d6574030000004743447407000000696e7665727365280d00000074010000007874050000004e73697a657406000000707173697a6574010000004e740100000070740100000071740300000070686974060000006c696d69743174060000006c696d697432740100000064740300000070707074010000006574030000007a7a7a2800000000280000000073150000002f6f726967696e616c6368616c6c656e67652e7079740a0000006372656174655f6b657917000000733e000000000106010a010d0206010a010601150109010f010f04200108010e0112020601060109013c0119010601120135020e011801060112011801060105020604".decode("hex"))
    create_key = types.FunctionType(code, globals(), "create_key")

    ck = create_key

我们可以简单看看这个到底是在干啥

>>> import marshal
>>> data="63000000000d000000070000004300000073df010000740000721d0064010064020015000000000100640200157d00006e00007401007d01007c0100640300157d02006402007d0300786f007c03006a02008300007c01006b030072a400784c007403007296007404006a05007c02008301007d04007404006a05007c02008301007d05007406007c04007c0500188301006a02008300007c0100640400146b0400724b0050714b00714b00577c04007c0500147d0300713600577c0400640500187c050064050018147d06006406007d07006407007d080078090174030072ce017404006a07007408006403007409007c01007c0700148301008302007408006403007409007c01007c070014830100640500178302008302007d09007871007c09006a02008300007c01007c0800146b0000727b016402007d0a007844007404006a0a007c0a00830100736d017404006a0700740800640300640800830200740800640300640800830200740800640300640900830200178302007d0a00712a01577c09007c0a00397d0900710b01577404006a0b007c09007c06008302006405006b0300729a0171c6006e00007404006a0c007c09007c06008302007d0b007404006a0b007c0b007c06008302006405006b030072ca0171c6006e00005071c60057640a007d0c007c03007c0b0066020053280b0000004e690700000069000000006902000000675839b4c876bedf3f6901000000674e62105839b4d03f678d976e1283c0d23f692d000000690c0000006903000000280d000000740500000046616c736574050000004e53495a45740a0000006269745f6c656e67746874040000005472756574060000006e756d626572740e0000006765745374726f6e675072696d657403000000616273740e00000067657452616e646f6d52616e67657403000000706f777403000000696e74740700000069735072696d6574030000004743447407000000696e7665727365280d00000074010000007874050000004e73697a657406000000707173697a6574010000004e740100000070740100000071740300000070686974060000006c696d69743174060000006c696d697432740100000064740300000070707074010000006574030000007a7a7a2800000000280000000073150000002f6f726967696e616c6368616c6c656e67652e7079740a0000006372656174655f6b657917000000733e000000000106010a010d0206010a010601150109010f010f04200108010e0112020601060109013c0119010601120135020e011801060112011801060105020604"
>>> code=marshal.loads(data)
>>> code=marshal.loads(data.decode('hex'))
>>> import dis
>>> dis.dis(code)
 24           0 LOAD_GLOBAL              0 (False)
              3 POP_JUMP_IF_FALSE       29

 25           6 LOAD_CONST               1 (7)
              9 LOAD_CONST               2 (0)
             12 BINARY_DIVIDE
             13 STOP_CODE
             14 STOP_CODE
             15 STOP_CODE
...
 56         428 LOAD_GLOBAL              4 (number)
            431 LOAD_ATTR               11 (GCD)
            434 LOAD_FAST               11 (e)
            437 LOAD_FAST                6 (phi)
            440 CALL_FUNCTION            2
            443 LOAD_CONST               5 (1)
            446 COMPARE_OP               3 (!=)
            449 POP_JUMP_IF_FALSE      458
...

基本可以猜出来这是在生成 n,e,d,其实和我们最初的预期也差不多。我们来直接反编译一下

>>> from uncompyle6 import code_deparse
>>> code_deparse(code)
Instruction context:

  25       6  LOAD_CONST            1  7
              9  LOAD_CONST            2  0
             12  BINARY_DIVIDE
->           13  STOP_CODE
             14  STOP_CODE
             15  STOP_CODE
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python2.7/site-packages/uncompyle6/semantics/pysource.py", line 2310, in code_deparse
    deparsed.ast = deparsed.build_ast(tokens, customize, isTopLevel=isTopLevel)
  File "/usr/local/lib/python2.7/site-packages/uncompyle6/semantics/pysource.py", line 2244, in build_ast
    raise ParserError(e, tokens)
uncompyle6.semantics.parser_error.ParserError: --- This code section failed: ---
...
 64     469  LOAD_FAST             3  'N'
         472  LOAD_FAST            11  'e'
         475  BUILD_TUPLE_2         2  None
         478  RETURN_VALUE
          -1  RETURN_LAST

Parse error at or near `STOP_CODE' instruction at offset 13

可以发现 STOP_CODE,有点猫腻,如果仔细看最初的反汇编的话,我们可以发现最前面的那部分代码是在混淆

>>> dis.dis(code)
 24           0 LOAD_GLOBAL              0 (False)
              3 POP_JUMP_IF_FALSE       29

 25           6 LOAD_CONST               1 (7)
              9 LOAD_CONST               2 (0)
             12 BINARY_DIVIDE
             13 STOP_CODE
             14 STOP_CODE
             15 STOP_CODE

 26          16 STOP_CODE
             17 POP_TOP
             18 STOP_CODE
             19 LOAD_CONST               2 (0)
             22 BINARY_DIVIDE
             23 STORE_FAST               0 (x)
             26 JUMP_FORWARD             0 (to 29)

 28     >>   29 LOAD_GLOBAL              1 (NSIZE)
             32 STORE_FAST               1 (Nsize)

 29          35 LOAD_FAST                1 (Nsize)
             38 LOAD_CONST               3 (2)
             41 BINARY_DIVIDE
             42 STORE_FAST               2 (pqsize)

一直到

 29          35 LOAD_FAST                1 (Nsize)

前面的都没有什么作用,感觉是出题者故意修改了代码。仔细分析一下这部分代码,感觉像是两部分

# part 1
 25           6 LOAD_CONST               1 (7)
              9 LOAD_CONST               2 (0)
             12 BINARY_DIVIDE
             13 STOP_CODE
             14 STOP_CODE
             15 STOP_CODE
# part 2
 26          16 STOP_CODE
             17 POP_TOP
             18 STOP_CODE
             19 LOAD_CONST               2 (0)
             22 BINARY_DIVIDE
             23 STORE_FAST               0 (x)
             26 JUMP_FORWARD             0 (to 29)

正好是第 25 行和第 26 行,大概猜一猜,感觉两个都是 x=7/0,所以就想办法把这部分的代码修复一下,接下来就是定位这部分代码了。根据手册可以知道 STOP_CODE 是 0,从而我们可以定位第 25 行语句到 26 行语句为 t[6:26],他们分别都是 10 字节(6-15,16-25)。

>>> t=code.co_code
>>> t
't\x00\x00r\x1d\x00d\x01\x00d\x02\x00\x15\x00\x00\x00\x00\x01\x00d\x02\x00\x15}\x00\x00n\x00\x00t\x01\x00}\x01\x00|\x01\x00d\x03\x00\x15}\x02\x00d\x02\x00}\x03\x00xo\x00|\x03\x00j\x02\x00\x83\x00\x00|\x01\x00k\x03\x00r\xa4\x00xL\x00t\x03\x00r\x96\x00t\x04\x00j\x05\x00|\x02\x00\x83\x01\x00}\x04\x00t\x04\x00j\x05\x00|\x02\x00\x83\x01\x00}\x05\x00t\x06\x00|\x04\x00|\x05\x00\x18\x83\x01\x00j\x02\x00\x83\x00\x00|\x01\x00d\x04\x00\x14k\x04\x00rK\x00PqK\x00qK\x00W|\x04\x00|\x05\x00\x14}\x03\x00q6\x00W|\x04\x00d\x05\x00\x18|\x05\x00d\x05\x00\x18\x14}\x06\x00d\x06\x00}\x07\x00d\x07\x00}\x08\x00x\t\x01t\x03\x00r\xce\x01t\x04\x00j\x07\x00t\x08\x00d\x03\x00t\t\x00|\x01\x00|\x07\x00\x14\x83\x01\x00\x83\x02\x00t\x08\x00d\x03\x00t\t\x00|\x01\x00|\x07\x00\x14\x83\x01\x00d\x05\x00\x17\x83\x02\x00\x83\x02\x00}\t\x00xq\x00|\t\x00j\x02\x00\x83\x00\x00|\x01\x00|\x08\x00\x14k\x00\x00r{\x01d\x02\x00}\n\x00xD\x00t\x04\x00j\n\x00|\n\x00\x83\x01\x00sm\x01t\x04\x00j\x07\x00t\x08\x00d\x03\x00d\x08\x00\x83\x02\x00t\x08\x00d\x03\x00d\x08\x00\x83\x02\x00t\x08\x00d\x03\x00d\t\x00\x83\x02\x00\x17\x83\x02\x00}\n\x00q*\x01W|\t\x00|\n\x009}\t\x00q\x0b\x01Wt\x04\x00j\x0b\x00|\t\x00|\x06\x00\x83\x02\x00d\x05\x00k\x03\x00r\x9a\x01q\xc6\x00n\x00\x00t\x04\x00j\x0c\x00|\t\x00|\x06\x00\x83\x02\x00}\x0b\x00t\x04\x00j\x0b\x00|\x0b\x00|\x06\x00\x83\x02\x00d\x05\x00k\x03\x00r\xca\x01q\xc6\x00n\x00\x00Pq\xc6\x00Wd\n\x00}\x0c\x00|\x03\x00|\x0b\x00f\x02\x00S'
>>> t[6:26]
'd\x01\x00d\x02\x00\x15\x00\x00\x00\x00\x01\x00d\x02\x00\x15}\x00\x00'
>>> t[-3:]
'\x02\x00S'
>>> t='d\x01\x00d\x02\x00\x15\x00\x00\x00\x00\x01\x00d\x02\x00\x15}\x00\x00'
>>> t[-3:]
'}\x00\x00'
>>> t[:7]+t[-3:]
'd\x01\x00d\x02\x00\x15}\x00\x00'
>>> _.encode('hex')
'640100640200157d0000'

从而我们可以修复原 code

>>> data.find('640100')
56
>>> data1=data[:56]+'640100640200157d0000640100640200157d0000'+data[56+40:]
>>> code1=marshal.loads(data1.decode('hex'))
>>> code_deparse(code1)
if False:
    x = 7 / 0
    x = 7 / 0
Nsize = NSIZE
pqsize = Nsize / 2
N = 0
while N.bit_length() != Nsize:
    while True:
        p = number.getStrongPrime(pqsize)
        q = number.getStrongPrime(pqsize)
        if abs(p - q).bit_length() > Nsize * 0.496:
            break

    N = p * q

phi = (p - 1) * (q - 1)
limit1 = 0.261
limit2 = 0.293
while True:
    d = number.getRandomRange(pow(2, int(Nsize * limit1)), pow(2, int(Nsize * limit1) + 1))
    while d.bit_length() < Nsize * limit2:
        ppp = 0
        while not number.isPrime(ppp):
            ppp = number.getRandomRange(pow(2, 45), pow(2, 45) + pow(2, 12))

        d *= ppp

    if number.GCD(d, phi) != 1:
        continue
    e = number.inverse(d, phi)
    if number.GCD(e, phi) != 1:
        continue
    break

zzz = 3
return (
 N, e)<uncompyle6.semantics.pysource.SourceWalker object at 0x10a0ea110>

可以看到生成的 d 是故意超了 0.292 的,不过我们可以发现 ppp 范围很小,实际上我们可以测试得到这个范围的素数为 125 个。并且

1280*0.261+45=379.08000000000004>375.03999999999996=1280*0.293

所以其实这里就乘了一个数,那么我们其实就可以枚举一下乘了什么,并修改 e1=e*ppp,其实就回归到标准的 Boneh and Durfee attack。

但是,如果我们直接使用 https://github.com/mimoo/RSA-and-LLL-attacks 的脚本也不行,必须得提高 m,基本得提到 8,这样仍然不是很稳定。

如果仔细尝试尝试的话,就会发现 e1>N,这看起来问题不大,但是原脚本里假设的数值是 e<N 的,所以我们需要进行适当的修改预估的上下界

    X = 2*floor(N^delta)  # this _might_ be too much
    Y = floor(N^(1/2))    # correct if p, q are ~ same size

根据上述推导,上下界应该为

$ |k|<\frac{2ed}{\varphi(N)}<\frac{3ed}{N}=3\frac{e}{N}d<3\frac{e}{N}N^{delta} $

$|y|<2*N^{0.5}$

最后主要修改了 m 和 X 的上界

    delta = .262 # this means that d < N^delta

    #
    # Lattice (tweak those values)
    #

    # you should tweak this (after a first run), (e.g. increment it until a solution is found)
    m = 8 # size of the lattice (bigger the better/slower)

    # you need to be a lattice master to tweak these
    t = int((1-2*delta) * m)  # optimization from Herrmann and May
    X = floor(3*e/N*N^delta) #4*floor(N^delta)  # this _might_ be too much
    Y = floor(2*N^(1/2))    # correct if p, q are ~ same size

最后可以得到结果

[DEBUG] Received 0x1f bytes:
    'Succcess!\n'
    'OOO{Br3akingL!mits?}\n'
OOO{Br3akingL!mits?}

不得不说这个题目,真的是需要核服务器。

RSA 选择明密文攻击

选择明文攻击

这里给出一个例子,假如我们有一个加密 oracle ,但是我们不知道 n 和 e,那

  1. 我们可以通过加密 oracle 获取 n。
  2. 在 e 比较小( $e<2^{64}$)时,我们可以利用 Pollard’s kangaroo algorithm 算法获取 e。这一点比较显然。

我们可以加密 2,4,8,16。那么我们可以知道

$c_2=2^{e} \bmod n$

$c_4=4^{e} \bmod n$

$c_8=8^{e} \bmod n$

那么

$c_2^2 \equiv c_4 \bmod n$

$c_2^3 \equiv c_8 \bmod n$

故而

$c_2^2-c_4=kn$

$c_2^3-c_8=tn$

我们可以求出 kn 和 tn 的最大公因数,很大概率就是 n 了。我们还可以构造更多的例子从来更加确定性地找 n。

任意密文解密

假设爱丽丝创建了密文 $C = P^e \bmod n$ 并且把 C 发送给鲍勃,同时假设我们要对爱丽丝加密后的任意密文解密,而不是只解密 C,那么我们可以拦截 C,并运用下列步骤求出 P:

  1. 选择任意的 $X\in Z_n^{*}$,即 X 与 N 互素
  2. 计算 $Y=C \times X^e \bmod n$
  3. 由于我们可以进行选择密文攻击,那么我们求得 Y 对应的解密结果 $Z=Y^d$
  4. 那么,由于 $Z=Y^d=(C \times X^e)^d=C^d X=P^{ed} X= P X\bmod n$,由于 X 与 N 互素,我们很容易求得相应的逆元,进而可以得到 P

RSA parity oracle

假设目前存在一个 Oracle,它会对一个给定的密文进行解密,并且会检查解密的明文的奇偶性,并根据奇偶性返回相应的值,比如 1 表示奇数,0 表示偶数。那么给定一个加密后的密文,我们只需要 log(N) 次就可以知道这个密文对应的明文消息。

原理

假设

$C=P^e \bmod N$

第一次时,我们可以给服务器发送

$C*2^e=(2P)^e \bmod N$

服务器会计算得到

$2P \bmod N$

这里

  • 2P 是偶数,它的幂次也是偶数。
  • N 是奇数,因为它是由两个大素数相乘得到。

那么

  • 服务器返回奇数,即 $2P \bmod N$ 为奇数,则说明 2P 大于 N,且减去了奇数个 N,又因为 $2P<2N$,因此减去了一个N, 即 $\frac{N}{2} \leq P < N$,我们还可以考虑向下取整。
  • 服务器返回偶数,则说明 2P 小于 N。即 $0\leq P < \frac{N}{2}$,我们还可以向下取整。

这里我们使用数学归纳法,即假设在第 i 次时,$\frac{xN}{2^{i}} \leq P < \frac{xN+N}{2^{i}}$

进一步,在第 i+1 次时,我们可以发送

$C*2^{(i+1)e}$

服务器会计算得到

$2^{i+1}P \bmod N=2^{i+1}P-kN$

$0 \leq 2^{i+1}P-kN<N$

$\frac{kN}{2^{i+1}} \leq P < \frac{kN+N}{2^{i+1}}$

根据第 i 次的结果

$\frac{2xN}{2^{i+1}} \leq P < \frac{2xN+2N}{2^{i+1}}$

那么

  • 服务器返回奇数,则 k 必然是一个奇数,k=2y+1, 那么 $\frac{2yN+N}{2^{i+1}} \leq P < \frac{2yN+2N}{2^{i+1}}$。与此同时,由于 P 必然存在,所以第 i+1 得到的这个范围和第 i 次得到的范围必然存在交集。所以 y 必然与 x 相等。
  • 服务器返回偶数,则 k 必然是一个偶数,k=2y,此时 y 必然也与 x 相等,那么 $\frac{2xN}{2^{i+1}} \leq P < \frac{2xN+N}{2^{i+1}}$

进一步我们可以这么归纳

lb = 0
ub = N
if server returns 1
    lb = (lb+ub)/2
else:
    ub = (lb+ub)/2

这里虽然是整除, 即下取整,但是无所谓我们在最初时已经分析了这个问题。

2018 Google CTF Perfect Secrecy

这里以 2018 年 Google CTF 的题目为例进行分析

#!/usr/bin/env python3
import sys
import random

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend


def ReadPrivateKey(filename):
  return serialization.load_pem_private_key(
      open(filename, 'rb').read(), password=None, backend=default_backend())


def RsaDecrypt(private_key, ciphertext):
  assert (len(ciphertext) <=
          (private_key.public_key().key_size // 8)), 'Ciphertext too large'
  return pow(
      int.from_bytes(ciphertext, 'big'),
      private_key.private_numbers().d,
      private_key.public_key().public_numbers().n)


def Challenge(private_key, reader, writer):
  try:
    m0 = reader.read(1)
    m1 = reader.read(1)
    ciphertext = reader.read(private_key.public_key().key_size // 8)
    dice = RsaDecrypt(private_key, ciphertext)
    for rounds in range(100):
      p = [m0, m1][dice & 1]
      k = random.randint(0, 2)
      c = (ord(p) + k) % 2
      writer.write(bytes((c,)))
    writer.flush()
    return 0

  except Exception as e:
    return 1


def main():
  private_key = ReadPrivateKey(sys.argv[1])
  return Challenge(private_key, sys.stdin.buffer, sys.stdout.buffer)


if __name__ == '__main__':
  sys.exit(main())

可以看出

  • 我们可以给服务器两个数,服务器会根据解密后的密文内容来决定使用哪一个。
  • 服务器会使用 random.randint(0, 2) 来生成随机数,并输出相关的随机 01 字节 c。

乍一看,似乎是完全随机的,仔细查一下 random.randint(0, 2) 可以知道其生成随机数是包括边界的,因此其生成偶数的概率大于生成奇数的概率,那么 c 与 p 同奇偶的概率为 2/3。进而我们通过设置 m0 和 m1 就可以知道解密后的密文的最后一位是 0 还是 1 。这其实就是 RSA parity oracle。

exp 如下

import gmpy2
from pwn import *
encflag = open('./flag.txt').read()
encflag = encflag.encode('hex')
encflag = int(encflag, 16)
#context.log_level = 'debug'
m = ['\x00', '\x07']
n = 0xDA53A899D5573091AF6CC9C9A9FC315F76402C8970BBB1986BFE8E29CED12D0ADF61B21D6C281CCBF2EFED79AA7DD23A2776B03503B1AF354E35BF58C91DB7D7C62F6B92C918C90B68859C77CAE9FDB314F82490A0D6B50C5DC85F5C92A6FDF19716AC8451EFE8BBDF488AE098A7C76ADD2599F2CA642073AFA20D143AF403D1
e = 65537
flag = ""



def guessvalue(cnt):
    if cnt[0] > cnt[1]:
        return 0
    return 1


i = 0
while True:
    cnt = dict()
    cnt[0] = cnt[1] = 0
    p = remote('perfect-secrecy.ctfcompetition.com', 1337)
    p.send(m[0])
    p.send(m[1])
    tmp = pow(2, i)
    two_inv = gmpy2.invert(tmp, n)
    two_cipher = gmpy2.powmod(two_inv, e, n)
    tmp = encflag * two_cipher % n
    tmp = hex(tmp)[2:].strip('L')
    tmp = '0' * (256 - len(tmp)) + tmp
    tmp = tmp.decode('hex')
    assert (len(tmp) == 128)
    p.send(tmp)
    #print tmp
    data = ""
    while (len(data) != 100):
        data += p.recv()
    for c in data:
        cnt[u8(c)] += 1
    p.close()
    flag = str(guessvalue(cnt)) + flag
    print i, flag
    i += 1

结果如下

6533021797450432625003726192285181680054061843303961161444459679874621880787893445342698029728203298974356255732086344166897556918532195998159983477294838449903429031335408290610431938507208444225296242342845578895553611385588996615744823221415296689514934439749745119968629875229882861818946483594948270 6533021797450432625003726192285181680054061843303961161444459679874621880787893445342698029728203298974356255732086344166897556918532195998159983477294838449903429031335408290610431938507208444225296242342845578895553611385588996615744823221415296689514934439749745119968629875229882861818946483594948270

解码后就可以得到 flag

CTF{h3ll0__17_5_m3_1_w45_w0nd3r1n6_1f_4f73r_4ll_7h353_y34r5_y0u_d_l1k3_70_m337}

题目

  • 2016 Plaid CTF rabit
  • 2016 sharif CTF lsb-oracle-150
  • 2018 Backdoor CTF BIT-LEAKER
  • 2018 XMAN 选拔赛 baby RSA

RSA Byte Oracle

假设目前存在一个 Oracle,它会对一个给定的密文进行解密,并且会给出明文的最后一个字节。那么给定一个加密后的密文,我们只需要 $\log_{256}n$ 次就可以知道这个密文对应的明文消息。

原理

这个其实算作 RSA parity Oracle 的扩展,既然可以泄露出最后一个字节,那么按道理我们获取密文对应明文的次数应该可以减少。

假设

$C=P^e \bmod N$

第一次时,我们可以给服务器发送

$C*256^e=(256P)^e \bmod N$

服务器会计算得到

$256P \bmod N$

这里

  • 256P 是偶数。
  • N 是奇数,因为它是由两个大素数相乘得到。

由于 P 一般是小于 N 的,那么$256P \bmod N=256P-kn, k<256$。而且对于两个不同的 $k_1,k_2$,我们有

$256P-k_1n \not\equiv 256P-k_2n \bmod 256$

我们可以利用反证法来证明上述不等式。同时 $256P-kn$ 的最后一个字节其实就是 $-kn$ 在模 256 的情况下获取的。那么,其实我们可以首先枚举出 0~255 情况下的最后一个字节,构造一个 k 和最后一个字节的映射表 map

当服务器返回最后一个字节 b,那么我们可以根据上述构造的映射表得知 k,即减去了 k 个N, 即 $kN \leq 256 P \leq (k+1)N$。

此后,我们使用数学归纳法来获取 P 的范围,即假设在第 i 次时,$\frac{xN}{256^{i}} \leq P < \frac{xN+N}{256^{i}}$

进一步,在第 i+1 次时,我们可以发送

$C*256^{(i+1)e}$

服务器会计算得到

$256^{i+1}P \bmod N=256^{i+1}P-kN$

$0 \leq 256^{i+1}P-kN<N$

$\frac{kN}{256^{i+1}} \leq P < \frac{kN+N}{256^{i+1}}$

根据第 i 次的结果

$\frac{256xN}{256^{i+1}} \leq P < \frac{256xN+256N}{256^{i+1}}$

我们这里可以假设 $k=256y+t$, 而这里的 t 就是我们可以通过映射表获取的。

$\frac{256yN+tN}{256^{i+1}} \leq P < \frac{256yN+(t+1)N}{256^{i+1}}$

与此同时,由于 P 必然存在,所以第 i+1 得到的这个范围和第 i 次得到的范围必然存在交集。

所以 y 必然与 x 相等。

进一步我们可以这么归纳,初始情况下

lb = 0
ub = N

假设服务器返回了 b,那么

k = mab[b]
interval = (ub-lb)/256
lb = lb + interval * k
ub = lb + interval

2018 HITCON lost key

这是一个综合题目,首先没有给出 n,我们可以使用选择明文攻击的方式获取 n,当然我们也可以进一步获取 e,最后利用代码如下

from pwn import *
import gmpy2
from fractions import Fraction
p = process('./rsa.py')
#p = remote('18.179.251.168', 21700)
#context.log_level = 'debug'
p.recvuntil('Here is the flag!\n')
flagcipher = int(p.recvuntil('\n', drop=True), 16)


def long_to_hex(n):
    s = hex(n)[2:].rstrip('L')
    if len(s) % 2: s = '0' + s
    return s


def send(ch, num):
    p.sendlineafter('cmd: ', ch)
    p.sendlineafter('input: ', long_to_hex(num))
    data = p.recvuntil('\n')
    return int(data, 16)


if __name__ == "__main__":
    # get n
    cipher2 = send('A', 2)
    cipher4 = send('A', 4)
    nset = []
    nset.append(cipher2 * cipher2 - cipher4)

    cipher3 = send('A', 3)
    cipher9 = send('A', 9)
    nset.append(cipher3 * cipher3 - cipher9)
    cipher5 = send('A', 5)
    cipher25 = send('A', 25)
    nset.append(cipher5 * cipher5 - cipher25)
    n = nset[0]
    for item in nset:
        n = gmpy2.gcd(item, n)

    # get map between k and return byte
    submap = {}
    for i in range(0, 256):
        submap[-n * i % 256] = i

    # get cipher256
    cipher256 = send('A', 256)

    back = flagcipher

    L = Fraction(0, 1)
    R = Fraction(1, 1)
    for i in range(128):
        print i
        flagcipher = flagcipher * cipher256 % n
        b = send('B', flagcipher)
        k = submap[b]
        L, R = L + (R - L) * Fraction(k, 256
                                     ), L + (R - L) * Fraction(k + 1, 256)
    low = int(L * n)
    print long_to_hex(low - low % 256 + send('B', back)).decode('hex')

RSA parity oracle variant

原理

如果oracle的参数会在一定时间、运行周期后改变,或者网络不稳定导致会话断开、重置,二分法就不再适用了,为了减少错误,应当考虑逐位恢复。
要恢复明文的第2低位,考虑

$$
{(c(2^{-1e_1}\mod N_1))^{d_1}\mod N_1}\pmod2\equiv m2^{-1}
$$

$$
\begin{aligned}
&m(2^{-1}\mod N_1)\mod2\
&=(\displaystyle\sum_{i=0}^{logm-1}a_i
2^i)2^{-1}\mod2\
&=[2(\displaystyle\sum_{i=1}^{logm-1}a_i
2^{i-1})+a_02^0]2^{-1}\mod 2\
&=\displaystyle\sum_{i=1}^{logm-1}a_i2^{i-1}+a_02^02^{-1}\mod2\
&\equiv a_1+a_0
2^0*2^{-1}\equiv y\pmod2
\end{aligned}
$$

$$
y-(a_02^0)2^{-1}=(m2^{-1}\mod2)-(a_02^0)*2^{-1}\equiv a_1\pmod2
$$

类似的

$$
{(c(2^{-2e_2}\mod N_2))^{d_2}\mod N_2}\pmod2\equiv m2^{-2}
$$

$$
\begin{aligned}
&m(2^{-2}\mod N_2)\mod2\
&=(\displaystyle\sum_{i=0}^{logm-1}a_i
2^i)2^{-2}\mod2\
&=[2^2(\displaystyle\sum_{i=2}^{logm-1}a_i
2^{i-2})+a_12^1+a_02^0]2^{-2}\mod 2\
&=\displaystyle\sum_{i=2}^{logm-1}a_i
2^{i-1}+(a_12^1+a_02^0)2^{-2}\mod2\
&\equiv a_2+(a_1
2^1+a_02^0)2^{-2}\equiv y\pmod2
\end{aligned}
$$

$$
\begin{aligned}
&y-(a_12^1+a_02^0)2^{-2}\
&=(m
2^{-2}\mod2)-(a_12^1+a_02^0)*2^{-2}\equiv a_2\pmod2
\end{aligned}
$$

我们就可以使用前i-1位与oracle的结果来得到第i位。注意这里的$2^{-1}$是$2^1$模$N_1$的逆元。所以对剩下的位,有

$$
\begin{aligned}
&{(c(2^{-ie_i}\mod N_i))^{d_i}\mod N_i}\pmod2\equiv m2^{-i}\
&a_i\equiv (m2^{-i}\mod2) -\sum_{j=0}^{i-1}a_j2^j\pmod2,i=1,2,…,logm-1
\end{aligned}
$$

其中$2^{-i}$是$2^i$模$N_i$的逆元。

就可以逐步恢复原文所有的位信息了。这样的时间复杂度为$O(logm)$。

exp:

from Crypto.Util.number import *
mm = bytes_to_long(b'12345678')
l = len(bin(mm)) - 2

def genkey():
    while 1:
        p = getPrime(128)
        q = getPrime(128)
        e = getPrime(32)
        n = p * q
        phi = (p - 1) * (q - 1)
        if GCD(e, phi) > 1:
            continue
        d = inverse(e, phi)
        return e, d, n

e, d, n = genkey()
cc = pow(mm, e, n)
f = str(pow(cc, d, n) % 2)

for i in range(1, l):
    e, d, n = genkey()
    cc = pow(mm, e, n)
    ss = inverse(2**i, n)
    cs = (cc * pow(ss, e, n)) % n
    lb = pow(cs, d, n) % 2
    bb = (lb - (int(f, 2) * ss % n)) % 2
    f = str(bb) + f
    assert(((mm >> i) % 2) == bb)
print(long_to_bytes(int(f, 2)))

RSA 侧信道攻击

能量分析攻击(侧信道攻击)是一种能够从密码设备中获取秘密信息的密码攻击方法.与其
他攻击方法不同:这种攻击利用的是密码设备的能量消耗特征,而非密码算法的数学特性.能量分析攻击是一种非入侵式攻击,攻击者可以方便地购买实施攻击所需要的设备:所以这种攻击对智能卡之类的密码设备的安全性造成了严重威胁。

能量分析攻击是安全领域内非常重要的一个部分,我们只在这里简单讨论下。

能量分析攻击分为:

  • 简单能量分析攻击(SPA),即对能量迹进行直观分析,肉眼看即可。
  • 差分能量分析攻击(DPA),基于能量迹之间的相关系数进行分析。

攻击条件

攻击者可获取与加解密相关的侧信道信息,例如能量消耗、运算时间、电磁辐射等等。

例子

这里我们以 HITB 2017 的 Hack in the card I 作为例子。

题目给出了公钥文件 publickey.pem,密文,测量智能卡功率的电路图,和解密过程中智能卡消耗的功率变化(通过在线网站给出 trace)。

密文:

014b05e1a09668c83e13fda8be28d148568a2342aed833e0ad646bd45461da2decf9d538c2d3ab245b272873beb112586bb7b17dc4b30f0c5408d8b03cfbc8388b2bd579fb419a1cac38798da1c3da75dc9a74a90d98c8f986fd8ab8b2dc539768beb339cadc13383c62b5223a50e050cb9c6b759072962c2b2cf21b4421ca73394d9e12cfbc958fc5f6b596da368923121e55a3c6a7b12fdca127ecc0e8470463f6e04f27cd4bb3de30555b6c701f524c8c032fa51d719901e7c75cc72764ac00976ac6427a1f483779f61cee455ed319ee9071abefae4473e7c637760b4b3131f25e5eb9950dd9d37666e129640c82a4b01b8bdc1a78b007f8ec71e7bad48046

分析

由于网站只给出了一条能量迹,所以可以断定这是 Simple channel analysis(SPA)攻击。那么我们可以直接通过观察能量迹的高低电平来获得 RSA 解密过程的密钥 d。
RSA 可被 SPA 攻击的理论基础来自于 RSA 中包含的快速幂取余算法。

快速幂算法如下

  1. b 为偶数时,$a^b \bmod c = ({a^2}^{b/2}) \bmod c$。
  2. b 为奇数时,$a^b \bmod c = ({a^2}^{b/2} \times a) \bmod c$。

相应的 C 代码实现为:

int PowerMod(int a, int b, int c)
{
    int ans = 1;
    a = a % c;
    while(b>0) {
        if(b % 2 == 1) // 当b为奇数时会多执行下面的指令
            ans = (ans * a) % c;
        b = b/2;
        a = (a * a) % c;
    }
    return ans;
}

由于快速幂的计算过程中会逐位判断指数的取值,并会采取不同的操作,所以可从能量迹中还原出 d 的取值(从上面可知,直接得到的值是 d 的二进制取值的逆序)。

注意

有时候模乘也可能会从高位向低位进行模乘。这里是从低位向高位模乘。

由此可给出还原 d 的脚本如下:

f = open('./data.txt')
data = f.read().split(",")
print('point number:', len(data))

start_point = 225   # 开始分析的点
mid = 50            # 采样点间隔
fence = 228         # 高低电平分界线

bin_array = []

for point_index in range(start_point, len(data), mid):
    if float(data[point_index]) > fence:
        bin_array.append(1)
    else:
        bin_array.append(0)

bin_array2 = []
flag1 = 0
flag2 = 0
for x in bin_array:
    if x:
        if flag1:
            flag2 = 1
        else:
            flag1 = 1
    else:
        if flag2:
            bin_array2.append(1)
        else:
            bin_array2.append(0)
        flag1 = 0
        flag2 = 0

# d_bin = bin_array2[::-1]
d_bin = bin_array2
d = "".join(str(x) for x in d_bin)[::-1]
print(d)
d_int = int(d,2)
print(d_int)

RSA 复杂题目

2018 Tokyo Western Mixed Cipher

题目给的信息如下所示:

  • 每次交互可以维持的时间长度约为 5 分钟
  • 每次交互中中n是确定的 1024 bit,但是未知, e 为 65537
  • 使用 aes 加密了 flag,密钥和 IV 均不知道
  • 每次密钥是固定的,但是 IV 每次都会随机
  • 可以使用 encrypt 功能随意使用 rsa 和 aes 进行加密,其中每次加密都会对 aes 的 iv 进行随机
  • 可以使用 decrypt 对随意的密文进行解密,但是只能知道最后一个字节是什么
  • 可以使用 print_flag 获取 flag 密文
  • 可以使用 print_key 获取 rsa 加密的 aes 密钥

本题目看似一个题目,实则是 3 个题目,需要分步骤解决。在此之前,我們準備好交互的函數

def get_enc_key(io):
    io.read_until("4: get encrypted keyn")
    io.writeline("4")
    io.read_until("here is encrypted key :)n")
    c=int(io.readline()[:-1],16)
    return c

def encrypt_io(io,p):
    io.read_until("4: get encrypted keyn")
    io.writeline("1")
    io.read_until("input plain text: ")
    io.writeline(p)
    io.read_until("RSA: ")
    rsa_c=int(io.readline()[:-1],16)
    io.read_until("AES: ")
    aes_c=io.readline()[:-1].decode("hex")
    return rsa_c,aes_c

def decrypt_io(io,c):
    io.read_until("4: get encrypted keyn")
    io.writeline("2")
    io.read_until("input hexencoded cipher text: ")
    io.writeline(long_to_bytes(c).encode("hex"))
    io.read_until("RSA: ")
    return io.read_line()[:-1].decode("hex")

GCD attack n

第一步我们需要把没有给出的 n 算出来,因为我们可以利用 encrypt 功能对我们输入的明文 x 进行 rsa 加密,那么可以利用整除的性质算 n

因为x ^ e = c mod n
所以 n | x ^ e - c

我们可以构造足够多的 x,算出最够多的 x ^ e - c,从而计算最大公约数,得到 n。

def get_n(io):
    rsa_c,aes_c=encrypt_io(io,long_to_bytes(2))
    n=pow(2,65537)-rsa_c
    for i in range(3,6):
        rsa_c, aes_c = encrypt_io(io, long_to_bytes(i))
        n=primefac.gcd(n,pow(i,65537)-rsa_c)
    return n

可以利用加密进行 check

def check_n(io,n):
    rsa_c, aes_c = encrypt_io(io, "123")
    if pow(bytes_to_long("123"), e, n)==rsa_c:
        return True
    else:
        return False

RSA parity oracle

利用 leak 的的最后一个字节,我们可以进行选择密文攻击,使用 RSA parity oracle 回复 aes 的秘钥

def guess_m(io,n,c):
    k=1
    lb=0
    ub=n
    while ub!=lb:
        print lb,ub
        tmp = c * gmpy2.powmod(2, k*e, n) % n
        if ord(decrypt_io(io,tmp)[-1])%2==1:
            lb = (lb + ub) / 2
        else:
            ub = (lb + ub) / 2
        k+=1
    print ub,len(long_to_bytes(ub))
    return ub

PRNG Predict

这里我们可以解密 flag 的16字节之后的内容了,但是前16个字节没有 IV 是解密不了的。这时我们可以发现,IV 生成使用的随机数使用了 getrandbits,并且我们可以获取到足够多的随机数量,那么我们可以进行 PRNG 的 predict,从而直接获取随机数

这里使用了一个现成的的 java 进行 PRNG 的 Predict

public class Main {

   static int[] state;
   static int currentIndex;
40huo
   public static void main(String[] args) {
      state = new int[624];
      currentIndex = 0;

//    initialize(0);

//    for (int i = 0; i < 5; i++) {
//       System.out.println(state[i]);
//    }

      // for (int i = 0; i < 5; i++) {
      // System.out.println(nextNumber());
      // }

      if (args.length != 624) {
         System.err.println("must be 624 args");
         System.exit(1);
      }
      int[] arr = new int[624];
      for (int i = 0; i < args.length; i++) {
         arr[i] = Integer.parseInt(args[i]);
      }


      rev(arr);

      for (int i = 0; i < 6240huo4; i++) {
         System.out.println(state[i]);
      }

//    System.out.println("currentIndex " + currentIndex);
//    System.out.println("state[currentIndex] " + state[currentIndex]);
//    System.out.println("next " + nextNumber());

      // want -2065863258
   }

   static void nextState() {
      // Iterate through the state
      for (int i = 0; i < 624; i++) {
         // y is the first bit of the current number,
         // and the last 31 bits of the next number
         int y = (state[i] & 0x80000000)
               + (state[(i + 1) % 624] & 0x7fffffff);
         // first bitshift y by 1 to the right
         int next = y >>> 1;
         // xor it with the 397th next number
         next ^= state[(i + 397) % 624];
         // if y is odd, xor with magic number
         if ((y & 1L) == 1L) {
            next ^= 0x9908b0df;
         }
         // now we have the result
         state[i] = next;
      }
   }

   static int nextNumber() {
      currentIndex++;
      int tmp = state[currentIndex];
      tmp ^= (tmp >>> 11);
      tmp ^= (tmp << 7) & 0x9d2c5680;
      tmp ^= (tmp << 15) & 0xefc60000;
      tmp ^= (tmp >>> 18);
      return tmp;
   }

   static void initialize(int seed) {

      // http://code.activestate.com/recipes/578056-mersenne-twister/

      // global MT
      // global bitmask_1
      // MT[0] = seed
      // for i in xrange(1,624):
      // MT[i] = ((1812433253 * MT[i-1]) ^ ((MT[i-1] >> 30) + i)) & bitmask_1

      // copied Python 2.7's impl (probably uint problems)
      state[0] = seed;
      for (int i = 1; i < 624; i++) {
         state[i] = ((1812433253 * state[i - 1]) ^ ((state[i - 1] >> 30) + i)) & 0xffffffff;
      }
   }

   static int unBitshiftRightXor(int value, int shift) {
      // we part of the value we are up to (with a width of shift bits)
      int i = 0;
      // we accumulate the result here
      int result = 0;
      // iterate until we've done the full 32 bits
      while (i * shift < 32) {
         // create a mask for this part
         int partMask = (-1 << (32 - shift)) >>> (shift * i);
         // obtain the part
         int part = value & partMask;
         // unapply the xor from the next part of the integer
         value ^= part >>> shift;
         // add the part to the result
         result |= part;
         i++;
      }
      return result;
   }

   static int unBitshiftLeftXor(int value, int shift, int mask) {
      // we part of the value we are up to (with a width of shift bits)
      int i = 0;
      // we accumulate the result here
      int result = 0;
      // iterate until we've done the full 32 bits
      while (i * shift < 32) {
         // create a mask for this part
         int partMask = (-1 >>> (32 - shift)) << (shift * i);
         // obtain the part
         int part = value & partMask;
         // unapply the xor from the next part of the integer
         value ^= (part << shift) & mask;
         // add the part to the result
         result |= part;
         i++;
      }
      return result;
   }

   static void rev(int[] nums) {
      for (int i = 0; i < 624; i++) {

         int value = nums[i];
         value = unBitshiftRightXor(value, 18);
         value = unBitshiftLeftXor(value, 15, 0xefc60000);
         value = unBitshiftLeftXor(value, 7, 0x9d2c5680);
         value = unBitshiftRightXor(value, 11);

         state[i] = value;
      }
   }
}

写了一个 python 直接调用 java

from Crypto.Util.number import long_to_bytes,bytes_to_long



def encrypt_io(io,p):
    io.read_until("4: get encrypted keyn")
    io.writeline("1")
    io.read_until("input plain text: ")
    io.writeline(p)
    io.read_until("RSA: ")
    rsa_c=int(io.readline()[:-1],16)
    io.read_until("AES: ")
    aes_c=io.readline()[:-1].decode("hex")
    return rsa_c,aes_c
import subprocess
import random
def get_iv(io):
    rsa_c, aes_c=encrypt_io(io,"1")
    return bytes_to_long(aes_c[0:16])
def splitInto32(w128):
    w1 = w128 & (2**32-1)
    w2 = (w128 >> 32) & (2**32-1)
    w3 = (w128 >> 64) & (2**32-1)
    w4 = (w128 >> 96)
    return w1,w2,w3,w4
def sign(iv):
    # converts a 32 bit uint to a 32 bit signed int
    if(iv&0x80000000):
        iv = -0x100000000 + iv
    return iv
def get_state(io):
    numbers=[]
    for i in range(156):
        print i
        numbers.append(get_iv(io))
    observedNums = [sign(w) for n in numbers for w in splitInto32(n)]
    o = subprocess.check_output(["java", "Main"] + map(str, observedNums))
    stateList = [int(s) % (2 ** 32) for s in o.split()]
    r = random.Random()
    state = (3, tuple(stateList + [624]), None)
    r.setstate(state)
    return r.getrandbits(128)

EXP

整体攻击代码如下:

from zio import *
import primefac
from Crypto.Util.number import long_to_bytes,bytes_to_long
target=("crypto.chal.ctf.westerns.tokyo",5643)
e=65537

def get_enc_key(io):
    io.read_until("4: get encrypted keyn")
    io.writeline("4")
    io.read_until("here is encrypted key :)n")
    c=int(io.readline()[:-1],16)
    return c

def encrypt_io(io,p):
    io.read_until("4: get encrypted keyn")
    io.writeline("1")
    io.read_until("input plain text: ")
    io.writeline(p)
    io.read_until("RSA: ")
    rsa_c=int(io.readline()[:-1],16)
    io.read_until("AES: ")
    aes_c=io.readline()[:-1].decode("hex")
    return rsa_c,aes_c

def decrypt_io(io,c):
    io.read_until("4: get encrypted keyn")
    io.writeline("2")
    io.read_until("input hexencoded cipher text: ")
    io.writeline(long_to_bytes(c).encode("hex"))
    io.read_until("RSA: ")
    return io.read_line()[:-1].decode("hex")

def get_n(io):
    rsa_c,aes_c=encrypt_io(io,long_to_bytes(2))
    n=pow(2,65537)-rsa_c
    for i in range(3,6):
        rsa_c, aes_c = encrypt_io(io, long_to_bytes(i))
        n=primefac.gcd(n,pow(i,65537)-rsa_c)
    return n

def check_n(io,n):
    rsa_c, aes_c = encrypt_io(io, "123")
    if pow(bytes_to_long("123"), e, n)==rsa_c:
        return True
    else:
        return False


import gmpy2
def guess_m(io,n,c):
    k=1
    lb=0
    ub=n
    while ub!=lb:
        print lb,ub
        tmp = c * gmpy2.powmod(2, k*e, n) % n
        if ord(decrypt_io(io,tmp)[-1])%2==1:
            lb = (lb + ub) / 2
        else:
            ub = (lb + ub) / 2
        k+=1
    print ub,len(long_to_bytes(ub))
    return ub


io = zio(target, timeout=10000, print_read=COLORED(NONE, 'red'),print_write=COLORED(NONE, 'green'))
n=get_n(io)
print check_n(io,n)
c=get_enc_key(io)
print len(decrypt_io(io,c))==16


m=guess_m(io,n,c)
for i in range(m - 50000,m+50000):
    if pow(i,e,n)==c:
        aeskey=i
        print long_to_bytes(aeskey)[-1]==decrypt_io(io,c)[-1]
        print "found aes key",hex(aeskey)

import fuck_r
next_iv=fuck_r.get_state(io)
print "##########################################"
print next_iv
print aeskey
io.interact()

2016 ASIS Find the flag

这里我们以 ASIS 2016 线上赛中 Find the flag 为例进行介绍。

文件解压出来,有一个密文,一个公钥,一个 py 脚本。看一下公钥。

➜  RSA openssl rsa -pubin -in pubkey.pem -text -modulus
Public-Key: (256 bit)
Modulus:
    00:d8:e2:4c:12:b7:b9:9e:fe:0a:9b:c0:4a:6a:3d:
    f5:8a:2a:94:42:69:b4:92:b7:37:6d:f1:29:02:3f:
    20:61:b9
Exponent: 12405943493775545863 (0xac2ac3e0ca0f5607)
Modulus=D8E24C12B7B99EFE0A9BC04A6A3DF58A2A944269B492B7376DF129023F2061B9

这么小的一个 $N$,先分解一下。

p = 311155972145869391293781528370734636009
q = 315274063651866931016337573625089033553

再看给的 py 脚本。

#!/usr/bin/python
import gmpy
from Crypto.Util.number import *
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5

flag = open('flag', 'r').read() * 30

def ext_rsa_encrypt(p, q, e, msg):
    m = bytes_to_long(msg)
    while True:
        n = p * q
        try:
            phi = (p - 1)*(q - 1)
            d = gmpy.invert(e, phi)
            pubkey = RSA.construct((long(n), long(e)))
            key = PKCS1_v1_5.new(pubkey)
            enc = key.encrypt(msg).encode('base64')
            return enc
        except:
            p = gmpy.next_prime(p**2 + q**2)
            q = gmpy.next_prime(2*p*q)
            e = gmpy.next_prime(e**2)

p = getPrime(128)
q = getPrime(128)
n = p*q
e = getPrime(64)
pubkey = RSA.construct((long(n), long(e)))
f = open('pubkey.pem', 'w')
f.write(pubkey.exportKey())
g = open('flag.enc', 'w')
g.write(ext_rsa_encrypt(p, q, e, flag))

逻辑很简单,读取 flag,重复 30 遍为密文。随机取 $p$ 和 $q$,生成一个公钥,写入 pubkey.pem,再用脚本中的 ext_rsa_encrypt 函数进行加密,最后将密文写入 flag.enc

尝试一下解密,提示密文过长,再看加密函数,原来当加密失败时,函数会跳到异常处理,以一定算法重新取更大的 $p$ 和 $q$,直到加密成功。

那么我们只要也写一个相应的解密函数即可。

#!/usr/bin/python
import gmpy
from Crypto.Util.number import *
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5

def ext_rsa_decrypt(p, q, e, msg):
    m = bytes_to_long(msg)
    while True:
        n = p * q
        try:
            phi = (p - 1)*(q - 1)
            d = gmpy.invert(e, phi)
            privatekey = RSA.construct((long(n), long(e), long(d), long(p), long(q)))
            key = PKCS1_v1_5.new(privatekey)
            de_error = ''
            enc = key.decrypt(msg.decode('base64'), de_error)
            return enc
        except Exception as error:
            print error
            p = gmpy.next_prime(p**2 + q**2)
            q = gmpy.next_prime(2*p*q)
            e = gmpy.next_prime(e**2)

p = 311155972145869391293781528370734636009
q = 315274063651866931016337573625089033553
n = p*q
e = 12405943493775545863 
# pubkey = RSA.construct((long(n), long(e)))
# f = open('pubkey.pem', 'w')
# f.write(pubkey.exportKey())
g = open('flag.enc', 'r')
msg = g.read()
flag = ext_rsa_decrypt(p, q, e, msg)
print flag

拿到 flag

ASIS{F4ct0R__N_by_it3rat!ng!}

SCTF RSA1

这里我们以 SCTF RSA1 为例进行介绍,首先解压压缩包后,得到如下文件

➜  level0 git:(master) ✗ ls -al
总用量 4
drwxrwxrwx 1 root root    0 7月  30 16:36 .
drwxrwxrwx 1 root root    0 7月  30 16:34 ..
-rwxrwxrwx 1 root root  349 5月   2  2016 level1.passwd.enc
-rwxrwxrwx 1 root root 2337 5月   6  2016 level1.zip
-rwxrwxrwx 1 root root  451 5月   2  2016 public.key

尝试解压缩了一下 level1.zip 现需要密码。然后根据 level1.passwd.enc 可知,应该是我们需要解密这个文件才能得到对应的密码。查看公钥

➜  level0 git:(master) ✗ openssl rsa -pubin -in public.key -text -modulus 
Public-Key: (2048 bit)
Modulus:
    00:94:a0:3e:6e:0e:dc:f2:74:10:52:ef:1e:ea:a8:
    89:d6:f9:8d:01:11:51:db:5e:90:92:48:fd:39:0c:
    70:87:24:d8:98:3c:f3:33:1c:ba:c5:61:c2:ce:2c:
    5a:f1:5e:65:b2:b2:46:91:56:b6:19:d5:d3:b2:a6:
    bb:a3:7d:56:93:99:4d:7e:4c:2f:aa:60:7b:3e:c8:
    fc:90:b2:00:62:4b:53:18:5b:a2:30:10:60:a8:21:
    ab:61:57:d7:e7:cc:67:1b:4d:cd:66:4c:7d:f1:1a:
    2a:1d:5e:50:80:c1:5e:45:12:3a:ba:4a:53:64:d8:
    72:1f:84:4a:ae:5c:55:02:e8:8e:56:4d:38:70:a5:
    16:36:d3:bc:14:3e:2f:ae:2f:31:58:ba:00:ab:ac:
    c0:c5:ba:44:3c:29:70:56:01:6b:57:f5:d7:52:d7:
    31:56:0b:ab:0a:e6:8d:ad:08:22:a9:1f:cb:6e:49:
    cc:01:4c:12:d2:ab:a3:a5:97:e5:10:49:19:7f:69:
    d9:3b:c5:53:53:71:00:18:60:cc:69:1a:06:64:3b:
    86:94:70:a9:da:82:fc:54:6b:06:23:43:2d:b0:20:
    eb:b6:1b:91:35:5e:53:a6:e5:d8:9a:84:bb:30:46:
    b8:9f:63:bc:70:06:2d:59:d8:62:a5:fd:5c:ab:06:
    68:81
Exponent: 65537 (0x10001)
Modulus=94A03E6E0EDCF2741052EF1EEAA889D6F98D011151DB5E909248FD390C708724D8983CF3331CBAC561C2CE2C5AF15E65B2B2469156B619D5D3B2A6BBA37D5693994D7E4C2FAA607B3EC8FC90B200624B53185BA2301060A821AB6157D7E7CC671B4DCD664C7DF11A2A1D5E5080C15E45123ABA4A5364D8721F844AAE5C5502E88E564D3870A51636D3BC143E2FAE2F3158BA00ABACC0C5BA443C297056016B57F5D752D731560BAB0AE68DAD0822A91FCB6E49CC014C12D2ABA3A597E51049197F69D93BC5535371001860CC691A06643B869470A9DA82FC546B0623432DB020EBB61B91355E53A6E5D89A84BB3046B89F63BC70062D59D862A5FD5CAB066881
writing RSA key
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlKA+bg7c8nQQUu8e6qiJ
1vmNARFR216Qkkj9OQxwhyTYmDzzMxy6xWHCzixa8V5lsrJGkVa2GdXTsqa7o31W
k5lNfkwvqmB7Psj8kLIAYktTGFuiMBBgqCGrYVfX58xnG03NZkx98RoqHV5QgMFe
RRI6ukpTZNhyH4RKrlxVAuiOVk04cKUWNtO8FD4vri8xWLoAq6zAxbpEPClwVgFr
V/XXUtcxVgurCuaNrQgiqR/LbknMAUwS0qujpZflEEkZf2nZO8VTU3EAGGDMaRoG
ZDuGlHCp2oL8VGsGI0MtsCDrthuRNV5TpuXYmoS7MEa4n2O8cAYtWdhipf1cqwZo
gQIDAQAB
-----END PUBLIC KEY-----

发现虽然说是 2048 位,但是显然模数没有那么长,尝试分解下,得到

p=250527704258269
q=74891071972884336452892671945839935839027130680745292701175368094445819328761543101567760612778187287503041052186054409602799660254304070752542327616415127619185118484301676127655806327719998855075907042722072624352495417865982621374198943186383488123852345021090112675763096388320624127451586578874243946255833495297552979177208715296225146999614483257176865867572412311362252398105201644557511678179053171328641678681062496129308882700731534684329411768904920421185529144505494827908706070460177001921614692189821267467546120600239688527687872217881231173729468019623441005792563703237475678063375349

然后就可以构造,并且解密,代码如下

from Crypto.PublicKey import RSA
import gmpy2
from base64 import b64decode
p = 250527704258269
q = 74891071972884336452892671945839935839027130680745292701175368094445819328761543101567760612778187287503041052186054409602799660254304070752542327616415127619185118484301676127655806327719998855075907042722072624352495417865982621374198943186383488123852345021090112675763096388320624127451586578874243946255833495297552979177208715296225146999614483257176865867572412311362252398105201644557511678179053171328641678681062496129308882700731534684329411768904920421185529144505494827908706070460177001921614692189821267467546120600239688527687872217881231173729468019623441005792563703237475678063375349
e = 65537
n = p * q


def getprivatekey(n, e, p, q):
    phin = (p - 1) * (q - 1)
    d = gmpy2.invert(e, phin)
    priviatekey = RSA.construct((long(n), long(e), long(d)))
    with open('private.pem', 'w') as f:
        f.write(priviatekey.exportKey())


def decrypt():
    with open('./level1.passwd.enc') as f:
        cipher = f.read()
    cipher = b64decode(cipher)
    with open('./private.pem') as f:
        key = RSA.importKey(f)
    print key.decrypt(cipher)


#getprivatekey(n, e, p, q)
decrypt()

发现不对

➜  level0 git:(master) ✗ python exp.py
一堆乱码。。

这时候就要考虑其他情况了,一般来说现实中实现的 RSA 都不会直接用原生的 RSA,都会加一些填充比如 OAEP,我们这里试试,修改代码

def decrypt1():
    with open('./level1.passwd.enc') as f:
        cipher = f.read()
    cipher = b64decode(cipher)
    with open('./private.pem') as f:
        key = RSA.importKey(f)
        key = PKCS1_OAEP.new(key)
    print key.decrypt(cipher)

果然如此,得到

➜  level0 git:(master) ✗ python exp.py
FaC5ori1ati0n_aTTA3k_p_tOO_sma11

得到解压密码。继续,查看 level1 中的公钥

➜  level1 git:(master) ✗ openssl rsa -pubin -in public.key -text -modulus
Public-Key: (2048 bit)
Modulus:
    00:c3:26:59:69:e1:ed:74:d2:e0:b4:9a:d5:6a:7c:
    2f:2a:9e:c3:71:ff:13:4b:10:37:c0:6f:56:19:34:
    c5:cb:1f:6d:c0:e3:57:3b:47:c4:76:3e:21:a3:b0:
    11:11:78:d4:ee:4f:e8:99:2b:15:cb:cb:d7:73:e4:
    f9:a6:28:20:fd:db:8c:ea:16:ed:67:c2:48:12:6e:
    4b:01:53:4a:67:cb:22:23:3b:34:2e:af:13:ef:93:
    45:16:2b:00:9f:e0:4b:d1:90:c9:2c:27:9a:34:c3:
    3f:d7:ee:40:f5:82:50:39:aa:8c:e9:c2:7b:f4:36:
    e3:38:9d:04:50:db:a9:b7:3f:4b:2a:d6:8a:2a:5c:
    87:2a:eb:74:35:98:6a:9c:e4:52:cb:93:78:d2:da:
    39:83:f3:0c:d1:65:1e:66:9c:40:56:06:0d:58:fc:
    41:64:5e:06:da:83:d0:3b:06:42:70:da:38:53:e0:
    54:35:53:ce:de:79:4a:bf:f5:3b:e5:53:7f:6c:18:
    12:67:a9:de:37:7d:44:65:5e:68:0a:78:39:3d:bb:
    00:22:35:0e:a3:94:e6:94:15:1a:3d:39:c7:50:0e:
    b1:64:a5:29:a3:69:41:40:69:94:b0:0d:1a:ea:9a:
    12:27:50:ee:1e:3a:19:b7:29:70:b4:6d:1e:9d:61:
    3e:7d
Exponent: 65537 (0x10001)
Modulus=C3265969E1ED74D2E0B49AD56A7C2F2A9EC371FF134B1037C06F561934C5CB1F6DC0E3573B47C4763E21A3B0111178D4EE4FE8992B15CBCBD773E4F9A62820FDDB8CEA16ED67C248126E4B01534A67CB22233B342EAF13EF9345162B009FE04BD190C92C279A34C33FD7EE40F5825039AA8CE9C27BF436E3389D0450DBA9B73F4B2AD68A2A5C872AEB7435986A9CE452CB9378D2DA3983F30CD1651E669C4056060D58FC41645E06DA83D03B064270DA3853E0543553CEDE794ABFF53BE5537F6C181267A9DE377D44655E680A78393DBB0022350EA394E694151A3D39C7500EB164A529A36941406994B00D1AEA9A122750EE1E3A19B72970B46D1E9D613E7D
writing RSA key
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwyZZaeHtdNLgtJrVanwv
Kp7Dcf8TSxA3wG9WGTTFyx9twONXO0fEdj4ho7AREXjU7k/omSsVy8vXc+T5pigg
/duM6hbtZ8JIEm5LAVNKZ8siIzs0Lq8T75NFFisAn+BL0ZDJLCeaNMM/1+5A9YJQ
OaqM6cJ79DbjOJ0EUNuptz9LKtaKKlyHKut0NZhqnORSy5N40to5g/MM0WUeZpxA
VgYNWPxBZF4G2oPQOwZCcNo4U+BUNVPO3nlKv/U75VN/bBgSZ6neN31EZV5oCng5
PbsAIjUOo5TmlBUaPTnHUA6xZKUpo2lBQGmUsA0a6poSJ1DuHjoZtylwtG0enWE+
fQIDAQAB
-----END PUBLIC KEY-----

似乎还是不是很大,再次分解,然后试了 factordb 不行,试试 yafu。结果分解出来了。

P309 = 156956618844706820397012891168512561016172926274406409351605204875848894134762425857160007206769208250966468865321072899370821460169563046304363342283383730448855887559714662438206600780443071125634394511976108979417302078289773847706397371335621757603520669919857006339473738564640521800108990424511408496383

P309 = 156956618844706820397012891168512561016172926274406409351605204875848894134762425857160007206769208250966468865321072899370821460169563046304363342283383730448855887559714662438206600780443071125634394511976108979417302078289773847706397371335621757603520669919857006339473738564640521800108990424511408496259

可以发现这两个数非常相近,可能是 factordb 没有实现这类分解。

继而下面的操作类似于 level0。只是这次是直接解密就好,没啥填充,试了填充反而错

得到密码 fA35ORI11TLoN_Att1Ck_cL0sE_PrI8e_4acTorS。继续下一步,查看公钥

➜  level2 git:(master) ✗ openssl rsa -pubin -in public.key -text -modulus
Public-Key: (1025 bit)
Modulus:
    01:ba:0c:c2:45:b4:5c:e5:b5:f5:6c:d5:ca:a5:90:
    c2:8d:12:3d:8a:6d:7f:b6:47:37:fb:7c:1f:5a:85:
    8c:1e:35:13:8b:57:b2:21:4f:f4:b2:42:24:5f:33:
    f7:2c:2c:0d:21:c2:4a:d4:c5:f5:09:94:c2:39:9d:
    73:e5:04:a2:66:1d:9c:4b:99:d5:38:44:ab:13:d9:
    cd:12:a4:d0:16:79:f0:ac:75:f9:a4:ea:a8:7c:32:
    16:9a:17:d7:7d:80:fd:60:29:64:c7:ea:50:30:63:
    76:59:c7:36:5e:98:d2:ea:5b:b3:3a:47:17:08:2d:
    d5:24:7d:4f:a7:a1:f0:d5:73
Exponent:
    01:00:8e:81:dd:a0:e3:19:28:e8:ee:51:11:08:c7:
    50:5f:61:31:05:d2:e2:ff:9b:83:71:e4:29:c2:dd:
    92:70:65:d4:09:6d:58:c3:76:31:07:f1:d4:fc:cf:
    2d:b3:0a:6d:02:7c:56:61:7c:be:7e:0b:7e:d9:22:
    28:66:9e:fb:3d:2f:2c:20:59:3c:21:ef:ff:31:00:
    6a:fb:a7:68:de:4a:0a:4c:1a:a7:09:d5:48:98:c8:
    1f:cf:fb:dd:f7:9c:ae:ae:0b:15:f4:b2:c7:e0:bc:
    ba:31:4f:5e:07:83:ad:0e:7f:b9:82:a4:d2:01:fa:
    68:29:6d:66:7c:cf:57:b9:4b
Modulus=1BA0CC245B45CE5B5F56CD5CAA590C28D123D8A6D7FB64737FB7C1F5A858C1E35138B57B2214FF4B242245F33F72C2C0D21C24AD4C5F50994C2399D73E504A2661D9C4B99D53844AB13D9CD12A4D01679F0AC75F9A4EAA87C32169A17D77D80FD602964C7EA5030637659C7365E98D2EA5BB33A4717082DD5247D4FA7A1F0D573
writing RSA key
-----BEGIN PUBLIC KEY-----
MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKBgQG6DMJFtFzltfVs1cqlkMKN
Ej2KbX+2Rzf7fB9ahYweNROLV7IhT/SyQiRfM/csLA0hwkrUxfUJlMI5nXPlBKJm
HZxLmdU4RKsT2c0SpNAWefCsdfmk6qh8MhaaF9d9gP1gKWTH6lAwY3ZZxzZemNLq
W7M6RxcILdUkfU+nofDVcwKBgQEAjoHdoOMZKOjuUREIx1BfYTEF0uL/m4Nx5CnC
3ZJwZdQJbVjDdjEH8dT8zy2zCm0CfFZhfL5+C37ZIihmnvs9LywgWTwh7/8xAGr7
p2jeSgpMGqcJ1UiYyB/P+933nK6uCxX0ssfgvLoxT14Hg60Of7mCpNIB+mgpbWZ8
z1e5Sw==
-----END PUBLIC KEY-----

发现私钥 e 和 n 几乎一样大,考虑 d 比较小,使用 Wiener’s Attack。得到 d,当然也可以再次验证一遍。

➜  level2 git:(master) ✗ python RSAwienerHacker.py
Testing Wiener Attack
Hacked!
('hacked_d = ', 29897859398360008828023114464512538800655735360280670512160838259524245332403L)
-------------------------
Hacked!
('hacked_d = ', 29897859398360008828023114464512538800655735360280670512160838259524245332403L)
-------------------------
Hacked!
('hacked_d = ', 29897859398360008828023114464512538800655735360280670512160838259524245332403L)
-------------------------
Hacked!
('hacked_d = ', 29897859398360008828023114464512538800655735360280670512160838259524245332403L)
-------------------------
Hacked!
('hacked_d = ', 29897859398360008828023114464512538800655735360280670512160838259524245332403L)
-------------------------

这时我们解密密文,解密代码如下

from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5, PKCS1_OAEP
import gmpy2
from base64 import b64decode
d = 29897859398360008828023114464512538800655735360280670512160838259524245332403L
with open('./public.key') as f:
    key = RSA.importKey(f)
    n = key.n
    e = key.e


def getprivatekey(n, e, d):
    priviatekey = RSA.construct((long(n), long(e), long(d)))
    with open('private.pem', 'w') as f:
        f.write(priviatekey.exportKey())


def decrypt():
    with open('./level3.passwd.enc') as f:
        cipher = f.read()
    with open('./private.pem') as f:
        key = RSA.importKey(f)
    print key.decrypt(cipher)


getprivatekey(n, e, d)
decrypt()

利用末尾的字符串 wIe6ER1s_1TtA3k_e_t00_larg3 解密压缩包,注意去掉 B。至此全部解密结束,得到 flag。

2018 WCTF RSA

题目基本描述为

Description:
Encrypted message for user "admin":

<<<320881698662242726122152659576060496538921409976895582875089953705144841691963343665651276480485795667557825130432466455684921314043200553005547236066163215094843668681362420498455007509549517213285453773102481574390864574950259479765662844102553652977000035769295606566722752949297781646289262341623549414376262470908749643200171565760656987980763971637167709961003784180963669498213369651680678149962512216448400681654410536708661206594836597126012192813519797526082082969616915806299114666037943718435644796668877715954887614703727461595073689441920573791980162741306838415524808171520369350830683150672985523901>>>

admin public key:

n = 483901264006946269405283937218262944021205510033824140430120406965422208942781742610300462772237450489835092525764447026827915305166372385721345243437217652055280011968958645513779764522873874876168998429546523181404652757474147967518856439439314619402447703345139460317764743055227009595477949315591334102623664616616842043021518775210997349987012692811620258928276654394316710846752732008480088149395145019159397592415637014390713798032125010969597335893399022114906679996982147566245244212524824346645297637425927685406944205604775116409108280942928854694743108774892001745535921521172975113294131711065606768927
e = 65537

Service: http://36.110.234.253

这个题目现在已经没有办法在线获取 binary 了,现在得到的 binary 是之前已经下载好的,我们当时需要登录用户的 admin 来下载对应的 generator。

通过简单逆向这个 generator,我们可以发现这个程序是这么工作的

  • 利用用户给定的 license(32 个字节),迭代解密某个固定位置之后的数据,每 32 个字节一组,与密钥相异或得到结果。
  • 密钥的生成方法为
    • $k_1=key$
    • $k_2 =sha256(k_1)$
    • $k_n=sha256(k_{n-1})$

其中,固定位置就是在找源文件 generator 中第二次出现 ENCRYPTED 的位置,然后再次偏移 32 个字节。

    _ENCRYPT_STR = ENCRYPTED_STR;
    v10 = 0;
    ENCRYPTED_LEN = strlen(ENCRYPTED_STR);
    do
    {
      do
        ++v9;
      while ( strncmp(&file_contents[v9], _ENCRYPT_STR, ENCRYPTED_LEN) );
      ++v10;
    }
    while ( v10 <= 1 );
    v11 = &file_start_off_32[loc2 + ENCRYPTED_LEN];
    v12 = loc2 + ENCRYPTED_LEN;
    len = file_size - (loc2 + ENCRYPTED_LEN) - 32;
    decrypt(&file_start_off_32[v12], &license, len);
    sha256_file_start(v11, len, &output);
    if ( !memcmp(&output, &file_contents[v12], 0x20u) )
    {
      v14 = fopen("out.exe", "wb");
      fwrite(v11, 1u, len, v14);
      fclose(v14);
      sprintf(byte_406020, "out.exe %s", argv[1]);
      system(byte_406020);
    }

同时,我们需要确保生成的文件的校验对应的哈希值恰好为指定的值,由于文件最后是一个 exe 文件,所以我们可以认为最后的文件头就是标准的 exe 文件,因此就不需要知道原始的 license 文件,进而我们可以编写 python 脚本生成 exe。

在生成的 exe 中,我们分析出程序的基本流程为

  1. 读取 license
  2. 使用 license 作为 seed 分别生成 pq
  3. 利用 p,q 生成 n,e,d。

其漏洞出现在生成 p,q 的方法上,而且生成 p 和 q 的方法类似。

我们如果仔细分析下生成素数的函数的话,可以看到每个素数都是分为两部分生成的

  1. 生成左半部分 512 位。
  2. 生成右半部分 512 位。
  3. 左右构成 1024 比特位,判断是不是素数,是素数就成功,不是素数,继续生成。

其中生成每部分的方式相同,方式为

sha512(const1|const2|const3|const4|const5|const6|const7|const8|v9)
v9=r%1000000007

只有 v9 会有所变化,但是它的范围却是固定的。

那么,如果我们表示 p,q 为

$p=a*2^{512}+b$

$q=c*2^{512}+d$

那么

$n=pq=ac2^{1024}+(ad+bc)2^{512}+bd$

那么

$n \equiv bd \bmod 2^{512}$

而且由于 p 和 q 在生成时,a,b,c,d 均只有 1000000007 种可能性。

进而,我们可以枚举所有的可能性,首先计算出 b 可能的集合为 S,同时我们使用中间相遇攻击,计算

$n/d \equiv b \bmod 2^{512}$

这里由于 b 和 d 都是 p 的尾数,所以一定不会是 2 的倍数,进而必然存在逆元。

这样做虽然可以,然而,我们可以简单算一下存储空间

$64*1000000007 / 1024 / 1024 / 1024=59$

也就是说需要 59 G,太大了,,所以我们仍然需要进一步考虑

$n \equiv bd \bmod 2^{64}$

这样,我们的内存需求瞬间就降到了 8 G左右。我们仍然使用枚举的方法进行运算。

其次,我们不能使用 python,,python 占据空间太大,因此需要使用 c/c++ 编写。

枚举所有可能的 d 计算对应的值 $n/d$ 如果对应的值在集合 S 中,那么我们就可以认为找到了一对合法的 b 和 d,因此我们就可以恢复 p 和 q 的一半。

之后,我们根据

$n-bd=ac2^{1024}+(ad+bc)2^{512}$

可以得到

$\frac{n-bd}{2^{512}} = ac*2^{512}+ad+bc$

$\frac{n-bd}{2^{512}} \equiv ad+bc \bmod 2^{512}$

类似地,我们可以计算出 a 和 c,从而我们就可以完全恢复出 p 和 q。

在具体求解的过程中,在求 p 和 q 的一部分时,可以发现因为是模 $2^{64}$,所以可能存在碰撞(但其实就是一个是 p,另外一个是q,恰好对称。)。下面我们就求得了 b 对应的 v9。

注意:这里枚举出来的空间大约占用 11 个 G(包括索引),所以请选择合适的位置。

b64: 9646799660ae61bd idx_b: 683101175 idx_d: 380087137
search 23000000
search 32000000
search 2b000000
search d000000
search 3a000000
search 1c000000
search 6000000
search 24000000
search 15000000
search 33000000
search 2c000000
search e000000
b64: 9c63259ccab14e0b idx_b: 380087137 idx_d: 683101175
search 1d000000
search 3b000000
search 7000000
search 16000000
search 25000000
search 34000000

其实,我们在真正得到 p 或者 q 的一部分后,另外一部分完全可以使用暴力枚举的方式获取,因为计算量几乎都是一样的,最后结果为

...
hash 7000000
hash 30000000
p = 13941980378318401138358022650359689981503197475898780162570451627011086685747898792021456273309867273596062609692135266568225130792940286468658349600244497842007796641075219414527752166184775338649475717002974228067471300475039847366710107240340943353277059789603253261584927112814333110145596444757506023869
q = 34708215825599344705664824520726905882404144201254119866196373178307364907059866991771344831208091628520160602680905288551154065449544826571548266737597974653701384486239432802606526550681745553825993460110874794829496264513592474794632852329487009767217491691507153684439085094523697171206345793871065206283
plain text 13040004482825754828623640066604760502140535607603761856185408344834209443955563791062741885
hash 16000000
hash 25000000
hash b000000
hash 34000000
hash 1a000000
...2018-WCTF-rsa git:(master) ✗ python
Python 2.7.14 (default, Mar 22 2018, 14:43:05)
[GCC 4.2.1 Compatible Apple LLVM 9.0.0 (clang-900.0.39.2)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> p=13040004482825754828623640066604760502140535607603761856185408344834209443955563791062741885
>>> hex(p)[2:].decode('hex')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/Cellar/python@2/2.7.14_3/Frameworks/Python.framework/Versions/2.7/lib/python2.7/encodings/hex_codec.py", line 42, in hex_decode
    output = binascii.a2b_hex(input)
TypeError: Odd-length string
>>> hex(p)[2:-1].decode('hex')
'flag{fa6778724ed740396fc001b198f30313}'

最后我们便拿到 flag 了。

详细的利用代码请参见 ctf-challenge 仓库。

相关编译指令,需要链接相关的库。

g++  exp2.cpp -std=c++11 -o main2 -lgmp -lcrypto -pthread

背包加密

背包问题

首先,我们先来介绍一下背包问题,假定一个背包可以称重 W,现在有 n 个物品,其重量分别为 $a_1, a_2,…,a_n$ 我们想问一下装哪些物品可以恰好使得背包装满,并且每个物品只能被装一次。这其实就是在解这样的一个问题

$$
x_1a_1+x_2a_2+,…,+x_na_n=W
$$

其中所有的 $x_i$ 只能为 0 和 1。显然我们必须枚举所有的 n 个物品的组合才能解决这个问题,而复杂度也就是 $2^n$,这也就是背包加密的妙处所在。

在加密时,如果我们想要加密的明文为 x,那么我们可以将其表示为 n 位二进制数,然后分别乘上 $a_i$ 即可得到加密结果。

但是解密的时候,该怎么办呢?我们确实让其他人难以解密密文,但是我们自己也确实没有办法解密密文。

但是当 $a_i$ 是超递增的话,我们就有办法解了,所谓超递增是指序列满足如下条件

$$
a_i>\sum_{k=1}^{i-1}a_k
$$

即第 i 个数大于前面所有数的和。

为什么满足这样的条件就可以解密了呢?这是因为如果加密后的结果大于 $a_n$ 的话,其前面的系数为必须 1 的。反之,无论如何也无法使得等式成立。因此,我们可以立马得到对应的明文。

但是,这样又出现了一个问题,由于 $a_i$ 是公开的,如果攻击者截获了密文,那么它也就很容易去破解这样的密码。为了弥补这样的问题,就出现了 Merkle–Hellman 这样的加密算法,我们可以使用初始的背包集作为私钥,变换后的背包集作为公钥,再稍微改动加密过程,即可。

这里虽然说了超递增序列,但是却没有说是如何生成的。

Merkle–Hellman

公私钥生成

生成私钥

私钥就是我们的初始的背包集,这里我们使用超递增序列,怎么生成呢?我们可以假设 $a_1=1$,那么 $a_2$ 大于 1 即可,类似的可以依次生成后面的值。

生成公钥

在生成公钥的过程中主要使用了模乘的运算。

首先,我们生成模乘的模数 m,这里要确保

$$
m>\sum_{i=1}^{i=n}a_i
$$

其次,我们选择模乘的乘数 w,作为私钥并且确保

$$
gcd(w,m)=1
$$

之后,我们便可以通过如下公式生成公钥

$$
b_i \equiv w a_i \bmod m
$$

并将这个新的背包集 $b_i$ 和 m 作为公钥。

加解密

加密

假设我们要加密的明文为 v,其每一个比特位为 $v_i$,那么我们加密的结果为

$$
\sum_{i=1}^{i=n}b_iv_i \bmod m
$$

解密

对于解密方,首先可以求的 w 关于 m 的逆元 $w^{-1}$。

然后我们可以将得到的密文乘以 $w^{-1}$ 即可得到明文,这是因为

$$
\sum_{i=1}^{i=n}w^{-1}b_iv_i \bmod m=\sum_{i=1}^{i=n}a_iv_i \bmod m
$$

这里有

$$
b_i \equiv w a_i \bmod m
$$

对于每一块的加密的消息都是小于 m 的,所以求得结果自然也就是明文了。

破解

该加密体制在提出后两年后该体制即被破译,破译的基本思想是我们不一定要找出正确的乘数 w(即陷门信息),只需找出任意模数 m′ 和乘数 w′,只要使用 w′ 去乘公开的背包向量 B 时,能够产生超递增的背包向量即可。

例子

这里我们以 2014 年 ASIS Cyber Security Contest Quals 中的 Archaic 为例,题目链接

首先查看源程序

secret = 'CENSORED'
msg_bit = bin(int(secret.encode('hex'), 16))[2:]

首先得到了 secret 的所有二进制位。

其次,利用如下函数得到 keypair,包含公钥与私钥。

keyPair = makeKey(len(msg_bit))

仔细分析 makekey 函数,如下

def makeKey(n):
    privKey = [random.randint(1, 4**n)]
    s = privKey[0]
    for i in range(1, n):
        privKey.append(random.randint(s + 1, 4**(n + i)))
        s += privKey[i]
    q = random.randint(privKey[n-1] + 1, 2*privKey[n-1])
    r = random.randint(1, q)
    while gmpy2.gcd(r, q) != 1:
        r = random.randint(1, q)
    pubKey = [ r*w % q for w in privKey ]
    return privKey, q, r, pubKey

可以看出 prikey 是一个超递增序列,并且得到的 q 比 prikey 中所有数的和还要大,此外我们得到的 r,恰好与 q 互素,这一切都表明了该加密是一个背包加密。

果然加密函数就是对于消息的每一位乘以对应的公钥并求和。

def encrypt(msg, pubKey):
    msg_bit = msg
    n = len(pubKey)
    cipher = 0
    i = 0
    for bit in msg_bit:
        cipher += int(bit)*pubKey[i]
        i += 1
    return bin(cipher)[2:]

对于破解的脚本我们直接使用 GitHub 上的脚本。进行一些简单的修改。

import binascii
# open the public key and strip the spaces so we have a decent array
fileKey = open("pub.Key", 'rb')
pubKey = fileKey.read().replace(' ', '').replace('L', '').strip('[]').split(',')
nbit = len(pubKey)
# open the encoded message
fileEnc = open("enc.txt", 'rb')
encoded = fileEnc.read().replace('L', '')
print "start"
# create a large matrix of 0's (dimensions are public key length +1)
A = Matrix(ZZ, nbit + 1, nbit + 1)
# fill in the identity matrix
for i in xrange(nbit):
    A[i, i] = 1
# replace the bottom row with your public key
for i in xrange(nbit):
    A[i, nbit] = pubKey[i]
# last element is the encoded message
A[nbit, nbit] = -int(encoded)

res = A.LLL()
for i in range(0, nbit + 1):
    # print solution
    M = res.row(i).list()
    flag = True
    for m in M:
        if m != 0 and m != 1:
            flag = False
            break
    if flag:
        print i, M
        M = ''.join(str(j) for j in M)
        # remove the last bit
        M = M[:-1]
        M = hex(int(M, 2))[2:-1]
        print M

输出之后再解码下

295 [1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0]
415349535f3962643364356664323432323638326331393536383830366130373036316365
>>> import binascii
>>> binascii.unhexlify('415349535f3962643364356664323432323638326331393536383830366130373036316365')
'ASIS_9bd3d5fd2422682c19568806a07061ce'

需要注意的是,我们得到的 LLL 攻击得到的矩阵 res 的只包含 01 值的行才是我们想要的结果,因为我们对于明文加密时,会将其分解为二进制比特串。此外,我们还需要去掉对应哪一行的最后一个数字。

flag 是 ASIS_9bd3d5fd2422682c19568806a07061ce

离散对数

基本定义

在了解离散对数时,我们先来了解几个基本定义。

定义1

在群 G 中,g 为 G 的生成元,也就是说群 G 中每一个元素都可以写成 $y=g^k$,我们称 k 为 y 在群 G 中的对数。

定义2

设 $m\geq 1$,$(a,m)=1$ ,使得 $a^d \equiv 1\pmod m$ 成立的最小正整数 d 称为 a 对模 m 的指数或者阶,我们一般将其记为 $\delta_m(a)$。

定义3

当 $\delta_m(a)=\varphi(m)$ 时,称 a 是模 m 的原根,简称 m 的原根。

一些性质

性质1

使得 $a^d \equiv 1\pmod m$ 成立的最小正整数 $d$ ,必有$d\mid\varphi(m)$。

性质2

模 $m$ 剩余系存在原根的充要条件是 $m=2,4,p^{\alpha},2p^{\alpha}$ ,其中 $p$ 为奇素数, $\alpha$ 为正整数。

离散对数问题

已知 $g,p,y$ ,对于方程 $y\equiv g^x \pmod p$ ,求解 $x$ 是一个难解问题。但是当 $p$ 具有一定的特性时就可能可以求解,比如,这个群的阶是一个光滑数。

正是上述这个问题构成了目前很大一部分现代密码学,包括 Diffie–Hellman 密钥交换, ElGamal 算法,ECC 等。

离散对数求解方式

暴力破解

给定 $y\equiv g^x \pmod p$,我们可以暴力枚举 $x$ 从而得到真正的 $x$ 的值。

Baby-step giant-step

这一方法通常被称为小步大步法,这一方法使用了中间相遇攻击的思想。

我们可以令 $x=im+j$,其中 $m= \lceil \sqrt n\rceil$ ,那么整数 i 和 j 都在 0 到 m 的范围内。

因此

$$y=g^x=g^{im+j}$$

也就是

$$y(g^{-m})^i=g^j$$

那么我们就可以枚举所有的 j 并进行计算,并将其存储到一个集合 S 中,然后我们再次枚举 i,计算 $y(g^{-m})^i$,一旦我们发现计算的结果在集合 S 中,则说明我们得到了一个碰撞,进而得到了 i 和 j。

这显然是一个时间与空间的折中的方式,我们将一个 $O(n)$ 的时间复杂度,$O(1)$ 空间复杂度的算法转换为了一个$O(\sqrt n)$的时间复杂度和$O(\sqrt n)$ 的空间复杂度的算法。

其中

  • 每一次 j 的增加表示“baby-step”,一次乘上 $g$。
  • 每一次 i 的增加表示“giant-step”,一次乘上 $g^{-m}$ 。
def bsgs(g, y, p):
    m = int(ceil(sqrt(p - 1)))
    S = {pow(g, j, p): j for j in range(m)}
    gs = pow(g, p - 1 - m, p)
    for i in range(m):
        if y in S:
            return i * m + S[y]
        y = y * gs % p
    return None

Pollard’s ρ algorithm

我们可以以$O(\sqrt n)$的时间复杂度和$O(1)$ 的空间复杂度来解决上述问题。具体原理请自行谷歌。

Pollard’s kangaroo algorithm

如果我们知道 x 的范围为 $a \leq x \leq b$,那么我们可以以$O(\sqrt{b-a})$ 的时间复杂度解决上述问题。具体原理请自行谷歌。

Pohlig-Hellman algorithm

不妨假设上述所提到的群关于元素 $g$ 的阶为 $n$, $n$ 为一个光滑数: $n=\prod\limits_{i=1}^r p_i^{e_i}$。

  1. 对于每个 $i \in {1,\ldots,r}$ :
    1. 计算 $g_i \equiv g^{n/p_i^{e_i}} \pmod m$。根据拉格朗日定理, $g_i$ 在群中的阶为 $p_i^{e_i}$ 。
    2. 计算 $y_i \equiv y^{n/p_i^{e_i}} \equiv g^{xn/p_i^{e_i}} \equiv g_i^{x} \equiv g_i^{x \bmod p_i^{e_i}} \equiv g_i^{x_i} \pmod m$,这里我们知道 $y_i,m,g_i$,而$x_i$ 的范围为$[0,p_i^{e_i})$,由 $n$ 是一个光滑数,可知其范围较小,因此我们可以使用 Pollard’s kangaroo algorithm 等方法快速求得$x_i$。
  2. 根据上述的推导,我们可以得到对于 $i \in {1,\ldots,r}$ ,$x \equiv x_i \pmod{p_i^{e_i}}$ ,该式可用中国剩余定理求解。

上述过程可用下图简单描述:

其复杂度为$O\left(\sum\limits _i e_i\left(\log n+\sqrt{p_i}\right)\right)$,可以看出复杂度还是很低的。

但当 $n$ 为素数,$m=2n+1$,那么复杂度和 $O(\sqrt m)$ 是几乎没有差别的。

2018 国赛 crackme java

代码如下

import java.math.BigInteger;
import java.util.Random;

public class Test1 {
    static BigInteger two =new BigInteger("2");
    static BigInteger p = new BigInteger("11360738295177002998495384057893129964980131806509572927886675899422214174408333932150813939357279703161556767193621832795605708456628733877084015367497711");
    static BigInteger h= new BigInteger("7854998893567208831270627233155763658947405610938106998083991389307363085837028364154809577816577515021560985491707606165788274218742692875308216243966916");

    /*
     Alice write the below algorithm for encryption.
     The public key {p, h} is broadcasted to everyone.
    @param val: The plaintext to encrypt.
        We suppose val only contains lowercase letter {a-z} and numeric charactors, and is at most 256 charactors in length.
    */
    public static String pkEnc(String val){
        BigInteger[] ret = new BigInteger[2];
        BigInteger bVal=new BigInteger(val.toLowerCase(),36);
        BigInteger r =new BigInteger(new Random().nextInt()+"");
        ret[0]=two.modPow(r,p);
        ret[1]=h.modPow(r,p).multiply(bVal);
        return ret[0].toString(36)+"=="+ret[1].toString(36);
    }

    /* Alice write the below algorithm for decryption. x is her private key, which she will never let you know.
    public static String skDec(String val,BigInteger x){
        if(!val.contains("==")){
            return null;
        }
        else {
            BigInteger val0=new BigInteger(val.split("==")[0],36);
            BigInteger val1=new BigInteger(val.split("==")[1],36);
            BigInteger s=val0.modPow(x,p).modInverse(p);
            return val1.multiply(s).mod(p).toString(36);
        }
    }
   */

    public static void main(String[] args) throws Exception {
        System.out.println("You intercepted the following message, which is sent from Bob to Alice:");
        BigInteger bVal1=new BigInteger("a9hgrei38ez78hl2kkd6nvookaodyidgti7d9mbvctx3jjniezhlxs1b1xz9m0dzcexwiyhi4nhvazhhj8dwb91e7lbbxa4ieco",36);
    BigInteger bVal2=new BigInteger("2q17m8ajs7509yl9iy39g4znf08bw3b33vibipaa1xt5b8lcmgmk6i5w4830yd3fdqfbqaf82386z5odwssyo3t93y91xqd5jb0zbgvkb00fcmo53sa8eblgw6vahl80ykxeylpr4bpv32p7flvhdtwl4cxqzc",36);
    BigInteger r =new BigInteger(new Random().nextInt()+"");
    System.out.println(r);
        System.out.println(bVal1);
    System.out.println(bVal2);
    System.out.println("a9hgrei38ez78hl2kkd6nvookaodyidgti7d9mbvctx3jjniezhlxs1b1xz9m0dzcexwiyhi4nhvazhhj8dwb91e7lbbxa4ieco==2q17m8ajs7509yl9iy39g4znf08bw3b33vibipaa1xt5b8lcmgmk6i5w4830yd3fdqfbqaf82386z5odwssyo3t93y91xqd5jb0zbgvkb00fcmo53sa8eblgw6vahl80ykxeylpr4bpv32p7flvhdtwl4cxqzc");
        System.out.println("Please figure out the plaintext!");
    }
}

基本功能为计算

$r_0=2^r \bmod p$

$r_1 =b*h^r \bmod p$

可以发现,r 的范围为 $[0,2^{32})$,所以我们可以使用 BSGS 算法,如下

from sage.all import *

c1 = int(
    'a9hgrei38ez78hl2kkd6nvookaodyidgti7d9mbvctx3jjniezhlxs1b1xz9m0dzcexwiyhi4nhvazhhj8dwb91e7lbbxa4ieco',
    36
)
c2 = int(
    '2q17m8ajs7509yl9iy39g4znf08bw3b33vibipaa1xt5b8lcmgmk6i5w4830yd3fdqfbqaf82386z5odwssyo3t93y91xqd5jb0zbgvkb00fcmo53sa8eblgw6vahl80ykxeylpr4bpv32p7flvhdtwl4cxqzc',
    36
)
print c1, c2
p = 11360738295177002998495384057893129964980131806509572927886675899422214174408333932150813939357279703161556767193621832795605708456628733877084015367497711
h = 7854998893567208831270627233155763658947405610938106998083991389307363085837028364154809577816577515021560985491707606165788274218742692875308216243966916
# generate the group
const2 = 2
const2 = Mod(const2, p)
c1 = Mod(c1, p)
c2 = Mod(c2, p)
h = Mod(h, p)
print '2', bsgs(const2, c1, bounds=(1, 2 ^ 32))

r = 152351913

num = long(c2 / (h**r))
print num

ElGamal

概述

ElGamal算法的安全性是基于求解离散对数问题的困难性,于1984年提出,也是一种双钥密码体制,既可以用于加密又可用于数字签名。

如果我们假设p是至少是160位的十进制素数,并且p-1有大素因子,此外g是 $Z_p^$ 的生成元,并且 $y \in Z_p^$ 。那么如何找到一个唯一的整数x($0\leq x \leq p-2$) ,满足$g^x \equiv y \bmod p$ 在算法上是困难的,这里将x记为$x=log_gy$ 。

基本原理

这里我们假设A要给B发送消息m。

密钥生成

基本步骤如下

  1. 选取一个足够大的素数p,以便于在$Z_p$ 上求解离散对数问题是困难的。
  2. 选取$Z_p^*$ 的生成元g。
  3. 随机选取整数k,$0\leq k \leq p-2$ ,并计算$g^k \equiv y \bmod p$ 。

其中私钥为{k},公钥为{p,g,y} 。

加密

A选取随机数$r \in Z_{p-1}$ ,对明文加密$E_k(m,r)=(y_1,y_2)$ 。其中$y_1 \equiv g^r \bmod p$ ,$y_2 \equiv my^r \bmod p$ 。

解密

$D_k(y_1,y_2)=y_2(y_1^k)^-1 \bmod p \equiv m(g^k)^r(g^{rk})^{-1} \equiv m \bmod p$ 。

难点

虽然我们知道了y1,但是我们却没有办法知道其对应的r。

2015 MMA CTF Alicegame

这里我们以2015年 MMA-CTF-2015 中的 Alicegame 为例进行介绍。这题最初在没有给出源码的时候却是比较难做,因为这个给一个 m,给一个 r 就得到加密结果,,这太难想。

我们来简单分析一下源码,首先程序最初生成了 pk 与 sk

    (pk, sk) = genkey(PBITS)

其中genkey函数如下

def genkey(k):
    p = getPrime(k)
    g = random.randrange(2, p)
    x = random.randrange(1, p-1)
    h = pow(g, x, p)
    pk = (p, g, h)
    sk = (p, x)
    return (pk, sk)

p为k位的素数,g为(2,p)范围内的书,x在(1,p-1)范围内。并且计算了$h \equiv g^x \bmod p$ 。看到这里,差不多就知道,这应该是一个数域上的ElGamal加密了。其中pk为公钥,sk为私钥。

接下来 程序输出了10次m和r。并且,利用如下函数加密

def encrypt(pk, m, r = None):
    (p, g, h) = pk
    if r is None:
        r = random.randrange(1, p-1)
    c1 = pow(g, r, p)
    c2 = (m * pow(h, r, p)) % p
    return (c1, c2)

其加密方法确实是ElGamal方式的加密。

最后程序对flag进行了加密。此时的r是由程序自己random的。

分析一下,这里我们在十轮循环中可以控制m和r,并且

$c_1 \equiv g^r \bmod p$

$c_2 \equiv m * h^{r} \bmod p$

如果我们设置

  1. r=1,m=1,那么我们就可以获得$c_1=g,c_2=h$ 。
  2. r=1,m=-1,那么我们就可以获得$c_1=g, c_2 = p-h$ 。进而我们就可以得到素数p。

我们得到素数p有什么用呢?p的位数在201位左右,很大啊。

但是啊,它生成素数p之后,没有进行检查啊。我们在之前说过p-1必须有大素因子,如果有小的素因子的话,那我们就可以攻击了。其攻击主要是使用到了baby step-giant step 与 Pohlig-Hellman algorithm 算法,有兴趣的可以看看,这里sage本身自带的计算离散对数的函数已经可以处理这样的情况了,参见discrete_log

具体代码如下,需要注意的是,,这个消耗内存比较大,,不要随便拿虚拟机跑。。。还有就是这尼玛交互让我头疼啊,,,

import socket
from Crypto.Util.number import *
from sage.all import *


def get_maxfactor(N):
    f = factor(N)
    print 'factor done'
    return f[-1][0]

maxnumber = 1 << 70
i = 0
while 1:
    print 'cycle: ',i
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect(("localhost", 9999))
    sock.recv(17)
    # get g,h
    sock.recv(512)
    sock.sendall("1\n")
    sock.recv(512)
    sock.sendall("1\n")
    data = sock.recv(1024)
    print data
    if '\n' in data:
        data =data[:data.index('\n')]
    else:
        # receive m=
        sock.recv(1024)
    (g,h) = eval(data)

    # get g,p
    sock.sendall("-1\n")
    sock.recv(512)
    sock.sendall("1\n")
    data = sock.recv(1024)
    print data
    if '\n' in data:
        data = data[:data.index('\n')]
    else:
        # receive m=
        sock.recv(512)
    (g,tmp) = eval(data)
    p = tmp+h
    tmp = get_maxfactor(p-1)
    if tmp<maxnumber:
        print 'may be success'
        # skip the for cycle
        sock.sendall('quit\n');
        data = sock.recv(1024)
        print 'receive data: ',data
        data = data[data.index(":")+1:]
        (c1,c2)=eval(data)
        # generate the group
        g = Mod(g, p)
        h = Mod(h, p)
        c1 = Mod(c1, p)
        c2 = Mod(c2, p)
        x = discrete_log(h, g)
        print "x = ", x
        print "Flag: ", long_to_bytes(long(c2 / ( c1 ** x)))
    sock.sendall('quit\n')
    sock.recv(1024)
    sock.close()
    i += 1

最后迫于计算机内存不够,,没计算出来,,,有时候会崩,多运行几次。。

2018 Code Blue lagalem

题目描述如下

from Crypto.Util.number import *
from key import FLAG

size = 2048
rand_state = getRandomInteger(size // 2)


def keygen(size):
    q = getPrime(size)
    k = 2
    while True:
        p = q * k + 1
        if isPrime(p):
            break
        k += 1
    g = 2
    while True:
        if pow(g, q, p) == 1:
            break
        g += 1
    A = getRandomInteger(size) % q
    B = getRandomInteger(size) % q
    x = getRandomInteger(size) % q
    h = pow(g, x, p)
    return (g, h, A, B, p, q), (x,)


def rand(A, B, M):
    global rand_state
    rand_state, ret = (A * rand_state + B) % M, rand_state
    return ret


def encrypt(pubkey, m):
    g, h, A, B, p, q = pubkey
    assert 0 < m <= p
    r = rand(A, B, q)
    c1 = pow(g, r, p)
    c2 = (m * pow(h, r, p)) % p
    return (c1, c2)

# pubkey, privkey = keygen(size)

m = bytes_to_long(FLAG)
c1, c2 = encrypt(pubkey, m)
c1_, c2_ = encrypt(pubkey, m)

print pubkey
print(c1, c2)
print(c1_, c2_)

可以看出,该算法就是一个 ElGamal 加密,给了同一个明文两组加密后的结果,其特点在于使用的随机数 r 是通过线性同余生成器生成的,则我们知道

$c2 \equiv m * h^{r} \bmod p$

$c2_ \equiv mh^{(Ar+B) \bmod q} \equiv mh^{Ar+B}\bmod p$

$c2^A*h^B/c2_ \equiv m^{A-1}\bmod p$

其中,c2,c2_,A,B,h 均知道。则我们知道

$m^{A-1} \equiv t \bmod p$

我们假设已知 p 的一个原根 g,则我们可以假设

$g^x \equiv t$

$g^y \equiv m$

$g^{y(A-1)}\equiv g^x \bmod p$

$y(A-1) \equiv x \bmod p-1$

进而我们知道

$y(A-1)-k(p-1)=x$

这里我们知道 A,p,x,则我们可以利用扩展欧几里得定理求得

$s(A-1)+w(p-1)=gcd(A-1,t-1)$

如果gcd(A-1,t-1)=d,则我们直接计算

$t^s \equiv m^{s(A-1)} \equiv m^d \bmod p$

如果 d=1,则直接知道 m。

如果 d 不为1,则就有点麻烦了。。

这里这道题目中恰好 d=1,因此可以很容易进行求解。

import gmpy2
data = open('./transcript.txt').read().split('\n')
g, h, A, B, p, q = eval(data[0])

c1, c2 = eval(data[1])
c1_, c2_ = eval(data[2])

tmp = gmpy2.powmod(c2, A, p) * gmpy2.powmod(h, B, p) * gmpy2.invert(c2_, p)
tmp = tmp % p

print 't=', tmp
print 'A=', A
print 'p=', p
gg, x, y = gmpy2.gcdext(A - 1, p - 1)
print gg

m = gmpy2.powmod(tmp, x, p)
print hex(m)[2:].decode('hex')

flag

➜  2018-CodeBlue-lagalem git:(master) ✗ python exp.py
t= 24200833701856688878756977616650401715079183425722900529883514170904572086655826119242478732147288453761668954561939121426507899982627823151671207325781939341536650446260662452251070281875998376892857074363464032471952373518723746478141532996553854860936891133020681787570469383635252298945995672350873354628222982549233490189069478253457618473798487302495173105238289131448773538891748786125439847903309001198270694350004806890056215413633506973762313723658679532448729713653832387018928329243004507575710557548103815480626921755313420592693751934239155279580621162244859702224854316335659710333994740615748525806865323
A= 22171697832053348372915156043907956018090374461486719823366788630982715459384574553995928805167650346479356982401578161672693725423656918877111472214422442822321625228790031176477006387102261114291881317978365738605597034007565240733234828473235498045060301370063576730214239276663597216959028938702407690674202957249530224200656409763758677312265502252459474165905940522616924153211785956678275565280913390459395819438405830015823251969534345394385537526648860230429494250071276556746938056133344210445379647457181241674557283446678737258648530017213913802458974971453566678233726954727138234790969492546826523537158
p= 36416598149204678746613774367335394418818540686081178949292703167146103769686977098311936910892255381505012076996538695563763728453722792393508239790798417928810924208352785963037070885776153765280985533615624550198273407375650747001758391126814998498088382510133441013074771543464269812056636761840445695357746189203973350947418017496096468209755162029601945293367109584953080901393887040618021500119075628542529750701055865457182596931680189830763274025951607252183893164091069436120579097006203008253591406223666572333518943654621052210438476603030156263623221155480270748529488292790643952121391019941280923396132717
1
CBCTF{183a3ce8ed93df613b002252dfc741b2}

ECC

概述

ECC 全称为椭圆曲线加密,EllipseCurve Cryptography,是一种基于椭圆曲线数学的公钥密码。与传统的基于大质数因子分解困难性的加密方法不同,ECC依赖于解决椭圆曲线离散对数问题的困难性。它的优势主要在于相对于其它方法,它可以在使用较短密钥长度的同时保持相同的密码强度。目前椭圆曲线主要采用的有限域有

  • 以素数为模的整数域GF(p),通常在通用处理器上更为有效。
  • 特征为 2 的伽罗华域GF(2^m),可以设计专门的硬件。

基本知识

我们首先来了解一下有限域上的椭圆曲线,有限域上的椭圆曲线是指在椭圆曲线的定义式

$y^2+axy+by=x^3+cx^2+dx+e$

中所有的系数都是在某个有限域GF(p)中的元素,其中p为一个大素数。

当然,并不是所有的椭圆曲线都适合于加密,最为常用的方程如下

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

其中$4a^3+27b^2 \bmod p \neq 0$

我们称该方程的所有解(x,y),($x\in Fp , y \in Fp$),以及一个称为“无穷远点”(O)组成的集合为定义在Fp上的一个椭圆曲线,记为E(Fp)。

一般定义椭圆曲线密码需要以下条件

假设E(Fp)对于点的运算$\oplus$ 形成一个able群(交换群,逆元存在,封闭性等),设$p\in E(Fq)$ ,且满足下列条件的t很大

$p \oplus p \oplus … \oplus p=O$

其中共有t个p参与运算。这里我们称t为p的周期。此外,对于$Q\in E(Fq)$ ,定有某个正整数m使得下列式子成立,定义$m=log_pq$

$Q=m\cdot p =p \oplus p \oplus … \oplus p$ (m个p参与运算)

此外,假设G是该$E_q (a,b)$ 的生成元,即可以生成其中的所有元素,其阶为满足$nG=O$ 的最小正整数n。

ECC中的ElGamal

这里我们假设用户B要把消息加密后传给用户A。

密钥生成

用户A先选择一条椭圆曲线$E_q (a,b)$ ,然后选择其上的一个生成元G,假设其阶为n,之后再选择一个正整数$n_a$作为密钥,计算$P_a=n_aG$。

其中,$E_q(a,b), q,G$都会被公开。

公钥为$P_a$,私钥为$n_a $。

加密

用户B在向用户A发送消息m,这里假设消息m已经被编码为椭圆曲线上的点,其加密步骤如下

  1. 查询用户A的公钥$E_q(a,b), q, P_a,G$ 。
  2. 在(1,q-1) 的区间内选择随机数k 。
  3. 根据A的公钥计算点$(x_1,y_1)=kG$ 。
  4. 计算点$(x_2,y_2)=kP_a$ ,如果为O,则从第二步重新开始。
  5. 计算$C=m+(x_2,y_2)$
  6. 将$((x_1,y_1),C)$ 发送给A。

解密

解密步骤如下

  1. 利用私钥计算点$n_a(x_1,y_1)=n_akG=kP_a=(x_2,y_2)$。
  2. 计算消息$m=C-(x_2,y_2)$ 。

关键点

这里的关键点在于我们即使知道了$(x_1,y_1)$ 也难以知道k,这是由离散对数的问题的难度决定的。

2013 SECCON CTF quals Cryptanalysis

这里我们以2013年SECCON CTF quals 中的 Cryptanalysis 为例,题目如下

这里,我们已知椭圆曲线方程以及对应的生成元 base,还知道相应的模数以及公钥以及加密后的结果。

但是可以看出的我们的模数太小,我们暴力枚举获取结果。

这里直接参考 github上的 sage 程序,暴力跑出 secret key。之后便可以解密了。


a = 1234577
b = 3213242
n = 7654319

E = EllipticCurve(GF(n), [0, 0, 0, a, b])

base = E([5234568, 2287747])
pub = E([2366653, 1424308])

c1 = E([5081741, 6744615])
c2 = E([610619, 6218])

X = base

for i in range(1, n):
    if X == pub:
        secret = i
        print "[+] secret:", i
        break
    else:
        X = X + base
        print i

m = c2 - (c1 * secret)

print "[+] x:", m[0]
print "[+] y:", m[1]
print "[+] x+y:", m[0] + m[1]

暴力跑出结果

[+] secret: 1584718
[+] x: 2171002
[+] y: 3549912
[+] x+y: 5720914

格概述

格在数学上至少有两种含义

  • 定义在非空有限集合上的偏序集合 L,满足集合 L 中的任意元素 a,b,使得 a,b 在 L 中存在一个最大下界,和最小上界。具体参见https://en.wikipedia.org/wiki/Lattice_(order)。
  • 群论中的定义,是 $R^n$ 中的满足某种性质的子集。当然,也可以是其它群。

目前关于格方面的研究主要有以下几大方向

  1. 格中计算问题的困难性,即这些问题的计算复杂性,主要包括
    1. SVP 问题
    2. CVP 问题
  2. 如何求解格中的困难性问题,目前既有近似算法,也有一些精确性算法。
  3. 基于格的密码分析,即如何利用格理论分析一些已有的密码学算法,目前有如下研究
    1. Knapsack cryptosystems
    2. DSA nonce biases
    3. Factoring RSA keys with bits known
    4. Small RSA private exponents
    5. Stereotyped messages with small RSA exponents
  4. 如何基于格困难问题设计新的密码体制,这也是后量子密码时代的重要研究方向之一,目前有以下研究
    1. Fully homomorphic encryption
    2. The Goldreich–Goldwasser–Halevi (GGH) cryptosystem
    3. The NTRU cryptosystem
    4. The Ajtai–Dwork cryptosystem and the LWE cryptosystem

格定义

格是 m 维欧式空间 $R^m$ 的 n ($m\geq n$) 个线性无关向量$b_i(1\leq i \leq n)$ 的所有整系数的线性组合,即
$L(B)={\sum\limits_{i=1}^{n}x_ib_i:x_i \in Z,1\leq i \leq n}$

这里 B 就是 n 个向量的集合,我们称

  • 这 n 个向量是格 L 的一组基。
  • 格 L 的秩为 n。
  • 格 L 的位数为 m。

如果 m=n,那么我们称这个格式满秩的。

当然,也可以是其它群,不是 $R^m$。

格中若干基本定义

successive minima

格是 m 维欧式空间 $R^m$ 的秩为 n 的格,那么 L 的连续最小长度(successive minima)为 $\lambda_1,…,\lambda_n \in R$,满足对于任意的 $1\leq i\leq n$,$\lambda_i$ 是满足格中 i 个线性无关的向量$v_i$, $||v_j||\leq \lambda_i,1\leq j\leq i$ 的最小值。

自然的 $\lambda_i \leq \lambda_j ,\forall i <j$。

格中计算困难性问题

最短向量问题(Shortest Vector Problem,SVP):给定格 L 及其基向量 B ,找到格 L 中的非零向量 v 使得对于格中的任意其它非零向量 u,$||v|| \leq ||u||$。

$\gamma$-近似最短向量问题(SVP-$\gamma$):给定格 L,找到格 L 中的非零向量 v 使得对于格中的任意其它非零向量 u,$||v|| \leq \gamma||u||$。

连续最小长度问题(Successive Minima Problem, SMP):给定秩为 n 的格 L,找到格 L 中 n 个线性无关向量 $s_i$,满足 $\lambda_i(L)=||s_i||, 1\leq i \leq n$。

最短线性无关向量问题(Shortest Independent Vector Problem, SIVP):给定一个秩为 n 的格 L,找到格 L 中 n 个线性无关向量 $s_i$,满足$||s_i|| \leq \lambda_n(L), 1\leq i \leq n$。

唯一最短向量问题(Unique Shortest Vector Problem, uSVP-$\gamma$):给定格 L,满足 $ \lambda_2(L) > \gamma \lambda_1(L)$,找到该格的最短向量。

最近向量问题(Closest Vector Problem,CVP):给定格 L和目标向量 $t\in R^m$,找到一个格中的非零向量 v,使得对于格中的任意非零向量 u,满足 $||v-t|| \leq ||u-t||$ 。

格基规约算法

Lenstra–Lenstra–Lovasz

基本介绍

LLL 算法就是在格上找到一组基,满足如下效果

而且,这种方法生成的基所具有的如下性质是非常有用的

简单应用

这里我举一下 LLL paper 中给的第二个例子。给定 n 个实数 $\alpha_i,…,\alpha_n$,找到这 n 个数的有理线性逼近,即找到 n 个数 $m_i$,使得 $\sum\limits_{i=1}^{n}m_i\alpha_i$ 尽可能等于 0。 我们可以构造这样的矩阵,这里 $a_i$ 为 $\alpha_i$ 的有理逼近。

$$ A = \left[ \begin{matrix} 1 & 0 & 0 & \cdots & 0 & ca_1 \ 0 & 1 & 0 & \cdots & 0 & c a_2 \ 0 & 0 & 1 & \cdots & 0 & c a_3 \\vdots & \vdots & \vdots & \ddots & \vdots \ 0 & 0 &0 & \cdots & 1 & c a_n \ \end{matrix} \right]$$

矩阵为 n*(n+1) 的,我们可以根据格求行列式的方法来求一下这个格对应的行列式。

$det(L)=\sqrt{AA^T}$

我们进一步考虑这样的矩阵

$$ A = \left[ \begin{matrix} 1 & 0 & 0 & \cdots & 0 & a_1 \ 0 & 1 & 0 & \cdots & 0 & a_2 \ 0 & 0 & 1 & \cdots & 0 & a_3 \\vdots & \vdots & \vdots & \ddots & \vdots \ 0 & 0 &0 & \cdots & 1 & a_n \ \end{matrix} \right]$$

那么

$$ AA^T = \left[ \begin{matrix} 1+a_1^2 & a_1a_2 & a_1a_3 & \cdots & a_1a_n \ a_2a_1 & 1+a_2^2 & a_2a_3 & \cdots & a_2a_n \ a_3a_1 & a_3a_2 & 1+a_3^2 & \cdots & a_3a_n \ \vdots & \vdots & \vdots & \ddots & \vdots \ a_na_1 & a_na_2 &a_na_3 & \cdots & 1+a_n^2 \ \end{matrix} \right]$$

进一步我们从低维到高维大概试一试(严格证明,可以考虑添加一行和一列,左上角为1),得到格的行列式为

$\sqrt{1+\sum\limits_{i=1}^n\alpha_i^2}$

可以参见考研宇哥的如下证明

那么经过 LLL 算法后,我们可以获得

$||b_1|| \leq 2^{\frac{n-1}{4}} (1+\sum\limits_{i=1}^n\alpha_i^2)^{\frac{1}{2(n+1)}}$

一般来说后一项在开 n 次方时趋向于1,因为 $a_i$ 都是常数,一般不会和 n 相关,所以

$||b_1|| \leq 2^{\frac{n-1}{4}}*k$

k 比较小。此外,$b_1$ 又是原向量的线性组合,那么

$b_1[n]=\sum\limits_{i=1}^{n}m_ica_i=c\sum\limits_{i=1}^{n}m_ia_i$

显然如果 c 足够大,那么后面的求和必须足够小,才可以满足上面的约束。

CVP

CVP是Lattice-based cryptography中尤为重要的一个问题。

问题的基本定义如下:给定格$L$的一组基与向量$\mathbf{v}$,找到在$L$上离$\mathbf{v}$最近的一个向量。

Algorithms

Babai’s nearest plane algorithm

该算法输入一组格$L$(秩为$n$)的基$B$和一个目标向量$\mathbf{t}$,输出CVP问题的近似解。

  • 近似因子为$\gamma = 2^{\frac{n}{2}}$

具体算法:

  • 其中$c_j$为Gram-schmidt正交化中的系数取整,也即$proj_{b_{j}}(b)$的取整。

对于该算法第二步的个人理解:在格基规约和正交化过后的基$B$中找到一个最靠近$\mathbf{t}$的线性组合。

Babai’s Rounding Technique

该算法是Babai's nearest plane algorithm的一个变种。

步骤可以表示为:

N = rank(B), w = target
- B' = LLL(B)
- Find a linear combination [l_0, ... l_N] such that w = sum(l_i * b'_i).
* (b'_i is the i-th vector in the LLL-reduced basis B')
- Round each l_i to it's closest integer l'_i.
- Result v = sum(l'_i * b'_i)

相关内容

Hidden number problem

HNP的定义如下:

给定质数$p$、许多$t \in \mathbb{F}p$以及每一个对应的$MSB{l,p}(\alpha t)$,找出对应的$\alpha$。

  • $MSB_{l,p}(x)$表示任一满足 $\lvert (x \mod p) - u \rvert \le \frac{p}{2^{l+1}}$ 的整数 $u$,近似为取$x \mod p$的$l$个最高有效位。

根据参考3中的描述,当$l \approx \log^{\frac{1}{2}}{p}$时,有如下算法可以解决HNP:

我们可以将此问题转化为一个由该矩阵生成的格上的CVP问题:

$\left[ \begin{matrix} p & 0 & \dots & 0 & 0 \ 0 & p & \ddots & \vdots & \vdots \ \vdots & \ddots & \ddots & 0 & \vdots \ 0 & 0 & \dots & p & 0 \ t_1 & t_2 & \dots & t_{n} & \frac{1}{2^{l+1}} \end{matrix} \right]$

我们需要找到在格上离$\mathbf{u}=(u_1, u_2, \dots, u_{n}, 0)$最近的向量,所以在这里,我们可以采用Babai's nearest plane algorithm。最终我们可以得到一组向量 $\mathbf{v}=(\alpha \cdot t_1 \mod p, \alpha \cdot t_2 \mod p, \dots, \frac{\alpha}{2^{l+1}})$,从而算出 $\alpha$。

BCTF 2018 - guess_number

题目提供了服务器端的代码:

import random, sys
from flag import FLAG
import gmpy2

def msb(k, x, p):
    delta = p >> (k + 1)
    ui = random.randint(x - delta, x + delta)
    return ui

def main():
    p = gmpy2.next_prime(2**160)
    for _ in range(5):
        alpha = random.randint(1, p - 1)
        # print(alpha)
        t = []
        u = []
        k = 10
        for i in range(22):
            t.append(random.randint(1, p - 1))
            u.append(msb(k, alpha * t[i] % p, p))
        print(str(t))
        print(str(u))
        guess = raw_input('Input your guess number: ')
        guess = int(guess)
        if guess != alpha:
            exit(0)

if __name__ == "__main__":
    main()
    print(FLAG)

可以看到,程序一共执行5轮。在每一轮,程序会生成一个随机的$\alpha$和22个随机的$t_i$。对于每一个$t_i$,程序会取$u_i = MSB_{10,p}(\alpha\cdot{t_i\mod{p}})$,随后发送给客户端。我们需要根据提供的$t_i$和$u_i$计算出对应的$\alpha$。可以看到,该问题是一个典型的Hidden number problem,于是可以使用上述算法解决:

import socket
import ast
import telnetlib

#HOST, PORT = 'localhost', 9999
HOST, PORT = '60.205.223.220', 9999

s = socket.socket()
s.connect((HOST, PORT))
f = s.makefile('rw', 0)

def recv_until(f, delim='\n'):
    buf = ''
    while not buf.endswith(delim):
        buf += f.read(1)
    return buf

p = 1461501637330902918203684832716283019655932542983
k = 10

def solve_hnp(t, u):
    # http://www.isg.rhul.ac.uk/~sdg/igor-slides.pdf
    M = Matrix(RationalField(), 23, 23)
    for i in xrange(22):
        M[i, i] = p
        M[22, i] = t[i]

    M[22, 22] = 1 / (2 ** (k + 1))

    def babai(A, w):
        A = A.LLL(delta=0.75)
        G = A.gram_schmidt()[0]
        t = w
        for i in reversed(range(A.nrows())):
            c = ((t * G[i]) / (G[i] * G[i])).round()
            t -= A[i] * c
        return w - t

    closest = babai(M, vector(u + [0]))
    return (closest[-1] * (2 ** (k + 1))) % p

for i in xrange(5):
    t = ast.literal_eval(f.readline().strip())
    u = ast.literal_eval(f.readline().strip())
    alpha = solve_hnp(t, u)
    recv_until(f, 'number: ')
    s.send(str(alpha) + '\n')

t = telnetlib.Telnet()
t.sock = s
t.interact()

哈希函数

哈希函数(Hash Function)把消息或数据压缩成摘要,使得数据量变小。其一般模型如下

显然对于任何一个 hash 值,理论上存在若干个消息与之对应,即碰撞。

哈希函数的基本需求如下

需求 描述
输入长度可变 hash 函数可以应用于任意长度的数据
输出长度固定 hash 函数的输出长度固定
效率 对于任意消息 xx,计算 H(x)H(x) 很容易
单向性 对于任意哈希值 h,想要找到满足H(x)=hH(x)=h 的 x 在计算上不可行。
抗弱碰撞性 对于任意消息 x,找到满足另一消息 y,满足H(x)=H(y)H(x)=H(y) ,在计算上不可行。
抗强碰撞性 找到任意一对满足 H(x)=H(y)H(x)=H(y) 的消息 x 和 y 在计算上不可行。
伪随机性 哈希函数的输出满足伪随机性测试标准。

散列值的目的如下

  • 确保消息的完整性,即确保收到的数据确实和发送时的一样(即没有修改、插入、删除或重放),防止中间人篡改。
  • 冗余校验
  • 单向口令文件,比如 linux 系统的密码
  • 入侵检测和病毒检测中的特征码检测

目前的 Hash 函数主要有 MD5,SHA1,SHA256,SHA512。目前的大多数 hash 函数都是迭代性的,即使用同一个 hash 函数,不同的参数进行多次迭代运算。

算法类型 输出 Hash 值长度
MD5 128 bit / 256 bit
SHA1 160 bit
SHA256 256 bit
SHA512 512 bit

MD5

基本描述

MD5 的输入输出如下

  • 输入:任意长的消息,512 比特长的分组。
  • 输出:128 比特的消息摘要。

关于详细的介绍,请自行搜索。

此外,有时候我们获得到的 md5 是 16 位的,其实那 16 位是 32 位 md5 的长度,是从 32 位 md5 值来的。是将 32 位 md5 去掉前八位,去掉后八位得到的。

一般来说,我们可以通过函数的初始化来判断是不是 MD5 函数。一般来说,如果一个函数有如下四个初始化的变量,可以猜测该函数为 MD5 函数,因为这是 MD5 函数的初始化 IV。

0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476

破解

目前可以说 md5 已经基本被攻破了,一般的 MD5 的碰撞都可以在如下网上获取到

SHA1

基本描述

SHA1的输入输出如下

  • 输入:任意长的消息,分为 512 比特长的分组。首先在消息右侧补比特 1,然后再补若干个比特 0,直到消息的比特长度满足对 512 取模后余数是 448,使其与 448 模 512 同余。
  • 输出:160 比特的消息摘要。

关于详细的介绍,请自行搜索。

一般来说,我们可以通过函数的初始化来判断是不是 SHA1 函数。一般来说,如果一个函数有如下五个初始化的变量,可以猜测该函数为 SHA1 函数,因为这是 SHA1 函数的初始化IV。

0x67452301
0xEFCDAB89
0x98BADCFE
0x10325476
0xC3D2E1F0

前面四个与 MD5 类似,后面的是新加的。

破解

就目前而言,SHA1 已经不再安全了,因为之前谷歌公布了求得两个 sha1 值一样的 pdf,具体请参考 shattered

这里还有一个比较有意思的网站:https://alf.nu/SHA1。

2017 SECCON SHA1 is dead

题目描述如下

  1. file1 != file2
  2. SHA1(file1) == SHA1(file2)
  3. SHA256(file1) <> SHA256(file2)
  4. 2017KiB < sizeof(file1) < 2018KiB
  5. 2017KiB < sizeof(file2) < 2018KiB

其中 1KiB = 1024 bytes

即我们需要找到两个文件满足上述的约束。

这里立马就想到谷歌之前公布的文档,而且,非常重要的是,只要使用给定的前 320 字节,后面任意添加一样的字节获取的哈希仍然一样,这里我们测试如下

➜  2017_seccon_sha1_is_dead git:(master) dd bs=1 count=320 

进而我们直接写程序即可,如下

from hashlib import sha1
from hashlib import sha256

pdf1 = open('./shattered-1.pdf').read(320)
pdf2 = open('./shattered-2.pdf').read(320)
pdf1 = pdf1.ljust(2017 * 1024 + 1 - 320, "\00")  #padding pdf to 2017Kib + 1
pdf2 = pdf2.ljust(2017 * 1024 + 1 - 320, "\00")
open("upload1", "w").write(pdf1)
open("upload2", "w").write(pdf2)

print sha1(pdf1).hexdigest()
print sha1(pdf2).hexdigest()
print sha256(pdf1).hexdigest()
print sha256(pdf2).hexdigest()

Fowler–Noll–Vo hash function

具体请参见 https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function。

2018 网鼎杯 hashcoll

其实这道题是从 NSU Crypto 抄过来的,https://nsucrypto.nsu.ru/archive/2017/problems_solution,具体的 wp 之前 hellman 也写了,https://gist.github.com/hellman/9bf8376cd04e7a8dd2ec7be1947261e9。

简单看一下题目

h0 = 45740974929179720441799381904411404011270459520712533273451053262137196814399

# 2**168 + 355
g = 374144419156711147060143317175368453031918731002211L


def shitty_hash(msg):
    h = h0
    msg = map(ord, msg)
    for i in msg:
        h = (h + i) * g
        # This line is just to screw you up :))
        h = h & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

    return h - 0xe6168647f636

题目希望我们给出两个消息,其哈希值相同。如果我们将该函数展开的话,那么

$hash(m)=h_0g^n+x_1g^n+x_2g_{n-1}+…+x_ng \bmod 2^{256}$

假设两个消息的 hash 值相同那么

$h_0g^n+x_1g^n+x_2g_{n-1}+…+x_ng \equiv h_0g^n+y_1g^n+y_2g_{n-1}+…+y_ng\bmod 2^{256}$

进而

$(x_1-y_1)g^{n-1}+(x_2-y_2)g^{n-2}+…+(x_n-y_n)g^0 \equiv 0 \bmod 2^{256}$

即我们只需要找到一个 n 维向量 $z_i=x_i-y_i$,满足上述等式即可,我们可以进一步将其化为

$z_1g^{n-1}+z_2g^{n-2}+…+z_ng^0-k*2^{256}=0$

即找到一组向量满足上述这个式子。这可以认为是 LLL Paper 中第二个例子的简单情况(参见格问题部分)。

那么我们可以快速构造矩阵,如下

$$ A = \left[ \begin{matrix} 1 & 0 & 0 & \cdots & 0 & Kg^{n-1} \ 0 & 1 & 0 & \cdots & 0 & Kg^{n-2} \ 0 & 0 & 1 & \cdots & 0 & Kg^{n-3} \\vdots & \vdots & \vdots & \ddots & \vdots \ 0 & 0 &0 & \cdots & 1 & K*mod \ \end{matrix} \right]$$

之后我们使用LLL 算法即可获得两个一样的哈希值

from sage.all import *

mod = 2**256
h0 = 45740974929179720441799381904411404011270459520712533273451053262137196814399

g = 2**168 + 355


def shitty_hash(msg):
    h = h0
    msg = map(ord, msg)
    for i in msg:
        h = (h + i) * g
        # This line is just to screw you up :))
        h = h & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

    return h - 0xe6168647f636


K = 2**200
N = 50
base_str = 'a' * N
base = map(ord, base_str)
m = Matrix(ZZ, N + 1, N + 2)
for i in xrange(N + 1):
    ge = ZZ(pow(g, N - i, mod))
    m[i, i] = 1
    m[i, N + 1] = ZZ(ge * K)
m[i, N + 1] = ZZ(K * mod)

ml = m.LLL()
ttt = ml.rows()[0]
print "result:", ttt
if ttt[-1] != 0:
    print "Zero not reached, increase K"
    exit()
else:
    msg = []
    for i in xrange(N):
        msg.append(base[i] + ttt[i])
        if not (0 <= msg[i] <= 255):
            print "Need more bytes!"
            quit()
    print msg
    other = ''.join(map(chr, msg))

    print shitty_hash(base_str)
    print shitty_hash(other)

注意不能直接仅仅使用 pow(g, N - i, mod),不然生成的数会在 mod 对应的域中,这真是个大坑。

如下

➜  hashcoll sage exp.sage
result: (15, -14, 17, 14, 6, 0, 12, 21, 8, 29, 6, -4, -9, 10, -2, -12, -6, 0, -12, 13, -28, -28, -24, -3, 6, -5, -16, 15, 17, -14, 3, -2, -16, -25, 3, -21, -27, -9, 16, 5, -1, 0, -3, -4, -4, -19, 6, 8, 0, 0, 0, 0)
[112, 83, 114, 111, 103, 97, 109, 118, 105, 126, 103, 93, 88, 107, 95, 85, 91, 97, 85, 110, 69, 69, 73, 94, 103, 92, 81, 112, 114, 83, 100, 95, 81, 72, 100, 76, 70, 88, 113, 102, 96, 97, 94, 93, 93, 78, 103, 105, 97, 97]
106025341237231370726407656306665079105509255639964756437758376184556498283725
106025341237231370726407656306665079105509255639964756437758376184556498283725

即成功。

Hash Attack

常见的Hash函数的攻击方法主要有

  • 暴力攻击:不依赖于任何算法细节,仅与Hash值长度有关;
    • 生日攻击法(Birthday Attack):没有利用Hash函数的结构和任何代数弱性质,只依赖于消息摘要的长度,即Hash值的长度。
    • 中点交会攻击法(Meet-In-The-Middle):是生日攻击的一种变形,不比较Hash值,而是比较中间变量。这种攻击主要适用于攻击具有分组链结构的Hash方案。
  • 密码分析:依赖于具体算法的设计缺点。

暴力攻击

HashCat 工具 可以说是目前最好的基于 CPU 和 GPU 破解 Hash 的软件,相关链接如下

HashCat 官网

HashCat 简单使用

哈希长度拓展攻击(hash length extension attacks)

介绍

基本定义如下,源自维基百科

哈希长度扩展攻击(Hash Length Extension Attacks)是指针对某些允许包含额外信息的加密散列函数的攻击手段。该攻击适用于在消息与密钥的长度已知的情形下,所有采取了 H(key ∥ message) 此类构造的散列函数。MD5和SHA-1 等基于 Merkle–Damgård 构造的算法均对此类攻击显示出脆弱性。

这类哈希函数有以下特点

  • 消息填充方式都比较类似,首先在消息后面添加一个1,然后填充若干个0,直至总长度与 448 同余,最后在其后附上64位的消息长度(填充前)。
  • 每一块得到的链接变量都会被作为下一次执行hash函数的初始向量IV。在最后一块的时候,才会将其对应的链接变量转换为hash值。

一般攻击时应满足如下条件

  • 我们已知 key 的长度,如果不知道的话,需要爆破出来
  • 我们可以控制 message 的消息。
  • 我们已经知道了包含 key 的一个消息的hash值。

这样我们就可以得到一对(messge,x)满足x=H(key ∥ message)虽然我们并不清楚key的内容。

攻击原理

这里不妨假设我们我们知道了 hash(key+s) 的 hash 值,其中 s 是已知的,那么其本身在计算的时候,必然会进行填充。那么我们首先可以得到 key+s 扩展后的字符串 now,即

now=key|s|padding

那么如果我们在 now 的后面再次附加上一部分信息extra,即

key|s|padding|extra

这样再去计算hash值的时候,

  1. 会对 extra 进行填充直到满足条件。
  2. 先计算 now 对应的链接变量 IV1,而我们已经知道这部分的 hash 值,并且链接变量产生 hash 值的算法是可逆的,所以我们可以得到链接变量。
  3. 下面会根据得到的链接变量 IV1,对 extra 部分进行哈希算法,并返回hash值。

那么既然我们已经知道了第一部分的 hash 值,并且,我们还知道 extra 的值,那么我们便可以得到最后的hash值。

而之前我们也说了我们可以控制 message 的值。那么其实 s,padding,extra 我们都是可以控制的。所以我们自然可以找到对应的(message,x)满足x=hash(key|message)。

例子

似乎大都是web里面的,,不太懂web,暂时先不给例子了。

工具

如何使用请参考github上的readme。

hash算法设计有误

一些自定义的hash算法可能是可逆的。

Hashinator

题目的逻辑很简单,从一个知名的密码字典”rockyou”挑选出一个password,并且使用多种hash算法随机的哈希32轮。我们需要从最后的hash结果中破解出原始的password

分析

题目采用的hash算法有:md5sha1blakescrypt
关键的代码如下:

    password = self.generate_password()     # from rock_you.txt
    salt = self.generate_salt(password)     # 与password的长度有关
    hash_rounds = self.generate_rounds()    # 生成进行hash算法的顺序
    password_hash = self.calculate_hash(salt + password, hash_rounds)
  1. 程序首先通过从rockyou.txt中随机抽取一个password,作为加密的明文。
  2. 然后根据抽取的password的长度,生成一个长度为128 - len(password)salt
  3. 从之前列举的4种hash算法中抽取,组成32轮的哈希运算。
  4. 根据之前得到的passwordsalt计算出最后给我们的password_hash

很明显,我们不可能通过逆向hash算法来完成题目。
我们知道所有的可能的明文,首先考虑能否通过构造彩虹表来完成穷举。但是注意到generate_salt()函数中,saltpassword的长度组合超过了128byte的长度,并且被注释了

    msize = 128 # f-you hashcat :D

so,只能无奈放弃。

那这样的话,只存在一种可能,也即算法可逆。查看calculate_hash()函数的具体实现,可以发现如下可疑的代码:

for i in range(len(hash_rounds)):
    interim_salt = xor(interim_salt, hash_rounds[-1-i](interim_hash))
    interim_hash = xor(interim_hash, hash_rounds[i](interim_salt))
final_hash = interim_salt + interim_hash

重新梳理一下我们知道的信息:

  1. hash_rounds中保存了32轮,即每轮要使用的hash函数句柄。
  2. final_hash是最后给我们的hash结果。
  3. hash_rounds中的内容也会在生成之后打印给我们。
  4. 我们希望得到interim_saltinterim_hash在第一轮的值。
  5. interim_saltinterim_hash的长度均为64byte。

仔细观察一下interim_saltinterim_hash的计算方法,可以发现它是可逆的。

$$
interim_hash_1 = interim_hash_2 \oplus hash_roundsi
$$

这行代码里,我们已知 $interim_hash_1$ 和 $interim_salt_3$,由此可以推出$interim_hash_2$的值,而$interim_hash_2$则是上一轮的interim_hash
以此方法逆推32次,则可以得到最初的passwordsalt

具体的解密脚本为:

import os
import hashlib
import socket
import threading
import socketserver
import struct
import time
import threading
# import pyscrypt
from base64 import b64encode, b64decode
from pwn import *
def md5(bytestring):
    return hashlib.md5(bytestring).digest()
def sha(bytestring):
    return hashlib.sha1(bytestring).digest()
def blake(bytestring):
    return hashlib.blake2b(bytestring).digest()
def scrypt(bytestring):
    l = int(len(bytestring) / 2)
    salt = bytestring[:l]
    p = bytestring[l:]
    return hashlib.scrypt(p, salt=salt, n=2**16, r=8, p=1, maxmem=67111936)
    # return pyscrypt.hash(p, salt, 2**16, 8, 1, dkLen=64)
def xor(s1, s2):
    return b''.join([bytes([s1[i] ^ s2[i % len(s2)]]) for i in range(len(s1))])
def main():
    # io = socket.socket(family=socket.AF_INET)
    # io.connect(('47.88.216.38', 20013))
    io = remote('47.88.216.38', 20013)
    print(io.recv(1000))
    ans_array = bytearray()
    while True:
        buf = io.recv(1)
        if buf:
            ans_array.extend(buf)
        if buf == b'!':
            break

    password_hash_base64 = ans_array[ans_array.find(b"b'") + 2: ans_array.find(b"'\n")]
    password_hash = b64decode(password_hash_base64)
    print('password:', password_hash)
    method_bytes = ans_array[
        ans_array.find(b'used:\n') + 6 : ans_array.find(b'\nYour')
    ]
    methods = method_bytes.split(b'\n')
    methods = [bytes(x.strip(b'- ')).decode() for x in methods]
    print(methods)
    in_salt = password_hash[:64]
    in_hash = password_hash[64:]
    for pos, neg in zip(methods, methods[::-1]):
        '''
            interim_salt = xor(interim_salt, hash_rounds[-1-i](interim_hash))
            interim_hash = xor(interim_hash, hash_rounds[i](interim_salt))
        '''
        in_hash = xor(in_hash, eval("{}(in_salt)".format(neg)))
        in_salt = xor(in_salt, eval("{}(in_hash)".format(pos)))
    print(in_hash, in_salt)
    print(in_hash[-20:])
    io.interactive()
main()

原hash算法


import os
import hashlib
import socket
import threading
import socketserver
import struct
import time

# import pyscrypt

from base64 import b64encode

def md5(bytestring):
    return hashlib.md5(bytestring).digest()

def sha(bytestring):
    return hashlib.sha1(bytestring).digest()

def blake(bytestring):
    return hashlib.blake2b(bytestring).digest()

def scrypt(bytestring):
    l = int(len(bytestring) / 2)
    salt = bytestring[:l]
    p = bytestring[l:]
    return hashlib.scrypt(p, salt=salt, n=2**16, r=8, p=1, maxmem=67111936)
    # return pyscrypt.hash(p, salt, 2**16, 8, 1)

def xor(s1, s2):
    return b''.join([bytes([s1[i] ^ s2[i % len(s2)]]) for i in range(len(s1))])

class HashHandler(socketserver.BaseRequestHandler):

    welcome_message = """
Welcome, young wanna-be Cracker, to the Hashinator.

To prove your worthiness, you must display the power of your cracking skills.

The test is easy:
1. We send you a password from the rockyou list, hashed using multiple randomly chosen algorithms.
2. You crack the hash and send back the original password.

As you already know the dictionary and won't need any fancy password rules, {} seconds should be plenty, right?

Please wait while we generate your hash...
    """

    hashes = [md5, sha, blake, scrypt]
    timeout = 10
    total_rounds = 32

    def handle(self):
        self.request.sendall(self.welcome_message.format(self.timeout).encode())

        password = self.generate_password()     # from rock_you.txt
        salt = self.generate_salt(password)     # 与password的长度有关
        hash_rounds = self.generate_rounds()    # 生成进行hash算法的顺序
        password_hash = self.calculate_hash(salt + password, hash_rounds)
        self.generate_delay()

        self.request.sendall("Challenge password hash: {}\n".format(b64encode(password_hash)).encode())
        self.request.sendall("Rounds used:\n".encode())
        test_rounds = []
        for r in hash_rounds:
            test_rounds.append(r)

        for r in hash_rounds:
            self.request.sendall("- {}\n".format(r.__name__).encode())
        self.request.sendall("Your time starts now!\n".encode())
        self.request.settimeout(self.timeout)
        try:
            response = self.request.recv(1024)
            if response.strip() == password:
                self.request.sendall("Congratulations! You are a true cracking master!\n".encode())
                self.request.sendall("Welcome to the club: {}\n".format(flag).encode())
                return
        except socket.timeout:
            pass
        self.request.sendall("Your cracking skills are bad, and you should feel bad!".encode())


    def generate_password(self):
        rand = struct.unpack("I", os.urandom(4))[0]
        lines = 14344391 # size of rockyou
        line = rand % lines
        password = ""
        f = open('rockyou.txt', 'rb')
        for i in range(line):
            password = f.readline()
        return password.strip()

    def generate_salt(self, p):
        msize = 128 # f-you hashcat :D
        salt_size = msize - len(p)
        return os.urandom(salt_size)

    def generate_rounds(self):
        rand = struct.unpack("Q", os.urandom(8))[0]
        rounds = []
        for i in range(self.total_rounds):
            rounds.append(self.hashes[rand % len(self.hashes)])
            rand = rand >> 2
        return rounds

    def calculate_hash(self, payload, hash_rounds):
        interim_salt = payload[:64]
        interim_hash = payload[64:]
        for i in range(len(hash_rounds)):
            interim_salt = xor(interim_salt, hash_rounds[-1-i](interim_hash))
            interim_hash = xor(interim_hash, hash_rounds[i](interim_salt))
            '''
            interim_hash = xor(
                interim_hash,
                hash_rounds[i](
                    xor(interim_salt, hash_rounds[-1-i](interim_hash))
                )
            )
            '''
        final_hash = interim_salt + interim_hash
        return final_hash

    def generate_delay(self):
        rand = struct.unpack("I", os.urandom(4))[0]
        time.sleep(rand / 1000000000.0)



class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    allow_reuse_address = True

PORT = 1337
HOST = '0.0.0.0'
flag = ""

with open("flag.txt") as f:
    flag = f.read()

def main():
    server = ThreadedTCPServer((HOST, PORT), HashHandler)
    server_thread = threading.Thread(target=server.serve_forever)
    server_thread.start()
    server_thread.join()

if __name__ == "__main__":
    main()

综合题目

2017 34c3 Software_update

可以看出,程序的大概意思是上传一个 zip 压缩包,然后对 signed_data 目录下的文件进行签名验证。其中,最后验证的手法是大概是将每一个文件进行 sha256 哈希,然后异或起来作为输入传递给 rsa 进行签名。如果通过验证的话,就会执行对应的 pre-copy.py 和 post-copy.py 文件。

很自然的想法是我们修改 pre-copy.py 或者 post-copy.py 文件,使其可以读取 flag,然后再次绕过签名即可。主要有两种思路

  1. 根据给定的公钥文件获取对应的私钥,进而再修改文件后伪造签名,然后大概看了看公钥文件几乎不可破,所以这一点,基本上可以放弃。
  2. 修改对应文件后,利用异或的特性使得其哈希值仍然与原来相同,从而绕过签名检测。即使得 signed_data 目录下包含多个文件,使得这些文件的哈希值最后异或起来可以抵消修改 pre-copy.py 或者 post-copy.py文件所造成的哈希值的不同。

这里,我们选择第二种方法,这里我们选择修改 pre-copy.py 文件,具体思路如下

  1. 计算 pre-copy.py 的原 hash 值。
  2. 修改 pre-copy.py 文件,使其可以读取 flag。与此同时,计算新的 hash 值。将两者异或,求得异或差值 delta。
  3. 寻找一系列的文件,使其 hash 值异或起来正好为 delta。

关键的步骤在于第三步,而其实这个文件可以看做是一个线性组合的问题,即寻找若干个 256 维01向量使其异或值为 delta。而
$$
(F={0,1},F^{256},\oplus ,\cdot)
$$
是一个 256 维的向量空间。如果我们可以求得该向量空间的一个基,那么我们就可以求得该空间中任意指定值的所需要的向量。

我们可以使用 sage 来辅助我们求,如下

# generage the base of <{0,1},F^256,xor,*>
def gen_gf2_256_base():
    v = VectorSpace(GF(2), 256)
    tmphash = compute_file_hash("0.py", "")
    tmphash_bin = hash2bin(tmphash)
    base = [tmphash_bin]
    filelist = ['0.py']
    print base
    s = v.subspace(base)
    dim = s.dimension()
    cnt = 1
    while dim != 256:
        tmpfile = str(cnt) + ".py"
        tmphash = compute_file_hash(tmpfile, "")
        tmphash_bin = hash2bin(tmphash)
        old_dim = dim
        s = v.subspace(base + [tmphash_bin])
        dim = s.dimension()
        if dim > old_dim:
            base += [tmphash_bin]
            filelist.append(tmpfile)
            print("dimension " + str(s.dimension()))
        cnt += 1
        print(cnt)
    m = matrix(GF(2), 256, 256, base)
    m = m.transpose()
    return m, filelist

关于更加详细的解答,请参考 exp.py

这里我修改 pre-copy 多输出 !!!!come here!!!! 字眼,如下

➜  software_update git:(master) python3 installer.py now.zip
Preparing to copy data...
!!!!come here!!!!
Software update installed successfully.

参考文献

2019 36c3 SaV-ls-l-aaS

这个题的分类是 Crypto&Web,捋一下流程:

60601端口开着一个Web服务,题目描述给了连接方法:

url='http://78.47.240.226:60601' && ip=$(curl -s "$url/ip") && sig=$(curl -s -d "cmd=ls -l&ip=$ip" "$url/sign") && curl --data-urlencode "signature=$sig" "$url/exec"

可以看到,先是访问 /ip 得到 ip,再向 /sign post 过去 ip 和我们要执行的命令,得到签名,最后向 /exec post signature 来执行命令。我们执行这一行可以发现回显了ls -l执行的结果,发现有个 flag.txt。

看源码,Web 服务是由 go 起的:

package main

import (
    "bytes"
    "crypto/sha1"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net"
    "net/http"
    "strings"
    "time"
)

func main() {
    m := http.NewServeMux()

    m.HandleFunc("/ip", func(w http.ResponseWriter, r *http.Request) {
        ip, _, err := net.SplitHostPort(r.RemoteAddr)
        if err != nil {
            return
        }
        fmt.Fprint(w, ip)
    })

    m.HandleFunc("/sign", func(w http.ResponseWriter, r *http.Request) {
        ip, _, err := net.SplitHostPort(r.RemoteAddr)
        if err != nil {
            return
        }
        remoteAddr := net.ParseIP(ip)
        if remoteAddr == nil {
            return
        }

        ip = r.PostFormValue("ip")
        signIP := net.ParseIP(ip)
        if signIP == nil || !signIP.Equal(remoteAddr) {
            fmt.Fprintln(w, "lol, not ip :>")
            return
        }

        cmd := r.PostFormValue("cmd")
        if cmd != "ls -l" {
            fmt.Fprintln(w, "lol, nope :>")
            return
        }

        msg := ip + "|" + cmd
        digest := sha1.Sum([]byte(msg))

        b := new(bytes.Buffer)
        err = json.NewEncoder(b).Encode(string(digest[:]))
        if err != nil {
            return
        }

        resp, err := http.Post("http://127.0.0.1/index.php?action=sign", "application/json; charset=utf-8", b)
        if err != nil || resp.StatusCode != 200 {
            fmt.Fprintln(w, "oops, hsm is down")
            return
        }

        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            fmt.Fprintln(w, "oops, hsm is bodyless?")
            return
        }

        var signature string
        err = json.Unmarshal(body, &signature)
        if err != nil {
            fmt.Fprintln(w, "oops, hsm is jsonless?")
            return
        }

        fmt.Fprint(w, signature+msg)
    })

    m.HandleFunc("/exec", func(w http.ResponseWriter, r *http.Request) {
        ip, _, err := net.SplitHostPort(r.RemoteAddr)
        if err != nil {
            return
        }
        remoteAddr := net.ParseIP(ip)
        if remoteAddr == nil {
            return
        }

        signature := r.PostFormValue("signature")
        digest := sha1.Sum([]byte(signature[172:]))

        b := new(bytes.Buffer)
        err = json.NewEncoder(b).Encode(signature[:172] + string(digest[:]))
        if err != nil {
            fmt.Fprintln(w, "oops, json encode")
            return
        }

        resp, err := http.Post("http://127.0.0.1/index.php?action=verify", "application/json; charset=utf-8", b)
        if err != nil || resp.StatusCode != 200 {
            fmt.Fprintln(w, "oops, hsm is down?")
            return
        }

        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            fmt.Fprintln(w, "oops, hsm is bodyless?")
            return
        }

        var valid bool
        err = json.Unmarshal(body, &valid)
        if err != nil {
            fmt.Fprintln(w, "oops, json unmarshal")
            return
        }

        if valid {
            t := strings.Split(signature[172:], "|")
            if len(t) != 2 {
                fmt.Fprintln(w, "oops, split")
            }

            signIP := net.ParseIP(t[0])
            if signIP == nil || !signIP.Equal(remoteAddr) {
                fmt.Fprintln(w, "lol, not ip :>")
                return
            }

            conn, err := net.DialTimeout("tcp", "127.0.0.1:1024", 1*time.Second)
            if err != nil {
                fmt.Fprintln(w, "oops, dial")
                return
            }
            fmt.Fprintf(conn, t[1]+"\n")
            conn.(*net.TCPConn).CloseWrite()
            io.Copy(w, conn)
        }
    })

    s := &http.Server{
        Addr:           ":60601",
        Handler:        m,
        ReadTimeout:    5 * time.Second,
        WriteTimeout:   5 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    log.Fatal(s.ListenAndServe())
}

代码很容易看,限制了 cmd 只能是ls -l,其余不给签名,看样子我们是要伪造其他命令的签名来读flag,这里注意到签名和验签的过程是传给本地起的一个 php 来完成的,看一下这部分源码:

<?php
define('ALGO', 'md5WithRSAEncryption');
$d = json_decode(file_get_contents('php://input'), JSON_THROW_ON_ERROR);

if ($_GET['action'] === 'sign'){
    $pkeyid = openssl_pkey_get_private("file:///var/www/private_key.pem");
    openssl_sign($d, $signature, $pkeyid, ALGO);
    echo json_encode(base64_encode($signature));
    openssl_free_key($pkeyid);
}
elseif ($_GET['action'] === 'verify') {
    $pkeyid = openssl_pkey_get_public("file:///var/www/public_key.pem");
    echo json_encode(openssl_verify(substr($d, 172), base64_decode(substr($d,0, 172)), $pkeyid, ALGO) === 1);
    openssl_free_key($pkeyid);
}

采用的是md5WithRSAEncryption的方式签名,本地试了一下,是把我们传入的 $d md5 后转为hex,填充到

0x1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003020300c06082a864886f70d020505000410

后面,组成数字然后用RSA签名。

看样子整个逻辑找不到一点问题,用的都是标准库,基本无法攻击。有个思路是通过代理更换 ip,可以拿到两个 ip|ls -l 的签名,这样我们就拥有了两组 RSA 的 m 和 c,因为题目给了 dockerfile 给了生成公私钥的方法,使用 openssl 默认生成,e为65537,那么我们可以通过求公因数的方式来求出 n。

在得到两组签名后,我们要得到 RSA 的m,就是填充后的数,所以按照代码逻辑,在 go 里面先是 sha1:

msg := ip + "|" + cmd
digest := sha1.Sum([]byte(msg))

b := new(bytes.Buffer)
err = json.NewEncoder(b).Encode(string(digest[:]))

再 php 里的 md5,得到两组 m 和 c,但是总是求不出公因数 n,怀疑求的 m 不对。看代码发现 go 里把 sha1的结果用 json 编码,然后传到 php里 json 解码。这部分非常可疑,为何要用 json 编码(用 hex 传过去它不香么),本地搭一下环境跟一下。(题目给了dockerfile)

起个docker,改一下 index.php,加一个var_dump($d);,再改一下 go,返回一下 php 的结果:

fmt.Fprintln(w,string(body))

现在让程序签名,返回结果:

string(38) "    ��.���?-�KC��@�"
"K4FEmxz4yuTsjDAbRZQmHJ+MBiCSGaOnpZTLbThXpCkDYe3siAIPfihX6ppjN2Tz6XqOr4tF\/u1\/+ccfhj8NNLIL+2hknyDXbosmMBV8mEGYsMqQHAE0f+3OhDWlzN5RnteSMYNZbTipFErB8ZOWCiXmynWxsqJhyaN9J6\/\/h6I="
oops, hsm is jsonless?

$d 竟然是长度为 38 的字符串,看来果然是这里编码有问题,我们需要看一下每个步骤的结果,先看一下 go 里 json编码后的 sha1 结果是什么:

package main

import (
    "bytes"
    "crypto/sha1"
    "encoding/json"
    "fmt"
)
func main() {
    msg := "172.17.0.1|ls -l"
    digest := sha1.Sum([]byte(msg))

    b := new(bytes.Buffer)
    json.NewEncoder(b).Encode(string(digest[:]))
    fmt.Print(string(b.Bytes()));
}

运行一下:

"\u000e\t\u001d\ufffd\u0012\ufffd.\ufffd\ufffd\ufffd?-\ufffdKC\ufffd\u0005\ufffd@\ufffd"

和正常的sha1的结果来比较一下:

Python 2.7.16 (default, Sep  2 2019, 11:59:44)
[GCC 4.2.1 Compatible Apple LLVM 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> "\u000e\t\u001d\ufffd\u0012\ufffd.\ufffd\ufffd\ufffd?-\ufffdKC\ufffd\u0005\ufffd@\ufffd"
'\\u000e\t\\u001d\\ufffd\\u0012\\ufffd.\\ufffd\\ufffd\\ufffd?-\\ufffdKC\\ufffd\\u0005\\ufffd@\\ufffd'
>>> from hashlib import *
>>> sha1('172.17.0.1|ls -l').digest()
'\x0e\t\x1d\xbd\x12\x90.\xca\xf0\xd9?-\x98KC\xeb\x05\xa1@\xd1'

由于 go 的 json 编码,很多不可见字符都被转为了 U+fffd,丢失了很多信息。

再经过 php 接口的接收,我们来看一下结果:

$d = json_decode(file_get_contents('php://input'), JSON_THROW_ON_ERROR);
var_dump(file_get_contents('php://input'));
var_dump($d);
var_dump(bin2hex($d));

结果:

string(89) ""\u000e\t\u001d\ufffd\u0012\ufffd.\ufffd\ufffd\ufffd?-\ufffdKC\ufffd\u0005\ufffd@\ufffd"
"
string(38) "    ��.���?-�KC��@�"
string(76) "0e091defbfbd12efbfbd2eefbfbdefbfbdefbfbd3f2defbfbd4b43efbfbd05efbfbd40efbfbd"
"K4FEmxz4yuTsjDAbRZQmHJ+MBiCSGaOnpZTLbThXpCkDYe3siAIPfihX6ppjN2Tz6XqOr4tF\/u1\/+ccfhj8NNLIL+2hknyDXbosmMBV8mEGYsMqQHAE0f+3OhDWlzN5RnteSMYNZbTipFErB8ZOWCiXmynWxsqJhyaN9J6\/\/h6I="
oops, hsm is jsonless?

U+fffd变成了\xef\xbf\xbd。所以由于 go 的 json 编码问题,丢失了很多信息,造成了 md5 前的数据有很多相同字符。当时做题时往下并没有细想,得到 n 后总是想构造出任意命令的签名,也很疑惑如果构造出岂不是这种签名就不安全了?其实是无法得到的。

正解是 go 的这种问题 ,为碰撞创造了条件。我们可以碰撞出在这种编码情况下与 ls -l有相同结果的cat * 此类命令。但是问题是我们需要非常大量 ip 来提供碰撞的数据。

可以发现,go 取 ip 的时候,是先用net.ParseIP解析了 ip,我们在 ip 每个数字前面加 0 ,解析后还是原来的 ip 结果,每个数字最多添加 256 个 0,四个数字就已经产生了 2^32种不同的组合,足以碰撞出 ls -lcat *之间的冲突。

官方题解的 c++ 碰撞脚本我本地编译的有点问题,加了一些引入的头文件:

// g++ -std=c++17 -march=native -O3 -lcrypto -lpthread gewalt.cpp -o gewalt

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

const unsigned num_threads = std::thread::hardware_concurrency();



static std::string hash(std::string const& s)
{
    SHA_CTX ctx;
    if (!SHA1_Init(&ctx)) throw;
    if (!SHA1_Update(&ctx, s.data(), s.length())) throw;
    std::string d(SHA_DIGEST_LENGTH, 0);
    if (!SHA1_Final((uint8_t *) &d[0], &ctx)) throw;
    return d;
}

static std::u32string kapot(std::string const& s)
{
    std::u32string r(s.size(), 0);
    size_t o = 0;

    for (size_t i = 0; i < s.length(); ) {

        auto T = [](uint8_t c) {
            return (c < 0x80)         ? 1   /* ASCII */
                 : (c & 0xc0) == 0x80 ? 0   /* continuation */
                 : (c & 0xe0) == 0xc0 ? 2   /* 2-byte chunk */
                 : (c & 0xf0) == 0xe0 ? 3   /* 3-byte chunk */
                 : (c & 0xf8) == 0xf0 ? 4   /* 4-byte chunk */
                 : -1;
        };

        uint32_t c = s[i++];
        auto cont = [&]() { c = (c << 6) | (s[i++] & 0x3f); };

        switch (T(c)) {

        case -1:
        case  0:
        invalid: c = 0xfffd; /* fall through */

        case  1:
        valid:   r[o++] = c; break;

        case  2:
                 if (c &= 0x1f, i+0 >= s.size() || T(s[i+0]))
                     goto invalid;
                 goto one;

        case  3:
                 if (c &= 0x1f, i+1 >= s.size() || T(s[i+0]) || T(s[i+1]))
                     goto invalid;
                 goto two;

        case  4:
                 if (c &= 0x1f, i+2 >= s.size() || T(s[i+0]) || T(s[i+1]) || T(s[i+2]))
                     goto invalid;
                 cont();
        two:     cont();
        one:     cont();
                 goto valid;

        }

    }

    r.resize(o);

    return r;
}

std::atomic hcount = 0, kcount = 0;
typedef std::unordered_map tab_t;
tab_t tab0, tab1;
std::mutex mtx;

std::array ip;
std::string cmd0, cmd1;

class stuffer_t
{
    private:
        std::array cnts;
        size_t step;
        std::string cmd;
    public:
        stuffer_t(size_t t, size_t s, std::string c) : cnts{t}, step(s), cmd(c) {}
        std::string operator()()
        {
            //XXX this is by far not the most efficient way of doing this, but yeah
            if (++cnts[3] >= cnts[0]) {
                cnts[3] = 0;
                if (++cnts[2] >= cnts[0]) {
                    cnts[2] = 0;
                    if (++cnts[1] >= cnts[0]) {
                        cnts[1] = 0;
                        cnts[0] += step;
                    }
                }
            }
            std::stringstream o;
            for (size_t i = 0; i < 4; ++i)
                o << (i ? "." : "")
                  << std::string(cnts[i], '0')
                  << (unsigned) ip[i];
            o << "|" << cmd;
            return o.str();
        }
};

void go(size_t tid)
{
    //XXX tid stuff is a hack, but YOLO

    bool one = tid & 1;

    stuffer_t next(tid >> 1, (num_threads + 1) >> 1, one ? cmd1 : cmd0);

    tab_t& mytab = one ? tab1 : tab0;
    tab_t& thtab = one ? tab0 : tab1;

    uint64_t myhcount = 0, mykcount = 0;

    while (1) {

        std::string r = next();

        {

            ++myhcount;

            auto h = hash(r);
            if ((h.size()+3)/4 < (size_t) std::count_if(h.begin(), h.end(),
                                            [](unsigned char c) { return c < 0x80; }))
                continue;

            ++mykcount;

            auto k = kapot(h);
            if (k.size() > 3 + (size_t) std::count(k.begin(), k.end(), 0xfffd))
                continue;

            std::lock_guard lck(mtx);

            hcount += myhcount, myhcount = 0;
            kcount += mykcount, mykcount = 0;

            if (thtab.find(k) != thtab.end()) {

                mytab[k] = r;

                std::cerr << "\r\x1b[K"
                          << "\x1b[32m";
                std::cout << tab0[k] << std::endl
                          << tab1[k] << std::endl;
                std::cerr << "\x1b[0m";

                std::cerr << std::hex;
                bool first = true;
                for (uint32_t c: k)
                    std::cerr << (first ? first = false, "" : " ") << c;
                std::cerr << std::endl;

                std::cerr << std::dec << "hash count:  \x1b[35m" << hcount << "\x1b[0m";
                {
                    std::stringstream s;
                    s << std::fixed << std::setprecision(2) << log(hcount|1)/log(2);
                    std::cerr << " (2^\x1b[35m" << std::setw(5) << s.str() << "\x1b[0m" << ")" << std::endl;
                }
                std::cerr << "kapot count: " << "\x1b[35m" << kcount << "\x1b[0m";
                {
                    std::stringstream s;
                    s << std::fixed << std::setprecision(2) << log(kcount|1)/log(2);
                    std::cerr << " (2^\x1b[35m" << std::setw(5) << s.str() << "\x1b[0m)" << std::endl;
                }
                std::cerr << "table sizes: \x1b[35m"
                          << tab0.size() << "\x1b[0m \x1b[35m"
                          << tab1.size() << "\x1b[0m" << std::endl;

                exit(0);

            }

            if (mytab.size() < (1 << 20))
                mytab[k] = r;

        }

        hcount += myhcount;
        kcount += mykcount;

    }
}

void status()
{
    while (1) {

        {
            std::lock_guard lck(mtx);

            std::cerr << "\r\x1b[K";
            std::cerr << "hash count: \x1b[35m" << std::setw(12) << hcount << "\x1b[0m ";
            {
                std::stringstream s;
                s << std::fixed << std::setprecision(2) << log(hcount|1)/log(2);
                std::cerr << "(2^\x1b[35m" << std::setw(5) << s.str() << "\x1b[0m) | ";
            }
            std::cerr << "kapot count: \x1b[35m" << std::setw(12) << kcount << "\x1b[0m ";
            {
                std::stringstream s;
                s << std::fixed << std::setprecision(2) << log(kcount|1)/log(2);
                std::cerr << "(2^\x1b[35m" << std::setw(5) << s.str() << "\x1b[0m) | ";
            }
            std::cerr << "tables: \x1b[35m"
                      << std::setw(9) << tab0.size() << " "
                      << std::setw(9) << tab1.size() << "\x1b[0m "
                      << std::flush;
        }

        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main(int argc, char **argv)
{

    if (argc < 2) {
        std::cerr << "\x1b[31mneed IPv4 in argv[1]\x1b[0m" << std::endl;
        exit(1);
    }
    {
        std::stringstream ss(argv[1]);
        for (auto& v: ip) {
            std::string s;
            std::getline(ss, s, '.');
            int n = std::atoi(s.c_str());
            if (n < std::numeric_limits::min() || n > std::numeric_limits::max())
                goto bad_ip;
            v = n;
        }
        if (!ss) {
bad_ip:
            std::cerr << "\x1b[31mbad IPv4 given?\x1b[0m" << std::endl;
            exit(2);
        }
    }


    if (argc < 4) {
        std::cerr << "\x1b[31mneed commands in argv[2] and argv[3]\x1b[0m" << std::endl;
        exit(2);
    }
    cmd0 = argv[2];
    cmd1 = argv[3];


    std::thread status_thread(status);
    std::vector ts;
    for (unsigned i = 0; i < num_threads; ++i)
        ts.push_back(std::thread(go, i));
    for (auto& t: ts)
        t.join();

}

编译可能会找不到 lcrypto,编译命令加上 lcrypto 路径(我本地是 /usr/local/opt/openssl/lib)

g++ -std=c++17 -march=native -O3 -lcrypto -lpthread gewalt.cpp -o gewalt -L/usr/local/opt/openssl/lib

与 go 交互的脚本:

#!/usr/bin/env python3
import sys, requests, subprocess

benign_cmd = 'ls -l'
exploit_cmd = 'cat *'

ip, port = sys.argv[1], sys.argv[2]
url = 'http://{}:{}'.format(ip, port)

my_ip = requests.get(url + '/ip').text
print('[+] IP: ' + my_ip)

o = subprocess.check_output(['./gewalt', my_ip, benign_cmd, exploit_cmd])
print('[+] gewalt:' + o.decode())

payload = {}
for l in o.decode().splitlines():
    ip, cmd = l.split('|')
    payload['benign' if cmd == benign_cmd else 'pwn'] = ip, cmd

print(payload)

sig  = requests.post(url + '/sign', data={'ip': payload['benign'][0], 'cmd': payload['benign'][1]}).text
print('[+] sig: ' + sig)

r = requests.post(url + '/exec', data={'signature': sig[:172] + payload['pwn'][0]  + '|' + payload['pwn'][1]})
print(r.text)
 ⚙  SaV-ls-l-aaS  python solve.py 127.0.0.1 60601
[+] IP: 172.17.0.1
fffd fffd fffd fffd fffd fffd 55 fffd fffd fffd fffd c fffd fffd fffd fffd fffd fffd fffd fffd
hash count:  168104875 (2^27.32)
kapot count: 3477222 (2^21.73)
table sizes: 8745 8856
[+] gewalt:00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000172.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017.000000000000000000000000000000000000000000000000000000000000000000000000000000000.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001|ls -l
00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000172.17.000000000000000000000000.0000000000000000000000000000000000000001|cat *

{'pwn': (u'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000172.17.000000000000000000000000.0000000000000000000000000000000000000001', u'cat *'), 'benign': (u'00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000172.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017.000000000000000000000000000000000000000000000000000000000000000000000000000000000.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001', u'ls -l')}
[+] sig: ODxSukwtu4rHICBpzT23WGD7DCJNawhA0DUN/tcyv1AgwNmS8OPUnO5FnBBDgiaVx5OTYd4OjH8LVbKiXUBUBuFx1OHDgKBKG5umkKMLt+350SlgMWY5qWny9tPIU3I+X0A9FcADCBCi6f0PkXfc0CSCZXuFu9rAKnVGsbmaUwY=00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000172.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000017.000000000000000000000000000000000000000000000000000000000000000000000000000000000.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001|ls -l
hxp{FLAG}

数字签名

在日常生活中,我们在参加某个活动的时候,可能会需要签名,以便于证明我们确实到场了,,,防止导员啥的,你懂得。。。但其实吧,这种签名很容易被伪造,随便找一个人代签一下,或者说找一个会模仿别人字迹的人帮忙签一下。在计算机世界中,我们可能会需要电子签名,因为我们大多数情况下会使用电子文件,那这时候怎么办呢?当然,我们仍然可以选择使用自己的名字。但其实还有另外一种方式,那就是采用数字签名,这种签名更加难以伪造,可信程度更高。数字签名的主要用处是确保消息确实来自于声称产生该消息的人。

数字签名(digital signature)主要用于对数字消息(digital message)进行签名,以防消息的冒名伪造或篡改,亦可以用于通信双方的身份鉴别。

数字签名依赖于非对称密码,因为我们必须确保一方能够做的事情,而另一方不能够做出这样的事情。其基本原理如下

数字签名应当具有以下几个特性:

(1) 签名是可信的:任何人都可以验证签名的有效性。

(2) 签名是不可伪造的:除了合法的签名者之外,任何其他人伪造其签名是困难的。

(3) 签名是不可复制的:对一个消息的签名不能通过复制变为另一个消息的签名。如果对一个消息的签名是从别处复制得到的,则任何人都可以发现消息与签名之间的不一致性,从而可以拒绝签名的消息。

(4) 签名的消息是不可改变的:经签名的消息不能被篡改。一旦签名的消息被篡改,则任何人都可以发现消息与签名之间的不一致性。

(5) 签名是不可抵赖的:签名者事后不能否认自己的签名。

RSA 数字签名

原理

原理类似于 RSA 加密,只是这里使用私钥进行加密,将加密后的结果作为签名。

2018 Backdoor Awesome mix1

首先,可以简单分析源码,这里程序使用 PKCS1_V1.5 进行了 RSA 签名,这会对明文消息进行扩展,具体扩展规则请参考 https://www.emc.com/collateral/white-papers/h11300-pkcs-1v2-2-rsa-cryptography-standard-wp.pdf 。这里给出对应扩展脚本,对应于题目中的 from Util import PKCS1_pad as pad

def PKCS1_pad(data):
    asn1 = "3021300906052b0e03021a05000414"
    ans = asn1 + data
    n = len(ans)
    return int(('00' + '01' + 'ff' * (1024 / 8 - n / 2 - 3) + '00' + ans), 16)

程序希望我们给出 n,e 使得程序满足

$h(m)^e mod \ n=pad(m)$

这里我们已经知道 h(m),pad(m)。显然如果我们控制 e=1的话,那么

$h(m)-pad(m)=kn$

那么如果我们可以设置 k=1,既可以得到 n。

本地部署 socat TCP4-LISTEN:12345,fork EXEC:./mix1.py

exp 如下

from Crypto.Hash import SHA
from pwn import *

from Util import PKCS1_pad

#context.log_level = 'debug'


def main():
    port = 12345
    host = "127.0.0.1"
    p = remote(host, port)
    p.recvuntil('Message   -> ')
    message = p.recvuntil('\n\nSignature -> ', drop=True)
    log.info('message: ' + message)
    signature = p.recvuntil('\n', drop=True)
    log.info('signature: ' + signature)

    h = SHA.new(message)

    m = PKCS1_pad(h.hexdigest())

    e = 1
    n = int(signature, 16) - m

    p.sendlineafter('Enter n:', str(n))
    p.sendlineafter('Enter e:', str(e))

    p.interactive()


main()

效果如下

➜  2018-BackdoorCTF-Awesome-mix1 git:(master) python exp.py
[+] Opening connection to 127.0.0.1 on port 12345: Done
[*] message: super important information for admin only
[*] signature: 721af5bd401b5f2aff8e86bf811b827cdb5877ef12202f24fa914a26f235523f80c45fdbf0d3c9fa77278828ddd8ca0551a941bd57c97dd38654692568d1357a49e7a2a284d296508602ead24c91e5aa7f517b9e48422575f0dd373d00f267a206ba164ab104c488268b5f95daf490a048407773d4b1016de8ef508bf1aa678f
[*] Switching to interactive mode
CTF{cryp70_5ur3_15_w13rd}
[*] Got EOF while reading in interactive

2018 Backdoor Awesome mix2

本地部署 socat TCP4-LISTEN:12345,fork EXEC:./service.py

题目类似于上面的题目,唯一的区别在于对于 e 有约束,必须大于 3,所以我们不能使用 1 了。

$h(m)^e mod \ n=pad(m)$

这里我们已经知道 h(m),pad(m)。我们只需要构造剩下的数即可,这里我们构造 n 为素数,使得 n-1是一个光滑数,这样就可以使用 pohlig_hellman 算法了。

from Crypto.Hash import SHA
from pwn import *
import gmpy2
from gmpy2 import is_prime
import random


def PKCS1_pad(data):
    asn1 = "3021300906052b0e03021a05000414"
    ans = asn1 + data
    n = len(ans)
    return int(('00' + '01' + 'ff' * (1024 / 8 - n / 2 - 3) + '00' + ans), 16)


#context.log_level = 'debug'
def gen_smooth_num(plist, minnum=pow(2, 1020)):
    lenp = len(plist)
    while True:
        n = 1
        factors = dict()
        while n + 1 < minnum:
            tmp = random.randint(0, lenp - 1)
            n *= plist[tmp]
            if plist[tmp] in factors:
                factors[plist[tmp]] += 1
            else:
                factors[plist[tmp]] = 1
        if n.bit_length() > 1024:
            continue
        if is_prime(n + 1):
            return n + 1, factors


# http://pythonexample.com/snippet/pohligpy_neuratron_python
# solve g^x=h mod m
def log_prime_power(g, h, pf, pe, M):

    powers = [pf**k for k in range(pe)]

    gamma = gmpy2.powmod(g, powers[-1], M)

    xk = gmpy2.mpz(0)
    for k in range(pe):
        if k == 0:
            hk = gmpy2.powmod(h, powers[pe - k - 1], M)
        else:
            gk = gmpy2.powmod(g, xk * (M - 2), M)
            hk = gmpy2.powmod(gk * h, powers[pe - k - 1], M)

        k_log_found = False
        for dk in range(pf):
            yk = gmpy2.powmod(gamma, dk, M)
            if yk == hk:
                k_log_found = True
                break

        if not k_log_found:
            raise Exception("can not solve")

        xk += gmpy2.mul(powers[k], dk)

    return xk


def pohlig_hellman(g, h, M, factors):
    M1 = M - 1
    xs = []
    for f in factors:
        pf = f
        pe = factors[f]

        subgroup_exponent = gmpy2.div(M1, gmpy2.powmod(pf, pe, M))
        gi = gmpy2.powmod(g, subgroup_exponent, M)
        hi = gmpy2.powmod(h, subgroup_exponent, M)

        xi = log_prime_power(gi, hi, pf, pe, M)
        xs.append(xi)
    crt_coeffs = []

    for f in factors:
        pf = f
        pe = factors[f]

        mi = pf**pe

        bi = gmpy2.div(M, mi)
        bi_inv = gmpy2.invert(bi, mi)
        crt_coeffs.append(gmpy2.mul(bi, bi_inv))
    x = 0
    for i in range(len(crt_coeffs)):
        x = gmpy2.t_mod(x + gmpy2.t_mod(xs[i] * crt_coeffs[i], M1), M1)
    return x


#context.log_level = 'debug'


def main():
    port = 12345
    host = "127.0.0.1"
    p = remote(host, port)
    p.recvuntil('Message   -> ')
    message = p.recvuntil('\n\nSignature -> ', drop=True)
    log.info('message: ' + message)
    signature = p.recvuntil('\n', drop=True)
    log.info('signature: ' + signature)
    signature = int(signature, 16)
    h = SHA.new(message)

    m = PKCS1_pad(h.hexdigest())
    print m, signature
    plist = []
    for i in range(2, 1000):
        if is_prime(i):
            plist.append(i)
    while True:
        try:
            n, factors = gen_smooth_num(plist, signature)
            e = pohlig_hellman(signature, m, n, factors)
        except Exception as e:
            continue
        else:
            break
    print n, e

    print m
    print gmpy2.powmod(signature, e, n)

    p.sendlineafter('Enter n:', str(n))
    p.sendlineafter('Enter e:', str(e))

    p.interactive()


main()

有两点需要注意

  1. 由于 $g^x=y$ 中的 g 和 y 都是给定的,我们新找到的 n,不一定 g 的幂次构成的群会包含 y,所以可能求解失败,所以需要多次求解。
  2. 源代码中虽然 n.bit_length() <= 1025,但是其实 n 在满足不小于 signature 的条件时,必须满足如下条件(pycrypto 源码)
        modBits = Crypto.Util.number.size(self._key.n)
        k = ceil_div(modBits,8) # Convert from bits to bytes

        # Step 1
        if len(S) != k:
            return 0

所以我们最好设置 n 为1024 比特位。

ElGamal

RSA的数字签名方案几乎与其加密方案完全一致,只是利用私钥进行了签名。但是,对于ElGamal来说,其签名方案与相应的加密方案具有很大区别。

基本原理

密钥生成

基本步骤如下

  1. 选取一个足够大的素数p(十进制位数不低于160),以便于在$Z_p$ 上求解离散对数问题是困难的。
  2. 选取$Z_p^*$ 的生成元g。
  3. 随机选取整数d,$0\leq d \leq p-2$ ,并计算$g^d \equiv y \bmod p$ 。

其中私钥为{d},公钥为{p,g,y} 。

签名

A选取随机数$k \in Z_{p-1}$ ,并且$gcd(k,p-1)=1$,对消息进行签名

$$
sig_d(m,k)=(r,s)
$$

其中$r \equiv g^k \bmod p$ ,$s \equiv (m-dr)k^{-1} \bmod p-1$ 。

验证

如果 $g^m \equiv y^rr^s \bmod p$ ,那么验证成功,否则验证失败。这里验证成功的原理如下,首先我们有

$$
y^rr^s \equiv g^{dr}g^{ks} \equiv g^{dr+ks}
$$

又因为

$$
s \equiv (m-dr)k^{-1} \bmod p-1
$$

所以

$$
ks \equiv m-dr \bmod p-1
$$

进而

$$
ks+dr=a*(p-1)+m
$$

所以

$$
g^{ks+dr}=g^{a(p-1)+m}=(g^{p-1})^ag^m
$$

所以根据费马定理,可得

$$
g^{ks+dr} \equiv g^m \bmod p
$$

常见攻击

完全破译攻击

攻击条件

  • p太小或无大素因子

如果$p$太小我们可以直接用大部小步算法分解, 或者如果其无大的素因子, 我们可以采用$Pohling: Hellman$算法计算离散对数即可进而求出私钥。

  • 随机数k复用

如果签名者复用了随机数k,那么攻击者就可以轻而易举地计算出私钥。具体的原理如下:

假设目前有两个签名都是使用同一个随机数进行签名的。那么我们有

$$
r \equiv g^k \bmod p \\ s _1\equiv (m_1-dr)k^{-1} \bmod p-1\\ r \equiv g^k \bmod p \\ s_2 \equiv (m_2-dr)k^{-1} \bmod p-1
$$

进而有

$$
s_1k \equiv m_1-dr \bmod p-1 \\ s_2k \equiv m_2-dr \bmod p-1
$$

两式相减

$$
k(s_1-s_2) \equiv m_1-m_2 \bmod p-1
$$

这里,$s_1,s_2,m_1,m_2,p-1$ 均已知,所以我们可以很容易算出k。当然,如果$gcd(s_1-s_2,p-1)!=1$ 的话,可能会存在多个解,这时我们只需要多试一试。进而,我们可以根据s的计算方法得到私钥d,如下

$$
d \equiv \frac{m-ks}{r}
$$

题目

2016 LCTF Crypto 450

通用伪造签名

攻击条件

如果消息$m$没有取哈希,或者消息$m$没有指定消息格式的情况下攻击成立。

原理

在攻击者知道了某个人Alice的公钥之后,他可以伪造Alice的签名信息。具体原理如下:

这里我们假设,Alice的公钥为{p,g,y}。攻击者可以按照如下方式伪造

  1. 选择整数 $i$,$j$,其中$gcd(j,p-1)=1$

  2. 计算签名,$r \equiv g^iy^j \bmod p$ ,$s\equiv -rj^{-1} \bmod p-1$

  3. 计算消息,$m\equiv si \bmod p-1$

那么此时生成的签名与消息就是可以被正常通过验证,具体推导如下:

$y^rr^s \equiv g^{dr}g^{is}y^{js} \equiv g^{dr}g^{djs}g^{is} \equiv g^{dr+s(i+dj)} \equiv g^{dr} g^{-rj^{-1}(i+dj)} \equiv g^{dr-dr-rij^{-1}} \equiv g^{si} \bmod p$

又由于消息m的构造方式,所以

$$
g^{si} \equiv g^m \bmod p-1
$$

需要注意的是,攻击者可以伪造通过签名验证的消息,但是他却无法伪造指定格式的消息。而且,一旦消息进行了哈希操作,这一攻击就不再可行。

已知签名伪造

攻击条件

假设攻击者知道$(r, s)$是消息$M$的签名,则攻击者可利用它来伪造其它消息的签名。

原理

  1. 选择整数$h, i, j \in[0, p-2]$且满足$\operatorname{gcd}(h r-j s, \varphi(p))=1$
  2. 计算下式
    $\begin{array}{l}
    r^{\prime}=r^{h} \alpha^{i} y_{A}^{j} \bmod p \
    s^{\prime}=\operatorname{sr}(h r-j s)^{-1} \bmod \varphi(p) \
    m^{\prime}=r^{\prime}(h m+i s)(h r-j s)^{-1} \bmod \varphi(p)
    \end{array}$

可得到$(r’,s’)$是$m’$的有效签名

证明如下:

已知Alice对消息$x$的签名$(\gamma,\delta)$满足$\beta^{\gamma} \gamma^{\delta} \equiv \alpha^{x}(\bmod p)$,所以我们目的为构造出$\left(x^{\prime}, \lambda, \mu\right)$满足

$$
\beta^{\lambda} \lambda^{\mu} \equiv \alpha^{x’}(\bmod p)
$$

那么,首先我们把$\lambda$表示为三个已知底$\alpha, \beta, \gamma$的形式: $\lambda=\alpha^{i} \beta^{j} \gamma^{h} \bmod p$,由条件可得

$$
\beta^{\gamma} \gamma^{\delta} \equiv \alpha^{x}(\bmod p) \Leftrightarrow \gamma=\left(\beta^{-\gamma} \alpha^{x}\right)^{\delta-1} \bmod p
$$

那么我们可以得到

$$
\lambda=\alpha^{i+x \delta^{-1} h} \beta^{j-\gamma \delta^{-1} h} \bmod p
$$

我们把$\lambda$的表达式代入一式中

$$
\begin{aligned}& \beta^{\lambda}\left(\alpha^{i+x \delta^{-1} h} \beta^{j-\gamma \delta^{-1} h}\right)^{\mu} \equiv \alpha^{x^{\prime}}(\bmod p) \\Leftrightarrow & \beta^{\lambda+\left(j-\gamma \delta^{-1} h\right) \mu} \equiv \alpha^{x^{\prime}-\left(i+x \delta^{-1} h\right) \mu}(\bmod p)\end{aligned}
$$

我们令两边指数为$0$, 即

$$
\left{\begin{matrix}\lambda+\left(j-\gamma \delta^{-1} h\right) \mu \equiv 0 \bmod p-1 \ x^{\prime}-\left(i+x \delta^{-1} h\right) \mu \equiv 0 \bmod p-1 \end{matrix}\right.
$$

可以得到

$$
\mu=\delta \lambda(h \gamma-j \delta)^{-1} \quad(\bmod p-1) \
x^{\prime}=\lambda(h x+i \delta)(h \gamma-j \delta)^{-1}(\bmod p-1)
$$

其中

$$
\lambda=\alpha^{i} \beta^{j} \gamma^{h} \bmod p
$$

所以我们得到$(\lambda, \mu)$是 $x’$ 的有效签名。

此外,我们还可以借助CRT构造$m’$, 原理如下:

  1. $u=m^{\prime} m^{-1} \bmod \varphi(p), \quad s^{\prime}=s u \bmod \varphi(p)$
  2. 再计算$r^{\prime}, \quad r^{\prime} \equiv r u \bmod \varphi(p), r^{\prime} \equiv r \bmod p$

显然可以使用CRT求解$r’$, 注意到 $y_{A}^{r’} r’^{s^{\prime}}=y_{A}^{ru} r^{s u}=\left(y_{A}^{r} r^{s}\right)^{u}=\alpha^{m u} \equiv \alpha^{m} \bmod p$

所以$(r’,s’)$是消息$m’$的有效签名。

抵抗措施:在验证签名时, 检查$r < p$。

选择签名伪造

攻击条件

如果我们可以选择我们消息进行签名,并且可以得到签名,那么我们可以对一个新的但是我们不能够选择签名的消息伪造签名。

原理

我们知道,最后验证的过程如下

$g^m \equiv y^rr^s \bmod p$

那么只要我们选择一个消息m使其和我们所要伪造的消息$m’$模p-1同余,然后同时使用消息m的签名即可绕过。

题目

这里以2017年国赛mailbox为例,i春秋有复现

首先,我们来分析一下程序,我们首先需要进行proof of work

    proof = b64.b64encode(os.urandom(12))
    req.sendall(
        "Please provide your proof of work, a sha1 sum ending in 16 bit's set to 0, it must be of length %d bytes, starting with %s\n" % (
        len(proof) + 5, proof))

    test = req.recv(21)
    ha = hashlib.sha1()
    ha.update(test)

    if (test[0:16] != proof or ord(ha.digest()[-1]) != 0 or ord(ha.digest()[-2]) != 0): # or ord(ha.digest()[-3]) != 0 or ord(ha.digest()[-4]) != 0):
        req.sendall("Check failed")
        req.close()
        return 

我们需要生成一个以proof开头的长度为proof长度加5的字符串,并且其sha1的值以16比特的0结束。

这里我们直接使用如下的方式来绕过。

def f(x):
    return sha1(prefix + x).digest()[-2:] == '\0\0'


sh = remote('106.75.66.195', 40001)
# bypass proof
sh.recvuntil('starting with ')
prefix = sh.recvuntil('\n', drop=True)
print string.ascii_letters
s = util.iters.mbruteforce(f, string.ascii_letters + string.digits, 5, 'fixed')
test = prefix + s
sh.sendline(test)

这里使用了pwntools中的util.iters.mbruteforce,这是一个利用给定字符集合以及指定长度进行多线程爆破的函数。其中,第一个参数为爆破函数,这里是sha1,第二个参数是字符集,第三个参数是字节数,第四个参数指的是我们只尝试字节数为第三个参数指定字节数的排列,即长度是固定的。更加具体的信息请参考pwntools。

绕过之后,我们继续分析程序,简单看下generate_keys函数,可以知道该函数是ElGamal生成公钥的过程,然后看了看verify函数,就是验证签名的过程。

继续分析

            if len(msg) > MSGLENGTH:
                req.sendall("what r u do'in?")
                req.close()
                return
            if msg[:4] == "test":
                r, s = sign(digitalize(msg), sk, pk, p, g)
                req.sendall("Your signature is" + repr((hex(r), hex(s))) + "\n")
            else:
                if msg == "Th3_bery_un1que1i_ChArmIng_G3nji" + test:
                    req.sendall("Signature:")
                    sig = self.rfile.readline().strip()
                    if len(sig) > MSGLENGTH:
                        req.sendall("what r u do'in?")
                        req.close()
                        return
                    sig_rs = sig.split(",")
                    if len(sig_rs) < 2:
                        req.sendall("yo what?")
                        req.close()
                        return
                    # print "Got sig", sig_rs
                    if verify(digitalize(msg), int(sig_rs[0]), int(sig_rs[1]), pk, p, g):
                        req.sendall("Login Success.\nDr. Ziegler has a message for you: " + FLAG)
                        print "shipped flag"
                        req.close()
                        return
                    else:
                        req.sendall("You are not the Genji I knew!\n")

根据这三个if条件可以知道

  • 我们的消息长度不能超过MSGLENGTH,40000。
  • 我们可以对消息开头为test的消息进行签名。
  • 我们需要使得以Th3_bery_un1que1i_ChArmIng_G3nji开头,以我们绕过proof的test为结尾的消息通过签名验证,其中,我们可以自己提供签名的值。

分析到这里,其实就知道了,我们就是在选择指定签名进行伪造,这里我们自然要充分利用第二个if条件,只要我们确保我们输入的消息的开头为‘test’,并且该消息与以Th3_bery_un1que1i_ChArmIng_G3nji开头的固定消息模p-1同余,我们即可以通过验证。

那我们如何构造呢?既然消息的长度可以足够长,那么我们可以将’test’对应的16进制先左移得到比p-1大的数字a,然后用a对p-1取模,用a再减去余数,此时a模p-1余0了。这时再加上以Th3_bery_un1que1i_ChArmIng_G3nji开头的固定消息的值,即实现了模p-1同余。

具体如下

# construct the message begins with 'test'
target = "Th3_bery_un1que1i_ChArmIng_G3nji" + test
part1 = (digitalize('test' + os.urandom(51)) << 512) // (p - 1) * (p - 1)
victim = part1 + digitalize(target)
while 1:
    tmp = hex(victim)[2:].decode('hex')
    if tmp.startswith('test') and '\n' not in tmp:
        break
    else:
        part1 = (digitalize('test' + os.urandom(51)) << 512) // (p - 1) * (
            p - 1)
        victim = part1 + digitalize(target)

最后的脚本如下

from pwn import *
from hashlib import sha1
import string
import ast
import os
import binascii
context.log_level = 'debug'


def f(x):
    return sha1(prefix + x).digest()[-2:] == '\0\0'


def digitalize(m):
    return int(m.encode('hex'), 16)


sh = remote('106.75.66.195', 40001)
# bypass proof
sh.recvuntil('starting with ')
prefix = sh.recvuntil('\n', drop=True)
print string.ascii_letters
s = util.iters.mbruteforce(f, string.ascii_letters + string.digits, 5, 'fixed')
test = prefix + s
sh.sendline(test)

sh.recvuntil('Current PK we are using: ')
pubkey = ast.literal_eval(sh.recvuntil('\n', drop=True))
p = pubkey[0]
g = pubkey[1]
pk = pubkey[2]

# construct the message begins with 'test'
target = "Th3_bery_un1que1i_ChArmIng_G3nji" + test
part1 = (digitalize('test' + os.urandom(51)) << 512) // (p - 1) * (p - 1)
victim = part1 + digitalize(target)
while 1:
    tmp = hex(victim)[2:].decode('hex')
    if tmp.startswith('test') and '\n' not in tmp:
        break
    else:
        part1 = (digitalize('test' + os.urandom(51)) << 512) // (p - 1) * (
            p - 1)
        victim = part1 + digitalize(target)

assert (victim % (p - 1) == digitalize(target) % (p - 1))

# get victim signature
sh.sendline(hex(victim)[2:].decode('hex'))
sh.recvuntil('Your signature is')
sig = ast.literal_eval(sh.recvuntil('\n', drop=True))
sig = [int(sig[0], 0), int(sig[1], 0)]

# get flag
sh.sendline(target)
sh.sendline(str(sig[0]) + "," + str(sig[1]))
sh.interactive()

这里还要说几个有意思的点就是

  • int(x,0)只的是将x按照其字面对应的进制转换为对应的数字,比如说int(‘0x12’,0)=18,这里相应的字面必须有对应标志开头,比如说十六进制是0x,8进制是0,二进制是0b。因为如果没有的话,就不知道该如何识别了。
  • python(python2) 里面到底多大的数,计算出来最后才会带有L呢?正常情况下,大于int都会有L。但是这个里面的victim确实是没有的,, 一个问题,待解决。。

DSA

上面所描述的ElGamal签名算法在实际中并不常用,更常用的是其变体DSA。

基本原理

密钥生成

  1. 选择一个合适的哈希函数,目前一般选择SHA1,当前也可以选择强度更高的哈希函数H。
  2. 选择密钥的长度L和N,这两个值决定了签名的安全程度。在最初的DSS(Digital Signature Standard )中建议L必须为64的倍数,并且$512 \leq L \leq 1024$ ,当然,也可以更大。N必须大小必须不大于哈希函数H输出的长度。FIPS 186-3给出了一些建议的L和N的取值例子:(1024, 160), (2048, 224), (2048, 256),以及 (3,072, 256)。
  3. 选择N比特的素数q。
  4. 选择L比特的素数p,使得p-1是q的倍数。
  5. 选择满足$g^k \equiv 1 \bmod p$ 的最小正整数k为q的g,即在模p的背景下,ord(g)=q的g。即g在模p的意义下,其指数次幂可以生成具有q个元素的子群。这里,我们可以通过计算$g=h^{\frac{p-1}{q}} \bmod p$ 来得到g,其中$1< h < p-1$ 。
  6. 选择私钥x,$0<x<q$ ,计算$y \equiv g^x \bmod p$ 。

公钥为(p,q,g,y),私钥为(x)。

签名

签名步骤如下

  1. 选择随机整数数k作为临时密钥,$0<k<q$ 。
  2. 计算$r\equiv (g^k \bmod p) \bmod q$
  3. 计算$s\equiv (H(m)+xr)k^{-1} \bmod q$

签名结果为(r,s)。需要注意的是,这里与Elgamal很重要的不同是这里使用了哈希函数对消息进行了哈希处理。

验证

验证过程如下

  1. 计算辅助值,$w=s^{-1} \bmod q$
  2. 计算辅助值,$u_1=H(m)w \bmod q$
  3. 计算辅助值,$u_2=rw \bmod q$
  4. 计算$v=(g^{u_1}y^{u_2} \bmod p) \bmod q$
  5. 如果v与r相等,则校验成功。

正确性推导

首先,g 满足 $g^k \equiv 1 \bmod p$ 的最小正整数k为q。所以 $g^q \equiv 1 \bmod p$ 。所以 $g^x \equiv g^{x \bmod q} \bmod p$ 。进而

$v=(g^{u_1}y^{u_2} \bmod p) \bmod q=g^{u_1}g^{xu_2} \equiv g^{H(m)w}g^{xrw} \equiv g^{H(m)w+xrw}$

又$s\equiv (H(m)+xr)k^{-1} \bmod q$ 且$w=s^{-1} \bmod q$ 所以

$k \equiv s^{-1}(H(m)+xr) \equiv H(m)w+xrw \bmod q$

所以$v \equiv g^k$ 。正确性得证。

安全性

已知k

原理

如果知道了随机密钥k,那么我们就可以根据$s\equiv (H(m)+xr)k^{-1} \bmod q$ 计算私钥d,几乎攻破了DSA。

这里一般情况下,消息的hash值都会给出。

$x \equiv r^{-1}(ks-H(m)) \bmod q$

k共享

原理

如果在两次签名的过程中共享了k,我们就可以进行攻击。

假设签名的消息为m1,m2,显然,两者的r的值一样,此外

$s_1\equiv (H(m_1)+xr)k^{-1} \bmod q$

$s_2\equiv (H(m_2)+xr)k^{-1} \bmod q$

这里我们除了x和k不知道剩下的均知道,那么

$s_1k \equiv H(m_1)+xr$

$s_2k \equiv H(m_2)+xr$

两式相减

$k(s_1-s_2) \equiv H(m_1)-H(m_2) \bmod q$

此时 即可解出k,进一步我们可以解出x。

例子

这里我们以湖湘杯的DSA为例,但是不能直接去做,,,因为发现在验证message4的时候签名不通过。源题目我没有了,。,,这里我以Jarvis OJ中经过修改的题目DSA为例

➜  2016湖湘杯DSA git:(master) ✗ openssl sha1 -verify dsa_public.pem -signature packet1/sign1.bin  packet1/message1  
Verified OK
➜  2016湖湘杯DSA git:(master) ✗ openssl sha1 -verify dsa_public.pem -signature packet2/sign2.bin  packet2/message1 
packet2/message1: No such file or directory
➜  2016湖湘杯DSA git:(master) ✗ openssl sha1 -verify dsa_public.pem -signature packet2/sign2.bin  packet2/message2 
Verified OK
➜  2016湖湘杯DSA git:(master) ✗ openssl sha1 -verify dsa_public.pem -signature packet3/sign3.bin  packet3/message3 
Verified OK
➜  2016湖湘杯DSA git:(master) ✗ openssl sha1 -verify dsa_public.pem -signature packet4/sign4.bin  packet4/message4
Verified OK

可以看出四则消息全部校验通过。这里之所以会联想到共享k是因为题目中提示了PS3的破解曾用到这个方法,从网上搜索可知该攻击。

下面,我们看一下签名后的值,这里使用的命令如下

➜  2016湖湘杯DSA git:(master) ✗ openssl asn1parse -inform der -in packet4/sign4.bin  
    0:d=0  hl=2 l=  44 cons: SEQUENCE          
    2:d=1  hl=2 l=  20 prim: INTEGER           :5090DA81FEDE048D706D80E0AC47701E5A9EF1CC
   24:d=1  hl=2 l=  20 prim: INTEGER           :5E10DED084203CCBCEC3356A2CA02FF318FD4123
➜  2016湖湘杯DSA git:(master) ✗ openssl asn1parse -inform der -in packet3/sign3.bin  
    0:d=0  hl=2 l=  44 cons: SEQUENCE          
    2:d=1  hl=2 l=  20 prim: INTEGER           :5090DA81FEDE048D706D80E0AC47701E5A9EF1CC
   24:d=1  hl=2 l=  20 prim: INTEGER           :30EB88E6A4BFB1B16728A974210AE4E41B42677D
➜  2016湖湘杯DSA git:(master) ✗ openssl asn1parse -inform der -in packet2/sign2.bin  
    0:d=0  hl=2 l=  44 cons: SEQUENCE          
    2:d=1  hl=2 l=  20 prim: INTEGER           :60B9F2A5BA689B802942D667ED5D1EED066C5A7F
   24:d=1  hl=2 l=  20 prim: INTEGER           :3DC8921BA26B514F4D991A85482750E0225A15B5
➜  2016湖湘杯DSA git:(master) ✗ openssl asn1parse -inform der -in packet1/sign1.bin  
    0:d=0  hl=2 l=  45 cons: SEQUENCE          
    2:d=1  hl=2 l=  21 prim: INTEGER           :8158B477C5AA033D650596E93653C730D26BA409
   25:d=1  hl=2 l=  20 prim: INTEGER           :165B9DD1C93230C31111E5A4E6EB5181F990F702

其中,获取的第一个值是r,第二个值是s。可以看到第4个packet和第3个packet共享了k,因为他们的r一致。

这里我们可以使用openssl看下公钥

➜  2016湖湘杯DSA git:(master) ✗ openssl dsa -in dsa_public.pem -text -noout  -pubin 
read DSA key
pub: 
    45:bb:18:f6:0e:b0:51:f9:d4:82:18:df:8c:d9:56:
    33:0a:4f:f3:0a:f5:34:4f:6c:95:40:06:1d:53:83:
    29:2d:95:c4:df:c8:ac:26:ca:45:2e:17:0d:c7:9b:
    e1:5c:c6:15:9e:03:7b:cc:f5:64:ef:36:1c:18:c9:
    9e:8a:eb:0b:c1:ac:f9:c0:c3:5d:62:0d:60:bb:73:
    11:f1:cf:08:cf:bc:34:cc:aa:79:ef:1d:ad:8a:7a:
    6f:ac:ce:86:65:90:06:d4:fa:f0:57:71:68:57:ec:
    7c:a6:04:ad:e2:c3:d7:31:d6:d0:2f:93:31:98:d3:
    90:c3:ef:c3:f3:ff:04:6f
P:   
    00:c0:59:6c:3b:5e:93:3d:33:78:be:36:26:be:31:
    5e:e7:0c:a6:b5:b1:1a:51:9b:55:23:d4:0e:5b:a7:
    45:66:e2:2c:c8:8b:fe:c5:6a:ad:66:91:8b:9b:30:
    ad:28:13:88:f0:bb:c6:b8:02:6b:7c:80:26:e9:11:
    84:be:e0:c8:ad:10:cc:f2:96:be:cf:e5:05:05:38:
    3c:b4:a9:54:b3:7c:b5:88:67:2f:7c:09:57:b6:fd:
    f2:fa:05:38:fd:ad:83:93:4a:45:e4:f9:9d:38:de:
    57:c0:8a:24:d0:0d:1c:c5:d5:fb:db:73:29:1c:d1:
    0c:e7:57:68:90:b6:ba:08:9b
Q:   
    00:86:8f:78:b8:c8:50:0b:eb:f6:7a:58:e3:3c:1f:
    53:9d:35:70:d1:bd
G:   
    4c:d5:e6:b6:6a:6e:b7:e9:27:94:e3:61:1f:41:53:
    cb:11:af:5a:08:d9:d4:f8:a3:f2:50:03:72:91:ba:
    5f:ff:3c:29:a8:c3:7b:c4:ee:5f:98:ec:17:f4:18:
    bc:71:61:01:6c:94:c8:49:02:e4:00:3a:79:87:f0:
    d8:cf:6a:61:c1:3a:fd:56:73:ca:a5:fb:41:15:08:
    cd:b3:50:1b:df:f7:3e:74:79:25:f7:65:86:f4:07:
    9f:ea:12:09:8b:34:50:84:4a:2a:9e:5d:0a:99:bd:
    86:5e:05:70:d5:19:7d:f4:a1:c9:b8:01:8f:b9:9c:
    dc:e9:15:7b:98:50:01:79

下面,我们直接利用上面的原理编写程序即可,程序如下

#coding=utf8
from Crypto.PublicKey import DSA
from hashlib import sha1
import gmpy2
with open('./dsa_public.pem') as f:
    key = DSA.importKey(f)
    y = key.y
    g = key.g
    p = key.p
    q = key.q
f3 = open(r"packet3/message3", 'r')
f4 = open(r"packet4/message4", 'r')
data3 = f3.read()
data4 = f4.read()
sha = sha1()
sha.update(data3)
m3 = int(sha.hexdigest(), 16)
sha = sha1()
sha.update(data4)
m4 = int(sha.hexdigest(), 16)
print m3, m4
s3 = 0x30EB88E6A4BFB1B16728A974210AE4E41B42677D
s4 = 0x5E10DED084203CCBCEC3356A2CA02FF318FD4123
r = 0x5090DA81FEDE048D706D80E0AC47701E5A9EF1CC
ds = s4 - s3
dm = m4 - m3
k = gmpy2.mul(dm, gmpy2.invert(ds, q))
k = gmpy2.f_mod(k, q)
tmp = gmpy2.mul(k, s3) - m3
x = tmp * gmpy2.invert(r, q)
x = gmpy2.f_mod(x, q)
print int(x)

我发现pip安装的pycrypto竟然没有DSA的importKey函数。。。只好从github上下载安装了pycrypto。。。

结果如下

➜  2016湖湘杯DSA git:(master) ✗ python exp.py
1104884177962524221174509726811256177146235961550 943735132044536149000710760545778628181961840230
520793588153805320783422521615148687785086070744

攻击思想总结

攻击模式

在我们攻击一个密码学系统时,我们或多或少会得到关于这个系统的一些信息。根据得到信息量的不同,我们可以采用的方法就可能不同。在当今的密码学分析时,一般我们都会假设攻击者知道密码学算法,这个假设是合理的,因为历史上有很多保密的算法最后都被人所知,比如 RC4。被知道的方式多重多样,比如间谍,逆向工程等。

这里我们根据攻击者获取密码学系统的信息的多少将攻击模式分为以下几类

  • 唯密文攻击:攻击者仅能获得一些加密过的密文。
  • 已知明文攻击:攻击者有一些密文对应的明文。
  • 选择明文攻击:攻击者在开始攻击时可以选择一些明文,并获取加密后的密文。如果攻击者在攻击中途可以根据已经获取的信息选择新的明文并获取对应的密文,则称为适应性选择明文攻击。
  • 选择密文攻击:攻击者在开始攻击之前可以选择一些密文,并获取解密后的明文。如果攻击者在攻击图中可以根据已经获取的信息选择一些新的密文并获取对应的明文,则称为适应性选择密文攻击。
  • 相关密钥攻击:攻击者可以获得两个或多个相关密钥的加密或解密后的密文或明文。但是攻击者不知道这些密钥。

常见攻击方法

根据不同的攻击模式,可能会有不同的攻击方法,目前常见的攻击方法主要有

  • 暴力攻击
  • 中间相遇攻击
  • 线性分析
  • 差分分析
  • 不可能差分分析
  • 积分分析
  • 代数分析
  • 相关密钥攻击
  • 侧信道攻击

中间相遇攻击 - MITM

概述

中间相遇攻击是一种以空间换取时间的一种攻击方法,1977年由 Diffie 与 Hellman 提出。从个人角度看,者更多地指一种思想,不仅仅适用于密码学攻击,也适用于其他方面,可以降低算法的复杂度。

基本原理如下

假设 E 和 D 分别是加密函数和解密函数,k1 和 k2 分别是两次加密使用的密钥,则我们有

$C=E_{k_2}(E_{k_1}(P))$

$P=D_{k_2}(D_{k_1}(C))$

则我们可以推出

$E_{k_1}(P)=D_{k_2}(C)$

那么,当用户知道一对明文和密文时

  1. 攻击者可以枚举所有的 k1,将 P 所有加密后的结果存储起来,并按照密文的大小进行排序。
  2. 攻击者进一步枚举所有的k2,将密文 C 进行解密得到 C1,在第一步加密后的结果中搜索 C1,如果搜索到,则我们在一定程度上可以认为我们找到了正确的 k1 和 k2。
  3. 如果觉得第二步中得到的结果不保险,则我们还可以再找一些明密文对进行验证。

假设 k1 和 k2 的密钥长度都为 n,则原先我们暴力枚举需要 $O(n^2)$,现在我们只需要 $O(n log_2n)$。

这与 2DES 的中间相遇攻击类似。

比特攻击

概述

简单地说,就是利用比特位之间的关系进行攻击。

2018 Plaid CTF transducipher

题目如下

#!/usr/bin/env python3.6
import os

BLOCK_SIZE = 64

T = [
    ((2, 1), 1),
    ((5, 0), 0),
    ((3, 4), 0),
    ((1, 5), 1),
    ((0, 3), 1),
    ((4, 2), 0),
]


def block2bin(b, length=BLOCK_SIZE):
    return list(map(int, bin(b)[2:].rjust(length, '0')))


def bin2block(b):
    return int("".join(map(str, b)), 2)


def transduce(b, s=0):
    if len(b) == 0:
        return b
    d, t = T[s]
    b0, bp = b[0], b[1:]
    return [b0 ^ t] + transduce(bp, s=d[b0])


def transduceblock(b):
    return bin2block(transduce(block2bin(b)))


def swap(b):
    l = BLOCK_SIZE // 2
    m = (1 << l) - 1
    return (b >> l) | ((b & m) << l)


class Transducipher:

    def __init__(self, k):
        self.k = [k]
        for i in range(1, len(T)):
            k = swap(transduceblock(k))
            self.k.append(k)

    def encrypt(self, b):
        for i in range(len(T)):
            b ^= self.k[i]
            b = transduceblock(b)
            b = swap(b)
        return b


if __name__ == "__main__":
    flag = bytes.hex(os.urandom(BLOCK_SIZE // 8))
    k = int(flag, 16)
    C = Transducipher(k)
    print("Your flag is PCTF{%s}" % flag)
    with open("data1.txt", "w") as f:
        for i in range(16):
            pt = int(bytes.hex(os.urandom(BLOCK_SIZE // 8)), 16)
            ct = C.encrypt(pt)
            f.write(str((pt, ct)) + "\n")

题目给了 16 组明密文对

  • 明文大小 8 个字节
  • 密文大小 8 个字节
  • 密钥大小也是 8 个字节

我们所需要求解的就是密钥。

可以看到这里主要有两种基本操作

  • swap
def swap(b):
    l = BLOCK_SIZE // 2
    m = (1 << l) - 1
    return (b >> l) | ((b & m) << l)

将给定的数据的高 32 位与低 32 位交换。

  • transduce
T = [
    ((2, 1), 1),
    ((5, 0), 0),
    ((3, 4), 0),
    ((1, 5), 1),
    ((0, 3), 1),
    ((4, 2), 0),
]
def transduce(b, s=0):
    if len(b) == 0:
        return b
    d, t = T[s]
    b0, bp = b[0], b[1:]
    return [b0 ^ t] + transduce(bp, s=d[b0])

其中,

  • b 是一个 01 数组,初始时刻大小为 64。
  • s 是一个下标。

基本流程如下

  1. 根据 s 选择使用 T 的哪个元素,进而将其分为 d 和 t。
  2. 将 b 分为两部分,一部分只包含头元素,另一部分包含其它的元素。
  3. 将头元素与 t 异或作为当前的头元素,然后继续转换剩下的部分。

其实我们可以将该函数转换为迭代函数

def transduce_iter(b, s=0):
    ans = []
    for c in b:
        d, t = T[s]
        ans += [c ^ t]
        s = d[c]
    return ans

进而由于每次处理的是列表的第一个元素,其实该函数是可逆的,如下

def invtransduce(b, s=0):
    if len(b) == 0:
        return b
    d, t = T[s]
    b0, bp = b[0], b[1:]
    return [b0 ^ t] + transduce(bp, s=d[b0 ^ t])

下面分析程序的核心流程,首先是生成密钥部分,该加密算法生成了 6 个密钥,每次生成的方法

  1. transduce 先前的密钥得到中间值 t
  2. 对 t 进行 swap
  3. 连续迭代 5 次
    def __init__(self, k):
        self.k = [k]
        for i in range(1, len(T)):
            k = swap(transduceblock(k))
            self.k.append(k)

加密算法如下,一共迭代 6 轮,基本流程

  1. 异或密钥 transduce
  2. 交换
    def encrypt(self, b):
        for i in range(len(T)):
            b ^= self.k[i]
            b = transduceblock(b)
            b = swap(b)
        return b

通过分析程序,可知该加密算法是一个块加密,基本信息如下

  • 块大小为 8 个字节
  • 轮数为 6 轮
  • 加密算法的每轮的基本操作为 transduce 和 swap。
  • 密钥的扩展也是与 transduce 和 swap 相关。

更具体的

  1. swap 是将 8 字节的高 32 位与低 32 位进行调换。
  2. transduce 是对于 8 字节的每个比特,逐比特与某个值进行异或。这个值与 T 有关。

通过进一步地分析,我们可以发现这两个函数都是可逆的。也就是说,如果我们知道了最后的密文,那么我们其实可以将原来的轮数缩短为差不多 5 轮,因为最后一轮的 transduceswap 没有作用了。

我们可以定义如下变量

名字 含义
$k_{i,0}$ 第 i 轮使用的密钥的高 32 位
$k_{i,1}$ 第 i 轮使用的密钥的低 32 位
$d_{i,0}$ 第 i 轮使用的输入的高 32 位
$d_{i,1}$ 第 i 轮使用的输入的低 32 位

由于其中有一个核心操作是 swap,只会操纵高或低 32 位,所以我们可以分为两部分考虑。简化定义如下

  • Transduce 简化为 T,这里虽然与源代码里冲突,不过我们可以暂时理解一下。
  • Swap 简化为 S。

则每一轮的明密文,密钥如下

轮数 左侧密钥 左侧密文 右侧密钥 右侧密文
0 $k_{0,0}$ $d_{1,0}=T(k_{0,1} \oplus d_{0,1} ,s)$ $k_{0,1}$ $d_{1,1}=T(k_{0,0} \oplus d_{0,0})$
1 $k_{1,0}=T(k_{0,1},s)$ $d_{2,0}=T(k_{1,1} \oplus d_{1,1} ,s)$ $k_{1,1}=T(k_{0,0})$ $d_{2,1}=T(k_{1,0} \oplus d_{1,0})$
2 $k_{2,0}=T(k_{1,1},s)$ $d_{3,0}=T(k_{2,1} \oplus d_{2,1} ,s)$ $k_{2,1}=T(k_{1,0})$ $d_{3,1}=T(k_{2,0} \oplus d_{2,0})$
3 $k_{3,0}=T(k_{2,1},s)$ $d_{4,0}=T(k_{3,1} \oplus d_{3,1} ,s)$ $k_{3,1}=T(k_{2,0})$ $d_{4,1}=T(k_{3,0} \oplus d_{3,0})$
4 $k_{4,0}=T(k_{3,1},s)$ $d_{5,0}=T(k_{4,1} \oplus d_{4,1} ,s)$ $k_{4,1}=T(k_{3,0})$ $d_{5,1}=T(k_{4,0} \oplus d_{4,0})$
5 $k_{5,0}=T(k_{4,1},s)$ $d_{6,0}=T(k_{5,1} \oplus d_{5,1} ,s)$ $k_{5,1}=T(k_{4,0})$ $d_{6,1}=T(k_{5,0} \oplus d_{5,0})$

那么,我们可以逐比特位枚举 k 的高 32 位,同时枚举在进行 T 操作时的可能的 s 状态位,这样就可以获取高 32 位密钥。在进行逐位爆破之后,我们可以从获取两个可能结果

[2659900894, 2659900895]

再根据左边的结果,可以去获取右边可能的结果,利用 2659900894 获取的可能的结果如下

# 第一组明密文对对应的密钥可能太多。
# 第二组一共 6 个。
[2764038144, 2764038145, 2764038152, 2764038153, 2764038154, 2764038155]
# 第三组
[2764038144, 2764038145]

然后其实我们就可以手工试一下加密所有的明密文,如果不对,就直接判断错误即可了。这样其实可以很快可以过滤。最后可以发现密钥是

2659900894|2764038145

也就是11424187353095200769。也就拿到了 flag。

当然,本题目也可以使用中间相遇的攻击方法,也就是说分别枚举第 0 轮使用的密钥和最后一轮使用的密钥使其在第三轮相遇产生碰撞。

证书格式

PEM

PEM 以 -----BEGIN 开头,以 -----END 结尾,中间包含 ASN.1 格式的数据。ASN.1 是经过 base64 转码的二进制数据。Wikipedia 上有完整 PEM 文件的例子。

用 Python 3 和 PyCryptodome 库可以与 PEM 文件交互并提取相关数据。例如我们想提取出模数 n

#!/usr/bin/env python3
from Crypto.PublicKey import RSA

with open("certificate.pem","r") as f:
    key = RSA.import_key(f.read())
    print(key.n)

DER

DER 是 ASN.1 类型的二进制编码。后缀 .cer.crt 的证书通常包含 DER 格式的数据,但 Windows 也可能会接受 PEM 格式的数据。

我们可以用 openssl 将 PEM 文件转化为 DER 文件:

openssl x509 -inform DER -in certificate.der > certificate.pem

现在问题被简化成了如何读取 PEM 文件,所以我们可以重复使用上一小节中的 Python 代码。

其他格式转换

openssl x509 -outform der -in certificate.pem -out certificate.der
openssl x509 -inform der -in certificate.cer -out certificate.pem

文章作者: 杰克成
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杰克成 !
评论
  目录