Экспорт функции из ехе-файла

Ответить

Код подтверждения
Введите код в точности так, как вы его видите. Регистр символов не имеет значения.

BBCode ВКЛЮЧЁН
[img] ВКЛЮЧЁН
[url] ВКЛЮЧЁН
Смайлики ОТКЛЮЧЕНЫ

Обзор темы
   

Развернуть Обзор темы: Экспорт функции из ехе-файла

Re: Экспорт функции из ехе-файла

Koduc » 27 дек 2007, 16:48

Восстановление ссылок

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

call ds:[адрес]

При этом при загрузке .dll адрес указывает натурально на таблицу импорта, а при загрузке .exe - в неинициализированную область памяти. Естественно, что при запуске .exe как обычного процесса те-же самые адреса показывают на IAT.

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

По умолчанию в .exe такой секции нет. Однако попросить линкер сделать ее достаточно просто - всего лишь указать ключ /FIXED:NO (в .NET параметр называется Fixed Base Address

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

Для начала получим указатель на эту самую таблицу перемещений:

Код: Выделить всё

IMAGE_DOS_HEADER	*pdosh = (IMAGE_DOS_HEADER*)h;
IMAGE_NT_HEADERS	*pnth = (IMAGE_NT_HEADERS*)((byte*)h+pdosh->e_lfanew);

unsigned	delta_address = (unsigned)h - (unsigned)pnth->OptionalHeader.ImageBase;

IMAGE_DATA_DIRECTORY	*preloc_dir = pnth->OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_BASERELOC;
if( !preloc_dir->VirtualAddress)return false;
После этого достаточно пройти по всей таблице и на указанных страницах выполнить действия в соответствии с записанным кодом:

Код: Выделить всё

IMAGE_BASE_RELOCATION	*preloc = (IMAGE_BASE_RELOCATION*)((byte*)h+preloc_dir->VirtualAddress);
	
for( byte *ptr = (byte*)preloc; 
	ptr < (byte*)h+preloc_dir->VirtualAddress+preloc_dir->Size && preloc->SizeOfBlock;
	ptr += preloc->SizeOfBlock )
{
	preloc = (IMAGE_BASE_RELOCATION*)ptr;

	byte		*reloc_base = (byte*)h+preloc->VirtualAddress;
	wchar_t		*reloc_data = (wchar_t*)(preloc+1);
	int			count = (preloc->SizeOfBlock-sizeof( *preloc ))/sizeof( *reloc_data );

	DWORD	old_protect;
	if( !VirtualProtect( reloc_base, 2*4096, PAGE_EXECUTE_READWRITE, &old_protect ) )
		return false;

	for( int i = 0; i < count; i++ )
	{
		int	reloc_offset = reloc_data[i] & 0xFFF;
		int	reloc_type = reloc_data[i] >> 12;

		if( reloc_type == IMAGE_REL_BASED_ABSOLUTE )
			continue;
		else if( reloc_type == IMAGE_REL_BASED_HIGH )
			*(wchar_t*)(reloc_base+reloc_offset) += HIWORD( delta_address );
		else if( reloc_type == IMAGE_REL_BASED_LOW )
			*(wchar_t*)(reloc_base+reloc_offset) += LOWORD( delta_address );
		else if( reloc_type == IMAGE_REL_BASED_HIGHLOW )
			*(unsigned*)(reloc_base+reloc_offset) += delta_address; 
		else
		{
//				_assert( false );
			return false;
		}
	}
	if( !VirtualProtect( reloc_base, 4096, old_protect, &old_protect ) )
		return false;
}
Для верности будем грузить библиотеку как файл данных. Этим мы обезапасим себя от ситуации, когда какая-нибудь из операционок решит по своему трактовать .exe с секцией перемещений, загружаемому по LoadLibrary.

Замечу также, что для окончательного счастья в экспортированной функции загруженного .exe нужно проинициализировать CRT - в противном случае не будут работать милые многим OO программистам new и delete. Для их работы достаточно вызвать _heap_init(1);
Копирайт: Андрей Ямов, 28.04.2004

Re: Экспорт функции из ехе-файла

Koduc » 27 дек 2007, 16:47

Окончательного дзена я достиг покурив текст с какой-то из страниц из кэша гугля (заранее извиняюсь за возможные кривости с форматированием и тэгами кода):

Речь в этой статье пойдет о том, как можно загрузить исполняемый файл в адресное пространство процесса и вызвавать экспортированную функцию, не создавая новый процесс. "Зачем нужен такой изврат" возможно возникнет вопрос у досуживо читателя. Ответ на него крайне прост.

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

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

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

Итак начнем. Нам нужно научиться выполнять такой код в случае, если psz_dll содержит имя .exe:

Код: Выделить всё

bool call_dll( const char *psz_dll, const char *psz_func ) 
{ 
	HMODULE hdll = ::LoadLibrary( psz_dll );
	void (__cdecl *proc)(HINSTANCE h ) = 0;

	(FARPROC&)proc = ::GetProcAddress( hdll, psz_func );
	proc( hdll );

	::FreeLibrary( hdll );
}
На наше счастье функция LoadLibrary и GetProcAddress срабатывают вполне успешно и мы получаем нормальный адрес на функцию, описанную в .exe как экспортированную. Однако при попытке выполнения сколько-нибудь содержательного кода в рамках импортируемой функции система выдает ошибку выполнения.

Связано это со следующими проблемами:
Некорректна ссылка на таблицу импорта
Сама таблица импорта не инициализированна
Не инициализированна библиотека CRT

Восстанавливаем таблицу импорта

В случае, если наша программа собрана компоновщиком link из Visual C++ с опциями по умолчанию, восстановить таблицу импорта не составлит труда. Для начала нужно получить адрес импорта. Делается это и вовсе примитивно.

1. Вспомним, что HMODULE, возвращаемый нам LoadLibrary, в Win32 всего лишь указатель в адресном пространстве процесса. А указывает он на структуру IMAGE_DOS_HEADER:
IMAGE_DOS_HEADER *p = (IMAGE_DOS_HEADER*)hinst;
Естественно, хороший программист проверит значение поля pdosh->e_magic - оно должно быть равно знакомому нам с далеких времен ДОС-а "MZ"

Далее, получаем указатель на заголовок PE.

IMAGE_NT_HEADERS *pnth = (IMAGE_NT_HEADERS*)((byte*)h+pdosh->e_lfanew);

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

IMAGE_DATA_DIRECTORY *pimport_directory = pnth->OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_IMPORT;
if( !pimport_directory->VirtualAddress)return true;

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

Код: Выделить всё

IMAGE_IMPORT_DESCRIPTOR	*pimport_descr = 
	(IMAGE_IMPORT_DESCRIPTOR*)((byte*)h+pimport_directory->VirtualAddress);
for( ; pimport_descr->Characteristics != 0; pimport_descr++ )
{
	char	*psz_dll_name = (char*)((byte*)h+pimport_descr->Name);

	HMODULE	hmodule = ::LoadLibrary( psz_dll_name );

	if( !hmodule )
		return false;

	IMAGE_THUNK_DATA	*pimport_list = (IMAGE_THUNK_DATA*)((byte*)h+pimport_descr->OriginalFirstThunk);
	IMAGE_THUNK_DATA	*pthunk_list = (IMAGE_THUNK_DATA*)((byte*)h+pimport_descr->FirstThunk);
	for( ; pimport_list->u1.Function; pimport_list++, pthunk_list++ )
	{
		IMAGE_IMPORT_BY_NAME	*pimp_name = (IMAGE_IMPORT_BY_NAME*)((byte*)h+pimport_list->u1.AddressOfData);
		FARPROC	proc = ::GetProcAddress( hmodule, (char*)pimp_name->Name );
	
		if( !proc )
			return false;

		pthunk_list->u1.Function = (unsigned)proc;
	}
}

Re: Экспорт функции из ехе-файла

Romeo » 27 дек 2007, 14:50

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

Re: Экспорт функции из ехе-файла

Koduc » 27 дек 2007, 14:30

В общем сделал. Если в кратце, то:
1) делаем ехе с релоками (/FIXED:NO)
2) подгружаем этот ехе в память через LoadLibrary, разбираем все заголовки
2.1) восстанавливаем таблицу импорта
2.2) идем до секции релоков, релочим все адреса, высчитав перед этим дельта-смещение (адрес полученный от LoadLibrary минус адрес, прописанный в загруженном файле).
3) По хендлу от LoadLibrary получаем через GetProcAddress адрес на интересующую нас функцию и запускаем её.

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

Всем спасибо ;)

Re: Экспорт функции из ехе-файла

Koduc » 27 дек 2007, 08:51

CFF explorer'ом поставил этот флаг. Результат - файл даже по LoadLibrary грузится отказывается, сишный дебагер ругается на невалидное win32 приложение, без дебагера LoadLibrary просто NULL возвращает Грешил на саму програмулину, мало ли думаю неправильно хидеры ставит.. Поигрался с флагами, нет, вполне адекватная программа..
"Ничего не понимаю.." и не во флаге счастье.. Видимо придется в памяти полную трепанацию ехе файла проводить с поправкой смещений..

Re: Экспорт функции из ехе-файла

BBB » 26 дек 2007, 14:07

Koduc,
вот тут еще что-то похожее обсуждают:
http://forum.sources.ru/index.php?showtopic=200132

Даже есть исходник простейшего загрузчика

Я сильно не вникал, но вот любопытное сообщение #13 (может, и в смом деле возможно "преобразовать" EXE в DLL):

Что бы ЕХЕ превратить в ДЛЛ - единственное, что надо сделать - заменить в
FileHeader поле Characteristics на соответствуещее длл. И добавить релоки, но тут уже смотря что нужно будет делать и какой EXE-файл...

Re: Экспорт функции из ехе-файла

Koduc » 26 дек 2007, 13:49

Я думал меня одного такие мысли посещают ;) нет, не работает ;(
00A81000 push 0
00A81002 push offset _heap_term+0E0h (00403010)
00A81007 push offset _heap_term+0E0h (00403010)
00A8100C push 0
00A8100E call dword ptr [_setenvp+24h (00402044)]

вот вызов экспортируемой функции.. видно что сылки ведут далеко за пределы загруженных данных.. Но сама функция корректна (вызов messagebox'a с 4 параметрами) ;)

Re: Экспорт функции из ехе-файла

BBB » 26 дек 2007, 13:28

Romeo писал(а):Но использовать __declspec(dllimport) во время импорта все же нужно, я на этом настаиваю.
Так ведь у него же описана НЕ импортируемая функция, тип функции (typedef). Че-то, никогда в этом случае (в описании типа функции), вроде никаких директив импорта не ставилось. Я так понимаю, dllimport - это вообще больше для линкера, а не для компилятора.

А экспортировать ф-ии можно вообще без __declspec(dllexport), если описать их как экспортируемые в *.DEF-файле проекта.

-------------------------------

Koduc,
тупая кондовая мысль. Скорее, это бред и вряд ли поожет, но вдруг. А если у твоего файла поменять расширение с EXE на DLL?

Re: Экспорт функции из ехе-файла

Koduc » 26 дек 2007, 13:01

Доковырялся дизасемблером до причин вылета. При LoadLibrary ехе проецируется в память (точнее его ImageBase как я понимаю) по адресу, как привило большему или меньшему чем 0х0040000 (стандартное для ехе файлов). Точка входа в экспортируемую функцию определяется корректно. Но уже после входа в функцию имеем ссылки на данные и на функции, как если бы этот ехе загрузился по адресу 0х0040000.
Как можно с наименьшими трудовыми и временными затратами эти смещения исправить? Секцию .reloc в файл включаю, но для ехе это видимо не играет роли..

Re: Экспорт функции из ехе-файла

Romeo » 26 дек 2007, 11:19

На счёт смены конвеншенов вызова функции при использовании __declspec(dllexport) я всё-таки погорячился. В этом BBB прав. Но использовать __declspec(dllimport) во время импорта все же нужно, я на этом настаиваю. Это парные спецификаторы и они обязаны использоваться парно, то есть либо и там и там, либо ни там, ни там. Самый главный аргумент - это то, что спецификатор __declspec не является частью ANSI стандарта и его контент может быть всегда изменён когда угодно и как угодно уважаемым Microsoft.

Вернуться к началу