Skip to content
KitploitKITPLOIT
FerramentasBlog
Enviar
FerramentasBlog
Enviar

Ferramentas de Hacking, PenTest e Cibersegurança para o seu Arsenal de Segurança!

Kitploit é um diretório de ferramentas de hacking, cibersegurança e pentesting. Descubra as últimas atualizações de projetos para encontrar vulnerabilidades, analisar sistemas, automatizar testes e fortalecer sua segurança.

··Feeds·Contato·Privacidade·© 2026 Kitploit

Diretório de Ferramentas

Categorias

Ver todas as categorias
Loading categories
binprotect — Ofuscador bin2bin para PE x64 que não adiciona uma seção ao binário | Kitploit
Ferramentas/GitHubGitHub/noahware/binprotect
Análise EstáticaAnálise Dinâmica (Sandboxing)Engenharia ReversaAnálise de MalwareAnálise de Binários
GitHubnoahware/binprotect

binprotect

Ofuscador bin2bin para PE x64 que não adiciona uma seção ao binário

Ver Repositório
30436há 2 diasRevisado pelo Kitploit

Mais Populares

Ver todos →

Descubra as ferramentas mais usadas pela nossa comunidade.

Explore todas as ferramentas

Navegue pela nossa coleção de ferramentas

Ver todas as ferramentas →
Compartilhar

Instruções de compilação

Consulte 4. Compilação para instruções de compilação.

Conteúdo

  1. Introdução
  2. Reescritor binário
    • 2.1. Rastreamento de endereços relativos
    • 2.2. Desmontagem
      • 2.2.1. Divisão em blocos básicos
      • 2.2.2. Fluxo de controle indireto
        • 2.2.2.1. Tabelas de desvio
          • 2.2.2.1.1. Tabelas de desvio limitadas
          • 2.2.2.1.2. Tabelas de desvio ilimitadas
          • 2.2.2.1.3. Diferentes tipos de tabelas de desvio
        • 2.2.2.2. Casos extremos
      • 2.2.3. Funções
      • 2.2.4. Tratamento de chamadas 'noreturn'
    • 2.3. Suporte a exceções
      • 2.3.1. Suporte a desenrolamento
        • 2.3.1.1. Implementação
      • 2.3.2. Análise de informações de exceção
        • 2.3.2.1. SEH/C_SCOPE_TABLE
        • 2.3.2.2. FuncInfo3 e FuncInfo4
      • 2.3.3. Análise de RTTI e ThrowInfo
        • 2.3.3.1. RTTI
        • 2.3.3.2. ThrowInfo
  3. Ofuscação
    • 3.1. Máquina virtual
      • 3.1.1. Suporte a desenrolamento
    • 3.2. Blocos de predicado opaco
    • 3.3. Achatamento de fluxo de controle
    • 3.4. Substituição linear
    • 3.5. Aritmética booleana mista
  4. Compilação
  5. Uso
  6. Acrônimos
  7. Créditos

1. Introdução

Este ofuscador é bin2bin, o que significa que ele pega um binário executável já compilado e o reproduz com as passagens de ofuscação aplicadas. Isso pode ser usado para proteger um aplicativo sem ter acesso ao código-fonte original. Apenas arquivos PE (portable executable) x64 são suportados atualmente, mas há planos de adicionar suporte para outros formatos binários (ex.: ELF) no futuro.

Atualmente, todos os ofuscadores bin2bin conhecidos inserem uma seção no final do binário para colocar o código ou dados ofuscados. Isso é feito para que o layout original do binário ainda seja preservado, sem ter que alterar o conteúdo das seções pré-existentes. Isso é muito mais fácil de gerenciar, pois mantém a maioria dos RVAs (endereços relativos) válidos.

Este projeto adota uma abordagem única para bin2bin, onde qualquer código ou dados ofuscados são inseridos dentro das seções originais do binário. Isso exige o rastreamento de cada RVA no aplicativo. Os benefícios dessa abordagem são:

  • Menos suspeita para análises de malware, pois não há seções executáveis adicionais adicionadas ao binário. Nota: isso foi testado puramente para fins educacionais e de pesquisa.
  • Tamanho reduzido do binário executável de saída. Isso ocorre porque o código original não ofuscado pode ser apagado do binário, já que as seções podem ser redimensionadas.
  • Mais difícil para análise, pois um engenheiro reverso não conseguirá separar o código ofuscado do não ofuscado apenas pelas seções em que estão; cada rotina teria que ser verificada para ver se está ofuscada.

Este documento descreverá tanto a reescrita do binário executável quanto as técnicas de ofuscação implementadas. As seguintes técnicas de ofuscação foram implementadas:

  • Máquina virtual.
  • Predicados opacos.
  • Achatamento de fluxo de controle.
  • Substituição linear.
  • Aritmética booleana mista.

Além disso, este projeto também possui suporte a exceções (exceções C++ e SEH) e é capaz de ofuscar funções que possuem tratamento de exceções.

Para auxiliar na desmontagem e descoberta de código no binário, arquivos de símbolo (tanto PDB quanto MAP) são aceitos opcionalmente. O fornecimento de arquivos de símbolo não é obrigatório, mas auxilia na desmontagem em binários complexos. Alguns recursos, como suporte a exceções e achatamento de fluxo de controle, exigem o fornecimento de um arquivo de símbolo.

2. Reescritor binário

Um reescritor binário pega um binário executável e altera o código ou dados dentro dele para produzir um binário de saída com as alterações aplicadas.

2.1. Rastreamento de endereços relativos

Como o código ofuscado é inserido diretamente nas seções originais do binário, os endereços relativos no programa devem ser rastreados para que todas as referências a eles possam ser ajustadas. Isso é feito para que as referências ainda apontem para o mesmo local após a inserção do código e dos dados. Caso contrário, dados ou códigos seriam acessados no local errado, alterando o comportamento do binário de saída e causando graves instabilidades.

Sempre que uma referência a um endereço relativo é encontrada (ex.: instrução contendo operandos relativos ao rip ou diretórios de dados do PE), ela é adicionada a uma lista de rastreamento para ser atualizada ao final da reescrita. O RVA onde a referência ocorre é rastreado (para saber onde atualizar a referência), assim como o RVA que está sendo referenciado (para saber qual RVA usar para atualizar a referência).

Sempre que o desmontador encontra uma instrução relativa ao rip, ele a adiciona a uma lista de referências a serem atualizadas ao final da ofuscação. Isso garante que todas essas instruções ainda estejam apontando para o local que originalmente tinham. Outros casos de instruções relativas, como tabelas de desvio, também são adicionados como referências a serem atualizadas.

Todos os RVAs rastreados precisam ser ajustados sempre que bytes são inseridos ou removidos do binário. Por exemplo, aqui está o manipulador de inserção de bytes:```cpp
void binwrite::binary_t::insert(const rva_t rva, const std::span data, const bool inclusive)
{
buffer_.insert_range(buffer_.begin() + rva.value(), data);

root@kitploit:~
update_rvas(rva, static_cast<rva_t::size_type>(data.size()), inclusive);  

}

root@kitploit:~
`update_rvas` é onde cada RVA rastreada é atualizada para refletir a mudança que ocorreu no binário. Aqui está um diagrama deste processo:

![](https://assets.kitploit.com/production/public/readmes/8091/12aaec3746c267c932ab3804b13df50c08a463c403417bf6ba4a52f39d12f48a.png)

Figura 1. Rastreamento de endereços relativos.

Os dados inseridos (azul) deslocam os dados atuais (cinza). A RVA referenciada pela instrução (laranja) é atualizada para apontar para a mesma memória, levando em conta os dados inseridos (azul).

## 2.2. Desmontagem

Todas as entradas de código potenciais (exports, ponto de entrada, realocações apontando para a seção de código, etc) são adicionadas a uma fila de desmontagem. Se um arquivo de símbolo estiver presente, todas as funções descritas pelo arquivo de símbolo também são adicionadas à fila de desmontagem. Cada entrada na fila é tratada como um bloco básico individual.

Um bloco básico é um grupo de instruções sem desvios; isso significa que ele é terminado em instruções de fluxo de controle (ex.: jump, ret, int). Blocos básicos não terminam em chamadas (calls) pois espera-se que elas retornem na maioria dos casos. Algumas funções não retornam (ex.: _CxxThrowException) e serão referidas como chamadas 'noreturn' daqui em diante.

Quando um bloco básico da fila de desmontagem está sendo processado, cada instrução é desmontada a partir do início até que uma das seguintes situações ocorra:

- Outro bloco básico já analisado é alcançado, causando uma sobreposição. Veja “Divisão de blocos básicos”.  
- Instrução de término é encontrada (jump, return, int).  
- A desmontagem da instrução falhou.  
- Padding de código foi encontrado.

Abaixo está um diagrama da desmontagem e da entrada na fila de desmontagem (a verificação de padding de código foi omitida no diagrama). Isso é repetido até que a fila de desmontagem esteja vazia.

![](https://assets.kitploit.com/production/public/readmes/8091/7fe76b27e9f1f6ef620bc3aca182b85f9075c1dd31441e565099c05cfa396098.png)

Figura 2. Processamento da desmontagem.

### 2.2.1 Divisão de blocos básicos

Se dois blocos básicos se sobrepõem, então um deles deve ser dividido. Isso impede que dois blocos descrevam as mesmas instruções. Por exemplo:```asm  
wcslen proc  
    or      rax, 0FFFFFFFFFFFFFFFFh  
loc_140001078:  
    inc     rax  
    cmp     word ptr [rcx+rax*2], 0  
    jnz     short loc_140001078  
    retn  
wcslen endp  

Esta é uma implementação de wcslen, que obtém o comprimento de uma string larga. Quando a primeira instrução 'or rax, FFFFFFFFFFFFFFFF' é desassemblada como o início de um bloco básico, continuará desassemblando até o 'retn'.

O 'jnz loc_140001078' salta de volta para formar um loop. Isso será adicionado como uma referência, e o destino do jnz será adicionado à fila de desassemblagem, assim como o ramo de fallthrough (a próxima instrução). A instrução salta no meio do bloco já analisado, portanto não pode simplesmente formar um novo bloco e desassemblar novamente até o 'retn', pois teria uma representação duplicada.

O 'jnz' (salto condicional) também usaria o ramo de fallthrough para criar um novo bloco básico. Agora haveria 4 blocos básicos que se parecem com isto:

Bloco A:```asm
or rax, 0FFFFFFFFFFFFFFFFh
loc_140001078:
inc rax
cmp word ptr [rcx+rax*2], 0
jnz short loc_140001078

root@kitploit:~
Bloco B:```asm  
loc_140001078:  
    inc     rax  
    cmp     word ptr [rcx+rax*2], 0  
    jnz     short loc_140001078  

Block C:```asm
retn

root@kitploit:~
Bloco D:```asm  
    retn  

Isto é uma representação incorreta dos blocos básicos, pois duplica as mesmas instruções em 4 blocos. Isso pode ser corrigido dividindo quaisquer blocos básicos que se sobreponham à disassemblagem atual. Não haverá mais instruções duplicadas, porque em vez de criar uma representação duplicada das instruções em cada sobreposição, as instruções existentes seriam transferidas para o novo bloco básico. A representação correta usando divisão é a seguinte:

Bloco A:```asm
or rax, 0FFFFFFFFFFFFFFFFh

root@kitploit:~
Bloco B:```asm  
    inc     rax  
    cmp     word ptr [rcx+rax*2], 0  
    jnz     short loc_140001078  

Bloco C:```asm
retn

root@kitploit:~
### 2.2.2. Fluxo de controle indireto

#### 2.2.2.1. Tabelas de desvio

Tabelas de desvio são usadas para armazenar os endereços dos manipuladores para instruções switch. Em vez de ter muitas instruções if/saltos condicionais para cada caso em uma instrução switch, uma tabela com os endereços dos manipuladores dos casos é mantida. Aqui está um exemplo (binário LLVM/CLANG):```cpp  
std::int32_t sub_140004080(const std::int32_t a1)  
{  
  std::int32_t result;

  switch ( a1 )  
  {  
    case 0:  
      result = 9;  
      break;  
    case 1:  
      result = 4;  
      break;  
    case 2:  
      result = 3;  
      break;  
    case 3:  
      result = 1;  
      break;  
    default:  
      result = 0;  
      break;  
  }

  return result;  
}  

Esta instrução switch compila para o seguinte assembly:```asm
; ecx = a1
cmp ecx, 3 ; check if above bounds, must be default case
ja short def_140004097 ; goto default if a1 above 3
mov ecx, ecx
mov eax, ecx
lea rcx, jpt_140004097
movsxd rax, ds:(jpt_140004097 - 140004100h)[rcx+rax*4] ; select correct index of jump table for a1
add rax, rcx
jmp rax ; goto index specified - the case handler

jpt_140004097:
dd offset loc_140004099 - 140004100h ; address of handler for first case
dd offset loc_1400040D5 - 140004100h ; address of handler for second case
dd offset loc_1400040BD - 140004100h ; address of handler for third case
dd offset loc_1400040C9 - 140004100h ; address of handler for fourth case

root@kitploit:~
O valor 'a1' é comparado com o valor máximo dos casos, e se estiver acima, irá diretamente para o manipulador padrão. Se a1 estiver dentro do intervalo de casos, então acessa sua entrada na tabela de saltos e salta para o endereço do manipulador calculado.

As entradas da tabela de saltos, bem como as referências à tabela de saltos, são rastreadas para que sejam mantidas intactas.

##### 2.2.2.1.1. Tabelas de saltos limitadas

Se uma tabela de saltos não descreve todos os intervalos de valores que uma instrução switch pode usar, então ela usará uma tabela limitada para verificar os limites. Isso é para redirecionar o switch para a instrução default se estiver fora dos limites. Para analisar o número de instruções case, a instrução de comparação é verificada para encontrar o número de entradas. Por exemplo, a instrução `cmp ecx, 3` mostra que a quantidade de entradas na tabela de saltos é 3.

##### 2.2.2.1.2. Tabelas de saltos ilimitadas

Se uma tabela de saltos preenche todos os valores possíveis que as instruções case podem ter (por exemplo, do valor mínimo de UINT8 ao valor máximo de UINT8 para o tipo UINT8), então ela usará uma tabela ilimitada sem verificações de limite. Isso ocorre porque o compilador sabe que as tabelas de saltos cobrem todos os valores possíveis. Não há instrução de comparação para sugerir o número de entradas da tabela de saltos, portanto as entradas devem ser forçadas por brute force. A base da tabela é verificada incrementalmente em busca de RVAs válidos em uma seção de código, e cada entrada válida é rastreada. Isso não é tão seguro, pois pode analisar outros dados/instruções como entradas da tabela de saltos, motivo pelo qual a verificação de tabelas de saltos limitadas é usada sempre que possível.

##### 2.2.2.1.3. Diferentes tipos de tabelas de saltos

O re-escritor binário suporta tabelas de saltos em binários construídos com MSVC (incluindo tabelas multinível), LLVM/CLANG e GCC.

As tabelas de saltos do MSVC têm 2 formas: normal e multinível. As tabelas de saltos normais para MSVC são um array de RVAs. Cada RVA aponta para o manipulador da instrução case.

As tabelas multinível do MSVC são usadas para instruções switch com um grande número de instruções case que compartilham manipuladores. A versão multinível possui 2 tabelas: 1 para o array de RVAs dos manipuladores e 1 para combinar os valores dos casos com os índices na primeira tabela. Isso evita a repetição dos RVAs na primeira tabela, pois cada valor de caso precisa apenas descrever o índice de 1 byte em vez de um RVA de 4 bytes. Aqui está um exemplo de uma tabela de saltos multinível:```asm  
lea     rdx, cs:140000000h  
movsxd  rax, edi ; load value  
movzx   eax, ds:(byte_1400023D8 - 140000000h)[rdx+rax] ; get handler index by value  
mov     ecx, ds:(jpt_14000209D - 140000000h)[rdx+rax*4] ; get RVA of handler by handler index  
add     rcx, rdx  
jmp     rcx

jpt_14000209D dd offset loc_14000209F - 140000000h  
dd offset loc_1400020AB - 140000000h  
dd offset loc_1400020B7 - 140000000h  
dd offset loc_1400020CF - 140000000h  
dd offset loc_1400020E7 - 140000000h  
dd offset loc_1400020F3 - 140000000h  
dd offset loc_1400020FF - 140000000h  
dd offset loc_14000210B - 140000000h  
dd offset loc_140002117 - 140000000h  
dd offset loc_140002123 - 140000000h  
dd offset loc_1400020C3 - 140000000h  
dd offset loc_14000213B - 140000000h  
dd offset loc_140002147 - 140000000h  
dd offset loc_140002153 - 140000000h  
dd offset loc_14000216B - 140000000h  
dd offset loc_140002177 - 140000000h  
dd offset loc_140002183 - 140000000h  
dd offset loc_14000219B - 140000000h  
dd offset loc_1400021A7 - 140000000h  
dd offset loc_1400021B3 - 140000000h  
dd offset loc_1400021BF - 140000000h  
dd offset loc_1400021CB - 140000000h  
dd offset loc_1400021D7 - 140000000h  
dd offset loc_1400021E3 - 140000000h  
dd offset loc_1400021EF - 140000000h  
dd offset loc_1400021FB - 140000000h  
dd offset loc_140002207 - 140000000h  
dd offset loc_140002213 - 140000000h  
dd offset loc_14000221F - 140000000h  
dd offset loc_14000222B - 140000000h  
dd offset loc_140002237 - 140000000h  
dd offset loc_140002243 - 140000000h  
dd offset loc_14000224F - 140000000h  
dd offset loc_14000225B - 140000000h  
dd offset loc_140002267 - 140000000h  
dd offset loc_140002273 - 140000000h  
dd offset loc_14000227F - 140000000h  
dd offset loc_14000228B - 140000000h  
dd offset loc_140002294 - 140000000h  
; ... more handler addresses

byte_1400023D8:  
db 0, 2Bh, 1, 2, 2Bh, 3, 2Bh, 4, 2Bh, 5, 6, 7, 8, 9, 0Ah, 0Bh, 0Ch, 0Dh  
db 2Bh, 0Eh, 0Fh, 10h, 2Bh, 11h, 12h, 13h, 14h, 15h, 2Bh, 16h, 2Bh, 17h  
db 18h, 19h, 1Ah, 1Bh, 1Ch, 2Bh, 1Dh, 1Eh, 1Fh, 20h, 21h, 22h, 2Bh, 23h  
db 24h, 25h, 26h, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh, 2Bh  
; ... more indexes to handler table  

LLVM uses a table containing offsets relative to the base of the table. By adding the address of the base of the table with the offset described by the entry the handler address can be calculated.

GCC's jump table is an array of DIR64 relocations. Each relocation points to a case statement's address. Relocations are already tracked so those jump table entries are already fixed up. At runtime, those relocation entries will be offset by the base address so each entry can be dereferenced for the address of the case statement.

2.2.2.2. Casos extremos

There are some other forms of indirect control flow that have to be supported. For example, on CLANG binaries using FuncInfo3 C++ exceptions, the continuation address is loaded into the register rax and then returned to the caller. The caller will jump to the continuation address.```asm
lea rax, [rip+X]
retn

root@kitploit:~
Mesmo que o endereço para o qual o lea aponte esteja em uma seção de código, não há garantia de que seja realmente código. Tabelas de desvio e strings podem ser colocadas nas seções de código de um binário para localidade de cache. Os caminhos de código corretos devem ser descobertos e desmontados para rastrear as referências RVA dentro dele (além de ser possível ofuscá-lo), portanto é crucial que esses casos sejam identificados. Essas são referências 'arriscadas'.

Para corrigir a adição incorreta de tabelas de desvio à fila de desmontagem, o desmontador verifica primeiro se é uma tabela de desvio antes de adicionar essas referências arriscadas à fila.

Para corrigir a adição incorreta de strings com lea à fila de desmontagem, o desmontador tenta desmontá-lo como um bloco básico com verificações de sanidade extras (por exemplo, deve ter uma instrução de terminação se for uma dessas referências arriscadas). Se alguma dessas verificações de sanidade falhar em um bloco de referência arriscada, o bloco inteiro é ignorado e assumido como dados.

Se um arquivo de símbolos for fornecido, então nenhum símbolo de dados não será adicionado como uma referência arriscada.

### 2.2.3. Funções

Algumas passagens de ofuscação exigem saber quais blocos básicos pertencem a quais funções. Por esta razão, todos os blocos básicos são atribuídos às funções correspondentes que os possuem. Arquivos de símbolos são analisados para encontrar todos os endereços de funções e colocá-los em uma lista.

Para cada função, as seguintes etapas são realizadas:

- Obter o bloco básico de entrada da função (o bloco básico no endereço inicial da função).  
- Atribuir este bloco de entrada à função.  
- Encontrar todas as saídas do bloco básico (ramos de queda, ramos de destino).  
- Para cada bloco básico de saída, atribuí-lo à função se não for o bloco de entrada de outra função (RVA do bloco básico != RVA de qualquer função). Estas últimas 2 etapas são repetidas para cada bloco básico descoberto.  
- Quaisquer tabelas de desvio nos blocos descobertos são analisadas e seus blocos básicos de destino são atribuídos à função.

### 2.2.4. Tratamento de chamadas 'Noreturn'

Funções Noreturn não retornam. Se uma chamada noreturn ocorrer, o bloco básico continuará a ser desmontado, pois chamadas não terminam blocos básicos. Não se espera que o binário execute após a chamada, então o compilador não inseriu código adequado que termine o bloco básico. As verificações de desmontagem mencionadas provavelmente capturarão esses casos e terminarão o bloco básico.```asm  
sub_140006310 proc  
    sub     rsp, 38h  
    mov     rax, cs:__security_cookie  
    xor     rax, rsp  
    mov     [rsp+38h+var_8], rax  
    mov     [rsp+38h+pExceptionObject], 2Ah  
    lea     rdx, __TI1H  
    lea     rcx, [rsp+38h+pExceptionObject]  
    call    _CxxThrowException  
    db 0CCh  
sub_140006310 endp  
algn_14000633D:  
    align 20h  

Por exemplo, nesta chamada noreturn _CxxThrowException, o compilador inseriu padding na forma de instruções INT3, que seriam capturadas tanto como padding quanto como instrução de término. Existem outros casos, como instruções UD2 inseridas após uma chamada noreturn, que também são tratados.

Esta não é uma forma infalível de detectá-lo, como a equipe CodeDefender também discute, mas existem contramedidas em vigor para corrigir qualquer desmontagem que tenha ido longe demais. Se alguma entrada de tabela de saltos for encontrada dentro de um bloco básico, então o bloco básico é dividido para que a tabela de saltos receba prioridade. Isso impediria que entradas de tabela de saltos fossem desmontadas como instruções após uma chamada noreturn. Se as próximas instruções forem código válido, o bloco básico acabaria sendo dividido de qualquer forma quando o bloco básico do próximo endereço estiver sendo desmontado/processado.

2.3. Suporte a exceções

2.3.1. Suporte a desenrolamento

O ofuscador faz uso de alocações de pilha em suas passagens de ofuscação. Isso permite salvar o valor dos registradores que utiliza (ex.: push rax) e restaurá-los após a conclusão da passagem de ofuscação, para que os registradores não sejam sobrescritos. Um ponteiro de quadro é um registrador que aponta para uma localização específica na pilha.

Todas as alocações de pilha devem ser descritas pelos códigos de desenrolamento de uma função (se um ponteiro de quadro não estiver presente), o que permite ao manipulador de exceções do SO rastrear a pilha até o endereço de retorno e procurar o manipulador de exceções da aplicação. Essas alocações de pilha devem estar no prólogo (início da função) devido a um limite de quão longe podem estar do início da função. Se as alocações de pilha forem feitas fora do prólogo, os códigos de desenrolamento não podem descrevê-las e o SO não conseguiria desenrolar, quebrando o suporte a exceções.

Se um ponteiro de quadro for usado, o SO não precisa ser capaz de rastrear a pilha a partir do registrador rsp, e pode rastreá-la a partir do ponteiro de quadro. Isso significa que o ofuscador pode fazer alocações de pilha fora do prólogo sem ter que descrevê-las nos códigos de desenrolamento.

2.3.1.1 Implementação

O reescritor inserirá um registrador de ponteiro de quadro em toda função em tempo de execução que ainda não possua um. Isso permite que o SO desenrole a pilha mesmo após o ofuscador ter feito alocações de pilha fora do prólogo.

Abaixo está um exemplo das alterações feitas no prólogo e epílogo da função:

Prólogo da função original:```asm
DriverEntry proc
sub rsp, 38h

root@kitploit:~
Prólogo da função modificada:```asm  
DriverEntry proc  
    push    rbp  
    push    rbp  
    sub     rsp, 38h  
    lea     rbp, [rsp]  
    lea     rbp, [rsp]  

Epílogo da função original:```asm
add rsp, 38h
ren
DriverEntry endp

root@kitploit:~
Epílogo da função modificada:```asm  
    add     rsp, 38h  
    pop     rbp  
    pop     rbp  
    retn
DriverEntry endp  

Figura 3. Layout da pilha antes da inserção do ponteiro de quadro.

O registrador não volátil rbp é usado como ponteiro de quadro pelo reescritor. Registradores não voláteis devem ser preservados, então o valor de rbp é empurrado no prólogo e descrito por códigos de unwind (para que o desenrolador do SO possa restaurar o valor original de rbp). O registrador rbp é empurrado 2 vezes para realinhar a pilha para 16 bytes; o segundo empurrão é puramente para fins de realinhamento.

Como o valor de rbp é empurrado duas vezes no prólogo, ele também deve ser retirado no final da função para retornar o ponteiro de pilha ao seu valor original. Isso é feito para que o endereço de retorno esteja em [rsp] para a instrução de retorno.

Os respectivos códigos de unwind são inseridos para essas instruções de empurrão, para que o SO saiba que há mais alocações de pilha no prólogo (para desenrolar o ponteiro de quadro). É por isso que há 2 pops no epílogo.

Para encontrar esses blocos básicos de saída/epílogos, todos os blocos básicos de uma função são pesquisados por um 'ret' ou por um salto que vá para fora da função atual. Saltos indiretos (por exemplo, jmp rcx) também contam como indo para fora da função atual, exceto para tabelas de salto. Com todos os blocos básicos de saída agrupados, as 2 instruções pop podem ser inseridas neles para garantir que os efeitos dos empurrões no prólogo sejam revertidos ao sair da função.

O 'lea rbp, [rsp]' no final do prólogo está lá para informar ao SO uma localização concreta da pilha a partir da qual ele pode desenrolar, em vez de desenrolar a partir de rsp. As respectivas informações de unwind e códigos são inseridos para esta instrução de definição do ponteiro de quadro.

Figura 4. Layout incorreto da pilha após a inserção do ponteiro de quadro.

A outra consideração são os argumentos de pilha, que estão localizados antes do ponteiro de pilha. Se em uma função a pilha for acessada após a alocação local (igual ou após o slot do endereço de retorno), essas referências devem ser atualizadas. Isso ocorre porque os empurrões ajustam a pilha em 16 bytes, então todos os dados referenciados após eles também precisam ser deslocados em 16 bytes. O diagrama acima mostra como as referências de pilha ficam desalinhadas e precisam ser corrigidas.

Por exemplo, 'mov rax, [rsp+0x90]' teria 0x10 (decimal: 16) adicionado a ele para que ainda acesse o mesmo slot de pilha após os empurrões serem executados. A instrução corrigida seria: 'mov rax, [rsp+0xA0]'.

Se uma instrução acessar além da alocação local da pilha, ela será ajustada em 16 para pular os empurrões feitos na pilha. Às vezes, o ponteiro de pilha é movido para registradores diferentes e acessado através de um registrador diferente; nesse caso, esse registrador é monitorado e ajustado da mesma forma que é feito para o ponteiro de pilha.

Outro caso são os manipuladores de captura para exceções, pois em rdx eles recebem o endereço do EstablisherFrame (igual ao valor do nosso registrador de ponteiro de quadro no momento da exceção). Os manipuladores de captura acessam a pilha da função de exceção através de rdx e, portanto, rdx também deve ser rastreado e ajustado.

Abaixo está o diagrama do layout da pilha após ter sido corrigido com os ajustes de referência de pilha.

Figura 5. Layout corrigido da pilha após a inserção do ponteiro de quadro.

O suporte a exceções requer que um arquivo de símbolos seja fornecido ao ofuscador, pois ele precisa ter o máximo de informações possível sobre os símbolos do binário.

2.3.2. Análise de informações de exceção

O reescritor analisa as informações de unwind de um binário para encontrar informações sobre manipuladores de exceção. Quaisquer RVAs encontrados também são rastreados. Os tipos de informações de manipuladores de exceção suportados são:

  • SEH/C_SCOPE_TABLE (exceções no estilo C).
  • Exceções C++ dos tipos FuncInfo3 e FuncInfo4.

2.3.2.1. SEH/C_SCOPE_TABLE

O formato para isso é uma matriz das seguintes entradas de tabela:```cpp
struct c_scope_table_entry_t
{
std::uint32_t begin_rva; // where exception-throwing range begins
std::uint32_t end_rva; // where exception-throwing range ends
std::uint32_t handler_rva; // the handler type/rva (normally 1)
std::uint32_t target_rva; // the catch handler rva
};

struct c_scope_table_t
{
std::uint32_t entry_count;
c_scope_table_entry_t table[1];
};

root@kitploit:~
Os RVAs de início/fim descrevem qual intervalo de código pode lançar uma exceção. O RVA de destino descreve o manipulador catch que processa a exceção quando ela ocorre.

#### 2.3.2.2. FuncInfo3 e FuncInfo4

Usadas para exceções C++, a principal diferença entre FH3 e FH4 é que FH4 usa um formato compactado para tentar economizar memória. Elas compartilham os seguintes descritores:

- Mapa de unwind - lista de objetos C++ que precisam ser destruídos, além do deslocamento do objeto a partir do frame.  
- Mapa de blocos try - lista de manipuladores catch e os tipos que cada um pode capturar (ex.: std::runtime_error).  
- Mapa IP2State - descreve o estado dos objetos dependendo de qual é o ponteiro de instrução atual / o deslocamento na função.

Especificidades do FH3:

- O endereço de continuação no mapa de blocos try é mantido em código e retornado pelo manipulador catch em rax (ex.: lea rax, continuation_address).

Especificidades do FH4:

- Armazena informações do mapa em um formato de inteiro compactado para economizar espaço.  
- O mapa de blocos try contém o endereço de continuação codificado na estrutura de informações do FH4.

Apenas MSVC e CLANG/LLVM são suportados para exceções C++. O GCC não é suportado para exceções C++, pois usa um formato diferente de FH3/FH4.

### 2.3.3. Parsing de RTTI e ThrowInfo

Informações de tipo em tempo de execução (RTTI) e informações de throw (ThrowInfo) são estruturas usadas para introspectar tipos C++ em tempo de execução, inclusive para lançar exceções. Essas estruturas contêm muitos RVAs e, portanto, devem ser rastreadas por questões de estabilidade.

#### 2.3.3.1. RTTI

O mapa de blocos try nos descritores de exceção C++ contém informações de tipo para saber se capturam o tipo lançado. Essa informação de tipo é chamada de RTTI e também descreve outras coisas sobre o tipo, como:

- Tabelas de funções virtuais.  
- Nome do tipo.  
- Classes herdadas.

Para classes sem funções virtuais, tudo o que é gerado é um descritor de tipo:```cpp  
struct type_descriptor_t  
{  
	std::uint64_t vftable_address; // this is a DIR64 relocation  
	std::uint64_t unk;  
	char name[1];  
};  

Isto é encontrado ao escanear as seções de dados pela realocação DIR64 no campo membro 'vftable_address', que é verificado se é uma tabela de funções virtuais real.

Para classes com funções virtuais, um localizador de objeto completo e um descritor de hierarquia de classes são gerados. O descritor de hierarquia de classes contém um array de classes base.```cpp
struct complete_object_locator_t
{
std::uint32_t signature;
std::uint32_t offset;
std::uint32_t constructor_offset;
std::uint32_t type_rva;
std::uint32_t hierarchy_rva;
std::uint32_t self_rva;
};

struct hierarchy_descriptor_t
{
std::uint32_t signature;
std::uint32_t attributes;
std::uint32_t base_class_count;
std::uint32_t base_class_list_rva;
};

struct base_class_array_t
{
std::uint32_t class_rvas[1];
};

struct base_class_descriptor_t
{
std::uint32_t type_rva;
std::uint32_t element_count;
std::uint32_t member_displacement;
std::uint32_t unk;
std::uint32_t unk1;
std::uint32_t attributes;
std::uint32_t hierarchy_rva;
};

root@kitploit:~
#### 2.3.3.2. ThrowInfo

ThrowInfo é usado para descrever como destruir o objeto de exceção depois de processado, bem como o tipo lançado. As classes herdadas do tipo de exceção lançado são descritas no array de tipos capturáveis (contém referências RTTI) para garantir que o manipulador de exceção possa correspondê-lo às instruções catch.

O ThrowInfo é examinado nas seções de dados verificando o conteúdo do array de tipos capturáveis com as informações RTTI descobertas anteriormente. Todos os RVAs dos tipos capturáveis e do ThrowInfo são adicionados à lista de rastreamento.```cpp  
struct throw_info_t  
{  
	std::uint32_t attributes;  
	std::uint32_t pmfn_unwind; // address of exception object destructor  
	std::uint32_t forward_compat;  
	std::uint32_t catchable_type_array;  
};

struct catchable_type_array_t  
{  
	std::uint32_t count;  
	std::uint32_t type_rvas[1];  
};

struct catchable_type_t  
{  
	std::uint32_t attributes;  
	std::uint32_t rva_type;  
	std::uint32_t mdisp;  
	std::uint32_t pdisp;  
	std::uint32_t vdisp;  
	std::uint32_t size_of_thrown_object;  
	std::uint32_t optional_copy_constructor_rva;  
};  

3. Ofuscação

3.1. Máquina virtual

Esta técnica pega instruções da arquitetura x86-64 e as traduz para uma arquitetura de CPU virtual. Isso é muito mais difícil de analisar, pois um engenheiro reverso teria primeiro que entender a arquitetura da CPU virtual antes de analisar o que as instruções originais fazem.

Esta implementação utiliza uma abordagem genérica para gerar manipuladores de máquina virtual, de modo que uma ampla gama de instruções possa ser ofuscada sem a necessidade de codificar manualmente manipuladores para cada instrução x86-64.



Figura 6. Arquitetura da máquina virtual.

A sequência original de instruções que são virtualizadas é substituída por uma chamada ao bloco de entrada da máquina virtual.

Ao entrar na máquina virtual, todos os registradores de propósito geral (exceto rsp) são empurrados para a pilha. O registrador rflags também é empurrado para a pilha. Esses slots da pilha são usados como registradores virtuais, cada um correspondendo ao seu registrador original (então o slot para rax seria usado no lugar de rax). A ordenação desses registradores virtuais na pilha é aleatória, de modo que o layout dos registradores de cada manipulador da máquina virtual muda.

As instruções push para colocar os registradores de propósito geral no layout da pilha virtual também são aleatorizadas e podem ser um 'sub rsp, 8; mov [rsp] reg' ou um 'push reg'. Isso é feito para dificultar a criação de assinaturas para a entrada da máquina virtual.

Registradores de hardware são os registradores de propósito geral da arquitetura x86-64. Agora que os registradores de hardware foram salvos em seu estado de CPU virtual na pilha, eles estão livres para serem sobrescritos. O estado da máquina virtual rastreia uma lista de registradores de hardware atualmente disponíveis que podem ser usados pelos stubs da máquina virtual. Quando o stub é concluído, esses registradores de hardware são adicionados de volta à lista para que possam ser reutilizados.

Agora a aplicação entrou na máquina virtual e é hora de passar a execução para os manipuladores das instruções alvo. Instruções alvo são as instruções x86-64 que estão sendo virtualizadas para esta arquitetura de CPU.

Primeiro, os operandos da instrução alvo precisam ser carregados na pilha. Os valores dos operandos são carregados em um registrador de hardware livre. Os valores dos operandos são então ofuscados e empurrados para a pilha. Isso é feito a partir do bloco anterior ao manipulador de instrução (manipulador anterior ou bloco de entrada da máquina virtual, se este for o primeiro manipulador).

A ofuscação aplicada aos valores dos operandos é a seguinte:

  • Operação xor de número aleatório de 16 bits no valor do operando.
  • Operação de negação de complemento de um no valor do operando.

Para operandos imediatos, esta ofuscação pode ser feita no momento da ofuscação, pois o valor é conhecido, então o cálculo não é feito em tempo de execução e, portanto, é mais difícil de reverter.

Operandos ocultos que exigem registradores específicos (por exemplo, rsi e rdi para 'rep movsb') são carregados nesses registradores específicos em vez de um registrador de hardware aleatório.

No bloco do manipulador de instrução, os operandos são retirados da pilha e desofuscados. A operação reversa é executada para obter os valores originais dos operandos.

Se a instrução original lê do registrador rflags, então rflags é carregado do contexto da pilha antes de executar a instrução.

Se a instrução original escreve no registrador rflags, então rflags é carregado do contexto da pilha antes de executar a instrução. Após a execução da instrução original, o rflags atualizado é escrito de volta no contexto da pilha.

Isso garante que as instruções virtualizadas tenham exatamente o mesmo comportamento de flags que as instruções originais.

Dentro do bloco do manipulador de instrução, a instrução x86-64 original é codificada para usar operandos desofuscados. Uma vez que a instrução original é executada, os operandos de resultado são ofuscados e empurrados para a pilha.

Se este for o último manipulador de instrução, o próximo bloco básico será o bloco de saída da máquina virtual. Caso contrário, o próximo bloco será do próximo manipulador.

O próximo bloco básico retira os operandos de resultado ofuscados da pilha e aplica o mesmo processo de desofuscação. Os valores de resultado são escritos em seu destino original. Isso pode ser um registrador virtual no contexto da pilha ou um local específico na memória descrito pela instrução original.

Esse processo se repete até que uma instrução no bloco básico não possa ser virtualizada (por exemplo, uma instrução que usa o registrador rsp).

O contexto da máquina virtual precisa então ser descarregado para fazer a transição de volta para o código não virtualizado. Todos os registradores virtuais são retirados da pilha para seus registradores de propósito geral correspondentes. O registrador rflags modificado também é restaurado ao retirá-lo da pilha. Agora, o bloco de saída da máquina virtual retorna de volta ao chamador.

3.3.1 Suporte para desenrolamento

Se uma instrução virtualizada lançar uma exceção, o sistema operacional precisa ser capaz de desenrolar a partir do contexto da máquina virtual para encontrar um manipulador de exceção apropriado nos chamadores.

Para permitir isso, informações de desenrolamento precisam ser adicionadas ao binário, de modo que o layout da pilha da função da máquina virtual seja conhecido pelo sistema operacional.

Um ponteiro de quadro é carregado em rbp porque os manipuladores da máquina virtual fazem uso de alocações de pilha fora do prólogo. Isso significa que rbp não pode ser usado como um registrador de hardware 'disponível' pelos manipuladores da máquina virtual.

Todos os registradores de hardware da máquina virtual que são usados são empurrados para o contexto da pilha, então os códigos de desenrolamento correspondentes são inseridos para esses pushes.

A função de tempo de execução para a função da máquina virtual é então inserida no diretório de exceções. A máquina virtual agora é desenrolável.

3.2. Blocos de predicados opacos

Esta técnica cria ramificações para blocos básicos falsos com fluxo de dados incorreto para confundir um engenheiro reverso.

Predicados opacos são declarações que só se avaliam como verdadeiro ou falso.

Blocos básicos são duplicados e envolvidos em uma instrução if de predicado opaco. Um dos blocos terá seu fluxo de dados distorcido para que fique semelhante, mas incorreto.```cpp
if (opaque_statement_always_true)
{
… original basic block
}
else
{
… incorrect but similar basic block
}

root@kitploit:~
Para enganar ainda mais um engenheiro reverso, todos os operandos das instruções são coletados e randomizados. Cada instrução será recompilada com operandos aleatórios da lista coletada. Isso garante que o comportamento do bloco duplicado seja diferente do original.

A localização dos blocos também é embaralhada aleatoriamente, de modo que a localização física deles na memória não revelará qual é o ramo correto.

A condição necessária para a seleção do ramo também é escolhida aleatoriamente. Por exemplo, em uma iteração, o ramo fallthrough do salto condicional levará ao bloco correto. Em outra iteração, o ramo alvo do salto condicional levará ao bloco correto. Isso torna mais difícil encontrar o ramo correto.

A própria expressão de predicado opaco também é muito importante, pois se for fácil de avaliar, não será eficaz. Por essa razão, o [Último Teorema de Fermat](https://en.wikipedia.org/wiki/Fermat's_Last_Theorem) foi escolhido como a expressão opaca. O Último Teorema de Fermat "afirma que não existem três inteiros positivos a, b e c que satisfaçam a equação a^n + b^n = c^n para qualquer valor inteiro de n maior que 2" (onde ^ significa potência). Essa expressão foi comprovada como sempre falsa dadas as condições. Os 3 números a, b, c são escolhidos e elevados a uma potência escolhida aleatoriamente de 3 a 7. A expressão é executada em um bloco básico recém-criado e, em seguida, o desvio condicional é tomado para o bloco correto.

Se os parâmetros a, b, c e n fossem conhecidos, um atacante poderia resolver a expressão e encontrar o ramo correto. Para dificultar isso, os parâmetros são escolhidos a partir de valores decididos em tempo de execução: o ponteiro de pilha (rsp) e o ponteiro de instrução (o ASLR fará com que o rip seja realocado em tempo de execução). Por exemplo:```cpp  
if ((rsp^n) + (rip^n) != (c^n))  
{  
     … incorrect but similar basic block  
}  
else  
{  
    … original basic block  
}  

3.3. Achatamento do fluxo de controle

O fluxo de controle são os caminhos de execução que um programa segue (ex.: 'instruções if'). Uma mudança no fluxo de controle ocorre quando um bloco básico salta para outro. Essas mudanças no fluxo de controle podem ser agrupadas em um único stub de despacho e embaralhadas para dificultar a compreensão. O stub de despacho será responsável por alterar o fluxo de controle para o próximo bloco básico, em vez de isso ser feito diretamente.

Figura 7. Achatamento do fluxo de controle.

Todos os blocos básicos de uma função (exceto o prólogo) são agrupados em uma lista, e todos os seus ramos (condicionais, incondicionais) são coletados. Cada bloco básico recebe um ID/identificador único. Um stub de despacho recebe todos os ramos potenciais e constrói uma instrução switch com todos os IDs como casos. As instruções case saltam para o bloco básico de destino. Todos os saltos para os blocos básicos originais / o fluxo de controle original são substituídos por um salto para a instrução switch com o ID correto (o ID do bloco de destino).

O layout físico dos blocos básicos é embaralhado para que sua localização na memória não forneça pistas sobre qual era o fluxo de controle original.

3.4. Substituição linear

Esta técnica pega quaisquer números codificados em uma instrução (deslocamentos de operandos de memória, operandos imediatos) e oculta seu valor real calculando-os em tempo de execução.

No momento da ofuscação, um valor numérico aleatório é gerado com a mesma largura de bits do número original. Isso é adicionado ao número original e carregado em um registrador não utilizado como um operando imediato.

Em tempo de execução, o número aleatório é subtraído do registrador, resultando no número original.

Se R é o número aleatório e N é o número original, a expressão é efetivamente ((R+N) - R).

Para que os operandos de memória sejam recodificados, a inserção do registrador é feita no operando base. Se já houver um operando base, seu valor é adicionado em cima.

Para operandos de memória de pilha sendo substituídos, há um deslocamento adicionado ao valor para contabilizar o deslocamento da pilha causado pelos pushes. Os pushes são usados para salvar o registrador rflags, bem como o registrador não utilizado.

Aqui está um exemplo da substituição do operando de memória em 'mov [rsp+24h], 0':```asm
push r11
pushfq ; save flags for now
mov r11, rsp
add r11, 1D4D9F71h
sub r11, 1D4D9F3Dh
popfq ; restore flags, the original instruction will now execute and populate flags with the correct values
mov dword ptr [r11], 0 ; original instruction substituted (r11 == rsp+24h)
pop r11

root@kitploit:~
Aqui está outro exemplo da substituição do operando imediato em 'add rbx, 8':```asm  
push    r10  
pushfq  
mov     r10, 0FFFFFFFFE3F78112h  
sub     r10, 0FFFFFFFFE3F7810Ah  
popfq  
add     rbx, r10  
pop     r10  

3.5. Aritmética booleana mista

Esta técnica pega expressões aritméticas regulares (ex. x+y) e as transforma em expressões mais complexas que produzem o mesmo resultado. Isso é feito substituindo expressões por identidades lineares equivalentes.

Por exemplo, (x+y) tem a identidade equivalente ((x & y) + (x | y)). Quando o ofuscador processa (x+y), ele o substituirá por uma dessas identidades, que são mais difíceis de fazer engenharia reversa.

Existe uma lista de identidades para as seguintes instruções aritméticas: 'add', 'sub', 'and', 'or', 'xor'. Isso significa que para cada uma dessas instruções, uma substituição mais complexa e aleatória é escolhida para substituí-la. As identidades sendo escolhidas aleatoriamente de uma lista garantem que cada saída de ofuscação seja diferente.

Para tornar mais difícil a desofuscação, isso é aplicado recursivamente. Isso acaba com cada identidade substituída sendo re-substituída várias vezes, aumentando exponencialmente a complexidade. Abaixo está um exemplo da transformação de (x+y) após 2 aplicações desta técnica:

Figura 8. Aritmética booleana mista.

Um aspecto a considerar é o resultado do cálculo das flags da instrução. A instrução original teria tido um comportamento específico de flags aplicado ao registrador de flags, que não será o mesmo devido à expressão ser dividida em diferentes operações/instruções. Para resolver isso, o ofuscador emula o comportamento das flags inserindo um stub que calcula e aplica as flags corretas.

Para 'and', 'or', 'xor', o comportamento das flags é o mesmo da instrução 'test'; ao inserir uma instrução test com os mesmos operandos, as flags serão emuladas adequadamente para essas 3 instruções substituídas.

Para 'sub', a instrução 'cmp' tem o mesmo comportamento de flags, e pode ser inserida com os mesmos operandos para cálculo das flags.

Para 'add', as flags SF, ZF, PF podem ser calculadas por 'test', mas as flags CF, AF e OF precisam ser calculadas manualmente. Um stub é então inserido que calcula manualmente as flags CF, AF e OF para a instrução add.

Esses stubs de emulação de flags dão uma dica sobre qual era a instrução original, por isso são feitos apenas quando absolutamente necessário. O bloco básico inteiro é escaneado em busca de instruções que leem flags e escrevem flags. As condições para adicionar a emulação de flags são as seguintes:

  • A instrução MBA é a última instrução que escreve flags antes de uma instrução que lê flags.
  • A instrução MBA é a última instrução que escreve flags no bloco básico.

Isso garante que, sempre que o final de um bloco básico for alcançado ou as flags forem lidas, elas sejam mantidas atualizadas pelo stub de emulação de flags. Isso evita que condicionais (ex. instruções if) sigam os ramos errados.

4. Construção

Abaixo estão exemplos de comandos para construir o projeto usando CMake. Inicie a execução destes comandos a partir do diretório raiz do projeto.```
cmake -B build cmake --build build

root@kitploit:~
On Windows systems with Visual Studio installed, the last command can be skipped as the project can be built through the generated Visual Studio solution files (.sln). The Visual Studio solution files will be in the 'build/' folder.

# 5. Uso

The obfuscator has a command line based config system. The following can be configured through the command line arguments:

- Which obfuscation passes to use.  
- Path for input binary file.  
- Path for input symbol file (optional).  
- Path for output binary file (optional).

Below is an overview of the command line arguments:

Usage: binprotect binary-path symbol-path [--out-binary-path VAR] [--control-flow-flattening VAR] [--virtual-machine VAR] [--opaque-predicates VAR] [--linear-substitution VAR] [--mixed-boolean-arithmetic VAR]

Positional arguments:  
  binary-path                                               file path of input binary [required]  
  symbol-path                                               file path of input binary's symbols [optional]

  --out, --out-path, --out-binary-path                      desired file path of output binary  
  --cff, --control-flow-flattening                          enable control flow flattening pass [default: 1]  
  --vm, --virtual-machine                                   enable virtual machine pass [default: 1]  
  --opa, --opaque, --opaque-predicate, --opaque-predicates  enable opaque predicate pass [default: 1]  
  --lin, --linear-substitution                              enable linear substitution pass [default: 1]  
  --mba, --mixed-boolean-arithmetic                         specify amount of mixed boolean arithmetic passes [default: 2]

# 6. Acrônimos

- bin2bin - binary to binary.
- RVA - relative address.  
- MBA - mixed boolean arithmetic.  
- [SEH - structured exception handling](https://learn.microsoft.com/en-us/cpp/cpp/structured-exception-handling-c-cpp).
- FH3 - FuncInfo3.
- FH4 - FuncInfo4.
- [MSVC - Microsoft Visual C++](https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B).  
- [LLVM - low level virtual machine](https://en.wikipedia.org/wiki/LLVM).  
- [CLANG - C/C++ language frontend for LLVM](https://clang.llvm.org/).  
- [GCC - GNU compiler collection](https://en.wikipedia.org/wiki/GNU_Compiler_Collection).
- SF - sign flag.  
- ZF - zero flag.  
- PF - parity flag.  
- CF - carry flag.  
- AF - auxiliary carry flag.  
- OF - overflow flag.

# 7. Créditos

The following individuals gave invaluable advice during the development of the project:

- Aita.  
- Papstuc.  
- Eriktion.  
- IDontCode.  
- Abdulla.  
- Brit.  
- Phage.
Baixar ferramenta