开发背景
ssh -D 1080 user@192.168.1.1
如果常用SSH端口转发的技术人员应该很熟悉-D或-L参数的功能了,-D参数可以让ssh客户端本地监听的TCP端口是一个socks5代理服务,然后把接入的流量基于ssh的TCP通道从远程连接出去。-L参数就是纯端口转发,不支持代理协议。-D参数应该更常用。
由于Windows服务器的主要远程管理方式是3389端口的RDP协议,但RDP协议相比Linux系统的SSH协议却没有自带这个功能,然而,RDP协议通道预留了开发接口,可以让开发者在RDP的会话通道中嵌入自己的传输频道。
接下来我将分享如何开发插件实现Socks Over RDP隧道,我在开发这个之前有了解到一款叫SocksOverRDP的开源工具,经过试用,个人觉得易用性稳定性和兼容性都不如我意,本文分享的SooRDP这个插件可以更方便的为Windows的远程桌面客户端增加类似SSH的-D或-L参数的功能。项目源码github链接见底部。
SooRDP功能介绍
基本操作
下面是工具的文件列表,例如本地操作系统64位的就运行SooRDPClientApp64.exe,
然后软件界面如下,同类工具没提供这么方便自动注册和配置参数的界面呢。先配置端口和转发模式,还可以配置mstsc的参数,例如浏览个rdp文件,或者直接点打开启动远程桌面客户端,工具会先自动注册SooRDP-Plugin64.dll,不需要你去执行regsvr32,然后点打开按钮启动远程桌面客户端。
接下来把SooRDPServerApp复制粘贴到远程后运行,界面如下图,并点“启动”按钮。这时隧道就准备就绪了。这时远程桌面要保留住不能断开,最小化它就可以使用这个RDP通道了。
功能特点
- SooRDP为了方便用户使用,提供了界面操作配置RDP隧道参数,从客户端程序打开远程桌面时会自动注册DLL插件,关闭退出时自动卸载插件的注册表注册的信息。
- SooRDP支持Windows 2003至今,在全网似乎找不到能支持Server 2008之前的此类工具,SooRDP会根据系统自适应Static或Dynamic Virtual Channel接口,因为Server 2008之前远程桌面只有Static接口(用该接口针对实现端口转发功能难度较大)。
RDP的虚拟通道API接口编程
下面代码是我从MSDN修改而来,这个在远程模块的代码,我们可以通过API打开一个VirtualChannel的文件句柄,然后就可以像文件一样读写数据流和客户端插件通信了。
DWORD OpenVirtualChannel(
BOOL bDynamic,
LPCSTR szChannelName,
HANDLE* phWTSHandle,
HANDLE* phFile)
{
HANDLE hWTSHandle = NULL;
HANDLE hWTSFileHandle;
PVOID vcFileHandlePtr = NULL;
DWORD len;
DWORD rc = ERROR_SUCCESS;
BOOL fSucc;
if (bDynamic) {
if (gpWTSVirtualChannelOpenEx == NULL)
{
rc = ERROR_PROC_NOT_FOUND;
goto exitpt;
}
hWTSHandle = gpWTSVirtualChannelOpenEx(
WTS_CURRENT_SESSION,
(LPSTR)szChannelName,
WTS_CHANNEL_OPTION_DYNAMIC | WTS_CHANNEL_OPTION_DYNAMIC_PRI_HIGH);
}
else {
hWTSHandle = WTSVirtualChannelOpen(
WTS_CURRENT_SERVER_HANDLE,
WTS_CURRENT_SESSION,
(LPSTR)szChannelName);
}
if (NULL == hWTSHandle)
{
rc = GetLastError();
goto exitpt;
}
fSucc = WTSVirtualChannelQuery(
hWTSHandle,
WTSVirtualFileHandle,
&vcFileHandlePtr,
&len);
if (!fSucc)
{
rc = GetLastError();
goto exitpt;
}
if (len != sizeof(HANDLE))
{
rc = ERROR_INVALID_PARAMETER;
goto exitpt;
}
hWTSFileHandle = *(HANDLE*)vcFileHandlePtr;
fSucc = DuplicateHandle(
GetCurrentProcess(),
hWTSFileHandle,
GetCurrentProcess(),
phFile,
0,
FALSE,
DUPLICATE_SAME_ACCESS);
if (!fSucc)
{
rc = GetLastError();
goto exitpt;
}
rc = ERROR_SUCCESS;
if (phWTSHandle) {
*phWTSHandle = hWTSHandle;
hWTSHandle = NULL;
}
exitpt:
if (vcFileHandlePtr)
{
WTSFreeMemory(vcFileHandlePtr);
}
if (hWTSHandle)
{
WTSVirtualChannelClose(hWTSHandle);
}
return rc;
}
目前有两个版本的API接口,从Vista开始有WTSVirtualChannelOpenEx,早期是WTSVirtualChannelOpen。他们的主要区别是:
- 新版的支持动态,支持按需创建多个Channel,也就是RDP会话中,可以有多个子Channel,每个Channel就像TCP一样,插件可以感知每个Channel的建立和断开的状态。这种很方便就能实现把socks5的每个进来的TCP和Channel关联起来,实现流量转发。哪个TCP断了就把对应Channel关闭,远程就能自动感知Channel关闭。
- 旧版的只有静态模式,也就是只有一个Channel。静态Channel的建立和断开是随着RDP会话的建立和断开同步的。这种要实现socks5通道,相对比较麻烦,需要自己把所有socks5的连接封装到一个Channel。
Socks5封装编程
为了能同时兼容新版和旧版的Channel,我选择只用一个Channel,并把多个socks5的连接封装到其中,为此我实现了一个叫SoTunnel的协议,它是基于异步socket实现的,可以参见源码中的SoTunnel.h,它支持在一个数据流中,封装多个子数据流,为了能管理好每个子数据流,我简单实现了几个协议指令。
#define stb_cmd_newconn 0 //建立新的子通道
#define stb_cmd_data 1 //子通道的数据
#define stb_cmd_close 2 //关闭一个子通道
#define stb_cmd_config 3 //发送配置信息,是否启动socks5协议
#define stb_cmd_reset 4 //重置通道
#define stb_cmd_ready 5 //通道状态
#define stb_cmd_ruready 6 //询问通道状态
#define stb_cmd_ping 7 //测试子通道
#define stb_cmd_pong 8 //测试子通道
接着我还需要在SoTunnel的对端子数据流的代码中实现socks5代理服务器的代码,详见SoTunnel.cpp。值得一提,为了解决域名解析阻塞问题,异步socket我不用CAsyncSocket,而是重写了一个类似的,域名解析时创建线程来解析。
void CEndPointSocket::OnSocksTalk()
{
int ret;
if (s5_stage == 0) {
if (!s5data_read(2)) {
return;
}
if (s5buf[0] != 0x05) {
Close();
return;
}
s5_nmethod = s5buf[1];
s5buf.erase(0, 2);
s5_stage++;
}
if (s5_stage == 1) {
if (!s5data_read(s5_nmethod))
return;
s5buf.erase(0, s5_nmethod);
s5_stage++;
}
if (s5_stage == 2) {
if (!channel_write("\x05\x00", 2)) {
return;
}
s5_stage++;
}
if (s5_stage == 3) {
if (!s5data_read(4)) {
return;
}
unsigned char ver = s5buf[0];
unsigned char cmd = s5buf[1];
unsigned char rsv = s5buf[2];
unsigned char address_type = s5buf[3];
if (cmd != 1) { // tcp connect
Close();
return;
}
s5_stage++;
}
省略
由于我们能到的VirtualChannel是一个文件句柄,而SoTunnel是基于socket实现的,那么还需要实现将文件句柄的输入输出和SoTunnel的socket进行转发就可以了。总体实现原理大概就是如此,其他代码实现细节就不一一列举了。最后可以吐槽下,经过测试RDP通道的传输速度最快约12M/s,我开始怀疑是SoTunnel的实现问题,经过在复制粘贴文件测试,显示拷贝文件传输速度也是差不多速度。我推测是RDP内部为了图像数据优先级,其他VirtualChannel的数据优先级较低的原因。