Теория и практика туннелирования трафика

В этой статье я расскажу вам о туннелировании. Сама статья разбита на части. В первой части я напишу о туннелировании с помощью ssh-клиента. Вторая часть посвящена анализу кода программы, которая позволяет передавать данные через цепочку проксей. В третьей же части я опишу программы для туннелирования трафика.


Для начала давайте решим, зачем оно нам нужно? Туннелирование может оказаться полезным, если вы хотите шифровать потоки информации, которые обычно идут "открытым текстом".


Так, например, можно добиться надежного шифрованного доступа к электронной почте по протоколу POP3, даже если сервер не поддерживает шифрование, такое как POP-SSL. Такое туннелирование - очень мощная штука. Если вы имеете SSH-соединение от одного хоста до другого, то вы можете туннелировать и другие TCP-подключения по безопасному зашифрованному SSH-сеансу. Это позволяет вам защищать другие нешифруемые протоколы. В данном случае можно использовать SSH для туннелирования соединения к POP-серверу. Сейчас я опишу, как работает утилита ssh. Есть два вида туннелирования: LocalForwards и RemoteForwards. Чаще в нашем случае используется первый способ. Его и рассмотрим:


LocalForwards - туннель с вашей машины на удаленный ssh-сервер. Такой туннель можно создать в командной строке, добавив ключ –L (указывает, что надо создавать локальный порт). И необходимо указать следующие параметры:
-L local_port:destination_host:destination_port
где local_port - порт на локальной машине, который будет слушать ssh-демон. Это может быть номер порта или просто название службы, например http, smtp или mysql. Destination_host – удаленный комп, с которым происходит соединение, а Destination_port - это порт, к которому подключаемся.


Например:
8-) $ ssh users.celuloza.ro -L 31337:127.0.0.1:25
Это значит, что когда мы соединимся с 31337-ым портом на 127.0.0.1, все наши пакеты будут туннелированы на 25 порт user.celuloza.ro . И если потом мы выполним команду w, то увидим, что юзеров стало двое:
11:06PM up 1 day, 8:26, 2 users, load averages: 0.14, 0.16, 0.11
USER TTY FROM LOGIN@ IDLE WHAT
k0r0l p0 slip139-92-138-2 10:56PM - ssh genphys.phys.msu.ru -L 31337
k0r0l p1 users.celuloza.ro 11:06PM - w


Один подключился с удаленного компа, а второй через туннель. Теперь с другой консоли или с другого компа попробуем залезть на 31337-ой порт на 127.0.0.1:
220 users.celuloza.ro ESMTP Sendmail 8.11.3/8.11.3; Fri, 7 May 2004 23:14:51 +0400 (MSD)


На нас выскочит сендмэйл. Так как это подключение было туннелировано внутри существующего ssh сеанса, то оно полностью зашифровано, при этом от нас не потребовалось больших усилий по настройке системы шифрования и знания криптографии. Вы можете одновременно осуществить многократные подключения, и каждое из них будет туннелировано через надежно зашифрованный ssh-канал.


Старые версии OpenSSH позволяли подключаться к подобным туннелям только локальной машине клиента. Если вы хотите позволить другим машинам использовать этот туннель, тогда вы можете использовать опцию
ssh -g


Сейчас я описал самый примитивный способ создания туннелей (напрямую через командную строку). Но вы можете также создавать туннели внутри файла, используя
ssh ~/.ssh/options


Заносим в файл такие строчки:
Host testtunnel HostName users.celuloza.ro LocalForward 2525:localhost:25 GatewayPorts no


Теперь, если я хочу создать туннель к users.celuloza.ro, я соединяюсь с моим сервером и выполняю команду
ssh testtunnel



Теперь углубимся в изучение кода программы, позволяющей создавать туннели. Изучать мы будем программу, которая называется Proxychains (скачать можете тут). В качестве параметров он принимает имя запускаемого приложения и целевой хост. Затем это приложение запускается в контексте процесса proxychains . В результате proxychains перехватывает вызов connect и заменяет его своим. Приложение получает дескриптор сокета и даже не подозревает о существовании цепочки прокси. Proxychains поддерживает три типа цепочек:


- строгая (следует прямо по цепочке и в случае невозможности достижения целевого хоста возвращает ошибку)


- динамическая (если встречает мертвый прокси, пропускает его и следует дальше по цепочке)


- рандомная (выбирает проксики произвольным образом).


Цепочки прокси-серверов берутся из /etc/proxychains.conf. Разумеется, их туда надо сначала занести.


Теперь рассмотрим, как создавать туннель через прокси (HTTP, SOCKS4 и SOCKS5). В некоторых местах используются цитаты с RFC 2616 и 1928 и код программы.


Первое, оно же самое простое:


Туннель через HTTP Прокси.


Смотрим в RFC методы соединения:
OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT. Нас интересует метод CONNECT:
"Спецификация резервирует имя метода CONNECT для использования с прокси, которые могут динамически переключаться на работу в качестве туннеля (например, туннелирование SSL) Для более подробной информации смотрите в draft-luotonen-web-proxy-tunneling-XX ".


Этот документ описывает расширение протокола HTTP 1.x для организации туннеля через TCP. Теперь о методе работы:
1. Клиент соединяется с HTTP прокси, поддерживающим метод CONNECT.
2. Клиент посылает серверу строку вида:
CONNECT target_host:target_port HTTP/1.xrnUser-Agent: User Agentrn
Где target_host:target_port это IP и порт компа прокси-сервера.
3. В ответ на эту строку сервер может либо потребовать авторизацию, либо вернуть состояние удачного соединения или ошибку.


Вот как выглядит требование авторизации:
HTTP/1.x 407 Proxy auth required
Proxy-agent: Proxy Agent
Proxy-authenticate:...
<пустая строка>


Тогда необходимо послать серверу следующую строку:
CONNECT target_host:target_port HTTP/1.x
User-agent: User Agent
Proxy-authorization: basic password_in_base64_encoding
<пустая строка>


В случае удачного установленного соединения сервер вернет следующее:
HTTP/1.0 200 Connection established
Proxy-agent: Proxy Agent
<пустая строка>


То есть, при необходимости авторизации диалог принимает вид:
CLIENT -> SERVER SERVER -> CLIENT:
CONNECT target_host:target_port HTTP/1.x
User-agent: User Agent
<пустая строка>
HTTP/1.x 407 Proxy auth required
Proxy-agent: Proxy Agent
Proxy-authenticate: ...
<пустая строка>
CONNECT target_host:target_port HTTP/1.x
User-agent: User Agent
Proxy-authorization: ...
<пустая строка>
HTTP/1.x 200 Connection established
Proxy-agent: Netscape-Proxy/1.1
<пустая строка>
<начало данных>


Теперь цитата из draft-luotonen-web-proxy-tunneling-ХХ:
"Открытый туннель может использоваться не только как клиент-прокси канал, но и как прокси-прокси канал. Отсюда следует, что можно сделать цепочку из нескольких прокси."


Посмотрим код программы:
int len;
char buff[BUFF_SIZE];
bzero (buff,sizeof(buff));
sprintf(buff, "CONNECT %s:%d HTTP/1.0rnUser-Agent: ProxyChains 1.8rn", inet_ntoa( * (struct in_addr *) &ip), ntohs(port));
len = strlen (buff);
if(len!=send(sock,buff,len,0))
return SOCKET_ERROR;

//читаем, что нам вернул сервер
bzero(buff,sizeof(buff));
len=0 ;
while(len {
if(1==read_n_bytes(sock,buff+len,1))
len++;
else
return SOCKET_ERROR;
if ( len > 4 &&
buff[len-1]=='n' &&
buff[len-2]=='r' &&
buff[len-3]=='n' &&
buff[len-4]=='r' ) break;
}
//=========================

// если ответ не 200, значит либо порт закрыт, либо хост мертв
if ( (len==BUFF_SIZE) ||
! ( buff[9] =='2' &&
buff[10]=='0' &&
buff[11]=='0' )) return BLOCKED;
return SUCCESS;
//===============================================


Если все в порядке, то мы получаем рабочий туннель, и его можно использовать как обычное соединение с target_host:target_port.



Туннель через SOCKS 4 прокси.


Цитата из RFC 1928 о назначении SOCKS:
"Использование сетевых файрволлов и систем, эффективно скрывающих организацию внутренней сетевой структуры от внешней сети, такой как Интернет, становится все более популярным. Эти файрволлы обычно работают как шлюзы прикладного уровня между сетями, предлагая обычно telnet-, FTP-, и SMTP-доступ. С появлением более сложных протоколов прикладного уровня, предназначенных для облегчения глобального информационного взаимодействия, появилась потребность в обеспечении общей основы для прозрачной и безопасной работы через файрволл для этих протоколов. Существует также необходимость в строгой аутентификации при работе через файрволл, в некоторой степени похожей на используемые сейчас методы. Это требование обусловлено тем, что отношения типа клиент-сервер появляются между сетями различных организаций, и эти отношения должны быть управляемыми и, зачастую, строго аутентифицированы. Описываемый протокол разработан, чтобы обеспечить основу для удобного и безопасного использования сервиса сетевых файрволлов для приложений типа клиент-сервер, работающих по протоколам TCP и UDP. Протокол представляет собой "уровень-прокладку" между прикладным уровнем и транспортным уровнем, и, как таковой, не обеспечивает сервиса шлюзов сетевого уровня, такого, как пересылка пакетов ICMP."


Ну а теперь я покажу, как все выглядит на деле.


SOCKS-запрос формируется следующим образом:
+----+-----+------------+------------+
0 1 2 4 8
+----+-----+------------+------------+
|VER | CMD | DST.PORT | DST.ADDR |
+----+-----+------------+------------+

Где:
VER — версия протокола: 4;
CMD — CONNECT: 1, другие значения нас не интересуют;
DST.ADDR — требуемый адрес;
DST.PORT — требуемый порт (в сетевом порядке октетов).
Если в первых двух байтах ответа сервера содержится 0 и отличное от 90 значение — значит соединение успешно установлено с DST.ADDR:DST.PORT. Иначе — ошибка. Если нет ошибки, в исходный сокет можно писать туннелируемые данные.


Опять код Proxychains:
int len;
char buff[BUFF_SIZE];
bzero (buff,sizeof(buff));
memset(buff,0,sizeof(buff));
buff[0]=4; // socks version
buff[1]=1; // connect command
memcpy(&buff[2],&port,2); // dest port
memcpy(&buff[4],&ip,4); // dest host
len=strlen(user)+1; // username
if(len>1)
strcpy(&buff[8],user);
if((len+8)!=write_n_bytes(sock,buff,(8+len)))
return SOCKET_ERROR;

if(8!=read_n_bytes(sock,buff,8))
return SOCKET_ERROR;

if (buff[0]!=0||buff[1]!=90)
return BLOCKED;

return SUCCESS;



Туннелирование через SOCKS 5


Эта версия протокола имеет более сложную структуру по сравнению с четвертой версией. Соединение происходит следующем образом, похожим на процедуру рукопожатия при установке TCP соединения:
- клиент соединяется с сервером и посылает сообщение с номером версии и выбором соответствующего метода аутентификации:
+----+--------------+-------------+
|VER | NMETHODS | METHODS |
+----+--------------+-------------+
| 1 | 1 | 1 to 255 |
+----+--------------+-------------+


Поле NMETHODS содержит число пакетов в идентификаторах методов авторизации в поле METHODS.


Сервер выбирает один из предложенных методов, перечисленных в METHODS, и посылает ответ о выбранном методе:
+----+-----------+
|VER | METHOD |
+----+-----------+
| 1 | 1 |
+----+-----------+

Возможные значения для поля METHOD:
- 00 - аутентификация не требуется;
- 02 - USERNAME/PASSWORD (см. RFC1929).


Если выбранный метод в METHOD равен FF, то ни один из предложенных клиентом методов не применим, и клиент закрывает соединение.


После проверки версий клиент и сервер начинают аутентификацию, используя выбранный метод. После того, как аутентификация выполнена, клиент посылает детали запроса. Если выбранный метод аутентификации требует особое формирование пакетов с целью проверки целостности и/или конфиденциальности, запросы должны включаться в пакет, формат которого зависит от выбранного метода.


Формирование пакета SOCKS-5:
+----+-------+-------+--------+------------+------------+
|VER | CMD | RSV | ATYP | DST.ADDR | DST.PORT |
+----+-------+-------+--------+------------+------------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+----+-------+-------+--------+------------+------------+

Где:
VER — версия протокола: 05;
CMD — CONNECT: X'01'
RSV — зарезервировано;
ATYP — тип адреса, следующего вида:
IP v4 адрес: 01;
имя домена: 03;
IP v6 адрес: 04;
DST.ADDR — требуемый адрес;
DST.PORT — требуемый порт (в сетевом порядке октетов).


SOCKS-запрос посылается клиентом, как только он установил соединение с SOCKS-сервером и выполнил аутентификацию. Сервер обрабатывает запрос и посылает ответ в следующей форме:
+----+-----+--------+------+--------------+-------------+
|VER | REP | RSV | ATYP | BND.ADDR | BND.PORT |
+----+-----+--------+------+--------------+-------------+
| 1 | 1 | X'00' | 1 | Variable | 2 |
+----+-----+--------+------+--------------+-------------+

Где:
VER — версия протокола: 05;
REP - код ответа:
00 — успешный;
01 — ошибка SOCKS-сервера;
02 — соединение запрещено набором правил;
03 — сеть недоступна;
04 — хост недоступен;
05 — отказ в соединении;
06 — истечение TTL;
07 — команда не поддерживается;
08 — тип адреса не поддерживается;
от 09 до X'FF' — не определены.
RSV — зарезервирован;
ATYP — тип последующего адреса:
IP v4 адрес: 01;
имя домена: 03;
IP v6 адрес: 04;
BND.ADDR — выданный сервером адрес;
BND.PORT — выданный сервером порт (в сетевом порядке пакетов).
Если нет ошибки, в исходный сокет можно писать туннелируемые данные.
Значения зарезервированных (RSV) полей должны быть установлены в 00.
В ответ на CONNECT, BND.PORT становиться равным номеру порта, который сервер назначает для соединения с указанным хостом, а BND.ADDR содержит связанный IP-адрес. Выданный BND.ADDR зачастую отличается от IP-адреса, который клиент использует для доступа к SOCKS-северу, так как такие серверы часто имеют несколько IP-адресов. Ожидается, что сервер будет использовать DST.ADDR и DST.PORT и адрес клиента при обработке запроса CONNECT.


И опять код, реализующий процедуру данного подключения:
int len;
char buff[BUFF_SIZE];
bzero (buff,sizeof(buff));
memset(buff,0,sizeof(buff));
if(user)
{
buff[0]=5; //version
buff[1]=2; //nomber of methods
buff[2]=0; // no auth method
buff[3]=2; /// auth method -> username / password
if(4!=write_n_bytes(sock,buff,4))
return SOCKET_ERROR;
}
else
{
buff[0]=5; //version
buff[1]=1; //nomber of methods
buff[2]=0; // no auth method
if(3!=write_n_bytes(sock,buff,3))
return SOCKET_ERROR;
}
memset(buff,0,sizeof(buff));
if(2!=read_n_bytes(sock,buff,2))
return SOCKET_ERROR;
if (buff[0]!=5||(buff[1]!=0&&buff[1]!=2))
{
if((buff[0]==5)&&(buff[1]==0xFF))
return BLOCKED;
else
return SOCKET_ERROR;
}
if (buff[1]==2)
{
// authentication
char in[2];
char out[515]; char* cur=out;
int c;
*cur++=1; // version
c=strlen(user);
*cur++=c;
strncpy(cur,user,c);
cur+=c;
c=strlen(pass);
*cur++=c;
strncpy(cur,pass,c);
cur+=c;
if((cur-out)!=write_n_bytes(sock,out,cur-out))
return SOCKET_ERROR;
if(2!=read_n_bytes(sock,in,2))
return SOCKET_ERROR;
if(in[0]!=1||in[1]!=0)
{
if(in[0]!=1)
return SOCKET_ERROR;
else
return BLOCKED;
}
}
buff[0]=5; // version
buff[1]=1; // connect
buff[2]=0; // reserved
buff[3]=1; // ip v4
memcpy(&buff[4],&ip,4); // dest host
memcpy(&buff[8],&port,2); // dest port
if(10!=write_n_bytes(sock,buff,10))
return SOCKET_ERROR;
if(4!=read_n_bytes(sock,buff,4))
return SOCKET_ERROR;
if (buff[0]!=5||buff[1]!=0)
return SOCKET_ERROR;
switch (buff[3])
{
case 1: len=4; break;
case 4: len=16; break;
case 3: len=0;
if(1!=read_n_bytes(sock,(char*)&len,1))
return SOCKET_ERROR;
break;
default:
return SOCKET_ERROR;
}
if((len+2)!=read_n_bytes(sock,buff,(len+2)))
return SOCKET_ERROR;
return SUCCESS;

Вот таким образом работает данная программа.


Можно, собственно, и закончить, но поклонники виндувса начнут говорить, что их любимую ось обошли стороной. Ладно, расскажу, как можно использовать все описанные выше методы под Виндой. Для этого надо скачать прогу FreeCap (скачать можно тут). Процитирую разработчиков:
FreeCap -- это абсолютно бесплатная программа поставляемая под лицензией GNU General Public Licence.
Программа основана на универсальном внедрении DLL в чужой процесс (работает под всеми версиями виндов, начиная от Win95 и заканчивая Longhorn-ом), которая перехватывает API вызовы Winsock-а, перенаправляя запросы connect на SOCKS сервер. Написана на Delphi с небольшими вставками на ассемблере.
То есть, это программа, аналогичная Proxychains, только под винду.


Теперь точно конец! Тем более, спать уже пора. Если что неясно, пишите на форуме, там вам обязательно помогут.



K0r0l


Автор: K0r0l , @ , WWW