最近一段时间,一个诡异的内核问题困扰着格蠹的内核开发团队。在测试幽兰的系统镜像时,有一个随机的内核oops。
一旦这个oops发生,那么便产生一系列连锁的不良反应:比如声音无法播放,reboot失败等等。
内存越界
7月30日那天,我一边准备关于Windows 719大蓝屏的讲义,一边看这个oops,并在直播中提到了这个诡异的oops。
这个oops与719大蓝屏类似,也是非法访问内存,也就是通常所说的越界。内存是软件的舞台,内存问题就是人类社会的住房问题,千头万绪,盘根错节。
有趣的是,越界访问的内存地址非常奇怪,不是一般的0指针,也不是明显的小指针(小于4096的地址),而是一个很长的地址:
003a72656c646e69
根据多年的经验,我一眼看出这个地址中包含很多可读的ASCII字符,使用windbg的.formats命令转换,果然如此:
0:000> .formats 203a72656c646e69
Evaluate expression:
Hex: 203a7265`6c646e69
Decimal: 2322294337798696553
Octal: 0200723446255431067151
Binary: 00100000 00111010 01110010 01100101 01101100 01100100 01101110 01101001
Chars: :reldni
Time: Wed Jan 23 00:02:59.869 8960 (UTC + 8:00)
Float: low 1.10463e+027 high 1.57927e-019
Double: 1.9725e-153
也就是错误地址刚好对应的是" :reldni"这8个字符。把字节序调整一下,就是"indler: "。注意冒号后面还有一个空格(在栈上找到,有时体现在寄存器里)。
对于这个indler: ,如果加上一个h那么就很像软件世界里非常常用的一个单词:“handler: ”
这个oops是随机的,根据oop提供的函数地址,发生崩溃的内核函数名叫:sysfs_file_ops,源代码如下:
static const struct sysfs_ops *sysfs_file_ops(struct kernfs_node *kn)
{
struct kobject *kobj = kn->parent->priv;
if (kn->flags & KERNFS_LOCKDEP)
lockdep_assert_held(kn);
return kobj->ktype ? kobj->ktype->sysfs_ops : NULL;
}
但是大家都知道,对于这样的内存溢出问题,这肯定不是第一现场。这只是一个受害者。
全城搜索
对于内存越界这样的问题,因为溢出时常常无声无息,不知过了多久,才有现象,所以很难定位。
为了找到产生溢出的元凶,小伙伴们想了很多办法,比如根据使用UEFI启动没有oops,而使用u-boot启动有oops的现象,找u-boot的问题,修改了几版u-boot,看似有所缓解,其实是假象,因为本来就是个随机的问题。换了u-boot后,没看到oops,但过了几天又发生了。
上法器
对于越界这样的内存顽症,直接的方法就是上内存检查工具,比如ASAN(Address Sanitizer)。
Linux内核具有ASAN支持,称为KASAN,但是默认不启用,因为会影响性能。
为了抓到越界元凶,我一开始就建议启用KASAN,但是小伙伴启用KASAN编译出内核后,启动失败,上挥码枪调试,发现卡死在u-boot阶段。
第一次尝试KASAN失败后,小伙伴又想了一些其它排查手段,包括增加内核消息打印等,但都没有找到真正的凶手。
前天晚上,我亲自测试新版内核镜像,再次遇到开篇说的oops,很是恼火。
再上法器
昨天,我再次强推KASAN方法,坚定方向,一定要把这个方向走通。
大约一个小时后,好消息接连而至。
第一个好消息是小伙伴通过修改u-boot,成功把启用了KASAN的内核运行起来了。
第二个好消息是KASAN成功找到了一个内存越界写。
KASAN报告
听到这个消息,我立刻停下手上的其它工作。让小伙伴发我内核消息,凝神静气,仔细阅读。
内核时间戳25秒时,KASAN报告初始化完毕。
[ 25.146063] kasan: KernelAddressSanitizer initialized (generic)
33秒时,KASAN抓到越界写。
[ 33.918201] ==================================================================
[ 33.918234] BUG: KASAN: slab-out-of-bounds in __memcpy_fromio+0x8c/0x100
[ 33.918259] Write of size 8 at addr ffffff8101838afc by task systemd/1
[ 33.918283] CPU: 5 PID: 1 Comm: systemd Not tainted 6.1.43-rockchip-rk3588-taiyi #1.0.8
[ 33.918300] Hardware name: YourLand CodeBook (DT)
[ 33.918312] Call trace:
[ 33.918324] dump_backtrace+0xd0/0x130
[ 33.918338] show_stack+0x20/0x30
[ 33.918350] dump_stack_lvl+0xac/0xe0
[ 33.918368] print_report+0x164/0x464
[ 33.918384] kasan_report+0xc8/0x1a0
[ 33.918400] __asan_store8+0x80/0xa4
上面的调用栈省略了很多,因为涉及到敏感的安全问题(下文介绍)。
调用栈下面,有关于这次非法方法的详细细节。
[ 33.919049] The buggy address belongs to the object at ffffff8101838000
which belongs to the cache kmalloc-4k of size 4096
[ 33.919061] The buggy address is located 2812 bytes inside of
4096-byte region [ffffff8101838000, ffffff8101839000)
[ 33.919082] The buggy address belongs to the physical page:
[ 33.919094] page:00000000663e5886 refcount:1 mapcount:0 mapping:0000000000000000 index:0x0 pfn:0x101838
[ 33.919109] head:00000000663e5886 order:3 compound_mapcount:0 compound_pincount:0
[ 33.919123] flags: 0x8000000000010200(slab|head|zone=2)
[ 33.919146] raw: 8000000000010200 0000000000000000 dead000000000122 ffffff8100002a80
[ 33.919159] raw: 0000000000000000 0000000000040004 00000001ffffffff 0000000000000000
[ 33.919172] page dumped because: kasan: bad access detected
[ 33.919192] Memory state around the buggy address:
[ 33.919205] ffffff8101838a00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[ 33.919218] ffffff8101838a80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
[ 33.919234] >ffffff8101838b00: 01 fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[ 33.919246] ^
[ 33.919255] ffffff8101838b80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[ 33.919270] ffffff8101838c00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[ 33.919282] ==================================================================
安全问题
为什么上面故意省略了一些函数名呢?因为昨天夜里我仔细看了元凶代码后,觉得事情非常蹊跷。
为了方便描述,姑且把这个漏洞称为indler漏洞,根据就是最先发现它端倪的那个越界访问地址对应的是indler: 。
经过我们初步分析,indler漏洞的代码至少在2013年就已经存在,而且至今仍在内核主代码树。
这意味着,从2013年到今天的几乎所有Linux内核都有这个漏洞在。跨度长度十几年。
让我感觉不安的第二个特征是,这个漏洞可以在用户空间通过Linux的虚文件机制触发。这意味着黑客可以使用用户空间的某个应用做跳板,转而轻松攻击内核。
第三个关键特征是,这个漏洞可能导致的溢出可以非常大,可以长达数千字节。
查看这个漏洞代码的来源,它来自Google。这更让我觉得这个漏洞来头很大。
综合以上特征,黑客可以使用indler漏洞,实施多种攻击,包括向内核空间注入代码,实现远程代码执行(RCE)。至少,黑客可以通过这个漏洞,做DOS攻击,促发溢出,让内核崩溃,停止工作。
想到这个漏洞代码存在于从终端到云的数以亿计的计算机系统上,一旦被黑客利用,后果不堪设想。
因为此,我们故意隐藏了indler漏洞的具体函数名。正在与专业的安全团队合作,商讨下一步的计划。