藉助動態代碼生成技術在基於Webkit引擎的HTML5網頁JS內調用易語言函數

作者:莊曉立(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函數

動態生成一個回調函數,作爲參數傳遞給jsBindFunction即可:


函數調用次序總結

到了該總結一下的時候了:我們藉助動態代碼生成技術,在運行時生成一個符合fastcall調用約定的回調函數(jsNativeFunction),通過jsBindFunction將其註冊到Javascript引擎,同時賦予其一個JavaScript函數名。網頁腳本調用此JS函數時,回調函數被調用,進而回調函數又調用了代理函數,代理函數又調用了用戶函數,用戶函數返回後,返回值又被逐層返回給JS引擎。

測試代碼

首先註冊一個測試用的JS函數,易語言代碼:註冊JS函數("plus1",用戶函數示例)。
再先來一段HTML,加載到瀏覽器中:
<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”,說明測試成功。

寫在最後的思考

能否將前面的實現方案用易語言內置函數 “置入代碼” 替換?我(Liigo)想,至少有兩個阻力妨礙我們在此應用置入代碼:1、置入代碼只能作用於已經存在的函數,而不能運行時動態生成新的函數;2、置入代碼是編譯時行爲,置入的代碼不能包含可變量(例如上文的 CALL eax 恐怕就行不通)。勉強應用置入代碼,也不是不行,只是會讓每一個用戶函數都非常複雜,既包含了置入代碼,又包含了JS和易語言的類型轉換,還包含了業務邏輯,沒有任何封裝性可言,易用性約等於零。


全文完。謝謝收看!知道我是誰嗎?







































大名鼎鼎的御前四品帶刀護士!



----------


2017年4月Liigo更新:

碰巧了,前一陣子我又用到WKE/Webkit。找了一圈也沒找到之前備份的源代碼,不知道丟哪裏去了;只找到本文,照着文中圖片上的代碼,抄了一遍,你別說,還真管用。順便說句題外話,話說當年,兩年前,2015年3月,我正式發表本文沒過幾分鐘,就有易友照着圖片把代碼打出來了。說回正題,這次重抄一遍代碼,還發現了一個問題並解決掉了,詳見文中“回調函數(fastcall)”一節。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章