標籤:

【遊戲安全】看我如何通過hook攻擊LuaJIT

譯者:興趣使然的小胃

預估稿費:200RMB

投稿方式:發送郵件至linwei#360.cn,或登陸網頁版在線投稿

一、前言

如果你在遊戲行業摸爬滾打已久,你肯定聽說過Lua這個名詞。作為一門強大的腳本語言,Lua已經嵌入到數千種視頻遊戲中,提供了各種API介面,以便工程人員在遊戲客戶端以及伺服器上添加各種功能。

我會不斷強調一個觀點:為了讓攻擊技術更加便捷、更加可靠以及更加高效,最好的方法就是攻擊遊戲引擎,而不是攻擊遊戲本身。Hook一大堆函數、定位一大堆地址本身是個很好的辦法,然而這意味著只要遊戲更新版本,你就需要更新你所使用的偏移量。相反,如果你hook了遊戲使用的那些庫,這些問題就會迎刃而解。

Lua的普及性使得它成為hook的理想目標。此外,由於遊戲開發者使用Lua來添加內容及功能,因此遊戲所包含的Lua環境就成為擁有大量功能的強大主機環境。

出於性能要求,使用LuaJIT來替代vanilla Lua是非常常見的場景。因此,在本文中我會探討如何攻擊LuaJIT。只要稍作修改,這種攻擊技術也可以應用於vanilla Lua。

二、注入Lua代碼

為了創建Lua環境,我們需要調用luaL_newstate返回一個lua_State對象,然後將其作為參數,調用luaL_openlibs即可。有人可能想通過劫持luaL_newstate的執行來注入代碼,然而這種方法並不能奏效。因為此時程序庫還沒有載入,因此載入腳本不會起到任何作用。然而,我們可以劫持luaopen_jit函數,這個函數正是打開程序庫時所調用的最後一個函數(參考此處)。

動態鏈接LuaJIT時,我們可以查找導出表來定位這個函數:

靜態鏈接LuaJIT時,我們可以使用一些特徵字元串來進行定位:

一旦找到這個函數,hook就不是件難事。然而,在hook之前,我們需要找到兩個函數:luaL_loadfilex這個函數用來載入我們的Lua腳本,lua_pcall這個函數用來執行Lua腳本。動態鏈接時,我們可以在導出表中找到這兩個函數;靜態鏈接時,我們可以使用「=stdin」字元串來定位第一個函數(參考此處):

定位第二個函數需要多費點功夫,因為該函數沒有關聯某個特徵字元串。然而幸運的是,該函數在內部調用時(參考此處),位於「=(debug command)」之後:

注意,上圖中我們還能觀察到luaL_loadbuffer函數的地址,牢記這一點,回頭要用到。

識別出這些地址後,我們就可以開始寫hook代碼了:

typedef void* lua_State;nntypedef int (*_luaL_loadfilex)(lua_State *L, const char *filename, const char *mode);n_luaL_loadfilex luaL_loadfilex;nntypedef int (*_luaopen_jit)(lua_State *L);n_luaopen_jit luaopen_jit_original;nntypedef int (*_lua_pcall)(lua_State *L, int nargs, int nresults, int errfunc);n_lua_pcall lua_pcall;nnint luaopen_jit_hook(lua_State *L)n{nint ret_val = luaopen_jit_original(L);nluaL_loadfilex(L, "C:test.lua", NULL) || lua_pcall(L, 0, -1, 0);nreturn ret_val;n}nnBOOL APIENTRY DllMain(HMODULE mod, DWORD reason, LPVOID res)n{nswitch (reason) {ncase DLL_PROCESS_ATTACH: {nluaL_loadfilex = (_luaL_loadfilex)LOADFILEEX_ADDR;nlua_pcall = (_lua_pcall)PCALL_ADDR;nHookCode(OPENJIT_ADDR, luaopen_jit_hook, (void**)&luaopen_jit_original);nbreak;n}n}nreturn TRUE;n}n

我的hook代碼如上所示,使用的是自己開發的hook引擎。你可以使用Detours或者自己的引擎。需要牢記的是,hook點應該位於DLL中,以便注入到進程中。

現在,創建Lua環境時,「C:test.lua」就會被載入到這個環境中。通常情況下,我首先會注入代碼,使用debug.sethook來劫持對Lua函數的所有調用以及相應的參數,以便後續分析:

lua jit.off()nFILEPATH = "C:LuaJitHookLogs" STARTINGTIME = os.clock() GDUMPED = falsenfunction dumpGlobals() local fname = FILEPATH .. "globals" .. STARTING_TIME .. ".txt" local globalsFile = io.open(fname, "w") globalsFile:write(table.show(G, "G")) globalsFile:flush() globalsFile:close() endnfunction trace(event, line) local info = debug.getinfo(2)nif not info then return endnif not info.name then return endnif string.len(info.name) <= 1 then return endnif (not GDUMPED) thenndumpGlobals()nGDUMPED = truenendnlocal fname = FILE_PATH .. "trace_" .. STARTING_TIME .. ".txt"nlocal traceFile = io.open(fname, "a")ntraceFile:write(info.name .. "()n")nlocal a = 1nwhile true donlocal name, value = debug.getlocal(2, a)nif not name then break endnif not value then break endntraceFile:write(tostring(name) .. ": " .. tostring(value) .. "n")na = a + 1nendntraceFile:flush()ntraceFile:close()nend debug.sethook(trace, "c")n

這段代碼可以提取到一堆有價值的全局信息以及跟蹤信息,存放在「C:LuaJitHookLogs」目錄下。

三、詳細分析

如果你非常熟悉Lua,你可以跳過這個部分,不然的話,你可以跟著我分析這個腳本的具體內容。

首先,我調用了jit.off函數,因為debug庫無法劫持由jit引擎實時編譯的那些調用代碼。

在dumpGlobals函數內部,我將名為_G的表列印出來。這是個全局對象表,Lua使用這個表來跟蹤全局域內的所有內容,並將跟蹤結果以「key, value」鍵值對形式保存在該表中。你肯定能夠想到,這個表的價值非常高。根據你的具體情況,你可能需要晚一點再調用dumpGlobals函數,因為有些遊戲在調用第一個函數時並沒有把所有的全局變數分配完畢。

我使用了debug.sethook(trace, "c")語句,使Lua在每個函數調用完成之前調用trace這個函數。在trace函數內部,我調用了debug.getinfo(2)以獲取被劫持的函數名稱。由於trace函數為當前正在使用的函數,也就是說該函數在棧上的級別為1,因此被劫持的函數的級別為2。然後我循環調用了debug.getlocal(2, a),其中a的值從1開始不斷累加,直至該語句返回空值(nil)為止。通過這種方式,我們可以循環遍歷級別為2的棧,找到所有的本地變數,並將查找結果以鍵值對的形式保存起來。對某個遊戲這樣處理後,我找到了如下信息,你可以根據這些信息猜到這是哪個遊戲:

type()n(*temporary): table: 074FA7D0n{nIsDestroyed = false,nNumOfSpawnDisables = 0,nSpawnOrderMinionNames = n{n"Super",n"Melee",n"Cannon",n"Caster",n},nWillSpawnSuperMinion = 0,n}n

我們可以調用type來確定對象的類型,但對象本身就可以告訴我們關於該遊戲的一些有趣信息。

如果我們願意的話,我們可以使用debug.setlocal將參數改成某些函數。

四、劫持Lua代碼

在許多情況下,我們需要劫持整個Lua腳本。通過這種方式,我們不需要將跟蹤結果拼接起來,就可以詳細分析遊戲所用的腳本,理解腳本具體功能。Lua代碼可以以文件或者緩衝區的形式載入到LuaJIT環境中。由於分析磁碟上的文件比較容易,因此我們會重點關注使用緩衝區的這種情況以及luaL_loadbuffer這個函數。

這個函數實際上有兩種表現形式:分別為luaL_loadbuffer以及luaL_loadbufferex。這兩個函數基本相同,第一個函數會調用第二個函數,只不過會把最後一個參數設為NULL(參考此處)。你可以會認為,只要hook luaL_loadbufferex這個函數,我們就可以搞定這兩種情況,然而事實並非如此。由於LuaJIT主要是針對性能優化而設計的,因此通常情況下luaL_loadbufferex會以內聯形式使用。然而,這兩個函數最終都會調用如下這個函數(參考此處):

int lua_loadx(lua_State *L, lua_Reader reader, void *data, const char *chunkname, const char *mode);n

LuaJIT偏向於使用內聯代碼,導致這個函數也變成內聯形式。然而,這裡最有用的是lua_Reader reader,這個回調函數知道如何將void *data轉換為包含Lua代碼的緩衝區。當LuaJIT載入位於緩衝區中的代碼時,reader變為reader_string的地址(參考此處),而void *data所指向的char*字元串即為具體的Lua代碼。

reader_string不是內聯函數,因為它的地址可以作為回調指針來傳遞,因此我們可以在luaL_loadbuffer內部找到這個地址:

利用這個地址,我們可以構造一個hook,來劫持並顯示已載入的所有Lua緩衝區:

typedef const char* (*_reader_string)(lua_State *L, void *ud, size_t *size);n_reader_string reader_string_original;nnconst char* reader_string_hook(lua_State *L, void *ud, size_t *size)n{nif (((size_t*)ud)[1] > 0)nMessageBoxA(NULL, ((char**)ud)[0], "LuaJITHook", MB_OK);nreturn reader_string_original(L, ud, size);n}nn// from DllMain DLL_PROCESS_ATTACHnHookCode(READERSTRING_ADDR, reader_string_hook, (void**)&reader_string_original);n

當然使用對話框來顯示並不是優雅的解決辦法,不要在意這個細節,你理解我的意思就可以了。

五、總結

這種方法非常強大。許多遊戲提供了Lua腳本功能,可以實現自動化、拉高遊戲視圖以及ESP(透視)黑科技等。不同的遊戲使用Lua的方法有所不同,但他們的工作原理都與本文的例子相似。

你可以在這段hook代碼的基礎上進行修改,添加掃描功能,自動定位這些函數,比如,你可以使用XenoScan這個庫來完成這個任務。

如果你有什麼意見或者建議,可以隨時發表評論,也可以關注我的推特了解我最新發布的信息。


推薦閱讀:

Unity3D熱更新LuaFramework入門實戰(5)——UI
HammerSpoon - 不止是窗口管理
用好Lua+Unity,讓性能飛起來—LuaJIT性能坑詳解
Lua程序逆向之Luac文件格式分析
用LuaStudio調試Unity中SLua里的Lua5.3代碼

TAG:Lua | Hook | luajit |