L'histoire de la recherche et du développement en 3 parties. Partie 2 - développement.
Il existe de nombreux hêtres - encore plus d'avantages.
Dans la première partie de l' article, nous nous sommes familiarisés avec certains outils d'organisation des tunnels inverses, avons examiné leurs avantages et leurs inconvénients, étudié le mécanisme du multiplexeur Yamux et décrit les exigences de base du module PowerShell nouvellement créé. Il est temps de commencer à développer le module PowerShell client pour l'implémentation prête à l'emploi du tunnel inverse
RSocksTun .
Tout d'abord, nous devons comprendre dans quel mode notre module fonctionnera. Évidemment, pour le transfert de données prima, nous devrons utiliser le mécanisme de socket Windows et les capacités .Net pour diffuser en lecture-écriture sur les sockets. Mais, d'autre part, parce que Puisque notre module doit servir plusieurs flux yamux en même temps, toutes les opérations d'E / S ne doivent pas bloquer complètement l'exécution de notre programme. Cela suggère la conclusion que notre module devrait utiliser le multithreading logiciel et effectuer des opérations de lecture-écriture avec un serveur yamux, ainsi que des opérations de lecture-écriture sur les serveurs de destination dans différents flux de programme. Eh bien, bien sûr, il est nécessaire de prévoir un mécanisme d'interaction entre nos flux parallèles. Heureusement, powershell offre de nombreuses possibilités de lancement et de gestion des flux de programmes.
Algorithme général de travail
Ainsi, l'algorithme général de notre client devrait ressembler à ceci:
- établir une connexion SSL avec le serveur;
- connectez-vous avec un mot de passe pour que le serveur puisse nous distinguer d'un responsable de la sécurité;
- attendez que le package yamux installe un nouveau flux, répondant périodiquement aux demandes de maintien du serveur;
- démarrer un nouveau flux de programme socksScript (à ne pas confondre avec un flux) dès que le paquet yamux arrive pour installer un nouveau flux. À l'intérieur de socksScript, implémentez le travail du serveur socks5;
- à l'arrivée d'un paquet avec des données de yamux - comprendre à partir d'un en-tête de 12 octets à quels flux les données sont destinées, ainsi que leur taille, lire les données du serveur yamux et transférer les données reçues vers le flux avec le numéro de flux correspondant;
- surveiller périodiquement la disponibilité des données destinées au serveur yamux dans chacun des scripts de chaussettes en cours d'exécution. S'il existe de telles données, ajoutez-leur l'en-tête de 12 octets correspondant et envoyez-le au serveur yamux;
- à l'arrivée d'un paquet yamux pour fermer le flux, transmettre un signal au flux correspondant pour terminer le flux et se déconnecter, et après cela, terminer le flux lui-même;
Ainsi, chez notre client, il est nécessaire d'implémenter au moins 3 flux de programme:
- le principal, qui établira la connexion, se connectera au serveur yamux, en recevra les données, traitera les en-têtes yamux et enverra des données déjà brutes à d'autres flux de programme;
- flux avec des serveurs de chaussettes. Il peut y en avoir plusieurs - un pour chaque flux. Ils implémentent la fonctionnalité socks5. Ces flux interagiront avec les points de destination sur le réseau interne;
- flux inversé. Il reçoit les données des flux de chaussettes, leur ajoute des en-têtes yamux et les envoie au serveur yamux;
Et, bien sûr, nous devons prévoir l'interaction entre tous ces flux.
Nous devons non seulement fournir une telle interaction, mais également obtenir la commodité du streaming d'entrée-sortie (de manière similaire aux sockets). Le mécanisme le plus approprié serait d'utiliser des canaux logiciels. Sous Windows, les canaux sont enregistrés lorsque chaque canal a son propre nom, et anonymes - chaque canal est identifié par son gestionnaire. Par souci de confidentialité, nous utiliserons bien sûr des canaux anonymes. (Après tout, nous ne voulons pas que notre module soit calculé en utilisant des tuyaux enregistrés dans le système - non?). Ainsi, entre les flux principaux / inverses et les flux de chaussettes, l'interaction se fera via des canaux anonymes, prenant en charge les E / S de flux asynchrones. Entre le flux principal et le flux de retour, la communication se fera via le mécanisme d'objet partagé (variables synchronisées partagées) (vous pouvez lire
ici plus d'informations sur ces variables et comment vivre avec elles).
Les informations sur l'exécution des flux de chaussettes doivent être stockées dans la structure de données correspondante. Lors de la création d'un fil de chaussettes dans cette structure, nous devons écrire:
- numéro de session yamux: $ ymxstream;
- 4 variables pour travailler avec des pipes (canaux): $ cipipe, $ copipe, $ sipipe, $ sopipe. Étant donné que les canaux anonymes fonctionnent en IN ou OUT, pour chaque flux de chaussettes, nous avons besoin de deux canaux anonymes, dont chacun doit avoir deux extrémités (pipestream) (serveur et client);
- le résultat de l'appel au flux est $ AsyncJobResult;
- gestionnaire de flux - $ Psobj. Grâce à lui, nous fermerons le flux et libérerons des ressources;
- le résultat de la lecture asynchrone du canal anonyme par le flux inverse ($ readjob). Cette variable est utilisée dans le flux yamuxScript inverse pour la lecture asynchrone à partir du canal correspondant;
- tampon pour lire les données pour chaque flux de chaussettes;
Flux principal
Ainsi, du point de vue du traitement des données, le travail de notre programme est construit comme suit:
- le côté serveur (rsockstun - implémenté sur Golang) lève le serveur ssl et attend les connexions du client;
- à la réception d'une connexion du client, le serveur vérifie le mot de passe, et s'il est correct, établit une connexion yamux, augmente le port des chaussettes et attend les connexions des clients de chaussettes (nos chaînes proxy, navigateur, etc.), échangeant périodiquement des paquets persistants avec notre client. Si le mot de passe est incorrect - une redirection vers la page que nous avons spécifiée lors de l'installation du serveur est effectuée (il s'agit d'une page "légale" pour l'administrateur vigilant de la sécurité de l'information);
- à la réception d'une connexion d'un client chaussettes, le serveur envoie un paquet yamux à notre client pour établir un nouveau flux (YMX SYN);
Obtenir et analyser un en-tête YamuxNotre module établit d'abord une connexion SSL au serveur et se connecte avec un mot de passe:
$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')
Ensuite, le script attend un en-tête yamux de 12 octets et l'analyse.
Il y a une petite nuance ... Comme le montre la pratique, il suffit de lire 12 octets à partir du socket:
$num = $tcpStream.Read($tmpbuffer,0,12)
pas assez, car l'opération de lecture peut être terminée après l'arrivée d'une partie seulement des octets nécessaires. Par conséquent, nous devons attendre les 12 octets de la boucle:
do { try { $num = $tcpStream.Read($tmpbuffer,0,12) } catch {} $tnum += $num $ymxbuffer += $tmpbuffer[0..($num-1)] }while ($tnum -lt 12 -and $tcpConnection.Connected)
Une fois la boucle terminée, nous devons analyser l'en-tête de 12 octets contenu dans la variable $ ymxbuffer pour son type et définir des indicateurs conformément aux spécifications de Yamux.
L'en-tête Yamux peut être de plusieurs types:
- ymx syn - installe un nouveau flux;
- ymx fin - achèvement du flux;
- données ymx - représente des informations sur les données (quelle taille et quel flux elles sont destinées);
- ymx ping - message keepalive;
- mise à jour ymx win - confirmation du transfert d'une partie des données;
Tout ce qui ne correspond pas aux types d'en-têtes yamux répertoriés est considéré comme une situation exceptionnelle. Il y a 10 exceptions de ce type, et nous pensons que quelque chose ne va pas ici et nous terminons le travail de notre module.
(ainsi qu'effacer tous nos fichiers, essuyer le disque, changer le nom de famille, faire un nouveau passeport, quitter le pays, etc. selon la liste ...)Création d'un nouveau fil de chaussettesAyant reçu un package yamux pour établir un nouveau flux, notre client crée deux canaux de serveur anonyme ($ sipipe, $ sopipe), pour in / out, respectivement, crée des canaux client ($ cipipe, $ copipe) en fonction d'eux:
$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)
crée un espace d'exécution pour le flux socks, définit des variables partagées pour interagir avec ce flux (StopFlag) et exécute le scriptblock SocksScript, qui implémente la fonctionnalité du serveur socks dans un flux distinct:
$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()
Les variables créées sont écrites dans une structure spéciale ArrayList - un analogue de Dictionary in Python
[System.Collections.ArrayList]$streams = @{}
L'ajout s'effectue via la méthode Add intégrée:
$streams.add(@{ymxId=$ymxstream;cinputStream=$cipipe;sinputStream=$sipipe;coutputStream=$copipe;soutputStream=$sopipe;asyncobj=$AsyncJobResult;psobj=$PS;readjob=$null;readbuffer=$readbuffer}) | out-null
Traitement des données YamuxLors de la réception des données destinées à tout flux de chaussettes du serveur yamux, nous devons déterminer le numéro du flux yamux (le nombre de flux de chaussettes auquel ces données sont destinées) et le nombre d'octets de données de l'en-tête yamux de 12 octets:
$ymxstream = [bitconverter]::ToInt32($buffer[7..4],0) $ymxcount = [bitconverter]::ToInt32($buffer[11..8],0)
Ensuite, à partir du flux ArrayList, en utilisant le champ ymxId, nous obtenons les gestionnaires du serveur sortant correspondant à ce flux de chaussettes:
if ($streams.Count -gt 1){$streamind = $streams.ymxId.IndexOf($ymxstream)} else {$streamind = 0} $outStream = $streams[$streamind].soutputStream
Après cela, nous lisons les données du socket, en nous rappelant que nous devons lire un certain nombre d'octets dans la boucle:
$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)
et écrivez les données reçues dans le tube correspondant:
$num = $tcpStream.Read($buffer,0,$ymxcount) $outStream.Write($buffer,0,$ymxcount)
Traitement Yamux FIN - Fin du fluxLorsque nous recevons un paquet du serveur yamix qui signale la fermeture d'un flux, nous obtenons également d'abord le numéro du flux yamux à partir de l'en-tête de 12 octets:
$ymxstream = [bitconverter]::ToInt32($buffer[7..4],0)
puis, à travers une variable partagée (ou plutôt, un tableau de drapeaux, où l'index est le numéro de flux yamux), nous signalons au thread socks de se terminer:
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 }
après avoir défini l'indicateur, avant de tuer le flux de chaussettes, vous devez attendre un certain temps pour que le flux de chaussettes traite cet indicateur. 200 ms suffisent pour cela:
start-sleep -milliseconds 200 #wait for thread check flag
puis fermez tous les tuyaux liés à ce flux, fermez le Runspace correspondant et tuez l'objet Powershell pour libérer des ressources:
$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()
Après avoir fermé le flux de chaussettes, nous devons supprimer l'élément correspondant des flux ArrayList:
$streams.RemoveAt($streamind)
Et à la fin, nous devons forcer le garbage collector .Net à libérer les ressources utilisées par le thread. Sinon, notre script consommera environ 100-200 Mo de mémoire, ce qui peut attirer l'attention d'un utilisateur expérimenté et corrosif, mais nous n'en avons pas besoin:
[System.GC]::Collect()#clear garbage to minimize memory usage
Script Yamux - flux inversé
Comme mentionné ci-dessus, les données reçues des flux de chaussettes sont traitées par un flux yamuxScript distinct, qui démarre depuis le tout début (après une connexion réussie au serveur). Sa tâche consiste à interroger périodiquement les canaux de sortie des flux de chaussettes situés dans les flux ArrayList $:
foreach ($stream in $state.streams){ ... }
et si elles contiennent des données, envoyez-les au serveur yamux, après avoir fourni l'en-tête yamux de 12 octets correspondant contenant le numéro de la session yamux et le nombre d'octets de données:
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 surveille également l'indicateur défini dans le tableau $ StopFlag partagé pour chacun des threads socksScript qui sont exécutés. Cet indicateur peut être défini sur 2 si le serveur distant qui utilise socksScript se déconnecte. Dans cette situation, les informations doivent être rapportées au client chaussettes. La chaîne est la suivante: yamuxScript doit informer le serveur yamux de la déconnexion afin qu'il le signale à son tour au client 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() }
Mise à jour de la fenêtre Yamux
De plus, yamuxScript doit surveiller le nombre d'octets reçus du serveur yamux et envoyer périodiquement un message YMX WinUpdate. Ce mécanisme dans Yamux est responsable de la surveillance et de la modification de la soi-disant taille de fenêtre (similaire au protocole TCP) - le nombre d'octets de données qui peuvent être envoyés sans accusé de réception. Par défaut, la taille de la fenêtre est de 256 Ko. Cela signifie que lors de l'envoi ou de la réception de fichiers ou de données supérieurs à cette taille, nous devons envoyer le package de mise à jour windpw au serveur yamux. Pour contrôler la quantité de données reçues du serveur yamux, un tableau partagé spécial $ RcvBytes a été introduit, dans lequel le flux principal en incrémentant la valeur actuelle enregistre le nombre d'octets reçus du serveur pour chaque flux. Si le seuil défini est dépassé, yamuxScript doit envoyer un paquet au serveur WinUpdate et réinitialiser le compteur:
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
Passons maintenant directement à socksScript lui-même.
Rappelons que socksScript est invoqué de manière asynchrone:
$state = [PSCustomObject]@{"StreamID"=$ymxstream;"inputStream"=$cipipe;"outputStream"=$copipe} $PS = [PowerShell]::Create() .... $AsyncJobResult = $PS.BeginInvoke()
et au moment de l'appel, les données suivantes sont présentes dans la variable $ state transférée au flux:
- $ state.streamId - numéro de session yamux;
- $ state.inputStream - lire le tube;
- $ state.oututStream - écrire un tube;
Les données dans les tuyaux sont fournies sous forme brute sans en-têtes yamux, c'est-à-dire sous la forme dans laquelle ils provenaient du client chaussettes.
À l'intérieur de socksScript, tout d'abord, nous devons déterminer la version des chaussettes et nous assurer qu'elle est de 5:
$state.inputStream.Read($buffer,0,2) | Out-Null $socksVer=$buffer[0] if ($socksVer -eq 5){ ... }
Eh bien, nous faisons exactement ce qui est implémenté dans le script Invoke-SocksProxy. La seule différence sera qu'au lieu d'appels
$AsyncJobResult.AsyncWaitHandle.WaitOne(); $AsyncJobResult2.AsyncWaitHandle.WaitOne();
Il est nécessaire de surveiller la connexion tcp et l'indicateur de terminaison correspondant dans le tableau $ StopFlag en mode cyclique, sinon nous ne pourrons pas reconnaître la situation de la fin de la connexion du côté du client socks et du serveur ymux:
while ($StopFlag[$state.StreamID] -eq 0 -and $tmpServ.Connected ){ start-sleep -Milliseconds 50 }
Dans le cas où la connexion se termine du côté TCP du serveur auquel nous nous connectons, nous définissons cet indicateur sur 2, ce qui amènera yamuxscript à le reconnaître et à envoyer le paquet ymx FIN correspondant au serveur yamux:
if ($tmpServ.Connected){ $tmpServ.close() }else{ $StopFlag[$state.StreamID] = 2 }
Nous devons également définir cet indicateur si socksScript ne peut pas se connecter au serveur de destination:
if($tmpServ.Connected){ ... } else{ $buffer[1]=4 $state.outputStream.Write($buffer,0,2) $StopFlag[$state.StreamID] = 2 }
Conclusion de la deuxième partie
Au cours de nos recherches de codage, nous avons réussi à créer un client PowerShell pour notre serveur RsocksTun avec la capacité:
- Connexions SSL
- autorisation sur le serveur;
- travailler avec yamux-server avec le support des pings keepalive;
- mode de fonctionnement multithread;
- prise en charge du transfert de fichiers volumineux;
En dehors de l'article, il y avait une implémentation de la fonctionnalité de connexion via un serveur proxy et d'autorisation sur celui-ci, ainsi que la transformation de notre script en une version en ligne, qui peut être exécutée à partir de la ligne de commande. Ce sera dans la troisième partie.
C'est tout pour aujourd'hui. Comme ils disent - abonnez-vous, laissez des commentaires (en particulier concernant vos réflexions sur l'amélioration du code et l'ajout de fonctionnalités).