查看原文
其他

为无源码的数据批量处理软件添加功能

UltraTC 看雪学苑 2024-04-20




软件简介


chrep.exe是一款用于对文件进行数据批量查找,处理,替换的工具,它的主界面如下图所示:


使用PE分析工具DIE 对其进行分析,发现它是一个使用Visual C/C++编写的程序,使用了MFC静态库,链接器版本为7.10,是一款年代比较久远的软件。






需求


当我们搜索需要处理的字符串以后,统计一栏会展示出我们选中的文件当中,有多少匹配的字符串。

我们的需求是为它增加一个排序的功能,当点击它的列名 ”统计“ 时,它可以按照匹配字符串的数量进行排序





思路


观察该软件的主界面,很容易可以发现它使用了列表控件,而点击列表控件的列标题,列表控件会向主窗口发送一个WM_NOTIFY消息,我们可以修改该窗口的消息处理函数为自己实现的消息处理函数,对WM_NOTIFY消息做判断,如果是我们想要的消息,就获取列表控件并按照对应字段对其进行排序即可。

于是现在的问题转变为我们如何修改窗口的消息处理函数。我们需要做到在该进程当中执行代码,于是很自然的想到可以加载一个dll到该程序当中,然后在dll当中执行代码。

至于如何加载dll,我们有以下几个思路:


1.dll劫持


dll劫持是一项很流行的技术,它通过利用微软优先加载当前目录的dll的机制,从而做到将自己的dll加载到目标程序进程中,为了不影响程序的功能,需要将程序从被劫持dll导入的函数全部转发到原先的dll。

但是该方法并不是一个通用的方法,由于系统对HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs注册表项下的dll会优先从系统目录加载,所以如果该程序除了系统dll以外,没有其它依赖的dll的话,就会无法劫持,如果dll导入的函数较多的话,转发起来也会比较麻烦,需要通过写工具来完成。

通过CFF explorer工具查看该程序的导入表,发现确实有dll不在系统的白名单中,且导入的函数也不多,适合劫持:



2.修改导入表dll对应的模块名称


当操作系统加载程序时,会遍历其导入表,并将其中记录的每一个dll也加载进内存,可以通过修改某个导入表项对应的dll名称,然后系统就会加载修改名称后的dll,然后劫持的dll也导出原先程序导入的那些函数,然后在那些函数中调用修改前dll对应的函数,这样也不影响程序的使用。这也是一种劫持的思路,而且优势在于它不受系统白名单的限制。


3.加一个导入表项


可以考虑直接在原先导入表的基础上加入一项,然后填写需要加载的dll的信息,这样也就实现了dll的注入,这种方法也被称为导入表注入,它相对来说比较稳定,也没有什么明显的缺点。硬要说缺点的话,就是通常都没有足够的位置加一项导入表,需要添加节并将导入表整体挪动位置,比较麻烦。

目前的思路就是上面的三种,我选择的方法是第三种,加一个导入项,毕竟前几天刚刚学习了导入表的结构以及添加节的手法,正好可以派上用场。

到这里,已经可以加载dll到目标程序的进程了,现在的问题是在什么时机修改窗口过程函数呢?因为在加载dll时,窗口还没有被创建,所以显然无法直接在dllmain函数中修改,可以考虑的方法是创建一个线程并不断循环查找目标窗口,直到窗口创建完成,查找到对应的窗口后修改过程函数。也可以hook原程序的IAT表,然后在hook的函数中修改,因为到那个时候,窗口肯定已经创建完成了,可以直接找到窗口,而且IAT Hook也是一种比较稳定的hook方式,不存在Inline Hook的种种问题。考虑到刚在科锐学习了导入表的结构,所以考虑写一个IAT Hook,刚好可以加深对导入表的理解。

总体流程

这下,添加功能的总体流程就确定了:


1.修改原程序的导入表,增加一项来注入我们的dll
2.在我们的dll中Hook原程序的IAT表
3.在hook函数中修改窗口过程函数
4.窗口过程函数中处理WM_NOTIFY消息,对列表控件的Item进行排序






具体步骤


添加导入表项

首先通过数据目录找到其导入表位置:


导入表每一项有20个字节,它必须要以一个全零的导入表结构作为结尾,所以该软件当前的位置很显然不够再添加一项,于是只能将原导入表整体搬到另一个位置,然后再添加项。但是很难确定别处的数据是否会被使用,所以最为稳定的办法就是为其添加一个节,然后将原先导入表全都挪过去。

添加节,首先需要将文件头中的NumberOfSections字段加一,然后添加一个节表。如果原先头部空间不够添加一个节表,还需要扩大头部空间,然后修改SizeOfHeaders字段,并将所有节数据往后移,然后修正所有节表中每个节的文件偏移,也就是PointerToRawData字段。手工操作的话是很麻烦的,可以考虑将该功能工具化,有些PE工具也提供了这个功能。

幸运的是该软件的头部空间足够添加一个节表,所以直接为其添加,并且由于是存放导入信息,所以我为其取名为myidata节。

然后为其添加节数据,直接在文件末尾添加了一个页的数据,并将其内存偏移和文件偏移以及大小填入节表当中。


由于添加了节,所以内存镜像大小也发生了变化,SizeOfImage字段当然也需要增加1000h。上述步骤最好每做一步都测试一下PE是否依然有效,可以及时判断是否修改出现了问题,如果全修改完了但PE格式无效,很难判断是哪个步骤出现了问题。

增加完节之后,就将原先的导入表数据全都拷贝到新节当中,将数据目录中导入表的内存偏移修正,然后就可以新增一个导入表项,并构造新的导入表的数据。

此处我新加一个名叫Inject的dll,并为其导入一个函数名叫MyAdd。(这个叫什么并不重要,主要是为了测试添加导入表项是否成功)


然后也需要编写一个名叫Inject的dll,并导出一个叫做MyAdd的函数,这里我已经提前准备好了,然后将其放入原程序目录下,运行原程序,发现能正常运行,在调试器中查看其模块,发现我的dll确实已经被加载了,说明导入表注入已经成功了。


能加载dll以后,下一步就是在dll当中实现对应的功能。首先就是IAT hook。

IAT hook实现

IAT hook的原理已经有非常丰富的资料,在此就不做解释,此处我比较在意的一点是该段代码是需要在dllmain中执行的,如果我加载的dllmain执行时,需要hook的函数对应的IAT表项还没有填好,hook就会失败,于是我们就需要分别了解一下系统对IAT表填写的时机以及dllmain执行的时机

参考《深入解析Windows操作系统 第六版(上册)》的 第3章系统机制 中的3.10 映像加载器 这一节,可以发现在win10系统上,dllmain的调用是在后期处理的过程中完成的,是加载的最后一个步骤,而IAT的填写时机在它之前,所以我这样进行hook并没有问题。


IAT hook的代码实现:

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;

//此处暂时不考虑64位的情况,所以直接定义32位的选项头
PIMAGE_OPTIONAL_HEADER32 pOptionalHeader;

pDosHeader = (PIMAGE_DOS_HEADER)ImageBase;
if (pDosHeader->e_magic != 0x5A4D) {
return -1;
}

//获取NT头
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);

//获取需要hook的函数所在的模块对应的模块句柄
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;
}


修改窗口过程函数

接着,我需要在hook的函数当中修改窗口过程函数,我选择hook的函数是GetMessage,因为这个函数调用的时机一定是在窗口创建之后,而且调用非常频繁。如果有其它合适的函数也可以hook。

下面是我修改窗口过程函数的具体流程,首先通过软件标题获取主窗口的句柄,由于该软件支持多种语言,所以有多种标题的可能,此处全都给它查找一下,考虑到标题重名的可能性,所以判断一下获取到的窗口句柄是否属于该进程。

然后通过IsWindowUnicode函数判断该窗口的字符集,然后调用对应版本的SetWindowLong函数修改窗口的过程函数为自己的函数,这一步骤非常重要,如果调用的函数字符集不对,修改会失败,而且GetLastError也不会报错,是一个非常难以排查的错误,张老师之前也反复强调过这个问题。

修改完之后再调用原先的GetMessageA函数。

由于窗口过程函数修改一次就好,所以使用一个IsModify标志来判断是否已经被修改过,防止反复修改。

代码实现:

//记录过程函数是否已经被修改
BOOL IsModify = FALSE;

BOOL WINAPI MyGetMessageA(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax)
{
if (!IsModify)
{
char* szWindowNames[3] = {
"全能字符串批量替换机7.0 无限制版(替换/查找/抽取/改名)", //Chinese(S)
"つ匡ゅンい猔種称", //Chinese(T)
"Super Replacer(Replace/Find/extract/rename)" //English
};

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

//调用原先被hook的函数
return GetMessageA(lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);
}

接着就是在自己的窗口过程函数中实现具体的功能了。

窗口过程函数功能实现

主要功能就是判断来的消息是否是WM_NOTIFY消息,如果是,就判断它传过来的NMHDR结构体中的code是不是对应LVN_COLUMNCLICK通知号,接着判断控件ID是否是我想要的,然后判断点击的列数是不是我想要的列。如果全都满足要求,那就对列表控件的各个项按个数进行排序。其它情况全都交给原先的窗口过程函数处理。

这里有一个问题,就是如何获取列表控件的ID号,这个ID号存储在原程序的资源节。

我们可以使用vs将原程序以资源文件的形式打开:


vs会自动解析它的资源节,我们就可以从中获取它的ID号为 1012:


此时还有一个小细节,就是列表控件的排序函数SortItem,使用时需要先用SetItemData来为其设置上对应项的标号,否则传给比较函数的值会出现问题,如果想避免这个问题,可以考虑调用它的Ex版本,也就是SortItemsEx函数。

代码实现:

#define ID_LISTCTRL 1012

//旧的窗口过程函数
PFN g_OldWindowProc = NULL;

//记录过程函数是否已经被修改
BOOL IsModify = FALSE;

HWND g_Wnd;

int g_nClickedColumn;
CListCtrl* g_pListCtrl;
bool IsReverseOrder = false;
int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
CListCtrl* pListCtrl = g_pListCtrl;
CString strItem1 = pListCtrl->GetItemText(lParam1, g_nClickedColumn);
CString strItem2 = pListCtrl->GetItemText(lParam2, g_nClickedColumn);

int nItem1 = StrToInt(strItem1);
int nItem2 = StrToInt(strItem2);
int ret = nItem2 - nItem1;

if (!IsReverseOrder)
{
ret = 0 - ret;
}

return ret;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (uMsg == WM_NOTIFY)
{
NMHDR* pNMHDR = (NMHDR*)lParam;
if (pNMHDR->code == LVN_COLUMNCLICK)
{
NMLISTVIEW* pNMLV = (NMLISTVIEW*)pNMHDR;
if (pNMLV->hdr.idFrom == ID_LISTCTRL) // 检查控件ID,确保是你要处理的列表控件
{
HWND hListCtrl = ::GetDlgItem(g_Wnd, ID_LISTCTRL);
CListCtrl* pListCtrl = reinterpret_cast<CListCtrl*>(CListCtrl::FromHandle(hListCtrl));
g_pListCtrl = pListCtrl;

g_nClickedColumn = pNMLV->iSubItem;
if (g_nClickedColumn == 2)
{
//将每一项进行排序
pListCtrl->SortItemsEx(CompareFunc, (LPARAM)pListCtrl);

//再次点击的话,就将顺序颠倒
IsReverseOrder = !IsReverseOrder;
}
return TRUE;
}
}
}

return g_OldWindowProc(hwnd, uMsg, wParam, lParam);
}





总结


到这个,我们就完成了对数据批量处理软件功能的添加,由于没有这个软件的源码,所以只能通过注入代码,对它打补丁的形式为其添加功能,这种方法也适用于其它一些没有源代码,并且原作者也没有提供插件扩展功能,但是需要对软件功能做调整的情况。

但是其中涉及到的知识点以及细节很多,有一步出错就可能导致功能添加失败或者影响到原程序的运行,所以修改的时候需要非常谨慎,如果需要修改软件,就要多做备份,最好每一步都对上一步完成的软件存一个备份,防止出现意外情况。




看雪ID:UltraTC

https://bbs.kanxue.com/user-home-980660.htm

*本文为看雪论坛精华文章,由 UltraTC 原创,转载请注明来自看雪社区



# 往期推荐

1、移动应用安全与风控——应用分析常用工具

2、Nep CTF password:rc4和换表base64算法分析

3、摘除MiniFilter回调的正确姿势

4、bpf在android逆向中的辅助效果

5、NDK集成OLLVM模块流程记录

6、【NKCTF】babyHeap-Off by one&Tcache Attack




球分享

球点赞

球在看


点击阅读原文查看更多

继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存