这是内核漏洞挖掘技术系列的第二篇。
第一篇:内核漏洞挖掘技术系列(1)——trinity

前言

2013年Project Zero的j00ru开源了用bochs的插桩API实现的挖掘内核double fetch漏洞的工具bochspwn(https://github.com/googleprojectzero/bochspwn),2018年j00ru更新了bochspwn,还开源了同样用bochs的插桩API实现的挖掘未初始化导致的内核信息泄露漏洞的工具bochspwn-reloaded(https://github.com/googleprojectzero/bochspwn-reloaded)。这两个工具发现了windows/linux等操作系统中的多个漏洞。因为bochspwn搭建环境有一些坑,所以这篇文章先讲解double fetch漏洞原理,再搭建bochspwn环境,最后讲解bochspwn原理。

double fetch漏洞

double fetch原理说起来很简单,如图所示,用户通常会通过调用内核函数完成特定功能,当内核函数两次从同一用户内存地址读取同一数据时,通常第一次读取用来验证数据或建立联系,第二次则用来使用该数据。与此同时,用户空间并发运行的恶意线程可以在两次内核读取操作之间利用竞争条件对该数据进行篡改,从而造成内核使用数据的不一致。double fetch漏洞可造成包括缓冲区溢出、信息泄露、空指针引用等后果,最终造成内核崩溃或者恶意提权。

在bochspwn论文中,对double fetch是这样定义的:

  • 同一个虚拟地址至少有两次内存读取
  • 两次读取操作发生在很短的时间内
  • 读取的代码运行在内核态
  • 多次读取的虚拟地址必须驻留在内存中并且用户态可写

在windows系统上比较典型的double fetch长下面这样。

环境搭建(ubuntu18.10+win 10)

如果要使用bochs提供的插桩API,需要把自己写的代码放在源代码中instrument目录下一个单独的目录(比如bochspwn),在编译时启用--enable-instrumentation选项并指定该目录。

./configure [...] --enable-instrumentation="instrument/bochspwn"

所以需要先在linux上搭建交叉编译环境编译出bochs.exe再在windows上使用。sudo apt-get install g++-mingw-w64并按提示安装其它需要的包。

bochspwn使用protobuf记录日志,接下来到https://github.com/protocolbuffers/protobuf下载2.5.0版的protobuf,执行下面的命令编译protobuf并在mingw目录中安装头文件和库。

./configure --host=i686-w64-mingw32 --prefix=/usr/i686-w64-mingw32/
make
sudo make install

make时会出现下面这样的错误。

注释掉src目录下Makefile中下图所示第3114行即可。

之后sudo apt-get install g++并按提示安装其它需要的包,执行下面的命令编译linux上用的。

./configure
make
sudo make install

之后运行protoc –version检查安装是否成功,此时会出现下面这样的错误。这是因为protobuf的默认安装路径是/usr/local/lib,而/usr/local/lib不在ubuntu体系默认的LD_LIBRARY_PATH里。创建文件/etc/ld.so.conf.d/libprotobuf.conf并写入/usr/local/lib,sudo ldconfig再运行protoc --version就可以正常看到版本号了。


https://sourceforge.net/projects/bochs/files/bochs/下载bochs2.6.9的源代码,https://github.com/googleprojectzero/bochspwn下载bochspwn的源代码。在bochs-2.6.9/instrument/目录下创建bochspwn目录,将bochspwn/instrumentation和bochspwn/third_party/instrumentation中的文件拷贝到我们创建的bochs-2.6.9/instrument/bochspwn中。在windows操作系统中找到32位的dbghelp.lib或者dbghelp.dll(原来的文档中这里说法有点问题,我在github上提了个issue之后改过来了),也拷贝到我们创建的bochs-2.6.9/instrument/bochspwn中。如果安装了windbg可以在C:\Program Files (x86)\Debugging Tools for Windows (x86)\中找到这些文件(我认为这些文件的版本应该对应于待fuzz的windows操作系统的版本,原来的文档中并没有说明这一点)。之后在bochs-2.6.9目录下执行下面的命令。

export MINGW=i686-w64-mingw32
CXXFLAGS="-O2 -I/usr/${MINGW}/include/ -D_WIN32 -L/usr/${MINGW}/lib -static-libgcc -static-libstdc++" CFLAGS="-O2 -I/usr/${MINGW}/include/ -D_WIN32 -L/usr/${MINGW}/lib" LIBS="/usr/${MINGW}/lib/libprotobuf.a instrument/bochspwn/dbghelp.lib" ./configure \
--host=i686-w64-mingw32 \
--enable-instrumentation="instrument/bochspwn" \
--enable-x86-64 \
--enable-e1000 \
--with-win32 \
--without-x \
--without-x11 \
--enable-cpu-level=6 \
--enable-pci \
--enable-pnic \
--enable-fast-function-calls \
--enable-fpu \
--enable-cdrom \
--disable-all-optimizations

切换到bochs-2.6.9/instrument/bochspwn,protoc --cpp_out=. logging.proto生成logging.pb.cc和logging.pb.h,之后make编译。第一次报错找不到DbgHelp.h,将dbghelp.h重命名为DbgHelp.h。

第二次报错有关__in和__out,这是因为微软风格的代码使用__in和__out来注释函数参数,执行下面的命令把/usr/lib/gcc/i686-w64-mingw32/7.3-win32/include目录下所有文件中的__in和__out替换成___in和___out。

sudo sed -ri 's/\b(__in|__out)\b/_&/g' $(egrep -rl '\b(__in|__out)\b' include)


编译成功之后应该会有一个libinstrument.a文件。

然后切换回bochs-2.6.9,之后make编译。第一次报错narrow conversion,CXXFLAGS加上-Wno-narrowing忽略此错误。

第二次报错没有找到windres,把Makefile中所有的windres替换成i686-w64-mingw32-windres。

第三次报错是因为链接器没有指定-lws2_32,在Makefile相关命令后加上-lws2_32。


之后就应该不会再有什么错误了。如果我们想编译64位版本的bochs从第一步编译protobuf开始把编译器换成x86_64-w64-mingw32,把dbghelp.lib/dll换成64位的版本,其余步骤应该一样。
接下来以fuzz linux内核为例。在windows操作系统上安装好bochs-2.6.9,用VirtualBox安装好ubuntu sever 18.04.2。这里用server也是因为没有图形化界面,bochs运行起来相对流畅一些。升级到最新的内核并安装好符号包(http://ddebs.ubuntu.com/pool/main/l/linux/)。然后下载并安装好这个系列第一篇介绍的trinity(https://github.com/kernelslacker/trinity)或者其它能帮助提高代码覆盖率的工具。bochspwn在生成日志文件的时候需要一些特定的结构体信息,使用gdb加载内核符号文件之后打印出这些信息。在原来的文档中提供了打印出这些信息需要的gdb脚本。

print &((struct thread_info*)0)->task
print &((struct task_struct*)0)->pid
print &((struct task_struct*)0)->tgid
print &((struct task_struct*)0)->comm
print &modules
print &((struct module*)0)->list
print &((struct module*)0)->name
print &((struct module*)0)->module_core
print &((struct module*)0)->core_size
maintenance info sections

不过内核的数据结构已经有了一些变化,我不太确定我下面的脚本是否正确(我不是很清楚struct module中core_layout和init_layout有什么区别)。

修改bochspwn中的config.txt,增加一项ubuntu_server_64_4.15.0-47-generic。


通过查看源码可以发现task_comm_len和module_name_len都没有变。



使用virtualbox自带的工具将硬盘格式由VDI格式转换成RAW格式。

根据原来的文档bochspwn应该包含一个bochsrc.txt,但是我却并没有找到。从bochs-2.6.9的安装目录C:\Program Files (x86)\Bochs-2.6.9中找到bochsrc-sample.txt复制过来,重命名为bochsrc.txt。在这个文件中指定我们转换的raw文件的路径。

我们还可以根据情况调整使用的内存。

现在终于可以用bochs运行我们的虚拟机了,把我们在linux上交叉编译出来的bochs.exe拷贝到bochspwn目录下,如下图所示设置并运行。

出现了一个小错误,注释掉bochsrc.txt这一行即可。

成功启动之后运行trinity,我们应该能够得到memlog.bin和modules.bin。之后可以使用类似下面的命令编译tools目录下的工具进行进一步分析。

bochspwn原理

在bochspwn下有三个含有代码的目录:instrumentation,third_party和tools。因为在泉哥的一篇博客中有较为详细的分析[1],这里只简单分析tools目录下的代码。

  • separate.cc:将日志文件分成更小的部分以便处理
  • doubleread.cc:找到日志文件中double fetch的逻辑
    在doubleread.cc中有两个重要的STL成员。map类型的memory_accesses[key] = {acc1, acc2, ...}中key是被访问的地址,{acc1, acc2, ...}是对应的log_data_st。如果对于一个key来说memory_accesses[key].size() > 1说明这个地址被访问了不止一次,就有可能存在double fetch漏洞。vector类型的access_structs存储所有memory_accesses中的log_data_st(不重复)。

    在分析日志的过程中,如果该地址的访问类型是write,检查是不是ProbeForWrite函数,如果是则从对应的memory_accesses[key]和access_structs中删除最后一个元素。

    如果该地址的访问类型是read,首先将log_data_st加入access_structs。在log_data_st结构中,lin表示被访问的地址,len表示被访问的长度,repeated表示从lin开始的连续内存地址已经被访问了多少个len。所以len*repeated表示访问内存区域的总大小。对于repeated>1的访问要把log_data_st加到所有对应的memory_accesses[key]中。

    memory_accesses[key].size() > 1,进入到处理double fetch逻辑的函数。
  • win32_symbolize.cc\win32_symbolize.py\linux_symbolize.py:给日志文件添加符号信息以方便阅读并确定漏洞
  • print.cc:日志文件打印
  • no_cidll.c:windows操作系统上的CI.dll可能导致假阳性,在使用doubleread.cc之前可以先使用no_cidll.c过滤一遍来解决这个问题
  • stat.c:统计日志文件中有多少次读/写和读/写内存大小等信息
  • unhandled_access.cc:找到日志文件中unhandled access的逻辑(访问用户态内存时没有设置异常处理)。比如说下面的代码如果没有try/except的话,可能就会导致本地DOS。

    在Windows x86中,异常处理程序链接在SEH中(从fs:[0]开始),其中每个异常处理程序由以下结构描述。

    _EH3_EXCEPTION_REGISTRATION驻留在它们相应函数的栈帧中,并在这些函数的开头用__SEH_prolog4(_GS)例程初始化。

    稍后,try{}块的开头通过将其基于0的索引写入TryLevel字段来表示,然后在关闭块并禁用异常处理时用-2(0xFFFFFFFE)覆盖它们。下面是一个try/except块的示例,它将单个DWORD值写入用户态内存。

    任何用户态内存访问时的整个调用栈看起来类似于下面这样。

    Bochs插桩可以遍历SEH链,确定启用哪些处理程序以及它们对应的函数。如果不存在异常记录,或者所有异常记录的TryLevel字段都设置为0xFFFFFFFE,那么此时发生的异常可能会导致操作系统崩溃。

    (这部分在原理在j00ru的后续博客中有提到[3])
  • count_callstack_depth.cc:计算调用栈深度
  • count_excp_handlers.cc:计算异常处理栈深度
  • common.cc\common.h:提供一些通用的函数

参考资料

1.Bochspwn漏洞挖掘技术深究(1):Double Fetches 检测
2.Bochspwn: Identifying 0-days via system-wide memory access pattern analysis
3.Windows Kernel Local Denial-of-Service #1: win32k!NtUserThunkedMenuItemInfo (Windows 7-10)

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖