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)
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 :
- simple et lisible
- documenté
- un bytecode pour stack machine
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 :
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 :
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.