Contents

Develop
2003.04.23 11:13

[c] 지나가는 패킷 잡기

조회 수 8802 댓글 0
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
#########################################################
  리눅스에서 pcap library를 사용하여 패킷을 잡아보기 v0.3 
                                                       
  글쓴이 : 노광민                                       
  e-mail : djstop@orgio.net
  homepage : http://myhome.shinbiro.com/~nkm24
  tcpdump, libpcap 소스 구할 수 있는 곳 : ftp://ftp.ee.lbl.gov
#########################################################
 
저는 시스템, 네트웍 프로그램에 흥미가 많았고 윈도우즈는 이것들을 하기 위한 
소스를 충분히 제공하지 못하는 OS입니다.
하지만 리눅스는 실험정신과 도전정신만 있다면 이런 프로그램도 충분히 할 
수 있죠. 저와 같이 시스템, 네트웍 프로그램은 하고 싶은데 당장 뛰어들기가
막막하신 분들을 위해 이렇게 짧은 지식이나마 먼저 해 보았던 경험을 토대로
한 번 글을 올려 봅니다. 여기에서 설명하는 내용들은 실제 socket을 이용한
네트웍 프로그램과는 조금 다른 것입니다.
 
리눅스에는 네트웍 프로그램을 하기 위해 참고할 만한 많은 소스들이 많이 있습니다.
제가 프로젝트를 할 때 참고로 했던 것은 tcpdump입니다. 이 놈은 네트웍상의
각종 패킷을 잡아서 텍스트 정보로 요약해서 보여주는 프로그램이지요..
그럼 이 놈을 보면 나두 네트웍상의 패킷을 잡아서 볼 수 있겠구나하고 생각을 했죠.
 
그럼 패킷을 잡는 것이 얼마나 쉬운지 한 번 보겠습니다.
 
int main(int argc, char *argv[])
{
        .
        .
        .
        .
        .
        
        opterr = 0;
        
        if (device == NULL ) {
            if ( (device = pcap_lookupdev(ebuf) ) == NULL) {
                perror(ebuf);           
                   exit(-1);
               }
           }
                
        pd = pcap_open_live(device, snaplen, PROMISCUOUS, 1000, ebuf);
        if(pd == NULL) {
               perror(ebuf);          
               exit(-1);
           }
 
        if(pcap_lookupnet(device, &localnet, &netmask, ebuf) < 0) {
               perror(ebuf);
               exit(-1);
           }
        
        setuid(getuid());
        
        if(pcap_compile(pd, &fcode, filter_rule, 0, netmask) < 0) {
               perror(ebuf);
               exit(-1);
           }
        
        if(pcap_setfilter(pd, &fcode) < 0) {
               perror(ebuf);
               exit(-1);
           }
        
        fflush(stderr);
        
        printer = lookup_printer(pcap_datalink(pd));
        pcap_userdata = 0;
        
        if(pcap_loop(pd, packetcnt, printer, pcap_userdata) < 0) {
               perror("pcap_loop error");
               exit(-1);
           }
        
        pcap_close(pd);
        exit(0);
}
 
네트웍 상의 패킷을 잡기 위해 pcap library의 위와 같은 함수들을 나열하기만 
하면 됩니다. 차례대로 볼까요?
 
        device = pcap_lookupdev(ebuf);
        
요놈은 리눅스 머신의 네트웍 디바이스를 가져오는 함수입니다. 패킷을 잡으려면
네트웍 디바이스를 지정해야 겠죠?  이놈은 가능한 다비이스중 가장 번호가 
낮은 디바이스를 가져오게 됩니다. 리눅스라면 eth0이겠죠...
i 옵션으로 디바이스를 수동으로 지정할 수 있습니다.
 
        pd = pcap_open_live(device, snaplen, PROMISCUOUS, 1000, ebuf)
        
위 함수는 실제 기기를 열어주는 기능을 하는 것으로 snaplen는 패킷당 저장할
바이스 수, 실제 datalink계층부터 패킷의 크기를 계산하여 원하는 부분만을
얻어오면 되는 것입니다. 헤더정보만을 보고싶은데 쓸데없이 데이타까지 받을
필요는 없겠죠. 데이터까지 보고싶으면 이를 늘리면 됩니다. 
PROMISCUOUS는 1이며 네트웍 디바이스에 오는 모든 패킷을 받겠다는
의미입니다. 이 모드를 자세하게 설명하면 lan은 모든 패킷이 broadcasting되며
일단 모든 네트웍 디바이스는 동일 네트웍내의 다른 호스트의 패킷도 일단 접하게
됩니다.  그러나, 네트웍 디바이스는 기본적으로 자신의 패킷만을
받게끔 되어있습니다. 그러므로 다른 호스트의 패킷은 버리게 되는 것입니다.
그러나 promiscuous모드로 디바이스 모드를 바꾸게 되면 모든 패킷을 받아들이게
되는 것입니다. 스니퍼링 프로그램은 모두 이 모드를 사용하게 됩니다.
세 번째 인자는 패킷이 버퍼로 전달될 때 바로 전달되는 것이 아니라
위에서 명시한 시간을 넘겼을 때나 버퍼가 다 채워졌을 때 응용프로그램으로
전달되는 것입니다. 
 
 
        pcap_lookupnet(device, &localnet, &netmask, ebuf)
        
열려진 패킷 캡쳐 디바이스에 네트웍 주소와 서브넷 마스크를 넘겨줍니다.
 
        pcap_compile(pd, &fcode, filter_rule, 0, netmask)        
        
정해진 필터룰에 의해 필터 프로그램을 컴파일하게 되는데 우리가 원하는 패킷은
필터룰을 주어야만 원하는 패킷만을 얻을 수 있습니다. 실제 tcpdump에서 사용하는 
필터룰이 여기에서 쓰입니다. 예를 들면 "tcp port 80" ... 자세한 필터룰에 대한 
설명은 tcpdump의 메뉴얼을 보면 알 수 있습니다.
 
        pcap_setfilter(pd, &fcode)
        
위는 앞서 컴파일한 필터 프로그램을 패킷 캡쳐 디바이스로 읽어들이게 됩니다.
이렇게 하여 원하는 패킷을 얻을 준비를 하게 됩니다.        
 
        printer = lookup_printer(pcap_datalink(pd));
        
위는 패킷 캡쳐 디바이스의 datalink계층의 종류를 넘겨 받아 이에 따른 적절한
함수포인터를 할당하게 됩니다.
 
        pcap_loop(pd, packetcnt, printer, pcap_userdata)
        
이 놈이 실제 패킷을 잡아서 실행할 함수를 지정해 주는 함수입니다.
packetcnt의 수만큼 패킷을 잡아서 잡을 때 마다 printer가 가르치는 함수를 
수행하게 됩니다. packetcnt를 0으로 지정하면 무한대로 함수를 실행합니다.
 
더 자세한 설명을 원한다면 스티븐스 아저씨가 쓴 unp(unix network programming)
을 보면 26장에 pcap library에 대한 설명이 나와 있습니다.
 
그럼 이 pcap library를 이용해 실제 네트웍상의 ip기반의 tcp, udp, icmp의 패킷을
잡아서 텍스트로 필드별로 뿌려주는 소스를 보기로 합시다.
이 소스는 제가 tcpdump의 소스를 보고 노가다 프로그램을 한 것이며 
소스에 대한 설명은 주석으로 대신하겠습니다.
 
##################################################################################
#include <sys/time.h>
#include <netinet/in.h>
#include <net/ethernet.h>
#include <pcap/pcap.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <netinet/ip_icmp.h>
 
#define         PROMISCUOUS 1
 
struct   iphdr    *iph;
struct   tcphdr  *tcph;
struct   udphdr *udph;
struct   icmp     *icmph;
static   pcap_t   *pd;
int sockfd;
 
int pflag;      // DATA를 문자로 찍을 것인지.
int rflag;      // DATA를 생으로 찍을 것인지.
int eflag;     // DATALINK layer print option
int cflag;     // 패킷을 이 순자만큼 찍어주고 종료한다.
int chcnt;    // 문자를 찍을 때 문자카운터 다음줄에 찍기위해
 
char        *device, *filter_rule;
 
void packet_analysis(unsigned char *, const struct pcap_pkthdr *, 
                    const unsigned char *);
 
struct printer {
   pcap_handler f;
   int type;
};
   
/* datalink type에 따른 불리어질 함수들의 
   목록들을 갖는 구조체                       
Data-link level type codes. 
#define DLT_NULL                0         no link-layer encapsulation 
#define DLT_EN10MB        1         Ethernet (10Mb) 
#define DLT_EN3MB                2         Experimental Ethernet (3Mb)
#define DLT_AX25                3         Amateur Radio AX.25
#define DLT_PRONET        4         Proteon ProNET Token Ring
#define DLT_CHAOS                5         Chaos
#define DLT_IEEE802        6         IEEE 802 Networks
#define DLT_ARCNET        7         ARCNET
#define DLT_SLIP                8         Serial Line IP
#define DLT_PPP                9         Point-to-point Protocol
#define DLT_FDDI                10         FDDI
#define DLT_ATM_RFC1483        11         LLC/SNAP encapsulated atm
#define DLT_RAW                12         raw IP
#define DLT_SLIP_BSDOS        13         BSD/OS Serial Line IP
#define DLT_PPP_BSDOS        14         BSD/OS Point-to-point Protocol
bpf.h 라는 헤더화일에 위와 같은 내용으로 정의되어 있다.                */
 
static struct printer printers[] = {
   { packet_analysis, DLT_IEEE802 },
   { packet_analysis, DLT_EN10MB  },
   { NULL, 0 },
};
   
/*  datalink type에 따라 수행될 함수를 결정하게 된다.
    이는 pcap_handler라는 함수형 포인터의 값으로 대입된다. */
static pcap_handler lookup_printer(int type) 
{
        struct printer *p;
    
        for(p=printers; p->f; ++p)
               if(type == p->type)
                return p->f;
                
        perror("unknown data link type");
}
 
 
/* pcap_loop()에 의해 패킷을 잡을 때마다 불려지는 함수
   pcap_handler가 이 함수를 포인터하고 있기 때문이다 */
void packet_analysis(unsigned char *user, const struct pcap_pkthdr *h, 
                    const unsigned char *p)
{
            int j, temp;
        unsigned int length = h->len;
        struct ether_header *ep;
        unsigned short ether_type;
        unsigned char *tcpdata, *udpdata,*icmpdata;
        register unsigned int i;
        
        chcnt = 0;
        
        // 잡은 패킷을 그대로 생으로 찍기        
        if(rflag) {
            while(length--) {
                printf("%02x ", *(p++));
                if( (++chcnt % 16) == 0 ) printf("nt");
            }
            fprintf(stdout, "n");
            return;
        }
 
        length -= sizeof(struct ether_header);
        
        // ethernet header mapping
        ep = (struct ether_header *)p;
        // ethernet header 14 bytes를 건너 뛴 포인터
        p += sizeof(struct ether_header);
        // datalink type
        ether_type = ntohs(ep->ether_type);
        
        printf("n");
        // lan frame이 IEEE802인경우 ether_type필드가 길이필드가 된다.
        if(ether_type <= 1500) {
            ;
            /*while(length--) {
                if(++is_llchdr <= 3) {
                    fprintf(stdout,"%02x",*p++);
                    continue;
                }
                if(++next_line == 16) {
                    next_line = 0;        
                    printf("nt");
                }
                printf("%02x",*p++);
            }*/
        }
        else 
        {    
            if(eflag) {
                    printf("nn    =================== Datalink layer ===================nt");
                    for(j=0; j<ETH_ALEN; j++){ 
                    printf("%X", ep->ether_dhost[j]); 
                    if(j != 5) printf(":");
                    }
                    printf("  ------> ");
                    for(j=0; j<ETH_ALEN; j++) {
                    printf("%X", ep->ether_shost[j]);
                            if(j != 5) printf(":");
                    }        
                    printf("ntether_type -> %xn", ntohs(ep->ether_type));
            }
 
            iph = (struct iphdr *) p;
            i = 0;
            if (ntohs(ep->ether_type) == ETHERTYPE_IP) {        // ip 패킷인가?
                // packet capturing한 것을 화면에 출력하는 부분
                        printf("nn    ===================    IP HEADER   ===================n");
                printf("t%s -----> ",   inet_ntoa(iph->saddr));
                printf("%sn", inet_ntoa(iph->daddr));
                printf("tVersion:         %dn", iph->version);
                printf("tHerder Length:   %dn", iph->ihl);
                printf("tService:         %#xn",iph->tos);
                printf("tTotal Length:    %dn", ntohs(iph->tot_len)); 
                printf("tIdentification : %dn", ntohs(iph->id));
                printf("tFragment Offset: %dn", ntohs(iph->frag_off)); 
                printf("tTime to Live:    %dn", iph->ttl);
                printf("tChecksum:        %dn", ntohs(iph->check));
        
                /* packet의 ip부분을 건너뛴 곳에서부터 tcp header의 시작이 된다.                            */
                if(iph->protocol == IPPROTO_TCP) {
                        tcph = (struct tcphdr *) (p + iph->ihl * 4);
                        // tcp data는 
                        tcpdata = (unsigned char *) (p + (iph->ihl*4) + (tcph->doff * 4));
                                printf("nn    ===================   TCP HEADER   ===================n");
                               printf("tSource Port:              %dn", ntohs(tcph->source));
                        printf("tDestination Port:         %dn", ntohs(tcph->dest));
                        printf("tSequence Number:          %dn", ntohl(tcph->seq));
                        printf("tAcknowledgement Number:   %dn", ntohl(tcph->ack_seq));
                        printf("tData Offset:              %dn", tcph->doff);
                        printf("tWindow:                   %dn", ntohs(tcph->window));
                        printf("tURG:%d ACK:%d PSH:%d RST:%d SYN:%d FIN:%dn", 
                        tcph->urg, tcph->ack, tcph->psh, tcph->rst, 
                        tcph->syn, tcph->fin, ntohs(tcph->check), 
                        ntohs(tcph->urg_ptr));
                        printf("n    ===================   TCP DATA(HEXA)  =================nt"); 
                        chcnt = 0;
                        for(temp = (iph->ihl * 4) + (tcph->doff * 4); temp <= ntohs(iph->tot_len) - 1; temp++) {
                               printf("%02x ", *(tcpdata++));
                            if( (++chcnt % 16) == 0 ) printf("nt");
                        }
                        if (pflag) {
                           printf("n    ===================   TCP DATA(CHAR)  =================n"); 
                                   tcpdata = (unsigned char *) ((p + iph->ihl*4) + (tcph->doff*4));
                           for(temp = (iph->ihl * 4) + (tcph->doff * 4); temp <= ntohs(iph->tot_len) - 1; temp++)
                                   printf("%c", *(tcpdata++));
                                }
                        printf("ntt<<<<< End of Data >>>>>n");
                        }
                else if(iph->protocol == IPPROTO_UDP) {
                        udph = (struct udphdr *) (p + iph->ihl * 4);
                    udpdata = (unsigned char *) (p + iph->ihl*4) + 8;
                    printf("n    ==================== UDP HEADER =====================n");
                    printf("tSource Port :      %dn",ntohs(udph->source));
                    printf("tDestination Port : %dn", ntohs(udph->dest));
                    printf("tLength :           %dn", ntohs(udph->len));
                       printf("tChecksum :         %xn", ntohs(udph->check));
                                printf("n    ===================  UDP DATA(HEXA)  ================nt");         
                    chcnt = 0;
                    for(temp = (iph->ihl*4)+8; temp<=ntohs(iph->tot_len) -1; temp++) {
                       printf("%02x ", *(udpdata++));
                       if( (++chcnt % 16) == 0) printf("nt"); 
                    }
 
                    udpdata = (unsigned char *) (p + iph->ihl*4) + 8;
                    if(pflag) {
                        printf("n===================  UDP DATA(CHAR)  ================n");         
                        for(temp = (iph->ihl*4)+8; temp<=ntohs(iph->tot_len) -1; temp++) 
                            printf("%c", *(udpdata++));
                    }
                    
                    printf("ntt<<<<< End of Data >>>>>n");
                }          
                else if(iph->protocol == IPPROTO_ICMP) {
                        icmph = (struct icmp *) (p + iph->ihl * 4);
                        icmpdata = (unsigned char *) (p + iph->ihl*4) + 8;
                                printf("nn    ===================   ICMP HEADER   ===================n");
                               printf("tType :                    %dn", icmph->icmp_type);
                        printf("tCode :                    %dn", icmph->icmp_code);
                        printf("tChecksum :                %02xn", icmph->icmp_cksum);
                        printf("tID :                      %dn", icmph->icmp_id);
                        printf("tSeq :                     %dn", icmph->icmp_seq);
                        printf("n    ===================   ICMP DATA(HEXA)  =================nt"); 
                        chcnt = 0;
                        for(temp = (iph->ihl * 4) + 8; temp <= ntohs(iph->tot_len) - 1; temp++) {
                               printf("%02x ", *(icmpdata++));
                            if( (++chcnt % 16) == 0 ) printf("nt");
                        }
                        printf("ntt<<<<< End of Data >>>>>n");
                      }
            }        
        }
}
 
void sig_int(int sig)
{
    printf("Bye!!n");
    pcap_close(pd);
    close(sockfd);
    exit(0);
}
 
void usage(void)
{
    fprintf(stdout," Usage : pa filter_rule [-pch]n");
    fprintf(stdout,"         -p  :  데이타를 문자로 출력한다.n");
    fprintf(stdout,"         -c  :  주어진 숫자만큼의 패킷만 덤프한다n");
    fprintf(stdout,"          -e  :  datalink layer를 출력한다.n");
    fprintf(stdout,"          -e  :  잡은 패킷을 생으로 찍는다.n");
    fprintf(stdout,"         -h  :  사용법n");
}
 
int main(int argc, char *argv[])
{
        struct        bpf_program fcode;
        pcap_handler printer;
        char        ebuf[pcap_ERRBUF_SIZE];
        int        c, i, snaplen = 512, size, packetcnt;
        bpf_u_int32 myself, localnet, netmask;
        unsigned char        *pcap_userdata;
                
        filter_rule = argv[1];                // ex) src host xxx.xxx.xxx.xxx and tcp port 80
        
        signal(SIGINT,sig_int);        // signal hanlder 등록 
        
        opterr = 0;
        
        if(argc-1 < 1) {                // option check
            usage(); 
            exit(1);
        }
        
        while( (c = getopt(argc, argv,"i:c:pher")) != -1) {
            switch(c) {
                    case 'i'  :                        // 패킷 캡쳐 기기 지정 
                            device = optarg        
                            break;
                    case 'p' :                 // 데이터를 문자로 출력하는 옵션
                        pflag = 1; 
                        break;
                    case 'c' :                 // 덤프하려는 패킷의 수
                        cflag = 1; 
                        packetcnt = atoi(optarg);
                        if(packetcnt <= 0) {
                        fprintf(stderr,"invalid pacet number %s",optarg);
                        exit(1);
                        }
                        break;
                case 'e' :                      // 데이터링크 계층 출력
                        eflag = 1;
                        break;                
                case 'r' :                      // 잡은 패킷을 맵핑없이 16진수로 모두 찍는다.
                        rflag = 1;
                        break;                
                    case 'h' :                        // 사용법
                        usage();
                        exit(1);
            }
        }            
        
        if (device == NULL ) {
            if ( (device = pcap_lookupdev(ebuf) ) == NULL) {
                perror(ebuf);           
                   exit(-1);
               }
           }
           fprintf(stdout, "device = %sn", device);
        
        pd = pcap_open_live(device, snaplen, PROMISCUOUS, 1000, ebuf);
        if(pd == NULL) {
               perror(ebuf);          
               exit(-1);
           }
        
        i = pcap_snapshot(pd);
        if(snaplen < i) {
               perror(ebuf);                            
              exit(-1);
           }
        
        if(pcap_lookupnet(device, &localnet, &netmask, ebuf) < 0) {
               perror(ebuf);
               exit(-1);
           }
        
        setuid(getuid());
        
        if(pcap_compile(pd, &fcode, filter_rule, 0, netmask) < 0) {
               perror(ebuf);
               exit(-1);
           }
        
        if(pcap_setfilter(pd, &fcode) < 0) {
               perror(ebuf);
               exit(-1);
           }
        
        fflush(stderr);
        
        printer = lookup_printer(pcap_datalink(pd));
        pcap_userdata = 0;
        
        if(pcap_loop(pd, packetcnt, printer, pcap_userdata) < 0) {
               perror("pcap_loop error");
               exit(-1);
           }
        
        pcap_close(pd);
        exit(0);
}
 
 
##################################################################################
 
실제 실행결과와 사용방법이 홈페이지에 올려져 있습니다.
수정할 부분이나 계선할 점이 많으니 소스를 고쳐서 더욱 강력한 유틸리티로 만들어 보는 
것이 좋을 것 같군요. 그래도 우리나라에 많은 도전정신과 실험정신을 가지고 있는 리눅서들이 
만든 프로그램을 사용하는 편이 좋겠죠...^^
 
위의 소스를 컴파일해서 사용하기 위해서는 pcap library가 있어야 하며 
아마 배포판 리눅스라면 /usr/lib/libpcap.a라는 화일로 있을 것입니다.
아래와 같이 링크시켜 컴파일하시면 됩니다.
컴파일시 bpf.h가 없다는 메세지가 나올 경우 /usr/include/pcap/net/bpf.h 를 
/usr/include/net/bpf.h로 복사를 해주시면 됩니다.
 
#cp /usr/include/pcap/net/bpf.h /usr/include/net
#gcc -g -o noh_pa noh_pa.c -lpcap
#./noh_pa "src host xxx.xxx.xxx.xxx and tcp port 80" -i eth0 -e -p
 
그리고 tcp/ip에 관련된 지식이 요구되는 소스이므로 관련책을 같이 보시면서 프로그램을
하시는 것이 수월하실 것입니다.
 
여기서는 단순히 패킷만을 잡아서 보여주는 데 그치지만 조금 더 관심을 가지시면
패킷을 직접 만들어 다른 호스트로 보낼 수도 있습니다.
pcap_library의 소스를 고쳐서 기능을 구현할 수도 있고 raw socket을 이용할 수도 
있습니다. 
raw socket을 이용하는 대표적인 예로는 ping 프로그램을 들 수 있습니다.
unp에 보시면 소스가 나와 있는데 이를 잘 분석하시면 icmp뿐 아니라 ip,tcp,udp등의
패킷도 직접 만들어 보낼 수 있습니다.
리눅스의 헤더화일등을 살펴보면 각 프로토콜별로 헤더형식의 구조체가 있는데 이를
참고하면 됩니다. 위 소스의 include 부분의 화일들입니다. 
물론 다른 프로토콜도 헤더형식에 맞게 구성만 해 준다면 가능하지요...
 
저의 홈페이지에 보시면 icmp의 목적지미도달 패킷을  직접 구성하여 해당 호스트의 접속을 끊는 
소스가 있습니다. 참고바랍니다.
 
소개한 내용과 소스가 관련 프로그램을 막 시작하려는 분들에게 조금이나마 도움이 
되었으면 하는 바램입니다.


?

List of Articles
번호 분류 제목 글쓴이 날짜 조회 수
» Develop [c] 지나가는 패킷 잡기 hooni 2003.04.23 8802
944 Develop [c] 스택/힙 오버플로우 테스트(overflow) file hooni 2003.04.23 7324
943 Develop [c] 반올림 함수!! ㅋㅋ hooni 2003.04.23 8084
942 Develop [c] 분수계산 함수^^ hooni 2003.04.23 10055
941 Develop [c] 소수 구하기 #1 (한정된 숫자 내에 있는 소수 걸러내기) hooni 2003.04.23 7810
940 Develop [c] 캘린더 양음 변환 함수 hooni 2003.04.23 8721
939 Develop [c] 날짜로 요일 찾기.. hooni 2003.04.23 8482
938 Develop [c] 문자열 뒤집기 (문자열 거꾸로 출력하는 간단소스) hooni 2003.04.23 9981
937 Develop [linux] tar 명령어 뽀개기.. ㅋㅋ hooni 2003.04.23 7664
936 Develop [c/c++] Makefile 사용하기.. ㅋㅋ hooni 2003.04.23 7319
935 Develop [js] 초간단 암호화/복호화 소스.. hooni 2003.04.23 8837
934 Develop [c]디렉토리 탐색 file hooni 2003.04.23 7301
Board Pagination Prev 1 ... 15 16 17 18 19 20 21 22 23 24 ... 98 Next
/ 98