gargoyle ,一个逃过内存扫描的技术
2017年 3 月 4 日
gargoyle 是一项将一个程序的所有可执行代码都隐藏在不可执行的内存块中的技术。在一些程序员定义区间内, gargoyle 会活跃起来 ——结合一些ROP 欺骗——把自己标记为可执行并发挥作用。
在扫描内存来寻找异常时,通常会扫描可执行的内存块。 gargoyle 可以在 Windows上实现将数据隐藏在不可执行的内存块中。
1. gargoyle 产生一些任意代码后,设置一些尾递归。
2. VirtualProtectEx函数标记 gargoyle 为不可执行并返回到 WaitForSingleObjectEx函数,该函数在等待我们发送 Windows 计时器给它。
3. 计时器的结束程序是一个 ROP配置, pop * ; pop esp ; ret 。它会把堆栈指针移到一个我们可以仔细地手动控制的堆栈中。
4. 我们特殊的堆栈让 ret调用 VirtualProtectEx 函数,使我们可以读 / 写 / 执行。
5. VirtualProtectEx函数返回到 gargoyle ,继续下一个循环。
此处只展示了该技术在 32位 Windows 系统上的应用。下面,我们来挖掘该技术实现的所有细节。
动态内存分析
动态的分析内存确实是一项代价很高的操作 ——如果你在使用Windows Defender ,可能已经面临这个问题了( Google 搜索 “Antimalware Service Executable ” )(译者注:这是 Windows Defender的一个进程,在后台执行扫描时会出现 CPU 和内存使用率居高不下的情况)。因为程序必须在可执行内存块中常驻,所以一种减少计算负担的技术就是只分析可执行代码页。在许多进程中,需要分析的内存数量会减少一个数量级。
gargoyle 显示这是一个有风险的行为。通过使用 Windows 异步过程调用,只读 / 写内存可以被调用为可执行内存来执行一些任务。一旦它完成了任务,就会恢复到读 / 写内存,直到计时器到期再重复这个循环。
当然, Windows API中不存在 InvokeNonExecutableMemoryOnTimerEx 函数。重复这个循环需要其他的一些工作......
Windows异步过程调用( APC , Windows Asynchronous Procedure )
异步编程允许一些任务推迟执行,可能是在一个单独线程的执行上下文中。每个线程都有自己的 APC队列,当一个线程进入 alertable 状态时, Windows 会从线程的 APC 队列调度工作到等待线程中。
有很多方法排队 APC:
· ReadFileEx
· SetWaitableTimer
· SetWaitableTimerEx
· WriteFileEx
有很多方法让线程进入 alertable状态:
· SleepEx
· SignalObjectAndWait
· MsgWaitForMultipleObjectsEx
· WaitForMultipleObjectsEx
· WaitForSingleObjectEx
我们采用一个组合,用 CreateWaitableTimer创建一个计时器,然后用 SetWaitableTimer 排队 APC:
这些默认的安全属性就很好,我们不用手动重置,也不用命名计时器。所以 CreateWaitableTimer的所有参数都默认为 0 或 nullptr 。函数返回一个句柄给我们新定义的计时器。接着,我们需要排队 APC :
第一个参数是我们从 CreateWaitableTimer得到的句柄。 pDueTime 参数是一个指向 LARGE_INTEGER 的指针,指定第一个计时器到期的时间。例如,我们将它简单设置为 0 (马上到期)。 lPeriod 参数定义了毫秒级的到期间隔。这决定了时钟频率,即哪个 gargoyle 会被调用。
下一个参数 pfnCompletionRoutine是我们重点考虑的部分。这表示 Windows 调用等待线程的地址。当 APC 在被派遣时,可执行内存块中是没有 gargoyle 代码的。如果我们想在 gargoyle 中找到 pfnCompletionRoutine ,我们需要以一个数据执行保护( DEP )冲突来解决。
相反,我们使用一种特殊的返回导向编程( ROP)二进制指令代码块( gadget )(译者注: gadget 是从函数开始到 ret 返回指令之间的代码块, ROP 技术按照一定顺序拼接这些代码,即通过代码复用,来执行想要的操作),通过 lpArgToCompletionRoutine ,将可执行线程的堆栈定位到 SetWaitableTimer 的第二个参数的地址指针。当 ROP 代码块中执行 返回指令 r et S ,预先准备好的特殊的返回栈会调用 VirtualProtectEx 函数来标记 gargoyle 为可执行。
最后一个参数表示是否在计时器到期时唤醒一个休眠的计算机。我们在这里将其设置为 false。
Windows数据执行保护(DEP)和 VirtualProtectEx 函数
最后使用的是 VirtualProtectEx函数,用来标记内存中的各种安全属性:
我们会两次调用 VirtualProtectEx: gargoyle 执行完之后(我们将线程标记为 alertable 状态之前)和 gargoyle 开始执行时(线程派遣 APC 完成之后)。更多细节请看信息图。
在本文的证明中, gargoyle、 trampoline 、 ROP 代码块和读 / 写内存都是同一个进程的,所以第一个参数 hProcess 相当于 GetCurrentProcess 。参数 lpAddress等于 gargoyle 的地址,参数 dwSize 等于 gargoyle 可执行内存块的大小。我们将想要的内存保护属性赋给参数 flNewProtect 。我们不用关心原先的保护属性是什么,但是 lpflOldProtect 不是一个可选属性,所以我们把它指向一些预先留出的空内存。
唯一取决于上下文的参数是 flNewProtect。当 gargoyle 转为休眠状态时,我们将其标记为 PAGE_READWRITE 或 0x04 。在 gargoyle 转为可执行之前,我们将其标记为 PAGE_EXECUTE_READ 或 0x20 。
堆栈跳板
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课