SystemTap追踪自己开发的内核模块




背景


看了动态追踪技术漫谈(https://blog.openresty.com.cn/cn/dynamic-tracing/)这篇文章之后,就想着拿systemtap追踪一下自己开发的内核模块。


然而,systemtap官方文档(https://sourceware.org/systemtap/documentation.html),对如何追踪内核模块的描述中,只是拿内核源码树中的驱动举例,比如:probe module(“ext3”).function(“*”) { },实际验证确实也很顺利(我的系统使用的是xfs文件驱动,stap命令执行后,随便vi一个文件,就能看到xfs_iread()函数被调用了,并列出了参数信息)

    SystemTap追踪自己开发的内核模块

    

然后,我写了一个最简单的驱动,test.c:


#include <linux/module.h>


void test(int n)
{
    printk("%s(), %d: %dn", __FUNCTION__, __LINE__, n);
}

static int __init test_init(void)
{
    test(100);
    printk("%s(), %dn", __FUNCTION__, __LINE__);
    return 0;
}

static void __exit test_exit(void)
{
    test(100);
    printk("%s(), %dn", __FUNCTION__, __LINE__);
}



module_init(test_init);
module_exit(test_exit);

MODULE_LICENSE("GPL");

    

Makefile:


ifneq ($(KERNELRELEASE),)
obj-m := test.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
endif

    

以及x.stap:


#!/usr/bin/env stap


probe begin {
  printf("beginn");
}

probe module("test").function("*").call {
  printf("%s -> %sn", thread_indent(1), probefunc())
}

probe module("test").function("*").return {
  printf("%s <- %sn", thread_indent(-1), probefunc())
}

    

接下来,编译test.c->加载test.ko->执行x.stap(此刻,我还在一个思维陷阱里,以为加载驱动,是stap追踪该驱动的前提)

    SystemTap追踪自己开发的内核模块





解决"stap x.stp"执行失败

    

我一开始认为,驱动都已经加载了,stap却还不认识,那问题应该出在test.ko的编译上,为了解决这个问题,兜兜绕绕了一大圈。

    

首先是百度、google了一遍,看了前几页的回答,都是说要安装内核debuginfo,不过我的系统,正好之前已经安装过内核debuginfo,而且我要追踪的是自己开发的驱动,按道理也不需要依赖内核debuginfo。


     SystemTap追踪自己开发的内核模块

    

然后到一些微信群里询问也无果,可能大佬都很忙,没空理我。

    

最后只能自己再翻翻官方文档,开始各种推测与尝试。

    

记忆中,之前看过一款动态追踪工具的原理,提到在编译被追踪程序时,gcc必须添加-pg编译选项,这会使gcc在每个函数入口,添加5条nop指令,从而预留5个字节,可以在动态追踪时,替换成”call mcount”的机器码,才能使在追踪点注入的代码有机会执行。想到这,就在Makefile里加了一行:

ccflags-y += -pg

    

结果,问题仍然存在,再回过头搜一下资料,得知ftrace才依赖-pg编译选项,systemtap底层依赖的是kprobe,它可以追踪任何地址处的指令,原理是将追踪地址的第一个字节,替换成0xCC(即”int 3″指令),利用中断机制实现的。

    

那么,和xfs驱动相比,除了是否已加载和编译选项之外,还有什么区别?

SystemTap追踪自己开发的内核模块

    

官方文档中的这么一句话,虽然只是阐述了一个客观情况,并没有表达,.ko文件一定要放在/lib/modules/$(uname -r)/目录,才能被追踪的意思,但是test.ko和xfs驱动相比,目前能想的的区别,也就这个了,所以就侥幸的试了一下,竟然成功了:


    SystemTap追踪自己开发的内核模块

    

并且,额外的惊喜是,”stat x.stp”的执行,并不依赖先”insmod test.ko”,也就是说,test_init()函数,也可以被追踪。不过想想也是,如果连内核模块加载函数都追踪不了,那systemtap还号称什么”利器”。





未显示函数名称

    

本来以为,接下来就可以尽情的畅游了。

    

然而,通过”insmod test.ko”和”rmmod test”分别触发test_init()和test_exit()执行,发现stap的打印内容是这样:


    SystemTap追踪自己开发的内核模块

    

其中,0xffffffffc037f000是test_init()函数的加载地址,0xffffffffc0876000是test_exit()函数的加载地址,这可以在test.ko卸载之前,通过以下3种方式证实:

    

① 查看test.ko节区的加载地址


        SystemTap追踪自己开发的内核模块

    

② 查看内核符号表中,属于test驱动的符号及其加载地址(不清楚为什么没看到”test_init”)


        SystemTap追踪自己开发的内核模块

    

③ 查看这两个内存地址的内容,与test_init()/test_exit()函数反汇编的机器码对比(这种方式要求系统安装了内核debuginfo)

        

test_exit()函数机器码:


        SystemTap追踪自己开发的内核模块

        

内存查看:

        

SystemTap追踪自己开发的内核模块


确认打印内容中,”->”之后是被追踪函数的地址之后,还存在另外3个疑问:

    

1. stap打印的为什么是函数地址,而不是函数名称?

        

这个可以通过将x.stp脚本中的probefunc(),替换成ppfunc()解决,同时也能避免以下问题3中的现象:


        SystemTap追踪自己开发的内核模块

    

2. test_init()和test_exit()函数都可以追踪了,test()函数为什么没被追踪到?


第4节专门介绍。

    

3. “<-“与”->”后面的地址不同,又代表什么地址?

        

解决问题2后,让函数多调用几层,就能看出,”->”后面是callee函数地址,”<-“后面是caller函数地址。





未追踪到test()函数

    

x.stp脚本明明追踪的是所有函数(function(“*”)),test()函数却没被追踪到。

    

首先尝试的是,在Makefile中加一行:


ccflags-y += -g

    

然而,仍然不能追踪test()函数。

    

执行”readelf -S test.ko”可以发现,不加-g编译,test.ko就已经包含debug_info节区了,节区数量也不比加了-g编译少:


    SystemTap追踪自己开发的内核模块

    

这时就想着看看,init_test()/test_exit()函数中,是怎么调用test()函数的:


SystemTap追踪自己开发的内核模块


可以看出,调用test()的call指令,在test_init()和test_exit()函数中的偏移,都是0x1e,那么0x1f处一定有对应的重定项(这里需要一点链接原理的知识,可以看看我写的”32位elf格式中的10种重定位类型(https://bbs.kanxue.com/thread-246373.htm)“):


    SystemTap追踪自己开发的内核模块

    

最终得出,0x1f处,原本应该填充为test()函数地址,但是却被直接填充为printk()函数的地址了,所以大致可以推测,可能由于test()除了调用一次printk(),其它什么也没干,编译时就被gcc优化成直接调用printk()了。

    

为了证实想法,在Makefile添加一行:

ccflags-y += -O0

    

再看重定项信息,就是test()函数了(重新编译后,调用test()的call指令,位于0x09偏移)


    SystemTap追踪自己开发的内核模块

    

并且可以看到test()可以被追踪了:


    SystemTap追踪自己开发的内核模块




无法获取test()函数的参数和局部变量值

    

搞到这里,估计大家都不想再有什么妖蛾子了,但是systemtap才不管你想不想!

    

加了-O0选项编译,可以追踪test()函数之后,我又油然而生了一个僭越的想法,便将x.stp改了,试图追踪到test()函数时,打印一下参数和局部变量值:


probe module("test").function("test").call {
  printf("%s -> %s, %s, %dn", thread_indent(1), ppfunc(), $$parms, $n)
}

    

结果却是:


    SystemTap追踪自己开发的内核模块

    

n的值明显不对,n等于100才对!

    

由于-O0给过我惊喜,加上如果优化级别为0都有问题,更何况更高的优化级别呢,所以我并没有第一时间想到它会害我,后来无意去掉”ccflags-y += -O0″,发现获取到了”n=0x64″,才没再留恋,果断弃了它。

    

不过去掉”ccflags-y += -O0″,又得回去面对追踪不到test()函数的问题,但这不是systemtap的问题,而是gcc作祟,也可以理解为,test()函数确实简单到不需要追踪了,所以,只要将test()函数改”复杂”,就可以”解决”这个问题:


void test(int n)
{
    int m = n/10 + 7;
    printk("%s(), %d: %d, %dn", __FUNCTION__, __LINE__, n, m);
}

    

然而,再次被systemtap玩耍:

    SystemTap追踪自己开发的内核模块

    

不过还是被我冷静的发现,开始执行”cat /proc/kallsyms”时,除了没看到”test_init”,也没看到”test”。

   

对于”test_init”,按常理应该和”test_exit”一样被显示才对,至于为什么没显示,我没再深究,但是可以感觉到,肯定和”test”没被显示是有区别的,因为test_init()函数,一直都是可以被追踪的。由此推测,得让test()函数,在驱动加载后,也存在于内核符号表才行。

    

于是尝试在test.c中,导出test()函数名称:

EXPORT_SYMBOL(test);

    

最终达到了满意的效果:


    SystemTap追踪自己开发的内核模块




SystemTap追踪自己开发的内核模块


看雪ID:jmpcall

https://bbs.kanxue.com/user-home-815036.htm

*本文为看雪论坛优秀文章,由 jmpcall 原创,转载请注明来自看雪社区

SystemTap追踪自己开发的内核模块

# 往期推荐

1、区块链智能合约逆向-合约创建-调用执行流程分析

2、在Windows平台使用VS2022的MSVC编译LLVM16

3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱

4、为什么在ASLR机制下DLL文件在不同进程中加载的基址相同

5、2022QWB final RDP

6、华为杯研究生国赛 adv_lua


SystemTap追踪自己开发的内核模块


SystemTap追踪自己开发的内核模块

球分享

SystemTap追踪自己开发的内核模块

球点赞

SystemTap追踪自己开发的内核模块

球在看

原文始发于微信公众号(看雪学苑):SystemTap追踪自己开发的内核模块

版权声明:admin 发表于 2024年1月9日 下午6:00。
转载请注明:SystemTap追踪自己开发的内核模块 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...