雖然現在的應用開發越來越趨向于web應用,大型軟件也大量使用了現有的框架,隨著現有框架和引擎的完善,絕大多數安全問題已經被解決。但是遇到一些定制需求時,開發人員還是不得不從底層一點點進行設計。這時,沒有安全經驗的開發人員很容易犯下錯誤,導致嚴重的安全隱患。本文以一款自主引擎的大型網絡游戲為例,展示開發中容易被忽略的隱患。
Lua作為一種功能強大又輕量級的腳本語言,可以非常容易地嵌入其他語言的程序中,越來越多的游戲引擎使用了Lua來實現具體的邏輯過程。這使得我們避開復雜的逆向工程直接分析游戲功能的邏輯成為可能。Lua腳本的一些特性甚至讓我們可以直接調試游戲中的腳本,比如使用著名的Decoda Lua調試器。如果配合簡單逆向資源包等手段,我們可以輕松獲取游戲中的腳本代碼(有可能需要反編譯),這使得我們的分析過程進一步簡化,由黑盒變成了半白盒。
首先我們對游戲進行了簡單的逆向來簡化腳本分析的難度。通過逆向工程得知,游戲引擎對Lua腳本的依賴度非常高,C++只是用于基本的類和方法以及游戲渲染,幾乎所有邏輯都是由腳本完成。同時為了更加方便我們的分析工作,對游戲的資源文件包進行了簡單的逆向,成功解包出了游戲客戶端的Lua腳本,至此,準備工作全部完成。
下列例子按照被發現的時間排序:
這個嚴格來說不算是漏洞,只是一個非常有意思的BUG。
在開發功能的時候,開發人員通常只會考慮實現功能和功能內部基本的安全性。這樣并沒有錯,但是卻有一種情況是這種開發模式無法防御的:正常功能特性被濫用。一個典型的案例就是下面這個隱身的BUG。紅框中玩家模型處于不可見狀態,無法通過點擊模型選中,在選中他之前也無法發現他的存在。
這種隱身從邏輯角度來看肯定是不正常的,但不是從代碼角度來看,它卻又是正常的,所以在測試中也很難被發現。我們來看看實現隱身的代碼:
這段代碼其實只做了一件事情,那就是頻繁的修改人物外觀的顯示狀態,那么為什么人物會消失呢?這就得從基礎說起了:游戲的渲染機制是每次模型外觀出現改變,將模型刪除,使用新的參數重新創建模型。這樣,在刪除模型和重新創建之間有一個時間差,這段時間內對應的玩家模型是不存在的!
在正常情況下這并不是什么大問題,但是如上面那一段代碼,如果極其頻繁地修改模型,就會導致模型根本沒有機會顯示出來,而這種問題如果在設計階段沒有引起注意,到了開發階段就很難被發現。
這是本文中最有意思的漏洞了。某一次大更新后,例行解包客戶端看看更新了什么內容,無意中看到一個服務器腳本(不要驚訝,我也不知道為什么,但是客戶端里確實有一部分服務器腳本~)中增加了一個名為OnCanJoinNormalMap的遠程調用函數,看名字似乎是跟進入地圖有關。
難道這是傳說中的任意傳送!小伙伴們驚呆了!實際測試一下,真的可以傳送到任意指定坐標!
在一陣驚喜過后,腦洞大開的小伙伴們還寫了一個完整的利用工具來實現真正意義上的任意傳送(雖然不到一周時間漏洞就修了~):
修復以后的腳本變成了這樣:
從注釋內容來看,這個接口似乎在上線時根本沒有實際使用,這也是開發人員經常犯的錯誤之一——上線一些根本沒有使用但是卻已經實現的接口。由于這些接口出于開發階段,可能并沒有完備的防護機制,比如上圖這個接口實現,沒有任何過濾。這些接口一旦被意外找到,事情就開始變得不可控了。
我們知道,游戲里通常都有一些可使用的道具,但是很少有人關心過物品使用這一過程是如何實現的,其中可能存在什么問題。
下面我們來看看物品使用的過程到底發生了什么。
首先點擊物品的函數是這樣的:
函數結尾的OnUseItem才是使用物品的關鍵,那么這個函數又是如何實現的呢?
可以看到一些物品的處理是由UseItem函數完成,這個函數不再是lua寫的函數,而是C++函數了,部分代碼如下:
#!c++
KItem::UseItem(DWORD dwBox, DWORD dwX, KTarget& rTarget)
{
......
pItem = GetItem(dwBox, dwX);
......
if (pItem->m_dwSkillID != 0 && pItem->m_dwScriptID == 0)
{
eRetCode = UseItemSkill(pItem, rTarget);
KG_PROCESS_ERROR_RET_CODE(eRetCode == uircSuccess, eRetCode);
}
.....
}
這里有一個不太尋常的調用:UseItemSkill,而從OnUseItem函數中我們也可以看到skill的影子。
從代碼中可以看出item對象存在item.dwSkillID, item.dwSkillLevel兩個屬性,難道說這是使用物品實際上是一種技能釋放?也就是說我們可以通過直接調用技能釋放接口來使用一些我們根本沒有的物品?
測試證明,確實是這樣的。
一個20級身上空空如也的小號,在執行了OnAddOnUseSkill(4894,1)后獲得了必須裝備某道具才能獲得的效果(4894為該物品對應的技能ID)。
在使用物品的方法中存在校驗物品數量的代碼,但是釋放技能卻不需要校驗物品存在,通過直接釋放物品對應的技能,我們成功繞過了物品的校驗,使用了玩家身上并不存在的物品。
在一些設計中,不同的功能可能會存在同一套底層的實現,如果在底層實現中沒有對調用來源進行充分驗證,就有可能繞過前端,直接調用底層實現方法來繞過原本的保護機制。
大并發的問題其實很常見,也在很早的時候就有過分析。但是在一些正常情況下不可能存在并發的位置,程序員很有可能忽略了對大并發的防護。
游戲中有一個幫會福利,每周幫會可以使用幫會資金的一部分作為“工資”發給幫會成員,而幫會資金的來源,可以是任務獎勵,也可以是玩家捐贈。
這里出現了一進(捐贈)一出(發工資),如果捐贈的接口存在并發問題,那么我們就有可能獲取到額外的金幣。
通過游戲界面的按鈕,找到了捐贈的接口,于是開始了邪惡的計劃:
這是原本的幫會資金和個人財富
執行一下我們的邪惡代碼
身上的錢變成了負的90萬!而幫會資金卻增加了90萬!我們成功進行了一次透支操作,接下來只需要用其他號把幫會里的資金取出就可以消費了~活脫脫的信用卡套現!
這是一個非常容易被忽略的地方。由于正常玩家操作都是通過界面點擊,輸入金額進行操作,根本不可能出現并發請求,作為一個不公開的內部接口,程序員對此處毫不設防。但是接口被挖掘出來,用腳本來實現,大并發的問題就凸顯出來。
這恐怕是所有問題中最嚴重的了,也是很難被發現的一處。在游戲引擎中,經常需要通過腳本進行一些操作,比如創建界面元素后可能需要腳本完成初始化,這次的問題就出現在了初始化腳本上。
通常大型網游為了信息傳遞更加方便,聊天欄是允許發送物品鏈接的,這樣一來就會引入一段代碼專門用于鏈接的生成,通常會使用腳本對生成的鏈接進行初始化,比如下面這個函數:
看似很正常,但是我們發現其中出現了“script=”,這里應該就是附帶的初始化腳本了。原本的腳本代碼只是設置控件的屬性,但是熟悉sql注入的朋友可能會發現一個問題:這里的腳本代碼是直接用參數拼接而成,似乎沒有過濾。我們可以嘗試構造一個特殊的內容截斷原本的腳本,執行我們自己的代碼。
游戲中玩家聊天信息發送是一個自定義表結構,閱讀接收信息的腳本得知,MakeEventLink函數的4個參數中szText, szName, szLinkInfo均是直接來源于接收到的聊天數據。接下來我們開始構造攻擊語句
使用引號閉合語句,寫入我們自己的代碼,這里要注意閉合最后多余的一個引號,否則會導致語法錯誤無法執行。
效果好象不錯的樣子
由于問題出在客戶端接收聊天信息的代碼里,所以這個漏洞可以被遠程利用,也就是說在權限足夠的情況下甚至可以調用os庫遠程格盤,不愧是居家旅行,殺人滅口必備漏洞!
開發過程中有太多容易被忽視的安全問題,其中絕大多數都是過分“信任”造成的。信任同事的代碼,信任自己的代碼,信任調用來源的合法性,信任用戶的操作,正是這些原本不應該的信任給攻擊者留下了攻擊的空間。開發原本就是一個創造性的工作,我們更應該去懷疑而不是信任。懷疑一切也許并不能提高開發效率,但在關鍵時刻卻可以挽救整個系統。