Back in 2009, we applied the notion of vaccine to Conficker.C to protect workstations and servers against this rapidly propagating worm. Let’s check whether we can apply the same concept to the Locky ransomware.

IMPORTANT: the vaccines presented in this blog post are not efficient against the current Locky variant. A new dynamic vaccine is described in another post.

In the rest of this blog post, we will call vaccine any minor system modification which aims at blocking, without any user interaction, part or all of the harmful effects related to the execution of a malicious program. Obviously, the vaccine must be administered before the disease arrives…

A minor modification can be a specific mutex or registry key creation, or a simple system parameter modification which will not cause any inconvenience to the user. A a counter-example, at the beginning of its execution, Locky checks the system language and doesn’t infect those configured in Russian:locky_ru

It is therefore possible to set the system language to Russian to prevent from being infected but the system is likely to be hardly usable for many people 🙂

Vaccine #1: ACL on the Locky registry key

After checking the language, Locky tries to create the HKCU\Software\Locky registry key; if that fails for any reason, Locky immediately terminates.

locky_reg2

By creating this key before Locky with ACLs preventing anyone from opening it, the system is fully vaccinated:

locky_reg

Vaccine #2: completed registry value

Locky then checks the HKCU\Software\Locky key and looks for the id (identifier of the infected machine), pubkey (public key fetched from the server, more on this later), paytext (text to be displayed to the user, in his language) and completed values. This last value is used to indicate the end of the encryption process. Locky verifies if completed is set to 1; if it is and if the id value also contains the correct identifier for the system, it stops its execution:

locky_completed2

The identifier generation algorithm is simple and gives the following result on our test machine:

  1. GetWindowsDirectoryA() : C:\WINDOWS
  2. GetVolumeNameForVolumeMountPointA(C:\WINDOWS) : \\?\Volume{ b17db400-ae8a-11de-9cee-806d6172696f}
  3. md5({b17db400-ae8a-11de-9cee-806d6172696f}) : 1d9076e6fd853ab665d25de4330fee06
  4. converting to uppercase ASCII and truncating to the first 16 characters: 1D9076E6FD853AB6

Creating these two registry values, one of them being system-dependent, prevents Locky from encrypting files:

locky_completed

Vaccine #3: corrupted RSA key

Before encrypting files, Locky sends an HTTP POST request to its C&C with the following clear data:

(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

The first line is the MD5 hash of the rest of the buffer. This data is basically encoded before being sent:
locky_encode

Decoding the data received in response is done with a similar algorithm:
locky_decode

These two algorithms can be implemented in a few lines of 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

Decoded data looks like this:

 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                               ....

The first 16 characters are the MD5 hash of the rest of the buffer. From offet 0x10, we can spot a BLOB_HEADER structure:

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

This is a public RSA key, so the following bytes are an RSAPUBKEY structure:

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

As mentioned previously, this structure is stored as is (minus the MD5 hash) in the pubkey value. If this value exists but contains an invalid value, files are neither renamed nor encrypted by Locky. The following capture shows a test machine with a pubkey value containing a single null byte. Locky has been executed on the machine but has left the monfichier.txt (“myfile.txt“) file present on the desktop untouched although its extension is targeted:

locky_bypass1

Moreover, if the pubkey value contains an RSA 1024 key, the file will be renamed but not encrypted (“ceci est un secret” means “this is a secret” in French):

locky_bypass2

Vaccine #4: kown RSA private key

Locky has another interesting design bug: if the pubkey value already exists, it is used without any kind of verification when encrypting files, allowing to force the use of a public RSA key under our control and for which we have the corresponding private key.

The following C code generates an RSA 2048 key pair in the BLOB_HEADER format used by Locky in its pubkey value:

#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);

After a few lines of Python, we have a .reg file at our disposal that we can insert into the registry to force the use of our public key.

Let’s observe precisely how a file is encrypted. After obtaining a handle to the PROV_RSA_AES CSP (Cryptographic Service Provider)  via a call to CryptAcquireContext(), Locky imports with CryptImportKey() the blob containing the public key, stored in the pubkey registry value. It renames the targeted file to <id><16 random bytes>.locky. Via the CryptGenRandom() function, it then generates another 16 random bytes:

locky_genrandom

(gdb) hexdump 0x009ef8a0 16
9d 86 d3 42 48 3a 45 04 1a cb 95 1c 77 90 8f 7c

These bytes are copied after a BLOB_HEADER structure imported with CryptImportKey():

(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
  • version 0x02
  • 2 reserved bytes
  • ALG_ID = 0x660e = CALG_AES_128
  • key size = 0x10 bytes

So this is an AES-128 key. The GetSetKeyParam() function is then called to give more information about how the key is to be used:

(gdb) x/w $esp+4
0x9ef830: 0x00000004

(gdb) x/w *(int*)($esp+4+4)
0x9ef858: 0x00000002

Value 4 in the second argument means KP_MODE and allows to specify the operation mode: value 2 in the third argument means CRYPT_ MODE_ECB.

The CryptEncrypt() function is called to encrypt this AES key with the RSA public key. The following result is obtained:

(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.|

This buffer is copied as is in the .locky file.

Then, Locky builds a 0x800-byte buffer filled with zeros, except at the end of each line where a counted in incremented:

(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 |................|

These bytes are encrypted with AES-128-ECB via CryptEncrypt() with the key generated above, to obtain the following K buffer:

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 
[...]

The first element to be ciphered is the file name. Locky reserves a 0x230-byte buffer, fills it with zeros, and copies the file name before XORing it with the K buffer which is used as a key stream. As an example, for bytes 0 to 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 |................|

For bytes 4 to 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 |................|

And so on. The resulting buffer is copied as is in the .locky file after the RSA-encrypted AES key.

Locky then encrypts the file content in the same way as its name but uses the key stream from offset 0x230 (address 0x009267c8 in our case). This is unlikely to be a random choice inasmuch as if Locky developpers had used the same key stream for both elements, decrypting the file content would have been trivial… Indeed, the 0x230 bytes reserved for the name are almost all zeros, and the XOR operation on these bytes, whose result can be found in the .locky file, would have provided us with a huge proportion of the key stream itself without knowing the corresponding AES secret key.

The content of the file is XORed with the key stream from offset 0x230:

(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..............|

The result of the full XOR operation is copied as is at the beginning of the .locky file.

If the size of the file is greater than the initial incremental buffer, Locky encrypts other buffers of the same layout with the CryptEncrypt(), incrementing the counter as needed:

(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
[...]

Finally, a .locky file has the following layout:

locky_format1_enlocky_format2

Decrypting a .locky file takes the following steps:

  1. deciphering the RSA buffer with our private key to obtain 16 bytes
  2. using these bytes as an AES-128-ECB key to encrypt an incremental, constant buffer
  3. using the first 0x230 bytes of the result as a key stream to XOR the file name
  4. using the bytes from offset 0x230 as a key stream to XOR its content

The following C code implements the first step:

// 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);

The AES key is retrieved correctly:

$ xxd aeskey.raw
9d86 d342 483a 4504 1acb 951c 7790 8f7c

The following Python script allows to decipher the file name and its content by using this key:

#! /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

Let’s see if it works:

$ ./unlocky.py
UnLocky - Locky decryption tool, CERT-LEXSI 2016
[+] File name:
monfichier.txt // "myfile.txt"

[+] Content:
ceci est un secret // "this is a secret"

Conclusion

Locky has been wreaking havoc for many weeks but there exists very simple means to prevent files from being encrypted, without any anti-virus or security tool, provided that the system has been prepared for it. As an example, one of the 4 vaccines presented in this blog post forces the RSA public key which the AES keys are encrypted with.

As far as cryptography is concerned, the analysis detailed above shows that files are not directly encrypted with AES-128-ECB. Locky builds an incremental buffer, encrypts it with AES-128-ECB and the result is used as two separate key streams to XOR the file name and its content, which looks more like CTR.