Таблица экспорта в РЕ файле

Jul 20, 2022 21:47

   Как говорится, лучше один раз потрогать, чем сто раз увидеть, поэтому для разбора таблицы экспорта РЕ файла идеально воспользоваться отладчиком. Возьмем WinDBG, он неудобный, но равных по возможностях ему нет. Если бы Microsoft умели делать юзабилити..однако, что есть то есть:
Запускаем любое 64 битное приложение, аттачимся, пишем

lm m kernel32

Маппим dos header, затем nt по стартовому адресу:

dt _IMAGE_DOS_HEADER output_from_prev_command_here
..конвертируем e_lfanew в 16ричную систему, прибавляем в базовому адресу
dt -r _IMAGE_NT_HEADERS64 тут_адрес

Массив директорий - в новой версии windbg (uwp) можно просто кликнуть и выведет такой код, в старых см. вариант два
в первом случае offset это база + 0х88 (см. ниже ) + e_lfanew
dx -r1 (*((ntdll!_IMAGE_DATA_DIRECTORY (*)[16])offset))
dt -a16c _IMAGE_DATA_DIRECTORY base_address+88 (offset 0x70 в OptionalHeader , который 0x18 в NT headers)

Нам нужен первый (нулевой) массив, т.е. экспорт. В старой виндбг это нужно выводить через dd адрес, будет массив двордов, подробнее см. структуру в хидерах
dx -r1 (*((ntdll!_IMAGE_DATA_DIRECTORY *)offset))

Важно! виндбг может не знать всех структур, поэтому пишем
.symopt- 100
но тогда он может подвисать, если ввести какую-то ерунду (т.к. будет искать везде).

С предыдущего пункта берем VA (virtual adress, конвертировать в RVA не надо, мы в памяти), прибавляем к базе
dt _IMAGE_EXPORT_DIRECTORY база + VA

Смотрим поле например AddressOfNames, берем оттуда значение (опять же VA)
dd база + VA AddressOfNames

Покажет массив двордов, которые суть виртуальные адреса имен (названий) функций. Выведем для примера одно
da база + первый дворд с массива

Если все верно, покажет ascii имя функции.

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

Программно это будет несколько проще

LPBYTE lpBase = (LPBYTE)GetModuleHandleW(L"kernel32.dll"); //получаем базовый адрес
if(!lpBase) return -1;

PIMAGE_DOS_HEADER p_dos = (PIMAGE_DOS_HEADER)lpBase; //структуры РЕ файла, для примера дано 64 бит, для 32 бит надо менять
PIMAGE_NT_HEADERS64 p_nt = (PIMAGE_NT_HEADERS64)((LPBYTE)lpBase + p_dos->e_lfanew);
PIMAGE_EXPORT_DIRECTORY p_exp = (PIMAGE_EXPORT_DIRECTORY)(lpBase +\
p_nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

//массив имен и функций
DWORD * lpNames = (DWORD*) (lpBase+p_exp->AddressOfNames);
DWORD * functions = (DWORD*)(lpBase+p_exp->AddressOfFunctions);

for(UINT i = 0; i < 10; i++) //для упрощения, вывести первые 10
{
printf("%s",(lpBase + lpNames[i]));
LPBYTE fn = lpBase+functions[i];

//если адрес функции находится внутри таблицы экспорта, то это редирект (в формате: имя апи - нуллбайт - имя длл-точка-имя апи), иначе валидный адрес
if(fn >= (LPBYTE)p_exp && fn < ((LPBYTE)p_exp + p_nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size))
//REDIRECT
(lpBase + lpNames[i])); //либо строка либо адрес
}

Код вроде бы работает, выводит экспорт, но это идеальные условия. В реальном проекте все может быть несколько сложнее. Например, возьмем kerberos.dll и сравним выхлоп нашей программы с любым РЕ вьювером. И чудо - оффсеты функций совершенно другие! Все дело в запутанной структуре экспорта. В нем есть три таблицы - имена, ординалы имен (не путать с просто ординалами, это другое), и собственно функции. В таблице ординалов имен данные могут идти по порядку (как в kernel32), а могут - вперемешку, как в вышеупомянутом kerberos, где таблица имеет вида рендома (5,7,2,6,0..). Поэтому, алгоритм таков:

1. Получить смещение функции, проверить его на null и форвардинг.
2. Если ок, то перебираем в цикле массив ординалов, ища в нем запись с таким же числом, как порядковый номер функции . Т.е. если она нулевая, то ищем число 0.
3. Допустим, число 0 найдено под номером 5. Отлично! 5ая (точнее, 6ая, если по порядку, ибо отсчет с 0) запись в массиве имен и будет именем искомой функции.
p.s. если идет вызов по ординалу, там очень - перебор всех функций по очереди, и к текущему счетчику прибавляем цифру из Base таблицы экспорта - это и будет номером искомого ординала.

Лучше всего объяснить кодом (начало см. выше):

for(UINT i = 0; i < p_exp->NumberOfFunctions; i++)
{
LPBYTE fn = lpBase+functions[i]; //адрес функции
LPBYTE name = NULL;

if (fn == lpBase) //на диске проверка на NULL, а в памяти это будет начало образа
continue;
else if(fn >= (LPBYTE)p_exp && fn < ((LPBYTE)p_exp + p_nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size))
printf("---REDIRECT--- %s \n",(lpBase + lpNames[i]));
else
{
for (UINT j=0; j < p_exp->NumberOfNames; j++ )
{
if ( ordinals[j] == LOWORD(i) )
{
name = (lpBase + lpNames[j]);
break;
}
}
printf("fn offset %x %s , ordinal %d\n", fn,name,i+p_exp->Base);
}
}

заметки, windows, PE формат

Previous post Next post
Up