Escribimos el proxy Reverse socks5 en powershell. Parte 2

La historia de la investigación y el desarrollo en 3 partes. Parte 2 - desarrollo.
Hay muchas hayas, incluso más beneficios.

En la primera parte del artículo, nos familiarizamos con algunas herramientas para organizar túneles inversos, analizamos sus ventajas y desventajas, estudiamos el mecanismo del multiplexor Yamux y describimos los requisitos básicos para el módulo PowerShell recién creado. Es hora de comenzar a desarrollar el módulo PowerShell del cliente para la implementación preparada del túnel inverso RSocksTun .

En primer lugar, debemos entender en qué modo funcionará nuestro módulo. Obviamente, para la transferencia de datos prima, tendremos que usar el mecanismo de socket de Windows y las capacidades .Net para transmitir lectura-escritura a sockets. Pero, por otro lado, porque Dado que nuestro módulo debe servir varias transmisiones de yamux al mismo tiempo, todas las operaciones de E / S no deberían bloquear completamente la ejecución de nuestro programa. Esto sugiere la conclusión de que nuestro módulo debe usar software de subprocesamiento múltiple y realizar operaciones de lectura y escritura con un servidor yamux, así como operaciones de lectura y escritura en servidores de destino en diferentes flujos de programas. Bueno, por supuesto, es necesario proporcionar un mecanismo de interacción entre nuestros flujos paralelos. Afortunadamente, powershell ofrece amplias oportunidades para iniciar y administrar flujos de programas.

Algoritmo general de trabajo


Por lo tanto, el algoritmo general de nuestro cliente debería ser algo como esto:

  • establecer una conexión SSL al servidor;
  • inicie sesión con una contraseña para que el servidor pueda distinguirnos de un oficial de seguridad;
  • espere a que el paquete yamux instale una nueva secuencia, respondiendo periódicamente a las solicitudes de mantenimiento del servidor;
  • inicie una nueva secuencia del programa socksScript (que no debe confundirse con una secuencia) tan pronto como llegue el paquete yamux para instalar una nueva secuencia. Dentro de socksScript, implemente el trabajo del servidor socks5;
  • a la llegada del paquete con datos de yamux: comprenda, a partir del encabezado de 12 bytes, a qué flujos están destinados los datos, así como su tamaño, lea los datos del servidor yamux y transfiera los datos recibidos al flujo con el número de flujo correspondiente;
  • supervise periódicamente la disponibilidad de datos destinados al servidor yamux en cada uno de los scripts de calcetines en ejecución. Si hay tales datos, agregue el encabezado de 12 bytes correspondiente y envíelos al servidor yamux;
  • a la llegada de un paquete yamux para cerrar el flujo, transmitir la señal al flujo correspondiente para finalizar el flujo y desconectarse, y después de eso, completar el flujo en sí;

Entonces, en nuestro cliente es necesario implementar al menos 3 flujos de programa:

  1. el principal, que establecerá la conexión, iniciará sesión en el servidor yamux, recibirá datos del mismo, procesará los encabezados de yamux y enviará datos sin procesar a otros flujos de programas;
  2. transmisiones con servidores de calcetines. Puede haber varios, uno para cada transmisión. Implementan la funcionalidad socks5. Estos flujos interactuarán con los puntos de destino en la red interna;
  3. flujo inverso. Recibe datos de secuencias de calcetines, les agrega encabezados yamux y los envía al servidor yamux;

Y, por supuesto, debemos prever la interacción entre todos estos flujos.

No solo necesitamos proporcionar dicha interacción, sino también obtener la conveniencia de la transmisión de entrada-salida (de manera similar a los sockets). El mecanismo más apropiado sería utilizar tuberías de software. En Windows, las tuberías se registran cuando cada tubería tiene su propio nombre y son anónimas: cada tubería se identifica por su controlador. En aras del secreto, por supuesto, utilizaremos tuberías anónimas. (Después de todo, no queremos que nuestro módulo se calcule utilizando tuberías registradas en el sistema, ¿sí?). Por lo tanto, entre los flujos principal / inverso y los flujos de calcetines, la interacción se realizará a través de tuberías anónimas, que admitirán E / S de flujo asíncrono. Entre los flujos principal y de retorno, la comunicación se llevará a cabo a través del mecanismo de objetos compartidos (variables sincronizadas compartidas) (puede leer más sobre qué son estas variables y cómo vivir con ellas aquí ).

La información sobre la ejecución de secuencias de calcetines debe almacenarse en la estructura de datos correspondiente. Al crear un hilo de calcetines en esta estructura, debemos escribir:

  • número de sesión de yamux: $ ymxstream;
  • 4 variables para trabajar con tuberías (canales): $ cipipe, $ copipe, $ sipipe, $ sopipe. Dado que los canales anónimos funcionan ya sea ENTRADA o SALIDA, para cada secuencia de calcetines necesitamos dos canales anónimos, cada uno de los cuales debe tener dos extremos (canalización) (servidor y cliente);
  • el resultado de la llamada a la transmisión es $ AsyncJobResult;
  • controlador de flujo - $ Psobj. A través de él cerraremos el flujo y liberaremos recursos;
  • El resultado de la lectura asíncrona de la tubería anónima por la secuencia inversa ($ readjob). Esta variable se usa en la secuencia inversa de yamuxScript para la lectura asíncrona de la tubería correspondiente;
  • buffer para leer datos para cada flujo de calcetines;

Corriente principal


Entonces, desde el punto de vista del procesamiento de datos, el trabajo de nuestro programa se construye de la siguiente manera:

  • el lado del servidor (rsockstun - implementado en Golang) levanta el servidor ssl y espera conexiones del cliente;
  • Al recibir una conexión del cliente, el servidor verifica la contraseña y, si es correcta, establece una conexión yamux, levanta el puerto de los calcetines y espera las conexiones de los clientes de los calcetines (nuestras cadenas proxy, navegador, etc.), intercambiando periódicamente paquetes keepalive. con nuestro cliente Si la contraseña es incorrecta, se realiza una redirección a la página que especificamos al instalar el servidor (esta es una página "legal" para el administrador vigilante de seguridad de la información);
  • cuando recibe una conexión de un cliente de socks, el servidor envía un paquete yamux a nuestro cliente para establecer una nueva transmisión (YMX SYN);

Obtener y analizar un encabezado de Yamux

Nuestro módulo primero establece una conexión SSL con el servidor e inicia sesión con una contraseña:

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

Luego, el script espera un encabezado yamux de 12 bytes y lo analiza.
Hay un pequeño matiz ... Como muestra la práctica, simplemente leyendo 12 bytes del socket:

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

no es suficiente, ya que la operación de lectura puede completarse después de la llegada de solo una parte de los bytes necesarios. Por lo tanto, debemos esperar los 12 bytes en el bucle:

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

Una vez que se completa el ciclo, debemos analizar el encabezado de 12 bytes contenido en la variable $ ymxbuffer para su tipo y establecer banderas de acuerdo con la especificación de Yamux.

El encabezado de Yamux puede ser de varios tipos:

  • ymx syn: instala una nueva transmisión;
  • aleta ymx: finalización del flujo;
  • datos ymx: representa información sobre los datos (de qué tamaño y para qué flujo están destinados);
  • ymx ping: mensaje de respuesta activa;
  • ymx win update: confirmación de la transferencia de una parte de los datos;

Cualquier cosa que no se ajuste a los tipos enumerados de encabezados yamux se considera una situación excepcional. Hay 10 de esas excepciones, y creemos que algo está mal aquí y estamos completando el trabajo de nuestro módulo. (así como borrar todos nuestros archivos, borrar el disco, cambiar el apellido, hacer un nuevo pasaporte, salir del país, etc. de acuerdo con la lista ...)

Crear un nuevo hilo de calcetines

Habiendo recibido un paquete yamux para establecer una nueva secuencia, nuestro cliente crea dos canales de servidor anónimos ($ sipipe, $ sopipe), para entrada / salida, respectivamente, crea tubos de cliente ($ cipipe, $ copipe) basados ​​en ellos:

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

crea un espacio de ejecución para la secuencia de calcetines, establece variables compartidas para interactuar con esta secuencia (StopFlag) y ejecuta el bloque de script SocksScript, que implementa la funcionalidad del servidor de calcetines en una secuencia separada:

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

Las variables creadas se escriben en una estructura ArrayList especial, un análogo de Dictionary en Python

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

La adición se realiza a través del método Agregar incorporado:

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

Procesamiento de datos de Yamux

Al recibir los datos destinados a cualquier flujo de calcetines del servidor yamux, debemos determinar el número del flujo de yamux (el número del flujo de calcetines para el cual están destinados estos datos) y el número de bytes de datos del encabezado yamux de 12 bytes:

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

Luego, desde la secuencia ArrayList, usando el campo ymxId, obtenemos los controladores de la salida del servidor correspondiente a esta secuencia de calcetines:

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

Después de eso, leemos los datos del socket, recordando que necesitamos leer una cierta cantidad de bytes a través del bucle:

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

y escriba los datos recibidos en la tubería correspondiente:

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


Procesamiento FIN de Yamux - Finalizar flujo

Cuando recibimos un paquete del servidor yamix que señala el cierre de una secuencia, también obtenemos primero el número de la secuencia yamux del encabezado de 12 bytes:

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

luego, a través de una variable compartida (o más bien, una matriz de indicadores, donde el índice es el número de flujo de yamux), le indicamos al hilo de calcetines que 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 } 

después de configurar la bandera, antes de eliminar la secuencia de calcetines, debe esperar una cierta cantidad de tiempo para que la secuencia de calcetines procese esta bandera. 200 ms es suficiente para esto:

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

luego cierre todas las tuberías relacionadas con esta secuencia, cierre el Runspace correspondiente y elimine el objeto Powershell para liberar recursos:

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

Después de cerrar la secuencia de calcetines, debemos eliminar el elemento correspondiente de las secuencias de ArrayList:

 $streams.RemoveAt($streamind) 

Y al final, necesitamos forzar al recolector de basura .Net a liberar los recursos utilizados por el hilo. De lo contrario, nuestro script consumirá aproximadamente 100-200 MB de memoria, lo que puede llamar la atención de un usuario experimentado y corrosivo, pero no necesitamos esto:

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

Script Yamux - flujo inverso


Como se mencionó anteriormente, los datos recibidos de las secuencias de calcetines son procesados ​​por una secuencia separada de yamuxScript, que comienza desde el principio (después de una conexión exitosa al servidor). Su tarea es sondear periódicamente las tuberías de salida de los flujos de calcetines ubicados en ArrayList $ streams:
 foreach ($stream in $state.streams){ ... } 

y si hay datos en ellos, envíelos al servidor yamux, después de proporcionar el encabezado yamux de 12 bytes correspondiente con el número de la sesión yamux y el número de bytes de datos:

  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 también supervisa el conjunto de indicadores en la matriz compartida $ StopFlag para cada uno de los hilos de socksScript que se ejecutan. Este indicador se puede establecer en 2 si el servidor remoto que socksScript funciona con desconexiones. En esta situación, la información debe informarse al cliente de calcetines. La cadena es la siguiente: yamuxScript debe informar al servidor yamux acerca de la desconexión para que a su vez se lo indique al cliente de calcetines.

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

Actualización de la ventana de Yamux


Además, yamuxScript debe monitorear la cantidad de bytes recibidos del servidor yamux y enviar periódicamente un mensaje YMX WinUpdate. Este mecanismo en Yamux es responsable de monitorear y cambiar el llamado tamaño de ventana (similar al protocolo TCP): la cantidad de bytes de datos que se pueden enviar sin acuse de recibo. Por defecto, el tamaño de la ventana es de 256 Kbytes. Esto significa que al enviar o recibir archivos o datos de un tamaño superior a este, debemos enviar el paquete de actualización windpw al servidor yamux. Para controlar la cantidad de datos recibidos del servidor yamux, se ha introducido una matriz compartida especial $ RcvBytes, en la cual la secuencia principal al incrementar el valor actual registra el número de bytes recibidos del servidor para cada secuencia. Si se supera el umbral establecido, yamuxScript debería enviar un paquete al servidor WinUpdate y restablecer el contador:

  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


Ahora pasemos directamente a socksScript.
Recuerde que socksScript se invoca de forma asíncrona:

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

y en el momento de la llamada, los siguientes datos están presentes en la variable $ state transferida a la secuencia:

  • $ state.streamId: número de sesión de yamux;
  • $ state.inputStream - leer canalización;
  • $ state.oututStream - tubería de escritura;

Los datos en las tuberías vienen en forma cruda sin encabezados yamux, es decir en la forma en que provienen del cliente de calcetines.

Dentro de socksScript, antes que nada, necesitamos determinar la versión de los calcetines y asegurarnos de que sea 5:

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

Bueno, entonces hacemos exactamente lo implementado en el script Invoke-SocksProxy. La única diferencia será que en lugar de llamadas

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

Es necesario monitorear la conexión tcp y el indicador de terminación correspondiente en la matriz $ StopFlag en un modo cíclico, de lo contrario no podremos reconocer la situación del final de la conexión desde el lado del cliente de socks y el servidor ymux:

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

En caso de que la conexión termine en el lado tcp del servidor al que nos estamos conectando, establecemos este indicador en 2, lo que obligará a yamuxscript a reconocer esto y enviará el paquete FIN ymx correspondiente al servidor yamux:

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

También debemos establecer este indicador si socksScript no puede conectarse al servidor de destino:

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

Conclusión de la segunda parte.


En el curso de nuestra investigación de codificación, logramos crear un cliente powershell para nuestro servidor RsocksTun con la capacidad de:

  • Conexiones SSL
  • autorización en el servidor;
  • trabajar con yamux-server con soporte para pings keepalive;
  • modo de operación multihilo;
  • soporte para transferir archivos grandes;

Fuera del artículo, hubo una implementación de la funcionalidad de conectarse a través de un servidor proxy y autorizar en él, así como convertir nuestro script en una versión en línea, que se puede ejecutar desde la línea de comandos. Será en la tercera parte.

Eso es todo por hoy. Como dicen: suscríbete, dale me gusta, deja comentarios (especialmente con respecto a tus pensamientos sobre cómo mejorar el código y agregar funcionalidad).

Source: https://habr.com/ru/post/453970/


All Articles