---
# System prepended metadata

title: HITCON CTF 2024 Writeup

---

# HITCON CTF 2024 Writeup
## Hyper512
* Source:
```py
import secrets
from hashlib import sha256

MASK1 = 0x6D6AC812F52A212D5A0B9F3117801FD5
MASK2 = 0xD736F40E0DED96B603F62CBE394FEF3D
MASK3 = 0xA55746EF3955B07595ABC13B9EBEED6B
MASK4 = 0xD670201BAC7515352A273372B2A95B23


class LFSR:
    def __init__(self, n, key, mask):
        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: int):
        self.lfsr1 = LFSR(128, key, MASK1)
        key >>= 128
        self.lfsr2 = LFSR(128, key, MASK2)
        key >>= 128
        self.lfsr3 = LFSR(128, key, MASK3)
        key >>= 128
        self.lfsr4 = LFSR(128, key, MASK4)

    def bit(self):
        x = self.lfsr1() ^ self.lfsr1() ^ self.lfsr1()
        y = self.lfsr2()
        z = self.lfsr3() ^ self.lfsr3() ^ self.lfsr3() ^ self.lfsr3()
        w = self.lfsr4() ^ self.lfsr4()
        return (
            sha256(str((3 * x + 1 * y + 4 * z + 2 * w + 3142)).encode()).digest()[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(512)
    cipher = Cipher(key)
    gift = cipher.encrypt(b"\x00" * 2**12)
    print(gift.hex())
    ct = cipher.encrypt(flag)
    print(ct.hex())


# a790f917b594f5751b83a7b901f87680093b2eab35e375b2045b1cbba9052c431d7a21da43c3d52cebb583b67c5a4eb99a2e882fea0e1800a1993fcaafba4ce517bdf6c80ad8d1b5737ed80777ee581a21182bfc387f63841555652bbce45d6262c4f767c4faf7c5ad5b60c0c4253b316485e3a91ebc6d5a75dd940a935f3967a91812e75968c8b3f3bd0ce983dbc1f2166c063cb175b5447e76de7237aec59c33713e19defa2d75759e6f8175308e72618d7ade2deaf43c2c47f8eabcfaa39a471069979f0fb175a555c7f8369df7fda8181924e7eb3ef87f14336783e95fcac285902c14c3af485809d4139ac34ac5233c9a4e1e30873e3a5479e38496319b8f9e9f803b1fa73f75660278ac3c3c482a1943a40374f77767892b3a4bee959cd9100434d05a1b8f846429e4c80b040b2636fe8d7a4c8843c0ec8d74a1b145de81a87077b092d083388236c9b60dbbf7495d618855354f5c612be152cbaf41a56f7c7b452d8343f00c06baeaf7de5146366fb3b5ebc68ee3c2e6eef46a7e5306c2acc613afcf55a85ef319bb4c1333b5c057a1282abad88f23a1b216205791ca250dc3054df98412e5abf00578806d632577f98645de35d6f98dcc67795d45abbeac3db7c02e55c6b01375ca7fb4f74ea5c68e5539996de57b1282cce2e1775bffa5bc5d3a033f161572c979a92d83a8ec0c3b938d1c91983a1b14124a49ef893ea4685c9796a3c8b9dcf396b8c63b736de9f87ab57ede64ddd16430f53d7a8c1f91bab1976694e72f92d133264d41ce40f4806c0c57902d05d0660388805bf890fa3740ce87833a0fc2e23b52e816af7d2cc2661543d23ed33a1fc01d5d58fcb2db1e75790a6b06cc1f8921d94cefb2c1bc7c4415442a979b30ffbb0e8390c8e6bc6a3076777959522ea7d053d81671de01fd123d11960250ac517004afa1b375ea5cafe60bb5bdb8fef151a1dc068494a582616fa0460ff03f98e9c867edec107cdbdee478b42182cd598ebba1e01e992bf6e83009c4db6d28e63ed3b5b7944c64976a81c187b4a8c0423bb805ec61bb6f138afc4f146628e55486d3d6949554a7af3eeaec1376b8af320a20035bedff56d358345a85beca00c13fbf1e14e760503270294e3f4f8a330016a77f72fc5ca1c6401707337e334a191fca7fef47e61a9f750dd00200170b06887a866175a87c2e566591e1cf147f41a7dd837d9e045c62f8fb8f0bc6ee983dc6409ec2bf13f133e6bcf1ad904318d029e381f5c48613cb6f49f999a3c13850101bfaa39e6754636e7108485572b46cd140a034fa1d37d4d6b12fad7445dc8310173110a8b8cb3b4cd520593f9e6cd26e83f6a4d8319d59b3b576be8c46c223229f37e72eb6cc83bd9e66576dbfb7a9078c03af211276f35c27d6d3c570438126df32ae9dd13f0f8e8809ae0949c7fe897b87b5e311950147e9f16a88b14bc60ab2ca34ce6b249dd26f224a896f01a6c4adea6bfd33f96f030d52eb6676ba26c09b49119fee3e77191cd282387059ba2883de8c017ba67fe40cd16cd38486237930dd8dc7efb0513c68c3d4335b81201693e6ac148c9afb9c02096f0467f4f3c96841d37a0c23a252d5752f60af0ad3db8f15b48819effd7f4af27a60c966f49a0946cb878c50e640a560ad974ea3b577f76e15b4cc0743f28d4a81f7556715fb159504c904a8cd3635c49fba984e8b8880023813ee6d0ef04a4a778fbbd2c1b8862b69f31e5f4ef35d4e0a71dfeb519e3b3a1e672c31e4dfa5b09459b169aec350280cf65decb8c9e0726ac030b9b3aa91e236010742f63f8fc8d8f3de7aa013b198768c100ca1823a6146932e91c4c916cc614645a870ecd6833428df94c3a55c8a7244a8d61fb703175313456e3dabf8f580057e72a3057a3551ba7d572d336586ca7c237259ddbfe786a626c8060b586e8c42b9598ba82a5945af032ef0f22bcf93e45f56f7bf4b7bce735b24a37cdd5d1c7400e96a4de3a533dba8178f4329d1be4d3f2d99147da8d35607d2e4f823459a0595833b2adf0ee55ddf0c358bc4bfbc83d4b9bed97e5521fac784423e28dd5df72c9c271580a86e2a1d3ab599310e6d605fa56a7d797b6ed36be3d0579da2913e4eb7d7f97d094ee970aefabbb0d79d304fde0b70cb48e531298c9a5f8d84d132f301521367740d0b83e99aa40338fb22c6ad0c947e1c7f3e48ed8fb3bb00396af0c0eab9dfcb260432036724926aab9fb8c34894e44676fffe08ecb63d24abc796e20a67519ab9922e933e993644d75ea8f7ab6e7bdc4748012e67631f2f5bae6f4a81b07fd58d9d29d604499bd44aa9ed7c039769558e4e8ccab170f1974e5fcb54a5cb17e3d6b7d98cdb4730701a8975f6f26c4185b08bf9aa90cbb1c2d9bf3b720eec7b27417400ae302563cf53e0201bf81a88aaa5006b57941e29ba47491407c81dc007668bfb7885cf930d0cfd38c17758e779b9469d41a494511f1b738a2da86e975dc07ac9eb6f7342e1655091f8834f590e2630528a8feabcfdaee1d2cf4f49d5cf48392395238836dfbb6835afa5508678735b12d7cfded20d4242589b5c025f062a81aedd608a11f52dfecd02bd58c6d3233104a2b97b4c6b7f5059c0ecc8bf91da32a3915c593aa24b6a9e20bcfd48f982e00a91a6198074deeffbf22a6b56346d9895b5354091482d0776c37175144ca38f6002be755737c1233185a10251463e711435bf6c1034285497650c583ff370bee675a1653076ce4215dcf6b74019925dcd99194a0b46413e7201a69acee4975026f05daf95943ff27f13ce9017bfa964ef416ab7f1f109b5007e6b1ff82c277ebcc421cdfcead5e72d1d0c18a2cd132238128d8f4c2d17f123b04cc4af68b75379e80471b868875bd05c7ef6f4d194793260fc4442f41b5d40190b7e5c911448ddbd532ae9aa0fd6dc8d63b973188c40d88ccecbcdf995e935dacd7e50c7a16a66c60b65f72107c863ac6d60a3bdce99d06f56387c3498cfaca6b7ab688b364955b11d5d7ef05225b65181f58d582883b349ecc842ae14c8ffb1051e2c5332039a066497c780cdf039c66a2dba90e890b77620dc7e829e223fdf7e96505693cd70fb0431327825721061aa1b67c2438504a25e6d10b7938015e0be2be1ab80496ea28e7bd7ee2a931338cdb8e6c0766eac7a6489f3fc90b298bd3033d711bb7ec95d910834b7ec6935be420c52b6fa94045030ea92cbb9f708c7dc8288965b1e3683d1058f9c5a0d3aabc6b415a8fb5fdd98670d2e49958848010ce5bd297ad7870c36d2d5a1d92807ce0dafb1de6b9e50548af121865635534ecc1a5cb4c26f4af5f8ea19d2bc4315b23ef4531c39439801f9790293c79d3215f7f4de25516a78b1cf04bdce06e46b9a238b75104f5a6c79d3a73d5decb2f88af74a42cae3dc8a0a58dab27b81f496e0aca9cd820e4c92de8bfd3481567f83cd0b203875babd18ca99272d431dab864e19748a05f0414a8ee400d943e891f08336592d9a1a732d7d6cdb99bfa9b0f5ad49052004b3289a9e6f1a363e4a34c15accd1d63673a6ed5bf672fb24a184794980af075bf448e4f58426f1094583ac8ea39da417376687e6fa449344131f2ce38768a335df1e9008675ef1c40cfb7c0f1873086d4ee255fe0e3cca5bd34c15e44ccc377979d1005b33bc389eaaad1f892a1dd3cdfc014d315f52efcf95142cf2fcee6bfb40872ce8fd412fc0fa0b0be59813d9f587d961aa2a9eaf15eb0bf78dca8e6a64543cc423bea182e9326d20f07746f89ce30cadaf1fa17176bb00d92031ab4e3ab8d23b2bbf6683e360508e394ac542a45efc3a997f6f7909383e0dbde3042e2ec6eeca4a58bb15901b7fe20f93c5ce879dbda27ed660884aac5c61c0014894507b64aae26027e1976af5e0b7c585ce27737a6095d81fa069768c12dadd97810d362fb99f795e78553deff6112ea30176e1fd4798f7791e64144927a3051d3aa5356d803e2e04cecf1524400b87f1e2ae0f379f8acb0080d4b6caf9560ae295667eee6f8ecececd710d2043482ffb3186de3cd2b972d20dafcc2f0064142aa37a5ad5e8308c4dd3dde87e50a2cf7b2be60c0649bb7a8b50a3bb1a20e35f043fa8e65ab90cb7b0517210ffa3f6cf0c2714d1ddc9d9aa2710cfc7a6d03208f5e569998fcfa0c8599ab8e633619546c6793a232debddcae6847a20e2085a15a58dfe7879c32cea344f650382b568b773ecd696ce9a8bee6d971fe8545a4d8cbcec2ab8689fe33932ad7bdf68255775e06498d9f414a5494111b73997b5759824569dc7fd8fbc80f0ab6551bc0679690fd2dbfa6a76d5d17ddae2a5261b65c1dfe41eabee7124c11843f700d27d71316d39553edb793b70a73c1feb04ce2e920fc45214b187c1e6bc5c1814bb0b94ee8268cb326e60e7b00084ceb31c5a80cdae587d8fe5fdd1c22a8a493b7ae9ef22d09f2cc4282106a216134dd41ef89ab1b8174b82ff8ed9a288381d7e8248975cf5b12e97266e329c7752ea0e8eccad671dce399cd295b4462fd0866946c8e7bb75c6e2885a4ef62288737bff25a2c641948edf21115d3fbee53f1d6df6db7134c3517f93fc0f912fada2d2aa9df19496f881ed126bf22de27c41d3be11a4824be52c9efa4cc7157ef829b2e514c59e3b85639cf0a2c55c4e48c679eeb40f8d123edc138da02a75c82f3e9d06060b122fa23a3ddcc6cfa9229e2617b1d0d06e1be4f70389a210ddaa3fde718a3155bdb54b75cc0a9bfd939bf80260436c6f10296e273e2b9e8b8001b88a955ea6bc087007636269b9abdf075e9c71f291c3e3221d8c297b88d193e5a26fb1740b95af9f2ce3088bffac2f5aaf63370c1bcb77c3fafc7e7e0bc7a5d41de637536d87dc000d774af7f977a9688540ceb2068381eb9cd1bb2e51590534f66e7ba134bcbcddbdf62c2cf80c162f7c4ea57af6fe4fd83734d6161f77354a394125e37034ffb41c3ada3a48d15d4d97318b9fa67a98d930869fcf7aa39aabf4be7bda632d50253607ffdc7ba31c1a5dd48e9eb828ad8aa329e0d29118a402e0e2bdf460dc1926ffe4d3437133554d836c43c0a8170ba329157c31ebae1ba3c0e40c98180ffdc4dfd9435f607f972c909b324208193d8490ac26284e2f84fdba129b508eb818c36fe49d2181bce213c13917821ef3882e73c878e77a0f25792539f432c4987d4c02f35aa2b1cda09f8aba341809941dd804fc287ed59a477c610b883188cf2fdfd1c51f2fd3fbbda72fdc5257ae1d769bd71bc7048c899cc185de7bd849cfa732f3bf9b010e207f203dddcba1e855285233625e0e06676f68ef3e2acd77eb2dde1044f2f40e210d0ee1b1690710bbefdc2bddd78972eff559401c3e8dd095064cc61f10dd8e214e2d8a019d0686e9199c6fd79dd1928cf3cd8676e3f35221bec1b1c5a4d20b7b981e7508898cf682501446ebd9a542575b94ce7ca94d45f19d5ca7a426a4909da30092ab5716c4da1feee7e9a6990e1da86cb838fd392a25c1c3b6fa8e9a830f91b46805efc04eec66dee65642d0310a94ec194d5e8a6459f29e5fead9665387fce109f99cb687a461b2466045f47392a881903a051076ccbd37afdfe5a85a9d89e1e97fcc2279e54e6ffc7d66509fd0f5cb5af4b70fadcd584b1c86cc61b2b7d7565c11f32131291670167f79d6ef60b15f9598aa5a806dba706203a2e699740314ac47868628576308162f9eb930cf49b345865ca50eb1b3ca1abae9d776e19a313a9d2badd28fd907080bce17a671025ca2280fd754
# 16c63370ac3860ec7eb12f9ec357d462f8513ee887cd86481b521b2bd7995d8abecd595e2ef6fc554cb04d813848b19c06290f0818274303842e68fdc280f1fec612826f
```
### **LFSR** là gì?
* **[LFSR](https://en.wikipedia.org/wiki/Linear-feedback_shift_register)** (~Linear Feedback Shift Register~)  là một loại mạch điện tử đặc biệt được sử dụng trong các ứng dụng như mã hóa, tạo số ngẫu nhiên, và kiểm tra lỗi. LFSR là một thanh ghi dịch mà trong đó đầu vào của mỗi bit được xác định bởi một hàm tuyến tính của các bit khác trong thanh ghi.
* **Cấu trúc cơ bản của LFSR:**
Thanh ghi dịch (Shift Register): Đây là một dãy các bit có thể dịch sang phải hoặc trái.
Hàm phản hồi tuyến tính (Linear Feedback): Là hàm toán học xác định bit mới sẽ được thêm vào thanh ghi sau mỗi lần dịch. Hàm này thường là phép XOR của một số bit trong thanh ghi.
* **Hoạt động của LFSR:**
1. Khởi tạo: LFSR được khởi tạo với một giá trị ban đầu, thường gọi là "seed".
2. Dịch: Tại mỗi bước, toàn bộ các bit trong thanh ghi dịch sang một vị trí.
3. Phản hồi: Bit mới nhất được tạo ra bằng cách thực hiện phép XOR trên một số bit trong thanh ghi (theo định nghĩa của hàm phản hồi).
4. Chu kỳ: LFSR tiếp tục dịch và tạo bit mới cho đến khi trở về giá trị ban đầu hoặc đạt đến một trạng thái nào đó, tạo thành một chu kỳ.
* **Ứng dụng:**
Mã hóa: LFSR được sử dụng trong mã hóa dòng (stream cipher) để tạo ra chuỗi bit giả ngẫu nhiên.
Tạo số ngẫu nhiên: Do tính chất chu kỳ và khả năng tạo ra chuỗi bit có vẻ ngẫu nhiên, LFSR thường được sử dụng trong việc tạo số giả ngẫu nhiên.
Kiểm tra lỗi: LFSR cũng được sử dụng trong các mạch kiểm tra lỗi như CRC (Cyclic Redundancy Check).
* **Ví dụ**:
```py
Cho seed = 01101001

Ta sẽ chọn 3 vị trí trong seed trên: Vị trí thứ 2, 5, và 7. 
Những vị trí này được gọi là tap. Như vậy, rõ ràng output sẽ phụ thuộc vào tap.

Ta XOR những bit của seed ở những tap:
seed[2] xor seed[5] xor seed[7] = 1 xor 1 xor 0 = 0

Sau đó ta đưa bit 0 này lên đầu seed, bỏ đi bit cuối. Đây là output
của chúng ta : 00110100

Ta lại có thể tiếp tục thực hiện thuật toán nếu ta không ưng ý với output.
```
* Đọc thêm tại : 
https://codelearn.io/sharing/stream-cipher-lfsr
https://www.youtube.com/watch?v=Ks1pw1X22y4
https://www.youtube.com/watch?v=SVyTSfeO2do
### Giải.
* Với **LFSR** của bài này, chia làm 2 phần: 
  * 1 là `Combined generators` hay `linear`: tính toán trên 1 phương trình tuyến tính nào đó.![image](https://hackmd.io/_uploads/BkIGJgqqR.png)
  * 2 là `Fillter Generator`: tính toán trên 1 phương trình không tuyến tính, hay là các phép toán Boolean ![image](https://hackmd.io/_uploads/SkCvfy95C.png)
* Để giải quyết bài này thì dùng [Fast Correlation Attack](https://en.wikipedia.org/wiki/Correlation_attack)
* Đọc thêm về nó tại [đây](https://iacr.org/archive/fse2011/67330055/67330055.pdf)
![image](https://hackmd.io/_uploads/SkCvfy95C.png)
* Dựa vào đoạn code này thì mình đi xây dựng bảng chân trị và tìm dạng chuẩn tắc đại số của nó.
* Code gen bảng chân trị:
```py


from Crypto.Util.number import *
from pwn import * 
from gmpy2 import * 
import math
from tqdm import tqdm
from sympy.ntheory.modular import crt
from sympy.ntheory.residue_ntheory import discrete_log
import secrets
import os
import secrets
from hashlib import sha256

def bit(x,y,z,w):
    return (
    sha256(str((3 * x + 1 * y + 4 * z + 2 * w + 3142)).encode()).digest()[0] & 1
)

print("-"*45)
print("|   x   |   y   |   z   |   w   | f(x,y,z,w)|")
for x in range(0,2):
    for y in range(0,2):
        for z in range(0,2):
            for w in range(0,2):
                print("|  ",x,"  |  ",y,"  |  ",z,"  |  ",w,"  |    ",bit(x,y,z,w),"    |")
print("-"*45)
```
* Bảng chân trị:

|     x    |     y    |    z     |    w     |    f(x,y,z,w)  |
|:-------- |:--------:|:--------:|:--------:| --------:|
|    0     |  0       |     0    |  0       |     0    |
|    0     |  0       |     0    |  1       |     0    |
|    0     |  0       |     1    |  0       |     1    |
|    0     |  0       |     1    |  1       |     1    |
|    0     |  1       |     0    |  0       |     1    |
|    0     |  1       |     0    |  1       |     0    |
|    0     |  1       |     1    |  0       |     0    |
|    0     |  1       |     1    |  1       |     1    |
|    1     |  0       |     0    |  0       |     0    |
|    1     |  0       |     0    |  1       |     0    |
|    1     |  0       |     1    |  0       |     1    |
|    1     |  0       |     1    |  1       |     0    |
|    1     |  1       |     0    |  0       |     1    |
|    1     |  1       |     0    |  1       |     1    |
|    1     |  1       |     1    |  0       |     1    |
|    1     |  1       |     1    |  1       |     0    |

* Dạng chuẩn tắc đại số : $f(x,y,z,w)=xyz+xyw+xzw+yw+y+z$
* Dựa vào bảng chân trị trên , ta thấy xác suất để $f(a) = a$ cho 4 giá trị là:
  * $Probability (x) = 0,5$
  * $Probability (y) = 0,625$
  * $Probability (z) = 0,625$
  * $Probability (w) = 0,375$
* Với **Fast Correlation Attack** thì việc recover lại từng phần tử sẽ khả quan khi $Prob>0,5$. $Prob<=0,5$ thì hên xui. Vậy tức là ở bài này thì ra sẽ recover được $y,z$ hay là $key_2,key_3$ trước.
* Đối với [paper này](https://iacr.org/archive/fse2011/67330055/67330055.pdf) thì thuật toán `Fast Correlation Attack` sẽ tính toán giá trị $p*$ gọi là xác suất đúng tại từng vị trí, rồi làm theo 2 thuật toán theo paper trên là **Algorithm A** và **Algorithm B**.
* **Algorithm B**: 
  * 1. Tính $p_i*$ với mỗi $keystream_i$ tương ứng dựa trên số lượng phương trình thỏa mãn.
  * 2. Đảo ngược tất cả các $keystream_i$ nếu $p_i*$ < $p_{thr}$. Với $p_{thr}$ ở một ngưỡng nhất định nào đó.
  * 3. Dừng lại tới khi hệ phương trình tuyến tính của mình giải được. 
* Sau khi tìm được $key_2,key_3$ thì hãy xem bảng chân trị khi $y,z = 1$.
* 
| x  |  y  |   z |   w | f(x,y,z,w) |
|:---|:---:|:---:|:---:|-----------:|
|  0 |   1 |   1 |   0 |          0 |
|  0 |   1 |   1 |   1 |          1 |
|  1 |   1 |   1 |   0 |          1 |
|  1 |   1 |   1 |   1 |          0 |

* Để lại cột $x,w$:
* 
| x | w | f(x,w) |
|:---|:---:|---:|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |

* Bây giờ dạng chuẩn tắc đại số $f(x,w) = x + w$ .
* $x,w$ hay là $key_1,key_4$ tìm giống như trên.
* **Script**:
```py
from sage.all import *
from sage.matrix.berlekamp_massey import berlekamp_massey
import secrets, random, sys
from hashlib import sha256
from sage.crypto.boolean_function import BooleanFunction
from functools import lru_cache
from tqdm import tqdm, trange
from chall import MASK1, MASK2, MASK3, MASK4, LFSR, Cipher
from binteger import Bin

F2 = GF(2)
PR = PolynomialRing(F2, "x")
x = PR.gen()

output_file = "output.txt" if len(sys.argv) < 2 else sys.argv[1]


stream = Bin(bytes.fromhex("a790f917b594f5751b83a7b901f87680093b2eab35e375b2045b1cbba9052c431d7a21da43c3d52cebb583b67c5a4eb99a2e882fea0e1800a1993fcaafba4ce517bdf6c80ad8d1b5737ed80777ee581a21182bfc387f63841555652bbce45d6262c4f767c4faf7c5ad5b60c0c4253b316485e3a91ebc6d5a75dd940a935f3967a91812e75968c8b3f3bd0ce983dbc1f2166c063cb175b5447e76de7237aec59c33713e19defa2d75759e6f8175308e72618d7ade2deaf43c2c47f8eabcfaa39a471069979f0fb175a555c7f8369df7fda8181924e7eb3ef87f14336783e95fcac285902c14c3af485809d4139ac34ac5233c9a4e1e30873e3a5479e38496319b8f9e9f803b1fa73f75660278ac3c3c482a1943a40374f77767892b3a4bee959cd9100434d05a1b8f846429e4c80b040b2636fe8d7a4c8843c0ec8d74a1b145de81a87077b092d083388236c9b60dbbf7495d618855354f5c612be152cbaf41a56f7c7b452d8343f00c06baeaf7de5146366fb3b5ebc68ee3c2e6eef46a7e5306c2acc613afcf55a85ef319bb4c1333b5c057a1282abad88f23a1b216205791ca250dc3054df98412e5abf00578806d632577f98645de35d6f98dcc67795d45abbeac3db7c02e55c6b01375ca7fb4f74ea5c68e5539996de57b1282cce2e1775bffa5bc5d3a033f161572c979a92d83a8ec0c3b938d1c91983a1b14124a49ef893ea4685c9796a3c8b9dcf396b8c63b736de9f87ab57ede64ddd16430f53d7a8c1f91bab1976694e72f92d133264d41ce40f4806c0c57902d05d0660388805bf890fa3740ce87833a0fc2e23b52e816af7d2cc2661543d23ed33a1fc01d5d58fcb2db1e75790a6b06cc1f8921d94cefb2c1bc7c4415442a979b30ffbb0e8390c8e6bc6a3076777959522ea7d053d81671de01fd123d11960250ac517004afa1b375ea5cafe60bb5bdb8fef151a1dc068494a582616fa0460ff03f98e9c867edec107cdbdee478b42182cd598ebba1e01e992bf6e83009c4db6d28e63ed3b5b7944c64976a81c187b4a8c0423bb805ec61bb6f138afc4f146628e55486d3d6949554a7af3eeaec1376b8af320a20035bedff56d358345a85beca00c13fbf1e14e760503270294e3f4f8a330016a77f72fc5ca1c6401707337e334a191fca7fef47e61a9f750dd00200170b06887a866175a87c2e566591e1cf147f41a7dd837d9e045c62f8fb8f0bc6ee983dc6409ec2bf13f133e6bcf1ad904318d029e381f5c48613cb6f49f999a3c13850101bfaa39e6754636e7108485572b46cd140a034fa1d37d4d6b12fad7445dc8310173110a8b8cb3b4cd520593f9e6cd26e83f6a4d8319d59b3b576be8c46c223229f37e72eb6cc83bd9e66576dbfb7a9078c03af211276f35c27d6d3c570438126df32ae9dd13f0f8e8809ae0949c7fe897b87b5e311950147e9f16a88b14bc60ab2ca34ce6b249dd26f224a896f01a6c4adea6bfd33f96f030d52eb6676ba26c09b49119fee3e77191cd282387059ba2883de8c017ba67fe40cd16cd38486237930dd8dc7efb0513c68c3d4335b81201693e6ac148c9afb9c02096f0467f4f3c96841d37a0c23a252d5752f60af0ad3db8f15b48819effd7f4af27a60c966f49a0946cb878c50e640a560ad974ea3b577f76e15b4cc0743f28d4a81f7556715fb159504c904a8cd3635c49fba984e8b8880023813ee6d0ef04a4a778fbbd2c1b8862b69f31e5f4ef35d4e0a71dfeb519e3b3a1e672c31e4dfa5b09459b169aec350280cf65decb8c9e0726ac030b9b3aa91e236010742f63f8fc8d8f3de7aa013b198768c100ca1823a6146932e91c4c916cc614645a870ecd6833428df94c3a55c8a7244a8d61fb703175313456e3dabf8f580057e72a3057a3551ba7d572d336586ca7c237259ddbfe786a626c8060b586e8c42b9598ba82a5945af032ef0f22bcf93e45f56f7bf4b7bce735b24a37cdd5d1c7400e96a4de3a533dba8178f4329d1be4d3f2d99147da8d35607d2e4f823459a0595833b2adf0ee55ddf0c358bc4bfbc83d4b9bed97e5521fac784423e28dd5df72c9c271580a86e2a1d3ab599310e6d605fa56a7d797b6ed36be3d0579da2913e4eb7d7f97d094ee970aefabbb0d79d304fde0b70cb48e531298c9a5f8d84d132f301521367740d0b83e99aa40338fb22c6ad0c947e1c7f3e48ed8fb3bb00396af0c0eab9dfcb260432036724926aab9fb8c34894e44676fffe08ecb63d24abc796e20a67519ab9922e933e993644d75ea8f7ab6e7bdc4748012e67631f2f5bae6f4a81b07fd58d9d29d604499bd44aa9ed7c039769558e4e8ccab170f1974e5fcb54a5cb17e3d6b7d98cdb4730701a8975f6f26c4185b08bf9aa90cbb1c2d9bf3b720eec7b27417400ae302563cf53e0201bf81a88aaa5006b57941e29ba47491407c81dc007668bfb7885cf930d0cfd38c17758e779b9469d41a494511f1b738a2da86e975dc07ac9eb6f7342e1655091f8834f590e2630528a8feabcfdaee1d2cf4f49d5cf48392395238836dfbb6835afa5508678735b12d7cfded20d4242589b5c025f062a81aedd608a11f52dfecd02bd58c6d3233104a2b97b4c6b7f5059c0ecc8bf91da32a3915c593aa24b6a9e20bcfd48f982e00a91a6198074deeffbf22a6b56346d9895b5354091482d0776c37175144ca38f6002be755737c1233185a10251463e711435bf6c1034285497650c583ff370bee675a1653076ce4215dcf6b74019925dcd99194a0b46413e7201a69acee4975026f05daf95943ff27f13ce9017bfa964ef416ab7f1f109b5007e6b1ff82c277ebcc421cdfcead5e72d1d0c18a2cd132238128d8f4c2d17f123b04cc4af68b75379e80471b868875bd05c7ef6f4d194793260fc4442f41b5d40190b7e5c911448ddbd532ae9aa0fd6dc8d63b973188c40d88ccecbcdf995e935dacd7e50c7a16a66c60b65f72107c863ac6d60a3bdce99d06f56387c3498cfaca6b7ab688b364955b11d5d7ef05225b65181f58d582883b349ecc842ae14c8ffb1051e2c5332039a066497c780cdf039c66a2dba90e890b77620dc7e829e223fdf7e96505693cd70fb0431327825721061aa1b67c2438504a25e6d10b7938015e0be2be1ab80496ea28e7bd7ee2a931338cdb8e6c0766eac7a6489f3fc90b298bd3033d711bb7ec95d910834b7ec6935be420c52b6fa94045030ea92cbb9f708c7dc8288965b1e3683d1058f9c5a0d3aabc6b415a8fb5fdd98670d2e49958848010ce5bd297ad7870c36d2d5a1d92807ce0dafb1de6b9e50548af121865635534ecc1a5cb4c26f4af5f8ea19d2bc4315b23ef4531c39439801f9790293c79d3215f7f4de25516a78b1cf04bdce06e46b9a238b75104f5a6c79d3a73d5decb2f88af74a42cae3dc8a0a58dab27b81f496e0aca9cd820e4c92de8bfd3481567f83cd0b203875babd18ca99272d431dab864e19748a05f0414a8ee400d943e891f08336592d9a1a732d7d6cdb99bfa9b0f5ad49052004b3289a9e6f1a363e4a34c15accd1d63673a6ed5bf672fb24a184794980af075bf448e4f58426f1094583ac8ea39da417376687e6fa449344131f2ce38768a335df1e9008675ef1c40cfb7c0f1873086d4ee255fe0e3cca5bd34c15e44ccc377979d1005b33bc389eaaad1f892a1dd3cdfc014d315f52efcf95142cf2fcee6bfb40872ce8fd412fc0fa0b0be59813d9f587d961aa2a9eaf15eb0bf78dca8e6a64543cc423bea182e9326d20f07746f89ce30cadaf1fa17176bb00d92031ab4e3ab8d23b2bbf6683e360508e394ac542a45efc3a997f6f7909383e0dbde3042e2ec6eeca4a58bb15901b7fe20f93c5ce879dbda27ed660884aac5c61c0014894507b64aae26027e1976af5e0b7c585ce27737a6095d81fa069768c12dadd97810d362fb99f795e78553deff6112ea30176e1fd4798f7791e64144927a3051d3aa5356d803e2e04cecf1524400b87f1e2ae0f379f8acb0080d4b6caf9560ae295667eee6f8ecececd710d2043482ffb3186de3cd2b972d20dafcc2f0064142aa37a5ad5e8308c4dd3dde87e50a2cf7b2be60c0649bb7a8b50a3bb1a20e35f043fa8e65ab90cb7b0517210ffa3f6cf0c2714d1ddc9d9aa2710cfc7a6d03208f5e569998fcfa0c8599ab8e633619546c6793a232debddcae6847a20e2085a15a58dfe7879c32cea344f650382b568b773ecd696ce9a8bee6d971fe8545a4d8cbcec2ab8689fe33932ad7bdf68255775e06498d9f414a5494111b73997b5759824569dc7fd8fbc80f0ab6551bc0679690fd2dbfa6a76d5d17ddae2a5261b65c1dfe41eabee7124c11843f700d27d71316d39553edb793b70a73c1feb04ce2e920fc45214b187c1e6bc5c1814bb0b94ee8268cb326e60e7b00084ceb31c5a80cdae587d8fe5fdd1c22a8a493b7ae9ef22d09f2cc4282106a216134dd41ef89ab1b8174b82ff8ed9a288381d7e8248975cf5b12e97266e329c7752ea0e8eccad671dce399cd295b4462fd0866946c8e7bb75c6e2885a4ef62288737bff25a2c641948edf21115d3fbee53f1d6df6db7134c3517f93fc0f912fada2d2aa9df19496f881ed126bf22de27c41d3be11a4824be52c9efa4cc7157ef829b2e514c59e3b85639cf0a2c55c4e48c679eeb40f8d123edc138da02a75c82f3e9d06060b122fa23a3ddcc6cfa9229e2617b1d0d06e1be4f70389a210ddaa3fde718a3155bdb54b75cc0a9bfd939bf80260436c6f10296e273e2b9e8b8001b88a955ea6bc087007636269b9abdf075e9c71f291c3e3221d8c297b88d193e5a26fb1740b95af9f2ce3088bffac2f5aaf63370c1bcb77c3fafc7e7e0bc7a5d41de637536d87dc000d774af7f977a9688540ceb2068381eb9cd1bb2e51590534f66e7ba134bcbcddbdf62c2cf80c162f7c4ea57af6fe4fd83734d6161f77354a394125e37034ffb41c3ada3a48d15d4d97318b9fa67a98d930869fcf7aa39aabf4be7bda632d50253607ffdc7ba31c1a5dd48e9eb828ad8aa329e0d29118a402e0e2bdf460dc1926ffe4d3437133554d836c43c0a8170ba329157c31ebae1ba3c0e40c98180ffdc4dfd9435f607f972c909b324208193d8490ac26284e2f84fdba129b508eb818c36fe49d2181bce213c13917821ef3882e73c878e77a0f25792539f432c4987d4c02f35aa2b1cda09f8aba341809941dd804fc287ed59a477c610b883188cf2fdfd1c51f2fd3fbbda72fdc5257ae1d769bd71bc7048c899cc185de7bd849cfa732f3bf9b010e207f203dddcba1e855285233625e0e06676f68ef3e2acd77eb2dde1044f2f40e210d0ee1b1690710bbefdc2bddd78972eff559401c3e8dd095064cc61f10dd8e214e2d8a019d0686e9199c6fd79dd1928cf3cd8676e3f35221bec1b1c5a4d20b7b981e7508898cf682501446ebd9a542575b94ce7ca94d45f19d5ca7a426a4909da30092ab5716c4da1feee7e9a6990e1da86cb838fd392a25c1c3b6fa8e9a830f91b46805efc04eec66dee65642d0310a94ec194d5e8a6459f29e5fead9665387fce109f99cb687a461b2466045f47392a881903a051076ccbd37afdfe5a85a9d89e1e97fcc2279e54e6ffc7d66509fd0f5cb5af4b70fadcd584b1c86cc61b2b7d7565c11f32131291670167f79d6ef60b15f9598aa5a806dba706203a2e699740314ac47868628576308162f9eb930cf49b345865ca50eb1b3ca1abae9d776e19a313a9d2badd28fd907080bce17a671025ca2280fd754")).list
flag_ct = Bin(bytes.fromhex("16c63370ac3860ec7eb12f9ec357d462f8513ee887cd86481b521b2bd7995d8abecd595e2ef6fc554cb04d813848b19c06290f0818274303842e68fdc280f1fec612826f")).list

m = len(stream)


def mask_to_poly(mask, n):
    return PR(list(map(int, f"{mask:0{n}b}"[::-1]))) + x**n


def poly_to_eq(poly):
    return [i for i, v in enumerate(poly) if v]


def poly_to_mask(poly):
    return int(poly.change_ring(ZZ)(2) - 2 ** poly.degree())


def vec_to_state(v):
    return int("".join(map(str, v[::-1])), 2)


@lru_cache
def S(p, t):
    if t == 1:
        return p
    return p * S(p, t - 1) + (1 - p) * (1 - S(p, t - 1))


def find_square_eqs(eq, length):
    # find related equations by squaring
    # i.e. a[k]=a[k+3]+a[k+4] -> a[k]=a[k+6]+a[k+8]
    assert eq[0] == 0, "eq must start with 0 (constant term)"
    eqs = [eq]
    cur_eq = eq
    while True:
        if cur_eq[-1] * 2 >= length:
            break
        squared_eq = [2 * x for x in cur_eq]
        eqs.append(squared_eq)
        cur_eq = squared_eq
    return eqs


def build_equations(eqs, length):
    # given a list of base equations, build all possible equations by shifting
    # and return a list of related equations (by index) for each position
    pos_eqs = [[] for _ in range(length)]
    new_eqs = []
    for eq in tqdm(eqs, "Build equations"):
        assert eq[0] == 0, "eq must start with 0 (constant term)"
        for shift in range(length - max(eq)):
            eq_index = len(new_eqs)
            new_eqs.append(eq)
            for pos in eq:
                pos_eqs[pos].append(eq_index)
            eq = [x + 1 for x in eq]
    return new_eqs, pos_eqs




def p_star_fn(p, m, h, s):
    p1 = p * s**h * (1 - s) ** (m - h)
    p2 = (1 - p) * s ** (m - h) * (1 - s) ** h
    return p1 / (p1 + p2)


def find_candidates(eqs, pos_eqs, stream, p_corr):
    # find candidate positions for noise estimation
    # given equations and related equations for each position
    # as well as the stream and the correlation probability
    length = len(stream)
    t = len(eqs[0])
    s = S(p_corr, t)
    candidates = []  # list of (p_star, pos)
    for pos in trange(length, desc="Find candidates"):
        h = 0  # number of satisfied equations at position pos
        for eq_index in pos_eqs[pos]:
            eq = eqs[eq_index]
            tmp = 0
            for i in eq:
                tmp ^= stream[i]
            h += tmp == 0
        m = len(pos_eqs[pos])
        p1 = p_corr * s**h * (1 - s) ** (m - h)
        p2 = (1 - p_corr) * s ** (m - h) * (1 - s) ** h
        p_star = p1 / (p1 + p2)
        candidates.append((p_star, pos))
    candidates.sort(reverse=True)
    return candidates


def get_linsys(feedback_poly, length):
    n = feedback_poly.degree()
    M = companion_matrix(feedback_poly, "bottom")
    # 1 ....
    #   1...
    #.....1
    # a1 a2 a3 ..
    Mn = M**n
    rows = []
    I = matrix.identity(n)
    for i in trange(length // n + 1, desc="Get linear system"):
        rows.extend(I.rows())
        I *= Mn
    return rows


def take_linear_system(linsys, candidates, stream, to_take):
    mat = matrix(GF(2), [linsys[pos] for _, pos in candidates[:to_take]])
    target = []
    for p_star, pos in candidates[:to_take]:
        target.append(stream[pos])
    return mat, vector(GF(2), target)


def solve_fca(feedback_poly, eq, prob, stream):
    stream = stream[:]  # copy
    n = feedback_poly.degree()
    
    m = len(stream)
    t = len(eq)
    print(f"{S(prob, t) = }")
    eqs = find_square_eqs(eq, m)
    print(eqs)
    eqs, pos_eqs = build_equations(eqs, m)
  
    linsys = get_linsys(feedback_poly, m)
    candidates = find_candidates(eqs, pos_eqs, stream, prob)
    for it in range(100):
        p_thr = candidates[-m // 32][0]
        for p_star, pos in candidates:
            if p_star <= p_thr:
                stream[pos] = 1 - stream[pos]
        candidates = find_candidates(eqs, pos_eqs, stream, prob)

        if it >= 5:
            mat, target = take_linear_system(linsys, candidates, stream, 2 * n)
            try:
                return mat.solve_right(target)
            except ValueError:
                continue


f1 = mask_to_poly(MASK1, 128)
f2 = mask_to_poly(MASK2, 128)
f3 = mask_to_poly(MASK3, 128)
f4 = mask_to_poly(MASK4, 128)

g2 = x**612 + x**421 + 1

g3 = x**518 + x**475 + 1


assert g2 % f2 == 0
assert g3 % f3 == 0

key2 = solve_fca(f2, poly_to_eq(g2), 5 / 8, stream)
key3 = solve_fca(f3, poly_to_eq(g3), 5 / 8, stream)
print(key2 , key3)
k2 = vec_to_state(key2)
k3 = vec_to_state(key3)
print(f"{k2 = :#x}")
print(f"{k3 = :#x}")
lfsr2 = LFSR(128, k2, MASK2)
lfsr3 = LFSR(128, k3, MASK3)  # 4x of the original
stream2 = [lfsr2() for _ in range(m)]
stream3 = [lfsr3() for _ in range(m)]
print(
    "correlation stream ~ stream2",
    len([1 for x, y in zip(stream, stream2) if x != y]) / m,
)  # 3/8
print(
    "correlation stream ~ stream3",
    len([1 for x, y in zip(stream, stream3) if x != y]) / m,
)  # 3/8


# when y == z == 1, the output is x ^ w, which is linear
# so we a treat it as a 256-bit LFSR

lfsr1tmp = LFSR(128, 48763, MASK1)
lfsr4tmp = LFSR(128, 48763, MASK4)


def combined():
    x = lfsr1tmp() ^ lfsr1tmp() ^ lfsr1tmp()
    w = lfsr4tmp() ^ lfsr4tmp()
    return x ^ w


f1_cube = (companion_matrix(f1, "bottom") ** 3).charpoly()
f14 = berlekamp_massey([F2(combined()) for _ in range(2048)])

assert f14 == f1_cube * f4
linsys_14 = get_linsys(f14, m)

lhs = []
rhs = []
for i in range(m):
    s, s2, s3 = stream[i], stream2[i], stream3[i]
    if s2 == s3 == 1:
        lhs.append(linsys_14[i])
        rhs.append(s)
        if len(lhs) >= 256 + 10:
            break
key14 = matrix(F2, lhs).solve_right(vector(F2, rhs))
mask14 = poly_to_mask(f14)
k14 = vec_to_state(key14)
print(f"{k14 = :#x}")
lfsr14 = LFSR(256, k14, mask14)
stream14 = [lfsr14() for _ in range(m)]
for i in range(m):
    s, s2, s3, s14 = stream[i], stream2[i], stream3[i], stream14[i]
    if s2 == s3 == 1:
        assert s14 == s, "?????"

# then solve a linear system to get the initial states of LFSR1 and LFSR4 from their XOR

M1 = companion_matrix(f1_cube, "bottom")
M4 = companion_matrix(f4, "bottom")
T = F2**256
s1_0_sym = matrix(T.gens()[:128])
s4_0_sym = matrix(T.gens()[128:])
s1_1_sym = M1**128 * s1_0_sym
s4_1_sym = M4**128 * s4_0_sym
sol = (
    (s1_0_sym + s4_0_sym)
    .stack(s1_1_sym + s4_1_sym)
    .solve_right(vector(F2, stream14[:256]))
)
key1 = list(sol[:128])
key4 = list(sol[128:])
k1 = vec_to_state(key1)
k4 = vec_to_state(key4)
print(f"{k1 = :#x}")
print(f"{k4 = :#x}")
lfsr1 = LFSR(128, k1, poly_to_mask(f1_cube))
lfsr4 = LFSR(128, k4, MASK4)
stream1 = [lfsr1() for _ in range(m)]
stream4 = [lfsr4() for _ in range(m)]


def mix(x, y, z, w):
    return sha256(str((3 * x + 1 * y + 4 * z + 2 * w + 3142)).encode()).digest()[0] & 1


rec = [mix(x, y, z, w) for x, y, z, w in zip(stream1, stream2, stream3, stream4)]
assert rec == stream
for i in range(len(flag_ct)):
    flag_ct[i] ^= mix(lfsr1(), lfsr2(), lfsr3(), lfsr4())

print(Bin(flag_ct).bytes)
# hitcon{larger_states_is_still_no_match_of_fast_correlation_attacks!}
```