3

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

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 程序。那么什么有效呢?

4

3 回答 3

3

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

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

    • 例如,您可以生成一个 C 源文件generated.c,派生一个进程,也许用system("gcc -fPIC -O -shared generated.c -o generated.so");编译它,然后dlopen("./generated.so", RTLD_GLOBAL)dlsym. 有关详细信息,请参见dlopen(3)手册页。仅供参考,MELT正在这样做。

    • 您还可以在内存中生成函数的机器代码(可能通过使用标志的mmap(2)获得)。PROT_EXEC有几个 JIT(即时翻译)库可用:GNU Lightning(快速生成运行缓慢的机器代码)、myjitlibjitLLVM(优化机器代码的缓慢生成)、LuaJIT ...

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

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

于 2012-10-18T09:39:17.483 回答
3

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

于 2012-10-18T05:02:23.853 回答
2

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

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() ”和“ f2() ”中对“ printf() ”的调用是相对的,而不是绝对的。而且我找不到如何使它们成为绝对的(“ -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 ) ”。它用于区分应该替换的内容(打印的数字)和不应该替换的内容(相对引用)。显然,这是一个非常简化的算法。您将必须实现自己的,适合您的任务。

于 2012-10-18T10:17:21.287 回答