ฉันเริ่มเขียนบันทึกเกี่ยวกับวิดีโอที่เกี่ยวข้องกับความปลอดภัยที่ฉันดู (เป็นวิธีการเรียกคืนอย่างรวดเร็ว)
สิ่งเหล่านี้อาจมีประโยชน์มากกว่าสำหรับผู้เริ่มต้น
ลำดับของบันทึกย่อที่นี่ ไม่ได้ อยู่ในความยากลำบาก แต่ในลำดับย้อนกลับลำดับของวิธีที่ฉันเขียนพวกเขา (เช่นล่าสุดก่อน)
งานนี้ได้รับใบอนุญาตภายใต้ใบอนุญาต International Creative Commons Noncommercial-Shareike 4.0 International
เขียนเมื่อวันที่ 12 สิงหาคม 2017
ได้รับอิทธิพลจากความเชื่อมั่นของ Gynvael CTF 2017 LiveStreams ที่นี่และที่นี่; และโดย Google CTF Quals 2017 LiveStream ที่นี่
บางครั้งความท้าทายอาจใช้งานที่ซับซ้อนโดยการใช้ VM ไม่จำเป็นเสมอไปที่จะต้องย้อนกลับวิศวกร VM อย่างสมบูรณ์และทำงานเพื่อแก้ปัญหาความท้าทาย บางครั้งคุณสามารถอีกนิดหน่อยและเมื่อคุณรู้ว่าเกิดอะไรขึ้นคุณสามารถขอเข้าไปใน VM และเข้าถึงสิ่งที่คุณต้องการ นอกจากนี้การโจมตีแบบแชนเนลตามเวลาที่ใช้เวลาจะง่ายขึ้นใน VMS (ส่วนใหญ่เกิดจากคำสั่ง "จริง" ที่ดำเนินการมากขึ้น
ฟังก์ชั่นที่น่าสนใจในการเข้ารหัสในไบนารีสามารถรับรู้ได้อย่างรวดเร็วโดยมองหาค่าคงที่และค้นหาพวกเขาทางออนไลน์ สำหรับฟังก์ชั่น crypto มาตรฐานค่าคงที่เหล่านี้เพียงพอที่จะเดาได้อย่างรวดเร็ว ฟังก์ชั่น crypto ที่ง่ายกว่าสามารถจดจำได้ง่ายขึ้น หากคุณเห็น Xors และสิ่งต่าง ๆ มากมายเช่นนั้นเกิดขึ้นและไม่มีค่าคงที่ที่สามารถระบุตัวตนได้อย่างง่ายดายมันอาจเป็น crypto ที่รีดด้วยมือ (และอาจพัง)
บางครั้งเมื่อใช้ IDA กับ hexrays มุมมองการถอดประกอบอาจจะดีกว่ามุมมองการสลายตัว นี่เป็นเรื่องจริงโดยเฉพาะอย่างยิ่งหากคุณสังเกตเห็นว่าดูเหมือนว่าจะมีภาวะแทรกซ้อนมากมายเกิดขึ้นในมุมมองการสลายตัว แต่คุณสังเกตเห็นรูปแบบซ้ำ ๆ ในมุมมองการถอดประกอบ (คุณสามารถสลับ B/W ทั้งสองได้อย่างรวดเร็วโดยใช้แถบอวกาศ) ตัวอย่างเช่นหากมีการใช้ห้องสมุดขนาดใหญ่ (ขนาดคงที่) (ขนาดคงที่) ที่นำมาใช้แล้วมุมมองการสลายตัวนั้นแย่มาก แต่มุมมองแบบถอดประกอบนั้นง่ายต่อการเข้าใจสิ่งต่าง ๆ (และจดจำได้ง่ายเนื่องจากคำแนะนำที่ adc นอกจากนี้เมื่อวิเคราะห์เช่นนี้การใช้คุณสมบัติ "โหนดกลุ่ม" ในมุมมองกราฟของ IDA นั้นมีประโยชน์อย่างยิ่งในการลดความซับซ้อนของกราฟของคุณอย่างรวดเร็วเมื่อคุณเข้าใจว่าแต่ละโหนดทำอะไร
สำหรับสถาปัตยกรรมแปลก ๆ การมีตัวจำลองที่ดีนั้นมีประโยชน์อย่างยิ่ง โดยเฉพาะอย่างยิ่งอีมูเลเตอร์ที่สามารถให้ความทรงจำของหน่วยความจำสามารถใช้เพื่อหาสิ่งที่เกิดขึ้นได้อย่างรวดเร็วและรับรู้บางส่วนที่น่าสนใจเมื่อคุณมีหน่วยความจำออกจากตัวจำลอง นอกจากนี้การใช้ตัวจำลองที่ใช้ในภาษาที่สะดวกสบาย (เช่น Python) หมายความว่าคุณสามารถเรียกใช้สิ่งที่คุณชอบได้ ตัวอย่างเช่นหากมีบางส่วนที่น่าสนใจของรหัสที่คุณอาจต้องการเรียกใช้หลายครั้ง (ตัวอย่างเช่นการใช้กำลังเดรัจฉานหรืออะไรบางอย่าง) จากนั้นใช้ตัวจำลองคุณสามารถเขียนโค้ดบางสิ่งที่ทำเฉพาะส่วนหนึ่งของรหัสแทนที่จะต้องเรียกใช้โปรแกรมที่สมบูรณ์
การขี้เกียจเป็นสิ่งที่ดีเมื่อรีด อย่าเสียเวลาด้านวิศวกรรมย้อนกลับทุกอย่าง แต่ใช้เวลามากพอที่จะทำ Recon (แม้ในการท้าทายอีกครั้ง!) เพื่อที่จะสามารถลดเวลาที่ใช้ในการทำภารกิจที่ยากขึ้นของ Reing สิ่งที่ Recon ในสถานการณ์เช่นนี้หมายถึงเพียงแค่ดูฟังก์ชั่นที่แตกต่างอย่างรวดเร็วโดยไม่ต้องใช้เวลามากเกินไปในการวิเคราะห์แต่ละฟังก์ชั่นอย่างละเอียด คุณวัดได้อย่างรวดเร็วว่าฟังก์ชั่นเกี่ยวกับอะไร (ตัวอย่างเช่น "ดูเหมือนสิ่งที่เข้ารหัสลับ" หรือ "ดูเหมือนการจัดการหน่วยความจำ" ฯลฯ )
สำหรับฮาร์ดแวร์หรือสถาปัตยกรรมที่ไม่รู้จักใช้เวลาพอที่จะค้นหาบน Google คุณอาจโชคดีด้วยเครื่องมือหรือเอกสารที่มีประโยชน์มากมายที่อาจช่วยให้คุณสร้างเครื่องมือได้เร็วขึ้น บ่อยครั้งที่คุณจะพบการใช้งานของเล่น emulator ฯลฯ ซึ่งอาจเป็นประโยชน์เป็นจุดเริ่มต้นที่จะเริ่มต้นจาก หรือคุณอาจได้รับข้อมูลที่น่าสนใจ (เช่นวิธีการจัดเก็บบิตแมปหรือวิธีการจัดเก็บสตริงหรืออะไรบางอย่าง) ซึ่งคุณสามารถเขียนสคริปต์ "แก้ไข" ได้อย่างรวดเร็วแล้วใช้เครื่องมือปกติเพื่อดูว่ามีสิ่งที่น่าสนใจหรือไม่
GIMP (เครื่องมือจัดการภาพ) มีฟังก์ชั่นเปิด/โหลดที่เย็นมากเพื่อดูข้อมูลพิกเซลดิบ คุณสามารถใช้สิ่งนี้เพื่อค้นหาสินทรัพย์หรือโครงสร้างซ้ำ ๆ ในข้อมูลไบนารีดิบได้อย่างรวดเร็ว ใช้เวลายุ่งกับการตั้งค่าเพื่อดูว่าสามารถรวบรวมข้อมูลเพิ่มเติมได้หรือไม่
เขียนเมื่อ 2 ก.ค. 2017
ได้รับอิทธิพลจากการสนทนากับ @P4N74 และ @H3RCUL35 ในการแชท infoseciitr #bin เรากำลังพูดคุยกันว่าบางครั้งผู้เริ่มต้นพยายามที่จะเริ่มต้นด้วยความท้าทายที่มีขนาดใหญ่ขึ้นโดยเฉพาะอย่างยิ่งเมื่อถูกถอดออก
ในการแก้ปัญหาความท้าทาย RE หรือเพื่อให้สามารถ PWN ได้เราจะต้องวิเคราะห์ไบนารีที่กำหนดก่อนเพื่อให้สามารถใช้ประโยชน์จากมันได้อย่างมีประสิทธิภาพ เนื่องจากไบนารีอาจถูกถอดออก ฯลฯ (พบโดยใช้ file ) เราจึงต้องรู้ว่าจะเริ่มการวิเคราะห์ได้ที่ไหนเพื่อให้ตั้งหลักเพื่อสร้างขึ้นมา
มีการวิเคราะห์บางรูปแบบเมื่อมองหาช่องโหว่ในไบนารี (และจากสิ่งที่ฉันรวบรวมทีม CTF ที่แตกต่างกันมีความชอบที่แตกต่างกัน):
1.1. transpiling รหัสสมบูรณ์เป็น c
การวิเคราะห์ประเภทนี้หายาก แต่ค่อนข้างมีประโยชน์สำหรับไบนารีขนาดเล็ก แนวคิดคือการไปในวิศวกรย้อนกลับทั้งหมดของรหัส แต่ละฟังก์ชั่นเปิดใช้งานใน IDA (โดยใช้มุมมอง decompiler) และการเปลี่ยนชื่อ (ทางลัด: n) และ retyping (ทางลัด: y) ใช้เพื่อทำให้รหัสถอดรหัสสามารถอ่านได้มากขึ้น จากนั้นรหัสทั้งหมดจะถูกคัดลอก/ส่งออกไปยังไฟล์. c แยกต่างหากซึ่งสามารถรวบรวมเพื่อให้ได้ไบนารีที่เทียบเท่า (แต่ไม่เหมือนกัน) กับต้นฉบับ จากนั้นการวิเคราะห์ระดับซอร์สโค้ดสามารถทำได้เพื่อค้นหาช่องโหว่ ฯลฯ เมื่อพบจุดช่องโหว่จากนั้นการหาประโยชน์จะถูกสร้างขึ้นบนไบนารีดั้งเดิมโดยทำตามไปตามแหล่งที่สลายตัวใน IDA แบบเคียงข้างด้วยมุมมองแบบถอดประกอบ (ใช้แท็บเพื่อสลับระหว่างทั้งสองอย่างรวดเร็ว
1.2. การวิเคราะห์การสลายตัวน้อยที่สุด
สิ่งนี้ทำได้ค่อนข้างบ่อยเนื่องจากไบนารีส่วนใหญ่ค่อนข้างไร้ประโยชน์ (จากมุมมองของผู้โจมตี) คุณจะต้องวิเคราะห์ฟังก์ชั่นที่น่าสงสัยหรืออาจนำคุณไปสู่ Vuln ในการทำเช่นนี้มีบางวิธีที่จะเริ่มต้น:
1.2.1. เริ่มจากหลัก
ตอนนี้โดยปกติแล้วสำหรับไบนารีที่ถูกถอดออกแม้กระทั่งหลักก็ไม่ได้ติดป้ายกำกับ (IDA 6.9 เป็นต้นไปทำเครื่องหมายให้คุณ) แต่เมื่อเวลาผ่านไปคุณเรียนรู้ที่จะรับรู้ถึงวิธีการเข้าถึงหลักจากจุดเข้า (ที่ IDA เปิดโดยค่าเริ่มต้น) คุณข้ามไปที่สิ่งนั้นและเริ่มวิเคราะห์จากที่นั่น
1.2.2. ค้นหาสตริงที่เกี่ยวข้อง
บางครั้งคุณรู้ว่าสตริงเฉพาะบางอย่างที่อาจถูกส่งออก ฯลฯ ซึ่งคุณรู้ว่าอาจมีประโยชน์ (ตัวอย่างเช่น "ขอแสดงความยินดีธงของคุณคือ %s" สำหรับความท้าทายอีกครั้ง) คุณสามารถข้ามไปยังมุมมองสตริง (ทางลัด: Shift+F12) ค้นหาสตริงและทำงานย้อนหลังโดยใช้ XREF (ทางลัด: X) XREFs ช่วยให้คุณค้นหาเส้นทางของฟังก์ชั่นไปยังสตริงนั้นโดยใช้ XREFs ในฟังก์ชั่นทั้งหมดในห่วงโซ่นั้นจนกว่าคุณจะถึงหลัก (หรือบางจุดที่คุณรู้)
1.2.3. จากฟังก์ชั่นสุ่มบางอย่าง
บางครั้งการไม่เฉพาะสตริงอาจมีประโยชน์และคุณไม่ต้องการเริ่มต้นจากหลัก ดังนั้นคุณจะพลิกรายการฟังก์ชั่นทั้งหมดอย่างรวดเร็วมองหาฟังก์ชั่นที่ดูน่าสงสัย (เช่นมีค่าคงที่จำนวนมากหรือ Xors จำนวนมาก ฯลฯ ) หรือเรียกฟังก์ชั่นสำคัญ (xrefs ของ malloc ฟรี ฯลฯ ) และคุณเริ่มจากที่นั่นและไปข้างหน้าทั้งสอง
1.3. การวิเคราะห์การถอดชิ้นส่วนบริสุทธิ์
บางครั้งคุณไม่สามารถใช้มุมมองการสลายตัว (เนื่องจากสถาปัตยกรรมแปลก ๆ หรือเทคนิคการต่อต้านการกำจัดหรือชุดประกอบที่เขียนด้วยมือหรือการถอดรหัสดูซับซ้อนเกินไปโดยไม่จำเป็น) ในกรณีนั้นมันถูกต้องอย่างสมบูรณ์แบบที่จะมองอย่างหมดจดในมุมมองการถอดชิ้นส่วน มันมีประโยชน์อย่างยิ่ง (สำหรับสถาปัตยกรรมใหม่) ในการเปิดความคิดเห็นอัตโนมัติซึ่งแสดงความคิดเห็นที่อธิบายการเรียนการสอนแต่ละคำสั่ง นอกจากนี้การกำหนดสีโหนดและการทำงานของโหนดกลุ่มยังมีประโยชน์อย่างมาก แม้ว่าคุณจะไม่ได้ใช้สิ่งเหล่านี้การทำเครื่องหมายความคิดเห็นอย่างสม่ำเสมอในการถอดชิ้นส่วนช่วยได้มาก ถ้าฉันทำสิ่งนี้เป็นการส่วนตัวฉันชอบเขียนความคิดเห็นเหมือน Python เพื่อที่ฉันจะได้อย่างรวดเร็วจากนั้น transpile ใน Python ด้วยตนเอง (มีประโยชน์โดยเฉพาะอย่างยิ่งสำหรับความท้าทายอีกครั้งที่คุณอาจต้องใช้ Z3 ฯลฯ )
1.4. การใช้แพลตฟอร์มเช่น BAP ฯลฯ
การวิเคราะห์ประเภทนี้เป็นแบบอัตโนมัติ (กึ่ง) และมักจะมีประโยชน์มากกว่าสำหรับซอฟต์แวร์ที่มีขนาดใหญ่กว่ามากและไม่ค่อยใช้โดยตรงใน CTFs
การฟัซซิ่งอาจเป็นเทคนิคที่มีประสิทธิภาพในการไปที่ Vuln อย่างรวดเร็วโดยไม่ต้องเข้าใจจริงในตอนแรก ด้วยการใช้ฟัสเซอร์หนึ่งสามารถได้รับรูปแบบของช่องโหว่ที่มีผลกระทบต่ำจำนวนมากซึ่งจำเป็นต้องได้รับการวิเคราะห์และ triaged เพื่อไปยังช่องโหว่ที่เกิดขึ้นจริง ดูบันทึกย่อของฉันเกี่ยวกับพื้นฐานของการฟัซซิงและการฟัซซัสทางพันธุกรรมสำหรับข้อมูลเพิ่มเติม
การวิเคราะห์แบบไดนามิกสามารถใช้หลังจากค้นหาช่องโหว่โดยใช้การวิเคราะห์แบบคงที่เพื่อช่วยสร้างการหาประโยชน์อย่างรวดเร็ว อีกทางเลือกหนึ่งสามารถใช้เพื่อค้นหา vuln เอง โดยปกติแล้วหนึ่งเริ่มต้นการทำงานภายในดีบักเกอร์และพยายามที่จะไปตามเส้นทางรหัสที่กระตุ้นข้อผิดพลาด โดยการวางจุดพักในสถานที่ที่เหมาะสมและวิเคราะห์สถานะของการลงทะเบียน/heap/stack/ฯลฯ หนึ่งสามารถได้รับความคิดที่ดีเกี่ยวกับสิ่งที่เกิดขึ้น หนึ่งยังสามารถใช้ debuggers เพื่อระบุฟังก์ชั่นที่น่าสนใจอย่างรวดเร็ว สิ่งนี้สามารถทำได้เช่นโดยการตั้งค่าจุดพักชั่วคราวในฟังก์ชั่นทั้งหมดในตอนแรก จากนั้นดำเนินการต่อไป 2 เดิน - หนึ่งผ่านเส้นทางรหัสที่ไม่น่าสนใจทั้งหมด และหนึ่งผ่านเส้นทางที่น่าสนใจเพียงอย่างเดียว การเดินครั้งแรกเดินทางไปยังฟังก์ชั่นที่ไม่น่าสนใจทั้งหมดและปิดการใช้งานจุดพักเหล่านั้นดังนั้นจึงปล่อยให้สิ่งที่น่าสนใจปรากฏขึ้นเป็นจุดพักในระหว่างการเดินครั้งที่สอง
สไตล์ส่วนตัวของฉันสำหรับการวิเคราะห์คือการเริ่มต้นด้วยการวิเคราะห์แบบคงที่โดยปกติจะมาจากการใช้งานหลัก (หรือสำหรับแอปพลิเคชันที่ไม่ได้อยู่ในคอนโซลจากสตริง) และทำงานเพื่อค้นหาฟังก์ชั่นที่ดูแปลก ๆ อย่างรวดเร็ว จากนั้นฉันใช้เวลาและแยกออกไปข้างหน้าและถอยหลังจากที่นี่เขียนความคิดเห็นเป็นประจำและเปลี่ยนชื่อและพิมพ์ตัวแปรอย่างต่อเนื่องเพื่อปรับปรุงการสลายตัว เช่นเดียวกับคนอื่น ๆ ฉันใช้ชื่อเช่น Apple, Banana, Carrot, ฯลฯ สำหรับดูเหมือนว่ามีประโยชน์ แต่ในขณะที่ยังไม่ทราบฟังก์ชั่น/ตัวแปร/ฯลฯ เพื่อให้ง่ายต่อการวิเคราะห์ (การติดตามชื่อชื่อ Func_123456 นั้นยากเกินไปสำหรับฉัน) ฉันยังใช้มุมมองโครงสร้างใน IDA เป็นประจำเพื่อกำหนดโครงสร้าง (และ enums) เพื่อทำให้การสลายตัวดียิ่งขึ้น เมื่อฉันพบช่องโหว่ฉันมักจะย้ายไปเขียนสคริปต์ด้วย pwntools (และใช้สิ่งนั้นเพื่อเรียก gdb.attach() ) ด้วยวิธีนี้ฉันสามารถควบคุมสิ่งที่เกิดขึ้นได้มากมาย ภายใน GDB ฉันมักจะใช้ GDB ธรรมดาแม้ว่าฉันได้เพิ่มคำสั่ง peda ที่โหลด PEDA ทันทีหากจำเป็น
สไตล์ของฉันมีการพัฒนาอย่างแน่นอนเนื่องจากฉันได้รับความสะดวกสบายมากขึ้นกับเครื่องมือของฉันและด้วยเครื่องมือที่กำหนดเองที่ฉันได้เขียนเพื่อเร่งความเร็ว ฉันยินดีที่จะได้ยินรูปแบบการวิเคราะห์อื่น ๆ รวมถึงการเปลี่ยนแปลงสไตล์ที่เป็นไปได้ที่อาจช่วยให้ฉันเร็วขึ้น สำหรับความคิดเห็น/การวิพากษ์วิจารณ์/การสรรเสริญที่คุณมีเช่นเคยฉันสามารถติดต่อได้ที่ Twitter @jay_f0xtr0t
เขียนเมื่อวันที่ 4 มิ.ย. 2017
ได้รับอิทธิพลจากสตรีมสดที่ยอดเยี่ยมนี้โดย Gynvael Coldwind ซึ่งเขาพูดถึงพื้นฐานของ ROP และให้คำแนะนำและกลเม็ดเล็กน้อย
Return Oriented Programming (ROP) เป็นหนึ่งในเทคนิคการเอารัดเอาเปรียบแบบคลาสสิกที่ใช้ในการหลีกเลี่ยงการป้องกัน NX (ไม่สามารถใช้งานได้) Microsoft ได้รวม NX เป็น DEP (การป้องกันการดำเนินการข้อมูล) แม้แต่ Linux ฯลฯ ก็มีประสิทธิภาพซึ่งหมายความว่าด้วยการป้องกันนี้คุณไม่สามารถวางเชลล์ลงบนฮีป/สแต็กได้อีกต่อไปและให้มันดำเนินการเพียงแค่กระโดดไปที่มัน ดังนั้นตอนนี้เพื่อให้สามารถเรียกใช้งานรหัสได้คุณจะข้ามไปยังรหัสที่มีอยู่แล้ว (ไบนารีหลักหรือไลบรารีของมัน-LIBC, LDD ฯลฯ บน Linux; Kernel32, NTDLL ฯลฯ บน Windows) ROP เข้ามามีอยู่โดยใช้ชิ้นส่วนของรหัสนี้ที่มีอยู่แล้วและหาวิธีที่จะรวมชิ้นส่วนเหล่านั้นเข้ากับการทำสิ่งที่คุณต้องการทำ (ซึ่งแน่นอนว่าแฮ็คโลก !!!)
ในขั้นต้น ROP เริ่มต้นด้วย Ret2LIBC และจากนั้นก็ก้าวหน้าขึ้นเมื่อเวลาผ่านไปโดยใช้รหัสชิ้นเล็ก ๆ อีกมากมาย บางคนอาจบอกว่าตอนนี้ ROP เป็น "ตาย" เนื่องจากมีการป้องกันเพิ่มเติมเพื่อบรรเทามัน แต่ก็ยังสามารถถูกนำไปใช้ในสถานการณ์จำนวนมาก (และจำเป็นอย่างยิ่งสำหรับ CTFs จำนวนมาก)
ส่วนที่สำคัญที่สุดของ ROP คืออุปกรณ์ แกดเจ็ตคือ "รหัสที่ใช้งานได้สำหรับ ROP" นั่นมักจะหมายถึงชิ้นส่วนของรหัสที่ลงท้ายด้วย ret (แต่อุปกรณ์ประเภทอื่น ๆ อาจมีประโยชน์เช่นที่ลงท้ายด้วย pop eax; jmp eax ฯลฯ ) เราห่วงโซ่แกดเจ็ตเหล่านี้เข้าด้วยกันเพื่อสร้างการหาประโยชน์ซึ่งเป็นที่รู้จักกันในชื่อ เชน ROP
หนึ่งในสมมติฐานที่สำคัญที่สุดของ ROP คือคุณสามารถควบคุมสแต็ก (เช่นตัวชี้สแต็กชี้ไปที่บัฟเฟอร์ที่คุณควบคุม) หากสิ่งนี้ไม่เป็นความจริงคุณจะต้องใช้กลอุบายอื่น ๆ (เช่นสแต็กหมุน) เพื่อควบคุมนี้ก่อนที่จะสร้างโซ่ ROP
คุณแยกอุปกรณ์อย่างไร? ใช้เครื่องมือที่ดาวน์โหลดได้ (เช่น RopGadget) หรือเครื่องมือออนไลน์ (เช่น Ropshell) หรือเขียนเครื่องมือของคุณเอง (อาจมีประโยชน์มากกว่าสำหรับความท้าทายที่ยากขึ้นบางครั้งเนื่องจากคุณสามารถปรับแต่งให้เป็นความท้าทายเฉพาะได้หากจำเป็น) โดยพื้นฐานแล้วเราแค่ต้องการที่อยู่ที่เราสามารถข้ามไปได้สำหรับอุปกรณ์เหล่านี้ นี่คือที่ที่อาจมีปัญหากับ aslr ฯลฯ (ในกรณีนี้คุณจะได้รับการรั่วไหลของที่อยู่ก่อนที่จะย้ายไปที่ ROP จริง)
ดังนั้นตอนนี้เราจะใช้อุปกรณ์เหล่านี้เพื่อสร้าง ropchain ได้อย่างไร? ก่อนอื่นเรามองหา "แกดเจ็ตพื้นฐาน" เหล่านี้เป็นอุปกรณ์ที่สามารถทำงาน ง่ายๆ สำหรับเรา (เช่น pop ecx; ret ซึ่งสามารถใช้ในการโหลดค่าลงใน ECX โดยการวางอุปกรณ์ตามด้วยค่าที่จะโหลดตามด้วยส่วนที่เหลือของโซ่ซึ่งจะถูกส่งกลับไปหลังจากโหลดค่า) แกดเจ็ตพื้นฐานที่มีประโยชน์มากที่สุดมักจะ "ตั้งค่าการลงทะเบียน", "ค่าการลงทะเบียนร้านค้าตามที่อยู่ชี้ไปที่การลงทะเบียน" ฯลฯ
เราสามารถสร้างขึ้นจากฟังก์ชั่นดั้งเดิมเหล่านี้เพื่อให้ได้ฟังก์ชั่นระดับที่สูงขึ้น (คล้ายกับโพสต์ของฉันที่มีชื่อว่าการเอารัดเอาเปรียบที่เป็นนามธรรม) ตัวอย่างเช่นการใช้อุปกรณ์การลงทะเบียน Set-register และร้านค้าที่อยู่อาศัยที่อยู่อาศัยเราสามารถสร้างฟังก์ชั่น "Poke" ซึ่งช่วยให้เราตั้งค่าที่อยู่เฉพาะด้วยค่าที่เฉพาะเจาะจง การใช้สิ่งนี้เราสามารถสร้างฟังก์ชั่น "poke-string" ที่ให้เราจัดเก็บสตริงใดก็ได้ที่ตำแหน่งใดก็ได้ในหน่วยความจำ ตอนนี้เรามี poke-string เราเกือบจะเสร็จแล้วเนื่องจากเราสามารถสร้างโครงสร้างใด ๆ ที่เราต้องการในหน่วยความจำและยังสามารถเรียกฟังก์ชั่นใด ๆ ที่เราต้องการด้วยพารามิเตอร์ที่เราต้องการ (เนื่องจากเราสามารถตั้งค่าการลงทะเบียนและสามารถวางค่าบนสแต็ก)
หนึ่งในเหตุผลที่สำคัญที่สุดในการสร้างจากลำดับแรกที่ต่ำกว่าเหล่านี้ไปจนถึงฟังก์ชั่นขนาดใหญ่ที่ทำสิ่งที่ซับซ้อนมากขึ้นคือการลดโอกาสในการทำผิดพลาด (ซึ่งเป็นเรื่องธรรมดาใน ROP เป็นอย่างอื่น)
มีแนวคิดเทคนิคและเคล็ดลับที่ซับซ้อนมากขึ้นสำหรับ ROP แต่นั่นอาจเป็นหัวข้อสำหรับโน้ตแยกต่างหากในเวลาที่แตกต่างกัน :)
PS: Gyn มีบล็อกโพสต์เกี่ยวกับการแสวงหาผลประโยชน์แบบส่งคืนที่อาจคุ้มค่ากับการอ่าน
เขียนเมื่อวันที่ 27 พฤษภาคม 2017; ขยายวันที่ 29 พฤษภาคม 2017
ได้รับอิทธิพลจากการถ่ายทอดสดที่น่าทึ่งนี้โดย Gynvael Coldwind ซึ่งเขาพูดถึงทฤษฎีพื้นฐานที่อยู่เบื้องหลังการฟัซซี่ทางพันธุกรรมและเริ่มสร้างฟัซเซอร์ทางพันธุกรรมขั้นพื้นฐาน จากนั้นเขาก็ดำเนินการเพื่อดำเนินการในสตรีมสดนี้ให้เสร็จสมบูรณ์
"ขั้นสูง" ฟัซซิ่ง (เปรียบเทียบกับฟัซเซอร์ตาบอดที่อธิบายไว้ใน "พื้นฐานของการฟัซซิง" ของฉัน) นอกจากนี้ยังปรับเปลี่ยน/กลายพันธุ์ไบต์ ฯลฯ แต่มันก็ฉลาดกว่าคนตาบอด "โง่" ฟัซเซอร์
ทำไมเราต้องใช้ฟัซเซอร์ทางพันธุกรรม?
บางโปรแกรมอาจเป็น "น่ารังเกียจ" ต่อฟัซเปอร์ใบ้เนื่องจากเป็นไปได้ว่าช่องโหว่อาจต้องใช้เงื่อนไขมากมายที่จะต้องไปถึง ในฟัสเซอร์ที่โง่เรามีความน่าจะเป็นที่ต่ำมากที่เกิดขึ้นเพราะมันไม่มีความคิดใด ๆ หากมีความคืบหน้าหรือไม่ เป็นตัวอย่างที่เฉพาะเจาะจงถ้าเรามีรหัส if a: if b: if c: if d: crash! (เรียกมันว่ารหัส Crasher) จากนั้นในกรณีนี้เราต้องการ 4 เงื่อนไขที่จะทำให้โปรแกรมขัดข้อง อย่างไรก็ตามฟัซเซอร์ใบ้อาจไม่สามารถผ่าน a ได้เพียงเพราะมีโอกาสต่ำมากที่การกลายพันธุ์ทั้ง 4 a , b , c , d , เกิดขึ้นในเวลาเดียวกัน ในความเป็นจริงแม้ว่ามันจะดำเนินไปโดยการทำเพียง a การกลายพันธุ์ครั้งต่อไปอาจกลับไปที่ !a เพียงเพราะมันไม่รู้อะไรเกี่ยวกับโปรแกรม
เดี๋ยวก่อนโปรแกรม "กรณีที่ไม่ดี" แบบนี้จะปรากฏขึ้นเมื่อใด
เป็นเรื่องธรรมดาในรูปแบบไฟล์ตัวแยกวิเคราะห์ที่จะทำตัวอย่างหนึ่ง ในการเข้าถึงเส้นทางรหัสเฉพาะบางอย่างอาจต้องผ่านการตรวจสอบหลายครั้ง "ค่านี้ต้องเป็นสิ่งนี้และค่านั้นจะต้องเป็นเช่นนั้นและค่าอื่น ๆ จะต้องเป็นอย่างอื่น" และอื่น ๆ นอกจากนี้เกือบจะไม่มีซอฟต์แวร์ในโลกแห่งความเป็นจริงที่ "ไม่ซับซ้อน" และซอฟต์แวร์ส่วนใหญ่มีเส้นทางรหัสที่เป็นไปได้มากมายหลายเส้นทางซึ่งบางอย่างสามารถเข้าถึงได้หลังจากหลายสิ่งหลายอย่างในรัฐได้รับการตั้งค่าอย่างถูกต้อง ดังนั้นเส้นทางรหัสของโปรแกรมเหล่านี้จำนวนมากจึงไม่สามารถเข้าถึงได้โดยไม่สามารถใช้เลือนลาง นอกจากนี้บางครั้งเส้นทางบางเส้นทางอาจไม่สามารถเข้าถึงได้อย่างสมบูรณ์ (แทนที่จะเป็นเพียงแค่ไม่น่าจะเป็นไปได้อย่างบ้าคลั่ง) เนื่องจากการกลายพันธุ์ไม่เพียงพอที่จะทำอะไรก็ตาม หากเส้นทางใด ๆ เหล่านี้มีข้อบกพร่องฟัซเซอร์ใบ้จะไม่สามารถหาได้
แล้วเราจะทำได้ดีกว่า Fuzzers ใบ้ได้อย่างไร?
พิจารณากราฟการควบคุมการไหล (CFG) ของรหัส Crasher ที่กล่าวถึงข้างต้น หากบังเอิญมีฟัซเซอร์โง่ ๆ ก็ a ต้องก็เช่นกันมันก็จะไม่รับรู้ว่ามันมาถึงโหนดใหม่ แต่มันจะเพิกเฉยต่อสิ่งนี้ต่อไปโดยทิ้งตัวอย่าง ในทางกลับกันสิ่งที่ AFL (และพันธุกรรมอื่น ๆ หรือ "สมาร์ท" ฟัซเปอร์) ทำคือพวกเขาจำได้ว่านี่เป็นข้อมูลชิ้นใหม่ ("เส้นทางที่เพิ่งมาถึงใหม่") และเก็บตัวอย่างนี้เป็นจุดเริ่มต้นใหม่ในคลังข้อมูล สิ่งนี้หมายความว่าตอนนี้ฟัซเซอร์สามารถเริ่มต้นจากบล็อก a และย้ายไปอีก แน่นอนบางครั้งมันอาจกลับไปที่ !a จาก a แต่ส่วนใหญ่แล้วมันจะไม่และอาจเข้าถึงบล็อก b แทนได้ นี่เป็นโหนดใหม่อีกครั้งดังนั้นเพิ่มตัวอย่างใหม่ลงในคลังข้อมูล สิ่งนี้ยังคงดำเนินต่อไปอนุญาตให้มีการตรวจสอบเส้นทางที่เป็นไปได้มากขึ้นเรื่อย ๆ และในที่สุดก็ถึง crash! -
ทำไมถึงทำงาน?
ด้วยการเพิ่มตัวอย่างที่กลายพันธุ์ลงในคลังข้อมูลที่สำรวจกราฟมากขึ้น (เช่นชิ้นส่วนที่ไม่ได้สำรวจมาก่อน) เราสามารถเข้าถึงพื้นที่ที่ไม่สามารถเข้าถึงได้ก่อนหน้านี้ เนื่องจากเราสามารถเลือนพื้นที่ดังกล่าวได้เราอาจสามารถค้นพบข้อบกพร่องในภูมิภาคเหล่านั้น
เหตุใดจึงเรียกว่าการฟุ่มเฟือยทางพันธุกรรม?
"สมาร์ท" แบบนี้เป็นเหมือนอัลกอริทึมทางพันธุกรรม การกลายพันธุ์และครอสโอเวอร์ของตัวอย่างทำให้เกิดตัวอย่างใหม่ เราเก็บตัวอย่างที่เหมาะสมกับเงื่อนไขที่ดีกว่า ในกรณีนี้เงื่อนไขคือ "มีกี่โหนดในกราฟถึง" สิ่งที่สำรวจมากขึ้นสามารถเก็บไว้ได้ นี่ไม่เหมือนกับ Algos ทางพันธุกรรม แต่เป็นรูปแบบ (เนื่องจากเราเก็บตัวอย่างทั้งหมดที่ข้ามอาณาเขตที่ยังไม่ได้สำรวจและเราไม่ได้ทำครอสโอเวอร์) แต่มีความคล้ายคลึงกันมากพอที่จะได้รับชื่อเดียวกัน โดยทั่วไปตัวเลือกจากประชากรที่มีอยู่ก่อนตามด้วยการกลายพันธุ์ตามด้วยการทดสอบการออกกำลังกาย (ไม่ว่าจะเห็นพื้นที่ใหม่) และทำซ้ำ
เดี๋ยวก่อนดังนั้นเราแค่ติดตามโหนดที่ยังไม่ได้เข้าถึง?
ไม่ไม่จริงๆ AFL ติดตามการเดินทางผ่านขอบในกราฟมากกว่าโหนด นอกจากนี้มันไม่เพียง แต่พูดว่า "Edge เดินทางหรือไม่" มันติดตามว่ามีกี่ครั้งที่ขอบถูกสำรวจ หากขอบถูกสำรวจ 0, 1, 2, 4, 8, 16, ... ครั้งมันถือว่าเป็น "เส้นทางใหม่" และนำไปสู่การเพิ่มเข้าไปในคลังข้อมูล สิ่งนี้ทำเพราะการดูขอบแทนที่จะเป็นโหนดเป็นวิธีที่ดีกว่าในการแยกแยะระหว่างสถานะแอปพลิเคชันและการใช้จำนวนการข้ามขอบที่เพิ่มขึ้นแบบทวีคูณให้ข้อมูลเพิ่มเติม
ดังนั้นคุณต้องการอะไรในฟัซเซอร์ทางพันธุกรรม?
เราต้องการ 2 สิ่งส่วนแรกเรียกว่า tracer (หรือเครื่องมือติดตาม) โดยทั่วไปจะบอกคุณว่าคำสั่งใดที่ดำเนินการในแอปพลิเคชัน AFL ทำสิ่งนี้ในวิธีง่ายๆโดยการกระโดดระหว่างขั้นตอนการรวบรวม หลังจากการสร้างแอสเซมบลี แต่ก่อนที่จะประกอบโปรแกรมมันจะมองหาบล็อกพื้นฐาน (โดยมองหาตอนจบโดยตรวจสอบคำสั่งกระโดด/ประเภทสาขา) และเพิ่มรหัสในแต่ละบล็อกที่ทำเครื่องหมายบล็อก/ขอบตามที่ดำเนินการ (อาจเป็นหน่วยความจำเงาหรือบางสิ่งบางอย่าง) หากเราไม่มีซอร์สโค้ดเราสามารถใช้เทคนิคอื่น ๆ สำหรับการติดตาม (เช่น PIN, Debugger ฯลฯ ) ปรากฎว่าแม้ Asan สามารถให้ข้อมูลความคุ้มครอง (ดูเอกสารสำหรับสิ่งนี้)
สำหรับส่วนที่สองจากนั้นเราจะใช้ข้อมูลความคุ้มครองที่กำหนดโดย Tracer เพื่อติดตามเส้นทางใหม่ตามที่ปรากฏและเพิ่มตัวอย่างที่สร้างขึ้นลงในคลังข้อมูลสำหรับการเลือกแบบสุ่มในอนาคต
มีกลไกหลายอย่างที่จะทำให้ผู้ติดตาม พวกเขาสามารถใช้ซอฟต์แวร์หรือใช้ฮาร์ดแวร์ สำหรับฮาร์ดแวร์มีคุณสมบัติเช่นมีคุณสมบัติของ Intel CPU บางอย่างที่ได้รับบัฟเฟอร์ในหน่วยความจำมันจะบันทึกข้อมูลของบล็อกพื้นฐานทั้งหมดที่ผ่านเข้าไปในบัฟเฟอร์นั้น มันเป็นคุณสมบัติเคอร์เนลดังนั้นเคอร์เนลจึงต้องรองรับและให้มันเป็น API (ซึ่ง Linux ทำ) สำหรับซอฟต์แวร์ที่ใช้เราสามารถทำได้โดยการเพิ่มรหัสหรือใช้ดีบั๊ก (โดยใช้จุดพักชั่วคราวหรือผ่านการก้าวเดียว) หรือใช้ความสามารถในการติดตามของ Sanitizer หรือใช้ตะขอหรืออีมูเลเตอร์หรือวิธีอื่น ๆ ทั้งหมด
อีกวิธีหนึ่งในการแยกความแตกต่างของกลไกคือการติดตามกล่องดำ (ซึ่งคุณสามารถใช้ไบนารีที่ไม่ได้แก้ไขเท่านั้น) หรือการติดตามกล่องสีขาวของซอฟต์แวร์ (ที่คุณสามารถเข้าถึงซอร์สโค้ดและแก้ไขรหัสเองเพื่อเพิ่มรหัสติดตาม)
AFL ใช้เครื่องมือวัดซอฟต์แวร์ในระหว่างการรวบรวมเป็นวิธีการติดตาม (หรือผ่านการจำลอง QEMU) Honggfuzz รองรับทั้งวิธีการติดตามซอฟต์แวร์และฮาร์ดแวร์ สมาร์ทสมาร์ทอื่น ๆ อาจแตกต่างกัน สิ่งที่ Gyn Builds ใช้การติดตาม/ความคุ้มครองที่จัดทำโดยที่อยู่ sanitizer (ASAN)
ฟัซเซอร์บางคนใช้ "Speedhacks" (เช่นเพิ่มความเร็วฟัซซิง) เช่นโดยการทำ forkserver หรือความคิดอื่น ๆ อาจจะคุ้มค่าที่จะดูสิ่งเหล่านี้ในบางจุด :)
เขียนเมื่อวันที่ 20 เมษายน 2017
ได้รับอิทธิพลจากสตรีมสดที่ยอดเยี่ยมนี้โดย Gynvael Coldwind ที่ซึ่งเขาพูดถึงสิ่งที่เกี่ยวกับการฟัซซิงและยังสร้างฟัซเซอร์พื้นฐานตั้งแต่เริ่มต้น!
ฟัซเซอร์คืออะไรในตอนแรก? แล้วทำไมเราถึงใช้มัน?
พิจารณาว่าเรามีไลบรารี/โปรแกรมที่ใช้ข้อมูลอินพุต อินพุตอาจมีโครงสร้างในบางวิธี (พูด PDF หรือ PNG หรือ XML ฯลฯ แต่ไม่จำเป็นต้องเป็นรูปแบบ "มาตรฐาน" ใด ๆ ) จากมุมมองด้านความปลอดภัยเป็นที่น่าสนใจหากมีขอบเขตความปลอดภัยระหว่างอินพุตและกระบวนการ / ไลบรารี / โปรแกรมและเราสามารถผ่าน "อินพุตพิเศษ" บางอย่างซึ่งทำให้เกิดพฤติกรรมที่ไม่ได้ตั้งใจเกินขอบเขตนั้น ฟัสเซอร์เป็นวิธีหนึ่งในการทำเช่นนี้ มันทำสิ่งนี้โดย "การกลายพันธุ์" สิ่งต่าง ๆ ในอินพุต ( อาจ ทำให้เสียหาย) เพื่อนำไปสู่การดำเนินการปกติ (รวมถึงข้อผิดพลาดที่จัดการอย่างปลอดภัย) หรือความผิดพลาด สิ่งนี้สามารถเกิดขึ้นได้เนื่องจากตรรกะเคสขอบไม่ได้รับการจัดการที่ดี
การชนเป็นวิธีที่ง่ายที่สุดสำหรับเงื่อนไขข้อผิดพลาด อาจมีคนอื่นเช่นกัน ตัวอย่างเช่นการใช้ ASAN (ที่อยู่ฆ่าเชื้อ) ฯลฯ อาจนำไปสู่การตรวจจับสิ่งต่าง ๆ มากขึ้นเช่นกันซึ่งอาจเป็นปัญหาด้านความปลอดภัย ตัวอย่างเช่นไบต์ที่ล้นของบัฟเฟอร์อาจไม่ทำให้เกิดความผิดพลาดในตัวของมันเอง แต่ด้วยการใช้ ASAN เราอาจสามารถจับได้แม้กระทั่งสิ่งนี้ด้วยฟัซเซอร์
การใช้งานที่เป็นไปได้อีกอย่างหนึ่งสำหรับฟัซเซอร์คืออินพุตที่สร้างขึ้นโดยการฟัซซิงกันหนึ่งโปรแกรมสามารถใช้ในไลบรารี/โปรแกรมอื่นและดูว่ามีความแตกต่างหรือไม่ ตัวอย่างเช่นข้อผิดพลาดของคณิตศาสตร์คณิตศาสตร์ที่มีความแม่นยำสูงบางอย่างถูกสังเกตเห็นเช่นนี้ สิ่งนี้มักจะไม่นำไปสู่ปัญหาด้านความปลอดภัยดังนั้นเราจะไม่ให้ความสนใจกับเรื่องนี้มากนัก
ฟัสเซอร์ทำงานอย่างไร?
ฟัสเซอร์นั้นเป็นลูปการกลายพันธุ์แบบจำลองซ้ำที่สำรวจพื้นที่สถานะของแอปพลิเคชันเพื่อพยายาม "สุ่ม" ค้นหาสถานะของความผิดพลาด / ความปลอดภัย ไม่ พบการหาประโยชน์เพียงแค่ช่องโหว่ ส่วนหลักของฟัซเซอร์คือตัวกลายพันธุ์ของตัวเอง เพิ่มเติมเกี่ยวกับเรื่องนี้ในภายหลัง
เอาต์พุตจากฟัซเซอร์?
ใน Fuzzer มีการดีบักเกอร์ (บางครั้ง) ที่แนบมากับแอปพลิเคชันเพื่อรับรายงานบางอย่างจากความผิดพลาดเพื่อให้สามารถวิเคราะห์ได้ในภายหลังเนื่องจากความปลอดภัย Vuln เทียบกับความผิดพลาดที่ไม่เป็นพิษเป็นภัย (แต่อาจสำคัญ)
วิธีการพิจารณาว่าโปรแกรมใดที่ดีที่สุดในการฟัซก่อน?
เมื่อฟัซซัสเราต้องการที่จะมีสมาธิกับชิ้นส่วนชิ้นเดียวหรือชุดชิ้นเล็ก ๆ ของโปรแกรม โดยปกติจะทำเพื่อลดปริมาณการดำเนินการที่จะทำ โดยปกติแล้วเรามุ่งเน้นไปที่การแยกวิเคราะห์และการประมวลผลเท่านั้น อีกครั้งขอบเขตความปลอดภัยมีความสำคัญ อย่างมาก ในการตัดสินใจว่าชิ้นส่วนใดมีความสำคัญต่อเรา
ประเภทของ Fuzzers?
ตัวอย่างอินพุตที่กำหนดให้กับฟัซเซอร์เรียกว่า คลังข้อมูล ใน Oldschool Fuzzers (aka "Blind"/"Dumb" Fuzzzers) มีความจำเป็นสำหรับคลังข้อมูลขนาดใหญ่ คนใหม่กว่า (aka "พันธุกรรม" ฟัซเซอร์เช่น AFL) ไม่จำเป็นต้องมีคลังข้อมูลขนาดใหญ่เช่นนี้เนื่องจากพวกเขาสำรวจรัฐด้วยตนเอง
Fuzzers มีประโยชน์อย่างไร?
Fuzzers ส่วนใหญ่มีประโยชน์สำหรับ "ผลไม้แขวนต่ำ" มันจะไม่พบข้อบกพร่องของตรรกะที่ซับซ้อน แต่สามารถหาข้อบกพร่องได้ง่าย (ซึ่งบางครั้งก็ง่ายที่จะพลาดในระหว่างการวิเคราะห์ด้วยตนเอง) ในขณะที่ฉันอาจพูด อินพุต ตลอดโน้ตนี้และมักจะอ้างถึง ไฟล์อินพุต แต่ก็ไม่จำเป็นต้องเป็นเพียงอย่างนั้น Fuzzers สามารถจัดการอินพุตที่อาจเป็น stdin หรืออินพุตไฟล์หรือซ็อกเก็ตเครือข่ายหรืออื่น ๆ อีกมากมาย เราสามารถคิดได้ว่ามันเป็นเพียงไฟล์ในตอนนี้
จะเขียนฟัซเซอร์ (พื้นฐาน) ได้อย่างไร?
อีกครั้งมันต้องเป็นห่วงการกลายพันธุ์ซ้ำซ้ำ เราต้องสามารถเรียกเป้าหมายได้บ่อยครั้ง ( subprocess.Popen ) นอกจากนี้เรายังต้องสามารถส่งอินพุตลงในโปรแกรม (เช่นไฟล์) และตรวจจับความผิดพลาด ( SIGSEGV ฯลฯ ทำให้เกิดข้อยกเว้นที่สามารถจับได้) ตอนนี้เราเพียงแค่ต้องเขียน mutator สำหรับไฟล์อินพุตและเรียกใช้เป้าหมายบนไฟล์กลายพันธุ์
Mutators? อะไร?!?
อาจมีหลาย mutators ที่เป็นไปได้ ง่าย (เช่นง่ายต่อการใช้งาน) อาจมีการกลายพันธุ์บิตไบต์กลายพันธุ์หรือกลายพันธุ์เป็นค่า "เวทมนตร์" เพื่อเพิ่มโอกาสของการชนแทนที่จะเปลี่ยนเพียง 1 บิตหรือบางสิ่งบางอย่างเราสามารถเปลี่ยนได้หลายอย่าง (อาจเป็นเปอร์เซ็นต์พารามิเตอร์บางส่วนของพวกเขา?) นอกจากนี้เรายังสามารถ (แทนที่จะกลายพันธุ์แบบสุ่ม) เปลี่ยนไบต์/คำ/dwords/etc เป็นค่า "เวทมนตร์" บางอย่าง ค่าเวทย์มนตร์อาจเป็น 0 , 0xff , 0xffff , 0xffffffff , 0x80000000 (32 บิต INT_MIN ), 0x7fffffff (32 บิต INT_MAX ) ฯลฯ เลือกที่พบบ่อยในการทำให้เกิดปัญหาด้านความปลอดภัย เราสามารถเขียน mutators ที่ชาญฉลาดหากเรารู้ข้อมูลเพิ่มเติมเกี่ยวกับโปรแกรม (ตัวอย่างเช่นสำหรับจำนวนเต็มที่ใช้สตริงเราอาจเขียนสิ่งที่เปลี่ยนสตริงจำนวนเต็มเป็น "65536" หรือ -1 ฯลฯ ) mutators ที่ใช้ก้อนอาจย้ายชิ้นส่วนไปรอบ ๆ (โดยทั่วไปการจัดระเบียบอินพุตใหม่) การกลายพันธุ์ของสารเติมแต่ง/ต่อท้ายยังใช้งานได้ (ตัวอย่างเช่นทำให้อินพุตที่ใหญ่ขึ้นลงในบัฟเฟอร์) ผู้ตัดทอนอาจใช้งานได้ (ตัวอย่างเช่นบางครั้ง EOF อาจไม่ได้รับการจัดการที่ดี) โดยพื้นฐานแล้วลองใช้วิธีการสร้างสรรค์สิ่งต่าง ๆ มากมาย ยิ่งมีประสบการณ์เกี่ยวกับโปรแกรม (และการแสวงประโยชน์โดยทั่วไป) ยิ่งมีการกลายพันธุ์ที่มีประโยชน์มากขึ้น
แต่สิ่งนี้คือ "พันธุกรรม" ฟัซซิ่ง?
นั่นอาจเป็นการสนทนาในภายหลัง อย่างไรก็ตามลิงก์สองสามลิงก์ไปยังฟัซเซอร์ที่ทันสมัย (โอเพ่นซอร์ส) คือ AFL และ Honggfuzz
เขียนเมื่อวันที่ 7 เมษายน 2017
ได้รับอิทธิพลจากความท้าทายที่ดีใน PICOCTF 2017 (ชื่อของความท้าทายถูกระงับเนื่องจากการแข่งขันยังคงดำเนินการอยู่)
คำเตือน: โน้ตนี้อาจดูง่าย/ชัดเจนสำหรับผู้อ่านบางคน แต่จำเป็นต้องพูดว่าเนื่องจากการฝังรากลึกไม่ได้ชัดเจนสำหรับฉันจนกระทั่งเมื่อไม่นานมานี้
แน่นอนเมื่อการเขียนโปรแกรมเราทุกคนใช้ abstractions ไม่ว่าจะเป็นคลาสและวัตถุหรือฟังก์ชั่นหรือฟังก์ชั่นเมตาหรือ polymorphism หรือ monads หรือ functors หรือแจ๊สทั้งหมด อย่างไรก็ตามเราสามารถมีเรื่องแบบนี้ได้หรือไม่? เห็นได้ชัดว่าเราสามารถใช้ประโยชน์จากความผิดพลาดที่เกิดขึ้นในการใช้นามธรรมดังกล่าว แต่ที่นี่ฉันกำลังพูดถึงสิ่งที่แตกต่าง
ใน CTF หลายรายการเมื่อใดก็ตามที่ฉันได้เขียนการหาประโยชน์ก่อนหน้านี้มันเป็นสคริปต์การหาประโยชน์แบบเฉพาะกิจที่ลดลงเชลล์ ฉันใช้ pwntools ที่น่าทึ่งเป็นกรอบ (สำหรับการเชื่อมต่อกับบริการและการแปลงสิ่งต่าง ๆ และ dynelf ฯลฯ ) แต่นั่นเกี่ยวกับมัน การหาประโยชน์แต่ละครั้งมีแนวโน้มที่จะเป็นวิธีการทำงานเฉพาะกิจในการทำงานเพื่อบรรลุเป้าหมายของการดำเนินการตามอำเภอใจ อย่างไรก็ตามความท้าทายในปัจจุบันนี้เช่นเดียวกับการคิดเกี่ยวกับบันทึกก่อนหน้าของฉันเกี่ยวกับการแสวงหาผลประโยชน์สตริง "ขั้นสูง" ทำให้ฉันรู้ว่าฉันสามารถเลเยอร์การหาประโยชน์ของฉันในวิธีที่สอดคล้องกันและย้ายผ่านเลเยอร์นามธรรมที่แตกต่างกันเพื่อบรรลุเป้าหมายที่จำเป็นในที่สุด
ตัวอย่างเช่นให้เราพิจารณาช่องโหว่ที่จะเป็นข้อผิดพลาดเชิงตรรกะซึ่งช่วยให้เราทำการอ่าน/เขียน 4 ไบต์บางแห่งในช่วงเล็ก ๆ หลังจาก บัฟเฟอร์ เราต้องการที่จะใช้ในทางที่ผิดไปตลอดทางเพื่อรับการดำเนินการรหัสและในที่สุดก็คือธง
ในสถานการณ์นี้ฉันจะพิจารณาสิ่งที่เป็นนามธรรมนี้ให้เป็นแบบดั้งเดิม short-distance-write-anything ด้วยตัวเองเห็นได้ชัดว่าเราไม่สามารถทำอะไรได้มาก อย่างไรก็ตามฉันทำฟังก์ชั่น Python ขนาดเล็ก vuln(offset, val) อย่างไรก็ตามเนื่องจากหลังจากบัฟเฟอร์อาจมีข้อมูล/เมตาดาต้าบางส่วนที่อาจเป็นประโยชน์เราสามารถใช้สิ่งนี้ในทางที่ผิดเพื่อสร้างทั้ง read-anywhere และ write-anything-anywhere ซึ่งหมายความว่าฉันเขียนฟังก์ชั่น Python สั้น ๆ ที่เรียกฟังก์ชัน vuln() ที่กำหนดไว้ก่อนหน้านี้ ฟังก์ชั่น get_mem(addr) และ set_mem(addr, val) เหล่านี้ทำขึ้นง่ายๆ (ในตัวอย่างปัจจุบันนี้) เพียงแค่ใช้ฟังก์ชั่น vuln() เพื่อเขียนทับตัวชี้ซึ่งสามารถตรวจสอบได้ที่อื่นในไบนารี
ตอนนี้หลังจากที่เรามี abstractions get_mem() และ set_mem() ฉันสร้าง anti-aslr abstraction โดยทั่วไปรั่ว 2 ที่อยู่ 2 ที่อยู่จาก Get ผ่าน get_mem() และเปรียบเทียบกับฐานข้อมูล LIBC (ขอบคุณ @niklasb สำหรับการสร้างฐานข้อมูล) ออฟเซ็ตจากสิ่งเหล่านี้ทำให้ฉันมี libc_base อย่างน่าเชื่อถือซึ่งช่วยให้ฉันสามารถแทนที่ฟังก์ชั่นใด ๆ ใน GOT กับอีกคนหนึ่งจาก LIBC
สิ่งนี้ทำให้ฉันสามารถควบคุม EIP ได้ (ช่วงเวลาที่ฉันสามารถ "ทริก เกอร์ " หนึ่งในฟังก์ชั่นเหล่านั้นเมื่อฉันต้องการ) ตอนนี้สิ่งที่เหลืออยู่สำหรับฉันที่จะเรียกทริกเกอร์ด้วยพารามิเตอร์ที่เหมาะสม ดังนั้นฉันจึงตั้งค่าพารามิเตอร์เป็นนามธรรมแยกต่างหากจากนั้นเรียก trigger() และฉันมีการเข้าถึงเชลล์บนระบบ
TL; DR: เราสามารถสร้างการแสวงหาผลประโยชน์ขนาดเล็ก (ซึ่งไม่มีพลังมากเกินไป) และโดยการรวมพวกเขาและการสร้างลำดับชั้นของดั้งเดิมที่แข็งแกร่งกว่าเราสามารถดำเนินการได้อย่างสมบูรณ์
เขียนเมื่อวันที่ 6 เมษายน 2017
ได้รับอิทธิพลจากสตรีมสดที่ยอดเยี่ยมนี้โดย Gynvael Coldwind ที่ซึ่งเขาพูดถึงการแสวงหาผลประโยชน์สตริงรูปแบบ
String String แบบง่าย ๆ การหาประโยชน์:
คุณสามารถใช้ %p เพื่อดูว่ามีอะไรอยู่บนสแต็ก หากสตริงรูปแบบตัวเองอยู่บนสแต็กหนึ่งสามารถวางที่อยู่ (พูด foo ) ลงบนสแต็กแล้วค้นหาโดยใช้ตัวระบุตำแหน่ง n$ (ตัวอย่างเช่น AAAA %7$p อาจส่งคืน AAAA 0x41414141 ถ้า 7 เป็นตำแหน่งบนกอง) We can then use this to build a read-where primitive, using the %s format specifier instead (for example, AAAA %7$s would return the value at the address 0x41414141, continuing the previous example). We can also use the %n format specifier to make it into a write-what-where primitive. Usually instead, we use %hhn (a glibc extension, iirc), which lets us write one byte at a time.
We use the above primitives to initially beat ASLR (if any) and then overwrite an entry in the GOT (say exit() or fflush() or ...) to then raise it to an arbitrary-eip-control primitive, which basically gives us arbitrary-code-execution .
Possible difficulties (that make it "advanced" exploitation):
If we have partial ASLR , then we can still use format strings and beat it, but this becomes much harder if we only have one-shot exploit (ie, our exploit needs to run instantaneously, and the addresses are randomized on each run, say). The way we would beat this is to use addresses that are already in the memory, and overwrite them partially (since ASLR affects only higher order bits). This way, we can gain reliability during execution.
If we have a read only .GOT section, then the "standard" attack of overwriting the GOT will not work. In this case, we look for alternative areas that can be overwritten (preferably function pointers). Some such areas are: __malloc_hook (see man page for the same), stdin 's vtable pointer to write or flush , etc. In such a scenario, having access to the libc sources is extremely useful. As for overwriting the __malloc_hook , it works even if the application doesn't call malloc , since it is calling printf (or similar), and internally, if we pass a width specifier greater than 64k (say %70000c ), then it will call malloc, and thus whatever address was specified at the global variable __malloc_hook .
If we have our format string buffer not on the stack , then we can still gain a write-what-where primitive, though it is a little more complex. First off, we need to stop using the position specifiers n$ , since if this is used, then printf internally copies the stack (which we will be modifying as we go along). Now, we find two pointers that point ahead into the stack itself, and use those to overwrite the lower order bytes of two further ahead pointing pointers on the stack, so that they now point to x+0 and x+2 where x is some location further ahead on the stack. Using these two overwrites, we are able to completely control the 4 bytes at x , and this becomes our where in the primitive. Now we just have to ignore more positions on the format string until we come to this point, and we have a write-what-where primitive.
Written on 1st April 2017
Influenced by this amazing live stream by Gynvael Coldwind, where he explains about race conditions
If a memory region (or file or any other resource) is accessed twice with the assumption that it would remain same, but due to switching of threads, we are able to change the value, we have a race condition.
Most common kind is a TOCTTOU (Time-of-check to Time-of-use), where a variable (or file or any other resource) is first checked for some value, and if a certain condition for it passes, then it is used. In this case, we can attack it by continuously "spamming" this check in one thread, and in another thread, continuously "flipping" it so that due to randomness, we might be able to get a flip in the middle of the "window-of-opportunity" which is the (short) timeframe between the check and the use.
Usually the window-of-opportunity might be very small. We can use multiple tricks in order to increase this window of opportunity by a factor of 3x or even up to ~100x. We do this by controlling how the value is being cached, or paged. If a value (let's say a long int ) is not aligned to a cache line, then 2 cache lines might need to be accessed and this causes a delay for the same instruction to execute. Alternatively, breaking alignment on a page, (ie, placing it across a page boundary) can cause a much larger time to access. This might give us higher chance of the race condition being triggered.
Smarter ways exist to improve this race condition situation (such as clearing TLB etc, but these might not even be necessary sometimes).
Race conditions can be used, in (possibly) their extreme case, to get ring0 code execution (which is "higher than root", since it is kernel mode execution).
It is possible to find race conditions "automatically" by building tools/plugins on top of architecture emulators. For further details, http://vexillium.org/pub/005.html
Written on 31st Mar 2017
Influenced by this amazing live stream by Gynvael Coldwind, where he is experimenting on the heap
Use-after-free:
Let us say we have a bunch of pointers to a place in heap, and it is freed without making sure that all of those pointers are updated. This would leave a few dangling pointers into free'd space. This is exploitable by usually making another allocation of different type into the same region, such that you control different areas, and then you can abuse this to gain (possibly) arbitrary code execution.
Double-free:
Free up a memory region, and the free it again. If you can do this, you can take control by controlling the internal structures used by malloc. This can get complicated, compared to use-after-free, so preferably use that one if possible.
Classic buffer overflow on the heap (heap-overflow):
If you can write beyond the allocated memory, then you can start to write into the malloc's internal structures of the next malloc'd block, and by controlling what internal values get overwritten, you can usually gain a read-what-where primitive, that can usually be abused to gain higher levels of access (usually arbitrary code execution, via the GOT PLT , or __fini_array__ or similar).