LIMB
Вход Регистрация

тема

Holepunching без посредника

ec5745y · · 0 ответов · 335 просмотров

Пробитие любого NAT без "rendezvous" сервера
Не удалось прогрузить медиа
​

Привет! В этой статье я расскажу про UDP holepunching без посредника. 
Это позволит осуществлять связь между машинами за всеми типами NAT, при этом не используя никаких серверов с белыми IP.
Я пытался найти информацию на схожие темы на этом и других форумах, однако ничего не обнаружил.

Спойлер: holepunch
Не удалось прогрузить медиа
Не удалось прогрузить медиа
Не удалось прогрузить медиа

Не удалось прогрузить медиа
Варианты использования можно перечислять бесконечно: скрытый канал связи за NAT, P2P C2, ботнеты и обход ограничений на входящие подключения.

Что такое 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​
Теперь самое интересное - установка соединения между двумя машинами за Overload NAT.
Проблема состоит в том, что машины не знают портов друг друга, что делает связь затруднительной, особенно при поведении 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–60999
Windows:
netsh int ipv4 set dynamicport tcp start=49152 num=16383 // 49152–65535 tcp
netsh 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​
👍32 🔥3 😁1 😢7 💯7

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.