Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

articles:c-tricks-19h [2017/09/05 02:55] (текущий)
Строка 1: Строка 1:
 +====== c-tricks-19h ======
 +<​sub>​{{c-tricks-19h.odt|Original file}}</​sub>​
 +
 +====== сишные трюки\\ (19h выпуск) ======
 +
 +крис касперски ака мыщъх, a.k.a. souriz, a.k.a. nezumi, no-email
 +
 +**очередная порция трюков от мыщъх****'​****а — загрузка ****dll ****с турбо-наддувом. прямого отношения к си не имеет, однако,​ работает (не без изменений,​ конечно) как под ****windows****,​ так и ****Linux/​BSD****,​ ускоряя загрузку динамических библиотек в десятки и даже тысячи раз, а как дополнительный бонус — затрудняет дизассемблирование программы и препятствует снятию дампа, что очень даже хорошо!**
 +
 +===== #1 – генерация таблицы вызовов на стадии компиляции =====
 +
 +Загрузка динамических библиотек занимает значительное время, особенно при большом количестве импортируемых функций. И хотя Microsoft предлагает кучу продвинутых типов импорта (bound import, delay import) положение они не исправляют,​ а при динамическом импорте,​ когда определение адресов функций определяется посредством GetProcAddress (один вызов на каждую функцию),​ производительность вообще падает ниже плинтуса. В Linux/BSD ситуация обстоит не так плачевно,​ но все равно издержки на загрузку динамических библиотек весьма значительны,​ потому,​ оптимизацией приходится заниматься самостоятельно.
 +
 +Идея состоит в переносе вызовов GetProcAddress из реал-тайма на стадию компиляции программы,​ при которой время их выполнения уже не так существенно (в самом деле, какая разница сколько собирается программа — 60 или 90 минут, главное,​ — чтобы она работала как фотонный звездолет).
 +
 +Последовательность действий при этом такова (разумеется,​ здесь дается лишь общая схема без углубления в детали):​
 +
 +  - компилируем DLL как обычно;​
 +  - пишем вспомогательную утилиту,​ загружающую DLL вызовом h = LoadLibrary("​dll_name.dll"​) для определения ее базового адреса,​ зная который нетрудно вычислить RVA-адреса всех экспортируемых функций:​ RVA_Fn = (DWORD)GetProcAddress("​Fn"​) - (DWORD)h;​ остается только сгенерировать заголовочный .h файл, поместив туда прототипы функций:​ typedef int (*$Fn)(int);​ $Fn Fn;​вместе c процедурой их инициализации:​init_name_dll(HANDLE h) {Fn = ($Fn) ((DWORD)Fn + (DWORD)h );}. Конечно,​ без хака тут не обошлось и наглое преобразование указателей в DWORD при переносе на другие платформы ни к чему хорошему не приведет,​ поэтому,​ в коммерческих продуктах,​ придется чуть-чуть усовершенствовать наш генератор,​ поставляя вместо DWORD целочисленный тип с размером,​ равным размеру указателю на функцию,​ что делается либо вручную с учетом разрядности конкретной платформы (например,​ x86-64), либо цепочкой #if/#else в препроцессоре,​ но это уже детали;​
 +  - подключаем сгенерированный заголовочный файл к базовой программе,​ загружаем динамическую библиотеку через h = LoadLibrary("​dll_name.dll"​) и передаем полученный базовый адрес процедуре инициации init_dll_name(h);​
 +  - экспортируемые функции вызываем как обычно,​ например,​ a=Fn(b); (законченный пример реализации можно найти в файлах trick-01-*, собранных в архив tricks-19h.7z,​ прилагаемый к журналу);​
 +За счет чего достигается преимущество в скорости?​ На первый взгляд,​ процедура инициации должна "​съесть"​ весь выигрыш. Однако,​ функция GetProcAddressвыполняется _намного_ медленнее,​ чем сложение двух переменных ((DWORD)Fn + (DWORD)h) в процедуре инициализации загружаемой динамической библиотеки. Тоже самое относится и к статической компоновке,​ при которой для каждой импортируемой функции осуществляется "​полнотекстовой"​ поиск в таблице экспорта.
 +
 +Накладных расходов на вызов функции у нас нет и они вызываются так же, как и функции,​ импортируемые обычным образом (CALL DS:​[func_name]),​ но если с обычным импортом любой дизассемблер справляется на ура, то в нашем случае func_name представляет RVA адрес, совершенно ничего не говорящий ни дизассемблеру,​ ни хакеру (см. листинг 1) и чтобы определить что именно за функция вызывается,​ необходимо прогнать программу под отладчиком или снять с нее дамп (а помешать отладчику намного проще, чем дизассемблеру!).
 +
 +0401034push0
 +
 +0401036pushoffset aHello_1
 +
 +040103Bpushoffset aHello_0
 +
 +0401040push0
 +
 +0401042calloff_405030;​ вызов MessageBoxA
 +
 +
 +
 +0405030off_405030 dd 3D81h; <- ничего не говорящий RVA-адрес
 +
 +Листинг 1 IDA Pro не смогла распознать "​хитрый"​ импорт API-функции MessageBoxA
 +
 +Единственный недостаток предложенного метода в том, что при изменении динамической библиотеки,​ целевое приложение придется перекомпилировать заново,​ что не есть гуд и нужно что-то делать. А что мы, собственно,​ можем сделать?​!
 +
 +===== #2 – универсальный загрузчик динамических библиотек =====
 +
 +Для чужих библиотек мы, действительно,​ ничего не можем сделать,​ поэтому дальше будем говорить только о своих собственных. Совсем несложно расположить в DLL специальный массив,​ хранящий указатели на все "​внешние"​ функции в строго обозначенном порядке,​ а затем экспортировать его, попутно сократив при этом размер таблицы экспорта,​ поскольку,​ указатель на массив окажется _единственным_ экспортируемым элементом.
 +
 +При изменении версии DLL адреса функций могут меняться,​ как и адрес массива указателей на них, но это уже не развалит нашу программу,​ поскольку,​ адрес массива прописан в таблице экспорта,​ а указатели на функции — в нем самом (см. листинг 2).
 +
 +int done;
 +
 +__declspec(dllexport) DWORD f_table[2];
 +
 +BOOL WINAPI DllMain(HINSTANCE hs, DWORD reason,​LPVOID lpvRes)
 +
 +{
 +
 +if (done) return 1; done = 1;
 +
 +f_table[0] = (DWORD)foo - (DWORD) hs;
 +
 +f_table[1] = (DWORD)bar - (DWORD) hs;
 +
 +
 +
 +return 1;
 +
 +}
 +
 +Листинг 2 "​рукотворная"​ таблица экспорта. намного лучше, чем у Microsoft
 +
 +Постойте,​ но ведь… при этом мы фактически создадим свой собственный вариант таблицы экспорта. Чем он будет лучше уже существующего в реализации от Microsoft?! А тем, что в _нашем_ массиве поиск экспортируемых функций _не_ осуществляется. Вместо этого выполняется обращение по предопределенным индексам. Оверхид на вызов функций ничуть не увеличивается,​ защищенность программы так же остается на высоте (дизассемблер показывает ничего незначащие RVA-адреса),​ а единственным побочным эффектом становится невозможность удаления из массива уже существующих индексов (иначе нарушится их последовательность!). Добавлять новые функции (к концу массива) — можно, а вот удалять старые — нет. То есть, функции из DLL удалять,​ конечно,​ можно, но вот указатели из массива все-таки придется оставить,​ прописав там 0 (типа нет такой функции) или воткнув указатель на функцию-пустышку,​ ничего не делающую,​ а только возвращающую код ошибки.
 +
 +Готовый пример содержится в файлах trick-03-*, собранных в архив tricks-19h.7z,​ прилагаемый к журналу.
 +
 +===== #3 – реальный хардкод физических адресов =====
 +
 +Предыдущий вариант можно значительно улучшить,​ отказавшись от процедуры инициализации фактических адресов функций,​ складывающей RVA-адрес _каждой_ функции с базовым адресом загрузки динамической библиотеки:​ foo = ($foo) ((DWORD)foo +  (DWORD)h); И ведь все это происходит в ран-тайме! Естественно,​ чем больше мы импортируем функций,​ тем дольше длиться загрузка.
 +
 +К счастью,​ задел для оптимизации есть и какой задел!!! Во-первых,​ сначала динамическая библиотека инициализирует массив функций,​ вычитая (в ран-тайме) базовый адрес загрузки модуля из адреса _каждой_ функции,​ чтобы получить RVA-адрес,​ который затем приходится преобразовывать в фактический адрес функции складывая (опять-таки в ран-тайме) RVA с базовым адресом загрузки модуля. Зачем нам делать двойную работу?​! А затем, что базовый адрес, прописанный в заголовке DLL, является не более чем рекомендацией и системный загрузчик может расположить библиотеку где-нибудь в другом месте, особенно,​ если выясниться,​ что диапазон адресов,​ на которых она претендует,​ уже кем-то занят.
 +
 +Однако,​ учитывая,​ что _все_ нормальные динамические библиотеки имеют таблицу перемещаемых элементов (фиксапы),​ благодаря чему могут быть перемещены по любому свободному адресу,​ то для самих себя мы можем сделать исключение:​ убив таблицу перемещаемых элементов у исполняемого файла и DLL (ключ /FIXED линкера MS Link), заставим систему грузить их по требуемому адресу,​ а не куда хвост на душу положит.
 +
 +Главное,​ выбрать адреса загрузки так, что бы не зацепить библиотеки NTDLL.DLL и KERNEL32.DLL,​ поскольку они проецируются на адресное пространство процесса еще до его создания и потому становятся неперемещаемыми. Во всех системах вплоть до Вислы, эта парочка прижата к верхней границе пользовательского адресного пространства (2 Гбайта по умолчанию),​ так что волноваться не приходится. Но вот Висла с ее рандомизацией адресного пространства выбирает случайные адреса загрузки для всех системных библиотек,​ включая NTDLL.DLL/​KEREL32.DLL. Как быть?! Поковырявшись в ядре, мыщъх выяснил,​ что они ни при каких обстоятельствах не могут опускаться ниже отметки в 32 Мбайта,​ так что оперативный простор для загрузки своих DLL у нас есть, а остальные — пускай подвинутся.
 +
 +Скорость загрузки при этом возрастает во много раз, программный код существенно упрощается (см. файлы trick-03-*),​ но это ерунда. А вот если расположить динамическую библиотеку _перед_ исполняемым файлом (т.е. в более младших адресах) это серьезно озадачит дамперы процессов и все полученные дампы для непосредственного дизассемблирования окажутся _непригодными_
 +
 +Но оптимизация на этом не заканчивается,​ а только начинается…
 +
 +