Категория > Новости > Фундаментальные основы хакерства. Ищем тестовые строки в чужой программе - «Новости»
Фундаментальные основы хакерства. Ищем тестовые строки в чужой программе - «Новости»23-04-2022, 00:00. Автор: Page |
странице автора. Казалось бы, что может быть сложного в идентификации строк? Если то, на что ссылается указатель, выглядит как строка, это и есть строка! Более того, в подавляющем большинстве случаев строки обнаруживаются и идентифицируются тривиальным просмотром дампа программы (при условии, конечно, что они не зашифрованы, но шифрование — тема отдельного разговора). Так‑то оно так, да не все столь просто! Читайте также - Столовые предметы из серебра, охватывают весь необходимый перечень посуды для правильной сервировки стола, а по своему качеству исполнения удовлетворит любого даже самого изысканного эстета. Столовое серебро по доступным ценам. Задача номер один — автоматизированное выявление строк в программе: не пролистывать же мегабайтовые дампы вручную? Существует множество алгоритмов идентификации строк. Самый простой (но не самый надежный) основан на двух тезисах:
Условимся считать минимальную длину строки равной N байтам. Тогда для автоматического выявления всех строк достаточно отыскать все последовательности из N и более «строковых» символов. Весь вопрос в том, чему должна быть равна N и какие символы считать «строковыми». Если N имеет малое значение, порядка трех‑четырех байтов, то мы получим очень большое количество ложных срабатываний. Напротив, когда N велико, порядка шести‑восьми байтов, число ложных срабатываний близко к нулю и ими можно пренебречь, но все короткие строки, например Зайдем с другого конца. Если в программе есть строка, значит, на нее кто‑нибудь да ссылается. А раз так, можно поискать среди непосредственных значений указатель на распознанную строку. И если он будет найден, шансы на то, что это действительно именно строка, а не случайная последовательность байтов, резко возрастают. Все просто, не так ли? Просто, да не совсем! Рассмотрим следующий пример ( program writeln_d; begin Writeln('Hello, Sailor!'); end. Откомпилируем этот пример. Хотелось бы сказать, любым Pascal-компилятором, только любой нам не подойдет, поскольку нам нужен бинарный код под архитектуру x86-64. Это автоматически сужает круг подходящих компиляторов. Даже популярный Free Pascal все еще не умеет билдить программы для Windows x64. Но не убирай его далеко, он нам еще пригодится. В таком случае нам придется воспользоваться Embarcadero Delphi 10.4. Настрой компилятор для построения 64-битных приложений и загрузи откомпилированный файл в дизассемблер: IDA определила, что перед вызовом функции .text:000000000040E6DC aHelloSailor: ; DATA XREF: _ZN9Writeln_d14initializationEv+26↑o .text:000000000040E6DC text "UTF-16LE", 'Hello, Sailor!',0 Ага! Текст в кодировке UTF-16LE. Что такое UTF-16, думаю, всем понятно. Два конечных символа обозначают порядок байтов. В данном случае, поскольку приложение скомпилировано для архитектуры x86-64, в которой используется порядок байтов «от младшего к старшему» — little endian, упомянутые символы говорят именно об этом. В противоположном случае, например на компьютере с процессором SPARC, кодировка имела бы название UTF-16BE от big endian. Из этого следует, что Delphi кодирует каждый символ переменным количеством байтов: 2 или 4. Посмотрим, как себя поведет Visual C++ 2019 с аналогичным кодом: #include <stdio.h> int main() { printf("%s", "Hello, Sailor!"); } Результат дизассемблирования: main proc near sub rsp, 28h lea rdx, aHelloSailor ; "Hello, Sailor!" lea rcx, _Format; "%s" call printf xor eax, eax add rsp, 28h retn main endp Поинтересуемся, что находится в сегменте данных только для чтения ( .rdаta:0000000140002240 aHelloSailor db 'Hello, Sailor!',0 ; DATA XREF: main+4↑o Никаких дополнительных сведений о размере символов. Из этого можно сделать вывод, что используется стандартная 8-битная кодировка ASCII, в которой под каждый символ отводится только 1 байт. Ядро Windows NT изначально использовало для работы со строковыми символами кодировку UTF-16, однако до Windows 10 в пользовательском режиме применялись две кодировки: UTF-16 и ASCII (нижние 128 символов для английского языка, верхняя половина — для русского). Начиная с Windows 10, в user mode используется только UTF-16. Между тем символы могут храниться и в ASCII, что мы видели в примере выше. В C/C++ char является исходным типом символа и позволяет хранить любой символ нижней и верхней частей кодировки ASCII размером 8 бит. Хотя реализация типа wchar_t полностью лежит на совести разработчика компилятора, в Visual C++ он представляет собой полный аналог символа кодировки UTF-16LE, то есть позволяет хранить любой символ Юникода. Для демонстрации двух основных символьных типов в Visual C++ напишем элементарный пример: int main() { std::cout << "size of 'char': " << sizeof(char) << "n"; std::cout << "size of 'wchar': " << sizeof(wchar_t) << "n"; char line1[] = "Hello, Sailor!"; wchar_t line2[] = L"Hello, Sailor!"; std::cout << "size of 'array of chars': " << sizeof(line1) << "n"; std::cout << "size of 'array of wchars': " << sizeof(line2) << "n";} Результат его выполнения представлен ниже. Размеры символьных данных Думаю, все понятно без подробных пояснений: infoВ стандарте C++ есть типы символов: Размеры символов важны для обнаружения границ строк при анализе дизассемблерных листингов программ. Можно сделать вывод, что современные Visual C++ и Delphi оперируют одинаковыми типами строк, неважно какого размера, но оканчивающиеся символом 0. Но так было не всегда. В качестве исторического экскурса откомпилируем пример Среда Free Pascal Загрузим результат в IDA. IDA определила, что загружаемый исполняемый файл 32-разрядный argc = dword ptr 8argv = dword ptr 0Chenvp = dword ptr 10hpush Так‑так‑так… какой интересный код для нас приготовил Free Pascal! Сразу же бросается в глаза смещение адрес которого помещается в регистр Заглянем‑ка в документацию по компилятору… Есть контакт! По умолчанию в коде механизма вызова процедур для процессоров i386 используется соглашение register. У нормальных людей оно называется fastcall. И, поскольку для платформы x86 оно не стандартизировано, в отличие от x64, для передачи параметров используются все свободные регистры! Поэтому в том, что используется регистр Чтобы окончательно убедиться в нашей догадке, посмотрим, как распоряжается переданным параметром вызываемая функция FPC_WRITE_TEXT_SHORTSTR proc near ; CODE XREF: _main+1C↑p ; sub_403320+31↑p ... push ebx push esi push edi mov ebx, eax mov esi, edx mov edi, ecx ; Копирование параметра в регистр EDI Но тут много чего копируется, поэтому эта инструкция не доказательство. Смотрим дальше.
Ага! Следующая инструкция копирует указатель, преобразуя его в 32-разрядное значение без учета знака (указатель не может быть отрицательным). Затем с помощью команды movzx eax, byte ptr [edi] cmp eax, ebx jge short loc_40665C movzx eax, byte ptr [edi] mov edx, ebx sub edx, eax mov eax, esi call sub_4064F0 lea esi, [esi+0] loc_40665C: ; CODE XREF: FPC_WRITE_TEXT_SHORTSTR+49↑j ...где происходит похожая на манипуляцию со строкой деятельность. movzx ecx, byte ptr [edi] lea edx, [edi+1] mov eax, esi call sub_406460 ... Теперь мы смогли убедиться в правильности нашего предположения! Вернемся к основному исследованию и посмотрим, что же скрывается под подозрительным смещением: .rdаta:00409005 db 48h ; H.rdаta:00409006 db 65h ; e.rdаta:00409007 db 6Ch ; l.rdаta:00409008 db 6Ch ; l.rdаta:00409009 db 6Fh ; o.rdаta:0040900A db 2Ch ; ,.rdаta:0040900B db 20h.rdаta:0040900C db 53h ; S.rdаta:0040900D db 61h ; a.rdаta:0040900E db 69h ; i.rdаta:0040900F db 6Ch ; l.rdаta:00409010 db 6Fh ; o.rdаta:00409011 db 72h ; r.rdаta:00409012 db 21h ; !.rdаta:00409013 db Согласись, не это мы ожидали увидеть. Однако последовательное расположение символов строки «в столбик» дела не меняет. Интересен другой момент: в начале строки стоит число, показывающее количество символов в строке, — Оказывается, мало идентифицировать строку, требуется еще как минимум определить ее границы. Типы строкНаиболее популярны следующие типы строк: С‑строки, которые завершаются нулем; DOS-строки, завершаются символом С-строки С‑строки, также именуемые ASCIIZ-строками (от Zero — ноль на конце) или нуль‑терминированными, — весьма распространенный тип строк, широко использующийся в операционных системах семейств Windows и UNIX. Символ Фактическая длина ASCIIZ-строк лишь на байт длиннее исходной ASCII-строки. Несмотря на перечисленные выше достоинства, С‑строкам присущи и некоторые недостатки. Во‑первых, ASCIIZ-строка не может содержать нулевых байтов, поэтому она непригодна для обработки бинарных данных. Во‑вторых, операции копирования, сравнения и конкатенации С‑строк сопряжены со значительными накладными расходами — современным процессорам невыгодно работать с отдельными байтами, им желательно иметь дело с четверными словами. Но, увы, длина ASCIIZ-строк наперед неизвестна, и ее приходится вычислять на лету, проверяя каждый байт на символ завершения. Правда, разработчики некоторых компиляторов идут на хитрость: они завершают строку семью нулями, что позволяет работать с двойными словами, а это на порядок быстрее. Почему семью, а не четырьмя, ведь в двойном слове байтов четыре? Да, верно, четыре, но подумай, что произойдет, если последний значимый символ строки придется на первый байт двойного слова? Верно, его конец заполнят три нулевых байта, но двойное слово из‑за вмешательства первого символа уже не будет равно нулю! Вот поэтому следующему двойному слову надо предоставить еще четыре нулевых байта, тогда оно гарантированно будет равно нулю. Впрочем, семь служебных байтов на каждую строку — это уже перебор! DOS-строки В MS-DOS (и не только в ней) функция вывода строки воспринимает знак Перейти обратно к новости |