Гайд [Reverse-Engineering] Crackme со встроенным отладчиком

Всем большой шалом! В этой статье я разобрал очень интересный для меня CrackMe, давным-давно я пытался прорешать его, но на тот момент у меня отсутствовали необходимые знания и опыт работы с тем, что вы увидите далее.

Этот CrackMe имеет в себе одну особенность, которую я до этого не видел в крякмисах, уж извините за тавтологию, связана она с отладкой дочернего процесса, но обо всём по порядку.

Вступление

Pasted-image-20231201233914.png


Для начала, наш крякмис извлекает идентификатор своего процесса, открывает хендл к самому себе и проверяет соответствия пути исполняемого файла.

Pasted-image-20231202005552.png


Если всё гуд, то переходим к инициализации отладчика. Первым делом функция пытается в очередное открытие хендла к самому себе и аттачнуть дебаггер, если же всё прошло нормально, то следующим в очередь идёт создание потока, где рисуется диалоговое окно.

Pasted-image-20231201235642.png


Pasted-image-20231202005848.png


Внутри каллбека в самом конце есть вызов GetDlgItemTextA и вызов некой функции, которая принимает в качестве аргумента текст, введенный пользователем, оно-то и является нашей функцией для проверки пароля.

Pasted-image-20231202010330.png


Вот только сама функция оказалась полностью засранной
Pasted-image-20231201235733.png


По началу я думал, что это зашифрованный код, и то что дебаггер нужен здесь исключительно для декрипта некоторых инструкции, но оказалось всё намного проще.

Встроенный отладчик

Вернёмся обратно к функции инициализации дебаггера. Мы знаем, что функция проверки пароля похерена изначально, значит будем пытаться восстановить его вручную.

Когда отладчик уходит в бесконечный цикл, он начинает выполнять несколько проверок, одна из них является на то, какое исключение выбросила программа.
Pasted-image-20231202011005.png


Исключение 0x80000003 означает EXCEPTION_BREAKPOINT, в простонародье - точка останова, а если быть ещё точнее, то её выбрасывает инструкция int3. Мы неоднократно видели эту инструкцию в функции проверки пароля, значит отладчик так или иначе взаимодействует с нашей функцией. Когда цикл прерывается после обнаружения точки останова, она вызывает функцию dbg::RedirectInstructionPtr.
Pasted-image-20231202011322.png


Эта функция вначале читает первый байт после int3, сохраняет его в буфер и затем проделывает с ним побитовое насилие, Eip переключается на другую инструкцию, а регистр Eax делает сдвиг влево, учтём этот момент.


Восстановление алгоритма

Теперь мы в добавок ещё знаем, что отладчик на самом деле не пытается декриптить инструкцию, а пытается отступить некоторое количество байт, чтобы перейти на нужную ему инструкцию и продолжить выполнение, вплоть до следующей точки останова.

Для начала занопаем бесполезный отрывок кода
Pasted-image-20231202090330.png


Почему бесполезный? По всей видимости автор проверяет умение читать асм код, но если говорить вкратце о том, почему этот код не имеет смысла, то тут стоит приглядеться, что первой же инструкцией он сохраняет значение еах в стеке, затем проводит с ним некоторые операции и возвращает ему старое значение из стека, с ebx ещё легче.

Теперь перейдем к основному алгоритму, посмотрим как работает логика отладчика.
Pasted-image-20231202091201.png


Видим, что программа читает некий байт по адресу 0x401BC5 и затем пытается перейти в 0x401BC6.
Pasted-image-20231202013127.png


Но на нашем скриншоте видно, что из-за мешающихся ненужных опкодов крякми прыгает туда, куда не видит дизассемблер визуально. Значит нопаем этот байтик и получаем валидный опкод, и не забываем добавить rol eax, 1, ведь именно это и делал отладчик когда обрабатывал точку останова.

Pasted-image-20231202092112.png


Следующий адрес
Pasted-image-20231202092211.png


Крякми читает инструкцию "mov bh, 0xAA", вернее её первый байт -> 0xB7.
Теперь видим, что крякми пытается прыгнуть на инструкцию по адресу 0x401BD0.
Pasted-image-20231202092914.png


Теперь отсчитываем сколько байт пропустил наш крякми чтобы выйти на следующую инструкцию, (0x401BD0 - 0x401BC1) = получаем тоже 5 байт.

Выходит так, что все 5 байт, которые идут после 401BCA до 401BD0 никоим образом не будут выполняться, просто потому что регистр Eip их скипает, значит их тоже будем нопать.

Следующий адрес
Pasted-image-20231202093508.png


Pasted-image-20231202093603.png


Проделываем тоже самое, что и с прошлым адресом, видим, что он читает 0x9B байт, (0x401BE7 - 0x401BE0 = 6), то есть как и у отладчика мы пропустим 6 следующих байт после текущей инструкции и выйдем на инструкцию 'sub eax, 0xFFBDD1F7', а значит три выше инструкции не нужны и их надо затирать, после всего проделанного добавим инструкцию 'rol eax, 1'
Pasted-image-20231202094003.png


По такому же алгоритму фиксим еще несколько адресов
Pasted-image-20231202094323.png


Ближе к концу я столкнулся с такой проблемой
Perl:
00401BFE | 90                          | nop                                             |
00401BFF | 90                          | nop                                             |
00401C00 | 90                          | nop                                             |
00401C01 | 81C2 54E16363               | add edx,6363E154                                |
00401C07 | 31C0                        | xor eax,eax                                     |
00401C09 | 09CA                        | or edx,ecx                                      |
00401C0B | 75 0E                       | jne crackme.401C1B                              |
00401C0D | CC                          | int3                                            |
00401C0E | C166 8B 53                  | shl dword ptr ds:[esi-75],53                    |
00401C12 | 0866 81                     | or byte ptr ds:[esi-7F],ah                      |
00401C15 | FA                          | cli                                             |
00401C16 | 6900 7501405B               | imul eax,dword ptr ds:[eax],5B400175            |
00401C1C | 5D                          | pop ebp                                         |
00401C1D | C3                          | ret                                             |

Изучая алгоритм, я понял, что последний int3 не вызвался, поскольку его вызов означает ввод пользователя верным, а пароля мы не знаем, значит и не узнаем от отладчика куда прыгать. Тут уже надо догадываться самому. Долго гадать не пришлось, отступая три байта после прыжка jne нопаем так, чтобы у нас первым байтом шёл 0x8B, и на выходе получим такую красоту:
Pasted-image-20231202100411.png


Вот как выглядит почищенный от всего алгоритм проверки пароля:
Perl:
.text:00401BBC ; int __cdecl fuckme::IsCorrectPass(char *userKey)
.text:00401BBC userKey         = dword ptr  8
.text:00401BC6                 push    ebp
.text:00401BC7                 mov     ebp, esp
.text:00401BC9                 rol     eax, 1
.text:00401BD0                 push    ebx
.text:00401BD1                 lea     ebx, [ebp+userKey]
.text:00401BD4                 mov     ebx, [ebx]
.text:00401BD6                 mov     eax, [ebx]
.text:00401BD8                 xor     eax, 0CC9B402Ah
.text:00401BDD                 rol     eax, 1
.text:00401BDF                 rol     eax, 1
.text:00401BE7                 sub     eax, 0FFBDD1F7h
.text:00401BEC                 mov     ecx, eax
.text:00401BEE                 mov     eax, [ebx+4]
.text:00401BF1                 rol     eax, 1
.text:00401BF7                 mov     edx, eax
.text:00401BF9                 add     edx, eax
.text:00401BFB                 add     edx, eax
.text:00401BFD                 rol     eax, 1
.text:00401C01                 add     edx, 6363E154h
.text:00401C07                 xor     eax, eax
.text:00401C09                 or      edx, ecx
.text:00401C0B                 jnz     short loc_401C1B
.text:00401C0D                 rol     eax, 1
.text:00401C10                 mov     edx, [ebx+8]
.text:00401C13                 cmp     dx, 69h ; 'i'
.text:00401C18                 jnz     short loc_401C1B
.text:00401C1A                 inc     eax
.text:00401C1B
.text:00401C1B loc_401C1B:                             ; CODE XREF: fuckme__IsCorrectPass+4F↑j
.text:00401C1B                                         ; fuckme__IsCorrectPass+5C↑j
.text:00401C1B                 pop     ebx
.text:00401C1C                 pop     ebp
.text:00401C1D                 retn

Алгоритм пароля

После того, как мы пофиксили алгоритм, мы можем его декомпилировать:
C++:
int __cdecl fuckme::IsCorrectPass(char *userKey)
{
  int result; // eax

  result = 0;
  if ( !((__ROL4__(__ROL4__(*userKey ^ 0xCC9B402A, 1), 1) + 0x422E09) | (3 * __ROL4__(*(userKey + 1), 1) + 0x6363E154)) )
  {
    if ( userKey[8] == 'i' )
      ++result;
  }
  return result;
}

Первая часть пароля ксорится на константу 0xCC9B402A и затем сдвигается влево на 2 бита, после чего прибавляется следующая константа - 0x422E09.
Для второй части пароля тоже используется битовой сдвиг влево на 1 бит, далее умножается на 3 и плюсуется константа 0x6363E154.
И как бонус, проверяется 9 элемент пароля на символ 'i'.

Попробуем представить этот алгоритм в ином виде:
Python:
IsCorrectPass(userKey):
    isCorrect = false
    FirstPartOfPass = RotateLeft(userKey[0] xor 0xCC9B402A, 2) + 0x422E09
    SecondPartOfPass = (3 * RotateLeft(userKey[4], 1)) + 0x6363E154
    if (FirstPartOfPass or SecondPartOfPass) == 0:
        if userKey[8] == 'i':
            isCorrect = true
    return isCorrect

Для решения задачи надо найти два заветных числа, которые проходят первую проверку, можно использовать инвертированные операции для алгоритма. В данном случае, для инструкции rol используется инструкция ror (битовой сдвиг вправо), а для отрицательных чисел используются положительные числа, а для деления - умножения.

Спустя пару попыток инверсации алгоритма, получаем что-то подобное на правду:
Python:
FirstPartOfPass = Invert(RotateRight(neg(0x422E09), 2) xor 0xCC9B402A);
SecondPartOfPass = Invert(RotateRight(neg(0x6363E154) / 3, 1));

Проверяем и получаем два значения: 57347433 и 725A6F6F
Переводим в символьный тип: W4t3 и rZoo

К слову не забываем про символ 'i', который должен быть последним .
Получаем пароль W4t3rZooi.

Проверяем, и... бинго!
Pasted-image-20231202223435.png


К слову, помимо фикса алгоритма я прикола ради так же отвязывал и отладчик, чтобы проверить работоспособность пропатченного алгоритма. Стоит ещё учитывать, что в функции каллбека DialogProc тоже есть int3, но он там не выполняет никакой роли фактически, отладчик обрабатывает его, но ничего с ним не делает в последующем, он скорее нужен чтобы избежать декомпиляции каллбека в иде.

Собсна обозревать мне тут больше нечего.

Заключение

Задумка в крякми на самом деле прикольная, обычно встроенные дебаггеры, обрабатывающие какую-либо информацию я видел лишь в читах, а тут решили лайтовенько его задействовать, получилось круто хд

Подписывайтесь на мой блог: https://t.me/colby5engineering

Всего доброго!