ldloc.s <d0t.net> stloc.s <Reversed>
-
Автор темы
- #1
Всем привет. Статья будет длинная, тяжелая, осилит не каждый, но кому-то полезна, надеюсь, станет. Сегодня рассмотрим разбор приложения(как назвал его автор – “CrackMe”, по факту это же просто тест на прочность), также цель, которая далась автором: “Вывести окно после инжекта”.
#0# Скачиваем “CrackMe” с этой темы https://yougame.biz/threads/145493/
#1# Сразу же кидаем его на анализ ExeInfoPe:
Видим, что протектор у нас Enigma версии 6.0.0.0, поэтому Detect It Easy даже не смейте открывать.
Понятное дело, что это репак от Gautam’а, напичканный плагинами, даже без виртуальной машины (вырезана), не советую использовать эту затычку.
Еще 1 признак Enigma Protector, это зайти в раздел секций файла и увидеть, что они не имеют никакого названия:
#2# Дальнейшие действия это узнать, с чем имеем дело? Открываем отладчик(использую x64dbg)… Сразу ставим хуки в сцилле на:
Давайте смотреть строки – ничего интересного, плагины энигмы, еще прочая муть, это нам не интересно. Пробежимся по загруженным модулям(библиотекам) – тут их было много, и я увидел интересную вещь:
Хм, но ведь автор указал же в теме C/C++, отчасти он прав, но ведь я то думал чистый натив. Оказывается не все так просто. Решил я открыть дампер…
Далее (сейчас будет относительно быстрое решение, но оно только кажется таким, на самом деле я первый раз столкнулся с C++/CLI приложением и поэтому я угробил более двух дней на изучение и делюсь с вами можно сказать универсальным решение и “фиксом” этого чуда) открываем .NET Dumper, видим, что показывает, что это и вправду .NET, мысленно оскорбив автора за создание темы не в том разделе:
(Как писали “великие” люди, цитата: “Так это ж дот нет”), ну что ж, двигаемся дальше… Конечно же дампим, и что мы видим в папке Dumps? Ни-че-го
Наверное потерли заголовки, но нет, проблема в том, что дампер работает только с чистым управляемым кодом, и он отлично их сортирует. Но в нашем случаем этого не произошло – первая причина понять, что что-то не так. Поэтому, заглядываем в папку “UnknownName” и видим такую картину:
Запустить – не получится, не валидное тело образа. Второй и четвертый файл абсолютно одинаковые – особенность дампера, оставлю второй, остальное удалю. Советую еще создать бэкап до всех действий;
#3# Закидываем в мой любимый dnSpy. Перейдя к точке входа я уже увидел что-то неладное:
_WinMainCRTStartup()? Да, спасибо одному человеку (не буду афишировать, мало ли что), что сказал, что это C++/CLI, интересно получается, а где тело метода, спросите вы? Дело в том, что вот как выглядит эта точка входа при создании приложения Win32, она же WinMainCRTStartup или WinMain:
C++/CLI компилируется в байт код для виртуальной машины .NET; Поэтому мы видим спокойно код в dnSpy. Оказывается, WinMain метод есть в сборке:
О чудо, они совпадают, давайте изменим точку входа этого приложения на WinMain (спасибо форуму tuts4you.com и A200K). Т.к. в dnSpy иногда происходит баг, что при изменении EntryPoint и сохранении файла, данные не сохраняются. Поэтому открываем всеми любимый CFF Explorer и изменяем значение “EntryPointToken” во вкладке .NET Directory, НЕ “AddressOfEntryPoint”, на токен нашего WinMain(0x06000486), далее, важно! Во вкладке “Flags” устанавливаем галочки напротив “IL only”, а также “32bit required” и перезаписываем файл. Отлично!
Открываем заново файл в dnSpy и удаляем из нашей новой точки входа (WinMain) все аргументы метода. Клик по “WinMain” -> “Изменить метод” -> Вкладка “Подпись”, и удаляем все аргументы:
Далее там же, во вкладке “Параметры” удаляем все имена аргументов. Изменяем “Возвращаемый тип” int на void. В итоге получится сигнатура метода:
Увидите ошибку декомпилирования – не кидайтесь в панику, это нормально, с таким будете часто сталкиваться. Чтобы пофиксить метод, кликаем по “WinMain” методу -> ПКМ -> “Изменить тело метода”:
Во-первых, nop’аем заглушку метода, для этого листаем на самое ДНО, и между IL-инструкциями pop и ret затираем нопами некоторый мусор.
Во-вторых, мы удалили из аргумента метода наш _HINSTANCE, поэтому там, где используется этот аргумент, будет ошибка, фиксим ее: для этого листаем в самый верх IL-листинга и кликаем на самую первую(нулевую) инструкцию нажимаем ПКМ -> “Добавить новую инструкцию перед выделением” и так 4 раза.
Итак. У вас 4 пустые инструкции, нужно добавить локальную переменную IntPtr, чуть пониже по смыслу, это тип native int. Для этого переходим в этом же окне на вкладку “Локальные переменные” -> Листаем в самый низ, кликаем на самую последнюю переменную -> ПКМ -> “Добавить новую локальную переменную после выделения”, настраиваем ее: по умолчанию она будет иметь тип int32, нажимаем на него -> кнопка “Очистить” -> “Тип” -> вбиваем в поиск IntPtr и нажимаем на него -> подтверждаем “Ок”. Должно получится следующее: (вместо 138 возможно будет другое число у вас)
Возвращаем во вкладку “Инструкции”, и 4 nop’а, которые вы заготавливали, заменяем на инструкции по порядку:
На опкоде вызова(call) выбираете “Метод” -> и пишите GetModuleHandleA() – там будет в списке 1 единственный вызов этой винапишки.
И последний штрих – нужно заменить все опкоды ldarg.0, которые загружают на стек аргумент метода с индексом 0 на опкод ldloc.s с операндом индексом вашей созданной локальной переменной (в моём случае это 138 индекс). Это нужно сделать, т.к. мы убрали из метода все аргументы, т.к. они нам не передаются. Т.е. было:
Чтобы было легче, написал примерные индексы с нахождением ldarg.0 опкодов:
10, 53, 68, 130, 1404;
Можете нажимать на кнопочку “ОК”; Как видим, метод успешно декомпилируется:
#4# Теперь идем чистить конструктор. ПКМ по сборке -> Перейти к <Module> .cctor:
Видим такую картину:
Далее кликаем на <Module> -> ПКМ -> Изменить инструкции IL -> и выделяем все инструкции(шифтом) кроме последнего опкода ret, далее нажимаем хоткей “N”, все выделенные инструкции должны замениться на nop; но не спешите нажимать “ОК”, переходим во вкладку “Локальные переменные” и удаляем от туда 1 переменную, а также в “Обработчик исключений” и удаляем от туда тоже 1 хэндлер. Подтверждаем.
Должно получиться это:
Сохраняем модуль! Не забывайте это делать как можно чаще при всяких изменениях и проверять работоспособность.
Как видим, при запуске мы не получаем никаких исключений и успешно брякаемся на точке входа:
#5# Объясню в чем проблема реверсить c++/cli приложение; Если оно использует WinApi’шки, то названия библиотек не будут сдамплены и вам придется всю тонну кода фиксить вручную(за бесплатно время на это терять не вижу смысла). Поэтому пока вы не сможете запускать его. Покажу как можно фиксить вручную вызовы функций:
Переходим на функцию, например, GetModuleHandleA() -> видим такую картину:
В атрибуте DllImport(“”) нет названия библиотеки, откуда вызывается эта функция. Для этого заходим в гугл, вбиваем название функции и переходим на оф. Сайт с документацией от Microsoft, листаем в самый низ и ищем строчку “DLL”:
Видим библиотеку Kernel32.dll – это то, что нам нужно.
Переходим обратно в dnSpy, нажимает на функцию -> ПКМ -> “Изменить метод” -> Вкладка “Реализация” -> и заполняем поля: “Имя” и “Модуль”, GetModuleHandleA и Kernel32.dll соответсвтвенно:
Еще не всё. Переходим во вкладку “Основные” и меняем CodeType с Native на IL; Доступ с Assembly на Public; ManagedType на Managed; VtableLayout на ReuseSlot:
Всё, подтверждаем. В этом и заключается фиксинг винапи вызовов. Но будет сложно, т.к. иногда будут попадаться такие вызовы:
Да, может быть сложно, берете в руки отладчик, запускаете приложение и ищете в символах эти вызовы и сравниваете. Самая сложная часть. Но она выполняема(проверено). Работа очень рутинная.
#6# Анализ:
Сейчас проведем краткий осмотр функций; Думаю тут всё понятно, что происходит, объяснять не нужно:
Кстати автор забыл добавить выход из приложения после MessageBox’а, в итоге асинхронно работает приложение и им можно пользоваться:
Сразу скажу анализ проводился достаточно времени, и сразу покажу функцию, влияющую на все остальное:
Здесь заменяем ОПКОД 2D на 2C (== на !=), тем самым не прыгаем на метку IL_244 и свойству присваиваем true;
#7# Ну а теперь перейдем к самому приятному. Патчинг!
Принцип патча: отлаживаем, ждем распаковки файла энигмой, ставим бряк на функцию до этого кода -> хотлайн патчим байтики -> запускаем.
Начинаем. Поднастроим SharpOD:
1, 3, 4, 9, 11 галочки!
Ставим бряк, например, на WSASocketW, т.к. соединяемся с хостом и будет отправлять и получать данные, значит мы по любому попадет на эту функцию (ставим не в начало функции, иначе палимся). Переходим к карте памяти -> наша основная секция с кодом (без названия, но она будет первой):
Далее в дамп этой секции с кодом, видим распакованный код, и помните кидал HEX-байты функции, вот делаем поиск по паттерну этому: 12101209289B0500062D05, находит 2 результата, переходим по последнему в дамп -> патчим байт 2D (brtrue.s) на 2C (brfalse.s):
Также инжект здесь сломан в софте, поэтому будет бесконечное ожидание (можете смотреть сурс, и понять почему:D ), ищем паттерн 7E250200041212280605000612112806050006287106000626 и зануляем его(нопаем), почему зануляем? Потому что в MSIL nop == 0x00:
Запускаем! Видим окошко уже с кнопкой:
Нажимаем Inject:
Поздравляю! Задача решена, цель выполнена, сообщение выведено. Если ты дочитал до сюда – спасибо, надеюсь я не зря для тебя писал статью. Писал ночью, сонный, если есть какая-либо неточность – извините. Принимаю аргументированную критику. Всех благ!
Приступаем…
#0# Скачиваем “CrackMe” с этой темы https://yougame.biz/threads/145493/
#1# Сразу же кидаем его на анализ ExeInfoPe:
Видим, что протектор у нас Enigma версии 6.0.0.0, поэтому Detect It Easy даже не смейте открывать.
Понятное дело, что это репак от Gautam’а, напичканный плагинами, даже без виртуальной машины (вырезана), не советую использовать эту затычку.
Еще 1 признак Enigma Protector, это зайти в раздел секций файла и увидеть, что они не имеют никакого названия:
#2# Дальнейшие действия это узнать, с чем имеем дело? Открываем отладчик(использую x64dbg)… Сразу ставим хуки в сцилле на:
- NtQuerySystemInformation
- NtQueryInformationProcess
- NtUserQueryWindow
Давайте смотреть строки – ничего интересного, плагины энигмы, еще прочая муть, это нам не интересно. Пробежимся по загруженным модулям(библиотекам) – тут их было много, и я увидел интересную вещь:
Хм, но ведь автор указал же в теме C/C++, отчасти он прав, но ведь я то думал чистый натив. Оказывается не все так просто. Решил я открыть дампер…
Далее (сейчас будет относительно быстрое решение, но оно только кажется таким, на самом деле я первый раз столкнулся с C++/CLI приложением и поэтому я угробил более двух дней на изучение и делюсь с вами можно сказать универсальным решение и “фиксом” этого чуда) открываем .NET Dumper, видим, что показывает, что это и вправду .NET, мысленно оскорбив автора за создание темы не в том разделе:
(Как писали “великие” люди, цитата: “Так это ж дот нет”), ну что ж, двигаемся дальше… Конечно же дампим, и что мы видим в папке Dumps? Ни-че-го
Наверное потерли заголовки, но нет, проблема в том, что дампер работает только с чистым управляемым кодом, и он отлично их сортирует. Но в нашем случаем этого не произошло – первая причина понять, что что-то не так. Поэтому, заглядываем в папку “UnknownName” и видим такую картину:
Запустить – не получится, не валидное тело образа. Второй и четвертый файл абсолютно одинаковые – особенность дампера, оставлю второй, остальное удалю. Советую еще создать бэкап до всех действий;
#3# Закидываем в мой любимый dnSpy. Перейдя к точке входа я уже увидел что-то неладное:
_WinMainCRTStartup()? Да, спасибо одному человеку (не буду афишировать, мало ли что), что сказал, что это C++/CLI, интересно получается, а где тело метода, спросите вы? Дело в том, что вот как выглядит эта точка входа при создании приложения Win32, она же WinMainCRTStartup или WinMain:
C++/CLI компилируется в байт код для виртуальной машины .NET; Поэтому мы видим спокойно код в dnSpy. Оказывается, WinMain метод есть в сборке:
О чудо, они совпадают, давайте изменим точку входа этого приложения на WinMain (спасибо форуму tuts4you.com и A200K). Т.к. в dnSpy иногда происходит баг, что при изменении EntryPoint и сохранении файла, данные не сохраняются. Поэтому открываем всеми любимый CFF Explorer и изменяем значение “EntryPointToken” во вкладке .NET Directory, НЕ “AddressOfEntryPoint”, на токен нашего WinMain(0x06000486), далее, важно! Во вкладке “Flags” устанавливаем галочки напротив “IL only”, а также “32bit required” и перезаписываем файл. Отлично!
Открываем заново файл в dnSpy и удаляем из нашей новой точки входа (WinMain) все аргументы метода. Клик по “WinMain” -> “Изменить метод” -> Вкладка “Подпись”, и удаляем все аргументы:
Далее там же, во вкладке “Параметры” удаляем все имена аргументов. Изменяем “Возвращаемый тип” int на void. В итоге получится сигнатура метода:
Увидите ошибку декомпилирования – не кидайтесь в панику, это нормально, с таким будете часто сталкиваться. Чтобы пофиксить метод, кликаем по “WinMain” методу -> ПКМ -> “Изменить тело метода”:
Во-первых, nop’аем заглушку метода, для этого листаем на самое ДНО, и между IL-инструкциями pop и ret затираем нопами некоторый мусор.
Во-вторых, мы удалили из аргумента метода наш _HINSTANCE, поэтому там, где используется этот аргумент, будет ошибка, фиксим ее: для этого листаем в самый верх IL-листинга и кликаем на самую первую(нулевую) инструкцию нажимаем ПКМ -> “Добавить новую инструкцию перед выделением” и так 4 раза.
Итак. У вас 4 пустые инструкции, нужно добавить локальную переменную IntPtr, чуть пониже по смыслу, это тип native int. Для этого переходим в этом же окне на вкладку “Локальные переменные” -> Листаем в самый низ, кликаем на самую последнюю переменную -> ПКМ -> “Добавить новую локальную переменную после выделения”, настраиваем ее: по умолчанию она будет иметь тип int32, нажимаем на него -> кнопка “Очистить” -> “Тип” -> вбиваем в поиск IntPtr и нажимаем на него -> подтверждаем “Ок”. Должно получится следующее: (вместо 138 возможно будет другое число у вас)
Возвращаем во вкладку “Инструкции”, и 4 nop’а, которые вы заготавливали, заменяем на инструкции по порядку:
На опкоде вызова(call) выбираете “Метод” -> и пишите GetModuleHandleA() – там будет в списке 1 единственный вызов этой винапишки.
И последний штрих – нужно заменить все опкоды ldarg.0, которые загружают на стек аргумент метода с индексом 0 на опкод ldloc.s с операндом индексом вашей созданной локальной переменной (в моём случае это 138 индекс). Это нужно сделать, т.к. мы убрали из метода все аргументы, т.к. они нам не передаются. Т.е. было:
Чтобы было легче, написал примерные индексы с нахождением ldarg.0 опкодов:
10, 53, 68, 130, 1404;
Можете нажимать на кнопочку “ОК”; Как видим, метод успешно декомпилируется:
#4# Теперь идем чистить конструктор. ПКМ по сборке -> Перейти к <Module> .cctor:
Видим такую картину:
Далее кликаем на <Module> -> ПКМ -> Изменить инструкции IL -> и выделяем все инструкции(шифтом) кроме последнего опкода ret, далее нажимаем хоткей “N”, все выделенные инструкции должны замениться на nop; но не спешите нажимать “ОК”, переходим во вкладку “Локальные переменные” и удаляем от туда 1 переменную, а также в “Обработчик исключений” и удаляем от туда тоже 1 хэндлер. Подтверждаем.
Должно получиться это:
Сохраняем модуль! Не забывайте это делать как можно чаще при всяких изменениях и проверять работоспособность.
Как видим, при запуске мы не получаем никаких исключений и успешно брякаемся на точке входа:
#5# Объясню в чем проблема реверсить c++/cli приложение; Если оно использует WinApi’шки, то названия библиотек не будут сдамплены и вам придется всю тонну кода фиксить вручную(за бесплатно время на это терять не вижу смысла). Поэтому пока вы не сможете запускать его. Покажу как можно фиксить вручную вызовы функций:
Переходим на функцию, например, GetModuleHandleA() -> видим такую картину:
В атрибуте DllImport(“”) нет названия библиотеки, откуда вызывается эта функция. Для этого заходим в гугл, вбиваем название функции и переходим на оф. Сайт с документацией от Microsoft, листаем в самый низ и ищем строчку “DLL”:
Видим библиотеку Kernel32.dll – это то, что нам нужно.
Переходим обратно в dnSpy, нажимает на функцию -> ПКМ -> “Изменить метод” -> Вкладка “Реализация” -> и заполняем поля: “Имя” и “Модуль”, GetModuleHandleA и Kernel32.dll соответсвтвенно:
Еще не всё. Переходим во вкладку “Основные” и меняем CodeType с Native на IL; Доступ с Assembly на Public; ManagedType на Managed; VtableLayout на ReuseSlot:
Всё, подтверждаем. В этом и заключается фиксинг винапи вызовов. Но будет сложно, т.к. иногда будут попадаться такие вызовы:
Да, может быть сложно, берете в руки отладчик, запускаете приложение и ищете в символах эти вызовы и сравниваете. Самая сложная часть. Но она выполняема(проверено). Работа очень рутинная.
#6# Анализ:
Сейчас проведем краткий осмотр функций; Думаю тут всё понятно, что происходит, объяснять не нужно:
Кстати автор забыл добавить выход из приложения после MessageBox’а, в итоге асинхронно работает приложение и им можно пользоваться:
Сразу скажу анализ проводился достаточно времени, и сразу покажу функцию, влияющую на все остальное:
Здесь заменяем ОПКОД 2D на 2C (== на !=), тем самым не прыгаем на метку IL_244 и свойству присваиваем true;
#7# Ну а теперь перейдем к самому приятному. Патчинг!
Принцип патча: отлаживаем, ждем распаковки файла энигмой, ставим бряк на функцию до этого кода -> хотлайн патчим байтики -> запускаем.
Начинаем. Поднастроим SharpOD:
1, 3, 4, 9, 11 галочки!
Ставим бряк, например, на WSASocketW, т.к. соединяемся с хостом и будет отправлять и получать данные, значит мы по любому попадет на эту функцию (ставим не в начало функции, иначе палимся). Переходим к карте памяти -> наша основная секция с кодом (без названия, но она будет первой):
Далее в дамп этой секции с кодом, видим распакованный код, и помните кидал HEX-байты функции, вот делаем поиск по паттерну этому: 12101209289B0500062D05, находит 2 результата, переходим по последнему в дамп -> патчим байт 2D (brtrue.s) на 2C (brfalse.s):
Также инжект здесь сломан в софте, поэтому будет бесконечное ожидание (можете смотреть сурс, и понять почему:D ), ищем паттерн 7E250200041212280605000612112806050006287106000626 и зануляем его(нопаем), почему зануляем? Потому что в MSIL nop == 0x00:
Запускаем! Видим окошко уже с кнопкой:
Нажимаем Inject:
Поздравляю! Задача решена, цель выполнена, сообщение выведено. Если ты дочитал до сюда – спасибо, надеюсь я не зря для тебя писал статью. Писал ночью, сонный, если есть какая-либо неточность – извините. Принимаю аргументированную критику. Всех благ!
Последнее редактирование: