Die Geschichte von Forschung und Entwicklung in 3 Teilen. Teil 2 - Entwicklung.
Es gibt viele Buchen - noch mehr Vorteile.
Im ersten Teil des Artikels haben wir einige Tools zum Organisieren von Rückwärtstunneln kennengelernt, ihre Vor- und Nachteile untersucht, den Mechanismus des Yamux-Multiplexers untersucht und die grundlegenden Anforderungen für das neu erstellte Powershell-Modul beschrieben. Es ist Zeit, mit der Entwicklung des Client-Powershell-Moduls für die vorgefertigte Implementierung des
RSocksTun- Reverse-Tunnels zu beginnen.
Zunächst müssen wir verstehen, in welchem Modus unser Modul funktioniert. Offensichtlich müssen wir für die primäre Datenübertragung den Windows-Socket-Mechanismus und die .Net-Funktionen zum Streaming von Lese- und Schreibzugriff auf Sockets verwenden. Aber andererseits, weil Da unser Modul mehrere Yamux-Streams gleichzeitig bedienen muss, sollten alle E / A-Operationen die Ausführung unseres Programms nicht vollständig blockieren. Dies legt die Schlussfolgerung nahe, dass unser Modul Software-Multithreading verwenden und Lese- / Schreibvorgänge mit einem Yamux-Server sowie Lese- / Schreibvorgänge auf Zielservern in verschiedenen Programmströmen ausführen sollte. Natürlich ist es notwendig, einen Mechanismus für die Interaktion zwischen unseren parallelen Strömungen bereitzustellen. Glücklicherweise bietet Powershell zahlreiche Möglichkeiten zum Starten und Verwalten von Programmabläufen.
Allgemeiner Arbeitsalgorithmus
Daher sollte der allgemeine Algorithmus unseres Kunden ungefähr so aussehen:
- eine SSL-Verbindung zum Server herstellen;
- Melden Sie sich mit einem Passwort an, damit der Server uns von einem Sicherheitsbeauftragten unterscheiden kann.
- Warten Sie, bis das Yamux-Paket einen neuen Stream installiert hat, und antworten Sie regelmäßig auf Server-Keepalive-Anforderungen.
- Starten Sie einen neuen SocksScript-Programm-Stream (nicht zu verwechseln mit einem Stream), sobald das Yamux-Paket eintrifft, um einen neuen Stream zu installieren. Implementieren Sie in socksScript die Arbeit des socks5-Servers.
- Bei Eintreffen eines Pakets mit Daten von yamux - anhand eines 12-Byte-Headers verstehen, für welchen der Streams die Daten bestimmt sind, sowie deren Größe, die Daten vom yamux-Server lesen und die empfangenen Daten mit der entsprechenden Stream-Nummer an den Stream übertragen;
- Überwachen Sie regelmäßig die Verfügbarkeit von Daten, die für den Yamux-Server bestimmt sind, in jedem der laufenden Socken-Skripte. Wenn solche Daten vorhanden sind, fügen Sie ihnen den entsprechenden 12-Byte-Header hinzu und senden Sie ihn an den yamux-Server.
- Übertragen Sie bei Ankunft eines Yamux-Pakets zum Schließen des Streams das Signal an den entsprechenden Stream, um den Stream zu beenden und die Verbindung zu trennen, und vervollständigen Sie anschließend den Stream selbst.
In unserem Client müssen also mindestens 3 Programmabläufe implementiert werden:
- Der Hauptteil, der die Verbindung herstellt, sich beim Yamux-Server anmeldet, Daten von ihm empfängt, Yamux-Header verarbeitet und Rohdaten an andere Programmströme sendet.
- Streams mit Socken-Servern. Es kann mehrere geben - eine für jeden Stream. Sie implementieren die Funktionalität von socks5. Diese Flows interagieren mit Zielpunkten im internen Netzwerk.
- Rückfluss. Es empfängt Daten von Socken-Streams, fügt ihnen Yamux-Header hinzu und sendet sie an den Yamux-Server.
Und natürlich müssen wir für die Interaktion zwischen all diesen Flüssen sorgen.
Wir müssen nicht nur eine solche Interaktion bereitstellen, sondern auch die Bequemlichkeit des Streamings von Eingabe-Ausgabe (ähnlich wie bei Sockets) erhalten. Der am besten geeignete Mechanismus wäre die Verwendung von Software-Pipes. In Windows werden Pipes registriert, wenn jede Pipe ihren eigenen Namen hat, und anonym - jede Pipe wird von ihrem Handler identifiziert. Aus Gründen der Geheimhaltung werden wir natürlich anonyme Pipes verwenden. (Schließlich möchten wir nicht, dass unser Modul mithilfe registrierter Pipes im System berechnet wird - ja?) Somit erfolgt die Interaktion zwischen den Haupt- / Rückflussströmen und den Sockenflüssen über anonyme Pipes, die asynchrone Stream-Input-Output-Operationen unterstützen. Zwischen dem Haupt- und dem Rückfluss erfolgt die Kommunikation über den Mechanismus für gemeinsam genutzte Objekte (gemeinsam genutzte synchronisierte Variablen) (mehr darüber, was diese Variablen sind und wie man mit ihnen lebt, lesen Sie
hier ).
Informationen zum Ausführen von Sockenströmen sollten in der entsprechenden Datenstruktur gespeichert werden. Wenn wir einen Socken-Thread in dieser Struktur erstellen, müssen wir schreiben:
- Yamux-Sitzungsnummer: $ ymxstream;
- 4 Variablen für die Arbeit mit Pipes (Kanälen): $ cipipe, $ copipe, $ sipipe, $ sopipe. Da anonyme Kanäle entweder in IN oder OUT funktionieren, benötigen wir für jeden Socken-Stream zwei anonyme Kanäle, von denen jeder zwei Enden haben muss (Pipestream) (Server und Client).
- Das Ergebnis des Aufrufs des Streams ist $ AsyncJobResult.
- Stream-Handler - $ Psobj. Dadurch werden wir den Stream schließen und Ressourcen freigeben.
- das Ergebnis des asynchronen Lesens aus der anonymen Pipe durch den Reverse Stream ($ readjob). Diese Variable wird im umgekehrten YamuxScript-Stream zum asynchronen Lesen aus der entsprechenden Pipe verwendet.
- Puffer zum Lesen von Daten für jeden Sockenstrom;
Hauptstrom
Aus Sicht der Datenverarbeitung ist die Arbeit unseres Programms also wie folgt aufgebaut:
- Die Serverseite (rsockstun - auf Golang implementiert) löst den SSL-Server aus und wartet auf Verbindungen vom Client.
- Beim Empfang einer Verbindung vom Client überprüft der Server das Kennwort. Wenn es korrekt ist, stellt er eine Yamux-Verbindung her, erhöht den Socken-Port und wartet auf Verbindungen von Socken-Clients (unsere Proxy-Ketten, Browser usw.), wobei er regelmäßig Keepalive-Pakete austauscht mit unserem Kunden. Wenn das Passwort falsch ist, wird eine Weiterleitung zu der Seite ausgeführt, die wir bei der Installation des Servers angegeben haben (dies ist eine "legale" Seite für den aufmerksamen Administrator der Informationssicherheit).
- Nach Erhalt einer Verbindung von einem Socken-Client sendet der Server ein Yamux-Paket an unseren Client, um einen neuen Stream (YMX SYN) einzurichten.
Abrufen und Analysieren eines Yamux-HeadersUnser Modul stellt zunächst eine SSL-Verbindung zum Server her und meldet sich mit einem Kennwort an:
$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')
Dann wartet das Skript auf einen 12-Byte-Yamux-Header und analysiert ihn.
Es gibt eine kleine Nuance ... Wie die Praxis zeigt, lesen Sie einfach 12 Bytes aus dem Socket:
$num = $tcpStream.Read($tmpbuffer,0,12)
nicht genug, da der Lesevorgang nach dem Eintreffen nur eines Teils der erforderlichen Bytes abgeschlossen werden kann. Daher müssen wir auf alle 12 Bytes in der Schleife warten:
do { try { $num = $tcpStream.Read($tmpbuffer,0,12) } catch {} $tnum += $num $ymxbuffer += $tmpbuffer[0..($num-1)] }while ($tnum -lt 12 -and $tcpConnection.Connected)
Nachdem die Schleife abgeschlossen ist, sollten wir den in der Variablen $ ymxbuffer enthaltenen 12-Byte-Header auf seinen Typ analysieren und Flags gemäß der Spezifikation von Yamux setzen.
Es gibt verschiedene Arten von Yamux-Headern:
- ymx syn - Installiere einen neuen Stream;
- ymx fin - stream vervollständigung;
- ymx-Daten - stellen Informationen zu den Daten dar (für welche Größe und für welchen Stream sie bestimmt sind);
- ymx ping - Keepalive-Nachricht;
- ymx win update - Bestätigung der Übertragung eines Teils der Daten;
Alles, was nicht zu den aufgeführten Arten von Yamux-Headern passt, wird als Ausnahmesituation angesehen. Es gibt 10 solcher Ausnahmen, und wir glauben, dass hier etwas nicht stimmt, und wir schließen die Arbeit unseres Moduls ab.
(sowie alle unsere Dateien löschen, die Festplatte löschen, den Nachnamen ändern, einen neuen Pass erstellen, das Land verlassen usw. gemäß der Liste ...)Erstellen eines neuen SockenfadensNachdem unser Client ein Yamux-Paket zum Einrichten eines neuen Streams erhalten hat, erstellt er zwei anonyme Server-Pipes ($ sipipe, $ sopipe), für In / Out erstellt er Client-Pipes ($ cipipe, $ copipe) basierend auf diesen:
$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)
Erstellt einen Runspace für den Socken-Stream, legt gemeinsam genutzte Variablen für die Interaktion mit diesem Stream fest (StopFlag) und führt den Skriptblock SocksScript aus, der die Funktionalität des Socken-Servers in einem separaten Stream implementiert:
$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()
Die erstellten Variablen werden in eine spezielle ArrayList-Struktur geschrieben - ein Analogon zu Dictionary in Python
[System.Collections.ArrayList]$streams = @{}
Das Hinzufügen erfolgt über die integrierte Add-Methode:
$streams.add(@{ymxId=$ymxstream;cinputStream=$cipipe;sinputStream=$sipipe;coutputStream=$copipe;soutputStream=$sopipe;asyncobj=$AsyncJobResult;psobj=$PS;readjob=$null;readbuffer=$readbuffer}) | out-null
Yamux DatenverarbeitungNach dem Empfang von Daten, die für einen Socken-Stream bestimmt sind, vom Yamux-Server müssen wir die Nummer des Yamux-Streams (die Anzahl der Socken-Streams, für die diese Daten bestimmt sind) und die Anzahl der Datenbytes aus dem 12-Byte-Yamux-Header bestimmen:
$ymxstream = [bitconverter]::ToInt32($buffer[7..4],0) $ymxcount = [bitconverter]::ToInt32($buffer[11..8],0)
Dann erhalten wir aus dem ArrayList-Stream unter Verwendung des Felds ymxId die Handler der Server-Out-Pipe, die diesem Socken-Stream entsprechen:
if ($streams.Count -gt 1){$streamind = $streams.ymxId.IndexOf($ymxstream)} else {$streamind = 0} $outStream = $streams[$streamind].soutputStream
Danach lesen wir die Daten aus dem Socket und denken daran, dass wir eine bestimmte Anzahl von Bytes durch die Schleife lesen müssen:
$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)
und schreiben Sie die empfangenen Daten in die entsprechende Pipe:
$num = $tcpStream.Read($buffer,0,$ymxcount) $outStream.Write($buffer,0,$ymxcount)
Yamux FIN-Verarbeitung - EndstromWenn wir vom Yamix-Server ein Paket empfangen, das das Schließen eines Streams signalisiert, erhalten wir auch zuerst die Nummer des Yamux-Streams aus dem 12-Byte-Header:
$ymxstream = [bitconverter]::ToInt32($buffer[7..4],0)
Dann signalisieren wir über eine gemeinsam genutzte Variable (oder besser gesagt ein Array von Flags, wobei der Index die Yamux-Stream-Nummer ist), dass der Socken-Thread abgeschlossen ist:
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 }
Nachdem Sie das Flag gesetzt haben, müssen Sie vor dem Beenden des Socken-Streams eine gewisse Zeit warten, bis der Socken-Stream dieses Flag verarbeitet. Dafür reichen 200 ms:
start-sleep -milliseconds 200 #wait for thread check flag
Schließen Sie dann alle Pipes, die sich auf diesen Stream beziehen, schließen Sie den entsprechenden Runspace und beenden Sie das Powershell-Objekt, um Ressourcen freizugeben:
$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()
Nach dem Schließen des Socken-Streams müssen wir das entsprechende Element aus den ArrayList-Streams entfernen:
$streams.RemoveAt($streamind)
Und am Ende müssen wir den .Net-Garbage Collector zwingen, die vom Thread verwendeten Ressourcen freizugeben. Andernfalls verbraucht unser Skript etwa 100 bis 200 MB Speicher, was die Aufmerksamkeit eines erfahrenen und ätzenden Benutzers auf sich ziehen kann. Wir benötigen dies jedoch nicht:
[System.GC]::Collect()#clear garbage to minimize memory usage
Yamux Script - Rückfluss
Wie oben erwähnt, werden von Socken-Streams empfangene Daten von einem separaten YamuxScript-Stream verarbeitet, der von Anfang an beginnt (nach einer erfolgreichen Verbindung zum Server). Seine Aufgabe besteht darin, die Ausgabepipes von Socken-Streams in ArrayList $ -Streams regelmäßig abzufragen:
foreach ($stream in $state.streams){ ... }
und wenn sie Daten enthalten, senden Sie sie an den Yamux-Server, nachdem Sie zuvor den entsprechenden 12-Byte-Yamux-Header bereitgestellt haben, der die Nummer der Yamux-Sitzung und die Anzahl der Datenbytes enthält:
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 überwacht auch das im freigegebenen $ StopFlag-Array gesetzte Flag für jeden der ausgeführten socksScript-Threads. Dieses Flag kann auf 2 gesetzt werden, wenn der Remote-Server, auf dem socksScript ausgeführt wird, die Verbindung trennt. In dieser Situation müssen die Informationen an den Socken-Client gemeldet werden. Die Kette ist wie folgt: yamuxScript muss den yamux-Server über die Trennung informieren, damit dies wiederum dem Socken-Client signalisiert.
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 Fenster Update
Darüber hinaus sollte yamuxScript die Anzahl der vom yamux-Server empfangenen Bytes überwachen und regelmäßig eine YMX-WinUpdate-Nachricht senden. Dieser Mechanismus in Yamux ist für die Überwachung und Änderung der sogenannten Fenstergröße (ähnlich dem TCP-Protokoll) verantwortlich - der Anzahl der Datenbytes, die ohne Bestätigung gesendet werden können. Standardmäßig beträgt die Fenstergröße 256 KB. Dies bedeutet, dass wir beim Senden oder Empfangen von Dateien oder Daten, die größer als diese Größe sind, das windpw-Aktualisierungspaket an den yamux-Server senden müssen. Um die Menge der vom Yamux-Server empfangenen Daten zu steuern, wurde ein spezielles gemeinsam genutztes Array $ RcvBytes eingeführt, in das der Hauptstrom durch Inkrementieren des aktuellen Werts die Anzahl der vom Server empfangenen Bytes für jeden Strom aufzeichnet. Wenn der festgelegte Schwellenwert überschritten wird, sollte yamuxScript ein Paket an den WinUpdate-Server senden und den Zähler zurücksetzen:
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
Gehen wir jetzt direkt zu socksScript.
Denken Sie daran, dass socksScript asynchron aufgerufen wird:
$state = [PSCustomObject]@{"StreamID"=$ymxstream;"inputStream"=$cipipe;"outputStream"=$copipe} $PS = [PowerShell]::Create() .... $AsyncJobResult = $PS.BeginInvoke()
und zum Zeitpunkt des Aufrufs sind die folgenden Daten in der $ state-Variablen vorhanden, die an den Stream übertragen wird:
- $ state.streamId - yamux Sitzungsnummer;
- $ state.inputStream - Pipe lesen;
- $ state.oututStream - Pipe schreiben;
Die Daten in den Pipes kommen in Rohform ohne Yamux-Header, d. H. in der Form, in der sie vom Socken-Client kamen.
In socksScript müssen wir zunächst die Version der Socken bestimmen und sicherstellen, dass es 5 ist:
$state.inputStream.Read($buffer,0,2) | Out-Null $socksVer=$buffer[0] if ($socksVer -eq 5){ ... }
Nun, dann machen wir genau das, was im Invoke-SocksProxy-Skript implementiert ist. Der einzige Unterschied wird sein, dass anstelle von Anrufen
$AsyncJobResult.AsyncWaitHandle.WaitOne(); $AsyncJobResult2.AsyncWaitHandle.WaitOne();
Es ist notwendig, die TCP-Verbindung und das entsprechende Beendigungsflag im $ StopFlag-Array in einem zyklischen Modus zu überwachen, da wir sonst die Situation des Verbindungsende von der Seite des Socken-Clients und des ymux-Servers nicht erkennen können:
while ($StopFlag[$state.StreamID] -eq 0 -and $tmpServ.Connected ){ start-sleep -Milliseconds 50 }
Falls die Verbindung auf der TCP-Seite des Servers endet, zu dem wir eine Verbindung herstellen, setzen wir dieses Flag auf 2, wodurch yamuxscript dies erkennt und das entsprechende ymx-FIN-Paket an den yamux-Server sendet:
if ($tmpServ.Connected){ $tmpServ.close() }else{ $StopFlag[$state.StreamID] = 2 }
Wir müssen dieses Flag auch setzen, wenn socksScript keine Verbindung zum Zielserver herstellen kann:
if($tmpServ.Connected){ ... } else{ $buffer[1]=4 $state.outputStream.Write($buffer,0,2) $StopFlag[$state.StreamID] = 2 }
Fazit zum zweiten Teil
Im Verlauf unserer Codierungsforschung konnten wir einen Powershell-Client für unseren RsocksTun-Server mit den folgenden Funktionen erstellen:
- SSL-Verbindungen
- Autorisierung auf dem Server;
- Arbeit mit Yamux-Server mit Unterstützung für Keepalive-Pings;
- Multithread-Betriebsart;
- Unterstützung für die Übertragung großer Dateien;
Außerhalb des Artikels wurde die Funktionalität implementiert, eine Verbindung über einen Proxyserver herzustellen und zu autorisieren sowie unser Skript in eine Inline-Version umzuwandeln, die über die Befehlszeile ausgeführt werden kann. Es wird im dritten Teil sein.
Das ist alles für heute. Wie sie sagen - abonnieren Sie gerne Kommentare (insbesondere in Bezug auf Ihre Gedanken zur Verbesserung des Codes und zum Hinzufügen von Funktionen).