
Segurança do kernel Linux, recursos e Seccomp
BPF oferece uma maneira poderosa de estender o kernel sem comprometer a estabilidade, segurança ou velocidade. Por esse motivo, os desenvolvedores do kernel acharam que seria uma boa ideia aproveitar sua versatilidade para melhorar o isolamento de processos no Seccomp, implementando filtros Seccomp suportados por programas BPF, também conhecidos como Seccomp BPF. Neste capítulo, vamos explicar o que é Seccomp e como ele é aplicado. Em seguida, você aprenderá como escrever filtros Seccomp usando programas BPF. Depois disso, vamos dar uma olhada nos ganchos BPF embutidos que o kernel tem para os módulos de segurança do Linux.
O Linux Security Modules (LSMs) é uma plataforma que fornece um conjunto de funções que podem ser usadas para padronizar a implementação de vários modelos de segurança. O LSM pode ser usado diretamente na árvore de origem do kernel, como Apparmor, SELinux e Tomoyo.
Vamos começar discutindo os recursos do Linux.
Capacidades
A essência das capacidades do Linux é que você precisa conceder permissão a um processo não privilegiado para realizar uma tarefa específica, mas sem suid para essa finalidade, ou de outra forma tornar o processo privilegiado, reduzindo a possibilidade de ataques e permitindo que o processo execute determinadas tarefas. Por exemplo, se seu aplicativo precisa abrir uma porta privilegiada, digamos 80, em vez de executar o processo como root, você pode simplesmente dar a ele o recurso CAP_NET_BIND_SERVICE.
Considere um programa Go denominado main.go:
package main
import (
"net/http"
"log"
)
func main() {
log.Fatalf("%v", http.ListenAndServe(":80", nil))
}
Este programa atende a um servidor HTTP na porta 80 (esta é uma porta privilegiada). Normalmente o executamos logo após a compilação:
$ go build -o capabilities main.go
$ ./capabilities
No entanto, como não estamos concedendo privilégios de root, este código gerará um erro ao vincular a porta:
2019/04/25 23:17:06 listen tcp :80: bind: permission denied
exit status 1
capsh (ferramenta de controle de shell) é uma ferramenta que inicia um shell com um conjunto específico de recursos.
Neste caso, como já foi mencionado, em vez de conceder direitos de root completos, você pode habilitar ligações de portas privilegiadas habilitando cap_net_bind_service junto com tudo o mais que já está no programa. Para fazer isso, podemos envolver nosso programa em capsh:
# capsh --caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' \
--keep=1 --user="nobody" \
--addamb=cap_net_bind_service -- -c "./capabilities"
Vamos entender um pouco sobre este comando.
- capsh - use capsh como uma concha.
- --caps = 'cap_net_bind_service + eip cap_setpcap, cap_setuid, cap_setgid + ep' - uma vez que precisamos alterar o usuário (não queremos executar como root), especificaremos cap_net_bind_service e a capacidade de realmente alterar o ID do usuário de root para ninguém, ou seja, cap_setuid e cap_setgid ...
- --keep=1 — , root.
- --user=«nobody» — , , nobody.
- --addamb=cap_net_bind_service — root.
- — -c "./capabilities" — .
— , , execve(). , , , , .
Você provavelmente está se perguntando o que + eip significa após especificar um recurso na opção --caps. Esses sinalizadores são usados para especificar que o recurso:
-deve ser ativado (p);
-disponível para aplicação (e);
- pode ser herdado por processos filho (i).
Como queremos usar cap_net_bind_service, precisamos fazer isso com o sinalizador e. Em seguida, iniciamos o shell no comando. Isso iniciará o binário de recursos e precisamos marcá-lo com o sinalizador i. Finalmente, queremos que o recurso seja ativado (fizemos isso sem alterar o UID) com p. Parece cap_net_bind_service + eip.
Você pode verificar o resultado com ss. Reduza um pouco a saída para caber na página, mas mostrará a porta associada e o ID do usuário diferente de 0, neste caso 65.534:
# ss -tulpn -e -H | cut -d' ' -f17-
128 *:80 *:*
users:(("capabilities",pid=30040,fd=3)) uid:65534 ino:11311579 sk:2c v6only:0
Neste exemplo, usamos capsh, mas você pode escrever um shell usando libcap. Veja man 3 libcap para mais informações.
Ao escrever programas, o desenvolvedor muitas vezes não conhece com antecedência todos os recursos exigidos pelo programa em tempo de execução; além disso, esses recursos podem mudar em novas versões.
Para entender melhor os recursos de nosso programa, podemos usar a ferramenta compatível com BCC, que define kprobe para a função de kernel cap_capable:
/usr/share/bcc/tools/capable
TIME UID PID TID COMM CAP NAME AUDIT
10:12:53 0 424 424 systemd-udevd 12 CAP_NET_ADMIN 1
10:12:57 0 1103 1101 timesync 25 CAP_SYS_TIME 1
10:12:57 0 19545 19545 capabilities 10 CAP_NET_BIND_SERVICE 1
Podemos conseguir o mesmo usando bpftrace com o kprobe de uma linha na função de kernel cap_capable:
bpftrace -e \
'kprobe:cap_capable {
time("%H:%M:%S ");
printf("%-6d %-6d %-16s %-4d %d\n", uid, pid, comm, arg2, arg3);
}' \
| grep -i capabilities
Isso irá gerar algo como o seguinte se os recursos do nosso programa forem ativados após o kprobe:
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 10 1
A quinta coluna são os recursos de que o processo precisa e, como essa saída inclui eventos de não auditoria, vemos todas as verificações de não auditoria e, finalmente, a capacidade necessária com o sinalizador de auditoria (o último na saída) definido como 1. Capacidade. no qual estamos interessados é CAP_NET_BIND_SERVICE, ele é definido como uma constante no código-fonte do kernel no arquivo include / uapi / linux / capacity.h com ID 10:
/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */
#define CAP_NET_BIND_SERVICE 10<source lang="go">
Os recursos são frequentemente aproveitados no tempo de execução para contêineres como runC ou Docker para serem executados no modo sem privilégios, mas só são permitidos aqueles recursos necessários para executar a maioria dos aplicativos. Quando um aplicativo requer recursos específicos, o Docker pode fornecer --cap-add:
docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummy
Este comando fornecerá ao contêiner o recurso CAP_NET_ADMIN, que permitirá que ele configure um link de rede para adicionar a interface dummy0.
A próxima seção demonstra o uso de recursos como filtragem, mas com um método diferente que nos permite implementar programaticamente nossos próprios filtros.
Seccomp
Seccomp significa Secure Computing, é uma camada de segurança implementada no kernel Linux que permite aos desenvolvedores filtrar certas chamadas de sistema. Embora o Seccomp seja comparável aos recursos do Linux, sua capacidade de lidar com chamadas de sistema específicas o torna muito mais flexível do que é.
Os recursos do Seccomp e do Linux não são mutuamente exclusivos e costumam ser usados juntos para se beneficiar de ambas as abordagens. Por exemplo, você pode querer dar a um processo a capacidade CAP_NET_ADMIN, mas não permitir que ele aceite conexões de soquete, bloqueando as chamadas de sistema de aceitar e aceitar4.
O método de filtragem Seccomp é baseado em filtros BPF operando no modo SECCOMP_MODE_FILTER, e a filtragem de chamadas do sistema é realizada da mesma maneira que para pacotes.
Os filtros Seccomp são carregados usando prctl por meio da operação PR_SET_SECCOMP. Esses filtros estão na forma de um programa BPF que é executado para cada pacote Seccomp representado pela estrutura seccomp_data. Essa estrutura contém a arquitetura de referência, um ponteiro para as instruções do processador durante a chamada do sistema e um máximo de seis argumentos de chamada do sistema, expressos como uint64.
É assim que a estrutura seccomp_data se parece na fonte do kernel no arquivo linux / seccomp.h:
struct seccomp_data {
int nr;
__u32 arch;
__u64 instruction_pointer;
__u64 args[6];
};
Como você pode ver nesta estrutura, podemos filtrar pela chamada do sistema, seus argumentos ou uma combinação de ambos.
Depois de receber cada pacote Seccomp, o filtro deve realizar o processamento para tomar uma decisão final e dizer ao kernel o que fazer a seguir. A decisão final é expressa em um dos valores de retorno (códigos de status).
- SECCOMP_RET_KILL_PROCESS - término de todo o processo imediatamente após a filtragem de uma chamada de sistema que não é executada por causa disso.
- SECCOMP_RET_KILL_THREAD - término da thread atual imediatamente após filtrar uma chamada de sistema, que por causa disso não é executada.
- SECCOMP_RET_KILL - alias para SECCOMP_RET_KILL_THREAD, deixado para compatibilidade com versões anteriores.
- SECCOMP_RET_TRAP - A chamada do sistema é desabilitada e o sinal SIGSYS (Bad System Call) é enviado para a tarefa de chamada.
- SECCOMP_RET_ERRNO - A chamada do sistema não é executada e parte do valor de retorno do filtro SECCOMP_RET_DATA é passado para o espaço do usuário como errno. Diferentes valores de errno são retornados dependendo da causa do erro. Os números dos erros são listados na próxima seção.
- SECCOMP_RET_TRACE - Usado para notificar o ptrace com - PTRACE_O_TRACESECCOMP para interceptar quando uma chamada de sistema é feita para ver e controlar este processo. Se o rastreador não estiver conectado, um erro é retornado, errno é definido como -ENOSYS e a chamada do sistema não é executada.
- SECCOMP_RET_LOG - A chamada do sistema é permitida e registrada.
- SECCOMP_RET_ALLOW - a chamada do sistema é simplesmente permitida.
ptrace é uma chamada de sistema para implementação de mecanismos de rastreamento em um processo denominado tracee, com a capacidade de monitorar e controlar a execução do processo. O programa de rastreamento pode influenciar efetivamente a execução e alterar os registros de memória do rastreamento. No contexto do Seccomp, ptrace é usado quando disparado pelo código de status SECCOMP_RET_TRACE, para que o rastreador possa impedir que a chamada do sistema seja executada e implementar sua própria lógica.
Erros Seccomp
De vez em quando, ao trabalhar com Seccomp, você encontrará vários erros, que são identificados por um valor de retorno do tipo SECCOMP_RET_ERRNO. Para relatar um erro, a chamada do sistema seccomp retornará -1 em vez de 0.
Os seguintes erros são possíveis:
- EACCESS - O chamador não tem permissão para fazer uma chamada do sistema. Isso geralmente acontece porque ele não tem o privilégio CAP_SYS_ADMIN ou no_new_privs não está definido com prctl (mais sobre isso depois);
- EFAULT - os argumentos passados (args na estrutura seccomp_data) não possuem um endereço válido;
- EINVAL - pode haver quatro razões aqui: - a
operação solicitada é desconhecida ou não é suportada pelo kernel na configuração atual;
- os sinalizadores especificados são inválidos para a operação solicitada;
-operation inclui BPF_ABS, mas há problemas com o deslocamento especificado, que pode exceder o tamanho da estrutura seccomp_data;
- o número de instruções passadas para o filtro excede o máximo;
- ENOMEM - memória insuficiente para executar o programa;
- EOPNOTSUPP - a operação indicou que uma ação estava disponível com SECCOMP_GET_ACTION_AVAIL, mas o kernel não suporta retorno em argumentos;
- ESRCH - ocorreu um problema ao sincronizar outro stream;
- ENOSYS - nenhum rastreador é anexado à ação SECCOMP_RET_TRACE.
prctl é uma chamada de sistema que permite a um programa de espaço de usuário manipular (definir e obter) aspectos específicos de um processo, como sequência de bytes, nomes de encadeamentos, modo de computação seguro (Seccomp), privilégios, eventos Perf e assim por diante.
O Seccomp pode parecer uma tecnologia sandbox para você, mas não é. Seccomp é um utilitário que permite aos usuários desenvolver um mecanismo de sandbox. Agora vamos ver como criar programas de interação personalizados usando um filtro chamado diretamente pela chamada do sistema Seccomp.
Filtro de amostra BPF Seccomp
Aqui mostraremos como combinar as duas ações discutidas anteriormente, a saber:
- escrever o programa Seccomp BPF, que será usado como um filtro com diferentes códigos de retorno dependendo das decisões tomadas;
- carregue o filtro usando prctl.
Primeiro você precisa dos cabeçalhos da biblioteca padrão e do kernel do Linux:
#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>
Antes de tentar este exemplo, precisamos ter certeza de que o kernel está compilado com CONFIG_SECCOMP e CONFIG_SECCOMP_FILTER definido como y. Em uma máquina de produção, você pode testá-lo assim:
cat /proc/config.gz| zcat | grep -i CONFIG_SECCOMP
O resto do código é uma função install_filter de duas partes. A primeira parte contém nossa lista de instruções de filtragem BPF:
static int install_filter(int nr, int arch, int error) {
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3),
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
};
As instruções são definidas usando as macros BPF_STMT e BPF_JUMP definidas no arquivo linux / filter.h.
Vamos seguir as instruções.
- BPF_STMT (BPF_LD + BPF_W + BPF_ABS (offsetof (struct seccomp_data, arch))) - o sistema carrega e acumula com BPF_LD na forma da palavra BPF_W, os dados do pacote estão localizados em um deslocamento fixo BPF_ABS.
- BPF_JUMP (BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) - verifica usando BPF_JEQ se o valor da arquitetura na constante do acumulador BPF_K é igual a arch. Nesse caso, ele salta no deslocamento 0 para a próxima instrução; caso contrário, ele salta no deslocamento 3 (neste caso) para lançar um erro, porque o arco não corresponde.
- BPF_STMT (BPF_LD + BPF_W + BPF_ABS (offsetof (struct seccomp_data, nr))) - baixa e acumula com BPF_LD na forma da palavra BPF_W, que é o número de chamada do sistema contido no deslocamento fixo BPF_ABS.
- BPF_JUMP (BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) - compara o número de chamada do sistema com o valor da variável nr. Se eles forem iguais, ele continua para a próxima instrução e desabilita a chamada do sistema; caso contrário, habilita a chamada do sistema com SECCOMP_RET_ALLOW.
- BPF_STMT (BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (erro & SECCOMP_RET_DATA)) - termina o programa com BPF_RET e, como resultado, emite um erro SECCOMP_RET_ERRNO com um número da variável err.
- BPF_STMT (BPF_RET + BPF_K, SECCOMP_RET_ALLOW) - termina o programa com BPF_RET e permite a execução de uma chamada de sistema usando SECCOMP_RET_ALLOW.
SECCOMP IS CBPF Você deve
estar se perguntando por que uma lista de instruções é usada em vez de um objeto ELF compilado ou um programa C compilado JIT.
Há duas razões para isso.
• Primeiro, o Seccomp usa cBPF (BPF clássico), não eBPF, o que significa que ele não possui registradores, mas apenas um acumulador para armazenar o último resultado do cálculo, como você pode ver no exemplo.
• Em segundo lugar, Seccomp leva um ponteiro para um array de instruções BPF diretamente e nada mais. As macros que usamos ajudam apenas a especificar essas instruções de uma forma conveniente para os programadores.
Se precisar de mais ajuda para entender esta montagem, considere o pseudocódigo que faz o mesmo:
if (arch != AUDIT_ARCH_X86_64) {
return SECCOMP_RET_ALLOW;
}
if (nr == __NR_write) {
return SECCOMP_RET_ERRNO;
}
return SECCOMP_RET_ALLOW;
Depois de definir o código do filtro na estrutura socket_filter, você precisa definir um sock_fprog contendo o código e o comprimento do filtro calculado. Esta estrutura de dados é necessária como um argumento para declarar o trabalho do processo no futuro:
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
Resta apenas uma coisa a fazer na função install_filter - baixar o próprio programa! Para fazer isso, usamos prctl, tendo PR_SET_SECCOMP como uma opção para entrar no modo de computação seguro. Em seguida, dizemos ao modo para carregar o filtro usando SECCOMP_MODE_FILTER, que está contido na variável prog do tipo sock_fprog:
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
return 0;
}
Finalmente, podemos usar nossa função install_filter, mas antes disso precisamos usar prctl para definir PR_SET_NO_NEW_PRIVS para a execução atual e, assim, evitar uma situação em que os processos filhos obtenham mais privilégios que seus pais. No entanto, podemos fazer as seguintes chamadas para prctl na função install_filter sem ter direitos de root.
Agora podemos chamar a função install_filter. Vamos bloquear todas as chamadas de sistema de gravação relacionadas à arquitetura X86-64 e apenas dar permissão, o que bloqueia todas as tentativas. Depois de instalar o filtro, continue a execução usando o primeiro argumento:
int main(int argc, char const *argv[]) {
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("prctl(NO_NEW_PRIVS)");
return 1;
}
install_filter(__NR_write, AUDIT_ARCH_X86_64, EPERM);
return system(argv[1]);
}
Vamos começar. Podemos usar clang ou gcc para compilar nosso programa, de qualquer forma, é apenas compilar o arquivo main.c sem opções especiais:
clang main.c -o filter-write
Conforme observado, bloqueamos todas as entradas do programa. Para testar isso, você precisa de um programa que produza algo - ls parece um bom candidato. É assim que ela normalmente se comporta:
ls -la
total 36
drwxr-xr-x 2 fntlnz users 4096 Apr 28 21:09 .
drwxr-xr-x 4 fntlnz users 4096 Apr 26 13:01 ..
-rwxr-xr-x 1 fntlnz users 16800 Apr 28 21:09 filter-write
-rw-r--r-- 1 fntlnz users 19 Apr 28 21:09 .gitignore
-rw-r--r-- 1 fntlnz users 1282 Apr 28 21:08 main.c
Perfeitamente! É assim que nosso programa shell se parece: nós apenas passamos o programa que queremos testar como o primeiro argumento:
./filter-write "ls -la"
Quando executado, este programa produz uma saída completamente vazia. No entanto, podemos usar o strace para ver o que está acontecendo:
strace -f ./filter-write "ls -la"
O resultado do trabalho é bastante encurtado, mas a parte correspondente mostra que os registros estão bloqueados com o erro EPERM - o mesmo que configuramos. Isso significa que o programa não produz nada porque não pode acessar a chamada de sistema de gravação:
[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "write error", 11) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "\n", 1) = -1 EPERM (Operation not permitted)
Agora você entende como funciona o Seccomp BPF e tem uma boa ideia do que pode ser feito com ele. Mas você não gostaria de fazer o mesmo com o eBPF em vez do cBPF para usar todo o seu poder?
Ao pensar em programas eBPF, a maioria das pessoas pensa que está apenas escrevendo e carregando-os com privilégios de administrador. Embora essa afirmação seja geralmente verdadeira, o kernel implementa um conjunto de mecanismos para proteger objetos eBPF em vários níveis. Esses mecanismos são chamados de armadilhas BPF LSM.
Traps BPF LSM
Para fornecer monitoramento independente de arquitetura de eventos do sistema, o LSM implementa o conceito de armadilhas. Uma chamada de gancho é tecnicamente semelhante a uma chamada de sistema, mas é independente do sistema e integrada à infraestrutura. O LSM fornece um novo conceito no qual a camada de abstração pode ajudar a evitar problemas que surgem ao lidar com chamadas de sistema em arquiteturas diferentes.
No momento em que este livro foi escrito, o kernel tinha sete ganchos associados a programas BPF e o SELinux é o único LSM integrado que os implementa.
O código-fonte dos ganchos está localizado na árvore do kernel no arquivo include / linux / security.h:
extern int security_bpf(int cmd, union bpf_attr *attr, unsigned int size);
extern int security_bpf_map(struct bpf_map *map, fmode_t fmode);
extern int security_bpf_prog(struct bpf_prog *prog);
extern int security_bpf_map_alloc(struct bpf_map *map);
extern void security_bpf_map_free(struct bpf_map *map);
extern int security_bpf_prog_alloc(struct bpf_prog_aux *aux);
extern void security_bpf_prog_free(struct bpf_prog_aux *aux);
Cada um deles será chamado em diferentes estágios de execução:
- security_bpf - executa verificações iniciais de chamadas de sistema BPF executadas;
- security_bpf_map - verifica quando o kernel retorna um descritor de arquivo para o mapa;
- security_bpf_prog - Verifica quando o kernel retorna um descritor de arquivo para o programa eBPF;
- security_bpf_map_alloc - verifica se o campo de segurança dentro dos mapas BPF foi inicializado;
- security_bpf_map_free - verifica se o campo de segurança dentro dos mapas BPF está limpo;
- security_bpf_prog_alloc - verifica se o campo de segurança foi inicializado dentro de programas BPF;
- security_bpf_prog_free - verifica se o campo de segurança está limpo dentro de programas BPF.
Agora, vendo tudo isso, entendemos que a ideia por trás dos interceptores LSM BPF é que eles podem fornecer proteção para todos os objetos eBPF, garantindo que apenas aqueles com os privilégios apropriados possam realizar operações em mapas e programas.
Resumo
A segurança não é algo que você possa impor de uma maneira única para qualquer coisa que queira proteger. É importante ser capaz de proteger os sistemas em diferentes níveis e de maneiras diferentes. Acredite ou não, a melhor maneira de proteger um sistema é organizar diferentes níveis de proteção de diferentes posições, de modo que a degradação da segurança de um nível impeça o acesso a todo o sistema. Os desenvolvedores do kernel fizeram um ótimo trabalho nos fornecendo um conjunto de diferentes camadas e pontos de contato. Esperamos ter dado a você uma boa compreensão do que são camadas e como usar programas BPF para trabalhar com elas.
Sobre os autores
David Calavera é CTO da Netlify. Ele trabalhou para o Suporte Docker e contribuiu para o desenvolvimento de ferramentas Runc, Go e BCC, bem como outros projetos de código aberto. Conhecido por seu trabalho em projetos Docker e no desenvolvimento do ecossistema de plug-ins Docker. David gosta muito de gráficos em degradê e sempre se esforça para otimizar o desempenho.
Lorenzo Fontana faz parte da equipe de desenvolvimento de código aberto da Sysdig, onde está principalmente envolvido no Falco, um projeto da Cloud Native Computing Foundation que fornece segurança em tempo de execução de contêiner e detecção de anomalias por meio do módulo de kernel e eBPF. Ele é apaixonado por sistemas distribuídos, rede definida por software, kernel do Linux e análise de desempenho.
»Mais detalhes sobre o livro podem ser encontrados no site da editora
» Índice
» Trecho
Para Habitantes desconto de 25% no cupom - Linux No
ato do pagamento da versão em papel do livro, é enviado um e-book por e-mail.