はじめに
PowerShell(.ps1)やバッチファイル(.bat)を配布する際、「ソースコードを直接見せたくない」「実行時に黒いコンソール画面を出したくない」と思ったことはありませんか?
今回は、Gemini のアドバイスを受けながら作成した、ドラッグ&ドロップで簡単にスクリプトをBase64パッキングし、アイコン付きのEXEや難読化BATに変換するGUIツールを紹介します。
この記事の作成にあたっても、Gemini を活用しました。
このツールでできること
-
マルチ対応:
.ps1と.batの両方を入力可能。 - EXE変換: C#コンパイラを利用し、実行時にウィンドウを表示しない「完全ステルス」なEXEを作成。
- BAT変換: Base64化したコマンドを1行で実行するバッチファイルを作成。
-
アイコン設定: お好みの
.icoファイルをEXEに付与(プレビュー機能付き)。 - 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
# 役割: 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()
使い方
-
元ファイル: 変換したい
.ps1または.batを選ぶ(またはドロップ)。 -
アイコン: 必要なら
.icoを選ぶ(プレビュー枠で確認できます)。 -
ボタンを選択:
- 「BAT作成」→ 難読化されたバッチが生成されます。
- 「EXE作成」→ アイコン付き、ウィンドウ非表示のEXEが生成されます。
まとめ
PowerShellでのGUI開発(WPF)と、C#コンパイラの呼び出しを組み合わせることで、実用的なツールを1ファイルで実現できました。
特にBase64エンコードを利用した手法は、配布用スクリプトの作成に非常に役立ちます。ぜひ活用してみてください。
免責事項
本スクリプトの使用により生じた損害やトラブルについて、筆者は一切の責任を負いません。内容を十分に理解した上で、自己責任でのご利用をお願いいたします。