chrep.exe是一款用于对文件进行数据批量查找,处理,替换的工具,它的主界面如下图所示:
使用PE分析工具DIE 对其进行分析,发现它是一个使用Visual C/C++编写的程序,使用了MFC静态库,链接器版本为7.10,是一款年代比较久远的软件。
当我们搜索需要处理的字符串以后,统计一栏会展示出我们选中的文件当中,有多少匹配的字符串。
我们的需求是为它增加一个排序的功能,当点击它的列名 ”统计“ 时,它可以按照匹配字符串的数量进行排序。
观察该软件的主界面,很容易可以发现它使用了列表控件,而点击列表控件的列标题,列表控件会向主窗口发送一个WM_NOTIFY消息,我们可以修改该窗口的消息处理函数为自己实现的消息处理函数,对WM_NOTIFY消息做判断,如果是我们想要的消息,就获取列表控件并按照对应字段对其进行排序即可。
于是现在的问题转变为我们如何修改窗口的消息处理函数。我们需要做到在该进程当中执行代码,于是很自然的想到可以加载一个dll到该程序当中,然后在dll当中执行代码。
至于如何加载dll,我们有以下几个思路:
dll劫持是一项很流行的技术,它通过利用微软优先加载当前目录的dll的机制,从而做到将自己的dll加载到目标程序进程中,为了不影响程序的功能,需要将程序从被劫持dll导入的函数全部转发到原先的dll。
但是该方法并不是一个通用的方法,由于系统对HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs注册表项下的dll会优先从系统目录加载,所以如果该程序除了系统dll以外,没有其它依赖的dll的话,就会无法劫持,如果dll导入的函数较多的话,转发起来也会比较麻烦,需要通过写工具来完成。
通过CFF explorer工具查看该程序的导入表,发现确实有dll不在系统的白名单中,且导入的函数也不多,适合劫持:
当操作系统加载程序时,会遍历其导入表,并将其中记录的每一个dll也加载进内存,可以通过修改某个导入表项对应的dll名称,然后系统就会加载修改名称后的dll,然后劫持的dll也导出原先程序导入的那些函数,然后在那些函数中调用修改前dll对应的函数,这样也不影响程序的使用。这也是一种劫持的思路,而且优势在于它不受系统白名单的限制。
可以考虑直接在原先导入表的基础上加入一项,然后填写需要加载的dll的信息,这样也就实现了dll的注入,这种方法也被称为导入表注入,它相对来说比较稳定,也没有什么明显的缺点。硬要说缺点的话,就是通常都没有足够的位置加一项导入表,需要添加节并将导入表整体挪动位置,比较麻烦。
目前的思路就是上面的三种,我选择的方法是第三种,加一个导入项,毕竟前几天刚刚学习了导入表的结构以及添加节的手法,正好可以派上用场。
到这里,已经可以加载dll到目标程序的进程了,现在的问题是在什么时机修改窗口过程函数呢?因为在加载dll时,窗口还没有被创建,所以显然无法直接在dllmain函数中修改,可以考虑的方法是创建一个线程并不断循环查找目标窗口,直到窗口创建完成,查找到对应的窗口后修改过程函数,也可以hook原程序的IAT表,然后在hook的函数中修改,因为到那个时候,窗口肯定已经创建完成了,可以直接找到窗口,而且IAT Hook也是一种比较稳定的hook方式,不存在Inline Hook的种种问题。考虑到刚在科锐学习了导入表的结构,所以考虑写一个IAT Hook,刚好可以加深对导入表的理解。
这下,添加功能的总体流程就确定了:
首先通过数据目录找到其导入表位置:
导入表每一项有20个字节,它必须要以一个全零的导入表结构作为结尾,所以该软件当前的位置很显然不够再添加一项,于是只能将原导入表整体搬到另一个位置,然后再添加项。但是很难确定别处的数据是否会被使用,所以最为稳定的办法就是为其添加一个节,然后将原先导入表全都挪过去。
添加节,首先需要将文件头中的NumberOfSections字段加一,然后添加一个节表。如果原先头部空间不够添加一个节表,还需要扩大头部空间,然后修改SizeOfHeaders字段,并将所有节数据往后移,然后修正所有节表中每个节的文件偏移,也就是PointerToRawData字段。手工操作的话是很麻烦的,可以考虑将该功能工具化,有些PE工具也提供了这个功能。
幸运的是该软件的头部空间足够添加一个节表,所以直接为其添加,并且由于是存放导入信息,所以我为其取名为myidata节。
然后为其添加节数据,直接在文件末尾添加了一个页的数据,并将其内存偏移和文件偏移以及大小填入节表当中。
由于添加了节,所以内存镜像大小也发生了变化,SizeOfImage字段当然也需要增加1000h。上述步骤最好每做一步都测试一下PE是否依然有效,可以及时判断是否修改出现了问题,如果全修改完了但PE格式无效,很难判断是哪个步骤出现了问题。
增加完节之后,就将原先的导入表数据全都拷贝到新节当中,将数据目录中导入表的内存偏移修正,然后就可以新增一个导入表项,并构造新的导入表的数据。
此处我新加一个名叫Inject的dll,并为其导入一个函数名叫MyAdd。(这个叫什么并不重要,主要是为了测试添加导入表项是否成功)
然后也需要编写一个名叫Inject的dll,并导出一个叫做MyAdd的函数,这里我已经提前准备好了,然后将其放入原程序目录下,运行原程序,发现能正常运行,在调试器中查看其模块,发现我的dll确实已经被加载了,说明导入表注入已经成功了。
能加载dll以后,下一步就是在dll当中实现对应的功能。首先就是IAT hook。
IAT hook的原理已经有非常丰富的资料,在此就不做解释,此处我比较在意的一点是该段代码是需要在dllmain中执行的,如果我加载的dllmain执行时,需要hook的函数对应的IAT表项还没有填好,hook就会失败,于是我们就需要分别了解一下系统对IAT表填写的时机以及dllmain执行的时机。
参考《深入解析Windows操作系统 第六版(上册)》的 第3章系统机制 中的3.10 映像加载器 这一节,可以发现在win10系统上,dllmain的调用是在后期处理的过程中完成的,是加载的最后一个步骤,而IAT的填写时机在它之前,所以我这样进行hook并没有问题。
IAT hook的代码实现:
接着,我需要在hook的函数当中修改窗口过程函数,我选择hook的函数是GetMessage,因为这个函数调用的时机一定是在窗口创建之后,而且调用非常频繁。如果有其它合适的函数也可以hook。
下面是我修改窗口过程函数的具体流程,首先通过软件标题获取主窗口的句柄,由于该软件支持多种语言,所以有多种标题的可能,此处全都给它查找一下,考虑到标题重名的可能性,所以判断一下获取到的窗口句柄是否属于该进程。
然后通过IsWindowUnicode函数判断该窗口的字符集,然后调用对应版本的SetWindowLong函数修改窗口的过程函数为自己的函数,这一步骤非常重要,如果调用的函数字符集不对,修改会失败,而且GetLastError也不会报错,是一个非常难以排查的错误,张老师之前也反复强调过这个问题。
修改完之后再调用原先的GetMessageA函数。
由于窗口过程函数修改一次就好,所以使用一个IsModify标志来判断是否已经被修改过,防止反复修改。
代码实现:
接着就是在自己的窗口过程函数中实现具体的功能了。
主要功能就是判断来的消息是否是WM_NOTIFY消息,如果是,就判断它传过来的NMHDR结构体中的code是不是对应LVN_COLUMNCLICK通知号,接着判断控件ID是否是我想要的,然后判断点击的列数是不是我想要的列。如果全都满足要求,那就对列表控件的各个项按个数进行排序。其它情况全都交给原先的窗口过程函数处理。
这里有一个问题,就是如何获取列表控件的ID号,这个ID号存储在原程序的资源节。
我们可以使用vs将原程序以资源文件的形式打开:
vs会自动解析它的资源节,我们就可以从中获取它的ID号为 1012:
此时还有一个小细节,就是列表控件的排序函数SortItem,使用时需要先用SetItemData来为其设置上对应项的标号,否则传给比较函数的值会出现问题,如果想避免这个问题,可以考虑调用它的Ex版本,也就是SortItemsEx函数。
代码实现:
到这个,我们就完成了对数据批量处理软件功能的添加,由于没有这个软件的源码,所以只能通过注入代码,对它打补丁的形式为其添加功能,这种方法也适用于其它一些没有源代码,并且原作者也没有提供插件扩展功能,但是需要对软件功能做调整的情况。但是其中涉及到的知识点以及细节很多,有一步出错就可能导致功能添加失败或者影响到原程序的运行,所以修改的时候需要非常谨慎,如果需要修改软件,就要多做备份,最好每一步都对上一步完成的软件存一个备份,防止出现意外情况。这就是这篇帖子的全部内容了,非常感谢各位的阅读。
科锐47期学员
int
HookIAT(
char
* szModuleName,
char
* szFuncName,
int
NewFuncAddress)
{
if
(szModuleName == NULL || szFuncName == NULL || NewFuncAddress == NULL)
{
return
-1;
}
int
ImageBase = (
int
)GetModuleHandle(NULL);
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNtHeader;
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader;
pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;
if
(pDosHeader->e_magic != 0x5A4D) {
return
-1;
}
pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + ImageBase);
if
(pNtHeader->Signature != 0x00004550) {
return
-1;
}
pOptionalHeader = &pNtHeader->OptionalHeader;
IMAGE_DATA_DIRECTORY DataDirectoryImport = pOptionalHeader->DataDirectory[1];
PIMAGE_IMPORT_DESCRIPTOR pImportDirectory = (PIMAGE_IMPORT_DESCRIPTOR)(DataDirectoryImport.VirtualAddress + ImageBase);
HMODULE
hModule = GetModuleHandle(szModuleName);
if
(hModule == NULL)
{
return
-1;
}
int
pFunc = (
int
)GetProcAddress(hModule, szFuncName);
if
(pFunc == NULL)
{
return
-1;
}
while
(pImportDirectory->OriginalFirstThunk != 0)
{
char
* szDllName = (
char
*)(pImportDirectory->Name + ImageBase);
if
(!
strcmp
(szDllName, szModuleName))
{
DWORD
* pAddress = (
DWORD
*)(pImportDirectory->FirstThunk + ImageBase);
while
(*pAddress != 0)
{
if
(*pAddress == pFunc)
{
DWORD
OldProtect = 0;
VirtualProtect(pAddress, 0x1000, PAGE_EXECUTE_READWRITE, &OldProtect);
*pAddress = (
DWORD
)NewFuncAddress;
VirtualProtect(pAddress, 0x1000, OldProtect, &OldProtect);
break
;
}
pAddress++;
}
break
;
}
pImportDirectory++;
}
return
0;
}
int
HookIAT(
char
* szModuleName,
char
* szFuncName,
int
NewFuncAddress)
{
if
(szModuleName == NULL || szFuncName == NULL || NewFuncAddress == NULL)
{
return
-1;
}
int
ImageBase = (
int
)GetModuleHandle(NULL);
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNtHeader;
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader;
pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;
if
(pDosHeader->e_magic != 0x5A4D) {
return
-1;
}
pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + ImageBase);
if
(pNtHeader->Signature != 0x00004550) {
return
-1;
}
pOptionalHeader = &pNtHeader->OptionalHeader;
IMAGE_DATA_DIRECTORY DataDirectoryImport = pOptionalHeader->DataDirectory[1];
PIMAGE_IMPORT_DESCRIPTOR pImportDirectory = (PIMAGE_IMPORT_DESCRIPTOR)(DataDirectoryImport.VirtualAddress + ImageBase);
HMODULE
hModule = GetModuleHandle(szModuleName);
if
(hModule == NULL)
{
return
-1;
}
int
pFunc = (
int
)GetProcAddress(hModule, szFuncName);
if
(pFunc == NULL)
{
return
-1;
}
while
(pImportDirectory->OriginalFirstThunk != 0)
{
char
* szDllName = (
char
*)(pImportDirectory->Name + ImageBase);
if
(!
strcmp
(szDllName, szModuleName))
{
DWORD
* pAddress = (
DWORD
*)(pImportDirectory->FirstThunk + ImageBase);
while
(*pAddress != 0)
{
if
(*pAddress == pFunc)
{
DWORD
OldProtect = 0;
VirtualProtect(pAddress, 0x1000, PAGE_EXECUTE_READWRITE, &OldProtect);
*pAddress = (
DWORD
)NewFuncAddress;
VirtualProtect(pAddress, 0x1000, OldProtect, &OldProtect);
break
;
}
pAddress++;
}
break
;
}
pImportDirectory++;
}
return
0;
}
BOOL
IsModify = FALSE;
BOOL
WINAPI MyGetMessageA(LPMSG lpMsg,
HWND
hWnd,
UINT
wMsgFilterMin,
UINT
wMsgFilterMax)
{
if
(!IsModify)
{
char
* szWindowNames[3] = {
"全能字符串批量替换机7.0 无限制版(替换/查找/抽取/改名)"
,
"つ匡ゅンい猔種称"
,
"Super Replacer(Replace/Find/extract/rename)"
};
HWND
hWnd = NULL;
bool
IsMyWindow =
false
;
for
(
int
i = 0; hWnd == NULL && i < 3; i++)
{
hWnd = ::FindWindow(NULL, szWindowNames[i]);
}
if
(hWnd != NULL)
{
DWORD
ProcessId = 0;
if
(GetWindowThreadProcessId(hWnd, &ProcessId) != 0)
{
DWORD
MyProcessId = GetCurrentProcessId();
if
(MyProcessId == ProcessId)
{
IsMyWindow =
true
;
}
}
}
if
(IsMyWindow)
{
g_Wnd = hWnd;
BOOL
nRet = IsWindowUnicode(hWnd);
if
(nRet)
{
g_OldWindowProc = (PFN)SetWindowLongW(hWnd, GWL_WNDPROC, (
int
)WindowProc);
}
else
{
g_OldWindowProc = (PFN)SetWindowLongA(hWnd, GWL_WNDPROC, (
int
)WindowProc);
}
IsModify = TRUE;
}
}
return
GetMessageA(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);
}
BOOL
IsModify = FALSE;
BOOL
WINAPI MyGetMessageA(LPMSG lpMsg,
HWND
hWnd,
UINT
wMsgFilterMin,
UINT
wMsgFilterMax)
{
if
(!IsModify)
{
char
* szWindowNames[3] = {
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2024-3-5 15:28
被kanxue编辑
,原因: