grim7reaper

Un artisan du code

Crackme — Python

Le crackme étudié dans cet article provient d’ici.

Cinquième crackme, et cette fois nous allons travailler non pas sur du code machine, mais sur du bytecode Python.

Un fichier Python compilé reste un fichier binaire, donc nous pouvons commencer par la traditionnelle commande strings.

% strings ch19.pyc
__main__u$
Welcome to the RootMe python crackmeu
Enter the Flag: u'
I know, you love decrypting Byte Code !i
You Winu
Try Again !N(
__name__u
printu
inputu
PASSu
KEYu
SOLUCEu
KEYOUTu
appendu
ordu
len(
crackme.pyu
<module>

Bon rien de bien concluant. Il est maintenant temps de se renseigner sur ce fameux fichier de bytecode, et plus particulièrement sur son format. Après quelques recherches, je suis tombé sur ce fil de la liste de diffusion Python-ideas. La partie intéressante est la suivante :

Currently .pyc files use a very simple format:

- MAGIC number (4 bytes, little-endian)
- last modification time of source file (4 bytes, little-endian)
- code object (marshaled)

Gabriel Genellina

Et bien ça c’est une bonne nouvelle :-). Le format de ce fichier est extrêmement simple. Deux entiers 32-bit et un objet code sérialisé.

Cela dit, le fait que l’objet code soit sérialisé via le module marshal plutôt que via le module pickle implique qu’il va nous falloir utiliser une version de Python relativement proche de celle qui a généré le bytecode, sinon on risque de ne pas pouvoir désérialiser l’objet.
Le crackme ayant été publié le 3 juillet 2013, je vais tenter ma chance avec le Python 3.3 de mon Archlinux.

Essayons donc de lire ce fichier :

% bpython
>>> import marshal
>>> import struct
>>> import time
>>> pyc = open('ch19.pyc', 'rb')
>>> magic_number = struct.unpack('<i', pyc.read(4))[0]
>>> hex(magic_number)
'0xa0d0c4f'
>>> timestamp = struct.unpack('<i', pyc.read(4))[0]
>>> time.strftime('%Y/%m/%d', time.localtime(timestamp))
'2013/07/02'
>>> code = marshal.load(pyc)
>>> code
<code object <module> at 0x7fb7b60e80c0, file "crackme.py", line 8>
>>> pyc.close()

Ok, c’est bien du Python 3. À titre de comparaison, voilà ce qui arrive si l’on essaye de lire le fichier avec du Python 2.7.

% python2
Python 2.7.5 (default, Sep  6 2013, 09:55:21)
[GCC 4.8.1 20130725 (prerelease)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import marshal
>>> import struct
>>> import time
>>> pyc = open('ch19.pyc', 'rb')
>>> magic_number = struct.unpack('<i', pyc.read(4))[0]
>>> hex(magic_number)
'0xa0d0c4f'
>>> timestamp = struct.unpack('<i', pyc.read(4))[0]
>>> time.strftime('%Y/%m/%d', time.localtime(timestamp))
'2013/07/02'
>>> code = marshal.load(pyc)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: bad marshal data (unknown type code)

Ok, on arrive donc bien à désérialiser notre binaire en un objet code. Et maintenant, on fait quoi avec cet objet ?
C’est là que l’excellente et très complète bibliothèque standard de Python intervient. Dans cette bibliothèque on trouve une section Python Language Services qui contient le module dis. Et c’est exactement ce qu’il nous faut !

Allez, faisons un petit script pour désassembler ce fichier compilé et avoir du bytecode à nous mettre sous la dent.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3

import dis
import marshal
import struct
import sys

def disassemble(filepath):
    with open(filepath, 'rb') as pyc:
        # Read the magic number.
        _ = struct.unpack('<i', pyc.read(4))
        # Read the timestamp
        _ = struct.unpack('<i', pyc.read(4))
        # Read the code.
        code = marshal.load(pyc)
    dis.disassemble(code)

if __name__ == '__main__':
    if len(sys.argv) == 2:
        disassemble(sys.argv[1])
    else:
        print('Error: missing argument', file=sys.stderr)
        print('Usage: {} file.pyc'.format(sys.argv[0]))

Et maintenant, appliquons-le à notre crackme.

% ./disass.py ch19.pyc
  8           0 LOAD_NAME                0 (__name__) 
              3 LOAD_CONST               0 ('__main__') 
              6 COMPARE_OP               2 (==) 
              9 POP_JUMP_IF_FALSE      219 

  9          12 LOAD_NAME                1 (print) 
             15 LOAD_CONST               1 ('Welcome to the RootMe python crackme') 
             18 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
             21 POP_TOP              

 10          22 LOAD_NAME                2 (input) 
             25 LOAD_CONST               2 ('Enter the Flag: ') 
             28 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
             31 STORE_NAME               3 (PASS) 

 14          34 LOAD_CONST               3 ('I know, you love decrypting Byte Code !') 
             37 STORE_NAME               4 (KEY) 

 16          40 LOAD_CONST               4 (5) 
             43 STORE_NAME               5 (I) 

 17          46 LOAD_CONST               5 (57) 
             49 LOAD_CONST               6 (73) 
             52 LOAD_CONST               7 (79) 
             55 LOAD_CONST               8 (16) 
             58 LOAD_CONST               9 (18) 
             61 LOAD_CONST              10 (26) 
             64 LOAD_CONST              11 (74) 
             67 LOAD_CONST              12 (50) 
             70 LOAD_CONST              13 (13) 
             73 LOAD_CONST              14 (38) 
             76 LOAD_CONST              13 (13) 
             79 LOAD_CONST               7 (79) 
             82 LOAD_CONST              15 (86) 
             85 LOAD_CONST              15 (86) 
             88 LOAD_CONST              16 (87) 
             91 BUILD_LIST              15 
             94 STORE_NAME               6 (SOLUCE) 

 19          97 BUILD_LIST               0 
            100 STORE_NAME               7 (KEYOUT) 

 22         103 SETUP_LOOP              75 (to 181) 
            106 LOAD_NAME                3 (PASS) 
            109 GET_ITER             
        >>  110 FOR_ITER                67 (to 180) 
            113 STORE_NAME               8 (X) 

 23         116 LOAD_NAME                7 (KEYOUT) 
            119 LOAD_ATTR                9 (append) 
            122 LOAD_NAME               10 (ord) 
            125 LOAD_NAME                8 (X) 
            128 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
            131 LOAD_NAME                5 (I) 
            134 BINARY_ADD           
            135 LOAD_NAME               10 (ord) 
            138 LOAD_NAME                4 (KEY) 
            141 LOAD_NAME                5 (I) 
            144 BINARY_SUBSCR        
            145 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
            148 BINARY_XOR           
            149 LOAD_CONST              17 (255) 
            152 BINARY_MODULO        
            153 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
            156 POP_TOP              

 24         157 LOAD_NAME                5 (I) 
            160 LOAD_CONST              18 (1) 
            163 BINARY_ADD           
            164 LOAD_NAME               11 (len) 
            167 LOAD_NAME                4 (KEY) 
            170 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
            173 BINARY_MODULO        
            174 STORE_NAME               5 (I) 
            177 JUMP_ABSOLUTE          110 
        >>  180 POP_BLOCK            

 30     >>  181 LOAD_NAME                6 (SOLUCE) 
            184 LOAD_NAME                7 (KEYOUT) 
            187 COMPARE_OP               2 (==) 
            190 POP_JUMP_IF_FALSE      206 

 31         193 LOAD_NAME                1 (print) 
            196 LOAD_CONST              19 ('You Win') 
            199 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
            202 POP_TOP              
            203 JUMP_ABSOLUTE          219 

 33     >>  206 LOAD_NAME                1 (print) 
            209 LOAD_CONST              20 ('Try Again !') 
            212 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
            215 POP_TOP              
            216 JUMP_FORWARD             0 (to 219) 
        >>  219 LOAD_CONST              21 (None) 
            222 RETURN_VALUE         

On constate que le bytecode Python produit par l’interpréteur de référence CPython est :

Si l’on ajoute à cela le fait que le script à l’origine de ce bytecode est court, alors il est simple de faire une décompilation à la main, à partir du bytecode, qui nous permet de retrouver le script suivant :

crackme.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env python3

if __name__ == '__main__':
    print('Welcome to the RootMe python crackme')
    PASS = input('Enter the Flag: ')
    KEY = 'I know, you love decrypting Byte Code !'
    I = 5
    SOLUCE = [57, 73, 79, 16, 18, 26, 74, 50, 13, 38, 13, 79, 86, 86, 87]
    KEYOUT = []
    for X in PASS:
        KEYOUT.append(ord(X) + I ^ ord(KEY[I]) % 255)
        I = I + 1 % len(KEY)
    if SOLUCE == KEYOUT:
        print('You Win')
    else:
        print('Try Again !')

Maintenant que l’on connaît le résultat à obtenir et comment la chaîne en entrée est transformée, il nous suffit d’appliquer la transformation inverse (et ici la transformation est simple à inverser) sur le résultat attendu (SOLUCE) afin d’obtenir ce que nous cherchons(PASS). Pour ce faire, j’ai fait un petit script Ruby :

breakit.rb
1
2
3
4
5
6
7
#!/usr/bin/env ruby

SOLUCE = [57, 73, 79, 16, 18, 26, 74, 50, 13, 38, 13, 79, 86, 86, 87]
KEY = 'I know, you love decrypting Byte Code !'.each_char.map(&:ord).to_a
I = 5
psw = SOLUCE.zip(KEY[I..-1]).each_with_index.map { |(x, k), i| (x^k) - (i+I) }
puts(psw.map(&:chr).join)

Et il nous donne :

% ./breakit.rb
I_hate_RUBY_!!!

Si j’avais su ^^
Bon allez, testons cela :

% ./crackme.py 
Welcome to the RootMe python crackme
Enter the Flag: I_hate_RUBY_!!!
You Win

Gagné !
Voilà, ce crackme un peu original est terminé. Il était relativement simple, mais il aura fallu quelques recherches pour en venir à bout. En tout cas, ce fut instructif.