首页
社区
课程
招聘
[原创]自动化提取恶意文档中的shellcode
发表于: 2022-12-16 20:15 16592

[原创]自动化提取恶意文档中的shellcode

2022-12-16 20:15
16592

该shellcode提取器的应用对象是Maldoc,通过将市面上存在的相关分析工具进行组合,形成工具链,达到自动化定位及提取shellcode的目的。

实现并不复杂,本人认为该工具的实现思路是它的闪光点。实现思路,即将分析人员手动定位并提取shellcode的步骤自动化。
整体流程主要由解包、定位、提取这三个环节组成,后续的优化并不会改变整体流程。

通过oleid检查maldoc的基本信息,并根据其中Container format字段的键值判断该文件的具体类型,根据类型进入不同的分支,但最终都会走入OLE对象处理分支。

通过oleid检查maldoc的基本信息,根据其中的Encrypted字段的键值判断该文件是否被加密。

若该文件被加密,可以通过msoffcrypto-crack对密码进行爆破,并保存完成解密的文件。

本人对RTF文件的处理主要分为静态和动态两种形式,其中静态的优点在于可以不依赖于Windows环境,直接基于REMnux来完成OLE对象及其中shellcode的提取操作,但面对一些经过特殊混淆的RTF文件时,可能不能有效识别其中的OLE对象,因而本人基于frida实现了一个动态定位并获取OLE对象的工具,该工具的灵感来源于Denis O'Brien大佬的一篇文章(Reference中最后一项)。

静态方法主要利用Didier Stevens的rtfdump工具中最新更新的“-F”选项,其在blog中说到,该选项是由“-H”和“-S”演变过来的,简单根据源码说一下本人对这两个选项功能的理解。
“-H”和“-S”主要由HexDecode这个函数来实现,其功能是对字节流进行处理,并将其转换为十六进制的形式,其中包含判读奇偶和补零操作(“-S”)。虽然一个正常的hex流其肯定应该是偶数位的,这样才有正确解析的可能,但由于一些RTF解析器会对hex流为奇数位(有些maldoc构造者为了逃避检测会在hex流最后的位置额外添加一个十六进制字符)的问题进行处理,以完成解析,因此该函数中的判断奇偶和补零操作,应该就是为了正确解析进行了这种混淆操作的字节流;同样其也可以对抗在hex流中添加了大量空白字符这种混淆手法。本人对RTF的混淆方式了解还不是很全面,难免会有纰漏,欢迎指正和补充。
具体源码如下:

通过“-F”选项找到所有OLE对象后,将所有OLE对象的标识保存下来,为后续提取做准备。

将所有找到的OLE对象,根据标识定位并保存下来,传给OLE对象处理模块,进行后续处理。

通过运行与hook相结合的方式,可以让我们不去考虑去混淆的问题,直接找到OLE对象并将其进行保存就可以,而Denis O'Brien大佬的Silver Bullet方法给我了用运行和hook解决该问题的灵感和方法论。
本人所实现的hook是基于frida的,整体实现十分简洁,准备部分是由python实现的,hook部分是用js实现的,python部分没啥可说的,很容易理解,可以看一下frida官方给出的例子,以及DarunGrim的Using Frida For Windows Reverse Engineering这篇文章,且这篇文章写的非常精彩,它为我们提供了基于frida hook对office宏指令进行有效处理的方法,值得学习。那我们来看一下hook的实现吧。
要想获取到OLE对象,OleConvertOLESTREAMToIStorage是关键API,我们来看一下Microsoft给出的详细解释,该API功能是将OLE对对象的格式从OLE1转换到OLE2,其中一定包含了我们想要的OLE对象信息。

因而我们可以hook该API,并通过该API的第一个参数lpolestream(其是指向包含OLE1格式的OLE对象的指针)找到OLE对象,再根据存储结构找到OLE对象的具体位置及OLE对象的整体长度即可,具体操作请看Silver Bullet方法,大佬写的十分清晰且详细,复现非常简单,本文也不再进行过多赘述,hook的具体实现请看本人写的js代码,主要就是进行了获取并保存OLE地址和OLE长度的工作。
js代码如下:

通过复现Silver Bullet,我也发现了一个小问题,即通过API参数获取到的OLE对象地址(0x0A6A5B28),并未直接指向OLE对象的标识头(红框中十六进制字节),若直接根据地址和长度对该块内存进行dump,所获取的文件并不能直接传入本人所写的shellcode提取器,这是由于文件头的问题,导致oledump无法对该OLE对象进行正确解析,后续的操作也就无从谈起。

通过查阅frida文档,我发现可以用frida提供的内存数据匹配来实现精确拷贝,将OLE对象头作为匹配所用的模式串即可解决该问题。
js代码如下:

OOXML文件其实是个压缩包,若其中存在OLE文件也仅能存在于activeX1.bin文件中,因而我们可以通过由Didier Stevens编写的zipdump来判断目标OOXML文件中是否包含.bin文件,若包含.bin文件则直接进入处理OLE对象流程中。

先通过oleid获取所有maldoc的所用stream标签,该处实现还有优化的空间,可以将一维list改为二维list来存储stream标签,这样当maldoc中存在多级stream的情况时,结构更为清晰,不优化的话也可以,结果上不会存在太多差异。

再根据获取到的stream标签,对maldoc中的每一个子stream都进行检查,通过xorsearch输出中的Socre值来判断对应子stream中是否包含shellcode,目前只去得分最高者进行后续操作。该处可以优化为:有得分即保留,都进入到下一阶段。

这次我们将目光转向xorsearch输出结果的偏移值上,将出现的所有偏移值进行保存。

再利用偏移值和scdbg进行测试,确定shellcode的具体开始位置。

若scdbg可以判断出shellcode的具体位置,那我们通过cut-bytes对其进行精确切割,并将结果保存成文件。

若scdbg不能确定shellcode的具体位置,则将整个子stream保存为文件,作为最终的shellcode。

提供word路径,样本文件路径,以及hook脚本文件即可。

提供maldoc文件或OLE文件即可,以刚刚动态获取到的OLE文件为例。

对得到的shellcode(final_shellcode_file)进行一下简单的验证。
静态:可以看到其中有LoadLibraryW和GetProcAddress,看到这俩函数可以确定,其在获取目标函数的地址,方便后续使用;又看到ExpandEnvironmentStringsW,可以确定其是将其中的“%APPDATA%”进行扩展;随后再调用URLDownloadToFileW来下载后续文件,写入指定路径中。

动态:选择scdbg进行模拟执行来获取更加准确的信息,通过shellcodedbg执行后的结果,可以更加清晰地看到,其调用了哪些函数,参数是什么,整体流程与上文通过静态分析得到的结果基本一致,但模拟执行向我们展示了准确的URL,以及从远端下载了什么。

可以看到通过两者结合我们快速且精确的得到了maldoc中的shellcode,并通过静态或模拟执行来获取到其主要行为,快速结束maldoc的前戏,将主要精力放在分析后续的主体行为上,提高效率,愉悦心情,哈哈。

该工具目前只是一个雏形,逻辑较为简单,且可能存在不严谨之处,亦存在一定问题和优化空间,主要是提供一种从特例中提取特征,从而向普适化转变的思路,在此也十分感谢这些原始工具的创作者,在提供基础功能的同时,也给我了我很多向前的思路,希望可以像这些大佬一样,持续、高效的输出成果,做一点想做的事情出来。
一晃也正式工作快半年了,这个小工具也算是给我这段生活和2022交一份答卷了,想做的事情很多,也做了很多尝试,但因能力和精力原因有许多问题无法解决,导致了卡在半途难以向前,幸运的是有了做这个小工具的契机,也十分幸运的解决了大部分问题,并最终完成了它。也希望未来有机会和能力把那些没研究明白的东西整明白,再产出一些成果。
最后,青春不过几届世界杯,从08奥运会上的初识,到如今的卡塔尔,真是岁月匆匆啊,祝愿我梅老板今年可以梦圆卡塔尔。

  • 语言:python + javascript
  • 环境:REMnux docker + win7 (注:不使用RTF中的动态方法,可以摒弃win7,但面对一下经混淆的RTF文件可能无法正确提取shellcode)
  • The docker image(g0mx/remnux-shellcode_extractor) created by myself for extracting shellcode from maldoc based on REMnux

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2023-7-18 09:29 被g0mx编辑 ,原因: 新增基于Remnux docker构建的portable运行环境
收藏
免费 9
支持
分享
最新回复 (6)
雪    币: 227
活跃值: (45)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2

ole32!OleConvertOLESTREAMToIStorage实际数据可能是0x0A6A5B88开头这里,格式在[MS-OLEDS]里有提到。

而且里面的对象并不能保证都是CFB文件(D0CF11E0开头)。
例如之前比较常见的,文件里嵌一个Package释放到临时目录,再用FileMoniker去调起文件的方法,Package的数据头就是02XXXXXX。

之前我是用WinDbg自动提取这个对象的数据的,记录的内容是这样的:


------

用WinDbg附加到WINWORD.exe,然后执行下面的命令(需要在C盘根目录下建立一个名为MemoryDump的文件夹)。

.symopt +0x100; r $t0 = 0; bc *; bu ole32!OleConvertOLESTREAMToIStorage; g; .while (1) { aS /c ${/v:filename} .printf "C:\\MemoryDump\\Dump_%i.OLEObject", $t0; .block { .writemem ${filename} poi(poi(poi(esp+4)+8)) L dwo(poi(esp+4)+c) }; al; ad *; r $t0 = $t0 + 1; g; }


详细解释

.symopt +0x100; 关闭不必要的符号加载 否则可能会把输出的错误信息写到aS定义的别名里
r $t0 = 0; 使用寄存器t0作为计数器
bc *; 清除所有断点
bu ole32!OleConvertOLESTREAMToIStorage; 对OleConvertOLESTREAMToIStorage下延迟的符号断点
g; 恢复执行

.while (1)
{
    aS /c ${/v:filename} .printf "C:\\MemoryDump\\Dump_%i.OLEObject", $t0; 定义一个别名filename, 值为要写入的文件路径
    .block { .writemem ${filename} poi(poi(poi(esp+4)+8)) L dwo(poi(esp+4)+c) }; 将OLE对象的内容写入到文件
    al; 显示别名(调试用)
    ad *; 清除别名
    r $t0 = $t0 + 1; 计数器加1
    g; 恢复执行
}


写入到文件的内容为OLEObject结构,在[MS-OLEDS]里有详细的定义。下面是一个简单的说明。
该结构的头部定义如

- offset -   0 1  2 3  4 5  6 7  8 9  A B  C D  E F  0123456789ABCDEF
0x00000000  0105 0000 0200 0000 1800 0000 4d73 786d  ............Msxm
0x00000010  6c32 2e53 4158 584d 4c52 6561 6465 722e  l2.SAXXMLReader.
0x00000020  362e 3000 0000 0000 0000 0000 0006 0000  6.0.............


第一个DWORD是OLEVersion,即OLE对象的版本号,通常为01050000。
第二个DWORD是ObjectType,即OLE对象的类型。
01表示LinkedObject(链接对象),02表示EmbeddedObject(嵌入对象),大多数情况是02。

接下来是3个LengthPrefixedAnsiString结构。
这个结构首先用一个DWORD用于表示后面的字符串长度,随后是一个该长度的以\0结尾的字符串。如果长度为0,则后面的字符串不存在。

第一个LengthPrefixedAnsiString结构是ClassName,用于描述对象的类别。
当ObjectType为02时,后两个LengthPrefixedAnsiString结构的字符串长度均为0。

如果ObjectType为02,在这3个LengthPrefixedAnsiString结构的数据就是OLE对象的实际数据(NativeData)了。

NativeData以一个DWORD开头(数据长度,设为length),随后的length个字节是数据(如果这个数据是02开头,通常是Package,如果是D0CF11E0就是复合文件结构了)。


------


对应到图里的内容的话,就是这样的:

0x0A6A5B88:
EA930B52                            | 可能是修改过的OLE头, 一般来说这个值是01050000
02000000                            | 0x02, EmbeddedObject
10000000                            | 第一个LengthPrefixedAnsiString, 0x10, 随后的0x10个字节是一个以00结尾的ANSI字符串
34344A55 664C6D79 5A493858 48586300 |
00000000                            | 第二个LengthPrefixedAnsiString
00000000                            | 第三个LengthPrefixedAnsiString
00100000                            | NativeData的大小, 0x1000(4KB)
D0CF11E0 A1B11AE1 ...               | NativeData


最后于 2023-5-29 09:36 被ranni0225编辑 ,原因: 格式修改
2023-5-27 09:04
0
雪    币: 2034
活跃值: (3561)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
3
ranni0225 ole32!OleConvertOLESTREAMToIStorage实际数据可能是0x0A6A5B88开头这里,格式在[MS-OLEDS]里有提到。而且里面的对象并不能保证都是CFB文件(D0CF1 ...
非常感谢您的回复与指正,是否有对应的样本呢,我这边想研究一下。
2023-5-29 09:20
0
雪    币: 3573
活跃值: (31026)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
感谢分享
2023-5-29 09:23
1
雪    币: 2034
活跃值: (3561)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
5
ranni0225 ole32!OleConvertOLESTREAMToIStorage实际数据可能是0x0A6A5B88开头这里,格式在[MS-OLEDS]里有提到。而且里面的对象并不能保证都是CFB文件(D0CF1 ...
这里以“D0CF11E0”作为标识去寻找的原因是,在下一步处理中用到了“oledump”这个解包脚本,该脚本会对OLE对象进行解包,因而我只需提取完整的OLE对象(完整)即可。
2023-5-29 09:27
0
雪    币: 227
活跃值: (45)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
g0mx 这里以“D0CF11E0”作为标识去寻找的原因是,在下一步处理中用到了“oledump”这个解包脚本,该脚本会对OLE对象进行解包,因而我只需提取完整的OLE对象(完整)即可。
直接用写字板在RTF里插入一个Package对象应该就可以,不用特意构造的。方便留个联系方式么O.O。
2023-5-29 09:30
0
雪    币: 2034
活跃值: (3561)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
7
ranni0225 直接用写字板在RTF里插入一个Package对象应该就可以,不用特意构造的。方便留个联系方式么O.O。
可以的,我的邮箱g0mx_xm@163.com
2023-5-29 10:27
0
游客
登录 | 注册 方可回帖
返回
//