Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

small intro notes, how to start to make it easier for people to get on board #16

Open
doomedraven opened this issue Nov 22, 2024 · 2 comments
Labels
documentation Improvements or additions to documentation

Comments

@doomedraven
Copy link
Contributor

i have made my small notes to make it easier to start with RKP, would be nice to add it somewhere maybe wiki, or howto.md idk :)
keeping a copy here right now so we can elaborate it better

Intro

  • Rat king parser AKA RKP uses dnfile to handle dotnet. It does nice job to abstract everything.
  • The way of design of the "framework" makes it super easy to extend, once you understand how it works. (c) @doomedraven

Where to start

  • How it works?

    1. Searches for VerifyHash() function by regex pattern. Normally is at the bottom of method which loads config entries. In case of not found, See next step.
    2. Does bruteforce. RKP loops all the Classes and gets all .cctor methods.
  • Once we have method(s):

    • We loop them and apply config_item.py types to extract the field names and values. If we match encrypted string, we try to brutefoce the decryption to looping all the decryptors available.
    • Availalbe decryptors/deobfuscators can be found here src/rat_king_parser/config_parser/utils/decryptors.
    • Currently we support:
      • AES: CBC, EBC, CFB
      • 3DES ECB
      • XORed

Config entries

  • RKP uses MIN_THRESHOLD_MATCH == 3 for minimal config match,
    • Ensure that your config entries are in this list.
    • Create Pull Request if you found new fork/variant with new entries.
  • To extract all types of config values as bool, int, str, etc RKP uses src/rat_king_parser/config_parser/utils/config_item.py.
    • When you discover another encrypted string pattern. You need to extend detection by adding it to config_item.py. See as example: class EncryptedStringConfigItem(ConfigItem):, add your class to SUPPORTED_CONFIG_ITEMS at the bottom and to src/rat_king_parser/config_parser/rat_config_parser.py to match it as Example:
      • if type(item) in (config_item.EncryptedStringConfigItem, config_item.EncryptedStringConfigItem2):
    • ToDo consider make those regex patterns are list, so we can have one class and loop it.
@jeFF0Falltrades
Copy link
Owner

@doomedraven I love this idea, and thank you very much for putting together some initial notes as someone coming to the project with a fresh perspective.

I'm going to leave this Issue open and develop a feature branch associated with it to put together a CONTRIBUTING.md file for the project.

In it, I will include your notes above, and I will also elaborate on a few points and add some areas that I think are most relevant for those coming in to create new decryptors.

After I get a draft done, I'll ping you back for review.

Thank you!

@jeFF0Falltrades jeFF0Falltrades added the documentation Improvements or additions to documentation label Nov 24, 2024
@doomedraven
Copy link
Contributor Author

lets call it v1, i leave a copy here while we elaborate it in private

[TOC]

RAT KING PARSER + DotNet handling

How it works?

  • Searches for VerifyHash() function by regex pattern. Normally is at the bottom of method which loads config entries. In case of not found, See next step.

  • Does bruteforce. RKP loops all the Classes and gets all .cctor methods using method methods_from_name.

  • Example:

namespace Quasar.Client.Config
{
    // Token: 0x02000087 RID: 135
    public static class Settings
    {
        // Token: 0x0600028B RID: 651 RVA: 0x0000E2F8 File Offset: 0x0000C4F8
        public static bool Initialize()
        {
            if (string.IsNullOrEmpty(Settings.VERSION))
            {
                return false;
            }
            Aes256 aes = new Aes256(Settings.ENCRYPTIONKEY);
            Settings.TAG = aes.Decrypt(Settings.TAG);
            Settings.VERSION = aes.Decrypt(Settings.VERSION);
            Settings.HOSTS = aes.Decrypt(Settings.HOSTS);
            Settings.SUBDIRECTORY = aes.Decrypt(Settings.SUBDIRECTORY);
            Settings.INSTALLNAME = aes.Decrypt(Settings.INSTALLNAME);
            Settings.MUTEX = aes.Decrypt(Settings.MUTEX);
            Settings.STARTUPKEY = aes.Decrypt(Settings.STARTUPKEY);
            Settings.LOGDIRECTORYNAME = aes.Decrypt(Settings.LOGDIRECTORYNAME);
            Settings.SERVERSIGNATURE = aes.Decrypt(Settings.SERVERSIGNATURE);
            Settings.SERVERCERTIFICATE = new X509Certificate2(Convert.FromBase64String(aes.Decrypt(Settings.SERVERCERTIFICATESTR)));
            Settings.SetupPaths();
            return Settings.VerifyHash();
        }

// Token: 0x0600028D RID: 653 RVA: 0x0000E430 File Offset: 0x0000C630
private static bool VerifyHash()

RKP code - dotnetpe_payload.py Explained:

  • In initial code example we can see:
    Token, RID, RVA, File Offset
// Token: 0x0600028B RID: 651 RVA: 0x0000E2F8 File Offset: 0x0000C4F8
public static bool Initialize()
  • _generate_method_list lists all the references and creates object file with all those details
def _generate_method_list(self) -> list[DotNetPEMethod]:
        method_objs = []

        for idx, method in enumerate(self.dotnetpe.net.mdtables.MethodDef.rows):
            method_offset = self.offset_from_rva(method.Rva)

            # Parse size from flags
            flags = self.data[method_offset]
            method_size = 0
            if flags & 3 == 2:  # Tiny format
                method_size = flags >> 2
            elif flags & 3 == 3:  # Fat format (add 12-byte header)
                method_size = 12 + bytes_to_int(self.data[method_offset + 4 : method_offset + 8])

            method_objs.append(
                DotNetPEMethod(
                    method.Name.value,
                    method_offset,
                    method.Rva,
                    method_size,
                    (MDT_METHOD_DEF ^ idx) + 1, # MDT_METHOD_DEF = 0x6000000
                )
            )
        return method_objs
  • Example of the DotNetPEMethod entry:
# dnspy: // Token: 0x0600028B RID: 651 RVA: 0x0000E2F8 File Offset: 0x0000C4F8
# dnspy: public static bool Initialize()
# DotNetPEMethod - name: Initialize. offset: 0xc4f8. rva: 0xe2f8. size: 0xd8. token: 0x600028b.
  • To get function/method token we need to:

    • (MDT_METHOD_DEF ^ idx) + 1
    • Where:
      • MDT_METHOD_DEF = 0x6000000
      • idx is positional id from dnfile.net.mdtables.MethodDef.rows
  • To find function usage/references we can use method_from_instruction_offset:

# Given the offset to an instruction, reverses the instruction to its
# parent Method, optionally returning an adjacent Method using step to
# signify the direction of adjacency, and using by_token to determine
# whether to calculate adjacency by token or offset
    def method_from_instruction_offset(self, ins_offset: int, step: int = 0, by_token: bool = False):  # ->DotNetPEMethod:
    for idx, method in enumerate(self._methods_by_offset):
        if method.offset <= ins_offset < method.offset + method.size:
            return (
                self._methods_by_token[self._methods_by_token.index(method) + step]
                if by_token
                else self._methods_by_offset[idx + step]
            )
    raise ConfigParserException(f"Could not find method from instruction offset {hex(ins_offset)}")
  • We know from dnspy that VerifyHash() method is at offset 0xC630.
// Token: 0x0600028D RID: 653 RVA: 0x0000E430 File Offset: 0x0000C630
private static bool VerifyHash()
  • method_from_instruction_offset will return us:

    • DotNetPEMethod(name='.cctor', offset=50840, rva=58520, size=217, token=100663950) # token 0x600028e
  • In dnspy we see it as at assembly panel (rigth):

> Quasar.Client.Config
    > Settings @0200087
        > Base Type and interfaces
        > Derived Types
        > .cctor(): void @0600028e
  • Where we have the config entries
// Quasar.Client.Config.Settings
// Token: 0x0600028E RID: 654 RVA: 0x0000E498 File Offset: 0x0000C698
// Note: this type is marked as 'beforefieldinit'.
static Settings()
{
	Settings.VERSION = "gXqfa1E9yGniW4VWnl5FkeenOtkCPXVE52Tt9fhCNeAuqr6bEymGEbHpfYcw7FO6QFvadQBuujgGhJdIBeXZ/g==";
	Settings.HOSTS = "QXCMSAn9RQ/VBoWMf6G0O+GrLCzdhbfe4UgztC/EFgRmGjJe0GzV2Ut/Rix1phYfNFmGl4hxwOg9lUaAlgsH2CL8fiALtHoXO+qs4CAe5hA=";
	<redacted for shortness>
}

How to extract config entries?

  • We get method body that loads the configuration with method method_body_from_method.
  • We apply config_item.py where each type of variable has it own regex.
  • Example:
    • BoolConfigItem - b"(\x16|\x17)\x80(.{3}\x04)"
    • ByteArrayConfigItem - rb"\x1f(.\x8d.{3}\x01\x25\xd0.{3}\x04)\x28.{3}\x0a\x80(.{3}\x04)"
    • IntConfigItem - b"(\x20.{4}|[\x18-\x1e])\x80(.{3}\x04)"
    • NullConfigItem - b"(\x14\x80)(.{3}\x04)"
    • SpecialFolderConfigItem - b"\x1f(.)\x80(.{3}\x04)"
    • EncryptedStringConfigItem - b"\x72(.{3}\x70)\x80(.{3}\x04)"
      • Ex: Pay attention to the bytes sequence: 72D4310070 and 807D010004
Settings.VERSION = "gXqfa1E9yGniW4VWnl5FkeenOtkCPXVE52Tt9fhCNeAuqr6bEymGEbHpfYcw7FO6QFvadQBuujgGhJdIBeXZ/g==";
/* 0x0000C6A4 72D4310070   */ IL_0000: ldstr     "gXqfa1E9yGniW4VWnl5FkeenOtkCPXVE52Tt9fhCNeAuqr6bEymGEbHpfYcw7FO6QFvadQBuujgGhJdIBeXZ/g=="
/* 0x0000C6A9 807D010004   */ IL_0005: stsfld    string Quasar.Client.Config.Settings::VERSION
  • Each of those classes has _derive_item_value method to properly convert those matches to required format/data.

  • To enxtract those entries we go to IL with C# view in dnspy and see next :

//     Settings.VERSION = "gXqfa1E9yGniW4VWnl5FkeenOtkCPXVE52Tt9fhCNeAuqr6bEymGEbHpfYcw7FO6QFvadQBuujgGhJdIBeXZ/g==";

	/* 0x0000C6A4 72D4310070   */ IL_0000: ldstr     "gXqfa1E9yGniW4VWnl5FkeenOtkCPXVE52Tt9fhCNeAuqr6bEymGEbHpfYcw7FO6QFvadQBuujgGhJdIBeXZ/g=="
	/* 0x0000C6A9 807D010004   */ IL_0005: stsfld    string Quasar.Client.Config.Settings::VERSION

	//     Settings.HOSTS = "QXCMSAn9RQ/VBoWMf6G0O+GrLCzdhbfe4UgztC/EFgRmGjJe0GzV2Ut/Rix1phYfNFmGl4hxwOg9lUaAlgsH2CL8fiALtHoXO+qs4CAe5hA=";

	/* 0x0000C6AE 7287320070   */ IL_000A: ldstr     "QXCMSAn9RQ/VBoWMf6G0O+GrLCzdhbfe4UgztC/EFgRmGjJe0GzV2Ut/Rix1phYfNFmGl4hxwOg9lUaAlgsH2CL8fiALtHoXO+qs4CAe5hA="
	/* 0x0000C6B3 807E010004   */ IL_000F: stsfld    string Quasar.Client.Config.Settings::HOSTS

	//     Settings.RECONNECTDELAY = 5000;

	/* 0x0000C6B8 2088130000   */ IL_0014: ldc.i4    5000
	/* 0x0000C6BD 807F010004   */ IL_0019: stsfld    int32 Quasar.Client.Config.Settings::RECONNECTDELAY
  • OpCodes.Ldstr Field
Format Assembly Format Description
72 < T > ldstr mdToken Pushes a string object for the metadata string token mdToken.

Example

  • To get Settings.VERSION we use function user_string_from_rva.
    • RPK gets string mdtoken: 72D4310070. Converts to rva/from little endiant = 700031d4
      • 72 is ldstr - docs
      • 700031d4 (reversed/big endiant)
>>> hex(rva) = '0x700031d4'
>>> dnfile.net.user_strings.get(rva ^ MDT_STRING).value = 'gXqfa1E9yGniW4VWnl5FkeenOtkCPXVE52Tt9fhCNeAuqr6bEymGEbHpfYcw7FO6QFvadQBuujgGhJdIBeXZ/g=='
>>> hex(MDT_STRING) = '0x70000000'
>>> hex(rva ^ MDT_STRING) ='0x31d4'
  • TIP: another way to get it:
    # instead of 72D4310070 we just need to use D4310070 (removed 0x72)
    def get_string_from_mdtoken(self, mdtoken: bytes):
        mdtoken = struct.unpack_from("<I", mdtoken)[0] & 0xFFFFFF
        return dnfile.net.user_strings.get(mdtoken).value

Crypto part

  • We need to check if config entries are encrypted.
  • For that we have regex patterns to detect encryption routines. For example:
    • AES:
_PATTERN_AES_KEY_AND_BLOCK_SIZE_AND_ALGO = re.compile(
        rb"[\x06-\x09]\x20(.{4})\x6f.{4}[\x06-\x09]\x20(.{4})\x6f.{4}[\x06-\x09](.)\x6f.{4}|\x11\x01\x20(.{4})\x28.{4}\x11\x01\x20(.{4})\x6f.{4}\x11\x01(.)\x6f.{4}", re.DOTALL,
    )
  • Currently we support following encryption:

    • AES: CBC, EBC, CFB
    • 3DES ECB
    • XORed
  • RKP uses MIN_THRESHOLD_MATCH == 3 for minimal config items match,

    • Ensure that your config entries are in this list.
    • Create Pull Request if you found new fork/variant with new entries.
  • When you discover another encrypted string pattern.

  • You will need to extend detection by adding it to config_item.py.

  • See as example: class EncryptedStringConfigItem(ConfigItem):

    • Add your class to SUPPORTED_CONFIG_ITEMS at the bottom and to src/rat_king_parser/config_parser/rat_config_parser.py. Ensure that your class name startswith EncryptedStringConfigItem. So it will be recognized automatically by in rat_king_parser.py:
if item.__class__.__name__.startswith("EncryptedStringConfigItem"):
  • As example we have 28 ?? ?? ?? ?? in the middle of previos pattern:
class EncryptedStringConfigItem2(ConfigItem):
    def __init__(self) -> None:
        super().__init__("encrypted string", rb"\x72(.{3}\x70)\x28.{4}\x80(.{3}\x04)")

    # Returns the encrypted string's RVA
    def _derive_item_value(self, enc_str_rva: bytes) -> int:
        return bytes_to_int(enc_str_rva)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

When branches are created from issues, their pull requests are automatically linked.

2 participants