לדלג לתוכן

3.1 כתיבת shellcode ידני תרגול

תרגול - כתיבת shellcode ידני

בתרגול הזה תכתבו shellcode של execve("/bin/sh", 0, 0) בשתי הארכיטקטורות, תוודאו שהוא נקי מבתים אפס, ותוכיחו שהוא באמת פותח shell דרך טוען. אל תעתיקו את הפתרון - כתבו את האסמבלי בעצמכם. המטרה היא שהאצבעות שלכם ידעו לכתוב את זה בשינה, כי בהמשך הקורס תעשו את זה עשרות פעמים.

סביבה והנחות

כל התרגילים רצים מקומית על לינוקס (או מכונת אימון כמו של pwnable.kr). ההנחות שלנו: אין ASLR שמפריע, ואנחנו מריצים את ה-shellcode מתוך אזור זיכרון ניתן להרצה (עמוד RWX או מחסנית עם execstack). כלומר, אנחנו מדמים סביבה בלי NX פעיל - בדיוק הסביבה שבה shellcode רלוונטי.

הכלים שתצטרכו: python3 עם pwntools מותקן, nasm, gcc (כולל תמיכת 32 ביט: sudo apt install gcc-multilib), ו-objdump/xxd.

טוען לבדיקה (שמרו בשם loader.c, ומחליפים את מערך הבתים בשלכם):

#include <string.h>
#include <sys/mman.h>

unsigned char sc[] =
    "\x90\x90\x90";   // <-- paste the bytes of your shellcode here

int main(void) {
    void *page = mmap(0, 4096, PROT_READ | PROT_WRITE | PROT_EXEC,
                      MAP_ANON | MAP_PRIVATE, -1, 0);
    memcpy(page, sc, sizeof(sc));
    ((void (*)(void))page)();
    return 0;
}

תרגיל 1 - shellcode ל-x86-64

כתבו קובץ אסמבלי שמריץ execve("/bin/sh", 0, 0) ב-x86-64, הרכיבו אותו עם pwntools, וודאו שהוא נקי מבתים אפס.

המטרה:

  1. כתבו את האסמבלי שמאפס את rdx ו-rsi, בונה את "/bin//sh" על המחסנית, מכניס את הכתובת ל-rdi, שם rax = 59, ומריץ syscall.
  2. הרכיבו עם asm() והדפיסו את האורך ואת ה-hex.
  3. הוסיפו assert b'\x00' not in sc. אם הוא נכשל - תקנו את ההוראה שמכניסה את האפס.

רמז: אל תשתמשו ב-mov rax, 59 - היא מייצרת בתים אפס. השתמשו ב-xor eax, eax ואז mov al, 59.

רמז: כדי לדחוף 64 ביט למחסנית צריך קודם לטעון אותם לרגיסטר (mov rbx, ...) ורק אז push rbx. הוראת push לא מקבלת מיידי של 64 ביט.

רמז: המחרוזת "/bin//sh" היא הערך 0x68732f2f6e69622f. ודאו שהבית התחתון הוא 0x2f (התו /).

תרגיל 2 - shellcode ל-x86-32

עכשיו אותו דבר, אבל ל-x86-32. שימו לב שהמספרים והמנגנון שונים.

המטרה:

  1. קבעו context.arch = 'i386' והרכיבו shellcode של 32 ביט.
  2. השתמשו ב-int 0x80 ובמספר הקריאה הנכון ל-32 ביט.
  3. שוב, ודאו שאין בתים אפס.

רמז: מספר הקריאה של execve ב-32 ביט הוא 11, לא 59. הרגיסטרים הם ebx (נתיב), ecx (argv), edx (envp).

רמז: כל דחיפה היא 4 בתים, אז חלקו את "/bin//sh" לשניים ודחפו בסדר הפוך: קודם "//sh" ואז "/bin".

רמז: ההוראה mov al, 11 עובדת רק אם אתם כבר יודעים ש-eax אופס קודם. תסתכלו מתי אתם עושים xor eax, eax.

תרגיל 3 - הרכבה עם nasm וטוען ב-C

עד עכשיו הרכבתם עם pwntools. עכשיו תעשו את זה גם עם nasm, ותבדקו את ה-shellcode דרך הטוען ב-C.

המטרה:

  1. כתבו את ה-shellcode של 64 ביט בקובץ sh64.asm (עם BITS 64, global _start, section .text).
  2. הרכיבו עם nasm -f elf64, חלצו את הבתים של .text, והדביקו אותם למערך sc[] בטוען.
  3. קמפלו את הטוען עם gcc והריצו. הוכיחו שנפתח shell - הריצו id ו-ls.
  4. חזרו על אותו תהליך ל-32 ביט: nasm -f elf32, וקומפילציה של הטוען עם gcc -m32.

רמז: כדי לחלץ רק את הבתים: objcopy -O binary -j .text sh64.o sh64.bin, ואז xxd sh64.bin כדי לראות אותם.

רמז: כדי להפוך קובץ בינארי לפורמט \xNN מוכן להדבקה, נסו: python3 -c "print(''.join('\\\\x%02x'%b for b in open('sh64.bin','rb').read()))".

רמז: אם הטוען קורס במקום לפתוח shell, פרקו אותו עם objdump -d sh64.o וודאו שההוראות הן מה שהתכוונתם, ושהעתקתם את כל הבתים בלי לפספס.

תרגיל 4 (אתגר) - קיצור והשוואה

עכשיו כשיש לכם shellcode עובד, בואו נשווה אותו למקצועני.

המטרה:

  1. מדדו את האורך המדויק של ה-shellcode שלכם בשתי הארכיטקטורות (len(sc)).
  2. ייצרו את ה-shellcode של pwntools עם shellcraft (asm(shellcraft.amd64.linux.sh()) ו-asm(shellcraft.i386.linux.sh())) והשוו אורכים.
  3. פרקו את שתי הגרסאות עם disasm(sc) והסבירו את ההבדלים. האם pwntools עושה משהו חכם יותר? האם יש הוראות מיותרות אצלכם?
  4. נסו לקצר את ה-shellcode שלכם ולו בבית אחד, בלי להכניס בית אפס.

רמז: shellcraft לפעמים משתמש ב-cdq (בית אחד) כדי לאפס את edx כשהוא כבר יודע שהסימן של eax הוא 0. שווה להכיר את הטריק הזה.

רמז: אחרי כל שינוי, אל תשכחו להריץ שוב את assert b'\x00' not in sc ולבדוק בטוען שה-shell עדיין נפתח. shellcode קצר שלא עובד שווה פחות מ-shellcode ארוך שכן.