假设有一个DLL Error in lapply(labels, as.character) :
argument "labels" is missing, with no default
,其中有一个已知的入口点A.DLL
,我在某种程度上用我自己的DLL DoStuff
连接,以便系统调用我的fakeA.dll
1}}而不是。如何编写这样的函数,以便它可以在不知道函数参数的情况下调用钩子DLL(DoStuff
)的相同入口点?即我在A.DLL
中的功能看起来像
fakeA.DLL
我目前的想法是参数在堆栈上,所以我猜测我必须有一个足够大的堆栈变量(例如一个大屁股LONG DoStuff(
// don't know what to put here
)
{
FARPROC pfnHooked;
HINSTANCE hHooked;
LONG lRet;
// get hooked library and desired function
hHooked = LoadLibrary("A.DLL");
pfnHooked = GetProcAddress(hHooked, "DoStuff");
// how do I call the desired function without knowing the parameters?
lRet = pfnHooked( ??? );
return lRet;
}
)来捕获任何参数和然后将它传递给struct
?即。
pfnHooked
这会有用吗?如果是这样,有更好的方法吗?
更新:经过一些基本的(和非结论性的)测试后传递任意块作为参数DOES工作(这并不奇怪,因为程序只是从堆栈中读取它需要的东西)。但是,收集返回值更难,因为它太大会导致访问冲突。将任意返回大小设置为8个字节(或者对于x86来说可能是4个)可能是大多数情况下的解决方案(包括无效返回),但这仍然是猜测。如果我有一些方法可以知道DLL中的返回类型(不一定是在运行时),那将是宏伟的。
答案 0 :(得分:5)
这应该是一个注释,但是元回答是肯定的,你可以在不知道调用约定和参数的情况下在x64 / x86平台上挂钩函数。它可以纯粹用C语言完成吗?不,它还需要对各种调用约定和汇编编程有很多理解。挂钩框架将有一些用Assembly编写的位。
大多数挂钩框架通过创建一个蹦床来固有地做到这一点,该蹦床将执行流程从被调用函数的前导码重定向到存根代码,该存根代码通常独立于它所挂钩的功能。在用户模式下,您可以保证堆栈始终存在,这样您就可以在同一堆栈上推送自己的局部变量,只要您可以弹出它们并将堆栈恢复到其原始状态。
您真的不需要将现有参数复制到您自己的堆栈变量中。您可以只检查堆栈,在您尝试任何操作之前,一定要阅读有关调用约定以及如何在汇编中为各种类型的调用构建堆栈的方法。
答案 1 :(得分:2)
是的,这可能是通用挂钩100%正确 - 一个常见的多个函数具有不同的参数计数和调用约定。对于 x86 / x64(amd64)平台。
但是为了这个需要使用一些 asm 存根 - 当然它对于 x86 / x64 会有所不同 - 但它会非常小 - 只有几行代码 - 2个小存根程序 - 一个用于过滤器预呼叫,一个用于呼叫后。但是大多数代码实现(95%以上)都是独立于平台的,并且在 c ++ 中(当然这可以在 c 上进行,但比较 c ++ - c 源代码将更大,更难以实现)
在我的解决方案中,需要为每个挂钩api分配小的可执行代码块(每个挂钩的api一个块)。在这个块中 - 存储函数名称,原始地址(或预调用之后的传输控制 - 这取决于挂钩方法)和一个相对调用指令到常见的asm预调用存根。这个调用的神奇之处不仅在于它将控制转移到公共存根,而且堆栈中的返回地址将指向块本身(好吧,有一些偏移,但是如果我们将使用 c ++ < / strong>和继承 - 它将正好指向一些基类,我们从中派生可执行块类。作为公共预备存根的结果,我们将获得信息 - 我们在此处挂钩,然后将此信息传递给 c ++ 公共处理程序。
一个注意事项,因为在 x64 中相对调用只能在范围[rip-0x80000000, rip+0x7fffffff]
中需要声明(分配)此代码块在我们的 PE 中单独的 bss 部分并将此部分标记为 RWE 。我们不能简单地使用 VirtualAlloc 来分配存储空间,因为返回的地址可能离我们的常见预备存根存在太远。
共同 asm 预先存根存根代码必须保存 x64 的 rcx,rdx,r8,r9 寄存器(这是绝对必需的)和< em> ecx,edx 为x86注册。如果函数使用 __ fastcall 调用约定,则需要这种情况。但是例如windows api几乎没有使用 __ fastcall - 只有几个 __ fastcall 函数存在于成千上万的win api中(为了确保这一点并找到了这个函数 - 转到 LIB < / em>文件夹并搜索__imp_@
字符串(这是 __ fastcall 公共前缀)然后调用 c ++ 公共处理程序,它必须返回原始函数的地址(到其中转移控制)到存根。存根恢复 rcx,rdx,r8,r9 (或 ecx,edx )寄存器和跳转(但不是< strong>致电!)到这个地址
如果我们只想预先调用过滤器,这就是我们所需要的。但是在大多数情况下需要filter(hook)和post-call - 用于查看/修改函数返回值和 out 参数。这也是可能的,但需要更多的编码。
for hook post-call显然我们必须替换hooked api的返回地址。但是我们必须改变什么地址?哪里保存原始退货地址?为此,我们不能使用全局变量。甚至不能使用本地线程(__declspec( thread )
或thread_local
)因为调用可以是reqursive。不能使用volatile寄存器(因为它在api调用期间发生了变化)并且不能使用非易失性寄存器 - 因为在这种情况下我们将保存它,以便稍后恢复 - 但是有一些问题 - 在哪里?
这里只有一个(和很好的)解决方案 - 分配一小块可执行内存( RWE ),其中包含一条相对调用指令给普通的调用后asm存根。和一些数据 - 保存原始返回地址,函数参数(用于检查后处理程序中的参数)和函数名称
再次,x64的一些发行者 - 这个块必须离普通的post stub(+/- 2GB)不太远 - 所以最好还在单独的 .bss 部分中分配这个存根(带有预调用存根)。有多少需要这个ret-stubs?每个api呼叫一个(如果我们想要控制呼叫)。所以api不会在任何时候激活。通常说256个预先分配的块 - 绰绰有余。即使我们失败也在预先调用中分配了这个块 - 我们不会在调用后控制它,但不会崩溃。并且我们不能为所有人提供api想要控制后呼叫但仅限于某些人。
非常快速和互锁的alloc / free这个块 - 需要构建堆栈语义。通过互锁弹出分配并通过互锁推送自由分配。并且在开始时预先初始化(调用指令)这个块(同时将它全部推入堆栈,以便每次在预调用时不重新初始化它)
asm 中常见的调用后存根非常简单 - 这里我们不需要保存任何寄存器。我们只需用块地址调用 c ++ post处理程序(我们从堆栈中弹出它 - 调用指令从块开始的结果)并使用原始返回值( rax 或 eax )。严格说 - api函数可以返回对 rax + rdx 或 eax + edx 但是99.9%+的windows api在单个寄存器中返回值,我假设我们只会挂钩这个api。但是如果想要,也很少调整代码来处理这个问题(在大多数情况下这不需要) c ++ 帖子调用处理程序恢复原始返回地址(通过使用_AddressOfReturnAddress()
),可以记录调用和/或修改输出参数,最后返回到api的原始调用者。我们的处理程序返回什么 - 这将是api调用的最终返回值。通常我们桅杆返回原始值。
c ++ 代码
#if 0
#define __ASM_FUNCTION __pragma(message(__FUNCDNAME__" proc\r\n" __FUNCDNAME__ " endp"))
#define _ASM_FUNCTION {__ASM_FUNCTION;}
#define ASM_FUNCTION {__ASM_FUNCTION;return 0;}
#define CPP_FUNCTION __pragma(message("extern " __FUNCDNAME__ " : PROC ; " __FUNCTION__))
#else
#define _ASM_FUNCTION
#define ASM_FUNCTION
#define CPP_FUNCTION
#endif
class CODE_STUB
{
#ifdef _WIN64
PVOID pad;
#endif
union
{
DWORD code;
struct
{
BYTE cc[3];
BYTE call;
};
};
int offset;
public:
void Init(PVOID stub)
{
// int3; int3; int3; call stub
code = 0xe8cccccc;
offset = RtlPointerToOffset(&offset + 1, stub);
C_ASSERT(sizeof(CODE_STUB) == RTL_SIZEOF_THROUGH_FIELD(CODE_STUB, offset));
}
PVOID Function()
{
return &call;
}
// implemented in .asm
static void __cdecl retstub() _ASM_FUNCTION;
static void __cdecl callstub() _ASM_FUNCTION;
};
struct FUNC_INFO
{
PVOID OriginalFunc;
PCSTR Name;
void* __fastcall OnCall(void** stack);
};
struct CALL_FUNC : CODE_STUB, FUNC_INFO
{
};
C_ASSERT(FIELD_OFFSET(CALL_FUNC,OriginalFunc) == sizeof(CODE_STUB));
struct RET_INFO
{
union
{
struct
{
PCSTR Name;
PVOID params[7];
};
SLIST_ENTRY Entry;
};
INT_PTR __fastcall OnCall(INT_PTR r);
};
struct RET_FUNC : CODE_STUB, RET_INFO
{
};
C_ASSERT(FIELD_OFFSET(RET_FUNC, Entry) == sizeof(CODE_STUB));
#pragma bss_seg(".HOOKS")
RET_FUNC g_rf[1024];//max call count
CALL_FUNC g_cf[16];//max hooks count
#pragma bss_seg()
#pragma comment(linker, "/SECTION:.HOOKS,RWE")
class RET_FUNC_Manager
{
SLIST_HEADER _head;
public:
RET_FUNC_Manager()
{
PSLIST_HEADER head = &_head;
InitializeSListHead(head);
RET_FUNC* p = g_rf;
DWORD n = RTL_NUMBER_OF(g_rf);
do
{
p->Init(CODE_STUB::retstub);
InterlockedPushEntrySList(head, &p++->Entry);
} while (--n);
}
RET_FUNC* alloc()
{
return static_cast<RET_FUNC*>(CONTAINING_RECORD(InterlockedPopEntrySList(&_head), RET_INFO, Entry));
}
void free(RET_INFO* p)
{
InterlockedPushEntrySList(&_head, &p->Entry);
}
} g_rfm;
void* __fastcall FUNC_INFO::OnCall(void** stack)
{
CPP_FUNCTION;
// in case __fastcall function in x86 - param#1 at stack[-1] and param#2 at stack[-2]
// this need for filter post call only
if (RET_FUNC* p = g_rfm.alloc())
{
p->Name = Name;
memcpy(p->params, stack, sizeof(p->params));
*stack = p->Function();
}
return OriginalFunc;
}
INT_PTR __fastcall RET_INFO::OnCall(INT_PTR r)
{
CPP_FUNCTION;
*(void**)_AddressOfReturnAddress() = *params;
PCSTR name = Name;
char buf[8];
if (IS_INTRESOURCE(name))
{
sprintf(buf, "#%04x", (ULONG)(ULONG_PTR)name), name = buf;
}
DbgPrint("%p %s(%p, %p, %p ..)=%p\r\n", *params, name, params[1], params[2], params[3], r);
g_rfm.free(this);
return r;
}
struct DLL_TO_HOOK
{
PCWSTR szDllName;
PCSTR szFuncNames[];
};
void DoHook(DLL_TO_HOOK** pp)
{
PCSTR* ppsz, psz;
DLL_TO_HOOK *p;
ULONG n = RTL_NUMBER_OF(g_cf);
CALL_FUNC* pcf = g_cf;
while (p = *pp++)
{
if (HMODULE hmod = LoadLibraryW(p->szDllName))
{
ppsz = p->szFuncNames;
while (psz = *ppsz++)
{
if (pcf->OriginalFunc = GetProcAddress(hmod, psz))
{
pcf->Name = psz;
pcf->Init(CODE_STUB::callstub);
// do hook: pcf->OriginalFunc -> pcf->Function() - code for this skiped
DbgPrint("hook: (%p) <- (%p)%s\n", pcf->Function(), pcf->OriginalFunc, psz);
if (!--n)
{
return;
}
pcf++;
}
}
}
}
}
asm x64 代码:
extern ?OnCall@FUNC_INFO@@QEAAPEAXPEAPEAX@Z : PROC ; FUNC_INFO::OnCall
extern ?OnCall@RET_INFO@@QEAA_J_J@Z : PROC ; RET_INFO::OnCall
?retstub@CODE_STUB@@SAXXZ proc
pop rcx
mov rdx,rax
call ?OnCall@RET_INFO@@QEAA_J_J@Z
?retstub@CODE_STUB@@SAXXZ endp
?callstub@CODE_STUB@@SAXXZ proc
mov [rsp+10h],rcx
mov [rsp+18h],rdx
mov [rsp+20h],r8
mov [rsp+28h],r9
pop rcx
mov rdx,rsp
sub rsp,18h
call ?OnCall@FUNC_INFO@@QEAAPEAXPEAPEAX@Z
add rsp,18h
mov rcx,[rsp+8]
mov rdx,[rsp+10h]
mov r8,[rsp+18h]
mov r9,[rsp+20h]
jmp rax
?callstub@CODE_STUB@@SAXXZ endp
asm x86代码
extern ?OnCall@FUNC_INFO@@QAIPAXPAPAX@Z : PROC ; FUNC_INFO::OnCall
extern ?OnCall@RET_INFO@@QAIHH@Z : PROC ; RET_INFO::OnCall
?retstub@CODE_STUB@@SAXXZ proc
pop ecx
mov edx,eax
call ?OnCall@RET_INFO@@QAIHH@Z
?retstub@CODE_STUB@@SAXXZ endp
?callstub@CODE_STUB@@SAXXZ proc
xchg [esp],ecx
push edx
lea edx,[esp + 8]
call ?OnCall@FUNC_INFO@@QAIPAXPAPAX@Z
pop edx
pop ecx
jmp eax
?callstub@CODE_STUB@@SAXXZ endp
你可以问我从哪里知道像?OnCall@FUNC_INFO@@QAIPAXPAPAX@Z
这样的装饰名字?查找 c ++ 代码的开头 - 对于几个宏 - 并首次使用#if 1
进行编译并查看输出窗口。希望你明白(你可能需要使用这个名字,但不是我的名字 - 装饰可能不同)
以及如何致电void DoHook(DLL_TO_HOOK** pp)
?那样:
DLL_TO_HOOK dth_kernel32 = { L"kernel32", { "VirtualAlloc", "VirtualFree", "HeapAlloc", 0 } };
DLL_TO_HOOK dth_ntdll = { L"ntdll", { "NtOpenEvent", 0 } };
DLL_TO_HOOK* ghd[] = { &dth_ntdll, &dth_kernel32, 0 };
DoHook(ghd);
答案 2 :(得分:1)
假设存在一个带有已知入口点
的DLLA.DLL
DoStuff
如果入口点DoStuff
已知,则应该在某处记录,至少在某些C头文件中。因此,一种可能的方法可能是解析该标头以获得其签名(即DoStuff
的C声明)。也许你可以使用所有系统头文件中声明的所有函数的签名来填充一些数据库......或者如果你有它,也许可以使用调试信息。
如果你打电话给某个功能(在C中)并且没有提供所有必需的参数,calling convention&amp; ABI仍将被使用,并且这些(缺失的)参数获取垃圾值(如果调用约定定义了要在寄存器中传递的参数,该寄存器中的垃圾;如果约定定义要传递的参数call stack,特定调用堆栈槽内的垃圾)。所以你可能会崩溃,肯定会有一些undefined behavior(这是可怕的,因为你的程序似乎似乎可以工作,但仍然是非常错误的。)
但是,请查看libffi。一旦你知道(在运行时)传递给某个任意函数的内容,就可以构造一个调用它来传递正确数量和类型的参数。
我目前的想法是参数在堆栈上
我认为这是错误的(至少在许多x86-64系统上)。一些参数通过寄存器传递。阅读x86 calling conventions。
这会有用吗?
不,它不会起作用,因为一些参数通过寄存器传递,并且因为调用约定取决于被调用函数的签名(浮点值可能在不同的寄存器中传递,或者总是在堆栈中传递;可变函数具有特定的调用约定;等等......)
BTW,最近的一些C optimizing compilers能够进行tail call优化,这可能会使事情复杂化。答案 3 :(得分:1)
没有标准的方法可以做到这一点,因为调用约定,指针大小等很多东西在传递参数时很重要。您将必须阅读适用于您的平台的ABI并编写实现,我担心在C中将无法实现。您将需要一些内联汇编。
一种简单的方法是(对于像X86_64这样的平台) -
MyDoStuff:
jmpq *__real_DoStuff
这个钩子什么都不做,只是调用真正的函数。如果你想在挂钩时做任何有用的事情,你必须在通话前保存恢复一些寄存器(再次保存的内容取决于ABI)