当对称的每一个部分都是线性的时候,其实可以用线性代数的方法去解
@V 在他的文章里已经总结了三种方法了,那我在这里就在补充一种
PS:同时感谢@石卡酷提供的灵感
题目:CRYPTO01.zip
审题,直接看encrypt
函数,就是执行rounds = 14
轮的
Mi+1=P−1(S(P(Mi))⊕K)
其中Mi是第i轮的明文,K是密钥
接下来主要做的是分析P和S这两个操作
P操作(Permutation)·
P操作就是置换(Permutation)操作,即只改变传入数字的二进制比特的位置,不会把0比特变成1比特或把1变成0
位置是怎么改变的由元组P_permutation
决定,令pi为P_permutation
的第i个元素,令mi为消息M的第i个比特的话,这里的P操作就是
P(M):=mi→mpi
这个操作肯定是线性的,要用线性代数的方式实现这个操作,只需要实现一个置换矩阵即可
举个栗子(注:这也是Wiki上的例子),假设P_permutation
是
π=(2,1,3,0)
根据置换矩阵的性质,其第i行上只有第πi列为1,所以得到的矩阵就是
MP=⎣⎡0001010010000010⎦⎤
简单做个验算,可以得到
⎣⎡0001010010000010⎦⎤⎣⎡0123⎦⎤=⎣⎡2130⎦⎤=π
对题目中的P_permutation
构造置换矩阵也是同样的方法,只不过矩阵更大而已
那么现在的P操作就是(令vM为明文M的二进制向量表达)
P(vM)=Mp⋅vM
然后P−1就是P的逆操作,这个直接求逆矩阵即可
P−1(vM)=Mp−1⋅vM
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| flag = b'flag{199999999999997}' message = pad(flag) message = [Integer(bytes_to_long(message[i:i+8])) for i in range(0, len(message), 8)]
n = len(P_permutation) def n2v(m): return vector(m.bits() + [0] * (n - m.nbits()))
def v2n(v): return Integer(''.join([str(vi) for vi in v[::-1]]), 2)
m = message[0] MP = matrix(GF(2), n) for i in range(n): MP[i, P_permutation[i]] = 1 MPinv = MP ** (-1)
assert n2v(P(m)) == MP * n2v(m) assert n2v(P_inv(m)) == MPinv * n2v(m)
|
S操作(Substitution)·
S操作就是替换(Substitution)操作,理论上S操作不应该是线性操作,但是稍微分析一下
令mi为消息M的第i个比特的话,这里的S操作就是
S(M):=mi→mi ⊕ [(mi−IV & MASKi) ⊕ (mi−IV ∣ MASKi)]
看起来有点复杂,先对中括号里面的化简
理论上需要一堆离散数学的运算进行化简,但因为这里只有mi−IV和MASKi两个变量,所以可以偷懒打个真值表
mi−IV0011MASKi0101(mi−IV & MASKi) ⊕ (mi−IV ∣ MASKi)0⊕0=00⊕1=10⊕1=11⊕0=0
相同为0不同为1,这就是一个异或操作,所以S操作可以化简为三个东西的异或
S(M):=mi→mi ⊕ mi−IV ⊕ MASKi
这里已经可以看出其实S操作是线性的了,不妨把异或操作转换为模2的加法操作,就更明显了
S(M):=mi→mi+mi−IV+ MASKi(mod2)
到这里还有一个问题,就是i−IV可能会小于0,所以还要做个分类讨论,i−IV小于0的时候根据左移补0的性质,要设mi−IV=0
S(M):=mi→{mi+mi−IV+ MASKi(mod2)mi+ MASKi(mod2),i≥IV,i<IV
这里直接转换为矩阵还真不好转,所以我的做法是把mi+mi−IV做成一个MS矩阵,然后把后面的MASK
做成向量vMASK,以
S(vM)≡MS⋅vM+vMASK(mod2)
的方式进行运算
MS矩阵的构造也不难,根据公式就是把mi和mi−IV加起来,所以如果i≥IV的话把第i行的第i和i−IV列设为1;如果i<IV的话把第i行设为1即可(这个就不验算了)
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| flag = b'flag{199999999999997}' message = pad(flag) message = [Integer(bytes_to_long(message[i:i+8])) for i in range(0, len(message), 8)]
n = len(P_permutation) def n2v(m): return vector(m.bits() + [0] * (n - m.nbits()))
def v2n(v): return Integer(''.join([str(vi) for vi in v[::-1]]), 2)
m = message[0] MS = matrix(GF(2), n) for i in range(n): MS[i, i] = 1 if i >= 7: MS[i, i-7] = 1
vmask = n2v(Integer(MASK)) assert n2v(S(m)) == MS * n2v(m) + vmask
|
加密操作·
到这里就可以重写一下加密操作了
已知加密操作为rounds = 14
轮的
Mi+1=P−1(S(P(Mi))⊕K)
而且根据上面分析可知
P(vM)P−1(vM)S(vM)=Mp⋅vM=Mp−1⋅vM≡MS⋅vM+vMASK(mod2)
所以重写一下就是
vMi+1=P−1(S(Mp⋅vMi)⊕K)≡P−1((MS⋅Mp⋅vMi+vMASK)⊕K)(mod2)≡P−1(MSMp⋅vMi+vMASK+vK)(mod2)≡Mp−1⋅(MSMp⋅vMi+vMASK+vK)(mod2)≡Mp−1MSMp⋅vMi+Mp−1⋅(vMASK+vK)(mod2)
不妨令
Ab=Mp−1MSMp=Mp−1(vMASK+vK)
的话,就是
vMi+1≡A⋅vMi+b(mod2)
重复k次
vMi+k≡Ak⋅vMi+(j=0∑k−1Aj)b(mod2)
于是得到加密操作为
vC=Enc(vM)≡A14⋅vM+(j=0∑14−1Aj)b(mod2)
不妨令d=(∑j=014−1Aj)b,可以进一步化简为
vC≡A14⋅vM+d(mod2)
解密操作·
最后就是解密了
再看一遍加密操作,加密是分组的,每一组是8个字节,而比赛中的flag头是wdflag{
,就可以知道7个字节,那么只要枚举剩下的一个字节,就可以知道一组明密文对
也就是现在我知道vM和vC,只需要计算
d≡vC−(A14⋅vM)(mod2)
就可以得到一个等效的密钥d,后续给定任意的密文组C′,只需要计算
vM′≡(A14)−1⋅(vC′−d)(mod2)
即可解密
另外,因为加密时有对消息进行pad
,所以只需要通过对最后一组解密判断pad
是否正确,即可知道解出的等效密钥d是否正确
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| from sage.all import * from Crypto.Util.number import * from hashlib import md5 import string
block_size = 64 rounds = 14
MASK = 0b1110001001111001000110010000100010101111101100101110100001001001 IV = 7
b2i = lambda x: Integer(sum([x[i] * 2**i for i in range(len(x))]))
def pad(x): padlen = block_size - len(x) % block_size return x + bytes([padlen] * padlen)
def P(x): bit_x = x.bits() if len(bit_x) < block_size: bit_x.extend([0] * (block_size - len(bit_x))) bit_x = [bit_x[P_permutation[i]] for i in range(block_size)] return b2i(bit_x)
def P_inv(x): bit_x = x.bits() if len(bit_x) < block_size: bit_x.extend([0] * (block_size - len(bit_x))) bit_x = [bit_x[inverse_P_permutation[i]] for i in range(block_size)] return b2i(bit_x)
def S(x): x1 = x x2 = x << IV & MASK x3 = x << IV & ((1 << block_size) - 1) | MASK return x1 ^ x2 ^ x3
def encrypt(message_block, key): ret = message_block for i in range(rounds): print(ret) ret = P_inv(S(P(ret)) ^ key) return ret
ciphertext = [15556852711617030977, 17425085605625351983, 2506358089440944923, 1278751751083902772, 13156242808241775459, 2735122367365725460, 6194195534313299228, 6194195534313299228] c = [Integer(ci) for ci in ciphertext] P_permutation = [51, 44, 35, 24, 50, 59, 31, 58, 57, 7, 23, 10, 47, 6, 60, 18, 37, 56, 1, 36, 55, 14, 49, 0, 28, 40, 45, 25, 29, 19, 15, 26, 17, 54, 2, 8, 48, 42, 11, 32, 3, 61, 30, 22, 52, 12, 62, 9, 33, 4, 27, 53, 34, 20, 43, 41, 13, 39, 5, 46, 16, 21, 38, 63] inverse_P_permutation = [P_permutation[i] for i in range(block_size)]
n = len(P_permutation) def n2v(m): return vector(m.bits() + [0] * (n - m.nbits()))
def v2n(v): return Integer(''.join([str(vi) for vi in v[::-1]]), 2)
MP = matrix(GF(2), n) for i in range(n): MP[i, P_permutation[i]] = 1 MPinv = MP ** (-1)
MS = matrix(GF(2), n) for i in range(n): MS[i, i] = 1 if i >= 7: MS[i, i-7] = 1 vmask = n2v(Integer(MASK))
A = MPinv * MS * MP A14 = A ** rounds A14inv = A14 ** (-1)
def dec(d, c): vm = A14inv * (n2v(c) - d) return long_to_bytes(v2n(vm))
vc0 = n2v(c[0]) for s in string.printable: m0 = 'wdflag{%s' % s m0 = Integer(bytes_to_long(m0.encode())) vm0 = n2v(m0)
d = vc0 - A14 * vm0 tmp = dec(d, c[-1]) if tmp[-1] == tmp[-2]: print(s) break
m = b''.join([dec(d, ci) for ci in c]) print(m)
|
另外,能否直接从d中算出一个更准确的密钥呢
当时我试了一下,是不行的
因为
d≡(j=0∑14−1Aj)b(mod2)
如果要知道vK的话至少需要知道b,所以要求∑j=014−1Aj的逆
但实际上这个矩阵是不可逆的,也就是说密钥K会存在多解
虽然可以使用solve_right
解出一组特解,但这个特解和真实的密钥也会不一样