E: Outdated, I've updated my methods since then, but I decided to post anyway since it's very informational.
Console Output using Halo Engine
By: Skarma
September 05, 2008
-----------------------------------------------------------------
Using Halo engine to draw console text

Console output using Halo engine
Prologue
This is a short reference guide on how Halo outputs messages for the console and how to hook it so you can do it youself. The examples given will be in Assembly and C++. Programming experience is required to follow along, although it is still a good read. I will be showing pictures of disassembly with comments to the side, so you can see a more visual of what's happening.
Where to start
There are other text output functions in Halo, but I will be focusing on just one. The most common used one is the console output. If you have ever used console or devmode, you know what I am refering to. "Requested function "%s" cannot be executed now." is probably the most common string you will see, since it always outputs when a console or devmode command is not valid.
If you don't already know how to use console, you will need to edit the Halo desktop shortcut and add a command line parameter to enable it. To do so, create a shortcut to "halo.exe" on your desktop if you have not done so already. Right click the shortcut and click on Properties. A new window will pop up and you will see an edit box field named Target, which we will be editing. To enable console, you will need to add -console at the end of the Target box, after the quotes. Put a space between each parameter that you add. When I am working with Halo and I need to access tools, I also add -window as a parameter which will run Halo in a window every time I start it with the shortcut.
Code:
Example:
"C:\Program Files\Microsoft Games\Halo\halo.exe" -window -console
For a full list of argument, visit: http://hce.halomaps.org/index.cfm?nid=309.
To open / close console, press the Tilde ( ~ ) key anytime that Halo is open. To output the string I mentioned earlier, just type in some random text into the console box and press enter. I will not be using this string as a reference point, since is a different text out function that I don't personally like. I will be using the string "sv_kick is a server-only function!". You move a color into a register before calling it, which is why I like it. I use a debugger called OllyDbg to analyze binary code, which I will using in this guide for Halo. If you don't already have it you can download it at the official website: http://www.ollydbg.de/. I am already assuming you know how to use a debugger, so unfortunatly I will not be going over that subject. Let's move on.
Finding the console output function
Let's start by attaching Halo to OllyDbg. If you have a version of Halo before 1.08 and it has SafeDisc ( no crack ), you will need to reset the debug port, which is an anti debug protection SafeDisc uses. It's a simple detection, but there are tools on the net to bypass this. I like using HelioS Debug Reset. Fortunately for us, the 1.08 patch removed this protection and allows you to play with no cd.
Once attached, open Executable Modules and double click halo.exe a few times to open it in the CPU window. Right click in the CPU window and select Search for > All referenced text strings. Once in the reference window, right click and select Search for text. We need to find the string we will be referencing from, so just type in sv_kick is and click OK. It will bring you to the string we need, which was "sv_kick is a server-only function!". Hit Enter or double click it to see it in the CPU window.
Examine the function
The address for Halo 1.08 is 0x004E3C85, where this string is pushed on the stack. Let's examine the code.

I have commented out the above disassembly, which tells exactly what is happening. This is the regular procedure of outputting console text. When formatted arguments are added, they are also pushed on the stack before the text function is called, but we won't be dealing with that since we can format a string in our code before we push it on the stack.
You can see above that they are pushing a colour from a static address in memory. If you go to that address in RAM hex editor, you will see an array of floating point values in the format of ARGB ( Alpha, Red, Green, Blue ). You will also notice that it is 1.0f, 1.0f, 0.0f, 0.0f which is Red, the colour of the string when it is outputted to the Halo screen. This is how I figured out that this was the colour. Usually, colour values are in the range from 0 to 255. Well, this is the same thing, but using floats. If you have a colour value in this range, you can do an easy conversion: float value = range / 255.
After the colour is moved to EAX, the string is pushed on top of the stack, then the console output function is called, followed by a stack balance. If you don't balance out the stack, the game will just crash.
Pretty simple huh? Let's continue on and put it to good use!
Recreating the function in C++
I like using DLL projects, because you can directly work with the process once you have it injected into the process space. The benefits are not having to use any Windows memory API's like WriteProcessMemory, which are slow. Instead we can use pointers, since we have direct access to the process, which is more efficient in the end.
In C++, you can use the __asm keyword to write inline assembly, just like what we see in a debugger. I will be using this to recreate our function. We don't need to return anything, so our function will be of a void type. I want to be able to use custom colours. Let's use a float array of 4 elements for our color parameter ( remember ARGB? ). We also need to pass a string, which is in ASCII, so I will use a pointer to a char. The function I use looks exactly like this:
Code:
void hkTextOut( char * pString, float fColor[4] )
{
__asm
{
MOV EAX,fColor
PUSH pString
CALL DWORD PTR DS:[oTextOut]
ADD ESP, 04h
}
}
We need to create a function prototype and an instance so we can detour this function in Halo to our recreated function. This is also called hooking. oTextOut will be a call to the original function.
Code:
typedef void (*pTextOut)(char *pString, const float fColor[4]);
pTextOut oTextOut;
Detouring
The word detouring is exactly what it means. We detour something. In this case we are detouring the original console output function to our function, which will then call the original. There are a few ways of doing this. You can use Microsoft Detours Library, which is free to download. In the most recent library version is the function DetourAttach, which accomplishes what we need. In the old detours library, the function was called DetourFunction, which ultimately does the same exact thing. The other way we could do it is write our own detour function, so we do not need to include any extra files. I choose to write my own, which I will share with you:
Code:
void *DetourFunc(BYTE *src, const BYTE *dst, const int len)
{
BYTE *jmp = (BYTE*)malloc(len+5);
DWORD dwback;
VirtualProtect(src, len, PAGE_READWRITE, &dwback);
memcpy(jmp, src, len); jmp += len;
jmp[0] = 0xE9;
*(DWORD*)(jmp+1) = (DWORD)(src+len - jmp) - 5;
src[0] = 0xE9;
*(DWORD*)(src+1) = (DWORD)(dst-src) - 5;
VirtualProtect(src, len, dwback, &dwback);
return (jmp-len);
}
To use this function to detour the original Halo function, we will do this:
Code:
DWORD dwTextOut = 0x00496CD0;
oTextOut = (pTextOut)DetourFunc((PBYTE)dwTextOut,(PBYTE)&hkTextOut, 6);
The length is usually 5 or 6 bytes, depending on the disassembly. This just inserts a JMP from the original code to our function so each time it gets called, it will "detour" to ours. The alternate way to do this would be to write a code cave, which is best when writing a normal Windows Application, but this is not needed since we have direct access.
Page Protection & Calling our output function
Like most binary executables, it is write protected, which means we cannot modify any code in memory, so calling our DetourFunc will not do anything and most likely crash the game or our application. We need to change the access protection of the region we are modifying. The VirtualProtect API is just fine for doing this. After we are done, we should put the original protection back, just in case. While we have access we need to call our the console output function before setting the original protection. I wrote the below function for drawing text to minimize the task to something very, very simple. The detouring and page protection is all done for you when you call it.
Note: When detouring the text function, we need to write the original bytes back. If you enter in a console command while the function is detoured, the game will crash. In the below function I declare the original bytes and write them after I am done drawing text and before I change back the original page protection.
Code:
DWORD __stdcall hkDrawText(char *pString, const float fColor[4])
{
DWORD dwOldProtect = 0;
DWORD dwTextOut = 0x00496CD0;
BYTE bTextOutOrig[6] = {0x83, 0xEC, 0x10, 0x57, 0x8B, 0xF8};
VirtualProtect((void*)dwTextOut, 10, PAGE_EXECUTE_READWRITE, &dwOldProtect);
oTextOut = (pTextOut)DetourFunc((PBYTE)dwTextOut,(PBYTE)&hkTextOut, 6);
hkTextOut(pString, fColor);
memcpy((void*)dwTextOut, (void*)bTextOutOrig, 6);
VirtualProtect((void*)dwTextOut, 10, dwOldProtect, &dwOldProtect);
return 0;
}
Custom Colours and Drawing Text!
To create your own custom colours for your text output, you need to create a float array of 4 elements in the format of ARGB:
Code:
float fRed[4] = {1.0f, 1.0f, 0.0f, 0.0f};
float fGreen[4] = {1.0f, 0.0f, 1.0f, 0.0f};
float fBlue[4] = {1.0f, 0.0f, 0.0f, 1.0f};
These are just examples. I explained earlier how to do a conversion from those 0 to 255 values. Let's call our function!
Code:
hkDrawText( "I am drawing text! Woo!", fGreen );
That's it! If you want to format you string with additional arguements, you can use sprintf function in the fstream header like so:
Code:
char buf[256]; // will hold our formatted string
int a = 2; // an argument I am adding
sprintf(buf, "This is example %i with blue text!", a); // format our string
hkDrawText(buf, fBlue); // draw
Epilogue
This ends the draw text guide. I hope it was as exciting for you as it was for me!
-------------------------------------------------------------------------------------------------------
I have hooked 2 functions that can draw text like console output through Halo's Engine! I don't feel like explaining how it's all done, but I will just provide the source code here as an example of how to do it in C++/inline asm. One draws gray text and one draws white text.
Code:
#include <windows.h>
#include <detours.h>
//------------------------------------------------------------------------------
typedef void (*pDrawGrayText)(char * pString);
pDrawGrayText oDrawGrayText;
//------------------------------------------------------------------------------
void hkDrawGrayText(char * pString)
{
__asm PUSH pString
__asm PUSH 0;
__asm CALL DWORD PTR DS:[oDrawGrayText];
__asm ADD ESP, 08h;
}
//------------------------------------------------------------------------------
typedef void (*pDrawWhiteText)(char * pString);
pDrawWhiteText oDrawWhiteText;
//------------------------------------------------------------------------------
void hkDrawWhiteText(char * pString)
{
__asm MOV EAX,DWORD PTR DS:[0x0067D1EC]
__asm PUSH pString
__asm CALL DWORD PTR DS:[oDrawWhiteText];
__asm ADD ESP, 04h;
}
//------------------------------------------------------------------------------
DWORD __stdcall dwHook( void *)
{
HANDLE hand = GetCurrentProcess();
DWORD old = NULL;
struct AText { unsigned char back[6]; };
struct BText { unsigned char back[6]; };
struct CText { unsigned char jmp[6]; };
AText * aText = ( AText * )0x004C4770; //graytext
BText * bText = ( BText * )0x00495120; //whitetext
CText * cText = ( CText * )0x004C477D; //draw w/console closed
unsigned char nop = 0x90;
unsigned char back1[6] = { 0xA0, 0xC0, 0xE2, 0x6A, 0x00, 0x81 };
unsigned char back2[6] = { 0x83, 0xEC, 0x10, 0x57, 0x8B, 0xF8 };
// DrawGrayText
VirtualProtectEx(hand,(void*)0x004C4770,10,PAGE_EXECUTE_READWRITE,&old);
// Draw without console open
VirtualProtectEx(hand,(void*)0x004C477D,10,PAGE_EXECUTE_READWRITE,&old);
for( unsigned char i = 0; i < 6; i++ )cText->jmp[i] = nop;
// DrawWhiteText
VirtualProtectEx(hand,(void*)0x00495120,10,PAGE_EXECUTE_READWRITE,&old);
while(1)
{
if (GetAsyncKeyState(VK_F5)&1)
{
oDrawGrayText = (pDrawGrayText)DetourFunction((PBYTE)0x004C4770,(PBYTE)hkDrawGrayText );
hkDrawGrayText("DeltronZero :]");
for( unsigned char a = 0; a < 6; a++ ) aText->back[a] = back1[a];
}
if (GetAsyncKeyState(VK_F6)&1)
{
oDrawWhiteText = (pDrawWhiteText)DetourFunction((PBYTE)0x00495120,(PBYTE)hkDrawWhiteText );
hkDrawWhiteText("DeltronZero :]");
for( unsigned char a = 0; a < 6; a++ ) bText->back[a] = back2[a];
}
Sleep( 10 );
}
return 0;
}
//------------------------------------------------------------------------------
bool __stdcall DllMain( HMODULE hModule, DWORD ulReason, LPVOID lpReserved )
{
if( ulReason == DLL_PROCESS_ATTACH )
{
CreateThread( 0, 0, dwHook, 0, 0, 0 );
}
return true;
}
//------------------------------------------------------------------------------
Bookmarks