diff --git a/game_patch/multi/network.cpp b/game_patch/multi/network.cpp index a691d5fc..11fba73b 100644 --- a/game_patch/multi/network.cpp +++ b/game_patch/multi/network.cpp @@ -72,8 +72,6 @@ static std::unordered_map g_af_gi_req_seen; static std::unordered_map g_rcon_access_by_addr; // Client-side: stashed game_info packet for AF extension parsing -static std::vector g_game_info_stashed; - // Client-side: per-server extra data parsed from AF game_info extension static std::unordered_map g_server_browser_extra; @@ -138,9 +136,6 @@ class BufferOverflowPatch // Note: server browser internal functions use strings safely (see 0x0044DDCA for example) // Note: level filename was limited to 64 because of VPP format limits std::array g_buffer_overflow_patches{ - BufferOverflowPatch{0x0047B2D3, 0x0047B2DE, 256}, // process_game_info_packet (server name) - BufferOverflowPatch{0x0047B334, 0x0047B33D, 256}, // process_game_info_packet (level name) - BufferOverflowPatch{0x0047B38E, 0x0047B397, 256}, // process_game_info_packet (mod name) BufferOverflowPatch{0x0047ACF6, 0x0047AD03, 32}, // process_join_req_packet (player name) BufferOverflowPatch{0x0047AD4E, 0x0047AD55, 256}, // process_join_req_packet (password) BufferOverflowPatch{0x0047A8AE, 0x0047A8B5, 64}, // process_join_accept_packet (level filename) @@ -519,106 +514,177 @@ static void handle_rcon_request_packet(const uint8_t* pkt, size_t len, const rf: } } -// Parse AF v2 extension from game_info packet tail using the AFFooter. -// Tail layout: [stock fields...][af_game_info_ext_v2][level_filename\0][AFFooter] -// The footer at the end of the payload lets us jump directly to the extension -// without parsing the stock fields. -static bool parse_game_info_af_tail(const uint8_t* pkt, size_t pkt_len, AFGameInfoExtra& out) +std::vector af_game_info_ext_v2::serialize_to_wire() const { - if (!pkt || pkt_len < sizeof(RF_GamePacketHeader)) - return false; - - RF_GamePacketHeader gh; - std::memcpy(&gh, pkt, sizeof(gh)); - const size_t payload_len = gh.size; - if (pkt_len < sizeof(gh) + payload_len) - return false; - - const uint8_t* payload = pkt + sizeof(gh); - const uint8_t* end = payload + payload_len; - - // read footer from the end of the payload - if (payload_len < sizeof(AFFooter)) - return false; - AFFooter footer; - std::memcpy(&footer, end - sizeof(AFFooter), sizeof(AFFooter)); - if (footer.magic != AF_FOOTER_MAGIC) - return false; + // Wire layout: [sig][ver*4][flags][filename\0][bot_counts*4] + std::string fname = level_filename.substr(0, 63); - // locate the extension data using footer.total_len - const size_t core_len = footer.total_len; - const uint8_t* footer_start = end - sizeof(AFFooter); - if (core_len > payload_len - sizeof(AFFooter)) - return false; - const uint8_t* ext_start = footer_start - core_len; - if (ext_start < payload) - return false; - - // parse af_game_info_ext_v2 from the extension data - if (core_len < sizeof(af_game_info_ext_v2)) - return false; - - const auto* ext = reinterpret_cast(ext_start); - if (ext->af_signature != ALPINE_FACTION_SIGNATURE) - return false; + std::vector buf; + buf.reserve(wire_pre_fname_size + fname.size() + 1 + wire_post_fname_size); - const uint16_t ext_size = ext->ext_size; - if (ext_size < sizeof(af_game_info_ext_v2) || ext_size > core_len) - return false; - - out.version_major = ext->version_major; - out.version_minor = ext->version_minor; - out.version_patch = ext->version_patch; - out.version_type = ext->version_type; - out.af_flags = ext->af_flags; - out.num_bots = ext->num_bots; - out.num_human_players = ext->num_human_players; - out.num_browsers = ext->num_browsers; - out.num_total_clients = ext->num_total_clients; - - // level filename follows the fixed struct - const uint8_t* fname_start = ext_start + ext_size; - const uint8_t* p = fname_start; - while (p < footer_start && *p != '\0') ++p; - if (p >= footer_start) - return false; - out.level_filename.assign(reinterpret_cast(fname_start), p - fname_start); + auto write = [&](const void* data, size_t n) { + const auto* p = static_cast(data); + buf.insert(buf.end(), p, p + n); + }; - return true; + // pre-filename fields + write(&af_signature, sizeof(af_signature)); + write(&version_major, sizeof(version_major)); + write(&version_minor, sizeof(version_minor)); + write(&version_patch, sizeof(version_patch)); + write(&version_type, sizeof(version_type)); + write(&af_flags, sizeof(af_flags)); + + // filename + buf.insert(buf.end(), fname.begin(), fname.end()); + buf.push_back(0); + + // post-filename fields + write(&num_bots, sizeof(num_bots)); + write(&num_human_players, sizeof(num_human_players)); + write(&num_browsers, sizeof(num_browsers)); + write(&num_total_clients, sizeof(num_total_clients)); + + return buf; } +// Fully replaces the stock process_game_info_packet handler. +// Parses stock fields with bounds checking (fixing buffer overflows in the stock code), +// updates the server browser entry, and parses the AF extension inline. FunHook process_game_info_packet_hook{ 0x0047B2A0, [](char* data, const rf::NetAddr& addr) { - process_game_info_packet_hook.call_target(data, addr); + const auto* payload = reinterpret_cast(data); + uint64_t key = addr_key(addr); + + // Clear stale AF extra on any parse failure + auto clear_extra = [&]() { g_server_browser_extra.erase(key); }; + + // Read payload length from the sub-packet header immediately before the payload + uint16_t payload_len; + std::memcpy(&payload_len, data - 2, sizeof(payload_len)); + const uint8_t* end = payload + payload_len; + + if (payload_len < 1) { clear_extra(); return; } + + const uint8_t* r = payload; + uint8_t version = *r++; + if (version != RF_VER_10_11 && version != RF_VER_12 && version != RF_VER_13) { + // use for any servers that don't match + // rf 1.0, 1.1, 1.2, or 1.3 protocol version (should never happen) + rf::multi_join_game_add_server( + addr.ip_addr, addr.port, + rf::strings::incompatible_version, "", "", 0, 0, 0, 0); + clear_extra(); + return; + } - // If this packet is from the server that we are connected to, use game_info for the netgame name - // Useful for joining using protocol handler because when we join we do not have the server name available yet - const char* server_name = data + 1; - if (addr == rf::netgame.server_addr) { - rf::netgame.name = server_name; + // Helper: read a null-terminated string safely into dst, advance r past the terminator + auto read_string = [&](char* dst, size_t dst_size) -> bool { + const uint8_t* start = r; + while (r < end && *r != '\0') ++r; + if (r >= end) return false; + size_t len = std::min(static_cast(r - start), dst_size - 1); + std::memcpy(dst, start, len); + dst[len] = '\0'; + ++r; // skip null terminator + return true; + }; + + // Parse stock fields: name\0 + game_type(1) + players(1) + max_players(1) + level\0 + mod\0 + flags(1) + char name[256]{}; + if (!read_string(name, sizeof(name))) { clear_extra(); return; } + if (end - r < 3) { clear_extra(); return; } + uint8_t game_type = std::clamp(*r++, 0, 7); + uint8_t players = *r++; + uint8_t max_players = *r++; + + char level_name[256]{}; + char mod_name[256]{}; + if (!read_string(level_name, sizeof(level_name))) { clear_extra(); return; } + if (!read_string(mod_name, sizeof(mod_name))) { clear_extra(); return; } + if (r >= end) { clear_extra(); return; } + uint8_t flags = *r++; + + rf::multi_join_game_add_server( + addr.ip_addr, addr.port, + name, level_name, mod_name, + players, max_players, game_type, flags); + + // Parse AF extension if present: [sig][ver*4][af_flags][filename\0][bot_counts*4] + constexpr size_t pre_sz = af_game_info_ext_v2::wire_pre_fname_size; + constexpr size_t post_sz = af_game_info_ext_v2::wire_post_fname_size; + bool parsed = false; + + if (static_cast(end - r) >= pre_sz + 1 + post_sz) { + uint32_t sig; + std::memcpy(&sig, r, sizeof(sig)); + + // r[4] = 19 matches the deprecated AF 1.3 beta format + if (sig == ALPINE_FACTION_SIGNATURE && r[4] != 19) { + AFGameInfoExtra extra{}; + const uint8_t* ext_r = r + sizeof(sig); + extra.version_major = *ext_r++; + extra.version_minor = *ext_r++; + extra.version_patch = *ext_r++; + extra.version_type = *ext_r++; + std::memcpy(&extra.af_flags, ext_r, sizeof(extra.af_flags)); + ext_r += sizeof(extra.af_flags); + + const uint8_t* fname_start = ext_r; + const uint8_t* fname_limit = end - post_sz; + while (ext_r < fname_limit && *ext_r != '\0') ++ext_r; + if (ext_r < fname_limit) { + extra.level_filename.assign( + reinterpret_cast(fname_start), ext_r - fname_start); + ++ext_r; + extra.num_bots = *ext_r++; + extra.num_human_players = *ext_r++; + extra.num_browsers = *ext_r++; + extra.num_total_clients = *ext_r++; + g_server_browser_extra[key] = std::move(extra); + parsed = true; + } + } } - // Parse AF extension from the stashed packet data - if (!g_game_info_stashed.empty()) { - AFGameInfoExtra extra{}; - uint64_t key = addr_key(addr); - if (parse_game_info_af_tail(g_game_info_stashed.data(), g_game_info_stashed.size(), extra)) { + // DEPRECATED: AF 1.3 beta compat — remove when no 1.3 beta servers remain. + // Old wire format: [sig][ext_size(2)][ext_version(1)][ver*4][af_flags][bot_counts*4][filename\0][AFFooter] + if (!parsed && static_cast(end - r) >= 19 + 1 + sizeof(AFFooter)) { + uint32_t sig; + std::memcpy(&sig, r, sizeof(sig)); + if (sig == ALPINE_FACTION_SIGNATURE && r[4] == 19) { + AFGameInfoExtra extra{}; + const uint8_t* ext_r = r + sizeof(sig); + ext_r += 3; // skip ext_size(2) + ext_version(1) + extra.version_major = *ext_r++; + extra.version_minor = *ext_r++; + extra.version_patch = *ext_r++; + extra.version_type = *ext_r++; + std::memcpy(&extra.af_flags, ext_r, sizeof(extra.af_flags)); + ext_r += sizeof(extra.af_flags); + extra.num_bots = *ext_r++; + extra.num_human_players = *ext_r++; + extra.num_browsers = *ext_r++; + extra.num_total_clients = *ext_r++; + const uint8_t* fname_start = ext_r; + const uint8_t* footer_start = end - sizeof(AFFooter); + while (ext_r < footer_start && *ext_r != '\0') ++ext_r; + if (ext_r < footer_start) + extra.level_filename.assign( + reinterpret_cast(fname_start), ext_r - fname_start); g_server_browser_extra[key] = std::move(extra); + parsed = true; } - else { - g_server_browser_extra.erase(key); - } - g_game_info_stashed.clear(); } - }, -}; -CodeInjection process_game_info_packet_game_type_bounds_patch{ - 0x0047B30B, - [](auto& regs) { - // Valid game types are members of the NetGameType enum - regs.ecx = std::clamp(regs.ecx, 0, 7); + if (!parsed) + clear_extra(); + + // Update netgame name if this is from the connected server + if (addr == rf::netgame.server_addr) { + rf::netgame.name = name; + } }, }; @@ -1243,12 +1309,11 @@ CallHook send_game_info_packet_hook std::memcpy(fname, s.c_str(), fname_len); } - if (req_ver >= 3) { - // v2 extension for modern AF clients: [af_game_info_ext_v2][fname\0] + // extension for modern AF clients (v1.3+) + if (req_ver >= 4) { af_game_info_ext_v2 ext{}; ext.set_flags(g_game_info_server_flags); - // count players by type uint8_t num_bots = 0; uint8_t num_human_players = 0; uint8_t num_browsers = 0; @@ -1265,25 +1330,10 @@ CallHook send_game_info_packet_hook ext.num_browsers = num_browsers; ext.num_total_clients = static_cast(num_bots + num_human_players + num_browsers); - // build tail: [ext][fname\0][AFFooter] - size_t fname_actual = fname_len ? fname_len : 1; - size_t core_len = sizeof(ext) + fname_actual; - - AFFooter footer{}; - footer.total_len = static_cast(core_len); - footer.magic = AF_FOOTER_MAGIC; - - std::vector tail; - tail.reserve(core_len + sizeof(footer)); - tail.insert(tail.end(), reinterpret_cast(&ext), - reinterpret_cast(&ext) + sizeof(ext)); if (fname_len) - tail.insert(tail.end(), fname, fname + fname_len); - else - tail.push_back(0); - tail.insert(tail.end(), reinterpret_cast(&footer), - reinterpret_cast(&footer) + sizeof(footer)); + ext.level_filename.assign(reinterpret_cast(fname), fname_len - 1); + auto tail = ext.serialize_to_wire(); auto [buf, new_len] = extend_packet_bytes(data, len, tail.data(), tail.size()); return send_game_info_packet_hook.call_target(addr, buf.get(), new_len); } @@ -1342,8 +1392,8 @@ std::pair, size_t> append_af_tail(const std::byte* CallHook send_game_info_req_packet_hook{ 0x0047B470, [](const rf::NetAddr* addr, std::byte* data, size_t len) { - AFGameInfoReq core{ALPINE_FACTION_SIGNATURE, 3}; - // version 3: server sends af_game_info_ext_v2 with bot count and proper versioning + // version 4: server sends extension with bot counts and proper versioning + AFGameInfoReq core{ALPINE_FACTION_SIGNATURE, 4}; auto [buf, new_len] = append_af_tail(data, len, &core, sizeof(core)); return send_game_info_req_packet_hook.call_target(addr, buf.get(), new_len); }, @@ -2560,13 +2610,6 @@ CodeInjection multi_io_process_packets_injection{ } } } - // client-side: stash game_info packet for AF extension parsing - if (!rf::is_server && packet_type == static_cast(RF_GamePacketType::RF_GPT_GAME_INFO)) { - const auto* base = static_cast(regs.ecx); - const int off = regs.ebp; - const int len = regs.edi; - g_game_info_stashed.assign(base + off, base + off + len); - } }, }; @@ -2807,12 +2850,9 @@ void network_init() multi_get_obj_handle_from_server_handle_hook.install(); multi_set_obj_handle_mapping_hook.install(); - // Use server name from game_info packet for netgame name if address matches current server + // Replace stock process_game_info_packet with bounds-checked parser + AF extension support process_game_info_packet_hook.install(); - // Fix game_type out of bounds vulnerability in game_info packet - process_game_info_packet_game_type_bounds_patch.install(); - // Fix shape_index out of bounds vulnerability in boolean packet process_boolean_packet_validate_shape_index_patch.install(); diff --git a/game_patch/multi/network.h b/game_patch/multi/network.h index 50d9ae1b..1363ec9f 100644 --- a/game_patch/multi/network.h +++ b/game_patch/multi/network.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "server_internal.h" #include "../rf/multi.h" @@ -127,24 +128,25 @@ struct af_sign_packet_ext } }; -// Appended to game_info packets (v2, sent to gi_req_ext_ver >= 3 clients) -// On-wire layout: [af_game_info_ext_v2][level_filename\0][AFFooter] -// ext_size covers the fixed struct only; the variable-length filename follows after it. -// Future versions can add fields to the struct (incrementing ext_version and ext_size), -// and older parsers skip to offset ext_size to find the filename. -#pragma pack(push, 1) +// In-memory representation of the AF game_info v2 extension. +// On-wire layout: [sig][ver*4][flags][filename\0][bot_counts*4] +// The wire format is built by serialize_to_wire(); it does not match the in-memory layout. struct af_game_info_ext_v2 { + // on-wire size of fields before the filename (sig + ver*4 + flags) + static constexpr size_t wire_pre_fname_size = 12; + // on-wire size of fields after the filename (bot_counts*4) + static constexpr size_t wire_post_fname_size = 4; + uint32_t af_signature = ALPINE_FACTION_SIGNATURE; - uint16_t ext_size = sizeof(af_game_info_ext_v2); // size of this fixed struct - uint8_t ext_version = 3; // matches gi_req_ext_ver that triggers this response uint8_t version_major = VERSION_MAJOR; uint8_t version_minor = VERSION_MINOR; uint8_t version_patch = VERSION_PATCH; uint8_t version_type = VERSION_TYPE; uint32_t af_flags = 0; + std::string level_filename; uint8_t num_bots = 0; - uint8_t num_human_players = 0; // excludes bots and browsers + uint8_t num_human_players = 0; uint8_t num_browsers = 0; uint8_t num_total_clients = 0; // bots + humans + browsers @@ -152,9 +154,10 @@ struct af_game_info_ext_v2 { af_flags = flags.game_info_flags_to_uint32(); } + + // Build the wire representation: [sig][ver*4][flags][filename\0][bot_counts*4] + std::vector serialize_to_wire() const; }; -#pragma pack(pop) -static_assert(sizeof(af_game_info_ext_v2) == 19, "unexpected af_game_info_ext_v2 size"); #pragma pack(push, 1) struct AlpineFactionJoinAcceptPacketExt diff --git a/game_patch/rf/localize.h b/game_patch/rf/localize.h index d7868ba7..686f7412 100644 --- a/game_patch/rf/localize.h +++ b/game_patch/rf/localize.h @@ -25,6 +25,7 @@ namespace rf static const auto &player_name = array[835]; static const auto &exiting_game = array[884]; static const auto &usage = array[886]; + static const auto &incompatible_version = array[891]; static const auto &level_name = array[920]; static const auto &level_time = array[922]; static const auto &days = array[923]; diff --git a/game_patch/rf/multi.h b/game_patch/rf/multi.h index 098aad23..5ceb2c0c 100644 --- a/game_patch/rf/multi.h +++ b/game_patch/rf/multi.h @@ -286,6 +286,9 @@ namespace rf static auto& multi_io_process_packets = addr_as_ref(0x004790D0); static auto& multi_kill_local_player = addr_as_ref(0x004757A0); static auto& send_game_info_req_packet = addr_as_ref(0x0047B450); + static auto& multi_join_game_add_server = addr_as_ref(0x0044DD50); static auto& multi_entity_is_female = addr_as_ref(0x004762C0); static auto& multi_powerup_has_player = addr_as_ref(0x004802B0); static auto& multi_powerup_get_time_until = addr_as_ref(0x004802D0);