65.9K
CodeProject 正在变化。 阅读更多。
Home

如何将二进制文件转换为 Powershell 脚本以便在服务器上静默复制它们

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (8投票s)

2023年10月9日

CPOL

9分钟阅读

viewsIcon

13563

一个工具,可以创建一个或多个 Powershell 脚本,这些脚本又可以重新创建一个或多个二进制文件

引言

BinaryToPowershellScript 是一个 .NET Core 控制台应用程序,可将一个或多个二进制文件转换为 PowerShell 脚本,这些脚本在服务器上执行时,可以精确地重新创建脚本化的文件集。

背景

主要思想是将二进制文件转换为创建相同二进制文件的脚本。我选择了PowerShell,因为它是一种较新的技术,我熟悉它,并且它的PowerShell Core版本支持所有主要平台(Windows、Linux 和 Mac)。当然,这一切也可以用纯 Bash(Linux)或批处理文件(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 实例/配置。

Using the Code

您可以在我的 GitHub 页面上找到源代码,其中有一个详细的README说明如何使用该控制台应用程序,并附有命令行示例和生成的脚本。

我现在将专注于代码,基本上,我们只有一个 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 自定义属性对其进行装饰,该属性指定选项的短格式和长格式、帮助文本、是否必需以及目标数据类型(stringbool 等)。解析只需一行代码即可自动完成,您将之前创建的 Options 类作为泛型传递,然后收到一个包含解析参数的同名 Options 类的 Action。如果一切正常(否则将显示帮助和错误)。
    Parser.Default.ParseArguments<Options>(args).WithParsed<Options>
                                          (o => CreateScript(o));
  • EncryptBytes:这是一个辅助函数,用于使用 -p 命令行选项中指定的用户密码,通过 AES256 算法加密输入字节。它使用标准的 .NET Aes 类,通过 PBKDF2 密钥派生函数(Rfc2898DeriveBytes 类)从密码派生密钥来执行加密。一切都很标准,我不会花太多时间解释这段代码。我只想强调一点:PBKDF2 密钥派生函数不是最好的,存在更好的替代方案(例如 Scrypt),但它们没有在 .NET Core 中本地实现,因此需要使用外部库,在这种情况下,还需要将它们嵌入到输出的 PowerShell 脚本中,以便在解密时允许它再次从密码派生密钥(目前,我决定不这样做)。
  • CopyBytesToStream:这是一个辅助函数,它基本上会将输入的字节数组传递到流中,以便进行压缩/解压缩/加密/解密。
  • CreateScriptHeader:这是一个辅助函数,它创建输出 PowerShell 脚本的初始部分。它基本上将以下函数注入到输出的 PowerShell 中
    • copyBytesToStream:代码与上面相同,只是也用 PowerShell 实现。
    • StringToByteArray:它基本上将十六进制字符串转换为字节数组。
    • decryptBytes:它解密 AES 加密的二进制文件,并且仅在 -p 命令行参数中传递密码时才注入。
    • createFile:始终注入,它基本上将字节数组写入磁盘,如果目标文件夹不存在则创建所有目标文件夹,并在需要时解密数据。
    • createFiles:始终注入,它是脚本执行的主函数,它接受密码作为输入参数,并为每个输入文件定义二进制数据的十进制字节数组十六进制/base64 字符串,然后依次调用 createFile 函数来重新创建文件。
  • base64 格式的多个输入文件的单个输出 PowerShell 脚本示例

  • 十六进制字节数组格式的多个文件输出 PowerShell 脚本示例

  • 十进制字节数组格式的多个文件输出 PowerShell 脚本示例

关注点

脚本通过将其粘贴到目标远程桌面会话中即可完美运行,无需将其保存到文件并执行,从而绕过了 Set-ExecutionPolicy 限制(例如,它甚至可以在最严格的 AllSigned 执行策略下工作)。稍后我将在 YouTube 上发布演示视频,展示重新创建的文件与原始文件的哈希值相同。

如果远程桌面会话出于对策目的禁止复制粘贴任何文本,该怎么办?

嗯,如果剪贴板被禁用,作为一种变通方法,您可以尝试以下操作:

  • 创建另一个在客户端计算机上运行的程序,该程序基本上启动远程桌面客户端,将其窗口置于焦点,然后
    • 按下“Windows+R”打开运行窗口
    • 输入“PowerShell”文本以启动新的 PowerShell 窗口
    • 最后将输出的 PowerShell 脚本作为键盘输入发送.

在 Windows 中,可以通过使用WScript.Shell COM 对象VBScript SendKeys 方法发送键盘输入(请参阅我的另一篇文章以获取思路)。

好的,明白了,但是如果作为额外的对策,上述SendKeys方法被某种方式禁用了怎么办?

游戏结束?不可能 :-)

IMHO,这不会发生,但无论如何,RDP 协议虽然是专有的,但它作为 Microsoft 的开放规范是免费提供的,因此您可以尝试实现自定义客户端或改编现有的开源客户端,如 FreeRDP,以支持键盘脚本。

终极挑战:如果服务器没有网络,不接受任何 USB 驱动器或设备,并且您必须亲自前往才能访问它,该怎么办?

好吧,世界上任何物理服务器总会有两个设备

  • 一个输入设备,如键盘,可用于注入任何键盘输入序列,例如,通过插入一个模拟 USB 键盘的可编程微控制器,并在特定条件发生时发送输出的 PowerShell 脚本作为键盘输入(这是另一个项目的想法)。如果服务器只有一个旧的 PS2 键盘,您可以设计或外包一个具有相同功能的定制硬件。或者,如果您想要一个快速的解决方案,您可以花点钱购买一个可编程键盘。
  • 一个输出设备,如显示器,可用于通过注入和执行上述脚本方法的自定义应用程序来静默地提取服务器数据。这个应用程序基本上会读取任何二进制文件,并将其转码为“QR 码”(或像素图像)序列,然后可以通过拍摄显示器的手机摄像头记录和解码。这并不是什么新鲜事,因为这早在 90 年代就在一台名为 Amiga 的旧 32 位计算机上通过视频备份系统完成过,该系统基本上将二进制文件(通常是软盘镜像)转码为黑白像素图像序列,然后存储在录像带上。作为奖励,还可以考虑转码为彩色像素图像以增加数据密度(从而提高吞吐量),但这又是一个项目的想法

最后但同样重要的是,我想强调脚本语言,无论是 PowerShell、Bash 还是 Bat,其重要性并不那么大(也许 Bat 存在一些限制),重要的是此实现背后的理念,例如通过精心设计的脚本来重新创建二进制文件。

附加说明:为了绕过最严格的环境,我还将上述 C# 代码也实现了 PowerShell 版本。您可以在 GitHub 上找到它,以及 C# 代码。要绕过 PowerShell 执行策略,您只需复制代码并将其粘贴到 PowerShell 控制台中,然后按 Enter 键。完成此操作后,您就可以通过调用 BinaryToPowershellScript 函数(使用上述相同参数)来转换任何您想要的文件。

历史

  • V1.0(2023 年 10 月 9 日)
    • 初始版本
  • V1.0.1(2023 年 10 月 13 日)
    • 添加了 -c 选项,通过 gzip 压缩来压缩文件,如果压缩后的文件大小大于原始文件大小,则自动禁用压缩。
    • 添加了 -h 选项,以添加 SHA256 哈希值作为文件完整性检查。
    • 改进了十六进制格式,现在它使用 2 字节而不是 4 字节表示输入文件的每个字节。
    • 添加了 PowerShell 实现,可以在不允许运行可执行文件时使用。
    • 添加了动态输出文件生成:现在它仅在指定相关选项时才生成辅助函数(例如,仅当您指定 -h 选项时才添加哈希辅助代码,依此类推)。
    • 在控制台输出中添加了输出脚本长度(以 KB 为单位)。
    • 一些错误修复。
  • V1.0.2(2023 年 1 月 31 日)
    • 关注点 部分末尾添加了附加说明,关于另一种也可在 PowerShell 语言中使用的实现。
© . All rights reserved.