interceptando ping
Publicado; julho 5, 2008 Arquivado em: C, programação, python, rede | Tags: ansi c, icmp, interceptar, pacote, python, raw socket, sniffer 3 ComentáriosDepois de umas discussões sobre como interceptar pacotes ICMP que chegam no kernel, vim escrever esse artigo e registrar o código dos programas que fazem isso, da maneira mais simples possível.
Na verdade, o procedimento pra interceptar o ICMP echo request (ping), é o mesmo de qualquer sniffer de pacotes, e por isso precisa ser executado como root. Isso ocorre porque o programa que intercepta precisa criar um socket do tipo RAW, e essa função do kernel não pode ser usada por usuários comuns.
Embora o código seja relativamente simples, envolve bastante teoria sobre programação com sockets, e conhecimento (nesse caso) do protocolo ICMP e suas características. Aquele papo furado que você aprendeu na faculdade, com os cabeçalhos de pacotes na forma de desenho colorido não serve pra nada aqui – pois aqui, os cabeçalhos de pacotes são estruturas de linguagem C.
Quando um programa solicita a criação de um SOCK_RAW pro kernel, é possível especificar um determinado protocolo, pra que o próprio kernel já faça um filtro e não envie coisas que você não quer. Assim, posso especificar o protocolo identificado pelo número 1 (veja no /etc/protocols, é o ICMP). Há ainda uma função, tanto em C quanto em python, chamada getprotobyname, onde você especifica o nome do protocolo e recebe o número como resposta – mas isso é bem juvenil, embora seja um padrão.
Depois de criar o SOCK_RAW, já filtrando o conteúdo por ICMP, basta sair capturando os pacotes com recvfrom. Apesar dessa função retornar, entre outros, o endereço IP do remetente do pacote, o cabeçalho IP inteiro estará disponível nos dados do pacote. Ao ler de um RAW socket filtrado por ICMP, você terá o seguinte nos dados do pacote:
- cabeçalho IP
- cabeçalho ICMP
- dados
Basicamente, o kernel picota o ethernet frame / ARP e manda o resto pro programa. Abaixo, o programa que faz essa malandragem e sai mostrando na tela os pacotes ICMP recebidos na sua máquina:
/* icmpd.c 20080704 AF * * print incoming icmp echo-request * compile: * cc -Wall icmpd.c -o icmpd * references: * /usr/include/netinet/ip_icmp.h * SOCK_RAW socket(2) */ #include <stdio.h> #include <fcntl.h> #include <netdb.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <sys/socket.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> // raw packet struct packet { struct ip ip; struct icmp icmp; char data[4096]; }; int main() { struct packet pkt; int fd, len, srclen; struct sockaddr_in src; if((fd = socket(AF_INET, SOCK_RAW, 1)) == -1) perror("socket"), exit(1); for(;;) { srclen = sizeof(src); memset(&pkt, 0, sizeof(pkt)); memset(&src, 0, sizeof(src)); len = recvfrom(fd, &pkt, sizeof(pkt), 0, (struct sockaddr *)&src, (socklen_t *)&srclen); if(len >= 1 && !pkt.icmp.icmp_type) fprintf(stdout, "icmp echo-request sequence %d with %d bytes from %s\n", ntohs(pkt.icmp.icmp_hun.ih_idseq.icd_seq), len-20, inet_ntoa(src.sin_addr)); } return 0; }
Tem vários truques escondidos ai nesse programa. Primeiro, o que é lido pelo recvfrom “encaixa” no tipo de dados chamado packet, que foi criado lá em cima contendo a struct ip, struct icmp e um campo de dados que só armazena até 4096 bytes, menos o tamanho dos outros dois cabeçalhos. Se o programa receber um pacote ICMP com mais dados que isso (tipo, ping -s 5000 localhost), ele irá imprimir o tamanho do pacote errado.
Segundo, que depois do recvfrom, só entra no if quando o ICMP é do tipo 0 (echo reply), e não tipo 8 (echo request). Aí, quando o ICMP é do tipo zero, aquela union chamada icmp_hun (vide /usr/include/netinet/ip_icmp.h), provê os dados da struct ih_idseq, ao invés de qualquer outro dentro dela – quem não sabe como funciona union já viaja nessa parte; mas não desanime, procure no google que é fácil.
Pra fechar, imprimir o número de sequência do pacote requer uma conversão, visto que esse número está armazenado em network byte order, e não host byte order! Por isso, é necessário usar aquele ntohs() malandro ali. Não por menos, o tamanho do pacote len-20 é pra subtrair os 20 bytes do cabeçalho ip, e mostrar o tamanho correto, igual ao ping.
Um outro detalhe é que esse programa está baseado na struct icmp do BSD, porque estou usando um mac osx aqui. Porém, ele é totalmente compatível com lunix sem necessidade de alterar o código – os kernel/libc developers do lunix são muito malandros e deixam os headers (ip.h, ip_icmp.h, etc) no esquema.
Se você se empolgou com isso, eu não podia deixar por menos e me sinto quase que obrigado a colocar uma versão desse programa em python. É tão simples… veja aí:
#!/usr/bin/env python # icmpd.py 20080704 AF # # print incoming icmp echo-request # references: # /usr/include/netinet/ip_icmp.h # http://docs.python.org/lib/module-struct.html import struct, socket def parse(pkt): raw_size = len(pkt) pkt_size = raw_size - 1 # set format and unpack format = '!B' + (pkt_size and '%ds' % pkt_size or '') rawdata = struct.unpack(format, pkt) # check for echo-reply if rawdata[0] or not pkt_size: return None # extract reply sequence number sequence = struct.unpack('!hh', rawdata[1][3:7]) return (raw_size, sequence[1]) if __name__ == '__main__': fd = socket.socket(socket.AF_INET, socket.SOCK_RAW, 1) while 1: chunk, src = fd.recvfrom(4096) obj = parse(chunk[20:]) if obj: print 'icmp echo-request sequence %d with %d bytes from %s' % (obj[1], obj[0], src[0])
O princípio é o mesmo. A diferença é que no python não é possível acessar aquelas structs do kernel direto, e por isso é necessário usar o módulo struct (hehe). Esse também tem vários truques, veja…
Logo depois de ler os dados, já envio pra função parse(), o chunk[20:], que passa os dados à partir dos 20 primeiros bytes – ou seja, já picota o cabeçalho IP que estava ali. Dentro da função parse, crio um formato tipo ‘!B60s’, que significa:
- o exclamação determina que os dados estão em network byte order
- o primeiro B especifica um unsigned char, o primeiro campo da struct icmp – que contém o tipo!
- o número acompanhado de “s”, especifica um campo char com N bytes, que não será modificado
Esse unpack retorna uma lista com apenas dois índices – o primeiro com o tipo do pacote ICMP, e o segundo com o resto do cabeçalho.
Se eu quisesse, por exemplo, pegar os três primeiros campos da struct icmp, usaria algo assim: ‘!BBh’. Veja no /usr/include/netinet/ip_icmp.h que os campos são: unsigned char type, unsigned char code e unsigned short checksum. Se comparar com o manual do módulo struct do python, verá que ‘!BBh’ significa a mesma coisa, e o exclamação ali pra indicar o formato dos dados – network byte order.
Por fim, pra pegar o número de sequência do pacote tem outro truque. Como o rawdata[0] era o tipo do pacote icmp, e o rawdata[1] era o resto do cabeçalho ICMP, era necessário pegar apenas aquela struct ih_idseq lá dentro, que possui dois campos do tipo unsigned short. Considerando que o rawdata[1] era o cabeçalho, à partir do campo “code”, pulei três bytes – 1 do code, e 2 do checksum. À partir dali, mandei ler os outros 32, sendo 16 de cada unsigned short – icd_id e então icd_seq.
Por isso, aquele struct.unpack(‘!hh’, rawdata[1][3:7]) retorna exatamente os dois campos da struct icmp e permite obter o número de sequência do pacote.
Comentários