动态修改进程代码段(二)

简介

上一次我们的文章讲述了一些如何动态修改进程代码段的基础内容,包括

  • 如何定位要修改的代码段的地址
  • 如何准备要修改的代码
  • 修改代码段的方式

并且用一个简单的例子说明如何在程序中进行代码段的自修改。

我们在上文的结尾处说过,自修改其实用处不大,我们举那个例子主要是为了说明原理,另外,上文使用的直接覆盖函数代码段的方式也有缺陷,具体原因我们已经在上文描述过,那么在本文中,我们将讲解更为有用的动态修改其他程序代码段的原理,并且使用跳转的方式来安置新的程序数据,最后给出一个例子, 说明它的用处和用法。

在本文中,我们主要讲解如下的点:

  • 如何使用ptrace(2)系统调用来修改其他进程代码段
  • 如何使用跳转指令来安置新的程序数据

ptrace系统调用

我们先来讲解ptrace系统调用,这个系统调用对一般的应用程序员来说可能比较陌生,它的作用主要是监控另一个进程的运行,同时还可以修改被监控进程的内存空间和寄存器的值。它的主要用途是实现程序调试器,比如我们常用的gdb就使用了这个系统调用来实现大部分的功能。

ptrace位于sys/ptrace.h头文件中,函数签名如下:

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

其中:

  1. request表示对被监控程序所进行的某种特定的操作,这个参数有许多不同的值,我们在这儿主要使用的有:PTRACE_ATTACHPTRACE_DETACHPTRACT_CONTPTRACE_GETREGSPTRACE_SETREGESPTRACE_PEEKTEXTPTRACE_POKETEXT
  2. pid表示被监控的程序的pid。
  3. addrdata提供了对于每个request所对应的参数来说的更为具体的参数,因此对于不同的rquest,它们有不同的意义。

大概流程

对于我们的需求来说,我们使用ptrace的一般流程是这样的:首先使用ptrace连接要被修改的进程,然后修改目标进程,使其调用dlopen(2)加载一个我们事先准备好的动态链接库,在这个动态链接库里,我们编写一个新的函数,这个函数将用来替换原进程中的旧函数。然后,在这个动态链接库中有一个标记为__attribute__((constructor))的函数,这个函数在该动态链接库被使用dlopen加载后自动执行,在这个函数里,我们使用上文用到的自动修改自身代码的技术来修改进程代码,不过这一次,我们使用跳转的方式来修改代码,而不是直接覆盖代码段。

使用跳转指令来跳转到新函数

这一步其实比较简单,我们使用jmp指令,使程序跳转到新的位置,假如新的函数名字为new_func,旧函数的名字为old_func,那么我们只需要使用下面的代码来完成替换即可。

memset(old_func, 0x48, 1);
memset(old_func + 1, 0xb8, 1);
memcpy(old_func + 2, &new_func, 8);
memset(old_func + 10, 0xff, 1);
memset(old_func + 11, 0xe0, 1);

上面的代码意思是将old_func的代码替换为:

mov $new_func, %rax
jmp %rax

这两句汇编代码一目了然,我就不再解释意义了。

使用这种方式的一大好处是,我们无需再担心new_func的长度太长从而会导致在整体替换old_func时会产生的溢出问题。

还是一个具体的例子

我们尽量使程序保持简单。

待修改的程序

我们准备一个待修改的程序,这个程序只有一个函数:say_hello,这个程序内容如下

#include <stdio.h>
#include <stdlib.h></stdlib.h></stdio.h>

void say_hello()
{
printf("hello world\n");
}

int main()
{
for (;;)
{
say_hello();
sleep(3);
}
}

编译运行,我们可以看到这个程序不断的输出。

新函数

下面我们准备一个动态链接库,这个动态链接库需要包含新的函数,并且还有用于替换这个新函数的函数(用__attribute__((constructor))标识)。

代码如下:

void new_func() {
printf("hello deepin\n");
}

static void __attribute__((constructor)) init(void) {
void* old_func = dlsym(NULL, "say_hello");
int pagesize = sysconf(_SC_PAGE_SIZE);
#define PAGE_MASK (~(pagesize-1))

int ret = mprotect((void*)((uintptr_t)old_func &amp; PAGE_MASK), numpages * pagesize, PROT_READ|PROT_WRITE|PROT_EXEC);

memset(old_func, 0x48, 1);
memset(old_func + 1, 0xb8, 1);
memcpy(old_func + 2, &amp;new_func, 8);
memset(old_func + 10, 0xff, 1);
memset(old_func + 11, 0xe0, 1);
}

上面的部分是核心代码,我们使用dlsym来找到旧函数say_hello的地址,另外实际编写代码时,不要忘记最后将old_func的地址改为正确的读写权限。

将其编译为动态链接库。

动态程序修改器

下面是这个例子中最重要的部分,我们暂时把它叫做动态程序修改器,它使用了我们前面提到的方法,使用ptrace来修改被监控进程的代码,使其执行dlopen来加载我们准备的动态链接库。

我们将程序打散,并且添加必要的解释。

pid_t pid = (pid_t)strtol(argv[1], '\0', 10);
uintptr_t loader_libc = find_lib_base(getpid(), "libc-");
uintptr_t T_libc = find_lib_base(pid, "libc-");
uintptr_t loader_dlopen = (uintptr_t)dlsym(NULL, "__libc_dlopen_mode");
uintptr_t T_dlopen = T_libc + (loader_dlopen - loader_libc);

首先我们需要找到目标进程内存空间中中dl_open的地址,通过执行上面的代码,T_dlopen最后就是得到的dl_open的地址。我们需要这么做的原因是linux内核在加载二级制程序时,每次都会有一个随机的偏移,但是内存中函数的相对地址却是不变的。然后根据本进程中dl_openlibc的地址来计算出dl_openlibc库中的偏移量,然后就可以根据这个偏移量和目标进程的libc加载地址得到dl_open在目标进程的加载地址。

至于libc的加载地址,我们可以通过读取/proc/\/mem这个文件并分析得到。这正是find_lib_base函数所做的事情。

static unsigned long find_lib_base(pid_t pid, char *so_hint) {
FILE *fp;
char maps[4096], mapbuf[4096], perms[32], libpath[4096];
char *libname;
unsigned long start, end, file_offset, inode, dev_major, dev_minor;

sprintf(maps, "/proc/%d/maps", pid);
fp = fopen(maps, "rb");
if (!fp) {
fprintf(stderr, "Failed to open %s: %s\n", maps, strerror(errno));
return 0;
}

while (fgets(mapbuf, sizeof(mapbuf), fp)) {
sscanf(mapbuf, "%lx-%lx %s %lx %lx:%lx %lu %s", &start,
&end, perms, &file_offset, &dev_major, &dev_minor, &inode, libpath);

libname = strrchr(libpath, '/');
if (libname)
libname++;
else
continue;

if (!strncmp(perms, "r-xp", 4) && strstr(libname, so_hint)) {
fclose(fp);
return start;
}
}

fclose(fp);
return 0;
}

这个函数比较直观,我就不再解释了。

得到这个地址(T_dlopen)之后,我们调用ptrace来连接到目标进程

static int ptrace_attach(pid_t pid) {
int status;
if (ptrace(PTRACE_ATTACH, pid, NULL, NULL)) {
fprintf(stderr, "Failed to ptrace_attach: %s\n", strerror(errno));
return 1;
}

if (waitpid(pid, &status, __WALL) < 0) {
    fprintf(stderr, "Failed to wait for PID %d, %s\n", pid, strerror(errno));
    return 1;
}
return 0;
}

接着,我们保存目标进程现在的现场

int ptrace_getregs(pid_t pid, struct user_regs_struct *regs) {
    if (ptrace(PTRACE_GETREGS, pid, 0, regs)) {
        fprintf(stderr, "Failed to ptrace_getregs: %s\n", strerror(errno));
        return 1;
    }
    return 0;
}

struct user_regs_struct saved_regs;
memset(&saved_regs, 0, sizeof(struct user_regs_struct));

ptrace_getregs(pid, saved_regs);

下面就是比较重要的部分,

struct user_regs_struct regs;
memcpy(®s, saved_regs, sizeof(struct user_regs_struct));
unsigned long invalid = 0x0;
regs.rsp -= sizeof(invalid);
ptrace_poketext(pid, regs.rsp, ((void *)&invalid), sizeof(invalid));
ptrace_poketext(pid, regs.rsp - 4096, filename, strlen(filename) + 1);

long jmp = 0xe0ff;

*msave = ptrace(PTRACE_PEEKTEXT, pid, regs.rip, 0);
if (*msave == -1) {
    fprintf(stderr, "Err: %s\n", strerror(errno));
}
long ret = ptrace(PTRACE_POKETEXT, pid, regs.rip, jmp);
if (ret != 0) {
    fprintf(stderr, "Err: %s\n", strerror(errno));
}

regs.rdi = regs.rsp - 4096;
regs.rsi = RTLD_LAZY;
regs.rax = dlopen_addr;
ptrace_setregs(pid, ®s);

这上面这段代码中,我们先复制一份刚才保存的现场,然后我们要对这个现场做某些调整,

具体来说:

  1. 将当前指令寄存器指向的内存地址内容保存,并修改为 jmp rax
  2. rax寄存器中放着dlopen的地址,意味着接下来程序将跳转执行dlopen函数。
  3. 按照x86函数调用规约设置好dlopen需要的参数。
  4. 将当前栈帧寄存器 指向一个非法地址(内容为0x0),这样在执行完dlopen之后,程序将会收到一个信号并停止运行,
    我们将在那时重新恢复保存的现场。

做完这些工作之后,我们调用ptrace函数使目标进程继续运行

static int ptrace_cont(pid_t pid) {
int status;
if (ptrace(PTRACE_CONT, pid, NULL, 0)) {
    fprintf(stderr, "Failed to ptrace_cont: %s\n", strerror(errno));
    return 1;
}

if (waitpid(pid, &status, __WALL) < 0) {
    fprintf(stderr, "Failed to wait for PID %d, %s\n", pid, strerror(errno));
    return 1;
}
return 0;
}

当dl_open函数执行完成之后,我们所期望的函数已经替换完成。并且由于我们之前将此时,我们再恢复保存的现场,使目标进程继续运行,

ptrace_setregs(pid, &saved_regs);
ptrace(PTRACE_DETACH, pid, 0, 0);

至此,整个程序执行完成之后,我们可以观察到目标程序的输出变化,说明我们的动态修改已经生效。

发表评论

电子邮件地址不会被公开。 必填项已用*标注