摘要
Microsoft® Application Verifier (AppVerifier) 是针对非托管代码的运行时验证工具,它有助于找到细小的编程错误、安全问题和受限的用户帐户特权问题,使用常规的应用程序测试技术很难识别出这些错误和问题。
本页内容
简介
什么是 AppVerifier?
AppVerifier 功能
何时使用 AppVerifier
如何使用 AppVerifier
分析 AppVerifier 数据
有关 AppVerifier 的要点
附录 A:Driver Verifier
附录 B:使用 Application Verifier 测试安全功能
附录 C:参考站点
简介
程序员、软件架构师、测试人员以及安全咨询师所面临的最大难题之一是,了解其应用程序部署到产品时的结果。即使具备对源代码的访问,也很难掌握执行过程中将发生的每个操作,这归咎于存在多种依赖项(例如,服务于代码或利用外部组件的多个组)。在帮助管理这种复杂性以及错误带来的潜在的副面影响方面,Microsoft AppVerifier 扮演着重要的角色。AppVerifier 有助于找到细小的编程错误、安全问题和用户帐户特权问题,这些错误和问题在典型的测试过程中是很难识别的。
本文提供的信息,涉及 AppVerifier 如何工作以及在软件开发生命周期 (Software Development Lifecycle) 过程中如何应用 AppVerifier。
什么是 AppVerifier?
AppVerifier 可以免费下载,特别用于检测和帮助调试内存损坏、危险的安全漏洞以及受限的用户帐户特权问题。AppVerifier 有助于创建可靠且安全的应用程序,方法是监视应用程序与 Microsoft® Windows® 操作系统的交互,并配置应用程序使用的对象、注册表、文件系统和 Win32 API(包括堆、句柄和锁)。AppVerifier 还包括检查,以便预测应用程序在非管理员环境中的执行情况。
在整个软件开发生命周期中使用时,AppVerifier 可节约开发工作的成本,因为它能方便地在早期就识别出问题,而在早期修复错误是比较容易且成本较低的。AppVerifier 还有助于检测出可能被忽视掉的错误,并确保最终的应用程序能够在受限的(例如,非管理员)环境中执行(最后这一点对于 Windows Vista 应用程序更为重要)。
AppVerifier 识别的问题
AppVerifier 有助于确定:
-
应用程序正确地使用 API 的时刻:
-
不安全的 TerminateThread API。
-
正确使用线程本地存储(Thread Local Storage,TLS)API。
-
正确使用虚拟空间操作(例如,VirtualAlloc 和 MapViewOfFile)。
-
-
应用程序是否使用结构化的异常处理隐藏访问冲突。
-
应用程序是否试图使用无效的句柄。
-
堆中是否有内存损坏或存在内存问题。
-
应用程序是否在资源不足的情况下用尽了内存。
-
是否正确使用了临界区。
-
运行在管理员环境中的应用程序在具有较低特权的环境中是否能良好运行。
-
当应用程序作为受限用户运行时是否会存在潜在的问题。
-
在线程的上下文中,是否在将来的函数调用中会存在未初始化的变量。
AppVerifier 功能
AppVerifier 创建于 2001 年,在登录过程中使用。然而,在 Download Center 的 3.0 版中进行了一些更改。用户界面经过了更改,增加了对安全的关注,更新的体系结构可以提供更健壮的报告(基于 XML 的日志),灵活性方面表现为能添加新的测试(例如,LuaPriv 等更多的新特性)。
AppVerifier 测试
AppVerifier 包含称为"验证层"的测试集。可以针对进行测试的每个应用程序打开或关闭它们。通过在测试区域内扩展验证层,将显示特定的测试。要打开针对该应用程序的测试,选择它旁边的复选框。要打开整个验证层(例如,Basics),选择最上层的复选框。
AppVerifier 可以执行四种不同类型的测试(在版本 3.0.28 中):
-
Basics 包含针对以下内容的测试:
-
Exceptions — 使用结构化的异常处理确保应用程序不隐藏访问冲突。
-
Handles — 确保应用程序不尝试使用无效的句柄。
-
Heaps — 查看堆中的内存损坏问题。
-
Locks — 验证临界区的正确使用。
-
Memory — 确保正确使用虚拟空间操作的 API。
-
TLS — 确保正确使用线程本地存储 API。
-
-
Low resource simulation 尝试在资源不足的情况下模拟环境,因此,您可定义一个编号 (0–100) 来指示关于以下内容的错误可能性调用:
-
Wait(例如,WaitForXXXX API)。
-
Heap_Alloc(堆分配 API)。
-
Virtual_Alloc(虚拟内存分配 API)。
-
注册表(注册表 API)。
-
文件(诸如 CreateFile 的文件 API)。
-
事件(诸如 CreateEvent 的事件 API)。
-
MapView(诸如 CreateMapView MapView API 的 CreateMapView)。
-
Ole_Alloc(诸如 SysAllocString 的 Ole API)。
-
-
LuaPriv 包含 31 种不同的测试,它们可在以下两种方案中使用:
-
Predictive — 确定一个以管理权限运行的应用程序如果以较小特权(通常,作为常规用户)运行时是否能正常工作。例如,如果应用程序对只允许管理员访问的文件进行写操作,那么如果该应用程序以非管理员身份运行,则它将无法写入此类文件。
-
Diagnostic — 以非管理员身份运行时,将识别可能已经存在于当前运行中的潜在问题。例如,如果应用程序尝试写入只允许管理员访问的文件,则应用程序将产生一个 ACCESS_DENIED 错误。
-
-
Miscellaneous 包含针对以下方面的测试:
-
Dangerous API — 进行跟踪,查看应用程序是否正在使用以下不安全的操作:
-
对 TerminateThread 的危险调用。
-
内存不足情况下潜在的堆栈溢出。
-
在多个线程仍然运行的情况下退出调用的进程。
-
LoadLibrary 在 DllMain 过程中调用。
-
FreeLibrary 在 DllMain 过程中调用。
-
Dirty Stacks 用内存模式填充(周期性地)未使用的堆栈部分。这有助于检测线程上下文的将来函数调用中未初始化的变量。
-
-
属性
AppVerifier 中有两组属性。一组基于测试,一组基于应用程序。每组属性按以下方式定义:
-
Name - 针对每个属性的唯一名称。
-
Type - Boolean、DWORD、String 和 MultipleString。
-
Value - 基于类型变化的可更改元素。
-
Description - 该描述解释属性的内容。
针对每个应用程序编辑属性
对于每个应用程序,以下属性可用于编辑:
-
Propagate — 将验证器设置从父进程传播到子进程。False(不选中该框)不传播这些设置,而 True(选中该框)则传播这些设置。
-
AutoClr — 当指定的图像开始运行之后,验证的图像将清除自己的设置。False(不选中该框)不进行此操作;True(选中该框)则进行此操作。
-
AutoDisableStop — AppVerifier 对于一个错误将只解释一次。如果再次发现该问题,它不会生成错误。False 在每次发现问题时都将生成错误。True 将只生成一个错误。
-
LoggingWithLocksHeld — 将记录 dll 加载/卸载事件。验证器在加载器锁处于保持状态时进行 I/O。这可能会挂起应用程序。False 不会记录该事件,而 True 将记录该事件。
编辑测试属性
有关特定测试的属性的详细信息,参见 AppVerifier 中的 Help 内容。要编辑这些属性,可采用以下两种方法之一:
-
Individual — 在属性窗口中,双击要编辑的属性。将显示一个对话框,允许编辑该项并将其设置回默认内容。更改该项并单击"OK"。
-
Group — 从 Test 区域,单击要编辑的测试或验证层。使用右键单击选项显示包含所有属性的屏幕。更改这些属性或重置为默认内容,并单击"OK"。
日志
运行应用程序而且生成错误时,创建日志。日志为 XML 格式,可以使用浏览器、XML 或 XLST 查看。
何时使用 AppVerifier
您应该在软件开发生命周期(SDL,又称为安全开发生命周期 Security Development Lifecycle)的整个过程中使用 AppVerifier:
-
需求 阶段 — 规划 AppVerifier 的使用,并安排它的执行与后续操作。
-
设计阶段 — 定义将测试哪些组件(模块、Dll 或 EXE)。
-
实现阶段 — 从开发中不同组件的稳定版本(从 Alpha 到 RTM)运行 AppVerifier(单独和共同测试组件很重要)。
-
验证阶段 — 测试人员应该使用 AppVerifier 进行他们的所有测试(手动和自动),因为这是第一次对应用程序进行限制,而且将提交意外的行为和数据。AppVerifier 对于安全咨询师而言也是一个进行审核(黑盒和白盒)的强大工具,因为它能够快速枚举实际(或潜在)的攻击/破坏向量。
-
发布阶段 — 客户和安全咨询师可以对发布的二进制文件使用 AppVerifier,从而识别出潜在的安全漏洞。
-
支持和服务阶段 — 使用 AppVerifier 确保代码更改(例如,错误修复)不会引入回归。
如何使用 AppVerifier
安装和配置
-
安装:从 Microsoft Download Center 下载最新版的 AppVerifier,其中您将发现针对以下支持的体系结构的安装二进制文件:x86、ia64 和 amd64。
-
调试器安装:已验证的应用程序应该运行于用户模式的调试器中或者系统应该运行于内核调试器中,因为出现错误时,将中断调试器。参阅 Help 内容获取有关调试器的详细信息。
-
设置: 对于运行中的进程,不能启用 AppVerifier。需要按照下面描述的方式创建设置,然后启动应用程序。除非显式删除,否则这些设置将一直保留;无论启动应用程序多少次,在启动时都会启用 AppVerifier,除非删除这些设置。
注 AppVerifier 也可以在 Visual Studio 2005 Team System 中使用。
使用 AppVerifier Basic 测试
下面的方案展示推荐的命令行和用户界面选项。这些内容应该在执行代码的所有测试中运行以确保完全覆盖。这些方案的预期目标是,应用程序不 中断调试器,而且所有测试在不启用 AppVerifier 运行时以相同的通过率通过。
-
针对要测试的应用程序(一个或多个)启用验证器,通过:
从命令行: appverif /verify MyApp.exe
从用户界面:
-
a. 右键单击 Applications 区域并单击 Add Application 添加应用程序。
-
b. 从 Tests 区域选择 Basics。
-
c. 单击 Save 按钮。
-
注:
-
/verify 将启用 Basic 测试。
-
如果要测试一个 DLL 应用程序,必须为执行该 DLL 的测试 .exe 文件启用 AppVerifier。
-
单独运行验证层。例如,在一个会话中启用所有 Basic,而在另一个会话中启用所有 LuaPriv 检查。
-
-
-
运行执行应用程序的所有测试。
-
分析遇到的任何调试器中断(一个或多个)。如果发生一个中断,则需要了解中断的原因以及如何修复它。(Help 内容提供关于中断的详细信息以及如何研究它们。)
-
完成时,删除所有设置:
从命令行:appverif /n MyApp.exe
从用户界面:
使用 AppVerifier 低资源模拟(错误注入)
该方案的预期目标是,应用程序不中断该调试器。这意味着没有需要解决的错误。
由于在常规操作中引入随机错误注入,因此这些测试的通过率可能显著降低。
-
针对一个或多个应用程序启用应用程序验证器低资源模拟(错误注入):
从命令行:Appverif /verify MyApp.exe /faults;
从用户界面:
-
a. 右键单击 Applications 区域并单击 Add Application 添加应用程序。
-
b. 从 Tests 区域选择 Low Resource Simulation。
-
c. 单击 Save 按钮。
注 如果要测试一个 DLL,可以在特定的 DLL 上(而非整个过程中)应用低资源模拟(错误注入)。命令行格式如下所示:
appverif /verify TARGET [/faults [PROBABILITY [TIMEOUT [DLL ?]]]]Example: appverif /verify mytest.exe /faults 5 1000 d3d9.dll
-
-
运行执行应用程序的所有测试。
-
完成时,删除所有设置。分析遇到的任何调试器中断(一个或多个)。如果出现中断,需要了解中断的原因以及如何修复它。
-
完成时,删除所有设置:
从命令行:appverif /n MyApp.exe
从用户界面:
注 在应用程序中是否使用错误注入将导致执行完全不同的代码路径,因此为了完全受益于 AppVerifier 必须运行这两个方案。
分析 AppVerifier 数据
在 AppVerifier 分析过程中创建的所有数据存储在一个二进制格式的 %ALLUSERSPROFILE%\AppVerifierLogs 文件夹中。然后,这些日志可通过用户界面或命令行转换为 XML 以供进一步分析。要查看 XML 文件,使用以下方式之一:
-
Web 浏览器 — 现在,所有的 Web 浏览器都能够以结构化以及颜色编码的格式显示 XML 文件。
-
XSL 转换 — 创建一个 XSLT,将原始内容转换为只包含相关信息的报告。
-
导入到 Excel — 将 XML 文件导入到 Excel,使用筛选器或 Pivot 表识别并分析收集的数据。
-
导入到数据库 — 保存 XML 文件,并将其导入到一个数据库中(例如,SQL、Access 数据库等)。
有关 AppVerifier 的要点
本节提供有关潜在 AppVerifier 用户遇到的最常见问题的信息:
系统要求
AppVerifier 旨在测试 Windows XP、Windows Server 2003 和 Windows Vista 上的非托管应用程序(例如,非 .NET Framework 应用程序)。当运行一个完整的页面堆时,建议最少需要 1 GB。
尽管应用程序的符号或调试信息的可用性在所收集数据的质量和有用性方面差异巨大,但 AppVerifier 在执行测试时不需要访问源代码。
AppVerifier 将验证什么?
AppVerifier 用于在 Windows XP、Windows Server 2003 和 Windows Vista 中测试用户模式的应用程序。
-
要测试驱动程序或内核模块,使用 Driver Verifier。有关更多信息,请参阅附录 A:Driver Verifier 以及知识库文章 How to Use Driver Verifier to Troubleshoot Windows Drivers。
-
要测试 Windows CE,使用 Windows CE Test Kit (CETK) 中包含的特定的 Platform AppVerifier。
-
需要注意的重要一点是,AppVerifier 将只测试在测试执行过程中调用的函数。这意味着,如果单元测试可用(从目标应用程序/模块),它们应该用于确保最大的代码覆盖 - 如果调用生成请求的方法,则只检测错误。
AppVerifier 如何工作?
AppVerifier 的工作方式是修改非托管 DLL Method Table,以便在执行实际函数之前执行所需的检查(也称为"函数挂钩")。例如,Win32 API CreateFileA 方法的地址被替换为一个内部的 AppVerifier 方法,后者将触发需要记录的一系列测试。
当新进程启动时,使用 AppVerifier 的 Method Table Hooking 技术将由在特定注册表键中创建的项进行控制。如果存在注册表项,那么 AppVerifier DLL 将加载到新创建的进程中,该进程将在现有的以及后续加载的 DLL 中处理 Method Table 替换。由于这些挂钩在加载 DLL 时生成的,因此在已经运行的进程上使用 AppVerifier 3.0 是可能的。
AppVerifier 用户界面 (UI) 用于控制注册表键设置并提供关于现有日志的信息。当在 UI 中设置了应用程序和测试并单击"Save"按钮时,将进行 Registry 设置。然后需要重新启动该应用程序,这将启动监视操作。需要注意的重要一点是,这些设置将一直持续到应用程序从 AppVerifier 中移除。
当发现一个问题时,验证器将停止。提供的编号用于标识问题发生的实质和原因。要更好地了解停止操作,参见 AppVerifier Help 文件中包含的详细解释:appverif.chm。
页面堆技术细节
为了检测堆损坏(上溢或下溢),AppVerifier 将修改内存分配方式,方法是在分配内存之前和之后使用完整的不可写的页或特殊标记填充所请求的内存。
当使用完整的不可写的页填充所请求的内存时(在页面堆的属性部分启用 FULL 设置并使用默认设置),AppVerifier 将使用大量虚拟内存,而且它具有个优势,即当上溢或下溢发生时,将实时缓存堆损坏事件。谨记,该模式中的内存可能如下所示
[AppVerifier Read-Only Heap Page (4k)] [Amount of memory requested by Application under test] or like this [Amount of memory requested by Application under test] [AppVerifier Read-Only Heap Page (4k)]。
堆检查将根据 Backward 属性在分配的开始或结尾放置一个保护页。如果 Backward 设置为 False(默认值),则它将在分配的末尾放置一个保护页以捕获缓冲区溢出。如果它设置为 True,则该保护页放置在分配的开始以捕获缓冲区的不足。
当使用特殊标记(通过清除堆属性中的"Full"复选框项来启动)填充所需的内存时,如果释放该内存,AppVerifier 将进行检查并向您发出警告。使用这些技巧时的主要问题是,有时内存损坏只能在内存释放时(最小的内存块是 8 字节)进行检测,因此操作一个 3 字节的变量或发生 5 字节溢出时,不会立即检测到它。
对于一个下溢事件,将尝试写入一个只读页。这将引发一个异常。请注意,仅当目标应用程序在调试器中执行时才引发该异常。还需注意,完整的页面堆模式也将检测这些错误,因为它使用填充+保护页。使用轻量级页面堆的原因是,防止计算机无法承受完整页面堆的高内存约束。
对于内存密集型应用程序,或者需要长期使用 AppVerifier 时(例如,压力测试),最好运行常规(轻量)堆测试而非完整的模式,以防止性能降低。但当您遇到问题时,请打开完整的页面堆进行深入研究。