[Python] Raw소켓으로 Ping 보내기 (ICMP 스캐너)
뻘짓

[Python] Raw소켓으로 Ping 보내기 (ICMP 스캐너)

Ping 좀 보내본 사람이라면 아마 ICMP 패킷에 대해 알고 있을 것이다.

Ping 요청과 응답

 ICMP 프로토콜의 구조는 다음과 같은데, 핵심은 Type과 Code 로 내가 수행할 행동을 정의 하는 것이다. nmap 에서는 이런 프로토콜들의 다이어그램을 제공한다.

사랑해요 nmap ! : https://nmap.org/book/tcpip-ref.html

 

 여기서 내가 Type:8, Code:0 으로 ICMP 메세지를 보내면, 이걸 받은 상대는 Type:0, Code:0 으로 응답을 해주게 되고, 이것이 Ping 명령의 실체인 것이다. 덧붙여 tracert 명령같은 경로 추적 기능은 IP헤더의 TTL을 일부러 작게 설정해 경로상에 놓인 장비들이 TTL을 보고 패킷을 드롭 시킬때 ICMP의 TTL Exceed 메세지를 보내는 매커니즘을 이용하게 된다. (Type:11, Code:0)

긴 말 않고 한번 코드를 살펴보자.

import socket
import struct
import sys
import ipaddress
import time
import threading
 
PingResponse = {}
Running:bool
 
def checksum(data) -> int:
    s = 0; n = len(data) % 2
    for i in range(0len(data)-n, 2):
        s+=int.from_bytes(data[i:i+2], byteorder='big')
 
    if n : s+=int.from_bytes(data[i:i+1], byteorder='big'# 잔여 바이트 처리
 
    while (s >> 16):
        s = (s & 0xFFFF+ (s >> 16)
 
    s = ~s & 0xFFFF
    return s
 
def SendICMP(sock:socket.socket, ip:str):
    type = 8; code = 0; csum = 0; icmpid = 0; seq = 0
    TmpData = struct.pack("!BBHHH", type, code, csum, icmpid, seq)
    csum = checksum(TmpData)
    RealData = struct.pack("!BBHHH", type, code, csum, icmpid, seq)
    sock.sendto(RealData, (ip, 0))
 
 
def isPrefix(Mask:int-> bool:
    if Mask<0 or Mask>32 : return False
    return True
 
def Prefix2Range(Mask:int-> int:
    """
    넷마스크를 범위로 바꾼다.\n
    ex : 22 -> 1024
    ex : 24 -> 256
    """
    if isPrefix(Mask)==False : return 0
    inverse = 32 - Mask
    return 1<<inverse
 
def PrintUsage():
    """
    인자를 잘못 넣었을때 출력할 도움말
    """
    print("wrong command.\n"
          "usage : pingtest <ip[/prefix]>\n"
          "ex    : pingtest 8.8.8.8\n"
          "ex    : pingtest 192.168.0.0/24\n")
 
 
def PingListenThread():
    sock = socket.socket(socket.AF_INET,socket.SOCK_RAW, socket.IPPROTO_ICMP)
    sock.bind(("0.0.0.0"0))
    while True:
        data, address = sock.recvfrom(1500)
        if address[0in PingResponse :
            PingResponse[address[0]] = True
 
def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) #ICMP 로우소켓 만들기
    
    """
    if len(sys.argv) != 2: PrintUsage(); exit()
    try:
        address:str = sys.argv[1]
    except:
        PrintUsage()
        exit()
    """
    
    address = "172.30.1.0/24".split("/"# <= input 으로 입력 받아도 되고, 위 코드 주석 해제해서 인자로 받아도 된다.
    if len(address)==2 :
        ip, prefixStr = address
        prefix:int = int(prefixStr)
    else :
        ip = address[0]; prefix:int=32 
    HostRange:int = Prefix2Range(prefix) # 이 대역 안에 총 몇개의 IP 가 있을 수 있는지?
    if not HostRange: PrintUsage(); exit()
 
    mask:int = 0xffffffff & ~((1 <<(32 - prefix)) - 1# 네트워크ID 마스크
 
    RawIP = int(ipaddress.ip_address(ip))
    NetworkID = mask & RawIP
    for HostID in range(HostRange):
        ip = socket.inet_ntoa(int.to_bytes(NetworkID+HostID, 4"big")) # 네트워크 ID와 호스트ID를 합치면 IP가 됨
        PingResponse[ip] = False # 응답 받을 목록 초기화
 
    Listener = threading.Thread(target=PingListenThread, daemon=True#핑 응답 받을 스레드 생성
    Listener.start()
 
    for HostID in range(HostRange):
        ip = socket.inet_ntoa(int.to_bytes(NetworkID+HostID, 4"big")) # 네트워크 ID와 호스트ID를 합치면 IP가 됨
        SendICMP(sock, ip) # Ping 전송
 
    time.sleep(3# 응답 3초 대기 ~~
 
    for tmp in PingResponse :
        tmp:dict
        if PingResponse[tmp]==Trueprint(tmp)
    
main()
quit()
cs

실행 결과

 

 

문제점 # 1 

 결과적으로 보면, 되긴 된다. 현재 코드에서는 IP주소와 대역을 하드코딩 하였지만, 코드의 주석에 쓰여진대로 약간의 수정을 거치면 명령 행 인자로 입력받아 손쉽게 사용 할 수 있다. 

 문제는 이 코드를 윈도우에서 실행하면 여러가지 애로사항이 꽃핀다는 것이다. 이 코드는 ICMP 를 직접 만들어 전송 하게 되는데 이러한 데이터를 전송 하려면 Raw 소켓을 생성해야 한다.

 자... 윈도우와 Raw 소켓? 지난 포스팅 했던 글이 생각난다. https://nitwit.tistory.com/18

 

로우소켓에 대해서...

 소켓 프로그래밍을 배우다보면 로우소켓이라는 신비한 존재를 접하게 된다. 처음 이 로우소켓을 보고나면 재밌는것들이 꽤 많이 떠오른다 TCP - SYN Flood 공격, 공격이 아닌 SYN 스캔, TCP의 다양한

nitwit.tistory.com

 Raw 소켓은 윈도우에서 상상도 못할 방법으로 제약이 걸린다. 가령, Raw 소켓을 열어, ICMP 요청을 의미하는 Type:8, Code:0 셋팅으로 데이터를 보내면 관리자권한이 없어도 정상적으로 패킷이 나간다. 그런데 ICMP 응답을 의미하는 Type:0, Code:0 을 보내려 하면 이때는 관리자권한이 아닌 이상 권한 관련 오류를 내게 된다.

윈도우에서 VSCode 로 ICMP 필드를 원하는 대로 조작해서 테스트 해보고싶다면, VSCode를 관리자 권한으로 실행 해야한다.

 

문제점 # 2

 윈도우는 악성코드의 감염 및 확산을 방지 하기 위해 운영체제 레벨에서 매우 깐깐한 트래픽 관리를 한다. 이 코드를 그대로 실행 하게 되면, 자기 자신의 IP 주소밖에 보이지 않는다. 과연 왜 그런것일까?

윈도우 디펜더는 홈 네트워크가 아니거나, 내가 모르는 IP 로부터 온 데이터를 드랍 시킨다.

 윈도우 기본 방화벽은 커널이 받은 데이터를 응용프로그램으로 넘겨주기 전에 깐깐한 필터링을 한다. 실제로 와이어샤크를 켜서 보면 ICMP Reply 패킷을 잘 온 것을 확인할 수 있지만, 이 프로그램에서는 받지 못한 것 처럼 멀뚱히 아무 동작도 하지 않는다. 따라서 이 방화벽을 전부 내려줘야 제대로 받아 볼 수 있다.

 이 소스의 ICMP 리스너 스레드 코드를 한번 살펴보자.

bind 함수의 인자를 주목하자

 통상, 소켓이라 함은 0번 포트에 bind 시키면 시스템에 흘러다니는 모든 패킷을 전부 볼 수 있다. (이더넷 프레임이나 802.11 은 제외하고). 하지만 파이썬은 이렇게 소켓을 열어도 방화벽을 내리기 전 까진 모든 데이터를 전부 받아오지 않는다. 사실 이러한 현상은 C/C++ 로 짰을때는 전혀 발생하지 않는다. 방화벽을 내리거나 시스템의 옵션을 건들지 않아도 모든 트래픽을 받아올 수 있어야 정상적인 상황이다. (이 현상은 좀 더 연구가 필요 해 보인다.)

 

결론 : 윈도우가 윈도우 했다.