반응형
외부 프로세스가 우리의 프로세스에 접근해야하거나, 온디바이스 형태로 올라올 때 보안적인 이슈가 생길 수 있는데요.
우리가 제한해놓은 범위안에서 허용된 명령어로만 접근하면 좋겠지만, 악의적으로 시스템 깊이 접근해서 파일쓰기/읽기 같은 행위를 하게된다면 보안적으로 아주 위험한 상황이 되겠죠..
Windows API로 구현하는 샌드박스 기술로서 프로세스 격리 기술을 OS단에서 구현하는 보안 시스템을 구현하는 방법입니다.

Windows 무결성 메커니즘(MIC)의 이해
Windows에는 MIC(Mandatory Integrity Control)라는 보안 모델이 있습니다. 모든 프로세스는 무결성 등급을 가집니다.
- High (관리자): 시스템 설정 변경 가능
- Medium (일반 유저): 일반적인 앱 실행 권한
- Low (샌드박스): 매우 제한된 영역(예: 임시 폴더) 외에는 읽기/쓰기 차단
우리의 목표는 내부 프로그램 위에 외부 프로그램을 실행해야하는 일이 생길 때, 이 등급을 강제로 'Low'로 낮추어 OS 단에서 시스템을 보호하면서 프로세스를 띄우는 것입니다.
프로세스 격리 5단계 워크플로우
다음의 단계를 거쳐서 프로세스를 구성할 수 있습니다.
- Token Open: 현재 실행 중인 프로세스의 권한 증명서(Token)를 가져옵니다. 외부 프로그램을 띄울 때 자동으로 생성되는 보안토큰이라고 보면 되요.
- Privilege Enable: 증명서의 내용을 고칠 수 있는 특수 권한(SeRelabelPrivilege)을 활성화합니다. 우리가 프로그램을 관리자권한으로 실행하여 더 높은 범위의 기능을 활용하듯이, 특수 권한을 켜줌으로써 다른 프로그램의 권한까지 조정할 수 있게되는 것이죠.
- Token Duplicate: 원본을 복제하여 수정 가능한 새 증명서를 만듭니다. 간단하게 보안토큰을 복사해서 복사한토큰의 권한조정을 해서 프로세스를 띄울 예정이에요.
- Integrity SID 설정: 새 증명서에 이 프로세스는 최하위 계급(Low Integrity)이다 라는 낙인(SID: S-1-16-4096)을 찍습니다.
- CreateProcessAsUser: 이 '낙인찍힌 증명서'를 들고 외부 프로세스를 띄워줍니다.
코드 작성
일반적인 Process.Start()를 사용하지 않고 Win32 API인 CreateProcessAsUser를 사용하는 이유는, 수정된 보안 토큰을 직접 주입할 수 있는 유일한 방법이기 때문입니다.
using System.ComponentModel;
using System.Diagnostics;
using System.Management;
using System.Runtime.InteropServices;
using System.Text;
namespace SecuritySystem.AppGateway.Security
{
public class ProcessManager : IDisposable
{
private Process _processToWatch;
private ManagementEventWatcher _processEventWatcher;
private readonly string _processPathToLaunch;
public event EventHandler ProcessExited;
public ProcessManager(string processPathToLaunch)
{
if (string.IsNullOrEmpty(processPathToLaunch) || !File.Exists(processPathToLaunch))
{
throw new FileNotFoundException("ProcessManager: The specified executable file does not exist.", processPathToLaunch);
}
_processPathToLaunch = processPathToLaunch;
}
public void LaunchAndWatch(string arguments)
{
try
{
_processToWatch = StartProcessWithLowIntegrity(_processPathToLaunch, arguments);
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine($"[ProcessManager] Successfully launched '{Path.GetFileName(_processPathToLaunch)}' (PID: {_processToWatch.Id}) with Low Integrity.");
Console.ResetColor();
StartProcessExitWatcher();
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[ProcessManager] FATAL: Failed to launch process. {ex.Message}");
Console.ResetColor();
throw;
}
}
private Process StartProcessWithLowIntegrity(string processPath, string arguments)
{
IntPtr hToken = IntPtr.Zero, hNewToken = IntPtr.Zero, pIntegritySid = IntPtr.Zero;
var pInfo = new PROCESS_INFORMATION();
var sInfo = new STARTUPINFO { cb = Marshal.SizeOf(typeof(STARTUPINFO)) };
Console.WriteLine("\n--- Starting Low Integrity Process ---");
bool stepResult;
int lastError;
try
{
// [1단계] 현재 프로세스 토큰 열기
stepResult = OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out hToken);
lastError = Marshal.GetLastWin32Error();
if (!stepResult) throw new Win32Exception(lastError);
// [2단계] SeRelabelPrivilege 활성화 : 프로세스 레벨 수정 특수권한
stepResult = EnableRelabelPrivilege(hToken); // 레벨 변경 작업의 권한 활성화를 위한
lastError = Marshal.GetLastWin32Error();
if (!stepResult) throw new Win32Exception(lastError, "Failed to enable SeRelabelPrivilege.");
// [3단계] 토큰 복제
stepResult = DuplicateTokenEx(hToken, 0, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out hNewToken);
lastError = Marshal.GetLastWin32Error();
if (!DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out hNewToken))
throw new Win32Exception(Marshal.GetLastWin32Error(), "DuplicateTokenEx failed.");
// [4단계] SID 생성 - Low Level : 낮은 무결성 등급(S-1-16-4096)
stepResult = ConvertStringSidToSid("S-1-16-4096", out pIntegritySid);
lastError = Marshal.GetLastWin32Error();
if (!stepResult) throw new Win32Exception(lastError);
// [5단계] 토큰 정보 설정
var til = new TOKEN_MANDATORY_LABEL { Label = { Sid = pIntegritySid, Attributes = SE_GROUP_INTEGRITY } };
// EnableRelabelPrivilege로 인해 가능한 SetTokenInformation 작업
stepResult = SetTokenInformation(hNewToken, TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, ref til, (uint)(Marshal.SizeOf(til) + GetLengthSid(pIntegritySid)));
lastError = Marshal.GetLastWin32Error();
if (!stepResult) throw new Win32Exception(lastError);
var commandLine = new StringBuilder($"\"{processPath}\" {arguments}");
// 프로세스 생성
if (!CreateProcessAsUser(hNewToken, null, commandLine, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, null, ref sInfo, out pInfo))
throw new Win32Exception(Marshal.GetLastWin32Error());
return Process.GetProcessById((int)pInfo.dwProcessId);
}
finally
{
CloseHandle(hToken);
CloseHandle(hNewToken);
if (pIntegritySid != IntPtr.Zero) LocalFree(pIntegritySid);
CloseHandle(pInfo.hProcess);
CloseHandle(pInfo.hThread);
}
}
// WMI 감시 시작
private void StartProcessExitWatcher()
{
if (_processToWatch == null) return;
string query = $"SELECT * FROM __InstanceDeletionEvent WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' AND TargetInstance.ProcessId = {_processToWatch.Id}";
_processEventWatcher = new ManagementEventWatcher(query);
// '비상 연락처' 등록: 이벤트가 발생하면, OnProcessExited를 호출
_processEventWatcher.EventArrived += OnProcessExited;
_processEventWatcher.Start();
Console.ForegroundColor = ConsoleColor.Blue;
Console.WriteLine($"[ProcessManager] WMI watcher is now monitoring the client process (PID: {_processToWatch.Id}).");
Console.ResetColor();
}
/
// 모든 자원을 정리하는 '퇴근 절차'
public void Dispose()
{
// 1. WMI 감시자부터 정리
DisposeWatcher();
// 2. 관리하던 프로세스가 아직 살아있으면, 강제 종료
try
{
if (_processToWatch != null && !_processToWatch.HasExited)
{
Console.WriteLine($"[ProcessManager] Terminating the monitored process (PID: {_processToWatch.Id})...");
_processToWatch.Kill();
_processToWatch.Dispose();
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"[ProcessManager] Error while terminating process: {ex.Message}");
Console.ResetColor();
}
finally
{
_processToWatch = null;
}
}
private bool EnableRelabelPrivilege(IntPtr hToken)
{
var luid = new LUID();
if (!LookupPrivilegeValue(null, "SeRelabelPrivilege", ref luid)) return false;
var tp = new TOKEN_PRIVILEGES { PrivilegeCount = 1, Privileges = new LUID_AND_ATTRIBUTES[1] };
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
return AdjustTokenPrivileges(hToken, false, ref tp, (uint)Marshal.SizeOf(tp), IntPtr.Zero, IntPtr.Zero);
}
#region P/Invoke Declarations
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CreateProcessAsUser(IntPtr hToken, string lpApplicationName, StringBuilder lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool SetTokenInformation(IntPtr TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, ref TOKEN_MANDATORY_LABEL TokenInformation, uint TokenInformationLength);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool OpenProcessToken(IntPtr ProcessHandle, uint DesiredAccess, out IntPtr TokenHandle);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, TOKEN_TYPE TokenType, out IntPtr phNewToken);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern bool ConvertStringSidToSid(string StringSid, out IntPtr pSid);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern int GetLengthSid(IntPtr pSid);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
private static extern IntPtr LocalFree(IntPtr hMem);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, uint BufferLength, IntPtr PreviousState, IntPtr ReturnLength);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, ref LUID lpLuid);
[StructLayout(LayoutKind.Sequential)] public struct LUID { public uint LowPart; public int HighPart; }
[StructLayout(LayoutKind.Sequential)] public struct LUID_AND_ATTRIBUTES { public LUID Luid; public uint Attributes; }
[StructLayout(LayoutKind.Sequential)] public struct TOKEN_PRIVILEGES { public uint PrivilegeCount; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] public LUID_AND_ATTRIBUTES[] Privileges; }
[StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; }
[StructLayout(LayoutKind.Sequential)] public struct SID_AND_ATTRIBUTES { public IntPtr Sid; public uint Attributes; }
[StructLayout(LayoutKind.Sequential)] public struct TOKEN_MANDATORY_LABEL { public SID_AND_ATTRIBUTES Label; }
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct STARTUPINFO { public int cb; public string lpReserved; public string lpDesktop; public string lpTitle; public int dwX; public int dwY; public int dwXSize; public int dwYSize; public int dwXCountChars; public int dwYCountChars; public int dwFillAttribute; public int dwFlags; public short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; }
private const uint TOKEN_DUPLICATE = 0x0002;
private const uint TOKEN_QUERY = 0x0008;
private const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
private const uint SE_GROUP_INTEGRITY = 0x00000020;
private const uint SE_PRIVILEGE_ENABLED = 0x00000002;
private const uint TOKEN_ALL_ACCESS = 0x000F01FF;
public enum TOKEN_TYPE { TokenPrimary = 1 }
public enum SECURITY_IMPERSONATION_LEVEL { SecurityImpersonation = 2 }
public enum TOKEN_INFORMATION_CLASS { TokenIntegrityLevel = 25 }
#endregion
}
}
이중 방어(Defense in Depth)
설령 우리 프로그램의 RBAC(권한 제어) 로직에 버그가 있더라도, 실행된 워드패드 자체가 OS 레벨에서 'LOW 권한'이기 때문에 C:\Windows 같은 민감한 디렉토리에 파일을 쓰거나 시스템 설정을 파괴하는 행위가 물리적으로 불가능해집니다.
이는 애플리케이션 보안과 운영체제 보안이 협력하는 가장 이상적인 모델입니다.
반응형
'IT > Architecture' 카테고리의 다른 글
| 사용자 권한을 데이터로 관리하기 | RBAC 엔진 구현하기 (0) | 2026.01.25 |
|---|---|
| OS 상태 체크 | WMI를 이용한 실시간 프로세스 모니터링 (0) | 2026.01.25 |
| TDD 도입 및 안착 가이드 (1) | 2026.01.17 |
| 🧪테스트주도개발(TDD) 하기 | 핵심 용어 (0) | 2025.12.26 |
| 도메인 주도 설계, DDD하기 (0) | 2025.12.03 |