Pull to refresh

Исследование Crackme Chiwaka1

Reading time14 min
Views15K
imageВ данной статье сделана попытка подробно описать исследование одного занимательного crackme. Занимательным он будет в первую очередь для новичков в реверс-инжиниринге (коим и я являюсь, и на которых в основном рассчитана статья), но возможно и более серьезные специалисты сочтут используемый в нем приём интересным. Для понимания происходящего потребуются базовые знания ассемблера и WinAPI.

Предисловие


Университетский курс ассемблера пробудил дремавший пару лет дух исследователя, зародившийся ещё в средней школе. Этому духу быстро были принесены в жертву несколько простеньких 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 с заголовком окна, напоминающем о приключениях!



Вместо заключения


В исследованном крэкми демонстрируется интересный способ прочной «привязки» защиты к работе самой программы. Используемый механизм достаточно сильно затрудняет патчинг. Мне трудно представить себе применение такого способа в коммерческой защите (однако буду рад, если кто-то расскажет о примерах), но для головоломки на пару деньков она очень даже хороша, особенно для начинающих.
Tags:
Hubs:
+21
Comments0

Articles