Saltar en medio de una función
FENOMENOS EXTRAÑOS
Saltar en medio de una función
2024-09-11
Por
Carolyn Lightrun

El otro día me pregunté… ¿Cómo podría hacer que un programa saltara en medio de una función?. Lo creáis o no, había una razón para pensar eso, pero en el momento de escribir este artículo no la recuerdo… Sin embargo, esa pregunta aparentemente estúpida, me ha llevado a un viaje muy interesante que me gustaría compartir con vosotros.

La verdad que a parte de una forma de ofuscar el código y confundir a los desesambladores/descompiladores, no se me ocurre ninguna otra razón por la que intentar saltar en medio de una función, pero bueno, quizás la haya y acabe recordándolo o alguno de vosotros me deis alguna idea.

La idea es algo como esto:

int foo () {
    puts ("Entramos en Función foo");
en_medio:
    puts ("Código en medio de la función");
    puts ("Salimos de Función foo");
}
int main () {
   goto en_medio;
}

El código de arriba no funciona pero su objetivo es solo ilustrar el concepto del que vamos a hablar en este artículo. La razón de que no funcione es que las etiquetas (labels) solo están definidas dentro de la función en la que se usan y no pueden ser usadas desde fuera de ellas y, por lo tanto, goto no tiene ni idea de que estamos hablando cuando le decimos que salte a en_medio.

setjmp/longjmp

La primera solución que se nos vendría a la cabeza sería utilizar las funciones setjmp y longjmp. Estas funciones para mi despiertan un sentimiento de estar usando magia arcana y la verdad que solo las he visto utilizar por código bastante viejo del de los origines de UNIX y el dialecto K&R de C. Si os preguntáis por su utilidad en el mundo real… bueno, nos permiten implementar en C un comportamiento similar al de las excepciones de otros lenguajes.

Este es un ejemplo clásico de su uso:

#include <stdio.h>
#include <setjmp.h>

jmp_buf b1;

int foo () {
  puts (" > foo inicio\n");
  longjmp (b1, 1);
}

int main () {
  puts ("> main ()\n");
  if (setjmp (b1) != 0) {
    puts ("Llamada especial\n");
  } else
    foo ();
  puts ("> main fin\n");
}

La función setjmp marca un punto de retorno y para ello almacena el estado actual del programa (el valor de los registros del procesador y alguna cosa más) en una estructura global de tipo jmp_buf. La función comprueba el valor de retorno. Si es cero, significa que es la primera ejecución de la función y dicho de forma sencilla, estaremos grabando el estado actual del programa al que volveremos en caso de que pase algo.

Si setjmp retorna un valor distinto de cero, significa que hemos vuelto a este punto del programa debido a una llamada a la función longjmp y por tanto debemos restaurar el estado y que todo siga como la primera vez que llamamos a setjmp.

En nuestro código de ejemplo. La primera llamada a setjmp devolverá cero tras inicializa el estado actual y, acto seguido, ejecutará la función foo.

La función foo hace sus cosas, y en un momento determinado, algo pasa y ejecuta longjmp pasándole como parámetro el estado al que queremos retornar y el valor que va a recibir setjmp. Si, podemos enviar distintos valores con distintas llamadas a longjmp. En nuestro código, el efecto de este longjmp es la impresión del mensaje Llamada Especial.

longjmp fuerza el retorno a la llamada setjmp anterior restaurando el estado, pero devolviendo el valor 1, lo que hace que la condición se cumpla y entremos en la parte then del bloque if.

Parece que esto funciona super bien, así que vamos a utilizarlo para nuestro caso.

Como podéis ver, estas dos funciones también nos permiten hacer un bucle infinito de una forma bastante peculiar… Si lo implementáis Mostradnos el código!

SALTANDO CON LONGJMP

Con todo lo que sabemos de setjmp y longjmp podríamos re-escribir nuestro programa para saltar en medio de una función de la siguiente forma:

#include <stdio.h>
#include <setjmp.h>

jmp_buf b1;

int foo () {
  puts (" > foo inicio\n");
  if (setjmp (b1) != 0) {
    puts ("Llamada especial\n");
  } else
    puts (" > foo final\n");
}

int main () {
  foo (); // poin setjmp
  longjmp (b1, 1);
}

En este caso, tenemos que hacer una llama extra a foo para poder ejecutar setjmp y almacenar el estado que nos permita volver al punto en el que ejecutar nuestro código especial. En otras palabras, setjmp va a definir el punto en medio de la función al que queremos saltar.

Bueno, si compiláis y ejecutáis este programa obtendréis un fallo de segmentación. Lo que sucede es que estas funciones restauran los registros y alguna información adicional, pero no pueden almacenar el estado de la pila. Cuando las usamos como en el caso anterior en el que setjmp esta en el mismo marco de pila o superior a longjmp todo va bien, puesto que longjmp limpiará la pila de forma automática al restaurar el segmento de pila. Pero si lo hacemos al revés, longjmp va a restaurar la pila a un posición que era válida dentro de la función, pero que ya no lo es y además ha podido ser modificada por otro código que se haya ejecutado entre medias.

ABUSANDO DE GOTO

Ya que la forma estándar de saltar entre funciones no ha funcionado como esperamos, vamos a utilizar la forma más básica de saltar a una determinada posición de memoria… goto… la instrucción maldita.

Una forma de implementar esto sería con el siguiente código:

#include <stdio.h>

void *f1 = NULL;

int foo (int a) {
  f1 = &&label_foo1;
  puts ("> foo inicio");
  
  if (a) {
  label_foo1:
    puts ("Llamada especial");
    return 0;
  }
  printf ("> foo final\n");
  return 0;
}

int main () {
  puts ("> main ()");
  foo (0);
  goto *f1;
  puts ("> main fin");
}

Si bien, el programa es corto y tiene pocos elementos, hay muchas cosas que explicar. Lo primero es la declaración de un puntero como una variable global (la primera línea después de los includes). Este puntero tiene que ser global puesto que la forma más directa de obtener un puntero a una parte específica de una función es utilizando etiquetas, y solo podemos acceder a las etiquetas de una función desde esa misma función. Para poder acceder desde fuera tenemos que almacenar ese puntero en un lugar que podamos leer desde otras funciones, como por ejemplo… Una variable global.

La siguiente línea que puede que os llame la atención es

f1 = &&label_foo1;

El operador && es específico de gcc y nos permite obtener la dirección de memoria asociada a una etiqueta. Dicho esto la línea ya no tiene misterios. Estamos almacenando en el puntero f1 la dirección de memoria a la que queremos saltar.

Finalmente, la función main, incluye una primera llamada a foo con un parámetro para inicializar el puntero global. Una vez inicializa podemos saltar usando goto simplemente dereferenciando el puntero.

Veamos si esta solución funciona:¨

$ make jump-ex01
cc     jump-ex01.c   -o jump-ex01
$ ./jump-ex01
> main ()
> foo inicio
> foo final
Llamada especial

Pero parece que tenemos un problemilla con esta implementación. ¿Podéis verlo?…

LOS PROBLEMAS CON GOTO

El primer problema es que tras ejecutar el código de la “Llamada Especial”, en lugar de retornar a main nuestro programa termina. Os imaginaréis que es lo que pasa. Cuando retornamos de foo al haber saltado en medio de la función, seguimos en el stack frame de main, con lo que al restaurar el puntero de pila y retornar, estamos efectivamente haciendo un return desde main.

Esto lo podemos solucionar fácilmente, forzando que la función retorne después de la llamada especial pero sin restaurar el stack_frame, algo como esto:

  if (a) {
  label_foo1:
    puts ("Llamada especial");
    __asm__ ("ret");
  }

Solo con esta modificación el programa producirá una violación de segmento, ya que la dirección de retorno no está en la pila y la instrucción ret que hemos introducido va a saltar a una dirección aleatoria. Para solucionar esto, debemos asegurarnos que el primer valor en la pila sea la dirección de retorno correcta.

Podríamos hacer todo esto insertando ensamblador, pero la verdad que usar ensamblador en GCC de esta forma es muy engorroso, y cuanto más ensamblador introduzcamos menos portable será nuestro programa. Así que lo que vamos a hacer es decirle al compilador que en lugar de saltar (usar la instrucción goto que se convierte en un jmp en ensamblador), ejecute una función (call), la cual ya va a introducir en la pila la dirección correcta para retornar.

Esto lo podemos hacer sustituyendo nuestro goto *f1 por con una línea tan críptica como esta:

((int (*)())f1)();

La línea anterior simplemente hace un cast de nuestra dirección de memoria en medio de foo a una función, y luego le dice al compilador que invoque la función (los () del final). Esto hará que el compilador introduzca una instrucción call que es lo que necesitamos.

El otro problema de usar goto es que la forma en la que lo hemos usado es una extensión de GCC y no sigue el estándar, el cual indica que solo podemos usar como parámetro a goto etiquetas locales. Sin embargo, con la última modificación que hemos hecho, ya hemos solucionado ese problema al dejar de utilizar goto.

MEJORANDO NUESTRO EJEMPLO

Vamos a extender nuestro programa de ejemplo, añadiendo dos puntos de entrada a la función foo y utilizando punteros a funciones directamente, de forma que no es necesario utilizar esos casts tan extraños como el que vimos en la sección anterior.

El programa sería algo como esto:

#include <stdio.h>

int (*f1)() = NULL;
int (*f2)() = NULL;


int foo (int a) {
  f1 = &&label_foo1;
  f2 = &&label_foo2;
  puts ("> foo inicio");
  
  if (a) {
  label_foo1:
    puts ("Llamada especial 1");
    __asm__ ("ret");
    
  label_foo2:
    puts ("Llamada especial 2");
    __asm__ ("ret");

  }
 label_end:
  puts ("> foo final");
  return 0;
}

int main () {
  puts ("> main ()");
  foo (0);
  f1();
  f2();
  puts ("> main fin");
}

Sin sorpresas verdad?. Simplemente hemos declarado nuestras variables globales f1 y f2 como punteros a funciones, y así la llamada en main a ambas etiquetas es tan sencilla como llamar a un función.

El programa funciona correctamente y ahora, lo que vamos a hacer, es intentar que las llamadas especiales no se puedan ver claramente cuando el programa es desensamblado. Como referencia, esto es lo que vemos con el programa tal cual con objdump y con radare2:

Claramente podemos ver las llamadas especiales en ambos casos y radare incluso nos muestra las cadenas. En el resto del artículo solo mostraremos la salida de radare2 que es mucho más completa.

El objetivo ahora es confundir a radare2 para que no nos muestre inmediatamente las funciones especiales que hemos escondido en foo.

OCULTANDO EL CÓDIGO I

Lo primero que vamos a hacer es insertar algunos caracteres aleatorios justo antes de nuestra primera etiqueta, con el objetivo de confundir al desensamblador al encontrar código que no se corresponden con instrucciones. Esto lo podemos hacer de la siguiente forma:

  if (a) {
    __asm__ (".asciz \"ROOR\"");
  label_foo1:
    puts ("Llamada especial 1");
    __asm__ ("ret");

Si ahora vemos la salida de radare2 obtendremos lo siguiente:

Como podemos ver ahora radare no es capaz de encontrar la cadena de la primera llamada especial. Aún podemos ver algo de código de la función, pero ya no es tan evidente como antes. Podemos repetir el proceso antes de la segunda etiqueta para intentar ocultar también la segunda cadena:

Pero si usamos las capacidades de análisis de radare, ejecutando el comando

[0x00001050]> aaaaaa
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Analyze entrypoint (af@ entry0)
INFO: Analyze symbols (af@@@s)
INFO: Recovering variables
INFO: Analyze all functions arguments/locals (afva@@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Analyzing methods
INFO: Recovering local variables (afva)
INFO: Type matching analysis for all functions (aaft)
INFO: Propagate noreturn information (aanr)
INFO: Scanning for strings constructed in code (/azs)
INFO: Finding function preludes (aap)
INFO: Enable anal.types.constraint for experimental type propagation
INFO: Reanalizing graph references to adjust functions count (aarr)
INFO: Autoname all functions (.afna@@c:afla)

Ahora veremos lo siguiente:

La segunda cadena ha vuelto a aparecer. Maldita sea, radare es más listo de lo que pensábamos. Veamos en detalle el comentario junto a la nueva línea que ha descubierto.

      │   0x0000118e  ~   00488d         add byte [rax - 0x73], cl
      │   ; DATA XREF from sub.foo_1139 @ 0x1152(r)
┌ 16: sub.Llamada_especial_2_118f ();
│       │   0x0000118f      488d058e0e..   lea rax, str.Llamada_especial_2 ; 0x2024 ; "Llamada especial 2"

Al parecer ha encontrado una referencia cruzada en la dirección 0x1152 y gracias a ello ha sido capaz de averiguar que hay algo en esa dirección. La dirección en cuestión contiene:

0x0001152      488d053600..   lea rax, [sub.Llamada_especial_2_118f] ; 0x118f ; "H\x8d\x05\x8e\x0e"

Es decir, es la línea en la que asignamos a la variable global f2 la dirección de la etiqueta label_foo2

ESCONDIENDO EL CÓDIGO MÁS

Bueno, vamos a ponérselo un poco más difícil a radare. En lugar de almacenar las direcciones directamente en las variables globales, hagamos algunas operaciones con ellas:

int foo (int a) {
  f1 = &&label_foo1 - 0x1234;
  f2 = &&label_foo2 + 0x4321;
  puts ("> foo inicio");
  f1 += 0x1234;
  f2 -= 0x4321;
  (...)

Y esto es lo que obtenemos:

Genial, ya no hay referencia cruzada que valga. Sin embargo, aún podemos ver todo el código si bien no es evidente cuales son los puntos de acceso, ya que las direcciones se calculan en tiempo de ejecución. Un sencillo análisis dinámico las mostrará, pero eso significa que ya hemos forzado al investigador al siguiente nivel, el cual implica una cierta preparación.

A partir de este punto podéis aplicar distintas técnicas de ofuscación para hacer más complicado averiguar que es lo que hace el programa. Por mi parte voy a insertar una serie de saltos al final de la función justo antes del código de las funciones especiales, para que parezca que el código real nunca se ejecuta… si bien esto es algo muy sencillo de ver cuando se hace un análisis dinámico.

Los cambios al código son los siguientes:

 if (a) {
    __asm__ ("jmp . + 123");
    __asm__ (".asciz \"PRUEBA\"");
  label_foo1:
    puts ("Llamada especial 1");
    __asm__ ("ret");
    __asm__ ("jmp . + 123");
    __asm__ (".asciz \"PRUEBA\"");
  label_foo2:
    puts ("Llamada especial 2");
    __asm__ ("ret");
  }

Como podéis ver, hemos cambiado ROOR por PRUEBA que genera más opcodes inválidos.Ahora solo tenemos que calcular los offsets de los jmps del código de arriba y sustituir los valores.

Para ello vamos a volcar la función foo con objdump tras compilar esta última versión:

occams@razor > objdump -d jump-ex08 | grep "<foo>:" -A 50
0000000000001139 <foo>:
    1139:   55                      push   %rbp
    113a:   48 89 e5                mov    %rsp,%rbp
    113d:   48 83 ec 10             sub    $0x10,%rsp
    1141:   89 7d fc                mov    %edi,-0x4(%rbp)
    1144:   48 8d 05 27 ee ff ff    lea    -0x11d9(%rip),%rax        # ffffffffffffff72 <_end+0xffffffffffffbf42>
    114b:   48 89 05 ce 2e 00 00    mov    %rax,0x2ece(%rip)        # 4020 <f1>
    1152:   48 8d 05 87 43 00 00    lea    0x4387(%rip),%rax        # 54e0 <_end+0x14b0>
    1159:   48 89 05 c8 2e 00 00    mov    %rax,0x2ec8(%rip)        # 4028 <f2>
    1160:   48 8d 05 9d 0e 00 00    lea    0xe9d(%rip),%rax        # 2004 <_IO_stdin_used+0x4>
    1167:   48 89 c7                mov    %rax,%rdi
    116a:   e8 c1 fe ff ff          call   1030 <puts@plt>
    116f:   48 8b 05 aa 2e 00 00    mov    0x2eaa(%rip),%rax        # 4020 <f1>
    1176:   48 05 34 12 00 00       add    $0x1234,%rax
    117c:   48 89 05 9d 2e 00 00    mov    %rax,0x2e9d(%rip)        # 4020 <f1>
    1183:   48 8b 05 9e 2e 00 00    mov    0x2e9e(%rip),%rax        # 4028 <f2>
    118a:   48 2d 21 43 00 00       sub    $0x4321,%rax
    1190:   48 89 05 91 2e 00 00    mov    %rax,0x2e91(%rip)        # 4028 <f2>
    1197:   83 7d fc 00             cmpl   $0x0,-0x4(%rbp)
    119b:   74 34                   je     11d1 <foo+0x98>
    119d:   eb 79                   jmp    1218 <main+0x30>
    119f:   50                      push   %rax
    11a0:   52                      push   %rdx
    11a1:   55                      push   %rbp
    11a2:   45                      rex.RB
    11a3:   42                      rex.X
    11a4:   41 00 48 8d             add    %cl,-0x73(%r8)
    11a8:   05 64 0e 00 00          add    $0xe64,%eax
    11ad:   48 89 c7                mov    %rax,%rdi
    11b0:   e8 7b fe ff ff          call   1030 <puts@plt>
    11b5:   c3                      ret
    11b6:   eb 79                   jmp    1231 <main+0x49>
    11b8:   50                      push   %rax
    11b9:   52                      push   %rdx
    11ba:   55                      push   %rbp
    11bb:   45                      rex.RB
    11bc:   42                      rex.X
    11bd:   41 00 48 8d             add    %cl,-0x73(%r8)
    11c1:   05 5e 0e 00 00          add    $0xe5e,%eax
    11c6:   48 89 c7                mov    %rax,%rdi
    11c9:   e8 62 fe ff ff          call   1030 <puts@plt>
    11ce:   c3                      ret
    11cf:   eb 01                   jmp    11d2 <foo+0x99>
    11d1:   90                      nop
    11d2:   48 8d 05 5e 0e 00 00    lea    0xe5e(%rip),%rax        # 2037 <_IO_stdin_used+0x37>
    11d9:   48 89 c7                mov    %rax,%rdi
    11dc:   e8 4f fe ff ff          call   1030 <puts@plt>
    11e1:   b8 00 00 00 00          mov    $0x0,%eax
    11e6:   c9                      leave
    11e7:   c3                      ret

Si miramos con atención, el primer salto está en 0x119b y el siguiente salto está en 0x11b6. Queremos que parezcan puntos de salida, así que haremos que salten a 0x11e7. Lo que significa que debemos usar los offsets:

$ echo $((0x11e7-0x119d))
74
$ echo $((0x11e7-0x11b6))
49

Si actualizamos el código y recompilamos, esto es lo que mostrará ahora radare.

OTROS PUNTOS DE VISTA

En todo el artículo hemos estado mirando al código usando el comando pd de radare2. Este comando hace el desensamblado de la zona de memoria que le indicamos, sin embargo, radare2 ofrece formas más convenientes de ver al código que son las que la mayoría de la gente usa. Una de ellas es utilizando el comando pdf el cual nos ofrece el desensamblado de una zona de memoria, pero suponiendo que se trata de una función. Esto es lo que obtendríamos:

Como podéis ver, todo el código especial que hemos incluido no se muestra, sin embargo, si prestamos atención, veréis dos puntos que rompen las líneas de los saltos, indicándonos que ahí hay algo que no se está mostrando…

La otra forma que la gente usa para ver al código es el modo gráfico, popularizado por la herramienta IDA Pro y que nos permite, de un vistazo, ver la estructura general del programa de forma gráfica. radare2 ofrece esta vista en modo texto, pero podéis usar alguno de los GUIs para interactuar con el para verlo en modo gráfico. Para nuestro ejemplo, esta vista mostraría lo siguiente:

Para mostrar el gráfico de la figura, una vez cargado el programa, ejecutad el análisis completo con el comando aaaaaa y luiego entrad en modo gráfico con el comando VV. Una ver en modo gráfico, para mostrar la función foo , pulsad g e introducid el nombre que radare2 le ha dado (sym.foo).

En esta vista, no es obvio ver que hay una parte del código que no se está mostrando. Podéis utilizar el comando p para cambiar la representación y añadir las direcciones… sin embargo, a no ser que soñéis en hexadecimal y prestéis bastante atención, es bastante probable que no os percatéis que falta un cacho de código.

Así que recordad que si las cosas no cuadran igual hay que mirar más en detalle ;).


SOBRE Carolyn Lightrun
Carolyn posee una sensibilidad especial para entran en contacto con el más allá. Cuando programa, las cosas más extrañas ocurren y Carolyn ha dedicado su carrera a investigar estos inquietantes fenómenos y encontrar la explicación lógica tras ellos.

 
Tu publicidad aquí :)