Настольная книга компьютерного исследователя


Expect language

1. Что такое expect ?

Expect — это расширения языка tcl, которые дают возможность управлять в скриптах интерактивными программами типа telnet, ftp и подобными, то есть можно проверять результат выполнения каждой операции той программой, которой мы управляем, и в зависимости от этого выбирать следующие действия или данные, которые мы передаем процессу. Это дает возможность полностью автоматизировать многие процессы, причем с приложением наименьших усилий, так как например при программировании для tcp/ip не нужно самому писать реализацию протокола- все уже реализованно в клиенте, которым мы и будем управлять. К тому же, этот язык вполне прост. Мне больше всего это напоминает всем известные сканеры x.25 сетей типа ultrahack, где в зависимости от ответа ПАДа выбиралось, что же делать дальше. Что же можно сканить в инете такими штуками? Хотя бы те же x.25 сети. А также можно перебирать пароли, всякие там аськи и вообще выполнять процесс тупого перебора без участия человека. Для этого надо всего лишь найти шелл (юниксовый — я никогда не видел реализацию под винды), где установлен expect, дать команду типа :

nohup <имя_вашего_скрипта> &

, а потом logout и идти спать, а поутру забрать результаты. С другой же стороны, tcl — язык, очень похожий на перл, и на C. То есть на нем можно создавать всякие хитрые циклы и так далее, что не было возможно (?вроде… не помню) в скриптовых языках всяких терминалок и ultrahack’ов. Однако, чтобы написать сценарий на expect-е который что-нибудь ломает (или просто делает) перебором, не обязательно слишком много знать о tcl. Но tcl является для многих задач неплохим эквивалентом perl’a (та же идеология), и может пригодиться юниксоиду.

2. Основа.

Так как данный язык является интерпретатором, подобным UNIX’овому шеллу (expect кстати можно использовать интерактивно, но нахрен это надо, если он нужен для устранения интерактивности?), то начало будет такое же, как и везде:

#!/usr/bin/expect

то есть указание пути к интерпретатору. А дальше все, как и в шелл скриптах, и в перле: пишем команды, сохраняемся, chmod +x <имя_файла> и вперед. Чтобы найти интерпретатор (то есть правильный к нему путь), можно использовать команду `which expect` Основная структура команды языка такова:

имя_команды аргументы

Команды разделяются либо символами новой строки, либо точкой с запятой, но я советую всегда после команды ставить точку с запятой — будет меньше геморроя и ошибок, хотя бы с теми же комментариями ( которые как и в перле начинаются с #) и с добавлением нового кода к уже написанному.

3. Управление процессом.

По мнению авторов всех мануалов и книг, чтобы начать работать с expect’ом, нужно знать всего четыре команды для общения с интерактивным процессом. На самом деле для написания сложных вещей этих команд может не хватить, так как язык поддерживает множество полезных вещей, например есть команда fork, которая работает аналогично юниксово-сишной, то есть создает копию текущего процесса, есть и всякие там exit и wait, что позволяет организовать многопоточность, но об этом — читаем уже в более серьезной литературе.

Итак, основные команды expect:

Spawn — запускает процесс для управления. Формат команды:
spawn [аргументы_команды_spawn] program [аргументы_процесса], но так как обычно юниксовые клиенты могут обойтись и без всяких параметров, то обычно можно обойтись чем-нибудь типа spawn /usr/bin/telnet send — послать в управляемый процесс входные данные. Тут все просто,и хотя у команды тоже есть всякие флаги, обычно обходятся без них, например:
send «hello world\r», где \r — это, как и везде, аналогично нажатию кнопочки <ENTER>. Можно использовать и переменные, что-нибудь типа send»$login_name\r» (о переменных будет написано потом).

Expect (нет,не язык expect,а команда expect) — основа одноименного языка. Именно она выполняет какое-либо действие в зависимости от выходной информации процесса, которым мы управляем. Команда представляет собой примерно следующее:


expect
{
[ключ] "вариант_ответа_процесса_1" {действие_1}
[ключ] "вариант_ответа_процесса_2" {действие_2}
[ключ] "вариант_ответа_процесса_n" {действие_n}
}

то есть вещь, напоминающая сишный switch: процесс передал вариант_ответа_процесса_1 -выполняем действие_1 и так далее. То есть в скрипте в команде expect надо описать всю возможную реакцию системы на введенную нами команду, а если мы что-то не опишем — для этого будет ветка timeout, то же что default в switch. Ключ — необязательная часть, который задает правило обработки варианта ответа. Наиболее распространенные ключи: -re (возможно задание разных вариантов в одном месте, например -re «failed|invalid password», для совпадения достаточно, чтобы подстрока встречалась в ответе системы), и -ex (без интерпретации всяких * и прочих wildcards, сравнение целиком строк). Кстати, этот timeout — системная переменная, период ожидания можно сменить командой set timeout <сколько_надо_секунд>.

Вот простенький пример из книжки Немет:

 while 1 { expect {
     "Name*: " {send "$login\r"}
     "Password:" {send $password\r"}
     "ftp> "     {break}
     "failed" {send_user "cannot login\r"; exit 1 }
     timeout {send_user "timeout\r", exit 2 }
 }}

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

exit — выход (а вы чего ждали?) из скрипта;
break — выход из цикла (тоже в принципе очевидно);
send_user — то же, что и send, только не в процесс, а для пользователя.

Я думаю понятно, что так можно например перебирать пароли по telnet, сканить x.25 через гейт или диалаут и так далее, а не просто мирно заходить на ftp, как показано выше.

Четвертой важной командой считается interact — для передачи управления пользователю. Так как все эти скрипты нужны именно для того, чтобы не беспокоить юзера(то есть нас самих), не знаю, понадобится ли она нам вообще. Однако кроме этих команд есть и еще не менее важные, важность которых ощутится лишь в более-менее сложных скриптах:

close -разрывает связь с текущим запущенным процессом. Если процесс был грохнут руками, то после убийства надо вызвать close. Сразу говорю как убить порожденный процесс. Команда exp_pid без параметров возвращает PID текущего процесса. Используя блок командного замещения (этот блок начинается с «[" и заканчивается "]«, при выполнении на место блока будет подставлен результат выполнения команды, записанной в этих самых квадратных скобочках) можем создать переменную, в которой лежит PID- set procpid [exp_pid] и впоследствии убить этот процесс командой exec kill $procpid (как, я думаю, вы уже догадались, exec выполняет
внешнюю команду).
wait — команду можно использовать не только как в C для ожидания, пока отработают порожденные процессы, но и для того, чтобы подождать завершения текущего процесса. Если процесс постоянно необходимо завершать и снова запускать, то использование close (для текущего процесса-без параметров) позволит избежать всяческих нехороших ситуаций.

match_max — изменить размер буфера. В случае, когда процесс присылает много информации, размера по умолчанию (2000) может не хватить. Формат — match_max <новый_размер>.

4. Переменные, работа с ними.

Как и в перле, в tcl нету различных типов данных, есть два типа переменных — скаляры и массивы. Скалярная переменная может быть либо числом, либо текстом в разное время. Числа (действительные и целые) могут быть даны во всех форматах ANSI C (10, 012, 0xa etc), чтобы создать переменную, используется команда set, например set loginname «anonymous», а обращение к переменной осуществляется в виде $имя_переменной, например send «$loginname\r». Как видно выше, иногда именам переменных предшествует знак $, иногда нет. Так вот, когда он есть, значение переменной замещается до того, как выполняется команда, то есть при изменении $ ставится, а при получении значения — нет. Уничтожается переменная командой unset. Для обработки числовых значений tcl предоставляет две команды — incr и expr. Команда incr добавляет целое число к переменной, ее синтаксис таков: incr <имя_переменной> <целое>. (очень полезно при переборе чего-нибудь типа ip-адресов или ICQ UIN’ов. Для более сложных математических операций существует команда expr, которая работает со всеми стандартными операторами стандарта ANSI C. Вот пример ее использования:

set c [expr $a / $b].

Кроме того, она понимает множество всяких функций типа тригонометрических, но нахрен они нашему скрипту я не знаю. При работе со строками используются команды: append, которая сцепляет строки и переменные — append <строка1> <строка2> … <строка n>.
Вот как например с помощью ее составить ip адрес из октетов при переборе ip адресов:

set a 127; set b 0; set c 0; set d 1;
append a «.» $b «.» $c «.» $d , и теперь в $a у нас 127.0.0.1

Кроме того, есть команда string, позволяющая осуществлять более сложные действия, но за этим — в мануал. Сначала я думал, что массивы не нужны в простых скриптах, потом, в процессе написания одного скрипта (см. пример) все-таки стал применять их, и поэтому упомяну и массивы. Кратко о массивах: обращение к элементу массива производится в форме имя_массива(индекс_элемента), например $ipaddress(0). Массивы в tcl ассоциативны, то есть можно использовать нечисловые индексы, скажем $ipaddress (first_octet). Инициализируется массив так же, по элементам, как и обычная переменная: set ipaddress(0) 127;set ipaddress(1) 0 и так далее — создаем столько элементов, сколько нужно.

5. Условные операторы.

Ну тут у нас все как у людей:

  if (условие) {
        действие
              }
  elseif (условие) {
         действие
                   }
  else {
    действие
  }

(соответствующие части могут быть опущены, как и во всех языках с подобным оператором) или:

  switch (ключ) {
  значение1 {действие1}
  значение2 {действие2}
  значениеN {действиеN}

По-моему, обьяснять здесь нечего.

6. Циклы.

Тут тоже все довольно понятно — стандартные циклы, стандартные команды управления (break и continue).
Итак:
while {условие} {команды} — пока условие истинно, цикл выполняется. Без комментариев.
foreach <переменная> {элементы} {команды} — это аналогия цикла `for` в UNIX shell, то есть <переменная>-это имя переменной, которой будут по очереди присваиваться значение каждого элемента из соответствующего множества.
Ну и наконец for — любимый всеми сишный цикл, только с другим синтаксисом:
for {инициализация} {условие} {приращение} {тело}

Вот тут можно привести первый пример использования всего вышеописанного (как раз с циклом for. Как известно, давным давно известен способ получения 6-значной аси через давно закрытые почтовые ящики: например, на хотмейле ящик грохается вроде бы через полгода, и потом можно снова зарегистрировать юзера с этим именем и получить аськин пароль. Остается найти только таких юзеров, у которых ящик на hotmail. Напишем скрипт составления базы данных юзеров ICQ с шестизначными UIN’ами:

# Пример не является готовым кодом, а лишь показывает смысл.

spawn /usr/local/bin/micq; #запускаем асин клиент
 ... #тут коннектимся и ожидаем асиного приглашения
 for {set uin 100001} {$uin

7. Работа с файлами.

Работа с файлами в expect очень напоминает аналогичные действия в C. Вот например открытие файла:

set f [ open /etc/shadow r ]

где f-файловая переменная
/etc/shadow — имя файла (а что же еще?)
r — режим открытия. Режимы такие же, как и в си: r, r+, w, w+, a, a+
Вместо имени файла можно указать для открытия процесса, выходные данные которого будут доступны через файловую переменную:

set f [ open { |ls } r ]

После работы с файлами его надо (желательно?) закрыть командой close:

close $f

Основные действия с файлами для скриптов — это естественно, построчное чтение файла (перебор по словарю, скажем) и запись в файл (ведение лога). Итак, для обработки всех строк файла используем цикл while:

while { [ gets $f $string ] >=0 }

Таким образом, реализуем перебор по словарю:

 ... # Запуск того, в чем будем перебирать.
 set flogin [open logins r]; #открываем файл logins, в котором лежат логины
                             #для перебора
 while { [ gets $flogin login ] >=0 }
 {
    set fpassword [open passwords r];             # То же самое для паролей.
    while { [ gets $fpassword password ] >=0 }
     {

    .... # Тут у нас доступны логин и пароль в переменных $login и $password.
         # Посылаем  их в нужный  процесс и обрабатываем ответ таким образом
         # каждой строке из файла logins сопоставляются по очереди все строки
         # из passwords.

            }
        close $fpassword; # Закрываем файл, чтобы в следующем проходе открыть
                          # снова.
 }; # Все кончилось.

Однако на самом деле например для того же телнета не все так просто, как кажется — процесс периодически помирает (после N неудачных попыток логина), и поэтому надо отслеживать его выполнение и перезапускать. После перезапуска процесса процесса могут возникнуть проблемы с файлами, открытыми после предыдущего spawn’а, так что поэтому приходится довольно хитро с этим изворачиваться, но это уже тонкости алгоритмизации, которые решать вам самим. Ближе к концу статьи будет приведен сляпанный недавно на коленке пример, глючный, но рабочий — как это накодил я сам, когда понадобилось. Сразу говорю — my coding
sucks. Второе важное дело — ведение лога. Тут все тоже не просто, а очень просто: открываем файл на запись, пишем в него командой puts, где на первом месте — файловая переменная:

puts $log «this is a line for logfile»;

И потом закрываем файл.

8. Прикладное программирование =).

В этой статье я описал наиболее часто используемые возможности языка expect, и как следствие немножко tcl. На самом деле, средства этих языков намного мощнее, и если кто-то заинтересовался ими — тому прямая дорога к man’ам и книгам. В принципе, из приведенных кусков примеров уже можно собрать рабочие скрипты, но надо прикинуть, зачем это будет нужно вам самим. В юниксах почти на каждый интернет-сервис есть консольный интерактивный клиент, а значит его можно автоматизировать под вашу задачу. А если нет готового клиента — есть netcat, с которым можно так же пообщаться.

9. Expect everywhere.

Библиотека libexpect позволяет использовать expect прямо из сишного кода. Я не занимался таким подключением, и вообще не знаю, надо ли оно кому-нибудь. Тех, кто заинтересовался, отсылаю к man 3 libexpect. В природе существует и expectk — интерпретатор, обьединяющий в себе expect и tk-shell wish. Вообще-то этот wish используется для создания интерфейса X Window, так что похоже expectовому сценарию можно сделать X-морду, но зачем? Однако tk — это уже совсем другая история…
Кроме того, со всем этим добром обычно распространяется тулза autoexpect, которая сама создает скрипт. Метод ее действия таков: запускаем под ее руководством нужный процесс, например:

autoexpect -f имя_файла_скрипта telnet 127.0.0.1

И делаем нужные нам действия. Когда процесс умрет, наши действия будут записаны на языке expect в скрипте. Нужна она по-моему только для обучения, и то не всегда.

10. А вот и рабочий пример…

Задача была такая — есть база логинов одного провайдера. Логины — в одном файле, соответствующие им пассворды — в другом. У провайдера открыт telnet, у которого разные реакции на рабочий логин и нерабочий. Надо отфильтровать рабочие аккаунты от нерабочих для последующего заюзывания. Как я уже говорил, скрипт писался на коленке. Обработка падучести телнета тут сделана не очень, и поэтому иногда остаются левые процессы-зомби. В принципе, если постоянно не висеть на шелле, то можно вставить exec /path/to/killall telnet, или придумать самим что-нибудь более оригинальное.

#!/usr/bin/expect

 set flogin [open slogins r]
 set fpass [open spass r]
 set flog [open ex_log w]
 set exitstat 0

 while 1 {
     for {set i 1} {$i0 } { break }
 }

На примере этого примера можно убедиться, что хоть язык-то expect и простой, но иногда приходится потрахаться c вобщем-то вполне очевидными вещами. [кстати к вопросу о потрахаться - первоначально создатели хотели обозвать свой язык `sex` (от Send-EXpect), но потом отказались от такой задумки ))))))))))].



©2013 Журнал Хакера Entries (RSS) and Comments (RSS)