How SSH client and server handle port forwarding ?


In the previous article, we saw how an application can use a SOCKS client (such as proxychains) to contact a SOCKS server (the SSH client),

allowing it to reach an address who cannot be contacted directly.

Here we will see:

  • what happens when the SSH client is launched with a dynamic port forwarding string (through the -D option)
  • what the SSH client does when proxychains (or any other SOCKS client !) connects to it, and how it forwards traffic to the SSH server
  • how the SSH server receives the forwarded traffic and resends it to its final destination

Which source ?

We get the portable version of OpenSSH from github:

git clone https://github.com/openssh/openssh-portable.git

Then we checkout the last tag:

git checkout V_9_6_P1

We follow the steps described in INSTALL file to compile SSH.

What happens when we launch ssh -D 9999 user@host

How port forwarding string is parsed ?

When we launch the command ssh -D 9999 user@host, the piece of code (it’s part of main() function in ssh.c) below is executed:

case 'D':
    if (parse_forward(&fwd, optarg, 1, 0)) {
        add_local_forward(&options, &fwd);
    } else {
        fprintf(stderr,
            "Bad dynamic forwarding specification "
            "'%s'\n", optarg);
        exit(255);
    }
    break;

Two functions are called:

The first one is parse_forward(): Implemented in readconf.c, this function parses the port forwarding string into a struct Forward *:

/*
 * parse_forward
 * parses a string containing a port forwarding specification of the form:
 *   dynamicfwd == 0
 *    [listenhost:]listenport|listenpath:connecthost:connectport|connectpath
 *    listenpath:connectpath
 *   dynamicfwd == 1
 *    [listenhost:]listenport
 * returns number of arguments parsed or zero on error
 */
int
parse_forward(struct Forward *fwd, const char *fwdspec, int dynamicfwd, int remotefwd)

Here, fwdspec is optarg, that is to say 9999, dynamicfwd is set to 1, and remotefwd is 0.

The function will populate a struct Forward *:

/* Data structure for representing a forwarding request. */
struct Forward {
    char     *listen_host;      /* Host (address) to listen on. */
    int   listen_port;      /* Port to forward. */
    char     *listen_path;      /* Path to bind domain socket. */
    char     *connect_host;     /* Host to connect. */
    int   connect_port;     /* Port to connect on connect_host. */
    char     *connect_path;     /* Path to connect domain socket. */
    int   allocated_port;   /* Dynamically allocated listen port */
    int   handle;       /* Handle for dynamic listen ports */
};

The parse_forward() function parses the port forwarding argument (which here is « 9999 ») into &fwd:

switch (i) {
case 1:
    if (fwdargs[0].ispath) {
        fwd->listen_path = xstrdup(fwdargs[0].arg);
        fwd->listen_port = PORT_STREAMLOCAL;
    } else {
        fwd->listen_host = NULL;
        fwd->listen_port = a2port(fwdargs[0].arg);
    }
    fwd->connect_host = xstrdup("socks");
    break;

Let’s try to figure out the content of fwd with gdb:

(gdb) p i
$4 = 1
(gdb) p *((struct Forward *)fwd)
$5 = 
{
    listen_host = 0x0,
    listen_port = 9999,
    listen_path = 0x0,
    connect_host = 0x555555641a30 "socks",
    connect_port = 0,
    connect_path = 0x0,
    allocated_port = 0,
    handle = 0
}

Then comes a block how depends on dynamicfwd (which is set to 1 here, as we want to perform dynamic forwarding):

if (dynamicfwd) {
    if (!(i == 1 || i == 2))
        goto fail_free;
} else {
    if (!(i == 3 || i == 4)) {
        if (fwd->connect_path == NULL &&
            fwd->listen_path == NULL)
            goto fail_free;
    }
    if (fwd->connect_port <= 0 && fwd->connect_path == NULL)
        goto fail_free;
}

Here the value of i (which is the number of arguments in the port forwarding string) is 1, so nothing is executed.

The next block depends on a condition which is true only if the configuration put into fwd is not correct:

    if ((fwd->listen_port < 0 && fwd->listen_path == NULL) ||
        (!remotefwd && fwd->listen_port == 0))
        goto fail_free;

Then comes another block who depends on a condition true only if the configuration put into fwd is not correct:

    if (fwd->connect_host != NULL &&
        strlen(fwd->connect_host) >= NI_MAXHOST)
        goto fail_free;

After come several other sanity checks. All of them depends on conditions who are not satisfied in our situation, and we get out of the parse_forward() function.

The second function is add_local_forward():

This function is defined in readconf.c too.

It copies the content of fwd (obtained by parsing the port forwarding string with parse_forward()) into the local_forwards substructure of options:

/*
 * Adds a local TCP/IP port forward to options.  Never returns if there is an
 * error.
 */

void
add_local_forward(Options *options, const struct Forward *newfwd)
{
    struct Forward *fwd;
    int i;

    /* Don't add duplicates */
    for (i = 0; i < options->num_local_forwards; i++) {
        if (forward_equals(newfwd, options->local_forwards + i))
            return;
    }
    options->local_forwards = xreallocarray(options->local_forwards,
        options->num_local_forwards + 1,
        sizeof(*options->local_forwards));
    fwd = &options->local_forwards[options->num_local_forwards++];

    fwd->listen_host = newfwd->listen_host;
    fwd->listen_port = newfwd->listen_port;
    fwd->listen_path = newfwd->listen_path;
    fwd->connect_host = newfwd->connect_host;
    fwd->connect_port = newfwd->connect_port;
    fwd->connect_path = newfwd->connect_path;
}

By the way,

the line fwd = &options->local_forwards[options->num_local_forwards++]; increments options->num_local_forwards.

Where does the SSH calls bind() ?

The SSH client will necessarily start to listen on the port specified in the listen_port of the Forward structure. Let’s look for calls to the bind() function.

Several candidates are present:

$ grep -nre "bind(" ./ --exclude-dir={regress,autom4te.cache}
(...)
./sshconnect.c:417:    if (bind(sock, (struct sockaddr *)&bindaddr, bindaddrlen) != 0) {
./misc.c:1926:    if (bind(sock, (struct sockaddr *)&sunaddr, sizeof(sunaddr)) == -1) {
./channels.c:3858:        if (bind(sock, ai->ai_addr, ai->ai_addrlen) == -1) {
./channels.c:5028:            if (bind(sock, ai->ai_addr, ai->ai_addrlen) == -1) {
./sshd.c:1067:        if (bind(listen_sock, ai->ai_addr, ai->ai_addrlen) == -1) {
(...)
./openbsd-compat/bindresvport.c:102:        error = bind(sd, sa, salen);
./openbsd-compat/rresvport.c:90:        if (bind(s, sa, salen) >= 0)

Moreover, at this stage we do not know yet whether the SSH client starts to listen before or after connecting to the SSH server.

We therefore launch the SSH client with an high verbosity into gdb, after placing a breakpoint on the bind() function:

$ gdb ./ssh
(...)
(gdb) start -D 9999 thomas@1.2.3.4 -vvv
Temporary breakpoint 1 at 0xb770: file ssh.c, line 669.
Starting program: /home/thomas/bordels/openssh-portable/ssh -D 9999 thomas@1.2.3.4 -vvv
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Temporary breakpoint 1, main (ac=5, av=0x7fffffffe248) at ssh.c:669
669    {
(gdb) break bind
Breakpoint 2 at 0x7ffff7b9da80

When the secure channel with the SSH server is established, the user authenticates:

debug1: Authenticating to 1.2.3.4:22 as 'thomas'
(...)
debug1: Next authentication method: password
thomas@1.2.3.4's password: 
debug3: send packet: type 50
debug2: we sent a password packet, wait for reply
debug3: receive packet: type 52
Authenticated to 1.2.3.4 ([1.2.3.4]:22) using "password".

and bind() is called for the first time:

debug1: Local connections to LOCALHOST:9999 forwarded to remote address socks:0
debug3: channel_setup_fwd_listener_tcpip: type 2 wildcard 0 addr NULL

Breakpoint 2, 0x00007ffff7b9da80 in bind () from /lib/x86_64-linux-gnu/libc.so.6

We have a look at the backtrace:

(gdb) bt
#0  0x00007ffff7b9da80 in bind () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x00007ffff7bb8ec2 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#2  0x00007ffff7b863d1 in getaddrinfo () from /lib/x86_64-linux-gnu/libc.so.6
#3  0x000055555559ae31 in channel_setup_fwd_listener_tcpip (ssh=0x5555556406b0, type=2, fwd=0x555555642a50, allocated_listen_port=0x0, fwd_opts=0x7fffffffc330) at channels.c:3800
#4  0x00005555555622fb in ssh_init_forwarding (ifname=<synthetic pointer>, ssh=0x5555556406b0) at ssh.c:2045
#5  ssh_session2 (cinfo=0x555555644410, ssh=0x5555556406b0) at ssh.c:2207
#6  main (ac=<optimized out>, av=<optimized out>) at ssh.c:1787

The bind() function has the following signature:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

The most interesting argument is probably the second one. As we are studying a x64 binary, the address of this argument is stored in $rsi register.

We therefore print it as a const struct sockaddr *:

(gdb) p *((const struct sockaddr *)$rsi)
$1 = {sa_family = 16, sa_data = "\000\000\000\000\000\000\000\000\000\000\062\062) "}

The first field of this structure identifies the address family. To see what means the value 16, we have a look at socket.h:

#define PF_INET        2   /* IP protocol family.  */
#define PF_AX25        3   /* Amateur Radio AX.25.  */
#define PF_IPX        4   /* Novell Internet Protocol.  */
#define PF_APPLETALK    5   /* Appletalk DDP.  */
#define PF_NETROM    6   /* Amateur radio NetROM.  */
#define PF_BRIDGE    7   /* Multiprotocol bridge.  */
#define PF_ATMPVC    8   /* ATM PVCs.  */
#define PF_X25        9   /* Reserved for X.25 project.  */
#define PF_INET6    10  /* IP version 6.  */
#define PF_ROSE        11  /* Amateur Radio X.25 PLP.  */
#define PF_DECnet    12  /* Reserved for DECnet project.  */
#define PF_NETBEUI    13  /* Reserved for 802.2LLC project.  */
#define PF_SECURITY    14  /* Security callback pseudo AF.  */
#define PF_KEY        15  /* PF_KEY key management API.  */
#define PF_NETLINK    16

This is therefore a PF_NETLINK. According to the call stack, it seems that bind() was called by getaddrinfo(), which is called in channel_setup_fwd_listener_tcpip():

if ((r = getaddrinfo(addr, strport, &hints, &aitop)) != 0) {

Then we break a second time:

(gdb) c
Continuing.
debug3: sock_set_v6only: set socket 4 IPV6_V6ONLY
debug1: Local forwarding listening on ::1 port 9999.

Breakpoint 2, 0x00007ffff7b9da80 in bind () from /lib/x86_64-linux-gnu/libc.so.6

This time, bind() was explicitely called by channel_setup_fwd_listener_tcpip():

(gdb) bt
#0  0x00007ffff7b9da80 in bind () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x000055555559af38 in channel_setup_fwd_listener_tcpip (ssh=0x5555556406b0, type=2, fwd=0x555555642a50, allocated_listen_port=0x0, fwd_opts=0x7fffffffc330) at channels.c:3858
#2  0x00005555555622fb in ssh_init_forwarding (ifname=<synthetic pointer>, ssh=0x5555556406b0) at ssh.c:2045
#3  ssh_session2 (cinfo=0x555555644410, ssh=0x5555556406b0) at ssh.c:2207
#4  main (ac=<optimized out>, av=<optimized out>) at ssh.c:1787

We dump the addr:

(gdb) p *((const struct sockaddr *)$rsi)
$2 = {sa_family = 10, sa_data = "'\017", '\000' <repeats 11 times>}

This time, the sa_family is 10, which stands for PF_INET6. This is consistent with the « debug1: Local forwarding listening on ::1 port 9999. » debug message.

Moreover, if we list the connections when reaching the breakpoint:

$ netstat -tplen | grep ssh
(Tous les processus ne peuvent être identifiés, les infos sur les processus
non possédés ne seront pas affichées, vous devez être root pour les voir toutes.)

and just after:

$ netstat -tplen | grep ssh
(Tous les processus ne peuvent être identifiés, les infos sur les processus
non possédés ne seront pas affichées, vous devez être root pour les voir toutes.)
tcp6       0      0 ::1:9999                :::*                    LISTEN      1000       9142268    1420192/ssh

We see that SSH client is now listening on 9999 TCP/IPv6 port.

We reach the breakpoint a third time:

debug1: channel 0: new port-listener [port listener] (inactive timeout: 0)
debug1: Local forwarding listening on 127.0.0.1 port 9999.

Breakpoint 2, 0x00007ffff7b9da80 in bind () from /lib/x86_64-linux-gnu/libc.so.6
(gdb) bt
#0  0x00007ffff7b9da80 in bind () from /lib/x86_64-linux-gnu/libc.so.6
#1  0x000055555559af38 in channel_setup_fwd_listener_tcpip (ssh=0x5555556406b0, type=2, fwd=0x555555642a50, allocated_listen_port=0x0, fwd_opts=0x7fffffffc330) at channels.c:3858
#2  0x00005555555622fb in ssh_init_forwarding (ifname=<synthetic pointer>, ssh=0x5555556406b0) at ssh.c:2045
#3  ssh_session2 (cinfo=0x555555644410, ssh=0x5555556406b0) at ssh.c:2207
#4  main (ac=<optimized out>, av=<optimized out>) at ssh.c:1787

As we can see, the callstack is still the same, and the debug message « debug1: Local forwarding listening on 127.0.0.1 port 9999. » leads to think SSH client is starting to listen on the 9999 TCP/IPv4 port.

This is consistent with the addr argument:

(gdb) p *((const struct sockaddr *)$rsi)
$3 = {sa_family = 2, sa_data = "'\017\177\000\000\001\000\000\000\000\000\000\000"}

Indeed, 2 stands for PF_INET.

And if we netstat just after entering continue in gdb, we can see that SSH client is now listening on the 9999 TCP/IPv4 port:

$ netstat -tplen | grep ssh
(Tous les processus ne peuvent être identifiés, les infos sur les processus
non possédés ne seront pas affichées, vous devez être root pour les voir toutes.)
tcp        0      0 127.0.0.1:9999          0.0.0.0:*               LISTEN      1000       9160728    1420192/ssh         
tcp6       0      0 ::1:9999                :::*                    LISTEN      1000       9142268    1420192/ssh

So let’s sum up:

  • The ssh_session2() is called at the very end of the main() function of the client:
 skip_connect:
    exit_status = ssh_session2(ssh, cinfo);
    ssh_conn_info_free(cinfo);
    ssh_packet_close(ssh);

    if (options.control_path != NULL && muxserver_sock != -1)
        unlink(options.control_path);

    /* Kill ProxyCommand if it is running. */
    ssh_kill_proxy_command();

    return exit_status;
}

At this point, the port forwarding string has been handled far more earlier in the beginning of the main() function.

Calling ssh_init_forwarding() is one of the first things done in ssh_session2():

static int
ssh_session2(struct ssh *ssh, const struct ssh_conn_info *cinfo)
{
    int r, interactive, id = -1;
    char *cp, *tun_fwd_ifname = NULL;

    /* XXX should be pre-session */
    if (!options.control_persist)
        ssh_init_stdio_forwarding(ssh);

    ssh_init_forwarding(ssh, &tun_fwd_ifname);

The ssh_init_forwarding() function initializes both TC/IP local and remote forwarding:

static void
ssh_init_forwarding(struct ssh *ssh, char **ifname)
{
    int success = 0;
    int i;

    ssh_init_forward_permissions(ssh, "permitremoteopen",
        options.permitted_remote_opens,
        options.num_permitted_remote_opens);

    if (options.exit_on_forward_failure)
        forward_confirms_pending = 0; /* track pending requests */
    /* Initiate local TCP/IP port forwardings. */
    for (i = 0; i < options.num_local_forwards; i++) {
        debug("Local connections to %.200s:%d forwarded to remote "
            "address %.200s:%d",
            (options.local_forwards[i].listen_path != NULL) ?
            options.local_forwards[i].listen_path :
            (options.local_forwards[i].listen_host == NULL) ?
            (options.fwd_opts.gateway_ports ? "*" : "LOCALHOST") :
            options.local_forwards[i].listen_host,
            options.local_forwards[i].listen_port,
            (options.local_forwards[i].connect_path != NULL) ?
            options.local_forwards[i].connect_path :
            options.local_forwards[i].connect_host,
            options.local_forwards[i].connect_port);
        success += channel_setup_local_fwd_listener(ssh,
            &options.local_forwards[i], &options.fwd_opts);
    }
    if (i > 0 && success != i && options.exit_on_forward_failure)
        fatal("Could not request local forwarding.");
    if (i > 0 && success == 0)
        error("Could not request local forwarding.");

    /* Initiate remote TCP/IP port forwardings. */
    (...)

Here options.num_local_forwards is set 1 (it is initially set to 0 and incremented in add_local_forward),

and channel_setup_local_fwd_listener is therefore called once in our context.

This function essentially calls channel_setup_fwd_listener_streamlocal or channel_setup_fwd_listener_tcpip on whether fwd->listen_path is NULL or not:

/* protocol local port fwd, used by ssh */
int
channel_setup_local_fwd_listener(struct ssh *ssh,
    struct Forward *fwd, struct ForwardOptions *fwd_opts)
{
    if (fwd->listen_path != NULL) {
        return channel_setup_fwd_listener_streamlocal(ssh,
            SSH_CHANNEL_UNIX_LISTENER, fwd, fwd_opts);
    } else {
        return channel_setup_fwd_listener_tcpip(ssh,
            SSH_CHANNEL_PORT_LISTENER, fwd, NULL, fwd_opts);
    }
}

In our context, fwd->listen_path is NULL so channel_setup_fwd_listener_tcpip is called.

In channel_setup_fwd_listener_tcpip, the first call to bind() is done by getaddrinfo().

The getaddrinfo() function is used to enumerate the network addresses of the SSH client.

    /*
     * getaddrinfo returns a loopback address if the hostname is
     * set to NULL and hints.ai_flags is not AI_PASSIVE
     */
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = ssh->chanctxt->IPv4or6;
    hints.ai_flags = wildcard ? AI_PASSIVE : 0;
    hints.ai_socktype = SOCK_STREAM;
    snprintf(strport, sizeof strport, "%d", fwd->listen_port);
    if ((r = getaddrinfo(addr, strport, &hints, &aitop)) != 0) {
        if (addr == NULL) {
            /* This really shouldn't happen */
            ssh_packet_disconnect(ssh, "getaddrinfo: fatal error: %s",
                ssh_gai_strerror(r));
        } else {
            error_f("getaddrinfo(%.64s): %s", addr,
                ssh_gai_strerror(r));
        }
        return 0;
    }

The aitop is a chained-list of struct addrinfo.

The channel_setup_fwd_listener_tcpip function then iterates over this list.

Each turn of the loop calls:

  • socket() to create a new socket
  • bind() on the address stored in ai, and
  • listen() to start listening for connection on the new socket bind and the specified address:
    for (ai = aitop; ai; ai = ai->ai_next) {
(...)
        /* Create a port to listen for the host. */
        sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
        if (sock == -1) {
            /* this is no error since kernel may not support ipv6 */
            verbose("socket [%s]:%s: %.100s", ntop, strport,
                strerror(errno));
            continue;
        }
(...)
        debug("Local forwarding listening on %s port %s.",
            ntop, strport);

        /* Bind the socket to the address. */
        if (bind(sock, ai->ai_addr, ai->ai_addrlen) == -1) {
(...)        
        /* Start listening for connections on the socket. */
        if (listen(sock, SSH_LISTEN_BACKLOG) == -1) {
            error("listen [%s]:%s: %.100s", ntop, strport,
                strerror(errno));
            close(sock);
            continue;
        }       
(...)        
    }

This concretely explains why the SSH client starts to listen on ::1:9999 (tcp6)

and on 127.0.0.1:9999 (tcp).

What happens when proxychains connects to the SSH client ?

As we’ve seen, the parsing of dynamic port forwarding string happens in the beginning of the main() function,

and one of the last thing done in main() is calling ssh_session2() function:

 skip_connect:
    exit_status = ssh_session2(ssh, cinfo);
    ssh_conn_info_free(cinfo);
    ssh_packet_close(ssh);

    if (options.control_path != NULL && muxserver_sock != -1)
        unlink(options.control_path);

    /* Kill ProxyCommand if it is running. */
    ssh_kill_proxy_command();

    return exit_status;
}

The ssh_init_forwarding() function who indirectly bind() and starts to listen() as a SOCKS server is called at the beginning of the ssh_session2() function.

At its end, this function then calls the client_loop() function, which is the SSH client endless loop.

    return client_loop(ssh, tty_flag, tty_flag ?
        options.escape_char : SSH_ESCAPECHAR_NONE, id);
}

Every further interaction with the SSH client will therefore be handled by the client_loop() function, at least indirectly.

According to the chapter 7.2 of RFC 4524 (who describes the SSH connection protocol),
a client who wants to forward a local port to the other side will send a SSH_MSG_CHANNEL_OPEN packet, whose string will be « direct-tcpip »:

byte      SSH_MSG_CHANNEL_OPEN
string    "direct-tcpip"
uint32    sender channel
uint32    initial window size
uint32    maximum packet size
string    host to connect
uint32    port to connect
string    originator IP address
uint32    originator port

Moreover, we also know that data transfer from client to server will imply sending of SSH_MSG_CHANNEL_DATA packets.

To understand what happens, we can therefore set a breakpoint on the function dedicated to sending SSH packets, and have a look at the backtrace.

Digging a little bit in the source code leads to the sshpkt_send() function:

/* send it */
int
sshpkt_send(struct ssh *ssh)
{
    if (ssh->state && ssh->state->mux)
        return ssh_packet_send_mux(ssh);
    return ssh_packet_send2(ssh);
}

We could do this manually but let’s simplify our life and make a gdb script instead:

file ./ssh

start thomas@1.2.3.4 -D 9999

break sshpkt_send

set var $sshpkt_send_addr = sshpkt_send

continue

while 1

    if $rip == $sshpkt_send_addr
        print "i have reached sshpkt_send function !"
        print "backtrace:"
        bt

        print "outgoing packet:"

        set $state = (*(struct ssh *)$rdi)->state
        set $pkt = (*(struct session_state *)$state)->outgoing_packet
        set $buf = (*(struct sshbuf *)$pkt)->d
        x /64c $buf

        continue
    end
end

This script will:

  • break on every sshpkt_send() call;
  • print the backtrace;
  • dump the content plaintext content of the SSH packet.

It leads to the following observations:

Opening of « direct-tcpip » channel

Sending the SSH_MSG_CHANNEL_OPEN involves the following sequence of function calls:

client_loop()
 |
 +--> client_wait_until_can_do_something()
       |
       +--> channel_prepare_poll()
             |
             +--> channel_handler()
                   |
                   +--> port_open_helper()
                         |
                         +--> sshpkt_send()

Sending of forwarded data

Sending the SSH_MSG_CHANNEL_DATA involves the following sequence of function calls:

client_loop()
 |
 +--> channel_output_poll()
       |
       +--> channel_output_poll_input_open()
             |
             +--> sshpkt_send()

End of sending

Sending the SSH_MSG_CHANNEL_EOF involves the following sequence of function calls:

client_loop()
 |
 +--> channel_output_poll()
       |
       +--> channel_output_poll_input_open()
             |
             +--> chan_ibuf_empty()
                   |
                   +--> chan_send_eof2()
                         |
                         +--> sshpkt_send()

Channel closure

Sending the SSH_MSG_CHANNEL_CLOSE involves the following sequence of function calls:

client_loop()
 |
 +--> client_wait_until_can_do_something()
       |
       +--> channel_prepare_poll()
             |
             +--> channel_handler()
                   |
                   +---> channel_garbage_collect()
                          |
                          +--> chan_is_dead()
                                |
                                +--> chan_send_close2()
                                      |
                                      +-> sshpkt_send()`

Handling of client connection

The proxychains connection to SSH acting as a SOCKS server is done via the following function calls:

client_loop()
 |
 +--> client_wait_until_can_do_something()
       |
       +--> channel_prepare_poll()
             |
             +--> channel_handler()
                   |
                   +--> channel_pre_dynamic()
                         |
                         +--> channel_decode_socks5()

What data is exchanged ?

When we proxychains wget http://www.somewhere.com,

we see two sequences of

SSH_MSG_CHANNEL_OPEN/SSH_MSG_CHANNEL_DATA/SSH_MSG_CHANNEL_EOF/SSH_MSG_CHANNEL_CLOSE.

The first one is made to perform a DNS request: what is the IP address of www.somewhere.com ?

The second one is dedicated to the HTTP exchange properly speaking.

How does the SSH server forwards the proxychains traffic to the destination ?

We use the same method as on the client side: We recompile the server from the github source, we put breakpoints with gdb and voilà !

  • Forwarding the DNS request from the client:
server_loop2()
|
+-> process_buffered_input_packets()
    |
    +-> ssh_dispatch_run_fatal()
        |
        +-> ssh_dispatch_run()
            |
            +-> server_input_channel_open()
                |
                +-> server_request_direct_tcpip()
                    |
                    +-> channel_connect_to_port(ssh, "4.2.2.2", 53, "direct-tcpip", ...)
                        |
                        +-> connect_to_helper(ssh, "4.2.2.2", 53, ...)
                            |
                            +-> connect_next()
                                |
                                +-> __libc_connect()
  • Forwarding the HTTPS traffic from the client:
server_loop2()
 |
 +-> process_buffered_input_packets()
      |
      +-> ssh_dispatch_run_fatal()
           |
           +-> ssh_dispatch_run()
                |
                +-> server_input_channel_open()
                     |
                     +-> server_request_direct_tcpip()
                          |
                          +-> channel_connect_to_port(..., "80.77.95.49", 443, ...)
                               |
                               +-> connect_to_helper(ssh, "80.77.95.49", 443, ...)
                                    |
                                    +-> connect_next()
                                         |
                                         +-> __libc_connect()

Laisser un commentaire

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