דילוג לתוכן
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

יסודות Bash לעבודת אבטחה

pipes, הפניות, משתנים, לולאות, קודי יציאה ו-set -euo pipefail, ואז שימוש בהם כדי לספור התחברויות כושלות ולחשב hash לתיקייה.

מתחילים11 דקות קריאהמאת NotHoly

Bash הוא ה-shell שמאחורי רוב הטרמינלים של Linux. הוא רץ גם ב-macOS, בתוך קונטיינרים רבים וב-Git Bash ב-Windows, הסביבה שעליה נבדק המאמר הזה. למי שלומד אבטחה זו הדרך המהירה ביותר לשאול קובץ לוג שאלה, לבדוק שינויים בתיקייה או להריץ את אותה בדיקה על קבצים רבים. לא צריך לדעת לתכנת כדי להשתמש בו. צריך טרמינל, עורך טקסט וכשעה.

המאמר מתחיל במה שה-shell עושה, ואז מכסה את ההרגלים שהופכים עבודת shell לאמינה: pipes, הפניות, משתנים, לולאות וקודי יציאה. אחר כך מהדקים סקריפט עם set -euo pipefail. בסוף יש שני תרגילים קטנים. בראשון סופרים התחברויות SSH כושלות לכל כתובת IP בלוג לדוגמה. בשני כותבים סקריפט קצר שרושם hash של SHA-256 לכל קובץ בתיקייה.

הכול כאן רץ על קבצים שיוצרים בעצמך. שום דבר במאמר לא נוגע במערכת אחרת.

מה זה Bash

Bash הוא קיצור של Bourne Again SHell. shell קורא את השורות שמקלידים, מריץ תוכנות אחרות ומדפיס את מה שהן מפיקות. Bash הוא אחד מכמה shells, לצד sh, ‏zsh וגם fish. הוא ברירת המחדל בהפצות Linux רבות, ורוב הסקריפטים שנמצאים ברשת מניחים אותו.

שני מונחים חוזרים בכל סעיף. פקודה היא שם תוכנה יחד עם הארגומנטים שלה, כמו grep -c Failed sample.log. סקריפט הוא קובץ טקסט שמפרט פקודות, ו-Bash קורא אותו מלמעלה למטה. סקריפט מאפשר לחזור על עבודה בלי להקליד אותה מחדש, ומאפשר למישהו אחר לקרוא בדיוק מה נעשה.

המקור לכל התנהגות שמתוארת במאמר הזה הוא GNU Bash Reference Manual. כשתוצאה מפתיעה, בודקים במדריך לפני שסומכים על ניחוש.

למה זה חשוב לעבודת אבטחה

רוב נתוני האבטחה הם טקסט. לוגי אימות, לוגי גישה של שרתי web, לוגי שאילתות DNS, דוחות סורקים ורשימות hash מגיעים כולם כשורות. חלק גדול מהעבודה היומיומית הוא לשאול את השורות האלה שאלות. כמה פעמים הכתובת הזאת נכשלה בהתחברות? אילו קבצים השתנו מאז השבוע שעבר? איזה חשבון הופיע היום לראשונה?

גיליון אלקטרוני או תוכנית Python יכולים לענות על כל אחת מהשאלות האלה. Bash עונה עליהן לעיתים קרובות מהר יותר, בשורה אחת, וכל שלב נשאר גלוי. למתחילים הנראות הזאת חשובה יותר מהמהירות. כשתוצאה נראית שגויה, מריצים לבד את החלק הראשון של הצינור ורואים מה הוא הפיק.

פקודות shell מופיעות גם במדריכי התקנה, בנקודות כניסה של קונטיינרים ובמשימות אוטומציה. קריאה שלהן היא חלק מהעבודה, גם כשמעדיפים לכתוב Python.

איך זה עובד, שלב אחר שלב

Pipes שולחים פלט של תוכנה אחת לבאה

pipe, שנכתב בסימן |, מחבר את הפלט הסטנדרטי של הפקודה השמאלית לקלט הסטנדרטי של הפקודה הימנית. רוב כלי שורת הפקודה קוראים שורות, עושים עבודה אחת ומדפיסים שורות. ה-shell מחבר ביניהם.

bash
echo "one two three" | wc -w

פלט:

text
3

wc -w סופר מילים. הוא לא צריך שם קובץ כי ה-pipe מזין לו את הטקסט מהפקודה echo. תוכנות קטנות שמחוברות ב-pipes הן ההרגל המרכזי של עבודת shell.

הפניות קובעות לאן הפלט הולך

הפניה משנה מאיפה פקודה קוראת או לאן היא כותבת. > כותב פלט סטנדרטי לקובץ ומחליף את התוכן שלו. >> מוסיף לסוף. 2> שולח שגיאה סטנדרטית, הערוץ שבו תוכנות מדפיסות את התלונות שלהן, לקובץ. 2>&1 שולח שגיאה סטנדרטית לאותו מקום כמו הפלט הסטנדרטי.

bash
echo first > notes.txt
echo second >> notes.txt
cat notes.txt

פלט:

text
first
second

ה-shell פותח ומרוקן את הקובץ לפני שהפקודה רצה. לכן הפקודה הזאת מוחקת את הנתונים שהיא הייתה אמורה למיין:

bash
sort d.txt > d.txt

אחרי השורה הזאת d.txt ריק. בקובץ בדיקה, התוצאה הייתה אפס בתים. כותבים לקובץ חדש, בודקים את התוצאה, ואז מחליפים את הישן.

שגיאה סטנדרטית חשובה בעבודת אבטחה כי שגיאות מחזיקות לעיתים קרובות את התשובה. קובץ חסר או הודעת permission denied הם ראיה. הפניית שגיאות עם 2>/dev/null הופכת את הפלט לשקט יותר, אבל אז קובץ חסר נראה כמו קובץ בלי התאמות. בזמן הלמידה עדיף להשאיר שגיאות גלויות.

bash
ls missing-file 2>/dev/null
echo "status=$?"

פלט:

text
status=2

טקסט השגיאה הלך לקובץ ההשלכה, ולכן הטרמינל מציג רק את קוד הסטטוס.

משתנים ומירכאות

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

bash
target="10.0.0.5"
echo "Checking $target"

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

bash
name="a b"
printf "[%s]\n" $name
printf "[%s]\n" "$name"

פלט:

text
[a]
[b]
[a b]

הפקודה printf הראשונה קיבלה שתי מילים, כי ה-shell פיצל את הערך. השנייה קיבלה אחת. אותו דבר קורה עם קובץ בשם my report.txt כשמקלידים cat $file בלי מירכאות.

סקריפטים מקבלים ארגומנטים כמשתנים ממוספרים. $1 הוא הארגומנט הראשון, $2 השני. הביטוי "${1:-.}" פירושו "להשתמש בארגומנט הראשון אם הוא מוגדר ולא ריק, אחרת בתיקייה הנוכחית". סקריפט ה-hashing משתמש בו.

לולאות חוזרות על פקודה לכל פריט

לולאת for מריצה את הגוף שלה פעם אחת לכל מילה ברשימה. בדיקה בסוגריים מרובעים, כמו [ "$n" -eq 2 ], מחזירה 0 כשהתנאי נכון ו-1 כשהוא שגוי. משפט if משתמש בסטטוס הזה כדי לבחור ענף.

bash
for n in 1 2 3; do
  if [ "$n" -eq 2 ]; then
    echo "stop at $n"
    break
  fi
  echo "ok $n"
done

פלט:

text
ok 1
stop at 2

break יוצא מהלולאה מוקדם, ולכן הפריט השלישי אף פעם לא רץ.

קודי יציאה אומרים אם פקודה הצליחה

כל פקודה מסתיימת בסטטוס יציאה, מספר מ-0 עד 255. אפס פירושו הצלחה. כל דבר אחר פירושו כישלון, וכל תוכנה מגדירה את המספרים שלה. grep של GNU מחזיר 0 כשהוא בוחר לפחות שורה אחת, 1 כשאין התאמה, ו-2 בשגיאה. קוראים את הסטטוס עם $? בשורה הבאה.

bash
grep -q Failed sample.log
echo "match=$?"
grep -q nomatch sample.log
echo "nomatch=$?"

פלט:

text
match=0
nomatch=1

האפשרות -q מדכאת פלט וקובעת רק את הסטטוס, וכך אפשר להשתמש בפקודה grep בתוך if בלי להדפיס כלום.

לצינור יש סטטוס אחד, וכברירת מחדל זה הסטטוס של הפקודה האחרונה שלו. הסעיף הבא משנה את זה.

מצב קפדני: set -euo pipefail

Bash ממשיך לרוץ אחרי שגיאות אלא אם אומרים לו אחרת. שלוש אפשרויות גורמות לסקריפט להיכשל מוקדם. הן מתועדות תחת ה-builtin ‏set במדריך.

  • set -e עוצר את הסקריפט כשפקודה נכשלת. פקודות בבדיקת if, ופקודות ברשימת && או || מלבד האחרונה, לא עוצרות אותו, כי Bash מצפה שהן ייכשלו לפעמים.
  • set -u מתייחס למשתנה לא מוגדר כשגיאה, כך ששגיאת הקלדה בשם משתנה עוצרת את ההרצה במקום להפוך בשקט למחרוזת ריקה.
  • set -o pipefail גורם לצינור להיכשל אם פקודה כלשהי בו נכשלת, ולא רק האחרונה.

קודם התנהגות ברירת המחדל:

bash
bash -c 'false | true; echo "status=$?"'

פלט:

text
status=0

הפקודה false נכשלה, אבל true הגיעה אחרונה והצליחה, ולכן הצינור דיווח על הצלחה. עם pipefail:

bash
bash -c 'set -o pipefail; false | true; echo "status=$?"'

פלט:

text
status=1

עכשיו set -e עם פקודה שנכשלת:

bash
bash -c 'set -e; echo one; false; echo two'

פלט:

text
one

הסקריפט נעצר לפני two. הקריאה bash -c עצמה יצאה עם סטטוס 1.

set -u תופס ארגומנט חסר:

bash
bash -c 'set -u; echo "$1"' _

פלט:

text
_: line 1: $1: unbound variable

הצורה המשולבת, set -euo pipefail, היא מה שרוב האנשים מתכוונים אליו במצב קפדני. יש לזה מחיר. חיפוש שלא מוצא כלום הוא תוצאה רגילה, אבל grep מדווח עליה בסטטוס 1, ולכן במצב קפדני הסקריפט נעצר. בודקים את המקרה לפני שמסתמכים עליו:

bash
bash -c 'set -euo pipefail; grep "Nope" sample.log | wc -l; echo "after"'

פלט:

text
0

הסקריפט הדפיס את הספירה ואז נעצר בלי להדפיס after. אם "אין התאמות" היא תוצאה תקינה עבור הסקריפט שלך, מוסיפים || true אחרי הצינור, או בודקים את הסטטוס בעצמך.

תרגיל מעשי: ספירת התחברויות כושלות לכל IP

בונים קובץ לוג קטן, ואז סופרים כמה פעמים כל כתובת מקור מופיעה בשורות של סיסמה שגויה. הלוג סינתטי. הכתובות שלו מגיעות מהטווח הפרטי 10.0.0.0/8, והשורות הולכות לפי הפורמט של הודעות OpenSSH במארח מעבדה. לוגי אימות אמיתיים מכילים כתובות של אנשים אמיתיים, ולכן מתייחסים אליהם כמידע פרטי וקוראים רק לוגים שמותר לך לקרוא.

יצירת הלוג לדוגמה

יוצרים תיקייה למעבדה הזאת, נכנסים אליה וכותבים את הקובץ. המחרוזת 'EOF' במירכאות מונעת מ-Bash להרחיב משהו בתוך הטקסט.

bash
cat > sample.log <<'EOF'
Jan 10 10:00:01 lab sshd[1]: Failed password for root from 10.0.0.5 port 5000 ssh2
Jan 10 10:00:03 lab sshd[2]: Failed password for admin from 10.0.0.5 port 5001 ssh2
Jan 10 10:01:00 lab sshd[3]: Accepted publickey for alice from 10.0.0.9 port 5002 ssh2
Jan 10 10:02:00 lab sshd[4]: Failed password for bob from 10.0.0.7 port 5003 ssh2
EOF

בודקים את מספר השורות עם wc -l sample.log. הוא מדווח 4.

ספירת הכישלונות בצינור אחד

bash
grep 'Failed password' sample.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -nr

פלט:

text
      2 10.0.0.5
      1 10.0.0.7

קוראים את הצינור שלב אחר שלב. מריצים לבד את שתי הפקודות הראשונות כדי לראות מה כל שלב תורם.

  1. grep 'Failed password' sample.log שומר את שלוש השורות הכושלות ומשמיט את השורה עם Accepted. מאשרים את הספירה עם grep -c 'Failed password' sample.log, שמדפיס 3.
  2. awk '{print $(NF-3)}' מדפיס שדה אחד מכל שורה. NF מחזיק את מספר השדות בשורה הנוכחית. הסיומת של כל שורה היא from ADDRESS port PORT ssh2, ולכן הכתובת היא השדה הרביעי מהסוף, כלומר $(NF-3). מאשרים את זה בשורה הראשונה:
bash
sed -n '1p' sample.log | awk '{print NF, $(NF-3)}'

פלט:

text
14 10.0.0.5

לשורה יש 14 שדות, ושדה 11 הוא הכתובת.

  1. sort מניח כתובות זהות זו ליד זו. uniq -c סופר כפילויות סמוכות ומדפיס את הספירה לפניהן. בלי הפקודה sort, ‏uniq היה סופר רק חזרות שכנות, ולכן המיון תמיד בא קודם.
  2. sort -nr ממיין את הספירות מספרית, מהגדולה לקטנה.

מספר השדה של awk שייך לפורמט הלוג הזה. אם מכוונים את אותה פקודה ללוג אחר, בודקים קודם את מיקום השדה עם השורה של sed שלמעלה. מספר שעבד פעם אחת הוא לא הוכחה שהוא מתאים לקובץ הבא.

אפשר לקבל את אותה תוצאה מתוכנית awk אחת, כי מערכים של awk סופרים בשבילך:

bash
grep 'Failed password' sample.log | awk '{c[$(NF-3)]++} END {for (ip in c) print c[ip], ip}'

פלט:

text
2 10.0.0.5
1 10.0.0.7

awk לא מבטיח סדר עבור for (ip in c), ולכן ממיינים את התוצאה אם הסדר חשוב. GNU Awk User's Guide מכסה מערכים ואת הכלל END.

תרגיל מעשי: hash לכל קובץ בתיקייה

hash הוא טביעת אצבע באורך קבוע של תוכן קובץ. אם רושמים היום את ה-hash של SHA-256 לכל קובץ בתיקייה, אפשר להריץ את אותה פקודה בשבוע הבא ולראות אילו קבצים השתנו. hash לא מוכיח מי ביצע שינוי או אם קובץ בטוח. הוא מראה רק אם הבתים השתנו מאז שנרשמו.

כתיבת הסקריפט

יוצרים את hashall.sh באותה תיקייה, באותו דפוס של heredoc במירכאות.

bash
cat > hashall.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
for f in "${1:-.}"/*; do [ -f "$f" ] && sha256sum "$f"; done
EOF

קוראים את הסקריפט שורה אחר שורה:

  • השורה הראשונה היא ה-shebang. היא אומרת למערכת להריץ את הקובץ עם Bash, שנמצא דרך env.
  • set -euo pipefail מפעיל מצב קפדני, כפי שתואר קודם.
  • "${1:-.}"/* הוא התיקייה שמעבירים, או התיקייה הנוכחית כשלא מעבירים כלום. הסימן * הוא glob. ה-shell מחליף אותו בשמות שבתיקייה, ממוינים. שמות שמתחילים בנקודה מדולגים כברירת מחדל, כפי שסעיף filename expansion במדריך מתאר.
  • [ -f "$f" ] && sha256sum "$f" מחשב hash רק לקבצים רגילים. sha256sum על תיקייה היה מדפיס שגיאה, ולכן הבדיקה מסננת תיקיות.

נותנים לסקריפט הרשאת הרצה ומריצים אותו על התיקייה הנוכחית:

bash
chmod +x hashall.sh
./hashall.sh .

chmod +x מוסיף הרשאת הרצה. הקידומת ./ אומרת ל-shell להריץ את הקובץ בתיקייה הזאת, כי כברירת מחדל התיקייה הנוכחית לא נמצאת במשתנה PATH.

פלט בתיקייה שהחזיקה רק את hashall.sh ואת sample.log:

text
288797e3d2baa72b086c5fca9821282d06726cb44272647d41128ababf255c94 *./hashall.sh
2c086498f19a2a8ca7fe112417ad8113449bf640bceabd4795808bd00a996893 *./sample.log

Git Bash מדפיס כוכבית לפני כל שם, וזה מסמן מצב בינארי ב-GNU coreutils. מערכות אחרות עשויות להציג רווח במקום. ה-hash עצמו נשאר זהה. אם הערכים אצלך שונים, בודקים שהקבצים שלך תואמים לטקסט בית אחר בית, כולל סיומות שורה.

מאשרים ערך אחד ידנית:

bash
sha256sum sample.log

זה מדפיס עבור sample.log את אותו hash שהסקריפט הדפיס.

איך hash משתנה

מוסיפים שורה אחת ללוג, ואז מחשבים שוב את ה-hash.

bash
echo "x" >> sample.log
sha256sum sample.log

בהרצת הבדיקה ה-hash החדש היה 3084d4c5a7659a2f3636b6079636b081b7ab1b6fca6a5e691c3afdd160959687, כשקודם היה 2c086498f19a2a8ca7fe112417ad8113449bf640bceabd4795808bd00a996893. הקובץ גדל בשני בתים וטביעת האצבע השתנתה איתו. לפני שממשיכים, משחזרים את הקובץ המקורי מהעותק שלך.

טעויות נפוצות

משתנים ונתיבים בלי מירכאות. cat $file נשבר על שמות עם רווחים. כברירת מחדל שמים מירכאות, ומסירים אותן רק כשברור שהערך לא יכול להכיל רווחים או wildcards.

דריסת קובץ קלט עם >. sort d.txt > d.txt מרוקן את הקובץ לפני שהפקודה sort קוראת אותו. כותבים לקובץ חדש, בודקים את הפלט ואז מעבירים אותו למקום.

אמון במספר שדה. $(NF-3) עבד על הדוגמה. בודקים את מיקום השדה על כמה שורות אמיתיות לפני שבונים עליו דוח.

מצב קפדני שנעצר על "אין התאמה" תקין. כפי שהודגם למעלה, grep שמחזיר 1 מסיים צינור תחת pipefail. מחליטים אם היעדר התאמות הוא שגיאה עבור הסקריפט, ומטפלים בזה בכוונה.

לולאה שמדווחת על סטטוס שגוי. הסקריפט hashall.sh שלמעלה מחשב hash לכל קובץ רגיל. אבל כשהשם האחרון בתיקייה הוא תת־תיקייה, הסקריפט מדפיס את כל ה-hashes ואז יוצא עם סטטוס 1. הסטטוס הגיע מהבדיקה האחרונה [ -f ], שנכשלה. הצורה המפורשת עם if עושה את אותה עבודה ויוצאת עם 0:

bash
#!/usr/bin/env bash
set -euo pipefail
for f in "${1:-.}"/*; do
  if [ -f "$f" ]; then sha256sum "$f"; fi
done

בתיקיית בדיקה שבה תת־תיקייה הייתה הרשומה האחרונה, הגרסה הזאת הדפיסה hash לכל קובץ ויצאה עם סטטוס 0. הגרסה עם && יוצאת גם עם סטטוס 1 על תיקייה ריקה, ולכן הצורה עם if היא ברירת המחדל העדיפה.

הרצת סקריפטים שלא כתבת. סקריפט יכול למחוק או לשלוח קבצים באותה קלות שבה הוא מחשב להם hash. קוראים קודם את כל הקובץ, בודקים כל פקודה שהוא קורא לה ומריצים אותו על עותק של התיקייה.

איך ממשיכים

קוראים את הסעיפים של GNU Bash Reference Manual על pipelines, ‏redirections, ‏shell parameter expansion ועל ה-builtin ‏set. הם מכסים את רוב מה שהופיע כאן. החומר על סטטוס יציאה שימושי כהפניה כשסקריפט מתנהג בצורה לא צפויה.

אחר כך קוראים את המדריך של GNU grep. כדאי לנסות grep -i, grep -v וגם grep -E על הלוג לדוגמה ולרשום איך כל אפשרות משנה את סטטוס היציאה.

GNU Awk User's Guide מכסה שדות, תבניות ומערכים. המונה בשורה אחת שלמעלה הוא דוגמה קטנה למה ש-awk יכול לעשות. כשזה מרגיש טבעי, כותבים תבנית שנייה שסופרת התחברויות מוצלחות לכל משתמש.

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

מקורות

מקורות

  1. GNU Bash Reference Manual
  2. GNU grep manual
  3. GNU Awk User's Guide

עוד בנושא Linux וכלי פיתוח

Linux וכלי פיתוח

הרשאות ב-Linux: משתמשים, קבוצות, chmod ו-umask

בעלים וקבוצות ב-Linux, ביטי rwx,‏ chmod,‏ umask ו-setuid, עם מעבדה שבודקת גישה כמשתמש שני ומוצאת הרשאות קבצים מסוכנות.

מתחילים10 דקות קריאה