Python Reversing

Author: GabiTulba
Contest: TJCTF 2018

Problem statement:

Found this flag checking file and it is quite vulnerable

My opinion:

I still don’t understand why this problem was tagged as Reverse Engineering, it was a simple cryptography problem with a classic attack, in my opinion it was way easier than Caesar’s Complication, the number of solves talk for themselves.

Understanding the encryption

We were given the following code:

import numpy as np

flag = 'redacted'

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

Ok, let’s see what’s happening step by step:

  1. The flag is transformed into an array called arr.
  2. Then another array otherof the same length as arr with random values from 1 to 4 is generated using a constant seed (clearly a bad idea since now the encryption is deterministic).
  3. The array arr is multiplied with the array other by arr[i]*=other[i].
  4. Then the array b=arr is xored element by element with the array lmao.
  5. The encrypted flag is the concatenation of the binary representation of every value in array c=b.

One thing that makes this problem a little harder (the encryption isn’t easily reversable) is that when the array arr is multiplied with the array other, in some cases the result is bigger than 256. I saw that when I saw that the length of the output wasn’t divisible by 8:

>>> <br> x='1001100001011110110100001100001010000011110101001100100011101111110100011111010101010000000110000011101101110000101111101010111011100101000011011010110010100001100010001010101001100001110110100110011101'
>>> len(x)%8

But still, since we know every variable (including other) we can ecnrypt, so we can guess the flag character by character by encrypting every possible byte and checking if it matches the desired output, this is known as a Chosen-plaintext attack.

Decrypting the flag:

Here’s the code:

import numpy as np

flag= ''

def encrypt(flag):
	arr = np.array([ord(c) for c in flag])
	other = np.random.randint(1,5,len(arr))
	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)]
	y=(''.join(bin(x)[2:].zfill(8) for x in c))
	while(p<len(y) and enc[p]==y[p]):
	return p
	for j in range(256):
		if(L<=encrypt(flag+chr(j))-8): #at least 8 new bits have to mach the output
			print L
print flag

NOTE : It just happened for that my solution worked, since there’s not necesarily a one to one mapping to plaintext-ciphertext. Flag: tjctf{pYth0n_1s_tr1v14l}