原文地址:http://blog.sina.com.cn/s/blog_46d85b2a01010qpt.html
http://blog.sina.com.cn/s/articlelist_1188584234_0_1.html
WinSDK 是编程中的传统难点,曾经听有一个技术不是很好的朋友乱说什么给你 API 谁都会用,其实并非那么简单,个人写的WinAPI 程序也不少了,其实之所以难就难在每个调用的 API 都包含着 Windows 这个操作系统的潜规则或者是 windows 内部的运行机制。
首先来谈谈 句柄,初学习 WinSDK的朋友刚看到这个词头大了吧? 其实我也是了,我们来看看 programming windows里面是怎么说的,一个句柄仅仅是用来识别某些事情的数字。它唯一的标识这当前的一个实例。这样说确实不容易懂。那么我们这么看,比如你打开 windows自带的计算器。你多打开几次是不是桌面上出现了很多个计算器呢? 你使用其中一个计算器的时候当你按下等于按钮的时候运算结果是否会出现在其他的计算机结果栏里? 不会,那windows怎么知道让结果出现在哪里呢?这就是句柄的作用了,句柄唯一的标识着一个程序,你打开的每一个窗口(计算器) 都有一个不同的句柄,你你每一步操作都是指定了在某个句柄下的,所以,他不会出错。而且你打开的每一个计算机都共享着同样的代码和内存。通过句柄系统会把所需的资源充分的调用到当前的某个程序自己的数据区。
不仅是窗口,各种菜单,GDI 对象(图形设备接口(Graphics Device Interface, 或Graphical Device Interface)) 都有自己的句柄。
获取句柄的手段也是多重多样,不过当然是通过调用API函数实现了,如:
MFC 中的 hHandle = GetSafeHandle();
API 编程中的 hBrush = GetStorkObject(BLACK_BRUSH);
很多操作都需要将 句柄 添加到参数列表中,
当你没有直接定义句柄变量的时候可能要记忆很多API的返回类型来间接获取。如:
hPen = SelectObject(hdc,GetStockObject(&logicpen));
// SelectObject()这个函数在设置本设备描述表下的GDI对象时会返回设置前的GDI对象句柄
MoveToEx(hdc, pt1.x, pt1.y, &apt);
LineTo(hdc, pt2.x,pt2.y);
SelectObject(hdc,hPen);
完成选择自定义的GDI对象的操作。句柄的种类很多,掌握一种的使用方法后,其他所有的不学自通,WinAPI 编程永远伴随的元素中句柄是其中之一。非常重要。由于是浅谈,所以就说到这里了。
接下来是 windows下的 消息映射 机制了,呵呵,窗口过程,刚学的朋友难理解吧? WinSDK编程基于C,但是和 C 的理念有着完全的不同,这中间的不同,在我看来最多的也就是来自于这个消息映射,后面什么吹的很炫的 Hook技术,木马技术,键盘截获,都是来自于特殊消息的捕捉,映射自定义的特殊消息来实现的 (当然和我接下来谈的稍微有点不同)。
首先我们应该先明白 消息 和 事件 的区别,Windows是消息驱动的操作系统,这里 消息的产生来自于某个实例化的对象上用户的操作,来自控件,菜单,或者是系统本身产生的。而 事件 是靠消息触发的,但这也不是绝对的。可以用一个简单的例子去解释,我这里越写越觉得自己难表达清楚,就比如这么一个例子:“某男杀人这条消息导致被枪毙这个事件” 不过最重要的区别是在消息产生后并不会被直接处理,而是先插入windows系统的消息队列,然后系统判断此消息产生于哪个程序,送入此程序的消息循环,由 LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam)处理。而事件是操作系统处理消息的过程中反馈的结果。
用户操作 -> 产生消息 -> 发送系统 -> 系统判断来源 -> 发给相应的窗口过程或者其他Callback函数 -> 消息处理 -> 等待下一条消息的产生
以上为消息循环整个过程。
创建Windows窗体 : WinMain() 与 WndProc():https://www.cnblogs.com/shangdawei/p/3345132.html
#include
#include
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); //声名消息处理函数(处理windows和接收windows消息)
//hInstance:系统为窗口分配的实例号,2和3忘了.4是显示方式
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("HelloWin") ; //窗体名
HWND hwnd;//句柄
MSG msg;//消息体
WNDCLASS wndclass;//这义一个窗体类实例
//设置窗体参数
wndclass.style = CS_HREDRAW | CS_VREDRAW ; //样式
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;//窗体实例名,由windows自动分发
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;//显示上面的图标titlte
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;//窗口光标
wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ;//背景刷
wndclass.lpszMenuName=NULL;
wndclass.lpfnWndProc=WndProc;//设置窗体接收windws消息函数
wndclass.lpszClassName= szAppName;//窗体类名
if (!RegisterClass (&wndclass))//注册窗体类
{
MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ;
return 0 ;
};
//创建一个窗体。已分配内存。返回一个窗体句柄
hwnd = CreateWindow( szAppName, // window class name
TEXT ("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT,// initial x position
CW_USEDEFAULT,// initial y position
CW_USEDEFAULT,// initial x size
CW_USEDEFAULT,// initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL) ;
ShowWindow (hwnd,iCmdShow);//显示窗口
UpdateWindow (hwnd) ;//更新窗体
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage (&msg);//翻译消息并发送到windows消息队列
DispatchMessage (&msg) ;//接收信息
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)//消息的处理程序
{
HDC hdc ;
PAINTSTRUCT ps ;
RECT rect ;
switch (message)
{
case WM_CREATE:
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
TextOut(hdc,0,0,"Hello",strlen("Hello"));
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
上面大概流程:
LRSULT CALLBACK WndProc(hwnd , uint,wParam,lParam);
int WINAPI WinMain(…)
{
MSG msg; // 消息体
RegisterClass(…); // 注册窗口类
CreateWindow(…); // 创建窗口
ShowWindow(…); // 显示窗口
UpdateWindow(…);
While(GetMessage(&msg,…)){ // 消息循环
TranslateMessage(…);
DispatchMessage(…);
}
return msg.wParam ;
}
// 窗口过程函数,用于映射switch语句中各个需要被处理的消息。即 消息处理程序
LRSULT CALLBACK WndProc(hwnd , uint,wParam,lParam);
{
While((UINT)message)
{
Switch(message)
Case…
Case…
………
Default……….
}
}
以上是最基本的 WinAPi 编程的代码结构。其实这里面最重要的结构莫过于 while(GetMessage(&msg)) 和 Winproc 这个函数,这也是传统的C面向过程编程的区别,win编程总等着特定事件触发对应的消息映射函数来完成代码功能,并不是一条代码从头走到尾。关于特殊消息的映射,这里不谈,这里仅是个入门指引。
最后谈一点就是重绘问题。其实在我看来这个东西更多是属于GDI编程里面的东西,说起来其实难度不大,但是处理起来确实是个难点。先拿刚才的代码来说吧,在 WndProc 函数里面 先添加一条关于 WM_LBUTTONDOWN 的消息映射:
Static int apt[2];
case WM_LBUTTONDOWN:
hdc = GetDC(hwnd);
apt[1].x = LOWORD (lParam);
apt[1].y = HIWORD (lParam);
hPen = CreatePen(BLACK_PEN,3,RGB(125,125,125));
SelectObject(hdc,hPen);
MoveToEx(hdc,apt[0].x,apt[0].y,NULL);
LineTo(hdc,apt[1].x,apt[1].y);
apt[0].x = apt[1].x;
apt[0].y = apt[1].y;
DeleteObject(hPen);
ReleaseDC(hwnd,hdc);
return 0;
这段代码实现一个简单的画线功能,当你在你的客户区胡点一通鼠标后试着拖动一下窗口大小,或者将其最小化或者被其他窗口覆盖一下你都会发现你原来画的线没了,可是其他窗口为什么被覆盖了以后再弹出窗口还会有原来的东西呢? 那就是重绘,要重新绘制整个客户区(准确的说是失效的矩形),以上说的操作都会导致你的客户区失效,这时会产生重绘消息WM_PAINT,我们要想保存这些线那么我们就必须保存这些你用鼠标左键点过的点。当然这是重绘技术中最简单的,当你的客户区上是一个复杂的画面的话,就不仅仅需要保存点,还有各种形状的图形,颜色等等……这里给大家一段我自己写的代码来实现以上的 WM_LBUTTONDOWN消息映射来产生的点。通过单链表来动态添加点来实现重绘。
case WM_PAINT:
hdc = BeginPaint(hwnd,&ps);
TextOut(hdc,cxClient/6,cyClient/6,TEXT("图形重绘"),strlen("图形重绘"));
ReDrawLines(&MyList,hdc);
EndPaint(hwnd,&ps);
return 0;
case WM_LBUTTONDOWN:
hdc = GetDC(hwnd);
apt[1].x = LOWORD (lParam);
apt[1].y = HIWORD (lParam);
hPen = CreatePen(BLACK_PEN,2,RGB(125,0,0));
SelectObject(hdc,hPen);
MoveToEx(hdc,apt[0].x,apt[0].y,NULL);
LineTo(hdc,apt[1].x,apt[1].y);
MyList.pCurrent->x = apt[0].x;
MyList.pCurrent->y = apt[0].y;
MyList.pCurrent->pNext->x = apt[1].x;
MyList.pCurrent->pNext->y = apt[1].y;
MyList.m_iCounter = MyList.m_iCounter+2;
MyList.pCurrent = MyList.pCurrent->pNext->pNext;
apt[0].x = apt[1].x;
apt[0].y = apt[1].y;
DeleteObject(hPen);
ReleaseDC(hwnd,hdc);
return 0;
其中的重绘函数代码如下:
void ReDrawLines(LinkList* pLinkList,HDC hdc)
{
pMyPoint p = pLinkList->pHead;
int iSaver =pLinkList->m_iCounter;
while(iSaver!=0)
{
MoveToEx(hdc,p->x,p->y,NULL);
LineTo(hdc,p->pNext->x,p->pNext->y);
p=p->pNext->pNext;
iSaver=iSaver-2;
}
}
添加了以上的代码你会发现再次拖动窗口大小等等你原来画的线就都能重现出来了。呵呵是不是觉得一个看似简单的东西其实里面需要很多代码实现呢?也许,这就是windows.
好了,WinSDK入门的东西就谈这么多,希望能给初学者一定的帮助,这么多的字,都是我一个一个打出来没有任何借鉴和摘抄的。相信做为一个过来人能更多的理解大家学习中的困难。
比较大应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此协作来完成整个软件系统的工作。其中可能存在一些模块的功能较为通用,在构造其它软件系统时仍会被使用。在构造软件系统时,如果将所有模块的源代码都静态编译到整个应用程序EXE文件中,会产生一些问题:一个缺点是增加了应用程序的大小,它会占用更多的磁盘空间,程序运行时也会消耗较大的内存空间,造成系统资源的浪费;另一个缺点是,在编写大的EXE程序时,在每次修改重建时都必须调整编译所有源代码,增加了编译过程的复杂性,也不利于阶段性的单元测试。
Windows系统平台上提供了一种完全不同的较有效的编程和运行环境,你可以将独立的程序模块创建为较小的DLL(Dynamic Linkable Library)文件,并可对它们单独编译和测试。在运行时,只有当EXE程序确实要调用这些DLL模块的情况下,系统才会将它们装载到内存空间中。这种方式不仅减少了EXE文件的大小和对内存空间的需求,而且使这些DLL模块可以同时被多个应用程序使用。Microsoft Windows自己就将一些主要的系统功能以DLL模块的形式实现。例如IE中的一些基本功能就是由DLL文件实现的,它可以被其它应用程序调用和集成。
一般来说,DLL是一种磁盘文件(通常带有DLL扩展名),它由全局数据、服务函数和资源组成,在运行时被系统加载到进程的虚拟空间中,成为调用进程的一部分。如果与其它DLL之间没有冲突,该文件通常映射到进程虚拟空间的同一地址上。DLL模块中包含各种导出函数,用于向外界提供服务。Windows 在加载DLL模块时将进程函数调用与DLL文件的导出函数相匹配。
在Win32环境中,每个进程都复制了自己的读/写全局变量。如果想要与其它进程共享内存,必须使用内存映射文件或者声明一个共享数据段。DLL模块需要的堆栈内存都是从运行进程的堆栈中分配出来的。
DLL 现在越来越容易编写。Win32 已经大大简化了其编程模式,并有许多来自 AppWizard 和 MFC类库的支持。
DLL文件中包含一个导出函数表。这些导出函数由它们的符号名和称为标识号的整数与外界联系起来。函数表中还包含了DLL中函数的地址。当应用程序加载 DLL模块时时,它并不知道调用函数的实际地址,但它知道函数的符号名和标识号。动态链接过程在加载的DLL模块时动态建立一个函数调用与函数地址的对应表。如果重新编译和重建DLL文件,并不需要修改应用程序,除非你改变了导出函数的符号名和参数序列。
简单的DLL文件只为应用程序提供导出函数,比较复杂的DLL文件除了提供导出函数以外,还调用其它DLL文件中的函数。这样,一个特殊的DLL可以既有导入函数,又有导入函数。这并不是一个问题,因为动态链接过程可以处理交叉相关的情况。
在DLL代码中,必须像下面这样明确声明导出函数:
__declspec(dllexport) int MyFunction(int n);
但也可以在模块定义(DEF)文件中列出导出函数,不过这样做常常引起更多的麻烦。在应用程序方面,要求像下面这样明确声明相应的输入函数:
__declspec(dllimport) int MyFuncition(int n);
仅有导入和导出声明并不能使应用程序内部的函数调用链接到相应的DLL文件上。应用程序的项目必须为链接程序指定所需的输入库(LIB文件)。而且应用程序事实上必须至少包含一个对DLL函数的调用。
应用程序导入函数与DLL文件中的导出函数进行链接有两种方式:隐式链接和显式链接。所谓的隐式链接是指在应用程序中不需指明DLL文件的实际存储路径,程序员不需关心DLL文件的实际装载。而显式链接与此相反。
采用隐式链接方式,程序员在建立一个DLL文件时,链接程序会自动生成一个与之对应的LIB导入文件。该文件包含了每一个DLL导出函数的符号名和可选的标识号,但是并不含有实际的代码。LIB文件作为DLL的替代文件被编译到应用程序项目中。当程序员通过静态链接方式编译生成应用程序时,应用程序中的调用函数与LIB文件中导出符号相匹配,这些符号或标识号进入到生成的EXE文件中。LIB文件中也包含了对应的DLL文件名(但不是完全的路径名),链接程序将其存储在EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows根据这些信息发现并加载DLL,然后通过符号名或标识号实现对DLL函数的动态链接。
显式链接方式对于集成化的开发语言(例如VB)比较适合。有了显式链接,程序员就不必再使用导入文件,而是直接调用Win32 的LoadLibary函数,并指定DLL的路径作为参数。LoadLibary返回HINSTANCE参数,应用程序在调用 GetProcAddress函数时使用这一参数。GetProcAddress函数将符号名或标识号转换为DLL内部的地址。假设有一个导出如下函数的 DLL文件:
extern "C" __declspec(dllexport) double SquareRoot(double d);
下面是应用程序对该导出函数的显式链接的例子:
typedef double(SQRTPROC)(double);
HINSTANCE hInstance;
SQRTPROC* pFunction;
VERIFY(hInstance=::LoadLibrary("c:\\winnt\\system32\\mydll.dll"));
VERIFY(pFunction=(SQRTPROC*)::GetProcAddress(hInstance,"SquareRoot"));
double d=(*pFunction)(81.0);//调用该DLL函数
在隐式链接方式中,所有被应用程序调用的DLL文件都会在应用程序EXE文件加载时被加载在到内存中;但如果采用显式链接方式,程序员可以决定DLL文件何时加载或不加载。显式链接在运行时决定加载哪个DLL文件。例如,可以将一个带有字符串资源的DLL模块以英语加载,而另一个以西班牙语加载。应用程序在用户选择了合适的语种后再加载与之对应的DLL文件。
在Win16环境中,符号名链接效率较低,所有那时标识号链接是主要的链接方式。在Win32环境中,符号名链接的效率得到了改善。Microsoft 现在推荐使用符号名链接。但在MFC库中的DLL版本仍然采用的是标识号链接。一个典型的MFC程序可能会链接到数百个MFC DLL函数上。采用标识号链接的应用程序的EXE文件体相对较小,因为它不必包含导入函数的长字符串符号名。
DllMain函数是DLL模块的默认入口点。当Windows加载DLL模块时调用这一函数。系统首先调用全局对象的构造函数,然后调用全局函数 DLLMain。DLLMain函数不仅在将DLL链接加载到进程时被调用,在DLL模块与进程分离时(以及其它时候)也被调用。下面是一个框架 DLLMain函数的例子。
HINSTANCE g_hInstance;
extern "C" int APIENTRY DllMain(HINSTANCE hInstance,DWORD dwReason,LPVOID lpReserved)
{
if(dwReason==DLL_PROCESS_ATTACH)
{
TRACE0("EX22A.DLL Initializing!\n");
//在这里进行初始化
}
else if(dwReason=DLL_PROCESS_DETACH)
{
TRACE0("EX22A.DLL Terminating!\n");
//在这里进行清除工作
}
return 1;//成功
}
如果程序员没有为DLL模块编写一个DLLMain函数,系统会从其它运行库中引入一个不做任何操作的缺省DLLMain函数版本。在单个线程启动和终止时,DLLMain函数也被调用。正如由dwReason参数所表明的那样。
Exported fn(): send - Ord:0013h
地址 机器码 汇编代码
:71A21AF4 55 push ebp //将被HOOK的机器码(第1种方法)
:71A21AF5 8BEC mov ebp, esp //将被HOOK的机器码(第2种方法)
:71A21AF7 83EC10 sub esp, 00000010
:71A21AFA 56 push esi
:71A21AFB 57 push edi
:71A21AFC 33FF xor edi, edi
:71A21AFE 813D1C20A371931CA271 cmp dword ptr [71A3201C], 71A21C93 //将被HOOK的机器码(第4种方法)
:71A21B08 0F84853D0000 je 71A25893
:71A21B0E 8D45F8 lea eax, dword ptr [ebp-08]
:71A21B11 50 push eax
:71A21B12 E869F7FFFF call 71A21280
:71A21B17 3BC7 cmp eax, edi
:71A21B19 8945FC mov dword ptr [ebp-04], eax
:71A21B1C 0F85C4940000 jne 71A2AFE6
:71A21B22 FF7508 push [ebp+08]
:71A21B25 E826F7FFFF call 71A21250
:71A21B2A 8BF0 mov esi, eax
:71A21B2C 3BF7 cmp esi, edi
:71A21B2E 0F84AB940000 je 71A2AFDF
:71A21B34 8B4510 mov eax, dword ptr [ebp+10]
:71A21B37 53 push ebx
:71A21B38 8D4DFC lea ecx, dword ptr [ebp-04]
:71A21B3B 51 push ecx
:71A21B3C FF75F8 push [ebp-08]
:71A21B3F 8D4D08 lea ecx, dword ptr [ebp+08]
:71A21B42 57 push edi
:71A21B43 57 push edi
:71A21B44 FF7514 push [ebp+14]
:71A21B47 8945F0 mov dword ptr [ebp-10], eax
:71A21B4A 8B450C mov eax, dword ptr [ebp+0C]
:71A21B4D 51 push ecx
:71A21B4E 6A01 push 00000001
:71A21B50 8D4DF0 lea ecx, dword ptr [ebp-10]
:71A21B53 51 push ecx
:71A21B54 FF7508 push [ebp+08]
:71A21B57 8945F4 mov dword ptr [ebp-0C], eax
:71A21B5A 8B460C mov eax, dword ptr [esi+0C]
:71A21B5D FF5064 call [eax+64]
:71A21B60 8BCE mov ecx, esi
:71A21B62 8BD8 mov ebx, eax
:71A21B64 E8C7F6FFFF call 71A21230 //将被HOOK的机器码(第3种方法)
:71A21B69 3BDF cmp ebx, edi
:71A21B6B 5B pop ebx
:71A21B6C 0F855F940000 jne 71A2AFD1
:71A21B72 8B4508 mov eax, dword ptr [ebp+08]
:71A21B75 5F pop edi
:71A21B76 5E pop esi
:71A21B77 C9 leave
:71A21B78 C21000 ret 0010
1,把API入口的第一条指令是PUSH EBP指令(机器码0x55)替换成INT 3(机器码0xcc),然后用WINDOWS提供的调试函数来执行自己的代码,这中方法被SOFT ICE等DEBUGER广泛采用,它就是通过BPX在相应的地方设一条INT 3指令来下断点的。但是不提倡用这种方法,因为它会与WINDOWS或调试工具产生冲突,而汇编代码基本都要调试;
2,把第二条mov ebp,esp指令(机器码8BEC,2字节)替换为INT F0指令(机器码CDF0),然后在IDT里设置一个中断门,指向我们的代码。我这里给出一个HOOK代码:
lea ebp,[esp+12] //模拟原指令mov ebp,esp的功能
pushfd //保存现场
pushad //保存现场
//在这里做你想做的事情
popad //恢复现场
popfd //恢复现场
iretd //返回原指令的下一条指令继续执行原函数(71A21AF7地址处)
这种方法很好,但缺点是要在IDT设置一个中断门,也就是要进RING0。
3,更改CALL指令的相对地址(CALL分别在71A21B12、71A21B25、71A21B64,但前面2条CALL之前有一个条件跳转指令,有可能不被执行到,因此我们要HOOK 71A21B64处的CALL指令)。为什么要找CALL指令下手?因为它们都是5字节的指令,而且都是CALL指令,只要保持操作码0xE8不变,改变后面的相对地址就可以转到我们的HOOK代码去执行了,在我们的HOOK代码后面再转到目标地址去执行。
假设我们的HOOK代码在71A20400处,那么我们把71A21B64处的CALL指令改为CALL 71A20400(原指令是这样的:CALL 71A21230)
而71A20400处的HOOK代码是这样的:
71A20400:
pushad
//在这里做你想做的事情
popad
jmp 71A21230 //跳转到原CALL指令的目标地址,原指令是这样的:call 71A21230
这种方法隐蔽性很好,但是比较难找这条5字节的CALL指令,计算相对地址也复杂。
4,替换71A21AFE地址上的cmp dword ptr [71A3201C], 71A21C93指令(机器码:813D1C20A371931CA271,10字节)成为
call 71A20400
nop
nop
nop
nop
nop
(机器码:E8 XX XX XX XX 90 90 90 90 90,10字节)
在71A20400的HOOK代码是:
pushad
mov edx,71A3201Ch //模拟原指令cmp dword ptr [71A3201C], 71A21C93
cmp dword ptr [edx],71A21C93h //模拟原指令cmp dword ptr [71A3201C], 71A21C93
pushfd
//在这里做你想做的事
popfd
popad
ret
这种方法隐蔽性最好,但不是每个API都有这样的指令,要具体情况具体操作。
以上几种方法是常用的方法,值得一提的是很多人都是改API开头的5个字节,但是现在很多杀毒软件用这样的方法检查API是否被HOOK,或其他病毒木马在你之后又改了前5个字节,这样就会互相覆盖,最
APIHook 一直是使大家感兴趣的话题。屏幕取词,内码转化,屏幕翻译,中文平台等等都涉及到了此项技术。有很多文章涉及到了这项技术,但都闪烁其词不肯明明白白的公布。我仅在这里公布以下我用Delphi制作APIHook的一些心得。
1、自己写一个动态链接库,里面定义自己写的想取代系统的API。把这个动态链接库映射到2G以上的系统动态链接库所在空间,把系统动态链接库中的该API的指向修改指向自己的函数。这种方法的好处就是可以取代系统中运行全部程序的该API。但他有个局限,就是只适用于Win9x。(原因是NT中动态链接库不是共享的,每个进程都有自己的一份动态链接库在内存中的映射)
2、自己写一个动态链接库,里面定义自己写得象替代系统的API。把这个动态链接库映射到进程的空间里。将该进程对API的调用指向自己写的动态链接库。这种方法的好处是可以选择性的替代哪个进程的API。而且适用于所有的Windows操作系统。
这里我选用的是第二种方法。
第二种方法需要先了解一点PE文件格式的知识。
首先是一个实模式的的DOS文件头,是为了保持和DOS的兼容。接着是一个DOS的代理模块。你在纯DOS先运行Win32的可执行文件,看看是不是也执行了,只是显示的的是一行信息大意是说该Windows程序不能在DOS实模式下运行。
然后才是真正意义上的Windows可执行文件的文件头。它的具体位置不是每次都固定的。是由文件偏移$3C决定的。我们要用到的就是它。
如果我们在程序中调用了一个MessageBoxA函数那么它的实现过程是这样的。他先调用在本进程中的MessageBoxA函数然后才跳到动态链接库的MessageBoxA的入口点。即:
call messageBoxA(0040106c)
jmp dword ptr [_jmp_MessageBoxA@16(00425294)]
其中00425294的内容存储的就是就是MessageBoxA函数的入口地址。如果我们做一下手脚,那么......
那就开始吧!
我们需要定义两个结构
type
PImage_Import_Entry = ^Image_Import_Entry;
Image_Import_Entry = record
Characteristics: DWORD;
TimeDateStamp: DWORD;
MajorVersion: Word;
MinorVersion: Word;
Name: DWORD;
LookupTable: DWORD;
end;
type
TImportCode = packed record
JumpInstruction: Word; file: //定义跳转指令jmp
AddressOfPointerToFunction: ^Pointer; file: //定义要跳转到的函数
end;
PImportCode = ^TImportCode;
然后是确定函数的地址。
function LocateFunctionAddress(Code: Pointer): Pointer;
var
func: PImportCode;
begin
Result := Code;
if Code = nil then exit;
try
func := code;
if (func.JumpInstruction = $25FF) then
begin
Result := func.AddressOfPointerToFunction^;
end;
except
Result := nil;
end;
end;
参数Code是函数在进程中的指针,即那条Jmp XXX的指令。$25FF就是跳转指令的机器码。
在这里我将要实现转跳。有人说修改内存内容要进入Ring 0 才可以。可是Windows本身提供了一个写内存的指令WriteProcessMemory。有了这把利器,我们几乎无所不能。如游戏的修改等在这里我们只谈APIHOOK。
function RepointFunction(OldFunc, NewFunc: Pointer): Integer;
var
IsDone: TList;
function RepointAddrInModule(hModule: THandle; OldFunc, NewFunc: Pointer): Integer;
var
Dos: PImageDosHeader;
NT: PImageNTHeaders;
ImportDesc: PImage_Import_Entry;
RVA: DWORD;
Func: ^Pointer;
DLL: string;
f: Pointer;
written: DWORD;
begin
Result := 0;
Dos := Pointer(hModule);
if IsDone.IndexOf(Dos) >= 0 then exit;
IsDone.Add(Dos);
OldFunc := LocateFunctionAddress(OldFunc);
if IsBadReadPtr(Dos, SizeOf(TImageDosHeader)) then exit;
if Dos.e_magic <> IMAGE_DOS_SIGNATURE then exit;
NT := Pointer(Integer(Dos) + dos._lfanew);
RVA := NT^.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
.VirtualAddress;
if RVA = 0 then exit;
ImportDesc := pointer(integer(Dos) + RVA);
while (ImportDesc^.Name <> 0) do
begin
DLL := PChar(Integer(Dos) + ImportDesc^.Name);
RepointAddrInModule(GetModuleHandle(PChar(DLL)), OldFunc, NewFunc);
Func := Pointer(Integer(DOS) + ImportDesc.LookupTable);
while Func^ <> nil do
begin
f := LocateFunctionAddress(Func^);
if f = OldFunc then
begin
WriteProcessMemory(GetCurrentProcess, Func, @NewFunc, 4, written);
if Written > 0 then Inc(Result);
end;
Inc(Func);
end;
Inc(ImportDesc);
end;
end;
begin
IsDone := TList.Create;
try
Result := RepointAddrInModule(GetModuleHandle(nil), OldFunc, NewFunc);
finally
IsDone.Free;
end;
end;
有了这两个函数我们几乎可以更改任何API函数。
我们可以先写一个DLL文件。我这里以修改Text相关函数为例:
先定义几个函数:
type
TTextOutA = function(DC: HDC; X, Y: Integer; Str: PAnsiChar; Count: Integer): BOOL; stdcall;
TTextOutW = function(DC: HDC; X, Y: Integer; Str: PWideChar; Count: Integer): BOOL; stdcall;
TTextOut = function(DC: HDC; X, Y: Integer; Str: PChar; Count: Integer): BOOL; stdcall;
TDrawTextA = function(hDC: HDC; lpString: PAnsiChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
TDrawTextW = function(hDC: HDC; lpString: PWideChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
TDrawText = function(hDC: HDC; lpString: PChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
var
OldTextOutA: TTextOutA;
OldTextOutW: TTextOutW;
OldTextOut: TTextOut;
OldDrawTextA: TDrawTextA;
OldDrawTextW: TDrawTextW;
OldDrawText: TDrawText;
......
function MyTextOutA(DC: HDC; X, Y: Integer; Str: PAnsiChar; Count: Integer): BOOL; stdcall;
begin
OldTextOutA(DC, X, Y, ''''ABC'''', length(''''ABC''''));
end;
function MyTextOutW(DC: HDC; X, Y: Integer; Str: PWideChar; Count: Integer): BOOL; stdcall;
begin
OldTextOutW(DC, X, Y, ''''ABC'''', length(''''ABC''''));
end;
function MyTextOut(DC: HDC; X, Y: Integer; Str: PChar; Count: Integer): BOOL; stdcall;
begin
OldTextOut(DC, X, Y, ''''ABC'''', length(''''ABC''''));
end;
function MyDrawTextA(hDC: HDC; lpString: PAnsiChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
begin
OldDrawTextA(hDC, ''''ABC'''', length(''''ABC''''), lpRect, uFormat);
end;
function MyDrawTextW(hDC: HDC; lpString: PWideChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
begin
OldDrawTextW(hDC, ''''ABC'''', length(''''ABC''''), lpRect, uFormat);
end;
function MyDrawText(hDC: HDC; lpString: PChar; nCount: Integer; var lpRect: TRect; uFormat: UINT): Integer; stdcall;
begin
OldDrawText(hDC, ''''ABC'''', length(''''ABC''''), lpRect, uFormat);
end;
调用时我们要把原来的函数地址保存下来:
if @OldTextOutA = nil then
@OldTextOutA := LocateFunctionAddress(@TextOutA);
if @OldTextOutW = nil then
@OldTextOutW := LocateFunctionAddress(@TextOutW);
if @OldTextOut = nil then
@OldTextOut := LocateFunctionAddress(@TextOut);
if @OldDrawTextA = nil then
@OldDrawTextA := LocateFunctionAddress(@DrawTextA);
if @OldDrawTextW = nil then
@OldDrawTextW := LocateFunctionAddress(@DrawTextW);
if @OldDrawText = nil then
@OldDrawText := LocateFunctionAddress(@DrawText);
然后很顺其自然的用自己的函数替换掉原来的函数
RepointFunction(@OldTextOutA, @MyTextOutA);
RepointFunction(@OldTextOutW, @MyTextOutW);
RepointFunction(@OldTextOut, @MyTextOut);
RepointFunction(@OldDrawTextA, @MyDrawTextA);
RepointFunction(@OldDrawTextW, @MyDrawTextW);
RepointFunction(@OldDrawText, @MyDrawText);
在结束时不要忘记恢复原来函数的入口,要不然你会死得很难看哟!好了我们在写一个Demo程序。你会说怎么文字没有变成ABC呀?是呀,你要刷新一下才行。最小化然后在最大化。看看变了没有。
要不然你就写代码刷新一下好了。至于去拦截其他进程的API那就用SetWindowsHookEx写一个其他的钩子将DLL映射进去就行了,我就不再浪费口水了。
掌握了该方法你几乎无所不能。你可以修改其它程序。你可以拦截Createwindow等窗口函数改变其他程序的窗口形状、你还可以入侵其它的程序,你还可以......嘿嘿。干了坏事别招出我来就行了。