作为本系列博文的开篇,有必要先做些声明,用于免责、以绝口水:

  1. 博文仅围绕已经弃用的、C/S结构的《上海市个人非营业性客车额度竞拍程序》客户端(NetBidClient)进行介绍,对于正在使用的系统不进行任何讨论。
  2. 作者从未向“代拍黄牛”提供过任何技术支持或外挂软件,也没有依赖相关技术从事任何营利活动。研究此类技术仅是个人兴趣使然。
  3. 请勿使用相关技术从事非法活动,“出来混,迟早要还的”。

言归正传,看完定场诗咱们开始。

`说书唱戏劝人方 三条大路走中央`
`善恶到头终有报 人间正道是沧桑`

"神器"做了些什么?

其实市面上的“神器”一点也不神秘,它所做的事无非就是本来你使用软件竞标时所做的那些事——根据策略掌握时机出价、识别验证码、完成出价。只是计算机在完成这一系列步骤的时候,不会紧张、不会犹豫、不会出错、速度还比我们快许多(只要几百毫秒),大概这就是它们“神”的理由吧。

根据“神器”的上述功能,本系列博文将分为以下几个方面,依次展开讨论:

  1. 如何实现计算机模拟键盘鼠标的操作。
  2. 验证码的识别。
  3. 竞拍程序(NetBidClient)分析。

本讲内容

“天下武功,无坚不摧,唯快不破”,神功第一重,内容如下:

  • 调用SendInput()函数实现键鼠模拟。
  • 为NetBidClient竞拍程序部署一个演示用服务器,用于以后测试。

模拟键盘鼠标输入

先来看看,计算机若要替代人类进行竞拍程序操作,需要完成那些招式:

  1. 首先获取窗口句柄,并激活窗口。
  2. 获取窗口的屏幕位置坐标。
  3. 根据窗口的屏幕坐标计算出控件的屏幕坐标。
  4. 向控件发送鼠标或者键盘的操作指令。

以上这些招,依赖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的更多相关文章

  1. fir.im Weekly - 论个人技术影响力是如何炼成的

    每个圈子都有一群能力强且懂得经营自己的人,技术圈也是如此.本期 fir.im Weekly 一如往期精选了一些实用的 iOS,Android 开发工具和源码分享,还有一些关于程序员的成长 Tips 和 ...

  2. 我的 Github 个人博客是怎样炼成的

    Joey's Blog 长大后才发现政府建造 GFW 真是太 TM 机智了,由于本人自制力较差,且不说 91porn, youporn 等两性知识网站的超强战斗力,单单一个Youtube就可以让我瞬间 ...

  3. 自由是有代价的:聊聊这几年尝试的道路 要想生活好,别看哲学书和思想书。简单看看可以,看多了问题就大了。还是要去研究研究些具体的问题。别jb坐在屋子里,嘴里念着海子的诗,脑袋里想康德想的事情,兜里屁都没有,幻想自己是大国总理,去想影帝是怎么炼成的。

    自由是有代价的:聊聊这几年尝试的道路 现在不愿意写过多的技术文章了,一点是现在做的技术比较偏,写出来看的人也不多,二来是家庭事务比较繁多,没以前那么有时间写了.最近,园子里多了一些写经历的文章,我也将 ...

  4. 2星|《10W+走心文案是怎样炼成的》:标题党。实际是台湾创意总监的一些人生感悟和两三个很一般的创意文案

    10W+走心文案是怎样炼成的 作者是台湾人,曾在台湾奥美担任创意总监,做过一些广告.本书是他的一些经验介绍. 总体来说是标题党,作者的广告基本是电视广告,跟文案也有关系,估计播放量也很容易过10W+, ...

  5. 测度论--长度是怎样炼成的[zz]

    http://www.58pic.com/newpic/27882296.html http://www.58pic.com/newpic/27893137.html http://699pic.co ...

  6. AI算法工程师炼成之路

    AI算法工程师炼成之路 面试题: l  自我介绍/项目介绍 l  类别不均衡如何处理 l  数据标准化有哪些方法/正则化如何实现/onehot原理 l  为什么XGB比GBDT好 l  数据清洗的方法 ...

  7. 老杜告诉你java小白到大神是怎么炼成的(转载)

    老杜告诉你java小白到大神是怎么炼成的 1. 学习前的准备 一个好的学习方法(应该怎么学习更高效): 一个合格的程序员应该具备两个能力 有一个很好的指法速度(敲代码快) 有一个很好的编程思想(编程思 ...

  8. 开会不用把人都轰进一个小黑屋子——《Office妖精是怎样炼成的》续2

    <Office妖精是怎样炼成的>http://blog.sina.com.cn/s/articlelist_1446470001_6_1.html 一本不是技术图书却含有技术内容的图书,一 ...

  9. 学习型的“文山表海无限发展公司”——《Office妖精是怎样炼成的》续1

    本篇无故事情节版:https://www.cnblogs.com/officeplayer/p/14841590.html <Office妖精是怎样炼成的>http://blog.sina ...

  10. 王者荣耀是怎样炼成的(一)《王者荣耀》用什么开发,游戏入门,unity3D介绍

    在国内,如果你没有听说过<王者荣耀>,那你一定是古董级的人物了. <王者荣耀>(以下简称“农药”),专注于移动端(Android.IOS)的MOBA游戏.笔者看到这么火爆,就萌 ...

随机推荐

  1. [Spring+SpringMVC+Mybatis]框架学习笔记(二):Spring-IOC-DI

    上一章:[Spring+SpringMVC+Mybatis]框架学习笔记(一):SpringIOC概述 下一章:[Spring+SpringMVC+Mybatis]框架学习笔记(三):Spring实现 ...

  2. 玩转 PI 系列-如何在 Rockchip Arm 开发板上安装 Docker Tailscale K3s Cilium?

    概述 618 买了几个便宜的 Purple PI OH 开发板 (500 块多一点买了 3 个), 这个开发板类似树莓派,是基于 Rockchip(瑞芯微) 的 rx3566 arm64 芯片.如下: ...

  3. Blazor阻止冒泡传播

    在你的组件的外面套上一个div,并添加@onclick:stopPropagation="true" <div @onclick:stopPropagation=" ...

  4. python: linux使用多版本python

    安装python3.6 $ sudo add-apt-repository ppa:deadsnakes/ppa $ sudo apt update $ sudo apt install python ...

  5. Parallel 与 ConcurrentBag<T> 这对儿黄金搭档(C#)【并发编程系列】

    〇.前言 日常开发中经常会遇到数据统计,特别是关于报表的项目.数据处理的效率和准确度当然是首要关注点. 本文主要介绍,如何通过 Parallel 来并行处理数据,并组合 ConcurrentBag&l ...

  6. 连续下雨天,.net开发者如何预防流感

    最近连续下了3天雨,天气变化大,很容易引发感冒咳嗽等疾病.对于.NET技术开发人员来说,如何保持身体健康,保证工作效率是一个很重要的问题. 首先,我们需要注意保持室内空气流通,避免长时间处于封闭的空间 ...

  7. 使用JDK自带工具调优JVM的常用命令

    前言 对于Java进程常见问题,可以通过JVM监控工具(比如Prometheus).Arthas等,或者使用JDK自带的工具.如果第三方监控工具线上没有的话,对jdk自带的工具就要多熟悉熟悉. 线上J ...

  8. vlak

    2023-7-14 题目 luogu题目传送门 题目描述 Nina 和 Emilija 正在玩一个特殊的游戏.这个游戏是在一张最开始为空白的纸上进行的.在每一个人的行动回合内,这个人会在这张纸上当前的 ...

  9. torch.nn基础学习教程 | PyTorch nn Basic Tutorial

    基于torch.nn搭建神经网络的基础教程大纲: 1. 引言 在我们开始深入探讨torch.nn之前,我们首先需要理解PyTorch及其神经网络库的基础知识.这一部分的内容将帮助你对PyTorch有一 ...

  10. 《SQL与数据库基础》18. MySQL管理

    目录 MySQL管理 系统数据库 常用工具 mysql mysqladmin mysqlbinlog mysqlshow mysqldump mysqlimport source 本文以 MySQL ...