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 A
y 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»
Joder. Bravísimo. Lo seguiré paso a paso para ir tomando apuntes.
Los comentarios están cerrados.