本文为作者本科毕业设计相关内容的系列文章,记录从零基础到开发一个linux下软件行为检测程序(或者只是学习成果)的过程(详细笔记)。
前文连接
[linux下进程行为检测] 1.proc、sys文件系统
[linux下进程行为检测] 2.auditd、bpftrace工具了解
strace
strace可以用来监控用户空间进程和内核的交互,比如系统调用、信号传递、进程状态变更等。
strace 从内核接收信息,而且不需要以任何特殊的方式来构建内核。
lab.c
#include <stdio.h>
#include <stdlib.h>
int main(){
FILE *fp = fopen("./a.txt", "w");
fprintf(fp, "hello");
fclose(fp);
return 0;
}
strace ./lab
execve("./lab", ["./lab"], 0x7fff00a36c10 /* 53 vars */) = 0
brk(NULL) = 0x5621d7564000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=90100, ...}) = 0
mmap(NULL, 90100, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f0fe8b61000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\200\177\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1839168, ...}) = 0
...
close(3) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0fe8998000
arch_prctl(ARCH_SET_FS, 0x7f0fe8b60540) = 0
mprotect(0x7f0fe8b55000, 12288, PROT_READ) = 0
mprotect(0x5621d6cf3000, 4096, PROT_READ) = 0
mprotect(0x7f0fe8ba1000, 4096, PROT_READ) = 0
munmap(0x7f0fe8b61000, 90100) = 0
brk(NULL) = 0x5621d7564000
brk(0x5621d7585000) = 0x5621d7585000
openat(AT_FDCWD, "./a.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
write(3, "hello", 5) = 5
close(3) = 0
exit_group(0) = ?
+++ exited with 0 +++
ptrace
ptrace是一个函数接口
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
ptrace提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包括寄存器)的值。其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被系统标注为TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行。
demo示例
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <stdio.h>
int main()
{ pid_t child;
struct user* user_space = NULL;
long orig_eax;
child = fork();
if(child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", NULL);
}
else {
wait(NULL);
orig_eax = ptrace(PTRACE_PEEKUSER,child, &user_space->regs.orig_rax,NULL);
printf("The child made a ""system call %ld\n", orig_eax);
ptrace(PTRACE_CONT, child, NULL, NULL);
}
return 0;
}
PTRACE_TRACEME:本进程被其父进程所跟踪。
PTRACE_PEEKUSER:检查用户态内存区域,从用户区域中读取一个字节,偏移量为addr
PTRACE_CONT:继续运行
父进程 fork() 子进程,子进程中执行要trace的程序,在子进程调用exec()前,子进程需要先调用一次 ptrace,以 PTRACE_TRACEME 为参数告诉内核当前进程是traced状态,当子进程执行 execve() 之后,子进程会进入暂停状态,通过SIG_CHLD信号把控制权转给父进程。
父进程在fork()后调用了wait() 等待子进程,当 wait() 返回后,父进程查看子进程的寄存器或者对子进程做其它的事情。
当系统调用发生时,内核会把当前的寄存器中的内容(即系统调用的编号)保存到子进程的用户态代码段中,通过调用函数ptrace(PTRACE_PEEKUSER,…)来读取这个寄存器的值,拿到数据后通过ptrace(PTRACE_CONT,…)让子进程继续运行。