這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本人在一家互連網金融公司上班,對於一家互連網金融公司,最基本的功能就是客戶入金和出金,而出金的穩定性是很重要的,出金不暢容易導致投資人恐慌,本文講的是出金,出金介面我們對接的是招商銀行的銀企直聯絡統,那麼銀企直連繫統是一個什麼樣的程式呢?
沒錯,這個程式是運行在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
image.png
繼續使用 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將會鎖定,公司出金服務就掛了。。。。
為什麼會密碼輸入錯誤呢?因為很有可能在自動輸入時,被其他程式幹擾了一下
我們在代碼中會盡量用 SetForegroundWindow 讓視窗保持在最前面,成為啟用狀態
那麼對於通訊故障,解決的辦法就只能是重新嘗試了
剩下的問題,我個人認為發出警示,人工處理一下會比較合適。
此時迎來兩個新問題,
- 我們如何知道訊息框裡的內容是什麼
- 我們如何知道外掛登入成功了呢?
對於第一個問題,我們可以通過 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服務發送警示
歡迎關注我的公眾號:DeepIn-z