Zeecka zeecka

U_u

Aperi'CTF 2019 - Reverse (200 pts)

Aperi’CTF 2019 - U_u

Détails du challenge

EventChallengeCategoryPointsSolves
Aperi’CTF 2019U_uReverse1753

VoUs AlL3z 4d0R3r PyTh0n !

Challenge : U_u.py - md5sum : 985e5c28dd1fbb3ec233edf70b82f326

TL;DR

Il s’agissait d’une comparaison de caractères avec un encodage « uu ». Le script utilise son propre code source pour comparer les caractères, ce qui rend le debug moins évident.

Méthodologie

Code complet

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
from __builtin__ import iter as var
import random
import hashlib

__,____,___,_____ = sys,open,eval,False
random.seed(hashlib.sha1(open(__file__).read()).hexdigest())
_ = lambda x : x+1
v = var(___("__ohvygvaf__.enj_vachg".encode("rot_13")+"()").encode("uu"))
exec("w=[];".encode("rot_13"))
while 1<2:
    try:
        j = j+[v.next()]
    except StopIteration:
        r = ____(__file__[:-(len(j))]).read()
        z=var(''.join(j[::-1]))
        #     :D
        _____,j = not 1,[]
        while hashlib.sha1(r[:500]+r[-500:]).hexdigest() == "3b32b5601c722e59fd5b0ba81c31f230c3666ca1":
            try:
                x = z.next()
                j = j+[x]
            except StopIteration:
                if ''.join(j).index("5=UMW22!50") == 37:
                    import antigravity
                    __.exit("You van validate with the flag :)")
            if (_____ == 7 and ord(j[_____]) != _____+25) or \
               (_____ == 8 and ord(j[_____]) != _____+24) or \
               (_____ == 10 and j[_____] != r[-201]) or \
               (_____ == 11 and j[_____] != chr(ord(r[0])^ord(r[7]))) or \
               (_____ == 12 and j[_____] != r[240].upper()) or \
               (_____ == 13 and j[_____] != chr(ord(str(not True)[0])^ord("`"))) or \
               (_____ == 14 and j[_____] != ",") or \
               (_____ == 15 and j[_____] != chr(ord("L")+5)) or \
               (_____ == 16 and j[_____] != str(int(r[1088])-1)) or \
               (_____ == 17 and j[_____] != """'""") or \
               (_____ == 18 and j[_____] != chr(ord("+")+2)) or \
               (_____ == 9 and j[_____] != r[-845]):
                sys.exit()
            _____ += 1
    while len(__file__)%2:
        break
    __file__ += chr(random.randint(32,0x7e))
    if (_____ == 28 and j[_____] != "]") or \
       (_____ == 40 and j[_____] != ")") or \
       (_____ == 29 and j[_____] != "E") or \
       (_____ == 30 and j[_____] != "3") or \
       (_____ == 41 and not (j[_____-10] == j[_____] == "F")) or \
       (_____ == 43 and j[_____] != "7") or \
       (_____ == 38 and not (j[_____] == j[_____-6] == ",")) or \
       (_____ == 33 and j[_____] != "P") or \
       (_____ == 34 and j[_____] != str(int(j[_____-4])*3)) or \
       (_____ == 35 and j[_____] != "#") or \
       (_____ == 46 and not (j[_____-2] == j[_____] == j[_____-10] == "-") or \
       (_____ == 37 and j[_____] != "?") or \
       (_____ == 39 and j[_____] != "&") or \
       (_____ == 42 and j[_____] != "=") or \
       (_____ == 45 and j[_____] != chr(ord(j[_____-0x10])-2))):
        __.exit(":(")
    _(_____)

Valeurs

Utilisons l’interpréteur python pour récupérer la valeur des différentes chaînes. Puisque le script importe __builtin__ et qu’on ne peut pas importer __builtin__ en python3, on peut confirmer que le script s’exécute en python2.

Remplaçons __file__ par "U_u.py" dans la génération de la seed pour obtenir la seed (on verra dans les lignes suivantes que la partie aléatoire est inutile).

>>>import hashlib
>>>
>>>hashlib.sha1(open("U_u.py").read()).hexdigest()
'2a2173559b717dadfe2643c937a5cacd7a8d69c0'
>>>
>>>"__ohvygvaf__.enj_vachg".encode("rot_13")+"()"
'__builtins__.raw_input()'

v est le raw_input. Il est encodé avec uuencode (voir .encode("uu")). Ensuite, v est défini comme un itérateur (voir import iter as var). Autrement dit : chaque itération sur v renverra une lettre de l’uuencode() de l’entrée.

>>>"w=[];".encode("rot_13")
'j=[];'

j est une liste.

Partie 1

Analysons maintenant cette partie du code :

while 1<2:
    try:
        j = j+[v.next()]
    except StopIteration:
        # exception
    # code

On a une boucle infinie avec un try sur l’itérateur v et une exception StopIteration. C’est l’équivalent d’une boucle for sur v. Ici, à chaque itération, on ajoute l’élément suivant de v à la liste j. Le # code est atteint après chaque try. L’except est atteint à la fin de l’itérateur. Autrement dit, l’except est l’équivalent du code situé après la boucle for.

Pour la première partie, on a :

while 1<2:
  try:
      j = j+[v.next()]
  except StopIteration:
      # ...
  while len(__file__)%2:
      break
  __file__ += chr(random.randint(32,0x7e))
  if (_____ == 28 and j[_____] != "]") or \
     (_____ == 40 and j[_____] != ")") or \
     (_____ == 29 and j[_____] != "E") or \
     (_____ == 30 and j[_____] != "3") or \
     (_____ == 41 and not (j[_____-10] == j[_____] == "F")) or \
     (_____ == 43 and j[_____] != "7") or \
     (_____ == 38 and not (j[_____] == j[_____-6] == ",")) or \
     (_____ == 33 and j[_____] != "P") or \
     (_____ == 34 and j[_____] != str(int(j[_____-4])*3)) or \
     (_____ == 35 and j[_____] != "#") or \
     (_____ == 46 and not (j[_____-2] == j[_____] == j[_____-10] == "-")) or \
     (_____ == 37 and j[_____] != "?") or \
     (_____ == 39 and j[_____] != "&") or \
     (_____ == 42 and j[_____] != "=") or \
     (_____ == 45 and j[_____] != chr(ord(j[_____-0x10])-2)):
      __.exit(":(")
  _(_____)

Tout d’abord, cette partie du code est inutile et peut être supprimée 😀 :

while len(__file__)%2:
    break

La variable __file__ reçoit un caractère aléatoire supplémentaire à chaque itération __file__ += chr(random.randint(32,0x7e)). Ce caractère peut être prédit grâce à la seed qu’on a identifiée mais, encore une fois, cette partie aléatoire ne sera jamais utilisée dans le code 😕.

Ensuite, on a une grosse condition qui invoque exit si elle est vérifiée. Dans cette condition, on vérifie la valeur de _____ qui est initialisée à False au début du code et incrémentée à chaque itération avec _(_____) (car _ est défini au début du code avec _ = lambda x : x+1).

De plus, la variable _____ est utilisée comme index de j. Ainsi, j[_____] est comparé à différents caractères.

Pour résumer, l’entrée est encodée avec uuencode et une partie de l’entrée encodée est vérifiée avec des caractères en dur. Par exemple _____ == 28 and j[_____] != "]" signifie que le 28e caractère de l’entrée uuencodée est ].

Réordonnons la condition :

if (_____ == 28 and j[_____] != "]") or \
   (_____ == 29 and j[_____] != "E") or \
   (_____ == 30 and j[_____] != "3") or \
   (_____ == 33 and j[_____] != "P") or \
   (_____ == 34 and j[_____] != str(int(j[_____-4])*3)) or \
   (_____ == 35 and j[_____] != "#") or \
   (_____ == 37 and j[_____] != "?") or \
   (_____ == 38 and not (j[_____] == j[_____-6] == ",")) or \
   (_____ == 39 and j[_____] != "&") or \
   (_____ == 40 and j[_____] != ")") or \
   (_____ == 41 and not (j[_____-10] == j[_____] == "F")) or \
   (_____ == 42 and j[_____] != "=") or \
   (_____ == 43 and j[_____] != "7") or \
   (_____ == 46 and not (j[_____-2] == j[_____] == j[_____-10] == "-") or \
   (_____ == 45 and j[_____] != chr(ord(j[_____-0x10])-2))):

Ici, j[27:45] est égal à ]E3F,P9#-?,&)F=7-C-.

Partie 2

On retrouve maintenant le même processus dans l’exception StopIteration :

r = ____(__file__[:-(len(j))]).read()
z=var(''.join(j[::-1]))
#     :D
_____,j = not 1,[]
while hashlib.sha1(r[:500]+r[-500:]).hexdigest() == "3b32b5601c722e59fd5b0ba81c31f230c3666ca1":
    try:
        x = z.next()
        j = j+[x]
    except StopIteration:
        if ''.join(j).index("5=UMW22!50") == 37:
            import antigravity
            __.exit("You van validate with the flag :)")
    if (_____ == 7 and ord(j[_____]) != _____+25) or \
       (_____ == 8 and ord(j[_____]) != _____+24) or \
       (_____ == 10 and j[_____] != r[-201]) or \
       (_____ == 11 and j[_____] != chr(ord(r[0])^ord(r[7]))) or \
       (_____ == 12 and j[_____] != r[240].upper()) or \
       (_____ == 13 and j[_____] != chr(ord(str(not True)[0])^ord("`"))) or \
       (_____ == 14 and j[_____] != ",") or \
       (_____ == 15 and j[_____] != chr(ord("L")+5)) or \
       (_____ == 16 and j[_____] != str(int(r[1088])-1)) or \
       (_____ == 17 and j[_____] != """'""") or \
       (_____ == 18 and j[_____] != chr(ord("+")+2)) or \
       (_____ == 9 and j[_____] != r[-845]):
        sys.exit()
    _____ += 1

On a r = ____(__file__[:-(len(j))]).read(). Ici, __file__[:-len(j)] est l’équivalent de __file__ avant chaque caractère aléatoire. Autrement dit, les caractères aléatoires ajoutés à __file__ étaient inutiles et on conserve le nom de fichier original. Ensuite, le fichier est ouvert avec ____ (défini comme open), lu et placé dans la variable r. Désormais, le contenu de r est le code source du programme.

Ensuite, z est un itérateur sur l’entrée inversée (j).

On a une condition pour parser notre entrée, qui est

while hashlib.sha1(r[:500]+r[-500:]).hexdigest() == "6a708a16690da9519be4f584774b0d80f860d6d6":

Cette condition calcule un hash à partir du début et de la fin du script et vérifie ce hash pour continuer. Autrement dit, si vous modifiez une partie du script, alors le script est corrompu et ne fonctionnera plus.

On a le même processus que dans la partie 1 : j est une liste vide et à chaque itération on ajoute un caractère depuis z, l’entrée inversée. Réordonnons les conditions :

if (_____ == 7 and ord(j[_____]) != _____+25) or \
   (_____ == 8 and ord(j[_____]) != _____+24) or \
   (_____ == 9 and j[_____] != r[-845]) or \
   (_____ == 10 and j[_____] != r[-201]) or \
   (_____ == 11 and j[_____] != chr(ord(r[0])^ord(r[7]))) or \
   (_____ == 12 and j[_____] != r[240].upper()) or \
   (_____ == 13 and j[_____] != chr(ord(str(not True)[0])^ord("`"))) or \
   (_____ == 14 and j[_____] != ",") or \
   (_____ == 15 and j[_____] != chr(ord("L")+5)) or \
   (_____ == 16 and j[_____] != str(int(r[1023])-1)) or \
   (_____ == 17 and j[_____] != """'""") or \
   (_____ == 18 and j[_____] != chr(ord("+")+2)):

On va maintenant donner un exemple de chaque type de conditions :

Condition - type 1
(_____ == 7 and ord(j[_____]) != _____+25)

Est l’équivalent de

ord(j[7]) != 7+25

ce qui signifie

j[7] = chr(32) = ' '
Condition - type 2
(_____ == 9 and j[_____] != r[-845])

Ici, r fait référence au code source. Le code source doit être identique à celui fourni dans le challenge, on va le charger dans un shell python :

>>>r = open("U_u.py","r").read()
>>>r[-845]
'0'

On obtient donc

j[9] = '0'
Condition - type 3
(_____ == 11 and j[_____] != chr(ord(r[0])^ord(r[7])))

Dans un shell python :

>>>r = open("U_u.py","r").read()
>>> chr(ord(r[0])^ord(r[7]))
'A'
Décoder la condition complète

Si on décode la condition complète, on obtient la chaîne suivante :

  0?AX&,Q0'-

Puisque z a été inversé, on peut inverser les chaînes et obtenir les caractères -18 à -7 (j[-18:-7]) :

-'0Q,&XA?0   

Partie 3

La dernière partie est la suivante :

if ''.join(j).index("5=UMW22!50") == 37:
    import antigravity
    __.exit("You van validate with the flag :)")

Pour atteindre le flag, j (le flag uuencodé inversé) doit contenir 5=UMW22!50.

Une fois inversé, on obtient 05!22WMU=5 pour les caractères j[-46:-37].

uuencode

Voyons comment fonctionne uuencode :

>>>"a".encode("uu")
'begin 666 <data>\n!80  \n \nend\n'
>>>"ç".encode("uu")
'begin 666 <data>\n"PZ< \n \nend\n'

On obtient 'begin 666 <data>\nXXXXX\n \nend\n' où XXXXX correspond aux données encodées. Le début et la fin de l’entrée encodée sont fixes et connus.

D’après les 3 parties obtenues, on a la chaîne uuencodée suivante :

Part 3 : j[-46:-37] = 05!22WMU=5
Part 1 : j[27:45] = ]E3F,P9#-?,&)F=7-C-
Part 2 : j[-18:-7] = -'0Q,&XA?0  

Avec le préfixe et le suffixe uuencode (à noter que j[-18] == j[45]) :

begin 666 <data>\n=05!22WMU=5]E3F,P9#-?,&)F=7-C-'0Q,&XA?0  \n \nend\n

Décodons-le avec python :

>>> "begin 666 <data>\n=05!22WMU=5]E3F,P9#-?,&)F=7-C-'0Q,&XA?0  \n \nend\n".decode("uu")
'APRK{uu_eNc0d3_0bfusc4t10n!}\x00'

Flag

APRK{uu_eNc0d3_0bfusc4t10n!}

Zeecka