Приводим статью, которая увидела свет еще в середине 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
Руководство по исследованию программ, написанных на 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
New Member
- Регистрация:
- 27 фев 2017
- Публикаций:
- 532
Исследование исполняемого файла можно разделить на три этапа: поверхностный, глубокий, хирургический. На первом мы малыми силами собираем информацию о подопытном файле. Под «малыми силами» я подразумеваю легкие в использовании и широко распространенные средства анализа. В этой статье мы поговорим о них и для наглядности взломаем несложную защиту.
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен 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 JE
→ 75 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, так как если бы не их туториалы на английском,
то этой статьи возможно и не было бы.
Удачи и спасибо за то, что дочитали статью до конца.
Исследование алгоритма работы программ, написанных на языках высокого уровня, традиционно начинается с реконструкции ключевых структур исходного языка — функций, локальных и глобальных переменных, ветвлений, циклов и так далее. Это делает дизассемблированный листинг более наглядным и значительно упрощает его анализ.
Содержание
- Исследование алгоритма работы программ
- Идентификация функций
- Непосредственный вызов функции
- Вызов функции по указателю
- Вызов функции по указателю с комплексным вычислением целевого адреса
- «Ручной» вызов функции инструкцией JMP
- Автоматическая идентификация функций посредством IDA Pro
- Пролог
- Эпилог
- Специальное замечание
- «Голые» (naked) функции
- Идентификация встраиваемых (inline) функций
- Заключение
Исследование алгоритма работы программ
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Мы попытались обновить этот объемный труд и перенести его из времен 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 успешно распознала «ручной» вызов функции
Пролог
Большинство неоптимизирующих компиляторов помещают в начало функции следующий код, называемый прологом:
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
Сразу в начале своего выполнения программа вызывает встраиваемую функцию, передавая ей два шестнадцатеричных числа. В качестве результата функция возвращает большее из них, преобразованное в шестнадцатеричный формат. После чего основная функция выводит его в консоль.
Следующим действием программа берет параметр командной строки. Она различает числа двух форматов: десятичные и шестнадцатеричные, определяя их по отсутствию или наличию префикса
0x. Два последующих оператора идентичны, в них происходят вызовы функции
max, которой оба раза передаются одинаковые параметры:
0x666 и параметр командной строки, преобразованный из строки в число. Эти два последовательных оператора, как и в прошлый раз, позволят нам проследить вызовы функции.
Вывод приложения
Вместе с дополнительной функциональностью соответственно увеличился дизассемблерный листинг. Тем не менее суть происходящего не изменилась. Чтобы не приводить его здесь (он занимает реально много места), предлагаю тебе разобраться с ним самостоятельно.
Заключение
Тема «Идентификация ключевых структур» очень важна, хотя бы потому, что в современных языках программирования этих структур великое множество. И в сегодняшней статье мы только начали рассматривать функции. Ведь, кроме приведенных выше функций (обычных, голых, встраиваемых) и способов их вызова (непосредственный вызов, по указателю, с комплексным вычислением адреса), существуют также виртуальные, библиотечные. Кроме того, к функциям можно отнести конструкторы и деструкторы. Но не будем забегать вперед.
РЕКОМЕНДУЕМ:
Как обмануть нейронную сеть
Прежде чем переходить к методам объектов, статическим и виртуальным функциям, надо научиться идентифицировать стартовые функции, которые могут занимать значительную часть дизассемблерного листинга, но анализировать которые нет необходимости (за небольшими исключениями). Поэтому, дорогой друг, напиши в комментах к статье, что ты думаешь о теме идентификации и какие конструкции тебе интересны для анализа.
(3 оценок, среднее: 5,00 из 5)
Загрузка…