TJCTF 2018: Python Reversing
Détails du challenge
| Event | Challenge | Category | Points | Solves |
|---|---|---|---|---|
| TJCTF 2018 | Python Reversing | Reverse Engineering | 40 | 221 solves |
Téléchargement : source.py - md5: 544000ae1981ca703e22201fdbfedf14
Description
J’ai trouvé ce fichier de vérification de flag et il est plutôt vulnérable
TL;DR
Le script n’était pas vraiment obfusqué. Nous avions l’output du flag hashé et
devions retrouver l’input.
Le texte en clair était d’abord randomisé avec l’équivalent du code suivant :
flag = [random.randint(1,5) * ord(x) for x in flag]
Ensuite un xor était appliqué avec la clé suivante : ligma_sugma_sugondese_
Enfin le résultat était affiché en binaire, chaque lettre pouvait faire 8 à 10 chiffres et était concaténée.
Un bruteforce / une cryptanalyse efficace nous a donné le flag.
Méthodologie
Analyse des fichiers
La première chose que nous avons faite a été d’ouvrir le code source 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
En regardant le code, on peut voir un peu d’aléatoire, un xor et une conversion en binaire. Cependant, en regardant de plus près, la taille de original_output n’est pas un multiple de 8, ce qui signifie qu’il y aura plus de bruteforce et de cryptanalyse que prévu.
Reversing
Un code vaut mieux qu’un texte, voici donc le code reversé commenté
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
À cause de la multiplication aléatoire, notre algorithme n’est pas déterministe, nous devons choisir entre différentes lettres lorsqu’il y a plusieurs possibilités. J’ai choisi de bruteforcer caractère par caractère, en partant d’un flag vide et de l’output complet, et en retirant progressivement l’output attendu :
Étape 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
J’ai choisi t pour tjctf. On retire le binaire correspondant du début de
l’output, et on ajoute la lettre au flag
Étape 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
J’ai choisi j pour tjctf.
Étape 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
J’ai choisi { pour tjctf{}.
…
Choisir entre les lettres n’était pas trop difficile car on s’attendait à des mots comme « python ».
Dernière étape
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}