破解勒索軟體

wyzsk發表於2020-08-19
作者: mssp299 · 2015/07/16 11:08

原文地址:http://blog.cylance.com/cracking-ransomware

0x00 背景


在2013年初,某公司向我們求助,因為他們有大量的重要檔案被勒索軟體加密,導致無法訪問。這次攻擊者使用的,是一款披著“反兒童色情郵件”外衣的勒索軟體,它會遍歷所有磁碟,並加密其中的重要檔案。由於該軟體發動攻擊時,會安裝備份驅動器,因此總的來說,公司的資料會面臨一定的損失。不過幸運的是,我們能夠破解其口令,從而將被加密的資料恢復如初。下面,我們詳細介紹破解的流程。

我們之所以拖到現在才將其公之於眾,主要擔心會促使勒索軟體的開發者採用強度更高的口令保護方法。有大量證據表明,惡意軟體開發者經常會瀏覽安全公司的有關文章。例如,在我們破解這個軟體幾個月後 ,就有另外一家公司公開宣稱能夠恢復被該勒索軟體所加密的檔案。雖然這家公司沒有公佈細節,但是看上去惡意軟體作者仍然認真採取了相應的對策。該勒索軟體的新版本不久就面世了,與此同時,原來的口令猜解技術也毫無懸念地失效了。不僅如此,其作者還在軟體註釋中明確介紹了弱口令生成缺陷的有關情況。雖然早在2013年的美國黑帽大會上,我們就介紹過如何攻擊偽隨機數生成器,但是在如何破解勒索軟體方面,這還是頭一回公之於眾。

在公佈勒索軟體本身漏洞方面,我們希望研究人員酌情處理;最穩妥的方案是將其提交給受害者信賴的機構或公司,這樣,只要漏洞仍然存在於勒索軟體中,就能為受害者爭取更長的救助時間。

0x01 改進型的ACCDFISA


最初的勒索軟體樣本,都是來自同一個系列:ACCDFISA。由於該系列開發者的人為錯誤或者疏漏,該軟體系列早就被研究人員幹倒了。不過,這次我們所面臨的勒索軟體,比以前的有了很大改進。它自稱使用了AES-256加密演算法,為每位受害者都單獨使用了一個長256個字元的隨機金鑰,並且將其傳送給了攻擊者。此外,它還聲稱為了防止未加密的原始檔案和口令被人恢復,已經將其徹底刪除了。實際上,這些宣告只是勒索軟體妄圖讓受害者放棄反抗,乖乖就範的攻心術罷了。但是,勒索軟體作者會不會真有如此神通呢?

為此,我們來研究被加密一個的檔案,這個檔案已經被更名為:“(!! to decrypt email id ... to ...@... !!).exe”。它實際上是一個存放經過加密的RAR檔案的WinRAR自解包程式。很明顯,如果想要尋找WinRAR加密實現的漏洞的話,那是指望不了,所以,我們另闢蹊徑,直接從破解口令下手。為此,我們需要了解建立該口令的那些程式碼。

在被感染的驅動器上,我們不僅發現了這個勒索軟體,同時還發現其他一些看起來也與此有關的檔案。其中一個檔案,就是Microsoft Sysinternals中的sdelete工具,該工具的作用是永久性的刪除檔案,所以我們不僅暢想:要是該軟體存在Bug的話,我們就能速戰速決了。此外,我們還找到了一個名為“NoSafeMode”的庫,以及一個建立自解包程式的RAR工具。這些檔案的存在不禁使人浮想聯翩:攻擊者已經制造出了一個竊取程式碼的弗蘭肯斯坦怪物,然後與勒索軟體的主要可執行程式碼簡單縫合在一起,就像PureBasic 所寫的那樣。這裡的RAR工具就是我們破解該勒索軟體的突破口。由於這個工具需要提供金鑰口令來作為命令列引數,因此我們推斷,如果從勒索軟體中啟動這個壓縮工具的程式碼處開始回溯,就能夠找到構造該口令的程式碼了。

0x02 尋找口令生成器


首先,在我們一次性的機器上執行該勒索軟體,然後,連線除錯工具來攔截CreateProcess呼叫,這個呼叫的作用是啟動加密檔案的那個RAR工具。不一會兒,除錯工具就停下來,這時就可以檢視完整的命令列了,其中口令就位於“-hp”選項中。

enter image description here

當在除錯工具中執行的勒索軟體試圖啟動該RAR工具(這裡它被偽裝成“svchost.exe”)時,就會被我們截獲。這樣,我們可以看到執行勒索軟體的命令的具體內容了,其中就包括用來加密檔案的口令。

經過一番折騰後,我們得到了一個口令,雖然它未必能夠用來解密受害者檔案,但是,至少為逆向工程提供了有用的線索:我們可以尋找口令或者口令中的片段,搜尋可能用於生成口令的“字母表”(如果它是隨機生成的話),以及從命令列中尋找建立口令的“-hp”字串。

攔截到的口令,看起來像是由57個字母、數字和標點符號混合而成的。對於這個口令,如果是由人鍵入的話,看起來好像太隨意了一點,並且其字首為字串“aes”。後者也許只是一個巧合,就像汽車牌照上也會出現有意義的單詞或者號碼,同時,這個字首也可能是勒索軟體中的硬編碼字串造成的。事實上,當我們用反彙編程式開啟該勒索軟體時,我們不僅找到了字串“aes”,而且發現,更為完整的字首實際上是“aesT322”:

enter image description here

在這個勒索軟體的反彙編程式碼中,出現了硬編碼的字串“aesT322”,這與我們之前攔截的口令的前七個字元非常吻合。同時,我們還為在之前的逆向工程期間得到的一些函式(“_main”, “_strcat_edx”)和全域性變數(“PasswordPrefix”)進行了命名。

這表明,該口令事實上是由“aesT322”後跟50個左右的隨機字元組合而成的。

在上圖中,部分高亮顯示的指令,就是用來載入指向“aesT322”字串的引用的。我們由此推斷,後面的指令,將載入一個指向儲存字串的全域性變數的引用。我們已經給該變數取名為“PasswordPrefix”,下面我們來定位該變數在記憶體中的具體地址。

enter image description here

該勒索軟體的資料段包含了一些與生成隨機口令有關的全域性變數。就像以前一樣,這裡也對某些變數重新進行命名並加以註釋。

知道了這些變數的地址後,我們就可以利用除錯工具來檢視在實際測試期間,這些變數到底存放了哪些值,具體如下所示:

enter image description here

儘管我們可以輕鬆地透過反彙編程式瀏覽勒索軟體的程式程式碼,但是這樣做的缺點是,這隻能針對磁碟上的靜態程式,或者說是“無生命的”程式。要想操作執行在記憶體中的、活蹦亂跳的勒索軟體程式的話,我們還需藉助於除錯工具。下面,我們就透過它來觀察三個字串變數的取值情況。

正如我們所預料的那樣,名為“passwordprefix”的變數指向字首字串“aest322”的一個副本,名為“passwordrandom”的變數指向由50個隨機字元構成的字串,而名為“passwordfull”的變數則指向由前面兩部分連線而成的一個字串。

然後,利用前面的發現和方法,繼續跟蹤字串“-hp”。透過反彙編程式,我們迅速鎖定了幾個例項,下面便是其中之一:

enter image description here

我們在程式程式碼中發現了字串“-hp”的一個例項,顯然,它肯定位於構建含有口令的命令列並(透過 CreateProcessAShellExecuteA)執行該命令的程式碼中。 到此為止,我們雖然對該勒索軟體有了更多的瞭解,但是仍然不知道是否足以解救受害者。不過有一件事情是肯定的,那就是該口令不能適用於所有受害者。幸運的是,透過反彙編程式我們可以輕鬆找出程式中所有訪問某個變數的程式碼,這樣我們就能追蹤到構造“PasswordFull”的程式碼了:

enter image description here

上面的程式碼,透過連線“aesT322”和隨機字串而構造了“PasswordFull”所指向的字串。

然後,我們利用交叉引用找到了“PasswordRandom”。

enter image description here

這個迴圈(圖中用粗虛線箭頭加以指示)的作用是從由78個字元組成的“字母表”中挑選50個字元組成一個串,透過前面攔截的口令可以看出,它好像是隨機挑選出來的。 我們還發現了一個迴圈,它的迴圈計數是從1到50,這跟口令中隨機部分的那50個字元的長度非常吻合。同時,我們還在這個迴圈內部找到了一個字串,看起來非常像是用來隨機選擇50個字元的“字母表”。在這個字母表中,包含了26個小寫字母,26個大寫字母,10個數字,以及16個標點符號。

接下來,我們需要找出選擇隨機字元的那個函式,當然,上面迴圈內部的指令肯定會呼叫這個函式的。儘管人們普遍認為計算機能夠產生真正的隨機數,但是現實是,它只能帶來偽隨機數。長期以來,人們不斷探索針對偽隨機數生成器的攻擊技術,以便能夠戰勝加密術,這為我們的工作做好了很好的鋪墊。上面,我們標出了一個名為“_get_random_Alphabet_char”的函式,其反彙編程式碼如下所示:

enter image description here

觀察上面的反彙編程式碼不難發現,函式“_get_random_Alphabet_char”好像生成了一個隨機數,並將其儲存到了區域性變數“var_8”中。並且,這段迴圈程式碼會順序遍歷字母表的各個字元,直到抵達某個字元為止。然後,那個字元被轉換成一個單字元字串,其地址被存放到被我們命名為“RandomAlphabetChar”的全域性變數內。

當確定出上面高亮顯示的函式就是PureBasic的隨機數生成函式“Rnd”之後,這些彙編程式碼就立馬變得非常容易閱讀了。這個函式的程式碼如下所示:

enter image description here

隨機數函式“Rnd”的作用是,確保初始化偽隨機數生成器,以生成一個介於0到指定最大值“arg_0”之間的一個數值。當然,這個範圍也包括了它的兩個邊界值。 函式“Rnd”封裝了許多其他函式的呼叫,尤其是我們前面命名的那些函式。這裡的“_internal_Randomize_default”函式,除了呼叫一些Windows函式外,還呼叫了該勒索軟體自身的一個函式,該函式被命名為“_internal_RandomInit”。下面,我們將這些函式並列顯示,具體如下圖所示。

enter image description here

在上圖中,左視窗顯示的是“_internal_Randomize_default”函式的反彙編結果,而右視窗中是我們更為感興趣的“_internal_RandomInit”函式的反彙編結果。 現在,我們終於取得了一個重大突破。在左邊視窗可以看到,PRNG是透過由執行這個程式程式碼的執行緒的標示符和以毫秒計算的系統執行時間共同得到的一個32位數字來初始化或者說指定種子值的,而這兩個數值相對來說都比較容易預測。在右邊的視窗中,我們高亮顯示了一個常量“magic”,這是一個專用的數字,通常用於網路搜尋。在這裡,這個數字好像採用了16進位制的形式,具體為53A9B4FBh,當然,還有其他可能的表示形式,如0x53A9B4FB,或者十進位制表示形式1403630843。它後面的指令,可以轉換為“1 - EAX * 0x53A9B4FB”的形式,這意味著我們所看到的這個常量實際上可能是負數-1403630843,如果看作是無符號數字的話,還可以表示為AC564B05h0xAC564B05或者2891336453。利用上面的這些內容作為搜尋詞進行網路搜尋,我們竟然在PureBasic論壇中找到了與這些隨機數生成程式有關的原始碼,以及相應的反彙編程式碼。

下面所謂“Rnd”函式的反彙編程式碼中明顯的迴圈操作進一步印證了我們的發現,它跟我們從網路上找到的原始碼的反彙編程式碼是一致的。

enter image description here

上面高亮顯示的“ror”助記符表示的是迴圈左移指令,分別表示迴圈左移13位元和5位元。並且,我們從網上找到的原始碼也在類似的上下文中執行迴圈左移操作,甚至移動的位數也是相同的。

好了,見證奇蹟的時刻就要到了。由於它使用了32位的種子值,也就是說,可能的口令至多能有2的32次方種,而不是從78個字元組成的字母表中真正隨機挑選出50個字元的可能組合數。之所以出現這種情況,是因為計算機無法真正實現隨機性。對於任何給定的種子值,PRNG每次用它初始化的時候,都會以完全相同的次序生成完全相同的數值。由於這個隨機數種子值是一個32位的數字,也就是說其取值範圍是從0到2的32次方左右,因此可能的初始狀態也會受到相同的限制。

如果我們能知道被侵害系統在受到攻擊之前已經執行的時間的話,那麼就能極大地縮小種子值中執行時間分量的取值範圍,從而顯著減少需要嘗試的口令的數量。當然,列出所有2的32次方個口令也很麻煩。但是就本例而言,種子值的來源,即執行緒識別符號和系統執行時間卻給我們提供了莫大的幫助。前者是4的倍數,並且通常小於10000,而後者的變化範圍則要更大一些:它以49.7天為週期,取值從0變為2的32次方,之後,又從0開始迴圈往復,步進通常為15或者16。如果我們能獲悉被侵害系統在受到攻擊之前已經執行的時間的話,那麼就能極大地縮小種子值的執行時間分量的取值範圍,從而顯著減少需要嘗試的口令的數量。

0x03 猜解口令


實際上,猜口令並不是很難,問題在於這需要耗費大量的時間。當然,單純利用給定種子值生成一個口令的話,速度是極快的,但是對於我們匆匆拼湊起來的這臺一次性機器來說,我們只進行了一種猜解試驗,即嘗試解密RAR看看能否找出有用的東西來,這種嘗試所需的運算量非常之大,尤其是需要嘗試幾百萬次的時候。正如結果所展示的那樣,雖然沒能找到受害者電腦執行時間方面的線索,但是,卻出乎意料地發現了可能比這個更有用的東西。 首先,在檢查被感染的磁碟的時候,我們注意到,\ProgramData資料夾下面隱藏了許多子目錄,這些目錄名是由隨機字母組合而成的,並且這些子目錄下面還隱藏了許多奇怪的檔案。此外,我們還發現了一個名為\ProgramData\svcfnmainstvestvs\stppthmainfv.dll的文字檔案,其中共21行,每行含有8個隨機字母。隨著進一步的檢查,我們發現每一行內容實際上就是勒索軟體\ProgramData目錄下面的由隨機字母組成的子目錄名或檔名,只不過順序給倒過來了。

enter image description here

檔案\ProgramData\svcfnmainstvestvs\stppthmainfv.dll中的內容,是我們在執行測試過程中得到的。雖然這個檔案的副檔名是.dll,但是這純粹是忽悠人的,它實際上就是一個文字檔案。對於每一行字母,只要把它們倒過來,例如chlqfohk,都對應於一個子目錄或者檔案的隨機名稱,這些都是勒索軟體在\ProgramData目錄下面建立的。

當然,這些資料的價值不在於我們需要這些名稱,而在於我們知道了它是PRNG在感染後輸出的。在實際發生感染的時候,例如本次面臨的這個案例,我們當然無法像測試執行這樣獲得相應口令,但是仍然有機會找到stppthmainfv.dll或者根據被感染驅動器的\ProgramData目錄中的內容來重構它。利用這些資料,我們就能夠暴力破解所有可能的種子值,最多需要2的32次方次,就能找出PRNG生成這些隨機名稱時所對應的種子值了。對於目前的計算機來說,這種暴力搜尋可能需要幾個小時才能完成,但是相對於利用RAR工具逐個嘗試推測的口令來說,速度要快上好幾個數量級了。

這裡有幾點需要注意。首先,就像在隨機函式中識別出的執行緒本地儲存(TLS)呼叫所暗示的那樣,每個執行緒都有自己的PRNG狀態,並且在第一次被“Rnd”呼叫時,都會單獨進行初始化。碰巧的是,那八個隨機字母構成的名稱是由勒索軟體程式的主執行緒生成的,而密碼是由分執行緒決定的。下面是生成21個名稱的一段迴圈程式碼;標為綠色的程式碼交叉引用(“CODE XREF”)註釋表明,該程式碼駐留在程式的“start”函式中,並且該函式是該程式第一個執行的執行緒。

enter image description here

在勒索軟體的“start”函式中的這段迴圈程式碼,將建立21個隨機的檔名和子目錄名。

enter image description here

這是上面提到過的那個從A到Z的字母表中隨機選擇8個字母函式中的迴圈程式碼段。正如所料,該函式會針對每個字元都呼叫一次“Rnd”。

透過跟蹤生成口令的程式碼,我們找到了一個標記為“sub_406582”的函式,它會被我們命名為“_ServiceMain”的函式呼叫,當勒索軟體作為一個Windows服務執行的時候,這個函式將在一個單獨的執行緒中執行。也就是說,我們關心的這兩個程式碼段在執行時會使用不同的PRNG狀態,每個狀態都是由不同的隨機數的種子值來產生的。透過暴力破解種子值,我們可以得到隨機的檔名或子目錄名,但是卻無法直接得到口令的種子值,儘管如此,我們已經離成功越來越近了,這主要得益於口令種子值構成部分的簡單性和可預測性。換句話說,種子值的系統執行時間部分和執行緒識別符號部分還是有很大的不同的,因為執行緒可以在不同的時刻啟動,並且如果多個執行緒同時執行的話,必須使用不同的ID,但是它們不會相差太大。因此,要是手頭上有了第一個種子值,我們就能適當地將第二個種子值的取值範圍縮小到幾十萬個之內。

第二個注意事項是,我們已經知道了字母的次序,但是這一點很容易就會被忽視。另外,從技術上講PRNG會發出一個數字序列。就本例而言,就像前面的快照所顯示的那樣,數字0到25表示的就是字母A到Z,所以很明顯,這個字母表就是用來將字母對映到一個數字的。然而,在其他情況下,這些字母可能會出現遺漏、重複、重新排序,或者穿插了stppthmainfv.dll中所見之外的字元。無論出現上述哪一種情況,都意味著我們需要花費許多小時來破除這些阻止我們暴力破解的障礙。

第三個需要注意的事項是,我們無法確定PRNG的種子值初始化之後,該勒索軟體是否立即生成了這些名稱或口令,也就是說,這中間否有什麼岔子。如果其他程式碼先於這兩個執行緒中的某一個,或先於這兩個執行緒之前就已經呼叫過了Rnd的話,則意味著PRNG的狀態已經不是我們所關心的隨機數生成程式碼給它提供種子值時的原始狀態了。我們需要弄清,“Rnd”被我們感興趣的那個執行緒呼叫之前,已經被其他執行緒呼叫了多少次,同時,還要去掉在生成我們自己推測性的名稱和口令之前的那些隨機數。

因此,我們需要找出針對“Rnd”的各個呼叫。如果將反彙編程式碼稍微上滾一點,就會看到“start”函式中針對“Rnd”的唯一的一個呼叫,具體如下所示:

enter image description here

這是該勒索軟體對“Rnd”的第一次呼叫,並且是生成檔案和子目錄名稱的迴圈程式碼之前的唯一的一次呼叫。

另一方面,執行口令生成程式碼的“_ServiceMain”執行緒對“Rnd”總共呼叫了3次,並且都是在它使用PRNG構造口令之前呼叫的,具體見下圖。

enter image description here

這三個針對“Rnd”的呼叫,都先於“_ServiceMain”執行緒中生成口令的那些程式碼。

因為存在這三個呼叫,因此,當我們準備開始生成候選密碼的時候,每次初始化好隨機數種子值後,最好放棄前三個隨機數。

現在,我們終於要開始暴力破解了。對於尋找名稱的種子值的程式碼,如下所示。需要注意的是,為了簡潔起見,我們這裡省略了實現PRNG的PureBasic程式碼。

#!c
for (unsigned int seed = 0; ; seed++)
{
    // seed the PRNG with a possible value
    RandomInit(seed);

    // discard one random number
    Rnd();

    size_t i;
    for (i = 0; i < 21 * 8; i++)
    {
        // generate the next random letter
        // Rnd(25): from 0 to 25 inclusive
        char ch = "abcdefghijklmnopqrstuvwxyz"[Rnd(25)];

        // does this letter match the next in the sequence?
        if (ch != "chlqfohkayfwicdd...dszeljdp"[i])
            break;
    }

    // did we complete the entire sequence?
    if (i == 21 * 8)
    {
        // yes, display the result and finish
        printf("Names seed = %u\n", seed);
        break;
    }

    // no, try the next seed value
}

在一個單核機器上面,我們使用了大約4秒鐘的時間,測試了31956209種可能的值,並發現最後一個種子值即31956208生成的字母序列與“stppthmainfv.dll”找到的完全一致。這個數字說明,我們之前的觀點是正確的。

儘管我們這個將這些資訊轉化為結果的系統算不上優雅,但作為原型卻是行之有效的。即使我們靠硬猜的話,需要嘗試的用來生成口令的種子值不會超過32768或者180000(以毫秒為單位的3分鐘時間),就能找出前面恢復出來的名稱對應的種子值。因此,我們可以根據這個範圍內的種子值來生成一個大約包含200000個口令的清單,程式碼如下所示:

#!c
// this is the names seed value we brute-forced earlier
unsigned int namesseed = 31956208;

// loop through a range of seed values around the determined names seed value
for (unsigned int seed = namesseed - 32768; seed <= namesseed + 180000; seed++)
{
    // seed the PRNG with a candidate password seed value
    RandomInit(seed);

    // discard three random numbers
    Rnd();
    Rnd();
    Rnd();

    char pwrandom[51];
    pwrandom[50] = '\0';

    // generate the fifty-char random portion of the password corresponding to
    // the candidate seed value, using the alphabet extracted from the ransomware
    // Rnd(77): from 0 to 77 inclusive (this alphabet contains 78 characters)
    for (size_t i = 0; i < 50; i++)
      pwrandom[i] = "abc...xyzABC...XYZ0123456789!@#$%^&*&*()-+_="[Rnd(77)];;

    // output the full password; this output can be captured to compile a list
    printf("aesT322%s\n", pwrandom);
}

執行上面的程式碼,將執行結果重定向到一個文字檔案中。這個存放待測試的口令的文字檔案的大小隻有12MB左右,但是,如果不縮小口令種子值取值範圍的話,則需要測試的口令有2的32次方個,那麼存放它們的文字檔案的大小就會陡升至236GB左右。這樣一來,我們就可以編寫一個批處理檔案,讓它執行7-Zip,遍歷檔案中的口令來解壓經過加密的RAR自解包程式。對於7-Zip來說,有時候即使已經正確解密了,它也會謊報軍情,為此,我們需要使用find命令在其輸出中搜尋只有檔案被正確解密才可能出現的東西。我們使用的批處理檔案如下所示:

#!c
@for /f %%p in (pwlist.txt) do @(
 7z.exe l "testtargetfile_0123456789abcdef.docx(!! to decrypt email id ... to ... !!).exe" "-p%%p" 2>&1 |find ".."
 if NOT ERRORLEVEL 1 (echo "%%p")
)

我們讓這個批處理檔案通宵執行,在第二天早晨終於找到了期盼已久的東西:正確的口令。至此,我們大功告成了! 我們沒有辜負受害者對我們的信任,最終將其資料恢復如初。

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章