Obj файлы на топчане или...

         

Битва за API


Транслятор MASM (входящий, в частности, в состав NTDDK) не выдает не единой ошибки (ну да! еще бы!!! чуть хвост не треснул, пока мы их вылавливали) и генерирует obj-файл. Наступает волнующее время линковки:

$link.exe demo_3.obj

Microsoft (R) Incremental Linker Version 5.12.8181

Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

LINK:fatal error LNK1221: a subsystem can't be inferred and must be defined

Листинг 2 попытка линковки только что ассемблированного файла

Линкер материться, что подсистема не задана и ни хвоста линковать не хочет. Ну это даже не вопрос! Подсистема задается через ключ /SUBSYSTEM за которым следует одно из следующих ключевых слов: NATIVE — для драйверов, WINDOWS — для GUI приложений, CONSOLE — для консольных приложений, WINDOWSCE — для платформы Windows CE, POSIX – э… ну… это эдакая пародия на UNIX, но все равно ни хрена не работающая.

Фактически выбирать приходится между WINDOWS и CONSOLE. Чем они отличаются? С точки зрения PE-формата, одним битом в заголовке, указывающему системному загрузчику создавать или не создавать консоль при запуске файла. Попытка линковки консольного файла как GUI заканчивается фатально (консоль не создается и весь ввод/вывод обламывается). Обратное не столь плачевно, но пустое консольное окно на фоне GUI выглядит как-то странно. Но мы-то знаем, что наше приложение — консольного типа, следовательно пишем:

$link /SUBSYSTEM:CONSOLE demo_3.obj

Microsoft (R) Incremental Linker Version 5.12.8181

Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

demo_3.obj:error LNK2001: unresolved external symbol _WriteFile

demo_3.obj:error LNK2001: unresolved external symbol _GetVersion



demo_3.obj:error LNK2001: unresolved external symbol _SetStdHandle

demo_3.obj:error LNK2001: unresolved external symbol _CloseHandle

demo_3.exe:fatal error LNK1120: 40 unresolved externals

Листинг 3 подсистема определена, но линкер не может найти API-функции


Хорошая новость — линкер заглатывает наживку и пытается переварить файл. Плохая новость — это у него не получается. А не получается потому, что он не распознает имена API функций, которых в нашем демонстрационном примере аж целых 40 штук! В переводе с английского ругательство "error LNK2001: unresolved external symbol _WriteFile" звучит как "Егор: LNK2001: неразрешимый внешний символ _WriteFile".

Сразу же возникает вопрос: откуда взялся знак прочерка и почему это WriteFile вдруг стала неразрешимым символом?! Смотрим в ассемблерный листинг. Контекстный поиск по "_WriteFile" ничего не дает! API-функция там объявлена _без_ знака прочерка:

; Segment type:      Externs

; BOOL __stdcall WriteFile(HANDLE hFile, LPCVOID lpBuffer,

; DWORD nBytesToWrite, LPDWORD lpNumberOfBytesWritten,LPOVERLAPPED lpOverlapped);

       extrn WriteFile:dword

Листинг 4 объявления API-функций в ассемблерном листинге

А теперь открываем demo_3.obj в любом hex-редакторе (например, в FAR'е по <F3> или в HIEW'е) и повторяем процедуру поиска еще раз:



Рисунок 1 просмотр obj-файла в hex-редакторе

Строка "WriteFile" встречается дважды (см. рис. 1) — один раз со знаком прочерка, другой — без. Вот этот самый прочерк линкеру и не нравится. Откуда же он берется?! А оттуда! Курим листинг 4 и убеждаемся, насколько IDA Pro коварна и хитра. Тип API-функции (stdcall) задан только в комментарии! Транслятор же комментариев не читает, и берет тип по умолчанию, которым в данном случае является Си (cdecl), предписывающий перед всеми символьными именами ставить знак прочерка, что, собственно говоря, и происходит.

Кстати говоря, комментарий неправильный. Потому что тип вызова никак не stdcall, согласно которому, транслятор должен превратить "WriteFile" в "_WriteFile@20", где 20 — размер аргументов в байтах, заданный в десятеричной нотации. Это вообще не сама функция, а двойное слово, в которое операционная система заносит эффективный адрес WriteFile при загрузке PE-файла в память. В библиотеке KERNEL32.LIB (входящей, в частности, в состав SDK) ему соответствует имя "__imp__WriteFile@20". Именно такое "титул" должен носить прототип API-функции, если мы хотим успешно слинковать obj-файл и именно это имя мы используем при вызове API-функции при программировании на "голом" ассемблере (без включаемых файлов). Вот только IDA Pro во все эти подробности не вникает, перекладывая  их на наши плечи и хвост.



Если во всем ассемблерном листинге заменить "WriteFile" на "_imp__WriteFile@20", то линкер переварит его вполне нормально и даже не отрыгнет. Нет, это не опечатка. Именно "_imp__WriteFile@20", а не "__imp__WriteFile@20". Почему?! Да потому, что второй символ прочерка транслятор добавит самостоятельно. Если же сразу указать два символа прочерка, то на выходе их образуется целых три, а это уже передоз.

Копируем "demo_3.asm" в "demo_3_test.asm", загружаем его в FAR по <F4>, давим <CTRL-F7> (replace) и заменяем "WriteFile" на "_imp__WriteFile@20", ассемблируем как и раньше, после чего повторяем попытку линковки с явным указанием имени библиотеки KERNEL32.LIB:

$link /SUBSYSTEM:CONSOLE demo_3_test.obj KERNEL32.LIB

Microsoft (R) Incremental Linker Version 5.12.8181

Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

demo_3_test.obj:error LNK2001: unresolved external symbol _GetVersion



demo_3_test.obj:error LNK2001: unresolved external symbol _SetStdHandle

demo_3_test.obj:error LNK2001: unresolved external symbol _CloseHandle

demo_3_test.exe:fatal error LNK1120: 39 unresolved externals

Листинг 5 результат линковки после замены WriteFile на _imp_WriteFile@20

Оторвать мыщъх'у хвост! Это работает! Количество ошибок уменьшилось на единицу и первое неразрешенное имя теперь не _WriteFile, а _GetVersion! Переименовав оставшиеся API-функции, мы добьемся нормальной линковки программы, но… это же сколько труда предстоит! И в каждом новом ассемблерном файле, эту тупую работу придется повторять заново, вращая хвостом вплоть до полного его залегания. А ведь мы, мыщъх'и, не хотим, чтобы наш хвост залегал, верно?

Настоящие мыщъх'и (хакеры в смысле) идут другим путем — воспользовавшись директивами "externdef" и "equ" они создают для каждой API-функции свой алисас (alias), заставляющий транслятор трактовать функцию "func" как "__imp__func@XX". В частности, для WriteFile это будет выглядеть так:



externdef imp__WriteFile@20:PTR

pr5

WriteFile equ <_imp__WriteFile@20>

Листинг 6 создание алиаса для API-функции WriteFile

Эту работу необязательно выполнять вручную и за вечер-другой можно написать утилиту, захватывающую DLL и выдающую готовый набор алиасов на выходе. Другой вариант — воспользоваться макросредствами FAR'а или редактора TSE-Pro (бывший QEDIT), позволяющих делать все что угодно и даже больше.

Самое главное, что "коллекцию" алиасов можно разместить в отдельном файле, подключаемом к ассемблерному листингу директивой "include". Создав все необходимые включаемые файлы один-единственный раз, мы можем пользоваться ими сколько угодно, причем не только для ассемблирования дизассемблерных листингов, полученных IDA Pro, но и в своих собственных ассемблерных программах.

Параметр "prN", идущий после "PTR", показывает сколько аргументов принимает функция и численно равен их размеру (число после символа "@") деленному на размер двойного слова, составляющему, как известно, 4 байта. То есть, в случае с WriteFile мы получаем: 20/4 = 5. Так же обратите внимание на символы прочерка. В первой строке "imp__func@XX" пишется вообще без знаков прочерка, во второй — с одним прочерком. Любые другие варианты не работают. Так что не надо косячить!



Рисунок 2 никакой хак не обходится без черной магии


Содержание раздела