From:https://www.lexsi.com/securityhub/abusing-bugs-in-the-locky-ransomware-to-create-a-vaccine/?lang=en
在2009年,我們利用免疫系統的概念來保護工作站或者服務器免受快速傳播的蠕蟲病毒Conficker的入侵。讓我們來看一下是否能把這一防御概念用在勒索軟件Locky上。
下面我們將對系統進行一些小的修改,來打造一個免疫系統,在不需要用戶交互的情況下,部分或者完全地阻止惡意程序在執行的時候對系統造成的危害。很明顯,我們必須要讓“疾病”到來之前使免疫系統取得控制權.......
這些小的改動可以是一個特別的互斥體或者一個注冊表鍵的創建或者一個簡單的系統參數的修改,但是這些改動不能對用戶造成任何不便。這里有一個反例,Locky在開始執行的時候回檢查系統語言,它不會感染系統語言是俄語的系統:
因此,把系統語言改為俄語可以免受其感染,但是這樣會給很多人造成使用上的不方便。
在檢查完語言后,Locky會嘗試創建注冊表項HKCU\Software\Locky?,如果創建失敗,它就會立即終止。
當這個鍵被創建以前我們利用ACLs來阻止任何人打開這個鍵,這樣系統就得到了免疫。
Locky會檢查注冊表項HKCU\Software\Locky的鍵,查找ID(被感染機器系統標識),pubkey(服務器上的公鑰,后面詳述), paytext(以它指定的語言格式,顯示給用戶的文字) 和completed值。最后一項的值代表著對用戶系統的加密過程是否結束。按照Locky的定義:如果completed被設置為1,同時id值為正確的系統標識符,那么它將停止執行。
這個標識符生成的算法比較簡單,我們在測試機器上產生的結果如下:
\\?\Volume{ b17db400-ae8a-11de-9cee-806d6172696f}
創建這樣兩個鍵值,其中id的值隨著系統不同而不同,這樣就能阻止Locky加密系統文件。
在加密文件前,Locky會發送一個HTTP POST請求到它的C&C服務器,發送明文如下:
#!bash
(gdb) hexdump 0x923770 0x65
88 09 0c da 46 fd 2c de 1d e8 e4 45 89 18 ae 46 |....F.,....E...F|
69 64 3d 31 44 39 30 37 36 45 36 46 44 38 35 33 |id=1D9076E6FD853|
41 42 36 26 61 63 74 3d 67 65 74 6b 65 79 26 61 |AB6&act=getkey&a|
66 66 69 64 3d 33 26 6c 61 6e 67 3d 66 72 26 63 |ffid=3&lang=fr&c|
6f 72 70 3d 30 26 73 65 72 76 3d 30 26 6f 73 3d |orp=0&serv=0&os=|
57 69 6e 64 6f 77 73 2b 58 50 26 73 70 3d 32 26 |Windows+XP&sp=2&|
78 36 34 3d 30 |x64=0
第一行是后面字符串的MD5值,這個數據在發送前會進行簡單的編碼:
用類似的算法可以解碼返回數據:
這兩個算法可以用幾行Python代碼實現:
#!python
def encode(buff):
buff = md5(buff).digest() + buff
out = ""
key = 0xcd43ef19
for index in range(len(buff)):
ebx = ord(buff[index])
ecx = (ror(key, 5) - rol(index, 0x0d)) ^ ebx
out += chr(ecx & 0xff)
edx = (rol(ebx, index & 0x1f) + ror(key, 1)) & 0xffffffff
ecx = (ror(index, 0x17) + 0x53702f68) & 0xffffffff
key = edx ^ ecx
return out
def decode(buff):
out = ""
key = 0xaff49754
for index in range(len(buff)):
eax = (ord(buff[index]) - index - rol(key, 3)) & 0xff
out += chr(eax)
key += ((ror(eax, 0xb) ^ rol(key, 5) ^ index) + 0xb834f2d1) & 0xffffffff
return out
解碼后數據如下:
#!bash
00000000: 3af6 b4e2 83b1 6405 0758 854f b971 a80a :.....d..X.O.q..
00000010: 0602 0000 00a4 0000 5253 4131 0008 0000 ........RSA1....
00000020: 0100 0100 2160 3262 90cb 7be6 9b94 d54a ....!`2b..{....J
00000030: 45e0 b6c3 f624 1ec5 3f28 7d06 c868 ca45 E....$..?(}..h.E
00000040: c374 250f 9ed9 91d3 3bd2 b20f b843 f9a3 .t%.....;....C..
00000050: 1150 5af5 4478 4e90 0af9 1e89 66d2 9860 .PZ.DxN.....f..`
00000060: 4b60 a289 1a16 c258 3754 5be6 7ae3 a75a K`.....X7T[.z..Z
00000070: 0be4 0783 9f18 46e4 80f7 8195 be65 078e ......F......e..
00000080: de62 3793 2fa6 cead d661 e7e4 2b40 c92b .b7./....a..+@.+
00000090: 23c9 4ab3 c3aa b560 2258 849c b9fc b1a7 #.J....`"X......
000000a0: b03f d9b1 e5ee 278c bf75 040b 5f48 9501 .?....'..u.._H..
000000b0: 80f6 0cbf 2bb4 04eb a4b5 7e8d 30ad f4d4 ....+.....~.0...
000000c0: 70ba f8fb ddae 7270 9103 d385 359a 5a91 p.....rp....5.Z.
000000d0: 4995 9996 3620 3a12 168e f113 1753 d18b I...6 :......S..
000000e0: fdac 1eed 25a1 fa5c 0d54 6d9c dcbd 9cb7 ....%..\.Tm.....
000000f0: 4b8e 1228 8b70 be13 2bfd face f91a 8481 K..(.p..+.......
00000100: dc33 185e b181 8b0f ccbd f89d 67d3 afa8 .3.^........g...
00000110: c680 17d8 0100 6438 4eba a7b7 04b1 d00f ......d8N.......
00000120: c4fc 94ba ....
前16個字節是后面字節的MD5值,從偏移0x10處開始我們可以發現一個BLOB_HEADER結構:
這是一個RSA公鑰,因此下面是RSAPUBKEY結構:
這個結構(除去MD5 hash部分)保存在上面提到的pubkey鍵值里,如果這個值存在,并且是一個錯誤的值,那么系統里的文件既不會被改名,也不會被加密。下圖中,把pubkey用一個NULL字節填充,然后在測試機器上運行Locky,盡管Locky對txt格式的文件具有針對性,但是這時候桌面上的文件monfichier.txt 并沒有發生變化。
另外如果pubkey是一個RSA 1024密鑰,文件將會被改名,但是不會被加密(“ceci est un secret”,法語:“這是一個秘密”):
Locky有另外一個設計漏洞:如果pubkey的值存在,Locky用pubkey加密文件的時候不做任何驗證,允許我們強制使用我們自己掌控的一個公鑰來加密文件,同時我們又知道這個公鑰相對應的私鑰。
下面的C代碼將生產一個BLOB_HEADER 格式的RSA 2048 密鑰對,用來作為pubkey的值:
#!cpp
#define RSA2048BIT_KEY 0x8000000
CryptAcquireContext(&hCryptProv, "LEXSI", NULL, PROV_RSA_FULL, 0);
CryptGenKey(hCryptProv, AT_KEYEXCHANGE, RSA2048BIT_KEY|CRYPT_EXPORTABLE, &hKey);
// Public key
CryptExportKey(hKey, NULL, PUBLICKEYBLOB, 0, NULL, &dwPublicKeyLen);
pbPublicKey = (BYTE *)malloc(dwPublicKeyLen);
CryptExportKey(hKey, NULL, PUBLICKEYBLOB, 0, pbPublicKey, &dwPublicKeyLen);
hPublicKeyFile = CreateFile("public.key", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
WriteFile(hPublicKeyFile, (LPCVOID)pbPublicKey, dwPublicKeyLen, &lpNumberOfBytesWritten, NULL);
// Private key
CryptExportKey(hKey, NULL, PRIVATEKEYBLOB, 0, NULL, &dwPrivateKeyLen);
pbPrivateKey = (BYTE *)malloc(dwPrivateKeyLen);
CryptExportKey(hKey, NULL, PRIVATEKEYBLOB, 0, pbPrivateKey, &dwPrivateKeyLen);
hPrivateKeyFile = CreateFile("private.key", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
WriteFile(hPrivateKeyFile, (LPCVOID)pbPrivateKey, dwPrivateKeyLen, &lpNumberOfBytesWritten, NULL);
通過上述代碼,我們生成一個.reg文件,導入注冊表后,我們就可以強制Locky用我們設置的RSA公鑰。
讓我們仔細看看Locky是怎么加密文件的。當通過CryptAcquireContext()函數獲得一個指向PROV_RSA_AES結構的CSP(密碼庫)句柄后,通過CryptImportKey()函數導入pubkey鍵值里包含有公鑰的二進制數據,然后它把文件按<id><16個隨機字符>.locky
格式改名。它通過CryptGenRandom()函數來產生16個隨機字符:
#!bash
(gdb) hexdump 0x009ef8a0 16
9d 86 d3 42 48 3a 45 04 1a cb 95 1c 77 90 8f 7c
這些字節將會被復制到由CryptImportKey()函數導入的BLOB_HEADER?結構的后面。
#!bash
(gdb) hexdump 0x009ef784 0x1c
08 02 00 00 0e 66 00 00 10 00 00 00 9d 86 d3 42
48 3a 45 04 1a cb 95 1c 77 90 8f 7c
這是一個AES-128 密鑰,然后GetSetKeyParam()函數會被調用,對其下斷,這里我們可以獲得更多關于這個密鑰是如何被使用的信息:
#!bash
(gdb) x/w $esp+4
0x9ef830: 0x00000004
(gdb) x/w *(int*)($esp+4+4)
0x9ef858: 0x00000002
內存值4在第二個參數里,代表?KP_MODE ,可以允許自定義操作模式;內存值2在第三個參數里,代表CRYPT_MODE_ECB。CryptEncrypt()函數用RSA公鑰來加密AES的密鑰,獲取到的結果如下:
#!bash
(gdb) hexdump 0x9ef8a0 256
64 ab 20 75 75 56 ae f4 af 20 7f 38 81 d7 d6 56 |d. uuV... .8...V|
22 89 92 6e 30 e0 61 d2 24 f0 a1 d6 2a 20 7f 6c |"..n0.a.$...* .l|
e0 10 cc ab 26 62 33 66 71 8d 93 4c 04 61 8a 9a |....&b3fq..L.a..|
86 e7 f4 75 58 ae 8a 68 96 1f a8 69 15 aa 2f e7 |...uX..h...i../.|
8b cd ca 2e b0 7b e1 89 5f 3e 65 61 4c 0b 43 5e |.....{.._>eaL.C^|
60 3b 17 48 0e d2 08 80 bd 4d e2 38 5b 51 c9 82 |`;.H.....M.8[Q..|
26 bf 94 8a 45 40 82 62 1e 88 42 aa 35 2a 3e 58 |&...E@.b..B.5*>X|
d2 7d 03 4d cd d4 e6 3b 7d 44 e9 5f dc 4d 1c 4b |.}.M...;}D._.M.K|
27 a9 39 0c 74 ed 46 97 60 af 3a 97 3f 89 33 28 |'.9.t.F.`.:.?.3(|
bf 27 67 57 f8 c5 4e 03 72 45 60 88 03 e5 11 98 |.'gW..N.rE`.....|
6f 49 af 92 72 69 db ec b7 c7 51 9a 05 f2 34 e0 |oI..ri....Q...4.|
17 e4 1b 7e c5 97 ff 3d 42 5d ff a5 69 a4 58 f8 |...~...=B]..i.X.|
3b bd 9f 84 6e a5 c7 81 4e 0e aa 5d 40 ff 06 01 |;...n...N..]@...|
e9 ee 3c e5 0f b2 b4 80 af 56 c5 b8 25 af 11 2e |..<......V..%...|
22 82 c1 f1 93 50 b2 a4 76 98 46 2e db 6c 76 bb |"....P..v.F..lv.|
b5 1e 70 44 41 e2 15 31 f9 02 7d 92 7a e5 73 17 |..pDA..1..}.z.s.|
這個數據和.locky文件里的一致。
然后Locky產生一個0x800字節用0填充的緩沖空間,并且每行結尾用一字節計數:
#!bash
(gdb) hexdump 0x00926598 0x800
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 |................|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 |................|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 |................|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 |................|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 05 |................|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 06 |................|
[...]
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7a |...............z|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7b |...............{|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7c |...............||
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7d |...............}|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7e |...............~|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7f |................|
然后CryptEncrypt()函數會使用上面產生的密鑰來對這些字節進行AES-128-ECB加密,然后得到如下數據(它是用來進行異或加密的K):
#!bash
0x00926598: fc d3 bb 90 ac 1e 1e 6e 76 88 09 52 66 76 71 fc
0x009265a8: d5 e2 07 fd a5 0c 02 50 d0 83 4e 9b 95 1c 0b 60
0x009265b8: 3f c5 49 e5 df b2 05 56 bd ce eb f6 0d 70 9f 62
0x009265c8: 98 f1 e8 b7 e2 8e d8 97 7f a1 83 14 2b db 82 98
0x009265d8: 5b 4a 94 f7 fb 60 81 cd bb c7 a2 33 60 b1 c0 c7
0x009265e8: 1c c5 c7 40 af 7c ea 4b e2 74 b0 32 c2 37 5e fa
0x009265f8: cf 40 69 9b 81 92 b8 f1 77 79 83 97 32 19 75 a6
[...]
0x009267c8: 96 9a 1d bd 9b 03 33 2f d5 e7 a7 fc ac fc 09 c9
0x009267d8: f6 bd c5 73 ce 9e ce bc fd e4 ef 6f 06 dd 7d 15
0x009267e8: 7d 95 e6 18 78 87 46 ba 75 5e 58 2e f8 ba 5c 14
0x009267f8: 3d a9 f3 d3 af ef 0b 39 00 ae 0c 32 2b fd 37 eb
0x00926808: 3f 3a 68 11 b8 d1 ae e7 28 40 0a 20 33 31 8f 7e
0x00926818: c3 8f 55 2a 5f b5 31 26 02 41 d7 e3 84 c5 79 9b
[...]
第一個要加密的元素就是文件名。Locky先分配0x230字節空間,用0填充,
先把文件名復制進緩沖區,然后再與K進行異或加密。例如,如下我們觀察前3字節:
#!bash
(gdb) p/x $edx // 加密KEY
$3 = 0x90bbd3fc
(gdb) hexdump $edi 64 //XOR 加密前
2a a1 1b d4 6d 00 6f 00 6e 00 66 00 69 00 63 00 |*...m.o.n.f.i.c.|
68 00 69 00 65 00 72 00 2e 00 74 00 78 00 74 00 |h.i.e.r...t.x.t.|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
(gdb) hexdump $edi 64 //XOR 加密后 #1
d6 72 a0 44 6d 00 6f 00 6e 00 66 00 69 00 63 00 |.r.Dm.o.n.f.i.c.|
68 00 69 00 65 00 72 00 2e 00 74 00 78 00 74 00 |h.i.e.r...t.x.t.|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
再看4-7字節
#!bash
(gdb) p/x $eax // key
$4 = 0x6e1e1eac
(gdb) hexdump $edi 64 // XOR 加密后#2
d6 72 a0 44 c1 1e 71 6e 6e 00 66 00 69 00 63 00 |.r.D..qnn.f.i.c.|
68 00 69 00 65 00 72 00 2e 00 74 00 78 00 74 00 |h.i.e.r...t.x.t.|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
以此類推,加密結果與加密后的.locky文件一致。
然后Locky以同樣的方式來加密文件內容,但是KEY 從偏移0x230處(本場景中在0x009267c8處)開始。這不可能是一個隨機的選擇,因為如果Locky的開發者采用相同KEY來加密,那么文件內容解密將會是非常順利成章的。實際上,保存文件名的0x230字節幾乎全是0,對這些字節進行異或操作后,在.locky文件里可以看到結果,這樣在不知道AES相應的密鑰的情況下,我們可以分析出相當大比例的KEY。
文件內容加密如下:
#!bash
(gdb) p/x $edx
$5 = 0xbd1d9a96 // key stream from offset 0x230 (0x009267c8)
(gdb) hexdump $edi 64 //before XOR
63 65 63 69 20 65 73 74 20 75 6e 20 73 65 63 72 |ceci est un secr|
65 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |et..............|
(gdb) hexdump $edi 64 //after XOR #1
f5 ff 7e d4 20 65 73 74 20 75 6e 20 73 65 63 72 |..~. est un secr|
65 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |et..............|
這里和.locky文件開頭部分一致。
如果文件大小大于初始化時候的緩沖區,Locky將通過CryptEncrypt()函數以同樣的布局加密其余的緩沖空間,并增加計數:
#!bash
(gdb) hexdump *(int*)($esp+4+4+4+4) 128
0x009255e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 80
0x009255f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 81
0x00925600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 82
[...]
(gdb) hexdump *(int*)($esp+4+4+4+4) 128
0x009255e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00
0x009255f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 01
0x00925600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 02
[...]
最終一個.Locky文件布局如下:
可以按以下步驟來解密:
第一步用C代碼實現如下:
#!cpp
// Importing the RSA private key
hPrivateKeyFile = CreateFile("private.key", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
dwPrivateKeyLen = GetFileSize(hPrivateKeyFile, NULL);
pbPrivateKey = (BYTE *)malloc(dwPrivateKeyLen);
ReadFile(hPrivateKeyFile, pbPrivateKey, dwPrivateKeyLen, &dwPrivateKeyLen, NULL);
CryptImportKey(hCryptProv, pbPrivateKey, dwPrivateKeyLen, 0, 0, &hKey);
// Reading the RSA buffer
hEncryptedFile = CreateFile("encrypted.rsa", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
dwEncryptedDataLen = GetFileSize(hEncryptedFile, NULL);
pbEncryptedFile = (BYTE *)malloc(dwEncryptedDataLen);
ReadFile(hEncryptedFile, pbEncryptedFile, dwEncryptedDataLen, &dwEncryptedDataLen, NULL);
// Decrypting the AES key
CryptDecrypt(hKey, NULL, TRUE, 0, pbEncryptedFile, &dwEncryptedDataLen);
hClearFile = CreateFile("aeskey.raw", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
WriteFile(hClearFile, (LPCVOID)pbEncryptedFile, dwEncryptedDataLen, &lpNumberOfBytesWritten, NULL);
獲取到AES密鑰:
#!bash
$ xxd aeskey.raw
9d86 d342 483a 4504 1acb 951c 7790 8f7c
通過上面的密鑰,用Python代碼實現解密文件名和文件內容如下:
#!python
#! /usr/bin/env python
from Crypto.Cipher import AES
print "UnLocky - Locky decryption tool, CERT-LEXSI 2016
key = "9d86d342483a45041acb951c77908f7c".decode("hex")
# NB: small files only here
counter = ""
for i in range(0x80):
counter += "\x00"*15 + chr(i)
keystream = AES.new(key, AES.MODE_ECB).encrypt(counter)
data = open("1D9076E6FD853AB6C931AFE2B33C3AF9.locky").read()
enc_size = len(data) - 0x230 - 0x100 - 0x14
enc_filename = data[-0x230:]
enc_content = data[:enc_size]
clear_filename = ""
for i in range(0x230):
clear_filename += chr(ord(enc_filename[i]) ^ ord(keystream[i]))
print "[+] File name:"
print clear_filename
clear_content = ""
for i in range(enc_size):
clear_content += chr(ord(enc_content[i]) ^ ord(keystream[0x230+i]))
print "[+] Content:"
print clear_content
我們看看它是否能解密文件:
#!bash
$ ./unlocky.py
UnLocky - Locky decryption tool, CERT-LEXSI 2016
[+] File name:
monfichier.txt // "myfile.txt"
[+] Content:
ceci est un secret // "this is a secret"
Locky已經肆虐很久了,這里提供了一些簡單的方法,在不需要任何反病毒或者安全工具的前提下,可以防止用戶系統文件被加密,并保護系統為原樣。比如,可以使用本文提到的4個“疫苗”中的一個“疫苗”:強制替換用來加密AES KEY的RSA公鑰。
通過上面的分析,Locky并不是直接通過AES-128-ECB來加密文件,而是通過AES-128-ECB來加密一個增量緩沖區,并把結果分為兩部分,分別作為異或加密文件名和文件內容的KEY,看上去和CTR非常類似。