為什麼空密碼能夠取得你的ROOT許可權?

Editor發表於2018-02-03

為什麼空密碼能夠取得你的ROOT許可權?

背景

為防止你還不知道這個訊息,目前出現了一個足以影響到最新版macOS(High Sierra)的嚴重的安全漏洞。任何一個掌握了此漏洞的人都可以憑此使用空密碼或是他們自己選的密碼登入ROOT賬戶。哎呀!


顯然,這個漏洞剛剛才被披露出來,危害還不大,在蘋果公司的內部開發者論壇中,此漏洞是用來幫助那些有登陸問題的使用者的:


為什麼空密碼能夠取得你的ROOT許可權?


但是,當Lemi Orhan Ergin (@lemiorhan)發表了推特稱:“我們注意到macOS(High Sierra)存在著嚴重的安全問題”之後,這個漏洞贏得了更廣泛的關注。


為什麼空密碼能夠取得你的ROOT許可權?

我對這個漏洞感到相當的好奇,因此決定對macOS進行逆向,以追查其根源。在這篇博文中,我公佈了我的發現,並揭示了這個bug的底層原因。


所以,不要再囉嗦了,讓我們馬上進入正題!


深層挖掘


首先,讓我們看看發生在高階別的事情。當使用者(或攻擊者)嘗試登入到當前未啟用的帳戶(即root)時,出於某種未知的原因,系統將天真地使用該使用者指定的任意密碼(即使該密碼為空)建立該帳戶。那麼使用者(或攻擊者)就可以輕易地登入到該帳戶:


為什麼空密碼能夠取得你的ROOT許可權?


這兩個步驟的過程解釋了為什麼要執行這個攻擊,你必須敲擊enter鍵或點選“解鎖”兩次:


為什麼空密碼能夠取得你的ROOT許可權?


目前已被證實,如果使用者啟用了螢幕共享等服務,這種攻擊也可以遠端執行! 


為什麼空密碼能夠取得你的ROOT許可權?


當然,在不提供任何形式認證的情況下,不應該允許使用者隨意啟用帳戶,特別是具有全部強大功能的root帳戶(通過遠端方式!)。所以,到底發生了什麼?到了對macOS系統進行深挖的時候,讓我們看看幕後發生了什麼!


當使用者(或攻擊者)試圖向一個賬戶進行認證時,這個操作由'opendirectory'守護程式(opendirectoryd)處理。通過除錯這個守護程式,我們可以檢視此守護程式接收到一個mach XPC認證資訊時發生的函式呼叫的順序:


# ps aux | grep opendirectoryd

root 70 /usr/libexec/opendirectoryd

lldb -p 70

... 

(lldb) bt

* frame #0: opendirectoryd`od_verify_crypt_password

  frame #1: PlistFile`___lldb_unnamed_symbol26$$PlistFile

  frame #2: PlistFile`odm_RecordVerifyPassword

  frame #3: opendirectoryd`___lldb_unnamed_symbol37$$opendirectoryd

  frame #4: opendirectoryd`___lldb_unnamed_symbol313$$opendirectoryd


我們將從odm_RecordVerifyPassword函式開始。這個函式在PlistFile二進位制檔案中被實現。這個包(庫)從/System/Library/OpenDirectory/Modules/PlistFile.bundle路徑下被動態載入到opendirectoryd程式中:


(lldb) image list

[ 0] 50686B40-3B06-347D-B906-DCEF1D9F10E1 0x00000001041e5000 /usr/libexec/opendirectoryd

...

[188] A38BC5A0-67AA-3D75-89AD-57A7DF6D20BE 0x000000010447f000 /System/Library/OpenDirectory/Modules/PlistFile.bundle/Contents/MacOS/PlistFile


在odm_RecordVerifyPassword函式上設定一個斷點,我們可以轉存它的引數(通過RDI,RSI,RDX,RCX傳遞進來的):


Process 70 stopped

* thread #15, stop reason = breakpoint 1.1

PlistFile`odm_RecordVerifyPassword:

-> 0x10448e50b: pushq %rbp

(lldb) po $rdi

<OS_od_module: 0x7fcb0dc29110>

(lldb) po $rsi

<OS_od_connection: 0x7fcb0dc26cb0>

(lldb) po $rdx

<OS_od_request: 0x7fcb0dc78d30>

(lldb) po $rcx

<OS_od_moduleconfig: 0x7fcb0dc203b0>


來看看它的反編譯,我們可以看到它呼叫了另一個函式:sub_18f1


sub_18f1(&var_818, odconnection_get_context(rbx), r13);


傳遞給此函式(R13)的最後一個引數是一個字典,其中包含使用者(或攻擊者)正嘗試向其進行身份驗證的帳戶資訊:


(lldb) po $r13

{

  "dsAttrTypeStandard:AppleMetaNodeLocation" = (

     "/Local/Default"

  );

  "dsAttrTypeStandard:GeneratedUID" = (

     "FFFFEEEE-DDDD-CCCC-BBBB-AAAA00000000"

  );

  "dsAttrTypeStandard:Password" = (

    "*"

  );

  "dsAttrTypeStandard:RecordName" = (

     root,

     "BUILTIN\\Local System"

  );

  "dsAttrTypeStandard:RecordType" = (

     "dsRecTypeStandard:Users"

  );

  "dsAttrTypeStandard:UniqueID" = (

     0

  );

}


請注意dsAttrTypeStandard鍵的值:*。我們稍後會看到這個值!


接下來,odm_RecordVerifyPassword函式會呼叫另一個輔助函式:sub_826b,它依次呼叫sub_5192。反編譯這個函式出現的一串字串顯示它將從使用者(或攻擊者)正在嘗試登入的帳戶讀取“shadowhash data”。這個'shadowhash資料'儲存在'dsAttrTypeNative:ShadowHashData'鍵中:


為什麼空密碼能夠取得你的ROOT許可權?

可以使用命令dscl . -read / Users / <user>通過終端檢視使用者的“shadowhash”,或直接從路徑/ private / var / db / dslocal / nodes / Default / users / <user>下讀取:


$ dscl . -read /Users/user

...

AuthenticationAuthority: ;ShadowHash;HASHLIST:

<SALTED-SHA512-PBKDF2,SRP-RFC5054-4096-SHA512-PBKDF2>

;Kerberosv5;;user@LKDC:SHA1.F69BF62F41274B2B983399C0D143CD33961... GeneratedUID: A39EF7FC-E5B1-46B8-AB47-3C2B7DA49425


應該注意的是,對於已啟用的帳戶,如使用者帳戶,sub_519函式將成功讀取...因為這個“shadowhash”資料確實存在。但是,對於已禁用的帳戶(例如正在作為目標的root帳戶),此資訊不存在:


$ dscl . -read /Users/root | grep ShadowHash | wc

0   0   0


當“shadowhash”資料不存在時,sub_5192函式將失敗(返回0x0)。這會導致在sub_826b函式中執行“else”子句:


rax = sub_5192(var_98, r15, r12, r14, &var_88, &var_80);

if (rax != 0x0) {

   //found shadow hash data 

}

//no shadow hash data found 

else {

   //read 'dsAttrTypeStandard:Password' 

   rax = odproplist_get_array(r12, *_kODAttributeTypePassword);

   ...

   var_41 = 0x0;

   var_54 = 0x1388;

   if (od_verify_crypt_password(var_70, rax, var_60, &var_54, &var_41) != 0x0) {

      //upgrade password 

      sub_13d00(arg7, var_60);

      sub_14324(var_70, var_A0, var_68, var_50, r15, var_60, arg7);

...


在'else'語句中,程式碼首先從kODAttributeTypePassword(dsAttrTypeStandard:Password)鍵中讀取值。然後,它呼叫od_verify_crypt_password函式來驗證使用者(或攻擊者)傳入的密碼是否與該帳戶的密碼相匹配。例如,如果嘗試使用密碼“hunter2”登入(已被禁用的)根帳戶,則使用“*”(根帳戶的dsAttrTypeStandard:Password值)和“hunter2”呼叫od_verify_crypt_password:


Process 70 stopped

* thread #12, stop reason = breakpoint 1.1

opendirectoryd`od_verify_crypt_password:

(lldb) po $rdi

<OS_od_request: 0x7fcb0f2625e0> 

(lldb) po $rsi

<__NSCFArray 0x7fcb0f2511b0>(

*

)

(lldb) po $rdx

hunter2


如果我們繼續呼叫,它會返回一個非零值(al = 0x1)....意味著成功?有趣! 


(lldb) reg read al

al = 0x01


由於返回了非零值,並且沒有執行其他檢查,所以程式碼將執行假定提供有效密碼的邏輯(儘管真實情況並非如此!)具體來說,會呼叫各種方法,如sub_13d00。正如在反編譯中顯示的除錯日誌語句,它們將執行從crypt密碼到shadowhash或securetoken的升級:


"found crypt password in user-record - upgrading to shadowhash or securetoken"


如果我們查詢一下這些“升級”子程式(比如sub_13d00)是如何被呼叫的話,那麼我們會發現其實是我們提供的密碼(即'hunter2')起了作用:


Process 70 stopped

* thread #10, stop reason = breakpoint 2.1

PlistFile`___lldb_unnamed_symbol26$$PlistFile:

-> 0x104487552 <+743>: callq 0x104492d00

   0x104487557 <+748>: subq $0x8, %rsp

   0x10448755b <+752>: movq -0x70(%rbp), %rdi

   0x10448755f <+756>: movq -0xa0(%rbp), %rsi

(lldb) po $rsi

hunter2


這個新的“使用者指定的”值隨後被轉換為shadowhash / securetoken,然後儲存到帳戶(例如root賬戶)中。 因此,使用者(或攻擊者)便可以登入,因為他們可以用指定的密碼訪問對應帳戶!


讓我們回顧一下。 當使用者(或攻擊者)嘗試使用任何密碼(包括空白)對帳戶進行身份驗證時:


對於被禁用的帳戶(即沒有“shadowhash”資料的),macOS將嘗試執行升級


在此升級過程中,od_verify_crypt_password返回一個非零值,並且不執行其他檢查,因此程式碼假定升級成功


接下來“新”使用者提供的密碼被成功更新(“shadowhash / securetoken”)併為對應帳戶儲存下來


...這解釋了(很大程度上)為什麼可以使用任意(或空白)密碼啟用和訪問root帳戶。


剩下唯一的問題是(除了這個bug是如何通過High Sierra版本中的QA測試釋出出來的),即為什麼od_verify_crypt_password函式沒有失敗? 或者如果失敗了,為什麼沒有被發現? 現在我們來仔細看看。


顧名思義,od_verify_crypt_password應該驗證使用者(或攻擊者)指定的密碼是否對帳戶有效。 例如,當我們嘗試使用'hunter2'來驗證禁用的root帳戶時,od_verify_crypt_password應該果斷地告訴我們GTFO(滾開)。


od_verify_crypt_password函式直接在“opendirectory”守護程式(opendirectoryd)中實現。 如前所述,它被PlistFile包,特別是'sub_826b'函式呼叫:


//sub_826b

//check password and upgrade if necessary

if (od_verify_crypt_password(var_70, rax, var_60, &var_54, &var_41) != 0x0)

{

   //upgrade password

   sub_13d00(arg7, var_60);

   sub_14324(var_70, var_A0, var_68, var_50, r15, var_60, arg7);

}


我們已經注意到它被呼叫時所使用的各種引數,如帳戶的密碼雜湊和使用者/攻擊者指定的密碼。 但是,第四個引數(var_54)也是很重要的! 在被呼叫之前,它被設定為0x1388:


var_54 = 0x1388;

if (od_verify_crypt_password(var_70, rax, var_60, &var_54, &var_41) != 0x0){

...


在osstatus.com上查詢0x1388(十進位制數5000),顯示這個值對應於'kODErrorCredentialsInvalid': 


為什麼空密碼能夠取得你的ROOT許可權?


要執行現實的密碼驗證,od_verify_crypt_password函式會呼叫crypt_verify。帳戶密碼雜湊值(例如,對於被禁用的root帳戶;'*')、所提供的密碼(例如'hunter2')及var_54(被傳遞給od_verify_crypt_password的引數)都會被傳遞給此函式。這個引數同時被儲存到R14暫存器中,當且僅當某些字串比較結果為真時,該引數被設定為0x0:


int _crypt_verify(int arg0, int arg1, int arg2, int arg3) {

 r12 = arg3;

 r14 = arg2;

 ...

 if (strcmp(&var_130, r13) == 0x0) {

   *(int32_t *)r14 = 0x0;

 }


通過靜態和動態分析,我們可以確定(如預期的那樣),這個字串比較是將提供的密碼的雜湊值與帳戶的實際密碼的雜湊值進行比較。 換句話說,它正在驗證密碼(雜湊)匹配:


Process 70 stopped

opendirectoryd`crypt_verify:

-> 0x104243f3c <+965>: callq 0x104249e8a; symbol stub for: strcmp

(lldb) x/s $rdi

0x70000665eec0: "*.dAJ47YHEIRE"

(lldb) x/s $rsi

0x7fcb0ddcb851: "*"


這些字串顯然不匹配,所以strcmp函式不會成功(即返回值不會== 0x0)。 因此,我們正在跟蹤的被傳入的引數不會被設定為0x0。


在這一點上,我們對這個引數的作用有了清晰的認識。 它是一個指向一個變數的指標,當且僅當密碼(雜湊)匹配時,才從crypt_verify函式中設定為0x0的od_verify_crypt_password傳入。 因此,我們可以想象下面的虛擬碼:


//verify

// 'match' will be set to 0x0 if verification is ok! 

int match = kODErrorCredentialsInvalid; 

od_verify_crypt_password(accountHash, providedPassword, &match, ...);

....

//verify by checking hashes

// 'match' will be set to 0x0 if verification is ok!

if(strcmp(providedPWHash, accountPWHash, user) == 0x0) {

   *match = 0x0;

}


正如我們前面指出的,只有od_verify_crypt_password的返回值會被檢查....而不是實際驗證的結果!(即 所謂“匹配”的變數,var_54)。


這可以通過檢查以下的反編譯來確認,這段反編譯程式碼顯示了對od_verify_crypt_password的呼叫。請注意,所謂“匹配”變數(var_54)在呼叫之後從未被檢查。 相反,升級函式(sub_13d00,sub_14324)被錯誤地呼叫:


為什麼空密碼能夠取得你的ROOT許可權?


蘋果官方的回應


釋出這個部落格不久之後,蘋果釋出了一個針對macOS 10.13和10.13.1的補丁。 該補丁可以從蘋果的支援網站直接下載:


為什麼空密碼能夠取得你的ROOT許可權?

或者,它應該自動顯示為安全更新(在macOS應用商店中):


為什麼空密碼能夠取得你的ROOT許可權?

該錯誤被分配為CVE-2017-13872,蘋果在安全釋出說明中指出,這僅僅是“證書驗證中存在的邏輯錯誤”。 他們指出,他們的補丁“改進了憑證驗證”。

你可能想知道他們是如何修補這個bug的? ...我們在這個部落格中發現的根本問題是正確的嗎?


比較未打補丁和修補的PlistFile二進位制檔案,我們可以看到蘋果新增了程式碼來檢測無效的憑據(即,當未經身份驗證的攻擊者試圖設定root密碼時):


為什麼空密碼能夠取得你的ROOT許可權?

特別要說的是,如預期的那樣,他們現在在呼叫od_verify_crypt_password之後也會檢查所謂“匹配”變數的結果:


lea rbx, qword [rbp+var_54]    ;load addr of 'match' in rbx

mov rcx, rbx                   ;move into arg4 for call

call imp___stubs__od_verify_crypt_password

mov ecx, dword [rbx] ;get value of 'match'

test ecx, ecx        ;is it 0x0?

jne noMatch          ;no, then bail!


因此,我們的分析是正確的!


不幸的是,與其他的蘋果補丁一樣,這個補丁似乎還帶來了一些嚴重的問題。首先,它破壞了不同使用者的檔案共享:


為什麼空密碼能夠取得你的ROOT許可權?

正如我指出的(謝謝@alvarnell),這種不相容性很快就被一個新的補丁修復了(將構建帶到17C1003)。


更糟糕的是,據Wired報導,如果macOS 10.13上的使用者使用該補丁,然後升級到macOS 10.13.1,則會重新引入該錯誤:


為什麼空密碼能夠取得你的ROOT許可權?

聰明的Pepijn Bruienne(@bruienne)指出,這可能是由於蘋果公司既“沒有碰撞內部版本號”,也沒有“將 #iamroot bug的補丁放到10.13.1中:


為什麼空密碼能夠取得你的ROOT許可權?

使用者還報告說,在應用補丁後,需要重新啟動!


我的好朋友托馬斯·裡德(@thomasareed)發表了一篇很好的文章,全面總結了蘋果公司在這個補丁上的失誤。


結論


那麼,這是一個總結!在本部落格中,為了在Apple釋出補丁之前揭示了時下臭名昭著的#iamroot bug的根本原因,我們逆向了“opendirectory”守護程式的各種元件!


我們確定蘋果公司忘記檢查一個必要的變數(儲存有一個賬戶驗證結果)的值。


一旦一個補丁被髮布,我們將會逆向它,以確認我們的發現是正確的。萬歲!


本文由看雪翻譯小組 jasonk龍蓮編譯,來源objective-see轉載請註明來自看雪社群

相關文章