Write-up del reto Reversing: Linux Binary

Hola a tod@s soy Dialluvioso, en esta entrada les intentaré explicar el proceso metodológico a seguir para resolver el reto de ingeniería inversa: rev_fwr.

Recopilación de información

Lo primero que deberíamos hacer es ver a qué nos estamos enfrentando, para dicha labor utilizaremos el comando integrado file:

❯ file rev_fwr                                                                            ⏎
rev_fwr: ELF 64-bit LSB pie executable x86-64, version 1 (GNU/Linux), statically linked, stripped

Este nos devuelve información muy interesante. Podemos observar que es un binario con formato ELF (Executable and Linkable format), la arquitectura es de 64-bit, utiliza PIE (Position-independent executable), está enlazado estáticamente y se han despojado sus símbolos.

Probemos a ejecutarlo:

❯ ./rev_fwr 

	                  .".
                         /  |
                        /  /
                       / ,"
           .-------.--- /
          ".____.-/ o. o\
                 (    Y  )
                  )     /
                 /     (
                /       Y
            .-"         |
           /  _     \    \
          /    `. ". ) /' )
         Y       )( / /(,/
        ,|      /     )
       (_|     /     /
          \_  (__   (__        [Defended by the Guardian Rabbit]
            "-._,)--._,)

Podemos observar que está siendo “defendido” por el Guardian Rabbit. Intentemos recopilar algo más de información. Podemos utilizar DIE (Detect-It-Easy), es una aplicación multiplataforma que nos oferta mucha información respecto al tipo del archivo.
Una vez lo abrimos en DIE, nos identifica que el binario ha sido empacado con UPX 3.91 (Ultimate Packer for eXecutables).
Que haya sido empacado significa que cuando ejecutamos el programa, decomprime el código original y lo ejecuta.
El nivel de entropía es un buen indicador para hacernos a la idea de si el software ha sido comprimido no, en este caso es muy alta 7.51499 bits por byte. Los archivos comprimidos suelen tener valores cercanos a 8 bits de entropía por byte.
También podemos observar el histograma, este gráfico nos mostrará la distribución de frecuencias en las abscisas representa los intervalos de clase (en este caso bytes, desde 0x00 hasta 0xff) y en las ordenadas las frecuencias absolutas.

Unpacking

Ahora que sabemos con qué ha sido empacado, procederemos a desempacarlo. En esta ocasión es trivial, bastaría con descargarse la versión adecuada de upx y utilizar la flag -d (decompress) para desempacar/decomprimir.

❯ upx -d ../rev_fwr 
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2017
UPX 3.94        Markus Oberhumer, Laszlo Molnar & John Reiser   May 12th 2017

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
     13128 <-      6644   50.61%   linux/amd64   rev_fwr

Unpacked 1 file.
❯ file rev_fwr 
rev_fwr: ELF 64-bit LSB pie executable x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=ff0ebf4a0d96441e21e2e86674504448daf4fe5e, not stripped

Parece ser que el binario original está enlazado dinámicamente y no ha sido despojado de sus simbolos!

Guardian Rabbit

Tras desensamblar la función principal, observamos que justo tras el prólogo realiza una llamada a una función un tanto extraña:
0x0000000000000f82 <+16>: call 0xefe <QrgrpgOernxcbvagf>
Veamos que sucede (en gdb, disass QrgrpgOernxcbvagf):

   0x0000000000000efe <+0>:	push   rbp
   0x0000000000000eff <+1>:	mov    rbp,rsp
   0x0000000000000f02 <+4>:	sub    rsp,0x20
   0x0000000000000f06 <+8>:	mov    DWORD PTR [rbp-0xc],0x0
   0x0000000000000f0d <+15>:	lea    rax,[rip+0xfffffffffffff78c]        # 0x6a0 <_start>
   0x0000000000000f14 <+22>:	mov    QWORD PTR [rbp-0x8],rax
   0x0000000000000f18 <+26>:	lea    rax,[rip+0x17e]        # 0x109d
   0x0000000000000f1f <+33>:	mov    QWORD PTR [rbp-0x18],rax
   0x0000000000000f23 <+37>:	jmp    0xf63 <QrgrpgOernxcbvagf+101>
   0x0000000000000f25 <+39>:	mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000000f29 <+43>:	mov    eax,DWORD PTR [rax]
   0x0000000000000f2b <+45>:	movzx  eax,al
   0x0000000000000f2e <+48>:	cmp    eax,0xcc
   0x0000000000000f33 <+53>:	jne    0xf5e <QrgrpgOernxcbvagf+96>
   0x0000000000000f35 <+55>:	add    DWORD PTR [rbp-0xc],0x1
   0x0000000000000f39 <+59>:	lea    rdi,[rip+0x180]        # 0x10c0
   0x0000000000000f40 <+66>:	mov    eax,0x0
   0x0000000000000f45 <+71>:	call   0x660 <printf@plt>
   0x0000000000000f4a <+76>:	mov    edi,0xa
   0x0000000000000f4f <+81>:	call   0x640 <putchar@plt>
   0x0000000000000f54 <+86>:	mov    edi,0x1
   0x0000000000000f59 <+91>:	call   0x680 <exit@plt>
   0x0000000000000f5e <+96>:	add    QWORD PTR [rbp-0x8],0x1
   0x0000000000000f63 <+101>:	mov    rax,QWORD PTR [rbp-0x8]
   0x0000000000000f67 <+105>:	cmp    rax,QWORD PTR [rbp-0x18]
   0x0000000000000f6b <+109>:	jne    0xf25 <QrgrpgOernxcbvagf+39>
   0x0000000000000f6d <+111>:	mov    eax,DWORD PTR [rbp-0xc]
   0x0000000000000f70 <+114>:	leave  
   0x0000000000000f71 <+115>:	ret  

Esta rutina lo que hace es recorrer la sección .text, iterando desde _start (este símbolo es el entry point del programa) hasta __etext (fin de la sección .text) en busca del opcode 0xcc (INT 3, esta instrucción es utilizada por los depuradores para remplazar temporalmente una instrucción y establecer un poner un punto de ruptura, generando una interrupción software en el caso de GNU/Linux SISTRAP). Una vez que encuentra dicho opcode, imprime un mensaje y finaliza la ejecución.
El problema de la implementación es que no tiene en cuenta los falsos positivos.
La mera comparación: 0x0000000000000f2e <+48>: cmp eax,0xcc utilizando el opcode “hardcodeado” ya supone un falso positivo, por eso al ejecutar el programa sin ningún breakpoint nos muestra el mensaje y finaliza.
Por ahora para evadir este mecanismo nos centraremos en evitar que se ejecute durante el traceo del programa con el debugger, simplemente configurando el registro apuntador en la siguiente instrucción a ejecutar main+21 (main+21 es la siguiente instrucción, después del call a la rutina que busca los breakpoint).

Buscando la flag

Dentro de la función principal observamos lo siguiente:

   0x0000000000000fb0 <+62>:	jmp    0xff0 <main+126>
   0x0000000000000fb2 <+64>:	mov    eax,DWORD PTR [rbp-0xc]
   0x0000000000000fb5 <+67>:	cmp    eax,0xa
   0x0000000000000fb8 <+70>:	je     0xfc1 <main+79>
   0x0000000000000fba <+72>:	cmp    eax,0x5c
   0x0000000000000fbd <+75>:	je     0xfd7 <main+101>
   0x0000000000000fbf <+77>:	jmp    0xfde <main+108>
   0x0000000000000fc1 <+79>:	lea    rax,[rbp-0x210]
   0x0000000000000fc8 <+86>:	mov    esi,0x200
   0x0000000000000fcd <+91>:	mov    rdi,rax
   0x0000000000000fd0 <+94>:	call   0x7d0 <print>
   0x0000000000000fd5 <+99>:	jmp    0xff0 <main+126>
   0x0000000000000fd7 <+101>:	sub    QWORD PTR [rbp-0x8],0x1
   0x0000000000000fdc <+106>:	jmp    0xff0 <main+126>
   0x0000000000000fde <+108>:	lea    rdi,[rip+0x76b]        # 0x1750
   0x0000000000000fe5 <+115>:	mov    eax,0x0
   0x0000000000000fea <+120>:	call   0x660 <printf@plt>
   0x0000000000000fef <+125>:	nop
   0x0000000000000ff0 <+126>:	call   0x670 <getchar@plt>
   0x0000000000000ff5 <+131>:	mov    DWORD PTR [rbp-0xc],eax
   0x0000000000000ff8 <+134>:	cmp    DWORD PTR [rbp-0xc],0xffffffff
   0x0000000000000ffc <+138>:	jne    0xfb2 <main+64>

A grandes rasgos nos interesa la siguiente comparación:
0x0000000000000fb5 <+67>: cmp eax,0xa
Es decir, si el carácter leído es nueva línea \n (0xa) se ejecutará una función llamada print dicha función llamará a otra función f(), la cual imprime una falsa flag y a su vez llama a otra nueva función aaaa().
El desensamblado sería:

   0x000000000000086a <+0>:	push   rbp
   0x000000000000086b <+1>:	mov    rbp,rsp
   0x000000000000086e <+4>:	sub    rsp,0x20
   0x0000000000000872 <+8>:	movabs rax,0x6a74666730703973
   0x000000000000087c <+18>:	mov    QWORD PTR [rbp-0x20],rax
   0x0000000000000880 <+22>:	movabs rax,0x36757b6e31615f65
   0x000000000000088a <+32>:	mov    QWORD PTR [rbp-0x18],rax
   0x000000000000088e <+36>:	movabs rax,0x723338372d767769
   0x0000000000000898 <+46>:	mov    QWORD PTR [rbp-0x10],rax
   0x000000000000089c <+50>:	mov    DWORD PTR [rbp-0x8],0x567a7179
   0x00000000000008a3 <+57>:	mov    WORD PTR [rbp-0x4],0x6f78
   0x00000000000008a9 <+63>:	mov    BYTE PTR [rbp-0x2],0x0
   0x00000000000008ad <+67>:	lea    rax,[rbp-0x20]
   0x00000000000008b1 <+71>:	mov    rdi,rax
   0x00000000000008b4 <+74>:	mov    eax,0x0
   0x00000000000008b9 <+79>:	call   0x8c1 <m>
   0x00000000000008be <+84>:	nop
   0x00000000000008bf <+85>:	leave  
   0x00000000000008c0 <+86>:	ret    

Podemos observar que esta función ejecuta otra función m(), y le pasa como argumento s9p0gftje_a1n{u6iwv-783ryqzVxo.
Esta nueva función es bastante extensa, mostraremos una parte reducida:

   0x00000000000008cd <+12>:	movabs rax,0x7767737430636666
   0x00000000000008d7 <+22>:	mov    QWORD PTR [rbp-0x30],rax
   0x00000000000008db <+26>:	movabs rax,0x67727b6173745f72
   0x00000000000008e5 <+36>:	mov    QWORD PTR [rbp-0x28],rax
   0x00000000000008e9 <+40>:	movabs rax,0x763331375f69686e
   0x00000000000008f3 <+50>:	mov    QWORD PTR [rbp-0x20],rax
   0x00000000000008f7 <+54>:	mov    DWORD PTR [rbp-0x18],0x665f7265
   0x00000000000008fe <+61>:	mov    WORD PTR [rbp-0x14],0x616c
   0x0000000000000904 <+67>:	jmp    0xee9 <m+1576>
   0x0000000000000909 <+72>:	mov    rax,QWORD PTR [rbp-0x38]
   0x000000000000090d <+76>:	movzx  eax,BYTE PTR [rax]
   0x0000000000000910 <+79>:	movzx  eax,al
   0x0000000000000913 <+82>:	cmp    eax,DWORD PTR [rbp-0x4]
   0x0000000000000916 <+85>:	jne    0x937 <m+118>
   0x0000000000000918 <+87>:	movzx  eax,BYTE PTR [rbp-0x15]
   0x000000000000091c <+91>:	movzx  eax,al
   0x000000000000091f <+94>:	mov    esi,eax
   0x0000000000000921 <+96>:	lea    rdi,[rip+0x789]        # 0x10b1
   0x0000000000000928 <+103>:	mov    eax,0x0
   0x000000000000092d <+108>:	call   0x660 <printf@plt>

Para entender lo que sucede, llamemos A al conjunto de caracteres recibido como argumento y B al conjunto de caracteres que vamos a introducir en dicha función.
El algoritmo es sencillo, comprueba que cada carácter de B pertenece también a A, cada vez que se cumpla tal condición imprime un carácter de la flag.
Ejemplo:
A = { a, b, c}
B = { c, d }
La intersección de ambos conjuntos es c, por tanto obtendríamos un único carácter de la flag.
Para poder obtener la flag, la intersección de Ay B debe ser A (conjunto que ya conocemos).
La cuestión que nos abordaría en estos momentos es, ¿cual es el orden correcto para que imprima la flag valida? El orden en el que ha sido pasado el argumento a la función: s9p0gftje_a1n{u6iwv-783ryqzVxo.

Obteniendo la flag

Para obtener la flag utilizaremos el depurador y definiremos un hook.
Un hook en gdb son un tipo especial de comandos definidos por el usuario, definen lo que debe suceder justo después de que sean ejecutados. En nuestro caso el hook sorteará el mecanismo de protección.

define hook-stop
set $rip = main+21
continue
end
❯ gdb -q ./rev_fwr
Reading symbols from ./rev_fwr...(no debugging symbols found)...done.
(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>set $rip = main+21
>continue
>end
(gdb) b *main+16
Breakpoint 1 at 0xf82
(gdb) r
Starting program: /root/Desktop/rev_fwr 

||====================================================================||
||//$\\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\//$\\||
||(100)=================| LINUX BINARY LIFE STYLE|===============(100)||
||\\$//        ~         '------========--------'                \\$//||
||<< /        /$\              // ____ \\                         \ >>||
||>>|        //L\\            // ///..) \\              XXXX       |<<||
||<<|        \\ //           || <||  >\  ||                        |>>||
||>>|         \$/            ||  $$ --/  ||          fwr{XXXX}     |<<||
||<<|     Free to Use        *\\  |\_/  //*                        |>>||
||>>|                         *\\/___\_//*                         |<<||
||<<\      Reversing FWR _____/ @n4ivenom \____ __    XX XXXXX     />>||
||//$\                ~|   FOLLOW THE WHITE RABBIT   |~           /$\\||
||(100)===================  CARIÑO QUIERO LA FLAG ===============(100)||
||\\$//\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\\$//||
||====================================================================||
D O  Y O U  W A N T  P L A Y  A  G A M E  W I T H  M E?

66 77 72 7b 6e 6f 74 5f 72 65 61 6c 5f 66 6c 61 67 3a 28 7d 
>>>s9p0gftje_a1n{u6iwv-783ryqzVxo
66
77
72
7b
63
30
6e
67
72
61
74
73
5f
74
68
69
73
5f
72
65
76
33
31
37
5f
66
6c
61
67
0

Solo quedaría visualizarla:

In [1]: "6677727b63306e67726174735f746869735f7265763331375f666c6167".decode("hex")
Out[1]: 'fwr{c0ngrats_this_rev317_flag'

Vemos que le falta la última llave, por tanto la flag final sería:
fwr{c0ngrats_this_rev317_flag}
Y eso es todo amig@s!

 

Un comentario en «Write-up del reto Reversing: Linux Binary»

Los comentarios están cerrados.