如何确定函数的长度?

4
考虑以下代码,它将函数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 进行了测试。

任何试图这样做的代码都无法远离标准。甚至没有保证一个函数在内存中占用连续的空间。当然,也没有保证字节MAGICNUMBER不会出现在函数的代码中,而不是表示返回值,而只是因为它恰好是某个操作码的一部分。 - Steve Jessop
函数的代码不一定要连续。编译器也没有生成位置无关代码的要求。(大多数编译器都没有这个要求。) - Raymond Chen
或者操作系统将允许您执行位于堆栈上的代码。 - JeremyP
具有哈佛架构的机器不允许您轻松地将函数指针转换为数据指针或反之,并直接在C中读取/修改代码。 - Alexey Frunze
哈佛架构已经相当过时了,对于语言专家来说只是一个好奇点。不过其他问题都是完全相关的。 - R.. GitHub STOP HELPING ICE
4个回答

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

如果他使用的是x64,他可以发出PIC,这将解决问题的一半。 - Necrolis
2
不,这并不能解决问题;实际上,PIC会让情况变得更糟。非PIC代码只会硬编码数据访问的绝对地址,只要它不进行函数调用,就可以在不同的地址运行代码,但是PIC代码将编码数据(或GOT)的相对地址,如果函数移动,这些地址将会发生变化。只有在整个DSO一起重新定位且没有内部相对地址改变时,PIC才能正常工作。它无法在单个函数级别上工作。 - R.. GitHub STOP HELPING ICE
这是我使用的定义:“位置无关代码可以复制到任何内存位置并在不修改的情况下执行”。(http://en.wikipedia.org/wiki/Position-independent_code) - Necrolis
那个定义并不符合实际的现实世界使用情况(例如,在gcc中的-fPIC),除非你将“代码”解释为整个DSO。 - R.. GitHub STOP HELPING ICE
这取决于您对外部引用的预期结果。一种解释是移动的代码将与移动的外部引用一起重新定位(例如,如果您移动访问变量的函数,则移动的函数会假定该变量也已移动)。另一种是移动的代码将与共享的外部引用一起重新定位(移动的函数访问原始变量)。您正在使用的定义在外部方面存在歧义,并且大多数人使用第一种解释(移动的外部),因为该解释更有用。 - Raymond Chen
只是作为一种说明,我接受了这个答案,因为它阐述了操纵任何非平凡(不像我刚举的那个例子)函数的徒劳无功。 - otto

1
在C标准中,没有内省或反射的概念,因此您需要自行设计方法,就像您所做的那样,但是还存在其他更安全的方法。
有两种方法:
  1. 在运行时反汇编函数,直到遇到最终的RETN/JMP/等指令,同时考虑到switch/jump表。当然,这需要对你反汇编的函数进行一些深入分析(使用像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或者你重新汇编它以调整地址等,否则它不会是可重定位的。


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

我并不是在暗示这是你问题的直接答案,但你所做的事情太糟糕了,你需要重新考虑你的设计。


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)调用编辑后的函数时会崩溃。


1
根据编译器的优化,你可能会发现 end 出现在 start 之前 - Raymond Chen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接