"拍牌神器"是怎样炼成的(一)--- 键鼠模拟之WinAPI
作为本系列博文的开篇,有必要先做些声明,用于免责、以绝口水:
- 博文仅围绕已经弃用的、C/S结构的《上海市个人非营业性客车额度竞拍程序》客户端(NetBidClient)进行介绍,对于正在使用的系统不进行任何讨论。
- 作者从未向“代拍黄牛”提供过任何技术支持或外挂软件,也没有依赖相关技术从事任何营利活动。研究此类技术仅是个人兴趣使然。
- 请勿使用相关技术从事非法活动,“出来混,迟早要还的”。
言归正传,看完定场诗咱们开始。
`说书唱戏劝人方 三条大路走中央`
`善恶到头终有报 人间正道是沧桑`
"神器"做了些什么?
其实市面上的“神器”一点也不神秘,它所做的事无非就是本来你使用软件竞标时所做的那些事——根据策略掌握时机出价、识别验证码、完成出价。只是计算机在完成这一系列步骤的时候,不会紧张、不会犹豫、不会出错、速度还比我们快许多(只要几百毫秒),大概这就是它们“神”的理由吧。
根据“神器”的上述功能,本系列博文将分为以下几个方面,依次展开讨论:
- 如何实现计算机模拟键盘鼠标的操作。
- 验证码的识别。
- 竞拍程序(NetBidClient)分析。
本讲内容
“天下武功,无坚不摧,唯快不破”,神功第一重,内容如下:
- 调用SendInput()函数实现键鼠模拟。
- 为NetBidClient竞拍程序部署一个演示用服务器,用于以后测试。
模拟键盘鼠标输入
先来看看,计算机若要替代人类进行竞拍程序操作,需要完成那些招式:
- 首先获取窗口句柄,并激活窗口。
- 获取窗口的屏幕位置坐标。
- 根据窗口的屏幕坐标计算出控件的屏幕坐标。
- 向控件发送鼠标或者键盘的操作指令。
以上这些招,依赖WinAPI函数就能完成(当然还有其它的方法可选,如果你想了解其它“门派”的武功可以看看这里)。
好,我们来看分解动作:
第1招, 获取窗口句柄,并激活
这招,通过调用FindWindow和SetForegroundWindow两个函数实现,看看函数名就能猜到他们是干什么的,声明如下:
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool SetForegroundWindow(IntPtr hWnd);
FindWindow有两个String类型的传入参数——lpClassName指窗口的类名和lpWindowName指窗口的标题名,我们使用VS的工具Spy++来获得它们,打开Spy++的查找窗口,拖拽“查找程序工具”(那个十字准星)到目标窗口上就行,结果如下图。

FindWindow函数的返回值就是窗口句柄,把获得的窗口句柄作为参数传给SetForegroundWindow函数,就能让窗口激活。
第2招, 获得窗口的屏幕坐标
调用GetWindowRect函数,即可获得以像素为单位的窗口位置与宽高信息。
[DllImport("user32.dll", SetLastError=true)]
static extern bool GetWindowRect(IntPtr hwnd, out RECT lpRect);
第3招, 计算窗口控件的屏幕坐标
屏幕坐标指的是以屏幕左上角为原点的向下坐标系,窗口坐标指以窗口左上角为原点的坐标系。
我们点击一个控件,或者在控件中输入字符时,SendInput函数要求我们提供屏幕坐标。由于窗口在屏幕上的位置不固定,所以控件的屏幕坐标也不是固定的,还好我们可以通过控件的窗口坐标加上窗口的屏幕坐标获得控件的屏幕坐标。
//screenX, screenY 是控件的屏幕坐标(x,y)
//window.RECT是上面GetWindowRect获得的窗口位置信息
// dx,dy 是控件在窗口坐标
screenX = window.RECT.Left + dx
screenY = window.RECT.Top + dy
第4招, 发送鼠标或键盘的操作指令
通过调用API函数SendInput函数来模拟键盘鼠标输入,这个算本讲的大招,需重点说说,先看声明:
[DllImport("user32.dll")]
internal static extern uint SendInput(uint nInputs,
[MarshalAs(UnmanagedType.LPArray), In] INPUT[] pInputs,
int cbSize);
SendInput函数有3个传入参数,先看第二个pInputs,它是INPUT结构的数组,每个INPUT结构中定义了一次键鼠操作,既然pInputs参数是个数组类型,说明调用一次SendInput函数可以完成多个键鼠操作,例如,把鼠标移动到TextBox控件上(MoveTo)、按下鼠标左键(LeftDown,LeftUp)、输入字符(KeyChrDownUp)这一列动作可以一次传给SendInput去执行。
nInputs参数是指pInputs[]中有多少个INPUT,cbSize参数指INPUT结构的尺寸。
再来看看INPUT及部分结构体的定义。
[StructLayout(LayoutKind.Sequential)]
public struct INPUT
{
internal InputType type;
internal InputUnion U;
internal static int Size
{
get { return Marshal.SizeOf(typeof(INPUT)); }
}
}
internal enum InputType : uint
{
MOUSE = 0,
KEYBOARD = 1,
HARDWARE = 2
}
[StructLayout(LayoutKind.Explicit)]
internal struct InputUnion
{
[FieldOffset(0)]
internal MOUSEINPUT mi;
[FieldOffset(0)]
internal KEYBDINPUT ki;
[FieldOffset(0)]
internal HARDWAREINPUT hi;
}
[StructLayout(LayoutKind.Sequential)]
internal struct KEYBDINPUT
{
internal VirtualKeyShort wVk;
internal short wScan;
internal KEYEVENTF dwFlags;
internal int time;
internal UIntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
internal struct MOUSEINPUT
{
internal int dx;
internal int dy;
internal int mouseData;
internal MOUSEEVENTF dwFlags;
internal uint time;
internal IntPtr dwExtraInfo;
}
看了上面的定义,有点明白怎么用了吧!先告诉INPUT.type是MOUSE还是KEYBOARD操作,然后再在INPUT.U中放个MOUSEINPUT或KEYBDINPUT就行了,MOUSEINPUT和KEYBDINPUT结构体分别用于说明你想怎么操作鼠标或键盘。下面我们用个代码片断来看看SendInput函数的调用。
至于完整的声明及定义可在本文例子中找到(在WinAPIHelper.cs里)。
POINT p = new Point();
int perWidth = (0xFFFF / (GetSystemMetrics(SystemMetric.SM_CXSCREEN) - 1));
int perHeight = (0xFFFF / (GetSystemMetrics(SystemMetric.SM_CYSCREEN) - 1));
GetCursorPos(out p);
//把鼠标从当前位置,向右移动200个像素,向下移动300个像素
p.X = p.X + 200;
p.Y = p.Y + 300;
var pInputs = new[]{
new INPUT() //第一个动作
{
type = InputType.MOUSE, //一个鼠标操作
U = new InputUnion()
{
mi = new MOUSEINPUT()
{
dx = p.X * perWidth, //移动鼠标
dy=p.Y * perHeight,
mouseData = 0,
time = GetTickCount(),
dwFlags = MOUSEEVENTF.MOVE| MOUSEEVENTF.ABSOLUTE, //移动鼠标,绝对坐标
dwExtraInfo = GetMessageExtraInfo()
}
}
},
new INPUT()
{
type = InputType.MOUSE, //一个鼠标操作
U = new InputUnion()
{
mi = new MOUSEINPUT()
{
dx = 0,
dy= 0,
mouseData = 0,
time = GetTickCount(),
dwFlags = MOUSEEVENTF.LEFTDOWN, //鼠标左键按下
dwExtraInfo = GetMessageExtraInfo()
}
}
},
new INPUT()
{
type = InputType.MOUSE, //一个鼠标操作
U = new InputUnion()
{
mi = new MOUSEINPUT()
{
dx = 0,
dy= 0,
mouseData = 0,
time = GetTickCount(),
dwFlags = MOUSEEVENTF.LEFTUP, //鼠标左键弹起
dwExtraInfo = GetMessageExtraInfo()
}
}
}
,
new INPUT()
{
type = InputType.KEYBOARD, //一个键盘操作
U = new InputUnion()
{
ki = new KEYBDINPUT()
{
wScan =ScanCodeShort.KEY_1, //按下1键
wVk = VirtualKeyShort.KEY_1,
dwFlags =KEYEVENTF.UNICODE
}
}
}
,
new INPUT()
{
type = InputType.KEYBOARD, //一个键盘操作
U = new InputUnion()
{
ki = new KEYBDINPUT()
{
wScan =ScanCodeShort.KEY_1, //1键弹起
wVk = VirtualKeyShort.KEY_1,
dwFlags =KEYEVENTF.KEYUP | KEYEVENTF.UNICODE
}
}
}
};
SendInput((uint)pInputs.Length, pInputs, INPUT.Size);
在这个例子中, 鼠标从当前位置向右移动200px,再向下移动300px,点击一下鼠标左键,再按一下数字1键,如果在鼠标移到的位置上有个TextBox控件,你会发现TextBox里被输入了一个“1”。
另外,需注意一下,MOUSEINPUT结构中dx,dy的值,并不是以像素为单位的坐标系,它定义屏幕的左上角为原点,右下角的坐标为(0xFFFF,0xFFFF),使用的时候记得把你的像素坐标转化一下下。
秘籍
C#中使用WinAPI函数时,声明函数、定义各种结构类型、枚举类型,实在是个繁琐且容易出错的工作。下面给大家推荐一个Visual Studio的扩展工具,它能让您调用WinAPI函数的工作更容易些:
首先在这里下载,双击下载完成的.vsix文件,就会为VS安装扩展工具。
完成安装后,在VS IDE环境中会增加如图菜单

- 选择"Insert PInvoke Signatures"菜单,即可在光标处插入您想使用的API函数声明或结构定义等。

试一试吧, 是不是So easy? “以后妈妈再也不用担心我调用WinAPI函数了”。
部署竞拍演示服务器
作者并不了解上海国拍行的竞拍服务器采用的是什么技术,下面给出的演示服务程序,仅仅是根据NetBidClient程序的需要,模仿了部分服务器返回值而已,目的是能让NetBidClient成功登录,并能显示验证码。
- 下载附件中DemoSvr.zip文件,展开DemoSvr目录下的内容,目录结构保持不变。
- 在IIS中新建站点(.NetFramework 4),绑定HTTP和HTTPS,内容目录指向DemoSvr。
- 修改本机的hosts文件中,添加如下内容:
`
127.0.0.1 toubiao.alltobid.com
127.0.0.1 toubiao2.alltobid.com
127.0.0.1 tblogin.alltobid.com
127.0.0.1 tblogin2.alltobid.com
127.0.0.1 tbquery.alltobid.com
127.0.0.1 tbquery2.alltobid.com
`
好了,启动你的Web站点,访问一下https://toubiao.alltobid.com/car/gui/login.aspx,如果有返回值就成功了,打开NetBidClient程序登录吧,投标号/密码随便输。
结束语
非常感谢您读到了这里, 希望您能明白我说了此什么,如果我没说清楚,附件里有些例子供您参考。
下一次我们将用更简单的方法来模拟键鼠输入。
附件:
DemoSvr.zip 旧版拍牌程序NetBidClient,演示服务程序和源码
SimuWAPI.zip 本文例子程序
"拍牌神器"是怎样炼成的(一)--- 键鼠模拟之WinAPI的更多相关文章
- fir.im Weekly - 论个人技术影响力是如何炼成的
每个圈子都有一群能力强且懂得经营自己的人,技术圈也是如此.本期 fir.im Weekly 一如往期精选了一些实用的 iOS,Android 开发工具和源码分享,还有一些关于程序员的成长 Tips 和 ...
- 我的 Github 个人博客是怎样炼成的
Joey's Blog 长大后才发现政府建造 GFW 真是太 TM 机智了,由于本人自制力较差,且不说 91porn, youporn 等两性知识网站的超强战斗力,单单一个Youtube就可以让我瞬间 ...
- 自由是有代价的:聊聊这几年尝试的道路 要想生活好,别看哲学书和思想书。简单看看可以,看多了问题就大了。还是要去研究研究些具体的问题。别jb坐在屋子里,嘴里念着海子的诗,脑袋里想康德想的事情,兜里屁都没有,幻想自己是大国总理,去想影帝是怎么炼成的。
自由是有代价的:聊聊这几年尝试的道路 现在不愿意写过多的技术文章了,一点是现在做的技术比较偏,写出来看的人也不多,二来是家庭事务比较繁多,没以前那么有时间写了.最近,园子里多了一些写经历的文章,我也将 ...
- 2星|《10W+走心文案是怎样炼成的》:标题党。实际是台湾创意总监的一些人生感悟和两三个很一般的创意文案
10W+走心文案是怎样炼成的 作者是台湾人,曾在台湾奥美担任创意总监,做过一些广告.本书是他的一些经验介绍. 总体来说是标题党,作者的广告基本是电视广告,跟文案也有关系,估计播放量也很容易过10W+, ...
- 测度论--长度是怎样炼成的[zz]
http://www.58pic.com/newpic/27882296.html http://www.58pic.com/newpic/27893137.html http://699pic.co ...
- AI算法工程师炼成之路
AI算法工程师炼成之路 面试题: l 自我介绍/项目介绍 l 类别不均衡如何处理 l 数据标准化有哪些方法/正则化如何实现/onehot原理 l 为什么XGB比GBDT好 l 数据清洗的方法 ...
- 老杜告诉你java小白到大神是怎么炼成的(转载)
老杜告诉你java小白到大神是怎么炼成的 1. 学习前的准备 一个好的学习方法(应该怎么学习更高效): 一个合格的程序员应该具备两个能力 有一个很好的指法速度(敲代码快) 有一个很好的编程思想(编程思 ...
- 开会不用把人都轰进一个小黑屋子——《Office妖精是怎样炼成的》续2
<Office妖精是怎样炼成的>http://blog.sina.com.cn/s/articlelist_1446470001_6_1.html 一本不是技术图书却含有技术内容的图书,一 ...
- 学习型的“文山表海无限发展公司”——《Office妖精是怎样炼成的》续1
本篇无故事情节版:https://www.cnblogs.com/officeplayer/p/14841590.html <Office妖精是怎样炼成的>http://blog.sina ...
- 王者荣耀是怎样炼成的(一)《王者荣耀》用什么开发,游戏入门,unity3D介绍
在国内,如果你没有听说过<王者荣耀>,那你一定是古董级的人物了. <王者荣耀>(以下简称“农药”),专注于移动端(Android.IOS)的MOBA游戏.笔者看到这么火爆,就萌 ...
随机推荐
- 【go语言】2.1.1 变量,常量和数据类型
Go 语言中,变量是存储数据的基本单位,常量则是固定不变的数据.每个变量和常量都有其对应的数据类型. 变量 在 Go 语言中,你可以使用 var 关键字来声明一个变量: var name string ...
- RAT蓝队自动化测试框架
RAT蓝队自动化测试框架 介绍 RAT 是根据 MITRE ATT&CK 战术矩阵测试蓝队检测能力的脚本框架,由 python2.7 编写,共有 50 多种不同 ATT&CK 技术点和 ...
- eclipse module-info.java文件
module 本模块的名称{ exports 对外暴露的包路径; requires 需要依赖的其他模块名称; } module-info.java不是类,不是接口,是一些模块描述信息.module也不 ...
- axios快速上手(简单使用)
axios对ajax请求进行了封装,并且使用promise的链式调用使得网络请求的代码逻辑更为清晰,同时支持async和await的编写方式使代码看起来像同步,更加方便于理解和阅读.axios这个库的 ...
- 聚焦Web前端安全:最新揭秘漏洞防御方法
在 Web 安全中,服务端一直扮演着十分重要的角色.然而前端的问题也不容小觑,它也会导致信息泄露等诸如此类的问题.在这篇文章中,我们将向读者介绍如何防范Web前端中的各种漏洞.[万字长文,请先收藏再阅 ...
- [刺客伍六七&黑客] 魔刀千刃
魔刀千刃的特写 诞生之日:2023.7.29 此后会在此记录如何自己写一个自己的python库以及魔刀千刃的维护过程. 魔刀千刃(evilblade) **只攻不防,天下无双** 实战 (和堆攻击帖子 ...
- 《深入理解Java虚拟机》读书笔记:Class类文件的结构
Class类文件的结构 Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的的程序存储格式--字节码(ByteCode),从而实现了程序 ...
- Java内存溢出时,还能正常处理请求吗?
当你被问到"当Java程序发生内存溢出时,进程还能正常处理请求吗?"这样的面试题,会不会很懵?这里分享一次网友车辙在当初刚毕业那几年,意义风发,总觉得天下没有自己不会的面试题.然后 ...
- Sealos 国内集群正式上线,可一键运行 LLama2 中文版大模型!
2023 年 7 月 19 日,MetaAI 宣布开源旗下的 LLama2 大模型,Meta 首席科学家.图灵奖得主 Yann LeCun 在推特上表示 Meta 此举可能将改变大模型行业的竞争格局. ...
- 淘宝详情api接口的使用说明
淘宝详情API接口是一种可以用来获取淘宝商品详细信息的服务,包括图片.标题.价格.销量.评论等数据.下面是淘宝详情API接口的使用说明: 1.关于申请API接口权限: 在使用淘宝详情API接口前,需要 ...