miércoles, 27 de julio de 2016

Global Cyberlympics 2016


Esta semana fue el Global Cyberlympics 2016.  Un CTF de solo 12 horas para equipos de 5 personas. Pasamos la noche jugando pues en estos lados del mundo venia siendo de 7 p.m. a 7 a.m. Al final, logramos marcar 1400 puntos (el equipo ganador hizo 3275) con lo que terminamos en el puesto 32 (de entre unos 120 equipos).

Lo interesante es que todo el juego tenia un aire muy ochentero, pues estaba enmarcado en un MUD de espionaje. Entonces, para poder resolver los retos primero había que encontrarlos en medio del MUD. Y habia algunos retos intermedios que no daban puntos pero que se necesitaban para pasar de una sala a la siguiente.

Como siempre que se intenta algo nuevo, no faltaron los problemas (desde empezar a jugar sin scoreboard hasta reenviar las flags correctas varias veces porque no eran aceptadas), pero en general la experiencia fue muy buena. Adicionalmente, este juego incluyo bastante reversing y eso lo hizo aun mas interesante desde mi punto de vista.

Como en mi equipo algunos nunca han intentado los retos de reversing, al final del juego pidieron los write-ups para esos niveles. Y eso es lo que presento en este post. Y como la idea es que sirva de introduccion al reversing, he intentado resolver cada reto de varias formas. Espero que se entienda al menos una forma en cada reto :P

Primer binario: tang0


El primer binario que reversamos fue "tang0". No era un reto para puntos, solo lo necesitabamos para sacar un password con el que se pasaba un punto de control en el MUD. Este es el enlace de descarga y el password del .7z (ambos los sacamos del MUD):

https://www.dropbox.com/s/palfyem66jheff9/tang0.7z?dl=1 What$th3P@$$w0rd?

"tang0" via analisis dinámico (para resolver las cosas rápido)


Aqui esta la salida de "file":

user@ubuntu:~$ file tang0 tang0: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=3fd192f8069383d84a6038955454a3a67ce63ecd, not stripped

Nos interesa que es un ELF64, que esta enlazado dinamicamente (lo que significa, que tendremos que reversar menos codigo, pues las funciones estandar se quedan en al libreria compartida) y que no esta "stripped" (no se como decirlo en español, pero significa que la informacion de depuracion no fue removida del binario, y por tanto sera mas facil reversarlo).

Aqui esta la salida de "strings":

user@ubuntu:~$ strings tang0 /lib64/ld-linux-x86-64.so.2 libc.so.6 fopen puts printf strlen __isoc99_fscanf fclose strcmp __libc_start_main __gmon_start__ GLIBC_2.2.5 GLIBC_2.7 fffff. M.a.r.k.H 8.3.f t0k3n.txH []A\A]A^A_ &#x%x; So close...but it's not gonna happen How did you get here Access denied Congratulations, you've found the GGoCySEA guard station password. This password came at a great cost, use it wisely! Access denied. This file does not contain the correct t0k3n. Unable to locate the t0k3n file. ;*3$"

La he truncado porque continua con algunos otros strings estandar, pero aquí ya hay informacion suficiente para pasar el reto. Estas cadenas nos dicen que hace el binario:

So close...but it's not gonna happen How did you get here Access denied Congratulations, you've found the GGoCySEA guard station password. This password came at a great cost, use it wisely! Access denied. This file does not contain the correct t0k3n. Unable to locate the t0k3n file.

Por el mensaje "Congratulations, you've (…)" inferimos que el binario va a darnos un password. Por el mensaje "Unable to (…)" inferimos que necesitamos darle un "t0k3n file". Finalmente, por el mensaje "This file (…)" inferimos que en en ese t0k3n file debemos suministrarle un "t0k3n" correcto.

Entre los strings tambien vemos estas funciones estándar:

fopen puts printf strlen __isoc99_fscanf fclose strcmp

Ese "strcmp" es nuestro mejor target, Pues asumimos que lo usara para conmparar el t0k3n que ingresemos con el t0k3n correcto.

Finalmente, tenemos estos strings interesantes, pero los ignoraremos para hacer la cosa mas productiva :)

M.a.r.k.H 8.3.f t0k3n.txH

Vamos a crear un "token_file" en el que almacenaremos un "token" y veremos como se comporta el binario:

user@ubuntu:~$ echo "token" > token_file user@ubuntu:~$ ./tang0 Access denied user@ubuntu:~$ ./tang0 token_file Unable to locate the t0k3n file.

OK, usaremos "ltrace" para capturar los parametros de strcmp (esto se conoce como hooking):

user@ubuntu:~$ ltrace ./tang0 token_file __libc_start_main(0x4007ce, 2, 0x7ffcaa531a08, 0x400910 strcmp("token_file", "t0k3n.txt") = 63 puts("Unable to locate the t0k3n file."...Unable to locate the t0k3n file. ) = 33 +++ exited (status 0) +++

Y es obvio que compara nuestro "token_file" con "t0k3n.txt", asi que ya tenemos el nombre del archivo correcto: "t0k3n.txt"

user@ubuntu:~$ echo "token" > t0k3n.txt user@ubuntu:~$ ./tang0 t0k3n.txt Access denied. This file does not contain the correct t0k3n.

Ya reconoce el archivo, pero el t0k3n es incorrecto. Usamos de nuevo "ltrace":

user@ubuntu:~$ ltrace ./tang0 t0k3n.txt __libc_start_main(0x4007ce, 2, 0x7ffde02afc58, 0x400910 strcmp("t0k3n.txt", "t0k3n.txt") = 0 fopen("t0k3n.txt", "r") = 0xe01010 __isoc99_fscanf(0xe01010, 0x400af1, 0x7ffde02afb40, 0x7fad54bdc790) = 1 strcmp("token", "M.a.r.k.8.3.a") = 5 puts("Access denied.\nThis file does no"...Access denied. This file does not contain the correct t0k3n. ) = 61 fclose(0xe01010) = 0 +++ exited (status 0) +++

Y esta alli muy claro que compara nuestro "token" con "M.a.r.k.8.3.a" :)

user@ubuntu:~$ ./tang0 t0k3n.txt
Congratulations, you've found the GGoCySEA guard station password.
This password came at a great cost, use it wisely!
ţ
ȟ
ʓ
 
ᴛ
ʘ
ʞ
ᴣ
ᴖ
 
ẏ
Ṏ
ṹ
 
ⓢ
⒠
⒠
⒦
 
ị
ṩ
 
፡
 
b
y
p
a
s
s
i
t
n
o
w
user@ubuntu:~$

Que obviamente significa: ţȟʓ ᴛʘʞᴣᴖ ẏṎṹ ⓢ⒠⒠⒦ ịṩ ፡ bypassitnow, pero esto ya no es reversing y les queda de tarea entender de donde sale :)

"tang0" via analisis estático (para aprender un poco mas de reversing)

Si cargamos el binario en IDA Pro (si, si van a usar IDA tendra que ser el Pro, pues la version Free no soporta binarios de 64 bits) tenemos varias opciones para empezar:
  • podemos analizar los strings (como hicimos en el anterior, pero desde IDA)
  • podemos analizar los imports (y encontrariamos ese "strcmp" magico)
  • podemos analizar desde el principio: la funcion "main" (solo porque es un reto de un CTF y por lo tanto es un binario suficientemente corto como para entenderlo completo).
  • podemos hacer alguna otra cosa, seguramente hay mas formas de resolverlo, pero se me ocurren esas tres…

Analisis estático de "tang0" iniciando via strings

Vamos a "View" → "Open subview" → "Strings" (El shortcut es Shift+F12) y sale esto:

.rodata:0000000000400A00 00000008 C &#x%x;\n
.rodata:0000000000400A08 00000025 C So close...but it's not gonna happen
.rodata:0000000000400ACC 00000015 C How did you get here
.rodata:0000000000400AE1 0000000E C Access denied
.rodata:0000000000400AF8 00000076 C Congratulations, you've found the GGoCySEA guard station password.\nThis password came at a great cost, use it wisely!
.rodata:0000000000400B70 0000003D C Access denied.\nThis file does not contain the correct t0k3n.
.rodata:0000000000400BB0 00000021 C Unable to locate the t0k3n file.
.eh_frame:0000000000400C7F 00000006 C ;*3$\"

Alli solo necesitamos darle doble-click al string que nos interese para ir a la seccion relevante. Por ejemplo, si queremos saber el nombre del t0k3n file, podemos darle a "Unable to locate the t0k3n file" y nos lleva a esto:

.rodata:0000000000400B70 ; char aAccessDenied_T[]
.rodata:0000000000400B70 aAccessDenied_T db 'Access denied.',0Ah ; DATA XREF: main:loc_4008C5↑o
.rodata:0000000000400B70 db 'This file does not contain the correct t0k3n.',0
.rodata:0000000000400BAD align 10h

Lo que necesitamos es ese "DATA XREF", que es una referencia cruzada a esta porcion de datos. Esto es, una referencia al lugar desde donde se estan usando estos datos.
Podemos darle click a ese "loc_4008C5↑o" y presionar la tecla "X", o darle click derecho para ver el menu contextual que nos da la opcion "Jump to xref to operand" con el shortcut "X".

Esto nos lleva a este bloque de codigo:

.text:00000000004008C5 ; ---------------------------------------------------------------------------
.text:00000000004008C5
.text:00000000004008C5 loc_4008C5: ; CODE XREF: main+C4↑j
.text:00000000004008C5 mov edi, offset aAccessDenied_T ; "Access denied.\nThis file does not cont"...
.text:00000000004008CA call _puts
.text:00000000004008CF mov rax, [rbp+stream]
.text:00000000004008D3 mov rdi, rax ; stream
.text:00000000004008D6 call _fclose
.text:00000000004008DB jmp short loc_4008E7


La referencia al texto que vamos a mostrar va en el registro edi y luego hacemos un "call _puts". Obviamente estamos en la parte dodne se imprime el mensaje. Pero lo que queremos es evitar llegar hasta aqui! Nuevamente podemos usar el XREF (esta vez es un CODE XREF porque la referencia viene de la seccion de codigo, no de la seccion de datos). Si seguimos ese "CODE XREF: main+C4↑j" llegamos a unas pocas lineas mas arriba (esta vez pegare tambien unas lineas

anteriores para tener contexto):
.text:0000000000400885 mov rsi, rdx ; s2
.text:0000000000400888 mov rdi, rax ; s1
.text:000000000040088B call _strcmp
.text:0000000000400890 test eax, eax
.text:0000000000400892 jnz short loc_4008C5 ← LLEGAMOS AQUI!!!
.text:0000000000400894 lea rax, [rbp+s]
.text:0000000000400898 mov rdi, rax ; s
.text:000000000040089B call _strlen
.text:00000000004008A0 mov [rbp+var_14], eax
.text:00000000004008A3 mov edi, offset aCongratulation ; "Congratulations, you've found the GGoCy"...
.text:00000000004008A8 call _puts
.text:00000000004008AD mov eax, [rbp+var_14]
.text:00000000004008B0 mov edi, eax
.text:00000000004008B2 call t0k3n_g3n
.text:00000000004008B7 mov rax, [rbp+stream]
.text:00000000004008BB mov rdi, rax ; stream
.text:00000000004008BE call _fclose
.text:00000000004008C3 jmp short loc_4008E7


Entonces, en las dos primeras lineas cargamos dos parametros para strcmp en los registros rdi y rsi y en la tercera linea llamamos a strcmp. Luego, usamos ese test eax, eax para verificar el valor que retorna strcmp. Hasta ahora, pueden imaginarlo como "eax = strcmp(rdi, rsi);"  Recuerden que strcmp retorna 0 si los strings son iguales, entonces la siguiente linea es un JNZ = Jump-if-Not-Zero, esto es saltaremos a imprimir el mensaje de error si el strcmp no retorna cero.

Y si no saltamos? significa que los strings son iguales y en las siguientes lineas se muestra el mensaje "Congratulations" con puts. Y luego se llama a la funcion t0k3n_g3n que seguramente signigica "token generator" o algo asi. Esta es la funcion que queremos reversar.

Podemos resumirlo como:

if (!strcmp(s1, s2)) {
    puts("Congratulations, you've (…)");
    t0k3n_g3n();
}

Realmente, justo antes del puts() se llamo tambien a strlen(), que se necesita pasar como parametro a t0k3n_g3n, pero eso no es importante en estos momentos…

Veamos t0k3n_g3n (podemos seguir el XREF o buscarla en la ventana de funciones de IDA. Por cierto, estos nombres de funciones los sabemos porque el binario no esta "stripped", si hubieran usado el comando "strip, ISA le daria algun nombre generico):

Esta funcion parece que solo hace algunos calculos y verificaciones para luego llamar a la funcion "tang0".  Vemos sumas, restas, multiplicaciones, corriemientos de bits… Tambien hay un valor magico: "60F25DEBh"…  Podriamos analizarla, pero como aun no estamos imprimiendo el password, parece que lo importante esta en tang0, asi que veremos esa:


Esta es la funcion importante.  Ayuda bastante el hecho de que se llama igual que el binario, pero ademas de eso, vemos que imprime unos valores con printf:

.text:0000000000400746 mov eax, [rbp+rax*4+var_90]
.text:000000000040074D mov esi, eax
.text:000000000040074F mov edi, offset format ; "&#x%x;\n"
.text:0000000000400754 mov eax, 0
.text:0000000000400759 call _printf


y como puse el analsis dinamic primero, ya sabemos que ese "&#x%x;\n" es el formato con el que nos da el password, asi que estamos seguros de que esta es la funcion importante :)

En edi esta el format string, y en esi la referencia al string. Unas lineas antes vemos que lo carga desde [rbp+rax*4+var_90]. Y si buscamos mas referencias a ese "var_90" encontramos esto unas lineas mas arriba:


.text:0000000000400706 lea rax, [rbp+var_90]
.text:000000000040070D mov edx, offset unk_400A40

ese "unk_400A40" es un buffer con datos estaticos que tenemos que mirar:

Y allí vemos el password que queriamos :)  ¿No? OK, ignoren los "db 0" y toda el texto inicial:

.rodata:0000000000400AA0 db 62h ; b
.rodata:0000000000400AA4 db 79h ; y
.rodata:0000000000400AA8 db 70h ; p
.rodata:0000000000400AAC db 61h ; a
.rodata:0000000000400AB0 db 73h ; s
.rodata:0000000000400AB4 db 73h ; s
.rodata:0000000000400AB8 db 69h ; i
.rodata:0000000000400ABC db 74h ; t
.rodata:0000000000400AC0 db 6Eh ; n
.rodata:0000000000400AC4 db 6Fh ; o
.rodata:0000000000400AC8 db 77h ; w
Aqui combinaron dos trucos:
  1. almacenaron cada byte como un dword para que la cadena no se identifique inmediatamente
  2. escribieron el texto inicial en unicodes raros, por para que uno pase por encima del buffer y no los identifique como texto.
Solo para que lo recuerden, esta era la salida que veiamos en analisis dinamico si ignoramos el mensaje inicial que estaba representado en unicode:

b
y
p
a
s
s
i
t
n
o
w
Y confirmamos que son los mismos valores :)

Analisis estático de "tang0" iniciando via imports

La segunda opcion que mencione para antes para hacer el analsiis estatico era usar imports.  Vamos a "View" → "Open subview" → "Imports" (No tiene shortcut) y sale esto:

Address          Ordinal Name                           Library
-------          ------- ----                           -------
0000000000600FB8         __isoc99_fscanf@@GLIBC_2.7          
0000000000600FBC         puts@@GLIBC_2.2.5                    
0000000000600FC0         fclose@@GLIBC_2.2.5                  
0000000000600FC4         strlen@@GLIBC_2.2.5                  
0000000000600FC8         printf@@GLIBC_2.2.5                  
0000000000600FCC         __libc_start_main@@GLIBC_2.2.5      
0000000000600FD0         strcmp@@GLIBC_2.2.5                  
0000000000600FD4         fopen@@GLIBC_2.2.5                  
0000000000600FD8         __isoc99_fscanf                      
0000000000600FDC         puts                                
0000000000600FE0         fclose                              
0000000000600FE4         strlen                              
0000000000600FE8         printf                              
0000000000600FEC         __libc_start_main                    
0000000000600FF0         strcmp                              
0000000000600FF4         fopen                                
0000000000600FF8         _ITM_deregisterTMCloneTable          
0000000000600FFC         __gmon_start__                      
0000000000601000         _Jv_RegisterClasses                  
0000000000601004         _ITM_registerTMCloneTable          

El "strcmp" nos llama de inmediato la atencion (ya explicamos porque) asi que buscamos referencias a esta funcion (recuerden, primero doble-click y luego "x":

Aparece esta:

Up o .got.plt:off_600F88 dq offset strcmp

Lo que encontramos fue una referencia a otra referencia (esto es comun con los imports, asi funcionan pero no vamos a parar a explicar porque).  Solo es darle "x" de nuevo:

Direction Type Address Text          
--------- ---- ------- ----          
Up        p    main+68 call    _strcmp
Up        p    main+BD call    _strcmp

Estas son las dos comparaciones que hace el bianrio para validar el nombre de archivo y el token.  Podemos seguir cada una y el procedimiento seria el mismo de la tecnica anterior con "strings".

Tambien podriamos devolvernos, y en lugar de intentar sacar el password nosotros mismos, podriamos intentar obtener los valores necesarios para ejecutar el binario… en ese caso solo tendriamos que seguir seguir el XREF anterior a cada strcmp para regresar un poco… pero esto
se parece mucho a la opcion 3. Asi que veamos esa directamente:

Analisis estático de "tang0" iniciando en el main()

Otra buena opcion, especialmente en binarios cortos, es empezar desde el "principio".  Esta es la funcion main():



Una buena parte ya la hemos visto antes, pues es donde se hacen lso strcmp y se imprimen los mensajes de exito o fracaso…  La parte interesante y nueva esta justo al principio de la funcion:

.text:00000000004007CE                 push    rbp
.text:00000000004007CF                 mov     rbp, rsp
.text:00000000004007D2                 sub     rsp, 60h
.text:00000000004007D6                 mov     [rbp+var_54], edi
.text:00000000004007D9                 mov     [rbp+var_60], rsi
.text:00000000004007DD                 cmp     [rbp+var_54], 2
.text:00000000004007E1                 jz      short loc_4007F2
.text:00000000004007E3                 mov     edi, offset aAccessDenied ; "Access denied"
.text:00000000004007E8                 call    _puts
.text:00000000004007ED                 jmp     locret_400902
.text:00000000004007F2 ; ---------------------------------------------------------------------------
.text:00000000004007F2
.text:00000000004007F2 loc_4007F2:                             ; CODE XREF: main+13↑j
.text:00000000004007F2                 mov     rax, 2E6B2E722E612E4Dh
.text:00000000004007FC                 mov     qword ptr [rbp+var_40], rax
.text:0000000000400800                 mov     [rbp+var_38], 2E332E38h
.text:0000000000400807                 mov     [rbp+var_34], 61h
.text:000000000040080D                 mov     [rbp+var_32], 0
.text:0000000000400811                 mov     rax, 78742E6E336B3074h
.text:000000000040081B                 mov     qword ptr [rbp+s2], rax
.text:000000000040081F                 mov     [rbp+var_48], 74h
.text:0000000000400823                 jmp     loc_4008E7

El primer bloque solo es la validacion del numero de argumentos. Recuerden que salia "Access denied" si no le pasabamos un nombre de archivo como argumento…

Pero el segundo bloque es muy interesante porque tiene cinco valores magicos:
  • 2E6B2E722E612E4Dh
  • 2E332E38h
  • 61h
  • 78742E6E336B3074h
  • 74h
Cuando nos encontramos este tipo de constantes (tambien llamadas "inmediatos"), lo ideal es entender que son.  A veces podemos buscarlos e identificar el algoritmos solo a partir de los valores inmediatos (por ejemplo, MD5, CRC32, SHA1, todos tienen sus constantes caracteristicas, eso ayuda mucho).  En este caso, la cosa es mas facil: si le damos click derecho a cualquiera de estas constantes, IDA nos muestra representaciones alternativas (es decir, como se veria esto como octal, como binario, como decimal, como string…) y entre ellas vemos que estos valores pueden representarse como strings (tambien pueden darle click a cada constante y usar el shortcut "r"):
  • 2E6B2E722E612E4Dh  =  '.k.r.a.M'
  • 2E332E38h  =  '.3.8'
  • 61h  =  'a'
  • 78742E6E336B3074h  =  'xt.n3k0t'
  • 74h  =  't'
Entonces en un qword, un dword y un byte esta el string "a.3.8.k.r.a.M" que es el token que necesitamos (visto al reves por asuntos del endianess). Y en un qword y umn dword esta el string "txt.n3k0t" que es el nombre del archivo (nuevamente, al reves por el endianess).  Aqui solo seria ejecutar con estos valores como hicimos inicialmente.

Segundo binario: reverseme

El segundo binario fue reverseme.  Otro ELF64. Este si daba un t0k3n para la plataforma por 200 puntos.  Este es el enlace y el password del 7z:

 https://www.dropbox.com/s/wsr8y2uioga4fv5/reverseme.7z?dl=1 R3verS3ME!

Si cargamos en IDA Pro y vemos el desensamblado solo encontraremos una funcion. Ningun import.  Esto es señal de que este binario se escribio directamtne en assembly, no en C como el anterior.

Este es el desensamblado de la funcion. No hace falta que traten de entenderla, solo la pongo para tener la referencia completa y asi hacernos a una idea de la clase de funcion que tendriamos que analizar (una serie larguisima de operaciones aritmeticas o logicas que parecen un trabalenguas).

Es interesante que solo incluye cuatro saltos: 3 condicionales (lineas 13, 21, y 32) y uno incondicional (linea 26).  Los dos primeras saltos condicionales parecen ser algunas verificaciones iniciales, pero el tercera es una bifurcacion interesante porque comparamos el registro ecx con un valor magico 0EFBA893Fh (linea 31)y si son iguales (ese JZ = Jump-if-Zero es equivalente a JE = Jump-if-Equal) saltamos hacia b30e55a8 (para ejecutar todo ese trabalenguas), pero si no son iguales llamamos a "sys_exit" y el programa termina de inmediato.

Asi que para que se ejecute todo ese bloque larguisimo de operaciones logicas y aritmeticas ecx tiene que llegar con el valor 0EFBA893Fh.

"reverseme" via parchado del binario

Esta vez veremos otra tecnica distinta. Modificaremos el binario para cambiar su comportamiento.  Lo interesante es que aplicar parches es generalmente la tecnica mas rapida posible.

La idea principal es que quisieramos que ese bloque largo de instrucciones se ejecute siempre, no solamente cuando se cumple la condicion de ECX = 0xEFBA893F.

Lo que haremos sera modificar el salto, cambiando este:

.text:080480B9                 jz      short b30e55a8

por un salto incondicional:

.text:080480B9                 jmp     short b30e55a8

Y todo queda resuelto :)

Para esto necesitamos saber dos cosas:
  1. Como se representa un JMP en hexadecimal (pues vamos a editar directamente el valor numerico en el archivo)
  2. En que posicion del archivo esta lo que queremos cambiar?

La primera es relativamente facil. La verdad es que los JMP pueden escribirse de varias formas diferentes dependiendo de que tan lejos tenemos que saltar (algunos desensambladores muestran diferentes opciones como JMP SHORT, JMP NEAR, JMP FAR, otros solo los llamaran JMP).  Para saber que tan lejos estamos saltando una opcion es ver como se codifico el salto condicional (si tambien hay varios valores posibles en hexa para cada salto condicional).  Para esto, le damos click a la linea que queremso estudiar:

.text:080480B9                 jz      short b30e55a8

Y vamos  "View" → "Open subviews" → "Hex Dump"

y aparece un volcado hexadecimal con dos valores resaltados "74 09":

080480B9  74 09 B8 01 00 00 00 31  DB CD 80 31 C0 31 DB 31
080480C9  C9 31 D2 50 89 C1 29 C8  BB 41 4D 2E 2A B9 37 4D
080480D9  2E 2A 01 D8 29 C8 89 C6  89 CB 29 D9 B8 82 BF 24
080480E9  47 BB 4B BF 24 47 01 C1  29 D9 C1 E6 08 09 CE 89
080480F9  DA 29 D3 B8 5D BB A9 7F  BA 29 BB A9 7F 01 C3 29
08048109  D3 C1 E6 08 09 DE 89 CA  29 D1 B8 1D 01 9F 7D BA
08048119  EA 00 9F 7D 01 C1 29 D1  C1 E6 08 09 CE 56 31 F6
08048129  89 C2 29 D0 B9 F0 DB 8E  3B BA 7E DB 8E 3B 01 C8
08048139  29 D0 89 C6 89 DA 29 D3  B8 0A 24 97 20 BA C3 23
08048149  97 20 01 C3 29 D3 C1 E6  08 09 DE 89 DA 29 D3 B9
08048159  75 7A 3F 70 BA 55 7A 3F  70 01 CB 29 D3 C1 E6 08
08048169  09 DE 89 C3 29 D8 BA A4  C2 84 16 BB 80 C2 84 16
08048179  01 D0 29 D8 C1 E6 08 09  C6 56 31 F6 89 C3 29 D8
08048189  BA D3 E7 D0 68 BB B2 E7  D0 68 01 D0 29 D8 89 C6


Aqui 0x74 representa el salto condicional y 0x09 representa el destino. Veamos de nuevo el salto y algunas instrucciones mas:

.text:080480B9                 jz      short b30e55a8
.text:080480BB                 mov     eax, 1
.text:080480C0                 xor     ebx, ebx        ; status
.text:080480C2                 int     80h             ; LINUX - sys_exit
.text:080480C4 ; ---------------------------------------------------------------------------
.text:080480C4
.text:080480C4 b30e55a8:                               ; CODE XREF: a7f143da+39↓j

El salto esta en la primera linea (que corresponde a la direccion 0x080480B9) y su destino es la etiqueta "b30e55a8" (que esta en la ultima linea de ese trozo y corresponde a la instruccion 0x080480C4).  Si calculamos la diferencia entre las direcciones vemos que  0x080480C4 - 0x080480B9 es igual a 11 bytes.

¿11 bytes? pero el salto esta codificado como "09", ¿no deberían ser 9 bytes? si, son 9. Solo tenemos que restar la longitud de la instruccion del salto, recuerden que ese "jz short b30e55a8" se codifica como "74 09". entonces son 11 bytes - 2 bytes de la instruccion = 9 bytes.

Lo importante es que tenemos un salto condicional corto "JZ SHORT" (ya IDA nos lo decia desde el principio) y que el destino esta a solo 9 bytes de distancia.

Cambiaremos el salto condicional corto por un salto incondicional corto. ¿Como se representa un salto incondicional corto? 0xEB.  Creanme.

Bueno, no me crean.  En el binario ya hay un salto incondicional corto, podemos usarlo para verificar:

.text:080480AC                 jmp     short a650fe19

Si verifican el volcado hexadecimal veran que se codifica como: "EB F1" ( y en este caso el F1 representa un valor negativo, es un salto para devolverse, pero esto es irrelevante. lo importante es que el 0xEB es JMP SHORT).

Para mas info solo busquen algo como: "x86 assembly jmp short" en google. Esta primera referencia lo explica bien: http://thestarman.pcministry.com/asm/2bytejumps.htm

OK.  Tenemos el cambio que queremos: necesitamos cambiar un 0x74 por un 0xEB :)  o tambien podriamos cambiarlo por un 0x75 que significa JNZ o JNE (Jump-if-Not-Equal).  Asi saltaria en cualquier caso menos cuando le llegue el valor esperado.  Pero el JMP es mas limpio.

Podemos verificar el cambio directamente en IDA.  En la vista Hex Dump le damos click derecho y "Edit", cambiamos 74 por EB, click derecho y "Apply changes". Al volver al desensamblado tendremos un JMP en lugar de un JZ :)

Sin embargo, esto no guarda los cambios en el binario. Solo en la representacion que hace IDA del mismo… La verdad no se si IDA tiene la opcion apra guardar tambien el cambio en el binario. tal vez si, nunca he buscado.

Para hacer eso uso siempre algun editor hexadecimal. En esta maquina tengo instalado HexWorkshop.  Pero sirve cualquiera…

Aqui volvemos al segundo problema: "En que posicion del archivo esta lo que queremos cambiar?"  Obviamente no es solo buscar un 74 porque puede estar muchas veces por ejemplo 0x74 tambien representaria el caracter 't'.

Lo normal es identificar unos 10 bytes que sirvan de patron para ubicarnos. Bueno, 10 o 20 o los que se requieran hasta encontrar un patron unico. Podriamos por ejemplo buscar la linea entera que muestra IDA en el dump: "74 09 B8 01 00 00 00 31  DB CD 80 31 C0 31 DB 31".  Aunque  en este caso solo hay un "74 09" en todo el binario. Asi que con eso podemos ubicarnos.

Vamos a HexWorkshop, cambiamos el binario, vamos a "Edit" → "Find". Y le ingresamos "7409" (o algun patron mas largo)

Eso nos ubica en la region correcta. Hacemos el cambio del "74" a "EB". Guardamos los cambios en un nuevo binario (yo lo llame "reverseme-patched") y lo ejecutamos:

user@ubuntu:~$ ./reverseme-patched
Can you reverse me?
th3 k3y y0u s33k !s: R3ver5!nG !$ Gr347

Ese era el token que buscabamos :) y ademas es muy cierto.

"reverseme" via parchado en memoria

Les dije que parchar era una opcion muy rapida y aun asi los demore mucho… esta es solo una alternativa para hacer las cosas mas rapido: en lugar de generar un nuevo binario, parcharemos en memoria el binario original.

Empezamos por lanzar el binario en Linux con gdbserver (vamos a usar IDA desde Windows
via GDB en Linux)

user@ubuntu:~$ gdbserver localhost:23946 ./reverseme
Process ./reverseme created; pid = 2142
Listening on port 23946

Luego nos vamos a IDA y en "Debugger" → "Switch debugger" seleccionamos: "Remote GDB debugger". Cuando nos pregunte Hostname le damos la IP del Linux y en puerto, el puerto en el que esta escuchando gbd: 23946

Nos preguntara "Do you want to attach?" y le decimos que si.  Esto nos llevara a IDA como depurador, usando un esquema de color terriblemente feo.

Ahora, le damos click en la linea donde verifica el valor magico:

.text:080480B3 cmp     ecx, 0EFBA893Fh

Y vamos a "Debugger" → "Breakpoints" → "Add breakpoint" (el shortcut es F2). Al hacer esto, la linea se resalta en rojo. Vamos a "Debugger" → "Continue process" (el shortcut es F9) y el programa corre hasta la parte donde se va a realizar la comparacion.

Aqui podemos editar el valor del registro ECX para ingresarle el numero que esta esperando (recuerden que era 0EFBA893Fh).  Para hacer esto, vamos a la ventana "General registers"  (por defecto esta en la esquina superior derecha) y le damos doble-click al registro RCX (asi se llama el registro de 64 bits, ECX es el nombre del registro de 32 bits, CX el de 16 bits CL el de 8 bits…)
e ingresamos el valor 0xEFBA893F.

Pulsamos F9 (o "Debugger" → "Continue process") y miramos de nuevo la consola en Linux:

user@ubuntu:~$ gdbserver localhost:23946 ./reverseme
Process ./reverseme created; pid = 2142
Listening on port 23946
Remote debugging from host 192.168.1.19
Remote side has terminated connection.  GDBserver will reopen the connection.
Listening on port 23946
Remote debugging from host 192.168.1.19
Can you reverse me?
th3 k3y y0u s33k !s: R3ver5!nG !$ Gr347

Child exited with status 0
Remote side has terminated connection.  GDBserver will reopen the connection.
Listening on port 23946

Efectivamente, nos dio el token y termino el proceso :)  Mucho mas rapido, ¿no?

Otra opcion habria sido poner el breakpoint en el JZ en lugar de ponerlo en el CMP.  Entonces, en lugar de modificar el registo RCX, tendriamos que modificar la flag ZF.

Finalmente, esto de parchar en memoria tambien podria hacerse directamente en GDB, sin enlazarse con IDA:

user@ubuntu:~$ gdb ./reverseme
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./reverseme...(no debugging symbols found)...done.

(gdb) break *0x080480B3
Breakpoint 1 at 0x80480b3

(gdb) run
Starting program: /home/user/reverseme
Can you reverse me?

Breakpoint 1, 0x080480b3 in b8653e9a ()

(gdb) set $ecx = 0xEFBA893F

(gdb) continue
Continuing.
th3 k3y y0u s33k !s: R3ver5!nG !$ Gr347
[Inferior 1 (process 2259) exited normally]
(gdb) quit
user@ubuntu:~$

Tercer binario: codeName.exe

El tercer binario que reversamos fue codeName.exe, un PE32. Otros 200 puntos. Aqui esta el enlace y la contraseña que teniamos para el 7z:

https://www.dropbox.com/s/g0n8vona55pfdcu/codeName.7z?dl=1 cur74lcun4bul4

Creanlo o no este era todo un regalo pero se volvio dificil por no incluir las dll necesarias (y luego por algunos otros males).  Mucha gente en el IRC lloraba por eso. Y nosotros tambien lo sufrimos.

Por ahora lo haremos de la forma dificil, sin ejecutar nunca el binario:

Esta vez les ahorrare el desensamblado.  Pero la idea es similar a los anteriores.  Hay algunos strcmp atractivos que nos ayudan a entender lo que esta pasando.  De hecho, esto es mas o menos lo que pasa:



¿Que? ¿de donde salio eso?

Si. IDA Pro no solo desensambla y depura, tambien decompila… pero no habia querido decirles para que no hicieran trampa en lso dos niveles anteriores…

Ese es el decompilado que genera HexRays (el decompilador de IDA Pro, que soporta x86, x86-64, y ARM, pero cada uno se vende por separado).

Es suficientemente claro que solicita el nombre una "operacion" y lo valida. Algunas ya estan expiradas, por ejemplo: "Supersonic Snake" o "Duck Meat", pero hay una que esta activa y nos dara el mensaje: "Operation in effect, th3 t0k3n you s33k is: %s\n"

Vemos que hay una funcion que se encarga de generar el nombre de la operacion activa: generateOpName(), vemos tambien que poco despues de ella se llama a tambian a la funcion sha256().

Veamos que hace generateOpName():

int __cdecl generateOpName(int a1)
{
  int v1; // ST78_4@1
  int v2; // eax@1
  char v3; // al@1
  int v4; // eax@1
  char v5; // al@1
  int v6; // eax@1
  char v7; // al@1
  int v8; // eax@1
  char v9; // al@1
  int v10; // eax@1
  char v11; // al@1
  int v12; // eax@1
  char v13; // al@1
  int v14; // eax@1
  char v15; // al@1
  int v16; // eax@1
  char v17; // al@1
  int v18; // eax@1
  char v19; // al@1
  int v20; // eax@1
  char v21; // al@1
  int v22; // eax@1
  char v23; // al@1
  int v24; // eax@1
  char v25; // al@1
  int v26; // eax@1
  char v27; // al@1
  int v28; // eax@1
  char v30; // [sp+93h] [bp-9h]@1

  std::allocator<char>::allocator(&v30);
  std::string::string(a1);
  std::allocator<char>::~allocator(&v30);
  v1 = (char)a();
  v2 = std::string::operator+=(a1);
  std::string::operator=(a1, v2);
  v3 = b();
  v4 = std::string::operator+=(a1);
  std::string::operator=(a1, v4);
  v5 = c();
  v6 = std::string::operator+=(a1);
  std::string::operator=(a1, v6);
  v7 = d();
  v8 = std::string::operator+=(a1);
  std::string::operator=(a1, v8);
  v9 = e();
  v10 = std::string::operator+=(a1);
  std::string::operator=(a1, v10);
  v11 = f();
  v12 = std::string::operator+=(a1);
  std::string::operator=(a1, v12);
  v13 = g();
  v14 = std::string::operator+=(a1);
  std::string::operator=(a1, v14);
  v15 = h();
  v16 = std::string::operator+=(a1);
  std::string::operator=(a1, v16);
  v17 = a();
  v18 = std::string::operator+=(a1);
  std::string::operator=(a1, v18);
  v19 = j();
  v20 = std::string::operator+=(a1);
  std::string::operator=(a1, v20);
  v21 = k();
  v22 = std::string::operator+=(a1);
  std::string::operator=(a1, v22);
  v23 = l();
  v24 = std::string::operator+=(a1);
  std::string::operator=(a1, v24);
  v25 = m();
  v26 = std::string::operator+=(a1);
  std::string::operator=(a1, v26);
  v27 = n();
  v28 = std::string::operator+=(a1);
  std::string::operator=(a1, v28);
  return a1;
}

No es tan claro como el anterior por que C++ no decompila tan bien como C, pero lo que esta pasando es que se llama a varias funciones con nombres de letras: a(), b(), c(), … y asi hasta la n().
Entre cada llamado se mete tambien ese "operator+=" y ´para los que nunca aprendimos C++, siemrpe esta la documentacion online: http://www.cplusplus.com/reference/string/string/operator+=/
asi que, basicamente, estamos concatenando las salidas de esas funciones…

Si analizamos cada una de esas mini-funciones veremos que solo retornan una letra y ya :P

Por ejemplo, este es el desensamblado de la funcion a():

signed int a(void)
{
  return 'T';
}

facil!

y la funcion b()?

signed int b(void)
{
  return 'i';
}

facil :)

Si seguimos asi llegamos a la funcion n():

signed int n(void)
{
  return 10;
}

que retorna 0x10 o '\n' :)

Concatenando todo se forma el string: 'Time to Tang0\n'

Y si calculamos el sha256 tenemos el token correcto:

user@ubuntu:~$ echo "Time to Tang0" | sha256sum
e48e631e8ffeee32a377541d50ec4f2080ca6020d4d224770ed9e6687b1ed7b0  -

Pero al principio no me funcionaba.  Primero porque el primer binario que subieron estaba malo y tuvieron que cambiarlo, y luego porque al parecer el scoreboard no soportaba tokens tan largos :S

En fin, ese si es el token correcto, pero para estar seguros lo ideal es ejecutar el binario y dejar que el lo calcule :)

Como les dije antes, ejecutar el binario era un todo reto aparte. A nosotros tambien nos hizo sufrir. Alguien del equipo encontro unas dll que parecian funcionar, pero no… Se rompia mas adelante :S

A decir verdad, solo logre ejecutarlo cuando alguien dijo en el IRC que logro hacerlo correr con las dll que vienen en apkstudio.  Sabiendo esto el reto no tomaba ni un segundo :(

Descargue apkstudio-2.0.3b-windows.zip y fui sacando las DLL que me pedia el binario. Al final se necesitan tres: libgcc_s_dw2-1.dll, libstdc++-6.dll, y libwinpthread-1.dll

[Anaconda2] E:\Users\ruben\Desktop>codeName.exe
Enter the uber secret operation code name: Time to Tang0
Operation in effect, th3 t0k3n you s33k is: e48e631e8ffeee32a377541d50ec4f2080ca
6020d4d224770ed9e6687b1ed7b0

Si, efectivamente ese es el nombre de la operacion, y ese es el token correcto :)

Ahora bien, ¿que habria pasado si hubieramos encontrado las dll correctas desde el principio?

Hay varias formas de afrontarlo, pero podriamos por ejemplo usar el mismo truco del reto anterior (mas o menos):

.text:00401924                 mov     [esp+108h+Format], eax ; Str2
.text:00401928                 lea     eax, [ebp+Buf]
.text:0040192E                 mov     [esp+108h+var_108], eax ; this
.text:00401931                 call    _strcmp
.text:00401936                 test    eax, eax
.text:00401938                 setz    al
.text:0040193B                 test    al, al
.text:0040193D                 jz      short loc_40195B
.text:0040193F                 lea     eax, [ebp+var_24]
.text:00401942                 mov     ecx, eax
.text:00401944                 call    __ZNKSs5c_strEv ; std::string::c_str(void)
.text:00401949                 mov     [esp+108h+Format], eax
.text:0040194D                 mov     [esp+108h+var_108], offset aOperationInEff ; "Operation in effect, th3 t0k3n you s33k"...
.text:00401954                 call    _printf
.text:00401959                 jmp     short loc_401967

Tenemos un este JZ:

.text:0040193D                 jz      short loc_40195B

Pero esta vez no queremos cambiarlo por un JMP pues siempre fallaria, aun con el nombre de la oepracion correcta.  Lo que se hace en estos casos es cambiarlo las instrucciones por NOP (Por tantos NOP como sea necesario).  Ya sabemos del reto anterior que un JZ SHORT requiere dos bytes.  Pues bien la operacion NOP (que no hace nada, solo ocupa espacio) requiere un byte y se representa por 0x90 entonces podriamos cambiar el "74 1C" que representa ese salto por un "90 90" que no hace nada, y el nuevo binario siempre nos dara el token.  Bueno, no siempre.  Si le damos algun nombre de operacion expirada como "Supersonic Snake" aun nos dira que esta expirada, pero cualquier otra cosa, como "abc" o "123" nos dara el token correcto :)

Ademas, podriamos no parchar nada y leer en la memoria el nombre de la operacion correcta:

Si ponemos un breakpoint aqui:

.text:00401924 mov     [esp+108h+Format], eax          ; Str2

Al ejecutar podemos poner cualquier nombre de operacion. Le damos ENTER y el para en el breakpoint.

Si en esa linea le damos doble-click a "eax" nos muestra el valor que hay almacenado alli:

debug018:003418B4 db  54h ; T
debug018:003418B5 db  69h ; i
debug018:003418B6 db  6Dh ; m
debug018:003418B7 db  65h ; e
debug018:003418B8 db  20h
debug018:003418B9 db  74h ; t
debug018:003418BA db  6Fh ; o
debug018:003418BB db  20h
debug018:003418BC db  54h ; T
debug018:003418BD db  61h ; a
debug018:003418BE db  6Eh ; n
debug018:003418BF db  67h ; g
debug018:003418C0 db  30h ; 0
debug018:003418C1 db  0Ah
debug018:003418C2 db    0

Tambien podemos pulsar "a" para verlo como un string en ascii:

debug018:003418B4 aTimeToTang0 db 'Time to Tang0',0Ah,0

Y desde aqui volvemos a ejecutar con el nombre correcto y obtenemos el token.

Moraleja

La moraleja es que aunque las cosas pueden resolverse solo con analisis estatico o solo con analisis dinamico, generalmente es mas facil mezclar las dos cosas.  En un CTF esto es especialmente util porque la idea es puntuar tan rapido como sea posible, pero ya a la hora de los write-up, vale la pena tomarse un tiempo adicional para mejorar otras habilidades, especialmente las relacionadas con el analisis estatico ;)

Ahhh! y para no terminar el post sin una imagen, que les parece esta:

Es el reto mas loco que he visto en mucho tiempo…
No me pregunten como hizo c4fdez para sacar de ahí el numero "762351".


















No hay comentarios:

Publicar un comentario