2026-03-13
1989 words
10 minutes
Decrypting and Abusing Predefined BIOCs in Palo Alto Cortex XDR

Summary#

The Windows Cortex XDR agent uses CLIPS rules for behavioral detections. These rules are shipped encrypted in content updates. We decrypted these rules and discovered numerous hardcoded exceptions including global whitelists that can be abused to bypass those rules. For example, if a process’s command-line arguments contain :\Windows\ccmcache, it is excluded from about half of all behavioral detections. This allows an attacker, for example, to dump LSASS using simple, well-known techniques without detection.

We initially looked at the Cortex Windows agent versions 8.7 and 8.8 with content version 1790-16658. Palo Alto removed the global whitelists in Agent Version 9.1 with content version 2160 at the end of February 2026.

Plaintext rules and decryption scripts are available on GitHub.

Introduction#

EDR/XDR agents such as Palo Alto Cortex XDR use different methods for detection, such as static signature-based detections, local file analysis, or machine-learning-based detections. Among the most important of these are the Behavioral Indicators of Compromise (BIOCs) rules. During a recent red team engagement, we noticed that Cortex XDR agent 8.8 detected several techniques in our payload that were previously undetected. The Cortex portal provides limited details on what exactly triggered the alert. With some exceptions, such as Elastic or HarfangLab, most vendors maintain a closed detection rule set. Therefore, we tried to access the actual predefined rules, which are regularly updated through content updates.

Finding and Decrypting the Rules#

The content updates include numerous files:

ls content-1790-16658
ace-sandbox-plugin-dev-win-x86_64.zip          network_inspect_policies_encrypted-64.zip
ace-sandbox-plugin-no-ocr-prod-win-x86_64.zip  network_inspect_policies_encrypted_7_5-64.zip
ace-sandbox-plugin-prod-win-x86_64.zip         network_inspect_policies_encrypted_pre_2025_ev-64.zip
agent_scripts_win_arm64.zip                    network_inspect_policies_slowroll-64.zip
agent_scripts_win_x64.zip                      network_inspect_policies_threat_hunting.zip
agent_scripts_win_x86.zip                      network_inspect_pre_2025_ev-64.zip
cert.zip                                       network_inspect_slowroll-64.zip
collector-64-legacy.zip                        nmap-win32.zip
collector-64.zip                               payload-32-one-dir-openssl3.zip
content_processed.zip                          payload-32-one-dir.zip
content_raw.zip                                payload-32.zip
cwp-compliance.zip                             payload-64-one-dir.zip
cyber.zip                                      payload-64.zip
dse_config.zip                                 payload-win-arm64-one-dir.zip
dse_ng.zip                                     payload-win-x86_64-one-dir-openssl3.zip
dse.zip                                        payload-win-x86_64-one-dir.zip
edrdotnet_slowroll.zip                         periodic_scripts.zip
edrdotnet.zip                                  pki-fedramp.zip
env.zip                                        pki-gov-fedramp.zip
infra.zip                                      pki.zip
ivanti-content-win.zip                         recognizer_plugin_32.zip
ivanti-deltas-win.zip                          recognizer_plugin_64.zip
local_analysis_plugin_32.zip                   recognizer_plugin_arm64.zip
local_analysis_plugin_64.zip                   threat_hunting_ng.zip
local_analysis_plugin_arm64.zip                threat_hunting.zip
manifest.json                                  tsf_collector.zip
ml_plugin_32.zip                               va-scanner-winx64.zip
ml_plugin_64.zip                               va-scanner-winx86.zip
ml_plugin_8_4_32.zip                           winpmem-32.zip
ml_plugin_8_4_64.zip                           winpmem-64.zip
ml_plugin_arm64.zip                            xdrhealth-32.zip
model_doc_ole_7_2.zip                          xdrhealth-64.zip
model_doc_openXml_7_2.zip                      xdrhealth-ARM64.zip
model_js_8_8.zip                               yara_plugin_32.zip
model_macro_7_1.zip                            yara_plugin_64.zip
model_pe_7_1.zip                               yara_plugin_8_4_32.zip
model_powershell_7_3.zip                       yara_plugin_8_4_64.zip
model_powershell_8_4.zip                       yara_plugin_arm64.zip
model_vbs_8_6.zip                              yara_signature_rules_encrypted_mitre.zip
network_inspect-64.zip                         yara_signature_rules_encrypted.zip
network_inspect_7_5-64.zip

These files are unzipped into C:\ProgramData\Cyvera\LocalSystem\Download\content\. As already pointed out by this blog post from SafeBreach, the metadata of behavioral rules is stored in the file dse_rules_config.lua. The content of this file could already be used to avoid Mimikatz detections by abusing whitelists, as described in the referenced blog post. However, the actual detection logic and other exclusions are not included in this file.

The content looks like this:

["bioc.ntsystemdebugcontrol_get_eprocess"] = {
    versions = {"8_8_0","8_8_0_999"},
    signatureConfiguration =     {
        default =         {
            settings =             {
                action = "silent",
                friendlyName = "ntsystemdebugcontrol_get_eprocess",
                technique_id = {"T1055"},
                tactic_id = {"TA0004","TA0005"},
                mth_action = "report",
                mth_severity = 1,
                severity = 4,
                external_description = "Lsass Dump Attempt",
                module = 2
            }
        }
    }
},

It’s not hard to guess where the rules are stored, however, they are encrypted:

PS C:\ProgramData\Cyvera\LocalSystem\Download\content> ls dse*


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         6/18/2025  12:16 AM         195276 dse.lua
-a----         6/18/2025  12:16 AM          23703 dse_common.lua
-a----         6/18/2025  12:16 AM           3228 dse_config_schema.json
-a----         6/18/2025  12:16 AM         133964 dse_internals.json
-a----         6/18/2025  12:16 AM           2998 dse_modules.json
-a----         6/18/2025  12:16 AM        1308480 dse_rules_8_4_0_windows_encrypt.clp
-a----         6/18/2025  12:16 AM        1308752 dse_rules_8_5_0_windows_encrypt.clp
-a----         6/18/2025  12:16 AM        1311456 dse_rules_8_6_0_windows_encrypt.clp
-a----         6/18/2025  12:16 AM        1313808 dse_rules_8_7_0_windows_encrypt.clp
-a----         6/18/2025  12:16 AM        1319536 dse_rules_8_8_0_windows_encrypt.clp
-a----         6/18/2025  12:16 AM        1319920 dse_rules_8_9_0_windows_encrypt.clp
-a----         6/18/2025  12:16 AM       15273694 dse_rules_config.lua

Getting Plaintext Rules#

To identify where and how these files are decrypted, I used ProcMon to get the stack trace for the read operation on the dse_rules_8_7_0_windows_encrypt.clp file:

Fortunately, cysvc.dll includes debug strings, which were used to rename functions in IDA Pro with a small script.

There is a function called dse::ClipsEngineFilter::DecryptClpData() that sounded promising. Using WinDBG as a Kernel-Debugger to avoid EDR’s self-protection mechanisms, I set a breakpoint at this function. I then analyzed the decompiled code to infer the purpose of the function’s four input parameters. We found that LLMs are quite effective at helping to interpret decompiled code and suggest the purpose of the parameters.

As shown in the following, the first parameter is the encrypted content:

kd> dps rcx L1
000001ff`12ff4f80  000001ff`27c90020
kd> db 000001ff`27c90020
000001ff`27c90020  6f c4 62 6e dd 4e d9 99-16 32 5a e2 0a e4 b5 07  o.bn.N...2Z.....
000001ff`27c90030  cf 78 be c5 ec 7d 8f c6-29 5a f4 69 a9 fc 79 cb  .x...}..)Z.i..y.
000001ff`27c90040  29 42 7c 9d d4 93 09 ff-d3 00 90 e5 f1 7d f7 ba  )B|..........}..
000001ff`27c90050  b1 e7 07 f4 a8 2d c5 00-62 95 f5 cc 0e ff 48 5e  .....-..b.....H^
000001ff`27c90060  bd d3 e4 73 65 12 96 31-aa 0c 27 d0 70 8b b6 84  ...se..1..'.p...
000001ff`27c90070  7c bf e2 3e 80 7d d8 2a-a0 c3 29 5e c4 3e b8 d8  |..>.}.*..)^.>..
000001ff`27c90080  9f 62 01 58 a0 3f 26 38-97 b1 f8 e2 15 c2 7f d2  .b.X.?&8........
$ hexdump -C dse_rules_8_7_0_windows_encrypt.clp
00000000  6f c4 62 6e dd 4e d9 99  16 32 5a e2 0a e4 b5 07  |o.bn.N...2Z.....|
00000010  cf 78 be c5 ec 7d 8f c6  29 5a f4 69 a9 fc 79 cb  |.x...}..)Z.i..y.|
00000020  29 42 7c 9d d4 93 09 ff  d3 00 90 e5 f1 7d f7 ba  |)B|..........}..|
00000030  b1 e7 07 f4 a8 2d c5 00  62 95 f5 cc 0e ff 48 5e  |.....-..b.....H^|
00000040  bd d3 e4 73 65 12 96 31  aa 0c 27 d0 70 8b b6 84  |...se..1..'.p...|
00000050  7c bf e2 3e 80 7d d8 2a  a0 c3 29 5e c4 3e b8 d8  ||..>.}.*..)^.>..|
00000060  9f 62 01 58 a0 3f 26 38  97 b1 f8 e2 15 c2 7f d2  |.b.X.?&8........|

The second parameter includes a JSON document with part of the decryption key while the third parameter is not related to the encryption.

The fourth parameter contains the decrypted content upon the function’s completion:

kd> dps r9 L1
00000080`a47fd880  00000000`00000000
kd> pt
cysvc!CySvcServiceHandler+0x89a724:
0033:00007ffc`4f50e3b4 c3              ret
kd> dps 00000080`a47fd880
00000080`a47fd880  000001ff`18800020
kd> db 000001ff`18800020
000001ff`18800020  28 64 65 66 74 65 6d 70-6c 61 74 65 20 69 6e 74  (deftemplate int
000001ff`18800030  65 72 6e 61 6c 2e 64 65-62 75 67 5f 62 75 69 6c  ernal.debug_buil
000001ff`18800040  64 5f 74 69 6d 65 73 74-61 6d 70 20 28 73 6c 6f  d_timestamp (slo
000001ff`18800050  74 20 63 69 64 29 20 28-73 6c 6f 74 20 70 72 69  t cid) (slot pri
000001ff`18800060  6f 29 20 28 73 6c 6f 74-20 74 69 6d 65 73 74 61  o) (slot timesta
000001ff`18800070  6d 70 29 20 28 73 6c 6f-74 20 62 75 69 6c 64 5f  mp) (slot build_
000001ff`18800080  74 69 6d 65 73 74 61 6d-70 29 29 0a 28 64 65 66  timestamp)).(def
000001ff`18800090  74 65 6d 70 6c 61 74 65-20 64 69 72 65 63 74 6f  template directo
kd> .writemem C:\tmp\dse_decrypt_dump.txt 000001ff18800020 000001ff18800020+00852647

The plaintext CLIPS rules were then dumped to a file using .writemem. The decrypted rules are available on GitHub.

Getting Decryption Keys#

Identifying the algorithm, key, and IV was relatively straightforward with assistance from an LLM.

A hardcoded string is present in the binary, but only a portion of it is used as the key (maybe this is supposed to be a obfuscation technique):

Another part is in a lua file which is stored on disk in plaintext:

These two strings are combined to the following key material:

Algorithm: aes-256-cbc
Key: gstHaxzjfuS1u=EsVU#QbD#d@MPhj!58
IV:  HXZnRN5f*Gg$akQD

Using CyberChef, it can be confirmed, that the key material is correct:

The keys seem to be the same across tenants and versions.

Abusing Exceptions and Whitelists#

Many of the rules have simple exceptions that can be abused to bypass them. The following is an example of how to use a well-known method to dump LSASS using ProcDump from SysInternals.

Usually, this activity is detected and prevented:

Now we can check the rule which was triggered:

(defrule bioc.sync.procdump.1
  (read_virtual_memory
    (cid ?cid)
    (instance_id ?instance_id)
    (user_generic_value1 ?user_generic_value1_4
      &: (and
            (integerp ?user_generic_value1_4)
            (eq (bit_and ?user_generic_value1_4 ?*Is_lsass_process*) ?*Is_lsass_process*)
            (eq (bit_and ?user_generic_value1_4 ?*is_werfault_process*) 0)))
    (bytes_copied 4)
    (base_address ?base_address_4)
    (buffer "40000000")
    (timestamp ?timestamp_4))
  (read_virtual_memory
    (cid ?cid)
    (instance_id ?instance_id)
    (user_generic_value1 ?user_generic_value1_3
      &: (and
            (integerp ?user_generic_value1_3)
            (eq (bit_and ?user_generic_value1_3 ?*Is_lsass_process*) ?*Is_lsass_process*)
            (eq (bit_and ?user_generic_value1_3 ?*is_werfault_process*) 0)))
    (bytes_copied 4)
    (base_address ?base_address_3
      &: (eq (bit_and ?base_address_4 -4294967296) (bit_and ?base_address_3 -4294967296)))
    (buffer "68000000")
    (timestamp ?timestamp_3
      &: (and
            (>= ?timestamp_4 ?timestamp_3)
            (is_in_time_frame 20000000 ?timestamp_4 ?timestamp_3))))
  (read_virtual_memory
    (cid ?cid)
    (instance_id ?instance_id)
    (user_generic_value1 ?user_generic_value1_2
      &: (and
            (integerp ?user_generic_value1_2)
            (eq (bit_and ?user_generic_value1_2 ?*Is_lsass_process*) ?*Is_lsass_process*)
            (eq (bit_and ?user_generic_value1_2 ?*is_werfault_process*) 0)))
    (bytes_copied 8)
    (base_address ?base_address_2
      &: (eq (bit_and ?base_address_3 -4294967296) (bit_and ?base_address_2 -4294967296)))
    (buffer ?buffer_2
      &: (eq ?base_address_2 (parse_hex (hex_reverse_byte_order ?buffer_2))))
    (timestamp ?timestamp_2
      &: (and
            (>= ?timestamp_3 ?timestamp_2)
            (is_in_time_frame 20000000 ?timestamp_3 ?timestamp_2))))
  (read_virtual_memory
    (cid ?cid)
    (instance_id ?instance_id)
    (user_generic_value1 ?user_generic_value1_1
      &: (and
            (integerp ?user_generic_value1_1)
            (eq (bit_and ?user_generic_value1_1 ?*Is_lsass_process*) ?*Is_lsass_process*)
            (eq (bit_and ?user_generic_value1_1 ?*is_werfault_process*) 0)))
    (bytes_copied 8)
    (base_address ?base_address_1
      &: (eq (bit_and ?base_address_2 -4294967296) (bit_and ?base_address_1 -4294967296)))
    (timestamp ?timestamp_1
      &: (and
            (>= ?timestamp_2 ?timestamp_1)
            (is_in_time_frame 20000000 ?timestamp_2 ?timestamp_1))))
  (or
    (process_start
      (cid ?cid)
      (instance_id ?instance_id)
      (file_info_description ?file_info_description)
      (command_line ?command_line)
      (file_info_original_name ?file_info_original_name)
      (process_image_path ?process_image_path
        &: (and
              (not
                (or
                  (and
                    (contains_one_of ?process_image_path "windows\\system32" "windows\\syswow64")
                    (starts_with (file_name ?process_image_path) "werfault"))
                  (contains_one_of (lowcase ?file_info_description) "windows problem reporting" "windows fault reporting")))
              (not
                (and
                  (or
                    (eq ?file_info_original_name "rdrleakdiag.exe")
                    (eq (file_name ?process_image_path) "rdrleakdiag.exe"))
                  (not
                    (contains_one_of ?command_line "-fullmemdmp" "/fullmemdmp" "-memdmp" "/memdmp")))))))
    (not (process_start (cid ?cid) (instance_id ?instance_id))))
  (not (bioc.sync.procdump.1 (cid ?cid) (instance_id ?instance_id)))
  (internal.debug_build_timestamp)
  (not (internal.global_whitelist (cid ?cid)))
  =>
  (assert (bioc.sync.procdump.1 (cid ?cid) (instance_id ?instance_id))))

There are several exceptions for werfault such as:

(contains_one_of (lowcase ?file_info_description) "windows problem reporting" "windows fault reporting")))

After adding this file info description to procdump64.exe (renamed to test.exe), this rule no longer triggers:

However, another rule was triggered because several rules exist to prevent dumping LSASS. But the rule above contains another interesting exception that allows us to reach the goal faster than bypassing several rules individually:

(not (internal.global_whitelist (cid ?cid)))

The global_whitelist is defined as:

(defrule internal.global_whitelist
  ?the_f <- (process_start (cid ?cid) (command_line ?command_line &: (contains_one_of ?command_line ":\\windows\\ccmcache\\")))
  (not (internal.global_whitelist (cid ?cid)))
  =>
  (assert (internal.global_whitelist (cid ?cid) (prio ?*retention_priority_higher*) (dbg_fact_id (fact-index ?the_f)))))

The bypass is remarkably simple:

An implant started with this command line could do almost anything on the host without triggering the Cortex detection and prevention rules.

Disclosure Timeline#

DateEvent
2025‑07‑28We reported the abuse of the global whitelists to Palo Alto Networks.
2025‑08‑11The Director of Offensive Security at Palo Alto Networks contacted us and asked for a delay of the 90-day disclosure policy, as well as more detailed information about the decryption of the rules.
2025‑08‑20The Director of Offensive Security at Palo Alto Networks requested a meeting to discuss the timeline
2025‑09‑01Meeting with Palo Alto. They want to publish a fix in February 2026. We granted the extended time to protect Cortex customers from an abuse of global whitelists.
2026‑01‑09Palo Alto is on track to publish a fix by the end of February.
2026‑02‑24We asked for a final date.
2026‑02‑24Palo Alto responded that a fix is already published in Cortex XDR Agent version 9.1 and Content version 2160.

Analysing the Fix#

The old encryption keys no longer work to decrypt the rules. However, the string !7H1@j@%J8VqP9c7SgstHaxzjfuS1u=EsVU#QMKHgogc*X8*sXoho2*t&paX5NJ@ in cysvc.dll from which a part of the key was derived is still the same. The second part and the “derivation” function changed slightly.

The actual issue, however, was the global whitelists in the decrypted rules. They fully removed the global whitelists. Spawning an implant that bypasses all kinds of rules at the same time is no longer possible. Bypassing individual rules, however, is of course still a lot easier when knowing the rules. Most of them have exceptions which can be abused.

Conclusion#

There will always be a cat-and-mouse game between offensive and defensive security. A common approach in malware development is to create new techniques to bypass EDR agents, for example, by using new methods for shellcode injection. This post describes another method for evading detections when targeting specific EDRs. About half of the predefined behavioral detection rules in Cortex XDR could be bypassed by simply adding the command line argument :\Windows\ccmcache to the process that executes malicious actions. These rules are not directly accessible in plaintext, which can be compared to a “security by obscurity” approach. This raises the question of whether vendors’ detection rules should be hidden or open. For example, Elastic and HarfangLab use open rules, but this creates a trade-off, as less-skilled attackers can also inspect them. Defenders should know their tools and not blindly trust a black-box detection and prevention solution.

Decrypting and Abusing Predefined BIOCs in Palo Alto Cortex XDR
https://labs.infoguard.ch/posts/decrypting-and-abusing_paloalto-cortex-xdr_behavioral-rules_biocs/
Author
Manuel Feifel
Published at
2026-03-13