Chinaunix首页 | 论坛 | 博客
  • 博客访问: 341567
  • 博文数量: 89
  • 博客积分: 5152
  • 博客等级: 大校
  • 技术积分: 1155
  • 用 户 组: 普通用户
  • 注册时间: 2006-02-25 15:12
文章分类

全部博文(89)

文章存档

2012年(1)

2011年(5)

2010年(14)

2009年(69)

我的朋友

分类: LINUX

2009-04-14 09:26:17


以 ptrace 系统呼叫来追踪/修改进程
一月份南下分享主题为 [快快乐乐学 GNU Debugger] 的演讲,当时为了说明 GNU Debugger (gdb) 在 Linux 运作的原理,提及 ptrace 系统呼叫,这是何以 gdb 能行使动态追踪、分析,进而修改执行中进程 (process) 的关键。本文试着以简要的桉例,说明如何使用 ptrace 系统呼叫,达到类似 gdb 的行为。

在 MS-Windows 中,「拦截」或「追踪」其他进程,是相当进阶的议题,而且为了达到目的,往往得诉诸颇多 hacks,然而,在 UNIX 的世界裡,作业系统提供 ptrace 系统呼叫,允许我们优雅地进行这些动作。且让我们问问男人 (双关语,UNIX "manual",缩写为 "man"),看 ptrace() 的描述:

      # man 2 ptrace
      The ptrace() system call provides a means by which a parent process may observe and control the execution of another process, and examine and change its core image and registers. It is primarily used to implement breakpoint debugging and system call tracing.

      The parent can initiate a trace by calling fork(2) and having the resulting child do a PTRACE_TRACEME, followed (typically) by an exec(3). Alternatively, the parent may commence trace of an existing process using PTRACE_ATTACH.
      ... (后略) ...

整理我们的初步认知:

    * ptrace 系统呼叫用以实做 gdb 一类可断点 (breakpoint) 的追踪除错,或作系统呼叫的追踪分析
    * ptrace 允许一个 parent process 去监控另一个 process 的执行,并得以检验 / 更改执行时期的系统 image (映射于虚拟记忆体) 和寄存器
    * 使用情境可透过 fork 系统呼叫去建立 child process (搭配 exec 系统呼叫) 或者直接追踪某个已执行的 process

另外,依据此陈述,我们也可发现,在使用者层级 (user-level / user-space) 追踪其他进程是可行的。而在实际「追踪」前,我们应该要很清楚无论哪个进程,只要存取到系统服务,哪怕只是 "Hello World" 等级的应用程序想透过标准 C 函式库呼叫 printf() 印列字元,都涉及系统呼叫 (system call,以下简称 "syscall") 的处理,在前年、去年的 [深入浅出 Hello World] 系列演讲中,已对此做了系统性的探讨,本文不再细究其原理,仅点出重点以衔接 ptrace 的角色。

对 Linux 来说,在 IA32 (32 位元的 x86 系统,本文也记作 i386) 上,一个进程欲使用 syscall 时,需要将相关参数推入至寄存器 (register),并触发 0x80 (hex) 软体中断,就绪后,进程的控制权,就从 user-space 切换到 kernel-space,由核心来完成系统呼叫的具体动作。示意图如下: (出处:〈Kernel command using Linux system calls〉, M. Tim Jones)


上图以 getpid() 这个 syscall 为例,实际上,C 语言的应用程序并非直接呼叫 syscall 的,而需要透过 GNU glibc 一类 C-Library 裡头 syscall wrapper 所提供的函式呼叫进行,其细部的实做就是在 eax 寄存器设定 getpid syscall 的编号 (定义于 syscall 表格的 _NR_getpid 项目),之后触发 int 0x80。由上图可见,执行权移转到核心,核心由刚刚设定的 eax 寄存器作为索引,去 system_call_table[] 找到真正实做 getpid syscall 的进入点 (entry),去呼叫并在执行后,将控制权交回 user-space,对原本的进程来说,就是 getpid() 这个 C 函式的返回。

那麽,ptrace 这个 syscall 在何时现身?本文大幅略过平台相关的部份,仅探讨行为模式:在 syscall 真正呼叫前,Linux 核心会检查目前的进程是否处于「被追踪」(traced) 的状态,若是,核心会暂停 (stop) 目前的进程,并将控制权交与欲追踪 (主动去追踪其他进程者) 的进程,让跟踪进程得以监控 traced process 的寄存器,当然也包含执行时期的 pc (Program Counter,在 IA32 上,就是 eip 寄存器)。

有了基础概念后,我们就可实际作点事。笔者于两年前撰写过一篇文章 [SM 版 Hello World],谈及 Linux/i386 上,如何实现 SMC (Self-Modifying Code),而标题的 "SM" 当然是指一个进程如 "Hello World" 者,如何自我修改执行时期的内容,也就是 Selt-Modifying,感觉是「自虐」,反而不若一般说 SM (施虐与受虐) 的语意。那麽,今天我们就来作个真正有 "SM" 能力的程序,展示动态追踪 / 修改其他进程的行为,预想的情境为先让一个无限迴圈的进程保持等待,然后另一个进程透过 ptrace syscall 去拦截该进程并修改执行内容 (是的,就是「施虐」的动作),最后让被 ptrace 进程产生变化 (也就是「受虐」者)。

ptrace 的函式声明如下:

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

首个引数规范 ptrace syscall 的具体行为与其使用模式,有以下值:

    * PTRACE_TRACEME
    * PTRACE_PEEKTEXT, PTRACE_PEEKDATA
    * PTRACE_PEEKUSER
    * PTRACE_POKETEXT, PTRACE_POKEDATA (*)
    * PTRACE_POKEUSER
    * PTRACE_GETREGS (*), PTRACE_GETFPREGS
    * PTRACE_GETSIGINFO
    * PTRACE_SETREGS (*), PTRACE_SETFPREGS
    * PTRACE_SETSIGINFO
    * PTRACE_SETOPTIONS
    * PTRACE_GETEVENTMSG
    * PTRACE_CONT
    * PTRACE_SYSCALL, PTRACE_SINGLESTEP
    * PTRACE_SYSEMU, PTRACE_SYSEMU_SINGLESTEP
    * PTRACE_KILL
    * PTRACE_ATTACH (*)
    * PTRACE_DETACH (*)

标注 (*) 者,为本文范例所用到的 request。其中 PTRACE_GETSIGINFO 与 PTRACE_SETSIGINFO 为 Linux 2.3.99-pre6 后所追加;PTRACE_SETOPTIONS 则为 2.4.6 后追加,引入若干新的 bit mask;PTRACE_GETEVENTMSG 为 2.5.46 后追加;PTRACE_SYSEMU 与 PTRACE_SYSEMU_SINGLESTEP 为 2.6.14 后追加,提供给 UML (User-Mode Linux) 一类的系统作为 syscall 模拟使用。由此可见 Linux 改版时,ptrace 也会随着更动,注意,addr 与 data 这两个引数在某些 request 会被忽略,详情可参考 man-pages 描述。

咱们动手来写程序,就取名为 [injector.c] 表示符合前述情境。以下为程序码列表:

#include
#include
#include /* ptrace() */

#include    /* wait() */
#include
#include    /* struct user_regs_struct */

/* _exit(1) implementation in shellcode */

static char shellcode[] =
    "\x31\xc0"         /* xor  %eax,%eax */
    "\x40"             /* inc  %eax */
    "\xcd\x80"         /* int  $0x80 */ ;


#include
#define OUT_MSG(x, ...) printf("* " x "\n",## __VA_ARGS__)
#define ERR_MSG(x) printf("\t[Error] " x "\n")

int main(int argc, char *argv[])
{
    int pid, offset;
    struct user_regs_struct regs;

    OUT_MSG("Injector starts.");
    if (argc < 2) {
        ERR_MSG("PID required in parameter.");
        return -1;
    }

    pid = atoi(argv[1]);
    OUT_MSG("Attaching process (PID=%d)...", pid);
    if (ptrace(PTRACE_ATTACH, pid, NULL, NULL) < 0) {
        ERR_MSG("Fail to ptrace process");
        ptrace(PTRACE_DETACH, pid, NULL, NULL);
        return -1;
    }

    OUT_MSG("Process attached.");
    /* see if  a child has stopped (but not traced via ptrace(2)) */

    if (waitpid(pid, NULL, WUNTRACED) < 0) {
        ERR_MSG("WUNTRACED");
        exit(1);
    }

    OUT_MSG("Getting registers from process.");
    if (ptrace(PTRACE_GETREGS, pid, NULL, ®s) < 0) {
        ERR_MSG("Fail to get registers.");
        ptrace(PTRACE_DETACH, pid, NULL, NULL);
        exit(1);
    }

    OUT_MSG("Injecting shellcode into process...");
    for (offset = 0; offset < sizeof(shellcode); offset++) {
        if (ptrace(PTRACE_POKEDATA, pid,
                   regs.esp + offset,
                   *(int *) &shellcode[offset])) {
            ERR_MSG("Fail to inject.");
            ptrace(PTRACE_DETACH, pid, NULL, NULL);
            exit(1);
        }
    }

    regs.eip = regs.esp;
    regs.eip += 2;

    OUT_MSG("Adjust program counter (EIP) of process to 0x%x",
            (unsigned int) regs.eip);
    if (ptrace(PTRACE_SETREGS, pid, NULL, ®s) < 0) {
        ERR_MSG("Unable to set registers.");
        ptrace(PTRACE_DETACH, pid, NULL, NULL);
        exit(1);
    }

    OUT_MSG("Detach process (PID=%d).", pid);
    ptrace(PTRACE_DETACH, pid, NULL, NULL);
    OUT_MSG("Done");
    return 0;
}

既然有了「施虐」者程序,就再弄个「受虐」者程序,比方说 [dummy-loop-prog.c],以下是程序列表:

#include
int main()
{
    while (1)
        sleep(1);
    return 0;
}

没什麽好说,就是一直透过 sleep syscall 等待的小程序。一开始,我们让 dummy-loop-prog 先执行,如下图:

而当前述的 injector 执行时,我们将会发现:

injector 透过 ptrace syscall 去 attach 到执行中 dummy-loop-prog 的进程 (PID=943),先取得寄存器的值,当然,最有兴趣的是 ESP,也就是指向目前的 stack frame 的指标,因为我们要注入 shellcode。一旦完成注入的动作后,需要将 EIP 寄存器的值调整,这时候 PID=943 的进程就被迫执行到 shellcode 的内容,也就是 _exit(1),这会使原本的无限迴圈因而终止,进程也会结束。

[injector.c] 的程序码中,将 request 设定为 PTRACE_GETREGS 传递给 ptrace syscall 可得到被追踪的进程现行的寄存器内容,保存于 user_regs_struct 结构体之中,而这定义于 档头。而 request 设定为 PTRACE_PEEKDATA / PTRACE_POKEDATA 时,则可窥视 / 修改执行中进程的内容,就像笔者展示植入 shellcode 的行为一般。更甚者,request 设定为 PTRACE_SINGLESTEP 时,ptrace 允许对 child process 进行单步执行的动作,这会使得核心在 child process 的每条指令执行前,会先中断等待着,而将控制权交给追踪的进程,就好比 gdb 的行为一般,当然,能做的事情就更多了。

ptrace 系统呼叫无疑是对 Linux 底层运作机制作寻幽访胜,一个相当强大且优雅的工具,若能善用,并搭配既有的工具如 gdb,将能如虎添翼。另外,由于 ptrace syscall 在若干应用,如 User-Mode-Linux 与 multi-threading 环境中,有先天设计的缺陷,RedHat 工程师 Roland McGrath 则展开 [utrace] 的新发展,目标是完全取代 ptrace,允许 user-space 进程透过 utrace syscall,得以掌握更多 Linux 核心的资讯,并引入名为 "tracing engine" 的新机制,可更深入地追踪掌控进程的状态。
由 jserv 发表于 May 23, 2008 02:07 AM

阅读(1296) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~