ssh client代码阅读 (unfinished)

继续来读代码。今天读ssh client。在这之前,让我们先了解一下ssh协议。

SSH协议介绍如下:
https://www.ssh.com/academy/ssh/protocol

SSH协议是一个经典的CS模型,SSH的客户端主导连接设置过程,并使用公钥加密来验证SSH服务器的身份。
在设置阶段之后,SSH协议使用强对称加密和散列算法来确保客户端和服务器之间交换的数据的私密性和完整性。

1. 客户端链接服务器;(C->S)
2. 服务端发送服务端的公钥;(S->C)
3. 服务端和客户端互相沟通,产生一个安全通道;(C<->S)
4. 用户登录到服务端的操作系统里

SSH常用的协议包括:

RFC 4251 - Secure Shell (SSH) 协议架构
RFC 4253 - Secure Shell (SSH) 传输层协议
RFC 4252 - Secure Shell (SSH) 身份验证协议
RFC 4254 - Secure Shell (SSH) 连接协议

以及一个基于SSH的,SFTP文件传输协议。看RFC不如直接看代码,开始。在ssh.c中有一个大型结构体session_state。connection_in和out两个变量用于保存通信时的文件描述符,connection_in用于读取;out用于写入;如果是socket的话,他们两个可以是同一个描述符。对应的,receive/send_context用于加解密时两个方向的上下文。input/output用于加解密时传输的原始数据。incoming/outgoing_packet是当前正在处理的收取/发送包。compression_buffer用于packet的加解密。

struct session_state {
    int connection_in;
    int connection_out;

    /* Protocol flags for the remote side. */
    u_int remote_protocol_flags;

    /* Encryption context for receiving data.  Only used for decryption. */
    struct sshcipher_ctx *receive_context;

    /* Encryption context for sending data.  Only used for encryption. */
    struct sshcipher_ctx *send_context;

    /* Buffer for raw input data from the socket. */
    struct sshbuf *input;

    /* Buffer for raw output data going to the socket. */
    struct sshbuf *output;

    /* Buffer for the partial outgoing packet being constructed. */
    struct sshbuf *outgoing_packet;

    /* Buffer for the incoming packet currently being processed. */
    struct sshbuf *incoming_packet;

    /* Scratch buffer for packet compression/decompression. */
    struct sshbuf *compression_buffer;

#ifdef WITH_ZLIB
    /* Incoming/outgoing compression dictionaries */
    z_stream compression_in_stream;
    z_stream compression_out_stream;
#endif
    int compression_in_started;
    int compression_out_started;
    int compression_in_failures;
    int compression_out_failures;

    /* default maximum packet size */
    u_int max_packet_size;

    /* Flag indicating whether this module has been initialized. */
    int initialized;

    /* Set to true if the connection is interactive. */
    int interactive_mode;

    /* Set to true if we are the server side. */
    int server_side;

    /* Set to true if we are authenticated. */
    int after_authentication;

    int keep_alive_timeouts;

    /* The maximum time that we will wait to send or receive a packet */
    int packet_timeout_ms;

    /* Session key information for Encryption and MAC */
    struct newkeys *newkeys[MODE_MAX];
    struct packet_state p_read, p_send;

    /* Volume-based rekeying */
    u_int64_t max_blocks_in, max_blocks_out, rekey_limit;

    /* Time-based rekeying */
    u_int32_t rekey_interval;   /* how often in seconds */
    time_t rekey_time;  /* time of last rekeying */

    /* roundup current message to extra_pad bytes */
    u_char extra_pad;

    /* XXX discard incoming data after MAC error */
    u_int packet_discard;
    size_t packet_discard_mac_already;
    struct sshmac *packet_discard_mac;

    /* Used in packet_read_poll2() */
    u_int packlen;

    /* Used in packet_send2 */
    int rekeying;

    /* Used in ssh_packet_send_mux() */
    int mux;

    /* Used in packet_set_interactive */
    int set_interactive_called;

    /* Used in packet_set_maxsize */
    int set_maxsize_called;

    /* One-off warning about weak ciphers */
    int cipher_warning_done;

    /* Hook for fuzzing inbound packets */
    ssh_packet_hook_fn *hook_in;
    void *hook_in_ctx;

    TAILQ_HEAD(, packet) outgoing;
};

以incoming_packet为例,可以在main等 --> ssh_packet_set_connection --> ssh_alloc_session_state 中看到它的踪迹。ssh_alloc_session_state()初始化一个ssh结构体。

struct ssh *
ssh_alloc_session_state(void)
{
    struct ssh *ssh = NULL;
    struct session_state *state = NULL;

    if ((ssh = calloc(1, sizeof(*ssh))) == NULL ||
        (state = calloc(1, sizeof(*state))) == NULL ||
        (ssh->kex = kex_new()) == NULL ||
        (state->input = sshbuf_new()) == NULL ||
        (state->output = sshbuf_new()) == NULL ||
        (state->outgoing_packet = sshbuf_new()) == NULL ||
        (state->incoming_packet = sshbuf_new()) == NULL)
        goto fail;
    TAILQ_INIT(&state->outgoing);
    TAILQ_INIT(&ssh->private_keys);
    TAILQ_INIT(&ssh->public_keys);
    state->connection_in = -1;
    state->connection_out = -1;
    state->max_packet_size = 32768;
    state->packet_timeout_ms = -1;
    state->p_send.packets = state->p_read.packets = 0;
    state->initialized = 1;
    /*
     * ssh_packet_send2() needs to queue packets until
     * we've done the initial key exchange.
     */
    state->rekeying = 1;
    ssh->state = state;
    return ssh;
 fail:
    if (ssh) {
        kex_free(ssh->kex);
        free(ssh);
    }
    if (state) {
        sshbuf_free(state->input);
        sshbuf_free(state->output);
        sshbuf_free(state->incoming_packet);
        sshbuf_free(state->outgoing_packet);
        free(state);
    }
    return NULL;
}

在上述函数中出现的sshbuf_new定义如下。它新建一个sshbuf结构体,并申请256(SSHBUF_SIZE_INIT)字节的初始内存,最大内存是0x800000(SSHBUF_SIZE_MAX)。同时可以看到它具有一个refcount。

struct sshbuf *
sshbuf_new(void)
{
    struct sshbuf *ret;

    if ((ret = calloc(sizeof(*ret), 1)) == NULL)
        return NULL;
    ret->alloc = SSHBUF_SIZE_INIT;
    ret->max_size = SSHBUF_SIZE_MAX;
    ret->readonly = 0;
    ret->refcount = 1;
    ret->parent = NULL;
    if ((ret->cd = ret->d = calloc(1, ret->alloc)) == NULL) {
        free(ret);
        return NULL;
    }
    return ret;
}

继续以incoming_packet为例,可以看到它被引用的位置全部都在packet.c中。因此这个文件一定是与收发包相关的重要文件。从密度上看,ssh_packet_read_poll2是密度最高的,它有如下的调用关系:ssh_packet_read (ssh_packet_read_expect) --> ssh_packet_read_seqnr --> ssh_packet_read_poll_seqnr --> ssh_packet_read_poll2。其中,ssh_packet_read(_expect)没有任何引用,因此放弃这条路线;另一条路线是main (skip_connect label) --> ssh_session2 --> client_loop --> client_process_buffered_input_packets(clientloop.c,serverloop.c中也有类似路径) --> ssh_dispatch_run_fatal --> ssh_dispatch_run --> ssh_packet_read_seqnr --> ssh_packet_read_poll_seqnr --> ssh_packet_read_poll2。显然下面这条是client的路径。我们先不看main,而是直接从client_loop这里开始。

client_loop用于实现与服务器的交互会话。它在用户通过身份验证,并且在远程主机上启动了一个命令后被调用。 escape_char是 SSH_ESCAPECHAR_NONE以外的字符时,会被用于终止或暂停会话的控制字符。

client_loop的第一阶段,初始化变量,如下所示。

/* Initialize variables. */
last_was_cr = 1;
exit_status = -1;
connection_in = ssh_packet_get_connection_in(ssh);
connection_out = ssh_packet_get_connection_out(ssh);

/* Returns the socket used for reading. */

int
ssh_packet_get_connection_in(struct ssh *ssh)
{
    return ssh->state->connection_in;
}

/* Returns the descriptor used for writing. */

int
ssh_packet_get_connection_out(struct ssh *ssh)
{
    return ssh->state->connection_out;
}

然后,设置不同的信号由signal_handler处理。

/*
 * Set signal handlers, (e.g. to restore non-blocking mode)
 * but don't overwrite SIG_IGN, matches behaviour from rsh(1)
 */
if (ssh_signal(SIGHUP, SIG_IGN) != SIG_IGN)
    ssh_signal(SIGHUP, signal_handler);
if (ssh_signal(SIGINT, SIG_IGN) != SIG_IGN)
    ssh_signal(SIGINT, signal_handler);
if (ssh_signal(SIGQUIT, SIG_IGN) != SIG_IGN)
    ssh_signal(SIGQUIT, signal_handler);
if (ssh_signal(SIGTERM, SIG_IGN) != SIG_IGN)
    ssh_signal(SIGTERM, signal_handler);
ssh_signal(SIGWINCH, window_change_handler);

然后是检查是否有pty,如果有则调用enter_raw_mode进入交互模式。接着是设置escape_char_arg,设置定期的服务器活动检查。

if (have_pty)
    enter_raw_mode(options.request_tty == REQUEST_TTY_FORCE);

session_ident = ssh2_chan_id;
if (session_ident != -1) {
    if (escape_char_arg != SSH_ESCAPECHAR_NONE) {
        channel_register_filter(ssh, session_ident,
            client_simple_escape_filter, NULL,
            client_filter_cleanup,
            client_new_escape_filter_ctx(
            escape_char_arg));
    }
    channel_register_cleanup(ssh, session_ident,
        client_channel_closed, 0);
}

schedule_server_alive_check();

接下来进入大循环。只要还正常工作,就在循环的开头处理服务器传来的packet。

/* Main loop of the client for the interactive session mode. */
while (!quit_pending) {

    /* Process buffered packets sent by the server. */
    client_process_buffered_input_packets(ssh);

处理代码如下,核心在于ssh_dispatch_run。这也正是链条上的:main --> ssh_session2 --> client_loop --> client_process_buffered_input_packets --> ssh_dispatch_run_fatal --> [[ssh_dispatch_run]] --> ssh_packet_read_seqnr --> ssh_packet_read_poll_seqnr --> ssh_packet_read_poll2 这个位置。让我们仔细阅读。

int
ssh_dispatch_run(struct ssh *ssh, int mode, volatile sig_atomic_t *done)
{
    int r;
    u_char type;
    u_int32_t seqnr;

    for (;;) {
        if (mode == DISPATCH_BLOCK) {
            r = ssh_packet_read_seqnr(ssh, &type, &seqnr);
            if (r != 0)
                return r;
        } else {
            r = ssh_packet_read_poll_seqnr(ssh, &type, &seqnr);
            if (r != 0)
                return r;
            if (type == SSH_MSG_NONE)
                return 0;
        }
        if (type > 0 && type < DISPATCH_MAX &&
            ssh->dispatch[type] != NULL) {
            if (ssh->dispatch_skip_packets) {
                debug2("skipped packet (type %u)", type);
                ssh->dispatch_skip_packets--;
                continue;
            }
            r = (*ssh->dispatch[type])(type, seqnr, ssh);
            if (r != 0)
                return r;
        } else {
            r = sshpkt_disconnect(ssh,
                "protocol error: rcvd type %d", type);
            if (r != 0)
                return r;
            return SSH_ERR_DISCONNECTED;
        }
        if (done != NULL && *done)
            return 0;
    }
}

void
ssh_dispatch_run_fatal(struct ssh *ssh, int mode, volatile sig_atomic_t *done)
{
    int r;

    if ((r = ssh_dispatch_run(ssh, mode, done)) != 0)
        sshpkt_fatal(ssh, r, "%s", __func__);
}
static void
client_process_buffered_input_packets(struct ssh *ssh)
{
    ssh_dispatch_run_fatal(ssh, DISPATCH_NONBLOCK, &quit_pending);
}

首先,基于是否阻塞,选择调用ssh_packet_read_seqnr还是ssh_packet_read_poll_seqnr。获取一个type,并用于不同的回调函数上(*ssh->dispatch[type])(type, seqnr, ssh);。这里也是一个典型的状态机没跑了。

    if (mode == DISPATCH_BLOCK) {
        r = ssh_packet_read_seqnr(ssh, &type, &seqnr);
        if (r != 0)
            return r;
    } else {
        r = ssh_packet_read_poll_seqnr(ssh, &type, &seqnr);
        if (r != 0)
            return r;
        if (type == SSH_MSG_NONE)
            return 0;
    }

其余的并没有太多有实际影响的操作,让我们跟入ssh_packet_read_seqnr。它有一个8192字节的buf,等待ssh server的输入,如果有的话,则调用ssh_packet_read_poll_seqnr,这很好,因为上面我们也准备看这个函数。刚好可以一起看掉。

int
ssh_packet_read_seqnr(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p)
{
    struct session_state *state = ssh->state;
    int len, r, ms_remain;
    struct pollfd pfd;
    char buf[8192];
    struct timeval start;
    struct timespec timespec, *timespecp = NULL;

    DBG(debug("packet_read()"));

    /*
     * Since we are blocking, ensure that all written packets have
     * been sent.
     */
    if ((r = ssh_packet_write_wait(ssh)) != 0)
        goto out;

    /* Stay in the loop until we have received a complete packet. */
    for (;;) {
        /* Try to read a packet from the buffer. */
        r = ssh_packet_read_poll_seqnr(ssh, typep, seqnr_p);
        if (r != 0)
            break;
        /* If we got a packet, return it. */
        if (*typep != SSH_MSG_NONE)
            break;

目光转向ssh_packet_read_poll_seqnr。它调用ssh_packet_read_poll2,并根据消息类型来加以处理。

int
ssh_packet_read_poll_seqnr(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p)
{
    struct session_state *state = ssh->state;
    u_int reason, seqnr;
    int r;
    u_char *msg;

    for (;;) {
        msg = NULL;
        r = ssh_packet_read_poll2(ssh, typep, seqnr_p);
        if (r != 0)
            return r;
        if (*typep) {
            state->keep_alive_timeouts = 0;
            DBG(debug("received packet type %d", *typep));
        }
        switch (*typep) {

对于ssh_packet_read_poll2,如果是mux状态(multiplex多路),则转交给ssh_packet_read_poll2_mux来处理,否则进入自己的流程。

int
ssh_packet_read_poll2(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p)
{
    struct session_state *state = ssh->state;
    u_int padlen, need;
    u_char *cp;
    u_int maclen, aadlen = 0, authlen = 0, block_size;
    struct sshenc *enc   = NULL;
    struct sshmac *mac   = NULL;
    struct sshcomp *comp = NULL;
    int r;

    if (state->mux)
        return ssh_packet_read_poll2_mux(ssh, typep, seqnr_p);

    *typep = SSH_MSG_NONE;

老规矩,先看看ssh_packet_read_poll32_mux。代码不长。这里会遇到几个工具函数,第一个是sshbuf_ptr。它先检查buf是否合法。这个检查非常严格,包括buf是否已经不在使用,超出大小,长度不正常,offset已经超过其偏移。这些都会导致其抛出SIGSEGV。如果ok的话,返回cd + off。cd是buffer的const data。它是另一个成员“d”的拷贝,只不过加了修饰符const使其不能被修改。当然在编译后就没有区别了。

static inline int
sshbuf_check_sanity(const struct sshbuf *buf)
{
    SSHBUF_TELL("sanity");
    if (__predict_false(buf == NULL ||
        (!buf->readonly && buf->d != buf->cd) ||
        buf->refcount < 1 || buf->refcount > SSHBUF_REFS_MAX ||
        buf->cd == NULL ||
        buf->max_size > SSHBUF_SIZE_MAX ||
        buf->alloc > buf->max_size ||
        buf->size > buf->alloc ||
        buf->off > buf->size)) {
        /* Do not try to recover from corrupted buffer internals */
        SSHBUF_DBG(("SSH_ERR_INTERNAL_ERROR"));
        ssh_signal(SIGSEGV, SIG_DFL);
        raise(SIGSEGV);
        return SSH_ERR_INTERNAL_ERROR;
    }
    return 0;
}

const u_char *
sshbuf_ptr(const struct sshbuf *buf)
{
    if (sshbuf_check_sanity(buf) != 0)
        return NULL;
    return buf->cd + buf->off;
}

static int
ssh_packet_read_poll2_mux(struct ssh *ssh, u_char *typep, u_int32_t *seqnr_p)
{
    struct session_state *state = ssh->state;
    const u_char *cp;
    size_t need;
    int r;

    if (ssh->kex)
        return SSH_ERR_INTERNAL_ERROR;
    *typep = SSH_MSG_NONE;
    cp = sshbuf_ptr(state->input);
    if (state->packlen == 0) {
        if (sshbuf_len(state->input) < 4 + 1)
            return 0; /* packet is incomplete */
        state->packlen = PEEK_U32(cp);
        if (state->packlen < 4 + 1 ||
            state->packlen > PACKET_MAX_SIZE)
            return SSH_ERR_MESSAGE_INCOMPLETE;
    }
    need = state->packlen + 4;
    if (sshbuf_len(state->input) < need)
        return 0; /* packet is incomplete */
    sshbuf_reset(state->incoming_packet);
    if ((r = sshbuf_put(state->incoming_packet, cp + 4,
        state->packlen)) != 0 ||
        (r = sshbuf_consume(state->input, need)) != 0 ||
        (r = sshbuf_get_u8(state->incoming_packet, NULL)) != 0 ||
        (r = sshbuf_get_u8(state->incoming_packet, typep)) != 0)
        return r;
    if (ssh_packet_log_type(*typep))
        debug3_f("type %u", *typep);
    /* sshbuf_dump(state->incoming_packet, stderr); */
    /* reset for next packet */
    state->packlen = 0;
    return r;
}

上面这段太长了,我们单独粘出来看。如果当前packlen是0,检查input中的长度是否大于4,大于4的话,取出32字节,作为packlen。它是一个unsigned int类型,下面随后检查大小是否合法(在5~PACKET_MAX_SIZE之间)。PACKET_MAX_SIZE是(256 * 1024)。如果不合法,返回错误。

cp = sshbuf_ptr(state->input);
if (state->packlen == 0) {
    if (sshbuf_len(state->input) < 4 + 1)
        return 0; /* packet is incomplete */
    state->packlen = PEEK_U32(cp);
    if (state->packlen < 4 + 1 ||
        state->packlen > PACKET_MAX_SIZE)
        return SSH_ERR_MESSAGE_INCOMPLETE;
}

随后,声明需要packlen+4的长度,如果太短则放弃;如果正常则开始接受数据。从cp+4处拷贝packlen的数据到incoming_packet里。这里涉及两个函数,一个是sshbuf_put,一个是sshbuf_reserve。reserve函数用于分配指定长度的数据(如果已经超过max length,则有一个奇特的pack操作,即将一半长度的buffer移动到前面去)。返回保留后的地址,修正buf的真实大小。然后返回到上一层sshbuf_put,它调用memcpy将长度为len的数据从v拷贝到p中。

int
sshbuf_reserve(struct sshbuf *buf, size_t len, u_char **dpp)
{
    u_char *dp;
    int r;

    if (dpp != NULL)
        *dpp = NULL;

    SSHBUF_DBG(("reserve buf = %p len = %zu", buf, len));
    if ((r = sshbuf_allocate(buf, len)) != 0)
        return r;

    dp = buf->d + buf->size;
    buf->size += len;
    if (dpp != NULL)
        *dpp = dp;
    return 0;
}

int
sshbuf_put(struct sshbuf *buf, const void *v, size_t len)
{
    u_char *p;
    int r;

    if ((r = sshbuf_reserve(buf, len, &p)) < 0)
        return r;
    if (len != 0)
        memcpy(p, v, len);
    return 0;
}


need = state->packlen + 4;
if (sshbuf_len(state->input) < need)
    return 0; /* packet is incomplete */
sshbuf_reset(state->incoming_packet);
if ((r = sshbuf_put(state->incoming_packet, cp + 4,
    state->packlen)) != 0 ||
    (r = sshbuf_consume(state->input, need)) != 0 ||
    (r = sshbuf_get_u8(state->incoming_packet, NULL)) != 0 ||
    (r = sshbuf_get_u8(state->incoming_packet, typep)) != 0)
    return r;

随后,调用sshbuf_consume,将刚刚读取完的need从里面去除。再调用sshbuf_get_u8连续获取两个字符,第二个作为type。获取的时候也有校验。读取完以后返回。注意这个typep是传进来的指针,这里会直接把类型给修改掉。

回到ssh_packet_read_poll2的后半段。其实差不多,只不过多了一个cipher_crypt的环节。

标签:none

添加新评论

captcha
请输入验证码