LFSR Note2-一些代数攻击
Swizzer

对LFSR的一些代数打法

存在线性递推关系

单个LFSR的输出存在线性递推关系,当给出的输出足够长时,这一弱点可以被直接攻破。

序列递推式基础

对于一个序列 ,其minimal polynomial是唯一的一个首一多项式 ,满足:若 满足线性递推关系 (对所有 成立),则 整除多项式

写成初等数学的形式大概长这样:

若一个序列 满足:

其中 ,则记序列为其递推式, 称为该递推式的阶数。 数列 的最短递推式即为阶数最小的递推式。对最短递推式 称为的minimal polynomial。

当然,minimal polynomial不一定存在。如果存在,那么称其递推式的阶数/多项式本身的度为序列线性复杂度

Berlekamp-Massey算法

原理

虽然我一贯是调包侠的作风,不过还是稍微记录下算法的原理吧。以下文字摘抄自该blog

我们的问题是,在数列已知的情况下,如何求出其最短线性递推式

考虑增量法,假设我们已经求出了 的最短线性递推式 ,如何求出 的最短线性递推式。 定义 的最短线性递推式 为当前递推式,记递推式被更改的次数为 ,第 次更改后的递推式为 ,特别地,定义 为空,那么当前递推式应当为 。 记 ,其中 为当前递推式,显然若 ,那么当前递推式就是 的最短线性递推式。否则,我们认为 处出错了,定义 最早的出错位置,则有 。 若 ,这意味着 是序列中第一个非零元素,我们可以令 ,即用 个 0 填充线性递推式,此时由于不存在 使得 ,因此 显然为 的线性递推式,并且由于 是序列中第一个非零元素,不难证明 也是 的最短线性递推式。 否则,即 ,考虑 出错的位置 fail ,记 。我们希望得到数列 ,使得 对于任意 均成立,并且 。如果能够找到这样的数列 ,那么令 即可(其中+定义为各位分别相加)。 构造数列 如下: ,即填充 个零,然后将数列 的 mul 倍放在后面。容易验证其合法性,故令 即可。在最坏情况下,我们可能需要对数列进行 次修改,因此该算法的时间复杂度为


然后还有大佬的实数域上的算法code:

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
#include <bits/stdc++.h>
#define pii pair<int, int>
#define mp(x, y) make_pair(x, y)
#define pb push_back
#define eb emplace_back
#define fi first
#define se second
#define int long long
#define mem(x, v) memset(x, v, sizeof(x))
#define mcpy(x, y, n) memcpy(x, y, sizeof(int) * (n))
#define lob lower_bound
#define upb upper_bound
using namespace std;

inline int read() {
int x = 0, w = 1;char ch = getchar();
while (ch > '9' || ch < '0') { if (ch == '-')w = -1;ch = getchar(); }
while (ch >= '0' && ch <= '9') x = x * 10 + ch - '0', ch = getchar();
return x * w;
}

const int MN = 2e3 + 5;
const int Mod = 998244353;
const int inf = 1e9;
const double eps = 1e-8;

inline int qPow(int a, int b = Mod - 2, int ret = 1) {
while (b) {
if (b & 1) ret = ret * a % Mod;
a = a * a % Mod, b >>= 1;
}
return ret;
}

#define dbg

int N, c, fail[MN];
double val[MN], delta[MN];
vector <double> ans[MN];

signed main(void) {
N = read();
for (int i = 1; i <= N; i++)
scanf("%lf", &val[i]);
for (int i = 1; i <= N; i++) {
double tmp = val[i];
for (int j = 0; j < ans[c].size(); j++)
tmp -= ans[c][j] * val[i - j - 1];
delta[i] = tmp;
if (fabs(tmp) <= eps) continue;
fail[c] = i;
if (!c) {
ans[++c].resize(i);
continue;
}
double mul = delta[i] / delta[fail[c - 1]];
++c, ans[c].resize(i - fail[c - 2] - 1);
ans[c].pb(mul);
for (int j = 0; j < ans[c - 2].size(); j++)
ans[c].pb(ans[c - 2][j] * -mul);
if (ans[c].size() < ans[c - 1].size()) ans[c].resize(ans[c - 1].size());
for (int j = 0; j < ans[c - 1].size(); j++)
ans[c][j] += ans[c - 1][j];
}
for (int i = 0; i < ans[c].size(); i++)
printf("%.lf ", ans[c][i]);
return 0;
}

调库

说了这么多,总之sagemath可以一把梭就对了!

来个🌰尝尝鲜:

imaginaryCTF round53-maskLFSR

task.py

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
from os import urandom
import matplotlib.pyplot as plt

flag = b'the flag is ictf{REDACTED}'

class lsfr:
def __init__(self):
self.state = int.from_bytes(urandom(8),'big')
self.mask_1 = int.from_bytes(urandom(8),'big')
self.mask_2 = int.from_bytes(urandom(8),'big')
def randbit(self):
b_1 = bin(self.state & self.mask_1).count('1')
b_2 = bin(self.state & self.mask_2).count('1')
output = (b_1 & 1) ^ (self.state & 1)
self.state = (self.state >> 1) + ((b_2 & 1) << 63)
return output

def bits_to_bytes(bits):
byte_array = bytearray()
for i in range(0, len(bits), 8):
byte = 0
for bit in bits[i:i+8]:
byte = (byte << 1) | bit
byte_array.append(byte)
return byte_array


L = lsfr()

random_bytes = bits_to_bytes([L.randbit() for _ in range(8 * len(flag))])
encrypted_flag = bytes(a ^ b for a, b in zip(random_bytes, flag))
print(f'encrypted_flag = {encrypted_flag}')

记初始状态为,那么output的每一个bit都是的线性组合而已,因为存在线性递推关系,所以可以知道最终的也应该存在线性递推关系,其阶数不超过64。这就说明output还是一个LFSR,而给出的输出也足够长,直接调库一把梭:

exp

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
#!/usr/local/bin/sage
from sage.matrix.berlekamp_massey import berlekamp_massey

F = GF(2)

def bytes_to_bits(byte_array):
bits = []
for byte in byte_array:
for i in range(7, -1, -1):
bits.append((byte >> i) & 1)
return bits

def bits_to_bytes(bits):
byte_array = bytearray()
for i in range(0, len(bits), 8):
byte = 0
for bit in bits[i:i+8]:
byte = (byte << 1) | bit
byte_array.append(byte)
return byte_array



with open("out.txt", 'r', encoding='utf-8') as file:
exec(file.read())
pt = b'the flag is ictf'
encrypted_flag_bits = bytes_to_bits(encrypted_flag)



A = bytes_to_bits([a ^^ b for a,b in zip(encrypted_flag[:128],pt)])

minimal_poly = berlekamp_massey([F(i) for i in A])
V = vector(F,minimal_poly.list()[:-1])
l = len(V)

for i in range(128,len(encrypted_flag_bits)):
prev = vector(F,A[-l:])
A.append(int(V.dot_product(prev)))

decrypted_flag = bytes([a ^^ b for a,b in zip(encrypted_flag,bits_to_bytes(A))])

print(decrypted_flag)

imaginaryCTF round53-maskLFSR2

task.py

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
from os import urandom
from secrets import randbelow,choice
import string

message = 'ictf{REDACTED}'
class lsfr:
def __init__(self):
self.state = int.from_bytes(urandom(8),'big')
self.mask_1 = int.from_bytes(urandom(8),'big')
self.mask_2 = int.from_bytes(urandom(8),'big')


def randbit(self):
b_1 = bin(self.state & self.mask_1).count('1')
b_2 = bin(self.state & self.mask_2).count('1')
output = (b_1 & 1) ^ (self.state & 1)
self.state = (self.state >> 1) + ((b_2 & 1) << 63)
return output

class lsfr2:
def __init__(self):
self.lsfr_1 = lsfr()
self.lsfr_2 = lsfr()

def randbit(self):
output = self.lsfr_1.randbit() ^ self.lsfr_2.randbit()
return output




def bytes_to_bits(byte_array):
bits = []
for byte in byte_array:
for i in range(7, -1, -1):
bits.append((byte >> i) & 1)
return bits

def bits_to_bytes(bits):
byte_array = bytearray()
for i in range(0, len(bits), 8):
byte = 0
for bit in bits[i:i+8]:
byte = (byte << 1) | bit
byte_array.append(byte)
return byte_array



def pad(M,n = 256):
N = n - len(M)
l = ''.join(choice(string.printable.strip()) for _ in range(randbelow(N)))
r = ''.join(choice(string.printable.strip()) for _ in range(N - len(l)))
return l + M + r




L = lsfr2()
flag = pad(message).encode()


random_bytes = bits_to_bytes([L.randbit() for _ in range(8 * len(flag))])
encrypted_flag = bytes(a ^ b for a, b in zip(random_bytes, flag))

print(f'encrypted_flag = {encrypted_flag}')
print(f'gift = {flag[:16]}')

类似上一题,还是可以推知output是LFSR,只不过这次阶数大概在128左右,输出有点不够用了。不过想想,因为所有字符都是printable,所以每个字符的MSB一定是0。这意味着从密文中每8bit取1bit就能得到LFSR的间断输出。如果把output这个LFSR的状态转移矩阵记作,那么每8bit取1bit的间断输出对应的状态转移矩阵是的minimal polynomial整除的minimal polynomial,而的minimal polynomial阶数又不超过128,所以小爆一下就好了。

exp

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
#!/usr/local/bin/sage
from sage.matrix.berlekamp_massey import berlekamp_massey
import re

F = GF(2)

def bytes_to_bits(byte_array):
bits = []
for byte in byte_array:
for i in range(7, -1, -1):
bits.append((byte >> i) & 1)
return bits

def bits_to_bytes(bits):
byte_array = bytearray()
for i in range(0, len(bits), 8):
byte = 0
for bit in bits[i:i+8]:
byte = (byte << 1) | bit
byte_array.append(byte)
return byte_array

def crack_lfsr(V,initial):
l = len(V)
bits = initial[:]
for i in range(len(bits),len(encrypted_flag_bits)):
prev = vector(F,bits[-l:])
bits.append(int(V.dot_product(prev)))
decrypted_flag = bytes([a ^^ b for a,b in zip(encrypted_flag,bits_to_bytes(bits))])
return decrypted_flag

encrypted_flag = b'\x12\xba\x0b\x94\x19$\xceQD\x90\x9a\xc4Y\xb1FB\\4\x0f\xf0\xf8\xf5\xbe\x8c\x90v /c1\x8a}\\~\\\xa1&\xb4\x10\xc8\xd2\x8cn\xf9\x0c/\xff\xad\tW\x0cG\xeb\x8d\xd9\xdb\xf6\x9b\x03{\xb0[\xc7=<\x92d \x85\x9ew\xbaq\xa7\xc9\xc7\xe7\xb0c\xb2\x92\xfch\xa9H\x8c\x83\x9b\xb9\x19\xa2\x9c\xf5*\x00\xdc`\xc4\xe25e\t?\nyp\\j5 \xbfm\xe5W\xfa\xc8\xd4\x8d\xba5\xb2\xf5\xc3\xc6\x18[\xeb\xe9"\xd3\xfb\xe4\x93\x9fy\xed\xc6\x1bxtP\xca\x99\xbfR\xe9\xf1\xffX\x1b#\xe2\xa8\xbb2b9C\x8d\xe3\x83\xc2\xc4\x93\x03\xb2\xba\xebz\x9e\xbd-\x9a\xf0I/\xd6z\x07\x18\x9d\x07Oy\x91\xb3\xd9\xf9eT\xd9\x06|\x86\x8c\xec\x865\xf1\xb7_\xfc\xbec\x14\xb4\x19O\xbbN\xee\x81\x8e\xb4\xe3e\x04\xc5\xf3h\x00i\xb2\xde\x1ba\x88\x8f\x9f\xdcN)\n\xf7l?X\xe1 \x86\xc5ruLj*r\x0ck\xebj8\xf4\xd4Z\xe5'
gift = b'9DXBZY3[Wj.G^=HN'



first_16 = bytes_to_bits([a ^^ b for a,b in zip(encrypted_flag[:16],gift)])

encrypted_flag_bits = bytes_to_bits(encrypted_flag)
R.<x> = PolynomialRing(F)

B = [encrypted_flag_bits[8*i] for i in range(len(encrypted_flag_bits) // 8)]
minimal_poly_factor = berlekamp_massey([F(i) for i in B])
polys = list(R.polynomials(max_degree=128-minimal_poly_factor.degree()))[1:]

for poly in polys:
candidate_annihilating_polynomial = minimal_poly_factor * poly
V = vector(F,candidate_annihilating_polynomial.list()[:-1])
possible = crack_lfsr(V,first_16)
try:
decoded = possible.decode('utf-8')
matches = re.findall(r'ictf\{.*\}', decoded)
if (len(matches) != 0):
print(f'Found likely flag: {matches}')
break
except:
continue

不存在线性递推关系

如果不存在线性递推关系,基本上跟LFSR的关系也就不大了。之所以还是放在LFSR的blog里,主要还是因为CTF中很容易遇到利用多个LFSR的组合构造不存在线性递推关系的输出的例子。当然,第一思路还是想办法构造方程求解,这里我们引入另一种代数攻击手法:代数免疫攻击。

代数免疫度

给定一个布尔函数,它的代数免疫度是满足的度数最小的的度。这样的称为的annihilator。

annihilator有个好处,就是当f(x)=1时,g(x)一定为0。如果g的度比f低,那么我们求解输入参数所需要构造的方程数量就会少一点;比方说,如果g的度是1,那么我们只需要构造线性方程;如果g的度是2,那么我们所需的方程数大概也就是(单项式的数量),为未知数数量。

来看个🌰:

modified from HITCON CTF 2024 Qual-Hyper512

task

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
import secrets
from hashlib import shake_256

MASK1 = 0x77E3816DD9E3627340D7EE76204ED9F9
MASK2 = 0x512E5CEC93B9AE8D6E28E2AB78B8432B


class LFSR:
def __init__(self, n: int, key: int, mask: int):
self.n = n
self.state = key & ((1 << n) - 1)
self.mask = mask

def __call__(self):
b = self.state & 1
self.state = (self.state >> 1) | (
((self.state & self.mask).bit_count() & 1) << (self.n - 1)
)
return b


class Cipher:
def __init__(self, key):
self.lfsr1 = LFSR(128, key[0], MASK1)
self.lfsr2 = LFSR(128, key[1], MASK2)
self.lfsr3 = LFSR(128, key[0] ^ key[1], MASK2)

def bit(self):
x = self.lfsr1()
y = self.lfsr2() ^ self.lfsr2()
z = self.lfsr3() ^ self.lfsr3() ^ self.lfsr3()
return shake_256(str(x + 2 * y + 3 * z + 624).encode()).digest(64)[0] & 1

def stream(self):
while True:
b = 0
for i in reversed(range(8)):
b |= self.bit() << i
yield b

def encrypt(self, pt: bytes):
return bytes([x ^ y for x, y in zip(pt, self.stream())])

def decrypt(self, ct: bytes):
return self.encrypt(ct)

if __name__ == "__main__":
with open("flag.txt", "rb") as f:
flag = f.read().strip()
key = [secrets.randbits(256), secrets.randbits(256)]
cipher = Cipher(key)
gift = cipher.encrypt(b"\x00" * 96)
print(gift.hex())
ct = cipher.encrypt(flag)
print(ct.hex())

bit的输出形式很抽象,先来构造真值表看看它究竟长啥样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

from sage.crypto.boolean_function import BooleanFunction
from hashlib import shake_256

truth_table = []
for z in range(2):
for y in range(2):
for x in range(2):
bit = shake_256(str(x + 2 * y + 3 * z + 624).encode()).digest(64)[0] & 1
truth_table.append(bit)

f = BooleanFunction(truth_table)
fp = f.algebraic_normal_form()
fp
# x0*x1*x2 + x0*x2 + x1 + x2

度数为3,直接攻击不大可行。再检查一下annihilator:

1
2
3
4
imu , g = f.algebraic_immunity(annihilator = True)
assert fp*g == 0
print(f"{imu = }, {g = }")
# imu = 1, g = x1 + x2 + 1

度数为1,正适合代数免疫的打法。从tl2cents大佬那抄一份exp过来:

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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import os
import json
import signal
from sage.all import *
from itertools import combinations
from tqdm import tqdm
import secrets
MASK1 = int(0x77E3816DD9E3627340D7EE76204ED9F9)
MASK2 = int(0x512E5CEC93B9AE8D6E28E2AB78B8432B)

ct = "..."
enc_flag = "..."
ct = bytes.fromhex(ct)
enc_flag = bytes.fromhex(enc_flag)


class LFSRSymbolic:
def __init__(self, n, key, mask):
assert len(key) == n, "Error: the key must be of exactly 128 bits."
self.state = key
self.mask = mask
self.n = n
self.mask_bits = [int(b) for b in bin(self.mask)[2:].zfill(n)]

def update(self):
s = sum([self.state[i] * self.mask_bits[i] for i in range(self.n)])
self.state = [s] + self.state[:-1]

def __call__(self):
b = self.state[-1]
self.update()
return b
class CipherSymbolic:
def __init__(self, key: list):
self.lfsr1 = LFSRSymbolic(128, key[-128:], MASK1)
self.lfsr2 = LFSRSymbolic(128, key[-256:-128], MASK2)
self.lfsr3 = LFSRSymbolic(128, [a - b for a, b in zip(key[-128:],key[-256:-128])], MASK2)

def filter_polynomial(self, x0, x1, x2, x3):
# x0*x1*x2 + x0*x2 + x1 + x2
return x0*x1*x2 + x0*x2 + x1 + x2

def bit(self):
x,y,z = self.get_xyz()
return self.filter_polynomial(x, y, z)

def get_xyz(self):
x = self.lfsr1()
y = self.lfsr2() + self.lfsr2()
z = self.lfsr3() + self.lfsr3() + self.lfsr3()
return x,y,z

def get_yz(self):
y = self.lfsr2() + self.lfsr2()
z = self.lfsr3() + self.lfsr3() + self.lfsr3()
return y,z

def stream(self, n):
return [self.bit() for _ in range(n)]

def xor(self, a, b):
return [x + y for x, y in zip(a, b)]

def encrypt(self, pt: bytes):
pt_bits = [int(b) for b in bin(int.from_bytes(pt, 'big'))[2:].zfill(8 * len(pt))]
key_stream = self.stream(8 * len(pt))
return self.xor(pt_bits, key_stream)

key = secrets.randbits(256)
key_bits = [int(i) for i in bin(key)[2:].zfill(256)]
br256 = BooleanPolynomialRing(256, [f"x{i}" for i in range(256)])
key_sym = list(br256.gens())
print(len(key_sym))
# cipher = Cipher(key)
cipher_sym = CipherSymbolic(key_sym)

pt = b"\x00" * 128
ct_bits = [int(b) for b in bin(int.from_bytes(ct, 'big'))[2:].zfill(8 * len(ct))]
print(ct_bits.count(1))

# check if yz_list.obj exists
if os.path.exists("./yz_list.obj.sobj"):
yz_list = load("./yz_list.obj.sobj")
else:
yz_list = []
for i in tqdm(range(len(pt) * 8)):
yz_list.append(cipher_sym.get_yz())
save(yz_list, "./yz_list.obj")



eqs = []
for i, bit in enumerate(ct_bits):
if bit == 1:
eqs.append(yz_list[i][0] + yz_list[i][1] + 1)

def all_monomials(x1s, x2s):
d1_monos = x1s[:] + x2s[:]
return [1] + d1_monos

def fast_coef_mat(monos, polys, br_ring):
mono_to_index = {}
for i, mono in enumerate(monos):
mono_to_index[br_ring(mono)] = i
# mat = matrix(GF(2), len(polys), len(monos))
mat = [[0] * len(monos) for i in range(len(polys))]
for i, f in tqdm(list(enumerate(polys))):
for mono in f:
# mat[i,mono_to_index[mono]] = 1
mat[i][mono_to_index[mono]] = 1
return mat
x2s = key_sym[128:256]
x1s = key_sym[:128]
monos = all_monomials(list(x1s), list(x2s))
print(f"[+] total equations {len(eqs)}")
print(f"[+] total monomials {len(monos)}")

mat = fast_coef_mat(monos, eqs, br256)
mat = matrix(GF(2), mat)
B = vector(GF(2),[1 for j in range(len(eqs))])
mat = mat[:, 1:]
print(f"[+] {mat.dimensions() = }, {mat.rank() = }")
try:
sol = mat.solve_right(B)
print(f"[+] solution found")
print(f"[+] solution: {sol}")
ker = mat.right_kernel()
for v in ker.basis():
print(f"[+] kernel vector: {v}")
# break
except:
print(f"[+] no solution")

最后这里不满秩,需要再手动处理一下拿到最终的key。

嗯,就酱。

由 Hexo 驱动 & 主题 Keep
访客数 访问量