Cobaltstrike shellcode行为特征分析
嗯,用户让我总结一下这篇文章的内容,控制在一百个字以内,而且不需要用“文章内容总结”这样的开头。首先,我需要通读整篇文章,理解其主要信息。 文章主要介绍了Windows进程相关的结构,包括TEB、PEB、PEB_LDR_DATA、LDR_DATA_TABLE_ENTRY、UNICODE_STRING、IMAGE_EXPORT_DIRECTORY、IMAGE_IMPORT_DESCRIPTOR、IMAGE_THUNK_DATA、IMAGE_BASE_RELOCATION以及shellcode行为分析等内容。这些都是Windows系统中进程和线程管理的关键数据结构。 TEB是线程环境块,每个线程都有自己的TEB,存储线程运行时信息。PEB是进程环境块,存储进程全局信息。PEB_LDR_DATA管理已加载模块的元数据,包含三个双向链表:按加载顺序、内存布局顺序和初始化顺序排列的模块列表。 LDR_DATA_TABLE_ENTRY结构体存储了DLL模块的相关属性,如基地址、入口点、大小等。UNICODE_STRING用于表示宽字符字符串。导出表和导入表分别描述了模块的导出函数和导入依赖。 此外,文章还详细分析了shellcode的行为,包括stager加载器阶段的特征值压入、模块加载与API获取、beacon文件的解密与执行过程等。这些内容对于理解Windows进程内部机制和逆向工程非常有帮助。 现在我需要将这些关键点浓缩到100字以内。首先确定主要结构:TEB和PEB的基本概念及其包含的关键信息。然后提到导出表和导入表的作用。最后简要提及shellcode的行为分析。 确保语言简洁明了,避免使用复杂的术语。同时注意字数限制,确保不超过100字。 </think> 文章介绍了Windows进程相关结构(如TEB、PEB)及其功能,并详细讲解了导出表(Export Table)、导入表(Import Table)、重定位表(Base Relocation)等关键数据结构的工作原理。同时分析了shellcode行为模式,包括模块加载与API获取、内存分配与加密解密过程等。 2026-1-21 10:18:17 Author: www.freebuf.com(查看原文) 阅读量:1 收藏

windows 进程相关结构介绍

TEB

TEB(Thread Environment Block,线程环境块)是每个线程私有的关键数据结构,用于存储与当前线程相关的运行时信息。它位于用户地址空间中,由操作系统内核维护,但可被用户态代码直接访问。同一进程中的不同线程拥有各自的 TEB,但共享同一个 PEB。

TEB有哪些关键信息?

  • PEB地址

  • 线程局部存储(TLS)

  • 异常处理链

  • LastErrorValue

如何获取到TEB结构?

  • 32位可以通过FS:[0]访问

  • 在 x64 架构中,通过GS:[0]访问

PEB

PEB(Process Environment Block,进程环境块),主要用于保存与进程相关的全局信息。可以通过线程TEB中的PEB指针,直接获取到进程的PEB内容(FS:[30]、GS:[60])。

PEB有哪些关键信息?
字段用途
BeingDebugged被调试器附加时为 1(IsDebuggerPresent()就是读这个值)
ImageBaseAddress当前进程主模块(EXE)的加载基址
Ldr指向_PEB_LDR_DATA,包含已加载模块(DLL)的双向链表(InMemoryOrderModuleList 等)
ProcessParameters指向_RTL_USER_PROCESS_PARAMETERS,含命令行、ImagePathName、环境变量等
NtGlobalFlag若为0x70(FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS),通常表示被调试
ProcessHeap/ProcessHeaps默认堆及所有堆列表
OSMajorVersion/OSMinorVersion/OSBuildNumber操作系统版本信息
SessionId当前会话 ID

_PEB_LDR_DATA

专门用于管理进程中所有已加载模块(EXE/DLL)的元数据,偏移位置:+0x0C(x86)或+0x18(x64)

typedef struct _PEB_LDR_DATA {
    ULONG Length;		//结构体大小
    BOOLEAN Initialized;		//知否初始化
    PVOID SsHandle;			//保留字段(旧版用于子系统句柄)
    LIST_ENTRY InLoadOrderModuleList;      // 按加载顺序
    LIST_ENTRY InMemoryOrderModuleList;    // 按内存布局顺序(常用)
    LIST_ENTRY InInitializationOrderModuleList; // 按初始化顺序
	...
} PEB_LDR_DATA, *PPEB_LDR_DATA;
  • 三个双向链表

    1. InLoadOrderModuleList:按模块被加载的先后顺序。

    2. InMemoryOrderModuleList:按模块在内存中的地址顺序。

      1. 第一项是当前exe文件自己的主模块,dll文件从第二项开始

    3. InInitializationOrderModuleList:按 DllMain 被调用的顺序(仅 DLL)。

_LDR_DATA_TABLE_ENTRY

_LDR_DATA_TABLE_ENTRY结构体,存储了DLL模块相关的诸多属性,其中关键字段如下:

字段偏移字段名类型说明
+0x00InLoadOrderLinksLIST_ENTRY双向链表节点,用于将模块按 加载顺序 链入PEB_LDR_DATA.InLoadOrderModuleList。第一个节点是主 EXE。
+0x08InMemoryOrderLinksLIST_ENTRY双向链表节点,用于按 内存基址顺序 链入InMemoryOrderModuleList。遍历时需从此地址减 0x08 得到结构体起始地址。
+0x10InInitializationOrderLinksLIST_ENTRY双向链表节点,用于按 DllMain 初始化顺序 链入初始化链表。仅包含 DLL,不包含主 EXE。
+0x18DllBasePVOID模块在进程地址空间中的 加载基地址(如0x7c800000)。用于解析导出表和计算 API 地址。
+0x1CEntryPointPVOID模块入口点地址(DLL 为DllMain,EXE 为启动函数)。若无入口点则为NULL
+0x20SizeOfImageULONGPE 映像在内存中占用的总大小(按 Section 对齐),单位:字节。
+0x24FullDllNameUNICODE_STRING模块完整路径(如C:\Windows\System32\kernel32.dll)。包含长度、最大长度和宽字符缓冲区指针。
+0x2CBaseDllNameUNICODE_STRING模块文件名(如kernel32.dll)。shellcode 常通过此字段查找关键系统 DLL。
+0x34FlagsULONG模块加载标志。常见值:
0x4=LDRP_IMAGE_DLL(是 DLL)
0x1000=LDRP_ENTRY_PROCESSED(已处理)
+0x38LoadCountUSHORT引用计数(LoadLibrary-FreeLibrary次数)。Vista 后通常为0xFFFF(-1),表示永不卸载。
+0x3ATlsIndexUSHORTTLS(线程局部存储)索引。未使用 TLS 时为 0。
+0x3CHashLinks/SectionPointerLIST_ENTRYPVOID联合体:
• 作为HashLinks:用于内部哈希表
• 作为SectionPointer:指向映射的 section 对象
+0x40(联合体内)CheckSumULONGPE 文件头中的校验和(通常为 0,除非是驱动或系统文件)。
+0x44TimeDateStampULONGPE 编译时间戳(与IMAGE_FILE_HEADER.TimeDateStamp一致)。可用于版本识别或完整性检测。
  • 由于InMemoryOrderLinks指向的是_LDR_DATA_TABLE_ENTRY结构中的第二项,因此后续的偏移地址计算时需要减去0x08的偏移

_UNICODE_STRING

NICODE_STRING是 Windows 内核和用户态中广泛使用的字符串结构,用于高效表示 **宽字符(UTF-16LE)**字符串,从而并避免频繁调用wcslen等函数,占用8字节。

typedef struct _UNICODE_STRING {
    USHORT Length;         // +0x00:字符串长度(字节,不含结尾 \0)
    USHORT MaximumLength;  // +0x02:缓冲区总大小(字节,含 \0)
    PWSTR  Buffer;         // +0x04:指向宽字符字符串的指针(4 字节)
} UNICODE_STRING, *PUNICODE_STRING;
  • 偏移+0x02指向字符串的长度

  • 偏移+0x04指向字符串

  • 宽字节字符串

    • U+0000 – U+FFFF:2 字节

    • U+10000 – U+10FFFF:4 字节

IMAGE_EXPORT_DIRECTORY

导出表(Export Table)是 PE(Portable Executable)文件结构中用于描述模块(EXE/DLL)对外暴露的函数(即“导出函数”)的关键数据结构。

typedef struct _IMAGE_EXPORT_DIRECTORY {
    ULONG Characteristics;       // +0x00 — 通常为 0(保留)
    ULONG TimeDateStamp;         // +0x04 — 编译时间戳
    USHORT MajorVersion;         // +0x08 — 主版本号(通常为 0)
    USHORT MinorVersion;         // +0x0A — 次版本号(通常为 0)
    ULONG Name;                  // +0x0C — 模块名 RVA(如 "kernel32.dll")
    ULONG Base;                  // +0x10 — 导出函数起始序号(通常为 1)
    ULONG NumberOfFunctions;     // +0x14 — 总函数数(含空项)
    ULONG NumberOfNames;         // +0x18 — 有名函数数量(≤ NumberOfFunctions)
    ULONG AddressOfFunctions;    // +0x1C — 导出地址表 RVA(EAT)
    ULONG AddressOfNames;        // +0x20 — 函数名地址表 RVA
    ULONG AddressOfNameOrdinals; // +0x24 — 函数名序号表 RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
关键字段解释
偏移字段说明
+0x00Characteristics保留字段,始终为 0
+0x04TimeDateStamp与 PE 文件头一致,可用于版本识别
+0x08 / +0x0AMajorVersion/MinorVersion很少使用,通常为 0
+0x0CName指向模块名称字符串(如"ntdll.dll"
+0x10Base导出函数的起始序号。例如,若Base = 1,则第一个函数序号为 1。
+0x14NumberOfFunctions导出地址表(EAT)中的总条目数(包括 forwarder 和空项)
+0x18NumberOfNames有名函数的数量(即可以通过名称查找的函数数)
+0x1CAddressOfFunctions指向 导出地址表(Export Address Table, EAT)
+0x20AddressOfNames指向 函数名地址表(Name Pointer Table)
+0x24AddressOfNameOrdinals指向 序号表(Ordinal Table),每个元素是 2 字节(USHORT),表示对应函数在 EAT 中的索引
  • 导出地址表:EAT,DWORD[]

    • 导出表存储在 DLL 的“数据段”中(通常是.rdata.edata节)。若函数地址 在导出表所在段范围内,那么说明该地址并不在代码段,此时,该地址指向一个ascii字符串,格式为"ModuleName.FunctionName",加载器会自动解析该模块,然后将函数地址写入IAT中,也就是导出表的转发功能。

  • 函数名地址表(Name Pointer Table):DWORD[]

    • 每个元素指向一个以\0结尾的 ASCII 函数名

  • 序号表 WORD[]

    • 由于不是所有的导出函数都有名字、而且函数名地址表是通过字典序排序的,而EAT是按照序号排序的,导致函数和地址无法一一对应,所以需要序号表进行关联

    • 每个元素是 EAT 的索引

GetProcAddress函数的内部实现流程
  • 验证hModulehModule必须是由LoadLibrary/GetModuleHandle返回的有效模块基址(即 PE 映像的 ImageBase)。

  • 定位导出表:从该 DLL 的 PE 可选头中读取数据目录项

  • 根据输入类型查找

    1. 按名称

      • 遍历AddressOfNames数组(共NumberOfNames项),每项是一个函数名 RVA。

      • 对每个名称,与目标"FunctionName"进行 ASCII 字符串比较

      • 若找到匹配项,获取其在AddressOfNames中的索引i

      • AddressOfNameOrdinals[i]读取对应的序号偏移值ordinal_index

      • 最终函数地址 =AddressOfFunctions[ordinal_index](这是一个 RVA)。

      • 将 RVA 转换为 VA(加上 DLL 的 ImageBase),返回给调用者。

    2. 按序号

      • 计算索引号

      DWORD ordinal = (DWORD)(UINT_PTR)lpProcName; // 如 123
      

DWORD index = ordinal - pExportDir->Base; // 通常 Base=1,所以 index=122
```

* 检查 `index`是否在 `[0, NumberOfFunctions)`范围内。
    * 若有效,则函数地址 RVA = `AddressOfFunctions[index]`。
    * 转换为 VA 并返回。

IMAGE_IMPORT_DESCRIPTOR

导入表,实际上是一个由IMAGE_IMPORT_DESCRIPTOR结构组成的数组。每个IMAGE_IMPORT_DESCRIPTOR结构代表一个被当前模块导入的DLL的信息。这个数组以一个全零的IMAGE_IMPORT_DESCRIPTOR作为结束标志

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    DWORD OriginalFirstThunk;   // RVA to INT (Import Name Table)
    DWORD TimeDateStamp;            // 0=正常导入;非0=绑定导入的时间戳
    DWORD ForwarderChain;           // -1=无转发;否则为首个转发函数索引
    DWORD Name;                     // RVA to DLL name string (e.g., "KERNEL32.dll")
    DWORD FirstThunk;               // RVA to IAT (Import Address Table)
} IMAGE_IMPORT_DESCRIPTOR;
字段类型含义
OriginalFirstThunkDWORD (RVA)指向 INT 数组,保存原始导入信息(不变)
TimeDateStampDWORD0 表示普通导入;非 0 表示绑定导入的 DLL 时间戳
ForwarderChainDWORD转发链起始索引(-1 表示无转发)
NameDWORD (RVA)指向 DLL 名称的 ASCII 字符串
FirstThunkDWORD (RVA)指向 IAT 数组,加载后被覆盖为函数地址
IMAGE_THUNK_DATA

INT 和 IAT 均为 IMAGE_THUNK_DATA数组,每个元素 4 字节(32 位)或 8 字节(64 位)。

// 32位
typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;  // 转发器字符串 RVA(极少用)
        DWORD Function;         // 加载后:真实函数地址
        DWORD Ordinal;          // 按序号导入:最高位为1,低31位为序号
        DWORD AddressOfData;    // 按名导入:指向 IMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;

typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD  Hint;      // 可选提示:目标 DLL 导出表中的索引(加速查找)
    CHAR  Name[1];   // 函数名,ASCII,以 \0 结尾
} IMAGE_IMPORT_BY_NAME;
windows加载器导入函数的详细过程
  • 读取 DLL 名称

    • IMAGE_IMPORT_DESCRIPTOR.Name获取 DLL 名(如"kernel32.dll"),调用LoadLibraryA()加载该 DLL,得到模块句柄hMod

  • 遍历 INT(OriginalFirstThunk),对每个非零 Thunk:

    • 检查最高位:若为 0 → 按名称导入。

      • 将 Thunk 值作为 RVA,转换为IMAGE_IMPORT_BY_NAME* pByName

    • 若最高位为1,则表示按照序号导入,低16位表示序号

      • 提取序号:ordinal = thunk & 0xFFFF

  • 调用 GetProcAddress,获取对应函数地址

GetProcAddress函数的内部实现逻辑在导出表部分有介绍

  • 写入 IAT

    • addr写入FirstThunk所指向的 IAT 位置(运行时调用即从此处跳转)。

IMAGE_BASE_RELOCATION

重定位表位于DataDirectory中的第6项,指向一个或多个IMAGE_BASE_RELOCATION结构组成的块(Block)。每个块对应一个 4KB(0x1000 字节)的页面(Page),包含该页内所有需要重定位的偏移。

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD VirtualAddress;   // 该块所对应的 RVA(相对虚拟地址)
    DWORD SizeOfBlock;      // 整个块的大小(包括本结构)
    // WORD TypeOffset[...]; // 变长数组,每项高4位为类型,低12位为页内偏移
} IMAGE_BASE_RELOCATION;
重定位表的具体工作流程
  • 加载器检测是否需要重定位

    • ImageBase已被占用 → 必须重定位。

    • 若 PE 文件头中DllCharacteristics包含IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE(即支持 ASLR),则即使未冲突也可能随机化地址并触发重定位。

  • 遍历重定位表

    • 对每个IMAGE_BASE_RELOCATION块:

      • 计算该块对应的内存地址:Base + VirtualAddress

      • 遍历TypeOffset数组(每项 2 字节):

        • 提取 低 12 位→ 得到页内偏移offset

        • 提取 高 4 位→ 得到重定位类型(32 位中最常见的是IMAGE_REL_BASED_HIGHLOW = 3

  • 执行地址修正

    • 加载器读取该地址处的 4 字节值(原编译时的绝对地址)

    • 将重定位的地址写回该内存位置

重定位表,是dll文件自身内部使用的,由于基地址的变化,导致内部的符号的地址发生变化,因此需要通过重定位表来定位、修改这些地址;而导入表、导出表,则是在别的函数调用dll中的函数时使用的

shellcode行为分析

stager加载器阶段

压入特征值

stager的主要功能是通过http请求获取完整的beacon文件,需要加载wininet模块,调用其中网络相关的API。同时为了避免在代码中直接出现关键API名字,被杀软检测到,需要对函数名称进行处理,然后通过特征值进行对比,获取指定函数地址。

具体特征提取算法为:依次读取模块名称ascii字符,将小写转化为大写,然后累加,将结果循环右移14位。

具体过程如下:

  1. 将返回地址pop到ebp寄存器

  2. 压入特征值(726774c:LoadLibraryExA函数的hash值),以及wininet ascii码( 74 65 6e 69 6e 69 77)

  3. call ebp返回

加载wininet模块

获取LoadLibraryExA函数

这一部分主要是通过获取到当前进程的PEB,然后遍历进程加载的dll,从导出表中,获取导出函数,对导出函数名做上述同样的hash处理,然后与开始是压入的特征值进行对比,直到找到LoadLibraryExA函数。

详细过程如下:

  • 通过fs:30获取PEB的地址

  • PEB地址偏移+0xC获取PEB_LDR_DATA结构,

  • 偏移+0x14,获取InMemoryOrderModuleList地址

  • InMemoryOrderModuleList地址偏移+0x28,获取到当前模块的名称字符串起始地址

  • InMemoryOrderModuleList地址偏移+0x26,获取到当前模块的名称长度

  • 获取模块加载基址

  • 通过基址,获取PE文件结构

    • 基址偏移+0x3c:PE头起始地址

    • PE头起始地址偏移+0x78:导出表地址

    • 判断导出表是否为空,如果为空,则跳过,遍历下一个模块

    • 当导出表不为空时

      • +0x18获取导出函数总数

      • +0x20获取导出函数名称地址表

      • 遍历函数名称地址表

        • 对字符串进行hash处理,然后与指定的特征值进行比较

      • hash匹配完成之后

        • 根据当前循环次数i

        • AddressOfNameOrdinals[i]得到 EAT 索引idx

        • AddressOfFunctions[idx]得到 函数 RVA

        • 计算真实地址:dllBase + RVA

加载wininet.dll,获取关键API地址

通过LoadLibraryExA函数加载wininet.dll,然后通过之前的方式,从导出表中获取http相关API的地址,为后续网络请求的发起做准备。

主要的API如下:

模块API 名称主要功能说明
WinINetInternetOpenA初始化 WinINet 会话,返回一个会话句柄,用于后续网络操作。
WinINetHttpOpenRequestA在已建立的连接上创建一个 HTTP 请求句柄,用于后续发送请求。
WinINetHttpSendRequestA向服务器发送 HTTP 请求(包括可选数据体和头部)。
WinINetInternetReadFile从已打开的 WinINet 句柄(如 HTTP 响应)中读取数据到缓冲区。
Kernel32VirtualAlloc在调用进程的虚拟地址空间中保留、提交或更改内存页的状态。常用于动态分配可执行内存。

拉取完整的beacon文件

在本阶段,stager将与C2服务器建立连接,stager会将指定的url发送http请求,C2服务器会返回完整的beacon文件。

具体过程如下:

  1. 通过InternetOpenA,初始化一个WinINet 会话,包括user-agent、代理等基本属性。

  2. 通过HttpOpenRequestA,创建一个http请求句柄,包括请求方式(get)、路径、http版本等信息。

  3. 通过HttpSendRequestA,正式向C2服务器发送请求,在这一步中,会正式与服务器建立TCP连接,并组装完整的http请求报文,并阻塞直到获取到所有服务器返回响应。该过程并不会读取响应。

  4. 通过VirtualAlloc申请内存空间,存放beacon文件。

  5. 通过InternetReadFile读取返回到指定的内存地址

解密beacon

之前从服务端获取的beacon是加密后的文件,需要进行解密操作,其实就是最简单的异或操作。而key就保存在文件偏移+0x3d的位置,4字节大小。

具体过程如下:

  • 获取beacon长度:作为后续解密的循环次数

    • 通过对内存偏移+0x3d的4字节内存与+0x42四字节内存做异或运算。

  • 解密:

    • 0x46开始,每4字节与0x3d的key做异或,然后写入当前位置,直到循环结束。

发现解密后的文件以4D5A开头,是一个DLL文件

beacon文件执行过程

ReflectiveLoader函数的调用

经过上述的解密操作,我们知道beacon是一个PE文件,而正常的PE文件需要被加载器、动态链接器处理之后才能被执行。此时我们是将“PE文件”整体被当作代码段执行的,直接跳转到4D 5A开始执行。

一个PE文件开头是一个64字节的DOS header ,要想被加载器正确的识别,需要满足以下条件:

  • e_magic(+0x00):必须为0x5A4D("MZ")

  • e_lfanew(+0x3c):执行PE头的开始,即所指向地址的内容为50 45 00 00("PE\0\0")

因此在剩余的空间中,可以填充其他指令,完成ReflectiveLoader函数的调用。

具体过程如下:

  1. 4D 5A 对应的操作是dec ebp;pop edx

  2. 通过call 命令(e8 00 00 00 00) 获取当前代码地址,然后通过pop 指令弹出地址。

  3. 还原4D 5A的操作push edx;nc ebp

  4. 移动ebp,重新开辟栈空间,直接通过偏移跳转到ReflectiveLoader函数的地址,执行ReflectiveLoader函数

    • 由于此时的dll是以文件的形式读取到内存中,没有经过加载器处理,因此,不能直接使用dll文件的导出表中的地址执行,需要转化为该函数在文件中的偏移地址,计算过程简单如下:

      • 首先通过导出表的RVA减去代码段的起始地址(0x1000),获得该函数在代码段中的偏移位置,然后加上文件头的大小(0x400),获得该函数在文件中的偏移地址。

ReflectiveLoader的执行

ReflectiveLoader函数是主要功能是完成beacon文件的加载。

准备工作

为了后续修复导出表的工作,需要通过之前介绍的方式,获取一些关键API的地址,

API 名称主要功能说明
GetProcAddress获取指定模块中导出函数的地址(返回函数指针),用于动态调用 DLL 中的函数。
GetModuleHandleA获取已加载模块的句柄(即基地址)。若模块未加载,返回NULL。不增加引用计数。
LoadLibraryA将指定 DLL 加载到调用进程的地址空间,返回其模块句柄(基地址)。若已加载,则仅增加引用计数。
LoadLibraryExALoadLibraryA的扩展版本,支持额外标志(如LOAD_LIBRARY_AS_DATAFILEDONT_RESOLVE_DLL_REFERENCES等),提供更精细的加载控制。
VirtualAlloc在调用进程的虚拟地址空间中保留、提交或同时保留+提交一块内存区域。常用于分配可执行内存(配合PAGE_EXECUTE_READWRITE)。
VirtualProtect更改已提交内存页的保护属性(如从READONLY改为EXECUTE_READWRITE),常用于 shellcode 注入或 JIT 场景。

具体过程:

  1. 还是通过call 命令,获取当前地址

  2. 定位pe文件的Dos 头、PE头

  3. 从Kernel32模块的导出表中,找到上述函数的地址

开始“加载”DLL文件

之前的dll文件一直是以文件的形式存在,要使得dll可以被正确运行,首先要进行对齐的调整,因此需要重新申请一块内存,然后以内存映像的形式,将dll复制过去。

  1. 通过virtualAlloc申请内存空间,大小为PE头中的SizeOfImage

  2. 复制整个PE头,判断是否存在重定位表(PE文件头中Characteristics属性)

  3. 复制所有的节块

    1. 首先通过PE头文件,获取PE头的大小,跟在后面的就是节表

    2. 根据节表中每个节块在文件中的起始偏移、大小,将节块复制到对应的内存空间

修复导入表

进程运行时,内部正常调用dll中的函数,都是通过IAT进行的,这一部分工作,本来应该是动态链接器负责的,现在需要我们手动进行。

  1. 在PE文件头+0x80获取导入表位置

  2. 在每一个导入描述符中,+0x0c指向dll的文件名

  3. 通过LoadLibraryA函数加载该dll

  4. 然后通过GetProcAddress获取指定函数的地址,写入对应的IAT

修复重定位表

如果该dll存在重定位表,那么还需要根据真实的加载地址,修改重定位表项中的内容。

  1. 获取重定位项的数目

    1. 每个重定位块中有块的大小SizeOfBlock,每个重定位项2字节,即可计算出数目

  2. 重定项高四位为0011时,将低12位,与base地址相加,即可得到真实的地址

  3. 将修复后的地址写回

dllmain函数执行

至此,beacon文件已经完成加载,接下来就是跳转到dllmain函数,进行一些初始化的操作。

  1. 从dll的PE头中找到ep地址(+0x28),加上base,即可获取到dllmain函数的地址。

  2. 此时dllmain的fdwReason参数为1,会再次对数据段中的一些数据进行xor解密,包括C2地址、心跳地址、UA等等敏感数据。

  3. 上述结束后,会再次调用dllmain,此时fdwReason参数为4,真正开始执行dllmain

    1. 获取上述解密的数据

    2. 定期与C2服务器进行通信,同样是通过InternetReadFile获取返回的内容,然后根据返回执行对应的指令

shellcode 与服务器通信流量分析

请求完整的stage文件

在stager向服务器请求完整beacon文件时,会向一个url地址发送http请求,这个地址是通过checksum8生成的一个随机字符串。具体的算法过程就是将字符串每个字节的 ASCII 值相加,取低 8 位(即模 256),如果是32位,checksum8的结果为0x92,如果是64位,checksum8的结果为0x93

通过访问符合上述结果的url,即可获取到beacon文件,在shellcode行为分析中,我们知道beacon文件的加密是通过xor进行,而且key就在beacon文件中,因此可以从中解密到初始文件。以下是一些关键信息:

  • RAS加密的公钥

  • 心跳连接地址

  • user-agent

  • 命令执行结果回传地址

加密通信过程

beacon文件中包含后续加密使用的公钥,在后续与服务器的心跳连接中,会先使用公钥加密数据,然后写入到cookie中

  • 心跳默认的地址为:/updates.rss

  • C2服务器的私钥存储在目录下的./cobaltstrike.beacon_keys文件,通过对该文件进行反序列化即可获得私钥

  • cookie中存储的信息为

    • Raw Key:可以从中获取到后续所需的对称加密密钥

    • 被控主机信息:主机名、用户名等

后续,beacon会定期通过心跳连接的形式,对C2地址发送请求,然后服务器会返回待执行的命令,beacon后续通过POST的方式,发送命令执行的结果到服务器,url地址为/submit.php,这个阶段通过之前的对称加密密钥进行加密

流程特征总结

  • stager请求完整beacon

    • url的地址经过checksum8处理之后,结果为0x92/0x93

    • 响应内容比较大(1M以上)

  • 加密通信过程

    • 默认心跳地址:/updates.rss,或者beacon文件中指定地址

    • Cookie:base64加密,不是正常的键值对

    • 响应中的Content-length为0

    • 响应中的Content-type为application/octet-stream

  • 结果回传

    • 默认回传地址:/submit.php,或者beacon文件中指定地址

    • 默认存在id参数

    • POST请求体中的Content-type为application/octet-stream


文章来源: https://www.freebuf.com/articles/endpoint/467537.html
如有侵权请联系:admin#unsafe.sh