Tags: crypto
Rating: 5.0
# CHALLENGE
A stream cipher in only 122 bytes!
Note: This has been tested on python versions 3.8 and 3.9
# WRITEUP
In this challenge we are given a python script `mingen.py` that produced an output `output.txt`
The script is written in only two lines, but takes a complex analysis. I will try to break it down in simple terms:
```Python
exec('def f(x):'+'yield((x:=-~x)*x+-~-x)%727;'*100)
g=f(id(f));print(*map(lambda c:ord(c)^next(g),list(open('f').read())))
```
The first line of the code is declaring a function 'f' taking an argument 'x' `def f(x)` which does some crazy math with you input 100 times. Therefore, it generates a 100-byte long generator item. In simple terms, you can compare this to a list of 100 items.
Then, the function is called using `id(f)` as parameter and stored in the variable `g`.
In simple terms, the function `id()` function returns the identity of an object. You could say that this is "random", since everytime you execute this script it will generate a different number.
So now we know that we are inputing a "random" number as x to `f(x)`, some black magic happens 100 times, and this "list" (generator) is stored in the variable `g`.
However, when you try to `print(g)`, you get something like `<generator object f at 0x7f834f0d3660>`. In order see the actual numbeers, we need to run `print(*g)`, this is because the `*` collects all the positional arguments in a tuple, similar how `for item in items` would collect every item in a list.
Let's move on with the code analysis:
this big part: `print(*map(lambda c:ord(c)^next(g),list(open('f').read())))` is doing the following:
1. opens the file "f" (we assume it's the flag file), reads it, and returns it as a list, so that every character of the file is now a list item.
2. stores the flag in the `c` variable, which is done using `lambda c`.
3. xor each flag character with each number in the `g` variable.
I'll rewrite the whole code now in simple terms:
```Python
def f(x):
my_list = []
for i in range(100):
x = ((-~x)*x+-~-x)%727 #I didn't actually verify if this works, think of as pseudo-code just to give a basic idea of what's happening
my_list.append(x)
return my_list
flag = open('f').read()
output = []
random_number = id(f)
g = f(random_number)
for i in range(len(flag)):
output.append(ord(flag[i]) ^ g[i])
print(output)
```
Now, the thing about xor encryption is that it can be easily reversed. This is because of the following:
a ^ b = c
c ^ a = b
c ^ b = a
We already know our c, because that's what's in the `output.txt` file given.
We know that a is supposed to be the real flag. The real flag always starts with `rarctf{`
We know that b is the generator object stored in the `g` variable.
So we can take the first 7 elements of our `output.txt` and xor with `rarctf{`. Hence c ^ a = b.
```Python
output = [281, 547, 54, 380, 392, 98, 158, 440, 724, 218, 406, 672, 193, 457, 694, 208, 455, 745, 196, 450, 724]
flag = 'rarctf{'
print("xor result of flag and output:")
for i in range(len(flag)):
print(ord(flag[i]) ^ output[i], end = ' ')
```
result = 363 578 68 287 508 4 229
Now let's brute force the `f(x)` to see what input we need to return a sequence that starts in those numbers:
```Python
print("number for x that will have g start in 363")
for i in range(1000):
g = f(i)
if next(g) == 363:
print(i, end = ' ')
```
result = 256 470 983
So these numbers will generate a sequence that starts in 363, just like our "c ^ a" result above. One of these numbers might generate a sequence that's identical to what we're looking for. Let's test them:
```Python
x = 256
print(f"Next 6 digits in g for x starting in {x}")
g = f(x)
for i in range(7):
print(next(g), end = ' ')
```
result = 363 150 666 457 250 45 569
Not what we're looking for, let's move on:
```Python
x = 470
print(f"Next 6 digits in g for x starting in {x}")
g = f(x)
for i in range(7):
print(next(g), end = ' ')
```
result = 363 578 68 287 508 4 229
Bingo!
This matches the result of output ^ flag
Now that we know that `470` will generate the list we want, we only need to xor it with the output to get the flag:
```Python
g = f(x)
print("xor output with g to get the flag")
for n in output:
print(chr(n ^ next(g)), end = '')
```
result = **rarctf{pyg01f_1s_fun}**
Full code:
```Python
exec('def f(x):'+'yield((x:=-~x)*x+-~-x)%727;'*100)
print("number for x that will have g start in 363")
for i in range(1000):
g = f(i)
if next(g) == 363:
print(i, end = ' ')
print("\n")
x = 470
print(f"Next 6 digits in g for x starting in {x}")
g = f(x)
for i in range(7):
print(next(g), end = ' ')
print("\n")
output = [281, 547, 54, 380, 392, 98, 158, 440, 724, 218, 406, 672, 193, 457, 694, 208, 455, 745, 196, 450, 724]
flag = 'rarctf{'
print("xor result of flag and output:")
for i in range(len(flag)):
print(ord(flag[i]) ^ output[i], end = ' ')
print("\n")
g = f(x)
print("xor output with g")
for n in output:
print(chr(n ^ next(g)), end = '')
print("\n")
```
Alternative two-line solution:
```Python
exec('def f(x):'+'yield((x:=-~x)*x+-~-x)%727;'*100)
g=f(470);print(*map(lambda c:chr(int(c)^next(g)),open('output.txt').read().split(' ')))
```