Disguising C2 traffic as FIX-spec stock trading
╔═══╦═══╦═══╦═══╦═══╦═══╗Have you ever wondered how to disguise C2 traffic as quant trading network chatter?
Probably not...
Is such an endeavor actually practical for anything?
Again, probably not...
I had this idea late one night and wanted to explore it though, so here it is.
Financial Information eXchange (FIX) is an open standard for exchanging real-time trade/market-related information between financial institutions and trade clients. This includes things like stock prices, transaction requests, and other market data. FIX is distinct from other finance related protocols like SWIFT in that SWIFT is used internally between banks for sending actual payments, whereas FIX is used for market data/requests and is generally used as an interface by external parties. For example, a quantitative trading firm might use a FIX API provided by a financial institution to quickly and dynamically consume market info and execute trades based on a trading algorithm. I imagine there are probably more niche protocols used in various parts of the quant world, but FIX is a widely adopted standard.
In general, FIX messages are composed of a header, body, and trailer
in which data is encoded as tag=value pairs separated by a
start-of-heading (SOH) character (ASCII 0x01). Tags are decimal numbers,
where 1-4999 are defined by the protocol specification. 5000+ are user
defined but have blocks reserved by various institutions for fields
specific to them.
Depending on the FIX version being used, headers may vary. The basic required elements are:
BeginString (tag 8): This is the beginning of a FIX
message and basically just holds the FIX version. Example:
8=FIX.4.4BodyLength (tag 9): The number of bytes in the message,
inclusively starting with the MsgType tag
(35=) and ending exclusively before the checksum tag
(10=) in the trailer. Example: 9=123MsgType (tag 35): A character that identifies the
message type. Some of the basic message types are Heartbeat
(0), Logon (A), Logout (5),
NewOrderSingle (D), and ExecutionReport (8).
Example: 35=DSenderCompID (tag 49): Identifies the sending party.
Example: 49=ID123TargetCompID (tag 56): Identifier for receiving party.
Example: 56=ID456MsgSeqNum (tag 34): Integer message sequence number.
Example: 34=1SendingTime (tag 52): Time of message transmission
(UTC). Example: 52=20060102-15:04:05.000The fields in the body of a FIX message will be dependent on the
message type. For example, if you were placing a limit order for 100
shares of McDonald’s stock at a price of $250 (MsgType
35=D), the message body would include the following:
ClOrdID (tag 11): Unique order identifier. Example:
11=ORD001HandlInst (tag 21): Handling
instructions. There are a few options for this (like automated vs
manual). Depending on the FIX version this isn’t always required. For
our purposes it doesn’t really matter and 1 is a good default. Example:
21=1Symbol (tag 55): Symbol identifying a
trade asset, such as a stock ticker. Example: 55=MCDSide (tag 54): Order side (buy=1, sell=2, and there are
a bunch of others). Example: 54=1OrderQty
(tag 38): Order quantity. Example: 38=100TransactTime (tag 60): Time order was created (UTC).
Example: 60=20060102-15:04:05.000OrderType
(tag 40): Order type (market=1, limit=2, and a bunch of others).
Example: 40=2Price (tag 44): Target price
in the case of a limit order. Example: 44=250.00The only required field in the trailer is a checksum (tag 10) (there are also optional fields for Signature/SignatureLength if the SecureData (tag 91) field is being used). The checksum is calculated by summing the ASCII value of all characters in the message modulo 256 (including the SOH separators but excluding the checksum field itself).
Example: 10=132 (note this checksum probably isn’t right
for this example)
FIX conversations exist as a session/presentation/application layer
on top of an underlying TCP connection. Putting the header/body/trailer
parts together, the human readable form of the limit buy example would
look like the following (replacing the SOH character with |
for readability):
8=FIX.4.2|9=126|35=D|49=ID123|56=ID456|34=1|52=20060102-15:04:05.000|11=ORD001|21=1|55=MCD|54=1|38=100|60=20060102-15:04:05.000|40=2|44=250.00|10=132|
(again note I didn’t feel like calculating the actual checksum for this example so just pretend it’s correct)
Ok, one glaring thing to address here: historically, FIX was sent in plaintext, but modern FIX conversations are generally going to be TLS encrypted (at least as far as I can tell). Practicality aside, we’re just focusing on the plaintext protocol itself for the hell of it. Obviously if you were using TLS encrypted C2 there wouldn’t be much point to using FIX underneath it. This is all assuming you’re trying to conceal stuff in naked FIX messages going across the wire. This whole idea is a midnight intrigue and I’m not trying to justify the end practicality of it.
With that out of the way, there are two primary considerations:
There are a ton of valid ways you can approach this, which is part of what made using the FIX protocol an interesting exercise.
As a baseline, there are generally going to be 2 parties involved in a FIX conversation: an executor (the financial institution providing market data and executing trades), and a trade client (the party consuming market data and submitting trade orders). I’m sure there are a bunch of different dynamics that exist in real life, but this is the general dynamic we need to be aware of. For example, it doesn’t make sense for both the executor and the trade client to submit buy orders to eachother. Additionally, the trade client is the one that would be initiating the logon request to the executor, not the other way around. This is all theater with the assumption that someone is watching the messages going across the wire and would be discerning about suspicious/anomalous dialogue dynamics. Probably not realistic, but that’s the assumption for what we’re trying to do here.
Assuming you keep to some realistic dialogue dynamic, you have a lot of flexibility in how you implement the C2 model. You can have the C2 implant act as the trade client and the C2 server act as executor (mimicking the dynamic of a reverse shell) or vice versa (mimicking the way a bind shell would be reached out to by a C2 server rather than the implant initiating a callback). When first messing around with how I wanted implement a basic proof-of-concept, I kept flip flopping between different approaches. For basic demonstration, I’m going to use the example dynamic used thus far and tailor the C2 implementation around it: a client making trade requests to an executor and receiving execution reports back. This is going to take the form of a bind shell. Here’s the basic outline of the exchange model:
Logon message to
initiate a connection.Logon
message. The FIX protocol supports a username/password for
authentication in the logon exchange, but before I knew about that I
came up with the following for authentication…Logout message is sent.Here’s a basic example of a reverse shell model (the typical C2 implant calling back to a C2 server):
Logon request along with whatever
authentication scheme you might want.Logon
confirmation (accepting/rejecting based on the authentication
scheme)News messages (message type B).FIX is a highly extensible protocol, and there are many ways we could choose to embed the data involved in C2 exchange. One important consideration to guide this is placement of C2 payloads in such a way that they would make sense (at least as much as possible) to be sent in the FIX message types they’re being sent in. After looking into what the protocol supports, a few distinct options seem like viable methods for embedding C2 data:
The FIX protocol spec includes a few fields where we can place arbitrary data blobs. These include:
Text (tag 58): Free format text strings, can be used
with pretty much every message type.EncodedText/EncodedTextLen (tag 355/354):
Encoded text, also can be used in pretty much every message type.RawData/RawDataLength (tag 96/95):
Supports arbitrary raw data. Used in logon, news, user request, and
email message types.SecureData/SecureDataLen (tag 91/90):
Stores arbitrary encrypted data. I’m not totally sure what this field is
commonly used for or the degree to which this is actually used given
most real exchanges are going to be TLS encrypted (it seems like it’s
somewhat deprecated). Before TLS was commonly used I imagine this might
store sensitive transaction information like account numbers.News message type uses a
Headline/EncodedHeadline field where you could
put arbitrary data, but typically you’d also be sending more data with
this message in the Text or EncodedText field
so this might not make much sense to use.As was briefly mentioned earlier, field tags 1-4999 are reserved by the protocol spec itself. Tags 5000+ are user defined, with the added caveat of various blocks being officially reserved by different institutions for fields unique to exchanging with them. We could encode or encrypt C2 payloads and either place it all in one user defined field, or chunk it across multiple.
Instead of using fields supporting free-form data which might be scrutinized if our dialogue was investigated, we could chunk C2 data across unassuming fields spanning multiple messages. This could also invite scrutiny if not done carefully (such as putting raw bytes in fields where that should only carry a small integer, or packing raw bytes in a field that should be a timestamp). This approach also adds more encoding/parsing complexity.
There are a bunch of libraries out there you can use for making apps
that communicate with FIX. I probably should have used one of these but
ended up going down the painful route of making my own basic parser for
a few message types. It uses the bind shell model and supports
logon/logout, authentication with an order “knock”, and C2 exchange via
trade orders and execution reports. Payloads are XOR encrypted using the
randomized orderID/execID sent with the message, base64 encoded, and
embedded in the EncodedText field. The basic proof of
concept is hosted here.
In wireshark, trade orders with embedded commands from the trade client look like this:


The command output sent back as execution reports by the executor (the C2 implant) look like this:


A basic connection, brief command exchange, and logout looks like the following in a wireshark capture:

Using plaintext FIX as a disguise for C2 exchange probably isn’t practical for anything and I spent way too much time implementing this. The vast majority of networks aren’t going to have plaintext FIX flying around, so using this exact approach would probably make you quite suspicious. The concept of using a “benign” message type for embedding C2 traffic, however, can be extremely useful depending on the network you’re trying to blend in with. A lot of R&D + creativity goes into designing evasive malware, but having a communication protocol that disguises how you talk to it is just as important. Opaque TLS traffic is generally the most practical, but designing comms around an overlooked and under-scrutinized protocol can be just as useful.