
Uma biblioteca leve de instrumentação dinâmica
Copyright 2020 Google LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
## O que é o TinyInst?
O TinyInst é uma biblioteca de instrumentação dinâmica leve que pode ser usada para instrumentar apenas módulo(s) selecionado(s) no processo, deixando o restante do processo executar nativamente. Ele foi feito para ser fácil de entender, fácil de modificar e fácil de usar para hackear. Não foi projetado para ser compatível com todos os alvos (mais sobre isso adiante).
### Como ele se compara ao [DynamoRIO](https://dynamorio.org/) e ao [PIN](https://software.intel.com/en-us/articles/pintool)?
O TinyInst não foi concebido como um substituto para frameworks complexos de instrumentação como DynamoRIO e PIN, mas sim como uma alternativa para cenários em que uma solução mais leve seria suficiente. O TinyInst assume que o alvo é bem-comportado (no sentido explicado abaixo), o que não é o caso para frameworks mais complexos. Assim, provavelmente você não conseguirá executar o TinyInst com sucesso contra malware como [foi feito com o DynamoRIO anteriormente](https://www.slideshare.net/MaximShudrak/fuzzing-malware-for-fun-profit-applying-coverageguided-fuzzing-to-find-bugs-in-modern-malware). Por outro lado, se um alvo não funciona com outros frameworks devido ao módulo que não precisa ser instrumentado, e o módulo instrumentado é bem-comportado, ele pode funcionar com o TinyInst. Como com o TinyInst a maior parte do processo executa nativamente, ele terá um tempo de inicialização de processo mais curto e pode superar outras soluções em casos em que o processo alvo gasta muito tempo nos módulos onde a instrumentação não é necessária.
### Como ele se compara ao [Mesos](https://github.com/gamozolabs/mesos) e ao [TrapFuzz](https://github.com/googleprojectzero/p0tools/tree/master/TrapFuzz)?
O TinyInst é uma solução completa de reescrita de binários, portanto, comportamentos arbitrários podem ser alterados no módulo alvo. Isso permite, por exemplo, extrair cobertura de arestas em vez de apenas blocos básicos. Além disso, o TinyInst não depende de outros softwares, como o IDA Pro, para identificar blocos básicos.
### Quais sistemas operacionais o TinyInst suporta?
O TinyInst funciona no Windows (x86 e x64), macOS (x64 e ARM64), Linux (x64 e ARM64) e Android (ARM64). Consulte o README no diretório correspondente de cada sistema operacional para notas e limitações adicionais.
### Quais alvos são compatíveis com o TinyInst?
O TinyInst assume que todos os módulos instrumentados são bem-comportados no sentido de que
- Não há código automodificável
- O endereço de retorno na pilha nunca é acessado diretamente pelo programa
OU/E (dependendo das configurações)
- Nenhum dado é armazenado antes do topo da pilha (em endereços abaixo do apontado por ESP/RSP). Essa condição pode ser flexibilizada para "nenhum dado antes de (ESP/RSP - arbitrary_offset)" usando a flag `-stack_offset`.
O TinyInst também exige que DEP/NX esteja habilitado para o processo alvo. Se esse não for o caso, você pode usar a flag `-force_dep` para forçar a ativação. No entanto, no caso improvável de o alvo realmente precisar de DEP desabilitado para funcionar corretamente, forçá-lo pode fazer com que ele se comporte mal.
### Qual é a sobrecarga de desempenho?
De acordo com medições iniciais em decodificação de imagem, em um alvo de 64 bits bem-comportado com as configurações padrão do TinyInst, a sobrecarga de desempenho foi de cerca de 15% sem um cliente e cerca de 20% com o cliente de exemplo que coleta cobertura. Observe que isso não inclui o tempo limite introduzido pela instrumentação inicial dos módulos. Veja as dicas de desempenho abaixo para mais detalhes.
## Compilando o TinyInst
1. Abra um terminal e configure seu ambiente de compilação (por exemplo, no Windows, execute vcvars64.bat / vcvars32.bat)
2. Navegue até o diretório que contém o código-fonte
3. Execute os seguintes comandos (altere o generator de acordo com a versão do IDE e da plataforma para a qual deseja compilar):
#### Windows```
mkdir build
cd build
cmake -G "Visual Studio 16 2019" -A x64 ..
cmake --build . --config Release
mkdir build cd build cmake -G Xcode .. cmake --build . --config Release
#### Linux```
mkdir build
cd build
cmake ..
cmake --build . --config Release
mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE=</path/to/android/ndk>build/cmake/android.toolchain.cmake -DANDROID_NDK=</path/to/android/ndk> -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM= .. cmake --build . --config Release
Nota #1: o build de 64 bits também será executado em alvos de 32 bits nos sistemas operacionais Windows e Linux
Nota #2: Encontrando problemas ao criar um build de 32 bits em Windows de 64 bits devido ao ambiente não estar configurado corretamente e bibliotecas ausentes? Abra o arquivo .sln gerado no Visual Studio e compile a partir daí em vez de executar cmake --build. Observe também que o build de 64 bits funcionará em alvos de 32 bits, então criar um build de 32 bits pode não ser necessário.
## Usando o TinyInst
O TinyInst é projetado principalmente para ser usado como uma biblioteca dentro de outros programas.
Um cliente TinyInst é escrito como uma subclasse da classe TinyInst. O cliente pode então sobrescrever os métodos da API que precisar. Os métodos da API estão definidos abaixo.
Após a criação do cliente, ele deve ser inicializado com opções de linha de comando chamando
`void init(int argc, char **argv);`
As opções de linha de comando são definidas abaixo e um cliente também pode definir as suas próprias. Depois disso, para executar e controlar um programa instrumentado, as seguintes funções podem ser usadas.
`DebuggerStatus Run(int argc, char **argv, uint32_t timeout);`
`DebuggerStatus Attach(unsigned int pid, uint32_t timeout);`
Essas funções ou executam um programa (usando a linha de comando especificada) ou anexam a um programa já em execução. Se nenhum método alvo for especificado, o alvo continuará em execução até que o programa encerre, o programa sofra um crash ou o tempo limite (dado em milissegundos) expire. Se um método alvo for definido, o TinyInst retornará sempre que o método alvo for inserido e sempre que o método alvo retornar, permitindo que o chamador execute tarefas adicionais.
Quando `Run` e `Attach` retornam enquanto o processo alvo ainda está ativo, as seguintes funções podem ser usadas para encerrar o processo ou continuar a execução.
`DebuggerStatus Kill();`
`DebuggerStatus Continue(uint32_t timeout);`
O TinyInst acompanha um binário de cobertura de exemplo, que pode ser invocado usando
`<options> -- <target command line>`
Exemplo no Windows:
`litecov.exe -instrument_module notepad.exe -coverage_file coverage.txt -- notepad.exe`
## API de Instrumentação
### Callbacks de eventos do depurador
Esses callbacks são apenas informativos e o cliente não deve emitir nenhum código instrumentado durante eles. Os clientes devem chamar o mesmo handler definido na superclasse antes de tratar esses eventos por conta própria.
`OnProcessCreated`
Chamado quando o processo alvo é criado ou anexado.
`OnProcessExit`
Chamado quando o processo alvo encerra.
`OnProcessEntrypoint`
Chamado quando o entrypoint do processo (binário principal) é alcançado
`OnTargetMethodReached`
Se o método alvo for definido, chamado quando o método alvo é alcançado pela primeira vez.
`OnModuleLoaded`
Chamado quando um módulo é carregado. Chamado para cada módulo, não apenas para os instrumentados.
`OnModuleUnloaded`
Chamado quando um módulo é descarregado. Chamado para cada módulo, não apenas para os instrumentados.
`OnException`
Chamado quando uma exceção é encontrada. O cliente deve retornar true (se a exceção foi tratada) ou o resultado do mesmo método na classe pai.
### Callbacks de instrumentação
Durante esses callbacks, o cliente pode adicionar código ao alvo chamando `WriteCode()`. Observe que o cliente é responsável por salvar e restaurar qualquer contexto (como registradores e flags alterados no código inserido).
`InstrumentBasicBlock`
Pode ser usado para inserir código que será executado em um bloco básico específico
`InstrumentEdge`
Pode ser usado para inserir código que será executado em uma aresta específica. Nota: por motivos de desempenho, esse callback só é emitido em arestas não determinísticas (ou seja, saltos condicionais) e saltos/chamadas indiretas (por exemplo, `call rax`). Para arestas em que o próximo bloco básico é sempre conhecido dado o bloco básico anterior (por exemplo, `jmp offset`, `call offset`), nenhum callback será emitido.
`InstrumentInstruction`
Pode ser usado para modificar a instrução ou inserir código antes dela. Dependendo do código de retorno, a instrução original será ou não emitida após o callback.
### Outros callbacks
`OnModuleEntered`
Chamado quando um fluxo de controle é transferido de outro módulo para um módulo instrumentado
`OnModuleInstrumented`
Chamado quando um módulo é instrumentado. Isso acontece geralmente quando o entrypoint do processo é alcançado (se o método alvo não estiver definido) ou quando o método alvo é alcançado (se estiver definido). O cliente pode inicializar seus dados relacionados à instrumentação aqui
`OnModuleUninstrumented`
Chamado quando os dados de instrumentação não são mais válidos e precisam ser limpos. Observe que isso não é o mesmo que o módulo ser descarregado, pois, por padrão, a instrumentação persiste entre descarregamentos/recarregamentos de módulos. Esse callback pode ser usado para limpar quaisquer dados relacionados à instrumentação no cliente.
### API de Hook
Além da API de uso geral documentada acima, o TinyInst também implementa uma API de hooking mais adequada para inspecionar e modificar o comportamento de funções individuais. Essa API está documentada em uma [página separada](https://github.com/googleprojectzero/TinyInst/blob/master/hook.md).
## Opções de Linha de Comando
### Relacionadas à instrumentação
`-instrument_module [module name]` especifica qual módulo instrumentar; múltiplas opções `-instrument_module` podem ser especificadas para instrumentar vários módulos.
`-instrument_transitive [module name]` semelhante a `-instrument_module`, exceto que apenas o código inserido a partir de outros módulos instrumentados será executado instrumentado. Usado principalmente como otimização para chamadas como module1->module2->module1, onde não é importante instrumentar o módulo module2 inteiro, mas as entradas module2->module1 estão causando lentidão.
`-indirect_instrumentation [none|local|global|auto]` qual instrumentação usar para saltos/chamadas indiretas
`-patch_return_addresses` - substitui o endereço de retorno pelo valor original, faz com que os retornos sejam instrumentados usando o método `-indirect_instrumentation` especificado
`-generate_unwind` - Gera dados de stack unwinding para código instrumentado (para tratamento de exceções C++ mais rápido). Observe que pode não funcionar corretamente em algumas versões mais antigas do Windows.
`-persist_instrumentation_data` (padrão = true) Não reinstrumenta o módulo em descarregamentos/recarregamentos de módulos. Só funciona se o módulo for carregado no mesmo endereço em que foi carregado anteriormente.
`-instrument_cross_module_calls` (padrão=true) Se vários módulos `-instrument_module` forem especificados e um chamar outro, salta para o código instrumentado do outro módulo sem causar uma exceção (o que causaria lentidão).
`-stack_offset` (padrão=0) Ao salvar o contexto na pilha, deixe esse número de bytes no topo da pilha (antes do stack pointer) inalterados.
`-patch_module_entries [off|data|code|all]` Tenta resolver lentidões causadas por entradas excessivas de módulos procurando por ponteiros para entrypoints previamente detectados e os substituindo por suas contrapartes instrumentadas. O valor da flag controla onde procurar esses ponteiros. Aviso: ativar isso pode potencialmente introduzir instabilidades no alvo.
### Relacionadas à depuração
`-trace_debug_events` - imprime eventos do depurador (módulos carregados, exceções etc.)
`-trace_basic_blocks` - imprime os blocos básicos à medida que são executados
`-trace_module_entries` - imprime todas as entradas no código instrumentado
`-trace_syscalls` - [apenas Linux/Android] Permite que o cliente receba eventos de início/fim de syscalls por meio dos callbacks `OnSyscall()` / `OnSyscallEnd()`.
`-full_address_map` - Mantém um mapa em nível de instrução dos endereços no código instrumentado para os endereços no código original. Consome muita memória, mas é útil para depuração.
### Método alvo e persistência
O TinyInst permite que o usuário defina um método alvo. Se um método alvo for definido, nenhum código será instrumentado (tudo será executado nativamente) até que o método alvo seja alcançado pela primeira vez. Além disso, o TinyInst interromperá a execução na entrada e na saída do método alvo.
`-target_module` - módulo que contém o método alvo
`-target_method` - nome do método alvo. Isso só funciona se o método alvo for exportado ou se você tiver símbolos para o módulo alvo.
`-target_offset` - use quando o método alvo não puder ser especificado pelo nome. Endereço relativo do método alvo a partir da base do módulo
`-loop` - se essa flag for especificada, o TinyInst executará o método alvo em um loop infinito (ou até que Kill() seja chamado ou o processo termine por outro motivo). Os argumentos da função serão salvos e restaurados entre iterações. Isso é usado principalmente para forçar persistência em fuzzing.
`-nargs` - número de argumentos do método alvo a salvar entre iterações. Deve ser usado junto com `-loop`
`-callcon [ms64|stdcall|fastcall|thiscall]` - convenção de chamada usada pelo método alvo. Deve ser usado junto com `-loop`
### Outras
`-target_env key=value` - [atualmente apenas macOS e Linux/Android] especifica uma variável de ambiente adicional a ser passada ao processo alvo. Múltiplas opções `-target_env` podem ser especificadas para passar várias variáveis de ambiente.
`-force_dep` - [apenas Windows] Força a ativação de DEP para o processo alvo.
## Módulo de cobertura
O TinyInst acompanha um módulo de cobertura (de exemplo), `LiteCov`. O módulo de cobertura pode coletar cobertura de blocos básicos ou de arestas (controlada pela flag `-covtype`). Além disso, o módulo pode extrair cobertura de "comparação" (contando o número de bytes que coincidem em instruções cmp/sub) especificando a flag `-cmp_coverage`.
Uma característica especial do módulo de cobertura é que o buffer de cobertura no processo alvo é inicialmente alocado como somente leitura, causando uma exceção na primeira vez que uma nova cobertura é encontrada. Combinado com uma opção para ignorar um certo subconjunto de cobertura, isso permite consultar rapidamente se executar o alvo com uma determinada entrada resultou em nova cobertura ou não.
## Como o TinyInst funciona?
O TinyInst é construído sobre um depurador personalizado. O depurador monitora o processo alvo em busca de eventos como carregamento de módulos, breakpoints atingidos, exceções disparadas etc. O depurador também implementa breakpoints e persistência se o método alvo for especificado.
Quando um módulo a ser instrumentado é carregado, ele é inicialmente "instrumentado" da seguinte forma
- Todas as regiões executáveis do módulo são marcadas como não executáveis, mantendo as outras permissões (leitura/escrita) como eram originalmente. Isso causa uma exceção sempre que o fluxo de controle atinge um módulo instrumentado, que é capturada e tratada pelo depurador.
- Uma região de memória executável é alocada dentro de 2GB do intervalo de endereços do módulo original. É onde o código instrumentado/reescrito do módulo será colocado. 2GB é importante, pois permite que todas as instruções que usam endereçamento na forma [rip+offset] sejam substituídas por [rip+fixed_offset].
Sempre que um módulo instrumentado é acessado (seja na primeira vez ou em qualquer outra), o bloco básico atingido é instrumentado, juntamente com todos os blocos básicos que podem ser descobertos de forma confiável seguindo recursivamente ramificações condicionais, bem como chamadas e saltos diretos (por exemplo, jmp offset, call offset).
Isso é suficiente para executar o código instrumentado porque
- todos os saltos/chamadas diretos cairão no código instrumentado no local correto
- todos os saltos/chamadas indiretos (por exemplo, call rax) cairão em seu local no código original, o que causa uma exceção, que o depurador resolve substituindo o instruction pointer pelo local correspondente no código instrumentado.
No entanto, embora isso funcione, observe que causará uma exceção em cada chamada/salto indireto cujo alvo esteja em um módulo instrumentado. Como o tratamento de exceções é lento, instrumentar alvos com muita indireção (por exemplo, métodos virtuais em C++, ponteiros de função) será lento sem instrumentação adicional.
### Instrumentando chamadas e saltos indiretos
O TinyInst pode instrumentar chamadas e saltos indiretos para evitar exceções em alvos indiretos (já vistos). Uma chamada/salto instrumentado, em vez de saltar para o alvo original, saltará para o início da lista encadeada de stubs. Cada stub contém um par de (original_target, translated_target). Ele testa se o alvo do salto/chamada corresponde a original_target e, em caso afirmativo, o fluxo de controle é direcionado para translated_target. Caso contrário, ele salta para o próximo stub. Se o final da lista for alcançado, isso significa que o alvo do salto/chamada ainda não foi visto. Isso causará um breakpoint que é capturado pelo depurador e resolvido criando outro stub e inserindo-o na lista.
Esse mecanismo pode ser implementado de 2 maneiras
- lista por callsite (local)
- hashtable global usada por todos os saltos/chamadas indiretos
A hashtable global resulta em melhor desempenho. A lista local (por callsite) permite obter arestas corretas (com o endereço de origem correto) em chamadas/saltos indiretos.
Observe que no Windows moderno, devido ao CFG, todos os saltos/chamadas indiretos ocorrem do mesmo local; portanto, com binários compilados com CFG, é impossível (sem algum tipo de tratamento especial) obter arestas precisas de qualquer forma. Isso, juntamente com o benefício de desempenho, é o motivo pelo qual a hashlist global é o método padrão para lidar com chamadas/saltos indiretos no TinyInst.
### Correção de endereço de retorno
Por padrão, quando uma chamada ocorre no código instrumentado, o endereço de retorno gravado será a próxima instrução no *código instrumentado*. Isso funciona corretamente na maioria dos casos; no entanto, causará problemas se o processo alvo acessar endereços de retorno para fins diferentes de retornar. Um exemplo notável disso é o stack unwinding durante o tratamento de exceções em sistemas operacionais de 64 bits. Portanto, alvos que precisam capturar exceções não funcionarão corretamente com o TinyInst por padrão.
Isso pode ser resolvido na maioria dos casos adicionando a flag `-generate_unwind`, que faz o TinyInst gerar e registrar metadados de stack unwinding / tratamento de exceções para o processo alvo. Observe que `-generate_unwind` pode não funcionar corretamente em algumas versões mais antigas do Windows por exigir a versão 2 do UNWIND_INFO.
O TinyInst também tem uma opção (exposta pela flag `-patch_return_addresses`) para reescrever endereços de retorno em seus valores correspondentes no código não instrumentado sempre que uma chamada ocorre. Observe, no entanto, que essa opção introduz uma sobrecarga bastante grande, pois causa uma troca de contexto em cada retorno (aresta de retorno) de um módulo não instrumentado para um módulo instrumentado.
## Dicas de desempenho
A maior sobrecarga no TinyInst vem de uma exceção ser lançada sempre que um módulo instrumentado é acessado a partir de um módulo não instrumentado. Você pode ver essas exceções sendo disparadas usando a flag `-trace_module_entries`. A instrumentação de saltos/chamadas indiretos deve ser usada sempre que possível, e a instrumentação de retornos não deve ser usada sempre que possível. O TinyInst tem melhor desempenho em módulos (ou grupos de módulos) razoavelmente autocontidos. Por exemplo, se você tiver dois módulos, A e B, em que A chama B com frequência, mas apenas B é instrumentado, isso causará muita lentidão. Um desempenho melhor poderia ser alcançado instrumentando tanto A quanto B.
## Dicas de depuração
Use `-trace_basic_blocks` para ver os blocos básicos conforme são executados. Você verá tanto os endereços no código instrumentado quanto os endereços correspondentes no código não instrumentado.
Use o callback OnException() para examinar o estado do programa quando o crash ocorre.
## Aviso legal
Este não é um produto oficial do Google.