PrivateLoader Triage
Config Extractor for PrivateLoader
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]}')