Silkroad Online – Simple access to UI-elements

I was asked if I can share my knowledge about in-Game notifications. Luckily, this is something I recently achieved, so lets go.

To make things clear from the start: We are talking about these nice little things.

Research

I’m pretty sure every single one of you can tell what each color is used for. Red is Notices from Game Masters. Blue is (mostly) game-related information. And green is used for annoying messages during Fortress War, Capture the Flag and other things like quests (who does quests…?)

The quest-related data is likely to be client-side. Also the green annoying messages tend to be client-side because they are localized. We can look for their localization identifiers from the clients textdata.

We could know that the overall interface-layout is stored in the Media.pk2/resinfo/ginterface.txt. Looking for CIFNotify will show three notification areas.

GDR_NOTICE:CIFNotify
{
    ID=INTEGER,"20"
}

GDR_UPDATE_QUEST_INFO:CIFNotify
{
    ID=INTEGER,"42"
}

GDR_WARNING_WND:CIFNotify
{
    ID=INTEGER,"35"
}

I filtered out the relevant information for you. Everything in this file has an ID, which is (probably) unique. Since the whole UI is backed by these files, there must be a function that can get controls by their ID.

Analysis

There are tons of strings to look at. And all of them lead to the correct location, but how do we know which one is the right one? Most of the references lead in BIG! functions like this oneor this one …

or maybe this one …

You COULD reverse engineer these … I’ve marked interesting locations in pink. You have to look very closely on the first one to spot it. It’s really a pain to do.

Avoiding reversing big functions

We don’t want to reverse these. Having a basic knowledge of what they are for is enough most of the time. When researching, we also stumbled upon ginterface.txt. We can search for either one of these IDs. Lets make a quick assumption: Which number will appear less often in the code? 20, 35 or 42? 20 is an even number that’s dividable by 4, it will appear on various occasions, lets drop this one. 42 is also an even number, but not dividable by 4, there is a chance that it will appear less often, and 35 is uneven, making it, in theory, unlikely to appear.

Considering that 35 is WARNING_WND aka. blue notification and 42 is UDPATE_QUEST_INFO aka. green notification, i consider 42 of the more valuable choice. We can easily trigger a notification ourselves.

Looking for immediate value “42” will return about 700 occurrences. We only need the ones in code, we don’t need arithmetic operations. If you are brave, you can assume that you only need PUSH-instructions at all. Filtering out all but PUSH-instructions will reduce the list to 67 occurrences. That’s a number we can handle. Set a breakpoint on every occurrence and hit that run-button. Remove any triggered breakpoint until the client is up and running. You might need to restart a few times because the client does not like to get interrupted in certain situations. Don’t get trolled because some p-servers use notifications to welcome their players. This might kill your important breakpoint.

Once up and running trigger a notification. You’ll land here:

Obviously, you won’t have these labels. Take these as a gift, you would have reversed these sooner or later. Cross-references to GetGUIObjectByID reveal over 3000 usages, CIFNotify::ShowMessage has only three, one is this one, and the other two are the two missing colors. They look similar to this one.

Reconstructing the signature

Calling convention and basic parameters

Lets figure out the obvious things first: The function has one parameter. Aside from the fact that IDA told us we can assume it from the RETN-instruction. It moves the stack by 4 bytes when returning. Typical for an STDCALL. Second, the function seems to have no return value. EAX is not touched before leaving the function. Also there is a call right before returning, which could also indicate that the return value of ShowMessage is kept. We can’t tell for sure at this point. Third and last, ECX is accessed without being properly initialized. This is pretty much a safe indicator for THISCALL-calling convention on MSVCx86. Taking into account, that we know that the two other functions look pretty much the same, we can also define these:

class UnknownClass {
 void ShowMessage_PINK(void* a1);
 void ShowMessage_GREEN(void* a1);
 void ShowMessage_BLUE(void* a1);
};

Extracting parameter types

We can go in different directions. Depending on your knowledge and experience, all can lead to success. But lets have a look at the function again, first. The first and only parameter should be the message, am i right? That makes sense. But why is the message pushed before the call to GetGUIObjectByID? Doesn’t that mean its a parameter of GetGUIObjectByID? No, it doesn’t. It doesn’t necessarily need to be a parameter even if it gets pushed long before the call it belongs to. It’s the compilers choice to do so. We can also verify that its not a parameter by examining GetGUIObjectByID and its parameter-count. In conclusion: Our parameter is passed to CIFNotify::ShowMessage instead.

We now could analyze ShowMessage to see how the message is used. Depending on your knowledge and experience, this will lead to success. Sometimes its easier to look at how a function is used, which is also true for this case.

But we can still carry on assuming things. We know that GREEN and BLUE notifications come from the textdata, which runs through the localization engine CTextStringManager. Lets assume our call is fed by the return-value of CTextStringManager::GetTranslation. If you are a reader of this blog, you might remember i told you the return value of CTextStringManager::GetTranslation before. I assumed it would be std::string. I was wrong. But don’t worry, that only proves that I’m still a little human inside. std::wstring would have been the correct answer.

With that assumption being set, lets see if we can find any evidence for it. Look through the different cross-references of all three functions.That looks like a proof. You only have to believe me that I’m right with std::wstring this time ;). We can also proof this. These are the guts of CIFNotify::ShowMessage.

Notice the length check of the parameter and the separation for internal and external buffer. A typical thing for std::string and std::wstring. Both can store 16 byte internally, but std::wstring has 2 bytes per character, which makes 8 characters in total.

What about this?

Looking for the origin of the this-pointer can be the most time consuming job in this whole process. Luckily for us, its quite easy in this case. Looking back at the call to ShowMessage_BLUE:

Our relevant register ECX is set right before the call, which is another typical sign for a THISCALL, but, its set to an intermediate value. No pointers, no offsets, nothing. Just an intermediate value.

Putting everything together, we end up with something like this:

class CGInterface {
 void ShowMessage_PINK(std::wstring *msg);
 void ShowMessage_GREEN(std::wstring *msg);
 void ShowMessage_BLUE(std::wstring *msg);
};

Calling a function like this

To be honest, function pointers look very confusing all the time. Don’t be scared. Lets start with a simple function with no return and a wstring-parameter.

void (*)(std::wstring *msg);

Now lets add the calling convention:

void (__thiscall *)(int pthis, std::wstring *msg);

Note that we can not use the name this, because its a reserved keyword. Also note, that i’m not using the correct type for this, for simplicity reasons.

Lets make that a type, so it doesn’t get super confusing:

typedef void (__thiscall * fpShowMessage_t)(int pthis, std::wstring *msg);

We can now use a reinterpret_cast to call it. For this, we need two “new” components: The address of the function and the intermediate value for the this-pointer.

#define g_CGInterface *(reinterpret_cast(0x0110F80C))
#define funPtr 0x0077B580

typedef void (__thiscall * fpShowMessage_t)(int pthis, std::wstring *msg);

// Define our message
std::wstring mymsg("Hello World");
// Call the function
reinterpret_cast(funPtr)(g_CGInterface, &mymsg);

What you should know…

Microsofts C++ library can be a bitch. If you compile on DEBUG, std::wstring will have an debug-allocator embedded, causing the structure to shift, the offsets to be wrong and your game to crash. Also, implementations and structure of STL-types might change over time. Silkroad Online is build on VC80, aka. 2005. 13 years ago. The newer your library and compiler, the more likely is a broken code. Consider reconstructing std::wstring for your own use so you won’t run into problems using newer libraries.

7 thoughts on “Silkroad Online – Simple access to UI-elements

Leave a comment