C: libssh – пример SSH-“клиента”

Автор: | 26/05/2017

Ниже рассматривается пример написания SSH-клиента на C с использованием libssh.

Сама библиотека libssh уже устарела, и вместо неё рекомендуется libssh2.

Сравнение libssh и libssh2 есть тут>>>.

Тем не менее у libssh отличные примеры (которые и используются в примерах ниже с небольшими отличаями) и документация, поэтому использую её.

RFC 4251 в SSH Protocol Architecture описывает три основных компонента:

  • SSH-TRANS: The Secure Shell (SSH) Transport Layer Protocol
    Предоставляет авторизацию сервера, секретность и целостность данных. так же может предосталвять компрессию. Как правило работает поверх установленного TCP/IP соединения.
  • SSH-USERAUTH: The Secure Shell (SSH) Authentication Protocol
    Предоставляет аутентификацию клиента на сервере. Работает поверх SSH Transport Layer Protocol.
  • SSH-CONNECTThe Secure Shell (SSH) Connection Protocol
    Разделяет зашифрованный туннель на несколько логических каналов. Предоставляет интерактивные сессии для авторизации, удалённого выполенния команд, форвардинга TCP/IP и X11 соединений. Работает поверх SSH Authentication Protocol.

Последовательность установления сессии хорошо описана тут>>> и тут>>>.

Кратко – стандартная SSH сессия устанавливается следующим образом:

  • Установка соединения с SSH-сервером. Выполняется handshake, в результате которого клиент получает от сервера публичный ключ. Клиент выполняет проверку ключа с помощью отпечатка или файла known_hosts.
  • Клиент выполняет авторизацию. Стандартный способ – пароль, так же может использоваться авторизация по ключам (RSA или DSA, сгенерированные с помощью openssl) или SSH-агент.
  • Как только клиент авторизован – можно открыть один или больше каналов (теоретически – любое кол-во). Каналы являются способом передачи данных через единое SSH-соединение. Каждый такой канал имеет свой stdout и stderr.
  • Открыв канал клиент может:
    • выполнить удалённую команду
    • открыть командную оболочку (shell)
    • использовать подсистемы sftp или scp для передачи данных
  • По завершении сеанса – закрывается канал, затем закрывается само соединение.

Создание и параметры сессии

Наиболее важным объектом в SSH соединении является SSH сессия.

Для создания сессии – используется функция ssh_new():

#include <stdio.h>
#include <libssh/libssh.h>
#include <stdlib.h>

int main() {

    ssh_session session;
    session = ssh_new();
    if (session == NULL) exit(-1);
    ssh_free(session);
}

libssh следует модели allocate-it-deallocate-it, т.е. каждый объект, созданный с помощью xxxxx_new() должен быть удалён с помощью xxxxx_free().

В данном случае – мы создаём объект с помощью ssh_new(), и очищаем его из памяти с помощью ssh_free().

Собираем, проверяем:

[simterm]

$ gcc -lssh ssh_client_libssh1.c -o ssh_client_libssh1
$ ./ssh_client_libssh1; echo $?
0

[/simterm]

Для задания параметров сесси используется функция ssh_options_set().

Наиболее важные опции:

  • SSH_OPTIONS_HOST: имя хоста, к котормоу выполняется подключение
  • SSH_OPTIONS_PORT: порт для подключения (по умолчанию 22)
  • SSH_OPTIONS_USER: пользователь
  • SSH_OPTIONS_LOG_VERBOSITY: уровень детализации сообщений об ошибках

Обязательным параметром является только SSH_OPTIONS_HOST.

Если вы не указываете SSH_OPTIONS_USER – будет использовано имя пользователя, который выполняет программу:

#include <stdio.h>
#include <libssh/libssh.h>
#include <stdlib.h>

int main() {

    int port = 22;
    int verbosity = SSH_LOG_FUNCTIONS;
    
    ssh_session session;
    session = ssh_new();

    if (session == NULL) exit(-1);

    ssh_options_set(session, SSH_OPTIONS_HOST, "localhost");
    ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
    ssh_options_set(session, SSH_OPTIONS_PORT, &port);
    ssh_options_set(session, SSH_OPTIONS_USER, "setevoy");

    ssh_free(session);
}

Проверяем:

[simterm]

$ ./ssh_client_libssh1; echo $?
0

[/simterm]

Подключение

Теперь, когда сессия и параметры готовы – можно выполнить подключение.

Для этого используем ssh_connect().

Перепишем код – вынесем имя хоста в первый аргумент, порт – во второй, и попробуем подключиться:

#include <stdio.h>
#include <libssh/libssh.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char **argv) {

    // check for args num
    if (argc < 3) exit(-1);

    // assign first arg to host var
    char host[20];
    strcpy(host, argv[1]);

    // assign second arg to port var
    int port = atoi(argv[2]);

// set verbosity if need
//    int verbosity = SSH_LOG_FUNCTIONS;
//    int verbosity = SSH_LOG_PROTOCOL;
    int connection;
    
    ssh_session session;
    session = ssh_new();

    if (session == NULL) exit(-1);

    ssh_options_set(session, SSH_OPTIONS_HOST, host);
//    ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
    ssh_options_set(session, SSH_OPTIONS_PORT, &port);
    ssh_options_set(session, SSH_OPTIONS_USER, "setevoy");

    printf("Connecting to host %s and port %d\n", host, port);
    connection = ssh_connect(session);

    if (connection != SSH_OK) {
        printf("Error connecting to %s: %s\n", host, ssh_get_error(session));
        exit -1;
    } else {
        printf("Connected.\n");
    }     

    ssh_free(session);
}

Собираем, проверяем:

[simterm]$ gcc -lssh ssh_client_libssh1.c -o ssh_client_libssh1

$ ./ssh_client_libssh1 hostname 22
Connecting to host hostname and port 22
Connected.

$ ./ssh_client_libssh1 hostname 23
Connecting to host hostname and port 23
Error connecting to hostname: Connection refused[/simterm]

Авторизация сервера

Как только вы выполнили подключение к серверу – необходимо убедиться в том, что это именно тот сервер, к которому вы хотели подключиться.

Для этого имеется два способа. Первый, рекомендованный – с помощью функции ssh_is_server_known(). Она проверит файл ~/.ssh/known_hosts в поисках имени хоста.

Другой способ – функция ssh_get_pubkey_hash(), для отображения отпечатка публичного ключа сервера (на случай, если вы его помните на память).

В случае, если подключение выполняется первый раз – вы можете спросить пользователя доверяет ли он этому хосту. Если да – то используем функцию ssh_write_knownhost(), что бы добавить отпечаток ключа сервера в known_hosts.

Добавим в наш клиент новую функцию:

...
int verify_knownhost(ssh_session session) {
  int state, hlen;
  unsigned char *hash = NULL;
  char *hexa;
  char buf[10];
  state = ssh_is_server_known(session);
  hlen = ssh_get_pubkey_hash(session, &hash);
  if (hlen < 0)
    return -1;
  switch (state) {
    case SSH_SERVER_KNOWN_OK:
      break; /* ok */
    case SSH_SERVER_KNOWN_CHANGED:
      fprintf(stderr, "Host key for server changed: it is now:\n");
      ssh_print_hexa("Public key hash", hash, hlen);
      fprintf(stderr, "For security reasons, connection will be stopped\n");
      free(hash);
      return -1;
    case SSH_SERVER_FOUND_OTHER:
      fprintf(stderr, "The host key for this server was not found but an other"
        "type of key exists.\n");
      fprintf(stderr, "An attacker might change the default server key to"
        "confuse your client into thinking the key does not exist\n");
      free(hash);
      return -1;
    case SSH_SERVER_FILE_NOT_FOUND:
      fprintf(stderr, "Could not find known host file.\n");
      fprintf(stderr, "If you accept the host key here, the file will be"
       "automatically created.\n");
      /* fallback to SSH_SERVER_NOT_KNOWN behavior */
    case SSH_SERVER_NOT_KNOWN:
      hexa = ssh_get_hexa(hash, hlen);
      fprintf(stderr,"The server is unknown. Do you trust the host key? [yes/no]\n");
      fprintf(stderr, "Public key hash: %s\n", hexa);
      free(hexa);
      if (fgets(buf, sizeof(buf), stdin) == NULL)
      {
        free(hash);
        return -1;
      }
      if (strncasecmp(buf, "yes", 3) != 0)
      {
        free(hash);
        return -1;
      }
      if (ssh_write_knownhost(session) < 0)
      {
        fprintf(stderr, "Error %s\n", strerror(errno));
        free(hash);
        return -1;
      }
      break;
    case SSH_SERVER_ERROR:
      fprintf(stderr, "Error %s", ssh_get_error(session));
      free(hash);
      return -1;
  }
  free(hash);
  return 0;
}
...

И её вызов после подключения:

...
    if (verify_knownhost(session) < 0) {
        ssh_disconnect(session);
        ssh_free(session);
        exit(-1);
    }
...

Полностью наш клиент теперь выглядит так:

#include <stdio.h>
#include <libssh/libssh.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

int verify_knownhost(ssh_session session) {

  int state, hlen;
  unsigned char *hash = NULL;
  char *hexa;
  char buf[10];

  state = ssh_is_server_known(session);
  hlen = ssh_get_pubkey_hash(session, &hash);

  if (hlen < 0)
    return -1;
  switch (state)

  {
    case SSH_SERVER_KNOWN_OK:
      break; /* ok */
    case SSH_SERVER_KNOWN_CHANGED:
      fprintf(stderr, "Host key for server changed: it is now:\n");
      ssh_print_hexa("Public key hash", hash, hlen);
      fprintf(stderr, "For security reasons, connection will be stopped\n");
      free(hash);
      return -1;
    case SSH_SERVER_FOUND_OTHER:
      fprintf(stderr, "The host key for this server was not found but an other"
        "type of key exists.\n");
      fprintf(stderr, "An attacker might change the default server key to"
        "confuse your client into thinking the key does not exist\n");
      free(hash);
      return -1;
    case SSH_SERVER_FILE_NOT_FOUND:
      fprintf(stderr, "Could not find known host file.\n");
      fprintf(stderr, "If you accept the host key here, the file will be"
       "automatically created.\n");
      /* fallback to SSH_SERVER_NOT_KNOWN behavior */
    case SSH_SERVER_NOT_KNOWN:
      hexa = ssh_get_hexa(hash, hlen);
      fprintf(stderr,"The server is unknown. Do you trust the host key [yes/no]?\n");
      fprintf(stderr, "Public key hash: %s\n", hexa);
      free(hexa);
      if (fgets(buf, sizeof(buf), stdin) == NULL)
      {
        free(hash);
        return -1;
      }
      if (strncasecmp(buf, "yes", 3) != 0)
      {
        free(hash);
        return -1;
      }
      if (ssh_write_knownhost(session) < 0)
      {
        fprintf(stderr, "Error %s\n", strerror(errno));
        free(hash);
        return -1;
      }
      break;
    case SSH_SERVER_ERROR:
      fprintf(stderr, "Error %s", ssh_get_error(session));
      free(hash);
      return -1;
  }

  free(hash);
  return 0;
}

int main(int argc, char **argv) {

    // check for args num
    if (argc < 3) exit(-1);

    // assign first arg to host var
    char host[20];
    strcpy(host, argv[1]);

    // assign second arg to port var
    int port = atoi(argv[2]);

// set verbosity if need
//    int verbosity = SSH_LOG_FUNCTIONS;
//    int verbosity = SSH_LOG_PROTOCOL;
    int connection;

    ssh_session session;
    session = ssh_new();

    if (session == NULL) exit(-1);

    ssh_options_set(session, SSH_OPTIONS_HOST, host);
//    ssh_options_set(session, SSH_OPTIONS_LOG_VERBOSITY, &verbosity);
    ssh_options_set(session, SSH_OPTIONS_PORT, &port);
    ssh_options_set(session, SSH_OPTIONS_USER, "setevoy");

    printf("Connecting to host %s and port %d\n", host, port);
    connection = ssh_connect(session);

    if (connection != SSH_OK) {
        printf("Error connecting to %s: %s\n", host, ssh_get_error(session));
        exit -1;
    } else {
        printf("Connected.\n");
    }

    if (verify_knownhost(session) < 0) {
        ssh_disconnect(session);
        ssh_free(session);
        exit(-1);
    }

    ssh_disconnect(session);
    ssh_free(session);
}

Собираем, проверяем:

[simterm]

$ gcc -lssh ssh_client_libssh1.c -o ssh_client_libssh1

$ ./ssh_client_libssh1 localhost 22
Connecting to host localhost and port 22
Connected.
The server is unknown. Do you trust the host key [yes/no]?
Public key hash: d9:a4:5a:26:6d:2c:79:14:d5:6b:fc:c5:f1:8b:21:44
yes

[/simterm]

Проверяем файл know_hosts:

[simterm]

$ cat ~/.ssh/known_hosts 
localhost ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGHw/Xg9XYq2HiVXrtzgVsO9vOkJoROl4hdLD9SmSdc3

[/simterm]

Авторизация пользователя

Последним шагом – нам надо авторизовать пользователя на удалённой системе.

libssh поддерживает несколько методов авторизации, наиболее используемые – password и public key:

  • password: клиент отправляет пароль серверу, сервер принимает или овтергает его
  • public key method: на сервере хранится публичная часть ключа пользователя, и пользователь при авторизации указывает его приватную часть.

Ниже используем обычную авторизацию по паролю с помощью ssh_userauth_password().

Больше по авторизации – тут>>>.

Обновляем код, после вызова проверки сервера – вызываем авторизацию пользоватля:

...
    int rc;
    char *password;
    password = getpass("Password: ");
    rc = ssh_userauth_password(session, NULL, password);

    if (rc != SSH_AUTH_SUCCESS) {
        fprintf(stderr, "Error authenticating with password: %s\n",
            ssh_get_error(session));
        ssh_disconnect(session);
        ssh_free(session);
        exit(-1);
    }
...

Проверяем:

[simterm]

$ ./ssh_client_libssh1 localhost 22
Connecting to host localhost and port 22
Connected.
Password:

[/simterm]

И вводим неверный пароль:

[simterm]

$ ./ssh_client_libssh1 localhost 22
Connecting to host localhost and port 22
Connected.
Password: 
Error authenticating with password: Access denied. Authentication that can continue: publickey,password

[/simterm]

Выполнение команд

Теперь, когда сервер и пользователь авторизованы – можно выполнить команду на удалённом хосте.

Самый простой способ выполнить удалённую команду – ssh_channel_request_exec().

Добавляем новую функцию:

...
int show_remote_processes(ssh_session session) {
  ssh_channel channel;
  int rc;
  char buffer[256];
  int nbytes;
  channel = ssh_channel_new(session);
  if (channel == NULL)
    return SSH_ERROR;
  rc = ssh_channel_open_session(channel);
  if (rc != SSH_OK)
  {
    ssh_channel_free(channel);
    return rc;
  }
  rc = ssh_channel_request_exec(channel, "ps aux | grep ssh");
  if (rc != SSH_OK) {
    ssh_channel_close(channel);
    ssh_channel_free(channel);
    return rc;
  }
  nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
  while (nbytes > 0) {
    if (write(1, buffer, nbytes) != (unsigned int) nbytes)
    {
      ssh_channel_close(channel);
      ssh_channel_free(channel);
      return SSH_ERROR;
    }
    nbytes = ssh_channel_read(channel, buffer, sizeof(buffer), 0);
  }
    
  if (nbytes < 0)
  {
    ssh_channel_close(channel);
    ssh_channel_free(channel);
    return SSH_ERROR;
  }
  ssh_channel_send_eof(channel);
  ssh_channel_close(channel);
  ssh_channel_free(channel);
  return SSH_OK;
}
...

И её вызов в main():

...
    if (show_remote_processes(session) != SSH_OK) {
        printf("Error executing request\n");
        ssh_get_error(session);
        ssh_disconnect(session);
        ssh_free(session);
        exit(-1);
    } else {
        printf("\nRequest completed successfully!\n");
    }
...

Проверяем:

[simterm]

$ ./ssh_client_libssh1 localhost 22
Connecting to host localhost and port 22
Connected.
Password: 
setevoy   3223  0.0  0.0  44116  3928 pts/1    S+   May15   1:11 ssh -D 3033 [email protected] -p 2222
setevoy  30089  1.0  0.0  26856  2840 pts/3    SL+  13:04   0:00 ./ssh_client_libssh1 localhost 22
root     30090  0.5  0.0 102476  7524 ?        Ss   13:04   0:00 sshd: setevoy [priv]
setevoy  30129  0.0  0.0 102476  3908 ?        S    13:04   0:00 sshd: setevoy@notty
setevoy  30130  0.0  0.0  11960  2452 ?        Ss   13:04   0:00 bash -c ps aux | grep ssh
setevoy  30132  0.0  0.0   9024   860 ?        S    13:04   0:00 grep ssh
setevoy  30962  0.1  0.0  35644  8204 pts/6    S+   12:35   0:02 vim ssh_client_libssh1.c

Request completed successfully!

[/simterm]

Проверим на RTFM:

[simterm]

$ ./ssh_client_libssh1 rtfm.co.ua 22
Connecting to host rtfm.co.ua and port 22
Connected.
The server is unknown. Do you trust the host key [yes/no]?
Public key hash: 10:ab:0b:0e:38:06:b0:71:53:c7:37:9a:d0:20:23:6e
yes
Password: 
root       659  0.0  0.1  55184  1216 ?        Ss    2016   0:00 /usr/sbin/sshd -D
setevoy   6685  0.0  0.2  44428  2428 pts/4    S+   Jan26   0:00 ssh [email protected] -i /home/setevoy/.ssh/rtfm_prod.pem
root     25832  0.0  0.5  80652  5776 ?        Ss   10:15   0:00 sshd: setevoy [priv]
setevoy  25834  0.0  0.3  80652  4024 ?        S    10:15   0:00 sshd: setevoy@notty 
setevoy  25835  0.0  0.2  13228  2780 ?        Ss   10:15   0:00 bash -c ps aux | grep ssh
setevoy  25837  0.0  0.2  12732  2192 ?        S    10:15   0:00 grep ssh

Request completed successfully!

[/simterm]

Готово.

Полностью “клиент” можно посмотреть тут>>>.

The Secure Shell (SSH) Protocol Architecture

Red Hat Enterprise Linux 3: Reference Guide Chapter 19. SSH Protocol

Creating an SSH Session

libssh documentation

libssh2 documentation

ssh client example