"拍牌神器"是怎樣煉成的(一)--- 鍵鼠模擬之WinAPI


作為本系列博文的開篇,有必要先做些聲明,用於免責、以絕口水:

  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++的查找窗口,拖拽“查找程序工具”(那個十字准星)到目標窗口上就行,結果如下圖。

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 本文例子程序


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM