Windows 시스템 보호를 위한 고급 기술: ZTool 분석

개요

ZTool은 원격 제어 프로그램, 로컬 시스템 접근 제한 소프트웨어, 전체 화면 잠금 애플리케이션, 그리고 지속적인 창 상단 고정 기능을 사용하는 특정 랜섬웨어에 대응하기 위해 설계된 강력한 유틸리티입니다. 이 도구는 다양한 유형의 원격 제어 바이러스, 로컬에 설치된 제어 소프트웨어, 교육용 전자 교실 및 PC방 관리 프로그램, 유해 사이트 차단 솔루션, 가짜 블루스크린 장난 프로그램 등으로부터 시스템을 보호하는 데 활용될 수 있습니다.

주요 기능

기본 원격 제어 방어

  • 다른 애플리케이션의 마우스 및 키보드 후킹 제어를 차단합니다.
  • ZTool 창이 스크린샷이나 화면 녹화에 의해 캡처되는 것을 방지합니다.
  • ZTool 창을 항상 최상위에 유지하며, System 권한에서는 UIAccess를 통해 '슈퍼 최상위'로 고정됩니다.

프로세스 제어

  • 프로세스 이름 또는 PID를 기반으로 프로세스를 종료, 일시 중단/재개할 수 있습니다.
  • 프로세스 권한을 'Untrusted' 수준으로 격하시킵니다.
  • 모든 비시스템 프로세스를 종료하거나, 새로운 프로세스 생성을 금지합니다.
  • 선택적으로 '강력 모드'를 활성화하여 더욱 강력한 제어 기능을 사용할 수 있습니다.

창 관리

  • 활성화된 창을 강제로 닫습니다.
  • 창이 속한 프로세스를 식별하고 해당 프로세스를 종료합니다.
  • 창 닫기 메시지 가로채기를 우회하기 위해 대상 창을 조작 가능한 다른 창 내부에 중첩시킵니다.

전원 옵션

  • 시스템 강제 종료, 재시작, 로그오프를 수행합니다.
  • 시스템 블루스크린(BSOD)을 트리거합니다.

자체 보호 및 실시간 감시

  • 다른 애플리케이션 계층 프로세스로부터 ZTool 프로세스가 종료되는 것을 방지합니다.
  • 비시스템 프로세스의 디버깅, 종료, 원격 종료, 드라이버 로드 특권 등을 박탈합니다.

기타 기능

  • System 권한으로 작업 관리자 및 명령 프롬프트를 실행합니다.
  • '2048' 미니 게임이 내장되어 있습니다.
  • 자동 권한 상승 기능을 통해 System 권한 및 UIAccess 권한 획득을 선택할 수 있습니다.

작동 원리 분석

기본 원격 제어 방어 기술

마우스 및 키보드 제어 후킹 방어

대부분의 원격 제어 프로그램은 두 가지 방식으로 마우스와 키보드를 제어합니다. 첫 번째는 SetWindowsHookEx API를 사용하여 키보드 및 마우스 훅을 등록하여 사용자 입력을 가로채거나 차단하는 방식입니다. 두 번째는 ClipCursor 또는 SetCursorPos를 사용하여 마우스 커서의 이동 범위를 제한하는 방식입니다.

후킹 무력화

SetWindowsHookEx를 통한 후킹에 대응하기 위해, ZTool은 지속적으로 두 개의 저수준 키보드 및 마우스 훅을 등록하고 해제하는 방식으로 원격 제어 프로그램의 훅을 덮어씁니다. 이는 주기적인 훅 재등록을 통해 기존 훅을 무효화하는 효과를 가집니다.

// 키보드 이벤트 후킹을 지속적으로 무력화하는 함수
void MonitorAndReapplyKeyboardHooks() {
    HHOOK kbdHookA = NULL; // 첫 번째 키보드 훅 핸들
    HHOOK kbdHookB = NULL; // 두 번째 키보드 훅 핸들

    while (TRUE) {
        // 기존 훅을 해제하고 새 저수준 키보드 훅을 등록하여 다른 프로그램의 훅을 덮어씁니다.
        if (kbdHookA) {
            UnhookWindowsHookEx(kbdHookA);
        }
        kbdHookA = SetWindowsHookEx(WH_KEYBOARD_LL, GlobalLowLevelKeyboardProc, NULL, 0);

        if (kbdHookB) {
            UnhookWindowsHookEx(kbdHookB);
        }
        kbdHookB = SetWindowsHookEx(WH_KEYBOARD_LL, GlobalLowLevelKeyboardProc, NULL, 0);
        
        Sleep(25); // 짧은 대기 후 반복
    }
}

// 마우스 이벤트 후킹을 지속적으로 무력화하는 함수
void MonitorAndReapplyMouseHooks() {
    HHOOK mouseHookA = NULL; // 첫 번째 마우스 훅 핸들
    HHOOK mouseHookB = NULL; // 두 번째 마우스 훅 핸들

    while (TRUE) {
        // 기존 훅을 해제하고 새 저수준 마우스 훅을 등록하여 다른 프로그램의 훅을 덮어씁니다.
        if (mouseHookA) {
            UnhookWindowsHookEx(mouseHookA);
        }
        mouseHookA = SetWindowsHookEx(WH_MOUSE_LL, GlobalLowLevelMouseProc, NULL, 0);

        if (mouseHookB) {
            UnhookWindowsHookEx(mouseHookB);
        }
        mouseHookB = SetWindowsHookEx(WH_MOUSE_LL, GlobalLowLevelMouseProc, NULL, 0);
        
        Sleep(25); // 짧은 대기 후 반복
    }
}

마우스 커서 제한 우회

ClipCursor를 사용하여 마우스 커서 범위가 제한될 경우, ZTool은 커서의 움직임을 감지하여 비정상적인 이동(예: 너무 빠른 순간이동)이 발생하면 커서를 원래 위치로 강제로 되돌립니다. 또한, ClipCursor(NULL)을 주기적으로 호출하여 커서 제한을 해제하고 데스크톱 전체로 커서 이동이 가능하도록 합니다.

// 특정 좌표로 마우스 커서를 강제 이동 및 제한 해제 함수
void EnforceCursorPosition(int x, int y) {
    ClipCursor(NULL); // 모든 커서 제한을 해제
    RECT targetRect = {x, y, x, y}; // 목표 위치에 1x1 픽셀 사각형 생성
    ClipCursor(&targetRect); // 커서를 목표 위치에만 제한
    Sleep(1);
    ClipCursor(NULL); // 제한 해제
}

// 마우스 후킹 및 커서 제한을 감지하고 대응하는 함수
void DetectAndCounterCursorControl() {
    POINT currentPos = {-1, -1}; // 현재 마우스 위치
    POINT lastPos = {-1, -1};    // 이전 마우스 위치

    while (TRUE) {
        GetCursorPos(¤tPos); // 현재 커서 위치를 가져옵니다.

        RECT clipRect, desktopRect;
        GetClipCursor(&clipRect); // 현재 커서 제한 영역을 가져옵니다.
        GetWindowRect(GetDesktopWindow(), &desktopRect); // 전체 데스크톱 영역을 가져옵니다.

        // 마우스가 너무 빠르게 이동했거나 (원격 제어 의심),
        // 커서가 제한되어 있으면서 동시에 제한 영역 밖에 있는 경우
        int distanceSquared = (currentPos.x - lastPos.x) * (currentPos.x - lastPos.x) + 
                              (currentPos.y - lastPos.y) * (currentPos.y - lastPos.y);
        
        // (가상의 'IsPointOutsideRect' 함수가 필요)
        // bool isOutsideClip = IsPointOutsideRect(currentPos.x, currentPos.y, clipRect); 

        if (distanceSquared > 50000 && lastPos.x != -1) { // 픽셀 거리 제곱이 너무 큰 경우
            EnforceCursorPosition(lastPos.x, lastPos.y); // 이전 위치로 강제 복귀
        } 
        // else if (!EqualRect(&clipRect, &desktopRect) && isOutsideClip) { // 커서가 제한되어 있고 제한 영역 밖에 있는 경우
        //     EnforceCursorPosition(lastPos.x, lastPos.y); // 이전 위치로 강제 복귀
        // } 
        else {
            lastPos = currentPos; // 현재 위치를 이전 위치로 저장
        }
        
        ClipCursor(NULL); // 주기적으로 커서 제한을 해제
        Sleep(5);
    }
}

화면 캡처 및 녹화 방지

ZTool 창이 스크린샷이나 화면 녹화에 의해 캡처되는 것을 방지하기 위해 SetWindowDisplayAffinity(hwnd, 0x11) API를 사용합니다. 이 API는 창의 표시 속성을 변경하여 캡처 도구의 화면 출력을 제한합니다.

항상 최상위 창 유지

ZTool 창이 항상 다른 창 위에 표시되도록 지속적으로 SetWindowPos API를 호출합니다. 특히, System 권한에서 실행될 경우 UIAccess 특권을 활용하여 작업 관리자와 같은 시스템 최상위 창까지 덮어쓸 수 있는 '슈퍼 최상위' 상태를 유지합니다.

// 특정 창을 항상 최상위에 유지하는 함수
void EnsureWindowAlwaysOnTop(HWND targetWindowHandle) {
    int lastX = 0, lastY = 0; // 창의 마지막 유효한 위치

    while (TRUE) {
        EnableWindow(targetWindowHandle, TRUE); // 창이 활성화되어 있는지 확인
        // 창을 최상위(HWND_TOPMOST)로 설정하고 크기를 고정합니다. (SWP_NOMOVE는 위치 변경 방지)
        SetWindowPos(targetWindowHandle, HWND_TOPMOST, 0, 0, 400, 400, SWP_NOMOVE);
        ShowWindow(targetWindowHandle, SW_SHOW);    // 창 표시
        ShowWindow(targetWindowHandle, SW_NORMAL);  // 일반 상태로 표시

        RECT windowRect;
        int screenWidth = GetSystemMetrics(SM_CXSCREEN);
        int screenHeight = GetSystemMetrics(SM_CYSCREEN);

        // 창이 화면 밖으로 벗어났는지 확인하고, 벗어났다면 마지막 유효 위치로 조정합니다.
        if (GetWindowRect(targetWindowHandle, &windowRect)) {
            int currentRight = windowRect.right;
            int currentBottom = windowRect.bottom;
            int currentLeft = windowRect.left;
            int currentTop = windowRect.top;

            if (currentLeft < 0 || currentRight > screenWidth || currentTop < 0 || currentBottom > screenHeight) {
                // 창이 화면 밖으로 벗어났다면 마지막 유효 위치로 되돌립니다.
                SetWindowPos(targetWindowHandle, HWND_TOPMOST, lastX, lastY, 400, 400, 0);
            } else {
                // 창이 화면 내에 있다면 현재 위치를 저장합니다.
                lastX = currentLeft;
                lastY = currentTop;
            }
        }
        Sleep(5); // 짧은 대기 후 반복
    }
}

프로세스 제어 기술

프로세스 종료

일반적인 애플리케이션 계층에서 보호된 프로세스를 종료하는 것은 매우 어렵습니다. ZTool은 이를 위해 다양한 방법을 순차적으로 시도합니다.

ZTool의 일반 모드에서는 주로 대상 프로세스의 메인 창에 종료 메시지를 전송하거나, 표준 NtOpenProcessNtTerminateProcess API를 사용합니다. 반면, '강력 모드'가 활성화되면 ZTool은 다음과 같은 15가지 방법을 순차적으로 시도하여 프로세스 종료를 강행합니다:

  1. 프로세스 메인 창으로 종료 메시지 전송
  2. 표준 NtOpenProcessNtTerminateProcess API 호출
  3. 프로세스를 JobObject에 연결 후 종료
  4. 원격 스레드 주입을 통해 대상 프로세스 내부에서 ExitProcess 실행
  5. NtDuplicateHandle을 사용하여 프로세스 핸들을 복제 후 표준 종료 방법 적용
  6. 대상 프로세스의 모든 핸들을 강제 종료
  7. 대상 프로세스의 모든 스레드를 강제 종료
  8. 대상 프로세스의 모든 스레드를 하이재킹하여 ExitProcess 실행
  9. 대상 프로세스의 모든 메모리 페이지를 접근 불가능 상태로 설정
  10. NtDebugActiveProcess를 사용하여 프로세스 종료
  11. 대상 프로세스의 ntdll 모듈을 언로드
  12. NtGetNextProcess를 사용하여 프로세스 핸들을 얻은 후 표준 종료 방법 적용
  13. lsass.exe에 원격 스레드를 주입하여 DebugActiveProcess로 대상 프로세스 종료
  14. 프로세스 권한을 격하한 후 대상 창에 대량의 메시지 폭탄 전송
  15. EndTask API 사용

강력 모드는 일부 경량급 안티바이러스 프로그램을 종료할 수 있으며, 대부분의 원격 제어 프로그램에는 충분히 효과적입니다.

프로세스 일시 중단/재개

프로세스의 실행을 일시적으로 중단하거나 다시 시작하기 위해 NtSuspendProcessNtResumeProcess API를 활용합니다.

프로세스 권한 'Untrusted'로 격하

ZTool은 대상 프로세스를 PROCESS_QUERY_LIMITED_INFORMATION 권한으로 열고, 디버그 권한을 포함하는 토큰 핸들을 획득합니다. 이후 SetTokenInformation을 사용하여 프로세스 토큰의 무결성 수준을 SECURITY_MANDATORY_UNTRUSTED_RID로 변경합니다.

// 특정 프로세스의 디버그 권한을 활성화하는 함수
bool ObtainProcessDebugPrivilege(HANDLE processHandle, HANDLE* tokenHandle) {
    LUID debugPrivilegeLuid;
    TOKEN_PRIVILEGES tokenPrivileges;

    // 프로세스 토큰을 열어 모든 권한 및 쿼리 권한을 얻습니다.
    if (!OpenProcessToken(processHandle, TOKEN_ALL_ACCESS | TOKEN_QUERY, tokenHandle)) {
        return FALSE;
    }

    // SE_DEBUG_NAME 권한의 LUID를 조회합니다.
    if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &debugPrivilegeLuid)) {
        CloseHandle(*tokenHandle);
        return FALSE;
    }

    // 토큰 권한 구조를 설정하여 SE_DEBUG_NAME을 활성화합니다.
    tokenPrivileges.PrivilegeCount = 1;
    tokenPrivileges.Privileges[0].Luid = debugPrivilegeLuid;
    tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;

    // 토큰 권한을 조정합니다.
    if (!AdjustTokenPrivileges(*tokenHandle, FALSE, &tokenPrivileges, sizeof(tokenPrivileges), NULL, NULL)) {
        return FALSE;
    }
    return TRUE;
}

// 특정 프로세스의 무결성 수준을 'Untrusted'로 격하하는 함수
void DemoteProcessIntegrityLevel(DWORD processId) {
    HANDLE pHandle = NULL;
    HANDLE pToken = NULL;

    // 제한된 정보 쿼리 권한으로 프로세스를 엽니다.
    pHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processId);
    if (!pHandle) return;

    // 디버그 권한을 획득합니다.
    if (!ObtainProcessDebugPrivilege(pHandle, &pToken)) {
        CloseHandle(pHandle);
        return;
    }

    // Untrusted 무결성 수준의 SID를 구성합니다.
    DWORD untrustedLevel = SECURITY_MANDATORY_UNTRUSTED_RID;
    SID sidStructure = {};
    sidStructure.Revision = SID_REVISION;
    sidStructure.SubAuthorityCount = 1;
    sidStructure.IdentifierAuthority.Value[5] = 16; // SECURITY_MANDATORY_LABEL_AUTHORITY
    sidStructure.SubAuthority[0] = untrustedLevel;

    TOKEN_MANDATORY_LABEL mandatoryLabel = {};
    mandatoryLabel.Label.Attributes = SE_GROUP_INTEGRITY;
    mandatoryLabel.Label.Sid = &sidStructure;

    // 프로세스 토큰의 무결성 수준을 Untrusted로 설정합니다.
    SetTokenInformation(pToken,
                        TokenIntegrityLevel,
                        &mandatoryLabel,
                        sizeof(TOKEN_MANDATORY_LABEL) + GetLengthSid(&sidStructure));
    
    CloseHandle(pToken);
    CloseHandle(pHandle);
}

프로세스가 'Untrusted'로 격하되면 다음과 같은 효과가 나타납니다:

  • 작업 관리자에서 해당 프로세스가 'Idle' (시스템 유휴) 또는 'Interrupts' (시스템 인터럽트)로만 보일 수 있습니다.
  • 격하된 프로세스가 새 프로세스를 생성하려고 하면 "애플리케이션을 시작할 수 없습니다." 오류가 발생할 수 있습니다.
  • 일부 작업 수행 시 프로세스가 충돌할 수 있습니다.

주의할 점은 권한 격하가 되돌릴 수 없으며, 격하된 프로세스는 시스템에 대한 대부분의 작업을 수행할 수 없게 된다는 것입니다. 하지만 커널 드라이버와는 여전히 통신할 수 있으므로, 커널 드라이버를 로드한 프로세스에는 이 방법이 유효하지 않습니다.

모든 비시스템 프로세스 종료

ZTool은 하드코딩된 시스템 필수 프로세스 화이트리스트를 사용하여 비시스템 프로세스를 식별하고 종료합니다. 이 화이트리스트에 없는 프로세스는 잠재적으로 불필요하거나 악성으로 간주되어 종료 대상이 됩니다. 일반적인 시스템 프로세스(예: explorer.exe, svchost.exe, dwm.exe 등)는 종료되지 않도록 보호됩니다.

// 비시스템 프로세스 화이트리스트 (예시)
bool IsSystemProcess(const char* processName, DWORD pid) {
    // 현재 ZTool 프로세스 자체는 보호
    if (pid == GetCurrentProcessId()) return true;

    // 핵심 시스템 프로세스 및 시스템 기능을 제공하는 프로세스
    if (strcmp("System", processName) == 0 ||
        strcmp("[System Process]", processName) == 0 ||
        strcmp("Registry", processName) == 0 ||
        strcmp("smss.exe", processName) == 0 ||
        strcmp("csrss.exe", processName) == 0 ||
        strcmp("wininit.exe", processName) == 0 ||
        strcmp("services.exe", processName) == 0 ||
        strcmp("lsass.exe", processName) == 0 ||
        strcmp("winlogon.exe", processName) == 0 ||
        strcmp("explorer.exe", processName) == 0 ||
        strcmp("dwm.exe", processName) == 0 ||
        strcmp("svchost.exe", processName) == 0 ||
        strcmp("taskhostw.exe", processName) == 0 ||
        strcmp("StartMenuExperienceHost.exe", processName) == 0 ||
        strcmp("ShellExperienceHost.exe", processName) == 0 ||
        strcmp("sihost.exe", processName) == 0 ||
        strcmp("TextInputHost.exe", processName) == 0 ||
        strcmp("dllhost.exe", processName) == 0 ||
        strcmp("WUDFHost.exe", processName) == 0 ||
        strcmp("spoolsv.exe", processName) == 0 ||
        strcmp("lsm.exe", processName) == 0 ||
        strcmp("LMS.exe", processName) == 0 || // Lenovo Management Service (예시)
        strcmp("audiodg.exe", processName) == 0 ||
        strcmp("fontdrvhost.exe", processName) == 0 ||
        strcmp("conhost.exe", processName) == 0) {
        return true;
    }
    return false;
}

// 숨겨진 프로세스를 포함하여 모든 비시스템 프로세스를 식별하고 종료하는 함수
void IdentifyAndTerminateHiddenProcesses() {
    bool knownPids[65536] = {}; // Process32Snapshot으로 파악된 PID를 기록하는 배열
    PROCESSENTRY32 processEntry = {sizeof(PROCESSENTRY32)};
    HANDLE snapshotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    // 우선 Process32Snapshot을 통해 일반적인 프로세스 목록을 확보합니다.
    if (Process32First(snapshotHandle, &processEntry)) {
        do {
            knownPids[processEntry.th32ProcessID] = true;
            // 화이트리스트에 없는 프로세스는 여기서 바로 종료 시도 (선택적)
            if (!IsSystemProcess(processEntry.szExeFile, processEntry.th32ProcessID)) {
                // TerminateProcessById(processEntry.th32ProcessID); // 예시: 종료 함수 호출
            }
        } while (Process32Next(snapshotHandle, &processEntry));
    }
    CloseHandle(snapshotHandle);

    ULONG bufferSize = 0x4000; // 초기 버퍼 크기
    LPVOID systemInfoBuffer = NULL;
    NTSTATUS status;

    // ZwQuerySystemInformation을 사용하여 시스템 핸들 정보를 가져옵니다.
    // 숨겨진 프로세스는 NtQuerySystemInformation 훅을 우회하기 위해 이 방법을 사용합니다.
    do {
        systemInfoBuffer = malloc(bufferSize);
        if (systemInfoBuffer == NULL) {
            return; // 메모리 할당 실패
        }
        memset(systemInfoBuffer, 0, bufferSize);
        status = ZwQuerySystemInformation(SystemHandleInformation, systemInfoBuffer, bufferSize, NULL);

        if (status == (NTSTATUS)0xC0000004) { // STATUS_INFO_LENGTH_MISMATCH (버퍼 부족)
            free(systemInfoBuffer);
            systemInfoBuffer = NULL;
            bufferSize *= 2; // 버퍼 크기를 두 배로 늘립니다.
        }
    } while (status == (NTSTATUS)0xC0000004);

    if (status != 0) { // 성공하지 못한 경우
        if (systemInfoBuffer) free(systemInfoBuffer);
        return;
    }

    PSYSTEM_HANDLE_INFORMATION handleInfo = (PSYSTEM_HANDLE_INFORMATION)systemInfoBuffer;
    DWORD currentPid = 0;

    // 핸들 목록을 순회하며 알려지지 않은(숨겨진) 프로세스를 식별합니다.
    for (DWORD i = 0; i < handleInfo->Count; i++) {
        if (currentPid != handleInfo->Handle[i].OwnerPid) {
            currentPid = handleInfo->Handle[i].OwnerPid;
            if (currentPid != 0 && !knownPids[currentPid]) {
                // 알려진 PID 목록에 없는 프로세스는 숨겨진 프로세스일 가능성이 높습니다.
                // TerminateProcessById(currentPid); // 예시: 종료 함수 호출
            }
        }
    }

    if (systemInfoBuffer) free(systemInfoBuffer);
}

프로세스 생성 금지

ZTool은 실시간으로 현재 실행 중인 프로세스 목록을 감시합니다. 새로운 프로세스가 생성되면 즉시 이를 감지하고 강제로 종료하여 시스템 내에서의 무단 프로세스 실행을 방지합니다.

창 관리 기술

창 닫기

대상 창을 닫기 위해 ZTool은 먼저 표준 WM_CLOSE 메시지를 전송합니다. 이 메시지가 가로채져 창이 닫히지 않는 경우, ZTool은 더욱 강력한 방법을 사용합니다.

Windows의 DestroyWindow API는 일반적으로 대상 창이 속한 스레드에서만 호출 가능합니다. 이를 우회하기 위해 ZTool은 두 가지 접근 방식을 사용합니다:

  1. 대상 창 스레드를 하이재킹하여 해당 스레드 컨텍스트 내에서 DestroyWindow를 호출합니다.
  2. 새로운 빈 창을 생성하고 SetParent API를 사용하여 대상 창을 이 빈 창의 자식으로 설정합니다. 그 후, 빈 부모 창을 DestroyWindow로 파괴하여, 부모와 함께 자식 창도 파괴되도록 합니다.

이러한 방법도 실패하면, ZTool은 대상 창에 대량의 다양한 메시지를 전송하여 창을 불안정하게 만들거나 강제로 종료되도록 유도합니다.

// 원격 스레드 하이재킹을 통해 DestroyWindow를 호출하는 함수 (64비트 기준)
bool HijackThreadToCallDestroy(DWORD pid, HANDLE hThread, HWND hWnd) {
    // 1. 대상 스레드 일시 중단 및 CONTEXT 정보 획득
    if (SuspendThread(hThread) == (DWORD)-1) return false;
    CONTEXT threadCtx = {};
    threadCtx.ContextFlags = CONTEXT_ALL;
    if (!GetThreadContext(hThread, &threadCtx)) {
        ResumeThread(hThread);
        return false;
    }

    // 2. 원격 프로세스 내에서 필요한 API 주소의 RVA(Relative Virtual Address)를 분석
    // (GetRemoteModuleBase는 원격 프로세스에서 모듈의 베이스 주소를 찾는 가상의 함수)
    auto ResolveRemoteApiRva = [&](LPCSTR apiName, const wchar_t* moduleName) -> ULONG_PTR {
        ULONG_PTR localBase = reinterpret_cast(GetModuleHandleW(moduleName));
        ULONG_PTR localApiAddr = reinterpret_cast(GetProcAddress((HMODULE)localBase, apiName));
        return localApiAddr - localBase;
    };
    ULONG_PTR rvaDestroyWindow = ResolveRemoteApiRva("DestroyWindow", L"user32.dll");
    ULONG_PTR rvaNtContinue  = ResolveRemoteApiRva("NtContinue",   L"ntdll.dll");
    ULONG_PTR user32Base = GetRemoteModuleBase(pid, L"user32.dll"); // 가상 함수
    ULONG_PTR ntdllBase  = GetRemoteModuleBase(pid, L"ntdll.dll");  // 가상 함수
    ULONG_PTR pDestroyWindow = user32Base + rvaDestroyWindow;
    ULONG_PTR pNtContinue    = ntdllBase  + rvaNtContinue;
    if (!pDestroyWindow || !pNtContinue) {
        ResumeThread(hThread);
        return false;
    }

    // 3. 원격 메모리 할당: 원본 CONTEXT 저장 공간 + 셸코드 삽입 공간
    const SIZE_T shellcodeMaxSize = 128;
    SIZE_T bufferSize = sizeof(CONTEXT) + shellcodeMaxSize;
    HANDLE hProc = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid);
    if (!hProc) {
        ResumeThread(hThread);
        return false;
    }
    LPBYTE remoteAlloc = (LPBYTE)VirtualAllocEx(hProc, nullptr, bufferSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (!remoteAlloc) {
        ResumeThread(hThread);
        CloseHandle(hProc);
        return false;
    }

    // 원본 CONTEXT를 원격 메모리에 기록
    WriteProcessMemory(hProc, remoteAlloc, &threadCtx, sizeof(threadCtx), nullptr);

    // 4. 셸코드(stub) 구성: DestroyWindow(hWnd); NtContinue(&ctx, FALSE);
    BYTE shellcode[64]; BYTE* p = shellcode;
    // mov rcx, hWnd (DestroyWindow 첫 번째 인자)
    *p++ = 0x48; *p++ = 0xB9;
    *reinterpret_cast(p) = (ULONG_PTR)hWnd; p += 8;
    // call DestroyWindow
    *p++ = 0x48; *p++ = 0xB8;
    *reinterpret_cast(p) = pDestroyWindow; p += 8;
    *p++ = 0xFF; *p++ = 0xD0;
    // mov rcx, &ctx (NtContinue 첫 번째 인자 - 원본 CONTEXT 주소)
    *p++ = 0x48; *p++ = 0xB9;
    *reinterpret_cast(p) = (ULONG_PTR)remoteAlloc; p += 8;
    // xor edx, edx (NtContinue 두 번째 인자 - FALSE)
    *p++ = 0x33; *p++ = 0xD2;
    // mov rax, NtContinue
    *p++ = 0x48; *p++ = 0xB8;
    *reinterpret_cast(p) = pNtContinue; p += 8;
    // jmp rax (NtContinue로 점프하여 원본 실행 흐름 복원)
    *p++ = 0xFF; *p++ = 0xE0;

    SIZE_T shellcodeSize = p - shellcode;
    // 셸코드를 원격 메모리의 CONTEXT 뒤에 기록
    WriteProcessMemory(hProc, remoteAlloc + sizeof(threadCtx), shellcode, shellcodeSize, nullptr);
    // 셸코드 메모리 영역을 실행 가능하게 변경
    DWORD oldProtection;
    VirtualProtectEx(hProc, remoteAlloc, bufferSize, PAGE_EXECUTE_READ, &oldProtection);

    // 5. 대상 스레드의 CONTEXT를 수정하여 셸코드로 점프하도록 설정
    threadCtx.Rip = (ULONG_PTR)(remoteAlloc + sizeof(threadCtx)); // 셸코드 시작 주소로 RIP 설정
    // 스택 포인터 조정 (섀도우 스페이스 및 반환 주소 할당)
    threadCtx.Rsp -= 0x28; 
    ULONG_PTR returnAddr = threadCtx.Rip + shellcodeSize; // 셸코드 실행 후 돌아올 주소 (여기서는 NtContinue로 점프)
    WriteProcessMemory(hProc, (LPVOID)threadCtx.Rsp, &returnAddr, sizeof(returnAddr), nullptr);

    // 수정된 CONTEXT를 적용하고 스레드 재개
    SetThreadContext(hThread, &threadCtx);
    ResumeThread(hThread);
    CloseHandle(hProc);
    return true;
}

// 창을 강제로 파괴하는 시퀀스 (하이재킹 시도)
void AttemptForcedWindowDestruction(HWND targetWindow) {
    DWORD processId = 0;
    DWORD threadId = GetWindowThreadProcessId(targetWindow, &processId);
    if (!threadId) return;

    HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, FALSE, threadId);
    if (!hThread) return;

    // 대상 스레드를 하이재킹하여 DestroyWindow 실행 시도
    if (!HijackThreadToCallDestroy(processId, hThread, targetWindow)) {
        // 하이재킹 실패 시 오류 처리
    }
    CloseHandle(hThread);
}

// 창을 포괄적으로 닫는 함수
void ComprehensiveWindowClosure(HWND targetWindow, HWND messageWindow) {
    // 창 스타일을 변경하여 표준 창처럼 동작하도록 시도
    SetWindowLongPtr(targetWindow, GWL_EXSTYLE, WS_EX_APPWINDOW);
    SetWindowLongPtr(targetWindow, GWL_STYLE, WS_OVERLAPPEDWINDOW | WS_VISIBLE);

    // 1. 표준 종료 메시지 전송 시도
    PostMessage(targetWindow, WM_SYSCOMMAND, SC_CLOSE, 0);
    PostMessage(targetWindow, WM_CLOSE, 0, 0);
    Sleep(300);
    if (!IsWindow(targetWindow)) return; // 닫혔다면 종료

    // 2. 스레드 하이재킹을 통한 강제 DestroyWindow 호출 시도
    AttemptForcedWindowDestruction(targetWindow);
    Sleep(300);
    if (!IsWindow(targetWindow)) return; // 닫혔다면 종료

    // 3. 빈 부모 창을 통한 파괴 시도
    HWND tempParentWindow = CreateWindowEx(0, "STATIC", "", 0, 0, 0, 0, 0, NULL, NULL, NULL, NULL);
    SetWindowLongPtr(targetWindow, GWL_EXSTYLE, WS_EX_APPWINDOW);
    SetWindowLongPtr(targetWindow, GWL_STYLE, WS_OVERLAPPEDWINDOW | WS_VISIBLE);
    SetParent(targetWindow, tempParentWindow); // 대상 창을 빈 창의 자식으로 설정
    PostMessage(tempParentWindow, WM_CLOSE, 0, 0); // 부모 창 닫기를 통해 자식도 파괴
    if (!IsWindow(targetWindow)) return; // 닫혔다면 종료
    
    // 4. EndTask API를 통한 강제 종료 시도 (가상의 EndTask2 함수)
    // if (IsWindow(targetWindow)) {
    //     EndTask2(targetWindow, 0, 1);
    // }
    // Sleep(300);
    if (!IsWindow(targetWindow)) return; // 닫혔다면 종료

    // 5. 자식 창들까지 모두 처리 (가상의 EnumChildWindowsEx 및 cwnd 배열)
    // ccnt = 0;
    // EnumChildWindowsEx(targetWindow); // 자식 창들을 열거
    // for (int i = 1; i <= cnt; i++) {
    //     PostMessage(cwnd[i], WM_QUIT, 0, 0);
    //     PostMessage(cwnd[i], WM_DESTROY, 0, 0);
    //     CloseWindow(cwnd[i]);
    //     SetWindowPos(cwnd[i], 0, 5000, 5000, 0, 0, 0); // 화면 밖으로 이동
    //     SetWindowLongPtr(cwnd[i], GWLP_USERDATA, NULL);
    //     HWND childParent = CreateWindowEx(0, "STATIC", "", 0, 0, 0, 0, 0, NULL, NULL, NULL, NULL);
    //     SetWindowLongPtr(cwnd[i], GWL_EXSTYLE, WS_EX_APPWINDOW);
    //     SetWindowLongPtr(cwnd[i], GWL_STYLE, WS_OVERLAPPEDWINDOW | WS_VISIBLE);
    //     SetParent(cwnd[i], childParent);
    //     PostMessage(childParent, WM_CLOSE, 0, 0);
    // }

    // 6. 대상 창을 화면 밖으로 이동 및 데이터 초기화
    SetWindowPos(targetWindow, 0, 5000, 5000, 0, 0, 0);
    SetWindowLongPtr(targetWindow, GWLP_USERDATA, NULL);

    // 7. 대량의 메시지 폭탄 전송 (최후의 수단)
    for (int i = 0; i < 0x1000; i++) {
        PostMessage(targetWindow, i, 0, 0);
    }
}

창을 통한 프로세스 식별 및 종료

특정 창이 속한 프로세스를 식별하기 위해 GetWindowThreadProcessId API를 사용합니다. 이 API로 프로세스 ID를 얻은 후, 위에서 설명한 프로세스 종료 기술을 사용하여 해당 프로세스를 종료합니다.

창 중첩을 통한 메시지 가로채기 우회

창이 닫기 메시지를 가로채거나 무시할 때, ZTool은 다음과 같이 대응합니다:

  • 일반 모드: 대상 창의 스타일을 일반 창으로 변경하고, 전체 화면 및 최상위 설정을 해제한 다음, 창을 화면 중앙으로 이동시킵니다.
  • 강력 모드: 새로운 빈 창을 생성하고 SetParent API를 사용하여 대상 창을 이 빈 창의 자식으로 만듭니다. 이는 대상 창이 부모 창의 메시지 처리 메커니즘을 따르도록 하여 메시지 가로채기를 우회할 수 있습니다.

전원 옵션

강제 종료/재시작

시스템을 강제로 종료하거나 재시작하기 위해 ZTool은 먼저 ExitWindowsEx API를 사용합니다. 200ms 후에도 시스템이 여전히 실행 중이면, NtInitiatePowerAction을 사용하여 강제 전원 동작을 수행합니다. 최종적으로 ZTool이 직접 시스템 종료 권한을 얻지 못할 경우, 접근 가능한 모든 프로세스에 원격 스레드를 주입하여 해당 프로세스 컨텍스트 내에서 종료/재시작 명령을 실행하도록 합니다.

// 특정 프로세스에 종료 동작을 주입하는 함수
void InjectPowerAction(HANDLE processHandle, SHUTDOWN_ACTION actionType) {
    if (processHandle == NULL || processHandle == INVALID_HANDLE_VALUE) return;

    HANDLE tokenHandle;
    // 프로세스 토큰을 열어 권한을 쿼리하고 조정합니다.
    OpenProcessToken(processHandle, TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &tokenHandle);

    TOKEN_PRIVILEGES tokenPrivileges;
    // SE_SHUTDOWN_NAME 권한의 LUID를 조회합니다.
    LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &tokenPrivileges.Privileges[0].Luid);
    tokenPrivileges.PrivilegeCount = 1;
    tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // 권한 활성화

    // 토큰 권한을 조정하여 종료 권한을 얻습니다.
    AdjustTokenPrivileges(tokenHandle, FALSE, &tokenPrivileges, 0, NULL, 0);
    CloseHandle(tokenHandle);

    // 원격 스레드를 생성하여 대상 프로세스 내에서 NtShutdownSystem 호출
    CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)NtShutdownSystem, (LPVOID)actionType, 0, NULL);
}

// 시스템 전체에 걸쳐 전원 동작을 수행하는 함수 (모든 프로세스에 주입 시도)
void PerformSystemWidePowerAction(SHUTDOWN_ACTION actionType) {
    NtShutdownSystem(actionType); // 우선 직접 종료 시도

    HANDLE remoteProcessHandle = NULL;
    PROCESSENTRY32 processEntry = {sizeof(PROCESSENTRY32)};
    HANDLE snapshotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    // 현재 실행 중인 모든 프로세스를 순회합니다.
    if (Process32First(snapshotHandle, &processEntry)) {
        do {
            if (processEntry.th32ProcessID == GetCurrentProcessId()) continue; // ZTool 자신은 제외

            // PROCESS_ALL_ACCESS 권한으로 프로세스를 엽니다.
            remoteProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, processEntry.th32ProcessID);
            if (remoteProcessHandle) {
                InjectPowerAction(remoteProcessHandle, actionType); // 종료 동작 주입
                CloseHandle(remoteProcessHandle);
            }
        } while (Process32Next(snapshotHandle, &processEntry));
    }
    CloseHandle(snapshotHandle);

    NtShutdownSystem(actionType); // 다시 직접 종료 시도 (혹시라도 실패했을 경우)
}

// 강제 재시작 함수
void InitiateForcedReboot() {
    ExitWindowsEx(EWX_REBOOT | EWX_FORCE | EWX_FORCEIFHUNG, 0); // 표준 API를 통한 재시작
    Sleep(200);
    NtInitiatePowerAction(PowerActionShutdownReset, PowerSystemShutdown, 0, TRUE); // NT API를 통한 강제 재시작
    PerformSystemWidePowerAction(ShutdownReboot); // 시스템 전체에 걸쳐 재시작 주입
}

// 강제 종료 함수
void InitiateForcedShutdown() {
    ExitWindowsEx(EWX_SHUTDOWN | EWX_FORCE | EWX_FORCEIFHUNG | EWX_POWEROFF, 0); // 표준 API를 통한 종료
    Sleep(200);
    NtInitiatePowerAction(PowerActionShutdownOff, PowerSystemShutdown, 0, TRUE); // NT API를 통한 강제 종료
    PerformSystemWidePowerAction(ShutdownPowerOff); // 시스템 전체에 걸쳐 종료 주입
}

강제 로그오프

사용자를 강제로 로그오프시키기 위해 WTSLogoffSessionExitWindowsEx API를 함께 사용합니다.

고급 시작 옵션 메뉴 진입

Windows API로는 직접 지원되지 않으므로, 커맨드 라인 명령어인 shutdown -r -o -t 0을 사용하여 고급 시작 옵션 메뉴로 시스템을 재시작합니다.

블루스크린 트리거

시스템 블루스크린(BSOD)을 강제로 트리거하기 위해 NtRaiseHardError API를 사용합니다. 이 함수는 심각한 시스템 오류를 발생시켜 시스템이 중지되도록 합니다.

// 시스템 블루스크린을 트리거하는 함수
void TriggerSystemCrash() {
    ULONG hardErrorStatus;
    // NtRaiseHardError를 호출하여 치명적인 오류를 발생시킵니다.
    // 0xc0000000은 STATUS_UNSUCCESSFUL을 나타내는 일반적인 오류 코드입니다.
    NtRaiseHardError(0xc0000000, 0, 0, 0, 6, &hardErrorStatus);
    ExitProcess(0); // 오류 발생 후 프로세스 종료
}

자체 보호 및 실시간 감시

다른 애플리케이션으로부터 프로세스 보호

ZTool 프로세스 자체를 다른 애플리케이션 계층 프로세스로부터 보호하기 위해 SetSecurityInfo API를 사용하여 프로세스의 DACL(Discretionary Access Control List)을 수정합니다. 이를 통해 ZTool 프로세스는 보호된 상태가 되며, 디버그 권한을 가진 프로세스나 커널 계층에서만 접근할 수 있게 됩니다. 이 기능은 창 메시지 가로채기 및 아래에서 설명할 권한 박탈 기능과 결합하여 강력한 자체 보호를 제공합니다.

// ZTool 프로세스를 다른 애플리케이션 계층 프로세스로부터 보호하는 함수
BOOL ConfigureProcessProtection(HANDLE protectedProcessHandle) {
    PACL securityDacl;
    PTOKEN_USER currentUserTokenInfo;
    SID_IDENTIFIER_AUTHORITY worldSidAuthority = SECURITY_WORLD_SID_AUTHORITY;
    PSID everyoneSid;
    BOOL successStatus = FALSE;

    // 'Everyone' SID를 할당하고 초기화합니다.
    successStatus = AllocateAndInitializeSid(&worldSidAuthority, 1, 0, 0, 0, 0, 0, 0, 0, 0, &everyoneSid);
    if (!successStatus) goto Cleanup;

    HANDLE tokenHandle;
    // 보호할 프로세스의 토큰을 쿼리 권한으로 엽니다.
    successStatus = OpenProcessToken(protectedProcessHandle, TOKEN_QUERY, &tokenHandle);
    if (!successStatus) goto Cleanup;

    DWORD returnLength;
    // 토큰 정보를 가져와 사용자 정보를 위한 버퍼 크기를 결정합니다.
    GetTokenInformation(tokenHandle, TokenUser, NULL, 0, &returnLength);
    if (returnLength == 0 || returnLength > 0x400) goto Cleanup; // 버퍼 크기 검증

    LPVOID tokenInformationBuffer = LocalAlloc(LPTR, returnLength);
    if (!tokenInformationBuffer) goto Cleanup;

    // 토큰 사용자 정보를 가져옵니다.
    successStatus = GetTokenInformation(tokenHandle, TokenUser, tokenInformationBuffer, returnLength, &returnLength);
    if (!successStatus) {
        LocalFree(tokenInformationBuffer);
        goto Cleanup;
    }
    currentUserTokenInfo = (PTOKEN_USER)tokenInformationBuffer;

    BYTE aclBuffer[0x200]; // ACL을 위한 버퍼
    securityDacl = (PACL)&aclBuffer;
    // DACL을 초기화합니다.
    successStatus = InitializeAcl(securityDacl, sizeof(aclBuffer), ACL_REVISION);
    if (!successStatus) {
        LocalFree(tokenInformationBuffer);
        goto Cleanup;
    }

    // 'Everyone' 그룹에 대해 모든 접근(0xFFFFFFFF)을 거부하는 ACE를 추가합니다.
    successStatus = AddAccessDeniedAce(securityDacl, ACL_REVISION, 0xFFFFFFFF, everyoneSid);
    if (!successStatus) {
        LocalFree(tokenInformationBuffer);
        goto Cleanup;
    }

    // 현재 사용자에게는 특정 접근(0x00100701)을 허용하는 ACE를 추가합니다.
    successStatus = AddAccessAllowedAce(securityDacl, ACL_REVISION, 0x00100701, currentUserTokenInfo->User.Sid);
    if (!successStatus) {
        LocalFree(tokenInformationBuffer);
        goto Cleanup;
    }

    // 프로세스의 보안 정보를 설정하여 DACL을 적용하고 보호된 DACL 플래그를 설정합니다.
    if (SetSecurityInfo(protectedProcessHandle, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, NULL, NULL, securityDacl, NULL) == ERROR_SUCCESS) {
        successStatus = TRUE;
    }

Cleanup:
    if (tokenHandle != NULL) CloseHandle(tokenHandle);
    if (protectedProcessHandle != NULL) CloseHandle(protectedProcessHandle);
    if (everyoneSid != NULL) FreeSid(everyoneSid);
    if (tokenInformationBuffer != NULL) LocalFree(tokenInformationBuffer);
    return successStatus;
}

이러한 보호는 대부분의 애플리케이션 계층 프로세스에 효과적이지만, EndTask API는 권한 제한을 무시하고 프로세스를 종료할 수 있으므로, EndTask를 통한 종료는 막을 수 없습니다. EndTask를 방어하려면 csrss.exe에 대한 커널 훅이 필요하며, 이는 애플리케이션 계층의 범위를 벗어납니다.

비시스템 프로세스의 특정 권한 박탈

ZTool은 시스템 프로세스를 제외한 다른 모든 프로세스의 특정 권한(예: 디버깅 권한, 시스템 종료 권한, 원격 종료 권한)을 박탈합니다. 이는 AdjustTokenPrivileges API를 사용하여 각 프로세스의 토큰에서 해당 권한을 제거함으로써 이루어집니다. 참고: ZTool 버전 1.0.3부터는 드라이버 로드 특권(SE_LOAD_DRIVER_NAME)을 박탈하지 않아, 다른 드라이버 기반 도구와 함께 사용할 수 있도록 변경되었습니다.

// 특정 프로세스로부터 지정된 권한을 박탈하는 함수
void RevokeSpecificPrivilege(HANDLE targetProcessHandle, LPCSTR privilegeName) {
    if (targetProcessHandle == NULL || targetProcessHandle == INVALID_HANDLE_VALUE) return;

    HANDLE tokenHandle;
    // 프로세스 토큰을 열어 모든 권한을 얻습니다.
    if (!OpenProcessToken(targetProcessHandle, TOKEN_ALL_ACCESS, &tokenHandle)) {
        return;
    }

    TOKEN_PRIVILEGES tokenPrivileges;
    // 박탈할 권한의 LUID를 조회합니다.
    if (!LookupPrivilegeValue(NULL, privilegeName, &tokenPrivileges.Privileges[0].Luid)) {
        CloseHandle(tokenHandle);
        return;
    }
    
    tokenPrivileges.PrivilegeCount = 1;
    tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_REMOVED; // 권한을 제거하도록 설정

    // 토큰 권한을 조정하여 지정된 권한을 제거합니다.
    AdjustTokenPrivileges(tokenHandle, FALSE, &tokenPrivileges, 0, NULL, 0);
    CloseHandle(tokenHandle);
}

// 비시스템 프로세스의 특정 권한을 지속적으로 감시하고 박탈하는 함수
void ContinuouslyRevokePrivileges() {
    while (TRUE) {
        // ZTool 프로세스의 작업 세트 크기를 최적화
        SetProcessWorkingSetSize(GetCurrentProcess(), (SIZE_T)-1, (SIZE_T)-1); 

        PROCESSENTRY32 processEntry = {sizeof(PROCESSENTRY32)};
        HANDLE snapshotHandle = CreateTooltool32Snapshot(TH32CS_SNAPPROCESS, 0);

        if (Process32First(snapshotHandle, &processEntry)) {
            do {
                // (가상의 IsSystemProcess 함수가 필요)
                // 현재 프로세스가 시스템 프로세스가 아닌 경우
                if (!IsSystemProcess(processEntry.szExeFile, processEntry.th32ProcessID)) {
                    HANDLE procHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, processEntry.th32ProcessID);
                    if (procHandle) {
                        RevokeSpecificPrivilege(procHandle, SE_DEBUG_NAME);         // 디버그 권한 박탈
                        // RevokeSpecificPrivilege(procHandle, SE_LOAD_DRIVER_NAME); // 드라이버 로드 권한 (현재는 박탈 안 함)
                        RevokeSpecificPrivilege(procHandle, SE_REMOTE_SHUTDOWN_NAME); // 원격 종료 권한 박탈
                        RevokeSpecificPrivilege(procHandle, SE_SHUTDOWN_NAME);       // 종료 권한 박탈
                        CloseHandle(procHandle);
                    }
                }
            } while (Process32Next(snapshotHandle, &processEntry));
        }
        CloseHandle(snapshotHandle);
        Sleep(10); // 짧은 대기 후 반복
    }
}

기타 기능

시스템 권한의 작업 관리자 및 명령 프롬프트 실행

ZTool이 System 권한으로 실행될 경우, 레지스트리 설정에 의해 "작업 관리자/명령 프롬프트 사용 금지" 설정이 되어 있어도 이를 무시하고 ShellExecute를 통해 작업 관리자나 명령 프롬프트를 정상적으로 실행할 수 있습니다.

2048 미니 게임

사용자가 잠시 휴식을 취하거나 다른 활동을 위장할 수 있도록 고전적인 '2048' 게임이 내장되어 있습니다.

System 권한 및 UIAccess 획득

ZTool은 실행 시 자동으로 권한 상승을 시도합니다. 이 과정은 크게 세 단계로 나뉩니다:

  1. UAC 관리자 권한 확인 및 재시작: ZTool은 먼저 현재 관리자 권한으로 실행되고 있는지 확인합니다. 그렇지 않다면, 사용자에게 UAC(사용자 계정 컨트롤) 프롬프트를 통해 관리자 권한으로 재시작할 것을 요청합니다.
  2. System 권한 획득: 관리자 권한을 얻은 후, ZTool은 lsass.exe 또는 winlogon.exe 프로세스의 토큰을 복제합니다. 이 토큰을 사용하여 ZTool 자신을 다시 시작함으로써, System 권한으로 실행됩니다.
  3. UIAccess 권한 획득 (슈퍼 최상위): System 권한을 얻은 후, ZTool은 smss.exe 또는 winlogon.exe 프로세스의 토큰을 복제하고, 이 토큰의 TokenUIAccess 속성을 TRUE로 변경합니다. 이 수정된 토큰으로 ZTool을 다시 시작하면, ZTool 창은 UIAccess 권한을 획득하여 작업 관리자, 돋보기, 입력기, 시작 메뉴 등 대부분의 시스템 창 위에 '슈퍼 최상위'로 표시될 수 있습니다.

이러한 권한 상승 과정은 보안 소프트웨어에 의해 차단될 수 있으며, 디버그 권한이 필요합니다. 따라서 두 개의 System 권한을 가진 ZTool 인스턴스를 동시에 실행하는 것은 이론적으로 불가능합니다.

// System 권한 및 UIAccess 권한 상승을 시도하는 함수
void ElevateAndAcquireUIAccess() {
    SID_IDENTIFIER_AUTHORITY ntAuthority = SECURITY_NT_AUTHORITY;
    PSID administratorsGroupSid;
    BOOL isAdmin = FALSE;

    // 관리자 그룹 SID를 할당하고 초기화합니다.
    if (AllocateAndInitializeSid(&ntAuthority, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &administratorsGroupSid)) {
        // 현재 토큰이 관리자 그룹의 멤버인지 확인합니다.
        CheckTokenMembership(NULL, administratorsGroupSid, &isAdmin);
        FreeSid(administratorsGroupSid);
    }

    std::string commandLineStr = GetCommandLine(); // 현재 커맨드 라인 가져오기

    // 1단계: UAC 관리자 권한 상승
    if (!isAdmin) {
        // 커맨드 라인에 'A'가 없으면 (이미 관리자 권한으로 재시작된 경우가 아님)
        if (commandLineStr.empty() || commandLineStr.back() != 'A') {
            MessageBox(GetConsoleWindow(), L"일부 기능은 관리자 권한이 필요합니다. '예'를 클릭하여 관리자 권한으로 프로그램을 재실행하거나, '아니오'를 클릭하여 제한된 권한으로 계속합니다.", L"권한 상승 필요", MB_ICONASTERISK | MB_YESNO);
            if (IDYES == MessageBox(GetConsoleWindow(), L"관리자 권한으로 다시 실행하시겠습니까?", L"확인", MB_YESNO)) {
                TCHAR currentPath[MAX_PATH];
                GetModuleFileName(NULL, currentPath, MAX_PATH);
                ShellExecute(NULL, L"runas", currentPath, L"", NULL, SW_SHOWDEFAULT);
                ExitProcess(0); // 현재 프로세스 종료
            }
        }
        // 관리자 권한이 아니거나 사용자가 거부한 경우, 가능한 모든 권한을 조정합니다.
        // for (int i = 1; i <= 0x100; i++) AdjustPrivilege(i, TRUE, FALSE, &isAdmin); // AdjustPrivilege는 가상 함수
        return;
    }

    // 관리자 권한 획득 후, 필요한 모든 권한을 활성화합니다.
    // for (int i = 1; i <= 0x100; i++) AdjustPrivilege(i, TRUE, FALSE, &isAdmin); // AdjustPrivilege는 가상 함수

    // 3단계: UIAccess 권한 획득 (두 번째 재시작)
    if (commandLineStr.back() == 'S') { // 커맨드 라인에 'S'가 있으면 System 권한으로 이미 시작됨
        MessageBox(GetConsoleWindow(), L"UIAccess 권한 획득을 위한 2차 초기화...", L"정보", MB_ICONINFORMATION);
        DWORD smssPid = 0, winlogonPid = 0;
        PROCESSENTRY32 pe;
        pe.dwSize = sizeof(PROCESSENTRY32);
        HANDLE processSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
        if (processSnapshot && Process32First(processSnapshot, &pe)) {
            do {
                if (_stricmp(pe.szExeFile, "smss.exe") == 0) smssPid = pe.th32ProcessID;
                else if (_stricmp(pe.szExeFile, "winlogon.exe") == 0) winlogonPid = pe.th32ProcessID;
            } while (Process32Next(processSnapshot, &pe));
        }
        CloseHandle(processSnapshot);

        HANDLE sourceProcessHandle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, smssPid);
        if (!sourceProcessHandle) sourceProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, winlogonPid); // smss.exe 접근 실패 시 winlogon.exe 시도

        HANDLE duplicateToken, primaryToken;
        OpenProcessToken(sourceProcessHandle, TOKEN_DUPLICATE, &duplicateToken);
        DuplicateTokenEx(duplicateToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &primaryToken);
        CloseHandle(sourceProcessHandle);
        CloseHandle(duplicateToken);

        // UIAccess 플래그를 TRUE로 설정
        BOOL fUIAccess = TRUE;
        SetTokenInformation(primaryToken, TokenUIAccess, &fUIAccess, sizeof(fUIAccess));

        STARTUPINFOW startupInfo = {};
        startupInfo.cb = sizeof(startupInfo);
        startupInfo.lpDesktop = L"winsta0\\default"; // 기본 데스크톱 사용
        PROCESS_INFORMATION processInfo = {};
        
        // ZTool을 UIAccess 권한으로 재시작 (커맨드 라인에 'A' 추가)
        wchar_t newCommandLine[MAX_PATH];
        lstrcpyW(newCommandLine, GetCommandLineW());
        lstrcatW(newCommandLine, L" A");

        CreateProcessWithTokenW(primaryToken, LOGON_NETCREDENTIALS_ONLY, NULL, newCommandLine, NORMAL_PRIORITY_CLASS | CREATE_NEW_PROCESS_GROUP, NULL, NULL, &startupInfo, &processInfo);
        CloseHandle(primaryToken);
        ExitProcess(0); // 현재 프로세스 종료
    }

    // 2단계: System 권한 획득 (첫 번째 재시작)
    if (commandLineStr.back() != 'A') { // 커맨드 라인에 'A'가 없으면 (아직 UIAccess로 시작되지 않음)
        MessageBox(GetConsoleWindow(), L"SYSTEM 권한으로 상승하시겠습니까? (일부 백신에 의해 차단될 수 있습니다)", L"권한 상승 확인", MB_ICONASTERISK | MB_YESNO);
        if (IDNO == MessageBox(GetConsoleWindow(), L"SYSTEM 권한 상승을 하시겠습니까?", L"확인", MB_YESNO)) {
            // 사용자가 거부한 경우, 가능한 모든 권한을 조정합니다.
            // for (int i = 1; i <= 0x100; i++) AdjustPrivilege(i, TRUE, FALSE, &isAdmin); // AdjustPrivilege는 가상 함수
            return;
        }

        MessageBox(GetConsoleWindow(), L"SYSTEM 권한 획득을 위한 1차 초기화...", L"정보", MB_ICONINFORMATION);
        DWORD lsassPid = 0, winlogonPid = 0;
        PROCESSENTRY32 pe;
        pe.dwSize = sizeof(PROCESSENTRY32);
        HANDLE processSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
        if (processSnapshot && Process32First(processSnapshot, &pe)) {
            do {
                if (_stricmp(pe.szExeFile, "lsass.exe") == 0) lsassPid = pe.th32ProcessID;
                else if (_stricmp(pe.szExeFile, "winlogon.exe") == 0) winlogonPid = pe.th32ProcessID;
            } while (Process32Next(processSnapshot, &pe));
        }
        CloseHandle(processSnapshot);

        HANDLE sourceProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, lsassPid);
        if (!sourceProcessHandle) sourceProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, winlogonPid); // lsass.exe 접근 실패 시 winlogon.exe 시도

        HANDLE duplicateToken, primaryToken;
        OpenProcessToken(sourceProcessHandle, TOKEN_DUPLICATE, &duplicateToken);
        DuplicateTokenEx(duplicateToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &primaryToken);
        CloseHandle(sourceProcessHandle);
        CloseHandle(duplicateToken);

        STARTUPINFOW startupInfo = {};
        startupInfo.cb = sizeof(startupInfo);
        startupInfo.lpDesktop = L"winsta0\\default"; // 기본 데스크톱 사용
        PROCESS_INFORMATION processInfo = {};

        // ZTool을 System 권한으로 재시작 (커맨드 라인에 'S' 추가)
        wchar_t newCommandLine[MAX_PATH];
        lstrcpyW(newCommandLine, GetCommandLineW());
        lstrcatW(newCommandLine, L" S");

        CreateProcessWithTokenW(primaryToken, LOGON_NETCREDENTIALS_ONLY, NULL, newCommandLine, NORMAL_PRIORITY_CLASS, NULL, NULL, &startupInfo, &processInfo);
        CloseHandle(primaryToken);
        ExitProcess(0); // 현재 프로세스 종료
    }
}

태그: Windows API System Internals Privilege Escalation Process Control Window Management

6월 16일 20:14에 게시됨