作者:莊曉立(Liigo)
日期:2015年3月3日夜(2017年4月更新,詳見文中和文末說明)
原創鏈接:http://blog.csdn.net/liigo/article/details/44045177
版權所有,轉載請註明出處:http://blog.csdn.net/liigo
前兩天我協助朋友解決了一個技術問題,在此稍作記錄和總結。
具體來說,就是在使用基於Webkit引擎的封裝組件wke的過程中,需要把一個易語言函數註冊給JavaScript引擎,讓它可以在網頁裏被調用(就像在網頁裏調用普通JavaScript函數一樣)。如果能做到這一點,就基本實現了從JavaScript傳遞參數到易語言、易語言返回值給JavaScript的雙向溝通機制,以後有廣泛的應用空間。
在整體思路上,還是蠻簡單的。因爲wke已經提供了頗爲直觀的接口函數(雖然嚴重缺乏文檔):
#define JS_CALL __fastcall
typedef jsValue (JS_CALL *jsNativeFunction) (jsExecState es);
WKE_API void jsBindFunction(const char* name, jsNativeFunction fn, unsigned int argCount);
WKE_API void jsBindGetter(const char* name, jsNativeFunction fn); /*get property*/
WKE_API void jsBindSetter(const char* name, jsNativeFunction fn); /*set property*/
WKE_API int jsArgCount(jsExecState es);
WKE_API jsType jsArgType(jsExecState es, int argIdx);
WKE_API jsValue jsArg(jsExecState es, int argIdx);
......
這裏面最核心的函數是 jsBindFunction(),調用它就能註冊一個新的JavaScript函數,只需提供函數名、實現回調函數、參數個數。在回調函數內部,通過 jsArgCount/jsArgType/jsArg 讀取js傳進來的參數,通過其他一些接口函數創建js值對象,都是一目瞭然的事情,這都不是事兒。
回調函數(fastcall)
首先卡在該回調函數的調用約定上:jsBindFunction的第二個參數,要求是 fastcall 調用約定的回調函數!可是易語言編譯器根本就不支持編譯生成fastcall調用約定的函數呀(僅支持stdcall)。fastcall 約定通過寄存器 ecx 和 edx 傳遞前兩個參數,其餘參數按照從右向左(從後往前)的順序壓棧,被調用者負責清理、平衡棧。這跟stdcall有一些類似但又明顯不同。如果不管三七二十一盲目傳遞 stdcall 調用約定的回調函數進去,程序運行時非崩潰不可。
那怎麼辦呢?易語言編譯器不支持fastcall,我們只好自食其力,純手工生成二進制X86機器指令,人肉編譯生成符合fastcall調用約定的回調函數。該函數聲明的原型是:jsValue (__fastcall *jsNativeFunction) (jsExecState es),唯一個參數可從 ecx 寄存器中讀取,沒有入棧的參數,因而也不用平衡棧,直接 ret 就完事了。爲了方便起見,我們引入兩個易語言編寫的函數:代理函數和用戶函數,其中代理函數負責JS和易語言的類型轉換,用戶函數負責具體的執行邏輯,這兩個函數毫無疑問都只能是stdcall調用約定(易語言編譯器也不支持別的什麼約定嘛)。下面設計我們的回調函數結構,以僞彙編代碼來表示:
PUSH 用戶函數地址
PUSH ecx
MOV eax, 代理函數地址
CALL eax
RET
這些僞彙編代碼,要是用易語言寫的話,其實就是一句話:返回(代理函數(es,用戶函數))。(注:參數es是JavaScript引擎通過ecx寄存器傳遞進來的透明數據。)
易語言代碼固然是簡單,但因爲編譯器的限制,我們不能這麼寫。彙編代碼稍微複雜一點,但我們仍然不能直接嵌入彙編(易語言編譯器不支持)。只能手寫機器碼!把Intel指令集手冊拿出來,查表,開工。既然是動態生成代碼,當然需要先申請一塊內存,然後把機器碼填進去,然後把這塊內存的首地址返回——這個內存的首地址也就是我們人肉編譯生成的符合fastcall調用約定的回調函數的首地址。具體代碼如下:
2017年4月Liigo更新:發現在Windows Server 2008系統下,“申請內存”申請到的內存區域不具有可執行權限,一嘗試執行程序就崩潰了。
解決辦法是將下面這一行代碼:
函數體 = 申請內存 (32, 真)
替換爲如下代碼:
' Liigo 20170322
' 確保申請的內存具有可執行權限,否則在Windows Server 2008等系統下執行失敗(access violation when executing)
' https://www.corelan.be/index.php/2010/06/16/exploit-writing-tutorial-part-10-chaining-dep-with-rop-the-rubikstm-cube/
函數體 = VirtualAlloc (0, 32, 十六進制 (“1000”), 十六進制 (“40”)) ' MEM_COMMIT(0x1000), PAGE_EXECUTE_READWRITE(0x40)。可不釋放。
其中的VirtualAlloc是Windows API函數。
代理函數(stdcall)和用戶函數(stdcall)
前面提到的代理函數,是一個很普通的易語言函數(stdcall),它負責解讀JavaScript傳遞進來的參數,轉換成易語言數據類型,轉調易語言版的用戶函數(也是stdcall),最後再把易語言用戶函數的返回值轉換爲JavaScript類型後返回給JavaScript引擎。它接收兩個參數,都是我們前面手工生成的回調函數傳遞進去的。代碼如下:
代理函數的返回值是長整數型,也就是64位整數。根據 jsValue 的定義,它是64位指針,恰好可以用易語言的長整數表示。
JavaScript文本確定是UTF-8編碼,轉換到易語言文本之前,最好先執行編碼轉換(UTF-8 => GB18030),否則中文亂碼。這一步驟非常簡單,就作爲課後作業吧。
我們完全可以改進這個代理函數,或者寫另外一個代理函數,用於支持不同類型的用戶函數(例如不同的參數類型和參數個數以及返回值類型)。
剩下的用戶函數就更簡單了,下面只是一個常規的示例(後面的測試代碼就用到此函數):
把易語言函數註冊爲JavaScript函數
函數調用次序總結
到了該總結一下的時候了:我們藉助動態代碼生成技術,在運行時生成一個符合fastcall調用約定的回調函數(jsNativeFunction),通過jsBindFunction將其註冊到Javascript引擎,同時賦予其一個JavaScript函數名。網頁腳本調用此JS函數時,回調函數被調用,進而回調函數又調用了代理函數,代理函數又調用了用戶函數,用戶函數返回後,返回值又被逐層返回給JS引擎。
測試代碼
<a href='#' οnclick="document.getElementById('result').value=plus1('liigo');">link</a>
<p>
<textarea rows='6' cols='36' id='result'>hello</textarea>
當點擊網頁中的鏈接時,之前註冊的JS函數 plus1 將被執行,進而易語言函數 用戶函數示例 被調用。易函數返回的文本,成了 plus1 的返回值,最終輸出到網頁內的編輯框中。如果編輯框中文本顯示爲“liigo hohoho”,說明測試成功。