Drunkmars's Blog

通过嵌入x64汇编隐藏数据&反调试

字数统计: 2.6k阅读时长: 10 min
2022/04/16

我们知道在x64里面,从3环进入0环会调用syscall,那么如果是32位的程序就需要首先转换为x64模式再通过syscall进入0环,这里就会涉及到一系列64位寄存器的操作,我们通过探究其实现原理来达到隐藏数据和反调试的效果。

基础知识

在支持IA-32e模式的系统上,提供扩展功能启用寄存器(IA32_EFER),该寄存器控制IA-32e模式的激活和其他IA-32e模式的操作。下图是该寄存器的布局:

avatar

比特 名称 描述
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=1CR0.PG=1时,处理器会将EFER.LMA置为1,当其成功置为1以后,才表示IA-32e模式处于激活状态。

处理器在上电开始运行时或复位后是处于实地址模式,CR0控制寄存器的PE标志用来控制处理器处于实地址模式还是保护模式。标志寄存器(EFLAGS)的VM标志用来控制处理器是在虚拟8086模式还是普通保护模式下,EFER寄存器的LME用来启用IA-32e模式

当第8位为1则处于IA-32e模式

image-20220416101850996

看一个00209b00 00000000,拆分可得0010 0000 1001 1011,即L位为1,处于IA-32e模式下,P=1证明段选择子有效,DPL=0为0环权限,Type大于8为代码段

image-20220416105056401

也可以直接使用dg 10,即与GDT表的相对偏移

image-20220416105355272

再看00409300 0000000,拆分可得0100 0000 1001 0011,Type小于8为数据段,在数据段L位被废弃,P=1证明段选择子有效,DPL=0为0环权限

image-20220416105500616

image-20220416110013065

然后再看一个特殊的TSS段,这里的TSS段的Base并不是74c93000

image-20220416160409447

image-20220416160435716

而是通过后面的进行拼接得到fffff806 74008bc9

image-20220416160541121

在x86里面TSS段保存了一堆寄存器的值,但是在x64里TSS段不用来任务切换,主要保存一堆rsp备用指针中断门描述符扩展到128位

注意第一个4字节是保留的

image-20220416160920069

中断门标识符同样拓展到了128位,取fffff80672425100进行拼接

image-20220416161829495

然后看一下x64的IDT

image-20220416162011320

注意这里1、2、8好的IST是由特殊分配的

image-20220416163145576

如下所示

image-20220416162238259

几个特殊的地址如下所示

image-20220416184622252

image-20220416184631559

系统调用

只使用一张SSDT 表,x64用户程序通过 syscall进入内核,x86用户程序在ring3转入x64模式再进入内核

首先看x64的NtOpenProcess

image-20220417104717665

调用了syscall

image-20220417105113060

再打开一个32位的notepad

image-20220417110242775

首先调用ntdll77878870

image-20220417110321933

跳转到全局变量

image-20220417110522063

这里到了777E7000这个地方,在正常x86的情况下,cs是0023,而33则是为x64,也就是说x86的程序在x64上执行会首先将cs从0023转换为0033,即从x86->x64,但是这里在3环的调试器是不能实现这个转换过程的,而且单步执行到777E7000这个地方如果想要再往下执行是会失败的

image-20220417110539028

我们去windbg里面看断点看一下情况,这里是通过r15寄存器来进行操作

image-20220417111122574

然后我们在777e7009这个地方下断点

image-20220417111238787

g继续执行即可断在jmp语句的位置

image-20220417111332918

这里可以看一下r15寄存器可以发现,编译器在开始之前就已经初始化了r15寄存器这块的内存

image-20220417111524864

然后继续F11往下跟,发现又使用了r14寄存器

image-20220417111353567

这里就不单步走了,直接找到syscall的位置,这里可以发现编译器一共使用了r8r11r13r14r15五个寄存器

image-20220417111820996

思路

首先我们说一下数据隐藏的思路,我们知道在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取出

image-20220417171143684

这里看一下硬编码是4C:892425,这里一般都不使用2425,如果使用2425会导致寻址比较麻烦

image-20220417171157693

image-20220417171216564

这里直接使用25指令,可以看到ds里面的地址已经变成了7FFED53D11AE,这里跳转的地址先不管,假设我们已经拿到了寄存器的值

image-20220417171250573

那么这里就要进行x64返回x86的操作,windows并没有提供直接返回x86的指令,如果使用jmp 0023会报错

image-20220417191548783

那么这里我们就可以选择将跳转地址存到rax里面,再通过jmp far指令跳转回x86

image-20220417171447568

image-20220417171533505

总体的汇编指令如下

image-20220417172649913

这里把硬编码抠出来如下所示,这里地址先不管,我们放在后面补充

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__asm {
__emit 0x4c //mov qword ptr ds:[0], r12
__emit 0x89
__emit 0x25
__emit 0x00
__emit 0x00
__emit 0x00
__emit 0x00

__emit 0xb8 // mov eax, 0
__emit 0x00
__emit 0x00
__emit 0x00
__emit 0x00

__emit 0x48 //jmp far tword ptr ds:[rax]
__emit 0xff
__emit 0x28
}

再就是写入数据的函数,我们将数据存放到r12寄存器里面,使用mov r12, 0x12345678

image-20220417172719482

image-20220417172831624

然后还是跟上面一样将返回地址压入rax寄存器,通过jmp far指令返回x86

image-20220417172854758

image-20220417172918414

总体的汇编代码如下

image-20220417173003616

将硬编码抠出来写成go_write函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__asm {
//__emit 0xcc
__emit 0x49 //mov r12, 0x12345678
__emit 0xc7
__emit 0xc4
__emit 0x78
__emit 0x56
__emit 0x34
__emit 0x12

__emit 0xb8 // mov eax, 403018h
__emit 0x18
__emit 0x30
__emit 0x40
__emit 0x00

__emit 0x48 //jmp far tword ptr ds:[rax]
__emit 0xff
__emit 0x28

}

我们在上面已经实现了go_readgo_write函数,那么这里我们首先将这两个函数的地址打印出来

printf("go_read: %p\n", go_read);
printf("go_write: %p\n", go_write);

注意这里需要将随机基址关闭,否则每次生成的地址都会不相同

image-20220417185839002

这里得到go_read的地址为401000go_write的地址为401020

image-20220417185148937

x86部分

我们去到x32dbg,在上面我们已经分析了x86程序在64位系统里面会首先从x86转换成x64再通过syscall进入0环,那么这里同样的我们需要自己构造汇编语句

这里如果直接使用jmp far 33:0x00401000会报错,这里是因为函数的地址不对

image-20220417190120093

我们去看一下系统汇编的实现,是通过EA/3300这两个硬编码实现

image-20220417190135929

那么这里我们通过ctrl+e修改十六进制文件

image-20220417190152890

即可构造出跳转到go_write函数地址的语句

image-20220417190521283

将硬编码抠出来如下

1
2
3
4
5
6
7
8
9
__asm {
__emit 0xea
__emit 0x20
__emit 0x10
__emit 0x40
__emit 0x00
__emit 0x33
__emit 0x00
}

构造跳转到go_read语句同理

image-20220417190551281

将硬编码抠出来如下

1
2
3
4
5
6
7
8
9
__asm {
__emit 0xea
__emit 0x00
__emit 0x10
__emit 0x40
__emit 0x00
__emit 0x33
__emit 0x00
}

这里我们再定义两个数组,far_jmp数组存放的值即为x64返回x86要跳转的函数地址,secret数组用来存放从r12取出来的值

1
2
char far_jmp[10] = { 0x78, 0x10, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x00 };//403018
char secret[8] = { 0 }; //403388

far_jmp的地址为00403018secret的地址为00403388

image-20220417193910062

通过计算可以将go_read里面的地址填充上去,将r12寄存器的值存入secret数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __declspec(naked) go_read() 
{
__asm {
__emit 0x4c //mov qword ptr ds:[0x403388], r12
__emit 0x89
__emit 0x25
__emit 0x7A
__emit 0x23
__emit 0x00
__emit 0x00

__emit 0xb8 // mov eax, 403018h
__emit 0x18
__emit 0x30
__emit 0x40
__emit 0x00

__emit 0x48 //jmp far tword ptr ds:[rax]
__emit 0xff
__emit 0x28

}
}

go_write函数同样填充403018这个地址即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void __declspec(naked) go_write()
{
__asm {
//__emit 0xcc
__emit 0x49 //mov r12, 0x12345678
__emit 0xc7
__emit 0xc4
__emit 0x78
__emit 0x56
__emit 0x34
__emit 0x12

__emit 0xb8 // mov eax, 403018h
__emit 0x18
__emit 0x30
__emit 0x40
__emit 0x00

__emit 0x48 //jmp far tword ptr ds:[rax]
__emit 0xff
__emit 0x28

}
}

然后这里首先进入x64模式调用go_write函数,设置一个断点,再调用go_read读取寄存器里面的值并打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	*(unsigned int*)far_jmp = 0x00401064;
__asm {
__emit 0xea
__emit 0x20
__emit 0x10
__emit 0x40
__emit 0x00
__emit 0x33
__emit 0x00
}
L1://00401064
printf("write data ok\n");
system("pause");
*(unsigned int*)far_jmp = 0x0040108c;
__asm {
__emit 0xea
__emit 0x00
__emit 0x10
__emit 0x40
__emit 0x00
__emit 0x33
__emit 0x00
}
L2: //0040108c
printf("back to x86\n");
printf("%p\n", *(int*)secret);
system("pause");

实现效果

数据隐藏

这里我们首先运行程序

image-20220417204508726

打开ce搜索字符串,这里是没有显示的

image-20220417204736641

这里再继续向下执行可以看到这里返回了x86并将12345678写入到了内存,所以在ce里面能够看到

image-20220417204821422

反调试

这里首先运行程序,可以看到存放字符串的数组地址0040337C是空白的

image-20220417205853777

然后这里继续运行,可以看到存放字符的地址已经有了值

image-20220417205905916

那么这里我们如果要调试程序一般会在存放字符串的地址添加一个硬件写入断点

image-20220417210113692

这里当我们通过硬件断点断在了771F2AACret 4,这里可以看到是在ZwClose里面,这里明显是通过异常处理函数来到了771F2AAC这个位置,虽然这里0x0040337C这个位置写入了数值,但是这里程序的eip已经不能够继续跟下去了,起到了反调试的效果

image-20220417210229225

CATALOG
  1. 1. 基础知识
  2. 2. 系统调用
  3. 3. 思路
  4. 4. 实现
    1. 4.1. x64部分
    2. 4.2. x86部分
  5. 5. 实现效果
    1. 5.1. 数据隐藏
    2. 5.2. 反调试