Battlefield 2 and the mystery of +password
I recently received a message from a member of the Battlefield 2 community asking if I could fix an issue the game had
Fixing the server password flag would something I'd be interested in ๐ BF2 is super stupid in that regard. It has a flag to pass the game server password, but the somewhat ignores the flag and just tries to connect to the server as if it had no password. 1942, Vietnam and 2142 all just prompt for the password if none is given, BF2 does not even work when you give it the password.
Namely, the +password
flag. The game supports joining servers via the command line or via startup parameters with the +joinServer
flag. Now servers can be password protected so +password
complements this feature to allow you to pass the password as well. Well it would though it seems to be completely ignored.
I was curious, so I fired up Ida Pro to take a look and after some searching I came across dice::hfe::BF2Engine::parseParameters
now this looks promising.
And well, this here even more.
std::string::string(v155, "Set the server password when joining a server");
std::string::string(v156, "password");
sub_45CE50(5, v156, v155);
case 5:
std::string::string(v65, "GSPassword");
dice::hfe::settings->setStringSetting(
dice::hfe::settings,
v65,
v5);
v7 = v65;
break;
So, the game internally translates +password to a setting called GSPassword
. Let's search for that with Ida.
Alright 4 references(function names were added manually), we came from parseParameters
so let's see what the others do.
sub_405C10
seems to handle a callback or call into from Swiffplayer.dll which is the game's dll that handles the main menu which runs in Flash with actionscript 2.sub_488320
appears to handle some retrieving for the NetClient
class. This sounds interesting.startGame
this is on the same level as the NetClient
class. Except that startGame
retrieves a lot more information and makes this very interesting call.
gameClient = dice::hfe::game->pVMT->queryInterface(
dice::hfe::game,
120002);
if ( !gameClient || (_BYTE)a2 )
return v52;
std::string::string(v44, "127.0.0.1");
std::string::string(v49, "GSJoinAddress");
v51 = dice::hfe::settings->getSettingStringValue(
dice::hfe::settings,
v49,
v44);
std::string::~string(v49);
std::string::~string(v44);
std::string::string(v44, byte_87FB54);
std::string::string(v49, "GSPassword");
password = dice::hfe::settings->getSettingStringValue(
dice::hfe::settings,
v49,
v44);
std::string::~string(v49);
std::string::~string(v44);
a3 = 16567;
std::string::string(v49, "GSPort");
v28 = dice::hfe::settings->getSettingIntValue(
dice::hfe::settings,
v49,
&a3);
std::string::~string(v49);
HIBYTE(a3) = 0;
std::string::string(v49, "GSClPunkBuster");
LOBYTE(a2) = dice::hfe::settings->getSettingBoolValue(
dice::hfe::settings,
v49,
(char *)&a3 + 3);
std::string::~string(v49);
v37 = v28;
v29 = v51;
v30 = gameClient->pVMT->startClient(//<-- starts the client aka connects
gameClient,
v51,
v37,
password,
1084227584,
a2);
startClient
, now what could this be doing?
if ( this->netInterface )
return 0;
netClient = bf_malloc(0x158);
if ( netClient )
netClient = dice::hfe::io::NetClient::NetClient(netClient);
else
netClient = 0;
this->netInterface = netClient;
NetVersionNumber = dice::hfe::BuildNrUtil::getNetVersionNumber();
this->netInterface->setVersion(this->netInterface, NetVersionNumber);
netInterface = this->netInterface;
if ( !netInterface )
return 0;
v33 = a2;
this->netInterface->init(this->netInterface, 1);
Jackpot, it initializes the NetClient
class. And a bit further down it also calls the function we have been looking for
v16 = this->netInterface->joinServer(
this->netInterface,
a3,
ArgList,
1,
password_param,
(int)(a6 * 1000.0),
v38,
anticheat_param);
The password is passed into this function
netClient = this;
lpParameter = this;
if ( (_BYTE)a4 )
{
v9 = port;
v26 = port;
v25 = port;
do
{
anticheat_param_2 = anticheat_param;
v20 = a7;
v19 = a6;
password_param_2 = password_param;
v17 = a4;
v23 = &v16;
if ( !dword_981100 && dword_981104 == dword_981100 )
{
v13 = sub_6BC510(v9, v9);
}
else
{
v10 = dword_981188;
v11 = dword_98118C;
v12 = (int (__cdecl *)(int, int, int *, int))dword_981100;
sub_6BD940(v22, &v25, &v26);
v13 = v12(v10, v11, v22, (int)v22 >> 31);
netClient = (dice::hfe::io::NetClient *)lpParameter;
v9 = port;
}
v23->HighPart = v13;
v14 = sub_6BEB80(netClient, a2, v16, v17, password_param_2, v19, v20, anticheat_param_2);
}
while ( v14 == 18 );
}
else
{
v16.HighPart = sub_6BDE40(port, port);
v14 = sub_6BEB80(netClient, a2, v16, 0, password_param, a6, a7, anticheat_param);
}
if ( !v14 && BYTE1(netClient->dword110) )
{
if ( !sub_6BE290(netClient) )
{
((void (__thiscall *)(dice::hfe::io::NetClient *))netClient->pVMT->sub_6BC0B0)(netClient);
return 9;
}
Sleep(0x32u);
}
return v14;
We have some more code in here, but the main point of interest seems to be sub_6BEB80
which is called regardless of a4
and whose result would seem to be rather important.
password = password_param;
v10 = *(_DWORD *)this->gapC8;
if ( *(_DWORD *)(v10 + 160) != 1 )
{
sub_6D60F0(v10);
*(_DWORD *)(*(_DWORD *)this->gapC8 + 160) = 1;
LOBYTE(this->dword11C) = 0;
if ( !dice::hfe::io::socketManager )
return 6;
if ( *(_DWORD *)(password + 20) >= 0x20u )
return 17;
if ( *(_DWORD *)(a2 + 24) < 0x10u )
LOBYTE(v12) = a2 + 4;
else
v12 = *(_DWORD *)(a2 + 4);
sub_6BDEA0(PerformanceCount.HighPart, PerformanceCount.HighPart);
sub_4310D0((int)v23, "%s:%d", v12);
if ( !this->udpSocket )
{
v13 = dice::hfe::io::socketManager->createSocket(
dice::hfe::io::socketManager,
v23,
a4);
this->udpSocket = v13;
if ( !v13 )
{
std::string::~string(v23);
return 4;
}
}
*(_DWORD *)this->gapFC = 0;
QueryPerformanceCounter((LARGE_INTEGER *)((char *)&this->large_integerE8.QuadPart + 4));
QueryPerformanceFrequency((LARGE_INTEGER *)((char *)&this->large_integerF0.QuadPart + 4));
this->dwordC4 = 1499034250;
this->gapB4[8] = 1;
std::string::~string(v23);
}
It first initializes a socket if it hadn't been initialized already and then uses the password and anticheat flag here
sub_6BE9E0(this, anticheat, (bf2_basic_string *)password, a7)
Which then does
if ( *((_DWORD *)password_1 + 6) < 0x10u )
v5 = (char *)password_1 + 4;
else
v5 = (const char *)*((_DWORD *)password_1 + 1);
strncpy(password, v5, 0x20u);
if ( *(_DWORD *)(a4 + 24) < 0x10u )
v6 = (const char *)(a4 + 4);
else
v6 = *(const char **)(a4 + 4);
strncpy(v18, v6, 4u);
std::string::string(v16, "mods/bf2");
std::string::string(v17, "GSModDirectory");
v7 = dice::hfe::settings->getSettingStringValue(dice::hfe::settings, v17, v16);
if ( *(_DWORD *)(v7 + 24) < 0x10u )
v8 = (const char *)(v7 + 4);
else
v8 = *(const char **)(v7 + 4);
strncpy(modName, v8, 0x20u);
std::string::~string(v17);
std::string::~string(v16);
dice::hfe::io::BitStream::setBuffer(this->bitStreamOut, this->dword104, 1473);
sub_6B6680(this->bitStreamOut, 0);
dice::hfe::io::BitStream::writeBasicHeader(this->bitStreamOut, 1u, 1);
bitStreamOut = this->bitStreamOut;
password_1 = (bf2_basic_string *)4098;
dice::hfe::io::BitStream::writeBits(bitStreamOut, &password_1, 32);
v10 = (bf2_basic_string *)sub_6BDEA0(this->netVersion, this->netVersion);
v11 = this->bitStreamOut;
password_1 = v10;
dice::hfe::io::BitStream::writeBits(v11, &password_1, 32);
dice::hfe::io::BitStream::writeBits(this->bitStreamOut, &anticheat, 1);
dice::hfe::io::BitStream::writeBits(this->bitStreamOut, v18, 32);
dice::hfe::io::BitStream::writeBits(this->bitStreamOut, password, 256);
dice::hfe::io::BitStream::writeBits(this->bitStreamOut, modName, 256);
v12 = -this->udpSocket->send(
this->udpSocket,
*(_DWORD *)this->bitStreamOut,
(*(_DWORD *)(this->bitStreamOut + 24) >> 3) + ((*(_DWORD *)(this->bitStreamOut + 24) & 7) != 0)) != 0);
LOBYTE(v12) = v12 & 0xFA;
return v12 + 6;
Eureka, this is writing bits and then sending them out. We have found where the game sends it's ConnectionRequest
which is where the password has to be passed.
Debugging revealed that the password is actually never passed to startClient
so somewhere along the lines, most definitely in Swiffplayer.dll, the value is cleared. Now we can go into another rabbit hole to find out why it does that and how it can be fixed. But the community member just wants it to work and I have already spent a good few hours on this so let us keep Swiffplayer.dll for a later date and patch the problem in the main executable.
So with that out of the way, let us patch this in NetClient->joinServer
using our good old Microsoft Detours.
The resulting patch looks like this
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include "Tools.h"
#include <vector>
#include <string>
#include "detours.h"
#pragma comment(lib, "detours.lib")
static char* BF2ServerPassword;
typedef int(__thiscall* __real_NetClient_JoinServer)(DWORD* _this, int a2, int port, int a4, bf2string* password_param, float a6, int a7, int anticheat_param);
__real_NetClient_JoinServer realNetClientJoinServer;
void parseArguments()
{
LPWSTR params = GetCommandLine();
std::wstring separators = L" ";
std::vector<std::wstring> args = splitManyW(params, separators);
std::vector<std::wstring> arguments = std::vector<std::wstring>();
for (int i = 0; i < args.size(); i++)
{
if (args[i].substr(0, 9) == L"+password")
{
arguments.push_back(args[i]);
arguments.push_back(args[i + 1]);
}
}
for (int i = 0; i < arguments.size(); i++)
{
if (arguments[i] == L"+password")
{
const wchar_t* password = (wchar_t*)arguments[i + 1].c_str();
size_t requiredSize = std::wcstombs(NULL, password, 0) + 1;
BF2ServerPassword = (char*)malloc(requiredSize);
std::wcstombs(BF2ServerPassword, password, requiredSize);
}
}
}
int __fastcall DetourNetClientJoinServer(DWORD* _this, DWORD EDX, int a2, int port, int a4, bf2string* password_param, float a6, int a7, int anticheat_param)
{
if (BF2ServerPassword && (!password_param || password_param->size() == 0))
{
bf2string password = bf2string(BF2ServerPassword);
return realNetClientJoinServer(_this, a2, port, a4, &password, a6, a7, anticheat_param);
}
return realNetClientJoinServer(_this, a2, port, a4, password_param, a6, a7, anticheat_param);
}
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
parseArguments();
realNetClientJoinServer = (__real_NetClient_JoinServer)DetourFunction((PBYTE)0x6BEE80, (PBYTE)DetourNetClientJoinServer);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
You can find the source code for this at my GitHub.
I will briefly explain how it is structured and why. I went with the argument method because I will be visiting this game again in the future and I'd like to make it open enough to add more fixes as I find issues. I used the default +password
parameter so that players don't have to change anything outside of injecting this dll on startup. I wrote the detour using the Microsoft Detours library which is excellent for making simple detours like this. Now the keen eyed among you may have noticed that some things seem off. Namely the typedef claims this function is a thiscall but my detour is a fastcall, my detour has an extra argument named EDX
, the password is of type bf2string
and I hardcoded the address.
The detour uses fastcall because thiscall is a class member specific function call. And since the detour is not a part of this class we use __fastcall
to ensure the _this
argument enters correctly via ECX
. The side effect being that we also get the EDX
register which we just add but ignore.
The game uses a debug version of std::string
, no clue why but this has caused me to bash my head a few times on why the char pointers looked so out of place. Now the solution came from an odd place, namely a place for cheats. Unknowncheats.
That said whilst the goal of releasing this information was to make cheats, which is inherently wrong, it has helped us to crack this issue.
I hardcoded the address because the game is so old it does not use ASLR. Makes it easy for us.
That being said, I hope this was an informative journey and that you learned something along the way. There are sure to be more things on the way but I love puzzles like these.
Subscribe to my newsletter
Read articles from Matthias Hoste directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by