viernes, 11 de mayo de 2007

El origen del mal - 0x02

El mal
Por lo que sabemos se puede sobrescribir el espacio de memoria de un programa con código binario. Pero ¿cómo se puede crear un código a medida meterlo en memoria y encima que realmente funcione? Ese código arbitrario que se ejecuta en los buffer overflows es un shellcode; unas instrucciones en binario con un objetivo muy concreto: crear un shell o interprete de comandos, abrir un puerto de comunicaciones, etc...

Antes de aprender a crear shellcodes, es muy necesario acostumbrarse al uso del depurador o debugger. Es más, habría que aprender a depurar antes que compilar.

[día 2 : el depurador]

Conociendo el depurador o debugger:

Supongamos que tenemos este programa:

void saludar (char *q)
{
char saludo[10] = "Hola ";
char quien[15] = " don ";

printf("%s, %s %s\n", saludo, quien, q);
}

int main(int argc, char * argv[])
{
int entero;
entero = 0;
saludar(argv[1]);
entero = 1;
printf("Ok, valor del entero %d\n", entero);

return 0;
}

Para poder depurarlo mejor, debemos compilarlo con el flag ggdb:

gcc -ggdb -o ejemplo ejemplo.c

Ahora ya podemos iniciar el depurador gdb:

gdb
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details. This GDB was configured as "i386-linux".
(gdb)

Podemos invocarlo sin más y para cargar el programa que queremos depurar haríamos algo así:

(gdb) file ejemplo
Reading symbols from /home/pello/b0f/dia2/ejemplo...done.
Using host libthread_db library "/lib/tls/libthread_db.so.1".
(gdb)

Y si no podríamos invocar el debugger directamente con el programa que queremos depurar:
gdb ejemplo
...
(gdb)

list
Bien. Con el comando list o l podemos echar un ojo al código fuente. Esto es posible gracias a que está compilado con -ggdb
Se le pueden añadir números para indicar las lineas que queremos ver:
list 1 : para ver a partir de la 1ª linea
list 3,6 : para ver de la linea 3 a la 6

(gdb) list
3 char saludo[10] = "Hola ";
4 char quien[15] = " don ";
5
6 printf("%s, %s %s\n", saludo, quien, q);
7 }
8
9 int main(int argc, char * argv[])
10 {
11 int entero;
12 entero = 0;

(gdb) list 1,5
1 void saludar (char *q)
2 {
3 char saludo[10] = "Hola ";
4 char quien[15] = " don ";
5
(gdb)

disassemble
Esta opción desensambla el binario y muestra las instrucciones de ensamblador que componen el programa. Se le indica el nombre de función, y puede ser la función main o principal

(gdb) disas main
Dump of assembler code for function main:
0x0804840c <main+0>: push %ebp
0x0804840d <main+1>: mov %esp,%ebp
0x0804840f <main+3>: sub $0x18,%esp
0x08048412 <main+6>: and $0xfffffff0,%esp
0x08048415 <main+9>: mov $0x0,%eax
0x0804841a <main+14>: sub %eax,%esp
0x0804841c <main+16>: movl $0x0,0xfffffffc(%ebp)
0x08048423 <main+23>: mov 0xc(%ebp),%eax
0x08048426 <main+26>: add $0x4,%eax
0x08048429 <main+29>: mov (%eax),%eax
0x0804842b <main+31>: mov %eax,(%esp)
0x0804842e <main+34>: call 0x80483a4 <saludar>
0x08048433 <main+39>: movl $0x1,0xfffffffc(%ebp)
0x0804843a <main+46>: mov 0xfffffffc(%ebp),%eax
0x0804843d <main+49>: mov %eax,0x4(%esp)
0x08048441 <main+53>: movl $0x8048588,(%esp)
0x08048448 <main+60>: call 0x80482b8 <_init+56>
0x0804844d <main+65>: mov $0x0,%eax
0x08048452 <main+70>: leave
0x08048453 <main+71>: ret
End of assembler dump.
(gdb)

En esta especie de batiburrillo se puede aprender a distinguir algunas cosas: los valores más a la izquierda son las direcciones de memoria en la que se encuentran las instrucciones primero en hexadecimal y luego en una notación relativa al inicio de main.

Si desensamblamos la función saludar:
(gdb) disas saludar
Dump of assembler code for function saludar:
0x080483a4 <saludar+0>: push %ebp
0x080483a5 <saludar+1>: mov %esp,%ebp
0x080483a7 <saludar+3>: push %edi
0x080483a8 <saludar+4>: sub $0x34,%esp
0x080483ab <saludar+7>: mov 0x8048564,%eax
0x080483b0 <saludar+12>: mov %eax,0xffffffe8(%ebp)
0x080483b3 <saludar+15>: movzwl 0x8048568,%eax
0x080483ba <saludar+22>: mov %ax,0xffffffec(%ebp)
0x080483be <saludar+26>: movl $0x0,0xffffffee(%ebp)

run
Para ejecutar el programa usamos la instrucción run o r. Se le pueden pasar parámetros como si estuvieramos en la linea de comandos.

(gdb) run Juan
Starting program: /home/64kbytes/b0f/dia2/ejemplo
Hola , don Juan
Ok, valor del entero 1

Program exited normally.
(gdb)

Pero claro, puede que nos interese ejecutar paso a paso. Para eso establecemos un punto de ruptura o break point

break
Con break o b establecemos dónde debe detenerse la ejecución para que se siga paso a paso. Podemos establecerlo por nombre de función o por linea:

break main : para depurarlo desde el principio
break 10 : para depurar desde la linea 10
(gdb) break main
Breakpoint 2 at 0x804841c: file ejemplo.c, line 12.
(gdb)

A partir de ahí podemos iniciar la ejecución paso a paso con distintas instrucciones:
step
Mediante step o s ejecutamos el programa linea a linea. En caso de pasarlo un parámetro numérico repetiría el código de la linea ese número de veces.

stepi o si e similar pero en ese caso solo se ejecuta una instrucción al margen de lo que haya en toda la linea.

Vamos a ejecutar paso a paso:

(gdb) break main
Note: breakpoints 2, 3 and 4 also set at pc 0x804841c.
Breakpoint 5 at 0x804841c: file ejemplo.c, line 12.
(gdb) run
Starting program: /home/pello/b0f/dia2/ejemplo Juan

Breakpoint 2, main (argc=2, argv=0xbffff9b4) at ejemplo.c:12
12 entero = 0;
(gdb) s
13 saludar(argv[1]);
(gdb) s

Breakpoint 1, saludar (q=0x2 <Address 0x2 out of bounds>) at ejemplo.c:2
2 {
(gdb) s
3 char saludo[10] = "Hola ";
(gdb)

...y seguiría.

Existen otras instrucciones similares a step.
next
Next y nexti tratan las llamadas a funciones como una instrucción¿ más sin meterse a depurar dentro de ellas.

kill
Podemos interrumpir la ejecución con la instrucción kill o k.
(gdb) k
Kill the program being debugged? (y or n) y
(gdb)

info registers
Este comando te da información sobre el estado de los registros. Muy útil en la depuración y en los overflows ya que son valores muy indicativos:

(gdb) info registers
eax 0x0 0
ecx 0x4003de6d 1073995373
edx 0x2 2
ebx 0x40156ff4 1075146740
esp 0xbffff920 0xbffff920
ebp 0xbffff938 0xbffff938
esi 0x0 0
edi 0x40015cc0 1073831104
eip 0x804841c 0x804841c
eflags 0x282 642
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb)

info frame
Con este comando podemos ver el contenido del frame actual de la pila. Muy útil para ver los vlaores del EIP por ejemplo:
(gdb) info frame
Stack level 0, frame at 0xbffff950:
eip = 0x804841e in main (pila.c:34); saved eip 0x4003dea8
source language c.
Arglist at 0xbffff948, args: argc=2, argv=0xbffff9c4
Locals at 0xbffff948, Previous frame's sp is 0xbffff950
Saved registers:
ebp at 0xbffff948, eip at 0xbffff94c
(gdb) n

info stack
Con info stack podemos ver las llamadas acumuladas en la pila.
(gdb) info stack
#0 0x080483f0 in cambiar (valor=4) at pila.c:21
#1 0x08048436 in main (argc=2, argv=0xbffff9c4) at pila.c:34
(gdb)

x: examinando la memoria
Para saber qué narices se guarda en memoria nada mejor que el comando x con sus muchas opciones.
El formato general del comando es:
x/[formato] dirección
Donde el [formato] puede ser, además de un número, o de octal, x de hexadecimal, d de decimal e u de decimal sin signo, y otros como t de binario, f de float, a para direcciones, i para instrucciones, c caracteres and s para cadenas. Y además se le añade el tamaño: b para bytes, h para medio word, w para word, g para 8 bytes.
Una combinación útil para examinar la memoria en bytes
(gdb) x/bx main
0x80483e6 <main>: 0x55
(gdb) [enter]
0x80483e7 <main+1>: 0x89
(gdb) [enter]
0x80483e8 <main+2>: 0xe5
(gdb)
Ahí vemos cada instrucción del programa: 0x55, 0x89, 0xe5 en hexadecimal.

Por ejemplo, para ver la parte de memoria a la que apunta el $esp
(gdb) x/100 $esp
0xbffffc50: "\fþÿ¿@_25@"
0xbffffc59: ""
0xbffffc5a: ""
0xbffffc5b: ""
0xbffffc5c: "@_25@@e01@"
0xbffffc65: ""
0xbffffc66: ""
0xbffffc67: ""
...
print
Para sacar los valores concretos de variables y registros podemos usar la función print o p, indicándole lo que queremos mostrar.

Por ejemplo el valor de un registro:
(gdb) print $ebx
$1 = 1075146740
(gdb)

O el valor de una variable
(gdb) print saludo
$3 = "Hola 00000000"
(gdb)

Y de momento es suficiente para poder empezar

1 comentario: