使用snoopy进行execve/execv、connect、init_module hook

一、简述

Snoopy旨在通过提供已执行命令的日志来帮助系统管理员,它对用户和应用程序是完全透明,通过将它链接到程序中,以提供对execve()调用的封装,记录信息通过syslog完成。

Snoopy development has been migrated to github. Please follow the link “Snoopy Logger Web Site” below.

Snoopy is designed to aid a sysadmin by providing a log of commands executed. Snoopy is completely transparent to the user and applications. It is linked into programs to provide a wrapper around calls to execve(). Logging is done via syslog.

二、注意事项

  • Hook函数的覆盖完备性,对于Linux下的指令执行来说,有7个glibc的api都可实现指令执行功能,对这些API对要进行hook:
// 执行参数filename字符串所代表的文件路径,第二个参数是利用数组指针来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组,该函数属于内核级系统调用;
int execve(const char *filename, char *const argv[], char *const envp[]);
// 执行参数filename字符串所代表的文件路径,第二个参数代表执行该文件时传递的argv[0],argv[1].....最后一个参数必须用空指针NULL作结束,该函数需要调用execve的库函数;
int execl(const char *filename, const char *arg0, ... /* (char *)0 */ );
// 内核级系统调用
int execv(const char *filename, char *const argv[]);
// 内核级系统调用
int execle(const char *filename, const char *arg0, .../* (char *)0, char *const envp[] */ );
// 内核级系统调用
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
// 内核级系统调用
int execvp(const char *filename, char *const argv[]);
int fexecve(int fd, char *const argv[], char *const envp[]);
  • 系统中是否存在hook函数的重名覆盖问题,通常在以下场景下:
    • /etc/ld.so.preload中填写了多条.so加载条目;
    • 其他程序通过export LD_PRELOAD=..临时指定了待加载so的路径,在很多情况下,出于系统管理或者集群系统日志收集的目的,运维人员会向系统中注入.so文件,对特定function函数进行hook,这个时候,当我们注入的.so文件中的hook函数和原有的hook函数存在同名的情况,Linux会自动忽略之后载入了hook函数,这种情况我们称之为"共享对象全局符号介入";
  • 注入.so对特定函数进行hook要保持原始业务的兼容性,即处理完之后仍然需要执行原函数的调用,为了实现透明hook(完成业务逻辑的同时不影响正常的系统行为)、维持调用链,那么需要使用RTLD_NEXT
  • 尽量减小hook函数对原有调用逻辑的延时,hook操作本身也会产生一定的延时,我们需要尽量减少从函数入口到调用原函数这块的代码逻辑,尽量减少多余的执行时间;

三、代码实践

  • hook.c
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>

#include <stdlib.h>
#include <sys/types.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>

#include <netinet/in.h>
#include <linux/ip.h>
#include <linux/tcp.h>

#if defined(RTLD_NEXT)
# define REAL_LIBC RTLD_NEXT
#else
# define REAL_LIBC ((void *) -1L)
#endif

#define FN(ptr, type, name, args) ptr = (type (*)args)dlsym (REAL_LIBC, name)

int execve(const char *filename, char *const argv[], char *const envp[]) {
static int (*func)(const char *, char **, char **);
FN(func,int,"execve",(const char *, char **const, char **const));

// print the log
printf("filename: %s, argv[0]: %s, envp:%s\n", filename, argv[0], envp);

return (*func) (filename, (char**) argv, (char **) envp);
}

int execv(const char *filename, char *const argv[]) {
static int (*func)(const char *, char **);
FN(func,int,"execv", (const char *, char **const));

// print the log
printf("filename: %s, argv[0]: %s\n", filename, argv[0]);

return (*func) (filename, (char **) argv);
}

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
static int (*func)(int, const struct sockaddr *, socklen_t);
FN(func,int,"connect", (int, const struct sockaddr *, socklen_t));

/* print the log, 获取、打印参数信息的时候需要注意
* 1. 加锁
* 2. 拷贝到本地栈区变量中
* 3. 然后再打印
* 调试的时候发现直接获取打印会导致core dump */
printf("socket connect hooked!!\n");

//return (*func) (sockfd, (const struct sockaddr *) addr, (socklen_t)addrlen);
return (*func) (sockfd, addr, addrlen);
}

int init_module(void *module_image, unsigned long len, const char *param_values) {
static int (*func)(void *, unsigned long, const char *);
FN(func,int,"init_module",(void *, unsigned long, const char *));

// print the log, lkm的加载不需要取参数,只需要捕获事件本身即可
printf("lkm load hooked!!\n");

return (*func) ((void *)module_image, (unsigned long)len, (const char *)param_values);
}
  • 编译运行:
# 编译
gcc -fPIC -shared -o hook.so hook.c -ldl

# 运行
LD_PRELOAD=./hook.so nc www.baidu.com 80
  • 运行结果:
socket connect hooked!!
socket connect hooked!!
Author: bugwz
Link: https://bugwz.com/2019/01/01/snoopy/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.