我们在powershell上编写Reverse socks5代理。第2部分

研发故事分为3部分。 第2部分-开发性。
山毛榉有很多-甚至更多。

在本文的第一部分 ,我们了解了一些用于组织反向隧道的工具,了解了它们的优缺点,研究了Yamux多路复用器的机制,并描述了新创建的powershell模块的基本要求。 现在应该开始开发客户端Powershell模块,以用于RSocksTun反向隧道的现成实现。

首先,我们需要了解我们的模块将以哪种模式工作。 显然,对于数据的初步传输,我们将需要使用Windows套接字机制和.Net功能以流方式对套接字进行读写。 但是,另一方面,因为 由于我们的模块必须同时提供多个yamux流,因此所有I / O操作都不应完全阻止程序的执行。 这表明了以下结论:我们的模块应该使用软件多线程并与yamux服务器执行读写操作,以及对不同程序流中的目标服务器执行读写操作。 好吧,当然,有必要提供一种并行流之间的交互机制。 幸运的是,powershell为启动和管理程序流提供了充足的机会。

通用工作算法


因此,我们客户的一般算法应如下所示:

  • 建立与服务器的SSL连接;
  • 使用密码登录,以便服务器可以将我们与安全员区分开来;
  • 等待yamux软件包安装新的流,并定期响应服务器的keepalive请求;
  • yamux软件包到达后,立即开始新的socksScript程序流(不要与流混淆)以安装新的流。 在socksScript中,实现socks5服务器的工作;
  • 当带有yamux的数据的数据包到达时-从12字节的报头中了解数据的目标流以及它们的大小,从yamux服务器读取数据,并将接收到的数据传输到具有相应流号的流;
  • 在每个正在运行的袜子脚本中,定期监视用于yamux服务器的数据的可用性。 如果有这样的数据,请向它们添加相应的12字节标头,然后将其发送到yamux服务器;
  • 当yamux包到达以关闭流时,将信号传输到相应的流以结束流并断开连接,此后,完成流本身;

因此,在我们的客户中,必须实现至少3个程序流:

  1. 最主要的一个将建立连接,登录到yamux服务器,从中接收数据,处理yamux头,并将已经原始的数据发送到其他程序流。
  2. 流与袜子服务器。 可能有多个-每个流一个。 他们实现了socks5功能。 这些流将与内部网络上的目的地交互。
  3. 逆流。 它从袜子流接收数据,向它们添加yamux标头,然后将其发送到yamux服务器。

并且,当然,我们需要提供所有这些流之间的交互。

我们不仅需要提供这种交互,而且还需要获得流输入/输出(类似于套接字)的便利。 最合适的机制是使用软件管道。 在Windows中,当每个管道都有其自己的名称且为匿名时,管道将被注册-每个管道均由其处理程序标识。 为了保密起见,我们当然会使用匿名管道。 (毕竟,我们不希望使用系统中已注册的管道来计算模块-是吗?)。 因此,在主流/反向流与袜子流之间,交互将通过匿名管道进行,从而支持异步流I / O。 在主流和回流之间,将通过共享对象机制(共享的同步变量)进行通信(有关这些变量的含义以及如何使用它们的更多信息,请参见此处 )。

有关正在运行的袜子流的信息应存储在相应的数据结构中。 在此结构中创建袜子线程时,我们必须编写:

  • yamux会话号:$ ymxstream;
  • 使用管道(通道)的4个变量:$ cipipe,$ copipe,$ sipipe和$ sopipe。 由于匿名通道可以在IN或OUT中工作,因此对于每个袜子流,我们需要两个匿名通道,每个匿名通道必须具有两端(管道流)(服务器和客户端)。
  • 对该流的调用结果为$ AsyncJobResult;
  • 流处理程序-$ Psobj。 通过它,我们将关闭流并释放资源;
  • 反向流($ readjob)从匿名管道异步读取的结果。 该变量在反向yamuxScript流中用于从相应管道进行异步读取;
  • 用于读取每个袜子流的数据的缓冲区;

主流


因此,从数据处理的角度来看,我们程序的工作构建如下:

  • 服务器端(rsockstun-在Golang上实现)提高ssl服务器并等待来自客户端的连接;
  • 当接收到来自客户端的连接时,服务器检查密码,如果正确,则建立yamux连接,提高socks端口并等待socks客户端(我们的代理链,浏览器等)的连接,并定期交换keepalive数据包与我们的客户。 如果密码不正确,则会重定向到我们在安装服务器时指定的页面(对于警惕的信息安全管理员来说,这是“合法”页面);
  • 当接收到来自袜子客户端的连接时,服务器会向我们的客户端发送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) 

循环完成后,我们必须根据Yamux的规范分析$ ymxbuffer变量中包含的12字节标头的类型和设置标志。

Yamux标头可以有几种类型:

  • ymx syn-安装新的流;
  • ymx fin-流完成;
  • ymx data-表示有关数据的信息(它们打算用于什么大小和什么流);
  • ymx ping-保持活动消息;
  • ymx win update-确认部分数据的传输;

任何不符合列出的yamux标题类型的情况都被视为例外情况。 有10个此类异常,我们认为这里有些问题,我们正在完成模块的工作。 (以及删除所有文件,擦拭磁盘,更改姓氏,换新护照,离开国家等)……)

创建一个新的袜子线程

接收到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) 

为socks流创建一个运行空间,设置共享变量以与该流交互(StopFlag),并运行脚本块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结构-Python中Dictionary的类似物

 [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数据处理

从yamux服务器收到目的地为任何socks流的数据后,我们必须确定yamux流的数量(此数据旨在用于socks流的数量)和12字节yamux标头中的数据字节数:

 $ymxstream = [bitconverter]::ToInt32($buffer[7..4],0) $ymxcount = [bitconverter]::ToInt32($buffer[11..8],0) 

然后,使用ymxId字段从ArrayList流中,获取与此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) 

然后,通过一个共享变量(或更确切地说,是一个标志数组,其中的索引是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 

然后关闭与此流相关的所有管道,关闭相应的运行空间并杀死Powershell对象以释放资源:

 $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.RemoveAt($streamind) 

最后,我们需要强制.Net垃圾收集器释放线程使用的资源。 否则,我们的脚本将消耗大约100-200 MB的内存,这可以吸引有经验的腐蚀性用户的注意,但是我们不需要这样做:

 [System.GC]::Collect()#clear garbage to minimize memory usage 

Yamux脚本-反向流


如上所述,从袜子流接收的数据由单独的yamuxScript流处理,该流从头开始(成功连接到服务器后)。 它的任务是定期轮询位于ArrayList $ stream中的socks流的输出管道:
 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还监视每个执行的socksScript线程在共享$ StopFlag数组中设置的标志。 如果使用socksScript的远程服务器断开连接,则可以将此标志设置为2。 在这种情况下,必须将信息报告给袜子客户。 链如下:yamuxScript必须通知yamux服务器有关断开连接的信息,以便依次向袜子客户端发出信号。

 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窗口更新


另外,yamuxScript应该监视从yamux服务器接收的字节数,并定期发送YMX WinUpdate消息。 Yamux中的此机制负责监视和更改所谓的窗口大小(类似于TCP协议)-无需确认即可发送的数据字节数。 默认情况下,窗口大小为256 KB。 这意味着,当发送或接收大于此大小的文件或数据时,我们需要将windpw更新包发送到yamux服务器。 为了控制从yamux服务器接收的数据量,引入了一个特殊的共享数组$ 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.streamId-yamux会话号;
  • $ state.inputStream-读取管道;
  • $ state.oututStream-写管道;

管道中的数据是原始格式,没有yamux标题,即 它们来自袜子客户的形式。

首先,在socksScript中,我们需要确定socks的版本并确保它是5:

 $state.inputStream.Read($buffer,0,2) | Out-Null $socksVer=$buffer[0] if ($socksVer -eq 5){ ... } 

好吧,然后我们完全按照Invoke-SocksProxy脚本中的实现进行操作。 唯一的不同是,它不会调用

 $AsyncJobResult.AsyncWaitHandle.WaitOne(); $AsyncJobResult2.AsyncWaitHandle.WaitOne(); 

必须以循环方式监视$ StopFlag数组中的tcp连接和相应的终止标志,否则我们将无法从socks客户端和ymux服务器的侧面识别连接结束的情况:

 while ($StopFlag[$state.StreamID] -eq 0 -and $tmpServ.Connected ){ start-sleep -Milliseconds 50 } 

如果连接在我们要连接的服务器的tcp端结束,则将此标志设置为2,这将迫使yamuxscript识别出该标志并将相应的ymx FIN数据包发送到yamux服务器:

 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 } 

结论到第二部分


在我们的编码研究过程中,我们设法为RsocksTun服务器创建了一个Powershell客户端,具有以下功能:

  • SSL连接
  • 服务器上的授权;
  • 与yamux-server合作,支持keepalive ping;
  • 多线程操作模式;
  • 支持传输大文件;

在本文之外,实现了通过代理服务器连接并对其进行授权以及将我们的脚本转换为可以从命令行运行的内联版本的功能的实现。 它将在第三部分中。

今天就这些了。 就像他们所说的那样-订阅,留下评论(特别是关于您对改进代码和添加功能的想法)。

Source: https://habr.com/ru/post/zh-CN453970/


All Articles