Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
binprotect — ofuscador bin2bin para PE x64 que no añade una sección al binario | Kitploit
Herramientas/GitHubGitHub/noahware/binprotect
Análisis EstáticoAnálisis Dinámico (Sandboxing)Ingeniería InversaAnálisis de MalwareAnálisis de Binarios
GitHubnoahware/binprotect

binprotect

ofuscador bin2bin para PE x64 que no añade una sección al binario

Ver Repositorio
304364hace 21 díasRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

Instrucciones de compilación

Vea 4. Compilación para obtener instrucciones de compilación.

Contenido

  1. Introducción
  2. Reescritor binario
    • 2.1. Seguimiento de direcciones relativas
    • 2.2. Desensamblado
      • 2.2.1. División de bloques básicos
      • 2.2.2. Flujo de control indirecto
        • 2.2.2.1. Tablas de saltos
          • 2.2.2.1.1. Tablas de saltos acotadas
          • 2.2.2.1.2. Tablas de saltos no acotadas
          • 2.2.2.1.3. Diferentes tipos de tablas de saltos
        • 2.2.2.2. Casos límite
      • 2.2.3. Funciones
      • 2.2.4. Manejo de llamadas 'noreturn'
    • 2.3. Soporte de excepciones
      • 2.3.1. Soporte de desenrollado
        • 2.3.1.1. Implementación
      • 2.3.2. Análisis de información de excepciones
        • 2.3.2.1. SEH/C_SCOPE_TABLE
        • 2.3.2.2. FuncInfo3 y FuncInfo4
      • 2.3.3. Análisis de RTTI y ThrowInfo
        • 2.3.3.1. RTTI
        • 2.3.3.2. ThrowInfo
  3. Ofuscación
    • 3.1. Máquina virtual
      • 3.1.1. Soporte de desenrollado
    • 3.2. Bloques de predicados opacos
    • 3.3. Aplanamiento de flujo de control
    • 3.4. Sustitución lineal
    • 3.5. Aritmética booleana mixta
  4. Compilación
  5. Uso
  6. Acrónimos
  7. Créditos

1. Introducción

Este ofuscador es bin2bin, lo que significa que toma un binario ejecutable ya compilado y lo reproduce aplicando los pases de ofuscación. Esto se puede usar para proteger una aplicación sin tener acceso al código fuente original. Por ahora solo se admiten archivos PE x64 (ejecutables portátiles), pero hay planes de agregar soporte para otros formatos binarios (por ejemplo, ELF) en el futuro.

Actualmente, todos los ofuscadores bin2bin conocidos insertan una sección al final del binario para colocar el código o los datos ofuscados en su interior. Esto es para que el diseño original del binario se conserve sin tener que cambiar el contenido de las secciones preexistentes. Esto es mucho más fácil de gestionar ya que mantiene la mayoría de las RVA (direcciones relativas) válidas.

Este proyecto adopta un enfoque único para bin2bin, donde cualquier código o dato ofuscado se inserta dentro de las secciones originales del binario. Esto requiere rastrear cada RVA en la aplicación. Los beneficios de este enfoque son:

  • Menos sospechoso para el análisis de malware ya que no se agregan secciones ejecutables adicionales al binario. Nota: esto se probó únicamente con fines educativos y de investigación.
  • Tamaño reducido del binario ejecutable de salida. Esto se debe a que el código original no ofuscado se puede borrar del binario ya que las secciones pueden redimensionarse.
  • Más difícil de analizar, ya que un ingeniero inverso no podrá separar el código ofuscado y no ofuscado solo por las secciones en las que se encuentran, cada rutina tendría que ser revisada para ver si está ofuscada.

Este documento describirá tanto la reescritura del binario ejecutable como las técnicas de ofuscación implementadas. Se han implementado las siguientes técnicas de ofuscación:

  • Máquina virtual.
  • Predicados opacos.
  • Aplanamiento de flujo de control.
  • Sustitución lineal.
  • Aritmética booleana mixta.

Además, este proyecto también tiene soporte de excepciones (excepciones de C++ y SEH) y es capaz de ofuscar funciones que tienen manejo de excepciones.

Para ayudar con el desensamblado y el descubrimiento de código en el binario, se aceptan opcionalmente archivos de símbolos (tanto PDB como MAP). Proporcionar archivos de símbolos no es obligatorio, pero ayuda con el desensamblado en binarios complejos. Algunas funciones como el soporte de excepciones y el aplanamiento de flujo de control requieren que se proporcione un archivo de símbolos.

2. Reescritor binario

Un reescritor binario toma un binario ejecutable y cambia el código o los datos en su interior para producir un binario de salida con los cambios aplicados.

2.1. Seguimiento de direcciones relativas

A medida que el código ofuscado se inserta directamente en las secciones originales del binario, se deben rastrear las direcciones relativas en el programa para que todas las referencias a ellas puedan ajustarse. Esto es para que las referencias sigan apuntando a la misma ubicación después de que se haya insertado el código y los datos. De lo contrario, se accedería a datos o código en la ubicación incorrecta, lo que cambiaría el comportamiento del binario de salida y causaría una inestabilidad grave.

Cada vez que se encuentra una referencia a una dirección relativa (por ejemplo, instrucciones que contienen operandos relativos a rip o directorios de datos PE), se agrega a una lista de seguimiento para actualizarla al final de la reescritura. Se rastrea la RVA donde ocurre la referencia (para saber dónde actualizar la referencia) así como la RVA a la que se hace referencia (para saber con qué RVA actualizar la referencia).

Cada vez que el desensamblador encuentra una instrucción relativa a rip, la agrega a una lista de referencias para actualizar al final de la ofuscación. Esto asegura que todas esas instrucciones sigan apuntando a la ubicación que tenían originalmente. Otros casos de instrucciones relativas, como las tablas de saltos, también se agregan como referencias a actualizar.

Todas las RVA rastreadas deben ajustarse cada vez que se insertan o eliminan bytes del binario. Por ejemplo, aquí está el manejador de inserción 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` es donde se actualiza cada RVA rastreado para reflejar el cambio que ocurrió en el binario. Aquí hay un diagrama de este proceso:

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

Figura 1. Seguimiento de direcciones relativas.

Los datos insertados (azul) desplazan los datos actuales (gris). El RVA referenciado por la instrucción (naranja) se actualiza para apuntar a la misma memoria, teniendo en cuenta los datos insertados (azul).

## 2.2. Desensamblaje

Todas las entradas de código potenciales (exportaciones, punto de entrada, reubicaciones que apuntan a la sección de código, etc.) se añaden a una cola de desensamblaje. Si hay un archivo de símbolos, todas las funciones descritas por dicho archivo también se añaden a la cola de desensamblaje. Cada entrada en la cola se trata como un bloque básico individual.

Un bloque básico es un grupo de instrucciones sin bifurcaciones; esto significa que termina en instrucciones de flujo de control (por ejemplo, salto, ret, int). Los bloques básicos no terminan en llamadas, ya que se espera que retornen en la mayoría de los casos. Algunas funciones no retornan (por ejemplo, _CxxThrowException) y se denominarán llamadas 'noreturn' en adelante.

Cuando se procesa un bloque básico de la cola de desensamblaje, cada instrucción se desensambla comenzando desde el inicio hasta que ocurre una de las siguientes condiciones:

- Se alcanza otro bloque básico ya analizado, lo que provoca una superposición. Consulte "División de bloques básicos".  
- Se encuentra una instrucción de terminación (salto, retorno, int).  
- El desensamblaje de la instrucción ha fallado.  
- Se ha encontrado relleno de código.

A continuación se muestra un diagrama del desensamblaje y la entrada de la cola de desensamblaje (la verificación de relleno de código se omite en el diagrama). Esto se repite hasta que la cola de desensamblaje esté vacía.

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

Figura 2. Procesamiento de desensamblaje.

### 2.2.1 División de bloques básicos

Si dos bloques básicos se superponen, entonces uno de ellos debe dividirse. Esto evita que dos bloques describan las mismas instrucciones. Por ejemplo:```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 es una implementación de wcslen, que obtiene la longitud de una cadena ancha. Cuando la primera instrucción 'or rax, FFFFFFFFFFFFFFFF' se desensambla como el inicio de un bloque básico, continuará desensamblando hasta el 'retn'.

El 'jnz loc_140001078' salta hacia atrás formando un bucle. Esto se añadirá como una referencia, y el destino del jnz se añadirá a la cola de desensamblaje, así como la rama de continuación (la siguiente instrucción). La instrucción salta en medio del bloque ya analizado, por lo que no puede simplemente formar un nuevo bloque y desensamblar de nuevo hasta el 'retn', ya que tendría una representación duplicada.

El 'jnz' (salto condicional) también tomaría la rama de continuación para crear un nuevo bloque básico. Ahora habría 4 bloques básicos que se verían así:

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

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

Bloque C:```asm
retn

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

Esta es una representación incorrecta de los bloques básicos, ya que duplica las mismas instrucciones en 4 bloques. Esto se puede solucionar dividiendo cualquier bloque básico que se superponga con el desensamblado actual. Ya no habrá instrucciones duplicadas porque, en lugar de crear una representación duplicada de las instrucciones en cada superposición, las instrucciones existentes se transferirían al nuevo bloque básico. La representación correcta utilizando la división es la siguiente:

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

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

Bloque C:```asm
retn

root@kitploit:~
### 2.2.2. Flujo de control indirecto

#### 2.2.2.1. Tablas de salto

Las tablas de salto se utilizan para almacenar las direcciones de los manejadores de las sentencias switch. En lugar de tener muchas sentencias if/saltos condicionales para cada caso en una sentencia switch, se mantiene una tabla con las direcciones de los manejadores de los casos. Aquí hay un ejemplo (binario 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 instrucción switch se compila al siguiente ensamblado:

root@kitploit:~
; código de ensamblado sin cambios
``````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  

El valor 'a1' se compara con el valor máximo de los cases, y si está por encima, entonces irá directamente al manejador por defecto. Si a1 está dentro del rango de cases, entonces accede a su entrada en la tabla de saltos y salta a la dirección del manejador calculada.

Las entradas de la tabla de saltos, así como las referencias a la tabla de saltos, se rastrean para que se mantengan intactas.

2.2.2.1.1. Tablas de saltos acotadas

Si una tabla de saltos no describe todos los rangos de valores que puede usar una sentencia switch, entonces usará una tabla acotada para verificar los límites. Esto sirve para redirigir el switch a la sentencia default si está fuera de los límites. Para analizar el número de sentencias case, se verifica la instrucción de comparación para encontrar la cantidad de entradas. Por ejemplo, la instrucción 'cmp ecx, 3' muestra que la cantidad de entradas en la tabla de saltos es 3.

2.2.2.1.2. Tablas de saltos no acotadas

Si una tabla de saltos cubre todos los valores posibles que pueden tener las sentencias case (por ejemplo, desde el valor mínimo de UINT8 hasta el valor máximo de UINT8 para el tipo UINT8), entonces se usará una tabla no acotada sin verificaciones de límites. Esto se debe a que el compilador sabe que las tablas de saltos cubren todos los valores posibles. No hay una instrucción de comparación que indique el número de entradas de la tabla de saltos, por lo que las entradas deben obtenerse por fuerza bruta. La base de la tabla se verifica incrementalmente en busca de RVA válidos en una sección de código, y se rastrea cada entrada válida. Esto no es tan seguro, ya que podría interpretar otros datos/instrucciones como entradas de la tabla de saltos, por lo que se utiliza la verificación de tablas de saltos acotadas cuando sea posible.

2.2.2.1.3. Diferentes tipos de tablas de saltos

El reescritor binario admite tablas de saltos en binarios compilados con MSVC (incluyendo tablas multinivel), LLVM/CLANG y GCC.

Las tablas de saltos de MSVC tienen 2 formas: normal y multinivel. Las tablas de saltos normales para MSVC son un arreglo de RVA. Cada RVA apunta al manejador de la sentencia case.

Las tablas multinivel de MSVC se utilizan para sentencias switch con una gran cantidad de sentencias case que comparten manejadores. La versión multinivel tiene 2 tablas: una para el arreglo de RVA de los manejadores y otra para emparejar los valores de los case con los índices de la primera tabla. Esto evita la repetición de los RVA en la primera tabla, ya que cada valor de case solo necesita describir el índice de 1 byte en lugar de un RVA de 4 bytes. Aquí hay un ejemplo de una tabla de saltos multinivel:```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

root@kitploit:~
LLVM utiliza una tabla que contiene desplazamientos relativos a la base de la tabla. Sumando la dirección de la base de la tabla con el desplazamiento descrito por la entrada, se puede calcular la dirección del manejador.

La tabla de saltos de GCC es un arreglo de reubicaciones DIR64. Cada reubicación apunta a la dirección de una sentencia case. Las reubicaciones ya están rastreadas, por lo que esas entradas de la tabla de saltos ya están ajustadas. En tiempo de ejecución, esas entradas de reubicación serán desplazadas por la dirección base, de modo que cada entrada puede ser desreferenciada para obtener la dirección de la sentencia case.

#### 2.2.2.2. Casos límite

Existen otras formas de flujo de control indirecto que deben ser soportadas. Por ejemplo, en binarios de CLANG que utilizan FuncInfo3 para excepciones de C++, la dirección de continuación se carga en el registro rax y luego se devuelve al llamador. El llamador saltará a la dirección de continuación.```asm  
lea rax, [rip+X]  
retn  

Incluso si la dirección a la que apunta lea se encuentra en una sección de código, no hay garantía de que sea realmente código. Las tablas de saltos y las cadenas pueden colocarse en las secciones de código de un binario por razones de localidad de caché. Las rutas de código correctas deben descubrirse y desensamblarse para poder rastrear las referencias RVA dentro de ellas (además de poder ofuscarlas), por lo que es crucial que estos casos puedan identificarse. Estas son referencias 'arriesgadas'.

Para evitar que las tablas de saltos se añadan incorrectamente a la cola de desensamblado, el desensamblador primero verifica si se trata de una tabla de saltos antes de agregar esas referencias arriesgadas a la cola.

Para evitar que las cadenas referenciadas con lea se añadan incorrectamente a la cola de desensamblado, el desensamblador intenta desensamblarlas como un bloque básico con comprobaciones adicionales de cordura (por ejemplo, debe tener una instrucción terminadora si es una de estas referencias arriesgadas). Si alguna de estas comprobaciones de cordura falla en un bloque de referencia arriesgada, todo el bloque se ignora y se asume que son datos.

Si se proporciona un archivo de símbolos, ningún símbolo de datos se añadirá como referencia arriesgada.

2.2.3. Funciones

Algunos pases de ofuscación requieren saber qué bloques básicos pertenecen a qué funciones. Por esta razón, todos los bloques básicos se asignan a las funciones correspondientes que los poseen. Los archivos de símbolos se analizan para encontrar todas las direcciones de funciones y colocarlas en una lista.

Para cada función, se llevan a cabo los siguientes pasos:

  • Obtener el bloque básico de entrada de la función (el bloque básico en la dirección de inicio de la función).
  • Asignar este bloque de entrada a la función.
  • Encontrar todas las salidas del bloque básico (ramificaciones fallthrough, ramificaciones destino).
  • Para cada bloque básico de salida, asignarlo a la función si no es el bloque de entrada de otra función (RVA del bloque básico != RVA de cualquier función). Estos últimos 2 pasos se repiten para cada bloque básico descubierto.
  • Cualquier tabla de saltos en los bloques descubiertos se analiza y sus bloques básicos destino se asignan a la función.

2.2.4. Manejo de llamadas 'noreturn'

Las funciones noreturn no retornan. Si ocurre una llamada noreturn, el bloque básico continuará desensamblándose, ya que las llamadas no terminan los bloques básicos. No se espera que el binario ejecute más allá de la llamada, por lo que el compilador no ha insertado el código adecuado que termine el bloque básico. Las comprobaciones de desensamblado mencionadas probablemente detectarán estos casos y terminarán el bloque 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

root@kitploit:~
Por ejemplo, en esta llamada `noreturn _CxxThrowException`, el compilador ha insertado relleno en forma de instrucciones INT3, que serían detectadas tanto como relleno y como una instrucción de terminación. Existen otros casos, como instrucciones UD2 insertadas después de una llamada sin retorno, que también se manejan.

Esta no es una forma infalible de detectarlo, como [también discute el equipo de CodeDefender](https://codedefender.io/blog/2024/07/02/), pero existen contramedidas para corregir cualquier desensamblado que haya ido demasiado lejos. Si se encuentran entradas de tabla de saltos dentro de un bloque básico, entonces el bloque básico se divide para que la tabla de saltos tenga prioridad. Esto evitaría que las entradas de la tabla de saltos se desensamblen como instrucciones después de una llamada sin retorno. Si las siguientes instrucciones son código válido, el bloque básico terminaría dividiéndose de todos modos cuando el bloque básico de la siguiente dirección se esté desensamblando/procesando.

## 2.3. Soporte de excepciones

### 2.3.1. Soporte de desenrollado

El ofuscador utiliza asignaciones de pila en sus pasos de ofuscación. Esto le permite guardar el valor de los registros que usa (por ejemplo, push rax) y restaurarlos después de que el paso de ofuscación se complete, para que los registros no se sobrescriban. Un puntero de marco es un registro que apunta a una ubicación específica en la pila.

Todas las asignaciones de pila deben ser descritas por los códigos de desenrollado para una función (si no hay un puntero de marco presente), lo que permite al manejador de excepciones del SO retroceder la pila hasta la dirección de retorno y buscar el manejador de excepciones de la aplicación. Estas asignaciones de pila deben estar en el prólogo (inicio de la función) debido a un límite sobre qué tan lejos pueden estar del inicio de la función. Si las asignaciones de pila se realizan fuera del prólogo, los códigos de desenrollado no pueden describirlas y el SO no podría desenrollar, lo que rompe el soporte de excepciones.

Si se utiliza un puntero de marco, el SO no tiene que ser capaz de retroceder la pila a partir del registro rsp, sino que puede retrocederla a partir del puntero de marco. Esto significa que el ofuscador puede hacer asignaciones de pila fuera del prólogo sin tener que describirlas en los códigos de desenrollado.

#### 2.3.1.1 Implementación

El reescritor insertará un registro de puntero de marco en cada función en tiempo de ejecución que no tenga uno. Esto permite que el SO desenrolle la pila incluso después de que el ofuscador haya realizado asignaciones de pila fuera del prólogo.

A continuación se muestra un ejemplo de los cambios que se realizan en el prólogo y epílogo de la función:

Prólogo original de la función:```asm  
DriverEntry proc  
    sub     rsp, 38h  

Prólogo de función modificado:```asm
DriverEntry proc
push rbp
push rbp
sub rsp, 38h
lea rbp, [rsp]
lea rbp, [rsp]

root@kitploit:~
Epílogo de la función original:```asm  
    add     rsp, 38h  
    ren  
DriverEntry endp  

Epílogo de función modificada:```asm
add rsp, 38h
pop rbp
pop rbp
retn DriverEntry endp

root@kitploit:~
![](https://assets.kitploit.com/production/public/readmes/8091/5974475161fee46781ae597219071d6b2580577065820981370676998617fc1e.png)

Figura 3. Disposición de la pila antes de la inserción del puntero de marco.

El registro no volátil rbp se utiliza como puntero de marco por el reescritor. Los registros no volátiles deben preservarse, por lo que el valor de rbp se inserta (push) en el prólogo y se describe mediante códigos de desenredo (unwind codes) (para que el desenredador del SO pueda restaurar el valor original de rbp). El registro rbp se inserta 2 veces para realinear la pila a 16 bytes; el segundo push es puramente con fines de realineación.

Como el valor de rbp se inserta dos veces en el prólogo, también debe eliminarse (pop) al final de la función para devolver el puntero de la pila a su valor original. Esto es para que la dirección de retorno esté en [rsp] para la instrucción de retorno.

Los códigos de desenredo correspondientes se insertan para estas instrucciones push, de modo que el SO sepa que hay más asignaciones de pila en el prólogo (de dónde desenredar el puntero de marco). Es por eso que hay 2 pops en el epílogo.

Para encontrar esos bloques básicos de salida (epílogo(s)), se buscan en todos los bloques básicos de una función una instrucción 'ret' o un salto que vaya fuera de la función actual. Los saltos indirectos (por ejemplo, jmp rcx) también cuentan como salidas de la función actual, excepto las tablas de salto. Con todos los bloques básicos de salida agrupados, se pueden insertar las 2 instrucciones pop en ellos para asegurar que los efectos de los pushes en el prólogo se reviertan al salir de la función.

La instrucción 'lea rbp, [rsp]' al final del prólogo está ahí para indicarle al SO una ubicación concreta de la pila desde la cual puede desenredar, en lugar de hacerlo desde rsp. La información y los códigos de desenredo correspondientes se insertan para esta instrucción de configuración del puntero de marco.

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

Figura 4. Disposición incorrecta de la pila después de la inserción del puntero de marco.

La otra consideración son los argumentos de pila, que se encuentran antes del puntero de la pila. Si en una función se accede a la pila después de la asignación local (igual o después de la ranura de la dirección de retorno), esas referencias deben actualizarse. Esto se debe a que los pushes ajustan la pila en 16 bytes, por lo que todos los datos referenciados después también deben desplazarse 16 bytes. El diagrama anterior muestra cómo las referencias a la pila se desalinean y deben corregirse.

Por ejemplo, 'mov rax, [rsp+0x90]' tendría añadido 0x10 (decimal: 16) para que siga accediendo a la misma ranura de la pila una vez que se ejecuten los pushes. La instrucción corregida sería: 'mov rax, [rsp+0xA0]'.

Si una instrucción accede más allá de la asignación local de la pila, se ajustará en 16 para saltar por encima de los pushes que se realizan en la pila. A veces, el puntero de la pila se mueve a diferentes registros y se accede a través de un registro distinto; en ese caso, ese registro se monitorea y se ajusta de la misma manera que se hace con el puntero de la pila.

Otro caso son los manejadores catch para excepciones, ya que en rdx reciben la dirección de EstablisherFrame (igual al valor de nuestro registro de puntero de marco en el momento de la excepción). Los manejadores catch accederán a la pila de la función de excepción a través de rdx, por lo que rdx también debe ser seguido y ajustado.

A continuación se muestra el diagrama de la disposición de la pila después de haber sido corregida con los ajustes de referencia de la pila.

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

Figura 5. Disposición de la pila corregida después de la inserción del puntero de marco.

El soporte de excepciones requiere que se proporcione un archivo de símbolos al ofuscador, ya que necesita tener la mayor cantidad de información posible sobre los símbolos del binario.

### 2.3.2. Análisis de información de excepciones

El reescritor analiza la información de desenredo (unwind info) de un binario para encontrar información del manejador de excepciones. También se rastrean las RVA encontradas. Los tipos de información de manejador de excepciones compatibles son:

- SEH/C_SCOPE_TABLE (excepciones al estilo C).
- Excepciones de C++ de tipos FuncInfo3 y FuncInfo4.

#### 2.3.2.1. SEH/C_SCOPE_TABLE

El formato para esto es una matriz de las siguientes entradas de tabla:```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];  
};  

Las RVA de inicio/fin describen qué rango de código puede lanzar una excepción. La RVA de destino describe el controlador de captura que procesa la excepción cuando ocurre.

2.3.2.2. FuncInfo3 y FuncInfo4

Usado para excepciones de C++, la principal diferencia entre FH3 y FH4 es que FH4 utiliza un formato comprimido para intentar ahorrar memoria. Comparten los siguientes descriptores:

  • Mapa de desenrollado (unwind map) - lista de objetos de C++ que necesitan ser destruidos, así como el desplazamiento del objeto desde el marco.
  • Mapa de bloques try - lista de controladores de captura y los tipos que cada uno puede capturar (por ejemplo, std::runtime_error).
  • Mapa IP2State - describe el estado de los objetos dependiendo de qué valor tenga el puntero de instrucción actual o el desplazamiento en la función.

Específicos de FH3:

  • La dirección de continuación en el mapa de bloques try se mantiene en el código y es retornada por el controlador de captura en rax (por ejemplo, lea rax, continuation_address).

Específicos de FH4:

  • Almacena información del mapa en un formato entero comprimido para ahorrar espacio.
  • El mapa de bloques try contiene la dirección de continuación codificada en la estructura de información FH4.

Solo MSVC y CLANG/LLVM son compatibles con excepciones de C++. GCC no es compatible con excepciones de C++ ya que utiliza un formato diferente a FH3/FH4.

2.3.3. Análisis de RTTI y ThrowInfo

La información de tipo en tiempo de ejecución (RTTI) y la información de lanzamiento (throw info) son estructuras utilizadas para inspeccionar tipos de C++ en tiempo de ejecución, incluyendo para lanzar excepciones. Estas estructuras contienen muchas RVA y por lo tanto deben ser rastreadas por razones de estabilidad.

2.3.3.1. RTTI

El mapa de 'bloques try' en los descriptores de excepción de C++ tiene información de tipo para saber si capturan el tipo lanzado. Esta información de tipo se llama RTTI, y también describe otras cosas sobre el tipo, como:

  • Tablas de funciones virtuales.
  • Nombre del tipo.
  • Clases herederas.

Para clases sin funciones virtuales, todo lo que se genera es un descriptor de tipo:```cpp
struct type_descriptor_t
{
std::uint64_t vftable_address; // this is a DIR64 relocation
std::uint64_t unk;
char name[1];
};

root@kitploit:~
Esto se encuentra escaneando las secciones de datos en busca de la reubicación DIR64 en el campo miembro 'vftable_address', el cual se verifica si es una tabla de funciones virtuales real.

Para clases con funciones virtuales, se genera un localizador de objeto completo y un descriptor de jerarquía de clases. El descriptor de jerarquía de clases contiene una matriz de clases 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;  
};  

Para encontrar estos, las secciones de datos se escanean en busca de reubicaciones DIR64 que apunten a un localizador de objetos completo. Se realizan comprobaciones sobre el destino de la reubicación DIR64 para asegurar que apunta a un localizador de objetos completo (por ejemplo, si self_rva apunta a la RVA de la base de la clase, si los descriptores de tipo y los descriptores de jerarquía se analizan correctamente).

2.3.3.2. ThrowInfo

ThrowInfo se utiliza para describir cómo destruir el objeto de excepción una vez procesado, así como el tipo lanzado. Las clases heredadas del tipo de excepción lanzada se describen en la matriz de tipos capturables (contiene referencias RTTI) para garantizar que el manejador de excepciones pueda coincidir con las sentencias catch.

El ThrowInfo se escanea en las secciones de datos comprobando el contenido de la matriz de tipos capturables con la información RTTI descubierta previamente. Todas las RVA de los tipos capturables y el ThrowInfo se añaden a la lista de seguimiento.```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;
};

root@kitploit:~
# 3. Ofuscación

## 3.1. Máquina virtual

Esta técnica toma instrucciones de la arquitectura x86-64 y las traduce a una arquitectura de CPU virtual. Esto es mucho más difícil de analizar, ya que un ingeniero inverso primero tendría que entender la arquitectura de CPU virtual antes de analizar lo que hacen las instrucciones originales.

Esta implementación utiliza un enfoque genérico para generar manejadores de máquina virtual, de modo que se pueda ofuscar una amplia gama de instrucciones sin tener que codificar manejadores para cada instrucción x86-64.

![](https://assets.kitploit.com/production/public/readmes/8091/b21a90f7d3e6c1b79b5bf81f447d00bbf7bc2623987832b1dfdc306936cdd17d.png)
![](https://assets.kitploit.com/production/public/readmes/8091/b3cf262b57cbc3bd3c53fc312e2ad2feb94f68ed77d5a9111879fdf9f52676eb.png)  
![](https://assets.kitploit.com/production/public/readmes/8091/2d5615dbae58ca45fbdaa91f79534d1690a190e6104b87dfc9409ce8a7c91a85.png)  
![](https://assets.kitploit.com/production/public/readmes/8091/996397440d85c087eb220250937ada234e83d7968618a4d69534513e27d675c2.png) 

Figura 6. Arquitectura de la máquina virtual.

La secuencia original de instrucciones que se virtualizan se reemplaza con una llamada al bloque de entrada de la máquina virtual.

Al entrar en la máquina virtual, todos los registros de propósito general (excepto rsp) se colocan en la pila. El registro rflags también se coloca en la pila. Estas ranuras de pila se utilizan como registros virtuales, cada una correspondiente a su registro original (por lo que la ranura para rax se usaría en lugar de rax). El orden de estos registros virtuales en la pila se aleatoriza, por lo que la disposición de los registros de cada manejador de máquina virtual cambia.

Las instrucciones push para colocar los registros de propósito general en la disposición de la pila virtual también se aleatorizan, y pueden ser un 'sub rsp, 8; mov [rsp] reg' o un 'push reg'. Esto se hace para dificultar la creación de firmas para la entrada de la máquina virtual.

Los registros de hardware son los registros de propósito general de la arquitectura x86-64. Ahora que los registros de hardware se han guardado en su estado de CPU virtual en la pila, están libres para ser sobrescritos. El estado de la máquina virtual rastrea una lista de registros de hardware actualmente disponibles que pueden ser utilizados por los stubs de la máquina virtual. Una vez que el stub se completa, esos registros de hardware se añaden de nuevo a la lista para poder reutilizarlos.

Ahora la aplicación ha entrado en la máquina virtual y es momento de pasar la ejecución a los manejadores de las instrucciones objetivo. Las instrucciones objetivo son las instrucciones x86-64 que se están virtualizando en esta arquitectura de CPU.

Primero, los operandos de la instrucción objetivo deben cargarse en la pila. Los valores de los operandos se cargan en un registro de hardware libre. Luego, los valores de los operandos se ofuscan y se colocan en la pila. Esto se hace desde el bloque anterior al manejador de instrucción (manejador anterior o bloque de entrada de la máquina virtual si este es el primer manejador).

La ofuscación aplicada a los valores de los operandos es la siguiente:

- Operación xor con un número aleatorio de 16 bits sobre el valor del operando.  
- Operación de negación de complemento a uno sobre el valor del operando.

Para operandos inmediatos, esta ofuscación se puede realizar en tiempo de ofuscación ya que el valor es conocido, por lo que el cálculo no se realiza en tiempo de ejecución y, por lo tanto, es más difícil de revertir.

Los operandos ocultos que requieren registros específicos (por ejemplo, rsi y rdi para 'rep movsb') se cargan en esos registros específicos en lugar de en un registro de hardware aleatorio.

En el bloque del manejador de instrucción, los operandos se extraen de la pila y se desofuscan. Se ejecuta la operación inversa para obtener los valores originales de los operandos.

Si la instrucción original lee del registro rflags, entonces rflags se carga desde el contexto de la pila antes de ejecutar la instrucción.

Si la instrucción original escribe en el registro rflags, entonces rflags se carga desde el contexto de la pila antes de ejecutar la instrucción. Después de que la instrucción original se ejecuta, el rflags actualizado se escribe de nuevo en el contexto de la pila.

Esto asegura que las instrucciones virtualizadas tengan exactamente el mismo comportamiento de banderas que las instrucciones originales.

Dentro del bloque del manejador de instrucción, la instrucción x86-64 original se codifica para usar operandos desofuscados. Una vez que la instrucción original se ejecuta, los operandos de resultado se ofuscan y se colocan en la pila.

Si este es el último manejador de instrucción, el siguiente bloque básico será el bloque de salida de la máquina virtual. Si no, el siguiente bloque será del siguiente manejador.

El siguiente bloque básico extrae los operandos de resultado ofuscados de la pila y aplica el mismo proceso de desofuscación. Los valores de resultado se escriben en su destino original. Esto podría ser un registro virtual en el contexto de la pila o una ubicación específica en la memoria descrita por la instrucción original.

Este proceso se repite hasta que una instrucción en el bloque básico no puede ser virtualizada (por ejemplo, una instrucción que utiliza el registro rsp).

Luego, el contexto de la máquina virtual debe descargarse para volver al código no virtualizado. Todos los registros virtuales se extraen de la pila a sus correspondientes registros de propósito general. El registro rflags modificado también se restaura extrayéndolo de la pila. Ahora, el bloque de salida de la máquina virtual regresa al llamante.

### 3.3.1 Soporte de desenrollado

Si una instrucción virtualizada lanza una excepción, el sistema operativo necesita ser capaz de desenrollar fuera del contexto de la máquina virtual para poder encontrar un manejador de excepciones apropiado en los llamantes.

Para permitir esto, se debe agregar información de desenrollado al binario para que el diseño de la pila de la función de la máquina virtual sea conocido por el sistema operativo.

Se carga un puntero de marco en rbp porque los manejadores de la máquina virtual hacen uso de asignaciones de pila fuera del prólogo. Esto significa que rbp no puede ser utilizado como un registro de hardware 'disponible' por los manejadores de la máquina virtual.

Todos los registros de hardware de la máquina virtual que se utilizan se colocan en el contexto de la pila, por lo que se insertan los códigos de desenrollado correspondientes para esos pushes.

La función en tiempo de ejecución para la función de la máquina virtual se inserta entonces en el directorio de excepciones. La máquina virtual ahora es desenrollable.

## 3.2. Bloques de predicado opaco

Esta técnica crea ramas a bloques básicos falsos con flujo de datos incorrecto para confundir a un ingeniero inverso.

Los predicados opacos son declaraciones que solo se evalúan como verdadero o falso.

Los bloques básicos se duplican y se envuelven en una declaración if de predicado opaco. Uno de los bloques tendrá su flujo de datos sesgado para que sea similar, pero incorrecto.```cpp  
if (opaque_statement_always_true)  
{  
    … original basic block  
}  
else  
{  
    … incorrect but similar basic block  
}  

Para engañar aún más a un ingeniero inverso, todos los operandos de las instrucciones se recopilan y aleatorizan. Cada instrucción se recompilará con operandos aleatorios de la lista recopilada. Esto asegura que el comportamiento del bloque duplicado sea diferente al original.

La ubicación de los bloques también se mezcla aleatoriamente, por lo que la ubicación física de los mismos en la memoria no revelará cuál es la rama correcta.

La condición requerida para la selección de la rama también se elige aleatoriamente. Por ejemplo, en una iteración, la rama de continuación (fallthrough) del salto condicional llevará al bloque correcto. En otra iteración, la rama destino del salto condicional llevará al bloque correcto. Esto dificulta encontrar la rama correcta.

La expresión del predicado opaco en sí misma también es muy importante, ya que si es fácil de evaluar no sería efectiva. Por esta razón, se ha elegido el Último Teorema de Fermat como expresión opaca. El Último Teorema de Fermat “establece que no existen tres enteros positivos a, b y c que satisfagan la ecuación a^n + b^n = c^n para ningún valor entero de n mayor que 2” (donde ^ significa potencia). Se ha demostrado que esta expresión siempre es falsa dadas las condiciones. Se eligen los 3 números a, b, c y se elevan a una potencia elegida aleatoriamente entre 3 y 7. La expresión se lleva a cabo en un bloque básico recién creado y luego se toma la rama condicional hacia el bloque correcto.

Si se conocieran los parámetros a, b, c y n, un atacante podría resolver la expresión y encontrar la rama correcta. Para dificultar esto, los parámetros se eligen a partir de valores decididos en tiempo de ejecución: el puntero de pila (rsp) y el puntero de instrucción (ASLR hará que el rip se reubique en tiempo de ejecución). Por ejemplo:```cpp
if ((rsp^n) + (rip^n) != (c^n))
{
… incorrect but similar basic block
}
else
{
… original basic block
}

root@kitploit:~
## 3.3. Aplanamiento del flujo de control

El flujo de control son las rutas de ejecución que toma un programa (por ejemplo, 'sentencias if'). Un cambio en el flujo de control ocurre cuando un bloque básico salta a otro. Estos cambios en el flujo de control se pueden agrupar en un único stub de despachador y mezclarse para dificultar su comprensión. El stub de despachador será el encargado de cambiar el flujo de control al siguiente bloque básico en lugar de que se haga directamente.

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

Figura 7. Aplanamiento del flujo de control.

Todos los bloques básicos de una función (excepto el prólogo) se agrupan en una lista, y se recopilan todas sus ramificaciones (condicionales, incondicionales). A cada bloque básico se le asigna un ID/identificador único. Un stub de despachador toma todas las ramificaciones potenciales y construye una sentencia switch con todos los IDs como casos. Las sentencias case saltan al bloque básico objetivo. Todos los saltos a los bloques básicos originales/flujo de control original se reemplazan con un salto a la sentencia switch con el ID correcto (el ID del bloque objetivo).

La disposición física de los bloques básicos se mezcla para que su ubicación en memoria no dé pistas sobre cuál era el flujo de control original.

## 3.4. Sustitución lineal

Esta técnica toma cualquier número codificado en una instrucción (desplazamientos de operandos de memoria, operandos inmediatos) y oculta su valor real calculándolos en tiempo de ejecución.

En el momento de la ofuscación, se genera un valor numérico aleatorio del mismo ancho de bits que el número original. Este se suma al número original y se carga en un registro no utilizado como operando inmediato.

En tiempo de ejecución, el número aleatorio se resta del registro, lo que da como resultado el número original.

Si R es el número aleatorio y N es el número original, la expresión es efectivamente ((R+N) - R).

Para que los operandos de memoria sean recodificados, la inserción del registro se realiza sobre el operando base. Si ya hay un operando base, su valor se suma encima.

Para los operandos de memoria de **pila** que se sustituyen, se añade un desplazamiento al valor para tener en cuenta el desplazamiento de pila causado por los pushes. Los pushes se utilizan para guardar el registro rflags así como el registro no utilizado.

Aquí hay un ejemplo de la sustitución del operando de memoria en '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  

Aquí hay otro ejemplo de la sustitución del operando inmediato en 'add rbx, 8':```asm
push r10
pushfq
mov r10, 0FFFFFFFFE3F78112h
sub r10, 0FFFFFFFFE3F7810Ah
popfq
add rbx, r10
pop r10

root@kitploit:~
## 3.5. Aritmética booleana mixta

Esta técnica toma expresiones aritméticas regulares (por ejemplo, x+y) y las transforma en expresiones más complejas que producen el mismo resultado. Lo hace sustituyendo expresiones por identidades lineales equivalentes.

Por ejemplo, (x+y) tiene la identidad equivalente ((x & y) + (x | y)). Cuando el ofuscador procesa (x+y), lo sustituirá por una de esas identidades que son más difíciles de aplicar ingeniería inversa.

Existe una lista de identidades para las siguientes instrucciones aritméticas: 'add', 'sub', 'and', 'or', 'xor'. Esto significa que para cada una de estas instrucciones se elige una sustitución más compleja y aleatoria para reemplazarla. El hecho de que las identidades se elijan aleatoriamente de una lista asegura que cada salida de ofuscación sea diferente.

Para dificultar la desofuscación, esto se aplica de forma recursiva. Esto termina con cada identidad sustituida siendo re-sustituida múltiples veces, aumentando exponencialmente la complejidad. A continuación se muestra un ejemplo de la transformación de (x+y) después de 2 pases de esta técnica:

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

Figura 8. Aritmética booleana mixta.

Un aspecto a considerar es el resultado del cómputo de banderas de la instrucción. La instrucción original habría tenido un comportamiento específico de banderas aplicado al registro de banderas, el cual no será el mismo debido a que la expresión se divide en diferentes operaciones/instrucciones. Para solucionar esto, el ofuscador emula el comportamiento de las banderas insertando un stub que calcula y aplica las banderas correctas.

Para 'and', 'or', 'xor', el comportamiento de las banderas es el mismo que el de la instrucción 'test'; insertando una instrucción test con los mismos operandos, las banderas se emularán correctamente para esas 3 instrucciones sustituidas.

Para 'sub', la instrucción 'cmp' tiene el mismo comportamiento de banderas y se puede insertar con los mismos operandos para el cálculo de banderas.

Para 'add', las banderas SF, ZF, PF se pueden calcular con 'test', pero las banderas CF, AF y OF deben calcularse manualmente. Luego se inserta un stub que calcula manualmente las CF, AF y OF para la instrucción add.

Estos stubs de emulación de banderas dan una pista sobre cuál era la instrucción original, por lo que solo se realiza cuando es absolutamente necesario. Se escanea todo el bloque básico en busca de instrucciones que lean y escriban banderas. Las condiciones para agregar emulación de banderas son las siguientes:

- La instrucción MBA es la última instrucción que escribe banderas antes de una instrucción que lee banderas.
- La instrucción MBA es la última instrucción que escribe banderas en el bloque básico.

Esto asegura que cada vez que se alcanza el final de un bloque básico o se leen las banderas, se mantengan actualizadas mediante el stub de emulación de banderas. Esto evita que los condicionales (por ejemplo, sentencias if) tomen las ramas incorrectas.

# 4. Compilación

A continuación se presentan comandos de ejemplo para compilar el proyecto usando CMake. Comience la ejecución de estos comandos desde el directorio raíz del proyecto.```  
cmake -B build
cmake --build build

En sistemas Windows con Visual Studio instalado, el último comando se puede omitir, ya que el proyecto se puede compilar a través de los archivos de solución de Visual Studio (.sln) generados. Los archivos de solución de Visual Studio estarán en la carpeta 'build/'.

5. Uso

El ofuscador tiene un sistema de configuración basado en línea de comandos. Lo siguiente se puede configurar a través de los argumentos de línea de comandos:

  • Qué pases de ofuscación utilizar.
  • Ruta del archivo binario de entrada.
  • Ruta del archivo de símbolos de entrada (opcional).
  • Ruta del archivo binario de salida (opcional).

A continuación se presenta una visión general de los argumentos de línea de comandos:

Uso: binprotect ruta-binario ruta-símbolos [--out-binary-path VAR] [--control-flow-flattening VAR] [--virtual-machine VAR] [--opaque-predicates VAR] [--linear-substitution VAR] [--mixed-boolean-arithmetic VAR]

Argumentos posicionales:
binary-path ruta del archivo del binario de entrada [obligatorio]
symbol-path ruta del archivo de símbolos del binario de entrada [opcional]

--out, --out-path, --out-binary-path ruta deseada del archivo del binario de salida
--cff, --control-flow-flattening habilita el pase de aplanamiento de flujo de control [predeterminado: 1]
--vm, --virtual-machine habilita el pase de máquina virtual [predeterminado: 1]
--opa, --opaque, --opaque-predicate, --opaque-predicates habilita el pase de predicados opacos [predeterminado: 1]
--lin, --linear-substitution habilita el pase de sustitución lineal [predeterminado: 1]
--mba, --mixed-boolean-arithmetic especifica la cantidad de pases de aritmética booleana mixta [predeterminado: 2]

6. Acrónimos

  • bin2bin - binary to binary.
  • RVA - relative address.
  • MBA - mixed boolean arithmetic.
  • SEH - structured exception handling.
  • FH3 - FuncInfo3.
  • FH4 - FuncInfo4.
  • MSVC - Microsoft Visual C++.
  • LLVM - low level virtual machine.
  • CLANG - C/C++ language frontend for LLVM.
  • GCC - 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

Las siguientes personas brindaron consejos invaluables durante el desarrollo del proyecto:

  • Aita.
  • Papstuc.
  • Eriktion.
  • IDontCode.
  • Abdulla.
  • Brit.
  • Phage.
Descargar herramienta