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

时间:2010-07-17 04:42:53

标签: c macos 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非常适合这个目的,因为它允许代码注入特定的进程。

10 个答案:

答案 0 :(得分:12)

我关注了this postthis presentation,并为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 - 相对跳转,因此在此时添加$rip的偏移量(在指令后面dyld_stub_rand+6)。

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

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

答案 1 :(得分:9)

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

对于仅限gdb的解决方案,请参阅我的other solution.


// -*- 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

答案 2 :(得分:5)

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

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

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

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

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

  6. - 棘手的部分 - 查找jumpq 0x*my_rand_addr*指令的十六进制代码
  7. 使用set {int}*rand_addr* = *my_rand_addr*更改符号表指令
  8. Continue执行:现在每当调用rand时,它都会跳转到my_rand而不是
  9. 这有点复杂,非常圆润,但我很确定它会起作用。我还没有完成的唯一事情是创建jumpq指令代码。到目前为止,一切都运行良好。

答案 3 :(得分:2)

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

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

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

答案 4 :(得分:2)

这里的几个答案以及你在答案中链接到的code injection article涵盖了我认为最佳gdb导向解决方案的大块,但没有一个解决方案将所有问题全部拉到一起或涵盖所有点。解决方案的代码表达有点长,所以这里是重要步骤的总结:

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

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

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

    (gdb) source detour.py
    (gdb) exec-file a
    (gdb) set follow-fork-mode child
    (gdb) catch exec
    Catchpoint 1 (exec)
    (gdb) run
    Starting program: /home/llasram/ws/detour/a 
    a: 1933263113
    a: 831502921
    [New process 8500]
    b: 918844931
    process 8500 is executing new program: /home/llasram/ws/detour/b
    [Switching to process 8500]
    
    Catchpoint 1 (exec'd /home/llasram/ws/detour/b), 0x00007ffff7ddfaf0 in _start ()
       from /lib64/ld-linux-x86-64.so.2
    (gdb) break main
    Breakpoint 2 at 0x4005d0: file b.c, line 7.
    (gdb) cont
    Continuing.
    
    Breakpoint 2, main (argc=1, argv=0x7fffffffdd68) at b.c:7
    7       {
    (gdb) detour libc.so.6:rand inject2.so:rand inject2.so:rand__
    (gdb) cont
    Continuing.
    rand invoked! result = 392103444
    b: 22
    
    Program exited normally.

    在子进程中,我从rand()中的libc.so.6函数到rand()中的inject2.so函数创建绕道并存储指向原始蹦床的指针rand()变量rand__中的inject2.so。正如预期的那样,注入的代码调用原始代码,显示完整的结果,并以47模式返回结果。

    由于长度,我只是链接到包含the code for my detour command的糊状物。这是一个相当肤浅的实施(特别是在蹦床生成方面),但它应该在很大比例的情况下运作良好。我在Linux上使用gdb 7.2(最近发布的版本)测试了它,包括32位和64位可执行文件。我没有在OS X上测试它,但任何差异都应该相对较小。

答案 5 :(得分:1)

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

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

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

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

答案 6 :(得分:1)

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

Shared library call redirection using ELF PLT infection

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

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

答案 7 :(得分:1)

如果您使预加载的函数了解它正在使用的情况,您仍然可以使用LD_PRELOAD。这是一个将正常使用rand()的示例,除非在分叉进程内部总是返回42.我使用dl例程将标准库的rand()函数加载到函数指针中,供被劫持的rand()使用。

// -*- compile-command: "gcc -Wall -fPIC -shared my_rand.c -o my_rand.so -ldl"; -*-
//my_rand.c
#include <sys/types.h>
#include <unistd.h>

#include <dlfcn.h>


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 <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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

答案 8 :(得分:0)

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

此处还有一个很好的 Q&amp; A代码注入Mac http://www.mikeash.com/pyblog/friday-qa-2009-01-30-code-injection.html

答案 9 :(得分:0)

我经常使用代码注入作为模拟自动测试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 <sys/types.h>
#include <unistd.h>
#include <limits.h>
#include <stdio.h>
#include <dlfcn.h>

#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
a: 644034683
a: 2011954203
b: 375870504
b: 1222326746
$ LD_PRELOAD=$PWD/inject.so ./a
a: 1023059566
a: 986551064
b: 47
b: 47

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