Skip to content

Unify clean shutdown and update C2 channel with session tracking #358

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

terrorbyte
Copy link
Collaborator

This unifies the work from @lobsterjerusalem in #340 and #268 and supersedes it with a different path. Instead of handling signal shutdowns and cleanup per-C2, instead this updates the C2 interface and channel.Channel to have:

  • channel.Channel.Shutdown *atomic.Bool - A C2 specific atomic to track if the C2 needs to shutdown for any reason.
  • c2.Shutdown() bool - A uniform shutdown interface for calling shutdown, manually or triggered from the shutdown tracking atomic
  • c2.Channel() *channel.Channel - Allows access to the underlying C2 channel struct
  • channel.Channel.Sessions map[string]Session - Basic "session" tracking. This allows a map of random session IDs of any string type to a basic session struct that contains all the current connected clients.
  • channel.Input io.Reader - Allow for the channel input to be user controlled. This is in preparation for adding end-to-end tests for C2s as os.Stdin needs to be controlled.
  • (c *Channel) HasSessions() bool - The long awaited way to check if a C2 is connected or not. Instead of accessing a simple boolean, this can handle callback lifecycle future needs in exchange for a bit of clarity and some reference juggling. Can be invoked with:
c, ok := c2.GetInstance(conf.C2Type)
c.Channel().HasSessions()
  • (c *Channel) AddSession(conn *net.Conn, addr string) bool - Adds a session to track. At the moment the conn is optional and if it is set, then Close() will be called on cleanup, but if it is nil it will be skipped. My current version uses a 16bit base32 string for unique identifiers in order to stay within stdlib and because I think it's easier to suss out than UUIDs, but I suspect some people will have thoughts.
  • (c *Channel) RemoveSession(id string) bool - Remove the session from the tracking map and if it has a non-nil net.Conn it will close the connection.
  • (c *Channel) RemoveSessions() bool - Remove all sessions and close all open net.Conns.

This allows us to put the shutdown logic for OS signals in the cli/framework, as well as from inside of the C2 itself. This allows uniform clean shutdown for each of the C2s, including (finally) fixing the shutdown of open shell sessions.


There's some documentation and better comments I want to add to make sure to clarify some components such as how to track the atomic for new C2s. It will also require a change to our experimental C2s for cleanup and channel tracking. This should be considered to be an API break for those C2 users (of which I don't see any public ones at the moment besides us, but it should be noted on tag).

@terrorbyte terrorbyte added enhancement New feature or request proposal labels Apr 10, 2025
@terrorbyte terrorbyte self-assigned this Apr 10, 2025
@terrorbyte
Copy link
Collaborator Author

This also needs a lot more hands on testing for each of the C2 types.

@terrorbyte
Copy link
Collaborator Author

terrorbyte commented Apr 10, 2025

Until the C2 tests get automated I'm going to manually test each C2 and record the results based on the above changes, with the following test plan:

  • Callbacks work as expected
  • Ctrl-c kills the server and cleans up clients
  • exit\n kills the server and cleans up clients
  • Multiple callbacks clean up neatly
  • Timeouts function as expected
  • Timeouts on successful exploitation but no callback

SSLShellServer

  • - Callback
  • - OS signal
  • - Manual exit
  • - Multiple callbacks - NOTE: It does work, but the connection close data is not explicit until the AddSession was moved into Run and not handleSimpleConn. The placement of that could be contextual the the C2, but for now prefer too many sessions to make sure it all gets cleaned up vs identifying the one that gets queued and potentially missing.
  • - Multiple callbacks exit from exit - See above
  • - Shutdown on non-target
  • - Timeout no callback

SimpleShellServer

  • - Callback
  • - OS signal
  • - Manual exit
  • - Multiple callbacks exit from signal - NOTE: Same as SSLShellServer
  • - Multiple callbacks exit from exit - NOTE: Same as SSLShellServer
  • - Shutdown on non-target
  • - Timeout no callback

HTTPServeShell / HTTPServeFile

  • - Callback
  • - OS signal
  • - Manual exit
  • - Multiple callbacks from signal
  • - Multiple callbacks exit from exit
  • - Shutdown on non-target
  • - Timeout no callback

ShellTunnel

  • - Callback
  • - OS signal
  • - Manual exit
  • - Multiple callbacks from signal - N/A
  • - Multiple callbacks exit from exit - N/A
  • - Shutdown on non-target
  • - Timeout no callback
  • - Shutdown when callback server fails

Things fixed from this:

  • Ordering of session catching to kill off all net.Conn instances
  • Fixed doScan and doVerify holding C2 open until timeout, this appears to be a regression and will now instantly close the C2 if these fail.
  • Ensured that -o was supported
  • Clarified phrasing of shutdown belongs to C2
  • HTTPServeShell emits 2 shutdown messages right now since it starts up 2 instances. Might be worth adding context to the channel for C2Type and printing that. Just juggled waitgroup properly and moved the Drop to inside the core check.
  • ShellTunnel required some coercing of Session logic in order to track connections for client and server upstream. There's a few hacks to deal with shutdown logic and currently can't handle a queue of sessions if we ever add support for that, it'll need to revisit some of that logic if we do (but that applies to everything).

template

  • - Callback
  • - OS signal
  • - Manual exit
  • - Multiple callbacks from signal -
  • - Multiple callbacks exit from exit -
  • - Shutdown on non-target
  • - Timeout no callback

- Ordering of session catching to kill off all `net.Conn` instances
- Fixed doScan and doVerify holding C2 open until timeout, this appears to be a regression and will now instantly close the C2 if these fail.
- Ensured that `-o` was supported
- Clarified phrasing of shutdown belongs to C2
- `HTTPServeShell` emits 2 shutdown messages right now since it starts up 2 instances. Might be worth adding context to the channel for C2Type and printing that. Just juggled waitgroup properly and moved the Drop to inside the core check.
- `ShellTunnel` required some coercing of Session logic in order to track connections for client and server upstream. There's a few hacks to deal with shutdown logic and currently can't handle a queue of sessions if we ever add support for that, it'll need to revisit some of that logic if we do (but that applies to everything).
@terrorbyte
Copy link
Collaborator Author

I think it just needs docs and I will undraft.

@terrorbyte terrorbyte marked this pull request as ready for review April 11, 2025 16:57
@@ -314,6 +331,8 @@ func startC2Server(conf *config.Config) bool {
}

// execute verify, version check, and exploit. Return false if an unrecoverable error occurred.
//
//nolint:gocognit
Copy link
Contributor

Choose a reason for hiding this comment

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

The linter hates thinking.

}
// Handle the signal interrupt channel. If the signal is triggered, then trigger the done
// channel which will clean up the server and close cleanly.
go func(sigint <-chan os.Signal, channel *channel.Channel) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is something that has been brought up but maybe not thoroughly discussed.
Do we make this opt-in or opt-out?

With this in place as-is you can opt-out using something like:
if sigint: os.Exit(0)
But will this become boilerplate in everything or will the alternative (letting this code do the shutdown) be the default case?

Seems like a 'gallery' discussion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Why would you opt out? I don't think I see that behavior ever being set up as a flag. if there was one thing I'd like is to maybe split it into an OS specific build gate so we could support non-unix-y stuff

@terrorbyte
Copy link
Collaborator Author

Working on patching the HTTPShellServer, then this will be g2g

if httpServer.Channel().Shutdown.Load() {
server.Close()
httpServer.Shutdown()
wg.Done()
Copy link
Contributor

Choose a reason for hiding this comment

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

should we not defer this on line 267 instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

My theory with this was that if an error occured that was fatal to the http server that close would still be called and defer would be triggered, while the channel load checks are just for any other situation that occurs: timeout, manual exit, other c2 trigger, demons, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request proposal
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants