Как перехватить вызовы метода API в 64-битном процессе?
Фон
Я работаю над устаревшим продуктом, который может успешно, через инъекцию DLL, перехватить вызов произвольного метода, который процесс инженера пытается превратить в произвольную DLL. В частности, библиотека gdi32.dll. К сожалению, он не работает, когда он встроен в 64-битные приложения. Это стало горячей темой, и пришло время обновить ее функциональность. Также, к сожалению, источник лишен комментариев (типично>:-<), и, судя по всему, тот, кто написал это, был достаточно знаком с набором команд x86. Я не работал со сборкой много лет, и когда я это делал, это была сборка Motorola.
Поискав в интернете, я наткнулся на эту статью сотрудника Intel. Если бы наш исходный код не датировал эту статью около 7 лет, я бы сказал, что именно здесь наш г-н NoComments разработчик научился выполнять перехват методов API. Вот так похожа процедура. Эта статья также подытожена в хорошем PDF-файле ( Перехват системных вызовов API), который также можно найти по ссылке с вышеупомянутого веб-сайта.
проблема
Я хотел бы по-настоящему понять пример, приведенный в ссылке на веб-страницу Intel, чтобы я мог хорошо разобраться в создании решения для 64-битного сценария. Это хорошо задокументировано и мне немного легче понять. Ниже приведена выдержка из подпрограммы InterceptAPI(). Я добавил свои собственные комментарии, обозначенные "// #" (исходные комментарии отмечены стандартом "//"), где я объясняю, что я думаю, что знаю, а что нет:
BOOL InterceptAPI(HMODULE hLocalModule, const char* c_szDllName,
const char* c_szApiName, DWORD dwReplaced, DWORD dwTrampoline, int offset)
{
//# Just a foreword. One of the bigger mysteries of this routine to me is
//# this magical number 5 and the offset variable. Now I'm assuming, that
//# there are 5 bytes at the beginning of every method that are basically
//# there to set up some sort of pre-method-jump context switch, since its
//# about to leave the current method and jump to another. So I'm guessing
//# that for all scenarios, the minimum number of bytes is 5, but for some
//# there may be more than 5 bytes so that's what the "offset" variable is
//# for. In the aforementioned article, the author writes "One additional
//# complication exists, in that the sixth byte of the original code may be
//# part of the previous instruction. In that case, the function overwrites
//# part of the previous instruction and then crashes." So some method
//# starting code contains multi-byte opcodes while others don't apparently.
//# And if you don't know the instruction set well enough, I'm guessing
//# you'll just have to figure it out by trial and error.
int i;
DWORD dwOldProtect;
//# Fetching the address of the method that we want to capture and reroute
//# Example: c_szDllName="user32", c_szApiName="SelectObject"
DWORD dwAddressToIntercept = (DWORD)GetProcAddress(
GetModuleHandle((char*)c_szDllName), (char*)c_szApiName);
//# Storing address of method we are about to intercept in another variable
BYTE *pbTargetCode = (BYTE *) dwAddressToIntercept;
//# Storing address of method we are going to use to take the place of the
//# intercepted method in another variable.
BYTE *pbReplaced = (BYTE *) dwReplaced;
//# "Trampoline" appears to be a "Microsoft Detours" term, but its basically
//# a pointer so that we can get to the original "implementation" of the method
//# we are intercepting. Most of the time your replacement function will
//# want to call the original function so this is pretty important. What its
//# pointing to must already be pre allocated by the caller. The author of
//# the aforementioned article states "Prepare a dummy function that has the
//# same declaration that will be used as the trampoline. Make sure the dummy
//# function is more than 10 bytes long." I believe I'd prefer allocating this
//# memory within this function itself just to make using this InterceptAPI()
//# method easier, but this is the implementation as it stands.
BYTE *pbTrampoline = (BYTE *) dwTrampoline;
// Change the protection of the trampoline region
// so that we can overwrite the first 5 + offset bytes.
//# This is voodoo magic to me, but I'm guessing you just can't hop on the
//# stack and start changing execute instructions without ringing some
//# alarms, so this makes sure the alarms don't ring. Here we are allowing
//# permissions so we can change the bytes at the beginning of our
//# trampoline method.
VirtualProtect((void *) dwTrampoline, 5+offset, PAGE_WRITECOPY, &dwOldProtect);
//# More voodoo magic to me, but this appears to be a way to copy over extra
//# opcodes that may be needed. Some opcodes are multi byte I believe so this
//# is where you can make sure you don't miss them.
for (i=0;i<offset;i++)
*pbTrampoline++ = *pbTargetCode++;
//# Resetting the pbTargetCode pointer since it was modified it in the above
//# for loop.
pbTargetCode = (BYTE *) dwAddressToIntercept;
// Insert unconditional jump in the trampoline.
//# This is pretty understandable. 0xE9 the x86 JMP command. I looked
//# this up in Intel's documentation and it can be followed by a 16-bit
//# offset or a 32-bit offset. The 16-bit version is not supported in 64-bit
//# architecture but lets just hope they are all 32-bit and that this does
//# indeed do what it is intended in 64-bit scenarios
*pbTrampoline++ = 0xE9; // jump rel32
//# So basically here it looks like we are following up our jump command with
//# the address its supposed to jump too. This is a relative offset, that's why
//# we are subtracting pbTargetCode and pbTrampoline. Also, since JMP opcodes
//# jump relative to the address AFTER the jump address, that's why we are
//# adding 4 to pbTrampoline. Also, offset is added to pbTargetCode because we
//# advanced the pointers in the for loop above an "offset" number of bytes.
*((signed int *)(pbTrampoline)) = (pbTargetCode+offset) - (pbTrampoline + 4);
//# Not quite sure why we are changing the permissions on the trampoline function
//# again, but looks like we are making it executable here. Maybe this is the
//# last thing we have to do before it is actually callable and usable.
VirtualProtect((void *) dwTrampoline, 5+offset, PAGE_EXECUTE, &dwOldProtect);
// Overwrite the first 5 bytes of the target function
//# It seems we are now setting permissions so we can modify the original
//# intercepted routine. It is still pointing to its original code so we
//# need to eventually redirect it.
VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_WRITECOPY, &dwOldProtect);
//# This will now instruct the original method to instead jump to the next
//# address it sees on the stack.
*pbTargetCode++ = 0xE9; // jump rel32
//# this is the address we want our original intercepted method to jump to.
//# Where its jumping to will have the code of our replacement method.
//# The "+ 4" is because the jump occurs relative to the address of the
//# NEXT instruction after the 4byte address.
*((signed int *)(pbTargetCode)) = pbReplaced - (pbTargetCode +4);
//# Changing the permissions of our original intercepted routine back to execute
//# permissions so it can be called by other methods.
VirtualProtect((void *) dwAddressToIntercept, 5, PAGE_EXECUTE, &dwOldProtect);
// Flush the instruction cache to make sure
// the modified code is executed.
//# I guess this is just to make sure that if any instructions from the old
//# state of the methods we changed, have wound up in cache, that it gets
//# purged out of there before it gets used.
FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
return TRUE;
}
Я думаю, что у меня есть довольно хорошее понимание того, что происходит здесь в этом коде. Итак, вопрос на миллион долларов: что по этому поводу не работает для 64-битных процессов? Моей первой мыслью было: "О, ну, теперь адреса должны быть 8 байтов, так что это должно быть неправильно". Но я думаю, что команда JMP по-прежнему принимает только относительный 32-битный адрес, поэтому код операции должен быть действительным даже с 32-битным адресом в 64-битном процессе. Кроме этого, единственное, что я считаю, это то, что наши магические 5 байтов в начале вызова метода на самом деле являются каким-то другим магическим числом. Кто-нибудь получил лучшее понимание?
Примечание: я знаю, что есть и другие решения, такие как "Microsoft Detours" и "EasyHook". Первое слишком дорого, и я в настоящее время изучаю второе, но пока его разочаровывает. Итак, я хотел бы продолжить обсуждение этой темы конкретно. Я нахожу это интересным, а также лучшим решением моей проблемы. Поэтому, пожалуйста, не говорите: "Эй, я ничего не знаю об этом посте в частности, но попробуйте {вставьте стороннее решение здесь}".
2 ответа
Есть много вещей, которые не работают в вашем примере.
1) Вы используете VirtualProtect для PAGE_WRITECOPY, что приведет к сбою. Вы хотите использовать VirtualProtect для PAGE_EXECUTE_READWRITE.
2) Прыжок с патчем не работает, если ваш "шим" находится на расстоянии более 4 ГБ от библиотеки DLL, которую вы пытаетесь перехватить, потому что вы используете форму E9 инструкции jmp.
3) Когда вы возвращаете VirtualProtect, вы защищаете PAGE_EXECUTE, а не PAGE_EXECUTE_READ. На практике вы должны фактически использовать flProtect, который вы получили от первого VirtualProtect, чтобы вы положили его обратно.
Кстати, "магическое число 5" - это размер кода операции инструкции перехода E9, то есть E9 в виде байта, за которым следует DWORD в качестве смещения.
Батут таков, что вы можете перезвонить исходному API изнутри вашего кода (т. Е. Если вы используете shimm CreateFileW, вы не можете вызвать CreateFileW изнутри вашего shim, или вы в конечном итоге вызовете ваш shim!).
Вызов FlushInstructionCache не влияет на x86/x64. Вы должны удалить это.
Поскольку предложенный код выглядит как целевой для платформ Microsoft, я бы посоветовал вам просто использовать Detours. Используя Detours ваш батут будет работать на 32 и 64-битных системах.