DGZ's Blog.

流密码简单学习

Word count: 4.3kReading time: 19 min
2020/05/06 Share

起点:https://0xdktb.top/2020/03/12/Summary-of-Crypto-in-CTF-stream/#Stream-Cipher-Many-Time-Pad

根据师傅的这一篇博客进行了一系列的补充

Stream Cipher - Many Time Pad

Theorem

  • 流密钥循环使用

  • 猜测密钥长度(未知密钥长度)

    Hamming Distance(汉明距离,二进制下两个等长字符串的比特位差异)

    1011101 与 1001001 有两位差异,所以两个字符串之间的汉明距离是 2。

    大小写英文字符两两的平均Hamming距离为2 ~ 3,而任意字符两两的平均Hamming距离为4

    其中,a(01100001)^ space(001000000) = A (0100 0001) ,a与A的汉明距离为1,

取合适的密钥长度上下界,进行分组计算汉明距离

将二元组(key_length, distance)按distance升序排列,取前五组验证即可

  • 逐字节破解密钥

    在猜测的密钥长度基础上,分割Cipher(用同一密钥字节加密的密文归入同组)

    字频分析

    遍历range(0xff),和标准频率表(含空格和字母)内积(就是相乘)最大的即猜测为正确密钥字节

    因为不正确密钥字节下也可能内积很大,所以进行如下filter:

  • decode(‘ascii’)出错 => return 0

    • cipher分组里为空格或字母的字符总数 < len(密钥片段) // 1.5 => return 0

    爆破空格

    基于空格和小写字母异或结果为对应大写字母,和大写字母异或结果为对应小写字母这一特性

    对同一分组的cipher进行两两异或(相当于plain两两异或),记录每个字符与其他所有字符异或结果落在落在大小写字母和0x00上的次数,即可认为最大次数对应的字符位置上的明文为空格

exp1

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
import string
from binascii import hexlify, unhexlify
std_rate = {' ': 0.18399868388580254, 'a': 0.06528332604415538, 'b': 0.012531342868095015, 'c': 0.021018439349774747, 'd': 0.03523431934577844, 'e': 0.10261742470196494, 'f': 0.019179805952664334, 'g': 0.016178672025026573, 'h': 0.050890812568726566, 'i': 0.056467348737766584, 'j': 0.0011847320400802188, 'k': 0.006037640287976256, 'l': 0.03310705983030898,
'm': 0.02089774777877722, 'n': 0.056334978507447564, 'o': 0.06194486601507346, 'p': 0.014653281404481763, 'q': 0.0009593791262805568, 'r': 0.048625794552908115, 's': 0.051741663225580235, 't': 0.07413556318261176, 'u': 0.023188985607049396, 'v': 0.008010827191217628, 'w': 0.018155155658972653, 'x': 0.0014651754741116037, 'y': 0.015511271998835988, 'z': 0.0006457026385315064}
def bytes_xor(x, y):
if len(y) > len(x):
x, y = y, x
return bytes([i ^ j for i, j in zip(x[:len(y)], y)])
def hamming_distance(x, y):
dis = 0
for byte in bytes_xor(x, y):
dis += bin(byte).count('1')
return dis
def get_key_length(cipher):
average_distances = []
for key_length in range(2, 40):
cipher_fragments = []
for i in range(10):
if key_length * (i + 1) > len(cipher):
break
cipher_fragments.append(
cipher[key_length * i: key_length * (i + 1)])
distance = 0
for i in range(len(cipher_fragments) - 1):
distance += hamming_distance(
cipher_fragments[i], cipher_fragments[i + 1])
distance /= (key_length * len(cipher_fragments))
average_distances.append((key_length, distance))
average_distances.sort(key=lambda x: x[1])
return average_distances
'''
# 字频分析
def dot_multiply(p):
rate = [0] * 27
tmp_len = 0
for _ in p:
try:
_ = bytes([_]).decode('ascii')
except:
return 0
_ = _.lower()
if _ in std_rate:
tmp_len += 1
if _ == ' ':
rate[0] += 1
else:
rate[ord(_) - ord('a') + 1] += 1
if not tmp_len or tmp_len < len(p) // 1.5:
return 0
rate = [i / tmp_len for i in rate]
res = rate[0] * std_rate[' ']
for i in range(1, 27):
res += rate[i] * std_rate[chr(ord('a') + i - 1)]
return res
def crack_key_fragment(cipher_fragment):
max_dotmul_res = 0
pro_key_fragment = 0
for key_fragment in range(0xff):
key_set = [key_fragment] * len(cipher_fragment)
plain_fragment = bytes_xor(key_set, cipher_fragment)
dotmul_res = dot_multiply(plain_fragment)
if dotmul_res > max_dotmul_res:
max_dotmul_res = dotmul_res
pro_key_fragment = key_fragment
return pro_key_fragment
def crack_key(cipher, average_distances):
for key_length, _ in average_distances[:5]:
key = []
cipher_fragments = [[] for _ in range(key_length)]
for i, byte in enumerate(cipher):
cipher_fragments[i % key_length].append(byte)
for cipher_fragment in cipher_fragments:
key.append(crack_key_fragment(cipher_fragment))
print(bytes(key))
'''
# 爆破空格
def crack_key_fragment(cipher_fragment):
max_possible = 0
pro_space = 0 # 空格下标索引
letters = string.ascii_letters.encode('ascii')
for i in range(len(cipher_fragment)):
possible = 0
for j in range(len(cipher_fragment)):
if i == j:
continue
_ = cipher_fragment[i] ^ cipher_fragment[j]
if _ in letters or _ == 0:
possible += 1
if possible > max_possible:
max_possible = possible
pro_space = i
return cipher_fragment[pro_space] ^ 0x20
def crack_key(cipher, average_distances):
for key_length, _ in average_distances[:5]:
key = []
cipher_fragments = [[] for _ in range(key_length)]
for i, byte in enumerate(cipher):
cipher_fragments[i % key_length].append(byte)
for cipher_fragment in cipher_fragments:
key.append(crack_key_fragment(cipher_fragment))
print(bytes(key))
def main():
masked_cipher = b'input your masked_cipher here'
salt = b'input your salt here'
cipher = bytes_xor(masked_cipher, salt *
(len(masked_cipher) // len(salt) + 1))
average_distances = get_key_length(cipher)
crack_key(cipher, average_distances)
if __name__ == "__main__":
main()

比较尴尬的是这个加密解密需要盐,总体上师傅的这个思路比较完整

当不需要盐时

来自:https://www.jianshu.com/p/ea2bda3a0099 的脚本

另外:https://blog.csdn.net/Lynn_coder/java/article/details/101179763

师傅上面提到的针对密文是一长串字符串,而且加密时还用了盐这里我们另外记录另一种情况,对于这种情况,只需爆破空格。如何找到这十段密文中,哪些位置是空格?首先我们要知道,这里每两个16进制字符代表明文的一个字符。我们把10段密文的每一段进行两个两个划分,然后与剩余9个密文的相同位置进行异或,如果异或出来结果大部分都是字母,则说明了这个位置极有可能是个空格。

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
# -*- coding: utf-8 -*-
import string
import collections
import sets

def strxor(a, b): # xor two strings (trims the longer input)
return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)])

def OPT_create(key,crypto):
ciphertext=strxor(key,crypto)
return ciphertext

key='afctf{OPT_1s_Int3rest1ng}'
c1='Dear Friend, This time I u'
c2='nderstood my mistake and u'
c3='sed One time pad encryptio'
c4='n scheme, I heard that it '
c5='is the only encryption met'
c6='hod that is mathematically'
c7=' proven to be not cracked '
c8='ever if the key is kept se'
c9='cure, Let Me know if you a'
c10='gree with me to use this e'
c11='ncryption scheme always.'

ciphers = [c1, c2, c3, c4, c5, c6, c7, c8,c9,c10,c11]

for x in ciphers:
k=OPT_create(key,x).encode('hex')
print k
#print k.decode('hex')

求key:

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
#!/usr/bin/python
## OTP - Recovering the private key from a set of messages that were encrypted w/ the same private key (Many time pad attack) - crypto100-many_time_secret @ alexctf 2017
# Original code by jwomers: https://github.com/Jwomers/many-time-pad-attack/blob/master/attack.py)

import string
import collections
import sets, sys

# 11 unknown ciphertexts (in hex format), all encrpyted with the same key

c1='25030206463d3d393131555f7f1d061d4052111a19544e2e5d'
c2='0f020606150f203f307f5c0a7f24070747130e16545000035d'
c3='1203075429152a7020365c167f390f1013170b1006481e1314'
c4='0f4610170e1e2235787f7853372c0f065752111b15454e0e09'
c5='081543000e1e6f3f3a3348533a270d064a02111a1b5f4e0a18'
c6='0909075412132e247436425332281a1c561f04071d520f0b11'
c7='4116111b101e2170203011113a69001b475206011552050219'
c8='041006064612297020375453342c17545a01451811411a470e'
c9='021311114a5b0335207f7c167f22001b44520c15544801125d'
c10='06140611460c26243c7f5c167f3d015446010053005907145d'
c11='0f05110d160f263f3a7f4210372c03111313090415481d49'
ciphers = [c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11]
# The target ciphertext we want to crack
#target_cipher = "0529242a631234122d2b36697f13272c207f2021283a6b0c7908"

# XORs two string
def strxor(a, b): # xor two strings (trims the longer input)
return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)])

def target_fix(target_cipher):
# To store the final key
final_key = [None]*150
# To store the positions we know are broken
known_key_positions = set()

# For each ciphertext
for current_index, ciphertext in enumerate(ciphers):
counter = collections.Counter()
# for each other ciphertext
for index, ciphertext2 in enumerate(ciphers):
if current_index != index: # don't xor a ciphertext with itself
for indexOfChar, char in enumerate(strxor(ciphertext.decode('hex'), ciphertext2.decode('hex'))): # Xor the two ciphertexts
# If a character in the xored result is a alphanumeric character, it means there was probably a space character in one of the plaintexts (we don't know which one)
if char in string.printable and char.isalpha(): counter[indexOfChar] += 1 # Increment the counter at this index
knownSpaceIndexes = []

# Loop through all positions where a space character was possible in the current_index cipher
for ind, val in counter.items():
# If a space was found at least 7 times at this index out of the 9 possible XORS, then the space character was likely from the current_index cipher!
if val >= 7: knownSpaceIndexes.append(ind)
#print knownSpaceIndexes # Shows all the positions where we now know the key!

# Now Xor the current_index with spaces, and at the knownSpaceIndexes positions we get the key back!
xor_with_spaces = strxor(ciphertext.decode('hex'),' '*150)
for index in knownSpaceIndexes:
# Store the key's value at the correct position
final_key[index] = xor_with_spaces[index].encode('hex')
# Record that we known the key at this position
known_key_positions.add(index)

# Construct a hex key from the currently known key, adding in '00' hex chars where we do not know (to make a complete hex string)
final_key_hex = ''.join([val if val is not None else '00' for val in final_key])
# Xor the currently known key with the target cipher
output = strxor(target_cipher.decode('hex'),final_key_hex.decode('hex'))

print "Fix this sentence:"
print ''.join([char if index in known_key_positions else '*' for index, char in enumerate(output)])+"\n"

# WAIT.. MANUAL STEP HERE
# This output are printing a * if that character is not known yet
# fix the missing characters like this: "Let*M**k*ow if *o{*a" = "cure, Let Me know if you a"
# if is too hard, change the target_cipher to another one and try again
# and we have our key to fix the entire text!

#sys.exit(0) #comment and continue if u got a good key

target_plaintext = "cure, Let Me know if you a"
print "Fixed:"
print target_plaintext+"\n"

key = strxor(target_cipher.decode('hex'),target_plaintext)

print "Decrypted msg:"
for cipher in ciphers:
print strxor(cipher.decode('hex'),key)

print "\nPrivate key recovered: "+key+"\n"

for i in ciphers:
target_fix(i)

YE3fTH.png

由于加密时用到了zip函数,而key的长度是25位,只会加密密文中前25位,所以还原出来的明文就会有点缺失

LFSR

参考:https://www.anquanke.com/post/id/181811#h3-3

除了LFSR之外,还包括NFSR(非线性反馈移位寄存器)。

FSR是流密码产生密钥流的一个重要组成部分,在GF(2)上的一个n级FSR通常由n个二元存储器一个反馈函数组成

GF是Galois Field的缩写,中文称其为Galois域或有限域,GF(2)是最简单的有限域,只有0,1二元及+(异或运算)、×(与运算)

也就是LFSR主要围绕异或运算和二进制展开

如果这里的反馈函数是线性的,我们则将其称为LFSR

假设给定一个5级的LFSR,其初始状态(即a1到a5这5个二元存储器的值)为:

其反馈函数为:

整个过程可以表示为下图所示的形式:

YEtyLV.png

接下来我们来计算该LFSR的输出序列,输出序列的前5位即为我们的初始状态10011第6位的计算过程如下:

第7位的计算过程如下:

由此类推,可以得到前31位的计算结果:1001101001000010101110110001111

对于一个n级的LFSR来讲,其最大周期为2^n-1,因此对于我们上面的5级LFSR来讲,其最大周期为2^5-1=31,再后面的输出序列即为前31位的循环。

因此对于一个LFSR来讲,我们目前主要关心三个部分:初始状态反馈函数输出序列,那么对于CTF中考察LFSR的题目来讲也是如此,大多数情况下,我们在CTF中的考察方式都可以概括为:给出反馈函数输出序列,要求我们反推出初始状态初始状态即为我们需要提交的flag,另外大多数情况下,初始状态的长度我们也是已知的。

2018 CISCN 线上赛 oldstreamgame

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
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()

key文件转换成16进制字符串:

1
2
3
4
with open('key', 'r') as f:
print f.read().encode('hex')
#20FDEEF8A4C9F4083F331DA8238AE5ED083DF0CB0E7A83355696345DF44D7C186C1F459BCE135F1DB6C76775D5DCBAB7A783E48A203C19CA25C22F60AE62B37DE8E40578E3A7787EB429730D95C9E1944288EB3E2E747D8216A4785507A137B413CD690C
#转成二进制

我们的已知条件:

已知flag内的长度为8,之后R=int(flag[5:-1],16),暗示内容是16进制,将长度为8的16进制字符(也就是4个16进制字符)转成了整形,也就是初始状态的长度为4个16进制数,即32位,初始状态的值即我们要去求的flag。
已知反馈函数,只不过这里的反馈函数是代码的形式,我们需要提取出它的数学表达式。
已知输出序列。

那么我们就是通过分析lfsr函数,整理成数学表达式的形式求解即可,接下来我们一行一行的来分析这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#接收两个参数,R是32位的初始状态(即flag),mask是32位的掩码,由于mask已知,所以我们就直接把他当做一个常数即可。
def lfsr(R,mask):

#把R左移一位后低32位(即抹去R的最高位,然后在R的最低位补0)的值赋给output变量。
output = (R << 1) & 0xffffffff

#把传入的R和mask做按位与运算,运算结果取低32位,将该值赋给i变量。
i=(R&mask)&0xffffffff

#从i的最低位向i的最高位依次做异或运算,将运算结果赋给lastbit变量。
lastbit=0
while i!=0:
lastbit^=(i&1)
i=i>>1

#将output变量的最后一位设置成lastbit变量的值。
output^=lastbit

#返回output变量和lastbit变量的值,output即经过一轮lfsr之后的新序列,lastbit即经过一轮lfsr之后输出的一位。
return (output,lastbit)

通过上面的分析,我们可以看出在这道题的情境下,lfsr函数本质上就是一个输入R输出lastbit的函数,虽然我们现在已经清楚了R是如何经过一系列运算得到lastbit的,但是我们前面的反馈函数都是数学表达式的形式,我们能否将上述过程整理成一个表达式的形式呢?这就需要我们再进一步进行分析:

mask只有第3、5、8、12、20、27、30、32这几位为1,其余位均为0。

mask与R做按位与运算得到i,当且仅当R的第3、5、8、12、20、27、30、32这几位中也出现1时,i中才可能出现1,否则i中将全为0。

lastbit是由i的最低位向i的最高位依次做异或运算得到的,在这个过程中,所有为0的位我们可以忽略不计(因为0异或任何数等于任何数本身,不影响最后运算结果),因此lastbit的值仅取决于i中有多少个1:当i中有奇数个1时,lastbit等于1;当i中有偶数个1时,lastbit等于0。 当R的第3、5、8、12、20、27、30、32这几位依次异或结果为1时,即R中有奇数个1,因此将导致i中有奇数个1;当R的第3、5、8、12、20、27、30、32这几位依次异或结果为0时,即R中有偶数个1,因此将导致i中有偶数个1。

因此我们可以建立出联系:lastbit等于R的第3、5、8、12、20、27、30、32这几位依次异或的结果。

将其写成数学表示式的形式,即为:

显然,lastbit和R之间满足线性关系,那么接下来我们就可以开始求解了:

我们想象这样一个场景,当即将输出第32位lastbit时,此时R已经左移了31位,根据上面的数学表达式,我们有:

YExRKO.png

(很蛋疼到这里就卡住了,我完全不知道他的lastbit是怎么算出来的)

雪特,后面发现是我python的hex转二进制有点小毛病

在线网站转码:https://www.rapidtables.com/convert/number/ascii-hex-bin-dec-converter.html

1
00100000 11111101 11101110 11111000 10100100 11001001 11110100 00001000 00111111 00110011 00011101 10101000 00100011 10001010 11100101 11101101 00001000 00111101 11110000 11001011 00001110 01111010 10000011 00110101 01010110 10010110 00110100 01011101 11110100 01001101 01111100 00011000 01101100 00011111 01000101 10011011 11001110 00010011 01011111 00011101 10110110 11000111 01100111 01110101 11010101 11011100 10111010 10110111 10100111 10000011 11100100 10001010 00100000 00111100 00011001 11001010 00100101 11000010 00101111 01100000 10101110 01100010 10110011 01111101 11101000 11100100 00000101 01111000 11100011 10100111 01111000 01111110 10110100 00101001 01110011 00001101 10010101 11001001 11100001 10010100 01000010 10001000 11101011 00111110 00101110 01110100 01111101 10000010 00010110 10100100 01111000 01010101 00000111 10100001 00110111 10110100 00010011 11001101 01101001 00001100

而python2中:

1
2
3
s=0x20FDEEF8A4C9F4083F331DA8238AE5ED083DF0CB0E7A83355696345DF44D7C186C1F459BCE135F1DB6C76775D5DCBAB7A783E48A203C19CA25C22F60AE62B37DE8E40578E3A7787EB429730D95C9E1944288EB3E2E747D8216A4785507A137B413CD690C
print bin(s)
#0b100000111111011110111011111000101001001100100111110100000010000011111100110011000111011010100000100011100010101110010111101101000010000011110111110000110010110000111001111010100000110011010101010110100101100011010001011101111101000100110101111100000110000110110000011111010001011001101111001110000100110101111100011101101101101100011101100111011101011101010111011100101110101011011110100111100000111110010010001010001000000011110000011001110010100010010111000010001011110110000010101110011000101011001101111101111010001110010000000101011110001110001110100111011110000111111010110100001010010111001100001101100101011100100111100001100101000100001010001000111010110011111000101110011101000111110110000010000101101010010001111000010101010000011110100001001101111011010000010011110011010110100100001100

是没毛病的,但是!

1
2
3
s='20FDEEF8A4C9F4083F331DA8238AE5ED083DF0CB0E7A83355696345DF44D7C186C1F459BCE135F1DB6C76775D5DCBAB7A783E48A203C19CA25C22F60AE62B37DE8E40578E3A7787EB429730D95C9E1944288EB3E2E747D8216A4785507A137B413CD690C'
print bin(bytes_to_long(s))
#0b11001000110000010001100100010001000101010001010100011000111000010000010011010001000011001110010100011000110100001100000011100000110011010001100011001100110011001100010100010001000001001110000011001000110011001110000100000101000101001101010100010101000100001100000011100000110011010001000100011000110000010000110100001000110000010001010011011101000001001110000011001100110011001101010011010100110110001110010011011000110011001101000011010101000100010001100011010000110100010001000011011101000011001100010011100000110110010000110011000101000110001101000011010100111001010000100100001101000101001100010011001100110101010001100011000101000100010000100011011001000011001101110011011000110111001101110011010101000100001101010100010001000011010000100100000101000010001101110100000100110111001110000011001101000101001101000011100001000001001100100011000000110011010000110011000100111001010000110100000100110010001101010100001100110010001100100100011000110110001100000100000101000101001101100011001001000010001100110011011101000100010001010011100001000101001101000011000000110101001101110011100001000101001100110100000100110111001101110011100000110111010001010100001000110100001100100011100100110111001100110011000001000100001110010011010101000011001110010100010100110001001110010011010000110100001100100011100000111000010001010100001000110011010001010011001001000101001101110011010000110111010001000011100000110010001100010011011001000001001101000011011100111000001101010011010100110000001101110100000100110001001100110011011101000010001101000011000100110011010000110100010000110110001110010011000001000011

就错了,果然是对这些转换很不熟练

所以我们可以取出前32比特,也就是:00100000 11111101 11101110 11111000 也就是lastbit的初始状态

下面回到正文

这样我们就可以求出R的第1位,同样的方法,我们可以求出R的第2位:

YEzFMT.png

以此类推,R的全部32位我们都可以依次求出了,将这一计算过程写成代码形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mask = '10100100000010000000100010010100'
key = '00100000111111011110111011111000'

tmp=key

R = ''
for i in range(32):
output = '?' + key[:31]
ans = int(tmp[-1-i])^int(output[-3])^int(output[-5])^int(output[-8])^int(output[-12])^int(output[-20])^int(output[-27])^int(output[-30])
R += str(ans)
key = str(ans) + key[:31]

R = format(int(R[::-1],2),'x')
flag = "flag{" + R + "}"
print flag

以上我直接复制,很明显按他的代码存在一些对数字的逆序处理,而且mark也是倒过来的,我很难绕欸

我们看回题目

1
2
3
4
for j in range(8):
(R,out)=lfsr(R,mask)
tmp=(tmp << 1)^out
f.write(chr(tmp))

这里8个lastbit输出一个tmp,也就是一个字符

这里面R经过这一轮8次加密后就会变成另外一个模样,下一个8轮加密开始的R就是这个变了的R

但是哦,这个R在每轮加密过程中,是左移一位(移去最高位),然后最后一位0异或这个lastbit

也就是经过32次加密才会与初始的R完全不同

思考了半天得出了z3好像也可以解

再思考一会发现果然是他画的图太绕了,不好理解

我另外画一个:

YVYyFg.png

同样地我们也是倒着求,但是我们的公式要变一下

这里的X代表output,也就是变换过的R,我们也同样固定X1到X32,如上图所示

另外我们这里的R固定为flag,

倒着求的话,首先第一个要求的是R32也就是初始序列的最后一位

而我们第32位lastbit(X32)用到了这个这一位R32,

此时有关系

显然可以求出这个R32

这样倒着求就可以求出来flag了,脚本也是这么理解

改个变量名这样看更亲切

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mask = '10100100000010000000100010010100'
xlsb = '00100000111111011110111011111000'

tmp=xlsb

R = ''
for i in range(32):
output = '?' + xlsb[:31]
ans = int(tmp[-1-i])^int(output[-3])^int(output[-5])^int(output[-8])^int(output[-12])^int(output[-20])^int(output[-27])^int(output[-30])
R += str(ans)
xlsb = str(ans) + xlsb[:31]

R = format(int(R[::-1],2),'x')
flag = "flag{" + R + "}"
print flag
CATALOG
  1. 1. Stream Cipher - Many Time Pad
    1. 1.1. Theorem
    2. 1.2. exp1
    3. 1.3. 当不需要盐时
  2. 2. LFSR
    1. 2.1. 2018 CISCN 线上赛 oldstreamgame