Chinaunix

标题: 使用kvm接口探察内核空间的方法 [打印本页]

作者: 雨丝风片    时间: 2006-04-30 09:49
标题: 使用kvm接口探察内核空间的方法
我用FreeBSD提供的内核内存接口kvm写了一个例子。作为示范,这个简单的例子仅仅打印了内核ipv4 radix路由表中的中间根节点的内容,只需稍加扩展,即可打印完整的路由表内容,相当于netstat命令。

使用kvm按符号地址探察内核的一些要点:
1、用kvm_openfiles()函数获取后续操作所需的文件描述符。
2、用kvm_nlist()函数获取所关心的符号在内核中的地址。
3、用kvm_read()函数逐步将所关心的内容从内核空间复制到用户空间。

只要你知道内核中的数据是如何组织的,就可以通过kvm接口一步一步地把它挖出来。我原来分析内核路由表的时候用的是内核调试的方法,相比之下,kvm要简单方便多了。详细资料请参考man 3 kvm。

在编译的时候,需要使用-lkvm选项链接kvm库。另外,这个程序执行的时候需要root权限。

  1. #include <fcntl.h>
  2. #include <kvm.h>
  3. #include <sys/param.h>
  4. #include <sys/socket.h>
  5. #include <net/route.h>

  6. /* 这个数组用于向kvm_nlist()函数传递需要查找其地址的所有内核符号,
  7. * 数组最后一个元素须设为NULL。
  8. */
  9. struct nlist nl[] = {
  10.         { "_rt_tables"},
  11.         { NULL },
  12. };

  13. /* 这个数组用于复制内核中的rt_tables[]数组的内容 */
  14. struct  radix_node_head *rt_tables[AF_MAX+1];

  15. int main(void)
  16. {
  17.     kvm_t *kvmd;
  18.     char buf[100];
  19.     struct radix_node_head v4head;
  20.     struct radix_node rnode;

  21.     /* 获取kvm文件描述符 */
  22.     if ((kvmd = kvm_openfiles(NULL, NULL, NULL, O_RDONLY, buf)) == NULL) {
  23.         printf("kvm_openfiles fail! %s\n", kvm_geterr(kvmd));
  24.         exit(0);
  25.     }

  26.     /* 获取内核中的rt_tables符号的地址 */
  27.     if (kvm_nlist(kvmd, nl) < 0) {
  28.         printf("kvm_nlist fail! %s\n", kvm_geterr(kvmd));
  29.         exit(0);
  30.     }

  31.     /* 将内核中的路由表头指针数组复制到rt_tables[]指针数组中 */
  32.     if (kvm_read(kvmd, nl[0].n_value, &rt_tables, sizeof(rt_tables))
  33.             != sizeof(rt_tables)) {
  34.         printf("kvm_read rt_tables fail! %s\n", kvm_geterr(kvmd));
  35.         exit(0);
  36.     }

  37.     /* 将内核中的ipv4路由表头节点复制到v4head结构体中 */
  38.     if (kvm_read(kvmd, (int)rt_tables[AF_INET], &v4head, sizeof(v4head))
  39.             != sizeof(v4head)) {
  40.         printf("kvm_read v4 route headfail! %s\n", kvm_geterr(kvmd));
  41.         exit(0);
  42.     }

  43.     printf("v4head.rnh_treetop = %p\n", v4head.rnh_treetop);

  44.     /* 将内核中的ipv4路由表的中间根节点的内容复制到rnode结构体中 */
  45.     if (kvm_read(kvmd, (int)v4head.rnh_treetop, &rnode, sizeof(rnode))
  46.             != sizeof(rnode)) {
  47.         printf("kvm_read v4 root node fail! %s\n", kvm_geterr(kvmd));
  48.         exit(0);
  49.     }

  50.     printf("rnode.rn_mklist = %p\n", rnode.rn_mklist);
  51.     printf("rnode.rn_parent = %p\n", rnode.rn_parent);
  52.     printf("rnode.rn_bit = %d\n", rnode.rn_bit);
  53.     printf("rnode.rn_bmask = %x\n", rnode.rn_bmask);
  54.     printf("rnode.rn_flags = %x\n", rnode.rn_flags);
  55.     printf("rnode.rn_offset = %d\n", rnode.rn_offset);
  56.     printf("rnode.rn_left = %p\n", rnode.rn_left);
  57.     printf("rnode.rn_right = %p\n", rnode.rn_right);
  58. }
复制代码


打印结果示例:

v4head.rnh_treetop = 0xc1afc24c
rnode.rn_mklist = 0xc1afb4b0
rnode.rn_parent = 0xc1afc24c
rnode.rn_bit = 32
rnode.rn_bmask = ffffff80
rnode.rn_flags = 6
rnode.rn_offset = 4
rnode.rn_left = 0xc1c5cc78
rnode.rn_right = 0xc1afc264


v4路由表头的rnh_treetop字段应该指向的是radix树的中间根节点,这里是0xc1afc24c。而我们下面所打印的也正是中间根节点,它的rn_parent字段应该指向它自己,即0xc1afc24c,结果正确。这个根节点的rn_bit表示它的bit测试位置,这里等于32,正是ipv4地址在socket地址结构体中的实际起始bit偏移量。字节掩码为0x80,也表示它测试的是最左面的那个bit。rn_flags等于6,这是由RNF_ROOT(2)和RNF_ACTIVE(4)相或的结果。rn_offset为4,表示ipv4地址在socket地址结构体中的实际起始位置的字节偏移为4,这和前面的32bit是相对应的。从上面的分析可以看出,通过kvm接口读出的ipv4路由表中间根节点的内容完全正确,这确实是一个探察内核空间的好方法。


[ 本帖最后由 雨丝风片 于 2006-4-30 13:07 编辑 ]
作者: congli    时间: 2006-04-30 10:54
收藏!
学习ing
作者: gvim    时间: 2006-04-30 12:14
还没怎么看懂。对kvm还不熟悉,嘿嘿
作者: 雨丝风片    时间: 2006-04-30 13:01
又写了一个探察指定进程的kinfo_proc结构体及其地址空间结构的例子。与前一个例子的不同之处就是这里要使用kvm_getprocs()函数来获得指定进程的kinfo_proc结构体,并由此开始探察进程的地址空间结构。目标进程是在命令行上用pid指定的。

同样,运行这个程序也需要root权限。

  1. #include <fcntl.h>
  2. #include <kvm.h>
  3. #include <sys/param.h>
  4. #include <sys/sysctl.h>
  5. #include <sys/user.h>

  6. int main(int argc, char *argv[])
  7. {
  8.     kvm_t *kvmd;
  9.     char buf[100];
  10.     int nentries;
  11.     struct kinfo_proc *kp;
  12.     struct  vmspace pspace;

  13.     if (argc <= 1) {
  14.         printf("PID, please!\n");
  15.         exit(0);
  16.     }

  17.     /* 获取kvm文件描述符 */
  18.     if ((kvmd = kvm_openfiles(NULL, NULL, NULL, O_RDONLY, buf)) == NULL) {
  19.         printf("kvm_openfiles fail! %s\n", kvm_geterr(kvmd));
  20.         exit(0);
  21.     }

  22.     /* 获取指定进程的kinfo_proc结构体指针 */
  23.     if ((kp = kvm_getprocs(kvmd, KERN_PROC_PID, atoi(argv[1]),
  24.             &nentries)) == NULL) {
  25.         printf("kvm_getprocs fail! %s\n", kvm_geterr(kvmd));
  26.         exit(0);
  27.     }

  28.     printf("kp->ki_pid = %d\n", kp->ki_pid);
  29.     printf("kp->ki_uid = %d\n", kp->ki_uid);
  30.     printf("kp->ki_size = %d\n", kp->ki_size);
  31.     printf("kp->ki_rssize = %d\n", kp->ki_rssize);


  32.     /* 读出指定进程的vmspace结构体的内容 */
  33.     if (kvm_read(kvmd, (int)kp->ki_vmspace, &pspace, sizeof(pspace))
  34.             != sizeof(pspace)) {
  35.         printf("kvm_read fail! %s\n", kvm_geterr(kvmd));
  36.         exit(0);
  37.     }

  38.     printf("pspace.vm_swrss = %d\n", pspace.vm_swrss);
  39.     printf("pspace.vm_tsize = %d\n", pspace.vm_tsize);
  40.     printf("pspace.vm_dsize = %d\n", pspace.vm_dsize);
  41.     printf("pspace.vm_ssize = %d\n", pspace.vm_ssize);
  42.     printf("pspace.vm_taddr = %p\n", pspace.vm_taddr);
  43.     printf("pspace.vm_daddr = %p\n", pspace.vm_daddr);
  44.     printf("pspace.vm_maxsaddr = %p\n", pspace.vm_maxsaddr);
  45. }
复制代码


使用示例:
假设系统中有一个正在运行的用户程序a.out,ps输出如下:

  1. # ps -a -v
  2.   PID STAT      TIME  SL  RE PAGEIN   VSZ   RSS   LIM TSIZ %CPU %MEM COMMAND
  3. 4320 R+    57:13.84   0 127      0  1168   444     -    4 98.0  0.1 ./a.out
  4. #
复制代码


我们以指定进程号4320的方式运行前面给出的例子,输出结果如下:

kp->ki_pid = 4320
kp->ki_uid = 1001
kp->ki_size = 1196032
kp->ki_rssize = 111
pspace.vm_swrss = 0
pspace.vm_tsize = 1
pspace.vm_dsize = 1
pspace.vm_ssize = 32
pspace.vm_taddr = 0x8048000
pspace.vm_daddr = 0x8049000
pspace.vm_maxsaddr = 0xbbc00000


我们可以看到,例程所找到的ki_pid就是我们指定的4320,而其uid则是1001,正是运行a.out程序的用户id,这说明我们成功找到了4320这个进程的kinfo_proc结构体。ki_size显示为1196032,这是一个以字节为单位的数字,换算成KB就是1168,和ps输出中的VSZ字段一致。ki_rssize显示为111,这是一个以页面为单位的数字,换算成KB就是444,和ps输出中的RSS字段一致。后面的打印则是进程的vmspace结构体中的一些内容,从vmspace结构体开始,我们就可以探察构成进程地址空间的全部管理结构的内容了。

[ 本帖最后由 雨丝风片 于 2006-4-30 13:02 编辑 ]
作者: 雨丝风片    时间: 2006-04-30 13:06
原帖由 gvim 于 2006-4-30 12:14 发表
还没怎么看懂。对kvm还不熟悉,嘿嘿


呵呵,俺也是昨天下午看着man手册慢慢捣鼓出来的,这可解决了大问题,
作者: mingyanguo    时间: 2006-04-30 14:48
我记得可以通过kvm不用KLD的形式给kernel在运行时打补丁,这对于那些禁止加载KLD的环境很有用。
其实,我的看法是,kvm应该就是对内核地址空间的一种抽象,根据符号表查找符号地址然后读写kvm对应与直接读写内核的地址空间相应的地址。
作者: 雨丝风片    时间: 2006-04-30 14:59
原帖由 mingyanguo 于 2006-4-30 14:48 发表
我记得可以通过kvm不用KLD的形式给kernel在运行时打补丁,这对于那些禁止加载KLD的环境很有用。
其实,我的看法是,kvm应该就是对内核地址空间的一种抽象,根据符号表查找符号地址然后读写kvm对应与直接读写内核 ...


那应该就是使用kvm_write()函数了,这玩意儿至少gdb用得着,

kvm就像是蒙在内核外面的面纱,里面的东西许看不许摸,实在想摸就得通过kvm接口来中转一下。
作者: duanjigang    时间: 2006-05-05 18:15
顶拉,在top里面看过kvm还不太懂,学习一下
作者: 雨丝风片    时间: 2006-05-05 18:36
原帖由 duanjigang 于 2006-5-5 18:15 发表
顶拉,在top里面看过kvm还不太懂,学习一下


top、ps以及netstat这些工具程序实际上使用的就是kvm接口,把上面的例子扩展一下就能写出自己的工具程序,打印出所需的内核数据结构的内容。
作者: newcmd    时间: 2006-07-06 01:04
linux有没有这个接口阿,找了半天都没找到。
作者: 雨丝风片    时间: 2006-07-06 08:49
原帖由 newcmd 于 2006-7-6 01:04 发表
linux有没有这个接口阿,找了半天都没找到。


不清楚有没有,但至少可以去读/proc。

下面这个帖子可以作为参考
http://bbs.chinaunix.net/viewthr ... &extra=page%3D3
作者: newcmd    时间: 2006-07-06 10:59
原帖由 雨丝风片 于 2006-7-6 08:49 发表


不清楚有没有,但至少可以去读/proc。

下面这个帖子可以作为参考
http://bbs.chinaunix.net/viewthr ... &extra=page%3D3

感谢呵!原来linux是这样解决的。
作者: 大大狗    时间: 2006-07-17 10:45
嗯,收藏一下,好好学习学习,回家俺也捅咕捅咕~~~~~




欢迎光临 Chinaunix (http://bbs.chinaunix.net/) Powered by Discuz! X3.2