Praticando Piping e Redirecionamento


Cyberguard: Linux Luminarium.

Você deve ter percebido que alguns comandos exibem dados no seu terminal quando executados. Até agora, isso serviu para imprimir diversas flags na sua tela, mas, como tantas coisas no Linux, a engenharia por trás disso vai muito mais fundo. Os mecanismos de manipulação de entrada e saída na linha de comando são os grandes responsáveis pelo verdadeiro poder do terminal.

Este módulo ensinará você sobre redirecionamento de entrada e saída (I/O redirection). Em termos simples, todo processo no Linux possui três canais padrão e iniciais de comunicação:

  • Entrada Padrão (Standard Input): canal pelo qual o processo recebe dados de entrada. Por exemplo, a sua shell utiliza a Entrada Padrão para ler os comandos digitados por você.
  • Saída Padrão (Standard Output): canal pelo qual os processos exibem dados normais, como as flags exibidas nos desafios anteriores ou a lista de arquivos retornada por comandos como ls.
  • Saída de Erro Padrão (Standard Error): canal pelo qual os processos enviam detalhes de erros e mensagens de diagnóstico. Por exemplo, se você digitar incorretamente um comando, a shell avisará pela saída de erro que o comando não existe.

Por serem utilizados a todo momento no Linux, esses três canais são conhecidos por suas siglas abreviadas: stdin, stdout e stderr. Neste módulo, você aprenderá a redirecionar, encadear via pipes, duplicar e manipular esses fluxos como um verdadeiro especialista. Boa sorte!


Primeiramente, vamos explorar o redirecionamento da saída padrão (stdout) para arquivos. Você pode realizar isso utilizando o caractere >, desta forma:

hacker@dojo:~$ echo hi > asdf

Isso redirecionará a saída de echo hi (que seria hi) para o arquivo asdf. Você pode então usar um comando como cat para inspecionar o conteúdo desse arquivo:

hacker@dojo:~$ cat asdf
hi

Neste desafio, você deve usar esse redirecionamento de saída para escrever a palavra PWN (em letras maiúsculas) em um arquivo chamado COLLEGE (também em letras maiúsculas).

Além de redirecionar a saída de echo, você pode, evidentemente, redirecionar a saída de qualquer comando. Neste nível, /challenge/run entregará a flag novamente, mas apenas se você redirecionar a sua saída para o arquivo myflag. Sua flag será gravada diretamente no arquivo myflag!

Você notará que /challenge/run ainda imprimirá mensagens no terminal, mesmo após você ter redirecionado o stdout. Isso acontece porque o programa envia instruções e mensagens de feedback pela saída de erro padrão (stderr), imprimindo apenas a flag pela saída padrão (stdout)!


AVISO: Se algum teste anterior tiver deixado um arquivo, diretório ou symlink chamado myflag no seu diretório atual, remova-o antes para que a shell possa criar um arquivo gravável normal.

Um caso de uso muito comum do redirecionamento de saída é armazenar o resultado de comandos para análise posterior. Frequentemente, você deseja fazer isso de forma agregada: executar vários comandos sucessivos, acumular suas saídas e depois pesquisar nelas com grep. Nesse cenário, você precisa que a nova saída seja sempre adicionada ao final do mesmo arquivo, enquanto o operador > cria um arquivo novo a cada execução, sobrescrevendo o conteúdo anterior.

Você pode redirecionar a saída em modo de anexo (append mode) utilizando >> em vez de >, assim:

hacker@dojo:~$ echo pwn > outfile
hacker@dojo:~$ echo college >> outfile
hacker@dojo:~$ cat outfile
pwn
college
hacker@dojo:$

Para praticar, execute /challenge/run redirecionando a saída em modo de anexo (append mode) para o arquivo /home/hacker/the-flag. O desafio gravará a primeira metade da flag no arquivo e tentará enviar a segunda metade para o stdout se o stdout estiver sendo redirecionado para o arquivo. Se você redirecionar corretamente com >>, a segunda metade será anexada à primeira. Porém, se usar o modo de truncamento (>), a segunda metade sobrescreverá a primeira e a flag ficará incompleta!

Mãos à obra!

Assim como a saída padrão, você também pode redirecionar o canal de erros dos comandos. Aqui, aprenderemos sobre os Descritores de Arquivo (File Descriptors ou FDs). Um Descritor de Arquivo é um número inteiro que identifica um canal de comunicação no Linux. Você já os tem usado, mesmo sem perceber. Já conhecemos os três principais:

  • FD 0: Entrada Padrão (Standard Input - stdin)
  • FD 1: Saída Padrão (Standard Output - stdout)
  • FD 2: Saída de Erro Padrão (Standard Error - stderr)

Ao redirecionar a comunicação de um processo, você faz isso indicando o número do FD, embora alguns sejam implícitos. Por exemplo, o operador > sem número implícita 1>, redirecionando o FD 1 (stdout). Portanto, os dois comandos a seguir são equivalentes:

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

Redirecionar erros torna-se muito simples a partir deste conceito. Se houver um comando que produza dados via erro padrão (como /challenge/run), você pode fazer:

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

Isso redirecionará o canal de erro padrão (FD 2) para o arquivo errors.log. Além disso, você pode redirecionar múltiplos descritores de arquivo simultaneamente em um mesmo comando! Por exemplo:

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

Esse comando redirecionará a saída padrão para output.log e os erros para errors.log.

Vamos colocar isso em prática! Neste desafio, você precisará redirecionar a saída de /challenge/run para myflag, e os "erros" (no nosso caso, as instruções e orientações) para instructions. Você perceberá que nada aparecerá no terminal, pois você terá redirecionado absolutamente tudo! Ao concluir a operação com sucesso, você encontrará as instruções em instructions e a sua flag em myflag!


AVISO: Se algum teste anterior tiver deixado um arquivo, diretório ou symlink chamado myflag ou instructions no seu diretório atual, remova-o antes para que a shell possa criar arquivos graváveis normais.

Assim como você pode redirecionar a saída de programas, também pode redirecionar a entrada para dentro dos programas! Isso é feito com o operador <, como no exemplo:

hacker@dojo:~$ echo yo > message
hacker@dojo:~$ cat message
yo
hacker@dojo:~$ rev < message
oy

O redirecionamento de entrada abre inúmeras possibilidades interessantes com diversos programas! Neste nível, praticaremos com /challenge/run, que exigirá que você redirecione o arquivo PWN para sua entrada padrão, e que esse arquivo PWN contenha exatamente o valor COLLEGE! Para gravar esse valor no arquivo PWN, lembre-se do que aprendeu nos desafios anteriores sobre redirecionamento de saída com echo!

Você já sabe executar comandos, redirecionar a saída deles (por exemplo, com >) e pesquisar no arquivo resultante (por exemplo, com grep). Vamos reunir tudo isso!

Como preparação para níveis mais complexos, seu objetivo é:

  1. Redirecionar a saída de /challenge/run para /tmp/data.txt.
  2. Isso gerará cerca de cem mil linhas de texto em /tmp/data.txt, sendo uma delas a flag.
  3. Usar o grep nesse arquivo para encontrar a flag!

Acontece que você pode "cortar o intermediário" e eliminar a necessidade de salvar resultados em um arquivo em disco, como fizemos no nível anterior. Você faz isso utilizando o operador | (pipe ou canalização). A saída padrão (stdout) do comando à esquerda do pipe é conectada diretamente à entrada padrão (stdin) do comando à direita. Por exemplo:

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

Agora experimente por conta própria! O programa /challenge/run gerará cem mil linhas de texto, incluindo a flag. Use o grep via pipe para capturar a flag diretamente na saída em tempo real!

Você já sabe redirecionar erros para um arquivo e sabe encadear saídas para outro programa usando pipes, como com grep. Mas e se você quiser usar o grep diretamente nos fluxos de erro?

O operador > redireciona um descritor de arquivo para um arquivo em disco, e você já usou 2> para redirecionar o FD 2 (saída de erro padrão). Já o operador | redireciona apenas a saída padrão para outro programa, e não existe uma forma nativa como 2|! Ele redireciona exclusivamente o stdout (descritor de arquivo 1).

Felizmente, onde há uma shell, há uma solução!

A shell possui o operador >&, que redireciona um descritor de arquivo para outro descritor de arquivo. Isso nos permite um processo em duas etapas para filtrar erros com grep: primeiro, redirecionamos a saída de erro padrão para a saída padrão (2>&1) e depois canalizamos com pipe (|) o fluxo combinado resultante diretamente para o grep!

Experimente agora! Assim como no nível anterior, este desafio gerará um enorme volume de dados, mas desta vez na saída de erro padrão (stderr). Utilize o grep filtrando esse fluxo para encontrar a flag!

O comando grep possui uma opção extremamente útil: -v (invert match ou inversão de correspondência). Enquanto o grep normal exibe apenas as linhas que CORRESPONDEM ao padrão, o grep -v exibe as linhas que NÃO correspondem ao padrão:

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

Às vezes, a única maneira de isolar exatamente o dado desejado é filtrando e descartando tudo aquilo que você não quer. Neste desafio, /challenge/run imprimirá a flag na saída padrão (stdout), mas junto a ela virão mais de 1000 flags falsas (contendo a palavra DECOY em alguma parte do texto) misturadas com a flag verdadeira. Você precisará filtrar e eliminar todos os despistes (decoys) para revelar a flag real!

Use grep -v para descartar todas as linhas que contenham "DECOY" e obter a flag verdadeira!

O grep não é a única forma de manipular padrões. Às vezes, o dado real e o ruído inútil estão misturados dentro da mesma linha, e queremos remover apenas esse lixo textual. Para isso, temos o sed (stream editor). O sed oferece uma forma prática de substituir padrões em um fluxo de texto por outro termo. A sintaxe básica de busca e substituição é simples:

sed "s/oldword/newword/g"

s/ - substituição (substitute)
oldword - o termo a ser localizado e substituído
newword - o substituto para oldword (pode ser vazio para deletar)
/g - global (aplica a todas as ocorrências na linha)

Neste desafio, /challenge/run imprimirá a flag, mas entre cada caractere legítimo haverá a string "FAKEFLAG". Seu objetivo é filtrar e eliminar esse ruído da flag. Boa sorte!

Quando você canaliza dados de um comando para outro com pipe (|), você naturalmente deixa de vê-los na sua tela. Isso nem sempre é o desejado: por exemplo, você pode querer inspecionar os dados conforme eles trafegam entre os comandos para depurar comportamentos inesperados (como por exemplo: "por que o segundo comando falhou???").

Felizmente, há uma solução perfeita! O comando tee, cujo nome deriva da conexão em forma de "T" usada no encanamento hidráulico (plumbing), duplica os dados que fluem pelo pipe, enviando-os tanto para a saída padrão quanto para qualquer quantidade de arquivos informados na linha de comando. Por exemplo:

hacker@dojo:~$ echo hi | tee pwn college
hi
hacker@dojo:~$ cat pwn
hi
hacker@dojo:~$ cat college
hi
hacker@dojo:~$

Como visto acima, ao fornecer dois arquivos para o tee, obtivemos três cópias dos dados: uma na saída padrão (stdout), uma no arquivo pwn e outra no arquivo college. Imagine como você pode usar isso para depurar pipelines com problemas:

hacker@dojo:~$ command_1 | command_2
Command 2 failed!
hacker@dojo:~$ command_1 | tee cmd1_output | command_2
Command 2 failed!
hacker@dojo:~$ cat cmd1_output
Command 1 failed: must pass --succeed!
hacker@dojo:~$ command_1 --succeed | command_2
Commands succeeded!

Agora é a sua vez! Neste desafio, a saída de /challenge/pwn deve ser conectada via pipe à entrada de /challenge/college, mas você precisará interceptar os dados para entender o que /challenge/pwn exige de você! Use tee para duplicar a saída da sua primeira tentativa em um arquivo, inspecione as informações salvas e, em seguida, ajuste o seu comando com base no que descobriu.

Às vezes, você precisa comparar a saída de dois comandos em vez de dois arquivos pré-existentes. Sua primeira ideia poderia ser salvar a saída de cada um em um arquivo temporário:

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

Mas há uma maneira muito mais elegante! O Linux segue a consagrada filosofia de que "tudo é um arquivo". Ou seja, o sistema busca oferecer uma interface semelhante a arquivos para a maioria dos recursos, inclusive para a entrada e a saída de programas em execução! A shell reflete diretamente essa filosofia, permitindo, por exemplo, que utilitários que esperam caminhos de arquivos como argumentos recebam diretamente a saída de processos.

Podemos ir além e conectar a entrada e saída de programas aos argumentos dos comandos. Isso é feito usando a Substituição de Processos (Process Substitution). Para ler a saída de um comando como se fosse um arquivo de entrada, usamos <(comando). Ao escrever <(comando), o bash executa o comando e conecta sua saída a um arquivo temporário especial. Esse não é um arquivo comum gravado no disco; trata-se de um pipe nomeado (named pipe), que possui um caminho identificável no sistema:

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

De onde surgiu esse /dev/fd/63? O bash substituiu <(echo hi) pelo caminho do pipe nomeado conectado à saída do comando! Enquanto o comando estiver em execução, qualquer leitura desse arquivo lerá diretamente os dados da saída padrão do processo. Isso é frequentemente usado com comandos que aceitam arquivos como argumentos:

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

E, é claro, você pode utilizar essa substituição múltiplas vezes no mesmo comando:

hacker@dojo:~$ echo <(echo pwn) <(echo college)
/dev/fd/63 /dev/fd/64
hacker@dojo:~$ cat <(echo pwn) <(echo college)
pwn
college
hacker@dojo:~$

Agora, o seu desafio! Lembre-se do que aprendeu sobre o comando diff nos módulos anteriores. Naquela ocasião, você comparou dois arquivos físicos. Agora, você comparará a saída de dois programas em tempo real: /challenge/print_decoys, que imprime várias flags falsas (decoys), e /challenge/print_decoys_and_flag, que imprime os mesmos despistes acrescidos da flag verdadeira.

Use substituição de processo com diff para comparar as saídas desses dois programas diretamente e capturar a flag!

Você aprendeu que a substituição de processos permite tratar a saída de um comando como se fosse um arquivo de leitura com <(comando). Mas você também pode usar a substituição de processos para escrever dados na entrada de comandos!

Vimos que podemos duplicar dados para dois arquivos com tee:

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

E também vimos o tee duplicar dados para um arquivo e para um comando simultaneamente:

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

Mas e se quisermos duplicar o fluxo de dados para dois comandos diferentes? Como a manpage do tee explica, ele foi concebido para gravar em arquivos e na saída padrão:

TEE(1)                           User Commands                          TEE(1)

NAME
       tee - read from standard input and write to standard output and files

Mas espere! Você acabou de aprender que o bash pode fazer comandos se comportarem como arquivos usando a substituição de processos! Para escrever na entrada de um comando (substituição de processo para saída), utilizamos >(comando). Se você passar o argumento >(rev), o bash inicializará o comando rev (que lê dados do stdin, inverte a ordem dos caracteres e imprime no stdout), mas conectará a entrada dele a um arquivo de pipe nomeado temporário. Quando outros comandos escrevem nesse arquivo, os dados são enviados diretamente para a entrada padrão do comando:

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

No exemplo acima, a seguinte sequência de eventos ocorreu:

  1. O bash iniciou o comando rev, conectando um pipe nomeado (como /dev/fd/63) à entrada padrão do rev.
  2. O bash iniciou o comando tee, conectando um pipe ao seu stdin e substituindo o primeiro argumento de tee por /dev/fd/63. O tee sequer tomou conhecimento de >(rev); a shell realizou a substituição antes de disparar o tee.
  3. O bash usou o comando embutido echo para enviar HACK ao stdin do tee.
  4. O tee leu HACK, escreveu na saída padrão e em seguida gravou em /dev/fd/63 (que está conectado ao stdin do rev).
  5. O rev leu HACK do seu stdin, inverteu os caracteres e escreveu KCAH na saída padrão.

Agora é a sua vez! Neste desafio, temos /challenge/hack, /challenge/the e /challenge/planet. Execute o comando /challenge/hack e duplique a sua saída como entrada para ambos os comandos /challenge/the e /challenge/planet! Lembre-se de que o tee aceita múltiplos alvos de saída, e >(comando) é uma forma válida de alvo. Revise os desafios anteriores sobre tee e substituição de processos caso queira relembrar os detalhes.


Curiosidade:

O leitor atento perceberá que as seguintes linhas produzem o mesmo efeito:

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

Existem muitas formas de encadear fluxos de dados! Naturalmente, a segunda alternativa é muito menos legível e mais difícil de manter. Por exemplo:

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

Isso fica desnecessariamente confuso! A lição aqui é que, embora a Substituição de Processos seja uma ferramenta incrivelmente poderosa no seu cinto de utilidades, ela é um recurso bastante especializado; use-a com sabedoria!

Agora, vamos reunir todo o seu conhecimento. Você deve dominar a tarefa definitiva de piping: redirecionar o stdout para um programa e o stderr para outro programa simultaneamente.

O desafio aqui é que o operador | conecta apenas o stdout do comando à esquerda com o stdin do comando à direita. Você já usou 2>&1 para redirecionar stderr para stdout e enviar via pipe, mas isso mistura os dois fluxos em um só. Como mantê-los separados e direcionados a processos distintos?

Você precisará combinar seu conhecimento de >(), 2> e |. A forma exata de estruturar essa linha é um quebra-cabeça que deixamos com você.

Neste desafio, você tem:

  • /challenge/hack: este binário produz dados tanto no stdout quanto no stderr
  • /challenge/the: você deve redirecionar o stderr do hack para a entrada deste programa
  • /challenge/planet: você deve redirecionar o stdout do hack para a entrada deste programa

Vá em busca da flag!


DESAFIO BÔNUS: Para um nível extra de maestria, encontre uma solução que funcione sem utilizar o operador |.

Você aprendeu sobre pipes usando |, e viu que a substituição de processos cria pipes nomeados temporários (como /dev/fd/63). Você também pode criar seus próprios pipes nomeados persistentes, que permanecem no sistema de arquivos até serem removidos! Eles são chamados de FIFOs, sigla para First (byte) In, First (byte) Out (primeiro a entrar, primeiro a sair).

Você cria um FIFO usando o comando mkfifo:

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

Repare na letra p no início da string de permissões — isso indica que se trata de um canal de pipe! Isso difere claramente do - exibido no início de arquivos regulares, como some_file no exemplo acima.

Ao contrário dos pipes nomeados automáticos da substituição de processos:

  • Você tem controle sobre o local onde os FIFOs são criados
  • Eles persistem até que você os exclua explicitamente
  • Qualquer processo pode ler ou escrever neles pelo caminho do arquivo (por exemplo, echo hi > my_pipe)
  • Você pode visualizá-los com ls e inspecioná-los como arquivos comuns

Uma característica crucial dos FIFOs é que as operações neles realizam "bloqueio" (blocking) até que ambas as pontas (escritor e leitor) estejam conectadas e prontas. Por exemplo, veja a situação:

hacker@dojo:~$ mkfifo myfifo
hacker@dojo:~$ echo pwn > myfifo

Para atender a echo pwn > myfifo, o bash abrirá o arquivo myfifo em modo de escrita. Contudo, essa operação ficará travada (aguardando) até que outro comando também abra o arquivo em modo de leitura (completando o circuito do pipe). Isso pode ocorrer em outro terminal simultâneo:

hacker@dojo:~$ cat myfifo
pwn
hacker@dojo:~$

O que aconteceu aqui? Quando executamos cat myfifo, o pipe teve seus dois lados conectados e foi desbloqueado, permitindo que echo pwn > myfifo terminasse e transferisse o texto pwn através do pipe, sendo imediatamente lido pelo cat.

Naturalmente, arquivos comuns também conseguem trocar dados (você já usou echo para gravar e cat para ler). Por que então usar um FIFO? Veja as diferenças fundamentais:

  1. Sem armazenamento em disco: Os FIFOs transferem dados diretamente entre processos na memória RAM — nada é gravado no disco físico.
  2. Dados efêmeros: Assim que os dados são lidos de um FIFO, eles desaparecem do canal (diferente de arquivos regulares, onde o conteúdo permanece).
  3. Sincronização automática: Processos de escrita bloqueiam até que haja um leitor pronto, e vice-versa. Isso fornece sincronização automática entre processos. No exemplo acima, não importa a ordem de disparo: se cat myfifo ou echo pwn > myfifo for executado primeiro, um esperará pacientemente pelo outro. Em arquivos normais, o leitor leria um arquivo vazio se executado antes da escrita terminar.
  4. Fluxos de dados complexos: FIFOs são ideais para orquestrar fluxos de dados avançados, divisão e junção de canais, suportando múltiplos leitores e escritores.

Este desafio será uma introdução prática aos FIFOs. Você precisará criar o FIFO em /tmp/flag_fifo e redirecionar a saída padrão (stdout) de /challenge/run para ele. Se fizer tudo certo, /challenge/run enviará a flag para dentro do FIFO! Mãos à obra!


DICA: O comportamento bloqueante dos FIFOs pode tornar difícil resolver este desafio em um único terminal sem processos em segundo plano. Pode ser muito prático abrir dois terminais (por exemplo, no modo Desktop ou VSCode) para ler e escrever simultaneamente no FIFO.


30-Day Scoreboard:

This scoreboard reflects solves for challenges in this module after the module launched in this dojo.

Rank Hacker Badges Score