通过QEMU/KVM GDB Stub进行内核调试

内核准备

内核配置

建议从源代码重新编译内核,这样可以更方便地进行源代码级调试;当然自己从发行版官网下载相应内核的调试符号亦可以,通常情况下只用于应急分析,比如crash dump分析等。

自己编译Linux内核的话,最好打开两个选项:

CONFIG_DEBUG_INFO=y
CONFIG_GDB_SCRIPTS=y

可通过make menuconfig来设置内核配置项,分别以5.12及6.1.0为例: 5.12: Linux kernel 5.12 menuconfig 6.1.0: Linux kernel 6.1.0 menuconfig

禁止编译优化

无法针对Linux内核禁止全局优化,但可以针对特定的源代码文件做禁止编译优化的处理,可通过以下方式达成:

在需要被调试的源代码文件头部增加下面一行代码:

#prgama GCC optimize ("O0")

或者修改其对应的Makefile,以文件名core.c为例:

CFLAGS_REMOVE_core.o := -O2
CFLAGS_core.o := -O0

镜像准备

建议通过libvirt来管理本地KVM虚拟机,这样使用起来非常方便,如果条件不允许的话亦可用qemu命令行方式来运行。

添加 gdb stub

下面的示例是以命令行方式来运行调试目标:

qemu-system-x86_64 -m 4096 -nographic -net user,hostfwd=tcp::44334-:22 -net nic -hda \
    debian-10.7.qcow2 -machine type=pc,accel=kvm -cpu host -smp 4 -gdb tcp:127.0.0.1:44333

“-gdb tcp:127.0.0.1:44333” 就是gdb stub的命令行参数,即gdb可连接localhost:44333来调试目标系统。

如果使用libvirt的话则需要修改QEMU配置,可通过命令virsh edit来进行修改,需要在末尾加上中间4行:

   </devices>

   <qemu:commandline>
    <qemu:arg value='-gdb'/>
    <qemu:arg value='tcp:localhost:44333'/>
  </qemu:commandline>

</domain>

另外还需要更改虚拟机配置文件的domain type,即将第一行从

<domain type='kvm'>

改为:

<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>

修改内核启动项

本步骤主要为了禁止内核地址随机化,不然内核符号无法定位并正确绑定。可通过修改grub项完成,在安装新内核过程中会自加增加nokaslr及nopti,当然在编译内核时亦可直接禁止相应功能亦可。

root@T490:~# cat /etc/default/grub | grep nokaslr
GRUB_CMDLINE_LINUX_DEFAULT=&quot;noquiet nopti nokaslr console=ttyS0&quot;

最直接的方式就是直接修改/boot/grub/grub.cfg文件,或者在系统启动时手工编译grub启动项亦可:

root@T490:~# cat /boot/grub/grub.cfg | grep nokaslr
    linux   /boot/vmlinuz-5.10.14 root=UUID=c4072cd3-9ed9-4376-b85c-95d1306e817b ro  noquiet nopti nokaslr console=ttyS0,115200

host端准备

前面我们就完成了guest端即调试目标机的配置,然后就是准备host端,可以用gdb,亦可以用vscode等gui工具,甚至可以用Windows Visual Studio配合VisualKernel工具来调试Linux系统。

源代码及调试符号vmlinux

将相应源代码及调试符号vmlinux复制到host端即可,复制时注意软链接问题,比如:

vmlinux-gdb.py -> /BUILD/linux-5.12/scripts/gdb/vmlinux-gdb.py

设置gdb的auto-load路径

建议直接修改 ~/.gdbinit文件,当然手动执行亦可,当处理多个调试机时最好通过.gdbinit来保存配置:

matt@T490 /W/K/debian-10.7> cat ~/.gdbinit 
add-auto-load-safe-path /usr/share/gdb/python/gdb/
add-auto-load-safe-path /usr/share/gdb/python/
add-auto-load-safe-path /usr/share/gdb/
add-auto-load-safe-path /BUILD/linux-5.12
set auto-load python-scripts on
source /BUILD/pwndbg/gdbinit.py

我本地的调试环境里安装的是pwndbg,已配置好了python组件

启动gdb

cd /BUILD/linux-5.12; gdb ./vmlinux

在gdb中执行以下执行开启内核调试:

    target remote :44333

然后gdb会自动加载pwndbg并中断目标机: Linux kernel 5.12 gdb

在内核gdb scripts加载成功的情况下可以执行lx系列扩展命令以获取内核信息: Linux kernel 5.12 gdb scripts

可通过apropos lx查询相应的gdb scripts扩展命令,如lx-lsmod, 变量:$lx_current():

gdb> apropos lx
function lx_clk_core_lookup -- Find struct clk_core by name
function lx_current -- Return current task.
function lx_device_find_by_bus_name -- Find struct device by bus and name (both strings)
function lx_device_find_by_class_name -- Find struct device by class and name (both strings)
function lx_module -- Find module by name and return the module variable.
function lx_per_cpu -- Return per-cpu variable.
function lx_rb_first -- Lookup and return a node from an RBTree
function lx_rb_last -- Lookup and return a node from an RBTree.
function lx_rb_next -- Lookup and return a node from an RBTree.
function lx_rb_prev -- Lookup and return a node from an RBTree.
function lx_task_by_pid -- Find Linux task by PID and return the task_struct variable.
function lx_thread_info -- Calculate Linux thread_info from task variable.
function lx_thread_info_by_pid -- Calculate Linux thread_info from task variable found by pid
lx-clk-summary -- Print clk tree summary
lx-cmdline --  Report the Linux Commandline used in the current kernel.
lx-configdump -- Output kernel config to the filename specified as the command
lx-cpus -- List CPU status arrays
lx-device-list-bus -- Print devices on a bus (or all buses if not specified)
lx-device-list-class -- Print devices in a class (or all classes if not specified)
lx-device-list-tree -- Print a device and its children recursively
lx-dmesg -- Print Linux kernel log buffer.
lx-fdtdump -- Output Flattened Device Tree header and dump FDT blob to the filename
lx-genpd-summary -- Print genpd summary
lx-iomem -- Identify the IO memory resource locations defined by the kernel
lx-ioports -- Identify the IO port resource locations defined by the kernel
lx-list-check -- Verify a list consistency
lx-lsmod -- List currently loaded modules.
lx-mounts -- Report the VFS mounts of the current process namespace.
lx-ps -- Dump Linux tasks.
lx-symbols -- (Re-)load symbols of Linux kernel and currently loaded modules.
lx-timerlist -- Print /proc/timer_list
lx-version --  Report the Linux Version of the current kernel.

Linux kernel 6.0 gdb Linux kernel 6.0 gdb Linux kernel 6.0 gdb

gdb常用命令及调试技巧

常用命令

常用gdb指令:bt, disass, step, next, break/delete, print, x/[i/g/d]等,具体可查询gdb命令手册

断点设置

使用break命令可设置断点,支持以下几种命令格式:

  • break 函数名: break dump_stack
  • break source_file:line_number: break smith_hook.c:70
  • break 逻辑地址:break 0xffffffff81003010,可以强制break跳过符号/函数名识别

针对模块的调试,可以考虑在模块中增加不常用内核函数的调用,如dump_stack;模块加载前是无法设置断点的,但内核函数的调用是可以的,这样就可以在模块调用指定内核函数时触发断点,然后再通过add-symbol-file /path_to/ko_file base_address,base_address或通过lx-lsmod查询得到,即模块在内核中的内存位置,之后就可以访问模块内的符号了,如全局变量或内部函数

X86_64位环境调试32位虚拟机

需要在attach前设置目标及当前系统架构,如 set architecture i386:x86-64。建议安装gdb-multiarch,以免系统自带gdb不支持多种架构:

pwndbg> target remote :44323
Remote debugging using :44323
warning: Selected architecture i386 is not compatible with reported target architecture i386:x86-64
warning: Architecture rejected target-supplied description
Remote 'g' packet reply is too long (expected 312 bytes, got 608 bytes): 102b91c10......0000

pwndbg> set architecture i386:x86-64
The target architecture is set to "i386:x86-64".
pwndbg> target remote :44323
Remote debugging using :44323
0x00000000c1912b23 in default_idle () at arch/x86/kernel

Linux系统安全-入侵检测与防护思路

1 系统安全简介及现状分析

主机安全涉及防御的整个纵深,内容繁多,如防火墙、WAF、RASP、IDS、IPS、安全审计、病毒杀毒、补丁管理、安运与安服等等。本文的目标主要是解决单点的系统威胁的发现与防御问题,属于系统安全的范畴,主要包含攻击发现、本地提权、rootkit防护三方面的内容。

本文目标在操作系统层面仅限于Linux系统,体系架构上以X86为主,兼顾ARM64。公司目前的主机系统主要是Intel XEON架构。既然以服务器系统为主,所以必须应对生产服务器对稳定性及性能的苛刻要求,并在此前提下达成安全上的需求。

2 核心需求

  1. 实现入侵行为的检测与防护,保障服务安全
  2. 稳定性与性能兼顾,保证服务可用性

总之,从检测能力及防护能力上着手,进一步接近并达成反入侵的需求,确实做好安全与保障。

3 攻击路径

大部分的入侵行为均可分成以下几个阶段:

  1. 扫描探测:通过扫描、社工或钓鱼等方式以了解主机信息,如网络结构、操作系统及服务类型等
  2. 漏洞触发:针对系统漏洞或配置错误发起攻击,前期攻击办能搭载较轻量的payload
  3. 主体下载:成功触发漏洞并达成了RCE (shellcode执行)或取得了shell权限,然后下载工作负载,如挖矿、勒索程序
  4. 主体执行:运行新下载的工作负载以启动真正的攻击,如挖矿、勒索、横向攻击、提权或持久化
  5. 本地提权:利用系统漏洞组合攻击,这一步有难度较高,且并非必需,主要由攻击意图决定
  6. 后门固化:即持久化,并隐藏自身或伪装、混淆

在主机这个单点上由于缺少关联数据,针对“扫描探测”只能做有限应对,如弱密码爆破场景,其它复杂场景需要汇集终端及其它主机的数据综合分析以作判定。第二步的漏洞触发也是很难检测到的,如未知0DAY漏洞本身就没有任何的先验知识,毕竟漏洞的存在是不可回避的事实。好在从第三步开始,主机端均有机会发现并做出合理判定,因为攻击意图必须以特定的攻击行为方能达成。

攻击行为相对于系统正常行为来说还是少之又少,并且多数攻击行为和正常操作并没有太大差异,这也是攻击检测的难点。攻击行为可分为两部分分别应对:可疑攻击和未知攻击,可疑攻击部分毕竟有先验知识,可以划定出较清楚的边界,此部分可以马上着手实现,这也是本文的重点。

4 可疑攻击

这部分主要依据经验总结,是针对当前HIDS的补充和增强,检测部分主要有4类检测项目,实现思路部分先做个简单阐述,等商定后再做详细详解。

4.1 攻击检测

4.1.1 CPU关键控制位

此部分主要针对CPU硬件的安全特性的检测,攻击者为了展开攻击或扩大成果不得不关闭这些安全特性:

  1. MSR寄件器EFER.NXE位:即DEP/NX位,数据区防执行,本安全特性默认开启,Linux系统允许通过grub的kernel参数开启或关闭。另外亦可通过修改MSI寄存Extended Feature Enable Register (EFER)的Bit 11调节。EFER寄存器是位于MSR的C0000080H处。
  2. CR4.SMEP位:Supervisor Mode Execution Prevention,此功能可阻止内核特权模式下运行用户层代码,可阻止ret2usr类的攻击。
  3. CR4.SMAP位:Supervisor Mode Access Prevention,配合EFLAGS的AC位可开启或关闭,可以阻止内核特权模式下直接访问用户层内存。在必须访问用户内存时,系统支持函数copy_to/from_user会临时设置EFLAGS的AC位。此特性从Linux 3.7开始支持。
  4. CR4.TSD位:Time Stamp Disable, 高精度计数器,可用于时序攻击,如Meltdown及Spectre/MDS等针对CPU漏洞的攻击所采用的就是rdtsc或rdtscp指令以精准确定CPU cache是否命中。
  5. CR4.PCE位: Performance-monitor Counter Enable,类似TSD,但精度上不如TSC。

4.1.2 内核检查项

这部分主要是针对内核对象及内核空间的检测,需要内核权限:

  1. 内核镜像: KIMAGE_VADDR, kpti/kaslr等属性
  2. 核心表项:idt/syscall table/shadow kernel
  3. 内核模块:映射区域:MODULES_VADDR - MODULES_END,3条链:module/sysfs/kobject
  4. 关键内核对象:file/dentry/inode_operations等,涉及文件系统(包含/proc、设备等重要数据结构,常被rootkit篡改以隐藏进程、网络连接、病毒文件等
  5. 内核通知链notifier_chain: 内核支持几十个,较重要的有module、keyboard等,后者可用于实现keylogger
  6. 网络驱动netfilter内部结构及操作表项
  7. binfmt: 二进制可执行行程序加载:elf程序运行、内核模块加载等

4.1.3 进程检查项

此部分检测针对用户进程,主要是进程相关的核心数据结构、以及对进程自身的R3用户空间的内存的检测:

  1. 可执行内存区域检测: a) W + X 内存映射区,针对jit程序存在误报可能 b) 不属于任何模块的映射区:如通过【REF1: ELF反射加载器】自展开加载的模块或程序
  2. VDSO及vsyscall映射区域防篡改校验:常用于ret2dir及DirtyCow攻击,可通过PFN与内容HASH进行校验
  3. 运行时安全属性检测: a) LD_PRELOAD: 动态库搜索路径,最好程序运行时进行检查 b) 第三方模块注入,如Apache插件,第三方注入在Linux系统上并不常见
  4. task_struct结构:如属主权限信息:uid/suid/euid/gid/egid/fsuid/fsgid, cap_effective/inheritable/permitted等,定时检测可以发现“任意内存写”攻击 a) 非法pid值检查:利用非法pid值可以达成进程隐藏 b) 线程Stack Pointer检查 (Stack Pivot攻击),难点是:go/rust/jit的

4.1.4 高危调用

这里我们主要关注进程、程序/动态库/模块加载、内存及文件相关的调用,与Linux LSM的监控点基本符合。以4.4.131的内核为例,系统共提供了多达198个监控点回调,较新的5.4.2内核已经支持多达216监控回调:

类别 钩子函数个数 功能
ptrace 2 ptrace操作的权限检查
能力机制 3 Capabilities相关操作的权限检查
磁盘配额 2 配盘配额管理操作的权限检查
超级块 13 mount/umount/文件系统/超级块相关操作的权限检查
dentry 2 dentry相关操作的权限检查
文件路径 11 文件名/路径相关操作的权限检查
inode 27 inode相关操作的权限检查
file 13 file相关操作的权限检查
程序加载 5 运行程序时的权限检查
进程 27 进程/进程相关操作的权限检查
IPC 21 IPC消息/共享内存/信号量等操作的权限检查
proc文件系统 2 proc文件系统相关操作的权限检查
系统设置 2 日志,时间等相关操作的权限检查
内存管理 1 内存管理相关操作的权限检查
安全属性 7 对安全相关数据结构的操作的权限检查
网络 37 网络相关操作的权限检查
XFRM 11 XFRM架构想要关操作的权限检查
密钥 4 密钥管理相关操作的权限检查
审计 4 内核审计相关操作的权限检查
Android Binder 4 Android Binder架构有关操作的权限检查
总计 198

重点关注项:

  1. prctl: 进程项调节
  2. ptrace: 调试
  3. mmap/mprotect (W+X):内存映射、可执行权限设置
  4. dlopen/dlmopen: 动态库加载
  5. LKM/module加载时针对modprobe_path的检查
  6. execve/execveat等:针对主、客体的关联判断,另外可增加主被动交互的侦测,如通过bash.readline可以判断是否是用户手动输入
  7. commit_creds/setuid/seteuid/setegid...
  8. chown/chmod +s/rename/unlink/link等:文件属主、属性相关操作

4.2 实现思路简述

需要两种检测机制:

4.2.1 定时检测:CPU及纯内核数据

此部分需要周期性的定时检测,实现方式有多种,如hrtimer/timer回调,延时工作队列(delayed workqueue)、硬件PMU或Intel PT等机制,目前来看延时工作队列的方式足以满足需求,如果后期对实时性要求更高可改用高精度时钟(hrtimer),这两种方式是都是Linux内核本身所支持的,有良好的普适性和稳定性。

在涉及内核对抗的情况下,可采用PMU或PT等硬件机制,硬件机制还能应对基于虚拟化的攻击,参考实现有【REF3: HARDWARE-ASSISTED ROOTKITS】及【REF4: GhostHook – Bypassing PatchGuard with Processor Trace Based Hooking】。

4.2.2 事件触发检测:进程相关数据

  1. 模块加载、进程创建等操作可通过通知链或kprobe监控点触发,如LD_PRELOAD及modprobe_path等项目
  2. 进程相关数据检测:要通过进程调度事件触发(trace_sched_switch),当进程时间片用完被调试出去时检测task_struct结构及相关内存映射属性等项目
  3. 高危调用

这部分数据的检测要注意事件回调的上下文环境,部分数据检测会涉及进程用户层内存的访问,属于softirq的hrtimer回调中是不能进行此操作的,需要转至workqueue或私有内核线程。

另外,纯用户层内存的检测亦可以通过R3 HOOK【REF2: TrustSec: LINUX: HOW’S MY MEMORY】或uprobe机制实现(自Linux 3.8开始支持),但必要性并不大,完全内核态的实现没有引入过多复杂性。

5 未知攻击

针对未知攻击一直没有较好的应对方式,一些比较苛刻的环境采用了可信计算的机制,即非白即黑的策略。可信计算主要解决了程序来源的问题,并不能有效应对程序的漏洞问题。业界公司应对内存安全的方案,所解决的也是类似的未知攻击及内存安全问题。

本章节所描述的应对思路便遵循了非白即黑的策略,当然此部分并不是完全的可信计算,而是一种轻量级的程序来源可信及可信执行的工程化实现。

5.1 程序可信

程序可信所解决的是来源问题,有两种可实现方式:

  1. 白名单机制:利用现有的IMA/Integrity进行来源控制
  2. 针对ELF文件增加新section,所有的程序均要做此处理,技术上需要修改elf加载器

此部分对用户行为有较大影响,程序发布及扩散环节都要做处理,特别是第二种方式。

5.2 执行可信

执行可信考虑的不是程序的来源,而是程序自身的代码逻辑,可分为两种粒度的控制:

  1. 指令控制流防护(CFI):需要编译器在编译阶段生成指令时加入分支指令操作的判断,这部分相对已经成熟,gcc及llvm均有支持,会增加程序的体量和性能损耗,但总体上可控。grSecurity的RAP方式就是类似的方式,可参见:【REF5: grSecurity RAP】
  2. 关键API调用序列防护:在API或系统调用层次上实现的执行防护,需要预先针对可执行程序进行静态或动态分析以生成规则,并在运行时进行插桩检测,同RASP异曲同工,只是所解决的问题不在一个层面上。相关的参考有【REF6: BlackBox: Lightweight Security C Monitoring for COTS Binaries】及【REF7: Virsec Trusted Execution】

5.3 脚本防护

Linux下可执行脚本种类繁多,且功能较大,没有统一的方式来应对,可常用的应对方案有以下几中:

  1. 用行为沙箱或类RASP的方式来做启发式分析
  2. 通过文件集进行执行管控: 如/tmp目录、web服务upload目录(新上传)脚本防执行,当然也有些例外情况,如安装包会自动生成脚本并执行
  3. 全量收集以进行后续人工分析及机器学习分类

6 其它功能

6.1 自保功能:防杀防卸载 6.2 ARM64等新体系结构的支持 6.3 代码优化及性能提升

7 样本数据库建设

安全不只是技术,还是人的行为管理和数据的运营,所以有必要从以下两个方面来强化数据的收集与关联:

7.1 行为数据及画像库

常登录的服务器、登录时间、经常的操作、来源ip等习惯行为数据

7.2 程序样本数据库

所有的程序(可执行程序、动态库、脚本)及安装包、来源信息的整理,现在360、腾讯等公司均已建立了较为完备的样本库,同样亦适用于URL/IP、威胁及漏洞等数据

8 参考资料

  1. REF1: ELF反射加载器
  2. REF2: TrustSec: LINUX: HOW’S MY MEMORY
  3. REF3: Hardware-Assisted Rootkits: Abusing Performance Counters on the ARM and x86 Architectures
  4. REF4: GhostHook – Bypassing PatchGuard with Processor Trace Based Hooking
  5. REF5: grSecurity RAP
  6. REF6: BlackBox: Lightweight Security C Monitoring for COTS Binaries
  7. REF7: Virsec Trusted Execution
  8. REF8: Integrity Measurement Architecture (IMA)

Windows HIPS防护面

1. 防护范围

Windows HIPS (Host Intrusion Prevent System) 主机入侵防御系统核心部分可以归纳为3D,即AD(Application Defend,应用程序防御体系)、RD(Registry Defend,注册表防御体系)和FD(File Defend,文件防御体系)。另外还有ND(Network Defend,网络防火墙)及DD(Device Defend,设备/外设防护),后两者也算是管控的范畴

2. AD

Windows的用户权限管理比较宽松,AD防护不仅复杂还面临较大的攻击面:

针对AD防护,仅利用系统内核层回调很难达成全面防护,只有当攻击已发生且已经成功的情况下才能发现攻击,此时恶意攻击已然达成。AD防护的短板极有必要采用R0或R3 HOOK的方式予以弥补和加固。

操作 方式 说明
创建 - 通用回调可以在进程启动时得到通知,但在“进程为什么会启动”的原因上缺少必要信息,R0/R3 HOOK可以作为必要的补充手段来实现全方面的进程监控
普通进程 在进程加载阶段可侦测,可采用手段:FD及通用回调
脚本程序 可侦测,如cmd batch、powershell、python及WScript或CScript等,可侦测到解释程序的启动及相关命令行参数
计划任务 可侦测,可通过父进程判断是不是计划任务,但计划任务的创建无从感知。当然计划任务的创建亦可从文件系统层感知并发现,但此方式只是个间接方案
服务创建及修改 加载时通过进程回调可侦测,服务创建及修改可通过RD侦测
驱动创建及修改 同服务
WMI串链 可侦测新进程启动,但不能定位由谁创建
DCOM串链 可侦测,但不能定位原始启动进程
DDE加载 可侦测
MSI安装拦截 可侦测
PICO/WSL进程 可发现、可拦截(Win10系统)
...
结束 - 通用回调可以在得到进程退出通知,但时机上已经太晚了;只能通过R3或R0 HOOK实现更早和更好时机上的拦截
窗口消息 通过SendMessage/PostMessage/PostThreadMessage方式直接发送退出消息
动作模拟 通过模拟按键及鼠标输入,直接发送ALT+F4或者鼠标点击事件
结束进程 TerminateProcess/NtTerminateProcess
停止或结束线程 SuspendThread/TerminateThread/ NtSuspendTHread/NtTerminateThread
调试模式 NtDebugActiveProcess DebugSetProcessKillOnExit ...
job调度 通过AssignProcessToJobObject及TerminateJobObject强制关闭进程,或通过job作业限制进程的执行调度
WMI利用
WinStation
执行劫持 通过远程线程、APC或SetThreadContext将流程导向ExitProcess
资源耗尽 耗尽进程的Handle/VM资源等以造成程序运行错误
镜像破坏 通过VirtualProtectEx改变关键页面属性至PAGE_NOACCESS等,或通过WriteProcessMemory进行破坏
内核攻击 方式非常多,不在我们考虑之列
...
注入 - 在R3 HOOK统一框架设计中有详细描述,在此只简单列举常用方式。通过模块加载系统回调可以感知注入事件的发生,但不能直接拦截
APC注入 R0及R3均可攻击,除R0/R3 HOOK外没有好的方案
远程线程注入 R0及R3均可攻击,可通过线程创建回调侦测到
消息钩子 可通过模块加载侦测,但无法溯源至“消息钩子”
线程Context劫持 R0及R3均可攻击,不可侦测,只能采用HOOK方案
内存写入 R0及R3均可攻击,不可侦测,只能采用HOOK方案
UserModeCallback 不可侦测,只能采用HOOK方案
Section Un-mapping 不可侦测:UnMapViewOfSection/NtUnmapViewOfSectionEx
SetWindowLong[Ptr]注入 不可侦测,只能采用HOOK方案
打印、输入法注入 可通过模块回调侦测到,但无法溯源
DLL劫持 DLL搜索路径篡改、DLL文件替换等
...
其它 - 隐私窃取、信息泄漏
消息拦截 不可侦测
KeyLogger 一般均是通过DLL注入实现
白利用、提权、沙箱逃逸 发现与拦截均有难度

3. RD

RD防护通过注册表操作回调基本可以达成我们针对注册表操作的监控、审计及拦截的需求。注册表回调操作自Windows XP系统之后均有支持,但XP系统的支持并不完善,有可能需要采用OB HOOK等方式以弥补回调功能的缺失。

Vista之后系统针对注册表操作增加了事务处理,WDK已有演示程序。

操作 前置回调 后置回调
键删除 RegNtPreDeleteKey RegNtPostDeleteKey: SRV2003
值创建、改变 RegNtPreSetValueKey RegNtPostSetValueKey: SRV2003
值删除 RegNtPreDeleteValueKey RegNtPostDeleteValueKey: SRV2003
键属性修改 RegNtPreSetInformationKey RegNtPostSetInformationKey: SRV2003
键改名 RegNtPreRenameKey RegNtPostRenameKey: SRV2003
子键枚举 RegNtPreEnumerateKey RegNtPostEnumerateKey: SRV2003
值枚举 RegNtPreEnumerateValueKey RegNtPostEnumerateValueKey: SRV2003
获取键信息 RegNtPreQueryKey RegNtPostQueryKey: SRV2003
值内容提取 RegNtPreQueryValueKey RegNtPostQueryValueKey: SRV2003
RegNtPreQueryMultipleValueKey RegNtPostQueryMultipleValueKey: SRV2003
子键创建 RegNtPreCreateKey RegNtPostCreateKey
子键创建 RegNtPreCreateKeyEx: SRV2003 RegNtPostCreateKeyEx: SRV2003
子键打开 RegNtPreOpenKey RegNtPostOpenKey
子键打开 RegNtPreOpenKeyEx: SRV2003 RegNtPostOpenKeyEx: SRV2003
子键关闭 RegNtPreKeyHandleClose RegNtPostKeyHandleClose: SRV2003
RegNtPreFlushKey: VISTA RegNtPostFlushKey: VISTA
RegNtPreLoadKey: VISTA RegNtPostLoadKey: VISTA
RegNtPreUnLoadKey: VISTA RegNtPostUnLoadKey: VISTA
安全描述符提取 RegNtPreQueryKeySecurity: VISTA RegNtPostQueryKeySecurity: VISTA
安全描述符设置 RegNtPreSetKeySecurity: VISTA RegNtPostSetKeySecurity: VISTA
RegNtPreRestoreKey: VISTA SP2 RegNtPostRestoreKey: VISTA SP2
RegNtPreSaveKey: VISTA SP2 RegNtPostSaveKey: VISTA SP2
RegNtPreReplaceKey: VISTA SP2 RegNtPostReplaceKey: VISTA SP2
RegNtPreQueryKeyName: WIN10 RegNtPostQueryKeyName: WIN10
RegNtCallbackObjectContextCleanup: VISTA

4. FD

针对FD的防护将基于Windows FS Mini Filter架构实现,Windows FS Mini Filter可支持Win2k SP4、XP SP2及最新的Windows操作系统。XP及Win2k系统上的支持最新版本Windows的支持有些差别,在实现时要多加注意。另外Vista之后文件操作事务的支持增加了攻击面,使得针对文件的攻击手段更加隐蔽,因此文件事务操作的支持是必须的。

Mini Filter框架不仅可对本地及网络文件进行监控,亦支持邮槽及命名管道的操作监控,后期可根据实际需求进行灵活扩展。

操作 描述 备注
IRP_MJ_CREATE 文件打开、删除、覆写(清空)操作
IRP_MJ_CLEANUP 文件句柄关闭
IRP_MJ_CLOSE
IRP_MJ_CREATE_MAILSLOT 只针对邮槽驱动
IRP_MJ_CREATE_NAMED_PIPE 只针对管道驱动
IRP_MJ_DEVICE_CONTROL 与应用层通信、存储设备通信
IRP_MJ_DIRECTORY_CONTROL 目录项管理
IRP_MJ_FILE_SYSTEM_CONTROL
IRP_MJ_FLUSH_BUFFERS
IRP_MJ_INTERNAL_DEVICE_CONTROL
IRP_MJ_LOCK_CONTROL byte range lock(flock)支持
IRP_MJ_PNP 设备热插拔处理
IRP_MJ_QUERY_EA EA数据流处理
IRP_MJ_QUERY_INFORMATION 文件信息处理
IRP_MJ_QUERY_QUOTA 用户Quota支持
IRP_MJ_QUERY_SECURITY 安全属性及安全描述
IRP_MJ_QUERY_VOLUME_INFORMATION 卷属性操作
IRP_MJ_READ 读操作
IRP_MJ_SET_EA EA创建或写入
IRP_MJ_SET_INFORMATION 设置文件信息、改名、删除文件等
IRP_MJ_SET_QUOTA 设置用户Quota
IRP_MJ_SET_SECURITY 设置安全描述符
IRP_MJ_SET_VOLUME_INFORMATION 设置卷属性、卷标信息
IRP_MJ_SHUTDOWN 关机事件
IRP_MJ_SYSTEM_CONTROL WMI
IRP_MJ_WRITE 写操作
IRP_MJ_ACQUIRE_FOR_CC_FLUSH 锁操作回调
IRP_MJ_ACQUIRE_FOR_MOD_WRITE 锁操作回调: mmap
IRP_MJ_ACQUIRE_FOR_SECTION_SYNCHRONIZATION 锁操作回调: Section首次创建
IRP_MJ_FAST_IO_CHECK_IF_POSSIBLE Fast i/o调用检测
IRP_MJ_MDL_READ MDL i/o,一般用于SRV
IRP_MJ_MDL_READ_COMPLETE
IRP_MJ_MDL_WRITE_COMPLETE
IRP_MJ_NETWORK_QUERY_OPEN
IRP_MJ_PREPARE_MDL_WRITE
IRP_MJ_RELEASE_FOR_CC_FLUSH 锁操作回调
IRP_MJ_RELEASE_FOR_MOD_WRITE 锁操作回调
IRP_MJ_RELEASE_FOR_SECTION_SYNCHRONIZATION 锁操作回调
IRP_MJ_VOLUME_DISMOUNT 卷卸载操作
IRP_MJ_VOLUME_MOUNT 新卷加载操作

5. ND

ND所实现的就是一个轻量的防火墙,可以将五元组信息与进程进行关联。Windows的网络框架随不同的版本有较大的变动,NDIS、TDI、WSK/Winsock Kernel、WFP/Windows Filter Platform

6. DD

DD即设备防护,主要应用于对外设的管控,可实现设备的停用或限制

7. 参考链接

1: Magnesium Object Manager Sandbox, A More Effective Sandbox Method for Windows 7

2: MSDN: Filtering Registry Calls

3: Filter Manager Support for Minifilter Drivers

4: MSDN: REG_NOTIFY_CLASS enumeration

5: CodeMachine: Kernel Callback Functions

6: OSR: Kernel Mode Extensions Driver

7: WDK Samples: Registry Logging

8: https://blog.csdn.net/whatday/article/details/52623878

9: https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/handling-notifications

Cache相联度测量

从上篇文章中我们得知Cache在兼顾性能及成本后都是设计成组相联方式的,以Intel Pentinum G5500T为例:L1是8路组相联,L2是4路组相联,LLC是16路组相联。针对一款未知CPU,可以通过cpuid指令来查询到以上信息,但我们的目标是通过内存访问延时的度量来测算出每级Cache的相联度。

测量方法

测量的原理是尝试制造“相联度冲突”强制触发CacheLine的驱逐(Evict),以此来推算出Cache的相联度。参见下图: Cache 相联度测量方法

这里我们先将整个测试内存块分成不同数量的Window大小,然后以Window为间隔进行跳跃式的访问,为此分别构造两个指针链,第一条指针链是左图所示的每个Cache Line的第一个DWORD(或QWORD),右图则是第二指针链,将每个Cache Line的最后一个DOWRD(或QWORD)串联起来。测量过程分为两步预热和度量:先通过第一条链的访问达成Cache的预热(从内存加载至Cache),然后度量遍历第二条链的访问时间。

如果所有Window的内存块均在Cache中,将不会发生冲突,此时的延时将是最短的。如果不断增加Window的数量(即相联度)的话,必然在超过Cache所设计的相联度之后发生冲刷效应,如L1 Cache是8路组的,就是说可容纳8个来自不同页面的Cache Line,如果再访问第9个内存页的话,CPU将不得不驱逐一个Cache Line,从而导致度量第二条指针链时访问延时的增加。

测量结果

Cache 相联度测量结果

1,水平方向基本呈现4个台阶,分别对应不同的访问速度,但并不严格对应L1/L2/L3和内存,因为每次测量都是混合访问的结果,即L1、L2、L3或内存都有访问,但各自的比率不同,Cache的命中率越高延时越低,即垂直方向上高度越低

2,先以1K的步长来分析,1K应该一直在最低台阶上,然后在X轴32处时间跳跃,说明L1 的Cache Size为32K,当探测组数超过32组时会发生挤出效应,同理2K/4K分别是16组及8组的探测位置出现分叉

3,打32K的第一个分叉点:8,这时说明以32K为步长的话,同时访问超过8组的话会导致时延变长,直接跳到了最高台阶,说明L1 Cache是8路分组的

4,从第二个台阶水平 ,从最右边向最左分别找到32 8K及16 16K两个分叉点,即256K的容量

5,然后以256K的线从最左侧向右寻找跃升点,分别在4和8的位置跃升,说明L2是4路分组的。那为什么会在8的位置也出现跃升呢?大家可以自己想一想

特殊的LLC

从上图中来看L3,疑似L3的台阶从右向左只在256K的线发生了跃升,跃升点是32,即32 * 256K = 4M; 然后再找4M的线,结果4M的线只在4和8点有跃升,然后一直平稳在L3的台阶上。8M的线亦同样,与L1及L2相对呈现出了不同的规律。

其实原因在于L3即LLC采用了不同的分组hash计算方式及不同的替换策略,以致当前的探测方式不再适用。但我们依然可以做个猜测,比如 32K、64K、128K的直接越过了L3,说明了什么问题?当32K、64K、128K超过8组的时就已经超出了L3的组容量,即产生了挤出效应,从而造成了直接内存访问的局面。关于LLC的映射算法及替换策略可以参见文末的参考链接,等后续有时间我再进行详细解读。

参考链接

Cache与内存的映射

1. 理解Cache Line

在大家印象中Cache Line就是一个数据块,能有什么理解难度呢?不妨再反问一下自己:真的是这样吗?

上篇文章中就说到Cache本身的容量远远小于内存容量,CPU为了能正确定位数据所在的CacheLine还需要做很多工作。简单来说可以将Cache Line理解成一个带标签的存储槽,下面以一个直观的例子来说明:

小A同学坐在座位上,如果老师想检查小A的作业的话就需要先定位到小A所在的座位号,然后再从小A的座位上拿到小A作业本。在这个例子中,小A的作业本就对应Cache Line中的数据,老师就是CPU Core,小A其实对应着内存地址(TAG),小A的座位就是Cache Line本身,而座位号就是Cache Line在Cache中的位置(索引,Index),教室里所有的座位就可以理解为整个Cache,而学校里所有的学生则是全部的系统内存。 Cache Flags

上图就是Cache的内部组织,除了TAG和数据外,还有两个标志位,V表示此CacheLine中的数据是否是有效的,即座位上有没有学生,D表示CacheLine中的数据有没有被修改过,修改过的数据是不能直接清除的,需要写回到主存(内存)。

2. Cache与内存的映射

映射方式有3种:

  1. 直接映射 (Direct Mapping)
  2. 组相联 (K-Way Set Associative Mapping)
  3. 全相联 (Full Associative Mapping)

2.1 全相联

继续以学生和座位为例,正常情况下我们并不限定位置,大家随便坐,先到先得。老师如果想检查一个同学的作业的话,也可以一个一个来找,即线性搜索,当然此举必然花费老师很多时间。另外一点需要强调,学生并不是老老实实坐在座位上的,不断会有学生进来或离开,也会有同学出去后又进来但前后的坐位并不相同的情况,这样的话“线性搜索”将不再可能。

想像最常见的一个场景,老师并不会主动来找,而是直接喊这个同学的学号(名字可能会有重名,但学号不会),所有的学生在听到名字后同时判断老师叫的是不是自己,只有被叫到的同学才会应答。在听到老师的请求后所有的学生同时并行判断的动作,对应于CPU及硬件实现就是一个并行的比较电路,而比较的内容则是座位上的同学和老师叫的学号,分别对应着TAG和内存地址。

这个例子中的随机编排座位的方式就是全相联映射。全相联映射方式的定位操作是最高效的,但是硬件成本亦最高,毕竟要实现复杂的并行比较电路,只有容量较小且对性能至关重要的缓存才会使用,如CPU的内存管理单元 (MMU, Mmemory Management Unit) 的TLB缓存 (Translation Lookaside Buffer),TLB中存放的是虚拟地址与物理地址的1:1的转换关系。

2.2 直接映射

假设上面例子中的教室只有20个座位,编号为0-19,全校共有400名学生,学号分别为0-399。直接映射方式的求是座位不能随便选择,而是通过学号和所有座位数的余数决定,即学号为0、20、40、60...的同学只能坐在0号座位上,同样学号为1、21、41、61...的同学只能争1号座位,后来的同学会将正坐在位置上的同学挤出去,如果1号和21号同学都要来教室的话就会发生两人不断被挤出去的情况,这就是Cache的颠簸效应(Cache Thrashing)。

2.3 组相联

为了解决直接映射的颠簸效应,遂引入了组相联映射。假设学校共有5个系,每个系均有80名学生,那我们可以这样安排座位,一系的学生只能选择0/5/10/15这4个座,二系则是1/6/11/16,依次类推,如下图所示:每个系有4个座位,这4个座位是没有顺序的,即本系的学生随意坐,但全系的80名学生将争抢这4个座位,这种分为5组的映射方式就是4路组相连映射,4路的意思是每组中有4个座位,即4个CacheLine。现实中CPU Cache无论是组数还是相联度(路)均是2的幂指数,不会出现本例中奇数5的情形。 Cache Groups 直接映射方式其实是组相联映射的一种特例,亦称作是单路组相联。

2.4 特别说明

更详细的阐述可参考链接1 [蜗窝科技: 浅谈Cache Memory],作者smcdef通过举例来说明三种方式的实现细节,作图精美,本文不再做过多说明。

3. Cache的分类及访问流程

目前主流的CPU,如Intel及AMD的桌面或服务器平台的CPU都是支持分页的,即物理内存的管理是以页划分和管理的。X86平台的页大小通常是4K,即4096字节。CPU层所操作的内存地址称作是虚拟地址(逻辑地址),并不是物理的内存地址,从虚拟地址至物理地址的翻译由硬件MMU来执行。Cache与MMU的位置关系直接决定了Cache的分类:如果Cache相对MMU离CORE的距离更近,则此类Cache被称作为逻辑Cache;相反则称作是物理Cache。下图是L1、L2及LLC与MMU的关系,L1是集成在CORE内部的,所以可以得出L1是逻辑Cache,而L2和LLC则是物理Cache。

Memory Paging & Addressing

如果再加上TAG的取值,取物理地址或虚拟地址,又可以组合成4种情况:

3.1. PIPT (Physically Indexed Virtually Tagged)

下图是2路组相联的PIPT的示例,CacheLine大小为64字节,即6个比特,组索引(SET)由S个比特来编址,剩余的部分即为标签项(TAG),CPU通过组索引来定位此地址所在Cache组,然后通过两组比较电路进行TAG值的并发比较,然后即可确定Cache所在位置,当然也有可能出现没有命中的情况。 Cache PIPT} L2及LLC,以及早期的CPU的L1均是使用PIPT的方式,这是因为内存的物理地址是唯一的,这就大大简单化PIPT的实现逻辑。

3.2. VIVT (Virtually Indexed Virtually Tagged)

VIVT的Index和Tag均使用虚拟地址,全程不需要MMU的参与,是性能最好的定位方式,但是由于虚拟地址的特性会导致歧义和别名的问题,最终决定了VIVT方式的不可行:

歧义 (Ambiguity,亦称Homonyms问题):不同的进程拥有相同的虚拟地址空间,虚拟化平台上不同的虚拟机亦是同样的情况。从而会导致相同的虚拟地址(不同进程或不同虚拟机)所对应的物理页面不同的,VIVT的方式将不能正确区分。

别名 (Alias):是指同一个物理页映射至不同的虚拟地址,即同一个物理地址将有多个虚拟地址的别名。多个别名的情况下VIVT将会映射至不同的CacheLine,将不能保证数据的一致性。

3.3. VIPT (Virtually Indexed Physically Tagged)

CPU层的指令所操作的均是虚拟地址,使用虚拟地址作为索引来直接进行Cache的定位不必经过MMU的转换。此时Cache Set的定位和MMU的转换过程是并发的,定位到指定Cache Set之后再进行两个Cache Line的Tag的对比,此时两个Tag的比对也是并行处理的。 Cache VIPT}

3.4. PIVT (Physically Indexed Virtually Tagged)

PIVT方式相比PIPT和VIPT来说没有任何优势,性能上必须等MMU转换完成之后,而且在数据一致性上无法处理好别名问题。

4. L1 Cache (VIPT)的别名问题

在Cache与内存的映射关系上,考虑到成本及性能,多采用组相联方式;在CacheLine的定位和访问方式上,L1 Cache多是VIPT,而L2及LLC是PIPT。以Intel Pentinum G5500T为例:L1是8路组相联,大小为32K; L2是4路组相联,大小为256K;LLC即L3则是16路组相联,大小为4M。

L1采用的是VIPT映射方式,虽然VIPT方式通过物理地址TAG解决了歧义的问题,但别名的问题依然存在,是怎样解决的呢?

实际上L1 Cache的每个Cache Set的数量正好限定在 (PAGE_SIZE/CACHE_LINE) 之内的,以G5500T为例,32K/(8*64) = 4K/64 = 64。虚拟地址最低6位为CacheLine中的偏移;位6-11共6位是组索引,6+6正好是12,即一个4K PAGE的大小,从而可推论出不管虚拟地址是多少,任意内存页面的固定偏移总是映射到相同的Cache Set中,而TAG的选择正是物理地址中的是12-47,可以理解为PFN,即每一个物理页面的唯一帧号,所以同一物理页的不同的虚拟地址最终将定位到同一个Cache Set中的同一个Cache Line上。

L1是8路组相联的,也就是说一个Cache Set中容纳了8个Cache Line,由上面的结论可得出:这8个Cache Line将分别来自于不同的物理页。最后大家不妨想一个问题,如果要将L1 Cache从32K增加到64K的话,怎样构造L1的相联度及Cache Set组数才是合理的?有多少种组合?

5. 参考连接