Фундаментальные основы хакерства

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


Был бы омут, а черти будут.

народная поговорка

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

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

Взглянем еще раз на исходный текст ломаемого нами примера "simple.c"

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, а остальное уже – дело техники!

Итак, приступим (мы еще не знаем, во что мы ввязываемся – но, увы – в жизни все сложнее, чем в теории). Запускам SIMPLE.EXE, вводим любой пришедший на ум пароль (например, "KPNC Kaspersky++"), пропускаем возмущенный вопль "Wrong" мимо ушей и нажимаем <Ctrl-D> - "горячую" комбинацию клавиш для вызова Айса.
Так, теперь будем искать? Подождите, не надо бежать впереди лошадей: Windows 9x\NT – это не Windows 3.x и, тем более, не MS-DOS с единым адресным пространством для всех процессоров. Теперь, по соображениям безопасности, - дабы один процесс ненароком не залез во владения другого, каждому из них предоставляется собственное адресное пространство. Например, у процесса A по адресу 23:0146660 может быть записано число "0x66", у процесса B по тому же самому адресу 23:0146660 может находиться "0x0", а у процесса C и вовсе третье значение. Причем, процессы А, B и C не будет даже подозревать о существовании друг друга (ну, разве что воспользуются специальными средствами межпроцессорного взаимодействия).

Подробнее обо всем этом читайте у Хелен или Рихтера, здесь же нас больше заботит другое – вызванный по <Ctrl-D> отладчик "всплывает" в произвольном процессе (скорее всего Idle) и контекстный поиск в памяти ничего не даст. Необходимо насильно переключить отладчик в необходимый контекст адресного пространства и лишь затем что-то предпринимать.

Из прилагаемой к Айсу документации можно узнать, что переключение контекстов осуществляется командой ADDR, за которой следует либо имя процесса, урезанное до восьми символов, либо его PID. Узнать и то, и другое можно с помощью другой команды – PROC (В том, случае если имя процесса синтаксически неотличимо от PID, например, "123", приходится использовать PID процесса – вторая колонка цифр слева, в отчете PROC).

:addr simple

Отдаем команду "addr simple" и… ничего не происходит, даже значения регистров остаются неизменными! Не волнуйтесь – все ОК, что и подтверждает надпись 'simple' в правом нижнем углу, идентифицирующая текущий процесс. А регистры… это небольшой глюк Айса. Он них игнорирует, переключая только адреса. В частности поэтому, трассировка переключенной программы невозможна. Вот поиск – другое дело. Это – пожалуйста!

:s 23:0 L -1 "KPNC Kaspersky"



Пояснения: первый слева аргумент после s – адрес, записанный в виде "селектор: смещение". Под Windows 2000 для адресации данных и стека используется селектор номер 23, в других операционных системах он может отличаться (и отличается!). Узнать его можно загрузив любую программу, и списав содержимое регистра DS. Смещение – вообще-то, начинать поиск с нулевого смещения – идея глупая. Судя по карте памяти, здесь расположен служебный код и искомого пароля быть не может. Впрочем, это ничему не вредит, и так гораздо быстрее, чем разбираться: с какого адреса загружена программа, и откуда именно начинать поиск. Третий аргумент – "L –1" – длина региона для поиска. "-1", как нетрудно догадаться, – поиск "до победы". Далее - обратите внимание, что мы ищем не всю строку – а только ее часть ("KPNC Kaspersky++" против "KPNC Kaspersky") . Это позволяет избавиться от ложных срабатываний – Айс любит выдавать ссылки на свои внутренние буфера, содержащие шаблон поиска. Вообще-то они всегда расположены выше 0х80000000. Там – где никакой нормальный пароль "не живет", но все же будет нагляднее если по неполной подстроке находится именно наша строка.

Pattern found at 0023:00016E40 (00016E40)

Так, по крайней мере, одно вхождение уже найдено. Но вдруг в памяти есть еще несколько? Проверим это, последовательно отдавая команды "s" вплоть до выдачи сообщения "Pattern not found" или превышении адреса поиска 0x80000000.

:s

Pattern found at 0023:0013FF18 (0013FF18)

:s

Pattern found at 0023:0024069C (0024069C)

:s

Pattern found at 0023:80B83F18 (80B83F18)

Целых два вхождения, да еще одно "в уме" – итого три! Не много ли для нас, начинающих? Во-первых, неясно – вводимые пароли они, что плоятся ака кролики? Во-вторых, ну не ставить же все три точки останова. В данном случае четырех отладочных регистров процессора хватит, а как быть, если бы мы нашли десяток вхождений? Да и в трех бряках немудрено заблудиться с непривычки!



Итак – начинаем работать головой. Вхождений много, вероятнее всего потому, что при чтении ввода с клавиатуры символы сперва попадают в системные буфера, которые и дают ложные срабатывания. Звучит вполне правдоподобно, но вот как отфильтровать "помехи"?

На помощь приходит карта памяти – зная владельца региона, которому принадлежит буфер, об этом буфере очень многое можно сказать. Наскоро набив команду "map32 simple" мы получим приблизительно следующее.

:map32 simple

Owner     Obj Name  Obj#  Address        Size      Type

simple    .text     0001  001B:00011000  00003F66  CODE  RO

simple    .rdata    0002  0023:00015000  0000081E  IDATA RO

simple    .data     0003  0023:00016000  00001E44  IDATA RW

Ура, держи Тигру за хвост, есть одно отождествление! Буфер на 0x16E40 принадлежит сегменту данных и, видимо, это и есть то, что нам нужно. Но не стоит спешить! Все не так просто. Поищем-ка адрес 0x16E40 в самом файле simple.exe (учитывая обратный порядок байт это будет "40 E6 01 00"):

> dumpbin /SECTION:.data /RAWDATA simple.exe

RAW DATA #3

  00016030: 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00  Enter password:.

  00016040: 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00  myGOODpassword..

  00016050: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00  Wrong password..

  00016060: 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00  Password OK.....

  00016070: 40 6E 01 00 00 00 00 00 40 6E 01 00 01 01 00 00  @n......@n......

  00016080: 00 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00  ................

Есть, да? Даже два раза! Посмотрим теперь, кто на него ссылается – попробуем найти в дизассемблированном тексте подстроку "16070" – адрес первого двойного слова, указывающего на наш буфер.

  00011032: 68 70 60 01 00     push        16070h     ; <<<

  00011037: 6A 64              push        64h ; Макс. длина пароля (== 100 dec)

  00011039: 8D 4D

98           lea         ecx,[ebp-68h]



;Указатель ^^^^^^ на буфер куда записывать пароль

  0001103C: 51                 push        ecx

  0001103D: E8 E2 00 00 00     call        00011124   ; fgets

  00011042: 83 C4 0C           add         esp,0Ch    ; Выталкиваем три аргумента

В общем, все ясно, за исключением загадочного указателя на указатель 0x16070. Заглянув в MSDN, где описан прототип этой функции, мы обнаружим, что "таинственный незнакомец" – указатель на структуру FILE (аргументы по Си-соглашению, как мы помним заносятся в стек справа налево). Первый член структуры FILE – указатель на буфер (файловый ввод-вывод в стандартной библиотеке Си буферизован, и размер буфера по умолчанию составляет 4 Кб). Таким образом, адрес 0x16E40 – это указатель на служебный буфер и из списка "кандидатов в мастера" мы его вычеркиваем.

Двигаемся дальше. Претендент номер два – 0x24069C. Легко видеть он выходит за пределы сегмента данных и вообще непонятно чему принадлежит. Почесав затылок, мы вспомним о такой "вкусности" Windows как куча (heap). Посмотрим, что у нас там…

:heap 32 simple

    Base      Id  Cmmt/Psnt/Rsvd  Segments  Flags     Process

    00140000  01  0003/0003/00FD         1  00000002  simple

    00240000  02  0004/0003/000C         1  00008000  simple

    00300000  03  0008/0007/0008         1  00001003  simple

Ну, Тигр, давай на счастье хвост! Есть отождествление! Остается выяснить, кто выделил этот блок памяти – система под какие-то свои нужды или же сам программист. Первое, что бросается в глаза – какой-то подозрительно-странный недокументированный флаг 0x8000. Заглянув в WINNT.H можно даже найти его определение, которое, впрочем, мало чем нам поможет, разве что намекнет на системное происхождение оного.

#define HEAP_PSEUDO_TAG_FLAG            0x8000

А чтобы окончательно укрепить нашу веру, загрузим в отладчик любое подвернувшееся под лапу приложение и тут же отдадим команду "heap 32 proc_name". Смотрите – система автоматически выделяет из кучи три региона! Точь-в-точь такие, как и в нашем случае.


ОК, значит, и этот кандидат ушел лесом.

Остается последний адрес – 0x13FF18. Ничего он не напоминает? Постой-ка, постой. Какое было значение ESP при загрузке?! Кажется 0x13FFC4 или около того (внимание, в Windows 9x стек расположен совершенно в другом месте, но все рассуждения справедливы и для нее – необходимо лишь помнить местоположение стека в собственной операционной системе и уметь навскидку его узнавать).

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

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

:bpm 23:13FF18

:x

Break due to BPMB #0023:0013FF18 RW DR3  (ET=369.65 microseconds)

  MSR LastBranchFromIp=0001144F

    MSR LastBranchToIp=00011156

001B:000110B0  MOV     EAX,[EDX]

001B:000110B2  CMP     AL,[ECX]     

001B:000110B4  JNZ     000110E4

001B:000110B6  OR      AL,AL

001B:000110B8  JZ      000110E0

001B:000110BA  CMP     AH,[ECX+01]

001B:000110BD  JNZ     000110E4

001B:000110BF  OR      AH,AH

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

:d edx

0023:0013FF18 4B 50 4E 43 2D 2D 0A 00-70 65 72 73 6B 79 2B 2B  KPNC Kaspersky++

:d ecx

0023:00016040 6D 79 47 4F 4F 44 70 61-73 73 77 6F 72 64 0A 00  myGOODpassword..

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

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


Его основное достоинство – простота. А недостатки… недостатков у него много.

– если программист очистит буфера после сравнения, поиск веденного пароля ничего не даст. Разве что останутся системные буфера, которые так просто не затрешь, но отследить перемещения пароля из системных буферов в локальные не так-то просто!

– ввиду изобилия служебных буферов, очень трудно определить: какой из них "настоящий". Программист же может располагать буфер и в сегменте данных (статический буфер), и в стеке (локальный буфер), и в куче, и даже выделять память низкоуровневыми вызовами типа VirtualAlloc или… да мало ли как разыграется его фантазия. В результате, под час приходится "просеивать" все найденные вхождения тупым перебором.

В качестве тренировки разберем другой пример – "crackme 01". Это то же самое, что simple.exe, только с GUI-рым интерфейсом и ключевая процедура выглядит так:

void CCrackme_01Dlg::OnOK()

{

char buff[PASSWORD_SIZE];

m_password.GetWindowText(&buff[0],PASSWORD_SIZE);

if (strcmp(&buff[0],PASSWORD))

{

MessageBox("Wrong password");

m_password.SetSel(0,-1,0);

return;

}

else

{

MessageBox("Password OK");

}

CDialog::OnOK();

}

Листинг 5 Исходный текст ядра защитного механизма crackme 01

Кажется, никаких сюрпризов не предвидится. Что ж, вводим пароль (как обычно "KPNC Kaspersky++"), выслушиваем "ругательство" и, до нажатия ОК, вызываем отладчик, переключаем контекст…

:s 23:0 L -1 'KPNC Kaspersky'

Pattern found at 0023:0012F9FC (0012F9FC)

:s

Pattern found at 0023:00139C78 (00139C78)

Есть два вхождения! И оба лежат в стеке. Подбросим монетку, чтобы определить с какого из них начать? (Правильный ответ – с первого). Устанавливаем точку останова и терпеливо ждем всплытия отладчика. Всплытие ждать себя не заставляет, но показывает какой-то странный, откровенно "левый" код. Ждем "x" для выхода, - следует целый каскад всплытий одно непонятнее другого.



Лихорадочно подергивая бородку (варианты – усики, волосы в разных местах) соображаем: функция "CCrackme_01Dlg::OnOK" вызывается непосредственно в момент нажатия на "ОК" – ей отводится часть стекового пространства под локальные переменные, которая автоматически "экспроприируется" при выходе из функции – переходя во всеобщее пользование. Таким образом, локальный буфер с введенным нами паролем существует только в момент его проверки, а потом автоматически затирается. Единственная зацепка – модальный диалог с ругательством. Пока он на экране – буфер еще содержит пароль и его можно найти в памяти. Но это не сильно помогает в отслеживании когда к этому буферу произведет обращение… Приходится терпеливо ждать, отсеивая ложные всплытия один за другим. Наконец, в окне данных искомая строка, а в окне кода – какой-то осмысленный код:

0023:0012F9FC 4B 50 4E 43 20 4B 61 73-70 65 72 73 6B 79 2B 2B  KPNC Kaspersky++

0023:0012FA0C 00 01 00 00 0D 00 00 00-01 00 1C C0 A8 AF 47 00  ..............G.

0023:0012FA1C 10 9B 13 00 78 01 01 00-F0 3E 2F 00 00 00 00 00  ....x....>/.....

0023:0012FA2C 01 01 01 00 83 63 E1 77-F0 AD 47 00 78 01 01 00  .....c.w..G.x...

001B:004013E3  8A10                MOV     DL,[EAX]

001B:004013E5  8A1E                MOV     BL,[ESI] 

001B:004013E7  8ACA                MOV     CL,DL

001B:004013E9  3AD3                CMP     DL,BL

001B:004013EB  751E                JNZ     0040140B

001B:004013ED  84C9                TEST    CL,CL

001B:004013EF  7416                JZ      00401407

001B:004013F1  8A5001              MOV     DL,[EAX+01]

На всякий "пожарный" смотрим, на что указывает ESI:

:d esi

0023:0040303C 4D 79 47 6F 6F 64 50 61-73 73 77 6F 72 64 00 00  MyGoodPassword..

Остается "пропадчить" исполняемый файл, и тут (как и следовало ожидать по закону бутерброда) нас ждут очередные трудности. Во-первых, хитрый компилятор заоптимизировал код, подставив код функции strcmp вместо ее вызова, а во-вторых, условных переходов… да ими все кишит! Попробуй-ка, найди нужный.


На этот раз бросать монетку мы не станем, а попытаемся подойти к делу по-научному. Итак, перед нами дизассемблированный код, точнее его ключевой фрагмент, осуществляющий анализ пароля:

>dumpbin /DISASM crackme_01.exe

  004013DA: BE 3C 30 40 00     mov         esi,40303Ch

  0040303C: 4D 79 47 6F 6F 64 50 61 73 73 77 6F 72 64 00 MyGoodPassword

В регистр ESI помещается указатель на оригинальный пароль

  004013DF: 8D 44 24 10        lea         eax,[esp+10h]

В регистр EAX – указатель на пароль, введенный пользователем

  004013E3: 8A 16              mov         dl,byte ptr [esi]

  004013E5: 8A 1E              mov         bl,byte ptr [esi]

  004013E7: 8A CA              mov         cl,dl

  004013E9: 3A D3              cmp         dl,bl

Проверка первого символа на совпадение

  004013EB: 75 1E              jne         0040140B ß---(3) ---à (1)

Первый символ уже не совпадает – дальше проверять бессмысленно!

  004013ED: 84 C9              test        cl,cl

Первый символ первой строки  равен нулю?

  004013EF: 74 16              je          00401407 --à

(2)

Да, достигнут конец строки – значит, строки идентичны

  004013F1: 8A 50 01           mov         dl,byte ptr [eax+1]

  004013F4: 8A 5E 01           mov         bl,byte ptr [esi+1]

  004013F7: 8A CA              mov         cl,dl

  004013F9: 3A D3              cmp         dl,bl

Проверяем следующую пару символов

  004013FB: 75 0E              jne         0040140B ---à (1)

Если не равна – конец проверке

  004013FD: 83 C0 02           add         eax,2

  00401400: 83 C6 02           add         esi,2

Перемещаем указатели строк на два символа вперед

  00401403: 84 C9              test        cl,cl

Достигнут конец строки?

  00401405: 75 DC              jne         004013E3 -à (3)

Нет, еще не конец, сравниваем дальше.

  00401407: 33 C0              xor         eax,eax ß--- (2)



  00401409: EB

05              jmp         00401410 --à

(4)

Обнуляем EAX (strcmp в случае успеха возвращает ноль) и выходим

  0040140B: 1B C0              sbb         eax,eax ß--- (3)

  0040140D: 83 D8 FF           sbb         eax,0FFFFFFFFh

Эта ветка получат управление при несовпадении строк. EAX устанавливает равным в ненулевое значение (подумайте почему).

  00401410: 85 C0              test        eax,eax ß--- (4)

Проверка значения EAX на равенство нулю

  00401412: 6A 00              push        0

  00401414: 6A 00              push        0

Что-то заносим в стек…

  00401416: 74 38              je         00401450 <<<< ---à(5)

Прыгаем куда-то….

  00401418: 68 2C 30 40 00     push        40302Ch

  0040302C: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 00 .Wrong password

Ага, "Вронг пысворд". Значит, прыгать все-таки надо…. Смотрим, куда указывает je (а код ниже – уже не представляет интереса – и так ясно: это "матюгальщик").

Теперь, когда алгоритм защиты в общих чертах ясен, можно ее и сломать, например, поменяв условный переход в строке 0x401416 на безусловный jump short (код 0xEB).


Содержание раздела