SocksTroy - An interesting use case with byte_jump, isdataat, and stream_size

What is SocksTroy?

Hey Folks,

A while back, we got a notification from @jaydinbas about a new type of backdoor proxy discovered in the wild. We chose to name it SocksTroy after the name discovered in its PDB metadata. As the twitter thread indicates, this proxy software is interesting in that it seems to operate like a SOCKS5 proxy (complete with authentication), but does not appear to follow the SOCKS5 protocol.

We were provided with the feedback:

“Call with “-host -auth ” and it connects to the host. must contain @ or is ignored.”

The MD5 of the sample I tested was: 0f9b876031ffc16c7eedfeaf2ca9dc5b
Here is a link to the Virustotal report.

Observations

Using this information, at first I tried running it in our sandbox with the arguments suggested and initially got no activity. After that, I attempted to run it in my VM lab. I ran the payload manually on my Windows host, and set up a netcat listener + tcpdump on a Linux system in my lab.

Note: for the purposes of this post, the infected windows host in my lab environment is at IP address 172.16.12.46, while the server side is located at 172.16.12.49 at TCP port 4444

I ran the malware a number of times with changes to the length of the -auth field. I noticed something interesting:

  • The auth packet (the only packet I was able to successfully send) had two fields in front of the auth field, totaling 7 bytes.
  • The first field, totaling 4 bytes in length, always described the size of the rest of the data payload minus the length of this 4-byte header. Additionally, this size field was always sent as a separate packet in the TCP stream.
  • The second field, was 3 bytes in length. The first byte would always read fe, followed by two hex digits that I presumed were use to calculate the length of the authentication message. however I noticed if the size of the field was larger than 255, that no matter how much bigger the packet is, the 3 byte field would always read: fe 00 03

In addition to these things, I assumed that, the maximize size recorded by the size headers would be no larger that 1518 bytes (max size of a basic ethernet frame in its entirety – excluding 802.1q headers and/or jumbo frames), that means that for the first 4-byte size field, only the first two bytes would ever be used to describe the entire size of this authentication message. That means that the remaining two bytes of the first size field will, in 99% of cases, be 00 00, followed immediately by fe 00, and if the second, 3-byte size field is larger than 255 bytes, it will always read:fe 00 03 Using this information, we can come to two conclusions:

  • If the size of the packet sent to destination is less than 263 (255 + 7 = 262), then bytes 2-6 of the first seven bytes will always read: 00 00 fe 00

  • If the size of the packet sent to the destination is greater than 262, then bytes 2-7 of those first seven bytes will always read: 00 00 fe 00 03

I can use this logic, along with byte_jump and isdataat, and stream_size to enforce size constraints and confirm that the data packet we trigger the alert on meets the size criteria we’ve observed in the authentication packets. Here are the rules I came up with. They’re already in the ETOPEN ruleset:

Rule Logic and Explanation

alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"ET MALWARE Win32/SocksTroy Session Initiation Attempt M1"; stream_size:client,<,263; flow:established,to_server; content:"|00 00 fe 00|"; offset:2; depth:4; byte_jump:4,0, little, from_beginning, post_offset 3; isdataat:!2,relative; classtype:trojan-activity; sid:2042771; rev:1;)

alert tcp $HOME_NET any -> $EXTERNAL_NET any (msg:"ET MALWARE Win32/SocksTroy Session Initiation Attempt M2"; stream_size:client,>,262; flow:established,to_server; content:"|00 00 fe 00 03|"; offset:2; depth:5; byte_jump:4,0, little, from_beginning, post_offset 3; isdataat:!2,relative; classtype:trojan-activity; sid:2042772; rev:1;)

Let’s talk about the logic being used in these rules a little bit.

Now, the first thing we’re doing is using the stream_size rule option to determine how big the client’s side of the TCP stream is. If you’re looking for documentation on the stream_size feature, as always take a look at suricata’s read the docs for the version of suricata you are using, or the snort user manual, respectively.

Here is the entry on stream_size for the latest stable release of suricata as of mine writing this.

Here is the entry in the snort user guide as well.

Stream size allows us to set a size criteria for the entire TCP stream being observed
“The client side of this TCP stream is less than 50 bytes” (stream_size:client,<,50;)
“The entire stream must be exactly 600 bytes between the client and the server” (stream_size:both,=,600;)
“We only want to look at streams where the server side bytes transferred is greater than 5000 bytes” (stream_size:server,>,5000;)

If its at all helpful, think of Wireshark. Wireshark allows users to follow streams (Mainly TCP streams), by right clicking a packet that is a part of a TCP stream, then selecting Follow > TCP Stream:

When you’re reviewing a TCP stream, there is a small drop-down menu in the lower left of the window that allows you to isolate traffic from the client side, or the server side. It will also tell you sizes for reach side of the converstation, or the conversation as a whole:

sockstroy

This packet capture is from one (of many sockstroy runs in which the size of the client side stream is 261 bytes, which fits in to the "less than 263 bytes criteria for the M1 rule above.

The bottom line is that if you’re looking for a visual representation of stream_size, then this drop-down menu in the Follow TCP Stream Window in Wireshark is a quick way to figure out how many bytes the full stream is, and how many bytes each side of the conversation (client vs. server) transmitted as a part of that stream.

In our case the M1 rule is using stream_size:client,<,263; – That means the M1 rule only cares about TCP streams in which the client size is less than 263 bytes in total. The packet capture in the screen cap above fits this criteria.

The M2 rule on the other hand uses stream_size:client,>262; to cover client data streams larger than 262 bytes.

as always, we are using flow:to_server,established to tell suricata that we’re only interested in connections in which the TCP three-way handshake has been established, and the client is initiating data transfer to a remote server.

Next up, the M1 rule has content:"|00 00 fe 00|"; offset:2; depth:4;. This denotes that the we’re looking for those last two bytes of the first size field, followed by the first two bytes of the second size field at the start of the authentication packet:

sockstroy2

The black boxes denote the areas in which we are looking for our content match and we can see that the data matches – the content 00 00 fe 00 is located 2 bytes in from the start of the TCP stream, and is contained with a depth of 4 bytes from that offset. Did you notice how the first row starts at offset 0x00 then the second row’s offset starts at 0x04? This is because that first four-byte size field is actually a separate packet in the full TCP stream. When you follow a full TCP stream in wireshark, it glues together that Data portion of all the packets into one contiguous stream.

On the other hand, the M2 version of this rule is looking for content:"|00 00 fe 00 03|"; offset: 2; depth:5 and in one of the other packets I collected, that data looks like:

sockstroy3

sockstroy4

As you can see, the size of the client stream is greater than 262 bytes, and the content at offset 2 with a depth of 5 matches the content we’re looking for.

Now that brings us to the rest of the rule content for both rules: byte_jump:4,0, little, from_beginning, post_offset 3; isdataat:!2,relative;

If you’re not familiar with byte_jump, isdataat and the quirks in how they interact, I highly suggest checking out Brandon Murphy’s very detailed tutorial on how byte_jump jump works in this very forum titled The Complexities of byte_jump. No analyst is an island on the Emerging Threats team, and using information that your peers put together benefits everybody.

In our case here, we’re telling byte_jump to pull 4 bytes of data from the very beginning of the TCP data stream – those 4 bytes that represent the size of the remaining data in the authentication packet – read them in little endian format, then jump that far ahead into the packet, Then jump forward another 3 bytes.

We’re going to pick on the packet that matches the M2 version of this rule. In the image directly above, we can see that the first four bytes of the TCP stream read 06 01 00 00. Little endian format states that values at lower addresses are less significant. In plain english, we have to read these four bytes in reverse. If you do that, you get 00 00 01 06 take the value 01 06 and convert that to decimal, and you get 262.

sockstroy5

The Windows programmer calculator is your friend. Here we can see the value 106 in hex, is 262 in decimal. The initial 4-byte field is saying that the remaining data payload is 262 bytes long. If we add four bytes to that value, we get 266, the full size of client side of this TCP stream. I bring this up, because our byte jump is only jumping 262 bytes into the data stream. In order to reach the final byte of the stream for our isdataat:!2,relative check to evaluate as true, we have to hop forward three more bytes. That is why we use the post_offset 3 to push the detection pointer forward to the correct position.

I hope you find this post helpful! Happy Hunting.

Tony “da_667” Robinson

2 Likes