首页
社区
课程
招聘
[原创] 为无源码的数据批量处理软件添加功能
2024-2-27 15:06 9730

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

2024-2-27 15:06
9730

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

目录

软件简介

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不在系统的白名单中,且导入的函数也不多,适合劫持:

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

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

  1. 加一个导入表项

可以考虑直接在原先导入表的基础上加入一项,然后填写需要加载的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的代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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标志来判断是否已经被修改过,防止反复修改。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
//记录过程函数是否已经被修改
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函数。

代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#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);
}

总结

到这个,我们就完成了对数据批量处理软件功能的添加,由于没有这个软件的源码,所以只能通过注入代码,对它打补丁的形式为其添加功能,这种方法也适用于其它一些没有源代码,并且原作者也没有提供插件扩展功能,但是需要对软件功能做调整的情况。但是其中涉及到的知识点以及细节很多,有一步出错就可能导致功能添加失败或者影响到原程序的运行,所以修改的时候需要非常谨慎,如果需要修改软件,就要多做备份,最好每一步都对上一步完成的软件存一个备份,防止出现意外情况。这就是这篇帖子的全部内容了,非常感谢各位的阅读。

科锐47期学员


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2024-3-5 15:28 被kanxue编辑 ,原因:
上传的附件:
收藏
点赞11
打赏
分享
最新回复 (10)
雪    币: 29414
活跃值: (18690)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
kanxue 8 2024-2-27 15:15
2
1
感谢分享!文章写的不错,分析和动手能力很强!
chrep这工具在在批量文本(如程序代码,或网站程序),批量查找替换(可以用正则匹配),非常方便。
雪    币: 162
活跃值: (616)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
UltraTC 1 2024-2-27 15:31
3
0
非常感谢坛主认可
雪    币: 162
活跃值: (616)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
UltraTC 1 2024-2-27 15:32
4
0
还有很多不足之处,今后一定好好努力
雪    币: 3123
活跃值: (2806)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
dayday向上8 2 2024-2-27 16:36
5
0
写得好,期待更多文章
雪    币: 162
活跃值: (616)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
UltraTC 1 2024-2-27 16:55
6
0
dayday向上8 写得好,期待更多文章

感谢大佬支持

最后于 2024-2-27 16:56 被UltraTC编辑 ,原因:
雪    币: 2481
活跃值: (2726)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
院士 2024-2-27 23:08
7
0
好深奥。
雪    币: 19299
活跃值: (28938)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2024-2-28 09:44
8
1
感谢分享
雪    币: 7086
活跃值: (2614)
能力值: (RANK:520 )
在线值:
发帖
回帖
粉丝
netwind 13 2024-2-29 15:42
9
0
好文,感谢分享!
雪    币: 25818
活跃值: (3508)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
sky东 2024-3-6 17:21
10
0
win7下运行不了
雪    币: 216
活跃值: (829)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
iaoedsz2018 2024-3-14 15:03
11
0
写得好
游客
登录 | 注册 方可回帖
返回