RunUO Community

This is a sample guest message. Register a free account today to become a member! Once signed in, you'll be able to participate on this site by adding your own topics and posts, as well as connect with other members through your own private inbox!

Crypt.dll Reverse Engineering

Reetus

Sorceror
Hello,

With the source for the Razor client available, but without the DLL sources, I've embarked on the crazy journey of reverse engineering it in IDA Pro and re-implementing it in C.

The plan is/was to make a function compatible DLL that you can just drop in your Razor directory and everything works the same.

My C is really rusty as you'll see by the code, it's pretty horrid.

So far you can log into the game (localhost only hardcoded at the moment), and encryption is removed regardless of the setting, performance can be horrid, I assume it's from all the mutex locks but I haven't been able to determine why mine performs worse than real Crypt.dll.

The packet sizes are currently hardcoded for the client I've been testing with, which is latest I believe, the functions that are not currently implemented just have the stubs there for the DLL exports.

I'm searching for any programmers who can take a look at the code and fix any errors.

The source is up on Github, with the source for a working Loader.dll, the work-in-progress Crypt.dll code, and a small c# test app (just copy/pasted from Razor) to load the client and filter the packets through.

https://github.com/Reetus/RazorRE
 

_Epila_

Sorceror
Just a feedback: I've compiled/tested your DLL and the test app, it does load the client and connect, but hang after character selection, using the latest client
I knew it was a matter of time to someone release something like this. Don't know how the community will react but thank you for releasing it :)
 

Reetus

Sorceror
Just a feedback: I've compiled/tested your DLL and the test app, it does load the client and connect, but hang after character selection, using the latest client
I knew it was a matter of time to someone release something like this. Don't know how the community will react but thank you for releasing it :)


It probably didn't hang after character selection, just performance is really bad at the moment, so if you were trying to login to a crowded location, it can take minutes to actually have the game appear, although running it with real Razor outside of Visual Studio increases performance alot.

There is still some errors with the packet sizes and things that will hang the client once you're in game though.

One of the reasons I made the post so soon was hopefully some coders could look it over with fresh eyes and improve of what I've done so far, but I don't know if there is many that still play/have interest in UO.

Also I don't think the community should react harshly, there is already enough pieces of knowledge out there for anyone determined, I'm not a professional software developer, just a hobbyist (although I've probably spent more time studying Assembly language than the average person).

Razor with my Crypt.dll running a macro:

 

_Epila_

Sorceror
Even though I "know" how the main part of it works (hook, injection...) I don't know how to work with assembly.

Feedback 2: I've just restarted my PC and I'm suddenly not able to open the client with the C# app, Load() from Loader.dll returns no error but the client crash before showing any window
Razor is able to run with the new dlls but does crash after character selection and the client hangs again. ,

I'm making these feedbacks to let you know how it behave on someone else computer.
 

_Epila_

Sorceror
Praxiis managed to successfully intercept the communications, you may want to take a look at UltimaLive source
 

Zippy

Razor Creator
Razor uses the client's own packet size table. Crypt.dll has a class called a "MemoryFinder" which make it easier to locate a bunch of different stuff in the client memory based on small "expected" fragments. Here's the code for finding the packet table:

Code:
#pragma pack(1)

struct ClientPacketInfo // 12 bytes
{
    int Id;
    int Unk;
    unsigned short Length;
    short Unk0000;
};
 
#define PACKET_TBL_STR "\x07\0\0\0\x03\0\0\0"
#define PACKET_TS_LEN 8
#define PACKET_TBL_OFFSET (0-(8+12+12))
// elsewhere: MemoryFinder mf;
 
    int i = 0;
    while (( addr = mf.GetAddress( PACKET_TBL_STR, PACKET_TS_LEN, i++ )) != 0)
    {
        memset( pShared->PacketTable, 0xFF, 512 );
 
        addr += PACKET_TBL_OFFSET;
        if ( IsBadReadPtr( (void*)addr, sizeof(ClientPacketInfo)*128 ) )
            continue;
        ClientPacketInfo *tbl = (ClientPacketInfo*)addr;
 
        if (tbl[0].Id == 1 || tbl[0].Id == 2 || tbl[0].Id >= 256)
            continue;
       
        // this one isnt in order because OSI are idiots (0xF8)
        pShared->PacketTable[tbl[0].Id] = tbl[0].Length;
       
        int idx = 1;
        bool got1 = false, got2 = false;
        for (int prev = 0; prev < 255 && idx < 256; idx++)
        {
            if (IsBadReadPtr( (void*)(tbl + idx), sizeof(ClientPacketInfo)) ||
                tbl[idx].Id <= prev || tbl[idx].Id >= 256)
            {
                break;
            }
 
            got1 |= tbl[idx].Id == 1 && tbl[idx].Length == StaticPacketTable[1];
            got2 |= tbl[idx].Id == 2 && tbl[idx].Length == StaticPacketTable[2];
 
            prev = tbl[idx].Id;
            if ( pShared->PacketTable[prev] == 0xFFFF )
                pShared->PacketTable[prev] = tbl[idx].Length;
        }
 
        if (idx < 128 || !got1 || !got2)
            continue;
        else
            break;
    }
 
    if (!addr)
        CopyFailed = true;

... Where StaticPacketTable is a dump of the "normal" table. Mine is from an ancient version of UO, any would work.


As for the mutexes I'm not really sure. My instinct was that your newSelect() was broken, but I looked over it and now I'm not so sure. Here's Crypt.dll's version in case it helps.
Code:
int PASCAL HookSelect( int ndfs, fd_set *readfd, fd_set *writefd, fd_set *exceptfd, const struct timeval *timeout )
{
    bool checkRecv = false;
    bool checkErr = false;
    bool modified = false;
    int realRet = 0;
    int myRet = 0;

    if ( CurrentConnection )
    {
        if ( readfd != NULL ) 
            checkRecv = FD_ISSET( CurrentConnection, readfd );

        if ( exceptfd != NULL ) 
            checkErr = FD_ISSET( CurrentConnection, exceptfd );
    }

    timeval myTimeout;

    if ( SmartCPU )
    {
        int length = 0;

        if ( Active )
        {
            LARGE_INTEGER end;
            QueryPerformanceCounter( &end );

            length = int( 1000000.0 * ((end.QuadPart-Counter.QuadPart)/double(PerfFreq.QuadPart)) );
        }

        if ( length < 33333 )
        {
            myTimeout.tv_sec = 0;
            myTimeout.tv_usec = 33333 - length;
            timeout = &myTimeout;
        }
    }

    realRet = (*(SelectFunc)OldSelect)( ndfs, readfd, writefd, exceptfd, timeout );

    if ( SmartCPU )
        QueryPerformanceCounter( &Counter );

    if ( checkRecv )
    {
        if ( FD_ISSET( CurrentConnection, readfd ) )
        {
            FD_CLR( CurrentConnection, readfd );
            RecvData();
            realRet--;
        }

        WaitForSingleObject( CommMutex, INFINITE );
        if ( pShared->OutRecv.Length > 0 || ( pShared->ForceDisconn && pShared->AllowDisconn ) )
        {
            FD_SET( CurrentConnection, readfd );
            myRet++;
        }
        ReleaseMutex( CommMutex );
    }

    if ( checkErr && !FD_ISSET( CurrentConnection, exceptfd ) )
    {
        WaitForSingleObject( CommMutex, INFINITE );
        if ( pShared->ForceDisconn && pShared->AllowDisconn && pShared->OutRecv.Length <= 0 )
        {
            FD_SET( CurrentConnection, exceptfd );
            myRet++;
        }
        ReleaseMutex( CommMutex );
    }

    if ( realRet < 0 )
    {
        return myRet;
    }
    else
    {
        return realRet + myRet;
    }
}

The main differences I see are that Crypt is only calling select() once, and that crypt uses newSend/HookSend to flush the outgoing (to the server) network buffers (using "realSend"). Crypt also does a realSend anytime it gets a SEND message from Razor.

Now that I look at this again I do see a problem where "SmartCPU" can cause data incoming from Razor to wait in the buffers longer than it should (because the "fake" recv isn't subject to the select() timeout). Oh well, that's only been broken for... 10 years....

Good luck!
 

Zippy

Razor Creator
Here's that MemoryFinder class:

Code:
#pragma pack(1)
#pragma once

#include <vector>
using std::vector;

class MemFinder
{
public:
    static DWORD Find( const void *data, int length, DWORD hint = 0x00400000, DWORD addressMax = 0x01000000 );

    MemFinder();
    ~MemFinder();

    void AddEntry( const void *data, int length, unsigned int maxResults, DWORD hint );
    void AddEntry( const void *data, int length, DWORD hint = 0x00400000 ) { AddEntry( data, length, 1, hint ); }

    DWORD GetAddress( const void *data, int length, unsigned int idx = 0 );

    void Execute();

    void Clear();

private:
    struct Entry
    {
        char *Data;
        int Length;
        DWORD PositionHint;
        unsigned int MaxResults;
        vector<DWORD> Results;
    };

    vector<Entry> _Entries;
    bool _Executed;
    DWORD _StartPos;
};

Code:
#include "stdafx.h"
#include "MemFinder.h"

DWORD MemFinder::Find( const void *data, int length, DWORD addressHint, DWORD addressMax )
{
    for( DWORD addr = addressHint; addr < addressMax && !IsBadReadPtr( (void*)addr, length ) ; addr++ )
    {
        if ( memcmp( (const void*)addr, data, length ) == 0 )
            return addr;
    }

    return 0;
}

MemFinder::MemFinder()
{
    Clear();
}

MemFinder::~MemFinder()
{
    Clear();
}

void MemFinder::Clear()
{
    _Executed = false; 
    _StartPos = 0xFFFFFFFF;

    for(unsigned int i=0;i<_Entries.size();i++)
        delete[] _Entries[i].Data;

    _Entries.clear();
}


void MemFinder::AddEntry( const void *data, int length, unsigned int maxResults, DWORD hint )
{
    Entry ent;

    ent.Data = new char[length];
    memcpy( ent.Data, data, length );

    ent.Length = length;
    ent.MaxResults = maxResults;
    ent.PositionHint = hint;

    if ( hint < _StartPos )
        _StartPos = hint;

    _Entries.push_back( ent );
}

DWORD MemFinder::GetAddress( const void *data, int length, unsigned int idx )
{
    if ( !_Executed )
        return 0;

    for(unsigned int i=0;i<_Entries.size();i++)
    {
        if ( _Entries[i].Length != length )
            continue;

        if ( !memcmp( data, _Entries[i].Data, length ) )
        {
            if ( idx < _Entries[i].Results.size() )
                return _Entries[i].Results[idx];
            else
                return 0;
        }
    }

    return 0;
}

void MemFinder::Execute()
{
    bool allDone = false;
    for( DWORD pos = _StartPos; pos < 0x02000000 && !allDone; pos++ )
    {
        allDone = true;
        for(unsigned int i=0;i<_Entries.size();i++)
        {
            Entry &e = _Entries[i];

            if ( e.Results.size() >= e.MaxResults )
                continue;

            if ( IsBadReadPtr( (void*)pos, e.Length ) )
                continue;

            allDone = false;

            if ( e.PositionHint > pos )
                continue;

            if ( !memcmp( (void*)pos, e.Data, e.Length ) )
                e.Results.push_back( pos );
        }
    }

    _Executed = true;
}

Typical usage:
Code:
    DWORD addr = 0, oldProt;
 
    MemFinder mf;
    mf.AddEntry( "Electronic Arts Inc.", 20 );
    mf.AddEntry( "intro.bik", 10 );
    mf.AddEntry( "osilogo.bik", 12 );

    mf.Execute();

    addr = mf.GetAddress( "intro.bik", 10 );
    if ( addr )
        MemoryPatch( addr, "intro.SUX", 10 );
    addr = mf.GetAddress( "ostlogo.bik", 12 );
    if ( addr )
        MemoryPatch( addr, "osilogo.SUX", 12 );

    addr = mf.GetAddress( "Electronic Arts Inc.", 20 );
    if ( addr )
    {
        addr -= 7;
        VirtualProtect( (void*)addr, 52, PAGE_EXECUTE_READWRITE, &oldProt );
        strncpy( (char*)addr, "[Powered by Razor - The cutting edge UO Assistant]\0", 52 );
        VirtualProtect( (void*)addr, 52, oldProt, &oldProt );
    }
 

Reetus

Sorceror
Good luck!


Thanks, appreciate it.

I haven't been working on it much since my initial post, but I did start dabbling the other day with reading the packet table from the client after seeing a post about it on another forum, then looking back at IDA and realising thats what you were doing.

I will properly digest your post when I get some time to work on it.

The speed issue initially seems to be mostly resolved anyway, and was just mainly caused by my test app instead of using real Razor with my crypt.dll.
 

_Epila_

Sorceror
I'm not the best person to do things like that but I've managed to find the addresses where the (newer) client set/store the game screen width/height using Zippy's memory finder
Code:
case UONET_SETGAMESIZE:
                // TODO
                int x = (short)(lParam);
                int y = (short)(lParam>>16);
                LogPrintf("SetGameSize: %dx%d\r\n", x, y);
                dataBuffer->gameSizeX = x;
                dataBuffer->gameSizeY = y;
 
                MemFinder mf;
                DWORD addr;
                unsigned char gscreen[] = { 0x39, 0x44, 0x24, 0x08, 0x75, 0x14 }; //client.exe+160370 - mov eax,[esp+04] //7.0.23.1 - CheatEngine - this memory region is where the client set the w/h from the options menu - also works for 6.0.14.1 and 7.0.35.1 (guess clients in this range also works)
                mf.AddEntry(gscreen, sizeof(gscreen));
 
                mf.Execute();
 
                addr = mf.GetAddress(gscreen,sizeof(gscreen));
                if (addr)
                {
                    printf("found %X\n",addr);
                    addr += 8;
                    DWORD offsetX, offsetY;
                    memcpy(&offsetX,(void*)addr,4);
                    offsetY = offsetX + 4;
                    printf("offsetX %X\n",offsetX);
 
                    //memcpy(&x,(void*)offsetX,sizeof(x));
                    //printf("width %i\n",x);
                    //memcpy(&y,(void*)offsetY,sizeof(y));
                    //printf("height %i\n",y);
 
                    int * px = (int*)offsetX;
                    *px = x;
 
                    int * py = (int*)offsetY;
                    *py = y;
                }
There is things I'm missing since the client crash a few seconds after the width/height is set (note that client reset this if set right after startup)
 

Zippy

Razor Creator
This is what Crypt.dll does for changing the window size:

In setup:
Code:
    SizePtr = (SIZE*)mf.GetAddress( "\x80\x02\x00\x00\xE0\x01\x00\x00", 8 );
    if ( SizePtr )
    {
        addr = mf.GetAddress( "\x8B\x44\x24\x04\xBA\x80\x02\x00\x00\x3B\xC2\xB9\xE0\x01\x00\x00", 16 );
        if ( addr )
        {
            int i;
            DWORD origAddr = addr;
 
            VirtualProtect( (void*)origAddr, 128, PAGE_EXECUTE_READWRITE, &oldProt );
            for (i = 16; i < 128; i++)
            {
                if ( *((BYTE*)(addr+i)) == 0xE9 ) // find the first jmp
                {
                    memset( (void*)addr, 0x90, i ); // nop
                   
                    // mov eax, dword [esp+4]
                    *((BYTE*)(addr+0)) = 0x8B; // mov
                    *((BYTE*)(addr+1)) = 0x44; //  eax
                    *((BYTE*)(addr+2)) = 0x24; //  [esp
                    *((BYTE*)(addr+3)) = 0x04; //      +4]
                    addr += 4;
                   
                    *((BYTE*)addr) = 0x50; // push eax
                    addr++;
                    // call OnSetUOWindowSize
                    *((BYTE*)addr) = 0xE8;
                    *((DWORD*)(addr+1)) = ((DWORD)OnSetUOWindowSize) - (addr + 5);
                    addr += 5;
                    break;
                }
            }
            VirtualProtect( (void*)origAddr, 128, oldProt, &oldProt );
        }
    }
 
// ....

SIZE *SizePtr = NULL;
void __stdcall OnSetUOWindowSize( int width )
{
    if ( width != 800 && width != 600 ) // in case it actually the height for some reason
    {
        SizePtr->cx = 640;
        SizePtr->cy = 480;
    }
    else
    {
        *SizePtr = DesiredSize;
    }
}
 

_Epila_

Sorceror
I was using Reetus's SetGameSize and it works perfectly for latest clients and 6x (5x or lower crashes as the dll is not compatible with them?)
But thanks Zippy for showing parts of how Crypt works and Reetus for pointing me the UOMachine code, these are helping me

I'm still trying to understand how to find those signatures/addresses; the ones I found are mostly wrong...but I'm still figuring out how to work with low level things :)

I'm currently searching for client max draw distance, so when the game screen is increased we do not see those black areas
 

Reetus

Sorceror
latest clients and 6x (5x or lower crashes as the dll is not compatible with them?)


I don't even have a client installed thats less than version 7 at the moment, I'll work on that at a later date.

The replacement title bar is what I'm currently working on at the moment.
 

Reetus

Sorceror


Still a work-in-progress, but I've commit the code to Github if anyone wants to work on it, that tall hat second from the right is looking a little wonky.

If anyone wants to work on adding features to Razor client whilst I work on the DLL, that would be cool too.
 

cLandon

Wanderer
I used to use a gump instead of messing with the title bar. There's so much more you can do with a gump than you can with the title bar alone. I remember thinking there was no way you could update the gump fast enough without it "flashing" but it worked so well it was surprising. And the fact that you could move it anywhere, have it be any size you wished, etc, it was by far the best solution.

It was pretty easy to do, you pretty much just mimic how RunUO sends gumps to clients, use unique IDs for your gumps so you can block anything related to them from getting sent to the server and whatnot. I did this like 7 or 8 years ago so I don't remember everything to be honest and my code is long gone.

It's awesome to see Zippy throwing some of the closed source Razor code out there, I remember when my friend and I were working on a UOA/Razor "clone" he helped us out with some of the winsock hooking code.
 
Top