如何确定函数的长度?

时间:2011-11-25 13:26:07

标签: c

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#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进行了测试。

4 个答案:

答案 0 :(得分:2)

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

答案 1 :(得分:1)

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

有两种方法:

  1. 反汇编函数(在运行时)直到您点击 final RETN / JMP / etc,同时考虑切换/跳转表。这当然需要对你拆卸的功能进行一些繁重的分析(使用类似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);
    

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

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


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

答案 2 :(得分:0)

这是符合标准的方法,可以实现您想要的结果:

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;
}

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

答案 3 :(得分: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)上崩溃。