Поиск проблемных образцов
Поиск проблемных образцов
Теперь воспользуемся тем, что мы узнали в главе 9 и перейдем дальше. Только что мы говорили о поиске подозрительных объектов; давайте теперь рассмотрим образцы (patterns), которые могут быть признаками подозрительной активности. Покажем это на примере программы, выполняющей примитивный анализ журналов в поисках потенциальных взломов.
Пример строится на предположении, что большинство пользователей, удаленно регистрирующихся в системе, делают это из одного и того же места или нескольких мест. Обычно они регистрируются либо с одной машины, либо используя адреса из диапазона, принадлежащего одному и тому же провайдеру. Если вы обнаружите, что пользователь регистрировался из нескольких доменов, это верный признак того, что данная учетная запись была «взломана» и ее пароль стал доступен многим. Очевидно, что такое предположение не справедливо для постоянно перемещающихся пользователей, но если вы обнаружите, что пользователь регистрировался сначала из Бразилии, а потом из Финляндии с интервалом в два часа, то это достаточный повод заподозрить что-то неладное.
Рассмотрим программу, реализующую поиск таких признаков. Сама программа написана для Unix, но демонстрируемые в ней приемы не зависят от платформы. Во-первых, это встроенная документация.
Неплохо поместить нечто подобное ближе к началу программы для тех, кто будет смотреть на исходный код. Перед тем как двигаться дальше, обязательно взгляните, какие аргументы поддерживаются
программой:
sub usage {
print «"EOU"
lastcheck - проверяет вывод команды last,
ИСПОЛЬЗОВАНИЕ: lastchecK [args]. где вместо может быть:
-i: для IP-адресов, считан-^ сеть класса С одним 'доменом
-л: помощь (это сообщение)
/usr/ucb/last
exit:
Сначала мы анализируем аргументы из командной строки, просматриваются аргументы программы и соответствующим образом устанавливается $ор1_<буква_фла!а>. Двоеточие после буквы
говорит о том, что этот параметр принимается как аргумент:
use Getopt::Std: ft сгиндарт.ньи: процессор
&usage if (defined $opt_h):
it допустимое количество уникальных
Smaxdomains = (defined $opt_ni) 9 Sopt.r : 3:
В следующих строчках реализуется выбор, сделанный в пользу переносимости (но в ущерб эффективности) - об этом мы говорили в главе 9. На этот раз мы решили вызвать внешнюю программу. Для того чтобы сделать программу менее переносимой, но несколько более эффективной, можно было использовать uripack(), о чем тоже говорилось в той
главе:
Slastex = (defined $opt_l) ? $opt_l : "/usr/ucb/last":
open(LAST,"$lastex|") [| die "Невозможно выполнить программу Slastex:$!\n";
Перед тем как двигаться дальше, давайте взглянем на хэш списков, с помощью которого программа обрабатывает данные, полученные от last. Ключами этого хэша являются имена пользователей, а значениями - ссылки на список уникальных доменов, с которых регистрировался пользователь.
К примеру, запись может выглядеть так:
Suserinfo { laf } = [ 'ccs.neu.edu', 'xerox,com', 'foobar.edu' ]
Эта запись говорит о том, что пользователь laf регистрировался с доменов ccs.neu.edu, xerox.com и foobar.edu.
Начинаем мы с того, что обходим в цикле вывод команды last. На нашей системе он выглядит примерно так:
Cindy pts/10 sinai.ccs.neu.ea Fri Mar 27 13:51 still logged in
michael pts/3 regulus. ccs. neu Fn Mar 27 13:51 still logged I"'
david pts/5 fruity-pebbles.с Fri Mar 27 13:48 still logged in
deborah pts/5 grape-nuts.ccs.n Fri Mar 27 11:43 - 11:53 (00:09)
barbara pts/3 152,148.23,66 Fri Mar 27 10:48 - 13:20 (02:31)
Jerry pts/3 nat16. asoer-tec с Fri Mar 27 09:24 - 09:26 (00:01)
Заметьте, что имена узлов (в 3-й колонке) в выводе команды last усечены. В главе 9 мы уже говорили об ограничениях на длину имени узла, но до сих пор мы обходили стороной это препятствие. Когда мы попробуем заполнить нашу структуру данных, проблемы станут очевидными.
Раньше в цикле while мы пытались пропустить строчки, содержащие данные, которые нас не интересуют. Как правило, проверка особых случаев в самом начале цикла до какой-либо обработки данных (на-
пример, при помощи spnt()) - неплохая идея. Это позволяет программе быстро определить, что можно пропустить определенную строчку и перейти к дальнейшему чтению данных:
while (<LAST>){
игнорируем специальных погьзонателей
next if /~rePoot\s]"shutdown\s|~flp\s/:
если использовался ключ -и д/'.я определения конкретного
пользователя, пропускаем все записи, не относящиеся к
нему (имя сохраняется в $opt_u функцией getopts)
next if (defined $opt_u && !/"$opt_u\s/);
игнорируем вход с консоли X
next if /:0\s+:0/;
# ищем имя пользователя, терминал и имя удаленного узла
($user, $tty,$host) = split;
игнорируем, если запись в журнале имеет "плохое" имя
пользователя
next if (length($user) < 2);
игнорируем, если для данного имени нет информации о домене
next if $host !" Д./;
ищем доменное имя узла (см. приведенное ниже объяснение)
$dn = &domain($host);
игнорируем, если доменное имя фиктивное
next if (length ($dn) < 2);
игнорируем эту строку, если она находится в домене,
заданном ключом -f
next if (defined $opt_f && ($dn =' /~$opt_f/));
если мы не встречали раньше имя этого пользователя,
просто создаем список доменов для этого пользователя и
сохраняем эту информацию в хэше списков
unless (exists $userinfo{$user}){
$userinfo{$user} = [$dn];
в противном случае нам придется нелегко:
см. приведенное ниже объяснение
else {
&AddToIfo($user.Sdr):
closed-AST).
Теперь рассмотрим отдельные подпрограммы, предназначенные для разрешения сложных ситуаций в программе. Первая подпрограмма &domain() принимает полностью заданное доменное имя, т. е. имя узла с полным доменным именем, и возвращает лучшую догадку о доменном имени для этого узла. Есть две причины, по которым подпрограмма должна быть довольно умна:
- Не все имена узлов из журналов будут именами. Это вполне может быть и IP-адрес. В этом случае, если пользователь устанавливает ключ -г, мы полагаем, что любой получаемый нами IP-адрес - это
адрес сети класса С, разделенной на подсети по границе байта. На практике это означает, что доменным именем мы считаем первые три октета адреса. Это позволяет нам считать регистрацию в системе с адресов 192.168.1.10 и 192.168.1.12 регистрацией из одного логического источника. Вероятно, это не лучшее предположение, но это лучшее, что мы можем сделать, не обращаясь при этом к другому источнику информации (да и в большинстве случаев это работает). Если пользователь не указывает ключ -i, мы считаем весь IP- адрес доменом. - Как говорилось раньше, имена узлов могут быть усечены. Это приводит к тому, что мы имеем дело с неполными записями, подобными grape-nuts, ccs. n и nat16. aspentec.c. Это не так страшно, как кажется, потому что полностью определенное имя домена в журнале каждый раз будет усекаться на одном и том же месте. В подпрограмме &AddToInfo() мы попробуем сделать все возможное, чтобы справиться с этим ограничением. Но об этом чуть позже.
А пока вернемся к программе:
принимаем полностью определенное имя домена и пытаемся
определить домен
sub domain{
ищем IP-адреса
if ($„[0] =' /-\d+\.\d+v\d+v\d+$/) {
если пользователь не указал ключ -i. просто
возвращаем IP-адрес как есть
unless (defined $opt_i){
return $_[0];
}
иначе возвращаем все. кроме последнего октета
else {
$_[0] =~ /(.-)\.\d+$/:
return $1;
(
}
переводим все в нижний регистр, чтобы потом было
# проще и быстрее обрабатывать информации!
S. [.и] = 1с($_[С]);
}
}
Следующая очень короткая подпрограмма заключает в себе самую сложную часть программы. Подпрограмма &AodToTr ч;() работает с усеченными именами узлов и сохраняет информацию в хэш-таблицу. Мы применим способ сравнения подстрок, который может пригодиться и в ином контексте.
В нашем случае было бы неплохо, если бы все эти имена доменов (читались бы и сохранялись бы в массиве уникальных имен доменов для пользователя как одно имя:
ccs.neu.edu
ccs.neu.ed
ccs. n
Решая, является ли имя домена уникальным, необходимо проверить три вещи:
1. Совпадает ли имя домена полностью с чем-нибудь, что уже сохранено для этого пользователя?
2. Является ли это имя домена подстрокой уже сохраненных данных?
3. Являются ли подстрокой проверяемого имени домена сохраненные данные?
Если верно что-либо из этого списка, значит, нет необходимости добавлять новую запись к структуре данных, поскольку эквивалентная под строка уже сохранена в списке доменов для пользователя. Если выполняется пункт 3, мы заменим сохраненную запись текущей, если, конечно, мы сохраняем строки максимальной длины. Внимательные читатели могли заметить, что выполнение первых двух пунктов можно проверять одновременно, поскольку точное совпадение эквивалентно совпадению подстроки по всем символам.
Если же не справедлив ни один из этих случаев, то необходимо сохранить новую запись. Посмотрим сначала на код, а потом обсудим, как он работает:
цио AodTolPfc!
проверка 1-го и 2-го случаев: есть л/ полное или
и частичное совпадение9
renirn If (1ndOvC$ $п;гИ -- IV
и проверка 3-го случая, го есть. явлй^:.;ч ,-;,, :-с„отоокоп
# сохраненные данные
if (index($un. $_.) > -1){
return:
}
# в противном случае это новый домен, добавляем его в список
push @{$userinfo{$user}}, $dn:
}
Конструкция @{$userinfo{$user}} возвращает список доменов, сохраненных для этого пользователя. Мы обходим в цикле все элементы из этого списка, проверяя, можно ли найти среди них $dn. Если можно, то
мы выходим из подпрограммы, т. к. эквивалентная подстрока уже сохранена.
Если эта проверка пройдена, то можно перейти к пункту 3. Мы проверяем каждую запись из списка, чтобы выяснить, встречается ли она в текущем домене. Мы заменяем запись из списка на текущий домен, если совпадение найдено, тем самым сохраняя более длинную из двух строк.
Поскольку это не вредит, замена производится и при точном совпадении. Мы переписываем запись, используя специальное свойство операторов fо г и fо reach в Perl. Присваивая значение переменной $_ в середине цикла for, Perl в действительности присваивает значение текущему элементу списка. Переменная цикла становится псевдонимом для переменной списка. После того как мы поменяли местами значения, можно выходить из подпрограммы. Если были пройдены все три проверки, то
в последней строке к списку доменов для пользователя добавляется рассматриваемое имя домена.
Это все, что касается «кровавых» деталей просмотра файла и создания структуры данных. Чтобы завершить эту программу, рассмотрим всех найденных пользователей и проверим, со скольких доменов они регистрировались (т. е. выясним длину сохраненного для каждого из них списка). По тем записям, для которых найдено больше доменов, чем можно, мы выводим полный список:
for (sort keys %usermfo){
if ($#{$jsenr-fo{$J} > $."iaxaora:"s)!
}
РГЩГ "V:' Протокол SNMH
Что ж, программу вы видели и вас, вероятно, интересует, действительно ли работает этот метод. Вот реальный отрывок вывода этой программы для пользователя, чей пароль был украден:
38.254.131
bj.edu
ccs.neu.ed
dac.neu.ed
hials.no
ipt. a
tntl.bosl
tntl.bost
tntl. dia
tnt2.bos
tntS.bos
tnt4.bo
toronto4.di
Некоторые из этих записей выглядят нормально для пользователя, живущего в Бостоне. Однако запись toronto4.di выглядит несколько подозрительной, а сайт hials.no вообще находится в Норвегии. Схвачены с поличным!
Программу можно усовершенствовать, добавив проверку времени или сравнение с другими журналами, например, полученных при помощи tcpwrappers. Но как видите, поиск шаблонов часто важен сам по себе.