לדלג לתוכן

0.5 היכרות עם pwnable.kr הרצאה

עד עכשיו הקמנו סביבת עבודה, חזרנו על זיכרון התהליך והמחסנית, ולמדנו את היסודות של pwntools. עכשיו צריך מקום לתרגל בו את כל מה שנלמד בהמשך - זירת אימונים חוקית, מדורגת בקושי, שלא נמאסת. הזירה הזו היא pwnable.kr, ואנחנו נלווה אותה לאורך כל הקורס. בהרצאה הזו נכיר את האתר: איך הוא בנוי, איך אתגר טיפוסי נראה מבפנים, מה תהליך העבודה הנכון, ואיזה פרק בקורס מפרק לגורמים איזה אתגר.

למה דווקא pwnable.kr

יש היום לא מעט אתרי wargames (למשל HackTheBox, pwn.college, ROP Emporium), אבל pwnable.kr מיוחד בכמה דברים שהופכים אותו למושלם ללימוד:

  • כל אתגר הוא בינארי אמיתי עם קוד מקור פתוח. אתם רואים בדיוק את הבאג, לא צריכים לנחש איפה הוא.
  • הקושי מדורג בצורה עדינה. מתחילים מבאגים של שורה אחת ומטפסים עד לexploit heap וkernel.
  • האתגרים רצים על שרת אמיתי עם הרשאות אמיתיות, אז אתם מתרגלים את כל הchain - חיבור, קריאת קוד, שחזור מקומי, וexploit מרחוק - בדיוק כמו במחקר חולשות אמיתי.
  • זה חינמי, יציב, ורץ כבר שנים. יש קהילה ענקית וכתובות פתרון (write-ups) כשנתקעים.

הרעיון פשוט: נרשמים לאתר, פותרים אתגר, מוציאים ממנו מחרוזת דגל (flag), ומגישים אותה באתר כדי לקבל נקודות. הדגל הוא ההוכחה שפתרתם. המטרה שלנו בקורס היא שבסופו תסיימו את כל האתגרים באתר.

ארבע הקטגוריות

האתגרים ב-pwnable.kr מחולקים לארבע קטגוריות לפי רמת קושי וסוג הידע שנדרש:

הקטגוריה מה מצפה לכם
בקבוק לפעוטות - Toddler's Bottle האתגרים הקלים ביותר. באגים פשוטים, טעויות תמימות של מתכנת, ולפעמים רק הבנה של מושג בסיסי במערכת ההפעלה. נקודה עד כמה נקודות בודדות.
טעויות של מתחיל - Rookiss הexploit חולשות אמיתי אבל בסיסי - הבאגים שמתחיל טיפוסי היה עושה. כאן מתחילים לנצל ברצינות: shellcode, format string, וexploit heap ראשוני.
מכוער אבל טעים - Grotesque כואב לפתור אבל מספק. אתגרים ארוכים ומורכבים שדורשים שרשור של כמה טכניקות יחד.
סודות של האקר - Hacker's Secret טכניקות מיוחדות ונדירות. הרמה הגבוהה ביותר באתר.

ההמלצה שלנו: לכו לפי הסדר. אל תקפצו ל-Rookiss לפני שסיימתם את רוב ה-Toddler's Bottle. כל אתגר בונה על ידע מהאתגרים שלפניו, והקורס בנוי בדיוק כך.

איך אתגר SSH בנוי

רוב אתגרי ה-Toddler's Bottle עובדים באותו דפוס, וכדאי להפנים אותו טוב כי הוא חוזר שוב ושוב. מתחברים לשרת של האתגר דרך SSH:

ssh <challenge-name>@pwnable.kr -p2222

לדוגמה, לאתגר בשם fd מתחברים עם ssh fd@pwnable.kr -p2222. שימו לב לפורט הלא-סטנדרטי, 2222. הסיסמה לרמה הראשונה של כל אתגר מופיעה בעמוד האתגר באתר עצמו - עבור אתגרי Toddler's Bottle היא כמעט תמיד guest. אל תחפשו סיסמה מסובכת: המטרה היא לתת לכם דריסת רגל על השרת, ומשם הכל תלוי בכם.

ברגע שאתם בפנים, אתם משתמש רגיל וחלש בשם האתגר (למשל המשתמש fd). בתיקיית הבית תמצאו בדרך כלל שלושה קבצים:

$ id
uid=1017(fd) gid=1017(fd) groups=1017(fd)

$ ls -l
-r-sr-x--- 1 fd_pwn fd      7322 ...  fd          # setuid binary, owned by fd_pwn
-rw-r--r-- 1 root   root     418 ...  fd.c        # the source code - readable by everyone
-r-------- 1 fd_pwn fd_pwn    50 ...  flag        # the flag - only fd_pwn can read it

שלושת הרכיבים האלה הם הלב של האתגר:

  • קובץ fd.c - קוד המקור המלא של הבינארי, קריא לכל אחד. כאן מסתתר הבאג.
  • קובץ fd - הבינארי המהודר, עם ביט ה-setuid דלוק ובעלות של המשתמש fd_pwn.
  • קובץ flag - קובץ הדגל, בהרשאה 400 ובבעלות fd_pwn. המשתמש fd שאתם מחוברים בתור אינו יכול לקרוא אותו ישירות.

בואו נבין למה זה מסודר בדיוק ככה, כי בזה טמון כל הרעיון.

למה זה עובד - ביט ה-setuid

הביט s בהרשאות של הבינארי (-r-sr-x---) הוא ביט ה-setuid. משמעותו פשוטה אבל קריטית: כשמריצים את התוכנית, היא רצה בהרשאות של בעל הקובץ ולא בהרשאות של מי שהריץ אותה. הבעלים כאן הוא fd_pwn, אז כשאתם (המשתמש fd) מריצים ./fd, התהליך מקבל את הזהות האפקטיבית של fd_pwn.

your user               the binary (setuid fd_pwn)        the flag
+-----------+  runs     +----------------------+  reads +-------------+
|    fd     | ------>  | runs with fd_pwn perms | -----> | flag (400,  |
| (weak)    |          |                      |        | owned by fd_pwn)|
+-----------+          +----------------------+        +-------------+
       ^                                                     |
       | cannot read flag directly (permission 400 of fd_pwn) |
       +-----------------------------------------------------+

הבינארי fd_pwn כן יכול לקרוא את flag (הוא הבעלים). המשימה שלנו: לגרום לבינארי, שרץ בהרשאות fd_pwn, לעשות את מה שאנחנו רוצים - להדפיס לנו את הדגל.

איך? תלוי באתגר. יש שני סוגים:

  • באתגרים הקלים, מספיק לגרום לבינארי להגיע לנתיב ה"ניצחון" שלו. למשל, אם התוכנית מריצה system("/bin/cat flag") כשמזינים קלט מסוים, כל מה שצריך זה למצוא את הקלט הנכון. זה באג לוגי, בלי שום קורפשן זיכרון.
  • באתגרים המתקדמים יותר, יש חולשת זיכרון (buffer overflow, format string, UAF), ואנחנו מנצלים אותה כדי לחטוף את זרימת התוכנית ולהריץ /bin/sh או לקרוא את הדגל בעצמנו. גם אז - ה-shell שנקבל רץ בהרשאות fd_pwn, ולכן יוכל לקרוא את הדגל.

בשני המקרים העיקרון זהה: הבינארי המיוחס הוא הגשר שלכם אל הדגל.

תהליך העבודה הנכון

אפשר לפתור אתגר קל ישירות על השרת, אבל ברגע שהאתגרים מתחילים לדרוש exploit אמיתי, עבודה ישירה על השרת היא סיוט: אין לכם שם pwndbg נוח, אין הרשאות, וכל ניסיון לוקח נצח. תהליך העבודה המקצועי הוא תמיד אותו דבר, ונחזור עליו לאורך כל הקורס:

שלב 1 - קריאת המקור

לפני שנוגעים במקלדת, קוראים את קובץ ה-.c עד הסוף ומזהים את הבאג:

cat fd.c

מחפשים את החשודים המיידיים: gets, strcpy, sprintf, system, printf(user_input), scanf בלי הגבלת אורך, אריתמטיקה על אינדקסים, השוואות חשודות. ב-Toddler's Bottle הבאג בדרך כלל צועק מהדף.

שלב 2 - שחזור מקומי

מעתיקים את הבינארי (ולפעמים גם את המקור) למכונה שלכם כדי לחקור בנוחות. אפשר עם scp על אותו פורט:

scp -P2222 fd@pwnable.kr:~/fd ./fd

עכשיו בודקים מה יש לנו ביד. אילו הגנות פעילות? אילו הוראות מעניינות? מהי הארכיטקטורה?

file ./fd
checksec ./fd
objdump -d ./fd | less

הפלט של checksec הוא הדבר הראשון שקובע את כל האסטרטגיה. דוגמה טיפוסית לאתגר ישן ופשוט:

Arch:     i386-32-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

מכאן אנחנו כבר יודעים המון: אין canary (אז overflow על המחסנית לא תיתפס), ה-NX דלוק (אז shellcode על המחסנית לא ירוץ, ונצטרך ROP או ret2libc), ואין PIE (אז כתובות הקוד קבועות וקל לכוון אליהן). את שאר הכלים - ROPgadget, one_gadget, gdb עם pwndbg - נריץ מקומית בזמן שאנחנו מפתחים את ה-exploit.

חשוב: אם האתגר תלוי בגרסת libc מסוימת של השרת, מעתיקים גם אותה (בדרך כלל /lib/i386-linux-gnu/libc.so.6 או המקבילה ל-64 ביט) ומריצים מולה מקומית, כדי שהכתובות יתאימו למרחוק.

שלב 3 - פיתוח וexploit מרחוק

בונים את ה-exploit מקומית מול הבינארי שהעתקנו, מנפים אותו עד שהוא עובד באופן יציב, ורק אז מפנים אותו לשרת האמיתי. עם pwntools זו החלפה של שורה אחת בין הרצה מקומית להרצה מרחוק:

from pwn import *

context.binary = elf = ELF('./fd')

# during development - run locally:
# p = process('./fd')

# against the real server - the exact same script, only the target changes:
p = remote('pwnable.kr', 2222)

# ... here we build the payload and send it ...

p.interactive()   # get a shell / read the flag

הרעיון החשוב: אותו סקריפט בדיוק עובד מקומית ומרחוק. מפתחים בנוחות מול העותק המקומי, ומגישים מול השרת. אם ה-exploit יציב מקומית, הוא יעבוד מרחוק. שימו לב שחלק מהאתגרים אינם נפתרים ב-SSH הרגיל אלא כשירות רשת נפרד (ראו בהמשך), ואז remote מצביע על הפורט של השירות ולא על 2222.

אתגרים שהם שירות רשת

לא כל אתגר עובד בדפוס ה-SSH-plus-setuid. חלק מהאתגרים רצים כשירות רשת שאליו מתחברים ישירות עם nc, בעוד שקוד המקור מוצג בעמוד האתגר באתר:

nc pwnable.kr 9000

אתגרים כאלה (למשל bof, coin1, blackjack) נפתרים בדיוק באותו רוח - קוראים את המקור, משחזרים מקומית, ומנצלים מרחוק - רק שהחיבור הוא ל-nc/remote על פורט ייעודי במקום login ב-SSH. הדגל מגיע חזרה על אותו החיבור.

כללי התנהגות ונקיון

השרת של האתגר משותף לכל הלומדים בעולם שפותרים אותו כרגע. אתם, המשתמש fd, חולקים את אותה מכונה עם עוד מאות אנשים. לכן יש כמה כללי נימוס בסיסיים שחשוב לכבד:

  • אל תפריעו לשחקנים אחרים. אל תהרגו תהליכים שלא שלכם, אל תמלאו את הזיכרון או את הדיסק, ואל תריצו fuzzing פראי על השרת. הכל נבנה מקומית - השרת הוא רק היעד הסופי.
  • עבדו בתיקייה זמנית משלכם. תיקיית הבית לרוב אינה כתיבה, אז יוצרים תיקייה אקראית תחת /tmp ועובדים בה:
mkdir /tmp/`whoami`_$RANDOM && cd $_
# or, cleaner:
d=$(mktemp -d) && cd "$d"
  • נקו אחריכם. כשסיימתם, מחקו את הקבצים הזמניים שיצרתם:
cd /tmp && rm -rf "$d"
  • אל תדרסו קבצים של אתגרים אחרים ואל תנסו "לתקן" את השרת. הבאג הוא חלק מהאתגר, לא תקלה.

התנהגות נקייה היא לא רק נימוס - היא הרגל של חוקר חולשות מקצועי. אתם עתידים לעבוד על מערכות של אנשים אחרים, ושם בלגן עולה כסף ואמון.

מפת דרכים - אילו פרקים תוקפים אילו אתגרים

הקורס בנוי כך שכל פרק נותן לכם בדיוק את הכלים לפצח קבוצת אתגרים ב-pwnable.kr. הנה המפה המלאה:

פרק בקורס אתגרים ב-pwnable.kr קטגוריה עיקרית
1 - חולשות פשוטות ובאגים לוגיים fd, collision, mistake, lotto, blackjack, random, input, coin1, cmd1, cmd2, shellshock Toddler's Bottle
2 - buffer overflow על המחסנית bof Toddler's Bottle
3 - Shellcode asm, ascii_easy Toddler's Bottle ו-Rookiss
4 - עקיפת NX ו-ROP בסיסי ביסוס ret2libc ו-ROP (בסיס לאתגרי הexploit בהמשך) Rookiss
5 - חולשות מחרוזת format passcode, fsb Toddler's Bottle ו-Rookiss
6 - ROP מתקדם stack pivoting, SROP, ret2dlresolve (טכניקות לאתגרים הקשים) Grotesque
7 - exploit ה-Heap uaf, unlink Toddler's Bottle
8 - reverse engineering לexploit flag, leg, tiny_easy, memcpy, brainfuck, simple login, otp, md5 calculator, blukat, crypto1, rsa calculator, echo1, echo2, loveletter, dragon, horcruxes Toddler's Bottle, Rookiss ו-Grotesque
9 - exploit kernel ואתגרים מתקדמים האתגרים הקשים ביותר באתר Grotesque ו-Hacker's Secret

אל תילחצו מהרשימה הארוכה. בסוף כל פרק בקורס תמצאו את האתגרים המתאימים כתרגול, עם רמזים מדורגים ופתרון מלא. הקצב נבנה בהדרגה: עד שתגיעו ל-horcruxes כבר תרגישו בבית עם ROP ו-format string.

סיכום

  • אתר pwnable.kr הוא זירת האימונים החוקית שנלווה לאורך כל הקורס, עם אתגרים מדורגים וקוד מקור פתוח.
  • ארבע הקטגוריות, מהקל לקשה: Toddler's Bottle, Rookiss, Grotesque, Hacker's Secret. עובדים לפי הסדר.
  • אתגר SSH טיפוסי בנוי משלושה קבצים: קוד מקור .c קריא, בינארי setuid בבעלות משתמש _pwn, וקובץ flag שרק אותו משתמש קורא. מנצלים את הבינארי המיוחס כדי להגיע לדגל.
  • ביט ה-setuid הוא הלב: הבינארי רץ בהרשאות הבעלים שלו, אז אנחנו מכוונים אותו לקרוא לנו את הדגל.
  • תהליך העבודה: קוראים מקור, משחזרים מקומית (scp, file, checksec, objdump, pwndbg), מפתחים ומנצלים מרחוק. אותו סקריפט pwntools עובד מקומית ומרחוק - מחליפים רק process ב-remote.
  • חלק מהאתגרים הם שירות רשת שמתחברים אליו עם nc על פורט ייעודי, והמקור מוצג באתר.
  • מתנהגים יפה: לא מפריעים לאחרים, עובדים ב-/tmp בתיקייה אקראית, ומנקים אחרינו.