Socks server and proxychains


What we want

In addition to allowing to connect to a remote machine, SSH offers several ways to forward tcp ports,

which can be very useful to access a terminal A to access to a server C it cannot reach directly,

provided that it can reach an SSH server that can both speak to A and C, such as in the diagram below:


terminal A <======> server B <======> server C
1.2.3.4             1.2.3.5 &         192.168.2.1 
                    192.168.1.1
ssh client          ssh server        a server !
a client <---------------------------->

In this diagram,

  • A can speak to B
  • A cannot speak to C
  • A wants to speak to C
  • B can speak to C

We want to speak from C to A via the ssh tunnel between A & B.

The most known way to forward TCP ports using SSH is probably through the -L option, which allows to automagically connect a local port to a remote port via the following syntax:

ssh -L 1234:remote_server:443 user@ssh_server

This command does the following:

  • Open an SSH connection to ssh_server using the user account,
  • Listen on the 1234 TCP port,
  • Forward traffic reaching localhost:1234 to TCP port 443 of remote_server through the SSH tunnel.

Another way to forward TCP ports is via the -D option, which allows to dynamically forward TCP ports. To sum up, ssh -D 9999 user@ssh_server will do the following:

  • Open an SSH connection to ssh_server using the user account,
  • Listen on the 9999 TCP port,
  • Forward traffic reaching localhost:9999 to a dynamically chosen port a of a dynamically chosen server through the SSH tunnel.

The man ssh command is very useful to better understand what this -D option does:

-D [bind_address:]port
     Specifies a local “dynamic” application-level port forwarding.  This works by allocating a socket to listen to port on the local side, optionally bound to the specified bind_address.  Whenever
     a connection is made to this port, the connection is forwarded over the secure channel, and the application protocol is then used to determine where to connect to from the remote machine.
     Currently the SOCKS4 and SOCKS5 protocols are supported, and ssh will act as a SOCKS server.  Only root can forward privileged ports.  Dynamic port forwardings can also be specified in the
     configuration file.

     IPv6 addresses can be specified by enclosing the address in square brackets.  Only the superuser can forward privileged ports.  By default, the local port is bound in accordance with the
     GatewayPorts setting.  However, an explicit bind_address may be used to bind the connection to a specific address.  The bind_address of “localhost” indicates that the listening port be bound
     for local use only, while an empty address or ‘*’ indicates that the port should be available from all interfaces.

TL;DR :

  • When launched with -D 9999 option, the ssh client will act as a socks server (SOCK5 by default) on the client side.
  • Traffic will be automagically teleported to the SSH server and will be reemitted from it. This magic requires a small tool, proxychains.

Socks protocol 101

SOCKSv5 is described in the RFC 1928.

The introduction of this RFC describes what is SOCKSv5:

« a general framework (…) to transparently and securely traverse a firewall ».

Below in the RFC is described how SOCKSv5 works:

« When a TCP-based client wishes to establish a connection to an object
that is reachable only via a firewall (…), it must open a TCP connection to the
appropriate SOCKS port on the SOCKS server system. »

and:

« If the connection
request succeeds, the client enters a negotiation for the
authentication method to be used, authenticates with the chosen
method, then sends a relay request. The SOCKS server evaluates the
request, and either establishes the appropriate connection or denies
it. »

Ok, let’s try to see how SOCKSv5 packets are made, and what they do.

Client connection

The first SOCKSv5 packet is sent by the client to connect to the server.

+----+----------+----------+
|VER | NMETHODS | METHODS  |
+----+----------+----------+
| 1  |    1     | 1 to 255 |
+----+----------+----------+
  • The VER field is set to 0x5 for this version of the protocol.
  • The NMETHODS field contains the number of method identifier octets that appear in the METHODS field.

Server response

The message is sent by the server as a response to the client connection request.

The server selects from one of the methods given in `METHODS`, and
   sends a `METHOD` selection message:

                         +----+--------+
                         |VER | METHOD |
                         +----+--------+
                         | 1  |   1    |
                         +----+--------+


If the selected METHOD is `0xFF`, none of the methods listed by the
   client are acceptable, and the client MUST close the connection.

   The values currently defined for METHOD are:

          o  X'00' NO AUTHENTICATION REQUIRED
          o  X'01' GSSAPI
          o  X'02' USERNAME/PASSWORD
          o  X'03' to X'7F' IANA ASSIGNED
          o  X'80' to X'FE' RESERVED FOR PRIVATE METHODS
          o  X'FF' NO ACCEPTABLE METHODS

Requests

If the server successfully responsed to the client connection packet,

the client sends a request packet which tells the SOCKSv5 server which address and port it wants to reach.

+----+-----+-------+------+----------+----------+
|VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     |
+----+-----+-------+------+----------+----------+

     Where:

          o  VER    protocol version: X'05'
          o  CMD
             o  CONNECT X'01'
             o  BIND X'02'
             o  UDP ASSOCIATE X'03'
          o  RSV    RESERVED
          o  ATYP   address type of following address
             o  IP V4 address: X'01'
             o  DOMAINNAME: X'03'
             o  IP V6 address: X'04'
          o  DST.ADDR       desired destination address
          o  DST.PORT desired destination port in  network octet order

Replies

The server evaluates the request, and
returns a reply formed as follows:

+----+-----+-------+------+----------+----------+
|VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
+----+-----+-------+------+----------+----------+
| 1  |  1  | X'00' |  1   | Variable |    2     | 
+----+-----+-------+------+----------+----------+

     Where:

          o  VER    protocol version: X'05'
          o  REP    Reply field:
             o  X'00' succeeded
             o  X'01' general SOCKS server failure
             o  X'02' connection not allowed by ruleset
             o  X'03' Network unreachable
             o  X'04' Host unreachable
             o  X'05' Connection refused
             o  X'06' TTL expired
             o  X'07' Command not supported
             o  X'08' Address type not supported
             o  X'09' to X'FF' unassigned
          o  RSV    RESERVED
          o  ATYP   address type of following address
             o  IP V4 address: X'01'
             o  DOMAINNAME: X'03'
             o  IP V6 address: X'04'
          o  BND.ADDR       server bound address
          o  BND.PORT       server bound port in network octet order

   Fields marked RESERVED (RSV) must be set to X'00'.

UDP case

+----+------+------+----------+----------+----------+
|RSV | FRAG | ATYP | DST.ADDR | DST.PORT |   DATA   |
+----+------+------+----------+----------+----------+
| 2  |  1   |  1   | Variable |    2     | Variable |      +----+------+------+----------+----------+----------+

     The fields in the UDP request header are:

          o  RSV  Reserved X'0000'
          o  FRAG    Current fragment number
          o  ATYP    address type of following addresses:
             o  IP V4 address: X'01'
             o  DOMAINNAME: X'03'
             o  IP V6 address: X'04'
          o  DST.ADDR       desired destination address
          o  DST.PORT       desired destination port
          o  DATA     user data

How to connect to C from A ?

The first step is to connect to an SSH server with the -D argument for dynamic port forwarding:

thomas@tolva:~/ ssh -D 9999 my@ssh_server
(...)

Then to reach the web server on 192.168.2.1 through the ssh server, we simply launch the « normal » command but prefixed by proxychains:

thomas@tolva:~/ proxychains wget https://192.168.2.1
(...)

PROXYCHAINS

How is configured proxychains ?

The default proxychains configuration file looks like this:

# proxychains.conf  VER 3.1
#
# HTTP, SOCKS4, SOCKS5 tunneling proxifier with DNS.
#	

# The option below identifies how the ProxyList is treated.
# only one option should be uncommented at time,
# otherwise the last appearing option will be accepted
#
#dynamic_chain
#
# Dynamic - Each connection will be done via chained proxies
# all proxies chained in the order as they appear in the list
# at least one proxy must be online to play in chain
# (dead proxies are skipped)
# otherwise EINTR is returned to the app
#
strict_chain
#
# Strict - Each connection will be done via chained proxies
# all proxies chained in the order as they appear in the list
# all proxies must be online to play in chain
# otherwise EINTR is returned to the app
#
#random_chain
#
# Random - Each connection will be done via random proxy
# (or proxy chain, see  chain_len) from the list.
# this option is good to test your IDS :)

# Make sense only if random_chain
#chain_len = 2

# Quiet mode (no output from library)
#quiet_mode

# Proxy DNS requests - no leak for DNS data
proxy_dns 

# Some timeouts in milliseconds
tcp_read_time_out 15000
tcp_connect_time_out 8000

# ProxyList format
#  type  host  port [user pass]
#  (values separated by 'tab' or 'blank')
#
#
#  Examples:
#
#     socks5	192.168.67.78	1080	lamer	secret
#     http	192.168.89.3	8080	justu	hidden
#     socks4	192.168.1.49	1080
#     http	192.168.39.93	8080	
#		
# (...)
#
[ProxyList]
# add proxy here ...
# meanwile
# defaults set to "tor"
#socks4 	127.0.0.1 9050
socks5  127.0.0.1 9999

What does proxychains do exactly ?

Code is law ! Let’s have a look at the code from https://github.com/haad/proxychains

usage is something such as:

$ proxychains4 -f /etc/proxychains-other.conf wget https://www.targethost2.com

But by default -f option is not needed and /etc/proxychains.conf configuration file will be used.

Where is proxychains ?

thomas@tolva:~$ file $(which proxychains)
/usr/bin/proxychains: symbolic link to /etc/alternatives/proxychains
thomas@tolva:~$ file /etc/alternatives/proxychains
/etc/alternatives/proxychains: symbolic link to /usr/bin/proxychains4
thomas@tolva:~$ file /usr/bin/proxychains4 
/usr/bin/proxychains4: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=35739dc2dc6e333399f3b93a3b14a1a89f6e0bdd, for GNU/Linux 3.2.0, (...)

What does it depend on ?

$ ldd /usr/bin/proxychains4
    linux-vdso.so.1 (0x00007ffdc771a000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff7fb3be000)
    /lib64/ld-linux-x86-64.so.2 (0x00007ff7fb5bf000)

Here are the most important parts of proxychains/src/main.c :

(...)
int main(int argc, char *argv[]) {
(...)
    const char *prefix = NULL;

    for(i = 0; i < MAX_COMMANDLINE_FLAGS; i++) {
        if(start_argv < argc && argv[start_argv][0] == '-') {
            if(argv[start_argv][1] == 'q') {
                quiet = 1;
                start_argv++;
            } else if(argv[start_argv][1] == 'v') {
                printf("Proxychains4 version: %d.%d.%d\n", PROXYCHAINS_VERSION_MAJOR, PROXYCHAINS_VERSION_MINOR, PROXYCHAINS_VERSION_BUGFIX);
                exit(EXIT_SUCCESS);

            } else if(argv[start_argv][1] == 'f') {

                if(start_argv + 1 < argc)
                    path = argv[start_argv + 1];
                else
                    return usage(argv);

                start_argv += 2;
            }
        } else
            break;
    }
(...)
    /* Set PROXYCHAINS_CONF_FILE to get proxychains lib to use new config file. */
    setenv(PROXYCHAINS_CONF_FILE_ENV_VAR, path, 1);
(...)
    // search DLL
    set_own_dir(argv[0]);

    i = 0;
    while(dll_dirs[i]) {
        snprintf(buf, sizeof(buf), "%s/%s", dll_dirs[i], dll_name);
        if(access(buf, R_OK) != -1) {
            prefix = dll_dirs[i];
            break;
        }
        i++;
    }
(...)
    snprintf(buf, sizeof(buf), "%s/%s", prefix, dll_name);
    setenv("LD_PRELOAD", buf, 1);
(...)
    execvp(argv[start_argv], &argv[start_argv]);
(...)
}

To summarize,

proxychains wget https://192.168.2.1

launches

wget https://192.168.2.1

with /usr/lib/x86_64-linux-gnu/libproxychains.so.4

preloaded via LD_PRELOAD.

The preloaded library source code is in libproxychains.c.

Here is some commented extracts of its code:

(...)
connect_t true_connect;
gethostbyname_t true_gethostbyname;
getaddrinfo_t true_getaddrinfo;
freeaddrinfo_t true_freeaddrinfo;
getnameinfo_t true_getnameinfo;
gethostbyaddr_t true_gethostbyaddr;

Those true_ functions will be called in hooks:

The proxychained program will use redefined versions of connect, gethostbyname and so on.

Those redefined versions are called via the LD_PRELOAD mechanic,

and are implemented below. Each fake function will in fine call the real one.

Below is a part of the code of the load_sym() function:

(...)
static void* load_sym(char* symname, void* proxyfunc) {

    void *funcptr = dlsym(RTLD_NEXT, symname);
    if(!funcptr) {
        fprintf(stderr, "Cannot load symbol '%s' %s\n", symname, dlerror());
        exit(1);
    } else {
        PDEBUG("loaded symbol '%s'" " real addr %p  wrapped addr %p\n", symname, funcptr, proxyfunc);
    }
    if(funcptr == proxyfunc) {
        PDEBUG("circular reference detected, aborting!\n");
        abort();
    }
    return funcptr;
}

This load_sym function loads the real versions of hooked functions.

load_sym() is called via SETUP_SYM macro,

called via do_init() function,

called via init_lib_wrapper() function,

called in INIT() macro,

called at the begining of each hook.

As an example, let’s have a look to what the fake connect() does:

(...)
/*******  HOOK FUNCTIONS  *******/

int connect(int sock, const struct sockaddr *addr, socklen_t len) {
    int socktype = 0, flags = 0, ret = 0;
    socklen_t optlen = 0;
    ip_type dest_ip;
(...)
    struct in_addr *p_addr_in;
    struct sockaddr_in new_addr;
    dnat_arg *dnat = NULL;
    unsigned short port;
    size_t i;
    int remote_dns_connect = 0;

    INIT();
    optlen = sizeof(socktype);
    getsockopt(sock, SOL_SOCKET, SO_TYPE, &socktype, &optlen);
    if(!(SOCKFAMILY(*addr) == AF_INET && socktype == SOCK_STREAM))
        return true_connect(sock, addr, len);

    p_addr_in = &((struct sockaddr_in *) addr)->sin_addr;
    port = ntohs(((struct sockaddr_in *) addr)->sin_port);
(...)

    // check if connect called from proxydns
        remote_dns_connect = (ntohl(p_addr_in->s_addr) >> 24 == remote_dns_subnet);

    // more specific first
    for(i = 0; i < num_dnats && !remote_dns_connect && !dnat; i++)
        if(dnats[i].orig_dst.s_addr == p_addr_in->s_addr)
            if(dnats[i].orig_port && (dnats[i].orig_port == port))
                dnat = &dnats[i];

    for(i = 0; i < num_dnats && !remote_dns_connect && !dnat; i++)
        if(dnats[i].orig_dst.s_addr == p_addr_in->s_addr)
            if(!dnats[i].orig_port)
                dnat = &dnats[i];

    if (dnat) {
        if (dnat->new_port)
            new_addr.sin_port = htons(dnat->new_port);
        else
            new_addr.sin_port = htons(port);
        new_addr.sin_addr = dnat->new_dst;

        addr = (struct sockaddr *)&new_addr;
        p_addr_in = &((struct sockaddr_in *) addr)->sin_addr;
        port = ntohs(((struct sockaddr_in *) addr)->sin_port);
    }

    for(i = 0; i < num_localnet_addr && !remote_dns_connect; i++) {
        if((localnet_addr[i].in_addr.s_addr & localnet_addr[i].netmask.s_addr)
           == (p_addr_in->s_addr & localnet_addr[i].netmask.s_addr)) {
            if(!localnet_addr[i].port || localnet_addr[i].port == port) {
                PDEBUG("accessing localnet using true_connect\n");
                return true_connect(sock, addr, len);
            }
        }
    }

    flags = fcntl(sock, F_GETFL, 0);
    if(flags & O_NONBLOCK)
        fcntl(sock, F_SETFL, !O_NONBLOCK);

    dest_ip.as_int = SOCKADDR(*addr);

    ret = connect_proxy_chain(sock,
                  dest_ip,
                  SOCKPORT(*addr),
                  proxychains_pd, proxychains_proxy_count, proxychains_ct, proxychains_max_chain);

    fcntl(sock, F_SETFL, flags);
    if(ret != SUCCESS)
        errno = ECONNREFUSED;
    return ret;
}

To summarize, this function handles several cases where traffic shall not be proxified, and in its main path calls connect_proxy_chain().

This function is implemented in core.c. It performs a switch on the ct parameter.

int connect_proxy_chain(int sock, ip_type target_ip,
            unsigned short target_port, proxy_data * pd,
            unsigned int proxy_count, chain_type ct, unsigned int max_chain) {
(...)
    switch (ct) {
        case DYNAMIC_TYPE:
(...)

        case STRICT_TYPE:
(...)

        case RANDOM_TYPE:
(...)
    }
(...)
}

This ct parameter is an enum defined in core.h:

typedef enum {
    DYNAMIC_TYPE,
    STRICT_TYPE,
    RANDOM_TYPE}
chain_type;

Its purpose is to represent how proxies in the proxychains ProxyList are treated.
The possible values in /etc/proxychains.conf are

  • dynamic_chain: proxies are chained in their order of appearance in the config file. At least one proxy shall be alive and dead proxies are skipped
  • strict_chain: proxies are chained in their order of appearance in the config file. All of them must be alive.
  • random_chain: some proxies will be randomly chosen in the config file.

The default value in /etc/proxychains.conf is strict_chain:

#dynamic_chain
#
# Dynamic - Each connection will be done via chained proxies
# all proxies chained in the order as they appear in the list
# at least one proxy must be online to play in chain
# (dead proxies are skipped)
# otherwise EINTR is returned to the app
#
strict_chain
#
# Strict - Each connection will be done via chained proxies
# all proxies chained in the order as they appear in the list
# all proxies must be online to play in chain
# otherwise EINTR is returned to the app
#
#random_chain

Thus, *ct is set to STRICT_TYPE in the get_chain_data() function, which is (indirectly) called by the INIT() macro:

} else if(strstr(buff, "random_chain")) {
   *ct = RANDOM_TYPE;
} else if(strstr(buff, "strict_chain")) {
   *ct = STRICT_TYPE;
} else if(strstr(buff, "dynamic_chain")) {
   *ct = DYNAMIC_TYPE;

Let’s therefore focus on the case STRICT_TYPE of connect_proxy_chain() function:

case STRICT_TYPE:
    calc_alive(pd, proxy_count);
    offset = 0;
    if(!(p1 = select_proxy(FIFOLY, pd, proxy_count, &offset))) {
        PDEBUG("select_proxy failed\n");
        goto error_strict;
    }
    if(SUCCESS != start_chain(&ns, p1, ST)) {
        PDEBUG("start_chain failed\n");
        goto error_strict;
    }
    while(offset < proxy_count) {
        if(!(p2 = select_proxy(FIFOLY, pd, proxy_count, &offset)))
            break;
        if(SUCCESS != chain_step(ns, p1, p2)) {
            PDEBUG("chain_step failed\n");
            goto error_strict;
        }
        p1 = p2;
    }
    //proxychains_write_log(TP);
    p3->ip = target_ip;
    p3->port = target_port;
    if(SUCCESS != chain_step(ns, p1, p3))
        goto error;
    break;

This piece of code calls the start_chain() function.

Then a select_proxy() is called in a loop, until offset reaches proxy_count to select the next proxy in the proxychain (offset is incremented by select_proxy()).

As i’m not a proxychains power-user, i use the default configuration which only contains one proxy, and the code in this while loop is executed only one time.

The start_chain() function calls timed_connect() function which in its turn calls the true_connect() function.

The chain_steps() function is then called. This function itself calls the tunnel_to() function:

static int chain_step(int ns, proxy_data * pfrom, proxy_data * pto) {
    int retcode = -1;
    char *hostname;
    char ip_buf[16];

    PDEBUG("chain_step()\n");

    if(pto->ip.octet[0] == remote_dns_subnet) {
        hostname = string_from_internal_ip(pto->ip);
        if(!hostname)
            goto usenumericip;
    } else {
    usenumericip:
        inet_ntop(AF_INET, &pto->ip.octet[0], ip_buf, sizeof(ip_buf));
        hostname = ip_buf;
    }

    proxychains_write_log(TP " %s:%d ", hostname, htons(pto->port));
    retcode = tunnel_to(ns, pto->ip, pto->port, pfrom->pt, pfrom->user, pfrom->pass);
    switch (retcode) {
        case SUCCESS:
            pto->ps = BUSY_STATE;
            break;
        case BLOCKED:
            pto->ps = BLOCKED_STATE;
            proxychains_write_log("<--denied\n");
            close(ns);
            break;
        case SOCKET_ERROR:
            pto->ps = DOWN_STATE;
            proxychains_write_log("<--socket error or timeout!\n");
            close(ns);
            break;
    }
    return retcode;
}

This tunnel_to() function contains a switch over pt, the proxy type.

If the proxy is a socks5 proxy, the relevant switch case is case SOCKS5_TYPE, and a SOCKS5 packets exchange is made, as seen in the pcap.

Currently, proxychains supports four types of proxy:

typedef enum {
    HTTP_TYPE,
    RAW_TYPE,
    SOCKS4_TYPE,
    SOCKS5_TYPE
} proxy_type;

Again, in the default configuration, proxychains will attempt to connect to a SOCKS5 proxy, the pt argument will therefore be set to SOCKS5_TYPE.

Here is a sumup of the switch inside tunnel_to():

static int tunnel_to(int sock, ip_type ip, unsigned short port, proxy_type pt, char *user, char *pass) {
(...)
    switch (pt) {
(...)
        case SOCKS5_TYPE:{
(...)
            }
            break;
    }

    err:
    return SOCKET_ERROR;
}

In the SOCKS5_TYPE, the first packet sent to the socks server depends on authentication is used:

case SOCKS5_TYPE:{
        if(user) {
            buff[0] = 5;    //version
            buff[1] = 2;    //nomber of methods
            buff[2] = 0;    // no auth method
            buff[3] = 2;    /// auth method -> username / password
            if(4 != write_n_bytes(sock, (char *) buff, 4))
                goto err;
            } else {
            buff[0] = 5;    //version
            buff[1] = 1;    //nomber of methods
            buff[2] = 0;    // no auth method
            if(3 != write_n_bytes(sock, (char *) buff, 3))
                goto err;
        }

In the simplest configuration, no authentication is done, the if(user) won’t be True, and the first packet sent to the socks server will be

b'\x05\x01\x00'. This is indeed what is observed if we capture the traffic while interacting with an https server through to socks server:

The socks server response (that is to say the response of the SSH client on port 9999) is the following:

The socks server response is therefore b'\x05\x00': Server accepts to continue and no authentication is required.

The following part of code in the switch ensures that this server response is at least two bytes long:

if(2 != read_n_bytes(sock, (char *) buff, 2))
    goto err;

Then some checks are performed to ensure the first byte (encoding the protocol version) is set to 0x05,

and the second byte is set to 0x00 (no authentication) or 0x02 (user/password based authentication):

if(buff[0] != 5 || (buff[1] != 0 && buff[1] != 2)) {
    if(buff[0] == 5 && buff[1] == 0xFF)
        return BLOCKED;
    else
        goto err;
}

As we have seen, the packet received from the server is b'\x05\x00. The next block of code is executed only in case of user/password based authentication, so it will be skipped in our case as no authentication is in place.

The following piece of code to be executed will be this one:

size_t buff_iter = 0;
buff[buff_iter++] = 5;    // version
buff[buff_iter++] = 1;    // connect
buff[buff_iter++] = 0;    // reserved

if(!dns_len) {
    buff[buff_iter++] = 1;  // ip v4
    memcpy(buff + buff_iter, &ip, 4);   // dest host
    buff_iter += 4;
} else {
    buff[buff_iter++] = 3;  //dns
    buff[buff_iter++] = dns_len & 0xFF;
    memcpy(buff + buff_iter, dns_name, dns_len);
    buff_iter += dns_len;
}

memcpy(buff + buff_iter, &port, 2);    // dest port
buff_iter += 2;


if(buff_iter != write_n_bytes(sock, (char *) buff, buff_iter))
    goto err;

As we can see, a buffer having this structure:

  • first byte is the version byte (0x05)
  • second byte contains the value 0x01, which is the CONNECT method (see analysis of the rfc 1928, above)
  • third byte is the RSV byte, set to 0x00, as described in the rfc
  • Then comes a byte set to either 0x01, to indicate an IPv4 address, or 0x03, to indicate a DNS hostname. The following bytes are either the IPv4 address or hostname we want traffic be forwarded to
  • The last two bytes indicates the TCP port we we want traffic be forwared to.

is sent to the socks server.

The request packet observed in the pcap is b'\x05\x01\x00\x01\xc0\xa8\x02\x01\x01\xbb':

The IPv4 embedded in this packet is b\xc0\xa8\x02\x01, that is to say 192.168.2.1, and the TCP port is b\x01\xbb, that is to say 443, which is consistent with the initial command.

Then, the response from the socks server is read and analysed:

if(4 != read_n_bytes(sock, (char *) buff, 4))
    goto err;

if(buff[0] != 5 || buff[1] != 0)
    goto err;

switch (buff[3]) {

    case 1:
        len = 4;
        break;
    case 4:
        len = 16;
        break;
    case 3:
        len = 0;
        if(1 != read_n_bytes(sock, (char *) &len, 1))
            goto err;
        break;
    default:
        goto err;
}

As previously, the server response shall begin with 0x05.

Second byte shall be 0x00 (any other value means that connection didn’t succeed),

third byte is ignored (this byte, the RSV byte, should be set to 0x00, but the client doesn’t perform any check),

and a switch over buff[3] (that is to say over the value of ATYP) is performed.

The allowed values are 0x01 (in case of an IPv4 address), 0x04 (in case of an IPv6 address), and 0x03 (in case of a domain name).

In our study-case, the value 0x01 will be received, which will cause the len variable to be set to 0x04.

Finally, a check is performed to ensure that the length of remaining data in the server reply is consistent:

if(len + 2 != read_n_bytes(sock, (char *) buff, len + 2))
    goto err;

The last message is the server response who tells it accepts forwarding:

To sum up, the following dialogs takes place between proxychains and the socks server:

  • proxychained program sends a connection message
  • socks server (that is to say the SSH client) sends back a response
  • proxychained program sends a CONNECT request who basically tells « hi, i want my traffic to be forwared to the TCP port 443 of IPv4 address 192.168.2.1 »
  • sock server sends back a response telling whether it accepts or not the request.

Once done, if everything went well the traffic of proxychained program will be sent to the 9999 TCP port of SSH client, and will be therefore automagically transmitted to the 192.168.2.1:443 server via the SSH tunnel.

Then next packets exchanged with localhost:9999 after the socks handshake are packets from the TLS handshake:

Conclusion

We’ve seen how proxychains establishes a connection with a SOCKS server, and transparently hooks the real network functions to reroute traffic toward the SOCKS server who automagically forwards it.

But we know there is no such thing as « automagically » ! We will soon see how this magic works in SSH.


Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *