Malware Development

Offensive C (2)

Esta entrada del repositorio retoma y expande sobre los conocimientos detallados en Offensive C - Part 1. Esta segunda parte sirve para profundizar en más aspectos básicos y necesarios del lenguaje de C como las estructuras (structs), enums y operaciones lógicas.

Estructuras

Las estructuras, o structs, son tipos de datos definidos por el usuario que permiten agrupar varios elementos de distintos tipos dentro de una misma unidad. Se utilizan para almacenar información relacionada con un objeto o contexto concreto.

Ayudan a organizar grandes cantidades de información de forma clara y permiten acceder fácilmente a cada uno de sus campos. Cada elemento que forma parte de una estructura se conoce como miembro. Durante el desarrollo de malware es habitual encontrarse con APIs de Windows que reciben una estructura como parámetro de entrada. En otros casos, basta con declarar la estructura para que una función del sistema la rellene posteriormente.

La estructura es un ejemplo sencillo de struct definida por Windows:

typedef struct tagTHREADENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ThreadID;
DWORD th32OwnerProcessID;
LONG  tpBasePri;
LONG  tpDeltaPri;
DWORD dwFlags;
} THREADENTRY32;

Declaración de una estructura

Para definir una estructura suele utilizarse la sintaxis typedef struct, que permite declarar la estructura y además crear alias para referirse a ella de forma más cómoda.

typedef struct _STRUCTURE_NAME {

// miembros de la estructura

} STRUCTURE_NAME, *PSTRUCTURE_NAME;

En este ejemplo:

  • _STRUCTURE_NAME es el nombre interno de la estructura.
  • STRUCTURE_NAME es un alias para usar la estructura sin escribir struct.
  • PSTRUCTURE_NAME representa un puntero a esa estructura.

Microsoft suele utilizar el prefijo P para indicar que un tipo es un puntero.

Inicialización de una estructura

Una estructura puede inicializarse directamente al declararla. Una forma habitual es rellenar todos sus campos con cero:

STRUCTURE_NAME struct1 = { 0 };
// o bien
struct _STRUCTURE_NAME struct2 = { 0 };

En ambos casos, todos los miembros quedan inicializados a 0.

Esto es distinto de declarar un puntero a una estructura. En ese caso, lo normal es inicializar el puntero con NULL hasta que apunte a una dirección válida.

PSTRUCTURE_NAME structPointer = NULL;

Acceso a los miembros

Los miembros de una estructura pueden inicializarse o modificarse directamente usando el operador punto (.).

typedef struct _STRUCTURE_NAME {
int ID;
int Age;
} STRUCTURE_NAME, *PSTRUCTURE_NAME;

STRUCTURE_NAME struct1 = { 0 };
struct1.ID  = 1470;
struct1.Age = 34;

También es posible usar una inicialización explícita al declarar la estructura:

STRUCTURE_NAME struct1 = { .ID = 1470, .Age = 34 };

Si se trabaja con un puntero a la estructura, el acceso a los miembros se realiza con el operador flecha (->).

typedef struct _STRUCTURE_NAME {
int ID;
int Age;
} STRUCTURE_NAME, *PSTRUCTURE_NAME;

STRUCTURE_NAME struct1 = { .ID = 1470, .Age = 34 };

PSTRUCTURE_NAME structPointer = &struct1;

structPointer->ID = 8765;
printf("El valor de ID es: %d\n", structPointer->ID);

La expresión:

structPointer->ID

equivale a:

(*structPointer).ID

Primero se desreferencia el puntero y después se accede al miembro de la estructura.

Enumeraciones

Una enumeración, o enum, es un tipo de dato que permite definir un conjunto de constantes con nombre. Se usa cuando una variable solo puede tomar uno de varios valores concretos.

En C, si no se asignan valores manualmente, el compilador comienza en 0 y aumenta en 1 cada elemento posterior.

En desarrollo de malware y tooling ofensivo, las enumeraciones se utilizan a menudo para representar estados, códigos de error, tipos de ejecución o valores de retorno.

enum Dias {
lunes,
martes,
miercoles,
jueves,
viernes,
sabado,
domingo
};

Después puede declararse una variable de ese tipo:

enum Dias dia = viernes;

Y utilizarse en un switch:

switch (dia) {
case lunes:
    printf("Hoy es lunes\n");
    break;
case martes:
    printf("Hoy es martes\n");
    break;
case miercoles:
    printf("Hoy es miercoles\n");
    break;
case jueves:
    printf("Hoy es jueves\n");
    break;
case viernes:
    printf("Hoy es viernes\n");
    break;
case sabado:
    printf("Hoy es sabado\n");
    break;
case domingo:
    printf("Hoy es domingo\n");
    break;
default:
    break;
}

Los elementos de una enum no se acceden con el operador punto (.). Se utilizan directamente por su nombre.

Uniones

Una union es un tipo de dato que permite almacenar varios tipos distintos en la misma región de memoria. Todos sus miembros comparten la misma ubicación, por lo que solo uno de ellos puede contener un valor válido en un momento dado.

No es un tipo que se utilice constantemente, pero aparece con frecuencia en estructuras de Windows y en definiciones internas del sistema.

union ExampleUnion {
int   IntegerVar;
char  CharVar;
float FloatVar;
};

Como todos los miembros comparten memoria, modificar uno de ellos afecta al contenido del resto. Además, el tamaño total de una union es igual al tamaño de su miembro más grande.

El acceso a sus miembros se realiza con el operador punto (.), igual que en una estructura.

Operadores a nivel de bits

Los operadores a nivel de bits permiten modificar bits individuales dentro de un valor binario. Son especialmente importantes en programación de bajo nivel, desarrollo de malware, parsing de estructuras y manipulación de flags.

Los principales operadores son:

  • Desplazamiento a la derecha (>>)
  • Desplazamiento a la izquierda (<<)
  • OR (|)
  • AND (&)
  • XOR (^)
  • NOT (~)

Desplazamiento a la izquierda y a la derecha

Los operadores de desplazamiento mueven los bits de un valor binario hacia la izquierda o hacia la derecha un número determinado de posiciones.

El desplazamiento a la derecha (>>) descarta los bits menos significativos y desplaza el resto hacia la derecha.

El desplazamiento a la izquierda (<<) descarta los bits más significativos y desplaza el resto hacia la izquierda.

Estas operaciones son útiles para multiplicar o dividir por potencias de dos, trabajar con máscaras de bits o manipular campos concretos dentro de un valor.

Operador OR

El operador OR compara dos valores bit a bit. El resultado tendrá un 1 en cada posición donde al menos uno de los dos operandos tenga un 1.

Se usa mucho para activar flags dentro de una máscara de bits.

Operador AND

El operador AND también compara dos valores bit a bit, pero solo devuelve 1 cuando ambos bits tienen valor 1.

Se utiliza normalmente para comprobar si un flag concreto está activado.

Operador XOR

El operador XOR devuelve 1 únicamente cuando los bits comparados son distintos. Si ambos bits son iguales, el resultado es 0.

Se utiliza en operaciones de transformación, cifrado simple y modificación selectiva de bits.

Operador NOT

El operador NOT invierte todos los bits de un valor. Cada 0 pasa a 1 y cada 1 pasa a 0.

Se usa con frecuencia junto a máscaras para limpiar bits concretos dentro de una variable.

Paso por valor

El paso por valor consiste en enviar a una función una copia del dato original. Esto significa que la función trabaja sobre esa copia y no modifica la variable original.

int add(int a, int b)
{
int result = a + b;
return result;
}

int main()
{
int x = 5;
int y = 10;
int sum = add(x, y);

return 0;
}

En este ejemplo, x e y se pasan por valor. La función recibe una copia de ambos enteros.

Paso mediante punteros

En C no existe paso por referencia como tal, sino paso de direcciones mediante punteros. En este caso, la función recibe la dirección de memoria de una variable y puede modificar su contenido original.

void add(int *a, int *b, int *result)
{
*result = *a + *b;
}

int main()
{
int x = 5;
int y = 10;
int sum = 0;

add(&x, &y, &sum);

return 0;
}

En este ejemplo, la función recibe punteros a x, y y sum. Al desreferenciar esos punteros, puede acceder a los valores originales y modificar directamente la variable sum.