Register for your free account! | Forgot your password?

Go Back   elitepvpers > Coders Den > General Coding
You last visited: Today at 02:15

  • Please register to post and access all features, it's quick, easy and FREE!

Advertisement



BattlEye reverse engineered

Discussion on BattlEye reverse engineered within the General Coding forum part of the Coders Den category.

Reply
 
Old   #1
 
elite*gold: 0
Join Date: Apr 2007
Posts: 66
Received Thanks: 15
Good day. I actually feel like there are not many skilled people here and this makes me tend to avoid the forums. I think though I should give something on my own in order to expect others to give information.
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&#59; "BattlEye/BEClient_x86.dll"
.text:005039BD         call  @@LStrCat3   &#59; Build full path to BEClient.dll
.text:005039C2         mov   eax, [ebp+var_4]
.text:005039C5         call  @@LStrToPChar &#59; Convert delphi path string to C string
.text:005039CA         push  eax      &#59; lpLibFileName
.text:005039CB         call  LoadLibraryA  &#59; Load it
.text:005039D0         mov   hBEClientDll, eax&#59; Store handle to DLL
.text:005039D5         cmp   hBEClientDll, 0
.text:005039DC         jnz   short DllLoaded
Here we see Soldat building a correct path to the DLL and here

Code:
.text:005039E2         push  offset aInit  &#59; "Init"
.text:005039E7         mov   eax, hBEClientDll
.text:005039EC         push  eax      &#59; hModule
.text:005039ED         call  GetProcAddress_0&#59; Get proc address for Init() routine
.text:005039F2         mov   ds:BEInitRoutine, eax&#59; Store it
.text:005039F7         cmp   ds:BEInitRoutine, 0
.text:005039FE         jz   short InitNotFound
how the Init() routine proc address is retrieved. Nothing special.

The UnloadBE routine is even less spectacular:

Code:
.text:00503B8C UnloadBE    proc near       &#59; CODE XREF: sub_5177B8+1923p
.text:00503B8C                    &#59; 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      &#59; hLibModule
.text:00503BA2         call  FreeLibrary_0
.text:00503BA7         xor   eax, eax
.text:00503BA9         mov   hBEClientDll, eax
.text:00503BAE
.text:00503BAE locret_503BAE:             &#59; CODE XREF: UnloadBE+Ej
.text:00503BAE         retn
.text:00503BAE UnloadBE    endp
The Init() Routine

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]&#59; 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
As we can see here, 5 addresses are pushed onto the stack and Init() is called then. We also see Init() doesn't clean up the stack. Afterwards is a check if an error occurred and if not some bool value is set to one. I called it bBELoaded in the IDA code.

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

[...]

}
We can see a clbe.Run() function is called every frame (it is in the routine CL_Frame).

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());
}
which is of no special interested, just for completeness' sake here and now the key for our problem, the CL_BE_Load() routine:

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());
}
It looks pretty similar to what we found in Soldat. It loads the DLL and then gets the proc address and THEN calls Init():

Code:
if (Init(&CL_BE_PrintMessage, &CL_BE_SendPacket, &clbe.Run, &clbe.Command, &clbe.NewPacket))
Feels like birthday. Open source is great. The variable names say it all and looking at the routines itself was even more enlightening.

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);
}
It only accepts one argument, the message as C string so if you want to try it:

Code:
	char buffer[] = "BattlEye sucks";

	__asm
	{
 lea eax,[buffer]
 push eax
 mov eax,0x5036E0
 call eax
 add esp,4
	}
from the inside of Soldat (injected DLL for example) will print "BattlEye Client: BattlEye sucks".

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 interesting thing is that the DLL itself obviously doesn't handle the data sending/receiving itself but the game does. It passes messages it wants to be sent to the server on to the game!

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 Run() function itself can be found at 0x100013B0 in the BE DLL.

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    &#59; 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
Awesome.
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    &#59; DATA XREF: sub_5732B0+16B3r
.data:005A1BFC ppBENewPacket  dd offset pBENewPacket &#59; DATA XREF: sub_5177B8+D014r
.data:005A1AE0 ppNullsub    dd offset pNullsub   &#59; DATA XREF: sub_57A6C8+8EE9r
And what does BE do?

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:              &#59; 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:             &#59; 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:            &#59; CODE XREF: .text:10001402j
.text:10001412                    &#59; .text:1000140Bj
.text:10001412         retn
Disabling BattlEye

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 I even talked about this possible mechanism in the previous paper...

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	&#59;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: Spotlight on traffic

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 Packets in Detail

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
Nine bytes. What could that be? Two DWORDs and a header byte and indeed if you look at the 4 bytes after the 0x01 you see:

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
Which copies the addresses of the two DWORDs into some vars for later. It also sets the second of the two booleans to trigger self-checking to true (of course I named the vars like that...).

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.
wiz is offline  
Thanks
1 User
Old 08/31/2007, 13:34   #2
 
elite*gold: 0
Join Date: Aug 2007
Posts: 1
Received Thanks: 0
The link do not work .
Reupload it please!!!
Hectorzx is offline  
Old 08/31/2007, 13:49   #3
 
elite*gold: 0
Join Date: Aug 2007
Posts: 1
Received Thanks: 0
Wiz,your work are great...
But the file are removed. Plz upload it again or if someone have the fix,
email me:
Thnx for the ppl who helps me and Hectorzx
X_HuNTeR_X is offline  
Reply


Similar Threads Similar Threads
[FUN]Cam reverse =D
08/31/2010 - S4 League Hacks, Bots, Cheats & Exploits - 14 Replies
Hello everybody I is again = D here with another hack using CE let go then = D 1-Open suspend HGWC, S4Client.exe and Xtrap 2-Open s4 league and start 3-Suspend... 4-Open CE and Search: 4/1-value between 60 e 60 4/2-Value type float 5-take the values 6-put 6000000
(FUN)Cam reverse
08/30/2010 - S4 League - 1 Replies
Portuguese: Eu descobri um hack que deixa a camera ao contrario(reverse) fotos no fim do topico English: I found a hack that lets the camera on the contrary (reverse) photos at the end of the topic German(Deutsch): Ich fand einen Hack, lässt die Kamera auf das Gegenteil (Reverse)
DB-Bot and reverse
08/01/2010 - SRO Private Server - 1 Replies
Hey can anyone tell me why i cant use Reverse when i bot with db bot?? the is always the message cannot find target
reverse at DB BOT
06/03/2010 - SRO Private Server - 2 Replies
i cant use my reverse with DB BOT i Play on sjsro old With The ZSZC client
Reverse Hacking
09/02/2008 - RFO Hacks, Bots, Cheats, Exploits & Guides - 11 Replies
I just discoverd a very entertaining new hack you can frame people for things by only searching their name and editing values just as you would do for yourself in the fly hack. They must be visible while searching but you can totly make people look as though their dmg hacking or fly hacking or pothacking. Have fun destroy your enemies :eek:



All times are GMT +1. The time now is 02:15.


Powered by vBulletin®
Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
SEO by vBSEO ©2011, Crawlability, Inc.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

Support | Contact Us | FAQ | Advertising | Privacy Policy | Terms of Service | Abuse
Copyright ©2025 elitepvpers All Rights Reserved.