linux - solucion - ¿Desbordamiento de búfer de pila residente en 64 bits?
desbordamiento de pila (3)
Esas dos instrucciones están haciendo exactamente lo que espera que hagan. Ha sobrescrito el marco de pila anterior con 0x41
así que cuando leaveq
, está haciendo esto:
mov rsp, rbp
pop rpb
Ahora rsp
apunta a donde rbp
hizo antes. Sin embargo, ha sobrescrito esa región de la memoria, por lo que cuando hace pop rbp
, el hardware básicamente está haciendo esto
mov rbp, [rsp]
add rsp,1
Pero [rsp]
ahora tiene 0x41
''s. Entonces, esta es la razón por la que ves que rbp
se llena de ese valor.
En cuanto a por qué el rip
no se está configurando como se espera, es porque ret
está configurando el rip
a 0x41
y luego generando una excepción (error de página) en la búsqueda de instrucciones. No confiaría en GDB para mostrar lo correcto en este caso. Debería intentar sobreescribir el valor de retorno con una dirección válida dentro del segmento de texto del programa y es probable que no vea este comportamiento extraño.
Estoy estudiando algunas cosas relacionadas con la seguridad y ahora mismo estoy jugando con mi propia pila. Lo que estoy haciendo debe ser muy trivial, ni siquiera estoy intentando ejecutar la pila, simplemente para mostrar que puedo controlar el puntero de la instrucción en mi sistema de 64 bits. He desactivado todos los mecanismos de protección de los que soy consciente para poder jugar con ellos (NX-bit, ASLR, también compilando con -fno-stack-protector -z execstack). No tengo mucha experiencia con el ensamblaje de 64 bits y después de pasar algún tiempo buscando y experimentando, me pregunto si alguien podría arrojar algo de luz sobre un problema que estoy experimentando.
Tengo un programa (código fuente a continuación) que simplemente copia una cadena en un buffer residente de la pila sin verificación de límites. Sin embargo, cuando sobrescribo con una serie de 0x41, espero ver el RIP configurado en 0x4141414141414141, en cambio, descubro que mi RBP se establece en este valor. Me da un error de segmentación, pero RIP no se actualiza a este valor (ilegal) en la ejecución de la instrucción RET, incluso si RSP se establece en un valor legal. Incluso he verificado en GDB que hay memoria que se puede leer que contiene una serie de 0x41 en RSP inmediatamente antes de la instrucción RET.
Tenía la impresión de que la instrucción LEAVE:
MOV (E) SP, (E) BP
POP (E) BP
Sin embargo, en 64 bits, la instrucción "LEAVEQ" parece hacer (similar a):
MOV RBP, QWORD PTR [RSP]
Estoy pensando que hace esto simplemente por observar el contenido de todos los registros antes y después de la ejecución de esta instrucción. Sin embargo, LEAVEQ parece ser solo un nombre dependiente del contexto de la instrucción RET (que el desensamblador de GDB le da), ya que todavía es solo un 0xC9.
Y la instrucción RET parece hacer algo con el registro RBP, quizás desreferenciando? Tenía la impresión de que RET hizo (similar a):
MOV RIP, QWORD PTR [RSP]
Sin embargo, como mencioné, parece desreferenciar RBP, estoy pensando que lo hace porque tengo un error de segmentación cuando ningún otro registro parece contener un valor ilegal.
Código fuente para el programa:
#include <stdio.h>
#include <string.h>
int vuln_function(int argc,char *argv[])
{
char buffer[512];
for(int i = 0; i < 512; i++) {
buffer[i] = 0x42;
}
printf("The buffer is at %p/n",buffer);
if(argc > 1) {
strcpy(buffer,argv[1]);
}
return 0;
}
int main(int argc,char *argv[])
{
vuln_function(argc,argv);
return 0;
}
El bucle for está ahí para llenar la parte legal del búfer con 0x42, lo que hace que sea fácil ver en el depurador donde está antes del desbordamiento.
El extracto de la sesión de depuración sigue:
(gdb) disas vulnerable
Dump of assembler code for function vulnerable:
0x000000000040056c <+0>: push rbp
0x000000000040056d <+1>: mov rbp,rsp
0x0000000000400570 <+4>: sub rsp,0x220
0x0000000000400577 <+11>: mov DWORD PTR [rbp-0x214],edi
0x000000000040057d <+17>: mov QWORD PTR [rbp-0x220],rsi
0x0000000000400584 <+24>: mov DWORD PTR [rbp-0x4],0x0
0x000000000040058b <+31>: jmp 0x40059e <vulnerable+50>
0x000000000040058d <+33>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000400590 <+36>: cdqe
0x0000000000400592 <+38>: mov BYTE PTR [rbp+rax*1-0x210],0x42
0x000000000040059a <+46>: add DWORD PTR [rbp-0x4],0x1
0x000000000040059e <+50>: cmp DWORD PTR [rbp-0x4],0x1ff
0x00000000004005a5 <+57>: jle 0x40058d <vulnerable+33>
0x00000000004005a7 <+59>: lea rax,[rbp-0x210]
0x00000000004005ae <+66>: mov rsi,rax
0x00000000004005b1 <+69>: mov edi,0x40070c
0x00000000004005b6 <+74>: mov eax,0x0
0x00000000004005bb <+79>: call 0x4003d8 <printf@plt>
0x00000000004005c0 <+84>: cmp DWORD PTR [rbp-0x214],0x1
0x00000000004005c7 <+91>: jle 0x4005e9 <vulnerable+125>
0x00000000004005c9 <+93>: mov rax,QWORD PTR [rbp-0x220]
0x00000000004005d0 <+100>: add rax,0x8
0x00000000004005d4 <+104>: mov rdx,QWORD PTR [rax]
0x00000000004005d7 <+107>: lea rax,[rbp-0x210]
0x00000000004005de <+114>: mov rsi,rdx
0x00000000004005e1 <+117>: mov rdi,rax
0x00000000004005e4 <+120>: call 0x4003f8 <strcpy@plt>
0x00000000004005e9 <+125>: mov eax,0x0
0x00000000004005ee <+130>: leave
0x00000000004005ef <+131>: ret
Rompo justo antes de la llamada a strcpy (), pero después de que el búfer se haya llenado con 0x42.
(gdb) break *0x00000000004005e1
El programa se ejecuta con 650 0x41 como argumento, esto debería ser suficiente para sobrescribir la dirección de retorno en la pila.
(gdb) run `perl -e ''print "A"x650''`
Busco en la memoria la dirección de retorno 0x00400610 (que encontré al mirar el desmontaje de main).
(gdb) find $rsp, +1024, 0x00400610
0x7fffffffda98
1 pattern found.
Examino la memoria con x / 200x y obtengo una buena visión general que he omitido aquí debido a su tamaño, pero puedo ver claramente el 0x42 que denota el tamaño legal del buffer y la dirección de retorno.
0x7fffffffda90: 0xffffdab0 0x00007fff 0x00400610 0x00000000
Nuevo punto de interrupción justo después de strcpy ():
(gdb) break *0x00000000004005e9
(gdb) set disassemble-next-line on
(gdb) si
19 }
=> 0x00000000004005ee <vulnerable+130>: c9 leave
0x00000000004005ef <vulnerable+131>: c3 ret
(gdb) i r
rax 0x0 0
rbx 0x0 0
rcx 0x4141414141414141 4702111234474983745
rdx 0x414141 4276545
rsi 0x7fffffffe17a 140737488347514
rdi 0x7fffffffdb00 140737488345856
rbp 0x7fffffffda90 0x7fffffffda90
rsp 0x7fffffffd870 0x7fffffffd870
r8 0x1 1
r9 0x270 624
r10 0x6 6
r11 0x7ffff7b9fff0 140737349550064
r12 0x400410 4195344
r13 0x7fffffffdb90 140737488346000
r14 0x0 0
r15 0x0 0
rip 0x4005ee 0x4005ee <vulnerable+130>
0x00000000004005ee <vulnerable+130>: c9 leave
=> 0x00000000004005ef <vulnerable+131>: c3 ret
(gdb) i r
rax 0x0 0
rbx 0x0 0
rcx 0x4141414141414141 4702111234474983745
rdx 0x414141 4276545
rsi 0x7fffffffe17a 140737488347514
rdi 0x7fffffffdb00 140737488345856
rbp 0x4141414141414141 0x4141414141414141
rsp 0x7fffffffda98 0x7fffffffda98
r8 0x1 1
r9 0x270 624
r10 0x6 6
r11 0x7ffff7b9fff0 140737349550064
r12 0x400410 4195344
r13 0x7fffffffdb90 140737488346000
r14 0x0 0
r15 0x0 0
rip 0x4005ef 0x4005ef <vulnerable+131>
(gdb) si
Program received signal SIGSEGV, Segmentation fault.
0x00000000004005ee <vulnerable+130>: c9 leave
=> 0x00000000004005ef <vulnerable+131>: c3 ret
(gdb) i r
rax 0x0 0
rbx 0x0 0
rcx 0x4141414141414141 4702111234474983745
rdx 0x414141 4276545
rsi 0x7fffffffe17a 140737488347514
rdi 0x7fffffffdb00 140737488345856
rbp 0x4141414141414141 0x4141414141414141
rsp 0x7fffffffda98 0x7fffffffda98
r8 0x1 1
r9 0x270 624
r10 0x6 6
r11 0x7ffff7b9fff0 140737349550064
r12 0x400410 4195344
r13 0x7fffffffdb90 140737488346000
r14 0x0 0
r15 0x0 0
rip 0x4005ef 0x4005ef <vulnerable+131>
Verifico que la dirección del remitente ha sido sobrescrita y debería haber esperado ver que el RIP se establezca en esta dirección:
(gdb) x/4x 0x7fffffffda90
0x7fffffffda90: 0x41414141 0x41414141 0x41414141 0x41414141
(gdb) x/4x $rsp
0x7fffffffda98: 0x41414141 0x41414141 0x41414141 0x41414141
Sin embargo, RIP es claramente:
rip 0x4005ef 0x4005ef <vulnerable+131>
¿Por qué RIP no se actualizó como esperaba? ¿Qué es lo que LEAVEQ y RETQ realmente hacen en 64 bits? En resumen, ¿qué me falta aquí? He intentado omitir los argumentos del compilador al compilar solo para ver si hace alguna diferencia, pero no parece haber ninguna diferencia.
El motivo por el que se produce un bloqueo de EIP 0 × 41414141 en x32 es porque cuando el programa recupera el valor de EIP previamente guardado de la pila y vuelve a EIP, la CPU intenta ejecutar la instrucción en la dirección de memoria 0 × 41414141, lo que provoca una segfault. (debe buscar la página antes de la ejecución del curso)
Ahora, durante la ejecución de x64, cuando el programa recupera el valor RIP previamente guardado en el registro RIP, el kernel intenta ejecutar las instrucciones en la dirección de memoria 0 × 4141414141414141. En primer lugar, debido al direccionamiento de forma canónica, los bits 48 a 63 de cualquier dirección virtual deben ser copias del bit 47 (de una manera similar a la extensión de signo), o el procesador generará una excepción. Si eso no fuera un problema, el kernel realiza comprobaciones adicionales antes de invocar al controlador de fallas de página, ya que la dirección máxima de espacio de usuario es 0x00007FFFFFFFFFF.
Para recapitular, en la arquitectura x32, la dirección se pasa sin ninguna "validación" al controlador de fallas de página que intenta cargar la página, lo que hace que el kernel envíe el programa por defecto, pero x64 no llega tan lejos.
Pruébelo, sobrescriba RIP con 0 × 0000414141414141 y verá que el valor esperado se coloca en RIP ya que las precomprobaciones por el kernel pasan y luego se invoca al controlador de fallas de página como el caso x32 (que por supuesto, hace que el programa choque).
Las respuestas dadas por "kch" e "import os.boom.headshot" no son del todo correctas.
Lo que está sucediendo en realidad es que el valor en la pila (0x4141414141414141) que debe ser insertado en RIP por la instrucción RET contiene una dirección que está en el rango de direcciones "no canónicas" del procesador. Esto hace que la CPU genere una interrupción de falla de protección general (GPF) en lugar de una falla generada por una verificación previa del kernel. El GPF a su vez activa el kernel para reportar un error de segmentación antes de que RIP se actualice realmente y eso es lo que está viendo en GDB.
La mayoría de las CPU modernas solo proporcionan un rango de direcciones de 48 bits que se divide entre una mitad superior y una inferior que ocupan los rangos de direcciones de 0x0000000000000000 a 0x00007FFFFFFFFFFF y 0xFFFF800000000 a 0xFFFFFFFFFFFFFFFF respectivamente. Vea este enlace de Wikipedia para más información.
Si la dirección hubiera estado fuera del rango no canónico (0x00008FFFFFFFFFFF a 0xFFFF7FFFFFFFFFFF), entonces el RIP se habría actualizado como se esperaba. Por supuesto, el núcleo puede haber generado una falla posterior si la nueva dirección no es válida por cualquier otro motivo (es decir, fuera del rango de direcciones del proceso).