Introducción al Reversing – 0x06 Lotto

Buenos días hacía mucho tiempo que no escribía entradas de reversing y ahora vuelvo otra vez con la carga pero con una serie de retos Pwn en la cual realizaré ingeniería inversa del binario Lotto para poder tener un amplio conocimiento del funcionamiento de la lógica de la aplicación y así poder encontrar los bugs y su futura explotación. Pero como es un CTF, y tiene su correspondiente puntuación solo me limitaré hacer reversing y un poco de iteración, la explotación os la dejo a vosotros para obtener la flag ;).

En este binario tenemos un menu con 3 opciones. Antes de entrar en detalles del funcionamiento del programa vamos a ver realmente la función main() que contiene.

Función main()

Primero antes de nada realiza una serie de llamadas a la función puts() para sacar por salida estándar una serie de strings: (Selecciona menu, play lotto, help y exit).

0x00400a12 bf770d4000 mov edi, str.Select_Menu ; 0x400d77 ; "- Select Menu -"
0x00400a17 e854fcffff call sym.imp.puts ; int puts(const char *s)
0x00400a1c bf870d4000 mov edi, str.1._Play_Lotto ; 0x400d87 ; "1. Play Lotto"
0x00400a21 e84afcffff call sym.imp.puts ; int puts(const char *s)
0x00400a26 bf950d4000 mov edi, str.2._Help ; 0x400d95 ; "2. Help"
0x00400a2b e840fcffff call sym.imp.puts ; int puts(const char *s)
0x00400a30 bf9d0d4000 mov edi, str.3._Exit ; 0x400d9d ; "3. Exit"
0x00400a35 e836fcffff call sym.imp.puts ; int puts(const char *s)

Seguidamente llama a la función scanf() que obtendrá el dígito que nosotros introduzcamos en el menu y una vez salga de la función la variable local_4h tendrá el valor introducido que se moverá al registro eax para realizar las correspondientes comparaciones y saltar o no en la estructura condicional a diferentes porciones de código.

0x00400a4e e8adfcffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
0x00400a53 8b45fc     mov eax, dword [local_4h]
0x00400a56 83f802     cmp eax, 2 ; 2
0x00400a59 7416       je 0x400a71
0x00400a5b 83f803     cmp eax, 3 ; 3
0x00400a5e 741d       je 0x400a7d
0x00400a60 83f801     cmp eax, 1 ; 1
0x00400a63 7529       jne 0x400a8e
0x00400a65 b800000000 mov eax, 0
0x00400a6a e895fdffff call sym.play
0x00400a6f eb28       jmp 0x400a99
0x00400a71 b800000000 mov eax, 0
0x00400a76 e83cffffff call sym.help

Lo vemos claramente en memoria colocando un breakpoint justo después de la llamada a la función scanf().

;-- rip:
0x00400a53 b 8b45fc mov eax, dword [local_4h]

Elegimos la opción 2 ya que el scanf() esta esperando a que le pasemos por stdin el input, nuestro vector de entrada como usuario con la lógica de la aplicación.

[0x00400a03]>db 0x00400a53
[0x00400a03]>dc
2
hit breakpoint at: 400a53

Como podemos observar nuestro dígito 2 en memoria examinando rbp-0x4 dónde esta situado el puntero de nuestra variable local_4h:

[0x00400a43]>px@rbp-0x4
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x7ffdf04d62bc 0200 0000 a00a 4000 0000 0000 178b 6642 ......@.......fB

Si ejecutamos línea por línea del código saltará justo a la dirección de memoria 0x00400a71 dónde setea el registro eax a cero y seguidamente llama a la función help(), ya que fue nuestra opción escogida.

Función play()

Seguidamente vamos a centrarnos en la función play(). Para ello procedemos a colocar un breakpoint justo en el comienzo de la función en la dirección de memoria 0x00400804. Al darle a continuar hasta el breakpoint, seleccionamos en nuestro menu la primera opción y la ejecución del programa para justo al entrar en la función.
Lo mas resañable es la necesidad de escribir 6 bytes cuando realiza la llamada a la función read() como por ejemplo «$$$$$$», sino no saltará ya que no se cumple la comparación y dará un error.

0x00400893 lea rcx, qword [local_10h]
0x00400897 mov eax, dword [local_14h]
0x0040089a mov edx, 6
0x0040089f mov rsi, rcx
0x004008a2 mov edi, eax
0x004008a4 mov eax, 0
0x004008a9 call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
0x004008ae cmp eax, 6 ; 6
0x004008b1 je 0x4008c7
0x004008b3 mov edi, str.error2._tell_admin ; 0x400bd9 ; "error2. tell admin" 
0x004008b8 call sym.imp.puts ; int puts(const char *s)
0x004008bd mov edi, 0xffffffff ; -1
0x004008c2 call sym.imp.exit ; void exit(int status)
0x004008c7 mov dword [local_24h], 0

La segunda parte a mi modo de parecer de esta función play() más importante es un bucle for que itera con un contador hasta 6 veces. Lo podemos observar fácilmente en esta instrucción comparación y su posterior redirección del flujo de ejecución del programa hacia arriba:

;-- rip:
0x0040091e cmp dword [local_24h], 5 ; [0x5:4]=-1 ; 5
0x00400922 jle 0x4008d0

Lógicamente nuestra variable local_24h hará de contador y empieza por cero, podemos checkearlo en memoria en el stack:


[0x0040091e]>px@rbp-0x24
- offset - 0 1 2 3 4 5 6 7 8 9 A B 0123456789AB
0x7fffad25f78c 0000 0000 0000 0000 0000 0000 ............

Con el input introducido anteriormente sabemos que en hexadecimal el carácter ‘$’ es 24, y en decimal es 36. Es interesante tener en mente el valor en hexadecimal ya que aparecerá en bastantes ocasiones en el registro rax.
Una vez dentro del bucle lo más resañable es que en el registro rdx almacenará un valor de suma importancia ya que realizando debugging más adelante corresponde con un valor random en decimal. Daros cuenta que no disponemos del código fuente del programa, por lo tanto es muy importante saber interpretar y relacionar ciertos valores que luego aparecen en el programa y son de gran relevancia para poder encontrar fallos o entender la lógica del programa.
Al finalizar de realizar el debugging del bucle apuntando los valores almacenados justo antes de dar el salto del registro rdx, son los siguientes:

0 --> 16
1 --> 23
2 --> 10
3 --> 27
4 --> 1d
5 --> 24

¿Recordais? Nuestro valor ‘$’ en hexadecimal es 24 y coincide con el ultimo valor del registro rdx del bucle.
Justo la instrucción importante dentro del bucle dónde varía nuestro registro rdx es: 0x0040090e 8d5001 lea edx, dword [rax + 1] ; 1

También tenemos la instrucción que a continuación voy a mostrar, donde se mueve al registro rcx el valor en decimal de 45. ¿Quizás el programa genera unos valores random de 1-45? Tampoco lo sabemos bien porque aún vamos un poco a ciegas pero si sabemos que los valores que hemos obtenido en el bucle el más grande en decimal es 39. De seis posibilidades ninguna se ha excedido de 45, si queremos verificar esto sólo tendríamos que realizar muchas pruebas y sacar la conclusión.
0x00400900 b92d000000 mov ecx, 0x2d ; '-' ; 45

Una vez nuestro contador vale 5 salimos del bucle.

[0x00400911]> px@rbp-0x24
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x7ffd9ad4d2dc 0500 0000 0000 0000 0000 0000 0600 0000 ................

Se genera una serie de valores random y continuamos, pero antes de seguir debemos ver cual es el objetivo del reto. La comparación de vital importancia que podéis observar a continuación compara el valor 6 con el contenido de la variable local_20h, si es igual no salta y se le pasa de argumento /bin/cat flag a la función system() jejeje. Como el objetivo de esta entrada es hacer ingeniería inversa a la aplicación no quiero hacer spoiler de su resolución.

0x00400985 837de006   cmp dword [local_20h], 6 ; [0x6:4]=-1 ; 6
0x00400989 750c       jne 0x400997
0x0040098b bfec0b4000 mov edi, str.bin_cat_flag ; 0x400bec ; "/bin/cat flag"
0x00400990 e8fbfcffff call sym.imp.system ; int system(const char *string)

Finalmente tenemos un bucle anidado y una comparación que será importante cmp dl, al ya que si es igual no saltará y sumará a nuestra variable local_20h el valor de uno en el bucle.

0x00400962 0fb680882060  movzx eax, byte [rax + obj.submit] ; [0x602088:1]=36 ; "$$$$$$"
0x00400969 38c2          cmp dl, al
0x0040096b 7504          jne 0x400971
0x0040096d 8345e001      add dword [local_20h], 1
0x00400971 8345e401      add dword [local_1ch], 1
0x00400975 837de405      cmp dword [local_1ch], 5 ; rax ; [0x5:4]=-1
0x00400979 7ed8          jle 0x400953
0x0040097b 8345dc01      add dword [local_24h], 1
0x0040097f 837ddc05      cmp dword [local_24h], 5 ; rax ; [0x5:4]=-1
0x00400983 7ec5          jle 0x40094a

Si no es igual el valor saltará a la dirección de memoria 0x00400971 y no realizará dicha suma. Antes de finalizar y sin realizar mucho spoiler, vemos los valores del registro rdx que obtuvimos anteriormente realizando debugging. Justo cuando rip apunta a la comparación cmp dl, al el valor de nuestros registros es la siguiente.

RAX 0x24 Nuestro valor '$' en hexadecimal
RDX 0x16 Nuestro primer valor random que nos salio anteriormente

Como no son iguales realizará el salto y no sumará 1 a la variable local_20h jeje. Bueno creo que con esto esta entendido el funcionamiento de la aplicación ahora viene la parte de explotación para obtener la flag en la que no explicaré para evitar spoiler. Si quieren o tienen dudas de su resolución o explotación, pueden escribirme un mensaje privado o comentarlo por el grupo de telegram Hispapwners. Pueden revisar mi exploit si quieren cuando lo tengan resuelto usando la flag como password del .7z

Un saludo, naivenom.