0.2 סביבת העבודה הרצאה
בפרק הקודם הבנו מה זה מחקר חולשות ולמה זה מקצוע שלם. עכשיו, לפני שנתקוף את האתגר הראשון, צריך לבנות שולחן עבודה מסודר. חוקר חולשות בלי סביבה טובה הוא כמו נגר בלי ארגז כלים - אפשר לעבוד, אבל זה כואב וזה איטי. בהרצאה הזו נקים סביבת pwn שלמה: מכונה וירטואלית, מהדר ומנפה, תוסף gdb שנבנה בדיוק בשביל התחום שלנו, הספרייה pwntools, וכל שאר הכלים שילוו אותנו לאורך כל הקורס. בסוף ההרצאה יהיה לכם שולחן עבודה שאפשר לפתוח מולו כל בינארי ולהתחיל לפרק אותו.
חשוב לומר מראש: כל הכלים כאן רצים על לינוקס. אנחנו תוקפים בינאריים של ELF, והמקום הטבעי לעשות את זה הוא לינוקס עצמו. אל תנסו לעבוד מ-Windows עם WSL בהתחלה - זה אפשרי, אבל יש הבדלים עדינים בכתובות, ב-ASLR ובגרסת ה-libc שיבלבלו אתכם דווקא כשאתם לומדים. תתחילו נקי.
למה מכונה וירטואלית - Virtual Machine¶
הכלל הראשון: לא עובדים על המערכת האמיתית שלכם. אנחנו הולכים להוריד בינאריים לא מוכרים, להריץ shellcode, ולפעמים לגרום לתהליכים להתנהג בצורה מוזרה. כל זה צריך לקרות בתוך ארגז חול מבודד.
מכונה וירטואלית נותנת לנו שלושה דברים חשובים:
- בידוד - אם משהו משתבש, זה נשאר בתוך ה-VM ולא נוגע במחשב המארח.
- צילומי מצב - snapshots. אפשר לצלם את המצב של המכונה אחרי שהתקנו הכל, ואם בהמשך נשבור את הסביבה - חוזרים לצילום בלחיצת כפתור.
- סביבה עקבית - כולם בקורס עובדים על אותה גרסת מערכת, אותו gcc ואותה glibc, כך שכתובות ותוצאות יוצאות דומות.
איזו הפצה לבחור - distro¶
שתי אפשרויות טובות:
- הפצת Ubuntu (מומלץ למתחילים) - יציבה, נפוצה, וקל למצוא עליה גרסאות glibc מוכרות. הרבה אתגרי pwn נבנו מול גרסאות glibc של Ubuntu, אז זו התאמה נוחה.
- הפצת Kali - מגיעה עם המון כלי אבטחה מותקנים מראש, כולל Ghidra ו-radare2. אם אתם כבר מכירים אותה, מצוין.
לאורך הקורס נניח Ubuntu 22.04 בגרסת 64 ביט. אתם יכולים להשתמש ב-VirtualBox או ב-VMware, שניהם חינמיים למטרות לימוד. הקצו למכונה לפחות 4GB זיכרון ו-40GB דיסק, כי Ghidra והידור בינאריים אוהבים מקום.
נקודה קריטית: גרסת glibc¶
זכרו את המשפט הזה, הוא ילווה אותנו כל הקורס: הכתובות והתנהגות ה-heap תלויות בגרסת ה-glibc. שני מחשבים עם אותה תוכנית בדיוק אבל גרסת glibc שונה יכולים להתנהג אחרת לגמרי בזמן exploit. לכן חשוב לדעת בכל רגע איזו גרסת glibc יש לכם:
הפקודה השנייה, הרצה של libc כאילו הוא תוכנית, מדפיסה את מספר הגרסה המדויק. נחזור לזה בהמשך ההרצאה כשנדבר על אתגרים מרוחקים.
הבסיס: מהדר ומנפה - gcc ו-gdb¶
נתחיל מהכלים שאתם כבר מכירים מקורס הליבה. נעדכן את רשימת החבילות ונתקין את סביבת הבנייה הבסיסית:
sudo apt update
sudo apt install -y build-essential gdb gcc-multilib git python3 python3-pip python3-venv
מה קיבלנו כאן:
- החבילה build-essential מביאה את gcc, make ואת הכותרות של libc.
- החבילה gcc-multilib מאפשרת להדר גם ל-32 ביט עם
-m32. נזדקק לזה הרבה, כי אתגרים רבים הם 32 ביט וקל יותר ללמוד עליהם ROP. - המנפה gdb הוא הכלי שנחיה בתוכו. אבל gdb ברירת המחדל צנום מדי לצרכים שלנו, ומיד נשדרג אותו.
בדיקה מהירה שהכל עובד:
תוסף gdb לexploit - pwndbg¶
עכשיו לחלק הכי משמעותי בהקמת הסביבה. gdb הרגיל מציג לכם מצב מינימלי: אתם עוצרים בנקודת עצירה ורואים... כלום, עד שתבקשו במפורש כל אוגר וכל כתובת. בשביל exploit חולשות זה מייגע ואיטי.
תוסף pwntools-oriented ל-gdb משנה את החוויה מהקצה לקצה. בכל עצירה הוא מציג לכם אוטומטית:
- מצב כל האוגרים, עם פענוח חכם של מה שהם מצביעים עליו.
- תצוגת המחסנית (telescope) שהולכת אחרי מצביעים ומראה מה יש בכל שכבה.
- פירוק ההוראות סביב מצביע ההרצה הנוכחי.
- מפת הזיכרון של התהליך, ההרשאות של כל אזור, ומצב ההגנות.
שני התוספים הפופולריים הם pwndbg ו-GEF. שניהם מצוינים. בקורס נשתמש ב-pwndbg, אבל אם אתם כבר רגילים ל-GEF - הישארו איתו, העקרונות זהים.
התקנה של pwndbg:
הסקריפט מוסיף שורת טעינה לקובץ ~/.gdbinit, כך שבכל הפעלה של gdb התוסף נטען אוטומטית. מי שמעדיף GEF יכול במקום זאת להריץ:
שימו לב: אל תתקינו את שניהם באותו ~/.gdbinit, הם יתנגשו. בחרו אחד.
בדיקה שהתוסף נטען: פתחו gdb על תוכנית כלשהי והריצו checksec. אם אתם מקבלים טבלת הגנות מסודרת במקום שגיאה, התוסף חי. בתוך pwndbg יש לנו פקודות שיהפכו למטבע העובר: context להצגת המצב, vmmap למפת הזיכרון, telescope לקריאת המחסנית, cyclic ליצירת דפוס, ו-heap / bins לניתוח ה-heap כשנגיע לשם.
הספרייה pwntools¶
הספרייה pwntools היא ספריית פייתון שהיא בעצם ה-framework המרכזי של כתיבת exploits. במקום להתעסק ידנית עם sockets, אריזת מספרים ותקשורת עם התהליך, אנחנו כותבים כמה שורות נקיות והיא עושה את העבודה השחורה. נשתמש בה כמעט בכל אתגר בקורס.
מומלץ להתקין בתוך סביבה וירטואלית של פייתון כדי לא ללכלך את המערכת:
python3 -m venv ~/pwn-venv
source ~/pwn-venv/bin/activate
pip install --upgrade pip
pip install pwntools
מי שמעדיף התקנה גלובלית למשתמש יכול פשוט pip install --user pwntools. אחרי ההתקנה יש לכם גם ספריית פייתון וגם כלי שורת פקודה בשם pwn.
הנה טעימה קטנה של איך נראה שלד exploit טיפוסי. אל תדאגו אם לא הכל ברור, נלמד את pwntools לעומק בהרצאה 0.4:
from pwn import *
context.binary = elf = ELF('./demo') # determines architecture, bitness, and endianness automatically
context.log_level = 'debug'
# locally:
p = process('./demo')
# remotely we would write:
# p = remote('pwnable.kr', 9000)
payload = b'A' * 72 # fill up to the return address
payload += p64(elf.sym['win']) # overwrite the return address
p.sendline(payload)
p.interactive()
שימו לב ל-context.binary. השורה הזו לבדה חוסכת המון: pwntools קוראת מה-ELF אם זה 32 או 64 ביט, ומכוונת את p64, את יצירת ה-shellcode ואת שרשראות ה-ROP בהתאם. הרגל טוב להתחיל כל exploit עם השורה הזו.
הספרייה pwntools מביאה איתה גם כמה כלי שורת פקודה שימושיים:
pwn checksec ./demo # check protections
pwn cyclic 200 # generate a pattern to find the offset
pwn cyclic -l 0x6161616c # compute the offset from an overwritten value
pwn shellcraft amd64.linux.sh # generate ready-made shellcode
כלי בדיקה, גאדג'טים ופענוח¶
עכשיו נאסוף את שאר ארגז הכלים. כל אחד מהם פותר בעיה ספציפית שנתקל בה שוב ושוב.
בדיקת הגנות - checksec¶
לפני שתוקפים בינארי, הדבר הראשון שעושים הוא לבדוק אילו הגנות דלוקות. זה מכתיב את כל אסטרטגיית ההתקפה. הכלי checksec עונה על השאלה הזו:
הפלט מראה חמישה שדות שנחזור אליהם בלי סוף: RELRO, Stack Canary, NX, PIE ו-Fortify. אפשר גם להשתמש ב-pwn checksec שמגיע עם pwntools, וגם בפקודת checksec שבתוך pwndbg - שלושתם נותנים אותו מידע.
מציאת גאדג'טים - ROPgadget ו-ropper¶
כשנגיע ל-ROP (פרק 4 והלאה) נצטרך למצוא בתוך הבינארי רצפי הוראות קצרים שמסתיימים ב-ret. שני כלים עושים את זה:
pip install ROPgadget ropper
ROPgadget --binary ./demo --only "pop|ret"
ropper --file ./demo --search "pop rdi"
שניהם סורקים את הבינארי ומחפשים גאדג'טים. יש להם יתרונות שונים: ROPgadget מהיר ופשוט, ropper חזק יותר בחיפושים מורכבים ובתמיכה בארכיטקטורות. שווה להכיר את שניהם.
המתנה קסומה - one_gadget¶
הכלי one_gadget מחפש בתוך ה-libc כתובת קסם אחת: מקום שאם קופצים אליו בזמן שהתנאים מתקיימים, מקבלים shell מיד, בלי לבנות ROP chain שלמה ל-system("/bin/sh"). זה כלי ruby, אז נתקין קודם ruby:
ואז מריצים אותו על ה-libc, לא על הבינארי שלנו:
הפלט הוא רשימת offsets, ולכל אחד תנאים (constraints) שחייבים להתקיים ברגע הקפיצה, למשל שאוגר מסוים יהיה אפס. נלמד לבחור את ה-gadget הנכון בפרק על ret2libc.
מפרק וקומפיילר הפוך - Ghidra¶
הכלי Ghidra הוא כלי reverse engineering חינמי של ה-NSA. הוא מפרק בינאריים ומשחזר מהם קוד C קריא (decompiler), מה שהופך אתגרי reversing מקושחים למשימה אנושית. הוא כתוב בג'אווה, אז צריך JDK:
sudo apt install -y openjdk-17-jdk
# download the latest version from github.com/NationalSecurityAgency/ghidra/releases
# extract and run:
./ghidraRun
בקורס נשתמש ב-Ghidra בעיקר בפרק הreverse engineering, אבל שווה שיהיה מותקן מההתחלה. אלטרנטיבות טובות: הכלי radare2 ואחיו הגרפי Cutter.
ארכיטקטורות זרות - qemu ו-gdb-multiarch¶
חלק מהאתגרים (למשל leg ב-pwnable.kr) בנויים ל-ARM או ל-MIPS, לא ל-x86. המכונה שלנו לא יכולה להריץ קוד כזה באופן טבעי, אבל אפשר לחקות אותו עם qemu:
sudo apt install -y qemu-user qemu-user-static gdb-multiarch \
binutils-multiarch libc6-armel-cross libc6-mipsel-cross
הרצה של בינארי ARM דרך אמולציה:
qemu-arm ./arm_binary
# if the binary is dynamic and needs the ARM libraries:
qemu-arm -L /usr/arm-linux-gnueabi ./arm_binary
ולניפוי, מריצים את qemu עם שרת gdb מובנה ומתחברים אליו מ-gdb-multiarch:
# window 1: run the binary, waiting for the debugger on port 1234
qemu-arm -g 1234 ./arm_binary
# window 2: connect
gdb-multiarch ./arm_binary
(gdb) set architecture arm
(gdb) target remote :1234
המנפה gdb-multiarch הוא בעצם gdb שמכיר את כל סטי ההוראות, ו-pwndbg עובד גם מעליו. ככה נוכל לנפות ARM ו-MIPS עם אותו זרימת עבודה שאנחנו רגילים אליה ב-x86.
מעקב אחרי קריאות - ltrace ו-strace¶
לפני שצוללים ל-gdb, לפעמים כדאי להסתכל על הבינארי מלמעלה: אילו פונקציות ספרייה הוא קורא ואילו קריאות מערכת הוא מבצע. שני כלים קלילים נותנים את זה:
sudo apt install -y ltrace strace
ltrace ./demo # traces calls to libc functions like strcpy, printf, malloc
strace ./demo # traces system calls like read, write, open, execve
הכלי ltrace נהדר כדי לראות בשנייה אם התוכנית קוראת ל-strcpy בלי בדיקת גבולות, או מה בדיוק היא משווה. הכלי strace מראה את הדיאלוג עם הkernel, שימושי להבין קלט, פלט ופתיחת קבצים. שניהם כלי סקירה מהירים לפני שמתחילים לחפור.
התאמת גרסת libc לאתגר מרוחק - patchelf¶
הנה בעיה שתתקלו בה בכל אתגר heap רציני. השרת המרוחק רץ עם גרסת glibc מסוימת, נניח 2.27. המכונה שלכם רצה עם 2.35. מבנה ה-tcache, ה-offsets של הפונקציות וההתנהגות של ה-allocator שונים בין הגרסאות, אז exploit שעובד אצלכם מקומית פשוט ייכשל מול השרת. הפתרון: להריץ את הבינארי מקומית מול אותה glibc בדיוק כמו בשרת.
בדרך כלל האתגר מגיע עם קובצי libc.so.6 ו-ld-linux.so של השרת. עם patchelf אנחנו מנתבים את הבינארי להשתמש בהם:
sudo apt install -y patchelf
# point the binary to the server's loader and libc:
patchelf --set-interpreter ./ld-2.27.so ./chall
patchelf --replace-needed libc.so.6 ./libc-2.27.so ./chall
יש כלי שעושה את כל התהליך אוטומטית ומומלץ בחום, pwninit:
# install via cargo (Rust's package manager) or download a prebuilt binary:
cargo install pwninit
pwninit # detects the binary, libc, and ld in the folder and links everything
ובתוך ה-exploit, pwntools נותנת לנו לטעון את ה-libc הנכון כדי לחשב offsets:
libc = ELF('./libc-2.27.so')
# after leaking the address of some function from libc:
libc.address = leaked_puts - libc.sym['puts'] # compute the libc base
system = libc.sym['system']
binsh = next(libc.search(b'/bin/sh'))
הרעיון המרכזי: תמיד תעבדו מקומית מול אותה glibc כמו היעד. אם אין לכם את ה-libc של השרת, אפשר לזהות אותו לפי כמה בתים אחרונים של כתובת שדלפתם דרך שירות כמו libc-database. נחזור לכל זה בעומק בפרק ה-heap.
שליטה בהגנות בזמן הידור - compile flags¶
כדי ללמוד טכניקה מסוימת, לרוב נרצה לבנות בינארי הדגמה עם הגנות מכובות בכוונה, לבודד את מה שלומדים. gcc מודרני על Ubuntu מדליק כברירת מחדל canary, NX, PIE ו-RELRO חלקי. הנה הדגלים לכיבוי כל אחד מהם:
+------------------------+-------------------------------------------+
| flag | what it does |
+------------------------+-------------------------------------------+
| -fno-stack-protector | disables the canary on the stack |
| -z execstack | makes the stack executable (disables NX) |
| -no-pie | fixed code addresses (disables PIE) |
| -static | static linking, no external libc |
| -z norelro | disables RELRO (the GOT stays writable) |
| -z relro -z now | enables full RELRO |
| -m32 | compile to 32-bit |
+------------------------+-------------------------------------------+
דוגמה: בינארי הכי vulnerable שאפשר, בשביל ללמוד buffer overflow נקי, בלי שום הגנה:
ובודקים ש-checksec מסכים איתנו:
כיבוי ASLR מקומית¶
ה-ASLR הוא הגנה של המערכת, לא של הבינארי, אז מכבים אותו ברמת הkernel. בזמן שאנחנו מפתחים exploit ורוצים כתובות יציבות בין ריצות, נוח לכבות אותו זמנית:
# check the current state:
cat /proc/sys/kernel/randomize_va_space # 0=off, 1=partial, 2=full
# temporary disable until reboot:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
# restore to full:
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
שתי הערות חשובות. ראשית, gdb ממילא מכבה ASLR כברירת מחדל בזמן ניפוי (set disable-randomization on), אז כתובות בתוך gdb יציבות בכל מקרה - זו אחת הסיבות ש-exploit "עובד ב-gdb אבל לא בחוץ". שנית, על השרת האמיתי ה-ASLR כמעט תמיד דלוק, אז כיבוי מקומי הוא רק גלגלי עזר ללימוד. בסופו של דבר נלמד לעקוף ASLR דרך דליפת כתובות, לא לכבות אותו.
מפת הכלים - סיכום זריז¶
+-----------------------+------------------------------------------+
| tool | role |
+-----------------------+------------------------------------------+
| gcc + gdb | basic compiling and debugging |
| pwndbg / GEF | gdb plugin focused on exploitation |
| pwntools | framework for writing exploits |
| checksec | checks a binary's protections |
| ROPgadget / ropper | finds ROP gadgets |
| one_gadget | magic address for a shell inside libc |
| Ghidra | disassembles and recovers C code |
| qemu + gdb-multiarch | running and debugging ARM / MIPS |
| ltrace / strace | tracing library and system calls |
| patchelf / pwninit | matching the glibc version to a remote challenge |
+-----------------------+------------------------------------------+
סיכום¶
בהרצאה הזו הקמנו שולחן עבודה מלא לחוקר חולשות:
- הבנו למה עובדים בתוך מכונה וירטואלית מבודדת, ולמה גרסת ה-glibc היא פרט קריטי.
- התקנו את הבסיס: gcc, gcc-multilib ו-gdb.
- שדרגנו את gdb עם pwndbg כדי לראות אוטומטית אוגרים, מחסנית, מפת זיכרון והגנות.
- התקנו את pwntools, ה-framework המרכזי שלנו לכתיבת exploits.
- אספנו את ארגז הכלים: checksec, ROPgadget, ropper, one_gadget, Ghidra, qemu, gdb-multiarch, ltrace, strace ו-patchelf.
- למדנו את דגלי ההידור שמכבים כל הגנה (canary, NX, PIE, RELRO), ואיך לכבות ASLR מקומית לצורכי לימוד.
בתרגול תתקינו את כל הערכה בעצמכם, תהדרו בינארי הדגמה בכמה צורות, ותקראו את פלט ה-checksec לכל אחת מהן - כדי לראות בעיניים איך כל דגל משנה את פרופיל ההגנות. אחרי שהסביבה עומדת, אנחנו מוכנים לחזור בהרצאה הבאה על זיכרון התהליך ולצלול לאתגר הראשון.