четверг, 31 марта 2011 г.

История одного бага

Баг был простой - на машине тестера все зависало, при воспроизведении же на моей машине вместо зависания я увидел BSOD:

TERMINAL_SERVER_DRIVER_MADE_INCORRECT_MEMORY_REFERENCE (cf)
Arguments:
Arg1: 8c8d715c, memory referenced
Arg2: 00000008, value 0 = read operation, 1 = write operation
Arg3: 8c8d715c, If non-zero, the instruction address which referenced the bad memoryaddress.
Arg4: 00000002, Mm internal code.
    A driver has been incorrectly ported to Terminal Server. It is referencing session space addresses from the system process context. 
    Probably from queueing an item to a system worker thread. The broken driver's name is displayed on the screen.

Debugging Details:
------------------

WRITE_ADDRESS:  8c8d715c

FAULTING_IP:
win32k!NtGdiFlushUserBatch+0
8c8d715c ??              ???

IMAGE_NAME:  win32k.sys
...
STACK_TEXT: 
8570b674 818d873f 00000003 85700d84 00000000 nt!RtlpBreakWithStatusInstruction
8570b6c4 818d91ac 00000003 c04646b8 00000000 nt!KiBugCheckDebugBreak+0x1c
8570ba70 818a9ef2 00000050 8c8d715c 00000008 nt!KeBugCheck2+0x5f4
8570bae8 8188fa74 00000008 8c8d715c 00000000 nt!MmAccessFault+0x106
8570bae8 8c8d715c 00000008 8c8d715c 00000000 nt!KiTrap0E+0xdc
8570bb70 8188c90c c000000d 83925000 00000120 win32k!NtGdiFlushUserBatch
8570bb8c 8187f751 badb0d00 8570bc04 82004398 nt!KiFastCallEntry+0xcc
8570bbfc 81a7755e 80000150 00000000 00000000 nt!ZwWriteFile+0x11
8570bc4c 81a7721b 83925000 83925000 00000001 nt!EtwpRealtimeSaveBuffer+0x113
8570bc60 81a70ccd 83925000 00000000 83925000 nt!EtwpFlushBufferToRealtime+0x54
8570bc84 81a7105f 00000000 00000000 82f84648 nt!EtwpFlushBuffer+0xa2
8570bd3c 81a70a49 00000001 00000000 82f84398 nt!EtwpFlushActiveBuffers+0x25d
8570bd7c 81a254a8 82f84648 85700680 00000000 nt!EtwpLogger+0x233
8570bdc0 8189145e 81a70816 82f84648 00000000 nt!PspSystemThreadStartup+0x9d
00000000 00000000 00000000 00000000 00000000 nt!KiThreadStartup+0x16

Первой мыслью было посмотреть, какой же логгер ответственнен за эти вызовы.

В ядре есть массив этих самых логгеров:

kd> dds WmipLoggerContext
818ff800  00000000
818ff804  00000000
818ff808  839960c8
818ff80c  82f84648 <========= а вот и виновник торжества
818ff810  82b43908
818ff814  82b43388
818ff818  82fb5008
818ff81c  82fe9a48
...
Каждый логгер представлен структурой WMI_LOGGER_CONTEXT.
Чтобы посмотреть имя:

kd> dt nt!_WMI_LOGGER_CONTEXT 82f84648 LoggerName
   +0x030 LoggerName : _UNICODE_STRING "Eventlog-Security"

Информации было достаточно для того, чтобы поискать проблему в google:
Eventlog-Security+win32k.sys+TERMINAL_SERVER_DRIVER_MADE_INCORRECT_MEMORY_REFERENCE, но поиск ничего не дал.

Вернувшись к проблемному адресу:
WRITE_ADDRESS:  8c8d715c  

Видно, что тот невалиден:

kd> u 8c8d715c
win32k!NtGdiFlushUserBatch:
8c8d715c ??              ???

Далее была мысль, что smss.exe не успел стартовать и загрузить win32k.sys, но в списке загруженных модулей тот присутствовал:

kd> lm
start    end        module name
...    
8b835000 8b850000   luafv      (pdb symbols)          c:\symbols\luafv.pdb\C177291432194CC8A5D6B9E0834207602\luafv.pdb
8c800000 8c9ff000   win32k     (pdb symbols)          c:\symbols\win32k.pdb\593611FD42154641A8CC37E05A5F704D2\win32k.pdb
95000000 95009000   TSDDD      (pdb symbols)          c:\symbols\TSddd.pdb\C75665DBDE5247248B6C0D9389C4D3261\TSddd.pdb
...

И уже после этого я вспомнил, что win32k.sys грузится в сессионное адресное пространство, проверим, установив контекст сессии:

kd> !session -s 0

После чего он стал валидным:
kd> u 8c8d715c
win32k!NtGdiFlushUserBatch:
8c8d715c 6838010000      push    138h
8c8d7161 6860739c8c      push    offset win32k!__safe_se_handler_table+0x50d8 (8c9c7360)
8c8d7166 e839100000      call    win32k!_SEH_prolog4 (8c8d81a4)

То есть, логгер "Eventlog-Security" при наступлении какого-то события писал в real-time режиме в файл, и дальше вызывал код из win32k.sys который был недоступен из системного контекста, т.к. располагался в АП сессии.

Теперь, когда проблема понятна, непонятно было, что ее вызвало, т..к. в стеке никаких упоминаний о моих драйверах не было.

Сперва была мысль о коллауте, который вызывался из менеджера системных сервисов:

KiSystemServiceAccessTeb:
        or      ebx, [ecx]+TbGdiBatchCount ; may cause an inpage exception
        jz      short Kss40             ; if z, no batched calls
        push    edx                     ; save address of user arguments
        push    eax                     ; save service number
        call    [_KeGdiFlushUserBatch]  ; flush GDI user batch

Однако бряк поставленный сюда не сработал, да и непонятно с чего бы системному потоку быть gui-потоком.

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

Таким образом нашлась и причина падения - некорректные id у ф-ций NtCreateThread и NtWriteProcessMemory для vista sp0, которые я взял из таблицы в инете(http://www.metasploit.com/users/opcode/syscalls.html), т.к. на тот момент не было возможности проверить на живой машине.

Т.е. принцип "недоверяй ничему, пока сам не проверишь" в ядерном программировании с использованием всяких недокументированных вещей справедлив как никогда =].

понедельник, 28 марта 2011 г.

Еще один способ детекта скрытых процессов и потоков

Давным-давно появились штуки типа DKOM, позволяющие скрывать потоки/процессы/(да и другие объекты ядра) от посторонних глаз, через удаление их из списков ОС( PsActiveProcessLinks/SessionProcessLinks и т.д. ).

Но так получилось, что теже самые потоки/процессы удаленные из одних мест, вполне себе продолжали жить в других(как списках, так и модулях).
Яркий пример подобного можно было наблюдать в 2008м, когда EP_X0FF опубликовал детект процессов(CsrWalker) через перечисление их в сервере подсистемы вин32(csrss).

Однако скрыв потоки/процессы и в этом месте нужно не забыть скрыть их и в третьем.
Третье место находится в ядре графической подсистемы - в win32k.sys.

Каким образом в этом списке оказываются процессы и потоки?

При конвертации потока в gui-поток, то есть тогда, когда поток делает свой первый вызов из shadow sdt.

Конвертация осуществляется ф-цией PsConvertToGuiThread, которая создает новый, больший по размеру ядерный стек для потока(для механизма рекурсивных вызовов шадоу сервисов) и налету заменяет его, устанавливает у потока поле ServiceTable, а также вызывает 2 коллаута:

typedef NTSTATUS (*PKWIN32_PROCESS_CALLOUT)( IN PEPROCESS Process, IN BOOLEAN Initialize );
typedef NTSTATUS (*PKWIN32_THREAD_CALLOUT)( IN PETHREAD Thread, IN PSW32THREADCALLOUTTYPE CalloutType );

extern PKWIN32_PROCESS_CALLOUT PspW32ProcessCallout;
extern PKWIN32_THREAD_CALLOUT  PspW32ThreadCallout;

Которые и добавляют потоки в списки win32k.sys.

Что такое коллаут и с чем его едят?

При создании сессии smss.exe загружает ядро графической подсистемы win32k.sys через полудокументированный вызов: 

NtSetSystemInformation( SystemLoadAndCallImage, ... ), который маппит win32k.sys в сессинное пространство и вызывает точку входа драйвера.

А в точке входа вызывается ф-ция:

PsEstablishWin32Callouts( W32pProcessCallout,
                              W32pThreadCallout,
                              UserGlobalAtomTableCallout,
                              UserPowerEventCallout,
                              UserPowerStateCallout,
                              UserJobCallout,
                              (PVOID)NtGdiFlushUserBatch);

Которая инициализирует указатели валидными адресами ф-ций.
То есть коллаут это просто указатель который будет проинициализирован при загрузке win32k.sys.

Таким образом при конвертации потока в гуишный вызовется соответствующие коллауты для потока и процесса.
Внутри которых будут выделены новые entry принадлежащие win32k.sys и представленные структурами PROCESSINFO и THREADINFO.
Данные коллауты также вызываются и при уничтожении потока, чтобы соответственно отлинковаться из списков.

PPROCESSINFO gppiList;     // глобальный список процессов
ULONG gdwGuiThreads;       // счетчик гуишных потоков, апдейтится каждый раз при создании нового гуишного треда

Добравшись до процесса можно получить список его потоков:

typedef struct tagPROCESSINFO
{
    W32PROCESS;
    PTHREADINFO     ptiList;
...
}PROCESSINFO;

Ну и соот-но для любого потока есть указатель на структуру процесса:

typedef struct tagTHREADINFO
{
    W32THREAD;
    PTL             ptl;
    PPROCESSINFO    ppi;
...
}THREADINFO;

Кроме того, в данных структурах много интересной информации, такой как очереди сообщений(input/post), Integrity Level (vista+, т.к. UIPI появилась с висты), инфа о win station и десктопе и т.д.

Таким образом, недостаточно хорошо спрятанный процесс/поток может быть найден через перечисление процессов и их потоков в ядре графической подсистемы.
Ограничение: НЕ-gui потоки не будут найдены.

вторник, 15 марта 2011 г.

Легальный способ уронить windows в BSOD из юзермода


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

Фича связана с аудитом, в реестре есть параметр:
HKLM\SYSTEM\CurrentControlSet\Control\Lsa\CrashOnAuditFail(http://technet.microsoft.com/en-us/library/cc963220.aspx), который вызовет BSOD при переполнении журнала аудита.

Как воспроизвести:

1) HKLM\SYSTEM\CurrentControlSet\Control\Lsa\CrashOnAuditFail устанавливаем в 1 (The feature is on. The system halts when it cannot record an event in the Security Log.)

2) restart ( чтобы изменения вступили в силу )

2) ставим для журнала аудита минимальный размер ( 64 кб ), чтобы он переполнился поскорее

3) включаем аудит на создание и удаление процессов

4) выполняем нечто вроде:

for (i; i < MAX_PROCESS_COUNT; i++)
        WinExec("C:\\WINDOWS\\NOTEPAD.EXE", SW_SHOW );

чтобы заполнить журнал аудита

5) как только журнал заполнится система сгенерит BSOD с багчеком 0xc0000244.

После BSOD, CrashOnAuditFail будет сброшен системой( иначе невозможно было бы загрузится из-за повторных бсодов ), а в отладчике можно увидеть сообщение, что на данное значение полагаться не стоит (т.е. впоследствии его нужно переустановить вручную).

Но фишка моей заметки не описать то, что описано в мсдн, а копнуть немного глубже, а именно в сторону возможности перехвата процессов.

Если посмотреть стек вызовов на момент бсода, то:

kd> k
ChildEBP RetAddr 
f7a34900 804f7bad nt!RtlpBreakWithStatusInstruction
f7a3494c 804f879a nt!KiBugCheckDebugBreak+0x19
f7a34d2c 804f8cc5 nt!KeBugCheck2+0x574
f7a34d4c 80646c0b nt!KeBugCheckEx+0x1b
f7a34d74 80534c12 nt!PopGracefulShutdown+0x15d
f7a34dac 805c621c nt!ExpWorkerThread+0x100
f7a34ddc 80541de2 nt!PspSystemThreadStartup+0x34
00000000 00000000 nt!KiThreadStartup+0x16

Можно увидеть, что код выполняется в рабочем потоке, т.е. не в контексте вызывающего.
А вот если бы бсод случался в контексте вызывающего, то через перехват соответствующих ф-ций (любым способом) генерирующих багчек, и через откат состояний можно было бы возвращать управление на саму проверку аудита. Таким образом можно было бы перехватить кучу действий при включенном аудите (http://technet.microsoft.com/ru-ru/library/cc779526%28WS.10%29.aspx), причем обнаружить подобный перехват было бы нелегко.
Но увы и ах, не судьба =]

воскресенье, 20 февраля 2011 г.

Высокопроизводительный трейсер


Совершенно очевидно, что если говорить о высокопроизводительном трейсере, то вариант с Debugging API сразу отпадает, т.к. использует высокоуровневую модель сверху и LPC сообщения снизу.

Отладка посредством порта ислючений также не подходит под определение "высокопроизводительно", т.к. в основе лежит все тот же механизм LPC.

Т.е. вариант один - спускаться в ядро, на самый низкий уровень.

Путей реализации ring0 трейсера тоже несколько, один из них это перехват 1го прерывания и установка в обработчике TF флага, также можно установить свою KiDebugRoutine и так далее, вариантов масса.

Однако замена в IDT хендлера 1го прерывания плоха тем, что нужно самостоятельно строить трап фрейм, а это время.

KiDebugRoutine также плох тем, что задействован дисперчер исключений, сначала ядерный, затем пользовательский, а путь обработки исключения тернист и долог.

Поэтому мне видится оптимальным вариант вобще без задействования ядерного, а стало быть и юзермодного дисперчеров исключений, а также без замены хендлера в IDT, идея в том, чтобы перехватить сам код обработчика int 1, встроив свою ф-цию, которая будет вызывать ф-цию с реализацией логики трейсера, а после этого передавать управление не на ядерный диспетчер исключений, а сразу на код выхода из ISR (Kei386EoiHelper). В пользовательскую ф-цию передается указатель на трап фрейм, таким образом на руках будет вся нужная трейсеру информация, а на выходе из пользовательской ф-ции устанавливается трейс флаг.

Таким образом, такой способ реализации дает возможность избежать тяжеловесного кода по разруливанию ядром трассировочного исключения, и переключения контекста r0->r3 при передаче управления на юзермодный диспетчер исключений(KiUserExceptionDispatcher).

Никаких сложностей и проблем с поиском места для патча, и ф-ции Kei386EoiHelper нет.

Саму логику также предпочтительнее реализовывать в ядре(для того, чтобы избежать ненужных переключений r0-r3), все что нужно сделать в юзермоде, это передать установки для трейсера в ядро, и запустить приложение с введенным трейс флагом(хотя последнее можно делать и из нотификатора в ядре).

Что же касается темы выхода из трассировки, то это отдельная тема, о ней как-нибудь в другой раз.

Варианты использования трейсера наверно объяснять не нужно. Ниже пара картинок, первая это entry point нодов из блокнота, вторая тоже самое, но пропущенная через трейсер, цветом показана интенсивность попадания управления на данный нод(красный, как наверно понятно, взялся изза цикла). Одинокий нод справа - кусок от реализации высокоуровнего seh'a( хендлер из scopetable если точнее).



четверг, 10 февраля 2011 г.

Определение id сервисной ф-ции

Предположим, перехвачена ф-ция на уровне менеджера объектов, к примеру ObCreateObject.
Как определить, какая сервисная ф-ция вызвала перехваченную ObCreateObject?
Ну, логично брякнуться внутри перехваченной ф-ции, и посмотреть стек вызовов:

kd> k
ChildEBP RetAddr 
ee6aece0 f7966345 Template!InternalObCreateObject+0xee
ee6aece4 8060528e Template!NewObCreateObject+0x5
ee6aed48 8053d648 nt!NtCreateEvent+0x90
ee6aed48 7c90e514 nt!KiFastCallEntry+0xf8
00abfd74 7c90d09a ntdll!KiFastSystemCallRet
00abfd78 7c80a78a ntdll!NtCreateEvent+0xc
00abfdc4 76d525a0 kernel32!CreateEventW+0x67
WARNING: Frame IP not in any known module. Following frames may be wrong.
00abfe68 7c917719 0x76d525a0
00abffa0 77de354b ntdll!LdrpGetProcedureAddress+0xa6
00abffbc 00000000 ADVAPI32!ScSvcctrlThreadA+0x12

А дальше казалось бы, логично проделать теже действия, что и WinDbg, и достать id из стаба ntdll, т.е. выполнить стек бектрейс, например, используя RtlCaptureStackBackTrace. И все бы хорошо, но данная ф-ция вернет лишь три верхних строчки из приведенного лога. И это понятно, т.к. ф-ция прыгает по цепочкам ebp фреймов, но допрыгать ф-ция может лишь до места, где копируется юзермодный стек в ядерный ( в обработчике sysenter ).

Где же взять информацию о юзермодном стеке? Ответ прост: в трап фрейме:

kd> dt nt!_KTRAP_FRAME 0xee6aed64
...
   +0x070 EFlags           : 0x202
   +0x074 HardwareEsp      : 0xabfd78 <========== юзермодный стек
   +0x078 HardwareSegSs    : 0x23

а вот и то, что нужно:

kd> dds 0xabfd78
00abfd78  7c90d09a ntdll!NtCreateEvent+0xc
00abfd7c  7c80a78a kernel32!CreateEventW+0x67

kd> u 7c90d09a
ntdll!NtCreateEvent+0xc:
7c90d09a c21400          ret     14h
7c90d09d 90              nop

Теперь можно прочитать id:

kd> u NtCreateEvent
ntdll!ZwCreateEvent:
7c90d08e b823000000      mov     eax,23h <== id
7c90d093 ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c90d098 ff12            call    dword ptr [edx]
7c90d09a c21400          ret     14h

То есть, бектрейс в данном случае оказался совсем не нужным.
На х64 все еще проще, id сискола сохраняется в KTHREAD_SystemCallNumber(название по памяти пишу, могу ошибаться в имени поля).

вторник, 8 февраля 2011 г.

Как опознать TrapFrame в памяти (х86)

Сделать это довольно просто, т.к. трап фрейм имеет некую метку, по которой его можно опознать в памяти ( ну и дополнительные признаки, чтобы быть полностью уверенным - значение сегментных регистров, eflags/eip и т.д.).

Как известно, при любом прерывании/sysenter, обработчик прерывания/sysenter'a строит трап фрейм - заполняет структуру KTRAP_FRAME в стеке, точнее дополняет, т.к. некоторые члены структуры оказываются в стеке автоматически при прерывании(eflags, cs, eip, error code для без кольцевого переключения например), при этом в поле TsDbgArgMark заносится маска 0BADB0D00h ( макрос SET_DEBUG_DATA в сорцах ).

Таким образом, к примеру:

kd> dds 0xee8a9d64
ee8a9d64  00c0f7dc
ee8a9d68  7c90e514
ee8a9d6c  badb0d00 <== та самая метка трапфрейма
ee8a9d70  00c0f7b8
ee8a9d74  00000000

Чтобы убедиться:

kd> dt nt!_KTRAP_FRAME 0xee8a9d64
   +0x000 DbgEbp           : 0xc0f7dc
   +0x004 DbgEip           : 0x7c90e514
   +0x008 DbgArgMark       : 0xbadb0d00<== метка
   +0x00c DbgArgPointer    : 0xc0f7b8
   +0x010 TempSegCs        : 0
   +0x014 TempEsp          : 0
   +0x018 Dr0              : 0
   +0x01c Dr1              : 0
   +0x020 Dr2              : 0
   +0x024 Dr3              : 0
   +0x028 Dr6              : 0
   +0x02c Dr7              : 0
   +0x030 SegGs            : 0
   +0x034 SegEs            : 0x23         <=== дополнительный признак
   +0x038 SegDs            : 0x23         <=== дополнительный признак
   +0x03c Edx              : 0x15f0002
   +0x040 Ecx              : 0xc6270
   +0x044 Eax              : 0xc6270
   +0x048 PreviousPreviousMode : 1  <=== дополнительный признак
   +0x04c ExceptionList    : 0xffffffff    <=== дополнительный признак
   +0x050 SegFs            : 0x3b         <=== дополнительный признак
   +0x054 Edi              : 0
   +0x058 Esi              : 0
   +0x05c Ebx              : 0xc0f7f4
   +0x060 Ebp              : 0xc0f7dc
   +0x064 ErrCode          : 0
   +0x068 Eip              : 0x7c90e514  <=== дополнительный признак (валидный адрес)
   +0x06c SegCs            : 0x1b          <=== дополнительный признак
   +0x070 EFlags           : 0x246         <=== дополнительный признак
   +0x074 HardwareEsp      : 0xc0f7b0
   +0x078 HardwareSegSs    : 0x23     <=== дополнительный признак
   +0x07c V86Es            : 0
   +0x080 V86Ds            : 0
   +0x084 V86Fs            : 0
   +0x088 V86Gs            : 0

Т.к. селекторы известны и постоянны:

#define KGDT_NULL           0
#define KGDT_R0_CODE     8
#define KGDT_R0_DATA     16
#define KGDT_R3_CODE     24
#define KGDT_R3_DATA     32
#define KGDT_TSS              40
#define KGDT_R0_PCR        48
#define KGDT_R3_TEB        56
#define KGDT_VDM_TILE    64
#define KGDT_LDT              72
#define KGDT_DF_TSS       80
#define KGDT_NMI_TSS      88

Это дает нам практически 100%ю гарантию, что найден именно трап фрейм, а не участок памяти с совпавшей TsDbgArgMark.

Метка эта нигде не проверяется(в релизной версии винды) и была введена для отладки.
Для x64 она отсутствует.

понедельник, 31 января 2011 г.

Почему в Windows не может быть двух карт ввода-вывода одновременно

Карта ввода-вывода находится в TSS и описывает всё адресное пространство портов ввода/вывода, которое составляет 65536 портов, т.е. для описания всего адресного пространства понадобится 8Кб. 

TSS представлен дескриптором в GDT, с пределом равным 000020AB (8363 байта)

=============================================
Sel.    Base            Limit     DPL  P   G    Description
=============================================
0028  F7717D70  000020AB   0    P   1b   32-Bit TSS (Busy)
=============================================

#define INT_DIRECTION_MAP_SIZE   32
typedef UCHAR KINT_DIRECTION_MAP[INT_DIRECTION_MAP_SIZE];

#define IOPM_COUNT        1         // Number of i/o access maps that exist
#define IOPM_SIZE            8192    // Size of map callers can set.
#define PIOPM_SIZE          8196    // Size of structure we must allocate to hold it.

typedef struct _KiIoAccessMap
{
    KINT_DIRECTION_MAP DirectionMap;
    UCHAR IoMap[PIOPM_SIZE];
} KIIO_ACCESS_MAP;

В TSS лежит сама карта и смещение:

typedef struct _KTSS
{
...
   USHORT IoMapBase; // Смещение карты ввода/вывода от начала TSS.
   KIIO_ACCESS_MAP IoMaps[IOPM_COUNT];
...

Установить карту ввода-вывода для процесса можно с помощью ф-ции Ke386SetIoAccessMap, которая скопирует в TSS->IoMaps.IoMap пользовательскую карту и установит IoMapBase для процесса на нее.

Связь процесса с картой ввода-вывода:

typedef struct _KPROCESS
{
...
    USHORT IopmOffset; <== определено смещение на нее
    UCHAR Iopl;
...
} KPROCESS, *PKPROCESS, *PRKPROCESS;

При создании процесса вызывается ф-ция KeInitializeProcess:

VOID KeInitializeProcess( __out PRKPROCESS Process, __in KPRIORITY BasePriority, __in KAFFINITY Affinity, __in ULONG_PTR DirectoryTableBase[2], __in BOOLEAN Enable )
{
...
    //
    // Initialize IopmBase and Iopl flag for this process (i386 only)
    //

    Process->IopmOffset = KiComputeIopmOffset(IO_ACCESS_MAP_NONE); <== IoMapBase будет указывать за пределы сегмента TSS
...
}

Для всех процессов(если для них не установлена карта ввода-вывода):

+0x000 Pcb             : _KPROCESS
...
+0x030 IopmOffset  : 0x20ac <== смещение за пределы сегмента
+0x032 Iopl             : 0 ''

А если учесть, что TSS представлен дескриптором в GDT, с пределом равным 000020AB (8363 байта), то получается, что любое обращение процесса к TSS будет генерить #GF, если не установлено 2 бита в eflags.

Итог:

IOPM для всех процессов может быть только один, причем по умолчанию смещение на карту находится на пределами сегмента TSS, однако ф-ция Ke386IoSetAccessProcess к примеру для конкретного процесса и для общей TSS устанавливает IopmOffset на общую карту доступа портов ввода-вывода.

А само смещение обновляется при переключении процессов(аттач/детач):

cPublicProc _KiSwapProcess  ,2
...
;
;   Change IOPM
;
    mov     ax,[edx]+PrIopmOffset
    mov     [ecx]+TssIoMapBase,ax
...

То есть в Windows просто не возможно иметь две и более разных карт ввода-вывода в TSS, места там хватает только для одной( limit == 20ABh ).