代码注入的三种方法.docx
- 文档编号:6532403
- 上传时间:2023-01-07
- 格式:DOCX
- 页数:23
- 大小:62.57KB
代码注入的三种方法.docx
《代码注入的三种方法.docx》由会员分享,可在线阅读,更多相关《代码注入的三种方法.docx(23页珍藏版)》请在冰豆网上搜索。
代码注入的三种方法
代码注入的三种方法
作者:
RobertKuster
编译:
VCKBASE
原文出处:
ThreeWaystoInjectYourCodeintoAnotherProcess
下载源代码
目录
∙Windows钩子
∙CreateRemoteThread和LoadLibrary技术
——进程间通信
∙CreateRemoteThread和WriteProcessMemory技术
——如何用该技术子类化远程控件
——何时使用CreateRemoteThread和WriteProcessMemory技术
∙结束语
∙附录A
∙附录B
∙附录C
∙附录D
∙附录E
∙附录F
∙参考资料
简介
本文将讨论如何把代码注入不同的进程地址空间,然后在该进程的上下文中执行注入的代码。
我们在网上可以查到一些窗口/密码侦测的应用例子,网上的这些程序大多都依赖Windows钩子技术来实现。
本文将讨论除了使用Windows钩子技术以外的其它技术来实现这个功能。
如图一所示:
图一 WinSpy密码侦测程序
为了找到解决问题的方法。
首先让我们简单回顾一下问题背景。
要“读取”某个控件的内容——无论这个控件是否属于当前的应用程序——通常都是发送WM_GETTEXT消息来实现。
这个技术也同样应用到编辑控件,但是如果该编辑控件属于另外一个进程并设置了ES_PASSWORD式样,那么上面讲的方法就行不通了。
用WM_GETTEXT来获取控件的内容只适用于进程“拥有”密码控件的情况。
所以我们的问题变成了如何在另外一个进程的地址空间执行:
:
:
SendMessage(hPwdEdit,WM_GETTEXT,nMaxChars,psBuffer);
通常有三种可能性来解决这个问题。
1.将你的代码放入某个DLL,然后通过Windows钩子映射该DLL到远程进程;
2.将你的代码放入某个DLL,然后通过CreateRemoteThread和LoadLibrary技术映射该DLL到远程进程;
3.如果不写单独的DLL,可以直接将你的代码拷贝到远程进程——通过WriteProcessMemory——并用CreateRemoteThread启动它的执行。
本文将在第三部分详细描述该技术实现细节;
第一部分:
Windows钩子
范例程序——参见HookSpy和HookInjEx
Windows钩子主要作用是监控某些线程的消息流。
通常我们将钩子分为本地钩子和远程钩子以及系统级钩子,本地钩子一般监控属于本进程的线程的消息流,远程钩子是线程专用的,用于监控属于另外进程的线程消息流。
系统级钩子监控运行在当前系统中的所有线程的消息流。
如果钩子作用的线程属于另外的进程,那么你的钩子过程必须驻留在某个动态链接库(DLL)中。
然后系统映射包含钩子过程的DLL到钩子作用的线程的地址空间。
Windows将映射整个DLL,而不仅仅是钩子过程。
这就是为什么Windows钩子能被用于将代码注入到别的进程地址空间的原因。
本文我不打算涉及钩子的具体细节(关于钩子的细节请参见MSDN库中的SetWindowHookExAPI),但我在此要给出两个很有用心得,在相关文档中你是找不到这些内容的:
1.在成功调用SetWindowsHookEx后,系统自动映射DLL到钩子作用的线程地址空间,但不必立即发生映射,因为Windows钩子都是消息,DLL在消息事件发生前并没有产生实际的映射。
例如:
如果你安装一个钩子监控某些线程(WH_CALLWNDPROC)的非队列消息,在消息被实际发送到(某些窗口的)钩子作用的线程之前,该DLL是不会被映射到远程进程的。
换句话说,如果UnhookWindowsHookEx在某个消息被发送到钩子作用的线程之前被调用,DLL根本不会被映射到远程进程(即使SetWindowsHookEx本身调用成功)。
为了强制进行映射,在调用SetWindowsHookEx之后马上发送一个事件到相关的线程。
在UnhookWindowsHookEx了之后,对于没有映射的DLL处理方法也一样。
只有在足够的事件发生后,DLL才会有真正的映射。
2.当你安装钩子后,它们可能影响整个系统得性能(尤其是系统级钩子),但是你可以很容易解决这个问题,如果你使用线程专用钩子的DLL映射机制,并不截获消息。
考虑使用如下代码:
3.BOOLAPIENTRYDllMain(HANDLEhModule,
4.DWORDul_reason_for_call,
5.LPVOIDlpReserved)
6.{
7.if(ul_reason_for_call==DLL_PROCESS_ATTACH)
8.{
9.//IncreasereferencecountviaLoadLibrary
10.charlib_name[MAX_PATH];
11.:
:
GetModuleFileName(hModule,lib_name,MAX_PATH);
12.:
:
LoadLibrary(lib_name);
13.
14.//Safelyremovehook
15.:
:
UnhookWindowsHookEx(g_hHook);
16.}
17.returnTRUE;
}
那么会发生什么呢?
首先我们通过Windows钩子将DLL映射到远程
进程。
然后,在DLL被实际映射之后,我们解开钩子。
通常当第一个消息到达钩子作用线程时,DLL此时也不会被映射。
这里的处理技巧是调用LoadLibrary通过增加DLLs的引用计数来防止映射不成功。
现在剩下的问题是如何卸载DLL,UnhookWindowsHookEx是不会做这个事情的,因为钩子已经不作用于线程了。
你可以像下面这样做:
o就在你想要解除DLL映射前,安装另一个钩子;
o发送一个“特殊”消息到远程线程;
o在钩子过程中截获这个消息,响应该消息时调用FreeLibrary和UnhookWindowsHookEx;
目前只使用了钩子来从处理远程进程中DLL的映射和解除映射。
在此“作用于线程的”钩子对性能没有影响。
下面我们将讨论另外一种方法,这个方法与LoadLibrary技术的不同之处是DLL的映射机制不会干预目标进程。
相对LoadLibrary技术,这部分描述的方法适用于WinNT和Win9x。
但是,什么时候使用这个技巧呢?
答案是当DLL必须在远程进程中驻留较长时间(即如果你子类化某个属于另外一个进程的控件时)以及你想尽可能少的干涉目标进程时。
我在HookSpy中没有使用它,因为注入DLL的时间并不长——注入时间只要足够得到密码即可。
我提供了另外一个例子程序——HookInjEx——来示范。
HookInjEx将DLL映射到资源管理器“explorer.exe”,并从中/解除影射,它子类化“开始”按钮,并交换鼠标左右键单击“开始”按钮的功能。
HookSpy和HookInjEx的源代码都可以从本文的下载源代码中获得。
第二部分:
CreateRemoteThread和LoadLibrary技术
范例程序——LibSpy
通常,任何进程都可以通过LoadLibraryAPI动态加载DLL。
但是,如何强制一个外部进程调用这个函数呢?
答案是:
CreateRemoteThread。
首先,让我们看一下LoadLibrary和FreeLibraryAPI的声明:
HINSTANCELoadLibrary(
LPCTSTRlpLibFileName//库模块文件名的地址
);
BOOLFreeLibrary(
HMODULEhLibModule//要加载的库模块的句柄
);
现在将它们与传递到CreateRemoteThread的线程例程——ThreadProc的声明进行比较。
DWORDWINAPIThreadProc(
LPVOIDlpParameter//线程数据
);
你可以看到,所有函数都使用相同的调用规范并都接受32位参数,返回值的大小都相同。
也就是说,我们可以传递一个指针到LoadLibrary/FreeLibrary作为到CreateRemoteThread的线程例程。
但这里有两个问题,请看下面对CreateRemoteThread的描述:
1.CreateRemoteThread的lpStartAddress参数必须表示远程进程中线程例程的开始地址。
2.如果传递到ThreadFunc的参数lpParameter——被解释为常规的32位值(FreeLibrary将它解释为一个HMODULE),一切OK。
但是,如果lpParameter被解释为一个指针(LoadLibraryA将它解释为一个串指针)。
它必须指向远程进程的某些数据。
第一个问题实际上是由它自己解决的。
LoadLibrary和FreeLibray两个函数都在kernel32.dll中。
因为必须保证kernel32存在并且在每个“常规”进程中的加载地址要相同,LoadLibrary/FreeLibray的地址在每个进程中的地址要相同,这就保证了有效的指针被传递到远程进程。
第二个问题也很容易解决。
只要通过WriteProcessMemory将DLL模块名(LoadLibrary需要的DLL模块名)拷贝到远程进程即可。
所以,为了使用CreateRemoteThread和LoadLibrary技术,需要按照下列步骤来做:
1.获取远程进程(OpenProcess)的HANDLE;
2.为远程进程中的DLL名分配内存(VirtualAllocEx);
3.将DLL名,包含全路径名,写入分配的内存(WriteProcessMemory);
4.用CreateRemoteThread和LoadLibrary.将你的DLL映射到远程进程;
5.等待直到线程终止(WaitForSingleObject),也就是说直到LoadLibrary调用返回。
另一种方法是,一旦DllMain(用DLL_PROCESS_ATTACH调用)返回,线程就会终止;
6.获取远程线程的退出代码(GetExitCodeThread)。
注意这是一个LoadLibrary返回的值,因此是所映射DLL的基地址(HMODULE)。
在第二步中释放分配的地址(VirtualFreeEx);
7.用CreateRemoteThread和FreeLibrary从远程进程中卸载DLL。
传递在第六步获取的HMODULE句柄到FreeLibrary(通过CreateRemoteThread的lpParameter参数);
8.注意:
如果你注入的DLL产生任何新的线程,一定要在卸载DLL之前将它们都终止掉;
9.等待直到线程终止(WaitForSingleObject);
此外,处理完成后不要忘了关闭所有句柄,包括在第四步和第八步创建的两个线程以及在第一步获取的远程线程句柄。
现在让我们看一下LibSpy的部分代码,为了简单起见,上述步骤的实现细节中的错误处理以及UNICODE支持部分被略掉。
HANDLEhThread;
charszLibPath[_MAX_PATH];//“LibSpy.dll”模块的名称(包括全路径);
void*pLibRemote;//远程进程中的地址,szLibPath将被拷贝到此处;
DWORDhLibModule;//要加载的模块的基地址(HMODULE)
HMODULEhKernel32=:
:
GetModuleHandle("Kernel32");
//初始化szLibPath
//...
//1.在远程进程中为szLibPath分配内存
//2.将szLibPath写入分配的内存
pLibRemote=:
:
VirtualAllocEx(hProcess,NULL,sizeof(szLibPath),
MEM_COMMIT,PAGE_READWRITE);
:
:
WriteProcessMemory(hProcess,pLibRemote,(void*)szLibPath,
sizeof(szLibPath),NULL);
//将"LibSpy.dll"加载到远程进程(使用CreateRemoteThread和LoadLibrary)
hThread=:
:
CreateRemoteThread(hProcess,NULL,0,
(LPTHREAD_START_ROUTINE):
:
GetProcAddress(hKernel32,
"LoadLibraryA"),
pLibRemote,0,NULL);
:
:
WaitForSingleObject(hThread,INFINITE);
//获取所加载的模块的句柄
:
:
GetExitCodeThread(hThread,&hLibModule);
//清除
:
:
CloseHandle(hThread);
:
:
VirtualFreeEx(hProcess,pLibRemote,sizeof(szLibPath),MEM_RELEASE);
假设我们实际想要注入的代码——SendMessage——被放在DllMain(DLL_PROCESS_ATTACH)中,现在它已经被执行。
那么现在应该从目标进程中将DLL卸载:
//从目标进程中卸载"LibSpy.dll"(使用CreateRemoteThread和FreeLibrary)
hThread=:
:
CreateRemoteThread(hProcess,NULL,0,
(LPTHREAD_START_ROUTINE):
:
GetProcAddress(hKernel32,
"FreeLibrary"),
(void*)hLibModule,0,NULL);
:
:
WaitForSingleObject(hThread,INFINITE);
//清除
:
:
CloseHandle(hThread);
进程间通信
到目前为止,我们只讨论了关于如何将DLL注入到远程进程的内容,但是,在大多数情况下,注入的DLL都需要与原应用程序进行某种方式的通信(回想一下,我们的DLL是被映射到某个远程进程的地址空间里了,不是在本地应用程序的地址空间中)。
比如秘密侦测程序,DLL必须要知道实际包含密码的控件句柄,显然,编译时无法将这个值进行硬编码。
同样,一旦DLL获得了秘密,它必须将它发送回原应用程序,以便能正确显示出来。
幸运的是,有许多方法处理这个问题,文件映射,WM_COPYDATA,剪贴板以及很简单的#pragmadata_seg共享数据段等,本文我不打算使用这些技术,因为MSDN(“进程间通信”部分)以及其它渠道可以找到很多文档参考。
不过我在LibSpy例子中还是使用了#pragmadata_seg。
细节请参考LibSpy源代码。
第三部分:
CreateRemoteThread和WriteProcessMemory技术
范例程序——WinSpy
另外一个将代码拷贝到另一个进程地址空间并在该进程上下文中执行的方法是使用远程线程和WriteProcessMemoryAPI。
这种方法不用编写单独的DLL,而是用WriteProcessMemory直接将代码拷贝到远程进程——然后用CreateRemoteThread启动它执行。
先来看看CreateRemoteThread的声明:
HANDLECreateRemoteThread(
HANDLEhProcess,//传入创建新线程的进程句柄
LPSECURITY_ATTRIBUTESlpThreadAttributes,//安全属性指针
DWORDdwStackSize,//字节为单位的初始线程堆栈
LPTHREAD_START_ROUTINElpStartAddress,//指向线程函数的指针
LPVOIDlpParameter,//新线程使用的参数
DWORDdwCreationFlags,//创建标志
LPDWORDlpThreadId//指向返回的线程ID
);
如果你比较它与CreateThread(MSDN)的声明,你会注意到如下的差别:
∙在CreateRemoteThread中,hProcess是额外的一个参数,一个进程句柄,新线程就是在这个进程中创建的;
∙在CreateRemoteThread中,lpStartAddress表示的是在远程进程地址空间中的线程起始地址。
线程函数必须要存在于远程进程中,所以我们不能简单地传递一个指针到本地的ThreadFunc。
必须得先拷贝代码到远程进程;
∙同样,lpParameter指向的数据也必须要存在于远程进程,所以也得将它拷贝到那。
综上所述,我们得按照如下的步骤来做:
1.获取一个远程进程的HANDLE(OpenProces);
2.在远程进程地址空间中为注入的数据分配内存(VirtualAllocEx);
3.将初始的INDATA数据结构的一个拷贝写入分配的内存中(WriteProcessMemory);
4.在远程进程地址空间中为注入的代码分配内存;
5.将ThreadFunc的一个拷贝写入分配的内存;
6.用CreateRemoteThread启动远程的ThreadFunc拷贝;
7.等待远程线程终止(WaitForSingleObject);
8.获取远程来自远程进程的结果(ReadProcessMemory或GetExitCodeThread);
9.释放在第二步和第四步中分配的内存(VirtualFreeEx);
10.关闭在第六步和第一步获取的句柄(CloseHandle);
ThreadFunc必须要遵循的原则:
1.除了kernel32.dll和user32.dll中的函数之外,ThreadFunc不要调用任何其它函数,只有kernel32.dll和user32.dll被保证在本地和目标进程中的加载地址相同(注意,user32.dll并不是被映射到每个Win32的进程)。
如果你需要来自其它库中的函数,将LoadLibrary和GetProcAddress的地址传给注入的代码,然后放手让它自己去做。
如果映射到目标进程中的DLL有冲突,你也可以用GetModuleHandle来代替LoadLibrary。
同样,如果你想在ThreadFunc中调用自己的子例程,要单独把每个例程的代码拷贝到远程进程并用INJDATA为ThreadFunc提供代码的地址。
2.不要使用静态字符串,而要用INJDATA来传递所有字符串。
之所以要这样,是因为编译器将静态字符串放在可执行程序的“数据段”中,可是引用(指针)是保留在代码中的。
那么,远程进程中ThreadFunc的拷贝指向的内容在远程进程的地址空间中是不存在的。
3.去掉/GZ编译器开关,它在调试版本中是默认设置的。
4.将ThreadFunc和AfterThreadFunc声明为静态类型,或者不启用增量链接。
5.ThreadFunc中的局部变量一定不能超过一页(也就是4KB)。
注意在调试版本中4KB的空间有大约10个字节是用于内部变量的。
6.如果你有一个开关语句块大于3个case语句,将它们像下面这样拆分开:
7.switch(expression){
8.caseconstant1:
statement1;gotoEND;
9.caseconstant2:
statement2;gotoEND;
10.caseconstant3:
statement2;gotoEND;
11.}
12.switch(expression){
13.caseconstant4:
statement4;gotoEND;
14.caseconstant5:
statement5;gotoEND;
15.caseconstant6:
statement6;gotoEND;
16.}
END:
或者将它们修改成一个if-elseif结构语句(参见附录E)。
如果你没有按照这些规则来做,目标进程很可能会崩溃。
所以务必牢记。
在目标进程中不要假设任何事情都会像在本地进程中那样(参见附录F)。
GetWindowTextRemote(A/W)
要想从“远程”编辑框获得密码,你需要做的就是将所有功能都封装在GetWindowTextRemot(A/W):
中。
intGetWindowTextRemoteA(HANDLEhProcess,HWNDhWnd,LPSTRlpString);
intGetWindowTextRemoteW(HANDLEhProcess,HWNDhWnd,LPWSTRlpString);
参数说明:
hProcess:
编辑框控件所属的进程句柄;
hWnd:
包含密码的编辑框控件句柄;
lpString:
接收文本的缓冲指针;
返回值:
返回值是拷贝的字符数;
下面让我们看看它的部分代码——尤其是注入数据的代码——以便明白GetWindowTextRemote的工作原理。
此处为简单起见,略掉了UNICODE支持部分。
INJDATA
typedefLRESULT(WINAPI*SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);
typedefstruct{
HWNDhwnd;//编辑框句柄
SENDMESSAGEfnSendMessage;//指向user32.dll中SendMessageA的指针
charpsText[128];//接收密码的缓冲
}INJDATA;
INJDATA是一个被注入到远程进程的数据结构。
但在注入之前,结构中指向SendMessageA的指针是在本地应用程序中初始化的。
因为对于每个使用user32.dll的进程来说,user32.dll总是被映射到相同的地址,因此,SendMessageA的地址也肯定是相同的。
这就保证了被传递到远程进程的是一个有效的指针。
ThreadFunc函数
staticDWORDWINAPIThreadFunc(INJDATA*pData)
{
p
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 代码 注入 方法