Abonero (Rev 300)

By @xbytemx

Descripcion:


Ahora que ya sabes como utilizar el downloader cargad0r, descarga del  
directorio raiz de dicho servidor web interno el binario de nombre bunkerClient
Este binario es un port knocker, esto es, manda una secuencia de puertos al  
servidor en el Bunker para habilitar temporalmente (5 minutos) el acceso a 2  
puertos restringidos.

Tu tarea es hacerle ingenieria inversa a dicho binario y encontrar las llaves 
que permiten descifrar la secuencia de puertos para habilitar MySQL y el  
servicio ELITE respectivamente.

TU FLAG sera la contatenacion de ambas llaves descifradas en este orden: 
 
               <llave_MySQL><llave ELITE>

**** IMPORTANTE ****

Al resolver este reto, debes seguir una de las 2 siguientes trayectorias,  
ambas te llevaran a dentro del BUNKER por lo que no es necesario resolver las 2

Guardian (Pwn 400) o DrunkTillPwan (Web 400)

Recibimos un binario el cual tiene la siguiente salida via file:

xbytemx@laptop:~/dev/reto300$ file bunkerClient
bunkerClient: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3fd00d66ade317de7c674f5fc3f2da99f0c7fa04, for GNU/Linux 3.2.0, not stripped

Observamos que se trata de un binario ELF64, enlazado y not stripped. Si lo ejecutamos tenemos lo siguiente:

xbytemx@laptop:~/dev/reto300$ ./bunkerClient
Modo de uso: bunkerClient [MYSQL|ELITE] <llave>

Nos indica que recibe argumentos, entonces hacemos las siguientes pruebas:

xbytemx@laptop:~/dev/reto300$ ./bunkerClient asdasd
El parametro de conexión es incorrecto
xbytemx@laptop:~/dev/reto300$ ./bunkerClient asdasd asdasd
El parametro de conexión es incorrecto
xbytemx@laptop:~/dev/reto300$ ./bunkerClient asdasd asdasd asdasd
El parametro de conexión es incorrecto
xbytemx@laptop:~/dev/reto300$ ./bunkerClient asdasd asdasd asdasd asdasd
El parametro de conexión es incorrecto
xbytemx@laptop:~/dev/reto300$ ./bunkerClient MYSQL asdasd
Llave incorrecta
xbytemx@laptop:~/dev/reto300$ ./bunkerClient MYSQL asdasd asdasd
Llave incorrecta
xbytemx@laptop:~/dev/reto300$ ./bunkerClient MYSQL asdasd asdasd asdasd
Llave incorrecta

Iniciamos con el análisis estático con un strings el cual nos devuelve la siguiente información interesante:

u/UH
Llave inH
correctaH
[]A\A]A^A_
Modo de uso: bunkerClient [MYSQL|ELITE] <llave>
localhost
Cannot resolve hostname
MYSQL
ELITE
El parametro de conexi
n es incorrecto
Cannot open socket
hitting tcp localhost:%d
Open Sesame?
;*3$"

Tenemos el mensaje de sin argumentos, el de parámetros incorrectos y el de llave incorrecta. Adicionalmente vemos un hitting tcp y un open sesame. Ninguno de flag o marca de flag.

Función main

Lo que vemos a continuación es como se carga en dos partes del stack diferentes, dos arreglos de bytes.

En la primera parte tenemos desde RBP-0x80 hasta RBP-0x44, y en la segunda de RBP-0xc0 hasta RBP-0x84.


[0x000013d9]> pdf
┌ 889: int main (int argc, char **argv, char **envp);
           ; var int32_t var_3eh @ rbp-0x3e
           ; var int32_t var_3ch @ rbp-0x3c
           ; var int32_t var_40h @ rbp-0x40
           ; var int32_t var_2ch @ rbp-0x2c
           ; var int32_t var_28h @ rbp-0x28
           ; var int32_t var_22h @ rbp-0x22
           ; var int32_t var_4h @ rbp-0x4
           ; var int32_t var_10h @ rbp-0x10
           ; var int32_t var_20h @ rbp-0x20
           ; var int32_t var_84h @ rbp-0x84
           ; var int32_t var_88h @ rbp-0x88
           ; var int32_t var_8ch @ rbp-0x8c
           ; var int32_t var_90h @ rbp-0x90
           ; var int32_t var_94h @ rbp-0x94
           ; var int32_t var_98h @ rbp-0x98
           ; var int32_t var_9ch @ rbp-0x9c
           ; var int32_t var_a0h @ rbp-0xa0
           ; var int32_t var_a4h @ rbp-0xa4
           ; var int32_t var_a8h @ rbp-0xa8
           ; var int32_t var_ach @ rbp-0xac
           ; var int32_t var_b0h @ rbp-0xb0
           ; var int32_t var_b4h @ rbp-0xb4
           ; var int32_t var_b8h @ rbp-0xb8
           ; var int32_t var_bch @ rbp-0xbc
           ; var int32_t var_c0h @ rbp-0xc0
           ; var int32_t var_44h @ rbp-0x44
           ; var int32_t var_48h @ rbp-0x48
           ; var int32_t var_4ch @ rbp-0x4c
           ; var int32_t var_50h @ rbp-0x50
           ; var int32_t var_54h @ rbp-0x54
           ; var int32_t var_58h @ rbp-0x58
           ; var int32_t var_5ch @ rbp-0x5c
           ; var int32_t var_60h @ rbp-0x60
           ; var int32_t var_64h @ rbp-0x64
           ; var int32_t var_68h @ rbp-0x68
           ; var int32_t var_6ch @ rbp-0x6c
           ; var int32_t var_70h @ rbp-0x70
           ; var int32_t var_74h @ rbp-0x74
           ; var int32_t var_78h @ rbp-0x78
           ; var int32_t var_7ch @ rbp-0x7c
           ; var int32_t var_80h @ rbp-0x80
           ; var int32_t var_14h @ rbp-0x14
           ; var int32_t llave_insertada @ rbp-0xd0
           ; var int32_t arg_c @ rbp-0xc4
           ; arg signed int argc @ rdi
           ; arg char **argv @ rsi
           ; DATA XREF from entry0 @ 0x114d
           0x000013d9      55             push rbp
           0x000013da      4889e5         mov rbp, rsp
           0x000013dd      4881ecd00000.  sub rsp, 0xd0
           0x000013e4      89bd3cffffff   mov dword [arg_c], edi      ; argc
           0x000013ea      4889b530ffff.  mov qword [llave_insertada], rsi ; argv
           0x000013f1      c745ec050000.  mov dword [var_14h], 5
           0x000013f8      c74580a00000.  mov dword [var_80h], 0xa0
           0x000013ff      c74584ac0000.  mov dword [var_7ch], 0xac
           0x00001406      c74588ad0000.  mov dword [var_78h], 0xad
           0x0000140d      c7458c9d0000.  mov dword [var_74h], 0x9d
           0x00001414      c74590ad0000.  mov dword [var_70h], 0xad
           0x0000141b      c74594a70000.  mov dword [var_6ch], 0xa7
           0x00001422      c74598a30000.  mov dword [var_68h], 0xa3
           0x00001429      c7459ca40000.  mov dword [var_64h], 0xa4
           0x00001430      c745a09b0000.  mov dword [var_60h], 0x9b
           0x00001437      c745a4a50000.  mov dword [var_5ch], 0xa5
           0x0000143e      c745a8a40000.  mov dword [var_58h], 0xa4
           0x00001445      c745ac9b0000.  mov dword [var_54h], 0x9b
           0x0000144c      c745b0a20000.  mov dword [var_50h], 0xa2
           0x00001453      c745b49f0000.  mov dword [var_4ch], 0x9f
           0x0000145a      c745b8b50000.  mov dword [var_48h], 0xb5
           0x00001461      c745bcaa0000.  mov dword [var_44h], 0xaa
           0x00001468      c78540ffffff.  mov dword [var_c0h], 0xa9
           0x00001472      c78544ffffff.  mov dword [var_bch], 0x98
           0x0000147c      c78548ffffff.  mov dword [var_b8h], 0xaa
           0x00001486      c7854cffffff.  mov dword [var_b4h], 0x99
           0x00001490      c78550ffffff.  mov dword [var_b0h], 0xb3
           0x0000149a      c78554ffffff.  mov dword [var_ach], 0x9f
           0x000014a4      c78558ffffff.  mov dword [var_a8h], 0x9a
           0x000014ae      c7855cffffff.  mov dword [var_a4h], 0x9c
           0x000014b8      c78560ffffff.  mov dword [var_a0h], 0x9b
           0x000014c2      c78564ffffff.  mov dword [var_9ch], 0x9a
           0x000014cc      c78568ffffff.  mov dword [var_98h], 0x9d
           0x000014d6      c7856cffffff.  mov dword [var_94h], 0xa7
           0x000014e0      c78570ffffff.  mov dword [var_90h], 0xa4
           0x000014ea      c78574ffffff.  mov dword [var_8ch], 0x9f
           0x000014f4      c78578ffffff.  mov dword [var_88h], 0xb0
           0x000014fe      c7857cffffff.  mov dword [var_84h], 0x99
           0x00001508      83bd3cffffff.  cmp dword [arg_c], 1

¿Como sabemos que son dos arreglos diferentes? Si buscamos referencias en la función a cualquier otra variable inicial, solo encontraremos dos:


Renombraremos de ahora en adelante estas variables a buf1 (var_80h) y buf2 (var_c0h).

Cuando vemos la siguiente sección, tenemos un cmp de argc > 1, el cual si es verdadero realiza un jump (bloque derecho), sino se va por el bloque izquierdo. Este bloque izquierdo, imprime el texto de como usar el programa y termina con estado 1 el programa.
El bloque derecho manda a llamar a gethostbyname con el argumento localhost. La salida de esta función es almacenada en estado_gethostbyname.

Acto seguido, el valor almacenado en estado_gethostbyname es comparado con 0, lo cual significa que trata de validar que el resultado sea exitoso para continuar su viaje, teniendo el jump condicional not equal 0. Si estado_gethostbyname fue 0, nos vamos por el bloque izquierdo, el cual carga varios argumentos a fwrite, que como podemos ver en radare, nos imprimira en pantalla que no pudo resolver el hostname y terminara el programa con estatus 1.

Por otra parte si estado_gethostbyname fue diferente de 0, tomaremos el vector de argumentos y sacaremos el elemento argv[1] para mandarlo como argumento segundo argumento a la función strcmp. El primero corresponde al string MYSQL, el cual evalúa el resultado en las siguientes instrucciones para realizar un jump condicional:


Nuevamente de la siguiente evaluación, si el resultado de strcmp fue 0, el camino elegido seria el bloque de la izquierda, mientras que cualquier otro valor diferente el bloque de la derecha.

Si tomamos el camino de la izquierda, estaríamos llamando a la función validar_llave con los argumentos de buf1 y argv[2]. El resultado de dicha función lo estaríamos almacenando en puertos.

En caso de tomar el camino de la derecha, realizaríamos algo muy parecido al bloque anterior, en donde tomamos el valor de argv[1] y ELITE, y se lo mandamos para evaluar en strcmp.


Lo interesante es que este mismo camino abre la posibilidad a dos caminos:
  • strcmp devolvió 0, entonces mandamos a llamar a validar_llave con argumentos buf2 y argv[2], guardando el valor en puertos. Justo como con el bloque anterior.
  • strcmp es diferente de 0, nos imprime en pantalla ‘El parámetro de conexión incorrecto’ y el programa termina con un estado 1.

Si ELITE o MYSQL fueron enviados como argv[1], el programa continua:

Lo que tenemos a continuación, es un ciclo for, el cual inicializa port_counter en 0 y después compara si port_counter es menor o igual a 3. En caso de que sea menor o igual a 3, toma el camino de la izquierda, caso contrario toma el camino de la derecha.

Empezare por la derecha ya que es la hoja muerta. Básicamente imprime en pantalla “Open Sesame?” y termina el programa retornando 0 para la función main.

Si tomamos el camino de la izquierda, es decir, port_counter menor o igual a 3, tomaremos de puertos, el index de counter y lo salvaremos en puerto (puerto = puertos[port_counter]).

Terminando estas instrucciones, lo que continua es la creación de un socket, el cual se guarda en estado_socket. Si el socket fue creado correctamente, continuaremos a la derecha (estado_socket != -1), mientras que si estado_socket fue igual a -1, tomaremos el camino de la izquierda.

Como colocar las instrucciones es bastante largo, se pegara como texto y se colocaran los comentarios al final de la linea:

0x00001676      8b45d8         mov eax, dword [estado_socket]
0x00001679      ba00000000     mov edx, 0
0x0000167e      be03000000     mov esi, 3                          ; F_GETFL
0x00001683      89c7           mov edi, eax
0x00001685      b800000000     mov eax, 0
0x0000168a      e8b1f9ffff     call sym.imp.fcntl
0x0000168f      8945d4         mov dword [fd_flags], eax

Se manda a llamar a fcntl para validar a estado_socket (que en realidad es un file descriptor) y guarda las flags en fd_flags. Mas información vía “man fcntl”


0x00001692      8b45d4         mov eax, dword [fd_flags]
0x00001695      80cc08         or ah, 8
0x00001698      89c2           mov edx, eax
0x0000169a      8b45d8         mov eax, dword [estado_socket]
0x0000169d      be04000000     mov esi, 4                         ; F_SETFL
0x000016a2      89c7           mov edi, eax
0x000016a4      b800000000     mov eax, 0
0x000016a9      e892f9ffff     call sym.imp.fcntl

Se realiza un or entre las flags y 8, con la finalidad de habilitar el modo non-blocking en el file descriptor vía SET


0x000016ae      488d45c0       lea rax, [addr_s]
0x000016b2      ba10000000     mov edx, 0x10               ; size_t n
0x000016b7      be00000000     mov esi, 0                  ; int c
0x000016bc      4889c7         mov rdi, rax                ; void *s
0x000016bf      e8bcf9ffff     call sym.imp.memset         ; void *memset(void *s, int c, size_t n)

Se llama memset para escribir en addr_s 16 veces 0. Limpieza manual.


0x000016c4      66c745c00200   mov word [addr_s], 2
0x000016ca      488b45e0       mov rax, qword [estado_gethostbyname]
0x000016ce      488b4018       mov rax, qword [rax + 0x18]
0x000016d2      488b00         mov rax, qword [rax]
0x000016d5      488b00         mov rax, qword [rax]
0x000016d8      8945c4         mov dword [addr_s_addr], eax
0x000016db      0fb745de       movzx eax, word [puerto]
0x000016df      89c7           mov edi, eax
0x000016e1      e87af9ffff     call sym.imp.htons
0x000016e6      668945c2       mov word [addr_s_port], ax

Resulta que addr_s se trata de una estructura, la cual recibe 2 en la primera ubicación, estado_gethostbyname (la resolución a dirección) en la segunda ubicación y el puerto en la tercera ubicación.


0x000016ea      0fb745de       movzx eax, word [puerto]
0x000016ee      89c6           mov esi, eax
0x000016f0      488d3db50900.  lea rdi, str.hitting_tcp_localhost:_d ; 0x20ac ; "hitting tcp localhost:%d\n" ; const char *format
0x000016f7      b800000000     mov eax, 0
0x000016fc      e86ff9ffff     call sym.imp.printf         ; int printf(const char *format)

Mandamos a llamar a printf con el texto “hitting tcp localhost:%d\n”, donde “%d” es el puerto al cual nos vamos a tratar de conectar.


0x00001701      488d4dc0       lea rcx, [addr_s]
0x00001705      8b45d8         mov eax, dword [estado_socket]
0x00001708      ba10000000     mov edx, 0x10               ; size_t addrlen
0x0000170d      4889ce         mov rsi, rcx                ; void *addr
0x00001710      89c7           mov edi, eax                ; int socket
0x00001712      e8c9f9ffff     call sym.imp.connect        ; ssize_t connect(int socket, void *addr, size_t addrlen)

Llamamos a connect usando como argumentos, el file descriptor de socket, la estructura addr_s y la longitud 16 (misma que usamos en memset).


0x00001717      8b45d8         mov eax, dword [estado_socket]
0x0000171a      89c7           mov edi, eax                ; int fildes
0x0000171c      e86ff9ffff     call sym.imp.close          ; int close(int fildes)

Cerramos el fd una vez que connect termino, con lo que cerramos la conexión.


0x00001721      8b45ec         mov eax, dword [timer]
0x00001724      69c0e8030000   imul eax, eax, 0x3e8
0x0000172a      89c7           mov edi, eax                ; int s
0x0000172c      e8cff9ffff     call sym.imp.usleep         ; int usleep(int s)

Multiplicamos 1000 por timer y mandamos el resultado a usleep. Timer se trata de los segundos del timer.


0x00001731      8345fc01       add dword [port_counter], 1

Incrementamos port_counter en 1 y seguimos con el siguiente puerto.


Se realizar la conexión hacia el destino localhost:port y finalmente tras terminar los 4 eventos, se imprime el mensaje "Open Sesame?".

Concluimos que main recibe 2 argumentos, uno para control y otro como llave. Se envía llave a la función validar_llave y el resultado es un array de 4 elementos, los cuales son usados para realizar una conexión TCP hacia localhost con un delay de 5ms.

Función validar_llave

En el bloque inicial, tenemos un stack frame de 0xa0, después guardamos los argumentos en el stack, aquí las variables usadas son buf para cualquiera de los dos buffers enviados y llave para el valor de argv[2].

Se realiza un malloc de tamaño 16 bytes y se guarda la dirección en puertos. 
Inicializamos shift y letra_acomulada en 0, y guardamos dos partes de un string en dos variables, s1 y s2. Se llama a strlen que recibe el argumento llave y se compara el resultado con 16. Básicamente se valida si la llave es de 16 caracteres.

Si la llave es diferente de 16 caracteres tomamos el camino de la derecha, el cual carga s1 y “%s”, mandando a llamar a printf, el cual despliega que la contraseña es incorrecta. Como en otros casos, el programa termina con un estatus 1.

Si la llave es de longitud 16, entonces counter_caracter es igual a 0 y iniciamos un loop.
El loop compara si counter_caracter es menor de 16. En caso de que si sea menor, se toma el camino de la izquierda. En caso de que sea mayor o igual a 16, se toma el camino de la derecha donde tenemos un otra inicialización de counter_caracter y un nuevo counter, counter_splitter.

El siguiente bloque es donde se realiza en si, la validación de la llave.



El pseudocodigo de instrucciones seria algo como:

letra = llave[counter_caracter]
letra_acomulada & 3
shift = letra_acomulada & 3
letra_acomulada += letra
(1 << shift)
(letra – (1<<shift) -1) ^ 0xc7


Iniciamos var_8 en 0 y aplicamos una operación de protección que incluye un and contra 3, un shift left, un xor contra 0xc7 y varias aplicaciones cdqe que guardamos en el stack (ojo con la conversión a byte). Al final hacemos un cmp entre var_98 + counter y el resultado de las operaciones anteriores.

Si es diferente, imprimimos el mensaje de llave incorrecta. El programa continua en incrementando el counter.

Finalmente y después de varias conversiones, comparamos el valor del buffer contra el valor que resulta de la operación por cada carácter de la llave.



Si es verdadero, incrementamos el contador y iniciamos nuevamente el loop (hasta cumplir las 16 iteraciones). Si es falso, imprimimos el mensaje de llave incorrecta y terminamos el programa. Esto para cada carácter.

Como se menciono anteriormente, después del ciclo for inicial, iniciaba otro for que usaba a counter_caracter y a counter_splitter.

En este caso de hace 4 iteraciones en lugar de 16, como podemos ver por el primer bloque de 2 instrucciones.

Luego multiplicamos 100 por el valor del arreglo con ubicación en counter_splitter, y salvamos en ECX. Seguimos iterando ahora como counter_splitter +1 y guardamos en EAX.

Las siguientes instrucciones nos ayudaran a sumar el contenido de ambas direcciones RBP-70[counter_splitter]*100 y RBP-70[counter_splitter +1] para almacenarlo en puertos[counter_caracter].

Finalmente counter_caracter se incrementa 1 y counter_splitter se incrementa 2. Esto significa que guardamos 4 datos de tamaño de qword*4, usando el resultado del anterior for.



for (i=0, j=0; i<4; i++, i+=2) puertos[i] = valores[j]*100 + valores[j+1]

Solución


Tras googlear los strings, veremos que hace una referencia al programa knock con el open sesame. Solo que en este caso los puertos del port knocking se encuentran protegidos por una llave, por lo que sera necesario realizar el proceso inverso de protección o bien generar un bruteforcer que resuelva el proceso en una vía.

Se genera un solver con angr que ingresa todos los caracteres validos a las operaciones realizadas en el array de bytes enviados desde main a validar_llave.


#!/usr/bin/env python3

# -*- coding: utf-8 -*-

######################################################################

# Brute force al reto 300 de HackDef 2019                            #

######################################################################

import angr

def solver(buff):

    flag=''
    letra_acomulada = 0                         # Valor de acomulación 

    p = angr.Project("bunkerClient")            # Creamos un nuevo proyecto en angr

    state = p.factory.entry_state()             # Inicializamos el objeto de simulación   

    for i in range(16):                         # Iteramos los 16 bytes dentro de cada buffer

        letra = state.solver.BVS("l", 8)        # Definimos el objeto bitVec l de size de un byte.  

        shift = letra_acomulada & 3             # Iniciamos aplicando & 3

                                                # (posibles resultados del 0-3)

        alloc = 1 << shift                      # Luego generamos un valor usando el numero anterior

                                                # (posibles resultados 1,2,4,8)

        candidato = (letra - alloc -1)          # Usamos el valor letra y le restamos la base y uno
        candidato ^= 0xc7                       # Aplicamos la ultima protección, un xor con 0xc7
        state.solver.add(candidato == buff[i])  # Agregamos al solver de z3 final

        try:

            valor = state.solver.eval(letra)    # Realizamos los eval de candidatos a letra

            letra_acomulada += letra            # En caso de que sea valido, acomulamos letra

            flag += chr(valor)                  # Salvamos la letra valida y continuamos con la sig

        except angr.errors.SimUnsatError:       # En caso de fallo en la simulación (unsat)

            print("Fallo: " +hex(buff[i]))      # Imprimimos el valor que fallo

            state.solver._stored_solver = None  # Cerramos el solver al declararlo como no resuelto

            flag += '.'                         # Agregamos un . como valor no encontrado 

    return flag

def main():

    pass_mysql = [160, 172, 173, 157, 173, 167, 163, 164, 155, 165, 164, 155, 162, 159, 181, 170]

    pass_pwn   = [169, 152, 170, 153, 179, 159, 154, 156, 155, 154, 157, 167, 164, 159, 176, 153]

 

    print("MYSQL: {}, ELITE: {}\n".format(solver(pass_mysql), solver(pass_pwn)))

if __name__== '__main__':

    main()

Después de ejecutar el programa, encontraremos que los siguientes bytes son iguales a las siguientes llaves:

{160,172,173,157,173,167,163,164,155,165,164,155,162,159,181,170} = "ins_meme_de_gato" para MYSQL
{169,152,170,153,179,159,154,156,155,154,157,167,164,159,176,153} = "papaya_de_celaya" para ELITE

Al ejecutar el binario con dichos argumentos tendremos lo siguiente:


De esta manera, hemos pasado el filtro y tendremos los puertos para hacer el port knocking!!!

Comments