Saltar a contenido

Reversing C++ — Funciones, Stack, Shadow Space (Windows x64)

Apuntes estructurados basados en el análisis del calling convention de Windows x64 y el comportamiento de funciones en C/C++ visto al decompilar.


1. Declaración vs Definición de Funciones

Declaración (prototipo)

Indica al compilador que existe una función con cierto tipo de retorno y tipos de parámetros.

int mayor(int, int);

Características: - No necesita nombres de parámetros. - Permite llamar a la función antes de su definición. - Va al inicio del archivo o en un header .h.

Definición

Contiene el código de la función.

int mayor(int a, int b) {
    if (a > b)
        return a;
    return b;
}

Debe coincidir en: - tipo de retorno
- nombre
- tipos de parámetros


2. main NO es el entry point real

El ejecutable arranca en funciones del CRT (runtime de C), no en main.

El entry point: - Inicializa stack, heap, TLS. - Prepara argumentos, entorno, seguridad. - Finalmente hace call main.

En IDA:
Ctrl + E → entry point real.


3. El Stack en x64

  • El stack es LIFO.
  • RSP apunta al “tope de la pila”.
  • Cada slot típico ocupa 8 bytes.

call

  • Escribe el return address en el stack (push implícito).
  • Salta a la función.

ret

  • Lee el return address desde [RSP].
  • Salta de vuelta.
  • Ajusta RSP.

Dirección de crecimiento

  • El stack crece hacia abajo (hacia direcciones más bajas).
  • Cada reserva o push reduce RSP.

4. Calling Convention de Windows x64

Orden de argumentos

  1. RCX
  2. RDX
  3. R8
  4. R9
  5. Del 5.º en adelante → stack

Valor de retorno

  • Se devuelve en RAX (EAX para enteros de 32 bits).

5. Shadow Space (Home Space)

En Windows x64, toda función llamada debe tener en el caller una reserva obligatoria de 32 bytes justo debajo del return address.

Ese espacio sirve para que la función callee pueda, si quiere, guardar ahí los 4 argumentos pasados por registro.

Layout típico al entrar en una función:

[RSP]        → return address
[RSP+8]      → shadow slot 1 (arg1)
[RSP+16]     → shadow slot 2 (arg2)
[RSP+24]     → shadow slot 3 (arg3)
[RSP+32]     → shadow slot 4 (arg4)
[RSP+40]     → 5º argumento (si existe)
[RSP+48]     → 6º argumento (si existe)
...

El compilador: - Puede copiar los registros ahí. - O dejarlos vacíos si usa los registros directamente.


6. Ejemplo: mayor(int a, int b)

Caller

mov ecx, 5
mov edx, 6
sub rsp, 28h      ; shadow + stack alignment
call mayor
add rsp, 28h

Callee

Puede empezar con:

mov [rsp+8], ecx   ; guarda a
mov [rsp+16], edx  ; guarda b

Luego compara:

cmp ecx, edx
cmovle eax, edx

EAX contiene el valor de retorno.


7. Ejemplo con 6 argumentos

f(a,b,c,d,e,f);
  • a → RCX
  • b → RDX
  • c → R8
  • d → R9
  • e, f → stack

En el callee todos quedan en orden en el stack gracias al shadow space.


8. Stack frame en IDA

IDA permite ver:

  • Layout estático del stack (shadow, arguments, locals).
  • Cambios de RSP con “stack pointer tracking”.
  • Decompilado reconstruyendo parámetros a partir de [rsp+offset].

Ejemplo típico mostrado por IDA:

+----------------------+
| local_20             |
| local_18             |
| local_10             |
| local_8              |
+----------------------+
| arg_20 (6º)          |
| arg_18 (5º)          |
| shadow slot 4        |
| shadow slot 3        |
| shadow slot 2        |
| shadow slot 1        |
+----------------------+
| return address       |

9. Resumen mental para reversing x64

  1. 4 argumentos → RCX, RDX, R8, R9.
  2. Shadow space = 32 bytes entre return address y argumentos extra.
  3. Función puede copiar esos args al stack o no.
  4. 5º y 6º argumento siempre en stack.
  5. RAX/EAX = valor de retorno.
  6. El stack crece hacia direcciones bajas.
  7. IDA permite ver todo el frame bien representado.