01
✦
攻击描述
✦
近日,山石网科情报中心安全人员针对国内银狐黑产团伙样本的攻击手法进行了分析。该团伙通过钓鱼手段诱导受害者点击制作的钓鱼文档并加载多段恶意载荷,最后释放Ghost远控木马魔改的C2感染受害者机器。
02
✦
详细分析
✦
第一阶段
JS攻击载荷分析
看到内嵌在CHM文档中有存在一个test.html文件,该文件中内嵌了银狐恶意jscript脚本
med0x2e/GadgetToJScript (https://github.com/med0x2e/GadgetToJScript)
银狐团伙极有可能使用的是GadgetToJScript开源.NET转JS加载项目,下图左侧为该项目的JS加载器模板,右侧为银狐实际生成的JS样本。该团伙修改了部分逻辑用于加载不同位数的.NET DLL
该项目在自述中也详细描述了使用BinaryFormatter类时会触发Activator.CreateInstance()调用
Activator.CreateInstance()函数是用于调用要反射加载的的序列化示例的构造函数。这也就意味着加载到内存中的.NET恶意实例是以构造函数为入口点执行的。
我们通过项目中提供的入测试DLL也可以验证这一点
对比恶意样本中的DLL,也同样是利用了构造函数执行恶意代码逻辑。
该项目还提供除js以外的其他脚本加载形式,也需注意防范
我们主要梳理下本次银狐使用的js脚本加载反序列化恶意样本流程。
首先银狐为自己的恶意Payload添加Bypass命令以尝试绕过高版本.NET无法加载序列化对象的补丁。
具体的实现方法是利用反序列化对象在内存中找到静态对象检查的属性,将属性修改后达到关闭微软设置的序列化检查的目的从而实现bypass,最后利用TextFormattingRunPropertiesMarshal加载触发关闭检测逻辑
我们在高版本的.NET framework源码中GetObjectData函数中也能看到微软加了disableActivitySurrogateSelectorTypeCheck值防止代码滥用反序列化加载。
紧接着GadgetToJScript会读取黑客指定的DLL路径将目标DLL以二进制数据读取到内存
从资源模板中将两个stage替换到模板中便完成了第一阶段载荷,首先判断.NET Framework版本,加载第一段用于关闭类型检查的序列化实例用于bypass后续的二阶段载荷,然后再通过触发的异常加载第二段序列化过的恶意DLL完成第一阶段攻击
但是银狐团伙在使用该段JS却简单修改了这段逻辑,增加了版本判断,优先加载64位DLL,出现异常后再加载32位DLL
第二阶段
本地生成配置信息
并通过内存加载stage3下载器
进入Program的构造函数,可以看到儿一样本对基础配信信息的初始化,生成输出目录配置、C2服务器配置等
输出配置目录在C:USERSearches32-64随机字符串目录下,并且生成两个由进程id重复组合的配置文件
完成配置文件生成后会调用Load2()函数加载第三阶段的恶意dll
该段函数主要在内存中申请一段空间后将stage3的恶意pe加载到申请的内存中,然后反射加载stage3。
stage3的恶意代码数据存储在.net的静态全局数组中,分为32位和64位。
转换成IL代码可以看到两段静态数据的token和size
token对应的静态数据
import clr,sys,os
import json
clr.AddReference("dnlib")
clr.AddReference("System")
clr.AddReference("System.IO")
from System import Random,Environment,MarshalByRefObject
from System.IO import Path
import dnlib
from dnlib.DotNet import *
from dnlib.DotNet.Emit import OpCodes
SAMPLE_PATH = r"stage2.bin"
module = dnlib.DotNet.ModuleDefMD.Load(SAMPLE_PATH)
str_tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
mw_cfg = {"outDirectory":'',"urlPath":'',"bundleConfig":''}
def GenerateRandomString(length):
random = Random()
tmp_random_str = ''
#stringBuilder = StringBuilder(length)
for i in range(0,length):
num = random.Next(len(str_tab))
tmp_random_str += str_tab[num]
return tmp_random_str
def GetFileNameIdentifier():
chr = '@'
num = sys.argv.__len__()
for i in range(0,num):
tmp_path = sys.argv[i]
if chr in tmp_path:
return tmp_path.split('.')[0]
return ''
def IdentifierOR(fileName):
fileNameIdentifier = GetFileNameIdentifier()
if fileNameIdentifier != '':
return fileName
return fileNameIdentifier
def getField(instructions):
foundArr = False
for ins in instructions:
if ins.OpCode == OpCodes.Newarr:
foundArr = True
if (ins.OpCode == OpCodes.Ldtoken) & foundArr:
return ins.Operand
return None
def get_array_data_by_token(token):
for type in module.GetTypes():
if type.get_HasFields():
for field in type.get_Fields():
if field.get_MDToken() == token:
try:
return bytes(field.get_InitialValue())
except:
return None
return None
count = 0
for type in module.GetTypes():
if type.Namespace == "Shellcode":
for method in type.Methods:
if method.Name == ".cctor":
field = getField(method.Body.Instructions)
if field == None:
continue
mw_data = get_array_data_by_token(field.MDToken)
if mw_data != None:
count = count + 1
shellcodename = "shellcode"+str(count)+".bin"
with open(shellcodename,"wb+") as file:
file.write(mw_data)
file.close()
print(shellcodename + " " + "has been dump " + "file size is ",hex(field.GetFieldSize()))
taskList = {}
def AddTask2(remotePath,fileName):
tmp_remotePath = mw_cfg['urlPath'] + remotePath
tmp_outputPath = Path.Combine(mw_cfg['outDirectory'],fileName)
taskList[tmp_remotePath] = tmp_outputPath
for mtype in module.GetTypes():
if mtype.Namespace == "TestAssembly":
for method in mtype.Methods:
if method.Name == ".ctor":
for ptr in range(len(method.Body.Instructions)):
ins = method.Body.Instructions[ptr]
if ins.OpCode == OpCodes.Stfld:
prev_ins = method.Body.Instructions[ptr-1]
if (prev_ins.OpCode != OpCodes.Newobj):
if (prev_ins.OpCode == OpCodes.Ldstr) & (str(ins.Operand.Name) in mw_cfg.keys()):
mw_cfg[str(ins.Operand.Name)] = prev_ins.Operand
if (prev_ins.OpCode == OpCodes.Call) & (str(ins.Operand.Name) in mw_cfg.keys()):
if prev_ins.Operand.Name == "Combine":
tmp_path = Path.Combine(Path.Combine(Path.Combine("C:\Users", Environment.UserName), "Searches"), GenerateRandomString(Random().Next(32, 64)))
mw_cfg['outDirectory'] = tmp_path
continue
if prev_ins.Operand.Name == "Concat":
text = Path.Combine(Path.GetTempPath(), GenerateRandomString(Random().Next(32, 64)))
mw_cfg['bundleConfig'] = mw_cfg['outDirectory'] + "TG.exe=" + Path.Combine(text, "TG.exe")
AddTask2("libcef.exe", "svchost.exe");
AddTask2("libcef.dll", "libcef.dll");
AddTask2(IdentifierOR("libcef") + ".png", "libcef.png");
AddTask2("decod.exe", "decod.exe");
AddTask2("cache.dat", "cache.dat");
print(taskList)
js = json.dumps(taskList)
with open("mw_download.json","w") as file:
json.dump(js,file)
print("malware download config file has been export current directory")
js = json.dumps(mw_cfg)
print(js)
with open("mw_cfg.json","w") as file:
json.dump(js,file)
print("malware config file has been export current directory")

我们可以从提取的配置文件中得知恶意程序后续会从指定域名下载5个恶意程序,但是该样本只下载到了3个
第三阶段
恶意下载器执行
下载器是c++编写,存在大量的对象使用,代码并不直观,下面是调整后的代码,大概逻辑为读取配置文件,删除下载器自身实现隐藏,然后根据配置文件解析下载恶意程序并启动。
会为每个下载行为创建一个线程来执行
该下载器会在虚表中构建两个初始化类对象,用于加载不同的模块
可以看到com相关结构体存储了ole32.dll模块基址和四个com组件相关函数,CoInitialize、CoInitializeEx、CoUninitialize、CoCreateInstance
网络方面加载了urlmon.dll模块,使用URLOpenBlockingStreamW函数从配置文件中读取黑客服务器上的恶意软件流
读取到流量后调用CPP的IOStream::Read函数将恶意数据写入文件
随后样本进入一个函数,该函数通过向资源管理器发送消息隐藏窗口并模拟点击启动下载的恶意软件。
首先会调用函数判断是否已经获取了资源管理器的句柄,如果是则关闭父进程窗口
然后调用ShellExecuteA函数打开下载的样本目录
然后查找资源管理器窗口获取资源管理器的窗口句柄
最后模拟鼠标点击启动后续恶意样本
样本一共下载了3个stage4样本,利用白加黑方式加载执行。
第四阶段
白加黑执行Ghost远控
查看svchost.exe详细编译信息如下
发现该白文件默认依赖libcef.dll,银狐团伙正是利用这点实现了dll相关函数的劫持。
伪装的恶意libcef.dll劫持了白进程svchost的导入函数,每次svchost白进程执行时都会触发恶意dll的行为逻辑。
查看导出函数实际都跳转到了同一个函数,该函数用于解密PNG中的加密数据,并通过内存加载DLL的方式将C2加载到内存执行
加载器整体逻辑如下,读取libcef.png数据向下偏移16字节数据为解密数据
隐藏在png中的加密c2.dll加密数据
使用自定义的解密算法将数据解密
解密后加载器开始常规的内存加载,将PE数据加载到申请的内存空间
为每个节区分配内存
修复重定位
修复导入函数表
设置每个节数据的保护属性
最后跳转到Dll Entrypoint完成加载
完成加载后调用fuckyou导出函数完成C2上线
C2部分
在字符串表中可以看到项目目录有Ghost字样,样本很可能由Chost开源木马修改。这里仅作简单分析
使用IOCP协议
通过注册表中设置GUID判断是否已经完成感染
不存在GUID则设在HLKMSOFTWAREmachine_guid下设置guid
解除浏览器占用,并收集浏览器用户数据
盗取国内主流浏览器中的用户信息
清理火狐、IE等浏览器数据。可能目的迫使受害者使用期望浏览器
配置管理员UAC
检测关闭杀软
设置自启动注册表键值
伪装Windows update.exe
键盘监控
链接服务器IP103.210.237.33
山石情报中心已收录相关样本,用户可以在山石网科威胁情报中心云瞻中查看相关IOC信息:
03
✦
处置建议
✦
用户可以通过使用山石网科的防火墙和NIPS产品,利用其C2和AV防护模块来提高网络安全,从而降低潜在风险。为确保最佳保护,用户应及时升级特征库,确保其包含最新安全特征信息。山石网科的防火墙不仅提供了先进的网络安全防护功能,还集成了态势感知云景的智能能力,可以高效地监测和拦截与APT组织相关的威胁情报(IOC)和恶意行为。此外,它还能够快速响应和拦截与当前安全趋势、安全事件和相关样本有关的威胁。
04
✦
失陷指标
✦
06ed2c30954614fe1e8e9e8bd4619510
d1a88258376133409e0df56740683d30
0a5b0607f6db1e8c9e3d2ca0da5c8d59
b2d085ab9171d577f8b36cf58090278b
https[:]//muchengoss[.]oss-cn-hongkong[.]aliyuncs[.]com
https[:]//muchengoss[.]oss-cn-hongkong[.]aliyuncs[.]com/libcef[.]exe
https[:]//muchengoss[.]oss-cn-hongkong[.]aliyuncs[.]com/libcef[.]dll
https[:]//muchengoss[.]oss-cn-hongkong[.]aliyuncs[.]com/[.]png
https[:]//muchengoss[.]oss-cn-hongkong[.]aliyuncs[.]com/libcef[.]png
https[:]//muchengoss[.]oss-cn-hongkong[.]aliyuncs[.]com/decod[.]exe
https[:]//muchengoss[.]oss-cn-hongkong[.]aliyuncs[.]com/cache[.]dat
https[:]//muchengoss[.]oss-cn-hongkong[.]aliyuncs[.]com/TG[.]exe
103.210.237[.]33:65422
05
✦
关于山石网科情报中心
✦
山石云瞻威胁情报中心:
https://ti.hillstonenet.com.cn/
山石云影沙箱:
https://sandbox.hillstonenet.com.cn/
原文始发于微信公众号(山石网科安全技术研究院):银狐黑产团队钓鱼攻击过程及样本详细分析