Inspetor de pickle do Python
Um pickle não é bem um formato de dados: é um programa para uma pequena máquina de pilha, e o pickle.load o executa. Os opcodes podem importar qualquer módulo, chamar qualquer invocável e construir qualquer classe antes que um valor sequer apareça. Por isso esta ferramenta nunca desserializa nada. Ela lê o fluxo de bytes do mesmo jeito que o pickletools.dis: um opcode por vez, com o deslocamento, o argumento decodificado e o efeito sobre a pilha, e remonta apenas a parte do valor que pode ser remontada sem entrar no Python. Dicionários, listas, tuplas, conjuntos, cadeias, cadeias de bytes, inteiros de qualquer tamanho, floats, booleanos e None voltam como são; tudo que exigiria uma importação aparece como marcador explícito em vez de palpite. A versão do protocolo é informada duas vezes, porque as duas respostas podem divergir: do protocolo 2 em diante o arquivo abre com um opcode PROTO que declara a versão, enquanto os protocolos 0 e 1 não declaram nada, então também é calculado o protocolo mais alto que algum opcode realmente exige, e um arquivo que se diz mais antigo do que é recebe um aviso. A tabela do memo mostra quais objetos foram guardados e buscados de novo, que é o único caminho pelo qual referências compartilhadas e ciclos ficam visíveis: uma lista que contém a si mesma é a busca de uma chave guardada enquanto essa mesma lista ainda estava sendo construída. Os fluxos de protocolo 4 e 5 são divididos em quadros, e a tabela de quadros mostra onde cada bloco começa. O painel de achados é o que vale ler primeiro. Ele nomeia toda construção que executaria código no carregamento: GLOBAL e STACK_GLOBAL para os nomes importados, REDUCE para a chamada em si, INST, OBJ, NEWOBJ e NEWOBJ_EX para a construção de classes, BUILD para o __setstate__, PERSID para o gancho persistent_load do próprio carregador e EXT1/2/4 para o registro do copyreg, onde o nome nem está no arquivo. E lista cada módulo e atributo que o carregador procuraria, marcando os que aparecem em exploits publicados, como os.system, subprocess, builtins.eval e posix.system. Um pickle sem nenhum desses opcodes é dado; um pickle com eles é programa, e nenhuma inspeção torna seguro carregá-lo de uma origem em que você não confia. Arquivos malformados são nomeados em vez de lidos pela metade: um fluxo truncado, bytes sobrando depois do STOP (um segundo pickle escondido atrás do primeiro) e a busca de uma chave de memo nunca guardada são relatados como tais.
Como usar
- Solte na caixa um .pkl, um .pickle ou qualquer objeto serializado com pickle. Nada é enviado e nada é desserializado: o arquivo só é desmontado.
- Leia primeiro o painel de achados. «Apenas dados» quer dizer que nenhum opcode do arquivo consegue importar ou chamar nada. Se ele listar importações, trate o arquivo como código não confiável e não o carregue.
- Compare o protocolo declarado com o necessário. Um arquivo que declara o protocolo 2 mas usa opcodes do 4 foi montado à mão, não escrito pelo pickle.dumps.
- Percorra a tabela de opcodes. A indentação segue o aninhamento do MARK, então cada contêiner se lê como um bloco, e a coluna da pilha mostra quantos valores seguem vivos depois de cada passo.
- Abra a tabela do memo para achar objetos compartilhados: uma chave com reutilização maior que zero é um mesmo objeto aparecendo em vários lugares, e é assim também que um ciclo é escrito.
Perguntas frequentes
- É seguro abrir aqui um pickle de origem desconhecida?
- É, porque aqui nada é desserializado. O arquivo é lido opcode a opcode, exatamente como o pickletools.dis o lê, e a única coisa que acontece com um opcode GLOBAL ou REDUCE é os nomes de módulo e atributo serem impressos. Nada é importado e nada é chamado, e a remontagem para nos valores que dá para formar só com opcodes. O arquivo também não sai do seu navegador: é lido com a File API e analisado na própria página. O que a ferramenta não pode fazer é tornar o arquivo seguro depois; se o painel de achados listar importações, um pickle.load sobre esse mesmo arquivo continuará realizando-as.
- Por que um pickle consegue executar código? Parece um formato de serialização.
- Porque serializar objetos arbitrários exige um jeito de recriá-los, e a resposta do Python é o __reduce__: o objeto informa qual invocável, com quais argumentos, vai reconstruí-lo. O fluxo então guarda um nome a importar e uma chamada a fazer, e o desserializador executa as duas coisas. Esse único mecanismo é o que torna serializáveis os objetos datetime, os arrays do numpy e qualquer classe sua, e é o mesmo mecanismo que um atacante usa escrevendo os.system no lugar de datetime.datetime. Nada no formato separa os dois casos, e é por isso que ler os opcodes é a única forma de saber o que um arquivo vai fazer.
- Para que serve o memo e como um ciclo aparece nele?
- O memo é a tabela de objetos que o serializador já escreveu. Quando o mesmo objeto reaparece, a segunda ocorrência é uma busca no memo: PUT e GET no protocolo 0, BINPUT e BINGET depois, e MEMOIZE a partir do protocolo 4, que numera as chaves implicitamente em vez de escrevê-las. É isso que preserva a identidade: dois nomes que apontavam para uma lista continuam apontando para uma só lista depois do carregamento. Um ciclo é esse mesmo mecanismo um passo adiante: o contêiner vai para o memo antes de o conteúdo ser escrito, então o conteúdo pode se referir de volta a ele. Por isso uma lista que contém a si mesma é representável, e por isso aqui a referência é mostrada em vez de expandida sem fim.
- O que muda de fato entre as seis versões do protocolo?
- O protocolo 0 é ASCII: inteiros, floats e cadeias são escritos como texto terminado em quebra de linha, e por isso um pickle de protocolo 0 quase dá para ler a olho nu. O 1 acrescenta a forma binária de quase tudo. O 2 acrescenta construção eficiente de classes com NEWOBJ, tuplas de um byte e booleanos de verdade. O 3 acrescenta objetos bytes, que o Python 2 não conseguia representar. O 4 acrescenta os quadros, que cortam o fluxo em blocos com o comprimento à frente para o leitor puxar pedaços inteiros, além de tamanhos de 64 bits, STACK_GLOBAL, suporte nativo a conjuntos e chaves de memo implícitas. O 5 acrescenta os buffers fora de banda, em que um array grande viaja ao lado do fluxo e não dentro dele: por isso o NEXT_BUFFER pode aparecer sem dados atrás.
- O arquivo tem bytes depois do STOP. O que isso significa?
- O STOP encerra um pickle, e tudo depois dele pertence ao que vier a seguir. Escrever vários pickles em um arquivo só é um padrão comum — um laço de pickle.dump em um arquivo aberto, relido com um laço de pickle.load —, então bytes sobrando muitas vezes significam apenas que o arquivo guarda uma série de registros e não um objeto único. Também é o jeito de esconder uma segunda carga atrás de uma primeira inofensiva, já que um programa que chama load uma vez só vê o primeiro objeto e nunca olha o resto. Esta ferramenta desmonta o primeiro pickle e informa quantos bytes sobraram; para examinar o restante, corte o arquivo no deslocamento do STOP e abra a cauda separadamente.
- Dá para ver o conteúdo de um array do numpy ou de um DataFrame do pandas?
- Os valores não, porque esses objetos são remontados chamando o numpy e o pandas: o fluxo nomeia algo como numpy.core.multiarray._reconstruct ou uma classe do pandas, entrega a ela uma cadeia de bytes, e o array só existe depois que essa chamada roda. O que dá para ver é o formato do arquivo: os nomes que ele importaria, os buffers brutos que carrega e o tamanho deles, o que costuma bastar para dizer se o arquivo é o que afirma ser. Num arquivo de protocolo 5 os dados do array podem nem estar dentro dele: NEXT_BUFFER quer dizer que a carga foi passada fora de banda e precisa ser fornecida por quem chama.
Ferramentas relacionadas
Inspetor de bytecode .pyc do Python
Abra um .pyc de __pycache__ no navegador: qual CPython o escreveu, como é invalidado, a árvore de objetos de código e o desmontador.
Inspetor de arquivos .class do Java
Analise um .class compilado no navegador: versão do arquivo e do Java, sinalizadores de acesso, pool de constantes, campos, métodos e dependências.
Inspetor de bibliotecas estáticas .a
Leia um arquivo .a no navegador: membros com tamanhos e datas, os símbolos que cada objeto define e precisa, e se o índice de símbolos ficou desatualizado.
Inspetor de .npy e .npz do NumPy
Leia o cabeçalho de um .npy ou .npz no navegador: dtype, shape, ordem de bytes, campos e uma prévia de valores, sem NumPy e sem upload.
Codificador / Decodificador Base64
Codifique texto em Base64 ou decodifique Base64 de volta em texto.
Referência de Métodos HTTP
Cartão rápido por método HTTP (GET, POST, PUT, DELETE, PATCH, …) — segurança, idempotência, cacheabilidade, e se corpos de request / response são permitidos.