- Windows NT 3.1에서
i386kd로 Ctrl-C break-in을 걸면kd>프롬프트 대신 대상 486DX4 시스템이 재부팅되며, 원인은 enhanced 486 호환성 문제였음 - NTOSKRNL.EXE의
KiSaveProcessorControlState와KiRestoreProcessorControlState가 CPU 타입을 검사할 때 word 비교를 써서 CPUID 지원 486을 Pentium 이상처럼 오판함 - KPRCB의
CpuType과CpuID가 함께 읽히면서 CPUID 지원 486의 모델 번호가 256 크게 해석되고, 그 결과 486에 없는 CR4 레지스터 접근이 발생함 - 원래 장착된 486DX-33과 SMM 없는 write-through Am486DX4-NV8T에서는 커널 디버깅이 정상 동작해, 문제가 CPUID 명령을 제공하는 enhanced 486에 좁혀짐
- NTOSKRNL.EXE 안의 두
cmp ds:word_FFDFF138, 5위치를 byte compare로 패치하면 원본 NT 3.1 Advanced Server와 NT 3.1 SP3 모두에서 수정 가능함
재현 환경과 증상
- Compaq ProSignia 3080에 Windows NT 3.1을 설치한 뒤 커널 디버깅을 시도함
- 이 시스템은 실제 운용 시 Windows NT 3.1을 실행했고, Windows NT가 명시적으로 목표로 삼은 기계 중 하나로 보임
- RAM은 128MB로 확장했으며, 소켓형 Intel 486DX-33을 AMD enhanced 486DX4-SV8B로 교체함
- 해당 CPU는 write-back cache와 SMM을 지원하며 전압 어댑터 소켓에 장착됨
- BIOS의 486DX4 지원 처리는 미뤄두고 CPU를 2x 배수로 점퍼 설정함
- 칩셋의 L1 write-back 지원 없이 2x 배수로 설정하면 Intel 80486DX2-66과 소프트웨어 호환된다고 기대함
- Intel 80486DX2-66은 이 시스템의 지원 옵션임
- Windows NT 3.1 설치 자체는 정상적으로 완료됨
- Windows NT 3.1 CD에는 전체 디버깅 심볼이 포함되어 있어 커널 디버깅을 시도함
- NetDDE가 이벤트 로그에 오류를 남기는 이유를 조사하려는 목적이 있었음
- 특정 EISA 이더넷 카드에서 시스템이 크래시하는 문제도 있었고, 하드웨어 결함 가능성이 남아 있었음
- 최신 Windows 10 개발 키트의
kd나ntkd대신 Windows NT 3.1에 포함된i386kd를 사용해야 커널 디버깅 설정이 맞음 i386kd에서 Ctrl-C break-in을 시도하면 대상 기계가kd>프롬프트를 제공하지 않고 재부팅됨
원인에서 배제된 것들
- 메모리는 정상으로 확인됨
- 시스템 파일 손상은 아니었음
- 커널이 디버깅을 위해 중단될 때 시스템을 재부팅하는 하드웨어 워치독은 활성화되어 있지 않음
- 호스트 쪽 USB-serial 어댑터는 정상 통신함
- 위조 PL2301로 보이는 장치이지만, 디버거 명령을 잘못 보내 “reboot system” 명령을 전송한 것은 아님
- KD 프로토콜에는 실제로 reboot system 명령이 있음
- 메인보드의 원격 관리 또는 경고 옵션과도 관련 없음
실제 원인: enhanced 486과 NT 3.1 커널의 비호환
- Windows NT 3.1 커널은 enhanced 486 프로세서와 호환되지 않음
- 더 구체적으로는
CPUID명령을 제공하는 486 프로세서에서 문제가 생김 - 커널 디버깅은 다음 CPU에서는 정상 동작함
- 원래 장착되어 있던 486DX-33
- SMM이 없는 older non-enhanced core의 write-through Am486DX4-NV8T
- 단순히 NT 3.1 커널 디버깅을 시험하는 목적이라면 Windows NT 3.1과 기본 호환되는 CPU를 쓰는 편이 적합함
- NT 자체를 고치려면 NTOSKRNL.EXE의 CPU 타입 판별 버그를 패치해야 함
문제가 터지는 커널 코드 경로
- 비호환의 직접 원인은
KiSaveProcessorControlState의 버그임- 대응 함수인
KiRestoreProcessorControlState에도 유사한 버그가 있음
- 대응 함수인
KiSaveProcessorControlState는 NTOSKRNL.EXE 내부에서 세 위치에서 호출됨- 예외가
KdpTrap을 통해 커널 디버거로 반사될 때- Ctrl-C break-in에서는 타이머 tick interrupt 안의 break-in polling 기능에서 breakpoint exception이 발생함
KeBugCheckEx가 호출될 때, 즉 “블루스크린” 상황KiSaveProcessorState가 호출될 때- IDA의 NTOSKRNL.EXE 제어 흐름 분석이 완전하다면, 이 함수는 export되지 않고 NTOSKRNL 내부에서도 호출되지 않아 실제로는 발생하지 않는 것으로 보임
- 예외가
KiSaveProcessorControlState는 확장된CONTEXT구조체에 프로세서 제어 상태를 저장함- CR0, CR2, CR3를 저장함
- Pentium 이상에서는 CR4도 저장함
- DR0~DR3, DR6, DR7 디버그 레지스터를 저장함
- GDT 주소, IDT 주소, 활성 TSS selector, LDT selector 같은 전역 보호 모드 설정도 저장함
KPRCB 필드 해석 오류
- 프로세서 타입 판별에는 KPRCB의 값이 사용됨
- KPRCB는 KPCR의 일부임
- 부트 프로세서 또는 단일 프로세서 시스템의 KPRCB는 가상 주소
FFDFF120에 있으며, 해당 메서드에 하드코딩되어 있음
- Geoff Chappell이 정리한 NT 3.1 KPRCB 관련 필드는 다음과 같음
+018 CHAR CpuType+019 CHAR CpuID+01A UShort CpuStep
- 이 필드들은
KiSetProcessorType에서 초기화됨- 486 프로세서의 offset 18 byte는 4로 설정됨
- Pentium 프로세서는 5로 설정됨
- Pentium Pro 및 Pentium II/III 프로세서는 6으로 설정됨
- offset 19 byte는 프로세서가 CPUID를 지원하고 “reasonable”하게 동작하는지를 나타내는 boolean flag임
- 문제의 비교 명령은
FFDFF138주소의 byte가 아니라 word를 읽음FFDFF138은 KPRCB 시작점에서 18h byte 떨어진 위치임- 따라서
CpuType뿐 아니라 바로 다음 byte인CpuID까지 모델 번호 일부로 함께 해석됨
- CPUID를 지원하는 프로세서는 모델 번호가 실제보다 256 크게 취급됨
- CPUID 지원 80-4-86은 Windows NT 3.1에서 80-260-86처럼 취급됨
- 260은 Pentium 기준인 5보다 훨씬 크므로 커널은 해당 프로세서에 CR4가 있어야 한다고 판단함
- enhanced 486에는 CR4가 없기 때문에 break-in 경로에서 문제가 발생함
패치 방법
- 버그를 확인하면 수정은 단순함
- NTOSKRNL.EXE에서
cmp ds:word_FFDFF138, 5명령은 두 번만 등장함KiSaveProcessorControlStateKiRestoreProcessorControlState
- 두 위치 모두 word compare를 byte compare로 바꿔야 함
- 헥스 에디터로 다음 바이트열을 두 번 패치함
- 기존:
66 83 3D 38 F1 DF FF 05 - 변경:
90 80 3D 38 F1 DF FF 05
- 기존:
- 이 수정은 원본 NT 3.1 Advanced Server 배포판의 NTOSKRNL.EXE와 NT 3.1 SP3 모두에 적용됨