用Go語言寫一個Windows的外掛(上)
本人在一家網際網路金融公司上班,對於一家網際網路金融公司,最基本的功能就是客戶入金和出金,而出金的穩定性是很重要的,出金不暢容易導致投資人恐慌,本文講的是出金,出金介面我們對接的是招商銀行的銀企直聯絡統,那麼銀企直連繫統是一個什麼樣的程式呢?
沒錯,這個程式是執行在Windows上的,並且需要插入USBKey才能正常工作,這就意味著,不能簡單的使用命令列進行運維管理。
看到這裡,做運維的同學的內心應該和我一樣是崩潰的。。
跟大家解釋一下,這個服務是做什麼的,大家可以把這個程式當成是我們的業務系統和招商銀行溝通的信使,所有出金操作、查詢操作都是通過這個信使來完成。
由於各種未知的原因,比如網路不穩定,或者USBKey插入時間過長產生了一些莫名其妙的錯誤,那麼就需要人工去重啟一下服務或重新登入一下賬號,而且,這個工作有時候是在夜間操作的,這相當於要24小時待命啊,雖然故障頻率不高,但這根弦始終是崩著的,這簡直就是在破壞我的幸福美好生活啊。
這種體力活的事情,我堅決不能幹,所以一定要交給別人幹。
別想多了,【別人】也只能是個外掛而已,誰都不喜歡幹這種人肉體力活。
所以憑藉著我18歲那年的開發經驗,腦子裡想到了 Windows 的訊息模型,使用 SendMessage
給對應的窗體控制元件控制程式碼傳送特定的事件不就搞定了麼,異常自動重啟使用 CreateProcess
不就行了嗎?
天真的我腦子裡已經充滿了 SendMessage
的語句
LRESULT WINAPI SendMessage(
_In_ HWND hWnd,
_In_ UINT Msg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
有木有很熟悉的樣子,驚不驚喜,開不開心?是不是感覺傳送鍵盤點選事件、滑鼠點選事件就OK了?
後面會講到,其實還需要很多工作才能完成一個比較完善可用的外掛軟體,
SendMessage
基本上只能解決一部分問題
然而當我想完這些程式碼後,感覺還是太麻煩,因為按鍵精靈這類軟體就能解決,為什麼還要自己親自操刀?不過最終放棄了這種念頭,因為這是一個很重要的服務,說不定在未來會掌握好 幾千個億
的資金命運,如果安裝了不明軟體,資金安全如何得以保障???絕對不能這麼草草的做這種決定,所以還是決定老老實實的擼程式碼了。。。
用什麼語言是個問題,在Windows上可以使用 C++
, C#
系列,而且C#
我記得有一個automation框架可以完成類似的操作,不過本人最近這3年一直在使用 golang
,前兩種語言目前也只是偶爾用用的節奏,所以基本處於手生的狀態,而 golang
本身也支援使用 syscall
來呼叫 windows
的 DLL
(動態連結庫),所以果斷使用 golang
, 因為這個外掛大部分的WinAPI都在 user32.dll
和 kernel32.dll
裡,我們只需要能載入這幾個DLL
就可以呼叫強大的 WinAPI
了
大家可以使用 PE Explorer 檢視一個DLL有哪些輸出函式
var (
moduser32 = syscall.NewLazyDLL("user32.dll")
procSendMessage = moduser32.NewProc("SendMessageW")
procPostMessage = moduser32.NewProc("PostMessageW")
)
func SendMessage(hwnd HWND, msg uint32, wParam, lParam uintptr) uintptr {
ret, _, _ := procSendMessage.Call(
uintptr(hwnd),
uintptr(msg),
wParam,
lParam)
return ret
}
大家可以看到,在這裡我們使用的是SendMessageW
,而不是SendMessageA
,因為go
語言底層呼叫DLL
介面時,傳入的是utf16
,看看下面的程式碼就明白了
func SetWindowText(hwnd HWND, text string) {
procSetWindowText.Call(
uintptr(hwnd),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(text))))
}
這是一個設定窗體標題的API,第一個引數是窗體控制程式碼,第二個引數大家可以看到,是將go語言的字串轉換成UTF16格式,並獲取其指標。
另外值得注意的是,如果我們編譯出來的程式是32位
的,那麼儘量不要用來作為64位
程式的外掛,因為有很多複雜一點的功能無法實現,後續會提到這個部分,銀企直連
這個服務是32位的,因此我們的go語言也是安裝的32位的,同時為了更好的編譯測試,我的虛擬機器裝的是 Win2008 R2 32位
作業系統
那麼我們應該如何向一個窗體傳送訊息呢?能不能先做實驗,不寫程式碼呢?答案是肯定的,我們先請出我們的神器,Spy++
將瞄準器拖拽到具體的視窗上,就會得到視窗的控制程式碼,我們可以通過 FindWindowW 或 EnumChildWindows 來實現相同的功能
銀企直連正常工作需要兩個步驟
- 啟動HTTP服務監聽
- 登入
我們先看看啟動HTTP監聽按鈕
我們使用spy++
抓到了這個ToolBar的控制程式碼
然後用 spy++
向第一個按鈕傳送滑鼠點選事件,那麼就可以開啟監聽了
點選動作在Windows訊息來看,是分為兩個動作,一個是 WM_LBUTTONDOWN
而另一個是 WM_LBUTTONUP
,所以我們需要傳送兩次事件,當完成這兩次傳送後,我們可以看到下面的介面
沒錯,其實這裡是一個坑,啟動監聽還不好好啟動,非得彈出一個訊息框,同時伴隨著的是spy++
卡死了,為什麼呢? 因為我們使用的是SendMessage
,這是一個同步的過程,因為出現了訊息框,所以spy++
還未收到返回訊息,所以就卡死了。當我們點選完 確認 按鈕後就可以恢復了,當然我們也可以使用 PostMessage
,不過這個介面只適合不在乎執行結果的情況下執行。
好了,這裡我們出現了第一個坑:有彈窗,我們的外掛需要自動識別,並且能夠自動關閉彈窗。
OK, 我們繼續,我們該開始登陸了
剛才我們 SendMessage
裡的WPARAM是1,那麼,這個按鈕是4
繼續使用 spy++
傳送訊息
模擬完傳送,整個人一下子就不好了,因為這個按鈕根本就沒有反應,後面的兩個引數你也不知道到底傳什麼好,就在陷入了整個困局的時候,發現我們其實可以通過快捷鍵 ctrl+b
完成監聽, ctrl+i
進入登入介面
此時未插入USBKey
所以,我們需要使用另外一個API: SendInput
, 包括後面的密碼輸入,也一樣要使用這個API
我們看一下這個API的定義
UINT WINAPI SendInput(
_In_ UINT nInputs, // 按鍵數量
_In_ LPINPUT pInputs, // 按鍵內容陣列
_In_ int cbSize // 陣列內容結構體的尺寸
);
看上去很心塞,一堆引數。
由於本文講解的是調研篇,我們此處假設SendInput
可以完成快捷鍵的按鍵模擬,密碼輸入的按鍵模擬,實際上這個API確實是可以工作的,因為這個介面是真實的模擬鍵盤輸入,不針對某個視窗控制程式碼。
接下來我們會迎來第二個坑,如果USBKey正常工作,那麼使用者名稱裡的的內容是自動填寫好的,如圖:
這個使用者名稱是從USBKey裡讀出來的,讀取是需要時間的,因此我們可以在這裡不停的向這個文字框傳送WM_GETTEXT
訊息,拿到使用者名稱,如果使用者名稱是預期的資料,我們就認為此時USBKey是正常工作的,否則如果長時間使用者名稱未成功載入,則說明USBKey工作異常,應該傳送報警資訊。
我們大概會得到如下幾類錯誤
- 密碼錯誤
- 通訊故障
- USBKey有問題
對於密碼錯誤這個問題,我們的外掛應該立即停止工作,因為密碼輸入次數超過限制,USBKey將會鎖定,公司出金服務就掛了。。。。
為什麼會密碼輸入錯誤呢?因為很有可能在自動輸入時,被其他程式干擾了一下 我們在程式碼中會盡量用
SetForegroundWindow
讓視窗保持在最前面,成為啟用狀態
那麼對於通訊故障,解決的辦法就只能是重新嘗試了
剩下的問題,我個人認為發出報警,人工處理一下會比較合適。
此時迎來兩個新問題, 1. 我們如何知道訊息框裡的內容是什麼 2. 我們如何知道外掛登入成功了呢?
對於第一個問題,我們可以通過 EnumChildWindows
來遍歷這個訊息框的孩子控制程式碼,然後通過 GetWindowText
就可以知道是什麼內容了。
我們重點來討論第二個問題
此處有兩種解法:
- 向招行發起查詢請求,如果能查詢到資料,說明登入成功
- 檢查登陸資訊裡的內容
登陸資訊列表
為了提升難度,我們選擇方案2
這種方法是比較困難的,有困難,我們要解決,沒有困難我們也要創造困難來解決。。。。
為什麼難呢?
因為我們沒辦法通過SendMessage
傳送 WM_GETTEXT
事件獲取內容,但是我們可以通過 LVM_GETITEMTEXT
來獲取 listview 的列表內容
BUT..... 跨程式這麼拿是拿不到的,同時,不同位數的程式,也是拿不到資料的。
如何解決?
我們需要使用API VirtualAllocEx
向銀企直聯程式申請一塊記憶體空間,用於我們的外掛程式和銀企直聯進行資料溝通,當我們傳送 LVM_GETITEMTEXT
訊息之前,我們需要把引數資訊寫到這個記憶體塊裡,然後再使用SendMessage
,ListView的資料會寫到這個記憶體塊,最後我們通過 ReadProcessMemory
來讀取獲取到列表的資料
這裡就是為什麼32位
不能讀64位
程式的內容的原因了,雖然我們可以使用WriteProcessMemory
和 ReadProcessMemory
來寫入和讀取程式記憶體裡的資料,但是由於通過這種機制進行互動,指標大小是不同的,通過SendMessage
指令雖然能執行成功,但是回寫的資料內容會跑飛。
箭頭代表資料流向,所有的API呼叫都是在外掛這邊完成的
整個流程大概就是這樣的,我們需要藉助遠端程式的記憶體塊來做資料互動,但最後切記一定要使用VirtualFreeEx
釋放掉不用的記憶體塊。
此處應該有總結:
- 使用模擬鍵盤的方法開啟監聽和進入到登入介面而非
SendMessage
- 通過遠端申請記憶體塊的方式獲取登入結果內容
- 需要判斷彈出訊息框的內容,用以判斷是否有異常,同時需要關閉這些訊息視窗
到此為止,關鍵的技術內容我們已經調研完了,下一篇內容我們會講如何使用go語言實現一個真正可用的外掛。 我們先來預覽幾個外掛的截圖吧:
外掛工作中.....
當發生穩定性異常時,會通過bearychat的Incoming服務傳送報警
相關文章
- [Go語言寫介面]一、使用xcgui完成go語言第一個軟體介面GoGUI
- Go - 如何編寫 ProtoBuf 外掛 (一) ?Go
- Go 語言中的外掛Go
- 用 PHP 寫一個"程式語言"PHP
- 如何寫一個Vue的外掛Vue
- 自己寫一個Babel外掛Babel
- 用Go語言建立Windows視窗程式GoWindows
- 用Go語言寫HTTP中介軟體GoHTTP
- Go 語言的 10 個實用技巧Go
- Goland環境配置——Goland上的第一個Go語言程式GoLand
- 基礎入門: 編寫第一個 Go 語言程式Go
- 如何編寫一個Jquery外掛jQuery
- 在 AWS Lambda 上寫 Go 語言搭配 API GatewayGoAPIGateway
- GO語言一個簡單的工程Go
- 番外2: go語言寫的簡要資料同步工具Go
- 編寫一個簡單的babel外掛Babel
- Go語言的 10 個實用技術Go
- Go - 如何編寫 ProtoBuf 外掛 (三) ?Go
- Go - 如何編寫 ProtoBuf 外掛(二)?Go
- 用 Go 語言造了一個全新的 kv 儲存引擎Go儲存引擎
- 用 C 語言寫一個簡單的 Unix Shell(1)
- 用 C 語言寫一個簡單的 Unix Shell(2)
- 最近寫了一個demo,想看看java和go語言是怎麼寫的JavaGo
- 用C語言編寫windows服務程式C語言Windows
- 自己寫一個vue的loading外掛Vue
- 如何用原生js來寫一個swiper滑塊外掛(上)原理JS
- 使用Go語言構建一個解釋型語言Go
- 今天在github上發現一個go語言初學的文件GithubGo
- 自己動手編寫一個Mybatis外掛:Mybatis脫敏外掛MyBatis
- Go 語言簡介(上)— 語法Go
- windows下安裝go語言WindowsGo
- 用 C 語言編寫一個簡單的垃圾回收器
- Go 語言讀寫 Excel 文件GoExcel
- 假如用Go語言寫作文Go
- 用 Go 寫一個簡易的 dockerGoDocker
- go語言的31個坑Go
- Go 語言的分散式讀寫互斥Go分散式
- 寫了一個 gorm 樂觀鎖外掛GoORM