Python Reversing

TJCTF 2018 - Reverse Engineering (40 pts).

TJCTF 2018: Python Reversing

Challenge details

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

Download: source.py - md5: 544000ae1981ca703e22201fdbfedf14

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