В RDP-логах Windows есть значение, которое давно живёт как маленький мистический IoC: ::%16777216. Его встречали в описаниях атак, где злоумышленники прокидывали доступ к удалённому рабочему столу через туннель, чаще всего через ngrok. В отчётах это значение обычно фигурировало рядом с RDP, Event ID 1149 и полем Source Network Address, но почти не объяснялось: то ли это особенность ngrok, то ли странность Windows, то ли просто бесполезный артефакт, который ничего не говорит об источнике подключения.
Доклад Константина Грищенко из Positive Technologies был устроен как расследование именно этой детали. Не «как детектировать ngrok», не «как расследовать RDP-компрометацию», а гораздо уже и интереснее: что на самом деле означает ::%16777216, почему оно появляется в журналах, можно ли воспроизвести его без ngrok и где именно в Windows ломается адрес.
Короткий ответ: ::%16777216 — это не артефакт ngrok. Это результат ошибки Windows при обработке IPv6-адреса клиента RDP. В типичном туннельном сценарии за этим значением скрывается IPv6 localhost ::1, но из-за несогласованной интерпретации структуры адреса разными модулями Windows часть адреса съезжает в поле zone index.
Загадка в событии 1149
Точка входа в историю — событие Event ID 1149 из журнала:
Microsoft > Windows > TerminalServices-RemoteConnectionManager > Operational
Это событие появляется при успешной RDP-аутентификации. Среди его параметров есть имя пользователя, домен и Source Network Address. В обычном случае в этом поле ожидается IP-адрес клиента: например, привычный IPv4-адрес. Но при подключениях через туннель в расследованиях регулярно всплывало значение:
::%16777216
Проверка XML-представления события не меняет картину: это не красивость Event Viewer и не проблема отображения в интерфейсе. В данных события действительно лежит такая строка.
По форме она похожа на IPv6: два двоеточия напоминают сокращение нулевых фрагментов. Знак % тоже не чужой для IPv6: у link-local адресов бывает scoped literal, или zone index, например fe80::1ff:fe23:4567:890a%3 или fe80::1ff:fe23:4567:890a%eth2. Такой суффикс помогает локальной системе понять, к какому интерфейсу относится адрес.
Но ::%16777216 всё равно выглядит неправильно. После :: нет адресной части, а число после % подозрительно велико для человеческого индекса интерфейса. Чтобы понять, что это за число, докладчик разложил его в шестнадцатеричную форму:
16777216 dec = 01 00 00 00 hex
Это одна единица и много нулей. А IPv6 localhost ::1 — тоже одна единица и много нулей:
::1 = 0:0:0:0:0:0:0:1
Так появилась первая гипотеза: ::%16777216 в RDP-логе — это «сломанная» форма записи IPv6-адреса ::1.
Почему подозревали ngrok
Связь с ngrok возникла не на пустом месте. В реальных атаках злоумышленники действительно часто используют туннелирование, чтобы получить доступ к RDP снаружи: поднимают агент на скомпрометированной машине, пробрасывают порт и подключаются к удалённому рабочему столу через внешний сервис. В таких сценариях в логах Windows и появлялся ::%16777216.
Из-за этого значение стало восприниматься как ngrok-специфичный след. Но у гипотезы есть слабое место: если RDP-сервис видит подключение как локальное IPv6-соединение, то похожий эффект должен возникать не только с ngrok, а с любым туннелем, который приводит клиента на ::1:3389.
Поэтому проверяемая гипотеза была уточнена так: ::%16777216 — это странная форма записи IPv6 localhost ::1, возникающая не из-за ngrok.
Эксперимент без ngrok: SSH tunnel
Для проверки не понадобился ngrok. Достаточно было обычного SSH-туннеля, тем более OpenSSH в современных Windows доступен штатно. Схема эксперимента была простой: с одной машины открыть локальный порт и пробросить его на RDP-порт удалённой машины через IPv6 localhost:
ssh -L 33389:[::1]:3389 pc.testlab.lan
Здесь 33389 — локальный порт на машине, с которой выполняется подключение, а [::1]:3389 — адрес и порт на целевой стороне. Квадратные скобки нужны, чтобы в записи IPv6-адреса не спутать двоеточия адреса с разделителем порта.
После подключения RDP-клиента к проброшенному порту проблемное значение снова появилось в журнале. Это важный результат: ::%16777216 воспроизводится без ngrok. Значит, ngrok был не причиной, а одним из удобных способов создать сетевой сценарий, в котором Windows ошибочно логирует IPv6-адрес источника.
Где искать поломку
Дальше нужно было понять, кто именно превращает нормальный адрес в странную строку. Для этого докладчик использовал API Monitor — старую, но полезную утилиту для перехвата API-вызовов в приложениях и сервисах.
Сначала нужно было найти процесс, который участвует в обработке RDP-подключения. Это один из svchost.exe, внутри которого живут службы Terminal Services. Через Process Explorer и признаки вроде открытых портов, командной строки процесса и загруженных RDP-библиотек был найден нужный процесс. После этого в API Monitor включили наблюдение за сетевыми функциями.
Ключевой вызов оказался таким:
GetNameInfoW
GetNameInfoW из Windows API принимает структуру адреса и возвращает строковое представление имени узла. С флагом NI_NUMERICHOST она не пытается резолвить hostname, а возвращает числовую форму адреса. Именно такая функция логично подходит для превращения бинарного SOCKADDR в строку для записи в событие.
В сравнении IPv4/IPv6 поведение расходилось. На IPv4 всё выглядело нормально: какой адрес приходил на вход, такой и оказывался в логе. На IPv6 через туннель API Monitor показал решающий момент: на вход GetNameInfoW приходит структура, которая должна описывать IPv6-адрес клиента, а на выходе появляется ::%16777216.
То есть строка рождается не в Event Viewer и не в SIEM. Она появляется уже на уровне Windows API, когда бинарная структура адреса интерпретируется как sockaddr_in6 и форматируется в текст.
SOCKADDR, IPv6 и съехавшие четыре байта
Сетевые адреса в Windows, как и в других системах, описываются структурами семейства SOCKADDR. В зависимости от семейства адресов одна и та же область памяти может интерпретироваться как sockaddr_in для IPv4 или как sockaddr_in6 для IPv6. Размеры отличаются: для IPv4 это 16 байт, для IPv6 — 28 байт.
В API Monitor на этом месте обнаружилась отдельная маленькая проблема: встроенное описание структуры показывало не все данные для IPv6. Утилита знала значение 17, соответствующее AF_INET6, но отображала структуру так, будто полезных данных меньше. После правки XML-описания в sockets.h.xml стало видно содержимое целиком.
И тогда картина сложилась. Последняя единица из адреса ::1 никуда не исчезла. Она оказалась не в адресной части, а в поле, которое при разборе sockaddr_in6 воспринимается как scope ID. Иными словами, IPv6-адрес оказался сдвинут на четыре байта вправо.
Для ::1 этот сдвиг даёт особенно узнаваемый результат: почти весь адрес превращается в нули, а последняя часть попадает в zone index. Так и получается:
::%16777216
Чтобы убедиться, что дело не только в частном случае localhost, был проведен эксперимент с обычным IPv6-адресом. Он показал тот же принцип: весь адрес съезжает, в начале появляются четыре лишних байта, а хвост адреса превращается в число после %.
Где именно рождается ошибка
От API Monitor расследование перешло к отладке Windows-библиотек. Сложность здесь не в обфускации: Windows-бинарники в этом смысле удобны, есть символы и нормальный код. Сложность в объёме. Нужно найти не место, где проблема уже видна, а место чуть раньше, где структура заполняется неправильно для последующей интерпретации.
Помогли hardware breakpoints — аппаратные точки останова на доступ к памяти. Если известно, какой участок памяти потом попадет в GetNameInfoW, можно поставить брейкпоинт на этот участок и посмотреть, кто писал туда раньше.
Проблемное место оказалось в rdpcorets.dll, в методе:
CUMRDPConnection::GetClientData
На слайде был показан фрагмент:
movdqu xmmword ptr [r14+3088], xmm0
Эта инструкция записывает 128 бит IPv6-адреса клиента из регистра xmm0 в память по смещению r14+3088. Чуть выше значение 17h, то есть AF_INET6, записывается по смещению r14+3076. Разница между этими смещениями — 12 байт.
И здесь появляется важная структура:
WTS_SOCKADDR
В одном месте Windows заполняет данные как WTS_SOCKADDR, где есть четыре лишних байта для выравнивания, и IPv6-адрес оказывается по смещению 12. А дальше строка для журнала формируется уже в termsrv.dll через вызов GetNameInfoW из ws2_32.dll, где параметр ожидается как указатель на обычный SOCKADDR. Для sockaddr_in6 адресная часть читается со смещения 8.
Разница ровно в те самые четыре байта:
rdpcorets.dll: адрес записан как часть WTS_SOCKADDR со смещением 12
termsrv.dll/ws2_32.dll: адрес читается как sockaddr_in6 со смещением 8
Итог: разные модули Windows несогласованно интерпретируют одну и ту же структуру. Один кладет IPv6-адрес туда, где он должен быть для WTS_SOCKADDR, другой читает его как обычный sockaddr_in6. IPv4 это не ломает заметным образом, а IPv6 даёт съезд адреса и странный scoped literal.
Не только 1149
Проблема проявляется не только в Event ID 1149. По докладу, минимум ещё два события Security Log ведут себя аналогично:
4778— переподключение к RDP-сессии;4779— отключение RDP-сессии.
Практически это значит, что проверять нужно не только TerminalServices-RemoteConnectionManager/Operational, но и Windows Logs > Security, если в расследовании важны RDP-сессии и их источник.
Докладчик также нашёл похожую старую историю вокруг неправильного адреса в событии 4624 после перехода с RDP 7 на RDP 8. В том случае Microsoft объясняла проблему изменениями стека и структурой WTS_SOCKADDR. Судя по текущему расследованию, похожий класс ошибки действительно был исправлен не везде.
Версии Windows и статус MSRC
На момент тестов 10.06.2025 ошибка воспроизводилась на таких системах:
- Windows Server 2012 R2;
- Windows 10 Pro 22H2 19045.5854;
- Windows Server 2019 Standard 1809;
- Windows 11 Enterprise 23H2 22631.5335.
По совокупности тестов и старых материалов докладчик предположил, что проблема, вероятно, затрагивает версии начиная с Windows 8 и Windows Server 2012. Это именно предположение по докладу, а не результат полного перебора всех сборок.
Команда зарепортила проблему в Microsoft через MSRC. Хронология на слайдах такая:
- 10.06.2025 — репорт в MSRC;
- 08.11.2025 — ответ:
moderate, no CVE, no additional updates will be provided.
Логика ответа понятна: это не уязвимость в смысле прямого нарушения безопасности, а ошибка журналирования. CVE и bounty за неё не дали.
К моменту доклада ситуация уже изменилась. На 25.05.2026 по проверкам из презентации:
- Windows 10 Pro 22H2 19045.6456 — ошибка есть;
- Windows 11 24H2 26100.1742 — ошибка есть;
- Windows 11 24H2 26100.8457 — ошибки нет;
- Windows 11 26H1 28000.2113 — ошибки нет.
Вывод осторожный: похоже, в свежих сборках Windows 11 проблему уже поправили, но отдельного подробного ответа об исправлении в рамках репорта не было.
Как восстановить IPv6-адрес
Важная практическая часть доклада: данные не всегда потеряны безвозвратно. Если в лог попал «съехавший» IPv6-адрес, его можно восстановить.
Пример со слайда:
0:0:fe80::6e1d:980d%2607874452
Алгоритм:
- Взять часть после
%. - Перевести её из decimal в hex.
- Убрать слева два нулевых фрагмента из адреса.
- Дописать справа полученные байты в обратном порядке.
Для примера:
2607874452 dec = 9b 71 01 94 hex
С учётом обратного порядка байт получается нормальный IPv6-адрес:
fe80::6e1d:980d:9401:719b
Эту логику можно реализовать в SIEM, пайплайне нормализации логов или отдельной DFIR-утилите. Главное — понимать, что значение после % в таких событиях может быть не настоящим zone index, а хвостом IPv6-адреса, уехавшим из-за ошибки структуры.
Практический вывод для SIEM и DFIR
::%16777216 не стоит трактовать как «ngrok detected» в узком смысле. Более корректная формулировка: это индикатор RDP-подключения по IPv6 через локальный туннельный сценарий, при котором Windows ошибочно залогировала source address. Ngrok может быть одним из инструментов, но не единственным. Такой же эффект воспроизводится через SSH tunnel, а по смыслу возможен и с другими средствами проброса портов.
Для детектирования это всё равно полезный IoC. Если в Source Network Address для RDP-событий появляется ::%16777216, это сильный повод проверить:
- кто и когда подключался по RDP;
- были ли рядом события
1149,4778,4779; - запускались ли на хосте туннельные утилиты;
- есть ли следы ngrok, SSH tunnel, Dev Tunnels или других средств проброса;
- совпадает ли активность с интерактивным входом, созданием новых процессов, lateral movement или действиями администратора.
Для нормализации логов важно не выбрасывать такие значения как «битый адрес». Иногда их можно пересчитать обратно в IPv6. А для ::%16777216 полезно явно помечать, что это, вероятнее всего, сломанное представление ::1 в RDP-контексте.
Главная ценность расследования не в том, что найден ещё один красивый артефакт Windows. Ценность в снятии ложной привязки к конкретному инструменту. ::%16777216 указывает не на магию ngrok, а на сочетание RDP, IPv6, локального туннеля и бага в обработке WTS_SOCKADDR/sockaddr_in6 между rdpcorets.dll, termsrv.dll и ws2_32.dll.