Основы хакерства. Новые способы находить защитные механизмы в чужих программах

Основы хакерства. Новые способы находить защитные механизмы в чужих программах

Эксплойт

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

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

 


Способ 1. Прямой поиск введенного пароля в памяти

Пароль, хранящийся в теле программы открытым текстом, — скорее из ряда вон выходящее исключение, чем правило. К чему услуги хакера, если пароль и без того виден невооруженным взглядом? Поэтому разработчики защиты всячески пытаются скрыть его от посторонних глаз (о том, как именно они это делают, мы поговорим позже).

Впрочем, учитывая размер современных пакетов, программист может без особого труда поместить пароль в каком-нибудь завалявшемся файле, попутно снабдив его «крякушами» — строками, выглядящими как пароль, но паролем не являющимися. Попробуй разберись, где тут липа, а где нет, тем более что подходящих на эту роль строк в проекте средней величины может быть несколько сотен, а то и тысяч!

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

Взглянем еще раз на исходный текст ломаемого нами примера passCompare1.cpp:

for(;;)
{
    printf("Enter password:");
    fgets(&buff[0],PASSWORD_SIZE,stdin);
    if (strcmp(&buff[0],PASSWORD))
        printf("Wrong password\n");
    else break;
    if (++count>2) return -1;
}

Обрати внимание — в buff читается введенный пользователем пароль, сравнивается с оригиналом, затем (при неудачном сравнении) запрашивается еще раз, но (!) при этом buffне очищается! Отсюда следует, что, если после выдачи ругательства Wrong password вызвать отладчик и пройтись по памяти контекстным поиском, можно обнаружить тот заветный buff, а остальное уже дело техники!

Итак, приступим (мы еще не знаем, во что мы ввязываемся, — но, увы, в жизни все сложнее, чем в теории). На этот раз запустим passCompare1.exe отдельно от отладчика. Затем подключимся к процессу из отладчика (Attach to process в WinDbg). Обрати внимание: в окне выбора процесса отображаются все запущенные процессы и для каждого из них выводится его разрядность в столбце Platform. Вводим любой пришедший на ум пароль (например, KPNC Kaspersky++), пропускаем возмущенный вопль Wrong мимо ушей и в отладчике нажимаем Break (сочетание клавиш Alt + Del).

Окно со списком процессов для выбора

Попробуем отыскать в памяти введенный пароль:

0:001> s -a 0x0 L?0x7FFFFFFF "KPNC Kaspersky"

 

Пояснения

Первый параметр после команды s — флаг -a — определяет цель поиска как набор ASCII-символов. Второй параметр — смещение, откуда начать искать. Вообще-то начинать поиск с нулевого смещения — идея глупая. Судя по карте памяти, здесь расположен служебный код и искомого пароля быть не может. Впрочем, это ничему не вредит, и так гораздо быстрее, чем разбираться, с какого адреса загружена программа и откуда именно начинать поиск.

Третий параметр — верхний предел поиска, то есть «докуда». Здесь у нас стоит максимальное 32-битное знаковое число, таким образом мы охватываем весь возможный диапазон 32-битного процесса.

Последний параметр — собственно искомая строка. Обрати внимание, что мы ищем не всю строку, а только ее часть (KPNC Kaspersky++ против KPNC Kaspersky). Это позволяет избавиться от ложных срабатываний, возникающих из-за ссылок на внутренние буфера.

Результат (у тебя значения, скорее всего, получатся другими, и они будут меняться при каждом перезапуске приложения):

00f9f810 4b 50 4e 43 20 4b 61 73-70 65 72 73 6b 79 2b 2b KPNC Kaspersky++

0147fd80 4b 50 4e 43 20 4b 61 73-70 65 72 73 6b 79 2b 2b KPNC Kaspersky++

Целых два вхождения! Почему два? Предположим, что при чтении ввода с клавиатуры символы сперва попадают в системный буфер, который и дает ложное срабатывание. Тем не менее не ставить же, не разобравшись, сразу обе точки останова. В данном случае четырех отладочных регистров процессора хватит, а как быть, если бы мы нашли десяток вхождений? Да и в двух бряках немудрено заблудиться с непривычки! Как отфильтровать помехи?

Начинаем думать.

На помощь приходит карта памяти — зная владельца региона, которому принадлежит буфер, можно очень многое сказать об этом буфере. Наскоро набив уже знакомую команду !dh passCompare1, мы получим приблизительно следующее (выбраны сведения только о секциях .data и .rdata):

SECTION HEADER #2
.rdata name
A7E virtual size
2000 virtual address
C00 size of raw data
1200 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
40000040 flags
Initialized Data
(no align specified)
Read Only
 
SECTION HEADER #3
.data name
388 virtual size
3000 virtual address
200 size of raw data
1E00 file pointer to raw data
0 file pointer to relocation table
0 file pointer to line numbers
0 number of relocations
0 number of line numbers
C0000040 flags
Initialized Data
(no align specified)
Read Write

Заодно определим базовый адрес модуля приложения: lmf m passCompare1 (в моем конкретном случае он равен 0xDE0000, а у тебя значение, скорее всего, будет другим). Узнаем, куда в памяти загружена секция .rdata0xDE0000 + 0x2000 == 0xDE2000, а также куда загружена секция .data0xDE0000 + 0x3000 == 0xDE3000. Это гораздо выше найденных адресов расположения буферов с введенным паролем. Следовательно, найденные адреса не указывают в области .data и .rdata.

Думаем дальше. Адрес 0x147fd80 выходит далеко за пределы ломаемого приложения, и вообще непонятно, чему принадлежит. Почесав затылок, мы вспомним о такой «вкусности» Windows, как куча (heap). С помощью команды !heap посмотрим, где она начинается:

Index Address Name Debugging options enabled

1: 01470000

Из этого заключаем, что адрес 0x147fd80 явно находится в куче.

Разбираемся дальше. Поскольку стек растет снизу вверх (то есть от старших адресов к младшим), адрес 0xf9f810 находится в стеке. Уверенность подогревает тот факт, что большинство программистов размещает буфера в локальных переменных, ну а локальные переменные, в свою очередь, размещаются компилятором в стеке.

Ну что, попробуем поставить бряк по первому адресу?

0:001> ba r4 00f9f810

0:001> g

На втором запросе пароля снова вводим KPNC Kaspersky++. Жмем Enter и дожидаемся сиюминутной активации отладчика. Бряк произошел на второй из этих строк:

77c349f3 8806           mov     byte ptr [esi], al
77c349f5 46             inc     esi
77c349f6 8975d8         mov     dword ptr [ebp-28h], esi

Смотрим, что находится в регистре esi:

dc esi
00f9f810 434e504b 73614b20 73726570 2b2b796b KPNC Kaspersky++

Впрочем, этого и следовало ожидать. Попробуем выйти из текущей функции по Shift + F11. И мы снова попадем на эту же строку. Вновь посмотрим содержимое этого регистра:

00f9f811 20434e50 7073614b 6b737265 0a2b2b79 PNC Kaspersky++.

Ага, один символ откусан. Следовательно, мы находимся в сравнивающей процедуре. Выйдем из нее нажатием на F5, так как при нажатии на Shift + F11 мы перейдем в следующую итерацию перебора символов.

00de10e0 b80821de00 mov eax, offset passCompare1!`string’ (00de2108)
00de10e5 8a11 mov dl, byte ptr [ecx]
00de10e7 3a10 cmp dl, byte ptr [eax] ds:002b:00de2108=6d
00de10e9 751a jne passCompare1!main+0xc5 (00de1105)

И вот мы в теле уже хорошо нам знакомой (развивай зрительную память!) процедуры сравнения оригинального и введенного пользователем паролей. На всякий случай для пущей убежденности выведем значение указателей EAX и ECX, чтобы узнать, что с чем сравнивается:

0:000> dc eax
00de2108 4f47796d 6170444f 6f777373 000a6472 myGOODpassword..
0:000> dc ecx
00f9f810 434e504b 73614b20 73726570 2b2b796b KPNC Kaspersky++

Как раз то, что мы ищем!

Ну а остальное мы уже проходили. Записываем адрес условного перехода (ключевую последовательность для поиска), с помощью сведений из прошлой статьи находим адрес инструкции на носителе, соответствующей спроецированной в памяти, правим исполняемый файл, и все окей.

 

Выводы

Итак, мы познакомились с одним более или менее универсальным способом взлома защит, основанных на сравнении пароля (позже мы увидим, что он подходит и для защит, основанных на регистрационных номерах). Его основное достоинство — простота. А недостатки… недостатков у него много:

  • если программист очистит буфера после сравнения, поиск введенного пароля ничего не даст, разве что останутся системные буфера, которые так просто не затрешь, но отследить перемещения пароля из системных буферов в локальные не так-то легко;
  • служебных буферов много, и очень трудно определить, какой из них «настоящий». Программист же может располагать буфер и в сегменте данных (статический буфер), и в стеке (локальный буфер), и в куче, и даже выделять память низкоуровневыми вызовами типа VirtualAlloc или… да мало ли как разыграется его фантазия. В результате подчас приходится просеивать все найденные вхождения тупым перебором.

 


Способ 2. Бряк на функции ввода пароля

Взлом приложения с GUI

Настала пора разнообразить наш объект взлома. Теперь попробуем заломить приложение с графическим интерфейсом. В качестве тренировки разберем passCompare3. Это то же самое, что и passCompare1.exe, только с графическим интерфейсом на основе MFC Dialog Based App (см. в скачиваемых материалах к статье).

Старый добрый MFC Application Wizard

Также обрати внимание на то, что работа с текстом в этом примере организована по-другому. Если раньше мы работали с базовым типом char, то здесь используется обертка — класс CString, что, скорее всего, при взломе профессиональных приложений будет встречаться нам чаще. Кроме двух кнопок, идущих в заготовке по умолчанию, добавь на форму элемент Edit Control. Свяжи его с переменной m_password и создай событие обработки нажатия на кнопке OK. Это и будет ключевая процедура приложения, проверяющая введенный пароль на равенство эталонному:

const CString PASSWORD = _T("myGOODpassword");
…
void CpassCompare3Dlg::OnBnClickedOk()
{
    CString str = NULL;

    m_password.GetWindowText(str);
    if (PASSWORD.Compare(str))
    {
        MessageBox(_T("Wrong password"));
        m_password.SetSel(0, -1, 0);
        return;
    }
    else
    {
        MessageBox(_T("Password OK"));
    }
    CDialogEx::OnOK();
}

Кажется, никаких сюрпризов не предвидится.

При всем желании метод прямого поиска пароля в памяти элегантным назвать нельзя, да и практичным тоже. А собственно, зачем искать сам пароль, спотыкаясь о беспорядочно разбросанные буфера, когда можно поставить бряк непосредственно на функцию, его считывающую? Хм, можно и так… да вот угадать, какой именно функцией разработчик вздумал читать пароль, вряд ли будет намного проще.

На самом деле одно и то же действие может быть выполнено всего лишь несколькими функциями и их перебор не займет много времени. В частности, содержимое окна редактирования обычно добывается при помощи либо функции GetWindowTextW (чаще всего), либо функции GetDlgItemTextW (а это значительно реже). Все версии Windows NT и младше предпочитают работать с юникодом, поэтому на конце функций работы с текстом W(wide), а не A (ASCII).

Раз уж речь зашла об окнах, запустим наш GUI «крякмис» и установим точку останова на функцию GetWindowTextW (bp User32!GetWindowTextW). Хотя эта функция — системная, точка останова не будет глобальной и не затронет все приложения в системе, а будет функционировать только в контексте данного приложения.

Вводим какой-нибудь пароль (KPNC Kaspersky++, по обыкновению), нажимаем клавишу Enter, и отладчик незамедлительно всплывает:

USER32!GetWindowTextW:
7510a8e0 6a10 push 10h
7510a8e2 68e0041875 push offset USER32!__HrLoadAllImportsForDll+0x1e9 (751804e0)
7510a8e7 e88c510200 call USER32!_SEH_prolog4 (7512fa78)
7510a8ec 8b750c mov esi, dword ptr [ebp+0Ch]
7510a8ef 85f6 test esi, esi

Может случиться так, что сначала произойдет «левая» активация отладчика. Ее можно пропустить многократным нажатием F5 до тех пор, пока отладчик вновь не всплывет на этой же функции. А лучше, чтобы не пропустить нужный момент, как только мы попадем в GetWindowTextW, выйти из нее по нажатию Shift + F11 и посмотреть, куда попадаем. Если вокруг находятся вызовы перерисовки элементов, то, значит, это процедура перерисовки формы и нам не сюда.

Перерисовка элементов:

6b08a247 0f84804f0200 je COMCTL32!Button_DrawThemed+0x2519b (6b0af1cd)
6b08a24d 57 push edi
6b08a24e 50 push eax
6b08a24f ff33 push dword ptr [ebx]
6b08a251 ff15c0f51d6b call dword ptr [COMCTL32!_imp__GetWindowTextW (6b1df5c0)]
6b08a257 8bcb mov ecx, ebx
6b08a259 e80b010000 call COMCTL32!Button_GetTextFlags (6b08a369)

Если же после выхода из USER32!GetWindowTextW мы попадаем в passCompare3!CWnd::GetWindowTextW, делаем еще один выход. Следуя логике, мы в результате попадаем в обработчик нажатия кнопки OK на форме или Enter на клавиатуре, прямо на первую строчку приведенного ниже листинга:

00ef2809 e8c6750100 call passCompare3!CWnd::GetWindowTextW (00f09dd4)
00ef280e 8b45ec mov eax, dword ptr [ebp-14h]
00ef2811 85c0 test eax, eax
00ef284b 1bc0 sbb eax, eax
00ef284d 83c801 or eax, 1
00ef2850 8bce mov ecx, esi

Жмем F10, чтобы сделать шаг вперед в трассировке программы. Теперь можем узнать значение в регистре eax:

0:000> dc eax
014a6cb8 0050004b 0043004e 004b0020 00730061 K.P.N.C. .K.a.s.
014a6cc8 00650070 00730072 0079006b 002b002b p.e.r.s.k.y.+.+.

Хорошо, введенный пароль, есть контакт. Только почему после каждого символа стоит точка? Думаю, ты уже догадался, что она означает двухбайтовую природу символа перед ней. Отхлебнув пивка, кваса или лимонада (по желанию), вспоминаем, что, хоть класс CString и может работать с типами char (однобайтовое представление символов) и wchar_t(многобайтовое представление до четырех байт, то есть юникод в UTF-8, -16 или -32), это зависит от настроек компилятора. А именно от того, какой символ включен: MBCS — char, UNICODE — wchar_t. Чаще всего используется второй набор символов, так как по умолчанию включены именно широкие символы.

Судя по всему, где-то рядом притаился эталонный пароль. Сделаем пару шагов внутри процедуры. Мы попадаем на строку 00ef2850 8bce mov ecx, esi, которая находится в конце приведенного выше листинга.

Проверим содержимое регистра ecx:

0:000> dc ecx
01437518 0079006d 004f0047 0044004f 00610070 m.y.G.O.O.D.p.a.
01437528 00730073 006f0077 00640072 abab0000 s.s.w.o.r.d…..

И правда! Интуиция нас не подвела, эталонный пароль тут как тут.

Работа приложения

Изменяем тип данных

А что, если бы программист для сохранения введенного пароля воспользовался не классом CString, а по старинке — массивом широких символов wchar_t? Давай посмотрим семпл passCompare35. Он отличается от предыдущего только изменением типа данных считываемой строки и использованием перегруженного метода для ее считывания:

wchar_t str[MAX_PASSWORD_SIZE];
…
m_password.GetWindowText(str, MAX_PASSWORD_SIZE);

Как видишь, у этого метода появился параметр размерности строки, он же величина строкового буфера, добавь его объявление в начало программы: const int MAX_PASSWORD_SIZE = 0x666;.

Натравим отладчик на исполняемый файл. Поставим бряк на функцию GetWindowTextW, как в прошлом примере. Теперь, если проследить выполнение программы после всплытия отладчика, мы не обнаружим эталонный пароль на прежнем месте.

Что же делать? Как теперь его искать? Мы пойдем другим путем, но в том же направлении. Когда мы окажемся в passCompare3!CWnd::GetWindowTextW после вызова User32!GetWindowText, у нас уже будет считанный из элемента управления буфер, содержащий строку. Если на этот буфер поставить бряк, то мы доберемся до места, где пароли сравниваются.

Вот там мы поймаем эталонный пароль. Но как узнать адрес буфера? Есть по меньшей мере два пути. Первый — воспользоваться командой kp, она выведет стек вызовов всех функций с их параметрами. На вершине будет последняя вызванная функция, в которой мы сейчас находимся, с параметрами:

0:000> kp

# ChildEBP RetAddr

00 004fe4b8 00e127c0 passCompare35!CWnd::GetWindowTextW(wchar_t * lpszString = 0x004fe4d0 "KPNC Kaspersky++", int nMaxCount = 0n1638)+0x24 [f:\dd\vctools\vc7libs\ship\atlmfc\src\mfc\winocc.cpp @ 255]

WinDbg с окном Locals

Второй способ — воспользоваться сведениями из окна Locals (полезная штука): View → Locals. Если бы программист оставил пароль в локальных переменных какой-либо функции, мы бы просто увидели его в окне Locals. Удобно, безусловно. Итак, адрес буфера с паролем узнали (в твоем случае он будет другим), осталось легким движением руки поставить бряк:

ba r4 0x004fe4d0

Продолжим выполнение. Отладчик тут же вспыхивает снова в функции passCompare35!CpassCompare35Dlg::OnBnClickedOk, прямо на последней строке:

passCompare35!CpassCompare35Dlg::OnBnClickedOk:
00e127bb e805c50100 call passCompare35!CWnd::GetWindowTextW (00e2ecc5)
00e127c0 a1c4fefd00 mov eax, dword ptr [passCompare35!PASSWORD (00fdfec4)]
00e127c5 8d8d30f3ffff lea ecx, [ebp-0CD0h]
00e127cb 0f1f440000 nop dword ptr [eax+eax]
00e127d0 668b10 mov dx, word ptr [eax]
00e127d3 663b11 cmp dx, word ptr [ecx]
00e127d6 751e jne passCompare35!CpassCompare35Dlg::OnBnClickedOk+0x66 (00e127f6) [br=1]

Выделенная строка и код до нее крайне похожи на наш защитный механизм. Проверим хранящиеся в регистрах значения:

0:000> du eax

01547af0 "myGOODpassword"

0:000> du ecx

012fe838 "KPNC Kaspersky++"

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

Замечательно! Вот так, безо всяких ложных срабатываний, элегантно, быстро и красиво, мы победили защиту!

Этот способ универсален, и впоследствии мы еще не раз им воспользуемся. Вся соль — определить ключевую функцию защиты и поставить на нее бряк. В Windows все поползновения (обращения к ключевому файлу, реестру и прочее) сводятся к вызову функций API, перечень которых хоть и велик, но все же конечен и известен заранее.

Источник

Report Page