导语:许多创新系统都能确保软件安全。但是许多应用程序仍然容易受到Hook(钩子)和ROP(返回式编程)攻击。虽然不可能摆脱应用程序中的所有漏洞,但开发人员应该在编程阶段考虑可执行空间保护。本文介绍了一些检测Hook和ROP攻击的有效方法,我

一、简介

有时候,我们的应用程序会遭受网络犯罪分子使用Hook或ROP攻击,所以必须找到有效的方法来保护它们。在本文中,我描述了一个案例:当一个局外人(第三方应用程序,恶意软件或逆向工程师)在我们的应用程序中拦截系统调用以更改其行为或监控其性能时,如何检测。

我还描述了针对以下攻击类型的两种保护方法:

· Hook需要将第三方代码注入到目标应用程序中以更改内存页面的权限并重写源代码

· ROP攻击不需要任何代码注入

您可以使用这些方法来保护自己的应用程序,或在设计主动网络防御系统时采用这些方法来防止刚刚提到的各种攻击,甚至是0 day攻击。

二、Hooks

Hook用于多种目的,但网络犯罪分子通常使用它们来改变应用程序或操作系统的行为并监视其性能。有各种各样的钩子,但本文只考虑两种类型:

1. 修补导入地址表(IAT)

2. Splicing

为了hook一个函数,攻击者需要在内存加载的应用程序中更改代码。为了修补,他们必须在IAT中重写地址。对于Splicing,他们需要更改JMP指令的地址为函数开始的地址。结果,应用程序代码将被改变。

因此,为了检测代码更改,可以用FunctionForChecking(%necessary API%)替换所有函数调用。可以应用各种方法来验证%necessary API%是否真的是所需要的或者是否已被第三方替换。可以这样做:

· 检查函数的第一个字节,以获取识别异常行为的控制传输指令。

· 检查函数的所有校验和。更改的校验和表示指令已被替换。

· 确保用于传输控制的地址位于函数所在的加载模块中,不在第三方加载的模块中。

不重写源代码就很难做到这一点。此外,对于Hook检测,可以将分析过程中加载的模块与原始模块进行比较。这里有一个example。但是,这种检测并不主动,因为它只能检测已安装的钩子。

为了执行这样的Hook,第三方代码需要对内存进行写操作。但是,要做到这一点,就要获得写入内存页面的权限。只有在恶意软件代码调用VirtualProtect函数后才能获得这些权限。换句话说,为了拦截在我们的应用程序中对WinAPI的调用,第三方代码需要使用WinAPI本身。

因此,我们也可以拦截VirtualProtect并检查它。如果VirtualProtect由一个已知模块调用,那么我们称之为原始VirtualProtect。我们可以通过在应用程序启动时保存所有模块的开始和结束地址并检查调用模块是否在我们的模块列表中来实现。但是,定义模块是否合法加载是相当困难的。此方法仅能防止使用远程线程进行DLL注入。但是注入也可以通过使用库来写寄存器以及AppInit_DLLsKnownDlls 。

出于攻击检测的目的,假设在将模块加载到进程的地址空间后,不更改内存页面的权限。

三、保护措施

代码是通过实例化一个DLL来注入的,这需要创建一个远程线程。在此过程中,DLL尝试挂钩MessageBox。在我们的例子中,使用mhook来安装钩子。可以在这篇文章中了解更多关于mhook的信息。但是,当我尝试使用IAT注入钩子时,此方法也起作用,因为它也需要调用VirtualProtect。安装钩子后,我们的应用程序会显示一条消息,而不是如下MessageBox写的消息。

std::cout << "after pressing ENTER MessageBox will be shown\n";
getchar();
MessageBox(NULL, L"text", L"caption", 0);

在实施针对挂钩的保护后,应用程序中的代码如下所示:

HookDef hookDef;
if (hookDef.Init())ret
{ /* three lines above */ }

Init拦截和VirtualProtect和VirtualProtectEx。当使用mhook钩住了所提到的函数时,就会陷入无限递归之中,因为mhook本身使用VirtualProtectEx进行挂钩。因此,必须添加一个检查,如果挂钩函数是VirtualProtectEx,那么使用VirtualProtect来应用mhook。

之后,钩子通过调用CaptureStackBackTrace来定义返回地址,以便了解从哪个模块开始调用。如果它检测到传输给VirtualProtect的地址属于已经加载到内存的模块,那么有人正试图hook我们的应用程序。

if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
    reinterpret_cast<LPCTSTR>(vpAddr),
    &hmodule))
{
    InformAboutHook(callerAddress);
}

最后,得到结果:

四、ROP攻击

我已经在这篇文章中解释了如何执行ROP攻击。在此情形下,我使用了一个准备好的漏洞,可以从msvcp140.dll的gadget链中

调用VirtualProtect。可以在这篇文章中了解如何执行此漏洞利用。为了执行它,我更改了gadget地址,因为加载msvcp140.dll的地址与最初的地址不同:

在上图中,与原始ROP漏洞利用相比,gadget地址中的更改标记为红色。地址是用小写字母编写的,每个地址都是4个字节。因此,可以看到我偏移了小工具20个gadget。

让我们想象一下,在应用程序中,堆栈被上面的ROP链重写。执行漏洞利用程序首先执行指令链,每个指令链都以ret指令结束,准备调用VirtualProtect。跳到VirtualProtect代码是在最后的ret指令中执行的。

五、防止ROP攻击

开发人员如何检测应用程序中的ROP攻击?很明显,攻击过程需要操纵一个堆栈。因此,检测ROP攻击的多种选择方案都是基于堆栈已被改变这个假设。让我们来看看:

Shadow Stack是创建第二个堆栈的方法,其中返回地址与原始堆栈重复,并且在返回函数之前从两个调用堆栈中加载返回地址并对它们进行比较:如果记录不同,则其中一个已被重写。有趣的是,这种方法已经在处理器级别上实现 。

更改调用时使用

push %table_index%
jmp %function%

返回时使用

pop ebx
jmp table[%table_index%]

但是,这种方法不能检测到攻击;它能防止攻击并阻止使其发生。

G-Free方法增加了序言prologues和结语epilogues,而不是改变指令。如果函数序言中的加密返回地址,在结语中对其进行解密后结果不匹配,则ret指令将不会执行或将不能正确执行。

stack canaries方法将已知值放置在堆栈上的缓冲区和控制数据之间。这些值在返回之前被检查。如果返回地址已更改,那么这些值也已更改。这是StackGuard中一个堆栈实现的例子。

无论如何,这些解决方案都需要重新编译二进制文件。在本文中,我想考虑另一种方法:最后分支记录(LBR)。

六、最后分支记录LBR

使用gadgets,攻击者可以调用任何系统函数。但是,单个函数调用通常是不够的;有必要用更多的逻辑来执行某些事情。然而,创建具有更复杂逻辑的ROP链需要更多时间,并受可用模块以及激活的ASLR(地址空间布局随机化)的限制。这就是为什么ROP经常被用作绕过一些保护措施然后执行恶意代码。例如,你可以创建一个ROP链来为shellcode分配内存,调用VirtualProtect,并为这个shell代码传递控制权。

因此,为了检测ROP攻击,可以拦截对系统函数的调用,并尝试检查控制流程如何转移到此:通过调用指令(正常行为),还是ret指令(异常行为)。

但如何获得跳转地址?现代处理器嵌入了称为最后分支记录的机制。激活时,处理器将跳转地址记录到MSR(模型特定寄存器)中。记录的分支数量(CPU从中跳转到)取决于处理器的类型。例如,Intel Core i5-6200U记录31个分支。无论如何,仍然可以拥有最后一个分支的索引。

这里可以找到运行LBR的MSR地址并读取最后一个分支的索引。该文章中提到的硬编码值取自英特尔的手册(第1381页)。此外,英特尔处理器已经为分支存储应用了改进的功能 ,本文中未涉及此点。

七、LBR实现

必须使用内核模块才能使用MSR。我还尝试通过安装Dr7寄存器字节创建调试过程来从用户模式激活MSR,但此方法非常不稳定且受到限制。

我使用__writemsr 和__readmsrfunctions作为MSR输入输出。通过将第一个字节安装到地址0x1d9来激活LBR。调用DeviceIoControl来进一步与用户模式下的驱动程序进行通信:

IOCTL_ROPPROT_FN fn { (unsigned long long)addr };
IOCTL_ROPPROT_FN fromFnCalled { 0 };
DeviceIoControl(hDriverDevice, IOCTL_ROPPROT_CHECK_LBR, &fn, sizeof(fn), &fromFnCalled, sizeof(fromFnCalled), &dwReturn, NULL);

执行跳转的指令地址将纪录在FnCalled的返回值。

指令搜索的简化版本如下所示:

do
{
      toBr   = __readmsr(MSR_LASTBRANCH_0_TO_IP + lastLbrIdx);
      lastBr = __readmsr(MSR_LASTBRANCH_0_FROM_IP + lastLbrIdx);
      if (toBr == checkedAddr)
      {          
            *fromAddr = lastBr;
            return ROPPROT_SUCCESS;
      }
} while (lastLbrIdx--);

之后,调用者将比较返回地址的第一个字节与ret指令(在挂钩函数中)的操作码。

unsigned char byte = *(unsigned char*)fromFnCalled.addr;
if (IsRet(byte))
{
    MessageBox(NULL, L"ROP attack detected!", L"Alert", 0);
    Return false;
}
// call the original function

以下是它如何查找用户:

在检测到挂钩的VirtualProtect后,系统通过使用带有IOCTL_ROPPROT_CHECK_LBR的参数来调用DeviceIoControl来呼叫驱动程序。可以在DebugView窗口中看到驱动程序消息。它从最后记录的分支开始经过分支,并搜索挂钩的VirtualProtect(0x402a60)传输给它的地址。当它找到地址时,它会定义跳转的执行位置:0x77696930 (VirtualProtectStub)。由于它只是一个jmp指令,因此它会查看跳转的VirtualProtectStub——0x6b7a22fc的执行位置并返回该地址。我们的应用程序检查此地址的指令,如果是一个ret指令,就会发送一个警报。

在上面,可以看到最后一个gadget,它将esp地址更改为eax中VirtualProtectStub的地址。之后,ret指令重定向到VirtualProtectStub。右边是关于ROP链中的这个gadget的记录,它在溢出之后自动进入堆栈。

八、运行驱动的注意事项

如果想运行驱动程序,需要考虑以下几个方面。对于驱动程序开发,我使用了Windows Driver Kit。此外,我使用Visual Studio 2015进行驱动程序编译。

虽然驱动程序很可能会进行测试签名,但需要允许从命令行运行此类驱动程序,然后重启计算机:

bcdedit /set testsigning on

另外,需要预先禁用BIOS中的安全启动。此外,需要在寄存器中添加a Debug Print Filter记录,才能在DebugView中查看驱动程序的输出。

九、总结

文中所描述的方法并不打算成为最佳的Hook和ROP检测解决方案。他们只是可能的方法。例如,如果最后一个gadget选择使用jmp指令而不是ret,那么保护措施就不会检测到攻击。在这种情况下,需要对记录的分支执行更详细的分析,或者考虑重新记录的频率。

而且,我的防御只是基于使用VirtualProtect,但还需要保护一系列关键函数,例如LoadLibrary,这是ROP漏洞利用可能用于动态加载的关键函数。所描述的ROP攻击检测方法的思想来自于包含在Microsoft EMET中的kBouncer

至于Hook,需要考虑到所描述的保护措施可能会干扰某些使用VirtualProtect来保护其性能的保护器的工作。至于Microsoft EMET,它还在其Memory Protection功能中使用VirtualProtect hook。

从这里可以下载Hook和ROP攻击检测的例子:

hookdef_src

ropprot_src

源链接

Hacking more

...