https://reliaquest.com/blog/threat-spotlight-storm-0249-precision-endpoint-exploitation/

Spear.msi

HASH

SHA256:8113fc3b4f82fb49f8dd853ca8e1275e0dfb06e48f39830708e4437fe8afbdfb

MD5:aa157129a9df47ede836516dd4c7ec2d

SHA1:423f2fcf7ed347ee57c1a3cffa14099ec16ad09c

Spear.msi

Orca打开.msi文件

Storm

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

Storm

Register目录下可以看到通过修改注册表来实现持久化

把.msi安装包修改为.zip解压就能拿到需要分析的文件SentinelAgentWorker.exeSentinelAgentCore.dllvcruntime140.dll

Storm

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

Storm

SentinelAgentCore

Storm

该dll使用了一个吊销的签名

查看这个dll的导出表发现都使用wscanf_s进行了转发

Storm

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

Storm

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

Storm

sub_2ADACAD046E

遍历pe加载所需的dll与api

Storm

比较明显的dll反射式加载

Storm

写个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

Storm

Storm

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

Storm

Storm

Storm

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

Storm

再次写一个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的所有导出表都是指向这个循环函数

Storm

Storm

hash api调用

Storm

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;
}

反调试

Storm

检测网卡(反沙箱

Storm

#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

Storm

#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/

Storm

后续就是常规的c2功能

Storm

格式化获取到的信息