1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

スクリプトをワンクリックで保護・EXE化!GUIツールの紹介

1
Posted at

はじめに

PowerShell(.ps1)やバッチファイル(.bat)を配布する際、「ソースコードを直接見せたくない」「実行時に黒いコンソール画面を出したくない」と思ったことはありませんか?

今回は、Gemini のアドバイスを受けながら作成した、ドラッグ&ドロップで簡単にスクリプトをBase64パッキングし、アイコン付きのEXEや難読化BATに変換するGUIツールを紹介します。

この記事の作成にあたっても、Gemini を活用しました。

このツールでできること

  1. マルチ対応: .ps1.bat の両方を入力可能。
  2. EXE変換: C#コンパイラを利用し、実行時にウィンドウを表示しない「完全ステルス」なEXEを作成。
  3. BAT変換: Base64化したコマンドを1行で実行するバッチファイルを作成。
  4. アイコン設定: お好みの .ico ファイルをEXEに付与(プレビュー機能付き)。
  5. D&D対応: ファイルを画面に放り込むだけの直感操作。

仕組みの解説

このツールは大きく分けて3つのステップで動いています。

1. データの変身(Base64エンコード)

スクリプトの中身をそのままEXEに入れるのではなく、一度 Base64 という特殊な形式に変換します。これにより、コード内の特殊文字や改行が原因で起こるエラーを防ぎ、中身をパッと見では解読不能な「保護膜」で包みます。

2. 「中身」のパッキング

EXE作成時には、裏側で「PowerShellを隠しウィンドウで起動し、Base64化された命令を実行する」という小さなC#プログラムをその場で自動生成しています。

3. コンパイル

Windowsに標準搭載されている csc.exe(C#コンパイラ)を呼び出し、生成したプログラムとアイコン画像を一つのEXEファイルにガッチャンコして完成させます。

スクリプト本体

以下のコードを AnyScript_Converter.ps1 として保存し、PowerShellで実行してください。

AnyScript_Converter.ps1
# =================================================================================
# AnyScript_Converter.ps1
# 役割: PowerShellやバッチファイルを、Base64形式で保護してEXEや別のBATに変換します。
# =================================================================================

# 必要なライブラリ(WPFやWindowsフォームなどの画面を作るための機能)を読み込む
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Drawing, System.Windows.Forms

# Win32 APIの呼び出し設定:アイコンを表示した後にメモリを掃除(解放)するために必要
$nativeMethods = Add-Type -Name "NativeMethods" -Namespace "Win32" -MemberDefinition @"
    [DllImport("user32.dll", SetLastError=true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool DestroyIcon(IntPtr hIcon);
"@ -PassThru

# --- 画面レイアウト定義 (XAML) ---
# XML形式でボタンやテキストボックスの配置を記述
$xaml = @"
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="AnyScript Converter (EXE/BAT)" Height="400" Width="520"
        WindowStartupLocation="CenterScreen" ResizeMode="NoResize" Topmost="True">
    <Grid Margin="15">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/> 
            <RowDefinition Height="Auto"/> 
            <RowDefinition Height="Auto"/> 
            <RowDefinition Height="Auto"/> 
            <RowDefinition Height="Auto"/> 
            <RowDefinition Height="*"/>    
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="80"/>
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Row="0" VerticalAlignment="Center" Text="元ファイル:"/>
        <TextBox  Grid.Row="0" Grid.Column="1" Margin="5" Name="SrcPathBox" AllowDrop="True" VerticalContentAlignment="Center"/>
        <Button   Grid.Row="0" Grid.Column="2" Margin="5" Name="SrcBrowseBtn" Content="参照..."/>

        <TextBlock Grid.Row="1" VerticalAlignment="Center" Text="アイコン:"/>
        <TextBox  Grid.Row="1" Grid.Column="1" Margin="5" Name="IconPathBox" AllowDrop="True" VerticalContentAlignment="Center"/>
        <Button   Grid.Row="1" Grid.Column="2" Margin="5" Name="IconBrowseBtn" Content="参照..."/>

        <TextBlock Grid.Row="2" Grid.ColumnSpan="3" HorizontalAlignment="Center" Foreground="#666" FontSize="11" Margin="0,8,0,2"
                   Text="※アイコンを選択しない場合は Windows の標準 EXE アイコンが使用されます"/>

        <TextBlock Grid.Row="3" Grid.Column="1" VerticalAlignment="Center" Text="プレビュー:" Margin="40,0,0,0"/>
        <Border Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" HorizontalAlignment="Center" VerticalAlignment="Center"
                Margin="5" Width="48" Height="48" BorderBrush="#DDD" BorderThickness="1" CornerRadius="2" Background="#FDFDFD">
            <Image Name="IconPreview" Stretch="Uniform" />
        </Border>

        <TextBlock Grid.Row="4" VerticalAlignment="Center" Text="出力先パス:"/>
        <TextBox  Grid.Row="4" Grid.Column="1" Margin="5" Name="ExePathBox" VerticalContentAlignment="Center"/>
        <Button   Grid.Row="4" Grid.Column="2" Margin="5" Name="ExeBrowseBtn" Content="参照..."/>

        <StackPanel Grid.Row="5" Grid.ColumnSpan="3" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Bottom">
            <Button Name="BuildBatBtn" Content="BAT 作成" Width="100" Height="35" Margin="5" Background="#FFF9C4"/>
            <Button Name="BuildExeBtn" Content="EXE 作成" Width="100" Height="35" Margin="5" Background="#E3F2FD"/>
            <Button Name="ExitBtn" Content="終了" Width="80" Height="35" Margin="5"/>
        </StackPanel>
    </Grid>
</Window>
"@

# XAMLコードを読み込んで、実際のウィンドウオブジェクトを作成
$xmlReader = New-Object System.Xml.XmlNodeReader ([xml]$xaml)
$window    = [Windows.Markup.XamlReader]::Load($xmlReader)

# 画面上の各部品(テキストボックスやボタン)をPowerShell変数として操作できるように紐付ける
$srcBox = $window.FindName("SrcPathBox")
$icoBox = $window.FindName("IconPathBox")
$exeBox = $window.FindName("ExePathBox")
$iconImg = $window.FindName("IconPreview")
$buildExeBtn = $window.FindName("BuildExeBtn")
$buildBatBtn = $window.FindName("BuildBatBtn")

# --- イベント処理の定義 ---

# ドラッグ&ドロップされた時に、アイコンを「コピー可能」な表示に変える処理
$dragOverHandler = {
    param($sender, $e)
    if ($e.Data.GetDataPresent([System.Windows.DataFormats]::FileDrop)) {
        $e.Effects = [System.Windows.DragDropEffects]::Copy
    } else { $e.Effects = [System.Windows.DragDropEffects]::None }
    $e.Handled = $true
}

# ファイルがドロップされた時、そのファイルパスをテキストボックスに入力する処理
$dropHandler = {
    param($sender, $e)
    if ($e.Data.GetDataPresent([System.Windows.DataFormats]::FileDrop)) {
        $files = $e.Data.GetData([System.Windows.DataFormats]::FileDrop)
        $sender.Text = $files[0]
        # 「元ファイル」にドロップされた場合のみ、出力先パスも自動で作成して入力する
        if ($sender.Name -eq "SrcPathBox") {
            $exeBox.Text = [System.IO.Path]::ChangeExtension($files[0], ".exe")
        }
    }
}

# 各ボックスにドラッグ&ドロップイベントを登録する
$srcBox.Add_PreviewDragOver($dragOverHandler); $srcBox.Add_Drop($dropHandler)
$icoBox.Add_PreviewDragOver($dragOverHandler); $icoBox.Add_Drop($dropHandler)

# アイコンパスが書き換わったら、プレビュー画像を表示する処理
$icoBox.Add_TextChanged({
    if (-not [string]::IsNullOrWhiteSpace($icoBox.Text) -and (Test-Path $icoBox.Text)) {
        try {
            $icon = New-Object System.Drawing.Icon($icoBox.Text)
            $hIcon = $icon.Handle
            # .icoファイルをWPFで表示できる形式に変換する
            $source = [System.Windows.Interop.Imaging]::CreateBitmapSourceFromHIcon($hIcon, [System.Windows.Int32Rect]::Empty, [System.Windows.Media.Imaging.BitmapSizeOptions]::FromEmptyOptions())
            $source.Freeze()
            $iconImg.Source = $source
            # 使用したアイコンリソースをメモリから解放する
            [Win32.NativeMethods]::DestroyIcon($hIcon) | Out-Null
            $icon.Dispose()
        } catch { $iconImg.Source = $null }
    } else { $iconImg.Source = $null }
})

# --- ボタンクリック時の処理 ---

# 「参照」ボタン:ファイル選択ダイアログを開く
$window.FindName("SrcBrowseBtn").Add_Click({
    $dlg = New-Object System.Windows.Forms.OpenFileDialog -Property @{ Filter = "Scripts (*.bat;*.ps1)|*.bat;*.ps1" }
    if ($dlg.ShowDialog() -eq "OK") { 
        $srcBox.Text = $dlg.FileName 
        $exeBox.Text = [System.IO.Path]::ChangeExtension($dlg.FileName, ".exe")
    }
})

$window.FindName("IconBrowseBtn").Add_Click({
    $dlg = New-Object System.Windows.Forms.OpenFileDialog -Property @{ Filter = "Icon (*.ico)|*.ico" }
    if ($dlg.ShowDialog() -eq "OK") { $icoBox.Text = $dlg.FileName }
})

$window.FindName("ExeBrowseBtn").Add_Click({
    $dlg = New-Object System.Windows.Forms.SaveFileDialog -Property @{ Filter = "Output (*.exe;*.bat)|*.exe;*.bat" }
    if ($dlg.ShowDialog() -eq "OK") { $exeBox.Text = $dlg.FileName }
})

$window.FindName("ExitBtn").Add_Click({ $window.Close() })

# --- 変換ロジック ---

# 指定されたスクリプトを読み込み、Base64(特殊な文字列形式)に変換する関数
function Get-EncodedCommand {
    if ([string]::IsNullOrWhiteSpace($srcBox.Text) -or -not (Test-Path $srcBox.Text)) { return $null }
    $rawContent = Get-Content -Path $srcBox.Text -Raw -Encoding UTF8
    
    # バッチファイルが入力された場合は、PowerShellの中で動くようにラップ(包み込み)する
    if ($srcBox.Text.EndsWith(".bat", "CurrentCultureIgnoreCase")) {
        $rawContent = "& { " + $rawContent + " }"
    }
    # 文字列をUnicodeバイト列に変換してから、Base64文字列に変換する
    return [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($rawContent))
}

# BAT作成:Base64化したコマンドを実行するだけの1行バッチを作る
$buildBatBtn.Add_Click({
    $encoded = Get-EncodedCommand
    if (-not $encoded) { return }
    $targetPath = if ($exeBox.Text.EndsWith(".bat", "CurrentCultureIgnoreCase")) { $exeBox.Text } else { [System.IO.Path]::ChangeExtension($exeBox.Text, ".bat") }
    
    # 実行時にPowerShellのウィンドウ設定などを無視して実行するコマンドを組み立てる
    $batBody = "@echo off`r`npowershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded`r`nexit"
    try {
        $batBody | Out-File -FilePath $targetPath -Encoding ascii -Force
        [System.Windows.MessageBox]::Show($window, "BATファイルを作成しました。`n$targetPath", "完了", 0, 64)
    } catch { [System.Windows.MessageBox]::Show($window, "エラー: " + $_.Exception.Message, "失敗", 0, 16) }
})

# EXE作成:C#のコードをその場で生成してコンパイル(機械語に変換)する
$buildExeBtn.Add_Click({
    $encoded = Get-EncodedCommand
    if (-not $encoded) { return }
    $targetPath = if ($exeBox.Text.EndsWith(".exe", "CurrentCultureIgnoreCase")) { $exeBox.Text } else { [System.IO.Path]::ChangeExtension($exeBox.Text, ".exe") }
    
    # EXEの「中身」になるC#プログラム。実行時に黒い画面を出さない設定が含まれている
    $csCode = @"
using System;
using System.Diagnostics;
class Program {
    static void Main() {
        try {
            ProcessStartInfo psi = new ProcessStartInfo("powershell.exe");
            psi.Arguments = "-NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded";
            psi.WindowStyle = ProcessWindowStyle.Hidden; // ウィンドウを隠す
            psi.CreateNoWindow = true;                   // ウィンドウを作らない
            psi.UseShellExecute = false;
            Process.Start(psi);
        } catch {}
    }
}
"@
    # 一時的なC#ファイルを作成
    $tmpCs = Join-Path $env:TEMP ([Guid]::NewGuid().ToString() + ".cs")
    $csCode | Out-File -FilePath $tmpCs -Encoding UTF8
    
    # Windowsに標準搭載されているコンパイラ(csc.exe)を探す
    $cscPath = @("$env:WINDIR\Microsoft.NET\Framework64\v4.0.30319\csc.exe","$env:WINDIR\Microsoft.NET\Framework\v4.0.30319\csc.exe") | Where-Object { Test-Path $_ } | Select-Object -First 1
    
    # アイコン設定がある場合はオプションを追加
    $iconOpt = if (-not [string]::IsNullOrWhiteSpace($icoBox.Text) -and (Test-Path $icoBox.Text)) { "/win32icon:`"$($icoBox.Text)`"" } else { "" }
    
    # コンパイルを実行
    $argList = "/target:winexe /out:`"$targetPath`" $iconOpt `"$tmpCs`""
    $proc = Start-Process -FilePath $cscPath -ArgumentList $argList -NoNewWindow -PassThru -Wait
    
    if ($proc.ExitCode -eq 0) { 
        [System.Windows.MessageBox]::Show($window, "EXEを作成しました。`n$targetPath", "完了", 0, 64) 
    } else { 
        [System.Windows.MessageBox]::Show($window, "エラーが発生しました。", "失敗", 0, 16) 
    }
    # 使い終わった一時ファイルを削除
    Remove-Item $tmpCs -ErrorAction SilentlyContinue
})

# GUIウィンドウの表
[void]$window.ShowDialog()

使い方

  1. 元ファイル: 変換したい .ps1 または .bat を選ぶ(またはドロップ)。
  2. アイコン: 必要なら .ico を選ぶ(プレビュー枠で確認できます)。
  3. ボタンを選択:
    • 「BAT作成」→ 難読化されたバッチが生成されます。
    • 「EXE作成」→ アイコン付き、ウィンドウ非表示のEXEが生成されます。

まとめ

PowerShellでのGUI開発(WPF)と、C#コンパイラの呼び出しを組み合わせることで、実用的なツールを1ファイルで実現できました。
特にBase64エンコードを利用した手法は、配布用スクリプトの作成に非常に役立ちます。ぜひ活用してみてください。


免責事項
本スクリプトの使用により生じた損害やトラブルについて、筆者は一切の責任を負いません。内容を十分に理解した上で、自己責任でのご利用をお願いいたします。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?