0x00 前言
最近突然心血来潮,想找个样本来分析一下,顺便学习了解一下免杀以及恶意样本的手法,在网上冲浪的时候找到了一个银狐样本,于是拿来分析一下
样本地址:WindowsEvent 病毒木马程序 - 吾爱破解 - 52pojie.cn
0x01 沙箱分析
将整个文件打包压缩,丢到沙箱看看情况
这里看一下沙箱跑出来的执行流程:
可以看到通过这个WindowsEvent.exe创建了命令行任务管理工具,然后外连下载。还有释放文件。
0x02 样本分析
一、WindowsEvent.exe
首先die查查成分:
目测就是使用汇编写的,然后没有导入表,那么可能通过PEB来获取外部函数地址了。废话不多说就直接进入分析。由于是汇编写的,这个入口非常的简单,大概就是下面这样:
init初始化
首先通过init函数来进行初始化
通过decrypt_str函数来进行字符串解密,然后通过自定义的GetDllBase函数获取Kernel32.dll 的基址。然后通过自定义给GetProcAddress函数获取必要的API地址存放到全局变量当中。
decrypt_str
这里来看看这个decrypt_str:
可以看到就是一个简单的异或解密,然后addr第一个元素是字符串的长度,这里直接写一个IDAPython脚本,然后解密一下:
def decrypt(addr):
len = ida_bytes.get_byte(addr)
plain = []
for i in range(0, len, 1):
cipher = ida_bytes.get_byte(addr + i + 1)
plain.append(chr(cipher ^ 0x6A))
print(f"str[{len}] -> {''.join(plain)}")
print("============================")
decrypt(0x4009E0)
decrypt(0x4009ED+1)
decrypt(0x4009FB)
decrypt(0x400A08)
decrypt(0x400A15)
decrypt(0x400A23)
decrypt(0x400A30)
decrypt(0x400A3A)
decrypt(0x400A5E)
print("============================")
解密之后的API信息如下:
===========================
str[12] -> kernel32.dll
str[11] -> CreateFileA
str[11] -> CloseHandle
str[11] -> GetFileSize
str[12] -> VirtualAlloc
str[11] -> VirtualFree
str[8] -> ReadFile
str[14] -> VirtualProtect
str[14] -> CreateProcessA
============================
GetDllBase
通过GetDllBase这个函数来获取制定dll名的基址
这里通过PEB来获取InMemoryOrderModuleList,再通过遍历这个双向链表,然后比较BaseDllName来找到指定的dll,从而获取指定dll的基址。
GetProcAddress
GetProcAddress就是通过自解析PE结构的导出表,来获取到指定API的外部地址。
check_fileName
初始化完成之后会调用check_fileName来检查当前执行的这个文件的文件名是否有被修改:
这个函数比较字符的时候,会将获取到的字符转成大写,然后再进行比较。通过分析可以发现,仅仅只是对进程名当中的indo片进行了校验,只有在这个校验通过的时候才会返回1,否则返回0。然后进程退出
函数功能
这里检查进程名可能是为了绕过分析。有些沙箱或者虚拟分析环境在运行样本时,进程名可能是随机生成的,或者带有特定字样。这里可能是在检测这些环境,并且进行绕过。
start_WinSafe
这个函数会尝试使用schtasks这个计划任务管理器来立即执行这个WinSafe的任务。不过本机上没有这个计划任务。可能是后续恶意代码执行之后才添加的,这里还不太清楚。
execShellCode
恶意程序通过这个函数来解密ShellCode并执行。
由于之前根据解密之后的字符串修复了一下全局变量的名称,这里看就非常的简单了。可以大体流程先是使用VirtualAlloc申请一个内存空间,然后将byte_400AA0这个位置的数据移动到申请的内存空间当中。接着就是对ShellCode当中的数据进行解密,解密算法就是有限域下先 (x + 119) ^ 0x62,最后在使用VirtualProtect修改内存权限,然后直接使用函数指针执行。
这里要注意一下,VirtualAlloc和VirtualProtect这两个函数的参数,通过参数可以分析在VirtualAlloc和VirtualProtect执行的过程中,申请的权限分别是可读可写和可读可执行,而不是可读可写可执行。这样做的目的是绕过大部分杀软的检测。毕竟直接申请RWX内存是非常危险的操作。
通过分析知道了这个ShellCode的解密方法,那么就可以通过动态调试将这段shellCode dump下来分析,或者是直接获取数据然后解密写道文件当中。这里我选择后者,直接写一个IDAPython脚本来解密,然后顺便写到文件当中:
import idc
import idaapi
import ida_fpro
import os
# shellcode
start_addr = 0x400AA0
plain = []
for i in range(0x98B):
ch = ida_bytes.get_byte(start_addr+i)
dec = (((ch + 0x77) & 0xff) ^ 0x62) & 0xff
plain.append(dec)
#print(cipher)
idb_dir = idc.get_idb_path()
dir_path = os.path.dirname(idb_dir)
print(dir_path)
output_path = os.path.join(dir_path, "shellCode.bin")
def save_file(data, filename):
data_bytes = bytes(data)
with open(filename,"wb") as f:
f.write(data_bytes)
return len(data_bytes)
filelen = save_file(plain, output_path)
print(f"数据已保存到 : {output_path}")
print(f"写入 {filelen} 字节")
分析到这里,看起来这个exe已经没有其他逻辑了。
总结
通过上述的分析,这个WindowsEvent.exe其实就是一个ShellCodeLoader,还需要对其中加载的ShellCode进行分析才行。
二、ShellCode部分
字符串信息
在使用IDAPython将ShellCode解密,并且写入文件之后,先进行一些信息收集。首先用strings看看能不能提取出来一些有用的字符串:
发现有些有趣的字符串,以及IP地址和域名。还有最下面一串看起来像是逆序了的结构化配置信息,这里使用python回复一下:
通过AI分析,这个字符串定义了这个样本的连接命令与控制服务器的全部参数,包含了三组独立的C2地址:
- 主 C2(p1):
156.234.119.138:443(t1:1 可能指启用 TLS/SSL 加密)。
- 备用 C2 1(p2):
syumineyt.top:80(t2:1 可能指使用 HTTP 明文)。
- 备用 C2 2(p3):
syumingeyt.top:8080(t3:1 可能指使用 HTTP 或其他协议)。
IDA分析
接下来将这个ShellCode文件丢到IDA当中进行分析
上来首先就是最常见的使用PEB获取DllBase,这里用来判断是否找到正确字符串的方法是 hash 校验,通过计算字符串的hash值与目标字符串的hash值比较,如果比较成功则表示找到了这个字符串,这个也是shellcode当中常见的混淆。
解决方法:解决这种混淆的方法也比较简单。这里有两种方法:
- 首先就是一种比较复杂的方法,可以将hash算法还原一下,然后对一些常见的dll名称进行
hash计算,来校验。对于函数名称的话,可以对目标dll文件进行导出表解析,然后对API名称进行hash计算,找出目标hash对应的API名称
- 另外一种比较简单的方法就是直接动态调试了,通过动态调试来直接获取到目标API的名称。
ShellCode 的动态调试可以自己实现一个Loader来加载这段ShellCode
接着就是通过GetFuncByHash来获取API,
这一部分会逐步获取kernel32.dll 以及 ntdll.dll当中的一些API,具体如下:
GetFuncByHash
这个函数是用来获取外部模块提供的API的,这个函数使用了两种方法来获取API。如果参数2传入的是0,则会自己解析导出表来计算函数地址。而另外一种情况是参数2传入一个函数地址,这个函数就会使用传入的这个函数来获取API地址:
上图的上半部分是解析导出表的代码,这里就不放出来了。
根据上层函数的逻辑得出,在获取了GetProcAddress函数地址之后,后续的都是通过这个GetProcAddress函数来获取函数地址的。只不过在获取之前,还是会解析一遍导出表。
定位资源部分
接下来这个部分,也是笔者第一次了解到,刚看到的时候还有点懵。后来想想是个很不错的设计,这里通过汇编看看逻辑
首先会调用这个gotoNextIns的函数,函数如下,只有短短的两个指令:
将栈顶的值放到eax寄存器当中,此时栈顶是返回地址,也就是这个xor ecx, ecx指令的地址。所以这个函数其实是用了一个非常巧妙的方法,来获取下一条指令的地址。
接着会从这个指令地址遍历内存空间,直到找到一个连续的字符串codemark的位置。这个字符串不正是之前就通过strings找出来的吗?
从IDA当中的这个形式来看的话codemark这个地方以下的就是存放资源的地方,还可以看到ip地址,以及域名字符串。
那么其实这个codemark其实就是一个表示符,用于程序内部定位资源,这样比直接写死地址更加的灵活。
后续部分
在定位资源完成之后,会加载Ws2_32.dll,并且获取这个模块当中的一些API地址,大致如下:
结合这些API名称,根据一些经验分析,这个ShellCode可能也还是一个加载器。而且使用的是分离加载的模式,通过ws2库来下载另外一个ShellCode,然后执行真正的恶意逻辑。
木马下载部分
这个ShellCode使用了两种方式下载ShellCode。一种是原始的TCP协议(offset 0x33A),另一种是使用HTTP协议(offset 0x4DF)。这里使用动态调试跟进一下,看看使用的是哪一个方式。
经过动态调试之后发现使用的原始TCP协议进行下载,这里贴一下在汇编当中打的注释:
最终大概会在这个位置call ShellCode。由于已经连不上c2,无法下载木马,这里就没有没办法验证了。
三、Gurad.dll导出函数分析
既然exe部分已经分析的差不多了,那么这里就来看看这个dll到底干了什么。从导出表入手:
发现导出了一个MonitorAndRestart函数,来看看这个函数:
从这个函数调用API和行为来看,这个函数就是一个典型的持久化 + 守护进程的机制。
单例控制
首先通过创建全局互斥体"Global\\ProcessMonitorDLL_Mutex"来防止重复运行。防止多个监控线程/进程同时存在
监控进程
然后对目标进程进行监控,默认的进程名是WindowsEvent.exe,但是可以通过参数动态传入。
接下来会使用一个while循环来监控和拉起进程。首先通过findTargetProcess函数来检查目标进程是否存在。如果存在了跳转到LABEL_12的地方,休息1秒,然后继续检查。
findTargetProcess函数
这个函数是通过进程快照来检查目标进程是否存在:
如果进程不存在,则使用计划任务管理器来拉起任务:
schtasks /run /tn "WindowsEvent_Task"
启动之后使用WaitForSingleObject来等待5秒,然后获取执行结果。如果执行失败则打印日志,反之再次确认进程是否存在,不存在则记录日志。
如果CreateProcessW执行失败,则会执行:
sub_10001270();
该函数定义如下:
如果CreateProcessW失败,则会调用这个函数,然后提权,并且重启系统。
四、小结
这个样本其实就是个“分工很明确的工具人”。表面上的 WindowsEvent.exe 本身没干啥坏事,就是负责把一段加密的 ShellCode 解出来然后跑起来;真正的活是在后面那段 ShellCode 里,它会自己去系统里找 API、再去外网连服务器,把真正的木马程序下载下来在内存里直接执行(不落地,更隐蔽)。另外还配了一个 Guard.dll 做“保安”,一直盯着这个进程,只要被关掉就用计划任务拉起来,实在拉不起来就直接重启电脑,确保恶意程序还能活着。整体来看就是一个多阶段加载 + 网络下载 + 持久化守护的典型木马框架。