我们在2009年向 Conflicker.C 申请制造”疫苗”,目的就是保护工作站和服务器免受病毒肆虐的侵害。让我们来看看同样的办法能否够应用于Locky勒索软件?
接下来,我们会在不与用户进行交互的情况下将”疫苗”应用于任何旨在阻止相关功能的细微系统改造中,这些改造是为了阻止因恶意程序的执行而引发的负面影响。显而易见的是,”疫苗”的使用必须是在受到病毒感染之前。
这种细微的修改可能是一个具体的互斥体和注册表的创建,或是简单地系统参数的修正,反正这些都不会给用户造成任何不便。下面是一个例子,在其执行的初始阶段,Locky锁定了系统语言,但是没有感染俄语的相关配置:
因此,如果设置系统语言为俄语,那么该系统就不会被感染,而一般情况下很少有人会更改这个设置。
在检查系统语言之后,Locky会试图创建 HKCU/Software/Locky 注册表;一旦失败,Locky便会立刻停止。
在Locky之前使用ACL创建此密钥可以阻止任何人进入,这样该系统就完成“疫苗”的接种了。
之后Locky会检查HKCU/Software/Locky 密钥,然后寻找ID(被感染的识别码)、公共密钥(从服务器提取的密钥,之后会详细解释)、支付文本(以指定语言呈现给用户的文本)和完整的注册表值。最后的注册表值会显示加密过程的最后一步。Locky会进行验证,如果是完整的注册表值,那就会标示为1;如果ID值中还是包含正确的系统识别符,那它会停止运行:
标示符生成算法其实很简单,在我们的测试中得到这样的结果:
1、GetWindowsDirectoryA() : C:/WINDOWS
2.GetVolumeNameForVolumeMountPointA(C:/WINDOWS) : //?/Volume{ b17db400-ae8a-11de-9cee-806d6172696f}
3.md5({b17db400-ae8a-11de-9cee-806d6172696f}) : 1d9076e6fd853ab665d25de4330fee06
4.转换为大写ASCII并截取前16位字符: 1D9076E6FD853AB6
创建这两个注册表,其中一个是与系统有关的,通过加密阻止Locky:
在加密文件之前,Locky用下面的数据向C&C发送一个HTTP POST请求:
(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⟨=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 Hash值。数据会在发送之前进行简单编码的:
解码响应数据是通过这种类似的算法:
这两种算法可以通过几行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
编码之后的数据如下:
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 Hash值。从offet Ox10中,我们可以发现一个BLOB_HEADER结构:
•type 0x06 = PUBLICKEYBLOB
• 0x02版本
•2 个保留字节
•ALG_ID 0xa400 = CALG_RSA_KEYX
这是一个公共RSA密钥,所以下列字节是一种RSAPUBKEY结构:
•magic RSA1 = public key
•密钥大小: 0x800 = 2048 bits
•0x10001指数 = 65537
•弹性模型 2160…94ba
正如前文提及,如果该值存在但是包含无效值,文件也没有重命名或是被Locky加密,这种结构(不包括MD5 Hash值)是存储在公共密钥值里的。下面的截图显示使用公共密钥的测试机包含一个单独的null字节。Locky已经在计算中执行了,但是桌面中还有一个 monfichier.txt (“myfile.txt“) 文件没有被锁定,虽然其扩展名是被锁定的:
此外,如果公共密钥值中包含一个RSA的1024密钥,文件将会被重命名但是不会被加密(ceci est un secret在法语中的意思是“这是一个秘密”):
Locky存在另一个有趣的设计漏洞:如果公共密钥已经存在,在加密文件时它不会进行任何验证。这就允许我们将RSA公共密钥掌握在自己手里,因此我们也就会拥有相应的私人密钥。
Locky在其公共密钥值中使用下面的C语言代码生成了 BLOB_HEADER 格式的RSA2048密钥对:
#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);
在几行Python代码之后,我们有一个.reg的文件,这样就可以强制插入使用在我们控制之下的公共密钥的注册表。
让我们更加具体的了解一下文件到底是如何被加密的。在通过 CryptAcquireContext() 获得 PROV_RSA_AES CSP (加密服务提供商)权限之后,Locky引用了包含公共密钥的 CryptImportKey() ,存储在公共密钥注册表值中。它使用 CryptGenRandom() 功能将目标文件重命名到 <id><16 random bytes>.locky 。然后随机生成16个字节:
(gdb) hexdump 0x009ef8a0 16 9d 86 d3 42 48 3a 45 04 1a cb 95 1c 77 90 8f 7c
这些字节是在由 CryptImportKey() 导入 BLOB_HEADER 结构之后才被复制的:
(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
•type 0x08 = PLAINTEXTKEYBLOB
•0x02版本
•2个保留字节
•ALG_ID = 0x660e = CALG_AES_128
•密钥大小 = 0x10 字节
所以这是一个AES-128密钥。之后再使用 GetSetKeyParam() 功能获取更多密钥使用信息:
(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密钥。包含的是下列结果:
(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用0建立了一个0×800字节的缓冲区,只是在每行的末尾有增加的计数:
(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 加密,使用的就是上面的这种密钥,还包含了下列缓冲区:
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预设了一个0×230大小、全是0的缓冲区,并在使用K缓冲区(作为一个密钥流)XOR之前拷贝文件名。举个例子,从0到3字节:
(gdb) p/x $edx // key stream $3 = 0x90bbd3fc (gdb) hexdump $edi 64 //before 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 //after 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字节:
(gdb) p/x $eax // key stream $4 = 0x6e1e1eac (gdb) hexdump $edi 64 //after 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 |................|
以此类推。结果缓冲区都在使用RSA加密的AES密钥之后被复制在.locky文件中。
Locky会以同样的方式加密文件内容,不过会从偏移量0×230(在我们的案例中是0x009267c8 )使用密钥流。这不太可能是随机的选择,因为Locky开发者已经为这两个元素使用了同样的密钥流,解密文件内容很容易。事实上,预留给文件名的 0×230字节几乎都是0,这些字节都经过XOR运算,其结果可以在.locky文件中找到。这就为我们提供了其密钥流的大部分数据,尽管我们不知道对应的AES密钥。
文件的内容都是从偏移量0×230的密钥流中经过XOR运算的:
(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..............|
完整的XOR运算结果被拷贝在 .locky 文件的开头。
如果文件的大小大于除湿量缓冲区的大小,那么Locky会使用同样的方式借 CryptEncrypt() 加密其他布局,只是增加计数量:
(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文件就是下面呈现的这样:
解密一个.locky文件需要以下步骤:
1、用我们的私人密钥破译RSA缓冲区以获得16字节
2、使用这些字节作为AES-128-ECB密钥来加密增量和恒定的缓冲区。
3、使用结果中最开始的 0×230字节作为密钥流来进行XOR运算文件名。
4、将偏移量 0×230 字节作为密钥流来对其内容进行XOR运算。
下面的C语言代码就是第一个步骤:
// 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密钥:
$ xxd aeskey.raw 9d86 d342 483a 4504 1acb 951c 7790 8f7c
下面的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
让我们来看一下它的工作原理:
$ ./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密钥加密的RSA公共密钥。
就密码学而言,上文的详细分析表明文件不会被AES-128-EBS直接加密。Locky建立了增量的缓冲区,然后使用AES-128-EBS对其进行加密,将结果用作两个独立的密钥流来对文件名和文件内容进行XOR运算,这看起来更像是CTR运算。
*原文: lexsi ,FB小编极客小默编译,转自须注明来自FreeBuf黑客与极客(FreeBuf.COM)