Руководство по исследованию программ


Приводим статью, которая увидела свет еще в середине 2005-го года на станицах ресурса WASM.RU. Приводим статью полностью, без редактирования. Актуальность информации (Visual Basic 6.0 и ОС тех лет), а также актуальность приведенных ссылок требует дополнительного анализа. Но речь не об этом.

[Вступление]

Многие почему то считают, что Visual Basic это примитивный язык программирования, который не может компилировать программы, работать с адресами переменных в памяти и не позволяющий вставлять ассемблерные процедуры в код. Так вот, все это неправда. Начиная с версии 5.0 данный язык позволяет компилировать программы в Native Code, а также имеется возможность работы с адресами переменных в памяти (для этого существует функция varptr). Ассемблерные процедуры вставлять тоже можно, но не так просто. Для этого я написал уже 2 части статьи по вставке ассемблерных процедур в код на Visual Basic. Эти статьи можно найти на сайте www.dotfix.net. Как видите, недостатков у VB не так уж и много, а преимуществ настолько много, что я расскажу лишь о самых очевидных и важных:

  • Полноценная компиляция и маленький размер получаемых exe файлов. Несмотря на то, что программы требуют библиотеку MSVBVM60.DLL, это не является недостатком, так как эта библиотека интегрированная во все новые операционные системы и таскать ее вместе с приложением не нужно.
  • Простота написания кода. Все удобно и наглядно. Даже начинающий может писать неоптимизированные, но рабочие программы. Как говориться, умеешь писать качественно — пиши, от этого программы будут только лучше. Не умеешь — можешь писать неоптимизированные проги (переменные объявлять не обязательно, преобразовывать типы данных тоже не обязательно). Но постепенно приходят более глубокие познания и можно писать крупные проекты, не сильно уступающие аналогам на C++ или Delphi по скорости работы, но на разработку которых уходит гораздо меньше времени.Так вот, начинающие исследователи программ почему то считают, что программы, написанные на VB невозможно сломать. Если программа скомпилирована в pcode — я частично с ними соглашусь, но даже для этого вида компиляции уже написано множество отладчиков, способных понимать псевдокодовые инструкции. Что же касается Native Code, то тут все ломается также, как и любая другая программа, написанная например на C++ или Delphi. Но есть ряд особенностей. Все операции, которые выполняет программа выполняются с использованием узкоспециализированных функций из библиотеки MSVBVM60.DLL. Имена этих функций напоминают операторы Visual Basic’а, поэтому глядя на названия большинства из них, можно понять, какие операции они позволяют выполнять. Но есть и такие, которые не поддаются логике. О них я и расскажу в данной статье.

    [Функции]

    Самые непонятные исследователям функции — функции для преобразования данных из одного типа в другой. Чуть ниже я для удобства приведу существующие типы данных, используемых в Visual Basic 6.0 и функции для работы с ними. За большую часть данной информации хотелось бы поблагодарить Eternal_Bliss (http:\crackmes.cbj.net) и авторов сайта http://www.infonegocio.com/vbcrack/, хотя на момент второй редакции статьи (которую Вы сейчас видите перед собой) мне прилось значительно расширить эту информацию основываясь на собственном опыте.
    Часть имени каждой функции преобразования данных представляет собой набор аббревиатур, обозначающих типы исходных и конечных данных. Вот их расшифровка:

    Тип данных Расшифровка
    bool boolean
    str string
    i2 byte or integer (2 битный integer)
    ui2 unsigned integer (2 битный unsigned integer)
    i4 long (4 битный integer)
    r4 single (4 битный real)
    r8 double (8 битный real)
    cy currency
    var variant (VB) или variable (OLEAUT)
    fp число с плавающей точкой
    cmp сравнение
    comp сравнение

    Ниже представленые функции, экспортируемые библиотекой MSVBVM60.DLL. Если Вам потребуется вызывать эти вункции из своих программ, то имейте ввиду, что у VB свои типы данных, отличные в некоторых случаях от тех, которые используются в Delphi и С++. Строки вообще имеют свой особый формат (в начале строки два (или четыре) байта указывают на длину, затем идет строка). Отсюда бессмысленно пытаться вызвать перечисленные ниже функции из Delphi или C++ — такой вызов скорее всего приведет к ошибке. Что касается конвенции вызовов, то VB использует stdcall и другие не поддерживает, из чего логично сделать вывод, что Dll написанную на C++ с конвенцией вызова cdecl вызвать из VB практически невозможно.

  • Функции для преобразования типов данных
    __vbaI2Str преобразует String в Integer
    __vbaI4Str преобразует String в Long
    __vbar4Str преобразует String в Single
    __vbar8Str преобразует String в Double
    VarCyFromStr преобразует String в Currency
    VarBstrFromI2 преобразует Integer в String
  • Перенос данных
    __vbaStrCopy копирует строку в память — аналог API функции HMEMCPY
    __vbaVarCopy копирует переменный тип (variant) в память
    __vbaVarMove копирует переменный тип (variant) в память
  • Математические функции
    __vbavaradd сложение двух переменных типа Variant
    __vbavarsub деление двух переменных типа Variant
    __vbavarmul умножение двух переменных типа Variant
    __vbavaridiv сложение двух переменных типа Variant с выводом результата в переменную типа Integer
    __vbavarxor XOR
  • Другие функции
    __vbavarfornext используется в конструкциях For… Next… (Loop)
    __vbafreestr удаление переменной
    __vbafreeobj удаление объекта
    __vbastrvarval получения численного значения из строки
    multibytetowidechar преобразование кодировки
    rtcMsgBox показывает сообщение — аналог API messagebox/a/exa
    __vbavarcat объединяет две переменные типа Variant
    __vbafreevar удаляет переменную типа Variant
    __vbaobjset создает объект
    __vbaLenBstr определяет длину строки
    rtcInputBox показывает форму с полем ввода (используются также API функции getwindowtext/a, GetDlgItemtext/a)
    __vbaNew аналог API функции Dialogbox
    __vbaNew2 аналог API функции Dialogboxparam/a
    rtcTrimBstr удаляет пробелы вначале и в конце строки
  • Функции сравнения
    __vbastrcomp сравнивает 2 строковые переменные — аналог API функции lstrcmp
    __vbastrcmp сравнивает 2 строковые переменные — аналог API функции lstrcmp
    __vbavartsteq сравнивает 2 Variant переменные
    __vbaFpCmpCy сравнивает значение с плавающей точкой с Currency значением

    [Разблокирование элементов управления]

    Любой элемент управления на форме может быть видимым или невидимым, доступным или заблокированным. Очень часто для взлома программы бывает нужно разблокировать отдельные элементы управления на форме. Для установки свойств объектов существует функция __vbaObjSet. Именно с помощью нее можно заблокировать или разблокировать элемент управления и изменить любое из его свойств. Поэтому нам необходимо отлавливать вызов именно этой функции. Откроем например Olly Debugger, найдем эту функцию среди вызываемых программой и поставим на нее бряк нажав кнопку F2. Затем запустим исследуемую программу. Когда бряк сработает — посмотрите окружающий код. Если он напоминает

    CODE NOW!
    50 push eax
    52 push edx
    FFD7 call edi
    8BD8 mov ebx, eax
    6A00 push 00
    53 push ebx
    8B03 mov eax, dword ptr [ebx]

    то вы на верном пути. Как вы думаете, что это за «push 00»? 00h в VB означает FALSE, а FFh TRUE, из этого следует, что данная команда устанавливает свойство в FALSE, то есть возможно это и есть блокировка элемента управления на форме. Но это может быть и установка любого другого свойства формы в TRUE. Так как синтаксис один и тот же, что принадлежность данной команды к изменению свойства блокировки можно установить только анализом окружающего кода, но умаю с этой мелочью вы справитесь сами.

    [Методика взлома простейших проверок пароля]

    Нижеследующий текст — мой вольный перевод статьи How to Research Visual Basic Cracking с сайта http://www.infonegocio.com/vbcrack/. За английскую версию текста спасибо ее авторам.

    Что нам может понадобиться? Любой дизассемблер/отладчик. Подойдет Win32Dasm или Olly Debugger Если вы используете W32Dasm, то функции, используемые программой вы можете посмотреть в меню «Functions» -> «Imports».

    Если вы будете исследовать базу данных Jet, которая использует драйвера Micro$oft, то помните, что названия функции не говорят сами за себя и вам будет трудно узнать действие, которое выполняет та или иная функция. в данном случае Вам остается только смотреть рекомендации к функциям, которые любезно оставлены программистами Micro$oft. Дыр же в этих драйверах практически нет, поэтому исследовать программы для работы с базами данных довольно сложно.

    Рассмотрим пример защиты программы паролем. Для удобства поиска нужного кода поставим MessageBox. Откройте Visual Basic, добавьте на форму текстовое поле и кнопку, затем в обработчике щелчка по кнопке напишите следующий код:

    CODE NOW!
    Private Sub Command1_Click()
    Dim X&
    X = 43690
    MsgBox «Testing password»
    If CLng(Trim$(Text1.Text)) = X Then MsgBox («GoodBoy»)
    End Sub

    Теперь откроем Olly Debugger, загрузим в него прогу (предварительно прогу нужно откомпилировать) и ищем вызов функции rtcMsgBox.

    CODE NOW!
    00401F54 FF1520104000 CALL [MSVBVM60!rtcMsgBox] ;MsgBox «Testing password»
    00401F5A 8D459C LEA EAX,[EBP-64]
    00401F5D 8D4DAC LEA ECX,[EBP-54]
    00401F60 50 PUSH EAX
    00401F61 8D55BC LEA EDX,[EBP-44]
    00401F64 51 PUSH ECX
    00401F65 8D45CCLEA EAX,[EBP-34]
    00401F68 52 PUSH EDX
    00401F69 50 PUSH EAX
    00401F6A 6A04 PUSH 04
    00401F6C FF1508104000 CALL [MSVBVM60!__vbaFreeVarList]
    00401F72 8B0E MOV ECX,[ESI]
    00401F74 83C414 ADD ESP,14
    00401F77 56 PUSH ESI
    00401F78 FF9104030000 CALL [ECX+00000304]
    00401F7E 8D55DCLEA EDX,[EBP-24]
    00401F81 50 PUSH EAX
    00401F82 52 PUSH EDX
    00401F83 FF1524104000 CALL [MSVBVM60!__vbaObjSet]
    00401F89 8BF0 MOV ESI,EAX
    00401F8B 8D4DE4LEA ECX,[EBP-1C]
    00401F8E 51 PUSH ECX
    00401F8F 56 PUSH ESI
    00401F90 8B06 MOV EAX,[ESI]
    00401F92 FF90A0000000 CALL [EAX+000000A0]
    00401F98 3BC7 CMP EAX,EDI
    00401F9A DBE2 FCLEX
    00401F9C 7D12 JGE 00401FB0
    00401F9E 68A0000000 PUSH 000000A0
    00401FA3 6804184000 PUSH 00401804
    00401FA8 56 PUSH ESI
    00401FA9 50 PUSH EAX
    00401FAA FF1518104000 CALL [MSVBVM60!__vbaHresultCheckObj] ;получение содержимого Text1.Text
    00401FB0 8B55E4MOV EDX,[EBP-1C]
    00401FB3 52 PUSH EDX
    00401FB4 FF1514104000 CALL [MSVBVM60!rtcTrimBstr] ;Trim$
    00401FBA 8BD0 MOV EDX,EAX
    00401FBC 8D4DE0 LEA ECX,[EBP-20]
    00401FBF FF1584104000 CALL [MSVBVM60!__vbaStrMove]
    00401FC5 50 PUSH EAX
    00401FC6 FF1568104000 CALL [MSVBVM60!__vbaI4Str] ;CLng
    00401FCC 33C9 XOR ECX,ECX
    00401FCE 3DAAAA0000 CMP EAX,0000AAAA ;Число 43690 в HEX виде
    00401FD3 8D55E0LEA EDX,[EBP-20]
    00401FD6 8D45E4LEA EAX,[EBP-1C]
    00401FD9 0F94C1 SETZ CL
    00401FDC 52 PUSH EDX
    00401FDD 50 PUSH EAX
    00401FDE F7D9 NEG ECX
    00401FE0 6A02 PUSH 02
    00401FE2 8BF1 MOV ESI,ECX
    00401FE4 FF156C104000 CALL [MSVBVM60!__vbaFreeStrList]
    00401FEA 83C40C ADD ESP,0C
    00401FED 8D4DDC LEA ECX,[EBP-24]
    00401FF0 FF1594104000 CALL [MSVBVM60!__vbaFreeObj]
    00401FF6 663BF7 CMP SI,DI
    00401FF9 7463 JZ 0040205E ;переход на GoodBoy код (MsgBox «GoodBoy»)
    00401FFB B804000280 MOV EAX,80020004
    00402000 8D558C LEA EDX,[EBP-74]
    00402003 8D4DCCLEA ECX,[EBP-34]
    00402006 8945A4 MOV [EBP-5C],EAX
    00402009 895D9C MOV [EBP-64],EBX
    0040200C 8945B4 MOV [EBP-4C],EAX
    0040200F 895DAC MOV [EBP-54],EBX
    00402012 8945C4 MOV [EBP-3C],EAX
    00402015 895DBC MOV [EBP-44],EBX
    00402018 C7459418184000 MOV DWORD PTR [EBP-6C],00401818
    0040201F C7458C08000000 MOV DWORD PTR [EBP-74],00000008
    00402026 FF157C104000 CALL [MSVBVM60!__vbaVarDup]
    0040202C 8D4D9C LEA ECX,[EBP-64]

    Чтобы взломать данную программу — нужно изменить переход по адресу 00401FF9
    с условного на абсолютный. Для этого меняем JE на JMP (то есть 74h на EBh).
    Как видите — простенькие защиты VB программ ломаются не сложнее чем в программах, написанных на
    Delphi или C++

    [Взлом паролей в виде разных типов данных]

    Нижеследующий тест — мой вольный перевод статей:
    Cracking Visual Basic Serials — Strings, integers and Longs
    Cracking Visual Basic Serials — Single, Double and XOR
    с сайта http://www.infonegocio.com/vbcrack/. За английские версии этих статей спасибо их авторам.

  • Получение пароля, если он лежит строкой в открытом видеКогда VB копирует строку в память — Вы ее можете отловить.
    Для этого нужно поставить бряк на функцию __vbaStrCopy и когда бряк сработает, посмотрите код после je 66047B00 (он содержится в библиотеке и един для всех программ). вам нужно посмотреть содержимое edx-04 (в SoftICE нужно ввести команду d edx-04). При этом вы увидите строчку, с которой работает в данный момент функция.

    CODE NOW!
    Exported fn(): __vbaStrCopy — Ord:008Ah
    :66024532 56 PUSH ESI
    :66024533 57 PUSH EDI
    :66024534 85D2 TEST EDX, EDX
    :66024536 8BF9 MOV EDI, ECX
    :66024538 0F84C2350200 JE 66047B00
    :6602453E FF72FC PUSH [EDX-04] ;по этому адресу лежит строка
    :66024541 52 PUSH EDX
  • Получение пароля, если он сравнивается с введеннымПоставьте бряк на функцию __vbaStrComp и чуть выше ее вызова будут два push’а — они заносят в стек две Variant переменные для сравнения. Посмотрите содержимое EAX чтобы просмотреть адрес теста. Просмотрите, что лежит по этому адресу, наверняка это правильный пароль.
    CODE NOW!
    00401BC7 50 PUSH EAX ;то что Вы ввели
    00401BC8 6880174000 PUSH 00401780 ;верный пароль
    00401BCD FF1530104000 CALL [MSVBVM60!__vbaStrComp]
  • Получение пароля, если происходит сравнение 4х байтовых числовых переменныхПоставьте бряк на функцию __vbai4str. Эта функция используется программой для перевода введенной текстовой строки в 4х байтовое число. Когда бряк сработает — посмотрите код ниже — там наверняка будет проца сравнения паролей. Но значение пароля будет в HEX виде (например десятичное число 987654321 равно шестнадцатеричному 3ADE68B1).
    CODE NOW!
    00401B77 FF155C104000 CALL [MSVBVM60!__vbaI4Str]
    00401B7D 8D4DE0 LEA ECX, [EBP-20]
    00401B80 8BF8 MOV EDI, EAX
    00401B82 FF157C104000 CALL [MSVBVM60!__vbaFreeStr]
    00401B88 8D4DDC LEA ECX, [EBP-24]
    00401B8B FF1580104000 CALL [MSVBVM60!__vbaFreeObj]
    00401B91 81FFB168DE3A CMP EDI, 3ADE68B1 ; 3ADE68B1 — это пароль
    00401B97 7520 JNZ 00401BB9 ;функция вернет ноль
    ;если пароли верны, следовательно этот переход
    ;сработает только при неверном пароле

    Тут мы можем узнать верный пароль или отключить проверку пароля (для этого нужно JNZ заменить на NOP (то есть в данном случае менять нужно байты по адресу 00401B97 с 7520 на 9090)

  • Получение пароля при сравнивании двух 2х байтовых чиселОтличие от предыдущего примера в том, что нужно ставить бряк на функцию __vbai2Str. Но иногда предварительно производится преобразование строки сначала в 4х байтовое число посредством функции __vbaI2I4, а затем 4х байтовое число преобразуется в двухбайтовое.
  • Обход сравнения двух переменных типа SingleЭту проверку обойти довольно просто. Если Вашей целью является написание патча, то ставьте бряк на функцию __vbaR4Str и когда он сработает — пролистните код вниз. Вы увидите переход после проверки. Просто измените условие перехода на обратное.
    CODE NOW!
    00401C0C FF153C104000 CALL [MSVBVM60!_vbaR4Str]
    00401C12 D95DE4 FSTP REAL4 PTR [EBP-1C]
    00401C15 8D4DDC LEA ECX, [EBP-24]
    00401C18 8D55E0 LEA EDX, [EBP-20]
    00401C1B 51 PUSH ECX
    00401C1C 52 PUSH EDX
    00401C1D 6A02 PUSH 02
    00401C1F FF1570104000 CALL [MSVBVM60!__vbaFreeStrList]
    00401C25 83C40C ADD ESP, 0C
    00401C28 8D4DD8 LEA ECX, [EBP-28]
    00401C2B FF1594104000 CALL [MSVBVM60!__vbaFreeObj]
    00401C31 817DE43A92FC42 CMP DWORD PTR [EBP-1C], 42FC923A
    00401C38 7520 JNZ 00401C5A ;если ноль, то пароль неправильный

    Замените инструкцию по адресу 00401C38 на 9090 (чтобы в данном месте не выполнялось никаких операций) или измените условный переход на противоположный. Программисты на Visual Basic не смогут проверить то, что вы заменили эти байты на два NOP’а (9090). Если у Вас все же возникают опасения, что программа перестанет работать при отсутствии команд, просто введите команды, которые ничего не поменяют, например:

    CODE NOW!
    inc eax
    dec eax

    Эти команды однобайтовые и как раз впишутся на место условного перехода

  • Обход проверки значений с плавающей точкой (double).Это также сделать очень просто. Если хочется обойтись битхаком — ставьте бряк на функцию __vbaR8Str и пролистните ком чуть ниже бряка, Вы увидите процедуру сравнения (FCOMP REAL8 PTR [Address]), после нее идет TEST и jump. Измените условие перехода на противоположное.
    CODE NOW!
    00401C55 FF1510104000 CALL [MSVBVM60!rtcTrimBstr]
    00401C5B 8BD0 MOV EDX, EAX
    00401C5D 8D4DD8 LEA ECX, DWORD PTR [EBP-28]
    00401C60 FF1580104000 CALL [MSVBVM60!__vbaStrMove]
    00401C66 50 PUSH EAX
    00401C67 FF1560104000 CALL [MSVBVM60!__vbaR8Str]
    00401C6D DC1DD8104000 FCOMP REAL8 PTR [004010D8] ;сравнение паролей
    00401C73 DFE0 FSTSW AX ;обработка сопроцессором
    00401C75 F6C440 TEST AH, 40 ;проверка правильности пароля
    00401C78 7409 JE 00401C83 ;переход, если пароль верный

    Замените инструкцию по адресу 00401C78 на 9090 (чтобы в данном месте не выполнялось никаких операций) или измените условный переход на противоположный. Программисты на Visual Basic не смогут проверить то, что вы заменили эти байты на два NOP’а (9090). Если у Вас все же возникают опасения, что программа перестанет работать при отсутствии команд, просто введите команды, которые ничего не поменяют, например:

    CODE NOW!
    inc eax
    dec eax

    Эти команды однобайтовые и как раз впишутся на место условного перехода

  • Получение пароля, если он поXORен.Алгоритм работы бейсиковской процедуры таков. Введенный пароль посимвольно переводится в ASCII коды, которые по очереди XORятся. Затем производится обратный перевод, возможно с предварительными математическими манипуляциями с ASCII кодами. ANSI коды могут XORиться со случайным или фиксированным числом.
    CODE NOW!
    00401E6C 50 PUSH EAX
    00401E6D FF1544104000 CALL [MSVBVM60!rtcMidCharBstr]
    00401E73 8BD0 MOV EDX, EAX
    00401E75 8D4DC8 LEA ECX, [EBP-38]
    00401E78 FFD6 CALL ESI
    00401E7A 50 PUSH EAX
    00401E7B FF1518104000 CALL [MSVBVM60!rtcAnsiValueBstr]
    00401E81 0FBFC8 MOVSX ECX, AX
    00401E84 81F191000000 XOR ECX, 00000091 ;ANSI XOR 91
    00401E8A 51 PUSH ECX ;результат XOR’а
    00401E8B FF1570104000 CALL [MSVBVM60!rtcBstrFromAnsi]

    Ставим бряк на 00401E8A 51 PUSH ECX и отслеживаем изменение содержимого регистра ECX при прохождении цикла. Когда программа пройдет весь цикл — вы получите полный пароль.

    CODE NOW!
    CALL [MSVBVM60!rtcMidCharBstr] ;получает один символ из пароля
    CALL [MSVBVM60!rtcAnsiValueBstr] ;получает из символа Ansi код.
    CALL [MSVBVM60!rtcBstrFromAnsi] ;преобразует Ansi код в строку

    В этом случае ниже скорее всего будет сравнение и переход, при этом CALL [MSVBVM60!__vbaStrComp] может быть использован для сравнения строк. Если не производится обращение к MSVBVM60!rtcBstrFromAnsi тогда с паролем производятся определенные математические манипуляции, при этом он может быть представлен как 2х или 4х битное число или число с плавающей точкой. Об этих случаях было сказано выше.

    [Заключение]

    Надеюсь, что прочитав данную статью Вас уже больше не пугает исследование VB кода. Чтобы потренироваться советую поисследовать мои крякми (лежат на www.dotfix.net в разделе «Разное»), о взломе первого из которых написана статья (лежит на www.dotfix.net в разделе «Статьи»). Второй мой крякми еще никто не взломал 😉

    Еще раз благодарю авторов сайта http://www.infonegocio.com/vbcrack/, так как если бы не их туториалы на английском, то этой статьи возможно и не было бы.

    Удачи и спасибо за то, что дочитали статью до конца

    [C] GPcH

Источник WASM.RU


Поделиться в соц сетях

Руководство по исследованию программ, написанных на Visual Basic 6.0

Дата публикации 7 июн 2005

Руководство по исследованию программ, написанных на Visual Basic 6.0 — Архив WASM.RU

[Вступление]

Многие почему то считают, что Visual Basic это примитивный язык программирования,
который не может компилировать программы, работать с адресами переменных в памяти
и не позволяющий вставлять ассемблерные процедуры в код. Так вот, все это неправда.
Начиная с версии 5.0 данный язык позволяет компилировать программы в Native Code, а
также имеется возможность работы с адресами переменных в памяти (для этого существует
функция varptr). Ассемблерные процедуры вставлять тоже можно, но не так просто. Для
этого я написал уже 2 части статьи по вставке ассемблерных процедур в код на Visual Basic.
Эти статьи можно найти на сайте www.dotfix.net. Как видите, недостатков у VB не так уж
и много, а преимуществ настолько много, что я расскажу лишь о самых очевидных и важных:

  • Полноценная компиляция и маленький размер получаемых exe файлов.
    Несмотря на то, что программы требуют библиотеку MSVBVM60.DLL,
    это не является недостатком, так как эта библиотека интегрированная
    во все новые операционные системы и таскать ее вместе с приложением не нужно.
  • Простота написания кода. Все удобно и наглядно. Даже начинающий может
    писать неоптимизированные, но рабочие программы. Как говориться, умеешь
    писать качественно — пиши, от этого программы будут только лучше. Не
    умеешь — можешь писать неоптимизированные проги (переменные объявлять
    не обязательно, преобразовывать типы данных тоже не обязательно).
    Но постепенно приходят более глубокие познания и можно писать крупные проекты,
    не сильно уступающие аналогам на C++ или Delphi по скорости работы, но
    на разработку которых уходит гораздо меньше времени.

    Так вот, начинающие исследователи программ почему то считают, что
    программы, написанные на VB невозможно сломать. Если программа
    скомпилирована в pcode — я частично с ними соглашусь, но даже
    для этого вида компиляции уже написано множество отладчиков,
    способных понимать псевдокодовые инструкции. Что же касается
    Native Code, то тут все ломается также, как и любая другая программа,
    написанная например на C++ или Delphi. Но есть ряд особенностей.
    Все операции, которые выполняет программа выполняются с использованием
    узкоспециализированных функций из библиотеки MSVBVM60.DLL.
    Имена этих функций напоминают операторы Visual Basic’а, поэтому
    глядя на названия большинства из них, можно понять, какие операции
    они позволяют выполнять. Но есть и такие, которые не поддаются логике.
    О них я и расскажу в данной статье.


    [Функции]

    Самые непонятные исследователям функции — функции для преобразования данных
    из одного типа в другой. Чуть ниже я для удобства приведу существующие
    типы данных, используемых в Visual Basic 6.0 и функции для работы с ними.
    За большую часть данной информации хотелось бы поблагодарить Eternal_Bliss (http:\crackmes.cbj.net)
    и авторов сайта http://www.infonegocio.com/vbcrack/, хотя на момент
    второй редакции статьи (которую Вы сейчас видите перед собой) мне прилось значительно расширить эту информацию основываясь на собственном опыте.

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

    Тип данных Расшифровка
    bool boolean
    str string
    i2 byte or integer (2 битный integer)
    ui2 unsigned integer (2 битный unsigned integer)
    i4 long (4 битный integer)
    r4 single (4 битный real)
    r8 double (8 битный real)
    cy currency
    var variant (VB) или variable (OLEAUT)
    fp число с плавающей точкой
    cmp сравнение
    comp сравнение

    Ниже представленые функции, экспортируемые библиотекой MSVBVM60.DLL. Если Вам потребуется вызывать эти вункции из своих программ, то
    имейте ввиду, что у VB свои типы данных, отличные в некоторых случаях от тех, которые используются в Delphi и С++. Строки вообще имеют свой особый
    формат (в начале строки два (или четыре) байта указывают на длину, затем идет строка). Отсюда бессмысленно пытаться вызвать перечисленные ниже функции из Delphi или C++ — такой вызов скорее всего приведет к ошибке. Что касается конвенции вызовов, то VB использует stdcall и другие не поддерживает,
    из чего логично сделать вывод, что Dll написанную на C++ с конвенцией вызова cdecl вызвать из VB практически невозможно.

  • Функции для преобразования типов данных
    __vbaI2Str преобразует String в Integer
    __vbaI4Str преобразует String в Long
    __vbar4Str преобразует String в Single
    __vbar8Str преобразует String в Double
    VarCyFromStr преобразует String в Currency
    VarBstrFromI2 преобразует Integer в String

  • Перенос данных
    __vbaStrCopy копирует строку в память — аналог API функции HMEMCPY
    __vbaVarCopy копирует переменный тип (variant) в память
    __vbaVarMove копирует переменный тип (variant) в память

  • Математические функции
    __vbavaradd сложение двух переменных типа Variant
    __vbavarsub деление двух переменных типа Variant
    __vbavarmul умножение двух переменных типа Variant
    __vbavaridiv сложение двух переменных типа Variant с выводом результата в переменную типа Integer
    __vbavarxor XOR

  • Другие функции
    __vbavarfornext используется в конструкциях For… Next… (Loop)
    __vbafreestr удаление переменной
    __vbafreeobj удаление объекта
    __vbastrvarval получения численного значения из строки
    multibytetowidechar преобразование кодировки
    rtcMsgBox показывает сообщение — аналог API messagebox/a/exa
    __vbavarcat объединяет две переменные типа Variant
    __vbafreevar удаляет переменную типа Variant
    __vbaobjset создает объект
    __vbaLenBstr определяет длину строки
    rtcInputBox показывает форму с полем ввода (используются также API функции getwindowtext/a, GetDlgItemtext/a)
    __vbaNew аналог API функции Dialogbox
    __vbaNew2 аналог API функции Dialogboxparam/a
    rtcTrimBstr удаляет пробелы вначале и в конце строки

  • Функции сравнения
    __vbastrcomp сравнивает 2 строковые переменные — аналог API функции lstrcmp
    __vbastrcmp сравнивает 2 строковые переменные — аналог API функции lstrcmp
    __vbavartsteq сравнивает 2 Variant переменные
    __vbaFpCmpCy сравнивает значение с плавающей точкой с Currency значением

    [Разблокирование элементов управления]

    Любой элемент управления на форме может быть видимым или невидимым,
    доступным или заблокированным. Очень часто для взлома программы
    бывает нужно разблокировать отдельные элементы управления на форме.
    Для установки свойств объектов существует функция __vbaObjSet.
    Именно с помощью нее можно заблокировать или разблокировать элемент
    управления и изменить любое из его свойств. Поэтому нам необходимо
    отлавливать вызов именно этой функции. Откроем например Olly Debugger,
    найдем эту функцию среди вызываемых программой и поставим на нее
    бряк нажав кнопку F2. Затем запустим исследуемую программу.
    Когда бряк сработает — посмотрите окружающий код. Если он напоминает

    CODE NOW!
    50 push eax
    52 push edx
    FFD7 call edi
    8BD8 mov ebx, eax
    6A00 push 00
    53 push ebx
    8B03 mov eax, dword ptr [ebx]

    то вы на верном пути. Как вы думаете, что это за «push 00»?
    00h в VB означает FALSE, а FFh TRUE, из этого следует, что
    данная команда устанавливает свойство в FALSE, то есть
    возможно это и есть блокировка элемента управления на форме.
    Но это может быть и установка любого другого свойства формы в TRUE.
    Так как синтаксис один и тот же, что принадлежность данной
    команды к изменению свойства блокировки можно установить только
    анализом окружающего кода, но умаю с этой мелочью вы справитесь сами.


    [Методика взлома простейших проверок пароля]

    Нижеследующий текст — мой вольный перевод статьи How to Research Visual Basic Cracking
    с сайта http://www.infonegocio.com/vbcrack/. За английскую версию текста спасибо ее авторам.

    Что нам может понадобиться? Любой дизассемблер/отладчик. Подойдет Win32Dasm или Olly Debugger
    Если вы используете W32Dasm, то функции, используемые программой вы можете посмотреть в меню «Functions» -> «Imports».

    Если вы будете исследовать базу данных Jet, которая использует драйвера Micro$oft,
    то помните, что названия функции не говорят сами за себя и вам будет трудно узнать
    действие, которое выполняет та или иная функция. в данном случае Вам остается только
    смотреть рекомендации к функциям, которые любезно оставлены программистами Micro$oft.
    Дыр же в этих драйверах практически нет, поэтому исследовать программы для
    работы с базами данных довольно сложно.

    Рассмотрим пример защиты программы паролем. Для удобства поиска нужного
    кода поставим MessageBox. Откройте Visual Basic, добавьте на форму текстовое
    поле и кнопку, затем в обработчике щелчка по кнопке напишите следующий код:

    CODE NOW!
    Private Sub Command1_Click()
    Dim X&
    X = 43690
    MsgBox «Testing password»
    If CLng(Trim$(Text1.Text)) = X Then MsgBox («GoodBoy»)
    End Sub

    Теперь откроем Olly Debugger, загрузим в него прогу (предварительно прогу
    нужно откомпилировать) и ищем вызов функции rtcMsgBox.

    CODE NOW!
    00401F54 FF1520104000 CALL [MSVBVM60!rtcMsgBox] ;MsgBox «Testing password»
    00401F5A 8D459C LEA EAX,[EBP-64]
    00401F5D 8D4DAC LEA ECX,[EBP-54]
    00401F60 50 PUSH EAX
    00401F61 8D55BC LEA EDX,[EBP-44]
    00401F64 51 PUSH ECX
    00401F65 8D45CCLEA EAX,[EBP-34]
    00401F68 52 PUSH EDX
    00401F69 50 PUSH EAX
    00401F6A 6A04 PUSH 04
    00401F6C FF1508104000 CALL [MSVBVM60!__vbaFreeVarList]
    00401F72 8B0E MOV ECX,[ESI]
    00401F74 83C414 ADD ESP,14
    00401F77 56 PUSH ESI
    00401F78 FF9104030000 CALL [ECX+00000304]
    00401F7E 8D55DCLEA EDX,[EBP-24]
    00401F81 50 PUSH EAX
    00401F82 52 PUSH EDX
    00401F83 FF1524104000 CALL [MSVBVM60!__vbaObjSet]
    00401F89 8BF0 MOV ESI,EAX
    00401F8B 8D4DE4LEA ECX,[EBP-1C]
    00401F8E 51 PUSH ECX
    00401F8F 56 PUSH ESI
    00401F90 8B06 MOV EAX,[ESI]
    00401F92 FF90A0000000 CALL [EAX+000000A0]
    00401F98 3BC7 CMP EAX,EDI
    00401F9A DBE2 FCLEX
    00401F9C 7D12 JGE 00401FB0
    00401F9E 68A0000000 PUSH 000000A0
    00401FA3 6804184000 PUSH 00401804
    00401FA8 56 PUSH ESI
    00401FA9 50 PUSH EAX
    00401FAA FF1518104000 CALL [MSVBVM60!__vbaHresultCheckObj] ;получение содержимого Text1.Text
    00401FB0 8B55E4MOV EDX,[EBP-1C]
    00401FB3 52 PUSH EDX
    00401FB4 FF1514104000 CALL [MSVBVM60!rtcTrimBstr] ;Trim$
    00401FBA 8BD0 MOV EDX,EAX
    00401FBC 8D4DE0 LEA ECX,[EBP-20]
    00401FBF FF1584104000 CALL [MSVBVM60!__vbaStrMove]
    00401FC5 50 PUSH EAX
    00401FC6 FF1568104000 CALL [MSVBVM60!__vbaI4Str] ;CLng
    00401FCC 33C9 XOR ECX,ECX
    00401FCE 3DAAAA0000 CMP EAX,0000AAAA ;Число 43690 в HEX виде
    00401FD3 8D55E0LEA EDX,[EBP-20]
    00401FD6 8D45E4LEA EAX,[EBP-1C]
    00401FD9 0F94C1 SETZ CL
    00401FDC 52 PUSH EDX
    00401FDD 50 PUSH EAX
    00401FDE F7D9 NEG ECX
    00401FE0 6A02 PUSH 02
    00401FE2 8BF1 MOV ESI,ECX
    00401FE4 FF156C104000 CALL [MSVBVM60!__vbaFreeStrList]
    00401FEA 83C40C ADD ESP,0C
    00401FED 8D4DDC LEA ECX,[EBP-24]
    00401FF0 FF1594104000 CALL [MSVBVM60!__vbaFreeObj]
    00401FF6 663BF7 CMP SI,DI
    00401FF9 7463 JZ 0040205E ;переход на GoodBoy код (MsgBox «GoodBoy»)
    00401FFB B804000280 MOV EAX,80020004
    00402000 8D558C LEA EDX,[EBP-74]
    00402003 8D4DCCLEA ECX,[EBP-34]
    00402006 8945A4 MOV [EBP-5C],EAX
    00402009 895D9C MOV [EBP-64],EBX
    0040200C 8945B4 MOV [EBP-4C],EAX
    0040200F 895DAC MOV [EBP-54],EBX
    00402012 8945C4 MOV [EBP-3C],EAX
    00402015 895DBC MOV [EBP-44],EBX
    00402018 C7459418184000 MOV DWORD PTR [EBP-6C],00401818
    0040201F C7458C08000000 MOV DWORD PTR [EBP-74],00000008
    00402026 FF157C104000 CALL [MSVBVM60!__vbaVarDup]
    0040202C 8D4D9C LEA ECX,[EBP-64]

    Чтобы взломать данную программу — нужно изменить переход по адресу 00401FF9
    с условного на абсолютный. Для этого меняем JE на JMP (то есть 74h на EBh).
    Как видите — простенькие защиты VB программ ломаются не сложнее чем в программах, написанных на
    Delphi или C++


    [Взлом паролей в виде разных типов данных]

    Нижеследующий тест — мой вольный перевод статей:
    Cracking Visual Basic Serials — Strings, integers and Longs
    Cracking Visual Basic Serials — Single, Double and XOR
    с сайта http://www.infonegocio.com/vbcrack/. За английские версии этих статей спасибо их авторам.

  • Получение пароля, если он лежит строкой в открытом виде

    Когда VB копирует строку в память — Вы ее можете отловить.
    Для этого нужно поставить бряк на функцию __vbaStrCopy и когда бряк сработает,
    посмотрите код после je 66047B00 (он содержится в библиотеке и един для всех программ).
    вам нужно посмотреть содержимое edx-04 (в SoftICE нужно ввести команду d edx-04).
    При этом вы увидите строчку, с которой работает в данный момент функция.

    CODE NOW!
    Exported fn(): __vbaStrCopy — Ord:008Ah
    :66024532 56 PUSH ESI
    :66024533 57 PUSH EDI
    :66024534 85D2 TEST EDX, EDX
    :66024536 8BF9 MOV EDI, ECX
    :66024538 0F84C2350200 JE 66047B00
    :6602453E FF72FC PUSH [EDX-04] ;по этому адресу лежит строка
    :66024541 52 PUSH EDX
  • Получение пароля, если он сравнивается с введенным

    Поставьте бряк на функцию __vbaStrComp и чуть выше ее вызова будут два
    push’а — они заносят в стек две Variant переменные для сравнения.
    Посмотрите содержимое EAX чтобы просмотреть адрес теста.
    Просмотрите, что лежит по этому адресу, наверняка это правильный пароль.

    CODE NOW!
    00401BC7 50 PUSH EAX ;то что Вы ввели
    00401BC8 6880174000 PUSH 00401780 ;верный пароль
    00401BCD FF1530104000 CALL [MSVBVM60!__vbaStrComp]

  • Получение пароля, если происходит сравнение 4х байтовых числовых переменных

    Поставьте бряк на функцию __vbai4str. Эта функция используется программой для
    перевода введенной текстовой строки в 4х байтовое число. Когда бряк сработает — посмотрите
    код ниже — там наверняка будет проца сравнения паролей. Но значение пароля будет
    в HEX виде (например десятичное число 987654321 равно шестнадцатеричному 3ADE68B1).

    CODE NOW!
    00401B77 FF155C104000 CALL [MSVBVM60!__vbaI4Str]
    00401B7D 8D4DE0 LEA ECX, [EBP-20]
    00401B80 8BF8 MOV EDI, EAX
    00401B82 FF157C104000 CALL [MSVBVM60!__vbaFreeStr]
    00401B88 8D4DDC LEA ECX, [EBP-24]
    00401B8B FF1580104000 CALL [MSVBVM60!__vbaFreeObj]
    00401B91 81FFB168DE3A CMP EDI, 3ADE68B1 ; 3ADE68B1 — это пароль
    00401B97 7520 JNZ 00401BB9 ;функция вернет ноль
    ;если пароли верны, следовательно этот переход
    ;сработает только при неверном пароле

    Тут мы можем узнать верный пароль или отключить проверку пароля (для этого нужно JNZ
    заменить на NOP (то есть в данном случае менять нужно байты по адресу 00401B97 с 7520 на 9090)

  • Получение пароля при сравнивании двух 2х байтовых чисел

    Отличие от предыдущего примера в том, что нужно ставить бряк на
    функцию __vbai2Str. Но иногда предварительно производится преобразование
    строки сначала в 4х байтовое число посредством функции __vbaI2I4,
    а затем 4х байтовое число преобразуется в двухбайтовое.

  • Обход сравнения двух переменных типа Single

    Эту проверку обойти довольно просто. Если Вашей целью является написание патча,
    то ставьте бряк на функцию __vbaR4Str и когда он сработает — пролистните код
    вниз. Вы увидите переход после проверки. Просто измените условие перехода
    на обратное.

    CODE NOW!
    00401C0C FF153C104000 CALL [MSVBVM60!_vbaR4Str]
    00401C12 D95DE4 FSTP REAL4 PTR [EBP-1C]
    00401C15 8D4DDC LEA ECX, [EBP-24]
    00401C18 8D55E0 LEA EDX, [EBP-20]
    00401C1B 51 PUSH ECX
    00401C1C 52 PUSH EDX
    00401C1D 6A02 PUSH 02
    00401C1F FF1570104000 CALL [MSVBVM60!__vbaFreeStrList]
    00401C25 83C40C ADD ESP, 0C
    00401C28 8D4DD8 LEA ECX, [EBP-28]
    00401C2B FF1594104000 CALL [MSVBVM60!__vbaFreeObj]
    00401C31 817DE43A92FC42 CMP DWORD PTR [EBP-1C], 42FC923A
    00401C38 7520 JNZ 00401C5A ;если ноль, то пароль неправильный

    Замените инструкцию по адресу 00401C38 на 9090 (чтобы в данном месте не выполнялось никаких операций)
    или измените условный переход на противоположный. Программисты на Visual Basic не смогут проверить то,
    что вы заменили эти байты на два NOP’а (9090). Если у Вас все же возникают опасения, что программа
    перестанет работать при отсутствии команд, просто введите команды, которые ничего не поменяют,
    например:

    CODE NOW!
    inc eax
    dec eax

    Эти команды однобайтовые и как раз впишутся на место условного перехода

  • Обход проверки значений с плавающей точкой (double).

    Это также сделать очень просто. Если хочется обойтись битхаком — ставьте бряк на функцию __vbaR8Str и пролистните
    ком чуть ниже бряка, Вы увидите процедуру сравнения (FCOMP REAL8 PTR [Address]), после нее идет TEST и jump.
    Измените условие перехода на противоположное.

    CODE NOW!
    00401C55 FF1510104000 CALL [MSVBVM60!rtcTrimBstr]
    00401C5B 8BD0 MOV EDX, EAX
    00401C5D 8D4DD8 LEA ECX, DWORD PTR [EBP-28]
    00401C60 FF1580104000 CALL [MSVBVM60!__vbaStrMove]
    00401C66 50 PUSH EAX
    00401C67 FF1560104000 CALL [MSVBVM60!__vbaR8Str]
    00401C6D DC1DD8104000 FCOMP REAL8 PTR [004010D8] ;сравнение паролей
    00401C73 DFE0 FSTSW AX ;обработка сопроцессором
    00401C75 F6C440 TEST AH, 40 ;проверка правильности пароля
    00401C78 7409 JE 00401C83 ;переход, если пароль верный

    Замените инструкцию по адресу 00401C78 на 9090 (чтобы в данном месте не выполнялось никаких операций)
    или измените условный переход на противоположный. Программисты на Visual Basic не смогут проверить то,
    что вы заменили эти байты на два NOP’а (9090). Если у Вас все же возникают опасения, что программа
    перестанет работать при отсутствии команд, просто введите команды, которые ничего не поменяют,
    например:

    CODE NOW!
    inc eax
    dec eax

    Эти команды однобайтовые и как раз впишутся на место условного перехода

  • Получение пароля, если он поXORен.

    Алгоритм работы бейсиковской процедуры таков. Введенный пароль посимвольно переводится в ASCII коды,
    которые по очереди XORятся. Затем производится обратный перевод, возможно с предварительными математическими
    манипуляциями с ASCII кодами. ANSI коды могут XORиться со случайным или фиксированным числом.

    CODE NOW!
    00401E6C 50 PUSH EAX
    00401E6D FF1544104000 CALL [MSVBVM60!rtcMidCharBstr]
    00401E73 8BD0 MOV EDX, EAX
    00401E75 8D4DC8 LEA ECX, [EBP-38]
    00401E78 FFD6 CALL ESI
    00401E7A 50 PUSH EAX
    00401E7B FF1518104000 CALL [MSVBVM60!rtcAnsiValueBstr]
    00401E81 0FBFC8 MOVSX ECX, AX
    00401E84 81F191000000 XOR ECX, 00000091 ;ANSI XOR 91
    00401E8A 51 PUSH ECX ;результат XOR’а
    00401E8B FF1570104000 CALL [MSVBVM60!rtcBstrFromAnsi]

    Ставим бряк на 00401E8A 51 PUSH ECX и отслеживаем изменение содержимого регистра ECX при прохождении цикла.
    Когда программа пройдет весь цикл — вы получите полный пароль.

    CODE NOW!
    CALL [MSVBVM60!rtcMidCharBstr] ;получает один символ из пароля
    CALL [MSVBVM60!rtcAnsiValueBstr] ;получает из символа Ansi код.
    CALL [MSVBVM60!rtcBstrFromAnsi] ;преобразует Ansi код в строку

    В этом случае ниже скорее всего будет сравнение и переход, при этом CALL [MSVBVM60!__vbaStrComp]
    может быть использован для сравнения строк. Если не производится обращение к MSVBVM60!rtcBstrFromAnsi
    тогда с паролем производятся определенные математические манипуляции, при этом он может быть представлен
    как 2х или 4х битное число или число с плавающей точкой. Об этих случаях было сказано выше.

    [Заключение]

    Надеюсь, что прочитав данную статью Вас уже больше не пугает исследование VB кода.
    Чтобы потренироваться советую поисследовать мои крякми (лежат на www.dotfix.net в
    разделе «Разное»), о взломе первого из которых написана статья (лежит на www.dotfix.net в
    разделе «Статьи»). Второй мой крякми еще никто не взломал ;)

    Еще раз благодарю авторов сайта http://www.infonegocio.com/vbcrack/, так как если бы не их туториалы на английском,
    то этой статьи возможно и не было бы.

    Удачи и спасибо за то, что дочитали статью до конца

    © GPcH


  • archive

    archive
    New Member

    Регистрация:
    27 фев 2017
    Публикаций:
    532


    WASM

    Исследование исполняемого файла можно разделить на три этапа: поверхностный, глубокий, хирургический. На первом мы малыми силами собираем информацию о подопытном файле. Под «малыми силами» я подразумеваю легкие в использовании и широко распространенные средства анализа. В этой статье мы поговорим о них и для наглядности взломаем несложную защиту.

    Пят­надцать лет назад эпи­чес­кий труд Кри­са Кас­пер­ски «Фун­дамен­таль­ные осно­вы хакерс­тва» был нас­толь­ной кни­гой каж­дого начина­юще­го иссле­дова­теля в области компь­ютер­ной безопас­ности. Одна­ко вре­мя идет, и зна­ния, опуб­ликован­ные Кри­сом, теря­ют акту­аль­ность. Редак­торы «Хакера» попыта­лись обно­вить этот объ­емный труд и перенес­ти его из вре­мен Windows 2000 и Visual Studio 6.0 во вре­мена Windows 10 и Visual Studio 2019. Результатом стал цикл статей.

    Мы публикуем эту статью в честь начала предзаказов обновленной версии книги Криса, получившей новый подзаголовок: «Анализ программ в среде Win64». Оставить предзаказ на книгу, чтобы приобрести ее со скидкой 25%, вы можете на сайте издательства «Солон-пресс».

    Этап 1

    Если работа ведется в Linux, среди инструментов поверхностного анализа можно отметить такие приложения:

    • file определяет тип файла, анализируя его поля;

    • readelf отображает информацию о секциях файла;

    • ldd выводит список динамических библиотек, от которых зависит данный исполняемый файл;

    • nm выводит список декорированных имен функций, которые создаются компиляторами языков, поддерживающих перегрузку функций. Затем утилита может декодировать эти имена;

    • c++filt преобразует декорированные имена функций в первоначальные с учетом передаваемых и получаемых аргументов;

    • strings выводит строки, содержащиеся в файле, с учетом заданного шаблона.

    В Windows со многими из этих задач справляется утилита dumpbin, входящая в поставку Visual Studio.

    Если мы получили общее представление о файле и не нашли ничего полезного для взлома, есть смысл переходить к следующему этапу.

    Этап 2

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

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

    В качестве отладчика в Linux можно воспользоваться старым добрым GDB либо средствами трассировки в Radare2. В Windows выбор тоже невелик: OllyDbg постепенно устаревает и не обновляется. В нем отлаживать приложения можно только в режиме пользователя. После смерти SoftICE единственным нормальным отладчиком в Windows стал WinDbg. В нем можно отлаживать драйверы на уровне ядра.

    Статические дизассемблеры тоже делятся на две группы: линейные и рекурсивные. Первые перебирают все сегменты кода в двоичном файле, декодируют и преобразуют их в мнемоники языка ассемблера. Так работает большинство простых дизассемблеров, включая objdump и dumpbin. Может показаться, что так и должно быть. Однако, когда среди исполняемого кода встречаются данные, возникает проблема: их не надо преобразовывать в команды, но линейный дизассемблер не в состоянии отличить их от кода! Мало того, после того как сегмент данных завершится, дизассемблер будет рассинхронизирован с текущей позиции кода.

    С другой стороны, рекурсивные дизассемблеры, такие как IDA Pro, Radare2 и Ghidra, ведут себя иначе. Они дизассемблируют код в той же последовательности, в которой его будет исполнять процессор. Поэтому они аккуратно обходят данные, и в результате большинство команд благополучно распознается.

    Но и здесь есть ложка дегтя. Не все косвенные переходы легко отследить. Следовательно, какие-то пути выполнения могут остаться нераспознанными. Помогает то, что современные дизассемблеры применяют разные эвристические приемы с учетом конкретных компиляторов.

    Этап 3

    Когда адрес защитного механизма найден, можно приступать к третьему этапу — хирургическому. Существует несколько видов непосредственного взлома. Самый простой вариант — воспользоваться шестнадцатеричным редактором.

    Существует множество HEX-редакторов на любой вкус и цвет, например 010 Editor, HexEdit, HIEW. Вооружайся одним из них, и тебе останется только перезаписать команду по найденному адресу. Но в двоичном файле сильно не разбежишься! Существующий код не дает простора для манипуляций, поэтому нам приходится умещаться в имеющемся пространстве.

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

    Этим способом, однако, можно хакнуть большое количество защитных механизмов. А как быть, если надо не просто взломать программу, а добавить или заменить в ней какие-то функции? Понятно, что HEX-редактор тут тебе не друг.

    Здесь на помощь приходит техника оснащения двоичного файла. Оно позволяет вставить почти неограниченный блок кода в почти любое место файла. Понятно, что без посторонних средств этого не добиться. Оснащение реализуют специальные библиотеки, предоставляющие гибкие API.

    С помощью статического оснащения производится модификация файла непосредственно на диске. То есть бинарный файл сначала надо дизассемблировать, найти подходящее место, обладающее достаточным пространством для включения полезной нагрузки, и, внедряя потусторонний код, уследить, чтобы не поломались существующие указатели на код и данные. В этом деле призваны помочь такие инструменты, как Dyninst и PEBIL. После модификации файла он записывается в измененном виде обратно на диск.

    Динамическое оснащение работает по другому принципу. Подобно отладчикам движки динамического оснащения следят за выполняемыми процессами и вносят код прямо в память. Поэтому при динамическом оснащении не нужно ни дизассемблирование, ни изменение бинарника, от чего одним махом исчезают все негативные последствия.

    Однако во время динамического оснащения, так как программа выполняется «под наблюдением», ее производительность заметно падает. Для динамического оснащения используются системы DynamoRIO (совместный проект HP и MIT) и Pin (Intel).

    Может показаться, что динамическое оснащение лучше во всех отношениях. А вот и нет! Такую программу нельзя будет исполнить на другом компьютере, где нет никаких специальных средств. Тогда как в процессе статического оснащения мы заранее готовим бинарник и передаем его пользователю. Теми же средствами могут пользоваться и злоумышленники, желающие заразить программу вирусом или снабдить какой-то подлой функцией.

    Практический взлом

    Чтобы поупражняться на практике, проведем анализ и раскусим элементарную защиту.

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

    Достоинство такой защиты — крайне простая программная реализация. Ее ядро состоит фактически из нескольких строк, которые на языке С/C++ можно записать так:

    if (strcmp(введенный пароль, эталонный пароль)) 
    {/* Пароль неверен */} 
    else 
    {/* Пароль ОК*/}
    

    Давай дополним этот код процедурами запроса пароля и вывода результатов сравнения, а затем испытаем полученную программу на прочность, т. е. на стойкость к взлому.

    Пример простейшей системы аутентификации:

         #include "stdafx.h"
         // Простейшая система аутентификации 
         // Посимвольное сравнение пароля 
         #include <stdio.h>
         #include <string.h>
         #define PASSWORD_SIZE 100
         #define PASSWORD "myGOODpasswordn"
         // Этот перенос нужен затем, чтобы 
         // не выкусывать перенос из строки,
         // введенной пользователем
         int main()
         {
         // Счетчик неудачных попыток аутентификации 
         int count=0;
         // Буфер для пароля, введенного пользователем 
         char buff[PASSWORD_SIZE];
         // Главный цикл аутентификации 
         for(;;)
         {
         // Запрашиваем и считываем пользовательский пароль
         printf("Enter password:"); 
         fgets(&buff[0],PASSWORD_SIZE,stdin);
         // Сравниваем оригинальный и введенный пароль 
         if (strcmp(&buff[0],PASSWORD))
         // Если пароли не совпадают — «ругаемся»
         printf("Wrong passwordn");
         // Иначе (если пароли идентичны)
         // выходим из цикла аутентификации
         else break;
         // Увеличиваем счетчик неудачных попыток 
         // аутентификации и, если все попытки 
         // исчерпаны, завершаем программу 
         if (++count>3) return -1;
         }
         // Раз мы здесь, то пользователь ввел правильный пароль 
         printf("Password OKn");
         }

    В популярных кинофильмах крутые хакеры легко проникают в любые жутко защищенные системы, каким-то непостижимым образом угадывая искомый пароль с нескольких попыток. Почему бы не попробовать пойти их путем?

    Не так уж редко пароли представляют собой осмысленные слова наподобие Ferrari, QWERTY, имена любимых хомячков, названия географических пунктов и т. д. Угадывание пароля сродни гаданию на кофейной гуще — никаких гарантий на успех нет, остается рассчитывать на одно лишь везение. А удача, как известно, птица гордая — палец в рот ей не клади. Нет ли более надежного способа взлома?

    Раз эталонный пароль хранится в теле программы, то, если он не зашифрован каким-нибудь хитрым образом, его можно обнаружить тривиальным просмотром двоичного кода программы. Перебирая все встретившиеся в ней текстовые строки, начиная с тех, что больше всего смахивают на пароль, мы очень быстро подберем нужный ключ и откроем им программу! Причем область просмотра можно существенно сузить — в подавляющем большинстве случаев компиляторы размещают все инициализированные переменные в сегменте данных (в PE-файлах он размещается в секции .data или .rdata). Исключение составляют, пожалуй, ранние компиляторы Borland с их маниакальной любовью всовывать текстовые строки в сегмент кода — непосредственно по месту их вызова. Это упрощает сам компилятор, но порождает множество проблем. Современные операционные системы, в отличие от старушки MS-DOS, запрещают модификацию кодового сегмента, и все размещенные в нем переменные доступны лишь для чтения. К тому же на процессорах с раздельной системой кеширования они «засоряют» кодовый кеш, попадая туда при упреждающем чтении, но при первом же обращении к ним вновь загружаются из медленной оперативной памяти в кеш данных. В результате — тормоза и падение производительности.

    Что ж, пусть это будет секция данных! Остается только найти удобный инструмент для просмотра двоичного файла. Можно, конечно, нажать клавишу F3 в своей любимой оболочке (FAR, DOS Navigator) и, придавив кирпичом Page Down, любоваться бегущими циферками до тех пор, пока не надоест.

    Можно воспользоваться любым HEX-редактором (QVIEW, HIEW… — кому какой по вкусу), но в данном случае, по соображениям наглядности, приведен результат работы утилиты dumpbin из штатной поставки Microsoft Visual Studio.

    Натравим утилиту на исполняемый файл нашей программы, содержащей пароль, и попросим ее распечатать содержащую только для чтения инициализированные данные секцию — rdata (ключ /SECTION:.rdata) в «сыром» виде (ключ /RAWDATA:BYTES), указав значок > для перенаправления вывода в файл (ответ программы занимает много места, и на экране помещается один лишь «хвост»).

    dumpbin /RAWDATA:BYTES /SECTION:.rdata passCompare1.exe > rdata.txt
    004020E0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    004020F0: 18 30 40 00 68 30 40 00 45 6E 74 65 72 20 70 61  .0@.h0@.Enter pa
    00402100: 73 73 77 6F 72 64 3A 00 6D 79 47 4F 4F 44 70 61  ssword:.myGOODpa
    00402110: 73 73 77 6F 72 64 0A 00 57 72 6F 6E 67 20 70 61  ssword..Wrong pa
    00402120: 73 73 77 6F 72 64 0A 00 50 61 73 73 77 6F 72 64  ssword..Password
    00402130: 20 4F 4B 0A 00 00 00 00 00 00 00 00 00 00 00 00   OK.............
    00402140: 00 00 00 00 90 0A C1 5B 00 00 00 00 02 00 00 00  ......A[........
    00402150: 48 00 00 00 24 22 00 00 24 14 00 00 00 00 00 00  H...$"..$.......

    Среди всего прочего тут есть одна строка, до боли похожая на эталонный пароль. Испытаем ее? Впрочем, какой смысл — судя по исходному тексту программы, это действительно искомый пароль, открывающий защиту, словно золотой ключик. Слишком уж видное место выбрал компилятор для его хранения — пароль не мешало бы запрятать и получше.

    Один из способов сделать это — насильно поместить эталонный пароль в собственноручно выбранную нами секцию. Такая возможность не предусмотрена стандартом, и потому каждый разработчик компилятора (строго говоря, не компилятора, а линкера, но это не суть важно) волен реализовывать ее по-своему или не реализовывать вообще. В Microsoft Visual C++ для этой цели предусмотрена специальная прагма data_seg, указывающая, в какую секцию помещать следующие за ней инициализированные переменные. Неинициализированные переменные по умолчанию располагаются в секции .bss и управляются прагмой bss_seg соответственно.

    В примере аутентификации выше перед функцией main добавим новую секцию, в которой будем хранить наш пароль:

         // С этого момента все инициализированные 
         // переменные будут размещаться в секции .kpnc
         #pragma data_seg(".kpnc") 
         #define PASSWORD_SIZE 100
         #define PASSWORD "myGOODpasswordn"
         char passwd[] = PASSWORD;
         #pragma data_seg()

    Внутри функции main проинициализируем массив:

         // Теперь все инициализированные переменные 
         // вновь будут размещаться в секции по умолчанию, 
         // т. е. .rdata 
         char buff[PASSWORD_SIZE]="";

    Немного изменилось условие сравнения строк в цикле:

         if (strcmp(&buff[0],&passwd[0]))

    Натравим утилиту dumpbin на новый исполняемый файл:

    dumpbin /RAWDATA:BYTES /SECTION:.rdata passCompare2.exe > rdata.txt
    
    004020C0: D3 17 40 00 00 00 00 00 D8 11 40 00 00 00 00 00  O.@.....O.@.....
    004020D0: 00 00 00 00 2C 11 40 00 D0 11 40 00 00 00 00 00  ....,.@.?.@.....
    004020E0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    004020F0: 18 30 40 00 68 30 40 00 45 6E 74 65 72 20 70 61  .0@.h0@.Enter pa
    00402100: 73 73 77 6F 72 64 3A 00 57 72 6F 6E 67 20 70 61  ssword:.Wrong pa
    00402110: 73 73 77 6F 72 64 0A 00 50 61 73 73 77 6F 72 64  ssword..Password
    00402120: 20 4F 4B 0A 00 00 00 00 00 00 00 00 00 00 00 00   OK.............
    00402130: 00 00 00 00 6F CB C4 5B 00 00 00 00 02 00 00 00  ....oEA[........
    00402140: 48 00 00 00 14 22 00 00 14 14 00 00 00 00 00 00  H...."..........
    00402150: 6F CB C4 5B 00 00 00 00 0C 00 00 00 14 00 00 00  oEA[............

    Ага, теперь в секции данных пароля нет и хакеры «отдыхают»! Но не спеши с выводами. Давай сначала выведем на экран список всех секций, имеющихся в файле:

         dumpbin passCompare2.exe
     Summary
          1000 .data
          1000 .kpnc
          1000 .rdata
          1000 .reloc
          1000 .rsrc
          1000 .text

    Нестандартная секция .kpnc сразу же приковывает к себе внимание. А ну-ка посмотрим, что там в ней.

    dumpbin /SECTION:.kpnc /RAWDATA passCompare2.exe
     RAW DATA #4
      00404000: 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00  myGOODpassword..

    Вот он, пароль! Спрятали, называется… Можно, конечно, извратиться и засунуть секретные данные в секцию неинициализированных данных (.bss) или даже секцию кода (.text) — не все там догадаются поискать, а работоспособность программы такое размещение не нарушит. Но не стоит забывать о возможности автоматизированного поиска текстовых строк в двоичном файле. В какой бы секции ни содержался эталонный пароль, фильтр без труда его найдет (единственная проблема — определить, какая из множества текстовых строк представляет собой искомый ключ; возможно, потребуется перебрать с десяток-другой потенциальных кандидатов).

    Знакомство с дизассемблером

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

    Хакнуть, говорите?! Что ж, это несложно! Куда проблематичнее определиться, чем именно ее хакать. Инструментарий хакеров чрезвычайно разнообразен, чего тут только нет: и дизассемблеры, и отладчики, и API-, и message-шпионы, и мониторы обращений к файлам (портам, реестру), и распаковщики исполняемых файлов, и… Сложновато начинающему кодокопателю со всем этим хозяйством разобраться!

    Впрочем, шпионы, мониторы, распаковщики — второстепенные утилиты заднего плана, а основное оружие взломщика — отладчик (динамический дизассемблер) и дизассемблер (статический).

    Итак, дизассемблер применим для исследования откомпилированных программ и частично пригоден для анализа псевдокомпилированного кода. Раз так, он должен подойти для вскрытия парольной защиты passCompare1.exe. Весь вопрос в том, какой дизассемблер выбрать.

    Не все дизассемблеры одинаковы. Есть среди них и «интеллектуалы», автоматически распознающие многие конструкции, как то: прологи и эпилоги функций, локальные переменные, перекрестные ссылки и т. д., а есть и «простаки», чьи способности ограничены одним лишь переводом машинных команд в ассемблерные инструкции.

    Логичнее всего воспользоваться услугами дизассемблера-интеллектуала (если он есть), но… давай не будем спешить, а попробуем выполнить весь анализ вручную. Техника, понятное дело, штука хорошая, да вот не всегда она оказывается под рукой, и неплохо бы заранее научиться работе в полевых условиях. К тому же общение с плохим дизассемблером как нельзя лучше подчеркивает «вкусности» хорошего.

    Воспользуемся уже знакомой нам утилитой dumpbin, настоящим «швейцарским ножиком» со множеством полезных функций, среди которых притаился и дизассемблер. Дизассемблируем секцию кода (как мы помним, носящую имя .text), перенаправив вывод в файл, так как на экран он, очевидно, не поместится:

     dumpbin /SECTION:.text /DISASM passCompare1.exe > code-text.txt

    Заглянем еще раз в секцию данных (или в другую — в зависимости от того, где хранится пароль).

    Запомним найденный пароль: myGOODpassword. В зависимости от версии и настроек дизассемблера dumpbin инициализированные переменные, к которым обращается код, могут быть представлены по-разному: на их месте могут быть или символьные константы, или непосредственно шестнадцатеричное смещение. Попробуем найти выявленный ранее пароль в дизассемблированном листинге тривиальным контекстным поиском с помощью любого текстового редактора.

     0040107D: B9 08 21 40 00     mov         ecx,offset ??_C@_0BA@PCMCJPMK@myGOODpassword?6?$AA@
     00401082: 8A 10              mov         dl,byte ptr [eax]
     00401084: 3A 11              cmp         dl,byte ptr [ecx]
     00401086: 75 1A              jne         004010A2
     00401088: 84 D2              test        dl,dl
     0040108A: 74 12              je          0040109E

    Центральная часть этого листинга сравнивает значения регистров EAX и ECX. В последний, как мы видим в первой строке листинга, записывается эталонный пароль, следовательно, в первом — введенный пользователем. Затем происходит сравнение и выполняются переходы почти в одну и ту же точку: 0x4010A2 и 0x40109E. Интересно, что там. Заглянем:

     0040109E: 33 C0              xor         eax,eax
     004010A0: EB 05              jmp         004010A7
     004010A2: 1B C0              sbb         eax,eax
     004010A4: 83 C8 01           or          eax,1
     004010A7: 85 C0              test        eax,eax
     004010A9: 74 63              je          0040110E
     004010AB: 0F 1F 44 00 00     nop         dword ptr [eax+eax]
     004010B0: 68 18 21 40 00     push        offset ??_C@_0BA@EHHIHKNJ@Wrong?5password?6?$AA@
     004010B5: E8 56 FF FF FF     call        _printf

    Здесь центральную роль играет инструкция TEST EAX,EAX, размещенная по смещению 0x4010A7. Если EAX равен 0, следующая за ней команда JE совершает прыжок на 0x40110E.

    В противном же случае (то есть если EAX !=0) происходит выталкивание на вершину стека строки «Wrong password».

     push       offset ??_C@_0BA@EHHIHKNJ@Wrong?5password?6?$AA@

    А следом — вызов функции с говорящим названием:

     call        _printf

    Значит, ненулевое значение EAX свидетельствует о ложном пароле, а ноль — об истинном.
    Тогда переходим к анализу валидной ветви программы, что делается после прыжка на 0x40110E. А тут притаилась инструкция, которая помещает строку «Password OK» на вершину стека, а после этого вызывается процедура _printf, которая, очевидно, выводит строку на экран:

     0040110E: 68 28 21 40 00	  push		 offset   ??_C@_0N@MBEFNJID@Password?5OK?6?$AA@
     00401113: E8 F8 FE FF FF     call        _printf

    Оперативные соображения следующие: если команду JE заменить JNE, то программа отвергнет истинный пароль как неправильный, а любой неправильный пароль воспримет как истинный. А если TEST EAX,EAX заменить XOR EAX,EAX, то после исполнения этой команды регистр EAX будет всегда равен нулю, какой бы пароль ни вводился.

    Дело за малым — найти эти самые байтики в исполняемом файле и малость подправить их.

    Примечание: если в секции кода эталонный пароль не находится, возможно, он скрывается за шестнадцатеричным смещением. Поэтому надо сначала заглянуть в секцию данных и узнать, по какому смещению он там находится.

     00402100: 73 73 77 6F 72 64 3A 00 6D 79 47 4F 4F 44 70 61  ssword:.myGOODpa

    Очевидно, нам надо выровнять смещение, чтобы пароль начинался с начала строки. Прибавим к смещению 8 — число символов, на которые надо сместить (ssword:.). В результате будем искать итоговое смещение — 402108 в секции кода. Тем самым мы найдем ту же самую инструкцию, что и прежде:

     0040107D: B9 08 21 40 00   mov       ecx,402108h

    Только вместо символьной константы мы обнаруживаем шестнадцатеричное смещение в секции данных.

    Хирургическое вмешательство

    Как мы обсуждали выше, внесение изменений непосредственно в исполняемый файл — дело серьезное. Стиснутым существующим кодом, нам приходится довольствоваться только тем, что есть, и ни раздвинуть команды, ни даже сдвинуть их, выкинув из защиты «лишние запчасти», не получится. Ведь это привело бы к сдвигу смещений всех остальных команд, тогда как значения указателей и адресов переходов остались бы без изменений и стали бы указывать совсем не туда, куда нужно!

    Ну, с «выкидыванием запчастей» справиться как раз таки просто — достаточно забить код командами NOP (опкод которой 0x90, а вовсе не 0x0, как почему-то думают многие начинающие кодокопатели), т. е. пустой операцией (вообще-то NOP — это просто другая форма записи инструкции XCHG EAX,EAX, если интересно). С «раздвижкой» куда сложнее! К счастью, в PE-файлах всегда присутствует множество «дыр», оставшихся от выравнивания, в них-то и можно разместить свой код или свои данные.

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

    Приходится резать программу вживую. Легче всего это делать с помощью утилиты HIEW, которая «переваривает» PE-формат файлов и упрощает тем самым поиск нужного фрагмента. Подойдет любая версия этого HEX-редактора. Например, я использовал далеко не самую новую версию 6.86, прекрасно уживающуюся с Windows 10. Запустим ее, указав имя файла в командной строке (hiew32 passCompare1.exe), двойным нажатием клавиши Enter, переключимся в режим ассемблера и при помощи клавиши F5 перейдем к требуемому адресу. Как мы помним, команда TEST, проверяющая результат на равенство нулю, располагалась по адресу 0x4010A7.

    Чтобы HIEW мог отличить адрес от смещения в самом файле, предварим его символом точки: .4010A7.

     004010A7: 85 C0           test       eax,eax
     004010A9: 74 63           je        0040110E

    Ага, как раз то, что нам надо! Нажмем клавишу F3 для перевода HIEW в режим правки, подведем курсор к команде TEST EAX, EAX и, нажав клавишу Enter, заменим ее командой XOR EAX,EAX.

     004010A7: 33 C0           xor         eax,eax
     004010A9: 74 63           je         0040110E

    С удовлетворением заметив, что новая команда в аккурат вписалась в предыдущую, нажмем клавишу F9 для сохранения изменений на диске, а затем выйдем из HIEW и попробуем запустить программу, вводя первый пришедший на ум пароль:

     >passCompare1
     Enter password:Привет, шляпа!
     Password OK

    Получилось! Защита пала! Хорошо, а как бы мы действовали, не умей HIEW «переваривать» PE-файлы? Тогда пришлось бы прибегнуть к контекстному поиску. Обратим свой взор на шестнадцатеричный дамп, расположенный дизассемблером слева от ассемблерных команд. Конечно, если пытаться найти последовательность 85 C0 — код команды TEST EAX, EAX, ничего хорошего из этого не выйдет — этих самых тестов в программе может быть несколько сотен, а то и больше. А вот адрес перехода, скорее всего, во всех ветках программы различен, и подстрока TEST EAX,EAX/JE 0040110E имеет хорошие шансы на уникальность. Попробуем найти в файле соответствующий ей код: 85 C0 74 63 (в HIEW для этого достаточно нажать F7).

    Оп-с! Найдено только одно вхождение, что нам, собственно, и нужно. Давай теперь попробуем модифицировать файл непосредственно в HEX-режиме, не переходя в ассемблер. Попутно возьмем себе на заметку: инверсия младшего бита кода команды приводит к изменению условия перехода на противоположное, т. е. 74 JE75 JNE.

    Работает? В смысле защита свихнулась окончательно — не признает истинные пароли, зато радостно приветствует остальные. Замечательно!

    Авторы: Крис Касперски, Юрий Язев

    Автор: Сергей Чубченко. Дата публикации: 15.09.2004

    [Вступление]

    Многие почему-то считают, что Visual Basic самый плохой язык программирования,
    который не может компилировать программы в машинный код, не может работать с адресами переменных в памяти
    и не позволяюет вставлять ассемблерные процедуры в высокоуровневый код программы. Так вот, все это неправда.
    Начиная с версии 5.0 данный язык позволяет компилировать программы в машинный Native Code, а
    также имеется возможность работы с адресами переменных в оперативной памяти (для этого существуют
    функции VarPtr и StrPtr). Ассемблерные процедуры вставлять тоже можно, но не так просто. Для
    этого я написал уже 2 части статьи по вставке ассемблерных процедур в код на Visual Basic.
    Первая статья размещена здесь: Ассемблер в VB6 часть 1,
    вторая тут: Ассемблер в VB6 часть 2. Как видите, недостатков у VB не так уж
    и много, а преимуществ настолько много, что я расскажу лишь о самых очевидных и важных:

    — Полноценная компиляция и маленький размер получаемых exe файлов.
    Несмотря на то, что программы требуют библиотеку MSVBVM60.DLL,
    это не является недостатком, так как эта библиотека интегрирована
    во все новые операционные системы и таскать ее вместе с приложением не нужно.

    — Простота написания кода. Все удобно и наглядно. Даже начинающий может
    писать неоптимизированные, но рабочие программы. Как говорится, умеешь
    писать качественно — пиши, от этого программы будут только лучше. Не
    умеешь — можешь писать неоптимизированные программы (переменные объявлять
    не обязательно, преобразовывать типы данных тоже не обязательно).
    Но постепенно приходят более глубокие познания и можно писать крупные проекты,
    не уступающие аналогам на C++ или Delphi.

    Так вот, исследователи программ почему-то считают, что
    программы, написанные на Visual Basic’е невозможно анализировать. Если программа
    скомпилирована в P-Code — их частично можно понять, но даже
    для этого вида компиляции уже написано пара отладчиков,
    способных понимать псевдокодовые инструкции. Что же касается
    Native Code, то тут все исследуется так же, как и любой x86 программный код,
    написанный например на C++ или Delphi. Но есть ряд особенностей.
    Все операции, которые выполняет программа выполняются с использованием
    узкоспециализированных функций из библиотеки MSVBVM60.DLL.
    Имена этих функций напоминают операторы Visual Basic’а, поэтому
    глядя на названия большинства из них, можно понять, какие операции
    они позволяют выполнять. Но есть и такие, которые не поддаются логике.
    О них я и расскажу в данной статье.

    [Функции]

    Самые непонятные аналитикам функции — функции для преобразования данных
    из одного типа в другой. Чуть ниже я для удобства приведу существующие
    типы данных, используемых в Visual Basic 6.0 и функции для работы с ними.

    bool — boolean
    str — string
    i2 — integer (2 байтный integer)
    ui2 — unsigned integer (2 байтный unsigned integer)
    i4 — long (4 байтный integer)
    r4 — single (4 байтный float)
    r8 — double (8 байтный float)
    cy — currency
    var — variant (VB) или variable (OLEAUT)

    Названия части функций:

    fp — работа с float данными, переданными через st регистры или сохраняемые туда командами сопроцессора
    cmp — сравнение аргументов
    comp — сравнение аргументов

    — Функции ля преобразования типов данных:
    __vbaI2Str ‘преобразует String в Integer
    __vbaI4Str ‘преобразует String в Long
    __vbar4Str ‘преобразует String в Single
    __vbar8Str ‘преобразует String в Double
    VarCyFromStr ‘преобразует String в Currency
    VarBstrFromI2 ‘преобразует Integer в String

    — Перенос данных
    __vbaStrCopy ‘копирует строку в память — аналог API функции HMEMCPY
    __vbaVarCopy ‘копирует переменный тип (variant) в память
    __vbaVarMove ‘копирует переменный тип (variant) в память

    — Математические функции
    __vbavaradd ‘сложение двух переменных типа Variant
    __vbavarsub ‘деление двух переменных типа Variant
    __vbavarmul ‘умножение двух переменных типа Variant
    __vbavaridiv ‘сложение двух переменных типа Variant
    ‘с выводом результата в переменную типа Integer
    __vbavarxor ‘XOR

    — Другие функции
    __vbavarfornext ‘используется в конструкциях For… Next… (Loop)
    __vbafreestr ‘удаление переменной
    __vbafreeobj ‘удаление объекта
    __vbastrvarval ‘получения численного значения из строки
    multibytetowidechar ‘преобразование кодировки
    rtcMsgBox ‘показывает сообщение — аналог API messagebox/a/exa
    __vbavarcat ‘объединяет две переменные типа Variant
    __vbafreevar ‘удаляет переменную типа Variant
    __vbaobjset ‘создает объект
    __vbaLenBstr ‘определяет длину строки
    rtcInputBox ‘показывает форму с полем ввода (используются также API функции getwindowtext/a, GetDlgItemtext/a)
    __vbaNew ‘аналог API функции Dialogbox
    __vbaNew2 ‘аналог API функции Dialogboxparam/a
    rtcTrimBstr ‘удаляет пробелы вначале и в конце строки

    — Функции сравнения
    __vbastrcomp ‘сравнивает 2 строковые переменные — аналог API функции lstrcmp
    __vbastrcmp ‘сравнивает 2 строковые переменные — аналог API функции lstrcmp
    __vbavartsteq ‘сравнивает 2 Variant переменные
    __vbaFpCmpCy ‘сравнивает значение с плавающей точкой с Currency значением

    [Разблокирование элементов управления]

    Любой элемент управления на форме может быть видимым или невидимым,
    доступным или заблокированным.
    Для установки свойств объектов существует функция __vbaObjSet.
    Именно с помощью нее можно заблокировать или разблокировать элемент
    управления и изменить любое из его свойств. Поэтому нам необходимо
    отлавливать вызов именно этой функции. Откроем например Olly Debugger,
    найдем эту функцию среди вызываемых анализируемым файлом и поставим на нее
    breakpoint нажав кнопку F2. Затем запустим исследуемый файл.
    Когда breakpoint сработает — посмотрите окружающий код. Если он напоминает

    50 push eax
    52 push edx
    FFD7 call edi
    8BD8 mov ebx, eax
    6A00 push 00
    53 push ebx
    8B03 mov eax, dword ptr [ebx]

    то Вы на верном пути. Как вы думаете, что это за «push 00»?
    00h в VB означает FALSE, а FFh TRUE, из этого следует, что
    данная команда устанавливает свойство в FALSE, то,
    возможно, это и есть блокировка элемента управления на форме.
    Но это может быть и установка любого другого свойства формы в TRUE.
    Так как синтаксис один и тот же, то принадлежность данной
    команды к изменению свойства блокировки можно установить только
    анализом окружающего кода. Но думаю с этой мелочью Вы справитесь сами.

    [Методика анализа простейших проверок строковых данных]

    Нижеследующий текст — мой вольный перевод статьи How to Research Visual Basic
    с одного из зарубежных сайтов infonegocio.com. За английскую версию текста спасибо ее авторам.

    Что нам может понадобиться? Любой дизассемблер/отладчик. Подойдет Win32Dasm или Olly Debugger
    Если вы используете W32Dasm, то функции, используемые программой вы можете посмотреть в меню «Functions» -> «Imports».

    Если вы будете исследовать базу данных Jet,
    то помните, что названия функции не говорят сами за себя и вам будет трудно узнать
    действие, которое выполняет та или иная функция. В данном случае Вам остается только
    смотреть названия функций, которые любезно оставлены программистами.
    Дыр же в этих драйверах практически нет, поэтому исследовать код для
    работы с базами данных довольно сложно.

    Рассмотрим пример блокировки работы кода проверкой строки. Для удобства поиска нужного
    кода поставим MessageBox. Откройте Visual Basic, добавьте на форму текстовое
    поле и кнопку, затем в обработчике щелчка по кнопке напишите следующий код:

    Private Sub Command1_Click()
    Dim X&
    X = 43690
    MsgBox «Checking value»
    If CLng(Trim$(Text1.Text)) = X Then MsgBox («Complete»)
    End Sub

    Теперь откроем Olly Debugger, загрузим в него файл (предварительно откомпилируем его в VB6) и ищем вызов функции rtcMsgBox.

    00401F54 FF1520104000 CALL [MSVBVM60!rtcMsgBox] ;MsgBox «Checking value»
    00401F5A 8D459C LEA EAX,[EBP-64]
    00401F5D 8D4DAC LEA ECX,[EBP-54]
    00401F60 50 PUSH EAX
    00401F61 8D55BC LEA EDX,[EBP-44]
    00401F64 51 PUSH ECX
    00401F65 8D45CCLEA EAX,[EBP-34]
    00401F68 52 PUSH EDX
    00401F69 50 PUSH EAX
    00401F6A 6A04 PUSH 04
    00401F6C FF1508104000 CALL [MSVBVM60!__vbaFreeVarList]
    00401F72 8B0E MOV ECX,[ESI]
    00401F74 83C414 ADD ESP,14
    00401F77 56 PUSH ESI
    00401F78 FF9104030000 CALL [ECX+00000304]
    00401F7E 8D55DCLEA EDX,[EBP-24]
    00401F81 50 PUSH EAX
    00401F82 52 PUSH EDX
    00401F83 FF1524104000 CALL [MSVBVM60!__vbaObjSet]
    00401F89 8BF0 MOV ESI,EAX
    00401F8B 8D4DE4LEA ECX,[EBP-1C]
    00401F8E 51 PUSH ECX
    00401F8F 56 PUSH ESI
    00401F90 8B06 MOV EAX,[ESI]
    00401F92 FF90A0000000 CALL [EAX+000000A0]
    00401F98 3BC7 CMP EAX,EDI
    00401F9A DBE2 FCLEX
    00401F9C 7D12 JGE 00401FB0
    00401F9E 68A0000000 PUSH 000000A0
    00401FA3 6804184000 PUSH 00401804
    00401FA8 56 PUSH ESI
    00401FA9 50 PUSH EAX
    00401FAA FF1518104000 CALL [MSVBVM60!__vbaHresultCheckObj] ;получение содержимого Text1.Text
    00401FB0 8B55E4MOV EDX,[EBP-1C]
    00401FB3 52 PUSH EDX
    00401FB4 FF1514104000 CALL [MSVBVM60!rtcTrimBstr] ;Trim$
    00401FBA 8BD0 MOV EDX,EAX
    00401FBC 8D4DE0 LEA ECX,[EBP-20]
    00401FBF FF1584104000 CALL [MSVBVM60!__vbaStrMove]
    00401FC5 50 PUSH EAX
    00401FC6 FF1568104000 CALL [MSVBVM60!__vbaI4Str] ;CLng
    00401FCC 33C9 XOR ECX,ECX
    00401FCE 3DAAAA0000 CMP EAX,0000AAAA ;Число 43690 в HEX виде
    00401FD3 8D55E0LEA EDX,[EBP-20]
    00401FD6 8D45E4LEA EAX,[EBP-1C]
    00401FD9 0F94C1 SETZ CL
    00401FDC 52 PUSH EDX
    00401FDD 50 PUSH EAX
    00401FDE F7D9 NEG ECX
    00401FE0 6A02 PUSH 02
    00401FE2 8BF1 MOV ESI,ECX
    00401FE4 FF156C104000 CALL [MSVBVM60!__vbaFreeStrList]
    00401FEA 83C40C ADD ESP,0C
    00401FED 8D4DDC LEA ECX,[EBP-24]
    00401FF0 FF1594104000 CALL [MSVBVM60!__vbaFreeObj]
    00401FF6 663BF7 CMP SI,DI
    00401FF9 7463 JZ 0040205E ;переход на Complete код (MsgBox «Complete»)
    00401FFB B804000280 MOV EAX,80020004
    00402000 8D558C LEA EDX,[EBP-74]
    00402003 8D4DCCLEA ECX,[EBP-34]
    00402006 8945A4 MOV [EBP-5C],EAX
    00402009 895D9C MOV [EBP-64],EBX
    0040200C 8945B4 MOV [EBP-4C],EAX
    0040200F 895DAC MOV [EBP-54],EBX
    00402012 8945C4 MOV [EBP-3C],EAX
    00402015 895DBC MOV [EBP-44],EBX
    00402018 C7459418184000 MOV DWORD PTR [EBP-6C],00401818
    0040201F C7458C08000000 MOV DWORD PTR [EBP-74],00000008
    00402026 FF157C104000 CALL [MSVBVM60!__vbaVarDup]
    0040202C 8D4D9C LEA ECX,[EBP-64]

    Чтобы отключить проверку значения — нужно изменить переход по адресу 00401FF9 с условного на безусловный. Для этого меняем JE на JMP (то есть 74h на EBh). Как видите — такие простенькие проверки в VB коде также малоэффективны, как и их аналоги на С++.

    [Анализ значений в виде разных типов данных]

    Нижеследующий текст — мой вольный перевод статей с уже указанного выше сайта: infonegocio.com.
    За английские версии этих статей спасибо их авторам.

    — Получение данных, если они лежат строкой в открытом виде

    Когда VB копирует строку в память — Вы ее можете отловить.
    Для этого нужно поставить breakpoint на функцию __vbaStrCopy и когда он у Вас сработает,
    посмотрите код после je 66047B00 (он содержится в библиотеке и един для всех программ).
    Затем нужно посмотреть содержимое edx-04 (в SoftICE нужно ввести команду d edx-04).
    При этом вы увидите строчку, с которой работает в данный момент функция.

    Exported fn(): __vbaStrCopy — Ord:008Ah
    :66024532 56 PUSH ESI
    :66024533 57 PUSH EDI
    :66024534 85D2 TEST EDX, EDX
    :66024536 8BF9 MOV EDI, ECX
    :66024538 0F84C2350200 JE 66047B00
    :6602453E FF72FC PUSH [EDX-04] ;по этому адресу лежит строка
    :66024541 52 PUSH EDX

    — Получение строки, если она сравнивается с введенной

    Поставьте breakpoint на функцию __vbaStrComp и чуть выше ее вызова будут два
    push’а — они заносят в стек две Variant переменные для сравнения.
    Посмотрите содержимое EAX чтобы узнать адрес текста.
    Глянем что лежит по этому адресу, наверняка это нужная строка.

    00401BC7 50 PUSH EAX ;то что Вы ввели
    00401BC8 6880174000 PUSH 00401780 ;верная строка
    00401BCD FF1530104000 CALL [MSVBVM60!__vbaStrComp]

    — Получение данных, если происходит сравнение 4х байтовых числовых переменных

    Поставьте breakpoint на функцию __vbai4str. Эта функция используется программой для
    перевода введенной текстовой строки в 4х байтовое число. Когда breakpoint сработает — посмотрите
    код ниже — там наверняка будет процедура сравнения строк. Но значение нужное нам будет
    в HEX виде (например десятичное число 987654321 равно шестнадцатеричному 3ADE68B1).

    00401B77 FF155C104000 CALL [MSVBVM60!__vbaI4Str]
    00401B7D 8D4DE0 LEA ECX, [EBP-20]
    00401B80 8BF8 MOV EDI, EAX
    00401B82 FF157C104000 CALL [MSVBVM60!__vbaFreeStr]
    00401B88 8D4DDC LEA ECX, [EBP-24]
    00401B8B FF1580104000 CALL [MSVBVM60!__vbaFreeObj]
    00401B91 81FFB168DE3A CMP EDI, 3ADE68B1 ; 3ADE68B1 — это нужное значение
    00401B97 7520 JNZ 00401BB9 ;функция вернет ноль если введенное
    ;и сравниваемое значения равны
    ;следовательно этот переход сработает только если
    ;сравниваемые данные отличны друг от друга

    Тут мы можем узнать верное значение или отключить проверку вовсе (для этого нужно JNZ
    заменить на NOP). То есть в данном случае менять нужно байты по адресу 00401B97 с 7520 на 9090)

    — Получение правильных данных при сравнивании 2х байтовых чисел

    Отличие от предыдущего примера в том, что нужно ставить breakpoint на
    функцию __vbaI2Str. Но иногда предварительно производится преобразование
    строки сначала в 4х байтовое число посредством функции __vbaI2I4,
    а затем 4х байтовое число преобразуется в двухбайтовое.

    — Обход сравнения двух переменных типа Single

    Эту проверку обойти довольно просто. Ставьте breakpoint на функцию __vbaR4Str и когда он сработает — пролистните код
    вниз. Вы увидите переход после проверки. Просто измените условие перехода на обратное.

    00401C0C FF153C104000 CALL [MSVBVM60!_vbaR4Str]
    00401C12 D95DE4 FSTP REAL4 PTR [EBP-1C]
    00401C15 8D4DDC LEA ECX, [EBP-24]
    00401C18 8D55E0 LEA EDX, [EBP-20]
    00401C1B 51 PUSH ECX
    00401C1C 52 PUSH EDX
    00401C1D 6A02 PUSH 02
    00401C1F FF1570104000 CALL [MSVBVM60!__vbaFreeStrList]
    00401C25 83C40C ADD ESP, 0C
    00401C28 8D4DD8 LEA ECX, [EBP-28]
    00401C2B FF1594104000 CALL [MSVBVM60!__vbaFreeObj]
    00401C31 817DE43A92FC42 CMP DWORD PTR [EBP-1C], 42FC923A
    00401C38 7520 JNZ 00401C5A ;если ноль, то введенно неверное значение

    Замените инструкцию по адресу 00401C38 на 9090 (чтобы в данном месте не выполнялось никаких операций)
    или измените условный переход на противоположный. На Visual Basic нельзя проверить то,
    что вы заменили эти байты на два NOP’а (9090). Если у Вас все же возникают опасения, что код
    перестанет работать при отсутствии команд, просто введите команды, которые ничего не поменяют,
    например:

    inc eax
    dec eax

    Эти команды однобайтовые и как раз впишутся на место условного перехода

    — Обход проверки значений с плавающей точкой (double).

    Это также сделать очень просто. Ставьте breakpoint на функцию __vbaR8Str и пролистните код чуть ниже останова на breakpoint’е, Вы увидите процедуру сравнения (FCOMP REAL8 PTR [Address]), после нее идет test и jmp.
    Измените условие перехода на противоположное.

    00401C55 FF1510104000 CALL [MSVBVM60!rtcTrimBstr]
    00401C5B 8BD0 MOV EDX, EAX
    00401C5D 8D4DD8 LEA ECX, DWORD PTR [EBP-28]
    00401C60 FF1580104000 CALL [MSVBVM60!__vbaStrMove]
    00401C66 50 PUSH EAX
    00401C67 FF1560104000 CALL [MSVBVM60!__vbaR8Str]
    00401C6D DC1DD8104000 FCOMP REAL8 PTR [004010D8] ;сравнение значений
    00401C73 DFE0 FSTSW AX ;обработка сопроцессором
    00401C75 F6C440 TEST AH, 40 ;проверка правильности значений
    00401C78 7409 JE 00401C83 ;переход

    Замените инструкцию по адресу 00401C78 на 9090 (чтобы в данном месте не выполнялось никаких операций)
    или измените условный переход на противоположный. На Visual Basic нельзя проверить то,
    что вы заменили эти байты на два NOP’а (9090). Если у Вас все же возникают опасения, что код
    перестанет работать при отсутствии команд, просто введите команды, которые ничего не поменяют,
    например:

    inc eax
    dec eax

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

    — Получение строки, если она поXORена.

    Алгоритм работы бейсиковской процедуры таков. Введенная строка посимвольно переводится в ASCII коды,
    которые по очереди XORятся. Затем производится обратный перевод, возможно с предварительными математическими
    манипуляциями с ASCII кодами. ANSI коды могут XORиться со случайным или фиксированным числом.

    00401E6C 50 PUSH EAX
    00401E6D FF1544104000 CALL [MSVBVM60!rtcMidCharBstr]
    00401E73 8BD0 MOV EDX, EAX
    00401E75 8D4DC8 LEA ECX, [EBP-38]
    00401E78 FFD6 CALL ESI
    00401E7A 50 PUSH EAX
    00401E7B FF1518104000 CALL [MSVBVM60!rtcAnsiValueBstr]
    00401E81 0FBFC8 MOVSX ECX, AX
    00401E84 81F191000000 XOR ECX, 00000091 ;ANSI XOR 91
    00401E8A 51 PUSH ECX ;результат XOR’а
    00401E8B FF1570104000 CALL [MSVBVM60!rtcBstrFromAnsi]

    Ставим breakpoint на 00401E8A 51 PUSH ECX и отслеживаем изменение содержимого регистра ECX при прохождении цикла.
    Когда программа пройдет весь цикл — вы получите расшифрованную строку.

    CALL [MSVBVM60!rtcMidCharBstr] ;получает один символ из строки
    CALL [MSVBVM60!rtcAnsiValueBstr] ;получает из символа Ansi код
    CALL [MSVBVM60!rtcBstrFromAnsi] ;преобразует Ansi код в строку

    В этом случае ниже скорее всего будет сравнение и переход, при этом CALL [MSVBVM60!__vbaStrComp]
    может быть использован для сравнения строк. Если не производится обращение к MSVBVM60!rtcBstrFromAnsi
    тогда со строкой производятся определенные математические манипуляции, при этом она может быть представлена
    как 2х или 4х байтное число или число с плавающей точкой. Об этих случаях было сказано выше.

    [Заключение]

    Надеюсь, что прочитав данную статью Вас уже больше не пугает анализ VB кода. Чтобы посмотреть как выглядить VB код вживую — советую посмотреть мои примеры (лежат на данном сайте в разделе «Разное»).

    Еще раз благодарю авторов сайта infonegocio.com, так как если бы не их туториалы на английском,
    то этой статьи возможно и не было бы.

    Удачи и спасибо за то, что дочитали статью до конца.

    Исследование алгоритма работы программ, написанных на языках высокого уровня, традиционно начинается с реконструкции ключевых структур исходного языка — функций, локальных и глобальных переменных, ветвлений, циклов и так далее. Это делает дизассемблированный листинг более наглядным и значительно упрощает его анализ.

    Содержание

    1. Исследование алгоритма работы программ
    2. Идентификация функций
    3. Непосредственный вызов функции
    4. Вызов функции по указателю
    5. Вызов функции по указателю с комплексным вычислением целевого адреса
    6. «Ручной» вызов функции инструкцией JMP
    7. Автоматическая идентификация функций посредством IDA Pro
    8. Пролог
    9. Эпилог
    10. Специальное замечание
    11. «Голые» (naked) функции
    12. Идентификация встраиваемых (inline) функций
    13. Заключение

    Исследование алгоритма работы программ

    Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Мы попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2017.

    Современные дизассемблеры достаточно интеллектуальны и львиную долю распознавания ключевых структур берут на себя. В частности, IDA Pro успешно справляется с идентификацией стандартных библиотечных функций, локальных переменных, адресуемых через регистр
    ESP, case-ветвлений и прочего. Однако порой она ошибается, вводя исследователя в заблуждение, к тому же ее высокая стоимость не всегда оправдывает применение. Например, студентам, изучающим ассемблер (а лучшее средство изучения ассемблера — дизассемблирование чужих программ), она едва ли по карману.

    РЕКОМЕНДУЕМ:
    Лучший редактор бинарных файлов для Windows

    Разумеется, на IDA свет клином не сошелся, существуют и другие дизассемблеры — скажем, тот же DUMPBIN, входящий в штатную поставку SDK. Почему бы на худой конец не воспользоваться им? Конечно, если под рукой нет ничего лучшего, сойдет и DUMPBIN, но в этом случае об интеллектуальности дизассемблера придется забыть и пользоваться исключительно своей головой.

    Первым делом мы познакомимся с неоптимизирующими компиляторами — анализ их кода относительно прост и вполне доступен для понимания даже новичкам в программировании. Затем же, освоившись с дизассемблером, перейдем к вещам более сложным — оптимизирующим компиляторам, генерирующим очень хитрый, запутанный и витиеватый код.

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

    Неплохой сборник, как раз для продолжительной работы

    Идентификация функций

    Функция (также называемая процедурой или подпрограммой) — основная структурная единица процедурных и объектно ориентированных языков, поэтому дизассемблирование кода обычно начинается с отождествления функций и идентификации передаваемых им аргументов.

    Строго говоря, термин «функция» присутствует не во всех языках, но даже там, где он присутствует, его определение варьируется от языка к языку. Не вдаваясь в детали, мы будем понимать под функцией обособленную последовательность команд, вызываемую из различных частей программы. Функция может принимать один и более аргументов, а может не принимать ни одного; может возвращать результат своей работы, а может и не возвращать — это уже не суть важно. Ключевое свойство функции — возвращение управления на место ее вызова, а ее характерный признак — множественный вызов из различных частей программы (хотя некоторые функции вызываются лишь из одного места).

    Откуда функция знает, куда следует возвратить управление? Очевидно, вызывающий код должен предварительно сохранить адрес возврата и вместе с прочими аргументами передать его вызываемой функции. Существует множество способов решения этой проблемы: можно, например, перед вызовом функции поместить в ее конец безусловный переход на адрес возврата, можно сохранить адрес возврата в специальной переменной и после завершения функции выполнить косвенный переход, используя эту переменную как операнд инструкции
    jump… Не останавливаясь на обсуждении сильных и слабых сторон каждого метода, отметим, что компиляторы в подавляющем большинстве случаев используют специальные машинные команды
    CALL и
    RET, соответственно предназначенные для вызова функций и возврата из них.

    Инструкция
    CALL закидывает адрес следующей за ней инструкции на вершину стека, а
    RET стягивает и передает на него управление. Тот адрес, на который указывает инструкция
    CALL, и есть адрес начала функции. А замыкает функцию инструкция
    RET (но внимание: не всякий
    RET обозначает конец функции!).

    Таким образом, распознать функцию можно двояко: по перекрестным ссылкам, ведущим к машинной инструкции
    CALL, и по ее эпилогу, завершающемуся инструкцией
    RET. Перекрестные ссылки и эпилог в совокупности позволяют определить адреса начала и конца функции. Немного забегая вперед, заметим, что в начале многих функций присутствует характерная последовательность команд, называемая прологом, которая также пригодна и для идентификации функций. А теперь рассмотрим все эти темы поподробнее.

    Непосредственный вызов функции

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

    Рассмотрим следующий пример (Listing1 в материалах к статье):

    void func();

    int main(){

      int a;

      func();

      a=0x666;

      func();

    }

    void func(){

      int a;

      a++;

    }

    Компилируем привычным образом:

    Результат компиляции в IDA Pro должен выглядеть приблизительно так:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    .text:00401020    push    ebp

    .text:00401021    mov     ebp, esp

    .text:00401023    push    ecx

    .text:00401024    call    sub_401000

    .text:00401024    ; Вот мы выловили инструкцию call c непосредственным операндом,

    .text:00401024    ; представляющим собой адрес начала функции. Точнее ее смещение

    .text:00401024    ; в кодовом сегменте (в данном случае в сегменте .text).

    .text:00401024    ; Теперь можно перейти к строке .text:00401000 и, дав функции

    .text:00401024    ;  собственное имя, заменить операнд инструкции call на конструкцию

    .text:00401024    ;  «call offset Имя моей функции».

    .text:00401024    ;

    .text:00401029    mov     [ebp+var_4], 666h

    .text:00401029    ; А вот наше знакомое число 0x666, присваиваемое переменной

    .text:00401030    call    sub_401000

    .text:00401030    ; А вот еще один вызов функции! Обратившись к строке .text:401000,

    .text:00401030    ; мы увидим, что эта совокупность инструкций уже определена как функция,

    .text:00401030    ; и все, что потребуется сделать, заменить call 401000 на

    .text:00401030    ; «call offset Имя моей функции».

    .text:00401030    ;

    .text:00401035    xor     eax, eax

    .text:00401037    mov     esp, ebp

    .text:00401039    pop     ebp

    .text:0040103A    retn

    .text:0040103A    ; Вот нам встретилась инструкция возврата из функции, однако не факт,

    .text:0040103A    ; что это действительно конец функции, ведь функция может иметь

    .text:0040103A    ; и несколько точек выхода. Однако смотри: следом за ret расположено

    .text:0040103A    ; начало следующей функции. Поскольку функции не могут перекрываться,

    .text:0040103A    ; выходит, что данный ret конец функции!

    .text:0040103A sub_401020      endp

    .text:0040103B sub_40103B      proc near               ; DATA XREF: .rdata:0040D11C?o

    .text:0040103B    push    esi

    .text:0040103C    push    1

    ...

    Судя по адресам, «наша функция» в листинге расположена выше функции main:

    .text:00401000    push    ebp

    .text:00401000    ; На эту строку ссылаются операнды нескольких инструкций call.

    .text:00401000    ; Следовательно, это адрес начала «нашей функции».

    .text:00401001    mov     ebp, esp         ; <

    .text:00401003    push    ecx              ; <

    .text:00401004    mov     eax, [ebp+var_4] ; <

    .text:00401007    add     eax, 1           ; < тело «нашей функции»

    .text:0040100A    mov     [ebp+var_4], eax ; <

    .text:0040100D    mov     esp, ebp         ; <

    .text:0040100F    pop     ebp              ; <

    .text:00401010    retn                     ; <

    Как видишь, все очень просто.

    Вызов функции по указателю

    Однако задача заметно усложняется, если программист (или компилятор) использует косвенные вызовы функций, передавая их адрес в регистре и динамически вычисляя его (адрес, а не регистр!) на стадии выполнения программы. Именно так, в частности, реализована работа с виртуальными функциями, однако в любом случае компилятор должен каким-то образом сохранить адрес функции в коде. Значит, его можно найти и вычислить! Еще проще загрузить исследуемое приложение в отладчик, установить на «подследственную» инструкцию
    CALL точку останова и, дождавшись всплытия отладчика, посмотреть, по какому адресу она передаст управление. Рассмотрим следующий пример (Listing2):

    int func(){

      return 0;

    }

    int main(){

      int (*a)();

      a = func;

      a();

    }

    Результат его компиляции должен в общем случае выглядеть так (функция main):

    .text:00401010    push    ebp

    .text:00401011    mov     ebp, esp

    .text:00401013    push    ecx

    .text:00401014    mov     [ebp+var_4], offset sub_401000

    .text:0040101B    call    [ebp+var_4]

    .text:0040101B    ; Вот инструкция CALL, выполняющая косвенный вызов функции

    .text:0040101B    ; по адресу, содержащемуся в ячейке [ebp+var_4].

    .text:0040101B    ; Как узнать, что же там содержится? Поднимем глазки строчкой выше

    .text:0040101B    ; и обнаружим: mov [ebp+var_4], offset sub_401000. Ага!

    .text:0040101B    ; Значит, управление передается по смещению sub_401000,

    .text:0040101B    ; где располагается адрес начала функции! Теперь осталось только

    .text:0040101B    ; дать функции осмысленное имя.

    .text:0040101E    xor     eax, eax

    .text:00401020    mov     esp, ebp

    .text:00401022    pop     ebp

    .text:00401023    retn

    Вызов функции по указателю с комплексным вычислением целевого адреса

    В некоторых, достаточно немногочисленных программах встречается и косвенный вызов функции с комплексным вычислением ее адреса. Рассмотрим следующий пример (Listing3):

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    int func_1(){

      return 0;

    }

    int func_2(){

      return 0;

    }

    int func_3(){

      return 0;

    }

    int main(){

      int x;

      int a[3] = {(int) func_1,(int) func_2, (int) func_3}; int (*f)();

      for (x=0;x < 3;x++){

        f = (int (*)()) a[x];

        f();

      }

    }

    Результат дизассемблирования этого кода в общем случае должен выглядеть так:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    .text:00401030     push    ebp

    .text:00401031     mov     ebp, esp

    .text:00401033     sub     esp, 18h

    .text:00401036     mov     eax, ___security_cookie

    .text:0040103B     xor     eax, ebp

    .text:0040103D     mov     [ebp+var_4], eax

    .text:00401040     mov     [ebp+var_10], offset sub_401000

    .text:00401047     mov     [ebp+var_C], offset sub_401010

    .text:0040104E     mov     [ebp+var_8], offset sub_401020

    .text:00401055     mov     [ebp+var_14], 0

    .text:0040105C     jmp     short loc_401067

    .text:0040105E ; —————————————

    .text:0040105E

    .text:0040105E loc_40105E: ; CODE XREF: sub_401030+4A?j

    .text:0040105E     mov     eax, [ebp+var_14]

    .text:00401061     add     eax, 1

    .text:00401064     mov     [ebp+var_14], eax

    .text:00401067

    .text:00401067 loc_401067: ; CODE XREF: sub_401030+2C?j

    .text:00401067     cmp     [ebp+var_14], 3

    .text:0040106B     jge     short loc_40107C

    .text:0040106D     mov     ecx, [ebp+var_14]

    .text:00401070     mov     edx, [ebp+ecx*4+var_10]

    .text:00401074     mov     [ebp+var_18], edx

    .text:00401077     call    [ebp+var_18]

    .text:0040107A     jmp     short loc_40105E

    .text:0040107C ; —————————————

    .text:0040107C

    .text:0040107C loc_40107C: ; CODE XREF: sub_401030+3B?j

    .text:0040107C     xor     eax, eax

    .text:0040107E     mov     ecx, [ebp+var_4]

    .text:00401081     xor     ecx, ebp

    .text:00401083     call    @__security_check_cookie@4 ; __security_check_cookie(x)

    .text:00401088     mov     esp, ebp

    .text:0040108A     pop     ebp

    .text:0040108B     retn

    В строке
    call [ebp+var_18] происходит косвенный вызов функции. А что у нас в
    [ebp+var_18]?
    Поднимаем глаза на строку вверх — в
    [ebp+var_18]
    у нас значение
    edx. А чему равен сам
    edx? Прокручиваем еще одну строку вверх —
    edx равен содержимому ячейки
    [ebp+ecx*4+var_10].
    Вот дела! Мало того что нам надо узнать содержимое этой ячейки, так еще и предстоит вычислить ее адрес!

    Чему равен
    ECX? Содержимому
    [ebp+var_14].
    А оно чему равно? «Сейчас выясним…» — бормочем мы себе под нос, прокручивая экран дизассемблера вверх. Ага, нашли: в строке
    0x401064 в него загружается содержимое
    EAX! Какая радость! И долго мы будем так блуждать по коду?

    Конечно, можно, затратив неопределенное количество времени, усилий и бодрящего напитка, реконструировать весь ключевой алгоритм целиком (тем более что мы практически подошли к концу анализа), но где гарантия, что при этом не будут допущены ошибки?

    РЕКОМЕНДУЕМ:
    Взлом приложений для Андроид с помощью отладчика

    Гораздо быстрее и надежнее загрузить исследуемую программу в отладчик, установить бряк на строку
    text:00401077 и, дождавшись всплытия окна отладчика, посмотреть, что у нас расположено в ячейке
    [ebp+var_18].
    Отладчик будет всплывать трижды, причем каждый раз показывать новый адрес! Заметим, что определить этот факт в дизассемблере можно только после полной реконструкции алгоритма.

    Однако не стоит питать излишних иллюзий о мощи отладчика. Программа может тысячу раз вызывать одну и ту же функцию, а на тысяча первый — вызвать совсем другую. Отладчик бессилен это определить. Ведь вызов такой функции может произойти в непредсказуемый момент, например при определенном сочетании времени, обрабатываемых программой данных и текущей фазы Луны. Ну не будем же мы целую вечность гонять программу под отладчиком?

    Дизассемблер — дело другое. Полная реконструкция алгоритма позволит однозначно и гарантированно отследить все адреса косвенных вызовов. Вот потому дизассемблер и отладчик должны скакать в одной упряжке!

    Напоследок предлагаю взглянуть на такой участок дизассемблированного листинга:

    .text:0040103D     mov     [ebp+var_4], eax

    .text:00401040     mov     [ebp+var_10], offset sub_401000

    .text:00401047     mov     [ebp+var_C], offset sub_401010

    .text:0040104E     mov     [ebp+var_8], offset sub_401020

    .text:00401055     mov     [ebp+var_14], 0

    Воспользуемся средствами IDA и посмотрим, что загружается в ячейки памяти
    [ebp+…]. А это как раз адреса трех наших функций, последовательно размещенных компилятором друг за дружкой:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    .text:00401000     push    ebp

    .text:00401001     mov     ebp, esp

    .text:00401003     xor     eax, eax

    .text:00401005     pop     ebp

    .text:00401006     retn

    .text:00401010     push    ebp

    .text:00401011     mov     ebp, esp

    .text:00401013     xor     eax, eax

    .text:00401015     pop     ebp

    .text:00401016     retn

    .text:00401020     push    ebp

    .text:00401021     mov     ebp, esp

    .text:00401023     xor     eax, eax

    .text:00401025     pop     ebp

    .text:00401026     retn

    «Ручной» вызов функции инструкцией JMP

    Самый тяжелый случай представляют «ручные» вызовы функции командой
    JMP с предварительной засылкой в стек адреса возврата. Вызов через
    JMP в общем случае выглядит так:
    PUSH ret_addrr/JMP func_addr, где
    ret_addrr и
    func_addr — непосредственные или косвенные адреса возврата и начала функции соответственно. Кстати, заметим, что команды
    PUSH и
    JMP не всегда следуют одна за другой и порой бывают разделены другими командами.

    Возникает резонный вопрос: чем же так плох
    CALL и зачем прибегать к
    JMP? Дело в том, что функция, вызванная по
    CALL, после возврата управления материнской функции всегда передает управление команде, следующей за
    CALL. В ряде случаев (например, при структурной обработке исключений) возникает необходимость после возврата из функции продолжать выполнение не со следующей за
    CALL командой, а совсем с другой ветки программы. Тогда-то и приходится вручную заносить требуемый адрес возврата и вызывать дочернюю функцию через
    JMP.

    Идентифицировать такие функции очень сложно — контекстный поиск ничего не дает, поскольку команд
    JMP, использующихся для локальных переходов, в теле любой программы очень и очень много — попробуй-ка проанализируй их все! Если же этого не сделать, из поля зрения выпадут сразу две функции — вызываемая функция и функция, на которую передается управление после возврата. К сожалению, быстрых решений этой проблемы не существует, единственная зацепка — вызывающий
    JMP практически всегда выходит за границы функции, в теле которой он расположен. Определить же границы функции можно по эпилогу.

    Рассмотрим следующий пример (Listing4):

    int funct(){

      return 0;

    }

    int main(){

      __asm{

        LEA ESI, return_addr

        PUSH ESI

        JMP funct

        return_addr:

      }

    }

    Результат его компиляции в общем случае должен выглядеть так:

    .text:00401010     push    ebp

    .text:00401011     mov     ebp, esp

    .text:00401013     push    esi

    .text:00401014     lea     esi, loc_401020

    .text:0040101A     push    esi

    .text:0040101B     jmp     sub_401000

    ...

    Смотри, казалось бы, тривиальный условный переход, что в нем такого? Ан нет! Это не простой переход, это замаскированный вызов функции! Откуда он следует? Давай-ка перейдем по смещению
    sub_401000 и посмотрим:

    .text:00401000     push    ebp

    .text:00401001     mov     ebp, esp

    .text:00401003     xor     eax, eax

    .text:00401005     pop     ebp

    .text:00401006     retn

    Как ты думаешь, куда этот
    retn возвращает управление? Естественно, по адресу, лежащему на верхушке стека. А что у нас лежит на стеке?
    PUSH EBP из строки
    401000, обратно выталкивается инструкцией
    POP из строки
    401005. Возвращаемся назад, к месту безусловного перехода, и начинаем медленно прокручивать экран дизассемблера вверх, отслеживая все обращения к стеку. Ага, попалась птичка! Инструкция
    PUSH ESI из строки
    40101A закидывает на вершину стека содержимое регистра
    ESI, а он сам, в свою очередь, строкой выше принимает «на грудь» значение
    loc_401020 — это и есть адрес начала функции, вызываемой командой
    JMP (вернее, не адрес, а смещение, но это не принципиально важно):

    .text:00401020     pop     esi

    .text:00401021     pop     ebp

    .text:00401022     retn

    Автоматическая идентификация функций посредством IDA Pro

    Дизассемблер IDA Pro способен анализировать операнды инструкций
    CALL, что позволяет ему автоматически разбивать программу на функции. Причем IDA вполне успешно справляется с большинством косвенных вызовов. Между тем современные версии дизассемблера на раз-два справляются с комплексными и «ручными» вызовами функций командой
    JMP.

    IDA успешно распознала «ручной» вызов функции

    IDA успешно распознала «ручной» вызов функции

    Пролог

    Большинство неоптимизирующих компиляторов помещают в начало функции следующий код, называемый прологом:

    push   ebp

    mov    ebp, esp

    sub    esp, xx

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

    Последовательность
    PUSH EBP/MOV EBP,ESP/SUB ESP,xx может служить хорошей сигнатурой для нахождения всех функций в исследуемом файле, включая и те, на которые нет прямых ссылок. Такой прием, в частности, использует в своей работе IDA Pro, однако оптимизирующие компиляторы умеют адресовать локальные переменные через регистр
    ESP и используют
    EBP как и любой другой регистр общего назначения. Пролог оптимизированных функций состоит из одной лишь команды
    SUB ESP, xxx — последовательность слишком короткая для использования ее в качестве сигнатуры функции, увы. Более подробный рассказ об эпилогах функций нас ждет впереди.

    Эпилог

    В конце своей жизни функция закрывает кадр стека, перемещая указатель вершины стека «вниз», и восстанавливает прежнее значение
    EBP (если только оптимизирующий компилятор не адресовал локальные переменные через
    ESP, используя
    EBP как обычный регистр общего назначения). Эпилог функции может выглядеть двояко: либо
    ESP увеличивается на нужное значение командой
    ADD, либо в него копируется значение
    EBP, указывающее на низ кадра стека.

    Обобщенный код эпилога функции выглядит так. Эпилог 1:

    Эпилог 2:

    Важно отметить: между командами
    POP EBP/ADD ESP, xxx и
    MOV ESP,EBP/POP EBP могут находиться и другие команды — они необязательно должны следовать вплотную друг к другу. Поэтому для поиска эпилогов контекстный поиск непригоден — требуется применять поиск по маске.

    Если функция написана с учетом соглашения PASCAL, то ей приходится самостоятельно очищать стек от аргументов. В подавляющем большинстве случаев это делается инструкцией
    RET n, где
    n — количество байтов, снимаемых из стека после возврата. Функции же, соблюдающие С-соглашение, предоставляют очистку стека вызывающему их коду и всегда оканчиваются командой
    RET. API-функции Windows представляют собой комбинацию соглашений С и PASCAL — аргументы заносятся в стек справа налево, но очищает стек сама функция.

    Таким образом,
    RET может служить достаточным признаком эпилога функции, но не всякий эпилог — это конец. Если функция имеет в своем теле несколько операторов
    return (как часто и бывает), компилятор в общем случае генерирует для каждого из них свой собственный эпилог. Необходимо обратить внимание, находится ли за концом эпилога новый пролог или продолжается код старой функции. Также нельзя забывать и о том, что компиляторы обычно (но не всегда!) не помещают в исполняемый файл код, никогда не получающий управления. Иначе говоря, у функции будет всего один эпилог, а все находящееся после первого
    return будет выброшено как ненужное.

    Между тем не стоит спешить вперед паровоза. Откомпилируем с параметрами по умолчанию следующий пример (Listing5):

    int func(int a){

      return a++;

      a=1/a;

      return a;

    }

    int main(){

      func(1);

    }

    Откомпилированный результат будет выглядеть так (приведен код только функции
    func):

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    .text:00401000     push    ebp

    .text:00401001     mov     ebp, esp

    .text:00401003     push    ecx

    .text:00401004     mov     eax, [ebp+arg_0]

    .text:00401007     mov     [ebp+var_4], eax

    .text:0040100A     mov     ecx, [ebp+arg_0]

    .text:0040100D     add     ecx, 1           ; Производим сложение

    .text:00401010     mov     [ebp+arg_0], ecx

    .text:00401013     mov     eax, [ebp+var_4]

    .text:00401016     jmp     short loc_401027 ; Выполняем безусловный переход

    .text:00401016                              ; на эпилог функции

    .text:00401018 ; —————————————

    .text:00401018     mov     eax, 1

    .text:0040101D     cdq

    .text:0040101E     idiv    [ebp+arg_0]      ; Код деления единицы на параметр

    .text:00401021     mov     [ebp+arg_0], eax ; остался

    .text:00401024     mov     eax, [ebp+arg_0] ; Компилятор не посчитал нужным его убрать

    .text:00401027

    .text:00401027 loc_401027:                  ; CODE XREF: sub_401000+16?j

    .text:00401027     mov     esp, ebp         ; При этом эпилог только один

    .text:00401029     pop     ebp              ;

    .text:0040102A     retn

    Теперь посмотрим, какой код сгенерирует компилятор, когда внеплановый выход из функции происходит при срабатывании некоторого условия (Listing6):

    int func(int a){

      if (a != 0)

        return a++;

      return 1/a;

    }

    int main(){

      func(1);

    }

    Результат компиляции (только
    func):

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    .text:00401000    push    ebp

    .text:00401001    mov     ebp, esp

    .text:00401003    push    ecx

    .text:00401004    cmp     [ebp+arg_0], 0   ; Сравниваем аргумент функции с нулем

    .text:00401008    jz      short loc_40101E ; Если они равны, переходим на метку и

    .text:00401008                             ; выполняем команду деления

    .text:0040100A    mov     eax, [ebp+arg_0] ; Если же

    .text:0040100D    mov     [ebp+var_4], eax ; не равны,

    .text:00401010    mov     ecx, [ebp+arg_0] ; то выполняем

    .text:00401013    add     ecx, 1           ; инкремент

    .text:00401016    mov     [ebp+arg_0], ecx

    .text:00401019    mov     eax, [ebp+var_4]

    .text:0040101C    jmp     short loc_401027

    .text:0040101E ; —————————————

    .text:0040101E

    .text:0040101E loc_40101E:                 ; CODE XREF: sub_401000+8?j

    .text:0040101E    mov     eax, 1

    .text:00401023    cdq

    .text:00401024    idiv    [ebp+arg_0]      ; Деление 1 на аргумент

    .text:00401027

    .text:00401027 loc_401027:                 ; CODE XREF: sub_401000+1C?j

    .text:00401027    mov     esp, ebp         ; < Это явно эпилог

    .text:00401029    pop     ebp              ; <

    .text:0040102A    retn                     ; <

    Как и в предыдущем случае, компилятор создал только один эпилог. Обрати внимание: в начале функции в строке
    00401004 аргумент сравнивается с нулем, если условие выполняется, происходит переход на метку
    loc_40101E, где выполняется деление, за которым сразу следует эпилог. Если же условие в строке
    00401004 не выполняется, выполняется сложение и происходит безусловный прыжок на эпилог.

    Специальное замечание

    Начиная с процессора 80286 в наборе команд появились две инструкции
    ENTER и
    LEAVE, предназначенные специально для открытия и закрытия кадра стека. Однако они практически никогда не используются современными компиляторами. Почему?

    Причина в том, что
    ENTER и
    LEAVE очень медлительны, намного медлительнее
    PUSH EBP/MOV EBP,ESP/SUB ESB, xxx и
    MOV ESP,EBP/POP EBP. Так, на старом добром Pentium
    ENTER выполняется за десять тактов, а приведенная последовательность команд — за семь. Аналогично
    LEAVE требует пять тактов, хотя ту же операцию можно выполнить за два (и даже быстрее, если разделить
    MOV ESP,EBP/POP EBP какой-нибудь командой).

    РЕКОМЕНДУЕМ:
    Реверс Андроид приложений

    Поэтому современный исследователь никогда не столкнется ни с
    ENTER, ни с
    LEAVE. Хотя помнить об их назначении будет нелишне (мало ли, вдруг придется дизассемблировать древние программы или программы, написанные на ассемблере, — не секрет, что многие пишущие на ассемблере очень плохо знают тонкости работы процессора и их «ручная оптимизация» заметно уступает компилятору по производительности).

    «Голые» (naked) функции

    Компилятор Microsoft Visual C++ поддерживает нестандартный квалификатор
    naked, позволяющий программистам создавать функции без пролога и эпилога. Компилятор даже не помещает в конце функции
    RET, и это приходится делать «вручную», прибегая к ассемблерной вставке
    __asm{ret} (использование
    return не приводит к желаемому результату).

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

    Для нас же, кодокопателей, в первом приближении это означает, что в программе может встретиться одна или несколько функций, не содержащих ни пролога, ни эпилога. Ну и что в этом страшного? Оптимизирующие компиляторы так же выкидывают пролог, а от эпилога оставляют один лишь
    RET, но функции элементарно идентифицируются по вызывающей их инструкции
    CALL.

    Идентификация встраиваемых (inline) функций

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

    Чем плоха развертка функций для исследования программы? Прежде всего, она увеличивает размер материнской функции и делает ее код менее наглядным — вместо
    CALLTEST EAX,EAXJZ xxx с бросающимся в глаза условным переходом мы видим кучу ничего не напоминающих инструкций, в логике работы которых еще предстоит разобраться.

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

    Рассмотрим следующий пример, чтобы увидеть, как компилятор оптимизирует встраиваемую функцию (Listing7):

    #include <stdio.h>

    inline int max(int a, int b){

      if(a > b)

        return a;

      return b;

    }

    int main(int argc, char **argv){

      printf(«%xn»,max(0x666,0x777));

      printf(«%xn»,max(0x666,argc));

      printf(«%xn»,max(0x666,argc));

      return 0;

    }

    Результат компиляции этого кода будет иметь следующий вид (функция main):

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    .text:00401000    push    ebp

    .text:00401001    mov     ebp, esp

    .text:00401003    push    777h              ; Подготавливаем два

    .text:00401008    push    666h              ; аргумента функции

    .text:0040100D    call    sub_401070        ; Вызов сравнивающей функции max

    .text:00401012    add     esp, 8

    .text:00401015    push    eax

    .text:00401016    push    offset unk_412160 ; Добавляем параметр %x

    .text:0040101B    call    sub_4010D0        ; Вызов printf

    .text:00401020    add     esp, 8

    .text:00401023    mov     eax, [ebp+arg_0]  ; Берем аргумент argc количество аргументов

    .text:00401026    push    eax

    .text:00401027    push    666h              ; Запихиваем константу

    .text:0040102C    call    sub_401070        ; Вызов функции max

    .text:00401031    add     esp, 8

    .text:00401034    push    eax

    .text:00401035    push    offset unk_412164 ; Добавляем параметр %x

    .text:0040103A    call    sub_4010D0        ; Вызов printf

    .text:0040103F    add     esp, 8

    .text:00401042    mov     ecx, [ebp+arg_0]  ; <

    .text:00401045    push    ecx               ; <

    .text:00401046    push    666h              ; <

    .text:0040104B    call    sub_401070        ; < Аналогичная

    .text:00401050    add     esp, 8            ; < последовательность

    .text:00401053    push    eax               ; < действий

    .text:00401054    push    offset unk_412168 ; <

    .text:00401059    call    sub_4010D0        ; <

    .text:0040105E    add     esp, 8

    .text:00401061    xor     eax, eax

    .text:00401063    pop     ebp

    .text:00401064    retn

    «Так-так», — шепчем себе под нос. И что же он тут накомпилировал? Встраиваемую функцию представил в виде обычной! Вот дела! Компилятор забил на наше желание сделать функцию встраиваемой (мы ведь написали модификатор
    inline). Ситуацию не исправляет даже использование параметров компилятора:
    /Od или
    /Oi. Первый служит для отключения оптимизации, второй — для создания встраиваемых функций. Такими темпами компилятор вскоре будет генерировать код, угодный собственным предпочтениям или предпочтениям его разработчика, а не программиста, его использующего! Остальное ты можешь увидеть в комментариях к дизассемблированному листингу.

    Сравнивающая функция
    max в дизассемблированном виде будет выглядеть так:

    .text:00401070    push    ebp

    .text:00401071    mov     ebp, esp

    .text:00401073    mov     eax, [ebp+arg_0]

    .text:00401076    cmp     eax, [ebp+arg_4] ; Сравниваем и в зависимости

    .text:00401079    jle     short loc_401080 ; от результата переходим

    .text:0040107B    mov     eax, [ebp+arg_0]

    .text:0040107E    jmp     short loc_401083 ; Безусловный переход на эпилог

    .text:00401080 loc_401080:                 ; CODE XREF: sub_401070+9?j

    .text:00401080    mov     eax, [ebp+arg_4]

    .text:00401083 loc_401083:                 ; CODE XREF: sub_401070+E?j

    .text:00401083    pop     ebp

    .text:00401084    retn

    Здесь тоже все важные фрагменты прокомментированы.

    Напоследок предлагаю откомпилировать и рассмотреть следующий пример (Listing8). Он немного усложнен по сравнению с предыдущим, в нем в качестве одного из значений для сравнения используется аргумент командной строки, который преобразуется из строки в число и при выводе обратно.

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    #include <iostream>

    #include <sstream>

    #include <string>

    using namespace std;

    inline string max(int a, int b){ // Встраиваемая функция нахождения максимума

      int val = (a > b) ? a : b;

      stringstream stream;

      stream << «0x» << hex << val; // Преобразуем значение в hex-число

      string res = stream.str();

      return res;

    }

    int main(int argc, char **argv){

      cout << max(0x666, 0x777) << endl;

      string par = argv[1];

      int val;

      if (par.substr(0, 2) == «0x») // Если впереди параметра есть символы ‘0x’,

        val = stoi(argv[1], nullptr, 16); // тогда это hex-число,

      else

        val = stoi(argv[1], nullptr, 10); // в ином случае — dec-число

      cout << max(0x666, val) << endl;

      cout << max(0x666, val) << endl;

      return 0;

    }

    VS Code

    VS Code

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

    Следующим действием программа берет параметр командной строки. Она различает числа двух форматов: десятичные и шестнадцатеричные, определяя их по отсутствию или наличию префикса
    0x. Два последующих оператора идентичны, в них происходят вызовы функции
    max, которой оба раза передаются одинаковые параметры:
    0x666 и параметр командной строки, преобразованный из строки в число. Эти два последовательных оператора, как и в прошлый раз, позволят нам проследить вызовы функции.
    Вывод приложения

    Вывод приложения

    Вместе с дополнительной функциональностью соответственно увеличился дизассемблерный листинг. Тем не менее суть происходящего не изменилась. Чтобы не приводить его здесь (он занимает реально много места), предлагаю тебе разобраться с ним самостоятельно.

    Заключение

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

    РЕКОМЕНДУЕМ:
    Как обмануть нейронную сеть

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

    Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (3 оценок, среднее: 5,00 из 5)

    Загрузка…

    Понравилась статья? Поделить с друзьями:
  • Мануал для рено симбол
  • Как собрать собачку из змейки пошаговая инструкция
  • Приемка товара в аптеке инструкция пошагово
  • Инструкция пользователя сканера икс 431 лаунчер
  • Силиплант инструкция по применению для роз