AmateursCTF 2023 - lce cream generator Writeup

Writeup for the Ice cream generator Cryptography challenge.

Note
This writeup was one of the 5 winners of Amateurs CTF writeups prize

Overview

This was the first edition of Amateurs CTF and I absolutely loved the challenges. Managed to solve 7/12 crypto challenges. They were really fun and educational. But Ice cream generator was the most beautiful of them all (also my favorite problem of this year so far) 😍. I plan to make a detailed writeup of it.

The challenge

Challenge Information
  • Points: 495
  • Description: Ice cream. Or wait… Ice cream. Same difference.

nc amt.rs 31310

main.py

Let’s take a look at the given code (you can skip it though, looks scary).

#!/usr/local/bin/python
from Crypto.Util.number import *
from os import urandom
from flag import flag

class lcg:
    def __init__(self, p):
        while (a:=bytes_to_long(urandom(16))) > p:
            pass
        while (b:=bytes_to_long(urandom(16))) > p:
            pass
        self.a, self.b, self.p = a, b, p
    
    seed = 1337

    def gen_next(self):
        self.seed = (self.a*self.seed + self.b) % self.p
        return self.seed

class order:
    def __init__(self, p):
        self.p = p

        self.inner_lcg = lcg(p)
    
        for i in range(1337):
            self.inner_lcg.gen_next()

        self.flavors = [self.inner_lcg.gen_next() for i in range(1338)]

        self.flavor_map = {i:self.flavors[i] for i in [1,2,3,4,5,6]}
        self.private = {i:self.flavors[i] for i in [1,2,3,4,5,6,1337]}
    
    bowls = [0, 0, 0]
    used = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0}
    recipe = []

    def make_bowl(self):
        global flavor_indices
        self.bowls = [0, 0, 0]
        self.recipe = []
        new = {}
        available = []
        for i, n in self.used.items():
            if n == 0:
                new[i] = 0
                available += [i]
        self.used = new
        print("\nREMAINING FLAVORS: ")
        for i in available:
            print(f"Flavor {i} - {flavor_indices[i]}")
        while True:
            command = input("\nAdd, combine, or finish? ")
            if command.lower() == "add":
                try:
                    add = input("\nGive a flavor and a bowl: ").rsplit(" ", 1)
                    self.add(*add)
                except Exception as e:
                    print()
                    print(e)
            elif command.lower() == "combine":
                try:
                    combination = input("\nGive two bowls and an operation: ").split()
                    assert len(combination) == 3, "Invalid Input Length"
                    self.combine_bowl(*combination)
                except Exception as e:
                    print()
                    print(e)
            elif command.lower() == "finish bowl":
                self.finish_bowl()
            elif command.lower() == "finish":
                self.finish()
                break
            elif command.lower() == "exit":
                exit(1)
            else:
                print("\nPlease give a valid input.")
            

    def mod(self):
        self.bowls = [i % self.p for i in self.bowls]

    def add(self, flavor, bowl):
        assert "0" < bowl < "4", "Invalid Bowl"
        bowl = int(bowl) - 1
        global flavor_names
        if flavor not in ["1", "2", "3", "4", "5", "6"]:
            try:
                if self.used[flavor_names[flavor]] < 5:
                    self.bowls[bowl] += self.flavor_map[flavor_names[flavor]]
                    self.used[flavor_names[flavor]] += 1
                    self.recipe += [[flavor_names[flavor], bowl]]
                else:
                    print(f"\nCannot order {flavor} due to stock issues.")
            except:
                print("\nInvalid Flavor")
        else:
            try:
                flavor = int(flavor)
                if self.used[flavor] < 5:
                    self.bowls[bowl] += self.flavor_map[flavor]
                    self.used[flavor] += 1
                    self.recipe += [[flavor, bowl]]
                else:
                    print(f"\nCannot order {flavor} due to stock issues.")
            except:
                print("\nInvalid Flavor")
    
    def combine_bowl(self, a, b, op):
        assert op in ['add', 'sub', 'mult', 'div'], "Invalid Operation. Please choose either 'add', 'sub', 'mult', or 'div'."
        assert "0" < a < "4" and "0" < b < "4" and a != b, "Invalid Bowl"
        a = int(a) - 1
        b = int(b) - 1
        if op == 'add':
            self.bowls[a] += self.bowls[b]
        elif op == 'sub':
            self.bowls[a] -= self.bowls[b]
        elif op == 'mult':
            self.bowls[a] *= self.bowls[b]
        elif op == 'div':
            assert self.bowls[b] != 0, "Empty Bowl for Division"
            self.bowls[a] *= pow(self.bowls[b], -1, self.p)
        else:
            print("\nwtf")
            exit(1)
        self.recipe += [[op, a, b]]
        self.bowls[b] = 0
        self.mod()
    
    def finish_bowl(self):
        unique = 0
        for i, n in self.used.items():
            if n and n != 1337:
                unique += 1
        if unique < min(3, len(self.used)):
            print("\nAdd more flavor!")
            return False
        recipe = str(self.recipe).replace(' ', '')
        signature = sum(self.bowls) % self.p
        self.bowls = [0, 0, 0]
        self.recipe = []
        for i in self.used:
            if self.used[i]:
                self.used[i] = 1337
        print(f"\nUser #: {self.p}")
        print(f"\nRecipe: \n{recipe}")
        print(f"\n\nSignature: \n{signature}")
        return True
    
    def finish(self):
        if sum(self.bowls):
            if not self.finish_bowl():
                print("\nOk the bowls will be dumped.")
        print("\nOrder done!")
        return True

    def verify(self, recipe, signature):
        bowls = [0, 0, 0]
        for i in recipe:
            try:
                if len(i) == 2:
                    bowls[i[1]] += self.private[i[0]]
                elif len(i) == 3:
                    if i[0] == 'add':
                        bowls[i[1]] += bowls[i[2]]
                    elif i[0] == 'sub':
                        bowls[i[1]] -= bowls[i[2]]
                    elif i[0] == 'mult':
                        bowls[i[1]] *= bowls[i[2]]
                    elif i[0] == 'div':
                        bowls[i[1]] *= pow(bowls[i[2]], -1, self.p)
                    bowls[i[2]] = 0
                bowls = [i % self.p for i in bowls]
            except:
                exit("\nInvalid Recipe")
        try:
            assert sum(bowls) % self.p == signature, "\nInvalid Signature"
            print("\nYou have successfully redeemed your lce cream!")
            if signature == self.private[1337]:
                print(flag)
        except Exception as e:
            print(e)
        

flavor_names = {"revanilla":1, "cryptolatte":2, "pwnstachio":3, "strawebrry":4, "miscnt":5, "cookie dalgo":6, "flaudge chocolate":1337}
flavor_indices = {i:n for n, i in flavor_names.items()}

intro = \
"""
----------------------------------------------------
            WELCOME TO THE LCE CREAM SHOP!          
----------------------------------------------------
  HERE AT THE LCE CREAM SHOP WE HAVE A FEW BELIEFS  

 1. Don't be boring! Choose at least 3 flavors of lce cream. All of it tastes the same anyways...
 2. Don't be repetitive! Well... that and the fact that we have some stock issues. After getting one lce cream with one flavor, you don't get to choose that flavor again.
 3. Since I rolled my own signature system that is extremely secure, if you can manage to forge an arbitrary flavor, I'll give it to you! As long as it exists...
 4. These aren't really beliefs anymore but we only have 6 flavors (available to the customer), and you're only allowed to order once (stock issues again smh). Choose wisely!
 5. To help with the boringness, I will allow you to mix flavors in any way you want. But you can only use up to 5 scoops of each flavor to concoct your lce cream (once again stock issues).
 6. I AM ONLY ACCEPTING ONE RECIEPT. If the first fails, too bad.
 7. I heard there's a special flavor called "flaudge chocolate", it's like the 1337th flavor or something.
 8. Orders can have multiple lce cream mixtures, as long as they follow the rules above.
 9. I am accepting reciepts for TAX PURPOSES only.
10. Each scoop costs $5 (stock issues AGAIN).
11. The reciept itself costs $1.
12. Everything is free. Have fun!
13. Zero indexing sucks. Here at LCE CREAM SHOP we use one indexing.

Oh yeah here are the options:"""

options = \
"""
OPTIONS:
(1) Generate order
(2) View flavors
(3) Redeem a reciept
(4) Exit
Choice: """

print(intro)

while True:
    choice = input(options)
    if choice == "1":
        if 'user' in vars():
            print("\nYou already ordered.")
            continue
        user = order(getPrime(128))
        user.make_bowl()
        print()
    elif choice == "2":
        print("\nThe only valid flavors are: ")
        [print(f"Flavor {i} - {n}") for i, n in flavor_indices.items() if i != 1337]
    elif choice == "3":
        if 'user' not in vars():
            print("\nNo user.")
        else:
            userid = int(input("\nENTER NUMBER: "))
            assert userid == user.p, "You seem to have lost your reciept."
            recipe = input("\nENTER RECIPE: ")
            assert all([i in "[,]01234567abdilmstuv'" for i in recipe]), "\n\nSir, please don't put junk in my lce cream machine!"
            recipe = eval(recipe, {"__builtins__": {}}, {"__builtins__": {}}) # screw json or ast.literal_eval
            signature = input("\nENTER SIGNATURE: ")
            user.verify(recipe, int(signature))
            exit("\nGoodbye.")
    elif choice == "4":
        exit("\nGoodbye.")
    else:
        print("\nINVALID CHOICE. Please input '1', '2', '3', or '4'")

At first glance, this seems to be a huge code with over 200 lines 😨. I immediately dropped the problem, thinking it was overcomplicated for my level, and also because it only had like 2 solves at that moment. I returned to this problem almost a day later, when someone on the discord mentioned that this was much simpler than it looks.

At its core, this is a very simple LCG problem. I would explain what it is, in a short while. What makes this problem special is how we have to crack the generator without having access to the generated random numbers(unlike other easy LCG problems, where we can just spam some template).

What is LCG?

LCG or Linear Congruential Generator is an algorithm to generate Pseudo Random Numbers using linear equations. The underlying algorithm is very simple to understand. We can represent it using a recurrence relation like this, $$X_{n+1} \ = \ aX_n + c \mod\ m$$

$X_0$ is the seed, used to generate the other random numbers $X_1, X_2, …, X_n$

$a$ is the multiplier

$c$ is the increment

$m$ is the modulus (though it’s denoted by $p$ in this problem)

Note that all values are bounded by the modulus $m$.

A simple python implementation of LCG follows

class LCG:
    def __init__(self, a, c, m, seed):
        self.a, self.c, self.p, self.seed = a, c, m, seed

    def gen_next(self):
        self.seed = (self.a*self.seed + self.c) % self.m
        return self.seed

How to crack LCG?

There can be different variations to the problems related to cracking LCG. Some give extra information, some go on to even truncate bits from the random numbers to hide information.

The most common variant is to give some generated numbers (usually 6 numbers is enough to recover all the parameters), and one has to find the a, c, m, seed from those values. A very beautiful writeup explaining it in detail goes here.

In our current problem, m and seed is already given, making it much easier to solve. We have to recover a and b.

Let’s say we have 3 generated numbers $X_1, X_2, X_3$. $$X_2 \ = \ aX_1 + c\mod\ m$$ $$X_3 \ = \ aX_2 + c\mod\ m$$

Subtracting the first equation from the second gives us, $$X_3 - X_2 \ = \ a(X_2 - X_1) \mod\ m$$ $$a \equiv\ \frac{X_3 - X_2}{X_2 - X_1} \mod\ m$$

So easy to recover a 😆. It’s trivial to recover c now. $$X_2 \ = \ aX_1 + c\mod\ m$$ $$c \equiv\ X_2 - aX_1 \mod\ m$$

Let’s dive into the given code

It was difficult trying to understand the monstrosity of a code. Let us try to understand the code function by function.

The code starts with the LCG class, meaning we have to crack LCG somehow. But where are the generated random numbers? A quick check reveals that under Order class, 1337 random numbers are generated through the LCG and dropped. The later 1338 numbers($X_{1338}$ to $X_{2675}$) are generated and stored in the flavors array. 2 corresponding dictionaries (flavor_map and private) are generated using values of the flavors array.

self.flavors = [self.inner_lcg.gen_next() for i in range(1338)]

self.flavor_map = {i:self.flavors[i] for i in [1,2,3,4,5,6]}
self.private = {i:self.flavors[i] for i in [1,2,3,4,5,6,1337]}

Some other variables are also there like,

bowls: A list of 3 bowls containing 0.

used: A dictionary showing how many times each flavor is used.

recipe: A list of what flavors used so far.

When the code is run, the following portion comes first:

options = \
"""
OPTIONS:
(1) Generate order
(2) View flavors
(3) Redeem a reciept
(4) Exit
Choice: """

print(intro)

while True:
    choice = input(options)
    if choice == "1":
        if 'user' in vars():
            print("\nYou already ordered.")
            continue
        user = order(getPrime(128))
        user.make_bowl()
        print()
    elif choice == "2":
        print("\nThe only valid flavors are: ")
        [print(f"Flavor {i} - {n}") for i, n in flavor_indices.items() if i != 1337]
    elif choice == "3":
        if 'user' not in vars():
            print("\nNo user.")
        else:
            userid = int(input("\nENTER NUMBER: "))
            assert userid == user.p, "You seem to have lost your reciept."
            recipe = input("\nENTER RECIPE: ")
            assert all([i in "[,]01234567abdilmstuv'" for i in recipe]), "\n\nSir, please don't put junk in my lce cream machine!"
            recipe = eval(recipe, {"__builtins__": {}}, {"__builtins__": {}}) # screw json or ast.literal_eval
            signature = input("\nENTER SIGNATURE: ")
            user.verify(recipe, int(signature))
            exit("\nGoodbye.")
    elif choice == "4":
        exit("\nGoodbye.")
    else:
        print("\nINVALID CHOICE. Please input '1', '2', '3', or '4'")

We have to give 1 as our choice since a user must be created first. Let’s see the functionalities of this make_bowl function that is called after the user is created.

make_bowl

while True:
    command = input("\nAdd, combine, or finish? ")
    if command.lower() == "add":
        try:
            add = input("\nGive a flavor and a bowl: ").rsplit(" ", 1)
            self.add(*add)
        except Exception as e:
            print()
            print(e)
    elif command.lower() == "combine":
        try:
            combination = input("\nGive two bowls and an operation: ").split()
            assert len(combination) == 3, "Invalid Input Length"
            self.combine_bowl(*combination)
        except Exception as e:
            print()
            print(e)
    elif command.lower() == "finish bowl":
        self.finish_bowl()
    elif command.lower() == "finish":
        self.finish()
        break
    elif command.lower() == "exit":
        exit(1)
    else:
        print("\nPlease give a valid input.")
  • We can choose from 4 options: add, combine, finish bowl, and finish.
  • Depending on the chosen option, the effect on the bowl varies.

add

try:
    flavor = int(flavor)
    if self.used[flavor] < 5:
        self.bowls[bowl] += self.flavor_map[flavor]
        self.used[flavor] += 1
        self.recipe += [[flavor, bowl]]
    else:
        print(f"\nCannot order {flavor} due to stock issues.")
except:
    print("\nInvalid Flavor")
  • We can add any of the 6 flavors to any of the 3 bowls.
  • Adding a flavor means adding a value from the flavor_map dictionary. That’s how we can leak values from the flavor list (random numbers generated from the LCG).
  • But we cannot use a flavor more than 4 times.

combine_bowl

a = int(a) - 1
b = int(b) - 1
if op == 'add':
    self.bowls[a] += self.bowls[b]
elif op == 'sub':
    self.bowls[a] -= self.bowls[b]
elif op == 'mult':
    self.bowls[a] *= self.bowls[b]
elif op == 'div':
    assert self.bowls[b] != 0, "Empty Bowl for Division"
    self.bowls[a] *= pow(self.bowls[b], -1, self.p)
else:
    print("\nwtf")
    exit(1)
self.recipe += [[op, a, b]]
self.bowls[b] = 0
  • We can perform addition, subtraction, multiplication, or division operation between 2 bowls.
  • The catch is that after an operation is done, the 2nd bowl is set to 0.

finish_bowl

unique = 0
for i, n in self.used.items():
    if n and n != 1337:
        unique += 1
if unique < min(3, len(self.used)):
    print("\nAdd more flavor!")
    return False
recipe = str(self.recipe).replace(' ', '')
signature = sum(self.bowls) % self.p
self.bowls = [0, 0, 0]
self.recipe = []
for i in self.used:
    if self.used[i]:
        self.used[i] = 1337
print(f"\nUser #: {self.p}")
print(f"\nRecipe: \n{recipe}")
print(f"\n\nSignature: \n{signature}")
return True
  • Prints p which is the modulus of the LCG.
  • Prints recipe, although it has no use.
  • Prints signature. Here signature denotes the sum of 3 bowls.
  • The unique variable ensures we use 3 different flavors that we haven’t used before.
  • Once a flavor is used, its use count is set to 1337, so that if we use it again, it won’t contribute to uniqueness.

verify

bowls = [0, 0, 0]
for i in recipe:
    try:
        if len(i) == 2:
            bowls[i[1]] += self.private[i[0]]
        elif len(i) == 3:
            if i[0] == 'add':
                bowls[i[1]] += bowls[i[2]]
            elif i[0] == 'sub':
                bowls[i[1]] -= bowls[i[2]]
            elif i[0] == 'mult':
                bowls[i[1]] *= bowls[i[2]]
            elif i[0] == 'div':
                bowls[i[1]] *= pow(bowls[i[2]], -1, self.p)
            bowls[i[2]] = 0
        bowls = [i % self.p for i in bowls]
    except:
        exit("\nInvalid Recipe")
try:
    assert sum(bowls) % self.p == signature, "\nInvalid Signature"
    print("\nYou have successfully redeemed your lce cream!")
    if signature == self.private[1337]:
        print(flag)
except Exception as e:
    print(e)
  • Out given recipe is simulated and stored in the bowls.
  • After the simulation is complete, if the 3 bowls sum to private[1337], we get the flag. That means we have to somehow recover the flavors list. Only then we can know what value private[1337] contains since flavors[1337] = private[1337].

My initial incorrect approach

  • Add a flavor to a bowl.
  • Print the value on that bowl using finish bowl.
  • Repeat the above 2 steps to leak all 6 flavors.
  • Then use those values to crack the LCG.

But the problem with this idea is that we have to use at least 3 flavors to use finish bowl. If we do that, we will get a sum of 3 flavors. This does not contribute much to leaking the random values.

Correct approach

We will leak the p, a, and c step-by-step. Let flavor_map = $[X_1, X_2, X_3, X_4, X_5, X_6]$.

The initial state of the bowls $[b_1, b_2, b_3]$ is:

$$\underbrace{0}_{b_1} \ \ \underbrace{0}_{b_2} \ \ \underbrace{0}_{b_3}$$

Recovering a

  • move $X_2$ to $b_1$ $$\underbrace{X_2}_{b_1} \ \ \underbrace{0}_{b_2} \ \ \underbrace{0}_{b_3} $$
  • move $X_3$ to $b_2$ $$\underbrace{X_2}_{b_1} \ \ \underbrace{X_3}_{b_2} \ \ \underbrace{0}_{b_3} $$
  • subtract $b_2$ from $b_1$ $$\underbrace{X_2 - X_3}_{b_1} \ \ \underbrace{0}_{b_2} \ \ \underbrace{0}_{b_3} $$
  • move $X_1$ to $b_2$ $$\underbrace{X_2 - X_3}_{b_1} \ \ \underbrace{X_1}_{b_2} \ \ \underbrace{0}_{b_3} $$
  • move $X_2$ to $b_3$ $$\underbrace{X_2 - X_3}_{b_1} \ \ \underbrace{X_1}_{b_2} \ \ \underbrace{X_2}_{b_3} $$
  • subtract $b_3$ from $b_2$ $$\underbrace{X_2 - X_3}_{b_1} \ \ \underbrace{X_1 - X_2}_{b_2} \ \ \underbrace{0}_{b_3} $$
  • divide $b_1$ by $b_2$ $$\underbrace{\frac{X_2 - X_3}{X_1 - X_2} \mod\ p}_{b_1} \ \ \underbrace{0}_{b_2} \ \ \underbrace{0}_{b_3} $$

Bowl 1 now contains the value of a. Since we have used 3 flavors, this is unique enough to use finish bowl. The signature will be the value of a.

Recovering c

Since we have already used $X_1, X_2, X_3$, using them again won’t contribute anything to uniqueness. So we are going to use $X_4, X_5, X_6$ instead to recover c.

  • move $X_5$ to $b_1$ $$\underbrace{X_5}_{b_1} \ \ \underbrace{0}_{b_2} \ \ \underbrace{0}_{b_3} $$
  • move $X_6$ to $b_2$ $$\underbrace{X_5}_{b_1} \ \ \underbrace{X_6}_{b_2} \ \ \underbrace{0}_{b_3} $$
  • subtract $b_2$ from $b_1$ $$\underbrace{X_5 - X_6}_{b_1} \ \ \underbrace{0}_{b_2} \ \ \underbrace{0}_{b_3} $$
  • move $X_4$ to $b_2$ $$\underbrace{X_5 - X_6}_{b_1} \ \ \underbrace{X_4}_{b_2} \ \ \underbrace{0}_{b_3} $$
  • move $X_5$ to $b_3$ $$\underbrace{X_5 - X_6}_{b_1} \ \ \underbrace{X_4}_{b_2} \ \ \underbrace{X_5}_{b_3} $$
  • subtract $b_3$ from $b_2$ $$\underbrace{X_5 - X_6}_{b_1} \ \ \underbrace{X_4 - X_5}_{b_2} \ \ \underbrace{0}_{b_3} $$
  • divide $b_1$ by $b_2$ $$\underbrace{\frac{X_5 - X_6}{X_4 - X_5} \mod\ p}_{b_1} \ \ \underbrace{0}_{b_2} \ \ \underbrace{0}_{b_3} $$ $$\underbrace{a}_{b_1} \ \ \underbrace{0}_{b_2} \ \ \underbrace{0}_{b_3}$$
  • move $X_4$ to $b_2$ $$\underbrace{a}_{b_1} \ \ \underbrace{X_4}_{b_2} \ \ \underbrace{0}_{b_3}$$
  • multiply $b_2$ with $b_1$ $$\underbrace{0}_{b_1} \ \ \underbrace{aX_4}_{b_2} \ \ \underbrace{0}_{b_3}$$
  • move $X_5$ to $b_1$ $$\underbrace{X_5}_{b_1} \ \ \underbrace{aX_4}_{b_2} \ \ \underbrace{0}_{b_3}$$
  • subtract $b_2$ from $b_1$ $$\underbrace{X_5 - aX_4}_{b_1} \ \ \underbrace{0}_{b_2} \ \ \underbrace{0}_{b_3}$$

So bowl 1 now has c. Using finish bowl will give c as the signature.

Recovering the flavors list

We can simulate the generation process to get the required sign and the flavors list.

lcg = LCG(a, c, p)

for i in range(1337):
    lcg.gen_next()

flavors = [lcg.gen_next() for i in range(1338)]
sign = flavors[1337]

Retrieving the flag

Since we have recovered the target signature, we send it to the verify function using Option 3. As a recipe, we send [[1337,0]]. Then after the simulation is done in the verify function, the state of the 3 bowls will be: $$\underbrace{private[1337]}_{b_1} \ \ \underbrace{0}_{b_2} \ \ \underbrace{0}_{b_3}$$ The sum of the 3 bowls will hence be, private[1337] which is what we need to recover the flag.

Flag: amateursCTF{bruh_why_would_you_use_lcg_for_signature}

What a beautiful problem, innit?

Solution script

from pwn import *

class LCG:
    def __init__(self, a, c, p):
        self.a, self.c, self.p = a, c, p
    
    seed = 1337

    def gen_next(self):
        self.seed = (self.a*self.seed + self.c) % self.p
        return self.seed

io = remote('amt.rs', 31310)

def add(a, b):
    io.recvuntil(b'finish?')
    io.sendline(b'add')
    io.recvuntil(b'bowl: ')
    to_send = ' '.join([str(i) for i in [a, b]])
    io.sendline(to_send.encode())

def combine_bowl(a, b, op):
    io.recvuntil(b'finish?')
    io.sendline(b'combine')
    io.recvuntil(b'operation: ')
    to_send = ' '.join([str(i) for i in [a, b, op]])
    io.sendline(to_send.encode())

def finish_bowl():
    io.recvuntil(b'finish?')
    io.sendline(b'finish bowl')
    io.recvline()
    p = int(io.recvline().decode().strip().split(': ')[1])
    io.recvuntil(b'Signature:')
    io.recvline()
    sign = int(io.recvline().decode().strip())
    return p, sign

def finish():
    io.recvuntil(b'finish?')
    io.sendline(b'finish')
    io.recvuntil(b'OPTIONS:')
    

io.recvuntil(b'Choice: ')
io.sendline(b'1')

#recover a, p
add('2', '1')
add('3', '2')
combine_bowl('1', '2', 'sub')
add('1', '2')
add('2', '3')
combine_bowl('2', '3', 'sub')
combine_bowl('1', '2', 'div')
p, a = finish_bowl()

print(a, p)

#recover c, p
add('5', '1')
add('6', '2')
combine_bowl('1', '2', 'sub')
add('4', '2')
add('5', '3')
combine_bowl('2', '3', 'sub')
combine_bowl('1', '2', 'div')
add('4', '2')
combine_bowl('2', '1', 'mult')
add('5', '1')
combine_bowl('1', '2', 'sub')
p, c = finish_bowl()
print(c, p)

finish()

lcg = LCG(a, c, p)

for i in range(1337):
    lcg.gen_next()

flavors = [lcg.gen_next() for i in range(1338)]
sign = flavors[1337]

io.recvuntil(b'Choice: ')
io.sendline(b'3')

io.recvuntil(b'NUMBER: ')
io.sendline(str(p).encode())

io.recvuntil(b'RECIPE: ')
io.sendline(b'[[1337,0]]')
io.recvuntil(b'SIGNATURE: ')
io.sendline(str(sign).encode())

io.interactive()
# amateursCTF{bruh_why_would_you_use_lcg_for_signature}

'''
Algortihm to recover a:
-------------------------

(x2-x3)/(x1-x2)

1) mov x2 to b1
2) mov x3 to b2
3) b1 - b2
4) mov x1 to b2
5) mov x2 to b3
6) b2 - b3
7) b1 / b2

Algorithm to recover b:
------------------------

x5 - x4 * a

x5 - x4 * (x5 - x6)/(x4 - x5)

1) mov x5 to b1
2) mov x6 to b2
3) b1 - b2
4) mov x4 to b2
5) mov x5 to b3
6) b2 - b3
7) b1 / b2
8) mov x4 to b2
9) b2 * b1
10) mov x5 to b1
11) b1 - b2

'''