что такое реверс в программировании
Реверсинг и обфускация, как это работает
Многие в детстве разбирали свои игрушки в надежде понять, как они устроены, т.е. задатки реверс-инженера есть у каждого второго. Однако, у кого-то это с возрастом прошло, в то время как другие, наоборот, отточили свои умения и достигли в этом определенного мастерства.
Кому нужен реверс-инжиниринг программного обеспечения?
Если отбросить промышленный шпионаж, реверс-инжиниринг широко используется аналитиками для препарирования вирусов и создания средств защиты. В то же время, аналогичный подход применяется для анализа ПО с закрытым исходным кодом, поиска уязвимостей и создания вирусов. Также энтузиастами проводится анализ драйверов и некоторых других полезных утилит с закрытым исходным кодом для того, чтобы создавать аналоги для Linux с открытым кодом. Хотите поиграть бесплатно? Генераторы ключей для платного ПО, пиратские серверы онлайн-игр также создаются с помощью реверс-инжиниринга. Однако, взлом софта и обратная разработка это, в большинстве случаев, разные вещи, для взлома, как правило, достаточно разобрать процесс проверки ключа лицензии, а во втором случае придется потратить намного больше времени и сил.
Как бороться с реверсингом?
Прежде всего, можно затруднить анализ программы на уровне разработчика, продвинутый кодер может раздуть код искусственным образом, тогда, даже имея оригинальные исходники, зачастую будет непонятно, как все работает. Этот способ не рекомендуется, потому как над одним и тем же проектом в разное время могут вести работу различные команды разработчиков, искусственное усложнение исходного кода может сильно затруднить работу.
Самым известным и популярным способом защиты является обфускация – превращение исходного кода в кашу, понять что-либо в которой в принципе невозможно. Программа берет исходную инструкцию в коде и делает из нее несколько, делающих то же самое, плюс еще много ложных инструкций, которые предназначены исключительно для того, чтобы запутать реверс-инженера. Широко применяется как в обычных программах, так и в вирусах, особенно в полиморфных, что приводит к появлению многих копий одной и той же вредоносной программы, каждая новая копия которой отличается от оригинала и других копий. С шедеврами в области обфускации кода и признанными мастерами в этой области можно познакомиться, например, на этом сайте.
Что касается надежных программ для обфускации кода, с этим сложнее. На самом деле, их сотни, и многие являются платными. Ссылки не привожу по той причине, что надежность той или иной программы определить сложно. Если на торренте лежит взломанный обфускатор, значит он уже проанализирован, алгоритмы его работы, возможно, уже известны, значит он бесполезен.
Помните о том, что помимо защиты от посторонних обфускатор не дает разработчику возможности отлаживать собственный код, поэтому при отладке необходимо его отключать.
Как бороться с защитой от реверсинга?
А тем, кто хочет стать программистом, рекомендуем профессию «Веб-разработчик».
Многие в детстве разбирали свои игрушки в надежде понять, как они устроены, т.е. задатки реверс-инженера есть у каждого второго. Однако, у кого-то это с возрастом прошло, в то время как другие, наоборот, отточили свои умения и достигли в этом определенного мастерства.
Кому нужен реверс-инжиниринг программного обеспечения?
Если отбросить промышленный шпионаж, реверс-инжиниринг широко используется аналитиками для препарирования вирусов и создания средств защиты. В то же время, аналогичный подход применяется для анализа ПО с закрытым исходным кодом, поиска уязвимостей и создания вирусов. Также энтузиастами проводится анализ драйверов и некоторых других полезных утилит с закрытым исходным кодом для того, чтобы создавать аналоги для Linux с открытым кодом. Хотите поиграть бесплатно? Генераторы ключей для платного ПО, пиратские серверы онлайн-игр также создаются с помощью реверс-инжиниринга. Однако, взлом софта и обратная разработка это, в большинстве случаев, разные вещи, для взлома, как правило, достаточно разобрать процесс проверки ключа лицензии, а во втором случае придется потратить намного больше времени и сил.
Как бороться с реверсингом?
Прежде всего, можно затруднить анализ программы на уровне разработчика, продвинутый кодер может раздуть код искусственным образом, тогда, даже имея оригинальные исходники, зачастую будет непонятно, как все работает. Этот способ не рекомендуется, потому как над одним и тем же проектом в разное время могут вести работу различные команды разработчиков, искусственное усложнение исходного кода может сильно затруднить работу.
Самым известным и популярным способом защиты является обфускация – превращение исходного кода в кашу, понять что-либо в которой в принципе невозможно. Программа берет исходную инструкцию в коде и делает из нее несколько, делающих то же самое, плюс еще много ложных инструкций, которые предназначены исключительно для того, чтобы запутать реверс-инженера. Широко применяется как в обычных программах, так и в вирусах, особенно в полиморфных, что приводит к появлению многих копий одной и той же вредоносной программы, каждая новая копия которой отличается от оригинала и других копий. С шедеврами в области обфускации кода и признанными мастерами в этой области можно познакомиться, например, на этом сайте.
Что касается надежных программ для обфускации кода, с этим сложнее. На самом деле, их сотни, и многие являются платными. Ссылки не привожу по той причине, что надежность той или иной программы определить сложно. Если на торренте лежит взломанный обфускатор, значит он уже проанализирован, алгоритмы его работы, возможно, уже известны, значит он бесполезен.
Помните о том, что помимо защиты от посторонних обфускатор не дает разработчику возможности отлаживать собственный код, поэтому при отладке необходимо его отключать.
Как бороться с защитой от реверсинга?
А тем, кто хочет стать программистом, рекомендуем профессию «Веб-разработчик».
Реверс-инжиниринг для начинающих: продвинутые концепции программирования
Авторизуйтесь
Реверс-инжиниринг для начинающих: продвинутые концепции программирования
В первой части мы рассмотрели базовые концепции программирования, такие как циклы и условный оператор, в этой статье будем рассматривать более сложные темы, необходимые для реверс-инжиниринга.
Примечание Для дизассемблирования в этой статье используется IDA Pro, но многие её функции (например блок-схемы, перевод в псевдокод и т. д.) можно найти в качестве надстроек в бесплатных дизассемблерах (radare2). Более того, для лучшего понимания имена некоторых дизассемблированных переменных были изменены с «v20» на имена, которые были у них в С. Также в этой статье исполняемый файл был скомпилирован в 64-битной версии, а для дизассемблирования используется 64-битная версия IDA Pro. Это на случай, если вы захотите повторить всё самостоятельно, потому что это может повлиять на конечный результат (например, на массивах будет сильное различие 32 и 64-битных версий, а также в 64-битной версии регистры становятся в два раза больше).
Массивы
Итак, начнём с массивов. Сначала рассмотрим код на Си:
Эти 12 строк кода превращаются в довольно внушительный блок машинного кода. Давайте рассмотрим его детально:
Объявление массива с литералом — дизассемблированный вид
При инициализации массива с константным размером компилятор просто инициализирует длину массива через локальную переменную.
Локальные переменные — массивы
Объявление массива через переменную — машинный код
Объявление массива с предопределёнными значениями — машинный код
При объявлении массива с предопределёнными значениями компилятор сохраняет каждое значение в свою переменную, которая представлена индексом массива (например objArray4 = objArray[4] ).
Инициализация элемента массива через индекс — машинный код
Так же как и с предопределёнными значениями, компилятор создаёт новую переменную для указанного индексного значения при инициализации элемента массива через индекс.
Извлечение элемента массива — машинный код
При извлечении элемента массива значение элемента берётся по указанному индексу и записывается в нужную переменную.
Создание матрицы с переменными — машинный код
Задание значения переменной в матрице — машинный код
При вводе в матрицу сначала определяется местоположение желаемого элемента массива с использованием базового местоположения матрицы. Затем содержимое указанного элемента массива устанавливается на желаемое входное значение (т.е. 1337 ).
Извлечение значения из матрицы — машинный код
При извлечении значения из матрицы происходят такие же вычисления, как и при внесении значения в неё. Однако при этом ничего не записывается — содержимое извлекается и записывается в нужную переменную (например MatrixLeet ).
Указатели
Теперь, когда мы понимаем, как массивы используются и выглядят в машинном коде, давайте перейдём к указателям.
Давайте сразу разберёмся в машинном коде:
int num = 10 в машинном коде
Вывод num — машинный код
Вывод переменной num на экран.
Вывод *pointer — assembly
Вывод переменной pointer на экран.
Вывод адреса num — машинный код
Вывод адреса num с помощью переменной pointer — машинный код
Вывод адреса pointer — машинный код
Динамическое распределение памяти
В этой статье будут рассмотрены следующие виды динамического распределения памяти:
malloc — динамическое выделение памяти
Сначала разберёмся в коде:
Теперь давайте посмотрим на машинный код:
Примечание Во время сборки вы можете увидеть инструкции « nop ». Эти инструкции были специально размещены на этапе подготовки к статье, чтобы различные части кода было проще понимать.
Динамическое распределение памяти с помощью malloc — машинный код
calloc — динамическое чистое выделение памяти
Динамическое распределение памяти с помощью calloc — машинный код
realloc — динамическое перераспределение памяти
Сначала посмотрим код.
Наконец, « 1337 h4x0r @nonymoose » копируется в только что перераспределённое пространство. Наконец, после вывода на экран память освобождается.
2 декабря, Онлайн, Беcплатно
Теперь посмотрим машинный код:
Динамическое распределение памяти с помощью realloc — машинный код
Программирование сокетов
Далее мы рассмотрим программирование сокетов, разобрав очень простую систему клиент-серверного TCP-чата.
Прежде чем мы начнём разбирать код сервера или клиента, важно указать следующую строку кода в верхней части файла:
Серверная часть
Сначала посмотрим на код:
Наконец, сервер отправляет строку serverhello по соединению до возврата функции.
Теперь давайте разберём его в машинном коде:
Инициализация серверных переменных
Сначала создаются и инициализируются переменные сервера.
server = socket(…) — машинный код
setockopt(…) — машинный код
Затем вызывается _setsockopt для задания параметров сокета в файле дескриптора “ server ».
Инициализация address — машинный код
bind(…) — машинный код
listen(…) — машинный код
sock = accept(…) — машинный код
value = read(…) — машинный код
send(…) — машинный код
В конце концов, сервер отсылает сообщение serverhello через переменную s в машинном коде.
Клиентская часть
Сначала разберёмся в коде:
Теперь разбёремся в машинном коде:
Инициализация переменных клиента — машинный код
Сначала инициализируются локальные переменные клиента.
sock = socket(…) — машинный код
memset(…) — машинный код
Клиент — настройка адреса — машинный код
Потом настраивается адресная информация сервера.
inet_pton(…) — машинный код
send(…) — машинный код
После подключения клиент отправляет строку helloClient на сервер.
Многопоточность
Наконец, мы рассмотрим основы потоков в C.
Во-первых, давайте посмотрим на код:
Теперь давайте разберём машинный код:
printf “This is before the thread” — машинный код
Сначала программа печатает «This is before the thread».
Создание нового потока — машинный код
Функция mythread() — машинный код
Как вы можете видеть, функция mythread() просто спит одну секунду перед выводом «Hello from mythread».
Примечание Внутри функции mythread() вы увидите два нопа. Они были специально размещены для облегчения навигации на этапе подготовки этой статьи.
Присоединение потока функции mythread к главному потоку — машинный код
printf “This is after the thread” — машинный код
Наконец, на экран выводится «This is after the thread» и происходит возврат из функции.
Заключение
В статье мы рассмотрели массивы, указатели, динамическое распределение памяти, программирование сокетов (сетевое программирование) и многопоточность. Понимание этих аспектов существенно поможет вам продвинуться в изучении реверс-инжиниринга.
Реверс-инжиниринг для самых маленьких: взлом кейгена
Вначале было слово. Двойное
Открыв файл кейгена в Ida, видим список функций.
Проанализировав этот список, мы видим несколько стандартных функций (WinMain, start, DialogFunc) и кучу вспомогательных-системных. Все это стандартные функции, составляющие каркас.
Пользовательские функции, которые представляют реализацию задач программы, а не ее обертку из API-шных и системных вызовов, дизассемблер не распознает и называет попросту sub_цифры. Учитывая, что такая функция здесь всего одна — она и должна привлечь наше внимание как, скорее всего, содержащая интересующий нас алгоритм или его часть.
Давайте запустим кейген. Он просит ввести две 4-значных строки. Предположим, в функцию расчета ключа отправляются сразу восемь символов. Анализируем код функции sub_401100. Ответ на гипотезу содержится в первых двух строках:
Вторая строка недвусмысленно намекает нам на получение аргумента функции по смещению 8. Однако размер аргумента — двойное слово, равное 4 байтам, а не 8. Значит, вероятнее всего за один проход функция обрабатывает одну строку из четырех символов, а вызывается она два раза.
Вопрос, который наверняка может возникнуть: почему для получения аргумента функции резервируется смещение в 8 байт, а указывает на 4, ведь аргумент всего один? Как мы помним, стек растет вниз; при добавлении в стек значения стековый указатель уменьшается на соответствующее количество байт. Следовательно, после добавления в стек аргумента функции и до начала ее работы в стек добавляется что-то еще. Это, очевидно, адрес возврата, добавляемый в стек после вызова системной функции call.
Найдем места в программе, где встречаются вызовы функции sub401100. Таковых оказывается действительно два: по адресу DialogFunc+97 и DialogFunc+113. Интересующие нас инструкции начинаются здесь:
В чем смысл этих операций? Выяснить очень просто даже на практике, без теории. Поставим в отладчике брейкпойнт, например, на инструкции push eax (перед самым вызовом подфункции) и запустим программу на выполнение. Кейген запустится, попросит ввести строки. Введя qwer и tyui и остановившись на брейкпойнте, смотрим значение еах: 72657771. Декодируем в текст: rewq. То есть физический смысл этих операций — инверсия строки.
Теперь мы знаем, что в sub_401100 передается одна из исходных строк, перевернутая задом наперед, в размере двойного слова, целиком умещающаяся в любом из стандартных регистров. Пожалуй, можно взглянуть на инструкции sub_401100.
Следующая команда LEA ecx, [eax+eax*8] элегантно и непринужденно умножает еах на 9 и записывает результат в есх. Затем это значение копируется в edx и сдвигается вправо на 13 разрядов: получаем 73213 в еdx и E6427B23 в есх. Затем — снова ксорим есх и edx, записывая в есх E6454930. Копируем это в еах, сдвигаем влево на 9 разрядов: 8А926000, затем инвертируем это, получая 756D9FFF. Прибавляем это значение к регистру есх — имеем 5BB2E92F. Копируем это в еах, сдвигаем вправо аж на 17 разрядов — 2DD9 — и ксорим с есх. Получаем в итоге 5BB2C4F6. Затем… затем… что там у нас? Что, все.
Итак, мы сохраняем это значение в область памяти по смещению var_4, загружаем из стека состояния регистров, снова берем из памяти итоговое значение и окончательно забираем из стека оставшиеся там состояния регистров, сохраненные в начале. Выходим из функции. Ура. впрочем, радоваться еще рано, пока что на выходе из первого вызова функции мы имеем максимум — четыре полупечатных символа, а ведь у нас еще целая необработанная строка есть, да и эту еще к божескому виду привести надо.
Перейдем на более высокий уровень анализа — от дизассемблера к декомпилятору. Представим всю функцию DialogFunc, в которой содержатся вызовы sub_401100, в виде С-подобного псевдокода. Собственно говоря, это дизассемблер называет его «псевдокодом», на деле это практически и есть код на С, только страшненький. Глядим:
Эпилог
bash-реализация пресловутой sub_401100:
Основная функция кейгена:
Реверс-инжиниринг для начинающих: основные концепции программирования
Авторизуйтесь
Реверс-инжиниринг для начинающих: основные концепции программирования
В этой статье мы заглянем под капот программного обеспечения. Новички в реверс-инжиниринге получат общее представление о самом процессе исследования ПО, общих принципах построения программного кода и о том, как читать ассемблерный код.
Примечание Программный код для этой статьи компилируется с помощью Microsoft Visual Studio 2015, так что некоторые функции в новых версиях могут использоваться по-другому. В качестве дизассемблера используется IDA Pro.
Инициализация переменных
Переменные — одна из основных составляющих программирования. Они делятся на несколько видов, вот некоторые из них:
Примечание в С++ строка — не примитивная переменная, но важно понять, как она будет выглядеть в машинном коде.
Давайте посмотрим на ассемблерный код:
Здесь можно увидеть как IDA показывает распределение пространства для переменных. Сначала под каждую переменную выделяется пространство, а потом уже она инициализируется.
Как только пространство выделено, в него помещается значение, которое мы хотим присвоить переменной. Инициализация большинства переменных представлена на картинке выше, но как инициализируется строка, показано ниже.
Инициализация строковой переменной в C++
Для инициализации строки требуется вызов встроенной функции.
Стандартная функция вывода
Примечание Здесь речь пойдёт о том, что переменные помещаются в стек и затем используются в качестве параметров для функции вывода. Концепт функции с параметрами будет рассмотрен позднее.
3–5 декабря, Онлайн, Беcплатно
Теперь посмотрим на машинный код. Сначала строковый литерал:
Вывод строкового литерала
Теперь посмотрим на вывод одной из переменных:
Математические операции
Сейчас мы поговорим о следующих математических операциях:
Переведём каждую операцию в ассемблерный код:
Для сложения мы используем инструкцию add :
При вычитании используется инструкция sub :
При умножении — imul :
При поразрядной конъюнкции используется инструкция and :
При поразрядной дизъюнкции — or :
При поразрядном исключающем ИЛИ — xor :
Поразрядное исключающее ИЛИ
При поразрядном отрицании — not :
При битовом сдвиге вправо — sar :
Битовый сдвиг вправо
При битовом сдвиге влево — shl :
Битовый сдвиг влево
Вызов функций
Мы рассмотрим три вида функций:
Вызов функций без параметров
Функция newfunc() просто выводит сообщение «Hello! I’m a new function!»:
Вызов такой функции выглядит следующим образом:
Вызов функции с параметрами
Посмотрим на код функции:
Циклы
Теперь, когда мы изучили вызов функции, вывод, переменные и математику, перейдём к контролю порядка выполнения кода (flow control). Сначала мы изучим цикл for:
Графический обзор цикла for
Прежде чем разбить ассемблерный код на более мелкие части, посмотрим на общий вариант. Как вы можете видеть, когда цикл for запускается, у него есть 2 варианта:
Теперь давайте взглянем на цикл while :
В этом цикле генерируется случайное число от 0 до 20. Если число больше 10, то произойдёт выход из цикла со словами «I’m out!», в противном случае продолжится работа в цикле.
Условный оператор
Теперь поговорим об условных операторах. Для начала посмотрим код:
Посмотрим на ассемблерный граф:
Ассемблерный граф для условного оператора
Оператор выбора
Оператор выбора очень похож на оператор условия, только в операторе выбора одна переменная или выражение сравнивается с несколькими «случаями» (возможными эквивалентностями). Посмотрим код:
Оператор выбора не следует правилу «Если X, то Y, иначе Z» в отличии от условного оператора. Вместо этого программа сравнивает входное значение с существующими случаями и выполняет только тот случай, который соответствует входному значению. Рассмотрим два первых блока подробней.
Два первых блока оператора выбора
Если var_D0 (A) равно 5, то код перейдёт в секцию, которая показана выше, выведет «5» и затем перейдёт в секцию возврата.
Пользовательский ввод
В этом разделе мы рассмотрим ввод пользователя с помощью потока сin из C++. Во-первых, посмотрим на код:
Разберём это в машинном коде. Во-первых, функция cin :
Функция C++ cin детальнее
Мы рассмотрели лишь основные принципы работы программного обеспечения на низком уровне. Без этих основ невозможно понимать работу ПО и, соответственно, заниматься его исследованием.