13 de out. de 2016

Python - Revertendo bytecodes

Caso você precise recuperar um arquivo .py de um .pyc uma boa opção é usar o Decompyle++, que reverte os bytecodes compilados à um código python legível por humanos. 

  • Instalando o Decompyle++:
1) Primeiro precisamos instalar o cmake:
$ wget http://www.cmake.org/files/v2.8/cmake-2.8.12.2.tar.gz
$ tar xzvf cmake-2.8.12.2.tar.gz
$ cd cmake-2.8.10.2
$ ./bootstrap
$ make
# make install

2) Faça o download do Decompile++ e compile:
$ git clone git@github.com:zrax/pycdc.git
$ cd pycdc
$ /usr/local/bin/cmake ../pycdc/
$ make

  • Recuperando o arquivo .py original:
$ ./pycdc dicionario.pyc > dicionario.py

  • Entendendo os bytecodes e opcodes:
Os bytecodes são a representação interna (.pyc) de um script python no compilador. O interpretador Cpython lê o arquivo binário e executa as intruções (opcodes) uma de cada vez. O script python .py resultante é nada mais do que uma sequencia de chamadas de diferentes funções.
O módulo dis inclui funções para lidar com os bytecodes. A função dis.dis() mostra a representação do código python (módulo, classe, método, função, ou objeto de código).

Por exemplo, revertendo a criação de um dicionário temos:
$ echo "dicionario={'a':1}" > dicionario.py 
$ python -m dis dicionario.py 
  1           0 BUILD_MAP                1
              3 LOAD_CONST               0 (1)
              6 LOAD_CONST               1 ('a')
              9 STORE_MAP           
             10 STORE_NAME               0 (dicionario)
             13 LOAD_CONST               2 (None)
             16 RETURN_VALUE      

A saída é organizada em colunas contendo a linha original, o endereço da instrução, o nome do opcode e os argumentos passados para o opcode. Nesse exemplo, o código usa 5 operações diferentes para criar, popular um dicionário e salvar os resultados em uma variável local.

Para ver o código opcode e a representação do caractere de cada operação, podemos usar o módulo opcode:
>>> import opcode
>>> opcode.opmap['BUILD_MAP']
105
>>> opcode.opmap['LOAD_CONST']
100
>>> chr(opcode.opmap['BUILD_MAP'])
'i'
>>> chr(opcode.opmap['LOAD_CONST'])
'd'
>>> hex(opcode.opmap['BUILD_MAP'])
'0x69'
>>> hex(opcode.opmap['LOAD_CONST'])
'0x64'


Para criar um arquivo .pyc podemos usar o py_compile:
>>> import py_compile
>>> py_compile.compile('dicionario.py')

Verificando os opcodes:
$ hexdump -C dicionario.pyc 
00000000  03 f3 0d 0a 07 f0 fe 57  63 00 00 00 00 00 00 00  |.......Wc.......|
00000010  00 03 00 00 00 40 00 00  00 73 11 00 00 00 69 01  |.....@...s....i.|
00000020  00 64 00 00 64 01 00 36  5a 00 00 64 02 00 53 28  |.d..d..6Z..d..S(|
00000030  03 00 00 00 69 01 00 00  00 74 01 00 00 00 61 4e  |....i....t....aN|
00000040  28 01 00 00 00 74 0a 00  00 00 64 69 63 69 6f 6e  |(....t....dicion|
00000050  61 72 69 6f 28 00 00 00  00 28 00 00 00 00 28 00  |ario(....(....(.|
00000060  00 00 00 73 0d 00 00 00  64 69 63 69 6f 6e 61 72  |...s....dicionar|
00000070  69 6f 2e 70 79 74 08 00  00 00 3c 6d 6f 64 75 6c  |io.pyt....<modul|
00000080  65 3e 01 00 00 00 73 00  00 00 00                 |e>....s....|
0000008b

Podemos confirmar a presença dos caracteres  ascii "i" e "d" e os hexadecimais 69 01 00 64 00 00 64 01 00 36 5a 00 00 64, indicando que temos os 2 opcodes BUILD_MAP e 3 LOAD_CONST,  como vimos antes com o dis. Assim, caso precise arrumar um arquivo corrompido .pyc,  conhecer os opcodes pode facilitar a tarefa. :)

* Fontes:
http://multigrad.blogspot.com.br/2014/06/fun-with-python-bytecode.html
http://turtle-philosophy.blogspot.com/2014/06/decompiling-python-bytecode-with-pycdc.html
https://pymotw.com/2/dis/
http://akaptur.com/blog/2013/08/14/python-bytecode-fun-with-dis/
http://vgel.me/posts/patching_function_bytecode_with_python/
ftp://ftp.ntua.gr/mirror/python/peps/pep-0339.html
https://docs.python.org/3/library/dis.html

0 comentários:

Postar um comentário