Skip to content

Commit e8048c1

Browse files
Marc-André Moreauawakecoding
Marc-André Moreau
authored andcommitted
fix TSPropertySet vtable version runtime detection
1 parent 4d3558c commit e8048c1

File tree

1 file changed

+180
-15
lines changed

1 file changed

+180
-15
lines changed

dll/RdpSettings.cpp

Lines changed: 180 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ static HRESULT Hook_ITSPropertySet_GetStringProperty(ITSPropertySet* This, const
151151

152152
if (SUCCEEDED(hr)) {
153153
char* propValueA = _com_util::ConvertBSTRToString((BSTR)*propValue);
154-
MsRdpEx_LogPrint(TRACE, "ITSPropertySet::GetStringProperty(%s, \"%s\")", propName, propValueA);
154+
MsRdpEx_LogPrint(TRACE, "ITSPropertySet::GetStringProperty(%s, \"%s\")", propName, propValueA ? propValueA : "");
155155
delete[] propValueA;
156156
}
157157
else {
@@ -233,6 +233,175 @@ static bool TSPropertySet_Hook(ITSPropertySet* pTSPropertySet, ITSPropertySetVtb
233233
return true;
234234
}
235235

236+
static bool IsLikelyVoidMethodNoArgs(void* fn)
237+
{
238+
if (!fn)
239+
return false;
240+
241+
MEMORY_BASIC_INFORMATION mbi = { 0 };
242+
if (!VirtualQuery(fn, &mbi, sizeof(mbi)))
243+
return false;
244+
245+
DWORD prot = mbi.Protect;
246+
247+
bool isExecutable =
248+
(prot & PAGE_EXECUTE) ||
249+
(prot & PAGE_EXECUTE_READ) ||
250+
(prot & PAGE_EXECUTE_READWRITE) ||
251+
(prot & PAGE_EXECUTE_WRITECOPY);
252+
253+
if (!isExecutable || mbi.State != MEM_COMMIT || (prot & PAGE_GUARD))
254+
return false;
255+
256+
#if defined(_M_AMD64) || defined(__x86_64__)
257+
uint8_t* code = (uint8_t*)fn;
258+
259+
size_t available = mbi.RegionSize - ((uintptr_t)fn - (uintptr_t)mbi.BaseAddress);
260+
if (available < 8)
261+
return false;
262+
263+
// Pattern 1: sub rsp, 0x28
264+
if (code[0] == 0x48 && code[1] == 0x83 && code[2] == 0xEC)
265+
return true;
266+
267+
// Pattern 2: push rbp; mov rbp, rsp
268+
if (code[0] == 0x55 && code[1] == 0x48 && code[2] == 0x89 && code[3] == 0xE5)
269+
return true;
270+
271+
// Pattern 3: add rcx, 0x50; jmp rel32 — lock method stubs
272+
if (code[0] == 0x48 && code[1] == 0x83 && code[2] == 0xC1 && code[3] == 0x50 &&
273+
code[4] == 0xE9)
274+
return true;
275+
276+
#elif defined(_M_IX86) || defined(__i386__)
277+
uint8_t* code = (uint8_t*)fn;
278+
279+
if ((mbi.RegionSize - ((uintptr_t)fn - (uintptr_t)mbi.BaseAddress)) < 3)
280+
return false;
281+
282+
// Typical prologue: push ebp; mov ebp, esp
283+
if (code[0] == 0x55 && code[1] == 0x8B && code[2] == 0xEC)
284+
return true;
285+
286+
// Alternate: sub esp, imm8
287+
if (code[0] == 0x83 && code[1] == 0xEC)
288+
return true;
289+
290+
#elif defined(_M_ARM64) || defined(__aarch64__)
291+
uint32_t* ins = (uint32_t*)fn;
292+
293+
size_t available = mbi.RegionSize - ((uintptr_t)fn - (uintptr_t)mbi.BaseAddress);
294+
if (available < 8)
295+
return false;
296+
297+
uint32_t instr0 = ins[0];
298+
uint32_t instr1 = ins[1];
299+
300+
// Pattern 1: stp x29, x30, [sp, #-16]! ; mov x29, sp
301+
if (instr0 == 0xA9BF7BF0 && instr1 == 0x910003FD)
302+
return true;
303+
304+
// Pattern 2: ret
305+
if (instr0 == 0xD65F03C0)
306+
return true;
307+
308+
// Pattern 3: add x0, x0, #imm ; b target
309+
if ((instr0 & 0xFFC003FF) == 0x91000000 && // ADD x0, x0, #imm
310+
(instr1 & 0xFC000000) == 0x14000000) // B <imm>
311+
return true;
312+
#endif
313+
314+
return false;
315+
}
316+
317+
static int TSPropertySet_FindLockFunctionsInVtbl(void** vtbl)
318+
{
319+
const int blockSize = 4;
320+
const int maxEntries = 30;
321+
322+
for (int i = 0; i <= maxEntries - blockSize; i++)
323+
{
324+
bool allMatch = true;
325+
326+
for (int j = 0; j < blockSize; j++) {
327+
void* fn = vtbl[i + j];
328+
329+
if (!IsLikelyVoidMethodNoArgs(fn)) {
330+
allMatch = false;
331+
break;
332+
}
333+
}
334+
335+
if (allMatch)
336+
return i; // found the start of a 4-function block
337+
}
338+
339+
return -1; // not found
340+
}
341+
342+
static int TSPropertySet_DetectVtblVersion(void** vtbl)
343+
{
344+
int vtblVersion = -1;
345+
DWORD ctlVersion = 0;
346+
347+
if (MsRdpEx_IsAddressInRdclientAxModule(vtbl))
348+
{
349+
ctlVersion = g_rdclientax.tscCtlVer;
350+
351+
if (ctlVersion >= 5326) {
352+
vtblVersion = 32;
353+
}
354+
else {
355+
vtblVersion = 30;
356+
}
357+
}
358+
else
359+
{
360+
ctlVersion = g_mstscax.tscCtlVer;
361+
362+
if (ctlVersion >= 27842) {
363+
// First seen in Windows 11 Insider Preview Build 27842
364+
vtblVersion = 32;
365+
}
366+
else {
367+
vtblVersion = 30;
368+
}
369+
}
370+
371+
/**
372+
* The vtable contains a block of 4 functions that take void and return void.
373+
* Detect common assembly pattern matching that simple function prototype,
374+
* and leverage this to find the offset where those functions begin:
375+
*
376+
* CTSPropertySet::EnterReadLock(void)
377+
* CTSPropertySet::LeaveReadLock(void)
378+
* CTSPropertySet::EnterWriteLock(void)
379+
* CTSPropertySet::LeaveWriteLock(void)
380+
*
381+
* Once we know the offset, use it to detect which vtable we're dealing with
382+
*/
383+
384+
int lockFunctionsOffset = TSPropertySet_FindLockFunctionsInVtbl(vtbl);
385+
386+
if (lockFunctionsOffset > 0) {
387+
MsRdpEx_LogPrint(DEBUG, "Found TSPropertySet lock functions at vtable offset %d", lockFunctionsOffset);
388+
389+
if (lockFunctionsOffset == 20) {
390+
vtblVersion = 32;
391+
}
392+
else if (lockFunctionsOffset == 18) {
393+
vtblVersion = 30;
394+
}
395+
else {
396+
MsRdpEx_LogPrint(WARN, "Unknown TSPropertySet lock functions vtable offset: %d", lockFunctionsOffset);
397+
}
398+
}
399+
400+
MsRdpEx_LogPrint(DEBUG, "TSPropertySet ctlVersion: %d vtblVersion: %d", ctlVersion, vtblVersion);
401+
402+
return vtblVersion;
403+
}
404+
236405
class CMsRdpPropertySet : public IMsRdpExtendedSettings
237406
{
238407
public:
@@ -244,20 +413,19 @@ class CMsRdpPropertySet : public IMsRdpExtendedSettings
244413

245414
if (m_pTSPropertySet)
246415
{
247-
if (MsRdpEx_IsAddressInRdclientAxModule(m_pTSPropertySet->vtbl))
248-
{
249-
DWORD version = g_rdclientax.tscCtlVer;
250-
251-
if (version >= 5326) {
252-
m_vtbl32 = (ITSPropertySetVtbl32*)m_pTSPropertySet->vtbl;
253-
} else {
254-
m_vtbl30 = (ITSPropertySetVtbl30*)m_pTSPropertySet->vtbl;
255-
}
416+
int vtblVersion = TSPropertySet_DetectVtblVersion(((void**)m_pTSPropertySet->vtbl));
417+
418+
if (vtblVersion == 32) {
419+
m_vtbl32 = (ITSPropertySetVtbl32*)m_pTSPropertySet->vtbl;
420+
m_vtbl30 = NULL;
256421
}
257-
else
258-
{
422+
else if (vtblVersion == 30) {
423+
m_vtbl32 = NULL;
259424
m_vtbl30 = (ITSPropertySetVtbl30*)m_pTSPropertySet->vtbl;
260425
}
426+
else {
427+
MsRdpEx_LogPrint(ERROR, "Unknown TSPropertySet vtable version: %d", vtblVersion);
428+
}
261429

262430
if (!g_TSPropertySet_Hooked) {
263431
TSPropertySet_Hook(m_pTSPropertySet, m_vtbl30, m_vtbl32);
@@ -423,7 +591,6 @@ class CMsRdpPropertySet : public IMsRdpExtendedSettings
423591

424592
HRESULT __stdcall GetBStrProperty(const char* propName, BSTR* propValue) {
425593
HRESULT hr = E_FAIL;
426-
BSTR bstrVal = NULL;
427594
WCHAR* wstrVal = NULL;
428595

429596
if (m_vtbl32) {
@@ -916,8 +1083,6 @@ HRESULT __stdcall CMsRdpExtendedSettings::SetRecordingPipeName(const char* recor
9161083

9171084
HRESULT CMsRdpExtendedSettings::AttachRdpClient(IMsTscAx* pMsTscAx)
9181085
{
919-
HRESULT hr;
920-
9211086
m_pMsTscAx = pMsTscAx;
9221087

9231088
ITSObjectBase* pTSWin32CoreApi = NULL;

0 commit comments

Comments
 (0)