利用勒索软件Locky的漏洞来免疫系统

0x00 简介


在2009年,我们利用免疫系统的概念来保护工作站或者服务器免受快速传播的蠕虫病毒Conficker的入侵。让我们来看一下是否能把这一防御概念用在勒索软件Locky上。

下面我们将对系统进行一些小的修改,来打造一个免疫系统,在不需要用户交互的情况下,部分或者完全地阻止恶意程序在执行的时候对系统造成的危害。很明显,我们必须要让“疾病”到来之前使免疫系统取得控制权…….

这些小的改动可以是一个特别的互斥体或者一个注册表键的创建或者一个简单的系统参数的修改,但是这些改动不能对用户造成任何不便。这里有一个反例,Locky在开始执行的时候回检查系统语言,它不会感染系统语言是俄语的系统:

p1

因此,把系统语言改为俄语可以免受其感染,但是这样会给很多人造成使用上的不方便。

0x01 利用访问控制列表ACL 来阻止Locky注册表项的创建


在检查完语言后,Locky会尝试创建注册表项HKCUSoftwareLocky ,如果创建失败,它就会立即终止。

p2

当这个键被创建以前我们利用ACLs来阻止任何人打开这个键,这样系统就得到了免疫。

p3

0x02 注册表键completed 的值


Locky会检查注册表项HKCUSoftwareLocky的键,查找ID(被感染机器系统标识),pubkey(服务器上的公钥,后面详述), paytext(以它指定的语言格式,显示给用户的文字) 和completed值。最后一项的值代表着对用户系统的加密过程是否结束。按照Locky的定义:如果completed被设置为1,同时id值为正确的系统标识符,那么它将停止执行。

p4

这个标识符生成的算法比较简单,我们在测试机器上产生的结果如下:

  1. GetWindowsDirectoryA() : C:WINDOWS
  2. GetVolumeNameForVolumeMountPointA(C:WINDOWS) : \?Volume{ b17db400-ae8a-11de-9cee-806d6172696f}
  3. md5({b17db400-ae8a-11de-9cee-806d6172696f}) : 1d9076e6fd853ab665d25de4330fee06
  4. 把上面的结果里字母转为大写,并取前16位: 1D9076E6FD853AB6

创建这样两个键值,其中id的值随着系统不同而不同,这样就能阻止Locky加密系统文件。

p5

0x03 破坏RSA密钥


在加密文件前,Locky会发送一个HTTP POST请求到它的C&C服务器,发送明文如下:

(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值,这个数据在发送前会进行简单的编码:

p6

用类似的算法可以解码返回数据:

p7

这两个算法可以用几行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值,从偏移0x10处开始我们可以发现一个BLOB_HEADER结构:

  • type 0x06 = PUBLICKEYBLOB
  • version 0x02
  • 2 reserved bytes
  • ALG_ID 0xa400 = CALG_RSA_KEYX

这是一个RSA公钥,因此下面是RSAPUBKEY结构:

  • magic RSA1 = public key
  • key size: 0x800 = 2048 bits
  • exponent 0x10001 = 65537
  • modulus 2160…94ba

这个结构(除去MD5 hash部分)保存在上面提到的pubkey键值里,如果这个值存在,并且是一个错误的值,那么系统里的文件既不会被改名,也不会被加密。下图中,把pubkey用一个NULL字节填充,然后在测试机器上运行Locky,尽管Locky对txt格式的文件具有针对性,但是这时候桌面上的文件monfichier.txt 并没有发生变化。

p8

另外如果pubkey是一个RSA 1024密钥,文件将会被改名,但是不会被加密(“ceci est un secret”,法语:“这是一个秘密”):

p9

0x04 获取RSA 私钥


Locky有另外一个设计漏洞:如果pubkey的值存在,Locky用pubkey加密文件的时候不做任何验证,允许我们强制使用我们自己掌控的一个公钥来加密文件,同时我们又知道这个公钥相对应的私钥。

下面的C代码将生产一个BLOB_HEADER 格式的RSA 2048 密钥对,用来作为pubkey的值:

#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个随机字符: