virusIn the previous post, we have presented the rootkit used by Kronos. Let’s carry on investigating this banking malware by decrypting its configuration file and injects.

 

Replaying C&C network traffic

Executing Kronos in a sandbox shows it uses a command-and-control (C&C) server located on the following domains:

  • managejave.myftp.org
  • update43x.myvnc.com
  • nonstop.serveminecraft.net

When CERT-LEXSI analysed the binary, these domains were already sinkholed by abuse.ch, preventing us from dynamically retrieving a configuration file. However, we were able to obtain a PCAP capture from one of our partners, created when one of these C&C was still alive. By locally redirecting DNS resolutions to an internal web server, it then became possible to replay this PCAP in a sandbox and observe how Kronos behaves when fed with “real” network traffic from its C&C.

The PCAP has a small size of 5 kB and contains the following data:

  1. DNS resolution of the managejave.myftp.org domain
  2. HTTP POST request to /upfornow/connect.php with an 74-byte encrypted payload
  3. response containing a 90-byte encrypted payload
  4. second HTTP POST request to /upfornow/connect.php?a=1 with a 74-byte encrypted payload, different from the first one
  5. response containing a 2208-byte encrypted payload

Even if banking trojans often use larger configuration files (several hundred kilobytes when including the injects), let’s assume the 2208-byte payload is Kronos’ configuration.

The analysis presented in the previous post showed the configuration file was stored in a sub-directory of %APPDATA% as a random GUID which is determined when the sample is first executed on the system, such as C:Documents and SettingsUserApplication DataMicrosoft{757931CE-B49C-4F86-8F0B-565454834F10}, in a file whose name is created after the first 8 bytes of the MD5 hash of the hard drive serial number, 2bf42c45.cfg in our case.

When replaying the server responses, the binary opens this file for writing but its content remains desperately empty 🙁 Our guess is that Kronos tried to decrypt the configuration received from the fake C&C but the key was invalid. Our sample is exactly the same as the one used to generate the PCAP, so there cannot be a static key, for example hardcoded in the binary. It therefore seems the C&C sends a configuration which is encrypted with some kind of dynamic key, which the client will be able to determine on its side when receiving the server response. As exchanging the required information can only be done via the network, the PCAP needs first to be decrypted before decrypting the configuration file.

Decrypting the PCAP

Here is the detailed content of the packets exchanged between the infected client and the C&C:

  • request #1 (74 bytes)
0000000: 577a 0f0f 0f0f 0f0f 0f0f 0f0f 0f0f 0f0f Wz..............
0000010: 0f0f 0f0f 0f0f 0f0f 0f0f 0f0f 0f0f 0f0f ................
0000020: 0f0f 572c 6466 6561 6f61 6765 7a15 1411 ..W,dfeaoagez...
0000030: 607a 6315 676e 7a15 6214 167a 6014 1661 `zc.gnz.b..z`..a
0000040: 6160 1415 6214 6112 2a57 a`..b.a.*W
  • response #1 (90 bytes)
0000000: 409a 5a00 f9fc f8f8 afa3 afaf ffa3 aeae @.Z.............
0000010: ada9 fef9 aba8 f8a2 a9a2 f8ae afa9 fbab ................
0000020: fbfe a2ac 9af2 eeee eaa0 b5b5 f7fb f4fb ................
0000030: fdff f0fb ecff b4f7 e3fc eeea b4f5 e8fd ................
0000040: b5ef eafc f5e8 f4f5 edb5 f9f5 f4f4 fff9 ................
0000050: eeb4 eaf2 eaa5 fba7 ab9a ..........
  • request #2 (74 bytes)
0000000: d0fd 8888 8888 8888 8888 8888 8888 8888 ................
0000010: 8888 8888 8888 8888 8888 8888 8888 8888 ................
0000020: 8888 d0ab e3e1 e2e6 e8e6 e0e2 fd92 9396 ................
0000030: e7fd e492 e0e9 fd92 e593 91fd e793 91e6 ................
0000040: e6e7 9392 e593 e695 add0 ..........
  • response #2 (2208 bytes)
0000000: c4d6 b12a c61d ee05 2bfb 1aa7 04c4 b114 ...*....+.......
0000010: 82f2 05e6 9fdd 3708 fe6c 0f7d 8f11 c055 ......7..l.}...U
0000020: e93e 9500 d70c 215d 8797 e68d 15e3 668d .>....!]......f.
0000030: 417a 088c 0adf 22f6 abbf 5fc4 ee85 716e Az...."..._...qn
0000040: 1a47 24c9 c736 d289 9541 7ee2 8e36 d297 .G$..6...A~..6..
0000050: b053 79a6 0258 a6fd d40e 83dc 02a6 675f .Sy..X........g_
0000060: 4a64 a3df b198 6d49 2d04 62b0 0d45 71d2 Jd....mI-.b..Eq.
0000070: 453f 15dd edfb 15e0 c447 ecdb 318e fb28 E?.......G..1..(
0000080: 5bed 56ac d99d 7978 1a80 bb6b 3160 c956 [.V...yx...k1`.V
0000090: 0409 5a99 1859 5c04 65eb bb37 c0a8 d7db ..Z..Y.e..7....
00000a0: c751 8180 38cd 3000 4465 09d0 c7dd 3bd9 .Q..8.0.De....;.
[...]

Except in response #2, repeated patterns can be noticed (for example 0x0f in request #1 and 0x88 in request #2) as well as a “lack of entropy” which may indicate the messages are simply encoded and not encrypted. What comes to mind at first is that these bytes may be the encoded versions of the null or space byte but after trying the corresponding XOR key candidates, the buffers are still unreadable.

By looking for functions the malware goes through before sending HTTP requests, we stumbled upon the function at 0x9ce5d which first generated the following cleartext buffer (they really are “X” characters):

(gdb) hexdump 0x7fd84 74
57 00 58 58 58 58 58 58 58 58 58 58 58 58 58 58 |W.XXXXXXXXXXXXXX|
58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 58 |XXXXXXXXXXXXXXXX|
58 58 00 7b 37 35 37 39 33 31 43 45 2d 42 34 39 |XX.{757931CE-B49|
43 2d 34 46 38 36 2d 38 46 30 42 2d 35 36 35 34 |C-4F86-8F0B-5654|
35 34 38 33 34 46 31 30 7d 00                   |54834F10}       |

At the end of the buffer, the random GUID can be spotted. The first byte is also random and Kronos simply uses it as a XOR key to encode the rest of the buffer from the third byte. A similar analysis shows responses are also XOR-encrypted, the key being the second byte of the buffer.

The first three messages can now be decoded:

  • request #1 (XOR 0x57)
0000000: 002d 5858 5858 5858 5858 5858 5858 5858 |.-XXXXXXXXXXXXXX
0000010: 5858 5858 5858 5858 5858 5858 5858 5858 |XXXXXXXXXXXXXXXX
0000020: 5858 007b 3331 3236 3836 3032 2d42 4346 |XX.{31268602-BCF
0000030: 372d 3442 3039 2d42 3543 412d 3743 4136 |7-4B09-B5CA-7CA6
0000040: 3637 4342 3543 3645 7d00                |67CB5C6E}.

This packet contains the random GUID of our partner’s system which the PCAP has been generated from.

  • response #1 (XOR 0x9a)
0000000: da00 c09a 6366 6262 3539 3535 6539 3434 |....cfbb5955e944
0000010: 3733 6463 3132 6238 3338 6234 3533 6131 |73dc12b838b453a1
0000020: 6164 3836 0068 7474 703a 2f2f 6d61 6e61 |ad86.http://mana
0000030: 6765 6a61 7665 2e6d 7966 7470 2e6f 7267 |gejave.myftp.org
0000040: 2f75 7066 6f72 6e6f 772f 636f 6e6e 6563 |/upfornow/connec
0000050: 742e 7068 703f 613d 3100                |t.php?a=1.

The server responds with a string which could be a MD5 hash or a key, followed by the URL used in the second request.

  • request #2 (XOR 0xd0)
0000000: 002d 5858 5858 5858 5858 5858 5858 5858 |.-XXXXXXXXXXXXXX
0000010: 5858 5858 5858 5858 5858 5858 5858 5858 |XXXXXXXXXXXXXXXX
0000020: 5858 007b 3331 3236 3836 3032 2d42 4346 |XX.{31268602-BCF
0000030: 372d 3442 3039 2d42 3543 412d 3743 4136 |7-4B09-B5CA-7CA6
0000040: 3637 4342 3543 3645 7d00                |67CB5C6E}.

This request is the same as the first one. We should now have all required information to decrypt the configuration file.

Decrypting the configuration from the network

After receiving response #2, the sample computes the MD5 hash of the random GUID, then calls cryptographic functions. More precisely, a first function myCrypto_part1 is given the first 16 bytes of this MD5:

crypto1

GUID dependent key

The key used for encryption seems therefore to depend on this GUID. Decoding the network traffic gave us the GUID used by the infected client which can be injected in memory just before getting the MD5 hash from the myComputeGuidMD5 function:

(gdb) hexdump *(int*)($esp) 48
7b 33 31 32 36 38 36 30 32 2d 42 43 46 37 2d 34 |{31268602-BCF7-4|
42 30 39 2d 42 35 43 41 2d 37 43 41 36 36 37 43 |B09-B5CA-7CA667C|
42 35 43 36 45 7d 00 00 00 00 00 00 00 00 00 00 |B5C6E}..........|

The MD5 hash of {31268602-BCF7-4B09-B5CA-7CA667CB5C6E} being 172552042b05c166629f72a39e901113, the first 16 bytes passed to the myCrypto_part1 function are 172552042b05c166 (as an ASCII string).

After altering the GUID in memory and letting the sample resume execution, the configuration file on disk is now 2 kB: our approach is proving successful and our sample seems to have correctly assimilated the configuration from the PCAP. By tracing the code step-by-step, the end of the decryption routine is easily found and the configuration appears in clear:

(gdb) hexdump $esi 2208
73 65 74 5f 75 72 6c 20 68 74 74 70 73 3a 2f 2f |set_url https://|
2a 6d 61 69 6c 2e 63 6f 6d 2a 20 47 50 0d 0a 0d |*mail.com* GP...|
0a 64 61 74 61 5f 62 65 66 6f 72 65 0d 0a 69 64 |.data_before..id|
3d 22 6c 6f 67 69 6e 2d 6c 69 6e 6b 22 3e 0d 0a |="login-link">..|
[...]

Encryption details

Determining the encryption algorithm details can be done in several ways. One could look for cryptographic constants but some algorithms will not be found (ex: RC4) and some pieces of malware encode these constants and only decode them when needed (such as Qadars for AES constants). One could also try to recognise some characteristic patterns of these algorithms, which should not be an issue with Kronos which is not obfuscated beyond its packer. As an example, by stepping into the myCrypto_part1 function, the following blocks are quickly found and they contain a one-byte rotation to the left, a XOR with a S-Box and a XOR of the first byte, that is the AES key schedule core:

crypto2

AES key schedule core

The steps following these blocks show it is AES-128 (for AES-192 and AES-256, additional steps are required in the key schedule routine, which are not performed by Kronos). At the end of the myCrypto_part1 function, the 176 bytes of round keys have been computed:

(gdb) hexdump 0x7fb9c-16 176
31 37 32 35 35 32 30 34 32 62 30 35 63 31 36 36 |172552042b05c166|
f7 32 37 ce c2 00 07 fa f0 62 37 cf 93 53 01 f9 |.27......b7..S..|
18 4e ae 12 da 4e a9 e8 2a 2c 9e 27 b9 7f 9f de |.N...N..*,.'....|
ce 95 b3 44 14 db 1a ac 3e f7 84 8b 87 88 1b 55 |...D....>......U|
02 3a 4f 53 16 e1 55 ff 28 16 d1 74 af 9e ca 21 |.:OS..U.(..t...!|
19 4e b2 2a 0f af e7 d5 27 b9 36 a1 88 27 fc 80 |.N.*....'.6..'..|
f5 fe 7f ee fa 51 98 3b dd e8 ae 9a 55 cf 52 1a |.....Q.;....U.R.|
3f fe dd 12 c5 af 45 29 18 47 eb b3 4d 88 b9 a9 |?.....E).G..M...|
7b a8 0e f1 be 07 4b d8 a6 40 a0 6b eb c8 19 c2 |{.....K..@.k....|
88 7c 2b 18 36 7b 60 c0 90 3b c0 ab 7b f3 d9 69 |.|+.6{`..;..{..i|
b3 49 d2 39 85 32 b2 f9 15 09 72 52 6e fa ab 3b |.I.9.2....rRn..;|

As the AES key schedule uses the key itself as its first bytes, we can infer the encryption key is 172552042b05c166, namely the first 16 bytes of the MD5 hash of the GUID.

In the rest of the routine, the binary decrypts 128-bit blocks from response #2 beginning at byte 16 (82f2 05e6 …). As for the mode of operation, the first decrypted block is XORed with the first 16 bytes from response #2, that is c4d6 b12a c61d ee05 2bfb 1aa7 04c4 b114, and each of the following blocks is XORed with its previous encrypted block:

crypto3

AES in CBC mode

It is the well-known CBC mode and the first 16 bytes of response #2 are the initialization vector.

All encryption details are now known and a simple script can statically decrypt response #2 into a 2192-byte configuration file (2208 minus 16 bytes of IV):

$ ./kronos_decrypt.py
[...]
set_url http*://lexsiclient.fr*auth* GP

data_before
<head*>
data_end

data_inject
<div style="position:fixed;top:0px;left:0px;width:100%;height:100%;z-index:1110;background:#ffffff;" id="none_div"></div>
<div style="display:none;" id="BMUD">BOT_MACHINE_UUID</div>
<script>
window.onerror = function (msg){return true};
var RTS = (function(){
[...]
</script>
data_end

data_after
data_end

The AES implementation in Kronos is therefore standard and the resulting configuration file contains the usual set_url, data_before, data_afterdata_inject and data_end tags, first introduced by Zeus and used since then by other malware families (mainly because this allows cybercriminals to reuse the same injects for distinct banking malware families).

The end of the decrypted file is:

0000860: 6970 743e 0d0a 6461 7461 5f65 6e64 0d0a |ipt>..data_end..
0000870: 6461 7461 5f61 6674 6572 0d0a 6461 7461 |data_after..data
0000880: 5f65 6e64 0d0a 0d0a 0000 0000 0000 0000 |_end............

Kronos computes the size of this file with the strlen function, which returns a size of 2184 bytes (2192 bytes minus 8 null bytes at the end). It finally computes the MD5 hash of these 2184 bytes and finds cfbb5955e94473dc12b838b453a1ad86, that is the MD5 sent by the C&C in its first response.

Conclusion

To obtain its configuration, Kronos sends the C&C the random GUID generated when first run on the system, encoded with a simple XOR. The configuration file returned by the server is encrypted with AES-128-CBC with the first 16 bytes of the MD5 hash of this GUID as the key and is preceded by the initialization vector.

Note: Thanks to our friends at S21sec for sharing the PCAP.