内容简介:最近拿到一个新的HWP样本,样本本身利用的是一个老漏洞,这个样本吸引我们的是shellcode部分。相关漏洞的细节我们在之前的文章中已有描述。需要注意的是,这次的样本和上次的样本在最终的执行流切换方面有一些差异。前一段时间我们曾审计过一些HWP样本,发现不同HWP样本在触发该漏洞后具体的执行流切换上存在4种不同的情况。上次的漏洞分析文章是第1种情况,本次的样本是第2种情况,此外还有2种其他情况,相关的MD5示例如下:这个样本在漏洞触发成功后执行的shellcode让我们眼前一亮,样本在漏洞触发后先执行第1阶
前言
最近拿到一个新的HWP样本,样本本身利用的是一个老漏洞,这个样本吸引我们的是shellcode部分。相关漏洞的细节我们在之前的文章中已有描述。需要注意的是,这次的样本和上次的样本在最终的执行流切换方面有一些差异。前一段时间我们曾审计过一些HWP样本,发现不同HWP样本在触发该漏洞后具体的执行流切换上存在4种不同的情况。上次的漏洞分析文章是第1种情况,本次的样本是第2种情况,此外还有2种其他情况,相关的MD5示例如下:
第1种情况: 33874577bf54d3c209925c9def880eb9 第2种情况: 660b607e74c41b032a63e3af8f32e9f5 e488c2d80d8c33208e2957d884d1e918 (本次调试样本) 第3种情况: f58e86638a26eb1f11dd1d18c47fa592 第4种情况: 14b985d7ae9b3024da487f851439dc04
本次调试环境为 windows7_sp1_x86 + HWP2010英文版 (hwpapp.dll 8.0.0.466) + windbg x86
这个样本在漏洞触发成功后执行的shellcode让我们眼前一亮,样本在漏洞触发后先执行第1阶段shellcode去解密第2阶段的shellcode。在第2阶段的shellcode中,通过hash比对的方式从kernel32.dll中获取功能函数,然后创建 C:Windowssystem32userinit.exe
进程并且在创建时挂起,接着从文档内容中查找标志头,定位到被加密的PE文件数据,随后通过两轮解密解出PE文件,将其写入userinit.exe进程的 0x400000
处,随后修改userinit.exe进程的 Peb.ImageBaseAddress
为新写入的PE文件,并且修改userinit.exe的主线程的线程上下背景文的 Context.eax
为新写入PE文件的 AddressOfEntryPoint
,然后恢复userinit.exe的主线程,从而将执行流切换到注入的PE文件的入口地址,这是一种 Process Hollowing
技术,相关原理在 这个网页 中有描述。这种方法让分析人员较难提取到注入的PE文件,在沙箱中跑时也不会显式drop出PE文件,可以说有效躲避了检测。注入的PE文件启动后,会收集系统信息保存到 %appdata%MicrosoftNetworkxyz
,随后发给远程C2( online[-]business.atwebpages[.]com
),然后在一个while循环中进行等待,如果收集的信息显示当前目标存在价值,远程C2会下发一个动态库保存到 %appdata%MicrosoftNetworkzyx.dll
并使之加载。比较遗憾的是,我们在调试时并没有得到 zyx.dll
。
文档信息
用 HwpScan2
工具打开该文档,先看一下基本属性部分。可以看到原始文档在2016年就已经生成。
原文档是限制编辑的,打开后文档内容无法复制,实际的段落内容被存储在”ViewText”流下,而不是常规的”BodyText”流下:
关于这一点,VB2018的一个 PPT 上有详细的介绍:
Section1和Section2这两个Section里面含有被压缩后的堆喷射数据,在文档打开期间解压后的数据会被喷射到指定的内存。
内存布局
这个样本用到了堆喷射来布局内存,我们在调试器里面看一下堆喷射的具体细节:
sxe ld:hwpapp.dll ... ModLoad: 046f0000 04ad1000 C:Program FilesHncHwp80HwpApp.dll eax=0012ee68 ebx=00000000 ecx=00000006 edx=00000000 esi=7ffdf000 edi=0012eff4 eip=772270b4 esp=0012ef0c ebp=0012ef60 iopl=0 nv up ei pl zr na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246 ntdll!KiFastSystemCallRet: 772270b4 c3 ret 0:000> bp hwpapp+1122f3 ".if(edx == hwpapp+bded0){g;}.else{}" 0:000> g DllMain() : DLL_PROCESS_ATTACH - ABase Start! (d8c.468): C++ EH exception - code e06d7363 (first chance) eax=20142014 ebx=0012f6bc ecx=20142014 edx=20142014 esi=02c86d18 edi=00000098 eip=048022f3 esp=0012ed90 ebp=02d881a8 iopl=0 nv up ei pl nz na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206 HwpApp!HwpCreateParameterArray+0x8c433: 048022f3 ffd2 call edx {20142014} 0:000> !heap NtGlobalFlag enables following debugging aids for new heaps: stack back traces Index Address Name Debugging options enabled 1: 00230000 2: 00010000 3: 01980000 4: 02650000 5: 02770000 6: 02950000 7: 025d0000 8: 028f0000 9: 03250000 10: 028d0000 11: 04e20000 12: 06720000 13: 07440000 14: 07590000 // 可以看到5号堆块几乎被完全用完 0:000> !heap -stat -h 02770000 heap @ 02770000 group-by: TOTSIZE max-display: 20 size #blocks total ( %) (percent of total busy bytes) 42003b0 2 - 8400760 (49.47) 420035c 2 - 84006b8 (49.47) e4 926 - 825d8 (0.19) f0 38a - 35160 (0.08) 194 1cf - 2daac (0.07) 24000 1 - 24000 (0.05) 18 15d9 - 20c58 (0.05) 20000 1 - 20000 (0.05) 100 190 - 19000 (0.04) aa00 2 - 15400 (0.03) 28 6e1 - 11328 (0.03) 10bc0 1 - 10bc0 (0.02) 10000 1 - 10000 (0.02) fda0 1 - fda0 (0.02) fb50 1 - fb50 (0.02) a8 158 - e1c0 (0.02) 4400 3 - cc00 (0.02) 2200 6 - cc00 (0.02) 800 17 - b800 (0.02) 82 13b - 9ff6 (0.01) 0:000> !heap -flt s 420035c _HEAP @ 230000 _HEAP @ 10000 _HEAP @ 1980000 _HEAP @ 2650000 _HEAP @ 2770000 HEAP_ENTRY Size Prev Flags UserPtr UserSize - state invalid allocation size, possible heap corruption 121b0018 84006d 0000 [00] 121b0030 420035c - (busy VirtualAlloc) invalid allocation size, possible heap corruption 242d0018 84006d 006d [00] 242d0030 420035c - (busy VirtualAlloc) _HEAP @ 2950000 _HEAP @ 25d0000 _HEAP @ 28f0000 _HEAP @ 3250000 _HEAP @ 28d0000 _HEAP @ 4e20000 _HEAP @ 6720000 _HEAP @ 7440000 _HEAP @ 7590000 0:000> !heap -flt s 42003b0 _HEAP @ 230000 _HEAP @ 10000 _HEAP @ 1980000 _HEAP @ 2650000 _HEAP @ 2770000 HEAP_ENTRY Size Prev Flags UserPtr UserSize - state 0a6d0018 840078 0000 [00] 0a6d0030 42003b0 - (busy VirtualAlloc) 200c0018 840078 0078 [00] 200c0030 42003b0 - (busy VirtualAlloc) _HEAP @ 2950000 _HEAP @ 25d0000 _HEAP @ 28f0000 _HEAP @ 3250000 _HEAP @ 28d0000 _HEAP @ 4e20000 _HEAP @ 6720000 _HEAP @ 7440000 _HEAP @ 7590000 // 推测Section1和Section2分别被映射了两次,我们来看一下堆喷射的总大小 0:000> ? 42003b0 / 400 / 400 Evaluate expression: 66 = 00000042 // 可以看到堆喷射的大小总大小为264MB,单个堆块大小为66MB,0x20142014地址稳定位于0x200c0030左右开始的喷射区域,所以可以很方便地劫持控制流。 0:000> ? 42 * 4 Evaluate expression: 264 = 00000108
第1阶段shellcode
漏洞触发成功之后,首先跳转到 0x20142014
这个地址,由于前面已经通过堆喷射布局内存,所以执行流可以一路滑行到 0x242bf714
(这里再强调一下,HWP2010并未开启DEP,所以可以直接在堆上执行shellcode)以执行第1阶段的shellcode。下面来看一下shellcode部分。
0:000> u 242bf714 242bf714 52 push edx 242bf715 53 push ebx 242bf716 56 push esi 242bf717 50 push eax 242bf718 57 push edi 242bf719 ba14201420 mov edx,20142014h ...
第1阶段的shellcode的主要目的是定位并解密第2阶段的shellcode。从下图可以看到,第1阶段shellcode通过第1轮循环(loc_A)定位到第2阶段shellcode地址,然后通过第2轮循环(loc_22)去解密第2阶段的shellcode。
我们用 python 模拟了一下上述shellcode的解密过程:
# -*- coding: utf-8 -*- import os import binascii cur_dir = os.path.dirname(__file__) path_encode = os.path.join(cur_dir, "sc_encode.bin") with open(path_encode, "rb") as f: bin_data = f.read() bin_data = binascii.b2a_hex(bin_data) i = 0 j = 0 k = 0 while k < 0x60D: a = ((int(bin_data[i:i+2], 16) & 0x0F) << 4) & 0xF0 b = int(bin_data[i+2:i+4], 16) & 0x0F c = '{:02x}'.format(a + b) bin_data = bin_data[:j] + c[0] + c[1] + bin_data[j+2:] i += 2 * 2 j += 2 * 1 k += 1 path_decode = os.path.join(cur_dir, "sc_decode.bin") with open(path_decode, "wb") as f: f.write(binascii.a2b_hex(bin_data))
实际解密时从下述数据开始:
解密完成后,我们可以得到如下数据:
第2阶段shellcode
获取功能函数
得到解密后的第2阶段shellcode后,就可以愉快地在IDA里进行后续分析了。
第2阶段shellcode上来就是一系列hash,看起来貌似是要通过hash比对搜索功能函数。
一番调试和逆向后,我们明白shellcode封装了一个辅助函数用来查找所需的功能函数:
在 GetFuncAddrFromEATByHash
函数内部,作者用循环右移13位的方式计算hash,并查找满足指定hash的动态库(kernel32.dll)内的满足指定hash的函数,然后将它们的地址保存到栈的指定位置,如上图所示。我们这里用 C语言 还原一下dll的hash的计算过程和api的hash的计算过程:
// 部分代码借鉴自网络,此处表示致谢 #include <stdio.h> #include <windows.h> #define ROTATE_RIGHT(x, s, n) ((x) >> (n)) | ((x) << ((s) - (n))) DWORD GetHashHWPUnicode(WCHAR *wszName) { printf("%S", wszName); DWORD dwRet = 0; WCHAR* wszCur = 0; do { dwRet = ROTATE_RIGHT(dwRet, 32, 0x0D); dwRet += *wszName; wszCur = wszName; wszName++; } while (*wszCur); printf(" function's hash is 0x%.8xn", dwRet); return dwRet; } DWORD GetHashHWPAscii(CHAR *szName) { printf("%s", szName); DWORD dwRet = 0; CHAR* szCur = 0; do { dwRet = ROTATE_RIGHT(dwRet, 32, 0x0D); dwRet += *szName; szCur = szName; szName++; } while (*szCur); printf(" function's hash is 0x%.8xn", dwRet); return dwRet; } int main(int argc, char* argv[]) { GetHashHWPUnicode(L"KERNEL32.dll"); GetHashHWPUnicode(L"KERNEL32.DLL"); GetHashHWPUnicode(L"kernel32.DLL"); GetHashHWPUnicode(L"kernel32.dll"); GetHashHWPAscii("ResumeThread"); GetHashHWPAscii("SetThreadContext"); GetHashHWPAscii("VirtualProtectEx"); GetHashHWPAscii("WriteProcessMemory"); GetHashHWPAscii("GetVersionExA"); GetHashHWPAscii("ReadProcessMemory"); GetHashHWPAscii("TerminateProcess"); GetHashHWPAscii("GetThreadContext"); GetHashHWPAscii("GetLastError"); GetHashHWPAscii("GetProcAddress"); GetHashHWPAscii("GetSystemDirectoryA"); GetHashHWPAscii("GetModuleHandleA"); GetHashHWPAscii("CreateProcessA"); GetHashHWPAscii("GlobalAlloc"); GetHashHWPAscii("GetFileSize"); GetHashHWPAscii("SetFilePointer"); GetHashHWPAscii("CloseHandle"); GetHashHWPAscii("VirtualAllocEx"); GetHashHWPAscii("ReadFile"); return 0; }
定位PE文件并解密
获得需要的功能函数后,shellcode首先通过 GlobalAlloc
函数申请一段内存,用来存储后面将要读入的PE数据。随后,从4开始,遍历句柄,暴力搜索文件大小等于当前hwp文件大小的文件句柄并保存,然后将文件指针移动到 0x9DE1
偏移处,并将大小为 3E40A Bytes
的内容读入之前申请的内存处。
然后,shellcode从读入的文档内容开始搜索两个连续的标志 0x42594F4A
, 0x4D545245
,并将第2个标志结束 +2
的地址处作为PE数据的首地址保存到[ebp-18]处。
可以在HWP文档中定位到相应数据区域:
不过此时的PE文件数据仍为被加密的状态,shellcode随后用两轮解密将解密的PE文件进行解密,相关汇编代码如下:
在理解上述代码的基础上一样可以用python写出解密程序,如下:
# -*- coding: utf-8 -*- import os import binascii cur_dir = os.path.dirname(__file__) path_encode = os.path.join(cur_dir, "pe_encode.bin") with open(path_encode, "rb") as f: bin_data = f.read() bin_data = binascii.b2a_hex(bin_data) i = 2 * 2 while (i / 2) < 0x18400: a = int(bin_data[i-4:i-4+2], 16) b = int(bin_data[i-2:i], 16) c = int(bin_data[i:i+2], 16) c = '{:02x}'.format((a ^ b ^ c) & 0xFF) bin_data = bin_data[:i] + c[0] + c[1] + bin_data[i+2:] i += 2 * 1 i = 2 * 2 while (i / 2) < 0x18400: c = int(bin_data[i:i+2], 16) c = ((c << ((i / 2) & 7)) & 0xFF) + ((c >> (8 - ((i / 2) & 7))) & 0xFF) ^ (i / 2) c = '{:02x}'.format(c & 0xFF) bin_data = bin_data[:i] + c[0] + c[1] + bin_data[i+2:] i += 2 * 1 path_decode = os.path.join(cur_dir, "pe_decode.bin") with open(path_decode, "wb") as f: f.write(binascii.a2b_hex(bin_data[4:]))
解密前的PE数据如下:
解密后的PE数据如下:
创建userinit.exe进程并挂起
得到解密的PE文件后,shellcode做了一系列准备并最终去启动userinit.exe进程,启动时传入 CREATE_SUSPENDED
标志,指明将userinit.exe启动后挂起:
替换userinit.exe主模块
随后shellcode调用 GetThreadContext
获取userinit.exe主线程的线程上下文并保存到栈的指定位置:
接着读取userinit.exe的 Peb.ImageBaseAddress
:
然后动态获取 ntdll!ZwUnmapViewOfSection
,并判断操作系统版本,如果操作系统主版本小于6(相关原理可以参考 这篇文章 ),则调用该API对主模块基地址的内存进行解映射,否则直接跳到后续步骤:
接着shellcode在userinit.exe进程内 0x400000
地址处(即PE文件中写入的进程默认加载基址)申请一片内存,内存大小等于解密出来的PE文件,并先将PE文件的头部写入所申请的内存( 0x400000
):
随后往上述内存区域循环写入PE文件的各个节区:
每写完一个节区后,shellcode获取PE文件中该节区的原始读写属性(通过 Characteristics
字段),并在内存中相应更新这些节区对应的内存属性:
完成上述步骤后,shellcode将userinit.exe进程的 Peb.ImageBaseAddress
域改写为 0x400000
(即注入后的PE基地址),并将线程上下文中 Context.eax
更新为所注入PE的 AddressOfEntryPoint
,这部分的原理可以参考 这篇文章 。
最后恢复userinit.exe的主线程,并关闭刚才打开的userinit.exe进程句柄,从而使主线程去执行 Process Hollowing
后的PE文件,达到偷天换日的目的。相关代码可以参考 这里 。
注入的PE文件
前面我们已经静态解密出了PE文件,我们现在来看一下解密出的PE文件的基本信息,用 pestudio
打开该PE文件,看一下这个PE文件的基本信息:
可以看到该PE文件的编译时间是 2017.12.26 10:13:17
,此外还可以知道该PE文件的链接器版本是9.0。
逆向PE文件
整个PE文件既没有加壳,也没有加花指令,整体逻辑非常清晰明了,拖进IDA基本上就原形毕露了。
PE文件主入口函数如下:
正如函数名所示,它首先调用 AdjustTokenPrivileges
提升自己的权限,然后分别从 Kernel32.dll/Wininet.dll/Advapi32.dll
获取所需的功能函数并保存到全局变量,最后启动一个新的线程,并在10秒后退出当前函数。
(以下几个函数貌似并没有被用到)
来看一下启动的线程干了哪些事情,如下图所示,这个线程的主要目的就是先收集系统信息,并保存到 %appdata%MicrosoftNetworkxyz
,随后将这些信息发送给远程C2,传完之后删除xyz文件。随后进入一个循环,每隔30分钟从远程服务器尝试下载一个 zyx.dll
并保存到 %appdata%MicrosoftNetworkzyx.dll
并尝试加载之。这里推测是C2端需要先判断目标用户是否有价值,然后才决定是否将下一阶段的载荷发送给目标用户。
收集信息部分的代码也很直接,如下:
随后将收集的信息发送给远程C2:
最后,一旦远程dll被下发到目标机器,PE文件会立即加载之,并在3分钟后卸载对应的dll并删除文件。由于我们调试期间并没有获得下发的dll,所以dll里面具体执行了什么逻辑不得而知。
IOC
HWP: e488c2d80d8c33208e2957d884d1e918 PE: 72d44546ca6526cdc0f6e21ba8a0f25d Domain: online[-]business.atwebpages[.]com IP: 185[.]176.43.82
参考链接
https://github.com/m0n0ph1/Process-Hollowing
https://cysinfo.com/detecting-deceptive-hollowing-techniques/
https://blog.csdn.net/lixiangminghate/article/details/42121929
https://www.virusbulletin.com/uploads/pdf/conference_slides/2018/KimKwakJang-VB2018-Dokkaebi.pdf
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 对某HWP漏洞样本的分析
- 首个完整利用WinRAR漏洞传播的恶意样本分析
- 一个CVE-2017-11882漏洞新变异样本的调试与分析
- python数据分析于实现,单样本体检验、独立样本体检验、相关分析、列联表分析!
- 鬼影样本分析
- 恶意样本分析手册——文件封装篇
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Defensive Design for the Web
37signals、Matthew Linderman、Jason Fried / New Riders / 2004-3-2 / GBP 18.99
Let's admit it: Things will go wrong online. No matter how carefully you design a site, no matter how much testing you do, customers still encounter problems. So how do you handle these inevitable bre......一起来看看 《Defensive Design for the Web》 这本书的介绍吧!