Пример сетевого приложения ::
(c) 0xDEADzED
Перед тем, как описывать нижеприведенную программу, несколько слов о серверах вообще. Назовем сеансом промежуток времени, в течение которого существует соединение между клиентом и сервером. В зависимости от длительности сеанса (времени существования соединения) к серверу предъявляются существенно разные требования. Если он короткий (по человеческим меркам, например, меньше 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 Первые 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(). Последнее обязательно, дабы не плодить зомби.
Страница сайта http://silicontaiga.ru
Оригинал находится по адресу http://silicontaiga.ru/home.asp?artId=4765 |