Remote code execution บน Windows Server 2012–2019 ด้วยช่องโหว่ CVE-2020–1350 (SIGRed) ได้อย่างไร

Datafarm
9 min readSep 25, 2020

--

by Worawit Wangwarunyoo , DATAFARM Research Team, Datafarm Company Limited

ช่องโหว่นี้เป็นช่องโหว่ของ Windows DNS server ค้นพบโดยนักวิจัยของ Check Point (CVE-2020–1350 (SIGRed) ) ความน่าสนใจคือมีระดับความรุนแรงสูง (คะแนน CVSS คือ 10.0) ที่สามารถทำ Remote Code Execution (RCE) ได้ รวมทั้ง ณ เวลาที่เขียนบทความนี้ (23 กันยายน 2563) ยังไม่มี demo หรือมีหลักฐานยืนยันว่าสามารถทำ RCE ได้จริง บนระบบปฏิบัติการ Windows Server 2012 ถึง Windows Server 2019 แต่บทความนี้ จะอธิบายว่า จะยึดครองระบบได้จริง ด้วยช่องโหว่นี้ อย่างไร

DNS เบื้องต้น

Domain Name System (DNS) คือระบบที่ช่วยการแปลง domain name เป็น IP address โดยปกติ DNS server ทำงานบน UDP และ TCP port 53 ข้อมูล DNS แต่ละอันจะเรียกว่า Resource Record (RR) ซึ่งจะมีอยู่หลายชนิด เช่น ชนิด A ไว้เก็บข้อมูล IPv4 ของ domain name, AAAA เก็บ IPv6 ของ domain name, NS เก็บข้อมูลของ DNS server ที่ทำหน้าที่ดูแลของ domain นั้นๆ

DNS server ที่ผู้ใช้งานเรียกใช้ จะเป็นแบบ Recursive Resolver (server จะทำตัวเป็น client ถามจาก dns server อื่นๆ หลายตัว แล้วตอบกลับหาผู้ใช้คำตอบเดียวว่าชื่อที่ต้องการหา มี IP address อะไร)

ตัวอย่างการทำงานของ DNS server (แบบ recursive) ในกรณีที่ DNS server เพิ่งเริ่มทำงาน ไม่มี cache ของ domain ใดๆ เลย

เมื่อมี client ถาม server ว่า IP address ของ www.wireshark.com คืออะไร จะทำให้เกิดการทำงานดังต่อไปนี้บน DNS server

packet 1 — DNS server ส่งไปถาม root server ว่า IP Address ของ www.wireshark.com คืออะไร

packet 2 — root server ตอบกลับมาว่า name server (ระดับนี้จะเรียกว่า Top-Level Domain, TLD) สำหรับหา IP Address ของ .com ชื่อว่าอะไร และ IP address อะไร (ดูตัวอย่างใน packet 4 เพราะคำตอบจาก root server ค่อนข้างยาว)

packet 3 — DNS server จะเลือกถาม com server ตัวหนึ่งว่า IP address ของ www.wireshark.com คืออะไร

packet 4 — ได้คำตอบกลับมาตามรูปข้างล่าง

จะเห็นว่าคำตอบที่ได้เป็น type NS (Name Server) ซึ่งหมายความว่า domain ที่เกี่ยวกับ wireshark.com ให้ไปถาม ns1.greenbaycrypto.com หรือ ns2.greenbaycrypto.com พร้อมกับ IP address ของ ns1 กับ ns2 ใน Additional records

packet 5 — ถาม ns1.greenbaycrypto.com ว่า IP address ของ www.wireshark.com คืออะไร

packet 6 — ได้คำตอบมาว่า IP address ของ www.wireshark.com คือ 23.91.67.6 เมื่อ server ได้คำตอบที่ต้องการแล้ว ก็จะทำการตอบ client ที่ถามมาตอนแรก นอกจากนี้ server จะจำคำตอบที่ได้มาชั่วคราวด้วย (cache) เพื่อลดขั้นตอนในการถามครั้งถัดๆ ไป

packet 7 — เกิดจากที่ client มีการถาม server อีกครั้งว่า IP address ของ mail.wireshark.com คืออะไร server จึงถาม ns2.greenbaycrypto.com ว่า IP address ของ mail.wireshark.com คืออะไร (ให้สังเกตว่าถามคนละ server กับที่ถาม www.wireshark.com)

packet 8 — ได้คำตอบว่า IP address ของ mail.wireshark.com คือ 23.91.67.6

packet 9,10 คือตัวอย่างที่ถามหา domain ที่ไม่มี

จะเห็นว่าหลังจาก packet ที่ 6 DNS server จะไม่มีการถาม root server หรือ TLD server เนื่องจากมีการจำว่า domain wireshark.com ต้องไปถามที่ไหน

เตรียมระบบสำหรับทดสอบ

เนื่องด้วยช่องโหว่ SIGRed นี้เป็นบั๊กของ Windows DNS Server ขณะทำการจัดการคำตอบของ SIG record ที่ได้รับมาจาก malicious DNS server ดังนั้นผู้โจมตีจะต้องมีการสร้าง domain ที่เข้าถึงได้จาก internet และติดตั้ง DNS server สำหรับ domain ของตนเอง เพื่อที่จะให้ DNS server ที่จะถูกโจมตีมาถาม DNS server ของ attacker

สำหรับการทดสอบ เพื่อความสะดวกที่ไม่ต้องสร้าง domain ที่ใช้ได้จริงใน internet ขึ้นมา เราจะทำการตั้ง Conditional Forwarder ใน Windows DNS server ให้ domain ของ evildns.com ไปที่ IP address 10.0.12.10 ตามรูปข้างล่าง ซึ่งมีผลลัพธ์เดียวกับที่ DNS Server ถามจาก server อื่นๆว่า evildns.com ให้ไปถาม server ที่ IP 10.0.12.10

รายละเอียดช่องโหว่

TL;DR ฟังก์ชัน dns!SigWireRead มีช่องโหว่ประเภท Integer Overflow ส่งผลให้เกิด Heap Buffer Overflow

Windows DNS server เวลารับ DNS response จาก server อื่น จะมีการเรียกฟังก์ชัน Wire_CreateRecordFromWire เพื่อจัดการ DNS resource record ที่อยู่ใน response ในฟังก์ชันนี้ จะมีการหาฟังก์ชันที่ใช้ในการจัดการ DNS resource record แต่ละชนิดโดยการเรียก RR_DispatchFunctionForType(&RRWireReadTable, recordType) โดย RRWireReadTable คือ lookup table สำหรับหาฟังก์ชัน

สมมติว่า Windows DNS Server ได้รับ response ตามรูปข้างล่าง server จะมีหาฟังก์ชันเพื่อจัดการแต่ละ Resource Record ในฟังก์ชัน RR_DispatchFunctionForType โดย recordType จะเป็นค่าในกรอบสีแดง กรณี type A (คือ 1) จะได้ฟังก์ชัน AWireRead สำหรับจัดการข้อมูลที่อยู่ในกรอบสีน้ำเงิน

เมื่อมาดูฟังก์ชัน SigWireRead ที่ใช้จัดการ SIG record ที่มีโครงสร้างดังรูปข้างล่างตาม RFC 2535

ทุก field ใน SIG record นั้นมีความยาวคงที่ ยกเว้น signer’s name กับ signature โดย signer’s name มีความยาวสูงสุงคือ 253 (ตามความยาวสูงสุดของ domain name) ส่วน signature นั้นมีความยาวสูงสุดเท่าไรก็ได้ ไม่มีกำหนดไว้ โดยโปรแกรมจะคิดจากตำแหน่งสุดท้ายของ packet ลบกับตำแหน่งเริ่มต้นของ signature

บรรทัดที่ 17 มีการจอง memory ด้วยฟังก์ชัน RR_AllocateEx ซึ่ง argument คือผลรวมของขนาดความยาวของ signer’s name กับ signature กับค่า 0x14

แต่เมื่อมาดูใน assembly จะเห็นว่าการผลลัพธ์การบวกใช้ register CX ที่มีขนาด 16 bit แล้วส่งให้ฟังก์ชัน RR_AllocateEx ดังนั้นถ้าเราทำให้ผลบวกของ ความยาว signer’s name + ความยาว signature + 0x14 นั้นมีค่ามากกว่า 65535 จะทำให้เกิด integer overflow ส่งผลให้ฟังก์ชัน RR_AllocateEx จองความจำขนาดเล็กกว่าที่ต้องการ และเมื่อโปรแกรมเรียกฟังก์ชัน memcpy ที่บรรทัด 26,27 จะทำให้เกิด heap buffer overflow เนื่องด้วยหน่วยความจำที่จองมามีขนาดเล็ก แต่ memcpy ทำการ copy ข้อมูลที่มีขนาดใหญ่กว่า

อาจจะดูเหมือนง่าย แค่เราใส่ข้อมูล signature ให้มีขนาดใหญ่น่าจะทำให้เกิด integer overflow ได้ แต่เมื่อเราไปดูใน DNS RFC 1035 จะเห็นว่าการรับส่งข้อมูล DNS ด้วย UDP จำกัดขนาดไว้แค่ 512 bytes (ถ้า support EDNS0 จะได้ขนาด 4096 bytes)

เมื่อเราไปดู DNS RFC 5966 จะมีเขียนไว้ว่า ถ้าขนาดของ DNS response มีขนาดใหญ่กว่าที่จำกัดไว้ของการส่งบน UDP (512 bytes) นั้น server สามารถที่จะ set TC flag เพื่อบอก client ว่า response มีขนาดยาวให้ทำการ query อีกครั้งด้วย TCP

และตาม DNS RFC 7766 นั้นให้ client และ server เพิ่ม 2 bytes สำหรับบอกว่าขนาดของข้อมูลคือเท่าไร เมื่อส่งบน TCP ตามรูปข้างล่าง ดังนั้นขนาดสูงสุดของ DNS response คือ 65535 bytes

ถึงแม้ขนาด 65535 bytes ก็ยังไม่เพียงพอเพราะว่า DNS response ต้องมีส่วนที่เป็น header และ Queries ของ request นั้น ดังนั้นขนาดข้อมูลของ dns record ในส่วน Answers ต้องน้อยกว่า 65535

DNS Message Compression

ใน DNS RFC 1035 หัวข้อ “4.1.4. Message Compression” ได้มีการอธิบายวิธีการลดขนาดของข้อมูล DNS โดยการย่อขนาดของ domain name ที่ซ้ำๆ ใน packet ตัวอย่างเช่น pointer ที่มีขนาด 2 bytes โดย 2 bits แรกต้องเป็น 1 และ 14 bits ที่เหลือคือ offset จาก byte แรกของ DNS response เช่น ในรูปข้างล่าง

จะเห็นว่าชื่อ www.datafarm.co.th ในส่วนของ Queries นั้นใช้ชื่อเต็มๆ “03 77 77 77 08 64 …” ซึ่งตรงนี้เป็นวิธีการ encode ชื่อใน DNS packet โดยจะแบ่งชื่อเป็นส่วนๆ ตามเครื่องหมาย “.” ดังนั้นในนี้จะมี 4 ส่วน และแต่ละส่วนจะนำหน้าด้วยความยาวของส่วนนั้น (DNS RFC ได้กำหนดว่าความยาวสูงสุดของแต่ละส่วนคือ 64) 03 ตัวแรกจึงหมายถึงความยาวของส่วนแรก (www) 08 หมายถึงความยาวของส่วนที่สอง (datafarm) และพอหมดส่วนสุดท้ายจะใช้ความยาวเป็น 00 เพื่อบอกว่าจบแล้ว

พอถึงในส่วนของ Answers นั้นชื่อ domain จะใช้แค่ c0 0c (1100 0000 0000 1100) ซึ่ง 2 bits แรกบอกว่าชื่อนี้คือ pointer และ 1100b บอกว่าให้ไปอ่านชื่อที่ offset 12 โดยเริ่มจากตรง d3 f9 … ซึ่งจะตรงกับชื่อที่อยู่ใน Queries พอดี

Triggering the bug

ใน SIG record ส่วนของ signer’s name นั้นก็สามารถที่จะใช้ name compression ที่เพิ่งกล่าวไป โดยถ้าดูในโค้ดก็คือฟังก์ชัน Name_PacketNameToCountNameEx ที่ทำหน้าที่อ่านชื่อ ถ้าชื่อเป็น pointer ก็จะแปลงมาเป็นชื่อเต็ม และใส่ความยาวของชื่อเต็มไว้ใน byte แรก

แม้ว่าเราจะใช้ name compression ปกติตรง signer’s name ก็ยังไม่สามารถที่จะทำ integer overflow ได้ เพราะชื่อเต็มต้องอยู่ใน packet แต่ถ้าเกิดชื่อเป็น 8ww.evildns.com (จะโดน encode เป็น 03 38 …) แล้วแก้ pointer ของ signer’s name เป็น c0 0d ทำให้ชี้ไปที่อักขระ 8 ค่าคือ 0x38 ซึ่งกลายเป็นว่าส่วนแรกของชื่อนี้มีความยาว 56 bytes ซึ่งเมื่อรวมกับ signature ที่มีขนาดยาวมาก ทำให้เกิด integer overflow

และผลลัพธ์ที่เกิดขึ้นใน windbg คือ

จะเห็นว่าเกิด Access violation ใน memcpy และถ้าดู stack trace จะเห็นว่าโปรแกรมยังอยู่ในฟังก์ชัน SigWireRead อยู่เลย ให้สังเกต “000000c5`2eb0c000=???????” ซึ่งหมายความ address ตรงนี้ process ยังไม่ได้จองเอาไว้ ซึ่งเมื่อลอง dump memory ส่วนนั้นออกมา

สาเหตุที่เป็นแบบนี้ เพราะเราได้มีเขียนเกินส่วนที่จองมาโดยประมาณ 64KB ซึ่งปกติแล้ว process heap จะค่อยๆ ขยายขึ้นตามการใช้งาน แล้ว memory ที่เพิ่งทำการจองโดยส่วนมากจะอยู่ช่วงท้ายๆ ทำให้การเขียนเกินที่ใหญ่ขนาดนี้ จบลงด้วยเขียนใน memory ที่ยังไม่มีการจองและเกิด access violation exception

โดยปกติช่องโหว่ที่เกี่ยวกับ heap buffer overflow โดยเฉพาะการเขียนเกินกว่าขนาดที่จองไว้ใหญ่ขนาดนี้ ต้องเข้าใจโปรแกรมมีการจัดการ heap memory ยังไง และมีการจองอะไรบ้างไว้ใน heap เพื่อที่เราจะให้ส่วนที่เขียนทับเฉพาะส่วนที่ไม่ส่งผลต่อโปรแกรม (หรือส่งผลต่อโปรแกรมหลังที่เรา exploit สำเร็จแล้ว) ไม่งั้นเราจะไปเขียนทับส่วนที่สำคัญ ส่งผลให้ crash ก่อนที่เราจะทำอะไรได้

WinDNS Heap Manager

WinDNS ได้มีส่วนจัดการ memory pools ของตัวเอง โดยจะมี 4 buckets สำหรับเก็บ memory chunk ขนาดต่างๆ (0x50, 0x68, 0x88, 0xa0) ที่ยังไม่ได้ใช้งาน (free list) แต่ถ้าขนาดของ memory chunk ที่ต้องการมีขนาดใหญ่กว่า 0xa0 bytes ก็จะให้เป็นหน้าที่ของ Windows heap

WinDNS ใช้ฟังก์ชัน Mem_Alloc เพื่อจอง memory ซึ่งมี pseudo code ดังรูป

และ WinDNS จะใช้ฟังก์ชัน Mem_Free เพื่อ free memory ซึ่งมี pseudo code ดังรูป

หลังจาก reverse engineer ฟังก์ชันที่ใช้สำหรับ allocate/free memory ของ WinDNS ทำให้เห็นบางอย่างที่ช่วยในการ exploit ช่องโหว่นี้ได้

WinDNS ไม่เคยคืน memory กลับหา native Windows Heap

ในกรณี free memory ขนาดของ chunk ที่ขนาดน้อยกว่าหรือเท่ากับ 0xa0 WinDNS จะเอา chunk นั้นใส่ใน linked list ทำให้ Windows Heap ยังมองว่า memory ส่วนนี้ ยังมีการใช้งานอยู่ ส่วนนี้ทำให้ Windows Heap ว่า heap chunk นี้ มี corruption หรือไม่ เพราะว่าการตรวจสอบจะทำตอน allocation กับ free เท่านั้น

รู้ทุกค่าใน chunk header

ใน chunk header จะมีค่าต่างๆ ได้แก่ ขนาดของ chunk, bucket index, tag, type และ cookie ที่เป็นค่าคงที่ ซึ่งทุกค่าเราสามารถรู้ได้ ทำให้เราสามารถเขียนทับค่าพวกนี้ด้วยค่าเดิมหรือค่าใหม่ ที่ไม่ทำให้โปรแกรมหยุดการทำงานได้ เงื่อนไขนี้สำคัญมากสำหรับช่องโหว่นี้ โดยไม่ใช้ช่องโหว่อื่นร่วมด้วย เพราะเราต้องเริ่มต้นการทำ overflow โดยที่ยังไม่รู้อะไรเลย

ใช้ singly linked list สำหรับเก็บ free chunk

วิธีเก็บแบบนี้ทำให้ allocation/free memory เป็นแบบ LIFO (Last In First Out) ซึ่งวิธีการจัดการ heap memory แบบนี้ ได้มีเทคนิคที่ช่วยในการ exploit อยู่แล้ว เช่น การควบคุม chunk ที่จะโดน allocate เนื่องด้วย LIFO, การสร้าง free list ปลอมเพื่อทำให้เกิด overlapped chunk

Triggering the bug without any crash

หลังจากที่เราทำความเข้าใจ WinDNS heap manager เราสามารถที่จะทำ heap buffer overflow โดยที่ไม่ให้โปรแกรม crash ได้ โดยเริ่มจากที่เรายังไม่มีข้อมูลอะไรเกี่ยวกับ dns server process เลยหลักการคือทำการจอง memory สำหรับ object ที่จะไม่โดนใช้งาน และโดน free ในช่วงเวลาที่เรา exploit จำนวนเยอะๆ ให้มีขนาดรวมกันมากกว่า 64KB หลังจากจากนั้นให้ free object ที่อยู่ช่วงต้นๆ แล้ว trigger bug เพื่อเขียนทับ object ที่เราจองทิ้งไว้เฉยๆ

Information Leak

ขั้นตอนนี้จำเป็นสำหรับการเขียน exploit ในปัจจุบัน เพราะ exploit mitigation ต่างๆ เช่น ASLR, DEP, CFG เป็นต้น ถึงแม้ว่าเราจะสามารถควบคุม PC ได้แล้ว เราไม่ทางรู้เลยว่าจะให้ PC เป็นค่าอะไรดี ถ้าไม่ทำ information leak ก่อน

เมื่อเราทำ heap buffer overflow เราต้องเขียนทับข้อมูลอื่นๆ จำนวนมาก ด้วยความรู้ที่เรามีเกี่ยวกับ WinDNS heap ทำให้เราสามารถแก้ไขค่าขนาดของ DNS resource record ที่ถูกเขียนทับได้ และเมื่อเราทำ dns query เพื่อให้ server อ่าน record ที่โดนแก้ขนาดข้อมูลserver จะอ่านข้อมูลเกินกว่าที่เขียนไว้ตอนแรกเลยไปถึงข้อมูลที่อยู่ใน memory chunk ถัดไป ถ้าเราทำ dns query เพื่ออ่านข้อมูลหลังจากทำ buffer overflow ทันที เราจะได้แค่ข้อมูลที่เราเขียนทับ ซึ่งไม่มีประโยชน์ แต่ถ้าเรา free chunk ที่อยู่ติดกับ resource record ที่โดนแก้ค่าขนาดข้อมูล WinDNS heap จะทำการเขียน pointer ว่า free chunk ถัดไปอยู่ที่ไหน แล้วค่อยทำ dns query เพื่ออ่านข้อมูลที่อยู่ติดกัน เราจะได้ address ที่อยู่ใน heap ซึ่งถ้าเราเรียงลำดับการ free ดีๆ เราสามารถที่จะได้ heap address ของส่วนที่เราเขียนทับ โดย heap memory ส่วนนี้อยู่ในการควบคุมของเรา ทำให้เราสามารถทำอย่างอื่นต่อได้ เช่น สร้าง free list ปลอมที่ชี้มาหา memory ส่วนที่เราควบคุม ทำให้ object อื่นๆ มาใช้ memory ส่วนนี้

นอกจากนี้แล้ว ยังพบว่ามีบาง object ใน heap มีการเก็บ pointer ที่ชี้ไปใน dns.exe (ผมเจอ 2 object อันหนึ่งชี้ไปในส่วนของ BSS และอีกอันชี้ไปในส่วนของ string ที่เป็น readonly) ซึ่งถ้าทำให้ข้อมูลพวกนี้ถูกจองอยู่ในส่วนที่เราควบคุมได้ เราสามารถที่อ่านข้อมูลของ object พวกนี้ ทำให้รู้ address ใน DNS module และสามารถนำมาคำนวณหา address เริ่มต้นของ dns.exe ได้

ตอนที่เราสามารถหา address หนึ่งได้ใน dns.exe นั้น เราสามารถที่จะระบุได้ว่าเรากำลัง exploit Windows version อะไร จาก 12 bits สุดท้าย

Controlling Program Counter (rip)

นอกจากที่ทำ information leak โดยการอ่านข้อมูลใน heap memory ที่อยู่ติดกับ DNS resource record ที่โดนแก้ขนาดแล้ว ข้อมูลใน heap ยังมีบาง object ที่เก็บ pointer to function เพื่อที่จะเรียกทีหลังได้ ซึ่งถ้าเราเขียนทับข้อมูลใน object นี้เราสามารถที่จะควบคุม PC ได้ ซึ่งผมเองใช้ object ที่สร้างใน dns!Timeout_FreeWithFunctionEx และโดนเรียกใช้ใน dns!Timeout_CleanupDelayedFreeList แต่ dns.exe ตั้งแต่ Windows 2012 มี Control Flow Guard (CFG) โดยโปรแกรมจะมีรายการฟังก์ชันที่อนุญาตให้ indirect call เรียกใช้ได้

จากรูปคือ CFG ใน Windows Server 2012 จะเห็นว่าถ้าเราแก้ให้กระโดดไป address ที่ไม่อยู่ในรายการอนุญาต ผลลัพธ์คือโปรแกรมหยุดการทำงาน ถึงจุดนี้ผมได้ลองหาวิธี bypass CFG ที่มีการได้มีการเปิดเผย จากที่เปิดอ่านผ่านๆ คืออาศัยที่ว่า Windows CFG ทำการตรวจสอบแค่ forward edge (call, jmp) ไม่มีการตรวจสอบ backward edge (ret) ดังนั้นให้ทำ arbitrary write ไปเขียนใน stack เพื่อแก้ค่า return address ที่เก็บอยู่ใน stack

แต่ตอนนี้เราไม่รู้แม้แต่ว่า stack address ของโปรแกรมอยู่ที่ไหน ทำได้แค่อ่านค่าใน heap memory ในส่วนที่เราควบคุมได้ ซึ่งมีโปรแกรมน้อยมากๆ ที่มี object ที่อยู่ใน heap แล้วเก็บ stack address ไว้ โดยผมไม่ลองหาในส่วนนี้ เพราะปกติจะไม่มี

หลังจากนั้น ผมลองหา object ที่มี pointer เพื่อที่จะเขียนทับ pointer แล้วโปรแกรมอ่านข้อมูล pointer ชี้ไว้แล้วส่งข้อมูลมาให้เรา เพื่อที่จะทำ ทำ arbitrary read แต่ที่หาเจอไม่สามารถใช้งานได้จริง เพราะมีเงื่อนไขมากเกินไป และก็ลองหาทางอื่นต่อ (อาจจะมีแต่ผมหาไม่เจอ)

หลังจากนั้น คือลองหาทางทำ arbitrary write ด้วยการทำ fake free list ที่ชี้ไปใน memory ใน dns module แต่ติดที่ Mem_Alloc มีการตรวจสอบ cookie ของ chunk ว่าเป็น free chunk หรือเปล่า ซึ่งกลายเป็นว่าต้องสามารถเขียนค่า cookie 8 bytes ก่อนที่จะให้ Mem_Alloc return address ในส่วนที่เราต้องการเขียนได้ ซึ่งผมมองว่าไม่น่าทำได้ เพราะต้องสามารถเขียนค่าให้ได้ 8 bytes ก่อน แต่เราต้องการที่จะทำ arbitrary write ให้ได้

หลังจากหลงทางไปนาน ผมได้กลับมาหาทาง bypass CFG โดยการหาฟังก์ชันที่เรียกได้ใน dns.exe เนื่องจากเรารู้ address ของฟังก์ชันทั้งหมดใน dns.exe และตอนนี้เราสามารถควบคุม rip เพื่อเรียกฟังก์ชัน พร้อมกับ argument ได้ 1 ตัว ผมก็เริ่มไล่ดูฟังก์ชันที่ใช้แค่ argument แรกตัวเดียว และในที่สุดก็เจอฟังก์ชัน dns!NsecDNSRecordConvert

จะเห็นว่า param_1 เป็น pointer to struct โดยที่บรรทัดที่ 15 นั้นคือการใช้ string pointer ใน struct เพื่อหาความยาวของ buffer ที่จะใช้ในการ copy และบรรทัดที่ 18 คือการจอง memory ที่สุดท้ายจะเรียก Mem_Alloc ซึ่งเราสามารถควบคุมได้ว่าเราจะให้ไปจองที่ไหน และบรรทัดที่ 22 ที่การ copy ข้อมูลจริงของ string ลงใน buffer ที่สร้างขึ้นมาใหม่ ดังนั้นถ้าโปรแกรมเรียกฟังก์ชันนี้ด้วย argument ที่เราควบคุม เราสามารถให้โปรแกรม copy ข้อมูลจาก memory address ที่เราต้องการจะอ่านมาไว้ใน memory ที่โดนจองใหม่ และเราก็สามารถที่จะอ่านข้อมูลจาก DNS resource record ที่โดนแก้ขนาดข้อมูล เท่ากับเราทำ arbitrary read ได้

หลังจากเจอวิธีทำ arbitrary read ได้ และมีวิธีควบคุมให้เรียก function ที่อนุญาตพร้อม argument 1 ตัว เป้าหมายของทำ code execution คือการเรียกฟังก์ชัน kernel32!WinExec หรือ msvcrt!system ซึ่งผมเลือก msvcrt!system เพราะว่า kerner32.dll อาจจะมี update จาก Microsoft patch ที่มีทุกเดือน ทำให้ offset ใน kernel32.dll โดนเปลี่ยน แต่ msvcrt.dll มีโอกาสโดน patch น้อยมาก

ดังนั้นผมจึงทำการ arbitrary read เพื่อหา msvcrt!memcpy จาก DNS import table และนำมาคำนวณหา msvcrt!system address และทำการแก้ไข pointer to function อีกครั้งเพื่อทำ code execution ด้วยฟังก์ชัน msvcrt!system

เริ่มต้นผม exploit สำหรับ Windows Server 2012 R2 ซึ่ง exploit นี้น่าจะใช้ได้กับ Windows Server 2016 และ Windows Server 2019 ด้วย หลังจากแก้ offset ของ dns กับ msvcrt.dll สำหรับ Windows server อีก 2 ตัว ทำ code execution ได้โดยไม่ต้องแก้ exploit logic

Note: dll ใน Windows Server 2019 นั้นมีการ compile ด้วย Export Suppression แต่ dns.exe นั้นไม่ได้เปิดใช้งาน feature นี้ ซึ่งถ้ามีการ enable จะทำให้ exploit นี้ต้องมีการแก้ไข (น่าจะเยอะพอสมควรเลย)

Demo Video Here (Windows Server 2012 R2, Windows Server 2016, Windows Server 2019)

แนะนำ

อย่างที่เห็นแล้วว่าช่องโหว่ CVE-2020–1350 นี้สามารถทำ Remote Code Execution ได้จริง ดังนั้นระบบที่มีการใช้งาน Windows DNS server ควรมีการติดตั้ง patch หรือถ้ายังไม่สามารถติดตั้ง patch ได้ให้ทำการแก้ไข registry

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\DNS\Parameters

DWORD = TcpReceivePacketSize

Value = 0xFF00

และทำการ restart DNS service

สรุป

ในบทความนี้ ได้แสดงถึงกระบวนการเขียน exploit ของช่องโหว่ CVE-2020–1350 บนระบบปฏิบัติการปัจจุบันที่มี mitigation ต่างๆ โดยเริ่มจากความรู้พื้นฐานของ DNS ที่เกี่ยวกับช่องโหว่ รวมถึงแสดงให้เห็นอุปสรรคต่างๆ ที่ต้องค่อยๆ แก้ไปทีละขั้น ซึ่งมีทั้งไปหาอ่านเพิ่มใน RFC การทำ reverse engineering การทำความเข้าใจ mitigation และสุดท้ายสามารถทำ remote code execution ได้จริง ซึ่งหวังว่าผู้อ่านจะเพลิดเพลินนะครับ

RCE on Windows Server 2012 R2 with CVE-2020–1350 (SIGRed)

RCE on Windows Server 2016 with CVE-2020–1350 (SIGRed)

RCE on Windows Server 2019 with CVE-2020–1350 (SIGRed)

--

--