(该系列将深入分析Linux ptrace的方方面面,争取一次把它搞定,对后续调试或开发都有益处。不定期更新。)
备注:文章中使用的Linux内核源码版本为Linux 5.9,使用的Linux版本为Linux ubuntu 5.4.0-65-generic
ptrace系统调用提供了一个进程(tracer
)可以控制另一个进程(tracee
)运行的方法,并且tracer
可以监控和修改tracee
的内存和寄存器,主要用作实现断点调试和系统调用追踪。
tracee
首先要被attach到tracer
上,这里的attach以线程为对象,在多线程场景(这里的多线程场景指的使用clone CLONE_THREAD
flag创建的线程组)下,每个线程可以分别被attach到tracer
上。ptrace的命令总是以下面的调用格式发送到指定的tracee
上:
一个进程可以通过调用fork()
函数来初始化一个跟踪,并让生成的子进程执行PTRACE_TRACEME
,然后执行execve
(一般情况下)来启动跟踪。进程也可以使用PTRACE_ATTACH
或PTRACE_SEIZE
进行跟踪。
当处于被跟踪状态时,tracee
每收到一个信号就会stop,即使是某些时候信号是被忽略的。tracer
将在下一次调用waitpid
或与wait
相关的系统调用之一)时收到通知。该调用会返回一个状态值,包含tracee
停止的原因。tracee
发生stop时,tracer
可以使用各种ptrace的request
来检查和修改tracee
。然后,tracer
使tracee
继续运行,选择性地忽略所传递的信号(甚至传递一个与原来不同的信号)。
当tracer
结束跟踪后,发送PTRACE_DETACH
信号释放tracee
,tracee
可以在常规状态下继续运行。
ptrace的原型如下:
其中request
参数表明执行的行为(后续将重点介绍), pid
参数标识目标进程,addr
参数表明执行peek
和poke
操作的地址,data
参数则对于poke
操作,指明存放数据的地址,对于peek
操作,指明获取数据的地址。
返回值,成功执行时,PTRACE_PEEK
请求返回所请求的数据,其他情况时返回0,失败则返回-1。
ptrace的内核实现在kernel/ptrace.c
文件中,内核接口是SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data)
。其代码如下,整体逻辑简单,需要注意的是对PTRACE_TRACEME
和PTRACE_ATTACH
进行了特殊处理(对于该函数的参数后续将进行深入解析)。
系统调用都改为了SYSCALL_DEFINE
的方式。如何获得上面的定义的呢?这里需要穿插一下SYSCALL_DEFINE
的定义(syscall.h):
宏定义进行展开:
__SYSCALL_DEFINEx
中的x
表示系统调用的参数个数,且sys_ptrace
的宏定义如下:
所以对应的__SYSCALL_DEFINEx
应该是SYSCALL_DEFINE4
,这与上面的定义SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr, unsigned long, data)
一致。
仔细观察上面的代码可以发现,函数定义其实在最后一行,结尾没有分号,然后再加上花括号即形成完整的函数定义。前面的几句代码并不是函数的实现(详细的分析可以跟踪源码,出于篇幅原因此处不放出每个宏定义的跟踪)。
定义的转换过程:
而对__MAP
宏和__SC_DECL
宏的定义如下:
按照如上定义继续进行展开
最后调用asmlinkage long sys_ptrace(long request, long pid, unsigned long addr, unsigned long data);
。
为什么要将系统调用定义成宏?主要是因为2个内核漏洞CVE-2009-0029,CVE-2010-3301,Linux 2.6.28及以前版本的内核中,将系统调用中32位参数传入64位的寄存器时无法作符号扩展,可能导致系统崩溃或提权漏洞。
内核开发者通过将系统调用的所有输入参数都先转化成long类型(64位),再强制转化到相应的类型来规避这个漏洞。
首先通过一个简单的例子来熟悉一下ptrace
的使用:
运行结果如下:
打印出系统调用号,并等待用户输入。查看/usr/include/x86_64-linux-gnu/asm/unistd_64.h
文件(64位系统)查看59对应的系统调用:
59号恰好为execve
函数调用。对上面的过程进行简单总结:
父进程通过调用fork()
来创建子进程,在子进程中,执行execl()
之前,先运行ptrace()
,request
参数设置为PTRACE_TRACEME
来告诉kernel当前进程正在被trace。当有信号量传递到该进程,进程会stop,提醒父进程在wait()
调用处继续执行。然后调用execl()
,执行成功后,新程序运行前,SIGTRAP
信号量被发送到该进程,子进程停止,父进程在wait()
调用处收到通知,获取子进程的控制权,查看子进程内存和寄存器相关信息。
当发生系统调用时,kernel保存了rax
寄存器的原始内容,其中存放的是系统调用号,我们可以使用request
参数为PTRACE_PEEKUSER
的ptrace
来从子进程的USER
段读取出该值。
系统调用检查结束后,子进程通过调用request
参数为PTRACE_CONT
的ptrace
函数继续执行。
执行结果:
在上面的程序中,跟踪的是wirte
的系统调用,ls
命令总计进行了三次write
的调用。request
参数为PTEACE_SYSCALL
时的ptrace
使kernel在进行系统调用进入或退出时stop子进程,这等价于执行PTRACE_CONT
并在下一次系统调用进入或退出时stop。
wait
系统调用中的status
变量用于检查子进程是否已退出,这是用来检查子进程是否被ptrace停掉或是否退出的典型方法。而宏WIFEXITED
则表示了子进程是否正常结束(例如通过调用exit
或者从main
返回等),正常结束时返回true
。
前面有介绍PTRACE_GETREGS
参数,使用它来获取寄存器的值相比前面一种方法要简单很多:
执行结果:
整体输出与前面的代码无所差别,但在代码开发上使用了PTRACE_GETREGS
来获取子进程的寄存器的值,简洁了很多。
[1]. https://www.linuxjournal.com/article/6100
[2]. https://blog.csdn.net/u012417380/article/details/60468697
[3]. Linux ptrace man page
ptrace(PTRACE_foom, pid, ...)
/
/
pid为linux中对应的线程
ID
ptrace(PTRACE_foom, pid, ...)
/
/
pid为linux中对应的线程
ID
long
ptrace(enum __ptrace_request request, pid_t pid, void
*
addr, void
*
data);
long
ptrace(enum __ptrace_request request, pid_t pid, void
*
addr, void
*
data);
SYSCALL_DEFINE4(ptrace,
long
, request,
long
, pid, unsigned
long
, addr,unsigned
long
, data)
{
struct task_struct
*
child;
long
ret;
if
(request
=
=
PTRACE_TRACEME) {
ret
=
ptrace_traceme();
if
(!ret)
arch_ptrace_attach(current);
goto out;
}
child
=
find_get_task_by_vpid(pid);
if
(!child) {
ret
=
-
ESRCH;
goto out;
}
if
(request
=
=
PTRACE_ATTACH || request
=
=
PTRACE_SEIZE) {
ret
=
ptrace_attach(child, request, addr, data);
/
*
*
Some architectures need to do book
-
keeping after
*
a ptrace attach.
*
/
if
(!ret)
arch_ptrace_attach(child);
goto out_put_task_struct;
}
ret
=
ptrace_check_attach(child, request
=
=
PTRACE_KILL ||
request
=
=
PTRACE_INTERRUPT);
if
(ret <
0
)
goto out_put_task_struct;
ret
=
arch_ptrace(child, request, addr, data);
if
(ret || request !
=
PTRACE_DETACH)
ptrace_unfreeze_traced(child);
out_put_task_struct:
put_task_struct(child);
out:
return
ret;
}
SYSCALL_DEFINE4(ptrace,
long
, request,
long
, pid, unsigned
long
, addr,unsigned
long
, data)
{
struct task_struct
*
child;
long
ret;
if
(request
=
=
PTRACE_TRACEME) {
ret
=
ptrace_traceme();
if
(!ret)
arch_ptrace_attach(current);
goto out;
}
child
=
find_get_task_by_vpid(pid);
if
(!child) {
ret
=
-
ESRCH;
goto out;
}
if
(request
=
=
PTRACE_ATTACH || request
=
=
PTRACE_SEIZE) {
ret
=
ptrace_attach(child, request, addr, data);
/
*
*
Some architectures need to do book
-
keeping after
*
a ptrace attach.
*
/
if
(!ret)
arch_ptrace_attach(child);
goto out_put_task_struct;
}
ret
=
ptrace_check_attach(child, request
=
=
PTRACE_KILL ||
request
=
=
PTRACE_INTERRUPT);
if
(ret <
0
)
goto out_put_task_struct;
ret
=
arch_ptrace(child, request, addr, data);
if
(ret || request !
=
PTRACE_DETACH)
ptrace_unfreeze_traced(child);
out_put_task_struct:
put_task_struct(child);
out:
return
ret;
}
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
/
*
*
The asmlinkage stub
is
aliased to a function named __se_sys_
*
() which
*
sign
-
extends
32
-
bit ints to longs whenever needed. The actual work
is
*
done within __do_sys_
*
().
*
/
__diag_push(); \
__diag_ignore(GCC,
8
,
"-Wattribute-alias"
, \
"Type aliasing is used to sanitize syscall arguments"
);\
asmlinkage
long
sys
__attribute__((alias(__stringify(__se_sys
ALLOW_ERROR_INJECTION(sys
static inline
long
__do_sys
asmlinkage
long
__se_sys
asmlinkage
long
__se_sys
{ \
long
ret
=
__do_sys
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return
ret; \
} \
__diag_pop(); \
static inline
long
__do_sys
SYSCALL_METADATA(sname, x, __VA_ARGS__) \
__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
/
*
*
The asmlinkage stub
is
aliased to a function named __se_sys_
*
() which
*
sign
-
extends
32
-
bit ints to longs whenever needed. The actual work
is
*
done within __do_sys_
*
().
*
/
__diag_push(); \
__diag_ignore(GCC,
8
,
"-Wattribute-alias"
, \
"Type aliasing is used to sanitize syscall arguments"
);\
asmlinkage
long
sys
__attribute__((alias(__stringify(__se_sys
ALLOW_ERROR_INJECTION(sys
static inline
long
__do_sys
asmlinkage
long
__se_sys
asmlinkage
long
__se_sys
{ \
long
ret
=
__do_sys
__MAP(x,__SC_TEST,__VA_ARGS__); \
__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__)); \
return
ret; \
} \
__diag_pop(); \
static inline
long
__do_sys
/
*
kernel
/
ptrace.c
*
/
asmlinkage
long
sys_ptrace(
long
request,
long
pid, unsigned
long
addr,
unsigned
long
data);
/
*
kernel
/
ptrace.c
*
/
asmlinkage
long
sys_ptrace(
long
request,
long
pid, unsigned
long
addr,
unsigned
long
data);
SYSCALL_DEFINE4(ptrace,
long
, request,
long
, pid, unsigned
long
, addr, unsigned
long
, data)
-
-
> SYSCALL_DEFINEx(
4
, _ptrace, __VA_ARGS__)
-
-
> __SYSCALL_DEFINEx(
4
, __ptrace, __VA_ARGS__)
asmlinkage
long
sys
-
-
> asmlinkage
long
sys_ptrace(__MAP(
4
,__SC_DECL,__VA_ARGS__))
SYSCALL_DEFINE4(ptrace,
long
, request,
long
, pid, unsigned
long
, addr, unsigned
long
, data)
-
-
> SYSCALL_DEFINEx(
4
, _ptrace, __VA_ARGS__)
-
-
> __SYSCALL_DEFINEx(
4
, __ptrace, __VA_ARGS__)
asmlinkage
long
sys
-
-
> asmlinkage
long
sys_ptrace(__MAP(
4
,__SC_DECL,__VA_ARGS__))
/
*
*
__MAP
-
apply
a macro to syscall arguments
*
__MAP(n, m, t1, a1, t2, a2, ..., tn, an) will expand to
*
m(t1, a1), m(t2, a2), ..., m(tn, an)
*
The first argument must be equal to the amount of
type
/
name
*
pairs given. Note that this
list
of pairs (i.e. the arguments
*
of __MAP starting at the third one)
is
in
the same
format
as
*
for
SYSCALL_DEFINE<n>
/
COMPAT_SYSCALL_DEFINE<n>
*
/
/
*
*
__MAP
-
apply
a macro to syscall arguments
*
__MAP(n, m, t1, a1, t2, a2, ..., tn, an) will expand to
*
m(t1, a1), m(t2, a2), ..., m(tn, an)
*
The first argument must be equal to the amount of
type
/
name
*
pairs given. Note that this
list
of pairs (i.e. the arguments
*
of __MAP starting at the third one)
is
in
the same
format
as
*
for
SYSCALL_DEFINE<n>
/
COMPAT_SYSCALL_DEFINE<n>
*
/
__MAP(
4
,__SC_DECL,
long
request,
long
pid, unsigned
long
addr,
unsigned
long
data)
-
-
> __MAP4(__SC_DECL,
long
, request,
long
, pid, unsigned
long
, addr,
unsigned
long
, data)
-
-
> __SC_DECL(
long
, request), __MAP3(__SC_DECL, __VA_ARGS__)
__MAP3(__SC_DECL,
long
, pid, unsigned
long
, addr, unsigned
long
, data)
-
-
> __SC_DECL(
long
, pid), __MAP2(__SC_DECL, unsigned
long
, addr, unsigned
long
, data)
-
-
>__SC_DECL(unsigned
long
, addr), __MAP1(__SC_DECL, __VA_ARGS__)
unsigned
long
addr, __SC_DECL(unsigned
long
, data)
-
-
> unsigned
long
data
long
pid, __SC_DECL(unsigned
long
, addr), __MAP1(__SC_DECL, __VA_ARGS__)
-
-
>
long
pid, unsigned
long
addr, unsigned
long
data
-
-
>
long
request, __SC_DECL(
long
, pid), __MAP2(__SC_DECL, __VA_ARGS__)
-
-
>
long
request,
long
pid, unsigned
long
addr, unsigned
long
data
__MAP(
4
,__SC_DECL,
long
request,
long
pid, unsigned
long
addr,
unsigned
long
data)
-
-
> __MAP4(__SC_DECL,
long
, request,
long
, pid, unsigned
long
, addr,
unsigned
long
, data)
-
-
> __SC_DECL(
long
, request), __MAP3(__SC_DECL, __VA_ARGS__)
__MAP3(__SC_DECL,
long
, pid, unsigned
long
, addr, unsigned
long
, data)
-
-
> __SC_DECL(
long
, pid), __MAP2(__SC_DECL, unsigned
long
, addr, unsigned
long
, data)
-
-
>__SC_DECL(unsigned
long
, addr), __MAP1(__SC_DECL, __VA_ARGS__)
unsigned
long
addr, __SC_DECL(unsigned
long
, data)
-
-
> unsigned
long
data
long
pid, __SC_DECL(unsigned
long
, addr), __MAP1(__SC_DECL, __VA_ARGS__)
-
-
>
long
pid, unsigned
long
addr, unsigned
long
data
-
-
>
long
request, __SC_DECL(
long
, pid), __MAP2(__SC_DECL, __VA_ARGS__)
-
-
>
long
request,
long
pid, unsigned
long
addr, unsigned
long
data
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2021-3-5 14:38
被有毒编辑
,原因: