From 4e17fc3467f4ef55fd9aa03e4ce42172a1febc02 Mon Sep 17 00:00:00 2001 From: jeFF0Falltrades <8444166+jeFF0Falltrades@users.noreply.github.com> Date: Sat, 23 May 2026 18:50:00 -0400 Subject: [PATCH 1/2] Address review feedback on small_optimizations PR - config_normalization: strip underscores from alias keys at map-build time so "mutex_string" (and similar) actually resolves to "Mutex". Restore unconditional underscore-stripping on the returned key to preserve prior behavior for unrecognized keys. - rat_config_parser._attempt_decryption: replace the unreachable conditional trailing return with an unconditional raise to make the "decrypt-or-raise" invariant explicit. - config_decryptor_aes_with_iv: restore the -> list[bytes] and -> dict[str, str] return type hints that were dropped during reformatting. - config_decryptor_aes_with_iv._get_aes_metadata: document why key_size/block_size/algo are extracted once and shared across all metadata candidates. Co-Authored-By: Claude Sonnet 4.6 --- .../config_parser/rat_config_parser.py | 5 ++--- .../config_parser/utils/config_normalization.py | 15 +++++++++++---- .../decryptors/config_decryptor_aes_with_iv.py | 10 +++++++--- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/rat_king_parser/config_parser/rat_config_parser.py b/src/rat_king_parser/config_parser/rat_config_parser.py index 2ce0d39..b82277f 100644 --- a/src/rat_king_parser/config_parser/rat_config_parser.py +++ b/src/rat_king_parser/config_parser/rat_config_parser.py @@ -171,9 +171,8 @@ def _attempt_decryption(self, item_data: dict[str, Any]) -> dict[str, Any]: logger.debug(f"Decryption failed with decryptor {decryptor} : {e}") self._decryptor = None - if self._decryptor is None: - raise ConfigParserException("All decryptors failed") - return item_data + # If we exit the loop without returning, no decryptor succeeded + raise ConfigParserException("All decryptors failed") def _remap_config( self, decoded_config: dict[str, Any], config_fields_map: dict[int, str] diff --git a/src/rat_king_parser/config_parser/utils/config_normalization.py b/src/rat_king_parser/config_parser/utils/config_normalization.py index 2783c0c..647a1a1 100644 --- a/src/rat_king_parser/config_parser/utils/config_normalization.py +++ b/src/rat_king_parser/config_parser/utils/config_normalization.py @@ -39,16 +39,23 @@ "Group": ("Group", "Groub", "GroupTag", "TAG"), } +# Strip underscores from aliases at build time so the lookup is consistent +# with the underscore-stripped input key (e.g. so "mutex_string" actually +# resolves to "Mutex") _normalized_keys_map = { - alias: k for k, aliases in normalized_keys.items() for alias in aliases + alias.replace("_", ""): k + for k, aliases in normalized_keys.items() + for alias in aliases } # Normalizes config keys/values for easier mapping def check_key_n_value(key: str, value: Any) -> tuple[str, Any]: - key_clean = key.replace("_", "") - if key_clean in _normalized_keys_map: - key = _normalized_keys_map[key_clean] + # Always strip underscores from the returned key to preserve the prior + # behavior of this function for unrecognized keys + key = key.replace("_", "") + if key in _normalized_keys_map: + key = _normalized_keys_map[key] if key in ("Hosts", "Ports") and isinstance(value, str): if value not in ("null", "false"): diff --git a/src/rat_king_parser/config_parser/utils/decryptors/config_decryptor_aes_with_iv.py b/src/rat_king_parser/config_parser/utils/decryptors/config_decryptor_aes_with_iv.py index 16646a6..994ded4 100644 --- a/src/rat_king_parser/config_parser/utils/decryptors/config_decryptor_aes_with_iv.py +++ b/src/rat_king_parser/config_parser/utils/decryptors/config_decryptor_aes_with_iv.py @@ -111,7 +111,8 @@ def _derive_aes_passphrase_candidates(self, key_val: str) -> list[bytes]: # Decrypts encrypted config values with the provided cipher data def decrypt_encrypted_strings( - self, encrypted_strings: dict[str, str]) -> dict[str, str]: + self, encrypted_strings: dict[str, str] + ) -> dict[str, str]: logger.debug("Decrypting encrypted strings...") if self._key_candidates is None: self._key_candidates = self._get_aes_key_candidates(encrypted_strings) @@ -193,7 +194,8 @@ def decrypt_encrypted_strings( # Extracts AES key candidates from the payload def _get_aes_key_candidates( - self, encrypted_strings: dict[str, str]) -> list[bytes]: + self, encrypted_strings: dict[str, str] + ) -> list[bytes]: logger.debug("Extracting AES key candidates...") keys = [] @@ -284,7 +286,9 @@ def _get_aes_metadata(self) -> None: if not self._metadata_candidates: raise ConfigParserException("Could not identify AES metadata") - # Extraction of common metadata + # Key size, block size, and algo are properties of the embedded AES + # class definition, not of any single salt/iter candidate, so they are + # extracted once and shared across all metadata candidates self._key_size, self._block_size, self._aes_algo = ( self._get_aes_key_and_block_size_and_algo() ) From 57b109a070697ff9edd5a65f8e1284d71b84c4f3 Mon Sep 17 00:00:00 2001 From: jeFF0Falltrades <8444166+jeFF0Falltrades@users.noreply.github.com> Date: Sat, 23 May 2026 19:11:37 -0400 Subject: [PATCH 2/2] Fix regression on template samples in AES decryptor PR #44 introduced a `successfully_decrypted_count == 0` guard at the end of `decrypt_encrypted_strings` that raises if no string was successfully decrypted. For template/builder samples (e.g. unconfigured AsyncRAT builds where every config value is a placeholder like `%Anti%`), every value fails the b64/length filter and is passed through unchanged without any decryption being attempted. The guard then raised, causing `_attempt_decryption` to discard the AES decryptor and fall through to the plaintext decryptor, which has no salt -- so `report["salt"]` was reported as "None" instead of the actual extracted salt. Track attempted decryptions separately and only raise when at least one string was actually attempted but every attempt failed. Template samples have zero attempts and now fall through cleanly with the AES decryptor intact, restoring the master behavior of reporting the salt. Verified against all 14 known sample expected outputs. Co-Authored-By: Claude Sonnet 4.6 --- .../decryptors/config_decryptor_aes_with_iv.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/rat_king_parser/config_parser/utils/decryptors/config_decryptor_aes_with_iv.py b/src/rat_king_parser/config_parser/utils/decryptors/config_decryptor_aes_with_iv.py index 994ded4..b6536d1 100644 --- a/src/rat_king_parser/config_parser/utils/decryptors/config_decryptor_aes_with_iv.py +++ b/src/rat_king_parser/config_parser/utils/decryptors/config_decryptor_aes_with_iv.py @@ -118,6 +118,7 @@ def decrypt_encrypted_strings( self._key_candidates = self._get_aes_key_candidates(encrypted_strings) decrypted_config_strings = {} + attempted_decryptions = 0 successfully_decrypted_count = 0 successful_key = None @@ -141,9 +142,11 @@ def decrypt_encrypted_strings( decrypted_config_strings[k] = v continue - # Otherwise, extract the IV from the 16 bytes after the HMAC - # (first 32 bytes) and the ciphertext from the rest of the data - # after the IV, and run the decryption + # Past this point we are actually attempting a decryption + attempted_decryptions += 1 + # Extract the IV from the 16 bytes after the HMAC (first 32 bytes) + # and the ciphertext from the rest of the data after the IV, and + # run the decryption iv, ciphertext = decoded_val[32:48], decoded_val[48:] result, last_exc = None, None @@ -180,7 +183,12 @@ def decrypt_encrypted_strings( logger.debug(f"Key: {k}, Value: {result}") decrypted_config_strings[k] = result - if successfully_decrypted_count == 0: + # Only raise if we actually tried to decrypt at least one string and + # none succeeded. Template/builder samples have 0 attempts (all values + # are placeholders that fail the b64/length filter above), and should + # fall through cleanly so this AES decryptor remains the active one + # and its salt is still reported. + if attempted_decryptions > 0 and successfully_decrypted_count == 0: raise ConfigParserException( "No strings could be decrypted with the available keys" )