如何在用户态模拟器中镜像内核虚拟地址以生成一致的执行轨迹?
我太懂你这个痛点了——要让用户态模拟器完美复刻内核里程序的执行轨迹,结果沙箱基地址的空间差异直接给你挖了个坑。内核里沙箱在高地址的内核空间,用户态用malloc分配的却在低地址用户空间,这个基地址一旦被程序存进寄存器(比如你例子里的x29),后续所有依赖它的计算、内存访问甚至分支判断都会跑偏,轨迹自然就对不上了。
先拿你给的AArch64代码举例,这段逻辑里的地址依赖问题特别典型:
mov x1, #10 // 访问沙箱内偏移量10的位置 add x1, x1, x29 // 加上沙箱基地址 str x1, [x1] // 存储计算得到的地址 ldr x2, [x1] // 将其加载回x2
这里x2最终的值直接包含了沙箱的基地址,如果后续有类似cmp x2, #0xffff0000的分支判断,内核和用户态下x2的数值天差地别,分支走向肯定不一样,轨迹一致性直接就崩了。
针对这个问题,我给你几个可行的解决思路:
地址虚拟化重映射:这是最通用也最可靠的方案。在模拟器里加一层地址转换逻辑,维护一个「沙箱偏移 ↔ 内核虚拟地址 ↔ 用户态实际地址」的映射表:
- 当程序执行地址计算指令(比如
add x1, x1, x29)时,让寄存器里的基地址保持为内核沙箱的基地址,让程序看到的是内核空间的地址上下文; - 当程序执行内存访问指令(
str/ldr)时,把计算出的内核虚拟地址转换成用户态malloc的实际地址去操作内存; - 当从内存中读取地址值时(比如例子里的
ldr x2, [x1]),要把用户态实际地址反向转换成内核虚拟地址再写入寄存器,确保后续程序拿到的还是内核语境下的地址。
- 当程序执行地址计算指令(比如
寄存器劫持与透明修正:对于持有沙箱基地址的寄存器(比如x29),模拟器初始化时直接把它设为内核中的沙箱基地址,而不是用户态的实际基地址。之后在处理所有内存访问指令时,偷偷完成地址的转换——程序全程看到的都是内核地址,模拟器后台实际操作的是用户态的内存区域,完全不影响程序的逻辑判断。
尝试匹配沙箱地址布局:如果你的操作系统支持,可以试试用
mmap(或者对应平台的内存分配接口)强制申请和内核沙箱相同范围的虚拟地址。不过这个方法局限性很大:内核空间的高地址通常是用户态无法直接申请的(比如AArch64的0xffff开头的地址),而且可能需要关闭地址空间随机化等系统特性,通用性不强,只适合特定场景。
总的来说,地址虚拟化重映射是最值得投入的方案,虽然需要在模拟器里对每一条涉及地址计算、内存访问的指令做拦截和转换,但能从根源上消除基地址差异带来的执行轨迹偏差,完美复刻内核中的程序行为。
内容的提问来源于stack exchange,提问作者Gal Kaptsenel

