在本文中,我們將以重建著名的本地Windows密碼後門為例,為讀者演示如何利用Frida,通過動態插樁技術實現軟體的快速探查與原型構建。

搭建Frida環境

首先,我們來介紹如何安裝和配置Frida。本文中,我們將使用基於Python的標準Frida環境,因為它提供了許多簡單易用的工具,非常便於上手。為此,只需在Windows上安裝好Python,然後,在命令行環境中執行pip install frida frida-tools命令即可完成Frida的安裝。

搭建好Frida環境之後,接下來要干點什麼呢?考慮到我對跟密碼相關的東東天生沒有抵抗力,因此,我決定鼓搗一下Windows本地安全認證子系統服務(lsass.exe)。雖然網路上面已經有許多介紹lsass的文獻,但是為了提高趣味性,我決定將Frida附加到lsass.exe,以開始我們的冒險之旅。對於當前載入的模塊以及任何模塊的導出函數,都將是我們感興趣的對象。最開始的時候,我嘗試以管理員身份從命令行中執行frida lsass.exe命令,可惜無法正確附加到該進程:

RtlCreateUserThread返回0xc0000022錯誤

不過,從PowerShell執行環境下運行Frida則會順風順水:

Frida已經附加到lsass.exe

事實證明,在我的Windows 10系統中,默認情況下並沒有賦予命令行環境SeDebugPrivilege許可權,但是在調用PowerShell時,則賦予了該許可權。

在管理員命令執行環境下SeDebugPrivilege許可權被禁用了

既然如此,我們不妨通過編寫腳本來枚舉lsass。我們最初的腳本非常簡單,只是調用Process.enumerateModules()函數枚舉並輸出各個模塊的名稱,從而弄清楚lsass中載入了哪些模塊。

枚舉lsass載入的模塊

藉助搜索引擎了解這些DLL時,其中的身份驗證包msv1_0.dll首先引起了我的關注。按照網上的介紹,它負責本地登錄驗證,所以,我們就把它定為我們的研究對象。Frida有一個名為frida-trace的實用程序(它是frida-tools包的一部分),可以用來「跟蹤」DLL中的函數調用。為了跟蹤msv1_0.dll,我們將使用runas完成本地互動式登錄。

msv1_0.dll的導出函數

執行兩次本地互動式身份驗證操作時,rida-trace對msv1_0.dll的跟蹤結果

如上所示,藉助frida-trace,我們可以輕鬆了解代碼的執行情況:LsaApCallPackageUntrust()->MsvSamValidate(),然後,在執行兩次本地登陸操作時,又調用了兩次LsaApLogonTerminated()函數。為了在不閱讀MsvSamValidate()的函數原型的情況下弄清其返回值,我決定在onLeave()函數中使用一個簡單的log(retval)語句來查看函數的返回值(如果有的話)。這個函數是frida-trace為那些與跟蹤目標相匹配的方法創建的、自動生成式處理程序的一部分,它會將一小段javascript代碼轉儲到__handler__目錄中。

MsvValidate的返回值

在這裡有一個簡單的假設:如果提供的憑證無效,MsvSamValidate()會返回一個非NULL值(可能是錯誤代碼或其他之類的東西)。這裡,我們並不關心該方法到底做了些什麼,特別是在提供正確憑證的情況下,相反,我們只想覆蓋其返回值,這樣,提供的憑證是否正確就無關緊要了。為此,可以編輯由frida-trace生成的處理程序,並在onLeave()方法中添加一個retval.replace(0x0)語句,結果……

Windows死機了

事實證明,盲目修改LSASS的內部結構的話,後果就會很嚴重,但是,把這作為一個練手的機會的話,還是很不錯的。好了,我們回到正題,要想正確地利用MsvSamValidate(),必須首先弄清其內部運行機制。

後門——方法#1

上次嘗試失利之後,我開始研究LSASS和身份驗證包的在線資料,其中讀到的一篇文章是介紹適用於任意本地Windows帳戶的「通用」密碼後門的。此後,我的注意力開始轉向RtlCompareMemory。根據該文章稱,這個模塊在進行身份驗證時會調用RtlCompareMemory,用來對來自本地SAM資料庫的MD4值與根據用戶提供的密碼計算得到的MD4值進行比較。為了演示其後門,這篇文章提供了許多示例代碼,並實現了一個通過硬編碼的密碼來觸發成功的身份驗證的場景。根據MSDN文檔的介紹,需要為RtlCompareMemory提供三個參數,其中前兩個是指針。第三個參數是要比較的位元組的長度。該函數只返回一個值,指出兩個內存塊中有多少位元組是相同的。在比較MD4的時候,如果有16個位元組是相同的,則認為兩個內存塊是相同的,這時,RtlCompareMemory函數將返回0x10。

為了從LSASS的角度來了解RtlCompareMemory的使用情況,我決定使用frida-trace來可視化函數的調用。得益於我們提前知道了自己感興趣的是哪個函數,所以,在使用frida-trace時直接指定目標函數名稱,就能找出哪些DLL或代碼調用了這個函數。

來自lsas.exe且未經過濾的RltCompareMemory調用

不難發現,在Kernel32.dll和ntdll.dll中都對RtlCompareMemroy函數進行了解析。雖然我關注的是ntdll.dll,但事實證明,兩者都用到了這個函數。調用該函數時,輸出會在終端中疾馳而過,所以很難看清(從上面屏幕截圖中的"ms"讀數就可以看出)。因此,需要對輸出進行過濾,讓其只顯示相關的調用。我遇到的第一個問題是:"這些調用是來自kernel32還是ntdll呢?",為此,我在自動生成的frida-trace處理程序中添加了一個模塊字元串來進行區分。

添加到log()中的模塊信息

運行修改後的處理程序時,我注意到兩個模塊中的RTLCompareMemory函數每次都被調用。這就有意思啦。於是,我決定記下它們的比較長度。我想兩者也許之間也許會有所不同吧?別忘了,RTLCompareMemory的第三個參數就是長度值,所以我們可以從內存中轉儲該值。

轉儲RTLCompareMemory的第三個參數

如您所見,在兩個已識別模塊中的RTLCompareMemory調用,就連它們的長度參數也是完全相同的。眼下,我決定把注意力放到ntdll.dll中的函數上面,暫時忽略kernel32.dll模塊。於是,我將進行比較的位元組轉儲到了正屏幕上,希望能夠從中得到一些有用的線索。辛運的是,Frida有一個專門的hexdump助手,正好可以用於完成這些工作!

RtlCompareMemory正在比較的位元組內容

我瞪著眼看了老長一段時間,努力從中尋找任何可能的線索,特別是與進行身份驗證時相關的內容。後來終於想起一件事:我給這個用戶帳戶設置的密碼是……password,最後,我發現存放密碼的緩衝區是RtlCompareMemory必須比較的內存塊之一。

用於存放利用ASCII、NULL填充的密碼的內存塊,是RTLCompareMemory用到的內存塊之一

我還注意到,RtlCompareMemory會使用不同的塊大小進行比較。由於本地Windows SAM資料庫將帳戶的密碼存儲為MD4哈希值,因此,這些哈希值在內存中可以表示為16個位元組。知道RtlCompareMemory要比較的位元組的長度後,我決定對其輸出進行過濾,只顯示比較16個位元組的內存的報告。這也是前面提到的文章中,檢查後門密碼時所採取的過濾方式。這樣一來,frida-trace生成的輸出的可讀性就更高了,可以幫助我們更好的了解軟體的內部機制。

  • 向runas命令提供正確的和錯誤的密碼,RtlCompareMemory函數在每種情況下都會被調用五次。
  • 對於輸入的密碼,其前八個字元似乎每個字元間都填充了一個0x00位元組,很可能是在進行unicode編碼時為組成一個16位元組流所致,以便於與其他內容(未知值)進行比較。
  • 對RtlCompareMemory進行第四次調用時,好像與來自SAM資料庫的哈希值進行了比較,該哈希值是以arg[0]的形式提供的。由於測試帳戶的密碼是password,因此,其MD4哈希值為8846f7eaee8fb117ad06bdd830b7586c。

當提供無效密碼testing123時,對RtlCompareMemory(包括內存塊內容)的5次調用,需要比較的內存長度是16個位元組

當提供有效密碼password時,對RtlCompareMemory(包括內存塊內容)的5次調用,需要比較的內存長度是16個位元組

此外,還需該記錄函數的返回值,以便了解在條件成立和不成立的情況下,到底返回什麼。為此,我又使用runas進行了兩次身份驗證,一次使用有效密碼,另一次使用無效密碼,觀察RtlCompareMemory函數的返回值。

RtlCompareMemory函數的返回值

對RtlCompareMemory的第四次調用,其返回值是在條件成立情況下(實際上比較的是MD4值)匹配的位元組數,它應該是16(十六進位形式為0x10)。根據目前掌握的情況,我天真地以為完全可以創建一個"通用後門",方法是對於所有來自LSASS內部的、比較長度為16位元組的RtlCompareMemory調用,一律讓其返回0x10。這就意味著可以使用任意密碼了,對吧?為此,我對frida-trace處理程序進行了相應的更新,主要在retval.replace(0x10)上面,這表示在onLeave方法中匹配了16個位元組並通過了測試!

覆蓋RtlCompareMemory的返回值後,身份驗證失敗!

RtlCompareMemory被調用的次數減少到只有2次(通常是5次),而且即使提供了正確的密碼,也無法通過身份驗證。看來,這個方法行不通。至於原因,我們猜測可能是覆蓋操作破壞了其內部驗證機制,在這些內部機制中,RtlCompareMemory的返回值可能用於陰性測試。

對於B計劃,我決定直接重新創建原文中的後門。這意味著,在使用特定密碼進行身份驗證時,設法讓每次檢查都成功(換句話說,讓RtlCompareMemory函數返回0x10)。從之前的測試可以了解到,第四次調用RtlCompareMemory會對兩個緩衝區進行比較,其中,一個緩衝區存放的是利用所提供密碼計算出的MD4值的,另一個緩衝區存放的是本地SAM資料庫中的MD4值。因此,我們應該先嵌入已知密碼的MD4值,並在提供該密碼時觸發該後門。下面這行python2代碼,可以用來計算單詞backdoor的MD4值,並將其格式化為可供javascript代碼使用的數組形式:

import hashlib;print([ord(x) for x inhashlib.new(md4,backdoor.encode(utf-16le)).digest()])

當在python2解釋器中運行時,上面的代碼的輸出結果類似於 [22, 115, 28,159, 35, 140, 92, 43, 79, 18, 148, 179, 250, 135, 82, 84] 這樣的內容。這是一個可以在Frida腳本中使用的位元組數組,用於比較提供的密碼是否為backdoor,如果是,則從RtlCompareMemory函數返回0x10。這麼做,還能防止由於讓RtlCompareMemory在比較任何16位元組內存時都盲目返回0x10而破壞其他驗證機制的情況發生。

到目前為止,我們一直在使用frida-trace及其自動生成的處理程序與RtlCompareMemory函數進行交互。在與目標函數快速進行交互方面,這種做法非常理想,但從長遠來看,我們還需要更強大的方法。理想情況下,我們希望能夠輕鬆共享簡單的javascript代碼段。為了複製我們一直在使用的功能,我們可以使用Frida Interceptor API,提供ntdll!RtlCompareMemory的地址,並在那裡使用自動生成的處理程序執行我們的邏輯。我們可以使用Module API找到函數的地址,並使用該地址調用getExportByName。

//from:https://github.com/sensepost/frida-windows-playground/blob/master/RtlCompareMemory_backdoor.js

const RtlCompareMemory=Module.getExportByName(ntdll.dll, RtlCompareMemory);

// generate bytearrays with python:
// import hashlib;print([ord(x) for xinhashlib.new(md4, backdoor.encode(utf-16le)).digest()])
//const newPassword = new Uint8Array([136,70, 247, 234,238, 143, 177, 23, 173, 6, 189, 216, 48, 183, 88, 108]); //password
const newPassword = new Uint8Array([22,115, 28, 159, 35,140, 92, 43, 79, 18, 148, 179, 250, 135, 82, 84]); //backdoor

Interceptor.attach(RtlCompareMemory, {
onEnter: function (args) {
this.compare = 0;
if (args[2] == 0x10) {
const attempt = newUint8Array(ptr(args[1]).readByteArray(16));
this.compare = 1;
this.original = attempt;
}
},
onLeave: function (retval) {
if (this.compare == 1) {
var match = true;
for (var i = 0; i !=this.original.byteLength; i++) {
if(this.original[i] != newPassword[i]) {
match= false;
}
}

if (match) {
retval.replace(16);
}
}
}
});

生成腳本後,從具有管理許可權的PowerShell環境中通過frida lsass.exe -l.ackdoor.js命令調用Frida時,任何本地帳戶都可以使用backdoor作為密碼通過身份驗證。

後門——方法#2

我們上面留後門的方法仍然具有某些局限性:首先,通過網路登錄(例如使用smbclient啟動的登錄)時,無法使用backdoor密碼,其次,我們希望可以使用任意密碼進行身份驗證,而非只能使用backdoor作為密碼(或腳本中嵌入的任何內容)。藉助於我們已經編寫好的腳本,我決定更深入地研究一下,並嘗試找出調用RTLCompareMemory的原因。

實際上,我還是非常喜歡進行反向跟蹤的,而對於Frida來說,只需藉助於Thread模塊的backtrace()方法即可生成相應的回溯信息。通過回溯,我們能夠找出RtlCompareMemory函數的調用源,從而進行更加深入的分析。

每次調用RtlCompareMemory時列印相應的回溯信息

我調查了5個回溯信息,從中發現了2個值得玩味的函數名稱:第一個是MsvValidateTarget,第二個是MsvpPasswordValidate。MsvpValidateTarget是緊跟在MsvpSamValidate之後進行調用的,這就是本文前面採用的鉤子技術的失敗原因,因為那裡可能正在進行更多的處理。MsvpPasswordValidate函數是在第4次調用RtlCompareMemory時進行調用的,其作用是在進行互動式身份驗證時比較兩個MD4哈希值是否相等,具體如前所述。此外,我還在網上搜索了MsvpPasswordValidate函數方面的資料,發現這個方法經常用於密碼後門!實際上,它與Inception用於身份驗證繞過的方法相同。太棒了,也許這條路走對了。由於在網上沒有找到MsvpPasswordValidate的函數原型,於是請出IDA神器,很快就發現MsvpPasswordValidate具有7個參數。我認為這裡是使用鉤子(hook),並記錄相應的返回值的大好時機。

轉儲MsvpPasswordValidate的參數和返回值

在命令行中執行runas /user:user cmd命令,當輸入正確的密碼時,MsvpPasswordValidate將返回0x1,否則將返回0x0。看起來並不難,對吧?於是,我著手改造現有的鉤子,實際上,只需讓MsvpPasswordValidate總是返回0x1即可。如此一來,對於任何有效用戶帳戶都可以通過任何密碼進行身份驗證,即使使用網路身份驗證時也是如此!

對於有效用戶帳戶,任何密碼都可以順利通過身份驗證

// from:https://github.com/sensepost/frida-windows-playground/blob/master/MsvpPasswordValidate_backdoor.js

const MsvpPasswordValidate =Module.getExportByName(null,MsvpPasswordValidate);
console.log(MsvpPasswordValidate @ +MsvpPasswordValidate);

Interceptor.attach(MsvpPasswordValidate, {
onLeave: function (retval) {
retval.replace(0x1);
}
});

創建獨立的Frida可執行文件

到目前為止,我們構建的鉤子對於Frida python模塊具有很大的依賴性。不過,並不是所有的Windows目標系統上都安裝了這個模塊,因此,我們必須設法解決這個問題。雖然可以使用py2exe來生成獨立的可執行文件,但是,生成的文件往往過於臃腫,此外,還有許多其他的缺點。因此,我們決定用C語言來構建一個獨立的可執行文件,從而完全擺脫對於Python運行時的依賴。

實際上,Frida源代碼存儲庫提供了一些示例代碼,以供那些使用較低級別綁定的用戶進行參考。如果希望選擇這條道路的話,需要在兩種類型的C綁定之間做出選擇:一個包含V8 javascript引擎(frida-core/frida-gumjs),另一個不包含V8 javascript引擎(frida-gum)。當使用frida-gum時,需要使用C API實現插樁,從而完全跳過javascript引擎層。當需要考慮二進位文件的大小時,使用frida-gum具有明顯的優勢,但是實現的複雜性會有所增加。如果使用frida-core,則可以重用前面已經編寫好的javascript鉤子,並將其嵌入到可執行文件中。但是,這時的可執行文件需要打包V8引擎,文件大小不如使用frida-gum時有優勢。

這裡提供了一個使用frida-core的完整示例,我們的例子就是以它為模板的,唯一的不同之處就是確定目標進程ID的方法。原始代碼以進程ID作為參數,而我們的代碼則使用frida_device_get_process_by_name_sync確定目標進程ID,並提供lsass.exe作為感興趣的實際進程PID。 該函數的定義位於frida-core.h中,您可以將其作為frida-coredevkit的一部分進行下載。接下來,我嵌入了繞過鉤子即MSVPasswordValidate,並使用Visual Studio Community 2017編譯了該項目。最後,得到了一個44MB的可執行文件,現在,目標系統是否安裝了Python,它都可以正常運行了。不過,從文件大小來看,也許Py2exe並不是一個壞主意……

passback二進位文件,大小為44MB,它將注入到lsass.exe中,以允許使用任何密碼通過本地身份驗證

實際上,我們還需要做一些工作來優化生成的可執行文件的整體大小,但這需要從源代碼重新構建frida-core devkit,同時還需要刪除我們不需要的部分。在這裡,這些任務不妨留給讀者作為練習。如果您有興趣的話,可以從這裡下載與passback有關的源代碼存儲庫,在VisualStudio 2017中打開它後,點擊「build」按鈕即可。

小結

在本文中,我們以重建兩個Windows後門密碼為例,為讀者詳細介紹了Frida的使用方法。對於文中用到的所有示例代碼,讀者可以從這個GitHub存儲庫中下載。

本文由白帽彙整理並翻譯,不代表白帽匯任何觀點和立場

來源:利用Frida重建著名的Windows通用密碼後門|NOSEC安全訊息平台 - 白帽匯安全研究院

原文:sensepost.com/blog/2019

白帽匯從事信息安全,專註於安全大數據、企業威脅情報。

公司產品:FOFA-網路空間安全搜索引擎、FOEYE-網路空間檢索系統、NOSEC-安全訊息平台。

為您提供:網路空間測繪、企業資產收集、企業威脅情報、應急響應服務。

推薦閱讀:

相关文章