It seems like everyone wants to hack his favourite game but noone has the skills and the ambition to acquire them. There are a few good coders and reverse engineers here though, keep it up!
This post now consists of a compilation of all the papers I wrote about BattlEye for a different forum so far. The example used here is Soldat as I spend quite a lot of time reverse engineering it and BE was recently added. The information given here can be applied to any game BE uses as the mechanisms are always the same. In fact I once used Warsow (which is open source) to see how they implemented the BE client into the game. So basically if you're not stupid and read the papers you should be able to port my BE fix(es) to Warsow as well.
This is going to be a big ass post. Let's get it on:
(Note that the very first paper was written before BE was activated. The code for it was already there though so the information given there is still true!)
BattlEye Paper
This article is supposed to be an introduction for BattlEye, the anti cheat protection Soldat will use in the near feature. I am highly anticipating the full implementation of it as it surely will make things way more interesting. And because parts of it can already be found in Soldat, I had a look at it. So let's spread some basic knowledge about BattlEye.
Basic Usage
As you probably noticed BattlEye has to be running on client- and server-side so if you join a server with BattlEye enabled (which almost all of them have probably due to the fact that it is enabled by default), Soldat will run the client as well. The BattlEye client consists of an unpacked DLL you can find in your Soldat dir called BEClient_x86.dll at the time of writing. It is dynamically loaded into Soldat when entering a BE protected server and unloaded when you leave the server.
BattlEye and Soldat exchange pointers to functions both of them use as a callback.For example when BattlEye wants Soldat to print some text (the "BattlEye Client: Initialized" for example is from BE itself), it just calls a function supplied by Soldat. When the client DLL is loaded, the one and only export from it (well, there are two, one of them being the DllEntryPoint), the Init() function is called. Once the DLL is loaded Soldat will call some Run() function every frame. The pointer to it is exchanged using the Init() method.
When the game is over, Soldat unloads the DLL and everything is done.
BE Loading/Unloading
Let's have a look at where BE is loaded. Because we don't want it to be loaded so I guess we are right here (the truth so far is: I guess we are wrong
The InitBE routine (I called it that way) can be found at 0x503990 and handles everything. It loads the DLL, gets the proc address for the Init() routine and eventually calls it.
Code:
.text:005039B8 mov ecx, offset aBattleyeBeclie; "BattlEye/BEClient_x86.dll" .text:005039BD call @@LStrCat3 ; Build full path to BEClient.dll .text:005039C2 mov eax, [ebp+var_4] .text:005039C5 call @@LStrToPChar ; Convert delphi path string to C string .text:005039CA push eax ; lpLibFileName .text:005039CB call LoadLibraryA ; Load it .text:005039D0 mov hBEClientDll, eax; Store handle to DLL .text:005039D5 cmp hBEClientDll, 0 .text:005039DC jnz short DllLoaded
Code:
.text:005039E2 push offset aInit ; "Init" .text:005039E7 mov eax, hBEClientDll .text:005039EC push eax ; hModule .text:005039ED call GetProcAddress_0; Get proc address for Init() routine .text:005039F2 mov ds:BEInitRoutine, eax; Store it .text:005039F7 cmp ds:BEInitRoutine, 0 .text:005039FE jz short InitNotFound
The UnloadBE routine is even less spectacular:
Code:
.text:00503B8C UnloadBE proc near ; CODE XREF: sub_5177B8+1923p .text:00503B8C ; sub_572DD8+1C7p ... .text:00503B8C mov bBELoaded, 0 .text:00503B93 cmp hBEClientDll, 0 .text:00503B9A jz short locret_503BAE .text:00503B9C mov eax, hBEClientDll .text:00503BA1 push eax ; hLibModule .text:00503BA2 call FreeLibrary_0 .text:00503BA7 xor eax, eax .text:00503BA9 mov hBEClientDll, eax .text:00503BAE .text:00503BAE locret_503BAE: ; CODE XREF: UnloadBE+Ej .text:00503BAE retn .text:00503BAE UnloadBE endp
Now let's have a look at the Init() routine call as this is were most of the magic happens:
(code from OllyDbg now, my code is commented in IDA and I don't want to spoil everything right now
Code:
00503A00 68 44845A00 PUSH Soldat.005A8444 00503A05 68 40845A00 PUSH Soldat.005A8440 00503A0A 68 3C845A00 PUSH Soldat.005A843C 00503A0F 68 2C385000 PUSH Soldat.0050382C 00503A14 68 E0365000 PUSH Soldat.005036E0 00503A19 FF15 48845A00 CALL DWORD PTR DS:[5A8448]; Call Init() 00503A1F 83C4 14 ADD ESP,14 00503A22 84C0 TEST AL,AL 00503A24 74 09 JE SHORT Soldat.00503A2F 00503A26 C605 00115A00 01 MOV BYTE PTR DS:[5A1100],1
I traced the Init() function for what BE does with the 5 pushed args and we can divide them into two groups. The first two addresses are addresses of routines inside Soldat which BE calls if neccessary. The last three are DWORDs in Soldat's memory that will hold addresses to functions inside the BE client DLL code that Soldat can call. As I said earlier function pointers are exchanged. Even though I had a look at the code I didn't figure out exactely what each function was for so I had to look for help.
And this is were Warsow, another game, comes into play (no pun intended). It is a free open source(!) 3D shooter based on the Quake engine and the best of all: It uses BattlEye as well. So I grabbed a copy of the Warsow SDK and had a look at it.
Intermezzo: Warsow BE Code
Before going on with the Init() function I want to show the BE code found in Warsow.
Code:
void CL_Frame( int realmsec, int gamemsec )
{
[...]
#ifdef BATTLEYE
// run BattlEye here
if( clbe.module )
{
if( !clbe.Run() )
{
// reload now
CL_BE_Unload();
CL_BE_Load();
}
}
#endif
[...]
}
The unload routine
Code:
void CL_BE_Unload(void)
{
if (!clbe.module)
return;
if (Sys_Library_Close(clbe.module))
clbe.module = NULL;
else
Com_Error(ERR_DROP, "Failed to unload BattlEye Client. %s", Sys_Library_ErrorString());
}
Code:
void CL_BE_Load(void)
{
if (clbe.module)
return;
Sys_FS_CreateDirectory(va("%s/BattlEye", FS_WriteDirectory()));
FS_MoveBaseFile("BattlEye/BEClient_" ARCH LIB_SUFFIX, "BattlEye/BEClient_" ARCH LIB_SUFFIX);
if ((clbe.module = Sys_Library_Open(va("%s/BattlEye/BEClient_" ARCH LIB_SUFFIX, FS_WriteDirectory()))))
{
// the BE "Init" export
qbyte (*Init)(void (*)(char *), void (*)(void *, size_t), qbyte (**)(void), void (**)(char *), void (**)(void *, size_t));
if ((Init = Sys_Library_ProcAddress(clbe.module, "Init")))
{
if (Init(&CL_BE_PrintMessage, &CL_BE_SendPacket, &clbe.Run, &clbe.Command, &clbe.NewPacket))
Com_Printf("BattlEye Client loaded\n");
else
{
CL_BE_Unload();
Com_Error(ERR_DROP, "Failed to initialize BattlEye Client");
}
}
else
{
CL_BE_Unload();
Com_Error(ERR_DROP, "Failed to get BattlEye Client procedure. %s", Sys_Library_ErrorString());
}
}
else
Com_Error(ERR_DROP, "Failed to load BattlEye Client. %s", Sys_Library_ErrorString());
}
Code:
if (Init(&CL_BE_PrintMessage, &CL_BE_SendPacket, &clbe.Run, &clbe.Command, &clbe.NewPacket))
Init() Routine, Part 2
The first argument passed to the Init() routine is the previously mentioned print function for "BattlEye Client: " messages which looks like:
Code:
static void CL_BE_PrintMessage(char *message)
{
Com_Printf(S_COLOR_RED "BattlEye Client" S_COLOR_WHITE ": %s\n", message);
}
Code:
char buffer[] = "BattlEye sucks";
__asm
{
lea eax,[buffer]
push eax
mov eax,0x5036E0
call eax
add esp,4
}
The next argument is CL_BE_SendPacket, implemented as follows:
Code:
static void CL_BE_SendPacket(void *packet, size_t len)
{
MSG_WriteByte(&cls.be.outMsg, clc_battleye);
MSG_WriteShort(&cls.be.outMsg, len);
MSG_CopyData(&cls.be.outMsg, packet, len);
}
The third argument for Init() is the most interesting (in the future, right now it's wasteland, just checking for updates). It is the first function BE delivers to Soldat and it is the address to the Run() function called every frame. Soldat calls it at 0x57496A:
Code:
.text:00574963 mov eax, ppBERun .text:00574968 mov eax, [eax] .text:0057496A call eax .text:0057496C test al, al .text:0057496E jnz loc_574A01
The forth argument is another address to a function BE gives out to Soldat but at the time of writing it is a nullsub, that is no code is executed there. We might find some code there in the future. It nontheless can be found at 0x100013E0.
The fifth and last argument is the address to the BE_New_Packet() function. When Soldat receives a packet from the BE server it is connected to, it doesn't parse the data on its own but passes it on to the BE client DLL. It is at 0x100013F0
So the fully commented call to the Init() function would be:
Code:
.text:00503A00 push offset pBENewPacket .text:00503A05 push offset pNullsub .text:00503A0A push offset pBERun .text:00503A0F push offset BESendPacket .text:00503A14 push offset BEPrintMessage .text:00503A19 call ds:BEInitRoutine .text:00503A1F add esp, 14h ; Clean up stack, init routine doesn't .text:00503A22 test al, al .text:00503A24 jz short InitFailed .text:00503A26 mov bBELoaded, 1 .text:00503A2D jmp short Success
Soldat stores pointers to the function pointers BE gives out and most of the time they are used. A quick list:
Code:
.data:005A1C60 ppBERun dd offset pBERun ; DATA XREF: sub_5732B0+16B3r .data:005A1BFC ppBENewPacket dd offset pBENewPacket ; DATA XREF: sub_5177B8+D014r .data:005A1AE0 ppNullsub dd offset pNullsub ; DATA XREF: sub_57A6C8+8EE9r
Very good question. I don't think it does anything so far besides checking for updates really.
I had a look at the Imports of the BE DLL and there is nothing really suspicious. I found calls to QueryPerformanceCounter(), GetTickCount() and a few others that could be used for anti cheat mechanisms but when I investigated more I found out they belong to a so-called security cookie introduced by Microsoft. They basically serve the plain purpose of building a small cookie that is written on the stack before every function call to detect buffer overflows. Cool technique by the way.
I also found IsDebuggerPresent() but both times it is called it is in an error function already so it doesn't get called in let's say BE_Run().
Talking about BE_Run(). I didn't find out what the code exactely does but it is very short and after having a quick look at it it only seems to check for updates. Nothing more.
The BENewPacket() function called when the BE servers starts to talk looks even less suspicious. No clue what it does but certainly no evil anti cheat hax:
Code:
.text:100013F0 BENewPacket: ; DATA XREF: Init+6Fo .text:100013F0 mov ecx, [esp+4] .text:100013F4 cmp byte ptr [ecx], 0 .text:100013F7 mov eax, 1 .text:100013FC ja short loc_10001404 .text:100013FE cmp [esp+8], eax .text:10001402 jz short locret_10001412 .text:10001404 .text:10001404 loc_10001404: ; CODE XREF: .text:100013FCj .text:10001404 cmp byte_10011E2C, 0 .text:1000140B jnz short locret_10001412 .text:1000140D mov byte_10011E2C, al .text:10001412 .text:10001412 locret_10001412: ; CODE XREF: .text:10001402j .text:10001412 ; .text:1000140Bj .text:10001412 retn
When the first version with integrated BattlEye support came out I tried a few ways to avoid it getting loaded. They all failed. Nopping out the DLL loading made Soldat crash a bit later, nopping the GetProcAddress call made Soldat crash. Everything inside the InitBE() routine seemed to lead to a crash. Today I just tried nopping out the BE_Run() command as this will surely be the place for future cheat checks and nothing crashed. So this means we load BE and even initialize it but if nobody tells it to check for cheats it doesn't! I can't say it is that easy as I don't know anything about client server communiciation.
In fact I'd like to know what the BE server says to the client. Is it sending raw code to be executed? Does it say "check the windows now"? I have no clue. As long as the BE server doesn't kick you for not responding, nopping out the BE_Run() call should work fine.
Everything about disabling BE is pure guesswork as we have no functional implementation. Let's see what the future brings.
That covers about everything I found out about BattlEye so far. I'd be glad to help you out with details or hints if you want to do further investigations. I guess and hope that as soon as BE is implemented, defeating it will be a community project.
BE Fix #1 06/20/07
I feel bad for releasing it already but it seems like it really works. I thought about releasing it to UG1 only but I already talked to Chrisgbk about how stupid the fix is so "they" basically know already
So let me get it straight: BE is stupid. I didn't even search for the exact location the memory is checked but I guess I'll have to for future fixes. Anyway. Every hack still works fine as long as it is not blocked by Soldat's own anti-cheat protection (that means it has to be renamed or homemade) and it is not modifying code. I didn't check wether Read/WriteProcessMemory really is hooked but I tend to say it isn't. DLL injection works fine as well.
Ok now th technique: From previous investigations I knew BE has to be called from Soldat. More precisely the function is the BE_Run() function. As I predicted in my previous paper I simply nopped out the call and that's it. Stupid, eh?
You have to understand: With BE_Run() being disabled (= not called) not a single check should be done! As long as they don't fix this, this fix will be always applicable!
I have a few ideas on how they will try to surpress this fix but I won't get into detail here.
Long story short:
Here's the fix:

Run it AFTER you started Soldat and BEFORE you join a BE protected server. Once it is run it will rip out the specific call and as long as you don't restart your Soldat should be cheatable.
And here the code:
Code:
%include "win32n.inc" EXTERN ExitProcess IMPORT ExitProcess kernel32.dll EXTERN FindWindowA IMPORT FindWindowA user32.dll EXTERN MessageBoxA IMPORT MessageBoxA user32.dll EXTERN MessageBoxA IMPORT MessageBoxA user32.dll EXTERN GetWindowThreadProcessId IMPORT GetWindowThreadProcessId user32.dll EXTERN OpenProcess IMPORT OpenProcess kernel32.dll EXTERN WriteProcessMemory IMPORT WriteProcessMemory kernel32.dll section .data USE32 SoldatTitle db "Soldat",0 ProcId dd 0 MagicOffset dd 0x57496A ErrorCaption db "Error",0 ErrorMessage db "An error occured.",0 SuccessCaption db "Done",0 SuccessMessage db "BE disabled",0 Fix db 0x90,0x90,0x84,0xC0,0xE9,0x8E,0x00,0x00,0x00,0x90 Len db $-Fix section .code USE32 ..start: push SoldatTitle push 0 call [FindWindowA] test eax,eax jz bailout push ProcId push eax call [GetWindowThreadProcessId] push dword [ProcId] push 0 push 0x0020+0x0008 ;PROCESS_VM_WRITE | PROCESS_VM_OPERATION call [OpenProcess] test eax,eax jz bailout push 0 push dword [Len] push Fix push dword [MagicOffset] push eax call [WriteProcessMemory] test eax,eax jz bailout push MB_OK push SuccessCaption push SuccessMessage push 0 call [MessageBoxA] jmp outtahere bailout: push MB_OK+MB_ICONERROR push ErrorCaption push ErrorMessage push 0 call [MessageBoxA] outtahere: push 0 call [ExitProcess]
BE Client Traffic
Today the spotlight of this paper will be on the BattlEye client traffic. Using my own packet sniffer from BEtray I logged the traffic and analyzed it while reading the disassembly of the BE_NewPacket routine. This is the result.
Basic Mechanism
The basic scheme of the traffic is a model based on packet length and contents. There are three different packets the server can send. They are 1, 5 or 9 bytes long. Sometimes the content of the packet is completely ignored and just the fact it is 1 byte for example triggers a response.
Sniffed Session
Here's a sniffed traffic session from when I joined a game. The last two packets were exchanged the rest of the time.
Code:
(>> = Received, << = Sent) 1. >> 0x00 2. << 0x00 0x00 3. >> 0x01 0x01 0x00 0x00 0x00 4. << 0x01 5. >> 0x01 0x00 0x10 0x40 0x00 0x00 0xC7 0x19 0x00 6. << 0xA2 0xAC 0x45 0xB0 0xA8 0x4B 0x74 0xA6 0xE9 0xDE 0x78 0xD8 0x36 0x97 0x80 0xF2
The 1-Byte Packet
As I mentioned before the single byte packet triggers a 2 byte packet with two zero bytes regardless of the content. I guess it is some "hello" or "I'm alive" packet to see if the peer is there (= BE client was loaded).
The 5-Byte Packet
The 5-byte packet is used to enable/disable self-checking in the BE client. There are two types of the 5-byte packet:
If the 2nd byte of it is a zero, the response is a single byte packet with a zero as well.
If the 2nd byte of it is a one, the response is a single byte packet with a one as well.
The 5 byte packet then changes a boolean I call "bSkipSelfcheck". If it is set to 1, no self-checking is performed. There is, however, another boolean that needs to be set in order to actually trigger self-checks. More on that now.
The 9-Byte Packet
The 9-byte packet is the most interesting one as it finally triggers a self-check. When I had a look at the contents of it I understood its structure quite fast, the code proved I was right:
Code:
0x01 0x00 0x10 0x40 0x00 0x00 0xC7 0x19 0x00
0x00 0x10 0x40 0x00
If you are familiar to numbers stored in memory you know you have to read them reversed and this becomes the dword 0x00401000 which is the image base address (if not changed) of every executable on windows. This made me suspect the first DWORD to be the base address of the MD5 self-check. The second DWORD (which is 0x0019C700) could not be the end address so it has to be the size of the area to be checked.
I then had a look at the code and I found:
Code:
.text:100017D2 mov ecx, [eax+1] .text:100017D5 mov edx, [eax+5] .text:100017D8 mov SelfcheckBase, ecx .text:100017DE mov SelfcheckSize, edx .text:100017E4 mov PerformSelfcheck, 1 .text:100017EB retn
From then on the BE client would send a 16-byte packet every few seconds and as I already mentioned in another paper this is the MD5 sum of the memory area specified in the 9-byte packet.
Conclusion
The traffic seems to be a bit weird to me but apart from that I have to admit I underestimated $able as I said "if he at least would tell the BE client which area of memory to check". Apparently, he does. Nontheless, so far I have not captured a single packet different from the above one which lead to the possibility to catch a good MD5 sum and inject it when needed. So we have to look out for changes here which will most probably appear soon.






