Пробитие любого NAT без "rendezvous" сервера
Не удалось прогрузить медиа
Привет! В этой статье я расскажу про UDP holepunching без посредника.
Это позволит осуществлять связь между машинами за всеми типами NAT, при этом не используя никаких серверов с белыми IP.
Я пытался найти информацию на схожие темы на этом и других форумах, однако ничего не обнаружил.
Спойлер: holepunch
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Что такое NAT и как он работает?
NAT это посредник между локальной сетью и глобальным интернетом, он меняет частный IP адрес на публичный при обращении к интернет сервисам, и наоборот, при получении ответа возвращает его нужному устройству в локальной сети.Покажу с визуализацией, чтобы было понятнее.
Есть машина в локальной сети - 10.0.0.1
Её NAT - 71.22.120.255
Сервер, к которому идёт подключение - 209.237.226.92
10.0.0.1 осуществляет запрос к серверу 209.237.226.92,
NAT для этого выдаст 10.0.0.1 порт 54321, чтобы именно на этот порт шли данные от 209.237.226.92 и не возникало путаницы с остальными клиентами локальной сети.
Далее NAT от порта :12345, со своего публичного IP адреса делает запрос к серверу 209.237.226.92.
Сервер 209.237.226.92 в качестве адреса отправителя получает 71.22.120.255:12345 и возвращает на него ответ.
NAT в своей таблице смотрит, к какому IP и порту привязан :12345 от 209.237.226.92:80 и возвращает ответ от сервера на 10.0.0.1:54321.
Не удалось прогрузить медиа
1. Устройство 10.0.0.1 отправляет запрос к 209.237.226.92:80, принимает ответы на :54321
2. NAT выделяет порт 12345 для этого соединения, создаёт запись "10.0.0.1:54321 <-- 71.22.120.255:12345"
3. NAT делает запрос к 209.237.226.92:80
4. Сервер отвечает на 71.22.120.255:12345
5. NAT пересылает ответ обратно на 10.0.0.1:54321
Есть несколько типов NAT:
Static NAT - его вы скорее всего увидете на VPS. В его случае есть постоянное соответсвие между внутренним и внешним IP.
Так же существует Dynamic NAT, он временно назначает IP из пула для сессии.
В примере выше был PAT или же Overload NAT, это когда несколько устройств из локальной сети используют один и тот же внешний IP, а различие между ними в присвоенных им портах.
У NAT может быть разное поведение:
Full Cone NAT:
Если машина хоть раз открыла соединение, любой хост может отправлять ответы на этот IP:порт
Restricted Cone NAT:
Принять ответ от хоста можно лишь только, если машина отправляла трафик на этот хост.
Важен лишь IP конечного сервера, порт может быть любым.
Port Restricted Cone NAT:
Ответы принимаются от того же IP и порта к которым машина отправляла трафик, в отличие от Restricted Cone важен не только IP, но и порт.
Symmetric NAT:
Идентично с Port Restricted Cone, однако для всех внешних адресов NAT выдаёт разные порты.
HOLEPUNCH
Проблема состоит в том, что машины не знают портов друг друга, что делает связь затруднительной, особенно при поведении Port Restricted или Symmetric.
Чтобы связь была без посредника, который существует лишь как реле, существует механизм holepunching.
Сперва идёт запрос посреднику, через которого машины друг другу передают IP своих NAT, а также порты. В будущем сервер-посредник не нужен.
10.0.0.1 и 11.1.1.2 узнают публичные айпи и порты
Не удалось прогрузить медиа
Первый этап.png
После этого происходит самое интересное. 1.
Одна машина, на примере, это 10.0.0.1 осуществляет подключение к 11.1.1.2 зная её внешний IP и порт.
71.122.120.255:12345 --> 12.123.45.21:9999
Это подключение не сработает, однако внутри NAT 71.122.120.255 (NAT машины, которая отправила запрос) оно создаст правило о том, что от 12.123.45.21:9999 можно принимать данные.
2.
11.1.1.2 осуществляет подключение к 10.0.0.1
12.123.45.21:9999 --> 71.122.120.255:12345
Этот запрос создаёт правило уже в NAT 12.123.45.21, помимо этого можно передать данные и они будут получены 10.0.0.1
3.
После первого и второго шага машины могут осуществлять подключение друг к другу, передавать данные.
Не удалось прогрузить медиа
Второй этап.png
В интернетах вы услышите про UDP holepunching, однако TCP тоже возможен, он технически сложнее, да и для задач, для которых нужно прямое соединение он излишен и по обычной технике часто может не сработать. Для исправления ошибок лучше использовать код Хэмминга.
Holepunch bruteforce
Проблема, по которой я полагаю, о holepunching нет постов на форумах заключается в необходимости использования rendezvous сервера (посредника), что делает концепцию бесполезной.Я предлагаю решение этой проблемы двумя путями, в зависимости от строгости NAT.
А вы знали, что такие порты, которые выдёт NAT клиентам - называют эфемерными?
Для linux этот диапазон 32768–60999 или 49152–65535, для Windows, как и для macOS, 49152–65535, а на совсем старых системах 1024–5000.
NAT выдаёт порт, ориентируясь на это, и как вы понимаете, их можно крутить и вертеть как угодно.
Linux, macOS:
sysctl -w net.ipv4.ip_local_port_range="32768 60999" // 32768–60999Windows:
netsh int ipv4 set dynamicport tcp start=49152 num=16383 // 49152–65535 tcpnetsh int ipv4 set dynamicport udp start=49152 num=16383 // 49152–65535 udpИдея задать диапазон возможных портов с двух сторон, не больше сотни, но и не значительно меньше, ведь NAT попросту не пропустит, а далее пробить NAT, перебирая порты с двух машин.
Не удалось прогрузить медиа
Шанс подобрать необходимый порт за один цикл - 63%
Покажу математику, можно пропустить (это пизда как скучно).
Потенциальных пар портов между машинами 100 * 100 = 10000, то есть шанс
1/10000, (10000*1/10000==1), в среднем будет одна взаимная пара на запуск.
Из этого следует, что вероятность того, что ни одна пара не сработает
(1-1/100^2)^(100^2)*100==36.7%
Поэтому с шансом 63.2% пара в раунде будет подобрана.
1 раунд - 63.2%
2 раунда - 86.4%
3 раунда - 95.0%
Есть ещё один вариант, он очень медленный, однако сработает даже за самыми строгими NAT :persevere:
Может быть сценарий, особенно за Symmetric NAT, который будет выдавать порты вне зависимости от того, что просит ваша ОС. Пример такого плохого NAT я указал в самой первой схеме.
В таком случае первое решение не сработает. Тогда можно выделить небольшой диапазон портов, к которым будут осуществлять подключение машины, с каким-то шансом повезёт. Если на один запрос уходит 100мс, а NAT сбрасывает таблицу минимум через 30 секунд (в любом случае, не можем узнать, даже если больше), то 10*30=300 портов и будет диапазоном.
65535−1024 возможных портов, которые случайно выбирает NAT и 300 из них с обеих сторон те что нужны.
Шансы подбора:
(1−((1−1/64512)^300)^659)*100
1 раунд - 0.46%
100 раундов - 37.1%
149 раундов - 49,0%
300 раундов - 75,2%
659 раундов - 95,1%
Не удалось прогрузить медиа
На скриншоте сильно модифицированный github.com/libnice/libnice/blob/master/examples/simple-example.c
PoC в статье работает схожим образом.
PoC для обеих примеров
Python:
import socket
import sys
import threading
import time
# python3 brutepunch.py 30000 30600 109.37.186.32 44444 45745
# [диапазон своих портов] [адрес другой машины] [диапазон чужих портов]
LstartPORT = int(sys.argv[1])
LendPORT = int(sys.argv[2])
PIP = sys.argv[3]
PstartPORT = int(sys.argv[4])
PendPORT = int(sys.argv[5])
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(0.1)
def LBIND(): # Вместо ограничения для всей ОС, как было описано в первом решении.
for BPORT in range(LstartPORT, LendPORT + 1): # Может не получить порт (sysctl -w net.ipv4.ip_local_port_range="32768 60999")
try:
sock.bind(("", BPORT))
return BPORT
except:
pass
def SPUNCH(PAYLOAD, DEST):
sock.sendto(PAYLOAD, DEST)
TP = threading.Timer(28, sock.sendto, args=(PAYLOAD, DEST)) # Повторный запрос с задержкой 28 секунд, при сбросе таблиц как минимум в 30. 2 секунды на всякий случай :)
TP.daemon = True
TP.start()
def PPUNCH(LPORT):
while True:
for BPORT in range(PstartPORT, PendPORT + 1):
SPUNCH(b"HOLY_PUNCH", (PIP, BPORT)) # HOLY_PUNCH будет выступать в качестве индикатора. Пейлоад полезно использовать так же при огромном колве потоков.
PAYLOAD, (ip, RPORT) = sock.recvfrom(1024)
if ip != PIP: # Фильтр по IP
continue
print(f"{LPORT} <--> {PIP}:{RPORT}")
return RPORT
def RECV():
while True:
PAYLOAD, (ip, RPORT) = sock.recvfrom(1024)
if ip != PIP:
continue
msg = PAYLOAD.decode()
if msg == "HOLY_PUNCH":
continue
print(f"\r[{RPORT}] > {msg}\n> ", end="", flush=True)
def HELO(DEST): # Чаще чем раз в 30 секунд стоит отправлять мусор, чтобы поддерживать соединение, иначе NAT может вычеркнуть запись
while True:Может быть в будущем покажу ботнитик с использованием этого принципа.
sock.sendto(b"HOLY_PUNCH", DEST)
time.sleep(25)
LPORT = LBIND()
PPORT = PPUNCH(LPORT)
threading.Thread(target=RECV, daemon=True).start()
threading.Thread(target=HELO, args=((PIP, PPORT),), daemon=True).start()
while True:
msg = input("> ")
sock.sendto(msg.encode(), (PIP, PPORT))
На этом статье конец! Техника может быть полезна во многих ситуациях.
Делитесь интересными сценариями для использования, буду рад почитать!
Может быть в будущем покажу ботнетик с использованием этого принципа.
Автор @ec5745y
Специально для forum.LIMB.cc
Специально для forum.LIMB.cc
👍32
🔥3
😁1
😢7
💯7
LIMB