返回列表 发帖

[转帖]防火墙的原理及实现

[这个贴子最后由x86在 2005/11/09 02:56pm 第 1 次编辑]

   如果你稍微注意一下关于网络安全方面的报道,就会明白如今的Internet是多么的危险。如果你的电脑没有任何安全措施而直接连接到Internet,那将是一件多么可怕的事情啊。也许,在你刚刚连接到Internet的时候,就已经被别人盯上了。他已经准备或者已经开始对你的电脑进行某些你不希望的操作。在公司,每个员工的电脑位于一个局域网中通过局域网共享连接到Internet,假设局域网内通信是安全的,但是由于局域网与Internet相连,使局域网内的计算机能够被Internet上的恶意程序进行控制和破坏。这样,因此防火墙逐渐成为一个互联网上家喻户晓的东东了。什么是防火墙?从字面上理解就是一堵防火的墙。而这里的火是指来自Internet的各种各样的威胁。防火墙就像一堵墙一样被安装在个人电脑或局域网与Internet之间,监视防火墙内部与Internet之间的通信,如果发现可疑情况,则可以对可疑的数据包进行拦截。究竟什么样的数据包算作可疑或者是恶意的呢?那就需要防火墙有一个策略模块来判断。一个防火墙的策略模块设计的好坏直接影响到防火墙的质量,当然现在多数防火墙能够让用户根据自己的实际情况设置一些策略,这样防火墙有了更大的适应性。
2.1 防火墙的分类
  现在的防火墙根据实现原理可以分为包过滤防火墙和代理防火墙。
  包过滤防火墙是指使用某种拦截技术拦截到本机接收或发送的数据包,对拦截到的数据包使用策略分析,从而决定是否让数据包通过。比如,在应用程序发送数据的时候,数据首先被防火墙拦截,在防火墙确认信息安全合法后,再真正的把数据发送出去。如果系统接收到数据包,也是先由防火墙拦截,在确认数据包安全合法后,再把数据包放行给需要接收的应用程序。通过这样的收发流程可以看出,包过滤防火墙位于外部主机与内部主机之间,他对数据进行了过滤。
  代理防火墙是指防火墙自身实现了一个代理模块防火墙内部与外部的连接都要通过这个代理进行,他们之间不会直接进行连接。这样就可以在代理模块中使用策略了。
  在本章将指导读者完成一个简单的防火墙软件的开发,我们将编写一个简单的应用层包过滤防火墙软件。该防火墙的功能如下:
  检测访问网络的信息
  能够分析出应用程序网络通信的一些tcp/ip族协议,并进行监视和记录
  能够进行ip和端口过滤
  为了实现这些功能我们设计3个模块:用户界面模块:UserUI是一个exe文件,它调用MainHookDll模块来完成拦截功能,调用FireWallDll模块来完成对防火墙策略的设置。
  防火墙功能模块:FireWallDll模块向MainHookDll模块提供需要拦截的API信息,并且它实现替换函数,并且防火墙策略也在此模块中。
  HOOK模块:MainHookDll模块主要实现HOOK API的相关功能。
2.2 各防火墙技术点睛
  包过滤防火墙最核心的技术是要能拦截到数据包,也就是说防火墙要在系统接收到数据包,但是还没有分发给某个应用程序处理前和某个应用程序发送数据包,但是系统还没有真正把数据包发送出去之前能够对数据包进行过滤。而拦截数据包可以实现在网络的不同层次上。我们现在讨论的应用都是在WinSock2的基础上进行的,而WinSock2主要提供了WS2_32.DLL文件,当安装WinSock2后,系统网络结构将像下图这

  无论是HOOK API还是SPI的方法,都是位于ISO/OSI的传输层的上面,而相对于TCP/IP模型,则它们都是属于应用层的方法。
  通过上图我们可以发现,Windows在TCP/IP所在的传输层、网络层之下的数据链路层中提供了NDIS(网络设备接口规范)。物理层的数据将传递给它进行处理后再由NDIS传递给网络层处理。由于NDIS属于驱动程序范围,所以在开发NDIS应用时需要使用DDK。

[转帖]防火墙的原理及实现

我要晕了,斑主,谢谢你,I KNOW A LITTLE

TOP

[转帖]防火墙的原理及实现

2.2.1.2 SPI
   通过以上分析看来,通过hook api的方法,可以方便的进行IP机端口过滤,还能对信息内容进行过滤。但是它是位于ws2_32.dll之上的,属于用应用层的拦截,只能对TCP/UDP等常见的协议进行较高层次的拦截。所以,市面上见到的防火墙软件很少使用这种技术实现。
下面再介绍一种在应用层的拦截数据包的技术SPI(网络服务提供者接口)。如果想详细的了解关于SPI防火墙的相关知识,请参考《Windows防火墙与网络风暴截获技术》。
2.2.2 传输层实现
   以上两种方法都是在应用层进行拦截过滤,在应用层,数据包还没有被切片处理,所以得到的信息很完整,很容易实现对内容的过滤。而且由于实现在应用层,所以有很好的平台无关性。只要安装了Winsock2的机器都可以使用这种方式实现过滤。但是他也有弊端,由于这两种拦截技术都是围绕ws2_32.dll进行的,所以如果没有使用Winsock2 API发送或接收的数据,将不能拦截,比如直接使用驱动层的函数进行收发的数据包。而现在那些无聊的黑客很容易使用驱动层的函数进行收发恶意数据,所以,市面上见到的防火墙大多不使用这两种技术。

TOP

[转帖]防火墙的原理及实现

下面的代码列出了EjectLib函数,它是使系统当前进程卸载我们的MainHookDll文件:int WINAPI CHookApi::EjectLib(DWORD dwProcessId, LPTSTR lpDllName)
{
     // open the process
     HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,FALSE,dwProcessId);
     if(hProcess == NULL)
     return -1;
  // 枚举进程中的模块
     HANDLE hModuleSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId);
if(hModuleSnap == INVALID_HANDLE_VALUE)
{
  CloseHandle(hProcess);
return -1;
}
MODULEENTRY32 me32;
me32.dwSize = sizeof(MODULEENTRY32);
  BOOL bFound = FALSE;
HMODULE hmod = NULL;
if(Module32First(hModuleSnap, &me32))
{
do
{
if(strcmpi(me32.szModule,lpDllName) == 0)
{
hmod = me32.hModule;
bFound = TRUE;
}
}while(!bFound && Module32Next(hModuleSnap, &me32));
}
CloseHandle(hModuleSnap);
if(hmod == NULL)
{
// 没有指定的模块
CloseHandle(hProcess);
return 0;
}
// 创建远程线程
PTHREAD_START_ROUTINE pfnRemote =(PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32"),"FreeLibrary");
if(pfnRemote ==NULL)
{
CloseHandle(hProcess);
return -1;
}
HANDLE hThread =CreateRemoteThread(hProcess,NULL,0,pfnRemote,hmod,0,NULL);
if(hThread ==NULL)
{
CloseHandle(hProcess);
return -1;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}
我们在函数中使用了Psapi中定义的函数列举指定进程中加载的模块,看看是否加载了我们的MainHookDll文件,如果加载了,那么使用FreeLibrary函数卸载它。

2.2.1.1.2《WINDOWS 核心编程》Jeffrer老大实现的方法,暂叫做IAT大法吧,这种方法在这里就不进行说明了,有兴趣的话去参考《Windows 核心编程》吧。
2.2.1.1.3 DLL替换大法
       这种方法是工作量最大,难度最低的方法。我还用拦截ws2_32.dll中的recv函数为例。大家都知道,如果某个应用程序要调用recv接收信息,那么它一定要加载ws2_32.dll。那么,我们可以制作一个自己的dll,它和ws2_32.dll的导出函数完全一致,而ws2_32.dll则被改名为ws2_32_old.dll,在我们的dll中加载ws2_32_old.dll,并且其他函数都直接调用原来的ws2_32_old.dll中的函数,而对于我们感兴趣的函数则进行自己的处理。不过步推荐这种做法,因为可能会因为ws2_32.dll版本的升级而使你的MainHookDll模块不能使用。

TOP

[转帖]防火墙的原理及实现

   CHookApi类完成了Hook的核心工作后,我们需要让系统所有的进程加载我们的MainHookDll.Dll,从而对系统所有进程中的指定API进行拦截。我们的想法是,MainHookDll模块提供2个导出函数,HookAllProcesses和UnhookAllProcesses函数完成这个功能,当UserUI调用HookAllProcesses函数的时候,系统所有的进程将加载MainHookDll.Dll,在加载的同时让CHookApi类开始工作,对目标进程中的所有指定API的前5个字节进行替换。当调用UnhookAllProcesses函数的时候,系统所有的进程将卸载MainHookDll.Dll,从而取消对指定API的拦截。在HookAllProcesses和UnhookAllProcesses函数中实际上是列举当前系统的所有进程,并对所有进程进行相同的操作。所以我们可以再实现两个函数,用CHookApi类的静态成员InjectDll和EjectLib函数完成真正的功能,而HookAllProcesses和UnhookAllProcesses中实现列举系统进程并对所有进程调用InjectDll或EjectLib函数。我们使用远程线程来实现InjectDll和EjectLib函数。远程线程是指在当前进程中使目标进程启动一个线程。可以通过以下方法完成远程线程的调用。首先使用OpenProcess打开目标进程得到进程句柄,要启动远程线程最少需要用PROCESS_CREATE_THREAD,PROCESS_QUERY_INFORMATION,PROCESS_VM_OPERATION,PROCESS_VM_WRITE, and PROCESS_VM_READ权限打开目标进程(我们使用PROCESS_ALL_ACCESS)。然后调用VirtualAllocEx函数在目标进程中分配内存,使用WriteProcessMemory函数将当前进程中的线程函数的参数写入在目标进程分配的内存中,最后调用CreateRemoteThread函数使目标进程执行线程函数。(实现过程请参考源代码中的注释)。下面给出InjectDll函数的代码,该函数将指定的Dll文件注入到指定ID号的进程中。
int WINAPI CHookApi::InjectDll(DWORD dwProcessId, LPTSTR lpDllName)
{
PTHREAD_START_ROUTINE pfnRemote =(PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32"), "LoadLibraryA");
if(pfnRemote ==NULL)
  return -1;
HANDLE hProcess =OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
if(hProcess ==NULL)
{
return -1;
}
int iMemSize = (int)strlen(lpDllName)+1;
void *pRemoteMem =VirtualAllocEx(hProcess, NULL, iMemSize, MEM_COMMIT, PAGE_READWRITE);
if(pRemoteMem ==NULL)
{
CloseHandle(hProcess);
return -1;
}
if (!WriteProcessMemory(hProcess, pRemoteMem, lpDllName, iMemSize,NULL))
{
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
  CloseHandle(hProcess);
return -1;
}
HANDLE hThread =CreateRemoteThread(hProcess, NULL, 0, pfnRemote, pRemoteMem, 0, NULL);
if(hThread ==NULL)
{
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
CloseHandle(hProcess);
return -1;
}
WaitForSingleObject(hThread, INFINITE);
VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE);
  CloseHandle(hProcess);
CloseHandle(hThread);
return 0;
}
函数一开始,指定了要被注入的线程函数的位置,在这里,我们的线程函数就是LoadLibraryA函数,远程线程中的线程函数应该是PTHREAD_START_ROUTINE类型的函数指针,该函数指针的声明如下:
typedef DWORD (*PTHREAD_START_ROUTINE)(LPVOID)
由声明可知,该函数是一个只有一个参数的,且返回值是DWORD类型的函数,而LoadLibraryA函数符合要求的,所以它可以直接作为远程线程的线程函数。当远程进程调用LoadLibraryA把我们的MainHookDll.Dll加载后,拦截工作就会自动进行。紧接着,用VirtualAllocEx函数在远程进程中开辟一块内存,开辟的内存的大小是MainHookDll的全路径名(注意最后的’\0’)。接着使用WriteProcessMemory函数将参数(即MainHookDll的全路径名)写入到刚刚开辟的远程进程中的地址空间。最后调用CreateRemoteThread函数启动远程线程。要注意的是,当线程结束后,要用VirtualFreeEx函数释放在远程进程内开辟的内存。

TOP

[转帖]防火墙的原理及实现

其中HOOKAPIINFO结构填写基本的拦截信息,在CHookApi内部将会把它转化为APIINFO结构。HOOKAPIINFO和APIINFO结构定义在CommonHeader.h文件中,CommonHeader.h文件如下: (CommonHeader.h) #define FIREWALLDLLNAME "FireWallDll.Dll" // 实现被拦截的API替换函数的名字 #define MAINHOOKDLLNAME "MainHookDll.Dll" // 拦截API主模块的名字 #define GETHOOKINFOFUNCNAMEINFIREWALLDLL "GetHookApiInfo" // 替换模块导出函数,它返回要拦截的信息 #define FREEHOOKINFOFUNCNAMEINFIREWALLDLL "FreeHookApiInfo" // 替换模块导出函数,它释放要拦截的信息 #define CALLCODE 0xE8 #ifndef COMMONHEADER_H #define COMMONHEADER_H #include using namespace std; typedef void(*CMAPIFUNC)(void); // 关于API函数信息的结构 typedef struct _APIINFO { // 要拦截的API函数名 char szOrgApiName[100]; // 要拦截的API的地址 CMAPIFUNC lfOrgApiAddr; // 我们要替换原来API的地址 CMAPIFUNC lfMyApiAddr; // 要保存的原来API入口点的前5个字节 BYTE OrgApiBytes[5]; // 对进程内存的保护状态,调用VirtualProtect来改变对内存访问权限时得到的。 DWORD dwOldProtectFlag; // 参数的个数 int ParamCount; // 临界对象,为了互斥用,避免同时修改原始api的前5个字节 CRITICAL_SECTION cs; // 是否已经HOOK了 BOOL bIsHooked; // 用户需要使用的结构,通过这个结构来了解用户需要拦截的API的信息 typedef struct _HOOKAPIINFO { union { struct { // 我们要替换的API所在的DLL的名字 char szMyModuleName[100]; // 我们要替换原来API的函数名字 char szMyApiName[50]; } MyApi; CMAPIFUNC lfMyApi; }; // 要拦截的API所在DLL的名字 char szOrgModuleName[100]; // 要拦截的API的名字 char szOrgApiName[50]; // 参数的个数 int ParamCount; // 提供MyAPI的方式 BOOL bMyApiType; // 如果是0代表提供地址,1代表使用模块名和函数名指定 } HOOKAPIINFO; // 获得要拦截的API的信息函数指针 typedef vector*(*GETHOOKAPIINFO)(void); // 释放要拦截的API的信息函数指针 typedef void(*FREEHOOKAPIINFO)(void); // 远程注入函数指针 typedef int (*INJECTDLL)(DWORD dwProcessId, LPTSTR lpDllName); #endif 在这个头文件中,除了定义了HOOKAPIINFO和APIINFO结构还有一些其它定义,FIREWALLDLLNAME宏指定防火墙策略模块的文件名称,MAINHOOKDLLNAME宏指定本模块的文件名称,GETHOOKINFOFUNCNAMEINFIREWALLDLL宏指定在防火墙策略模块中用来返回用户的HOOKAPIINFO结构容器指针的函数名称,程序将会自动加载防火墙策略模块并调用这个函数获得用户的HOOKAPIINFO结构容器的指针,根据后面的函数指针的定义不难看出,这个函数必须是一个返回值为vector*,并且没有参数的导出函数。FREEHOOKINFOFUNCNAMEINFIREWALLDLL 是释放返回的vector*指针的导出函数名字。CALLCODE宏指定要替换的跳转指令,这里只用CALL指令,他的机器码是E8。 在构造函数中调用Init函数和HookAllApis函数,这样使该类被构造的时候就能够自动进行初始化和hook工作。Init函数的主要工作是将用户提供的HOOKAPIINFO结构转化成APIINFO结构。HookAllApis函数内部循环调用HookOneApi函数进行真正的Hook操作。详见源代码中的注释。

TOP

[转帖]防火墙的原理及实现

前面提到过,我们的CHookApi类主要向外部提供2个方法,HookAllAPI方法和UnhookAllAPI方法。当调用HookAllAPI的时候,将拦截系统所有用户程序中我们感兴趣的API函数。当调用UnhookAllAPI的时候,将撤销拦截。当拦截启动之前,我们应该将所有我们感兴趣的,即需要拦截的API信息(如API名称,对应的替换函数名称、参数个数等)交给CHookApi,CHookApi类内部才能完成所有的拦截工作。以下是CHookApi类的声明 (HookApi.h) #include "../include/CommonHeader.h" #include #include #pragma comment(lib,"Psapi.lib") // 通用替换函数指针声明 typedef void(*COMMONFUNC)(void); /* CHookApi类实现了Hook的核心功能 */ class CHookApi { public: // 构造函数 CHookApi(void); // 析构函数 ~CHookApi(void); private: // 自身路径 static char m_szDllPathName[MAX_PATH]; // 此容器包含所有要Hook的API信息(在类中使用的有效信息) static vector m_vpApiInfo; // 此容器包含所有要Hook的API信息(用户填写的信息) vector* m_vpHookApiInfo; // 防火墙策略模块句柄 HMODULE m_hFireWall; // 加载防火墙策略模块 BOOL LoadFireWallModule(void); // 卸载防火墙策略模块 BOOL UnloadFireWallModule(void); public: // 使用远程线程注入的方法将我们的DLL注入到指定进程 static int WINAPI InjectDll(DWORD dwProcessId, LPTSTR lpDllName); // 使用远程线程注入的方法将我们的DLL卸载 static int WINAPI EjectLib(DWORD dwProcessId, LPTSTR lpDllName); protected: // 通用替换函数,这是技术核心部分 static void CommonFunc(void); private: // 初始化函数 BOOL Init(void); // 挂钩一个指定API的函数 BOOL HookOneApi(APIINFO* pai); // 挂钩所有指定API的函数 BOOL HookAllApis(void); // 取消挂钩一个指定API的函数 BOOL UnhookOneApi(APIINFO* pai); // 取消挂钩所有指定API的函数 BOOL UnhookAllApis(void); // 设置进程内存区域的存取权限 BOOL SetMemmoryAccess(APIINFO* pai,BOOL CanWritten); // 修改/恢复指定API的前5个字节 BOOL SetCallMemmory(APIINFO* pai,BOOL bHook); };

TOP

[转帖]防火墙的原理及实现

   此时我们可以看到,[EBP]为保存的ebp的值,现在对我们没有用处,函数返回前用于恢复EBP的值,[EBP+4]是recv函数的CALL XXXX后面指令的地址(也就是第六个字节的地址),我们可以通过将此值减去5来得到recv的入口地址,这样在我们所有hook的api函数的列表中进行检索,就可以匹配出用户调用的是哪一个API函数,从而为后面恢复和再次改变该API的入口5字节做准备,因为调用任何我们需要HOOK的API程序都会进入到这个无返回值无参数的函数,所以通过这种方法找到当前HOOK的是哪一个API,从而可以区分不同的API进行特殊的处理。[EBP+8]保存的是用户调用recv后的返回地址,由于我们执行完替换函数后,应该返回到这个地址,而不应该返回到recv的第6个字节处执行,所以我们还是需要保存下这个值,以便在我们用ret返回前把它压栈从而使程序返回到用户调用recv的下一条指令处继续运行。
我们先定义如下函数:
void CommonFunc(void);
我们现在实现它,请注意参考上面堆栈表格。
void CommonFunc(void)
{ DWORD pdwCall;         // recv入口地址
  DWORD dwRtAddr;            // 我们的函数真正要返回的地址
  DWORD* pdwParam;          // 第一个参数的地址
  DWORD dwParamCount;     // 参数个数
  DWORD dwParamSize; // 所有参数所占用的大小应该=4* dwParamCount
  DWORD dwRt;                   // 返回值
_asm
{
lea EAX,[EBP+4]          // recv入口处第6个字节的地址
mov [pdwCall],EAX
mov EAX,[EBP+8]        // 用户调用recv(即call XXXX)后面一条指令的地址
mov [dwRtAddr],EAX
lea EAX, [EBP+12] // 第一个参数的地址!
mov [pdwParam],EAX
}
(*pdwCall) -= 5;    // 获得recv入口地址
HOOKINFO *hi = findHookInfo(pdwCall);  // 通过原始API的入口地址获得此API的相关信息
memcpy(pdwCall,hi->OrgApi5bytes,5);              // 恢复被调用API的前5个字节,使下面的代码可以正常调用
// 下面准备进入用户针对此API的替换函数,现准备参数
dwParamCount = hi->ParamCount;      // 得到本API的参数个数
dwParamSize = 4*dwParamCount;             // 计算参数所占用大小
DWORD pdwESP;
_asm
{
sub esp,[dwParamSize]  // 将栈增加,可以容纳参数
mov [pdwESP],esp                     // 保存当前栈的地址
}
memcpy(pdwESP,pdwParam,dwParamSize);//将用户传递的参数拷贝到栈中
hi->myAPIFunc();  // 调用用户针对此API的替换函数
_asm
{
mov [dwRt],eax     // 保存返回值
}
// 如果是CreateProcess,那么继续hook它
pPi = (PROCESS_INFORMATION*)pdwParam[9];
if(strcmpi(pai->szOrgApiName,"CreateProcessA") != 0 || strcmpi(pai->szOrgApiName,"CreateProcessW") != 0)
{
InjectDll(pPi->dwProcessId,m_szDllPathName);
}
// 下面再次修改原始API的前5个字节
memcpy(pdwCall,JMPCODE,5);  // #define JMPCODE 0xE8
DWORD* pdwapi = pdwCall[1];
pdwapi[0] = (DWORD) CommonFunc – (DWORD)pdCall – 5; // CommonFunc函数地址的偏移
// 下面准备返回的操作
_asm
{ add esp,[dwParamSize] // 清理我们为了调用真正的替换函数而分配的堆栈里的参数
// 下面弹出所有保存的寄存器值(按照入栈的逆顺序)
pop EDI  // 恢复EDI
pop ESI // 恢复ESI
pop EBX // 恢复EBX
// 我们没有改动过EBP的值,所以EBP指向堆栈中OldEBP的位置
mov ESP,EBP
pop EBP // 恢复EBP
// 由于堆栈中还剩下参数和两个返回地址(我们真正要返回的地址和原始API中的第6个字节的地址),所以我们把这些数据也清除出堆栈
add ESP,8 // 清除两个返回地址
mov ECX,[dwParamSize]     // 获得参数的大小
add ESP,ECX // 清除参数
mov EAX,[dwRt]   // 设置返回值
// 由于调用ret返回时,程序先从堆栈中取出返回地址,所以我们把要真正返回的地址压入堆栈中
mov EDX,[dwRtAddr] // 设置返回地址
push EDX
ret // 返回
}
}
  最后要注意得一点是,如果要执行得API函数是CreateProcess,那么应该把它新开启得进程也HOOK掉。以上我们了解了通用替换函数的原理,那么让我们深入的讨论CHookApi类,并且实现它。

TOP

[转帖]防火墙的原理及实现

有了上面的知识,让我们回顾一下先前讨论的问题,首先,用户调用API的recv函数,程序运行到recv的入口地址处,此时堆栈中拥有用户调用recv的参数和用户代码中CALL [recv]的下一条指令的地址。堆栈如下图:
堆栈指针   堆栈的内容   堆栈内容的含义
[ESP]
0x00000100     0           参数
0x000000fc    len          参数
0x000000f8    buf          参数
0x000000f4     S           参数
0x000000f0  RetUserAddress 用户调用recv的下一条指令的地址

   然后程序指针EIP被修改为recv入口处的地址,而入口地址处有一条简单的CALL指令,它使程序将recv的第6个字节的地址压入栈中(因为CALL XXXX占用5个字节,第六个字节被认为为返回地址),然后跳转到我们的无参数无返回值的通用替换函数中去了,好了看看现在堆栈中都有些什么?如图:
堆栈指针   堆栈的内容   堆栈内容的含义
[ESP]
0x00000100     0           参数
0x000000fc    len          参数
0x000000f8    buf          参数
0x000000f4     S           参数
0x000000f0  RetUserAddress 用户调用recv的下一条指令的地址
0x00000ec   RetrecvAddress recv的第六个字节的地址

   首先是参数,其次是用户调用recv后的返回值,然后是recv调用我们的替换函数中的返回值,紧接着就像刚才提到的那样,程序将EBP当前内容压入栈中。如图
堆栈指针   堆栈的内容   堆栈内容的含义
[ESP]
0x00000100     0           参数
0x000000fc    len          参数
0x000000f8    buf          参数
0x000000f4     S           参数
0x000000f0  RetUserAddress 用户调用recv的下一条指令的地址
0x00000ec   RetrecvAddress recv的第六个字节的地址
0x000000e8  OldEBP         保存的旧的EBP的内容,然后[EBP]= 0x000000e8
0x000000e4  OldEBX         保存的旧的EBX的内容
0x000000e0  OldESI         保存的旧的ESI的内容
0x000000dc  OldEDI         保存的旧的EDI的内容,此时[ESP]=0x000000dc

TOP

[转帖]防火墙的原理及实现

2.2.1 应用层实现
2.2.1.1 HOOK API
在应用层我们可以使用HOOK API的方法,拦截windows中与网络通信相关的Socket API。由于一般的应用软件都是通过调用Windows socket API进行数据通信的。我们只考虑Winsock2,也就是说这些socket操作的API都被封装到ws2_32.dll中。关于HOOK API也有众多实现方法。
2.2.1.1.1跳转大法(我这么叫它)
   我们知道Windows系统API都是被封装到DLL中,在某个应用程序要调用一个API函数的时候,如果这个函数所在的DLL没有被加载到本进程中则加载它,然后保存当前环境当前值(各个寄存器和函数调用完后的返回地址等)。接着程序会跳转到这个API的入口地址去执行此处的指令。我们想在调用真正的API之前先调用我们的函数,那么可以修改这个API函数的入口处的代码,使他调用我们的函数,然后在我们的函数最后再调用原来的API函数。下面以拦截WS2_32.dll中的recv函数为例说明拦截的主要过程。首先可以使用普通的HOOK把自己编写的DLL挂接到系统当前运行的所有进程中(要排除一些Windows系统自身的进程,否则会出现问题,影响系统正常工作),也可以使用列举系统进程然后用远程线程注入的方法,但是后者只适用于Win2000以上的操作系统。
   当我们的DLL被所有目标进程加载后,我们就可以进行真正的工作了。首先使用Tool Help库的相关函数列举目标进程加载的所有模块,看看是否有ws2_32.dll,如果没有,说明这个进程没有使用Winsock提供的函数,那么我们就不用再给这个进程添乱了。如果找到ws2_32.dll模块,那么OK,我们可以开工了。先是用GetProcAddress函数获得进程中ws2_32.dll模块的recv函数的地址。刚才说过,我们想把recv函数起始位置加入一条跳转指令,让它先跳转到我们的函数中运行。跳转指令可以用0xE9来表示,后面还有4个字节的我们函数的相对地址。也就是我们要修改recv函数前5个字节。其中 相对地址 = 我们函数的地址 - 原API的地址 - 5(我们这条指令的长度)。好了,先让我们读取稍后要被覆盖的recv函数入口处的5个字节的内容,把它保存起来留着以后恢复时使用。
   然后向recv函数入口地址的5字节改写为跳转指令(在这里我们使用CALL指令)。这样每当recv被进程中的代码调用,那么都会先运行在入口处的执行跳转指令而跳转到我们的函数。在我们的函数中,你可以“为所欲为”了,但是在我们的函数里应该能够使用recv函数呀,所以在我们自己的函数中要调用recv函数之前先要恢复recv函数入口处的5个字节,然后调用它,调用完成后还要把它改写成跳转指令。慢着,你一定发现问题了吧?当我们为了调用原来的recv函数而刚刚把recv入口处的5个字节恢复,这时系统中的其他线程调用了recv函数,而这个调用将会成为漏网之鱼而不会进入到我们的函数中来。简单的解决办法是使用临界对象CriticalSection来保证同时只能有一个线程对recv函数入口处5个字节进行读写操作。最后记得在你想要停止拦截的时候恢复所有你修改过的进程和这些进程中被修改的API的前5个字节。其实原理讲着容易,在实现的时候会遇到各种各样的问题,如98下这些系统的DLL被加载到系统内存区供应用程序共享,所以这些内存是受保护的,不能随意修改,还有nt/2000下权限问题,还要考虑到不要拦截某些系统进程,否则会带来灾难性的后果。这些都是在实践当中遇到的实际问题。
   下面结合代码给大家讲解一下吧,首先我们要实现HOOK模块,我们给它起个名字叫做MainHookDll.DLL。在此模块中,主要要实现一个CHookApi的类,这个类完成主要的拦截功能,也是整个项目的技术核心和难点,后面将具体介绍它。而且,MainHookDll模块就是将来要注入到系统其它进程的模块,而远程调用函数是非常困难的事情,所以我们设计此模块的时候应让其被加载后自动执行拦截的初始化等工作。这样,我们只需要让远程的进程加载HOOK,然后MainHookDll.dll就能够自动执行其它操作从而HOOK该进程的相关API。
   MainHookDll模块中的CHookApi类拥有2个向外部提供的主要的方法,HookAllAPI,表示拦截指定进程中的指定API和UnhookAllAPI,表示取消拦截指定进程中的指定API。进行具体设计的时候,会遇到一个问题。大家看到,上文所说的开始将原始API的前5个字节写成CALL XXXX,而在我们的替换函数中要恢复保存的API原始的5个字节,在调用完成后又要把API前5个字节改为CALL XXXX。如果我们拦截多个API要在每个替换函数中按照如上的方法进行设置,这样虽然我们自己明白,但是可能您只是实现HOOKAPI部分,而别人实现调用,这样会使代码看起来很难维护,在别人写的替换函数中加上这些莫名奇妙的语句看来不是一个好主意,而且为了实现防火墙的功能,我们将需要拦截多个感兴趣的API函数,那样的话将会在每一个要拦截的函数里都有这些莫名其妙的代码将会是件很恶心得事情。而且对于CALL XXXX中的地址,要对于不同的API设置不同的替换函数地址。那么能不能把这些所有的函数归纳为一个函数,所有的API函数前5字节都改为CALL到这个函数的地址,这个函数先恢复API的前5字节,然后调用用户真正的替换函数,然后再设置API函数的前5字节,这样可以使真正的替换函数只做自己应该做的事情,而跟HOOK API相关的操作都由我们的通用函数来干。
   这样的想法是好的,但是有一个突出问题,因为替换函数的函数声明与原API一致,所以对于要拦截的不同的API,它们的的参数和返回值是不一样的。那我们怎样通过一个函数获得用户传递给API的参数,然后使用这些参数调用替换函数,最后把替换函数的返回值再返回给调用API的客户?要想实现这个功能,我们需要了解一个知识,也就是C++究竟是怎样调用一个函数的。我们以ws2_32.dll中提供的recv函数为例进行说明,recv函数的声明如下:
   int recv(SOCKET s,char* buf,int len,int flags);
   可以看出它具有4个参数,返回值类型是int。我们作如下调用:
   recv(s,buf,buflen,0);
   那么在调用recv前,这四个参数将按照从右向左的顺序压到栈中,然后用Call指令跳转到recv函数的地址继续执行。recv可以从栈中取出参数并执行其他功能,最后返回时返回值将被保存在寄存器EAX中。最后还要说明一点的是,在汇编语言看来这些参数和返回值都是以DWORD类型表示的,所以如果是大于4字节的值,就用这4个字节表示值所在的地址。
   有了这些知识我们就可以想到,如果用户调用recv函数并被拦截跳转到我们的函数中运行,但是我们并不知道有多少个参数和返回值,那么我们可以从栈中取出参数,但是参数的个数需要提供,当然我们可以在前面为每个API函数指定相应的参数个数,然后运行真正的替换函数,最后在返回前把替换函数的返回值放到寄存器EAX中,这样就解决了不知道参数和返回值个数的问题。那么我们的函数应该是看起来无参数无返回值的。
   基本原理我们大家都清楚了,但是继续之前我还是想讲一讲几个汇编的知识,如果没有这些知识那么看下面的代码就好像天书一样。
   1 关于参数
     我们讲过,在调用一个子函数前要把参数按顺序压栈,而子函数会从栈中取出参数。对于栈操作,我们一般使用EBP和ESP寄存器,而ESP是堆栈指针寄存器,所以多数情况下使用EBP寄存器对堆栈进行暂时操作。还是用调用recv函数为例,假设调用前ESP指向0x00000100处(程序运行时ESP是不可能为这个值的,此处只是为了举例说明问题)。先将参数一次压栈
   push 0          // flags入栈
   lea eax, [len]
   push eax        // len入栈
   lea eax, [buf]
   push eax        // buf入栈
   lea eax,
   push eax        // s入栈
   下面使用call调用真正的recv函数,
   call dword ptr [recv] // 调用recv
   call指令先将返回地址压入栈中,返回地址就是CALL指令的下一条指令的地址,然后跳转到recv入口地址处继续执行。进入recv后,recv使用EBP临时访问堆栈之前,要保存EBP的当前内容,以便以后再使用(在 关于调用函数时保存各个寄存器的值 部分将详细讨论)。所以位于recv函数开始可能是这样的
   push ebp    // 保存ebp的当前值
   mov ebp,esp // 使把esp负给ebp
堆栈指针    堆栈的内容    堆栈内容的含义
[ESP]
0x00000100    flags         参数
0x000000fc    len           参数
0x000000f8    buf           参数
0x000000f4    s             参数
0x000000f0    RetAddress    返回的地址
0x00000ec     OldEBP        保存EBP的当前值
   到此,我们可以知道,如果现在要想通过EBP获得最后一个入栈的参数,那么需要用EBP+8来获得,因为最后一个入栈的参数被保存在返回地址和EBP原始值的上面(一定记住,栈是由高地址到低地址的)。而返回地址被放在EBP+4处,EBP的原始值放在EBP+0处。
2 关于调用函数时保存各个寄存器的值
   当我们要调用其它函数的时候,程序应该先保存各个寄存器的值,然后转去调用其它函数,最后会恢复各个寄存器的值使它们恢复成调用其它函数之前的状态。当然我们使用高级语言写程序的时候,编译器为我们做了这些事情。使用vc调试程序,打开反汇编窗口。运行一个简单的程序,该程序调用一个我们自己写的简单函数,在这个简单函数中设置断点,可以看到,编译器生成的汇编代码使用堆栈保存各个寄存器的值,上面提到当执行一个函数的时候,首先保存的是EBP的值,然后依次压入栈中保存的寄存器为EBX、ESI、EDI,我们在恢复这些寄存器的值的时候将逆向出栈来完成。
   3 关于函数调用的返回
   调用子函数前ESP指针会因为压栈参数而改变,然后压入返回地址等,子函数中会使用ret指令从栈中取出返回地址并跳转到返回地址,而在子函数返回到CALL的下一条指令时栈中还保存着参数,所以我们需要手工的将栈中的参数所占用的空间释放,如在调用完成一个4个参数的子函数后,我们应该将ESP指针上移4*4个字节,如
   add esp,16
   这个操作在调用API的时候是不需要的,因为,windows API在函数中自己将参数弹出堆栈了。

TOP

返回列表 回复 发帖