About unusual way of ransomware decryption key recovery


Unbreakable pairing of RSA-1024 and AES-128 with correctly generated keys (secure system-provided randomizer). Still it’s possible to restore the encryption key and get user files back. Ransomware operator’s site allows us to decrypt “Name” from the KEY-file (provided with a ransom note to the victim) which among other contains victim’s private key, this can be abused to get private key decrypted instead of Name (we cannot decrypt this file ourselves because it is encrypted using AES + operator’s public RSA key). This article will show the technical details of making recovery attack work automatically.

This information was privately reported to Emsisoft (@fwosar) and Kaspersky. I hope they used this information to help victims. I was also monitoring some forums and offering free help for anyone who would send me a KEY-file and an example of encrypted file.

Since this was fixable from the server side I decided not to post it at that time. Now it’s almost 3 years since then, I think it’s safe to share. May be someone will find this interesting.

This note is about interesting example of strong encryption system with a little hole in implementation which allows us to ruin hacker’s plans about impossibility of getting decryption key without paying ransom.

At some organization PC was infected with ransomware, so access to some files was lost.

I started my research with some encrypted files and data that was created by the trojan itself:


HTML-page looked like this:


Figure 1. Ransom message in Russian. It detects user locale and will switch to English if necessary.

Trojan told us its name («SPORA RANSOMWARE»), so next step is getting the sample. Corresponding binary was found and downloaded from

Executable was packed using “UPX” and after unpacking has size about 24kB. It basically uses WinAPI only. Compiled in Visual Studio 2008 from single *.c file (Rich Signature info). Compilation date is Tue Jan 10 02:44:29 2017 (“TimeStamp” field of PE-header).

Overall trojan’s algorithm:

  1. Hacker’s RSA-1024 public key is decrypted from binary = img.

  2. Pair of RSA-1024 public / private key is generated = img.

  3. Malware is searching for files which is to be encrypted later. It has embedded list of extensions to look only for possibly important user files. There are six groups of files this ransomware wants to encrypt (it counts number of encrypted files by type, so it can later use it to adjust ransom $ amount).

    Documents: ".xls", ".doc", ".xlsx", ".docx", ".rtf", ".odt".
    Documents PDF: ".pdf".
    Projects of Photoshop / CorelDraw / AutoCad: ".psd", ".dwg", ".cdr".
    Database files: ".cd", ".mdb", ".1cd", ".dbf", ".sqlite", ".accdb”.
    Pictures: ".jpg", ".jpeg", ".tiff".
    Archives: ".zip", ".rar", ".7z", ".backup".
  4. For each file in the list:
    1. AES-256 key is generated = K.

    2. File body is encrypted using key K in CBC mode with IV = 0x0000..00 (16 bytes of 0x00). We will define result as img.

    3. Key K is encrypted using victim’s public key img. Result is img.

    4. Checksum is calculated - crc32(img = H.

    5. Original file replaced with concatenation of following: img.

    6. K is destroyed.

  5. After every scheduled file was encrypted, VICTIM_INFO is built:

  6. Victim’s identifier(USERID) is generated. Example:


Figure 3. USERID structure.

  1. *.KEY file is generated

    1. New AES-256 key is generated = K.
    2. img is calculated (CBC mode, IV = 16 NULL bytes).
    3. Key K is encrypted using hacker’s public key img. Let’s define it as img.
    4. File saved with following data: img .
    5. K is destroyed.
  2. img is destroyed.

  3. With the same method (Digital Envelope) described earlier, new AES-256 key is generated and encrypted with img. Encrypted key with the list of encrypted files is written inside *.LST file.

  4. The last generated file has numerical name. Numbers are decimal VolumeId of system partition. This file consists of 4 blocks, each one is encrypted using CryptProtectData function with optional entropy set to that VolumeId (name of the file).

    1 block – status-number of current malware state (files are encrypted, LST-file is generated etc.).

    2 block – file list just like in *.LST.

    3 block – victim’s public img.

    4 block – USERID.

Note: Function CryptProtectData (Windows CryptoApi) does encryption using some internal algorithm (AES most likely but it may depend on operating system version), key is generated based on «entropy» argument and some Windows user account specific data. If we knew the entropy and have access (offline is also OK, but 3rdparty tools may be required) we can decrypt data with the help of CryptUnprotectData..

Last file (VolumeId in name) is used for smooth continuation of encryption process in case of unexpected breaks (like shutdown etc.)

Summary: After encryption is complete it is impossible to recover files without hacker’s private key. It is so because each file is encrypted with AES encryption and key is securely random (we will assume that Windows Crypto API provides good crypto keys, trying to hack it is out of scope). This key is then encrypted with victims RSA-1024 public key, corresponding private key for which is stored in KEY-file. Unfortunately it is impossible to decrypt the KEY file without hackers private key.

Let’s go back to HTML-page and visit operator’s web site. At first they will ask us for USERID, and then to upload our KEY-file. You can see the interface on figure 4.

We can notice that interface has 2 fields that was delivered to the site via KEY file. It’s «Date of block» and «Username».

Note: Even if my screenshots of interface are in Russian, for non RU victims English interface would appear instead (and they even had separate chat).

Let’s try to modify KEY-file a little bit. I changed some random bytes in the last block of file (img) and uploaded it to the server. File was accepted, which means that server ignored “trash” part and there is no check with MD5-part(5 bytes) from USERID.

This means we can try do build “decrypting oracle” from the site and restore victim’s private key (this key is stored near “Username” in KEY file, see figure 5).

Note: Some of examples later will have corresponding plaintext. At the moment of recovery attack we obviously did not had that (the only exception is key format related strings in first two blocks) and I could only guess what it looks like. To make it easier to understand, plaintext will be provided for examples (you can also think of it as view from the hacker’s side).


Figure 4. One of the victims has paid the ransom (367$) and published USERID, this allowed us to login into that account. We can download decryptor but it will not work for us, because it contains private key of another victim.


Figure 5. Completely decrypted KEY-file (this data was recovered using vulnerability).

First thing that we have to do is achieve stable decrypted block output in one of interface fields. Field “Date of block” in not useful for us. Everything besides [0-9.] is filtered out from it. Second field “Username” prints almost all the symbols. The one that are lost: «/», «+», «\r». We will try to use the second one.

Note: List of filtered characters was compiled by manual KEY-file building. I generated a lot of KEY files with different usernames and checked server reaction on each one of them.

Before we start playing with blocks, let’s remember how CBC encryption mode is working (figure 6).


Figure 6. CBC mode encryption.

For any CBC encrypted data following statement is true:

If we duplicate any block of ciphertext, plaintext will have a "trash" in place of that block. Everything else will remain unchanged (some data will shift for one block forward).

See figure 7.




Figure 7. KEY-file before block duplication, after and what server had seen after decryption.

Note: Bytes in «trash» block look like something useless and meaningless, but we can actually predict it if we know:

Formula for predicting “trash” block:

If we send this KEY-file to server, it will respond with “Date of block: 20.01.2017245”, «Username: Fe███████». For us it is important that:

This allowed us to identify relative location of duplicated block inside plaintext.

As was mentioned before we have at least 2 pairs of ciphertext-plaintext because first two blocks are fixed RSA PRIVATE key header (text).

In CBC mode if even one such pair is known, we can inject arbitrary plaintext block which would appear after decryption (but it will also lead to appearing “trash” block before injected one).

We will try to inject plaintext block ‘AAAAAAAAAAAAAA|Q’ between Date and Name.

To achieve that, we took the first pair of chiphertext-plaintext and calculated the mask:

If we place blocks exactly like shown in figure 8, then after decryption block ‘AAAAAAAAAAAAAA\|Q’ will appear in place of known ciphertext-plaintext block (ciphertext part).

Figure 8 actually have 3 more additional blocks. This is my final payload for arbitrary block decryption (see figure 9, it is server-side view). Generally, that plaintext we used (‘AAA..A\Q’) will make server parser to separate everything before ‘|’ to the Date field (which ignores ‘A’ as we identified previously). Next block will contain our target block (we want to decrypt it) in form of (‘Q<decrypted and modified target block>’). Reason to place one more random block will be revealed later. Duplicated block is required to allow server to successfully decrypt Username and remain with minimal number of ‘|’ (this is actually not important, because server-side doesn’t check for it).

Note: We can not predict what will appear in place of some blocks (mask, random) after decryption, that’s why we can only hope that no bad sequences appear and nothing break the parser in the way that we became unable to abuse it (for example, unexpected appearing of ‘|’ or ‘\n’ will stop us from seeing decrypted block).


Figure 8. Payload for decryption of «all zero block» injected into KEY-file.


Figure 9. Payload. Server-side view.

Note: Next example uses different payload (another pair of known ciphertext-plaintext is used). Idea is still the same, but blocks are different. I decided not to touch hacker’s server more then necessary.

Finally, payload is sent to server, and we see something interesting in «Username» field:

QфюІaБQм_їАlИЇОЁѕc = {same data in hex} 51f4feb261c151ec5fbfc06cc8afcea8be63

We placed first byte Q (0x51) just for self check and it can be ignored.

Note: From now on word «mask» will have different meaning to us. This is not a block for injection arbitrary plaintext anymore, instead it is a XOR sum of predecessors according to CBC mode (blocks in original sequence and new).

This data is not exactly the plaintext of requested block, its somewhat modified. It XORed with 2 other blocks and even some bytes are lost (Name field is not showing all symbols to us). First mask is taken from the original previous block (because all blocks we want to decrypt are taken from the KEY-file itself, each one of them has a predecessor, predecessor of the first is IV). Second mask is from known pair we used. It is probably a good idea to save merged masks altogether with received trashy usernames.

I automated everything and got results for different masks and lost symbols. Example for 6th block (counting from 1):

m=ac309ffda0d13c51f67daa541879b714 [QísÕílaHŸ6ßNGxÅßfÄ] br=51ed73d5ed6c61489f36df4e4778c5df66c4

m=ac309ffda0d13c51f67daa541879b714 [QísÕílaHŸ6ßNûºcäúÀ] br=51ed73d5ed6c61489f36df4efbba63e4fac0

m=f281ba7d63dbef9e2cc76523fa77426b [Q³Âð._¿®tòPÈ81hoI5—Qt¤¬ì] br=51b3c2f02e8dbfae74f250c83831686f4935975174a4acec

m=f281ba7d63dbef9e2cc76523fa77426b [Q³Âð._¿®tòPÈ81žE’jœŠFs¢±] br=51b3c2f02e8dbfae74f250c838319e45926a9c8a4673a2b1

Notice that Name we receive from server differ while we have constant mask (mask=xor of 2 blocks). It remain same up to some symbol and then becoming completely different. This is result of insertion of «random» block into payload (random block is different every request while block we want to decrypt is constant). Common part of that name is exactly decrypted+modified part of KEY-file blocks.

For the mask ac309ffda0d13c51f67daa541879b714 (16 bytes) decrypted+modified block looks like ed73d5ed6c61489f36df4e (11 bytes). Which is obviously not enough bytes to apply a mask and get original plaintext. But we can continue process of getting more results by trying to decrypt it using other known blocks (initially we have only 2, but every successful block decryption will give us another pair!). After using about 2-6 pairs of known blocks we will finally get everything we need to restore plaintext.

As we knew which ranges of symbols can be in plaintext, we can build a tree-graph of solutions based on mask value (ac309ffda0d13c51f67daa541879b714) and corrupted blocks (ed73d5ed6c61489f36df4e)

At the leaves of this tree (moment then either mask or corrupted block end is reached) we will get solutions. This algorithm will give us all possible solutions, so if we make intersection, we will get correct positions of bytes in plaintext!


Figure 10. First three levels of solution tree. Any «Yes» answer fixes symbol in result, and it moves down the tree.

All possible variants found by algorithm:


Intersection of above variants:


Now we use another known pair to get another possible variant of plaintext:

(f281ba7d63dbef9e2cc76523fa77426b, b3c2f02e8dbfae74f250c83831).

We got ACJ??VP0X5??2???. We can use union operation to merge it into better result:

ACJ??VP0X5??2??? ∪ ACJ?M?P0?55???hZ = ACJ?MVP0X55?2?hZ

By repeating the algorithm and skipping variants that contradict with our mask we will finally identify all the bytes of plaintext.

Repeating this for all blocks at the end gives us full KEY file decrypted.


During the recovery attack ransomware operator did some actions to stop me from block collection, but fortunately to us, he most likely was not able to understand the idea behind spamming attack. From his side everything looked like someone is trying to fill his database with a bunch of “trash” records (the names, that have meaning to us, but look like complete nonsense from his side). At first he tried to add a restriction to upload of more than one KEY file from one IP address (→ I used TOR to bypass this, by jumping from one exit node to another). Later our private key was blacklisted so site would refuse to get new KEY files with it (→ I added some random blocks in the middle of encrypted private key)

Block collection was complete, private key recovered and all affected files were successfully decrypted on victim’s PC.

I later made some optimization related to which part of private key is only necessary for getting the private key. Check picture below, obviously, we do not need all _private key_, just one of the prime numbers is fine (RSA immediately will became trivial to break). Picture is in Russian, but everything should be obvious.