JavaScript使得利用WinDbg进行恶意软件分析更轻松
导语:在本文中,Cisco Talos讨论了新版本WinDbg调试器的数据模型及其与JavaScript和dx命令的新接口。
一、介绍
作为恶意软件研究人员,我们每周花几天时间调试恶意软件来了解有关它的更多信息。我们有几个功能强大且流行的用户模式工具可供选择,例如OllyDbg,x64dbg,IDA Pro和Immunity Debugger。所有这些调试器都使用一些脚本语言来自动执行任务,例如Python或OllyScript等专有语言。在内核模式分析时,确实有一个选项:Windows调试引擎及其接口CDB,NTSD,KD和WinDbg。
遗憾的是,即使WinDbg是最受用户欢迎的,但它也被广泛认为是一个世界上用户友好度最差的调试器。WinDbg命令的学习曲线非常陡峭,因为它将一个不直观且经常相互冲突的命令语法与过时的用户界面相结合。将传统的WinDbg脚本语言添加到此等式中并不会使用户更容易,因为它通过引入自己的特性来创建额外的复杂层。
谢天谢地,Windows 10的新WinDbg预览版使其符合现代编程环境。此预览版包括一个新的JavaScript引擎和一个公开的调试数据模型,通过一组JavaScript对象和函数。这些新功能使WinDbg符合如Visual Studio一样的现代编程环境,使用已经熟悉的用户界面元素。在这篇文章中,我们将讨论这个新版本的WinDbg调试器数据模型及其与JavaScript和dx命令的新接口。
二、调试数据模型
调试器数据模型是一个可扩展的对象模型,它允许调试器扩展以及WinDbg用户界面通过一致的接口访问许多内部调试器对象。与通过数据模型公开的恶意软件分析相关的对象是:
· Debugging sessions
· Processes
· Process environment (ex. Peb and Teb)
· Threads
· Modules
· Stack frames
· Handles
· Devices
· Code (disassembler)
· File system
· Debugger control
· Debugger variables
· Pseudo registers
1.DX显示表达式
所有上述类型的对象都通过新命令dx(显示调试器对象模型表达式)公开,该命令可用于访问对象并使用类似C ++的语法计算表达式,其方式比通过混合MASM和C ++表达式的方式更简单。由于为WinDbg添加了NatVis功能,dx命令的结果以更加用户友好的方式显示,使用DML作为默认输出的直观格式化。探索dx命令的起点就是在WinDbg命令窗口输入dx Debugger,它将显示公开数据模型中的顶级命名空间。这四个名称空间是Sessions,Settings,State和Utility。DML使用超链接生成输出,允许用户只需单击它们即可深入查看各个命名空间。例如,单击Sessions超链接,将执行命令dx -r1 Debugger.Sessions并显示其结果。
从顶级命名空间向下直到进程
如果我们进一步向下几层,也可以使用-r dx命令选项控制,我们将获得所有进程及其属性的列表,包括_EPROCESS内核对象作为Process调试器对象的成员KernelObject公开的字段。早期WinDbg版本的用户肯定会喜欢通过dx命令提供的调查的新便利性。dx命令还支持选项卡完成,这使得导航数据模型更加容易,并允许用户了解操作系统和WinDbg内部,如调试器变量和伪寄存器。例如,要遍历内部调试器变量列表,可以键入dx @ $然后重复按Tab键,它将从$ argreg开始循环遍历所有已定义的伪寄存器。伪寄存器和内部变量很有用,如果我们想避免在dx命令后输入完整的对象路径。可以简单地使用伪寄存器@ $ cursession而不是Debugger.Sessions [0],它指向当前会话数据模型对象。如果需要使用当前进程,只需键入dx @ $ curprocess而不是更长的dx Debugger.Sessions [0] .Process [procid]。
2.LINQ查询
Linq(语言集成查询)是.NET软件工程师已经熟悉的概念,允许用户在通过dx命令公开的对象集合上创建类似SQL的查询。有两种语法可用于为正常的.NET开发创建Linq表达式,但是WinDbg通过dx命令仅支持使用Lambda表达式语法创建查询。Linq查询允许对集合对象进行分割,并提取感兴趣的信息。Linq函数“Where”允许我们只选择满足由作为函数提供的Lambda表达式参数指定条件的那些对象。例如,要仅显示名称中包含字符串“Google”的进程,我们可以键入:
dx @$cursession.Processes.Where(p => p.Name.Contains("Google"))
就像在SQL中一样,“Select”函数允许我们选择要显示集合中对象的哪些成员。例如,对于已使用“Where”函数过滤的进程,我们可以使用“Select”来仅检索进程名及其ID:
dx -r2 @$cursession.Processes.Where(p => p.Name.Contains("Google")).Select(p => New { Name=p.Name, Id=p.Id })
深入一层,进入公开的_EPROCESS内核对象,我们可以选择显示被观察进程所拥有的句柄子集。例如,查找用户模式rootkit隐藏的进程的方法之一是枚举Windows客户端服务器子系统进程(csrss.exe)的进程句柄,并将该列表与使用标准进程枚举命令生成的列表进行比较。之前已列出由csrss.exe创建的进程,我们需要找到csrss.exe进程对象,一旦找到,切换到它们的上下文:
dx @$cursession.Processes.Where(p => p.Name.Contains("csrss.exe"))[pid].SwitchTo()
现在可以运行Linq查询来显示csrss.exe句柄表中存在的进程主模块的路径:
dx @$curprocess.Io.Handles.Where(h => h.Type.Contains("Process")).Select(h => h.Object.UnderlyingObject.SeAuditProcessCreationInfo.ImageFileName->Name)
由于ImageFileName是指向_OBJECT_NAME_INFORMATION类型结构的指针,因此我们需要使用箭头取消引用它并访问包含模块路径的“Name”字段。还有许多其他有用的Linq查询。例如,用户可以根据某些条件对显示的结果进行排序,这类似于Order By SQL子句,或使用“Count”函数计算查询结果。Linq查询也可以在JavaScript扩展中使用,但它们的语法略有不同。我们将在文章中稍后展示一个在JavaScript中使用Linq的示例。
三、WinDbg与JavaScript
我们已经介绍了调试器数据模型的基础知识和用于探索它的dx命令,现在我们可以继续使用WinDbg的JavaScript扩展。Jsprovider.dll是一个原生的WinDbg扩展,允许用户使用Microsoft的Chakra JavaScript引擎编写WinDbg脚本并访问数据模型。默认情况下,扩展不会加载到WinDbg进程空间,因此必须手动完成。这避免了与其他基于JavaScript的扩展的潜在冲突。Jsprovider使用标准命令加载扩展:
.load jsprovider.dll
虽然本文讨论了威胁研究人员在分析恶意软件样本时可能创建的传统脚本,但值得一提的是,JavaScript扩展还允许开发人员创建与现有二进制扩展一样的WinDbg扩展。在官方GitHub存储库提供的WinDbg JavaScript示例中,可以找到有关创建基于JavaScript扩展的更多信息。
WinDbg Preview包含一个用于编写JavaScript代码的全功能集成开发环境(IDE),允许开发人员重构调试实时程序或调查内存转储时的代码。以下WinDbg命令用于加载和运行基于JavaScript的脚本。好消息是,与管理WinDbg脚本的笨拙标准语法相比,处理基于JavaScript的脚本的命令更直观:
· scriptload命令将JavaScript脚本或扩展加载到WinDbg中,但不执行它。
· scriptrun运行加载的脚本。
· scriptunload从WinDbg和调试器数据模型命名空间卸载脚本。
· scriptlist列出所有当前加载的脚本。
1.JAVASCRIPT入口点
根据用于加载脚本的脚本命令,JavaScript provider将调用其中一个预定义的用户脚本入口点或在脚本根级别执行代码。从威胁研究人员的角度来看,有两个主要的入口点。第一个是一种名为initializeScript的脚本构造函数,在执行.scriptload命令时由provider调用。该函数通常被调用来初始化全局变量,并定义常量、结构和对象。在initializeScript函数中定义的对象将使用函数host.namespacePropertyParent和host.namedModelParent桥接到调试器数据模型命名空间。桥接对象可以使用dx命令对数据模型中的任何其他对象进行调查。第二个,甚至更重要的入口点是函数invokeScript,相当于C函数的main。当用户执行.scriptrun WinDbg命令时,将调用此函数。
2.有用的技巧
现在我们假设有一个名为“myutils.js”的脚本,其中保留了一组我们在日常研究中经常使用的功能。首先,我们需要使用.scriptload函数加载脚本。
从用户的Desktop文件夹加载脚本函数
3.WINDBG JAVASCRIPT模块和命名空间
我们用来与调试器交互的主要JavaScript对象是宿主对象。如果我们使用WinDbg预览脚本编辑器,Intellisense选项卡完成和函数文档将帮助我们学习可用的函数名和成员名。
IntelliSense
如果只想进行实验,可以将代码放入invokeScript函数中,每次执行脚本时都会调用它。一旦对代码感到满意,就可以重构它并定义自己的一组函数。在深入研究通过JavaScript接口使用函数之前,建议创建两个基本的辅助函数来在屏幕上显示文本。使用标准的WinDbg命令与调试器交互。它们将有助于与用户交互以及围绕JavaScript中原生不存在的某些函数创建变通方法,但需要它来进行调试。在此示例中,将这些函数命名为logme和exec。它们或多或少只是围绕JavaScript函数的封装器,具有一些额外的优势,即不需要键入完整的命名空间层次结构来访问它们。
帮助函数封装部分JavaScript WinDbg API
在函数exec中,可以看到通过引用host.namespace.Debugger命名空间,我们能够通过JavaScript访问相同的对象层次结构,就像使用WinDbg命令行中的dx命令一样。ExecuteCommand函数执行任何已知的WinDbg命令并以纯文本格式返回结果,可以解析它来获得所需的结果。此方法与流行的基于Python的WinDbg扩展pykd中可用的方法没有太大的不同。但是,Jsprovider优于pykd的地方是大多数JavaScript扩展函数都返回JavaScript对象,不需要任何额外的解析就可以用于编写脚本。
例如,我们可以通过host.currentProcess.Modules来循环访问一组进程模块。循环数组的每个成员都是Module类的对象,可以显示它的属性,在本例中属性值是name。
值得注意的是,Intellisense并不是一定能够显示JavaScript对象的所有成员,这在循环语句中可能非常有用。此循环允许我们遍历打印所有对象成员的name,以便在探索和开发过程中提供帮助。
显示Module对象的成员
另一方面,for-of循环语句遍历对象的所有成员并返回其值。重要的是要记住这两个循环形式之间的区别。
打印加载到当前进程空间的模块列表
我们还可以通过循环加载模块的Process Environment Block(PEB)链接列表来获取已加载模块的列表,尽管这需要通过调用JavaScript函数host.namespace.Debugger.Utility.Collections将链接列表转换为集合。下面是一个函数的完整列表,该函数将已加载模块的链接列表转换为JavaScript模块数组并显示其属性。
function ListProcessModulesPEB (){ //Iterate through a list of Loaded modules in PEB using FromListEntry utility function for (var entry of host.namespace.Debugger.Utility.Collections.FromListEntry(host.currentProcess.KernelObject.Peb.Ldr.InLoadOrderModuleList, "nt!_LIST_ENTRY", "Flink")) { //create a new typed object using a _LIST_ENTRY address and make it into //_LDR_TABLE_ENTRY var loaderdata=host.createTypedObject(entry.address,"nt","_LDR_DATA_TABLE_ENTRY"); //print the module name and its virtual address logme("Module "+host.memory.readWideString(loaderdata.FullDllName.Buffer)+" at "+ loaderdata.DllBase.address.toString(16) + " Size: "+loaderdata.SizeOfImage.toString(16)); } }
此函数包含从进程内存中读取值的代码,方法是访问host.memory命名空间并调用函数readMemoryValues,readString或readWideString中的一个,具体取决于需要读取的数据类型。
4.JAVASCRIPT 53-BIT宽度整型限制
虽然使用JavaScript编程WinDbg与标准的WinDbg脚本相比相对简单,但我们需要注意一些可能会引起一些麻烦的事实。第一个问题是JavaScript整数的宽度限制为53位,这在使用64位值时可能会导致一些问题。出于这个原因,JavaScript扩展有一个特殊的类host.Int64,当我们想要使用64位数字时,需要调用它的构造函数。幸运的是,当发生溢出时,解释器会发出警告。
host.Int64对象有许多函数,允许我们对它执行算术和按位运算。当试图创建一个函数来循环使用后面的帖子中显示的PspCreateProcessNotifyRoutine函数注册的回调数组时,我无法找到一种方法来应用64位宽的位掩码。掩码函数为53位宽,如果掩码宽度超过53,则会产生溢出。
使用53位掩码host.Int64会产生正确的结果,如果更宽则出错
幸运的是,有GetLowPart和GetHighPart函数,它们分别返回64位整数的低32位或高32位。这允许我们应用所需要的And掩码,通过将高32位值向左移32并将低32位加到其中来获取所需的64位值。WinDbg JavaScript实现的53位限制令人烦恼的是,如果WinDbg团队能够找到一种方法来克服它并支持64位数而不诉诸特殊的JavaScript类,那将是非常受欢迎的。
5.JAVASCRIPT中的LINQ
我们已经看到Linq查询如何使用dx命令来访问调试器数据模型对象及其成员的子集。但是在JavaScript中它们的语法略有不同,它要求用户提供返回所需的表达式数据类型或提供匿名函数作为Linq动作函数调用的参数,返回所需的数据类型。例如,对于“Where”Linq子句,返回的值必须是布尔类型。对于“Select”子句,需要提供一个想要选择的对象的成员或一个由查询对象成员的子集组成的新匿名对象。
这是一个使用Linq函数过滤模块列表的简单示例,仅显示名称中包含字符串“dll”的模块,并仅选择要显示的模块名称及其基址。
function ListProcessModules(){ //An example on how to use LINQ queries in JavaScript //Instead of a Lambda expression supply a function which returns a boolean for Where clause or let mods=host.currentProcess.Modules.Where(function (k) {return k.Name.includes("dll")}) //a new object with selected members of an object we are looking at (in this case a Module) .Select(function (k) {return { name: k.Name, adder:k.BaseAddress} }); for (var lk of mods) { logme(lk.name+" at "+lk.adder.toString(16)); } }
6.检查操作系统结构
获取内核函数和结构地址的一个很好的起点是函数host.getModuleSymbolAddress。如果我们需要存储在符号中的实际值,我们需要使用host.memory.readMemoryValues函数反引用地址或者反引用函数单一值。
下面是一个枚举回调的示例,该回调使用PspCreateProcessNotifyRoutinekernel函数注册,该函数注册驱动程序函数,每次创建或终止进程时都会通知这些函数。这也被内核模式恶意软件用于隐藏进程或防止恶意软件的用户模式模块终止。帖子中的示例受到C代码的启发,用于枚举由Matthieu Suiche开发的SwishDbgExtextension中实现的回调。此WinDbg扩展对于分析受内核模式恶意软件感染的系统以及内核内存转储非常有用。代码显示使用JavaScript可以相对容易地实现更复杂的功能。实际上,使用JavaScript开发对于恶意软件研究人员来说是理想的,因为编写代码,测试和分析都可以使用WinDbg预览IDE并行执行。
function ListProcessCreateCallbacks() { PspCreateNotifyRoutinePointer=host.getModuleSymbolAddress("ntkrnlmp","PspCreateProcessNotifyRoutine"); let PspCreateNotify=host.memory.readMemoryValues(PspCreateNotifyRoutinePointer,1,8); let PspCallbackCount=host.memory.readMemoryValues(host.getModuleSymbolAddress("ntkrnlmp","PspCreateProcessNotifyRoutineCount"),1,4); logme ("There are "+PspCallbackCount.toString()+" PspCreateProcessNotify callbacks"); for (let i = 0; i<PspCallbackCount;i++){ let CallbackRoutineBlock=host.memory.readMemoryValues(PspCreateNotifyRoutinePointer.add(i * 8),1,8); let CallbackRoutineBlock64=host.Int64(CallbackRoutineBlock[0]); //A workaround seems to be required here to bitwise mask the lowest 4 bits, //Here we have: //Get lower 32 bits of the address we need to mask and mask it to get //lower 32 bits of the pointer to the _EX_CALLBACK_ROUTINE_BLOCK (undocumented structure known in ReactOS) let LowCallback=host.Int64(CallbackRoutineBlock64.getLowPart()).bitwiseAnd(0xfffffff0); //Get upper 32 bits of the address we need to mask and shift it left to create a 64 bit value let HighCallback=host.Int64(CallbackRoutineBlock64.getHighPart()).bitwiseShiftLeft(32); //Add the two values to get the address of the i-th _EX_CALLBACK_ROUTINE_BLOCK let ExBlock=HighCallback.add(LowCallback); //finally jump over the first member of the structure (quadword) to read the address of the callback let Callback=host.memory.readMemoryValues(ExBlock.add(8),1,8); //use the .printf trick to resolve the symbol and print the callback let rez=host.namespace.Debugger.Utility.Control.ExecuteCommand(".printf \"%y\n\", " + Callback.toString()); //print the function name using the first line of the response of .printf command logme("Callback "+i+" at "+Callback.toString()+" is "+rez[0]); } }
在这里,我们看到了上面提到的64位地址的操作。我们将64位值分成高低32位并分别应用位掩码以避免JavaScript中53bit整数溢出。另一个有趣的点是使用标准调试器命令.printf来执行反向符号解析。尽管JavaScript函数host.getModuleSymbolAddress允许获取所需符号的地址,但在撰写本文时,并没有允许我们从地址获取符号名的函数。这就是解决方法.printf与%y格式说明符一起使用的原因,该格式说明符返回包含指定符号名的字符串。
7.调试脚本
任何流行语言的脚本开发人员都知道,为了成功开发,开发人员还需要一组允许调试的工具。调试器需要能够设置断点并检查变量和对象的值。当我们编写需要访问各种操作系统结构的脚本或分析恶意软件样本时,这也是必需的。WinDbg JavaScript扩展再一次以调试工具的形式提供所需的功能,其命令将对所有WinDbg用户非常熟悉。
通过执行命令.scriptdebug启动调试器,该命令准备用于调试特定脚本的JavaScript调试器。一旦调试器加载了脚本,就可以选择导致调试器停止的事件以及在特定的脚本代码行上设置断点。使用JavaScript调试器中的命令sxe,就像在WinDbg中一样,来定义调试器将在哪些事件之后中断。例如,要中断脚本的第一个执行行,只需键入sxe en。命令成功执行后,可以使用命令sx检查所有可用事件的状态。
Sx显示各种异常的JavaScript调试器中断状态
现在,我们还有机会指定应使用命令bp设置断点的脚本行,就像在标准的WinDbg语法中一样。要设置断点,用户需要指定行号以及行上的位置,例如bp 77:0。如果指定的行位置为0,则调试器会自动在行的第一个可能位置设置断点,这有助于避免计算所需的断点位置。
在行位置0上设置断点将其设置在第一个可能的位置
现在已经设置了所有必需的断点,我们必须退出调试器,这是一个有点不直观的步骤。通过访问WinDbg变量@$scriptContents并调用我们希望调试的脚本的任一函数或者像往常一样使用.scriptrun启动脚本,调用脚本后调试过程继续。当然,使用dx命令访问@$scriptContents变量。
使用@$scriptContents变量启动脚本进行调试
调试器包含自己的JavaScript评估命令??,它允许我们评估JavaScript表达式并检查脚本变量和对象的值。
用于检查JavaScript表达式显示结果的命令
JavaScript调试是正确开发所需的强大工具。虽然它的功能在早期的JavaScript扩展版本中已经足够了,但我们希望随着时间的推移,它的功能将变得更加丰富和稳定,因为WinDbg预览版接近其完整版。
四、总结
我们希望这篇文章能向您提供一些使用官方Microsoft JavaScript WinDbg扩展进行恶意软件分析的非常有用的指南。尽管JavaScript公开的API并不完整,但可以通过封装标准WinDbg命令并解析其输出来解决这些限制。此解决方案并不理想,我们希望将新功能直接添加到JavaScript provider,以使脚本体验更加用户友好。Windows开发团队的调试工具致力于添加新的JavaScript模块,如最近添加了文件系统交互和代码命名空间模块,它为代码分析打开了一整套新的可能性,我们可以在下一篇文章中介绍。感兴趣的读者可以查看Github上的官方示例存储库提供的CodeFlow JavaScript扩展。如果想了解有关使用WinDbg和JavaScript进行恶意软件分析的更多内容,思科Talos将在五月哥本哈根的CARO Workshop上进行展示。
参考
· MASM and C++ WinDbg evaluators
· Linq and the debugger data model
· Debugger data model for reversers
· Debugging JavaScript in WinDbg
· JavaScript debugger example scripts
发表评论