Skip to content

Add -CSVersion so profiles load on Cobalt Strike 4.13 (second half of #32) - #33

Open
warpedatom wants to merge 5 commits into
Tylous:mainfrom
warpedatom:feat/cs413-compat
Open

Add -CSVersion so profiles load on Cobalt Strike 4.13 (second half of #32)#33
warpedatom wants to merge 5 commits into
Tylous:mainfrom
warpedatom:feat/cs413-compat

Conversation

@warpedatom

@warpedatom warpedatom commented Aug 27, 2026

Copy link
Copy Markdown

This is the second half of #32, and it builds on #30, so it wants merging
after that one. #30 fixes two of the four reasons c2lint rejects every
generated profile on Cobalt Strike 4.13; this fixes the other two. Because the
branch is cut from #30, the diff here includes those commits until #30 lands.

The problem

Cobalt Strike 4.13 no longer accepts two of the stage options SourcePoint
emits, so no generated profile can be loaded on the current release:

Error: invalid option for <.stage> at line 68
       rdll_loader
Error: invalid option for <.stage> at line 77
       name

I isolated every stage option SourcePoint writes, one per minimal profile, to
confirm exactly which ones 4.13 rejects:

option 4.13
set rdll_loader "PrependLoader" rejected
set rdll_loader "StompLoader" rejected
set name "test.dll" rejected
set checksum "123" accepted
set rich_header "\x00" accepted
set copy_pe_header "false" accepted
set eaf_bypass "false" accepted
set rdll_use_syscalls "false" accepted
set syscall_method "None" accepted

rdll_loader is rejected on both values, so this is not the stomp loader
deprecation from an earlier release. And checksum and rich_header still
work while name does not, so PE cloning survives in 4.13, it just no longer
takes the cloned module's name.

The approach

I did not want to simply delete the two options, since that would quietly
degrade anyone still on an older team server: name appears in all 30
Peclone_list entries, and rdll_loader backs the documented -RdllLoader
flag.

So -CSVersion selects the target release. 4.13 and newer omit both options;
4.12 and older emit them exactly as before. It defaults to 4.13, on the
grounds that a tool should work against the current release out of the box, but
that default is a one-word change in Loader/CSVersion.go if you would rather
not alter behaviour for existing users.

$ ./SourcePoint -Host acme-email.com -Outfile p.profile -Profile 2
...
[!] Targeting Cobalt Strike 4.13: stage.rdll_loader and stage.name omitted, both removed in 4.13

The omission is reported rather than silent, so nobody wonders where their
-RdllLoader setting went. -RdllLoader is still validated regardless of
target, so a typo remains an error instead of being discarded along with the
directive.

A third incompatibility, same shape

With rdll_loader and name handled, c2lint gets further and rejects the PE
clone image sizes:

[-] .stage.image_size_x86 must be larger than 372736 bytes
[-] .stage.image_size_x64 must be larger than 462848 bytes

Cobalt Strike requires each to be at least the size of the Beacon DLL it stomps
into the image. The values in Peclone_list are the real sizes of the modules
being mimicked, and the Beacon has outgrown most of them, so on 4.13 only four
of the thirty clone entries produce a loadable profile: the four that happen to
carry no image_size directives at all.

Raising them to a fixed floor would age out again with the next Beacon growth,
so 4.13 and newer omit them and let Cobalt Strike size the image. The four
entries that already ship without them are the evidence that works.

One fix that falls out of this

The set name directive lives inside the Peclone_list entries, and the
"Beacon DLL Spoofed To:" summary line recovered the module name by splitting
the clone block on ; and indexing len-3. That arithmetic breaks as soon as
the block loses a directive, so the name is now read with a regex before the
directive is stripped. Four of the thirty entries also turn out to carry no
image_size directives, which the old index maths happened to survive and the
new code does not care about either way.

Verification

Against a licensed Cobalt Strike 4.13 team server:

  • a profile generated with the defaults loads into the team server, not
    just past c2lint: [+] I see you're into threat replication. loaded. and
    [+] Team server is up. The only remaining c2lint warning is the absent
    code-signing keystore.
  • that was with -PE_Clone 4, one of the twenty-six entries that fails without
    the image_size change, and with -DNS and -Stage true both on
  • generating a profile for all thirty clone entries leaves no image_size or
    name directive anywhere
  • a profile generated with -CSVersion 4.12 still fails on 4.13 with the
    stage errors, confirming the gate actually gates
  • -CSVersion nonsense is rejected up front

Tests cover version parsing and comparison, reading the module name out of
every Peclone_list entry, stripping removing the name directive and nothing
else, and set pipename / set ssh_pipename not being mistaken for set name.

warpedatom and others added 4 commits August 26, 2026 17:35
-Injector defaulted to an empty string with no empty case in GenerateProcessInject, so it was silently mandatory and no profile could be generated without it. Random table pickers used GenerateNumer(0, len-1), which is exclusive of its upper bound, making the last entry of every lookup table unreachable (4 of 8 SSH banners). GenerateURIValues dropped rejected URIs instead of retrying, so -Uri N returned fewer than N. Re-seeding math/rand from time.Now() on every call is deprecated and returns identical values within a clock tick. CDN_Value was never read from the YAML config, the config overlay clobbered flag defaults, and out-of-range numeric flags panicked instead of erroring. Adds unit tests and a build workflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Slack profile hardcoded its http-stager URIs as /messages/DALBNSf25 and /messages/DALBNSF25, so every profile generated from that template shared the same two paths. GoToMeeting derived both from UValue, making the x86 and x64 stager URIs identical and tying the staging request to the beacon check-ins, which reuse UValue in their prepends and cookie. Both now use independent per-architecture stager tokens of varied length. Outlook.Live already randomized correctly and is unchanged. Separately, -Sleep, -Jitter, -Datajitter, -Allocation and the three -Tasks*MaxSize flags were written into the profile unvalidated, so bad values only surfaced when the teamserver refused to load it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c2lint on Cobalt Strike 4.13 rejects every profile SourcePoint generates. Two of the causes are unambiguous. smartinject is a post-ex option, but the stage block set it too, which c2lint reports as 'invalid option for <.stage>'; the post-ex copy was hardcoded to true, so -SmartInject drove the invalid one and never affected the profile it was meant to. sleep_mask was emitted without quotes around its value, unlike every other boolean in the block, which c2lint reports as 'Unknown statement in <.stage>'. Both are fixed and covered by tests against the template text. Two further causes, stage.rdll_loader and stage.name, are version compatibility questions rather than bugs and are reported separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cobalt Strike 4.13 removed the stage.rdll_loader and stage.name malleable C2 options, so every profile SourcePoint generated was rejected by c2lint with 'invalid option for <.stage>'. Rather than dropping the options outright, which would degrade older team servers, -CSVersion selects the target release: 4.13 and newer omit them, 4.12 and older emit them as before. The flag defaults to 4.13. -RdllLoader is still validated regardless of target so a typo remains an error, and the omission is reported on stdout rather than being silent. The spoofed module name is now recovered with a regex before stripping instead of by splitting the clone block on ';' and indexing len-3, which broke once the block lost a directive. Verified with c2lint on a licensed 4.13 team server: a 4.13 profile compiles, a 4.12 profile still fails with the two stage errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 27, 2026 19:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Cobalt Strike requires stage.image_size_x86 and image_size_x64 to be at least the size of the Beacon DLL it stomps into the image, and rejects the profile otherwise with 'must be larger than N bytes'. The values in Peclone_list are the real sizes of the modules being mimicked and the Beacon has outgrown most of them, so on 4.13 only four of the thirty clone entries produce a loadable profile: the four that happen to carry no image_size directives at all. Raising them to a fixed floor would age out again as the Beacon grows each release, so they are dropped for 4.13 and newer and Cobalt Strike sizes the image itself. Verified by generating a profile for every clone entry and confirming none still carries image_size or name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants