Всем большой шалом! В этой статье я разобрал очень интересный для меня CrackMe, давным-давно я пытался прорешать его, но на тот момент у меня отсутствовали необходимые знания и опыт работы с тем, что вы увидите далее.
Этот CrackMe имеет в себе одну особенность, которую я до этого не видел в крякмисах, уж извините за тавтологию, связана она с отладкой дочернего процесса, но обо всём по порядку.
Вступление
Для начала, наш крякмис извлекает идентификатор своего процесса, открывает хендл к самому себе и проверяет соответствия пути исполняемого файла.
Если всё гуд, то переходим к инициализации отладчика. Первым делом функция пытается в очередное открытие хендла к самому себе и аттачнуть дебаггер, если же всё прошло нормально, то следующим в очередь идёт создание потока, где рисуется диалоговое окно.
Внутри каллбека в самом конце есть вызов GetDlgItemTextA и вызов некой функции, которая принимает в качестве аргумента текст, введенный пользователем, оно-то и является нашей функцией для проверки пароля.
Вот только сама функция оказалась полностью засранной
По началу я думал, что это зашифрованный код, и то что дебаггер нужен здесь исключительно для декрипта некоторых инструкции, но оказалось всё намного проще.
Встроенный отладчик
Вернёмся обратно к функции инициализации дебаггера. Мы знаем, что функция проверки пароля похерена изначально, значит будем пытаться восстановить его вручную.
Когда отладчик уходит в бесконечный цикл, он начинает выполнять несколько проверок, одна из них является на то, какое исключение выбросила программа.
Исключение 0x80000003 означает EXCEPTION_BREAKPOINT, в простонародье - точка останова, а если быть ещё точнее, то её выбрасывает инструкция int3. Мы неоднократно видели эту инструкцию в функции проверки пароля, значит отладчик так или иначе взаимодействует с нашей функцией. Когда цикл прерывается после обнаружения точки останова, она вызывает функцию dbg::RedirectInstructionPtr.
Эта функция вначале читает первый байт после int3, сохраняет его в буфер и затем проделывает с ним побитовое насилие, Eip переключается на другую инструкцию, а регистр Eax делает сдвиг влево, учтём этот момент.
Восстановление алгоритма
Теперь мы в добавок ещё знаем, что отладчик на самом деле не пытается декриптить инструкцию, а пытается отступить некоторое количество байт, чтобы перейти на нужную ему инструкцию и продолжить выполнение, вплоть до следующей точки останова.
Для начала занопаем бесполезный отрывок кода
Почему бесполезный? По всей видимости автор проверяет умение читать асм код, но если говорить вкратце о том, почему этот код не имеет смысла, то тут стоит приглядеться, что первой же инструкцией он сохраняет значение еах в стеке, затем проводит с ним некоторые операции и возвращает ему старое значение из стека, с ebx ещё легче.
Теперь перейдем к основному алгоритму, посмотрим как работает логика отладчика.
Видим, что программа читает некий байт по адресу 0x401BC5 и затем пытается перейти в 0x401BC6.
Но на нашем скриншоте видно, что из-за мешающихся ненужных опкодов крякми прыгает туда, куда не видит дизассемблер визуально. Значит нопаем этот байтик и получаем валидный опкод, и не забываем добавить rol eax, 1, ведь именно это и делал отладчик когда обрабатывал точку останова.
Следующий адрес
Крякми читает инструкцию "mov bh, 0xAA", вернее её первый байт -> 0xB7.
Теперь видим, что крякми пытается прыгнуть на инструкцию по адресу 0x401BD0.
Теперь отсчитываем сколько байт пропустил наш крякми чтобы выйти на следующую инструкцию, (0x401BD0 - 0x401BC1) = получаем тоже 5 байт.
Выходит так, что все 5 байт, которые идут после 401BCA до 401BD0 никоим образом не будут выполняться, просто потому что регистр Eip их скипает, значит их тоже будем нопать.
Следующий адрес
Проделываем тоже самое, что и с прошлым адресом, видим, что он читает 0x9B байт, (0x401BE7 - 0x401BE0 = 6), то есть как и у отладчика мы пропустим 6 следующих байт после текущей инструкции и выйдем на инструкцию 'sub eax, 0xFFBDD1F7', а значит три выше инструкции не нужны и их надо затирать, после всего проделанного добавим инструкцию 'rol eax, 1'
По такому же алгоритму фиксим еще несколько адресов
Ближе к концу я столкнулся с такой проблемой
Изучая алгоритм, я понял, что последний int3 не вызвался, поскольку его вызов означает ввод пользователя верным, а пароля мы не знаем, значит и не узнаем от отладчика куда прыгать. Тут уже надо догадываться самому. Долго гадать не пришлось, отступая три байта после прыжка jne нопаем так, чтобы у нас первым байтом шёл 0x8B, и на выходе получим такую красоту:
Вот как выглядит почищенный от всего алгоритм проверки пароля:
Алгоритм пароля
После того, как мы пофиксили алгоритм, мы можем его декомпилировать:
Первая часть пароля ксорится на константу 0xCC9B402A и затем сдвигается влево на 2 бита, после чего прибавляется следующая константа - 0x422E09.
Для второй части пароля тоже используется битовой сдвиг влево на 1 бит, далее умножается на 3 и плюсуется константа 0x6363E154.
И как бонус, проверяется 9 элемент пароля на символ 'i'.
Попробуем представить этот алгоритм в ином виде:
Для решения задачи надо найти два заветных числа, которые проходят первую проверку, можно использовать инвертированные операции для алгоритма. В данном случае, для инструкции rol используется инструкция ror (битовой сдвиг вправо), а для отрицательных чисел используются положительные числа, а для деления - умножения.
Спустя пару попыток инверсации алгоритма, получаем что-то подобное на правду:
Проверяем и получаем два значения: 57347433 и 725A6F6F
Переводим в символьный тип: W4t3 и rZoo
К слову не забываем про символ 'i', который должен быть последним .
Получаем пароль W4t3rZooi.
Проверяем, и... бинго!
К слову, помимо фикса алгоритма я прикола ради так же отвязывал и отладчик, чтобы проверить работоспособность пропатченного алгоритма. Стоит ещё учитывать, что в функции каллбека DialogProc тоже есть int3, но он там не выполняет никакой роли фактически, отладчик обрабатывает его, но ничего с ним не делает в последующем, он скорее нужен чтобы избежать декомпиляции каллбека в иде.
Собсна обозревать мне тут больше нечего.
Заключение
Задумка в крякми на самом деле прикольная, обычно встроенные дебаггеры, обрабатывающие какую-либо информацию я видел лишь в читах, а тут решили лайтовенько его задействовать, получилось круто хд
Подписывайтесь на мой блог: https://t.me/colby5engineering
Всего доброго!
Этот CrackMe имеет в себе одну особенность, которую я до этого не видел в крякмисах, уж извините за тавтологию, связана она с отладкой дочернего процесса, но обо всём по порядку.
Вступление
Для начала, наш крякмис извлекает идентификатор своего процесса, открывает хендл к самому себе и проверяет соответствия пути исполняемого файла.
Если всё гуд, то переходим к инициализации отладчика. Первым делом функция пытается в очередное открытие хендла к самому себе и аттачнуть дебаггер, если же всё прошло нормально, то следующим в очередь идёт создание потока, где рисуется диалоговое окно.
Внутри каллбека в самом конце есть вызов GetDlgItemTextA и вызов некой функции, которая принимает в качестве аргумента текст, введенный пользователем, оно-то и является нашей функцией для проверки пароля.
Вот только сама функция оказалась полностью засранной
По началу я думал, что это зашифрованный код, и то что дебаггер нужен здесь исключительно для декрипта некоторых инструкции, но оказалось всё намного проще.
Встроенный отладчик
Вернёмся обратно к функции инициализации дебаггера. Мы знаем, что функция проверки пароля похерена изначально, значит будем пытаться восстановить его вручную.
Когда отладчик уходит в бесконечный цикл, он начинает выполнять несколько проверок, одна из них является на то, какое исключение выбросила программа.
Исключение 0x80000003 означает EXCEPTION_BREAKPOINT, в простонародье - точка останова, а если быть ещё точнее, то её выбрасывает инструкция int3. Мы неоднократно видели эту инструкцию в функции проверки пароля, значит отладчик так или иначе взаимодействует с нашей функцией. Когда цикл прерывается после обнаружения точки останова, она вызывает функцию dbg::RedirectInstructionPtr.
Эта функция вначале читает первый байт после int3, сохраняет его в буфер и затем проделывает с ним побитовое насилие, Eip переключается на другую инструкцию, а регистр Eax делает сдвиг влево, учтём этот момент.
Восстановление алгоритма
Теперь мы в добавок ещё знаем, что отладчик на самом деле не пытается декриптить инструкцию, а пытается отступить некоторое количество байт, чтобы перейти на нужную ему инструкцию и продолжить выполнение, вплоть до следующей точки останова.
Для начала занопаем бесполезный отрывок кода
Почему бесполезный? По всей видимости автор проверяет умение читать асм код, но если говорить вкратце о том, почему этот код не имеет смысла, то тут стоит приглядеться, что первой же инструкцией он сохраняет значение еах в стеке, затем проводит с ним некоторые операции и возвращает ему старое значение из стека, с ebx ещё легче.
Теперь перейдем к основному алгоритму, посмотрим как работает логика отладчика.
Видим, что программа читает некий байт по адресу 0x401BC5 и затем пытается перейти в 0x401BC6.
Но на нашем скриншоте видно, что из-за мешающихся ненужных опкодов крякми прыгает туда, куда не видит дизассемблер визуально. Значит нопаем этот байтик и получаем валидный опкод, и не забываем добавить rol eax, 1, ведь именно это и делал отладчик когда обрабатывал точку останова.
Следующий адрес
Крякми читает инструкцию "mov bh, 0xAA", вернее её первый байт -> 0xB7.
Теперь видим, что крякми пытается прыгнуть на инструкцию по адресу 0x401BD0.
Теперь отсчитываем сколько байт пропустил наш крякми чтобы выйти на следующую инструкцию, (0x401BD0 - 0x401BC1) = получаем тоже 5 байт.
Выходит так, что все 5 байт, которые идут после 401BCA до 401BD0 никоим образом не будут выполняться, просто потому что регистр Eip их скипает, значит их тоже будем нопать.
Следующий адрес
Проделываем тоже самое, что и с прошлым адресом, видим, что он читает 0x9B байт, (0x401BE7 - 0x401BE0 = 6), то есть как и у отладчика мы пропустим 6 следующих байт после текущей инструкции и выйдем на инструкцию 'sub eax, 0xFFBDD1F7', а значит три выше инструкции не нужны и их надо затирать, после всего проделанного добавим инструкцию 'rol eax, 1'
По такому же алгоритму фиксим еще несколько адресов
Ближе к концу я столкнулся с такой проблемой
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, и на выходе получим такую красоту:
Вот как выглядит почищенный от всего алгоритм проверки пароля:
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.
Проверяем, и... бинго!
К слову, помимо фикса алгоритма я прикола ради так же отвязывал и отладчик, чтобы проверить работоспособность пропатченного алгоритма. Стоит ещё учитывать, что в функции каллбека DialogProc тоже есть int3, но он там не выполняет никакой роли фактически, отладчик обрабатывает его, но ничего с ним не делает в последующем, он скорее нужен чтобы избежать декомпиляции каллбека в иде.
Собсна обозревать мне тут больше нечего.
Заключение
Задумка в крякми на самом деле прикольная, обычно встроенные дебаггеры, обрабатывающие какую-либо информацию я видел лишь в читах, а тут решили лайтовенько его задействовать, получилось круто хд
Подписывайтесь на мой блог: https://t.me/colby5engineering
Всего доброго!