Skip to main content

這篇是客戶LockBit 2.0 付款贖金後,還是無法完整恢復重要數據,微軟事件團隊分析後發現是加密時候就有BUG

本文轉自: https://techcommunity.microsoft.com/t5/microsoft-security-experts-blog/part-1-lockbit-2-0-ransomware-bugs-and-database-recovery/ba-p/3254354

研究人員:Nino 和 Team Torstino(Microsoft 事件回應(以前稱為 DART/CRSP))

免責聲明:本文所包含的技術資訊僅供一般資訊和教育目的,不能取代專業建議。因此,在根據此類資訊採取任何行動之前,我們鼓勵您諮詢適當的專業人士。我們不對基於所提供資訊的特定結果或結果提供任何形式的保證。因此,使用或信賴本文中包含的任何資訊的風險完全由您自行承擔。

 


LockBit 2.0 勒索軟體是過去2~3年來主要的勒索軟體病毒之一。最近,FBI發布了一份緊急警報,概述了 LockBit 2.0 聯盟之「勒索軟體即服務」(Ransomware-as-a-service)相關的技術方面以及策略、技術和程序 (TTP)。

可以說,圍繞該勒索軟體的大量詳細研究是在2021 年夏天出現的「2.0」版本中出現的。然而,所有這些公開報告和技術承諾,都沒有提到微軟事件響應團隊研究人員發現的勒索軟體之一個關鍵方面,並且在提出勒索軟體主題時通常不會討論這一點:「錯誤代碼」以及不可預測的誘導後果。

這篇文章展示了針對 MSSQL 資料庫的勒索軟體復原的直接嘗試,其中我們發現並進一步利用 LockBit 2.0 勒索軟體程式碼中存在的Bug,直到我們能夠恢復這些資料庫檔案的加密過程,並恢復它們回到正常運轉狀態。這通常是一項不可能的任務,因為它意味著打破數十年來對密碼學的實際研究——不僅僅是在理論上,而且在實際實現中。

本篇將簡述某客戶機房所儲存的關鍵核心受損資料庫文件,以及所面對的各種復原嘗試步驟和挑戰。

背景

在與遭受到 LockBit 2.0 影響的客戶首次互動時,我們發現到該勒索軟體邏輯的嚴重不一致,該客戶有購買號稱可恢復勒索軟體造成的損壞,並將勒索軟體所加密的檔案還原。

但不幸的,這位客戶很快就發現,基於勒索軟體聯盟的「傳播商」所聲稱的支付贖金決心,獲得能解密的說法非常可疑。在嘗試使用購買的解密工具來恢復關鍵資料庫檔案時,客戶得到了非常令人失望的結果無法順利解密,並且對為何付費後這些資料庫檔案的復原還是沒有按預期進行,以及下一步要採取什麼步驟,完全不知道怎辦。

針對此事,微軟事件響應團隊與該客戶進行了接觸,獲得了勒索軟體加密器和解密工具方面的存取權限,並懷疑「錯誤的加密」正在發揮作用,因此開始進行分析。

我們對加密器的觀察並識別其異常情況

當懷疑有錯誤的加密/解密時,為了讓我們的生活更輕鬆,我們可以做的第一件事之一,就是首先避免深入研究任何有關密碼學的密集遲鈍方面的文獻,甚至更危險的現代密碼學。相反的,使用Sysinternals方便的 Procmon 工具,其提供監視檔案 I/O的功能,希望在加密器或解密器運作時,可發現任何類型的異常或不一致。

透過這種監控,我們應該快速並正確了解加密/解密演算法是如何實現的,假設它不是在記憶體中執行所有這些操作,並且確實像通常的情況一樣透過 I/O 管理器。

例如,圖 1 顯示了加密器在我們建立的測試虛擬檔案上的運作情況。值得注意的是,當假設有錯誤的加密演算法在起作用時,要測試各種檔案大小,看看它們如何/是否會產生不同的結果。我們經常在較大的檔案(至少 4GB 或更大)上看到一個常見錯誤,尤其是在 32 位元加密器中,不明白檔案長度越大,我們就越接近並最終跨越到標記領域。這些錯誤可能會導致對檔案大小、內部檔案指標設定方式等的錯誤檢查,從而導致加密器意外損壞。這是需要時時留意的事情。

縮圖 1 標題為圖 1。正在執行的加密器測試 #1圖 1. 運行中的加密器測試 #1

測試#1:進階觀察

  • 它會增加檔案長度
  • 它只加密從標頭開始處開始的前 0x1000 位元組(理論上,足以覆蓋掉任何標頭的元資料)
  • 在原始檔案大小(0x200 位元組)的末尾附加一些數據
  • .lockbit副檔名附加到原始檔名

總結:它附加到加密檔案末尾的資料,是解密工具在復原過程中所需使用到的解密資訊。每個檔案都使用唯一的 16 位元組初始化向量 (IV) 和 AES256 金鑰進行加密。兩者均以修改後的chaha加密方式儲存在每個加密檔案的末尾。解密工具反過來知道如何找到這個“解密斑塊(blob)”,提取唯一的 IV 和 AES256 金鑰,然後利用它們進行解密。其他資料也儲存在這些 blob 中,例如原始檔案大小和 AES 區塊大小。

圖 1 中 Procmon 輸出的測試 #1 ,顯示加密器改變了即將損壞的檔案的原始大小,因此只有當解密工具開始嘗試復原過程時,它才會在某處保留此原始資訊。至少理論上是這樣。在實踐中,我們很快就會發現,一些完全不同的事情有可能發生。

測試 1GB 檔案是一個好的開始,但讓我們嘗試一個更大的文件,然後再次透過 Procmon 觀察加密器的行為。

  縮圖 2 標題為圖 2。加密器運行中的測試 #2圖 2. 運作中的加密器測試#2

測試 #2:進階觀察:

  • 開始就像我們的第一次測試,但結束時卻截然不同
  • 奇怪的是,在附加「解密 (blob)」 時,Procmon 不會為WriteFile作業產生結果
  • 它似乎以 65,536 位元組的間隔進一步加密更多數據

與我們的第一次測試有一些明顯的差異,第二次測試引起了我們足夠的興趣,讓我們繼續深入挖掘,懷疑這裡有什麼嚴重不對勁。當我們嘗試查看Procmon 無法告訴我們附加解密(blob)結果的實例之後的WriteFile操作之呼叫堆疊時,它變得更加有趣。

縮圖 3 標題為圖 3. 查看 WriteFile 操作的呼叫堆疊圖 3. 查看 WriteFile 操作的呼叫堆疊

黃色突出顯示行中的空結果後面的每個WriteFile操作看起來都像右側的“事件屬性”框:空。這確實很奇怪,需要比 Procmon 給我們更深入的反思。在離開全能的 Procmon 之前,它繼續顯示其數值,為我們提供了一個有價值的有利位置:呼叫堆疊。我們可以看到,在偏移量+0xA0842處,我們可能永遠不會回來。

現在感覺是時候介紹我們最喜歡的工具集,來進行深入的故障排除:Time Travel Debugging逆向工程(TTD)

究竟是什麼問題?

在引入 TTD 框架之前,我們首先將加密器載入到 IDA Pro 中,並前往 Procmon 識別的偏移量以觀察該位置的程式碼。這樣做,我們可以看到我們達到對 ntdll!NtWriteFile 呼叫的回傳位址根據我們在反彙編或反編譯中可以進一步發現的內容,以下計劃是再次重新運行加密器,但這次是在TTTracer的控制下產生一些我們可以處理的運行時資料。

縮圖 4 標題為圖 4。負責將加密內容寫回磁碟的程式碼圖 4. 負責將加密內容寫回磁碟的程式碼

我們也展示了這段程式碼的清理反編譯,以便在更高的層級進行觀察。

縮圖 5 標題為圖 5。圖 4 的反編譯圖 5. 圖 4 的反編譯

如圖 4 和圖 5 所示,我們可以發現這裡有些不對勁;寫入檔案的 NTSTATUS回傳值未正確處理。事實上,這是完全錯誤的。我們可以示範這種寫入檔案操作處理不當後果的一種方法,是詢問加密器是否非同步操作。我們將很快解釋在我們的調查中引入這一點的原因。

但如果我們深入研究 IDA 內部的二進位文件,我們就可以確認加密器的非同步性,這是透過I/O 完成連接埠實現的。實際的檔案加密是透過作為線程執行的回調例程完成的,對於調試愛好者來說非常有趣的是隱藏線程。

縮圖 6 標題為圖 6。加密器多執行緒初始化並使用執行加密的隱藏執行緒圖 6. 加密器多執行緒初始化並使用執行加密的隱藏線程

對NtSetInformationThread的呼叫所做的是,在內部執行緒結構中設定HideFromDebugger標誌,這保證了偵錯器永遠不會收到該執行緒的任何偵錯事件,從而有效地錯過了這些執行緒的可控執行。嘗試以傳統方式調試此加密器時,需要注意的事項。由於我們計劃使用 TTTracer,因此這些反調試惡作劇沒有實際意義,我們可以完全忽略它們。

這很好,但是 NTSTATUS 值到底有什麼問題呢?首先,LockBit 2.0 開發人員錯誤地假設所有不成功的NTSTATUS值都已簽署。例如,考慮到加密器的異步行為,以下這些與加密器非常相關,並且顯然不是負數。

縮圖 7 標題為圖 7. NTSTATUS 值圖 7. NTSTATUS 值

其次,更重要的是,它們完全忽略了待處理 I/O 操作的處理:STATUS_PENDING。鑑於 Windows 上 I/O 的非同步特性,理論上這可能是每個檔案 I/O 操作。此外,考慮到加密也是透過 I/O 完成連接埠非同步執行的,ntdll!NtWriteFile 可以並且將會傳回 STATUS_PENDING,呼叫者必須正確考慮這一點。如何解釋它?稍後說明!(參見WaitForSingleObjectZwWaitForSingleObject

不這樣做將導致不可預測且可能具有破壞性的行為,因為當傳回值未簽署時,LockBit 2.0 會錯誤地認為每次寫入作業後都會成功。當多個執行緒同時運行時(它們將會如此),您現在會產生一種情況,可能會導致所有這些工作執行緒以不可預測的時間間隔進行寫入。看起來像是一個小考驗,但由於這種處理不當,加密器的整體穩定性現在受到了質疑。這些自然而然也會影響到解密工具。

IO_STATUS_BLOCK

NtWriteFile(
IN HANDLE           FileHandle,
IN HANDLE           Event OPTIONAL,
IN PIO_APC_ROUTINE  ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PVOID             Buffer,
IN ULONG             Length,
IN PLARGE_INTEGER ByteOffset OPTIONAL,
IN PULONG Key        OPTIONAL);
);

作業系統實作將 IO_STATUS_BLOCK 值寫入呼叫者提供的輸出緩衝區的支援例程。例如,請參閱 ZwOpenFile 或 NtOpenFile。這些例程傳回的狀態代碼可能與 IO_STATUS_BLOCK 結構中的狀態代碼不符。如果這些例程之一傳回 STATUS_PENDING,則呼叫者應等待 I/O 操作完成,然後檢查 IO_STATUS_BLOCK 結構中的狀態代碼以確定操作的最終狀態。

如果例程傳回 STATUS_PENDING 以外的狀態代碼,則呼叫者應依賴此狀態代碼而不是 IO_STATUS_BLOCK 結構中的狀態代碼。

關於損壞的解密工具(以及無法解密的檔案)

現在已經確定了至少一個可能導致加密錯誤的關鍵缺陷,讓我們將注意力轉移到解密過程本身,因為我們的主要目標是確認並希望實現已購買之解密工具應該能做的事情與能力。

客戶向我們提供了幾個 MSSQL 加密資料庫文件,這些文件有可能被正確解密。我們之所以可以做出這樣的聲明,是因為所需的解密資訊(參考前面提到的 Procmon 冒險經歷)在文件中的某處仍然完好無損。不是它應該在的地方,但它仍然在那裡。這種錯位是上述寫入檔案時,操作處理不當所導致的直接結果,也是導致解密工具無法檢索該資料區塊的原因。這種處理不當甚至可能會無意中把原始文件長度的截斷或擴大。在我們試圖完成的這個階段,僅僅將解密 (blob) 資訊存在於加密的二進位檔案中,並沒有真正的意義。

我們嘗試讓解密工具準確啟動並運行的第一件事,就是刪除加密資料庫檔案中的解密斑塊( blob)後面所有數據,使其看起來像最初的預期那樣「正確」附加結果。然後我們對其運行解密工具(在 TTTracer 下),看看會發生什麼。我們未能用這種方法解密文件,但透過生成的 TTD 跟踪,我們有一個窗口,可以窺視並識別我們一廂情願方法中的缺陷。

縮圖 8 標題為圖 8。找到了解密 blob,但它並不像應有的那樣位於檔案的末尾/尾部
圖 8. 找到了解密斑塊(blob),但它不像應該位於檔案的末端/尾部

透過查看產生的追蹤文件,我們能夠確定解密工具現在確實正確找到解密斑塊(blob),而且能夠成功解密它,以獲取解密所需的 IV 和 AES 金鑰。但是,該文件仍然沒有被解密。深入挖掘後,我們發現問題在於,它如何嘗試比較兩個 LARGE_INTEGER,即傳入的加密檔案長度,和儲存在解密斑塊(Blob)資料中的 AES 區塊大小(假設其已正確附加)。

縮圖 9 標題為圖 9。文件大小和我們正在處理的加密資料庫文件
圖 9. 文件大小和我們正在處理的加密資料庫文件

// disassembly responsible for initiating this sequence, by storing the incoming file size
.text:00428721 mov esi, dword ptr [eax+lb_encrypt_file_t.og_filesz] ; fetch the LowerPart of the file size
.text:00428724 mov eax, [eax+lb_encrypt_file_t.og_filesz.anonymous_0.HighPart] ; fetch the HighPart of the file size
.text:00428727 mov [esp+1Ch], eax ; store the HighPart of the file size
.text:0042872B lea eax, [esp+3E8h+var_268]
.text:00428732 push eax
.text:00428733 mov [esp+18h], esi ; save the LowerPart of the file size

// in the TTD trace, looking at the incoming file size being stored as a LARGE_INTEGER
00428724 8b4024 mov eax,dword ptr [eax+24h] 
ds:002b:1c9e0024=00000013
0:014> dd @eax
1c9e0000 00000000 00000000 00000000 00000000
1c9e0010 00000000 00000000 00000000 00000000
1c9e0020 fffec200 00000013 00000000 00000001

// size of the incoming file
0:014> dt ntdll!_LARGE_INTEGER 1c9e0020 QuadPart
0x00000013`fffec200
+0x000 QuadPart : 0n85899264512

// code that does the check after the offset has been calculated from the decryption blob
.text:004288E6 mov eax, [esi+lb_encrypt_file_t.byte_offset.anonymous_0.HighPart]
.text:004288E9 add edx, ecx
.text:004288EB adc edi, eax
.text:004288ED cmp [esp+1Ch], edx ; now check the LowerPart
.text:004288F1 jnz __size_check_fail_cleanup
.text:004288F7 cmp [esp+18h], edi ; now check the HigherPart
.text:004288FB jnz __size_check_fail_cleanup __success_go_for_decryption_of_encrypted_content

// go to the location where the check and “bug” is at
0:014> dx @$calls(0x4288ED).First().TimeStart.SeekTo()
Time Travel Position: 1CC3E8:F20 [Unindexed] Index
0:014> u . l4
decryptor+0x288ed:
004288ed cmp dword ptr [esp+1Ch],edx ; compare against LowerPart
004288f1 jne __size_check_fail_cleanup ; they have to match, otherwise decryption is skipped
004288f7 cmp dword ptr [esp+18h],edi ; compare against the HighPart
004288fb jne __size_check_fail_cleanup ; they have to match, otherwise decryption is skipped
0:014> r edx
edx=00000200 ; AES block size calculated out of the data inside the decryption blob
0:014> dd @esp+1c l1
1a73fb9c fffec200 ; LowPart of incoming file size, failing when being compared to the size of the decryption blob

0:014> r edi
edi=00000014 ; very revealing, this tells us where the decryption blob should actually be(what the HighPart should be)
0:014> dd @esp+18 l1
1a73fba4 00000013 ; HighPart, we see our cutting off all the data after the decryption blob breaks the logic here

根據 TTD 跟踪,簡單地切斷解密斑塊(blob)後面的所有資料也不起作用,但我們可以找出問題所在,甚至可以發現解密斑塊(blob) 最初應該在的位置:檔案中偏移量0x1400000000處 傳入檔案的大整數的高位元部分位於偏移量0x1300000000處,但與根據解密 blob 計算出的原始大小相比失敗:0x1400000000但即使在此之前, 0xfffec2000x200的比較也會失敗,因為它期望正確計算 AES 區塊大小,但事實並非如此。

意識到這一點,我們決定將解密斑塊(blob)「推」到其正確的偏移量,然後再次切斷其後面的所有數據,以再次將加密檔案重新創建為其最初的預期結構。完成後,我們透過解密工具重新運行它,並興奮地等待結果。

縮圖 10 標題為圖 10。在我們重新運行解密器之前正確對齊解密 blob
圖 10. 在重新運行解密器之前正確對齊解密斑塊(blob)

這次運行解密器後,我們成功解密了檔案!

decryptor_pp+0x288ed:
004288ed cmp     dword ptr [esp+0Ch],edx ss:002b:0271fb9c=00000200
0:007> r edx
edx=00000200// edx, as expected is 0x200
0:007> dd @esp+c l1
0271fb9c  00000200      // aes block size has correctly been calculated this time
0:007> t                 // step into, to validate the jne
decryptor_pp+0x288f1:
004288f1 jne     decryptor_pp+0x28c0a (00428c0a)         [br=0]
0:007> r zf
zf=1
0:007> t                 // step into to compare the next check for the HighPart
decryptor_pp+0x288f7:
004288f7 397c2414        cmp     dword ptr [esp+14h],edi ss:002b:0271fba4=00000014
0:007> dd @esp+14 l1
0271fba4  00000014// we see that they're the same, and the decryptor works as expected
0:007> r edi
edi=00000014
0:007> t
0:007> r zf
zf=1

縮圖 11,標題為圖 11。(L) 加密檔案; (R) 成功解密文件圖 11. (左) 加密檔案;(右) 成功解密文件

雖然這具有某種成功的欺騙性外表,但我們必須始終認識到加密器內部的致命錯誤。這些勒索軟體開發人員的關鍵缺陷,在於誤解了 NTSTATUS 值的工作原理,以及它們對簡單線程同步可能產生的後果。鑑於我們不想自己成為天真無知的受害者,我們很快就意識到問題的嚴重性現在才慢慢開始顯現。

 

Thx Chang

Author Thx Chang

More posts by Thx Chang