Overview

Private Loader is a pay-per-install (PPI) malware that is used to download and execute... more malware! There are there is a loader component and a main component, both written in C++.

Samples

  • Unpacked 1aa2d32ab883de5d4097a6d4fe7718a401f68ce95e0d2aea63212dd905103948 malshare
  • Zscaler analysis aa2c0a9e34f9fa4cbf1780d757cc84f32a8bd005142012e91a6888167f80f4d5

References

String Decryption

There are encrypted stack strings that are composed of the string data, and an accompanying XOR key. These are loaded onto the stack, then directly XOR decrypted.

X-Junior IDA Script

X-Junior has a script that we can try in IDA to decrypt these strings: GitHub Repo.

Andre Tavares Python Script

andretavare5 has a python script using capstone to decrypt the strings: Script Gist.

We have created our own hybrid of the two, which uses capstone for disassembly, but implements the logic from the IDA script...

def unhex(hex_string):
    import binascii
    if type(hex_string) == str:
        return binascii.unhexlify(hex_string.encode('utf-8'))
    else:
        return binascii.unhexlify(hex_string)

def tohex(data):
    import binascii
    if type(data) == str:
        return binascii.hexlify(data.encode('utf-8'))
    else:
        return binascii.hexlify(data)
import pefile
import struct
from capstone import *
from capstone.x86 import *

SAMPLE_PATH = '/tmp/private.bin'


def is_ascii(s):
    return all(c < 128 or c == 0 for c in s)


def get_data(instructions):


    data_chunks = []
    count = 0
    steps = 0
    steps_flag = 0
    flag_reg = 0
    search_count = 0
    search_limit = 400
    
    for inst in instructions:
        steps +=1
        
        if search_count > search_limit:
            break
        search_count += 1
         
        if  inst.mnemonic == 'call':
            break

        if  inst.mnemonic == 'mov' and  inst.operands[0].type == X86_OP_REG  and  inst.operands[1].type == X86_OP_IMM:
            flag_reg = 1
         
        if  inst.mnemonic == 'mov' and ( (inst.operands[0].type == X86_OP_MEM and inst.operands[0].value.mem.disp != 0) or inst.operands[0].type == X86_OP_REG ) and inst.operands[1].type == X86_OP_IMM:
            imm_value = inst.operands[1].value.imm
            #print(hex(imm_value))
            if imm_value & 0xff000000 == 0:
                break
            data_chunk = struct.pack('<I',imm_value)
            data_chunks.append(data_chunk)

            count += 1
            steps = 0
            steps_flag = 1

        if steps == 16 and steps_flag:
            break
         #if steps == 6 and steps_flag:  # if you got some garbage string use this instead of the above
            #break
       
    
    enc_data = data_chunks[0:count//2][::-1]
    key = data_chunks[count//2:count][::-1]
    
    if flag_reg :
        enc_data = sum(zip(enc_data[1::2], enc_data[::2]), ())
        key = sum(zip(key[1::2], key[::2]), ())
    return b''.join(enc_data),b''.join(key)


        
filename = SAMPLE_PATH

# disassemble .txt section
pe = pefile.PE(filename)
md = Cs(CS_ARCH_X86, CS_MODE_32) 
md.detail = True
addr = 0
instructions = []
txt = pe.sections[0]


# TODO: we don't seem to be disassembling the full section?!!
image_base = pe.OPTIONAL_HEADER.ImageBase
section_rva = txt.VirtualAddress

for inst in md.disasm(txt.get_data(), image_base + section_rva):
    instructions.append(inst)


# search, build and decrypt strings
strings = []
addr = None
string = ''

for i, inst in enumerate(instructions):
    if inst.mnemonic == 'pxor': #and inst.address == 0x009910F2:
        #try: # possible string decryption found  
        reversed_instruction_list = instructions[:i][::-1]
        encrypted_str, key = get_data(reversed_instruction_list)
#         print(f"str_len: {len(encrypted_str)}, key_len: {len(key)}")
#         print(encrypted_str)
#         print(key)

        out = bytearray(encrypted_str[j] ^ key[j] for j in range(len(key)))
        
        #print(out)
        out = out.replace(b'\x00',b'')
        #print(out.decode('utf-8'))
        
        if is_ascii(out):
            strings.append((inst.address,out.decode('utf-8')))

                           
print(len(strings))
                           
for s in strings:
    print(f'{hex(s[0])} {s[1]}')
    
122
0x9910f2 0.9
0x993a24 rb
0x99451d %X
0x9947ca Unknown
0x9949a1 SOFTWARE\Microsoft\Cryptography
0x9949f4 SOFTWARE\Microsoft\Cryptography
0x994b6e MachineGuid
0x994d62 Unknown
0x994fb5 _
0x99510a _
0x995222 _
0x995edf null
0x9968c1 http://163.123.143.4/proxies.txt
0x996911 http://163.123.143.4/proxies.txt
0x996961 http://163.123.143.4/proxies.txt
0x996c3e :1080
0x996e47 

0x997139 :1080
0x9973c5 :
0x9976f3 .
0x997a39 .
0x997ca1 .
0x997e59 .
0x998817 http://107.182.129.251/server.txt
0x99886a http://107.182.129.251/server.txt
0x9988bd http://107.182.129.251/server.txt
0x998a29 HOST:
0x998cc2 :
0x9991fd pastebin.com/raw/A7dSG1te
0x999250 pastebin.com/raw/A7dSG1te
0x9993e5 HOST:
0x99953d HOST:
0x9999f4 http://wfsdragon.ru/api/setStats.php
0x999a44 http://wfsdragon.ru/api/setStats.php
0x999a94 http://wfsdragon.ru/api/setStats.php
0x999f77 HOST:
0x99a20f :
0x99a8e4 softs-portal.com/api/registerUser.php
0x99a937 softs-portal.com/api/registerUser.php
0x99a98a softs-portal.com/api/registerUser.php
0x99ae70 HOST:
0x99b108 :
0x99b52f 163.123.143.12
0x99b9d1 telegram.org
0x99bb06 twitter.com
0x99bc38 yandex.ru
0x99bd76 google.com
0x99c4d7 data=
0x99c79a /service/communication.php
0x99c7ea /service/communication.php
0x99c8f3 http://
0x99d561 ipinfo.io/widget
0x99d5b4 ipinfo.io/widget
0x99d9b4 country
0x99db38 country
0x99dedd db-ip.com
0x99e0fd data-api-key="
0x99e39a /self
0x99e536 api.db-ip.com/v2/
0x99e589 api.db-ip.com/v2/
0x99eb4b countryCode
0x99eccf countryCode
0x99f172 www.maxmind.com/geoip/v2.1/city/me
0x99f1c2 www.maxmind.com/geoip/v2.1/city/me
0x99f212 www.maxmind.com/geoip/v2.1/city/me
0x99f588 country
0x99f70c country
0x99f8a8 country
0x99f9e4 iso_code
0x99fb71 country
0x99fcad iso_code
0x99ff09 GetIP
0x9a008b IP:
0x9a01d5 IP:
0x9a0730 api.ipgeolocation.io/ipgeo?include=hostname&ip=
0x9a0783 api.ipgeolocation.io/ipgeo?include=hostname&ip=
0x9a07d3 api.ipgeolocation.io/ipgeo?include=hostname&ip=
0x9a0b97 country_code2
0x9a0d1b country_code2
0x9a0f78 PowerControl
0x9a1125 PowerControl
0x9a12e9 \PowerControl
0x9a16cd \PowerControl_Svc.exe
0x9a1720 \PowerControl_Svc.exe
0x9a1a79 _old
0x9a1fd6 PowerControl
0x9a21d5 .\
0x9a258f Power monitoring service for your device.
0x9a25e2 Power monitoring service for your device.
0x9a2635 Power monitoring service for your device.
0x9a2759 PowerControl
0x9a29a4 WININET.dll
0x9a2ae7 WINHTTP.dll
0x9a2d74 GetVersion|
0x9a2f56 _old
0x9a30bb _old
0x9a32c8 GetUpdateLink
0x9a360b https://vipsofts.xyz/files/mega.bmp
0x9a365e https://vipsofts.xyz/files/mega.bmp
0x9a36b1 https://vipsofts.xyz/files/mega.bmp
0x9a3869 https://
0x9a3a88 .exe
0x9a3bc2 \
0x9a417d |
0x9a4295 GetLoaderLink|
0x9a44e3 Later
0x9a46ed https://
0x9a490c .exe
0x9a4a46 \
0x9a4f9a open
0x9a5256 " /tn "PowerControl HR" /sc HOURLY /rl HIGHEST
0x9a52a9 " /tn "PowerControl HR" /sc HOURLY /rl HIGHEST
0x9a52fc " /tn "PowerControl HR" /sc HOURLY /rl HIGHEST
0x9a5420 " /tr "
0x9a55e0 schtasks /create /f /RU "
0x9a5633 schtasks /create /f /RU "
0x9a5ba4 " /tn "PowerControl LG" /sc ONLOGON /rl HIGHEST
0x9a5bf7 " /tn "PowerControl LG" /sc ONLOGON /rl HIGHEST
0x9a5c4a " /tn "PowerControl LG" /sc ONLOGON /rl HIGHEST
0x9a5d6e " /tr "
0x9a5f2e schtasks /create /f /RU "
0x9a5f81 schtasks /create /f /RU "