【问题标题】:How to determine the length of a function?如何确定函数的长度?
【发布时间】:2011-11-25 13:26:07
【问题描述】:

考虑以下代码,它采用函数 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 不会出现在不代表返回值的函数代码中,而是因为它恰好是某些操作码的一部分。
  • 函数的代码不需要连续。也不要求编译器生成与位置无关的代码。 (大多数人不会。)
  • 或者操作系统将允许您执行堆栈上的代码。
  • 带有Harvard Architecture 的机器不会让您轻松地将函数指针转换为数据指针或以其他方式直接在 C 中读取/修改代码。
  • 哈佛建筑相当过时,对语言律师来说是一种好奇心。不过,其他问题都是完全相关的。

标签: c


【解决方案1】:

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

【讨论】:

  • 如果他在x64上,他可以发出PIC,这将解决一半的问题
  • 不,它不会解决问题; PIC实际上使情况变得更糟。非 PIC 代码只会硬编码它访问的数据的绝对地址,只要不进行函数调用,就可以安全地在不同的地址运行 代码,但 PIC 代码将编码数据(或 GOT)的相对地址,如果函数移动,这将是不同的。 PIC 仅在整个 DSO 一起重定位且内部相对地址不变时才有效。它不适用于单功能级别。
  • 这是我正在使用的定义:“位置无关的代码可以复制到任何内存位置并且无需修改即可执行”(en.wikipedia.org/wiki/Position-independent_code
  • 除非您将“代码”解释为整个 DSO,否则该定义与它的实际使用不匹配(例如 gcc 中的 -fPIC)。
  • 这取决于您对外部参考的期望。一种解释是移动的代码将随着移动的外部引用重新定位。 (例如,如果您移动访问变量的函数,则移动的函数假定变量也已移动。)另一个是移动的代码将使用共享的外部引用重新定位。 (移动函数访问原始变量。)您使用的定义在外部变量方面是模棱两可的,大多数人使用第一种解释(移动外部变量),因为这种解释更有用。
【解决方案2】:

在 C 标准中,没有自省或反射的概念,因此您需要自己设计一种方法,正如您所做的那样,但是存在一些其他更安全的方法。

有两种方式:

  1. 分解函数(在运行时)直到你打到finalRETN/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);
    

    这将有一些额外的填充,但它会是最小的(并且无法访问)。虽然风险很大,但比反汇编方法快很多。

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


@R.. 提出了一个很好的观点,除非它的 PIC 或你重新组合它以调整地址等,否则你的代码不会被重定位。

【讨论】:

    【解决方案3】:

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

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

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

    【讨论】:

      【解决方案4】:

      嗯,你可以在运行时使用标签获取函数的长度:

      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) 上崩溃。

      【讨论】:

      • 根据编译器优化,您可能会发现end 出现在之前 start
      猜你喜欢
      • 2015-06-22
      • 2017-11-10
      • 2017-10-18
      • 2013-05-22
      • 1970-01-01
      • 2022-01-11
      • 2010-10-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多