Управление потоками


Основы Linux

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

В данном модуле Вы познакомитесь с перенаправлением ввода и вывода. В каждой программе (процессе) в Linux изначально есть три стандартных канала обмена данными:

  • Стандартный ввод — канал, через который процесс получает данные. Например: Ваша оболочка использует стандартный ввод, чтобы читать команды, которые Вы набираете.
  • Стандартный вывод — канал, через который процессы выдают обычные данные. Например: флаг, который печатается Вам в предыдущих заданиях, или результат работы утилит вроде ls.
  • Стандартный вывод ошибок — канал, через который процессы выводят сведения об ошибках. Например: если Вы опечатаетесь в имени команды, оболочка выведет сообщение о том, что такой команды не существует, именно через стандартный вывод ошибок.

Поскольку эти три канала настолько часто используются в Linux, для них закрепились короткие обозначения: stdin (cтандартный ввод), stdout (cтандартный вывод), stderr (стандартный вывод ошибок).

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

ПРИМЕЧАНИЕ:

Отличное наглядное руководство по перенаправлению ввода‑вывода в Linux.

Сначала давайте рассмотрим перенаправление стандартного вывода (stdout) в файлы. Сделать это можно с помощью символа >, например так:

hacker@piping:~$ echo hi > asdf

В этом примере вывод команды echo hi (то есть строка hi) будет перенаправлен в файл asdf. Затем Вы можете воспользоваться, к примеру, программой cat, чтобы отобразить содержимое этого файла:

hacker@piping:~$ cat asdf
hi

Закрепим принцип на практике.


В данном задании Вам необходимо с помощью перенаправления вывода записать слово CTF (заглавными буквами) в файл с именем SCHOOL (также заглавными буквами). Файл SCHOOL нужно разместить в домашнем каталоге пользователя и запустить /challenge/run для проверки. Команда выдаст флаг при корректном выполнении всех условий.

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

Помимо перенаправления вывода команды echo, Вы можете перенаправлять вывод любой команды. В этом задании программа /challenge/run снова выдаст Вам флаг, но только в том случае, если Вы перенаправите её вывод в файл myflag. Соответственно, сам флаг окажется в файле myflag.

ПРИМЕЧАНИЕ:

Вы заметите, что /challenge/run по‑прежнему охотно печатает что‑то в Ваш терминал, даже когда Вы перенаправляете стандартный вывод. Это происходит потому, что свои инструкции и сообщения обратной связи программа отправляет через стандартный вывод ошибок, а вот флаг выводит только через стандартный вывод.

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

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

Вы можете перенаправлять вывод в режиме добавления (append) с помощью оператора >> вместо >, например:

hacker@piping:~$ echo ctf > outfile
hacker@piping:~$ echo school >> outfile
hacker@piping:~$ cat outfile
ctf
school
hacker@piping:$

Для практики запустите /challenge/run с перенаправлением вывода в режиме добавления в файл /home/hacker/the-flag.

Во время тренировки первая половина флага будет записана в файл, а вторая половина будет отправлена в stdout, если stdout перенаправлен в этот файл. Если Вы правильно перенаправите вывод в режиме добавления, вторая половина будет добавлена к первой; если же Вы выполните перенаправление в режиме усечения (>), вторая половина перезапишет первую, и Вы не получите флаг!

Попробуйте сделать это сейчас!

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

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

Файловый дескриптор (File Descriptor, FD) — это номер, который описывает канал ввода‑вывода в Linux. Вы уже ранее использовали их, даже если не задумывались об этом. Нам уже знакомы три идентификатора дескриптора:

  • FD 0: стандартный ввод (Standard Input)
  • FD 1: стандартный вывод (Standard Output)
  • FD 2: стандартный вывод ошибок (Standard Error)

Когда Вы перенаправляете обмен данными процесса, это делается по номеру файлового дескриптора, хотя некоторые номера могут подразумеваться неявно. Например, оператор > без указания FD означает 1>, то есть перенаправляет FD 1 (стандартный вывод). Таким образом, следующие две команды эквивалентны:

hacker@piping:~$ echo hi > asdf
hacker@piping:~$ echo hi 1> asdf

Выполнить перенаправление ошибок с этого момента довольно просто. Если у Вас есть команда, которая может выдавать данные через стандартный поток ошибок (например, /challenge/run), Вы можете выполнить:

hacker@piping:~$ /challenge/run 2> errors.log

Эта команда перенаправит стандартный вывод ошибок (FD 2) в файл errors.log. Кроме того, можно перенаправлять несколько файловых дескрипторов одновременно. Например:

hacker@piping:~$ some_command > output.log 2> errors.log

Эта команда перенаправит стандартный вывод в файл output.log, а ошибки — в errors.log.


Давайте применим знания на практике. В задании Вам нужно перенаправить вывод команды /challenge/run, как и раньше, в файл myflag, а «ошибки» (в нашем случае — инструкции) — в файл instructions. Вы заметите, что в терминал ничего выводиться не будет, потому что Вы перенаправите всё. Инструкции и обратную связь можно будет найти в файле instructions, а флаг — в myflag, когда Вы корректно выполните упражнение.

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

Точно так же, как Вы можете перенаправлять вывод программ, Вы можете перенаправлять и ввод программ. Это делается с помощью оператора <, например:

hacker@piping:~$ echo CTF > message
hacker@piping:~$ cat message
CTF
hacker@piping:~$ rev < message
CTF

С помощью перенаправления ввода можно делать интересные вещи с множеством различных программ.


В этом задани мы попрактикуемся с программой /challenge/run, для которой Вам потребуется перенаправить в неё файл CTF в качестве ввода и сделать так, чтобы файл CTF содержал значение SCHOOL. Чтобы записать это значение в файл CTF, вспомните предыдущее задание о перенаправлении вывода с помощью команды echo.

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

Вы уже умеете запускать команды, перенаправлять их вывод (например, с помощью >) и искать по получившемуся файлу (например, с помощью grep).

Давайте объединим всё знания вместе.

Вам предстоит:

  1. Перенаправить вывод команды /challenge/run в файл /tmp/data.txt.
  2. В результате в файле /tmp/data.txt окажется сто тысяч строк текста, одна из которых будет содержать флаг.
  3. Найти флаг в этом файле с помощью grep!

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

В этом уроке мы оптимизируем процесс «убрав посредника» и не будем сохранять результаты в файл, как Вы делали на предыдущем уровне. Сделать это можно с помощью оператора | (pipe). Стандартный вывод команды, стоящей слева от конвейера, будет подключён (передан по конвейеру) к стандартному вводу команды, стоящей справа от него.

Например:

hacker@piping:~$ echo no-no | grep yes
hacker@piping:~$ echo yes-yes | grep yes
yes-yes
hacker@piping:~$ echo yes-yes | grep no
hacker@piping:~$ echo no-no | grep no
no-no

Теперь попробуйте сами!

Команда /challenge/run выведет сто тысяч строк текста, среди которых будет и флаг. Используйте grep, чтобы найти флаг!

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

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

Оператор > перенаправляет указанный файловый дескриптор в файл, и Вы уже использовали 2>, чтобы перенаправить дескриптор 2, то есть стандартный поток ошибок. Оператор | перенаправляет только стандартный вывод в другую программу, и варианта 2| не существует. Он может перенаправлять только стандартный вывод (файловый дескриптор FD 1).

В оболочке bash есть оператор >&, который перенаправляет один файловый дескриптор в другой файловый дескриптор. Это означает, что можно организовать двухшаговый процесс для использования grep по потоку ошибок:

  1. Cначала перенаправить стандартный поток ошибок в стандартный поток вывода (2>&1).
  2. Затем, как обычно, пропустить теперь уже объединённые stderr и stdout через конвейер (|).

Закрепите этот подход на практике. Как и на прошлом уровне, команда /challenge/run «завалит» Вас выводом, но на этот раз — в стандартный поток ошибок stderr. Используйте grep, чтобы найти флаг!

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

Команда grep имеет очень полезный параметр: -v (invert match, инверсия совпадения). Обычно grep показывает строки, которые СОДЕРЖАТ указанный шаблон, а grep -v показывает строки, которые этому шаблону НЕ соответствуют:

hacker@piping:~$ cat data.txt
hello hackers!
hello world!
hacker@piping:~$ cat data.txt | grep -v world
hello hackers!
hacker@piping:~$

Иногда единственный способ получить только нужные данные — это отфильтровать данные, которые не нужны.


В этом задании /challenge/run будет выводить флаг в стандартный вывод, но вместе с ним — более 1000 «ложных» флагов, в которых где‑то внутри содержится слово DECOY, вперемешку с настоящим флагом. Вам нужно отфильтровать ложные флаги, сохранив при этом настоящий.

Используйте grep -v, чтобы исключить все строки, содержащие DECOY, и таким образом открыть настоящий флаг!

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

grep — не единственный способ сопоставлять данные с шаблоном. Иногда реальные данные и «мусорные» данные находятся в одной и той же строке, и нужно удалить именно «мусор». Для этого можно использовать sed. sed предоставляет простой способ заменить шаблон в тексте другим словом.

Синтаксис для поиска и замены достаточно прост:

sed "s/oldword/newword/g"
  • s/ — операция замены (substitute)
  • oldword — слово (или шаблон), которое нужно заменить
  • newword — замена для oldword
  • /g — флаг глобальности (global), указывает на замену всех вхождений шаблона в строке

Пример использования:

hacker@piping:~$ cat data.txt
hello world!
hacker@piping:~$ cat data.txt | sed "s/world/hackers/g"
hello hackers!
hacker@piping:~$

В этом задании /challenge/run будет выводить флаг, но между каждым символом флага будет строка "FAKEFLAG". Ваша задача — удалить «мусорные» данные из флага, не повредив нужные. Удачи!

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

Когда Вы передаёте данные по конвейеру | (pipe) от одной команды к другой, Вы, разумеется, больше не видите их на экране. Это не всегда удобно: например, иногда нужно видеть данные «по пути» между командами, чтобы отладить неожиданный результат и понять почему вторая команда не сработала.

У этой проблемы есть решение. Команда tee, названная в честь тройника из водопроводных труб. Команда дублирует поток данных, проходящий по конвейеру, в любое количество файлов, указанных в командной строке. Например:

hacker@piping:~$ echo Привет | tee CTF SCHOOL
Привет
hacker@piping:~$ cat CTF
Привет
hacker@piping:~$ cat SCHOOL
Привет
hacker@piping:~$

Как видно, передав два имени файлов в tee, мы получили три копии входных данных: одну в стандартный вывод stdout, одну в файл CTF и одну в файл SCHOOL. Легко представить, как это может помочь при отладке «странного» поведения конвейера:

hacker@piping:~$ command_1 | command_2
command_2 завершилась неудачно!
hacker@piping:~$ command_1 | tee cmd1_output | command_2
command_2 завершилась неудачно!
hacker@piping:~$ cat cmd1_output
Ошибка command_1: требуется параметр --succeed!
hacker@piping:~$ command_1 --succeed | command_2
Успех!

Попробуйте сами. Процесс /challenge/ctf должен быть «пропущен по конвейеру» (piped) в /challenge/school, но Вам нужно перехватить данные по пути, чтобы увидеть, что именно ctf ожидает от Вас!

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

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

hacker@piping:~$ command1 > file1
hacker@piping:~$ command2 > file2
hacker@piping:~$ diff file1 file2

Но есть более элегантный способ. Linux следует философии, согласно которой "всё — файл". То есть система стремится предоставить файловый интерфейс ко многим ресурсам, включая ввод и вывод работающих программ. Оболочка также следует этой философии, позволяя подключать любую утилиту, принимающую файлы в качестве аргументов, к выводу программ — как Вы уже узнали в предыдущих уровнях.

Интересно, что можно пойти ещё дальше и подключать ввод и вывод программ к аргументам других команд. Это делается с помощью подстановки процессов (Process Substitution). Для чтения из команды (подстановка процесса на ввод) используется форма <(command).

Когда Вы пишете <(command), bash запускает эту команду и подключает её вывод к времальному «файлу», который создаётся оболочкой. Это, конечно, не настоящий файл, а так называемый именованный канал (named pipe), который имеет файловое имя:

hacker@piping:~$ echo <(echo hi)
/dev/fd/63
hacker@piping:~$

Откуда взялся /dev/fd/63?

bash подставил вместо <(echo hi) путь к именованному каналу, связанному с выводом этой команды. Пока команда выполняется, чтение из этого «файла» фактически считывает данные из стандартного вывода команды. Обычно это используется с командами, которые ожидают имена входных файлов:

hacker@piping:~$ cat <(echo hi)
hi
hacker@piping:~$

Разумеется, можно указывать несколько таких подстановок:

hacker@piping:~$ echo <(echo ctf) <(echo school)
/dev/fd/63 /dev/fd/64
hacker@piping:~$ cat <(echo ctf) <(echo school)
ctf
school
hacker@piping:~$

Ваше задание на данный урок:

Вспомните то, что Вы узнали в задании про diff из раздела "Базовые команды". Напомним, там Вы сравнивали два файла. Теперь Вам предстоит сравнить вывод двух программ: /challenge/decoys, которая печатает набор ложных флагов, и /challenge/decoys_and_flag, которая выводит те же ложные флаги плюс настоящий флаг.

Используйте подстановку процессов совместно с diff, чтобы сравнить вывод этих двух программ и найти свой флаг!

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

Теперь Вы знаете, что подстановка процессов (process substitution) позволяет сделать так, чтобы вывод команды выглядел как файл для чтения — с помощью конструкции <(command). Однако подстановку процессов можно использовать и для записи в команды. Вы уже видели, как с помощью tee можно продублировать данные в два файла:

hacker@piping:~$ echo HACK | tee THE > PLANET
hacker@piping:~$ cat THE
HACK
hacker@piping:~$ cat PLANET
HACK
hacker@piping:~$

И как использовать tee для дублирования данных в файл и в команду:

hacker@piping:~$ echo HACK | tee THE | cat
HACK
hacker@piping:~$ cat THE
HACK
hacker@piping:~$

Но как быть, если нужно продублировать вывод сразу в две команды? Согласно справке tee (man‑странице), эта утилита предназначена для записи в файлы и стандартный вывод:

TEE(1)                           User Commands                          TEE(1)

НАЗВАНИЕ
       tee - считывает данные из стандартного устройства ввода и записывает
             их на стандартное устройство вывода или в файл

Но подождите! Вы только что узнали, что bash может «превращать» команды в каналы, похожие на файлы, с помощью подстановки процессов. Для записи в команду (output process substitution) используется форма >(command).

Если указать аргумент >(rev), bash запустит команду rev (эта команда читает данные из стандартного ввода, переворачивает их и пишет в стандартный вывод), а её стандартный ввод подключит к временному именованному каналу. Когда другие команды записывают данные в этот «файл», они фактически попадают в стандартный ввод команды:

hacker@piping:~$ echo HACK | rev
KCAH
hacker@piping:~$ echo HACK | tee >(rev)
HACK
KCAH

В приведённом выше примере происходит следующее:

  1. bash запускает команду rev, подключая именованный канал (предположительно /dev/fd/63) к стандартному вводу rev.
  2. bash запускает команду tee, подключая к её стандартному вводу канал и подставляя вместо первого аргумента >(rev) путь /dev/fd/63. Команда tee никогда не видит аргумент >(rev) — оболочка подставляет вместо него имя файла до запуска tee.
  3. bash выполняет встроенную команду echo, записывая HACK в стандартный ввод tee.
  4. tee считывает HACK, выводит его в стандартный вывод и записывает его в /dev/fd/63 (который подключён к стандартному вводу rev).
  5. rev считывает HACK из стандартного ввода, переворачивает его и выводит KCAH в стандартный вывод.

Теперь Ваша очередь.

В этом задании Вам даны команды /challenge/hack, /challenge/the и /challenge/planet. Запустите /challenge/hack и продублируйте её вывод в качестве входа сразу для двух команд: /challenge/the и /challenge/planet. При необходимости перечитайте прошлые задания данного модуля, чтобы освежить в памяти, как комбинировать tee и подстановку процессов.


ПРИМЕЧАНИЕ:

Внимательный читатель заметит, что следующее варианты эквивалентны:

hacker@piping:~$ echo hi | rev
ih
hacker@piping:~$ echo hi > >(rev)
ih
hacker@piping:~$

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

hacker@piping:~$ echo hi | rev | rev
hi
hacker@piping:~$ echo hi > >(rev | rev)
hi
hacker@piping:~$

Вариант echo hi > >(rev | rev) выглядит откровенно громоздко. Вывод: хотя подстановка процессов — мощный инструмент в Вашем арсенале, это очень специализированный инструмент, и использовать его стоит только там, где он действительно уместен, а не во всех случаях подряд.

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

Объединим все полученные знания. Вам предстоит разделить потоки и перенаправить их по различным конвейерами: stdout в одну программу, а stderr — в другую.

Сложность здесь в том, что оператор | связывает стандартный вывод (stdout) команды слева со стандартным вводом (stdin) команды справа. Ранее Вы использовали конструкцию 2>&1, чтобы перенаправить поток ошибок в стандартный вывод и затем передать его по конвейеру, но при этом stderr и stdout смешиваются. Как оставить их раздельными?

Вам нужно совместить знания о >(), 2> и |. Как именно это сделать — задача для Вас, подсказок не будет.

В этом задании у Вас есть:

  • /challenge/hack: эта команда выдаёт данные и в stdout, и в stderr
  • /challenge/the: сюда необходимо перенаправить stderr программы hack
  • /challenge/planet: сюда необходимо перенаправить stdout программы hack

Добудьте флаг!

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college

Вы уже познакомились с конвейерами (pipes), использующими |, и видели, что подстановка процесса создаёт временные именованные каналы (такие как /dev/fd/63). Вы также можете создавать собственные постоянные именованные каналы, которые остаются в файловой системе! Они называются FIFO (First (byte) In, First (byte) Out — «первый байт вошёл, первый байт вышел»).

FIFO создаются с помощью команды mkfifo:

hacker@piping:~$ mkfifo my_pipe
hacker@piping:~$ ls -l my_pipe
prw-r--r-- 1 hacker hacker 0 Jan 1 12:00 my_pipe
hacker@piping:~$ ls -l some_file
-rw-r--r-- 1 hacker hacker 0 Jan 1 12:00 some_file
hacker@piping:~$

Обратите внимание на p в начале строки прав доступа — это означает, что перед Вами канал (pipe). Это принципиально отличается от - в начале строки у обычных файлов, как у some_file в примере выше.

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

  • Вы сами контролируете, где создаются FIFO
  • Они существуют до тех пор, пока Вы их не удалите
  • Любой процесс может записывать в них данные по пути (например, echo hi > my_pipe)
  • Вы можете видеть их через ls и изучать их как обычные файлы

Важная особенность FIFO состоит в том, что они «блокируют» любые операции до тех пор, пока обе стороны канала — и сторона чтения, и сторона записи — не будут готовы к работе.

Рассмотрим следующий пример:

hacker@piping:~$ mkfifo myfifo
hacker@piping:~$ echo ctf > myfifo

Чтобы выполнить echo ctf > myfifo, bash откроет файл myfifo в режиме записи. Однако эта операция будет «зависать» до тех пор, пока другой процесс также не откроет этот файл в режиме чтения, тем самым завершив соединение канала.

Это можно сделать в другом терминале:

hacker@piping:~$ cat myfifo
ctf
hacker@piping:~$

Что здесь произошло?

Когда Вы запустили cat myfifo, обе стороны канала оказались готовы, и он разблокировался. Это позволило выполниться команде echo ctf > myfifo, которая отправила строку ctf в канал, откуда её прочитал cat.

Разумеется, нечто похожее можно сделать и с обычными файлами: Вы уже знаете, как записывать в них данные с помощью echo и читать с помощью cat. Зачем же тогда использовать FIFO?

Вот ключевые отличия:

  1. Отсутствие хранения на диске: FIFO передают данные напрямую между процессами в оперативной памяти — на диск ничего не записывается.
  2. Эфемерность данных: Как только данные прочитаны из FIFO, они исчезают — в отличие от файлов, где данные сохраняются.
  3. Автоматическая синхронизация: Процесс-писатель блокируется, пока не будет готов читатель, и наоборот. Это очень удобно! Благодаря этому свойству неважно, какая команда запущена первой — cat myfifo или echo pwn > myfifo: каждая из них просто будет ожидать другую. При работе с обычными файлами Вам пришлось бы самостоятельно следить за порядком запуска: сначала писатель, затем читатель.
  4. Сложные потоки данных: FIFO незаменимы при организации сложных потоков данных, гибком объединении и разделении данных. Например, FIFO поддерживают одновременную работу нескольких читателей и писателей.

Данное задание является упражнением по работе с FIFO. Вам необходимо создать файл /tmp/flag_fifo и перенаправить в него стандартный вывод (stdout) команды /challenge/run. Если Вы всё сделаете правильно, /challenge/run запишет флаг в FIFO!

Удачи!

ПОДСКАЗКА:
Блокирующее поведение FIFO усложняет решение этого задания в одном терминале. Воспользуйтесь символом & после команды — это позволит Вам запустить её в фоновом режиме, не блокируя оболочку bash.

Connect with SSH

Link your SSH key, then connect with: ssh hacker@dojo.pwn.college