65.9K
CodeProject 正在变化。 阅读更多。
Home

防篡改和自愈代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (42投票s)

2007年5月28日

CPOL

25分钟阅读

viewsIcon

218775

downloadIcon

8121

使用哈希和 Crypto++ 动态检测代码更改并在内存中修复可执行文件。

引言

本文是动态.TEXT节镜像验证的补充。本文将演示检测硬件故障或未经授权的补丁;回溯修补可执行文件以嵌入.text节的预期哈希值;并演示修复恶意代码(例如,未经授权的二进制补丁程序)影响的过程。

本文提出的思想无论可执行文件是在磁盘上还是在内存中打补丁,都同样有效。然而,自修复发生在内存中。在逆向工程和打补丁的背景下,Kris Kaspersky(与卡巴斯基实验室的同名创始人尤金·卡巴斯基无关,他曾是克格勃密码学家)在他的书Hacker Disassembling Uncovered中将此称为在线打补丁。

示例将使用一个平面GZip压缩文件来存储未经更改的.text节的副本。正如Garth Lancaster所指出的,读者应该探索使用可执行资源来嵌入哈希或存档的.text节。Adrian Cooper的添加和提取二进制资源中可以找到一个例子。

此外,本文将扩展Shaun Wilde的文章使用Crypto++库进行压缩和解压缩。最后,应该回顾Jessie Ezell的两行简单的自修复应用程序代码,以了解一个[大致]类似的MSI解决方案。

将讨论以下主题

  • 下载次数
  • 工具
  • 编译和集成Crypto++
  • SHA哈希函数
  • 自完整性检查
  • 自修复软件
  • 编译器回溯修补
  • 无错哈希转录
  • 哈希变量放置和初始化
  • 轮询与通知
  • 七个代码示例
  • Windows Vista兼容性

本文中提供的代码已在Windows 2000、Windows XP、Windows Server 2003和Windows Vista上成功测试。非常感谢Tim Deveaux和Joergen Sigvardsson在Windows Vista上测试代码方面的协助。请注意,标准用户账户在Windows Vista下成功执行了演示代码。有关讨论,请参阅本文末尾的“Windows Vista兼容性”部分。

下载次数

本文提供8个下载。大致而言,介绍了以下概念:

  • 自修复1 - 基线(遍历EXE头)
  • 自修复2 - 哈希.text节
  • 自修复3 - 自修复2带回溯修补
  • 自修复4 - 提取并压缩.text节
  • 自修复5 - 自修复4带回溯修补
  • 自修复6 - 存档.text节恢复,回溯修补
  • 自修复7 - 完整演示(篡改和修复)
  • RelExe - 自修复7的发布构建可执行文件

工具

本文所需的工具与动态TEXT节镜像验证中的相同,尽管本文较少关注之前演示的正确性。然而,使用Crypto++库中的GZip压缩例程和Gunzip解压缩例程确实提出了挑战。

作者的WinZip 11.0(版本7313)副本声称创建的存档无效(因为存档没有.gz扩展名)。WinZip似乎只依赖于文件名扩展名。作为替代,安装了WinRAR的用户应该会发现它是一个合适的替代品,因为它会检查文件的头部,所以工作正常。

编译和集成Crypto++

Screenshot - CryptoLogo.png

Crypto++可以从Wei Dai的Crypto++页面下载。有关编译和集成问题,请参阅将Crypto++编译和集成到Microsoft Visual C++环境中。本文基于前面提到的文章中提出的基本假设。

对于对其他C++加密库感兴趣的人,请参阅Peter Gutmann的Cryptlib或Victor Shoup的NTL

SHA哈希函数

本文将使用SHA-224。SHA-224属于SHA-2哈希家族,目前由NIST推荐。SHA-2哈希家族产生的摘要至少为160位,这是目前的最佳实践。在SHA-224的情况下,摘要长度为224位(28字节)。

SHA-1哈希家族不再被美国政府批准使用。请参阅NIST的密码工具包,安全哈希

对于那些希望使用平面C文件和非NIST推荐的人,ISO认可RIPEMDWHIRLPOOL(除了SHA)。RIPEMD和WHIRLPOOL都在Crypto++中实现。

自完整性检查

计算机病毒过去曾采用完整性检查,包括使用汉明码来攻击断点和纠正错误。例如,2000年引入的Yankee Doodle病毒家族。自完整性检查也一直是学术界研究的主题。例如,参见J. Giffin、M. Christodorescu和L. Kruger的Strengthening Software Self-Checksumming via Self-Modifying Code.

微软为.NET程序集提供了一种数字签名方案,称为强命名程序集。然而,正如破解.NET程序集所演示的那样,该系统很容易被颠覆。自修复与程序交织在一起,而不是作为程序周围的外壳(强命名程序集)。通过将完整性检查集成到可执行文件中,希望该系统更难被移除。

要了解计算机病毒用于延长寿命的其他技术,请参阅基于病毒生存技术的保护方案

自修复软件

有人建议将本文命名为“防篡改和自修复代码”。然而,作者认为“自修复代码”有点乏味和脱节——MSI安装程序进行修复。这段代码与程序员的工作结合得更紧密,因此使用了隐喻的“自愈代码”来体现这个过程。

媒体炒作

有时媒体对“自愈软件”这个话题大肆宣传,这会让人相信这个领域已被彻底研究(并申请了专利)。一旦调查,媒体所称的“自愈软件”似乎有些名不副实。

例如,通过Google搜索“自愈软件”发现以下新闻稿:自愈软件获得IBM推动。人们会期望看到一篇描述可以在航天飞机上飞行的软件,其程序代码中因辐射而发生位翻转,然后软件能自行修复的文章。

事实并非如此。IBM文章讨论的是Tivoli Monitoring软件的功能(在作者看来,这是一个非常好的产品)。与标题相反,IBM在文章中的声明是:

...Tivoli Monitoring 6.1 监控并修复电子邮件等在线应用程序的服务器或数据库中与IT服务相关的问题

这篇CodeProject文章的重点是软件完整性和自修复,而Compuworld文章中提到的“IT服务相关问题”讨论的是自动诊断和纠正诸如电子邮件服务器与防火墙之间的问题。

专利问题

Brooke Stephens博士确实发现了一项名为专利6530036,自愈计算机系统存储。然而,由于美国专利法的特点,专利6530036并不严格适用于这篇CodeProject文章。该专利的持有者在检测到异常(通过代理发生异常检测)时会重新启动其存储系统。本文描述的系统则是在运行时恢复,并且不使用代理。

自愈系统研讨会

2002年,首届自愈系统研讨会在南卡罗来纳州查尔斯顿举行。以下两篇论文备受关注。然而,这两个系统都没有将系统主动用作程序代码(每个都使用“外部代理”)。这两篇文章都可以在本文中下载。

读者应记住,作者既不是律师也不是程序员——他是一名对编程和密码学充满热情的网络工程师和网络架构师。编译器的工作原理和注意事项,结合本文中介绍的“青霉素代码”,是密码学的一个有趣应用,也带来了有趣的阅读体验。

回溯修补

最严格的意义上,回溯修补是编译器在编译阶段执行的操作。本文将借用这个术语,因为本文的努力与编译器对该术语的使用密切相关。

考虑以下计算奇偶校验的代码片段

if( 0 == a % 2 )
{
    p = 0;
}
else
{
    p = 1;
}

在第一次扫描时,编译器会遇到if( 0 == a % 2 )并生成进行比较的代码。接下来,遇到p = 0p = 1的赋值。这将生成一个比较指令,然后进入第一个赋值,或者一个跳转指令(跳过第一个赋值)并执行第二个赋值。在这个步骤中需要注意的是,“跳多远”是未知的,因为完整的if/else语句尚未评估。上面代码的反汇编如下所示(请注意,与本讨论无关的代码——模数归约——已被遮蔽)。

Screenshot - SelfHealing16.png

在第二次扫描时,p = 0p = 1语句的代码已经生成(即,发出的操作码大小现在已知),因此0x411A16处的跳转操作码可以用一个位移进行修补(更准确地说,操作数的立即值现在可以写入)。这被称为回溯修补。

无错哈希转录

这些示例将要求读者经常将计算出的哈希值复制到预期哈希值中。为此,提供以下提示。首先,打开Windows NT解释器的属性。

Screenshot - SelfHealing29.png

接下来,在 Windows NT 解释器中启用快速编辑模式。

Screenshot - SelfHealing30.png

启用快速编辑模式后,现在可以:

  1. 将插入符置于哈希值的第一个字符处
  2. 按住鼠标左键
  3. 高亮哈希文本
  4. 释放鼠标左键
  5. 按ENTER键复制到剪贴板

Screenshot - SelfHealing31.png

哈希变量放置和初始化

尽管哈希变量放置和初始化的问题直到示例二和三才会出现,但现在将对其进行讨论。与变量放置和初始化相关的两个重要注意事项。

已初始化的全局哈希变量

第一个注意事项,考虑以下程序片段:

BYTE cbExpectectedImageHash[ CryptoPP::SHA224::DIGESTSIZE ];

请注意,BYTE数组cbExpectedImageHash已声明但未初始化。此分配将存在于.bss节(未初始化数据节)中。第一次运行将产生以下结果。

Screenshot - SelfHealing04.png

在调试构建中,这种运行是预期的,编译器已代表程序员将BYTE数组初始化为预期值。接下来,人们将获取计算出的图像哈希(09165E0392F4028240D0AEEA30B6CAF494CC929089757082347119ED),并用它来初始化cbExpectedImageHash,如下所示:

BYTE cbExpectectedImageHash[ CryptoPP::SHA224::DIGESTSIZE ]; =
    { 0x09, 0x16, ..., 0x19, 0xED };

上述操作的效果是微妙的:变量cbExpectedImageHash从未初始化数据节(.bss节)移动到初始化数据节(.data节)。

在此期间,编译器发出了不同的代码:尽管cbExpectedImageHash仍将存在于DATA段(现在是初始化数据节)中,但实例具有不同的初始化代码,默认情况下该代码将始终位于.text节中。也许一个简单的

memset( cbExpectectedImageHash, 0x00, sizeof( cbExpectectedImageHash ) );

当数据存在于.bss节(未初始化数据节)中时已被移除。再次运行上述代码将产生以下不正确的结果

Screenshot - SelfHealing05.png

需要第三次运行才能正确计算预计算哈希。

局部哈希变量

DATA段初始化中的最后一个注意事项与哈希变量在堆栈上的放置有关。简单地说,一个人可以在非 Visual Studio 运行(即环境外部)之间根据需要多次对可执行文件进行回溯修补,但当变量不在全局作用域时,一个人“将无法获得正确的结果”。这是因为 cbExpectedHashImage 位于程序的堆栈上,并且初始化代码位于 .text 段中。在命令行项目的情况下,变量 cbExpectedImageHash 必须放置在 main() 之外。因此,以下代码将无法产生预期结果:

int main(int argc, char* argv[])
{
    HMODULE hModule = NULL;
    PVOID   pVirtualAddress = NULL;
    PVOID   pCodeStart = NULL;
    PVOID   pCodeEnd = NULL;
    SIZE_T  dwCodeSize = 0;

    BYTE cbExpectedImageHash[ CryptoPP::SHA224::DIGESTSIZE ] =
        { 0x09,0x16,0x5E,0x03,0x92,0xF4,0x02,
          0x82,0x40,0xD0,0xAE,0xEA,0x30,0xB6,
          0xCA,0xF4,0x94,0xCC,0x92,0x90,0x89,
          0x75,0x70,0x82,0x34,0x71,0x19,0xED };

    BYTE cbCalculatedImageHash[ CryptoPP::SHA224::DIGESTSIZE ];

    ImageInformation( hModule, pVirtualAddress, pCodeStart,
                      dwCodeSize, pCodeEnd );

    DumpImageInformation( hModule, pVirtualAddress, pCodeStart,
                          dwCodeSize, pCodeEnd );
    ....

    return 0;
}

代码生成分析

查看以下简单代码的反汇编,揭示了当BYTE array[]放在main()内部时代码不断变化的原因——操作码中立即数的编码。

int main( )
{
    BYTE array [ 28 ] =
        { 0x00,0x01,0x02,0x03,0x04,0x05,0x06,
          0x00,0x01,0x02,0x03,0x04,0x05,0x06,
          0x00,0x01,0x02,0x03,0x04,0x05,0x06,
          0x00,0x01,0x02,0x03,0x04,0x05,0x06 };

    return 0;
}

Screenshot - SelfHealing25.png

要了解为什么全局变量不会导致上述代码生成问题,可以使用PE Browse来检查以下示例中可执行文件的.data节(已初始化数据节)。

BYTE array [ 28 ] =
     { 0x00,0x01,0x02,0x03,0x04,0x05,0x06,
       0x00,0x01,0x02,0x03,0x04,0x05,0x06,
       0x00,0x01,0x02,0x03,0x04,0x05,0x06,
       0x00,0x01,0x02,0x03,0x04,0x05,0x06 };

int main()
{
    return 0;
}

请注意,下面array现在存储在.data节中,而不是立即数值操作码的集合或.bss节(未初始化数据节)。请记住,本文不哈希数据节——只哈希指定的.text节。这是array的“分配和初始化”。因此,没有改变.text节代码的原因。

以下是使用PE Browse检查可执行文件时.data节的视图。

如果在Visual Studio调试器中将鼠标悬停在变量array上,Intellisense将报告array的地址为0x408030。如果意外地溢出array内存,第一个将被覆盖的字节将是0x40805C——字节值为0xA0。

轮询与通知

本文使用Crypto++和哈希来通过轮询确定.text节何时在内存中被修改。轮询似乎是程序员唯一可用的选项。一个显而易见的观察点是:如果触发是可能的,那么一个在磁盘上应用了未经授权补丁的可执行文件将不会触发事件。

Windows API

如果Microsoft Windows为程序员提供了内存写入通知(进入.text节)API,那么只需等待触发并根据需要注入青霉素代码即可。根据Microsoft MVP Newcomer博士的说法,这样的通知是不可用的。

调试寄存器

正如Matthew Faithfull所指出的(Oleg Starodumov在上面重申),在Visual Studio调试器下,可以设置硬件断点来完成数据任务。在调试应用程序中,John Robbin's提供了调试器的源代码。然而,该程序使用软件断点而非硬件断点。

以下内容摘自Intel 架构软件开发人员手册 第3卷:系统编程。第15章调试和性能监控指出,硬件不支持通知,原因有二:

  • 硬件断点对.text段写入无效
  • 使用硬件断点,只能指定最多4字节的长度参与监控

防护页

根据Slava Usov(在microsoft.public.win32.programmer.kernel的一个帖子中)的说法,使用VirtualQueryEx()VirtualProtectEx()可能实现通知。有关完整讨论,请参阅MSDN中创建防护页。为方便读者,以下是重现的步骤。

  1. 在(您自己的)调试器下运行应用程序。
  2. 使用 VirtualQueryEx() 获取相关内存位置的当前页面保护。
  3. 使用 VirtualProtectEx() 将内存位置的保护更改为 current_page_protection | PAGE_GUARD
  4. 查找带有代码STATUS_GUARD_PAGE且地址属于该内存位置的异常[WaitForDebugEvent()]。(STATUS_GUARD_PAGE在头文件中未定义(我真希望我知道为什么);其数值为0x80000001。)
  5. 一旦您(您的调试器)收到此类异常,执行您想要的操作,然后使用SetThreadContext()将线程设置为单步执行,然后处理调试事件[ContinueDebugEvent(),使用DBG_CONTINUE]。如果目标进程是多线程的,您应该暂停所有其他线程(否则其他线程可能会访问内存位置而您无法察觉)。
  6. 等待EXCEPTION_SINGLE_STEP异常,之后调用ContinueDebugEvent()并传入DBG_CONTINUE,然后转到步骤2。如果您在上一步暂停了线程,请现在恢复它们。

就本文而言,感兴趣的异常将是STATUS_GUARD_PAGE_VIOLATION

自修复1

自修复1取自动态TEXT节镜像验证。它是一个基本的重写(这应该在上一篇文章中完成)——主要是通过复制粘贴来重新排列可执行文件以实现功能和美观。它将作为本文的起点。

Screenshot - SelfHealing01.png

动态TEXT节镜像验证中示例一的感兴趣函数是

VOID ImageInformation( HMODULE& hModule, PVOID& pVirtualAddress,
                       PVOID& pCodeStart, SIZE_T& dwCodeSize,
                       PVOID& pCodeEnd )

ImageInformation()通过结合GetModuleHandle()返回的地址并解析各种头部,在内存中定位TEXT节的起始位置,从而填充程序后续使用的参数。

该示例随后通过使用标准内存读取函数读取内存中的.text节来转储遇到的字节码——请注意,由于操作在其自身进程范围内,因此不需要MapViewOfFile()ReadProcessMemory()

自修复2

自修复2中提供的示例通过添加加密哈希函数来构建在先前示例的基础上。哈希函数创建可执行文件.text节的摘要。

Screenshot - SelfHealing02.png

代码的数据通过添加两个BYTE数组进行修改,用于.text节的SHA-224哈希:预期(预计算)哈希和计算(运行时)哈希。

除了用于哈希的BYTE数组外,还添加了一个哈希对象和执行哈希的代码。此代码如下所示。

VOID CalculateImageHash( PVOID pCodeStart, SIZE_T dwCodeSize,
                         PBYTE pcbDigest )
{
    CryptoPP::SHA224 hash;

    hash.Update( (PBYTE)pCodeStart, dwCodeSize );
    hash.Final( pcbDigest );
}

要构建一个功能正常的EXECUTABLE,需要两次编译:第一次编译和随后的运行生成预期的(现在称为预计算的)哈希。然后将预计算的哈希添加到EXECUTABLE中。最后,第二次运行将导致预计算的哈希等于运行时哈希。

正如动态TEXT节镜像验证所演示的,可以使用磁盘上的.text节镜像,也可以使用内存中的.text节镜像。.text镜像是一致的。

“请注意,在调试器下运行可执行文件会导致哈希值发生变化。”这是因为Visual Studio调试器会在程序中插入软件断点(0xCC操作码或中断3)。更糟糕的是,在查看反汇编时,软件断点不会显示。根据Microsoft VC++ MVP Oleg Starodumov的说法:

[Visual Studio 调试器只能对数据访问使用硬件断点]
(仅用于写入)。如果您需要在代码执行时中断,请考虑使用WinDbg。

最后,引用微软SDK MVP Ken Johnson 的话:

...在WinDbg中,如果使用'ba'命令,则相关代码字节不会被修改(即不会被0xcc/int 3替换)。你最多只能同时激活4个'ba'断点,因为它们使用硬件提供的调试寄存器,这些寄存器只支持四个目标地址。

由于Visual Studio软件断点的问题,程序是在命令行而不是Visual Studio环境中构建并运行的。如果读者观察标题栏文本的变化,这一点就显而易见。

在下面的两张图片中,自修复2分别从Visual Studio环境(黄色文本)和命令行(绿色文本)运行一次,以演示断点问题。在这两种情况下,代码是完全相同的。

Screenshot - SelfHealing20.png

Screenshot - SelfHealing21.png

上述代码用于创建预计算哈希。在程序第一次运行和第二次运行之间的中间步骤中,需要对可执行文件进行回溯修补以填充正确的预期摘要。自修复2的代码在第一次运行前显示如下。

BYTE cbExpectedImageHash[ CryptoPP::SHA224::DIGESTSIZE ] =
    { 0x00,0x01,0x02,0x03,0x04,0x05,0x06,
      0x00,0x01,0x02,0x03,0x04,0x05,0x06,
      0x00,0x01,0x02,0x03,0x04,0x05,0x06,
      0x00,0x01,0x02,0x03,0x04,0x05,0x06, };

BYTE cbCalculatedImageHash[ CryptoPP::SHA224::DIGESTSIZE ];

int main(int argc, char* argv[])
{
    HMODULE hModule = NULL;
    PVOID   pVirtualAddress = NULL;
    PVOID   pCodeStart = NULL;
    PVOID   pCodeEnd = NULL;
    SIZE_T  dwCodeSize = 0;

    ImageInformation( hModule, pVirtualAddress, pCodeStart,
                      dwCodeSize, pCodeEnd );

    DumpImageInformation( hModule, pVirtualAddress, pCodeStart,
                          dwCodeSize, pCodeEnd );

    HexDump( pCodeStart, pCodeStart, DUMP_SIZE );

    CalculateImageHash( pCodeStart, dwCodeSize, cbCalculatedImageHash );

    DumpHash( cbExpectedImageHash, CryptoPP::SHA224::DIGESTSIZE,
              "SHA-224 Expected Image Hash" );
    DumpHash( cbCalculatedImageHash, CryptoPP::SHA224::DIGESTSIZE,
              "SHA-224 Calculated Image Hash" );

    if( 0 == memcmp( cbExpectedImageHash, cbCalculatedImageHash,
                     CryptoPP::SHA224::DIGESTSIZE ) )
    {
        std::cout << "Image is verified." << std::endl;
    }
    else
    {
        std::cout << "Image has been modified." << std::endl;
    }

    return 0;
}

有了正确的预期哈希值(E259A10464E487076CDB8F83E6D06ACB53564A1684BA84B3ABA72F4B),现在可以将其插入代码中,以正确初始化cbExpectedImageHash,如下所示。

BYTE cbExpectedImageHash[ CryptoPP::SHA224::DIGESTSIZE ] =
     { 0xE2,0x59,0xA1,0x04,0x64,0xE4,0x87,
       0x76,0xCD,0xB8,0xF8,0x3E,0x6D,0x06,
       0xAC,0xB5,0x35,0x64,0xA1,0x68,0x4B,
       0x16,0x84,0xB3,0xAB,0xA7,0x2F,0x4B, };

自修复3(经过少量代码更改后——因此哈希值不同)的命令行运行演示了预期结果。

Screenshot - SelfHealing03.png

正确的代码如下所示。BYTE数组位于全局作用域,并且要么

  • 用占位符填充(0x00, 0x01, ..., 0x05, 0x06 - 重复四次)
  • 用正确的哈希值填充(0x09, 0x16, ... 0xED)

再次强调,这种预填充或回溯修补确保了编译器生成的代码在不同编译器调用之间保持一致。

// Self Healing 2.cpp

#include "stdafx.h"

#include "sha.h"        // SHA
#include "hex.h"        // HexEncoder
#include "filters.h"    // StringSink

VOID ImageInformation( HMODULE& hModule, PVOID& pVirtualAddress,
                       PVOID& pCodeStart, SIZE_T& dwCodeSize,
                       PVOID& pCodeEnd );

VOID DumpImageInformation( HMODULE hModule, PVOID pVirtualAddress,
                           PVOID pCodeStart, SIZE_T dwCodeSize,
                           PVOID pCodeEnd );

VOID CalculateImageHash( PVOID pCodeStart, SIZE_T dwCodeSize,
                         PBYTE pcbDigest );

VOID DumpHash( PBYTE pcbDigest, SIZE_T dwSize, std::string message );

VOID HexDump( LPCVOID pcbStartAddress,
              LPCVOID pDisplayBaseAddress = (PVOID)-1,
              DWORD dwSize = DEFAULT_DUMP_SIZE );

// These values must be Global. Place them inside
//   main(), and you get different code generation.
BYTE cbExpectedImageHash[ CryptoPP::SHA224::DIGESTSIZE ] =
    { 0x09,0x16,0x5E,0x03,0x92,0xF4,0x02,
      0x82,0x40,0xD0,0xAE,0xEA,0x30,0xB6,
      0xCA,0xF4,0x94,0xCC,0x92,0x90,0x89,
      0x75,0x70,0x82,0x34,0x71,0x19,0xED };

BYTE cbCalculatedImageHash[ CryptoPP::SHA224::DIGESTSIZE ];

int _tmain(int argc, _TCHAR* argv[])
{
    HMODULE hModule = NULL;
    PVOID   pVirtualAddress = NULL;
    PVOID   pCodeStart = NULL;
    PVOID   pCodeEnd = NULL;
    SIZE_T  dwCodeSize = 0;

    ImageInformation( hModule, pVirtualAddress, pCodeStart,
                      dwCodeSize, pCodeEnd );

    DumpImageInformation( hModule, pVirtualAddress, pCodeStart,
                          dwCodeSize, pCodeEnd );

    HexDump( pCodeStart, pCodeStart, DUMP_SIZE );

    CalculateImageHash( pCodeStart, dwCodeSize, cbCalculatedImageHash );

    DumpHash( cbExpectedImageHash, CryptoPP::SHA224::DIGESTSIZE,
              "SHA-224 Expected Image Hash" );
    DumpHash( cbCalculatedImageHash, CryptoPP::SHA224::DIGESTSIZE,
              "SHA-224 Calculated Image Hash" );

    if( 0 == memcmp( cbExpectedImageHash, cbCalculatedImageHash,
        CryptoPP::SHA224::DIGESTSIZE ) )
    {
        std::tcout << _T("Image is verified.") << std::endl;
    }
    else
    {
        std::tcout << _T("Image has been modified.") << std::endl;
    }

    return 0;
}

VOID DumpHash( PBYTE pcbDigest, SIZE_T dwSize, std::string message )
{
    CryptoPP::HexEncoder encoder;
    std::string sink;

    encoder.Attach( new CryptoPP::StringSink (sink) );
    encoder.Put( pcbDigest, dwSize );
    encoder.MessageEnd();

    std::cout << std::endl;

    if( 0 != message.length() )
    {
        std::cout << message << std::endl;
    }

    std::cout << sink << std::endl << std::endl;
}

VOID CalculateImageHash( PVOID pCodeStart, SIZE_T dwCodeSize,
                         PBYTE pcbDigest )
{
    CryptoPP::SHA224 hash;

    hash.Update( (PBYTE)pCodeStart, dwCodeSize );
    hash.Final( pcbDigest );
}

VOID DumpImageInformation( HMODULE hModule, PVOID pVirtualAddress,
                           PVOID pCodeStart, SIZE_T dwCodeSize,
                           PVOID pCodeEnd )
{
    std::tcout << _T("****************************************************");
    std::tcout << _T("************* Memory Image Information *************");
    std::tcout << _T("****************************************************");
    std::tcout << std::endl;

    std::tcout << _T("         hModule: ");
    std::tcout << HEXADECIMAL_OUTPUT(8);
    std::tcout << hModule << std::endl;

    ...

    std::tcout << std::endl;
}

VOID ImageInformation( HMODULE& hModule, PVOID& pVirtualAddress,
                       PVOID& pCodeStart, SIZE_T& dwCodeSize,
                       PVOID& pCodeEnd )
{
    const UINT PATH_SIZE = 2 * MAX_PATH;
    TCHAR szFilename[ PATH_SIZE ] = { 0 };

    __try {

        /////////////////////////////////////////////////
        /////////////////////////////////////////////////
        if( 0 == GetModuleFileName( NULL, szFilename, PATH_SIZE ) )
        {
            std::tcerr << _T("Error Retrieving Process Filename");
            std::tcerr << std::endl;
            __leave;
        }

        hModule = GetModuleHandle( szFilename );

        ...
    }
}

其他值得注意的地方如下:

  • HexEncoderBYTE 数组转换为 std::string
  • StringSink 是库的内置机制,用于将数据(在此例中是人类可读的字符串)发送到对象(std::string
  • Attach() 是库用于动态附加 StringSink 的方法
  • Put() 是库用于将数据推送到对象(HexEncoder)的方法
  • MessageEnd() 通知编码器完成其操作并刷新其缓冲区

自修复3

尽管之前已经介绍过,自修复3是示例2从调试器外部命令行正确运行,并且预期图像哈希变量已回溯修补(且处于全局作用域)。

自修复4

第四个示例代码是从可执行文件中提取和压缩未修改的.text节的代码。本文的这一部分,压缩后的.text节将保存到一个名为TextImage.gz的文件中。

提取并压缩的.text节是随后恢复的数据,如果检测到加载错误或未经授权的内存补丁。读者应该探索其他存储提取和压缩数据的方法。候选方案包括:

  • 作为可执行文件的资源
  • 作为资源DLL
  • 作为文件
  • 在Windows注册表中

就候选方案而言,Windows注册表可能是最不理想的(如果选择将预期哈希值嵌入,因为哈希值将少于32字节,则情况并非如此)。Microsoft建议数据限制约为2048字节。请阅读MSDN中Microsoft的注册表元素大小限制

创建各种资源的步骤可以在创建资源DLL创建仅包含资源的DLL中找到。如果读者选择,则将其作为练习。

选择平面文件是为了简单、功能,并演示Crypto++的GzipGunzip类。

此示例只是获取内存中的.text节,将其压缩,然后写入文件。自修复4将在下一个小节,即回溯修补之后进行详细检查。为了完整性,命令行运行如下所示。注意预期哈希的占位符:0x00, 0x01, ..., 0x05, 0x06,以确保不同运行的回溯修补代码生成一致。

Screenshot - SelfHealing07.png

自修复5

自修复5在压缩图像后执行TEXT节导出。请注意,已经发生了回溯修补。

Screenshot - SelfHealing08.png

由于程序是从命令行运行的,因此解释器可能具有与程序目录不同的 pwd(当前工作目录)。在这种情况下,pwd 是 C:\。因此,存档被放置在 C:\ 中,而不是程序的构建目录中。

在WinRAR中导航到C:\的根目录并打开存档,会发现一个一致的TEXT段图像。从第五个示例中转储的信息来看,.text段大小为0x17FCE5,即1,572,069十进制字节。

Screenshot - SelfHealing09.png

最后一步是解压TextImage.gz,然后使用十六进制编辑器打开解压后的文件,以验证压缩和解压操作的正确性。这在下面使用UltraEdit32进行了验证。

Screenshot - SelfHealing10.png

本示例中的代码添加了一个函数调用,如下所示。pCodeStartdwCodeSize正在从ImageInformation()中使用。

    std::string filename = "TextImage.gz";
    ExportTextImage( filename, pCodeStart, dwCodeSize );

最后,创建存档的Crypto++代码

    CryptoPP::Gzip zipper(
        new CryptoPP::FileSink (filename.c_str(), true ),
    CryptoPP::Gzip::MAX_DEFLATE_LEVEL ); // Gzip

    zipper.Put( (byte*)pCodeStart, dwCodeSize );
    zipper.MessageEnd( );

GZip构造函数接受一个BufferdTransformation*FileSink对象)和一个Deflate级别作为参数。正在使用的文档化构造函数GzipFileSink如下所示。请参考Crypto++手册中的GzipFileSink类。

Gzip (BufferedTransformation *attachment=NULL,
    unsigned int deflateLevel=DEFAULT_DEFLATE_LEVEL,
    unsigned int log2WindowSize=DEFAULT_LOG2_WINDOW_SIZE,
    bool detectUncompressible=true);

FileSink (const char *filename, bool binary=true);

然后遇到之前在HexEncoder中遇到的Put()MessageEnd()函数。不同的对象(Gzip vs. HexEncoder),相同的结果——数据被推入对象,处理,然后通知对象完成其操作并刷新其缓冲区。

值得一提的是,FileSinkHexEncoderGzip(以及其他)都具有一个共同的祖先:BufferedTransformation。这是Crypto++中管道(Pipelining)过滤器链(Chaining)的基础。有关该主题的更详细讨论,请参阅基于高级加密标准的产品密钥

自修复6

示例6相当枯燥——它只是读取TEXT节存档,将其放入缓冲区(命令行项目调试构建中一个相当大的缓冲区),并转储前96个字节以与原始TEXT节进行比较。此示例在回溯修补操作之后呈现(回溯修补在示例2到5中执行,本质上将一个示例变为两个)。

Screenshot - SelfHealing11.png

Gunzip代码是无耻地从Wei的Crypto++,test.cpp中复制而来(并添加了try/catch块的封装)

VOID ImportTextImage( const std::string& filename,
                      PBYTE pBuffer, SIZE_T dwBufferSize )
{
    try {

        std::string RecoveredTextSection;
        CryptoPP::FileSource( filename.c_str(), true,
            new CryptoPP::Gunzip(
                new CryptoPP::StringSink( RecoveredTextSection )
            ) // Gunzip
        ); // FileSource

        if( RecoveredTextSection.length() > dwBufferSize )
        {
            std::tcerr << _T("ImportTextImage: Executing Buffer Overflow");
        }

        memcpy( pBuffer, RecoveredTextSection.c_str(), dwBufferSize );
    }

    catch( CryptoPP::Exception& e )
    {
        std::cerr << e.what() << std:: endl;
    }

    catch( ... )
    {
        std::tcerr << _T("Caught Unknown Exception");
        std::tcerr << std:: endl;
    }
}

以下代码片段和发布构建运行(使用绿色文本)的图示演示了基于_DEBUG的条件编译。程序员现在可以享用调试和发布对的4个构建。值得注意的是,发布构建的.text节大小显著减小:0x40130或262,448十进制字节。压缩后为135,687十进制字节。

#ifdef _DEBUG
BYTE cbExpectedImageHash[ CryptoPP::SHA224::DIGESTSIZE ] =
    { 0x12,0xE3,0x39,0xB5,0xBF,0xF8,0xEF,
      0xAD,0xEF,0x2A,0x6F,0xA8,0x6E,0x04,
      0x7D,0x27,0xF9,0xA6,0x18,0x1F,0x9A,
      0x45,0x38,0x57,0xCE,0x14,0xFD,0xF7 };
#else
BYTE cbExpectedImageHash[ CryptoPP::SHA224::DIGESTSIZE ] =
    { 0x10,0xBD,0x17,0xD7,0x86,0x83,0xB9,
      0x55,0xA5,0x20,0xDC,0x0B,0x30,0x6F,
      0x14,0x19,0x06,0xEE,0x25,0x02,0xEE,
      0xE7,0x95,0x1F,0x6A,0x6B,0x5A,0x25 };
#endif

Screenshot - SelfHealing12.png

自修复7

自修复7是本文的最终概念验证。程序执行以下步骤(与示例六一样,回溯修补已提前执行):

  1. 转储原始.text
  2. 转储存档的.text
  3. 比较哈希值
  4. 修改一个字节
  5. 转储修改后的.text
  6. 比较哈希值
  7. 恢复存档的.text(无先验知识)
  8. 转储修复后的.text
  9. 比较哈希值

在此示例中,以下代码将不起作用,因此将使用WriteProcessMemory()

((PBYTE)pCodeStart)[ 0 ] = 0x90;  // No Operation (and flush the CPU's cache)

Screenshot - SelfHealing13.png

一旦切换到WriteProcessMemory()进行篡改(1字节),示例再次使用该函数进行修复。然而,整个.text节都被恢复了。极端恢复的原因是作者花费了大量时间尝试执行功能级别检测和恢复。

人们认为可以执行功能级检测和恢复,但并非没有动态反汇编器。这显然是可行的,因为SoftICE(以及其他调试器)具有此功能。话虽如此,Russell Osterlund礼貌地拒绝分享他的PEBrowse源代码。

在调试构建中未按预期执行的是address (&) 运算符和函数包围。考虑以下代码片段:

void main()
{
    Function1();

    Function2();
}

void Function1() { ... }

void Function2() { ... }

main()的起始地址可以用&main()确定;相反,第一个函数的地址是&Function1()。人们会错误地得出结论,sizeof( main )是地址之差。

在调试版本中,&main()将返回一个跳转桩的地址(有关讨论,请参阅动态TEXT节镜像验证中的GetAddressOfMain())。其次,无法保证调试或发布版本的二进制布局与源文件一致。最后,发布版本中的函数内联可能会完全优化掉函数调用。

鼓励读者通过创建功能级检测和恢复的确定性方法来进一步开展这项工作。

调试(蓝色文本)和发布(绿色文本)执行的结果如下所示。

Screenshot - SelfHealing14.png

Screenshot - SelfHealing15.png

感兴趣的函数现在是AlterTextImage()HealTextImage()AlterImageText()简单地将一个空操作指令写入.text节的第一个字节。

VOID AlterTextImage( LPVOID pcbStartAddress, BYTE OpCode )
{
    HANDLE hProcess = NULL;
    BOOL bResult = FALSE;
    SIZE_T dwBytesWritten = 0;

    __try
    {
        hProcess = OpenProcess( PROCESS_VM_OPERATION | PROCESS_VM_WRITE,
                                FALSE, GetCurrentProcessId() );

        if( NULL == hProcess )
        {
            std::tcerr << std::endl;
            std::tcerr << _T("Unable to Open Process");
            std::tcerr << std::endl;
            __leave;
        }

        bResult = WriteProcessMemory( hProcess, pcbStartAddress,
                        &OpCode, sizeof( OpCode ), &dwBytesWritten );

        if( FALSE == bResult || 1 != dwBytesWritten )
        {
            std::tcerr << std::endl;
            std::tcerr << _T("Unable to Alter .text Section");
            std::tcerr << std::endl;
        }
    }

    __except( EXCEPTION_EXECUTE_HANDLER ) {
        std::tcerr << std::endl;
        std::tcerr << _T("Caught Exception in AlterTextImage");
        std::tcerr << std::endl;
    }

    if( NULL != hProcess ) { CloseHandle( hProcess ); }
}

以及HealTextImage()中对应的青霉素代码。请注意,此代码将整个.text节用一个已知的良好副本重新写入

VOID HealTextImage( LPVOID pStartAddress, LPCVOID pArchivedText,
                            SIZE_T dwSize )
{
    HANDLE hProcess = NULL;
    BOOL bResult = FALSE;
    SIZE_T dwBytesWritten = 0;

    __try
    {
        hProcess = OpenProcess( PROCESS_VM_OPERATION | PROCESS_VM_WRITE,
                                FALSE, GetCurrentProcessId() );

        if( NULL == hProcess )
        {
            std::tcerr << std::endl;
            std::tcerr << _T("Unable to Open Process");
            std::tcerr << std::endl;
            __leave;
        }

        bResult = WriteProcessMemory( hProcess, pStartAddress,
                        pArchivedText, dwSize, &dwBytesWritten );

        if( FALSE == bResult || dwSize != dwBytesWritten )
        {
            std::tcerr << std::endl;
            std::tcerr << _T("Unable to Heal .text Section");
            std::tcerr << std::endl;
        }
    }

    __except( EXCEPTION_EXECUTE_HANDLER ) {
        std::tcerr << std::endl;
        std::tcerr << _T("Caught Exception in HealTextImage");
        std::tcerr << std::endl;
    }

    if( NULL != hProcess ) { CloseHandle( hProcess ); }
}

为完整起见,重新生成示例7的main()函数。下面代码中的程序流程与以下(前面概述的)内容对应:

  1. 转储原始.text
  2. 转储存档的.text
  3. 比较哈希值
  4. 修改一个字节
  5. 转储修改后的.text
  6. 比较哈希值
  7. 恢复存档的.text(无先验知识)
  8. 转储修复后的.text
  9. 比较哈希值

//  These values must be Global. Place them inside
//  main(), and you get different code generation
//  after each back patch operation.
#ifdef _DEBUG
BYTE cbExpectedImageHash[ CryptoPP::SHA224::DIGESTSIZE ] =
    { 0xF2,0x1A,0xCF,0x46,0x53,0xAB,0x47,
      0x02,0xD5,0x00,0x24,0xBC,0xF8,0xA1,
      0x8E,0xD6,0xFF,0xFF,0x60,0x06,0x18,
      0x01,0x85,0x70,0x83,0x46,0x7C,0x4F };
#else
BYTE cbExpectedImageHash[ CryptoPP::SHA224::DIGESTSIZE ] =
    { 0x44,0x23,0x76,0xCF,0x3C,0x5E,0x7C,
      0x7B,0x81,0x86,0xAA,0x23,0xD7,0x59,
      0xFE,0x21,0xF6,0xB9,0xCB,0x52,0x11,
      0x0A,0x9F,0x63,0xB8,0x7F,0xF8,0x70 };
#endif

BYTE cbCalculatedImageHash[ CryptoPP::SHA224::DIGESTSIZE ];

int _tmain(int argc, _TCHAR* argv[])
{
    HMODULE hModule = NULL;
    PVOID   pVirtualAddress = NULL;
    PVOID   pCodeStart = NULL;
    PVOID   pCodeEnd = NULL;
    SIZE_T  dwCodeSize = 0;


    // Set Up - Develop EXE Information
    ImageInformation( hModule, pVirtualAddress, pCodeStart,
                      dwCodeSize, pCodeEnd );

    // Set Up - Dump Information
    DumpImageInformation( hModule, pVirtualAddress, pCodeStart,
                          dwCodeSize, pCodeEnd );

    // Set Up - Export .text Section
    std::string filename = "TextImage.gz";
    ExportTextImage( filename, pCodeStart, dwCodeSize );

    // Set Up - Import .text Section
    SIZE_T dwBufferSize = dwCodeSize;
    PBYTE pArchiveBuffer = new BYTE[ dwBufferSize ];
    if( NULL == pArchiveBuffer ) { return -1; }
    ImportTextImage( filename, pArchiveBuffer, dwBufferSize );

    // Step 1: Dump Original .text
    std::tcout << _T("Original TEXT Section") << std::endl;
    HexDump( pCodeStart, pCodeStart, DUMP_SIZE );
    std::tcout << std::endl;

    // Step 2: Dump Archived .text
    std::tcout << _T("Archived TEXT Section") << std::endl;
    HexDump( pArchiveBuffer, (LPCVOID)NULL, DUMP_SIZE );
    std::tcout << std::endl;

    // Set Up: Calculate Hashes
    CalculateImageHash( pCodeStart, dwCodeSize, cbCalculatedImageHash );

    // Step 3: Compare Hashes
    std::tcout << std::endl;
    DumpHash( cbExpectedImageHash, CryptoPP::SHA224::DIGESTSIZE,
              "SHA-224 Expected Image Hash" );
    DumpHash( cbCalculatedImageHash, CryptoPP::SHA224::DIGESTSIZE,
              "SHA-224 Calculated Image Hash" );

    // Step 3: Compare Hashes
    if( 0 == memcmp( cbExpectedImageHash, cbCalculatedImageHash,
        CryptoPP::SHA224::DIGESTSIZE ) )
    {
        std::tcout << _T("Image is verified.") << std::endl;
    }
    else
    {
        std::tcout << _T("Image has been modified.") << std::endl;
    }
    std::tcout << std::endl;
    std::tcout << "================================" << std::endl;

    // Step 4: Alter Bytes
    AlterTextImage( pCodeStart, 0x90 );

    // Step 5: Dump Altered .text
    std::tcout << _T("Altered TEXT Section") << std::endl;
    HexDump( pCodeStart, pCodeStart, DUMP_SIZE );
    std::tcout << std::endl;

    // Set Up: Calculate Hashes
    CalculateImageHash( pCodeStart, dwCodeSize, cbCalculatedImageHash );

    // Step 6: Compare Hashes
    std::tcout << std::endl;
    DumpHash( cbExpectedImageHash, CryptoPP::SHA224::DIGESTSIZE,
              "SHA-224 Expected Image Hash" );
    DumpHash( cbCalculatedImageHash, CryptoPP::SHA224::DIGESTSIZE,
              "SHA-224 Calculated Image Hash" );

    // Step 6: Compare Hashes
    if( 0 == memcmp( cbExpectedImageHash, cbCalculatedImageHash,
        CryptoPP::SHA224::DIGESTSIZE ) )
    {
        std::tcout << _T("Image is verified.") << std::endl;
    }
    else
    {
        std::tcout << _T("Image has been modified.") << std::endl;
    }
    std::tcout << std::endl;
    std::tcout << "================================" << std::endl;

    // Step 7: Heal the Code
    //   We should not fall through this
    if( 0 != memcmp( cbExpectedImageHash, cbCalculatedImageHash,
        CryptoPP::SHA224::DIGESTSIZE ) )
    {
        HealTextImage( pCodeStart, pArchiveBuffer, dwBufferSize );
    }

    // Step 8: Dump Healed .text
    std::tcout << _T("Healed TEXT Section") << std::endl;
    HexDump( pCodeStart, pCodeStart, DUMP_SIZE );
    std::tcout << std::endl;

    // Set Up: Calculate Hashes
    CalculateImageHash( pCodeStart, dwCodeSize, cbCalculatedImageHash );

    // Step 9: Compare Hashes
    std::tcout << std::endl;
    DumpHash( cbExpectedImageHash, CryptoPP::SHA224::DIGESTSIZE,
              "SHA-224 Expected Image Hash" );
    DumpHash( cbCalculatedImageHash, CryptoPP::SHA224::DIGESTSIZE,
              "SHA-224 Calculated Image Hash" );

    // Step 9: Compare Hashes
    if( 0 == memcmp( cbExpectedImageHash, cbCalculatedImageHash,
        CryptoPP::SHA224::DIGESTSIZE ) )
    {
        std::tcout << _T("Image is verified.") << std::endl;
    }
    else
    {
        std::tcout << _T("Image has been modified.") << std::endl;
    }

    // Cleanup
    if( NULL != pArchiveBuffer ) { delete[] pArchiveBuffer; }

    return 0;
}

最佳实践要求在执行上述操作之前验证.text节存档副本的完整性(可能使用哈希)。一个更好的解决方案是对存档代码进行数字签名,这样就无法伪造哈希或其中包含的青霉素代码而不被检测到。有关带恢复功能的消息签名的示例,请参阅基于RSA签名的产品激活。这些练习留给读者完成。

Windows Vista兼容性

作者非常高兴地报告,这些技术100%兼容Windows Vista。遇到了一个小问题:在标准用户账户下运行程序时,无法在C:\中创建TextImage.gz

Screenshot - SelfHealing42.png

Windows消息框被调用是因为有人向.text节写入了垃圾——回想一下,存档创建和随后的恢复失败了。不可否认,作者应该在演示代码中放置更多的检查。

如果存档文件存在(来自之前在管理员账户下的运行),程序按预期工作。

Screenshot - SelfHealing41.png

这是因为在没有虚拟化的情况下,本地用户(其中已验证用户是成员)默认拥有三个权限。请注意,这台计算机是私人域(home.pvt)的一部分。

Screenshot - SelfHealing40.png

请注意,默认情况下未启用写入权限。有关Microsoft最新用户安全尝试的讨论,请参阅Mark Russinovich的Windows Vista用户账户控制内幕

致谢

  • Wei Dai for Crypto++ 及其在 Crypto++ 邮件列表上的宝贵帮助
  • A. Brooke Stephens 博士,他为我打下了密码学基础

修订

  • 2007年11月5日 添加自完整性检查部分
  • 2007年6月14日 修改“自修复软件”部分
  • 2007年6月1日 扩展“Vista兼容性”部分
  • 2007年6月1日 在“引言”部分添加了讨论主题
  • 2007年5月31日 添加“Vista兼容性”部分
  • 2007年5月31日 添加了对Intel系统编程手册的引用
  • 2007年5月30日 添加“轮询与通知”部分
  • 2007年5月30日 添加对Hacker Disassembling Uncovered的引用
  • 2007年5月29日 更改存档扩展名从.zip到.gz
  • 2007年5月29日 添加解释器快速编辑技巧
  • 2007年5月29日 添加对Ezells的自修复应用的引用
  • 2007年5月29日 添加了全局和局部变量初始化的解释
  • 2007年5月28日 重写“自修复1”部分
  • 2007年5月28日 添加了ImageImformation()的解释
  • 2007年5月28日 添加“自修复软件”部分
  • 2007年5月28日 添加了Johnson关于WinDbg 'ba'命令的说明
  • 2007年5月28日 添加了回溯修补的检查(带反汇编)
  • 2007年5月28日 添加了Starodumov关于硬件调试寄存器的说明
  • 2007年5月28日 首次发布

校验和

  • SelfHealing1.zip
    • MD5: 7A351D99FE8C43DAE69E86BC91E7C156
    • SHA-1: 7E29AE5A2885A177A40A8FFCDEE61DF7C281A39B
  • SelfHealing2.zip
    • MD5: 0A4B07E14F6D9C793390B8535A6B0D0C
    • SHA-1: 2F29D87B2A13AD9AB73882F1366D101382B4F8B3
  • SelfHealing3.zip
    • MD5: B8D600316115EAA35C706868F7E782AA
    • SHA-1: 6C97B50102DBCACE3687F72BB41F84936667C460
  • SelfHealing4.zip
    • MD5: CA07633F5A015A31B8B1A54F8D4D95EE
    • SHA-1: 4EBB021699095A4A13FAAFABB41ACB5FD317E107
  • SelfHealing5.zip
    • MD5: 7D829223D51F3D10BDF41D3922B8DC6C
    • SHA-1: 888F21D9FCC086265AF4338D59D6C6E2810EFBF4
  • SelfHealing6.zip
    • MD5: 5B675B51DB28C78748B5E483F48EE213
    • SHA-1: 326B6A1F4DECB75474DDDECF9EA146090997EF08
  • SelfHealing7.zip
    • MD5: A9EDE7E9082380FDED436658D7B6E32D
    • SHA-1: 8CE27BBA9E7C6999CCEEF402154657FEE7A151AD
  • RelExe.zip
    • MD5: D54BC40AD94F316414D1239E637EB82E
    • SHA-1: 06F0C78AB952C1E0B15AC4F339CFE377CB1C592E
© . All rights reserved.