TL;DR: estou escrevendo um módulo do núcleo que lerá comandos de um payload ICMP e os executará no servidor, mesmo que você tenha perdido a conexão SSH. Para os mais impacientes, todo o código está em .
Cuidado! Programadores experientes em C podem acabar chorando lágrimas de sangue! Posso estar errado, até mesmo na terminologia, mas toda crítica é bem-vinda. Este post é dirigido àqueles que têm uma ideia muito básica de programação em C e querem dar uma olhada nas entranhas do Linux.
Nos comentários do meu primeiro mencionaram o SoftEther VPN, que pode se disfarçar como alguns protocolos 'comuns', em particular HTTPS, ICMP e até DNS. Eu só consigo imaginar o funcionamento do primeiro, já que conheço bem o HTTP(S), e o tunelamento sobre ICMP e DNS precisei estudar.

Sim, em 2020 eu descobri que é possível inserir payloads arbitrários em pacotes ICMP. Mas melhor tarde do que nunca! E já que é possível fazer algo a respeito, é preciso fazê-lo. Como no meu cotidiano eu geralmente uso a linha de comando, inclusive via SSH, a ideia de um shell ICMP me veio à mente imediatamente. E, para conseguir um completo bingo de bullshit, decidi escrever como um módulo do Linux em uma linguagem da qual tenho apenas um entendimento básico. Um shell desse tipo não aparecerá na lista de processos, pode ser carregado no núcleo e não estará presente no sistema de arquivos, você não verá nada suspeito na lista de portas escutando. Em termos de funcionalidades, é um rootkit completo, mas espero aprimorá-lo e usá-lo como um shell de última esperança, quando a carga média estiver muito alta para acessar via SSH e executar pelo menos echo i > /proc/sysrq-trigger, para restaurar o acesso sem reiniciar.
Precisamos de um editor de texto, habilidades básicas de programação em Python e C, Google e , que não seja um problema sacrificar caso tudo dê errado (opcional — VirtualBox/KVM local/etc) e vamos lá!
Parte do cliente
Eu pensei que para a parte do cliente eu precisaria escrever um script de cerca de 80 linhas, mas apareceram pessoas generosas que fizeram o trabalho para mim . O código se mostrou surpreendentemente simples, cabendo em 10 linhas significativas:
import sys
from scapy.all import sr1, IP, ICMP
if len(sys.argv) < 3:
print('Uso: {} IP "comando"'.format(sys.argv[0]))
exit(0)
p = sr1(IP(dst=sys.argv[1])/ICMP()/"run:{}".format(sys.argv[2]))
if p:
p.show() O script aceita dois argumentos, o endereço e o payload. Antes de enviar o payload, ele é precedido pela chave. executar:, precisaremos dele para excluir pacotes com payload aleatório.
O núcleo requer privilégios para criar pacotes, portanto, o script deve ser executado com permissões de superusuário. Não se esqueça de conceder permissões de execução e instalar o próprio scapy. No Debian, há um pacote chamado python3-scapy. Agora podemos verificar como tudo isso funciona.
Execução e saída do comando
morq@laptop:~\/icmpshell$ sudo .\/send.py 45.11.26.232 "Olá, mundo!"
Iniciar emissão:
.Concluído o envio de 1 pacote.
*
Recebido 2 pacotes, obtido 1 resposta, 0 pacotes restantes
###[ IP ]###
versão = 4
ihl = 5
tos = 0x0
len = 45
id = 17218
flags =
frag = 0
ttl = 58
proto = icmp
chksum = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
options
###[ ICMP ]###
tipo = echo-reply
código = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Olá, mundo!
Assim é como aparece no sniffer
morq@laptop:~\/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Executando como usuário "root" e grupo "root". Isso pode ser perigoso.
Capturando em 'wlp1s0'
Quadro 1: 59 bytes na rede (472 bits), 59 bytes capturados (472 bits) na interface wlp1s0, id 0
Protocolo Internet Versão 4, Origem: 192.168.0.240, Destino: 45.11.26.232
Protocolo de Controle de Mensagens da Internet
Tipo: 8 (Echo (ping) request)
Código: 0
Soma de verificação: 0xd603 [correta]
[Status da Soma de Verificação: Bom]
Identificador (BE): 0 (0x0000)
Identificador (LE): 0 (0x0000)
Número de sequência (BE): 0 (0x0000)
Número de sequência (LE): 0 (0x0000)
Dados (17 bytes)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Dados: 72756e3a48656c6c6f2c20776f726c6421
[Comprimento: 17]
Quadro 2: 59 bytes na rede (472 bits), 59 bytes capturados (472 bits) na interface wlp1s0, id 0
Protocolo da Internet Versão 4, Src: 45.11.26.232, Dst: 192.168.0.240
Protocolo de Controle de Mensagens da Internet
Tipo: 0 (Resposta de Echo (ping))
Código: 0
Checksum: 0xde03 [correto]
[Status da Soma de Verificação: Bom]
Identificador (BE): 0 (0x0000)
Identificador (LE): 0 (0x0000)
Número de sequência (BE): 0 (0x0000)
Número de sequência (LE): 0 (0x0000)
[Quadro de solicitação: 1]
[Tempo de resposta: 19.094 ms]
Dados (17 bytes)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Dados: 72756e3a48656c6c6f2c20776f726c6421
[Comprimento: 17]
^C2 pacotes capturados
O payload no pacote de resposta não muda.
Módulo do núcleo
Para compilar em uma VM com Debian, você precisará de pelo menos make e linux-headers-amd64, o restante será puxado como dependências. Não irei apresentar todo o código do artigo, você pode cloná-lo no GitHub.
Configuração do hook
Primeiro, precisamos de duas funções para carregar e descarregar o módulo. A função para descarregar não é obrigatória, mas então rmmod não poderá ser executado, o módulo só será descarregado ao desligar.
#include <linux/module.h>
#include <linux/netfilter_ipv4.h>
static struct nf_hook_ops nfho;
static int __init startup(void)
{
nfho.hook = icmp_cmd_executor;
nfho.hooknum = NF_INET_PRE_ROUTING;
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net, &nfho);
return 0;
}
static void __exit cleanup(void)
{
nf_unregister_net_hook(&init_net, &nfho);
}
MODULE_LICENSE("GPL");
module_init(startup);
module_exit(cleanup);O que acontece aqui:
- Dois arquivos de cabeçalho são puxados para manipulações com o módulo e com o netfilter.
- Todas as operações passam pelo netfilter, onde podem ser definidas hooks. Para isso, é necessário declarar uma estrutura em que a hook será configurada. O mais importante é indicar a função que será executada como hook:
nfho.hook = icmp_cmd_executor;chegarei à própria função mais adiante.
Em seguida, defini o momento de processamento do pacote:NF_INET_PRE_ROUTINGindica processar o pacote assim que ele aparecer no núcleo. Pode-se usarNF_INET_POST_ROUTINGpara processar o pacote na saída do núcleo.
Coloco um filtro no IPv4:nfho.pf = PF_INET;.
Atribuo a maior prioridade ao meu hook:nfho.priority = NF_IP_PRI_FIRST;
E registro a estrutura de dados como um hook:nf_register_net_hook(&init_net, &nfho); - Na função final, o hook é removido.
- A licença é explicitamente indicada para que o compilador não reclame.
- Funções
module_init()emodule_exit()definem outras funções como inicializadoras e finalizadoras do funcionamento do módulo.
Extração do payload
Agora é preciso extrair o payload, e isso se revelou a tarefa mais difícil. O núcleo não possui funções integradas para trabalhar com payloads, é possível apenas analisar os cabeçalhos de protocolos de nível superior.
#include <linux/ip.h>
#include <linux/icmp.h>
#define MAX_CMD_LEN 1976
char cmd_string[MAX_CMD_LEN];
struct work_struct my_work;
DECLARE_WORK(my_work, work_handler);
static unsigned int icmp_cmd_executor(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
struct iphdr *iph;
struct icmphdr *icmph;
unsigned char *user_data;
unsigned char *tail;
unsigned char *i;
int j = 0;
iph = ip_hdr(skb);
icmph = icmp_hdr(skb);
if (iph->protocol != IPPROTO_ICMP) {
return NF_ACCEPT;
}
if (icmph->type != ICMP_ECHO) {
return NF_ACCEPT;
}
user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
tail = skb_tail_pointer(skb);
j = 0;
for (i = user_data; i != tail; ++i) {
char c = *(char *)i;
cmd_string[j] = c;
j++;
if (c == ' ')
break;
if (j == MAX_CMD_LEN) {
cmd_string[j] = ' ';
break;
}
}
if (strncmp(cmd_string, "run:", 4) != 0) {
return NF_ACCEPT;
} else {
for (j = 0; j <= sizeof(cmd_string)/sizeof(cmd_string[0])-4; j++) {
cmd_string[j] = cmd_string[j+4];
if (cmd_string[j] == ' ')
break;
}
}
schedule_work(&my_work);
return NF_ACCEPT;
}O que está acontecendo:
- Foi necessário incluir arquivos de cabeçalho adicionais, desta vez para manipular cabeçalhos IP e ICMP.
- Defino o comprimento máximo da string:
#define MAX_CMD_LEN 1976. Por que exatamente esse valor? Porque um valor maior gera avisos do compilador! Já me disseram que preciso entender melhor sobre a pilha e o heap, e um dia eu definitivamente farei isso e talvez até corrija o código. Imediatamente defino a string onde o comando ficará:char cmd_string[MAX_CMD_LEN];. Ela deve ser visível em todas as funções, explicarei isso em mais detalhes no item 9. - Agora é necessário inicializar (
struct work_struct my_work;) a estrutura e vinculá-la a mais uma função (DECLARE_WORK(my_work, work_handler);). Sobre a razão disso, também explicarei no nono ponto. - Agora declaro a função que servirá como hook. O tipo e os argumentos aceitos são determinados pelo netfilter, e nos interessa apenas
skb. Este é o buffer do socket, uma estrutura de dados fundamental que contém todas as informações disponíveis sobre o pacote. - Para o funcionamento da função, serão necessárias duas estruturas e algumas variáveis, incluindo dois iteradores.
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0; - Podemos prosseguir com a lógica. Para o funcionamento do módulo, não são necessários pacotes além do ICMP Echo, portanto analisamos o buffer com funções integradas e descartamos todos os pacotes que não são ICMP ou Echo. O retorno
NF_ACCEPTsignifica que o pacote é aceito, mas você também pode descartar pacotes, retornandoNF_DROP.iph = ip_hdr(skb); icmph = icmp_hdr(skb); if (iph->protocol != IPPROTO_ICMP) { return NF_ACCEPT; } if (icmph->type != ICMP_ECHO) { return NF_ACCEPT; }Não verifiquei o que acontece sem a verificação dos cabeçalhos IP. Meu conhecimento básico de C sugere que, sem verificações adicionais, algo horrível certamente ocorrerá. Ficarei feliz se você me convencer do contrário!
- Agora que temos um pacote do tipo certo, podemos extrair os dados. Sem a função integrada, é necessário primeiro obter um ponteiro para o início do payload. Isso é feito de uma maneira peculiar, pegando um ponteiro para o início do cabeçalho ICMP e deslocando-o pelo tamanho desse cabeçalho. Para tudo isso, usamos a estrutura
icmph:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
O final do cabeçalho deve coincidir com o final da carga útil emskb, então obtemos isso por meio de recursos nucleares da estrutura correspondente:tail = skb_tail_pointer(skb);.
A imagem foi retirada , você pode ler mais sobre o buffer do socket. - Após obter os ponteiros de início e fim, podemos copiar os dados para a string
cmd_string, verificar se há um prefixoexecutar:e, ou descartá-lo em caso de ausência, ou sobrescrever a string novamente, removendo esse prefixo. - Bom, agora podemos chamar mais um manipulador:
schedule_work(&my_work);. Como não é possível passar um parâmetro em tal chamada, a string com o comando deve ser global.schedule_work()colocará a função associada à estrutura passada na fila global do escalonador de tarefas e finalizar-se-á, permitindo não esperar a conclusão do comando. Isso é necessário porque o hook deve ser muito rápido. Caso contrário, você terá, a escolher, nada iniciado ou receberá um kernel panic. Procrastinação é morte certa! - Pronto, podemos receber o pacote com o retorno apropriado.
Chamada de programa no espaço do usuário
Essa função é a mais clara. Seu nome foi definido em DECLARE_WORK(), o tipo e os argumentos aceitos não são interessantes. Pegamos a string com o comando e a passamos inteira para o shell. Que ele se encarregue da análise, busca de binários e tudo o mais.
static void work_handler(struct work_struct * work)
{
static char *argv[] = {"/bin/sh", "-c", cmd_string, NULL};
static char *envp[] = {"PATH=/bin:/sbin", NULL};
call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
}- Definimos os argumentos em um array de strings
argv[]. Presumo que todos saibam que os programas são executados assim, e não como uma única string com espaços. - Definimos as variáveis de ambiente. Eu inserei apenas o PATH com um conjunto mínimo de caminhos, supondo que todos já tenham unidos
/bincom/usr/bine/sbincom/usr/sbin. Outros caminhos raramente são relevantes na prática. - Pronto, vamos executar! A função do núcleo
call_usermodehelper()recebe como entrada: o caminho para o binário, o array de argumentos, o array de variáveis de ambiente. Aqui também presumo que todos compreendem o sentido de passar o caminho para o arquivo executável como um argumento separado, mas você pode perguntar. O último argumento indica se deve esperar pela conclusão do processo (UMH_WAIT_PROC), iniciar o processo (UMH_WAIT_EXEC) ou não aguardar de forma alguma (UMH_NO_WAIT). Também existeUMH_KILLABLE, eu não me aprofundei nisso.
Compilação
A construção de módulos do núcleo é realizada através da própria estrutura de make do núcleo. É invocada make dentro de um diretório especial vinculado à versão do núcleo (definido aqui: KERNELDIR:=/lib/modules/$(shell uname -r)/build), e a localização do módulo é passada pela variável M nos argumentos. Nos alvos icmpshell.ko e clean, esse framework é usado completamente. Em obj-m especifica o arquivo objeto que será transformado em módulo. A sintaxe que transforma main.o em icmpshell.o (icmpshell-objs = main.o) para mim não parece muito lógica, mas que assim seja.
KERNELDIR:=/lib/modules/$(shell uname -r)/build
obj-m = icmpshell.o
icmpshell-objs = main.o
all: icmpshell.ko
icmpshell.ko: main.c
make -C $(KERNELDIR) M=$(PWD) modules
clean:
make -C $(KERNELDIR) M=$(PWD) clean
Compilando: make. Carregando: insmod icmpshell.ko. Pronto, você pode verificar: sudo ./send.py 45.11.26.232 "date > /tmp/test". Se um arquivo apareceu na sua máquina /tmp/test e ele contém a data de envio da solicitação, significa que você fez tudo certo e eu fiz tudo certo.
Conclusão
Minha primeira experiência em desenvolvimento de núcleo foi muito mais simples do que eu esperava. Mesmo sem ter experiência em desenvolvimento em C, orientando-me pelas dicas do compilador e as respostas do Google, consegui escrever um módulo funcional e me senti como um hacker de núcleo, além de um script kiddie. Além disso, entrei no canal Kernel Newbies, onde me recomendaram usar schedule_work() em vez da chamada call_usermodehelper() dentro do próprio hook e me corrigiram, justo suspeitando de um scam. Centenas de linhas de código me custaram cerca de uma semana de desenvolvimento nas horas vagas. Uma experiência bem-sucedida que destruiu meu mito pessoal sobre a complexidade insuperável do desenvolvimento de sistemas.
Se alguém concordar em fazer uma revisão de código no GitHub, ficarei agradecido. Estou quase certo de que cometi muitos erros bobos, especialmente ao lidar com strings.
Fonte: habr.com

