本文旨在自己动手实现一个类似于“按键精灵”的桌面软件。第一部分介绍了简单的模拟方式,但是有些软件能够屏蔽掉这种简单模拟带来的效果,因此第二部分将介绍如何从驱动级层面进行模拟。
“游戏外挂一般分为三个级别:初级是鼠标、键盘模拟,中级是Call游戏内部函数,读写内存,高级是抓包,封包的“脱机挂”(完全模拟客户端网络数据,不用运行游戏)。用C#写外挂的不是很多,大部分是C++,主要原因是MS的C#目前不支持内联汇编功能。因此用C++写底层库,然后用C#调用成为DONET爱好者开发外挂的首选。”——某开发者言
.NET没有提供改变鼠标指针位置、模拟点击操作的函数,但是可以通过调用Windows API函数实现。
[DllImport("user32.dll")]
static extern bool SetCursorPos(int X,int Y);
该函数用于设置鼠标的位置,其中X和Y是相对于屏幕左上角的绝对位置.
[DllImport("user32.dll")]
static extern void mouse_event(MouseEventFlag flags,int dx,int dy,uint data,UIntPtr extraInfo);
该函数不仅可以设置鼠标指针绝对位置,而且可以以相对坐标来设置位置.
其中flags标志位集,指定点击按钮和鼠标动作的多种情况.dx指鼠标沿x轴绝对位置或上次鼠标事件位置产生以来移动的数量.dy指沿y轴的绝对位置或从上次鼠标事件以来移动的数量.data如果flags为MOUSE_WHEEL则该值指鼠标轮移动的数量(否则为0),正值向前转动.extraInfo指定与鼠标事件相关的附加32位值.
[DllImport("user32.dll")]
static extern IntPtr FindWindow(string strClass, string strWindow);
该函数根据类名和窗口名来得到窗口句柄,但是这个函数不能查找子窗口,也不区分大小写.如果要从一个窗口的子窗口查找需要使用FIndWindowEX函数.
[DllImport("user32.dll")]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string strClass, string strWindow);
该函数获取一个窗口的句柄,该窗口的类名和窗口名与给定的字符串相匹配,该函数查找子窗口时从排在给定的子窗口后面的下一个子窗口开始。其中参数hwnParent为要查找子窗口的父窗口句柄,若该值为NULL则函数以桌面窗口为父窗口,查找桌面窗口的所有子窗口。 hwndChildAfter子窗口句柄,查找从在Z序中的下一个子窗口开始,子窗口必须为hwnParent直接子窗口而非后代窗口,若hwnChildAfter为NULL,查找从父窗口的第一个子窗口开始。 strClass指向一个指定类名的空结束字符串或一个标识类名字符串的成员的指针。 strWindow指向一个指定窗口名(窗口标题)的空结束字符串.若为NULL则所有窗体全匹配。返回值:如果函数成功,返回值为具有指定类名和窗口名的窗口句柄,如果函数失败,返回值为NULL。
1-引入命名空间using System.Runtime.InteropServices; 因为要使用user32.dll中的接口。
2-创建一个新类MouseFlag
3-声明函数
- 此处的位置都是屏幕绝对位置
- 设置鼠标位置:public static extern int SetCursorPos(int x, int y);
- 鼠标事件:static extern void mouse_event(MouseEventFlag flags, int dx, int dy, uint data, UIntPtr extraInfo);虽然这个已经弃用了,但是上手简单。这里需要一个类型:MouseEventFlag
4-创建MouseEventFlag :enum MouseEventFlag : uint
5-用类包起来
(一个控制台程序)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace Mouse
{
class Program
{
public class MouseFlag
{
enum MouseEventFlag : uint
{
Move = 0x0001,
LeftDown = 0x0002,
LeftUp = 0x0004,
RightDown = 0x0008,
RightUp = 0x0010,
MiddleDown = 0x0020,
MiddleUp = 0x0040,
XDown = 0x0080,
XUp = 0x0100,
Wheel = 0x0800,
VirtualDesk = 0x4000,
Absolute = 0x8000
}
[DllImport("user32.dll")]
static extern void mouse_event(MouseEventFlag flags, int dx, int dy, uint data, UIntPtr extraInfo);
[DllImport("user32.dll")]
public static extern int SetCursorPos(int x, int y);
public static void MouseLeftClickEvent(int dx, int dy, uint data)
{
SetCursorPos(dx, dy);
System.Threading.Thread.Sleep(2 * 1000);
mouse_event(MouseEventFlag.LeftDown, dx, dy, data, UIntPtr.Zero);
mouse_event(MouseEventFlag.LeftUp, dx, dy, data, UIntPtr.Zero);
}
public static void MouseRightClickEvent(int dx, int dy, uint data)
{
SetCursorPos(dx, dy);
System.Threading.Thread.Sleep(2 * 1000);
mouse_event(MouseEventFlag.RightDown, dx, dy, data, UIntPtr.Zero);
mouse_event(MouseEventFlag.RightUp, dx, dy, data, UIntPtr.Zero);
}
}
static void Main(string[] args)
{
System.Threading.Thread.Sleep(6 * 1000);
MouseFlag.MouseLeftClickEvent(10, 1000, 0);
}
}
}
/// 按键的虚拟键值
/// 扫描码,一般不用设置,用0代替就行
/// 选项标志:0:表示按下,2:表示松开
/// 一般设置为0
[DllImport("user32.dll")]
public static extern void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);
可类比“模拟鼠标操作”的实现步骤,直接学习以下实例吧。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
namespace VKB
{
class Program
{
public class KeyBoard
{
public const byte vKeyLButton = 0x1; // 鼠标左键
public const byte vKeyRButton = 0x2; // 鼠标右键
public const byte vKeyCancel = 0x3; // CANCEL 键
public const byte vKeyMButton = 0x4; // 鼠标中键
public const byte vKeyBack = 0x8; // BACKSPACE 键
public const byte vKeyTab = 0x9; // TAB 键
public const byte vKeyClear = 0xC; // CLEAR 键
public const byte vKeyReturn = 0xD; // ENTER 键
public const byte vKeyShift = 0x10; // SHIFT 键
public const byte vKeyControl = 0x11; // CTRL 键
public const byte vKeyAlt = 18; // Alt 键 (键码18)
public const byte vKeyMenu = 0x12; // MENU 键
public const byte vKeyPause = 0x13; // PAUSE 键
public const byte vKeyCapital = 0x14; // CAPS LOCK 键
public const byte vKeyEscape = 0x1B; // ESC 键
public const byte vKeySpace = 0x20; // SPACEBAR 键
public const byte vKeyPageUp = 0x21; // PAGE UP 键
public const byte vKeyEnd = 0x23; // End 键
public const byte vKeyHome = 0x24; // HOME 键
public const byte vKeyLeft = 0x25; // LEFT ARROW 键
public const byte vKeyUp = 0x26; // UP ARROW 键
public const byte vKeyRight = 0x27; // RIGHT ARROW 键
public const byte vKeyDown = 0x28; // DOWN ARROW 键
public const byte vKeySelect = 0x29; // Select 键
public const byte vKeyPrint = 0x2A; // PRINT SCREEN 键
public const byte vKeyExecute = 0x2B; // EXECUTE 键
public const byte vKeySnapshot = 0x2C; // SNAPSHOT 键
public const byte vKeyDelete = 0x2E; // Delete 键
public const byte vKeyHelp = 0x2F; // HELP 键
public const byte vKeyNumlock = 0x90; // NUM LOCK 键
//常用键 字母键A到Z
public const byte vKeyA = 65;
public const byte vKeyB = 66;
public const byte vKeyC = 67;
public const byte vKeyD = 68;
public const byte vKeyE = 69;
public const byte vKeyF = 70;
public const byte vKeyG = 71;
public const byte vKeyH = 72;
public const byte vKeyI = 73;
public const byte vKeyJ = 74;
public const byte vKeyK = 75;
public const byte vKeyL = 76;
public const byte vKeyM = 77;
public const byte vKeyN = 78;
public const byte vKeyO = 79;
public const byte vKeyP = 80;
public const byte vKeyQ = 81;
public const byte vKeyR = 82;
public const byte vKeyS = 83;
public const byte vKeyT = 84;
public const byte vKeyU = 85;
public const byte vKeyV = 86;
public const byte vKeyW = 87;
public const byte vKeyX = 88;
public const byte vKeyY = 89;
public const byte vKeyZ = 90;
//数字键盘0到9
public const byte vKey0 = 48; // 0 键
public const byte vKey1 = 49; // 1 键
public const byte vKey2 = 50; // 2 键
public const byte vKey3 = 51; // 3 键
public const byte vKey4 = 52; // 4 键
public const byte vKey5 = 53; // 5 键
public const byte vKey6 = 54; // 6 键
public const byte vKey7 = 55; // 7 键
public const byte vKey8 = 56; // 8 键
public const byte vKey9 = 57; // 9 键
public const byte vKeyNumpad0 = 0x60; //0 键
public const byte vKeyNumpad1 = 0x61; //1 键
public const byte vKeyNumpad2 = 0x62; //2 键
public const byte vKeyNumpad3 = 0x63; //3 键
public const byte vKeyNumpad4 = 0x64; //4 键
public const byte vKeyNumpad5 = 0x65; //5 键
public const byte vKeyNumpad6 = 0x66; //6 键
public const byte vKeyNumpad7 = 0x67; //7 键
public const byte vKeyNumpad8 = 0x68; //8 键
public const byte vKeyNumpad9 = 0x69; //9 键
public const byte vKeyMultiply = 0x6A; // MULTIPLICATIONSIGN(*)键
public const byte vKeyAdd = 0x6B; // PLUS SIGN(+) 键
public const byte vKeySeparator = 0x6C; // ENTER 键
public const byte vKeySubtract = 0x6D; // MINUS SIGN(-) 键
public const byte vKeyDecimal = 0x6E; // DECIMAL POINT(.) 键
public const byte vKeyDivide = 0x6F; // DIVISION SIGN(/) 键
//F1到F12按键
public const byte vKeyF1 = 0x70; //F1 键
public const byte vKeyF2 = 0x71; //F2 键
public const byte vKeyF3 = 0x72; //F3 键
public const byte vKeyF4 = 0x73; //F4 键
public const byte vKeyF5 = 0x74; //F5 键
public const byte vKeyF6 = 0x75; //F6 键
public const byte vKeyF7 = 0x76; //F7 键
public const byte vKeyF8 = 0x77; //F8 键
public const byte vKeyF9 = 0x78; //F9 键
public const byte vKeyF10 = 0x79; //F10 键
public const byte vKeyF11 = 0x7A; //F11 键
public const byte vKeyF12 = 0x7B; //F12 键
[DllImport("user32.dll")]
public static extern void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo);
public static void keyPress(byte keyName)
{
KeyBoard.keybd_event(keyName, 0, 0, 0);
KeyBoard.keybd_event(keyName, 0, 2, 0);
}
}
static void Main(string[] args)
{
System.Threading.Thread.Sleep(6 * 1000);
///MouseFlag.MouseLeftClickEvent(350, 70, 0);
///System.Threading.Thread.Sleep(5*1000);
///MouseFlag.MouseLeftClickEvent(20, 30, 0);
///System.Threading.Thread.Sleep(5 * 1000);
KeyBoard.keyPress(KeyBoard.vKeyBack);
System.Threading.Thread.Sleep(5 * 1000);
}
}
}
有时候,按照上面说的方式进行模拟会失败,桌面软件尤其是一些游戏,对于消息命令一点也不“服从”,我们有这样的疑问:它是如何识别真实键盘的按键的呢?难道是程序中有判断吗?在DirectX编程中有个叫DirectInput的API,就是它绕过了Windows的消息机制,它的目的是为了让软件(游戏)的实时性控制更好、更快。Windows消息是队列形式的,在传递过程中会有延时,比如即时对战类游戏对实时性控制要求是非常高的,Window消息机制不能满足这个需求。而DirectInput直接和键盘驱动程序打交道,效率当然要高出一大截。
那么怎么解决这个问题呢,有人发现“按键精灵”使用了WINIO驱动的原理。于是我们也可以通过WINIO实现这样的模拟。
注意要使用winio.dll这个动态库,是需要一些条件的:
1- WinIO函数库只允许被具有管理者权限的应用程序调用。如果使用者不是以管理者的身份进入的,则WinIO.DLL不能够被安装,也不能激活WinIO驱动器。通过在管理者权限下安装驱动器软件就可以克服这种限制。然而,在这种情况下,ShutdownWinIo函数不能在应用程序结束之前被调用,因为该函数将WinIO驱动程序从系统注册表中删除。
2- 64位操作系统下需要将WinIo.sys驱动文件添加数字签名。
3- 注意WinIo.sys和WinIo.dll这两个文件的存放路径,如果路径不对,会导致初始化失败。
可能遇到各种麻烦,请耐心解决。
(本实例摘自:点击打开链接)
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
namespace lizheAionWG
{
public class WinIo
{
public const int KBC_KEY_CMD = 0x64;
public const int KBC_KEY_DATA = 0x60;
[DllImport("winio.dll")]
public static extern bool InitializeWinIo();
[DllImport("winio.dll")]
public static extern bool GetPortVal(IntPtr wPortAddr, out int pdwPortVal, byte bSize);
[DllImport("winio.dll")]
public static extern bool SetPortVal(uint wPortAddr, IntPtr dwPortVal, byte bSize);
[DllImport("winio.dll")]
public static extern byte MapPhysToLin(byte pbPhysAddr, uint dwPhysSize, IntPtr PhysicalMemoryHandle);
[DllImport("winio.dll")]
public static extern bool UnmapPhysicalMemory(IntPtr PhysicalMemoryHandle, byte pbLinAddr);
[DllImport("winio.dll")]
public static extern bool GetPhysLong(IntPtr pbPhysAddr, byte pdwPhysVal);
[DllImport("winio.dll")]
public static extern bool SetPhysLong(IntPtr pbPhysAddr, byte dwPhysVal);
[DllImport("winio.dll")]
public static extern void ShutdownWinIo();
[DllImport("user32.dll")]
public static extern int MapVirtualKey(uint Ucode, uint uMapType);
public void sendwinio()
{
if (InitializeWinIo())
{
KBCWait4IBE();
}
}
///Wait for Buffer gets empty
private void KBCWait4IBE()
{
int dwVal = 0;
do
{
bool flag = GetPortVal((IntPtr)0x64, out dwVal, 1);
}
while ((dwVal & 0x2) > 0);
}
/// key down
public void MykeyDown(int vKeyCoad)
{
int btScancode = 0;
btScancode = MapVirtualKey((byte)vKeyCoad, 0);
KBCWait4IBE();
SetPortVal(KBC_KEY_CMD, (IntPtr)0xD2, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_DATA, (IntPtr)0xe2, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_CMD, (IntPtr)0xD2, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_DATA, (IntPtr)btScancode, 1);
}
/// Key up
public void MykeyUp(int vKeyCoad)
{
int btScancode = 0;
btScancode = MapVirtualKey((byte)vKeyCoad, 0);
KBCWait4IBE();
SetPortVal(KBC_KEY_CMD, (IntPtr)0xD2, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_DATA, (IntPtr)0xe0, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_CMD, (IntPtr)0xD2, 1);
KBCWait4IBE();
SetPortVal(KBC_KEY_DATA, (IntPtr)btScancode, 1);
}
/// Simulate mouse down
public void MyMouseDown(int vKeyCoad)
{
int btScancode = 0;
btScancode = MapVirtualKey((byte)vKeyCoad, 0);
KBCWait4IBE(); // 'wait for buffer gets empty
SetPortVal(KBC_KEY_CMD, (IntPtr)0xD3, 1);// 'send write command
KBCWait4IBE();
SetPortVal(KBC_KEY_DATA, (IntPtr)(btScancode | 0x80), 1);// 'write in io
}
/// Simulate mouse up
public void MyMouseUp(int vKeyCoad)
{
int btScancode = 0;
btScancode = MapVirtualKey((byte)vKeyCoad, 0);
KBCWait4IBE(); // 'wait for buffer gets empty
SetPortVal(KBC_KEY_CMD, (IntPtr)0xD3, 1); //'send write command
KBCWait4IBE();
SetPortVal(KBC_KEY_DATA, (IntPtr)(btScancode | 0x80), 1);// 'write in io
}
//----------------------------------------------------------------------------------
//VK codes
//----------------------------------------------------------------------------------
public enum Key
{
// mouse movements
move = 0x0001,
leftdown = 0x0002,
leftup = 0x0004,
rightdown = 0x0008,
rightup = 0x0010,
middledown = 0x0020,
//keyboard stuff
VK_LBUTTON = 1,
VK_RBUTTON = 2,
VK_CANCEL = 3,
VK_MBUTTON = 4,
VK_BACK = 8,
VK_TAB = 9,
VK_CLEAR = 12,
VK_RETURN = 13,
VK_SHIFT = 16,
VK_CONTROL = 17,
VK_MENU = 18,
VK_PAUSE = 19,
VK_CAPITAL = 20,
VK_ESCAPE = 27,
VK_SPACE = 32,
VK_PRIOR = 33,
VK_NEXT = 34,
VK_END = 35,
VK_HOME = 36,
VK_LEFT = 37,
VK_UP = 38,
VK_RIGHT = 39,
VK_DOWN = 40,
VK_SELECT = 41,
VK_PRINT = 42,
VK_EXECUTE = 43,
VK_SNAPSHOT = 44,
VK_INSERT = 45,
VK_DELETE = 46,
VK_HELP = 47,
VK_NUM0 = 48, //0
VK_NUM1 = 49, //1
VK_NUM2 = 50, //2
VK_NUM3 = 51, //3
VK_NUM4 = 52, //4
VK_NUM5 = 53, //5
VK_NUM6 = 54, //6
VK_NUM7 = 55, //7
VK_NUM8 = 56, //8
VK_NUM9 = 57, //9
VK_A = 65, //A
VK_B = 66, //B
VK_C = 67, //C
VK_D = 68, //D
VK_E = 69, //E
VK_F = 70, //F
VK_G = 71, //G
VK_H = 72, //H
VK_I = 73, //I
VK_J = 74, //J
VK_K = 75, //K
VK_L = 76, //L
VK_M = 77, //M
VK_N = 78, //N
VK_O = 79, //O
VK_P = 80, //P
VK_Q = 81, //Q
VK_R = 82, //R
VK_S = 83, //S
VK_T = 84, //T
VK_U = 85, //U
VK_V = 86, //V
VK_W = 87, //W
VK_X = 88, //X
VK_Y = 89, //Y
VK_Z = 90, //Z
VK_NUMPAD0 = 96, //0
VK_NUMPAD1 = 97, //1
VK_NUMPAD2 = 98, //2
VK_NUMPAD3 = 99, //3
VK_NUMPAD4 = 100, //4
VK_NUMPAD5 = 101, //5
VK_NUMPAD6 = 102, //6
VK_NUMPAD7 = 103, //7
VK_NUMPAD8 = 104, //8
VK_NUMPAD9 = 105, //9
VK_NULTIPLY = 106,
VK_ADD = 107,
VK_SEPARATOR = 108,
VK_SUBTRACT = 109,
VK_DECIMAL = 110,
VK_DIVIDE = 111,
VK_F1 = 112,
VK_F2 = 113,
VK_F3 = 114,
VK_F4 = 115,
VK_F5 = 116,
VK_F6 = 117,
VK_F7 = 118,
VK_F8 = 119,
VK_F9 = 120,
VK_F10 = 121,
VK_F11 = 122,
VK_F12 = 123,
VK_NUMLOCK = 144,
VK_SCROLL = 145,
middleup = 0x0040,
xdown = 0x0080,
xup = 0x0100,
wheel = 0x0800,
virtualdesk = 0x4000,
absolute = 0x8000
}
}
}
某开发者自己封装了一个dll动态库,可参考这篇博文: 点击打开链接。
实际上,github上有许多类似的东西,可以找找合适的代码。
C# 系统应用之鼠标模拟技术及自动操作鼠标
C#实现让鼠标点击任意绝对位置
C#窗体如何通过keybd_event()函数模拟键盘按键(组合键)产生事件
驱动级键盘模拟(C#)
Windows下对硬件端口的操作---WinIo库的使用
WinIo使用笔记