描述
Windows VPN中的PPTP协议基于通过套接字实现,Windows 内核中的套接字 – Winsock 内核 (WSK):https://docs.microsoft.com/en-us/windows-hardware/drivers/network/winsock-kernel-overview。主要以API的形式提供服务,WSK API 通常的使用方式是通过一组事件驱动的回调函数。一旦建立了套接字,应用程序就可以提供一个调度表,其中包含一组要为套接字相关事件调用的函数指针。为了使应用程序能够通过这些回调维护自己的状态,驱动程序还为每个回调提供了上下文结构,以便可以在连接的整个生命周期中跟踪连接的状态。
pptp协议整个过程分为控制通道和数据通道,控制通道的创建分为套接字、控制连接、呼叫连接几个阶段。
套接字生命周期
主要通过WskOpenSocket函数处理客户端套接字。传入如下几个参数客户端上下文、客户端调度表等等。
主要有这么几个回调函数。
对于接收到的客户端,使用WskConnAcceptEvent函数进行处理。
该函数先创建一个240大小,标记为“wana”的标记内存池。
之后把新的客户端套接字写入到这个内存。a1+8是控制连接上下文结构。
通过DBG看一下分别是什么。首先写入十进制1129010007,16进制是434b5357,也就是字符串"WSKC"
接着偏移2个QWORD处的指向函数raspptp!CtlReceiveCallback
偏移3处指向raspptp!CtlDisconnectCallback
偏移4处指向raspptp!CtlConnectQueryCallback
偏移6处指向raspptp!CtlLogWskLibActivity:
偏移8处处指向raspptp!CtlpSendMessageComplete
最后是sock释放函数指向FreeSockHandle和引用计数初始化为1。
把初始化后的客户端套接字指向当前堆栈结构体中的上下文字段,供整个过程使用。
释放
主要通过WskConnDisconnectEvent函数进行释放。
客户端套接字根据引用计数,决定继续使用或者释放。
每次使用上下文结构体时引用计数会递增,如下:
_InterlockedIncrement((volatile signed __int32 *)(a1 + 512));
通过调试可以看到当前被引用了3次
使用结束会对引用计数递减,当引用次数为0时,说明没有引用了,进行释放。
if ( !_InterlockedDecrement((volatile signed __int32 *)(a1 + 512)) )
(*(void (__fastcall **)(__int64))(a1 + 520))(a1);
控制连接生命周期
相关回调函数
控制连接对象在套接字对象中包含,主要包含以下几个回调函数
CtlConnectQueryCallback:控制连接查询,新的控制连接由这个回调函数处理
CtlDisconnectCallback:控制断开,结束掉当前控制连接。
CtlReceiveCallback:控制接收,处理控制接收的数据。
CtlpSendMessageComplete:控制发送消息完成,负责控制连接发送消息。
结构体定义
控制连接上下文主要通过CtlAlloc函数定义。
函数开始先分配0x290的大小,标记为"PTPT"
接下来进行结构体构造,看看都分配了什么。
偏移0h:存放自身指针地址
偏移8h:存放自身指针地址
偏移20h处存放字符PTPT
偏移28h处存放NDIS句柄
接着创建一个NDIS定时器,把构造好的Ctl结构体指向NDIS定时器上下文函数字段,同时配置NDIS定时器头设置为(NDIS_OBJECT_HEADER)0x180197,是一个NDIS对象标准头。标签设置为"PTMT",定时器函数为(PNDIS_TIMER_FUNCTION)CtlpEchoTimeout。
偏移358h处存放创建后的NDIS定时器指针。
新控制连接处理
新建控制连接由CtlConnectQueryCallback处理。
参数a1,未知
参数a2,未知
参数a3,猜测应该是一个wsk类型的套接字结构体对象,因为数据结构一样。
该函数先通过CtlAlloc创建一个控制连接对象,创建的对象并不完整,该函数会进一步赋值。
先设置状态为3,在对象的偏移30h处。
偏移48h处设置为1。
偏移58h处指向自旋锁
偏移80h处指向套接字结构体对象
偏移60h处存放IRQL,自旋锁释放用到。
偏移248h处指向NDIS类型的计时器句柄,设置时间为30秒
偏移238h处指向Echo计时器,如果有设置,按照设置的进行。
释放
跟套接字类似,也是使用引用计数的方式,调用的时候引用计数递增,调用结束引用计数递减。处理逻辑主要在函数CtlCleanup中。
递增。这里的a1 + 104,就是前边的控制连接结构体偏移48。因为这里的控制连接结构体是从a1偏移20h处开始的。
_InterlockedIncrement((volatile signed __int32 *)(a1 + 16));
*(_BYTE *)(a1 + 104) = 1;
递减,如果递减后等于0,调用自身指向的释放函数CtlFree
进行释放。
*(_BYTE *)(a1 + 104) = 0;
if ( !_InterlockedDecrement((volatile signed __int32 *)(a1 + 16)) )
(*(void (__fastcall **)(__int64))(a1 + 24))(a1);
呼叫连接(CallAlloc)
Call结构体定义
首先定义该结构体对象占内存大小为0x2B8ui,标志为"PTPC"
偏移30h处写入标志“PTPC”
偏移38h处指向一个NDIS_HANDLE类型的对象
偏移c2h处设置为0
偏移248h处设置为0
偏移60h、88h、a8h处分别是自旋锁指针,并进行初始化。
偏移1E0h处指向由NdisAllocateNetBufferList函数返回的网络缓冲列表。
偏移258h处指向由NdisAllocateIoWorkItem函数获取的IO工作项句柄。
如果获取的Io工作项为真,就再获取一次。
偏移260h处指向第二次获取的Io工作项
如果第二次获取的Io工作项仍然为真。
偏移78h、80h、A0h、98h处都设置为0。
然后设置NDIS定时器对象TimerCharacteristics属性。函数上下文指向初始化的Call结构体对象。
头指向NDIS_OBJECT_HEADER结构体,值为16进制180197
分配标签为"PTMT",跟控制连接的一样。
定时器回调函数指向CallpCloseTimeout。
使用NDIS库中的NdisAllocateTimerObject函数进行定时器分配,
偏移1a0h处指向分配后的定时器对象
接着分配Call Ack
定时器
偏移1c0h处指向Call Ack
定时器。
偏移200h处指向CallpDialTimeout计时器
释放
Call对象使用完之后,一般会调用CallCleanup
进行释放。同样的,也是使用引用计数的方式进行调用和释放。不同的是,在初始化Call对象时,如果出错,会直接调用 CallFree函数进行释放。
递减,如果等于1,调用偏移40处的函数进行释放。