【问题标题】:Is there a way to modify the code of a function in a Linux C program at runtime?有没有办法在运行时修改 Linux C 程序中的函数代码?
【发布时间】:2012-10-08 11:34:47
【问题描述】:

简单来说,我们有两个类似的功能:

void f1()
{
    printf("%d", 123);
}
void f2()
{
    printf("%d", 124);
}

现在我们在main中调用f1,它打印出123。编译时,f1的反汇编可能是这样的:

08048424 <f1>:
 8048424:       55                      push   %ebp
 8048425:       89 e5                   mov    %esp,%ebp
 8048427:       83 ec 18                sub    $0x18,%esp
 804842a:       b8 40 86 04 08          mov    $0x8048640,%eax
 804842f:       c7 44 24 04 7b 00 00    movl   $0x7b,0x4(%esp)
 8048436:       00
 8048437:       89 04 24                mov    %eax,(%esp)
 804843a:       e8 05 ff ff ff          call   8048344 <printf@plt>
 804843f:       c9                      leave
 8048440:       c3                      ret

f2 的机器码与 f1 类似。

现在我想在运行时将 f1 替换为 f2 的机器码。我使用 memcpy(f1, f2, SIZE_OF_F2_MACHINE_CODE)。 问题当然来了——段错误。

现在我想知道是否存在解决此问题的解决方案。这是一个常见的 C 程序。 据我所知,我们可以使用下面这样的代码来设置 Linux 内核中的可写页面:

int set_page_rw(long unsigned int addr)
{
    unsigned int level;
    pte_t *pte = lookup_address(addr, &level);

    if(pte->pte & ~_PAGE_RW)
        pte->pte |= _PAGE_RW
}

但它不适用于普通的 Linux C 程序。那么什么有效呢?

【问题讨论】:

  • 谷歌搜索“自修改代码”和“内存保护”。基本上,你不能只修改程序代码,因为它可能是只读的。
  • 如果你想改变程序必须执行的代码,你可以用不同的代码动态加载(和卸载)共享库。
  • 其实我想修改linux内核函数的代码。我怀疑它是否可以卸载。
  • ksplice (en.wikipedia.org/wiki/Ksplice) 修改正在运行的内核。内核支持已包含在主线中,因此您可以像修改正在运行的内核那样做。(例如,参见knoppix.mirrors.tds.net/pub/linux/frugalware/frugalware-1.6/…。)
  • 请注意,即使对于非常相似的源代码,函数的机器大小也可能会有所不同。也许将123 更改为12345 可能会改变其大小(例如,因为123 适合一个字节,但12345 不适合,所以需要不同的机器指令)。

标签: c linux gcc assembly writable


【解决方案1】:

不要覆盖过程,而是覆盖符号表中的符号引用。这确实需要动态链接。或者,您可以使用对另一个函数的调用来覆盖对该函数的调用,但是NX 位之类的东西可能会妨碍您。自我修改代码通常不受欢迎。

【讨论】:

    【解决方案2】:

    你为什么这么问?如果您只是希望最终能够调用其代码由同一进程生成的某些函数,您可以采取不同的方式:

    1. 始终使用函数指针来调用此类动态生成的函数;出于可读性原因,我的建议是,在声明指针之前先给他们签名typedef,请参阅this answer
    2. 生成函数并获取指向它的指针。

        1234563有关详细信息,请参阅 dlopen(3) 手册页。仅供参考,MELT 正在这样做。
    3. 您还可以在内存中生成函数的机器码(可能通过mmap(2) 使用PROT_EXECflag 获得)。几个 JIT(即时翻译)库可用:GNU lightning(快速生成运行缓慢的机器代码)、myjitlibjitLLVM(缓慢生成优化机器代码)、LuaJIT ...

    如果你真的想覆盖一些现有的函数代码,你可能会这样做,但这需要非常小心并且很痛苦(例如因为新的函数代码比旧的函数代码需要更多的空间,也因为@ 987654330@ 问题)。使用mmap(2) 和/或mprotect(2) 系统调用来获得此类技巧的许可。但要为调试噩梦做好准备。您可能希望使用your python scripts 编写gdb 调试器的脚本。

    对于内核模块,情况有所不同。我听说一些与网络相关的内核代码(可能是iptables?)可能会使用 JIT 技术来生成机器代码并运行它。

    【讨论】:

    • 另外一点——如果你的代码是用-fPIC编译的,在内存位置之间复制相同的代码可能不起作用。函数和静态变量的地址是相对于指令指针(AKA 程序计数器)计算的。
    【解决方案3】:

    我试图找到你的答案,但失败了。 我实际上成功做的 - 只是简化有问题的代码:

    void f1( )
    {
    }
    int main( )
    {
      *(char*) f1 = *(char*) f1;
      return( 0 );
    }
    

    是的,它因分段错误(在 gcc 中)或内存访问冲突(在 MS VC 中)而失败。

    编辑:

    其实我做到了你想做的事

    (基于 Basile Starynkevitch 的回答)。但仅适用于 x86,仅适用于 gcc,并且仅适用于您的具体示例。下面是几个代码示例。

    首先 - 简化示例。

    #include <stdio.h>
    #include <sys/mman.h>
    #include <unistd.h>
    
    void f1( )
    {
    }
    
    int main( )
    {
      int rc;
      int pagesize;
      char *p;
    
      f1( );
    
      pagesize = sysconf( _SC_PAGE_SIZE );
      printf( "pagesize=%d (0x%08X).\n", pagesize, pagesize );
      if( pagesize == -1 )
        return( 2 );
    
      p = (char*) f1;
      printf( "p=0x%08X.\n", p );
      p = (char*) ((size_t) p & ~(pagesize - 1));
      printf( "p=0x%08X.\n", p );
    
      rc = mprotect( p, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC );
      printf( "rc=%d.\n", rc );
      if( rc != 0 )
        return( 2 );
    
      printf( "'mprotect()' succeeded.\n" );
    
      *(char*) f1 = *(char*) f1;
    
      printf( "Write succeeded.\n" );
    
      f1( );
    
      printf( "Call succeeded.\n" );
    
      return( 0 );
    }
    

    您编译它并启动一次。它会失败,但你会知道页面大小。假设它是 4096。然后你像这样编译这个例子:

    gcc a1.c -falign-functions=4096
    

    它应该可以工作。

    输出:

    pagesize=4096 (0x00001000).
    p=0x00402000.
    p=0x00402000.
    rc=0.
    'mprotect()' succeeded.
    Write succeeded.
    Call succeeded.
    

    现在是高级示例:

    #include <stdio.h>
    #include <sys/mman.h>
    #include <unistd.h>
    #include <string.h>
    
    //extern void f1( void ) __attribute__(( aligned( 4096 ) ));
    __asm__( ".text" );
    __asm__( ".align 4096" );
    void f1( void )
    {
      printf( "%d\n", 123 );
    }
    
    void f2( void )
    {
      printf( "%d\n", 124 );
    }
    
    int main( void )
    {
      int rc;
      int pagesize;
      char *p;
      int i;
    
      printf( "f1=0x%08X.\n", f1 );
      printf( "f2=0x%08X.\n", f2 );
    
      f1( );
      f2( );
    
      pagesize = sysconf( _SC_PAGE_SIZE );
      printf( "pagesize=%d (0x%08X).\n", pagesize, pagesize );
      if( pagesize == -1 )
        return( 2 );
    
      p = (char*) f1;
      printf( "p=0x%08X.\n", p );
      p = (char*) ((size_t) p & ~(pagesize - 1));
      printf( "p=0x%08X.\n", p );
    
      rc = mprotect( p, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC );
      printf( "rc=%d.\n", rc );
      if( rc != 0 )
        return( 2 );
    
      printf( "'mprotect()' succeeded.\n" );
    
      for( i = 0; i < (size_t) f2 - (size_t) f1; i++ ) {
        if( ((char*) f2)[ i ] == 124 ) {
          printf( "i=%d.\n", i );
          ((char*) f1)[ i ] = ((char*) f2)[ i ];
        }
      }
    
      //memcpy( f1, f2, (size_t) f2 - (size_t) f1 );
    
      printf( "Write succeeded.\n" );
    
      f1( );
      f2( );
    
      printf( "Call succeeded.\n" );
    
      return( 0 );
    }
    

    您不能在此处使用“memcpy()”(已注释),因为在“f1()printf()” /strong>" 和 "f2()" 是相对的,不是绝对的。而且我找不到如何使它们成为绝对的(“-fPIC”和“-fno-PIC”在我的情况下都不起作用)。如果你在“f1()”和“f2()”中没有相关的函数调用,相信你可以使用“memcpy()”(但我没有尝试)。

    您还应该使用“f1()”与页面大小的对齐方式(除非您确定在“f1()”开始之前有足够的代码)。如果你有 gcc 4.3 和更高版本,你可以使用属性(因为我有 gcc v4.1.2,所以被注释了)。如果没有,您可以使用那个丑陋且不可靠的“_asm_”。

    输出:

    f1=0x00402000.
    f2=0x0040201E.
    123
    124
    pagesize=4096 (0x00001000).
    p=0x00402000.
    p=0x00402000.
    rc=0.
    'mprotect()' succeeded.
    i=12.
    Write succeeded.
    124
    124
    Call succeeded.
    

    当然还有那个可怕的“if( ((char*) f2)[ i ] == 124 )”。它用于区分应该替换的内容(打印的数字)和不应该替换的内容(相对引用)。显然,这是一个非常简化的算法。您必须实现自己的,适合您的任务。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-03-17
      • 2017-06-16
      • 2022-06-10
      • 2021-08-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-20
      相关资源
      最近更新 更多