gdb可以使函数指针指向另一个位置吗?

我会解释一下:

假设我有兴趣替换某个应用程序使用的rand()函数。

所以我将gdb附加到此进程并使其加载我的自定义共享库(具有自定义的rand()函数):

 call (int) dlopen("path_to_library/asdf.so") 

这会将自定义的rand()函数放在进程的内存中。 但是,此时符号rand仍将指向默认的rand()函数。 有没有办法让gdb将符号指向新的rand()函数,强制进程使用我的版本?

我必须说我也不允许使用LD_PRELOAD (linux)或DYLD_INSERT_LIBRARIES (mac os x)方法,因为它们只允许在程序执行开始时注入代码。

我想替换rand()的应用程序,启动几个线程,其中一些启动新进程,我有兴趣在其中一个新进程上注入代码。 正如我上面提到的,GDB非常适合这个目的,因为它允许代码注入特定的进程。

我按照这篇文章和本演示文稿 ,为OSX提供了以下一组带有x86-64可执行文件的gdb命令,在附加到进程时可以使用-x选项加​​载:

 set $s = dyld_stub_rand set $p = ($s+6+*(int*)($s+2)) call (void*)dlsym((void*)dlopen("myrand.dylib"), "my_rand") set *(void**)$p = my_rand c 

神奇的是在set $p = ...命令中。 dyld_stub_rand是一个6字节的跳转指令。 跳转偏移量为dyld_stub_rand+2 (4个字节)。 这是一个$rip -relative跳转,所以在此时加上$rip偏移量(在指令dyld_stub_rand+6dyld_stub_rand+6 )。

这指向一个符号表条目,它应该是真正的rand或动态链接器例程来加载它(如果它从未被调用过)。 然后由my_rand替换。

有时gdb会从libSystem或其他共享库中获取dyld_stub_rand ,如果发生这种情况,请在运行其他命令之前先使用remove-symbol-file卸载它们。

这个问题引起了我的兴趣,所以我做了一些研究。 您正在寻找的是’ dll注入 ‘。 你编写了一个函数来替换一些库函数,把它放在.so中,并告诉ld预加载你的dll。 我刚试了一下,效果很好! 我意识到这并没有真正回答你关于gdb的问题,但我认为它提供了一个可行的解决方法。

对于仅限gdb的解决方案,请参阅我的其他解决方案。


 // -*- compile-command: "gcc -Wall -ggdb -o test test.c"; -*- // test.c #include "stdio.h" #include "stdlib.h" int main(int argc, char** argv) { //should print a fairly random number... printf("Super random number: %d\n", rand()); return 0; } 

 / -*- compile-command: "gcc -Wall -fPIC -shared my_rand.c -o my_rand.so"; -*- //my_rand.c int rand(void) { return 42; } 

编译两个文件,然后运行: LD_PRELOAD="./my_rand.so" ./test

Super random number: 42

我有一个新的解决方案,基于 原始约束。 (我没有删除我的第一个答案,因为其他人可能觉得它很有用。)

我一直在进行一系列的研究,我认为它会更有效。

  1. 在.so中重命名替换rand函数,例如my_rand
  2. 编译所有内容并加载gdb
  3. 使用info functions在符号表中查找rand的地址
  4. 使用dlopen然后dlsym将函数加载到内存中并获取其地址

    call (int) dlopen("my_rand.so", 1) – > -val-

    call (unsigned int) dlsym(-val-, "my_rand") – > my_rand_addr

  5. – 棘手的部分 – 找到jumpq 0x*my_rand_addr*指令的hex代码
  6. 使用set {int}*rand_addr* = *my_rand_addr*更改符号表指令
  7. Continue执行:现在每当调用rand ,它都会跳转到my_rand

这有点复杂,非常圆润,但我很确定它会起作用。 我还没有完成的唯一事情是创建jumpq指令代码。 到目前为止,一切都运行良好。

我不确定如何在正在运行的程序中执行此操作,但也许LD_PRELOAD将适合您。 如果将此环境变量设置为共享对象列表,则运行时加载程序将在过程的早期加载共享对象,并允许其中的函数优先于其他对象。

 LD_PRELOAD=path_to_library/asdf.so path/to/prog 

在开始此过程之前,您必须执行此操作,但不必重建程序。

这里的几个答案以及你在答案中链接到的代码注入文章都涵盖了我认为最佳的面向gdb的解决方案的大块,但是没有一个解决方案将它们全部拉到一起或覆盖所有点。 解决方案的代码表达有点长,所以这里是重要步骤的摘要:

  1. 加载要注入的代码 。 这里发布的大多数答案都使用我认为最好的方法 – 在劣质过程中调用dlopen()来链接包含注入代码的共享库。 在您链接到作者的文章中,加载了一个可重定位目标文件,并将其与下级手工链接。 坦率地说,这是疯狂的 – 可重定位的对象不是“可以运行”的,甚至包括内部引用的重定位。 并且手工链接是繁琐且容易出错的 – 让真正的运行时动态链接器完成工作要简单得多。 这确实意味着首先将libdl引入流程,但是有很多选择可以做到这一点。
  2. 绕道而行 。 到目前为止,这里发布的大多数答案都涉及为感兴趣的函数定位PLT条目,使用它来查找匹配的GOT条目,然后修改GOT条目以指向您注入的函数。 这一点很好,但某些链接器function – 例如,使用dlsym – 可以规避GOT并提供对感兴趣function的直接访问。 确定拦截对特定函数的所有调用的唯一方法是覆盖该函数在内存中的代码的初始指令,以创建“绕行”将执行重定向到注入的函数。
  3. 创建一个蹦床 (可选)。 通常在进行这种注入时,您需要调用您正在拦截其调用的原始函数。 允许使用函数绕道的方法是创建一个小代码“trampoline”,其中包括原始函数的覆盖指令,然后跳转到原始函数的其余部分。 这可能很复杂,因为需要修改复制集中的任何IP相关指令以考虑其新地址。
  4. 自动化一切 。 即使在其他答案中发布了一些更简单的解决方案,这些步骤也可能很乏味。 确保每次使用可变参数(注入不同的函数等)正确完成步骤的最佳方法是自动执行它们。 从7.0系列开始, gdb包含了在Python中编写新命令的function。 这种支持可用于实现一种交钥匙解决方案,用于在劣质过程中注入和剔除代码。

这是一个例子。 我有与以前相同的ab可执行文件,并从以下代码创建了一个inject2.so

 #include  #include  int (*rand__)(void) = NULL; int rand(void) { int result = rand__(); printf("rand invoked! result = %d\n", result); return result % 47; } 

然后我可以将我的Python detour.py命令放在detour.py并进行以下gdb会话:

  (gdb)source detour.py
 (gdb)exec-file a
 (gdb)设置follow-fork-mode子
 (gdb)赶上执行官
 Catchpoint 1(exec)
 (gdb)运行
启动程序:/ home / llasram / ws / detour / aa:1933263113
 a:831502921
 [新流程8500]
 b:918844931
过程8500正在执行新程序:/ home / llasram / ws / detour / b
 [切换到流程8500]

 _start()中的Catchpoint 1(exec'd / home / llasram / ws / detour / b),0x00007ffff7ddfaf0
   来自/lib64/ld-linux-x86-64.so.2
 (gdb)打破主力
断点2在0x4005d0:文件bc,第7行。
 (gdb)续
继续。

断点2,main(argc = 1,argv = 0x7ffffffdd68)at bc:7
 7 {
 (gdb)绕道libc.so.6:rand inject2.so:rand inject2.so:rand__
 (gdb)续
继续。
兰德援引了! 结果= 392103444
 b:22

程序正常退出。 

在子进程中,我创建了一个绕过libc.so.6中的rand()函数到inject2.sorand()函数,并在inject2.sorand__变量中存储指向inject2.so的指针,用于原始rand() inject2.so 。 正如预期的那样,注入的代码调用原始代码,显示完整的结果,并返回结果模47。

由于篇幅,我只是链接到一个包含我的detour命令代码的牧师。 这是一个相当肤浅的实现(特别是在蹦床生成方面),但它应该在很大比例的情况下都能很好地工作。 我在Linux上使用gdb 7.2(最近发布的版本)测试了它,包括32位和64位可执行文件。 我没有在OS X上测试它,但任何差异都应该相对较小。

对于可执行文件,您可以使用objdump轻松找到存储函数指针的地址。 例如:

 objdump -R /bin/bash | grep write 00000000006db558 R_X86_64_JUMP_SLOT fwrite 00000000006db5a0 R_X86_64_JUMP_SLOT write 

因此,0x6db5a0是write指针的地址。 如果更改它,写入的调用将被重定向到您选择的函数。 在早期的post中已经介绍了在gdb中加载新库并获取函数指针。 可执行文件和每个库都有自己的指针。 替换仅影响指针已更改的模块。

对于库,您需要找到库的基址并将其添加到objdump给出的地址。 在Linux中, /proc//maps将其/proc//maps出来。 我不知道具有地址随机化的位置无关的可执行文件是否可行。 在这种情况下, maps信息可能不可用。

只要您要替换的函数位于共享库中,您就可以通过在PLT上进行戳操作,在运行时(调试期间)将调用重定向到该函数。 这篇文章可能会有所帮助:

使用ELF PLT感染的共享库调用重定向

它是从恶意软件修改程序的角度编写的,但更简单的过程适合于调试器中的实时使用。 基本上你只需要在PLT中找到函数的条目,并用要替换它的函数的地址覆盖地址。

谷歌搜索“PLT”以及“ELF”,“共享库”,“动态链接”,“PIC”等术语可能会找到有关该主题的更多详细信息。

如果你让preloaded函数理解它正在使用的情况,你仍然可以使用LD_PRELOAD 。这是一个将正常使用rand()的例子,除了在forked进程中总是返回42时。我使用dl例程将标准库的rand()函数加载到函数指针中以供被劫持的rand()

 // -*- compile-command: "gcc -Wall -fPIC -shared my_rand.c -o my_rand.so -ldl"; -*- //my_rand.c #include  #include  #include  int pid = 0; int (*real_rand)(void) = NULL; void f(void) __attribute__ ((constructor)); void f(void) { pid = getpid(); void* dl = dlopen("libc.so.6", RTLD_LAZY); if(dl) { real_rand = dlsym(dl, "rand"); } } int rand(void) { if(pid == getpid() && real_rand) return real_rand(); else return 42; } 

 //test.c #include  #include  #include  #include  int main(int argc, char** argv) { printf("Super random number: %d\n", rand()); if(fork()) { printf("original process rand: %d\n", rand()); } else { printf("forked process rand: %d\n", rand()); } return 0; } 

 jdizzle@pudding:~$ ./test Super random number: 1804289383 original process rand: 846930886 forked process rand: 846930886 jdizzle@pudding:~$ LD_PRELOAD="/lib/ld-linux.so.2 ./my_rand.so" ./test Super random number: 1804289383 original process rand: 846930886 forked process rand: 42 

我发现这个教程非常有用,到目前为止,它是我设法实现GDB的唯一方法: 运行Linux应用程序的代码注入 : http : //www.codeproject.com/KB/DLL/code_injection.aspx

Mac上的代码注入也有一个很好的问答 : http : //www.mikeash.com/pyblog/friday-qa-2009-01-30-code-injection.html

我经常使用代码注入作为模拟自动测试C代码的方法。 如果这就是你所处的那种情况 – 如果你使用GDB只是因为你对父进程不感兴趣,而不是因为你想以交互方式选择感兴趣的进程 – 那么你仍然可以使用LD_PRELOAD来实现您的解决方案。 您注入的代码只需要确定它是在父进程还是子进程中。 有几种方法可以做到这一点,但在Linux上,由于您的子进程处理exec() ,最简单的可能是查看活动的可执行映像。

我制作了两个可执行文件,一个名为a ,另一个名为b 。 可执行文件a打印两次调用rand()的结果,然后fork() s和exec() s b两次。 可执行文件b打印一次调用rand()的结果。 我使用LD_PRELOAD将以下代码的编译结果注入可执行文件:

 // -*- compile-command: "gcc -D_GNU_SOURCE=1 -Wall -std=gnu99 -O2 -pipe -fPIC -shared -o inject.so inject.c"; -*- #include  #include  #include  #include  #include  #define constructor __attribute__((__constructor__)) typedef int (*rand_t)(void); typedef enum { UNKNOWN, PARENT, CHILD } state_t; state_t state = UNKNOWN; rand_t rand__ = NULL; state_t determine_state(void) { pid_t pid = getpid(); char linkpath[PATH_MAX] = { 0, }; char exepath[PATH_MAX] = { 0, }; ssize_t exesz = 0; snprintf(linkpath, PATH_MAX, "/proc/%d/exe", pid); exesz = readlink(linkpath, exepath, PATH_MAX); if (exesz < 0) return UNKNOWN; switch (exepath[exesz - 1]) { case 'a': return PARENT; case 'b': return CHILD; } return UNKNOWN; } int rand(void) { if (state == CHILD) return 47; return rand__(); } constructor static void inject_init(void) { rand__ = dlsym(RTLD_NEXT, "rand"); state = determine_state(); } 

运行有无注射的结果:

 $ ./a a: 644034683 a: 2011954203 b: 375870504 b: 1222326746 $ LD_PRELOAD=$PWD/inject.so ./a a: 1023059566 a: 986551064 b: 47 b: 47 

我稍后会发布一个面向gdb的解决方案。