RDP(3/5):Server

一、编译、运行、调试

1.1 编译

对设置做两个修改,一是确保WITH_SERVER是ON,二是增加CMAKE_WINDOWS_VERSION,值至少WIN8以上。

确保WITH_SERVER是ON。WITH_SERVER决定生成的FreeRDP.sln是否要包含server相关模块,对要编译server的自然要设为ON。有两个方法,一是打开ConfigOptions.cmake,把定义WITH_SERVER初始值的“option”改为“ON”。二是运行CMake(cmake-gui)时,执行过“Configure”后,变量区找到“WITH_SERVER”,打勾。

<FreeRDP>/cmake/ConfigOptions.cmake
option(WITH_SERVER "Build server binaries" OFF)
改为
option(WITH_SERVER "Build server binaries" ON)

增加CMAKE_WINDOWS_VERSION,值至少WIN8以上。运行CMake(cmake-gui),在Configure前单击“Add Entry”。

图1 增加CMAKE_WINDOWS_VERSION
<FreeRDP>/CMakeLists.txt
if(NOT DEFINED CMAKE_WINDOWS_VERSION)
  set(CMAKE_WINDOWS_VERSION "WIN7")
endif()

if(CMAKE_WINDOWS_VERSION STREQUAL "WINXP")
  add_definitions(-DWINVER=0x0501 -D_WIN32_WINNT=0x0501)
elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN7")
  add_definitions(-DWINVER=0x0601 -D_WIN32_WINNT=0x0601)
elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN8")
  add_definitions(-DWINVER=0x0602 -D_WIN32_WINNT=0x0602)
elseif(CMAKE_WINDOWS_VERSION STREQUAL "WIN10")
  add_definitions(-DWINVER=0x0A00 -D_WIN32_WINNT=0x0A00)
endif()

CMAKE_WINDOWS_VERSION决定了预定义宏_WIN32_WINNT是什么值,要是不预先设置CMAKE_WINDOWS_VERSION,值将是0x0601,此时抓屏将使用“Mirage Driver”技术。

<FreeRDP>/server/Windows/wf_interface.h,
#if _WIN32_WINNT >= 0x0602
#define WITH_DXGI_1_2 1
#endif

Win8或以上系统已不再支持“Mirage Driver”,用它抓屏会出异常,必须使用DXGI,它们区别可参考“windows远程桌面实现之一 (抓屏技术总览 MirrorDriver,DXGI,GDI) ”。

1.2 运行

TLS握手阶段,server需向client发送证书、私钥。它们从哪来?FreeRDP自带了这两个文件:server.crt、server.key。

<FreeRDP>/server/Windows/wf_peer.c
static BOOL wf_peer_read_settings(freerdp_peer* client)
{
	client->settings->CertificateFile = _strdup("server.crt");
	...
	client->settings->PrivateKeyFile = _strdup("server.key");
}
改为
client->settings->CertificateFile = _strdup("c:/ddksample/server.crt");
client->settings->PrivateKeyFile = _strdup("c:/ddksample/server.key");

由于代码中用的是相对路径,如果不修改源码,server可能找不到这两个文件,修改方法正如上面示例的用绝对路径代替相对路径。

1.3 调试

编译出wfreerdp-server.exe,能成功运行,但断点无法进入wfreerdp-server.dll。举个例子,wfreerdp_server_start是wfreerdp-server.dll内的一个输出函数,但断点无法进入。原因是先后有两个工程会生成wfreerdp-server.lib,第一个是wfreerdp-server.dll,第二个是wfreerdp-server.exe。由于两个同名,后面exe生成的lib会覆盖掉第一个,导致wfreerdp-server.lib调试信息丢失,断点无法进入它内中函数。

以下方法只是应付性解决这个问题,修改的人需要知道在做什么。打开“wfreerdp-server”,修改三处。

  • General/Target Name: wfreerdp-server ==> wfreerdp-server-last
  • General/Configuration Type: Dynamic_Library(.dll) ==> Static library(.lib)
  • Advanced/Target File Extension: .dll ==> $(TargetExt)

对Target Name,可以不是wfreerdp-server-last,只要改为一个不存的*.lib的就行。

由于wfreerdp-server.lib改为了wfreerdp-server-last.lib,用到该库的wfreerdp-server-cli须要改它的“Linker/Input/Additional Dependencies”,当中的wfreerdp-server.lib改为wfreerdp-server-last.lib

二、顶层逻辑

可把整个server归为四个步骤。一是解析命令行参数,二是侦听3389端口、启动3389侦听线程,三是不断运行、直到侦听线程结束,四是退出app。

2.1 解析命令行参数

相比于client,server命令行参数要简单很多。实用的可能就是修改3389端口

wfreerdp-server.exe 3390

用以上参数后可把端口改到3390。即使是为学习FreeRDP代码,也不建议修改这个端口。因为调试时,client往往会用mstsc.exe,mstsc默认只会连3389的server

这阶段会创建抓屏线程:wf_update_thread,句柄赋给updateThread成员变量。但不会运行。运行要在第三步骤,要有一个client完成了连接阶段。

2.2 侦听3389端口、启动侦听线程

BOOL wfreerdp_server_start(wfServer* server);
  • instance.Open(Open是函数指针,指向freerdp_listener_open)。在本地侦听3389端口。设备有N个“网卡”地址,它就会调用N次socket、bind、listen。所有3389的侦听socket运行在非阻塞模式。
  • 创建并运行3389侦听线程:wf_server_main_loop,句柄赋给thread成员变量。它的运行过程基本就是server的整个生命周期。

2.3 不断等待,直到侦听线程wf_server_main_loop退出

wf_server_main_loop负责侦听是否有client连向了3389,不是处理具体事务的线程,举个例子,它不处理任何的收发RDP PDU。当侦听到有client要求建立新连接后,会调用freerdp_listener_check_fds。后者首先accept得到client ip,然后调用wf_peer_accepted创建针对该client的会话处理线程wf_peer_main_loop。wf_peer_main_loop才是具体事务线程,像收发RDP PDU,为什么要使用1+N线程模式?因为同一时刻可能有N个client连接着3389。

wf_server_main_loop主要工作是处理新连接,因而绝大多数时间是花在select。至于它何时退出?——好像最多用的是手动退出app。

2.4 退出app

BOOL wfreerdp_server_stop(wfServer* server);
void wfreerdp_server_free(wfServer* server);

wfreerdp_server_stop结束3389服务,void wfreerdp_server_free释放相关资源。

三、抓屏线程:wf_update_thread

抓屏线程由主线程创建,创建时机是在main解析参数前。但那时只是创建,运行要有一个client完成了连接阶段,即会话处理线程wf_peer_main_loop收到了client发来的DATA_PDU_TYPE_FONT_LIST pud、并成功处理。

对Win10,抓屏使用的技术是DXGI。

  1. wf_info_update_changes(wfi)。获取此时的屏幕数据放在gAcquiredDesktopImage(类型:ID3D11Texture2D*),数据类型是位置在GPU的2d纹理。
  2. wf_info_have_updates。简单的“wfi->framesWaiting == 0”判断,没意外总是返回true。
  3. wf_update_encode。可分为三步。1)wf_info_find_invalid_region。汇集此次“脏”短形为一个大矩形wfi->invalid。2)wf_info_getScreenData根据wfi->invalid“扣”出gAcquiredDesktopImage中的图像数据到pDataBits指示的内存块。3)rfx_compose_message。形成可供rdp发送server到client的rfx pdu。
  4. SetEvent(((wfPeerContext*)wfi->peers[index]->context)->updateEvent)。唤醒各会话线程wf_peer_main_loop,告知有新的屏幕图像pdu了。wf_peer_main_loop被唤配后,会调用wf_update_peer_send把pdu发送出去。
  5. wf_info_clear_invalid_region。清除和此次时间片处理相关的变量。SetRectEmpty(&wfi->invalid)。

四、CredSSP、用户登陆

4.1 CredSSP

RDP要用CredSSP把username和password从client发送到server。见图2,client在发第三个TSRequest时,必填的authInfo包含着username和password,双方前面两个TSRequest,目的就是为了安全、正确收发第三个。

用过win10自带的mstsc.exe,可能会遇到这个错误:这可能是由于CredSSP加密Oracle修正。遇到这个错误要怎么办,可通过修改client端的“加密Oracle修正(高版本叫加密数据库修正)”改为最低安全级别。

“加密Oracle修正(高版本叫加密数据库修正)”改为最低安全级加的“易受攻击”。client向server发CredSSP请求时将使用安全级别更高的加密方法,但server是老版本,不支持这种级别更高的加密码方法,于是报这个错误。针对freerdp-server,它不支持这种加密方法,于是也出些同样问题。要解决这问题的方法和mstsc.exe一样,在运行client的win10,“加密Oracle修正”改为最低安全级加的“易受攻击”。

深入代码,这错误是何时发生的?——在server收到client发来第二个TSRequest。第二个TSRequest目的是双方协商pubKeyAuth,具体步骤是client生成pubKeyAuth,发送前先sha256签名、加密,server收到后解密、验证签名,但server解密时调用DecryptMessage,解密失败了。

SECURITY_STATUS nla_decrypt_public_key_hash(rdpNla* nla)
{
  ......
  status = nla->table->DecryptMessage(&nla->context, &Message, nla->recvSeqNum++, &pfQOP);
  // 执行DecryptMessage,status指示失败,错误码:SEC_E_UNSUPPORTED_FUNCTION。  
}
图2 client在CredSSP发出的三个TSRequest

注1:client如何计算pubKeyAuth。

发送了第一个TSRequest后,state进入NLA_STATE_NEGO_TOKEN状态。收到server发来的针对第一个TSRequest应答后,freerdp调用nla_encrypt_public_key_hash,它将生成pubKeyAuth。

 CredSSP Client-To-Server Binding Hash\0  (38) <---值是固定字符串,38字节
+ClientNonce (32)  <---都是32字节
+PublicKey (270)   <----都是270字节,值来自于tls阶段
(340)==>hash256, 32个字节

注2:authInfo内容是TSCredentials结构。credType固定是1,即credentials包含的是TSPasswordCreds结构。TSPasswordCreds内,domain:空;user:用户名;password:密码。

4.2 用户登陆机制

图3 client端。要求client操作者决定后绪操作

步骤1。client向server发出请求,用户:macbookpro15.2,但server上正有其它用户在用,于是client弹出图3。如果client一直不选择“是”,并且server没有收到client发来的pdu,像鼠标移动导致的Fast-Path Input Event,在经过N秒(N=30)后,server就向client发出Server Set Error Info PDU,4字节的errorInfo设为ERRINFO_LOGOFF_BY_USER(0xc)。

图4 server端。要求server操作者决定后绪操作

步骤2。图3中client选择了“是”,server收到后,发现另外一个用户正在使用桌面,于是弹出图4要求server操作人选择接下怎么操作。这里有三种情况,一是一直不操作,即不选择“确定”、也不选择“取消”,N秒后(N=30),server会视为“确定”,进入步骤3。二是选择“确定”,进入步骤3。三是选择“取消”,server向client发Server Save Session Info PDU,4字节的infoType设为INFOTYPE_LOGON_EXTENDED_INFO(0x3)。ErrorNotificationType置为LOGON_MSG_DISCONNECT_REFUSED(0xFFFFFFF9),同时ErrorNotificationData值是LOGON_WARNING(0x00000003)。

步骤3。server切换到client登陆的用户macbookpro15.2,这时会连续发出下面三个pdu。

Server Set Error Info PDU。4字节的errorInfo设为ERRINFO_NONE(0)
Server Fast-Path Update PDU。updateCode:5, fragmentation:0, compression:2
Save Session Info Data PDU。UserName就是登陆的macbookpro15.2。

4.3 去掉CredSSP

为什么要去掉CredSSP?设想这么个应用,要在Android上建RDP Server,client使用win10内置的mstsc.exe。

  • Android server没必要实现Win10的多用户切换机制。CredSSP作用是把username、password由client传到server,功能上是可以不要CredSSP。
  • 一旦使用CredSSP,mstsc.exe会使用“较高等级”加密算法(似类“加密Oracle修正”),Android上的ssl库可能无法解密。
  • 去除CredSSP后,只是不再传username、password,对后绪数据传输还是基于TLS,没并降低高全性。

由此可看出,一些场合有必要去掉CredSSP,以下是去掉CredSSP后的连接阶段。用的client是Windows 10 Version 1903自带的mstsc.exe。

  1. CR(Connection Request)中的requestedProtocols指示client能支持安全协议,值0xb,即同时支持PROTOCOL_HYBRID_EX、PROTOCOL_HYBRID和PROTOCOL_SSL。
  2. 收到client发来的requestedProtocols后,server检查自已的安全能力,如果不修改源码,server安全能力是PROTOCOL_HYBRID(因为NlaSecurity=TRUE)、PROTOCOL_SSL(因为TlsSecurity=TRUE),结合client发来、协商出的值是PROTOCOL_HYBRID(2),并通过CC中的requestedProtocols发送出去。
  3. client发现此次会话要使用PROTOCOL_HYBRID,发出CredSSP阶段的第一个TSRequest。
  4. server收到第一个TSRequest后,调用nla->table->AcceptSecurityContext(函数指针,指向winpr_AcceptSecurityContext)。sspi_GetSecurityFunctionTableAByNameA由Name得到table,返回的table是nullptr,出错,返回SEC_E_SECPKG_NOT_FOUND,向client发指示失败TSRequest(还是没有errorcode,而是pubKeyAuth字段是0字节)。——定义了WITH_NATIVE_SSPI宏时,AcceptSecurityContext将指向win32 api(PSecurityFunctionTable中的AcceptSecurityContext成员变量),server也不会发失败。
  5. client收到server发来的应答,发现本该有效的pubKeyAuth字段是0字节,认为server不支持CredSSP。断开3389连接。重新发CR,requestedProtocols值由0xb变成了0x1。
  6. 收到client发来的requestedProtocols后,server检查自已的安全能力,结合client发来,协商出的值是PROTOCOL_SSL(1),并通过CC中的requestedProtocols发送出去。
  7. client发现此次会话要使用PROTOCOL_SSL,于是略过CredSSP,向server发Client MCS Connect Initial PDU with GCC Conference Create Request。

如果明确知道server不支持PROTOCOL_HYBRID,可以把NlaSecurity初始值设为FALSE,这样在第2步CC中的requestedProtocols值就是PROTOCOL_SSL,直接进入第7步。

为模拟去除CredSSP,在windows时可修改config.h,不定义WITH_NATIVE_SSPI

<FreeRDP>/out/config.h
/* #undef WITH_CUPS */
// #define WITH_NATIVE_SSPI // 在windows,它默认是定义的
/* #undef WITH_JPEG */

 

 

 

全部评论: 0

    写评论: