2 คะแนน โดย GN⁺ 2023-09-29 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • บน Windows NT 3.1 เมื่อสั่ง break-in ด้วย Ctrl-C ผ่าน i386kd ระบบเป้าหมาย 486DX4 จะรีบูตแทนที่จะแสดงพรอมป์ kd> โดยสาเหตุมาจากปัญหาเรื่อง ความเข้ากันได้กับ enhanced 486
  • ใน NTOSKRNL.EXE ฟังก์ชัน KiSaveProcessorControlState และ KiRestoreProcessorControlState ใช้การเปรียบเทียบแบบ word ตอนตรวจชนิด CPU ทำให้เข้าใจผิดว่า 486 ที่รองรับ CPUID เป็น Pentium ขึ้นไป
  • ค่า CpuType และ CpuID ใน KPRCB ถูกอ่านพร้อมกัน ทำให้หมายเลขรุ่นของ 486 ที่รองรับ CPUID ถูกตีความว่าสูงขึ้น 256 และส่งผลให้มีการเข้าถึง รีจิสเตอร์ CR4 ที่ไม่มีอยู่บน 486
  • ทั้ง 486DX-33 เดิมและ Am486DX4-NV8T แบบ write-through ที่ไม่มี SMM สามารถทำ kernel debugging ได้ตามปกติ จึงชี้ว่าปัญหาเกิดเฉพาะกับ enhanced 486 ที่มีคำสั่ง CPUID
  • หากแพตช์ตำแหน่ง cmp ds:word_FFDFF138, 5 สองแห่งใน NTOSKRNL.EXE ให้เป็น byte compare ก็จะแก้ได้ทั้งบน NT 3.1 Advanced Server รุ่นต้นฉบับและ NT 3.1 SP3

สภาพแวดล้อมที่ใช้ทดสอบและอาการ

  • ติดตั้ง Windows NT 3.1 บน Compaq ProSignia 3080 แล้วทดลองทำ kernel debugging
    • เครื่องนี้เคยใช้งาน Windows NT 3.1 จริง และดูเป็นหนึ่งในเครื่องที่ Windows NT ตั้งใจรองรับอย่างชัดเจน
    • ขยาย RAM เป็น 128MB และเปลี่ยน Intel 486DX-33 แบบซ็อกเก็ตเป็น AMD enhanced 486DX4-SV8B
    • CPU ตัวนี้รองรับ write-back cache และ SMM และติดตั้งผ่านซ็อกเก็ตอะแดปเตอร์แปลงแรงดัน
  • การรองรับ 486DX4 ของ BIOS ยังไม่ได้จัดการ จึงตั้งจัมเปอร์ CPU เป็น ตัวคูณ 2x
    • แนวคิดคือถ้าชิปเซ็ตไม่รองรับ L1 write-back ก็ให้ตั้งเป็น 2x เพื่อให้เข้ากันได้ทางซอฟต์แวร์กับ Intel 80486DX2-66
    • Intel 80486DX2-66 เป็นตัวเลือกที่ระบบนี้รองรับอยู่แล้ว
  • การติดตั้ง Windows NT 3.1 เสร็จสมบูรณ์ตามปกติ
  • แผ่น CD ของ Windows NT 3.1 มี debugging symbols แบบครบ จึงลองทำ kernel debugging
    • จุดประสงค์คือสืบหาสาเหตุที่ NetDDE ทิ้งข้อผิดพลาดไว้ใน event log
    • อีกปัญหาหนึ่งคือระบบล่มกับการ์ดอีเธอร์เน็ต EISA บางใบ และยังเป็นไปได้ว่าเกิดจากฮาร์ดแวร์เสีย
  • ต้องใช้ i386kd ที่มากับ Windows NT 3.1 แทน kd หรือ ntkd จาก Windows 10 development kit รุ่นใหม่ จึงจะตรงกับการตั้งค่า kernel debugging ของระบบนี้
  • เมื่อกด Ctrl-C เพื่อ break-in ใน i386kd เครื่องเป้าหมายจะ รีบูต แทนที่จะให้พรอมป์ kd>

สิ่งที่ตัดออกไปแล้วว่าไม่ใช่สาเหตุ

  • หน่วยความจำตรวจสอบแล้วว่าปกติ
  • ไม่ใช่ความเสียหายของไฟล์ระบบ
  • ไม่มีการเปิดใช้งานฮาร์ดแวร์ watchdog ที่จะรีบูตเครื่องเมื่อเคอร์เนลหยุดเพื่อการดีบัก
  • อะแดปเตอร์ USB-to-serial ฝั่งโฮสต์สื่อสารได้ปกติ
    • แม้จะดูเหมือนเป็น PL2301 ปลอม แต่ไม่ได้ส่งคำสั่ง debugger ผิดจนกลายเป็นคำสั่ง “reboot system”
    • ในโปรโตคอล KD มีคำสั่ง reboot system อยู่จริง
  • ไม่เกี่ยวกับตัวเลือก remote management หรือการแจ้งเตือนของเมนบอร์ด

สาเหตุจริง: enhanced 486 กับเคอร์เนล NT 3.1 ไม่เข้ากัน

  • เคอร์เนล Windows NT 3.1 ไม่รองรับโปรเซสเซอร์ enhanced 486
  • ให้เจาะจงกว่านั้นคือมีปัญหากับ 486 ที่รองรับคำสั่ง CPUID
  • kernel debugging ทำงานได้ตามปกติบน CPU ต่อไปนี้
    • 486DX-33 เดิมที่ติดมากับเครื่อง
    • Am486DX4-NV8T แบบ write-through บนคอร์รุ่นเก่าที่ไม่ใช่ enhanced และไม่มี SMM
  • ถ้าเพียงต้องการทดสอบ kernel debugging ของ NT 3.1 ก็ควรใช้ CPU ที่เข้ากันได้กับ Windows NT 3.1 โดยตรง
  • ถ้าจะซ่อม NT เอง ต้องแพตช์บั๊กการตรวจชนิด CPU ใน NTOSKRNL.EXE

เส้นทางโค้ดในเคอร์เนลที่ทำให้เกิดปัญหา

  • สาเหตุโดยตรงของความไม่เข้ากันคือบั๊กใน KiSaveProcessorControlState
    • ฟังก์ชันคู่กันอย่าง KiRestoreProcessorControlState ก็มีบั๊กคล้ายกัน
  • KiSaveProcessorControlState ถูกเรียกจากสามตำแหน่งภายใน NTOSKRNL.EXE
    • เมื่อ exception ถูกส่งต่อเข้า kernel debugger ผ่าน KdpTrap
      • สำหรับการ break-in ด้วย Ctrl-C จะเกิด breakpoint exception จากฟังก์ชัน polling สำหรับ break-in ภายใน timer tick interrupt
    • เมื่อมีการเรียก KeBugCheckEx หรือก็คือสถานการณ์ “หน้าจอฟ้า”
    • เมื่อมีการเรียก KiSaveProcessorState
      • หากการวิเคราะห์ control flow ของ NTOSKRNL.EXE โดย IDA ถูกต้องครบถ้วน ฟังก์ชันนี้ไม่ได้ export และภายใน NTOSKRNL เองก็ไม่มีการเรียกใช้ จึงดูเหมือนว่าในทางปฏิบัติจะไม่เกิดเส้นทางนี้
  • KiSaveProcessorControlState จะบันทึกสถานะควบคุมของโปรเซสเซอร์ลงในโครงสร้าง CONTEXT แบบขยาย
    • บันทึก CR0, CR2, CR3
    • บน Pentium ขึ้นไปจะบันทึก CR4 ด้วย
    • บันทึกรีจิสเตอร์ดีบัก DR0~DR3, DR6, DR7
    • รวมถึงค่าระดับ global protected mode เช่น ที่อยู่ GDT, ที่อยู่ IDT, active TSS selector และ LDT selector

การตีความฟิลด์ KPRCB ผิดพลาด

  • การระบุชนิดโปรเซสเซอร์อาศัยค่าจาก KPRCB
    • KPRCB เป็นส่วนหนึ่งของ KPCR
    • KPRCB ของ boot processor หรือระบบ single-processor อยู่ที่ virtual address FFDFF120 และเมธอดนี้ฮาร์ดโค้ดค่านั้นไว้
  • ฟิลด์ KPRCB ที่เกี่ยวข้องใน NT 3.1 ตามที่ Geoff Chappell สรุปไว้มีดังนี้
    • +018 CHAR CpuType
    • +019 CHAR CpuID
    • +01A UShort CpuStep
  • ฟิลด์เหล่านี้ถูกกำหนดค่าเริ่มต้นใน KiSetProcessorType
    • สำหรับโปรเซสเซอร์ 486 ค่า byte ที่ offset 18 จะถูกตั้งเป็น 4
    • โปรเซสเซอร์ Pentium จะถูกตั้งเป็น 5
    • โปรเซสเซอร์ Pentium Pro และ Pentium II/III จะถูกตั้งเป็น 6
    • byte ที่ offset 19 เป็น boolean flag ที่บอกว่าโปรเซสเซอร์รองรับ CPUID และทำงานอย่าง “สมเหตุสมผล” หรือไม่
  • คำสั่งเปรียบเทียบที่เป็นปัญหาอ่าน word ที่ address FFDFF138 ไม่ใช่ byte
    • FFDFF138 อยู่ห่างจากจุดเริ่มต้นของ KPRCB 18h ไบต์
    • ดังนั้นมันจึงตีความไม่เฉพาะ CpuType แต่รวม byte ถัดไปคือ CpuID เป็นส่วนหนึ่งของหมายเลขรุ่นด้วย
  • โปรเซสเซอร์ที่รองรับ CPUID จะถูกมองว่ามีหมายเลขรุ่นมากกว่าความจริง 256
    • 80-4-86 ที่รองรับ CPUID จะถูก 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
  • แพตช์ชุดไบต์ต่อไปนี้สองครั้งด้วย hex editor
    • เดิม: 66 83 3D 38 F1 DF FF 05
    • เปลี่ยนเป็น: 90 80 3D 38 F1 DF FF 05
  • การแก้ไขนี้ใช้ได้ทั้งกับ NTOSKRNL.EXE ของ NT 3.1 Advanced Server รุ่นดั้งเดิมและ NT 3.1 SP3

1 ความคิดเห็น

 
GN⁺ 2023-09-29
ความคิดเห็นบน Hacker News
  • ครั้งหนึ่งผมเคยทำ port check เพื่อดูว่าเซอร์วิสยังทำงานอยู่ไหม แต่แค่เปิด TCP port ไม่กี่พอร์ตบนเครื่องต่าง ๆ ก็ทำให้บริษัทครึ่งหนึ่งล่มไปเลย
    เป็นยุคที่เหลือเชื่อจริง ๆ

    • เคยมีช่วงหนึ่งที่ Win95 แค่ถูก ping ด้วยวิธีเฉพาะก็ล่ม หรือถึงขั้นถูกยึดเครื่องได้
      เรามาไกลมากแล้วจริง ๆ
      https://en.wikipedia.org/wiki/Ping_of_death
      บางเครื่องยังมี ช่องโหว่ SMB ที่ไม่ได้แพตช์อยู่นานหลายปีด้วย ตอนนั้นมี Metasploit แล้ว แค่ไม่กี่คำสั่งก็ฉีด VNC เข้าไปในโฮสต์ Windows ส่วนใหญ่ในเครือข่ายโลคัลได้แล้ว ทุกวันนี้อย่างน้อยความเร็วในการแพตช์ก็เร็วขึ้นมาก
    • ผมว่ามันเยอะกว่า “ไม่กี่” พอร์ตมากนะ
  • อา หลุมกระต่ายของ retro computing แม้จะแยกออกจากผลกระทบในโลกจริงอย่างไม่เป็นอันตราย แต่ก็ยังให้ความพึงพอใจมากจริง ๆ
    เป็นกับดักน้ำผึ้งของพวกเนิร์ดโดยแท้ สุดท้ายผมก็กดปุ่มขยาย “ดูอีก 6 ความคิดเห็น” จนได้

    • เห็นเคอร์เซอร์กำลังมุ่งหน้าไปหาปุ่มขยายนั้น เลยรีบดึงตัวเองออกมาได้ทัน
      วันนี้ผมอาจจะได้ทำงานบ้างก็ได้
    • เวลาเห็นคอมพิวเตอร์ยุคก่อนผม อย่าง PET อะไรพวกนั้น ก็คิดว่าน่าจะสนุกถ้าได้ลองเล่น กล่อง IRIX เก่า ๆ ก็น่าจะโอเค
      แต่ 486 นี่นะ? มันทำให้นึกถึงความทรงจำมากมายเกี่ยวกับจอฟ้า และการรอไม่รู้จบเพราะหน่วยความจำไม่พอจนต้อง swap ลงดิสก์ สำหรับผมคงยังเร็วเกินไป
    • เนื้อหาในบทความดูเหมือนยังเกี่ยวข้องกับปัจจุบันอยู่บ้าง
  • ทางแก้มันชัดเจนอยู่แล้ว” นี่พูดถูกจริง ๆ

    • อาจารย์คณิตศาสตร์ของผมเคยบอกว่าปัญหามีอยู่สองประเภท: ปัญหาง่าย ๆ กับปัญหาที่ยังไม่เข้าใจ
    • ก็จริงเมื่อระบุบั๊กได้แล้ว แต่ผมสงสัยว่าเขาหาบั๊กแบบนี้เจอได้อย่างไร
      เดาว่าน่าจะอ่าน disassembly ของโค้ด trap ไปเรื่อย ๆ จนเจอปัญหา ไม่น่าจะใช้ QEMU หรือวิธีดีบักอื่น ๆ ได้
  • ต้องอ่านให้ถึง วิธีการ ที่ถูกฝังอยู่ในคอมเมนต์ที่ซ่อนอยู่ช่วงท้ายด้วย

    • ผมไม่เคยเข้าใจการพับคอมเมนต์อัตโนมัติของ Stack Overflow เลยสักครั้ง
      ไม่ได้ซ่อนคอมเมนต์ที่คะแนนต่ำสุด ไม่ได้ซ่อนตามลำดับใหม่สุดหรือเก่าสุดด้วย รู้สึกเหมือนสุ่มและไม่จำเป็น
      ถ้าปัญหาคือพื้นที่ ผมว่าควรใส่ การแบ่งหน้า มากกว่าซ่อน
  • ของอย่าง slotket adapter ที่บอร์ด Abit, Asus และอาจจะ MSI รุ่นเก่า ๆ ใช้เพื่อความเข้ากันได้นั้น มีความสามารถด้านโอเวอร์คล็อกจริง ๆ
    ประเด็นคือความเร็ว RAM เท่าไร และเมนบอร์ดจะรับความเร็วได้แค่ไหนจากการตั้งค่าตัวคูณ นั่นเป็นยุคโอเวอร์คล็อกค่อนข้างแรก ๆ ที่มี Celeron กับ Pentium II
    ช่วงหนึ่งเพราะเรื่องความเข้ากันได้ ผมเสียบ Celeron 600 เข้ากับอะแดปเตอร์ แล้วเสียบต่อเข้า Slot II to Socket 370 adapter อีกที เป็นคอนฟิกแบบ [1] แล้วรันที่ 1.2GHz ได้สบาย ๆ ผมว่าถึงขั้นดันไป 1.4GHz บน Windows ME ด้วยซ้ำ และสุดท้ายก็ทำ CPU ตัวนั้นไหม้
    [1]http://krick.3feetunder.com/370mod/

    • การโอเวอร์คล็อกสนุกก็จริง แต่ก็ดีเหมือนกันที่ CPU สมัยใหม่สามารถควบคุม clock และ voltage ได้อย่างละเอียดเพื่อใช้ประโยชน์จาก ส่วนเผื่อความปลอดภัย แบบในอดีต
  • คำตอบที่อยู่ใต้คำถามนั้นยอดเยี่ยมจริง ๆ
    หัวข้อก็ดี คำตอบก็ยอดเยี่ยมมาก

    • จริง ๆ แล้วเขาตอบคำถามของตัวเอง แต่ก็ยังยอดเยี่ยมอยู่ดี
  • เท่มากที่คนถามมาตอบเองด้วย
    ผมชอบอะไรแบบนี้ ดีใจจริง ๆ ที่ผู้คนสนใจ retro computing
    การบันทึกปัญหาและวิธีแก้แบบนี้ไว้ให้ทุกคนเป็นเรื่องดี

  • ผมรู้อยู่แล้วว่าเจ้า CPUID นี่เป็นไอเดียที่ไม่ดี

    • ตลกดีที่นักพัฒนา Windows รู้สึกว่าต้องใส่ส่วน “trustworthiness” ไว้ใน struct
  • ทำไม timestamp ของคำถามกับคำตอบถึงเหมือนกัน?

    • เขาทำแบบนั้นเพื่อแชร์ข้อมูล จะเห็นได้ว่าผู้เขียนคำถามกับผู้เขียนคำตอบเป็นคนเดียวกัน
      เขาคงคิดว่าการเอาข้อมูลนี้ไปลงในรูปแบบถาม-ตอบของ SO จะอยู่ได้นานกว่าบล็อกโพสต์ที่ไม่มีใครหาเจอ
  • แย่ไหมที่ผมรู้ว่าเป็นเรื่องอะไรตั้งแต่ยังไม่เปิดดู?
    ช่างเถอะ ผมต้องไปตะโกนไล่เด็กสมัยนี้ออกจากสนามหญ้าหน้าบ้านแล้ว

    • การแก้ปัญหาในสมัยที่เราโตมามันต่างออกไป
      สิ่งที่พึ่งได้มีแค่ไหวพริบของตัวเอง และถ้าจำเป็นก็มี เซียน ที่โทรตามทางโทรศัพท์บ้านได้ ไม่มีอินเทอร์เน็ตสุดวิเศษที่เต็มไปด้วยคำตอบซึ่งมีประโยชน์ไม่เท่ากันแบบทุกวันนี้