https://reliaquest.com/blog/threat-spotlight-storm-0249-precision-endpoint-exploitation/
Spear.msi
HASH
SHA256:8113fc3b4f82fb49f8dd853ca8e1275e0dfb06e48f39830708e4437fe8afbdfb
MD5:aa157129a9df47ede836516dd4c7ec2d
SHA1:423f2fcf7ed347ee57c1a3cffa14099ec16ad09c
Spear.msi
Orca打开.msi文件

通过用户自定义行为可以很明显的发现需要分析的文件,以及释放的路径

Register目录下可以看到通过修改注册表来实现持久化
把.msi安装包修改为.zip解压就能拿到需要分析的文件SentinelAgentWorker.exe、SentinelAgentCore.dll、vcruntime140.dll

SentinelAgentWorker.exe为哨兵的exe,没有问题,通过查看SentinelAgentWorker导入表即可知道需要分析的dll以及函数

SentinelAgentCore

该dll使用了一个吊销的签名
查看这个dll的导出表发现都使用wscanf_s进行了转发

采用了dll劫持,分析wscanf_s_0会发现存在大量混淆(这个混淆暂时无法识别进行大量的SIMD运算,ida采用的是线性反汇编对这种代码识别效果不是很好,可以考虑使用ghidra尝试)

api流下断点会发现无法成功,这里采用插桩可以发现前面部分调用了VirtualAlloc等函数,但是具体调用方法无法很明确的分析(在VirtualAlloc函数下断点无法

sub_2ADACAD046E
遍历pe加载所需的dll与api

比较明显的dll反射式加载

写个ida python脚本把dll提取出来单独分析
exp:
import struct
import idaapi
import ida_name
import ida_bytes
import ida_kernwin
TARGET_NAME = "word_2ADACAD1C02" #地址 or 符号名称
def u16(b, off): return struct.unpack_from("<H", b, off)[0]
def u32(b, off): return struct.unpack_from("<I", b, off)[0]
def dump_embedded_pe(start_ea, out_path):
head = ida_bytes.get_bytes(start_ea, 0x1000)
if not head or len(head) < 0x100:
raise RuntimeError("无法读取PE头数据")
# DOS
if u16(head, 0x00) != 0x5A4D: # 'MZ'
raise RuntimeError("不是有效MZ头")
e_lfanew = u32(head, 0x3C)
# 确保能读到 NT headers
need = e_lfanew + 0x200
if len(head) < need:
more = ida_bytes.get_bytes(start_ea, need)
if not more:
raise RuntimeError("无法读取NT头")
head = more
# NT Signature
if u32(head, e_lfanew) != 0x00004550: # 'PE\0\0'
raise RuntimeError("不是有效PE签名")
file_hdr_off = e_lfanew + 4
num_sections = u16(head, file_hdr_off + 2)
size_opt_hdr = u16(head, file_hdr_off + 16)
opt_off = file_hdr_off + 20
magic = u16(head, opt_off)
if magic not in (0x10B, 0x20B):
raise RuntimeError("OptionalHeader Magic异常: 0x%X" % magic)
size_of_headers = u32(head, opt_off + 0x3C)
sec_off = opt_off + size_opt_hdr
# 读完整节表
need2 = sec_off + num_sections * 40
if len(head) < need2:
more = ida_bytes.get_bytes(start_ea, need2)
if not more or len(more) < need2:
raise RuntimeError("无法完整读取节表")
head = more
# 估算原始文件大小:max(SizeOfHeaders, each PointerToRawData + SizeOfRawData)
file_size = max(size_of_headers, 0x400)
for i in range(num_sections):
sh = sec_off + i * 40
size_raw = u32(head, sh + 16)
ptr_raw = u32(head, sh + 20)
end_raw = ptr_raw + size_raw
if end_raw > file_size:
file_size = end_raw
if file_size <= 0 or file_size > 200 * 1024 * 1024:
raise RuntimeError("计算出的文件大小异常: %d" % file_size)
blob = ida_bytes.get_bytes(start_ea, file_size)
if not blob or len(blob) < file_size:
raise RuntimeError("读取完整DLL失败,可能IDB未映射完整缓冲区")
with open(out_path, "wb") as f:
f.write(blob)
def main():
ea = ida_name.get_name_ea(idaapi.BADADDR, TARGET_NAME)
if ea == idaapi.BADADDR:
ea = ida_kernwin.ask_addr(idaapi.BADADDR, "符号未找到,请输入DLL起始地址")
if ea in (None, idaapi.BADADDR):
print("[-] 未提供有效地址")
return
out_path = ida_kernwin.ask_file(True, "*.dll", "保存导出的反射DLL")
if not out_path:
print("[-] 用户取消")
return
try:
dump_embedded_pe(ea, out_path)
except Exception as e:
print("[-] 导出失败: %s" % e)
if __name__ == "__main__":
main()
DLL1


检测rundll32.exe与regsvr32.exe进程(似乎有反沙箱的作用,询问ai得到的答案如下:



采用NtCreateSection + NtMapViewOfSection相较于VirtualAlloc/VirtualProtect存在的优势如下:

再次写一个ida python脚本将shellcode提取出来单独分析
exp:
import ida_bytes
import ida_name
import ida_idaapi
def dump_payload(
out_path=r"<Storage_Path>",#根据需要进行修改
symbol_name="byte_180003090",
fallback_start=0x180003090,
size=0x16600,
):
start = ida_name.get_name_ea(ida_idaapi.BADADDR, symbol_name)
if start == ida_idaapi.BADADDR:
start = fallback_start
blob = ida_bytes.get_bytes(start, size)
if blob is None or len(blob) != size:
print(f"[!] 导出失败: start={hex(start)} size={size}")
return False
with open(out_path, "wb") as f:
f.write(blob)
return True
if __name__ == "__main__":
dump_payload()
导出的shellcode查看属性可以发现就是一个dll
DLL2
该dll的所有导出表都是指向这个循环函数


hash api调用

HMODULE resolve_module_base_by_hash_peb(uint32_t target_hash)
{
PPEB peb = NtCurrentPeb();
PLIST_ENTRY head = &peb->Ldr->InLoadOrderModuleList;
for (PLIST_ENTRY e = head->Flink; e->Flink; e = e->Flink)
{
wchar_t* baseName = ((LDR_DATA_TABLE_ENTRY*)e)->BaseDllName.Buffer;
uint16_t byteLen = ((LDR_DATA_TABLE_ENTRY*)e)->BaseDllName.Length;
wchar_t* lower = wcs_to_lower_tmpbuf(baseName, byteLen / 2);
uint32_t hash = crc32_hash_bytes((uint8_t*)lower, 2 * wcslen16(lower));
if (hash == target_hash)
return ((LDR_DATA_TABLE_ENTRY*)e)->DllBase;
}
return 0;
}
反调试

检测网卡(反沙箱

#include <windows.h>
#include <iphlpapi.h>
#include <stdio.h>
#pragma comment(lib, "iphlpapi.lib")
static void* alloc_adapter_buffer(ULONG size) {
void* p = VirtualAlloc(NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
if (p) {
ZeroMemory(p, size);
}
return p;
}
// 0 -> 发现某适配器 AddressLength > 6
// 1 -> 未发现,或者流程失败
int check_adapters_like_sample(void) {
ULONG size = 0;
DWORD ret;
PIP_ADAPTER_INFO info = NULL, cur = NULL;
ret = GetAdaptersInfo(NULL, &size);
if (ret != ERROR_BUFFER_OVERFLOW) {
return 1;
}
info = (PIP_ADAPTER_INFO)alloc_adapter_buffer(size);
if (!info) {
return 1;
}
// 获取链表
ret = GetAdaptersInfo(info, &size);
if (ret != ERROR_SUCCESS) {
VirtualFree(info, 0, MEM_RELEASE);
return 1;
}
// 遍历链表,检查 AddressLength(常规为 6)
for (cur = info; cur != NULL; cur = cur->Next) {
if (cur->AddressLength > 6) {
VirtualFree(info, 0, MEM_RELEASE);
return 0;
}
}
VirtualFree(info, 0, MEM_RELEASE);
return 1;
}
int main(void) {
int r = check_adapters_like_sample();
printf("result = %d\n", r);
return 0;
}
获取系统配置信息(volume、user name

#include <windows.h>
#include <stdio.h>
#include <iphlpapi.h>
#include <lmcons.h>
#include <wchar.h>
#pragma comment(lib, "Advapi32.lib")
static int get_first_volume_info(void) {
WCHAR volumeName[MAX_PATH] = {0}; // \\?\Volume{GUID}\
WCHAR fsName[MAX_PATH] = {0}; // NTFS/FAT32/...
WCHAR label[MAX_PATH] = {0}; // 卷标
DWORD serialNumber = 0;
DWORD maxCompLen = 0;
DWORD fsFlags = 0;
HANDLE hFind = FindFirstVolumeW(volumeName, MAX_PATH);
if (hFind == INVALID_HANDLE_VALUE) {
printf("FindFirstVolumeW failed, err=%lu\n", GetLastError());
return 0;
}
BOOL ok = GetVolumeInformationW(
volumeName, // root path name
label, MAX_PATH, // volume label
&serialNumber, // serial number
&maxCompLen, // max component length
&fsFlags, // filesystem flags
fsName, MAX_PATH // filesystem name
);
FindVolumeClose(hFind);
if (!ok) {
printf("GetVolumeInformationW failed, err=%lu\n", GetLastError());
return 0;
}
wprintf(L"[Volume]\n");
wprintf(L" Name : %ls\n", volumeName);
wprintf(L" Label : %ls\n", label[0] ? label : L"(empty)");
wprintf(L" FS : %ls\n", fsName);
printf (" Serial : 0x%08lX\n", serialNumber);
printf (" MaxCompLen : %lu\n", maxCompLen);
printf (" FS Flags : 0x%08lX\n", fsFlags);
return 1;
}
static int get_username_info(void) {
CHAR user[UNLEN + 1] = {0};
DWORD size = (DWORD)sizeof(user);
if (!GetUserNameA(user, &size)) {
printf("GetUserNameA failed, err=%lu\n", GetLastError());
return 0;
}
printf("[User]\n");
printf(" Username : %s\n", user);
printf(" Length(+NUL): %lu\n", size);
char out[512] = {0};
wsprintfA(out, "Security Trainee %s", user);
printf(" Formatted : %s\n", out);
return 1;
}
int main(void) {
get_first_volume_info();
get_username_info();
return 0;
}
解密url( https://hristomasitomasdf.com/work/

后续就是常规的c2功能

格式化获取到的信息