前言
本博客的主要内容为Bochspwn的部署、使用与原理分析。本博文内容较长,因为涵盖了Bochspwn的几乎全部内容,从部署的详细过程到如何使用Bochspwn对操作系统内核的Double-Fetch漏洞进行检测,以及对Bochspwn进行漏洞检测的原理分析,相信认真读完本博文,各位读者一定会对Bochspwn有更深的了解。以下就是本篇博客的全部内容了。
1、概述
Bochspwn是一个专门针对Bochs模拟器的漏洞检测工具,旨在挖掘操作系统内核中的Double-Fetch漏洞。作为一个开源工具,Bochspwn提供了一系列功能和工具,帮助安全研究人员发现操作系统内核中的Double-Fetch漏洞。其主要特点包括:
- 针对Bochs模拟器:Bochspwn主要针对Bochs模拟器,这是一个功能强大且跨平台的x86/x86-64 PC模拟器。
- 漏洞挖掘工具:包含了一系列工具,用于在操作系统中进行漏洞挖掘,帮助研究人员发现潜在的漏洞。
- 控制和重现环境: 提供了一个可控的模拟环境,帮助研究人员控制和重现漏洞,以便更深入地理解漏洞的原理和影响。
- 针对操作系统内核:Bochspwn可以通过插桩的方式挖掘操作系统内核中的Double-Fetch漏洞。
- 检测Double-Fetch漏洞:Bochspwn主要检测操作系统内核中的Double-Fetch漏洞,这样的针对性可以使漏洞挖掘的效果更好。
- 跨平台支持:Bochspwn可以在多种操作系统上运行,包括Windows、Linux和macOS,为研究人员提供了灵活的选择。
总的来说,Bochspwn是一个强大的工具,为安全研究人员提供了一个方便的平台,用于在Bochs模拟器中进行漏洞挖掘。通过自动化工具和可控的环境,研究人员可以更有效地发现Double-Fetch漏洞。此外,Bochspwn工具基于C语言、C++语言和Python语言开发。
1.1、工作原理
关于Bochspwn的工作原理如下图所示。使用插桩后的Bochs启动待检测目标,当待检测目标启动后,我们在其上进行内存操作。此时插桩程序就会捕获到内存操作序列,并将潜在会发生Double-Fetch漏洞的内存操作记录下来,并形成漏洞检测日志。最终再通过已知的内存访问序列以及漏洞检测日志来反向分析潜在的Double-Fetch漏洞。
整个Bochspwn的工作原理通过上图以及对应的解释已经说明的很清楚了。只要一点需要额外强调一下,即上文中“反向分析”的含义。在这里采用“反向分析”这个概念,是因为Bochspwn框架检测潜在的Double-Fetch漏洞的逻辑与我们正常的逻辑不太一样。我们通过一个例子来说明。
假设现在有对内核地址空间的内存访问序列:“…ABBC…”(该内存访问序列是由用户操作的,故这是已知的),并假设“A”与“C”与其前后的内存访问并不连续。很明显在这个例子中,“B”是一个连续的内存访问,有可能引发Double-Fetch漏洞,我们来看Bochspwn框架如何处理该内存访问序列,并最终成功检测到潜在的Double-Fetch漏洞。
- Bochspwn首先会捕获“A”。按照作者的逻辑会保存“A”上一次的内存访问信息(该信息可以忽略)。
- 然后Bochspwn会捕获“B”,因为“B”与“A”不同。按照作者的逻辑会保存“A”的内存访问信息。
- 然后Bochspwn会捕获“B”,因为“B”与“B”(上一次的“B”)相同。按照作者的逻辑不会做任何有意义的处理。
- 然后Bochspwn会捕获“C”,因为“C”与“B”不同。按照作者的逻辑会保存“B”的内存访问信息。
- 然后Bochspwn会捕获“C”后面的内存访问序列,因为“C”与其不同(根据假设得来)。按照作者的逻辑会保存“C”的内存访问信息。
此时我们注意,除去忽略的内存访问信息,此时我们应该保存了“A”,“B”和“C”这三个内存访问信息。而我们已知的内存访问序列为“…ABBC…”,刚好将连续的内存访问“B”排除掉了,通过这种“反向分析”的方式,最终可以确定“B”内存访问就是潜在的Double-Fetch漏洞。
那么以上就是关于Boshspwn框架的工作原理的介绍,若想关注更多细节,请参考“1.2、工作流程”章节。
1.2、工作流程
Bochspwn的工作流程如下图所示。可以发现Bochspwn的工作流程包含很多步骤,不过我们着重关注“漏洞检测”的过程,因为这才是Bochspwn用于检测Double-Fetch漏洞的核心。其余内容都是服务于“漏洞检测”的过程的准备工作,不过我们也仍要学习一遍,因为这些内容对理解整个漏洞检测过程是有帮助的。本章节就将对Bochspwn的整个工作流程进行详细分析。
1.2.1、在Ubuntu 22.04.2上的工作
1.2.1.1、准备工作
在准备工作阶段,我们主要做了三件事:
- 下载Bochspwn源代码
- 将Bochspwn源代码目录中的插桩代码复制到Bochs源代码目录中
- 将待检测目标所在系统(通常是Windows操作系统)的“dbghelp.dll”复制到Bochs源代码目录中
最终“/bochs-2.6.9/instrument/bochspwn/”目录中将会包括如下内容:
该目录中的内容,都是后续编译所需要的,我们目前只需要知道在准备阶段我们把这些文件都准备好了,后续就可以直接使用了。关于其具体用法,我们后续使用的时候再介绍。
1.2.1.2、编译Bochs
在该阶段,我们做了第一个很重要的事情就是在“/bochs-2.6.9/”目录中通过sudo CXXFLAGS="-Wno-narrowing -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.dll" ./configure --host=x86_64-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
命令生成了当前目录以及子目录中的“Makefile.in”文件对应的“Makefile”文件,然后执行make
命令。当我们执行make
命令后,会构建在“/bochs-2.6.9/instrument/bochspwn/Makefile”文件的第77行实现的libinstrument.a
目标,即:
这部分规则用于构建静态库“libinstrument.a”。它的依赖是$(BX_OBJS)
,表示静态库的构建需要依赖于BX_OBJS
中列出的目标文件,具体来说:
-
rm -f libinstrument.a
:这个命令会删除旧的静态库文件,以确保每次构建都是从头开始的。 -
ar rv $@ $(BX_OBJS)
:这个命令使用ar工具将目标文件$(BX_OBJS)
打包成静态库文件“libinstrument.a”。ar
命令的选项rv
分别表示替换(r)已经存在的文件中的对象文件,并且以verbose(v)模式进行操作。$@是一个特殊的变量,代表当前目标(即“libinstrument.a”)的名称。需要注意的是,“libinstrument.a”所依赖的各文件(即
BX_OBJS`)定义在“/bochs-2.6.9/instrument/bochspwn/Makefile”文件的第50行。
-
$(RANLIB) libinstrument.a
:这个命令用于更新静态库文件的索引,以确保它能够被链接器正确地使用。在这里,$(RANLIB)
是一个变量,它被设置为echo
命令,所以实际上并不会执行任何操作。因此,这一步在本质上是可选的,因为在大多数情况下,静态库文件不需要额外的索引操作。
总之,构建完该规则后,将会在“/bochs-2.6.9/instrument/bochspwn/”目录中生成“libinstrument.a”静态库。该静态库非常重要,后面用到的时候我们会详细分析。
当上面这件事做完之后,在该阶段还做了一件很重要的事情,即来到“/bochs-2.6.9/”目录中执行make
命令,该make
命令会构建“/bochs-2.6.9/Makefile”文件(该文件是由前面的配置过程生成的)中的第175行的all
规则。
在这个“Makefile”中,all
是一个伪目标,其依赖于bochs
、bximage
和bxhub
这三个规则。当执行make
命令时,会按照依赖关系先后生成这三个可执行文件(需要注意的是,这三个可执行文件都是exe格式,因为在前面的配置过程制定了生成的可执行文件都是Windows平台下的,故其都是exe格式)。
- “bochs.exe”:它负责运行模拟器并提供用户界面和控制台交互。
- “bximage.exe”:用于创建硬盘镜像文件,用户可以使用它来生成虚拟硬盘镜像以供Bochs使用。
- “bxhub.exe”:是Bochs网络设备模拟器的一部分,用于模拟网络中心节点,并能够与其它Bochs实例通信,用于模拟网络环境。
在这里我们主要关注bochs
这个规则,bochs
规则实现在“/bochs-2.6.9/Makefile”文件中的第179行。
这段代码是Bochs
编译过程中的一个Makefile规则,它用于链接生成“bochs.exe”可执行文件。具体来说,其逻辑为:
- 指定编译器和选项:
- 使用
$(CXX)
指定C++编译器。 - 使用
$(CXXFLAGS)
和$(LDFLAGS)
来设置编译和链接的选项。
- 使用
- 指定源文件和库文件:
- 使用了多个变量来指定不同模块的目标文件和静态库文件,如
BX_OBJS
、SIMX86_OBJS
、iodev/libiodev.a
等。 - 这些变量包含了Bochs的核心模块、IO设备模块、显示模块、硬盘镜像模块、网络模块等的目标文件和静态库文件。
- 使用了多个变量来指定不同模块的目标文件和静态库文件,如
- 链接目标文件和库文件:
- 使用
$(CXX)
将所有的目标文件和库文件链接成一个可执行文件。 - 将目标文件和库文件按照指定的顺序进行链接。
- 使用
- 指定额外的链接选项和库文件:
- 使用
$(GUI_LINK_OPTS)
、$(DEVICE_LINK_OPTS)
、$(MCH_LINK_FLAGS)
、$(SIMX86_LINK_FLAGS)
等变量来指定额外的链接选项。 - 使用
$(READLINE_LIB)
、$(EXTRA_LINK_OPTS)
等变量来指定额外的库文件。
- 使用
- 最终生成可执行文件:
- 将链接后的目标文件生成为名为“bochs.exe”的可执行文件。
通过以上步骤,Makefile完成了将Bochs的各个模块编译后的目标文件链接成一个可执行文件的过程。最终将会在“/bochs-2.6.9/”目录中生成一个名为“bochs.exe”的可执行文件(其余生成的可执行文件我们并不关心,故不在此处赘述)。
在这里值得注意的是,“bochs.exe”文件在编译的过程中,链接了我们上面在“/bochs-2.6.9/instrument/bochspwn/”目录中生成“libinstrument.a”静态库和“/bochs-2.6.9/main.o”。“libinstrument.a”静态库后续我们会分析,而这个“main.o”又是什么呢?其实它是由“/bochs-2.6.9/main.cc”编译而来,这个“main.cc”中就包含了“bochs.exe”文件的入口函数,这将是我们在漏洞检测阶段分析的重点。
总之,在该阶段,我们在/bochs-2.6.9/instrument/bochspwn/”目录中生成“libinstrument.a”静态库,并且在“/bochs-2.6.9/”目录中生成了“bochs.exe”。
1.2.2、在Windows 10上的工作
1.2.2.1、准备待检测目标
在该阶段,我们需要准备待检测的目标,也就是Linux内核,而Linux内核又在各个版本的操作系统中,我们所有的测试都已Ubuntu系统为例,故我们需要来到Ubuntu镜像网站下载待检测的Linux内核对应的Ubuntu系统的ISO文件。
在这里需要注意的是,我们测试的所有Ubuntu系统都是Server版本的。最后将下载好的ISO文件挂载,并使用VirtualBox安装好对应的操作系统。此外,还需要开启主机操作系统的虚拟化技术。
以上就是本阶段所做的全部事情,可以发现该阶段所做的事情并不难,都是我们日常所操作的内容,只是有些细节需要注意。关于这些细节,可以参考对应的安装与使用章节。
1.2.2.2、部署内核调试符号包
当我们在Windows 10上部署好待检测的目标之后,就可以部署待检测目标的内核调试符号包了,这也是我们在这一阶段要做的事情。在该阶段,我们要搞懂三个事情。
- 什么是内核调试符号包?
内核调试符号包是一种包含了与Linux内核编译时所使用的源代码对应的调试信息的软件包。这些调试信息包括变量名称、函数名称、源文件名以及行号等,能够帮助调试器将程序运行时的机器代码与源代码进行关联,从而在调试过程中准确地定位到源代码的位置。通常情况下,编译Linux内核时会产生一个包含调试信息的特殊文件,通常被称为vmlinux文件,其中包含了整个内核的调试信息。 - 为什么要部署内核调试符号包?
部署内核调试符号包的目的是为了能够在调试期间方便地查看和分析内核代码。内核调试符号包包含了与内核编译时所使用的源代码对应的调试信息,这些信息包括变量名称、函数名称、源文件名以及行号等。有了这些调试符号,调试器就能够将程序运行时的机器代码与源代码进行关联,从而可以在调试过程中准确地定位到源代码的位置,帮助开发人员快速定位和解决问题。 - 如何部署内核调试符号包?
首先从Linux内核调试符号包官网下载对应版本的内核调试符号包,然后根据内核调试符号包部署方法来部署内核调试符号包。
1.2.2.3、加载内核符号信息
在该阶段,我们的核心操作是利用刚刚部署好的内核调试符号包来获取内核符号信息。具体来说,我们做了如下操作。
(gdb) print &((struct task_struct*)0)->pid
(gdb) print &((struct task_struct*)0)->tgid
(gdb) print &((struct task_struct*)0)->comm
(gdb) print &modules
(gdb) print &((struct module*)0)->list
(gdb) print &((struct module*)0)->name
(gdb) print &((struct module*)0)->core_layout->base
(gdb) print &((struct module*)0)->core_layout->size
这些GDB命令用于获取特定结构体中成员的地址偏移量。以下是这些每个命令的解释和用途:
(gdb) print &((struct task_struct*)0)->pid
:这条命令会打印出task_struct
结构体中pid
成员的地址偏移量。task_struct
是Linux内核中表示进程的重要结构体,其中pid
表示进程ID。了解pid
成员在task_struct
中的地址偏移量对于内核调试和分析进程相关信息很有用。(gdb) print &((struct task_struct*)0)->tgid
:类似地,这条命令会打印出task_struct
结构体中tgid
成员的地址偏移量。tgid
表示线程组ID,也是一个进程的ID。了解tgid
成员在task_struct
中的地址偏移量有助于理解进程和线程的关系。(gdb) print &((struct task_struct*)0)->comm
:这条命令打印出task_struct
结构体中comm
成员的地址偏移量。comm
用于存储进程的名称。了解comm
成员在task_struct
中的地址偏移量可用于分析进程的命名机制。(gdb) print &modules
:这条命令打印出modules
全局变量的地址。在Linux内核中,modules
用于存储已加载的内核模块的列表。了解modules
变量的地址有助于分析内核模块加载和管理的机制。(gdb) print &((struct module*)0)->list
:这条命令打印出module
结构体中list
成员的地址偏移量。list
用于连接已加载内核模块的链表。了解list
成员在module
结构体中的地址偏移量有助于分析内核模块链表的结构。(gdb) print &((struct module*)0)->name
:这条命令打印出module
结构体中name
成员的地址偏移量。name
用于存储内核模块的名称。了解name
成员在module
结构体中的地址偏移量可用于查看内核模块的命名机制。(gdb) print &((struct module*)0)->core_layout->base
:这条命令打印出module
结构体中core_layout
成员中base
成员的地址偏移量。core_layout
用于存储内核模块的核心布局信息,base
表示内核模块的基址。了解base
成员在core_layout
中的地址偏移量有助于分析内核模块加载和布局。(gdb) print &((struct module*)0)->core_layout->size
:这条命令打印出module
结构体中core_layout
成员中size
成员的地址偏移量。size
表示内核模块的大小。了解size
成员在core_layout
中的地址偏移量有助于分析内核模块的大小信息。
最后我们只需要在“/bochspwn/config”文件中的对应位置填入获取的以上信息即可。该阶段的任务也就完成了。
1.2.2.4、设置配置文件
在该阶段做的事情就比较简单了,我们只需要将“/bochspwn/bochsrc.txt”的第718行的path
属性值修改为RAW格式的硬盘路径即可,这样Bochs就可以使用该配置启动待检测目标。
1.2.2.5、漏洞检测
1.2.2.5.1、启动待检测目标
到该阶段我们终于做完了全部的准备工作,要开始进行漏洞检测了。在该阶段,我们通过执行如下三条命令开始进行漏洞检测。
set BXSHARE=C:\Program Files (x86)\Bochs-2.6.9
set BOCHSPWN_CONF=C:\bochspwn-master\config.txt
bochs.exe -f C:\bochspwn-master\bochsrc.txt
这三条命令中,最重要的就是上面最后一条命令,因为在“1.2.1.2、编译Bochs”章节我们已经分析过,最终是通过“bochs.exe”文件开始进行的漏洞检测,而“/bochs-2.6.9/main.cc”中就包含了“bochs.exe”文件的入口函数。故我们要从“/bochs-2.6.9/main.cc”开始进行分析。分析一个源代码文件,要从其主函数开始分析,“main.cc”的主函数实现在“/bochs-2.6.9/main.cc”的第533行。
在该函数中,只有一处重点,即最后调用了bxmain()
函数。而bxmain()
函数实现在“/bochs/main.cc”的第303行。
int bxmain(void)
{
#ifdef HAVE_LOCALE_H
// Initialize locale (for isprint() and other functions)
setlocale (LC_ALL, "");
#endif
bx_init_siminterface(); // create the SIM object
static jmp_buf context;
if (setjmp (context) == 0) {
SIM->set_quit_context (&context);
BX_INSTR_INIT_ENV();
if (bx_init_main(bx_startup_flags.argc, bx_startup_flags.argv) < 0) {
BX_INSTR_EXIT_ENV();
return 0;
}
// read a param to decide which config interface to start.
// If one exists, start it. If not, just begin.
bx_param_enum_c *ci_param = SIM->get_param_enum(BXPN_SEL_CONFIG_INTERFACE);
const char *ci_name = ci_param->get_selected();
if (!strcmp(ci_name, "textconfig")) {
#if BX_USE_TEXTCONFIG
init_text_config_interface(); // in textconfig.h
#else
BX_PANIC(("configuration interface 'textconfig' not present"));
#endif
}
else if (!strcmp(ci_name, "win32config")) {
#if BX_USE_WIN32CONFIG
init_win32_config_interface();
#else
BX_PANIC(("configuration interface 'win32config' not present"));
#endif
}
#if BX_WITH_WX
else if (!strcmp(ci_name, "wx")) {
PLUG_load_gui_plugin("wx");
}
#endif
else {
BX_PANIC(("unsupported configuration interface '%s'", ci_name));
}
ci_param->set_enabled(0);
int status = SIM->configuration_interface(ci_name, CI_START);
if (status == CI_ERR_NO_TEXT_CONSOLE)
BX_PANIC(("Bochs needed the text console, but it was not usable"));
// user quit the config interface, so just quit
} else {
// quit via longjmp
}
SIM->set_quit_context(NULL);
#if defined(WIN32)
if (!bx_user_quit) {
// ask user to press ENTER before exiting, so that they can read messages
// before the console window is closed. This isn't necessary after pressing
// the power button.
fprintf(stderr, "\nBochs is exiting. Press ENTER when you're ready to close this window.\n");
char buf[16];
fgets(buf, sizeof(buf), stdin);
}
#endif
BX_INSTR_EXIT_ENV();
return SIM->get_exit_code();
}
该函数是Bochs(仿真器)的主函数,负责初始化仿真环境、创建仿真对象,启动配置界面,并在退出时处理必要的清理工作。以下是其主要工作逻辑。
- 设置本地化环境:
- 使用
setlocale(LC_ALL, "");
初始化本地化环境,以便程序能正确处理与本地语言和地区相关的功能,例如字符输出。
- 使用
- 初始化仿真器接口:
- 调用
bx_init_siminterface();
函数创建SIM
对象,该对象可能是程序中用于模拟器交互的核心对象。
- 调用
- 异常处理机制:
- 定义了一个静态的
jmp_buf
类型变量context
,并使用setjmp(context)
保存当前执行位置,以便在需要时通过longjmp
跳转回来。
- 定义了一个静态的
- 设置退出上下文:
- 使用
SIM->set_quit_context(&context);
设置SIM
对象的退出上下文,以便在退出时进行清理工作。
- 使用
- 初始化仿真环境:
- 调用
BX_INSTR_INIT_ENV();
宏,可能是初始化仿真环境所需的一些操作。
- 调用
- 初始化主函数:
- 调用
bx_init_main(bx_startup_flags.argc, bx_startup_flags.argv)
函数进行一些初始化操作,如解析命令行参数等。
- 调用
- 选择配置界面:
- 根据配置参数选择不同的配置界面,并进行相应的初始化操作,如文本配置界面或图形配置界面。
- 禁用已选择的配置界面:
- 使用
ci_param->set_enabled(0);
禁用已选择的配置界面,避免重复启动。
- 使用
- 启动配置界面:
- 调用
SIM->configuration_interface(ci_name, CI_START)
函数启动配置界面。
- 调用
- 清除退出上下文:
- 使用
SIM->set_quit_context(NULL);
清除退出上下文,表示程序已经正常执行完毕。
- 使用
- 向用户输出提示信息:
- 使用
fprintf(stderr, "\nBochs is exiting. Press ENTER when you're ready to close this window.\n");
向标准错误流输出一条提示用户关闭窗口前按下回车键的消息。
- 使用
- 退出仿真环境:
- 调用
BX_INSTR_EXIT_ENV();
宏,可能是退出仿真环境所需的一些清理操作。
- 调用
- 返回仿真器的退出代码:
- 使用
return SIM->get_exit_code();
返回仿真器的退出代码,可能是仿真器执行完毕后的状态码。
- 使用
这段代码看起来有些长,不过我们只关注上面标红的三处逻辑,因为这三处逻辑才是使用Bochs启动待检测目标的核心,下面我们将对其进行分析。
- 初始化主函数
该逻辑由bx_init_main(bx_startup_flags.argc, bx_startup_flags.argv)
函数调用实现。而bx_init_main()
函数实现在“/bochs-2.6.9/main.cc”的第597行,
int bx_init_main(int argc, char *argv[])
{
// To deal with initialization order problems inherent in C++, use the macros
// SAFE_GET_IOFUNC and SAFE_GET_GENLOG to retrieve "io" and "genlog" in all
// constructors or functions called by constructors. The macros test for
// NULL and create the object if necessary, then return it. Ensure that io
// and genlog get created, by making one reference to each macro right here.
// All other code can reference io and genlog directly. Because these
// objects are required for logging, and logging is so fundamental to
// knowing what the program is doing, they are never free()d.
SAFE_GET_IOFUNC(); // never freed
SAFE_GET_GENLOG(); // never freed
// initalization must be done early because some destructors expect
// the bochs config options to exist by the time they are called.
bx_init_bx_dbg();
bx_init_options();
bx_print_header();
SIM->get_param_enum(BXPN_BOCHS_START)->set(BX_RUN_START);
// interpret the args that start with -, like -q, -f, etc.
int arg = 1, load_rcfile=1;
while (arg < argc) {
// parse next arg
if (!strcmp("--help", argv[arg]) || !strncmp("-h", argv[arg], 2)
#if defined(WIN32)
|| !strncmp("/?", argv[arg], 2)
#endif
) {
if ((arg+1) < argc) {
if (!strcmp("features", argv[arg+1])) {
fprintf(stderr, "Supported features:\n\n");
#if BX_SUPPORT_CLGD54XX
fprintf(stderr, "cirrus\n");
#endif
#if BX_SUPPORT_VOODOO
fprintf(stderr, "voodoo\n");
#endif
#if BX_SUPPORT_PCI
fprintf(stderr, "pci\n");
#endif
#if BX_SUPPORT_PCIDEV
fprintf(stderr, "pcidev\n");
#endif
#if BX_SUPPORT_NE2K
fprintf(stderr, "ne2k\n");
#endif
#if BX_SUPPORT_PCIPNIC
fprintf(stderr, "pcipnic\n");
#endif
#if BX_SUPPORT_E1000
fprintf(stderr, "e1000\n");
#endif
#if BX_SUPPORT_SB16
fprintf(stderr, "sb16\n");
#endif
#if BX_SUPPORT_ES1370
fprintf(stderr, "es1370\n");
#endif
#if BX_SUPPORT_USB_OHCI
fprintf(stderr, "usb_ohci\n");
#endif
#if BX_SUPPORT_USB_UHCI
fprintf(stderr, "usb_uhci\n");
#endif
#if BX_SUPPORT_USB_EHCI
fprintf(stderr, "usb_ehci\n");
#endif
#if BX_SUPPORT_USB_XHCI
fprintf(stderr, "usb_xhci\n");
#endif
#if BX_GDBSTUB
fprintf(stderr, "gdbstub\n");
#endif
fprintf(stderr, "\n");
arg++;
}
#if BX_CPU_LEVEL > 4
else if (!strcmp("cpu", argv[arg+1])) {
int i = 0;
fprintf(stderr, "Supported CPU models:\n\n");
do {
fprintf(stderr, "%s\n", SIM->get_param_enum(BXPN_CPU_MODEL)->get_choice(i));
} while (i++ < SIM->get_param_enum(BXPN_CPU_MODEL)->get_max());
fprintf(stderr, "\n");
arg++;
}
#endif
} else {
print_usage();
}
SIM->quit_sim(0);
}
else if (!strcmp("-n", argv[arg])) {
load_rcfile = 0;
}
else if (!strcmp("-q", argv[arg])) {
SIM->get_param_enum(BXPN_BOCHS_START)->set(BX_QUICK_START);
}
else if (!strcmp("-log", argv[arg])) {
if (++arg >= argc) BX_PANIC(("-log must be followed by a filename"));
else SIM->get_param_string(BXPN_LOG_FILENAME)->set(argv[arg]);
}
#if BX_DEBUGGER
else if (!strcmp("-dbglog", argv[arg])) {
if (++arg >= argc) BX_PANIC(("-dbglog must be followed by a filename"));
else SIM->get_param_string(BXPN_DEBUGGER_LOG_FILENAME)->set(argv[arg]);
}
#endif
else if (!strcmp("-f", argv[arg])) {
if (++arg >= argc) BX_PANIC(("-f must be followed by a filename"));
else bochsrc_filename = argv[arg];
}
else if (!strcmp("-qf", argv[arg])) {
SIM->get_param_enum(BXPN_BOCHS_START)->set(BX_QUICK_START);
if (++arg >= argc) BX_PANIC(("-qf must be followed by a filename"));
else bochsrc_filename = argv[arg];
}
else if (!strcmp("-benchmark", argv[arg])) {
SIM->get_param_enum(BXPN_BOCHS_START)->set(BX_QUICK_START);
if (++arg >= argc) BX_PANIC(("-benchmark must be followed by a number"));
else SIM->get_param_num(BXPN_BOCHS_BENCHMARK)->set(atoi(argv[arg]));
}
#if BX_ENABLE_STATISTICS
else if (!strcmp("-dumpstats", argv[arg])) {
if (++arg >= argc) BX_PANIC(("-dumpstats must be followed by a number"));
else SIM->get_param_num(BXPN_DUMP_STATS)->set(atoi(argv[arg]));
}
#endif
else if (!strcmp("-r", argv[arg])) {
if (++arg >= argc) BX_PANIC(("-r must be followed by a path"));
else {
SIM->get_param_enum(BXPN_BOCHS_START)->set(BX_QUICK_START);
SIM->get_param_bool(BXPN_RESTORE_FLAG)->set(1);
SIM->get_param_string(BXPN_RESTORE_PATH)->set(argv[arg]);
}
}
#ifdef WIN32
else if (!strcmp("-noconsole", argv[arg])) {
// already handled in main() / WinMain()
}
#endif
#if BX_WITH_CARBON
else if (!strncmp("-psn", argv[arg], 4)) {
// "-psn" is passed if we are launched by double-clicking
// ugly hack. I don't know how to open a window to print messages in,
// so put them in /tmp/early-bochs-out.txt. Sorry. -bbd
io->init_log("/tmp/early-bochs-out.txt");
BX_INFO(("I was launched by double clicking. Fixing home directory."));
arg = argc; // ignore all other args.
setupWorkingDirectory (argv[0]);
// there is no stdin/stdout so disable the text-based config interface.
SIM->get_param_enum(BXPN_BOCHS_START)->set(BX_QUICK_START);
char cwd[MAXPATHLEN];
getwd (cwd);
BX_INFO(("Now my working directory is %s", cwd));
// if it was started from command line, there could be some args still.
for (int a=0; a<argc; a++) {
BX_INFO(("argument %d is %s", a, argv[a]));
}
}
#endif
#if BX_DEBUGGER
else if (!strcmp("-rc", argv[arg])) {
// process "-rc filename" option, if it exists
if (++arg >= argc) BX_PANIC(("-rc must be followed by a filename"));
else bx_dbg_set_rcfile(argv[arg]);
}
#endif
else if (argv[arg][0] == '-') {
print_usage();
BX_PANIC(("command line arg '%s' was not understood", argv[arg]));
}
else {
// the arg did not start with -, so stop interpreting flags
break;
}
arg++;
}
#if BX_WITH_CARBON
if(!getenv("BXSHARE"))
{
CFBundleRef mainBundle;
CFURLRef bxshareDir;
char bxshareDirPath[MAXPATHLEN];
BX_INFO(("fixing default bxshare location ..."));
// set bxshare to the directory that contains our application
mainBundle = CFBundleGetMainBundle();
BX_ASSERT(mainBundle != NULL);
bxshareDir = CFBundleCopyBundleURL(mainBundle);
BX_ASSERT(bxshareDir != NULL);
// translate this to a unix style full path
if(!CFURLGetFileSystemRepresentation(bxshareDir, true, (UInt8 *)bxshareDirPath, MAXPATHLEN))
{
BX_PANIC(("Unable to work out bxshare path! (Most likely path too long!)"));
return -1;
}
char *c;
c = (char*) bxshareDirPath;
while (*c != '\0') /* go to end */
c++;
while (*c != '/') /* back up to parent */
c--;
*c = '\0'; /* cut off last part (binary name) */
setenv("BXSHARE", bxshareDirPath, 1);
BX_INFO(("now my BXSHARE is %s", getenv("BXSHARE")));
CFRelease(bxshareDir);
}
#endif
#if BX_PLUGINS
// set a default plugin path, in case the user did not specify one
#if BX_WITH_CARBON
// if there is no stdin, then we must create our own LTDL_LIBRARY_PATH.
// also if there is no LTDL_LIBRARY_PATH, but we have a bundle since we're here
// This is here so that it is available whenever --with-carbon is defined but
// the above code might be skipped, as in --with-sdl --with-carbon
if(!isatty(STDIN_FILENO) || !getenv("LTDL_LIBRARY_PATH"))
{
CFBundleRef mainBundle;
CFURLRef libDir;
char libDirPath[MAXPATHLEN];
if(!isatty(STDIN_FILENO))
{
// there is no stdin/stdout so disable the text-based config interface.
SIM->get_param_enum(BXPN_BOCHS_START)->set(BX_QUICK_START);
}
BX_INFO(("fixing default lib location ..."));
// locate the lib directory within the application bundle.
// our libs have been placed in bochs.app/Contents/(current platform aka MacOS)/lib
// This isn't quite right, but they are platform specific and we haven't put
// our plugins into true frameworks and bundles either
mainBundle = CFBundleGetMainBundle();
BX_ASSERT(mainBundle != NULL);
libDir = CFBundleCopyAuxiliaryExecutableURL(mainBundle, CFSTR("lib"));
BX_ASSERT(libDir != NULL);
// translate this to a unix style full path
if(!CFURLGetFileSystemRepresentation(libDir, true, (UInt8 *)libDirPath, MAXPATHLEN))
{
BX_PANIC(("Unable to work out ltdl library path within bochs bundle! (Most likely path too long!)"));
return -1;
}
setenv("LTDL_LIBRARY_PATH", libDirPath, 1);
BX_INFO(("now my LTDL_LIBRARY_PATH is %s", getenv("LTDL_LIBRARY_PATH")));
CFRelease(libDir);
}
#elif BX_HAVE_GETENV && BX_HAVE_SETENV
if (getenv("LTDL_LIBRARY_PATH") != NULL) {
BX_INFO(("LTDL_LIBRARY_PATH is set to '%s'", getenv("LTDL_LIBRARY_PATH")));
} else {
BX_INFO(("LTDL_LIBRARY_PATH not set. using compile time default '%s'",
BX_PLUGIN_PATH));
setenv("LTDL_LIBRARY_PATH", BX_PLUGIN_PATH, 1);
}
#endif
#endif /* if BX_PLUGINS */
#if BX_HAVE_GETENV && BX_HAVE_SETENV
if (getenv("BXSHARE") != NULL) {
BX_INFO(("BXSHARE is set to '%s'", getenv("BXSHARE")));
} else {
BX_INFO(("BXSHARE not set. using compile time default '%s'",
BX_SHARE_PATH));
setenv("BXSHARE", BX_SHARE_PATH, 1);
}
#else
// we don't have getenv or setenv. Do nothing.
#endif
// initialize plugin system. This must happen before we attempt to
// load any modules.
plugin_startup();
#if BX_SUPPORT_PCIUSB
// USB HC devices depend on USB core symbols, so we have to load it here.
// The devices init() unloads it if not used.
PLUG_load_plugin(usb_common, PLUGTYPE_CORE);
#endif
int norcfile = 1;
if (SIM->get_param_bool(BXPN_RESTORE_FLAG)->get()) {
load_rcfile = 0;
norcfile = 0;
}
// load pre-defined optional plugins before parsing configuration
SIM->opt_plugin_ctrl("*", 1);
SIM->init_save_restore();
SIM->init_statistics();
if (load_rcfile) {
// parse configuration file and command line arguments
#ifdef WIN32
int length;
if (bochsrc_filename != NULL) {
lstrcpy(bx_startup_flags.initial_dir, bochsrc_filename);
length = lstrlen(bx_startup_flags.initial_dir);
while ((length > 1) && (bx_startup_flags.initial_dir[length-1] != 92)) length--;
bx_startup_flags.initial_dir[length] = 0;
} else {
bx_startup_flags.initial_dir[0] = 0;
}
#endif
if (bochsrc_filename == NULL) bochsrc_filename = bx_find_bochsrc ();
if (bochsrc_filename)
norcfile = bx_read_configuration(bochsrc_filename);
}
if (norcfile) {
// No configuration was loaded, so the current settings are unusable.
// Switch off quick start so that we will drop into the configuration
// interface.
if (SIM->get_param_enum(BXPN_BOCHS_START)->get() == BX_QUICK_START) {
if (!SIM->test_for_text_console())
BX_PANIC(("Unable to start Bochs without a bochsrc.txt and without a text console"));
else
BX_ERROR(("Switching off quick start, because no configuration file was found."));
}
SIM->get_param_enum(BXPN_BOCHS_START)->set(BX_LOAD_START);
}
if (SIM->get_param_bool(BXPN_RESTORE_FLAG)->get()) {
if (arg < argc) {
BX_ERROR(("WARNING: bochsrc options are ignored in restore mode!"));
}
}
else {
// parse the rest of the command line. This is done after reading the
// configuration file so that the command line arguments can override
// the settings from the file.
if (bx_parse_cmdline(arg, argc, argv)) {
BX_PANIC(("There were errors while parsing the command line"));
return -1;
}
}
return 0;
}
该段代码是Bochs的初始化函数,负责解析命令行参数、加载配置文件并进行配置,初始化插件系统,并根据命令行参数覆盖配置文件中的设置,最后返回初始化状态。以下是其核心逻辑。
- 解析命令行参数:
- 遍历命令行参数,依次解析每个参数,并根据参数的含义进行相应处理:
- 检查是否存在帮助参数,如
--help
,-h
,/?
,若存在则打印支持的特性和支持的CPU模型,并退出Bochs仿真器。 - 处理其他常见参数:
-n
:禁止加载配置文件。-q
:启用快速启动模式。-log
:设置日志文件名。-f
:指定配置文件路径。-qf
:启用快速启动模式并指定配置文件路径。-benchmark
:设置基准测试参数。-r
:恢复模式,指定恢复文件路径。-noconsole
:Windows平台下,禁用控制台输出。- 在macOS平台下处理
-psn
参数,用于处理由双击应用图标启动的情况,设置默认工作目录并禁用文本配置界面。 - 处理其它特定参数,如处理调试器相关参数等。
- 本地化设置初始化:
- 如果系统支持本地化设置(即包含头文件“locale.h”),则通过
setlocale(LC_ALL, "")
初始化本地化设置,以便后续使用标准库函数时能正确处理本地化字符。
- 如果系统支持本地化设置(即包含头文件“locale.h”),则通过
- 创建仿真器对象和设置退出上下文:
- 调用
bx_init_siminterface()
函数创建Bochs仿真器对象,并设置退出上下文jmp_buf
,用于后续通过setjmp()
和longjmp()
进行异常处理和退出。
- 调用
- 初始化调试环境和选项:
- 调用
bx_init_bx_dbg()
和bx_init_options()
函数分别初始化调试环境和选项,确保Bochs仿真器处于良好的调试状态,并设置各项仿真器选项。
- 调用
- 打印 Bochs 版本信息:
- 调用
bx_print_header()
函数打印Bochs的版本信息,包括版本号、作者、版权等信息。
- 调用
- 设置仿真器启动方式和日志文件:
- 根据命令行参数解析设置仿真器的启动方式,如快速启动、日志文件名等。
- 解析命令行参数:
- 遍历命令行参数,解析并处理各种命令行选项,包括帮助信息、日志文件、配置文件、快速启动、恢复模式等。
- 初始化插件系统:
- 调用
plugin_startup()
函数初始化插件系统,包括加载预定义的可选插件,并为后续使用插件做准备。
- 调用
- 读取和解析配置文件:
- 根据命令行参数指定的配置文件名(如果存在),读取并解析配置文件,或者根据命令行参数设置仿真器的各项配置。
- 解析命令行参数并应用:
- 解析剩余的命令行参数,并将其应用到仿真器的设置中,确保命令行参数可以覆盖配置文件中的设置。
- 返回初始化结果:
- 返回初始化的结果,通常为
0
表示初始化成功,非0
表示初始化失败或出现错误。
- 返回初始化的结果,通常为
可以发现,该函数的主要目的是进行通过Bochs启动待检测目标之前的各项初始化工作,我们主要关注上面标黄的两部分逻辑,这两部分逻辑是顺承关系。即首先通过解析命令行参数-f
加载配置文件(即我们设置好的“/bochspwn/bochsrc.txt”文件),并将其存储在bochsrc_filename
变量中。然后通过bx_read_configuration(bochsrc_filename);
函数调用来读取和解析配置文件,最后将解析好的配置存储在norcfile
变量中。
- 选择配置界面
该逻辑主要由下面这段代码实现。
在这里可以发现由多种配置界面可供我们选择,不过由于我们最开始在编译“bochs.exe”的时候选定了Windows平台,故此时应使用init_win32_config_interface();
函数调用来完成配置阶段的选择。而init_win32_config_interface()
函数实现在“/bochs-2.6.9/gui/win32dialog.cc”的第704行。
该函数用于初始化Win32配置界面,注册了名为“win32config”的配置界面,并指定了相应的回调函数win32_ci_callback
。故当我们启动配置界面时,将使用win32_ci_callback
函数作为回调函数。
- 启动配置界面
该逻辑由SIM->configuration_interface(ci_name, CI_START);
函数调用实现。而configuration_interface()
函数实现在“/bochs-2.6.9/gui/siminterface.cc”的第863行。
这段代码是一个类成员函数bx_real_sim_c::configuration_interface
的实现。它用于处理配置接口的相关操作。具体来说:
- 函数首先获取当前配置接口的名称。
- 然后检查是否已加载配置接口,如果未加载则发出警告并返回错误。
- 接着检查是否试图加载不同的配置接口,如果是则发出警告并返回错误。
- 然后根据配置接口的名称设置相应的标志。
- 进入配置模式,并调用注册的配置接口回调函数进行配置操作。
- 最后退出配置模式,并返回操作结果。
这段代码主要负责配置接口的管理和调用,确保正确加载并执行配置操作。而其中的核心逻辑为上方标黄的部分,即(*ci_callback)(ci_callback_data, command);
函数调用。值得注意的是,在“选择配置界面”阶段,我们已经将回调函数设置为win32_ci_callback
,故下面我们将分析实现在“/bochs-2.6.9/gui/win32dialog.cc”的第665行的win32_ci_callback()
函数。
这段代码定义了一个静态函数win32_ci_callback()
,作为配置接口的回调函数。具体来说:
- 在收到
CI_START
命令时,该函数设置了通知回调函数,并根据当前Bochs的启动方式进行不同的处理:- 如果Bochs是以快速启动模式启动的,则直接开始仿真并退出。
- 如果Bochs是以菜单界面启动的,则显示主菜单对话框,根据用户的选择开始仿真并退出。
- 在收到
CI_RUNTIME_CONFIG
命令时,根据当前是否有GUI控制台来进行不同的处理:- 如果没有GUI控制台,则显示菜单界面,并在用户退出时退出仿真。
- 如果有GUI控制台,则显示文本配置界面进行运行时配置。
- 在收到
CI_SHUTDOWN
命令时,不进行任何操作。
该回调函数主要负责根据配置接口命令执行相应的操作,如启动仿真、显示菜单界面或运行时配置等。
可以发现,Bochs最终是通过该函数启动的待测试目标。不过我们并不基于该函数继续向下分析了,因为再向下继续分析本文档就成了Bochs的研究报告了,从而偏离了研究Bochspwn的初衷。我们只需要知道,最终Bochs在该处使用win32_ci_callback()
函数完成了对待测试目标的启动即可。
1.2.2.5.2、分析内存访问
当我们使用Bochs启动待测试目标后,就要开始对内存访问进行分析以检测潜在的Double-Fetch漏洞了。那么Bochspwn是怎么实现对潜在的Double-Fetch漏洞的检测的呢?这就是我们本章节要分析的内容。
在正式分析之前,我们回顾一下,前面我们在编译“bochs.exe”的时候,引入了事先编译好的“libinstrument.a”这个库,该库就是使Bochspwn对潜在的Double-Fetch漏洞进行检测的核心。而编译“libinstrument.a”的核心源代码文件为“/bochs-2.6.9/instrument/bochspwn/instrument.cc”,故最终Bochspwn就是通过该源代码文件中的代码对潜在的Double-Fetch漏洞进行检测的。故本章节分析的重点将针对“instrument.cc”源代码文件。
在正式分析之前,我们思考一个问题,Bochspwn是如何捕获到用户/操作系统对内存的访问的呢?我们只有了解Bochspwn是如何捕获到对内存的操作,才能进而分析Bochspwn是如何对漏洞进行检测的。为了搞清楚这个问题,我们首先来到“bochs-2.6.9/cpu/cpu.h”的第554行实现的BX_NOTIFY_LIN_MEMORY_ACCESS
宏。
该宏用于通知模拟器或调试器有关线性内存访问的详细信息,包括地址、大小、内存类型和读写操作等,以便用于内部分析或调试。我们在这里主要关注BX_INSTR_LIN_ACCESS
这个宏,该宏实际在多个源代码文件中都有实现,比如。
-
“/bochs-2.6.9/instrument/example0/instrument.h”的第95行:
-
“/bochs-2.6.9/instrument/example1/instrument.h”的第146行:
-
“/bochs-2.6.9/instrument/stubs/instrument.h”的第122行:
不管BX_INSTR_LIN_ACCESS
宏在哪实现,最终都调用了bx_instr_lin_access()
函数,虽然bx_instr_lin_access()
函数在Bochs的源代码中就有所实现,不过我们在编译“bochs.exe”的时候,引入了事先编译好的“libinstrument.a”这个库,而“libinstrument.a”中就包含重写的bx_instr_lin_access()
函数,故“bochs.exe”会首先使用“libinstrument.a”中的bx_instr_lin_access()
函数。
所以现在我们清楚了,当在待检测目标中进行了任何内存操作后,都会通过BX_INSTR_LIN_ACCESS
捕获,最终将捕获到的信息传递给“libinstrument.a”中的bx_instr_lin_access()
函数,从而实现对内存访问进行分析以检测潜在的Double-Fetch漏洞。故“libinstrument.a”中的bx_instr_lin_access()
函数才是我们接下来分析的重点。
经过上面的分析我们现在清楚,最终在待检测目标中的所有内存操作信息,都将被传递到实现在“/bochspwn/instrumentation/instrument.cc”的第112行实现的bx_instr_lin_access()
函数。
// Callback called on attempt to access linear memory.
//
// Note: the BX_INSTR_LIN_ACCESS instrumentation doesn't work when
// repeat-speedups feature is enabled. Always remember to set
// BX_SUPPORT_REPEAT_SPEEDUPS to 0 in config.h, otherwise Bochspwn might
// not work correctly.
void bx_instr_lin_access(unsigned cpu, bx_address lin, bx_address phy,
unsigned len, unsigned memtype, unsigned rw) {
BX_CPU_C *pcpu = BX_CPU(cpu);
// Not going to use physical memory address.
(void)phy;
// Read-write instructions are currently not interesting.
if (rw == BX_RW)
return;
// Is the CPU in protected or long mode?
unsigned mode = 0;
// Note: DO NOT change order of these ifs. long64_mode must be called
// before protected_mode, since it will also return "true" on protected_mode
// query (well, long mode is technically protected mode).
if (pcpu->long64_mode()) {
#if BX_SUPPORT_X86_64
mode = 64;
#else
return;
#endif // BX_SUPPORT_X86_64
} else if (pcpu->protected_mode()) {
// This is either protected 32-bit mode or 32-bit compat. long mode.
mode = 32;
} else {
// Nothing interesting.
// TODO(gynvael): Well actually there is the smm_mode(), which
// might be a little interesting, even if it's just the bochs BIOS
// SMM code.
return;
}
// Is pc in kernel memory area?
// Is lin in user memory area?
bx_address pc = pcpu->prev_rip;
if (!invoke_system_handler(BX_OS_EVENT_CHECK_KERNEL_ADDR, &pc, NULL) ||
!invoke_system_handler(BX_OS_EVENT_CHECK_USER_ADDR, &lin, NULL)) {
return; /* pc not in ring-0 or lin not in ring-3 */
}
// Check if the access meets specified operand length criteria.
if (rw == BX_READ) {
if (len < globals::config.min_read_size || len > globals::config.max_read_size) {
return;
}
} else {
if (len < globals::config.min_write_size || len > globals::config.max_write_size) {
return;
}
}
// Save basic information about the access.
log_data_st::mem_access_type access_type;
switch (rw) {
case BX_READ:
access_type = log_data_st::MEM_READ;
break;
case BX_WRITE:
access_type = log_data_st::MEM_WRITE;
break;
case BX_EXECUTE:
access_type = log_data_st::MEM_EXEC;
break;
case BX_RW:
access_type = log_data_st::MEM_RW;
break;
default: abort();
}
// Disassemble current instruction.
static Bit8u ibuf[32] = {0};
static char pc_disasm[64];
if (read_lin_mem(pcpu, pc, sizeof(ibuf), ibuf)) {
disassembler bx_disassemble;
bx_disassemble.disasm(mode == 32, mode == 64, 0, pc, ibuf, pc_disasm);
}
// With basic information filled in, process the access further.
process_mem_access(pcpu, lin, len, pc, access_type, pc_disasm);
}
这段代码是一个回调函数,用于处理对线性内存的访问。该函数通过检查CPU模式和内存访问的合法性,并保存基本信息,为进一步处理提供了必要的数据基础。以下是对其功能的详细分析:
- 获取CPU模式:
- 通过检查CPU的模式,确定其处于保护模式、长模式还是其他模式。
- 如果CPU处于长模式(64位模式),则将模式设置为64位。
- 如果CPU处于保护模式(32位模式),则将模式设置为32位。
- 检查内存访问的有效性:
- 检查当前指令指针(pc)是否位于内核空间,以及要访问的线性地址(lin)是否位于用户空间。
- 如果指令指针不在内核模式下,或者线性地址不在用户模式下,则说明访问不合法,函数直接返回。
- 检查访问的操作数长度:
- 根据读/写操作,检查访问的数据长度是否在指定的范围内。如果不在范围内,则忽略该访问。
- 保存基本信息:
- 根据读/写/执行的不同,确定访问的类型,并保存到相应的数据结构中。
- 反汇编当前指令:
- 通过读取当前指令的机器码,并调用反汇编器,获取当前指令的汇编代码。
- 反汇编后的指令信息将用于进一步处理访问。
- 进一步处理访问:
- 将获取的访问信息(线性地址、长度、操作类型、指令信息)传递给进一步处理函数,进行更深入的处理或记录。
该函数的逻辑比较清楚,首先对当前内存操作进行两步检查,包括:
- 检查当前CPU的默认,根据不同的CPU模式进行对应的设置
- 检查当前内存操作是访问用户空间还是内核空间,因为Double-Fetch漏洞只可能发生在用户空间与内核空间交互的情况下
- 检查当前访问的地址是否在指定的范围内,如果不在指定的范围内,说明并不是我们想要进行漏洞检测的内存访问
如果通过了以上检查,那么说明这是一个可能引发潜在的Double-Fetch漏洞的内存访问。那么就将此次内存访问交予其它函数,等待进一步处理。
在这整个过程中我们主要关注上面标红的两处逻辑,因为这两处逻辑比较重要,并且包含了后续处理的过程。下面我们将对其详细分析。
- 检查内存访问的有效性
该逻辑由实现在“/bochspwn/instrumentation/invoke.cc”的第25行的invoke_system_handler()
函数处理的。
该函数实现了根据系统事件类型调用相应的系统事件处理器的功能,并提供了异常处理机制以确保正确的系统事件处理器被调用。具体来说其逻辑为。
- 静态变量声明:
- 函数内部声明了一个静态指针
h
,用于存储当前选择的系统事件处理器函数指针数组。
- 函数内部声明了一个静态指针
- 系统事件处理器查找:
- 如果
h
为空,则会根据全局配置中指定的系统名称,在预定义的系统事件处理器列表中查找相应的处理器函数指针数组。 - 使用循环遍历预定义的系统事件处理器列表
kSystemEventHandlers
,直到找到匹配的系统名称或遍历完列表。
- 如果
- 系统事件处理器调用:
- 找到匹配的处理器数组后,将其地址存储在
h
中,以备后续调用。 - 根据传入的系统事件类型
type
,通过h[type]
调用相应处理器数组中的处理器函数。 - 将参数
arg1
和arg2
传递给处理器函数,并将其返回值直接返回给调用者。
- 找到匹配的处理器数组后,将其地址存储在
- 异常处理:
- 如果在全局配置中指定的系统名称未找到匹配的处理器数组,则调用
abort()
终止程序执行,表示发生了严重错误。
- 如果在全局配置中指定的系统名称未找到匹配的处理器数组,则调用
该函数的核心逻辑是通过判断给定的目标操作系统,选择对应的处理函数对内存访问进行检查(即上面标红的逻辑)。但是这些实现的细节都在哪里呢?其实都在kSystemEventHandlers
变量中。而kSystemEventHandlers
定义在“/bochspwn/instrumentation/invoke.h”的第54行。
这段代码定义了一个结构体数组 kSystemEventHandlers,用于存储不同操作系统的系统事件处理器函数指针数组。
- 每个结构体元素包含两个字段:
system
和handlers
。 system
字段存储操作系统的名称,例如windows
、linux
、freebsd
等。handlers
字段是一个函数指针数组,存储了与该操作系统相关的系统事件处理器函数指针。- 每个操作系统对应一个结构体元素,其中包含了该操作系统需要的系统事件处理器函数指针数组。
- 数组的最后一个元素的
system
字段为NULL
,用于表示数组的结束。
此数组的目的是根据当前的操作系统,选择相应的系统事件处理器函数指针数组。而我们测试的操作系统为Linux,所以我们只关注关于Linux操作系统的处理函数即可。在这里我们可以发现,对于Linux操作系统的处理函数有五个。
init
该函数实现在“/bochspwn/instrumentation/os_linux.cc”的第83行。
bool init(const char *config_path, void *unused) {
char buffer[256];
// Read Linux-specific configuration.
READ_INI_INT(config_path, globals::config.os_version, "thread_size",
buffer, sizeof(buffer), &conf_thread_size);
READ_INI_INT(config_path, globals::config.os_version, "thread_info_task",
buffer, sizeof(buffer), &off_thread_info_task);
READ_INI_INT(config_path, globals::config.os_version, "task_struct_pid",
buffer, sizeof(buffer), &off_task_struct_pid);
READ_INI_INT(config_path, globals::config.os_version, "task_struct_tgid",
buffer, sizeof(buffer), &off_task_struct_tgid);
READ_INI_INT(config_path, globals::config.os_version, "task_struct_comm",
buffer, sizeof(buffer), &off_task_struct_comm);
READ_INI_INT(config_path, globals::config.os_version, "task_comm_len",
buffer, sizeof(buffer), &conf_task_comm_len);
READ_INI_ULL(config_path, globals::config.os_version, "modules",
buffer, sizeof(buffer), &addr_modules);
READ_INI_INT(config_path, globals::config.os_version, "module_list",
buffer, sizeof(buffer), &off_module_list);
READ_INI_INT(config_path, globals::config.os_version, "module_name",
buffer, sizeof(buffer), &off_module_name);
READ_INI_INT(config_path, globals::config.os_version, "module_core",
buffer, sizeof(buffer), &off_module_core);
READ_INI_INT(config_path, globals::config.os_version, "module_core_size",
buffer, sizeof(buffer), &off_module_core_size);
READ_INI_INT(config_path, globals::config.os_version, "module_name_len",
buffer, sizeof(buffer), &conf_module_name_len);
READ_INI_ULL(config_path, globals::config.os_version, "kernel_start",
buffer, sizeof(buffer), &kernel_start);
READ_INI_ULL(config_path, globals::config.os_version, "kernel_end",
buffer, sizeof(buffer), &kernel_end);
// Put the kernel address and size in the special module list.
module_info *mi = new module_info(kernel_start, kernel_end - kernel_start, "kernel");
events::event_new_module(mi);
// Check some assumptions.
if (conf_task_comm_len >= MAX_TASK_COMM_LEN) {
fprintf(stderr,
"error: task_comm_len in config is larger than MAX_TASK_COMM_LEN;\n"
" you can recompile with -DMAX_TASK_COMM_LEN=<SizeYouNeed>\n"
" and try again\n");
abort();
}
if (conf_module_name_len >= MAX_MODULE_NAME_LEN) {
fprintf(stderr,
"error: conf_module_name_len in config is larger than MAX_MODULE_NAME_LEN;\n"
" you can recompile with -DMAX_MODULE_NAME_LEN=<SizeYouNeed>\n"
" and try again\n");
abort();
}
// Read the configuration specific to guest bitness.
if (globals::config.bitness == 32) {
guest_ptr_size = 4;
// This depends on the kernel configuration options
// (quote from Linux kernel - x86/Kconfig):
// config PAGE_OFFSET
// hex
// default 0xB0000000 if VMSPLIT_3G_OPT
// default 0x80000000 if VMSPLIT_2G
// default 0x78000000 if VMSPLIT_2G_OPT
// default 0x40000000 if VMSPLIT_1G
// default 0xC0000000
// depends on X86_32
// We assume it's 0xC0000000.
//
// TODO(gynvael): Move this to config and fetch it in init().
user_space_boundary = 0xC0000000;
kernel_space_boundary = 0xC0000000;
} else {
guest_ptr_size = 8;
user_space_boundary = 0x0000080000000000LL;
kernel_space_boundary = 0xffff800000000000LL;
}
return true;
}
该函数的作用是从配置文件中读取特定于Linux操作系统的配置信息,并进行一些初始化设置,包括创建内核模块、检查假设条件和设置全局变量。具体来说,其逻辑为。
- 函数参数:
- 函数接受两个参数,
config_path
是配置文件的路径,unused
是一个未使用的参数。
- 函数接受两个参数,
- 读取配置信息:
- 使用
READ_INI_INT
和READ_INI_ULL
宏从配置文件中读取Linux特定的配置信息。 - 读取的配置项包括线程大小、任务结构的偏移量、模块信息、内核起始地址和结束地址等。
- 使用
- 创建内核模块:
- 将内核的起始地址和大小创建为一个特殊的模块。
- 使用
events::event_new_module
函数将内核模块添加到模块列表中。
- 检查假设:
- 对一些假设进行了检查,如任务通信长度和模块名称长度是否超出了预设的最大值。
- 如果超出了最大值,则输出错误信息并终止程序。
- 设置全局变量:
- 根据操作系统的位数设置了一些全局变量,如
guest_ptr_size
表示指针大小,user_space_boundary
和kernel_space_boundary
表示用户空间和内核空间的边界。
- 根据操作系统的位数设置了一些全局变量,如
- 返回值:
- 函数返回
true
,表示初始化成功。
- 函数返回
总之,该函数从我们之前配置的“/bochspwn/config.txt”文件中读取配置信息,并设置相应的全局变量。
-
check_kernel_addr
该函数实现在“/bochspwn/instrumentation/os_linux.cc”的第167行。该函数比较简单,其作用就是检查给定的内核地址是否位于预期的内核地址范围内。
-
check_user_addr
该函数实现在“/bochspwn/instrumentation/os_linux.cc”的第175行。该函数比较简单,其作用就是检查给定的用户态地址是否位于预期的用户地址范围内。
-
fill_cid
该函数实现在“/bochspwn/instrumentation/os_linux.cc”的第175行。
该函数的作用是获取内核模式下的栈顶指针,并通过读取该栈顶指针处的任务结构信息,填充给定的客户端标识结构体。具体来说其逻辑如下。
- 准备阶段:
- 定义函数
fill_cid
,接受指向CPU对象的指针pcpu
和指向客户端标识结构体的指针cid
。
- 定义函数
- 获取内核栈顶指针:
- 在函数内部声明变量
kernel_rsp
,用于存储内核模式下的栈顶指针值,初始值为0
。 - 使用
read_lin_mem()
函数读取内核模式下的栈顶指针的值,读取的地址是当前任务寄存器(TR)中的基地址加上一个偏移量。
- 在函数内部声明变量
- 检查栈顶指针读取是否成功:
- 如果读取栈顶指针的值失败,则函数返回
false
。
- 如果读取栈顶指针的值失败,则函数返回
- 获取任务结构信息:
- 从获取的内核栈顶指针
kernel_rsp
中读取任务(进程)结构(task_struct)中的进程ID和线程ID。这里需要注意将kernel_rsp – 1
的值传递给获取任务结构的函数,因为内核栈顶指针通常指向栈顶下一个位置,而不是栈顶本身。
- 从获取的内核栈顶指针
- 检查获取任务结构信息是否成功:
- 如果获取任务结构信息失败,则函数返回
false
。
- 如果获取任务结构信息失败,则函数返回
- 填充客户端标识结构体:
- 将获取的进程ID和线程ID填充到传入的客户端标识结构体
cid
中。
- 将获取的进程ID和线程ID填充到传入的客户端标识结构体
- 返回操作结果:
- 最后,函数返回
true
表示操作成功。
- 最后,函数返回
fill_info
该函数实现在“/bochspwn/instrumentation/os_linux.cc”的第216行。
bool fill_info(BX_CPU_C *pcpu, void *unused) {
bx_address pc = globals::last_ld.pc();
// Fetch task structure address, and pid and tgid fields.
uint32_t tgid, pid;
uint64_t addr_task_struct;
if (!get_task_struct_pid_gid(pcpu, pcpu->gen_reg[BX_64BIT_REG_RBP].rrx,
&addr_task_struct, &tgid, &pid)) {
return false;
}
globals::last_ld.set_process_id(tgid);
globals::last_ld.set_thread_id(pid);
// Get the image file name.
// Note: The task_comm_len vs MAX_TASK_COMM_LEN is checked in the
// init() function.
char name_buffer[MAX_TASK_COMM_LEN + 1] = {0};
if (!read_lin_mem(pcpu, addr_task_struct + off_task_struct_comm,
conf_task_comm_len, name_buffer)) {
return false;
}
globals::last_ld.set_image_file_name(name_buffer);
// Get the thread create time.
// Note: It seems linux kernel doesn't explicitly store the time,
// but the time can be get from /proc/PID creation time - this might
// be a little tricky from CPU level though. Will see.
// Note2: address of task_struct or thread_info is good enough here btw.
globals::last_ld.set_create_time(addr_task_struct);
// Fill in the syscall cound.
thread_info& info = globals::thread_states[client_id(tgid, pid)];
globals::last_ld.set_syscall_count(info.syscall_count);
globals::last_ld.set_syscall_id(info.last_syscall_id);
// Set the call stack.
uint64_t ip = pc;
uint64_t bp = pcpu->gen_reg[BX_64BIT_REG_RBP].rrx;
int mod_idx = -1;
module_info *mi = NULL;
for (unsigned int i = 0; i < globals::config.callstack_length &&
ip >= kernel_space_boundary &&
bp >= kernel_space_boundary; i++) {
// Optimization: check last module first.
if (!mi || mi->module_base > ip || mi->module_base + mi->module_size <= ip) {
mod_idx = find_module(ip);
if (mod_idx == -1) {
mod_idx = update_module_list(pcpu, ip);
}
if (mod_idx != -1) {
mi = globals::modules[mod_idx];
} else {
mi = NULL;
}
}
log_data_st::callstack_item *new_item = globals::last_ld.add_stack_trace();
new_item->set_module_idx(mod_idx);
if (mi) {
new_item->set_relative_pc(ip - mi->module_base);
} else {
new_item->set_relative_pc(ip);
}
if (!bp || !read_lin_mem(pcpu, bp + guest_ptr_size, guest_ptr_size, &ip) ||
!read_lin_mem(pcpu, bp, guest_ptr_size, &bp)) {
break;
}
}
return true;
}
这个函数的作用是在系统调用发生前填充全局的最后一次线性内存访问信息对象。它通过获取与当前线程相关的任务结构地址、进程ID、线程ID、映像文件名和线程创建时间等信息,并根据当前的程序计数器值(PC)和栈基指针(BP)来填充调用栈信息。具体来说其逻辑如下。
- 获取当前线程相关的任务结构地址、进程ID和线程ID:
- 使用
get_task_struct_pid_gid
函数获取任务结构的地址、进程ID(tgid
)和线程ID(pid
)。 - 将获取到的进程ID和线程ID设置到全局的最后一次线性内存访问信息对象中。
- 使用
- 获取映像文件名:
- 使用
read_lin_mem
函数从任务结构中读取映像文件名。 - 将读取到的映像文件名设置到全局的最后一次线性内存访问信息对象中。
- 使用
- 获取线程创建时间:
- 目前尚未实现获取线程创建时间的功能。
- 填充系统调用计数和系统调用ID:
- 从全局的线程状态中获取当前线程的系统调用计数和最后一次系统调用ID。
- 将获取到的系统调用计数和系统调用ID设置到全局的最后一次线性内存访问信息对象中。
- 设置调用栈信息:
- 使用当前程序计数器值(PC)和栈基指针(BP)来构建调用栈信息。
- 遍历栈帧,获取每个栈帧的模块索引和相对于模块起始地址的偏移。
- 将每个栈帧的模块索引和偏移设置到调用栈信息中,并将调用栈信息添加到全局的最后一次线性内存访问信息对象中。
- 返回信息
- 返回
true
表示填充信息成功。
- 返回
现在我们已经清楚了对于Linux操作系统的处理的五个函数的作用,不过也带来了问题,在最开始的绿色逻辑中(即“/bochspwn/instrumentation/invoke.cc”第25行的invoke_system_handler()
函数中的h[type](arg1, arg2);
函数调用)究竟调用了这五个函数中的哪一个?要搞明白这个问题,我们还得继续向前分析。
我们回顾一下,在本小节最开始,我们分析的是“检查内存访问的有效性”这个逻辑(即invoke_system_handler(BX_OS_EVENT_CHECK_KERNEL_ADDR, &pc, NULL)
函数调用和invoke_system_handler(BX_OS_EVENT_CHECK_USER_ADDR, &lin, NULL)
)。虽然对于调用的invoke_system_handler()
函数我们已经分析的很清楚了(最终通过刚刚介绍的五个函数来处理请求),不过我们忽略了其参数,我们以invoke_system_handler(BX_OS_EVENT_CHECK_KERNEL_ADDR, &pc, NULL)
函数调用为例,其中有一个参数为BX_OS_EVENT_CHECK_KERNEL_ADDR
。该参数定义在“/bochspwn/instrumentation/invoke.h”的第42行。
很明显这是一个枚举,而根据枚举的特性,BX_OS_EVENT_CHECK_KERNEL_ADDR
的值应该为1
。故invoke_system_handler(BX_OS_EVENT_CHECK_KERNEL_ADDR, &pc, NULL)
函数调用中传入的BX_OS_EVENT_CHECK_KERNEL_ADDR
参数的值就是1
。所以最终h[type](arg1, arg2);
函数调用(此时type
就是BX_OS_EVENT_CHECK_KERNEL_ADDR
参数,即为1
)就会调用这五个函数中索引为1
的函数(即check_kernel_addr()
函数),从而实现检查给定的内核地址是否位于预期的内核地址范围内的目的。
对于其它操作也是一样的过程,即根据枚举所定义的值,来调用对应索引位置的处理函数,我们在此不再赘述。总而言之,这部分做了两件事情:
① 检查给定的内核地址是否位于预期的内核地址范围内
② 检查给定的用户态地址是否位于预期的用户地址范围内
- 进一步处理访问
该逻辑由实现在“/bochspwn/instrumentation/instrument.cc”的第308行实现的process_mem_access()
处理。
该函数用于处理内存访问,检查当前访问是否连续,并根据连续性决定是否输出上一次访问的信息,并填充当前内存访问的相关信息。具体来说其逻辑如下。
- 静态变量初始化:
- 声明静态变量
last_repeated
,用于记录连续内存访问次数,初始化为0
。
- 声明静态变量
- 连续内存访问检查:
- 通过一系列条件判断来确定当前内存访问是否连续于上一次访问。
- 条件包括:
- 上一次访问的程序计数器(
pc
)与当前指令的程序计数器不同。 - 上一次访问的长度(
len
)与当前访问的长度不同。 - 上一次访问的线性地址加上上一次访问的长度乘以
last_repeated
不等于当前访问的线性地址。 - 上一次访问的类型(
access_type
)与当前访问的类型不同。 - 上一次访问是否存在,即
globals::last_ld_present
为假。
- 处理不连续内存访问:
- 如果当前内存访问与上一次访问不连续:
- 如果上一次访问存在,则记录上一次访问的连续次数,并输出上一次访问的信息。
- 清空上一次访问的信息。
- 填充当前内存访问的相关信息到
globals::last_ld
中。 - 将
last_repeated
设置为1
,表示当前内存访问是第一次连续访问。 - 调用
invoke_system_handler(BX_OS_EVENT_FILL_INFO, pcpu, NULL)
来填充当前内存访问的额外信息(之前分析过)。
- 处理连续内存访问:
- 如果当前内存访问与上一次访问连续,则将
last_repeated
递增。
- 如果当前内存访问与上一次访问连续,则将
很明显,该函数是Bochspwn框架最终进行Double-Fetch漏洞检测的核心函数。通过捕获对内存的连续访问来检测到潜在的Double-Fetch漏洞。不过该函数的逻辑不太好理解,按理说,当我们捕获到对同一块内核地址空间的内存连续访问后,应该保存此次内存访问信息,因为这可能是一个潜在的Double-Fetch漏洞;而对于没有对同一块内核地址空间的内存访问,我们应该忽略。这个逻辑是很好理解且合理的。不过上述函数的逻辑恰恰相反,即。
- 捕获到对同一块内核地址空间的内存连续访问后,不进行操作,仅仅将
last_repeated
递增 - 捕获到对内核地址空间的内存访问不是同一块区域后,保存上一次内存访问信息,并更新内存访问信息以备下一次使用
当我阅读到此处代码时,也搞不懂这套逻辑设计的意义,我甚至认为这根本检测不到潜在的Double-Fetch漏洞,相信读者也有这个疑惑。当仔细阅读代码,并举一些适当的例子后发现,其实作者这样做也是合理的,这种方法可以反向的检测到潜在的Double-Fetch漏洞。比如,我们来看一个简例。
假设现在有对内核地址空间的内存访问序列:“…ABBC…”(该内存访问序列是由用户操作的,故这是已知的),并假设“A”与“C”与其前后的内存访问并不连续。很明显在这个例子中,“B”是一个连续的内存访问,有可能引发Double-Fetch漏洞,我们来看Bochspwn框架如何处理该内存访问序列,并最终成功检测到潜在的Double-Fetch漏洞。
- Bochspwn首先会捕获“A”。按照作者的代码逻辑会保存“A”上一次的内存访问信息(该信息可以忽略)。
- 然后Bochspwn会捕获“B”,因为“B”与“A”不同。故按照作者的代码逻辑会保存“A”的内存访问信息。
- 然后Bochspwn会捕获“B”,因为“B”与“B”(上一次的“B”)相同。故按照作者的代码逻辑不会做任何有意义的处理。
- 然后Bochspwn会捕获“C”,因为“C”与“B”不同。故按照作者的代码逻辑会保存“B”的内存访问信息。
- 然后Bochspwn会捕获“C”后面的内存访问序列,因为“C”与其不同(根据假设得来)。故按照作者的代码逻辑会保存“C”的内存访问信息。
此时我们注意,除去忽略的内存访问信息,此时我们应该保存了“A”,“B”和“C”这三个内存访问信息。而我们已知的内存访问序列为“…ABBC…”,刚好将连续的内存访问“B”排除掉了,通过这种“反向分析”的方式,最终可以确定“B”内存访问就是潜在的Double-Fetch漏洞。
1.2.2.5.3、转储漏洞检测结果
经过上一章节的分析,我们清楚了Bochspwn检测潜在的Double-Fetch漏洞的逻辑,可是Bochspwn又是如何保存漏洞检测结果的呢?其实该逻辑就是上一章节最后标绿的部分,即events::event_process_log();
函数调用。event_process_log()
函数实现在“bochspwn/instrumentation/events.cc”的第70行。
这个函数用于处理漏洞日志数据。它根据配置将最新的漏洞日志数据写入到文件中,可以选择以文本形式写入或者以二进制形式写入。其具体逻辑如下。
- 文件写入模式选择:
- 根据配置选择写入模式:文本模式或二进制模式。
- 文本模式写入:
- 如果配置为文本模式,则使用
fprintf
函数将日志数据格式化为文本,并写入文件。 - 使用
fprintf
将日志数据以文本形式写入文件。
- 如果配置为文本模式,则使用
- 非文本模式写入:
- 如果配置为非文本模式,则将最新的日志数据序列化为字符串,并将字符串长度写入文件。
- 序列化日志数据为字符串,并获取字符串长度。
- 使用
fwrite
将字符串长度写入文件。 - 使用
fwrite
将序列化后的日志数据写入文件。
- 写入失败处理:
- 在写入过程中,如果发生错误,则输出错误信息并中止程序执行。
- 如果无法将日志数据序列化为字符串,则输出错误信息并中止程序执行。
- 如果无法将字符串长度或序列化后的日志数据写入文件,则输出错误信息并中止程序执行。
- 写入成功返回:
- 如果写入操作成功完成,则返回
true
表示写入成功。
- 如果写入操作成功完成,则返回
总之,最终Bochspwn会通过该函数将漏洞检测结果保存到对应的文件/二进制中。在我们的测试中,漏洞检测结果被保存到“/bochspwn/modules.bin”“/bochspwn/memlog.bin”中,后续可以根据这些漏洞扫描结果来反向分析是否扫描到了Double-Fetch漏洞。
2、安装与使用
2.1、源码安装
对于此类工具的安装部署,需要满足其要求的环境,否则会出现很多莫名其妙的错误,为了后续的方便使用,记录Bochspwn工具的安装部署环境如下。另外,需要注意的是,Bochspwn是在Ubuntu 22.04.1(Desktop)下进行部署的,不过需要各种软件的配合,才能成功部署,部署成功后,需要将成功编译后的bochs.exe放至Windows 10(64位)系统下进行测试,故本节的全部内容都是关于如何在Ubuntu 22.04.1(Desktop)进行Bochspwn的部署。
软件环境 | 硬件环境 | 约束条件 |
---|---|---|
Ubuntu 22.04.2 LTS(内核版本为5.19.0-45-generic) | 内存16GB | 本文所讲解的Bochspwn源代码于2024.03.21下载 |
具体的软件环境可见“2.1、源码安装章节所示的软件环境” | 硬盘30GB | 本文所安装的Bochspwn源代码于2023.06.27下载 |
使用4个处理器,每个处理器4个内核,共分配16个内核 | 具体的约束条件可见“2.1、源码安装章节所示的软件版本约束” | |
Bochspwn部署在VMware Pro 17上的Ubuntu 22.04.2系统上(主机系统为Windows 11),硬件环境和软件环境也是对应的VMware Pro 17的硬件环境和软件环境 |
2.1.1、部署系统依赖组件
2.1.1.1、部署基础组件
- 首先使用如下命令更新软件源:
$ sudo apt-get update
- Bochspwn的安装部署需要很多软件的支持,除了系统自带的软件环境外,还需要使用如下命令对额外使用的软件进行安装:
$ sudo apt-get install gcc-mingw-w64
$ sudo apt-get install g++-mingw-w64
$ sudo apt-get install vim
$ sudo apt-get install git
$ sudo apt-get install g++
$ sudo apt-get install make
2.1.1.2、部署Protobuf 2.5.0
Protocol Buffers(简称 Protobuf)是一种由Google开发的语言无关、平台无关、可扩展的序列化数据结构的格式,用于进行结构化数据的序列化(Serialization),通常用于通信协议、数据存储等场景。它类似于XML或JSON等数据交换格式,但通常更为紧凑和高效。以下是Protobuf的一些关键特点和优势:
- 简洁高效:Protobuf使用二进制编码,相比XML和JSON,它更加紧凑、高效,适用于传输和存储大量数据。
- 可扩展性:Protobuf支持向现有的数据结构添加新字段,而不会破坏现有的代码,这使得它具有良好的可扩展性。
- 语言无关:Protobuf生成的数据结构和编解码器可以用于多种编程语言,包括Java、C++、Python等,使得不同语言之间的数据交换变得更加简单。
- 结构化数据:Protobuf使用.proto文件定义数据结构,提供了更为明确和结构化的数据描述方式,有助于更清晰地定义数据模型。
- 快速序列化和反序列化:Protobuf提供了高效的序列化和反序列化算法,可以快速地将结构化数据转换为二进制流,并将二进制流转换回结构化数据。
- 自动生成代码:Protobuf提供了代码生成工具,根据.proto文件自动生成相应语言的数据结构定义和序列化/反序列化代码,简化了开发过程。
总的来说,Protobuf是一种高效、灵活、可扩展的数据序列化格式,适用于各种场景下的数据交换和存储需求。以下是部署Protobuf 2.5.0的全部过程。
- 首先使用如下命令更新软件源:
$ sudo apt-get update
- 然后使用如下命令进入系统根目录并下载和解压Protobuf 2.5.0源代码:
$ cd /
$ sudo wget https://github.com/protocolbuffers/protobuf/releases/download/v2.5.0/protobuf-2.5.0.tar.gz
$ sudo tar -zxvf protobuf-2.5.0.tar.gz
- 然后进入到Protobuf 2.5.0源代码目录,执行下面的命令生成配置文件并编译Protobuf 2.5.0,然后将安装头文件和库安装到MinGW中:
$ cd /protobuf-2.5.0/
$ sudo ./configure --host=x86_64-w64-mingw32 --prefix=/usr/x86_64-w64-mingw32/
$ sudo make
$ sudo make install
- 然后再次进入到Protobuf 2.5.0源代码目录,清理原来的编译文件后再次重新生成配置文件并重新编译按照到Linux系统中:
$ cd /protobuf-2.5.0/
$ sudo make clean
$ sudo ./configure
$ sudo make
$ sudo make install
- 然后执行如下命令查看Protobuf 2.5.0是否安装成功:
$ protoc --version
- 出现如下图红框处内容即代表Protobuf 2.5.0安装成功:
注:实际执行中遇到的问题及解决方法
A 问题1:
-
在部署Protobuf的步骤3进行编译(sudo make)的时候,出现如下问题:
-
我们只需要执行如下命令打开Protobuf的Makefile文件:
$ sudo vim src/Makefile
-
然后注释掉3114行和3115行两句代码,需要注意3114行是3113行冒号后面的代码行:
-
保存修改后,回到部署Protobuf的步骤3重新从编译(sudo make)开始继续操作
B 问题2:
-
在部署Protobuf的步骤5查看Protobuf是否安装成功的时候,出现如下问题:
-
出现以上问题是因为Protobuf的默认安装路径为“/usr/local/lib”,而“/usr/local/lib”不在Ubuntu体系默认的LD_LIBRARY_PATH里。为了解决这个问题,首先使用如下命令在如下路径内容创建libprotobuf.conf:
$ sudo vim /etc/ld.so.conf.d/libprotobuf.conf
- 在新建的文件中写入如下内容:
/usr/local/lib
- 保存修改后退出,然后执行如下命令使配置生效:
$ sudo ldconfig
- 完成以上操作后,回到部署Protobuf的步骤5重新继续操作
2.1.1.3、部署Bochs 2.6.9
Bochs是一个开源的x86/x86-64 PC机模拟器,能够在多种操作系统上运行,包括Windows、Linux、macOS等。它允许用户在虚拟环境中模拟运行x86/x86-64架构的计算机系统,包括CPU、内存、磁盘、外部设备等,并且提供了对各种操作系统的支持,如Windows、Linux、FreeBSD等。以下是Bochs的一些主要特点和用途:
- 跨平台性:Bochs可以在多种操作系统上运行,包括Windows、Linux、macOS等,具有良好的跨平台性。
- 完整模拟:Bochs可以模拟整个计算机系统,包括CPU、内存、磁盘、外部设备等,实现了对x86/x86-64架构的完整模拟。
- 调试和测试:Bochs提供了丰富的调试功能,包括单步执行、断点设置、寄存器查看等,适用于软件开发和调试、系统测试等场景。
- 教育和研究:Bochs是一个理想的教学和研究工具,学生和研究人员可以使用它来学习计算机体系结构、操作系统原理等相关知识。
- 虚拟化:Bochs可以用作虚拟化平台,允许用户在虚拟环境中运行多个操作系统实例,从而实现对软件的隔离和测试。
- 开源社区:Bochs是一个开源项目,拥有活跃的开发者和用户社区,提供了持续的更新和改进。
总的来说,Bochs是一个功能丰富、灵活的计算机系统模拟器,适用于各种场景,包括软件开发、调试、教育和研究等。
- 首先使用如下命令更新软件源:
$ sudo apt-get update
- 然后使用如下命令进入系统根目录并下载和解压Bochs 2.6.9源代码:
$ cd /
$ sudo wget https://udomain.dl.sourceforge.net/project/bochs/bochs/2.6.9/bochs-2.6.9.tar.gz
$ sudo tar -zxvf bochs-2.6.9.tar.gz
2.1.2、使用源码安装系统
2.1.2.1、准备工作
- 首先使用如下命令更新软件源:
$ sudo apt-get update
- 然后使用如下命令进入系统根目录并下载Bochspwn源代码:
$ cd /
$ sudo git clone https://github.com/googleprojectzero/bochspwn.git
- 然后使用如下命令将以下文件复制到目标目录中:
$ sudo mkdir bochs-2.6.9/instrument/bochspwn
$ sudo cp bochspwn/instrumentation/* bochs-2.6.9/instrument/bochspwn/
$ sudo cp bochspwn/third_party/instrumentation/* bochs-2.6.9/instrument/bochspwn/
-
然后在Windows10-64bit系统(即待检测目标的所在系统)的“C:\Windows\System32”中找到“dbghelp.dll”,将其放置Ubuntu 22.04.1系统的根目录中:
-
然后将此“dbghelp.dll”复制到如下目录中:
$ sudo cp dbghelp.dll bochs-2.6.9/instrument/bochspwn/
2.1.2.2、编译Bochs 2.6.9
- 首先来到Boch 2.6.9源代码文件目录,并顺序执行如下命令以生成配置文件:
$ cd /bochs-2.6.9/
$ export MINGW=x86_64-w64-mingw32
$ sudo CXXFLAGS="-Wno-narrowing -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.dll" ./configure --host=x86_64-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
- 然后来到之前创建的文件夹中,并使用以下代码转换logging.proto协议以在此目录下生成“logging.pb.cc”和“logging.pb.h”:
$ cd /bochs-2.6.9/instrument/bochspwn/
$ sudo protoc --cpp_out=. logging.proto
- 然后执行如下命令进行编译:
$ sudo make
-
编译成功后会在此目录下生成一个名为“libinstrument.a”的文件:
-
然后再次来到Boch 2.6.9源代码文件目录,使用如下命令对Boch 2.6.9进行编译:
$ cd /bochs-2.6.9/
$ sudo make -j12
- 编译成功后,此目录会生成一个名为“bochs.exe”的可执行文件:
注:实际执行中遇到的问题及解决方法
A 问题1:
-
在编译Bochs 2.6.9的步骤3对Bochs 2.6.9进行编译的时候,出现如下问题:
-
这是因为名称有问题,我们首先使用如下命令打开“symbols.h”:
$ sudo vim symbols.h
-
将第25行的
DbgHelp.h
修改为dbghelp.h
:
-
然后使用如下命令打开“symbols.cc”:
$ sudo vim symbols.cc
-
将第28行的
DbgHelp.h
修改为dbghelp.h
:
-
做完以上操作并保存修改后,从编译Bochs 2.6.9的步骤3重新继续向下操作即可
B 问题2:
-
在编译Bochs 2.6.9的步骤5对Bochs 2.6.9进行编译的时候,出现如下问题:
-
我们只需要执行如下命令打开当前目录中的Makefile文件:
$ sudo gedit Makefile
-
然后将打开的Makefile文件中的
windres
全部替换为x86_64-w64-mingw32-windres
:
-
完成以上操作后,从编译Bochs 2.6.9的步骤5继续重新向下操作即可
C 问题3:
-
在编译Bochs 2.6.9的步骤5对Bochs 2.6.9进行编译的时候,出现如下问题:
-
我们只需要执行如下命令打开当前目录中的Makefile文件:
$ sudo gedit Makefile
-
定位到打开的Makefile文件的第257行(报错的代码行)的最后加上
-lws2_32
:
-
完成以上操作后,从编译Bochs 2.6.9的步骤5继续重新向下操作即可
2.2、使用方法
对于此类工具的测试,需要满足其要求的环境,否则会出现很多莫名其妙的错误,为了后续的方便使用,记录Bochspwn工具的测试环境如下。另外,需要注意的是,所有测试都是在Windows 10-64bit下进行的,即需要在Windows 10-64bit系统中安装VirtualBox 7.0.8,使用其创建虚拟系统Ubuntu 18.04.2(Server),最终使用编译出来的Bochs.exe对Ubuntu 18.04.2(Server)进行漏洞检测。故本节的全部内容都是关于如何在Windows 10-64bit系统中对Bochspwn进行测试。
此外,在本章节进行漏洞检测的目标为Linux 4.15.0-45-generic内核,整体使用的方法是通用的,关于测试细节,或者说对其它版本的内核进行测试,可以参考“3、测试用例”章节中的相关内容。
软件环境 | 硬件环境 | 约束条件 |
---|---|---|
Windows 10-64bit | 使用4个处理器,每个处理器4个内核,共分配16个内核 | 本文所讲解的Bochspwn源代码于2024.03.21下载 |
具体的软件环境可见“2.2、使用方法”章节所示的软件环境 | 内存16GB | 本文所安装的Bochspwn源代码于2023.06.27下载 |
硬盘60GB | 具体的约束条件可见“2.2、使用方法”章节所示的软件版本约束 | |
Bochspwn在VMware Pro 17上的Windows 10-64bit系统上(主机系统为Windows 11)进行测试,硬件环境和软件环境也是对应的VMware Pro 17的硬件环境和软件环境 |
2.2.1、部署测试依赖组件
2.2.1.1、部署VirtualBox 7.0.8
-
首先来到如下位置下载VirtualBox 7.0.8的安装包:
-
下载之后打开安装包:
-
选择安装位置后继续安装:
-
点击“是(Y)”:
-
点击“是(Y)”:
-
开始安装:
-
等待安装中:
-
安装完成后点击“完成(F)”,此时就完成了VirtualBox 7.0.8的部署:
2.2.1.2、部署Bochs 2.6.9
-
首先来到Bochs 2.6.9官网下载Windows 10-64bit的安装包:
-
双击打开下载好的安装包后开始安装:
-
点击“Next >”:
-
选择完全安装后点击“Next >”:
-
选择安装位置后点击“Install”:
-
安装完成后点击“Close”:
-
此时就完成了在Windows 10-64bit上部署Bochs 2.6.9
2.2.2、使用源码测试系统
2.2.2.1、准备工作
2.2.2.1.1、准备待检测目标
-
因为我们本次测试漏洞检测的目标为Linux 4.15.0-45-generic内核,故首先来到Ubuntu镜像网站下载Ubuntu 18.04.2(Server)的ISO文件,将其下载到Windows 10-64bit系统的某个位置,要记录一下,后面会用到:
-
然后打开VirtualBox 7.0.8,点击“新建(N)”:
-
进行相应配置后(此处的“虚拟光盘(I)”就需要选择刚才下载好的Ubuntu 18.04.2系统的ISO文件),点击“下一步(N)”。需要注意安装虚拟系统的目录不要有空格和特殊符号:
-
进行相应配置后,点击“下一步(N)”:
-
进行相应配置后,点击“下一步(N)”:
-
进行相应配置后,点击“下一步(N)”:
-
点击“完成(F)”:
-
启动系统中:
-
系统启动后会自动进行安装,只需要稍等片刻即可:
-
系统安装完后会自动重启,重启后会自动进入系统,我们只需要输入之前设置的用户名和密码即可进入新安装好的文件系统中。此时我们就完成了Ubuntu 18.04.2(Server)在虚拟机中的部署:
注:实际执行中遇到的问题及解决方法
A 问题1:
-
在部署Ubuntu 18.04.2(Server)的步骤8启动系统的时候,出现如下问题:
-
这是因为我们没有开启虚拟化技术,我们首先关闭Windows 10-64bit虚拟机:
-
然后进行如下选择:
-
然后重新开启Windows 10-64bit虚拟机:
-
重新开机之后,再次启动刚刚创建的Ubuntu 18.04.2(Server)虚拟机系统:
-
当完成以上步骤后,问题就已经解决了,可以回到部署Ubuntu 18.04.2(Server)的步骤9继续向下操作。另外,如果Windows 10-64bit系统不是部署在虚拟机中,也可以进入Bios中开启虚拟化技术,因为目前我们将系统部署在了虚拟机中,所以此种方法不再赘述
B 问题2:
-
在部署Ubuntu 18.04.2(Server)的步骤9安装系统的时候,出现如下问题:
-
出现这种情况是因为无法给Ubuntu的Server安装VirtualBox的虚拟化工具,而服务器版的Ubuntu并不需要此工具,所以也没必要安装。我们只需要选择“Continue”后按一下回车即可,然后回到部署Ubuntu 18.04.2(Server)的步骤9继续向下操作
2.2.2.1.2、部署内核调试符号包
-
首先启动我们部署好的Ubuntu 18.04.2(Server)系统:
-
启动系统后,首先使用
su
命令进入root用户权限:
-
然后使用如下命令查看当前系统中的Linux内核版本:
# uname -r
-
可以看到,本次测试使用的Linux内核版本为4.15.0-45-generic:
-
下面开始部署内核调试符号包,首先使用如下命令添加仓库配置:
# echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse
deb http://ddebs.ubuntu.com $(lsb_release -cs)-updates main restricted universe multiverse
deb http://ddebs.ubuntu.com $(lsb_release -cs)-proposed main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/ddebs.list
- 然后导入密钥:
# apt install ubuntu-dbgsym-keyring
- 然后更新安装包列表:
# apt-get update
- 然后使用如下命令安装内核调试符号包(由于Linux内核调试符号包官网不提供4.15.0-45-generic对应的内核调试符号包,所以我采用了与之版本最接近的内核调试符号包,如果有对应版本的内核调试符号包,一定要选择对应的内核调试符号包):
# wget http://ddebs.ubuntu.com/pool/main/l/linux/linux-image-unsigned-4.15.0-44-generic-dbgsym_4.15.0-44.47_amd64.ddeb
- 下载完成后,执行如下命令进行安装:
# dpkg -i linux-image-unsigned-4.15.0-44-generic-dbgsym_4.15.0-44.47_amd64.ddeb
- 安装完成后,可以使用如下命令查看内核调试符号包:
# ls /usr/lib/debug/boot/
- 若出现如下内容(不同版本的内核调试符号包的版本名称不同,不过长得都差不多),即代表内核调试符号包安装成功:
2.2.2.1.3、加载内核符号信息
-
首先使用
su
命令进入root用户权限:
-
因为我们要加载内核符号信息,所以首先需要将安装好的内核调试符号包与Linux内核源码相关联,所以执行如下命令,查看我们可以安装的Linux内核:
# apt-cache search linux-source
-
因为我们的测试系统的内核版本为4.15.0-45-generic,所以我们就选择Linux 4.15.0版本的源代码进行下载安装(如下图红框所示):
-
我们已经确定了要下载安装的Linux内核版本,所以直接执行如下命令下载安装即可:
# apt install linux-source-4.15.0
- 安装完后,使用如下命令对其解压,并进入其目录中:
# cd /usr/src
# tar -jxvf linux-source-4.15.0.tar.bz2
# cd linux-source-4.15.0
- 然后使用如下命令对其进行软链接:
# mkdir -p /build/linux-image/
# ln -s /usr/src/linux-source-4.15.0 /build/linux-image/linux-4.15.0
- 然后执行如下命令来下载安装GDB:
# apt install gdb
- 然后执行如下命令进入Linux 4.15.0-45-generic源码的GDB调试界面:
# gdb /usr/lib/debug/boot/vmlinux-4.15.0-44-generic
- 进入调试界面后,逐一运行下面的命令:
(gdb) print &((struct task_struct*)0)->pid
(gdb) print &((struct task_struct*)0)->tgid
(gdb) print &((struct task_struct*)0)->comm
(gdb) print &modules
(gdb) print &((struct module*)0)->list
(gdb) print &((struct module*)0)->name
(gdb) print &((struct module*)0)->core_layout->base
(gdb) print &((struct module*)0)->core_layout->size
-
运行以上命令后得到如下图红框所示信息,记录这些信息,后面会用到。得到下面信息后,使用
quit
命令退出调试环境:
-
使用如下命令查看系统版本:
# uname -a
-
使用如下命令查看系统版本,记录版本号和位数,后面会用到:
-
然后打开我们刚刚上传到Windows 10-64bit的Bochspwn源代码目录中的config.txt,在其中添加如下内容,填写的内容就是我们刚才获取到的信息,主要注意的是刚才获取到的
core_layout->base
和core_layout->size
分别对应module_core
和module_core_size
,其余获取到的数据的就按照名字一一对应即可,其余内容不需要修改,但是如果使用其它版本的内核进行测试时,可能需要进行修改,获取到相应信息进行修改即可:
[ubuntu_server_64_4.15.0-45-generic]
thread_size = 0x2000
thread_info_task = 0
task_struct_pid = 0x8a8
task_struct_tgid = 0x8ac
task_struct_comm = 0xa50
task_comm_len = 16
modules = 0xffffffff824e9970
module_list = 0x8
module_name = 0x18
module_core = 0x180
module_core_size = 0x188
module_name_len = 56
kernel_start = 0xffffffff81000000
kernel_end = 0xffffffff828e2000
- 填写完以上信息后,保存退出。然后在Windows 10-64bit系统中打开命令提示符窗口,使用VirtualBox自带的工具将硬盘格式由VDI格式转换成RAW格式:
"C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" internalcommands converttoraw C:\VM\Ubuntu18.04.2-Server-BochspwnTest\Ubuntu18.04.2-Server-BochspwnTest.vdi C:\VM\Ubuntu18.04.2-Server-BochspwnTest\Ubuntu18.04.2-Server-BochspwnTest.raw
- 转换成功后会在设置的目录中生成一个RAW格式的硬盘:
2.2.2.1.4、设置配置文件
-
首先将Bochspwn源代码文件复制到Windows 10-64bit的C盘中:
-
然后在Bochs-2.6.9的安装目录中,找到名为“bochsrc-sample.txt”的文件,将其复制到Bochspwn源代码目录中,并重命名为“bochsrc.txt”:
-
然后打开刚刚得到的“bochsrc.txt”,在其中的第718行的
path
属性值修改为我们刚刚得到的RAW格式的硬盘路径。修改完成后保存修改并退出:
2.2.2.2、开始测试
-
首先将之前在Ubuntu部署环境中交叉编译得到的Bochs.exe文件上传到Bochspwn源代码目录:
-
然后使用管理员权限打开命令提示符窗口,接着使用
cd C:\bochspwn-master
命令进入到Bochspwn源代码目录:
-
然后再打开的命令提示符窗口中执行如下三条命令启动虚拟机系统并加载Bochspwn。需要注意的是要将以下各个路径设置为自己配置的路径:
set BXSHARE=C:\Program Files (x86)\Bochs-2.6.9
set BOCHSPWN_CONF=C:\bochspwn-master\config.txt
bochs.exe -f C:\bochspwn-master\bochsrc.txt
-
点击红框处启动虚拟机系统:
-
出现如下界面即代表虚拟机系统启动成功,并且Bochspwn也已经加载成功。然后此时我们就可以进行各种针对内存的操作,以通过Bochspwn检测可能存在的Double-Fetch漏洞:
-
Bochspwn会将检测到的Double-Fetch漏洞记录在Bochspwn源代码目录中的“memlog.bin”和“modules.bin”的文件,可以通过对这两个文件的后续分析来查看Double-Fetch漏洞检测结果:
注:实际执行中遇到的问题及解决方法
A 问题1:
-
在6.3.2 开始测试的步骤3进行漏洞检测的时候,出现如下问题:
-
我们只需要将Bochspwn源代码目录中的bochsrc.txt中的第914行注释,然后保存修改后退出即可:
-
完成以上操作后,重新回到6.3.2 开始测试的步骤3重新继续向下操作
3、测试用例
3.1、对Linux 4.15.0-44-generic内核进行漏洞检测
本章节将会使用Windows 10-64bit操作系统作为基准平台,对Linux 4.15.0-45-generic内核进行漏洞检测。关于一些准备工作以及操作细节,请参考“2、安装与使用”章节中的对应内容。因为我们默认已经编译好Bochspwn,并且将其配置好了才能进行下面的操作。
3.1.1、准备工作
3.1.1.1、准备待检测目标
-
因为我们本次测试漏洞检测的目标为Linux 4.15.0-45-generic内核,故首先来到Ubuntu镜像网站下载Ubuntu 18.04.2(Server)的ISO文件,将其下载到Windows 10-64bit系统的某个位置,要记录一下,后面会用到:
-
然后打开VirtualBox 7.0.8,点击“新建(N)”(注:需要提前开启主机系统的虚拟化技术):
-
进行相应配置后(此处的“虚拟光盘(I)”就需要选择刚才下载好的Ubuntu 18.04.2系统的ISO文件),点击“下一步(N)”。需要注意安装虚拟系统的目录不要有空格和特殊符号:
-
进行相应配置后,点击“下一步(N)”:
-
进行相应配置后,点击“下一步(N)”:
-
进行相应配置后,点击“下一步(N)”:
-
点击“完成(F)”:
-
启动系统中:
-
系统启动后会自动进行安装,只需要稍等片刻即可:
-
然后会出现如下图所示的内容,只需要选择“Continue”后按一下“Enter”即可:
-
系统安装完后会自动重启,重启后会自动进入系统,我们只需要输入之前设置的用户名和密码即可进入新安装好的文件系统中。此时我们就完成了Ubuntu 18.04.2(Server)在虚拟机中的部署:
3.1.1.2、部署内核调试符号包
-
首先部署并启动Ubuntu 18.04.2(Server)系统:
-
启动系统后,首先使用
su
命令进入root用户权限:
-
然后使用如下命令查看当前系统中的Linux内核版本:
# uname -r
-
可以看到,本次测试使用的Linux内核版本为4.15.0-45-generic:
-
下面开始部署内核调试符号包,首先使用如下命令添加仓库配置:
# echo "deb http://ddebs.ubuntu.com $(lsb_release -cs) main restricted universe multiverse
deb http://ddebs.ubuntu.com $(lsb_release -cs)-updates main restricted universe multiverse
deb http://ddebs.ubuntu.com $(lsb_release -cs)-proposed main restricted universe multiverse" | sudo tee -a /etc/apt/sources.list.d/ddebs.list
- 然后导入密钥:
# apt install ubuntu-dbgsym-keyring
- 然后更新安装包列表:
# apt-get update
- 然后使用如下命令安装内核调试符号包(由于Linux内核调试符号包官网不提供4.15.0-45-generic对应的内核调试符号包,所以我采用了与之版本最接近的内核调试符号包,如果有对应版本的内核调试符号包,一定要选择对应的内核调试符号包):
# wget http://ddebs.ubuntu.com/pool/main/l/linux/linux-image-unsigned-4.15.0-44-generic-dbgsym_4.15.0-44.47_amd64.ddeb
- 下载完成后,执行如下命令进行安装:
# dpkg -i linux-image-unsigned-4.15.0-44-generic-dbgsym_4.15.0-44.47_amd64.ddeb
- 安装完成后,可以使用如下命令查看内核调试符号包:
# ls /usr/lib/debug/boot/
- 若出现如下内容(不同版本的内核调试符号包的版本名称不同,不过长得都差不多),即代表内核调试符号包安装成功:
3.1.1.3、加载内核符号信息
-
首先使用
su
命令进入root用户权限:
-
因为我们要加载内核符号信息,所以首先需要将安装好的内核调试符号包与Linux内核源码相关联,所以执行如下命令,查看我们可以安装的Linux内核:
# apt-cache search linux-source
-
因为我们的测试系统的内核版本为4.15.0-45-generic,所以我们就选择Linux 4.15.0版本的源代码进行下载安装(如下图红框处所示):
-
我们已经确定了要下载安装的Linux内核版本,所以直接执行如下命令下载安装即可:
# apt install linux-source-4.15.0
- 安装完后,使用如下命令对其解压,并进入其目录中:
# cd /usr/src
# tar -jxvf linux-source-4.15.0.tar.bz2
# cd linux-source-4.15.0
- 然后使用如下命令对其进行软链接:
# mkdir -p /build/linux-image/
# ln -s /usr/src/linux-source-4.15.0 /build/linux-image/linux-4.15.0
- 然后执行如下命令来下载安装GDB:
# apt install gdb
- 然后执行如下命令进入Linux 4.15.0-45-generic源码的GDB调试界面:
# gdb /usr/lib/debug/boot/vmlinux-4.15.0-44-generic
- 进入调试界面后,逐一运行下面的命令:
(gdb) print &((struct task_struct*)0)->pid
(gdb) print &((struct task_struct*)0)->tgid
(gdb) print &((struct task_struct*)0)->comm
(gdb) print &modules
(gdb) print &((struct module*)0)->list
(gdb) print &((struct module*)0)->name
(gdb) print &((struct module*)0)->core_layout->base
(gdb) print &((struct module*)0)->core_layout->size
-
运行以上命令后得到如下图红框所示信息,记录这些信息,后面会用到。得到下面信息后,使用
quit
命令退出调试环境:
-
使用如下命令查看系统版本:
# uname -a
-
使用如下命令查看系统版本,记录版本号和位数,后面会用到:
-
然后打开我们刚刚上传到Windows 10-64bit的Bochspwn源代码目录中的config.txt,在其中添加如下内容,填写的内容就是我们刚才获取到的信息,主要注意的是刚才获取到的
core_layout->base
和core_layout->size
分别对应module_core
和module_core_size
,其余获取到的数据的就按照名字一一对应即可,其余内容不需要修改:
[ubuntu_server_64_4.15.0-45-generic]
thread_size = 0x2000
thread_info_task = 0
task_struct_pid = 0x8a8
task_struct_tgid = 0x8ac
task_struct_comm = 0xa50
task_comm_len = 16
modules = 0xffffffff824e9970
module_list = 0x8
module_name = 0x18
module_core = 0x180
module_core_size = 0x188
module_name_len = 56
kernel_start = 0xffffffff81000000
kernel_end = 0xffffffff828e2000
- 填写完以上信息后,保存退出。然后在Windows 10-64bit系统中打开命令提示符窗口,使用VirtualBox自带的工具将硬盘格式由VDI格式转换成RAW格式:
"C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" internalcommands converttoraw C:\VM\Ubuntu18.04.2-Server-BochspwnTest\Ubuntu18.04.2-Server-BochspwnTest.vdi C:\VM\Ubuntu18.04.2-Server-BochspwnTest\Ubuntu18.04.2-Server-BochspwnTest.raw
- 转换成功后会在设置的目录中生成一个RAW格式的硬盘:
3.1.1.4、设置配置文件
-
首先将Bochspwn源代码文件复制到Windows 10-64bit的C盘中:
-
然后在Bochs-2.6.9的安装目录中,找到名为“bochsrc-sample.txt”的文件,将其复制到Bochspwn源代码目录中,并重命名为“bochsrc.txt”:
-
然后打开刚刚得到的“bochsrc.txt”,在其中的第718行的
path
属性值修改为我们刚刚得到的RAW格式的硬盘路径。修改完成后保存修改并退出:
3.1.2、开始测试
-
首先将之前在Ubuntu部署环境中交叉编译得到的Bochs.exe文件上传到Bochspwn源代码目录:
-
然后使用管理员权限打开命令提示符窗口,接着使用
cd C:\bochspwn-master
命令进入到Bochspwn源代码目录:
-
然后将Bochspwn源代码目录中的bochsrc.txt中的第914行注释,然后保存修改后退出即可:
-
然后在打开的命令提示符窗口中执行如下三条命令启动虚拟机系统并加载Bochspwn。需要注意的是要将以下各个路径设置为自己配置的路径:
set BXSHARE=C:\Program Files (x86)\Bochs-2.6.9
set BOCHSPWN_CONF=C:\bochspwn-master\config.txt
bochs.exe -f C:\bochspwn-master\bochsrc.txt
-
点击红框处启动虚拟机系统:
-
出现如下界面即代表虚拟机系统启动成功,并且Bochspwn也已经加载成功。然后此时我们就可以进行各种针对内存的操作,以通过Bochspwn检测可能存在的Double-Fetch漏洞:
-
Bochspwn会将检测到的Double-Fetch漏洞记录在Bochspwn源代码目录中的“memlog.bin”和“modules.bin”的文件,可以通过对这两个文件的后续分析来查看Double-Fetch漏洞检测结果:
4、总结
4.1、部署架构
关于Bochspwn部署的架构图,如下图所示。
对于以上架构图,我们具体来看Bochspwn是否对其中的组件进行了修改。详情可参见下方的表格。
是否有修改 | 具体修改内容 | 备注 | |
---|---|---|---|
主机内核 | 无 | 无 | 无 |
主机操作系统 | 无 | 无 | 无 |
Guest内核 | 有 | 使用GDB获取Guest内核的内核符号信息 | 目的是获取Guest内核中的内存布局信息 |
Guest操作系统 | 无 | 无 | 无 |
虚拟机监视器Bochs | 有 | 链接“libinstrument.a”静态库到“bochs.exe” | 目的是捕获Guest内核所有对内存的操作 |
4.2、漏洞检测对象
- 检测的对象为Guest内核
- 针对的内核版本为Linux 4.15.0-44-generic
- 检测的漏洞类型为Double-Fetch错误
4.3、漏洞检测方法
- 使用编译到Bochs中的静态库捕获Guest内核中所有对内存相邻两次的访问
- 将捕获到的潜在的Double-Fetch错误记录到主机中
4.4、种子生成/变异技术
由于不涉及种子,故没有用到任何种子生成/变异技术。
5、参考文献
- 内核漏洞挖掘技术系列之 bochspwn
- Ubuntu 21.10 安装调试符号
- 内核漏洞挖掘技术系列(2)——bochspwn
- Bochspwn漏洞挖掘技术深究(1):Double Fetches 检测
- googleprojectzero/bochspwn
- bochs-emu/Bochs
- Bochs源码分析 - 5: 从启动到cpu运行之前的一系列初始化
- Ubuntu镜像网站
- 内核调试符号包
- Bochs项目源码分析与注释
- syscan.pdf
总结
以上就是本篇博文的全部内容,可以发现,Bochspwn是一款针对Linux内核中的Double-Fetch漏洞进行检测的工具,并且其原理也并不复杂。相信读完本篇博客,各位读者一定对Bochspwn有了更深的了解。