2P by GN⁺ | ★ favorite | 댓글 1개
  • Windows NT 3.1에서 i386kd로 Ctrl-C break-in을 걸면 kd> 프롬프트 대신 대상 486DX4 시스템이 재부팅되며, 원인은 enhanced 486 호환성 문제였음
  • NTOSKRNL.EXE의 KiSaveProcessorControlStateKiRestoreProcessorControlState가 CPU 타입을 검사할 때 word 비교를 써서 CPUID 지원 486을 Pentium 이상처럼 오판함
  • KPRCB의 CpuTypeCpuID가 함께 읽히면서 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 개발 키트의 kdntkd 대신 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 명령은 두 번만 등장함
    • KiSaveProcessorControlState
    • KiRestoreProcessorControlState
  • 두 위치 모두 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 모두에 적용됨

댓글과 토론

Hacker News 의견들
  • 예전에 서비스가 살아 있는지 확인하려고 포트 체크를 만들었는데, 머신들에 TCP 포트 몇 개를 여는 것만으로 회사 절반을 죽여버린 적이 있음
    말도 안 되던 시절이었음

    • Win95는 특정한 방식으로 ping만 보내도 죽거나 심지어 장악될 수 있었던 때가 있었음
      정말 멀리 왔음
      https://en.wikipedia.org/wiki/Ping_of_death
      일부 머신에는 몇 년 동안 패치되지 않은 SMB 취약점도 있었음. 이미 Metasploit이 있던 시절이라, 명령 몇 개만으로 로컬 네트워크의 대부분 Windows 호스트에 VNC를 주입할 수 있었음. 요즘은 적어도 패치 속도는 엄청 빠름
    • “몇 개”보다는 훨씬 많았던 것 같음
  • 아, 레트로 컴퓨팅의 토끼굴. 현실 세계의 결과와는 무해하게 분리돼 있지만, 그래도 정말 만족스러움
    괴짜들을 위한 진짜 꿀덫임. 결국 “댓글 6개 더 보기” 확장 버튼을 눌러버렸음

    • 커서가 그 확장 버튼으로 향하는 걸 보고 간신히 빼냈음
      오늘은 일을 좀 할 수 있을지도 모름
    • 내 시대 이전의 컴퓨터들, PET 같은 걸 보면 갖고 놀면 재미있겠다는 생각이 듦. 오래된 IRIX 박스도 괜찮고
      그런데 486이라니? 블루스크린과, 메모리를 감당할 수 없어 디스크 스와핑하느라 끝없이 기다리던 기억이 너무 많이 떠오름. 아직 나에겐 너무 이른 듯함
    • 글의 내용은 오늘날에도 어느 정도는 관련 있어 보임
  • 해결책은 뻔하다”가 정말 맞는 말임

    • 수학 교수님이 문제에는 두 종류가 있다고 하셨음: 쉬운 문제와, 아직 이해하지 못한 문제
    • 버그가 식별되고 나면 그렇긴 함. 다만 이런 버그를 어떻게 찾아내는지가 궁금함
      아마 트랩 코드의 디스어셈블리를 계속 읽다가 문제를 찾아냈을 것 같음. QEMU나 다른 디버깅 수단을 쓸 수 있었을 것 같지는 않음
  • 끝부분의 숨겨진 댓글 안에 묻혀 있는 방법까지 꼭 읽어야 함

    • Stack Overflow의 댓글 자동 접기는 볼 때마다 이해가 안 됨
      최저 득표 댓글을 숨기는 것도 아니고, 최신순이나 오래된 순을 숨기는 것도 아님. 무작위처럼 느껴지고 불필요함
      공간이 문제라면 숨기기보다 페이지 나누기를 넣는 편이 낫다고 봄
  • 예전 Abit, Asus, 어쩌면 MSI 보드에서 호환성을 위해 쓰던 slotket 어댑터 같은 것들은 진짜 오버클러킹 능력이 있었음
    RAM 속도는 얼마인지, 메인보드가 배수 설정에서 어느 정도 클럭까지 버틸 수 있는지가 관건이었음. Celeron과 Pentium II가 있던, 비교적 초기 오버클러킹 시절이었음
    한때 호환성 때문에 Celeron 600을 어댑터에 꽂고, 다시 Slot II to Socket 370 어댑터에 꽂아 [1] 같은 구성으로 1.2GHz에서 잘 돌렸음. 심지어 Windows ME에서 1.4GHz까지 올렸던 것 같고, 결국 그 CPU를 태워먹었음
    [1]http://krick.3feetunder.com/370mod/

    • 오버클러킹은 재미있었지만, 현대 CPU가 예전의 안전 여유분을 활용하도록 클럭과 전압을 정밀하게 제어할 수 있게 된 것도 좋음
  • 질문에 달린 답변이 정말 훌륭했음
    주제도 좋고 답변도 아주 좋았음

    • 사실 자기 질문에 직접 답한 것임. 그래도 훌륭함
  • 질문한 사람이 직접 답변까지 달았다는 게 멋짐
    이런 게 좋음. 사람들이 레트로 컴퓨팅에 관심을 가져서 정말 기쁨
    이런 문제와 해결책을 모두를 위해 문서화하는 건 좋은 일임

  • CPUID라는 게 안 좋은 아이디어라는 걸 알고 있었음

    • Windows 개발자들이 구조체에 “trustworthiness” 섹션을 넣어야겠다고 느낀 게 웃김
  • 왜 질문과 답변의 타임스탬프가 같았을까?

    • 정보를 공유하려고 그런 것임. 질문 작성자와 답변 작성자가 같은 사람인 걸 볼 수 있음
      아마 이 정보를 SO의 질의응답 형태로 올리는 편이, 아무도 찾지 못할 블로그 글보다 더 오래 남는다고 판단했을 것임
  • 열어보지 않고도 뭔지 알았다는 게 나쁜 걸까?
    됐고, 요즘 애들한테 내 잔디밭에서 나가라고 소리치러 가야겠음

    • 우리가 자라던 시절의 문제 해결은 달랐음
      의지할 건 재치와, 필요하면 유선전화로 부를 수 있는 고수들 정도였음. 유용성이 제각각인 답변이 가득한 멋진 인터넷 같은 건 없었음