1.3 אקראיות צפויה תרגול
תרגול - אקראיות צפויה¶
בתרגול הזה נשבור את האקראיות המזויפת בפועל. נתחיל מאתגר random ב-pwnable.kr, שנפתר בחישוב יחיד בלי שום ברוט-פורס, ואז נבנה לעצמנו בינארי שמזריע לפי הזמן ונשבור גם אותו. המטרה: לצאת מכאן עם הרגל אוטומטי - כשאתם רואים rand() בקוד, השאלה הראשונה שלכם היא "האם ומתי זה הוזרע".
התחברות לאתגר¶
אתגר random נמצא בשרת האימונים pwnable.kr. מתחברים בעזרת SSH (הסיסמה למשתמש היא 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
התחילו תמיד בקריאת המקור:
תרגיל 1 - למה זה בכלל שביר¶
קראו את random.c וענו לעצמכם:
- איזה ערך משווים ל-
0xdeadbeef, ומאיפה הוא מגיע. - חפשו קריאה ל-
srand. מה מצאתם. - הסבירו במילים שלכם: אם אין
srand, מה הערך שלrandomבהרצה הזו, ומה יהיה בהרצה הבאה.
המטרה של התרגיל הזה היא לזהות את הבאג הלוגי בלי לגעת עדיין במספרים. אם הבנתם למה random קבוע, כבר פתרתם את האתגר בראש.
רמז: לפי תקן C, אם לא קוראים ל-srand לפני rand, ההתנהגות זהה ל-srand(1).
תרגיל 2 - למצוא את הקבוע בעצמכם¶
אל תסתמכו על זיכרון. הוכיחו מה הערך הראשון ש-rand() מחזיר בזרע דיפולטי, בשתי דרכים עצמאיות:
- כתבו תוכנית C בת ארבע שורות שקוראת ל-
rand()פעם אחת בליsrandומדפיסה את התוצאה. קמפלו והריצו. - עשו את אותו הדבר בתוך הבינארי
randomעצמו: פתחו אותו ב-GDB, עצרו מיד אחרי הקריאה ל-rand, וקראו את הערך המוחזר.
רשמו לעצמכם את הערך בהקסה ובעשרוני. תזדקקו לו בתרגיל הבא.
רמז: ב-x86-64 ערך ההחזרה של פונקציה יושב ב-rax (או eax לערך 32 ביט). ב-pwndbg אפשר break *main+N ואז run ו-p/x $eax. את ה-offset המדויק מוצאים עם disassemble main וחיפוש ה-call rand וההוראה שאחריו.
תרגיל 3 - לחשב את הקלט ולקבל את הדגל¶
עכשיו שיש לכם את הקבוע, פתרו את המשוואה:
- חלצו את
keyבעזרת התכונה ההופכית של XOR. - שלחו את
keyכמספר עשרוני לתוכנית וקבלו את הדגל.
עשו את זה בשלוש דרכים, כדי שכל אחת תתקבע לכם באצבעות:
- בשורת פקודה אחת עם
python3 -c ... | ./random. - עם pwntools מול התהליך המקומי (
process). - עם 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;
}
המשימה: כתבו סקריפט שמנחש את secret בהרצה אחת, בלי לקרוא את הפלט של "Wrong" ובלי לנחש עיוור. המפתח הוא שהזרע הוא time(NULL) - מספר שאתם יודעים בקירוב.
- הסבירו מהו מרחב הזרעים האפשרי מבחינת התוקף, וכמה גדול הוא בפועל.
- כתבו סקריפט Python שמחשב את
secretהצפוי ומזין אותו לתוכנית. - הריצו וודאו שקיבלתם Correct.
רמז 1: כדי שהחיזוי יהיה זהה בית-בית ל-C, קראו ל-srand/rand של libc עצמה דרך ctypes, ולא למחולל של Python (שהוא מחולל שונה לחלוטין).
רמז 2: time(NULL) ברזולוציית שנייה. נסו את הזרע של השנייה הנוכחית, ואם החמצתם בגלל השהיית הרצה, נסו חלון של שנייה-שתיים לכל כיוון.
מתקדם - להרחבה¶
- מה היה משתנה אילו התוכנית הייתה מזריעה עם
srand(time(NULL) ^ getpid()). האם עדיין אפשר לשבור, ואיך תגדילו את מרחב החיפוש. - אם התוכנית קוראת ל-
rand()מאה פעמים לפני שהיא בוחרת את הסוד, האם החיזוי עדיין אפשרי. (רמז: כן, פשוט מריצים אצלכם את אותן מאה קריאות.) - אם היו מחליפים את
rand()ב-getrandom(), האם הברוט-פורס עדיין עובד. הסבירו למה לא.