本文最后更新于:2022年4月13日 下午
宽字符
C语言中的宽字符
在C语言中如何使用上一章所述的编码格式表示字符串。
1 2
| ASCII码:char strBuff[] = "中国" Unicode编码(UTF-16):wchar_t strBuff[] = L"中国"
|
之前加上L是因为如果你不加的话,编译器会默认使用当前文件的编码格式去存储,所以我们需要加上。(注意使用这个的时候需要包含stdio.h这个头文件)
Unicode编码这种表现形式实际上就是宽字符,所以在提起宽字符的时候我们就应该想到这种方式。
ASCII编码和Unicode编码在内存中的存储方式不一样,所以我们使用相关函数的时候也要注意,如下图所示,ASCII编码使用左边的,而Unicode则是右边的:
例如我们想要在控制台中打印一个宽字符的字符串:
再一个例子就是字符串的长度:
char strBuff[] = “China”;
wchar_t strBuff1[] = L”China”;
strlen(strBuff); //取得多字节字符串中字符长度,不包含 00
wcslen(strBuff1); //取得多字节字符串中字符长度,不包含 00 00
Win32 API中的宽字符
了解什么是Win32 API
Win32 API就是Windows操作系统提供给我们的函数(应用程序接口),其主要存放在C:\Windows\System32(存储的DLL是64位)、C:\Windows\SysWOW64(存储的DLL是32位)下面的所有DLL文件(几千个)。
重要的DLL文件:
- Kernel32.dll:最核心的功能模块,例如内存管理、进程线程相关的函数等;
- User32.dll:Windows用户界面相关的应用程序接口,例如创建窗口、发送信息等;
- GDI32.dll:全称是Graphical Device Interface(图形设备接口),包含用于画图和显示文本的函数。
在C语言中我们想要使用Win32 API的话直接在代码中包含windows.h这个头文件即可。
比如我们想要弹出一个提示窗口,Win32 API文档中弹窗API的格式如下:
1 2 3 4 5 6
| int MessageBox( HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType );
|
这个代码可能看起来非常可怕,好像我们都没有接触过,但实际上其不是什么新的类型,所谓的新的类型无非就是给原有的类型重新起了一个名字,这样做是为了将所有类型统一化,便于读写,如果涉及到跨平台的话将原来的类型修改一下就好了,无需对代码进行重写。
例如以上代码中的类型LPCTSTR,实际上我们跟进一下代码(选中F12)会发现其本质就是const char *这个类型,只不过是换了一个名字罢了。
常用的数据类型在Win32中都重新起了名字:
在Win32中使用字符串
字符类型:
CHAR strBuff[] = “中国”; // char
WCHAR strBuff[] = L”中国”; // wchar_t
TCHAR strBuff[] = TEXT(“中国”); // TCHAR 根据当前项目的编码自动选择char还是wchar_t,在Win32中推荐使用这种方式
字符串指针:
PSTR strPoint = “中国”; // char*
PWSTR strPoint = L”中国”; // wchar_t*
PTSTR strPoint = TEXT(“中国”); // PTSTR 根据当前项目的编码自动选择如char还是wchar_t,在Win32中推荐使用这种方式
各种版本的MessageBox
MessageBox,其实际上本质就是MessageBoxW和MessageBoxA:
MessageBoxA只接受ASCII编码的参数,而MessageBoxW则只接受Unicode编码的参数。
从本质上来讲,Windows字符串都是宽字符的,所以使用MessageBoxW这种方式性能会更好一些,因为当你使用MessageBoxA的时候,在到内核的时候(系统底层)其会转化Unicode,所以性能相对差一些。
弹框调用如下:
1 2 3
| CHAR strTitle[] = "Title"; CHAR strContent[] = "Hello World!"; MessageBoxA(0, strContent, strTitle, MB_OK);
|
1 2 3
| WCHAR strTitle[] = L"Title"; WCHAR strContent[] = L"Hello World!"; MessageBoxW(0, strContent, strTitle, MB_OK);
|
1 2 3
| TCHAR strTitle[] = TEXT("Title"); TCHAR strContent[] = TEXT("Hello World!"); MessageBox(0, strContent, strTitle, MB_OK);
|
消息机制
什么是消息
当我们点击鼠标的时候,或者当我们按下键盘的时候,操作系统都要把这些动作记录下来,封装到一个结构体中,这个结构体就是消息。
1 2 3 4 5 6 7 8
| typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG, *PMSG;
|
消息的产生与处理流程
从消息发起这个点开始说,假设我们点击了某个窗口时就会产生一个消息,操作系统得到这个消息后先判断当前点击的是哪个窗口,找到对应的窗口对象,再根据窗口对象的里的某一个成员找到对应线程,一旦找到了对应线程,操作系统就会把封装好的消息(这是一个结构体,包含了你鼠标点击的坐标等等消息)存到对应的消息队列里,应用程序就会通过GetMessage不停的从消息队列中取消息。
能产生消息的情况有四种情况:1. 键盘 2. 鼠标 3. 其他应用程序 4. 操作系统内核程序,有这么多消息要处理,所以操作系统会将所有消息区分类别,每个消息都有独一无二的编号。
消息这个结构体存储的信息也不多,只能知道消息属于哪个窗口,根本不知道对应窗口函数是什么,所以我们不得不在之后对消息进行分发(DispatchMessage函数),而后由内核发起调用来执行窗口函数:
1 2 3 4 5 6
| LRESULT CALLBACK WindowProc( IN HWND hwnd, IN UINT uMsg, IN WPARAM wParam, IN LPARAM lParam );
|
换而言之,我们这个消息的结构体实际上就是传递给了窗口函数,其四个参数对应着消息结构体的前四个成员。
消息队列
每个线程只有一个消息队列。
消息类型
我们想要关注自己想要关注的消息类型,首先可以在窗口函数中打印消息类型来看看都有什么消息类型:
1 2 3 4 5 6 7 8
| LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { char szOutBuff[0x80]; sprintf(szOutBuff, "Message: %x - %x \n", hwnd, uMsg); OutputDebugString(szOutBuff); return DefWindowProc(hwnd, uMsg, wParam, lParam); }
|
可以看见这边输出了一个0x1,想要知道这个对应着什么,我们可以在C:\Program Files\Microsoft Visual Studio\VC98\Include目录中找到WINUSER.H这个文件来查看,搜索0x0001就可以找到:
那么我们可以看见对应的宏就是WM_CREATE,这个消息的意思就是窗口创建,所以我们有很多消息是不需要关注的,而且消息时刻都在产生,非常非常多。
处理窗口关闭
在窗口关闭时,实际上进程并不会关闭,所以我们需要在窗口函数中筛选条件,当窗口关闭了就退出进程。
1 2 3 4 5 6 7 8 9 10
| LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { case WM_DESTROY: { PostQuitMessage(0); break; } }
|
1 2 3
| return DefWindowProc(hwnd, uMsg, wParam, lParam); }
|
处理键盘按下
我们除了可以处理窗口关闭,处理键盘按下也是没问题的,键盘按下的宏是WM_KEYDOWN,但是我们想要按下a这个键之后才处理该怎么办?首先我们需要查阅一下MSDN Library:
1 2 3 4 5 6
| LRESULT CALLBACK WindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
|
可以很清楚的看见窗口函数的第三个参数就是虚拟键码(键盘上每个键都对应一个虚拟键码),我们可以输出下按下a,其对应虚拟键码是什么:
1 2 3 4 5 6 7 8 9 10 11 12
| LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(uMsg) { case WM_KEYDOWN: { char szOutBuff[0x80] sprintf(szOutBuff, "keycode: %x \n", wParam) OutputDebugString(szOutBuff) break } }
|
1 2 3
| return DefWindowProc(hwnd, uMsg, wParam, lParam); }
|
如上图所示,按下a之后输出的虚拟键码是0x41,所以我们可以根据这个来进行判断。
转换消息
之前我们举例可以处理键盘按下的消息,但是我们想要直观的看到底输入了什么而不是虚拟键码该怎么办?这时候我们就需要使用WM_CHAR这个宏了,但是在这之前,我们的消息是必须要经过转换的,只有其转换了,我们的虚拟键码才能变成具体的字符。
WM_CHAR宏对应的窗口函数参数作用如下:
1 2 3 4 5 6
| LRESULT CALLBACK WindowProc( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam )
|
第三个参数就是字符所以我们直接输出这个即可:
窗口与线程
当我们把鼠标点击左边窗口关闭按钮,为什么它会关闭,这个关闭(坐标、左右键…)操作系统会封装到结构体里(消息),那么这个消息如何精确的传递给对应进程的线程呢?
那是因为操作系统可以将坐标之类的作为索引,去找到对应的窗口,窗口在内核中是有窗口对象的,而这个窗口对象就会包含一个成员,这个成员就是线程对象的指针,线程又包含了消息,所以这样一个顺序就很容易理解了。
注意:一个线程可以有多个窗口,但是一个窗口只属于一个线程。
消息堆栈
调用消息处理函数(WinProc)形成的栈帧如下所示,这里假定使用ESP寻址方式:
如上可以看到,ESP+8就是我们需要的消息类型参数,基于此可以条件断点(消息断点)便于我们调试。
调试流程
Win32应用程序入口识别
还记得WinMain这个入口函数长什么样吗,它的特征是会有四个参数。也就是在反汇编调试的时候会对应着四个PUSH。
1 2 3 4
| int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
|
其中还有一个PUSH是GetModuleHandle函数的返回值(存在EAX)里,这些都是WinMain函数的特征。
窗口回调函数的定位
在定位到WinMain函数后,我们可以进一步定位到RegisterClass函数:
通过该函数我们可以找到我们定义的WNDCLASS结构体,进而拿到消息处理函数的地址:
具体事件的处理的定位
Windows的消息是多种多样的,我们需要快速筛选出我们需要关注的消息。这个可以通过条件断点来实现,在401120处打上断点,在断点界面输入相应的条件即可完成筛选:
含有子窗口的回调函数的定位
以按钮为例,按钮是一个特殊的子窗口,它的回调函数是由操作系统提供的。当我们点击按钮时,将按下图所示进行处理:
graph LR;
按钮--单击按钮-->Windows提供的WinProc函数
Windows提供的WinProc函数--转化为WM_COMMAND-->父窗口的WinProc
一个典型的C语言子窗口事件处理逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| case WM_COMMAND: { switch(LOWORD(wParam)) { case 1001: MessageBox(hwnd,"Hello Button 1","Demo",MB_OK); return 0; case 1002: MessageBox(hwnd,"Hello Button 2","Demo",MB_OK); return 0; case 1003: MessageBox(hwnd,"Hello Button 3","Demo",MB_OK); return 0; } return DefWindowProc(hwnd,uMsg,wParam,lParam); }
|
其中1001-1003是调用创建窗口函数时传入的菜单句柄参数。由上我们可以看到,在转成WM_COMMAND后的菜单句柄参数将放在wParam参数的低字中。
1 2 3 4 5 6 7 8 9 10 11
| hwndPushButton = CreateWindow( TEXT("button"), TEXT("普通按钮"), WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | BS_DEFPUSHBUTTON, 10, 10, 80, 20, hwnd, (HMENU)1001, hAppInstance, NULL);
|
所以在调试的时候,我们通过条件断点筛选出WM_COMMAND(0x111)再进行处理即可。
复杂程序的回调函数的定位
当我们要逆向的程序十分复杂,可能无法直接找到消息处理函数的时候,需要通过间接的方式帮助我们找到消息断点。
在Ollydbg的Windows窗口中,可以看到我们需要跟踪的窗口,在上面下消息断点即可。
Button在之前介绍过是先调用系统的消息处理函数再转换成WM_COMMAND消息传递给开发者自己编写的消息处理函数。
于是乎,我们可以在用户代码段下一个内存访问断点,当操作系统调用自定义的消息处理函数时就会触发。
这里注意有很多个.text段,需要找到自己的PE文件对应的.text段
设置内存访问断点后F9运行,就会断到用用户空间的消息处理函数。
这里需要同样注意的是,当我们点击按钮时可能不止产生一种消息,我们还需要通过消息堆栈中的消息类型来判断是否是我们关注的消息类型。在这里我们需要关注的是WM_COMMAND(0x111)。而最初的消息是0x135:
这个时候虽然不是我们关注的消息类型,但是我们已经找到了消息处理函数的位置了。我们可将内存访问断点取消并用消息处理函数的第一行代码上下断点代替,之后再次F9运行代码,这里得到的就是我们关注的0x111消息了,之后继续跟进即可。
线程
线程创建
举例说明之,在Win32程序中,如果有循环或者其他占用时间较长的代码,主线程的消息处理函数将无法得到正常执行。这个时候需要引入新的线程来完成业务逻辑。
1 2 3 4 5 6 7 8 9 10 11 12
| HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId );
|
创建线程时提供参数
向线程传递参数,如下图所示,我们想要自定义线程执行for循环的次数,将n传递进去,这时候需要注意参数传递到线程参数时在堆栈中存在,并且传递的时候需要强制转换一下:
这里有一个坑点是需要考虑参数的生命周期问题,如MyTest()这样的函数在执行完后会销毁堆栈,亦即清空其中的局部变量,当新的线程尝试引用这些变量时将发生错误。
解决方法之一是将参数放在全局变量区:
线程控制
Sleep
Sleep函数是让当前执行到本函数时延迟指定的毫秒之后再向下走,例如:
1 2 3 4
| for(int i = 0; i < 100; i++) { Sleep(500); printf("------ %d \n", i); }
|
线程挂起与恢复
SuspendThread
SuspendThread函数用于暂停(挂起)某个线程,当暂停后该线程不会占用CPU,其语法格式很简单,只需要传入一个线程句柄即可:
1 2 3
| DWORD SuspendThread( HANDLE hThread );
|
ResumeThread
ResumeThread函数用于恢复被暂停(挂起)的线程,其语法格式也很简单,只需要传入一个线程句柄即可:
1 2 3
| DWORD ResumeThread( HANDLE hThread );
|
需要注意的是,挂起几次就要恢复几次。
1 2
| SuspendThread(hThread); SuspendThread(hThread);
|
1 2
| ResumeThread(hThread); ResumeThread(hThread);
|
线程终止
线程终止有如下三种方式
①
1
| ::ExitThread(DWORD dwExitCode);
|
②线程函数返回
③
1 2
| ::TerminateThread(hThread,2); ::WaitForSingleObject(hThread,INFINITE);
|
①和③方式的区别就是①是同步的,③是异步的。
GetExitCodeThread
线程函数会有一个返回值(DWORD),这个返回值可以根据你的需求进行返回,而我们需要如何获取这个返回结果呢?这时候就可以使用GetExitCodeThread函数,其语法格式如下:
1 2 3 4
| BOOL GetExitCodeThread( HANDLE hThread, LPDWORD lpExitCode );
|
根据MSDN Library我们可以知道该函数的参数分别是线程句柄,而另一个则是out类型参数,这种类型则可以理解为GetExitCodeThread函数的返回结果。
1 2 3 4 5 6
| HANDLE hThread; hThread = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL); WaitForSingleObject(hThread, INFINITE); DWORD exitCode; GetExitCodeThread(hThread, &exitCode); printf("Exit Code: %d \n", exitCode);
|
若ExitCode为STILL_ACTIVE,则表示该线程正在运行。
需要注意的是这个函数应该搭配着如上所学的2个等待函数一起使用,不然获取到的值就不会是线程函数返回的值。
设置、获取线程上下文
线程上下文是指某一时间点CPU寄存器和程序计数器的内容,如果想要设置、获取线程上下文就需要先将线程挂起。
GetThreadContext函数
GetThreadContext函数用于获取线程上下文,其语法格式如下:
1 2 3 4
| BOOL GetThreadContext( HANDLE hThread, LPCONTEXT lpContext );
|
第一个参数就是线程句柄,这个很好理解,重点是第二个参数,其是一个CONTEXT结构体,该结构体包含指定线程的上下文,其ContextFlags成员的值指定了要设置线程上下文的哪些部分。
当我们将CONTEXT结构体的ContextFlags成员的值设置为CONTEXT_INTEGER时则可以获取edi、esi、ebx、edx、ecx、eax这些寄存器的值:
如下代码尝试获取:
1 2 3 4 5 6 7
| HANDLE hThread; hThread = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL); SuspendThread(hThread); CONTEXT c; c.ContextFlags = CONTEXT_INTEGER; GetThreadContext(hThread, &c); printf("%x %x \n", c.Eax, c.Ecx);
|
SetThreadContext函数
GetThreadContext函数是个设置修改线程上下文,其语法格式如下:
1 2 3 4
| BOOL SetThreadContext( HANDLE hThread, // handle to thread CONST CONTEXT *lpContext // context structure );
|
我们可以尝试修改Eax,然后再获取:
1 2 3 4 5 6 7 8 9 10 11
| HANDLE hThread; hThread = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL); SuspendThread(hThread); CONTEXT c; c.ContextFlags = CONTEXT_INTEGER; c.Eax = 0x123; SetThreadContext(hThread, &c); CONTEXT c1; c1.ContextFlags = CONTEXT_INTEGER; GetThreadContext(hThread, &c1); printf("%x \n", c1.Eax);
|
线程互斥与线程同步
几种相关的结构
临界区
临界区的实现
首先会有一个令牌,假设线程1获取了这个令牌,那么这时候令牌则只为线程1所有,然后线程1会执行代码去访问全局变量,最后归还令牌;如果其他线程想要去访问这个全局变量就需要获取这个令牌,但当令牌已经被取走时则无法访问。
假设你自己来实现临界区,可能在判断令牌有没有被拿走的时候就又会出现问题,所以自己实现临界区还是有一定的门槛的。
线程锁
线程锁就是临界区的实现方式,通过线程锁我们可以完美解决如上所述的问题,其步骤如下所示:
- 创建全局变量:CRITICAL_SECTION cs;
- 初始化全局变量:InitializeCriticalSection(&cs);
- 实现临界区:进入 → EnterCriticalSection(&cs); 离开 → LeaveCriticalSection(&cs);
临界区使用
我们就可以这样改写之前的售卖物品的代码:
在使用全局变量开始前构建并进入临界区,使用完之后离开临界区:
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
| #include <windows.h>
CRITICAL_SECTION cs; int countNumber = 10;
DWORD WINAPI ThreadProc(LPVOID lpParameter) { while (1) { EnterCriticalSection(&cs); if (countNumber > 0) { printf("Thread: %d\n", *((int*)lpParameter)); printf("Sell num: %d\n", countNumber); countNumber--; printf("Count: %d\n", countNumber); } else { LeaveCriticalSection(&cs); break; } LeaveCriticalSection(&cs); } return 0; }
int main(int argc, char* argv[]) {
InitializeCriticalSection(&cs); int a = 1; HANDLE hThread; hThread = CreateThread(NULL, NULL, ThreadProc, (LPVOID)&a, 0, NULL); int b = 2; HANDLE hThread1; hThread1 = CreateThread(NULL, NULL, ThreadProc, (LPVOID)&b, 0, NULL);
CloseHandle(hThread);
getchar(); return 0; }
|
互斥体
我们了解了使用线程锁来解决多个线程共用一个全局变量的线程安全问题;那么假设A进程的B线程和C进程的D线程,同时使用的是内核级的临界资源(内核对象:线程、文件、进程…)该怎么让这个访问是安全的?使用线程锁的方式明显不行,因为线程锁仅能控制同进程中的多线程。
那么这时候我们就需要一个能够放在内核中的令牌来控制,而实现这个作用的,我们称之为互斥体。
互斥体的使用
创建互斥体的函数为CreateMutex,该函数的语法格式如下:
1 2 3 4 5
| HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, // SD 安全属性,包含安全描述符 BOOL bInitialOwner, // initial owner 是否希望互斥体创建出来就有信号,或者说就可以使用,如果希望的话就为FALSE;官方解释为如果该值为TRUE则表示当前进程拥有该互斥体所有权 LPCTSTR lpName // object name 互斥体的名字 );
|
我们可以模拟一下操作资源然后创建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #include <windows.h>
int main(int argc, char* argv[]) { HANDLE cm = CreateMutex(NULL, FALSE, "XYZ"); WaitForSingleObject(cm, INFINITE);
for (int i = 0; i < 5; i++) { printf("Process: A Thread: B -- %d \n", i); Sleep(1000); } ReleaseMutex(cm); return 0; }
|
我们可以运行两个进程来看一下互斥体的作用:
互斥体和线程锁的区别
- 线程锁只能用于单个进程间的线程控制
- 互斥体可以设定等待超时,但线程锁不能
- 线程意外结束时,互斥体可以避免无限等待
- 互斥体效率没有线程锁高
课外扩展-互斥体防止程序多开
CreateMutex函数的返回值MSDN Library的介绍是这样的:如果函数成功,返回值是一个指向mutex对象的句柄;如果命名的mutex对象在函数调用前已经存在,函数返回现有对象的句柄,GetLastError返回ERROR_ALREADY_EXISTS(表示互斥体以及存在);否则,调用者创建该mutex对象;如果函数失败,返回值为NULL,要获得扩展的错误信息,请调用GetLastError获取。
所以我们可以利用互斥体来防止程序进行多开:
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
| #include <windows.h>
int main(int argc, char* argv[]) { HANDLE cm = CreateMutex(NULL, TRUE, "XYZ"); if (cm != NULL) { if (GetLastError() == ERROR_ALREADY_EXISTS) { printf("该程序已经开启了,请勿再次开启!"); getchar(); } else { WaitForSingleObject(cm, INFINITE); for (int i = 0; i < 5; i++) { printf("Process: A Thread: B -- %d \n", i); Sleep(1000); } ReleaseMutex(cm); } } else { printf("CreateMutex 创建失败! 错误代码: %d\n", GetLastError()); } return 0; }
|
事件
事件本身也是一种内核对象,其也是是用来控制线程的。
创建事件使用函数CreateEvent,其语法格式如下:
1 2 3 4 5 6
| HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, // SD 安全属性,包含安全描述符 BOOL bManualReset, // reset type 如果你希望当前事件类型是通知类型则写TRUE,反之FALSE BOOL bInitialState, // initial state 初始状态,决定创建出来时候是否有信号,有为TRUE,没有为FALSE LPCTSTR lpName // object name 事件名字 );
|
控制事件的函数只有一个
1
| BOOL SetEvent(HANDLE hEvent);
|
使用完事件对象要养成关闭句柄的好习惯
信号量
创建信号量
1 2 3 4 5 6 7 8 9
| HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName );
|
打开信号量
1 2 3 4 5 6 7 8 9 10
| HANDLE OpenSemaphore(
DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName );
|
增加信号量资源计数
1 2 3 4 5 6 7 8 9
| BOOL ReleaseSemaphore( HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount );
|
销毁或清理信号量
比较
临界区与互斥体
1、临界区只能用于进程内的线程互斥,性能较好.
2、互斥体属于内核对象,可以用于进程间的线程互斥,性能较差.
3、线程在没有正常退出互斥区而意外终结时,互斥体可以复位,但临界区不行.
事件与信号量
1、都是内核对象,使用完毕后应该关闭句柄.
2、信号量可以用于相当复杂的线程同步控制.
线程互斥
线程互斥是指对于共享的进程系统资源,在各单个线程访问时的排它性;当有若干个线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。
线程安全问题
每个线程都有自己的栈,局部变量是存储在栈中的,这就意味着每个进程都会有一份自己的“句柄变量”(栈),如果线程仅仅使用自己的“局部变量”那就不存在线程安全问题,反之,如果多个线程共用一个全局变量呢?那么在什么情况下会有问题呢?那就是当多线程共用一个全局变量并对其进行修改时则存在安全问题,如果仅仅是读的话没有问题。
如下所示代码,我们写了一个线程函数,该函数的作用就是使用全局变量,模拟的功能就是售卖物品,全局变量countNumber表示该物品的总是,其值是10,而如果有多个地方(线程)去卖(使用)这个物品(全局变量),则会出现差错:
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
| #include <windows.h>
int countNumber = 10; DWORD WINAPI ThreadProc(LPVOID lpParameter) { while (countNumber > 0) { printf("Sell num: %d\n", countNumber); countNumber--; printf("Count: %d\n", countNumber); } return 0; }
int main(int argc, char* argv[]) { HANDLE hThread; hThread = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL); HANDLE hThread1; hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL);
CloseHandle(hThread);
getchar(); return 0; }
|
如图,我们运行了代码,发现会出现重复售卖,并且到最后总数竟变成了-1:
出现这样的问题其本质原因是什么呢?因为多线程在执行的时候是同步进行的,并不是按照顺序来,并且存在着对同一共享资源进行同时写或一读一写的情况,这造成了上面现象的发生。
想要解决线程安全问题,就需要引伸出一个概念:临界资源,临界资源表示对该资源的访问一次只能有一个线程;访问临界资源可以有多重形式,其中一种形式是临界区。
线程同步
线程同步是指线程之间所具有的一种制约关系,一个线程的执行依赖另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒;同步的前提是互斥,其次就是有序,互斥并不代表A线程访问临界资源后就一定是B线程再去访问,也有可能是A线程,这就是属于无序的状态,所以同步就是互斥加上有序。
线程同步主要靠WaitForSingleObject和WaitForMultipleObjects两个函数来完成。
WaitForSingleObject函数用于等待一个内核对象(对内核对象的详细说明会在下一章中)状态发生变更,那也就是执行结束之后,才会继续向下执行,其语法格式如下:
1 2 3 4
| DWORD WaitForSingleObject( HANDLE hHandle, // handle to object 句柄 DWORD dwMilliseconds // time-out interval 等待超时时间(毫秒) );
|
如果你想一直等待的话,可以将第二参数的值设置为INFINITE。
1 2 3 4
| HANDLE hThread; hThread = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL); WaitForSingleObject(hThread, INFINITE); printf("OK...");
|
WaitForMultipleObjects函数与WaitForSingleObject函数作用是一样的,只不过它可以等待多个内核对象的状态发生变更,其语法格式如下:
1 2 3 4 5 6
| DWORD WaitForMultipleObjects( DWORD nCount, // number of handles in array 内核对象的数量 CONST HANDLE *lpHandles, // object-handle array 内核对象的句柄数组 BOOL bWaitAll, // wait option 等待模式 DWORD dwMilliseconds // time-out interval 等待超时时间(毫秒) );
|
等待模式的值是布尔类型,一个是TRUE,一个是FALSE,TRUE就是等待所有对象的所有状态发生变更,FALSE则是等待任意一个对象的状态发生变更。
1 2 3 4
| HANDLE hThread[2]; hThread[0] = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL); hThread[1] = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL); WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
|
生产者与消费者问题
想要证明事件和互斥体最本质的区别,我们可以使用生产者与消费者模型来举例子。
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合(依赖性)问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
我们就可以理解为生产者生产一个物品,将其放进容器里,然后消费者从容器中取物品进行消费,就这样“按部就班”下去…
互斥体
首先我们来写一段互斥体下的生产者与消费者的代码:
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
| #include "stdafx.h" #include <windows.h>
int container;
int count = 10;
HANDLE hMutex;
DWORD WINAPI ThreadProc(LPVOID lpParameter) { for (int i = 0; i < count; i++) { WaitForSingleObject(hMutex, INFINITE); int threadId = GetCurrentThreadId(); container = 1; printf("Thread: %d, Build: %d \n", threadId, container); ReleaseMutex(hMutex); } return 0; }
DWORD WINAPI ThreadProcB(LPVOID lpParameter) { for (int i = 0; i < count; i++) { WaitForSingleObject(hMutex, INFINITE); int threadId = GetCurrentThreadId(); printf("Thread: %d, Consume: %d \n", threadId, container); container = 0; ReleaseMutex(hMutex); } return 0; }
int main(int argc, char* argv[]) { hMutex = CreateMutex(NULL, FALSE, NULL);
HANDLE hThread[2]; hThread[0] = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL); hThread[1] = CreateThread(NULL, NULL, ThreadProcB, NULL, 0, NULL); WaitForMultipleObjects(2, hThread, TRUE, INFINITE); CloseHandle(hThread[0]); CloseHandle(hThread[1]); CloseHandle(hMutex); return 0; }
|
运行结果如下图所示:
我们可以清晰的看见结果并不是我们想要的,生产一次消费一次的有序进行,甚至还出现了先消费后生产的情况,这个问题我们可以去修改代码解决:
这样虽然看似解决了问题,但是实际上也同样会出现一种问题,那就是for循环执行了不止10次,这样会倒是过分的占用计算资源。
事件
我们使用事件的方式就可以更加完美的解决这一需求:
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
| #include <windows.h>
int container = 0;
int count = 10;
HANDLE eventA; HANDLE eventB;
DWORD WINAPI ThreadProc(LPVOID lpParameter) { for (int i = 0; i < count; i++) { WaitForSingleObject(eventA, INFINITE); int threadId = GetCurrentThreadId(); container = 1; printf("Thread: %d, Build: %d \n", threadId, container); SetEvent(eventB); } return 0; }
DWORD WINAPI ThreadProcB(LPVOID lpParameter) { for (int i = 0; i < count; i++) { WaitForSingleObject(eventB, INFINITE); int threadId = GetCurrentThreadId(); printf("Thread: %d, Consume: %d \n", threadId, container); container = 0; SetEvent(eventA); } return 0; }
int main(int argc, char* argv[]) { eventA = CreateEvent(NULL, FALSE, TRUE, NULL); eventB = CreateEvent(NULL, FALSE, FALSE, NULL);
HANDLE hThread[2]; hThread[0] = CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL); hThread[1] = CreateThread(NULL, NULL, ThreadProcB, NULL, 0, NULL);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE); CloseHandle(hThread[0]); CloseHandle(hThread[1]); CloseHandle(eventA); CloseHandle(eventB);
return 0; }
|
运行结果如下图:
内核对象
在铺垫完上面的内容后,我们可以正式引入内核对象这一概念了。
进程、线程、文件、互斥体、事件等等在内核都有一个对应的结构体,这些结构体都由内核负责管理,所以我们都可以称之为内核对象(**当我们创建一个进程,在内核层(高2G)就会创建一个结构体EPROCESS…**)。
记不住没关系,我们可以在MSDN Library中搜索CloseHandle这个函数,它是用来关闭句柄的,暂时先不用管其原理,我们只要知道它所支持关闭就都是内核对象:
管理内核对象
当我们使用如下图所示的函数创建时,会在内核层创建一个结构体,而我们该如何管理这些结构体呢?或者说如何使用这些结构体呢?一种方式是使用指针,亦即我们可以通过内核结构体地址来管理,但是这样做存在问题:应用层很有可能操作不当导致修改啦内核结构体的地址,我们写应用层代码都知道访问到一个不存在的内存地址就会报错,而如果访问到一个内核地址是错误的,微软系统下则直接会蓝屏。
微软为了避免这种情况的发生,所以其不会讲内核结构体的地址暴露给应用层,也就是说没法通过这种方式来直接管理。
句柄表
没法直接管理内核对象,这时候句柄表就诞生了,但是需要注意的是,只有进程才会有句柄表,并且每一个进程都会有一个句柄表。
句柄本质上就一个防火墙,将应用层、内核层隔离开来,通过句柄就可以控制进程内核结构体,我们得到所谓句柄的值实际上就是句柄表里的一个索引。
多进程共享内核对象
如下图所示,A进程通过CreateProcess函数创建了一个内核对象;B进程通过OpenProcess函数可以打开别人创建好的一个进程,也就是可以操作其的内核对象;A进程想要操作内核对象就通过其对应的句柄表的句柄(索引)来操作;B进程操作这个内核对象也是通过它自己的句柄表的句柄(索引)来操作内核对象。(需要注意的是:句柄表是一个私有的,句柄值就是进程自己句柄表的索引)
在之前的例子中我们提到了CloseHandle这个函数是用来关闭进程、线程的,其实它的本质就是释放句柄,但是并不代表执行了这个函数,创建的内核对象就会彻底消失;如上图中所示内核对象存在一个计数器,目前是2,它的值是根据调用A的次数来决定的,如果我们只是在A进程中执行了CloseHandle函数,内核对象并不会消失,因为进程B还在使用,而只有进程B也执行了CloseHandle函数,这个内核对象的计数器为0,就会关闭消失了。
最后:注意,以上所述特性适合于除了线程以外的所有内核对象,创建进程,同时也会创建线程,如果你想把线程关闭,首先需要CloseHandle函数要让其计数器为0,其次需要有人将其关闭,所以假设我们创建了一个IE进程打开了一个网站,如果我们只是在代码中使用了CloseHandle函数,这样IE浏览器并不会关闭,需要我们手动点击窗口的关闭按钮才行(只有线程关闭了,进程才会关闭)。
句柄是否”可以”被继承
除了我们上述的方式可以进行共享内核对象以外,Windows还设计了一种方式来提供我们共享内核对象,我们先来了解一下句柄是否”可以”被继承。
如下图所示(句柄表是有三列的,分别是句柄值、内核结构体地址、句柄是否可以被继承),比如说我们在A进程(父进程)创建了4个内核对象:
这四个函数都有一个参数LPSECURITY_ATTRIBUTES lpThreadAttributes,通过这个参数我们可以判断函数是否创建的是内核对象。
我们可以跟进看一下这个参数,它就是一个结构体:
结构体成员分别是:1.结构体长度;2.安全描述符;3.句柄是否被继承。
第一个成员我们见怪不怪了,在Windows设计下都会有这样一个成员;第二个安全描述符,这个对我们来说实际上没有任何意义,一般留空就行,默认它会遵循父进程的来,其主要作用就是描述谁创建了该对象,谁有访问、使用该对象的权限。
第三个成员是我们重点需要关注的,因为其决定了句柄是否可以被继承,如下图所示,我们让CreateProcess函数创建的进程、线程句柄可以被继承:
句柄是否”允许”被继承
我们可以让句柄被继承,但也仅仅是可以,要真正完成继承,或者说我们允许子进程继承父进程的句柄,这时候就需要另外一个参数了。
我们还是以CreateProcess函数举例,其有一个参数BOOL bInheritHandles,这个参数决定了是否允许创建的子进程继承句柄:
只有这个参数设置为TRUE时,我们创建的子进程才允许继承父进程的句柄。
进程
概念
程序所需要的资源(数据、代码…)是由进程提供的;进程是一种空间上的概念,它的责任就是提供资源,至于资源如何使用,与它无关。
每一个进程都有自己的一个4GB大小的虚拟空间,也就是从0x0-0xFFFFFFFF这个范围。
进程内存空间的地址划分如下,每个进程的内核是同一份(高2G),只有其他三个分区是进程独有的(低2G),而只有用户模式区是我们使用的范围:
进程也可以理解为是一对模块组成的,我们可以使用OD打开一个进程看一下:
这里面有很多的模块,每个模块都是一个可执行文件,它们遵守相同的格式,即PE结构,所以我们也可以理解进程就是一堆PE组合。
进程和线程之间的关系
一个进程可以包含多个线程,如老师所说(一个进程至少要包含一个线程,进程是4GB,线程就是EIP)。
打开任务管理器,可以看到一个进程包含的线程个数
进程的创建
我们需要知道任何进程都是别的进程创建的,当我们在Windows下双击打开一个文件,实际上就是explorer.exe这个进程创建的子进程。我们打开文件的进程,其使用的方法就是CreateProcess()
1 2 3 4 5 6 7 8 9 10 11 12
| BOOL CreateProcess( LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation );
|
进程创建的过程也就是CreateProcess函数:
- 映射EXE文件(低2G)
- 创建内核对象EPROCESS(高2G)
- 映射系统DLL(ntdll.dll)
- 创建线程内核对象RTHREAD(高2G)
- 系统启动线程:
- 映射DLL(ntdll.LdrInitializeThunk)
- 线程开始执行
如上图就是打开A.exe的创建过程图,进程是空间上的概念,只用于提供代码和数据资源等等…而想要使用这些资源的是线程,每个进程至少需要一个线程。
在映射EXE文件时主要做了如下几件事
- 将EXE拉伸,存储到指定的位置
- 遍历EXE导入表,将需要用到的DLL拉伸存储到指定位置,如果位置被占用就换个地方,并通过DLL的重定位表,修复全局遍历
- DLL如果引用了其他的DLL 递归第二步
- 修复EXE/DLL中的IAT表
Win32下的自动化测试
一个简单又必须掌握的技能
查找窗口与控制窗口
查找指定窗口
1 2 3 4 5 6 7 8 9 10 11
| TCHAR szTitle[MAX_PATH] = {0}; HWND hwnd = ::FindWindow(TEXT("#32770"),TEXT("飞鸽传书 IP Messenger")); if(hwnd != NULL) { ::SetWindowText(hwnd,"新的窗口标题"); } else { ::MessageBox(NULL,TEXT("窗口没有找到"),TEXT("[ERROR]"),MB_OK); }
|
控制窗口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| TCHAR szTitle[MAX_PATH] = {0}; HWND hwnd = ::FindWindow(TEXT("#32770"),TEXT("飞鸽传书 IP Messenger")); if(hwnd != NULL) { typedef void (WINAPI *PSWITCHTOTHISWINDOW) (HWND,BOOL); PSWITCHTOTHISWINDOW SwitchToThisWindow; HMODULE hUser32=LoadLibrary("user32.dll"); SwitchToThisWindow=(PSWITCHTOTHISWINDOW)GetProcAddress(hUser32,"SwitchToThisWindow");
SwitchToThisWindow(hwnd,false);
Sleep(3000); ::SendMessage(hwnd,WM_CLOSE,0,0); } else { ::MessageBox(NULL,TEXT("窗口没有找到"),TEXT("[ERROR]"),MB_OK); }
|
查找子窗口
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
| TCHAR szTitle[MAX_PATH] = {0}; HWND hwnd = ::FindWindow(TEXT("#32770"),TEXT("飞鸽传书 IP Messenger")); if(hwnd != NULL) { HWND hEdit = FindWindowEx(hwnd,NULL,"Edit",""); ::SetWindowText(hEdit,"文本框新的标题"); ::SendMessage(hEdit,WM_SETTEXT,0,(LPARAM)"新的内容"); } else { ::MessageBox(NULL,TEXT("窗口没有找到"),TEXT("[ERROR]"),MB_OK); } TCHAR szTitle[MAX_PATH] = {0}; HWND hwnd = ::FindWindow(TEXT("#32770"),TEXT("飞鸽传书 IP Messenger")); if(hwnd != NULL) { HWND hEdit =::GetDlgItem(hwnd,0x3E9); ::SendMessage(hEdit,WM_GETTEXT,MAX_PATH,(LPARAM)szTitle); ::SendMessage(hEdit,WM_SETTEXT,0,(LPARAM)"新的内容"); } else { ::MessageBox(NULL,TEXT("窗口没有找到"),TEXT("[ERROR]"),MB_OK); }
|
枚举子窗口控件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| BOOL CALLBACK EnumChildProc(HWND hWnd,LPARAM lParam) { TCHAR szTitle[MAX_PATH] = {0}; ::GetWindowText(hWnd,szTitle,MAX_PATH); MessageBox(NULL,szTitle,"[子窗口]",MB_OK); return true; } VOID EnumChildWindow() { TCHAR szTitle[MAX_PATH] = {0}; HWND hWnd = ::FindWindow(TEXT("#32770"),TEXT("飞鸽传书 IP Messenger")); if(hWnd != NULL) { ::EnumChildWindows(hWnd,EnumChildProc,0); } else { ::MessageBox(NULL,TEXT("窗口没有找到"),TEXT("[ERROR]"),MB_OK); } }
|
枚举所有打开窗口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| BOOL CALLBACK EnumOpenWindowProc(HWND hWnd,LPARAM lParam) { TCHAR szTitle[MAX_PATH] = {0}; ::GetWindowText(hWnd,szTitle,MAX_PATH); MessageBox(NULL,szTitle,"[窗口]",MB_OK); if(strcmp(szTitle,"飞鸽传书 IP Messenger") == 0) { MessageBox(NULL,szTitle,"[窗口]",MB_OK); return FALSE; } return TRUE; } VOID EnumOpenWindows() { EnumWindows(EnumOpenWindowProc,NULL); }
|
模拟鼠标单击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| TCHAR szTitle[MAX_PATH] = {0}; RECT r; HWND hwnd = ::FindWindow(TEXT("#32770"),TEXT("飞鸽传书 IP Messenger")); if(hwnd != NULL) { HWND hButton = FindWindowEx(hwnd,NULL,"Button","刷新(&R)"); ::GetWindowRect(hButton,&r); printf("%d %d",r.left,r.top); ::SetCursorPos(r.left+10,r.top+10); Sleep(2000); mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0); mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0); } else { ::MessageBox(NULL,TEXT("窗口没有找到"),TEXT("[ERROR]"),MB_OK); }
|
模拟键盘
这里模拟键盘点击需要自行搜索键盘键与虚拟键码对照表(不是不是ASCII码!!)
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
| TCHAR szTitle[MAX_PATH] = {0}; RECT r; HWND hwnd = ::FindWindow(TEXT("#32770"),TEXT("飞鸽传书 IP Messenger")); if(hwnd != NULL) { HWND hEdit =::GetDlgItem(hwnd,0x3E9); ::GetWindowRect(hEdit,&r); ::SetCursorPos(r.left+1,r.top+1); Sleep(1000); mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0); mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0); keybd_event(97,0,0,0); keybd_event(97,0,KEYEVENTF_KEYUP,0); Sleep(1000); keybd_event(66,0,0,0); keybd_event(66,0,KEYEVENTF_KEYUP,0); Sleep(1000); keybd_event(16,0,0,0); keybd_event(67,0,0,0); keybd_event(67,0,KEYEVENTF_KEYUP,0); keybd_event(16,0,KEYEVENTF_KEYUP,0); } else { ::MessageBox(NULL,TEXT("窗口没有找到"),TEXT("[ERROR]"),MB_OK); }
|
练习:模拟QQ登录
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 78 79 80 81 82 83 84 85 86 87
|
#include "stdafx.h"
TCHAR path[MAX_PATH]=_T("C:\\Program Files\\Tencent\\QQ\\Bin\\QQScLauncher.exe"); BOOL bFindFlag;
BOOL CALLBACK EnumOpenWindowProc(HWND hWnd,LPARAM lParam) { TCHAR szTitle[MAX_PATH] = {0}; RECT r; ::GetWindowText(hWnd,szTitle,MAX_PATH); if(lstrcmp(szTitle,_T("QQ")) == 0) { bFindFlag = TRUE; SwitchToThisWindow(hWnd,false); ::GetWindowRect(hWnd,&r);
::SetCursorPos(r.left+250,r.top+120); mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0); mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0); keybd_event(97,0,0,0); keybd_event(97,0,KEYEVENTF_KEYUP,0);
keybd_event(66,0,0,0); keybd_event(66,0,KEYEVENTF_KEYUP,0);
keybd_event(16,0,0,0); keybd_event(67,0,0,0); keybd_event(67,0,KEYEVENTF_KEYUP,0); keybd_event(16,0,KEYEVENTF_KEYUP,0);
Sleep(1000); keybd_event(9,0,0,0); keybd_event(9,0,KEYEVENTF_KEYUP,0); keybd_event(97,0,0,0); keybd_event(97,0,KEYEVENTF_KEYUP,0);
keybd_event(66,0,0,0); keybd_event(66,0,KEYEVENTF_KEYUP,0);
keybd_event(16,0,0,0); keybd_event(67,0,0,0); keybd_event(67,0,KEYEVENTF_KEYUP,0); keybd_event(16,0,KEYEVENTF_KEYUP,0);
keybd_event(13,0,0,0); keybd_event(13,0,KEYEVENTF_KEYUP,0);
return FALSE; } return TRUE; }
VOID EnumOpenWindows() { while (!bFindFlag) { EnumWindows(EnumOpenWindowProc,NULL); Sleep(5000); } }
int main(int argc, _TCHAR* argv[]) { STARTUPINFO si = {0}; si.cb = sizeof(si); PROCESS_INFORMATION pi; CreateProcess(path,NULL,NULL,NULL,FALSE,CREATE_NEW_CONSOLE,NULL,NULL,&si,&pi); EnumOpenWindows(); return 0; }
|
远程线程注入
之前我们是远程创建线程,调用的也是人家自己的线程函数,而如果我们想要创建远程线程调用自己定义的线程函数就需要使用远程线程注入技术。
什么是注入
所谓注入就是在第三方进程不知道或者不允许的情况下将模块或者代码写入对方进程空间,并设法执行的技术。
在安全领域,“注入”是非常重要的一种技术手段,注入与反注入也一直处于不断变化的,而且也愈来愈激烈的对抗当中。
已知的注入方式:
远程线程注入、APC注入、消息钩子注入、注册表注入、导入表注入、输入法注入等等。
远程线程注入的流程
远程线程注入的思路就是在进程A中创建线程,将线程函数指向LoadLibrary函数。
那么为什么可以这样呢?这是因为我们执行远程线程函数满足返回值是4字节,一个参数是4字节即可(ThreadProc就是这样的条件):
我们再来看一下LoadLibrary函数的语法格式:
1 2 3
| HMODULE LoadLibrary( LPCTSTR lpFileName // file name of module );
|
我们可以跟进(F12)一下HMODULE和LPCTSTR这两个宏的定义,就会发现其实都是4字节宽度。
具体实现步骤如下图所示:
如何执行代码
DLL文件,在DLL文件入口函数判断并创建线程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
#include "stdafx.h"
DWORD WINAPI ThreadProc(LPVOID lpParaneter) { for (;;) { Sleep(1000); printf("DLL RUNNING..."); } }
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); break; } return TRUE; }
|
文件我们用如下写的Test.exe即可,将编译好的DLL和Test.exe放在同一个目录并打开Test1.exe。
注入实现:
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
|
#include "StdAfx.h"
BOOL LoadDll(DWORD dwProcessID, char* szDllPathName) { HANDLE hProcess; HANDLE hThread; DWORD dwLength; DWORD dwLoadAddr; LPVOID lpAllocAddr; DWORD dwThreadID; HMODULE hModule; bRet = 0; dwLoadAddr = 0; hProcess = 0; hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID); dwLength = strlen(szDllPathName) + 1; lpAllocAddr = VirtualAllocEx(hProcess, NULL, dwLength, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hProcess, lpAllocAddr, szDllPathName, dwLength, NULL); hModule = GetModuleHandle("kernel32.dll"); dwLoadAddr = (DWORD)GetProcAddress(hModule, "LoadLibraryA"); hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)dwLoadAddr, lpAllocAddr, 0, &dwThreadID); CloseHandle(hThread); CloseHandle(hProcess); return TRUE; }
int main(int argc, char* argv[]) { LoadDll(384, "C:\\Documents and Settings\\Administrator\\桌面\\test\\B.dll"); getchar(); return 0; }
|
注入成功:
模块隐藏的基本方式
加载进程
加载进程的步骤如下:
1、将自己进程的ImageBase设置一个较大的值,让自己的程序在高空运行.
2、将要执行的进程读取进来,按照进程的ImageBase和SizeOfImage分配空间
3、拉伸进程
4、修复IAT表
5、跳转到入口执行
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
| #include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[]) { LPVOID pFileBuffer; LPVOID pImageBuffer; DWORD dwImageSize; HANDLE hCurrentProcess; LPVOID lpAllocAddr;
PIMAGE_DOS_HEADER pDosHeader = NULL; PIMAGE_NT_HEADERS pNTHeader = NULL; PIMAGE_FILE_HEADER pPEHeader = NULL; PIMAGE_OPTIONAL_HEADER pOptionHeader = NULL; LPVOID pImportDir; LPVOID pImportAddressDir;
DWORD dwImageBase; DWORD dwLastError; ReadPEFile("C:\\Users\\admin\\Desktop\\injected_exe.exe", &pFileBuffer); dwImageSize = CopyFileBufferToImageBuffer(pFileBuffer, &pImageBuffer); pDosHeader = (PIMAGE_DOS_HEADER) pFileBuffer; pNTHeader = (PIMAGE_NT_HEADERS) ((DWORD) pFileBuffer + pDosHeader->e_lfanew); pPEHeader = (PIMAGE_FILE_HEADER) (((DWORD) pNTHeader) + 4); pOptionHeader = (PIMAGE_OPTIONAL_HEADER) ((DWORD) pPEHeader + IMAGE_SIZEOF_FILE_HEADER);
dwImageBase = pOptionHeader->ImageBase; pImportAddressDir = (LPVOID)(pOptionHeader->DataDirectory[12].VirtualAddress + dwImageBase);
hCurrentProcess = GetCurrentProcess(); VirtualAllocEx(hCurrentProcess, (LPVOID)dwImageBase, dwImageSize, MEM_RESET, PAGE_READWRITE); lpAllocAddr = VirtualAllocEx(hCurrentProcess, (LPVOID)dwImageBase, dwImageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (lpAllocAddr == NULL ) { dwLastError = GetLastError(); OutputDebugString("VirtualAllocEx failed!"); CloseHandle(hCurrentProcess); return 1; }
if (lpAllocAddr != (LPVOID)dwImageBase ) { dwLastError = GetLastError(); OutputDebugString("VirtualAllocEx address inconsist!"); CloseHandle(hCurrentProcess); return 1; } WriteProcessMemory(hCurrentProcess, lpAllocAddr, pImageBuffer, dwImageSize, NULL);
pImportDir = (LPVOID)(pOptionHeader->DataDirectory[1].VirtualAddress + dwImageBase);
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR) pImportDir; int num = 0; BOOL break_flag = FALSE; PIMAGE_IMPORT_DESCRIPTOR pTemp = pImportDescriptor; DWORD procAddress; while (pTemp->Name != NULL) { num++; pTemp++; } pTemp = pImportDescriptor;
for (int j = 0; j < num; j++) { char *dll_name = (char *) (dwImageBase + (pTemp + j)->Name);
PIMAGE_THUNK_DATA pThunkName = (PIMAGE_THUNK_DATA) (dwImageBase + (pTemp + j)->OriginalFirstThunk); PIMAGE_THUNK_DATA pThunkProcAddress = (PIMAGE_THUNK_DATA) (dwImageBase + (pTemp + j)->FirstThunk);
PIMAGE_IMPORT_BY_NAME pImageImportByName = (PIMAGE_IMPORT_BY_NAME) (dwImageBase + pThunkName->u1.Ordinal); while (TRUE) { if ((pThunkName->u1.Ordinal & IMAGE_ORDINAL_FLAG) == 0) { pImageImportByName = (PIMAGE_IMPORT_BY_NAME) (dwImageBase + pThunkName->u1.Ordinal); procAddress = (DWORD)GetProcAddress(LoadLibrary(dll_name),pImageImportByName->Name); pThunkProcAddress->u1.Ordinal = procAddress; } else { DWORD dwOrdinal = ((pThunkName->u1.Ordinal << 1) >> 1); procAddress = (DWORD)GetProcAddress(LoadLibrary(dll_name),(LPCSTR)dwOrdinal);
pThunkProcAddress->u1.Ordinal = procAddress; } pThunkName++; pThunkProcAddress++; if (pThunkName->u1.Ordinal == 0) { break; } } }
DWORD dwOEPAddr = pOptionHeader->ImageBase + pOptionHeader->AddressOfEntryPoint; HANDLE hThread = CreateThread(NULL, 0, (PTHREAD_START_ROUTINE)dwOEPAddr,NULL , NULL, NULL); printf("Hello World\nBelow are the loaded exe:\n"); WaitForSingleObject(hThread, INFINITE);
return 0; }
|
内存写入
内存写入的思路如下:
1、获取自身句柄
2、得到ImageSize的大小,得到模块的ImageBuffer
3、在当前空间申请空间存放自身代码
4、拷贝自身到缓存
5、打开要注入的进程
6、在远程进程申请空间
7、对模块中的代码进行重定位
8、得到模块中要运行的函数的地址
9、将模块在进程中的地址作为参数传递给入口函数
10、将修正后的模块 通过WriteProcessMemory写入远程进程的内存空间中
11、通过CreateRemoteThread启动刚写入的代码
12、释放内存
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
| #include "stdafx.h"
LPTSTR lpszCurrentModuleName; LPVOID pFileBuffer; LPVOID pImageBuffer;
DWORD static WINAPI Entry(LPVOID pImageBuffer) { PIMAGE_DOS_HEADER pImageDosHeader = (PIMAGE_DOS_HEADER)pImageBuffer; PIMAGE_NT_HEADERS pImageNTHeader = (PIMAGE_NT_HEADERS) ((DWORD) pImageBuffer + pImageDosHeader->e_lfanew); PIMAGE_FILE_HEADER pImagePEHeader = (PIMAGE_FILE_HEADER) (((DWORD) pImageNTHeader) + 4); PIMAGE_OPTIONAL_HEADER pImageOptionHeader = (PIMAGE_OPTIONAL_HEADER) ((DWORD) pImagePEHeader + IMAGE_SIZEOF_FILE_HEADER);
DWORD dwImageBase = pImageOptionHeader->ImageBase; LPVOID pImportDir = (LPVOID)(pImageOptionHeader->DataDirectory[1].VirtualAddress + pImageOptionHeader->ImageBase);
PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR) pImportDir; int num = 0; BOOL break_flag = FALSE; PIMAGE_IMPORT_DESCRIPTOR pTemp = pImportDescriptor; DWORD procAddress; while (pTemp->Name != NULL) { num++; pTemp++; } pTemp = pImportDescriptor;
for (int j = 0; j < num; j++) { char *dll_name = (char *) (dwImageBase + (pTemp + j)->Name);
PIMAGE_THUNK_DATA pThunkName = (PIMAGE_THUNK_DATA) (dwImageBase + (pTemp + j)->OriginalFirstThunk); PIMAGE_THUNK_DATA pThunkProcAddress = (PIMAGE_THUNK_DATA) (dwImageBase + (pTemp + j)->FirstThunk);
PIMAGE_IMPORT_BY_NAME pImageImportByName = (PIMAGE_IMPORT_BY_NAME) (dwImageBase + pThunkName->u1.Ordinal); while (TRUE) { if ((pThunkName->u1.Ordinal & IMAGE_ORDINAL_FLAG) == 0) { pImageImportByName = (PIMAGE_IMPORT_BY_NAME) (dwImageBase + pThunkName->u1.Ordinal); procAddress = (DWORD)GetProcAddress(LoadLibrary(dll_name),pImageImportByName->Name);
pThunkProcAddress->u1.Ordinal = procAddress; } else { DWORD dwOrdinal = ((pThunkName->u1.Ordinal << 1) >> 1); procAddress = (DWORD)GetProcAddress(LoadLibrary(dll_name),(LPCSTR)dwOrdinal);
pThunkProcAddress->u1.Ordinal = procAddress; } pThunkName++; pThunkProcAddress++; if (pThunkName->u1.Ordinal == 0) { break; } } }
DWORD dwOEPAddr = pImageOptionHeader->ImageBase + pImageOptionHeader->AddressOfEntryPoint; HANDLE hThread = CreateThread(NULL, 0, (PTHREAD_START_ROUTINE)dwOEPAddr,NULL , NULL, NULL); printf("AnotherThread running...\n"); WaitForSingleObject(hThread, INFINITE); return 0; }
DWORD dwInjectedPid = 880; int _tmain(int argc, _TCHAR* argv[]) { DWORD dwCurrentPid = GetCurrentProcessId(); if (dwCurrentPid == dwInjectedPid ) { TestSuccess(); return 0; }
DWORD dwImageAddr = (DWORD)::GetModuleHandle(NULL);
PIMAGE_DOS_HEADER pSelfDosHeader = (PIMAGE_DOS_HEADER)dwImageAddr; PIMAGE_NT_HEADERS pSelfNTHeader = (PIMAGE_NT_HEADERS) (dwImageAddr + pSelfDosHeader->e_lfanew);; PIMAGE_FILE_HEADER pSelfPEHeader = (PIMAGE_FILE_HEADER) (((DWORD) pSelfNTHeader) + 4);; PIMAGE_OPTIONAL_HEADER pSelfOptionHeader = (PIMAGE_OPTIONAL_HEADER) ((DWORD) pSelfPEHeader + IMAGE_SIZEOF_FILE_HEADER);
DWORD dwSelfImageBase = pSelfOptionHeader->ImageBase; DWORD dwSelfImageSize = pSelfOptionHeader->SizeOfImage;
pImageBuffer = calloc(1,dwSelfImageSize); ReadProcessMemory(GetCurrentProcess(),(LPCVOID)dwSelfImageBase,pImageBuffer,dwSelfImageSize,NULL); HANDLE hInjectProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwInjectedPid); if (hInjectProcess == NULL) { OutputDebugString("进程不存在!"); return 1; }
LPVOID lpAllocAddr = VirtualAllocEx(hInjectProcess, NULL, dwSelfImageSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
PIMAGE_DOS_HEADER pSelfImageDosHeader = (PIMAGE_DOS_HEADER)pImageBuffer; PIMAGE_NT_HEADERS pSelfImageNTHeader = (PIMAGE_NT_HEADERS) ((DWORD) pImageBuffer + pSelfImageDosHeader->e_lfanew); PIMAGE_FILE_HEADER pSelfImagePEHeader = (PIMAGE_FILE_HEADER) (((DWORD) pSelfImageNTHeader) + 4); PIMAGE_OPTIONAL_HEADER pSelfImageOptionHeader = (PIMAGE_OPTIONAL_HEADER) ((DWORD) pSelfImagePEHeader + IMAGE_SIZEOF_FILE_HEADER); DWORD dwOriginalImageBase = pSelfImageOptionHeader->ImageBase; pSelfImageOptionHeader->ImageBase =(DWORD)lpAllocAddr;
PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pImageBuffer + pSelfImageOptionHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
DWORD dwDelta; dwDelta = (DWORD)lpAllocAddr - dwOriginalImageBase; if ((char*)pReloc != (char*)pImageBuffer) { while ((pReloc->VirtualAddress + pReloc->SizeOfBlock) != 0) { WORD* pLocData = (WORD*)((PBYTE)pReloc + sizeof(IMAGE_BASE_RELOCATION)); int nNumberOfReloc = (pReloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
DWORD* pAddress; for (int i = 0; i < nNumberOfReloc; i++) {
if ((DWORD)(pLocData[i] & 0x0000F000) == 0x00003000) { pAddress = (DWORD*)((PBYTE)pImageBuffer + pReloc->VirtualAddress + (pLocData[i] & 0x0FFF)); *pAddress += dwDelta; } }
pReloc = (PIMAGE_BASE_RELOCATION)((PBYTE)pReloc + pReloc->SizeOfBlock); } }
WriteProcessMemory(hInjectProcess, lpAllocAddr, pImageBuffer, dwSelfImageSize, NULL); DWORD dwFixedEntry = (DWORD)Entry + dwDelta; HANDLE hThread = CreateRemoteThread(hInjectProcess, NULL, 0, (LPTHREAD_START_ROUTINE)dwFixedEntry, lpAllocAddr, 0, NULL); WaitForSingleObject(hThread, INFINITE); return 0; }
|
基本HOOK方式(3环HOOK)
Hook技术被广泛应用于安全的多个领域,比如杀毒软件的主动防御功能,涉及到对一些敏感API的监控,就需要对这些API进行Hook;窃取密码的木马病毒,为了接收键盘的输入,需要Hook键盘消息;甚至是Windows系统及一些应用程序,在打补丁时也需要用到Hook技术
IAT HOOK
我们都知道几乎所有的PE文件都需要外部模块提供的函数,这些函数的信息存储在导入表IAT中,将IAT表中的信息修改为指定函数的地址即可达到HOOK的效果。
IAT HOOK的代码十分地精简,主要函数如下,它的功能是修改IAT表中的值。
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
| HMODULE hModule = GetModuleHandle(_T("user32.dll")); DWORD dwOldProcAddr = (DWORD)GetProcAddress(hModule, _T("MessageBoxA"));
BOOL SetIATHook(DWORD dwOldAddr, DWORD dwNewAddr){ init(); PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR) pImportDir; while (pImportDescriptor->FirstThunk != 0 && g_bIATHookFlag != 1) { pFuncAddr = (PDWORD) (dwImageAddr + pImportDescriptor->FirstThunk); while (*pFuncAddr){ if (*pFuncAddr == dwOldAddr) { *pFuncAddr = dwNewAddr; g_bIATHookFlag = TRUE; return TRUE; } pFuncAddr ++ ; } pImportDescriptor ++ ; } return FALSE; }
BOOL UnsetIATHook(DWORD dwOldAddr) { *pFuncAddr = dwOldAddr; return TRUE; }
DWORD GetFuncAddr(DWORD dwFuncAddr){ DWORD dwOffset = *(PDWORD)((PBYTE)dwFuncAddr + 1); return dwFuncAddr + 5 + dwOffset; }
DWORD WINAPI MyMessageBox(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType){
typedef DWORD (WINAPI *pfnMessageBox)(HWND _hWnd, LPCSTR _lpText, LPCSTR _lpCaption, UINT _uType);
printf("参数: %x %s %s %x\n",hWnd, lpText, lpCaption, uType); int ret = ((pfnMessageBox)dwOldProcAddr)(hWnd, lpText, lpCaption, uType); printf("返回值: %x\n",ret); return 0; }
VOID TestIATHook(){ BOOL bSuccess = SetIATHook(dwOldProcAddr,GetFuncAddr((DWORD)MyMessageBox));
if (bSuccess){ MessageBox(NULL,_T("测试IAT Hook"), _T("IAT Hook"), MB_OK);
UnsetIATHook(dwOldProcAddr); } }
int _tmain(int argc, _TCHAR* argv[]){
TestIATHook(); return 0; }
|
这里的GetFuncAddr函数是为了获得函数的地址。虽然也可以通过直接反汇编看函数的代码来看,但是这种方式重新编译后就可能会发生变化,比较不方便。
简单地将函数名转成DWORD也是不行的,原因如下图,有个jmp:
1
| BOOL bSuccess = SetIATHook(dwOldProcAddr,(DWORD)MyMessageBox);
|
运行结果:
简单地打印了一下参数和返回值
Inline HOOK
Inline HOOK更将简单粗暴一些,通过直接修改硬编码达到函数跳转的效果。
Inline HOOK实现的关键是如何保存原来线程的CONTEXT环境,以及被覆盖的硬编码的保存与执行问题。
对于第一个问题,我们可以定义一个Register类来保存原来线程的CONTEXT环境:
1 2 3 4 5 6 7 8 9 10
| typedef struct _REGISTER{ DWORD Eax; DWORD Ebx; DWORD Ecx; DWORD Edx; DWORD Esi; DWORD Edi; DWORD Esp; DWORD Ebp; }Register;
|
同时在我们的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
| extern "C" __declspec(naked) void HookProc(){ _asm { pushad pushfd }
_asm { mov reg.Eax, eax mov reg.Ebx, ebx mov reg.Ecx, ecx mov reg.Edx, edx mov reg.Esi, esi mov reg.Edi, edi mov reg.Esp, esp mov reg.Ebp, ebp }
_asm { popfd popad }
_asm { jmp g_pOldProcAddr; } }
|
对于第二个问题,这里直接给出代码:
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
| BOOL g_bHookSuccessFlag = FALSE; PBYTE g_pCodePatch;
BOOL SetInlineHook(DWORD dwHookAddr, DWORD dwProcAddr, DWORD dwLength, PBYTE* pOldCode){ BOOL bRet; DWORD dwJmpCode; if (dwHookAddr == NULL || dwProcAddr == NULL){ OutputDebugString("SetInlineHook函数执行失败:Hook地址/函数地址填写有误!"); return FALSE; } if (dwLength < 5){ OutputDebugString("SetInlineHook函数执行失败:修改的硬编码长度不能小于5!"); return FALSE; } DWORD dwOldProtectionFlag; bRet = VirtualProtectEx(::GetCurrentProcess(),(LPVOID)dwHookAddr,dwLength,PAGE_EXECUTE_READWRITE,&dwOldProtectionFlag); if (!bRet){ OutputDebugString("SetInlineHook函数执行失败:修改内存属性失败!"); return FALSE; } LPVOID pAllocAddr = VirtualAllocEx(::GetCurrentProcess(),NULL,dwLength + 5, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(pAllocAddr,(LPVOID)dwHookAddr,dwLength); *(PBYTE)((DWORD)pAllocAddr + dwLength) = 0xE9; *(PDWORD)((DWORD)pAllocAddr + dwLength +1) = dwHookAddr + dwLength - ((DWORD)pAllocAddr + dwLength) - 5; *pOldCode = (PBYTE)pAllocAddr;
dwJmpCode = dwProcAddr - dwHookAddr - 5;
memset((PBYTE)dwHookAddr,0x90,dwLength);
*(PBYTE)dwHookAddr = 0xE9; *(PDWORD)((PBYTE)dwHookAddr + 1) = dwJmpCode; g_bHookSuccessFlag = TRUE;
return TRUE; }
BOOL UnsetInlineHook(DWORD dwHookAddr, DWORD dwPatchAddr, DWORD dwLength){ if (g_bHookSuccessFlag){ memcpy((LPVOID)dwHookAddr,(LPVOID)dwPatchAddr,dwLength); return TRUE; }else{ OutputDebugString("没有Hook成功,无需恢复!"); return FALSE; } }
|
在安装InlineHook后,函数的执行流程变成了下面这种方式:
运行结果:
进程通信
进程通信的方式有很多,包括文件读写、自定义消息、文件映射、共享内存、管道(匿名/命名)等。
我们之前提到的信号量等进程同步机制也同样可以用于进程间的通信。
下面给出几种进程通信的Demo。
自定义消息
在Windows的消息机制中,消息的种类是有限的。最后一种消息是wm_user
(0x0400),这意味着往后就是Windows未使用的消息编号,我们可以使用其来传递消息。
发送端代码
1 2 3 4 5 6 7 8 9 10 11
| HWND hwnd = ::FindWindow(NULL,TEXT("接收端窗口名")); if(hwnd == NULL) { MessageBox(0,TEXT("没找到窗口"),TEXT("ERROR"),MB_OK); } else { PostMessage(hwnd,WM_USER+0x1, NULL, (LPARAM)100); }
|
接收端代码
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
| switch(uMsg) { case WM_CLOSE: { EndDialog(hDlg,0); break; } case WM_USER+0x1: { DWORD x = wParam; DWORD y = lParam;
MessageBox(0,0,0,0); break; } case WM_COMMAND: { switch (LOWORD (wParam)) { case IDC_BUTTON_RECV: { return TRUE; } } } break ; }
|
共享内存
Win32 API中共享内存(Shared Memory)是文件映射的一种特殊情况。进程在创建文件映射对象时用0xFFFFFFFF来代替文件句柄(HANDLE),就表示了对应的文件映射对象是从操作系统页面文件访问内存,其它进程打开该文件映射对象就可以访问该内存块。
补充:关于文件映射
文件映射(Memory-Mapped Files)能使进程把文件内容当作进程地址区间一块内存那样来对待。因此,进程不必使用文件I/O操作,只需简单的指针操作就可读取和修改文件的内容。
Win32 API允许多个进程访问同一文件映射对象,各个进程在它自己的地址空间里接收内存的指针。通过使用这些指针,不同进程就可以读或修改文件的内容,实现了对文件中数据的共享。
发送端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| int _tmain(int argc, _TCHAR* argv[]) { HANDLE hMapObject; HANDLE hMapView;
hMapObject = CreateFileMapping((HANDLE)0xFFFFFFFF,NULL,PAGE_READWRITE,0,0x1000,TEXT("shared")); if(!hMapObject) { MessageBox(NULL,TEXT("共享内存失败"),TEXT("Error"),MB_OK); return FALSE; } hMapView = MapViewOfFile(hMapObject,FILE_MAP_WRITE,0,0,0); if(!hMapView) { MessageBox(NULL,TEXT("内存映射失败"),TEXT("Error"),MB_OK); return FALSE; } strcpy((char*)hMapView,"Test Shared Memery"); getchar();
}
|
接收端代码
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
|
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[]) { HANDLE hMapObject; HANDLE hMapView;
hMapObject = CreateFileMapping((HANDLE)0xFFFFFFFF,NULL,PAGE_READWRITE,0,0x1000,TEXT("shared")); if(!hMapObject) { MessageBox(NULL,TEXT("共享内存失败"),TEXT("Error"),MB_OK); return FALSE; } hMapView = MapViewOfFile(hMapObject,FILE_MAP_WRITE,0,0,0); if(!hMapView) { MessageBox(NULL,TEXT("内存映射失败"),TEXT("Error"),MB_OK); return FALSE; } TCHAR szBuffer[0x1000] = {0}; memcpy(szBuffer,hMapView,10); MessageBox(NULL,szBuffer,TEXT("从发送端接收到数据"),MB_OK); getchar(); }
|
运行结果
同时运行发送端和接收端代码,结果如下:
匿名管道
匿名管道不需要知道创建对象管道的名字。它是通过内核对象的可继承性进行的,也就是说匿名管道只能作用于父子进程之间,在父进程创建子进程的时候通过对CreateProcess函数中传参,即可让子进程获得父进程的内核对象句柄。
匿名管道是单机上实现子进程标准I/O重定向的有效方法,它不能在网上使用,也不能用于两个不相关的进程之间。
具体实现细节,请参考《Windows核心编程》内核对象一章。
补充:关于命名管道
命名管道(Named Pipe)是服务器进程和一个或多个客户进程之间通信的单向或双向管道。不同于匿名管道的是命名管道可以在不相关的进程之间和不同计算机之间使用,服务器建立命名管道时给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。
父进程代码
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 78 79 80 81 82 83 84 85 86
|
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[]) { HANDLE hParentRead; HANDLE hParentWrite; HANDLE hChildRead; HANDLE hChildWrite;
SECURITY_ATTRIBUTES sa;
sa.bInheritHandle = TRUE; sa.lpSecurityDescriptor = NULL; sa.nLength = sizeof(SECURITY_ATTRIBUTES);
if(!CreatePipe(&hParentRead,&hChildWrite,&sa,0)) { MessageBox(0,TEXT("创建匿名管道失败!"),TEXT("Error"),MB_OK); } if(!CreatePipe(&hChildRead,&hParentWrite,&sa,0)) { MessageBox(0,TEXT("创建匿名管道失败!"),TEXT("Error"),MB_OK); }
STARTUPINFO si; PROCESS_INFORMATION pi;
ZeroMemory(&si,sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO); si.dwFlags = STARTF_USESTDHANDLES; si.hStdInput = hChildRead; si.hStdOutput = hChildWrite; si.hStdError = GetStdHandle(STD_ERROR_HANDLE);
LPTSTR szFileName = "C:\\Users\\admin\\Documents\\visual studio 2012\\Projects\\anonymous_pipe_child\\Debug\\anonymous_pipe_child.exe"; if(!CreateProcess(szFileName,"child",NULL,NULL,TRUE,CREATE_NEW_CONSOLE,NULL,NULL,&si,&pi)) { CloseHandle(hParentRead); CloseHandle(hParentWrite); hParentRead = NULL; hParentWrite = NULL;
CloseHandle(hChildRead); CloseHandle(hChildWrite); hChildRead = NULL; hChildWrite = NULL; MessageBox(0,TEXT("创建子进程失败!"),TEXT("Error"),MB_OK); } else { CloseHandle(pi.hProcess); CloseHandle(pi.hThread); }
TCHAR szWriteBuffer[] = "父进程:http:\\www.dtdebug.com"; DWORD dwWrite; if(!WriteFile(hParentWrite,szWriteBuffer,strlen(szWriteBuffer)+1,&dwWrite,NULL)) { MessageBox(0,TEXT("父进程写数据失败!"),TEXT("Error"),MB_OK); }
Sleep(5000);
TCHAR szReadBuffer[100]; DWORD dwRead; if(!ReadFile(hParentRead,szReadBuffer,100,&dwRead,NULL)) { MessageBox(NULL,TEXT("父进程读取数据失败!"),TEXT("Error"),MB_OK); } else { MessageBox(NULL,szReadBuffer,TEXT("[父进程读取数据]"),MB_OK); } return 0; }
|
子进程代码
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
|
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[]) { Sleep(1000); HANDLE hRead = GetStdHandle(STD_INPUT_HANDLE); HANDLE hWrite = GetStdHandle(STD_OUTPUT_HANDLE);
TCHAR szReadBuffer[100]; DWORD dwRead; if(!ReadFile(hRead,szReadBuffer,100,&dwRead,NULL)) { MessageBox(NULL,TEXT("子进程读取数据失败!"),TEXT("Error"),MB_OK); } else { MessageBox(NULL,szReadBuffer,TEXT("[子进程读取数据]"),MB_OK); }
Sleep(3000);
TCHAR szWriteBuffer[100] = "子进程:匿名管道"; DWORD dwWrite; if(!WriteFile(hWrite,szWriteBuffer,strlen(szWriteBuffer)+1,&dwWrite,NULL)) { MessageBox(NULL,TEXT("子进程写入数据失败!"),TEXT("Error"),MB_OK); } return 0; }
|
运行结果
DLL共享节
DLL共享节技术可以让使用同一个DLL的多个进程共享一块内存(共享节)。
我的理解是DLL共享节和共享内存技术有异曲同工之妙。它们的技术思想都是相同的,不同的是DLL共享节使用的是低2G的内存(DLL中),而共享内存使用的是高2G的内存。
控制侧代码
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 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
| #include "stdafx.h"
BOOL InjectDLL() { EnableDebugPrivilege(); HWND hWnd = FindWindow(NULL, "扫雷"); if (hWnd == NULL) { OutputDebugString("获取窗口句柄失败\n"); return FALSE; } DWORD dwPid = -1; GetWindowThreadProcessId(hWnd, &dwPid); HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid); if (hProcess == INVALID_HANDLE_VALUE) { OutputDebugString("打开进程失败\n"); return FALSE; } char szDllName[MAX_PATH] = "DLLShareSection-DLL.dll"; LPVOID pAddress = VirtualAllocEx(hProcess, NULL, MAX_PATH, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); WriteProcessMemory(hProcess, pAddress, szDllName, strlen(szDllName), NULL); HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibrary, pAddress, 0, NULL); CloseHandle(hProcess); return TRUE; }
BOOL EnableDebugPrivilege() { HANDLE hToken; BOOL fOk = FALSE; if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
fOk = (GetLastError() == ERROR_SUCCESS); CloseHandle(hToken); } return fOk; }
int main() { if (FALSE == InjectDLL()) { printf("注入DLL失败\n"); return -1; } else { printf("注入DLL成功\n"); }
HMODULE hModule = LoadLibrary("DLLShareSection-DLL.dll"); if (hModule == NULL) { printf("获取DLL句柄失败\n"); return -1; } typedef void (*PFNSETDATA)(char *, DWORD); typedef void (*PFNGETDATA)(char *); PFNSETDATA pFnSetData = (PFNSETDATA)GetProcAddress(hModule, "SetData"); PFNGETDATA pFnGetData = (PFNGETDATA)GetProcAddress(hModule, "GetData"); char szBuffer[0x1000]; while (1) { printf("输入要发送的数据: "); ZeroMemory(szBuffer, 0x1000); scanf("%s", szBuffer); pFnSetData(szBuffer, strlen(szBuffer)); if (strcmp(szBuffer, "quit") == 0) break; }
return 0; }
|
DLL侧代码
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
| #include "stdafx.h"
#pragma data_seg("Shared")
char g_buffer[0x1000] = {0};
#pragma data_seg() #pragma comment(linker,"/section:Shared,rws")
extern "C" __declspec(dllexport) void SetData(char *buf, DWORD dwDataLen) { ZeroMemory(g_buffer, 0x1000); memcpy(g_buffer, buf, dwDataLen); }
extern "C" __declspec(dllexport) void GetData(char *buf) { memcpy(buf, g_buffer, 0x1000); }
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: { char szModule[MAX_PATH] = { 0 }; GetModuleFileNameA(NULL, szModule, MAX_PATH); if (strstr(szModule, "winmine") != NULL) { MessageBoxA(NULL, "扫雷程序注入DLL成功", "", MB_OK); while (1) { if (strcmp(g_buffer, "quit") == 0) break; MessageBoxA(NULL, g_buffer, szModule, MB_OK); } } break; } case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
|
使用前需要将扫雷、DLL、控制端EXE放在同一个目录下。
后记
这是滴水三期初级班的过程,这篇文章前后参考了课件、网上的各种帖子和Github上gh0stkey师傅的项目,万分感谢。
挂一漏万,发现自己的内容被搬运的师傅请联系我补上。