On a dark and stormy night, I was playing with Forshaw’s fantastic NTOBJECTMANGER library which, among the millions of things, is able to “disassemble” RPC servers and implement Local RPC calls in .NET.
I was looking at “interesting” RPC servers on my Windows 10 1909 machine when my attention was caught by the “XblGameSave.dll” library.
This Dll is used by the “Xbox Live Game Service“:
What is the purpose of this service? Well, I never played with Xbox nor Xbox games on Windows, but MS states that:
“This service syncs save data for Xbox Live save enabled games. If this service is stopped, game save data will not upload to or download from Xbox Live.”
The service runs under the SYSTEM user context and is set to manual-triggered startup:
In short, XblGameSave can be started upon a remote procedure call event.
I immediately popped up a new powershell instance as a standard user, imported the Ntobjectmanager library and took a look at the Dll:
Looked promising! The Dll exported a Local RPC Call “svcScheduleTaskOperation” with an Interface ID: f6c98708-c7b8-4919-887c-2ce66e78b9a0 and running as SYSTEM , maybe I could abuse this call to schedule a task as a privileged user?
Side note: I was able to get all these detailed information because I also specified the relative .pdb symbol file located in c:\symbols. You can download the symbols files with the symchk.exe tool available with the Windows SDK:
symchk.exe /v c:\windows\system32\xblagamesave.dll /s SRV*c:\symbols\*http://msdl.microsoft.com/download/symbols
In order to obtain more information about the exposed interface, I created a client instance:
The mysterious svcScheduleTaskOperation() wanted a string as input parameter. Next step was connect to the RPC server:
Cool! when connecting my client I was able to trigger and start the service, so I tried to invoke the RPC call:
As you can imagine, now the problem was guessing which string the function was waiting for…
The return value -2147024809 was only telling me that the parameter was “Incorrect”. Thanks, this was a great help 😦
I hate fuzzing and bruteforcing and must admit that I’m really a noob in this field, this clearly was not the right path for me.
At this point, decompiling the Dll was no more an option! I had also the symbol file, so the odds of getting something readable and understandable by common humans like me were founded.
This was the pseudo-C code generated by IDA, and in short, as far as I understood (I’m not that good in reversing): if input parameter was not NULL (a2), the Windows API WindowsCreateString was called and the resulting HSTRING passed to the ScheduledTaskOperation() method belonging somehow the ConnectedStorage class.
I obviously googled for more information with the keyword “ConnectedStorage” but surprisingly all the resulting links pointing to MS site returned a 404 error… The only way was to retrieve cached pages: https://webcache.googleusercontent.com/search?q=cache:dEN5ets6TcYJ:https://docs.microsoft.com/en-us/gaming/xbox-live/storage-platform/connected-storage/connected-storage-technical-overview
It seemed that the “ConnectedStorage” class, implemented in this Dll, had the following purpose: “Store saved games and app data on Xbox One” (and probably Windows 10 “Xbox” games too?)
My goal was not to understand the deepest and mysterious mechanisms of these classes, so I jumped to the ScheduledTaskOperation() function:
void __fastcall ConnectedStorage::Service::ScheduledTaskOperation(LPCRITICAL_SECTION lpCriticalSection, const struct ConnectedStorage::SimpleHStringWrapper *a2) { const struct ConnectedStorage::SimpleHStringWrapper *v2; // rdi LPCRITICAL_SECTION v3; // rbx unsigned int v4; // eax const unsigned __int16 *v5; // r8 unsigned int v6; // eax const unsigned __int16 *v7; // r8 unsigned int v8; // eax const unsigned __int16 *v9; // r8 unsigned int v10; // eax const unsigned __int16 *v11; // r8 unsigned int v12; // eax const unsigned __int16 *v13; // r8 unsigned int v14; // eax const unsigned __int16 *v15; // r8 unsigned int v16; // eax const unsigned __int16 *v17; // r8 unsigned int v18; // eax const unsigned __int16 *v19; // r8 unsigned int v20; // eax const unsigned __int16 *v21; // r8 unsigned int v22; // eax const unsigned __int16 *v23; // r8 const unsigned __int16 *v24; // r8 int v25; // [rsp+20h] [rbp-40h] __int64 v26; // [rsp+28h] [rbp-38h] LPCRITICAL_SECTION v27; // [rsp+30h] [rbp-30h] __int64 v28; // [rsp+38h] [rbp-28h] __int128 v29; // [rsp+40h] [rbp-20h] int v30; // [rsp+50h] [rbp-10h] char v31; // [rsp+54h] [rbp-Ch] __int16 v32; // [rsp+55h] [rbp-Bh] char v33; // [rsp+57h] [rbp-9h] v2 = a2; v3 = lpCriticalSection; v27 = lpCriticalSection; v28 = 0i64; EnterCriticalSection(lpCriticalSection); v26 = 0i64; v4 = WindowsCreateString(L"standby", 7i64, &v26); if ( (v4 & 0x80000000) != 0 ) ConnectedStorage::ReportErrorAndThrow( (ConnectedStorage *)v4, (const wchar_t *)L"WindowsCreateString(str, static_cast<UINT32>(wcslen(str)), &_hstring)", v5); v25 = 0; v6 = WindowsCompareStringOrdinal(*(_QWORD *)v2, v26, &v25); if ( (v6 & 0x80000000) != 0 ) ConnectedStorage::ReportErrorAndThrow( (ConnectedStorage *)v6, (const wchar_t *)L"WindowsCompareStringOrdinal(_hstring, right._hstring, &result)", v7); WindowsDeleteString(v26); if ( v25 ) { v26 = 0i64; v8 = WindowsCreateString(L"maintenance", 11i64, &v26); if ( (v8 & 0x80000000) != 0 ) ConnectedStorage::ReportErrorAndThrow( (ConnectedStorage *)v8, (const wchar_t *)L"WindowsCreateString(str, static_cast<UINT32>(wcslen(str)), &_hstring)", v9); v25 = 0; v10 = WindowsCompareStringOrdinal(*(_QWORD *)v2, v26, &v25); if ( (v10 & 0x80000000) != 0 ) ConnectedStorage::ReportErrorAndThrow( (ConnectedStorage *)v10, (const wchar_t *)L"WindowsCompareStringOrdinal(_hstring, right._hstring, &result)", v11); WindowsDeleteString(v26); if ( v25 ) { v26 = 0i64; v12 = WindowsCreateString(L"testenter", 9i64, &v26); if ( (v12 & 0x80000000) != 0 ) ConnectedStorage::ReportErrorAndThrow( (ConnectedStorage *)v12, (const wchar_t *)L"WindowsCreateString(str, static_cast<UINT32>(wcslen(str)), &_hstring)", v13); v25 = 0; v14 = WindowsCompareStringOrdinal(*(_QWORD *)v2, v26, &v25); if ( (v14 & 0x80000000) != 0 ) ConnectedStorage::ReportErrorAndThrow( (ConnectedStorage *)v14, (const wchar_t *)L"WindowsCompareStringOrdinal(_hstring, right._hstring, &result)", v15); WindowsDeleteString(v26); if ( !v25 ) { v31 = 1; LABEL_11: v30 = 4; _mm_storeu_si128((__m128i *)&v29, (__m128i)GUID_LOW_POWER_EPOCH); v33 = 0; v32 = 0; ConnectedStorage::Power::PowerChangeCallback(v3 + 4, 0i64, &v29); goto LABEL_12; } v26 = 0i64; v16 = WindowsCreateString(L"testexit", 8i64, &v26); if ( (v16 & 0x80000000) != 0 ) ConnectedStorage::ReportErrorAndThrow( (ConnectedStorage *)v16, (const wchar_t *)L"WindowsCreateString(str, static_cast<UINT32>(wcslen(str)), &_hstring)", v17); v25 = 0; v18 = WindowsCompareStringOrdinal(*(_QWORD *)v2, v26, &v25); if ( (v18 & 0x80000000) != 0 ) ConnectedStorage::ReportErrorAndThrow( (ConnectedStorage *)v18, (const wchar_t *)L"WindowsCompareStringOrdinal(_hstring, right._hstring, &result)", v19); WindowsDeleteString(v26); if ( !v25 ) { v31 = 0; goto LABEL_11; } v26 = 0i64; v20 = WindowsCreateString(L"logon", 5i64, &v26); if ( (v20 & 0x80000000) != 0 ) ConnectedStorage::ReportErrorAndThrow( (ConnectedStorage *)v20, (const wchar_t *)L"WindowsCreateString(str, static_cast<UINT32>(wcslen(str)), &_hstring)", v21); v25 = 0; v22 = WindowsCompareStringOrdinal(*(_QWORD *)v2, v26, &v25); if ( (v22 & 0x80000000) != 0 ) ConnectedStorage::ReportErrorAndThrow( (ConnectedStorage *)v22, (const wchar_t *)L"WindowsCompareStringOrdinal(_hstring, right._hstring, &result)", v23); WindowsDeleteString(v26); if ( v25 ) ConnectedStorage::ReportErrorAndThrow( (ConnectedStorage *)0x80070057i64, (const wchar_t *)L"Service::ScheduledTaskOperation called with an invalid operation type.", v24); } } LABEL_12: LeaveCriticalSection(v3); }
In short:
- the expected input strings were “logon“, “standby“,”maintenance“, “testenter“, “testexit“
- “logon“, “standby“,”maintenance” did nothing! (fake??)
- “testenter” and “testexit” called the PowerChangeCallback method with a parameter set to GUID_LOW_POWER_EPOCH and a flag set 1 if “testenter” and 0 if “testexit“. This GUID identifies a “low power state” of the device.
The disassembled output of the PowerChangeCallback was the following:
__int64 __fastcall ConnectedStorage::Power::PowerChangeCallback(LPCRITICAL_SECTION lpCriticalSection, __int64 a2, __int64 a3) { LPCRITICAL_SECTION v3; // rdi int v4; // ebx HANDLE v5; // rcx __int64 v6; // rdx __int64 v7; // r8 HANDLE v8; // rcx DWORD v10; // eax const unsigned __int16 *v11; // r8 ConnectedStorage *v12; // rcx DWORD v13; // eax const unsigned __int16 *v14; // r8 ConnectedStorage *v15; // rcx v3 = lpCriticalSection; v4 = *(_DWORD *)(a3 + 20); EnterCriticalSection(lpCriticalSection); v5 = v3[1].OwningThread; if ( v4 ) { LOBYTE(v3[1].LockSemaphore) = 1; if ( !ResetEvent(v5) ) { v13 = GetLastError(); v15 = (ConnectedStorage *)((unsigned __int16)v13 | 0x80070000); if ( (signed int)v13 <= 0 ) v15 = (ConnectedStorage *)v13; ConnectedStorage::ReportErrorAndThrow(v15, L"Event: ResetEvent failed", v14); } v8 = v3[2].OwningThread; } else { LOBYTE(v3[1].LockSemaphore) = 0; if ( !SetEvent(v5) ) { v10 = GetLastError(); v12 = (ConnectedStorage *)((unsigned __int16)v10 | 0x80070000); if ( (signed int)v10 <= 0 ) v12 = (ConnectedStorage *)v10; ConnectedStorage::ReportErrorAndThrow(v12, L"Event: SetEvent failed", v11); } v8 = *(HANDLE *)&v3[3].LockCount; } if ( v8 ) (*(void (__fastcall **)(HANDLE, __int64, __int64))(*(_QWORD *)v8 + 8i64))(v8, v6, v7); LeaveCriticalSection(v3); return 0i64; }
This function was responsible for setting (“testenter“) and resetting (“testexit“) event objects in order to notify a waiting thread of the occurrence of the particular event ( I presume “low power change” in this case).
So back to us, what could I do with the original svcScheduleTaskOperation RPCcall ?
Probably nothing useful, maybe it has been exposed only for testing purpose. Why did MS not implement the other functions like logon, maintenance, standby ?
And why did they call it svcScheduleTaskOperation ? Perhaps they will complete it in a future Windows release?
Mystery!
As you can see, all legitimate commands returned 0 (success), that was a cold comfort 😦
My research sadly led to a dead end, but perhaps there are other forgotten or or leftover RPC interfaces to look for?
That’s all, for now 🙂