Viendo la pila

14

Recientemente comencé a aprender sobre los desbordamientos de búfer y cómo funcionan. Alguien compartió un binario para practicar (en una máquina virtual, no te preocupes). He estado introduciendo cadenas en el zócalo que abre el binario, y noté que, con cierta longitud, la cadena hará que el programa no responda con el mensaje que se supone que debe hacer. Además, si alimento otra cadena de cierta longitud, partes del mensaje se envían de vuelta a través del socket desde el servidor, pero las otras partes simplemente se imprimen en la consola en el extremo del servidor. No estoy completamente seguro de qué causó esto (esta no es la pregunta oficial de esta publicación, pero me encantaría escuchar una respuesta en los comentarios).

Y eso me lleva a mi pregunta: ¿existen aplicaciones que puedan generar una imagen de la pila o volcarla y, en general, qué se escribe en ella? Creo que sería realmente útil para ver lo que está sucediendo cuando alimente las cadenas de conexión de diferentes longitudes. Me encantaría que el tamaño de cada 'sección' de la pila (no sé cómo se llama) estuviera representado en la imagen con un tamaño relativo a las otras secciones (por lo tanto, puedo visualizar el tamaño de la pila) , o de forma legible.

Algo como esto sería genial si tu respuesta es sobre la generación de la imagen, excepto que sería bueno si se mostrara cuánto se escribe (de esa manera puedo ver cuando se está desbordando) ...

Probablemente genere una imagen cuando inicie el programa, y después de alimentar el socket con los valores enormes. Entonces lo compararía. Si hay otras formas de aprender, me encantaría escucharlas.

  

Edición # 1: Estoy haciendo pruebas de caja negra.

     

Edit # 2: Mientras que ya hay un   respuesta aceptada a esta pregunta, agradecería otras respuestas   también. Cuanta más información y respuestas, más podré   aprender. Por lo tanto, recompensaré las nuevas respuestas (si se merecen) con recompensas. Lo aprecio!

    
pregunta Arin 04.10.2016 - 02:08
fuente

2 respuestas

10

Obtención de un volcado de memoria de forma sencilla

Ejemplo:

En la terminal # 1:

/tmp$ ./test 
idling...
idling...
Segmentation fault <---- HERE I SEND THE 1st SIGSEGV
/tmp$ ulimit -c unlimited
/tmp$ ./test 
idling...
idling...
Segmentation fault (core dumped) <---- HERE IS THE 2d SIGSEGV
/tmp$ ls test
test    test.c  
/tmp$ ls -lah core 
-rw------- 1 1000 1000 252K Oct 10 17:42 core

En la terminal # 2

/tmp$ ps aux|grep test
1000  6529  0.0  0.0   4080   644 pts/1    S+   17:42   0:00 ./test
1000  6538  0.0  0.0  12732  2108 pts/2    S+   17:42   0:00 grep test
/tmp$ kill -SEGV 6529
/tmp$ ps aux|grep test
1000  6539  0.0  0.0   4080   648 pts/1    S+   17:42   0:00 ./test
1000  6542  0.0  0.0  12732  2224 pts/2    S+   17:42   0:00 grep test
/tmp$ kill -SEGV 6539

Tenga en cuenta que esto le dará un vuelco de su estado en el momento en que el binario obtuvo el SIGSEGV. Entonces, si tu binario consiste en main () y evil_function () y, mientras recibes SIGSEV, tu programa estaba ejecutando evil_function (), obtendrás la pila de evil_function (). Pero también puede inspeccionar alrededor para volver a la pila principal ().

Buen indicador de todo lo que es el documento de Aleph One: enlace

Adivinando el "mapeo" por ti mismo

Si imaginamos que su binario está implementando un desbordamiento de búfer básico, como en este fragmento de código:

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


int evil_function(char *evil_input)
{
    char stack_buffer[10];
    strcpy(stack_buffer, evil_input);
    printf("input is: %s\n", stack_buffer);
    return 0;
}


int main (int ac, char **av)
{
    if (ac != 2) 
    {
        printf("Wrong parameter count.\nUsage: %s: <string>\n",av[0]);
        return EXIT_FAILURE;
    }
    evil_function(av[1]);

    return (EXIT_SUCCESS);
}

Es bastante simple adivinar dónde deberías escribir tu dirección de búfer simplemente usando gdb. Probemos con el programa de ejemplo anterior:

/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x10")
input is: AAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x11")
input is: AAAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x12")
input is: AAAAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x13")
input is: AAAAAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x14")
input is: AAAAAAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x15")
input is: AAAAAAAAAAAAAAA
/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x16")
input is: AAAAAAAAAAAAAAAA
Segmentation fault (core dumped)

Ok, entonces la pila comienza a ser jodida después de dar 6 caracteres extra ... Echemos un vistazo a la pila:

/tmp/bo-test$ gdb test-buffer-overflow core
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
[...]
Core was generated by './test-buffer-overflow AAAAAAAAAAAAAAAA'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x00007f2cb2c46508 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
(gdb) bt
#0  0x00007f2cb2c46508 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x0000000000000000 in ?? ()
(gdb) Quit

Continuemos alimentándolo con más caracteres adicionales:

/tmp/bo-test$ ./test-buffer-overflow $(perl -e "print 'A'x26")
input is: AAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)
/tmp/bo-test$ gdb test-buffer-overflow core
GNU gdb (Debian 7.7.1+dfsg-5) 7.7.1
[...]
Core was generated by './test-buffer-overflow AAAAAAAAAAAAAAAAAAAAAAAAAA'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000004141 in ?? ()
(gdb) 

Oye ... mira esta dirección: 0x0000000000004141 ! 0x41 es un código ASCIH hexagonal para ... 'A': p Acabamos de reescribir la dirección RET :) Ahora, último intento, solo para ver:

/tmp/bo-test$ ./test-buffer-overflow AAAAAAAAAAAAAAAAAAAAAAAAABCDEFGHI
input is: AAAAAAAAAAAAAAAAAAAAAAAAABCDEFGHI
Segmentation fault (core dumped)
/tmp/bo-test$ gdb test-buffer-overflow core GNU gdb 
Core was generated by './test-buffer-overflow AAAAAAAAAAAAAAAAAAAAAAAAABCDEFGHI'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000000000400581 in evil_function (
    evil_input=0x7fff7e2712a6 'A' <repeats 25 times>, "BCDEFGHI")
    at test-buffer-overflow.c:12
12  }
(gdb) bt
#0  0x0000000000400581 in evil_function (
    evil_input=0x7fff7e2712a6 'A' <repeats 25 times>, "BCDEFGHI")
    at test-buffer-overflow.c:12
#1  0x4847464544434241 in ?? ()
#2  0x00007fff7e260049 in ?? ()
#3  0x0000000200000000 in ?? ()
#4  0x0000000000000000 in ?? ()

Esta vez, mira la dirección otra vez: 0x4847464544434241 ... Ahora sabes exactamente dónde escribir ...

    
respondido por el binarym 10.10.2016 - 18:15
fuente
4

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:

  1. 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)

  1. 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.

    
respondido por el grochmal 10.10.2016 - 22:49
fuente

Lea otras preguntas en las etiquetas