10.4 ניתוח CVE ו n day תרגול
תרגול - ניתוח CVE ו-n-day¶
בתרגול הזה נשחק את התפקיד של חוקר n-day. נקבל טלאי (patch) של תוכנית קטנה בדיוק כמו שהוא מופיע ב-advisory, נצטרך לזהות מהו הבאג שהוכנס ואז תוקן, ולכתוב PoC שמקריס את הגרסה הvulnerable. שימו לב: פה לא נלמד את הבאג מראש, המטרה היא שתגלו אותו בעצמכם מהטלאי, בדיוק כמו במציאות.
רקע - החולשה שקיבלנו¶
נניח שיצא advisory כזה:
Advisory: KVAULT-2024-002
Component: kvault key-value store
Affected: kvault <= 2.1.0
Fixed in: kvault 2.1.1
Type: CWE-787 Out-of-bounds Write in set_value()
Summary: A crafted length value sent to the SET command can cause a write
past the end of a stack buffer.
ה-advisory מפנה ל-commit שתיקן, וזה ה-diff המלא שלו:
--- a/store.c
+++ b/store.c
@@ -20,7 +20,7 @@ void set_value(void) {
if (read_exact(0, &len, sizeof(len)) < 0)
return;
- if (len > MAX_VALUE) {
+ if (len < 0 || len > MAX_VALUE) {
puts("value too long");
return;
}
זה כל השינוי. שורה אחת. הגרסה הvulnerable המלאה (kvault 2.1.0, לפני הטלאי):
// store.c - kvault 2.1.0 (the vulnerable version)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAX_VALUE 256
static int read_exact(int fd, void *buf, size_t n) {
size_t got = 0;
while (got < n) {
ssize_t r = read(fd, (char *)buf + got, n - got);
if (r <= 0) return -1;
got += r;
}
return 0;
}
void set_value(void) {
char value[MAX_VALUE];
int len;
if (read_exact(0, &len, sizeof(len)) < 0) // reads 4 bytes as a signed int
return;
if (len > MAX_VALUE) {
puts("value too long");
return;
}
read(0, value, len); // the attacker controls len
printf("stored %d bytes\n", len);
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0);
puts("kvault 2.1.0 - SET");
set_value();
return 0;
}
הפרוטוקול פשוט: התוכנית קוראת 4 בתים בתור אורך (מספר שלם עם סימן, little-endian), ואז קוראת את מספר הבתים הזה אל תוך value.
תרגיל 1 - לזהות את הבאג מהטלאי¶
לפני שנוגעים במקלדת, קוראים את ה-diff כמו חוקר.
המשימות:
- הטלאי שינה בדיוק תנאי אחד: מ-
if (len > MAX_VALUE)ל-if (len < 0 || len > MAX_VALUE). נסחו במשפט אחד מה הגרסה הישנה פספסה. - שימו לב לטיפוס של
len. הואintעם סימן, ונקרא ישירות מ-4 בתים שהתוקף שולט בהם. איזה טווח ערכים הוא יכול לקבל, וכולל אילו ערכים שליליים? - הסתכלו על הקריאה
read(0, value, len). מהו הטיפוס של הארגומנט השלישי שלreadלפי חתימת הפונקציה? מה קורה לערךlenשלילי כשהוא מומר לטיפוס הזה? - הסיקו: מהו הפרימיטיב שהתוקף מקבל? באיזה CWE מדובר, והאם זה תואם ל-advisory?
רמז: הבדיקה len > MAX_VALUE מגנה רק מפני מספרים גדולים מדי. היא לא אומרת כלום על מספרים קטנים מ-0.
רמז: הארגומנט השלישי של read הוא size_t, שהוא חסר סימן. מה הופך -1 כשמפרשים אותו כ-size_t ברוחב 64 ביט?
תרגיל 2 - לבנות ולהריץ את הגרסה הvulnerable¶
עכשיו נהפוך את התיאוריה לקובץ שאפשר לתקוף.
המשימות:
- שמרו את הגרסה הvulnerable כ-
store.cוהדרו אותה. התחילו בלי canary כדי לראות קריסה נקייה:
- הריצו
checksec --file=./storeורשמו את מצב ה-RELRO, Canary, NX ו-PIE. - הריצו את התוכנית ידנית עם קלט תקין (אורך קטן וכמה בתים) וודאו שהיא מתנהגת כרגיל. חשוב לוודא שהמסלול התקין עובד לפני שתוקפים אותו.
- פתחו את
set_valueב-GDB עםdisassemble set_valueומצאו באיזו כתובת יחסית ל-rbpיושב הbuffervalue. כמה בתים מתחילתvalueועד הreturn address?
רמז: הbuffer בגודל 256, אז ההסטה לreturn address היא בסביבות 256 + 8. אמתו את המספר המדויק מהדיסאסמבלי, הקומפיילר יכול להוסיף padding alignment.
תרגיל 3 - לכתוב PoC שמקריס¶
זה התרגיל המרכזי. המטרה: להפעיל את הבאג ולגרום לקריסה, בלי לנחש בעיוורון.
המשימות:
- חשבו איזה אורך לשלוח כדי שהבדיקה
len > MAX_VALUEתעבור בשלום, אבלreadתכתוב הרבה מעבר ל-256 בתים. שימו לב שמספר גדול חיובי (למשל 999999) ייחסם על ידי הבדיקה, אז החשיבה צריכה ללכת לכיוון אחר. - כתבו סקריפט pwntools שמפעיל את
process('./store'), שולח את שדה האורך (4 בתים), ואז שולח מספיק בתים כדי לגלוש ולדרוס את כתובת החזרה. - הריצו ובדקו שקיבלתם
SIGSEGV. זה ה-PoC שלכם. - הידרו מחדש בלי
-fno-stack-protector(כלומר עם canary), הריצו שוב את אותו PoC, וראו איך הקריסה משתנה מ-SIGSEGV ל-stack smashing detected. שתיהן קריסות תקפות שמוכיחות את הבאג.
רמז: אם len שלילי, הוא הופך למספר עצום כ-size_t. read תקרא כמה בתים שתשלחו לתוך buffer של 256. שלחו למשל 400 בתים.
רמז: את שדה האורך שולחים כ-4 בתים בינאריים. הערך -1 הוא p32(0xffffffff), והוא עובר את הבדיקה len > MAX_VALUE כי כ-int עם סימן הוא שלילי.
רמז לשלד ה-PoC:
from pwn import *
context.binary = elf = ELF('./store')
p = process('./store')
p.recvuntil(b'SET')
p.send(p32(????)) # the length field - what value?
p.send(b'A' * 400) # the bytes that overflow
print(p.recvall(timeout=2))
בונוס - להסביר למה הטלאי מספיק¶
הטלאי הוסיף רק len < 0. הוכיחו לעצמכם שאחרי התיקון ה-PoC שלכם כבר לא עובד: הידרו את הגרסה המתוקנת (עם התנאי החדש) והריצו מולה את אותו PoC. הסבירו בדיוק איזה קלט מה-PoC נחסם עכשיו, ולמה מספר גדול חיובי לא היה עוזר לתוקף כבר בגרסה הישנה (הבדיקה len > MAX_VALUE תפסה אותו). זו בדיוק סוג הבדיקה שחוקר עושה כדי לוודא שהוא הבין את שורש הבעיה נכון.