前言
在逆向学习的过程中,网络方面也是必不可少的,所以我展开了对 Windows 网络的研究。
既然是 Windows 平台,那么那些软件肯定是需要调用 Windows API 来完成网络请求的,所以我的思路也是很简单:
我想尽可能实现通杀,那么我就在 API 的入口等待你们的请求。
当然,我的这一套方案肯定是可以绕过去的,因为要是想要彻底拦截,还是要在驱动层蹲守。不过那就太费劲了,我们这里只考虑三环的情况,尽可能把它做得更好。
既然已经想好了在 API 等待网络请求,那就开始着手研究一共会经过哪些 DLL。
我找到了三个: Ws2_32.dll WinHttp.dll WinINet.dll
俗话说得好: 知己知彼,方能百战百胜。
那就需要先了解一下这三个 DLL 是如何工作的。
DLL 原理解析
观察这三个 DLL 的依赖关系,可以发现它们最终都会依赖到Ws2_32.dll:
WinINet.dll
└── WinHttp.dll
└── Ws2_32.dll
WinHttp.dll
└── Ws2_32.dll
Ws2_32.dll
既然最终都会回到 Ws2_32.dll,那么我们的工作量也就减少了很多。
我们再研究一下关键函数究竟是如何运行的,最终定位到 Ws2_32.dll,不就可以完美收场了吗?
一开始我也是这么想的。
WinINet
创建会话句柄
WinINet!InternetOpen
↓
打开目标 URL
WinINet!InternetOpenUrl
↓
读取服务器返回数据
WinINet!InternetReadFile
WinHTTP
连接服务器
WinHttp!WinHttpOpen
↓
创建 HTTP 请求
WinHttp!WinHttpConnect
↓
发送请求
WinHttp!WinHttpOpenRequest
↓
创建会话句柄
WinHttp!WinHttpSendRequest
↓
读取服务器返回数据
WinHttp!WinHttpReadData
Winsock
初始化环境
Ws2_32!WSAStartup
↓
查询域名
Ws2_32!getaddrinfo
↓
创建 Socket
Ws2_32!socket
↓
创建连接
Ws2_32!connect
↓
发送请求
Ws2_32!send
↓
读取数据
Ws2_32!recv
根据这个简略版的流程,我信心满满地去 Hook Ws2_32.dll里面的这些关键函数。
结果发现……
剩下两个 DLL 并没有按照我想象中的样子断下来。
这就不得不掏出 IDA 进行静态分析了。
经过漫长的分析和查找判断,最终找到了这两个 DLL 所使用的一条关键路径:
Ws2_32!GetAddrInfoExW
↓
Ws2_32!WSAIoctl
↓
Mswsock!MSAFD_ConnectEx
↓
Ws2_32!WSASend
↓
Ws2_32!WSARecv
这里你会惊奇地发现,混进来了一个陌生的 DLL:Mswsock.dll
这也是卡住我最久的一个地方。
谁能想到 WSAIoctl这里并不是简单地走 WSAConnect,而是通过 Winsock 扩展函数机制获取到了 ConnectEx 的函数指针,最终进入 Mswsock 中的实现。
估计微软也是为了兼容不同的 Winsock 扩展机制,才搞出了这么一套东西。
对于这种函数指针动态解析的过程,一直都是很有意思的过程,跟猫抓老鼠一样有趣。
下面就跟你们分享一下这里的过程。
首先来看一下 WSAIoctl。
这里使用的是:
SIO_GET_EXTENSION_FUNCTION_POINTER
配合:
WSAID_CONNECTEX
来获取 ConnectEx 对应的函数地址。
简单来说,就是:
WSAIoctl
│
├── SIO_GET_EXTENSION_FUNCTION_POINTER
│
├── WSAID_CONNECTEX
│
└── 输出 ConnectEx 函数地址
微软通过 GUID 查表的方式去找到 WSAID_CONNECTEX 对应的扩展函数地址。
这非常的微软。
我缓缓地扣出一个:? 哈哈哈。
在逆向过程中,我找到了一段比较有意思的代码:
48:895C24 18 mov qword ptr ss:[rsp+18],rbx ; <== 暂停条件(RDX == 0xC8000006)
.......................省略了很多行............................
49:8BC2 mov rax,r10
E8 DE520400 call 0x7FFE6B435010 ; <== 动态获取 MSAFD_ConnectEx 的地址
44:8BF0 mov r14d,eax ; <== *([rsp+28]) == MSAFD_ConnectEx 的地址
这里需要特别注意:
RDX == 0xC8000006 是我在调试过程中设置的暂停条件,它并不意味着 0xC8000006 本身就是 ConnectEx 的地址。
真正有意思的是后面的函数调用:
call 0x7FFE6B435010
这个过程最终会根据前面传入的扩展函数标识,找到对应的ConnectEx 实现。
也就是:
WSAID_CONNECTEX
↓
Winsock 扩展函数查找
↓
MSAFD_ConnectEx
↓
得到函数地址
开始实战
既然我们已经清楚了如何在 Ws2_32.dll 去蹲守,那就开始根据已有的结构开始布局吧。
这些函数的结构体你都可以在微软文档中获取,只有 MSAFD_ConnectEx 这个结构可能比较难直接找到。
根据逆向得到的调用形式,它的参数可以对应到:
BOOL MSAFD_ConnectEx(
SOCKET s,
const sockaddr *name,
int namelen,
PVOID lpSendBuffer,
DWORD dwSendDataLength,
LPDWORD lpdwBytesSent,
LPOVERLAPPED lpOverlapped
);
如果我需要获取域名,那就去 Hook:
Ws2_32!GetAddrInfoExW
进入函数后:
GetAddrInfoExW:
40:55 push rbp ; <== RCX 里面装着域名
53 push rbx
56 push rsi
在这里可以根据函数参数去获取对应的域名。
需要注意的是,因为域名解析结果可能会被缓存,所以并不是每次网络连接都会重新进行一次 DNS 查询。
所以如果我们需要更加准确地获取目标 IP,还是需要继续往后跟MSAFDConnectEx
这里可以获取连接目标的地址信息。
MSAFD_ConnectEx:
48 8B C4 mov rax, rsp ; <== RDX 指向 sockaddr
48 89 58 08 mov [rax+8], rbx ; [RDX+2] => Port
48 89 70 10 mov [rax+10h], rsi ; [RDX+4] => IPAddress
48 89 78 18 mov [rax+18h], rdi
通过这里就可以进一步获取连接目标的 IP 和端口。
这样,即使前面的域名解析走了缓存,我们依然可以从实际建立连接的地方获取到目标地址。
继续往后跟:
Ws2_32!WSASend
这里就装着发送的数据。
WSASend:
48:895C24 08 mov qword ptr ss:[rsp+8],rbx ; <== RDX => lpBuffers
48:896C24 10 mov qword ptr ss:[rsp+10],rbp ; [RDX] => WSABUF.len
48:897424 18 mov qword ptr ss:[rsp+18],rsi ; [RDX+8] => WSABUF.buf
WSASend 的关键就在于:
RDX
↓
lpBuffers
↓
WSABUF
├── len
└── buf
↓
实际发送的数据
所以我们就可以在这里拿到发送出去的数据。
如果是 HTTP 请求,那么通常就可以在这里看到类似:
GET / HTTP/1.1
Host: example.com
当然,具体数据还要根据程序使用的协议以及是否进行了加密来判断。
发送完请求之后,自然就是接收服务器返回的数据。
所以继续蹲:
Ws2_32!WSARecv
这里通常会有一个或者多个 WSABUF。
WSARecv:
48:895C24 08 mov qword ptr ss:[rsp+8],rbx ; <== RDX => lpBuffers
48:896C24 10 mov qword ptr ss:[rsp+10],rbp ; [RDX] => WSABUF.len
48:897424 18 mov qword ptr ss:[rsp+18],rsi ; [RDX+8] => WSABUF.buf
.......................省略了很多行............................
41:5F pop r15
41:5E pop r14
5F pop rdi ; <== 这里可以读取 [RDX+8] 指向的缓冲区
C3 ret
这里:
RDX
↓
lpBuffers
↓
WSABUF
├── len
└── buf
↓
服务器返回的数据
所以在合适的位置,我们就可以读取 [RDX+8] 指向的数据。
这里千万不要小瞧这个 buf。
如果数据本身是明文协议,例如 HTTP/1.1,那么你甚至可以在数据返回到上层之前观察甚至修改其中的内容。
例如:
HTTP/1.1 301 Moved Permanently
这类内容在合适的 Hook 点上都可以被观察到。
最后这里可以给你们提供一个简单的 Hook 模板。
func_address:
mov rax, hook_address
jmp rax
hook_address:
**:**** ; 首先还原你覆盖掉的那些字节
**:****
pushad
**:**** ; Hook 汇编内容
**:****
popad
mov rax, func_address+0x12
jmp rax
当然,这只是一个非常简化的模板。
实际使用的时候,还需要根据目标函数的前几条指令、指令长度、寄存器状态以及栈布局进行调整。
对模板进行微调之后,你就可以根据自己的需求获取对应的数据。
成果展示
根据上述对Windows 网络 DLL的了解后我们就可以针对性去编写hook函数,从而获取我们想要的数据包了
这里我没有放出成品软件,还是因为我是针对性调整并没有适配全部Windows系统
注入窗口
注入成功后
成功监听网络数据
总结
对于 Windows 网络来说,肯定还有其他的 API 是我没有覆盖全面的。
不过通过这次的学习,我也算是寻找到了一条比较有意思的网络 API 调用路径。
通过了解这些 API 函数,感觉比生硬地去看网络知识会更加有意思。
特别是当你真正拿到数据之后,再去做解析的过程
你拿到的就是一个指针。
然后你需要对这个指针进行解引用,才能真正看到文档里面描述的结构体以及其中的数据。
还有很多句柄,其实在一些关键的地方还是很有用的,也能帮助我们更好地去理解 Windows 中的句柄操作。
在编写这些函数的时候,你可能会遇到很多宏定义。
当时可能完全不理解,甚至会觉得很疑惑。
但是等到后面在逆向过程中真正找到它、跟进去、理解它之后,才能真正体会到这些宏定义背后的奥妙。
逆向其实就是这样。
很多东西你单独去看,可能觉得很枯燥。
但是当你真正把它串起来,从一个 API 一路跟到另一个 DLL,再跟到真正执行网络操作的位置时,整个过程就会变得非常有意思。
看到这里,相信你也对这三个 DLL 以及 Windows 网络 API 的调用过程有了一个基本的理解。