Windows는 RDP 세션에 대해 어떤 주소를 보고합니까?
WTS API는 하나의 RDP 세션에 대해 보고된 클라이언트 주소를 반환하며 레지스트리는 그렇지 않습니다. 올바른 세션 ID를 조회하고 IPv4 또는 IPv6를 정확히 디코딩하십시오.
RDP 세션에는 클라이언트 주소가 필요하지만 분명해 보이는 출처들은 서로 다릅니다. CLIENTNAME은 컴퓨터 이름을 주고, 레지스트리는 다른 세션의 값을 담고 있으며, 소켓의 상대편은 클라이언트가 아니라 게이트웨이나 NAT 주소일 수 있습니다. Windows는 세션별로 보고된 클라이언트 주소를 제공하지만, 올바른 세션을 조회하고 응답을 정확히 디코딩해야 합니다.
조회를 하나의 세션에 고정하십시오
사람이 아니라 세션에서 시작하십시오. Windows RDP 호스트는 한 계정에 여러 세션을 담을 수 있으며, 다시 연결하기 전에 본 주소는 더 이상 현재 링크와 맞지 않을 수 있습니다. 세션 ID, 사용자, 관찰 시각을 함께 기록하십시오. 세션 ID는 API가 기대하는 키이며, 두 세션을 혼동하지 않게 하는 값입니다.
호스트에서는 설치된 세션 도구가 무엇이 있는지 나열하고, 관리용 보기는 권한이 있을 때만 사용하십시오. 선택한 ID는, 재사용된 ID가 다른 세션의 데이터를 반환하므로, 조회 직전에 확인하십시오.
qwinsta
해당 ID에 대해 WTSQuerySessionInformationW를 호출하십시오
터미널 서비스 API는 정보 클래스 14, WTSClientAddress로 이 질문에 답합니다. Windows에서는 작은 ctypes 래퍼로 충분하며 다른 사용자의 하이브를 읽지 않아도 됩니다. 아래 코드는 명시적 세션 ID를 받아 로컬 호스트에 질의합니다.
import ctypes
import ipaddress
import socket
from ctypes import wintypes
class WTS_CLIENT_ADDRESS(ctypes.Structure):
_fields_ = [("AddressFamily", wintypes.DWORD),
("Address", ctypes.c_ubyte * 20)]
wts = ctypes.WinDLL("Wtsapi32", use_last_error=True)
query = wts.WTSQuerySessionInformationW
query.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.c_int,
ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(wintypes.DWORD)]
query.restype = wintypes.BOOL
free = wts.WTSFreeMemory
free.argtypes = [ctypes.c_void_p]
def client_address(session_id):
buf = ctypes.c_void_p()
size = wintypes.DWORD()
if not query(None, session_id, 14, ctypes.byref(buf), ctypes.byref(size)):
raise ctypes.WinError(ctypes.get_last_error())
try:
if size.value < ctypes.sizeof(WTS_CLIENT_ADDRESS):
raise RuntimeError("short WTS buffer")
value = ctypes.cast(buf, ctypes.POINTER(WTS_CLIENT_ADDRESS)).contents
raw = bytes(value.Address)
if value.AddressFamily == socket.AF_INET:
addr = ipaddress.ip_address(raw[2:6])
elif value.AddressFamily == socket.AF_INET6:
addr = ipaddress.ip_address(raw[:16])
else:
return None
return None if addr.is_unspecified else str(addr)
finally:
free(buf)
방금 확인한 정수 세션 ID를 전달하고 로컬 머신에는 null 서버 핸들을 유지하십시오. 실패한 호출은 둘이 서로 다른 결론으로 이어지므로 주소가 없는 경우가 아니라 오류로 처리하십시오. 다른 사용자의 세션을 조회하려면 적절한 Query Information 권한이 필요하므로, 그 자리에서 권한을 올리는 대신 해당 권한이 부여된 계정으로 도구를 실행하십시오. 반환된 버퍼는 API의 것이며 디코딩이 실패해도 해제해야 합니다.
패밀리를 디코딩한 다음 버퍼를 해제하십시오
이 구조체는 IP 문자열이 아닙니다. 패밀리 필드가 스무 개의 주소 바이트를 읽는 방식을 결정하며, IPv4는 두 번째 오프셋에서, IPv6는 첫 번째 오프셋에서 시작합니다. 지정되지 않은 주소는 클라이언트가 보고하지 않았다는 뜻이며 빈 문자열과는 다릅니다. Remote Desktop Gateway 연결은 주소를 사용할 수 없는 사례로 문서화되어 있으므로 게이트웨이를 거치는 세션은 클라이언트의 실제 주소에 대해 유용한 것을 보고하지 않습니다. 그때는 상관 작업이 게이트웨이 자체 기록으로 옮겨가야 합니다.
디버깅할 때는 둘의 불일치가 그럴듯하지만 틀린 값의 흔한 원인이므로 주소 옆에 패밀리를 출력하십시오. 버퍼는 finally 블록에서 WTSFreeMemory로 해제하여 예외가 누수시키지 않게 하십시오.
보고된 주소를 증거와 분리하십시오
보고된 클라이언트 주소는 클라이언트가 제공한 진술이지 신뢰할 수 있는 관찰이 아닙니다. 그것은 TCP 상대편도, VPN 게이트웨이의 임대 테이블도, 클라이언트의 공인 주소도 아닙니다. 진짜 질문이 어떤 VPN 임대가 한 사람의 것인지라면, 동시에 VPN 서버의 인증된 세션 기록과 상관시키십시오. 질문이 어떤 출처가 리스너에 도달했는지라면 연결 테이블이나 허용된 서버 측 로그를 읽으십시오.
어떤 지원 도구에서든 이 값들을 따로 레이블하여 보관하십시오. 누락된 WTS 결과는 다른 세션의 임의 레지스트리 값으로 대체되지 않고 누락된 채로 남아야 합니다. 같은 사용자가 다시 연결해 새 값을 받을 수 있으므로 주소 옆에 세션 ID를 기록하십시오. 클라이언트 이름도 보고된 주소도 접근을 허용하기에 충분한 증거가 아닙니다.