В данной статье сделана попытка подробно описать исследование одного занимательного crackme. Занимательным он будет в первую очередь для новичков в реверс-инжиниринге (коим и я являюсь, и на которых в основном рассчитана статья), но возможно и более серьезные специалисты сочтут используемый в нем приём интересным. Для понимания происходящего потребуются базовые знания ассемблера и WinAPI.
Университетский курс ассемблера пробудил дремавший пару лет дух исследователя, зародившийся ещё в средней школе. Этому духу быстро были принесены в жертву несколько простеньких crackme, но вскоре на жестком диске нашелся экземпляр поинтересней. К сожалению, за давностью лет, первоисточник этого crackme найти не удалось, поэтому залил архив на Google Drive.
Применявшиеся инструменты:
Внешний вид главного окна подопытного очень прост: два edit'a и несколько кнопочек. Причем первое поле ввода предназначено для того, чтобы дать возможность писать во второе, которое, судя по расположенной рядом кнопке Check, и будет основным препятствием на пути к регистрации крэкми.
Окошко About содержит одну интересную надпись «Patching is allowed, but where??», предваряющую самое интересное. Вся суть этой надписи раскроется в процессе исследования, а пока спойлерить не будем.
Кстати говоря, если кто-нибудь уже видел этот crackme и нашел способ его пропатчить (ну или если кому-то это показалось очень простым и он сделал это одной левой в процессе чтения статьи), то прошу рассказать всем об успехе, желательно еще и с комментариями.
Попытки вызвать хоть какую-то реакцию программы не увенчиваются успехом: никакой реакции на нажатия кнопок Enable и Check не следует. Однако бросается в глаза то, что в активном поле ввода можно писать только латиницей и ставить значок минус. Замечаем этот факт и начинаем работу.
Для победы данного crackme нам потребуется разобраться с двумя полями ввода.
Загрузив программу в OllyDbg, сразу замечаем интересный GetProcAddress неподалеку от точки входа:
Чтобы посмотреть, что же в неё передается, поставим бряк непосредственно на вызове GetProcAddress. Остановившись здесь, увидим, что в Procname «нарисовалась» функция SetWindowLongA, а в hModule — дескриптор user32.dll:
Вызов этой функции через GetProcAddress намекает, что от нас хотели его скрыть и тут происходит нечто важное. Чтобы разобраться, что именно там происходит, посмотрим на параметры, передаваемые этой функции. Это можно сделать очень быстро, поставив бряк на call eax, расположенный несколькими командами ниже.
После остановки на call eax можно повнимательней присмотреться к тому, что легло в стек. Olly любезно предлагает нам расшифровку передаваемых значений:
Беглый взгляд на описание функции и становится понятно, что тут происходит замена оконной процедуры одного из edit'ов. Чутье подсказывает, что это активный edit, но чтобы убедиться наверняка, нужно воспользоваться утилитой Zero Dump (оказавшийся под рукой аналог Spy++). Перетянув прицел Finder Tool на активный edit, обнаруживаем, что его дескриптор совпадает с переданным в SetWindowLongA.
При перезапуске крэкми это значение изменится, так что проверку надо проводить за один присест.
Теперь становится ясно, откуда ноги растут у фильтрации ввода, которую мы обнаружили ранее. Посмотрим, что еще интересного делает новая оконная процедура. Перейдем к адресу (Go To -> Expression или Ctrl+G в OllyDbg), который передавался параметром NewValue (40302b):
Константа 102, с которой происходит сравнение на 40302E, это WM_CHAR. Список всех констант, обозначающих тип сообщения, я смотрел в файледжентльменском студенческом наборе имелся.
Проследуем дальше по коду, как будто следующий прыжок JNE не выполняется. Код после JNE будет выполняться в случае прихода сообщения WM_CHAR, которое отправляется окну после нажатия (а точнее после того, как будет обработано сообщение WM_KEYDOWN) клавиши, в соответствие которой поставлен ASCII-код. В данном сообщении нас интересует wParam, в котором содержится код символа нажатой клавиши. Далее происходит запись этого значения в EAX и начинается серия проверок, не допускающих ввод чего-то кроме латиницы и минуса.
Интересна реакция на backspace (код 8 в ASCII): на 403056 происходит проверка некоторого значения в памяти на равенство нулю и декрмент этого значения на единицу в случае, если там не ноль. Такое поведение означает, что происходит подсчет длины введенной строки. И да, это действительно происходит чуть далее (последняя команда):
Также тут хорошо видно, что введенный символ записывается в память в конец уже введенной последовательности. В вычислении места для записи символа фигурирует тот же адрес (4050D5), значение по которому является длиной введенной строки.
Вывод: для поиска места, где происходит проверка введенных данных, не поможет поиск подходящего GetDlgItemText (так как его здесь и не будет), зато поможет бряк на памяти, в которую происходит запись.
Итак, установим бряк на чтение нескольких байтов, начиная с адреса 405070. Также имеет смысл перестраховаться и поставить брейкпоинт ещё и на чтение количества введенных символом (ранее установлено, что это 4050D5). Второе сделать лучше всего непосредственно перед нажатием Check, чтобы не отвлекаться на остановки во время ввода строки. Теперь запустим программу, введем что-нибудь в первый edit и нажмем на Check.
Видим, что остановка произошла на записи количества введенных символов в EAX, а следующей командой идет сравнение 11-ого символа (405070 + A) введенной строки с минусом (2D — код минуса в ASCII).
Хорошо заметно, что прыжок нам после сравнения не нужен, так как далее (с 402019) запускается некоторый цикл, который нам вряд ли хочется пропускать. Проверим это предположение и сфальсифицируем результат сравнения 11-ого символа с минусом через модификацию флага ZF (ну или просто перезапустим проверку с подходящей для этого сравнения строкой).
В данном цикле происходит проверка каждого третьего введенного символа до первого нуля и сложение кодов этих символов с регистром EAX, в котором, как мы помним, лежит длина введенной строки. На 4020300 вместо минуса записывается ноль и проверка завершается.
Проследуем по RETN.
Первой командой видим сравнение значения EAX c 30A и прыжок куда-то в случае неравенства. Далее же видится желанное «Well done! Keep going ;-)». Теперь нужно посмотреть, как туда попасть. Рядом с заветным SetDlgItemTextA заметен call, в который нам обязательно нужно сходить. Чтобы попасть туда, необходимо пройти проверку на 30A. Проще всего это сделать модификацией флага ZF перед прыжком JNE.
Выполнив Call, видим следующее:
Четыре GetProcAddress, в которые передаются части введенной нами строки! Первый раз передается адрес начала строки, второй — адрес 12 символа, и далее снова начало и снова 12-ый символ. Как мы помним, на место 11 символа (которым должен быть минус) записывается ноль. Теперь понятно, что это нужно для того, чтобы передавать в GetProcAddress null-terminated строку.
Теперь можно поставить вполне определенную задачу: найти две функции из user32.dll (об этом свидетельствует хэндл, передаваемый GetProcAddress), у каждой из которых два параметра (количество push'ей перед call eax), причем первая имеет длину десять символов.
Сузим круг поиска, посмотрев на передаваемые параметры, и сопоставив их с тем, что должно произойти после нажатия кнопки Enable. Мы уже видели SetDlgItemTextA с подбадривающей надписью, и есть вероятность, что она отобразится в первом поле ввода, так как второе нам еще нужно. А второе поле надо бы сделать активным, мы ведь все-таки на кнопку Enable жмем!
Чтобы быстро посмотреть на все функции, имеющие длину 10 символов, из user32.dll, я использовал поиск во встроенном редакторе фара по файлу
Осталось только активировать второе поле ввода. За два параметра это отлично сможет сделать EnableWindow.
Проверка найденной строки GetDlgItem-EnableWindow и… успех!
С первым полем ввода разобрались, переходим к «основному блюду».
Так как замена оконной процедуры выполнялась только для одного edit'a, текст из второго должен (во всяком случае есть основания на это надеяться) извлекаться более тривиальным путем. Воспользуемся командой Search for -> All intermodular calls и посмотрим, что может использоваться для получения текста из второго поля ввода.
Самым подходящим кандидатом на функцию, достающую текст из второго поля ввода, кажется один из GetProcAddress, который вероятно получает адрес GetDlgItemTextA. Чтобы убедиться в этом, запустим программу, разблокируем второе поле найденной строкой, а затем поставим бряки на все три GetProcAddress. Теперь можно нажать на Check и убедиться, что прогнозы оказались верны.
Далее происходит проверка количества введенных символов (GetDlgItemText возвращает его в EAX), запись этого значения в память и вызываются четыре функции. Вероятнее всего, именно в этих четырех функциях происходят какие-то действия со строкой, поэтому исследуем их по очереди.
Заглянув в первый call, наблюдаем некоторые махинации с символами в строке, выполняемые в цикле до обнаружения первого нуля (402126):
Присмотревшись к константам, которые тут встречаются, а потом к таблице ASCII, легко заметить, что этот call инвертирует регистр каждого из введенных символов. Тут все оказалось очень просто.
Второй call чуть побольше, и в нём совершается несколько действий. Первым делом происходит копирование введенной строки задом наперед в другое место в памяти с помощью следующего цикла:
Далее происходит то же самое с исходной строкой, однако инвертируется только 10 символов. Это может указывать на то, что в дальнейшем только они и будут использоваться, но загадывать рано.
Ну и наконец выполняется запись на 11-ую позицию исходной строки символа 'A':
Вспомнив, какой прием уже использовался в этом крэкми, можно предположить, что и тут будет нечто похожее, однако WinAPI функция будет вводиться не в готовом виде.
CALL 00401285 выводит нас еще на один call:
Тут происходят манипуляции с памятью, которая не связана ни с исходной строкой, ни с её копией, поэтому не вникая в тонкости происходящего, посмотрим, что происходит после возврата. А после возврата происходит обработка копии введенной строки с помощью xor'a. Скорее всего что-то из этих двух строк должно в итоге оказаться WinAPI функцией, а что-то — сообщением об успешной регистрации.
Всё прояснит последний call.
Осталось совсем чуть-чуть: снова решить задачку по поиску подходящей WinAPI функции. Вот, что нам о ней известно:
Для поиска подходящей функции я использовал поиск по файлу
Вспомнив, какие операции выполняются с ней перед передачей в GetProcAddress, преобразуем соответствующим образом: MessageBox — (инверсия регистра) — mESSAGEbOX — (инверсия порядка символов) — XObEGASSEm.
Введем заветную строку во второе поле ввода и получаем победный MessageBox с заголовком окна, напоминающем о приключениях!
В исследованном крэкми демонстрируется интересный способ прочной «привязки» защиты к работе самой программы. Используемый механизм достаточно сильно затрудняет патчинг. Мне трудно представить себе применение такого способа в коммерческой защите (однако буду рад, если кто-то расскажет о примерах), но для головоломки на пару деньков она очень даже хороша, особенно для начинающих.
Предисловие
Университетский курс ассемблера пробудил дремавший пару лет дух исследователя, зародившийся ещё в средней школе. Этому духу быстро были принесены в жертву несколько простеньких crackme, но вскоре на жестком диске нашелся экземпляр поинтересней. К сожалению, за давностью лет, первоисточник этого crackme найти не удалось, поэтому залил архив на Google Drive.
Применявшиеся инструменты:
- Отладчик (я использовал OllyDbg)
- Утилита для получения дескрипторов окон запущенных программ (например, Zero Dump или поставляющийся с Visual Studio Spy++)
- Некоторые файлы, входящие в архив fasm'a
Первый взгляд на противника
Внешний вид главного окна подопытного очень прост: два edit'a и несколько кнопочек. Причем первое поле ввода предназначено для того, чтобы дать возможность писать во второе, которое, судя по расположенной рядом кнопке Check, и будет основным препятствием на пути к регистрации крэкми.
Окошко About содержит одну интересную надпись «Patching is allowed, but where??», предваряющую самое интересное. Вся суть этой надписи раскроется в процессе исследования, а пока спойлерить не будем.
Кстати говоря, если кто-нибудь уже видел этот crackme и нашел способ его пропатчить (ну или если кому-то это показалось очень простым и он сделал это одной левой в процессе чтения статьи), то прошу рассказать всем об успехе, желательно еще и с комментариями.
Попытки вызвать хоть какую-то реакцию программы не увенчиваются успехом: никакой реакции на нажатия кнопок Enable и Check не следует. Однако бросается в глаза то, что в активном поле ввода можно писать только латиницей и ставить значок минус. Замечаем этот факт и начинаем работу.
В бой!
Для победы данного crackme нам потребуется разобраться с двумя полями ввода.
Первый edit и кнопка Enable
Загрузив программу в OllyDbg, сразу замечаем интересный GetProcAddress неподалеку от точки входа:
00401086 |. 68 56504000 PUSH OFFSET 00405056 ; /Procname = ""10,"&7",14,"*-',4",0F,",-$",02 0040108B |. FF35 9C504000 PUSH [DWORD DS:40509C] ; |hModule = NULL 00401091 |. E8 C0020000 CALL <JMP.&KERNEL32.GetProcAddress> ; \KERNEL32.GetProcAddress
Чтобы посмотреть, что же в неё передается, поставим бряк непосредственно на вызове GetProcAddress. Остановившись здесь, увидим, что в Procname «нарисовалась» функция SetWindowLongA, а в hModule — дескриптор user32.dll:
00401086 |. 68 56504000 PUSH OFFSET 00405056 ; /Procname = "SetWindowLongA" 0040108B |. FF35 9C504000 PUSH [DWORD DS:40509C] ; |hModule = 767F0000 ('USER32') 00401091 |. E8 C0020000 CALL <JMP.&KERNEL32.GetProcAddress> ; \KERNEL32.GetProcAddress
Вызов этой функции через GetProcAddress намекает, что от нас хотели его скрыть и тут происходит нечто важное. Чтобы разобраться, что именно там происходит, посмотрим на параметры, передаваемые этой функции. Это можно сделать очень быстро, поставив бряк на call eax, расположенный несколькими командами ниже.
Почему call eax?
По соглашению вызова stdcall, используемого в WinAPI функциях, целочисленное значение (т. е. адрес нужной функции), которое вернет GetProcAddress, должно находиться в регистре eax.
После остановки на call eax можно повнимательней присмотреться к тому, что легло в стек. Olly любезно предлагает нам расшифровку передаваемых значений:
0018FC04 /00020876 ; |hWnd = 00020876, class = Edit 0018FC08 |FFFFFFFC ; |Index = GWL_WNDPROC 0018FC0C |0040302B ; \NewValue = Chiwaka1.40302B
Беглый взгляд на описание функции и становится понятно, что тут происходит замена оконной процедуры одного из edit'ов. Чутье подсказывает, что это активный edit, но чтобы убедиться наверняка, нужно воспользоваться утилитой Zero Dump (оказавшийся под рукой аналог Spy++). Перетянув прицел Finder Tool на активный edit, обнаруживаем, что его дескриптор совпадает с переданным в SetWindowLongA.
При перезапуске крэкми это значение изменится, так что проверку надо проводить за один присест.
Теперь становится ясно, откуда ноги растут у фильтрации ввода, которую мы обнаружили ранее. Посмотрим, что еще интересного делает новая оконная процедура. Перейдем к адресу (Go To -> Expression или Ctrl+G в OllyDbg), который передавался параметром NewValue (40302b):
0040302B 55 PUSH EBP 0040302C 8BEC MOV EBP,ESP 0040302E 817D 0C 0201000 CMP [DWORD SS:EBP+0C],102 00403035 75 61 JNE SHORT 00403098 00403037 8B45 10 MOV EAX,[DWORD SS:EBP+10]
Константа 102, с которой происходит сравнение на 40302E, это WM_CHAR. Список всех констант, обозначающих тип сообщения, я смотрел в файле
%fasm_directory%\INCLUDE\EQUATES\USER32.INC
(ссылка на архив с fasm'ом в списке применявшихся инструментов), так как интернета под рукой не было, а вот fasm в Проследуем дальше по коду, как будто следующий прыжок JNE не выполняется. Код после JNE будет выполняться в случае прихода сообщения WM_CHAR, которое отправляется окну после нажатия (а точнее после того, как будет обработано сообщение WM_KEYDOWN) клавиши, в соответствие которой поставлен ASCII-код. В данном сообщении нас интересует wParam, в котором содержится код символа нажатой клавиши. Далее происходит запись этого значения в EAX и начинается серия проверок, не допускающих ввод чего-то кроме латиницы и минуса.
0040303A 3C 41 CMP AL,41 0040303C 72 04 JB SHORT 00403042 0040303E 3C 5A CMP AL,5A 00403040 76 10 JBE SHORT 00403052 00403042 3C 61 CMP AL,61 00403044 72 04 JB SHORT 0040304A 00403046 3C 7A CMP AL,7A 00403048 76 08 JBE SHORT 00403052 0040304A 3C 08 CMP AL,8 0040304C 74 04 JE SHORT 00403052 0040304E 3C 2D CMP AL,2D 00403050 75 61 JNE SHORT 004030B3 00403052 3C 08 CMP AL,8 00403054 75 12 JNE SHORT 00403068 00403056 833D D5504000 0 CMP [DWORD DS:4050D5],0 0040305D 74 09 JE SHORT 00403068 0040305F 832D D5504000 0 SUB [DWORD DS:4050D5],1
Интересна реакция на backspace (код 8 в ASCII): на 403056 происходит проверка некоторого значения в памяти на равенство нулю и декрмент этого значения на единицу в случае, если там не ноль. Такое поведение означает, что происходит подсчет длины введенной строки. И да, это действительно происходит чуть далее (последняя команда):
00403069 8B0D D5504000 MOV ECX,[DWORD DS:4050D5] 0040306F 8881 70504000 MOV [BYTE DS:ECX+405070],AL 00403075 8305 D5504000 0 ADD [DWORD DS:4050D5],1
Также тут хорошо видно, что введенный символ записывается в память в конец уже введенной последовательности. В вычислении места для записи символа фигурирует тот же адрес (4050D5), значение по которому является длиной введенной строки.
Вывод: для поиска места, где происходит проверка введенных данных, не поможет поиск подходящего GetDlgItemText (так как его здесь и не будет), зато поможет бряк на памяти, в которую происходит запись.
Итак, установим бряк на чтение нескольких байтов, начиная с адреса 405070. Также имеет смысл перестраховаться и поставить брейкпоинт ещё и на чтение количества введенных символом (ранее установлено, что это 4050D5). Второе сделать лучше всего непосредственно перед нажатием Check, чтобы не отвлекаться на остановки во время ввода строки. Теперь запустим программу, введем что-нибудь в первый edit и нажмем на Check.
Скрытый текст
Байты, расположенные в месте срабатывания бряка (а также много еще где в этом крэкми), идут в такой последовательности, что она может интерпретироваться по-разному. Если попытаться сместиться выше по командам, то место срабатывания бряка переанализируется и станет выглядеть по-другому. Чтобы исправить это, достаточно командой Go to -> Expression (Ctrl+G) перейти на адрес срабатывания бряка.
Видим, что остановка произошла на записи количества введенных символов в EAX, а следующей командой идет сравнение 11-ого символа (405070 + A) введенной строки с минусом (2D — код минуса в ASCII).
00402007 A1 D5504000 MOV EAX,[DWORD DS:4050D5] <--memory breakpoint when reading 4050D5 0040200C 803D 7A504000 2 CMP [BYTE DS:40507A],2D 00402013 75 1E JNE SHORT 00402033 00402015 33C9 XOR ECX,ECX 00402017 33DB XOR EBX,EBX 00402019 80B9 70504000 0 CMP [BYTE DS:ECX+405070],0 00402020 74 11 JE SHORT 00402033 00402022 8A99 70504000 MOV BL,[BYTE DS:ECX+405070] 00402028 03C3 ADD EAX,EBX 0040202A 83C1 03 ADD ECX,3 0040202D EB EA JMP SHORT 00402019 0040202F 33C0 XOR EAX,EAX 00402031 5E POP ESI 00402032 58 POP EAX 00402033 C605 7A504000 0 MOV [BYTE DS:40507A],0 0040203A C3 RETN
Хорошо заметно, что прыжок нам после сравнения не нужен, так как далее (с 402019) запускается некоторый цикл, который нам вряд ли хочется пропускать. Проверим это предположение и сфальсифицируем результат сравнения 11-ого символа с минусом через модификацию флага ZF (ну или просто перезапустим проверку с подходящей для этого сравнения строкой).
В данном цикле происходит проверка каждого третьего введенного символа до первого нуля и сложение кодов этих символов с регистром EAX, в котором, как мы помним, лежит длина введенной строки. На 4020300 вместо минуса записывается ноль и проверка завершается.
Проследуем по RETN.
004010F8 /. 3D 0A030000 CMP EAX,30A 004010FD |. 0F85 C6000000 JNE 004011C9 00401103 |. FF75 08 PUSH [DWORD SS:EBP+8] 00401106 |. E8 380F0000 CALL 00402043 0040110B |. C605 A4504000 MOV [BYTE DS:4050A4],1 00401112 |. 68 12504000 PUSH OFFSET 00405012 ; /Text = "Well done! Keep going ;-)" 00401117 |. 6A 65 PUSH 65 ; |ControlID = 101. 00401119 |. FF75 08 PUSH [DWORD SS:EBP+8] ; |hDialog => [ARG.EBP+8] 0040111C |. E8 23020000 CALL <JMP.&USER32.SetDlgItemTextA> ; \USER32.SetDlgItemTextA
Первой командой видим сравнение значения EAX c 30A и прыжок куда-то в случае неравенства. Далее же видится желанное «Well done! Keep going ;-)». Теперь нужно посмотреть, как туда попасть. Рядом с заветным SetDlgItemTextA заметен call, в который нам обязательно нужно сходить. Чтобы попасть туда, необходимо пройти проверку на 30A. Проще всего это сделать модификацией флага ZF перед прыжком JNE.
Выполнив Call, видим следующее:
00402043 55 PUSH EBP 00402044 8BEC MOV EBP,ESP 00402046 53 PUSH EBX 00402047 68 70504000 PUSH OFFSET 00405070 ; начало введенной строки 0040204C FF35 9C504000 PUSH [DWORD DS:40509C] 00402052 E8 FFF2FFFF CALL <JMP.&KERNEL32.GetProcAddress> ; Jump to kernel32.GetProcAddress 00402057 6A 66 PUSH 66 00402059 FF75 08 PUSH [DWORD SS:EBP+8] 0040205C FFD0 CALL EAX 0040205E 50 PUSH EAX 0040205F 68 7B504000 PUSH OFFSET 0040507B ; 12-ый символ введенной строки 00402064 FF35 9C504000 PUSH [DWORD DS:40509C] 0040206A E8 E7F2FFFF CALL <JMP.&KERNEL32.GetProcAddress> ; Jump to kernel32.GetProcAddress 0040206F 5B POP EBX 00402070 53 PUSH EBX 00402071 FFD0 CALL EAX 00402073 68 70504000 PUSH OFFSET 00405070 ; начало введенной строки 00402078 FF35 9C504000 PUSH [DWORD DS:40509C] 0040207E E8 D3F2FFFF CALL <JMP.&KERNEL32.GetProcAddress> ; Jump to kernel32.GetProcAddress 00402083 68 E8030000 PUSH 3E8 00402088 FF75 08 PUSH [DWORD SS:EBP+8] 0040208B FFD0 CALL EAX 0040208D 50 PUSH EAX 0040208E 68 7B504000 PUSH OFFSET 0040507B ; 12-ый символ введенной строки 00402093 FF35 9C504000 PUSH [DWORD DS:40509C] 00402099 E8 B8F2FFFF CALL <JMP.&KERNEL32.GetProcAddress> ; Jump to kernel32.GetProcAddress 0040209E 5B POP EBX 0040209F 6A 00 PUSH 0 004020A1 53 PUSH EBX 004020A2 FFD0 CALL EAX
Четыре GetProcAddress, в которые передаются части введенной нами строки! Первый раз передается адрес начала строки, второй — адрес 12 символа, и далее снова начало и снова 12-ый символ. Как мы помним, на место 11 символа (которым должен быть минус) записывается ноль. Теперь понятно, что это нужно для того, чтобы передавать в GetProcAddress null-terminated строку.
Теперь можно поставить вполне определенную задачу: найти две функции из user32.dll (об этом свидетельствует хэндл, передаваемый GetProcAddress), у каждой из которых два параметра (количество push'ей перед call eax), причем первая имеет длину десять символов.
Сузим круг поиска, посмотрев на передаваемые параметры, и сопоставив их с тем, что должно произойти после нажатия кнопки Enable. Мы уже видели SetDlgItemTextA с подбадривающей надписью, и есть вероятность, что она отобразится в первом поле ввода, так как второе нам еще нужно. А второе поле надо бы сделать активным, мы ведь все-таки на кнопку Enable жмем!
Чтобы быстро посмотреть на все функции, имеющие длину 10 символов, из user32.dll, я использовал поиск во встроенном редакторе фара по файлу
%fasm_directory%\INCLUDE\API\USER32.INC
. Для поиска применил регэксп '\s\i{10}\,'
. Можно было далее выбирать из найденных функций те, которые имею два параметра, но в этом не было необходимости, так как «по смыслу» идеально подходит GetDlgItem. Подтверждением этой мысли служит «волшебная константа» 66h, являющаяся ID второго, пока еще неактивного поля ввода (это можно увидеть с помощью zDump или аналогичной утилиты). Это значение является первым параметром у GetDlgItem.Осталось только активировать второе поле ввода. За два параметра это отлично сможет сделать EnableWindow.
Проверка найденной строки GetDlgItem-EnableWindow и… успех!
С первым полем ввода разобрались, переходим к «основному блюду».
Второй edit и решающий удар по кнопке Check
Так как замена оконной процедуры выполнялась только для одного edit'a, текст из второго должен (во всяком случае есть основания на это надеяться) извлекаться более тривиальным путем. Воспользуемся командой Search for -> All intermodular calls и посмотрим, что может использоваться для получения текста из второго поля ввода.
All intermodular calls
0040103C CALL <JMP.&USER32.DialogBoxParamA> 7684CB0C USER32.DialogBoxParamA TemplateName = 1, hParent = NULL, DialogProc = Chiwaka1.401061, InitParam = 0 004011B2 CALL <JMP.&USER32.DialogBoxParamA> 7684CB0C USER32.DialogBoxParamA TemplateName = 2, hParent = NULL, DialogProc = Chiwaka1.401206, InitParam = 0 004011C4 CALL <JMP.&USER32.EndDialog> 7682B99C USER32.EndDialog Result = 0 00401214 CALL <JMP.&USER32.EndDialog> 7682B99C USER32.EndDialog Result = 1 0040123D CALL <JMP.&USER32.EndDialog> 7682B99C USER32.EndDialog Result = 1 0040104C CALL <JMP.&KERNEL32.ExitProcess> 765779C8 kernel32.ExitProcess 00401072 CALL <JMP.&USER32.GetDlgItem> 7682F1BA USER32.GetDlgItem ItemID = 101. 00401002 CALL <JMP.&KERNEL32.GetModuleHandleA> 76571245 kernel32.GetModuleHandleA ModuleName = NULL 00401091 CALL <JMP.&KERNEL32.GetProcAddress> 76571222 kernel32.GetProcAddress Procname = ""10,"&7",14,"*-',4",0F,",-$",02 00401168 CALL <JMP.&KERNEL32.GetProcAddress> 76571222 kernel32.GetProcAddress Procname = ""04,"&7",07,"/$",LF,"7&.",17,"&;7",02 00401268 CALL <JMP.&KERNEL32.GetProcAddress> 76571222 kernel32.GetProcAddress 004010C4 CALL <JMP.&USER32.SendMessageA> 7681612E USER32.SendMessageA Msg = WM_COMMAND 0040111C CALL <JMP.&USER32.SetDlgItemTextA> 7681C4D6 USER32.SetDlgItemTextA ControlID = 101., Text = "Well done! Keep going ;-)" 004011DC CALL <JMP.&USER32.SetDlgItemTextA> 7681C4D6 USER32.SetDlgItemTextA ControlID = 101., Text = "Careful now !!"
Самым подходящим кандидатом на функцию, достающую текст из второго поля ввода, кажется один из GetProcAddress, который вероятно получает адрес GetDlgItemTextA. Чтобы убедиться в этом, запустим программу, разблокируем второе поле найденной строкой, а затем поставим бряки на все три GetProcAddress. Теперь можно нажать на Check и убедиться, что прогнозы оказались верны.
0040115D |> \68 46504000 PUSH OFFSET 00405046 ; /Procname = "GetDlgItemTextA" 00401162 |. FF35 9C504000 PUSH [DWORD DS:40509C] ; |hModule = 767F0000 ('USER32') 00401168 |. E8 E9010000 CALL <JMP.&KERNEL32.GetProcAddress> ; \KERNEL32.GetProcAddress 0040116D |. 6A 40 PUSH 40 0040116F |. 68 70504000 PUSH OFFSET 00405070 00401174 |. 6A 66 PUSH 66 00401176 |. FF75 08 PUSH [DWORD SS:EBP+8] 00401179 |. FFD0 CALL EAX
Далее происходит проверка количества введенных символов (GetDlgItemText возвращает его в EAX), запись этого значения в память и вызываются четыре функции. Вероятнее всего, именно в этих четырех функциях происходят какие-то действия со строкой, поэтому исследуем их по очереди.
Заглянув в первый call, наблюдаем некоторые махинации с символами в строке, выполняемые в цикле до обнаружения первого нуля (402126):
CALL 00402101
00402101 8D05 70504000 LEA EAX,[405070] ; введенная строка 00402107 EB 1D JMP SHORT 00402126 00402109 8038 40 CMP [BYTE DS:EAX],40 ; 40h = @ (последний символ в ASCII перед латиницей в верхнем регистре) 0040210C 76 0A JBE SHORT 00402118 0040210E 8038 5B CMP [BYTE DS:EAX],5B ; 5Bh = [ (первый символ после латиницы в верхнем регистре) 00402111 73 05 JAE SHORT 00402118 00402113 8000 20 ADD [BYTE DS:EAX],20 ; 20h - разница между буквой в разных регистрах 00402116 EB 0D JMP SHORT 00402125 00402118 8038 60 CMP [BYTE DS:EAX],60 ; 60h = ' (последний символ перед латиницей в нижнем регистре) 0040211B 76 08 JBE SHORT 00402125 0040211D 8038 7B CMP [BYTE DS:EAX],7B ; 7Bh = { (первый символ после латиницы в нижнем регистре) 00402120 73 03 JAE SHORT 00402125 00402122 8028 20 SUB [BYTE DS:EAX],20 ; 20h - разница между буквой в разных регистрах 00402125 40 INC EAX 00402126 8038 00 CMP [BYTE DS:EAX],0 00402129 75 DE JNE SHORT 00402109 0040212B C3 RETN
Присмотревшись к константам, которые тут встречаются, а потом к таблице ASCII, легко заметить, что этот call инвертирует регистр каждого из введенных символов. Тут все оказалось очень просто.
CALL 00402133
00402133 53 PUSH EBX 00402134 33DB XOR EBX,EBX 00402136 33D2 XOR EDX,EDX 00402138 8D05 70504000 LEA EAX,[405070] ; введенная строка 0040213E 50 PUSH EAX 0040213F 8B0D A5504000 MOV ECX,[DWORD DS:4050A5] 00402145 51 PUSH ECX 00402146 49 DEC ECX 00402147 8A1401 MOV DL,[BYTE DS:EAX+ECX] 0040214A 8893 A9504000 MOV [BYTE DS:EBX+4050A9],DL 00402150 43 INC EBX 00402151 83F9 00 CMP ECX,0 00402154 ^ 75 F0 JNE SHORT 00402146 00402156 59 POP ECX 00402157 58 POP EAX 00402158 49 DEC ECX 00402159 33DB XOR EBX,EBX 0040215B 33D2 XOR EDX,EDX 0040215D 33C0 XOR EAX,EAX 0040215F EB 1A JMP SHORT 0040217B 00402161 8A99 70504000 MOV BL,[BYTE DS:ECX+405070] ; введенная строка 00402167 8A90 70504000 MOV DL,[BYTE DS:EAX+405070] ; введенная строка 0040216D 8891 70504000 MOV [BYTE DS:ECX+405070],DL 00402173 8898 70504000 MOV [BYTE DS:EAX+405070],BL 00402179 49 DEC ECX 0040217A 40 INC EAX 0040217B 83F8 05 CMP EAX,5 0040217E ^ 72 E1 JB SHORT 00402161 00402180 C605 7A504000 4 MOV [BYTE DS:40507A],41 00402187 5B POP EBX 00402188 C3 RETN
Второй call чуть побольше, и в нём совершается несколько действий. Первым делом происходит копирование введенной строки задом наперед в другое место в памяти с помощью следующего цикла:
00402146 49 DEC ECX 00402147 8A1401 MOV DL,[BYTE DS:EAX+ECX] 0040214A 8893 A9504000 MOV [BYTE DS:EBX+4050A9],DL 00402150 43 INC EBX 00402151 83F9 00 CMP ECX,0 00402154 ^ 75 F0 JNE SHORT 00402146
Далее происходит то же самое с исходной строкой, однако инвертируется только 10 символов. Это может указывать на то, что в дальнейшем только они и будут использоваться, но загадывать рано.
Ну и наконец выполняется запись на 11-ую позицию исходной строки символа 'A':
00402180 C605 7A504000 4 MOV [BYTE DS:40507A],41
Вспомнив, какой прием уже использовался в этом крэкми, можно предположить, что и тут будет нечто похожее, однако WinAPI функция будет вводиться не в готовом виде.
CALL 00401285 выводит нас еще на один call:
CALL 004012C4
004012C4 /$ 8D05 BD504000 LEA EAX,[4050BD] 004012CA |. 33D2 XOR EDX,EDX 004012CC |. 8A15 F0204000 MOV DL,[BYTE DS:4020F0] 004012D2 |. 8810 MOV [BYTE DS:EAX],DL 004012D4 |. 8A15 31214000 MOV DL,[BYTE DS:402131] 004012DA |. 8850 01 MOV [BYTE DS:EAX+1],DL 004012DD |. 8A15 AE204000 MOV DL,[BYTE DS:4020AE] 004012E3 |. 8850 02 MOV [BYTE DS:EAX+2],DL 004012E6 |. 8A15 CF204000 MOV DL,[BYTE DS:4020CF] 004012EC |. 8850 03 MOV [BYTE DS:EAX+3],DL 004012EF |. 8A15 41204000 MOV DL,[BYTE DS:402041] 004012F5 |. 8850 04 MOV [BYTE DS:EAX+4],DL 004012F8 |. 8A15 40204000 MOV DL,[BYTE DS:402040] 004012FE |. 8850 05 MOV [BYTE DS:EAX+5],DL 00401301 |. 8A15 31214000 MOV DL,[BYTE DS:402131] 00401307 |. 8850 06 MOV [BYTE DS:EAX+6],DL 0040130A |. 8A15 05204000 MOV DL,[BYTE DS:402005] 00401310 |. 8850 07 MOV [BYTE DS:EAX+7],DL 00401313 |. 8A15 83124000 MOV DL,[BYTE DS:401283] 00401319 |. 8850 08 MOV [BYTE DS:EAX+8],DL 0040131C |. 8A15 5B124000 MOV DL,[BYTE DS:40125B] 00401322 |. 8850 09 MOV [BYTE DS:EAX+9],DL 00401325 \. C3 RETN
Тут происходят манипуляции с памятью, которая не связана ни с исходной строкой, ни с её копией, поэтому не вникая в тонкости происходящего, посмотрим, что происходит после возврата. А после возврата происходит обработка копии введенной строки с помощью xor'a. Скорее всего что-то из этих двух строк должно в итоге оказаться WinAPI функцией, а что-то — сообщением об успешной регистрации.
Всё прояснит последний call.
CALL 0040125D
0040125D /$ 68 70504000 PUSH OFFSET 00405070 ; /Procname = "введенная строка задом-наперед с инвертированным регистром и символом 'A' на конце" 00401262 |. FF35 9C504000 PUSH [DWORD DS:40509C] ; |hModule = 767F0000 ('USER32') 00401268 |. E8 E9000000 CALL <JMP.&KERNEL32.GetProcAddress> ; \KERNEL32.GetProcAddress 0040126D |. 6A 30 PUSH 30 0040126F |. 68 BB124000 PUSH 004012BB ; ASCII "API-API" 00401274 |. 68 A9504000 PUSH OFFSET 004050A9 ; ASCII "введенная строка задом-наперед с инвертированным регистром, к которой применен xor" 00401279 |. 6A 00 PUSH 0 0040127B |. FFD0 CALL EAX 0040127D \. C3 RETN
Осталось совсем чуть-чуть: снова решить задачку по поиску подходящей WinAPI функции. Вот, что нам о ней известно:
- длина имени — 11 символов, на конце буква A
- находится в user32.dll
- передается четыре параметра, из них два параметра — строки
Для поиска подходящей функции я использовал поиск по файлу
%fasm_directory%\INCLUDE\PCOUNT\USER32.INC
по следующему регулярному выражению '^\i{10}\%\s\=\s\s4'
. В данном файле не дублируются ANSI-версии функций, поэтому большую A в конце указывать нельзя. Подходяших функций оказалось не так уж и много, а по смыслу отлично подходит лишь одна: MessageBoxA.Вспомнив, какие операции выполняются с ней перед передачей в GetProcAddress, преобразуем соответствующим образом: MessageBox — (инверсия регистра) — mESSAGEbOX — (инверсия порядка символов) — XObEGASSEm.
Введем заветную строку во второе поле ввода и получаем победный MessageBox с заголовком окна, напоминающем о приключениях!
Вместо заключения
В исследованном крэкми демонстрируется интересный способ прочной «привязки» защиты к работе самой программы. Используемый механизм достаточно сильно затрудняет патчинг. Мне трудно представить себе применение такого способа в коммерческой защите (однако буду рад, если кто-то расскажет о примерах), но для головоломки на пару деньков она очень даже хороша, особенно для начинающих.