הפעלת RDP זקוקה לכתובת לקוח, והמקורות הברורים אינם מסכימים. CLIENTNAME נותן שם מחשב, הרישום מחזיק ערכים מהפעלות אחרות, והצד השני של השקע יכול להיות כתובת שער או NAT ולא הלקוח. Windows אכן חושף כתובת לקוח מדווחת לכל הפעלה, אבל צריך לבקש את ההפעלה הנכונה ולפענח את התשובה כראוי.

עגנו את החיפוש בהפעלה אחת

התחילו בהפעלה, לא באדם. מארח RDP של Windows יכול לשאת כמה הפעלות עבור חשבון אחד, וכתובת שנראתה לפני התחברות מחדש עשויה כבר לא להתאים לקישור הפעיל. רשמו יחד את מזהה ההפעלה, המשתמש וזמן התצפית. מזהה ההפעלה הוא המפתח שממשק ה-API מצפה לו, והוא הערך שמונע בלבול בין שתי הפעלות.

במארח, כלי ההפעלה המותקנים מפרטים מה קיים; השתמשו בתצוגה הניהולית רק כשאתם זכאים לה. אשרו את המזהה שנבחר מיד לפני השאילתה, כי מזהה שממוחזר מחזיר נתונים של הפעלה אחרת.

qwinsta

קראו ל-WTSQuerySessionInformationW עבור המזהה הזה

ממשק שירותי המסוף עונה על השאלה בעזרת מחלקת מידע 14, WTSClientAddress. מעטפת ctypes קטנה מספיקה ב-Windows, והיא נמנעת מקריאת hive של משתמש אחר. הקוד שלהלן מקבל מזהה הפעלה מפורש ושואל את המארח המקומי.

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)

העבירו את מזהה ההפעלה השלם שזה עתה אימתתם, ושמרו את ידית השרת האפסית עבור המכונה המקומית. התייחסו לקריאה שנכשלה כשגיאה ולא ככתובת חסרה, כי השניים מובילים למסקנות שונות. שאילתה על הפעלה של משתמש אחר דורשת את הרשאת Query Information המתאימה, לכן הריצו את הכלי תחת חשבון שקיבל אותה במקום להעלות הרשאות באופן אד הוק. החוצץ שהוחזר שייך לממשק וחייב להשתחרר גם כשהפענוח נכשל.

פענחו את המשפחה ואז שחררו את החוצץ

המבנה אינו מחרוזת IP. שדה המשפחה שלו קובע כיצד נקראים עשרים בתי הכתובת: IPv4 מתחיל בהיסט שניים, ואילו IPv6 מתחיל בהיסט אפס. הכתובת הבלתי מוגדרת משמעה שהלקוח לא דיווח על אחת, וזה אינו זהה למחרוזת ריקה. חיבורים דרך Remote Desktop Gateway מתועדים כמקרה שבו הכתובת אינה זמינה, ולכן הפעלה דרך שער אינה מדווחת דבר שימושי על הכתובת האמיתית של הלקוח. אז המתאם חייב לעבור לרשומות של השער עצמו.

הדפיסו את המשפחה לצד הכתובת בזמן ניפוי השגיאות, כי אי-התאמה בין השניים היא הסיבה הרגילה לערך סביר אך שגוי. שחררו את החוצץ בעזרת WTSFreeMemory בבלוק finally כדי שחריגה לא תדליף אותו.

הפרידו את הכתובת המדווחת מההוכחה

כתובת לקוח מדווחת היא הצהרה שהלקוח מספק, לא תצפית מהימנה. היא אינה הצד השני של TCP, אינה טבלת החכירות של שער ה-VPN, ואינה הכתובת הציבורית של הלקוח. כשהשאלה האמיתית היא איזו חכירת VPN שייכת לאדם, תאמו בו-זמנית את רשומת ההפעלה המאומתת של שרת ה-VPN. כשהשאלה היא איזה מקור הגיע למאזין, קראו את טבלת החיבורים או את היומנים המותרים בצד השרת.

שמרו על הערכים האלה מתויגים בנפרד בכל כלי תמיכה. תוצאת WTS חסרה צריכה להישאר חסרה במקום ליפול חזרה לערך רישום שרירותי מהפעלה אחרת. תעדו את מזהה ההפעלה לצד הכתובת, כי אותו משתמש יכול להתחבר מחדש ולקבל ערך חדש. לא שם הלקוח ולא הכתובת המדווחת הם הוכחה מספקת למתן גישה.

מאמרים קשורים