Battlefield 2 and the mystery of +password

Matthias HosteMatthias Hoste
8 min read

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.

0
Subscribe to my newsletter

Read articles from Matthias Hoste directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Matthias Hoste
Matthias Hoste