我们知道在x64里面,从3环进入0环会调用syscall
,那么如果是32位的程序就需要首先转换为x64模式再通过syscall
进入0环,这里就会涉及到一系列64位寄存器的操作,我们通过探究其实现原理来达到隐藏数据和反调试的效果。
基础知识
在支持IA-32e模式的系统上,提供扩展功能启用寄存器(IA32_EFER
),该寄存器控制IA-32e模式的激活和其他IA-32e模式的操作。下图是该寄存器的布局:
比特 | 名称 | 描述 |
---|---|---|
0 | SYSCALL Enable:IA32_EFER.SCE(R/W) | 在64位模式下启动SYSCALL/SYSRET指令 |
8 | IA-32e Mode Enable:IA32_EFER.LME(R/W) | 启用IA-32e模式 |
10 | IA-32e Mode Active: IA32_EFER.LMA(R) | 标识设置时IA-32e模式处于活动状态 |
11 | Execute Disable Bit Enable: IA32_EFER.NXE(R/W) | 该位为1时,表示可以使用Execution disable功能,给某些页定义为不可执行的页面 |
EFER是MSR中的一员,它的地址是0xC0000080
,它是x64体系的基石。当EFER.LME=1
时,表示要开起IA-32e
模式。可是,此时并不代表已经进入了IA-32e
模式,因为开启IA-32e
模式后必须要开启分页内存管理机制,也就是说,当EFER.LME=1
且CR0.PG=1
时,处理器会将EFER.LMA
置为1,当其成功置为1以后,才表示IA-32e
模式处于激活状态。
处理器在上电开始运行时或复位后是处于实地址模式,CR0控制寄存器的PE标志用来控制处理器处于实地址模式还是保护模式。标志寄存器(EFLAGS)的VM标志用来控制处理器是在虚拟8086模式还是普通保护模式下,EFER寄存器的LME用来启用IA-32e模式
当第8位为1则处于IA-32e
模式
看一个00209b00 00000000
,拆分可得0010 0000 1001 1011
,即L位为1,处于IA-32e
模式下,P=1证明段选择子有效,DPL=0为0环权限,Type大于8为代码段
也可以直接使用dg 10
,即与GDT表的相对偏移
再看00409300 0000000
,拆分可得0100 0000 1001 0011
,Type小于8为数据段,在数据段L位
被废弃,P=1证明段选择子有效,DPL=0为0环权限
然后再看一个特殊的TSS段,这里的TSS段的Base并不是74c93000
而是通过后面的进行拼接得到fffff806 74008bc9
在x86里面TSS段保存了一堆寄存器的值,但是在x64里TSS段不用来任务切换,主要保存一堆rsp备用指针中断门描述符扩展到128位
注意第一个4字节是保留的
中断门标识符同样拓展到了128位,取fffff806
和72425100
进行拼接
然后看一下x64的IDT
注意这里1、2、8好的IST是由特殊分配的
如下所示
几个特殊的地址如下所示
系统调用
只使用一张SSDT 表,x64用户程序通过 syscall
进入内核,x86用户程序在ring3
转入x64模式再进入内核
首先看x64的NtOpenProcess
调用了syscall
再打开一个32位的notepad
首先调用ntdll
的77878870
跳转到全局变量
这里到了777E7000
这个地方,在正常x86的情况下,cs是0023,而33则是为x64,也就是说x86的程序在x64上执行会首先将cs从0023转换为0033,即从x86->x64,但是这里在3环的调试器是不能实现这个转换过程的,而且单步执行到777E7000
这个地方如果想要再往下执行是会失败的
我们去windbg里面看断点看一下情况,这里是通过r15寄存器来进行操作
然后我们在777e7009
这个地方下断点
g
继续执行即可断在jmp
语句的位置
这里可以看一下r15
寄存器可以发现,编译器在开始之前就已经初始化了r15
寄存器这块的内存
然后继续F11往下跟,发现又使用了r14
寄存器
这里就不单步走了,直接找到syscall
的位置,这里可以发现编译器一共使用了r8
、r11
、r13
、r14
、r15
五个寄存器
思路
首先我们说一下数据隐藏的思路,我们知道在64位系统上运行x86程序需要首先通过jmp far
指令将x86程序转换为x64,那么我们就可以通过自己构造硬编码的方式将想要隐藏的内存放到没有使用的64位寄存器上(如r12),需要使用的时候再通过jmp far
指令转换为x64后再去读取寄存器的值即可实现数据隐藏
然后在x86程序里面嵌入x64汇编这种方式也可以有效的进行反调试,一般对我们程序的调试都是通过3环调试器,当3环调试器跟到x86转换到x64的汇编语句时,继续单步执行是会一直循环的,如果想要调试程序就需要使用0环调试器才能够进行调试,有效的进行了反调试
实现
x64部分
首先我们实现读寄存器的代码,我们的思路就是将我们要保存的值写入r12
寄存器,那么读寄存器就是将寄存器的值取出,这里用到mov qword ptr ds:[0], r12
取出
这里看一下硬编码是4C:892425
,这里一般都不使用2425
,如果使用2425
会导致寻址比较麻烦
这里直接使用25
指令,可以看到ds
里面的地址已经变成了7FFED53D11AE
,这里跳转的地址先不管,假设我们已经拿到了寄存器的值
那么这里就要进行x64返回x86的操作,windows并没有提供直接返回x86的指令,如果使用jmp 0023
会报错
那么这里我们就可以选择将跳转地址存到rax
里面,再通过jmp far
指令跳转回x86
总体的汇编指令如下
这里把硬编码抠出来如下所示,这里地址先不管,我们放在后面补充
1 | __asm { |
再就是写入数据的函数,我们将数据存放到r12
寄存器里面,使用mov r12, 0x12345678
然后还是跟上面一样将返回地址压入rax寄存器,通过jmp far
指令返回x86
总体的汇编代码如下
将硬编码抠出来写成go_write
函数
1 | __asm { |
我们在上面已经实现了go_read
和go_write
函数,那么这里我们首先将这两个函数的地址打印出来
printf("go_read: %p\n", go_read);
printf("go_write: %p\n", go_write);
注意这里需要将随机基址关闭,否则每次生成的地址都会不相同
这里得到go_read
的地址为401000
,go_write
的地址为401020
x86部分
我们去到x32dbg,在上面我们已经分析了x86程序在64位系统里面会首先从x86转换成x64再通过syscall
进入0环,那么这里同样的我们需要自己构造汇编语句
这里如果直接使用jmp far 33:0x00401000
会报错,这里是因为函数的地址不对
我们去看一下系统汇编的实现,是通过EA
/3300
这两个硬编码实现
那么这里我们通过ctrl+e修改十六进制文件
即可构造出跳转到go_write
函数地址的语句
将硬编码抠出来如下
1 | __asm { |
构造跳转到go_read
语句同理
将硬编码抠出来如下
1 | __asm { |
这里我们再定义两个数组,far_jmp
数组存放的值即为x64返回x86要跳转的函数地址,secret
数组用来存放从r12
取出来的值
1 | char far_jmp[10] = { 0x78, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00 };//403018 |
far_jmp
的地址为00403018
,secret
的地址为00403388
通过计算可以将go_read
里面的地址填充上去,将r12
寄存器的值存入secret
数组
1 | void __declspec(naked) go_read() |
go_write
函数同样填充403018
这个地址即可
1 | void __declspec(naked) go_write() |
然后这里首先进入x64模式调用go_write
函数,设置一个断点,再调用go_read
读取寄存器里面的值并打印
1 | *(unsigned int*)far_jmp = 0x00401064; |
实现效果
数据隐藏
这里我们首先运行程序
打开ce搜索字符串,这里是没有显示的
这里再继续向下执行可以看到这里返回了x86并将12345678
写入到了内存,所以在ce里面能够看到
反调试
这里首先运行程序,可以看到存放字符串的数组地址0040337C
是空白的
然后这里继续运行,可以看到存放字符的地址已经有了值
那么这里我们如果要调试程序一般会在存放字符串的地址添加一个硬件写入断点
这里当我们通过硬件断点断在了771F2AAC
的ret 4
,这里可以看到是在ZwClose
里面,这里明显是通过异常处理函数来到了771F2AAC
这个位置,虽然这里0x0040337C
这个位置写入了数值,但是这里程序的eip已经不能够继续跟下去了,起到了反调试的效果