Скрытый потенциал регистров отладки dr0-dr7

Tartuga

Бывалый
ПРОВЕРЕННЫЙ ПРОДАВЕЦ
PREMIUM USER

Tartuga

Бывалый
ПРОВЕРЕННЫЙ ПРОДАВЕЦ
PREMIUM USER
Регистрация
7 Фев 2020
Сообщения
525
Реакции
98
Репутация
147
Слово отладка, у большинства из нас ассоциируется с инструментальными средствами реверса типа WinDbg, x64Dbg, OllyDbg и прочие. Но в данной статье хотелось-бы рассмотреть это направление с другой проекции, а именно – использование отладочных возможностей CPU вне отладчика, непосредственно из пользовательского приложения. Исследования в этом ключе раскрывают огромный потенциал и что немаловажно, все действия остаются прозрачными для самих отладчиков, т.к. мы отнимаем у них ресурсы по типу "кто первым встал, того и тапки".

Формат регистров DR0-DR7 процессора



Процессоры х86 имеют восемь регистров отладки DR0-DR7, два из которых DR4.5 отправлены в резерв и не используются. В совокупности, отслеживая обращения к памяти они позволяют устанавливать аппаратные точки-останова на её чтение, запись, или исполнение. Более того, если в регистре-управления процессором CR4 сброшен бит[3] (Debug Extensions), то под надзор силовых структур попадают и обращения к портам ввода-вывода I/O. Взяв за основу фишку с отслеживанием физических портов, можно написать к примеру эмулятор какого-нибудь внешнего девайса, типа флопика или LPT/COM устройства.



К сожалению, под адреса Breakpoint'ов (далее HBP, Hardware-Breakpoint), инженеры выделили всего 4-регистра с номерами DR0-DR3. Соответственно мы можем установить столько-же аппаратных бряков за один раз. Если нужно больше.., придётся переназначать их динамически, после того-как отработает первая партия. На рисунке ниже, для лучшего восприятия принадлежность битов к каждой из четырёх точек выделены своим цветом. Наиболее информативным является регистр-конфигурации DR7, в котором программист должен определить характер Breakpoint 'ов – рассмотрим его формат подробней..



• В 2-битном поле LEN мы указываем размер ячейки памяти, при обращении к которой сработает бряк. Если HBP ставится на исполнение инструкции по определённому адресу, это поле игнорируется и обязательно должно быть сброшено в нуль (что равносильно размеру в 1-байт). Во-всех остальных случаях – как говорится "размер имеет значение".



• Следующее 2-битное поле RW определяет условие, на которое должен отреагировать проц исключением #DB (код = 0x80000004). Инженеры представили нам на выбор 4 варианта: исполнение адреса, запись в него, обращение к портам I/O, и чтение адреса. В этой статье я буду ставить бряки только на исполнение инструкций в секции-кода, поэтому старшая половина регистра DR7 с битами Len и RW всегда будет сброшена в нуль (на рисунке они выделены красным). Четыре пары младших бит[7:0] активируют соответствующую точку – для надёжности Intel рекомендует взводить сразу оба бита.



• Флаги 13,9,8 регистра DR7 относятся больше к системе, чем непосредственно к точкам-останова. В пользовательском режиме, системный протекторат позволяет нам оперировать только битом[8] Local-Break, а остальные – принудительно сбрасываются в нуль. При хороших обстоятельствах (в режиме ядра), установленный бит[13] генерил-бы исключение #DB всякий раз, когда процессору попадались инструкции обращения к регистрам DR0-DR7. Но поскольку любой отладчик типа OllyDbg наделён способностью ставить аппаратные бряки, система маскирует бит[13], чтобы не отнимать у отладчиков эту возможность.


Четыре регистра DR0-DR3 хранят линейные адреса 4-х точек останова, а регистр DR6 является информационным. Как только сработает любая из точек, в DR6 взведётся привязанный к ней битовый флаг. Здесь нужно отметить, что процессор не очищает флаги [3:0] регистра DR6, а только устанавливает их. Поэтому после обработки брэйкпоинта, их очистка для следующего раза полностью лежит на нашей совести. Биты [15,14,13] регистра DR6 уточняют условие срабатывания НВР и в данном случае нам не интересны.





Проблема доступа к регистрам DR0-DR7



Процессор имеет огромное кол-во регистров, большая часть из которых не доступна программисту с прикладного уровня – в их числе и регистры отладки DRx. Когда мы в реальном режиме или-же пишем драйвер под Win, то имеем полную власть над регистрам без каких-либо ограничений. Но возня с драйверами выходит за рамки данной статьи, так-что мы попытаемся утянуть эти регистры в защищённом режиме, с правами обычного юзера.



По сути, система ограничивает наш аппетит на уровне сегментного регистра CS (Code-Segment). Регистры CS/DS/ES и т.п. имеют видимую нам 2-байтную часть "селектор", и скрытую от посторонних глаз 8-байтную часть "дескриптор" – итого 10 байт. Дескриптор это отдельная тема – нам интересен сейчас только 16-битный селектор, в двух младших битах которого имеется поле RPL – Request Privilege Level, или уровень привилегии запроса.



Когда процессору встречается запрещённая для юзера инструкция (типа обращение к регистрам DRx), он в первую очередь проверяет, с какого именно кольца-защиты поступает запрос – нулевого (где тусуются драйвера и ядро системы), или третьего кольца (обитель наших программ). Для этого, процессору достаточно чекнуть поле RPL текущего регистра CS. У всех программ пользовательского уровня RPL=3 (взведены оба бита в селекторе), а у подопечных системы RPL=0. Это наглядно демонстрирует рисунок ниже:


Посмотрите на значения сегментных регистров в окне отладчика OllyDbg..

Как видим, селектор CS=1Bh, а это в двоичном 00011.0.11. Значит уровень привилегий =3, дескриптор сегмента находится в глобальной таблице GDT, и имеет в ней индекс [3]. Аналогичная картина и с селекторами данных DS/ES =23h = 00100.0.11, что подразумевает RPL=3, GDT, индекс записи =4.



Таким образом, из пользовательского приложения мы не имеем возможности оперировать регистрами отладки DR0-DR7, поскольку отправляя в свободный полёт, система вручила нашему коду самую низкую привилегию [3], отфутболив его обитать на последнее из колец Сатурна. Без драйвера, напрямую пробуриться из юзера в ядро нереально, а значит нужно искать обходные пути, и как оказалось такие пути есть.



Логику продуманов из Microsoft понять трудно – закрывая на 10-замков одну дверь, они сами-же отворяют на распашку другую. Причём обе лазейки ведут в один коридор, в конце которого виден тусклый свет ядра. В любом случае, именно такие забытые люки добавляют красок в скучную жизнь прикладного программиста. Судя по-всему, заранее раскурив наш план, индусы любезно предоставили нам полный доступ к контексту регистров, ..причем не только к контексту своего приложения, но и абсолютно любого, к которому мы сможем подобраться функцией OpenProcess().





Структура "CONTEXT"



В многозадачной системе, процессор берёт на грудь некоторое кол-во потоков Thread, и гоняет их исполнение по-кругу в виде циферблата часов. Каждому из этих потоков выделяется определённый квант времени (как-правило 31 ms), после чего управление передаётся сл.потоку в очереди. При таких обстоятельствах, чтобы прерванный поток смог продолжить свою работу с того-же места, система предварительно должна где-то сохранить значения всех его регистров – этот процесс известен как "Переключение контекста". В результате, у каждого из работающих в системе потоков оказывается свой собственный набор регистров CPU.



Контекст отражает состояние процессора на момент последнего исполнения потока, и записывается это состояние в одноимённую структуру "CONTEXT". Что примечательно, в этой структуре нашлось место и для регистров отладки DR0-DR7, и это способствует их изменению прямо во-время работы нашего приложения. Вот как отображает эту структуру ядерный отладчик WinDbg:


В составе Win32-API имеются функции для чтения и модификации этой структуры – Get/SetThreadContext() соответственно. При использовании этих функции мы должны явно указать системе, блок каких именно регистров хотим прочитать/записать. Для этого, в структуре "CONTEXT" имеется самый первый её член "ContextFlags" (на рис.выше он выделен зелёным), который может принимать следующие значения:

C-подобный:

CONTEXT_CONTROL = 0x10001
CONTEXT_INTEGER = 0x10002
CONTEXT_SEGMENTS = 0x10004
CONTEXT_FLOATING_POINT = 0x10008
CONTEXT_DEBUG_REGISTERS = 0x10010 ;// <---- наш клиент
CONTEXT_FULL = 0x1001f

;//----------------------------
BOOL SetThreadContext ( // <---- Обновить контекст регистров!
HANDLE hThread, // дескриптор потока (-2 для своего основного потока)
LPCONTEXT lpContext // адрес подготовленной структуры "CONTEXT"
);

По-умолчанию, в нёдрах компилятора FASM нет описаний структур для работы с контекстом, поэтому я приведу их здесь, а вам остаётся лишь добавить эти строки в инклуд "fasm\include\equates\kernel32.inc" (можно в самый конец файла).

C-подобный:

;// инклуд для fasm'a,
;// для пользовательской SEH-обработки внутрипоточных исключений
;// ------------------------------------------------------------
EXCEPTION_MAXIMUM_PARAMETERS = 15
SIZE_OF_80387_REGISTERS = 80
MAXIMUM_SUPPORTED_EXTENSION = 512

CONTEXT_CONTROL = 0x10001
CONTEXT_INTEGER = 0x10002
CONTEXT_SEGMENTS = 0x10004
CONTEXT_FLOATING_POINT = 0x10008
CONTEXT_DEBUG_REGISTERS = 0x10010
CONTEXT_FULL = 0x1001f

struct FLOATING_SAVE_AREA
ControlWord dd 0
StatusWord dd 0
TagWord dd 0
ErrorOffset dd 0
ErrorSelector dd 0
DataOffset dd 0
DataSelector dd 0
RegisterArea rb SIZE_OF_80387_REGISTERS
Cr0NpxState dd 0
ends

struct CONTEXT
ContextFlags dd 0
iDr0 dd 0
iDr1 dd 0
iDr2 dd 0
iDr3 dd 0
iDr6 dd 0
iDr7 dd 0
FloatSave FLOATING_SAVE_AREA
regGs dd 0
regFs dd 0
regEs dd 0
regDs dd 0
regEdi dd 0
regEsi dd 0
regEbx dd 0
regEdx dd 0
regEcx dd 0
regEax dd 0
regEbp dd 0
regEip dd 0
regCs dd 0
regFlag dd 0
regEsp dd 0
regSs dd 0
ExtendedRegisters rb MAXIMUM_SUPPORTED_EXTENSION
ends

struct EXCEPTION_RECORD
ExceptionCode dd 0
ExceptionFlags dd 0
pExceptionRecord dd 0
ExceptionAddress dd 0
NumberParameters dd 0
ExceptionInformation rd EXCEPTION_MAXIMUM_PARAMETERS
ends

struct EXCEPTION_POINTERS
pExceptionRecord dd 0
pExceptionFrame dd 0
pExceptionContext dd 0
pParam dd 0
ends

Ниже представлен фрагмент кода, который позволит установить аппаратный брейк на исполнение функции "Example". Здесь я просто заполняю нужные поля в структуре "CONTEXT", после чего вскармливаю её основному потоку через SetThreadContext():

C-подобный:

.data
ctx CONTEXT ;// импорт структуры CONTEXT из инклуда kernel32.inc
;//---------
.code
start:
mov ax,0000000000000000b ;// АХ = биты Len/RW четырёх точек HBP для регистра DR7
shl eax,16 ;// сделать их ст.словом в EAX (сдвинуть на 16 влево)
mov ax,1100000011b ;// в мл.слове EAX – биты включения HBP (здесь только нулевая точка)

mov [ctx.iDr7],eax ;// записать условие в регистр DR7 контекста
mov [ctx.iDr0],@Example ;// DR0 = адрес нулевой точки-останова
mov [ctx.ContextFlags],CONTEXT_DEBUG_REGISTERS ;// флаг = только регистры отладки

invoke SetThreadContext,-2,ctx ;// Обновить контекст регистров нашего/основного потока!!!

В документации мелкомягких можно встретить термин "псевдо/дескриптор". Это константы, которые мы можем использовать при вызовах API, вместо запроса их посредством увесистых функций. Так, если нам нужен дескриптор Handle своего процесса, мы можем использовать его превдо/дескриптор с константой -1. Аналогично, фиктивный дескриптор основного-потока приложения равен -2 или 0хFFFFFFFE (см.пример выше). Не пытайтесь использовать константу -2 для дополнительных потоков – их дескрипторы будут уже другими, и возвращает их функция создания потока CreateThread().





SEH – обработчик исключения #DB



Будем считать, что регистры DRx настроены и мы загрузили их в своё приложение. Теперь нужно запеленговать отладочное исключение #DB (INT-01h), которое сгенерит процессор при выполнении заданного нами в DR7 условия (т.е. нарвётся на брейкпоинт). В системах класса Win, ошибки и исключения попадают в цепкие лапы SEH – Structured Exception Handling – структурный обработчик исключений. Что это такое и с чем его едят обсуждалось в

Пожалуйста Авторизуйтесь или Зарегистрируйтесь для просмотра скрытого текста.

, поэтому повторяться не буду, однако в логику обработки эксепшена #DB окунуться стоит.



Если в двух словах, то при возникновении программных исключений, система передаёт управление по строго определённому адресу и этот адрес хранится в стековом SEH-фрейме. Если мы хотим перехватить исключение, то должны подкорректировать в этом фрейме указатель так, чтобы он указывал на наш заранее подготовленный обработчик.



Фишка в том, что в момент исключения система не просто передаёт управление в SEH-фрейм, но и предоставляет код-ошибки с указателем на контекст-регистров глючного потока (т.е. структуру Context). Более благоприятных обстоятельств для нашего случая и придумать сложно. В своём обработчике, нам остаётся лишь проверить код-ошибки на 0х80000004 (отладочное исключение #DB) пропуская ниже своих радаров все остальные. В общем случае, обработчик может быть оформлен примерно так:

C-подобный:

.code
start:
;// Добавляем свой SEH-фрейм перед системным
xor ebx,ebx ;//
push HBPhandler ;// адрес нашего обработчика
push dword[fs:ebx] ;// адрес следующего в цепочке
mov [fs:ebx],esp ;// зарегистрировать свой SEH-фрейм!

;//////////////////////////////////////////////////////
;// Теперь все ошибки будет отлавливать наш обработчик
;//////////////////////////////////////////////////////

proc HBPhandler pRecord,pFrame,pContext,pParam ;// система передаёт нам 4-аргумента

mov eax,[pRecord] ;// указатель на код-ошибки
mov eax,[eax] ;// возмём его в EAX
cmp eax,0x80000004 ;// проверить код на точку-останова #DB
je @found ;// если это наш клиент..
mov eax,1 ;// иначе: выходим из своего обработчика,
ret ;// ..передавая управление системе.

;// Значит сработа какая-то из наших точек HBP
;//--------------
@found:
mov esi,[pContext] ;// ESI = указатель на контекст регистров
mov eax,[esi+CONTEXT.iDr6] ;// EAX = значение регистра-статуса DR6
bt eax,0 ;// проверяем, какая именно точка сработала..
jc @00 ;// если это нулевая..
bt eax,1 ;// или первая
jc @01 ;//
;//...
;//==== Здесь обрабатываем точки-останова на своё усмотрение ==========
@00: nop
nop
@01: nop
nop
;//=====================================================================
mov esi,[pContext] ;// ESI = указатель на контекст
mov [esi+CONTEXT.iDr6],0 ;// очистить флаги в DR6 для сл.раза!!!
mov [esi+CONTEXT.regEip],@metka ;// установить EIP на нужный адрес в коде
xor eax,eax ;// команда "ребут" диспетчеру исключений
ret ;// продолжить исполнение кода..
endp

Практическая часть



В демонстрационном примере, я зашифрую две процедуры разными ключами и поставлю на начало каждой из них свой аппаратный бряк DR0 и DR1. В результате, как-только процессор дойдёт до первой процедуры, сработает первое исключение и его обработчик расшифрует свою процедуру. При этом вторая будет дожидаться очереди и по-прежнему лежать в памяти в зашифрованном виде. То-есть код программы будет раскрывать свои нёдра не сразу, а по мере продвижения регистра EIP. Это на порядок усложняет взлом.



Поскольку речь идёт о программной установки аппаратных точек, а не о шифровании кода, в тестовом варианте можно не тратить время на непосредственный крипт.. да и сам алгоритм шифрования не играет сейчас никакой роли (я выбрал обычный ксор). Поэтому в своей демке я буду шифровать файл не на диске, а в памяти, для чего просто вставлю вызов функции "Encrypt" в самом начале кода. После того-как файл загрузится в память, можно будет тупо снять дамп зашифрованных процедур в отладчике, и вшить этот дамп в тело дискового файла как шелл-код:


Обратите внимание на значение регистров-отладки DR0-DR7 рисунка выше.. На этот момент, я уже сконфигурировал их точками-останова функцией SetThreadContext() и под живым процессором они будут определены правильно. Однако отладчик упорно не хочет воспринимать этот факт как действительность, и показывает в своём окне девственное их состояние. В результате, он сам-же себе стреляет в ногу и уже не сможет раскусить наш план, поскольку мы оба с ним используем одни и те-же программные ресурсы.

C-подобный:

format pe console
include 'win32ax.inc'
entry start
;//---------
.data
ctx CONTEXT ;//<---- импорт структуры CONTEXT из инклуда
title db '*** HBP example ver.0.1 ***',0
ok db ' Pass OK!',10,10,0
wrong db ' <<-- Wrong pass -->>',0
pass db 10,' Type pass: ',0
size dd 32
buff db 0
;//---------
section '.code' data readable writable ;// секцию-кода нужно открыть на запись
start:
call Encrypt ;// Шифрование двух процедур в тестовом варианте
invoke SetConsoleTitle,title ;// Обзовём консоль..

;// Вставляем свой SEH-фрейм в цепочку
xor ebx,ebx
push HBPhandler
push dword[fs:ebx]
mov [fs:ebx],esp

;//********************************************************
;// Конфигурируем аппаратные брейкпоинты DR0 и DR1
;//********************************************************
mov ax,0000000000000000b ;// поля Len/RW четырёх HBP для DR7
shl eax,16 ;// отправить их в ст.часть EAX
mov ax,1100001111b ;// включить две точки: 0 и 1
mov [ctx.iDr7],eax ;// записать в контекст DR7
mov [ctx.iDr0],@CheckPass ;// DR0 = адрес нулевой точки
mov [ctx.iDr1],@BonusProc ;// DR1 = адрес первой
mov [ctx.ContextFlags],CONTEXT_DEBUG_REGISTERS ;// перезаписывать только DRx
invoke SetThreadContext,-2,ctx ;// Обновить контекст регистров!!!

;// Запрашиваем пароль у юзвера..
cinvoke printf,pass
cinvoke gets,buff

;//********************************************************
;// Здесь сработает нулевая точка-останова!!!
;// Процедура считает 16-битную сумму введённого пароля,
;// и проверяет её на валидность.
;// Должна лежать в памяти в зашифрованном виде,
;// т.к. обработчик исключения #DB будет расшифровывать её.
;//********************************************************
nop
nop
@CheckPass:
xor ebx,ebx
mov eax,ebx
mov esi,buff
@@: lodsb
add bx,ax ;//<--- сумма
or al,al
jne @b
nop
cmp bx,0x03CB
je @f
cinvoke printf,wrong
jmp E @Exit
@@: cinvoke printf,ok
@CheckPassEnd:
nop
nop
;//********************************************************
;// Здесь сработает следующая точка-останова DR1 !!!
;// Так-же должна лежать в зашифрованном виде.
;// Если пароль правильный, то сбоксит в окно имя компа.
;//********************************************************
@BonusProc:
invoke GetComputerName,buff,size
mov esi,buff
add esi,[size]
mov word[esi],' '
mov edi,esi
add edi,2
mov esi,buff
mov ebx,34
@@: mov ecx,[size]
add ecx,2
rep movsb
dec ebx
jnz @b
invoke MessageBox,0,buff,<'Hardware-Break',0>,0
@BonusProcEnd:
nop
nop

;//==== Конец программы ===================================
E @Exit : cinvoke gets,buff ;// ждём клаву..
cinvoke exit,0 ;// на выход!

;/////////////////////////////////////////////////////////////////
;//***************************************************************
;// Секция с пользовательским SEH-обработчиком.
;// Ключи, длина и адрес начала блоков для декрипта,
;// лежат в виде локальных данных.
;// Кроме того, в самом хвосте секции имеется функция шифрования,
;// которую можно использовать в отладочной версии программы.
;//***************************************************************
section '.help' data readable writable

proc HBPhandler pRecord,pFrame,pContext,pParam
locals
@HBp0 dd 0x7373,(@CheckPassEnd - @CheckPass)/2, @CheckPass ;// данные нулевой,
@HBp1 dd 0x315f,(@BonusProcEnd - @BonusProc)/2, @BonusProc ;// ..и первой точки-останова.
endl

mov eax,[pRecord] ;//
mov eax,[eax] ;//
cmp eax,0x80000004 ;// проверить код-ошибки на исключение #DB
je @found ;// если наш клиент..
mov eax,1 ;// иначе: выходим из своего обработчика,
ret ;// ..передавая управление диспетчеру-исключений.

@found:
mov esi,[pContext] ;// ESI = указатель на контекст
mov eax,[esi+CONTEXT.iDr6] ;// EAX = значение регистра-статуса DR6
bt eax,1 ;// проверить на точку #1
jc @01 ;// если да..
;//<------------------------- значит точка #0
mov ebx,[@HBp0 + 0] ;// ключ расшифровки
mov ecx,[@HBp0 + 4] ;// длина блока в словах
mov esi,[@HBp0 + 8] ;// адрес начала
@@: xor word[esi],bx ;//
add esi,2 ;//
loop @b ;// расшифровать весь блок!
mov esi,[pContext] ;//
mov eax,@CheckPass ;//
mov [esi+CONTEXT.regEip],eax ;// выставить EIP на начало расшифрованного блока
mov eax,[esi+CONTEXT.iDr7] ;//
and eax, not 0011b ;// выключить нулевую точку в регистре DR7
mov [esi+CONTEXT.iDr7],eax ;// обновить его
jmp @reboot ;// уходим на ребут контекста!

;//==== Аналогично оформляем и обработчик первой точки-останова
@01: mov ebx,[@HBp1 + 0] ;//
mov ecx,[@HBp1 + 4] ;//
mov esi,[@HBp1 + 8] ;//
@@: xor word[esi],bx ;//
add esi,2 ;//
loop @b ;//
mov esi,[pContext] ;//
mov eax,@BonusProc ;//
mov [esi+CONTEXT.regEip],eax ;//
mov eax,[esi+CONTEXT.iDr7] ;//
and eax, not 1100b ;//
mov [esi+CONTEXT.iDr7],eax ;//
@reboot:
and [esi+CONTEXT.iDr6],not 1111b ;// очистить флаги в регистре-статуса DR6 !!!
xor eax,eax ;// команда "Ребут-контекста" системному диспетчеру.
ret
endp

;//***************************************************************
;// Функция шифрования процедур, для отладочной версии программы.
;// Обязательно поместить в самый конец кода,
;// чтобы она не повлияла на смещение адресов, после её удаления.
;//***************************************************************
Encrypt:
mov ecx,(@CheckPassEnd - @CheckPass)/2
mov bx,0x7373
mov esi,@CheckPass
@@: xor word[esi],bx
add esi,2
loop @b

mov ecx,(@BonusProcEnd - @BonusProc)/2
mov bx,0x315f
mov esi,@BonusProc
@@: xor word[esi],bx
add esi,2
loop @b
ret
;//**********************************
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',user32,'user32.dll'
import msvcrt, printf,'printf',scanf,'scanf',gets,'gets',exit,'exit'
include 'api\kernel32.inc'
include 'api\user32.inc'

Чтобы не хранить пароли в открытом виде, имеет смысл сравнивать не сам введённый пароль, а его контрольную сумму. Вычислить сумму оригинального пароля можно и в обычном калькуляторе, складывая все ASCII-коды символов вручную. Но для автоматизации этого процесса можно воспользоваться и услугами 16-тиричного редактора "HxD". Вводим текст, и меню "Анализ" выбрав соответствующий пункт получаем сумму требуемой разрядности:


В скрепке я прикрепил готовый к исполнению файл, по типу Crackme. Можно загрузить его в отладчик и проверить свой скилл во-взломе. В нём всё лежит на поверхности и при желании можно подобрать к нему пароль меньше чем за минуту. Это чисто для того, чтобы продемонстрировать недееспособность отладчиков к подобного рода извращениям. Ну и для тренировки мозгов не помешает..

Заключение
Снискавшие себе заслуженную славу аппаратные Breakpoint'ы доминируют перед программными тем, что позволяют более гибко определять условия. Ведь что такое софт-точка? Это просто внедрённый перед какой-либо инструкцией 1-байтный код CCh. Дойдя до этого байта, процессор сгенерит исключение #BP, которое дёрнет INT-3. Достоинства программных точек лишь в том, что в отличии от четырёх/аппаратных, их может быть бесчисленное количество.



Другое дело регистры DR0-DR7.. Как было сказано выше, в регистре-конфигурации DR7 мы можем определять четыре условия их срабатывания – чтение, запись, исполнение памяти, а так-же контроль за обращением к портам ввода-вывода I/O. Пруф выше доказывает, что можно использовать аппаратные точки с любыми правами пользователя, причём работает эта фишка на всех системах, начиная от Win-2000 и вплоть до Win-10. Не сомневаюсь, что забросив эту технику в сундук, мы обязательно найдём ей применение в будущем.
 
Сверху