├── LICENSE ├── README.md ├── csm.py ├── requirements.txt └── resources └── readme.txt /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 gnzsystems 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Certificate Store Monitor 2 | Certificate Store Monitor based off of a concept by Steve Gibson of GRC. 3 | 4 | ## Summary 5 | CSM is the security tool that I never knew I needed. Based off of a concept initially broached by Steve Gibson on Security Now! Episode #551, this utility provides you with the ability to protect your Windows Certificate Store. In an age where even hardware manufacturers are trying to infiltrate your certificate store, this is a tool that every privacy-conscious individual should have in their arsenal. 6 | 7 | ## Features 8 | 9 | * **Active Monitoring**: CSM's WatchDog feature monitors the registry keys that contain the Windows Certificate Store. If a change is made to any of these keys, it notifies you immediately and allows you to remove the new certificate. 10 | * **Passive Scanning**: CSM provides a user-friendly wrapper around the [SysInternals Suite Sigcheck Utility](https://technet.microsoft.com/en-us/sysinternals/bb897441.aspx). Whenever CSM starts up, it uses SigCheck to check your certificate store against Microsoft's default list of trusted certificates. If there are any non-default certificates found, CSM provides an interface for removing them. 11 | 12 | ## Installation 13 | 14 | * **Quick Setup**: A goal of this project was to be completely portable. The compiled executable requires no installation. **Simply download the compiled executable, extract it anywhere on your computer, and run it.** You may receive a UAC prompt as the utility escalates its permissions. 15 | * **[Download the pre-compiled binary from the GNZ Systems website by clicking here!](https://www.gnzsystems.com/csm.html)** 16 | 17 | * **From Source**: CSM requries a few external modules. They're included in the requirements.txt file. On a Python 2.7 install simply run the command **pip install -r requirements.txt** to get yourself up and running! 18 | 19 | ## Moving Forward 20 | 21 | * **Refactoring, adding comments** 22 | For a project written from the ground up in 4 days, with no prior planning, the codebase is surprisingly readable. However, it's still very messy and inefficient. First priority is cleaning up the code base and adding comments to promote collaboration. 23 | 24 | * **Performance Optimization** 25 | Because there was very little prior planning, there are many redundant functions and unneccessary function calls at this point. This impacts the application's overall performance. Once the refactoring is complete, the next priority will be improving performance. 26 | 27 | * **User Interface Improvements** 28 | Currently, the application relies on dynamically generated VBScript notifications. This is *not* ideal. We are currently working on an improved interface design. This interface will handle notifications, provide a tray icon, and provide an interface for accessing currently-unused functionality. 29 | 30 | * **Functionality** 31 | Due to the lack of an *actual* user interface, there is currently some dormant back-end functionality. This functionality includes the ability to revert changes that this utility makes to the registry. 32 | 33 | * **Reduced reliance on SigCheck** 34 | Preliminary testing shows that SigCheck doesn't find *all* of the rogue certificates that may be floating around in your certificate store. Future versions will include an internal implementation of SigCheck, with improved checking against the Microsoft STL. 35 | 36 | * **Improved Active Monitoring** 37 | The current version only monitors parts of the key stores that already contain keys. This leaves some parts of the certificate store unmonitored, which may allow a certificate to be installed unnoticed. 38 | -------------------------------------------------------------------------------- /csm.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import time 5 | import uuid 6 | import json 7 | import base64 8 | import sqlite3 9 | import win32con 10 | import win32api 11 | import threading 12 | import subprocess 13 | import wincertstore 14 | import _winreg as reg 15 | from hashlib import sha1 16 | from time import strftime 17 | from hashlib import sha256 18 | from pyasn1_modules import rfc2459 19 | import win32com.shell.shell as shell 20 | from pyasn1.codec.der import decoder 21 | 22 | 23 | """ 24 | 25 | The MIT License (MIT) 26 | 27 | Copyright (c) 2016 GNZ Systems and Consulting, Inc. 28 | 29 | Permission is hereby granted, free of charge, to any person obtaining a copy 30 | of this software and associated documentation files (the "Software"), to deal 31 | in the Software without restriction, including without limitation the rights 32 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 33 | copies of the Software, and to permit persons to whom the Software is 34 | furnished to do so, subject to the following conditions: 35 | 36 | The above copyright notice and this permission notice shall be included in all 37 | copies or substantial portions of the Software. 38 | 39 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 40 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 41 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 42 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 43 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 44 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 45 | SOFTWARE. 46 | 47 | Software written by Jeff Gonzalez of GNZ Systems and Consulting, Inc. 48 | 49 | """ 50 | 51 | 52 | class CertificateStoreManager: 53 | 54 | def __init__(self, logCallback): 55 | MSstore = r"Software\Microsoft\SystemCertificates" 56 | GPstore = r"Software\Policy\Microsoft\SystemCertificates" 57 | self.regKeys = { 58 | "CU_STORE": [reg.HKEY_CURRENT_USER, MSstore], 59 | "LM_STORE": [reg.HKEY_LOCAL_MACHINE, MSstore], 60 | "USER_STORE": [reg.HKEY_USERS, MSstore], 61 | "CU_POLICY_STORE": [reg.HKEY_CURRENT_USER, GPstore], 62 | "LM_POLICY_STORE": [reg.HKEY_LOCAL_MACHINE, GPstore] 63 | } 64 | self.logCallback = logCallback 65 | 66 | def read_registry(self): 67 | keyHashes = {} 68 | for key in self.regKeys: 69 | self._log("Reading registry data from key: %s" % key) 70 | certData = {} 71 | try: 72 | self._log("Connecting to: %s" % key) 73 | hive = reg.ConnectRegistry(None, self.regKeys[key][0]) 74 | regKey = reg.OpenKey(hive, self.regKeys[key][1]) 75 | i = 0 76 | self._log("Enumerating key: %s" % key) 77 | while True: 78 | try: 79 | subkey_name = reg.EnumKey(regKey, i) 80 | self._log("Found subkey: %s" % subkey_name) 81 | a_subkey = reg.OpenKey(regKey, subkey_name) 82 | certs = {} 83 | try: 84 | a_subkey_certKey = reg.OpenKey(a_subkey, "Certificates") 85 | n = 0 86 | self._log("Enumerating certificate store: %s/Certificates" % subkey_name) 87 | while True: 88 | try: 89 | certKey_name = reg.EnumKey(a_subkey_certKey, n) 90 | self._log("Found certificate: %s" % certKey_name) 91 | certKey = reg.OpenKey(a_subkey_certKey, certKey_name) 92 | blob = reg.QueryValueEx(certKey, "Blob")[0] 93 | keyHash = self._hash(blob) 94 | certs[certKey_name] = { 95 | "hash": keyHash, 96 | "blob": base64.b64encode(blob), 97 | "path": "/".join([key, subkey_name, "Certificates", certKey_name]) 98 | } 99 | reg.CloseKey(certKey) 100 | n += 1 101 | except EnvironmentError: 102 | break 103 | self._log("Closing cert store: %s/Certificates" % subkey_name) 104 | reg.CloseKey(a_subkey_certKey) 105 | except EnvironmentError: 106 | pass 107 | reg.CloseKey(a_subkey) 108 | for certName in certs: 109 | try: 110 | certData[subkey_name][certName] = certs[certName] 111 | except KeyError: 112 | certData[subkey_name] = {certName: certs[certName]} 113 | try: 114 | self._log("Stored %d hash values for subkey: %s" % (len(certData[subkey_name]), 115 | subkey_name)) 116 | except KeyError: 117 | self._log("No keys stored for subkey: %s" % subkey_name) 118 | i += 1 119 | except EnvironmentError: 120 | self._log("Storing final values for key: %s" % key) 121 | keyHashes[key] = certData 122 | self._log("Closing key: %s" % key) 123 | reg.CloseKey(regKey) 124 | self._log("Closing registry handle for key: %s" % key) 125 | reg.CloseKey(hive) 126 | break 127 | except WindowsError: 128 | self._log("Unable to open key: %s" % key) 129 | pass 130 | self._log("All registry operations completed.") 131 | return keyHashes 132 | 133 | def read_subkeys(self, regKey): 134 | self._log("Reading subkeys for registry key: %s" % regKey) 135 | registryHandles = [] 136 | subkeys = [] 137 | path = regKey.split("/") 138 | hiveName = path.pop(0) 139 | hive = reg.ConnectRegistry(None, self.regKeys[hiveName][0]) 140 | registryHandle = reg.OpenKey(hive, self.regKeys[hiveName][1]) 141 | registryHandles.append(hive) 142 | self._log("Connected to registry at location: %s" % hiveName) 143 | for step in path: 144 | registryHandles.append(registryHandle) 145 | registryHandle = reg.OpenKey(registryHandle, step) 146 | i = 0 147 | while True: 148 | try: 149 | subkey = reg.EnumKey(registryHandle, i) 150 | self._log("Found subkey: %s" % subkey) 151 | subkeys.append(subkey) 152 | i += 1 153 | except EnvironmentError: 154 | break 155 | self._log("Found %d subkeys." % len(subkeys)) 156 | self._log("Closing %d registry handles..." % len(registryHandles)) 157 | for handle in registryHandles: 158 | reg.CloseKey(handle) 159 | self._log("Done. Subkey enumeration completed.") 160 | return subkeys 161 | 162 | def read_cryptoapi(self): 163 | certData = {} 164 | self._log("Retrieving certificate data from CryptoAPI") 165 | for storename in ("CA", "ROOT", "MY"): 166 | self._log("Gathering information from store: %s" % storename) 167 | with wincertstore.CertSystemStore(storename) as store: 168 | storecerts = {} 169 | for cert in store.itercerts(usage=None): 170 | certName = cert.get_name() 171 | self._log("Processing certificate: %s" % certName) 172 | keyName = re.sub(r"[\W]+", '', cert.get_name()) 173 | pem = cert.get_pem().decode("ascii") 174 | encodedDer = ''.join(pem.split("\n")[1:-2]) 175 | der = base64.b64decode(encodedDer) 176 | h = sha1() 177 | h.update(der) 178 | thumbprint = h.hexdigest() 179 | certificateInfo = { 180 | "Name": certName, 181 | "Thumbprint": thumbprint, 182 | "PEM": pem 183 | } 184 | self._log("Processing DER data for certificate: %s" % certName) 185 | derInfo = self._parse_der(der) 186 | for key in derInfo: 187 | certificateInfo[key] = derInfo[key] 188 | storecerts[keyName] = certificateInfo 189 | self._log("Finished processing certificate: %s" % certName) 190 | certData[storename] = storecerts 191 | return certData 192 | 193 | def get_unknown_certificate(self, thumbprint): 194 | certData = self.read_cryptoapi() 195 | for storeName in certData: 196 | for certificate in certData[storeName]: 197 | if certData[storeName][certificate]["Thumbprint"].upper() == thumbprint: 198 | return certData[storeName][certificate] 199 | return False 200 | 201 | def remove_certificate(self, certificate): 202 | CONTAINS_SUBKEYS = 0 203 | registryHandles = [] 204 | returnValue = False 205 | path = certificate["RegPath"].split("/") 206 | hiveName = path.pop(0) 207 | keyName = path.pop(-1) 208 | hive = reg.ConnectRegistry(None, self.regKeys[hiveName][0]) 209 | registryHandle = reg.OpenKey(hive, self.regKeys[hiveName][1]) 210 | self._log("Connected to registry at location: %s" % hiveName) 211 | for step in path: 212 | registryHandles.append(registryHandle) 213 | registryHandle = reg.OpenKey(registryHandle, step) 214 | try: 215 | deletionCandidate = reg.OpenKey(registryHandle, keyName) 216 | self._log("Querying deletion canditate: %s" % certificate["RegPath"]) 217 | if not reg.QueryInfoKey(deletionCandidate)[CONTAINS_SUBKEYS]: 218 | self._log("Attempting to delete key: %s" % certificate["RegPath"]) 219 | reg.CloseKey(deletionCandidate) 220 | reg.DeleteKey(registryHandle, keyName) 221 | self._log("Deleted key: %s" % certificate["RegPath"]) 222 | returnValue = True 223 | else: 224 | self._error_log("Unable to delete key: %s. Key contains subkeys." % certificate["RegPath"]) 225 | registryHandles.append(deletionCandidate) 226 | raise WindowsError 227 | except WindowsError as e: 228 | self._error_log("Unable to delete key: %s. Windows error." % certificate["RegPath"]) 229 | self._error_log("%s: %s" % (certificate["RegPath"], str(e))) 230 | pass 231 | self._log("Closing registry handles...") 232 | for handle in registryHandles: 233 | reg.CloseKey(handle) 234 | reg.CloseKey(hive) 235 | self._log("Registry handles closed.") 236 | return returnValue 237 | 238 | def _parse_der(self, der): 239 | sequences = [ 240 | "issuer", 241 | "validity", 242 | "subject" 243 | ] 244 | infoMap = { 245 | "2.5.4.10": "Organization", 246 | "2.5.4.11": "OU", 247 | "2.5.4.6": "Country", 248 | "2.5.4.3": "CN" 249 | } 250 | certificateInfo = {} 251 | cert = decoder.decode(der, asn1Spec=rfc2459.Certificate())[0] 252 | cert = cert["tbsCertificate"] 253 | for sequence in sequences: 254 | rdnsequence = cert[sequence][0] 255 | for rdn in rdnsequence: 256 | if not rdn: 257 | continue 258 | if len(rdn[0]) > 1: 259 | oid, value = rdn[0] 260 | oid = str(oid) 261 | value = ''.join(re.findall(r"[A-Za-z0-9\.\s]+", str(value))) 262 | try: 263 | if not infoMap[oid] == "Type": 264 | certificateInfo[infoMap[oid]] = value 265 | else: 266 | try: 267 | certificateInfo[infoMap[oid]] += ", %s" % value 268 | except KeyError: 269 | certificateInfo[infoMap[oid]] = value 270 | except KeyError: 271 | pass 272 | else: 273 | try: 274 | certificateInfo["Valid"] += ", %s" % str(rdn) 275 | except KeyError: 276 | certificateInfo["Valid"] = str(rdn) 277 | return certificateInfo 278 | 279 | def _error_log(self, msg): 280 | self.logCallback(msg, messageType="ERROR") 281 | 282 | def _log(self, msg): 283 | self.logCallback(msg) 284 | 285 | @staticmethod 286 | def _hash(keyData): 287 | h = sha256() 288 | h.update(str(keyData)) 289 | return h.hexdigest() 290 | 291 | 292 | class DatabaseEngine: 293 | 294 | def __init__(self, logCallback): 295 | self.logCallback = logCallback 296 | self.home = os.path.dirname(os.path.realpath(__file__)) 297 | self.dbFile = os.path.join(self.home, "certificates.db") 298 | self.database = self._open_database() 299 | self.queries = [] 300 | 301 | def close(self): 302 | self.database["cursor"].close() 303 | self.database["handle"].commit() 304 | self.database["handle"].close() 305 | 306 | def run_query(self, preparedQuery, queryInfo=None): 307 | try: 308 | if not queryInfo: 309 | self.database["cursor"].execute(preparedQuery) 310 | else: 311 | if (type(queryInfo) == str) or (type(queryInfo) == unicode): 312 | queryInfo = (queryInfo,) 313 | self.database["cursor"].execute(preparedQuery, queryInfo) 314 | result = self.database["cursor"].fetchall() 315 | self.database["handle"].commit() 316 | if not result: 317 | return True 318 | else: 319 | return result 320 | except sqlite3.Error as e: 321 | self._error_log("Database Error: %s" % str(e)) 322 | return False 323 | 324 | def run_query_batch(self): 325 | self._log("Batch executing %d queries..." % len(self.queries)) 326 | for query in self.queries: 327 | if (type(query[1]) == str) or (type(query[1]) == unicode): 328 | query[1] = (query[1],) 329 | try: 330 | self.database["cursor"].execute(query[0], query[1]) 331 | except sqlite3.Error as e: 332 | self._error_log("Database Error: %s" % str(e)) 333 | continue 334 | self.database["handle"].commit() 335 | self._log("Committing database changes...") 336 | return True 337 | 338 | def queue_query(self, preparedQuery, queryInfo): 339 | self.queries.append([preparedQuery, queryInfo]) 340 | 341 | def get_watch_keys(self): 342 | keys = [] 343 | result = self.run_query("SELECT path FROM registry") 344 | if not type(result) == bool: 345 | for line in result: 346 | line = line[0].split("/") 347 | line.pop(-1) 348 | watchKey = "/".join(line) 349 | if not watchKey in keys: 350 | keys.append(watchKey) 351 | return keys 352 | 353 | def certificate_is_known(self, thumbprint): 354 | query = "SELECT * FROM %s WHERE thumbprint=? LIMIT 1" 355 | tables = [ 356 | "registry", 357 | "cryptoapi" 358 | ] 359 | for table in tables: 360 | result = self.run_query(query % table, thumbprint) 361 | if not type(result) == bool: 362 | return True 363 | return False 364 | 365 | def certificate_is_active(self, thumbprint): 366 | result = self.run_query("SELECT status FROM registry WHERE thumbprint=? LIMIT 1", thumbprint) 367 | if not type(result) == bool: 368 | if not result[0][0] == -1: 369 | return True 370 | return False 371 | 372 | def get_certificate(self, thumbprint): 373 | result = {} 374 | query = "SELECT * FROM %s WHERE thumbprint=?" 375 | tables = [ 376 | "registry", 377 | "cryptoapi" 378 | ] 379 | for table in tables: 380 | queryResult = self.run_query(query % table, thumbprint) 381 | if type(queryResult) == list: 382 | result[table] = queryResult 383 | return result 384 | 385 | def set_certificate_inactive(self, path, thumbprint): 386 | self.queue_query("UPDATE registry SET status=-1 WHERE path=?", path) 387 | self.queue_query("UPDATE cryptoapi SET status=-1 WHERE thumbprint=?", thumbprint) 388 | self.run_query_batch() 389 | return True 390 | 391 | def correlate_tables(self): 392 | self._log("Correlating database tables...") 393 | regResult = self.run_query("SELECT thumbprint FROM registry") 394 | for thumbprint in regResult: 395 | thumbprint = thumbprint[0] 396 | self._log("Checking thumbprint: %s" % thumbprint) 397 | apiResult = self.run_query("SELECT thumbprint FROM registry WHERE thumbprint=?", thumbprint) 398 | if apiResult: 399 | self._log("Thumbprint exists in both tables. Queuing correlate flag change...") 400 | tables = ["registry", "cryptoapi"] 401 | for table in tables: 402 | self.queue_query("UPDATE %s SET correlated=1 WHERE thumbprint=?" % table, thumbprint) 403 | """ 404 | metaResult = self.run_query("SELECT thumbprint FROM meta WHERE thumbprint=?", thumbprint) 405 | if type(metaResult) == bool: 406 | self.queue_query("INSERT INTO meta VALUES (?,?,?,?,?)", [thumbprint, 0, 1, 0, None]) 407 | """ 408 | if self.queries: 409 | self._log("Executing %d changes..." % len(self.queries)) 410 | self.run_query_batch() 411 | self._log("Correlation finished!") 412 | return True 413 | 414 | def prepare_baseline_queries(self, registryInfo, apiInfo): 415 | regQuery = [] 416 | apiQuery = [] 417 | baseQuery = "INSERT INTO registry VALUES (?,?,?,?,?,?)" 418 | for registryHive in registryInfo: 419 | for activeStore in registryInfo[registryHive]: 420 | for thumbprint in registryInfo[registryHive][activeStore]: 421 | if not self.certificate_is_known(thumbprint): 422 | certificate = registryInfo[registryHive][activeStore][thumbprint] 423 | queryData = [ 424 | thumbprint, 425 | certificate["path"], 426 | certificate["hash"], 427 | certificate["blob"], 428 | 0, 429 | 0 430 | ] 431 | regQuery.append([baseQuery, queryData]) 432 | baseQuery = "INSERT INTO cryptoapi VALUES (?,?,?,?,?,?,?,?,?,?)" 433 | requiredInfo = ["Thumbprint", "Name", "Organization", "OU", "CN", "Country", "Valid", "PEM"] 434 | for store in apiInfo: 435 | for certificate in apiInfo[store]: 436 | if not self.certificate_is_known(apiInfo[store][certificate]["Thumbprint"]): 437 | preparedInfo = self._prepare_certificate_info(apiInfo[store][certificate], requiredInfo) 438 | for i in range(2): 439 | preparedInfo.append(0) 440 | apiQuery.append([baseQuery, preparedInfo]) 441 | queryContainers = [regQuery, apiQuery] 442 | i = 0 443 | for container in queryContainers: 444 | for query in container: 445 | self.queries.append(query) 446 | i += 1 447 | self._log("Prepared %d certificates for baseline." % i) 448 | return True 449 | 450 | def _open_database(self): 451 | self._log("Creating database handle...") 452 | if not os.path.isfile(self.dbFile): 453 | self._create_database() 454 | db = sqlite3.connect(self.dbFile) 455 | c = db.cursor() 456 | database = { 457 | "handle": db, 458 | "cursor": c 459 | } 460 | self._log("Handle created successfully!") 461 | return database 462 | 463 | def _create_database(self): 464 | if not os.path.isfile(self.dbFile): 465 | self._log("Database not found, creating...") 466 | db = sqlite3.connect(self.dbFile) 467 | c = db.cursor() 468 | c.execute("CREATE TABLE registry(thumbprint,path,hash,blob,status,correlated)") 469 | c.execute("CREATE TABLE cryptoapi(thumbprint,name,organization,ou,cn,country,valid,pem,status,correlated)") 470 | c.execute("CREATE TABLE meta(thumbprint,status,correlated,notify,message)") 471 | c.close() 472 | db.commit() 473 | db.close() 474 | self._log("Database created successfully!") 475 | return True 476 | else: 477 | self._log("Database already exists!") 478 | return False 479 | 480 | def _error_log(self, msg): 481 | self.logCallback(msg, messageType="ERROR") 482 | 483 | def _log(self, msg): 484 | self.logCallback(msg) 485 | 486 | @staticmethod 487 | def _prepare_certificate_info(certificateData, requiredInfo): 488 | preparedInfo = [] 489 | for info in requiredInfo: 490 | if info in certificateData: 491 | preparedInfo.append(certificateData[info]) 492 | else: 493 | preparedInfo.append(None) 494 | return preparedInfo 495 | 496 | 497 | class SigCheckWrapper: 498 | 499 | def __init__(self): 500 | self.resources = os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources") 501 | self.sigcheck = os.path.join(self.resources, "sigcheck.exe") 502 | self._validate() 503 | 504 | def check_store(self): 505 | storeNameRegex = re.compile(r"^.+[\\].+[:]$") 506 | certEndRegex = re.compile(r"^[V].+[o][:].+\d+[:]\d{2}.+[/]\d+[/]\d+.$") 507 | sc_process = subprocess.Popen([self.sigcheck, "-tv"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 508 | output, error = sc_process.communicate() 509 | unapproved_certs = {} 510 | current_store = None 511 | current_cert = [] 512 | for line in output.split("\n"): 513 | store_name = re.findall(storeNameRegex, line.rstrip("\r")) 514 | cert_end = re.findall(certEndRegex, line.strip()) 515 | if store_name: 516 | current_store = store_name[0] 517 | unapproved_certs[current_store] = {} 518 | elif cert_end: 519 | if current_store: 520 | current_cert.append(line) 521 | cert_name = current_cert.pop(0) 522 | unapproved_certs[current_store][cert_name] = current_cert 523 | current_cert = [] 524 | else: 525 | if current_store: 526 | current_cert.append(line) 527 | return self._prepare_output(unapproved_certs) 528 | 529 | def _validate(self): 530 | if not os.path.isfile(self.sigcheck): 531 | sys.stderr.write("ERROR: Resource not found: %s" % self.sigcheck) 532 | sys.exit(1) 533 | 534 | @staticmethod 535 | def _prepare_output(output): 536 | certificates = {} 537 | for store in output: 538 | for certificate in output[store]: 539 | h = sha256() 540 | h.update(certificate) 541 | h.update(store) 542 | h.update(str(output[store][certificate])) 543 | certHandle = h.hexdigest() 544 | certificates[certHandle] = { 545 | "Store": store.strip(), 546 | "Name": certificate.strip(), 547 | } 548 | for certInfo in output[store][certificate]: 549 | key, data = certInfo.split(":\t") 550 | certificates[certHandle][key.strip()] = data.strip() 551 | return certificates 552 | 553 | 554 | class BuiltInNotifier: 555 | 556 | def __init__(self): 557 | self.tempdir = os.path.join(os.environ["temp"], str(uuid.uuid4())) 558 | os.mkdir(self.tempdir) 559 | pass 560 | 561 | def confirm(self, msg): 562 | scriptPath = os.path.join(self.tempdir, "%s.vbs" % str(uuid.uuid4())) 563 | msgContent = msg["content"].replace("\n", '" & vbcrlf & "') 564 | script = [ 565 | 'result = MsgBox("%s", vbYesNo, "%s")' % (msgContent, msg["header"]), 566 | "exitCode = 0", 567 | "if result = vbYes Then exitCode = 1", 568 | "wscript.Quit(exitCode)" 569 | ] 570 | with open(scriptPath, "w") as outfile: 571 | for line in script: 572 | outfile.write("%s\n" % line) 573 | p = subprocess.Popen(["wscript.exe", scriptPath], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 574 | p.communicate() 575 | rc = p.returncode 576 | os.unlink(scriptPath) 577 | if rc == 1: 578 | return True 579 | else: 580 | return False 581 | 582 | def notify(self, msg): 583 | scriptPath = os.path.join(self.tempdir, "%s.vbs" % str(uuid.uuid4())) 584 | msgContent = msg["content"].replace("\n", '" & vbcrlf & "') 585 | script = [ 586 | 'result = MsgBox("%s", vbOKOnly, "%s")' % (msgContent, msg["header"]), 587 | "exitCode = 0", 588 | "if result = vbYes Then exitCode = 1", 589 | "wscript.Quit(exitCode)" 590 | ] 591 | with open(scriptPath, "w") as outfile: 592 | for line in script: 593 | outfile.write("%s\n" % line) 594 | p = subprocess.Popen(["wscript.exe", scriptPath], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 595 | p.wait() 596 | os.unlink(scriptPath) 597 | return True 598 | 599 | 600 | class WatchDog: 601 | 602 | def __init__(self, logCallback): 603 | self.csm = CertificateStoreManager(logCallback) 604 | self.database = DatabaseEngine(logCallback) 605 | self.notifier = BuiltInNotifier() 606 | self.sigcheck = SigCheckWrapper() 607 | self.logCallback = logCallback 608 | self.expect_change = False 609 | self.watchThreads = [] 610 | self.changedKeys = [] 611 | self.alive = False 612 | 613 | def establish_baseline(self): 614 | api = self.csm.read_cryptoapi() 615 | registry = self.csm.read_registry() 616 | self.database.prepare_baseline_queries(registry, api) 617 | self.database.run_query_batch() 618 | self.database.correlate_tables() 619 | return True 620 | 621 | def check_store(self): 622 | self._log("Checking for unauthorized certificates...") 623 | unauthorized_certificates = self.sigcheck.check_store() 624 | self._log("SigCheck wrapper returned successfully", messageType="DEBUG") 625 | if not len(unauthorized_certificates): 626 | self._log("No unauthorized certificates found.") 627 | else: 628 | self._log("Sending unauthorized certificates for removal.", messageType="DEBUG") 629 | self._certificate_removal_helper(unauthorized_certificates) 630 | self._log("SigCheck operations completed.") 631 | 632 | def _certificate_removal_helper(self, unauthorized_certificates): 633 | certificateInfo = [ 634 | "Store", 635 | "Cert Issuer", 636 | "Name", 637 | "Serial Number", 638 | "Valid Usage", 639 | "Cert Status", 640 | "Valid from", 641 | "Valid to", 642 | "Thumbprint" 643 | ] 644 | for certificate in unauthorized_certificates: 645 | self._log("Checking info for certificate: %s" % unauthorized_certificates[certificate]["Name"]) 646 | removalMessage = { 647 | "header": "CSM WatchDog", 648 | "content": "SigCheck has detected a certificate that is not in the default Windows store!\n\n" 649 | "Certificate Info:\n\n" 650 | } 651 | for info in certificateInfo: 652 | if not len(info) < 9: 653 | messageAddition = "%s:\t%s\n" % (info, unauthorized_certificates[certificate][info]) 654 | else: 655 | messageAddition = "%s:\t\t%s\n" % (info, unauthorized_certificates[certificate][info]) 656 | removalMessage["content"] += messageAddition 657 | if self.database.certificate_is_known(unauthorized_certificates[certificate]["Thumbprint"]): 658 | self._log("CSM can remove this certificate! Notifying user.") 659 | removalMessage["content"] += "\nCSM has isolated this certificate and can remove it for you.\n\n" 660 | removalMessage["content"] += "Would you like to remove this certificate?" 661 | confirmation = self.notifier.confirm(removalMessage) 662 | if confirmation: 663 | self._log("Removing certificate...") 664 | self._remove_certificate(unauthorized_certificates[certificate]) 665 | else: 666 | self._log("CSM is unable to remove this certificate.") 667 | removalMessage["content"] += "\nCSM was unable to isolate this certificate for you.\n\n" 668 | removalMessage["content"] += "Please use the Windows Certificate Manager to manually remove this " \ 669 | "certificate" 670 | self.notifier.notify(removalMessage) 671 | 672 | def _remove_certificate(self, certificate): 673 | DB_THUMBPRINT = 0 674 | DB_PATH = 1 675 | storeNameMap = { 676 | "MACHINE": "LM_STORE", 677 | "CA": "Root", 678 | "USER": "CU_STORE" 679 | } 680 | notified = [] 681 | removalSuccess = False 682 | tables = self.database.get_certificate(certificate["Thumbprint"]) 683 | for table in tables: 684 | self._log("Checking table: %s" % table, messageType="DEBUG") 685 | for DB_CERT in tables[table]: 686 | self._log("Checking cert: %s" % DB_CERT[DB_THUMBPRINT], messageType="DEBUG") 687 | if self.database.certificate_is_active(DB_CERT[DB_THUMBPRINT]): 688 | self._log("Certificate is active: %s" % DB_CERT[DB_THUMBPRINT], messageType="DEBUG") 689 | store = certificate["Store"].split(":")[0].split("\\") 690 | if store[1].upper() == "CA": 691 | regStoreName = storeNameMap[store[1].upper()] 692 | store[1] = "Root" 693 | else: 694 | regStoreName = storeNameMap[store[0].upper()] 695 | self._log(regStoreName, messageType="DEBUG") 696 | if regStoreName in DB_CERT[DB_PATH].split("/"): 697 | self._log("Path validation First Step Complete.", messageType="DEBUG") 698 | if store[1] not in DB_CERT[DB_PATH]: 699 | if store[1] == "Root": 700 | store[1] = "CA" 701 | if store[1] in DB_CERT[DB_PATH]: 702 | self._log("Path validation Second Step Complete.", messageType="DEBUG") 703 | certificate["RegPath"] = DB_CERT[DB_PATH] 704 | self._log("Removing certificate from registry...") 705 | self.expect_change = True 706 | removalSuccess = self.csm.remove_certificate(certificate) 707 | self.expect_change = False 708 | removalMessage = { 709 | "header": "CSM Removal Activity", 710 | "content": "No message yet..." 711 | } 712 | self._log("Certificate removed: %s" % removalSuccess, messageType="DEBUG") 713 | if removalSuccess: 714 | self._log("Certificate successfully removed!") 715 | removalMessage["content"] = "Certificate removed successfully!" 716 | else: 717 | self._log("Unable to remove certficate. Notifying user...") 718 | removalMessage["content"] = "CSM was unable to remove the certificate. " \ 719 | "Please use the Windows Certificate Manager " \ 720 | "to manually remove it." 721 | self._log("Checking if notification is necessary...", messageType="DEBUG") 722 | h = sha256() 723 | h.update(str(DB_CERT)) 724 | certHash = h.hexdigest() 725 | if not certHash in notified: 726 | self.notifier.notify(removalMessage) 727 | notified.append(certHash) 728 | else: 729 | self._log(DB_CERT[DB_PATH]) 730 | if removalSuccess: 731 | self._log("Setting certificate to 'Removed' in database.", messageType="DEBUG") 732 | self.database.set_certificate_inactive(certificate["RegPath"], certificate["Thumbprint"]) 733 | self._log("Removal actions completed.") 734 | return removalSuccess 735 | 736 | def _watch_thread_dispatcher(self): 737 | MSstore = r"Software\Microsoft\SystemCertificates" 738 | GPstore = r"Software\Policy\Microsoft\SystemCertificates" 739 | regKeys = { 740 | "CU_STORE": [win32con.HKEY_CURRENT_USER, MSstore], 741 | "LM_STORE": [win32con.HKEY_LOCAL_MACHINE, MSstore], 742 | "USER_STORE": [win32con.HKEY_USERS, MSstore], 743 | "CU_POLICY_STORE": [win32con.HKEY_CURRENT_USER, GPstore], 744 | "LM_POLICY_STORE": [win32con.HKEY_LOCAL_MACHINE, GPstore] 745 | } 746 | watchKeys = self.database.get_watch_keys() 747 | for regKey in watchKeys: 748 | self._log("Dispatcher preparing watch thread for key: %s" % regKey, messageType="DEBUG") 749 | key = regKey.split("/") 750 | storeName = key.pop(0) 751 | additionalValue = "\\%s" % "\\".join(key) 752 | keystore = regKeys[storeName] 753 | keyName = keystore[1] + additionalValue 754 | t = threading.Thread(target=self._watch_thread, args=(keystore[0], keyName, regKey, 755 | self._watch_thread_callback,)) 756 | self.watchThreads.append(t) 757 | self._log("Thread prepared.", messageType="DEBUG") 758 | self._log("Launching %d threads..." % len(self.watchThreads), messageType="DEBUG") 759 | for t in self.watchThreads: 760 | t.start() 761 | self._log("Dispatcher completed.", messageType="DEBUG") 762 | return 763 | 764 | def _watch_thread(self, hive, watchKey, name, callback): 765 | self._log("Watch thread active for key: %s" % name, messageType="DEBUG") 766 | while self.alive: 767 | watchHandle = win32api.RegOpenKey(hive, watchKey, 0, win32con.KEY_NOTIFY) 768 | win32api.RegNotifyChangeKeyValue(watchHandle, False, win32api.REG_NOTIFY_CHANGE_NAME, None, False) 769 | win32api.RegCloseKey(watchHandle) 770 | self._log("Change detected in thread: %s" % name, messageType="DEBUG") 771 | callback(name) 772 | self._log("Watch thread returning for key: %s" % name, messageType="DEBUG") 773 | return 774 | 775 | def _watch_thread_callback(self, regKey): 776 | if not self.expect_change: 777 | self._log("Unexpected change detected in registry key: %s" % regKey) 778 | self.changedKeys.append(regKey) 779 | else: 780 | self._log("Registry change detected, but it was expected. Change ignored.", messageType="DEBUG") 781 | return 782 | 783 | def _registry_change_finder(self, watchKeys): 784 | baseline = self._get_registry_baseline(watchKeys) 785 | while self.alive: 786 | if self.changedKeys: 787 | key = self.changedKeys.pop(0) 788 | self._log("Searching for changes in key: %s" % key, messageType="DEBUG") 789 | subkeys = self.csm.read_subkeys(key) 790 | if len(baseline[key]) > len(subkeys): 791 | for subkey in baseline[key]: 792 | if subkey not in subkeys: 793 | self._log("Certificate removed from registry. Thumbprint: %s" % subkey) 794 | baseline = self._get_registry_baseline(watchKeys) 795 | else: 796 | for subkey in subkeys: 797 | if subkey not in baseline[key]: 798 | self._log("Certificate added to registry. Thumbprint: %s" % subkey) 799 | certificate = self.csm.get_unknown_certificate(subkey) 800 | self._registry_change_handler(key, subkey, certificate) 801 | baseline = self._get_registry_baseline(watchKeys) 802 | else: 803 | time.sleep(1) 804 | 805 | def _registry_change_handler(self, key, subkey, certificate=None): 806 | certificateInfo = [ 807 | "Name", 808 | "Organization", 809 | "Country", 810 | "CN" 811 | ] 812 | notification = { 813 | "header": "CSM WatchDog: Change Detected", 814 | "content": "CSM WatchDog detected a new certificate in your certificate store.\n\n" 815 | "Thumbprint: %s\n\n" % subkey 816 | } 817 | if certificate: 818 | notification["content"] += "Additional information (Gathered from Windows CryptoAPI): \n\n" 819 | for info in certificateInfo: 820 | if not len(info) < 9: 821 | notification["content"] += "%s:\t" % info 822 | else: 823 | notification["content"] += "%s:\t\t" % info 824 | try: 825 | notification["content"] += "%s\n" % certificate[info] 826 | except KeyError: 827 | notification["content"] += "Unavailable\n" 828 | else: 829 | notification["content"] += "Additional information unavailable from the Windows CryptoApi.\n\n" 830 | notification["content"] += "\nCSM has isolated this certificate and can remove it for you.\n\n" \ 831 | "Would you like to remove this certificate?" 832 | confirmation = self.notifier.confirm(notification) 833 | if confirmation: 834 | key += "/%s" % subkey 835 | certificate["RegPath"] = key 836 | self.expect_change = True 837 | removalSuccess = self.csm.remove_certificate(certificate) 838 | self.expect_change = False 839 | notification = { 840 | "header": "CSM WatchDog: Certificate Removal", 841 | "content": "Nothing yet..." 842 | } 843 | if removalSuccess: 844 | notification["content"] = "Certificate removed successfully!" 845 | else: 846 | notification["content"] = "Unable to remove certificate. Please remove it manually using the Windows" \ 847 | "Certificate Manager." 848 | self.notifier.notify(notification) 849 | else: 850 | self._log("Ignoring certificate: %s" % key) 851 | 852 | def _get_registry_baseline(self, watchKeys): 853 | baseline = {} 854 | for key in watchKeys: 855 | subkeys = self.csm.read_subkeys(key) 856 | baseline[key] = subkeys 857 | return baseline 858 | 859 | def run(self, baselineEstablished=True): 860 | self._log("Initializing WatchDog...") 861 | self.alive = True 862 | if not baselineEstablished: 863 | self._log("Establishing certificate store baseline...") 864 | self.establish_baseline() 865 | self._log("Baseline established.") 866 | watchKeys = self.database.get_watch_keys() 867 | threads = [ 868 | threading.Thread(target=self._registry_change_finder, args=(watchKeys,)) 869 | ] 870 | self._log("WatchDog starting %d threads..." % len(threads), messageType="DEBUG") 871 | for t in threads: 872 | t.start() 873 | self._log("WatchDog starting watch thread dispatcher...", messageType="DEBUG") 874 | self._watch_thread_dispatcher() 875 | self._log("Running initial scan...") 876 | self.check_store() 877 | self._log("WatchDog Initialization completed.") 878 | return 879 | 880 | def exit(self): 881 | self._log("Stopping WatchDog...") 882 | self.alive = False 883 | self._log("Done.") 884 | return 885 | 886 | def _log(self, msg, messageType="WatchDog"): 887 | self.logCallback(msg, messageType) 888 | 889 | 890 | class Core: 891 | 892 | def __init__(self, config): 893 | self.msgQueue = [] 894 | self.logQueue = [] 895 | self.basedir = os.path.dirname(os.path.realpath(__file__)) 896 | self.config = config 897 | self.active = True 898 | 899 | def _centralized_logging(self, logfile): 900 | while self.active: 901 | if self.logQueue: 902 | msg = "%s\n" % self.logQueue.pop(0) 903 | with open(logfile, "a") as outfile: 904 | outfile.write(msg) 905 | else: 906 | time.sleep(5) 907 | return 908 | 909 | def _centralized_stdout(self): 910 | while self.active: 911 | if self.msgQueue: 912 | msg = "%s\n" % self.msgQueue.pop(0) 913 | sys.stdout.write(msg) 914 | else: 915 | time.sleep(5) 916 | return 917 | 918 | def _flush_messages(self): 919 | self.log_callback("Flushing messages...") 920 | while self.msgQueue: 921 | pass 922 | while self.logQueue: 923 | pass 924 | self.log_callback("Done!") 925 | return 926 | 927 | def log_callback(self, msg, messageType="DEBUG"): 928 | if messageType == "DEBUG": 929 | if not self.config["log level"] == "DEBUG": 930 | return 931 | msg = "[%s] [%s] %s" % (strftime("%H:%M:%S | %d/%m/%Y"), messageType, msg) 932 | self.msgQueue.append(msg) 933 | self.logQueue.append(msg) 934 | return 935 | 936 | def run(self): 937 | self._elevate_permissions() 938 | logfile = os.path.join(self.basedir, self.config["log name"]) 939 | threads = [ 940 | threading.Thread(target=self._centralized_logging, args=(logfile,)), 941 | threading.Thread(target=self._centralized_stdout) 942 | ] 943 | for thread in threads: 944 | thread.start() 945 | return 946 | 947 | def exit(self): 948 | self._flush_messages() 949 | self.active = False 950 | 951 | @staticmethod 952 | def _elevate_permissions(): 953 | f = sys.executable 954 | if not __file__.split("\\")[-1].split(".")[-1] == "py": 955 | f = __file__ 956 | params = ' '.join(sys.argv[1:] + ["asadmin"]) 957 | else: 958 | script = os.path.abspath(sys.argv[0]) 959 | params = ' '.join([script] + sys.argv[1:] + ['asadmin']) 960 | print params 961 | if not sys.argv[-1] == 'asadmin': 962 | shell.ShellExecuteEx(lpVerb='runas', lpFile=f, lpParameters=params) 963 | sys.exit(0) 964 | 965 | if __name__ == "__main__": 966 | configFile = os.path.join(os.path.dirname(os.path.realpath(__file__)), "config.json") 967 | if os.path.isfile(configFile): 968 | with open(configFile, "r") as infile: 969 | config = json.loads(infile.read()) 970 | else: 971 | config = { 972 | "log level": "Quiet", 973 | "log name": "log.txt", 974 | "baseline established": False 975 | } 976 | core = Core(config) 977 | watchdog = WatchDog(core.log_callback) 978 | core.run() 979 | watchdog.run(baselineEstablished=config["baseline established"]) 980 | config["baseline established"] = True 981 | with open(configFile, "w") as outfile: 982 | outfile.write(json.dumps(config)) 983 | core.log_callback("CSM Initialized successfully.") 984 | 985 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyasn1 2 | pypiwin32 3 | wincertstore 4 | pyasn1-modules -------------------------------------------------------------------------------- /resources/readme.txt: -------------------------------------------------------------------------------- 1 | Put SigCheck.exe in this folder. --------------------------------------------------------------------------------