From d478e9b0beb4edf2c985e84d366c2858b6db63ca Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Wed, 27 Dec 2023 23:08:17 -0800 Subject: [PATCH] implement BB client's config scramble logic --- src/CommandFormats.hh | 37 +++++++++--------- src/ProxyServer.cc | 2 +- src/ReceiveCommands.cc | 88 ++++++++++++++++++++++++++++-------------- src/ReplaySession.cc | 4 +- src/SendCommands.cc | 2 +- 5 files changed, 81 insertions(+), 52 deletions(-) diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index ff8fca3e..f6ebe213 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1684,12 +1684,12 @@ struct C_LoginExtendedV1_DC_93 : C_LoginV1_DC_93 { // 93 (C->S): Log in (BB) -struct C_Login_BB_93 { +struct C_LoginBase_BB_93 { le_uint32_t player_tag = 0x00010000; le_uint32_t guild_card_number = 0; le_uint32_t sub_version = 0; - uint8_t language; - int8_t character_slot; + uint8_t language = 0; + int8_t character_slot = 0; // Values for connection_phase: // 00 - initial connection (client will request system file, characters, etc.) // 01 - choose character @@ -1697,9 +1697,9 @@ struct C_Login_BB_93 { // 03 - apply updates from dressing room // 04 - login server // 05 - lobby server (and beyond) - uint8_t connection_phase; - uint8_t client_code; - le_uint32_t team_id = 0; + uint8_t connection_phase = 0; + uint8_t client_code = 0; + le_uint32_t security_token = 0; pstring username; pstring password; @@ -1708,20 +1708,21 @@ struct C_Login_BB_93 { // doesn't use it anyway). le_uint32_t menu_id = 0; le_uint32_t preferred_lobby_id = 0; +} __packed__; +struct C_LoginWithoutHardwareInfo_BB_93 : C_LoginBase_BB_93 { // Note: Unlike other versions, BB puts the version string in the client // config at connect time. So the first time the server gets this command, it - // will be something like "Ver. 1.24.3". Note also that some old versions - // (before 1.23.8?) omit the hardware_info field before the client config, so - // the client config starts 8 bytes earlier on those versions and the entire - // command is 8 bytes shorter, hence this odd-looking union. - union VariableLengthSection { - parray old_client_config; - struct NewFormat { - parray hardware_info; - parray client_config; - } __packed__ new_clients; - } __packed__ var; + // will be something like "Ver. 1.24.3". This format is used on older client + // versions (before 1.23.8?) + parray client_config; +} __packed__; + +struct C_LoginWithHardwareInfo_BB_93 : C_LoginBase_BB_93 { + // See the comment in the above structure. This format is used on newer client + // versions. + parray hardware_info; + parray client_config; } __packed__; // 94: Invalid command @@ -3080,7 +3081,7 @@ struct S_ClientInit_BB_00E6 { le_uint32_t error_code = 0; le_uint32_t player_tag = 0x00010000; le_uint32_t guild_card_number = 0; - le_uint32_t team_id = 0; + le_uint32_t security_token = 0; parray client_config; uint8_t can_create_team = 1; uint8_t episode_4_unlocked = 1; diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index a905a18a..86555a2f 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -372,7 +372,7 @@ void ProxyServer::UnlinkedSession::on_input(Channel& ch, uint16_t command, uint3 if (command != 0x93) { throw runtime_error("command is not 93"); } - const auto& cmd = check_size_t(data); + const auto& cmd = check_size_t(data, 0xFFFF); try { ses->license = s->license_index->verify_bb(cmd.username.decode(), cmd.password.decode()); } catch (const LicenseIndex::missing_license&) { diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index a26395f2..b15732f1 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -1043,24 +1043,58 @@ static void on_9E_XB(shared_ptr c, uint16_t, uint32_t, string& data) { send_command(c, 0x9F, 0x00); } +static void scramble_bb_security_data(parray& data, uint8_t which, bool reverse) { + static const uint8_t forward_orders[8][5] = { + {2, 0, 1, 4, 3}, + {3, 4, 0, 1, 2}, + {2, 3, 4, 0, 1}, + {2, 3, 0, 1, 4}, + {0, 2, 3, 4, 1}, + {1, 4, 2, 3, 0}, + {2, 0, 1, 4, 3}, + {1, 0, 3, 4, 2}, + }; + static const uint8_t reverse_orders[8][5] = { + {1, 2, 0, 4, 3}, + {2, 3, 4, 0, 1}, + {3, 4, 0, 1, 2}, + {2, 3, 0, 1, 4}, + {0, 4, 1, 2, 3}, + {4, 0, 2, 3, 1}, + {1, 2, 0, 4, 3}, + {1, 0, 4, 2, 3}, + }; + const auto& order = reverse ? reverse_orders[which & 7] : forward_orders[which & 7]; + parray scrambled_data; + for (size_t z = 0; z < 5; z++) { + for (size_t x = 0; x < 8; x++) { + scrambled_data[(z * 8) + x] = data[(order[z] * 8) + x]; + } + } + data = scrambled_data; +} + static void on_93_BB(shared_ptr c, uint16_t, uint32_t, string& data) { - const auto& cmd = check_size_t(data, sizeof(C_Login_BB_93) - 8, sizeof(C_Login_BB_93)); + const auto& base_cmd = check_size_t(data, 0xFFFF); auto s = c->require_server_state(); - bool is_old_format; - if (data.size() == sizeof(C_Login_BB_93) - 8) { - is_old_format = true; - } else if (data.size() == sizeof(C_Login_BB_93)) { - is_old_format = false; - } else { - throw runtime_error("invalid size for 93 command"); + parray config_data = (data.size() == sizeof(C_LoginWithoutHardwareInfo_BB_93)) + ? check_size_t(data).client_config + : check_size_t(data).client_config; + + // If security_token is zero, the game scrambles the client config data based + // on the first character in the username. We undo the scramble here. + if (base_cmd.security_token == 0) { + scramble_bb_security_data(config_data, base_cmd.username.at(0), true); } - c->config.set_flags_for_version(c->version(), cmd.sub_version); - c->channel.language = cmd.language; + c->config.set_flags_for_version(c->version(), base_cmd.sub_version); + c->channel.language = base_cmd.language; + string username = base_cmd.username.decode(); + string password = base_cmd.password.decode(); try { - auto l = s->license_index->verify_bb(cmd.username.decode(), cmd.password.decode()); + auto l = s->license_index->verify_bb(username, password); c->set_license(l); } catch (const LicenseIndex::no_username& e) { @@ -1080,9 +1114,9 @@ static void on_93_BB(shared_ptr c, uint16_t, uint32_t, string& data) { return; } else { auto l = s->license_index->create_license(); - l->serial_number = fnv1a32(cmd.username.decode()) & 0x7FFFFFFF; - l->bb_username = cmd.username.decode(); - l->bb_password = cmd.password.decode(); + l->serial_number = fnv1a32(username) & 0x7FFFFFFF; + l->bb_username = username; + l->bb_password = password; s->license_index->add(l); if (!s->is_replay) { l->save(); @@ -1093,16 +1127,10 @@ static void on_93_BB(shared_ptr c, uint16_t, uint32_t, string& data) { } } - try { - if (is_old_format) { - c->config.parse_from(cmd.var.old_client_config); - } else { - c->config.parse_from(cmd.var.new_clients.client_config); - } - } catch (const invalid_argument&) { - string version_string = is_old_format - ? cmd.var.old_client_config.as_string() - : cmd.var.new_clients.client_config.as_string(); + if (base_cmd.guild_card_number != 0) { + c->config.parse_from(config_data); + } else { + string version_string = config_data.as_string(); strip_trailing_zeroes(version_string); // Note: Tethealla PSOBB is actually Japanese PSOBB, but with most of the // files replaced with English text/graphics/etc. For this reason, it still @@ -1113,17 +1141,17 @@ static void on_93_BB(shared_ptr c, uint16_t, uint32_t, string& data) { c->config.set_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB); } } - c->channel.language = c->config.check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB) ? 1 : cmd.language; - c->bb_connection_phase = cmd.connection_phase; - c->bb_character_index = cmd.character_slot; + c->channel.language = c->config.check_flag(Client::Flag::FORCE_ENGLISH_LANGUAGE_BB) ? 1 : base_cmd.language; + c->bb_connection_phase = base_cmd.connection_phase; + c->bb_character_index = base_cmd.character_slot; - if (cmd.menu_id == MenuID::LOBBY) { - c->preferred_lobby_id = cmd.preferred_lobby_id; + if (base_cmd.menu_id == MenuID::LOBBY) { + c->preferred_lobby_id = base_cmd.preferred_lobby_id; } send_client_init_bb(c, 0); - if (cmd.guild_card_number == 0) { + if (base_cmd.guild_card_number == 0) { // On first login, send the client to the data server port send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at("bb-data1")->port); diff --git a/src/ReplaySession.cc b/src/ReplaySession.cc index 34970593..712b1502 100644 --- a/src/ReplaySession.cc +++ b/src/ReplaySession.cc @@ -203,7 +203,7 @@ void ReplaySession::check_for_password(shared_ptr ev) const { if (header.command == 0x04) { check_pw(check_size_t(cmd_data, cmd_size).password.decode()); } else if (header.command == 0x93) { - check_pw(check_size_t(cmd_data, cmd_size).password.decode()); + check_pw(check_size_t(cmd_data, cmd_size, 0xFFFF).password.decode()); } else if (header.command == 0x9C) { check_pw(check_size_t(cmd_data, cmd_size).password.decode()); } else if (header.command == 0x9E) { @@ -420,7 +420,7 @@ void ReplaySession::apply_default_mask(shared_ptr ev) { } case 0x00E6: { auto& mask = check_size_t(mask_data, mask_size); - mask.team_id = 0; + mask.security_token = 0; break; } } diff --git a/src/SendCommands.cc b/src/SendCommands.cc index 951a0c4e..b761796f 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -506,7 +506,7 @@ void send_client_init_bb(shared_ptr c, uint32_t error_code) { cmd.error_code = error_code; cmd.player_tag = 0x00010000; cmd.guild_card_number = c->license->serial_number; - cmd.team_id = team ? team->team_id : 0; + cmd.security_token = team ? team->team_id : 0; c->config.serialize_into(cmd.client_config); cmd.can_create_team = 1; cmd.episode_4_unlocked = 1;