如何确定函数的长度?

考虑以下带有函数f()的代码,将函数本身完整地复制到缓冲区,修改其代码并运行更改的函数。 实际上,克隆并修改返回编号22的原始函数以返回编号42。

#include  #include  #include  #define ENOUGH 1000 #define MAGICNUMBER 22 #define OTHERMAGICNUMBER 42 int f(void) { return MAGICNUMBER; } int main(void) { int i,k; char buffer[ENOUGH]; /* Pointer to original function f */ int (*srcfptr)(void) = f; /* Pointer to hold the manipulated function */ int (*dstfptr)(void) = (void*)buffer; char* byte; memcpy(dstfptr, srcfptr, ENOUGH); /* Replace magic number inside the function with another */ for (i=0; i < ENOUGH; i++) { byte = ((char*)dstfptr)+i; if (*byte == MAGICNUMBER) { *byte = OTHERMAGICNUMBER; } } k = dstfptr(); /* Prints the other magic number */ printf("Hello %d!\n", k); return 0; } 

代码现在依赖于猜测函数将适合1000字节缓冲区。 它还通过向缓冲区复制过多来违反规则,因为函数f()很可能比1000字节短很多。

这就引出了一个问题 :是否有一种方法可以计算出C中任何给定函数的大小? 一些方法包括查看中间链接器输出,并根据函数中的指令进行猜测,但这还不够。 有什么方法可以肯定吗?


请注意:它在我的系统上编译和工作但不完全遵守标准,因为函数指针和void *之间的转换不是完全允许的:

 $ gcc -Wall -ansi -pedantic fptr.c -o fptr fptr.c: In function 'main': fptr.c:21: warning: ISO C forbids initialization between function pointer and 'void *' fptr.c:23: warning: ISO C forbids passing argument 1 of 'memcpy' between function pointer and 'void *' /usr/include/string.h:44: note: expected 'void * __restrict__' but argument is of type 'int (*)(void)' fptr.c:23: warning: ISO C forbids passing argument 2 of 'memcpy' between function pointer and 'void *' /usr/include/string.h:44: note: expected 'const void * __restrict__' but argument is of type 'int (*)(void)' fptr.c:26: warning: ISO C forbids conversion of function pointer to object pointer type $ ./fptr Hello 42! $ 

请注意:在一些从可写内存执行的系统上是不可能的,这段代码会崩溃。 它已经在运行x86_64架构的Linux上使用gcc 4.4.4进行了测试。

你不能在C中做到这一点。即使你知道长度,函数的地址也很重要,因为函数调用和对某些类型数据的访问将使用程序计数器相对寻址。 因此,位于不同地址的函数的副本将不会与原始函数执行相同的操作。 当然还有很多其他问题。

在C标准中,没有内省或反思的概念,因此你需要自己设计一个方法,就像你所做的那样,然而存在一些其他更安全的方法。

有两种方法:

  1. 反汇编函数(在运行时 )直到你到达最后的 RETN / JMP /等,同时考虑开关/跳转表。 这当然需要对你拆卸的function进行一些繁重的分析(使用像beaEngine这样的引擎),这当然是最可靠的,但它的速度慢而且重。
  2. 滥用编译单元,这是非常危险的,而不是万无一失的,但如果您知道编译器在编译单元中按顺序生成函数,您可以按照以下方式执行操作:

     void MyFunc() { //... } void MyFuncSentinel() { } //somewhere in code size_t z = (uintptr_t)MyFuncSentinel - (uintptr_t)MyFunc; uint8_t* buf = (uint8_t*)malloc(z); memcpy(buf,(char*)MyFunc,z); 

    这将有一些额外的填充,但它将是最小的(和无法访问)。 虽然风险很高,但它的反汇编方法要快得多。

注意:这两种方法都要求目标代码具有读取权限。


@R ..提出了一个很好的观点,你的代码将无法重新定位,除非它的PIC或你就地重新调整它以调整地址等。

以下是符合标准的实现所需结果的方法:

 int f(int magicNumber) { return magicNumber; } int main(void) { k = f(OTHERMAGICNUMBER); /* Prints the other magic number */ printf("Hello %d!\n", k); return 0; } 

现在,你可能在没有参数的地方有很多f()的使用,并且不想通过你的代码改变每一个,所以你可以改为

 int f() { return newf(MAGICNUMBER); } int newf(int magicNumber) { return magicNumber; } int main(void) { k = newf(OTHERMAGICNUMBER); /* Prints the other magic number */ printf("Hello %d!\n", k); return 0; } 

我并不是说这是对你的问题的直接回答,但你所做的是如此可怕,你需要重新考虑你的设计。

那么,您可以使用标签在运行时获取函数的长度:

 int f() { int length; start: length = &&end - &&start + 11; // 11 is the length of function prologue // and epilogue, got with gdb printf("Magic number: %d\n", MagicNumber); end: return length; } 

执行此函数后,我们知道它的长度,因此我们可以将malloc为正确的长度,复制和编辑代码,然后执行它。

 int main() { int (*pointerToF)(), (*newFunc)(), length, i; char *buffer, *byte; length = f(); buffer = malloc(length); if(!buffer) { printf("can't malloc\n"); return 0; } pointerToF = f; newFunc = (void*)buffer; memcpy(newFunc, pointerToF, length); for (i=0; i < length; i++) { byte = ((char*)newFunc)+i; if (*byte == MagicNumber) { *byte = CrackedNumber; } } newFunc(); } 

现在还有另一个更大的问题,就是@R。 提及。 一旦修改(正确),使用此函数会在调用printf时导致分段错误,因为call指令必须指定一个错误的偏移量 。 您可以使用gdb查看此内容,使用disassemble f查看原始代码,使用x/15i buffer查看编辑过的代码。
顺便说一下,我的代码和你的代码在没有警告的情况下编译,但在调用编辑过的函数时崩溃在我的机器上( gcc 4.4.3 )。