Mientras creaba un detector de shellcode genérico y verificaba los resultados con entradas generadas al azar, terminé empleando con éxito la técnica fuzzing en diferentes bibliotecas de desensambladores de código abierto. La biblioteca de desensambladores que elegí para mi proyecto es libdasm debido a su historia relativamente larga y su licencia de dominio público. Pero escribir un desensamblador x86 exitoso y completo no es una tarea fácil dada la compleja naturaleza del grupo de instrucciones x86.
Libdasm solía tener problemas para desensamblar algunas instrucciones en coma flotante de manera correcta, pero esto sólo se debía a un error en las tablas de búsqueda del opcode (código de operación), en las que faltaban tres columnas NULL, por lo que encontrar la solución no fue tan difícil.
Lo que encontré hoy no parece un problema específico del opcode, sino un error en la decodificación correcta de instrucciones. Cuando libdasm desensambla las instrucciones con un prefijo de dirección de 16 bit, decodifica la dirección de forma incorrecta:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[~] Verifying shellcode candidate offset 8eb0f0 008fe0f0[ 67a02232e830] > mov al,[0x30e83222] 008fe0f6[ 61] > popa 008fe0f7[ f9] > stc 008fe0f8[ ff4038] > inc [eax+0x38] 008fe0fb[ b269] > mov dl,0x69 008fe0fd[ 52] > push edx 008fe0fe[ 3f] > aas 008fe0ff[ 5e] > pop esi 008fe100[ 1a3dc31168aa] > sbb bh,[0xaa6811c3] 008fe106[ 59] > pop ecx 008fe107[ 9c] > pushf 008fe108[................] < |
La instrucción en la dirección de la memoria virtual del usuario 008fe0f0
se decodifica con errores:
- 67 es el prefijo de dirección de 16 bit antes mencionado
- a0 es el opcode para mov al, moffs8
- 2232 es la dirección de 16 bit que se debe interpretar como el operando
- e830 no pertenece a esta instrucción
Así como siempre se debe pedir una segunda opinión médica en casos de enfermedades inusuales, yo utilicé udis86, una biblioteca de desensambladores diferente:
1 2 3 4 |
$ udcli -noff -32 -s `python -c 'print 0x8eb0f0'` -c 10 shellcode/urandom.bin 67a02232 a16 mov al, [0x3222] e83061f9ff call 0xfffffffffff96139 40 inc eax |
Por suerte, esta vez se desensambló la instrucción mov con éxito. Además, como e830 ya no se interpreta como parte de mov, se desensambla de forma correcta como una llamada de instrucción rel32. Es una pena que udis86 sea un desensamblador x86-64, por lo que extiende de manera interna el operando a una llamada, cometiendo otro error al desensamblarlo.
¿Qué es lo que mi CPU ejecuta y ve en realidad? Como esto es parte de un código de virtualización/emulación, podemos sólo agregar un cc breakpoint al prólogo del bloque y continuar con gdb (omitiendo lo innecesario):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Program received signal SIGTRAP, Trace/breakpoint trap. (gdb) disas $eip, $eip+5 => 0x0804b0c1: jmp 0x804b134 (gdb) si (gdb) disas $eip, $eip+10 Dump of assembler code from 0x804b134 to 0x804b13e: => 0x0804b134: addr16 mov 0x3222,%al 0x0804b138: call 0x7fe126d 0x0804b13d: inc %eax End of assembler dump. (gdb) si (gdb) si (gdb) disas $eip, $eip+10 Dump of assembler code from 0x7fe126d to 0x7fe1277: => 0x07fe126d: Cannot access memory at address 0x7fe126d |
Entonces el CPU ve una instrucción de llamada y trata de ejecutarla. En este caso particular, esto hubiese sido devastador porque habría permitido una vulnerabilidad de aumento de privilegios para entradas arbitrarias de usuarios, más que seguro shellcodes, para poder escapar del aislamiento virtual. Para que este método específico funcione de manera correcta, todas las instrucciones de modificación del control del flujo como llamadas tienen que ser emuladas en un programa. Pero si no vemos esa instrucción en el desensamblado, no podemos manejarla de forma adecuada.
Después de parchar libdasm (que terminó ignorando por completo los prefijos de dirección para el análisis sintáctico del operando), se lo logra desensamblar con éxito:
1 2 3 4 5 6 |
[*] 543 shellcode candidate offsets [~] Verifying shellcode candidate offset 8eb0f0 008fe0f0[ 67a02232] > mov al,[0x3222] 008fe0f4[................] < Emulating 008fe0f4: call 0x894229 Emulating CALL instruction from 8fe0f9. |
Lo que aprendimos:
- Es una buena idea usar el método fuzzing en tus programas con entradas aleatorias como parte de tu proceso de pruebas y puede revelar vulnerabilidades interesantes. Explotar este caso en particular habría sido muy difícil porque el descriptor de código de segmento y los descriptores de segmentos de datos dirigían a diferentes direcciones base, pero un atacante hábil lo hubiese logrado de cualquier manera.
- La versión pública de libdasm desensambla de forma incorrecta todas las instrucciones con prefijos de dirección. Esto creará vectores de ataque interesantes contra algunos proyectos en los que se usó libdasm. ¡Consigan un parche para libdasm!
Diferentes interpretaciones de X86 Bytecode