q2e74, Прот, который на всех играх был взломан, еще и задним числом; не зири, но всеже.. через паблик гипердебаг.
Код (Text): Вот что нашёл в таргете (Windows 11 ARM64, Build 26100). Полная картина детекции гипервизора: Детекция на ARM64 — совсем другой мир На x86 (из треда) Windows 8 использовала rdtsc + idiv timing heuristics. На ARM64 Windows 11 — ни одного timing-замера. Всё через регистры. nt!HalpDetectHypervisor — первичная детекция mrs x8, ID_AA64PFR0_EL1 ; читать Feature Register 0 tst x8, #0xF00 ; проверить bits[11:8] = EL2 support beq no_hypervisor ; EL2 не реализован → нет гипервизора ldr x10, [global_vendor_id] ; 0x534c5250 = "PRLS" (Parallels!) ldr x11, =0x4d4f4351 ; "QCOM" (Qualcomm) — референсное значение cmp x10, x11 csel x9, xzr, x10, eq ; если QCOM → x9=0, иначе x9=vendor Логика: Если vendor == "QCOM" — это нативный ARM платформенный гипервизор Qualcomm, игнорируем. Если любой другой — гипервизор обнаружен. Текущее состояние на таргете ┌────────────────────────────┬────────────────────┬───────────────────────────┐ │ Переменная │ Значение │ Расшифровка │ ├────────────────────────────┼────────────────────┼───────────────────────────┤ │ Vendor ID │ 0x534c5250 │ "PRLS" = Parallels │ ├────────────────────────────┼────────────────────┼───────────────────────────┤ │ Сравнение 1 │ 0x4d4f4351 │ "QCOM" — нативный │ │ │ │ Qualcomm │ ├────────────────────────────┼────────────────────┼───────────────────────────┤ │ Сравнение 2 │ 0x567265707948734d │ "MsHypreV" — Microsoft │ │ │ │ Hyper-V │ ├────────────────────────────┼────────────────────┼───────────────────────────┤ │ HalpDetectHypervisor │ 0x00 │ Не обнаружен (EL2 скрыт │ │ result │ │ от guest) │ ├────────────────────────────┼────────────────────┼───────────────────────────┤ │ HvlHypervisorPresent flag │ 0x00 │ Не подключён │ ├────────────────────────────┼────────────────────┼───────────────────────────┤ │ HviIsAnyHypervisorPresent │ PRLS ≠ QCOM → TRUE │ Обнаружен через Vendor ID │ └────────────────────────────┴────────────────────┴───────────────────────────┘ Парадокс HalpDetectHypervisor говорит НЕТ — потому что Parallels скрывает EL2 (bits[11:8] в ID_AA64PFR0_EL1 = 0 для guest). Но vendor ID глобально = "PRLS", и HviIsAnyHypervisorPresent видит, что PRLS ≠ QCOM → ДА. Механизм на ARM64 (вместо CPUID timing) x86 (Windows 8): ARM64 (Windows 11): cpuid leaf 0x40000000 mrs ID_AA64PFR0_EL1 (check EL2) rdtsc timing variance Vendor ID comparison (PRLS vs QCOM) idiv jitter heuristics HvcallGetVpRegister (HVC calls) CPUID bit 31 (hypervisor bit) System register reads HviGetHypervisorInterface и HviGetHypervisorFeatures используют HvcallGetVpRegister — это HVC инструкция (ARM64 Hypervisor Call), аналог VMCALL на x86. Регистры: - Interface: 0x00090006 (ARM64 VP register) - Features: 0x000200 (feature flags register) Ответ на тред Тайминг-код из Windows 8 (INIT:00910EDC) с rdtsc+idiv+variance — вырезан из современных билдов. На ARM64 его никогда не было. Вместо него — чисто архитектурная детекция через system registers. Timing-based подход不稳定 (как Ahimov и обнаружил — "на онлайн виртах анстаб и рандом"), и Microsoft от него отказалась в пользу deterministic register checks. Код (Text): ⏺ Конкретные команды: x nt!*Hypervisor* — найти все символы с Hypervisor uf nt!HalpDetectHypervisor — полная дизассемблика первичной детекции uf nt!HviIsAnyHypervisorPresent — проверка "любой гипервизор" uf nt!HviGetHypervisorInterface — получение интерфейса uf nt!HviGetHypervisorFeatures — получение фич uf nt!HvlIsHypervisorPresent — кэшированный флаг dq fffff801e06880f0 L2 — embedded константы ("QCOM", "MsHypreV") dq fffff801e0e107d8 L1 — глобальный Vendor ID → "PRLS" dq fffff801e05807e8 L1 — константа в HviGetHypervisorInterface dq fffff801e05807f0 L1 — номер VP регистра (0x00090006) dq fffff801e07c6ef0 L1 — константа "QCOM" в HviIsAnyHypervisorPresent dq fffff801e07c6ec8 L1 — константа "MsHypreV" в HviGetHypervisorFeatures db fffff801e1009A87 L1 — HvlIsHypervisorPresent флаг (0x00) db fffff801e0e10500 L8 — структура HalpDetectHypervisor: [+0]=result, [+1]=done, [+2]=detected Vendor ID "PRLS" раскодирован из 0x534c5250 побайтово: 0x50='P', 0x52='R', 0x4c='L', 0x53='S'. либо кто хочет Код (Text): ⏺ Вот практические методы через WinDbg для тайминг-детекции: Метод 1: Прочитать ARM64 счётчики напрямую На ARM64 аналог RDTSC — CNTVCT_EL0 (virtual) и CNTPCT_EL0 (physical). Гипервизор может перехватывать CNTPCT_EL0 — разница во времени чтения выдаст его. ; Прочитать виртуальный счётчик (быстро, не трапает) mrs x0, CNTVCT_EL0 ; Прочитать физический счётчик (может вызывать trap → VM-exit → медленно) mrs x0, CNTPCT_EL0 Через WinDbg нельзя напрямую выполнить mrs, но можно собрать stub: ; Выделить память и записать ARM64 код .pool ; mrs x0, CNTPCT_EL0 → d53be080 ; mrs x1, CNTVCT_EL0 → d53be020 ; str x0, [x2] → f9000040 ; str x1, [x2,#8] → f9000441 ; ret → d65f03c0 Метод 2: Вызвать KeQueryPerformanceCounter дважды ; Первый замер .call nt!KeQueryPerformanceCounter(0) r @$t0 = rax ; <подождать секунду> ; Второй замер .call nt!KeQueryPerformanceCounter(0) r @$t1 = rax ; Разница ? @$t1 - @$t0 Частота = 24 MHz (0x16e3600). За 1 секунду должно быть ~24,000,000 тиков. Если дельта другая — время искажается. Метод 3: Проверить TSC offset через гипервизор uf nt!HvlQueryHypervisorTscAdjustment В нашем таргете = mov x0, #0; ret — возвращает 0, значит offset не применяется (или гипервизор не подключён на уровне Hvl). Метод 4: Прочитать KUSER_SHARED_DATA и сравнить ; Прочитать InterruptTime и TickCount dq SharedUserData+0x8 L4 ; InterruptTime dq SharedUserData+0x318 L2 ; TickCount (32-bit) ; Подождать g ; запустить на секунду <Ctrl+Break> ; снова остановить dq SharedUserData+0x8 L4 ; новое InterruptTime ; Сравнить дельту с ожидаемой Метод 5: Сравнить частоту с реальным железом ; Частота generic timer ? nt!KdPerformanceCounterRate ; = 0x16e3600 = 24,000,000 = 24 MHz ; На реальном Apple Silicon M1/M2 частота = 24 MHz ; Если Parallels не подделывает → совпадает ; Если подделывает → будет другая Метод 6: Ключевой — CNTPCT vs CNTVCT timing Это ARM64 аналог rdtsc vs cpuid из треда: ; Написать тестовый stub в пул .pool ; Код: ; isb ; serialization barrier ; mrs x0, CNTPCT_EL0 ; physical — МОЖЕТ трапать в EL2 ; isb ; mrs x1, CNTVCT_EL0 ; virtual — обычно НЕ трапает ; sub x0, x0, x1 ; разница = offset ; ret Если CNTPCT_EL0 трапает в EL2 (Parallels перехватывает) — чтение займёт на порядки больше тиков, чем CNTVCT_EL0. Замеряешь это через KeQueryPerformanceCounter до и после каждого mrs. Суть (как на x86 из треда) ┌────────────────────────┬──────────────────────────────┬─────────────────────┐ │ x86 │ ARM64 │ Что детектит │ ├────────────────────────┼──────────────────────────────┼─────────────────────┤ │ rdtsc │ CNTVCT_EL0 │ Не трапает, быстрый │ ├────────────────────────┼──────────────────────────────┼─────────────────────┤ │ cpuid (serializing) │ CNTPCT_EL0 + ISB │ Трапает в EL2 = │ │ │ │ медленно │ ├────────────────────────┼──────────────────────────────┼─────────────────────┤ │ rdtsc:cpuid ratio ~2:1 │ CNTVCT:CNTVCT ratio 1:1 host │ Норма │ │ host │ │ │ ├────────────────────────┼──────────────────────────────┼─────────────────────┤ │ rdtsc:cpuid ratio │ Чтение CNTPCT в 50-100x │ Гипервизор │ │ ~95:1 VM │ медленнее VM │ │ └────────────────────────┴──────────────────────────────┴─────────────────────┘ Идея та же: сравнить тайминг инструкции которая не вызывает VM-exit с инструкцией которая вызывает. На железе разница минимальна, в VM — огромная. Код (Text): Результат чека: обнаружение гипервизора на ARM64 Уровень 1: HAL — HalpDetectHypervisor Результат: НЕ обнаружен (ложноотрицательный) Логика функции (/tmp/halp_detect.json): mrs x8, ID_AA64PFR0_EL1 ; читаем CPU features tst x8, #0xF00 ; проверяем биты [11:8] — EL2 support beq no_hypervisor ; если 0 → нет EL2 → нет гипервизора Значение в памяти структуры (/tmp/hv_flags.json): fffff801`e0e10500: 00 01 00 01 00 02 00 00 [+0]=0x00 → result=FALSE [+1]=0x01 → done=TRUE [+2]=0x00 → detected=FALSE Почему: Parallels скрывает EL2 от гостя. Поле ID_AA64PFR0_EL1[11:8] = 0. HAL думает, что голое железо. --- Уровень 2: HVL — HvlIsHypervisorPresent Результат: НЕ обнаружен ldrb w8, [x8, #0xA87] ; читает флаг cmp w8, #0 csetne w0 ; флаг = 0x00 → FALSE Флаг по адресу fffff801e1009a87 = 0x00. Слой HVL не подключён — гипервизор Parallels не использует Windows Hypervisor Platform (не Hyper-V). --- Уровень 3: HVI — HviIsAnyHypervisorPresent Результат: ОБНАРУЖЕН (единственный рабочий метод) ldr x8, [x8, #0x7D8] ; vendor ID = 0x534c5250 = "PRLS" ldr x9, =0x4d4f4351 ; "QCOM" (native) cmp x8, x9 cseleq x8, xzr, x8 ; если QCOM → 0, иначе → vendor value cmp x8, #0 csetne w0 ; PRLS ≠ QCOM → TRUE Глобальный Vendor ID по адресу fffff801e0e107d8 = 0x534c5250 = "PRLS" (Parallels). --- Сводная таблица ┌───────────────────────────┬───────────┬──────────────────────────────────────┐ │ Метод │ Результат │ Почему │ ├───────────────────────────┼───────────┼──────────────────────────────────────┤ │ HalpDetectHypervisor │ FALSE │ EL2 скрыт, ID_AA64PFR0_EL1[11:8]=0 │ ├───────────────────────────┼───────────┼──────────────────────────────────────┤ │ HvlIsHypervisorPresent │ FALSE │ Флаг HVL=0, Parallels ≠ Hyper-V │ ├───────────────────────────┼───────────┼──────────────────────────────────────┤ │ HviIsAnyHypervisorPresent │ TRUE │ Vendor "PRLS" ≠ "QCOM" │ ├───────────────────────────┼───────────┼──────────────────────────────────────┤ │ HviGetHypervisorInterface │ Работает │ Вызывает │ │ │ │ HvcallGetVpRegister(0x00090006) │ ├───────────────────────────┼───────────┼──────────────────────────────────────┤ │ HviGetHypervisorFeatures │ Работает │ Вызывает HvcallGetVpRegister(0x200) │ ├───────────────────────────┼───────────┼──────────────────────────────────────┤ │ HvlQueryTscAdjustment │ 0 (stub) │ mov x0, #0; ret — нет TSC-сдвига │ └───────────────────────────┴───────────┴──────────────────────────────────────┘ --- Вывод по таймингу На x86 классический метод — rdtsc vs cpuid timing ratio. На ARM64 аналог: 1. CNTVCT_EL0 (виртуальный счётчик) — читается без трапа в EL2, быстрый 2. CNTPCT_EL0 (физический счётчик) — может трапить в EL2, если гипервизор это настроил На таргете KeQueryPerformanceCounter работает через generic timer на 24 МГц (KdPerformanceCounterRate = 0x16e3600). Есть отдельные shift-значения для гипервизорного случая: - PpmPerformanceCounterShift = 0x0a00000005 - PpmHvPerformanceCounterShift = 0x0900000004 Это значит, что Windows ожидает разную скорость счётчика для голого железа vs гипервизора. Parallels настроил проход счётчика без трапов (иначе таймеры ядра плыли бы), поэтому чистый тайминг между CNTVCT и CNTPCT не даст заметной разницы. Рабочий вектор обнаружения для anti-VM на этом таргете: не тайминг, а прямая проверка vendor ID через HviIsAnyHypervisorPresent — она однозначно возвращает TRUE для PRLS.