Реверс-инжиниринг NET-приложений. Часть вторая: Введение в Byte Patching

Реверс-инжиниринг NET-приложений. Часть вторая: Введение в Byte Patching

В первой статье мы рассмотрели самые основы, касающиеся работы 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-кода:

  1. Call: Вызывает метод, указанный в передаваемом дескрипторе. В данном случае вызывается метод сравнения строк.
  2. Ldc.i4.0: Помещает целую величину 0 в стек вычислений как int32.
  3. Bne.un.s: Передает управление целевой инструкции (короткая форма), когда две беззнаковые или неупорядоченные величины с плавающей точкой не равны.
  4. Ldstr: Помещает в стек новый ссылку на объект, который представляет собой строковый литерал метаданных.

На данный момент только инструкция 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 (об этом в следующей статье).

Ссылки

Ученые доказали: чтение нашего канала продлевает жизнь!

Ладно, не доказали. Но мы работаем над этим

Поучаствуйте в эксперименте — подпишитесь