---
# System prepended metadata

title: World Wide CTF 2024
tags: [CTF Writeups]

---

## **epsilonDH**

![image](https://hackmd.io/_uploads/BJZT71371x.png)

:::spoiler **chall.py**
```python
    from Crypto.Util.number import getStrongPrime, getRandomNBitInteger, bytes_to_long, long_to_bytes
    import os
    
    p = getStrongPrime(1024)
    flag = b"fake+flag"
    
    class Epsilon:
        def __init__(self, a, b):
            self.a, self.b = a, b
        
        def __add__(self, other):
            if type(other) == int: other = Epsilon(other, 0)
            return Epsilon(self.a + other.a, self.b + other.b)
        def __radd__(self, other): return self.__add__(other)
    
        def __mul__(self, other):
            if type(other) == int: other = Epsilon(other, 0)
            return Epsilon(self.a * other.a, self.a * other.b + other.a * self.b)
        def __rmul__(self, other): return self.__mul__(other)
    
        def __mod__(self, other: int):
            return Epsilon(self.a % other, self.b % other)
        
        def __repr__(self): return f"{self.a} + {self.b}ɛ"
    
        @staticmethod
        def getRandomBits(n):
            return Epsilon(getRandomNBitInteger(n), getRandomNBitInteger(n))
    
    def powm(b, e, m):
        r = 1
        while e > 1:
            if e & 1:
                r = (r * b) % m
            b = (b * b)  % m
            e >>= 1
        return (b * r) % m
    
    ɛ = Epsilon(0, 1)
    g = ɛ.getRandomBits(1024)
    m = bytes_to_long(flag)
    assert m < p
    A = powm(g, m, p)
    
    print(f"{p = }")
    print(f"{g = }")
    print(f"{A = }")
```
:::   
    
:::spoiler **output.txt**
```
    p = 173924944755645003178406095718617168013285320974193311533464918516351624141198287888308296721497553891802368640344837769848433705383843820088678374708528763495103734488139368870389319280613181418960926879728892929013723036956818870578758055144789952650214552781344528622703875374067812710366180881422848078127
    g = 153222010878956025592659771364999461265827693159532862299380012549533704470078014065110463612108844661289052080113198166134196684645743591092035461757997498335465019478118882739217108862526250347939116529661007420054504044554198442479469991584947626223020239910145162698053768142977329057860163194054350707249 + 172891042743500566967040288858220451145776247635832845268756172370398885506225014595399937064138727095012954778403481826951857306135326675358326250562011754152669045113179084291737802426967956129601732530346663460456772733886633658030480267226610996560624379249886941665142384623344612516572694197005870648544ɛ
    A = 111358852433093434730054197107140594544307303976075171645711474646957878889456280742672183479878216988442037548855367380131019369757301409440037291726826948896290153981733240859717678222235993520619121828503359668550259222802623131077882382174117682287404839404525320091636778728025592053329591735052259548204 + 45354415949210290456746549581237886628185346518296265188224888250560968013577364380436312628842962917052795341010011570997705657164666282067908433629612354440756444902895692385443905786057605548586533595209894409891955713650856537806150873335015064767898088756645985769501205659617651498842444073593828856739ɛ
```
:::  

    

Challenge này thực hiện mã hóa dựa trên **epsilon number** trong đó $\epsilon$ là một giá trị "siêu nhỏ". Đây là một cấu trúc quen thuộc trong các hệ số đại số phi tiêu chuẩn hoặc số siêu thực.

Các phép toán đã được định nghĩa lại để phù hợp với epsilon number.

Flag được mã hóa như sau:  $A = g^{flag} \pmod p$

Mình có thấy p-1 factor được khá nhiều prime nhỏ nhưng ko biết áp dụng Polig-Hellman vào chỗ này làm sao. Sau đó mình phải đi xin hint =))))))

Ta nhận thấy:

$$
\begin{aligned}
(x + y\epsilon)^2 &= x^2 + 2xy\epsilon \\
(x + y\epsilon)^3 &= x^3 + 3x^2y\epsilon \\
(x + y\epsilon)^4 &= x^4 + 4x^3y\epsilon
\end{aligned}
$$

Theo quy luật này ta sẽ có:

$$
\begin{aligned}
&A = (g_x + g_y\epsilon)^{flag} = g_x^{flag} + flag*g_x^{flag - 1}*g_y\epsilon \\ &\Rightarrow A_y =  flag*g_x^{flag - 1}*g_y = flag*(A_x/g_x)*g_y \\ &\Rightarrow flag = (A_y * g_x) / (A_x * g_y) \pmod p
\end{aligned}
$$

:::spoiler **Script**
```python
    p = 173924944755645003178406095718617168013285320974193311533464918516351624141198287888308296721497553891802368640344837769848433705383843820088678374708528763495103734488139368870389319280613181418960926879728892929013723036956818870578758055144789952650214552781344528622703875374067812710366180881422848078127
    g = [153222010878956025592659771364999461265827693159532862299380012549533704470078014065110463612108844661289052080113198166134196684645743591092035461757997498335465019478118882739217108862526250347939116529661007420054504044554198442479469991584947626223020239910145162698053768142977329057860163194054350707249,172891042743500566967040288858220451145776247635832845268756172370398885506225014595399937064138727095012954778403481826951857306135326675358326250562011754152669045113179084291737802426967956129601732530346663460456772733886633658030480267226610996560624379249886941665142384623344612516572694197005870648544]
    A = [111358852433093434730054197107140594544307303976075171645711474646957878889456280742672183479878216988442037548855367380131019369757301409440037291726826948896290153981733240859717678222235993520619121828503359668550259222802623131077882382174117682287404839404525320091636778728025592053329591735052259548204 ,45354415949210290456746549581237886628185346518296265188224888250560968013577364380436312628842962917052795341010011570997705657164666282067908433629612354440756444902895692385443905786057605548586533595209894409891955713650856537806150873335015064767898088756645985769501205659617651498842444073593828856739]
    
    x, y = g
    mx, my = A
    m = (my * x * pow(mx * y, -1, p)) % p
    print(long_to_bytes(int(m
```
:::
    

    

## Twister

![image](https://hackmd.io/_uploads/S1muSkhQyg.png)

:::spoiler **deploy.py**
```python
    from dataclasses import dataclass
    from cmath import exp
    import secrets
    import time
    import os
    
    FLAG = os.getenv("FLAG") or "test{flag_for_local_testing}"
    
    @dataclass
    class Wave:
        a: int
        b: int
    
        def eval(self, x):
            theta = x / self.a + self.b
            return ((exp(1j * theta) - exp(-1j * theta)) / 2j).real
    
    ALL_WAVES = [Wave(a, b) for a in range(2, 32) for b in range(7)]
    
    class MaximTwister:
        """
        Next-generation PRNG with really **complex** nature.
        More reliable than /dev/random cuz doesn't block ever.
        """
    
        def __init__(self, state=None):
            if state is None:
                state = (1337, [secrets.randbits(1) for _ in ALL_WAVES])
    
            self.point = state[0]
            self.waves = [wave for wave, mask in zip(ALL_WAVES, state[1]) if mask]
    
        def get_randbit(self) -> int:
            result = 0
            for wave in self.waves:
                # you would never decompose a sum of waves 😈
                result += round(wave.eval(self.point))
            # especially if you know only the remainder, right? give up
            result %= 2
            self.point += 1
    
            return result
    
        def get_randbits(self, k: int) -> int:
            return int("".join(str(self.get_randbit()) for _ in range(k)), 2)
    
        def get_token_bytes(self, k: int) -> bytes:
            return bytes([self.get_randbits(8) for _ in range(k)])
    
    print("*** BUG DESTROYER ***")
    print("You encounter: 😈 SEGMENTATION FAULT 😈")
    opponent_hp = int(time.time()) * 123
    days_passed = 0
    
    random = MaximTwister()
    
    while True:
        print(
            f"🕺 You ({10-days_passed} days till release) -- 😈 SEGMENTATION FAULT ({opponent_hp} lines)"
        )
        print(f"Day {days_passed + 1}. You can:")epsilonDH
image

chall.py
output.txt
Challenge này thực hiện mã hóa dựa trên epsilon number trong đó 
 là một giá trị "siêu nhỏ". Đây là một cấu trúc quen thuộc trong các hệ số đại số phi tiêu chuẩn hoặc số siêu thực.

Các phép toán đã được định nghĩa lại để phù hợp với epsilon number.

Flag được mã hóa như sau: 

Mình có thấy p-1 factor được khá nhiều prime nhỏ nhưng ko biết áp dụng Polig-Hellman vào chỗ này làm sao. Sau đó mình phải đi xin hint =))))))

Ta nhận thấy:

 

Theo quy luật này ta sẽ có:

 

Script
    p = 173924944755645003178406095718617168013285320974193311533464918516351624141198287888308296721497553891802368640344837769848433705383843820088678374708528763495103734488139368870389319280613181418960926879728892929013723036956818870578758055144789952650214552781344528622703875374067812710366180881422848078127
    g = [153222010878956025592659771364999461265827693159532862299380012549533704470078014065110463612108844661289052080113198166134196684645743591092035461757997498335465019478118882739217108862526250347939116529661007420054504044554198442479469991584947626223020239910145162698053768142977329057860163194054350707249,172891042743500566967040288858220451145776247635832845268756172370398885506225014595399937064138727095012954778403481826951857306135326675358326250562011754152669045113179084291737802426967956129601732530346663460456772733886633658030480267226610996560624379249886941665142384623344612516572694197005870648544]
    A = [111358852433093434730054197107140594544307303976075171645711474646957878889456280742672183479878216988442037548855367380131019369757301409440037291726826948896290153981733240859717678222235993520619121828503359668550259222802623131077882382174117682287404839404525320091636778728025592053329591735052259548204 ,45354415949210290456746549581237886628185346518296265188224888250560968013577364380436312628842962917052795341010011570997705657164666282067908433629612354440756444902895692385443905786057605548586533595209894409891955713650856537806150873335015064767898088756645985769501205659617651498842444073593828856739]
    
    x, y = g
    mx, my = A
    m = (my * x * pow(mx * y, -1, p)) % p
    print(long_to_bytes(int(m
Twister
image

deploy.py
    from dataclasses import dataclass
    from cmath import exp
    import secrets
    import time
    import os
    
    FLAG = os.getenv("FLAG") or "test{flag_for_local_testing}"
    
    @dataclass
    class Wave:
        a: int
        b: int
    
        def eval(self, x):
            theta = x / self.a + self.b
            return ((exp(1j * theta) - exp(-1j * theta)) / 2j).real
    
    ALL_WAVES = [Wave(a, b) for a in range(2, 32) for b in range(7)]
    
    class MaximTwister:
        """
        Next-generation PRNG with really **complex** nature.
        More reliable than /dev/random cuz doesn't block ever.
        """
    
        def __init__(self, state=None):
            if state is None:
                state = (1337, [secrets.randbits(1) for _ in ALL_WAVES])
    
            self.point = state[0]
            self.waves = [wave for wave, mask in zip(ALL_WAVES, state[1]) if mask]
    
        def get_randbit(self) -> int:
            result = 0
            for wave in self.waves:
                # you would never decompose a sum of waves 😈
                result += round(wave.eval(self.point))
            # especially if you know only the remainder, right? give up
            result %= 2
            self.point += 1
    
            return result
    
        def get_randbits(self, k: int) -> int:
            return int("".join(str(self.get_randbit()) for _ in range(k)), 2)
    
        def get_token_bytes(self, k: int) -> bytes:
            return bytes([self.get_randbits(8) for _ in range(k)])
    
    print("*** BUG DESTROYER ***")
    print("You encounter: 😈 SEGMENTATION FAULT 😈")
    opponent_hp = int(time.time()) * 123
    days_passed = 0
    
    random = MaximTwister()
    
    while True:
        print(
            f"🕺 You ({10-days_passed} days till release) -- 😈 SEGMENTATION FAULT ({opponent_hp} lines)"
        )
        print(f"Day {days_passed + 1}. You can:")
        print("1. Make a fix")
        print("2. Call a senior")
        choice = input("> ").strip()
        if choice == "1":
            damage = random.get_randbits(32)
            opponent_hp -= damage
            if opponent_hp <= 0:
                print(
                    f"You commited a fix deleting {damage} lines. Miraculously, it worked!"
                )
                break
            else:
                print(f"You commited a fix deleting {damage} lines. The bug remained 😿")
        elif choice == "2":
            print("You called a senior. It's super effective! The bug is destroyed.")
            break
        else:
            print(
                f"You spent {random.get_randbits(4)} hours doing whatever {choice} means."
            )
    
        print("A day has passed. You couldn't fix the bug.")
        days_passed += 1
    
        if days_passed == 10:
            print("It's release date! The bug is still there. You're fired.")
            exit()
    
        print("1. Make a fix")
        print("2. Call a senior")
        choice = input("> ").strip()
        if choice == "1":
            damage = random.get_randbits(32)
            opponent_hp -= damage
            if opponent_hp <= 0:
                print(
                    f"You commited a fix deleting {damage} lines. Miraculously, it worked!"
                )
                break
            else:
                print(f"You commited a fix deleting {damage} lines. The bug remained 😿")
        elif choice == "2":
            print("You called a senior. It's super effective! The bug is destroyed.")
            break
        else:
            print(
                f"You spent {random.get_randbits(4)} hours doing whatever {choice} means."
            )
    
        print("A day has passed. You couldn't fix the bug.")
        days_passed += 1
    
        if days_passed == 10:
            print("It's release date! The bug is still there. You're fired.")
            exit()
    
    print("The bug is gone! You got a raise.")
    print(
        "In your new office you see a strange door. It is locked. You try to guess the password from the digital lock:"
    )
    password = input("> ")
    if bytes.fromhex(password) == random.get_token_bytes(16):
        print("Somehow, you guessed the password! The room opens before you.")
        print("You see a mysterious text:", FLAG)
        print(
            "What could it mean?... You turn around and see your boss right behind you..."
        )
        print("BAD ENDING")
    else:
        print("Incorrect. Well, let's get back to work...")
        print("GOOD ENDING")
    
```
:::

Chall này có 2 chức năng chính:
- option 1: trả về một giá trị random
- option 2: người dùng predict được giá trị random tiếp theo thì sẽ nhận được flag.


Cơ chế tạo ra số random như sau:
- State được random là các mảng bao gồm các bit 0 và 1 và state này không được cập nhật như hàm random thông thường.
```python!
state = (1337, [secrets.randbits(1) for _ in ALL_WAVES])
```
- Các ==waves== được chọn dựa vào state
```python!
waves = [wave for wave, mask in zip(ALL_WAVES, state[1]) if mask]
```

- Hàm get_ranbit()
```python!
 def get_randbit(self) -> int:
        result = 0
        for wave in self.waves:
            # you would never decompose a sum of waves 😈
            result += round(wave.eval(self.point))
        # especially if you know only the remainder, right? give up
        result %= 2
        self.point += 1

        return result
```

Các bit tạo thành dựa trên **point** và **wave** sau mỗi lần gọi hàm thì `point+=1`.

$result = \sum_{i=0}^{k} W_i(point)  \bmod 2$

với `W_i(point) = round(waves[i].eval(point))`

Nếu ta có thể khôi phục lại state thì có thể predict được các số random tiếp theo.

Gọi A là ma trận có dạng như sau:
$$
M = \begin{bmatrix}
W_0(point) & W_1(point) & ... & W_n(point)\\
W_0(point+1) & W_1(point+1) & ... & W_n(point+1) \\
\vdots \\ 
W_0(point+k) & W_1(point+k) & ... & W_n(point+k)
\end{bmatrix}	
$$
$\Rightarrow state * M = [bits]$

Ta nhận thấy len(state) = 210 nên chương trình có giới hạn 10 lần gửi thì ta vẫn tạo ra một ma trận hoàn chỉnh để tìm lại state.

:::spoiler **Script**
```python
from sage.all import *
from dataclasses import dataclass
from cmath import exp
import secrets
import time
import os

@dataclass
class Wave:
    a: int
    b: int

    def eval(self, x):
        theta = x / self.a + self.b
        return ((exp(1j * theta) - exp(-1j * theta)) / 2j).real

ALL_WAVES = [Wave(a, b) for a in range(2, 32) for b in range(7)]

def gen_base_matrix():
    point = 1337
    res = []
    tmp = []
    while point < 9*32 + 1337:
        for wave in ALL_WAVES:
        # you would never decompose a sum of waves 😈
            tmp.append(int(round(wave.eval(point))))
        # especially if you know only the remainder, right? give up
        res.append(tmp)
        tmp = []
        point += 1

    return res
    
def solver(arr):
    res = []
    for i in range(len(arr)):
        res += [0]*(32 - arr[i].bit_length()) + list(map(int, bin(arr[i])[2:]))

    res = vector(GF(2), res[:210])
    print(len(res))
    base = Matrix(GF(2), gen_base_matrix()[:210])
    new_state = base.inverse() * res
    return new_state


from pwn import *

numbers = []
io = remote("twister.chal.wwctf.com", 1337)
for i in range(9):
    io.recvuntil(b'> ')
    io.sendline(b'1')
    io.recvuntil(b'You commited a fix deleting ')
    number = int(io.recvuntil(b' ').decode())
    numbers.append(number)

print(numbers)
state = (1337, [int(i) for i in solver(numbers)])

class MaximTwister:
    """
    Next-generation PRNG with really **complex** nature.
    More reliable than /dev/random cuz doesn't block ever.
    """

    def __init__(self, state=state):
        print(state)
        if state is None:
            state = (1337, [secrets.randbits(1) for _ in ALL_WAVES])

        self.point = state[0]
        self.waves = [wave for wave, mask in zip(ALL_WAVES, state[1]) if mask]

    def get_randbit(self) -> int:
        result = 0
        for wave in self.waves:
            # you would never decompose a sum of waves 😈
            result += int(round(wave.eval(self.point)))
        # especially if you know only the remainder, right? give up
        result %= 2
        self.point += 1

        return result

    def get_randbits(self, k: int) -> int:
        return int("".join(str(self.get_randbit()) for _ in range(k)), 2)

    def get_token_bytes(self, k: int) -> bytes:
        return bytes([self.get_randbits(8) for _ in range(k)])
    
ran = MaximTwister()
print([ran.get_randbits(32) for i in range(9)])
predict = ran.get_token_bytes(16)
io.recvuntil(b'> ')
io.sendline(b'2')
io.recvuntil(b'> ')
io.sendline(predict.hex().encode())
io.interactive()

```
:::

![image](https://hackmd.io/_uploads/Bk6Tyl3mJg.png)
 ## Just Lattice
 ![image](https://hackmd.io/_uploads/S1pY5EnQke.png)
:::spoiler **chall.py**
```python!
import numpy as np
from secret import flag


def gen(q, n, N, sigma):
    t = np.random.randint(0, high=q//2, size=n)
    s = np.concatenate([np.ones(1, dtype=np.int32), t])
    A = np.random.randint(0, high=q//2, size=(N, n))
    e = np.round(np.random.randn(N) * sigma ** 2).astype(np.int32) % q
    b = ((np.dot(A, t) + e).reshape(-1, 1)) % q
    P = np.hstack([b, -A])
    return P, s


def enc(P, M, q):
    N = P.shape[0]
    n = len(M)
    r = np.random.randint(0, 2, (n, N))
    Z = np.zeros((n, P.shape[1]), dtype=np.int32)
    Z[:, 0] = 1
    C = np.zeros((n, P.shape[1]), dtype=np.int32)
    for i in range(n):
        C[i] = (np.dot(P.T, r[i]) + (np.floor(q/2) * Z[i] * M[i])) % q
    return C


q = 127
n = 3
N = int(1.1 * n * np.log(q))
sigma = 1.0

P, s = gen(q, n, N, sigma)


def prep(s):
    return np.array([int(b) for char in s for b in f'{ord(char):08b}'], dtype=np.int32)


C = enc(P, prep(flag), q)
P = P.tolist()
C = C.tolist()
print(f"{P=}")
print(f"{C=}")

```
:::

:::spoiler output.txt
P=[[54, -22, -60, -8], [45, -26, -16, -62], [83, -8, -32, -30], [106, -46, -25, -21], [23, -22, -20, -25], [102, -40, -46, -42], [111, -16, -60, -50], [35, -9, -4, -41], [22, -61, -22, -7], [46, -10, -35, -20], [89, -54, -12, -3], [58, -43, -4, -10], [18, -56, -47, -52], [16, 0, -54, -31], [45, -28, -41, -40]]
C=[[18, 33, 115, 66], [52, 88, 94, 117], [111, 44, 32, 14], [0, 33, 10, 56], [54, 34, 107, 121], [50, 69, 33, 29], [70, 116, 96, 17], [67, 80, 59, 21], [58, 49, 73, 27], [5, 47, 29, 87], [9, 30, 70, 70], [97, 18, 24, 47], [105, 31, 92, 66], [7, 12, 45, 17], [26, 78, 63, 69], [48, 37, 23, 31], [105, 22, 44, 48], [36, 44, 13, 89], [0, 100, 102, 50], [24, 11, 28, 34], [89, 64, 71, 75], [64, 101, 75, 10], [29, 95, 96, 16], [34, 71, 40, 4], [52, 7, 64, 57], [117, 94, 101, 30], [20, 97, 15, 37], [108, 36, 66, 118], [104, 91, 85, 78], [73, 38, 74, 93], [115, 120, 53, 96], [50, 21, 42, 11], [29, 120, 105, 63], [51, 88, 85, 100], [9, 70, 76, 65], [111, 85, 112, 55], [18, 42, 44, 110], [71, 97, 14, 110], [101, 57, 119, 100], [38, 106, 117, 18], [7, 77, 22, 92], [103, 31, 81, 106], [58, 48, 89, 49], [116, 43, 51, 83], [15, 51, 6, 26], [18, 105, 118, 123], [0, 18, 108, 83], [106, 63, 10, 17], [11, 33, 53, 87], [66, 43, 39, 54], [15, 64, 121, 79], [28, 0, 2, 110], [38, 110, 37, 98], [31, 37, 122, 34], [40, 42, 32, 56], [90, 40, 105, 90], [65, 39, 96, 52], [56, 111, 111, 66], [77, 24, 75, 91], [60, 26, 73, 38], [102, 5, 40, 7], [41, 10, 66, 116], [119, 18, 41, 23], [28, 102, 71, 37], [78, 99, 70, 105], [100, 4, 80, 47], [52, 105, 43, 104], [54, 83, 74, 113], [12, 87, 84, 20], [24, 96, 39, 63], [17, 44, 72, 124], [77, 121, 28, 100], [32, 94, 88, 114], [77, 51, 116, 63], [69, 125, 69, 45], [11, 108, 105, 57], [105, 15, 123, 19], [43, 17, 33, 75], [99, 116, 97, 106], [2, 82, 9, 6], [4, 21, 47, 39], [35, 23, 62, 110], [9, 77, 114, 45], [37, 112, 120, 92], [10, 115, 85, 90], [72, 96, 25, 15], [57, 59, 68, 121], [66, 33, 30, 91], [57, 111, 75, 78], [11, 29, 22, 120], [102, 20, 31, 46], [23, 41, 99, 109], [37, 88, 36, 88], [73, 33, 27, 16], [96, 70, 4, 121], [44, 33, 59, 28], [77, 112, 80, 34], [6, 6, 84, 122], [118, 64, 49, 68], [69, 4, 22, 96], [45, 6, 59, 73], [71, 98, 105, 3], [106, 118, 23, 26], [106, 60, 27, 116], [8, 38, 112, 63], [38, 123, 125, 108], [56, 57, 102, 20], [70, 92, 105, 95], [98, 51, 94, 62], [59, 75, 86, 26], [104, 125, 54, 37], [33, 24, 2, 46], [48, 37, 8, 113], [30, 6, 37, 21], [81, 104, 34, 83], [115, 117, 59, 3], [71, 41, 60, 105], [7, 31, 44, 7], [19, 4, 7, 56], [86, 70, 120, 112], [13, 70, 32, 1], [59, 23, 47, 75], [78, 125, 113, 111], [66, 54, 61, 122], [75, 41, 91, 89], [66, 82, 72, 1], [3, 73, 59, 19], [105, 81, 20, 42], [46, 75, 105, 101], [21, 106, 67, 7], [21, 99, 52, 21], [35, 119, 86, 60], [34, 104, 109, 86], [81, 92, 16, 70], [72, 16, 54, 94], [89, 21, 12, 99], [20, 119, 94, 95], [60, 26, 97, 114], [63, 35, 122, 66], [57, 33, 81, 47], [48, 101, 99, 9], [11, 6, 85, 118], [120, 88, 108, 8], [22, 36, 88, 64], [90, 103, 50, 11], [65, 21, 27, 73], [84, 85, 125, 92], [79, 45, 80, 1], [14, 25, 7, 59], [107, 16, 86, 109], [75, 106, 38, 30], [81, 74, 9, 81], [38, 13, 95, 41], [53, 46, 99, 121], [26, 103, 50, 11], [31, 34, 64, 68], [114, 104, 43, 60], [54, 50, 120, 113], [24, 8, 42, 116], [11, 1, 87, 92], [15, 114, 93, 108], [6, 96, 44, 4], [28, 100, 46, 59], [31, 76, 26, 46], [85, 67, 74, 122], [27, 3, 5, 41], [81, 102, 51, 85], [104, 124, 84, 25], [114, 58, 73, 62], [45, 30, 1, 101], [3, 91, 49, 6], [104, 87, 113, 21], [118, 0, 46, 57], [50, 62, 107, 32], [64, 124, 100, 79], [27, 101, 76, 110], [120, 20, 7, 124], [81, 118, 84, 126], [85, 96, 117, 100], [98, 44, 10, 42], [81, 5, 4, 122], [94, 46, 64, 28], [83, 0, 105, 32], [115, 56, 117, 46], [39, 108, 119, 107], [90, 77, 95, 102], [70, 18, 32, 91], [88, 57, 64, 24], [61, 99, 32, 56], [122, 16, 92, 114], [113, 113, 23, 10], [39, 89, 94, 65], [1, 25, 122, 117], [63, 8, 122, 64], [22, 8, 1, 66], [51, 49, 85, 119], [91, 121, 126, 75], [125, 107, 126, 31], [101, 48, 18, 4], [7, 50, 67, 106], [40, 126, 36, 50], [60, 26, 73, 38], [106, 3, 17, 27], [100, 42, 85, 21], [22, 5, 8, 63], [3, 11, 50, 35], [57, 61, 68, 85], [43, 31, 120, 0], [9, 101, 35, 72], [68, 105, 105, 121], [82, 120, 82, 53], [117, 70, 9, 80], [0, 58, 70, 17], [39, 83, 96, 111], [47, 78, 105, 120], [85, 57, 122, 40], [64, 13, 2, 60], [81, 120, 0, 25], [99, 80, 14, 31], [1, 60, 86, 25], [35, 107, 115, 28], [52, 101, 39, 72], [6, 77, 58, 80], [27, 124, 29, 87], [30, 46, 106, 22], [99, 77, 51, 93], [18, 95, 20, 99], [47, 102, 126, 35], [29, 80, 112, 120], [80, 9, 36, 44], [74, 14, 44, 103], [31, 66, 99, 20], [108, 76, 102, 112], [84, 105, 110, 14], [113, 41, 104, 123], [22, 15, 25, 94], [62, 8, 89, 90], [3, 83, 1, 93], [74, 96, 49, 30], [37, 112, 15, 48], [62, 52, 98, 60], [120, 50, 55, 37], [46, 25, 94, 26], [102, 41, 71, 36], [23, 82, 104, 91], [67, 78, 83, 83], [95, 123, 58, 106], [111, 92, 70, 96], [16, 99, 118, 88], [101, 31, 17, 76], [105, 67, 76, 111], [67, 60, 78, 123], [33, 50, 63, 20], [110, 3, 126, 37], [65, 73, 43, 113], [109, 88, 71, 104], [34, 39, 122, 75], [108, 102, 15, 93], [23, 62, 97, 108], [49, 50, 121, 35], [125, 61, 92, 119], [106, 37, 41, 21], [116, 60, 64, 75], [81, 36, 28, 28], [110, 120, 27, 105], [115, 11, 44, 13], [57, 13, 115, 89], [8, 43, 45, 79], [23, 116, 74, 110], [97, 33, 44, 116], [103, 19, 4, 42], [118, 104, 2, 54], [116, 92, 97, 125], [107, 5, 64, 41], [69, 20, 42, 71], [78, 74, 65, 107], [83, 50, 38, 114], [29, 93, 19, 119], [91, 58, 29, 21], [54, 30, 45, 65], [52, 91, 123, 39], [123, 122, 17, 55], [56, 54, 4, 56], [89, 76, 50, 122], [126, 100, 78, 35], [3, 111, 124, 57], [25, 48, 56, 104], [63, 59, 33, 96], [22, 87, 46, 25], [8, 56, 26, 36], [103, 96, 101, 80], [53, 3, 64, 5], [11, 79, 24, 18], [74, 94, 89, 1], [12, 120, 3, 42], [33, 1, 106, 58], [123, 63, 37, 4], [121, 126, 112, 88], [112, 36, 108, 87], [109, 23, 104, 95], [36, 111, 99, 80], [88, 65, 120, 71], [88, 106, 76, 112], [52, 57, 120, 126], [64, 22, 42, 46], [51, 35, 5, 33], [59, 41, 83, 33], [76, 31, 36, 91], [12, 49, 102, 92], [29, 3, 70, 70], [63, 84, 126, 1], [24, 23, 43, 44], [107, 9, 28, 47], [43, 91, 22, 14], [90, 0, 83, 84], [108, 108, 12, 73], [92, 86, 100, 77], [50, 56, 80, 12], [111, 36, 27, 25], [21, 20, 72, 58], [9, 79, 112, 107], [110, 74, 19, 38], [8, 38, 79, 6], [90, 13, 122, 123], [45, 115, 47, 14], [8, 12, 0, 45], [58, 117, 25, 68], [120, 47, 21, 67], [55, 13, 22, 54], [43, 108, 91, 87], [70, 97, 15, 29], [62, 74, 58, 122], [73, 88, 90, 78], [31, 13, 116, 23], [65, 75, 79, 123], [22, 61, 15, 80], [70, 101, 34, 26], [98, 5, 9, 23], [45, 68, 18, 92], [114, 112, 14, 40], [2, 82, 9, 6], [68, 108, 30, 48], [5, 80, 14, 49], [51, 123, 112, 61], [38, 45, 18, 21], [68, 59, 29, 0], [70, 101, 86, 11], [0, 58, 57, 125], [78, 56, 55, 38], [81, 40, 19, 5], [70, 50, 66, 82], [52, 0, 14, 96], [96, 49, 106, 107], [83, 13, 17, 71], [20, 32, 26, 0], [3, 109, 35, 107], [48, 86, 19, 6], [93, 85, 73, 51], [60, 33, 5, 124], [38, 43, 64, 28], [35, 38, 94, 105], [82, 87, 99, 106], [18, 74, 97, 79], [101, 20, 61, 89], [116, 79, 19, 62], [4, 79, 115, 116], [52, 99, 77, 125], [31, 49, 80, 47], [92, 82, 35, 32], [23, 59, 101, 102], [119, 56, 3, 98], [26, 89, 39, 72], [84, 68, 106, 83], [55, 55, 32, 73], [14, 61, 54, 88], [54, 11, 39, 34], [10, 90, 116, 47], [26, 12, 68, 68], [90, 94, 18, 15], [115, 124, 3, 59], [32, 0, 112, 53], [123, 38, 112, 11], [3, 57, 109, 124], [11, 108, 105, 57], [73, 126, 112, 21], [110, 70, 13, 19], [103, 126, 50, 10], [48, 106, 15, 51], [79, 2, 73, 10], [22, 89, 40, 52], [71, 75, 111, 81], [107, 22, 102, 50], [15, 6, 0, 120], [69, 69, 63, 63], [93, 39, 108, 101], [97, 39, 6, 49], [49, 92, 57, 22], [70, 61, 103, 123], [116, 64, 37, 73], [116, 79, 94, 16], [7, 117, 18, 35], [22, 47, 107, 38], [118, 10, 63, 96], [11, 69, 103, 14], [84, 99, 84, 98], [30, 92, 51, 97], [120, 121, 9, 76], [73, 25, 116, 14], [21, 21, 52, 78], [35, 124, 20, 89], [49, 29, 15, 39], [17, 4, 87, 108], [22, 43, 100, 72], [105, 104, 8, 88], [114, 71, 1, 7], [62, 46, 55, 112], [92, 44, 118, 81], [3, 67, 22, 5], [30, 35, 119, 70], [22, 3, 95, 18], [9, 94, 97, 52], [87, 53, 61, 66], [2, 68, 73, 24], [41, 126, 51, 56], [116, 124, 76, 45], [37, 79, 99, 26], [69, 23, 17, 109], [14, 82, 93, 32], [52, 119, 107, 41], [50, 58, 73, 62], [34, 79, 27, 42], [22, 48, 14, 55]]

:::

Vì không gian khóa khá nhỏ nên ta có thể brute-force được s. Khi đó ta có thể lấy được flag dễ dàng.

**Script**
```python!
import numpy as np
import itertools
from binteger import Bin

P=...
C=...

def dec(C, s, q):
    n = C.shape[0]
    M = np.zeros(n, dtype=np.int32)
    for i in range(n):
        inner_product = np.dot(C[i], s) % q
        M[i] = 1 if inner_product > q // 4 and inner_product < 3 * q // 4 else 0
    return M

q = 127
n = 3
N = int(1.1 * n * np.log(q))
sigma = 1.0

P = np.array(P, dtype=np.int32)
C = np.array(C, dtype=np.int32)

for ss in itertools.product(range(0, q // 2), repeat=3):
    s = np.concatenate([np.ones(1, dtype=np.int32), ss])
    e = P @ s % q
    e[e > q // 2] -= q
    if (e >= -2).all() and (e <= 2).all():
        print(s)
        break

M = dec(C, s, q)
print(Bin(M.tolist()).bytes)
# wwf{1f_y0u_5qu33z3_17_h4rd_3n0u6h_ju1c3_w1ll_c0m3_0u7}
```

