TEB(Thread Environment Block,线程环境块)是每个线程私有的关键数据结构,用于存储与当前线程相关的运行时信息。它位于用户地址空间中,由操作系统内核维护,但可被用户态代码直接访问。同一进程中的不同线程拥有各自的 TEB,但共享同一个 PEB。
TEB有哪些关键信息?
PEB地址
线程局部存储(TLS)
异常处理链
LastErrorValue
如何获取到TEB结构?
32位可以通过FS:[0]访问
在 x64 架构中,通过GS:[0]访问
PEB(Process Environment Block,进程环境块),主要用于保存与进程相关的全局信息。可以通过线程TEB中的PEB指针,直接获取到进程的PEB内容(FS:[30]、GS:[60])。
| 字段 | 用途 |
|---|---|
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 |
专门用于管理进程中所有已加载模块(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;
三个双向链表:
InLoadOrderModuleList:按模块被加载的先后顺序。
InMemoryOrderModuleList:按模块在内存中的地址顺序。
第一项是当前exe文件自己的主模块,dll文件从第二项开始
InInitializationOrderModuleList:按 DllMain 被调用的顺序(仅 DLL)。
_LDR_DATA_TABLE_ENTRY结构体,存储了DLL模块相关的诸多属性,其中关键字段如下:
| 字段偏移 | 字段名 | 类型 | 说明 |
|---|---|---|---|
| +0x00 | InLoadOrderLinks | LIST_ENTRY | 双向链表节点,用于将模块按 加载顺序 链入PEB_LDR_DATA.InLoadOrderModuleList。第一个节点是主 EXE。 |
| +0x08 | InMemoryOrderLinks | LIST_ENTRY | 双向链表节点,用于按 内存基址顺序 链入InMemoryOrderModuleList。遍历时需从此地址减 0x08 得到结构体起始地址。 |
| +0x10 | InInitializationOrderLinks | LIST_ENTRY | 双向链表节点,用于按 DllMain 初始化顺序 链入初始化链表。仅包含 DLL,不包含主 EXE。 |
| +0x18 | DllBase | PVOID | 模块在进程地址空间中的 加载基地址(如0x7c800000)。用于解析导出表和计算 API 地址。 |
| +0x1C | EntryPoint | PVOID | 模块入口点地址(DLL 为DllMain,EXE 为启动函数)。若无入口点则为NULL。 |
| +0x20 | SizeOfImage | ULONG | PE 映像在内存中占用的总大小(按 Section 对齐),单位:字节。 |
| +0x24 | FullDllName | UNICODE_STRING | 模块完整路径(如C:\Windows\System32\kernel32.dll)。包含长度、最大长度和宽字符缓冲区指针。 |
| +0x2C | BaseDllName | UNICODE_STRING | 模块文件名(如kernel32.dll)。shellcode 常通过此字段查找关键系统 DLL。 |
| +0x34 | Flags | ULONG | 模块加载标志。常见值: • 0x4=LDRP_IMAGE_DLL(是 DLL)• 0x1000=LDRP_ENTRY_PROCESSED(已处理) |
| +0x38 | LoadCount | USHORT | 引用计数(LoadLibrary-FreeLibrary次数)。Vista 后通常为0xFFFF(-1),表示永不卸载。 |
| +0x3A | TlsIndex | USHORT | TLS(线程局部存储)索引。未使用 TLS 时为 0。 |
| +0x3C | HashLinks/SectionPointer | LIST_ENTRY或PVOID | 联合体: • 作为 HashLinks:用于内部哈希表• 作为 SectionPointer:指向映射的 section 对象 |
| +0x40 | (联合体内)CheckSum | ULONG | PE 文件头中的校验和(通常为 0,除非是驱动或系统文件)。 |
| +0x44 | TimeDateStamp | ULONG | PE 编译时间戳(与IMAGE_FILE_HEADER.TimeDateStamp一致)。可用于版本识别或完整性检测。 |
由于InMemoryOrderLinks指向的是_LDR_DATA_TABLE_ENTRY结构中的第二项,因此后续的偏移地址计算时需要减去0x08的偏移
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 字节
导出表(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;
| 偏移 | 字段 | 说明 |
|---|---|---|
| +0x00 | Characteristics | 保留字段,始终为 0 |
| +0x04 | TimeDateStamp | 与 PE 文件头一致,可用于版本识别 |
| +0x08 / +0x0A | MajorVersion/MinorVersion | 很少使用,通常为 0 |
| +0x0C | Name | 指向模块名称字符串(如"ntdll.dll") |
| +0x10 | Base | 导出函数的起始序号。例如,若Base = 1,则第一个函数序号为 1。 |
| +0x14 | NumberOfFunctions | 导出地址表(EAT)中的总条目数(包括 forwarder 和空项) |
| +0x18 | NumberOfNames | 有名函数的数量(即可以通过名称查找的函数数) |
| +0x1C | AddressOfFunctions | 指向 导出地址表(Export Address Table, EAT) |
| +0x20 | AddressOfNames | 指向 函数名地址表(Name Pointer Table) |
| +0x24 | AddressOfNameOrdinals | 指向 序号表(Ordinal Table),每个元素是 2 字节(USHORT),表示对应函数在 EAT 中的索引 |
导出地址表:EAT,DWORD[]
导出表存储在 DLL 的“数据段”中(通常是.rdata或.edata节)。若函数地址 在导出表所在段范围内,那么说明该地址并不在代码段,此时,该地址指向一个ascii字符串,格式为"ModuleName.FunctionName",加载器会自动解析该模块,然后将函数地址写入IAT中,也就是导出表的转发功能。
函数名地址表(Name Pointer Table):DWORD[]
每个元素指向一个以\0结尾的 ASCII 函数名
序号表 :WORD[]
由于不是所有的导出函数都有名字、而且函数名地址表是通过字典序排序的,而EAT是按照序号排序的,导致函数和地址无法一一对应,所以需要序号表进行关联
每个元素是 EAT 的索引
验证hModule:hModule必须是由LoadLibrary/GetModuleHandle返回的有效模块基址(即 PE 映像的 ImageBase)。
定位导出表:从该 DLL 的 PE 可选头中读取数据目录项
根据输入类型查找
按名称
遍历AddressOfNames数组(共NumberOfNames项),每项是一个函数名 RVA。
对每个名称,与目标"FunctionName"进行 ASCII 字符串比较。
若找到匹配项,获取其在AddressOfNames中的索引i。
从AddressOfNameOrdinals[i]读取对应的序号偏移值ordinal_index。
最终函数地址 =AddressOfFunctions[ordinal_index](这是一个 RVA)。
将 RVA 转换为 VA(加上 DLL 的 ImageBase),返回给调用者。
按序号
计算索引号
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结构代表一个被当前模块导入的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;
| 字段 | 类型 | 含义 |
|---|---|---|
OriginalFirstThunk | DWORD (RVA) | 指向 INT 数组,保存原始导入信息(不变) |
TimeDateStamp | DWORD | 0 表示普通导入;非 0 表示绑定导入的 DLL 时间戳 |
ForwarderChain | DWORD | 转发链起始索引(-1 表示无转发) |
Name | DWORD (RVA) | 指向 DLL 名称的 ASCII 字符串 |
FirstThunk | DWORD (RVA) | 指向 IAT 数组,加载后被覆盖为函数地址 |
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;
读取 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 位置(运行时调用即从此处跳转)。
重定位表位于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中的函数时使用的
stager的主要功能是通过http请求获取完整的beacon文件,需要加载wininet模块,调用其中网络相关的API。同时为了避免在代码中直接出现关键API名字,被杀软检测到,需要对函数名称进行处理,然后通过特征值进行对比,获取指定函数地址。
具体特征提取算法为:依次读取模块名称ascii字符,将小写转化为大写,然后累加,将结果循环右移14位。
具体过程如下:
将返回地址pop到ebp寄存器
压入特征值(726774c:LoadLibraryExA函数的hash值),以及wininet ascii码( 74 65 6e 69 6e 69 77)
call ebp返回
wininet模块这一部分主要是通过获取到当前进程的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
通过LoadLibraryExA函数加载wininet.dll,然后通过之前的方式,从导出表中获取http相关API的地址,为后续网络请求的发起做准备。
主要的API如下:
| 模块 | API 名称 | 主要功能说明 |
|---|---|---|
| WinINet | InternetOpenA | 初始化 WinINet 会话,返回一个会话句柄,用于后续网络操作。 |
| WinINet | HttpOpenRequestA | 在已建立的连接上创建一个 HTTP 请求句柄,用于后续发送请求。 |
| WinINet | HttpSendRequestA | 向服务器发送 HTTP 请求(包括可选数据体和头部)。 |
| WinINet | InternetReadFile | 从已打开的 WinINet 句柄(如 HTTP 响应)中读取数据到缓冲区。 |
| Kernel32 | VirtualAlloc | 在调用进程的虚拟地址空间中保留、提交或更改内存页的状态。常用于动态分配可执行内存。 |
在本阶段,stager将与C2服务器建立连接,stager会将指定的url发送http请求,C2服务器会返回完整的beacon文件。
具体过程如下:
通过InternetOpenA,初始化一个WinINet 会话,包括user-agent、代理等基本属性。
通过HttpOpenRequestA,创建一个http请求句柄,包括请求方式(get)、路径、http版本等信息。
通过HttpSendRequestA,正式向C2服务器发送请求,在这一步中,会正式与服务器建立TCP连接,并组装完整的http请求报文,并阻塞直到获取到所有服务器返回响应。该过程并不会读取响应。
通过VirtualAlloc申请内存空间,存放beacon文件。
通过InternetReadFile读取返回到指定的内存地址
之前从服务端获取的beacon是加密后的文件,需要进行解密操作,其实就是最简单的异或操作。而key就保存在文件偏移+0x3d的位置,4字节大小。
具体过程如下:
获取beacon长度:作为后续解密的循环次数
通过对内存偏移+0x3d的4字节内存与+0x42四字节内存做异或运算。
解密:
从0x46开始,每4字节与0x3d的key做异或,然后写入当前位置,直到循环结束。
发现解密后的文件以4D5A开头,是一个DLL文件。
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函数的调用。
具体过程如下:
4D 5A 对应的操作是dec ebp;pop edx
通过call 命令(e8 00 00 00 00) 获取当前代码地址,然后通过pop 指令弹出地址。
还原4D 5A的操作push edx;nc ebp
移动ebp,重新开辟栈空间,直接通过偏移跳转到ReflectiveLoader函数的地址,执行ReflectiveLoader函数
由于此时的dll是以文件的形式读取到内存中,没有经过加载器处理,因此,不能直接使用dll文件的导出表中的地址执行,需要转化为该函数在文件中的偏移地址,计算过程简单如下:
首先通过导出表的RVA减去代码段的起始地址(0x1000),获得该函数在代码段中的偏移位置,然后加上文件头的大小(0x400),获得该函数在文件中的偏移地址。
ReflectiveLoader的执行ReflectiveLoader函数是主要功能是完成beacon文件的加载。
为了后续修复导出表的工作,需要通过之前介绍的方式,获取一些关键API的地址,
| API 名称 | 主要功能说明 |
|---|---|
GetProcAddress | 获取指定模块中导出函数的地址(返回函数指针),用于动态调用 DLL 中的函数。 |
GetModuleHandleA | 获取已加载模块的句柄(即基地址)。若模块未加载,返回NULL。不增加引用计数。 |
LoadLibraryA | 将指定 DLL 加载到调用进程的地址空间,返回其模块句柄(基地址)。若已加载,则仅增加引用计数。 |
LoadLibraryExA | LoadLibraryA的扩展版本,支持额外标志(如LOAD_LIBRARY_AS_DATAFILE、DONT_RESOLVE_DLL_REFERENCES等),提供更精细的加载控制。 |
VirtualAlloc | 在调用进程的虚拟地址空间中保留、提交或同时保留+提交一块内存区域。常用于分配可执行内存(配合PAGE_EXECUTE_READWRITE)。 |
VirtualProtect | 更改已提交内存页的保护属性(如从READONLY改为EXECUTE_READWRITE),常用于 shellcode 注入或 JIT 场景。 |
具体过程:
还是通过call 命令,获取当前地址
定位pe文件的Dos 头、PE头
从Kernel32模块的导出表中,找到上述函数的地址
之前的dll文件一直是以文件的形式存在,要使得dll可以被正确运行,首先要进行对齐的调整,因此需要重新申请一块内存,然后以内存映像的形式,将dll复制过去。
通过virtualAlloc申请内存空间,大小为PE头中的SizeOfImage
复制整个PE头,判断是否存在重定位表(PE文件头中Characteristics属性)
复制所有的节块
首先通过PE头文件,获取PE头的大小,跟在后面的就是节表
根据节表中每个节块在文件中的起始偏移、大小,将节块复制到对应的内存空间
进程运行时,内部正常调用dll中的函数,都是通过IAT进行的,这一部分工作,本来应该是动态链接器负责的,现在需要我们手动进行。
在PE文件头+0x80获取导入表位置
在每一个导入描述符中,+0x0c指向dll的文件名
通过LoadLibraryA函数加载该dll
然后通过GetProcAddress获取指定函数的地址,写入对应的IAT
如果该dll存在重定位表,那么还需要根据真实的加载地址,修改重定位表项中的内容。
获取重定位项的数目
每个重定位块中有块的大小SizeOfBlock,每个重定位项2字节,即可计算出数目
重定项高四位为0011时,将低12位,与base地址相加,即可得到真实的地址
将修复后的地址写回
至此,beacon文件已经完成加载,接下来就是跳转到dllmain函数,进行一些初始化的操作。
从dll的PE头中找到ep地址(+0x28),加上base,即可获取到dllmain函数的地址。
此时dllmain的fdwReason参数为1,会再次对数据段中的一些数据进行xor解密,包括C2地址、心跳地址、UA等等敏感数据。
上述结束后,会再次调用dllmain,此时fdwReason参数为4,真正开始执行dllmain
获取上述解密的数据
定期与C2服务器进行通信,同样是通过InternetReadFile获取返回的内容,然后根据返回执行对应的指令
在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