✎引子:
早些时候想去研究Windows Filter Platform (WFP),参考资料少且不齐全。贴吧、论坛搜集一些关于网络过滤、网络监听的工具。开始琢磨别人是怎样写,怎样实现的。然而没有去研究驱动层(不少原理性的东西须要时间),本身写用户层前一直琢磨,三环如何去实现这些网络监听?用什么API能够实现对数据包的捕获呢?怎样把这些数据进行处理?
当我看到其中的项目的时候,单纯的.exe文件,运行后也没有释放dll之类的动态资源,脑海中出现一个念头shellCode(这里就先叫shellCode了,其实准确说是机器码)。这个程序是好多年前的,比较单一,注入任意进程,捕获网络响应数据,兼容性也还不错,用360浏览器作测试,windows7~windwos10网络响应捕获正常。
这是给你们提供一些逆向的思路,并非教程系列,有必定逆向基础才能够(对汇编、网络编程、OD等工具了解)。当遇到相似的程序或者问题,对他们的实现原理作到心中有度。ios
✎逆向分析目录:shell
一、注入代码分析 | 二、shellCode调试方法 | 三、shellCode动态分析 |
---|---|---|
--------★ ★ ★------- | -----------------★ ★ ★------------------- | -----------★ ★ ★ ★----------- |
☂草稿示意图:
图片二:程序流程草图编程
1、☛注入代码分析:
➊ 用IDA先简略的浏览一下汇编指令,发现反汇编代码不算多,了解了基本的程序结构,拖到OD开始动态调试。
➋ 如图二中第一步所示,获取被注入的数据,须要获取选中的目标进程Id等,而且OpenProcess打开目标进程,获取句柄才能够完成注入,以下图所示(图中关键代码已给出解释):
图片三:获取目标进程信息及获取句柄
➌ 目标进程申请虚拟内存,以下图所示:
图片四:申请虚拟内存空间
➍ 目标进程虚拟内存申请以后,写入shellCode,且5次写入目标程序申请的虚拟内存空间,这个地方咱们无需关系写入shellCode的内容及做用,后面会详细介绍,咱们只须要经过反汇编简单看一下便可。
图片五:第一次写入shellCode
图片六:第二次写入shellCode
图片七:第三次写入shellCode
图片八:第四次写入shellCode
在第四次写入以后,又作了一些事情,如建立了事件(保证如下操做在多线程环境下安全),建立了一个全局句柄,以下图所示:
图片九:事件及新句柄建立
✍注意:第五次写入的是函数地址,图片中的注释是第一次分析时候注释,并非IAT,也不是修复重定位,只是为了方便shellCode调用而写入的地址,在目标程序中shellCode会用到的函数地址,做为一个格外的附加项写入到了目标程序,以下图所示:windows
图片十:第五次写入shellCode
➎ 建立远程线程及且把第五次写入的shellCode做为参数执行:
图片十一:建立远程线程
以上就是整个目标程序注入的过程,发现并不复杂,这时候又要考虑,注入到目标进程shellCode,如何去分析这些代码呢?浏览器
2、☛shellCode调试方法:
第一次用的是dump,dump下来的是丢失的、不是完整的代码,思路很阻塞...... 后来找朋友请教了一些问题,思考后大致有如下两种办法供参考:
一、手动构建pe文件,修改shellCode或者写入到目标进程中shellCode,在虚拟内存空间二进制复制出来。二进制复制的代码拖入IDA中,咱们须要手动去找些函数名称(根据第五次写入的函数),这样虽然能达到静态分析的过程,可是相对比较麻烦。下面是在010中打开的复制的shellCode,咱们能够看到与第5次写入的函数彻底一致,以下图所示:
图片十二:010中查看数据
二、双进程动态调试,在目标程序中分析观察(动态)。简单点来讲,被注入的进程是你可以附加并且能够调试的程序(有网络响应)。就能动态的观察虚拟内存的申请、写入的过程。能下内存访问断点,可以动态的调试,并且是真实的应用环境下进行的,更为精准。
第三部分的内容将采用这种方式进行解析,分析代码都干了什么事情?是怎样捕获这些网络数据?下面咱们一块儿来看。
3、☛shellCode动态分析:
一、双进程调试,注入程序与被注入程序。当注入程序(也就是图一软件),在目标进程中建立虚拟内存空间后,EAX会返回建立成功的地址,咱们要到目标进程中找到地址,注意是目标进程中!
二、通常会遇到这种问题:在目标进程中Ctrl+G查找地址的时候会找不到注入程序申请的虚拟内存?明明申请都成功了为什么还找不到?不慌!,咱们在OD中Alt+M,而后拉到最下面(通常都在最下面),就会发现申请的虚拟内存空间。
三、当注入的程序调用WriteProcessMemory,5次写入代码的时候,咱们就能够在目标程序的数据窗口跟随,动态的观察写入的数据,直到5次写入完成。
四、在建立远程线程以前,这时候目标程序中的虚拟内存应该是有数据的,由于写入已经完成。不要反汇编而后在申请的虚拟内存中F2,好像也没办法F2下断点。保险起见直接下内存访问断点便可,而后注入程序建立远程线程成功,咱们就可让目标程序跑起来,直接会在申请的虚拟内存中断下来,剩下的就好办了。安全
✎咱们开始动态调试shellCode,这段代码先干了些什么?以下图所示:
图片十三:获取send、recv函数地址
这段代码先来了个获取send、recv的函数地址,居然这样咱们科普一下这两个函数,为了让你们更容易理解,下面写了一段简单的网络编程,来看以如何进行网络通信。
先来看函数原型,send与recv两个函数,分别是发送与响应,函数原型以下:服务器
int WSAAPI recv( _In_ SOCKET s, _Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf, _In_ int len, _In_ int flags ); int WSAAPI send( _In_ SOCKET s, _In_reads_bytes_(len) const char FAR * buf, _In_ int len, _In_ int flags ); 参数基本相同,第二个参数是指向char* 类型的缓冲区,这第三个参数是缓冲区大小,这两个很关键。
服务器端:网络
#include "pch.h" #include <WinSock2.h> #include <iostream> #pragma comment(lib, "WS2_32.lib") using namespace std; /* Socket网络编程服务器端 */ // 用于接受客户端发来的消息 强转后查看是否数据一致(精准) typedef struct _Message { int Code; char Number; }Message, *pMessage; int main() { cout << "服务端:" << endl; WSADATA str_Data = { 0, }; int SockAddSize = sizeof(sockaddr_in); int nResult = 0; // 1. 初始化 nResult = WSAStartup(MAKEWORD(2, 2), &str_Data); if (nResult == SOCKET_ERROR) { cout << "WSAStartup() ErrorCode = " << GetLastError() << endl; system("pause"); return -1; } // 2. 建立套接字 SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 3. 初始化Ip及端口信息 sockaddr_in str_Addrs = { 0, }; str_Addrs.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); str_Addrs.sin_family = AF_INET; str_Addrs.sin_port = htons(8888); // 4. 绑定socket nResult = bind(sock, (sockaddr*)&str_Addrs, SockAddSize); if (SOCKET_ERROR == nResult) { closesocket(sock); WSACleanup(); cout << "bind() failuer ErrorCode = " << GetLastError() << endl; return -1; } // 5. 监听(失败概率与种500w有一拼,因此不作判断) try { listen(sock, SOMAXCONN); } catch (const std::exception&) { return -1; } sockaddr_in str_Client = { 0, }; // 6. 链接响应(若是不设置异步 会阻塞等待 tcp),知道有客户端去链接 SOCKET ClientSock = accept(sock, (sockaddr *)&str_Client, &SockAddSize); if (ClientSock == INVALID_SOCKET) { closesocket(sock); WSACleanup(); cout << "bind() failuer ErrorCode = " << GetLastError() << endl; } char nBuf[] = "消息已收到!"; int BufSize = sizeof(nBuf); Message str_Msg = {0,}; // 7. 等待链接(这是一个死循环) // 若是有客户端链接成功,发送一条消息看是否成功 if (SOCKET_ERROR == recv(ClientSock, (char*)&str_Msg, sizeof(Message), 0)) cout << "recvError Code = " << GetLastError() << endl; cout << "客户端发来消息: Code = " << str_Msg.Code << endl; cout << "客户端发来消息: Code = " << str_Msg.Number << endl; // 回复客户端一条消息 send(ClientSock, nBuf, BufSize, 0); system("pause"); return 0; }
客户端:多线程
#include "pch.h" #include <iostream> #include <WinSock2.h> #pragma comment (lib, "WS2_32.lib") using namespace std; /* Socket客户端 */ // 使用结构体 更直观表示经过send能够传送大量的数据 typedef struct _Message { int Code; char Number; }Message, *pMessage; int main() { cout << "客户端:" << endl; WSADATA str_Data = { 0, }; int nRet = 0; // 1. 初始化 nRet = WSAStartup(MAKEWORD(2, 2), &str_Data); if (SOCKET_ERROR == nRet) { cout << "WSAStartup() ErrorCode = " << GetLastError() << endl; return -1; } // 2. Socket初始化 SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in str_sockAdd = { 0, }; str_sockAdd.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); str_sockAdd.sin_family = AF_INET; str_sockAdd.sin_port = htons(8888); int socketSize = sizeof(sockaddr_in); nRet = connect(sock, (sockaddr *)&str_sockAdd, socketSize); if (SOCKET_ERROR == nRet) { cout << "connect failuer ErrorCode = " << GetLastError() << endl; closesocket(sock); WSACleanup(); return -1; } Message str_Msg = { 0, }; str_Msg.Code = 1; str_Msg.Number = 'a'; // 成功消息到服务器端 send(sock, (char*)&str_Msg, sizeof(Message), 0); char nBuf[20] = {0,}; // 响应服务器发来的消息 recv(sock, nBuf, sizeof(nBuf), 0); cout << "服务器端发来消息:" << nBuf << endl; system("pause"); return 0; }
若是对网络编程不熟悉,请把上面代码学习一下,由于下面是对这两个函数的inlinehook,因此掌握函数使用与实现很重要。
若是对hook不熟悉,请看之前写的博客https://blog.51cto.com/13352079/2342776异步
上面咱们分析了shellCode第一段代码,获取了recv与send函数,下面接着上图:
图片十三:读取函数前5个字节
图片十四:inlinehook的offset计算
图片十五:替换原函数前5个字节
简单的打个比方:
先读取原函数send的前5个字节,而后计算偏移: 中转地址 - 原函数地址 - 5。为何-5?如图十五所示,原函数前5个字节hook后变为jmp,运行后被响应而后跳转,若是你不-5,那不是又到了jmp,应该jmp执行以后,该执行jmp下一条指令,因此-5。
若是还不太清楚,咱们来作一个对比 hook前与hook后发生了哪些变化,以下图所示:
图片十六:hook以前的函数
图片十七:hook以后的函数
因此破坏了原函数前5个字节,一开始先读取是为了保存前5个字节的内容,执行JMP之后,跳转到JMP下一条指令以前(SUB ESP,0X20以前)仍是会执行保存的5个字节机器码,在跳转到SUB ESP,0X20继续执行原函数。
inlinehook的recv函数干些什么事??
图片十八:hook recv执行过程
✎注意!如上图所示,上述图片中缺乏少一个步骤,上面图关联到一块儿只是为了让你们好理解,可是缺乏了执行原函数栈顶的操做,其实CALL DWORD PTR DS:[ESI + 0XA90C]是跳转到本身的shellCode中,而后执行原函数的前5个字节,如图十二所示,到底CALL的是什么内容?以下图所示:
图片十九:执行原函数栈顶
如上图所示,CALL过来以后,执行机器指令8BFF558BEC(原函数的前5个字节),后面则是JMP ws2_32.74BF5FF5,其实就是 :中转地址 - (send或者recv函数地址) - 5,上面介绍计算的偏移的做用就体现出来了,正好跳转到原函数的JMP下一条指令。
inlinehook的send函数干些什么事??
图片二十:截获send函数
图片二十一:hook send执行流程1
根据截获跳转到BaseAddress + 0x400的地方,Getpc获取了当前的地址,注意GetPC这种方式,如E8 00000000是敏感操做,有时候这样使用当前地址如下的汇编指令将被截断,继续看:
图片二十二:hook send执行流程2
利用CreateFile在\.\Pipe\下面带开了文件句柄(图三中的文件路径),格式化输出的是什么?
图片二十三:wvsprintfA函数
格式化输出,咱们看到了一些关键的数据,如上图中PID,TID等等,为了传送给网络监控工具显示数据而准备。
图片二十四:截获的send消息写入文件句柄
实际上是ASCII截获的数据则是第二个参数,也就是缓冲区中的内容加上PID一些附加信息数据,写入大小是第三个参数加上PID等附加大小。注入程序去读取文件句柄内容,把捕获的消息数据经过ListView控件(MFC)显示到界面中。
简单来讲,就干了这么一件事,利用inlinehook技术,hook send 和 recv两个函数,截获第二个参数中的缓冲区,显示到三环。因此使用windows SDK网络编程,或者说使用这两个函数,你发送与响应的消息会被截获。 到此你应该知道软件实现原理及过程,能够本身写一个更适用的网络软件,还能够作过滤,对一些敏感的数据操做,从而实现三环的网络监控功能、过滤功能等。