Python/2.7 information

raw 소켓 파악하기 1.

qkqhxla1 2016. 3. 7. 08:12

최근에 개인적인 어플리케이션을 만들어보려고 했는데 네트워크에 대한 깊은 지식이 필요했다. 나름 공부도 열심히 해왔다고 자부했는데 이걸 만들려면 raw 소켓에 대한 지식이 필요하다는걸 알았고, raw 소켓을 파이썬에서 써본 적이 없다는 사실도 깨달았다.(간단히 해결할수있을것같아서 해봤으나 결국 포기. 자료가 별로 없어서 빡셌다.) 시간은 조금 있으니 일단 raw 소켓부터 공부하자는 마음으로 정리.


목표 : 아래의 기본적인 구글로 요청을 보내고 받아오는 tcp코드를 raw 소켓으로 만들어본다.

구글로 요청을 보내고 받아오는 'tcp'소켓 코드.

# -*- encoding: cp949 -*-
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('www.google.co.kr',80))
message = '''GET /?gws_rd=ssl HTTP/1.1
Host: www.google.co.kr


'''
s.sendall(message)
print s.recv(10000)

이걸 raw 소켓으로 구현하려면 어떻게 해야 할까? 패킷 구조 하나하나 다 정의해야 한다. 위의 tcp 소켓 예제는 정말 진짜진짜 편하다는걸 다시 한번 깨달았다. 근데 구글링을 하루죙일 해봤는데 쓸만한 정보가 없다. 그나마 이 주소를 찾았다. http://www.binarytides.com/raw-socket-programming-in-python-linux/

내 컴퓨터에선 작동하지 않는다. (그리고 나중에 알아냈는데 이 코드 이상하다. 리눅스 대상으로 만든 코드 같다. 리눅스에서 돌려보지는 않았는데 상당히 이상하다. 그냥 참고만 할 것.) 지지고 볶고 해봐도 저기서 뭘 변경해야할지 모르겠다. 그래서 일단 더 기본적인 예제부터 하며 차근차근 공부하기로 결심했다.


구글링에서 icmp 패킷을 만들어서 보내는 예제를 찾았다. 일단 그것부터 정리.

https://www.youtube.com/watch?v=R3mDH6DO-gc

윈도우에서 icmp패킷을 192.168.8.1로 보내는 코드이다.

# -*- encoding: cp949 -*-
import socket
import random
import struct

def checksum(data):
    s = 0
    n = len(data) % 2
    for i in range(0, len(data)-n, 2):
        s+= ord(data[i]) + (ord(data[i+1]) << 8)
    if n:
        s+= ord(data[i+1])
    while (s >> 16):
        s = (s & 0xFFFF) + (s >> 16)
    s = ~s & 0xFFFF
    return s

def icmp():
    type = 8
    code = 0
    chksum = 0
    id = random.randint(0,0xFFFF)
    seq = 1
    real_chksum = checksum(struct.pack('!BBHHH', type, code, chksum, id, seq))
    icmp_pkt = struct.pack('!BBHHH', type, code, socket.htons(real_chksum), id, seq)
    return icmp_pkt

s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
s.sendto(icmp(), ('192.168.8.1',0))

코드를 작성하면서 자잘자잘하게 깨달은 점.

1. raw 소켓을 실행하려면 반드시 관리자 모드에서 실행해야 한다. 즉 관리자 cmd창을 열고 수동으로 실행해야 한다. 안그럼 에러뜸. raw소켓이 패킷을 하나하나 조작할수 있어서 관리자권한으로만 실행되게 한거라도 어떤분이 써놓은 글을 봤다.

2. 소켓을 만들때 두번재 인자는 socket.SOCK_RAW이고, 세번째 인자는 패킷에 따라 다른것같다. icmp를 만들기로 했으니 3번째 인자를 icmp로 준것 같다. 더 찾아봤는데 일반 TCP 소켓 생성 시 요렇게 생성한다.

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 원래는 세번째 인자를 줘야하는데 첫번째와 두번째가 저렇게 설정되면 자동으로 tcp소켓으로 만들어진다고 한다. 원래는 세번째 인자로 socket.IPPROTO_IP을 줘야한다고.. 이것저것 찾아보다가 발견했다.

http://stackoverflow.com/questions/24590818/what-is-the-difference-between-ipproto-ip-and-ipproto-raw

3. 패킷을 보낼때 sendto()를 쓴다. tcp소켓에서 문자열 전송시 sendall()을 썼는데 sendto로 직접 주소지정해서 보내야되는것같다.


코드 분석을 위한 icmp패킷 구조다.

여기서 찾을수 있는걸 위 코드에서 다 찾을 수 있다.

icmp()함수에 차례대로 정의가 되어 있다. 마지막에 각각의 데이터들을 struct.pack함수로 변환해주는데, 첫번째 !는 네트워크 데이터, B는 unsigned char, H는 unsigned short를 뜻한다. http://qkqhxla1.tistory.com/325 솔직히 체크섬 계산은 어떻게 저 함수가 나온건지 문서를 봐도 잘 이해가 안간다;. 근데 잘 작동하니 따로 찾아보지는 않고 그냥 쓸 예정...

어쨋든 와이어샤크를 키고, 필터목록에 icmp를 입력해놓은 후, 저 코드를 관리자권한의 cmd에서 실행하면 

정상적인것처럼 보이는 패킷이 가는게 보인다. 그런데 ping 192.168.8.2로 진짜 핑을 실행했을때는 위의 icmp패킷 구조에서 보이듯이 data에 뭔가 내용이 있다. 

참고로 필터는 ip.dst==192.168.8.2 처럼 목적지를 설정할수 있다. ip.src==~~으로 출발지를 찾을수 있다. &&과 ||도 사용 가능하다.

데이터를 추가하려면.. 

ex) 데이터로 Hello world!를 보내고 싶다. 앞에서 struct.pack의 H형식은 unsigned short라고 했었다. 이걸로 보내도 되지만 한번에 긴 데이터를 보내기 위해 가장 큰 Q형식(unsigned long long)을 사용하였다. Hello와 world!로 두개로 나눠서 보냈다. 한번에 보내기에는 Q형식도 작다고 에러가 뜬다.

Hello world!의 hex값은 48656c6c6f20776f726c6421이다.


real_chksum = checksum(struct.pack('!BBHHHQQ', type, code, chksum, id, seq, 0x48656c6c6f, 0x20776f726c6421))

    icmp_pkt = struct.pack('!BBHHHQQ', type, code, socket.htons(real_chksum), id, seq, 0x48656c6c6f, 0x20776f726c6421)


중간 함수 내부의 보내는 부분만 이렇게 추가해보자. 맨 끝에 Hello와 World!의 값을 두개 나눠서 추가했으며, 형식에 Q두개를 추가했다. 이렇게 하고 실행시켜보면

Hello world!가 추가되었다. 패킷을 자세히 살펴보면 중간에 o와 스페이스 사이에 넣지도 않은 널값(00)이 포함되어있는데 이것저것 실험해 본 결과 패킷 사이를 나누어 주기 위한 널값인것같다. (위에 보면 Hello와 World!를 따로보냈음.) 


주의할 점.

위에서 언급했는데 tcp소켓을 직접 하나하나 만들어보려고 하면 대부분 처음으로 찾게되는 쓸만해보이는 사이트는 http://www.binarytides.com/raw-socket-programming-in-python-linux/ 요기다. 요 사이트에 들어가서 대충 훑어보면 raw소켓을 만들면 IP계층, TCP계층을 하나하나 다 만들수있는것처럼 설명이 나와있다. 그래서 IP헤더도 만드는게 나오고 그러는데 일단 내가 윈도우에서 실험해본결과 아니다.

위에 올린 icmp()함수를 보자. icmp()함수에는 icmp를 위한 헤더와 데이터값만 나와있다. 그리고 sendto()로 보내니까 위의 와이어샤크에서 볼수 있듯이 알아서 IP계층에 대한게 정의가 되어있다. 그러니까 결론은 내가 TCP를 만들고 싶다 하면 저 사이트처럼 IP헤더 뭐뭐 다 생각할 필요없이 그냥 TCP헤더만 정의해서 보내면 알아서 만들어질거라는 것이다. 이거때문에 몇시간을 날려먹은지 모르겠다 ㅡㅡ


다음글 : http://qkqhxla1.tistory.com/533