CTF NeoQuest 2018 has recently ended. Under the cat analysis of the second part of the task about the airship, ZeroNet , the shift register with feedback and the system of linear equations. The task was given the address of the site on the ZeroNet network, where the chat is deployed. Since in ZeroNet, the contents of the site are downloaded to the local computer, then having stuck the folder with the site, I found the first key, as well as encrypted_storage.json:
Apparently, we need to decrypt “secret_value”. You may notice that the length of the ciphertext for AdminUsername is 5 bytes. Perhaps clear text - “admin”? If it is a gamma cipher, then we can get 5 bytes of gamma and check whether they are suitable for other "secrets". Check on the first 5 bytes of SiteName , we get b "\ xe8Y \ x9aP5". The result is not very ...
Another site has this page: ')
Stumbling, you can see that the length of the ciphertext is equal to the length of the plaintext, and encryption and decryption is the same operation. Very similar to gamma, maybe there is the same algorithm that is encrypted second secret?
The algorithm itself is in the js / lfsr.js file . Gamma in it is generated by function
functiongenKeyStream(key, iv, length) { var res = [] var state_len = bitLength(key) var state = iv.mod(bigInt(1).shiftLeft(state_len)) for (var i = 0; i < length; i++) { var next_byte = 0for (var j = 0; j < 8; j++) { var next_bit = bitCount(state.and(key)) % 2 next_byte = (next_byte >>> 1) | (next_bit << 7) state = state.shiftRight(1).or(bigInt(next_bit).shiftLeft(state_len - 1)) } res.push(next_byte) } return res.reverse() }
This is a shift register with feedback. The register length is equal to the key length; the location of the taps is specified by the key. IV is cut to the length of the key and is used as the initial fill. Only in contrast to the usual shift register with feedback, here the gamma is taken not from the output of the register, but from its input.
You can also see that the first bytes of the gamma will encrypt the last bytes of the OT ... We return to encrypted_storage.json , take the gamma from “admin” and shuffle the last 5 bytes of SiteName to it : “oChat” is a completely different result! We decipher the end of the SiteAddress line - we see that the line contains “1NeoChatZK4DxZJdg5WwocwxcUppA1eJgL” - the site address. The length of this string is 34 bytes, i.e. we have the first 34 bytes (272 bits) gamma. In principle, encryption using shift registers is no longer considered persistent, but still having only a small piece of the gamut won't easily break it ... But this cipher has a slight difference from the “classics” - the gamma is taken from the register input. After thinking about it, it can be understood that after L = len (key) iterations IV will be completely “forced out” of the shift register, and the entire internal state (register filling) will be equal to the last L bits of the gamma. Then for all You can write the following equation:
Where: - i-th bit of gamma - i-th key bit - key length - logical AND - exclusive OR (XOR) Solving a system of L such equations, one can find the key. And knowing the key and the internal state (the last L bits of the gamma), you can generate as many gammas as you need! In order for a system to have at most one solution, it must have at least L linearly independent equations. Those. on the right side will stand for . So we can solve the problem if the key length is not more than bit.
To solve it, I had to write a function that solves the system according to the Gauss method, since all the ready-made functions found solved everything in the field of real or complex numbers, and not on the set (0, 1) with the AND and XOR operations. Then I went through different values of L, if the system turned out to be joint, I checked whether the next bits of the gamma generated by such a key would match those known to me. Match for . A few minutes later the key was counted!
In fact, it was possible not to go through different Ls, but to immediately write down a system of 134 equations with 134 unknowns, just for the key, the first 6 bits would be zero.
Decision code
from hashlib import sha1 N_ct = 70 * 8 N = 0x22 * 8 O_hex = "D7 C2 B1 CB 2D 88 15 66 40 4F 3E 25 F0 89 BC B0 F2 C7 13 F7 93 6D 7F C8 B7 08 FF CA C6 6C FA 99 E9 C4".replace(" ", "") O_int = int(O_hex, 16) O = [int(c) for c in bin(O_int)[2:]] O.reverse() O_full = O_int ct = int("C7 3A B5 4B 91 C6 09 9B 0F 00 81 EA 91 24 E2 F5 0E 84 6F 6F C6 74 04 6B 3B 41 A8 60 48 AE 3E 27 B0 4D 35 4C 9B 97 86 A1 58 F9 72 28 21 00 53 62 BC FA E8 D6 C0 A2 61 AD DB 0B 2E F8 E1 6F BD FC 85 1B 82 E0 82 9D".replace(" ", ""), 16) defsolve_linear_eq(A):"""A[l, l+1]""" l = len(A) assert len(A) + 1 == len(A[0]) for j in range(l): for i in range(j, l): if A[i][j] == 1: break A[j], A[i] = A[i], A[j] for i in range(l): if i != j and A[i][j] == 1: for k in range(l + 1): A[i][k] ^= A[j][k] for i in range(l): for j in range(l): if i != j and A[i][j] == 1or i==j and A[i][j] == 0: returnNonereturn [A[i][-1] for i in range(l)] defpreapare_linear_eq(L): A = [] for n in range(L, 2*L): A.append([]) for i in range(0, L): A[-1].append(O[n-L+i]) A[-1].append(O[n]) return A defgamma(K): O_full = O.copy() L = len(K) for n in range(N, N_ct): s = 0for i in range(0, L): s ^= O_full[n - L + i] & K[i] O_full.append(s) return O_full defcheck_key(K):for n in range(L, N): s = 0for i in range(0, L): s ^= O[n - L + i] & K[i] if s != O[n]: returnFalsereturnTrue found = 0 not_found = 0for L in range(8, N//2): A = preapare_linear_eq(L) K = solve_linear_eq(A) if K isnotNoneand check_key(K): found += 1 print(L, K) O_full = gamma(K) O_full_int = 0 O_full.reverse() for O in O_full: O_full_int = (O_full_int << 1) + O pt = O_full_int ^ ct pt = pt.to_bytes(70, "big") print(pt) breakelse: not_found += 1 print(sha1(pt).hexdigest())