2. 中国科学技术大学, 合肥 230026
2. University of Science and Technology of China, Hefei 230026, China
系统调用被广泛应用于进程行为分析和恶意软件入侵检测[1]等方面. 系统调用信息的捕获和分析是开展这些工作的关键. Ether[2]是一个基于硬件虚拟化的客户机自省工具, 其用来进行恶意软件分析. Ether利用x86快速系统调用条目机制的特性, 在系统调用执行期间, 其系统调用拦截机制使用x86处理器上的特殊寄存器, 在一个已有地址上产生页错误. Ether通过接收该地址上产生页错误的信号判断一个系统调用的产生. Nitro[3]同样也是一个使用硬件虚拟化扩展的用于客户机自省的开源项目, 它在KVM[4](Kernel Virtual Machine, 内核虚拟机)平台上实现. KVM由两个部分组成, 一个是基于QEMU[5]的用户空间应用程序, 一个是提供实际虚拟化功能的Linux内核模块. Nitro将这两部分进行了扩展, 实现了系统调用的跟踪和监控. 目前捕获系统调用的同类方法由于不能预知每个客户机操作系统的类型及其运行的应用, 无法制定统一的系统调用信息捕获和解析规则, 故仅能获取系统调用原始信息, 无法解析参数和返回值等重要信息. 此外, 现有方法没有考虑到捕获系统调用的性能开销问题, 没有对系统调用以进程为划分界限进行分类. 由于不同系统调用的参数类型不同, 参数解析面临很大的困难. 针对以上问题, 本文在KVM虚拟化平台上建立了一个实时捕获和分析客户机内系统调用的系统. 该系统根据不同系统调用建立相应的分析规则, 实现了对系统调用序列的捕获以及对参数和返回值的解析. 通过exit()系统调用判断出进程的终止操作, 并将进程从监视目标中移除, 从而区分系统调用序列所属的不同进程.
1 系统设计 1.1 设计目标系统调用监视系统旨在利用虚拟化技术[6]在客户机外部实现对其内部系统调用序列进行实时捕获, 并对参数和返回值进行解析. 同时, 不对宿主机和客户机带来太大的性能开销. 因此设计目标包括以下三个方面:
(1) 实现对系统调用参数和返回值的快速捕获. 基于此点要求, 需要在处理系统调用引发异常时, 对系统调用进入和退出指令进行模拟; 然后在模拟过程中获取客户机VCPU信息, 从中提取参数和返回值的原始信息(即二进制信息);
(2) 将低层次的系统调用信息还原成高层次语义[7]. 即在捕获到系统调用的二进制信息后, 使用内核自省工具libvmi[8]将其解析为高层次语义信息;
(3) 引入较小的性能开销. 在确保快速捕获系统调用参数和返回值的前提下, 使用线程池技术快速获取并解析多个VCPU的二进制信息, 尽量减小对宿主机和客户机带来的性能开销.
1.2 系统总体架构系统总体架构如图1所示, 主要由KVM内核功能扩展模块、监听模块、信息处理模块和参数解析模块四部分组成. 系统运行过程中, 监听模块将修改硬件规范使系统调用陷入的指令发送给KVM内核功能扩展模块, 然后KVM内核功能扩展模块将捕获到的系统调用低层次语义信息发送给监听模块. 信息处理模块将低层次语义信息转化为系统调用名和系统调用号这些高层次语义信息. 参数解析模块完成系统调用参数的解析.
实现该系统需要解决模拟指令的重写、语义的转换和进程的区分三个关键问题. 在由快速系统调用指令引起的异常发生时, VMM[9](Virtual Machine Monitor, 虚拟机监控器)捕获该异常进行处理后对指令进行模拟. 模拟指令需要解决的首要问题就是对指令进行重写, 重写后的指令一方面要维持原有指令的功能, 另一方面要满足捕获系统调用的需求, 即得到VCPU寄存器信息. 解决模拟指令重写的方法是在KVM模拟指令的函数(如em_syscall())中加入kvm_arch_vcpu_ioctl_get_regs()函数和kvm_arch_vcpu_ioctl_get_sregs()函数来获取通用寄存器信息和特殊寄存器信息, 这些信息对应了系统调用的参数和返回值. 语义的转换包含两方面, 一个是解析系统调用名, 方法是获取其在内核符号表中的虚拟地址, 然后使用libvmi的translate_v2ksym()进行解析, 另一个是解析系统调用参数, 针对字符串类型的参数, 需使用libvmi的read_str_va()进行解析. 捕获系统调用时需要区分已终止的进程和正在运行的进程, 其目的在于防止重复捕获同名同号新旧进程的系统调用造成新进程系统调用序列不再准确, 提高捕获效率, 解决进程区分问题的方法是识别进程是否调用了exit()或exit_group()函数.
2 系统实现 2.1 系统调用的捕获当前基于x86架构的操作系统大多使用基于syscall指令和基于sysenter指令的快速系统调用. 针对这两种类型的系统调用需要采用不同的捕获方法.
客户机正常执行时syscall指令不会陷入到VMM中, 因此无法实现对基于syscall指令系统调用的捕获. 根据Intel软件开发手册上的硬件规范可知, 在将EFER寄存器的SCE标志位置0时执行syscall指令, 系统会发生UD未定义的无效操作码异常陷入, 这样就成功实现了对基于syscall指令系统调用的捕获.
基于sysenter指令的系统调用同样依赖于一组MSR特殊寄存器, 分别是SYSENTER_CS_MSR, SYSENTER_ESP_MSR和SYSENTER_EIP_MSR. 为了使客户机系统在执行sysenter指令时能陷入到VMM中, 需要修改相关的硬件规范. 基于sysenter指令的系统调用入口地址保存在上述MSR寄存器中, 因此可以先将MSR寄存器原始值先保存起来, 然后装入一组NULL非法值, 系统在执行sysenter指令时由于找不到系统调用入口地址会产生GP通用保护错异常陷入, 这样就成功实现了对基于sysenter指令系统调用的捕获.
在客户机运行时, 用户模式下的监听模块首先将设置系统调用捕获的指令通过/dev/kvm设备文件的iotcl调用发送给KVM内核模块, 当客户机内的程序执行系统调用时发生异常陷入到VMM, KVM内核模块捕获到系统调用并将信息返回给监听模块, 监听模块收到事件信息后停止监听并关闭/dev/kvm设备文件的ioctl调用.
2.2 模拟指令的重写对于VM中发生的异常, 必须要识别出该异常是由于客户机正常执行过程中引起的还是由于改变了系统调用指令正常执行条件引起的. 为此本文在内核处理异常的代码中加入了对客户机异常的判断, 其原理是判断发生异常时VCPU的状态, 即: 使用is_invalid_opcode()识别无效操作码异常, 若异常在客户模式下产生, 则是正常情况下产生的异常, 就使用kvm_queue_exception()将异常注入到Guest OS中处理; 否则, 使用emulate_instruction()对引起异常的系统调用指令进行模拟, 最后返回Guest OS. 使用is_general_protection()识别通用保护异常, 若异常发生时VCPU处于sysenter_sysexit状态, 则模拟系统调用指令, 最后返回Guest OS; 否则是正常情况下产生的异常, 将异常注入到Guest OS中处理.
异常识别和处理的方法如算法1所示. 第1–6行用于识别和处理无效操作码异常. 第7–12行用于识别和处理通用保护异常.
算法1. 异常识别和处理算法
输入: 异常信息intr_info, 客户机虚拟CPU信息vcpu
输出: 顺利处理完异常, 返回1
1. IF(is_invalid_opcode(intr_info))
2. IF(is_guest_mode(vcpu))
3. kvm_queue_exception(vcpu);
4. RETURN 1;
5. ELSE emulate_instruction(vcpu);
6. RETURN 1;
7. ELSE IF(is_general_protection(intr_info))
8. IF(is_sysenter_sysexit(vcpu))
9. emulate_instruction(vcpu);
10. RETURN 1;
11. ELSE kvm_queue_exception(vcpu);
12. RETURN 1;
模拟指令是处理异常中的关键环节, 对该指令的重写是获取系统调用信息的关键因素. 客户机在执行完若干条传递系统调用参数的传送指令后执行sysenter快速系统调用指令从用户态快速进入内核态, 进入内核态后执行wrmsr特权指令实现MSRs寄存器的初始化工作. 由于修改了硬件规范, 因此在执行wrmsr特权指令时客户机会产生异常陷入到VMM中, 为了恢复客户机系统调用的正常执行, VMM需要对系统调用指令进行模拟. 对模拟指令进行重写有两个目的, 一个是重写模拟sysenter进入指令获取系统调用的参数, 另一个是重写模拟sysexit退出指令获取系统调用的返回值, 重写的方法是在内核代码arch/x86/kvm/emulate.c下的em_sysenter()和em_sysexit()函数中添加kvm_arch_vcpu_ioctl_get_regs()函数和kvm_arch_vcpu_ioctl_get_sregs()函数.
2.3 语义的转换语义的转换是将内核扩展模块捕获到的系统调用低层次语义信息转换为高层次语义信息, 信息处理模块完成对系统调用名和系统调用号的解析, 参数解析模块完成对系统调用参数的解析. 实现语义的转换必须要不断向KVM内核模块发送获取VCPU事件信息的请求, 在客户机中有多个VCPU的情况下, 如果只创建一个线程来监听多个VCPU的事件信息, 那么每捕获到一个VCPU上的系统调用就需要暂停一次客户机, 客户机暂停时间相应地增长了; 如果针对每个VCPU创建一个监听线程, 那么客户机暂停时间将缩短为多个监听线程中耗时最长的一个, 相比于单监听线程客户机性能得到了提升. 本文在监听模块中使用了线程池技术, 其原理是引入concurrent.futures模块, 该模块可实现并行计算, 即使用ThreadPoolExecutor类把监听多个VCPU事件信息的工作分配给多个Python线程处理, 进而实现系统调用二进制信息的快速获取和解析, 降低了客户机暂停时间, 提升了客户机的性能.
2.3.1 系统调用名和系统调用号解析在内核中维护着一张符号表System.map被称作内核符号表, 该表记录了内核中所有符号(函数, 全局变量等)的地址及名称, 这其中就包括系统调用表和系统调用名的地址和名称. 系统调用表sys_call_table中各表项是指向实现各种系统调用的内核函数的函数指针, 系统调用执行时, 系统调用处理程序会读取rax寄存器来获取系统调用号, 将其乘以4(32位下为4, 64位下为8)生成偏移地址, 然后以sys_call_table为基址, 基址加上偏移地址可得系统调用表项地址, 使用libvmi对表项地址进行解析可得系统调用服务例程地址, 最后使用libvmi解析服务例程地址可得系统调用名.
解析系统调用名的方法如算法2所示. 第1行定义了64位下系统调用表项大小. 第2行根据系统调用号rax计算系统调用表项的地址. 第3行使用libvmi的read_addr_va()解析表项地址得到系统调用服务例程地址. 第4行使用libvmi的translate_v2ksm()解析系统调用服务例程地址得到系统调用名. 第5行返回系统调用名.
算法2. 系统调用名解析算法
输入: 系统调用表地址sys_call_table_addr, 系统调用号rax
输出: 系统调用名sys_call_name
1. #define VOID_P_SIEZ 8
2. p_addr = sys_call_table+rax*VOID_P_SIZE
3. addr = libvmi.read_addr_va(paddr)
4. sys_call_name = libvmi.translate_v2ksym(addr)
5. RETURN sys_call_name
2.3.2 系统调用参数解析
基于syscall指令的系统调用其参数存放在rdi、rsi、rdx、r10、r8和r9寄存器中, 而基于sysenter指令的系统调用其参数存放在rbx、rcx、rdx、rsi、rdi、rbp中. 不同的系统调用参数个数和类型也不同, 为了解决系统调用参数解析困难的问题, 设计了参数解析模块. 该模块根据不同类型的系统调用设置了相应的系统调用参数处理规则. 例如, 针对文件进行操作的open和access系统调用, 其第一个参数是所要操作文件的文件名, 对该参数的处理规则就是将存放参数的寄存器中的地址信息转换为文件名对应的字符串, 转换方法为使用libvmi的read_str_va()函数.
2.4 进程区分现有的系统调用捕获方法在运行系统调用监控程序时, 没有考虑到进程终结的问题, 即不能判断已经退出的进程, 并将属于该进程的系统调用序列从整个系统调用的序列中剔除. 这样就会存在两个弊端: 第一, 存在与已退出旧进程同名同号的新进程, 由于监控程序无法判断旧进程的退出, 故会重新扫描旧进程的系统调用序列并加入新进程的系统调用序列, 造成新进程系统调用序列不再准确. 第二, 对于监控程序捕获到的多个进程的系统调用序列, 在上述情况下, 每个进程的系统调用序列都不再准确, 监控程序所捕获到的以一个进程系统调用序列为单位的总系统调用序列相应地也不再准确. 以上存在的弊端会严重降低系统调用序列捕获的准确率, 对宿主机的性能也会产生影响.
针对以上问题, 我们需要在捕获系统调用时及时判断进程是否退出, 如果进程退出, 就将属于该进程的系统调用序列从总的系统调用序列中移除. 在操作系统中, 进程退出一般会显式或隐式地调用exit()或exit_group()函数, 两者的区别在于前者只退出该进程, 而后者是退出属于该进程组的所有进程, 两者最后都会调用内核中的do_exit()函数(位于kernel/exit.c函数中). 在捕获系统调用时, 判断进程是否会调用此函数, 如果调用了, 说明进程已经退出, 将其从总的系统调用序列中移除并写到文件中.
3 实验与分析 3.1 实验环境本实验是在Linux操作系统下进行的, 以KVM作为虚拟化平台, 设计并实现了系统调用监控系统, Qemu版本为2.8; 宿主机操作系统版本为ubuntu-16.04-desktop-amd64,内核版本为4.9, CPU采用24核Intel(R) Xeon(R) CPU E5-2620 v2 @ 2.10 GHz; 客户机操作系统版本为ubuntu-12.04-server-amd64, 内核版本为3.2.0-23-generic, VCPU为单核, 内存为1 GB.
3.2 实验结果与分析 3.2.1 系统调用捕获分析从宿主机中捕获的客户机系统调用序列如图2所示(左侧是客户机外部视图, 右侧是客户机内部视图), 通过交叉视图对比发现, 客户机内外系统调用序列完全一致, 说明在客户机外部可以捕获到客户机内完整的系统调用序列.
由于解析的系统调用序列较长, 所以此处仅选择部分解析的系统调用序列进行分析, 客户机内外系统调用参数和返回值对比如图3所示.
Open系统调用第一个参数为打开文件的路径名. 由内外视图对比可以发现, open分别打开了/etc/ld.so.cache和/lib/x86_64-linux-gnu/libc.so.6文件, 这两个文件分别表示Linux动态共享库缓存文件和Glibc动态库文件. O_RDONLY表示以只读方式打开文件, O_CLOEXEC表示在进程执行exec系统调用时关闭此打开的文件描述符, 两者进行或操作后值为0x80000. 客户机内外部得到的返回值都为3. 对比发现, 客户机外部捕获到的open系统调用参数和返回值是正确的.
Access系统调用是检查调用进程是否可以对指定的文件执行某种操作. 由图可见, 客户机进程调用了access来确定/etc/ld.so.nohwcap文件是否存在, F_OK在头文件unistd.h中的预定义为0. 对比发现, 客户机外部捕获到的access系统调用参数和返回值是正确的.
Mmap系统调用是将一个文件或者其它对象映射进内存. 其第3个参数PROT_READ和第4个参数MAP_PRIVATE在sys/types.h中分别定义为1和2, 其余参数也一致. 但是得到的返回值有差异, 这是由于strace改变了原来系统调用的走向, 使得返回值发生了改变. 为了验证客户机外部得到的mmap返回值是否正确, 本文选择在测试程序运行过程中输出mmap的返回值, 如图4所示. 对比发现, 客户机外部捕获到的mmap、open和write系统调用参数和返回值是正确的.
由于本系统对模拟系统调用进入的指令进行重写, 捕获到了参数, 并对参数进行解析将低层次语义信息转换为高层次语义信息. 对模拟系统调用退出的指令进行重写, 捕获到了返回值. 以上实验数据充分说明本系统在捕获系统调用方面具有高效性和准确性的特点.
3.2.2 性能分析
本文实验使用了Nbench工具分别对宿主机和客户机的性能进行了分析. Nbench是一个简单的用于测试处理器和存储器性能的基准测试工具, 它的测试结果主要分为MEM、INT和FP, MEM指数主要体现处理器总线、CACHE和存储器性能, INT指整数处理性能, FP指双精度浮点性能. 测试所得数据能够反映宿主机和客户机在不同状态下的性能, 即原始状态的性能①、运行监控程序获取系统调用原始信息的性能②和对信息进行处理时的性能③, 宿主机性能比较如图5所示, 客户机性能比较如图6所示.
根据图5, 可以发现运行系统调用监控程序对宿主机并没有什么影响, 主要是由于宿主机只负责和客户机进行交互操作以及系统调用信息的处理等操作, 而这些操作主要涉及处理器的计算和内存的读写, 所以性能下降不是很明显.
根据图6, 可以发现监控程序在只捕获客户机系统调用原始信息的情况下, 即运行Nitro的情况下, 客户机的性能下降较小. 当监控程序需要对系统调用信息进行处理, 也就是需要解析系统调用参数和返回值、进程信息的时候, 客户机的性能下降较多, 主要是由于客户机在调用系统调用退出命令时陷入到VMM中, 陷入期间宿主机获取并解析系统调用参数, 客户机调用系统调用退出命令时再次陷入VMM中, 陷入期间宿主机获取系统调用返回值, 这两步造成客户机陷入时间过长. 针对MEM、INT和FP测试结果, 客户机原始性能指数分别为22.7、29.1和45.7, 开启信息处理之后客户机的性能指数分别为2.8、2.5和2.7, 性能开销比例分别为8.1=22.7/2.8、11.64=29.1/2.5和16.9=45.7/2.7.
综上分析可知, 性能开销在可接受的范围内, 并且在只获取系统调用原始信息的情况下, 性能开销比例只有1到1.5左右, 性能要优于熊海泉等人提出的非陷入系统调用指令捕获方法[10], 其方法首先在VMM中通过监测CR3的更新来识别客户机内的当前进程, 随后通过修改硬件规范实现对基于sysenter和基于syscall指令系统调用的捕获, 最后基于netlink机制输出系统调用信息. VMM在创建netlink连接向用户空间发送系统调用信息时客户机处于暂停状态, 额外增加了客户机的性能开销, 其方法对基于sysenter指令系统调用捕获的开销比例为2.608=1238.8/475.0, 对基于syscall指令系统调用捕获的开销比例为2.195=1213.3/552.8. 本监控系统性能优于其他方法的原因是在实现仿真指令的重写过程中即获取到了系统调用信息并实时传递给用户空间, 同时考虑到客户机存在多VCPU的情况, 引入了线程池技术实现系统调用信息的快速捕获和解析, 大大提升了客户机的性能.
4 结束语为了解决捕获客户机系统调用时无法解析系统调用参数和返回值的问题, 本文在KVM虚拟化平台上设计并实现了一个系统调用监控系统. 系统采用了模块化的设计, 内核功能扩展模块和用户空间模块相互配合. 该系统不仅可以实时捕获客户机内的系统调用序列, 同时还可以解析系统调用的参数和返回值, 将系统调用的原始信息转换为高层次语义信息.
目前该系统在解析系统调用原始信息时对客户机带来的性能开销较大, 对用户的使用有些影响. 此外, 系统未涉及针对基于系统调用的恶意软件的分析, 这些将是我们未来的研究工作.
[1] |
吴瀛, 江建慧, 张蕊. 基于系统调用的入侵检测研究进展. 计算机科学, 2011, 38(1): 20-25, 47. DOI:10.3969/j.issn.1002-137X.2011.01.004 |
[2] |
Dinaburg A, Royal P, Sharif M, et al. Ether: Malware analysis via hardware virtualization extensions. Proceedings of the 15th ACM Conference on Computer and Communications Security. Alexandria, VA, USA. 2008. 51–62.
|
[3] |
Pfoh J, Schneider C, Eckert C. Nitro: Hardware-based system call tracing for virtual machines. Proceedings of the 6th International Workshop Advances in Information and Computer Security. Tokyo, Japan. 2011. 96–112.
|
[4] |
Kivity A, Kamay Y, Laor D, et al. KVM: The Linux virtual machine monitor. Proceedings of Ottawa Linux Symposium. Ottawa. 2007. 1–8.
|
[5] |
Bartholomew D. QEMU: A multihost, multitarget emulator. Linux Journal, 2006, 145: 3. |
[6] |
广小明, 胡杰, 陈龙, 等. 虚拟化技术原理与实现. 北京: 电子工业出版社, 2012. 43–45.
|
[7] |
李勇钢, 崔超远, 李平. 基于虚拟机自省的客户机进程内容获取. 计算机工程与设计, 2016, 37(6): 1697-1701. |
[8] |
Xiong HQ, Liu ZY, Xu WZ, et al. Libvmi: A library for bridging the semantic gap between guest OS and VMM. Proceedings of the 2012 IEEE 12th International Conference on Computer and information Technology. Chengdu, China. 2012. 549–556.
|
[9] |
Agesen O, Garthwaite A, Sheldon J, et al. The evolution of an x86 virtual machine monitor. ACM SIGOPS Operating Systems Review, 2010, 44(4): 3-18. DOI:10.1145/1899928 |
[10] |
熊海泉, 刘志勇, 徐卫志, 等. VMM中Guest OS非陷入系统调用指令截获与识别. 计算机研究与发展, 2014, 51(10): 2348-2359. DOI:10.7544/issn1000-1239.2014.20130612 |