案例研究:在Linux内核中搜索漏洞

2019-09-11 约 2316 字 预计阅读 5 分钟

声明:本文 【案例研究:在Linux内核中搜索漏洞】 由作者 Pinging 于 2019-09-10 08:40:02 首发 先知社区 曾经 浏览数 1491 次

感谢 Pinging 的辛苦付出!

这篇简短的文章描述了我们在实战过程中的对Linux内核漏洞的调查以及我对Semmle QL和Coccinelle的使用经验,我曾经用该工具来搜索类似的bug。

内核漏洞

几天前,我的自定义syzkaller项目代码产生了崩溃。它具有一个非常稳定的reproducer ,所以我借用此模块进行研究。在这里,我将借此机会说syzkaller是一个非常棒的项目,对我们的行业产生了很大的影响。

我发现导致此崩溃的错误已在commit 229b53c9bf4e(2017年6月)中的drivers/block/floppy.c中引入。

compat_getdrvstat()函数代码如下所示:

static int compat_getdrvstat(int drive, bool poll,
                            struct compat_floppy_drive_struct __user *arg)
{
        struct compat_floppy_drive_struct v;

        memset(&v, 0, sizeof(struct compat_floppy_drive_struct));
...
        if (copy_from_user(arg, &v, sizeof(struct compat_floppy_drive_struct)))
                return -EFAULT;
...
}

这里copy_from_user()将用户空间指针arg作为复制目标,将内核空间指针&v作为源。 这显然是一个错误。 它可以由具有访问软盘驱动器的用户触发。

这个bug对x86_64的影响很大。 它从内核空间导致用户空间内存的memset()执行:

1 copy_from_user()源码(第二个参数)的access_ok()失败。

2 然后copy_from_user()尝试删除复制目标(第一个参数)。

3 但目标是在用户空间而不是内核空间...

4 所以我们产生了一个内核崩溃:

[   40.937098] BUG: unable to handle page fault for address: 0000000041414242
[   40.938714] #PF: supervisor write access in kernel mode
[   40.939951] #PF: error_code(0x0002) - not-present page
[   40.941121] PGD 7963f067 P4D 7963f067 PUD 0
[   40.942107] Oops: 0002 [#1] SMP NOPTI
[   40.942968] CPU: 0 PID: 292 Comm: d Not tainted 5.3.0-rc3+ #7
[   40.944288] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Ubuntu-1.8.2-1ubuntu1 04/01/2014
[   40.946478] RIP: 0010:__memset+0x24/0x30
[   40.947394] Code: 90 90 90 90 90 90 0f 1f 44 00 00 49 89 f9 48 89 d1 83 e2 07 48 c1 e9 03 40 0f b6 f6 48 b8 01 01 01 01 01 01 01 01 48 0f af c6 48 ab 89 d1 f3 aa 4c 89 c8 c3 90 49 89 f9 40 88 f0 48 89 d1 f3
[   40.951721] RSP: 0018:ffffc900003dbd58 EFLAGS: 00010206
[   40.952941] RAX: 0000000000000000 RBX: 0000000000000034 RCX: 0000000000000006
[   40.954592] RDX: 0000000000000004 RSI: 0000000000000000 RDI: 0000000041414242
[   40.956169] RBP: 0000000041414242 R08: ffffffff8200bd80 R09: 0000000041414242
[   40.957753] R10: 0000000000121806 R11: ffff88807da28ab0 R12: ffffc900003dbd7c
[   40.959407] R13: 0000000000000001 R14: 0000000041414242 R15: 0000000041414242
[   40.961062] FS:  00007f91115c4440(0000) GS:ffff88807da00000(0000) knlGS:0000000000000000
[   40.962603] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[   40.963695] CR2: 0000000041414242 CR3: 000000007c584000 CR4: 00000000000006f0
[   40.965004] Call Trace:
[   40.965459]  _copy_from_user+0x51/0x60
[   40.966141]  compat_getdrvstat+0x124/0x170
[   40.966781]  fd_compat_ioctl+0x69c/0x6d0
[   40.967423]  ? selinux_file_ioctl+0x16f/0x210
[   40.968117]  compat_blkdev_ioctl+0x21d/0x8f0
[   40.968864]  __x32_compat_sys_ioctl+0x99/0x250
[   40.969659]  do_syscall_64+0x4a/0x110
[   40.970337]  entry_SYSCALL_64_after_hwframe+0x44/0xa9

我还没有找到一种方法来利用它来进行权限提升。

变体应用分析:Semmle QL

我的第一个想法是在整个内核源代码中搜索类似的问题。 我决定尝试Semmle QL(Semmle最近非常活跃)。 这里有一个很好的QL和LGTM介绍,有足够的信息可以快速启动。
introduction to QL and LGTM

所以我在Query控制台中打开了Linux内核项目,搜索了copy_from_user()的调用:

import cpp

from FunctionCall call
where call.getTarget().getName() = "copy_from_user"
select call, "I see a copy_from_user here!"

此查询仅提供了616个结果。 这很奇怪,因为Linux内核中应该有更多的copy_from_user()调用。 我在LGTM文档中找到了答案:

LGTM从每个代码库中提取信息并生成数据库
并准备好查询。之后对于C/C++项目,构建源代码,并作为提取过程的一部分。

因此,用于内核构建的Linux内核配置限制了LGTM分析的范围。 如果配置中没有启用某个内核子系统,则它不会构建,因此我们无法在LGTM中分析其代码。

LGTM文档还说:

我们可能需要自定义流程以使LGTM能够构建项目。
我们可以通过向存储库添加项目的lgtm.yml文件来完成此操作。

我决定为Linux内核创建一个自定义lgtm.yml文件,并在LGTM社区论坛上提供一个默认文件。

LGTM团队的答案非常快速:

我们在lgtm.com上使用的工作机器很小且资源有限,所以不幸的是,defconfig只是我们可以使用的最大配置。每次提交完整构建+提取+分析需要3.5小时,而我们最多允许4个小时的分析过程。

这并不是一个好的回复,但他们目前正致力于大项目的解决方案。
所以我决定尝试另一种工具进行调查。

变体分析:Coccinelle

我听说过Coccinelle。 Linux内核社区经常使用这个工具。 此外,我记得Kees CookCoccinelle搜索了copy_from_user()错误。 所以我开始学习语义补丁语言(SmPL)并最终编写了这条规则(感谢Julia Lawall的反馈):

virtual report

@cfu exists@
identifier f;
type t;
identifier v;
position decl_p;
position copy_p;
@@

f(..., t v@decl_p, ...)
{
... when any
copy_from_user@copy_p(v, ...)
... when any
}

@script:python@
f << cfu.f;
t << cfu.t;
v << cfu.v;
decl_p << cfu.decl_p;
copy_p << cfu.copy_p;
@@

if '__user' in t:
  msg0 = "function \"" + f + "\" has arg \"" + v + "\" of type \"" + t + "\""
  coccilib.report.print_report(decl_p[0], msg0)
  msg1 = "copy_from_user uses \"" + v + "\" as the destination. What a shame!\n"
  coccilib.report.print_report(copy_p[0], msg1)

它的想法很简单。通常在将用户空间指针作为参数的函数中调用copy_from_user()。 我的规则描述了copy_from_user()将用户空间指针作为复制目标时的情况:

  • 规则的主要部分查找所有情况,当某个函数f()的参数v用作copy_from_user()的第一个参数。

  • 如果匹配,Python脚本将检查v是否在其类型中具有__user注释。

这是Coccinelle输出:

./drivers/block/floppy.c:3756:49-52: function "compat_getdrvprm" has arg "arg"
of type "struct compat_floppy_drive_params __user *"
./drivers/block/floppy.c:3783:5-19: copy_from_user uses "arg" as the
destination. What a shame!

./drivers/block/floppy.c:3789:49-52: function "compat_getdrvstat" has arg "arg"
of type "struct compat_floppy_drive_struct __user *"
./drivers/block/floppy.c:3819:5-19: copy_from_user uses "arg" as the
destination. What a shame!

因此,有两个(不是非常危险的)内核漏洞适合这种错误模式。

公共的0 day 漏洞

事实证明,我不是第一个发现这些错误的人。 Jann Horn于2019年3月向他们报告。他用sparse的方法找到它们。 我绝对相信它能找到比我的PoC Coccinelle规则更多的错误案例。

但事实上,Jann的补丁丢失了,并没有进入官方的更新主线。
所以这两个错误可以称为“公开的0 day”:-)

我已经向LKML报告了这个问题,而Jens Axboe将应用Jann的Linux内核v5.4补丁。

本文为翻译文章:http://blog.ptsecurity.com/2019/08/case-study-searching-for-vulnerability.html

关键词:[‘安全技术’, ‘二进制安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now