CryptoHack学习记录(二)
对称密码学
对称密钥密码是使用相同密钥对数据进行加密和解密的算法。目标是使用短密钥安全有效地发送长消息。
最著名的对称密钥密码是2001年标准化的AES。它如此广泛,以至于现代处理器甚至包含执行AES操作的特殊指令集。这里的第一部分将指导了解AES的内部工作原理,展示其独立组件如何协同工作,使其成为安全密码。到最后构建自己的AES解密代码。
我们可以将对称密钥密码分为两种类型,块密码和流密码。块密码将明文分解为固定长度的块,并通过加密函数将每个块与密钥一起发送。流密码同时通过将伪随机密钥流与数据进行异或运算,一次加密一个字节的明文。AES是一种分组密码,但可以使用CTR等操作模式转换为流密码。
块密码只指定如何加密和解密单个块,并且必须使用一种操作模式将密码应用于较长的消息。这是现实世界中实现经常失败的地方,因为开发人员不理解使用特定模式的微妙含义。剩下的挑战是如何攻击各种模式的常见误用。
Keyed Permutations
AES和所有分组密码一样,执行“密钥置换”。这意味着它将每个可能的输入块映射到唯一的输出块,并使用一个密钥来确定要执行的置换。
“块”只是指固定数量的位或字节,可以表示任何类型的数据。AES处理一个块并输出另一个块。我们将具体讨论AES的变体,它适用于128位(16字节)块和128位密钥,称为AES-128。
使用相同的密钥,可以反向执行排列,将输出块映射回原始输入块。重要的是,输入和输出块之间有一对一的对应关系,否则我们将无法依靠密文将其解密回最初的明文。
问:一对一对应的数学术语是什么?(crypto{术语})
试了function和reflection都不对,又试了个双射(bijection),这次成功了,想了想确实双射更符合这个意思。
Resisting Bruteforce
如果分组密码是安全的,攻击者应该无法将AES的输出与随机的比特排列区分开来。此外,没有比简单地对每个可能的密钥进行粗暴处理更好的方法来撤销排列。这就是为什么学者们认为密码理论上是“坏的”,如果他们能够找到一种比暴力破解密钥所需的步骤更少的攻击,即使这种攻击实际上是不可行的。
暴力攻击128位密钥空间有多难?有人估计,如果你将整个比特币挖矿网络的力量转向破解AES-128密钥,那么破解密钥的时间将是宇宙年龄的一百倍。
事实证明,有一种对AES的攻击比暴力攻击更好,但只是轻微的。它将AES-128的安全级别降低到126.1位,并且已经8年多没有改进。鉴于128位提供了巨大的“安全余量”,尽管进行了广泛的研究,但仍缺乏改进,因此不认为这对AES的安全性有可信的风险。但是的,在非常狭义的意义上,它“打破”了AES。
最后,虽然量子计算机有可能通过Shor算法彻底破解RSA等流行的公钥密码系统,但据认为,通过Grover算法,量子计算机只会将对称密码系统的安全级别降低一半。这就是为什么人们建议使用AES-256的原因之一,尽管它的性能较差,因为在量子未来,它仍然可以提供非常充分的128位安全性。
问:针对AES的最佳单密钥攻击的名称是什么?(crypto{术语})
打开“有一种对AES的攻击比暴力攻击更好”对应的超链接,看到这种攻击方式叫Biclique attack。
Structure of AES
为了实现不可能在没有密钥的情况下反转的密钥置换,AES在输入上应用大量点对点混合操作。这与RSA这样的公钥密码系统形成鲜明对比,后者基于优雅的个人数学问题。AES虽然不那么优雅,但速度非常快。
在高级别上,AES-128以“密钥计划表”开始,然后在同一个状态上运行10轮。起始状态只是我们要加密的明文块,表示为4x4字节矩阵。在10个循环的过程中,状态被多次可逆变换。
根据克劳德·香农(Claude Shannon)在20世纪40年代建立的安全密码的理论性质,每个转换步骤都有一个明确的目的。我们将在下面的挑战中详细了解其中的每一项。
以下是AES加密阶段的概述:
1.KeyExpansion:
从128位密钥导出11个单独的128位“round keys”:每个AddRoundKey步骤中使用一个。
2.Initial key addition(初始密钥添加):
AddRoundKey:第一轮密钥的字节与矩阵进行⊕操作。
3.循环。此阶段循环10次,共9个主循环加上一个“最后循环”:
①SubBytes-根据查找表(“S-box”),矩阵的每个字节被替换为不同的字节。
②ShiftRows-矩阵的最后三行在一列或两列或三列上进行转置。
③MixColumns-对矩阵的列进行线性变换,合并每列中的四个字节。这在最后一轮被跳过。
④AddRoundKey-当前循环密钥的字节与矩阵进行⊕操作。
其中包含一个bytes2matrix
函数,用于将初始明文块转换为状态矩阵。编写一个matrix2bytes
函数,将该矩阵转换回字节,并将生成的明文作为flag提交:
1 | def bytes2matrix(text): |
编写函数遍历转化即可:
1 | def matrix2bytes(matrix): |
Round Keys
现在我们将跳过KeyExpansion阶段的更精细的细节。重点是它接受了我们的16字节密钥,并从初始密钥中生成了11个4x4矩阵,称为“轮密钥”。这些轮密钥允许AES从我们提供的单个密钥中获得额外的里程。
接下来的初始密钥添加阶段只有一个AddRoundKey步骤。AddRoundKey步骤很简单:它将当前状态与当前轮密钥进行异或运算,比如对矩阵$a_{2,2}$的数值进行异或:
AddRoundKey也作为每一轮的最后一步出现。AddRoundKey使AES成为“键控排列”而不仅仅是排列。这是AES中唯一将密钥混合到状态中的部分,但对于确定发生的排列至关重要。
正如在前面的挑战中所看到的,如果知道密钥,XOR是一个容易反转的操作,但如果不知道,则很难反转。
完成add_round_key
函数,然后使用matrix2bytes
函数获取下一个flag:
1 | state = [ |
根据XOR运算和上一节学到的定义矩阵转字节:
1 | state = [ |
Confusion through Substitution
每个AES循环的第一步是SubBytes。这涉及获取状态矩阵的每个字节,并将其替换为预设16x16查找表中的不同字节。查找表被称为“Substitution box”或简称“S-box”。
1945年,美国数学家克劳德·香农发表了一篇关于信息论的开创性论文。它认为“混淆”是安全密码的一个基本属性。“混淆”意味着密文和密钥之间的关系应该尽可能复杂。只要给出一个密文,就应该没有办法了解有关密钥的任何信息。
如果密码的混淆性很差,则可以将密文、密钥和明文之间的关系表示为线性函数。例如,在凯撒密码中,密文=明文+密钥。这是一个明显的关系,很容易逆转。更复杂的线性变换可以使用Gaussian elimination等技术来解决。即使是低阶多项式,例如方程$x^4+51x^3+x$,也可以使用代数方法有效地求解。然而,多项式的阶数越高,通常就越难求解——只能用越来越多的线性函数来逼近。
S-box的主要目的是以抵抗线性函数逼近的方式变换输入。S-box的目标是高度非线性,虽然AES的不是完美的,但它非常接近。S盒中的快速查找是对输入字节执行非线性函数的捷径。该函数涉及在Galois域$2^8$中取模逆,然后应用复杂的仿射变换。表达函数的最简单方法是通过以下高次多项式:
为了制作S-box,已经对0x00到0xff的所有输入值和输入到查找表中的输出值计算了函数。实现sub_bytes
,通过逆S-box发送状态矩阵,然后将其转换为字节以获得flag:
1 | s_box = ( |
Diffusion through Permutation
我们已经看到了S-box替换是如何产生的。香农描述的另一个关键性质是“扩散”。这涉及到密码输入的每个部分应该如何扩展到输出的每个部分。
替代本身会产生非线性,但它不会将其分布到整个矩阵。如果没有扩散,在同一位置的同一字节将得到每一轮应用于它的相同变换。这将允许密码分析师分别攻击状态矩阵中的每个字节位置。我们需要通过加扰状态(以可逆的方式)来替换替换,以便应用于一个字节的替换影响状态中的所有其他字节。进入下一个S盒的每一个输入都会变成多个字节的函数,这意味着系统的代数复杂性会随着每一轮的增加而大大增加。
理想的扩散量会导致明文中一个比特的变化,从而导致密文中统计上一半比特的变化。这种理想的结果被称为雪崩效应。
ShiftRows和MixColumns步骤结合起来实现了这一点。它们协同工作,确保每个字节在两个回合内影响状态中的其他每个字节。
ShiftRows是AES中最简单的转换。它使状态矩阵的第一行保持不变。第二行向左移动一列,环绕。第三行移动两列,第四行移动三列。维基百科说得好:“这一步的重要性在于避免列被独立加密,在这种情况下,AES会退化为四个独立的分组密码。”
MixColumns更复杂。它在Rijndael的Galois域中执行状态矩阵列和预设矩阵列之间的矩阵乘法。因此,每列的每一个字节都会影响结果列的所有字节。
本题提供了执行MixColumns和ShiftRows操作的代码。在实现inv_shift_rows
之后,获取矩阵,在其上运行inv_mix_columns
,然后inv_shift-rows
,转换为字节,获得flag。
1 | def shift_rows(s): |
Bringing It All Together
除了KeyExpansion阶段之外,我们还概述了AES的所有组件。我们已经展示了SubBytes如何提供混淆,ShiftRows和MixColumns如何提供扩散,以及这两个属性如何协同工作,在状态上重复循环非线性转换。最后,AddRoundKey将密钥播种到这个替换置换网络中。
解密涉及反向执行“AES结构”挑战中描述的步骤,应用反向操作。请注意,KeyExpansion仍然需要首先运行,而轮密钥将以相反的顺序使用。AddRoundKey及其逆函数与XOR具有自逆特性相同。
这里提供了密钥扩展码,以及由AES-128正确加密的密文。复制到目前为止已编码的所有构建块,并完成实现图中所示步骤的解密函数。解密的明文是flag:
1 | N_ROUNDS = 10#十轮循环 |
Modes of Operation Starter
前一组挑战展示了AES如何对数据块执行密钥排列。实际上,我们需要对消息进行比单个块更长的加密。操作模式描述了如何在较长的消息上使用AES之类的密码。
如果使用不当,所有模式都有严重的弱点。这一类别的挑战将与API交互并利用这些弱点,来获取flag。
所提供的程序:
1 | from Crypto.Cipher import AES |
首先使用encrypted_flag()
获取程序加密的AES密文:
1 | {"ciphertext":"8ca64f9c03ef853d2fb33add832335cf941f5098477acf878da62721e465b5a1"} |
然后再用decrypt(ciphertext)
获得密文解密出的明文:
1 | {"plaintext":"63727970746f7b626c30636b5f633170683372355f3472335f663435375f217d"} |
对其进行转字节即可(当然,API中也有辅助转字节的程序):
1 | hex='63727970746f7b626c30636b5f633170683372355f3472335f663435375f217d' |
此页面为您提供了一种与挑战功能交互的便捷方式。如果需要,还可以使用GET的方式直接从routes/endpoints发送和接收数据。
看到大佬的方法使用python的requests库直接编写程序进行交互,有点厉害,学习一下:
1 | import requests |
稍微学习一下requests库:
我们首先知道这道题目的url,作为BASE_URL,等下要对其进行get请求。
题目中已经告诉了我们这题使用get请求的方法:
@chal.route('/block_cipher_starter/decrypt/<ciphertext>/')
@chal.route('/block_cipher_starter/encrypt_flag/')
requests.get(url)
可以返回一个response响应对象,以这道题为例:
1 | import requests |
根据题目所给代码形式,我们可以知道其返回值为json格式,我们可以直接获取内容:
1 | import requests |
我们在第六行使用了get请求使用了encrypt_flag
模块,并得到了返回值,变量data为获取的字典信息,用变量ciphertext得到所得到的密文。
用相同的方法,我们也可以使用get请求得到解密的文本(注意get请求格式),然后解码就能得到flag。
Passwords as Keys
对称密钥算法中的密钥必须是随机字节,而不是密码或其他可预测数据。应使用加密安全伪随机数生成器(CSPRNG)生成随机字节。如果密钥以任何方式都是可预测的,那么密码的安全级别就会降低,并且攻击者可以访问密文对其进行解密。
仅仅因为密钥看起来像是由随机字节组成的,并不意味着它一定是这样的。在这种情况下,密钥是使用哈希函数从简单的密码中派生出来的,这使得密文可以破解。
提供的程序:
1 | from Crypto.Cipher import AES |
在代码提供的网站链接里有随机密钥的词库,全部复制下来到文本里,我们可以遍历逐个进行AES解密,以含有flag头作为目标。
现学了一下AES库的使用,要求解密的密钥和密文都为字节形式即可:
1 | from Crypto.Cipher import AES |
ECB Oracle
AES_ECB是最简单的模式,每个明文块完全独立加密。在这种情况下,输入被添加到secret flag并被加密。我们不提供解密功能。
提供代码:
1 | from Crypto.Cipher import AES |
被这题卡了很久……
了解一下ECB加密的方式:
先观察一下代码,ECB模式将自己输入的和plaintext和flag组合起来,变成”\
由于ECB的分块加密特点,代码中分组为16一组,所以我们输入相同长度的不同明文,得到的后半段密文是不受影响的,因为它和前面我们输入的部分不是同一个加密块。所以我们可以布设一个“陷阱”,来骗到flag。
想想如果我们分别输入以下内容:
1 | AAAAAAAAAAAAAAAA crypto{………… |
这题里面flag头由于我们是知道的,先假设我们先不知道flag头第一位为c,我们先输入16个填充字符A,那么这16个A分在第一块,flag在后面的块;但是我们如果只输入了15个A作为填充字符,那么就会把第二块的首位提到第一个块里进行加密,这样我们就获得了来自flag的首字母和我们的填充字符组合的密文。
我们可以遍历数字字母和一些符号字符,组合成这样的填充块:
1 | 'AAAAAAAAAAAAAAA' + i |
将这个填充块发送,获得密文,与之前获得的含有flag首字母的密文进行比较,如果相等,不就代表了我们遍历到了flag的首字母吗。
进而减少一个填充字母A,末尾加上c,然后根据一开始的逻辑开始寻找第二个字母:
1 | AAAAAAAAAAAAAAAc crypto{………… |
这样我们又获得了含有第二个字母r的密文,接着遍历,比较返回值又可以得到第二个字母,一遍一遍循环,最终就能复原flag。
注意我们一开始并不知道flag的具体长度,简单思考一下,由于填充字符需要大于flag的长度我们才能得到完整的flag,所以我们可以估计一下flag长度,设置一个合理的填充字符初始值。
以上的理论都已具备,实践开始:
1 | import requests |
注意这里需要设置一个缓冲时间,避免因为过于频繁发送get请求出错,总共跑下来需要一段时间,等就行了。
ECB CBC WTF
在这里,您可以在CBC中加密,但只能在ECB中解密。这不应该是缺点,因为它们是不同的模式。
题目所给代码:
1 | from Crypto.Cipher import AES |
这里我们需要了解一下CBC加密的加密流程,比较一下ECB模式:
我们先获得密文:
1 | {"ciphertext":"c8c4aecc754d37c54d3e1c263db6bf79f87422fca703f4bb72ae314a7ced8e2fa5d8a53eeb2e575a7b8e3ed790f2c306"} |
然后再得到相应的明文:
1 | {"plaintext":"e0cb2f6b2c06ebff12dfa02a837798eaabb6d7bc01224cf62e5c431348d5d44ca74054cc9667ab8a45f1106b5dccaf52"} |
密文和明文都是96位16进制,也就是48字节,由于iv为16位,所以块密码分组16一组,我们可以分成三组,这里注意到:ciphertext = iv.hex() + encrypted.hex()
,这里我们得到的“密文”是由第一组iv和后面两组密文得到。
我们可以发现,CBC模式除了进行了一个额外的异或环节之外和ECB没有什么区别,我们使用解密函数得到的明文是在没有异或的基础上得到的,所以我们需要进行一步异或。根据上面的算法,我们有:
把这两个式子逆回去就能得到明文:
得到flag:
1 | from Crypto.Util.number import * |
LAZY CBC
我只是一个懒惰的开发人员,希望我的CBC加密能够工作。这些关于初始化向量的讨论是什么?听起来不重要。
1 | from Crypto.Cipher import AES |
这题我们需要得到key,系统会返回flag,明文和密文都是我们自己设置的。z
Flipping Cookie
你可以为我的网站获取cookie,但它不会帮助你阅读flag……
1 | from Crypto.Cipher import AES |
这道题目中get_cookie函数返回包含“admin=False”的纯文本密文,密文的前16个字节是iv。在check_admin函数中,输入文本通过iv进行解密,如果解密文本包含字符串“admin=True”,则即可得到flag。
我们要做的就是改一下生成的cookie中的admin,使其为True返回flag。
但是如果我们只改变密文,iv不变,用这个程序的key是复原不出带有“admin=True”的明文的。
由于True和False由于一个字母之差,会改变块加密的块结构,我在测试中的时候返回了以下内容:
1 | {"error":"Padding is incorrect."} |
所以,综合看起来,我们不能改变密文,那么我们可以生成一个新的iv,使其和密文异或能异或出含有True的明文:$plain\Rightarrow^{E_{iv}}cipher\Rightarrow^{D{iv’}}fake$
从这个理论关系分析,我们能知道:$fake=D(cipher)\oplus iv’$
那么这个新iv该如何得到?不难发现其构成都为我们已知的量:
那么问题就简单多了:
1 | from Crypto.Util.number import * |
Symmetry
一些分组密码模式,如OFB、CTR或CFB,将分组密码转换为流密码。流密码背后的思想是产生一个伪随机密钥流,然后将其与明文进行异或运算。流密码的一个优点是它们可以处理任意长度的明文,而不需要填充。
OFB是一种晦涩难懂的密码模式,目前与使用CTR相比没有任何实际好处。
1 | from Crypto.Cipher import AES |
注意到OFB模式中对iv进行反复加密,每段不同的iv参与每块的异或运算,根据异或的运算性质,明文生成的密文也可以看做明文,再进行一次相同iv的异或运算又能得到密文(最初的明文):
题目生成iv和flag用OFB加密后的密文,所以利用这里的iv和密文我们可以用encrypt函数进行加密得到flag:
1 | import requests |
Bean Counter
我一直在努力让PyCrypto的计数器模式做我想做的事情,所以我自己把ECB模式变成了CTR。我的计数器可以向上和向下移动,以甩掉密码分析师!他们不可能读到我的照片。
1 | from Crypto.Cipher import AES |
根据加密程序和加密方式图我们可以知道,如果我们能得到和明文密文一起异或的值,就能复原png图片,但是png图片的开头16字节是相同的,我们可以根据这种方法和密文异或得到这个值:
1 | from pwn import xor |
这里需要注意python的异或符号^是在整数上的异或,如果异或结果前面带0会被忽略掉,为了保证png文件的完整性我们需要在字节上进行异或,这里用了pwn库的xor。
RSA
RSA Starter 1
RSA中的所有操作都涉及模幂运算。
模幂运算是密码学中广泛使用的一种运算,通常写为:$2^{10} \bmod 17$
可以将此视为将某个数提高到某个幂($2^{10}=1024$),然后将除法的余数除以其他数($1024 \bmod 17=4$)。
在RSA中,模幂运算与素因子分解问题一起帮助我们构建”trapdoor函数”。这是一个在一个方向上很容易计算的函数,但在相反的方向上很难计算,除非你有正确的信息。它允许我们对消息进行加密,只有拥有密钥的人才能执行反向操作来解密它。
计算$101^{17}\bmod 22663$
1 | print(pow(101,17,22663))#19906 |
RSA Starter 2
RSA加密是具有指数$e$和模量$n$的消息的模幂,通常是两个素数的乘积:$n=pq$。
指数和模数一起构成RSA公钥(N,e)。$e$最常见的值是0x10001或65537。
使用指数$e=65537$和素数$p=17$和$q=23$加密数字12。你得到的密文是多少?
1 | e=65537 |
RSA Starter 3
RSA依赖于模n的因子分解的难度。如果可以找到素数,那么我们可以计算n的欧拉函数,从而解密密文。
给定$n=pq$和两个素数:
1 | p = 857504083339712752489993810777 |
$n$的欧拉函数是多少?
由于$p$和$q$都是素数且互不相等,$phi=(p-1)(q-1)$
RSA Starter 4
私钥d用于解密用相应公钥创建的密文(它也用于对消息进行“签名”)。
私钥是一条秘密信息或“trapdoor”,它允许我们快速反转加密功能。如果RSA实现得很好,如果您没有私钥,解密密文的最快方法是首先对模数进行因子分解。
在RSA中,私钥是对指数$e$关于$n$的欧拉函数的模逆。
1 | p = 857504083339712752489993810777 |
求d:
1 | import gmpy2 |
RSA Starter 5
我使用上一题的公钥参数加密了一个秘密号码:
N=882564595536224140639625987659416029426239230804614613279163
e=65537
使用您在上一个挑战中为这些参数找到的私钥来解密此密文:
c=77578995801157823671636298847186723593814843845525223303932
1 | d=121832886702415731577073962957377780195510499965398469843281 |
RSA Starter 6
如何确保收到你信息的人知道是你写的?我们可以通过签署消息来防止发出的消息被攻击。
假设你写了一条消息$m$。你用你朋友的公钥来加密这条消息:$c=m^{e_0} \bmod n_0$
要签署此消息,需要计算消息的哈希值:$H(m)$,并使用您的私钥“加密”:$s=H(m)^{d_1} \bmod n_1$
在真正的密码系统中,最好使用单独的密钥来加密和签名消息。
你的朋友可以使用他们的私钥解密消息:$m=c^{d_0} \bmod n_0$。使用您的公钥,他们计算$S=s^{e_1} \bmod n_1$
现在,通过计算$H(m)$并将其与$S$进行比较:确保$H(m)=S$,他们可以确保你发送给他们的消息就是他们收到的消息。
下面使用你的私钥和SHA256哈希函数对crypto{Immut4ble_m3ssag1ng}进行签名,这里提供了n和d。
哈希函数的输出需要转换为一个可以用于RSA数学的数字.
1 | import hashlib |
或者:
1 | import hashlib |
或者:
1 | from Crypto.Hash import SHA256 |
Factoring
到目前为止,我们一直在使用小素数的乘积作为模数,但小素数对RSA没有多大好处,因为它们可以使用现代方法进行因子分解。
现在,建议使用长度至少为1024位的素数乘以两个这样的1024位素数,得到2048位的模数。具有2048位模数的RSA被称为RSA-2048。
有人说,要真正做到经得起未来考验,你应该使用RSA-4096甚至RSA-8192。然而,这里有一个折衷方案;生成大质数需要更长的时间,加上大模数的模幂运算可以预测地更慢。
将150位数字510143758735509025530880200653196460532653147分解为其两个组成素数,给出较小的答案。
用factordb,得到两个因子19704762736204164635843 5889363174021185185929
Inferius Prime
这是我的超强RSA实现,因为它有1600位,应该是牢不可破的……至少我这么认为!
1 | from Crypto.Util.number import getPrime, inverse, bytes_to_long, long_to_bytes, GCD |
研究了半天也没发现他这个RSA强壮在哪……
1 | from Crypto.Util.number import * |
Monoprime
为什么每个人都如此痴迷于将RSA的两个素数相乘。为什么不只用一个呢?
1 | n = 171731371218065444125482536302245915415603318380280392385291836472299752747934607246477508507827284075763910264995326010251268493630501989810855418416643352631102434317900028697993224868629935657273062472544675693365930943308086634291936846505861203914449338007760990051788980485462592823446469606824421932591 |
本题中,$n$只是个质数,没有因子:
1 | import gmpy2 |
好像不影响做题:
1 | import gmpy2 |
Square Eyes
得到2048位的素数需要很长时间,所以我只生成了一个并使用了两次。
如果你被卡住了,再看看欧拉函数。
1 | n = 535860808044009550029177135708168016201451343147313565371014459027743491739422885443084705720731409713775527993719682583669164873806842043288439828071789970694759080842162253955259590552283047728782812946845160334801782088068154453021936721710269050985805054692096738777321796153384024897615594493453068138341203673749514094546000253631902991617197847584519694152122765406982133526594928685232381934742152195861380221224370858128736975959176861651044370378539093990198336298572944512738570839396588590096813217791191895941380464803377602779240663133834952329316862399581950590588006371221334128215409197603236942597674756728212232134056562716399155080108881105952768189193728827484667349378091100068224404684701674782399200373192433062767622841264055426035349769018117299620554803902490432339600566432246795818167460916180647394169157647245603555692735630862148715428791242764799469896924753470539857080767170052783918273180304835318388177089674231640910337743789750979216202573226794240332797892868276309400253925932223895530714169648116569013581643192341931800785254715083294526325980247219218364118877864892068185905587410977152737936310734712276956663192182487672474651103240004173381041237906849437490609652395748868434296753449 |
那就直接开方就行了,这里求解欧拉函数:$\phi(n)=\phi(p^2)=p\phi(p) $
1 | from Crypto.Util.number import * |
Manyprime
使用一个素因子绝对是个坏主意,所以我将尝试使用30以上的因子。
1 | n = 580642391898843192929563856870897799650883152718761762932292482252152591279871421569162037190419036435041797739880389529593674485555792234900969402019055601781662044515999210032698275981631376651117318677368742867687180140048715627160641771118040372573575479330830092989800730105573700557717146251860588802509310534792310748898504394966263819959963273509119791037525504422606634640173277598774814099540555569257179715908642917355365791447508751401889724095964924513196281345665480688029639999472649549163147599540142367575413885729653166517595719991872223011969856259344396899748662101941230745601719730556631637 |
试了下sympy库直接求欧拉函数,不大行,用yafu分解出了因子,可以求解:
1 | import gmpy2 |
Salty
最小的指数应该是最快的,对吧?
1 | from Crypto.Util.number import getPrime, inverse, bytes_to_long, long_to_bytes |
根据ct长度就能知道m的1次方小于n,ct=m,直接转就行:
1 | from Crypto.Util.number import * |
Modulus Inutilis
我的素数现在应该足够大了!
1 | from Crypto.Util.number import getPrime, inverse, bytes_to_long, long_to_bytes |
e=3,ct很小,跟上题类似的原理:
1 | from Crypto.Util.number import * |
Everything is Big
1 | N = 0x8da7d2ec7bf9b322a539afb9962d4d2ebeb3e3d449d709b80a51dc680a14c87ffa863edfc7b5a2a542a0fa610febe2d967b58ae714c46a6eccb44cd5c90d1cf5e271224aa3367e5a13305f2744e2e56059b17bf520c95d521d34fdad3b0c12e7821a3169aa900c711e6923ca1a26c71fc5ac8a9ff8c878164e2434c724b68b508a030f86211c1307b6f90c0cd489a27fdc5e6190f6193447e0441a49edde165cf6074994ea260a21ea1fc7e2dfb038df437f02b9ddb7b5244a9620c8eca858865e83bab3413135e76a54ee718f4e431c29d3cb6e353a75d74f831bed2cc7bdce553f25b617b3bdd9ef901e249e43545c91b0cd8798b27804d61926e317a2b745 |
维纳攻击,直接上脚本:
1 | import gmpy2 |
Crossed Wires
我让我的朋友在发送给我之前加密我们的秘密flag,但他们没有使用我的密钥,而是使用了自己的密钥!你能帮忙吗?
1 | from Crypto.Util.number import getPrime, long_to_bytes, bytes_to_long, inverse |
根据$n,d$,有算法可以因式分解:
我们知道$ed\equiv 1 \bmod phi$
可以假设$k=ed-1$
我们从$[0,n]$之间随机取一个整数$g$,当$k\bmod 2=0$时,让$k$值缩小一半,$x=(g^k\bmod n)-1$,如果$x≠0$而且$x,n$的最大公因数大于$1$,这个最大公因数就为$p$,否则继续循环:
1 | def pq(n,e,d): |
注意到这个题里进行了多次加密,使用同一个$n$和不同的$e$,所以我们可以计算多个$d$,因式分解后我们可以求出$\phi(n)$,所以就能求出每次加密的$d$,进而求解。
注意这里加密次序和解密次序无关,我们解密的时候没有必要按照相反的顺序进行解密,因为有:
最终求得flag:
1 | from Crypto.Util.number import * |
Everything is Still Big
好吧,这一次我保护了自己。现在没有什么能阻止我和我的超级计算机!
1 | #!/usr/bin/env python3 |
还是用维纳攻击:
1 | import gmpy2 |
Endless Emails
题目给了一系列的cne数:
1 | #!/usr/bin/env python3 |
首先想到的就是用中国剩余定理,由于给的组数过多,直接写个数组爆破求解就行:
1 | from libnum import * |
Infinite Descent
找到大素数很慢,所以我设计了一个优化。
1 | import random |
随机生成$p,q$,如果不是质数就加一个小很多的数,直到出现质数为止,猜测因数差很小,尝试使用yafu,结果n位数太大运行不了,试了试sympy库,有了:
1 | import sympy |
当然,考虑pq很接近也可以考虑使用费马因式分解进行分解:
1 | from sympy.ntheory.primetest import is_square |
Marin’s Secrets
1 | #!/usr/bin/env python3 |
这题生成的指数在二进制下都为0b11111……形式,所以理论上可以爆破出pq求解RSA:
最终在2000-3000的区间找到了p和q:
1 | from Crypto.Util.number import * |
也可以直接判断是否为n的因子,这样更快一点:
1 | from Crypto.Util.number import * |
Signing Server
提供了nc:
1 | from Crypto.Util.number import bytes_to_long, long_to_bytes |
靶机提供了加密和解密的服务,直接把他传回来的secret再传回去解密就得到flag了:
1 | from pwn import * |
Diffie-Hellman
Diffie-Hellman Starter 1
模$n$的整数集合,加上加法和乘法运算,就是一个环。这意味着将集合中的任意两个元素相加或相乘将返回集合中的另一个元素。
当模是质数:$n=p$时,保证了集合中每个元素有逆元,因此环被提升为域。我们将该域称为有限域$F_p$。
Diffie-Hellman协议与一些有限域$F_p$的元素一起工作,其中素模通常是大素数。
给定素数$p=991$,元素$g=209$,求逆元素$d$,使得$gd \bmod 991=1$。
1 | import gmpy2 |
Diffie-Hellman Starter 2
有限域$F_p$的每个元素都可以用于在重复的乘法作用下生成子群$H$。换句话说,对于元素$g:H=\{g,g^2,g^3,…\}$
$F_p$的本原元素是其子群$H=F_p$的元素,即$F_p$的每个元素,可以被写为某个整数$n$的$g^n \bmod p$。因此,本原元素有时被称为有限域的生成元。
对于$p=28151$的有限域,找到最小元素$g$,它是$F_p$的本原元素。
这个问题可以用蛮力解决,但也有一些聪明的方法来加速计算。
一开始想的是将所有生成的写进数组,然后看看数组不重复元素的数量,得到了本原元素7:
1 | p=28151 |
然后看了wp,感觉wp优化的算法比较好一些:
我们可以计算$j^i$通过检查它生成的循环是否在大小上小于$p$,如果我们在p个元素之前检测到一个循环,$j$就不能成为$Fp$的本原元素,因为$j^1=j$,我们就可以以$j$自身作为判断基准,如果$j^n=j,n≠1$出现在$p$的中间,说明这个数的任意次方无法覆盖到所有的元素,生成的子群$H$大小小于有限域,也就不是本原元素,省去了一大部分的计算空间:
1 | p=28151 |
Diffie-Hellman Starter 3
使用Diffie-Hellman协议是因为离散对数被认为是精心选择的群的“困难”计算。
Diffie-Hellman的第一步是建立一个素数$p$和有限域$g$的一些本原元素。必须仔细选择这些本原元素,以避免使用有效算法求解离散对数的特殊情况。例如,一个安全素数$p=2q+1$通常是这样选取的,即$p-1$的唯一因子是$2,q$,其中$q$是其他一些大素数。这保护DH免受Pohlig–Hellman算法的影响。
然后,用户选择一个秘密整数$a<p$并计算$g^a \bmod p$。这可以在不安全的网络上传输,由于离散对数的假设困难,秘密整数应该无法计算。
计算:$g^a \bmod p$
1 | g: 2 |
1 | g=2 |
Diffie-Hellman Starter 4
现在是时候使用从朋友Alice收到的数据来计算共享秘密了。与之前一样,我们将使用NIST的参数:
NIST是一家美国标准机构,为密码学等提供标准。这里的参数是建议在DH中使用的参数。
1 | g: 2 |
您已从Alice收到以下整数:
1 | A: 70249943217595468278554541264975482909289174351516133994495821400710625291840101960595720462672604202133493023241393916394629829526272643847352371534839862030410331485087487331809285533195024369287293217083414424096866925845838641840923193480821332056735592483730921055532222505605661664236182285229504265881752580410194731633895345823963910901731715743835775619780738974844840425579683385344491015955892106904647602049559477279345982530488299847663103078045601 |
生成秘密整数b并计算公共整数B,然后发送给Alice:
1 | b: 12019233252903990344598522535774963020395770409445296724034378433497976840167805970589960962221948290951873387728102115996831454482299243226839490999713763440412177965861508773420532266484619126710566414914227560103715336696193210379850575047730388378348266180934946139100479831339835896583443691529372703954589071507717917136906770122077739814262298488662138085608736103418601750861698417340264213867753834679359191427098195887112064503104510489610448294420720 |
你和Alice现在可以计算共享的秘密,如果只知道$g,p,a,B$,这是不可行的。
你的共享秘密是什么?
一些计算原理:$x$为共享的秘密。
我们接收消息并解密:
Alice解密:
外界很容易知道$A,B,p,g$,但是如果不知道$a,b$的话无法解密。
1 | p=2410312426921032588552076022197566074856950548502459942654116941958108831682612228890093858261341614673227141477904012196503648957050582631942730706805009223062734745341073406696246014589361659774041027169249453200378729434170325843778659198143763193776859869524088940195577346119843545301547043747207749969763750084308926339295559968882457872412993810129130294592999947926365264059284647209730384947211681434464714438488520940127459844288859336526896320919633919 |
Diffie-Hellman Starter 5
Alice想向您发送她的秘密flag,并要求您与她生成共享秘密。她还告诉你她将使用NIST标准:
1 | g: 2 |
您从Alice收到以下整数:
1 | A: 112218739139542908880564359534373424013016249772931962692237907571990334483528877513809272625610512061159061737608547288558662879685086684299624481742865016924065000555267977830144740364467977206555914781236397216033805882207640219686011643468275165718132888489024688846101943642459655423609111976363316080620471928236879737944217503462265615774774318986375878440978819238346077908864116156831874695817477772477121232820827728424890845769152726027520772901423784 |
然后生成您的秘密整数并计算您的公共整数,然后将其发送给Alice。
1 | b: 197395083814907028991785772714920885908249341925650951555219049411298436217190605190824934787336279228785809783531814507661385111220639329358048196339626065676869119737979175531770768861808581110311903548567424039264485661330995221907803300824165469977099494284722831845653985392791480264712091293580274947132480402319812110462641143884577706335859190668240694680261160210609506891842793868297672619625924001403035676872189455767944077542198064499486164431451944 |
每个人单独使用共享密钥来导出AES私钥。这允许您通过通道加密大量数据,而无需再次交换密钥。
Alice向您发送以下初始化向量(IV,Initialization Vector,AES加密用的)和密文:
1 | {'iv': '737561146ff8194f45290f5766ed6aba', 'encrypted_flag': '39c99bf2f0c14678d6a5416faef954b5893c316fc3c48622ba1fd6a9fe85f3dc72a29c394cf4bc8aff6a7b21cae8e12c'} |
本题提供了辅助用的解密脚本,按照上一题的算法进行计算然后利用脚本就可以得到flag:
1 | from Crypto.Cipher import AES |
Parameter Injection
我们不仅可以拦截Alice和Bob的DH密钥交换,还可以重写他们的消息。想一想如何利用他们计算的DH方程,从而避免了破解任何离散对数问题的需要。
在恢复共享秘密后,使用“Diffie Hellman Starter 5”中的脚本解密flag。
这题提供了nc的IP和端口:
1 | nc socket.cryptohack.org 13371 |
我先去简单了解了一下这种交互方式:
根据之前学习的知识,本来,Bob会获得来自Alice的$p,g,A$,然后Bob操作完后发送Alice参数$B$,Alice会用B解密$x=B^a\bmod p=g^{ab}\bmod p$出$x$,然后和flag进行AES加密,继续返回给Bob加密后的密文和IV。
现在我们作为一个中间人,获得了来自Alice的$p,g,A$,要由我们发送给Bob来自Alice的消息,Bob再进行他的操作,然后发送给我们$B=g^b$,由我们发送给Alice,接着Alice会用B解密$x=B^a\bmod p=g^{ab}\bmod p$出$x$,然后和flag进行AES加密,返回密文和IV。
所以,我们可以在这其中做些手脚,做到能破解shared_secret值即可,更确切的说,能确定$x$。
这里,我们可以发现,由于是Alice对密文进行AES加密,我们可以在发送B的时候把B参量的赋值做修改,也就是想办法更改$x=B^a\bmod p$得到$x$,注意到如果我们修改为$B=p$,那么Alice计算$x=p^a\bmod p=0$,相当于我们通过篡改消息进而改变了其中的shared_secret值,而Alice就用这个$x=0$和flag进行了加密。
测试了一下,发送{“B”: “0x1”}也可以,这样shared_secret值为1。
经过一顿操作,我们获得了来自Alice加密后的密文和IV:
1 | nc socket.cryptohack.org 13371 |
此时得到的encrypted_flag是用shared_secret=0得到的,所以我们就不需要其他参量了,直接进行破解:
1 | from Crypto.Cipher import AES |
Export-grade
Alice和Bob正在使用遗留代码库,需要协商他们都支持的参数。你在谈判过程中处于中间,之后可以被动观察。这次你怎么会毁了他们的一天?
还是提供了ip和端口,连上之后得到:
1 | nc socket.cryptohack.org 13379 |
Alice让Bob选择代码库,按照我的想法一定是数越小越有利于我们计算,那就给他修改一下吧:
1 | Send to Bob: {"supported": ["DH64"]} |
我们得到了64位的几个参数,但是我们需要知道$x$,我们有$A=g^a\bmod p$,但是我们不知道$a$,我们需要求解一个一元的离散对数方程,不过数应该不算大,本人暂时水平有限,先交给网站帮忙解一下吧:
Discrete logarithm calculator离散对数计算器
我们得到了$a$,下面的一切都好说了:
1 | from Crypto.Cipher import AES |