# Python Reversing

TJCTF 2018 - RE (40 pts).

# TJCTF 2018: Python Reversing

### Challenge details

Event Challenge Category Points Solves
TJCTF 2018 Python Reversing Reverse Engineering 40 221 solves

### Description

Found this flag checking file and it is quite vulnerable

### TL;DR

The script wasn’t really obfuscated. We had the output of the hashed flag and had to recover the input.
The clear text was first randomized with the equivalent of the following code:

``````flag = [random.randint(1,5) * ord(x) for x in flag]
``````

Then a xor was applied with the following key: `ligma_sugma_sugondese_`
Finally the result was printed in binary, letter could be 8 to 10 digits and were concatenated.

An efficient bruteforce / cryptanalysis gave us the flag.

### Methology

#### Looking at the files

The first thing we did was opening the source code source.py - md5: 544000ae1981ca703e22201fdbfedf14.

``````import numpy as np

flag = 'redacted'

np.random.seed(12345)
arr = np.array([ord(c) for c in flag])
other = np.random.randint(1,5,(len(flag)))
arr = np.multiply(arr,other)

b = [x for x in arr]
lmao = [ord(x) for x in ''.join(['ligma_sugma_sugondese_'*5])]
c = [b[i]^lmao[i] for i,j in enumerate(b)]
print(''.join(bin(x)[2:].zfill(8) for x in c))

# original_output was 1001100001011110110100001100001010000011110101001100100011101111110100011111010101010000000110000011101101110000101111101010111011100101000011011010110010100001100010001010101001100001110110100110011101
``````

By looking at the code we can see a little random, a xor and a convertion to binary. However by looking closely, the original_output size isn’t a multiple of 8, which mean there will be more bruteforce and cryptanalysis than expected.

#### Reversing

A code is better than a text so here is the commented reversed code

``````import numpy as np # Import numpy array library

flag = 'redacted' # Injection point
np.random.seed(12345) # Init random seed, no need to touch it
# String to list of ascii code: "abc" => [97,98,99]
arr = np.array([ord(c) for c in flag])
# Mask with integer in [1,5[ with the same size as flag
other = np.random.randint(1,5,(len(flag)))
# Array where ascii code is multiplied with the corresponding ranom int
arr = np.multiply(arr,other)

"""
Example here:
flag = redacted
arr = [114, 101, 100, 97, 99, 116, 101, 100]
other = [1,   1,   4,  1,  1,   1,   1,   2]  # Could be anything else
# After multiply:
arr = [114, 101, 400,  97,  99, 116, 101, 200]
"""

# At this point we got our input with random multiplication up to 5 * the original value
b = [x for x in arr] # Convert from numpy array to python array
# String to list of ascii code: convert ligma... key to list of integer for the xor
lmao = [ord(x) for x in ''.join(['ligma_sugma_sugondese_'*5])]
# c = XOR(arr,lmao)
c = [b[i]^lmao[i] for i,j in enumerate(b)]

# Print output
# /!\ While testing, bin(x)[2:].zfill(8) can be up to 10 digits !
print(''.join(bin(x)[2:].zfill(8) for x in c))

# original_output was 1001100001011110110100001100001010000011110101001100100011101111110100011111010101010000000110000011101101110000101111101010111011100101000011011010110010100001100010001010101001100001110110100110011101
``````

# Bruteforcing

Due to random multiplication, our algorithm isn’t determinist, we have to choose between differents letter when there is multiple possibility. I chose to bruteforce char by char, starting with empty flag and full output, and removing expected output progresivly:

## Step 1

``````flag = ""
original_output = "1001100001011110110100001100001010000011110101001100100011101111110100011111010101010000000110000011101101110000101111101010111011100101000011011010110010100001100010001010101001100001110110100110011101"

import string # import printable char
for l in string.printable: # for each printable char we test
for k in range(1,5): # Testing the differents multiplication from 1x to 4x
letter = (bin((ord(l)*k)^lmao[len(a)])[2:].zfill(8)) # xor and zfill
if outp.startswith(letter):
print(l+"  =>  "+letter)
``````
``````t  =>  100110000
z  =>  10011000
W  =>  100110000
=  =>  10011000
``````

I chose `t` for tjctf. We remove the corresponding binary from the beginning of the output, and append the letter to the flag

## Step 2

``````flag = "t"
original_output = "1011110110100001100001010000011110101001100100011101111110100011111010101010000000110000011101101110000101111101010111011100101000011011010110010100001100010001010101001100001110110100110011101"

import string # import printable char
for l in string.printable: # for each printable char we test
for k in range(1,5): # Testing the differents multiplication from 1x to 4x
letter = (bin((ord(l)*k)^lmao[len(a)])[2:].zfill(8)) # xor and zfill
if outp.startswith(letter):
print(l+"  =>  "+letter)
``````
``````5  =>  10111101
j  =>  10111101
``````

I chose `j` for tjctf.

## Step 6

``````flag = "tjctf"
original_output = "10101001100100011101111110100011111010101010000000110000011101101110000101111101010111011100101000011011010110010100001100010001010101001100001110110100110011101"

import string # import printable char
for l in string.printable: # for each printable char we test
for k in range(1,5): # Testing the differents multiplication from 1x to 4x
letter = (bin((ord(l)*k)^lmao[len(a)])[2:].zfill(8)) # xor and zfill
if outp.startswith(letter):
print(l+"  =>  "+letter)
``````
``````C  =>  101010011
R  =>  10101001
{  =>  10101001
``````

I chose `{` for tjctf{}.

Choosing between letters wasn’t too hard as we expected words such as “python”.

## Last Step

``````flag = "tjctf{pYth0n_1s_tr1v14l"
original_output = "110011101"

import string # import printable char
for l in string.printable: # for each printable char we test
for k in range(1,5): # Testing the differents multiplication from 1x to 4x
letter = (bin((ord(l)*k)^lmao[len(a)])[2:].zfill(8)) # xor and zfill
if outp.startswith(letter):
print(l+"  =>  "+letter)
``````
``````}  =>  110011101
``````

Flag !

# Flag

`tjctf{pYth0n_1s_tr1v14l}`

Zeecka