目录
介绍
BinaryToPowershellScript 是一个.NET Core控制台应用程序,它将一个或多个二进制文件转换为Powershell脚本,如果在服务器上执行,则会在其中重新创建完全相同的脚本文件集。
背景
主要思想是将二进制文件转换为重新创建相同二进制文件的脚本。我之所以选择Powershell,是因为它是一项我知道的最新技术,而且它的Powershell Core版本适用于所有主要平台(Windows、Linux和Mac),但同样,一切都可以在纯bash(Linux)或bat文件(Windows,在这种情况下很可能加密不可用)中重新实现。从二进制到脚本的转换主要是将二进制转换为文本,我想出了三种可能性:
- 使用Base64编码:此编码是在JSON文件/API中对二进制文件进行编码的标准方式。基本上,它将二进制文件细分为6位(因此略小于一个字节),并将这64个可能的值映射到64个ASCII字符(因此为1个字节)。这种编码的缺点是将大小增加33%(8位/6位),但如果不增加大小,就不可能进行二进制到文本的转换。另一个需要注意的是,这种编码可以被检测为对此类脚本的反制措施,即使这种检测也可能给其他合法脚本带来麻烦。
- 使用十六进制文本格式:此编码将二进制文件的每个字节转换为其2位ASCII表示形式(YZ,即2个字节)。这种编码是浪费的,因为它将原始大小乘以2倍。从历史上看,字节总是以十六进制格式表示在十六进制编辑器中(对于Windows,您可以尝试良好的HxD)。
- 使用十进制格式:此编码将二进制文件的每个字节转换为最多4位ASCII表示形式(0-255,从2到4字节)。这是最浪费的格式,但输出PowerShell脚本具有最简单的代码,因此它几乎适用于powershell的每个实例/配置。
使用代码
您可以在我的GitHub页面上找到源代码,其中有一份关于如何使用控制台应用程序的综合自述文件,其中包含命令行和生成的脚本示例。
我现在想专注于代码,基本上,我们只有一个文件Program.cs包含以下代码:
using System;
using System.Collections;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.IO;
using System.IO.Compression;
using System.Reflection.Metadata.Ecma335;
using System.Security.Cryptography;
using System.Text;
using CommandLine;
namespace BinaryToPowershellScript
{
public class Options
{
[Option('i', "inputs", Required = true,
HelpText = "Specifies the input file(s) to process,
you can use also a wildcard pattern or
specify multiple files separated by space")]
public IEnumerable<String>? Inputs { get; set; }
[Option('o', "outputfolder", Required = false,
HelpText = "Specify the output folder where all the powershell scripts
will be generated")]
public String? OutputFolder { get; set; }
[Option('b', "base64", Required = false,
HelpText = "Specify the base64 file format for the powershell script(s)")]
public bool Base64 { get; set; }
[Option('d', "decimal", Required = false,
HelpText = "Specify the decimal file format for the powershell script(s)")]
public bool Decimal { get; set; }
[Option('c', "compress", Required = false,
HelpText = "Specify to compress the input file(s) with gzip compression")]
public bool Compress { get; set; }
[Option('h', "hash", Required = false,
HelpText = "Specify to add a SHA256 hash as check on
file(s) integrity in the powershell script(s)")]
public bool Hash { get; set; }
[Option('s', "single", Required = false,
HelpText = "Specify to create just a single script file for all input files")]
public bool SingleFile { get; set; }
[Option('p', "password", Required = false,
HelpText = "Specify the password used to encrypt data with AES")]
public String? Password { get; set; }
[Option('r', "recurse", Required = false,
HelpText = "Specify to perform recursive search on all input file(s)")]
public bool Recurse { get; set; }
}
class Program
{
const int KEYSIZE = 256;
public static void Main(string[] args)
{
Parser.Default.ParseArguments<Options>
(args).WithParsed<Options>(o => CreateScript(o));
}
static string ComputeSha256Hash(byte[] bytes)
{
using (SHA256 sha256Hash = SHA256.Create())
{
return BitConverter.ToString
(sha256Hash.ComputeHash(bytes)).Replace("-", String.Empty);
}
}
public static byte[] EncryptBytes(byte[] input, string password)
{
var pbkdf2DerivedBytes = new Rfc2898DeriveBytes(password, 16, 2000);
using (var AES = Aes.Create())
{
AES.KeySize = KEYSIZE;
AES.Key = pbkdf2DerivedBytes.GetBytes(KEYSIZE / 8);
AES.Mode = CipherMode.CBC;
AES.Padding = PaddingMode.PKCS7;
using (MemoryStream memoryStream = new MemoryStream())
{
CryptoStream cryptoStream = new CryptoStream
(memoryStream, AES.CreateEncryptor(), CryptoStreamMode.Write);
memoryStream.Write(pbkdf2DerivedBytes.Salt, 0, 16); // 16 bytes of
// SALT for PBKDF2 derivation function, must not be encrypted
memoryStream.Write(AES.IV, 0, 16); // IV is always 128 bits for AES,
// must not be encrypted
cryptoStream.Write(input, 0, input.Length);
cryptoStream.FlushFinalBlock();
// uncomment this line to debug encryption
//Console.WriteLine($"Password {password} Salt
//{BitConverter.ToString(pbkdf2DerivedBytes.Salt)} IV
//{BitConverter.ToString(AES.IV)} Key {BitConverter.ToString(AES.Key)}
//Input {BitConverter.ToString(input)}
//ActualPosition {memoryStream.Length}");
return memoryStream.ToArray();
}
}
}
public static byte[] CopyBytesToStream(byte[] bytes,
bool fromStream, Func<Stream, Stream> streamCallback)
{
var inputMemoryStream = new MemoryStream(bytes);
var outputMemoryStream = new MemoryStream();
var stream = streamCallback(fromStream ?
inputMemoryStream : outputMemoryStream);
if (fromStream)
stream.CopyTo(outputMemoryStream);
else
{
inputMemoryStream.CopyTo(stream);
stream.Flush();
}
return outputMemoryStream.ToArray();
}
private static StringBuilder CreateScriptHeader(Options o)
{
var script = new StringBuilder();
if (o.Compress || !String.IsNullOrEmpty(o.Password))
{
script.AppendLine(@"
function copyBytesToStream {
[OutputType([byte[]])]
Param (
[Parameter(Mandatory=$true)] [byte[]] $bytes,
[Parameter(Mandatory=$true)] [System.Boolean] $fromStream,
[Parameter(Mandatory=$true)] [ScriptBlock] $streamCallback)
$InputMemoryStream = New-Object System.IO.MemoryStream @(,$bytes)
$OutputMemoryStream = New-Object System.IO.MemoryStream
$stream = (Invoke-Command $streamCallback -ArgumentList
$(if ($fromStream) { $InputMemoryStream } else { $OutputMemoryStream }))
if ($fromStream) {
$stream.CopyTo($OutputMemoryStream)
}
else {
$InputMemoryStream.CopyTo($stream)
$stream.Flush()
}
$result = $OutputMemoryStream.ToArray()
,$result
}
");
}
if (!o.Base64 && !o.Decimal)
{
script.AppendLine(@"
function StringToByteArray {
[OutputType([byte[]])]
Param ([Parameter(Mandatory=$true)] [System.String] $hexstring)
[byte[]] $bytes = New-Object Byte[] ($hexstring.Length/2)
for ($i=0; $i -lt $hexstring.Length;$i+=2) {
$bytes[$i/2] = [System.Byte]::Parse($hexstring.Substring($i,2),
[System.Globalization.NumberStyles]::HexNumber)
}
,$bytes
}
");
}
if (!String.IsNullOrEmpty(o.Password))
{
// uncomment these lines and put them in the decryptBytes function below
// (row "$Dec = $AES.CreateDecryptor()") to troubleshoot encryption
//Write - Host ""Password $password""
//Write - Host ""KEY: $([System.BitConverter]::ToString($AES.Key))""
//Write - Host ""IV: $([System.BitConverter]::ToString($AES.IV))""
//Write - Host ""EncryptedData:
//$([System.BitConverter]::ToString($EncryptedData))""
// uncomment these lines and put them in the decryptBytes function below
// (row ",$result") to troubleshoot encryption
//Write - Host ""DecryptedData:
//$([System.BitConverter]::ToString($result))""
script.Append(@$"function decryptBytes {{
[OutputType([byte[]])]
Param (
[parameter(Mandatory=$true)] [System.Byte[]] $bytes,
[parameter(Mandatory=$true)] [System.String] $password
)
# Split IV and encrypted data
$PBKDF2Salt = New-Object Byte[] 16
$IV = New-Object Byte[] 16
$EncryptedData = New-Object Byte[] ($bytes.Length-32)
[System.Array]::Copy($bytes, 0, $PBKDF2Salt, 0, 16)
[System.Array]::Copy($bytes, 16, $IV, 0, 16)
[System.Array]::Copy($bytes, 32, $EncryptedData, 0, $bytes.Length-32)
# Generate PBKDF2 from Salt and Password
$PBKDF2 = New-Object System.Security.Cryptography.Rfc2898DeriveBytes
($password, $PBKDF2Salt, 2000)
# Setup our decryptor
$AES = [Security.Cryptography.Aes]::Create()
$AES.KeySize = {KEYSIZE}
$AES.Key = $PBKDF2.GetBytes({KEYSIZE / 8})
$AES.IV = $IV
$AES.Mode = [System.Security.Cryptography.CipherMode]::CBC
$AES.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$Dec = $AES.CreateDecryptor()
[byte[]] $result = copyBytesToStream $EncryptedData $true {{ param ($EncryptedStream)
New-Object System.Security.Cryptography.CryptoStream
($EncryptedStream, $Dec,
[System.Security.Cryptography.CryptoStreamMode] 'Read') }}
,$result
}}
");
}
var decryptCode = String.IsNullOrEmpty(o.Password) ?
String.Empty : "\t\t$bytes = $(decryptBytes $bytes $password)";
var decompressCodeMultiRow = @"
if ($decompress) {
$bytes = copyBytesToStream $bytes $true { param ($EncryptedStream)
New-Object System.IO.Compression.DeflateStream($EncryptedStream,
[System.IO.Compression.CompressionMode ] 'Decompress') }
}
";
var decompressCode = o.Compress ? decompressCodeMultiRow : String.Empty;
var hashCodeMultiRow = @"
if (![System.String]::IsNullOrEmpty($hash)) {
$actualHash = (Get-FileHash -Path $file -Algorithm Sha256).Hash
if ($actualHash -ne $hash) {
Write-Error ""Integrity check failed on $file expected
$hash actual $actualHash!""
}
}
";
var hashCode = o.Hash ? hashCodeMultiRow : String.Empty;
script.Append($@"function createFile {{
param (
[parameter(Mandatory=$true)] [String] $file,
[parameter(Mandatory=$true)] [byte[]] $bytes,
[parameter(Mandatory=$false)] [String] $password,
[Parameter(Mandatory=$false)] [String] $hash,
[Parameter(Mandatory=$false)] [System.Boolean] $decompress=$false)
$null = New-Item -ItemType Directory -Path (Split-Path $file) -Force
{decryptCode}
{decompressCode}
if ($global:core) {{ Set-Content -Path $file -Value $bytes -AsByteStream -Force }}
else {{ Set-Content -Path $file -Value $bytes -Encoding Byte -Force }}
{hashCode}
Write-Host ""Created file $file Length $($bytes.Length)""
}}
");
script.Append($"function createFiles {{\n\tparam
([parameter(Mandatory={(String.IsNullOrEmpty(o.Password) ?
"$false" : "$true")})] [String] $password)\n\n");
script.Append("\t$setContentHelp = (help Set-Content) |
Out-String\n\tif ($setContentHelp.Contains(\"AsByteStream\"))
{ $global:core = $true } else { $global:core = $false }\n\n");
return script;
}
public static void CreateScript(Options o)
{
if (String.IsNullOrEmpty(o.OutputFolder))
o.OutputFolder = Directory.GetCurrentDirectory();
else
Directory.CreateDirectory(o.OutputFolder);
StringBuilder script = CreateScriptHeader(o);
var outputFile = Path.Combine(o.OutputFolder, $"SingleScript.ps1");
foreach (var input in o.Inputs)
{
var actualCompress = false;
var path = Path.GetDirectoryName(input);
foreach (var file in Directory.GetFiles(!String.IsNullOrEmpty(path) ?
path : ".", Path.GetFileName(input), o.Recurse ?
SearchOption.AllDirectories : SearchOption.TopDirectoryOnly))
{
if (!o.SingleFile)
{
script = CreateScriptHeader(o);
outputFile = Path.Combine(o.OutputFolder,
$"{Path.GetFileName(file).Replace(".", "_")}_script.ps1");
}
Console.Write($"Scripting file {file}
{(!o.SingleFile ? $"into {outputFile}..." : String.Empty)}");
var inputBytes = File.ReadAllBytes(file);
var hash = ComputeSha256Hash(inputBytes);
if (o.Compress)
{
var compressedFileBytes = CopyBytesToStream(inputBytes, false,
encryptedStream => new DeflateStream
(encryptedStream, CompressionMode.Compress));
if (compressedFileBytes.Length < inputBytes.Length)
{
inputBytes = compressedFileBytes;
actualCompress = true;
}
else
Console.Write("compression is useless, disabling it...");
}
var bytes = String.IsNullOrEmpty(o.Password) ?
inputBytes : EncryptBytes(inputBytes, o.Password);
if (o.Base64)
{
script.Append($"\t[byte[]] $bytes =
[Convert]::FromBase64String('{Convert.ToBase64String(bytes)}')");
}
else
{
script.Append(o.Decimal ? "\t[byte[]] $bytes = " :
"\t[byte[]] $bytes = (StringToByteArray '");
foreach (var b in bytes)
{
if (o.Decimal)
script.Append($"{b.ToString("D")},");
else
script.Append($"{b.ToString("X2")}");
}
if (!o.Decimal)
script.Append("')");
else
script.Length--;
}
script.Append($"\n\tcreateFile '{file}' $bytes $password
{(o.Hash ? $"'{hash}'" : "''")}
{(o.Compress ? $"${actualCompress}" : "$false")}\n\n");
if (!o.SingleFile)
{
script.Append($"}}\n\ncreateFiles '{o.Password}'\n");
var outputScript = script.ToString();
File.WriteAllText(outputFile, outputScript);
Console.WriteLine($"length
{Math.Round(outputScript.Length / 1024.0)}KB.");
}
else
Console.WriteLine("");
}
}
if (o.SingleFile)
{
script.Append($"}}\n\ncreateFiles '{o.Password}'\n");
var outputScript = script.ToString();
File.WriteAllText(outputFile, outputScript);
Console.WriteLine($"Created single script file {outputFile}
length {Math.Round(outputScript.Length / 1024.0)}KB.");
}
}
}
}
以下是代码主要组件的描述:
- 为了解析命令行选项,我使用CommandLineParser NuGet包,恕我直言,这是一个简单且非常有效的库来执行解析。基本上,您创建一个类Options,在其中定义一组属性,每个属性对应于命令行的一个选项,使用Option自定义属性来修饰它,该属性指定选项的长短形式、帮助文本、是否需要以及目标数据类型(string、bool等)。解析只通过一行代码自动发生,其中您将先前创建的Options类作为泛型传递,然后您依次收到包含解析参数的相同Options类的Action,如果一切正常(如果不是,帮助将与错误一起显示):
Parser.Default.ParseArguments<Options>(args).WithParsed<Options>
(o => CreateScript(o));
- EncryptBytes:这是一个辅助函数,用于使用AES256算法使用-p命令行选项中指定的用户密码加密输入字节。它使用标准的.NET Aes类,使用从密码派生的密钥执行加密,并具有PBKDF2密钥派生函数(Rfc2898DeriveBytes类)。一切都是标准的,所以我不会花太多时间解释这段代码。我只强调一点:PBKDF2密钥派生函数不是最好的函数,存在更好的替代方案(例如,Scrypt),但它们不是在.NET Core中原生实现的,因此必须使用外部库,在这种情况下,它们还需要嵌入到输出Powershell脚本中,以便允许它在解密时再次从密码派生密钥(目前, 我决定不走这条路)。
- CopyBytesToStream:这是一个辅助函数,它基本上传递流中输入中给出的字节数组,以便压缩/解压缩/加密/解密它们。
- CreateScriptHeader:它是一个帮助程序函数,用于创建输出Powershell脚本的初始部分。基本上,它将以下函数注入到输出Powershell中:
- copyBytesToStream:与上述代码相同,仅在Powershell中实现。
- StringToByteArray:基本上将十六进制字符串转换为字节数组。
- decryptBytes:解密AES加密的二进制文件,并且仅在-p命令行参数中传递密码时才会注入
- createFile:始终注入,它基本上将字节数组写入磁盘,创建所有目标文件夹(如果不存在),并在需要时解密数据。
- createFiles:始终注入,它是由脚本执行的main函数,它基本上接受密码作为输入参数,对于每个输入文件,它定义了二进制数据的decimal array of bytes或hex/base64 string,并依次调用createFile函数来重新创建文件。
- base64格式的多个输入文件的单输出Powershell脚本示例
- 多个文件之一的示例:以十六进制字节数组格式输出Powershell脚本
- 多个文件之一的示例:以十进制字节数组格式输出Powershell脚本
兴趣点
该脚本通过将其粘贴到目标远程桌面会话中而完美运行,而无需将其保存到文件中并执行它,从而绕过Set-ExecutionPolicy限制(例如,它也可以在最严格的AllSigned执行策略中工作)。稍后,我将在YouTube上发布一个带有演示的视频,显示重新创建的文件与原始文件具有相同的哈希值。
如果远程桌面会话禁止复制和粘贴任何文本作为对策,该怎么办?
好吧,如果剪贴板被禁用,作为解决方法,您可以尝试以下方法:
- 创建另一个在客户端计算机上运行的程序,该程序基本上启动远程桌面客户端,专注于其窗口,然后:
- 发送“Windows+R”打开运行窗口
- 输入“Powershell”文本以启动新的PowerShell窗口
- 最后将输出Powershell脚本作为击键发送。
在Windows中,可以通过使用WScript.Shell COM对象的VBScript SendKeys方法实现击键的发送。
好的,明白了,但是如果作为额外的对策,上面的SendKeys方法以某种方式被禁止,会发生什么?
恕我直言,这不会发生,但无论如何应该是这种情况,RDP协议虽然是专有的,但可以作为Microsoft的开放规范免费提供,因此您可以尝试实现自定义客户端或调整现有的开源客户端(如FreeRDP)以支持键盘脚本。
终极挑战:如果服务器没有网络,它不接受任何USB驱动器或设备,您必须亲自去那里才能访问它怎么办?
好吧,世界上任何物理服务器都将始终拥有两台设备:
- 像键盘这样的输入设备,可用于注入任何击键序列,例如,通过插入可编程微控制器(如ESP32),该微控制器模拟USB键盘并在特定条件发生时将输出Powershell脚本作为击键发送(这是另一个项目的想法)。如果服务器只有旧的PS2键盘,您可以设计或分包以相同方式工作的自定义硬件。或者,如果您想要一个快速的解决方案,您可以在可编程键盘上投资一些钱。
- 像监视器这样的输出设备,可用于通过使用上述脚本方法注入和执行自定义应用程序来从服务器静默提取数据。这个应用程序基本上会读取任何二进制文件并将其转码为“QR码”(或像素图像)序列,而这些二维码又可以通过手机摄像头在显示器上拍摄来记录和解码。这并不是什么新鲜事,因为它已经在90年代在一台名为Amiga的旧32位计算机上完成,带有视频备份系统,该系统基本上将二进制文件(通常是软盘图像)转码为存储在录像带上的黑白像素图像序列。作为奖励,人们还可以考虑转码为彩色像素图像,以增加数据的密度(从而增加吞吐量),但这又是另一个项目的想法。
最后但并非最不重要的一点是,我想强调的是,脚本语言,无论是Powershell还是bash还是bat都不是那么重要(也许在bat上你有一些限制),重要的是这个实现背后的想法,例如,通过精心设计的脚本重新创建二进制文件。
附加说明:为了绕过最强化的环境,我也在Powershell中实现了上述C#代码。您可以在GitHub上找到它与C#代码。若要绕过Powershell执行策略,只需复制代码并将其粘贴到Powershell控制台中,然后按return键即可。完成此操作后,您只需使用上述相同参数调用函数BinaryToPowershellScript即可转换所需的任何文件。
https://www.codeproject.com/Articles/5369565/How-to-Transform-Binary-Files-into-Powershell-Scri