Perl для системного администрирования

         

Анализ журналов


Некоторые системные администраторы никогда не заходят дальше фазы ротации в своих взаимоотношениях с журналами. До тех пор пока на диске существует информация, необходимая для отладки, они никогда не подумают об использовании информации из журналов в других целях. Хотелось бы намекнуть, что это недальновидный взгляд и что даже краткий анализ журналов может иметь большое значение. Мы рассмотрим несколько подходов, которые можно использовать для анализа журналов из Perl, начиная от самого простого и постепенно продвигаясь к более сложному.

Большая часть примеров из этого раздела использует журналы из Unix, т. к. в средней Unix-системе журналов больше, чем в двух других операционных системах вместе взятых, а применяемые подходы не зависят от операционной системы.

Чтение-подсчет потока

Самый простой подход - обычное «считывание и подсчет». Мы читаем поток данных из журнала, ищем интересующие нас данные и увеличиваем значение счетчика, когда их находим. Вот простой пример, подсчитывающий, сколько раз перегружалась машина, в котором использован файл wtmpx из Solaris 2.6:*

# шаблон для wtmpx из Solaris 2.6, подробности смотрите в

# документации no pack()

Stemplate = "А32 А4 А32 1 s s2 x2 12 1 x20 S A257 x";

# определяем размер записи $recordsize = length(pack($template,())); и открываем файл

open(WTMP,"/var/adm/wtmpx") or die "Невозможно открыть wtmpx:$!\n";

# считываем по одной записи

while (read(WTMP,Srecord,Srecordsize)) {

($ut_user,$ut_id,$ut_line,$ut_pid,$ut_type,$ut_e_termination,

$ut_e_exit,$tv_sec,$tv_usec,$ut_session,

$ut_syslen,$ut_host)= unpack($template,Srecord);

if ($ut_line eq "system boot"){

print "rebooted ".scalar localtime($tv_sec)."\n"; $reboots++;

}

}

clOse(WTMP);

print "Общее число перезагрузок: $reboots\n";

Расширим этот подход и рассмотрим пример сбора статистики при помощи Event Log из Windows NT. Как говорилось раньше, механизм ведения журналов в NT хорошо разработан и довольно сложен. Эта сложность несколько пугает начинающих программистов на Perl. Для получения основной информации из журналов мы будем




использовать некоторые подпрограммы из модулей для Win32.

Программы в NT и компоненты операционной системы записывают свои действия, регистрируя «события» в одном из журналов событий. Регистрация событий операционной системой сопровождается записью основной информации, например, времени наступления события, имени программы или функции операционной системы, зарегистрировавших событие, типа наступившего события (просто информативное или что-то более серьезное) и т. д.

В отличие от Unix, само описание события, т. е. сообщение, не хранится вместе с записью о событии. Вместо этого в журнал помещается идентификатор EventlD. Этот идентификатор содержит ссылку на определенное сообщение, хранящееся в библиотеке (.dll). Получить сообщение по идентификатору не просто. Этот процесс требует поиска нужной библиотеки в реестре и ее загрузки вручную. К счастью, этот процесс в текущей версии модуля Win32: : EventLog выполняется автоматически (ищите $Win32:: EventLog: :GetMessageText в первом примере с использованием Win32:: Eventlog).

В следующем примере мы сгенерируем простую статистику по числу записей в журнале System, содержащую сведения о том, откуда они поступили, и об уровне их важности. Мы напишем эту программу несколько иначе, чем первый пример в этой главе.

Первый наш шаг - загрузить модуль Win32: : EventLog, обеспечивающий связь между Perl и программами для работы с журналами событий в Win32. Затем мы инициализируем хэш-таблицу, которая будет использоваться для хранения результатов вызовов программ чтения журналов. Обычно Perl заботится об этом за нас, но иногда стоит добавить подобный код ради тех, кто будет впоследствии читать программу. Наконец, мы определяем небольшой список типов событий, который позже будет использоваться для печати статистики:

use Win32::EventLog;

my %event=('Length', NULL,

'RecordNumber',NULL, TimeGenerateo",

NULL. TimeWritten',NULL, 'EventID',NULL, 'EventType',NULL, 'Category',NULL, 'ClosingRecordNumber' .

NULL, 'Source',NULL, 'Computer',NULL, 'Strings',NULL, 'Data',NULL,):



tt

частичный список типов событий, то есть тип 1 -- "Error"

К 2 -- "Warning" и т. д.

@types = ("","Error","Warning","","Information");

Наш следующий шаг - открытие журнала событий System. Open() помещает дескриптор EventLog в $EventLog, который можно использовать для соединения с этим журналом:

Win32::EventLog::Open($EventLog,'System'.'') or die "Невозможно открыть журнал System:$~E\n";

Получив этот дескриптор, мы можем использовать его для подсчета событий в журнале и получения номера самой старой записи:

$EventLog->Win32::EventLog::GetNumber($numevents);

$EventLog->Win32::EventLog::Get01dest($oldestevent);

Эта информация указывается в первом операторе Read(), позиционирующем нас прямо перед первой записью. Это эквивалентно переходу в начало файла при помощи функции seek():

$EventLog->Win32::EventLog::Read((EVENTLOG_SEEK_READ |

EVENTLOG^FORWARDS_READ). $numevents + $oldestevent, $event);

Теперь в простом цикле прочитываем все записи. Флаг EVENTLOG_SEQ-UENTIAL_READ говорит: «Продолжайте читать с позиции после последней прочитанной записи». Флаг EVENTIOG_FORWARDS_READ перемещает нас вперед в хронологическом порядке. Третий аргумент Read() - смещение, в данном случае равное 0, потому что мы продолжаем с той же позиции, на которой остановились. Считывая каждую запись, мы записываем в хэш-таблицу счетчиков ее источник (Source) и тип события (EventType).

 обходим в цикле все события, записывая количество различных

источников (Source) и типов событий

(EventTypes) for ($i=0;$i<$numevents;$i++)

{

$EventLog->Read((EVENTLOG_SEQUENTIAL READ

 EVENTLOG_FORWARDS_READ) 0, Sevent):

$source{$event->{Source}}++:

$types{$event->{EventType}}++: }

it

выводим полученные результаты print "-->

Event Log Source Totals:\n"; for (sort keys %source) {

print "$_: $source{$_}\n": }

print "-"x30,"\n";

print "-->Event Log Type Totals:\n"; for (sort keys %types) {



print "$types[$_J: $types{$_}\n"; }

print "-"x30,"\n";

print "Total number of events: $numevents\n";

Мои результаты выглядят так: -->

Event Log Source Totals: Application Popup:

4 BROWSER: 228 DCOM: 12 Dhcp: 12 EventLog:

351 Mouclass: 6 NWCWorkstation: 2 Print: 27 Rdr: 12

RemoteAccess: 108 SNMP: 350 Serial: 175

Service Control Manager: 248 Sparrow: 5 Srv:

201 msbusmou: 162 msi8042: 3 msinport:

162 mssermou: 151 qic117: 2

--> Event Log Type Totals: Error: 493 Warning:

714 Information: 1014

Total number of events: 2220

Как я и обещал, вот пример кода, полагающегося на подобную программу для вывода содержимого журнала событий. В нем используется программа ElDump Джеспера Лоритсена (Jesper Lauritsen), которую можно загрузить с http://www.ibt.ku.dk/jesper/JespersNTtools.htm. ElDump похожа на DumpEl из NT Resource Kit:

Seldump = 'c:\bin\eldump'; # путь к ElDump

И выводим поля данных. оаз,:згяя их т/льдой ("). .

К текста сообщения (быстрее)

open(ELDUMP."Seldump $dumpflagsj") or die "Невозможно загустить $eidump:$!\n";

print STDERR "Считываем системный журнал.1:

while(<ELDUMP>){

($date, $time, $source, $type. Scategory Sevent, $i.ser, Scom.puter) =

split('-');

$$type{$source}+-t;

print STDERR "."; } print STDERR "done.\n";

close(ELDUMP);

# для каждого типа события выводим источники и количество

 событий

foreach $type (qw(Error Warning Information

AuditSuccess AuditFailure)){

print "-" x 65,"\n";

print uc($type)."s by source:\n";

for (sort keys %$type){

print "$_ ($$type{$_})\n";

} } print "-" x 65,"\n";

Вот выдержка из получаемых данных:

ERRORS by source:

BROWSER (8)

Cdrom (2)

DOOM (15)

Dhcp (2524)

Disk (1)

EventLog (5)

RemoteAccess (30)

Serial (24)

Service Control Manage1" ("00)

Sparrow (2)

atapi (2)

i8042prt (4i

WARNINGS by soiree:

BROWSER (80)

Cdrom (22)



Dhcp (76)

Print (8)

Srv (82)



Вариация на тему предыдущего примера



Простая вариация предыдущего подхода включает в себя многократный обход данных. Иногда это необходимо в случае с данными большого объема и ситуаций, когда сначала приходится просмотреть все данные, чтобы отличить интересные данные от неинтересных. В плане реализации это означает, что после первого обхода данных надо:

  • Перейти обратно к началу потока данных (который может быть файлом) при помощи seek( )или API-вызова.


  • или

  • Закрыть и вновь открыть дескриптор файла. Зачастую это единственный выбор, когда читаются данные из вывода программы, подобной last.


  • Вот пример, когда такой подход может пригодиться. Представьте, что надо справиться с проблемой в защите, связанной с тем, что кто-то получил несанкционированный доступ к одной из учетных записей. Один из первых вопросов, который приходит на ум, - «был ли получен доступ и к другим учетным записям с той же машины?». Найти полный ответ на такой кажущийся простым вопрос может оказаться сложнее, чем кажется. Давайте попробуем решить эту проблему. Приведенный ниже отрывок кода для SunOS принимает в качестве первого аргумента имя пользователя и необязательное регулярное выражение в качестве второго, чтобы отфильтровать узлы, которые мы хотим проигнорировать:

    Stemplate = "А8 А8 А16 1"; # для SunOS 4.1.x Srecordsize = length(pack($template,())); ($user,$ignore) = @ARGV;

    print "-- ищем узлы, с которых регистрировался пользователь Suser --\n"; open(WTMP,"/var/adm/wtrcp") or die "Невозможно открыть wtmp:$!\n": while (read(WTMP,Srecord.Srecordsize)) {

    ($tty, $name,$host:$time)=unpack($template.$reccKd):

    if ($user eq $name){

    next if (defined Signore and $host =" /$ignore/o); if (length($host) > 2 and 'exists $contacts{$nost}) {

    $connect = localti.Tie($time):

    $contacts{$ROSt}=$time:

    write: >

    print "-- ищем другие соединения с этих узлов --\п"; die "Невозможно перейти в начало wtmp:$'\n" unless (seek(WTMP,0,0));



    while (read(WTMP,$record.$recordsize)) {

    ($tty,Sname,$hostt $time)=unpack($template,$record);

    ft если это запись не о завершении работы с системой и нас

    # интересует этот узел и это соединение установлено для

    ft «другой» учетной записи, тогда записываем эти данные

     if (substr($name,1,1) ne "\0" and exists $contacts{$host} and $name ne $user){

    Sconnect = localtime($time); write;

    }

    } close(WTMP);

    ft вот формат вывода, вероятно, его потребуется скорректировать

    и в зависимости от шаблона

    format STDOUT =

    @«««« @««««««« @«««««««««

    $name,$host,Sconnect

    Сначала программа просматривает файл wtmp в поисках записей о регистрации в системе пользователей под «скомпрометированным» именем. По мере нахождения таких записей пополняется хэш, в который записываются имена всех узлов, где регистрировался пользователь под этим именем. Затем программа возвращается к началу файла и просматривает его заново, выполняя на этот раз поиск записей о соединениях с узлов из списка, и выводит совпадения по мере их появления. Не составит труда изменить эту программу так, чтобы она просматривала все файлы из каталога, в котором хранятся файлы ротации журнала wtmp.

    Единственная проблема этой программы - ее «узкая специализация». Она будет искать только точное совпадение имен узлов. Если злоумышленник регистрировался в системе, используя динамический адрес, получаемый от провайдера (что бывает часто), то очень велика вероятность, что имена узлов будут отличаться при каждом соединении. Тем не менее, даже неполные решения, подобные этому, очень сильно помогают.

    Помимо простоты, у рассмотренного нами подхода есть еще преимущества: он быстрее и требует меньших затрат памяти по сравнению с другими методами. Он лучше всего справляется с журналами, в которых регистрируются данные без поддержки состояния, о которых мы говорили раньше в этой главе. Но иногда, особенно при работе с данными с состоянием, необходимо использовать другие методы.



    Процесс «прочитал-запомнил»



    Крайность, противоположная предыдущему подходу (там мы «пробегали» по данным как можно быстрее), заключается в чтении их в память и последующей обработке после чтения. Рассмотрим несколько версий этой стратегии.



    Для начала приведем простой пример: скажем, у нас есть журнал FTP-сервера и требуется узнать, какие файлы скачивались чаще других. Вот несколько строк из журнала FTP-сервера wu-ftpd:

    Sun Dec 27 05:18:57 1998 1 nic.funet.fi 11868 /net/ftp.funet.fi/CPAN/

    MIRRORING.FROM a _ о a cpan@perl.org ftp 0 *

    Sun Dec 27 05:52:28 1998 25 kju.hc.congress.ccc.de 269273 /CPAN/doc/FAQs/FAQ/

    PerlFAQ.html a _ о a mozilla@ ftp 0 *

    Sun Dec 27 06:15:04 1998 1 rising-sun.media.mit.edu 11868 /CPAN/

    MIRRORING. FROM b __ о a root@rising-sun. media, mit. edu ftp 0 *

    Sun Dec 27 06:15:05 1998 1 rising-sun.media.mit.edu 35993 /CPAN/RECENT.html b

    о а root@rising-sun.media.mit.edu ftp 0

    А вот список полей, из которых состоят приведенные выше строки (все подробности о каждом поле ищите в страницах руководств xferlog(B) сервера wu-ftpd).




    Номер поля



    Имя поля

    0

    current-time (текущее время)

    1

    transfer-time (время передачи, в секундах)

    2

    remote-host (удаленный узел)

    3

    filesize (размер файла)

    4

    filename (имя файла)

    5

    transfer-type (тип передачи)

    6

    special-action-flag (специальный флаг)

    7

    direction (направление)

    8

    access-mode (режим доступа)

    9

    use r name (имя пользователя)

    10

    service-name (имя службы)

    11

    authentication-method (меод аутентификации)

    12

    authenticated-user-id (идентификатор аутентифицированного пользователя)
    Вот пример программы, сообщающей о том, какие файлы передавались чаще других:

    Sxferlog = "/var/adin/Iog/xferlog";

    open(XFERLOG,Sxferlog) or die "Невозможно открыть Sxferlog•$!\n":

    while (<XFERLOG>){

    $files{(sDlit)[8]}++ }

    close(XFERLOG);

    for (sort {$files{$b} <=> $files{$a}||$a cmp $b} keys %files){

    print "$_:$files{$_}\n"; >

    Мы считываем каждую строку файла, используя имя файла в качестве ключа кэша, и увеличиваем значение для этого ключа. Имя файла выделяется из каждой строки журнала при помощи индекса массива, ссылающегося на определенный элемент списка, возвращенного функцией split():



    $files{(split)[8]>++;

    Вы могли заметить, что элемент, на который мы ссылаемся (8), отличается от 8-го поля из списка полей xferlog, приведенного выше. Это печальные последствия того, что в оригинальном файле отсутствуют разделители полей. Мы разбиваем строки из журнала по пробелам (что является значением по умолчанию для split()), так что дата разбивается на пять отдельных элементов списка.

    В этом примере применяется искусный прием - сортировку значений выполняет анонимная функция sort:

    for (sort {$files{$b} <=> $files{$a}||$a cmp $b} keys %files){

    Обратите внимание, что переменные $а и $b в первой части расположены не в алфавитном порядке. Это приводит к тому, что sort выводит элементы в обратном порядке, т. е. первыми отображаются самые «популярные» файлы. Вторая часть анонимной функции sort ( I $a cmp $о) гарантирует, что файлы с одинаковой популярностью будут перечислены в отсортированном порядке.

    Для того чтобы этот сценарий подсчитывал только некоторые файлы и каталоги, можно задать регулярное выражение в качестве первого аргумента для сценария. Например, если добавить

    next unless /$ARGV[0]/o;

    в цикл while(), можно будет задавать регулярные выражения для ограничения учитываемых файлов. Давайте посмотрим на другой пример подхода «прочитал-запомнил», в котором используется программа «поиска брешей» из предыдущего раздела. В предыдущем примере выводилась информация только об успешной регистрации с сайтов злоумышленника. Узнать о неудавшихся попытках мы не можем. Чтобы получить такую информацию, мы рассмотрим другой файл журнала.



    Регулярные выражения



    Регулярные выражения - одна из самых важных составляющих анализа журналов. Регулярные выражения используются как сито для отсева интересных данных от данных, не представляющих интереса. В этой главе применяются только основные регулярные выражения, но вы вполне можете создавать более сложные для собственных нужд. Так, применение подпрограмм или технологии создания регулярных выражений из предыдущей главы позволяет использовать их еще более эффективно.



    Время, потраченное на получение навыков работы с регулярными выражениями, окупится с лихвой и не раз. Регулярные выражения лучше всего изучать по книге Джеффри Фридла (Jeffrey Friedl) «Mastering Regular Expressions» (Волшебство регулярных выражений) (O'Reilly).

    Эта проблема иллюстрирует один из недостатков Unix: информация из журналов в Unix-системах хранится в различных местах и в различных форматах. Для того чтобы справиться с этими различиями, существует не так много инструментов (к счастью, у нас есть Perl). Нередко приходится использовать более одного источника данных для решения подобных задач.

    Журнал, который сейчас больше всего нам пригодится, - это журнал, сгенерированный через syslog инструментом tcpwrappers, который предоставляет программы и библиотеки, позволяющие контролировать доступ к сетевым службам. Любую сетевую службу, например tel net, можно настроить так, чтобы все сетевые соединения для нее обрабатывались сначала программой tcpwrappers. После того как соединение будет установлено, программа tcpwrappers регистрирует попытку соединения через syslog и затем либо передает соединение настоящей службе, либо предпринимает некоторые действия (например, разрывает соединение). Решение, разрешить ли данное соединение, основывается на нескольких правилах, введенных пользователем (например, разрешать лишь для некоторых исходящих узлов), tcpwrappers также может принять меры предосторожности и послать запрос к DNS-серве-ру, чтобы убедиться, что соединение устанавливается оттуда, откуда

    ожидается. Кроме того, программу можно настроить так, чтобы в журнале регистрировалось имя пользователя, устанавливающего соединение (через протокол идентификации, описанный в RFC931), если это возможно. Более подробное описание tcpwrappers можно найти в книге Симеона Гарфинкеля (Simson Garfinkel) и Джина Спаффорда (Gene Spafford) «Practical Unix & Internet Security» (Unix в практическом использовании и межсетевая безопасность) (O'Reilly).

    Мы же просто добавим несколько строк к предыдущей программе, в которых просматривается журнал tcpwrappers (в данном случае tcpdlog) для поиска соединений с подозрительных узлов, найденных нами в wtmp. Если добавить этот код в конец предыдущего примера,





    местоположение журнала tcpd

    Stcpdlog = "/var/log/tcpd/tcpdlog";

    Shostlen = 16; Я максимальная длина имени узла в файле wtmp

    print "-- просматриваем tcpdlog --\n";

    open(TCPDLOG,Stcpdlog) or die "Невозможно прочитать $tcpdlog:$!\n";

    while(<TCPDLOG>){

    next if !/connect from /; tt нас беспокоят только соединения (Sconnecto,Sconnectfrom) = /(.+):\s+connect from\s+(.+)/; Sconnectfrom =" s/~.+@//;

    tt

    tcpwrappers может регистрировать имя узла целиком, а не

    # только первые N символов, как некоторые журналы wtmp. В

    результате необходимо усечь имя узла до той же длины, что

    Вив wtmp файле, если мы собираемся искать имя узла в кэше

    Sconnectfrom = substr($connectfrom.O,Shostlen);

    print if (exists $contacts{$connectfrom} and Sconnectfrom !~ /$ignore/o);

    то мы получим данные, подобные этим:

    -- ищем узлы, с которых регистрировался пользователь --

    user host.ccs.neu Fri Apr 3 13:41;47

    -- ищем другие соединения с этих узлов --

    user2 host.ccs.neu Thu Oct 9 17:06:49

    user2 host.ccs.neu Thu Oct 9 17:44:31

    user2 host.ccs.neu Fri Oct 10 22:00:41

    user2 host,ccs.neu Wed Oct 15 07:32:50

    user2 host.ccs.neu Wed Oct 22 16:24:12

    -- просматриваем tcpdlog --

    Jan 12 13:16:29 riost2 in. rshd[866]: connect from user4@host. ccs. neu. ed^

    Jan 13 14:38:54 hosts in, rlogind[4761]: connect from user5@host. ccs, nei., f-з ,

    Jan 15 14:30:17 host4 in.ftpd[18799]: connect from user6@host.ccs.reu.ecu

    Jan 16 19:48:19 hosts in.ftpd[5131]: connect from user7@host.ccs.neu.ca.,

    Читатели могли обратить внимание, что в приведенных выше результатах были замечены соединения, устанавливаемые в различное время. В файле wtmp были зарегистрированы соединения, установленные в период с 3 апреля по 22 октября, тогда как tcpwrappers показывает только январские соединения. Разница в датах говорит о том, что файлы wtmp и файлы tcpwrappers имеют различную скорость ротации. Необходимо учитывать такие детали, если вы пишете программу, которая полагается на то, что файлы журналов относятся к одному и тому же периоду времени.



    В качестве последнего и более сложного примера, демонстрирующего подход «прочитал-запомнил», рассмотрим задачу, требующую объединения данных с состоянием и без него. Для того чтобы получить более полную картину действий на сервере wu-ftpd, можно установить соответствие между информацией о регистрации в системе из файла wtmp и информацией о передаче файлов, записанной в файле xferlog сервера wu-ftpd. Было бы здорово увидеть, когда начался сеанс работы с FTP-сервером, когда он закончился и какие файлы передавались в течение этого сеанса.

    Вот отрывок вывода программы, которую мы собираемся написать. В нем показаны четыре FTP-сеанса за март. В первом сеансе на машину был передан один файл. В двух других файлы были переданы с этой машины, а в течение последнего сеанса файлов не было передано вообще:

    Thu Mar 12 18:14:30 1998-Thu Mar 12 18:14:38 1998 pitpc.ccs.neu.ed -> /home/dnb/makemod

    Sat Mar 14 23:28:08 1998-Sat Mar 14 23:28:56 1998 traal-22.ccs.neu <- /home/dnb/.emacs19

    Sat Mar 14 23:14:05 1998-Sat Mar 14 23:34:28 1998 traal-22.ccs.neu <- /home/dnb/lib/emacs19/

    cperl-mode.el <- /home/dnb/lib/emacs19/filladapt.el

    Wed Mar 25 21:21:15 1998-Wed Mar 25 21:36:15 1998 traal-22.ccs.neu (no transfers in xferlog)

    Получить такие данные не очень просто, поскольку приходится добавить данные без информации о состоянии в журнал данных с информацией о состоянии. В журнале xferlog приводится только время, когда была совершена передача файла, и узел, участвующий в этой предаче. В журнале wtmp приводится информация о соединениях и завершении соединений других узлов с сервером. Давайте посмотрим, как объединить эти два типа данных при помощи подхода «прочитал-запомнил». В этой программе мы определим некоторые переменные, а затем вызовем подпрограммы для выполнения каждой задачи :

     для преобразования дата ->время-в-1)п1х (количество секунд с начала эпохи) use Time::Local

    Sxferlog = "/var/log/xferlog":

     местоположение журнала передачи файлов

    $wtmp = "/var/adm/wtmp"; g местоположение wtmp $template = "A8 A8 A16 1";



    шаблон для wtmp в SunOS 4.1.4 Srecordsize = length(pack($template,()));

    размер каждой записи в wtmp Shostlen = 16;

     максимальная длина имени узла в wtmp

    карта соответствий имени месяца с номером

    %month = qw{Jan 0 Feb 1 Mar 2 Apr 3 May 4 Jun 5 Jul 6 Aug 7 Sep 8 Oct 9 Nov 10 Dec 11};

    &ScanXferlog;

     просматриваем журнал передачи файлов

    &ScanWtmp;

     просматриваем журнал wtmp

    &ShowTransfers;

     приводим в соответствие и выводим информацию о передачах

    Теперь рассмотрим процедуру, читающую журнал xferlog:

     просматриваем журнал передачи файлов сервера wu-ftpd и

    заполняем структуру данных %transfers

    sub ScanXferlog {

    local($sec, $min,$hours,$mday,$mon,$year); my($time,$rhost,$fname,Sdirection);

    print STDERR "Просматриваю Sxferlog...";

    open(XFERLOG,$xferlog) or

    die "Невозможно открыть $xferlog:$!\n";

    while (<XFERLOG>){

    Я используем срез массива для выбора нужных полей

    ($mon,$mday,$time,Syear,$rhost,$fname,Sdirection) = (split)[1,2,3,4,6,8,11];

    Я добавляем к имени файла направление передачи, 81- это передача на сервер

    $fname = (Sdirection eq '!' ? "-> " : "<- ") . $fnarne;

    и преобразуем время передачи к формату времени в Unix

    ($hours,$min,$sec) = split(':',Stime); Sunixdate =

    timelocal($sec,$min,Shours,$mday,$month{$mon},Syear);

    Я помещаем данные в хэш списка списков:

    push((s>{$transfers{substr($rhost, 0, Shostlen)}},

    [Sunixdate,Sfname]); }

    close(XFERLOG); print STDERR "Готово.\n"; }

    Строка push(), вероятно, заслуживает объяснения.

    В этой строке формируется хэш списка списков, выглядящий примерно так:

    $transfers{hostname} =

    ([timel, filenamel], [time2. filena'ne2]1 [time3 filenames] ..)

    Ключами хэша %transfers являются имена узлов, инициирующих передачи файлов. При создании каждой записи мы укорачиваем имя узла до максимальной длины, допустимой в wtmp.

    Для каждого узла мы сохраняем список пар, состоящих из времени передачи файла и его имени. Время сохраняется в «секундах, прошедших с начала эпохи» для упрощения дальнейшего сравнения. Подпрограмма timelocalO из модуля Time: : Local помогает выполнить преобразование времени к этому стандарту. Поскольку мы просматриваем журнал передачи файлов, записанный в хронологическом порядке, список пар тоже строится в хронологическом порядке, а это нам пригодится позже.



    Теперь перейдем к просмотру wtmp :

    просматриваем файл wtmp и заполняем структуру sessions

    информацией о ftp-сеансах sub ScanWtmp {

    my($record, $tty,$name,$host,$time,%connections);

    print STDERR "Просматриваю $wtmp...\n";

    open(WTMP,$wtmp) or die "Невозможно открыть $wtmp:$!\n";

    while (read(WTMP,$record, Srecordsize)) {

    it если запись начинается не с ftp, даже не пытаемся ее

    разбирать (unpack). ЗАМЕЧАНИЕ: мы получаем зависимость от

    формата wtmp в обмен на скорость next if (substr($record,0,3) ne "ftp");

    ($tty,$name,$nost,$time)=unpack($template,$ record);

    и если мы находим запись об открытии соединения, мы

    создаем хэш списка списков. Список списков позже № будет использован в качестве стэка.

    if ($name and substr($name,0,1) ne "\0"){

    push(@{$connections{$tty}},[$host,$time]); }

    # если мы находим запись о закрытии соединения, пытаемся и найти ей пару в записях об открытии соединений,

     найденных раньше else {

    unless (exists $connections{$tty}){

    warn "Найдено только завершение соединения с $tty:" .

    scalar localtime($time)."\n"; next;

     # будем использовать предыдущую запись об открытии

    # соединения и эту запись о закрытии соединения в

    качестве записи об одном сеансе. Чтобы сделать

     это, мы создаем список списков, где каждый список

     имеет вид (hostname, login, logout) push((8>sessions,

    [@{shift @{$connections{$tty}H, Stime]):

    ft если для этого терминала больше нет соединений в

    стэке, удаляем запись из хэша delete $connections{$tty>

    unless (@{$connections<$tty}});

    }

    }

    close(WTMP);

    print STDERR "Готово.\n";

     }

    Давайте посмотрим, что происходит в этой программе. Мы считываем по одной записи из файла wimp. Если эта запись начинается с ftp, мы знаем, что это сеанс FTP. Как говорится в комментарии, строка кода, в которой принимается это решение, явно привязана к формату записи в wtmp. Будь поле tty не первым полем записи, эта проверка не сработала бы. Однако возможность узнать, что строка не представляет для нас интереса, не выполняя для этого unpack(), того стоит.



    Когда мы находим строчку, начинающуюся с ftp, мы разбиваем ее, чтобы выяснить, относится она к открытию FTP-соединения или к закрытию. Если это открытие соединения, то мы записываем его в %connections, структуру данных, хранящую сводку по открытым соединениям. Как и %transfers из предыдущей подпрограммы, это хэш списка списков, на этот раз его ключами являются терминалы для каждого соединения. Каждое значение этого хэша - это набор пар, представляющих имя узла, установившего соединение, и время установки соединения.

    Зачем нужна такая сложная структура данных для слежения за открытием соединений? К сожалению, в wtmp нет простых пар строк «открытие-закрытие открытие-закрытие открытие-закрытие». Например, посмотрим на строки из wtmp (их выводила наша первая в этой главе программа, работающая с wtmp):

    ftpd1833:dnb:ganges.ccs.neu.e:Fn Mar 27 14:04:47 1998

     ttyp7:(logout):(logout):Fri Mar 27 14:05:11 1998

    ftpd1833:dnb:hotdiggitydog-he:Fri Mar 27 14:05:20 1998

    ftpd1833:(logout):(logout):Fri Mar 27 14:06:20 1998

    ftpd1833:(logout): (logout):Fn Mar 27 14:06:43 1998

    Обратите внимание на две записи об открытии FTP-соединения на одном и том же терминале (1-я и 3-я строчки). Если бы мы сохраняли по одному соединению для терминала в простом хэше, то потеряли бы информацию о первом соединении, встретив второе.

    Вместо этого мы в качестве стека используем список списков, ключами которого являются терминалы из %conrecnons. Когда встречается запись об открытии соединения, пара (host. iu<jin-time) помещается в стек для этого терминала. Каждый раз, когда встречается информация о закрытии соединения с этого терминала, одна из записей об открытии соединения «выбрасывается» из стека и вся информация о сеансе целиком сохраняется в другой структуре данных. Для этого в программе есть такая строка:

    push(@sessions,[@{shift @{$connections{$tty}}},$time]);

    Давайте разберемся с этой строкой «изнутри», чтобы все прояснить. Выделенная жирным часть строки возвращает ссылку на стек/список открытых соединений для данного терминала:



    push(@sessions, [@{shift (*{$connections{$tty}}},$time]);

    Эта часть выбрасывает из стека ссылку на первое соединение:

    push(@sessions,[@{shift @{$connections{$tty}}},$time]);

    Мы разыменовываем ее, чтобы получить сам список (host, login-time) для соединения. Если поместить эту пару в начало другого списка, заканчивающегося временем соединения, Perl интерполирует пары для соединения, и мы получим один список из трех элементов. Теперь у насесть группа (host, login-time, logout-time):

    push(@sessions,[©{shift @{$connections{$tty}}>,$time]):

    Теперь, когда у нас есть все составляющие (узел, начало соединения и конец соединения) для сеанса FTP в одном списке, можно добавить ссылку на этот список в список (sessions, который будет использоваться позже:

    push(@sessions, [@{shift @{$connections{$tty}}}, $time]);

    Благодаря одной очень насыщенной строке у нас есть список сеансов.

    Чтобы завершить работу в подпрограмме &ScanWtmp, необходимо проверить, пуст ли стэк для каждого терминала, т. е. проверить, что не осталось больше записей об открытии соединения. Если это так, можно удалить эту запись из хэша; мы знаем, что соединение завершилось:

    delete $connections{$tty} unless (@{$connectio.ns{$tty}});

    Настало время поставить в соответствие два различных набора данных. Эта задача ложится на плечи подпрограммы &ShowTransfers. Для каждого сеанса она выводит список из трех элементов, относящихся к соединению, и файлы, переданные во время этого сеанса.

    обходим в цикле журнал соединений, ставя в соответствие и сеансы с передачами файлов

    sub ShowTransfers { local($session);

    foreach Ssession (@sessions){

    # выводим время соединения print scalar localtime($$session[1]) . "-" .

    scalar localtime($$session[2]) . " $$session[0]\n";

     ищем все файлы, переданные в этом сеансе и выводим их

    print &FindFiles(@{$session}),"\n"; } }

    Вот самая сложная часть, в которой приходится решать, передавались ли файлы в течение сеанса связи:

     возвращает все файлы, переданные в течение данного сеанса sub FindFiles{



    my($rhost,$login,Slogout) = @_;

    my($transfer,@found);

      простой случай, передачи файлов не было

    unless (exists $transfers{$rhost}){

    return "\t(no transfers in xferlog)\n"; }

    простой случай, первая запись о передаче файлов записана

    # после регистрации

    ($transfers{$rhost}->[0]->[0] > $logout){

    return "\t(no transfers in xferlog)\n"; }

     ищем файлы, переданные во время сеанса

     foreach $transfer (@{$transfers{$rhost}}){

     если передача до регистрации

    next if ($$transfer[0] < Slogin);

    и если передача после регистрации

    last if ($$transfer[0] > Slogout);

     если мы уже использовали эту запись next unless (defined $$transfer[1]);

    Первым делом можно исключить простые случаи. Если мы не нашли записей о передаче файлов, выполненной этим узлом, или если первая передача произошла после завершения интересующего нас сеанса, это означает, что в течение данного сеанса файлы не передавались.

    Если нельзя исключить простые случаи, необходимо просмотреть список всех передач. Мы проверяем, произошла ли передача, связанная с данным узлом, после начала сеанса, но до его завершения. Мы переходим к следующей передаче, если какое-либо из этих утверждений неверно. Кроме того, мы прекращаем проверку остальных передач для этого узла, когда находим запись о передаче, произошедшей после завершения соединения. Помните, уже говорилось о том, что все записи о передаче файлов добавляются в структуру данных в хронологическом порядке? Это окупается именно здесь.

    Последняя проверка перед тем, как решить, засчитывать ли запись о передаче файла, выглядит несколько странно:

    Я если мы уже использовали эту запись next unless (defined $$transfer[1]);

    Если два анонимных сеанса с одного и того же узла происходят в одно и то же время, то нет никакого шанса выяснить, к какому из них относится запись о передаче этого файла. Ни в одном из журналов просто не существует информации, которая могла бы нам в этом помочь. Лучшее, что можно тут сделать, - определить правило и придерживаться его. Здесь правило такое: «Приписывать передачу первому возможному соединению». Эта проверка и последующий undef проводят его в жизнь.



    Если последняя проверка пройдена, мы объявляем о победе и добавляем имя файла к списку файлов, переданных в течение этого сеанса. После этого выводим информацию о сеансах и выполненных передачах файлов.

    Подобные программы, в которых выполняется поиск взаимосвязей, могут быть довольно сложными, особенно когда они объединяют источники данных, связи между которыми не являются достаточно четкими. Так что давайте посмотрим, можно ли подойти к этому проще.



    Черные ящики



    В мире Perl часто случается так, что когда вы пытаетесь написать что-то широко используемое, кто-то другой публикует свое решение этой задачи раньше. Это дает возможность просто передать свои данные в уже готовый модуль и получить результаты, не задумываясь о том, как выполняется данная задача. Это часто называют «подходом черного ящика».

    Один такой пример - это пакет SyslogScan Рольфа Харольда Нельсона (Rolf Harold Nelson). Раньше мы уже отмечали, что анализ почтового журнала sendmail может оказаться непростой задачей из-за информации о состоянии. Часто со строкой связана одна или несколько родственных строк, перемешанных с другими строками в этом же журнале. Пакет SyslogScan предоставляет простой способ обратиться к информации о доставке каждого сообщения, так что нет необходимости вручную просматривать файл и выбирать оттуда все связанные строки. Этот пакет позволяет найти в журнале определенные адреса и предоставляет некоторую статистику по найденным сообщениям.

    Пакет SyslogScan объектно-ориентированный, так что первым делом нужно загрузить модуль и создать новый экземпляр объекта:

    use SyslogScan::DeliveryIterator;

     список почтовых журналов

    syslog $maillogs = ["/var/log/mail/maillog"];

    $iterator = new SyslogScan::DeliveryIterator(syslogList => $maillogs):

    Метод new модуля SyslogScan: :DeliveryIterator возвращает итератор (iterator), т. е. указатель в файле, двигающийся от одной строки о доставке сообщения к другой. Применяя итератор, мы избавляемся от необходимости просматривать файл в поисках всех строк, относящихся к конкретному сообщению. Если вызвать метод next() для этого итератора, он вернет нас обратно к объекту доставки. Этот объект хранит информацию о доставке, прежде распределенную по нескольким строкам в журнале. Например, следующий код:



    while ($delivery = $iterator -> next()){ print $delivery->{Sender}." -> ".

    join(",",@{$delivery->{ReceiverList}}),"\n"; }

    позволяет получить такую информацию:

    root@host.ccs.neu.edu ->

    user1@cse.scu.edu




    owner-freebsd-java-digest@freebsd.org -
    >



    user2@ccs.neu.edu rootiahost.ccs.neu.edu -
    >

    user3@ccs.neu.edu

    Можно сделать еще лучше. Если передать итератор из SyslogScan методу new модуля SyslogScan: :Summary, new примет весь вывод метода next итератора и вернет итоговый объект. Этот объект содержит итоговую информацию по всем доставкам сообщений, которые только может вернуть итератор.

    Но SyslogScan переносит эту функциональность на другой уровень. Если передать последний объект методу new из SyslogScan: : ByGroup, мы получим объект bygroup, в котором вся информация сгруппирована по

    доменам и приводится статистика по этим группам. Вот как применяется то, о чем мы только что говорили:



    use SyslogScan::DeliveryIterator;




    use SyslogScan::Summary;




    use SyslogScan::ByGroup




    use SyslogScan::Usage;




    П местоположение maillog




    Smaillogs = ["/var/log/mail/maillog"];




    # получаем для этого файла итератор




    $iterator = new SyslogScan::DeliveryIterator(syslogList => $maillogs);




    # передаем итератор в ::Summary, получаем объект summary (сводка)




    Ssummary = new SyslogScan::Summary($iterator);




    tt передаем сводку в ::ByGroup и получаем обьект stats-by-group




    # (статистика по группам)




    Sbygroup = new SyslogScan::ByGroup($summary);




    ft выводим содержимое этого объекта foreach $group (sort keys %$bygroup){




    ($bmesg,$bbytes)=@{$bygroup->{$group}->




    {groupUsage}->getBroadcastVolume()}; ($smesg,$sbytes)=@{$bygroup->{$group}->




    {groupllsage}->getSendVolume()}; ($rmesg,$rbytes)=@{$bygroup->{$group}->




    {groupUsage}->getfieceiveVolume()}; ($rmesg,$rbytes)=@{$bygroup->{$group}->




    {groupUsage}->getReceiveVolume()}; write; }




    format STDOUT.TOP =




    Name Bmesg BByytes Smesg SBytes Rmesg Rbytes






    format STDOUT =




    @««««<«««« @»>» @»»>» @»>» @»»»> @»»> @»»»>




    Sgroup,Sbmesg,Sbbytes,$smesg,Ssbytes,$rmesg,$rbytes


    Результат представляет собой подробный отчет по количеству широковещательных, отправленных и полученных сообщений и их размера в байтах. Вот отрывок из получаемых результатов:

    Name Bmesg BByytes Smesg SByres Rmesg Roytes


    globalserve net

    1

    1245

    1

    1245

    0

    0

    gloDe.com

    0

    0

    0

    0

    1

    2040
    Положительная сторона такого подхода в том, что можно сделать многое благодаря тяжелой работе, проделанной автором модуля или сценария, не прикладывая больших усилий со своей стороны. Отрицательная сторона - необходимость во всем полагаться на код автора. В нем могут быть ошибки или может использоваться подход, не устраивающий вас. Всегда стоит сначала ознакомиться с программой, прежде чем «дать ей зеленую улицу» у себя на сайте.



    Использование баз данных



    Последний подход, который мы обсудим, для своей реализации требует других знаний помимо Perl. Так что мы просто рассмотрим технологию, которая со временем, вероятно, станет более популярной.

    Все рассмотренные предыдущие примеры хорошо работают с данными приемлемого размера и на машинах с приемлемым количеством памяти, но они не масштабируемы. В ситуациях, когда у вас много данных, особенно если они поступают из различных источников, естественным инструментом становятся базы данных.

    Существует по крайней мере два способа использования баз данных из Perl. Первый из них я называю методом «только Perl». В этом случае все действия осуществляются в Perl или в библиотеках, тесно связанных с Perl. Во втором применяются модули, например из семейства DBI, позволяющие сделать Perl клиентом баз данных, таких как MySQL, Oracle или MS-SQL. Рассмотрим оба подхода для обработки и анализа журналов.



    Использование баз данных, встроенных вРег!



    До тех пор пока данных не слишком много, можно применять только Perl. В качестве примера мы будем использовать расширенную версию вездесущего «искателя брешей в системе безопасности». До сих пор наша программа имела дело с соединениями только на одной машине. Как поступить, если захочется узнать о регистрации злоумышленников и на других наших машинах?



    Первый шаг - поместить все данные из wtmp для наших машин в ту или иную базу данных. Будем считать, что все машины имеют прямой доступ к некоторым разделяемым каталогам через некую сетевую файловую систему наподобие NFS. Перед тем как двигаться дальше, необходимо выбрать формат базы данных.

    В качестве «формата баз данных для Perl» я выбрал формат Berkeley DB. Я беру «формат баз данных для Perl» в кавычки потому, что хоть поддержка DB встроена в Perl, сами библиотеки DB необходимо достать в другом месте (http://www.sleepycat.com) и установить их до того, как поддержка Perl будет скомпилирована. Ниже приведено сравнение между различными поддерживаемыми форматами баз данных (табл. 9.4).



    Таблица 9.4.

    Сравнение поддерживаемых в Perl форматов баз данных

    Название Поддержка в Unix Поддержка в NT/2000 Поддержка в MacOS Ограничения

    на размеры ключей или значений
    Независимость

    от порядка байтов

    старый

    dbm
    Да Нет Нет Нет
    «новый» dbm Да Нет Да Нет
    Sdbm Да Да Нет 1К (по умолчанию) Нет
    Gdbm Да Да Нет Нет Нет

    DB Да Да Да Нет Да
    Мне нравится формат Berkeley-DB, поскольку он может обрабатывать большие объемы данных и не зависит от порядка байт. Независимость от порядка байт особенно важна для программы, которую мы собираемся рассмотреть, т. к. мы будем считывать и записывать данные в один и тот же файл с различных машин, у которых может быть различная архитектура.

    Начнем с заполнения базы данных. В целях простоты и переносимости мы остановим свой выбор на программе last, чтобы не использовать un-packQ для различных файлов wtmp. Вот программа, за которой следуют объяснения:

    use DB_File;

    use FreezeThaw qw(freeze thaw);

    use Sys::Hostname; n чтобы получить текущее имя узла

    use Fcntl; # для определения 0_CREAT и 0_RDWR

    ищем исполняемый файл для программы

    last (-х "/bin/last" and $lastex = "/bin/last")

    or (-x "/usr/ucb/last" and Slastex = "/usr/ucb/last");

    Suserdb = "userdata";



      файл базы данных пользователей Sconnectdb = "connectdata";

     файл базы данных соединений Sthishost = &hostname;

    ope'n(LAST, "$lastex|") or

    die "Невозможно запустить программу Slastex:$!\n'

    считываем каждую строку вывода last while (<LAST>){

    next if /~reboot\s/ or /~shutdown\s/ or /~ftp\s/ or /~wtmp\s/;

    ($user,$tty,$host,$day.$mon.SdateStime) = split;

    next if $tty =~ /":0/ or $tty =" /"consoles/;

    next if (length($host) < 4);

    $when = $mon." ".Sdate," ".Stime;

    tt сохраняем каждую запись в хэше списка списков

    push(ia{$users{$user}},[$thishost,$host,$when]);

    push(@{$connects{$host}},[$thishost,$user.$when]); }

    close(LAST);

    tt создаем файл базы данных (для чтения и записи); если он не

    существует, смотрите сноску в тексте re: $DB_BTREE tie %userdb,

    "DB_File",$userdb,0_CREAT]0_RDWR, 0600, $DB_BTREE or die

     "Невозможно открыть базу данных Suserdb для чтения/записи:$!\n"

    tt обходим в цикле пользователей и сохраняем информацию в базе

    данных при помощи freeze foreach $user (keys %users){ if (exists $userdb{$user}){

    (Suserinfo) = thaw($userdb{$user»;

    push(@{$userinfo},(a{$users{$user}});

    $userdb{$user}=freeze Suserinfo; } else {

    Suserdb!$user}=freeze $users{$user}; } } untie %userdb;

    tt делаем то же самое для соединений

    tie %connectdb, "DB_File",Sconnectdb,0_CREAT]0_RDWR,

    0600, $DB_BTREE

    or die "Невозможно открыть базу данных Sconnectdb для чтения/записи:$!

    foreach Sconnect (keys %connects){ if (exists $connectdb{$connect}){

    ($connectinfo) = thaw($connectdb<$connect}); piiSh(@{$connectinfo}. 5>{$connects{$cornect}}): $connectdb{$connect}=freeze($connectinfo); } else {

    $connectdb{$connect}=freeze($connects{$connect}); } } untie %conner,tdb

    Программа принимает вывод команды last и делает следующее:

    1. Отфильтровывает бесполезные строки.

    2. Сохраняет вывод в двух хэшах списка списков, структура данных которых выглядит так:



    $users{usernair;e} =

    [[ current host, connecting Host, connect time], [current host, connecting host, connect time]

    ]; $connects{host} =

    [[current host, usernamel, connect time], [current host, username2, connect time],

    ];

    3. Помещает структуру данных в память и пытается добавить ее в базу данных.

    Этот последний шаг самый интересный, поэтому рассмотрим его подробно. Мы связываем хэши %userdb и %connectdb с файлами баз данных. Это позволяет легко обращаться к хэшам, в то время как Perl «за сценой» обрабатывает сохранение и получение данных из файлов базы данных. Но в хэшах хранятся только простые строки. Как преобразовать наш «хэш списка списков» в одно значение?

    Модуль FreezeThaw Ильи Захаревича используется для хранения сложной структуры данных в одном скалярном значении, которое можно применять в качестве значения хэша. FreezeThaw принимает произвольную структуру данных Perl и представляет ее в виде строки. Существуют и другие модули, подобные этому, самые распространенные из которых Data::Dumper Гурусами Сарати (Gurusamy Sarathy) (входит в состав Perl) и Storable Рафаэля Манфреди (Raphael Manfredi). FreezeThaw обеспечивает наиболее компактное представление сложной структуры данных, поэтому он и используется здесь. Каждый из этих модулей имеет свои плюсы, так что внимательно изучите возможности всех трех, если вам нужно будет решать задачу, подобную нашей.

    В программе мы проверяем, существует ли запись для этого пользователя или узла. Если нет, мы просто «замораживаем» структуру данных в строку и сохраняем эту строку в базе данных при помощи связанного хэша. Если существует, мы «размораживаем» существующую структуру данных из базы данных в память, добавляем наши данные, вновь ее «замораживаем» и сохраняем.

    Выполнив эту программу на нескольких машинах, мы получим базу данных с некоторой потенциально полезной информацией, которую можно будет добавить в следующую версию нашей программы.

    Подходящее время для заполнения подобной базы данных - сразу после операции ротации журналов wtmp.



    Код, используемый здесь для заполнения базы данных, настолько прост (это скорее план, чем реальная программа), что его не стоит широко применять в жизни. Один недостаток, который бросается в глаза, это отсутствие механизма, предотвращающего попытки одновременного обновления базы данных несколькими экземплярами программы. Учитывая, что блокировка файлов через NFS по крайней мере неочевидна, было бы проще вызывать подобную программу из большей программы, которая возьмет на себя сбор информации от каждой машины.

    Теперь, заполнив базу данных, рассмотрим улучшенную версию программы, использующей эту информацию:

    use DB_File;

    use FreezeThaw qw(freeze thaw);

    use Fcntl;

     принимаем имя пользователя и узлы, которые мы игнорируем, в

     командной строке ($user,$ignore) = @ARGV;

     файлы баз данных, которые мы используем

    Suserdb ="userdata"; $connectdb ="connectdata";

    tie %userdb, "DB_File",Suserdb,0_RDONLY,666,$DB_BTREE or die

    "Невозможно открыть базу данных $userdb для чтения:$!\п";

    tie %connectdb, "DB_Fiie",Sconnectdb,0_RDONLY,666,$DB_BTREE or die

    "Невозможно открыть базу данных Sconnectdb для чтения :$!\г";

    Мы загрузили нужные нам модули, получили необходимые данные, установили несколько переменных и связали их с файлами базы данных. Теперь пришло время немного поработать:

    можно выходить, если этот пользователь не устанавливал

    соединений

    unless (exists $jserdb{$user}){

    print "Этот пользователь не регистрировался.

    untie %userdb;

    untie %cor,".ectdc,

    exit; }

    (Suserinfo) = thaw($userdb{$user}):

    print "-- first host contacts from $user --\n": foreacn $contact (@{$userinfo}){

    next if (defined Signore and $contact->[l] =~ /$ignore/o):

    print $contact->[1] . " -> " , $contact->[0] . " on ".$contact->[2]."\n";

    $otherhosts{$contact->[1]}=' ' ; }

    Вот как работает этот код: если мы видели этого пользователя, то воспроизводим в памяти записи о его соединениях при помощи thawQ. Для каждого контакта мы проверяем, нужно ли игнорировать соединения с этого узла. Если нет, то выводим информацию об этом соединении и записываем узел, с которого оно было установлено, в хэш %otherhosts.



    Здесь хэш применяется как простой способ собрать список уникальных узлов из всех записей о соединениях. Теперь, когда у нас есть список узлов, с которых мог зарегистрироваться злоумышленник, необходимо выяснить, какие еще пользователи регистрировались с этих подозрительных узлов.

    Найти эту информацию будет не сложно, потому что когда мы записывали, какие пользователи регистрировались на каких машинах, мы также записывали и обратное (т. е. на каких машинах регистрировались какие пользователи) в другом файле базы данных. Теперь мы смотрим на записи, соответствующие найденным на предыдущем шаге узлам. Если этот узел не надо игнорировать и с него было зарегистрировано соединение, мы собираем список пользователей, регистрировавшихся на этом узле, при помощи хэша %userseen:

    print "-- other connects from source machines --\n";

    foreacn $nost (keys %otherhosts){

    next if (defined Signore and $host =" /$ignore/o):

    next unless (exists $connectdb{$host});

    (Sconnectinfo) = thaw($connectdb{$host});

    foreach Sconnect ((°>{$connectinfo}){

    next if (defined Signore and Sconnect->[0] =~ /$ignore/o);

    $userseen{$connect->[1]}=''; } }

    Последнее действие этой драмы в трех актах имеет элегантный конец. Мы возвращаемся к первоначальной базе данных пользователей, чтобы найти все соединения, установленные подозрительными пользователями с подозрительных машин:

    foreach Suser (sort keys %userseen){ next unless (exists $userdb{$user}):

    foreach Sconract («(Suseri'ifo» !

    next if (iio'-i^ed $ignore and Scontact->[ 1 ]

    write i" (iiMsts $otherhos\ slSco

    }

    Нам осталось только подмести сцену и уйти домой:

    untie %userdb; untie %connectdb;

    format STDOUT =

    @«««« @<««««««« -> ffl<«««««««

    $user.":", Scontact->[1],Scontact->[0],$contact->[2]

    Вот как выглядит вывод этой программы (опять же, имена пользователей и машин изменены):

    -- first host contacts from baduser --

    badhostl.exampl -> machine1.ccs.neu.edu on Jan 18 09:55

    badhost2,exampl -> machine2.ccs.neu.edu on Jan 19 11:53



    -- other connects from source machines --

    baduser2: badhostl.exampl -> machine2.ccs.neu.e on Dec 15 13:26

    baduser2: badhost2.exampl -> machine2,ccs.neu.e on Dec 11 12:45

    baduser3: badhostl. exampl -> machinel. ccs. neu. ed on Ji;l 13 16:20

    baduser4: badhostl.exampl -> machinel.ccs.neu.ed on Jun 9 11:53

    baduser: badhost1.exampl -> machinel.ccs.neu.ed on Jan 18 09:55

    baduser: badhost2. exampl --> machine2. ccs. neu. e on Jan 19 11:53

    Эта программа хороша в качестве примера, но она не масштабируется дальше, чем на небольшую группу машин. Для каждого последующего вызова программы необходимо прочитать запись из базы данных, «растопить» (thaw()) ее в памяти, добавить новые данные, снова их «заморозить» (freezeO) и сохранить опять в базе данных. Это может потребовать больших затрат процессорного времени и памяти. Потенциально весь процесс происходит для каждого пользователя и соединения, так что все замедляется очень быстро.



    Использование баз данных SQL



    Теперь рассмотрим один из способов обращения с очень большими наборами данных. Вам может потребоваться загрузить данные в более сложную базу данных SQL (коммерческую или нет) и запрашивать из нее информацию, используя SQL. Тем, кто не знаком с SQL, я рекомендую ознакомиться с приложением D «Пятнадцатиминутное руководство по SQL», перед тем как смотреть на этот пример.

    Заполнить базу данных можно так:

    use ОВГ

    use Sys .

    $db = "drib":

    ищем месгоголожс.чие iasr (-х '/bin/last" and $]astex = "/от/ lasc")

     (-x "/usr/ucb/last" ana Slastex = '/азг7ьсо.' ;ast"):

     подсоединяемся к базе даинь.х Sybase

    переходим иа базу данных, которую мы будем использовать

    $dbh->do("use $db") or die "Невозможно перейти к $db: ".$dbh->errstr."\n";

    в

    создаем таблицу iastinfo, если ее еще не существует unless ($dbh->selectrow_array(

    q{SELECT name from sysobjects WHERE name="lastinfo"})){

    $dbh->do(q{create table Iastinfo (username char(8),



    localhost char(40), otherhost varchar(75), when char(18))}) or

    die "Невозможно создать таблицу Iastinfo: ".$dbh->errstr."\n"; }

    Sthishost = Shostnanie:

    $sth = $dbh->prepare(

    qq{INSERT INTO lastinfo(username,localhost,otherhost.when) VALUES (?, 'Sthishosf, ?, ?)}) or die

    "Невозможно подготовить запрос insert: ". Sdrj.h-^errstr , "\n":

    open(LAST."Slastexj") or die "Невозможно выполни:» программу Slastex

    while (<LAST>){

    next if /"reboot\.s/ or /'shutdown'.s/ or

    /"ftp';S/ or / "ivt-rpv.s/:

    $wher> = $!non " ".Scare." ". $tme:

    Sst1-->sxec-J'te;$j3e',SbcЈt.$/.ter^ close(LAST);

    $c)bh->di scanned;

    Теперь можно использовать базу данных по назначению. Вот набор простеньких SQL-запросов, которые легко можно выполнить из Perl при помощи интерфейсов DBI или ODBC, о которых мы говорили в главе 7 «Администрирование баз данных SQL»:

    -- сколько всего записей в таблице

    9 select count (*) from lastinfo;

    10068

    -- сколько пользователей было зарегистрировано

    9 select count (distinct username) from lastinfo;

    237

    -- сколько различных узлов устанавливали соединение с нашими машинами

     select count (distinct otherhost) from lastinfo;

    1000

    -- на каких локальных машинах регистрировался пользователь

    "dr.b"? select distinct localhost from lastinfo where username = "dnb": localhost

    hostl host2

    Эти примеры должны помочь читателю прочувствовать, как можно «исследовать» данные, когда они хранятся в настоящей базе данный Каждый из этих запросов требует для выполнения лишь около секунды. Базы данных могут быть быстрым, мощным инструментом дл системного администрирования.

    Анализ журналов - бесконечная тема для разговора. К счастью, 1 глава снабдила вас кое-какими инструментами и некоторым вдохновением.


    Содержание раздела