SOCKS4a Proxy in C#

Some time ago, I tweeted a teaser about implementing a SOCKS4 proxy in .NET. This post will finally provide a basic run-down of how I implemented it. There are some short-comings, which I’ll try and callout as they come up.

We start off by creating a class that will bring a bind address and port in on the constructor. This allows us to easily instantiate multiple instances of the proxy running on different IP/port combinations if desired. I also scaffold Start and Stop methods to (unsurprisingly) start and stop the proxy instance.

public class Socks4Proxy
{
    private readonly int _bindPort;
    private readonly IPAddress _bindAddress;

    public Socks4Proxy(IPAddress bindAddress = null, int bindPort = 1080)
    {
        _bindPort = bindPort;
        _bindAddress = bindAddress ?? IPAddress.Any;
    }

    public async Task Start()
    {

    }

    public void Stop()
    {

    }
}

Under the Start method I opted for a pattern to accept new connections on a loop until a token is cancelled, which can be done from the Stop method. Once the method drops out of the loop, the listener is stopped. The AcceptTcpClientAsync method on the TcpListener also takes a cancellation token, which means it will give up listening for new connections and allow code execution to continue.

private readonly CancellationTokenSource _tokenSource = new();

public async Task Start()
{
    var listener = new TcpListener(_bindAddress, _bindPort);
    listener.Start(100);

    while (!_tokenSource.IsCancellationRequested)
    {
        // this blocks until a connection is received or token is cancelled
        var client = await listener.AcceptTcpClientAsync(_tokenSource.Token);
        
        // do something with the connected client
    }
    
    listener.Stop();
}

public void Stop()
{
    _tokenSource.Cancel();
}

Once a connection has been accepted, we handle that specific client connection in a new thread. This should free-up the main thread to go back to listening for new connections.

var client = await listener.AcceptTcpClientAsync(_tokenSource.Token);
        
// handle client in new thread
var thread = new Thread(async () => await HandleClient(client));
thread.Start();

private async Task HandleClient(TcpClient client)
{
    // do stuff
}

After a client has connected it sends a “connect request” which we must read. The data sent by the client depends on which SOCKS version it’s asking to use. SOCKS4 and 4a are very similar, whilst SOCKS5 is completely different. For the sake of this post, I’m sticking with 4 and 4a.

A SOCKS4 connection is made of 5 fields.

  • Version number.
    • 0x04 for SOCKS 4 and 4a.
  • Command code.
    • 0x01 to establish a stream connection.
    • 0x02 to establish a port binding (ignoring for this post).
  • Destination port
    • A 2-byte number in network byte order.
  • Destination IP
    • A 4-byte IPv4 address.
  • ID
    • Some “random” null-terminated string ID of variable length (seems mostly unused).

The primary difference between SOCKS4 and 4a is where the DNS resolution happens to derive the destination IP. When SOCKS4 is used, the calling client does the lookup and specifies the destination IP in the connect request. With SOCKS4a, that DNS lookup is shifted to the proxy server. The request made from the client is practically identical, the exception being the destination IP is set to 0.0.0.x (usually .1) and the domain name is included at the end of the regular SOCKS4 request.

  • Version number
  • Command code
  • Destination port
  • Destination IP
  • ID
  • Domain name

In this case, the proxy server needs to perform a DNS lookup of the domain name to determine what the destination IP should be.

Once the first chunk of data has been read from the new client, we can check the SOCKS version being requested to ensure it’s indeed 4.

// read data from client
var data = await client.ReceiveData(_tokenSource.Token);
        
// read the first byte, which is the SOCKS version
var version = Convert.ToInt32(data[0]);

Curl is handy for testing.

PS C:\> curl -v http://httpbin.org/status/200 --socks4a localhost:1080

If the version is not 4 we send an error response back to the client, otherwise send a success. The server response contains 4 fields:

  • Reply version.
    • Can be null.
  • Reply code.
    • 0x5a for granted.
    • 0x5b for rejected.
  • Destination port.
    • Only used with bind requests, otherwise can be null.
  • Destination IP.
    • As above.

We can condense that down into a single method.

private async Task SendConnectReply(TcpClient client, bool success)
{
    var reply = new byte[]
    {
        0x00,
        success ? (byte)0x5a : (byte)0x5b,
        0x00, 0x00,
        0x00, 0x00, 0x00, 0x00
    };

    await client.SendData(reply, _tokenSource.Token);
}

Once we’re confident the client is making a SOCKS4/4a request, we can read the rest of the data. For that, I made a new class capable of parsing the raw bytes.

internal class Socks4Request
{
    public CommandCode Command { get; private init; }
    public int DestinationPort { get; private init; }
    public IPAddress DestinationAddress { get; private set; }

    public static async Task<Socks4Request> FromBytes(byte[] raw)
    {
        var request = new Socks4Request
        {
            Command = (CommandCode) raw[1],
            DestinationPort = raw[3] | raw[2] << 8,
            DestinationAddress = new IPAddress(raw[4..8])
        };

        // if this is SOCKS4a
        if (request.DestinationAddress.ToString().StartsWith("0.0.0."))
        {
            var domain = Encoding.UTF8.GetString(raw[9..]);
            var lookup = await Dns.GetHostAddressesAsync(domain);
            
            // get the first ipv4 address
            request.DestinationAddress = lookup.First(i => i.AddressFamily == AddressFamily.InterNetwork);
        }

        return request;
    }
    
    public enum CommandCode : byte
    {
        StreamConnection = 0x01,
        PortBinding = 0x02
    }
}

Once we have the client’s request, open a connection to the destination IP/port.

private async Task HandleClient(TcpClient client)
{
    // read connect request
    var request = await ReadConnectRequest(client);

    // connect to destination
    var destination = new TcpClient();
    var endPoint = new IPEndPoint(request.DestinationAddress, request.DestinationPort);
    await destination.ConnectAsync(endPoint);
}

When connected to the destination, read from the client again, send that data to the destination, read the response from the destination and then relay that back to the client.

Instead of trying to read from each TcpClient and looking for a none-zero length, I wrote another helper extension to determine if the underlying network stream has any data waiting to be read.

public static bool DataAvailable(this TcpClient client)
{
    var ns = client.GetStream();
    return ns.DataAvailable;
}

Then, it’s simply a case of looping and reading/writing the data between the two.

while (!_tokenSource.IsCancellationRequested)
{
    // read from client
    if (client.DataAvailable())
    {
        var req = await client.ReceiveData(_tokenSource.Token);
        
        // send to destination
        await destination.SendData(req, _tokenSource.Token);
    }

    // read from destination
    if (destination.DataAvailable())
    {
        var resp = await destination.ReceiveData(_tokenSource.Token);
    
        // send back to client
        await client.SendData(resp, _tokenSource.Token);
    }

    // sos cpu
    await Task.Delay(10);
}

One area this can be improved is detecting whether the client and/or destination has disconnected from us, so that we can properly free up the associated resources on our end.

To utilise the proxy, we can just do:

var proxy = new Socks4Proxy(); 
await proxy.Start();

If you don’t want to block, then:

var proxy = new Socks4Proxy();
_ = proxy.Start();

await Task.Delay(30000); // 30 seconds

proxy.Stop();

I tested the proxy in a variety of ways including HTTP and HTTPS via curl; Firefox & FoxyProxy; and Impacket.

The full project code is available to my Red and Black Hat Patrons.

, ,

Related posts

Evilginx, meet BITB

Obligatory disclaimer that I did not come up with any of these techniques -...

Latest posts

Evilginx, meet BITB

Obligatory disclaimer that I did not come up with any of these techniques -...