만들면서 이해하는 네트워크 공격 #2 포트 스캐닝

Eric·2021년 12월 17일
0
post-thumbnail

해당 시리즈는 방주원 저자의 네트워크 공격패킷 분석 책의 내용을 기반으로 합니다.

포트 스캐닝이란?

포트 스캐닝은 특정 호스트에서 열려있는 포트들을 확인하는 행위입니다. 포트 스캐닝을 통해 서버에서 구동중인 서비스들(ex. http/https, mysql, ssh 등..)을 확인할 수 있으며, 공격자들은 이를 이용해 취약점이 있을 법한 서비스를 파악하고 공격을 수행할 수 있습니다.

포트 스캐닝을 통해 가능한 시나리오

  • 포트 스캐닝을 통해 특정 포트에서 SSH가 구동 중임을 확인하였다.
    • 공격자는 해당 포트를 상대로 무차별 대입 공격을 수행해 서버의 권한을 탈취할 수 있었다.
  • 기타 등등..

포트 스캐닝의 방법들

TCP

  • TCP Open

    • TCP는 연결할 때 3-way 핸드쉐이킹 방식(동기화, 동기화-승인, 승인)을 사용하는데, 이를 이용한 포트 스캐닝입니다.
    • 경우의 수 :
      • 포트가 열려 있을 때 : SYN -> SYN-ACK -> ACK
      • 포트가 닫혀 있을 때 : SYN -> RST-ACK
      • 포트가 방화벽 등에 막혀 있을 때 : SYN -> 응답없음
    • 위와 같은 경우의 수를 이용해, 포트가 열렸는지를 확인할 수 있습니다.
  • Stealth Scan

    • 눈에 띄지 않고 포트를 스캔할 수 있는 기법(사실 PCAP나 wireshark 같은 도구들을 통해 탐지가 어느 정도 가능하기는 합니다)
    • 종류 :
      • TCP Half Scan : 일반적인 TCP Open 기법에서 핸드쉐이킹 마지막 과정인 ACK를 보내지 않아, 세션 수립을 막고 로그에 찍히는 걸 방지하는 방식
      • FIN Scan : 연결되지도 않은 타겟에 FIN(연결 종료) 플래그를 보내 반응 여부를 확인하는 기법
        • 포트가 열려있을 때 : 연결된 적이 없으므로 미응답(다만 방화벽에 막혀도 미응답이기 때문에 유의해야 함!)
        • 포트가 닫혀있을 때 : Tcp Open처럼 RST-ACK가 반환된다.
      • 기타 등등..

UDP

  • UDP Open

    • UDP는 3-way 핸드쉐이킹을 사용하지 않는 통신이지만, ICMP라는 프로토콜이 대신 포트가 닫혀있음을 알려주므로 이를 통해 포트의 오픈 여부를 확인할 수 있습니다.

포트 스캐닝 구현해보기

주의사항: 허가받지 않은 서버를 상대로 하는 포트 스캐닝은 엄연한 침입행위입니다

이번 게시글에서는 비교적 구현이 쉬운 TCP Half Scan을 구현해 보도록 하겠습니다.
앞으로 제가 구현한 코드들은 전부 제 깃허브에서 확인하실 수 있습니다.
🔗 Github에서 전체 코드 확인하기

해당 게시글은 winpcap와 pcap4j가 설치되어 있다는 가정 하에 작성됩니다.

1. pcap 핸들러와 listener 생성

먼저 패킷을 수신하고 발신할 수 있는 핸들러를 생성해야 합니다.

val nif = NifSelector().selectNetworkInterface() // 네트워크 인터페이스 선택
val handle = nif.openLive(65536, PcapNetworkInterface.PromiscuousMode.PROMISCUOUS, 1000) // 핸들러 생성

pcap4j에서는 사용자가 손쉽게 네트워크 인터페이스를 선택할 수 있도록 위와 같은 선택창을 지원합니다. (사진에서 나온 인터페이스들은 다 내부망 주소라서 그냥 모자이크를 생략했습니다)

2. SYN 전송 코드 작성

이후 클라이언트에게 SYN 패킷을 보내야 하기 때문에, 이를 위한 TCP 패킷을 작성해 주어야 합니다.

// SYN 요청을 위한 TCP 패킷 생성(전송 계층)
    val tcpSynPacket = TcpPacket.Builder()
        .syn(true) // flag를 SYN으로 설정
        .sequenceNumber((Math.random() * 100000000).toInt()) // 시퀀스 넘버는 랜덤으로
        .srcAddr(myIp).srcPort(TcpPort.getInstance((50000 + Math.random() * 9000).toInt().toShort())) // 공격자의 주소와 포트 설정
        .dstAddr(targetIp).dstPort(TcpPort.getInstance(port)) // 타겟의 주소와 포트 설정
        .correctChecksumAtBuild(true) // 체크섬 자동생성
        .correctLengthAtBuild(true) // 길이 자동설정
        .window(1024) // window size는 nmap과 동일하게 설정
        .paddingAtBuild(true) // padding 자동 설정
        .options(listOf( // 단편화 최대크기 설정(nmap과 동일하게 설정)
            TcpMaximumSegmentSizeOption.Builder().maxSegSize(1460).length(4).correctLengthAtBuild(true).build()
        ))

실제 코드에는 TCP를 페이로드로 삼을 IPv4와 Ethernet(데이터링크 계층)의 패킷 생성도 필요하지만, 여기에 작성할 경우 가독성도 떨어질 뿐더러 이번 주제와는 관련성이 낮은 내용이기 때문에 제외하였습니다. (코드 전체는 앞서 말씀드린 것처럼 Github를 참고해 주세요)

3. SYN/RST-ACK 수신 코드 작성

앞선 과정으로 SYN 패킷을 전송하였다면, 이제 타겟이 반환하는 패킷의 결과를 분석하여야 합니다.

해당 코드 역시 중요한 부분만 작성하였습니다.

fun isOpen(packet: Packet, targetIp: Inet4Address, targetPort: Short): OpenState {
    if(!packet.contains(TcpPacket::class.java)) return OpenState.NOT_THIS_PACKET // TCP 패킷이 아니면 리턴

    val ipPacket = packet.get(IpV4Packet::class.java)
    val tcpPacket = packet.get(TcpPacket::class.java)

    if(ipPacket.header.srcAddr != targetIp) return OpenState.NOT_THIS_PACKET
    if(tcpPacket.header.ack && tcpPacket.header.srcPort.value() == targetPort) { // ACK OOO이여야 여부 확인 가능
        return if(tcpPacket.header.syn) { // SYN-ACK
            OpenState.OPEN
        }else if(tcpPacket.header.rst) { // RST-ACK
            OpenState.CLOSED
        }else { // 기타 경우인 경우
            OpenState.NOT_THIS_PACKET
        }
    }else{
        return OpenState.NOT_THIS_PACKET
    }
}

작성 중 발생했었던 문제

초반에 코드를 작성한 뒤, Wireshark를 통한 테스트 및 결과를 확인해 본 결과 SYN 패킷은 정상적으로 발신되지만 SYN-ACK가 수신되지 않는 것이 확인되었습니다. 이 말인 즉슨, 타겟이 공격자의 요청을 수신하지 못한다는 의미였습니다.

이 문제를 해결하기 위해 nmap과의 작동 비교는 물론, 몇 시간에 걸친 구글링까지 하게 되었습니다만, 사실 알고보니 조금 어이없는 것이 원인이었습니다.

바로 체크섬 문제였습니다. TCP 단에서의 체크섬에는 자동생성을 설정해 놓았지만, IPv4 단에서는 설정을 빼먹은 것(...) 때문에 작동하지 않은 것입니다.

작동 시연

  1. 네트워크 인터페이스를 선택합니다.
  2. 본인의 IP와 타겟의 IP를 작성합니다.
  3. 오픈 여부를 알고 싶은 포트를 입력해 확인합니다.
    • 위와 같이 포트를 조회한 경우, Wireshark에서는 다음과 같은 TCP 요청/응답들을 확인할 수 있습니다.

포트 스캐닝을 막는 법

포트 스캐닝을 막는 방식은 다른 공격들에 비해 비교적 간단하다고 말씀드릴 수 있습니다.

이러한 공격을 막기 위해서는, HTTP/HTTPS와 같이 서비스 포트를 제외한 내부용 포트(mysql 등)는 전부 방화벽에서 차단하는 것이 좋습니다. 일반적으로 대부분의 서비스들은 화이트리스트(whitelist)를 통해 80(HTTP), 443(HTTPS)등의 포트만 외부 접근을 허용하고 있습니다.

마무리하며

어쩌면 간단하다고도 할 수 있는 첫번째 네트워크 공격의 구현부터 어려운 난관에 봉착할 뻔했는데요, 그래도 무사히 잘 완성해서 다행인 거 같습니다.

다음 시간에는 DDoS 공격에 사용되는 패킷에 대해 알아보고, 직접 구현하며 방어법을 파악해보는 시간을 가져보도록 하겠습니다. 감사합니다.

profile
Backend Engineer | 코드로 우리의 세상을 어떻게 바꿀 수 있는지 고민합니다

0개의 댓글