Nuestro amigo Chubert está tan orgulloso de su programa inacabado que ha decidido contarnos todos los secretos sobre como creo este desafiante desafío.... como diría el cocodrilo de Kung-Fu Panda.
Ah!. Y si no has sido capaz de solucionarlo no te pierdas la solución.La idea de este desafío surgió como un ejemplo de como utilizar los constructores y destructores para generar un desafío en el cual, a primera vista, el programa a analizar parece que está interminado. Mola eh?
Constructores y destructores
Así que comence con un ejemplo supertonto con el que probar como iba eso de los constructores y los destructores
#include <stdio.h>
static void conan (void) __attribute__ ((destructor));
static void matrix (void) __attribute__ ((constructor));
int
main (void)
{
printf ("El Programa Interminado de Chubert\n");
printf ("(c) Revista Occam's Razor 2019\n");
return 0;
}
void conan (void) {
printf ("[DESTRUCTOR] Conan El destructor estuvo aquí!\n");
}
void matrix (void) {
printf ("[CONSTRUCTOR] Generando programa en el constructor de Matrix\n");
}
Si compilamos y ejecutamos este programa obtendríamos la siguiente salida:
$ make p1 cc p1.c -o p1 $ ./p1 [CONSTRUCTOR] Generando programa en el constructor de Matrix El Programa Interminado de Chubert (c) Revista Occam's Razor 2019 [DESTRUCTOR] Conan El destructor estuvo aquí!
Y si lo analizamos con radare:
$ r2 p1 [0x00000560]> aaa [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze len bytes of instructions for references (aar) [x] Analyze function calls (aac) [x] Use -AA or aaaa to perform additional experimental analysis. [x] Constructing a function name for fcn.* and sym.func.* functions (aan) [0x00000560]> pdf @main ;-- main: / (fcn) sym.main 35 | sym.main (); | ; DATA XREF from 0x0000057d (entry0) | 0x0000066a 55 push rbp | 0x0000066b 4889e5 mov rbp, rsp | 0x0000066e 488d3dd30000. lea rdi, qword str.El_Programa_Interminado_de_Chubert ; 0x748 ; "El Programa Interminado de Chubert" | 0x00000675 e8c6feffff call sym.imp.puts ; int puts(const char *s) | 0x0000067a 488d3def0000. lea rdi, qword str.c__Revista_Occam_s_Razor_2019 ; 0x770 ; "(c) Revista Occam's Razor 2019" | 0x00000681 e8bafeffff call sym.imp.puts ; int puts(const char *s) | 0x00000686 b800000000 mov eax, 0 | 0x0000068b 5d pop rbp \ 0x0000068c c3 ret
Lo que parece un programa bastante inacabado :)
Version 2. Ocultando código
Mi versión original no está mal, pero a poco que analicemos el programa encontraremos el código del constructor/destructor. Por ejemplo, ejecutando objdump -d
veremos ambos sin mayor esfuerzo, así que tendremos que hacerlo mejor.
Para ello explotaremos el hecho de que Linux es bastante flexible con el formato de los binarios, mientras que las herramientas de depuración son un poco más pejigueras. Así que lo que vamos a hacer es modificar el fichero ELF indicando que el segmento de código es más corto de lo que realmente es, ocultando de esta forma parte del código.
El programa no cambia mucho:
#include <stdio.h>
static void conan (void) __attribute__ ((destructor));
static void matrix (void) __attribute__ ((constructor));
int
main (void)
{
printf ("El Programa Interminado de Chubert\n");
printf ("(c) Revista Occam's Razor 2019\n");
return 0;
}
int ya_no_me_ves (void) {
return 0;
}
void conan (void) {
printf ("[DESTRUCTOR] Conan El destructor estuvo aquí!\n");
}
void matrix (void) {
printf ("[CONSTRUCTOR] Generando programa en el constructor de Matrix\n");
}
Si. Solo añadí una función que me sirva de marca para determinar donde empieza el código que quiero ocultar.
$ make p2 $ objdump -d p2 (...) 000000000000068d <ya_no_me_ves>: 68d: 55 push %rbp 68e: 48 89 e5 mov %rsp,%rbp 691: b8 00 00 00 00 mov $0x0,%eax 696: 5d pop %rbp 697: c3 retq 0000000000000698 <conan>: 698: 55 push %rbp 699: 48 89 e5 mov %rsp,%rbp 69c: 48 8d 3d ed 00 00 00 lea 0xed(%rip),%rdi # 790 <_IO_stdin_used+0x50> 6a3: e8 98 fe ff ff callq 540 <puts@plt> 6a8: 90 nop 6a9: 5d pop %rbp 6aa: c3 retq 00000000000006ab <matrix>: 6ab: 55 push %rbp 6ac: 48 89 e5 mov %rsp,%rbp 6af: 48 8d 3d 0a 01 00 00 lea 0x10a(%rip),%rdi # 7c0 <_IO_stdin_used+0x80> 6b6: e8 85 fe ff ff callq 540 <puts@plt> 6bb: 90 nop 6bc: 5d pop %rbp 6bd: c3 retq 6be: 66 90 xchg %ax,%ax (...)
Como podemos ver, todo nuestro código se encuentra tras la funcion ya_no_me_ves
en el offset 0x68d
. En lugar de usar objdump y buscar nuestra función, podemos obtener este offset con el siguiente comando:
$ objdump -t p2 | awk '/ya_no_me_ves/ {print $1,"-",$6}' 000000000000068d - ya_no_me_ves
Si también podéis hacer objdump -d p2 | grep ya_no_me_ves
... pero objdump -t tiene alguna información más que ofrecernos, si bien, en este caso, no la necesitamos
Parcheando el binario
Después de pasar el programa por strip
para eliminar información de debug y símbolos, echémosle un vistazo a la cabecera. No os olvidéis de este paso ya que va a afectar al offset de las secciones que se encuentran al final del binario.
$ readelf -h p2 ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Shared object file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0x560 Start of program headers: 64 (bytes into file) Start of section headers: 4400 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 27 Section header string table index: 26
Axquí vemos que la cabecera ocupa 64 bytes y que los segmentos (Program Headers) empiezan justo después de la cabecera (Start of Program Headers: 64). Además vemos que cada segmento se almacena en un bloque de 56 bytes en el disco. Las secciones se encuentra en el offset 6536 (Start of section headers) y cada entrada tiene un tamaño de 64 bytes.
Con esta información lo primero que hice fué parchear la sección .text
. La que contiene el código:
$ readelf -S p2 (...) [14] .text PROGBITS 0000000000000560 00000560 00000000000001d2 0000000000000000 AX 0 0 16 (...)
Aquí podéis ver que el tamaño de la sección es 0x1d2
. El segmento de código va desde 0x560
hasta 0x732
(0x560 + 0x1d2 = 0x732
). Pero lo que yo quiero es que termine en 0x68d
que es donde empieza la función ya_no_me_ves
. Así que el nuevo tamaño de la sección será 0x68d - 0x560 = 0x12D
.
Lo parcheamos....
echo -ne '\x2D\x01' | dd of=p seek=$((4400 + 14*64 + 32)) bs=1 count=2 conv=notrunc
Y vemos que nos dice objdump
.
$ objdump -d p (...) 664: 5d pop %rbp 665: e9 66 ff ff ff jmpq 5d0 <__cxa_finalize@plt+0x80> 66a: 55 push %rbp 66b: 48 89 e5 mov %rsp,%rbp 66e: 48 8d 3d d3 00 00 00 lea 0xd3(%rip),%rdi # 748 <__cxa_finalize@plt+0x1f8> 675: e8 c6 fe ff ff callq 540 <puts@plt> 67a: 48 8d 3d ef 00 00 00 lea 0xef(%rip),%rdi # 770 <__cxa_finalize@plt+0x220> 681: e8 ba fe ff ff callq 540 <puts@plt> 686: b8 00 00 00 00 mov $0x0,%eax 68b: 5d pop %rbp 68c: c3 retq Disassembly of section .fini:
Ahora el desensamblado termina en 0x68c
... Veamos que sucede con gdb
.
$ gdb -q ./p Reading symbols from ./p...(no debugging symbols found)...done. (gdb) x/50i 0x68d 0x68d: Cannot access memory at address 0x68d (gdb)
Perfecto!!!.
HASTA EL PRÓXIMO NÚMERO
Ahora ya solo hay que escribir el desafío.... Si queréis ver el código completo del desafío pedidlo en los comentarios o en twitter @RevistaROOR. Espero que os haya resultado interesante y útil...Nos vemos en el próximo número.
■