简介
开始在HTMLHELP API的一个简单的错误狩猎结束了许多天后作为更好地理解HTMLHELP API和CHM文件格式的内部运作。最初的问题是一个恼人的功能,不断HTMLHELP基于文件锁定,即使它们被关闭后,防止文件的更新,直到你手动关闭所有接触文件的过程。问题只会转起来,如果你使用的上下文ID,不立即关闭您的应用程序后关闭HTMLHELP。奇怪的是,基于主题查找不会引发相同lock.nbsp的;
那么,如果你简单地交换所有基于上下文查找的主题为基础。需要更改代码行的数量之多,将很快停止这种做法 - 但你如何在运行时访问的背景/主题的映射?该解决方案横空出世,而讨厌的:破解CHM文件格式,解码内部流自己, 建立一个背景和主题之间的地图,并使用此地图把所有基于上下文查找主题。
所有的样本都是在C铁杆ITStorage打击将难以如果不是不可能实现在任何其他语言,不支持原生COM和心脏的Windows API的 - 随意证明我错了!
但是,首先压缩的HTML帮助文件的一个非常简要的介绍。入门
HTMLHELP背后的基本想法是移动到一个单一的压缩与扩展CHM文件(或)多个HTML和图形文件。当用户查看文件,内容将被解压缩内部的文件只是在时间,在需要时显示。观众是已经安装了Internet Explorer中,狡猾要么HH.EXE或较模糊控制shdocvw.ocx调用。 CHM扩展的文件,将推出与HH.EXE播放器,而它的扩展的文件将在Internet Explorer中直接推出。这两个扩展具有相同的格式,由控制COM接口ITStorage这将在后面介绍。
,您可以创建您的CHM文件,由Microsoft提供免费的HTML帮助车间。该计划是勉强够用,需要一个单独的HTML编辑器,具有良好的理解ofnbsp相结合;的HTMLHELP格式。确保您搜索的替代品,下面的链接,如果您以任何方式严重关于帮助创造。出于测试目的,我用这个工具创建一个小的帮助项目,有一些众所周知逆向工程。这是关于逆向工程的想法自己的简单文件,使得它较试图扭转工程师一个大文件更容易未知和未映射内容。
创建两个主题和上下文别名为你自己的CHM文件 - 或者干脆偷与本文的源代码发现一个。调用帮助 - HTMLHELP API
要真正CHM文件的链接您的程序,你需要使用MSDN库中的记录{A}。 API是真的只是一个具有以下的功能单一HTMLHELP
签名:// remember to #include <htmlhelp.h>
HWND WINAPI HtmlHelp(
HWND hwndCaller,
LPCSTR pszFile,
UINT uCommand,
DWORD_PTR dwData
);
在第三个参数给定的命令控制API调用的行为。像19种不同的命令是可用的,只有几个,其中最重要的是在下面的例子:{C}
上面的例子使用一个typedef HH_LAST_ERROR没有发现在我的HTMLHELP API版本,但它可以很容易地从{A2}库复制:
大声说话,一言不发!typedef struct tagHH_LAST_ERROR {
int cbStruct;
HRESULT hr;
BSTR description;
} HH_LAST_ERROR;
在最后一章的所有交互是单向的,消息只发送从应用程序的帮助系统。但API支持两种口味的"对讲"从CHM文件到自己的应用程序 - 培训卡和通知消息。
培训卡只是WM_TCARD用户的互动,你的主消息循环发送消息,wParam和一个32位的无符号整数,可选LPCSTR在lParam。他们很容易插在你的HTML文件,使用HTML帮助ActiveX控件命令研讨会向导。<OBJECT type="application/x-oleobject"
classid="clsid:adb880a6-d8ff-11cf-9377-00aa003b7a11"
codebase="hhctrl.ocx#Version=4,74,8793,0" width=100 height=100
>
<PARAM name="Command" value="TCard">
<PARAM name="Button" value="Text:Train">
<PARAM name="Item1" value="#FF0000">9999, SendText">
</OBJECT>
注意参数的可选文本,这是错误地插入我的这个应用程序的版本LT; PARAM NAME ="项目2"... ... GT,而文档及时指出,它应该被插入如上所示。当用户单击在了helpfile按钮,您将能够赶上这一事件在您的消息循环,并用它为自己的歪理邪说目的:BOOL CALLBACK DialogProc(HWND HwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
:
if (uMsg==WM_TCARD)
{
int idAction = (int) wParam;
LPCSTR pText = (LPCSTR) lParam;
printf("TRAIN: %d - %s", idAction, pText);
}
:
很简单的使用和易于集成,如果你有一个需要根据用户的动作发送到自己的应用程序的帮助文件中预定义的消息。
你,而不是需要跟踪机制,认真跟踪您的帮助文件的用户交互,然后开始寻找在通知消息。他们服务的跟踪帮助文件中的每个用户操作的复杂的需要,显然有点困难,安装和使用。首先你必须改变默认的窗口类型设置以启用邮件跟踪。这不是那么容易的,因为它的声音,文档的状态 - 但是,这是我如何做到这一点:#define ID_NOTIFICATION 4242
HH_WINTYPE wt = {0}, *pwt;
LPCSTR pszFile = "test.chm";
LPCSTR pszWin = "test.chm>main";
// get the wintype
HtmlHelp(HwndDlg, pszWin, HH_GET_WIN_TYPE, (DWORD) &pwt);
// copy contents of returned wintype
wt = *pwt;
// set notification id
wt.idNotify = ID_NOTIFICATION;
// rememeber to force the correct size
wt.cbStruct = sizeof(HH_WINTYPE);
// now send the new window type
htmp = HtmlHelp(HwndDlg, szFile, HH_SET_WIN_TYPE, (DWORD) &wt);
显然的通知,可以重新打开关闭的重复这个过程,并留下idNotify为空。
启用通知,从您的CHM文件,将启动的消息弹出消息循环,正确识别自己idNotify常数的wParam parameter.nbsp; lParam参数将指向一个HHN_NOTIFY结构和通知类型在代码读属性。代码应该是指针类型HHN_TRACK可以转换为一个HHNTRACK结构和具体的行动,可以在idAction属性中提取的指针。投入可读冗长的代码,看起来像这样:// Make sure it's the correct notification id
if (uMsg == WM_NOTIFY && wParam == ID_NOTIFICATION)
{
// get notify info
HHN_NOTIFY* pno = (HHN_NOTIFY*) lParam;
// check notification type
if (pno->hdr.code == HHN_WINDOW_CREATE)
printf("HHN_WINDOW_CREATE");
else if (pno->hdr.code == HHN_NAVCOMPLETE)
printf("HHN_NAVCOMPLETE");
else if (pno->hdr.code == HHN_TRACK)
{
printf("HHN_TRACT");
// stringized versions of the HHACT_ enum
LPCSTR pActions[] = { "HHACT_TAB_CONTENTS", "HHACT_TAB_INDEX", ---, "HHACT_NOTES" };
// cast pointer to a HHNTRACK struct
HHNTRACK* ptr = (HHNTRACK*) lParam;
// get correct action as string is possible
if (ptr->idAction >= HHACT_LAST_ENUM)
printf(", Custom=%08X", ptr->idAction);
else
printf(", Action=%s", pActions[ptr->idAction]);
}
else
printf("Unknown code");
printf(", url=%s\n", pno->pszUrl)
}
你现在可以跟踪每个用户点击您的CHM文件。的可能性是无穷的,但是我只能认为一个单一的信息可能使用 - 创建自己的CHM文件统计,狡猾地收集每个主题的点击数,也许最常用的通往每个主题...我没有看到任何错误
足够的互动和示范 - 让我们来看看实际的错误,引发本文使用几行代码,wherenbsp;,您尝试在追加模式打开CHM文件后,立即发出HH_CLOSE_ALL命令:LPCSTR pszFile = "test\\sample.chm";
// show specific existing context
res = HtmlHelp(NULL, pszFile, HH_HELP_CONTEXT, (DWORD) 1001);
// close the htmlhelp window
res = HtmlHelp(NULL, NULL, HH_CLOSE_ALL, NULL);
// open file in append mode
FILE* fp = fopen(pszFile, "a");
if (fp!=NULL)
fclose(fp);
else
printf("Failure, err=%d\n", GetLastError());
失败FOPEN GetLastError函数()返回ERROR_SHARING_VIOLATION纯文本告诉我们:"进程无法访问文件,因为它正由另一个进程使用"。谁是地球上动人的CHM文件,为什么?现在尝试重复测试,但替换命令HH_HELP_CONTEXT HH_DISPLAY_TOPIC和相应的主题:LPCSTR pszFile = "test\\sample.chm";
// show specific existing topic
res = HtmlHelp(NULL, pszFile, HH_DISPLAY_TOPIC, (DWORD) "first.htm");
// close the htmlhelp window
res = HtmlHelp(NULL, NULL, HH_CLOSE_ALL, NULL);
// test the file state
FILE* fp = fopen(pszFile, "a");
if (fp!=NULL)
fclose(fp);
else
printf("Failure, err=%d\n", GetLastError());
和CHM文件上的锁已神秘失踪后发出HH_CLOSE_ALL。
使用进程管理器,你可以清楚地看到,Internet Explorer的DLL将被保存在内存中,即使HH_CLOSE_ALL关闭帮助窗口 - 但是,只有当您使用HH_HELP_CONTEXT的命令!坚持HH_LOOKUP_TOPIC和DLL将被卸载,如预期,CHM文件锁定消失。我最好的猜测是,看完后上下文映射信息"某人"忘记关闭CHM文件内部流...
无论如何 - 一个相当烦人的bug - 但是,我们可以做些什么呢?针放大器;销 - 存储放大器;流
要做到对CHM文件的肮脏勾当,你需要访问"微软??资讯科技存储系统库"托管接口ITStorage itss.dll实施放在你的Windows目录之下的某个地方。接口定义是不是真的公众,但我一点点帮助提取以下,从{A3}:DECLARE_INTERFACE_(IITStorage, IUnknown)
{
STDMETHOD(StgCreateDocfile)
(const WCHAR* pwcsName, DWORD grfMode, DWORD reserved, IStorage** ppstgOpen) PURE;
STDMETHOD(StgCreateDocfileOnILockBytes)
(ILockBytes * plkbyt, DWORD grfMode, DWORD reserved, IStorage ** ppstgOpen) PURE;
STDMETHOD(StgIsStorageFile)
(const WCHAR * pwcsName) PURE;
STDMETHOD(StgIsStorageILockBytes)
(ILockBytes * plkbyt) PURE;
STDMETHOD(StgOpenStorage)
(const WCHAR * pwcsName, IStorage * pstgPriority, DWORD grfMode, SNB snbExclude,
DWORD reserved, IStorage ** ppstgOpen) PURE;
STDMETHOD(StgOpenStorageOnILockBytes)
(ILockBytes * plkbyt, IStorage * pStgPriority, DWORD grfMode, SNB snbExclude,
DWORD reserved, IStorage ** ppstgOpen ) PURE;
STDMETHOD(StgSetTimes)
(WCHAR const * lpszName, FILETIME const * pctime, FILETIME const * patime,
FILETIME const * pmtime) PURE;
STDMETHOD(SetControlData)
(PITS_Control_Data pControlData) PURE;
STDMETHOD(DefaultControlData)
(PITS_Control_Data *ppControlData) PURE;
STDMETHOD(Compact)
(const WCHAR* pwcsName, ECompactionLev iLev) PURE;
};
这几个结构/枚举和匹配的静态的GUID法术声明足以让你去 - 要打开ITStorage的文件,你需要做这样的事情:IITStorage* pITStorage;
IStorage* pStorage;
PWCHAR pwzFile = L"sample.chm";
// don't forget to init COM
CoInitialize(NULL);
// get ITStorage interface
hr = CoCreateInstance(CLSID_ITStorage, NULL, CLSCTX_INPROC_SERVER, IID_ITStorage,
(void **) &pITStorage);
// open the chm-file
hr = pITStorage->StgOpenStorage(pwzFile, NULL, STGM_READ | STGM_SHARE_DENY_WRITE,
NULL, 0, &pStorage);
使用枚举,发现你新开的ITStorage的内容 - 这将转储每个元素的类型和大小的存储根级别的内容:IEnumSTATSTG* pEnum = NULL;
STATSTG entry = {0};
LPCSTR typnam[] = { "STGTY_STORAGE", "STGTY_STREAM", "STGTY_LOCKBYTES",
"STGTY_PROPERTY" };
hr = pStorage->EnumElements(0, NULL, 0, &pEnum);
while (pEnum->Next(1, &entry, NULL)==S_OK)
printf("%S, type=%s, size=%I64u\n", entry.pwcsName, typnam[entry.type-1],
entry.cbSize.QuadPart);
pEnum->Release();
typnam阵列是刚刚添加的指示元素的类型,以改善可读性。请记住,流的大小是64位的无符号整数
每个类型STGTY_STORAGE的元素可以递归地打开和枚举(如目录),和类型STGTY_STREAM的元素可以读入内存(如纯文本文件)。读这样的内部#字符串到内存的特定流,可以做这样的:IStream* pStream = NULL;
PWCHAR pwzStream = L"#STRINGS";
// open stream
hr = pStorage->OpenStream(pwzStream, NULL, STGM_READ, 0, &pStream);
// get size
hr = pStream->Stat(&entry, STATFLAG_NONAME);
ULONG cbRead, cbSize = (ULONG) entry.cbSize.QuadPart;
// allocate buffer
LPVOID pBuffer = (LPVOID) LocalAlloc(LPTR, cbSize);
// read stream
hr = pStream->Read(pBuffer, cbSize, &cbRead);
// write buffer to file
FILE* fp = _wfopen(pwzStream, L"w");
size_t cbWrote = fwrite(pBuffer, sizeof(char), cbRead, fp);
// cleanup
fclose(fp);
LocalFree(pBuffer);
pStream->Release();
这里没有什么 - 只需打开流,获得一个stat调用它的大小,分配匹配的缓冲区,整个流读入缓冲区,并转储它与流的名称相同的文件。这是一样的,因为你需要得到破解。CHM文件格式开始。破解的格式
使用上述技术,您可以建立一个小工具,能够反编译一个HtmlHelp文件的全部内容。对于一个标准的HTMLHELP文件,这将揭示以#开头的几个文件 - 即有趣的内部流:#IDXHDR, type=STGTY_STREAM, size=4096
#ITBITS, type=STGTY_STREAM, size=0
#STRINGS, type=STGTY_STREAM, size=827
#SYSTEM, type=STGTY_STREAM, size=4264
#TOPICS, type=STGTY_STREAM, size=544
#URLSTR, type=STGTY_STREAM, size=1399
#URLTBL, type=STGTY_STREAM, size=408
#WINDOWS, type=STGTY_STREAM, size=400
HH.EXE工具实际上是有能力反编译你的CHM文件,但会留下所有有趣的内部文件。逆向工程是不可能的,因为[别名] hpp文件的部分不能没有#IVB部信息复制使用HH.EXE输出一个帮助项目 - 输入开裂!
使用HTML Help Workshop的几个主题创造一个小的CHM文件,并添加为每个上下文。现在的CHM文件上运行黑客反编译工具,使用您最喜爱的十六进制编辑器(可能不是六)浏览以#前缀的内部文件。这些流的格式相当棘手,但是这是到目前为止我发现: #字符串 - ANSI代表所有的字符串都是空的分离。每个字符串可以被引用其从其他流中的文件偏移。 #窗口 - 所有窗口定义 #系统 - 纯textnbsp HH编译器版本;默认主题名称。 #URLTBL - 文件中的所有URL,9空的分隔。 #IVB部 - 上下文ID#字符串的偏移之间的联系。
注意thenbsp;#IVB部流不存在,如果[别名]部分是帮助项目文件 - 这表明相当多的上下文映射是放在信息位。转换情境,Topicsnbsp;
两个流#字符串和#IVB部是我的目的只有有趣的文件 - 转换到一个主题串的上下文ID。我发现后,一个令人费解的回位来回#IVB部流的格式,像这样(所有32位值):
LT; sizegt; LT;上下文#1gt; LT;#1gt抵消; LT;上下文#2GT; LT;#2GT抵消; ... LT;上下文#NGT; LT;偏移#NGT; LT; sizegt;是该文件的大小 LT; contextgt;表示每次您在定义的别名部分(32位无符号整数)的背景 LT; offsetgt;如果相应的#字符串流项的字节偏移。
这使得逆向工程相当简单 - 打开两个流,读入缓冲区的内容,并再次关闭流:DWORD cbRead, cbSTRINGS, cbIVB, idxContext, idxString;
LPCSTR pSTRINGS, pTopic;
LPDWORD pIVB;
IStream* pStream = NULL;
// read #STRINGS
hr = pStorage->OpenStream(L"#STRINGS", NULL, STGM_READ, 0, &pStream);
hr = pStream->Stat(&entry, STATFLAG_NONAME);
cbSTRINGS = (DWORD) entry.cbSize.QuadPart;
pSTRINGS = (LPCSTR) LocalAlloc(LPTR, cbSTRINGS);
hr = pStream->Read((LPVOID)pSTRINGS, cbSTRINGS, &cbRead);
pStream->Release();
// read #IVB
hr = pStorage->OpenStream(L"#IVB", NULL, STGM_READ, 0, &pStream);
hr = pStream->Stat(&entry, STATFLAG_NONAME);
cbIVB = (DWORD) entry.cbSize.QuadPart;
pIVB = (LPDWORD) LocalAlloc(LPTR, cbIVB);
hr = pStream->Read((LPVOID)pIVB, cbIVB, &cbRead);
pStream->Release();
现在使用闪光灯从上下文ID映射到主题字符串的字符串缓冲区相结合,IVB部的缓冲区 - 只要记住免费使用后的缓冲区。
全部放在一起// show mapping between context and topic - first DWORD unused (contains size)
int nItems = cbIVB/sizeof(DWORD);
for (int i = 1; i<nItems;i+=2)
{
idxContext= pIVB[i];
idxString = pIVB[i+1];
pTopic = (char*)(pSTRINGS) + idxString;
printf("%d=%s\n", idxContext, pTopic);
}
// cleanup
LocalFree((LPVOID)pIVB);
LocalFree((LPVOID)pSTRINGS);
要使用大规模的CHM文件的背景/主题的映射,它很可能是明智的,存储在一个容器能够更多的东西比二进制搜索智能实际地图。我已经使用了STL的地图模板创建一个上下文DWORD值和char指针之间的有效映射到#字符串内存块。只需更换以下上下文映射上述倾销常规(东西离开,以提高可读性)#pragma warning(disable: 4786)
#include <list>
#include <map>
using namespace std;
:
typedef map <DWORD, LPCSTR, less<DWORD>, allocator<LPCSTR> > tMapIntString;
:
// create mapping between context and topic
tMapIntString map;
int nItems = cbIVB/sizeof(DWORD);
for (int i = 1; i<nItems;i+=2)
{
idxContext= pIVB[i];
idxString = pIVB[i+1];
pTopic = (char*)(pSTRINGS) + idxString;
map[idxContext] = pTopic;
}
// cleanup, leave pSTRINGS in memory (used by map)
LocalFree((LPVOID)pIVB);
STL的地图可以很容易地走过这样如果需要的话(使用迭代器类,"第一次"返回键和"老二"返回你的数据):// traverse map
tMapIntString::iterator pos;
for (pos = map.begin(); pos!=map.end(); pos++)
printf("%d=%s\n", (*pos).first, (*pos).second);
使用的地图是现在简单,因为这(使用"查找"功能,返回匹配元素的迭代器和提取数据与参数"第二"):// use the map
pos = map.find(1000);
if (pos != map.end())
HtmlHelp(NULL, pszFile, HH_DISPLAY_TOPIC, (DWORD) (*pos).second);
pos = map.find(1001);
if (pos != map.end())
HtmlHelp(NULL, pszFile, HH_DISPLAY_TOPIC, (DWORD) (*pos).second);
// invalid context
pos = map.find(1002);
if (pos == map.end())
printf("invalid context\n");
// close the chmfile
HtmlHelp(NULL, NULL, HH_CLOSE_ALL, NULL);
// free the map & string buffer
map.clear();
LocalFree((LPVOID)pSTRINGS);
测试表明,创作的背景图是不是一项昂贵的操作,并从上下文翻译主题是相当有效的,用很少的代码行。一个聪明的组成部分,是离开在连续的内存中的字符串缓冲区和滥用字符串是已经分开的事实空字符。示例应用程序 - CHM Explorer中
源文件是一个zip档案,其中包含三个子目录。 中药材 - 样品CHM项目的两个主题和背景,加上培训卡和跟踪所有需要的东西。 Chmexp - 一个VC的项目,使用本文中获得主题/上下文映射和使用(包括trainingcard /跟踪代码)的技术。 样本 - 在一个单一的VC工程贴满本文的所有代码示例。
Chmexp已经塞满了许多功能,以证明本文中所描述的所有问题。
输入CHM文本框一个CHM文件的完整路径,然后按"打开"按钮。 chm文件的状态将被刷新每秒旁边的CHM文件名所示,揭示,如果该文件被锁定或没有。 ,"关闭"将发出一个HH_CLOSE_ALL HTMLHELP API,"倾销",将提取所有的流在进入目录的物理文件,和"Notif"将切换跟踪信息。如果通知是打开的状态中,将显示在helpviewer窗口的导航。
在对话框底部的组合框将包含所有有效contextid后的成功充分open.nbsp和主题;选择或输入的背景下,按"ConTop,"使用背景/主题映射或"语境"发送上下文(调用的锁错误)。选择或输入一个主题,按"主题"按钮,在helpwindow显示该主题。只要和感谢所有的鱼
,但在离开我们吗? Anbsp;容忍神秘的CHM文件锁的解决方法已发现 - 在你的程序开始打开CHM文件,读取内部IVB部的映射和替换所有匹配的HH_LOOKUP_TOPIC呼叫HH_HELP_CONTEXT。 访问的ITStorage为您提供足够的信息来建立一个狡猾的CHM档案总管,揭示先前被隐藏的信息的秘密。 以前的内部格式的帮助文件的一部分已被反向工程的范围内,你可以创建一个改进的编译应用程序。你仍然需要锻炼的内部流的其余部分的格式,但至少在[别名]部分是破解。 帮助文件不再是黑盒子的材料和背景/主题的信息打开有趣的融合的可能性与标准的应用程序的运行时访问。实际上,你可以建立"帮助文件的一致性工具"的测试上下文有效的,哪些是
未使用。
无论如何,玩耍的格式和使更多的用途 - 下面的链接可以帮助你找到更多的信息,关于HTMLHELP格式: {A4} - HTML帮助中的技术信息,很多有关HTML帮助中的技术信息 - 一个必须阅读HTMLHELP编码。 {A5} - 吨的东西和文章的HTML帮助中心。 {A6} - 远HTMLHELP工具,可能的最好,最全面的工具设置的原产地。 {A7} - 杨波CHMUnpacker工具,特别感谢他的初始源代码的援助! {A8}部分ITStorage DEF /使用。