文中code https://gitee.com/bbjg001/darcy_common/tree/master/io_hook
需求引入
最近工作需要,需要验证一下我们的服务在硬盘故障下的鲁棒性。
从同事大佬哪里了解到hook技术,可以通过LD_PRELOAD
这个环境变量拦截依赖库的调用链,将对标准库函数的调用转移到自己自定义的函数,然后返回自定义的错误代码。
使用方式
export LD_PRELOAD=hook_lib.so
./main
# hook_lib.so是自行编译的用来拦截的so文件
# ./main是要运行的二进制文件
一个简单的例子
有这样一个简单的main函数
// main.cpp
#include <fcntl.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
int main()
{
int rfd = open("test.txt", O_RDONLY);
std::cout << "open(), with ret: " << rfd << ", err(" << errno << "): " << strerror(errno) << std::endl;
close(rfd);
}
如果test.txt不存在,输出是这样的
如果test.txt是一个存在的正常文件,输出
下面自定义open函数,通过hook拦截调用链调用自己的open函数,找到open函数的定义
自定义open函数,注意函数传参要与原open函数一致
// hook_lib.cpp
#include <unistd.h>
#include <iostream>
#include <dlfcn.h>
#include <fcntl.h>
typedef int(*OPEN)(const char *__path, int __oflag, ...);
int open(const char *__path, int __oflag, ...){
std::cout << "!!! open函数被拦截了" << std::endl;
errno = 2;
return -1;
}
正常编译并运行main.cpp
g++ main.cpp -o main && ./main
输出是正常的
下面拦截注入自己的open函数
# 把自己的函数文件编译成.so文件
g++ --shared -fPIC hook_lib.cpp -o hook_lib.so -ldl
# 通过LD_PRELOAD拦截调用链启动main函数
LD_PRELOAD=./hook_lib.so ./main
将hook函数的文件编译成.so文件
g++ --shared -fPIC hook_lib.cpp -o hook_lib.so -ldl
在启动时通过LD_PRELOAD指定hook的库文件
g++ main.cpp -o main
LD_PRELOAD=./hook_lib.so ./main
进一步的做更多自定义的逻辑
这次以write函数为例
返回正常的write函数
可以定义在某些情况下返回错误码,某些情况下返回正常的write函数。这里通过随机概率返回两者。
hook逻辑
// hook_lib
extern ssize_t std_write (int __fd, __const void *__buf, size_t __n) {
static void *handle = NULL;
static WRITE old_write = NULL;
if (!handle) {
handle = dlopen("libc.so.6", RTLD_LAZY);
old_write = (WRITE)dlsym(handle, "write");
}
return old_write(__fd, __buf, __n);
}
// 模拟的write函数
extern ssize_t write (int __fd, __const void *__buf, size_t __n) {
if (rand() % 100 / 100.0 > 0.5) {
errno = 2;
return -1;
}
return std_write(__fd, __buf, __n);
}
main函数
// main
int main(int argc, char *argv[]){
srand(time(NULL));
const char *f_path = "test.txt";
int fd = open(f_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
for (int i = 0; i < 10; i++){
int ret = write(fd, "HelloWorld", 10);
if (ret < 0){
std::cout << "write(), with ret: " << ret << ", err_info: " << strerror(errno) << std::endl;
}else{
std::cout << "write(), with ret: " << ret << std::endl;
}
}
close(fd);
return 0;
}
执行结果
$ LD_PRELOAD=./hook_lib.so ./main
write(), with ret: -1, err_info: No such file or directory
write(), with ret: 10
write(), with ret: -1, err_info: No such file or directory
write(), with ret: -1, err_info: No such file or directory
write(), with ret: 10
write(), with ret: -1, err_info: No such file or directory
write(), with ret: -1, err_info: No such file or directory
write(), with ret: 10
write(), with ret: -1, err_info: No such file or directory
write(), with ret: 10
控制注入异常的path
hook逻辑
// hook_lib
// 检查当前操作的文件是否是要注入异常的文件
bool is_current_path(int fd, std::string path){
if(path==""){
return false;
}
// get current path
char buf[256] = {'\0'};
char _file_path[256] = {'\0'};
std::string file_path;
snprintf(buf, sizeof (buf), "/proc/self/fd/%d", fd);
if (readlink(buf, _file_path, sizeof(_file_path) - 1) != -1) {
file_path = _file_path;
}
if(file_path.find(path) != std::string::npos){ // 路径中包含${path}即被命中
return true;
}
return false;
}
extern ssize_t write (int __fd, __const void *__buf, size_t __n) {
if (is_current_path(__fd, "test")) {
errno = 2;
return -1;
}
return std_write(__fd, __buf, __n);
}
main函数
// main
int main(int argc, char *argv[]){
const char *f_path = argv[1];
int fd = open(f_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
int ret = write(fd, "HelloWorld", 10);
if (ret < 0){
std::cout << "write(), with ret: " << ret << ", err_info: " << strerror(errno) << std::endl;
}else {
std::cout << "write(), with ret: " << ret << std::endl;
}
close(fd);
return 0;
}
执行结果
$ LD_PRELOAD=./hook_lib.so ./main test.txt
write(), with ret: -1, err_info: No such file or directory
$ LD_PRELOAD=./hook_lib.so ./main newfile.txt
write(), with ret: 10
延时返回
这里比较简单不再做代码示例
sleep(time_s); // 秒
usleep(time_ms); // 微秒
动态控制异常注入
希望能从第三方位置读取配置,通过变更配置动态的对指定path注入指定的错误(码)类型。
从文件获得配置
hook逻辑
// hook_lib
void get_ctrl_var_file(std::string *path, int *eno, int *sleep_time){
std::ifstream ifs("conf.txt");
ifs >> *path;
ifs >> *eno;
ifs >> *sleep_time;
ifs.close();
}
extern ssize_t write (int __fd, __const void *__buf, size_t __n) {
std::string epath;
int eno, ehang_time;
get_ctrl_var_file(&epath, &eno, &ehang_time);
if (is_current_path(__fd, epath)) {
errno = eno;
hang_sleep(ehang_time);
return -1;
}
return std_write(__fd, __buf, __n);
}
main函数
// main
int main(int argc, char *argv[]){
const char *f_path = argv[1];
int fd = open(f_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
int ret = write(fd, "HelloWorld", 10);
if (ret < 0){
std::cout << "write(), with ret: " << ret << ", err_info: " << strerror(errno) << std::endl;
}else {
std::cout << "write(), with ret: " << ret << std::endl;
}
close(fd);
return 0;
}
conf.txt
test.txt 2 1000000
执行结果
$ LD_PRELOAD=./hook_lib.so ./main test.txt
write(), with ret: -1, err_info: No such file or directory
$ LD_PRELOAD=./hook_lib.so ./main newfile.txt
write(), with ret: 10
从redis获得配置
hook逻辑
#include <hiredis/hiredis.h>
// hook_lib
void get_ctrl_var_redis(std::string *path, int *eno, int *sleep_time){
redisContext *conn = redisConnect("127.0.0.1", 6379);
if(conn != NULL && conn->err)
{
printf("connection error: %s\n",conn->errstr);
return;
}
redisReply *reply = (redisReply*)redisCommand(conn,"get %s", "/hook/write/epath");
*path = reply->str;
reply = (redisReply*)redisCommand(conn,"get %s", "/hook/write/eno");
*eno = std::atoi(reply->str);
reply = (redisReply*)redisCommand(conn,"get %s", "/hook/write/ehang");
*sleep_time = std::atoi(reply->str);
freeReplyObject(reply);
redisFree(conn);
}
extern ssize_t write (int __fd, __const void *__buf, size_t __n) {
std::string epath;
int eno, ehang_time;
get_ctrl_var_redis(&epath, &eno, &ehang_time);
if (is_current_path(__fd, epath)) {
errno = eno;
hang_sleep(ehang_time);
return -1;
}
return std_write(__fd, __buf, __n);
}
main函数
// main
int main(int argc, char *argv[]){
const char *f_path = argv[1];
int fd = open(f_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
int ret = write(fd, "HelloWorld", 10);
if (ret < 0){
std::cout << "write(), with ret: " << ret << ", err_info: " << strerror(errno) << std::endl;
}else {
std::cout << "write(), with ret: " << ret << std::endl;
}
close(fd);
return 0;
}
在redis中添加如下变量
set /hook/write/epath test.txt
set /hook/write/eno 5
set /hook/write/ehang 1000000
执行结果
$ LD_PRELOAD=./hook_lib.so ./main test.txt
write(), with ret: -1, err_info: Input/output error
$ LD_PRELOAD=./hook_lib.so ./main newfile.txt
write(), with ret: 10
in mac os
在mac os中需要使用其他的环境变量进行注入,简单试了下没能成功,抛砖引玉
https://stackoverflow.com/questions/34114587/dyld-library-path-dyld-insert-libraries-not-working
参考
https://blog.51cto.com/u_15703183/5464438
https://sq.sf.163.com/blog/article/173506648836333568
https://xz.aliyun.com/t/6883
https://www.cnblogs.com/wainiwann/p/3340277.html