พูดง่ายๆคือฉันเบื่อวันหนึ่งและถามเพื่อนของฉัน Maximus Hackerman เพื่อให้ฉันทำอะไรบางอย่าง ทันทีที่เขาส่งผ่านการปฏิบัติการง่าย ๆ ที่เรียกว่า "Reverseme.exe" (ลิงค์ virustotal) ด้วยความคิดเห็นง่ายๆของ "พิมพ์ข้อความที่ซ่อนอยู่ในแอปพลิเคชันของคุณเอง" โดยธรรมชาติแล้วไฟล์ส่วนหัวของ CPP ไม่ได้ให้ไว้ เมื่อเรียกใช้งานได้มันจะเปิดเป็นแอปพลิเคชันที่ใช้คอนโซลพิมพ์ "ส่งแพ็คเก็ตวิเศษทั้งหมดออกเร็ว ๆ นี้ Beep Boop!" แล้วออกไปเกือบจะทันที ฉันเดาว่า "เร็ว ๆ นี้" เป็นเรื่องส่วนตัว
โชคดีที่ดูเหมือนจะไม่ต่อต้านการเบ็ดดังนั้นฉันเดาว่าแฮ็กเกอร์แมนรู้สึกดีในวันนั้น
หลังจากติด Windbg และ Disassembler เราจะเห็นได้ว่าแอปพลิเคชันพุ่งขึ้นสิ่งที่ปรากฏในตอนแรกดูเหมือนจะเป็นการ divide by zero
อย่างไรก็ตามในการตรวจสอบอย่างใกล้ชิดเราพบว่าข้อยกเว้นนี้ถูกโยนลงไปก่อนที่แอปพลิเคชันจะพิมพ์ข้อความที่มองเห็นได้เท่านั้น เป็นสิ่งสำคัญที่จะต้องทราบว่ายานพาหนะสองคัน (ตัวจัดการข้อยกเว้น Vectored) ได้รับการลงทะเบียนเล็กน้อยก่อนถึงจุดนี้ดังนั้นจึงเป็นไปได้ว่าข้อยกเว้นนี้เป็นความตั้งใจและใช้ในการจัดการกระแสการควบคุม เคล็ดลับราคาถูกที่จะลองและทิ้งเราจริงๆ!
ลืมยานพาหนะในขณะนี้มันปลอดภัยที่จะสมมติว่าหากแอปพลิเคชันเป็นแพ็คเก็ต“ ส่ง” จากนั้นก็ใช้ฟังก์ชั่นการส่ง WinSock เพื่อทำเช่นนั้น (แม้ว่าแอปพลิเคชันจะบอกว่าพวกเขาเป็นแพ็คเก็ต“ เวทมนตร์” ดังนั้นเราจะเห็น)
ตามที่คาดไว้ปรากฎว่าคุณไม่สามารถส่งแพ็กเก็ตด้วยเวทมนตร์ได้ดังนั้นฟังก์ชั่น send WinSock จึงถูกนำเข้าอย่างแน่นอน
ด้วยการวางจุดพักบนฟังก์ชั่นการส่งเราสามารถดูและดูว่ามีข้อมูลที่เกี่ยวข้องใด ๆ ที่เราสามารถเป็นนามธรรมในเวลาที่ส่งได้หรือไม่ น่าเศร้าเมื่อถึงเวลาที่บัฟเฟอร์ถูกโหลดลงในฟังก์ชั่นข้อมูลจะถูกเข้ารหัสแล้ว อย่างไรก็ตามเราสามารถกำหนดได้ว่าบัฟเฟอร์มีความยาว 3 ไบต์เสมอซ็อกเก็ตจะถูกผูกไว้กับค่าฮาร์ดโค้ดเสมอที่ 0x69 ( ดี ) และจุดพักฟังก์ชั่นการส่งจะถูกตีทั้งหมด 14 เท่า
มีหลายวิธีในการดำเนินการเกี่ยวกับการเข้ารหัสดังกล่าว หนึ่งคือการย้อนกลับทั้งหมดซึ่งอาจกลายเป็นความพยายามอย่างมากอีกอย่างหนึ่งคือการค้นหาข้อมูลที่ต้องการก่อนการเข้ารหัสและเป็นนามธรรมก่อนที่มันจะเข้ารหัส หลังนั้นง่ายกว่าอดีตอย่างมากดังนั้นเราจึงไปกับสิ่งนั้น อย่าลังเลที่จะย้อนกลับการเข้ารหัสแม้ว่าฉันจะดูสั้น ๆ และมันก็ไม่น่ากลัว อีกทางเลือกหนึ่งหากคุณรู้สึกว่าแฟนซีคุณสามารถเขียนทับซ็อกเก็ตที่มีค่าได้เสมอและมีข้อตกลงแอปพลิเคชันที่ได้รับ
น่าเศร้าที่ไม่มีสตริงที่มีประโยชน์ในเซ็กเมนต์ข้อมูลแบบอ่านอย่างเดียวนอกเหนือจากข้อความเริ่มต้นดังนั้นจึงไม่มีตัวชี้ที่เป็นประโยชน์จากด้านนั้น!
กลับไปที่ยานพาหนะเราจะเห็นว่าตัวจัดการที่ลงทะเบียนทั้งสองใช้เพื่อคัดลอกวัตถุที่ไม่รู้จักสองสามตัวลงในบัฟเฟอร์หน่วยความจำที่จัดสรร สิ่งนี้ดูเหมือนจะทำโดยใช้ memcpy โดยใช้ฟังก์ชันเป็นพารามิเตอร์ที่สองซึ่งจะใช้จำนวนเต็ม hardcoded เป็นพารามิเตอร์ที่สอง เป็นที่น่าสังเกตว่าค่า hardcoded เหล่านี้ไม่เกิน 14 ณ จุดใด ๆ ฉันได้รวมยานพาหนะหนึ่งตัวไว้ในภาพหน้าจอเนื่องจากเป็นรหัสที่เหมือนกันโดยทั่วไปยกเว้นค่าฮาร์ดโค้ด
โดยการแบ่งที่หนึ่งในฟังก์ชั่น memcpy และตรวจสอบฟังก์ชั่นย่อยในพารามิเตอร์ที่สองเราจะเห็นได้ว่าจำนวนเต็ม hardcoded (13 / 0dh ในตัวอย่างด้านล่าง) ถูกตั้งค่าเป็นไบต์แรกของพารามิเตอร์ A1 และ A1+1 มีตัวละครหลังจากตัวชี้
หากเราตรวจสอบการโทรอีก 14 รายการไปยังฟังก์ชั่นนี้เราสามารถค้นหาพฤติกรรมที่เหมือนกันซ้ำได้ การจัดเรียงอักขระจาก 1 ถึง 14 โดยใช้หมายเลขที่เกี่ยวข้องเป็นตัวบ่งชี้การสั่งซื้อเราจะเห็นว่าพวกเขาเริ่มสะกดคำที่ชัดเจน ตอนนี้เราอาจขี้เกียจสุด ๆ และเพียงแค่สร้างแอพคอนโซลเพื่อพิมพ์ข้อความออกมาเมื่อเรารู้ว่ามันคืออะไร แต่นั่นรู้สึกเหมือนโกงจริงๆ นอกจากนี้ข้อความอาจเปลี่ยนแปลงได้ในบางจุด ดังนั้นเรามาเขียนถ้ำรหัสเพื่อสกัดกั้นค่าก่อนที่จะเข้ารหัส
ขออภัยเราใช้ C ++ สำหรับสิ่งนี้! หากคุณต้องการใช้ C#อย่าลังเลที่จะผ่านกระบวนการเรียกใช้แพลตฟอร์มทั้งหมดสำหรับฟังก์ชั่น 9.7 ล้านฟังก์ชั่นและกลับมาที่นี่เมื่อคุณทำเสร็จแล้ว อย่างไรก็ตามก่อนอื่นเราต้องตัดสินใจว่าจะใช้รหัสถ้ำที่ไหน โชคดีที่เรารู้อยู่แล้ว! โดยการวิเคราะห์ฟังก์ชั่นข้างต้นอย่างไรก็ตามสั้น ๆ เรารู้ว่าโดยจุดที่ไฮไลต์ RDX มีดัชนีและ RDX+1 มีอักขระที่สอดคล้องกัน ด้านล่างคือรหัสแอสเซมบลีสำหรับฟังก์ชั่นที่กล่าวถึง
ตอนนี้การพูดอย่างมีเหตุผลสถานที่ที่ดีที่สุดในการกระโดดจากจะอยู่ที่ mov [rsp+arg_8], rdx เนื่องจากเราไม่สนใจพารามิเตอร์ที่สาม แต่ต้องการสกัดกั้น RDX ลงทะเบียนและ RDX+1 ในการทำเด็กคนนี้เราจะต้องใช้ไบต์สองสามไบต์: 10 ไบต์สำหรับคำสั่ง MOV เพื่อย้ายที่อยู่ของถ้ำรหัสของเราไปยังการลงทะเบียน (เราจะใช้ RAX เพิ่มเติมในช่วงเวลา 10 นาฬิกา) และ 2 ไบต์สำหรับคำสั่ง JMP สำหรับพวกคุณที่มีพล็อตจากคณิตศาสตร์เพิ่มเติมทั้งหมดที่ 12 ไบต์ ตอนนี้ก่อนที่เราจะไปป้า Bessie และ Spaghettify รหัสนั้นด้วย WriteProcessMemory เราต้องพิจารณาว่าไม่มีสถานที่ที่เหมาะสำหรับการแทนที่ 12 ไบต์ในแอสเซมบลีข้างต้น หากเราต้องการกระโดดจากออฟเซ็ต 0x2905 ( mov [rsp+arg_8], rdx ) และเราต้องการ 12 ไบต์เพื่อทำเช่นนั้นนั่นจะพาเราไปชดเชย 0x2917 ซึ่งตบระหว่างคำแนะนำ MOV สองคำแนะนำ น่าเสียดายที่ถ้าเราเพียงแค่เขียนไบต์ของเราไปที่นั่นมันจะทำให้การชุมนุมอย่างสมบูรณ์และน่าจะทำให้เกิดผลข้างเคียงที่น่าสนใจ "น่าสนใจ" เป็นผลให้มันจะง่ายขึ้น (อาจจะแฮ็คมากกว่านี้อีกเล็กน้อยขออภัยไม่เสียใจ) เพื่อเพิ่มคำแนะนำแบบไบต์สำหรับช่องว่างภายในและปัดเศษไปยังจุดสิ้นสุดของคำสั่ง ยินดีต้อนรับบนเรือ, 0x90
อย่างไรก็ตามตอนนี้เรารู้ว่าแผนของเราคืออะไรดังนั้นเรามาเขียนโค้ด: เพลงแฮ็กเกอร์ที่เข้มข้นของคิว !
ด้านล่างคือสิ่งที่กระโดดเริ่มต้นไปยังถ้ำรหัสของเราจะดูเหมือนเมื่อไบต์ถูกเขียนลงในแอปพลิเคชันของแอปพลิเคชันพร้อมด้วยสไลด์ NOP ของตัวเอง
อย่างไรก็ตามก่อนที่เราจะเขียนและแทนที่แอสเซมบลีใด ๆ เราจำเป็นต้องเริ่มต้นแอปพลิเคชันในสถานะที่ถูกระงับ สิ่งนี้จะหยุดรันไทม์แอปพลิเคชันในระยะแรกเพื่อให้การเปลี่ยนแปลงหน่วยความจำสามารถทำได้ก่อนที่แอปพลิเคชันจะได้รับคำสั่งที่เราสนใจตัวดึงรหัสด้านล่างแสดงกระบวนการนี้และฉันจะไม่ผ่านมันมากเกินไปเท่าที่คุณสามารถดูได้ในไฟล์ต้นฉบับของ repo นี้และส่วนใหญ่พูดด้วยตัวเอง
ตอนนี้กระบวนการวางไข่เราจะต้องได้รับที่อยู่พื้นฐานของกระบวนการ โดยปกติคุณสามารถใช้ enumprocessmodules สำหรับสิ่งนี้ แต่ในขณะที่เราระงับเธรดกระบวนการหลักทันที PEB จะไม่มีโครงสร้าง PEB_LDR_DATA ที่มีประชากรทั้งหมดโดยเฉพาะอย่างยิ่ง InMemoryOrderModuleList ดังนั้นเราจึงไม่สามารถรับที่อยู่พื้นฐานได้ในขณะนี้ โดยวิธีการนี้ไม่ปรากฏว่ามีการบันทึกไว้ที่ใดก็ได้ใน MSDN โชคดีที่นี่เป็นเรื่องง่ายที่จะหลีกเลี่ยง โดยการกลับมาดำเนินการอย่างรวดเร็วการสอบถามโมดูลและจากนั้นกลับกระบวนการใหม่เราสามารถรับข้อมูลที่เราต้องการได้โดยไม่ต้องดำเนินการมากเกินไป เช่นเดียวกับสิ่งต่าง ๆ ใน Windows Microsoft ชอบที่จะย้ำว่าระบบปฏิบัติการเป็นระบบที่เหนือกว่าอีกครั้งโดยไม่บันทึกฟังก์ชั่นที่เราต้องการ: NtSuspendProcess และ NtResumeProcess สะดวกพ่อของฉันเป็นเพื่อนกับบิลเกตส์และเขาบอกฉันว่าฟังก์ชั่นเหล่านี้มีชีวิตอยู่คู่ใน ntdll.dll ดังนั้นเราจึงสามารถดึงพวกเขาได้โดยใช้คลาสด้านล่างที่ฉันทำก่อนหน้านี้:
ตอนนี้เรามีฟังก์ชั่นสองอย่างที่เราต้องการเราสามารถกลับมาใช้กระบวนการสอบถามโมดูลและกลับกระบวนการอีกครั้ง:
คุณอาจสงสัยว่าทำไมในขณะที่ลูปรอการค้นพบโมดูลสองโมดูลเมื่อเทียบกับหนึ่ง? Microsoft กำลังมองหาคะแนน Hattrick ด้วยลูกชิ้นที่ด้านหลังของสุทธิสปาเก็ตตี้ที่ไม่มีเอกสารโดยไม่ได้กล่าวถึงว่าโมดูลแรกที่พบโดย EnumProcessModules จะเป็น NTDLL.DLL และที่สองจะได้รับการปฏิบัติการ ในขณะที่ฟังดูสมเหตุสมผลเมื่อพบการดำเนินการแล้วมันจะเปลี่ยนดัชนีด้วย ntdll.dll นี่คือตัวอย่าง:
ผลลัพธ์หลังจากสอบถามเฉพาะโมดูลแรก:
ผลลัพธ์หลังจากสอบถามสองโมดูล:
ก่อนที่เราจะไปไกลกว่านี้เราต้องเขียนรหัสแอสเซมบลีที่กระโดดเริ่มต้นไปยังถ้ำรหัสและถ้ำรหัสเอง โดยพื้นฐานแล้วเราจะเขียนทับหน่วยความจำบางอย่างเริ่มต้นที่ออฟเซ็ต 0x2905 , กระโดดข้าม, การสอดแนมรหัสของเราแล้วกระโดดกลับไปที่ 0x2911 เพื่อดำเนินการต่อการไหลของโปรแกรมปกติ ในขั้นต้นเราประกาศการย้ายที่อยู่ไปยัง RAX register เป็น MOV RAX, 0x0 เนื่องจากที่อยู่ที่เราจะข้ามไปนั้นเป็นแบบไดนามิกและเรายังไม่รู้ว่ามันคืออะไร RAX เป็นทะเบียนที่ปลอดภัยที่จะใช้เนื่องจากมีการลงทะเบียนที่ผันผวนและถูกเขียนทับไม่นานหลังจากนั้น ความจริงสนุกนี่คือวิธีที่ Nintendo ตั้งโปรแกรม Platformer Mario Bros ดั้งเดิมด้วยการกระโดดมากมาย (บอกฉันว่าฉันตลก)! ด้านล่างเป็นวิธีที่รหัสดูในคอมไพเลอร์; มันสามารถสร้างได้ในรูปแบบอื่น แต่ฉันเลือกที่จะเผยแพร่คำแนะนำการประกอบที่จำเป็นลงใน bytecode หากคุณต้องการทำสิ่งนี้ที่บ้านให้ใช้เว็บไซต์นี้
รหัสสำหรับถ้ำรหัสจริงนั้นซับซ้อนกว่าเล็กน้อยและตรรกะสำหรับมันก็แสดงความคิดเห็นในไฟล์นี้ แต่นี่คือกระบวนการคร่าวๆ:
R10RDX ซึ่งมีดัชนีแพ็กเก็ตไปยัง R11BRDX ซึ่งมีตัวละครไปยัง R11B+12 ) เป็น R10 (ซึ่งเป็น 0x2911 )R10 นอกจากนี้เรายังต้องเขียนโค้ดแอสเซมบลีใหม่ที่เราเขียนทับ (ด้วยสไลด์ NOP) ลงในถ้ำรหัสของเราเพื่อรักษาสแต็ก ฯลฯ ; รหัสนี้มีการอ้างอิงในภูมิภาค“ Predetermined Assembly ล่วงหน้า” ไม่รวม NOPS ด้านล่างนี้เป็นถ้ำรหัสในความรุ่งโรจน์ที่น่าเกลียด:ส่วนที่ยากส่วนใหญ่
ณ จุดนี้เรามีทุกสิ่งที่เราต้องการเพื่อเติมข้อความที่ซ่อนอยู่จากหน่วยความจำเราเพียงแค่ต้องใช้มัน ในการสรุปเรามีการจัดการกับกระบวนการระงับที่เราวางไข่อาร์เรย์ 2 ไบต์ที่แสดงถึงตรรกะแอสเซมบลีที่อยู่พื้นฐานของกระบวนการระงับจัดเก็บไว้ใน modules[0] และชดเชยที่เราต้องเขียนตรรกะการกระโดด ตัวอย่างโค้ดด้านล่างสร้างที่อยู่ที่จะกระโดดจากที่อยู่ของถ้ำรหัส (เพื่อข้ามไป) ที่อยู่ของที่เก็บหน่วยความจำ 3 ไบต์ของเราเขียนที่อยู่ลงในรหัสแอสเซมบลีแล้วเขียนรหัสแอสเซมบลีกระบวนการระงับ
การคัดเลือกนักแสดงสไตล์แฮร์รี่พอตเตอร์ที่มีมนต์ขลังสำหรับ codeCaveStorageAddr คือการแปลงที่อยู่เป็นไบต์และค่า hardcoded ในลูปสำหรับตำแหน่งที่อยู่แบบไดนามิกในอาร์เรย์แอสเซมบลี แน่นอนว่าสิ่งนี้สามารถทำได้ในลักษณะที่สะอาดกว่ามาก (อย่าเขียนหมายเลข Magic Numbers Kids) ฉันแค่ขี้เกียจหลังจากต้องเขียนไบต์ทั้งหมดด้วยตนเอง บิตสองสามบิตสุดท้ายคือลูปที่จะอ่านโดยใช้ ReadProcessMemory, หน่วยความจำ, เก็บอักขระและดัชนีลงในอาร์เรย์ไบต์ที่สั่งซื้อส่งสัญญาณไบต์โดยใช้ WriteProcessMemory เมื่อการอ่านหน่วยความจำปัจจุบันเสร็จสิ้นและพิมพ์ข้อความที่ซ่อนอยู่ เรารู้ว่าฟังก์ชั่นการส่งเกิดขึ้นเพียง 14 ครั้งดังนั้นเราจึงออกจากช่วงเวลาที่วนรอบเมื่อเต็มอาร์เรย์ไบต์ สิ่งนี้สามารถเปลี่ยนแปลงได้ด้วยการแก้ไขหน่วยความจำมากขึ้นเพื่อส่งสัญญาณไปยังแอปพลิเคชันของเราว่ากระบวนการ“ ออกจาก” และสามารถหยุดลูปของเราได้แทนที่จะใช้ค่าฮาร์ดโค้ดที่ 14 แต่ใช้ได้กับตัวอย่างนี้
ผลลัพธ์? ข้อความที่ซ่อนอยู่ของเราถูกพิมพ์ในแอปพลิเคชันคอนโซลของเราเอง! หากใครมีความอยากรู้อยากเห็น NPT เป็นการอ้างอิงถึงซอฟต์แวร์ชิ้นส่วนสูงสุดของแฮ็กเกอร์-อะไรก็ตามที่ฉันเรียกว่าเขาเขียน