در static analysis ما برنامه را اجرا نمی‌کنیم و معمولاً فقط کد (یا کد decompile یا disassemble شده) آن را بررسی می‌کنیم. توضیح این روش از حوصله این مقاله خارج است (خودمونیش این میشه که گوگل کنید).

چرا static analysis؟

من یک مهندس امنیت محصول (ترجمه product security engineer) هستم و معمولا به کد محصولات دسترسی دارم. بررسی کد یکی از مهمترین بخشهای کار من است. محصولات نرم‌افزاری مدرن (و به خصوص بازیهای کامپیوتری) ملغمه ای از چند هزار کتابخانه و فریمورک هستند و بررسی دستی کد آنها غیرممکن است. به عنوان مثال یک بازی کامپیوتری حداقل چندین میلیون خط کد دارد:

  1. کد client بازی که روی کامپیوتر و یا کنسول اجرا می‌شود (معمولاً ++C).
  2. کد سرورهای مختلف مانند login, lobby, matchmaking و غیره که می‌تواند به هر زبانی باشد.
  3. کد برنامه موبایل همراه بازی مانند FIFA 21 Mobile companion app. زبان برنامه نویسی معمولاً جاوا (اندروید) و swift (برای iOS) است.
  4. کد integration با سرویسهای دیگر مانند market place و یا Steam.
  5. کد زیرساخت ابری (منظور اینجا infrastructure-as-code مثل Terraform است که فارسیش نمی‌دونم چیِ).

برای بررسی این همه کد باید به چندین زبان برنامه‌نویسی و فریمورک عمومی و خصوصی (ترجمه آزاد proprietary) مختلف مسلط بود (که تقریبا امکان ندارد) و از ابزارهای مختلف استفاده کرد.

استفاده از grep

مهمترین اسلحه من برای بررسی کد، grep است. معمولاً به دنبال کلمات کلیدی در کد می‌گردم تا قسمت‌های مهم را پیدا کنم. مثلاً grep -ir password در تمامی فایلهای دایرکتوری حاضر (و فرزندانش) به دنبال کلمه password (صرفنظر از حروف کوچک و بزرگ) می‌گردد.

در چند سال اخیر از برنامه ripgrep که با زبان برنامه‌نویسی Rust نوشته شده است، استفاده می‌کنم. به عنوان یک gopher سابق همیشه به شوخی می‌گویم که بالاخره Rust هم یک فایده‌ای داشت. تا یک سال قبل شاید 90 درصد باگ‌های امنیتی در کد را با grep پیدا کرده بودم. بعد از مدتی سروکله زدن با فریمورک‌ها و زبانهای برنامه‌نویسی مختلف لیستی از کلمات مهم درست می‌کنید و به دنبال آنها می‌گردید.

نقطه ضعف grep

بزرگترین مشکل grep برای تحلیل کد، ندانستن مفهوم کلمات است. grep برای جستجوی متن طراحی شده و برایش مهم نیست که این کلمه در آن زبان برنامه‌نویسی چه تایپی دارد (مثلاً تابع یا کامنت یا غیره). اگر به دنبال کلمه password در کد Go زیر بگردیم چهار نتیجه مختلف داریم:

// nem.go
package main
func main() {
    // کلمه پسورد اینجا بخشی از کامنت است
    // Hardcoded passwords are bad.

    // کلمه پسورد در اینجا نام متغیر است
    password := "hunter2"

    // کلمه پسورد در اینجا بخشی از یک استرینگ است
    errorMsg := "Incorrect password"
}

// کلمه پسورد در اینجا بخشی از نامِ تابع است
func validatePassword(p string) bool {
    // Do something
    return true
}

فرض کنیم هدف من توابعی است که در نام خود کلمه پسورد را دارند. در اینجا با استفاده از grep باید چهار نتیجه را بررسی کنم تا به جواب برسم. شاید بگویید که این مشکلی نیست ولی، در یک برنامه واقعی با میلیونها خط کد، هر جستجو صدها نتیجه بی‌ربط (false positive) دارد.

یک تکنیک من برای حل این مشکل جستجوی پرانتز به همراه نام تابع بود.

این تا حدی کمک می‌کند ولی مواردی که فاصله یا whitespace بین پرانتز و کلمه پسورد وجود دارد را پیدا نمی‌کند. می‌توانم با استفاده از regular expression جستجوی خود را بهتر کنم ولی در انتها راهی وجود ندارد که به grep بفهمانم که فقط به دنبال نام تابع بگردد.

ورود Semgrep

وز آشنایی با Semgrep یکی از بهترین روزهای زندگی شغلی من بود. با استفاده از Semgrep می‌توانم چند پله بالاتر از grep عمل کنم و به برنامه بفهمانم که فقط در یک تایپ خاص به دنبال کلمات بگردد. نمی‌خواهم این پست را به “آموزش Semgrep” تبدیل کنم. برای این کار از https://semgrep.dev/learn شروع کنید. ولی، چند مثال کوتاه را توضیح می‌دهم.

https://semgrep.dev/learn/2

می‌خواهیم در کد Python زیر همه مواردی که تابع logging.info با پارامتر تابع get_user فراخوانی شده را پیدا کنیم.

import logging as lg

def get_user(uid):
    d = {1: "harry", 2: "ron", 3: "hermione"}
    return d[uid]

# Match both of these using an ellipsis.
logging.info(get_user(1)
    + " logged in")
lg.info(get_user(2)
    + " logged in")

اگر از grep استفاده کنیم:

اینجا مورد اول پیدا شد و مورد دوم نه. چرا؟ چون grep نمی‌داند که lg در اینجا معادل logging است. با Semgrep این مشکل را نداریم چون می‌داند که اینجا lg و logging یکی هستند. … هم همان غیره خودمان است که با همه چیز match می‌شود (توضیح بیشترش را در خود آموزش بخوانید).

https://semgrep.dev/learn/6

اینجا به بحث شیرین metavariable می‌رسیم که می‌تواند جایگزین هر آیتم باشند. اگر بخواهیم همه توابع را در Python پیدا کنیم:

def $FUNC(...):
    ...

حالا می‌توانیم داخل این توابع جستجو کنیم. در اینجا فقط یک جستجوی ساده انجام می‌دهیم. می‌خواهیم که ببنیم آیا تابع با یک مِتُد از requests تمام می‌شود؟

حالا فرض کنید بخواهیم یک rule برای امنیت بنویسیم. می‌خواهیم چک کنیم که آیا ورودی تابع در پارامترهای مِتُدِ requests وجود دارد؟ چرا این کار ناامن است؟ فرض می‌کنیم که ورودی تابع مستقیماً از ورودی کاربر است و اگر به کاربر اجازه دهیم تا هر URL را دریافت کند ممکن است به مشکل SSRF بربخوریم. rule ما به این صورت است.

def $FUNC($USERINPUT):
    ...
    requests.$METHOD(...,$USERINPUT,...)

در خط اول یک metavariable تعریف کرده‌ام که به جای ورودی تابع است. سپس، چک می‌کنیم که ورودی به مِتُد می‌رسد یا خیر. یک مِتُد امن هم به کد اضافه کرده‌ام که نباید در نتایج باشد.

با metavariable ها کارهای عجیب غریبی می‌توانیم انجام بدهیم. مشابه همین کاری که کردیم را در بخش 8 آموزش می‌بینیم.

https://semgrep.dev/learn/8

اگر در Python یک فایل را برای خواندن باز کرده باشیم دیگر نباید به آن بنویسیم. در اینجا metavariable متغیر حاوی هَندِلِ فایل است. سپس می‌توانیم چک کنیم که آیا متد write را برای آن فراخوانی کرده‌ایم. پس rule ما این شکل می‌شود:

https://semgrep.dev/learn/13

درس 13 جالب است. می‌توانیم توسط pattern-inside بخشهایی که می‌خواهیم را جدا کنیم و سپس داخل آنها را با pattern بگردیم. اینجا می‌خواهیم که داخل توابع این کُدِ Go:

  1. آیا پارامتر ورودی از نوع http.ResponseWriter هست؟
  2. آیا مِتُدِ Write روی آن فراخوانی شده است؟

در ابتدا rule به این صورت است. اول همه توابع توسط pattern-inside انتخاب می‌شوند و بعد در داخل آنها به دنبال متد Write می‌گردد.

باید pattern-inside را دستکاری کنیم تا تنها توابعی را پیدا کند که یکی از پارامترهای ورودیشان از نوع http.ResponseWriter است. با گذاشتن … قبل و بعد پارامتر به Semgrep می‌گوییم که این پارامتر می‌تواند هرجا باشد.

- pattern-inside: |
    func $FUNC(..., $WRITER http.ResponseWriter, ...) {
      ...
    }    

حالا می‌توانیم توسط pattern چک کنیم که آیا مِتُدِ Write روی این ورودی فراخوانی شده یا خیر.

حل مساله اول

حتماً کل آموزش را تا انتها ادامه دهید ولی حل تک تک آنها در این پست فایده‌ای ندارد. بجای آن می‌خواهم مشکلی که در اول داشتیم را حل کنم. مشکل ما این بود که میخواستیم توابعی که در نام آنها کلمه password وجود دارد را پیدا کنیم. می‌توانید به صورت عملی راه‌حلهای خودتان را در این آدرس امتحان کنید https://semgrep.dev/s/WODo.

در مرحله اول یک pattern می‌نویسیم تا همه توابع را پیدا کند. این کار را قبلا انجام داده‌ایم و چیز عجیبی نیست.

جواب جستجو هر دو تابع را پیدا کرد. دقت کنید که الان نام تابع در metavariable به نام FUNC ذخیره شده. حالا، می‌توانیم از قابلیت metavariable-regex استفاده کنیم و یک regex را روی آن اجرا کنیم.

rules:
  - id: password-in-func-name
    languages:
      - go
    message: Find functions that have password in their name.
    patterns:
      - pattern: |
          func $FUNC(...) {
            ...
          }          
      - metavariable-regex:
          metavariable: $FUNC
          regex: .*password.*
    severity: ERROR

ولی باز هم کار نمی‌کند چون regex به صورت case-sensitive اجرا می‌شود. برای اجرای آن به صورت case-insensitive می‌توانیم از inline flag استفاده کنیم.

regex: (?i).*password.*

و جوابمان را گرفتیم.

چی یاد گرفتیم؟

یاد گرفتیم که با استفاده از Semgrep راحت‌تر داخل کد جستجو کنیم. با Semgrep کارهای خیلی عجیب غریبی کرده‌ام که خودم هم باورم نمی‌شود. شما هم می‌توانید از این ابزار مجانی در کار خود استفاده کنید و به قولی sky is the limit.

نقشه من برای بلاگ بعدی:

  1. چرا از Semgrep (مثلا به جای CodeQL) استفاده می‌کنم.
  2. چگونه از Semgrep در CI/CD استفاده می‌کنم.