近期要做Linux下libvirt事件审计,原计划是分析libvirt的通信数据从而进一步分析libvirt事件。这怎么看都觉得工作量浩大,第一反应就是能不能偷懒。对于一般的审计事件,首先想到的是函数截获:遍历ELF文件的导出函数,然后替换之。顺带一提,现在安卓上的进程注入就这么做的,哪天有空了我也放一篇Linux ELF文件注入的文章。不过这个工程也不小(相比原计划已经很小了),而且我也没十足的把握稳定代码。于是,退而求其次想到标题中这种方法,此法,也是首次尝试。
1) 先来整理下当时的处理思路:libvirt提供了一个命令行客户端工具--virsh,通过virsh可以使用libvirt提供的功能,如创建域。首先,可以肯定的是libvirt是个成功的项目,所以,客户端代码肯定和实际虚拟化操作是分开的,他们的唯一结合处一定是调用libvirt某个动态库导出的函数,来找一下他们的契合处:
root@ubuntu:~# which virsh
/usr/local/bin/virsh
root@ubuntu:~# ldd /usr/local/bin/virsh
linux-gate.so.1 => (0xb770d000)
libvirt.so.0 => /usr/lib/libvirt.so.0 (0xb743d000)
libvirt-qemu.so.0 => /usr/lib/libvirt-qemu.so.0 (0xb7439000)
libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xb741d000)
可以看到virsh依赖于libvirt.so这个动态库。再来看看这个动态库是否提供了virsh命令行所需要的接口:
virsh创建域会调用virDomainCreateXML这个API,来看下virDomainCreateXML在virsh中的符号形式:
root@ubuntu:~# nm /usr/local/bin/virsh|grep virDomainCreateXML
U virDomainCreateXML@@LIBVIRT_0.5.0
nm显示virDomainCreateXML在virsh中是未解析的符号名,意味着,virsh中没有对应的实现,那必须在其他模块中实现否则都不会通过链接的步骤。可以猜测,virDomainCreateXML肯定在libvirt中实现(我看了源码,当然知道在这个so中实现。你不服,可在ldd virsh输出的所有动态库中搜索这个API)。
root@ubuntu:~# nm /usr/lib/libvirt.so.0 | grep virDomainCreateXML
000f7c40 T virDomainCreateXML
nm显示virDomainCreateXML的确在libvirt.so中实现。如此可以确定virsh以至于其他客户端工具,要使用libvirt提供的虚拟化操作,都依赖这个库的实现。后面都用LD_PRELOAD来截获对该动态库的调用了,以此来实现对libvirt的审计。
2) 扯了这么久,还没怎么利用LD_PRELOAD。这不,思路很重要么,至少看到这知道了审计事件时,可以用这种方式去实现么。好,先放个链接:http://www.linuxdiyf.com/linux/18311.html,这篇文章详细介绍了什么是LD_PRELOAD以及如何利用。但是,文章只做了一半,还有一半没做:就是,你把水截流了,还得把水放到下游去啊,要不然下游人民怎么生活!光简单粗暴的截获了strcmp,然后return 0这不是改的strcmp他妈都不认识他了么?为了实现strcmp的原有功能,这里我用到了dlopen/dlsym,用来加载动态库和获得导出函数。类似于windows的LoadLibrary和GetProcAddr。值得一提的事,linux的进程加载器也是使用这两个函数加载所依赖的so库。
下面来看下我对那篇文章代码的扩充:
hack.c
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
typedef int (*detour_strcmp)(const char*,const char*);
detour_strcmp g_Strcmp;
int strcmp(const char *s1, const char *s2)
{
int res=0;
printf("hack function invoked. s1=<%s> s2=<%s>\n", s1, s2);
g_Strcmp = dlsym(RTLD_NEXT, "strcmp");
if(g_Strcmp==NULL)
{
printf("load strcmp failed\n");
return 0;
}
res = (*g_Strcmp)(s1,s2);
printf("compare result:%d\n",res);
return res;
}
因为使用了dlopen和dlsym编译连接时,需要加上-ldl
gcc -shared -o hack.so hack.c -ldl
最后,来看下新strcmp的输出:
root@ubuntu:~/Desktop# export LD_PRELOAD="./hack.so"
root@ubuntu:~/Desktop# ./verifypasswd
usage: ./verifypasswd <password>/nroot@ubuntu:~/Desktop# ./verifypasswd 1
hack function invoked. s1=<password> s2=<1>/ncompare result:1
Invalid Password!/n
输出无效的密码!至少可以知道strcmp是被调用过了。