首页 -> 安全研究

安全研究

绿盟月刊
绿盟安全月刊->第55期->安全文摘
期刊号: 类型: 关键词:
strace, anti-strace, anti anti-strace

作者:CoolQ <qufuping@ercist.iscas.ac.cn>
出处:http://www.linuxforum.net/forum/showflat.php?Cat=&Board=security&Number=
日期:2005-01-06

|=----------------=[ strace, anti-strace, anti anti-strace ]=---------------=|
|=--------------------------------------------------------------------------=|
|=-----------------=[ CoolQ <qufuping@ercist.iscas.ac.cn> ]=----------------=|
|=--------------------------------------------------------------------------=|

--[ 内容

    0 - 前言
    1 - strace的原理
    2 - strace死循环的分析
    3 - anti-strace
        3.1 方法一
        3.2 方法二
        3.3 方法三
    4 - anti anti-strace
        4.1 int3的情况
        4.2 kill的情况
    5 - 参考
    6 - strace.4.5.8.patch

--[ 0 - 前言

前面在介绍Burneye加密文件的时候,遇到了两个问题,一个是GDB中无法设置断点,另一个
问题是strace时死循环。GDB的问题已经找到,无法设置断点是GDB的一个Bug,具体的结
论见[1].至于strace的问题,一直没有解决,如果真能写出一个让strace死循环的程序,
也算是一种anti-strace的技术。但是一直没有将死循环的现象用程序重现。

后来经Grip2的指点,发现了问题的原因,经过对内核和strace源代码的研究,写了一个
防止strace死循环的patch,之后更进一步的patch,使得strace更加健壮,能够跟踪anti-
strace程序的系统调用。

在这里感谢Grip2的帮助和测试程序。

本文的环境是Redhat Fedora Core 2, Linux 2.6.5/2.6.8.1,
            gcc 3.3.1, strace 4.5.8

--[ 1 - strace的原理

要想了解strace的原理,首先得谈谈ptrace。
ptrace是操作系统为了调试为用户程序提供的系统接口。先来看看ptrace的用法:[2]

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

strace需要使用的__ptrace_request主要有以下几个:

被调试的进程)
       PTRACE_TRACEME 自己主动提供被跟踪的请求,当被跟踪的进程收到信号时,会先被
                      父进程截获.同样,execve时也是如此.

监视进程strace)
       PTRACE_GETREGS 获得被跟踪进程的寄存器状况,详细的结构请参见asm/user.h的
                      user_regs_struct
       PTRACE_PEEKDATA 获得系统堆栈中的参数
       PTRACE_SYSCALL 这是最重要的,每次本跟踪的进程在系统调用时,ptrace会返回
                      两次,一次是系统调用之前,会调用PTRACE_PEEKDATA获得参数
                      值,另一次是系统调用之后,会调用PTRACE_GETREGS获得返回值
ENTRY(system_call)
...
                    # system call tracing in operation
    testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
    jnz syscall_trace_entry
...
syscall_trace_entry:
    movl $-ENOSYS,EAX(%esp)
    movl %esp, %eax
    xorl %edx,%edx
    call do_syscall_trace    <-- ptrace的第一次sysycall跟踪
    movl ORIG_EAX(%esp), %eax
    cmpl $(nr_syscalls), %eax
    jnae syscall_call        <-- 系统调用
    jmp syscall_exit

    # perform syscall exit tracing
    ALIGN
syscall_exit_work:
    testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT), %cl
    jz work_pending
    sti                # could let do_syscall_trace() call
                    # schedule() instead
    movl %esp, %eax
    movl $1, %edx
    call do_syscall_trace    <-- ptrace的第二次sysycall跟踪
    jmp resume_userspace

接下来简单的说一下strace的流程:
strace首先fork,然后子进程先自动PTRACE_TRACEME,然后execve被调试的文件,等待父
进程的调试。父进程进入一个循环,用wait4等待子进程,并判断子进程的状态,退出状
态有几种 - 子进程的信号被截获、子进程系统调用之前与之后、子进程退出。然后按照
情况将截获的信息打印在屏幕上。之后,继续调用PTRACE_SYSCALL使程序继续执行。

--[ 2 - strace死循环的分析

我们先来看看死循环的提示
signal(SIGTRAP, 0x5371991)              = ? ERESTARTNOINTR (To be restarted)
signal(SIGTRAP, 0x5371991)              = ? ERESTARTNOINTR (To be restarted)

似乎是signal系统调用的不断重启,一开始没有搞清楚到底是内核重启系统调用还是
strace重启系统调用。搜索一下sys_signal系统调用和ERESTARTNOINTR的定义,
sys_signal->do_sigaction()
2298         if (signal_pending(current)) {
2299                 /*
2300                  * If there might be a fatal signal pending on multiple
2301                  * threads, make sure we take it before changing the action.
2302                  */
2303                 spin_unlock_irq(&current->sighand->siglock);
2304                 return -ERESTARTNOINTR;
2305         }
也就是说当程序执行sys_signal时,如果有信号还未处理,就直接返回-ERESTARTNOINTR
接下来当系统调用返回时,会依次调用
resume_userspace->work_pending->work_notify_sig->do_notify_resume->do_signal

590  no_signal:
591         /* Did we come from a system call? */
592         if (regs->orig_eax >= 0) {
593                 /* Restart the system call - no handlers present */
594                 if (regs->eax == -ERESTARTNOHAND ||
595                     regs->eax == -ERESTARTSYS ||
596                     regs->eax == -ERESTARTNOINTR) {
597                         regs->eax = regs->orig_eax;
598                         regs->eip -= 2;
599                 }
600                 if (regs->eax == -ERESTART_RESTARTBLOCK){
601                         regs->eax = __NR_restart_syscall;
602                         regs->eip -= 2;
603                 }
604         }
605         return 0;
606 }
此时,  regs->eip -= 2;正好代表int $0x80 (0xcd 0x80)两个字节,可见,内核重启
系统调用。

那么,究竟什么时候signal_pending(current)为真呢?经过GDB调试,发现strace在处理
sys_signal系统调用时,syscall.c:trace_syscall会调用signal的sys_signal函数
        sys_res = (*sysent[tcp->scno].sys_func)(tcp)
signal.c::sys_signal()
...
#ifndef USE_PROCFS
            if (tcp->u_arg[0] == SIGTRAP) {
                tcp->flags |= TCB_SIGTRAPPED;
                kill(tcp->pid, SIGSTOP);
            }
#endif /* !USE_PROCFS */

死循环的问题就在这个kill(tcp->pid, SIGSTOP)上,当程序被处于跟踪的时刻,向已经
停止的进程发送SIGSTOP本来是没有必要的,不知道strace的作者为什么还要单独来上这
么一句?Linux 2.4和2.6内核又出现了差异,在2.6内核的do_sigaction判断了是否有信
号pending,而2.4却没有,因此在2.4的机器上,运行strace不会出现死循环的情况,在
2.6上,我们只须将改行注释掉即可。
我们可以认为这是strace和2.6内核不兼容的一个Bug!

注意:如果你在程序里用的是C库的signal,实际上使用的是sys_rt_sigaction而不是
sys_signal,而strace在处理sys_rt_sigaction时,并没有kill(tcp->pid, SIGSTOP);
也许strace的作者认为用C写的程序不会用到sys_signal?

接下来让我们试一试新的strace来跟踪burneye加密的程序

#./strace /tmp/ls.new (ls.new是burneye加密的程序)
execve("/tmp/ls.new", ["/tmp/ls.new"], [/* 20 vars */]) = 0
signal(SIGTRAP, 0x5371991)              = 0 (SIG_DFL)
--- SIGSEGV (Segmentation fault) @ 0 (0) ---
+++ killed by SIGSEGV ++
OK,不死循环了,但是我们现在还无法继续跟踪,因为burneye加密的时候使用了某些anti-
strace的技术。

--[ 3 - anti-strace的原理

了解了strace的工作原理,就可以有针对的使用anti-strace的技术

--[ 3.1 方法一

利用上边介绍的strace与2.6内核的不兼容,造成死循环.对上边打过patch的strace不适用
测试程序(Grip2提供)
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/types.h>

void sig_handler(int sig)
{
    printf("signal trap\n");
    return;
}
static inline int my_signal(int num, void *func)
{
    int ret;

    
    __asm__ __volatile__ ( "int $0x80"
                :"=a"(ret)
                :"0" (48), "b" ((long)num),
                "c" ((int)func));
    return ret;
    
}    
    
int main(int argc, char *argv[])
{
    my_signal(SIGTRAP, sig_handler);
    return 0;
}
注意一定不能使用C库的signal,原因在前边已经提过

--[ 3.2 方法二

自己发送int3
这种方法是Silvio Cesare在[3]中介绍的方法,由程序自己执行int3,这样会产生一个
陷阱,内核会向程序发送一个SIGTRAP信号,由于程序被跟踪,因此信号由strace截获,
根据strace的源代码,if (ptrace(PTRACE_SYSCALL, pid, (char *) 1, 0) < 0)
可见ptrace并没有返回该信号(0),因此,程序可以设置自己的SIGTRAP处理函数,看有
没有截获信号。burneye就使用了这种方法

例子:
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/types.h>

static int not_trace

void sig_handler(int sig)
{
    not_trace++;
    return;
}
    
int main(int argc, char *argv[])
{
    signal(SIGTRAP, sig_handler);
    __asm__ __volatile__ ( "int3" );
    if(!not_trace){
        printf("TRACING...\n");
        exit(-1);
    }
    return 0;
}

--[ 3.3 方法三

程序使用kill向自己发送SIGTRAP,跟方法二类似,这里就不赘述了
    kill(getpid(), SIGTRAP);

--[ 4 - anti anti-strace

现在我们来进一步完善strace,让它也能对付方法二和方法三,其实问题的关键就是看
strace在ptrace退出之后,下次使用ptrace能不能将SIGTRAP信号返回给被跟踪程序。
根据ptrace的手册页对PTRACE_CONT和PTRACE_SYSCALL的描述
PTRACE_CONT ... If data is non-zero and not SIGSTOP, it is interpreted as a
             signal to be delivered to the child ...
PTRACE_SYSCALL ... Restarts the stopped child as for PTRACE_CONT
看来我们只须在下一次调用ptrace时指定返回的信号是SIGTRAP即可,不过不能胡乱发送,
只有在发现int3和kill(pid, SIGTRAP)的情况下才适用。

--[ 4.1 int3的情况

首先,我们需要判断int3的情况,因此,需要在strace.c::trace()最后添加以下几行

tracing:
        if(ptrace(PTRACE_GETREGS, pid, NULL, (int)&regs) < 0)
            SHOW_PTRACE_ERROR;
        else{
            unsigned int code;
            code = ptrace(PTRACE_PEEKTEXT, pid,
                    (void *)(regs.eip - 4), 0);
            if((code & 0xff000000) == 0xcc000000){
                tprintf("\n!! INT3 FOUND !!\n");
                if(ptrace(PTRACE_SYSCALL,
                    pid,
                    (char *)1,
                    SIGTRAP) < 0)
                    SHOW_PTRACE_ERROR;
                else
                    continue;
            }
        }
...
这段程序,会在每次被跟踪程序停止时用ptrace读取程序的寄存器,判断当前的字节是不
是0xcc(int 3),如果是,就在下一次的PTRACE_SYSCALL时返回SIGTRAP信号,实际的结果
是什么样的呢?我们就以方法二的程序作实验

#./strace /tmp/test.c
...
rt_sigaction(SIGTRAP, {0x80484dc, [TRAP], SA_RESTART}, {SIG_DFL}, 8) = 0

!! INT3 FOUND !!
fstat64(1, {st_mode=S_IFREG|0644, st_size=1463, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) =0xf70a6000
sigreturn()                             = ? (mask now [])

!! INT3 FOUND !!

!! INT3 FOUND !!
sigreturn()                             = ? (mask now [])

!! INT3 FOUND !!

!! INT3 FOUND !!
sigreturn()                             = ? (mask now [])

!! INT3 FOUND !!

!! INT3 FOUND !!
sigreturn()                             = ? (mask now [])

!! INT3 FOUND !!
...

又是死循环,这时sigreturn引起了我的注意,似乎sigreturn也在不断的重启,这是为什
么呢?

首先得弄明白sigreturn是干什么的:一个程序调用signal时,会在内核中注册一个信号
处理函数,当进程收到信号需要处理的时候,需要从内核中直接切换到用户的处理函数
中,切换点是进程在系统调用、中断、异常返回时,另一种情况是进程刚被唤醒时。由于
返回的并不是进入内核之前的代码中,因此,内核需要在进入用户态之前设置一个栈帧,
目的地就是信号处理函数,处理完信号之后,会调用sigreturn返回内核,再由内核返回
到原来进入内核的代码中。
因此,系统调用sigreturn时当前的字节也是0xcc,因此,PTRACE又会发送SIGTRAP信号,
形成了SIGTRAP处理的嵌套。这次的不断重启是strace的问题,与内核无关。
解决的方法是将sigreturn和rt_sigreturn的情况跳过:
加上这么一行:
    if(tcp->scno != __NR_sigreturn && tcp->scno != __NR_rt_sigreturn)

--[ 4.2 kill的情况

kill情况比较简单,只须在sys_kill调用的第二次ptrace返回时,将返回值设定为SIGTRAP
        if(tcp->scno == __NR_kill){
            if(!(tcp->flags & TCB_INSYSCALL)){
                tprintf("\n!! Self SIGTRAP !!\n");
                if(ptrace(PTRACE_SYSCALL,
                    pid,
                    (char *)1,
                    SIGTRAP) < 0)
                    SHOW_PTRACE_ERROR;
                else
                    continue;
            }
        }

--[ 5 - 参考

[1][http://www.linuxforum.net/forum/showthreaded.php?Cat=&Board=security&Number=532461&page=0&view=collapsed&sb=5&o=31]
   [http://www.linuxforum.net/forum/showflat.php?Cat=&Board=security&Number=532460&page=0&view=collapsed&sb=5&o=31&fpart=]
[2] ptrace手册页
[3] [http://vx.netlux.org/lib/vsc04.html]
[4] strace source code
[5] burneye source code
[6] linux kernel source code

--[ 6 - strace.4.5.8.patch

diff -urN strace-4.5.8/signal.c strace-4.5.8.new/signal.c
--- strace-4.5.8/signal.c    2004-10-06 18:11:54.000000000 -0400
+++ strace-4.5.8.new/signal.c    2004-12-29 19:37:15.581983872 -0500
@@ -1173,7 +1173,7 @@
#ifndef USE_PROCFS
            if (tcp->u_arg[0] == SIGTRAP) {
                tcp->flags |= TCB_SIGTRAPPED;
-                kill(tcp->pid, SIGSTOP);
+                //kill(tcp->pid, SIGSTOP);
            }
#endif /* !USE_PROCFS */
            tprintf("%#lx", tcp->u_arg[1]);
diff -urN strace-4.5.8/strace.c strace-4.5.8.new/strace.c
--- strace-4.5.8/strace.c    2004-10-19 22:04:15.000000000 -0400
+++ strace-4.5.8.new/strace.c    2004-12-29 19:37:06.185412368 -0500
@@ -46,6 +46,8 @@
#include <limits.h>
#include <dirent.h>

+#include <asm/user.h>
+
#if defined(IA64) && defined(LINUX)
# include <asm/ptrace_offsets.h>
#endif
@@ -63,6 +65,13 @@
#endif
#endif

+#define SHOW_PTRACE_ERROR     \
+    do{            \
+        perror("");     \
+        cleanup();    \
+        return -1;    \
+    }while(0)
+
int debug = 0, followfork = 0, followvfork = 0, interactive = 0;
int rflag = 0, tflag = 0, dtime = 0, cflag = 0;
int iflag = 0, xflag = 0, qflag = 0;
@@ -1990,6 +1999,7 @@
    int wait_errno;
    int status;
    struct tcb *tcp;
+    struct user_regs_struct regs;
#ifdef LINUX
    struct rusage ru;
#ifdef __WALL
@@ -2297,6 +2307,37 @@
            continue;
        }
    tracing:
+        if(ptrace(PTRACE_GETREGS, pid, NULL, (int)&regs) < 0)
+            SHOW_PTRACE_ERROR;
+        else{
+            unsigned int code;
+            code = ptrace(PTRACE_PEEKTEXT, pid,
+                    (void *)(regs.eip - 4), 0);
+            if((code & 0xff000000) == 0xcc000000){
+                if(tcp->scno != __NR_sigreturn &&
+                    tcp->scno != __NR_rt_sigreturn){
+                    tprintf("\n!! INT3 FOUND !!\n");
+                    if(ptrace(PTRACE_SYSCALL,
+                        pid,
+                        (char *)1,
+                        SIGTRAP) < 0)
+                        SHOW_PTRACE_ERROR;
+                    else
+                        continue;
+                }
+            }else if(tcp->scno == __NR_kill){
+                if(!(tcp->flags & TCB_INSYSCALL)){
+                    tprintf("\n!! Self SIGTRAP !!\n");
+                    if(ptrace(PTRACE_SYSCALL,
+                        pid,
+                        (char *)1,
+                        SIGTRAP) < 0)
+                        SHOW_PTRACE_ERROR;
+                    else
+                        continue;
+                }
+            }
+        }
        if (ptrace(PTRACE_SYSCALL, pid, (char *) 1, 0) < 0) {
            perror("trace: ptrace(PTRACE_SYSCALL, ...)");
            cleanup();

版权所有,未经许可,不得转载