Zero2Auto Bi-weekly challenge 5

The below are my notes for looking at Private Loader's string decryption. I'll update this post with the network analysis whenever I have time.

We get the sample from malware bazar and start using Floss:

Floss IDA plugin method

First we make sure we have the same IDA Python and cmd versions:

OA Labs showed a trick on their channel to compare ida python and cmd.exe python using:

import sys
sys.version

In cmd.exe

python --version

We then install floss

pip install flare-floss

Then run:

>python render-ida-import-script.py strings.json > apply_floss.py

We apply the strings to IDA by pressing Shift+F2 ; selecting Python and pasting the script in there and get the below:

This is quick and dirty, the annoying bit is that we don't get the string close to the XOR instruction offsets. It can make it confusing to understand which xor string is which.

Manual approach

WinMain has a couple of XORs to resolve libs

We can decrypt these with cyber-chef or just run the code and set a breakpoint right before the call to LoadLibrary and expect the value in the register or on the stack.

We get: kernel32.dll; WinHTTP.dll; wininet.dll

Further down we see what looks like dynamic imports; We can use this great blog-post from ired.team to understand what is what in this structure:

https://www.ired.team/offensive-security/code-injection-process-injection/finding-kernel32-base-and-function-addresses-in-shellcode

As well as https://www.geoffchappell.com/studies/windows/km/ntoskrnl/inc/api/ntpsapi_x/peb_ldr_data.htm

As a rule of thumb; when you see the offsets 0x7c and 0x78 after fs:30; It's probably dynamic importing (especially if inside a for loop); Below is the graph with commented structures

I then used a script to fix dynamic imports that I shamelessly stole/borrowed from hexorcist ; awesome and underrated youtube channel, can't recommend it enough.

None resolved dynamic imports

After running the script, the above gets annotated and arguments resolved.

Below is Hexorcist's script with our dll's (found through XORs)

import pefile
import idc
import idaapi

dlls = ["kernel32.dll", "WinHTTP.dll", "wininet.dll"]

start_seg = 0
size_seg = 0x5000

for dll in dlls:
	fname = f"c:\\windows\\syswow64\\{dll}"
	print(fname)
    
	pe = pefile.PE(fname)
	base_seg = start_seg
	idc.AddSeg(start_seg,start_seg+size_seg,0,1,0,idaapi.scPub)
	print(dll)
	idc.set_segm_name(start_seg,dll)
	set_segm_attr(start_seg, idc.SEGATTR_TYPE, idaapi.SEG_XTRN)
	set_segm_attr(start_seg, idc.SEGATTR_PERM, idaapi.SEGPERM_READ)
    
	for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
		if exp.name:
			create_dword(base_seg)
			set_name(base_seg,exp.name.decode())
			base_seg += 4
	start_seg+=size_seg

Again, we can also manually debug this and find the resolved import at ebp-0xd4 (here it's kernel32_SetPriorityClass)

And we then see a thread called which start at the offset labeled StartAdress below. More on that in the network section.

I wrote a script to decrypt some of the xors automatically and insert comments on the pxor addresses as Floss would add the string at the subroutine start as shown above. It sort of works but the first letter gets removed sometimes (not sure why). And sometimes it outputs junk as the encrypted string and key don't match. We can fix the missing strings manually by using the Floss strings output or manual debugging (although most of the missing strings are not needed to understand how privateloader works).

import idautils
import re


def convert_2_hex(hex_str:str):
   base16INT = int(hex_str, 16)
   return base16INT


keys=[]
decrypts=[]
key = []
decrypt =[]

is_key = True

print("Running XOR decrypt")

for function_ea in idautils.Functions():
    for ea in idautils.FuncItems(function_ea):
            cmd = idc.GetDisasm(ea)
            mnem = cmd.split(' ')[0]
            if mnem == 'mov':
              right_side = cmd.split(' ')[-1]
              if right_side[-1] == 'h' and len(right_side[:-1]) >=8 and 'FFFFFF' not in right_side and 'EFEFEFF' not in right_side:
                if right_side[0] == '0':
                    right_side = right_side[1:]
                if(len(key) < 4) and is_key:
                  key.append(right_side[:-1])
                elif len(decrypt) < 4:
                  decrypt.append(right_side[:-1])
                else:
                  pass
              if len(key) == 4:
                my_key = key[2] + key[3] + key[0] + key[1]
                keys.append(my_key)
                key = []
                is_key = False
              if len(decrypt) == 4:
                my_dec = decrypt[2] + decrypt[3] + decrypt[0] + decrypt[1]
                decrypts.append(my_dec)
                decrypt = []
                is_key = True
            if mnem == 'pxor' and len(decrypts) > 0 and len(keys) >0:
              for decrypt_part in decrypts:
                a_str = decrypt_part
                for key_part in keys:
                  b_str = key_part
                  xored = ''
                  for i in range(0,len(a_str)-2,2):
                    a = a_str[i] + a_str[i+1]
                    a = convert_2_hex(a)
                    b = b_str[i] + b_str[i+1]
                    b = convert_2_hex(b)
                    xored += chr(a^b)
                  xored = re.sub(r'[^a-zA-Z0-9\\\.\:"]', '', xored)
                  if len(xored.strip().strip("\x00")) >2:
                    print(xored[::-1])
                    idc.set_cmt(ea, xored[::-1],0)
    #              print(mnem)
              decrypts = []
              keys = []
              decrypt =[]
              key =[]

            if mnem == 'retn':
              decrypts = []
              keys = []
              decrypt =[]
              key =[]

That's it! I'll cover the protocol in a future update!

PrivateLoader protocol

Now that we have our imports resolved and strings decrypted, we pretty quickly identify what looks like our Networking function.

The below graph shows a switch-case structure where we compare the value of ecx and move a specific byte at whatever eax points to.

Last updated