Neste artigo, exploraremos as principais técnicas de trabalho com canais em Go e nos familiarizaremos com os métodos e práticas de escrita de código multithread. Como exemplo prático, criaremos nosso próprio fuzzer rápido que verificará a existência de arquivos de um dicionário em um host.
A implementação de concorrência em Go, através da troca de mensagens entre threads independentes, é chamada de CSP (Communicating Sequential Processes). Sua base são os canais, que transmitem mensagens de uma thread (goroutine) para outra.
Se você está familiarizado com o conceito de pipes de sistema, já terá uma ideia geral. No entanto, o runtime do Go não utiliza os recursos do sistema operacional; ele possui sua própria implementação de goroutines e canais.
Não nos aprofundaremos na metodologia de fuzzing em si: o objetivo principal é dominar Go, portanto, vamos nos concentrar nas ferramentas fornecidas pela linguagem de programação. Assim, a implementação será a mais simples possível: adicionaremos uma palavra do dicionário ao nome do host, executaremos uma requisição GET no endereço resultante e salvaremos o código de resposta.
Você pode obter um dicionário, por exemplo, aqui, mas recomendo não pegar uma lista muito grande, e sim escrever a sua com cerca de dez posições – isso é mais do que suficiente para começar.
Info: Frequentemente, o modelo CSP é descrito com uma afirmação: "não implemente comunicação através do compartilhamento de memória; em vez disso, compartilhe memória através da comunicação".
Web: O modelo CSP foi desenvolvido e descrito por Charles Antony Hoare – talvez você já tenha ouvido falar dele em relação ao algoritmo quicksort (qsort).
Criaremos um novo módulo:
bashmkdir dirfuzzer cd dirfuzzer go mod init dirfuzzer touch main.go
Dividindo o Código em Módulos
No arquivo main.go, inseriremos o seguinte código:
gopackage main import ( "bufio" "fmt" "io" "net/http" "os" "strings" "sync" "time" ) type Result struct { Name string Code int } // produce gera tarefas para processamento, combinando o host com os valores lidos do filename, // e os coloca no canal outCh. func produce(filename string, host string, outCh chan<- string) { file, err := os.Open(filename) if err != nil { fmt.Fprintf(os.Stderr, "opening %s: %v\n", filename, err) return } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { s := strings.TrimSpace(scanner.Text()) if s == "" { continue } outCh <- "https://" + host + "/" + s } if err := scanner.Err(); err != nil { fmt.Fprintf(os.Stderr, "reading %s: %v\n", filename, err) } } // worker recebe valores do canal inCh, enquanto ele estiver aberto, executa o processamento e coloca os resultados em outCh. func worker(c *http.Client, inCh <-chan string, outCh chan<- Result) { for job := range inCh { resp, err := c.Get(job) if err != nil { continue } resp.Body.Close() io.Copy(io.Discard, resp.Body) result := Result{ Name: job, Code: resp.StatusCode, } outCh <- result } } // collect recebe valores do canal resultCh, enquanto ele estiver aberto, e os escreve no arquivo filename. func collect(filename string, resultCh <-chan Result) { dstFile, err := os.Create(filename) if err != nil { fmt.Fprintf(os.Stderr, "creating %s: %v\n", filename, err) return } defer dstFile.Close() writer := bufio.NewWriter(dstFile) for r := range resultCh { s := fmt.Sprintf("%s - %d %s\n", r.Name, r.Code, http.StatusText(r.Code)) _, err = writer.WriteString(s) if err != nil { fmt.Fprintf(os.Stderr, "writing to %s: %v\n", filename, err) } } if err = writer.Flush(); err != nil { fmt.Fprintf(os.Stderr, "writing to %s: %v\n", filename, err) } } func main() { // TODO }
Como você pode ver, dividimos logicamente os trechos de código em funções separadas e isoladas, de acordo com os papéis que são atribuídos a esse código.
A função produce() gera tarefas para processamento posterior. Ela lê linhas de uma lista e forma URLs para as quais as tentativas de conexão serão feitas. O processamento em si está fora da responsabilidade desta função e não afeta seu código. Outros nomes frequentemente usados para tal código são: generate, source.
A função worker() processa as tarefas. Neste caso, é executada uma requisição GET ao servidor e o código de resposta é salvo. Como você pode ver, a função "não sabe" nada sobre a origem das tarefas, nem sobre o destino dos resultados – isso não lhe diz respeito. Outros nomes frequentemente encontrados para tal função são: process, handle.
A função collect() lê os resultados e os salva em um arquivo. Por analogia com as anteriores, ela não sabe e não deve saber de onde e como esses resultados foram obtidos. Ela tem apenas uma área de responsabilidade claramente definida – coletar e salvar. Outros nomes frequentemente encontrados são: consume, sink, save.
Essa divisão incorpora o princípio de construção de sistemas de software loose coupling, high cohesion (uma das variantes de tradução: "acoplamento fraco, alta coesão"). De acordo com este princípio, o sistema é construído a partir de componentes separados que dependem minimamente uns dos outros (isso é o loose coupling). Ao mesmo tempo, cada componente executa um papel claramente definido, e o código responsável por uma determinada função está concentrado dentro de seu "próprio" componente, e não espalhado por outros (e isso é o high cohesion).
Essa abordagem facilita a compreensão do funcionamento do sistema, permite a aplicação de testes modulares, melhora a reutilização de código e possibilita o aumento de funcionalidades no futuro.
Se quisermos, por exemplo, salvar os resultados em outro formato ou, em vez de escrever em um arquivo, enviá-los pela rede para outro servidor – só precisamos substituir o componente collect, sem tocar nos outros e sem sequer ler seu código. Este é um princípio geral, aplicável não apenas a funções ou classes separadas, mas a quaisquer componentes de um sistema de software em um sentido amplo.
Info: A divisão de um grande bloco monolítico de código que resolve uma determinada tarefa em componentes separados é chamada de modularização. Não sei sobre você, mas acho difícil até pronunciar, por isso prefiro o conceito relacionado – decomposição. Embora, é claro, seja mais sobre a abordagem para resolver um problema do que sobre a estruturação da implementação dessa solução.
Canais
Executaremos essas funções como goroutines separadas. Ao mesmo tempo, abandonaremos a abordagem "clássica" de troca de informações através de objetos compartilhados protegidos por mutexes. Em vez disso, utilizaremos canais – uma ferramenta especial de comunicação entre goroutines. Canais são thread-safe por definição e não requerem o uso de mutexes ou outros primitivos de sincronização.
O restante do conteúdo está disponível apenas para membros.
Materiais das últimas edições tornam-se disponíveis individualmente apenas dois meses após a publicação. Para continuar a leitura, é necessário se tornar um membro da comunidade "Xakep.ru".
Junte-se à comunidade "Xakep.ru"!
A associação à comunidade durante o período especificado lhe dará acesso a TODO o material "Xaker", permitirá baixar as edições em PDF, desativará a publicidade no site e aumentará seu desconto pessoal acumulado!
Saiba mais
Já sou membro "Xakep.ru"
← Anterior
No Brasil, sistema de alerta de emergência foi hackeado





