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 ;).
■
