Как решить crackme шаг за шагом: Crackme 001 в x64dbg
Автор: Diego J D Arias · Платформа: Windows x64 · Инструменты: x64dbg (IDA по желанию) · Уровень: начинающий
Скачать CrackMe
В этом руководстве мы решаем crackme, заранее не зная ключа: пробуем, ошибаемся и проверяем каждое предположение в отладчике. Идея в том, чтобы ты мог повторять всё с открытым бинарником и в конце получил метод, который подойдёт и для других crackme.
Что понадобится: crackme, x64dbg и калькулятор.
Смотрим на программу до запуска отладчика
Запускаем crackme, и появляется окно с четырьмя текстовыми полями и кнопкой Register. Очевидно, ключ разбит на четыре части.

Попробуем значения, которые потом будет легко узнать внутри кода, например:
777777777 888888888 999999999 111111111
Почему именно они? Потому что если позже мы увидим 7777 или 1111 в регистре, то сразу поймём, из какого поля оно пришло. Лучше избегать значений вроде 1234, повторённых во всех полях, так как их было бы трудно различить.
Если после нажатия Register ничего заметного не происходит или появляется сообщение об ошибке, нам нужно выяснить, что именно проверяется.
Совет: перед отладкой нужно попробовать программу и записать, что мы видим. Это даёт строки, размеры полей и сообщения, которые потом можно использовать для поиска ссылок.

Как добраться до функции проверки
Открываем crackme в x64dbg. Есть два быстрых способа добраться до функции, проверяющей ключ:
- Через строки: правый клик → Search for → Current Module → String references. Ищем текст сообщения об успехе, переходим по ссылке и поднимаемся до кода, который определяет результат.
- Через API ввода: ставим точку останова (breakpoint) на
GetDlgItemTextW— функцию Windows, которая считывает текст из поля, — и когда программа остановится, смотрим, кто её вызвал.
В данном случае мы будем искать строку Failed.

Здесь мы видим две интересные строки: Failed и Success. Перейдём к Success и посмотрим, что там.
00007FF735B6183E | FF15 EC180000 | call qword ptr ds:[<DefWindowProcW>] |
00007FF735B61844 | E9 8A000000 | jmp crackme.001.7FF735B618D3 |
00007FF735B61849 | E8 C2F9FFFF | call crackme.001.7FF735B61210 |
00007FF735B6184E | 45:33C9 | xor r9d,r9d |
00007FF735B61851 | 4C:8D05 201C0000 | lea r8,qword ptr ds:[7FF735B63478] | 00007FF735B63478:L"Registration Result"
00007FF735B61858 | 33C9 | xor ecx,ecx |
00007FF735B6185A | 84C0 | test al,al |
00007FF735B6185C | 74 0F | je crackme.001.7FF735B6186D |
00007FF735B6185E | 48:8D15 3B1C0000 | lea rdx,qword ptr ds:[7FF735B634A0] | 00007FF735B634A0:L"Success"
00007FF735B61865 | FF15 3D180000 | call qword ptr ds:[<MessageBoxW>] |
00007FF735B6186B | EB 64 | jmp crackme.001.7FF735B618D1 |
00007FF735B6186D | 48:8D15 3C1C0000 | lea rdx,qword ptr ds:[7FF735B634B0] | 00007FF735B634B0:L"Failed"
00007FF735B61874 | FF15 2E180000 | call qword ptr ds:[<MessageBoxW>] |Если посмотреть чуть выше, мы увидим je crackme.001.7FF735B6186D, который решает, будет ли Success или Failed. Ещё чуть выше находится call crackme.001.7FF735B61210 — именно её мы и будем анализировать. Отсюда и далее мы работаем с адресами этого исполняемого файла (у тебя они могут отличаться, если модуль загружен по другой базе).
Совет: не читай программу с самого начала. Начинай с краёв: с API, который считывает ввод, или с сообщения, которое сообщает результат.
Внутри функции первое, что мы видим, — пролог.
00007FF735B61210 | 48:8BC4 | mov rax,rsp |
00007FF735B61213 | 48:8958 10 | mov qword ptr ds:[rax+10],rbx |
00007FF735B61217 | 48:8970 18 | mov qword ptr ds:[rax+18],rsi |
00007FF735B6121B | 48:8978 20 | mov qword ptr ds:[rax+20],rdi |
00007FF735B6121F | 55 | push rbp |
00007FF735B61220 | 41:54 | push r12 |
00007FF735B61222 | 41:55 | push r13 |
00007FF735B61224 | 41:56 | push r14 |
00007FF735B61226 | 41:57 | push r15 |
00007FF735B61228 | 48:8D68 A1 | lea rbp,qword ptr ds:[rax-5F] |
00007FF735B6122C | 48:81EC A0000000 | sub rsp,A0 |
00007FF735B61233 | 0F2970 C8 | movaps xmmword ptr ds:[rax-38],xmm6 |
00007FF735B61237 | 48:8B05 CA3D0000 | mov rax,qword ptr ds:[7FF735B65008] |
00007FF735B6123E | 48:33C4 | xor rax,rsp |Всё это — шум компилятора: сохраняются регистры, строится кадр стека и устанавливается stack cookie (защита /GS). Есть только одна полезная подсказка: movaps xmmword ptr ds:[rax-38],xmm6 . Регистр xmm6 сохраняется, потому что функция позже будет использовать числа с плавающей запятой. Отмечаем это и идём дальше.
Мы также видим GetDlgItem и GetWindowTextLength, но результат тут же затирается командой xor eax,eax: он не используется. Тоже шум.
00007FF735B61245 | 48:8BD9 | mov rbx,rcx |
00007FF735B61248 | BA 79000000 | mov edx,79 | 79:'y'
00007FF735B6124D | 48:8B0D A43E0000 | mov rcx,qword ptr ds:[7FF735B650F8] |
00007FF735B61254 | FF15 961E0000 | call qword ptr ds:[<GetDlgItem>] |
00007FF735B6125A | 48:8BC8 | mov rcx,rax |
00007FF735B6125D | FF15 551E0000 | call qword ptr ds:[<GetWindowTextLengt |
00007FF735B61263 | 33C0 | xor eax,eax |Советы: всегда нужно уметь распознавать шум и пропускать его. Пролог, cookie и отбрасываемые результаты не в счёт.
Самое интересное: как считывается ввод
Здесь начинается самое интересное. Этот шаблон встречается четыре раза:
00007FF735B61265 | 48:8945 D7 | mov qword ptr ss:[rbp-29],rax |
00007FF735B61269 | 66:8945 DF | mov word ptr ss:[rbp-21],ax |
00007FF735B6126D | 44:8D48 05 | lea r9d,qword ptr ds:[rax+5] | cchMax = 5
00007FF735B61271 | 4C:8D45 D7 | lea r8,qword ptr ss:[rbp-29] | буфер, куда сохраняется текст
00007FF735B61275 | 8D50 79 | lea edx,qword ptr ds:[rax+79] | ID элемента управления (0x79)
00007FF735B61278 | 48:8BCB | mov rcx,rbx | дескриптор окна
00007FF735B6127B | FF15 671E0000 | call qword ptr ds:[<GetDlgItemTextW>] |
00007FF735B61281 | 83F8 04 | cmp eax,4 | если вернулось меньше 4 символов -> провал
00007FF735B61284 | 0F82 0B050000 | jb crackme.001.7FF735B61795 |Читаем аргументы GetDlgItemTextW(hDlg, nIDDlgItem, lpString, cchMax), которые в x64 передаются в rcx, rdx, r8 и r9. И делаем три вывода:
cchMax = 5: считывается максимум 4 символа плюс завершающий ноль. Даже если мы ввели 9 цифр, программа видит только первые 4. Наше777777777на самом деле попадает как7777.cmp eax,4 / jb: если поле вернуло меньше 4 символов, происходит переход на ветку провала. Каждому полю нужно ровно 4 значимых символа (больше не важно).- У четырёх элементов управления ID
0x79,0x7C,0x7Aи0x7B, именно в таком порядке чтения:
| Порядок | ID | Буфер в стеке |
|---|---|---|
| 1-й | 0x79 | [rbp-29] |
| 2-й | 0x7C | [rbp-19] |
| 3-й | 0x7A | [rbp-9] |
| 4-й | 0x7B | [rbp+7] |
Обрати внимание: порядок чтения не обязан совпадать с визуальным порядком полей. Этот вопрос мы оставляем открытым и сейчас же проверим.
Совет: читай аргументы API. Именно там видны ограничения (максимальные длины, ID и т. д.).
Преобразование в число похоже на лабиринт!
После считывания текстов код становится длинным и запутанным. Появляются циклы, сравнения с 7FFFFFFFFFFFFFFE, memcpy, вызовы неизвестных функций и блоки, которые, похоже, освобождают память. Прежде чем пугаться, стоит задать себе вопрос:
00007FF735B61320 | 48:FFC3 | inc rbx |
00007FF735B61323 | 66:393C58 | cmp word ptr ds:[rax+rbx*2],di |
00007FF735B61327 | 75 F7 | jne crackme.001.7FF735B61320 |
00007FF735B61329 | 49:BE FEFFFFFFFFFFFF7 | mov r14,7FFFFFFFFFFFFFFE |
00007FF735B61333 | 49:3BDE | cmp rbx,r14 |
00007FF735B61336 | 0F87 93040000 | ja crackme.001.7FF735B617CF |
00007FF735B6133C | 41:BC 0A000000 | mov r12d,A | 0A:'\n'
00007FF735B61342 | 48:C745 CF 07000000 | mov qword ptr ss:[rbp-31],7 |
00007FF735B6134A | 48:83FB 08 | cmp rbx,8 |
00007FF735B6134E | 73 1E | jae crackme.001.7FF735B6136E |
00007FF735B61350 | 48:895D C7 | mov qword ptr ss:[rbp-39],rbx |
00007FF735B61354 | 48:03DB | add rbx,rbx |
00007FF735B61357 | 4C:8BC3 | mov r8,rbx |
00007FF735B6135A | 48:8D55 D7 | lea rdx,qword ptr ss:[rbp-29] |
00007FF735B6135E | 48:8D4D B7 | lea rcx,qword ptr ss:[rbp-49] |
00007FF735B61362 | E8 FB180000 | call <JMP.&memcpy> |
00007FF735B61367 | 66:897C1D B7 | mov word ptr ss:[rbp+rbx-49],di |
00007FF735B6136C | EB 4E | jmp crackme.001.7FF735B613BC |
00007FF735B6136E | 4C:8BFB | mov r15,rbx |
00007FF735B61371 | 49:83CF 07 | or r15,7 |
00007FF735B61375 | 4D:3BFE | cmp r15,r14 |
00007FF735B61378 | 76 05 | jbe crackme.001.7FF735B6137F |
00007FF735B6137A | 4D:8BFE | mov r15,r14 |
00007FF735B6137D | EB 07 | jmp crackme.001.7FF735B61386 |
00007FF735B6137F | 4D:3BFC | cmp r15,r12 |
00007FF735B61382 | 4D:0F42FC | cmovb r15,r12 |
00007FF735B61386 | 49:8D57 01 | lea rdx,qword ptr ds:[r15+1] |
00007FF735B6138A | 48:8D4D B7 | lea rcx,qword ptr ss:[rbp-49] |
00007FF735B6138E | E8 9D050000 | call crackme.001.7FF735B61930 |
00007FF735B61393 | 48:8BF8 | mov rdi,rax |
00007FF735B61396 | 48:8945 B7 | mov qword ptr ss:[rbp-49],rax |
00007FF735B6139A | 48:895D C7 | mov qword ptr ss:[rbp-39],rbx |
00007FF735B6139E | 4C:897D CF | mov qword ptr ss:[rbp-31],r15 |
00007FF735B613A2 | 48:03DB | add rbx,rbx |
00007FF735B613A5 | 4C:8BC3 | mov r8,rbx |
00007FF735B613A8 | 48:8D55 D7 | lea rdx,qword ptr ss:[rbp-29] |
00007FF735B613AC | 48:8BC8 | mov rcx,rax |
00007FF735B613AF | E8 AE180000 | call <JMP.&memcpy> |Что этот блок делает с важными данными?
Если смотреть только на самое существенное, шаблон такой:
00007FF735B613BC | 45:8BC4 | mov r8d,r12d | r8d = 10 (основание системы счисления)
00007FF735B613BF | 33D2 | xor edx,edx | edx = 0
00007FF735B613C1 | 48:8D4D B7 | lea rcx,qword ptr ss:[rbp-49] | rcx = временная строка
00007FF735B613C5 | E8 D6050000 | call crackme.001.7FF735B619A0 | <-- возвращает число в eax
00007FF735B613CA | 44:8BE8 | mov r13d,eax | результат сохраняется в r13dС аргументами (строка, 0, 10) и числом в качестве возвращаемого значения это пахнет std::stoi(string, nullptr, 10): преобразованием текста в целое число в десятичной системе. Это предположение, а не подтверждённый факт, поэтому позже мы его проверим.
Всё остальное в блоке — обслуживание C++ std::wstring:
- цикл
inc rbx / cmp word [...],0 / jne— это встроенныйwcslen, 7FFFFFFFFFFFFFFE— этоmax_size()строки,- сравнение с 8 определяет, хранить ли текст во внутреннем буфере (SSO) или выделять память в куче, и при 4 символах всегда выбирается внутренний буфер,
- блок с
cmp rdx,1000 / add rdx,27 / cmp rax,1F— это деструктор строки, который при 4 символах пропускается, call [_invalid_parameter_noinfo_noreturn], за которым следуетint3, — это ветка ошибки, которая никогда не должна выполняться.
И появляется кое-что очень полезное: один и тот же блок повторяется четыре раза, по одному на каждое поле. Значит, мы разбираем его один раз, а в остальных смотрим только на то, что меняется: исходный буфер и регистр, в который попадает результат.
| Преобразование | Источник | Результат в |
|---|---|---|
| 1-е | [rbp-29] (ID 0x79) | r13d |
| 2-е | [rbp-19] (ID 0x7C) | r12d |
| 3-е | [rbp-9] (ID 0x7A) | r15d |
| 4-е | [rbp+7] (ID 0x7B) | ebx |
Эти четыре регистра потом не перезаписываются: это признак того, что они будут использоваться. Назовём их A, B, C и D по ID элемента управления: A = 0x79, B = 0x7A, C = 0x7B, D = 0x7C.
Совет: когда блок повторяется, разбирай его только один раз. А регистры, которые «переживают»
call, — это данные, которые окажутся важными дальше.
Проверяем в отладчике.
Здесь лучше перестать делать выводы и начать смотреть. Ставим точку останова на инструкцию, следующую за вторым call 19A0, по адресу 7FF7499E14C2, и запускаем. С вводом из шага 1 мы ожидали увидеть в eax значение второго поля (8888, что в шестнадцатеричном виде равно 0x22B8).
Мы видим другое: eax = 457.
Помним, что x64dbg показывает регистры в шестнадцатеричном виде: 0x457 = 1111. То есть второе число, которое читает программа, берётся не из нашего второго поля, а из четвёртого (того, где было 111111111).
Этим подтверждаются две вещи:
- Функция — это
stoiв десятичной системе, потому что результат преобразования"1111"оказался равен 1111. - Порядок чтения не совпадает с визуальным: ID
0x7Cсоответствует четвёртому полю.
Это даёт нам частичное соответствие. В ID 0x7C мы уверены; про остальные три можем лишь предположить, поскольку ID идут подряд (0x79, 0x7A, 0x7B), что они следуют визуальному порядку. Записываем это как непроверенное предположение.
Совет: вывод не становится фактом, пока не подтверждён точкой останова. Один взгляд на
eaxсразу послеcallстоит больше, чем десять минут интерпретации инструкций.
Находим проверку — то, что действительно важно
Мы пропускаем весь деструктор и попадаем на 7FF735B61700. И здесь меняется тип кода: больше нет работы со строками, есть арифметика с числами с плавающей запятой. Это та часть, которая принимает решение.
Этот шаблон из девяти инструкций встречается три раза:
00007FF735B61700 | 6641:0F6EF5 | movd xmm6,r13d | число пользователя, как int
00007FF735B61705 | F3:0FE6F6 | cvtdq2pd xmm6,xmm6 | преобразует его в double
00007FF735B61709 | F2:0F100D C71D0000 | movsd xmm1,qword ptr ds:[7FF735B634D8] | xmm1 = показатель степени
00007FF735B61711 | F2:0F1005 C71D0000 | movsd xmm0,qword ptr ds:[7FF735B634E0] | xmm0 = основание
00007FF735B61719 | E8 68150000 | call <JMP.&pow> | xmm0 = pow(основание, показатель)
00007FF735B6171E | F2:0F5905 A21D0000 | mulsd xmm0,qword ptr ds:[7FF735B634C8] | xmm0 = xmm0 * константа
00007FF735B61726 | 66:0F2EF0 | ucomisd xmm6,xmm0 | пользователь == вычисленное?
00007FF735B6172A | 7A 69 | jp crackme.001.7FF735B61795 |
00007FF735B6172C | 75 67 | jne crackme.001.7FF735B61795 | если не совпадает -> провалКак это читается:
- Число пользователя попадает в
xmm6, который переживает вызовpow(именно поэтому пролог его сохранил). - В Windows x64
pow(double, double)принимает основание вxmm0и показатель степени вxmm1. ucomisdсравнивает два double, аjp/jneпереходят наfail, если они не в точности равны.
Чтобы решить задачу, нам нужны значения этих констант.
Читаем константы и смотрим, что они нам говорят.
В x64dbg: Ctrl+G → 7FF735B634C0. В окне дампа правый клик → Float → Double. Дамп показывает по два значения в строке:

| Адрес | Значение |
|---|---|
7FF735B634C0 | 0.125 |
7FF735B634C8 | 0.25 |
7FF735B634D0 | 0.5 |
7FF735B634D8 | 4 |
7FF735B634E0 | 8 |
Теперь нужно посчитать
Решаем каждое условие на калькуляторе, внимательно следя за тем, что является основанием, а что — показателем степени:
| Поле | Вычисление | Результат |
|----------------|-----------------------------------|-----------|
| A (0x79, r13d) | pow(8, 4) × 0.25 = 4096 × 0.25 | 1024 |
| B (0x7A, r15d) | pow(8, 4) × 0.5 = 4096 × 0.5 | 2048 |
| D (0x7C, r12d) | pow(4, 8) × 0.125 = 65536 × 0.125 | 8192 |
| C (0x7B, ebx) | cmp ebx,200 → 0x200 | 512 |Есть деталь, которую нельзя упускать из виду: в условии D основание и показатель степени поменяны местами по сравнению с A и B (34D8 и 34E0 загружаются в обратном порядке). Поэтому результат равен 65536, а не 4096.
Последнее условие самое простое: cmp ebx,200, за которым следует sete al. Если всё совпало, al = 1 (ключ верный); если что-то не прошло, переход на 7FF735B61795 выполняет xor al,al и возвращает 0.
Совет: настоящая логика — это то, что работает с данными пользователя: сравнения, арифметика и константы. Когда код лишь проверяет соответствие константному выражению, ключ получается решением этого выражения: ничего «реверсить» не нужно.
Теперь соберём ключ и проверим его
У нас есть четыре значения. Осталось вспомнить ограничение из шага 4: каждому полю нужно не менее 4 символов, а в 512 их всего 3. Пишем его с ведущим нулём: 0512. stoi("0512", 10) возвращает 512, так что это подходит.
Исходя из предположения, что ID следуют визуальному порядку:
| Поле | Значение |
|---|---|
1-е (0x79) | 1024 |
2-е (0x7A) | 2048 |
3-е (0x7B) | 0512 |
4-е (0x7C) | 8192 |
Вводим их и нажимаем Register:

Появляется «Registration Result: Success». Предположение о порядке оказалось верным.
Если бы не получилось, запасной план был прост: оставить 8192 в четвёртом поле (уже подтверждено) и перебрать 6 перестановок 1024, 2048 и 0512 в первых трёх. Комбинаций совсем немного.
Резюме: путь в одну строку
Попробовать программу → добраться до функции через API ввода или сообщение об успехе → пропустить шум → прочитать ограничения ввода → понять повторяющийся шаблон преобразования → проверить точкой останова → решить финальное сравнение с помощью констант → проверить.
Метод для следующего crackme
- Попробуй программу с узнаваемыми значениями.
- Начинай с краёв (API ввода или строка об успехе), а не с начала.
- Распознавай и пропускай шум компилятора.
- Читай аргументы API: ограничения находятся там.
- Повторяющиеся блоки разбираются один раз; потом смотришь только на то, что меняется.
- Следи за регистрами, которые «выживают»: это важные данные.
- Проверяй точками останова, а не доверяй одним выводам.
- Когда нужно лишь удовлетворить сравнению, решай выражение.
Идеи для практики
- Написать кейген на Python (здесь это тривиально, потому что ключ фиксированный, но это формирует привычку).
- Пропатчить
sete al(0F 94 C0) наmov al,1, чтобы всегда возвращался успех. - Попробовать приём со строкой об успехе на следующем crackme, без подсказок, и сравнить, сколько пути он тебе сэкономит.
