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


0x00 前言

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

0x01 基础知识

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

835440_PKEJUDAQP2525N9.png

比特 名称 描述
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.png

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

image-20220416105056401.png

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

image-20220416105355272.png

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

image-20220416105500616.png

image-20220416110013065.png

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

image-20220416160409447.png

image-20220416160435716.png

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

image-20220416160541121.png

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

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

image-20220416160920069.png

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

image-20220416161829495.png

然后看一下x64的IDT

image-20220416162011320.png

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

image-20220416163145576.png

如下所示

image-20220416162238259.png

0x02 系统调用

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

首先看x64的NtOpenProcess

image-20220417104717665.png

调用了syscall

image-20220417105113060.png

再打开一个32位的notepad

image-20220417110242775.png

首先调用ntdll77878870

image-20220417110321933.png

跳转到全局变量

image-20220417110522063.png

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

image-20220417110539028.png

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

image-20220417111122574.png

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

image-20220417111238787.png

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

image-20220417111332918.png

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

image-20220417111524864.png

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

image-20220417111353567.png

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

image-20220417111820996.png

0x03 思路

首先我们说一下数据隐藏的思路,我们知道在64位系统上运行x86程序需要首先通过jmp far指令将x86程序转换为x64,那么我们就可以通过自己构造硬编码的方式将想要隐藏的内存放到没有使用的64位寄存器上(如r12),需要使用的时候再通过jmp far指令转换为x64后再去读取寄存器的值即可实现数据隐藏

然后在x86程序里面嵌入x64汇编这种方式也可以有效的进行反调试,一般对我们程序的调试都是通过3环调试器,当3环调试器跟到x86转换到x64的汇编语句时,继续单步执行是会一直循环的,如果想要调试程序就需要使用0环调试器才能够进行调试,有效的进行了反调试

0x04 实现

x64部分

首先我们实现读寄存器的代码,我们的思路就是将我们要保存的值写入r12寄存器,那么读寄存器就是将寄存器的值取出,这里用到mov qword ptr ds:[0], r12取出

image-20220417171143684.png

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

image-20220417171157693.png

image-20220417171216564.png

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

image-20220417171250573.png

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

image-20220417191548783.png

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

image-20220417171447568.png

image-20220417171533505.png

总体的汇编指令如下

image-20220417172649913.png

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

    __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.png

image-20220417172831624.png

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

image-20220417172854758.png

image-20220417172918414.png

总体的汇编代码如下

image-20220417173003616.png

将硬编码抠出来写成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

    }

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

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

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

image-20220417185839002.png

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

image-20220417185148937.png

x86部分

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

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

image-20220417190120093.png

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

image-20220417190135929.png

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

image-20220417190152890.png

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

image-20220417190521283.png

将硬编码抠出来如下

    __asm {
        __emit 0xea
        __emit 0x20
        __emit 0x10
        __emit 0x40
        __emit 0x00
        __emit 0x33
        __emit 0x00
    }

构造跳转到go_read语句同理

image-20220417190551281.png

将硬编码抠出来如下

    __asm {
        __emit 0xea
        __emit 0x00
        __emit 0x10
        __emit 0x40
        __emit 0x00
        __emit 0x33
        __emit 0x00
    }

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

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.png

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

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这个地址即可

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读取寄存器里面的值并打印

    *(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");

0x05 实现效果

数据隐藏

这里我们首先运行程序

image-20220417204508726.png

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

image-20220417204736641.png

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

image-20220417204821422.png

反调试

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

image-20220417205853777.png

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

image-20220417205905916.png

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

image-20220417210113692.png

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

image-20220417210229225.png

评论

Drunkmars

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

Android 文章:89 篇
IoT安全 文章:29 篇
逻辑漏洞 文章:15 篇
memcache安全 文章:1 篇
企业安全 文章:40 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录