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}