JanelaRat reversing

Found another new sample on MalwareBazaar (sha256:93c1733e9d5d2ecfc6e742308ef02d52e644f36867d2718015e90e966ff30ec4) which I took a look at.

What caught my attention was the encryption they place on their strings so I went deep-diving into it. Found out, how it decrypts the strings using an RSA-decryption with the corresponding private key in every file hardcoded for its’ strings.

What was strange was that many resulting strings were base64 which made no sense so I kept going…obviously xD

Further investigation brought me to an AES-decryption which used exactly these base64 strings. So I went for a hunt for interesting dotnet-dll calls like Connect() or similar on Sockets and usages of variables which sound familiar like ‘Host’ and ‘Port’ which I also successfully found.

I could encrypt the 2 places which are used for C2 communication and payload delivery.

I’ll post the scripts I did for it and later add a deeper dive into it, how I found out everything.

RSA decryption:

import base64
import os
import re
import traceback
import xml.etree.ElementTree as ET
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.PublicKey.RSA import RsaKey


def rsa_from_xml(xml_str: str) -> RSA.RsaKey:
    """
    Convert Microsoft-style RSA XML key into a PyCryptodome RSA key.
    """
    root = ET.fromstring(xml_str)

    def b64_to_int(tag):
        node = root.find(tag)
        if node is None:
            return None
        return int.from_bytes(base64.b64decode(node.text), byteorder="big")

    n = b64_to_int("Modulus")
    e = b64_to_int("Exponent")
    d = b64_to_int("D")
    p = b64_to_int("P")
    q = b64_to_int("Q")

    # Construct private key
    key = RSA.construct((n, e, d, p, q))
    return key

def decrypt_with_xml_key(rsa_key: RsaKey, ciphertext_b64: str) -> str:
    cipher = PKCS1_OAEP.new(rsa_key)  # <-- OAEP padding
    ciphertext = base64.b64decode(ciphertext_b64)
    plaintext = cipher.decrypt(ciphertext)
    return plaintext.decode("utf-8")

def extract_rsa_key(file_content: str) -> str:
    """Extract the XML key from a line like `string text = "…";`"""
    match = re.search(r'string\s+text\s*=\s*"(.+?)";', file_content, re.DOTALL)
    if match:
        return match.group(1)
    return None

def extract_b64_matches(file_content: str) -> list:
    """Extract all first parameters from decrypt_rsa calls."""
    # first renamed all the decryption functions of each class so all can be handled the same 
    return re.findall(r'decrypt_rsa\("([A-Za-z0-9+/=]+)"', file_content)


def process_directory(directory: str):
    with open(os.path.join(directory, "outputs.txt"), "w") as file:
        encryptions = []
        for root_dir, _, files in os.walk(directory):
            for filename in files:
                filepath = os.path.join(root_dir, filename)
                with open(filepath, "r", encoding="utf-8") as f:
                    content = f.read()

                # Extract RSA key
                raw_xml = extract_rsa_key(content)
                if not raw_xml:
                    continue
                xml_key = raw_xml.replace(r'\r\n', '\n').replace(r'\"', '"')
                rsa_key = rsa_from_xml(xml_key)

                # Extract all base64 ciphertexts
                b64_strings = extract_b64_matches(content)
                for b64 in b64_strings:
                    try:
                        plaintext = decrypt_with_xml_key(rsa_key, b64)
                        print(f"{filename}: {plaintext}")
                        encryptions.append(f"{b64} => {plaintext}")
                    except Exception as e:
                    # print(f"{filename}: Failed to decrypt {b64} -> {e}")
                        continue
                file.write("\n".join(encryptions))
if __name__ == "__main__":
    directory_path = "/investigation/encryptedClasses/"
    process_directory(directory_path)

After this there was a file which held the values. To then find out the URL’s and connections, I went manually searching for the parameter which was used in the connecting functions and pulled the base64values from the RSA encrypted strings file.

For the AES decryption, the algorithm took the first 16bytes of the contributed base64 string which is the IV key. The key itself was hardcoded and was always “8521”.

Here’s the code for the additional decryption logic:

import base64
from hashlib import md5
from Crypto.Cipher import AES

def decrypt_strings(input_b64: str) -> str:
    key = "8521"
    # Decode input
    input_bytes = base64.b64decode(input_b64)

    # Extract IV (first 16 bytes)
    iv = input_bytes[:16]

    # AES ciphertext is the rest
    ciphertext = input_bytes[16:]

    # Generate MD5 key from key
    key = md5(key.encode("utf-8")).digest()

    # Create AES cipher in CBC mode
    cipher = AES.new(key, AES.MODE_CBC, iv)

    # Decrypt
    decrypted = cipher.decrypt(ciphertext)

    # Remove PKCS7 padding
    pad_len = decrypted[-1]
    if isinstance(pad_len, str): 
        pad_len = ord(pad_len)
    decrypted = decrypted[:-pad_len]

    return decrypted.decode("utf-8")

if __name__ == "__main__":
    encrypted_b64 = [
        "wUjRMYQeV86c+SlSnhRPFtcnggAbEdBGZUf6yYk5oqEJwpvaRx9Zez6m1wBASwQ3rY1sGcFQiTMMREYvu3nuiA==",
        "bMqWKzId9jnW0+0GXWoj2M2Z0C4+m9QTTdXGDruV8ms=",
        "ENKRphQh63tdspRDhQHw1LUx6GmnDtaINPoXI3r4yvY=",
        "t04Wloctcf6qW6eZRwdM2dqVCLKz76PBjxoliI4Lr8Y=",
        "FRviEC0n/N4+2AwLUvqsG68H4biL3/f1MGMfyvTFpzc=",
        
        "DJAkTf6YraQfNl5HPDYcvXX2I4UmS1KisMXADhVQdu4cPCmGbCd0+U54XoDOJVB87elmOHYRHnOy445t1QBagJWULRN/nPu7JMpekGaamn1CYpWtlRHcBuWGsjMkpp+KGICYlmHHc8EbDsrczyXkDTQAgvs6+k9CfLXkvni1iEVtGM5gI8AApqq7gsJSUjHPyVSU6zfOu0KHN6UGp2nlmigUEPzXmhtvh5M3CNFZuKbKjU4XP1Ib5WPtie/R8VYEtMB6gxDu/XOy8CMZEJ7fZ5DSr/ff8qt+3pPCVw2vtZRhsBmbOvr+4oYmO1PbaI6K9iFE5BTA9vcCwRauYrYfw9J5PnE8HHrTeF5UCVFRG7s6yePKfwVDTS5QbuE1DDBHiAL/NrXfPetShMV5nMI8b5cb+L+VZt5p6pyeOEnjLtKY3X7DtJxTa85cvmQvZJEYkMECNgXP19SqIKi63bTKkb9EyD+zaahlR4w6mJMoncYFGqNHWb24G+qOWTWb6zab00sM/HfIU3GAuTjaStFfmFGEME76rJkVCQwtapD8cdOA67TbBbNo56NCT4CjVZ9Yr7EgQZM5zpU14PtwO0Ix5i502RznAkls+4tR8FGyghiqQmS65LXGGY3EG4E02UnRNPnNOi9VCNAImX6GiQYXL/+Sm3nikIeMJi4Ho9ApQ7I="
    ]
    for val in encrypted_b64:
        plaintext = decrypt_strings(val)
        print(plaintext)
        

The hardcoded b64 strings are the one’s extracted from the code. the first 5 represent a payload delivery url. and the last is a ‘|’ separated string where element at [2] and [3] are used and represent Host:port.


IOCs

TypeIOC
Delivery URLhxxp[://]68[.]154[.]1[.]115/CS/index[.]php?VS=V1[.]0&PL=SIM
C2 URLcabunca.mypets.ws:443

Seeya next time!

Take care.

R4ruk