SIGS: PackClient stream-aware signatures and Core startup correlation

Hi y’all,

I REd PackClient and identified a TCP stream robustness issue in the existing ET PackClient Launcher signatures.

The current PLH1 / PLC1 / PLA1 / PLK1 detections (ET SIDs 2069878–2069882) rely on packet-boundary assumptions that hold for the observed capture layout, but equivalent PackClient traffic can be segmented or coalesced differently during normal TCP transport.

I reproduced this under Suricata 7.0.3 and prepared stream-aware equivalents that retain the existing PackClient protocol anchors while matching against the reassembled TCP stream.

I also included an optional PackClient Core startup correlation based on traffic recovered during my reverse engineering. It correlates:

SYS|Q|EXT|STARTUP|PROBE|

with:

SYS|R|EXT|STARTUP|OK|tags=

on the same established TCP flow before alerting.

I originally submitted this to support@emergingthreats.net on September 11, but I’m posting it here as well for visibility and community review.

Proposed rules

The SIDs in the rules below are temporary submission/validation identifiers only. For ET SIDs 2069878–2069882, the existing upstream SID/message/reference/metadata can be retained while replacing the packet-boundary-dependent predicates with the corresponding stream-aware logic.

# PackClient — ET Open stream robustness submission
#
# Mapping:
#   9900101 -> ET 2069878 (PLH1 length prefix)
#   9900102 -> ET 2069879 (PLH1 check-in body)
#   9900103 -> ET 2069880 (PLC1 C2 response)
#   9900104 -> ET 2069881 (PLA1 payload request)
#   9900105 -> ET 2069882 (PLK1 C2 response)
#   9900110/9900111/9900112 -> optional Core startup correlation

alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"PackClient PLH1 length prefix stream"; flow:established,to_server,only_stream; content:"|24 00 40 5a|"; fast_pattern; reference:url,www.proofpoint.com/us/blog/threat-insight/carry-compromise-ta4922-packs-packclient; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900101; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"PackClient PLH1 stream"; flow:established,to_server,only_stream; content:"|24 00 40 5a 15 00 00 00|PLH1"; fast_pattern; reference:url,www.proofpoint.com/us/blog/threat-insight/carry-compromise-ta4922-packs-packclient; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900102; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

alert tcp $EXTERNAL_NET any -> $HOME_NET any (msg:"PackClient PLC1 stream"; flow:established,to_client,only_stream; content:"|1c 00 40 5a 15 00 00 00|PLC1"; fast_pattern; reference:url,www.proofpoint.com/us/blog/threat-insight/carry-compromise-ta4922-packs-packclient; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900103; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"PackClient PLA1 stream"; flow:established,to_server,only_stream; content:"|2c 00 40 5a 15 00 00 00|PLA1"; fast_pattern; reference:url,www.proofpoint.com/us/blog/threat-insight/carry-compromise-ta4922-packs-packclient; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900104; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

alert tcp $EXTERNAL_NET any -> $HOME_NET any (msg:"PackClient PLK1 stream"; flow:established,to_client,only_stream; content:"|3c 00 40 5a 15 00 00 00|PLK1"; fast_pattern; reference:url,www.proofpoint.com/us/blog/threat-insight/carry-compromise-ta4922-packs-packclient; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900105; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

# Optional Core startup correlation. Match each semantic message on reassembled
# stream data, then alert once both states are present on the same TCP flow.
alert tcp $EXTERNAL_NET any -> $HOME_NET any (msg:"PackClient Core startup probe state"; flow:established,to_client,only_stream; content:"SYS|7c|Q|7c|EXT|7c|STARTUP|7c|PROBE|7c|"; fast_pattern; flowbits:set,ET.packclient.core_startup_probe; flowbits:noalert; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900110; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"PackClient Core startup response state"; flow:established,to_server,only_stream; content:"SYS|7c|R|7c|EXT|7c|STARTUP|7c|OK|7c|tags="; fast_pattern; flowbits:set,ET.packclient.core_startup_response; flowbits:noalert; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900111; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

alert tcp $HOME_NET any <> $EXTERNAL_NET any (msg:"PackClient Core startup probe and response"; flow:established; flowbits:isset,ET.packclient.core_startup_probe; flowbits:isset,ET.packclient.core_startup_response; flowbits:unset,ET.packclient.core_startup_probe; flowbits:unset,ET.packclient.core_startup_response; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900112; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

Validation

The submitted rules parse successfully under Suricata 7.0.3.

Engine-level testing confirmed that:

  • the observed four-byte split layout satisfies the existing packet-oriented predicates;
  • equivalent PackClient frames delivered as coalesced TCP payloads can bypass those predicates;
  • segmentation within the PackClient framing can also prevent packet-oriented matching;
  • the stream-aware Launcher signatures continue to match after TCP reassembly;
  • the Core startup alert requires both the probe and response on the same TCP flow;
  • a response without the corresponding probe does not produce the correlated alert;
  • the Core startup correlation survives segmentation in both directions.

References

PackClient reverse-engineering research:

Original Proofpoint PackClient research:

Happy to adjust the signatures or provide additional captures/validation if useful.

Thanks,

Ivan Immanuel Shaji

Hello Ivan,

Thanks for the submission. I have some questions/requests:

  • I’m trying to better understand what the issues with these rules are. Suricata does TCP stream assembly by default, so the only_stream option on the flow keyword shouldn’t be necessary. Are you saying that you’ve noticed some of these options have been packed together in a single TCP stream or that the dsize option used isn’t accurate?

  • It would be very helpful if you could provide a pcap so that we can test against it, and better understand the problem.

Thanks,

-Tony

Hi Tony,

Thanks for taking a look.

You’re right about default TCP reassembly. What I was trying to show is the packet-size dependency in the existing tcp-pkt/dsize checks: the same PLH1 bytes match as 4+36-byte payloads, but miss as one 40-byte payload.

I’ve attached a small constructed repro using unchanged header bytes using header bytes from the recorded Triage capture. I checked it on Suricata 8.0.3—the existing rules miss this layout, while my candidate patterns match without only_stream too.

Thanks,
Ivan

packclient-launcher-constructed-repro.pcap (1.2 KB)

Ivan, thank you very much for the pcap, and the further explanation. I’ve do a bit of re-working of some of the rules I put out for PackClient, I’ve come up with a solution that is close to what you’re doing, but not exactly the same. Here are some changes I’m planning on making to existing rules in the ET ruleset:

alert tcp-pkt $HOME_NET any -> $EXTERNAL_NET any (msg:"ET MALWARE PackClient RAT CnC Checkin M1"; flow:established,to_server; xbits:set,ET.PackClient_RAT,track ip_pair,expire 300; dsize:<100; content:"|24 00 40 5a|"; fast_pattern; reference:md5,c55f9bc3c53ac32e6169ad3cc8b2ce49; classtype:trojan-activity; sid:1; rev:1;)

alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"ET MALWARE PackClient RAT CnC Checkin M2"; flow:established,to_server; xbits:set,ET.PackClient_RAT, track ip_pair, expire 300; content:"|15 00 00 00|PLH1"; fast_pattern; reference:md5,c55f9bc3c53ac32e6169ad3cc8b2ce49; classtype:trojan-activity; sid:2; rev:1;)

alert tcp $EXTERNAL_NET any -> $HOME_NET any (msg:"ET MALWARE PackClient RAT CnC Checkin M2 - C2 Response"; flow:established,to_client; xbits:isset,ET.PackClient_RAT, track ip_pair; content:"|40 5a 15 00 00 00|PLC1"; fast_pattern; reference:md5,c55f9bc3c53ac32e6169ad3cc8b2ce49; classtype:trojan-activity; sid:3; rev:1;)

alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"ET MALWARE PackClient RAT Payload Request"; flow:established,to_server; xbits:set,ET.PackClient_RAT, track ip_pair; content:"|15 00 00 00|PLA1"; fast_pattern; reference:md5,c55f9bc3c53ac32e6169ad3cc8b2ce49; classtype:trojan-activity; sid:4; rev:1;)

alert tcp $EXTERNAL_NET any -> $HOME_NET any (msg:"ET MALWARE PackClient RAT Payload Request - C2 Response"; flow:established,to_client; xbits:isset,ET.PackClient_RAT,track ip_pair; content:"|40 5a 15 00 00 00|PLK1"; fast_pattern; reference:md5,c55f9bc3c53ac32e6169ad3cc8b2ce49; classtype:trojan-activity; sid:5; rev:1;)

Here are the highlights:

  • the PackClient RAT CnC Checkin M1 and M2 rules now both set an xbit ET.PackClient_RAT. so if either of them trigger, either one should set the same xbit that keeps track of the entire network flow.
  • I’ve elected to keep the CnC Checkin M1 rule as a tcp-pkt rule, and have elected to set the dsize parameter to a range of less than 100 bytes. That fits your observations, and it also fits our observations.
  • I’ve removed all of the depth, dsize, byte_jump and isdataat checks for the remaining four rules. I’m assuming that if rule 1 or rule 2 has triggered, that there’s not a very significant chance of a false positive occurring for the follow-up rules past that point.

Now, that brings me to some questions related to the last three rules mentioned in your original post:

alert tcp $EXTERNAL_NET any -> $HOME_NET any (msg:"PackClient Core startup probe state"; flow:established,to_client,only_stream; content:"SYS|7c|Q|7c|EXT|7c|STARTUP|7c|PROBE|7c|"; fast_pattern; flowbits:set,ET.packclient.core_startup_probe; flowbits:noalert; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900110; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"PackClient Core startup response state"; flow:established,to_server,only_stream; content:"SYS|7c|R|7c|EXT|7c|STARTUP|7c|OK|7c|tags="; fast_pattern; flowbits:set,ET.packclient.core_startup_response; flowbits:noalert; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900111; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

alert tcp $HOME_NET any <> $EXTERNAL_NET any (msg:"PackClient Core startup probe and response"; flow:established; flowbits:isset,ET.packclient.core_startup_probe; flowbits:isset,ET.packclient.core_startup_response; flowbits:unset,ET.packclient.core_startup_probe; flowbits:unset,ET.packclient.core_startup_response; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900112; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

I don’t have any packclient traffic that matches this data. So, if you could help me by answering some questions, that would be great.

  1. The pcap you shared doesn’t have any traffic that triggers the rules above. Do you have a pcap or even a screenshot from wireshark, anything that shows this traffic?
  2. Does this traffic occur after the initial PLH1/PLC1 traffic? If so, we can forego setting both of the flowbits, and just check that the ET.PackClient_RAT xbit has been set.
  3. I don’t understand why the first two of these remaining 3 rules are set to noalert, and yet there is a third rule generated that has no content matches with two flowbit checks specified that is configured to alert. In general, this is a bad idea for performance, as well as excessive IDS alerts for the remainder of that TCP stream.

Provided that this traffic occurs after the initial PLH1/PLC2 C2 check-in and response, we can re-write these rules like so:

alert tcp $EXTERNAL_NET any -> $HOME_NET any (msg:"PackClient Core startup probe state"; flow:established,to_client; content:"SYS|7c|Q|7c|EXT|7c|STARTUP|7c|PROBE|7c|"; fast_pattern; xbits:isset,ET.PackClient_RAT,track ip_pair; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900110; rev:2; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"PackClient Core startup response state"; flow:established,to_server; content:"SYS|7c|R|7c|EXT|7c|STARTUP|7c|OK|7c|tags="; fast_pattern; xbits:isset,ET.PackClient_RAT,track ip_pair; reference:url,ivanimmanuel-dev.github.io/PackClient/; classtype:trojan-activity; sid:9900111; rev:1; metadata:attack_target Client_Endpoint, created_at 2026_09_11, deployment Perimeter, malware_family PackClient, confidence High, signature_severity Major, updated_at 2026_09_11;)

Thanks again for your feedback, as it definitely helps in improving accuracy, and reducing false negatives.

-Tony

Hi Tony,

Thanks for reworking these.

I’ve attached a real packet excerpt from the original Triage capture. Packets 24 and 26 show the Core startup probe and response.

To answer your second question, yes, this traffic happens after the Launcher exchange: the probe is about 13.23 seconds after PLC1, on a new TCP connection between the same IP pair.

The noalert rules were meant to keep track of both messages and give one combined alert. I tested both versions on this capture with Suricata 8.0.3. Your two Core rules triggered, but my original three-rule version didn’t, so I’m happy to go with your approach.

One other detail: the existing acknowledgement rule, SID 2069883, still requires the old ET.PackClient_RAT.Payload flowbit, so that will need updating too.

Thanks,
Ivan

packclient-launcher-core-startup.pcap (3.1 KB)

1 Like

Hi Ivan,
Great work! FYI we gave you a shoutout on our release thread today: Ruleset Update Summary - 2026/10/01 - v11302

We are happy to give you a shoutout with another handle or your name if you want.

Cheers,
:hotdog:

1 Like

Thanks for the shoutout!

Really glad to see these included.

Could you use Ivan Immanuel Shaji for the credit?

Appreciate it!

1 Like