前言
最近在维护公司的一个服务X,服务X因为协议设计的不够安全,存在被攻击的风险,所以修改协议提升安全性就显得势在必行了。然鹅,该项目涉及到多个版本,将改动合入到各个版本引起的开发及测试工作量颇大,最终讨论后决定通过动态库注入的方式进行修改。
何为动态库注入
动态库注入是指在程序启动或运行的时候,通过某种手段加载另一套接口库,替换原有依赖库中的函数。这样可以达到改变程序功能而又不对原有代码进行修改的目的。
如何做
linux下有一个环境变量叫LD_PRELOAD,动态链接器在载入一个程序所需的所有动态库之前,会先载入LD_PRELOAD环境变量所指定的动态库。运用这个机制,我们可以替换已有动态库中的方法,加入我们自己的逻辑,从而改变程序的执行行为。不过该方法只对动态链接的程序有效,对静态链接的程序无效。
看这样一个简单的例子,我写了一个main.c:
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%d\n", atoi("123"));
return 0;
}
该main函数简单的调用libc库中的atoi()函数将字符串“123”转换为int型输出。直接gcc main.c得到a.out编译运行,显然我们可以猜到结果:
通过ldd命令可以查看该二进制依赖的库:ldd a.out
通过命令strings /lib/x86_64-linux-gnu/libc.so.6 | grep atoi可以看到atoi()函数的符号就在其中:
现在让我们重新实现一个自己的atoi(),myfun.c,让其固定的返回456的数值:
int atoi(const char *nptr)
{
return 456;
}
然后将其编译成动态库:
在程序运行的命令前设置预加载库的路径为我们刚刚创建的动态库:
可以看到,我们已经成功的替换了libc中的atoi()函数,此时的库依赖顺序已经变成了这样的:
由于动态链接器是按照库的载入顺序进行符号解析的,当myfun.so首先载入之后,动态链接器已经找到了atoi()函数,所以会忽略libc库中的atoi()实现。
最后
我对服务X的改动也是通过LD_PRELOAD的方式预先载入新的库,在新的库中重新实现了recv()和send()函数,在判断是收发指定的协议包时,对其外层增加了一层封装。这样一来,只需一份代码,在多个环境下编译即可通用。
这种通过预加载的方式需要重启服务,其实还有另一种方式,可以不用重启服务就可以做到动态注入,其原理是修改了运行环境的上下文,具体实现还是比较复杂的,后面如果找到具体的实现再补上。