2010년 11월 15일 월요일

Scapy 로 패킷 핸들링하는 프로그램 만들기 - 세번째

Scapy 의 마지막으로 세번째 이야기를 소개해본다. 이번에는 Scapy 를 이용해 나만의 프로그램을
만드는 방법이다. 앞서 소개한 것과 같이 Scapy 는 파이썬기반이어서, 파이썬에서도 Scapy 모듈을 로드하여
내가 원하는 형태로 패킷을 쉽게 다룰 수가 있다.

모든 패킷 도구들이 내가 원하는 입맛에 맞게 맞춰져 있지는 않다. 이럴때 Scapy 는 간단하게 패킷을 쉽게
다룰 수 있는 방법을 제공해 준다. 이에 몇 가지 사용 예제를 소개해 본다. 예제를 보는 것 만으로도
쉽게 이해가 될 것이며, 이 예제를 기반으로 원하는 프로그램으로 확장하는 것은 그리 어렵지 않을 것이다.
여기 외에, 인터넷을 찾아보면 많은 예제파일들이 있으므로 여기서는 '아 ~ 이현형태로 이용가능하구나' 하는 것만을 인지해 두면 될것 같다.

1. 간단한 ICMP 패킷 보내보기

첫 시작으로 간단하게 ICMP 패킷 보내는 것을 해 보자. 우선, Scapy 모듈을 사용하기 위해서는
모듈을 불러들여야 한다.

from scapy.all import sr1, IP, ICMP

위와 같이 하여 sr1, IP, ICMP 만을 로드하였다. 물론 import * 로 하여 전체 관련된 것을 모두 로드할 수 도 있다. import sys 는 실행할때 인자로 받아들일 값을 사용하기 위해 sys 모듈을 로드하였고,
packet 변수에 IP()/ICMP() 와 같이 하여 IP 와 ICMP 헤더를 넣어주었다. 이때 IP 헤더에는 인자로 받은 값을 넘겨주어 목적지 주소를 지정한 것이다. show() 는 각 구성 형태를 세부적으로 보여주며, sr1 은 패킷 한개를 발송하도록 정의한 것이다.

#!/usr/local/bin/python
# PacketInside.com - Scapy Example
import sys
from scapy.all import sr1,IP,ICMP

packet=IP(dst=sys.argv[1])/ICMP()
print "+-------- Sending Packet INFO"
packet.show()
result = sr1(packet)
if result:
    print "+-------- Receiving Packet INFO"
    result.show()

발송하기 전에 패킷 구성 형태를 packet.show() 를 통해 한번 보여주고, 패킷 발송후에 성공했으면
전달 받은 패킷 데이터를 또 보여주는 간단한 구조다. 다음과 같이 실행해 본다.

# ./icmp.py google.com
WARNING: No route found for IPv6 destination :: (no default route?)
+-------- Sending Packet INFO
###[ IP ]###
  version   = 4
  ihl       = None
  tos       = 0x0
  len       = None
  id        = 1
  flags     =
  frag      = 0
  ttl       = 64
  proto     = icmp
  chksum    = None
  src       = 192.168.0.240
  dst       = Net('google.com')
  \options   \
###[ ICMP ]###
     type      = echo-request
     code      = 0
     chksum    = None
     id        = 0x0
     seq       = 0x0
Begin emission:
.Finished to send 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
+-------- Receiving Packet INFO
###[ IP ]###
  version   = 4L
  ihl       = 5L
  tos       = 0x0
  len       = 46
  id        = 56007
  flags     =
  frag      = 0L
  ttl       = 49
  proto     = icmp
  chksum    = 0x6d5f
  src       = 74.125.53.147
  dst       = 192.168.0.240
  \options   \
###[ ICMP ]###
     type      = echo-reply
     code      = 0
     chksum    = 0x0
     id        = 0x0
     seq       = 0x0
###[ Raw ]###
        load      = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
 
google.com 으로 ICMP 패킷을 보내고 전달받은 결과를 보여준다. 와이어샤크로 해당 패킷을 덤프해 보면
아래와 같이 제대로 패킷이 전송된 것을 알 수 있다.


만약 위 packet 변수에 아래와 같은 값을 입력하면 어떨까?

packet=IP(dst=sys.argv[1])/TCP(sport=4321,dport=80)/Raw(load="GET /index.html HTTP/1.0 \n\n")

의미는 간단해 보인다. TCP 출발지 포트를 4321, 목적지 포트를 80 으로 하고 페이로드에는 /index.html 을 요청하는 것을 집어 넣은 것이다. 하지만 이렇게 바꾸고 실행하면 오류가 발생한다. Scapy 모듈을 로드시에 IP,ICMP 만 했기 때문이다. 모듈 로드를 아래와 같이 해 주어야 한다.

from scapy.all import sr1,IP,TCP,Raw

2. ARP 패킷 모니터링

네트워크 인터페이스에서 ARP 트래픽만을 모니터링 하는 코드를 보자. 앞서 소개한 Scapy 에서 패킷 덤프를 할때 sniff 를 사용 한다고 했다. sniff() 를 통해 패킷 덤프를 하되, filter 를 통해 arp 프로토콜로만 한정을 한다.
#!/usr/local/bin/python
from scapy.all import *

def arp_monitor_callback(pkt):
    if ARP in pkt and pkt[ARP].op in (1,2):
        return pkt.sprintf("%ARP.hwsrc% %ARP.psrc%")

sniff(prn=arp_monitor_callback, filter="arp", store=0)

arp_monitor_callback 이라는 함수를 정의하고 넘어온 pkt 에서 ARP 가 있으면 하드웨어 맥 주소와 출발지 주소를 sprintf 로 리턴해 주어 출력하도록 한 것이다. ARP 이외도 ICMP, TCP 등 여러분들이 원하는 형태로 지정만 하면 출력이 가능해 지는 것이다. 위 코드의 실행결과는 아래와 같다:

# ./arp.py
WARNING: No route found for IPv6 destination :: (no default route?)
00:e0:4d:41:fc:XX 192.168.0.221
00:01:36:2e:0b:XX 192.168.0.200
00:01:36:2e:0b:XX 192.168.0.200
00:e0:4d:41:fc:XX 192.168.0.221

3. 패킷 추출정보 파일로 저장하기
 
지정한 파일을 읽어들이고 Payload 부분만 저장해 보자. 이때 출발지 주소가 192.168.0.240 인 IP 로만 한정을 한다. 각 세부 동작내용은 아래 주석을 참고하자

#!/usr/local/bin/python
from scapy.all import *

src="192.168.0.240" # 출발지 IP 정의
pcap_file = "test3.pcap" # 읽어들일 PCAP 파일 정의
pcap = rdpcap(pcap_file) # rdpcap() 을 통해 pcap_file 을 읽어들임

data = "" # data 변수 초기화
for packet in pcap: # 읽어들인 pcap 파일을 루프돌면서 처리
  ilayer = packet.getlayer("IP") # getlayer 를 통해 IP 레이어를 ilayer 에 저장
  if ilayer.src != src: # 출발지가 192.168.0.240 이 아닌 경우 Skip
    continue
  tlayer = packet.getlayer("TCP") # TCP 레이어를 tlayer 에 저장
  if packet.getlayer("Raw"): # Raw 영역이 존재하면, payload 를 data 에 추가
  data += str(tlayer.payload)
f = open("raw_data.dat", 'w') # raw_data.dat 를 오픈하고 기록한다.
f.write(data)
f.close()

실행을 해 보면 raw_data.dat 가 만들어지고, 이 파일에는 Payload 만 저장된 것을 확인할 수 있다.

# hexdump -C raw_data.dat | more
00000000  47 45 54 20 2f 20 48 54  54 50 2f 31 2e 31 0d 0a  |GET / HTTP/1.1..|
00000010  48 6f 73 74 3a 20 67 6f  6f 67 6c 65 2e 63 6f 6d  |Host: google.com|
00000020  0d 0a 55 73 65 72 2d 41  67 65 6e 74 3a 20 4d 6f  |..User-Agent: Mo|
00000030  7a 69 6c 6c 61 2f 35 2e  30 20 28 58 31 31 3b 20  |zilla/5.0 (X11; |
00000040  55 3b 20 4c 69 6e 75 78  20 69 36 38 36 3b 20 65  |U; Linux i686; e|
00000050  6e 3b 20 72 76 3a 31 2e  39 2e 30 2e 31 39 29 20  |n; rv:1.9.0.19) |
00000060  47 65 63 6b 6f 2f 32 30  30 38 30 35 32 38 20 45  |Gecko/20080528 E|
00000070  70 69 70 68 61 6e 79 2f  32 2e 32 32 0d 0a 41 63  |piphany/2.22..Ac|
00000080  63 65 70 74 3a 20 74 65  78 74 2f 68 74 6d 6c 2c  |cept: text/html,|
00000090  61 70 70 6c 69 63 61 74  69 6f 6e 2f 78 68 74 6d  |application/xhtm|
000000a0  6c 2b 78 6d 6c 2c 61 70  70 6c 69 63 61 74 69 6f  |l+xml,applicatio|
000000b0  6e 2f 78 6d 6c 3b 71 3d  30 2e 39 2c 2a 2f 2a 3b  |n/xml;q=0.9,*/*;|
000000c0  71 3d 30 2e 38 0d 0a 41  63 63 65 70 74 2d 4c 61  |q=0.8..Accept-La|
...(생략)

즉, 필요한 부분만 얻어내서 파일로 만들 수도 있다. 패킷파일의 모든 부분을 출력시키거나 쉽게 저장할 수도 있으며, getlayer() 를 통해 원하는 레이어 영역으로 쉽게 접근할 수 있다.

4. HTTP 패킷을 처리하는 프로그램 형태를 만들어 보자.

조금 더 프로그램 형태를 갖춰서 HTTP 정보를 얻어내는 코드를 살펴보도록 하겠다. 여기 예에서는 기본적인 부분에 대해서 설명하고 있는 것이므로, 응용만 하면 원하는 형태로 쉽게 핸들링이 가능해진다. 이런식으로 사용할 수 있다는 것을 보여주기 위해 작성된 것이므로, 여러분들이 다양하게 응용해 보기 바란다.

#!/usr/local/bin/python
# PacketInside.com - Examples
import sys
from scapy.all import *

CNT = 10 # CNT 에 10 을 지정

def run(target):

    try:
        pkt = rdpcap(target, count=CNT) # 패킷파일을 오픈할때 10 개 까지만 읽어들인다.
    except MemoryError: # 패킷파일을 오픈하다 메모리 에러가 발생하는 경우 Exception 처리
        print "Sorry - Memory Error"
        sys.exit() # 메모리 에러를 출력하고 프로그램 종료
    numPkt = len(pkt) # pkt 의 길이를 numPkt 에 저장

    print "Analyzing : " + target
    print "Total Packets: %d\n" % numPkt # 전체 패킷의 Count 를 출력, rdpcap() 에서 count 를 사용하지 않을시는 전체 패킷 건수를 표시하나 카운트를 10으로 제한했으므로 항상 10이 됨

    for packet in pkt:
        layer = packet.payload
        while layer: # 각 레이어 별로 루프 돌면서 처리
            layerName = layer.name # 레이어 이름을 layerName 에 저장
            if layerName == "IP": # 만약 레이어 이름이 IP 인 경우 다음 내용 수행
                print "[IP Layer]\nSRC: %s, DST: %s" % (layer.src, layer.dst)
# 출발지 와 목적지 주소를 출력 함

            if layerName == "TCP": # 레이어가 TCP 인 경우
                print "[TCP Layer]\nSPORT: %s, DPORT: %s" % (layer.sport, layer.dport)
            if layerName == "Raw": # 레이어가 Raw 인 경우 ( Payload 가 됨)
                result = processHTTP(layer.load) # Raw 가 있는 경우 processHTTP 를 호출함
                if result != "Error": # 넘어온 결과가 Error 인 경우 에러 결과를 출력
                    print result[0] + " " + result[1]

            layer = layer.payload

def processHTTP(data):

    str_method = ""
    str_uri = ""

    # 정규표현식을 통해 넘어온 데이터에서 METHOD, URI, HTTP 버전 정보등으로 구분함
    h = re.search("(?P<method>(^GET|^POST|^PUT|^DELETE)) (?P<uri>.+) (?P<version>.+)", data)
    if not h: return "Error" # 정규표현식에 해당하는 데이터가 없는 경우 Error 를 리턴해줌

    # method 로 정의된 부준은 str_method 에 저장
    if h.group("method"): str_method = h.group("method")
    # URI 데이터는 str_uri 에 저장
    if h.group("uri"): str_uri = h.group("uri")

    return str_method,str_uri # method 와 uri 를 리턴해 줌

# call run
run("tt.pcap") # run 함수를 호출, 이때 읽어들일 pcap 파일을 같이 전달


실행된 결과는 아래 화면과 같다 :

패킷을 분석할 데이터가 많은 경우에는 클래스와 함수를 적절히 사용하여 프로그램을 작성하면, 더욱 효과적으로 사용할 수 있을 것이다.

여기서는 소개하는데 한계가 있기 때문에, 이 정도 선에서 마무리 하고자 한다. 기본적인 것들은 다 언급하였기 때문에 파이썬에 익숙한 분들이라면 어렵지 않게 패킷분석 프로그램을 만들 수가 있다. 물론, 분석 대상의 범위에 따라서 달라지겠지만 Scapy 모듈만을 이용해서 뚝딱 원하는 형태로 패킷을 다룰 수 있으므로, 유용한 도구임에는 틀림없다.  

세번째 소개 글은 프로그램 예제를 만들고 검증하다 보니, 여러분들한테 소개하기까지 늦어졌다. 요 근래, 필자를 괴롭히는 귀차니즘이 한 몫하기도 했다. :-)

Scapy 를 이용하려는 분들에게 조금이라도 도움이 되길 바라며 Scapy 소개를 끝마친다.

From Rigel.

댓글 3개:

  1. 안녕하세요
    wireshark에서 패킷을 스니핑하고 pcap파일로 만들었습니다.
    이것을 open하려고 하는데 scapy에서 rdpcap()이나 open으로 열려고 해도 열리지 않습니다.
    여는 방법에 대해서 안내해주실 수 있나요?

    답글삭제
    답글
    1. 어떤 에러메시지가 발생하나요? 만들어진 pcap 파일이 제대로 된 패킷파일인지 다시 한번 오픈 또는 검증해 보시고요. 이외 다른 환경적 요인들은 없는지도 보세요. 퍼미션 또는 파일 포맷형태를요. 기본 저장될때 pcap-ng 로 저장되었다면 한번 pcap 으로 다시 저장해서 로드해 보시기 바랍니다.

      삭제
  2. pcap 파일은 어디서 얻을수있나요?

    답글삭제