脚本外挂的检测及预防

| 分类 甜点  | 标签 A_ScriptHwnd  HWND  消息 

导言:一些游戏在运行时能检测并屏蔽脚本外挂,原理是什么?有什么办法可以知道当前系统中开了多少脚本?包括使用默认托盘图标、自定义图标甚至隐藏了托盘图标的脚本。还有,能发现已编译成可执行文件的脚本吗?是否能控制这些脚本??

检测在运行的脚本

获取系统中当前运行的所有 AutoHotkey 脚本的信息(下面的脚本获取脚本标题和进程路径):

DetectHiddenWindows, on
WinGet, AHKWinList, List, ahk_class AutoHotkey 
Loop, %AHKWinList%
{
  AHKWinHWND := AHKWinList%A_Index%
  WinGetTitle, AHKWinTitle, ahk_id %AHKWinHWND%
  WinGet, AHKWinProcessPath, ProcessPath, ahk_id %AHKWinHWND%
  MsgBox, % "脚本标题:" AHKWinTitle "`n脚本进程路径:" AHKWinProcessPath
}
return

由于可以获取到窗口句柄,所以可以直接通过窗口命令获取指定脚本的各种信息。其中脚本标题中包含了脚本文件名和执行脚本的 AutoHotkey.exe 版本号,例如:

D:\Software\AutoHotkey\Scripts\test.ahk - AutoHotkey v1.1.15.00

当然,还可以使用其他方法。下面通过消息获取某脚本的进程 ID:

AHKScriptName := "MyScript.ahk"
SetTitleMatchMode, 2
DetectHiddenWindows, on
SendMessage, 0x44, 0x405, 0, , %AHKScriptName% ahk_class AutoHotkey
MsgBox %ErrorLevel% is the process id.

想判断哪些是未编译脚本, 哪些是已编译脚本, 请看下图: 运行的 AutoHotkey 脚本列表 图中显示当前系统运行了两个 AutoHotkey 脚本,我们对比这两个脚本的窗口名称:

  • D:\Software\AutoHotkey\Scripts\test.ahk - AutoHotkey v1.1.15.00
  • D:\Software\AutoHotkey\Scripts\test.exe

可以看出一个是未编译脚本,一个是已编译脚本,因此可以根据窗口名判断(其他信息也有差异)。注:从我使用这种方法开始到现在,窗口名称的规律没有发生变化,不过这里无法保证以后的所有版本都会遵从。

对这些脚本进行控制

在写脚本时,我们都知道可以很方便的对当前脚本进行控制,如:

Pause::Pause
^!s::Suspend
^!r::Reload
^+q::ExitApp
!l::ListLines
!v::ListVars
!k::KeyHistory

提示:我以前写脚本时习惯加上许多这样的热键,包括暂停、重启、列出热键、列出执行行、列出变量、显示按键历史等,调试时很方便。那么,对其他脚本我们可以实现这些功能吗?

AHKScriptName := "MyScript.ahk"
DetectHiddenWindows On  ; 才可以检测到脚本的隐藏主窗口.
SetTitleMatchMode 2  ; 避免为下面的文件指定完整的路径.
WM_COMMAND := 0x111
ID_FILE_PAUSE := 65403
ID_FILE_SUSPEND := 65404
PostMessage, WM_COMMAND, ID_FILE_PAUSE,,, %AHKScriptName% ahk_class AutoHotkey
PostMessage, WM_COMMAND, ID_FILE_SUSPEND,,, %AHKScriptName% ahk_class AutoHotkey
WinClose, %AHKScriptName% ahk_class AutoHotkey ; 关闭脚本,也可以使用消息,不过这里使用窗口命令可能直观一些。
; 下面两个同样是挂起和暂停的功能
PostMessage, 0x111, 65305,,, %AHKScriptName% ahk_class AutoHotkey ; 挂起
PostMessage, 0x111, 65306,,, %AHKScriptName% ahk_class AutoHotkey ; 暂停

注意:当前我不清楚如何判断脚本当前处于哪种状态(当前脚本可使用 A_IsSuspended、A_IsPaused)。

还有哪些可用的消息?我提供几点思路供参考:

  • 执行操作时通过工具截取消息(消息很多,这些属于 WM_COMMAND)
  • 直接到官方论坛询问作者

我比较懒,下面是通过源代码提取的一些消息号(可能不完整,前面部分是消息号分配的说明):

0: unused (possibly special in some contexts)
1: IDOK
2: IDCANCEL
3 to 1002: GUI window control IDs (these IDs must be unique only within their parent, not across all GUI windows)
1003 to 65299: User Defined Menu IDs
65300 to 65399: Standard tray menu items.
65400 to 65534: main menu items

消息号 含义
65300 ID_TRAY_OPEN
65400 ID_FILE_RELOADSCRIPT, ID_TRAY_RELOADSCRIPT
65401 ID_FILE_EDITSCRIPT, ID_TRAY_EDITSCRIPT
65402 ID_FILE_WINDOWSPY, ID_TRAY_WINDOWSPY
65403 ID_FILE_PAUSE, ID_TRAY_PAUSE
65404 ID_FILE_SUSPEND, ID_TRAY_SUSPEND
65405 ID_FILE_EXIT, ID_TRAY_EXIT
65406 ID_VIEW_LINES
65407 ID_VIEW_VARIABLES
65408 ID_VIEW_HOTKEYS
65409 ID_VIEW_KEYHISTORY
65410 ID_VIEW_REFRESH
65411 ID_HELP_USERMANUAL, ID_TRAY_HELP
65412 ID_HELP_WEBSITE

注:一般而言在更新版本时消息号的用途不太可能发生变化,不过为了安全,在使用前最好明确其用途,否则可能发生意外情况。

实现的具体原理

在前面的脚本中我们应该注意到两点:

  1. 开启了对隐藏窗口的检测
  2. 使用 AutoHotkey 类名获取窗口

每个 AutoHotkey 运行时都有类名为 AutoHotkey 的主窗口(这个窗口为什么称为主窗口?请参阅 A_ScriptHwnd),与是否创建自定义 GUI 窗口(自定义窗口的类名为 AutoHotkeyGUI)、是否隐藏托盘图标、是否打开调试窗口(这里的调试窗口即是主窗口,未打开时为隐藏状态)等无关。

说到这里,我想起了一个有趣的事情:为什么 AutoHotkey 被称为模拟多线程呢?从这里可以判断出,每个脚本运行时至少有两个线程:

  • 主线程,运行主窗口,负责接受消息、缓冲热键及伪线程的中断和切换等
    这里的描述可能不太准确和全面,不过大体上可以这么理解,其中的伪线程是指帮助中所说的线程概念(例如 Thread 中所描述的线程,注意帮助中除了 A_ScriptHwnd 外从未涉及到主线程)。
  • 脚本线程,实际执行脚本的线程,这不用多说了,它执行的就是我们写的脚本。

从 Microsoft Spy++ 中可以看到,实际上每个脚本也只有两个线程(你多开几个热键,多用几个计时器并不会出现更多线程)。对于模拟多线程的理解,请参阅什么是 Event Loop?,此文以 JavaScript 为例通俗地说明了这种模型的工作机制,尽管某些术语有所不同,不过不影响理解。

小结

尽管未谈到游戏,不过主要内容前面都说完了。因此,要检测 AutoHotkey 脚本的外挂只需在脚本启动及运行时定期执行:

DetectHiddenWindows, on
While WinExist("ahk_class AutoHotkey")
    WinKill

游戏不大可能用 AutoHotkey 写,这里的演示大家能看懂它的用途吧?不过这种检测方法只是我猜测的,对于如 AutoIt、按键精灵这样的脚本语言应该具有一定的可行性(如果是 C 语言这种估计没那么容易预防了)。正常的脚本如改键工具也无法例外了,有了解游戏开发的朋友能否能爆个真实情况。规避的方法也很简单,自己下载源码修改过类名编译喔。

本文与游戏关系不大,最开始的标题为脚本主窗口的妙用,不过这个标题是否更好呢?


上一篇     下一篇