improve BB proxy functionality

This commit is contained in:
Martin Michelsen
2025-05-26 18:43:46 -07:00
parent 2e158a1df8
commit 33b0ab3ed3
8 changed files with 83 additions and 68 deletions
+13
View File
@@ -663,6 +663,19 @@ Action a_generate_pc_v2_registry(
write_output_data(args, output_data.data(), output_data.size(), "reg");
});
Action a_encrypt_challenge_time(
"encrypt-challenge-time", nullptr, +[](phosg::Arguments& args) {
uint16_t time = args.get<uint16_t>(1);
uint32_t ret = encrypt_challenge_time(time);
phosg::fwrite_fmt(stderr, "{} => {:08X}\n", phosg::format_duration(time * 1000000), ret);
});
Action a_decrypt_challenge_time(
"decrypt-challenge-time", nullptr, +[](phosg::Arguments& args) {
uint32_t time = args.get<uint32_t>(1, phosg::Arguments::IntFormat::HEX);
uint16_t ret = decrypt_challenge_time(time);
phosg::fwrite_fmt(stderr, "{:08X} => {}\n", time, phosg::format_duration(ret * 1000000));
});
Action a_encrypt_challenge_data(
"encrypt-challenge-data", nullptr, +[](phosg::Arguments& args) {
string data = read_input_data(args);
+21 -20
View File
@@ -706,27 +706,28 @@ void PSOBBMultiKeyDetectorEncryption::encrypt(void* data, size_t size) {
}
void PSOBBMultiKeyDetectorEncryption::decrypt(void* data, size_t size) {
if (!this->active_crypt.get()) {
if (size != 8) {
throw logic_error("initial decryption size does not match expected first data size");
}
for (const auto& key : this->possible_keys) {
this->active_key = key;
this->active_crypt = make_shared<PSOBBEncryption>(*this->active_key, this->seed.data(), this->seed.size());
string test_data(reinterpret_cast<const char*>(data), size);
this->active_crypt->decrypt(test_data.data(), test_data.size());
if (this->expected_first_data.count(test_data)) {
break;
}
this->active_key.reset();
this->active_crypt.reset();
}
if (!this->active_crypt.get()) {
throw runtime_error("none of the registered private keys are valid for this client");
}
if (this->active_crypt.get()) {
this->active_crypt->decrypt(data, size);
return;
}
this->active_crypt->decrypt(data, size);
if (size != 8) {
throw logic_error("initial decryption size does not match expected first data size");
}
for (const auto& key : this->possible_keys) {
this->active_key = key;
this->active_crypt = make_shared<PSOBBEncryption>(*this->active_key, this->seed.data(), this->seed.size());
string test_data(reinterpret_cast<const char*>(data), size);
this->active_crypt->decrypt(test_data.data(), test_data.size());
if (this->expected_first_data.count(test_data)) {
memcpy(data, test_data.data(), size);
return;
}
this->active_key.reset();
this->active_crypt.reset();
}
throw runtime_error("none of the registered private keys are valid for this client");
}
PSOEncryption::Type PSOBBMultiKeyDetectorEncryption::type() const {
+15 -21
View File
@@ -356,11 +356,11 @@ static asio::awaitable<HandlerResult> S_B_03(shared_ptr<Client> c, Channel::Mess
resp.character_slot = c->bb_character_index;
resp.connection_phase = c->bb_connection_phase;
resp.client_code = c->bb_client_code;
resp.security_token = c->proxy_session->remote_bb_security_token;
resp.security_token = c->bb_security_token;
resp.username.encode(c->username, c->language());
resp.password.encode(c->password, c->language());
resp.hardware_id = c->hardware_id;
resp.client_config = c->proxy_session->remote_client_config_data;
resp.client_config = c->bb_client_config;
if (c->proxy_session->enable_remote_ip_crc_patch) {
*reinterpret_cast<le_uint32_t*>(resp.client_config.data() + 0x10) =
c->proxy_session->remote_ip_crc ^ (1309539928UL + 1248334810UL);
@@ -373,27 +373,17 @@ static asio::awaitable<HandlerResult> S_B_03(shared_ptr<Client> c, Channel::Mess
static asio::awaitable<HandlerResult> S_B_E6(shared_ptr<Client> c, Channel::Message& msg) {
const auto& cmd = msg.check_size_t<S_ClientInit_BB_00E6>(0xFFFF);
c->proxy_session->remote_guild_card_number = cmd.guild_card_number;
c->proxy_session->remote_bb_security_token = cmd.security_token;
c->proxy_session->remote_client_config_data = cmd.client_config;
c->bb_security_token = cmd.security_token;
c->bb_client_config = cmd.client_config;
auto s = c->require_server_state();
auto& pc = s->proxy_persistent_configs[c->login->account->account_id];
pc.account_id = c->login->account->account_id;
pc.remote_guild_card_number = c->proxy_session->remote_guild_card_number;
pc.remote_bb_security_token = c->proxy_session->remote_bb_security_token;
pc.remote_client_config_data = c->proxy_session->remote_client_config_data;
pc.enable_remote_ip_crc_patch = c->proxy_session->enable_remote_ip_crc_patch;
c->log.info_f("Updated persistent config for proxy session");
if ((c->bb_connection_phase == 0) && c->proxy_session->received_reconnect) {
c->proxy_session->server_channel->send(0x00E0); // Request system file
}
co_return HandlerResult::SUPPRESS;
}
static asio::awaitable<HandlerResult> C_B_E0(shared_ptr<Client>, Channel::Message&) {
co_return HandlerResult::SUPPRESS;
co_return HandlerResult::FORWARD;
}
static asio::awaitable<HandlerResult> S_V123_04(shared_ptr<Client> c, Channel::Message& msg) {
@@ -748,18 +738,18 @@ static asio::awaitable<HandlerResult> S_G_E4(shared_ptr<Client> c, Channel::Mess
static asio::awaitable<HandlerResult> S_B_22(shared_ptr<Client> c, Channel::Message& msg) {
// We use this command (which is sent before the init encryption command) to
// detect a particular server behavior that we'll have to work around later.
// It looks like this command's existence is another anti-proxy measure, since
// It looks like this command's existence is an anti-proxy measure, since
// this command is 0x34 bytes in total, and the logic that adds padding bytes
// when the command size isn't a multiple of 8 is only active when encryption
// is enabled. Presumably some simpler proxies would get this wrong.
// Editor's note: There's an unsavory message in this command's data field,
// hence the hash here instead of a direct string comparison. I'd love to hear
// the story behind why they put that string there.
// hence the hash here instead of a direct string comparison. I'd love to
// hear the story behind why they put that string there.
if ((msg.data.size() == 0x2C) && (phosg::fnv1a64(msg.data.data(), msg.data.size()) == 0x8AF8314316A27994)) {
c->log.info_f("Enabling remote IP CRC patch");
c->proxy_session->enable_remote_ip_crc_patch = true;
}
co_return HandlerResult::FORWARD;
co_return HandlerResult::SUPPRESS;
}
static asio::awaitable<HandlerResult> S_19_U_14(shared_ptr<Client> c, Channel::Message& msg) {
@@ -2249,7 +2239,7 @@ static on_message_t handlers[0x100][NUM_VERSIONS][2] = {
/* DE */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
/* DF */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}},
// CMD S_PC_PATCH C S_BB_PATCH C S_DC_NTE C S_DC_12_2000 C S_DC_V1 C S_DC_V2 C S_PC_NTE C S_PC_V2 C S_GC_NTE C S_GC_V3 C S_GC_EP3_NTE C S_GC_EP3 C S_XB_V3 C S_BB_V4 C
/* E0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, C_B_E0}},
/* E0 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
/* E1 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
/* E2 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {S_B_E2, nullptr}},
/* E3 */ {{S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {nullptr, nullptr}, {S_invalid, nullptr}, {nullptr, nullptr}},
@@ -2317,7 +2307,8 @@ asio::awaitable<void> on_proxy_command(shared_ptr<Client> c, bool from_server, u
}
}
asio::awaitable<void> handle_proxy_server_commands(shared_ptr<Client> c, shared_ptr<ProxySession> ses, shared_ptr<Channel> channel) {
asio::awaitable<void> handle_proxy_server_commands(
shared_ptr<Client> c, shared_ptr<ProxySession> ses, shared_ptr<Channel> channel) {
std::string error_str;
// server_channel can be changed by receiving a 19 command, hence the
// exception handler is inside the loop here
@@ -2326,6 +2317,9 @@ asio::awaitable<void> handle_proxy_server_commands(shared_ptr<Client> c, shared_
try {
msg = make_unique<Channel::Message>(co_await channel->recv());
if (c->proxy_session == ses) {
for (size_t z = 0; z < std::min<size_t>(c->proxy_session->prev_server_command_bytes.size(), msg->data.size()); z++) {
c->proxy_session->prev_server_command_bytes[z] = msg->data[z];
}
asio::co_spawn(co_await asio::this_coro::executor, on_proxy_command(c, true, std::move(msg)), asio::detached);
}
} catch (const std::system_error& e) {
-2
View File
@@ -10,8 +10,6 @@ ProxySession::ProxySession(shared_ptr<Channel> server_channel, const PersistentC
: server_channel(server_channel) {
if (pc) {
this->remote_guild_card_number = pc->remote_guild_card_number;
this->remote_bb_security_token = pc->remote_bb_security_token;
this->remote_client_config_data = pc->remote_client_config_data;
this->enable_remote_ip_crc_patch = pc->enable_remote_ip_crc_patch;
} else if (is_v4(this->server_channel->version)) {
this->remote_guild_card_number = 0;
-3
View File
@@ -44,7 +44,6 @@ struct ProxySession {
uint64_t server_ping_start_time = 0;
int64_t remote_guild_card_number = -1;
uint32_t remote_bb_security_token = 0;
parray<uint8_t, 0x28> remote_client_config_data;
enum class DropMode {
@@ -64,8 +63,6 @@ struct ProxySession {
struct PersistentConfig {
uint32_t account_id;
uint32_t remote_guild_card_number;
uint32_t remote_bb_security_token;
parray<uint8_t, 0x28> remote_client_config_data;
bool enable_remote_ip_crc_patch;
std::unique_ptr<asio::steady_timer> expire_timer;
};
+29 -19
View File
@@ -454,7 +454,7 @@ asio::awaitable<void> start_proxy_session(shared_ptr<Client> c, const string& ho
send_proxy_destinations_menu(c);
}
} else {
// Get persistent config if client is BB
// Get persistent config if available
ProxySession::PersistentConfig* pc = nullptr;
if (use_persistent_config) {
try {
@@ -477,8 +477,8 @@ asio::awaitable<void> start_proxy_session(shared_ptr<Client> c, const string& ho
std::format("C-{} proxy remote server at {}", c->id, netloc_str),
phosg::TerminalFormat::FG_YELLOW,
phosg::TerminalFormat::FG_RED);
c->proxy_session = make_shared<ProxySession>(channel, pc);
c->log.info_f("Server channel connected");
asio::co_spawn(*s->io_context, handle_proxy_server_commands(c, c->proxy_session, channel), asio::detached);
}
@@ -507,15 +507,6 @@ asio::awaitable<void> end_proxy_session(shared_ptr<Client> c, const std::string&
} catch (const out_of_range&) {
}
bool is_in_game = c->proxy_session->is_in_game;
c->proxy_session->server_channel->disconnect();
c->proxy_session.reset();
if (is_v4(c->version())) {
c->channel->disconnect();
co_return;
}
// Delete all the other players
for (size_t x = 0; x < c->proxy_session->lobby_players.size(); x++) {
if (c->proxy_session->lobby_players[x].guild_card_number == 0) {
@@ -527,6 +518,15 @@ asio::awaitable<void> end_proxy_session(shared_ptr<Client> c, const std::string&
c->channel->send(c->proxy_session->is_in_game ? 0x66 : 0x69, leaving_id, &cmd, sizeof(cmd));
}
bool is_in_game = c->proxy_session->is_in_game;
c->proxy_session->server_channel->disconnect();
c->proxy_session.reset();
if (is_v4(c->version())) {
c->channel->disconnect();
co_return;
}
if (is_in_game) {
string msg = std::format("You cannot return\nto $C6{}$C7\nwhile in a game.\n\n{}",
s->name, error_message);
@@ -1480,24 +1480,34 @@ static asio::awaitable<void> on_93_BB(shared_ptr<Client> c, Channel::Message& ms
c->preferred_lobby_id = base_cmd.preferred_lobby_id;
}
send_client_init_bb(c, 0);
if (!c->bb_client_config.is_filled_with(0xFF)) {
if (base_cmd.guild_card_number == 0) {
// There is a (bug? feature?) in the BB client such that it has to receive
// a reconnect command during the data server phase, or else it won't know
// where to connect to during character selection. It's not clear why they
// didn't just make it use the initial connection address by default...
send_client_init_bb(c, 0);
send_reconnect(c, s->connect_address_for_client(c), s->name_to_port_config.at("bb-data1")->port);
co_return;
} else if (s->proxy_destination_bb.has_value()) {
// Start a proxy session if there's a destination configured Ignore the
// persistent config if this is the first data server connection, to
// prevent quick reconnects from incorrectly reusing the old session's
// state.
// Start a proxy session immediately if there's a destination set. Two
// things to watch out for:
// - Ignore the persistent config if this is the first data server
// connection, to prevent quick reconnects from incorrectly reusing the
// old session's state.
// - We don't send 00E6 (send_client_init_bb) in this case. This is because
// the login command is resent to the remote server, and we forward its
// response back to the client directly.
const auto& [host, port] = *s->proxy_destination_bb;
co_await start_proxy_session(c, host, port, c->bb_connection_phase != 0);
c->proxy_session->remote_client_config_data = c->bb_client_config;
co_return;
} else if (c->bb_connection_phase >= 0x04) {
} else {
send_client_init_bb(c, 0);
}
if (c->bb_connection_phase >= 0x04) {
// This means the client is done with the data server phase and is in the
// game server phase; we should send the ship select menu or a lobby join
// command.
-2
View File
@@ -237,8 +237,6 @@ void send_server_init_bb(shared_ptr<Client> c, uint8_t flags) {
auto cmd = prepare_server_init_contents_bb(server_key, client_key, flags);
send_command_t(c, use_secondary_message ? 0x9B : 0x03, 0x00, cmd);
static const string primary_expected_first_data("\xB4\x00\x93\x00\x00\x00\x00\x00", 8);
static const string secondary_expected_first_data("\xDC\x00\xDB\x00\x00\x00\x00\x00", 8);
c->bb_detector_crypt = make_shared<PSOBBMultiKeyDetectorEncryption>(
c->require_server_state()->bb_private_keys,
bb_crypt_initial_client_commands,
+5 -1
View File
@@ -908,7 +908,11 @@ asio::awaitable<deque<string>> f_sc_ss(ShellCommand::Args& args) {
auto c = args.get_client();
if (args.command[1] == 's') {
co_await on_command_with_header(c, data);
if (c->proxy_session) {
send_command_with_header(c->proxy_session->server_channel, data.data(), data.size());
} else {
co_await on_command_with_header(c, data);
}
} else {
send_command_with_header(c->channel, data.data(), data.size());
}