Escrevemos o proxy reverso socks5 no PowerShell.

A história da pesquisa e desenvolvimento em 3 partes. Parte 2 - desenvolvimento.
Existem muitas faias - ainda mais benefícios.

Na primeira parte do artigo, nos familiarizamos com algumas ferramentas para organizar túneis reversos, analisamos suas vantagens e desvantagens, estudamos o mecanismo de operação do multiplexador Yamux e descrevemos os requisitos básicos para o módulo PowerShell recém-criado. É hora de começar a desenvolver o módulo do powershell do cliente para a implementação pronta do túnel reverso RSocksTun .

Primeiro de tudo, precisamos entender em que modo nosso módulo funcionará. Obviamente, para a transferência primária de dados, precisaremos usar o mecanismo de soquete do Windows e os recursos .Net para transmitir leitura e gravação em soquetes. Mas, por outro lado, porque Como nosso módulo deve atender a vários fluxos do yamux ao mesmo tempo, todas as operações de E / S não devem bloquear completamente a execução do nosso programa. Isso sugere a conclusão de que nosso módulo deve usar multithreading de software e executar operações de leitura e gravação com um servidor yamux, bem como operações de leitura e gravação para servidores de destino em diferentes fluxos de programas. Bem, é claro, é necessário fornecer um mecanismo de interação entre nossos fluxos paralelos. Felizmente, o PowerShell oferece amplas oportunidades para iniciar e gerenciar fluxos de programas.

Algoritmo geral de trabalho


Assim, o algoritmo geral do nosso cliente deve ser algo como isto:

  • estabelecer uma conexão SSL com o servidor;
  • faça login com uma senha para que o servidor possa nos diferenciar de um agente de segurança;
  • aguarde o pacote yamux instalar um novo fluxo, respondendo periodicamente às solicitações de manutenção do servidor;
  • inicie um novo fluxo de programa socksScript (para não confundir com um fluxo) assim que o pacote yamux chegar para instalar um novo fluxo. Dentro do socksScript, implemente o trabalho do servidor socks5;
  • após a chegada do pacote com dados do yamux - entenda no cabeçalho de 12 bytes para qual dos fluxos os dados se destinam, bem como seu tamanho, leia os dados do servidor yamux e transfira os dados recebidos para o fluxo com o número de fluxo correspondente;
  • monitore periodicamente a disponibilidade dos dados destinados ao servidor yamux em cada um dos scripts de meias em execução. Se houver esses dados, adicione o cabeçalho de 12 bytes correspondente a eles e envie-o para o servidor yamux;
  • após a chegada de um pacote yamux para fechar o fluxo, transmita um sinal ao fluxo correspondente para encerrar e desconectar o fluxo e, depois disso, concluir o próprio fluxo;

Portanto, em nosso cliente, é necessário implementar pelo menos três fluxos de programas:

  1. o principal, que estabelecerá a conexão, efetue login no servidor yamux, receba dados dele, processe os cabeçalhos do yamux e envie dados brutos para outros fluxos de programas;
  2. fluxos com servidores de meias. Pode haver vários - um para cada fluxo. Eles implementam a funcionalidade socks5. Esses fluxos irão interagir com os pontos de destino na rede interna;
  3. fluxo reverso. Ele recebe dados de fluxos de meias, adiciona cabeçalhos do yamux a eles e os envia ao servidor do yamux;

E, é claro, precisamos prever a interação entre todos esses fluxos.

Precisamos não apenas fornecer essa interação, mas também obter a conveniência de transmitir entrada e saída (da mesma forma que os soquetes). O mecanismo mais apropriado seria usar pipes de software. No Windows, os pipes são registrados quando cada canal tem seu próprio nome e são anônimos - cada canal é identificado por seu manipulador. Por uma questão de segredo, é claro, usaremos tubos anônimos. (Afinal, não queremos que nosso módulo seja calculado usando tubos registrados no sistema - certo?). Assim, entre os fluxos principal / reverso e os fluxos de meias, a interação será por meio de tubos anônimos, suportando E / S de fluxo assíncrono. Entre os fluxos principal e de retorno, a comunicação ocorrerá através do mecanismo de objeto compartilhado (variáveis ​​sincronizadas compartilhadas) (mais sobre o que são essas variáveis ​​e como conviver com elas, você pode ler aqui ).

Informações sobre fluxos de meias em execução devem ser armazenadas na estrutura de dados correspondente. Ao criar um segmento de meias nessa estrutura, devemos escrever:

  • número da sessão do yamux: $ ymxstream;
  • 4 variáveis ​​para trabalhar com tubos (canais): $ cipipe, $ copipe, $ sipipe, $ sopipe. Como os canais anônimos funcionam em IN ou OUT, para cada fluxo de meias, precisamos de dois canais anônimos, cada um dos quais deve ter duas extremidades (pipestream) (servidor e cliente);
  • o resultado da chamada para o fluxo é $ AsyncJobResult;
  • manipulador de fluxo - $ Psobj. Através dele, fecharemos o fluxo e liberaremos recursos;
  • o resultado da leitura assíncrona do canal anônimo pelo fluxo reverso ($ readjob). Essa variável é usada no fluxo yamuxScript reverso para leitura assíncrona do canal correspondente;
  • buffer para leitura de dados para cada fluxo de meias;

Fluxo principal


Portanto, do ponto de vista do processamento de dados, o trabalho do nosso programa é construído da seguinte maneira:

  • o lado do servidor (rsockstun - implementado no Golang) eleva o servidor ssl e aguarda as conexões do cliente;
  • ao receber uma conexão do cliente, o servidor verifica a senha e, se estiver correta, estabelece uma conexão yamux, eleva a porta socks e aguarda as conexões dos clientes socks (nossos proxychains, navegador etc.), trocando periodicamente pacotes keepalive com o nosso cliente. Se a senha estiver incorreta - é realizado um redirecionamento para a página que especificamos ao instalar o servidor (esta é uma página "legal" para o administrador vigilante da segurança da informação);
  • ao receber uma conexão de um cliente de meias, o servidor envia um pacote yamux ao nosso cliente para estabelecer um novo fluxo (YMX SYN);

Obtendo e analisando um cabeçalho Yamux

Nosso módulo primeiro estabelece uma conexão SSL com o servidor e efetua login com uma senha:

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

Em seguida, o script aguarda um cabeçalho yamux de 12 bytes e o analisa.
Há uma pequena nuance ... Como mostra a prática, basta ler 12 bytes do soquete:

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

não é suficiente, pois a operação de leitura pode ser concluída após a chegada de apenas parte dos bytes necessários. Portanto, precisamos aguardar todos os 12 bytes no loop:

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

Após a conclusão do loop, devemos analisar o cabeçalho de 12 bytes contido na variável $ ymxbuffer para seu tipo e definir sinalizadores de acordo com a especificação do Yamux.

O cabeçalho do Yamux pode ser de vários tipos:

  • ymx syn - instale um novo fluxo;
  • ymx fin - conclusão do fluxo;
  • ymx data - representa informações sobre os dados (qual tamanho e para qual fluxo eles se destinam);
  • ymx ping - mensagem keepalive;
  • ymx win update - confirmação da transferência de uma parte dos dados;

Qualquer coisa que não se encaixe nos tipos listados de cabeçalhos yamux é considerada uma situação excepcional. 10 exceções, e acreditamos que algo está errado aqui e estamos concluindo o trabalho do nosso módulo. (além de apagar todos os nossos arquivos, limpe o disco, mude o sobrenome, faça um novo passaporte, saia do país etc. de acordo com a lista ...)

Criando um novo segmento de meias

Após receber um pacote yamux para estabelecer um novo fluxo, nosso cliente cria dois pipes de servidor anônimos ($ sipipe, $ sopipe), pois in / out, respectivamente, cria pipes de clientes ($ cipipe, $ copipe) com base neles:

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

cria um espaço de execução para o fluxo de meias, define variáveis ​​compartilhadas para interagir com esse fluxo (StopFlag) e executa o bloco de script SocksScript, que implementa a funcionalidade do servidor de meias em um fluxo separado:

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

As variáveis ​​criadas são gravadas em uma estrutura ArrayList especial - um análogo do Dictionary em Python

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

A adição ocorre através do método Add interno:

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

Processamento de Dados Yamux

Após o recebimento dos dados destinados a qualquer fluxo de meias do servidor yamux, devemos determinar o número do fluxo do yamux (o número do fluxo de meias ao qual esses dados se destinam) e o número de bytes de dados do cabeçalho do yamux de 12 bytes:

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

Em seguida, no fluxo ArrayList, usando o campo ymxId, obtemos os manipuladores do canal de saída do servidor correspondentes a esse fluxo de meias:

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

Depois disso, lemos os dados do soquete, lembrando que precisamos ler um certo número de bytes através do loop:

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

e escreva os dados recebidos no canal correspondente:

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


Processamento FIN do Yamux - Fluxo final

Quando recebemos um pacote do servidor yamix que sinaliza o fechamento de um fluxo, também obtemos primeiro o número do fluxo yamux no cabeçalho de 12 bytes:

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

então, através de uma variável compartilhada (ou melhor, uma matriz de sinalizadores, onde o índice é o número do fluxo do yamux), sinalizamos para que o segmento de meias seja concluído:

 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 } 

depois de definir o sinalizador, antes de interromper o fluxo de meias, é necessário aguardar um certo tempo para que o fluxo de meias processe esse sinalizador. 200 ms é suficiente para isso:

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

feche todos os pipes relacionados a esse fluxo, feche o Runspace correspondente e mate o 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() 

Depois de fechar o fluxo de meias, precisamos remover o elemento correspondente dos fluxos ArrayList:

 $streams.RemoveAt($streamind) 

E, no final, precisamos forçar o coletor de lixo .Net a liberar os recursos usados ​​pelo encadeamento. Caso contrário, nosso script consumirá cerca de 100-200 MB de memória, o que pode chamar a atenção de um usuário experiente e corrosivo, mas não precisamos disso:

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

Script Yamux - fluxo reverso


Como mencionado acima, os dados recebidos dos fluxos de meias são processados ​​por um fluxo yamuxScript separado, que começa desde o início (após uma conexão bem-sucedida ao servidor). Sua tarefa é pesquisar periodicamente os tubos de saída dos fluxos de meias localizados nos fluxos ArrayList $:
 foreach ($stream in $state.streams){ ... } 

e se houver dados neles, envie-os para o servidor yamux, após fornecer o cabeçalho correspondente do yamux de 12 bytes com o número da sessão do yamux e o número de bytes de dados:

  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" } 

O YamuxScript também monitora o sinalizador definido na matriz compartilhada $ StopFlag para cada um dos threads socksScript que são executados. Esse sinalizador pode ser definido como 2 se o servidor remoto que socksScript estiver trabalhando com desconexões. Nessa situação, as informações precisam ser relatadas ao cliente de meias. A cadeia é a seguinte: o yamuxScript deve informar o servidor do yamux sobre a desconexão, para que por sua vez sinalize isso para o cliente de meias.

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

Atualização da janela do Yamux


Além disso, o yamuxScript deve monitorar o número de bytes recebidos do servidor yamux e enviar periodicamente uma mensagem YMX WinUpdate. Esse mecanismo no Yamux é responsável por monitorar e alterar o chamado tamanho da janela (semelhante ao protocolo TCP) - o número de bytes de dados que podem ser enviados sem reconhecimento. Por padrão, o tamanho da janela é de 256 Kbytes. Isso significa que, ao enviar ou receber arquivos ou dados maiores que esse tamanho, precisamos enviar o pacote de atualização do windpw para o servidor yamux. Para controlar a quantidade de dados recebidos do servidor yamux, foi introduzida uma matriz compartilhada especial $ RcvBytes, na qual o fluxo principal, incrementando o valor atual, registra o número de bytes recebidos do servidor para cada fluxo. Se o limite definido for excedido, o yamuxScript deverá enviar um pacote ao servidor WinUpdate e redefinir o 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


Agora vamos passar diretamente para o próprio socksScript.
Lembre-se de que o socksScript é chamado de forma assíncrona:

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

e no momento da chamada, os seguintes dados estão presentes na variável $ state transferida para o fluxo:

  • $ state.streamId - número da sessão do yamux;
  • $ state.inputStream - canal de leitura;
  • $ state.oututStream - canal de gravação;

Os dados nos tubos são fornecidos em forma bruta sem cabeçalhos yamux, ou seja, na forma em que eles vieram do cliente de meias.

Dentro do socksScript, antes de tudo, precisamos determinar a versão do socks e garantir que seja 5:

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

Bem, então fazemos exatamente como implementado no script Invoke-SocksProxy. A única diferença será que, em vez de chamadas

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

É necessário monitorar a conexão tcp e o sinalizador de terminação correspondente na matriz $ StopFlag em um modo cíclico; caso contrário, não poderemos reconhecer a situação do final da conexão pelo lado do cliente de meias e do servidor ymux:

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

Caso a conexão termine no lado tcp do servidor ao qual estamos nos conectando, definimos esse sinalizador como 2, o que forçará o yamuxscript a reconhecer isso e enviar o pacote ymx FIN correspondente ao servidor yamux:

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

Também devemos definir esse sinalizador se o socksScript não puder se conectar ao servidor de destino:

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

Conclusão para a segunda parte


No decorrer de nossa pesquisa de codificação, fomos capazes de criar um cliente powershell para o servidor RsocksTun com a capacidade de:

  • Conexões SSL
  • autorização no servidor;
  • trabalhe com o yamux-server com suporte para pings keepalive;
  • modo de operação multithread;
  • suporte para transferência de arquivos grandes;

Fora do artigo, houve uma implementação da funcionalidade de conectar-se através de um servidor proxy e autorizá-lo, além de transformar nosso script em uma versão embutida, que pode ser executada na linha de comando. Será na terceira parte.

Isso é tudo por hoje. Como se costuma dizer - inscreva-se, como, deixe comentários (especialmente sobre seus pensamentos em melhorar o código e adicionar funcionalidade).

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


All Articles