Sniffing - IP, ICMP Sniffer

KyungH·2025년 1월 18일
0

Cyber-Security

목록 보기
26/27

📝 패킷 스니핑

스니핑은 네트워크 패킷을 가로채서 분석하는 해킹 기법을 말한다.
관리자가 네트워크를 모니터링 하는데 사용할 수 있으나, 악의적인 사용자는 스니핑 도구를
이용하여 중요 정보를 감시하거나 탈취하는 데 사용할 수 있다.

네트워크 소캣이란 네트워크 통신에 있어 시작점이자 종착점으로, 클라이언트와 서버 모두
소캣을 가지고 있으며, 소캣을 통해 서로 데이터를 교환한다. 네트워크를 통한 컴퓨터 사이의
통신은 거의 IP 기반이므로 네트워크 소캣은 대부분 인터넷 소캣이다.

소켓의 종류에는 TCP 소켓, UDP 소켓, RAW 소켓이 있는데 우리가 구현할 스니핑 도구는
Raw 소켓(라우터나 네트워크 장비레서 활용되는 네트워크 소켓)을 활용한다.


📌Packet Sniffer

다음 코드는 실행되는 컴퓨터에 송수신되는 패킷 하나를 가로채서 그 내용을 출력한다.

from socket import *
import os


def sniffing(host):
    if os.name == "nt": # 윈도우인 경우
        socket_protocol = IPPROTO_IP
    else:
        socket_protocol = IPPROTO_ICMP

    sniffer = socket(AF_INET, SOCK_RAW, socket_protocol)
    sniffer.bind((host, 0))
    sniffer.setsockopt(IPPROTO_IP, IP_HDRINCL, 1)

    if os.name == "nt": # 윈도우인 경우
        sniffer.ioctl(SIO_RCVALL, RCVALL_ON)
    packet = sniffer.recvfrom(65565)
    print(packet)


def main():
    host = gethostbyname(gethostname())
    print("START SNIFFING at [%s]" % host)
    sniffing(host)


if __name__ == "__main__":
    main()

먼저 소켓 활용을 위해 socket 모듈과 컴퓨터의 OS 종류 확인을 위한 os 모듈을 가져오고,
sniffing(host) 메소드는 Raw 소켓을 생성한 뒤 인자로 입력된 host와 바인드하고
소켓 옵션으로 IP 헤더를 포함하여 수신을 설정한다.

윈도우인 경우 IPPROTO_IP를 아닌 경우에 IPPROTO_ICMP를 사용한 이유는 윈도우가
프로토콜에 관계없이 들어오는 모든 패킷을 가로채기 때문에 IP를 지정해도 무관하나,
유닉스나 리눅스는 ICMP를 가로채겠다는 것을 명시적으로 표시해야 하기 때문이다.

sniffer = socket(AF_INET, SOCK_RAW, socket_protocol)
    sniffer.bind((host, 0))
    sniffer.setsockopt(IPPROTO_IP, IP_HDRINCL, 1)

socket_protocol으로 지정된 프로토콜을 이용하는 Raw 소켓을 만들고 호스트와 바인드한다.
바인드란, 서버 프로그램에서 소켓을 통해 들어오는 네트워크 패킷을 수신할 준비를 말하며
전기 플러그를 소켓과 연결하는 것 이라고 생각하면 된다.

if os.name == "nt": # 윈도우인 경우
        sniffer.ioctl(SIO_RCVALL, RCVALL_ON)

윈도우의 경우 소켓을 promiscuous 모드로 변경하여 호스트에 전달되는 패킷을 수신한다.
이 모드를 설정하지 않을 경우 구동되는 컴퓨터가 목적지가 아닌 패킷은 모두 버리게 된다.

recvfrom(65565) 는 소켓으로 패킷이 들어올 때까지 대기하며, 65565는 버퍼의 크기다.

def main():
    host = gethostbyname(gethostname())
    print("START SNIFFING at [%s]" % host)
    sniffing(host)

socket 모듈의 gethostname()은 현재 호스트의 이름을 리턴한다.
이후 gethostbyname()은 호스트 이름을 IPv4 형식으로 바꾼다.

패킷이 스니핑 되면 결과를 튜플로 리턴하고 튜플의 첫 번째 멤버는 바이트 코드로 되어있어
사람이 알아보기 쉽게 바꾸어야 한다.

📌IP Header Sniffer

def recvData(sock):
    data = ""
    try:
        data = sock.recvfrom(65565)
    except timeout:
        data = ""
    return data[0]
count = 1
    try:
        while True:
            data = recvData(sniffer)
            print("SNIFFED [%d] %s" % (count, data[:20]))
            count += 1
    except KeyboardInterrupt:  # Ctrl+C key input
        if os.name == "nt":
            sniffer.ioctl(SIO_RCVALL, RCVALL_OFF)

recvData 메소드를 통해 수신한 패킷의 첫 번째 멤버를 리턴하여 바이트 코드를 해석한다.

IP 패킷의 구조를 살펴보면, Options 필드를 무시한 IP 헤더의 크기는 20바이트로 구성된다.
따라서 스니핑 결과의 첫 번째 멤버인 바이트 코드의 앞 부분 20바이트는 IP 헤더이므로
이를 참조하여 출력한다.

data[:20]은 IP 헤더를 말하며
IP 헤더의 각 필드는 struct 모듈의 unpack()을 활용하면 손쉽게 추출할 수 있다.
네트워크를 통해 송수신되는 데이터는 바이너리 데이터인데 이 데이터에 struct 모듈을
활용하면 파이썬에서 다루는 자료형으로 편리하게 변환한다.

import struct

def parseIPHeader(data):
    header = struct.unpack("!BBHHHBBH4s4s", data[:20])
    return header

header = struct.unpack("!BBHHHBBH4s4s", data[:20]) 구문은 data[:20]을 첫 번째
인자인 포맷 문자열에 맞게 변환한 후 튜플로 리턴한다.
사용하는 포맷 문자의 의미는 다음과 같다

struct 모듈 포맷 문자의미크기(바이트)
!네트워크 바이트 순서-
Bunsigned char1
Hunsigned short2
4schar[4]4

즉 위의 포맷 문자열은 네트워크 바이트 순서로
1, 1, 2, 2, 2, 1, 1, 2, 4, 4 바이트로 구분하여 튜플로 리턴한다.

위에서 얻은 header를 이용하여 다양한 정보를 출력할 수 있다.

def getDatagramSize(ipHeader):
    return ipHeader[2]


def getProtocol(ipHeader):
	# 프로토콜 번호는 1 - ICMP / 6 - TCP / 17 - UDP 로 정해져 있다 
    protocols = {1: "ICMP", 6: "TCP", 17: "UDP"}
    proto = ipHeader[6]
    if proto in protocols:
        return protocols[proto]
    else:
        return "OTHER"


def getIP(ipHeader):
	# 바이트 문자열을 우리가 익숙한 IP 주소 형식으로 변환하는 함수 inet_ntoa()
    src_ip = inet_ntoa(ipHeader[8])
    dest_ip = inet_ntoa(ipHeader[9])
    return (src_ip, dest_ip)

📌ICMP Sniffer

ICMP 메시지는 IP 네트워크에서 진단이나 제어 용도로 사용되며, 오류에 대한 응답으로
생성된다. ping 메시지 요청 및 이에 대한 응답이 ICMP 메시지를 이용한다.

ICMP 스니퍼는 ICMP 헤더 필드를 분석하여 우리가 원하는 값을 출력한다.
ICMP 헤더는 8바이트로 구성되며, 첫 번째 바이트인 타입과 두 번째 코드가 주목할 부분이다.
ICMP 메시지의 구조는 IP 헤더 뒤에 ICMP 헤더가 추가된 모습이다.

즉, IP 헤더 크기만 알면 ICMP 헤더의 시작 부분을 알 수 있다.

def getIPHeaderLength(ipheader):
    ipheaderlen = ipheader[0] & 0xF
    ipheaderlen *= 4
    return ipheaderlen

위에서 추출한 IP 헤더에서 ipheader[0]은 1바이트,
Version(4비트) + Header Length(4비트)를 말한다. 이 값에 0xF를 AND 연산하여
Header Length만 추출한다.

IP 헤더길이 필드는 4비트로, 단위는 32비트(4바이트) 단위이다.
즉, 헤더길이를 4바이트 묶음의 개수로 표시한다고 생각하면 쉽다.

값이 5라면, 5개의 32비트 묶음이 있으므로(=20비트).
이를 생각하면 Header Length에 4를 곱하여 바이트 단위로 변환할 수 있다.

ICMP 헤더가 시작하는 위치를 알았으므로 ICMP 타입과 코드값만 추출하는 함수를 구현한다.

def getTypeCode(icmp):
	# !BB -> 1바이트 단위로 추출
    icmpheader = struct.unpack("!BB", icmp[:2])
    icmpType = icmpheader[0]
    icmpCode = icmpheader[1]
    return (icmpType, icmpCode)

이후 윈도우 커맨드 창에서 ping 명령을 수행한 후 ICMP 스니퍼를 실행하면
ICMP 헤더를 스니핑한 결과를 출력하며, ICMP 타입 8ICMP Echo Request를 의미하며,
타입 0ICMP Echo Reply를 의미한다.


📌Host Scanner

해킹하고자 하는 시스템이 동작하고 있는지 확인하는 것은 매우 중요하다.
ICMP 스니핑을 활용하여 해당 호스트가 살이있는지 확인할 수 있다.

ICMP 메시지 중 타입 값이 3인 것을 Destination Unreachable이라고 하며,
네트워크 상에서 송신자에게 특정 목적지에 도달할 수 없는 상황을 알리기 위해 사용된다.

이 중에서 코드 값이 3인 것을 Port Unreachable이라 하며, 이는 UDP 프로토콜에서
사용자가 보낸 패킷이 도달하였으나, 해당 포트에서 처리할 애플리케이션이 없을 대
대상 호스트가 응답으로 보내는 ICMP 메시지이다.

스캐너가 목표 호스트에게 임의의 포트로 데이터가 없는 UDP 패킷을 보낸 후,
Port Unreachable 응답을 받을 경우 호스트가 활성화 되어있다는 뜻이며,
호스트가 비활성화 되어있을 경우 응답이 오지 않는다.

from socket import *
from netaddr import IPNetwork, IPAddress


def sendMsg(subnet, msg):
    # UDP socket
    sock = socket(AF_INET, SOCK_DGRAM)
    for ip in IPNetwork(subnet):
        try:
            print("Sending to %s" % ip)
            sock.sendto(msg.encode("utf-8"), ("%s" % ip, 9000))
        except Exception as e:
            print(e)


def main():
    host = gethostbyname(gethostname())
    subnet = host + "/24"
    msg = "KNOCK!KNOCK!"
    sendMsg(subnet, msg)


if __name__ == "__main__":
    main()

UDP 소캣을 생성하여 서브네트워크의 모든 IP 주소로 보낸다.
IPNetwork(subnet)은 서브네트워크의 모든 IP 주소를 담고있으며, subnet은 접두사를
활용하여 subnet = host + '/24'와 같이 표현한다.

9000번 포트로 메시지를 전송하고 sendto()에 유니코드 메시지는 오류가 발생하므로
UTF-8로 인코딩하여 전달한다.

우리가 보낸 ICMP 메시지의 내용을 확인하기 위해
위에서 만든 ICMP 스니퍼를 이용하여 ICMP 타입과 코드가 각각 3, 3인 경우의
ICMP 메시지를 확인하면 된다.

KNOCK!KNOCK!의 길이는 12 이므로 data[-12:]의 값을 확인하면 된다.


📌Message Sniff

실제 내용을 가로채기 위해서는 IP 헤더 분석을 통해 어디서 어디로 향하는 정보인지 확인하여
내용만 가로채서 분석을 통해 시스템의 취약점이나 허점을 알아낸다.

메시지를가로채서 효율적으로 분석하려면 분석을 원하는 메시지만 스니핑 하는 것이 좋다.
공격자가 메일 내용을 가로채서 분석하고자 하는 경우,
메일 서버로부터 오가는 정보만 추출하여 분석하는 것이 효율적이다.
메일을 위한 프로토콜인 SMTP, POP3, IMAP은 각각 25, 110, 143 번 포트를 사용한다.

만약 웹을 통해 오가는 정보만 추출하여 분석하고자 하면 80번 포트를 확인하면 된다.

구현은 파이썬의 Scapy 패키지를 이용하여 쉽게 패킷을 스니핑 할 수 있다.

from scapy.all import sniff, TCP, IP

def showpacket(pkt):
    # TCP 헤더를 제외한 실제 데이터만 출력
    data = "%s" % (pkt[TCP].payload)
    if "user" in data.lower() or "pass" in data.lower():
        print("+++[%s]: %s" % (pkt[IP].dst, data))

def main(filter):
	# filter: 원하는 패킷만 볼 수 있는 필터 지정
    # prn: 캡처한 패킷을 처리하기 위한 함수 지정
    # count: 패킷을 캡처하는 횟수 지정 (0이면 사용자가 중지할 때까지)
    # store: 캡처한 패킷을 저장할 지 여부 (0이면 네트워크 모니터링만 진행)
    sniff(filter=filter, prn=showpacket, count=0, store=0)

if __name__ == "__main__":
    filter = "tcp port 25 or tcp port 110 or tcp port 143"
    main(filter)

위 코드는 메일 서버가 사용하는 포트를 통해 오고가는 TCP 정보만 가로채고
TCP를 통해 전송되는 메시지에 'user' 나 'pass'라는 단어가 있으면 화면에 출력한다.


0개의 댓글

관련 채용 정보