问题描述
在系统启动的时候,/var/run、/run 目录的创建顺序影响某 systemd 服务的正常启动,需要确定是谁创建了这两个目录以定位问题。
由于此问题在 systemd 服务启动过程中触发,不能使用 auditd 等工具监测,于是想到通过 hook c 库中目录创建与软链接创建相关函数来记录日志以找到犯罪服务,在本文中记录一下。
hook demo 源码
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <unistd.h>
#include <fcntl.h> /* Definition of AT_* constants */
#include <execinfo.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#define MAXPATH 1024
int apply_filter(const char* func, const char *pathname, int ret)
{
char buf[MAXPATH];
int pid = getpid();
memset(buf, 0x0, sizeof(buf));
sprintf(buf, "/filter_path.sh %s %s %d %d", func, pathname, pid, ret);
system(buf);
}
int mkdir(const char *pathname, mode_t mode)
{
int (*old_mkdir)(const char *, mode_t);
int ret;
old_mkdir = dlsym(RTLD_NEXT, "mkdir");
ret = old_mkdir(pathname, mode);
apply_filter(__FUNCTION__, pathname, ret);
return ret;
}
int mkdirat(int dirfd, const char *pathname, mode_t mode)
{
int (*old_mkdirat)(int, const char *, mode_t);
int ret;
old_mkdirat = dlsym(RTLD_NEXT, "mkdirat");
ret = old_mkdirat(dirfd, pathname, mode);
apply_filter(__FUNCTION__, pathname, ret);
return ret;
}
int symlink(const char *target, const char *linkpath) {
int (*old_symlink)(const char*, const char*);
int ret;
old_symlink = dlsym(RTLD_NEXT, "symlink");
ret = old_symlink(target, linkpath);
apply_filter(__FUNCTION__, target, ret);
return ret;
}
int symlinkat(const char *target, int newdirfd, const char *linkpath)
{
int (*old_symlinkat)(const char*, int, const char*);
int ret;
old_symlinkat = dlsym(RTLD_NEXT, "symlinkat");
ret = old_symlinkat(target, newdirfd, linkpath);
apply_filter(__FUNCTION__, target, ret);
return ret;
}
上述代码 hook 了 mkdir 与 symlink 相关 c 库函数,主要逻辑如下:
- 在 hook 函数中先调用 dlsym 获取目标函数的地址
- 调用目标函数并获取返回值
- 插入一个 apply_filter 函数调用,此函数中调用用户编写的脚本并传入必要的参数信息
- 函数正常返回
在 c 语言中调用一个脚本,避免对源码不断的修改、编译以及二进制文件替换操作。
编译命令:
gcc -fPIC -shared hook_mkdir.c -o mkdir.so -ldl
部署方法:
拷贝到 /lib64 中,然后执行如下命令:echo /lib64/mkdir.so > /etc/ld.so.preload
过滤脚本内容示例
#!/bin/bash
FUNC=$1
PATHNAME=$2
pid=$3
ret=$4
program=$(readlink /proc/$pid/exe 2>/dev/null)
cmdline=$(cat /proc/$pid/cmdline 2>/dev/null | sed 's/\x0/ /g')
TIME=$(date)
echo "$TIME:pid:$pid $program execute $FUNC with argument $cmdline to create $PATHNAME with return value $ret" >> /tmp/mkdir.log
对于从 cmdline 文件中获取的字符串,由于其使用 ‘\0’ 分割,需要替换为空格以正常显示,sed 替换命令中使用的字符为 ‘\x0’ 而非 ‘\0’。
将上述脚本保存为 /filter_path.sh,使用前执行 chmod +x 给脚本添加可执行权限。
注意事项
在更新 preload hook so 时,先从 /etc/ld.so.preload 中移除项目,然后更新,直接更新可能存在问题。
如何判段是否生效?
使用 LD_DEBUG=”symbols” 环境变量执行 mkdir,有如下输出信息:
# LD_DEBUG="symbols" mkdir test 2>&1 | grep -A 2 'symbol=mkdir'
179361: symbol=mkdir; lookup in file=mkdir [0]
179361: symbol=mkdir; lookup in file=/lib64/mkdir.so [0]
179361: symbol=dlsym; lookup in file=mkdir [0]
179361: symbol=dlsym; lookup in file=/lib64/mkdir.so [0]
--
179361: symbol=mkdir; lookup in file=/lib/x86_64-linux-gnu/libselinux.so.1 [0]
179361: symbol=mkdir; lookup in file=/lib/x86_64-linux-gnu/libc.so.6 [0]
179361: symbol=apply_filter; lookup in file=mkdir [0]
179361: symbol=apply_filter; lookup in file=/lib64/mkdir.so [0]
第一次从 mkdir.so 中找到了 mkdir 符号,然后调用 dlsym 获取真实的 mkdir 符号位置,在 libc.so.6 中找到,有类似输出表明配置正常。
示例 log
/tmp# touch mkdir.log
/tmp# mkdir -p ./var/run; ln -s var/run run
/tmp# cat ./mkdir.log
Sun 16 Oct 2022 04:42:19 PM CST:pid:179414 /usr/bin/mkdir execute mkdir with argument mkdir -p ./var run to create var with return value -1
Sun 16 Oct 2022 04:42:19 PM CST:pid:179414 /usr/bin/mkdir execute mkdir with argument mkdir -p ./var/run to create run with return value 0
Sun 16 Oct 2022 04:42:19 PM CST:pid:179431 /usr/bin/ln execute symlinkat with argument ln -s var/run run to create var/run with return value 0
日志记录正常,测试通过。