Visualizzazione dello stack

14

Recentemente ho iniziato a conoscere buffer overflow e come funzionano. Qualcuno ha condiviso un binario per esercitarsi (in un VM, non preoccuparti). Ho alimentato stringhe alla presa che il binario si apre e ho notato che ad una certa lunghezza, la stringa farà sì che il programma non risponda con il messaggio che si suppone. Inoltre, se inserisco un'altra stringa di determinate lunghezze, le parti del messaggio vengono inviate indietro attraverso il socket dal server, ma le altre parti vengono stampate sulla console sul lato server. Non sono del tutto sicuro di cosa abbia causato questo (questa non è la domanda ufficiale per questo post, ma mi piacerebbe sentire una risposta nei commenti).

E questo mi porta alla mia domanda: ci sono delle applicazioni che possono generare un'immagine dello stack o scaricarlo e in generale cosa viene scritto su di esso? Penso che sarebbe davvero utile per vedere cosa sta accadendo quando alimento le stringhe di socket di diverse lunghezze. Mi piacerebbe molto se la dimensione di ogni 'sezione' della pila (non so come si chiama) fosse rappresentata nell'immagine con una dimensione relativa alle altre sezioni (quindi, posso visualizzare la dimensione della pila) , o in modo leggibile.

Qualcosa di simile sarebbe bello se la tua risposta riguardasse la generazione dell'immagine, eccetto che sarebbe bello se mostrasse quanto è scritto (in questo modo posso vedere quando sta straripando) ...

Probabilmente genererei un'immagine quando avvierò il programma e dopo che avrò alimentato il socket i valori enormi. Quindi mi confronto. Se ci sono altri modi / modi migliori di apprendimento, mi piacerebbe ascoltarli.

Edit #1: I'm black-box testing.

Edit #2: While there already is an accepted answer to this question, I would appreciate other answers too. The more information, and responses, the more I will be able to learn. Thus, I will reward new answers (if deserved) with bounties. Appreciate it!

    
posta Arin 04.10.2016 - 02:08
fonte

2 risposte

10

Ottenere un dump della memoria nel modo più semplice

Puoi semplicemente inviare il tuo processo vulnerabile a SIGSEGV (kill -SEGV pid) e, se è consentito il coredump (ulimit -c unlimited), otterrai un bel file di dump di base con tutta la tua memoria.

Esempio:

Sul terminale # 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

Sul terminale # 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

Si noti che questo vi darà una discarica del vostro stato nel momento in cui il file binario ha ottenuto SIGSEGV. Quindi, se il tuo binario è composto da main () e evil_function () e, durante la ricezione di SIGSEV, il tuo programma stava eseguendo evil_function (), otterrai lo stack di evil_function (). Ma puoi anche ispezionare per tornare allo stack main ().

Buon puntatore su tutto ciò che è Aleph One paper: link

Indovinare la "mappatura" autonomamente

Se immaginiamo che il tuo binario stia implementando un buffer overflow di base, come in questo frammento di codice:

#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);
}

È abbastanza semplice indovinare dove dovresti scrivere il tuo indirizzo di buffer semplicemente usando gdb. Proviamo con il programma di esempio sopra:

/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, quindi lo stack inizia a essere fottuto dopo aver dato 6 caratteri extra ... Diamo un'occhiata allo stack:

/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

Continuiamo ad alimentarlo con un extra char:

/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) 

Ehi ... guarda questo indirizzo: 0x0000000000004141 ! 0x41 è il codice hex ascii per ... 'A': p Abbiamo appena riscritto l'indirizzo RET :) Ora, ultimo tentativo, solo per vedere:

/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 ?? ()

Questa volta, guarda di nuovo l'indirizzo: 0x4847464544434241 ... Ora sai esattamente dove scrivere ...

    
risposta data 10.10.2016 - 18:15
fonte
4

@ La risposta di binarym è abbastanza buona. Spiega già le ragioni di un overflow del buffer, come puoi trovare un semplice overflow e come possiamo guardare allo stack usando un core file e / o GDB. Voglio solo aggiungere due dettagli extra:

  1. Un esempio di test della scatola nera più approfondito, cioè:

a description of how to consistently detect buffer overflows (black-box testing)

  1. Il quirk del compilatore, ovvero quando il testing della black-box fallisce (più o meno, è più come se un payload generato dalla black box potrebbe fallire).

Il codice che useremo è un po 'più complesso:

#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;
}

Mi sto prendendo gioco di HTTP con le richieste POST e GET. E sto usando getchar() per leggere STDIN carattere per carattere (che è una cattiva implementazione ma è educativo). Il codice distingue tra GET, POST e "spazzatura" (qualsiasi altra cosa), e lo fa usando un ciclo più o meno correttamente scritto (senza overflow).

Tuttavia, durante l'analisi del messaggio POST c'è un overflow, nel buffer message[128] . Sfortunatamente quel buffer è in profondità all'interno del programma (beh, non proprio così profondo ma una semplice lunga discussione non lo troverà). Facciamo la compilazione e proviamo le stringhe lunghe:

[~]$ gcc -O2 -o over over.c
[~]$ perl -e 'print "A"x2000' | ./over 
Some rubbish

Sì, non funziona. Poiché conosciamo il codice, sappiamo che se aggiungiamo "POST \ n" all'inizio causeremo l'overflow. Ma cosa succede se non conosciamo il codice? O il codice è troppo complesso? Entra nel test black-box.

Test della scatola nera

La tecnica di collaudo della scatola nera più popolare è la fuzzing. Quasi tutte le altre tecniche (scatola nera) sono una variante di esso. Il fuzzing sta semplicemente alimentando l'input casuale del programma finché non troviamo qualcosa di interessante. Ho scritto un semplice script fuzzing per controllare questo programma, diamo un'occhiata:

#!/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.

Semplicemente: alimenta input casuali sempre più grandi al programma. (ATTENZIONE: lo script richiede una buona quantità di RAM) Eseguo questo e dopo alcune ore ottengo un risultato interessante:

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

Il processo è scaduto -11, è un segfault? Vediamo:

kill -l | grep SIGSEGV
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM

Va bene un errore di segmentazione (vedi questa risposta per chiarimento ). Ora ho un campione di input che posso usare per simulare questo segfault e scoprire (con GDB) dove è l'overflow.

I quirk del compilatore

Hai visto qualcosa di strano sopra? C'è un pezzo di informazione che ho omesso, ho usato un tag spoiler qui sotto in modo da poter tornare indietro e provare a capire. La risposta è qui:

Why the hell I used gcc -O2 -o over over.c? Why a plain gcc -o over over.c is not enough? What is so special about compiler optimisation (-O2) in this context?

Per essere onesti, io stesso ho trovato sorprendente il fatto che ho potuto trovare questo comportamento in un programma così semplice. I compilatori riscrivono una buona parte del codice durante la compilazione, per motivi di prestazioni. Anche i compilatori cercano di attenuare diversi rischi (ad esempio, overflow chiaramente visibili). Spesso lo stesso codice può sembrare molto diverso con e senza l'ottimizzazione abilitata.

Diamo un'occhiata a questo specifico quirk, ma torniamo a perl dato che conosciamo già la vulnerabilità:

[~]$ 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ì, è esattamente quello che ci aspettavamo. Ma ora disabilitiamo l'ottimizzazione:

[~]$ 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

Che diavolo! Il compilatore è riuscito a correggere la vulnerabilità che ho creato con così tanto amore. Se osservi la lunghezza di quel messaggio, vedrai che è lunga 141 byte. Il buffer ha avuto un overflow, ma il compilatore ha aggiunto una sorta di assembly per fermare le scritture nel caso in cui l'overflow raggiungesse qualcosa di importante.

Per gli scettici, ecco la versione del compilatore che sto usando per ottenere il comportamento sopra:

[~]$ 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 morale della storia è che la maggior parte delle vulnerabilità di buffer overflow funzionano solo con lo stesso carico utile se compilate dallo stesso compilatore e con la stessa ottimizzazione (o anche altri parametri). I compilatori fanno cose cattive al tuo codice per farlo funzionare più velocemente, e anche se ci sono buone probabilità che un payload funzioni sullo stesso programma compilato da due compilatori, non è sempre vero.

Postscript

Ho fatto questa risposta per divertirmi e per tenere un registro per me stesso. Non merito la ricompensa perché non rispondo completamente alla tua domanda, rispondo solo alla domanda aggiuntiva aggiunta nella definizione di generosità. La risposta di bynarym merita la ricompensa perché risponde a più parti della domanda originale.

    
risposta data 10.10.2016 - 22:49
fonte