Solución. Programa interminado de Chubert
INGENIERÍA INVERSA
Solución. Programa interminado de Chubert
2019-10-12
Por
Wh1t3 D3M0n

Pues es una pena pero no vamos a poder inaugurar todavía el salón de la fama. Nadie nos ha enviado una solución al desafío del programa interminado de Chubert. Y no será porque no os hemos dado tiempo. Una lástima. Vamos a pensar que el desafío fué muy difícil... ya que la alternativa no nos dejaría en muy buen lugar :)

Así que... como prometimos.... aquí está la solución.... esperamos que os resulte interesante, y aprendáis un par de cosillas.

El nombre del programa

Quizás os hayáis preguntado de donde viene el nombre ese de programa interminado... sobre todo si no habéis intentando solucionarlo. Así que vamos a empezar resolviendo ese misterio.

El programa es un típico Crackme... uno de esos de buscar la clave, como otros que ya hemos visto antes en esta sección. La diferencia es que si vemos el contenido de la función main nos vamos a encontrar algo curioso.

En un segundo veremos como encontrar la función main y empezar a trabajar con el programa, pero para resolver este misterio sobre el nombre del desafío, vamos a echar mano de radare2 para encontrar rápidamente la función.

$ r2 ./symphony8
[0x000006a0]> aaaa
[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] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[0x000006a0]> pdf @ main
/ (fcn) main 34
|   main ();
|              ; DATA XREF from 0x000006bd (entry0)
|           0x000007aa      55             push rbp
|           0x000007ab      4889e5         mov rbp, rsp
|           0x000007ae      488d3df30200.  lea rdi, qword str.El_Programa_Interminado_de_Chubert ; 0xaa8 ; const char * s
|           0x000007b5      e8a6feffff     call sym.imp.puts           ; int puts(const char *s)
|           0x000007ba      488d3d0f0300.  lea rdi, qword str.c__Revista_Occam_s_Razor_2019 ; 0xad0 ; const char * s
|           0x000007c1      e89afeffff     call sym.imp.puts           ; int puts(const char *s)
|           0x000007c6      b800000000     mov eax, 0
\           0x000007cb      5d             pop rbp
[0x000006a0]>

Como podéis ver, parece que falta la mitad del programa.... Que el programa no está terminado.... Vamos.... que está interminado :o. La función main que encuentra radare simplemente muestra dos cadenas de texto en pantalla y ya está.... Como es esto posible?

Unas palabras sobre la secuencia de inicio de los programas

Como ya hemos comentado en otros artículos en esta vuestra humilde publicación, la función main no es lo primero que se ejecuta. Podéis echar un ojo a El Increíble Programa Menguante RELOADED para conocer los detalles, pero resumiendo, los programas empiezan ejecutando una función llamada _start que es añadida automáticamente por el compilador. Esta función depende en cierta medida de la librería utilizada y del tipo de binario. Nosotros vamos a hacer todo el análisis de forma que sepáis como enfrentaros a este problema en cualquier momento y en cualquier situación.

Así que vamos allá con gdb

$ gdb -q ./symphony8
Reading symbols from ./symphony8...
warning: Loadable section ".fini" outside of ELF segments

warning: Loadable section ".rodata" outside of ELF segments

warning: Loadable section ".eh_frame_hdr" outside of ELF segments

warning: Loadable section ".eh_frame" outside of ELF segments
(no debugging symbols found)...done.
(gdb)

Ummm... Que raro no?... Bueno, gdb es la leche (por eso siempre lo guardo en la nevera) y ya nos ha dado una buena pista de como atacar el problema.... Sin embargo, vamos a ignorar estas pistas e ir directos a saco, empujados por nuestro espíritu didáctico.

Siguiente Parada: _start

Nuestra siguiente parada va a ser la función _start. Esta función es en realidad el punto de entrada del binario. Para saber cual es punto de entrada podemos utilizar el siguiente comando.

(gdb) info file
Symbols from "/tmp/symphony8".
Local exec file:
    `/tmp/symphony8', file type elf64-x86-64.
    Entry point: 0x6a0
(...)

Ese número tan bajo nos indica que se trata de un binario PIE (Position Independent Executable). Estos programas se cargan en una dirección aleatoria en memoria y todas las direcciones en el fichero ELF son relativas a esa posición en la que se cargará. Por ahora eso no nos afecta, así que vamos a seguir y le echaremos un ojo a _start.

(gdb) x/15i 0x6a0
   0x6a0:   xor    %ebp,%ebp
   0x6a2:   mov    %rdx,%r9
   0x6a5:   pop    %rsi
   0x6a6:   mov    %rsp,%rdx
   0x6a9:   and    $0xfffffffffffffff0,%rsp
   0x6ad:   push   %rax
   0x6ae:   push   %rsp
   0x6af:   lea    0x3da(%rip),%r8        # 0xa90
   0x6b6:   lea    0x363(%rip),%rcx       # 0xa20
   0x6bd:   lea    0xe6(%rip),%rdi        # 0x7aa
   0x6c4:   callq  *0x200916(%rip)        # 0x200fe0
   0x6ca:   hlt
   0x6cb:   nopl   0x0(%rax,%rax,1)
   0x6d0:   lea    0x2009b9(%rip),%rdi    # 0x201090 <stdin>
   0x6d7:   push   %rbp
(gdb)

NOTA

El binario no tiene símbolos así que no podemos utilizar el commando disassemble y tenemos que volcar la memoria como instrucciones (x/Ni).

Bien... Esto que puede parecer un poco raro, es en realidad un pedazo de código muy estándar que muy pronto os resultará muy familiar. Este código básicamente llama a la función libc_start_main, cuyo prototipo es algo así como:

int __libc_start_main (int *(main) (int, char * *, char * *), 
                       int argc, char * * ubp_av, 
                       void (*init) (void), 
                       void (*fini) (void), 
                       void (*rtld_fini) (void), 
                       void (* stack_end));

Para el tema que nos ocupa nos interesan los 5 primeros parámetros. Como podéis ver, el primero de ellos es el puntero a la función main, y los dos siguientes son argc y argv, los parámetros que recibe esta función. A continuación recibe otros dos punteros a funciones de los que hablaremos en breve, tras echar un ojo a la función main.

Si recordáis el ABI para los binarios de 64bits GNU/Linux... Ya, yo tampoco lo recuerdo, así que vamos a ponerlo aquí otra vez a modo de recordatorio.

Parámetro 1 : RDI -> 0x7aa
Parámetro 2 : RSI -> ---
Parámetro 3 : RDX -> ---
Parámetro 4 : RCX -> 0xa20
Parámetro 5 : R8  -> 0xa90
Parámetro 6 : R9  -> ---

Si veis el código, RSI y RDX se inicializan con valores de la pila y el puntero a la pila... En otro artículo quizás hablemos de como se pasan los parámetros desde la línea de comandos a los programas... por ahora, podemos ignorarlos, y vamos a centrarnos en el resto de los registros. Como dijimos un poco más arriba, vamos a echarle un ojo a main (aunque ya lo hicimos al principio utilizando radare2).

(gdb) x/20i 0x7aa
   0x7aa:   push   %rbp
   0x7ab:   mov    %rsp,%rbp
   0x7ae:   lea    0x2f3(%rip),%rdi        # 0xaa8
   0x7b5:   callq  0x660 <puts@plt>
   0x7ba:   lea    0x30f(%rip),%rdi        # 0xad0
   0x7c1:   callq  0x660 <puts@plt>
   0x7c6:   mov    $0x0,%eax
   0x7cb:   pop    %rbp
   0x7cc:   Cannot access memory at address 0x7cc

Ahí podemos ver las dos llamadas a puts para escribir en pantalla. Comprobemos que es lo que se imprime

(gdb) x/s 0xaa8
0xaa8:  "El Programa Interminado de Chubert"
(gdb) x/s 0xad0
0xad0:  "(c) Revista Occam's Razor 2019"

Bien, ya hemos conseguido hacer lo que radare consiguió en 5 segundos :)... Es broma... ahora sabemos como encontrar la función main en cualquier caso... Y eso es bueno. Vamos a ver que pasa con los otros dos parámetros.

Constructores y Destructores

Vale. Las funciones init y fini que pasamos como parámetros a libc_start_main, son las encargadas de ejecutar los constructores y destructores del programa.... Y qué es eso? os estaréis preguntando... al menos los que no estéis ya totalmente perdidos. :).

Los constructores, son funciones que se ejecutar antes de la función main y los destructores son funciones que se ejecutan justo después de la función main.... Esto suena interesante verdad?.

En nuestro caso, y vista la estructura de la función main, parece que lo que se nos ha perdido es el final del programa... así que vamos a echarle un ojo a los destructores. La forma más sencilla de hacer esto es accediendo a las tablas de constructores/destructores en el fichero... Y donde está eso?... os preguntaréis.

Pues así:

$ readelf -S symphony8
There are 27 section headers, starting at offset 0x11a8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
(...) 
  [19] .init_array       INIT_ARRAY       0000000000200d90  00000d90
       0000000000000010  0000000000000008  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       0000000000200da0  00000da0
       0000000000000010  0000000000000008  WA       0     0     8
(...)

Ahora podemos volcarlas tablas desde gdb:

(gdb) x/6xg 0x200d90
0x200d90:   0x00000000000007a0  0x00000000000009d8
0x200da0:   0x0000000000000760  0x000000000000095a
0x200db0:   0x0000000000000001  0x0000000000000001
(gdb)

O directamente en la línea de comandos

$ dd if=symphony8 bs=1 skip=$((0xd90)) count=48 | xxd -g 8 -e
00000000: 00000000000007a0 00000000000009d8  ................
00000010: 0000000000000760 000000000000095a  `.......Z.......
00000020: 0000000000000001 0000000000000001  ................
48+0 records in
48+0 records out
48 bytes copied, 0.000388438 s, 124 kB/s

Estupendo. Ahora podemos analizar el constructor y el destructor del programa. Si... tenemos uno de cada. El primer valor no nos interesa ya que siempre está ahí tengamos o no tengamos constructores/destructores extra..... No me creeis?... Bueno, compilad un "Hola Mundo" y echadle un ojo

Veamos primero que esconde el constructor:

(gdb) x/50i 0x9d8
   0x9d8:   Cannot access memory at address 0x9d8

Oops!.... Vaya.

Llegados a este punto tenemos dos opciones. La primera es prestarle atención a los warnings de gdb e intentar arreglar el binario. La otra es optar por continuar con el análisis dinámico.

Análisis Dinámico

El análisis dinámico de un binario no es otra cosa que trabajar con el mientras se ejecuta... Hasta ahora hemos estado mirando el código que está en el fichero del ejecutable. Ahora vamos a ver el código mientras el programa se está ejecutando.

Así que ejecutamos el programa y cuando nos pida la clave lo paramos pulsando CTRL+C. Algo tal que así

(gdb) r
Starting program: /home/dmo/mine/roor/n05/challenge/sol/symphony8
El Programa Interminado de Chubert
(c) Revista Occam's Razor 2019
Clave? ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7af4081 in __GI___libc_read (fd=0, buf=0x555555756670, nbytes=1024)
    at ../sysdeps/unix/sysv/linux/read.c:27
27  ../sysdeps/unix/sysv/linux/read.c: No such file or directory.

Como estamos trabajando con un binario PIE, lo primero que debemos hacer es localizar la dirección en la que el programa se ha cargado.

Spoiler:En gdb el programa siempre se carga en la misma dirección

Ahora vamos a volcar la pila y ver la dirección de la primera llamada.

(gdb) bt
#0  0x00007ffff7af4081 in __GI___libc_read (fd=0, buf=0x555555756670, nbytes=1024)
    at ../sysdeps/unix/sysv/linux/read.c:27
#1  0x00007ffff7a71148 in _IO_new_file_underflow (fp=0x7ffff7dcfa00 <_IO_2_1_stdin_>) at fileops.c:531
#2  0x00007ffff7a723f2 in __GI__IO_default_uflow (fp=0x7ffff7dcfa00 <_IO_2_1_stdin_>) at genops.c:380
#3  0x00007ffff7a63e62 in __GI__IO_getline_info (eof=0x0, extract_delim=<optimized out>, delim=10,
    n=1023, buf=0x7fffffffd900 "}", fp=0x7ffff7dcfa00 <_IO_2_1_stdin_>, fp@entry=0x0) at iogetline.c:60
#4  __GI__IO_getline (fp=fp@entry=0x7ffff7dcfa00 <_IO_2_1_stdin_>, buf=buf@entry=0x7fffffffd900 "}",
    n=<optimized out>, delim=delim@entry=10, extract_delim=extract_delim@entry=1) at iogetline.c:34
#5  0x00007ffff7a62bcd in _IO_fgets (buf=0x7fffffffd900 "}", n=<optimized out>,
    fp=0x7ffff7dcfa00 <_IO_2_1_stdin_>) at iofgets.c:53
#6  0x00005555555549b2 in ?? ()
#7  0x00007ffff7de5b73 in _dl_fini () at dl-fini.c:138
#8  0x00007ffff7a27041 in __run_exit_handlers (status=0, listp=0x7ffff7dcf718 <__exit_funcs>,
    run_list_atexit=run_list_atexit@entry=true, run_dtors=run_dtors@entry=true) at exit.c:108
#9  0x00007ffff7a2713a in __GI_exit (status=<optimized out>) at exit.c:139
#10 0x00007ffff7a05b9e in __libc_start_main (main=0x5555555547aa, argc=1, argv=0x7fffffffdee8,
    init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffded8)
    at ../csu/libc-start.c:344
#11 0x00005555555546ca in ?? ()

Estupendo. El código de nuestro programa está cargado en 0x0000555555554000. Así que ahora deberíamos de poder ver el código de nuestro constructor que estaba en el ofset 0x9d8.

(gdb) x/21i 0x00005555555549d8
   0x5555555549d8:  push   %rbp
   0x5555555549d9:  mov    %rsp,%rbp
   0x5555555549dc:  movl   $0x0,-0x4(%rbp)         # RBP-04 -> var1
   0x5555555549e3:  jmp    0x555555554a0e -------+
+->0x5555555549e5:  mov    -0x4(%rbp),%eax       | # EAX = var1
|  0x5555555549e8:  lea    0x1(%rax),%edx        | # EDX = RAX+1
|  0x5555555549eb:  mov    %edx,-0x4(%rbp)       | # var1 = EDX -> var1++
|  0x5555555549ee:  movslq %eax,%rcx             | # RCX = var1
|  0x5555555549f1:  lea    0x200628(%rip),%rdx   | # 0x555555755020 -> G1
|  0x5555555549f8:  movzbl (%rcx,%rdx,1),%edx    | # EDX = (RDX + RCX) = G1[var1]
|  0x5555555549fc:  mov    %edx,%ecx             | # ECX = EDX = G1[var1]
|  0x5555555549fe:  xor    $0xffffff90,%ecx      | # ECX = ECX ^ 0x90
|  0x555555554a01:  movslq %eax,%rdx             | # EAX = RDX = &G1[var1]
|  0x555555554a04:  lea    0x200615(%rip),%rax   | # 0x555555755020 -> G1
|  0x555555554a0b:  mov    %cl,(%rdx,%rax,1)     | # G1[var1] = CL -> G1[var1] ^= 0x90
+->0x555555554a0e:  mov    0x2005fc(%rip),%eax   | # 0x555555755010 -> G2
   0x555555554a14:  cmp    %eax,-0x4(%rbp)       | # Si Var1 < G2
   0x555555554a17:  jl     0x5555555549e5  <-----+
   0x555555554a19:  nop
   0x555555554a1a:  pop    %rbp
   0x555555554a1b:  retq

El fragmento de código de arriba es un bucle en el que hacemos se decodifica una cadena almacenada en G1 y de longitud G2 codificada con un XOR 0x90. G2 es la longitud de la cadena que en este caso vale.

(gdb) x/2w 0x555555755010
0x555555755010: 0x00000011  0x00000000

Bien... Es hora de analizar el destructor... que es donde está toda la chica.

El destructor

El destructor se encontraba en el offset 0x95a, el cual, una vez cargado en memoria lo podremos localizar en la dirección 0x000055555555495a.

(gdb) x/28i 0x000055555555495a
   0x55555555495a:  push   %rbp
   0x55555555495b:  mov    %rsp,%rbp
   0x55555555495e:  sub    $0x410,%rsp
   0x555555554965:  mov    %fs:0x28,%rax
   0x55555555496e:  mov    %rax,-0x8(%rbp)
   0x555555554972:  xor    %eax,%eax
   
   0x555555554974:  mov    0x200696(%rip),%edx        # 0x555555755010 -> EDX = G2
   0x55555555497a:  mov    0x200704(%rip),%eax        # 0x555555755084 -> EAX = G3
   0x555555554980:  mov    %edx,%ecx                  # ECX = EDX = G2
   0x555555554982:  lea    0x200697(%rip),%rdx        # 0x555555755020 -> RDX = G1
   0x555555554989:  mov    %eax,%esi                  # ESI = EAX = G3
   0x55555555498b:  lea    0x2006f6(%rip),%rdi        # 0x555555755088 -> RDI = G4
   0x555555554992:  callq  0x5555555547d8             # func1 (G4, G3, G1, G2)
   0x555555554997:  mov    0x2006f2(%rip),%rdx        # 0x555555755090 RDX = <stdin>
   0x55555555499e:  lea    -0x410(%rbp),%rax          # RAX=RBP-410 -> LocalBuf
   0x5555555549a5:  mov    $0x400,%esi                # ESI = 0x400
   0x5555555549aa:  mov    %rax,%rdi                  # RDI = LocalBuf
   0x5555555549ad:  callq  0x555555554680 <fgets@plt> # fgets (LocalBuf, 0x400, stdin)
   0x5555555549b2:  lea    -0x410(%rbp),%rax          # RAX = LocalBuf
   0x5555555549b9:  mov    %rax,%rdi                  # RDI = LocalBuf
   0x5555555549bc:  callq  0x555555554842             # Func2 (LocalBuf)
   0x5555555549c1:  nop
Comprueba_Pila: 
   0x5555555549c2:  mov    -0x8(%rbp),%rax
   0x5555555549c6:  xor    %fs:0x28,%rax
   0x5555555549cf:  je     0x5555555549d6
   0x5555555549d1:  callq  0x555555554670 <__stack_chk_fail@plt>
   0x5555555549d6:  leaveq
   0x5555555549d7:  retq

Dejando a un lado, la comprobación de la integridad de la pila al inicio y fin de la función (el stack canary), todo el código de arriba se puede reducir a lo siguiente:

func1 (G4, G3, G2, G1);
fgets (LocalBuf, 0x400, stdin);
func2 (LocalBuf);

Vamos a analizarlo todo, pero ya os podéis imaginar lo que hacen func1 y func2 verdad? La función main solo imprimía dos cadenas no?

Func1

Así se ve func1 desde gdb.

(gdb) x/35i 0x5555555547d8
   0x5555555547d8:  push   %rbp
   0x5555555547d9:  mov    %rsp,%rbp
   0x5555555547dc:  sub    $0x30,%rsp
   0x5555555547e0:  mov    %rdi,-0x18(%rbp)
   0x5555555547e4:  mov    %esi,-0x1c(%rbp)
   0x5555555547e7:  mov    %rdx,-0x28(%rbp)
   0x5555555547eb:  mov    %ecx,-0x20(%rbp)
   0x5555555547ee:  movl   $0x0,-0x4(%rbp)
   0x5555555547f5:  mov    -0x4(%rbp),%eax
   0x5555555547f8:  mov    %eax,-0x8(%rbp)
   0x5555555547fb:  jmp    0x555555554833
   0x5555555547fd:  mov    -0x8(%rbp),%eax
   0x555555554800:  movslq %eax,%rdx
   0x555555554803:  mov    -0x18(%rbp),%rax
   0x555555554807:  add    %rdx,%rax
   0x55555555480a:  movzbl (%rax),%ecx
   0x55555555480d:  mov    -0x8(%rbp),%eax
   0x555555554810:  cltd
   0x555555554811:  idivl  -0x20(%rbp)
   0x555555554814:  mov    %edx,%eax
   0x555555554816:  movslq %eax,%rdx
   0x555555554819:  mov    -0x28(%rbp),%rax
   0x55555555481d:  add    %rdx,%rax
   0x555555554820:  movzbl (%rax),%eax
   0x555555554823:  xor    %ecx,%eax
   0x555555554825:  movzbl %al,%eax
   0x555555554828:  mov    %eax,%edi
   0x55555555482a:  callq  0x555555554650 <putchar@plt>
   0x55555555482f:  addl   $0x1,-0x8(%rbp)
   0x555555554833:  mov    -0x8(%rbp),%eax
   0x555555554836:  cmp    -0x1c(%rbp),%eax
   0x555555554839:  jl     0x5555555547fd
   0x55555555483b:  mov    $0x0,%eax
   0x555555554840:  leaveq
   0x555555554841:  retq

Y así después de trabajar un poco sobre ella. Si queréis hacerlo vosotros mismos no miréis al siguiente listado.

(gdb) x/35i 0x5555555547d8
   0x5555555547d8:  push   %rbp
   0x5555555547d9:  mov    %rsp,%rbp
   0x5555555547dc:  sub    $0x30,%rsp
   
   0x5555555547e0:  mov    %rdi,-0x18(%rbp)    # RBP-18 = Var1 = RDI = G4
   0x5555555547e4:  mov    %esi,-0x1c(%rbp)    # RBP-1c = Var2 = ESI = G3
   0x5555555547e7:  mov    %rdx,-0x28(%rbp)    # RBP-28 = Var3 = RDX = G2
   0x5555555547eb:  mov    %ecx,-0x20(%rbp)    # RBP-20 = Var4 = ECX = G1
   0x5555555547ee:  movl   $0x0,-0x4(%rbp)     # RBP-4 = Var5 = 0
   0x5555555547f5:  mov    -0x4(%rbp),%eax     # EAX = Var5 = 0
   0x5555555547f8:  mov    %eax,-0x8(%rbp)     # RBP-8 = Var6 = EAX = 0
   
   0x5555555547fb:  jmp    0x555555554833      # jmp l0 -> Un Bucle!!!
   
l1: 
   0x5555555547fd:  mov    -0x8(%rbp),%eax     # EAX = Var6
   0x555555554800:  movslq %eax,%rdx           # EDX = Var6
   0x555555554803:  mov    -0x18(%rbp),%rax    # RAX = G4
   0x555555554807:  add    %rdx,%rax           # RAX += EDX -> RAX = G4 + Var6
   0x55555555480a:  movzbl (%rax),%ecx         # ECX = G4[Var6]
   0x55555555480d:  mov    -0x8(%rbp),%eax     # EAX = Var6
   0x555555554810:  cltd                       # EAX -> RAX con extensión de signo
   0x555555554811:  idivl  -0x20(%rbp)         # EAX = Var6/G1 -> EDX = Var6 % G1
   0x555555554814:  mov    %edx,%eax           # EAX = Var6 % G1
   0x555555554816:  movslq %eax,%rdx           # Copia 32->64 con extensión de signo
   0x555555554819:  mov    -0x28(%rbp),%rax    # RAX = G2
   0x55555555481d:  add    %rdx,%rax           # RAX +=DX => G2 + Var6 % G1
   0x555555554820:  movzbl (%rax),%eax         # EAX = G2[Var6]
   0x555555554823:  xor    %ecx,%eax           # EAX = EAX ^ECX = G2[Var6 % G1] ^ G4[Var6]
   0x555555554825:  movzbl %al,%eax            # Se queda con un byte
   0x555555554828:  mov    %eax,%edi           # EDI = G2[Var6 %G1] ^ G4 [Var6]
   0x55555555482a:  callq  0x555555554650 <putchar@plt> #putchar (EDI)
   0x55555555482f:  addl   $0x1,-0x8(%rbp)     # Var6++
l0:   
   0x555555554833:  mov    -0x8(%rbp),%eax   # EAX = Var6
   0x555555554836:  cmp    -0x1c(%rbp),%eax  # Var6 == Var2 (G4)
   0x555555554839:  jl     0x5555555547fd    # Salta if Var6 < G4 a l1
   
   0x55555555483b:  mov    $0x0,%eax         # return 0
   0x555555554840:  leaveq
   0x555555554841:  retq

La función es bastante normal... El típico bloque en el que imprimimos caracter a caracter decodificando sobre la marcha de forma que la cadena decodificada no esté nunca en memoria.

Si tenéis dudas sobre lo que hace esta función poneros en contacto conmigo y os echaré un cable.

Func2

Esta es la función interesante... Así que vuuaaammoooos a por ella (si, soy fan de Buff Academy). Esta función es bastante larga asi que vamos a ir analizándola por partes. Recordad que entramos a la función con el valor introducido por el usuario en el registro RDI

   0x555555554842:  push   %rbp
   0x555555554843:  mov    %rsp,%rbp
   0x555555554846:  sub    $0x20,%rsp
   
   0x55555555484a:  mov    %rdi,-0x18(%rbp)   # RBP-18 = Var1 = RDI = user_input
   0x55555555484e:  movl   $0x0,-0x4(%rbp)    # RBP-04 = Var2 = 0
l0:   
   0x555555554855:  addl   $0x1,-0x4(%rbp)    # EAX = Var2 + 1
   0x555555554859:  mov    -0x4(%rbp),%eax    # Var2 = EAX = Var2 +1
   0x55555555485c:  movslq %eax,%rdx          # RDX = Var2
   0x55555555485f:  mov    -0x18(%rbp),%rax   # RAX = user_input
   0x555555554863:  add    %rdx,%rax          # RAX +RDX -> user_input + Var2 
   0x555555554866:  movzbl (%rax),%eax        # EAX = user_input[Var2]
   0x555555554869:  cmp    $0xa,%al           # si user_input[Var2] != '\n'
   0x55555555486b:  jne    0x555555554855     # Repite l0
   0x55555555486d:  mov    -0x4(%rbp),%eax    # EAX = Var2
   0x555555554870:  movslq %eax,%rdx          # RDX = EAX = Var2
   0x555555554873:  mov    -0x18(%rbp),%rax   # RAX = user_input
   0x555555554877:  add    %rdx,%rax          # RAX = user_input + Var2
   0x55555555487a:  movb   $0x0,(%rax)        # user_input[Var2] = 0

Bien... Esto ha sido fácil. La función empieza calculando la longitud de la cadena introducida por el usuario y eliminado el retorno de carro final (\n o 0x0a) que fgets nos devuelve.

Continuemos.

   0x55555555487d:  mov    0x2007b1(%rip),%eax   # 0x555555755034 -> G10
   0x555555554883:  cmp    %eax,-0x4(%rbp)       # (G10) == Var2
   0x555555554886:  jne    0x555555554922        # Distintos -> salta a sal01
   0x55555555488c:  movl   $0x0,-0x8(%rbp)       # RBP-08 = Var3 = 0
   
   0x555555554893:  jmp    0x555555554899        # Otro bucle. Contador en Var3
B02:   
   0x555555554895:  addl   $0x1,-0x8(%rbp)
B01:
   0x555555554899:  mov    -0x8(%rbp),%eax       # EAX = Var3
   0x55555555489c:  cmp    -0x4(%rbp),%eax       # Var3 == Var2 ?
   0x55555555489f:  jge    0x5555555548e6        # Mayor o iggual salta a sal02
   0x5555555548a1:  mov    -0x8(%rbp),%eax       # EAX = Var3 
   0x5555555548a4:  movslq %eax,%rdx             # RDX = EAX
   0x5555555548a7:  mov    -0x18(%rbp),%rax      # RAX = user_input
   0x5555555548ab:  add    %rdx,%rax             # RAX = RAX +RDX = user_input + Var3
   0x5555555548ae:  movzbl (%rax),%esi           # ESI = user_input[Var3]
   0x5555555548b1:  mov    0x200759(%rip),%ecx   # 0x555555755010 ECX = G2 
   0x5555555548b7:  mov    -0x8(%rbp),%eax       # EAX = Var3
   0x5555555548ba:  cltd                         # Claculo del modulo otra vez
   0x5555555548bb:  idiv   %ecx                  # EAX / G2
   0x5555555548bd:  mov    %edx,%eax             # EAX = EDX = Var3 % G2
   0x5555555548bf:  movslq %eax,%rdx             # RDX = Var3 % G2
   0x5555555548c2:  lea    0x200757(%rip),%rax   # 0x555555755020  RAX = G1
   0x5555555548c9:  movzbl (%rdx,%rax,1),%eax    # EAX = G1[(Var3 %G2)]
   0x5555555548cd:  xor    %eax,%esi             # ESI = G1[Var3 % G2] ^ user_input[Var3]
   0x5555555548cf:  mov    %esi,%ecx             # ECX = G1[Var3 % G2] ^ user_input[Var3]
   0x5555555548d1:  mov    -0x8(%rbp),%eax       # EAX = Var3
   0x5555555548d4:  movslq %eax,%rdx             # RDX = EAX
   0x5555555548d7:  lea    0x200762(%rip),%rax   # 0x555555755040 G11
   0x5555555548de:  movzbl (%rdx,%rax,1),%eax    # EAX = G11[Var3]
   0x5555555548e2:  cmp    %al,%cl               # G11[Var3] == G1[Var3 % G2] ^ user_input[Var3]
   0x5555555548e4:  je     0x555555554895        # j3 B02 Repite mientras los valores sean iguales
   0x5555555548e6:  mov    -0x8(%rbp),%eax       # EAX = Var3
   0x5555555548e9:  cmp    -0x4(%rbp),%eax       # Var3 == Var2
   0x5555555548ec:  jl     0x555555554925        # Si menor sal02
   

Este parece el código que realmente comprueba la clave. Lo primero que hace es comprobar que la longitud de la cadena que ha introducido el usuario es igual a G10. Así que parece que la clave que buscamos tiene una longitud de G10 que en este caso vale:

(gdb) x/2w 0x555555755034
0x555555755034: 0x00000011  0x00000000

Tras esto, la función inicia un bloque que solo terminará si ciertos valores calculados dentro del bucle son todos iguales. Cuando uno de estos valores no coincida, el bucle termina y se comprueba la longitud de la cadena. Si es la adecuada la clave es correcta. Y tras nuestro sesudo análisis, el cuerpo del bucle se puede representar con el siguiente pseudo-codigo.

for (Var3 = 0; user_input[Var3] ^ G1[Var3 % G2] == G11[Var3]; Var3++);
if (var3 != Var2) goto fail;

Lo que nos queda de la función son el Good Boy (el código que se ejecuta cuando acertamos la clave) y el Bad boy (el que se ejecuta cuando fallamos). Como podéis ver esta parte del programa hace uso de de func1, la cual ya hemos analizado. Del análisis anterior debería resulta obvio quien es quien... pero en el peor de los casos podéis poner 2 breakpoints en gdb y terminar de ejecutar el programa... a ver cual salta.

   0x5555555548ee:  mov    0x20071c(%rip),%edx        # 0x555555755010 EDX = G2
   0x5555555548f4:  mov    0x20075a(%rip),%eax        # 0x555555755054 EAX = G20
   0x5555555548fa:  mov    %edx,%ecx                  # ECX = G2
   0x5555555548fc:  lea    0x20071d(%rip),%rdx        # 0x555555755020 RDX= G1
   0x555555554903:  mov    %eax,%esi                  # ESI = G20
   0x555555554905:  lea    0x20074c(%rip),%rdi        # 0x555555755058 RDI=G21
   0x55555555490c:  callq  0x5555555547d8             # func1 (G21,G20, G1, G2)
   0x555555554911:  mov    $0xa,%edi
   0x555555554916:  callq  0x555555554650 <putchar@plt> # printf ("\n");
   0x55555555491b:  mov    $0x0,%eax
   0x555555554920:  jmp    0x555555554958             # jmp Salir
sal01:   
   0x555555554922:  nop
   0x555555554923:  jmp    0x555555554926
sal02:   
   0x555555554925:  nop
   0x555555554926:  mov    0x2006e4(%rip),%edx        # 0x555555755010 
   0x55555555492c:  mov    0x200732(%rip),%eax        # 0x555555755064
   0x555555554932:  mov    %edx,%ecx
   0x555555554934:  lea    0x2006e5(%rip),%rdx        # 0x555555755020
   0x55555555493b:  mov    %eax,%esi
   0x55555555493d:  lea    0x20072c(%rip),%rdi        # 0x555555755070
   0x555555554944:  callq  0x5555555547d8
   0x555555554949:  mov    $0xa,%edi
   0x55555555494e:  callq  0x555555554650 <putchar@plt>
   0x555555554953:  mov    $0xffffffff,%eax
Salir:   
   0x555555554958:  leaveq
   0x555555554959:  retq

Determinando la clave

Ahora que ya hemos destripado por completo el programa, es hora de determinar la clave. Si recordamos, el código que le dice al programa que mensaje mostrar era algo como esto:

for (Var3 = 0; user_input[Var3] ^ G1[Var3 % G2] == G11[Var3]; Var3++);

Lo que introducimos es codificado con una función XOR y la secuencia de bytes G1, y eso debe ser igual a G11. También sabemos que la clave tiene una longitud de 0x11 o 17 bytes.

Lo primero que haremos será obtener las secuencias G1 y G11.

-> G1
(gdb) x/20xb 0x555555755020
0x555555755020: 0x41    0x6c    0x6c    0x33    0x67    0x72    0x30    0x24
0x555555755028: 0x6d    0x30    0x44    0x33    0x72    0x34    0x74    0x30
0x555555755030: 0x21    0x00    0x00    0x00
-> G11
(gdb) x/20xb 0x555555755040
0x555555755040: 0x00    0x02    0x08    0x07    0x29    0x06    0x03    0x07
0x555555755048: 0x0e    0x00    0x0a    0x17    0x1f    0x04    0x20    0x7f
0x555555755050: 0x0a    0x00    0x00    0x00

Ahora solo tenemos que hacer el XOR de estas dos cadenas para obtener la clave.

  416c6c33677230246d3044337234743021
^ 00020807290603070e000a171f04207f0a
------------------------------------
  416e64344e74332363304e246d30544f2b

O en otras palabras...

And4Nt3#c0N$m0TO+

Comprobemos si esta es la clave correcta:

$ ./symphony8
El Programa Interminado de Chubert
(c) Revista Occam's Razor 2019
Clave? And4Nt3#c0N$m0TO+
Bien Hecho!

Conseguido!!!!!

Y si, G1 es la clave utilizada para cifrar todas las cadenas de caracteres... por eso strings no muestra todos los mensajes del programa. Si tenéis curiosidad por saber de donde vienen estas claves no podéis dejar de echar un ojo a este enlace.

Hasta la próxima entrega

Esperamos que os haya resultado interesante. El desafío era un poco más difícil de lo que pensábamos pero todavía en un nivel de dificultad medio/bajo. Como siempre, no dudéis en poneros en contacto conmigo para cualquier aclaración, comentario, corrección, o lo que os de la gana.

Header Image Credits: Thought

SOBRE Wh1t3 D3M0n
Me conocen como Wh1th3 D3M0n y soy un hacker. Me oculto entre las sombras de la red, paseándome entre máquinas y datos como un espectro… invisible a los ojos de los usuarios…

Que va es coña!

Twitter: @ChainkMaster | Blog: https://thehackerkid.tumblr.com/