Filter Di WinPcap: Nilai Tambah Sebuah Library

Program PING di tulisan sebelumnya baru sampai mengirim ECHO REQUEST saja. Untuk melengkapinya, aku akan mencoba menambahkan kode untuk membaca ECHO REPLY. Tapi kali ini tidak dengan memakai callback karena ada cara lain yang lebih tepat, yaitu dengan menggunakan fungsi pcap_next_ex() yang men-capture packet sesuai kebutuhan saja. Ini adalah contoh kode program yang aku buat:

int status;
while ((status=pcap_next_ex(handle, &header, &packetData))>=0) {
  if (status==0) break;
  struct ECHO_REPLY *echoReply = (struct ECHO_REPLY*)((void*)packetData);
  printf("Menerima ECHO REPLY dengan sequence %d.\n", echoReply->icmp_sequence);
  printf("Data ECHO:\n");
  for (int i=1; i<=32; i++) {     printf("%02x ", echoReply->icmp_data[i-1]);
    if (i%8==0) printf("\n");
  }
  printf("\n");
}

Fungsi pcap_next_ex() akan mengembalikan nilai 1 jika packet dibaca tanpa masalah, nilai 0 jika timeout, nilai -1 jika terjadi error, dan nilai -2 jika EOF saat membaca offline capture. Setelah membaca sebuah packet, aku menampikan nilai icmp_sequence dan juga data yang disisipkan bersama packet ICMP ECHO tersebut.

Sampai disini, akan ada satu masalah! pcap_next_ex() akan mencapture semua paket yang lalu-lalang di network card, baik yang dikirim keluar maupun dikirim masuk, baik TCP, UDP maupun ICMP. Padahal kode program di atas langsung mengandaikan bahwa packet yang diterima adalah packet ICMP ECHO REPLY dari IP tertentu yang dikirim ke komputer ini. Di saat-saat seperti ini, fitur filter milik WinPcap sangat dibutuhkan. Fitur ini dapat menyaring packet apa saja yang akan diterima oleh aplikasi berdasarkan ekspresi boolean yang aku tentukan lewat kode program. Untuk itu, aku harus melibatkan dua fungsi, yaitu pcap_compile() dan pcap_setfilter(). Fungsi pcap_compile() akan menerjemahkan ekspresi dalam bentuk teks ke dalam byte code, kemudian pcap_setfilter() akan menerapkan byte code tersebut. Berikut ini adalah contoh penggunaan pcap_compile():

if (pcap_compile(handle, &filterCode,
  "icmp[icmptype] = 0 and icmp[icmpcode] = 0 and src host 192.168.1.1", 1, 0) < 0) {
  printf("ERROR Compiling Filter\n");
  pcap_close(handle);
  exit(EXIT_FAILURE);
}

filterCode disini adalah sebuah struct bpf_program yang merepresentasikan filter yang sesungguhnya. Parameter terakhir seharusnya berupa network mask (seperti 0xffffff untuk 255.255.255.0). Nilai ini hanya dibutuhkan untuk menyaring packet broadcast, dan karena aku tidak membutuhkan packet broadcast maka aku mengabaikan nilai ini. Lalu ekspresi untuk filter dengan jelas menyatakan untuk hanya menampilkan packet ICMP dengan field icmptype = 0 dan icmpcode = 0 (atau dengan kata lain, ECHO REPLY), dan dengan source address dari 192.168.1.1.

Setelah melakukan proses compile, aku dapat menerapkan filter seperti pada kode berikut:

if (pcap_setfilter(handle, &filterCode) < 0) {
  printf("ERROR Setting Filter\n");
  pcap_close(handle);
  exit(EXIT_FAILURE);
}

Setelah menambahkan fungsi pcap_compile() dan pcap_setfilter() sebelum pcap_next_ex(), akhirnya program PING sederhana ini selesai juga. Saatnya beralih ke topik berikutnya.

Iklan

Mengirim Raw Packet: Latihan Ping

Setelah sebelumnya aku belajar membaca raw packet yang diterima network card, kali ini aku ingin mencoba mengirim raw packet secara langsung. Keuntungannya adalah aku bisa bebas menentukan semua parameter packet yang aku kirim. Sebagai latihan, aku akan mencoba membuat program yang mirip seperti program PING yang mengirim dan menerima packet ICMP ECHO.

Format packet ICMP ditentukan oleh RFC 792. Berikut ini adalah contoh packet ECHO REQUEST yang dikirim oleh program PING:

xx xx 08 00 4a 5c 02 00    01 00 61 62 63 64 65 66
67 68 69 6a 6b 6c 6d 6e    6f 70 71 72 73 74 75 76
77 61 62 63 64 65 66 67    68 68

Dan ini adalah contoh ECHO REPLY atas packet di atas:

xx xx 00 00 52 5c 02 00    01 00 61 62 63 64 65 66
67 68 69 6a 6b 6c 6d 6e    6f 70 71 72 73 74 75 76
77 61 62 63 64 65 66 67    68 69

Byte awal 08 menunjukkan kalau packet tersebut adalah ECHO REQUEST, dan byte 00 menunjukkan kalau packet tersebut adalah ECHO REPLY. Byte kedua (CODE) berisi nilai 00. Kemudian diteruskan dengan dua byte checksum. Dua byte berikutnya adalah IDENTIFIER, dilanjutkan dengan dua byte SEQUENCE NUMBER. Sepertinya program PING selalu menggunakan IDENTIFIER dengan nilai 0x0200. Nilai SEQUENCE NUMBER dipergunakan untuk mencocokkan antara request dan reply. Setelah itu PING mengirimkan 32 byte data yang berupa deretan huruf.

Sebelum memulai mengirim packet, aku harus memastikan bawah packet ICMP di-enkapsulasi lagi oleh packet IP, lalu oleh packet Ethernet. Untuk mempermudah pembuatan program, aku membuat sebuah struct sederhana yang merepresentasikan packet ECHO REQUEST seperti berikut ini:

struct ECHO_REQUEST {
  u_char mac_destination[6];
  u_char mac_source[6];
  u_short type;
  u_char ip_version_headerlength;
  u_char ip_services;
  u_short ip_totalLength;
  u_short ip_identification;
  u_char ip_flag;
  u_char ip_fragment_offset;
  u_char ip_ttl;
  u_char ip_protocol;
  u_short ip_checksum;
  u_char ip_source[4];
  u_char ip_destination[4];
  u_char  icmp_type;
  u_char  icmp_code;
  u_short icmp_checksum;
  u_short icmp_identifier;
  u_short icmp_sequence;
  u_char  icmp_data[32];
};

Jika aku ingin men-PING komputer dengan IP 192.168.1.1 dari komputer dengan IP 192.168.1.2, maka aku dapat mengisi setiap field di packet seperti:

void initializeEchoRequestPacket(ECHO_REQUEST *echoRequest) {
  u_char mac_destination[6] = {0x1, 0x1, 0x1, 0x1, 0x1, 0x1 };
  valuecpy(echoRequest->mac_destination, mac_destination, 6);
  u_char mac_source[6] = {0x2, 0x2, 0x2, 0x2, 0x2, 0x2 };
  valuecpy(echoRequest->mac_source, mac_source, 6);
  echoRequest->type = 0x0008;

  echoRequest->ip_version_headerlength = 0x45;
  echoRequest->ip_services = 0x00;
  echoRequest->ip_totalLength = 0x3C00;
  echoRequest->ip_identification = 0x0400;
  echoRequest->ip_flag = 00;
  echoRequest->ip_fragment_offset = 0x0000;
  echoRequest->ip_ttl = 0x80;
  echoRequest->ip_protocol = 0x01;
  echoRequest->ip_checksum = 0x0000;
  u_char ip_source[4] = {192,168,1,2};
  valuecpy(echoRequest->ip_source, ip_source, 4);
  u_char ip_dest[4] = {192,168,1,1};
  valuecpy(echoRequest->ip_destination, ip_dest, 4);

  echoRequest->icmp_type = 0x08;
  echoRequest->icmp_code = 0x00;
  echoRequest->icmp_checksum = 0x0000;
  echoRequest->icmp_identifier = 0x0002;
  echoRequest->icmp_sequence = 0x000d;
  for (int i=0; i<32; i++) {     echoRequest->icmp_data[i] = 'B';
  }
}

Agar packet ICMP ECHO REQUEST ini dapat dikirim dan diterima, selain mengisi IP address dengan benar, aku juga harus mengisi nilai MAC Source dan MAC Destination (yang merupakan bagian dari Ethernet Frame) dengan benar. Selain itu, aku juga harus melakukan kalkulasi untuk menghasilkan checksum dengan benar. Proses kalkulasi dan contoh kode program untuk kalkulasi checksum dapat dilihat di RFC 1071. Ini adalah contoh fungsi sederhana untuk menghitung nilai checksum untuk IP Header:

void calculateIPChecksum(ECHO_REQUEST *packet) {
  u_short *shortValue = (u_short*)
    (&packet->ip_version_headerlength);
  long totalValue = 0;

  for (int i=0; i<10; i++) {     
    totalValue += (*shortValue);     
    shortValue++;   
  }   
  while (totalValue>>16) {
    totalValue = (totalValue & 0xffff) + (totalValue>>16);
  }
  packet->ip_checksum = ~totalValue;
}

Fungsi di atas mengandaikan IP Header berukuran fixed 20 bytes atau 10 words. Kalkulasi checksum dilakukan dengan menjumlahkan seluruh word (integer 16-bit) yang membentuk IP Header. Jika hasil jumlah dalam bentuk angka 16-bit ini mengandung carry/overflow , maka carry dari MSB dijumlahkan ke LSB, dan ini dilakukan berulang sampai tidak ada carry lagi. Dan hasilnya, 1’s complement dari nilai tersebut adalah nilai cheksum yang dipakai.

Dan sebagai langkah terakhir untuk mengirim ECHO REQUEST, aku harus melakukan proses pengiriman packet. Cara yang paling gampang adalah dengan menggunakan fungsi pcap_sendpacket(). Sebagai contoh, aku mengirimkan 15 kali ECHO REQUEST dengan menggunaakan kode seperti berikut:

for (int j=0; j<15; j++) {   
  struct ECHO_REQUEST *echoRequest = 
    new struct ECHO_REQUEST();   
  initializeEchoRequestPacket(echoRequest);   
  echoRequest->ip_identification = (u_short) j;
  echoRequest->icmp_identifier = 0x0002;
  echoRequest->icmp_sequence = (u_short) j;
  calculateIPChecksum(echoRequest);
  calculateICMPChecksum(echoRequest);
  if (pcap_sendpacket(handle, (u_char*)((void*)echoRequest), 74)) {
    printf("ERROR: %s\n", pcap_geterr(handle));
    exit(EXIT_FAILURE);
  }
  delete echoRequest;
}

O ya, sebagai data pada ECHO REQUEST, aku mengirimkan 32 byte huruf ‘B’. Sebenarnya, aku punya kebebasan untuk memberikan nilai apa saja disini. Salah satu ide kreatif yang pernah aku temui adalah mengirimkan data melalui ICMP ECHO, seperti isi web page atau text chatting. Administrator cenderung memblokir komunikasi TCP/IP berdasarkan port. Jika ia berbaik hati membolehkan packet ICMP REQUEST keluar masuk dengan leluasa [test dengan mencoba apakah program PING dapat dipakai], maka seseorang bisa saja melakukan tunneling. Misalnya, request HTTP disisipkan sebagai data dalam packet ICMP ECHO REQUEST, kemudian diterima oleh komputer lain yang bebas dari blokiran. Program spesial di komputer lain tersebut membaca data spesial di ICMP ECHO REQUEST tersebut, lalu melakukan koneksi internet, dan mengirimkan hasilnya ke komputer pengguna, juga dalam bentuk ICMP ECHO REQUEST/REPLY. Sang administrator di ruangan khususnya, kini sedang serius membaca log firewall (atau mungkin membaca chat gadis-gadis QA sambil sesekali tertawa, no offense here, just a speculation), hanya akan melihat dua komputer saling nge-ping.

Network Packet Programming: Membaca Packet Dengan Callback

Setelah berhasil mendapatkan network device yang dapat di-sniffer, aku akan mencoba membaca packet milik network device tersebut. Aku harus mulai dengan fungsi pcap_open_live(). Setelah itu, untuk membaca packet, WinPcap menawarkan dua cara, dengan callback atao tanpa callback (cara biasa). Pada latihan kali ini, aku akan mencoba memakai callback, sehingga fungsi yang perlu aku panggil adalah pcap_dispatch() atau pcap_loop(). Aku akan memakai pcap_loop() yang akan terus menunggu hingga jumlah packet yang aku tentukan tercapai, walaupun hal ini dapat menyebabkan blocking jika jaringan sedang sibuk.

Untuk mempersingkat tulisan, aku tidak akan menyertakan kode program untuk menampilkan informasi device lagi. Aku hanya menambahkan sedikit kode yang memberi pilihan bagi user untuk menentukan device mana yang akan di-capture:

int pilihan=1, i=1;
printf("Masukkan nomor perangkat yang akan di-capture: ");
scanf_s("%d", &pilihan);
currentDevice = devices;
while (pilihan!=i) {
  currentDevice = currentDevice->next;
  i++;
}

Setelah itu, aku memanggil pcap_open_live() dengan kode seperti:

pcap_t *handle = pcap_open_live(currentDevice->name, 65535, 1, 1000, pesanError);
if (handle==NULL) {
  printf("ERROR: %s\n", pesanError);
  pcap_freealldevs(devices);
  exit(EXIT_FAILURE);
}

Pada pcap_open_live() di atas, aku men-capture maksimal 65.535 byte (seharusnya ini adalah maksimal ukuran kebanyakan packet); aku juga mengaktifkan modus promiscuous, yaitu tidak hanya menangkap packet yang ditujukan untuk komputer-ku tapi juga packet yang dalam perjalanan ke komputer lain tapi harus singgah sebentar ke komputer-ku (well, orang yang iseng pasti sangat suka ini!); dan aku menentukan read timeout sebesar 1000 ms.

Sampai pada bagian ini, aku tidak membutuhkan lagi informasi devices, sehingga aku bisa membebaskan alokasi memori untuk linked list yang berisi informasi tersebut. Setelah itu, aku bisa mendaftarkan fungsi callback:

pcap_freealldevs(devices);
pcap_loop(handle, 0, handlerPacketMasuk, NULL);

Sekarang, setiap kali ada packet yang masuk, fungsi handlerPacketMasuk() akan dikerjakan. Berikut ini adalah contoh fungsi tersebut:

void handlerPacketMasuk(u_char *user,
       const struct pcap_pkthdr *pkt_header,
       const u_char *pkt_data) {

  for (u_int i=1; i<=(pkt_header->len); i++) {
    printf("%02x ", pkt_data[i-1]);
    if (i%8==0) printf("\n");
  }

  printf("\n\n");

}

Fungsi di atas akan menampilkan isi packet dalam tampilan 8 byte per baris. Setidaknya hari ini aku sudah membuat sebuah program sniffer primitif.

Network Packet Programming: Percobaan Pertama

Sehubungan dengan topik yang sedang dipelajari, alangkah menyenangkannya jika aku dapat membuat program untuk menangkap dan mengirim packet secara langsung pada data link layer. Teman-temanku yang suka iseng pasti pernah menggunakan program serupa untuk menyadap atau memblokir koneksi internet seseorang. Tool tersebut harus dapat membuat packet palsu, dan ini hanya bisa dilakukan di data link layer. Sayangnya, Windows tidak menyediakan cara gampang untuk memodifikasi packet selain dengan membuat NDIS intermediate driver. Tapi kabar baiknya, ada sebuah library open source (plus driver siap jadi, tentunya) yang dapat digunakan untuk keperluan ini, yaitu WinPCap. Wireshark adalah salah satu tool yang memanfaatkan WinPCap untuk men-capture packet.

Setelah men-download WinPcap Developer Pack, aku akan mendapatkan beberapa header dan static library yang siap dipakai oleh program. Sekarang, aku akan menyiapkan project baru di Visual C++. Aku masih menggunakan Visual Studio 2008 dan sudah hampir setahun lebih aku tidak menyentuh gadis ini (kabarnya Visual Studio 2010 beta sudah dapat dicoba). Setelah sedikit meluangkan waktu untuk menyesuaikan diri, aku meng-import header-header yang ada. Aku juga tidak lupa menambahkan referensi bagi compiler, dengan men-klik kanan nama project dan memilih properties, C/C++, General. Lalu pada Additional Include Directories, aku menambahkan folder yang berisi header tersebut. Hal yang sama juga aku lakukan pada static library, dengan memilih Linker, Input, dan mengisi Additional Dependencies dengan lokasi static library milik WinPCap (wpcap.lib dan Packet.lib).

Aku mulai dengan membuat kode seperti:

#include "pcap.h"
#include <conio.h>

void exitHandler() {
  printf("Tekan sembarang tombol untuk keluar dari program...");
  _getch();
}

int main() {
  atexit(exitHandler);

  ...

  exit(EXIT_SUCCESS);
}

Langkah pertama adalah aku akan menampilkan perangkat network apa saja yang tersedia dan dapat dipergunakan oleh WinPcap, dengan memanggil fungsi pcap_findalldevs(). Bila ingin men-capture packet milik network card yang berada di jaringan remote (bukan di komputer lokal), aku harus menggunakan fungsi pcap_findalldevs_ex(). Syarat lainnya adalah komputer remote tersebut harus menjalankan daemon rpcapd.exe (terdapat di folder instalasi WinPcap).

pcap_if_t *devices;
char pesanError[PCAP_ERRBUF_SIZE];
if (pcap_findalldevs(&devices, pesanError)==-1) {
  printf("ERROR: %s\n", pesanError);
  exit(EXIT_FAILURE);
}

pcap_findalldevs() akan menghasilkan nilai -1 jika terjadi kesalahan saat mencari device apa saja yang dapat dipergunakan. Jika ia sukses, maka daftar device yang ada akan disimpan dalam struktur linked list pcap_if_t. Berikut ini adalah contoh kode program untuk menampilkan isi pcap_if_t:

#include "pcap.h"
#include <conio.h>

char *iptos(u_long in);

void exitHandler() {
  printf("Tekan sembarang tombol untuk keluar dari program...");
  _getch();
}

int main() {
  atexit(exitHandler);
  pcap_if_t *devices;
  char pesanError[PCAP_ERRBUF_SIZE];
  if (pcap_findalldevs(&devices, pesanError)==-1) {
    printf("ERROR: %s\n", pesanError);
    exit(EXIT_FAILURE);
  }

  pcap_if_t *currentDevice = devices;

  while (currentDevice != NULL) {

    printf("Device: %s\n", currentDevice->name);
    printf("Description: %s\n",
    currentDevice->description ? currentDevice->description : "(informasi tidak
      tersedia)");

    pcap_addr *address = currentDevice->addresses;
    while (address != NULL) {
      if (address->addr->sa_family == AF_INET) {
        sockaddr_in *sockaddr = (struct sockaddr_in *) address->addr;
        printf("Address: %s\n", iptos(sockaddr->sin_addr.s_addr));
      }
      address=address->next;
    }
    currentDevice = currentDevice->next;
    printf("\n\n");
  }

  pcap_freealldevs(devices);
  exit(EXIT_SUCCESS);
}

/* From tcptraceroute, convert a numeric IP address to a string */
#define IPTOSBUFFERS 12
char *iptos(u_long in)
{
  static char output[IPTOSBUFFERS][3*4+3+1];
  static short which;
  u_char *p;
  p = (u_char *)&in;
  which = (which + 1 == IPTOSBUFFERS ? 0 : which + 1);
  sprintf_s(output[which], "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
  return output[which];
}

Program di atas juga menampilkan alamat IP untuk setiap device yang disimpan dalam struktur linked list pcap_addr jika ada. Untuk menerjemahkan alamat IP numerik dan menampilkan ke dalam bentuk String (berbahagialah programmer Java yang punya toString()), aku men-copy paste fungsi iptos() yang ada di dokumentasi WinPcap. Dan lagi-lagi tidak seperti di Java, aku harus membebaskan alokasi memory setelah aku tidak membutuhkannya, dalam hal ini sudah ada fungsi pembantu pcap_freealldevs().