Thursday, October 14, 2010

Domain Name Generator for Murofet

This post describes a technique that allows building a domain name generator for Murofet.

The pseudo-random domain generators are not new – these were previously used by Sober, Kraken, or Conficker worms. The important thing about reproducing a particular domain generator is an ability to predict what domains the worm will query in the future. Once known, these domain registrations can potentially be blocked, "sinkholed" or at least monitored.

Now, domain generator reproduction is a tricky task. It can basically be done in 2 ways.

First, the original algorithm can be studied in its disassembled form, and then its logic reproduced in a higher programming language. The second method assumes that the original algorithm studying can take longer than expected, so it offers a shortcut solution – a "hack" – to take the original code "as is" and then either replicate it in a standalone tool written in Assembly from scratch (e.g. by using MASM) or use it in inline Assembler of a higher level language such as C++ or Delphi.

Another approach is to patch the malicious binary in order to force it looping the way you need and then hook and log some particular APIs it is calling (such as UrlDownloadToFile()) in order to obtain the output.

We’ll take the route of ripping the original code apart. This is a no brainer exercise – it shouldn’t take long time as we don’t have to understand how exactly the domain generator works – we only need to understand where the code is located, what it does functionally, and most importantly, how to interface it properly with our higher level code. That is, we need to "glue" or attach it correctly to our code.

Murofet is a file infector. It appends 1,771 bytes to a host executable.

The APIs it calls are dynamically retrieved from shlwapi.dll, urlmon.dll, kernel32.dll, advapi32.dll DLLs by matching their ASCII name hashes – it is a very common technique.

The domain generator routine requires 4 parameters:

  • a base address of the adavapi32.dll module – the domain generator needs it to dynamically retrieve the APIs CryptAcquireContectW(), CryptCreateHash(), CryptHashData(), CryptGetHashParam(), CryptDestroyHash(), CryptReleaseContext(). It calls these APIs during the domain name generation.

  • a seed value – it starts from a fixed number 119 (current year mod 256, multiplied by 17, that is 7 * 17)

  • current date (GMT)

  • a pointer to a buffer that will store the result – the generated domain name

Once the domain is generated, the sample attempts to download an executable from that domain. Next, it increments the seed value (119 -> 120), and repeats the same loop – that is, generates a new domain name, then attempts to download from there. The loop repeats 800 times.

Thus, Murofet, generates 800 domains a day.

The domain generation routine is called by Murofet the following way:

As seen in the listing, the routine takes 4 parameters on stack – base address of adavapi32.dll, the seed value, a pointer to the SYSTEMTIME structure filled with the current time and data (Murofet calls GetSystemTime() for that), and a pointer to a buffer that will receive the result.

In order to reproduce this algorithm, this is what has can be done.

  • Create a VC++ MFC project that generates a day, a month, and a year value, then creates a log file to dump there the generated domains.

  • Create a stub in your executable that declares a buffer filled with 0x90 values. The stub can take a few Kb in size.

  • Create an inline assembler code in your code that wraps the domain generation routine calling, that is, pushes 4 required parameters on stack – we’ll need this code to "glue in" the native Murofet domain name generator. The call to the routine itself should now consist of 5 NOPs – we’ll patch this call later as now we don’t know where in the code the routine will be located. This step is best outlined with the code below:

  • // get the base address of advapi32.dll - it will be needed by Murofet to obtain Crypto-API hashes

    HMODULE  hAdvapi32;
    hAdvapi32 = LoadLibrary("advapi32.dll");

    // prepare a SYSTEMTIME structure

    lpst = (LPSYSTEMTIME)malloc(sizeof(SYSTEMTIME));

    // prepare you year, month, and day values: wYear, wMonth, wDay
    // fill your SYSTEMTIME structure with these values

    lpst->wYear = wYear;
    lpst->wMonth = wMonth;
    lpst->wDay = wDay;

    // prepare a log file name

    char szLogFile[MAX_PATH];
    sprintf(szLogFile, "c:\\logs\\log_%d_%02d_%02d.txt", y, m, d);

    // prepare a buffer that will hold your domain name

    LPBYTE lpbyDomainName;
    lpbyDomainName = (LPBYTE)malloc(1024);

    // this is our "glue" - prepare the initial seed value of 0x77

          push 77h

    // start the loop - 800 domain will have to be generated for a given day (wYear, wMonth, wDay)

    for (int i = 0; i < 800; i++)

          memset(lpbyDomainName, 1024, 0);

                pop eax
                push eax

                xor edx, edx
                mov ecx, 3fch
                div ecx        ; divide the seed value by 1024 - the reminder is the same


                ;push 4 parameters on stack

                push hAdvapi32       ; 1st - base address of the advapi32.dll (retrieved earlier)
                push edx             ; 2nd - a reminder of division of the seed value by 1020, which is the same as the seed value
                push lpst            ; 3rd - a pointer to our SYSTEMTIME structure, where we've put a specific year, month and day
                push lpbyDomainName  ; 4th - a pointer to a buffer that will receive the result

                NOP                  ; these 5 NOPs will later by patched with a call to the routine
                NOP                  ; 1st NOP will be replaced with E8 (call)
                NOP                  ; other 4 NOPs with a distance between the following operand (see pop eax below)
                NOP                  ; and the routine itself

                pop eax              ; increment the seed value that we store in eax
                inc eax
                push eax

          // after the stub above is executed, we'll have the generated domain name in our lpbyDomainName buffer
          // drop it into the log

          DropLog(szLogFile, (LPCSTR)lpbyDomainName);

          pop eax

  • Compile your executable in debug mode.

  • Patch the section with the stub in it to make it executable.

  • Open your compile in a HEX editor, find your stub, replace 1,771 bytes in it with the original Murofet code. This way, we are sort of "infecting" our executable with the Murofet, but we don’t give its code any control just yet.

  • Open your executable in the disassembler, find your "glue" code created in step 3, and find the domain generation routine. Find the difference between their addresses, that is, subtract the address where your 5 NOPs are located (incremented by 5 as your call will take 5 bytes) from the address of the domain generation routine, let’s say the distance between these calls is 0x010203. Then, patch your 5 NOPs with a call to the generator by replacing them with E8 03 02 01 00.

  • Run your executable.

Thursday, August 19, 2010

Matryoshka in Flash

Second part of the article from the Crime Scene Investigation:Internet series has now been published by c't magazine.

This time the Action Script's p-code deobfuscation technique is illustrated.

You can read this article in German or in English.

Wednesday, August 4, 2010

Angriff der Killervideos

It took some time, some patience and some extra samples analysed to see how the original blog post on a Flash exploit has eventually evolved into an article for a German computer magazine c't (magazin für computertechnik).

Original article in German is available at this link. Its translation into English is available here.

Thanks to Frank Boldewin and Jürgen Schmidt for making it happen.

Sunday, May 2, 2010

Config Decryptor for ZeuS 2.0

ZeuS 2.0 kit release introduces a few tricks designed to complicate the analysis of its configuration files.

Apart from randomized side-effects that the new trojan leaves on a system, including its ability to morph in order to avoid hash-based detections (well, hash-based detections never worked against ZeuS anyway, given the sheer volume and frequency of the generated samples and the variety of used packers), it seems that this time a great care was taken in protecting its configuration files.

The trojan now uses more layers in order to decrypt its configuration files.

Shrek: Onions have layers. Ogres have layers... You get it? We both have layers.
Donkey: Oh, you both have layers..

The new decryption steps are illustrated below:

It starts from initializing a 256-byte key table. At first, its bytes are set to value N, where N is a position of the byte in the key table (from 0 to 255).

Next, the code utilizes a large permutation table - a dynamically constructed table with a variable size around 40,177 bytes, in order to generate a new key table.

The newly generated key table is then used to decipher (RC4) another dynamically constructed table, called in the scheme above a "small table".

Once deciphered, the small table will contain both the configuration file URL and a new key table to decipher (RC4) the configuration file that the trojan requests from the remote server.

The new key table is stored inside the small table at a variable offset.

Due to polymorphic nature of the trojan, the locations of the large permutation table, encrypted small table and the offset of the key inside the decrypted small table are random.

Nevertheless, these random values are recoverable from the heap memory of any process infected with ZeuS.

In order to decrypt configuration files of ZeuS 2.0 on a host infected with ZeuS (e.g. under a virtual machine), a special tool can be built.

The tool would firstly need to identify ZeuS heap pages with the signatures and then check for the presence of the following code within the same ZeuS page:

// 55                    push    ebp
// 8B EC                 mov     ebp, esp
// 51                    push    ecx
// A1 ?? ?? ?? ??        mov     eax, ds:image_base
// 8B 0D ?? ?? ?? ??     mov     ecx, ds:dwSmallTableOffsetVA
// 56                    push    esi
// 8D 34 01              lea     esi, [ecx+eax]
// A1 ?? ?? ?? ??        mov     eax, ds:XX
// 8B 0D ?? ?? ?? ??     mov     ecx, ds:dwLargeTablePtrVA
// 89 4D FC              mov     [ebp+large_table_ptr], ecx
// 83 F8 02              cmp     eax, 2
// 76 41                 jbe     short XX
// 57                    push    edi

The 1st wildcard (??) in the listing is the virtual address of the allocated page within the host process.

The 2nd wildcard is the virtual address of the small table offset within the same injected page; for example, the small table offset could be 0x33000. The first word of that table is the size of the large permutation table, with the actual small table following that word. The size of the small table is constant – it is 700 bytes in size.

The 4th wildcard in the listing is the virtual address of the large permutation table within the infected process. It is normally allocated as a separate heap page within the same host process.

Another offset still needs to be recovered from the identified malicious heap page – it is the offset of the key within the decrypted small table that is used to decipher (RC4) the configuration file itself. The value of this offset varies from 0 to 255.

To locate that offset, the infected memory page can be scanned for the presence of the following code:

// 8B 03                 mov     eax, [ebx]
// 56                    push    esi
// 57                    push    edi
// C6 45 FF 00           mov     [ebp+flag], 0
// 85 C0                 test    eax, eax
// 74 6E                 jz      short quit
// 8B 7B 04              mov     edi, [ebx+4]
// 81 C1 ?? 00 00 00     add     ecx, bKeyOffset
// 51                    push    ecx
// E8 ?? ?? ?? ??        call    dec_rc4_xor
// 89 43 04              mov     [ebx+4], eax
// 85 C0                 test    eax, eax

The key offset is the first wildcard in the listing above.

Once the tables and the key offset are fully recovered from the memory of an infected process, the tool can now decrypt the configuration file by using decryption algorithms derived from ZeuS via reverse engineering.

To assist those researchers who need to decrypt and analyze the contents of the ZeuS 2.0 configuration files, the ZeusDecryptor tool is available for download here.

Wednesday, April 28, 2010

WoW Factor or Back Into Matrix

Online gaming password stealers form a large malware category.

Moreover, it is growing: there is strong demand in the virtual experience, there is supply, there are online auction sites where such experience is sold to those who are ready to pay for it. That is, there are mechanisms for converting the virtual experience into the real money. And then there are bad guys are trying to hook into that chain for their personal gain by trying to compromise online gaming accounts in order to steal the virtual experience and then resell it.

However, why there is demand for the virtual experience in the first place?

What state of mind is required in order to pay several hundred dollars for something as virtual as this:

Why the practicality becomes less important and the virtual assets become more and more appealing up to the point when they are associated with a certain social status? Is it the same force that drives the sales of the sleek, glossy and shiny (but questionable practically) i-gadgets, the same sort of virtuality? Is this some kind of "this is me and I am not part of the crowd" message sent to the rest of the world, an attempt to demonstrate an open mind attitude that dismisses anything dogmatic?

By buying the virtual status in gaming, whether it is virtual gold or a level or experience, what are they trying to say? Is this a way to demonstrate to their friends how keen there are and how far they are prepared to go to gain their own social status in the modern world? But why buying the virtual social status instead of building one physically?

Hmm, this must be our evolution then.

Saturday, January 16, 2010

Trojan.Hydraq - Part II

Previous post described the installation process of the trojan and its backdoor commands.

Now it's time to inspect its connection details, in particular - where does it retrieve the host name of the remote command-and-control (C&C) server.

The source code of the trojan contains a hard-coded host name that is tried out every 5 seconds, but these values must have been used during testing only - they are replaced with the different ones during the runtime - we must establish which ones.

It is also worth noting that the trojan's code is very fragmented - it is deliberately split into small chunks with the size of a few instructions each, connected with the calls and jumps into a large maze: the code of Trojan.Hydraq contains 1,748 jumps and 922 calls - tracing it requires quite a bit of a patience. Graph image of the disassembled source indeed reminds a serpent-like beast - hence, probably, the name.

Hydraq's call-only graph:

The trojan carries its C&C connection details (server, name, port, retry delay, etc.) inside the internal resource (name is 100, type is 243). The resource is 344 bytes in size, and it is encrypted.

Decryption of the resource is performed in 4 stages:

  • The first 8 bytes of the resource are skipped, the remaining 336 bytes are XOR-ed with 0x99

  • Next, every byte from the 336 input buffer is translated according to the following logics:

    • if the byte is a character from 'A' to 'Z', it is subtracted 'A' value (0x41)

    • if the byte is a character from 'a' to 'z', it is subtracted 'G' value (0x47)

    • if the byte is a character from '0' to '9', it is added 4

    • if the byte is a character '+', its replaced with '>'

    • if the byte is a character '/', its replaced with '?'

    • if the byte is a character '=', its replaced with '\0'

    • otherwise, the byte is left as is

  • The input buffer is then converted into the resulting buffer which is 25% smaller. This is achieved by splitting every byte quartet (bytes 0-3) from the input buffer into 3 pairs (0-1, 1-2, 2-3), performing operations over these three pairs, and writing 3 byte results into the output buffer. This way, the 336 bytes will be converted into 252 bytes (4 -> 3 conversion); in some way, it's similar to unpacking all-ASCII base64 back into binary. The operations performed over the 3 pairs are illustrated below:

  • Finally, the resulting 252 bytes are XOR-ed with 0xAB

The fully decrypted resource is shown below:

Knowing this logics, the decryption can now be reconstructed in a stand-alone tool; a function that retrieves and decrypts the resource from the trojan DLL is provided below (should be run from a virus lab as loading the DLL will invoke its entry point):

#define IDD_RES_NAME   100
#define IDD_RES_TYPE 243

void DecodeHydraqResource()
    HMODULE hDll;
    HRSRC hRes;
    HGLOBAL hResLoad;
    BYTE lpBuffer[0x150];
    BYTE lpResult[0x150];
    int iResultOffset;
    int i;
    char szHost[MAX_PATH];
    int dwDelay;
    int dwPort;
    char szAltDnsServer[MAX_PATH];
    char szMessage[MAX_PATH * 4];
    BOOL bOk;

    szHost[0] = '/0';
    dwDelay = 0;
    dwPort = 0;
    szAltDnsServer[0] = '/0';
    bOk = FALSE;
    szMessage[0] = '/0';

    hDll = LoadLibrary(_T("sample.dll"));
    if (hDll)
        if (hRes)
            hResLoad = LoadResource(hDll, hRes);
            if (hResLoad)
                memset(lpResult, 0, 0x150);
                iResultOffset = 0;

                if (SizeofResource(hDll, hRes) == 0x158)
                    memset(lpBuffer, 0, 0x150);
                    memcpy(lpBuffer, (LPBYTE)hResLoad + 8, 0x150);

                    for (i = 0; i < 0x150; i++)
                        lpBuffer[i] ^= 0x99;

                        if ((lpBuffer[i] >= 'A') && (lpBuffer[i] <= 'Z'))
                            lpBuffer[i] -= 'A';
                        else if ((lpBuffer[i] >= 'a') && (lpBuffer[i] <= 'z'))
                            lpBuffer[i] -= 'G';
                        else if ((lpBuffer[i] >= '0') && (lpBuffer[i] <= '9'))
                            lpBuffer[i] += 4;
                        else if (lpBuffer[i] == '+')
                            lpBuffer[i] = '>';
                        else if (lpBuffer[i] == '/')
                            lpBuffer[i] = '?';
                        else if (lpBuffer[i] == '=')
                            lpBuffer[i] = 0;

                    for (i = 0; i < 0x150; i++)
                        lpResult[iResultOffset++] = (lpBuffer[i] * 4) ^ (lpBuffer[i + 1] / 16);
                        lpResult[iResultOffset++] = (lpBuffer[i + 1] * 16) ^ (lpBuffer[i + 2] / 4);
                        lpResult[iResultOffset++] = (lpBuffer[i + 2] * 64) ^ (lpBuffer[i + 3]);
                        i += 3;

                    for (i = 0; i < 0x150; i++)
                        lpResult[i] ^= 0xAB;

                    i = strlen((LPSTR)lpResult);

                    if ((i > 0) && (i < MAX_PATH))
                        strcpy(szHost, (LPSTR)lpResult);
                                lpResult[iResultOffset - 4],
                                lpResult[iResultOffset - 3],
                                lpResult[iResultOffset - 2],
                                lpResult[iResultOffset - 1]);
                        dwPort = *(LPDWORD)(lpResult + iResultOffset - 12);
                        dwDelay = *(LPDWORD)(lpResult + iResultOffset - 8);
                                _T("Remote Host: %s\nAlternative DNS Server: %s\nConnection Port: %d\nDelay between connection attempts: %d sec."),
                        bOk = TRUE;


    if (!bOk)
        MessageBox(NULL, _T("Failed to retrieve any details!"), _T("Error"), MB_OK);
        MessageBox(NULL, szMessage, _T("Success"), MB_OK);


Once the trojan knows its C&C server, it attempts to connect to it via the specified port - e.g. server, port 443.

It starts doing so by trying to resolve its host name first. If this attempt fails, the trojan makes a DNS query by crafting a TCP packet on port 53 of an alternative (legitimate) DNS server, also specified in its resource, in order to resolve the same host name. For example, the analysed sample has alternative DNS server - this is server located in Taiwan.

If Hydraq can't connect to the remote server, it falls asleep for the time specified in its resource (2 minutes), then repeats the beaconing again.

If the connection to remote host on port 443 succeeds, the malware prepares a packet to send - it is 20 bytes in size:

00 00 00 00 00 00 FF FF 01 00 00 00 00 00 00 00 00 00 77 00

The packet is encoded by inverting its bytes:


As soon as the packet is submitted to the live C&C server, it receives the response packet that is also 20 bytes in size. It is encrypted with the XOR 0xCC.

Hydraq decodes the received packet and retrieves the command ID from it - a number from 0 to 18, which is then converted into the backdoor command group - a number from 0 to 10. Conversion rules Command ID -> Backdoor Command Group are shown below:

  • 0 -> 0 (adjust privileges, terminate processes)

  • 1 -> 1 (control services group)

  • 2 -> 2 (remote file execution)

  • 3 -> 3 (registry manipulation group)

  • 4 -> 4 (file system manipulation group)

  • 5 -> 10 (receive more data)

  • 6 -> 5 (system manipulations - power off/reboot/uninstall/clear events)

  • 7 -> 6 (file search)

  • 8 -> 7 (call other components)

  • 9 -> 10

  • 10 -> 8

  • 11 -> 10

  • 12 -> 10

  • 13 -> 10

  • 14 -> 10

  • 15 -> 10

  • 16 -> 10

  • 17 -> 10

  • 18 -> 9 (networks.ics file manipulations)

Next, a specific command from the chosen group is executed. For more details on backdoor groups and commands within them, please check the previous post.

It may be assumed that upon successful connection to the remote C&C server (, the trojan was designed to be able to update itself. A new copy may have a different C&C server specified in its resource (e.g., or as in the last seen sample - in order to survive the shutdown of the old servers.

The presence of a resource that stores all the connection parameters could potentially indicate an intented cloud-based automation for updating the same template with a newly generated resource without the need to recompile the sample, with the obfuscation step added on top of it to evade existing detections. With the relatively high update frequency of such server-side polymorphism, the C&C server shutdown may always fall behind; given the fact the firewalls let the traffic on port 443 through (HTTPS traffic), a heuristic detection of Trojan.Hydraq (added as Trojan.Hydraq!gen1) is a viable option that reliably breaks this vicious circle.

Wednesday, January 13, 2010

Trojan.Hydraq Exposed

The post describes functionality (static analysis) of the trojan that was reported in the recent targeted attacks against some large companies.

Trojan.Hydraq trojan is a DLL that runs as a service within the context of the system process svchost.exe.

In order to be executed within the process svchost.exe at the system startup, the trojan employs no injection techniques - this is achieved with the steps described below.

Firstly, the trojan registers itself as a system service RaS[4 random characters] by creating registry entries under the newly created key:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RaS[4 random characters]

The "ImagePath" value of its service registry key is set to start svchost.exe, as shown below:

"ImagePath" = %SystemRoot%\system32\svchost.exe -k netsvcs
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RaS[4 random characters]

This will force the system process svchost.exe to look up its multi-string value "netsvcs", load all services specified in it into its address space, and then call their ServiceMain() exports.

To make svchost.exe aware of its existence and be loaded too, the trojan adds its service name into the list of strings (service names) stored in the value "netsvcs" of the registry key:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SvcHost

To make sure its service name is added to the list of services only once, the trojan queries the contents of the value "netsvcs" to make sure that the multiple strings stored in that value do not contain any string that starts from "RaS" (case-sensitive).

Other parameters of the newly installed service are specified in the values:

ObjectName = LocalSystem
Type = dword:0x20 (a win32 service that can share a process with other win32 services)
Start = 2 (to be loaded automatically for all startups)

of the registry key:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RaS[4 random characters]

Finally, to let svchost.exe process know where to load the DLL from, the image path of the trojan's service DLL is saved by setting the value:

ServiceDll = [path to trojan DLL]

of the registry key:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RaS[4 random characters]\Parameters

The file name of the trojan DLL is retrieved by calling GetModuleFileNameA() API, as the trojan knows its name may vary.

For instance, the trojan can create a copy of itself under a random filename in the %TEMP% directory; if it locates a file %TEMP%\c_1758.nls, it may rename that file under a different file name.

NOTE: %TEMP% is a variable that refers to the temporary folder in the short path form. By default, this is C:\Documents and Settings\[UserName]\Local Settings\Temp\ (Windows NT/2000/XP), or C:\User\[UserName]\AppData\Local\Temp (Windows Vista, Windows 7).

The Hydraq trojan installs a backdoor trojan that listens for incoming commands. The commands allow the trojan to perform multiple actions - the trojan organizes them into groups - these commands are enlisted below with the [group number].[internal command number] prefixes:

  • [0.0] adjust token privileges

  • [0.1] terminate processes

  • [1.0] enumerate name and status for all system services

  • [1.1] control arbitrary services

  • [1.2] query status for arbitrary services

  • [2.0] receive remote file and save it as %TEMP%\mdm.exe, then launch it by using command control program %SYSTEM%\cmd.exe

  • [3.0] enumerate registry keys under the specified key

  • [3.1] enumerate registry values for the specified key

  • [3.3] query registry values

  • [3.4] set registry values conditionally

  • [3.5] set registry values unconditionally

  • [3.6] delete registry keys

  • [3.7] create registry keys conditionally

  • [3.8] create registry keys unconditionally

  • [4.0] retrieve the list of logical drives on a system

  • [4.1] read files from the local file system

  • [4.2] execute arbitrary files

  • [4.3] copy files in the local file system

  • [4.4] delete arbitrary directories

  • [4.5] rename files

  • [4.6] change file attributes

  • [5.1] power off computer

  • [5.2] reboot Windows

  • [5.3] uninstall itself by deleting the registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\RaS[4 random characters]

  • [5.5] clear all system event logs (application, security, and system pools)

  • [6.0] enumerate files in the specified path

  • [7.11] check if %SYSTEM%\acelpvc.dll is present - if it is present, load it and call its EntryMain() export; check the presence of the DLL %SYSTEM%\VedioDriver.dll

  • [9.1] open the file %SYSTEM%\drivers\etc\networks.ics and read 616 bytes from it

  • [9.2] delete the file %SYSTEM%\drivers\etc\networks.ics

In addition to the commands enlisted above, the trojan retrieves CPU speed by querying the "~MHz" value from the registry key:

The stolen details are then delivered to the remote site.

Hydraq trojan is capable to keep inter-process communications with other components via a named pipe - a separate thread is spawned for that purpose.

Internal data or configuration is stored by the trojan in the values "IsoTp" and "AppleTlk" in the dedicated registry key:

Continued in part II.