RCE Endeavors 😅

April 30, 2014

Messing with MSN Internet Games (1/2)

Filed under: Game Hacking,General x86-64,Reverse Engineering — admin @ 12:08 AM

This post will entail the fun endeavors of reverse engineering the default MSN Internet Games that come with most “Professional” and higher versions of Windows (although discontinued from Windows 8 onwards). Namely the common protocol shared by Internet Backgammon, Internet Checkers, and Internet Spades.

backgammonUpon launching the game and connecting with another player, the first thing to do is to check what port everything is running on. In this case, it was port 443, which is the port most commonly used for SSL. This has the advantage of giving away a known protocol, but the disadvantage of not being able to read/modify any of the outgoing data. It can also mean that there is a custom protocol that is encrypted and has an SSL layer added on top before going out, but fortunately that is not the case here (spoilers).

ipStarting Point

Since SSL consists of part of the network code, the most logical place to start is in those respective modules which carry out the work: ncrypt.dll and bcrypt.dll. The prime target here is the SslEncryptPacket function. Presumably, this function will be called somewhere in the chain leading up in to the packet leaving the client. Per MSDN, two of the parameters for the function are:

pbInput [in]

    A pointer to the buffer that contains the packet to be encrypted.
cbInput [in]

    The length, in bytes, of the pbInput buffer.

If we can intercept the function call and inspect those parameters, there is a chance of being able to view the data that is leaving the client. If not, then inspecting further down the call stack will eventually lead to the plaintext anyway. There is also a corresponding SslDecryptPacket function which will serve as a starting point to getting and inspecting server responses.

The Plan

The plan of action is pretty straightforward.

  • Get into the address space of the target executable. This will be done through a simple DLL injection.
  • Find the target function for encrypting data (SslEncryptPacket) and decrypting data (follow call from SslDecryptPacket down).
  • Install hooks on these two functions. The chosen method will be through memory breakpoints.
  • Inspect the contents of incoming and outgoing messages in plaintext. Determine the protocol and begin messing with it.

The first step won’t be covered here due to the hundreds of different DLL injection tutorials/guides/tools already out there. The code in the injected DLL will be a pretty direct translation of the above steps. Something akin to the code below:

int APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
{
    switch(dwReason)
    {
    case DLL_PROCESS_ATTACH:
        (void)DisableThreadLibraryCalls(hModule);
        if(AllocConsole())
        {
            freopen("CONOUT$", "w", stdout);
            SetConsoleTitle(L"Console");
            SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
            printf("DLL loaded.\n");
        }
        if(GetFunctions())
        {
            pExceptionHandler = AddVectoredExceptionHandler(TRUE, VectoredHandler);
            if(SetBreakpoints())
            {
                printf("BCryptHashData: %016X\n"
                    "SslEncryptPacket: %016X\n",
                    BCryptHashDataFnc, SslEncryptPacketFnc);
            }
            else
            {
                printf("Could not set initial breakpoints.\n");
            }
        }
        break;
 
    case DLL_PROCESS_DETACH:
        //Clean up here usually
        break;
 
    case DLL_THREAD_ATTACH:
        break;
 
    case DLL_THREAD_DETACH:
        break;
    }
 
    return TRUE;
}

A “debug console” instance is created to save effort on having to attach a debugger in each testing instance. Pointers to the desired functions are then retrieved through the GetFunctions() function, and lastly memory breakpoints are installed on the two functions (encryption/decryption) to monitor the data being passed to them. For those wondering where BCryptHashData came from, it was traced down from SslDecryptData. It is actually called on both encryption/decryption, but will serve as the point of monitoring received messages from the server (in this post at least).

The second step is very easy and straightforward. By injecting a DLL into the process, we have full access to the process address space, and it is a simple matter of calling GetProcAddress on the desired target functions. This becomes basic WinAPI knowledge.

FARPROC WINAPI GetExport(const HMODULE hModule, const char *pName)
{
    FARPROC pRetProc = (FARPROC)GetProcAddress(hModule, pName);
    if(pRetProc == NULL)
    {
        printf("Could not get address of %s. Last error = %X\n", pName, GetLastError());
    }
 
    return pRetProc;
}
 
const bool GetFunctions(void)
{
    HMODULE hBCryptDll = GetModuleHandle(L"bcrypt.dll");
    HMODULE hNCryptDll = GetModuleHandle(L"ncrypt.dll");
    if(hBCryptDll == NULL)
    {
        printf("Could not get handle to Bcrypt.dll. Last error = %X\n", GetLastError());
        return false;
    }
    if(hNCryptDll == NULL)
    {
        printf("Could not get handle to Bcrypt.dll. Last error = %X\n", GetLastError());
        return false;
    }
    printf("Module handle: %016X\n", hBCryptDll);
 
    BCryptHashDataFnc = (pBCryptHashData)GetExport(hBCryptDll, "BCryptHashData");
    SslEncryptPacketFnc = (pSslEncryptPacket)GetExport(hNCryptDll, "SslEncryptPacket");
 
    return ((BCryptHashDataFnc != NULL) && (SslEncryptPacketFnc != NULL));
}

Installing the hooks (via memory breakpoints) is just an adaptation of the previous post on it. The code looks as follows:

const bool AddBreakpoint(void *pAddress)
{
    SIZE_T dwSuccess = 0;
 
    MEMORY_BASIC_INFORMATION memInfo = { 0 };
    dwSuccess = VirtualQuery(pAddress, &memInfo, sizeof(MEMORY_BASIC_INFORMATION));
    if(dwSuccess == 0)
    {
        printf("VirtualQuery failed on %016X. Last error = %X\n", pAddress, GetLastError());
        return false;
    }
 
    DWORD dwOldProtections = 0;
    dwSuccess = VirtualProtect(pAddress, sizeof(DWORD_PTR), memInfo.Protect | PAGE_GUARD, &dwOldProtections);
    if(dwSuccess == 0)
    {
        printf("VirtualProtect failed on %016X. Last error = %X\n", pAddress, GetLastError());
        return false;
    }
 
    return true;
}
 
const bool SetBreakpoints(void)
{
    bool bRet = AddBreakpoint(BCryptHashDataFnc);
    bRet &= AddBreakpoint(SslEncryptPacketFnc);
 
    return bRet;
}
 
LONG CALLBACK VectoredHandler(EXCEPTION_POINTERS *pExceptionInfo)
{
    if(pExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_GUARD_PAGE_VIOLATION)
    {        
        pExceptionInfo->ContextRecord->EFlags |= 0x100;
 
        DWORD_PTR dwExceptionAddress = (DWORD_PTR)pExceptionInfo->ExceptionRecord->ExceptionAddress;
        CONTEXT *pContext = pExceptionInfo->ContextRecord;
 
        if(dwExceptionAddress == (DWORD_PTR)SslEncryptPacketFnc)
        {
            DWORD_PTR *pdwParametersBase = (DWORD_PTR *)(pContext->Rsp + 0x28);
            SslEncryptPacketHook((NCRYPT_PROV_HANDLE)pContext->Rcx, (NCRYPT_KEY_HANDLE)pContext->Rdx, (PBYTE *)pContext->R8, (DWORD)pContext->R9,
                (PBYTE)(*(pdwParametersBase)), (DWORD)(*(pdwParametersBase + 1)), (DWORD *)(*(pdwParametersBase + 2)), (ULONGLONG)(*(pdwParametersBase + 3)),
                (DWORD)(*(pdwParametersBase + 4)), (DWORD)(*(pdwParametersBase + 5)));
        }
        else if(dwExceptionAddress == (DWORD_PTR)BCryptHashDataFnc)
        {
            BCryptHashDataHook((BCRYPT_HASH_HANDLE)pContext->Rcx, (PUCHAR)pContext->Rdx, (ULONG)pContext->R8, (ULONG)pContext->R9);
        }
 
        return EXCEPTION_CONTINUE_EXECUTION;
    }
 
    if(pExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)
    {
        (void)SetBreakpoints();
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    return EXCEPTION_CONTINUE_SEARCH;
}

checkersSoftware breakpoints will be set on the memory page that SslEncryptPacket and BCryptHashData are on. When these are hit a STATUS_GUARD_PAGE_VIOLATION will be raised and caught by the topmost vectored exception handler that the injected DLL installed upon load. The exception address will be checked against the two desired target addresses (SslEncryptPacket/BCryptHashData) and an inspection function will be called. In this case it will just echo the contents of the plaintext data buffers out to the debug console instance.  The single-step flag will be set so the program can continue execution by one instruction before raising a STATUS_SINGLE_STEP exception, upon which the memory breakpoints will be reinstalled (since guard page flags are cleared after the page gets accessed). For a more in-depth explanation, see the linked post related to memory breakpoints posted before on this blog.

The x64 ABI (on Windows) stores the first four parameters in RCX, RDX, R8, and R9 respectively, and the rest on the stack. There is no need to worry about locating any extra parameters in the case of BCryptHashData, which only takes four. However, SslEncryptData takes ten parameters, so there are another six to locate. In this case, there is no reason to care beyond the fourth parameter, but all of them are passed in for the sake of completeness. The base of the parameters on the stack were found by looking at how the function is called and verifying with a debugger during runtime.

The “hook” code, as mentioned above, will just print out the data buffers. The implementation is given below:

void WINAPI BCryptHashDataHook(BCRYPT_HASH_HANDLE hHash, PUCHAR pbInput, ULONG cbInput, ULONG dwFlags)
{
    printf("--- BCryptHashData ---\n"
        "Input: %.*s\n",
        cbInput, pbInput);
}
 
void WINAPI SslEncryptPacketHook(NCRYPT_PROV_HANDLE hSslProvider, NCRYPT_KEY_HANDLE hKey, PBYTE *pbInput, DWORD cbInput,
                              PBYTE pbOutput, DWORD cbOutput, DWORD *pcbResult, ULONGLONG SequenceNumber, DWORD dwContentType, DWORD dwFlags)
{
    printf("--- SslEncryptPacket ---\n"
        "Input: %.*s\n",
        cbInput, pbInput);
}

What Does It Look Like?

After everything is completed, it is time to inspect the protocol. Below are some selected packet logs from a session of Checkers.

STATE {some large uuid}
Length: 0x000003CD
 
<?xml version="1.0"?>
<StateMessage xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="h
ttp://www.w3.org/2001/XMLSchema" xsi:type="StateMessageEx" xmlns="http://zone.ms
n.com/stadium/wincheckers/">
  <nSeq>4</nSeq>
  <nRole>0</nRole>
  <eStatus>Ready</eStatus>
  <nTimestamp>578</nTimestamp>
  <sMode>normal</sMode>
  <arTags>
    <Tag>
      <id>chatbyid</id>
      <oValue xsi:type="ChatTag">
        <UserID>numeric user id</UserID>
        <Nickname>numeric nickname</Nickname>
        <Text>SYSTEM_ENTER</Text>
        <FontFace>MS Shell Dlg</FontFace>
        <FontFlags>0</FontFlags>
        <FontColor>255</FontColor>
        <FontCharSet>1</FontCharSet>
        <MessageFlags>2</MessageFlags>
      </oValue>
    </Tag>
    <Tag>
      <id>STag</id>
      <oValue xsi:type="STag">
        <MsgID>StartCountDownTimer</MsgID>
        <MsgIDSbKy />
        <MsgD>0</MsgD>
      </oValue>
    </Tag>
  </arTags>
</StateMessage>
 
STATE {some large uuid}
Length: 0x000006D1
 
<?xml version="1.0"?>
<StateMessage xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="h
ttp://www.w3.org/2001/XMLSchema" xsi:type="StateMessageEx" xmlns="http://zone.ms
n.com/stadium/wincheckers/">
  <nSeq>5</nSeq>
  <nRole>0</nRole>
  <eStatus>Ready</eStatus>
  <nTimestamp>2234</nTimestamp>
  <sMode>normal</sMode>
  <arTags>
    <Tag>
      <id>STag</id>
      <oValue xsi:type="STag">
        <MsgID>FrameworkUpdate</MsgID>
        <MsgIDSbKy />
        <MsgD>&lt;D&gt;&lt;StgSet&gt;&lt;SeatCnt&gt;2&lt;/SeatCnt&gt;&lt;GameT&g
t;AUTOMATCH&lt;/GameT&gt;&lt;AILvls&gt;2&lt;/AILvls&gt;&lt;GameM&gt;INIT_GAME&lt
;/GameM&gt;&lt;Start&gt;True&lt;/Start&gt;&lt;PMatch&gt;False&lt;/PMatch&gt;&lt;
ShowTeam&gt;False&lt;/ShowTeam&gt;&lt;/StgSet&gt;&lt;/D&gt;</MsgD>
      </oValue>
    </Tag>
    <Tag>
      <id>STag</id>
      <oValue xsi:type="STag">
        <MsgID>GameInit</MsgID>
        <MsgIDSbKy>GameInit</MsgIDSbKy>
        <MsgD>&lt;GameInit&gt;&lt;Role&gt;0&lt;/Role&gt;&lt;Players&gt;&lt;Playe
r&gt;&lt;Role&gt;0&lt;/Role&gt;&lt;Name&gt;8201314a      01&lt;/Name&gt;&lt;Type
&gt;Human&lt;/Type&gt;&lt;/Player&gt;&lt;Player&gt;&lt;Role&gt;1&lt;/Role&gt;&lt
;Name&gt;1d220e29      01&lt;/Name&gt;&lt;Type&gt;Human&lt;/Type&gt;&lt;/Player&
gt;&lt;/Players&gt;&lt;Board&gt;&lt;Row&gt;0,1,0,1,0,1,0,1&lt;/Row&gt;&lt;Row&gt
;1,0,1,0,1,0,1,0&lt;/Row&gt;&lt;Row&gt;0,1,0,1,0,1,0,1&lt;/Row&gt;&lt;Row&gt;0,0
,0,0,0,0,0,0&lt;/Row&gt;&lt;Row&gt;0,0,0,0,0,0,0,0&lt;/Row&gt;&lt;Row&gt;3,0,3,0
,3,0,3,0&lt;/Row&gt;&lt;Row&gt;0,3,0,3,0,3,0,3&lt;/Row&gt;&lt;Row&gt;3,0,3,0,3,0
,3,0&lt;/Row&gt;&lt;/Board&gt;&lt;GameType&gt;Standard&lt;/GameType&gt;&lt;/Game
Init&gt;</MsgD>
      </oValue>
    </Tag>
  </arTags>
</StateMessage>
 
CALL EventSend messageID=EventSend&XMLDataString=%3CMessage%3E%3CMove%3E%
3CSource%3E%3CX%3E6%3C/X%3E%3CY%3E5%3C/Y%3E%3C/Source%3E%3CTarget%3E%3CX%3E7%3C/
X%3E%3CY%3E4%3C/Y%3E%3C/Target%3E%3C/Move%3E%3C/Message%3E
 
CALL EventSend messageID=EventSend&XMLDataString=%3CMessage%3E%3CGameMana
gement%3E%3CMethod%3EResignGiven%3C/Method%3E%3C/GameManagement%3E%3C/Message%3E

The protocol basically screams XML-RPC. It appears that the entire state of the game is initialized and carried out over these XML messages. From a security perspective, it also presents an interesting target to fuzz, given the large variety of fields present within these messages, and the presence of a length field in the message.

Some Issues With This Approach

There are some issues with this approach. Firstly, ncrypt.dll and bcrypt.dll are delay loaded, so our DLL will have to be injected after a multiplayer session starts, or there will have to be some polling loop introduced to check whether these two DLLs have loaded. This is ugly and there is a much better way to get around this that will be talked about in the next post. Secondly, BCryptHashData is used for both incoming and outgoing messages. This makes it more difficult if we wish to mess with these messages as there will have to be logic added to distinguish between client and server messages. This will also be resolved in the next post.

The full source code relating to this can be found here.

3 Comments »

  1. You should look into using Rohitab’s API monitor (http://www.rohitab.com/apimonitor). Much of what was discussed in this article can be accomplished easily using that tool.

    Comment by 0xdabbad00 — April 30, 2014 @ 10:03 AM

  2. Point taken and well received. I like to experiment and present information on how it would be done without the use of external tools (excluding a debugger/disassembler of course). However, yes, this post can be condensed into a few sentences on what checkboxes to select if you were to use API Monitor.

    Comment by admin — April 30, 2014 @ 6:07 PM

  3. It is good and important to see the code to do this, especially for cases when the tools don’t work, and it’s harder to write about those cases. I just wanted to ensure you were aware of other potentially easier options, or for readers who might be unaware. Thank you for the post and keep up the good work!

    Comment by 0xdabbad00 — May 1, 2014 @ 2:43 PM

RSS feed for comments on this post. TrackBack URL

Leave a comment

 

Powered by WordPress