«Техника сетевых атак»

Вид материалаКраткое содержание

Содержание


Рисунок 72 Реакция системы на переполнение буфера
Подобный материал:
1   ...   40   41   42   43   44   45   46   47   ...   51

Рисунок 72 Реакция системы на переполнение буфера



Допустим, в программе присутствует некая функция (условно названная “root”), которая выполняет действия, необходимые злоумышленнику. Может ли он специально подобранной строкой изменить адрес возврата таким образом, чтобы вместо сообщения о неправильно набранном пароле, управление передавалось на эту функцию?

Для ответа на такой вопрос необходимо знать по какому адресу расположена интересующая злоумышленника функция, и какой именно байт из введенной строки затирает адрес возврата. Выяснить это можно с помощью дизассемблирования кода программы.

Дизассемблирование – процесс сложный и требующий от исследователя хороших знаний ассемблера, архитектуры операционной системы и техники компиляции кода. Без этого разобраться с алгоритмом работы программы практически невозможно. К сожалению, практически не существует литературы, посвященной дизассемблированию, поэтому, в большинстве случаев приходится осваивать эту тему самостоятельно308.

Все, сказанное ниже, рассчитано на читателя средней квалификации, как минимум знающего назначение наиболее употребляемых команд микропроцессора Intel 80x86. В качестве дизассемблера выбрана IDA PRO четвертой версии309, однако, можно воспользоваться и другими инструментами, такими как SOURCER, W32Dasm или на худой конец DumpBin, который поставляется с любым Windows компилятором.

Результат дизассемблирования buff.demo.exe показан ниже (на диске, прилагаемом к книге, он расположен в файле “/LOG/buff.demo.lst”). Исследователь должен изучить «устройство» функции Auth, (как ее найти во много килобайтовом листинге – тема отдельного разговора). Для облегчения понимания, листинг снабжен подробными комментариями.

  • .text:00401000 ; Segment type: Pure code
  • .text:00401000 _text segment para public 'CODE' use32
  • .text:00401000 assume cs:_text
  • .text:00401000 ;org 401000h
  • .text:00401000 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
  • .text:00401000 Root proc near
  • .text:00401000 ; Функции root расположена по адресу 0x401000
  • .text:00401000 push ebp
  • .text:00401000 ; ... назначение процедуры root значение не имеет
  • .text:00401000 ; ... для ее вызова достаточно знать по какому адресу она расположена в памяти
  • .text:00401000 ;... а расположена она по адресу 0x401000
  • .text:00401001 mov ebp, esp
  • .text:00401003 push offset aHelloRoot ; "Hello, Root!\n"
  • .text:00401008 call _printf
  • .text:0040100D add esp, 4
  • .text:00401010 pop ebp
  • .text:00401011 retn
  • .text:00401011 Root endp



  • .text:00401012
  • .text:00401012 ; ███████████████ S U B R O U T I N E ███████████████████████████████████████
  • .text:00401012
  • .text:00401012 ; Attributes: bp-based frame
  • .text:00401012
  • .text:00401012 auth proc near ; CODE XREF: main+10p
  • .text:00401012
  • .text:00401012 var_18 = byte ptr -18h
  • .text:00401012 var_C = byte ptr -0Ch
  • .text:00401012 ; Так IDA обозначает локальные переменные, а цифры указывают относительное
  • .text:00401012 ; расположение от конца кадра стека.
  • .text:00401012 ; В Момент вызова функции указатель стека указывает на адрес возврата
  • .text:00401012 push ebp
  • .text:00401012 ; В стек заносится регистр ebp, значение указателя стека уменьшается на 4
  • .text:00401013 mov ebp, esp
  • .text:00401013 ; Открывается кадр стека:
  • .text:00401013 ; В регистр ebp заносится значение регистра указателя стека esp.
  • .text:00401013 ; Регистр ebp будет использоваться для адресации локальных переменных относительно конца кадра стека
  • .text:00401015 sub esp, 18h
  • .text:00401015 ; Резервируется 0x18 (24 в десятичной нотации) байт под локальные переменные
  • .text:00401015 ; Но размер двух буферов равен 10+10=20 байтам! Откуда взялись четрые лишние байта?
  • .text:00401015 ; Для ускорения доступа к данным компилятор размещает начала каждого из буферов по адресам, кратным
  • .text:00401015 ; четырем байтам, так называемое выравнивание.
  • .text:00401015 ; Таким образом на данный момент стек выглядит так:
  • .text:00401015 ;
  • .text:00401015 ; Относительный адрес Содержимое ячейки
  • .text:00401015 ; - 0x18 буфер var_18[0]
  • .text:00401015 ; - 0x17 буфер var_18[1]
  • .text:00401015 ; - 0x16 буфер var_18[2]
  • .text:00401015 ; - 0x15 буфер var_18[3]
  • .text:00401015 ; - 0x14 буфер var_18[4]
  • .text:00401015 ; - 0x13 буфер var_18[5]
  • .text:00401015 ; - 0x12 буфер var_18[6]
  • .text:00401015 ; - 0x11 буфер var_18[7]
  • .text:00401015 ; - 0x10 буфер var_18[8]
  • .text:00401015 ; - 0x0F буфер var_18[9]
  • .text:00401015 ; - 0x0E дырка для выравнивания
  • .text:00401015 ; - 0x0D дырка для выравнивания
  • .text:00401015 ; - 0x0С буфер var_С[0] 01
  • .text:00401015 ; - 0x0B буфер var_С[1] 02
  • .text:00401015 ; - 0x0A буфер var_С[2] 03
  • .text:00401015 ; - 0x09 буфер var_С[3] 04
  • .text:00401015 ; - 0x08 буфер var_С[4] 05
  • .text:00401015 ; - 0x07 буфер var_С[5] 06
  • .text:00401015 ; - 0x06 буфер var_С[6] 07
  • .text:00401015 ; - 0x05 буфер var_С[7] 08
  • .text:00401015 ; - 0x04 буфер var_С[8] 09
  • .text:00401015 ; - 0x03 буфер var_С[9] 10
  • .text:00401015 ; - 0x02 дырка для выравнивания 11
  • .text:00401015 ; - 0x01 дырка для выравнивания 12
  • .text:00401015 ; 0x00 значение регистра ebp[0] 13
  • .text:00401015 ; + 0x01 значение регистра ebp[1] 14
  • .text:00401015 ; + 0x02 значение регистра ebp[2] 15
  • .text:00401015 ; + 0x03 значение регистра ebp[3] 16
  • .text:00401015 ; + 0x04 значение регистра eip[0] (адрес возврата) 17
  • .text:00401015 ; + 0x05 значение регистра eip[1] (адрес возврата) 18
  • .text:00401015 ; + 0x06 значение регистра eip[2] (адрес возврата) 19
  • .text:00401015 ; + 0x07 значение регистра eip[3] (адрес возврата) 20
  • .text:00401015 ; Таким образом, байты с 17 до 20 (не считая нуля завершающего строку) из буфера var_c затирают
  • .text:00401015 : адрес возврата сохраненный в стеке. Следовательно, строка из шестнадцати символов, включая
  • .text:00401015 ; завершающий ноль вызовет модификацию младшего байта адреса возврата.
  • .text:00401015 ; Остается отождествить буфер var_c – что он собой представляет имя пользователя или пароль?
  • .text:00401018 push offset aLogin ; "Login:"
  • .text:00401018 ; В стек заносится смещение строки “Login”, значение указателя стека уменьшается на 4
  • .text:00401018 ; Это первый (и единственный) аргумент функции printf
  • .text:0040101D call _printf
  • .text:0040101D ; Вывод на экран приглашения “Login:”
  • .text:00401022 add esp, 4
  • .text:00401022 ; Значение указателя стека увеличивается на четыре, чтобы избавится от занесенного в стек смещения
  • .text:00401025 ; строки “Login”. Си-функции не очищают стек после своего завершения
  • .text:00401025 lea eax, [ebp+var_C]
  • .text:00401025 ; В регистр eax заносится смещение буфера var_c, для последующей передачи его функции gets, читающей
  • .text:00401025 ; строку с клавиатуры.
  • .text:00401025 ; Следовательно, буфер var_c содержит имя пользователя
  • .text:00401028 push eax
  • .text:00401028 ; Значение eax заносится в стек
  • .text:00401029 call _gets
  • .text:00401029 ; Вызов функции _gets
  • .text:0040102E add esp, 4
  • .text:0040102E ; Удаление двойного слова из стека (для очистки аргумента функции gets)
  • .text:00401031 push offset aPassw ; "Passw:"
  • .text:00401031 ; Занесение в стек строки «Passw»
  • .text:00401036 call _printf
  • .text:00401036 ; Вывод строки “Passw” на экран с помощью функции printf
  • .text:0040103B add esp, 4
  • .text:0040103B ; Удаление двойного слова из стека
  • .text:0040103E lea ecx, [ebp+var_18]
  • .text:0040103E ; В регистр ecx заносится смещение буфера var_18 для последующей передачи его функции gets,
  • .text:0040103E ; читающей строку с клавиатуры. Следовательно, буфер var_18 содержит пароль
  • .text:00401041 push ecx
  • .text:00401041 ; Передача аргумента функции gets
  • .text:00401042 call _gets
  • .text:00401042 ; Чтение пароля в буфер var_18
  • .text:00401047 add esp, 4
  • .text:00401047 ; Балансировка стека
  • .text:0040104A push offset aGuest ; "guest"
  • .text:0040104A ; Занесение в стек смещения строки Guest для сравнения ее с введенным паролем
  • .text:0040104F lea edx, [ebp+var_18]
  • .text:0040104F ; В регистр edx заносится смещение буфера, содержащего введенный пароль
  • .text:00401052 push edx
  • .text:00401052 ; Сейчас в верхушке стека содержатся два значения
  • .text:00401052 ; смещение эталонного пароля и смещения буфера, содержащего введенный пароль
  • .text:00401053 call _strcmp
  • .text:00401053 ; Вызов функции strcmp(&pass[0],”Guest”)
  • .text:00401058 add esp, 8
  • .text:00401058 ; Балансировка стека
  • .text:0040105B test eax, eax
  • .text:0040105B ; Значение, возвращаемое функцией помещается в регистр eax
  • .text:0040105B ; если он равен нулю, то строки идентичны и наоборот
  • .text:0040105B ; если eax равен нулю, команда test выставляет флаг нуля
  • .text:0040105D jnz short loc_0_401066
  • .text:0040105D ; Если флаг не установлен (пароль не равен “Guest”), переход по адресу 401066
  • .text:0040105F mov eax, 1
  • .text:0040105F ; В регистр eax заносится значение 1, которое будет возвращено при выходе из нее
  • .text:00401064 jmp short loc_0_401068
  • .text:00401064 ; Переход по адресу 401068 (к выходу из функции)
  • .text:00401066 ; ───────────────────────────────────────────────────────────────────────────
  • .text:00401066
  • .text:00401066 loc_0_401066: ; CODE XREF: auth+4Bj
  • .text:00401066 xor eax, eax
  • .text:00401068 ; Обнулить значение регистра eax
  • .text:00401068 loc_0_401068: ; CODE XREF: auth+52j
  • .text:00401068 mov esp, ebp
  • .text:00401068 ; Восстановить значение регистра esp, который должен указывать на сохраненный в стеке регистр ebp
  • .text:0040106A pop ebp
  • .text:0040106A ; Восстановить ebp
  • .text:0040106B retn
  • .text:0040106B ; Выйти из функции. Команда retn снимает из стека двойное слово, которое при
  • .text:0040106B ; нормальном развитии событий должно быть равно адресу возврата (в данном примере 00401081
  • .text:0040106B ; (смотри функцию main)
  • .text:0040106B auth endp
  • .text:0040106B
  • .text:0040106C
  • .text:0040106C ; ███████████████ S U B R O U T I N E ███████████████████████████████████████
  • .text:0040106C
  • .text:0040106C ; Attributes: bp-based frame
  • .text:0040106C
  • .text:0040106C main proc near ; CODE XREF: start+AFp
  • .text:0040106C push ebp
  • .text:0040106C ; Занесение в стек значение регистра ebp
  • .text:0040106D mov ebp, esp
  • .text:0040106D ; Открытие кадра стека
  • .text:0040106F push offset aBufferOverflow ; "Buffer Overflows Demo\n"
  • .text:0040106F ; Занесение в стек смещения строки “ Buffer Overflows Demo” для вывода ее на экран
  • .text:00401074 call _printf
  • .text:00401074 ; Вызов функции printf(“Buffer Overflows Demo\n")
  • .text:00401079 add esp, 4
  • .text:00401079 ; Балансировка стека
  • .text:0040107C call Auth
  • .text:0040107C ; Вызов функции Auth(). В стек заносится адрес следующей за call команды, т.е. 00401081
  • .text:00401081 test eax, eax
  • .text:00401081 ; Функция Auth возвратила нулевое значение?
  • .text:00401083 jz short loc_0_401094
  • .text:00401083 ; Если функция возвратила нулевое значение перейти по адресу 401094
  • .text:00401085 push offset aPasswordOk ; "Password ok\n"
  • .text:00401085 ; Занесение в стек смещения строки «Password Ok”
  • .text:0040108A call _printf
  • .text:0040108A ; Вызов функции printf(“Password OK\n”);
  • .text:0040108F add esp, 4
  • .text:0040108F ; Балансировка стека
  • .text:00401092 jmp short loc_0_4010A1
  • .text:00401092 ; Переход по адресу 4010A1
  • .text:00401094 ; ───────────────────────────────────────────────────────────────────────────
  • .text:00401094
  • .text:00401094 loc_0_401094: ; CODE XREF: main+17j
  • .text:00401094 push offset aInvalidPasswor ; "Invalid password\n"
  • .text:00401094 ; Занесение в стек строки “ Invalid password”
  • .text:00401099 call _printf
  • .text:00401099 ; Вызов функции printf("Invalid password\n")
  • .text:0040109E add esp, 4
  • .text:0040109E ; Балансировка стека
  • .text:004010A1
  • .text:004010A1 loc_0_4010A1: ; CODE XREF: main+26j
  • .text:004010A1 pop ebp
  • .text:004010A1 ; Восстановление ebp
  • .text:004010A2 retn
  • .text:004010A2 ; Завершение программы
  • ...
  • .text:004010A2 main endp
  • .data:00406030 aHelloRoot db 'Hello, Root!',0Ah,0 ; DATA XREF: .text:00401003o
  • .data:0040603E align 4
  • .data:00406040 aLogin db 'Login:',0 ; DATA XREF: auth+6o
  • .data:00406047 align 4
  • .data:00406048 aPassw db 'Passw:',0 ; DATA XREF: auth+1Fo
  • .data:0040604F align 4
  • .data:00406050 aGuest db 'guest',0 ; DATA XREF: auth+38o
  • .data:00406056 align 4
  • .data:00406058 aBufferOverflow db 'Buffer Overflows Demo',0Ah,0 ; DATA XREF: main+3o
  • .data:0040606F align 4
  • .data:00406070 aPasswordOk db 'Password ok',0Ah,0 ; DATA XREF: main+19o
  • .data:0040607D align 4
  • .data:00406080 aInvalidPasswor db 'Invalid password',0Ah,0 ; DATA XREF: main+28o



Анализ кода позволил установить, что искомая функция располагается по адресу, равному 0x401000, а шестнадцатый символ имени пользователя затирает завершающим строку нулем младший байт адреса возврата.

Для передачи управления на функцию root() необходимо подменить адрес возврата на ее адрес. Поскольку, адрес возврата, уже содержащийся в стеке, равен 0х401081, а адрес функции root() равен 0x401000, для достижения поставленной цели достаточно всего лишь обнулить младший байт. Если ввести строку длиной 16 символов (не важно каких), завершающий ее нуль придется как раз на младший байт сохраненного в стеке регистра EIP и инструкция retn передаст управление на функцию root().

  • - 0x0С user[0] 01 X
  • - 0x0B user[1] 02 X
  • - 0x0A user[2] 03 X
  • - 0x09 user[3] 04 X
  • - 0x08 user[4] 05 X
  • - 0x07 user[5] 06 X
  • - 0x06 user[6] 07 X
  • - 0x05 user[7] 08 X
  • - 0x04 user[8] 09 X
  • - 0x03 user[9] 10 X
  • - 0x02 дырка 11 X
  • - 0x01 дырка 12 X
  • 0x00 ebp[0] 13 X
  • + 0x01 ebp[1] 14 X
  • + 0x02 ebp[2] 15 X
  • + 0x03 ebp[3] 16 X
  • + 0x04 eip[0] 81 17 0
  • + 0x05 eip[1] 10 18
  • + 0x06 eip[2] 40 19
  • + 0x07 eip[3] 00 20


Если на запрос имени пользователя ввести, например, такую строку, то на экран выдастся приветствие “Hello, Root!”, подтверждающие факт передачи управления функции root(), что не было предусмотрено разработчиком.

Однако сразу же после завершения функции root(), программа грохается, и операционная система выдает сообщение об исключительной ситуации, предлагая завершить работу приложения (смотри рисунок 073). (Реакция операционной системы зависти от самой операционной системы, данный скриршет иллюстрирует поведение Windows 2000)