В первой статье мы рассмотрели самые основы, касающиеся работы NET-приложений, механизма компиляции (его мы коснемся вновь более подробно) и декомпиляции кода при помощи утилиты Reflector.
Автор: Суфиан Тахири (Soufiane Tahiri)
Введение
В первой статье мы рассмотрели самые основы, касающиеся работы NET-приложений, механизма компиляции (его мы коснемся вновь более подробно) и декомпиляции кода при помощи утилиты Reflector. Теперь мы знаем, насколько просто обойти защиты, основанные на проверке конкретных серийных номеров (или паролей). На самом деле, это самые азы, и когда мы сталкиваемся с реальными защитами, приходится делать намного больше телодвижений.
Если говорить в общем об исследовании программ (а не только NET-приложений) - это целая наука, а не просто набор техник получения серийных номеров или паролей. Реверс-инжиниринг – искусство манипуляции байтами. Мы можем изменять, активировать или деактивировать отдельные функций программы, а в некоторых случаях, добавлять новый функционал в приложение (что не всегда простая задача). Необходимо отличное знание ассемблера, но не только его. Также требуется точно знать, где и какие байты необходимо изменить в приложении, что, обычно, не так просто.
В этой и следующей статьях мы рассмотрим некоторые техники изменения байтов (или «патчинга» байтов) в различных «доморощенных» программах. Первая цель – наш CrackMe#1-InfoSecInstitute-dotNET-Reversing (который мы изучали в первой статье), второй – «ReverseMe#1-InfoSecInstitute-dotNET-Reversing».
Компиляция NET-приложений
Из первой статьи мы знаем, что каждая NET-программа создается при помощи некоторого высокоуровнего языка (vb.NET, C#), и далее во время компиляции происходит преобразование к низкоуровнему языку Microsoft Intermediate Language (MSIL), который можно считать наименьшим общим знаменателем NET. Мы можем создать приложение, используя только MSIL, и хотя это не так интересно с точки зрения разработчика, но может быть полезно для понимания, как среда Common Language Runtime (CLR) работает и запускает наш высокоуровневый код.
Сродни Java и виртуальной машине Java любая NET-программа сначала компилируется (если можно так выразиться) в промежуточный язык IL (или MSIL), запускается в среде выполнения Common Language Runtime (CLR), а затем преобразуется в native-инструкции x86 или x86-64 в зависимости от того, какой процессор используется в системе. Это делается средой CLR посредством компиляции Just In Time (JIT).
Подводя итог, можно сказать, что CLR использует компилятор JIT для преобразования IL- (или MSIL) кода, который хранится в PE-формате (скомпилированный высокоуровневый NET-код), в платформо-зависимые инструкции, а затем происходит их выполнение. Все это означает, что .NET не является интерпретатором, а использование IL и JIT позволяет добиться переносимости NET-кода.
Наглядное изображение всего процесса показано на рисунке:
Понимание MSIL
Цель данной статьи – познакомить вас с некоторыми новыми инструкциями IL. Помимо очевидного любопытства, понимание IL и механизмов управления им откроет перед вами многие двери при исследовании NET-приложений. Конкретно в нашем случае – позволит выявить дыры в системах безопасности программ.
Перед тем как идти дальше, нелишним будет напомнить, что CLR исполняет IL-код. Учитывая такой механизм выполнения операций и обработки данных, CLR не управляет памятью напрямую. Вместо этого используется стек, представляющий из себя абстрактную структуру данных, которая работает по принципу LIFO (last in first out; последний зашел, первый вышел). Когда речь идет о стеке, мы может выполнять следующие действия: загружать и извлекать данные. После извлечения одного элемента из стека, все остальные перемещаются вверх в начало стека. Мы может управлять только самым верхним элементом.
А теперь вернемся к нашему «CrackMe#1-InfoSecInstitute-dotNET-Reversing». Если мы ввели неправильный пароль, Crack Me выводить сообщение об ошибке:
В прошлой статье мы обнаружили функцию, в которой введеное значение сравнивалось с определенной комбинацией символов. В этой статье мы изменим байты так, чтобы Crack Me принимал все возможные пароли.
Вернемся к декомпилированному Crack Me. Вот функция, которая проверяет введенный пароль:
private
void
btn.Chk.Click(object sender, EventArgs e)
{
if (this.txt.Pwd.Text == “p@55w0rd!”)
{
Interaction.MsgBox(“Congratulations !”, MsgBoxStyle.Information, “Correct!”);
}
else
{
Interaction.MsgBox(“Invalid password”, MsgBoxStyle.Critical, “Error!”);
}
}
Если мы переключим режим просмотра на IL-код, то увидим следующее:
.method
private
instance
void
btn.Chk.Click(object sender, class [mscorlib]System.EventArgs e) cil managed
{
.maxstack 3
L.0000: ldarg.0
L.0001: callvirt instance
class [System.Windows.Forms]System.Windows.Forms.TextBox
InfoSecInstitute.dotNET.Reversing.Form1::get.txt.Pwd()
L.0006: callvirt instance
string [System.Windows.Forms]System.Windows.Forms.TextBox::get.Text()
L.000b: ldstr “p@55w0rd!”
L.0010: ldc.i4.0
L.0011: call int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Operators::CompareString(string, string, bool)
L.0016: ldc.i4.0
L.0017: bne.un.s L.002d
L.0019: ldstr “Congratulations !”
L.001e: ldc.i4.s 0×40
L.0020: ldstr “Correct!”
L.0025: call valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxResult [Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::MsgBox(object, valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxStyle, object)
L.002a: pop
L.002b: br.s L.003f
L.002d: ldstr “Invalid password”
L.0032: ldc.i4.s 0×10
L.0034: ldstr “Error!”
L.0039: call valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxResult [Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::MsgBox(object, valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxStyle, object)
L.003e: pop
L.003f: ret
}
Это прямое представление внутреннего языка IL, с этим уровнем мы будем работать при внесении каких-либо изменений. Как было сказано выше, по существу, NET представляет собой стековую машину, и нам нужно понять, что означают отдельные инструкции IL-кода. Мы можем легко найти описание всех ассемблерных инструкций IL-кода. Я буду объяснять лишь те, которые относятся к реверс-инжинирингу.
IL-инструкции начинаются сразу после строки «.maxstack». Первая инструкция: L.0000: ldarg.0, которая загружает в стек аргумент 0. Эту инструкцию можно сравнить с инструкцией NOP традиционного ассемблерного кода, однако байт-код ldarg равен «00», а не «90». Если мы откроем любую программу в шестнадцатеричном редакторе, то найдем серии байт-кодов, каждый из который обозначает определенную IL-инструкцию нашей программы. Как раз эти байт-коды мы можем менять для изменения функционала программы (например, инвертировать некоторые проверки, переходы и, в общем, менять любые участки кода).
Традиционно, использование шестнадцатеричного редактора в некотором смысле является «грязным» способом для изменения необходимых байтов; позже мы рассмотрим, как это сделать, а также я покажу более «чистые» методы работы. Чтобы узнать смещение байтов, которые мы хотим изменить в редакторе, нам нужно преобразовать последовательность инструкций в набор байт-кодов (чем длиннее строка, тем лучше), а затем найти эту последовательность в файле (средствами редактора).
Каждая IL-инструкция имеет свое байтовое представление. Ниже представлен неполный перечень наиболее важных IL-инструкций и их байтовое представление. Можно использовать этот список в качестве справочника:
IL-инструкция |
Назначение |
Байтовое представление |
And |
Вычисляет побитовое И двух значений и помещает результат в стек вычислений. |
5F |
Beq |
Передает управление целевой инструкции, если два значения равны. |
3B |
Beq.s |
Передает управление целевой инструкции (короткая форма), если два значения равны. |
2E |
Bge |
Передает управление целевой инструкции, если первое значение больше или равно второму. |
3C |
Bge.s |
Передает управление целевой инструкции (короткая форма), если первое значение больше или равно второму. |
2F |
Bge.Un |
Передает управление целевой инструкции, если первое значение больше или равно второму при сравнении беззнаковых целых или неупорядоченных чисел с плавающей точкой. |
41 |
Bge.Un.s |
Передает управление целевой инструкции (короткая форма), если первое значение больше или равно второму при сравнении беззнаковых целых или неупорядоченных чисел с плавающей точкой. |
34 |
Bgt |
Передает управление целевой инструкции, если первое значение больше второго. |
3D |
Bgt.s |
Передает управление целевой инструкции (короткая форма), если первое значение больше второго. |
30 |
Bgt.Un |
Передает управление целевой инструкции, если первое значение больше второго при сравнении беззнаковых целых или неупорядоченных чисел с плавающей точкой. |
42 |
Bgt.Un.s |
Передает управление целевой инструкции (короткая форма), если первое значение больше второго при сравнении беззнаковых целых или неупорядоченных чисел с плавающей точкой. |
35 |
Ble |
Передает управление целевой инструкции, если первое значение меньше или равно второму. |
3E |
Ble.s |
Передает управление целевой инструкции (короткая форма), если первое значение меньше или равно второму. |
31 |
Ble.Un |
Передает управление целевой инструкции, если первое значение меньше или равно второму при сравнении беззнаковых целых или неупорядоченных чисел с плавающей точкой. |
43 |
Ble.Un.s |
Передает управление целевой инструкции (короткая форма), если первое значение меньше или равно второму при сравнении беззнаковых целых или неупорядоченных чисел с плавающей точкой. |
36 |
Blt |
Передает управление целевой инструкции, если первое значение меньше второго. |
3F |
Blt.s |
Передает управление целевой инструкции (короткая форма), если первое значение меньше второго. |
32 |
Blt.Un |
Передает управление целевой инструкции, если первое значение меньше второго при сравнении беззнаковых целых или неупорядоченных чисел с плавающей точкой. |
44 |
Blt.Un.s |
Передает управление целевой инструкции (короткая форма), если первое значение меньше второго при сравнении беззнаковых целых или неупорядоченных чисел с плавающей точкой. |
37 |
Bne.Un |
Передает управление целевой инструкции, если беззнаковые целые или неупорядоченные числа с плавающей точкой не равны. |
40 |
Bne.Un.s |
Передает управление целевой инструкции (короткая форма), если беззнаковые целые или неупорядоченные числа с плавающей точкой не равны. |
33 |
Br |
Безусловно передает управления целевой инструкции. |
38 |
Brfalse |
Передает управление целевой инструкции, если значение равно false, null (Nothing в Visual Basic), или ноль. |
39 |
Brfalse.s |
Передает управление целевой инструкции (короткая форма), если значение равно false, null (Nothing в Visual Basic), или ноль. |
2C |
Brtrue |
Передает управление целевой инструкции, если значение равно true, не равно null, или не равно нулю. |
3A |
Brtrue.s |
Передает управление целевой инструкции (короткая форма), если значение равно true, не равно null, или не равно нулю. |
2D |
Br.s |
Безусловно передает управления целевой инструкции (короткая форма). |
2B |
Call |
Вызывает метод, указанный в передаваемом дескрипторе. |
28 |
Clt |
Сравнивает два значения. Если первое меньше второго, в стек вычислений помещается целое число 1 (int32), иначе 0 (int32). |
FE 04 |
Clt.Un |
Сравнивает беззнаковые или неявные величины value1 и value2. Если value1 меньше value2, тогда в стек вычислений помещается целое число 1 (int32), иначе 0 (int32). |
FE 03 |
Jmp |
Выходит из текущего метода и переходит к другому методу. |
27 |
Ldarg |
Загружает аргумент (на который ссылается индекс) в стек. |
FE 09 |
Ldarga |
Загружает адрес аргумента в стек вычислений. |
FE 0A |
Ldarga.s |
Загружает адрес аргумента, в короткой форме, в стек вычислений. |
0F |
Ldarg.0 |
Загружает аргумент с индексом 0 в стек вычислений. |
02 |
Ldarg.1 |
Загружает аргумент с индексом 1 в стек вычислений. |
03 |
Ldarg.2 |
Загружает аргумент с индексом 2 в стек вычислений. |
04 |
Ldarg.3 |
Загружает аргумент с индексом 3 в стек вычислений. |
05 |
Ldarg.s |
Загружает аргумент (на который ссылается индекс в короткой форме) в стек вычислений. |
0E |
Ldc.I4 |
Помещает значение int32 в стек вычислений как int32. |
20 |
Ldc.I4.0 |
Помещает целое число 0 в стек вычислений как int32. |
16 |
Ldc.I4.1 |
Помещает целое число 1 в стек вычислений как int32. |
17 |
Ldc.I4.M1 |
Помещает целое число -1 в стек вычислений как int32 |
15 |
Ldc.I4.s |
Помещает число int8 в стек вычислений как int32, короткая форма. |
1F |
Ldstr |
Помещает в стек новую ссылку на объект, который представляет собой строковый литерал метаданных. |
72 |
Leave |
Выходит из защищенной области кода, безусловно передавая управление определенной целевой инструкции. |
DD |
Leave.s |
Выходит из защищенной области кода, безусловно передавая управление определенной целевой инструкции (короткая форма). |
DE |
Mul |
Умножает два значения и помещает результат в стек вычислений. |
5A |
Mul.Ovf |
Умножает два целочисленных значения, проверяет на переполнение и помещает результат в стек вычислений. |
D8 |
Mul.Ovf.Un |
Умножает два беззнаковых целочисленных значения, проверяет на переполнение и помещает результат в стек вычислений. |
D9 |
Neg |
Инвертирует значение и помещает результат в стек вычислений. |
65 |
Newobj |
Создает новый объект или новый тип значения, помещает ссылку на объект (тип O) в вершину стека вычислений. |
73 |
Not |
Вычисляет побитовое дополнение целого числа на вершине стека и помещает результат (того же типа) в стек вычислений. |
66 |
Or |
Вычисляет побитовое дополнение двух целых чисел на вершине стека и помещает результат в стек вычислений. |
60 |
Pop |
Удаляет текущее значение, находящееся на вершине стека вычислений. |
26 |
Rem |
Помещает остаток от деления двух чисел в стек вычислений. |
5D |
Rem.Un |
Помещает остаток от деления двух беззнаковых целых чисел в стек вычислений. |
5E |
Ret |
Возвращается в текущий метод, помещая возвращаемое значение (если оно есть) в стек вычислений. |
2A |
Rethrow |
Возвращает текущее исключение. |
FE 1A |
Stind.I1 |
Сохраняет число типа int8 по указанному адресу. |
52 |
Stind.I2 |
Сохраняет число типа int16 по указанному адресу. |
53 |
Stind.I4 |
Сохраняет число типа int32 по указанному адресу. |
54 |
Stloc |
Извлекает текущее значение, находящееся на вершине стека вычислений, и помещает его в локальную переменную с определенным индексом. |
FE 0E |
Sub |
Помещает в стек вычислений разность двух чисел. |
59 |
Sub.Ovf |
Вычисляет разность двух целых чисел, проверяет на переполнение и помещает результат в стек вычислений. |
DA |
Sub.Ovf.Un |
Вычисляет разность двух беззнаковых целых чисел, проверяет на переполнение и помещает результат в стек вычислений. |
DB |
Switch |
Выполняет переход к одному из значений. |
45 |
Throw |
Создает исключение и помещает его в стек вычислений. |
7A |
Xor |
Вычисляет побитовое исключающее ИЛИ двух чисел, находящихся на вершине стека вычислений, и помещает результат в стек вычислений |
61 |
Теперь, когда у нас есть добротный справочник IL-инструкций, вернемся к Reflector и нашему Crack Me и подумаем, как можно обойти защиту:
Из картинки, показанной выше, мы видим, что участок кода:
private
void
btn_Chk_Click(object sender, EventArgs e)
{
if (this.txt_Pwd.Text == “p@55w0rd!”)
{
Interaction.MsgBox(“Congratulations !”, MsgBoxStyle.Information, “Correct!”);
Преобразовался к следующему набору инструкций:
L_0010: ldc.i4.0
L_0011: call int32 [Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.Operators::CompareString(string, string, bool)
L_0016: ldc.i4.0
L_0017: bne.un.s L_002d
L_0019: ldstr “Congratulations !”
L_001e: ldc.i4.s 0×40
L_0020: ldstr “Correct!”
L_0025: call valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxResult [Microsoft.VisualBasic]Microsoft.VisualBasic.Interaction::MsgBox(object, valuetype [Microsoft.VisualBasic]Microsoft.VisualBasic.MsgBoxStyle, object)
Используя справочник IL-инструкций, выясняем логику работы IL-кода:
На данный момент только инструкция bne.un.s представляет сколь-нибудь существенный интерес. В случае с IL-выражением происходит трансформация в инструкцию передачи управления. Так, инструкция bne означает «если не равно» (BranchNotEqual) и используется, если две величины на вершине стека не равны между собой. Далее происходит переход к строке L_002d:
L_0016: ldc.i4.0
L_0017: bne.un.s L_002d
L_0019: ldstr “Congratulations !”
Постепенно ситуация проясняется, и мы начинаем понимать, как обойти проверку правильного пароля. Вместо показа фразы «Congratulations !» (в случае правильного пароля), мы можем сделать так, чтобы Crack Me показывал это сообщение, если пароль неправильный. Инструкция, которая для этого используется - Beq.s, ее назначение: «Передать управление целевой инструкции (короткая форма), если два значения равны между собой» (см. справочник инструкций).
Задачи:
Строго говоря, у нас есть две задачи. Во-первых, необходимо найти байтовое представление IL-инструкции, которое мы хотим изменить. Во-вторых, необходимо найти точное смещение инструкции, чтобы найти ее в шестнадцатеричном редакторе.
Решения:
В справочнике находим байтовое представление необходимых инструкций: bne.un.s = 0×33 и Beq.s = 0x2E; чтобы найти место файле, где нужно внести изменения, мы должны преобразовать в набор байтов последовательность инструкций (желательно, чтобы строка была достаточно длинной).
ldc.i4.0 = 0×16, bne.un.s L_002d = 0×33 и 0x?? (байтовое представление L_002d) и ldstr = 0×72.
Таким образом, строка поиска выглядит так: 1633??72. Знаки «??» означаю служебные символы регулярного выражения или знаки подстановки (в wildcard) (в зависимости от шестнадцатеричного редактора). Я пользуюсь WinHex, однако вы можете использовать любой другой шестнадцатеричный редактор:
Нам нужно поправить строку 16331472 на 162E1472 (не забывайте делать резервные копии).
Тестирование внесенных изменений
Запускаем модифицированную версию нашего Crack Me и вводим случайный пароль:
Кажется, все прошло на ура. Теперь посмотрим, как наши изменения отразились в исходном тексте (смотрим Reflector):
Переключаемся в режим просмотра IL-инструкций и видим следующее:
Мы рассмотрели самые основы, которые помогут нам в будущем освоить более сложные техники Byte Patching (об этом в следующей статье).
Ссылки
Ладно, не доказали. Но мы работаем над этим