Live From Serangoon Road
This is the third challenge from the Crypto section of CTF SG 2021. The challenge needs us to break a Stream Cipher that uses 4 LFSR.
Statement
Damnit, this telco is down again. It’s fine, we know exactly how they encrypted the message, maybe you might be able to help us decode it?
The encryption python code, encrypted bytes, first few words of the decrypted text is provided.
Observation
The challenge provided us the taps, key length of all the 4 LFSR.
The only thing that we don’t know is the initial seed of the LFSR.
Let \(a[i],b[i],c[i],d[i]\) be the i-th outputs of the 4 LFSR respectively
Let \(cip[i]\) be the i-th bit of the ciphertext
Let \(msg[i]\) be the i-th bit of the plaintext
Define a function
\[f(a,b,c,d) = ((a \oplus b) \land c) \lor ((\neg b \lor c) \land d)\]The key for i-th bit is \(f(a[i],b[i],c[i],d[i])\)
\[cip[i] = msg[i] \oplus f(a[i],b[i],c[i],d[i])\]Solution
I analyzed \(f(a,b,c,d)\) and found some interesting property from it.
Here’s the truth table of \(f(a,b,c,d)\)
\[\begin{array}{|c|c|c|c|c|} \hline a & b & c & d & f(a,b,c,d)\\ \hline \hline 1 & 1 & 1 & 1 & 1\\ \hline 1 & 1 & 1 & 0 & 0\\ \hline 1 & 1 & 0 & 1 & 0\\ \hline 1 & 1 & 0 & 0 & 0\\ \hline \hline 1 & 0 & 1 & 1 & 1\\ \hline 1 & 0 & 1 & 0 & 1\\ \hline 1 & 0 & 0 & 1 & 1\\ \hline 1 & 0 & 0 & 0 & 0\\ \hline \hline 0 & 1 & 1 & 1 & 1\\ \hline 0 & 1 & 1 & 0 & 1\\ \hline 0 & 1 & 0 & 1 & 0\\ \hline 0 & 1 & 0 & 0 & 0\\ \hline \hline 0 & 0 & 1 & 1 & 1\\ \hline 0 & 0 & 1 & 0 & 0\\ \hline 0 & 0 & 0 & 1 & 1\\ \hline 0 & 0 & 0 & 0 & 0\\ \hline \hline \end{array}\]Notice that
if \(a = 1\) and \(b = 1\) then \(f(a,b,c,d) = c \land d\)
if \(a = 1\) and \(b = 0\) then \(f(a,b,c,d) = c \lor d\)
if \(a = 0\) and \(b = 1\) then \(f(a,b,c,d) = c\)
if \(a = 0\) and \(b = 0\) then \(f(a,b,c,d) = d\)
Nice!! This can help me to brute force the key in a smarter way.
So to solve this challenge, I brute force for the seed for LFSR 1 and LFSR 2
This is doable because LFSR 1 and LFSR 2 only have key length of 8.
On the other hand, LFSR 3 and LFSR 4 have key length of 24.
Tap for LFSR 3 is 1310720 (0b000101000000000000000000)
. Which means that
Tap for LFSR 4 is 589824 (0b000010010000000000000000)
. Which means that
I can’t just try every key from 0 to \(2^{24}\) for LFSR 3 and LFSR 4 due to the long key length. However, the number of possible keys are greatly reduced with all these constraints based on the plaintext.
\[c[i+24] = c[i+3] \oplus c[i+5]\\ d[i+24] = d[i+4] \oplus d[i+7]\\ f(a[i],b[i],c[i],d[i]) = msg[i] \oplus cip[i]\\\]My code can find solution in less than 2 minutes on my laptop. It uses a backtracking method similar to how we solve sudoku and 8 queen problem.
Z3 is also possible but somehow it just doesn’t work for me.
Here’s the python code for the complete solution.
from Crypto.Util.number import long_to_bytes
KEYS = [8, 8, 24, 24]
TAPS = [96, 195, 1310720, 589824]
enc = open('encrypted.enc','rb').read()
pre = open('decrypted.txt','rb').read()
enc = list(map(int, ''.join('{0:08b}'.format(x, 'b') for x in enc)))
pre = list(map(int, ''.join('{0:08b}'.format(x, 'b') for x in pre)))
def f(a,b,c,d):
return ((a^b)&c)|(((~b)|c)&d)
class LSFR:
def __init__(self, state, taps, length):
self.taps = taps
self.state = state
self.length = 2**length
def getNext(self):
out = 0
xored = self.taps & self.state
while xored > 0:
if xored % 2 == 1:
out = 1 - out
xored //= 2
self.state = (self.state*2 + out)%self.length
return out
class StreamCipher:
def __init__(self, func, *args):
self.lsfr = list(args)
self.func = func
def encrypt(self, string):
bits = []
key = []
for char in string:
out = self.func(list(map(lambda x: x.getNext(), self.lsfr)))
bits.append(char ^ out)
key.append(out)
enc = []
while bits:
enc.append(chr(int(''.join(map(str, bits[0:8])), 2)))
bits = bits[8:]
return ''.join(enc)
c = [-1 for i in range(len(enc))]
d = [-1 for i in range(len(enc))]
for i in range(2**KEYS[0]):
print(i)
for j in range(2**KEYS[1]):
a = LSFR(i, TAPS[0], KEYS[0])
b = LSFR(j, TAPS[1], KEYS[1])
a = [a.getNext() for i in range(8+len(enc))][8:]
b = [b.getNext() for i in range(8+len(enc))][8:]
func(0)
def func(k) :
q = k-8
if (q >= 0 and q + 24 < len(pre)) :
i = q + 24
t1 = a[i]
t2 = b[i]
t3 = c[q+3] ^ c[q+5]
t4 = d[q+4] ^ d[q+7]
t = pre[i] ^ enc[i]
if (f(t1,t2,t3,t4) != t):
return
if (k == 24):
cc = c.copy()
dd = d.copy()
for i in range(len(enc)-24):
cc[24+i] = cc[i+3]^cc[i+5]
dd[24+i] = dd[i+4]^dd[i+7]
ans = b''
for i in range(0,len(enc)-8,8):
ca = 0
for j in range(8):
ca = ca << 1
key = f(a[i+j],b[i+j],cc[i+j],dd[i+j])
ca += key ^ enc[i+j]
ans += chr(ca).encode()
if (b'This is n' in ans):
print(ans)
return
t1 = a[k]
t2 = b[k]
t = pre[k] ^ enc[k]
if (t1 and t2):
if (t):
c[k] = 1
d[k] = 1
func(k+1)
else :
c[k] = 1
d[k] = 0
func(k+1)
c[k] = 0
d[k] = 1
func(k+1)
c[k] = 0
d[k] = 0
func(k+1)
if (t1 and not t2):
if (t):
c[k] = 1
d[k] = 1
func(k+1)
c[k] = 1
d[k] = 0
func(k+1)
c[k] = 0
d[k] = 1
func(k+1)
else :
c[k] = 0
d[k] = 0
func(k+1)
if (not t1 and t2):
c[k] = t
d[k] = 1
func(k+1)
c[k] = t
d[k] = 0
func(k+1)
if (not t1 and not t2):
c[k] = 0
d[k] = t
func(k+1)
c[k] = 1
d[k] = t
func(k+1)
c[k] = -1
d[k] = -1
flag : CTFSG{C0rRel4t10N_AtT4cK$_0n_LsFR_101}