文中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 pathchar 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