Чтобы иметь возможность ломать программы, на страже которых стоят хитрые защитные механизмы, необходимо знать разные способы нахождения этих самых механизмов в подопытном приложении. В сегодняшней статье будет показано, как это сделать, а в конце мы разберем пример работы с графическим приложением.
Способ взлома программ 1. Поиск введенного пароля в памяти
Пароль, хранящийся в теле программы открытым текстом, — скорее из ряда вон выходящее исключение, чем правило. К чему услуги хакера, если пароль и без того виден невооруженным взглядом? Поэтому разработчики защиты всячески пытаются скрыть его от посторонних глаз (о том, как именно они это делают, мы поговорим позже).
Впрочем, учитывая размер современных пакетов, программист может без особого труда поместить пароль в каком-нибудь завалявшемся файле, попутно снабдив его «крякушами» — строками, выглядящими как пароль, но паролем не являющимися. Попробуй разберись, где тут липа, а где нет, тем более что подходящих на эту роль строк в проекте средней величины может быть несколько сотен, а то и тысяч!
РЕКОМЕНДУЕМ:
Взлом приложений для Андроид с помощью отладчика
Давайте подойдем к решению проблемы от обратного — будем искать не оригинальный пароль, который нам неизвестен, а ту строку, которую мы скормили программе в качестве пароля. А найдя, установим на нее бряк, и дальше все точно так же, как и раньше. Бряк всплывает на обращение по сравнению, мы выходим из сравнивающей процедуры, корректируем JMP и…
Взглянем еще раз на исходный текст ломаемого нами примера passCompare1.cpp:
1 2 3 4 5 6 7 8 9 |
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).
Попробуем отыскать в памяти введенный пароль:
1 |
0:001> s -a 0x0 L?0x7FFFFFFF "KPNC Kaspersky" |
Пояснения
Первый параметр после команды s — флаг -a — определяет цель поиска как набор ASCII-символов. Второй параметр — смещение, откуда начать искать. Вообще-то начинать поиск с нулевого смещения — идея глупая. Судя по карте памяти, здесь расположен служебный код и искомого пароля быть не может. Впрочем, это ничему не вредит, и так гораздо быстрее, чем разбираться, с какого адреса загружена программа и откуда именно начинать поиск.
Третий параметр — верхний предел поиска, то есть «докуда». Здесь у нас стоит максимальное 32-битное знаковое число, таким образом мы охватываем весь возможный диапазон 32-битного процесса.
Последний параметр — собственно искомая строка. Обратите внимание, что мы ищем не всю строку, а только ее часть ( KPNC Kaspersky++ против KPNC Kaspersky). Это позволяет избавиться от ложных срабатываний, возникающих из-за ссылок на внутренние буфера.
Результат (у вас значения, скорее всего, получатся другими, и они будут меняться при каждом перезапуске приложения):
1 2 |
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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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, а у вас значение, скорее всего, будет другим). Узнаем, куда в памяти загружена секция .rdata: 0xDE0000 + 0x2000 == 0xDE2000, а также куда загружена секция .data: 0xDE0000 + 0x3000 == 0xDE3000. Это гораздо выше найденных адресов расположения буферов с введенным паролем. Следовательно, найденные адреса не указывают в области .data и .rdata.
Думаем дальше. Адрес 0x147fd80 выходит далеко за пределы ломаемого приложения, и вообще непонятно, чему принадлежит. Почесав затылок, мы вспомним о такой «вкусности» Windows, как куча (heap). С помощью команды !heap посмотрим, где она начинается:
1 2 |
Index Address Name Debugging options enabled 1: 01470000 |
Из этого заключаем, что адрес 0x147fd80 явно находится в куче.
Разбираемся дальше. Поскольку стек растет снизу вверх (то есть от старших адресов к младшим), адрес 0xf9f810 находится в стеке. Уверенность подогревает тот факт, что большинство программистов размещает буфера в локальных переменных, ну а локальные переменные, в свою очередь, размещаются компилятором в стеке.
Ну что, попробуем поставить бряк по первому адресу?
1 2 |
0:001> ba r4 00f9f810 0:001> g |
На втором запросе пароля снова вводим KPNC Kaspersky++. Жмем Enter и дожидаемся сиюминутной активации отладчика. Бряк произошел на второй из этих строк:
1 2 3 |
77c349f3 8806 mov byte ptr [esi], al 77c349f5 46 inc esi 77c349f6 8975d8 mov dword ptr [ebp-28h], esi |
Смотрим, что находится в регистре esi:
1 2 |
dc esi 00f9f810 434e504b 73614b20 73726570 2b2b796b KPNC Kaspersky++ |
Впрочем, этого и следовало ожидать. Попробуем выйти из текущей функции по Shift + F11. И мы снова попадем на эту же строку. Вновь посмотрим содержимое этого регистра:
1 |
00f9f811 20434e50 7073614b 6b737265 0a2b2b79 PNC Kaspersky++. |
Ага, один символ откусан. Следовательно, мы находимся в сравнивающей процедуре. Выйдем из нее нажатием на F5, так как при нажатии на Shift + F11 мы перейдем следующую итерацию перебора символов.
1 2 3 4 |
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, чтобы узнать, что с чем сравнивается:
1 2 3 4 |
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 (см. в скачиваемых материалах к статье).
Также обратите внимание на то, что работа с текстом в этом примере организована по-другому. Если раньше мы работали с базовым типом char, то здесь используется обертка — класс CString, что, скорее всего, при взломе профессиональных приложений будет встречаться нам чаще. Кроме двух кнопок, идущих в заготовке по умолчанию, добавьте на форму элемент Edit Control. Свяжите его с переменной m_password и создайте событие обработки нажатия на кнопке OK. Это и будет ключевая процедура приложения, проверяющая введенный пароль на равенство эталонному:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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, и отладчик незамедлительно всплывает:
1 2 3 4 5 6 |
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 и посмотреть, куда попадаем. Если вокруг находятся вызовы перерисовки элементов, то, значит, это процедура перерисовки формы и нам не сюда.
Перерисовка элементов:
1 2 3 4 5 6 7 |
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 на клавиатуре, прямо на первую строчку приведенного ниже листинга:
1 2 3 4 5 6 7 |
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:
1 2 3 |
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:
1 2 3 |
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. Он отличается от предыдущего только изменением типа данных считываемой строки и использованием перегруженного метода для ее считывания:
1 2 3 |
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, она выведет стек вызовов всех функций с их параметрами. На вершине будет последняя вызванная функция, в которой мы сейчас находимся, с параметрами:
1 2 3 4 |
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] … |
Второй способ — воспользоваться сведениями из окна Locals (полезная штука): View → Locals. Если бы программист оставил пароль в локальных переменных какой-либо функции, мы бы просто увидели его в окне Locals. Удобно, безусловно. Итак, адрес буфера с паролем узнали (в вашем случае он будет другим), осталось легким движением руки поставить бряк:
1 |
ba r4 0x004fe4d0 |
Продолжим выполнение. Отладчик тут же вспыхивает снова в функции passCompare35!CpassCompare35Dlg::OnBnClickedOk, прямо на последней строке:
1 2 3 4 5 6 7 8 9 10 |
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] … |
Выделенная строка и код до нее крайне похожи на наш защитный механизм. Проверим хранящиеся в регистрах значения:
1 2 3 4 |
0:000> du eax 01547af0 "myGOODpassword" 0:000> du ecx 012fe838 "KPNC Kaspersky++"[crayon-67593006acfed469305885 inline="true" ] |
[/crayon]
Введенная пользователем строка и эталонный пароль, как на блюдечке с голубой каемочкой! Одно лишь изменение типа данных может преобразить процесс взлома.
РЕКОМЕНДУЕМ:
Безопасность UEFI
Замечательно! Вот так, безо всяких ложных срабатываний, элегантно, быстро и красиво, мы обошли защиту программы.
Этот способ универсален, и впоследствии мы еще не раз им воспользуемся. Вся соль — определить ключевую функцию защиты и поставить на нее бряк. В Windows все поползновения (обращения к ключевому файлу, реестру и прочее) сводятся к вызову функций API, перечень которых хоть и велик, но все же конечен и известен заранее.
Интересная статья. Большое спасибо!
Нужна помощь в удаление из программы совершенного визита от 14.04