Making of: El programa interminado de Chubert
INGENIERÍA DIRECTA
Making of: El programa interminado de Chubert
2019-10-22
Por
Chubert

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.

Header Image Credits: Randy Fath

SOBRE Chubert
Chubert es un programador melómano fan incondicional de Schubert. Aficionado a la ingeniería inversa, colabora con Wh1t3 D3m0n para hacer las delicias de chainikis de todo el planeta. En su tiempo libre, Chubert juega al furgol con pelotas de metal