запутанный C # или Java легче декомпилировать, чем C, из-за разницы в стековой машине? или есть другие причины?

Я видел, как говорилось, что декомпиляция запутанного C# и Java упрощается за счет наличия вызовов API фреймворка, например String. Но для меня это не совсем имеет смысл, потому что не должна ли программа на C также иметь очевидные вызовы некоторых стандартных библиотек, которые были бы эквивалентны API C#?

Я также видел, как говорилось, что каким-то образом различие между регистровой машиной (аппаратное обеспечение, которое будет запускать сборку из C) и стековой машиной (виртуальная машина, которая будет запускать байт-код) важно для сложности декомпиляции.

Так является ли здесь главной проблемой машины стека/регистра? Скажем, если бы виртуальная машина CLR была повторно реализована как регистровая машина, стал бы вдруг байт-код C# так же трудно декомпилировать, как исполняемый файл C? Или есть какие-то другие существенные различия в сложности, которые не исчезнут при таком капитальном ремонте платформы?


person EndangeringSpecies    schedule 10.05.2012    source источник


Ответы (3)


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

.NET и JVM так легко декомпилировать в основном из-за метаданных: имен типов и методов и т.д.

С урезанным собственным исполняемым файлом вы получите все самое интересное: никаких осмысленных имен для функций, никаких явных типов данных, множество встроенного (а затем сильно изуродованного дальнейшей оптимизацией) кода, развернутые циклы, несокращаемый поток управления, развернутый хвост звонки и т.д.

В байт-коде большая часть таких оптимизаций не выполнялась (оставляя их JIT), поэтому он намного ближе к исходному коду, чем если бы были удалены метаданные и применены оптимизации. Неважно, стековая машина, регистровый, многопоточный форт-код или что-то еще.

person SK-logic    schedule 10.05.2012
comment
Отмечу, что в моем вопросе обсуждается запутанный C#, поэтому имена идентификаторов больше не являются проблемой. То, что вы говорите об отсутствии явных (даже с запутанным именем) типов данных, интересно, но я должен задаться вопросом, можно ли также вывести типы данных из сборки с помощью соответствующих инструментов. То, что вы говорите о развернутых циклах, хвостовых вызовах и других оптимизациях, также звучит интересно и предлагает естественные следующие шаги в эволюции обфускатора. - person EndangeringSpecies; 10.05.2012
comment
@EndangeringSpecies, может быть возможно деконструировать некоторые типы данных, но в большинстве случаев вы даже не сможете сопоставить эти типы друг с другом (например, если вы обращаетесь только к первому полю структуры, существует невозможно сделать вывод, что это та же структура, что и в функции, обращающейся только к своему второму полю). - person SK-logic; 10.05.2012
comment
хорошо, похоже, вы подчеркиваете проблему разрешения типов данных структур в динамически выделяемой памяти, потому что нет явного доступа к полю через ссылку на экземпляр. (Поэтому, по-видимому, мы найдем много статей о статическом анализе этой проблемы, но мало бесплатных качественных инструментов). Это интересно, потому что звучит как еще одно возможное будущее обфускатора, обфускация через замену объектов псевдодинамической памятью. Прохладный... - person EndangeringSpecies; 10.05.2012

В машинном коде, предназначенном для реального оборудования, вы не всегда знаете, где в памяти начинается код. Поскольку инструкции x86 имеют переменную длину, дизассемблер может декодировать инструкции с неправильным смещением. Также не помогает возможность арифметики указателей. В кодах операций .NET IL и java всегда ясно, где начинается и заканчивается код, а произвольная арифметика указателей не допускается. Поэтому дизассемблирование выполняется на 100% точно, даже если сгенерированный ассемблерный код трудночитаем. С реальным машинным кодом, по крайней мере на x86, вы никогда не узнаете точный поток управления и точки входа в код, если не запустите программу, даже если вы предполагаете, что не происходит преобразования кода.

person alexg    schedule 10.05.2012
comment
Можете ли вы пояснить, почему инструкции переменной длины являются проблемой? Разве токенизация инструкций не очевидна для декомпилятора? Если бы я мог извлечь список инструкций и их адреса, разве я не смог бы правильно разрешить все операторы goto? - person EndangeringSpecies; 10.05.2012
comment
Вы правы в том, что переменная длина здесь не совсем проблема. Проблема в том, что вы не можете точно знать точку входа в блок кода. Но если бы инструкции были фиксированной длины, я считаю, что было бы легче определить, когда дизассемблирование началось с неправильной точки входа, потому что вы бы быстрее увидели, что некоторые коды операций не имеют смысла, и вы бы вернулись и попробовали еще раз. с другим адресом. Переменная длина означает, что коды операций более компактны, поэтому меньше избыточности и меньше ошибок. Я думаю :) - person alexg; 11.05.2012
comment
что означает блок кода в более понятных терминах языка высокого уровня, то есть о какой детализации мы говорим здесь? Это методы? Или несколько строк без goto? Или несколько методов, сгруппированных по какому-то принципу? - person EndangeringSpecies; 11.05.2012

Для сравнения C++ и Java с точки зрения простоты обратного проектирования прочитайте вводный раздел моя статья. (Вы можете прочитать «С#» вместо «Java» и «CLR» вместо «JVM» :))

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

person Dmitry Leskov    schedule 03.06.2012