לדלג לתוכן

1.3 אקראיות צפויה תרגול

תרגול - אקראיות צפויה

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

התחברות לאתגר

אתגר random נמצא בשרת האימונים pwnable.kr. מתחברים בעזרת SSH (הסיסמה למשתמש היא guest):

ssh random@pwnable.kr -p2222
# password: guest

בתיקיית הבית תמצאו שלושה קבצים:

random       - the compiled binary (setuid, runs with permissions holding the flag)
random.c     - the source code
flag         - the flag, readable only through the binary

התחילו תמיד בקריאת המקור:

cat random.c

תרגיל 1 - למה זה בכלל שביר

קראו את random.c וענו לעצמכם:

  1. איזה ערך משווים ל-0xdeadbeef, ומאיפה הוא מגיע.
  2. חפשו קריאה ל-srand. מה מצאתם.
  3. הסבירו במילים שלכם: אם אין srand, מה הערך של random בהרצה הזו, ומה יהיה בהרצה הבאה.

המטרה של התרגיל הזה היא לזהות את הבאג הלוגי בלי לגעת עדיין במספרים. אם הבנתם למה random קבוע, כבר פתרתם את האתגר בראש.

רמז: לפי תקן C, אם לא קוראים ל-srand לפני rand, ההתנהגות זהה ל-srand(1).

תרגיל 2 - למצוא את הקבוע בעצמכם

אל תסתמכו על זיכרון. הוכיחו מה הערך הראשון ש-rand() מחזיר בזרע דיפולטי, בשתי דרכים עצמאיות:

  1. כתבו תוכנית C בת ארבע שורות שקוראת ל-rand() פעם אחת בלי srand ומדפיסה את התוצאה. קמפלו והריצו.
  2. עשו את אותו הדבר בתוך הבינארי random עצמו: פתחו אותו ב-GDB, עצרו מיד אחרי הקריאה ל-rand, וקראו את הערך המוחזר.

רשמו לעצמכם את הערך בהקסה ובעשרוני. תזדקקו לו בתרגיל הבא.

רמז: ב-x86-64 ערך ההחזרה של פונקציה יושב ב-rax (או eax לערך 32 ביט). ב-pwndbg אפשר break *main+N ואז run ו-p/x $eax. את ה-offset המדויק מוצאים עם disassemble main וחיפוש ה-call rand וההוראה שאחריו.

תרגיל 3 - לחשב את הקלט ולקבל את הדגל

עכשיו שיש לכם את הקבוע, פתרו את המשוואה:

key XOR (the constant value) == 0xdeadbeef
  1. חלצו את key בעזרת התכונה ההופכית של XOR.
  2. שלחו את key כמספר עשרוני לתוכנית וקבלו את הדגל.

עשו את זה בשלוש דרכים, כדי שכל אחת תתקבע לכם באצבעות:

  1. בשורת פקודה אחת עם python3 -c ... | ./random.
  2. עם pwntools מול התהליך המקומי (process).
  3. עם pwntools דרך SSH מול הבינארי שרץ על שרת pwnable.kr.

רמז: שימו לב ש-scanf באתגר קורא עם %d (מסומן), אבל מה שחשוב זו תבנית הביטים שנכתבת ל-key, לא הפרשנות המספרית. שלחו את המספר העשרוני כמו שהוא.

תרגיל 4 - הזרעה מבוססת זמן - time-based seed

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

// timelock.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
    srand(time(NULL));
    int secret = rand() % 1000000;

    int guess;
    printf("Guess my number: ");
    fflush(stdout);
    if (scanf("%d", &guess) != 1) return 1;

    if (guess == secret) {
        printf("Correct! flag{predictable_time_seed}\n");
    } else {
        printf("Wrong. it was %d\n", secret);
    }
    return 0;
}
gcc -o timelock timelock.c

המשימה: כתבו סקריפט שמנחש את secret בהרצה אחת, בלי לקרוא את הפלט של "Wrong" ובלי לנחש עיוור. המפתח הוא שהזרע הוא time(NULL) - מספר שאתם יודעים בקירוב.

  1. הסבירו מהו מרחב הזרעים האפשרי מבחינת התוקף, וכמה גדול הוא בפועל.
  2. כתבו סקריפט Python שמחשב את secret הצפוי ומזין אותו לתוכנית.
  3. הריצו וודאו שקיבלתם Correct.

רמז 1: כדי שהחיזוי יהיה זהה בית-בית ל-C, קראו ל-srand/rand של libc עצמה דרך ctypes, ולא למחולל של Python (שהוא מחולל שונה לחלוטין).

רמז 2: time(NULL) ברזולוציית שנייה. נסו את הזרע של השנייה הנוכחית, ואם החמצתם בגלל השהיית הרצה, נסו חלון של שנייה-שתיים לכל כיוון.

מתקדם - להרחבה

  • מה היה משתנה אילו התוכנית הייתה מזריעה עם srand(time(NULL) ^ getpid()). האם עדיין אפשר לשבור, ואיך תגדילו את מרחב החיפוש.
  • אם התוכנית קוראת ל-rand() מאה פעמים לפני שהיא בוחרת את הסוד, האם החיזוי עדיין אפשרי. (רמז: כן, פשוט מריצים אצלכם את אותן מאה קריאות.)
  • אם היו מחליפים את rand() ב-getrandom(), האם הברוט-פורס עדיין עובד. הסבירו למה לא.