Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Bloxstrap/Bloxstrap.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
<UseWPF>true</UseWPF>
<UseWindowsForms>True</UseWindowsForms>
<ApplicationIcon>Bloxstrap.ico</ApplicationIcon>
<Version>7.6.2</Version>
<FileVersion>7.6.2</FileVersion>
<Version>7.6.8</Version>
<FileVersion>7.6.8</FileVersion>
<AssemblyVersion>$(Version)</AssemblyVersion>
<AssemblyInformationalVersion>$(Version)</AssemblyInformationalVersion>
<ApplicationManifest>app.manifest</ApplicationManifest>
Expand All @@ -18,6 +18,7 @@
<Product>$(AssemblyName)</Product>
<RootNamespace>Bloxstrap</RootNamespace>
<SelfContained>false</SelfContained>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -79,7 +80,7 @@
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Markdig" Version="1.3.2" />
<PackageReference Include="Microsoft.Diagnostics.Tracing.TraceEvent" Version="3.2.6" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.321">
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.333">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="securifybv.ShellLink" Version="0.1.0" />
Expand Down
101 changes: 93 additions & 8 deletions Bloxstrap/GameSession/GameSessionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ public async Task<GameSessionRecord> BeginSessionAsync(CancellationToken cancell
}
}

/// <summary>
/// v7.6.3 — identitas proses dianggap cukup dikenal bila NAMA prosesnya
/// terbaca, walau path/StartTime tidak bisa dibaca (akibat handle akses
/// dibatasi pada proses tertentu, mis. browser Chromium terbaru, launcher
/// anti-cheat, atau aplikasi UWP). Dulu kondisi itu langsung diklasifikasi
/// Critical sehingga aplikasi yang justru paling ingin di-suspend tidak
/// pernah tersentuh (feedback: "aplikasi yang saya pilih tidak ikut suspend").
/// Proses tanpa nama tetap ditolak — nama dipakai untuk mencocokkan rule
/// dan untuk me-restore.
/// </summary>
public static bool IsKnownProcess(ProcessSnapshot snapshot)
{
return !String.IsNullOrWhiteSpace(snapshot.ProcessName);
}

private async Task<GameSessionRecord> BeginSessionCoreAsync(CancellationToken cancellationToken)
{
const string LOG_IDENT_LOCAL = "GameSession::BeginSession";
Expand All @@ -71,19 +86,68 @@ private async Task<GameSessionRecord> BeginSessionCoreAsync(CancellationToken ca
EndSessionCore(null);

if (Store.ReadActive() is not null)
throw new InvalidOperationException("A previous Game Session still has processes pending restore.");
{
// v7.6.3 FIX — dulu kondisi ini melempar InvalidOperationException
// SETIAP kali user masuk game, sehingga Game Session tampak "tidak
// pernah berhasil sama sekali" setelah satu kegagalan restore.
// Sekarang record pending yang tidak bisa di-restore dipulihkan
// sekaligus: semua thread di-resume lewat rescue scan, record
// dibuang, lalu sesi baru dimulai normal.
App.Logger.WriteLine(LOG_IDENT_LOCAL,
"Sesi sebelumnya masih pending restore — memaksa pemulihan dan memulai sesi baru");

try
{
IReadOnlyList<RescuedProcess> rescued = Suspension.RescueSuspendedProcesses();
App.Logger.WriteLine(LOG_IDENT_LOCAL,
$"Pemulihan paksa selesai: {rescued.Count} proses di-resume (rescue scan)");
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT_LOCAL, $"Rescue scan gagal (dilanjutkan): {ex.Message}");
}

EndSessionCore(null);
}
}

SecurityDetectionState detectorState;
try
{
detectorState = await Detector.RefreshAsync(cancellationToken);
}
catch (Exception ex)
{
// v7.6.3 FIX — kegagalan WMI (Timeout/COMException, sering di PC dengan
// antivirus third-party atau WMI repository bermasalah) dulu membuat
// BeginSessionAsync gagal total sehingga TIDAK ADA aplikasi yang
// ter-suspend. Sekarang detector yang error diperlakukan seperti
// Degraded: proses tetap aman dari daftar proteksi statis, user tetap
// bisa suspend aplikasi pilihannya sendiri.
App.Logger.WriteLine(LOG_IDENT_LOCAL, $"Detector refresh gagal — dilanjutkan sebagai Degraded: {ex.Message}");
detectorState = SecurityDetectionState.Degraded;
}

SecurityDetectionState detectorState = await Detector.RefreshAsync(cancellationToken);
List<ProcessSnapshot> processes = _processSource().ToList();

// PID semua Windows service (SCM). Service = komponen sistem/vendor —
// sinyal CRITICAL tambahan di classifier supaya audio stack, driver
// companion, dan sync service (bahkan yang jalan di session user,
// mis. OneDrive.Sync.Service) tidak pernah ter-suspend.
IReadOnlySet<int> serviceProcessIds = ServiceProcessDetector.GetServiceProcessIds(cancellationToken);
if (serviceProcessIds.Count > 0)
App.Logger.WriteLine(LOG_IDENT_LOCAL, $"{serviceProcessIds.Count} Windows service PID terdaftar sebagai protected");
// v7.6.3: kegagalan query WMI TIDAK lagi membatalkan seluruh sesi —
// cukup dengan daftar proteksi statis (ProcessClassifier) dijalankan.
IReadOnlySet<int> serviceProcessIds;
try
{
serviceProcessIds = ServiceProcessDetector.GetServiceProcessIds(cancellationToken);
if (serviceProcessIds.Count > 0)
App.Logger.WriteLine(LOG_IDENT_LOCAL, $"{serviceProcessIds.Count} Windows service PID terdaftar sebagai protected");
}
catch (Exception ex)
{
App.Logger.WriteLine(LOG_IDENT_LOCAL, $"Service PID enumeration gagal — lanjut tanpa daftar service: {ex.Message}");
serviceProcessIds = new HashSet<int>();
}

var session = new GameSessionRecord
{
Expand Down Expand Up @@ -164,8 +228,28 @@ private async Task<GameSessionRecord> BeginSessionCoreAsync(CancellationToken ca
continue;
}

ProcessSuspendResult result = Suspension.SuspendProcess(process.ProcessId, cancellationToken);
if (result.SuspendedThreadIds.Count == 0)
ProcessSuspendResult result;
try
{
result = Suspension.SuspendProcess(process.ProcessId, cancellationToken);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
// v7.6.3 FIX — satu proses bermasalah tidak boleh menggagalkan
// seluruh sesi (dulu exception dari SuspendProcess merambat ke
// try blok luar → EndSessionCore → SEMUA aplikasi yang sudah
// terlanjur disuspend ikut di-resume dan user kehilangan
// proteksi sepenuhnya).
App.Logger.WriteLine(LOG_IDENT_LOCAL,
$"Suspend PID={process.ProcessId} ({process.ProcessName}) gagal — dilewati: {ex.Message}");
continue;
}

if (result.SuspendedThreadIds.Count == 0 && !result.ProcessLevelSuspend)
continue;

session.AppliedRules.Add(RuleKey(rule));
Expand All @@ -179,7 +263,7 @@ private async Task<GameSessionRecord> BeginSessionCoreAsync(CancellationToken ca
AppliedRule = RuleKey(rule),
ThreadIds = result.SuspendedThreadIds,
TotalThreadCount = result.TotalThreadCount,
SuspendedThreadCount = result.SuspendedThreadIds.Count,
SuspendedThreadCount = result.ProcessLevelSuspend ? result.TotalThreadCount : result.SuspendedThreadIds.Count,
FailedThreadCount = result.FailedThreadCount,
PartiallySuspended = result.PartiallySuspended
});
Expand All @@ -188,6 +272,7 @@ private async Task<GameSessionRecord> BeginSessionCoreAsync(CancellationToken ca
App.Logger.WriteLine(
LOG_IDENT_LOCAL,
$"{process.ProcessName} ter-suspend {result.SuspendedThreadIds.Count}/{result.TotalThreadCount} thread" +
(result.ProcessLevelSuspend ? " [process-level]" : "") +
(result.PartiallySuspended ? " (PartiallySuspended)" : ""));
}
}
Expand Down
21 changes: 11 additions & 10 deletions Bloxstrap/GameSession/ProcessClassifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,14 @@ public static ProcessClassification Classify(
if (detector.State != SecurityDetectionState.Ok)
return ProcessClassification.Critical;

// A readable identity is required before a rule can mutate another process.
if (String.IsNullOrWhiteSpace(snapshot.ProcessName)
|| String.IsNullOrWhiteSpace(snapshot.ExecutablePath)
|| !snapshot.StartTimeUtc.HasValue)
{
// v7.6.3 FIX — dulu path executable DAN StartTime wajib terbaca, sehingga
// aplikasi yang handle aksesnya dibatasi (browser Chromium, launcher
// anti-cheat, UWP) tidak PERNAH bisa di-suspend meski user mencentangnya
// di UI (feedback: "tidak benar2 ke suspend dan masih berjalan").
// Nama proses sekarang cukup: pencocokan rule dan label restore memakai
// nama, dan keamanan target tetap dijaga daftar proteksi IsAlwaysProtected.
if (!GameSessionService.IsKnownProcess(snapshot))
return ProcessClassification.Critical;
}

return ProcessClassification.Safe;
}
Expand All @@ -97,10 +98,10 @@ public static bool IsCritical(
if (IsAlwaysProtected(snapshot, detector, selfProcessId, gameProcessId, serviceProcessIds))
return true;

// Unknown identity is never safe to touch.
return String.IsNullOrWhiteSpace(snapshot.ProcessName)
|| String.IsNullOrWhiteSpace(snapshot.ExecutablePath)
|| !snapshot.StartTimeUtc.HasValue;
// v7.6.3 — proses tanpa nama tidak bisa dicocokkan ke rule maupun
// di-restore, jadi tetap ditolak. Path/StartTime yang tak terbaca tidak
// lagi dianggap critical (lihat Classify).
return String.IsNullOrWhiteSpace(snapshot.ProcessName);
}

public static bool IsAlwaysProtected(
Expand Down
135 changes: 134 additions & 1 deletion Bloxstrap/GameSession/ProcessControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,51 @@ public interface IProcessAccessor : IDisposable
bool IsThreadSuspended(int threadId);
DateTime? GetStartTimeUtc();
long GetProcessorTimeTicks();

// v7.6.3 — suspend/resume level proses (atomik, mencakup thread yang lahir
// di tengah operasi). Implementasi boleh tidak mendukung (returns false).
bool TrySuspendProcess();
bool TryResumeProcess();
bool SupportsProcessLevelControl { get; }
}

/// <summary>
/// v7.6.3 FIX — feedback "aplikasi yang dipilih untuk di-suspend tidak benar-benar
/// ter-suspend dan masih berjalan seperti biasa".
///
/// Dulu suspend hanya per-thread via SuspendThread dengan maksimal 5 sweep pass
/// dalam 2 detik. Aplikasi seperti browser/launcher terus menambah thread baru,
/// sehingga sweep tidak pernah menangkap semuanya — thread yang lolos membuat
/// proses tetap hidup. Selain itu OpenThread(THREAD_SUSPEND_RESUME) bisa gagal
/// untuk thread yang baru dibuat, dan proses modern yang diproteksi
/// (PROCESS_SUSPEND_RESUME butuh akses eksplisit) menolak handle Process.GetProcessById.
///
/// Sekarang: NtSuspendProcess/NtResumeProcess (syscall ntdll yang sama dipakai
/// Process Explorer / pssuspend) men-suspend SELURUH proses secara atomik —
/// kernel menahan setiap thread termasuk yang baru lahir. Per-thread API tetap
/// ada sebagai fallback dan untuk verifikasi.
/// </summary>
internal sealed class Win32ProcessAccessor : IProcessAccessor
{
private const uint THREAD_SUSPEND_RESUME = 0x0002;

// PROCESS_SUSPEND_RESUME (0x0800) — hak minimal untuk NtSuspendProcess/NtResumeProcess.
private const uint PROCESS_SUSPEND_RESUME = 0x0800;

private const string LOG_IDENT = "GameSession::Win32ProcessAccessor";

private readonly Process _process;
private readonly int _processId;

public Win32ProcessAccessor(int processId)
{
_processId = processId;
_process = Process.GetProcessById(processId);
}

public int ProcessId => _process.Id;
public int ProcessId => _processId;

public bool SupportsProcessLevelControl => true;

public bool IsAlive
{
Expand All @@ -51,6 +82,94 @@ public IReadOnlyCollection<int> GetThreadIds()
}
}

public bool TrySuspendProcess()
{
if (TryProcessLevelSuspend())
return true;

// Fallback: sweep semua thread saat ini. Tidak sempurna (thread baru
// bisa lolos), tapi tetap lebih baik daripada gagal total.
int attempted = 0, succeeded = 0;
foreach (int threadId in GetThreadIds())
{
attempted++;
if (TrySuspendThread(threadId))
succeeded++;
}

App.Logger.WriteLine(LOG_IDENT,
$"PID={_processId}: NtSuspendProcess unavailable — per-thread fallback {succeeded}/{attempted} threads");
return attempted > 0 && succeeded == attempted;
}

public bool TryResumeProcess()
{
if (TryProcessLevelResume())
return true;

int attempted = 0, succeeded = 0;
foreach (int threadId in GetThreadIds())
{
attempted++;
if (TryResumeThread(threadId))
succeeded++;
}

App.Logger.WriteLine(LOG_IDENT,
$"PID={_processId}: NtResumeProcess unavailable — per-thread fallback {succeeded}/{attempted} threads");
return attempted > 0 && succeeded == attempted;
}

private bool TryProcessLevelSuspend()
{
IntPtr handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, (uint)_processId);
if (handle == IntPtr.Zero)
{
App.Logger.WriteLine(LOG_IDENT,
$"PID={_processId}: OpenProcess(PROCESS_SUSPEND_RESUME) failed (error={Marshal.GetLastWin32Error()})");
return false;
}

try
{
NTSTATUS status = NtSuspendProcess(handle);
if (status != NTSTATUS.Success)
{
App.Logger.WriteLine(LOG_IDENT, $"PID={_processId}: NtSuspendProcess returned {status}");
return false;
}

return true;
}
finally
{
CloseHandle(handle);
}
}

private bool TryProcessLevelResume()
{
IntPtr handle = OpenProcess(PROCESS_SUSPEND_RESUME, false, (uint)_processId);
if (handle == IntPtr.Zero)
return false;

try
{
NTSTATUS status = NtResumeProcess(handle);
if (status != NTSTATUS.Success)
{
App.Logger.WriteLine(LOG_IDENT, $"PID={_processId}: NtResumeProcess returned {status}");
return false;
}

return true;
}
finally
{
CloseHandle(handle);
}
}

public bool TrySuspendThread(int threadId)
{
IntPtr handle = OpenThread(THREAD_SUSPEND_RESUME, false, (uint)threadId);
Expand Down Expand Up @@ -130,6 +249,9 @@ public long GetProcessorTimeTicks()

public void Dispose() => _process.Dispose();

[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenProcess(uint desiredAccess, bool inheritHandle, uint processId);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr OpenThread(uint desiredAccess, bool inheritHandle, uint threadId);

Expand All @@ -142,5 +264,16 @@ public long GetProcessorTimeTicks()
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr handle);

private enum NTSTATUS : uint
{
Success = 0x00000000
}

[DllImport("ntdll.dll")]
private static extern NTSTATUS NtSuspendProcess(IntPtr processHandle);

[DllImport("ntdll.dll")]
private static extern NTSTATUS NtResumeProcess(IntPtr processHandle);
}
}
Loading
Loading