├── requirements.txt ├── demoinfogo.exe ├── screenshot.png ├── screenshot-verdict.png ├── .gitignore ├── README.md └── overwatcher.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | scapy 3 | selenium -------------------------------------------------------------------------------- /demoinfogo.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takeshixx/csgo-overwatcher/HEAD/demoinfogo.exe -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takeshixx/csgo-overwatcher/HEAD/screenshot.png -------------------------------------------------------------------------------- /screenshot-verdict.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/takeshixx/csgo-overwatcher/HEAD/screenshot-verdict.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | suspects.json 104 | chromedriver.exe 105 | *.dem 106 | *.bz2 107 | *.png 108 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS:GO Overwatcher 2 | 3 | This tool is a quick hack that allows to investigate the actual suspects of CS:GO Overwatch cases. In the default mode it runs in the background and waits until an Overwatch case will be downloaded. As soon as one clicks the download button, this tool will download and unpack the demo file. After that, it will analyze the demo and prints out the scoreboard of each round as well as a list of all players with their Steam community profile URLs (and some additional info) at the end. 4 | 5 | ![screenshot](screenshot.png) 6 | 7 | Overview of all Steam profiles after demo is finished: 8 | 9 | ![screenshot](screenshot-verdict.png) 10 | 11 | *Note*: This tool does not interfere with CS:GO in any way. So it should not get you banned. The demo files can be downloaded by everyone that has access to the URL. If you have further questions, you should probably not use this tool. 12 | 13 | ## Dependencies 14 | 15 | `csgo-overwatcher` requires the official [csgo-demoinfo](https://github.com/ValveSoftware/csgo-demoinfo) from Valve. On Linux, the [demoinfogo-linux](https://github.com/kaimallea/demoinfogo-linux) port can be used. However, this has not been tested (yet). 16 | 17 | See the [README.md](https://github.com/ValveSoftware/csgo-demoinfo/blob/master/demoinfogo/README.md) for information about how to compile `csgo-demoinfo` on Windows. 18 | 19 | ### Compiling csgo-demoinfo on Windows 20 | 21 | I used Visual Studio 2017 to build `libprotobuf.lib` and `demoinfogo.exe`. However, building [protobuf-2.5.0.zip](https://github.com/google/protobuf/releases/download/v2.5.0/protobuf-2.5.0.zip) some manual patching. The file `src/google/protobuf/io/zero_copy_stream_impl_lite.cc` requires the additional include `#include ` and all references to `min()`/`max()` have to be changed to `std::min()`/`std::max()`. After that building Release of libprotobuf should work. 22 | 23 | A precompiled version of `demoinfogo.exe` is included in this repository. But it may not work for everyone. I strongly recommend to compiled it for the respective target system! 24 | 25 | ## Installation 26 | 27 | `csgo-overwatcher` requires [Python 3](https://python.org) and some additional modules that can be installed with `pip` via a PowerShell: 28 | 29 | ``` 30 | pip3.exe install -r requirements.txt 31 | ``` 32 | 33 | ## Usage 34 | 35 | The default mode will just listen on the network and waits until a Overwatch case will be downloaded. After the download, it will automatically parse the demo and print all stats to stdout: 36 | 37 | ``` 38 | python3.exe overwatcher.py 39 | ``` 40 | 41 | The demo files will reside on the file system for later inspection. Already downloaded files can also be parsed as well: 42 | 43 | ``` 44 | python3.exe overwatcher.py 123123_123123123123123123132.dem 45 | ``` 46 | 47 | In case you want to analyse a case that you already started, you can follow these steps: 48 | 49 | * Close CS:GO (do not sent your verdict) 50 | * Go to your `csgo` folder (on windows it will be something like `C:\Program Files (x86)\Steam\steamapps\common\Counter-Strike Global Offensive\csgo`) 51 | * Delete the file `myassignedcase.evidence` 52 | * Run `overwatcher.py` 53 | * Start CS:GO and download the Overwatch case again 54 | 55 | Take a screenshot of a Steam profile, where *99999999999999999* is the profile ID (XUID): 56 | 57 | ``` 58 | python3.exe overwatcher.py -s 99999999999999999 59 | ``` 60 | 61 | You can also add the `-a`/`--anonymize` option to blur sensitive information like the user name, fiends list, profile avatar and more. 62 | 63 | *Note*: Screenhots require the [Selenium](https://www.seleniumhq.org/projects/webdriver/) module and the [chromedriver.exe](http://chromedriver.chromium.org/) in the local directory. 64 | 65 | ## Disclamier 66 | 67 | The author takes no responsibility for anything this tool will be used for. It has been built for educational purposes. Credits go to the authors of the tools that have been used to create this project. 68 | 69 | This code has been written for fun and it will most likely contain bugs. 70 | -------------------------------------------------------------------------------- /overwatcher.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os 4 | import re 5 | import bz2 6 | import requests 7 | import subprocess 8 | import json 9 | import time 10 | import argparse 11 | 12 | RE_URL = re.compile(r'GET (/730/\d+_\d+.dem.bz2)') 13 | RE_HOST = re.compile(r'Host: (replay\d+.valve.net)') 14 | RE_HOST_PW = re.compile(r'Host: (replay\d+.wmsj.cn)') 15 | RE_FILENAME = re.compile(r'GET /730/(\d+_\d+.dem.bz2)') 16 | RE_STEAMID = re.compile(r'STEAM_\d:\d:\d+') 17 | RE_DEMO_MSG = re.compile(rb'(?:^|(?:\r)?\n)(\w+|(?:\w+\s+\w+))(?:\r)?\n{(?:\r)?\n (.*?)?(?:\r)?\n}', re.S | re.M) 18 | RE_PROFILE_BAN = re.compile(rb'
[\r\n\t]+
[\r\n\t]+\d+ game ban on record[\r\n\t]+', re.S | re.M) 19 | TEAM_T = 2 20 | TEAM_CT = 3 21 | DEMOINFOGO = 'demoinfogo.exe' 22 | SUSPECTS_FILE = 'suspects.json' 23 | USER_AGENT = 'Valve/Steam HTTP Client 1.0 (730)' 24 | SCREENSHOT_DIR = 'screenshots' 25 | selenium = None 26 | 27 | ARGS = argparse.ArgumentParser(description='Investigate the actual suspects of CS:GO Overwatch cases') 28 | ARGS.add_argument('demo', metavar='demo file', nargs='?', 29 | help='parse a local Overwatch demo file') 30 | ARGS.add_argument('-r', '--recheck', dest='suspects', default=None, metavar='suspects file', 31 | help='recheck the VAC status of suspects file') 32 | ARGS.add_argument('-c', '--check-vac', dest='vac', default=None, metavar='XUID', 33 | help='check ban status of account') 34 | ARGS.add_argument('-s', '--screenshot', dest='screenshot', default=None, 35 | metavar='XUID', help='take profile screenshot') 36 | ARGS.add_argument('-a', '--anonymize', action='store_true', dest='anon', 37 | default=False, help='anonymize profile screenshots') 38 | 39 | 40 | def info(msg): 41 | print('[*] ' + str(msg)) 42 | 43 | 44 | def warn(msg): 45 | print('[W] ' + str(msg)) 46 | 47 | 48 | def error(msg): 49 | print('[E] ' + str(msg)) 50 | sys.exit(1) 51 | 52 | 53 | def download_demo(url, filename): 54 | if (os.path.isfile(filename) and \ 55 | os.path.getsize(filename) > 0) or \ 56 | (os.path.isfile(filename) and \ 57 | os.path.getsize(filename.replace('.bz2', '')) > 0): 58 | warn('Demo already loaded, skipping') 59 | return 60 | info('Downloading demo...') 61 | headers = {'User-Agent': USER_AGENT} 62 | req = requests.get(url, headers=headers) 63 | with open(filename, 'wb') as f: 64 | f.write(req.content) 65 | info('Written demo as {}'.format(filename)) 66 | if os.path.getsize(filename) > 0: 67 | decompress_demo(filename) 68 | else: 69 | warn('Demofile is too small, skipping.') 70 | 71 | 72 | def decompress_demo(demofile): 73 | with open(demofile.replace('.bz2', ''), 'wb') as w: 74 | with open(demofile, 'rb') as r: 75 | w.write(bz2.decompress(r.read())) 76 | info('Decompressed demofile {}'.format(demofile)) 77 | analyze_demo(demofile.replace('.bz2', '')) 78 | 79 | 80 | def find_demo(pkt): 81 | p = str(pkt) 82 | url_matches = RE_URL.findall(p) 83 | host_matches = RE_HOST.findall(p) 84 | host_matches_pw = RE_HOST_PW.findall(p) 85 | if url_matches and any([host_matches, host_matches_pw]): 86 | msg = 'Found new ' 87 | if host_matches_pw: 88 | msg += 'Perfect World ' 89 | url = 'http://{host}{url}'.format( 90 | host=host_matches_pw[0], 91 | url=url_matches[0]) 92 | else: 93 | url = 'http://{host}{url}'.format( 94 | host=host_matches[0], 95 | url=url_matches[0]) 96 | 97 | msg += 'demo: ' + url 98 | info(msg) 99 | filename = RE_FILENAME.findall(p)[0] 100 | download_demo(url, filename) 101 | 102 | def handle_suspect(players, demofile): 103 | info('Who is the suspect?') 104 | print('0\tSuspect is innocent (lol)') 105 | print('---') 106 | if isinstance(players, dict): 107 | players = list(players.values()) 108 | for i, player in enumerate(players): 109 | print('{id}\t{name} ({kills}/{assists}/{deaths})'.format( 110 | id=i + 1, 111 | name=player.name.decode(), 112 | kills=player.kills, 113 | assists=player.assists, 114 | deaths=player.deaths)) 115 | tries = 0 116 | choice = None 117 | while tries < 3: 118 | try: 119 | choice = input('Please provide the number: ') 120 | except KeyboardInterrupt: 121 | break 122 | try: 123 | choice = int(choice) 124 | break 125 | except ValueError: 126 | warn('Invalid player choice: ' + str(choice)) 127 | if choice and choice != 0: 128 | write_suspects_file(players[choice - 1], demofile) 129 | elif choice == 0: 130 | info('Suspect innocent') 131 | else: 132 | warn('Failed to provide a valid suspect id!') 133 | 134 | 135 | def write_suspects_file(player, demofile): 136 | assert isinstance(player, Player), 'Invalid Player object: ' + str(type(player)) 137 | info('Checking VAC status for ' + player.xuid.decode()) 138 | banned = check_vac_status(player.xuid.decode()) 139 | if banned: 140 | take_profile_screenshot(player.xuid.decode()) 141 | info('VAC status: ' + 'BANNED' if banned else 'not banned (yet)') 142 | suspect = {'xuid': player.xuid.decode(), 143 | 'name': player.name.decode(), 144 | 'stats': '{}/{}/{}'.format(player.kills, player.assists, player.deaths), 145 | 'banned': banned, 146 | 'added': int(time.time()), 147 | 'last_checked': int(time.time()), 148 | 'demo': os.path.basename(demofile)} 149 | data_json = [] 150 | if os.path.isfile(SUSPECTS_FILE) and \ 151 | os.path.getsize(SUSPECTS_FILE) > 0: 152 | with open(SUSPECTS_FILE, 'r') as f: 153 | data = f.read() 154 | try: 155 | data_json = json.loads(data) 156 | except Exception as e: 157 | print(e) 158 | return 159 | for s in data_json: 160 | if s['xuid'] == player.xuid.decode(): 161 | warn('Suspect already in suspects file, skipping.') 162 | return 163 | data_json.append(suspect) 164 | with open(SUSPECTS_FILE, 'w') as f: 165 | f.write(json.dumps(data_json)) 166 | info('Written suspect to ' + SUSPECTS_FILE) 167 | 168 | 169 | def check_vac_status(xuid): 170 | steam_url = 'https://steamcommunity.com/profiles/' + xuid 171 | r = requests.get(steam_url) 172 | if RE_PROFILE_BAN.findall(r.content): 173 | return True 174 | else: 175 | return False 176 | 177 | 178 | def check_local_suspects(): 179 | if os.path.isfile(SUSPECTS_FILE) and \ 180 | os.path.getsize(SUSPECTS_FILE) > 0: 181 | with open(SUSPECTS_FILE, 'r') as f: 182 | data = f.read() 183 | try: 184 | suspects = json.loads(data) 185 | except Exception as e: 186 | error(e) 187 | else: 188 | error('Cannot read suspects from ' + SUSPECTS_FILE) 189 | update_counter = 0 190 | for suspect in suspects: 191 | if suspect.get('banned'): 192 | continue 193 | banned = check_vac_status(suspect['xuid']) 194 | if banned: 195 | take_profile_screenshot(suspect['xuid']) 196 | info('https://steamcommunity.com/profiles/{} is now banned! =D'.format(suspect['xuid'])) 197 | suspect['banned'] = True 198 | suspect['last_checked'] = int(time.time()) 199 | update_counter += 1 200 | with open(SUSPECTS_FILE, 'w') as f: 201 | f.write(json.dumps(suspects)) 202 | info('Updated {} suspects'.format(update_counter)) 203 | 204 | 205 | def take_profile_screenshot(xuid, anonymize=False): 206 | if not selenium: 207 | try: 208 | from selenium import webdriver 209 | from selenium.webdriver.chrome.options import Options 210 | except ImportError as e: 211 | warn('Install selenium for screenshot support: ' + str(e)) 212 | return 213 | if not os.path.isdir(SCREENSHOT_DIR): 214 | warn('Screenshot directory does not exist, creating it.') 215 | os.makedirs(SCREENSHOT_DIR) 216 | steam_url = 'https://steamcommunity.com/profiles/' + xuid 217 | options = Options() 218 | options.add_argument('--headless') 219 | options.add_argument('--window-size=1920,1080') 220 | options.add_argument('--log-level=3') 221 | try: 222 | driver = webdriver.Chrome('chromedriver', chrome_options=options) 223 | except Exception as e: 224 | warn('Failed to load Chromedriver: ' + str(e)) 225 | return 226 | driver.get(steam_url) 227 | screenshot = SCREENSHOT_DIR + '/' + xuid 228 | if anonymize: 229 | info('Anonymizing profile') 230 | # Script source: https://pastebin.com/raw/pCTGVrsU 231 | hide_script = '''(function() { 232 | jQuery(".playerAvatarAutoSizeInner, .profile_small_header_name, .myworkshop_playerName, .workshop_showcase_mutiitem_ctn a > img, .profile_header_centered_persona, .profile_group_links > .profile_count_link_preview, .profile_topfriends, .favoritegroup_showcase_group, .playerAvatar, .commentthread_author_link, #es_permalink").css("filter", "blur(9px) grayscale(0.75)"); 233 | jQuery("#global_header").hide() 234 | })();''' 235 | driver.execute_script(hide_script) 236 | screenshot += '_anonymized.png' 237 | else: 238 | screenshot += '.png' 239 | info('Saving profile screenshot to ' + screenshot) 240 | element = driver.find_element_by_tag_name('body') 241 | element_png = element.screenshot_as_png 242 | with open(screenshot, 'wb') as f: 243 | f.write(element_png) 244 | driver.quit() 245 | 246 | 247 | class Player(object): 248 | def __init__(self, xuid, name, userid, steamid=None): 249 | self.xuid = xuid 250 | self.name = name 251 | self.userid = userid 252 | self.steamid = steamid 253 | self.steamid64 = None 254 | if self.steamid != b'BOT': 255 | self.steamid64 = self.convert_to_steamid64(self.steamid) 256 | self.kills = 0 257 | self.deaths = 0 258 | self.assists = 0 259 | self.team = 0 260 | self.is_alive = False 261 | self.is_connected = False 262 | 263 | def __repr__(self): 264 | return self.__str__() 265 | 266 | def __str__(self): 267 | if not self.is_bot(): 268 | return ('userID: {userid} Name: {name} team: {team} Steam: {steam} ' 269 | 'SteamRep: {steamrep} Kills: {kills} Assists: ' 270 | '{assists} Deaths: {deaths} ({xuid})').format( 271 | userid=self.userid.decode(), 272 | name=self.name.decode(), 273 | team=self.team or 'NOT_SET', 274 | steam=self.get_steamcommunity_url(), 275 | steamrep=self.get_steamrep_url(), 276 | kills=self.kills, 277 | assists=self.assists, 278 | deaths=self.deaths, 279 | xuid=self.xuid.decode()) 280 | else: 281 | return ('userID: {userid} Name: {name} Team: {team} Kills: {kills} ' 282 | 'Assists: {assists} Deaths: {deaths}').format( 283 | userid=self.userid.decode(), 284 | name='BOT ' + self.name.decode(), 285 | team=self.team or 'NOT_SET', 286 | kills=self.kills, 287 | assists=self.assists, 288 | deaths=self.deaths) 289 | 290 | def pretty_print(self): 291 | if self.is_bot(): 292 | output = '[BOT] ' + self.name.decode() 293 | else: 294 | output = self.name.decode() 295 | output += ' (' + self.xuid.decode() + ')\n' 296 | output += '\tK/A/D:\t\t{}/{}/{}'.format(self.kills, self.assists, self.deaths) + '\n' 297 | if not self.is_bot(): 298 | output += '\tSteam:\t\t' + self.get_steamcommunity_url() + '\n' 299 | output += '\tSteamRep:\t' + self.get_steamrep_url() 300 | return output 301 | 302 | def is_bot(self): 303 | return True if self.steamid == b'BOT' else False 304 | 305 | def convert_to_steamid64(self, steamid): 306 | if not steamid: 307 | return 308 | steam64id = 76561197960265728 309 | id_split = steamid.split(b':') 310 | steam64id += int(id_split[2]) * 2 311 | if id_split[1] == b'1': 312 | steam64id += 1 313 | return steam64id 314 | 315 | def get_steamcommunity_url(self): 316 | return 'https://steamcommunity.com/profiles/' + str(self.steamid64) if self.steamid64 else '' 317 | 318 | def get_steamrep_url(self): 319 | return 'https://steamrep.com/search?q=' + str(self.steamid64) if self.steamid64 else '' 320 | 321 | 322 | class DemoInfo(object): 323 | def __init__(self, demofile): 324 | if not os.path.isfile(demofile) or \ 325 | not os.path.getsize(demofile) > 0: 326 | error('Invalid demo file: ' + demofile) 327 | self.demofile = os.path.abspath(demofile) 328 | self.messages = {} 329 | self.players = {} 330 | self.current_round = 0 331 | self.ct_rounds_won = 0 332 | self.t_rounds_won = 0 333 | self.warmup_over = False 334 | self.team = None 335 | self.handle_suspect_callback = None 336 | 337 | def dump_demo(self, callback=None): 338 | info('Start dumping demo...') 339 | if callback: 340 | self.handle_suspect_callback = callback 341 | # -stringtables is required to get 'player info'/player_info 342 | cmd_args = [DEMOINFOGO, '-gameevents', '-nofootsteps', 343 | '-stringtables', '-nowarmup', self.demofile] 344 | p = subprocess.Popen(cmd_args, stdout=subprocess.PIPE) 345 | (output, perr) = p.communicate() 346 | p.wait() 347 | if perr: 348 | error('Running {} failed: {}'.format(DEMOINFOGO, error if error else 'Unknown error')) 349 | self.parse_demo_dump(output) 350 | 351 | def parse_demo_dump(self, dump): 352 | info('Parsing demo messages...') 353 | found_messages = RE_DEMO_MSG.findall(dump) 354 | for msg in found_messages: 355 | message_type = msg[0].replace(b' ', b'_') 356 | message_data = msg[1] 357 | if not message_type in self.messages.keys(): 358 | self.messages[message_type] = [] 359 | message_data = self.parse_message_data(message_data) 360 | self.messages[message_type].append(message_data) 361 | self.handle_message(message_type, message_data) 362 | for _, player in self.players.items(): 363 | if player.is_bot(): 364 | continue 365 | print(player.pretty_print()) 366 | print('---') 367 | if self.handle_suspect_callback and \ 368 | callable(self.handle_suspect_callback): 369 | self.handle_suspect_callback(self.players, self.demofile) 370 | 371 | def parse_message_data(self, data): 372 | return_dict = {} 373 | attributes = data.split(b'\r\n') 374 | for a in attributes: 375 | a = a.strip() 376 | try: 377 | key, val = a.split(b':', maxsplit=1) 378 | val = val.strip() 379 | except ValueError: 380 | key = a.split(b':')[0] 381 | val = None 382 | return_dict[key.strip()] = val 383 | return return_dict 384 | 385 | def handle_message(self, message_type, message_data): 386 | try: 387 | if message_type == b'player_info': 388 | if message_data[b'ishltv'] == b'1': 389 | return 390 | if not message_data[b'userID'] in self.players.keys(): 391 | id_temp = self.player_get_id_for_xuid(message_data[b'xuid']) 392 | if id_temp: 393 | # Players that reconnect get different userids? 394 | self.players[id_temp].is_connected = True 395 | self.players[id_temp].userid = message_data[b'userID'] 396 | self.players[message_data[b'userID']] = self.players[id_temp] 397 | del self.players[id_temp] 398 | return 399 | self.players[message_data[b'userID']] = Player( 400 | message_data[b'xuid'], 401 | message_data[b'name'], 402 | message_data[b'userID'], 403 | message_data[b'guid']) 404 | self.players[message_data[b'userID']].is_connected = True 405 | elif message_data[b'userID'] in self.players.keys() and message_data[b'updating'] == b'true': 406 | if self.players[message_data[b'userID']].xuid != message_data[b'xuid']: 407 | warn('User not known, skipping stats recovery') 408 | return 409 | new_player = Player( 410 | message_data[b'xuid'], 411 | message_data[b'name'], 412 | message_data[b'userID'], 413 | message_data[b'guid']) 414 | new_player.deaths = self.players[message_data[b'userID']].deaths 415 | new_player.kills = self.players[message_data[b'userID']].kills 416 | new_player.assists = self.players[message_data[b'userID']].assists 417 | del self.players[message_data[b'userID']] 418 | self.players[message_data[b'userID']] = new_player 419 | elif message_type == b'player_spawn': 420 | if not self.parse_id_from_userid(message_data[b'userid']) in self.players: 421 | return 422 | self.players[self.parse_id_from_userid(message_data[b'userid'])].is_alive = True 423 | self.players[self.parse_id_from_userid(message_data[b'userid'])].team = message_data[b'teamnum'] 424 | elif message_type == b'player_team': 425 | if message_data[b'disconnect'] == b'1' and message_data[b'isbot'] == b'0': 426 | self.players[self.parse_id_from_userid(message_data[b'userid'])].is_connected = False 427 | elif message_data[b'disconnect'] == b'1' and message_data[b'isbot'] == b'1': 428 | # Remove a after after disconnect 429 | if self.parse_id_from_userid(message_data[b'userid']) in self.players.keys(): 430 | del self.players[self.parse_id_from_userid(message_data[b'userid'])] 431 | else: 432 | if message_data[b'isbot'] == b'0' and message_data[b'team'] != b'0': 433 | self.players[self.parse_id_from_userid(message_data[b'userid'])].team = message_data[b'team'] 434 | elif message_type == b'player_death': 435 | if not self.warmup_over: 436 | return 437 | if self.parse_id_from_userid(message_data[b'userid']) in self.players.keys(): 438 | self.players[self.parse_id_from_userid(message_data[b'userid'])].deaths += 1 439 | self.players[self.parse_id_from_userid(message_data[b'userid'])].is_alive = False 440 | if self.parse_id_from_userid(message_data[b'userid']) != self.parse_id_from_userid(message_data[b'attacker']) and self.parse_id_from_userid(message_data[b'attacker']) in self.players.keys(): 441 | # Kill was not a suicide 442 | if self.players[self.parse_id_from_userid(message_data[b'attacker'])].is_alive == True: 443 | self.players[self.parse_id_from_userid(message_data[b'attacker'])].kills += 1 444 | if message_data[b'assister'] != b'0': 445 | if not self.parse_id_from_userid(message_data[b'assister']) in self.players.keys(): 446 | # Could be a bot that has disconnected already 447 | return 448 | self.players[self.parse_id_from_userid(message_data[b'assister'])].assists += 1 449 | elif message_type == b'round_start': 450 | # lol why?! 451 | if message_data[b'timelimit'] == b'999': 452 | return 453 | self.warmup_over = True 454 | self.current_round += 1 455 | elif message_type == b'round_end': 456 | if message_data[b'winner'] == b'1': 457 | return 458 | if message_data[b'winner'] == b'3': 459 | self.ct_rounds_won += 1 460 | else: 461 | self.t_rounds_won += 1 462 | if self.current_round == 15: 463 | # Switch teams after halftime 464 | ct_temp = self.ct_rounds_won 465 | self.ct_rounds_won = self.t_rounds_won 466 | self.t_rounds_won = ct_temp 467 | self.print_stats(message_data) 468 | except KeyError: 469 | warn(b'Error while handling ' + message_type + b' message: ' + str(message_data).encode()) 470 | raise 471 | 472 | def print_stats(self, data): 473 | header = '-- Player Stats - Round {total_rounds} - {current_winner}\'s won --'.format( 474 | total_rounds=self.current_round, 475 | current_winner='CT' if data[b'winner'] == b'3' else 'T') 476 | print(header) 477 | padding = max(len(p.name) for _, p in self.players.items()) + 2 478 | players_ct = [] 479 | players_t = [] 480 | for _, player in self.players.items(): 481 | if player.team == b'3' or player.team == 3: 482 | players_ct.append(player) 483 | else: 484 | players_t.append(player) 485 | team_head = '[ CT - ' + str(self.ct_rounds_won) 486 | team_head += ' ]' + '_' * (len(header) - len(team_head)) + '\n' 487 | player_output = team_head 488 | for p in sorted(players_ct, key=lambda player: player.kills, reverse=True): 489 | player_output += '{} [ {}:{}:{} ]'.format( 490 | p.name.decode().ljust(padding) if not p.is_bot() else 'BOT ' + p.name.decode().ljust(padding), 491 | p.kills, 492 | p.assists, 493 | p.deaths) 494 | player_output += '\n' 495 | team_head = '[ T - ' + str(self.t_rounds_won) 496 | team_head += ' ]' + '_' * (len(header) - len(team_head)) + '\n' 497 | player_output += team_head 498 | for p in sorted(players_t, key=lambda player: player.kills, reverse=True): 499 | player_output += '{} [ {}:{}:{} ]'.format( 500 | p.name.decode().ljust(padding) if not p.is_bot() else 'BOT ' + p.name.decode().ljust(padding), 501 | p.kills, 502 | p.assists, 503 | p.deaths) 504 | player_output += '\n' 505 | print(player_output) 506 | print('-' * len(header)) 507 | 508 | def sort_player_list(self, players): 509 | sorted_list = [] 510 | 511 | 512 | def parse_id_from_userid(self, userid): 513 | re_id = re.compile(rb'\s\(id:(\d{1,2})\)') 514 | ids = re_id.findall(userid) 515 | if ids: 516 | return ids[0] 517 | else: 518 | return userid 519 | 520 | def player_get_id_for_xuid(self, xuid): 521 | for playerid, player in self.players.items(): 522 | if player.xuid == xuid: 523 | return playerid 524 | return False 525 | 526 | def analyze_demo(filename): 527 | demoinfo = DemoInfo(filename) 528 | demoinfo.dump_demo(callback=handle_suspect) 529 | 530 | if __name__ == '__main__': 531 | args = ARGS.parse_args() 532 | if args.suspects: 533 | info('Checking suspects file') 534 | check_local_suspects() 535 | elif args.demo: 536 | info('Analyzing demo file') 537 | analyze_demo(args.demo) 538 | elif args.vac: 539 | info('Checking VAC status') 540 | if check_vac_status(args.vac): 541 | info('Account BANNED') 542 | else: 543 | info('Account not banned') 544 | elif args.screenshot: 545 | info('Taking profile screenshot') 546 | take_profile_screenshot(args.screenshot, args.anon) 547 | else: 548 | # Only import Scapy when we need it 549 | from scapy.all import sniff 550 | info('Sniffing for demo downloads...') 551 | sniff(filter='tcp port 80',prn=find_demo) 552 | --------------------------------------------------------------------------------