Minesweeper
מטרת המשחק היא לפתוח את כל הלוח מבלי לפגוע במוקשים המפוזרים בצורה אקראית בלוח.
אם תפתחו משבצת ללא מוקש יהיה מסומן עליה את כמות המוקשים הנמצאת בקרבת מקום, מה שנראה כך:
מטרת מאמר זה היא להציג לכם כיצד ניתן לפתור את המשחק באמצעות הנדסה לאחור וכיצד ניתן לאתר את פונקציית הניצחון של המשחק. בהמשך אציג לכם איך אנחנו נבנה קובץ dll שנזריק לתוך התוכנה כדי לאפשר לנו לנצח ברגע שנלחץ על מקש "הקסם" שנבחר.
כמו כן אציג לכם פתרון נוסף באמצעות כתיבת loader לתוכנה אשר יאפשר לנו לנצח ברגע שנלחץ על המקש "enter".
להלן סרטון המדגים כיצד לבצע את התהליך:
להורדת המשחק:
במאמר זה אנו נשתמש ב-2 כלים:
- WinDbg – לטובת ביצוע ה-debugging
- Ida – לטובת קריאת הקוד ותיעוד
בנוסף לכך שבסרטון הצגתי לכם את תהליך ה- debugging באמצעות OllyDbg, במאמר אני מעדיף להדגים לכם כיצד אפשר לעשות את אותו הדבר באמצעות WinDbg.
טרם ה-debugging, עלינו לבחון את הפונקציונאליות של התוכנה אותה אנו חוקרים ולנסות למצוא פיצ'ר כלשהו או חלון כלשהו שיכול לעזור לנו למצוא את הפונקציונאליות שאנו רוצים לחקור ולא בהכרח לחקור את כל המשחק.
חלון המשחק נראה כך:

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

לא לשכוח לסמן את record process. נפעיל את המשחק, כעת שחקו מעט וסגרו את החלון של המשחק.

כך תמצאו את עצמכם בתוך חלון ה-windbg בתוך ההקלטה של הריצה של המשחק, ההקלטה מאפשרת לכם לזוז אחורה וקדימה בזמן הריצה (time travel debugging), וכך לא תצטרכו להריץ את התוכנה שוב במידה ופספסתם משהו שאתם רוצים לחזור אליו.

כעת עלינו למצוא את כל הפונקציות שהתוכנה משתמשת בהם: פונקציות אלו נמצאות בטבלה הנקראת Import Address Table, טבלה זו מכילה רשימה של כל ה-dllים שאנחנו משתמשים בהם, הפונקציה שאנחנו משתמשים בה וכמובן הכתובת של הפונקציה.
כדי להגיע לטבלה עלינו תחילה למצוא את הכתובות של המודול שמעניין אותנו (base address) ואז להוסיף אליו את המרחק (offset) עד הטבלה, בשביל לעשות זאת נשתמש בפקודה:
!dh winmine
ונקבל את התוצאה הבאה:

ניתן לראות שה-base address הינו 0100 0000 והמרחק מהטבלה הינו 1000 כמו כן גודל הטבלה הינו 1B8 כך שכדי להדפיס את הטבלה נשתמש בפקודה:
dps 01000000+1000 01000000+1000+1B8
כמובן שאפשר היה להשתמש גם בשם המודול במקום לכתוב את ה-base address מכיוון שהוא בעצם מכיל בתוכו את ה-base address, לדוגמה אם נכתוב:
?winmine
נראה שהוא בעצם שווה ל-base address של עצמו שזה 0100 0000

כך שאת אותה הפקודה אפשר היה לכתוב כך:
dps winmine+1000 winmine+1000+1B8
והתוצאה תראה כך:

זאת בעצם רשימת כל הפונקציות שנמצאות בספריות חיצוניות שהתוכנה משתמשת בהן, כעת נבצע break point בפונקציה PlaySoundW באמצעות הפקודה:
bp WINMM!PlaySoundW
ונלחץ על g כדי להמשיך בריצת התוכנה. תראו שמיד תעצרו בפונקציה עוד לפני שהמשחק התחיל, כנראה שזה צליל הפעלת המשחק שלא קשור לניצחון כי עוד לא התחלתם בכלל את המשחק.
לחצו שוב על g ולחצו על הפונקציה השנייה ב-call stack, זו הפונקציה שבעצם קראה ל- PlaySoundW:

לחצו עליה פעמיים ותעברו אליה:

ניתן לראות שהפונקציה הזאת מקבלת 3 סוגי צלילים לפי הערך esp+4]] וקפיצה לצליל באמצעות je, כדי לבדוק את סוג הצליל נבצע break point חדש עם תנאי הדפסה בשורה הראשונה בפונקציה ונבחן את הצלילים שאנו מקבלים כרגע:
bp 010038ed ".echo SOUND VALUE; ?poi(@esp+4)"
כעת נפעיל את התוכנה מחדש מבלי להריץ הכל שוב על ידי לחיצה על כפתור ה-reset ואז על כפתור ה-go, כך תראו שעצרתם בשורה שרציתם והדפסתם את הצליל שהוא בעצם 1.

כתבו g בפקודות ותראו ששוב עצרתם עם 1
הערה: אם עצרתם ב-PlaySoundW בטלו את ה-break point, באמצעות bd 0 שזה בעצם ראשי התיבות של break point disable ו-0 זה מספר ה-break point מרשימת ה-break points שאותו אפשר לראות באמצעות הפקודה bl.
המשיכו לבצע את הפקודה g עד אשר תפסידו במשחק, שמתם לב שלפני ההפסד קיבלתם את הצליל 3?
מכאן המשימה ברורה, אנחנו צריכים למצוא פונקציה שיכולה להחזיר לנו את הצליל 2 שאפשר להסיק מכאן שזה צליל הניצחון.
נחזור באמצעות:
g-
לנקודת הזמן בה הודפס לנו 3:

ונבדוק איזו פונקציה קראה לקוד הזה באמצעות חזרה אחורה בזמן בעזרת הפקודה:
t-
כך נגיע לקטע הקוד הבא:

ניתן לראות שקטע קוד זה מושפע מ-esi לכן נהפוך את הלוגיקה ונראה מה היה esi כשקיבלנו את הספרה 3:
eax = 3
sub eax, 3
adc eax, eax
neg eax
ונקבל ש-esi היה בעצם 0, כעת נעשה את אותו החישוב עבור הספרה 2 שאנו רוצים לקבל (אותן הפעולות המתמטיות רק שהפעם נכניס ל-eax את הספרה 2 במקום 3)

ונקבל 1 (לא לשכוח את ה-CF), נסתכל על הפונקציה ובכן האם לקבל את הערך 1 ב-esi זה משהו אפשרי?

ניתן לראות ש-esi מושפע מהארגומנט שעובר לפונקציה, כך שכבר כאן ניתן לפתור את התרגיל על ידי ביצוע break point בשורה 01003488 ושינוי ערך ה-esi ל-1.
או פשוט לבנות dll שמריץ את הפונקציה 0100347c עם הארגומנט 1.
נבדוק את הטענה שלנו על הרצת התוכנה מחדש ללא time travel הפעם:

טרם הפעלת המשחק באמצעות g בצעו break point בשורה 0100347c והפעילו את המחשק על ידי הפקודה g, כעת נסו לאתר מוקש ותראו שהמשחק עצר ב-break point שלנו:

בצעו 3 פעמים step into על ידי הפקודה:
t
ותראו שהגעתם בדיוק לשורה 01003488 ו-esi שווה ל-0 בדיוק כפי שחישבנו.

כעת שנו את הערך של esi באמצעות הפקודה:
r esi=1
כדי להציג את האוגרים כתבו r, מה שיראה כך:

כעת המשיכו את המשחק על ידי הפקודה g, ותשמעו את צליל הניצחון של המשחק:

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

הגיע הזמן לפתוח ida ולהסתכל על אותה הכתובת באמצעות g, מה שיראה כך:

ניתן לראות שהפונקציה יודעת לקבל הן 1 והן 0, נעלה לראש הפונקציה ונבצע breakpoint בשורה 01003512:

בתוך ה-windbg כך:
bp 01003512
שחקו במשחק עד אשר תעצרו ב-breakpoint, כעת בצעו את הפקודה uf על מנת שנוכל לראות את הפונקציות בצורה נוחה:
ניתן לראות שיש לנו תנאי מעניין שבודק האם המשבצת שפתחנו שווה ל-80. אם אינה שווה מתבצעת קפיצה לאזור שבפוטנציאל יכול להחזיר 1 אחרת אנחנו ממשיכים רגיל בקוד ומגיעים לאזור שמחזיר לנו 0.

על מנת לבחון את הקוד בצורה יעילה נבצע 2 breakpoint חכמים, הראשון יציג לנו את הערך של ההשוואה והשני יציג לנו מה שווה ה-eax לפני שהוא מחליט להחזיר 1 בחלק הבא:

ה-breakpoint הראשון נראה כך:
bp 01003529 ".echo VALUE; db @edx L 1 ;gc"
והשני נראה כך:
bp 010035A1 ".echo CMP; ?@eax; gc"
שחקו מעט במשחק ובחנו את הערכים שהודפסו:

ניתן לראות שהפסדנו ברגע שלחצנו על משבצת שהיה בה מוקש ובנוסף הערך שהופיע ב-debugger היה 80, בשאר הפעמים שפתחנו משבצות ללא מוקש הערך היה 0.
כמו ניתן לראות שהערך ב-CMP הציג 12 ברגע שנפתחו 12 משבצות בלחיצה על הלוח. לאחר מכן פתחתי משבצת בודדה והערך שהוצג היה 13 ולאחר מכן שוב משבצת וכך הגעתי ל-14. נראה שיש 14 שדות פתוחים (מוזמנים לספור 😊)
כך שהמשחק בעצם מחליט שניצחנו ברגע שהצלחנו לפתוח את כל השדות במשחק מבלי לפגוע בשדה שערכו 80.
את ההשוואה מבצעים מול מספר שנמצא בכתובת 010057a0, שהוא מושפע מגודל הלוח שאתם בוחרים בתפריט.

במאמר הבא אנו נבנה dll כדי להזריק לתוך המשחק ואף נכתוב loader שיטען את המשחק וינצח בשבילנו.