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