"拍牌神器"是怎样炼成的(一)--- 键鼠模拟之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游戏.笔者看到这么火爆,就萌 ...
随机推荐
- MySQL——GROUP BY详解与优化
在 MySQL 中,GROUP BY用于将具有指定列中相同值的行分组在一起.这是在处理大量数据时非常有用的功能,允许对数据进行分类和聚合. 基本使用 语法 以下是GROUP BY子句的基本语法: &q ...
- ASP.NET WebForm中asp:Repeater和UI:Grid数据为空时如何显示表头?
一.asp:Repeater Repeater 控件用于显示被绑定在该控件上的项目的重复列表.Repeater 控件可被绑定到数据库表.XML 文件或者其他项目列表. 1.1-前台页面代码 <a ...
- P1941 [NOIP2014 提高组] 飞扬的小鸟 题解
我们先不管障碍物. 设 \(f[i][j]\) 表示来到点 \((i,j)\) 的最少点击屏幕数. 因为每秒要不上升 \(k\times x[i]\),要么下降 \(y[i]\). 所以有: \[f[ ...
- 使用kafka自带脚本进行压力测试
前言 kafka官方自带压力测试脚本: 消费者压力测试:kafka-consumer-perf-test.sh 生产者压力测试:kafka-producer-perf-test.sh 测试节点: 17 ...
- 调试linux内核(1): 环境准备和原理介绍
开篇 现在流行的开源项目经历了长时间的开发, 积累了大量的代码, 想要一行一行地阅读代码去学习开源项目, 需要的时间成本是巨大的. 所以, 我们也需要用一种高效的方式去"阅读"代码 ...
- Baby_python 反编译
ok,直接pyc直接反编译 逻辑清楚 拿到flag直接搞 结果提交给错了我把前缀改成flag{}这种格式也给错真是摸不着头脑 难道是base64解密错误?? 找了另外一个网站 ???这个是啥?? 与之 ...
- 关于 LLM 和图数据库、知识图谱的那些事
本文整理自 NebulaGraph 布道师 wey 在「夜谈 LLM」主题分享上的演讲,主要包括以下内容: 背景 LLM RAG Graph 知识抽取 Text2Cypher Graph RAG 未来 ...
- Unity UGUI的Toggle(复选框)组件的介绍及使用
Unity UGUI的Toggle(复选框)组件的介绍及使用 1. 什么是Toggle组件? Toggle(复选框)是Unity UGUI中的一个常用组件,用于实现复选框的功能.它可以被选中或取消选中 ...
- 解放双手!ChatGPT助力编写JAVA框架
亲爱的Javaer们,在平时编码的过程中,你是否曾想过编写一个Java框架去为开发提效?但是要么编写框架时感觉无从下手,不知道从哪开始.要么有思路了后对某个功能实现的技术细节不了解,空有想法而无法实现 ...
- 运行解压版tomcat中的startup.bat一闪而退的解决办法
Tomcat的startup.bat,它调用了catalina.bat,而catalina.bat则调用了setclasspath.bat,只要在setclasspath.bat的开头声明环境变量(红 ...