手把手教你让EXE调用自己写的DLL函数实增加功能

查看 122|回复 12
作者:jackyyue_cn   
新手第一次写逆向和改造的过程,难免有错误或走弯路的地方,请大神们多多指正。
本文旨在探讨程序编写技术,相关程序均为自已编写代码测试,请勿用于非法目的。
【情境介绍】注:为避免引起歧义,以下为虚拟情境,如有雷同,纯属巧合。
最近拿到某办公程序(x86):victim.exe,自动和服务器联网。在服务器下达通知消息时,会自动跳出提示,播放声音,几秒后停止。
要求播放的声音需要从功放输出,保证范围内所有人能听见。一般来说功放需要24小时不间断开机,但长时间设备容易老化损坏。
需要改造成在有提示声音播放时再自动打开功放电源,播放完毕自动断开功放电源。在程序源代码中增加这个功能也非常容易。
但本文从另一个角度来探讨:如果不修改源代码,能否实现增加功能呢?
【改造思路】
1、逆向分析EXE文件,找出提示开始和结束的代码;
2、编写自己的DLL文件,以供EXE调用实现功能;
3、改造EXE文件(添加DLL导入函数、打补丁调用函数)。
【知识准备】
1、大致了解EXE文件的结构、加载到内存的方式等
2、反汇编与调试
3、DLL的代码编写
4、EXE修改
【使用工具】
查壳工具:DiE
调试工具:OD
编程工具:vs2010(很老了……)
修改工具:StudyPE+、010Editor
记录工具:Programmer's Notepad
抓包工具:Wireshark
【过程记录】
一、逆向分析EXE文件,找出提示开始和结束的代码
1、上手用DiE查了一下,VC编译win32,无壳,studype查为动态基址。这一步并不是很难,如果是遇到加密加壳的,像我这种新手就无能为力了。
2、直接拖入OD,停在OEP(012BFCCC) 如果没有在OEP点一下小人运行到用户代码就行。接着点一下Az搜索字符串,因为字符串包含大量提示信息,很可能找到我们要的东西


1-1.jpg (69.93 KB, 下载次数: 0)
下载附件
2023-12-6 15:20 上传

3、果然,很快找到VoiceXXX::stop()和VoiceXXX::OnReceiveWarning()之类的信息,应该是写日志用的,字面一看这不就是声音的结束和开始吗,就从这里下手吧。
双击点进去,把地址记录下来,准备打补丁。


1-2.jpg (97.77 KB, 下载次数: 0)
下载附件
2023-12-6 15:26 上传

4、下面就是刚才那两处的地址
可以看出,push了一个字符串,然后call某个函数。
这种就很好打补丁,因为跳转的机器码和push字符串的机器码一样长(后面再说)。
到这里,第一步分析完成。
[Asm] 纯文本查看 复制代码012BAE83 | 68 9CEE2C01              | push victim.12CEE9C                                                    | 12CEE9C:"VoiceXXX::onReceivedWarning() type:"
012BAE88 | FF15 08A62C01            | call dword ptr ds:[

[color=]二、编写自己的DLL文件,以供EXE调用实现功能

这个可以用你自己喜欢的编写DLL的工具,写两个可以静态调用的函数,为了简单,没有参数,也没返回值。
我用vs2010向导建立了WIN32dll项目,取名叫customsg,过程中最好要选一下“导出符号”以便导出函数
我写了两个导出函数:MsgStart和MsgStop, 在函数里就可以做很多事情了,
比如发送系统消息、给串口发数据、访问某个网址等都行。
这里我采用了插入一个串口继电器,给串口发数据控制电源通断的方式。
[C++] 纯文本查看 复制代码//用于导出的函数
// 开始消息,设置dwMsg = 1
CUSTOMSG_API void MsgStart(void)
{
        wMsg = 1;
        PostMessage(HWND_BROADCAST, nPostMsgID, wMsg, dwMsgVar);
       
        SendSerialData(TRUE); //发送命令打开继电器
        PostData(strPostUrl, strPostDataStart);
}
// 停止消息,设置dwMsg = 0
CUSTOMSG_API void MsgStop(void)
{
        wMsg = 0;
        PostMessage(HWND_BROADCAST, nPostMsgID, wMsg, dwMsgVar);
       
        SendSerialData(FALSE);//发送命令关闭继电器
       
        PostData(strPostUrl, strPostDataStop);
}
//其它函数及具体代码自己实现
然后在项目中添加一个def文件用于导出规范名称的函数,要不然函数名是乱码的
[Plain Text] 纯文本查看 复制代码LIBRARY "customsg"
EXPORTS
MsgStart @1
MsgStop @2
编译成功,你就会得到一堆文件。我们只需要找到.dll这个:
customsg.dll
在Debug和Release目录都有,选一个复制到刚才那个victim.exe的目录中,这一步宣告完成。
三、改造EXE文件(添加DLL导入函数、打补丁调用函数)
这一步总体来说有点难度,我也是走了很多弯路才成功,遇到问题多百度可以解决掉大部分
关键在于细致
1、添加DLL导入函数
用StudyPE+打开我的victim.exe,点击“导入”,出现导入dll列表,右键点击任意一个,点击“添加导入函数”


3-1.jpg (60.41 KB, 下载次数: 0)
下载附件
2023-12-6 16:16 上传

浏览找到自己编写的custmsg.dll,从左侧选取要导入的函数(MsgStart、MsgStop),加到右侧,点击“添加”。


3-2.jpg (37.39 KB, 下载次数: 0)
下载附件
2023-12-6 16:19 上传

完成后,点击“文件-另存为”,保存为"victim2.exe",DLL导入函数添加完毕。
2、打补丁调用函数
这里最好稍微有点汇编的知识,百度学习一下也行
(1)再次启动OD,打开导入DLL函数的“victim2.exe",运行到用户代码,直接去我们在第一部分找到的地址处 按F2下个断点 方便找
【这里有个问题,就是动态基址,每次程序加载后地址可能会不一样。如果图简单,可以在刚才StudyPE+里顺便点一下“固定基址”就OK,就不会变了】
我这里就以动态基址的方式来改,也没问题,当作一次学习
这里我们把刚才的地址,和刚才记的OEP地址(012BFCCC)减一下  再和现在的OEP地址(00F6FCCC)减一下,就能算出来了
012BAE83 :    012BFCCC-012BAE83=4E49,  00F6FCCC-4E49=00F6AE83
......
012BA8A1:    012BFCCC-012BA8A1=542B,  00F6FCCC-542B=00F6A8A1
(2)在CPU窗口 按Ctrl+G输入要找的地址直接过去
[Asm] 纯文本查看 复制代码00F6AE83 | 68 9CEEF700              | push victim2.F7EE9C                                                    | F7EE9C:"VoiceXXX::onReceivedWarning() type:"
00F6AE88 | FF15 08A6F700            | call dword ptr ds:[
(3)在代码前后附近找一些空闲空间,放补丁代码
因为程序编译留了许多int3的调试指令,我们需要寻找一大片连续的CC字节 最好在20个以上
按Ctrl+B搜索匹配特征,在十六进制处输入24个连续的CC,按“确定”


3-3.jpg (35.37 KB, 下载次数: 0)
下载附件
2023-12-6 16:58 上传

运气好,搜索出来了在00F72EB2处有,双击点进去


3-4.jpg (26.4 KB, 下载次数: 0)
下载附件
2023-12-6 17:00 上传

(4)果然一大片。我们留一两个CC再开始,我从00F72EB5开始写调用MsgStart的代码
右键,二进制 编辑,
前8字节改为
60 FF 15 CC CC CC CC 61
[Asm] 纯文本查看 复制代码00F72EB5 | 60                       | pushad                                                                 |
00F72EB6 | FF15 CCCCCCCC            | call dword ptr ds:[CCCCCCCC]                                           |
00F72EBC | 61                       | popad                                                                  |
这段代码就可以完成调用某个函数的功能了,CCCCCCCC暂时不管,等会儿改成我们自己的函数地址
接着把00F6AE83的5个字节写过来
[Asm] 纯文本查看 复制代码00F72EBD | 68 9CEEF700              | push victim2.F7EE9C                                                    | F7EE9C:"VoiceXXX::onReceivedWarning() type:"
最后加个跳转指令,跳回原来的call那里
这里直接按空格键,输入"jmp 00F6AE88"就行


3-5.jpg (47.16 KB, 下载次数: 0)
下载附件
2023-12-6 17:13 上传

然后双击jmp那条指令,跳到上面的call,将前面那条指令改成跳转到我们补丁地址 "jmp F72EB5"


3-6.jpg (45 KB, 下载次数: 0)
下载附件
2023-12-6 17:16 上传

这样,就完成了EXE对我的DLL中MsgStart函数的调用框架
梳理一下思路:
在空闲区编写调用DLL函数的代码 -》在EXE中修改一条PUSH指令为JMP到空闲区 -》 空闲区调用DLL函数结束,执行原来的PUSH指令,JMP回去原来地址继续运行
用同样的方法,完成MsgStop函数的调用框架编写


3-7.jpg (34.94 KB, 下载次数: 0)
下载附件
2023-12-6 18:52 上传



3-8.jpg (40.06 KB, 下载次数: 0)
下载附件
2023-12-6 18:52 上传

(5)修复调用的DLL函数的地址
首先,到OD中,点击“符号”,选中左侧第一个"victim2.exe",在右边会出现许多它的导入函数。由于我们添加的DLL在最后,因此我们拉到最下面,会找到导入的两个函数:


3-9.jpg (170 KB, 下载次数: 0)
下载附件
2023-12-6 19:42 上传

记住这两个地址,并分别填写到刚才的CCCCCCCC处【注意低字节在前】
02A04C30 =customsg.MsgStart
02A04C34 =customsg.MsgStop
同时记下左上角黑色显示的那个基址【 00F40000】,下面要用到


3-10.jpg (46.59 KB, 下载次数: 0)
下载附件
2023-12-6 19:45 上传

可以看到,call的函数名称显示正确了
可是不要高兴得太早,因为这是动态基址,下一次运行还会变
(6)我们要计算出它的正常基址,让它再怎么变都能找到。
方法:用函数地址减去上面的基址【00F40000】,再加上正常基址【EXE一般为400000】,用StudyPE+可以查看ImageBase值。
02A04C30-00F40000+400000 =【01EC4C30】    //customsg.MsgStart
02A04C34-00F40000+400000 =【01EC4C34】    //customsg.MsgStop
同样的,上面我们PUSH的那个参数,也是动态地址,也需要减去上面的基址【00F40000】,再加上正常基址【EXE一般为400000】
68 9CEEF700 -》68 【9CEE4300】
68 68EEF700 -》68 【68EE4300】
在OD中,把这4个值对应修改进去


3-11.jpg (43.47 KB, 下载次数: 0)
下载附件
2023-12-6 20:03 上传

(7)记录数据偏移地址
OD里修改好了,等会儿还要在EXE文件里去修改。在动态基址的情况下,这几个值程序在加载的时候会重新计算,因此要找出这几个值在EXE文件加载时的偏移地址。
方法:数据地址减去基址【00F40000】


3-12-2.jpg (65.24 KB, 下载次数: 0)
下载附件
2023-12-6 20:26 上传

将这几个地方都算出来
[Asm] 纯文本查看 复制代码32EB6                 //00F72EB6 | FF15 304CEC01            | call dword ptr ds:[1EC4C30]                                            |
32EBD                //00F72EBD | 68 9CEE4300              | push 43EE9C                                                            |
32ECA                //00F72ECA | FF15 344CEC01            | call dword ptr ds:[1EC4C34]                                            |
32ED1                //00F72ED1 | 68 68EE4300              | push 43EE68                                                            |
由于每个指令前面是操作码FF15或68,后面才是数据,因此要加1到2字节,算出数据的地址:
[Plain Text] 纯文本查看 复制代码32EB8
32EBE
32ECC
32ED2
留着备用。
(8)应用补丁到文件
在OD中点右键,点击“补丁”,在出现的对话框中点击“全选” -》 “修补文件”,另存为"victim3.exe"
另存的时候可能会有个提示 “你的补丁和重定向区域重叠”,点YES就行


3-12.jpg (58.2 KB, 下载次数: 0)
下载附件
2023-12-6 20:07 上传

(9)为我们的补丁增加重定向项目
下载一个最新的010 Editor,打开victim3.exe,会提示你安装EXE模板,点“安装”。
完成后打开的victim3.exe会变得五颜六色,下面也会多出来分析的目录树:


3-13.jpg (169.84 KB, 下载次数: 0)
下载附件
2023-12-6 20:13 上传

在目录树里找到 RelocTable, 可以看到尺寸大小是78D0H


3-14.jpg (32.14 KB, 下载次数: 0)
下载附件
2023-12-6 20:18 上传

向下滑动到下面,找到RelocTable的最后一项点一下。可以看到最后一项后面有许多0,这里可以增加我们自己的重定位项。


3-15.jpg (89.49 KB, 下载次数: 0)
下载附件
2023-12-6 20:18 上传

重定位项的结构可以百度一下。
第1个四字节:VirtualAddress,可以理解为EXE展开到内存中的地址。这个要进行4K对齐,也就是以十六进制1000H为整数倍。
  找到第(7)步记录的偏移地址,看到我们的数据都在32EB8到32ED2,因此第1个VirtualAddress填32000H 【00 20 03 00】
第2个四字节:BlockSize,就是这个重定位块的长度。每个数据2字节 乘以四=8字节,再加上VirtualAddress和BlockSize,一共16字节,因此这里填10H 【10 00 00 00】
接下来是具体的重定位项,就是相对VirtualAddress的偏移量 【数据地址减去VirtualAddress】
根据重定位项定义,最高4个二进位值设置为“3”,表示这是个相对地址,加载时需要重新计算
以第一个数据的重定位项为例:数据地址 - VirtualAddress = 32EB8 - 32000 = EB8
最高4个二进制位设为3,合起来就是 3EB8H  【B8 3E】
同理,可算出其它3个数据的重定位项:【BE 3E】【CC 3E】【D2 3E】
紧挨着上面的数据,依次填充进去 :


3-16.jpg (87.93 KB, 下载次数: 0)
下载附件
2023-12-6 20:52 上传

最后,修改一下重定位表大小,确保我们添加的才能有效
方法:目录树上滑到开头,依次展开 NtHeader -》OptionalHeader -》DataDirArray,
找到一个“BaseRelocationTable”展开,点一下“Size”,看到是【D0 78 00 00】,也就是刚刚我们看到的78D0H
给它加上我们增加的块大小10H,得到78E0H ,所以将上面的数据修改为:【E0 78 00 00】,保存文件或另存为“victim4.exe"


3-17.jpg (75.67 KB, 下载次数: 0)
下载附件
2023-12-6 20:59 上传

(10)大结局
至此,EXE文件的改造工作全部结束。
再次用StudyPE+打开我们改造过后的EXE,查看导入表和重定位表,确认都没有问题。


3-18.jpg (56.12 KB, 下载次数: 0)
下载附件
2023-12-6 21:04 上传



3-19.jpg (66.52 KB, 下载次数: 0)
下载附件
2023-12-6 21:04 上传

运行测试,EXE程序工作正常,串口数据发送正常,电源控制正常。
由于技术生疏,边学边改,以上程序前后经历了一周多才完成。
因此记录下来,希望能给初接触EXE文件修改的新手们一点参考。

宋体, 下载次数

jackyyue_cn
OP
  


xiaolei1314555 发表于 2023-12-21 13:49
24个CC的特征码么有的话怎么办呢

理论上来说,我觉得只要是在EXE中的可执行的代码段范围内,而且程序加载时不会变动,应该都是可以的。
一般在OD的内存布局中找程序的.text段,从开始地址加上大小,得到段结束的地址,从结束地址往回找到一些全是0的空白区域,应该可以添加代码


4-1.jpg (50.57 KB, 下载次数: 0)
下载附件
2023-12-24 15:30 上传

如上图:00B11000 + 00142000 = 00C53000
往回找到00C52850左右就全是0了


4-2.jpg (66.25 KB, 下载次数: 0)
下载附件
2023-12-24 15:30 上传

jackyyue_cn
OP
  


少污污 发表于 2023-12-10 19:16
我看到别人在别的Exe上添加按键增加功能。怎么弄的

这个我还没有做过,我觉得思路可能有以下方面,不一定对
在EXE没有加密加壳的情况下
1、使用可编辑EXE资源的编辑器,增加一个按钮资源,然后用OD去修改按钮消息处理的函数代码,加入自己的按钮代码(也可以写DLL来调用)
2、直接在OD中找到窗口初始化之类的地方 用 CreateWindow函数创建按钮,然后同样找到消息处理函数添加按钮的代码
冥界3大法王   

看完你的贴子,立刻用Delphi写了一个DLL为了增强Everything的功能:
[Delphi] 纯文本查看 复制代码library Strong;
uses
  System.SysUtils,
  System.Classes,
  Windows,
  Unit7 in 'Unit7.pas' {Form7};
{$R *.res}
function GetDllPathX: string;
var
  ModuleName: string;
begin
  SetLength(ModuleName, 255);       // 取得Dll自身路径
  GetModuleFileName(HInstance, PChar(ModuleName), Length(ModuleName));
  Result := PChar(ModuleName);
end;
function LoadWindow(): Boolean;    //加载主窗口1 MainForm
begin
  if Form7 = nil then
  begin
    Form7 := TForm7.Create(nil);
    Form7.Visible := not Form7.Visible;
  end
  else
  begin
    Form7 := Form7.Create(nil);
    Form7.Visible := not Form7.Visible;
  end;
end;
exports
  GetDllPathX,
  LoadWindow;
begin
end.
到  [i]
这一步,我的DLL和你的就对不上路子了。
JMP xx修改后,x64dbg提示,指向可执行内存区域。。。
后边就不知该如何操作了。
bester   

1.studype去掉重定位跟动态基址 你可以省点事儿
2.extern "C" 两个函数声明时加这个 ,可以避免C++函数乱码,据说是因为C+有重载
3.然后其实可以直接inline hook 避免手动汇编
rsice   

感谢楼主分享,不知道何时自己也能做到
wasm2023   

感谢楼主分享
xiaobaixuepj   

感谢大佬分享
jackyyue_cn
OP
  


bester 发表于 2023-12-8 11:17
1.studype去掉重定位跟动态基址 你可以省点事儿
2.extern "C" 两个函数声明时加这个 ,可以避免C++函数乱码 ...

感谢指点,又学到了新的思路
我再去看一看inline hook方面的知识
jackyyue_cn
OP
  


rsice 发表于 2023-12-8 11:57
感谢楼主分享,不知道何时自己也能做到

有动力,学习就能事半功倍,加油!
您需要登录后才可以回帖 登录 | 立即注册

返回顶部