BlackMatter

BlackMatter 勒索软件分析

1. 前言

一个新的勒索软件团伙BlackMatter于 2021 年 7 月在地下论坛/Exploit//XSS/上招募附属机构。他们填补了在 Colonial Pipeline 攻击后DarkSide关闭以及REvil在 7 月中旬击败 Kaseya 后消失所留下的空白。该团伙表示:他们既不是DarkSide也不是REvil的继任者。宣称BlackMatter是将勒索软件LockBitREvilDarkSide各自的优势结合起来的最好的勒索软件。

IOC

SHA256 : 22d7d67c3af10b1a37f277ebabe2d1eb4fd25afbd6437d4377400e148bcc08d6

你可以在MalwareBazaar下载。

赎金票据

勒索信的内容在BlackMatter的配置中进行了加密,并动态解密写入到每个目录的勒索信文件中。赎金票据文件名的形式为:**.README.txt

2. 动态解析API

因为结合了诸多勒索软件的优点,那么动态解析API和基本的字符串加密是必不可少的,接下来就让我们来看看它到底是怎么做的。

IDA打开样本,首先便是动态解析API,函数如下:

2.1. 哈希计算

实际调用是使用Get_Func_Addr_By_ROR13_Hash导入API函数的hash进行函数加载。Get_Func_Addr_By_ROR13_Hash函数中可以看到首先确保LoadLibraryAGetProcAddress已加载,然后再进行hash比对:

hash计算采用了循环右移0xD位的方式,Get_Dll_Name_Hash函数内容如下:

采用python代码重写就美观了许多:

def Calc_Dll_Hash(dll_name):
    mask = 0xFFFFFFFF
    result = 0
    for each in dll_name+'\x00':
        each = ord(each)
        if(each > 0x40 and each < 0x5b):
            each = each | 0x20
        result = (result >> 0xd) | (result << 0x13)
        result = (result+each) & mask
    return result

Get_Func_Name_Hash函数略有不同,但大同小异:

python代码如下:

def Calc_Func_Hash(dll_name, func_name):
    mask = 0xFFFFFFFF
    result = dll_name
    for each in func_name+'\x00':
        each = ord(each)
        result = (result >> 0xd) | (result << 0x13)
        result = (result+each) & mask
    return result

检验代码的有效性

实际上要调用的函数散列可以如下这般进行计算:

dll_name = "kernel32.dll"
func_name = "LoadLibraryA"
print(hex(Calc_Func_Hash(Calc_Dll_Hash(dll_name), func_name)))

dll_name = "kernel32.dll"
func_name = "GetProcAddress"
print(hex(Calc_Func_Hash(Calc_Dll_Hash(dll_name), func_name)))

返回结果如下:

0x27d05eb2
0xbb93705c

实际上加载LoadLibraryAGetProcAddresshash确实如此:

2.2. 加载函数

深入到Resolve_API_Hash函数中去,可以发现其逻辑传入两个地址,第一个地址是加载后函数存放地址,第二个地址是待解密加载的hash,解密密钥也很清晰:0x22065FED,当获取到的地址是0xCCCCCCCC时退出加载循环。

加载函数前有两个push,通过动态调试可以知道第一个push是加载函数存放起始地址,第二个push是需要解密的函数hash存放起始地址。简单调试后可以得到知道每个dll加载时的hash

可以检索一下所有的本地dll文件,计算hash进行比对,

dict = {}
for filename in os.listdir("C:\\Windows\\System32"):
    if '.dll' in filename:
        dll_hash = Calc_Dll_Hash(filename)
        dll_name = filename
        dict[hex(dll_hash).upper() ] = filename
print('411677B7:'+dict['0X411677B7'])
print('B1FC7F66:'+dict['0XB1FC7F66'])
print('BCFA1667:'+dict['0XBCFA1667'])
print('7132A177:'+dict['0X7132A177'])
print('3032403A:'+dict['0X3032403A'])
print('391830B4:'+dict['0X391830B4'])
print('38327FBA:'+dict['0X38327FBA'])
print('C50C676F:'+dict['0XC50C676F'])
print('820A18A3:'+dict['0X820A18A3'])
print('FD1A17C6:'+dict['0XFD1A17C6'])
print('41E8A017:'+dict['0X41E8A017'])
print('3CFC1737:'+dict['0X3CFC1737'])
print('C3BC5607:'+dict['0XC3BC5607'])
print('76E87915:'+dict['0X76E87915'])

最终计算结果如下:

411677B7:ntdll.dll
B1FC7F66:kernel32.dll
BCFA1667:advapi32.dll
7132A177:user32.dll
3032403A:gdi32.dll
391830B4:shell32.dll
38327FBA:ole32.dll
C50C676F:shlwapi.dll
820A18A3:oleaut32.dll
FD1A17C6:wtsapi32.dll
41E8A017:RstrtMgr.dll
3CFC1737:netapi32.dll
C3BC5607:activeds.dll
76E87915:wininet.dll

查看第二个参数地址可以找到需要进行加载的hash再未进行异或前的hash

2.3. IDAPython解密

根据上面的结论,可以进行简单的逻辑实现:

# 传入Resolve_API_Hash函数的的地址
def Resolve_All_APIs(resolve_ea):
    # 获取dll加载函数的交叉引用地址
    for ref in idautils.CodeRefsTo(resolve_ea,1):
        current_ea = ref
        api_addr_ea = 0
        api_hashes_ea = 0

        while True:
            # 获取上一条汇编语言的地址
            prev_instruction_ea = idc.prev_head(current_ea)
            # 判断助记符是否为push
            if idc.print_insn_mnem(prev_instruction_ea) == 'push':
                if api_addr_ea == 0:
                    # 向上第一条push是api写入地址
                    api_addr_ea = idc.get_operand_value(prev_instruction_ea,0)
                else:
                    # 再向上一条push是apihash存储的地址
                    api_hashes_ea = idc.get_operand_value(prev_instruction_ea,0)
                    break
            current_ea = prev_instruction_ea

        api_addr_ea += 4
        api_hashes_ea += 4

        index = 0
        while True:
            # 根据地址获取hash
            api_hash = idc.get_wide_dword(api_hashes_ea + 4*index)
            if api_hash == 0xCCCCCCCC:
                break
            # 进行异或解密
            api_hash = api_hash ^ 0x22065FED
            if api_hash in export_hashes:
                print(export_hashes[api_hash])
                # 写入地址对应的api函数名
                idc.set_name(api_addr_ea + 4*index, 'mw_' + export_hashes[api_hash], idaapi.SN_CHECK)
            else:
                print(hex(api_addr_ea),'NOTFOUND')
            index += 1

3. BypassUAC

当成功加载dll后,开始检查当前用户的RID是否是administrators:

接着通过对比OSMajorVersionOSMinorVersion检查操作系统版本:

TOKEN_GROUPS中判断进程令牌是否是admin权限。

如果不是admin权限运行,则进行提权,使用LdrEnumerateLoadedModulesC:\\Windows\\System32\\dllhost.exe注入到进程的PEB中,这样可以使用dllhost.exe接管COM对象。

调用CoGetObject创建以下对象:

Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}

通过COM库重载自身,从而达到权限提升的目的,整个BypassUAC过程如下:

最后再调用NtTerminateProcess终止自身。

4. 勒索设备ID号生成

获取注册表项SOFTWARE\Microsoft\Cryptography中的MachineGuid,经过三次ror13,再进行字节反转,最后base64编码一次,再将+/=符号进行转义,生成勒索设备ID号。

解密出勒索信文件名%s.README.txt

5. 窃取登录凭证

接着会尝试使用LogonUserW窃取登录凭证:

然后检查该用户是否属于DOMAINNAME\Domain Admins

6. 勒索主体

该功能模块主要是根据不同的命令参数进行功能分化,不同的参数执行不同的功能:

6.1. 命令参数解析

可以看到BlackMatter支持以下四个命令参数:

-path <pwd>      加密<pwd>路径的文件
<pwd>            加密<pwd>路径的文件
-safe            安全模式重启
-wall            设置壁纸

由于存储在样本中的是hash,所以只能通过生成明文字典(very luncky),再计算出字符串的hash进行比对,最终确认出明文命令:

6.2. 指定路径模式

当使用-path <pwd><pwd>参数时,将创建I/O接口进行加密,然后再重写生成加密文件。

-path <pwd>中的<pwd>是服务器路径时,将检索共享资源进行加密:

如果-path <pwd>中的<pwd>是文件路径,或仅使用<pwd>参数,则直接加密目录文件:

6.3. 安全模式

当使用-safe参数时,BlackMatter会首先检测是否是RID是否属于BUILTIN\Administrators,如果是,则进行以下操作。

6.3.1. 创建自登录账户

随机生成一个12位的密码:由3个随机大写字母、1个随机字符(#&)、3个随机数字、1个随机字符(#&)、4个随机小写字母组成。然后使用NetUserSetInfo创建administrator,密码则是刚才生成的12位字符串。

然后创建注册表:SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon,设置以下键值:

AutoAdminLogon:1
DefaultUserName:Administrator
DefaultDomainName:[GetComputerNameW()]
DefaultPassword:[Random_Password()]

整体结构如下:

6.3.2. RunOnce自启动

为了实现持久化,BlackMatter会随机生成一个9位数值名称:由3个随机大写字母、3个随机数字和3个随机小写字母组成,数值内容为当前路径,写入到注册表:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce中。

6.3.3. 设置生成桌面壁纸

根据当前屏幕大小,修改注册表项中的hScreenwScreen

当操作系统是windows 10时,会在注册表SOFTWARE\\Policies\\Microsoft\\Windows\\OOBE中设置DisablePrivacyExperience:1

壁纸生成的具体细节如下:

创建Times New Roman字体的句柄,从内存中获取壁纸突显的字符串:

BlackMatter Ransomware encrypted all your files!
To get your data back and keep your privacy safe,
you must find [readme.txt] file
and follow the instructions!

然后以二进制形式将图片写入到C:\ProgramData\[id].bmp,从而生成壁纸文件:

修改注册表HKEY_CURRENT_USER\Control Panel\Desktop的键值,设置桌面壁纸:

WallPaper:C:\ProgramData\[id].bmp
WallpaperStyle:10

6.3.4. 重启进入安全模式

BlackMatter会根据当前运行的操作系统版本,选择相应的命令使用WinExec执行,并重启操作系统。 当传递的参数为True时(从上方可以得知,当前模式传递的是True),启用安全模式:

winsows vista 之前的操作系统:bcdedit /set {{current}} safeboot network
winsows vista 之后的操作系统:bootcfg /raw /a /safeboot:network /id 1

当传递的参数为False时,该函数还提供了关闭功能:

winsows vista 之前的操作系统:bcdedit /deletevalue {{current}} safeboot
winsows vista 之后的操作系统:bootcfg /raw /fastdetect /id 1

6.4. 壁纸模式

当使用-wall参数时,BlackMatter仅生成壁纸,具体可参考壁纸生成的具体细节

6.5. 默认模式

当什么都不添加时,就进入到该模式,该模式涵盖了大多数的功能。

6.5.1. 互斥锁

Mutex_Flag为真时,将运行一次互斥。首先是获取MachineGuid,再通过MD4加密生成hash,再使用OpenMutexW结果来测试是否已存在。

如果存在,则关闭并退出,如果不存在则创建并继续:

6.5.2. 网络请求

如果配置文件中的Post_Flag为真,则会收集数据并发送到C2服务器。

在加密前会进行一次基本信息的收集,数据结构的json格式如下:

{
   "bot_version":"%s",
   "bot_id":"%s",
   "bot_company":"%.8x%.8x%.8x%.8x%",
   "host_hostname":"%s",
   "host_user":"%s",
   "host_os":"%s",
   "host_domain":"%s",
   "host_arch":"%s",
   "host_lang":"%s",
   "disks_info":[
      {						//当存在多个磁盘,该字典将变多
         "disk_name":"%s",
         "disk_size":"%u",
         "free_size":"%u"
      }
   ]
}

以下是我在虚拟机中获取的加密前发送数据:

{
	"bot_version":"1.2",
	"bot_id":"26a3ebea3de1f2c535eb81c5456fc830",
	"bot_company":"512478c08dada2af19e49808fbda5b0b",
	"host_hostname":"WIN-B1ESUN1R9U8",
	"host_user":"alee",
	"host_os":"Windows 7 Ultimate",
	"host_domain":"WORKGROUP",
	"host_arch":"x64",
	"host_lang":"zh-CN",
	"disks_info":[
		{
			"disk_name":"C",
			"disk_size":"102397",
			"free_size":"70005"
		}
	]
}

当加密完成以后,还会进行一次结果统计,数据结构的json格式如下:

{
	"bot_version":"%s",
	"bot_id":"%s",
	"bot_company":"%.8x%.8x%.8x%.8x%",
	"stat_all_files":"%u",
	"stat_not_encrypted":"%u",
	"stat_size":"%s",
	"execution_time":"%u",
	"start_time":"%u",
	"stop_time":"%u"
}

以下是我在虚拟机中获取的加密后发送数据

{
	"bot_version":"1.2",
	"bot_id":"26a3ebea3de1f2c535eb81c5456fc830",
	"bot_company":"512478c08dada2af19e49808fbda5b0b",
	"stat_all_files":"16499",
	"stat_not_encrypted":"3",
	"stat_size":"1282",
	"execution_time":"829",
	"start_time":"1633799826",
	"stop_time":"1633800656"
}

获取到数据后,会通过AES加密,再通过base64编码:

AES加密的密钥为:A6F330B09CD47B4FB9214F7836AA46AD

最终的C2服务器为:https[:]//paymenthacks.com

6.5.3. 清除回收站数据

在加密前,会遍历每一个磁盘驱动器上的第一个recycle文件:

然后删除所有S-开头的文件:

6.5.4. 删除卷影副本

使用CoCreateInstance创建两个对象,然后使用IWbemServices::ExecQuery方法执行WQL查询SELECT * FROM Win32_ShadowCopy检索卷影副本对象:

调用IEnumWbemClassObject::Next枚举系统上的所有卷影副本,调用IEnumWbemClassObject::Get获取每个卷影副本的 ID,并调用IWbemServices::DeleteInstance删除它们。

6.5.5. 终止服务

Kill_Service_FlagTrue时,使用OpenSCManagerW获取服务控制管理器句柄,然后使用EnumServicesStatusExW枚举所有服务,对比是否在终止名单中,如果是,则使用ControlService发送控制代码,使用DeleteService删除服务,服务列表如下:

"mepocs"、"memtas"、"veeam"、"svc$"、"backup"、"sql"、"vss"

6.5.6. 杀死进程

Kill_Process_FlagTrue时,使用NtQuerySystemInformation检索进程列表,对比是否在杀死名单中,如果是,使用NtOpenProcess确认句柄,使用NtTerminateProcess杀死进程,进程列表如下:

"encsvc"、"thebat"、"mydesktopqos"、"xfssvccon"、"firefox"、"infopath"、"winword"、"steam"、"synctime"、"notepad"、"ocomm"、"onenote"、"mspub"、"thunderbird"、"agntsvc"、"sql"、"excel"、"powerpnt"、"outlook"、"wordpad"、"dbeng50"、"isqlplussvc"、"sqbcoreservice"、"oracle"、"ocautoupds"、"dbsnmp"、"msaccess"、"tbirdconfig"、"ocssd"、"mydesktopservice"、"visio"

6.5.7. I/O多线程加密

创建用于处理I/O数据包的子线程,这些子线程使用GetQueuedCompletionStatusPostQueuedCompletionStatus与主线程进行通信。接收到的数据包,都是需要处理的文件。

整个加密过程存在四个 case

  1. case 0:读取文件;

  1. case 1:加密和写入文件。加密采用了自定义的加密算法,待加密结束后,将文件重新写入磁盘;

  1. case 2:写入文件页脚标识。当完成加密后,会在文件末端写入标识,标记该文件已被加密;

  1. case 3:当加密完成并写入页脚后,调用NtClose关闭文件句柄,使用RtlFreeHeap释放缓冲区

6.5.8. 驱动器检索

Drive_FlagTrue时,采用FindFirstVolumeWFindNextVolumeW检索全部的卷,然后调用GetVolumePathNamesForVolumeNameW检索驱动器号和已安装文件夹路径的列表:

当操作系统版本是Windows 7以前的版本时,将bootmgr挂载到最后一个驱动器后:

6.5.9. 域内资源检索

Network_FlagTrue时,将通过DsGetDcNameW获取域控制器信息,然后使用DsGetDcOpenW打开控制器,通过DsGetDcNextW枚举出所有的域控制器:

然后采用ADsEnumerateNext来枚举域控制器内的DNS主机:

然后使用NetShareEnum多线程寻找域内主机:

剩下的就是目录过滤和加密了:

6.5.10. 检索文件并加密

Drive_FlagTrue时,使用FindFirstVolumeWFindNextVolumeW获取所有卷的驱动器号和已安装文件夹路径的列表;扫描每个驱动器目录,在目录中放置赎金票据,并使用FindFirstFileExWFindNextFileW枚举目录,避开所有名为...的文件或目录:

在对文件进行加密时,首先会解除服务或进程对文件的占用:

然后会检查文件是否已加密:

将文件更名为带加密拓展名的文件:

然后使用CreateIoCompletionPort向全局I/O注册文件句柄,让I/O子线程进行加密:

参考链接

RID 说明:https://docs.microsoft.com/zh-tw/dotnet/api/system.security.principal.windowsprincipal.isinrole?redirectedfrom=MSDN&view=windowsdesktop-5.0#overloads
OSMajorVersion 与 OSMinorVersion 对照表:https://docs.microsoft.com/en-us/windows-hardware/drivers/install/inf-manufacturer-section
样本配置文件提取器:https://github.com/advanced-threat-research/DarkSide-Config-Extract