|
| 1 | +from io import BytesIO |
| 2 | +import pefile |
| 3 | + |
| 4 | + |
| 5 | +class NXCModule: |
| 6 | + """ |
| 7 | + Module for detecting Windows lock screen backdoors |
| 8 | + Module by @E1A |
| 9 | + """ |
| 10 | + |
| 11 | + name = "lockscreendoors" |
| 12 | + description = "Detect Windows lock screen backdoors by checking FileDescriptions of accessibility binaries." |
| 13 | + supported_protocols = ["smb"] |
| 14 | + |
| 15 | + def __init__(self): |
| 16 | + # List of exe names with expected descriptions |
| 17 | + self.expected_descriptions = { |
| 18 | + "utilman.exe": ["Utility Manager"], |
| 19 | + "narrator.exe": ["Screen Reader", "Narrator"], |
| 20 | + "sethc.exe": ["Accessibility shortcut keys"], |
| 21 | + "osk.exe": ["Accessibility On-Screen Keyboard"], |
| 22 | + "magnify.exe": ["Microsoft Screen Magnifier"], |
| 23 | + "EaseOfAccessDialog.exe": ["Ease of Access Dialog Host"], |
| 24 | + "voiceaccess.exe": ["Voice access"], # Only on Windows 11 / Server 2025+ |
| 25 | + "displayswitch.exe": ["Display Switch"], |
| 26 | + "atbroker.exe": ["Windows Assistive Technology Manager", "Transitions Accessible technologies between desktops"], |
| 27 | + } |
| 28 | + |
| 29 | + # If description matches one of these it's almost certainly backdoored |
| 30 | + self.backdoor_descriptions = [ |
| 31 | + "Windows Command Processor", |
| 32 | + "Windows PowerShell" |
| 33 | + ] |
| 34 | + |
| 35 | + def options(self, context, module_options): |
| 36 | + """No options available""" |
| 37 | + |
| 38 | + def get_description(self, binary_data): |
| 39 | + # Extract the file description from version info |
| 40 | + try: |
| 41 | + pe = pefile.PE(data=binary_data, fast_load=True) |
| 42 | + pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"]]) |
| 43 | + for fileinfo in pe.FileInfo: |
| 44 | + for entry in fileinfo: |
| 45 | + if entry.Key.decode() == "StringFileInfo": |
| 46 | + for st in entry.StringTable: |
| 47 | + desc = st.entries.get(b"FileDescription") |
| 48 | + if desc: |
| 49 | + return desc.decode().strip() |
| 50 | + except Exception as e: |
| 51 | + self.context.log.debug(f"Failed to extract PE info: {e}") |
| 52 | + return None |
| 53 | + |
| 54 | + def on_admin_login(self, context, connection): |
| 55 | + target_path = "\\Windows\\System32" |
| 56 | + tampered = False |
| 57 | + |
| 58 | + for exe, expected_descs in self.expected_descriptions.items(): |
| 59 | + try: |
| 60 | + # Grab the binary from the share |
| 61 | + buf = BytesIO() |
| 62 | + connection.conn.getFile("C$", f"{target_path}\\{exe}", buf.write) |
| 63 | + binary = buf.getvalue() |
| 64 | + |
| 65 | + # Extract and normalize the file description |
| 66 | + file_desc = self.get_description(binary) |
| 67 | + if not file_desc: |
| 68 | + context.log.fail(f"{exe}: could not extract FileDescription") |
| 69 | + continue |
| 70 | + |
| 71 | + # Check if the description is as expected |
| 72 | + if file_desc not in expected_descs: |
| 73 | + tampered = True |
| 74 | + if file_desc in self.backdoor_descriptions: |
| 75 | + context.log.highlight(f"BACKDOOR DETECTED: {exe} has FileDescription '{file_desc}'") |
| 76 | + else: |
| 77 | + if len(expected_descs) == 1: |
| 78 | + expected_str = f"'{expected_descs[0]}'" |
| 79 | + else: |
| 80 | + expected_str = ", ".join(f"'{d}'" for d in expected_descs) |
| 81 | + expected_str = f"one of: {expected_str}" |
| 82 | + context.log.highlight(f"SUSPICIOUS: {exe} has unexpected FileDescription '{file_desc}' (expected {expected_str})") |
| 83 | + except Exception as e: |
| 84 | + context.log.debug(f"Failed to process {exe}: {e}") |
| 85 | + |
| 86 | + if not tampered: |
| 87 | + context.log.display("All lock screen executable descriptions are consistent with the expected values") |
0 commit comments