La respuesta de @ binarym es bastante buena. Él ya explica las razones detrás de un desbordamiento de búfer, cómo puedes encontrar un desbordamiento simple y cómo podemos mirar la pila usando un archivo de núcleo y / o GDB. Solo quiero agregar dos detalles adicionales:
- Un ejemplo de prueba de caja negra más profundo, es decir, esto:
una descripción de cómo detectar constantemente los desbordamientos de búfer (pruebas de caja negra)
- Las peculiaridades del compilador, es decir, donde falla la prueba de caja negra (más o menos, es más parecido a donde puede fallar una carga útil generada de caja negra).
El código que usaremos es un poco más complejo:
#include <stdio.h>
#include <string.h>
void do_post(void)
{
char curr = 0, message[128] = {};
int i = 0;
while (EOF != (curr = getchar())) {
if ('\n' == curr) {
message[i] = 0;
break;
} else {
message[i] = curr;
}
i++;
}
printf("I got your message, it is: %s\n", message);
return;
}
int main(void)
{
char curr = 0, request[8] = {};
int i = 0;
while (EOF != (curr = getchar())) {
request[i] = curr;
if (!strcmp(request, "GET\n")) {
printf("It's a GET!\n");
return 0;
} else if (!strcmp(request, "POST\n")) {
printf("It's a POST, get the message\n");
do_post();
return 0;
} else if (5 < strlen(request)) {
printf("Some rubbish\n");
return 1;
} /* else keep reading */
i++;
}
printf("Assertion error, THIS IS A BUG please report it\n");
return 0;
}
Me estoy burlando de HTTP con las solicitudes POST y GET. Y estoy usando getchar()
para leer STDIN carácter por carácter (es una implementación deficiente pero es educativa). El código diferenciará entre GET, POST y "basura" (lo que sea) y lo hace utilizando un bucle más o menos escrito correctamente (sin desbordamientos).
Sin embargo, al analizar el mensaje POST hay un desbordamiento, en el búfer message[128]
. Desafortunadamente, ese búfer está dentro del programa (bueno, no realmente tan profundo, pero un argumento largo y simple no lo encontrará). Vamos a compilarlo e intentar cadenas largas:
[~]$ gcc -O2 -o over over.c
[~]$ perl -e 'print "A"x2000' | ./over
Some rubbish
Sí, eso no funciona. Como conocemos el código, sabemos que si agregamos "POST \ n" al principio, activaremos el desbordamiento. Pero ¿y si no conocemos el código? ¿O es que el código es demasiado complejo? Entra en la prueba de caja negra.
Pruebas de caja negra
La técnica de prueba de caja negra más popular es fuzzing. Casi todas las otras técnicas (caja negra) son una variación de la misma. Fuzzing es simplemente alimentar el programa al azar hasta que encontremos algo interesante. Escribí un simple script de fuzzing para verificar este programa, echémosle un vistazo:
#!/usr/bin/env python3
from itertools import product
from subprocess import Popen, PIPE, DEVNULL
prog = './over'
valid_returns = [ 0, 1 ]
all_chars = list(map(chr, range(256)))
# This assumes that we may find something with an input as small as 1024 bytes,
# which isn't realistic. In the real world several megabytes of need to be
# tried.
for input_size in range(1,1024):
input = [p for p in product(all_chars, repeat=input_size)]
for single_input in input:
child = Popen(prog, stdin=PIPE, stdout=DEVNULL)
byte_input = (''.join(single_input)).encode("utf-8")
child.communicate(input=byte_input)
child.stdin.close()
ret = child.wait()
if not ret in valid_returns:
print("INPUT", repr(byte_input), "RETURN", ret)
exit(0)
# The exit(0) is not realistic either, in the real world I'd like to have a
# full log of the entire search space.
Simplemente hace eso: alimenta una entrada aleatoria cada vez más grande al programa. (ADVERTENCIA: el script requiere una gran cantidad de RAM) Ejecuto esto y después de unas horas obtengo una salida interesante:
INPUT b"POST\nXl_/.\xc3\x93\xc3\x90\xc2\x87\xc3\xa6dh\xc3\xaeH\xc2\xa0\xc2\x836\x16.\xc3\xb7\x1be\x1e,\xc3\x98\xc3\xa4\xc2\x81\xc2\x83 su\xc2\xb1\xc3\xb2\xc3\x8d^\xc2\xbc\xc2\xa11/\xc2\x9f\x12vY\x12[0\x0c]\xc3\xb6\x19zI\xc2\xb8\xc2\xb5\xc3\xbb\xc2\x9e\xc3\xab>^\xc2\x85\xc2\x91\xc2\xb5\xc2\xb5\xc3\xb6u\xc3\x8e).\xc3\xbcn\x1aM\xc3\xbb+{\x1c\xc3\x9a\xc3\x8b&\xc2\x93\xc2\xa1D\xc3\xad\xc3\xad\xc3\x81\xc2\xbd\xc2\x8d\xc2\xa3 \xc3\x87_\xc2\x82\xc3\x9asv\xc3\x92\xc2\x85IP\xc2\xb8\x1bS\xc3\xbe\xc3\x9e\\xc2\x8e\xc3\x9f\xc2\xb1\xc3\xa4\xc2\xbe\x1fue\xc3\x81\xc3\x8a\xc2\x8b'\xc3\xaf\xc2\xa1\xc3\x95'\xc2\xaa\xc3\xa8P\xc2\xa7\xc2\x8f\xc3\x99\xc2\x94S5\xc2\x83\xc3\x85U" RETURN -11
El proceso salió -11, ¿es un error de seguridad? Veamos:
kill -l | grep SIGSEGV
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
Es una falla de segmentación, está bien (vea esta respuesta para aclaración ). Ahora tengo una muestra de entrada que puedo usar para simular este segfault y descubrir (con GDB) dónde está el desbordamiento.
peculiaridades del compilador
¿Viste algo extraño arriba? Hay una información que omití, usé una etiqueta de spoiler a continuación para que pueda regresar y tratar de averiguar. La respuesta está aquí:
¿Por qué diablos usé gcc -O2 -o over over.c
? ¿Por qué un simple gcc -o over over.c
no es suficiente? ¿Qué tiene de especial la optimización del compilador ( -O2
) en este contexto?
Para ser justos, a mí mismo me pareció sorprendente que pudiera encontrar este comportamiento en un programa tan simple. Los compiladores reescriben una buena cantidad de código durante la compilación, por razones de rendimiento. Los compiladores también intentan mitigar varios riesgos (por ejemplo, desbordamientos claramente visibles). A menudo, el mismo código puede parecer muy diferente con y sin la optimización habilitada.
Echemos un vistazo a esta peculiaridad específica, pero volvamos a Perl ya que ya sabemos la vulnerabilidad:
[~]$ gcc -O2 -o over over.c
[~]$ perl -e 'print "POST\n" . "A"x2000' | ./over
It's a POST, get the message
I got your message, it is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAins
Segmentation fault (core dumped)
Sí, eso es exactamente lo que esperábamos. Pero ahora, vamos a desactivar la optimización:
[~]$ gcc -o over over.c
[~]$ perl -e 'print "POST\n" . "A"x2000' | ./over
It's a POST, get the message
I got your message, it is: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAÿ}
$ echo $?
0
¡Qué demonios! El compilador logró parchear la vulnerabilidad que creé con tanto amor. Si observa la longitud de ese mensaje, verá que tiene una longitud de 141 bytes. El búfer se desbordó, pero el compilador agregó algún tipo de ensamblaje para detener las escrituras en caso de que el desbordamiento llegue a algo importante.
Para los escépticos, aquí está la versión del compilador que estoy usando para obtener el comportamiento anterior:
[~]$ gcc --version
gcc (GCC) 6.2.1 20160830
Copyright (C) 2016 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.
La moraleja de la historia es que la mayoría de las vulnerabilidades de desbordamiento de búfer solo funcionan con la misma carga útil si son compiladas por el mismo compilador y con la misma optimización (o incluso otros parámetros). Los compiladores hacen cosas malvadas con su código para hacer que se ejecute más rápido, y aunque hay muchas posibilidades de que una carga útil funcione en el mismo programa compilado por dos compiladores, no siempre es cierto.
Postscript
Hice esta respuesta por diversión y para mantener un registro para mí. No merezco la recompensa porque no respondo completamente a su pregunta, solo respondo la pregunta adicional agregada en la definición de recompensa. La respuesta de Bynarym merece la recompensa porque responde más partes de la pregunta original.