بایگانی ماهیانه: خرداد 1401

چرا من از لینوکس استفاده می‌کنم : اتوماتیک کردن کارهام

امروز تصمیم گرفتم یکسری پست داخل وبلاگ داشته باشم که توی اون ها توضیح بدم که چرا از لینوکس استفاده میکنم و چجوری توی کار هام بهم کمک می‌کنه .
این اولین پست از این مجموعه پست ها هست و قراره توضیح بدم که لینوکس چطوری توی رزرو غذای دانشگاه بهم کمک می‌کنه.

الان که دارم این پست رو می‌نویسم دانشجوی ترم دو ارشد هستم و این اولین ترمی هست که بعد از تموم شدن کرونا داره به شکل حضوری برگزار میشه.
اینجا برای رزرو غذای هفته بعد باید یک هفته زودتر غذاتون رو رزرو کنید و اگر تا ساعت ۲ بعد از ظهر روز چهارشنبه هفته جاری غذایی رزرو نکنید، هفته بعد کاملا بدون غذا میمونید و باید غذای روزفروش بگیرید که قیمتش چند برابر غذای رزروی هست و عملا برای یک دانشجو نمی‌ارزه 🙂
من رزرو غذام رو هیچ وقت فراموش نمی‌کنم اما الان به ذهنم رسید خوبه که هر هفته یک ایمیل هم به شکل خودکار برام ارسال بشه که این رو بهم یاد‌آوری کنه .این کار هم باعث میشه چیز های جدید یاد بگیرم و هم خغن تره .

کاری که می‌خوام بکنم این هست که به شکل خودکار هر هفته روز های یکشنبه ساعت ۸ شب که تقریبا مطمئنم سیستمم روشنه یک ایمیل برام بیاد که رزرو غذا رو بهم یاد آوری کنه. البته اگر سیستمم هم خاموش باشه مشکلی نیست و راه حلش استفاده از anacron هست ولی من اینجا از اون استفاده نمی‌کنم

برای این کار به دوتا چیز نیاز دارم :
۱- اول اینکه بتونم یک ایمیل ارسال کنم
۲- بتونم این کار رو به شکل خودکار توی ساعت و روز خاصی انجام بدم

برای مورد اول از ترمینال لینوکس استفاده می‌کنم و با استفاده از آدرس ایمیل خودم به خودم ایمیل می‌زنم . اگر نمی‌دونید که چطوری از ترمینال ایمیل بفرستید کافیه مراحلی که اینجا گفته شده رو دنبال کنید .
برای قسمت دوم هم از cron job داخل لینوکس استفاده می‌کنم
cron job ها داخل لینوکس این امکان رو به ما میدن که بتونیم یکسری کار ها رو داخل زمان های مشخصی به شکل schedule انجام بدیم مثلا بهش بگیم که هر هفته ساعت ۲ شب که همه خوابن یک بکاپ از سیستم بگیر و اون رو روی هارد فلان با اسم و تاریخ فلان ذخیره کن.

خب اول یک برنامه یک خطی می‌نویسم و توی فایلی به نام food_reserve.sh ذخیره می‌کنم
کد برنامه میگه که یک ایمیل با متن reserve your food otherwise you have egg و موضوع Reserve Food برای ایمیل من که sinasoheili79[at]gmail.com هست ارسال کن

#!/bin/sh

echo "reserve your food otherwise you have egg" | mail -s "Reserve Food" sinasoheili79@gmail.com

حالا باید به cron بگیم که این فایل رو هر یکشنبه ساعت 8 شب که میشه ساعت 20 اجرا کن . برای این کار دستور crontab -e رو اجرا میکنم و محتوای فایلی که باز میشه رو به شکل زیر تغییر میدم

# m h  dom mon dow   command
  0 20   *   *   0    ~/bin/./food_email.sh

اگر براتون سوال هست که چرا مقدار dow رو 0 گذاشتم دلیلش این هست که خارجی ها روز اول هفتشون دو شنبه هست و توی کامپیوتر ما از 0 شروع می‌کنیم به شمردن پس روز یکشنبه رو باید 0 در نظر بگیریم. اگر برای مشخص کردن زمان داخل crontab شک دارید می‌تونید از این استفاده کنید.

پی‌نوشت : مسلما راه های ساده‌تری مثل استفاده از تقویم یا برنامه های reminder هم هست ولی من ترجیح میدم از این روش استفاده کنم چون باعث میشه با پیاده کردن همین برنامه ساده چیز های جدید یادبگیرم


حمله درب پشتی از طریق کامپایلر

حمله‌ی درب پشتی از طریق کامپایلر یا Compiler backdoor attack یکی از حمله هایی است که هنوز به شکل رسمی روشی برای جلوگیری از آن معرفی نشده است
توی این پست روش کار این حمله رو توضیح میدم و سعی میکنم اون رو برای کامپایلر زبان C پیاده کنم.

معرفی

کن تامسون، برنامه نویس سیستم عامل یونیکس در سال 1984 در مراسم Turing Award داخل سخنرانیش حمله درب پشتی از طریق کامپایلر رو معرفی و در موردش صحبت می‌کنه.
در سال 2015 نرم افزار XcodeGhost که برای ساخت برنامه های شرکت اپل از اون استفاده می‌شده از همین تکنیک استفاده می‌کرده و گفته میشه که این اولین حمله در مقیاس بزرگ برای اپ استور اپل بوده و باعث شده بیش از 4000 برنامه داخل فروشگاه اپل آلوده بشه.
همانطور که از اسم حمله هم مشخصه، کامپایلر شما زمانی که می‌خواهد کد شما رو کمپایل کنه بدون سر و صدا یک در پشتی رو داخل کد های شما ایجاد میکنه
شما هم که از این درب پشتی خبر ندارید پس برنامه خودتون رو منتشر می‌کنید و این شروع ماجراست. .

چرا این حمله مهمه ؟

برای جواب دادن به این سوال نیاز هست که کمی عمیق تر به مشکل نگاه کنیم.
بذارید اینجوری شروع کنیم که اصلا از کجا می‌تونیم مطمئن باشیم که کامپایلری که الان ازش استفاده می‌کنیم توی برنامه هامون درب پشتی درست نمیکنه ؟
احتمالا جواب این هست که خب کد های مربوط به کامپایلر به شکل open source هست و اگر چنین مشکلی وجود داشته باشه حتما هستند کسایی که اون کد رو بخونن و متوجه چنین مشکلی بشن.
حالا سوال جدید این هست که اگر فرض کنیم کد مربوط به کامپایلر منبع باز باشه و کسی هم باشه که اون کد رو کنترل کنه و بخونه، کد مربوط به خود کامپایلر نیاز داره که توسط یک کامپایلر دیگه کامپایل بشه و ما چطوری می‌تونیم مطمئن بشیم که اون کامپایلر، درب پشتی رو داخل کامپلر اول ایجاد نمی‌کنه ؟
ممکنه بگید که خب می‌تونیم از یک disassembler استفاده کنیم و چک کنیم که فایل های اجرایی کامپایلر ما کد مخربی داخلشون نباشه.
اما این روش هم جوابگو نیست چون disassembler هم در نهایت یک برنامه است و ممکنه خود این برنامه هم رفتار بدی از خودش نشون بده و زمانی که می‌خواهد کد های مربوط به کامپایلر رو برای ما ایجاد کنه، کد های بد رو در داخل سورس کد مخفی کنه و نمایش نده.
ممکنه دوباره در جواب بگید که خب این روش خیلی سخته و ممکنه اصلا کسی سراغ عملی کردن این مسئله نره و در جواب باید بگم که ساخت این کامپایلر خیلی ساده تر از اون چیزی هست که فکر می‌کنید و در ادامه نشون میدم که چطور توی کمتر از ۱۰۰ خط کد این کامپایلر رو میشه ساخت.

ساخت کامپایلر

برای نشان دادن این حمله من از این ریپوزیتوری استفاده می‌کنم.
برای شروع بیاید نگاهی به فایل Login.cpp بندازیم. توی این فایل برنامه ی ساده ای هست که میگه اگر کاربر رمز ورود رو test123 بزنه می‌تونه وارد بشه

#include <iostream>

using namespace std;

int main() {
    cout << "Enter password:" << endl;
    string enteredPassword;
    cin >> enteredPassword;
    if(enteredPassword == "test123")    
        cout << "Successfully logged in as root" << endl;
    else
        cout << "Wrong password, try again." << endl;
}

حالا بیایید این برنامه رو با استفاده از کامپایلری که داریم کامپایل کنیم. پس داخل ترمینال کد زیر رو می‌زنیم

./Compiler Login.cpp -o Login

حالا اگر کد کامپایل شده‌ی برنامه رو اجرا کنیم و به جای رمز ورورد backdoor رو بزنیم می‌بینیم که برنامه این پسورد رو درست تشخیص می‌ده و وارد می‌شه.
اما همونطور که داخل کد می‌بیندی ما فقط پسورد test123 رو قبول می‌کنیم.
احتمالا حدس میزنید که شاید مشکل از کامپایلری باشه که استفاده کردیم، پس بیاید نگاهی هم به سورس کد کامپایلر بندازیم.

#include <string>
#include <cstdlib> 

using namespace std;

int main(int argc, char *argv[]) {
    string allArgs = "";
    for(int i=1; i<argc; i++)
        allArgs += " " + string(argv[i]);
    string shellCommand = "g++" + allArgs;
    system(shellCommand.c_str());
}

همونطور که داخل کد هم مشخصه این کد از کامپایلر ++g استفاده ‌میکنه و فایل ما رو کامپایل می‌کنه
توی این کد هم مشکلی نیست پس چطوری این درب پشتی به کد ما اضافه می‌شه.

قبل از اینکه در مورد اضافه شدن درب پشتی به کد حرف بزنم خوبه که با اصطلاح self-hosting آشنا بشیم
اگر دفت کرده باشید کد کامپایلر ما خودش به زبان C نوشته شده و فایل اجرایی کامپایلر ما می‌تونه فایل برنامه خودش رو هم کامپایل کنه.
مثلا داخل کد زیر من کد برنامه کامپایلرم رو با فایل اجرایی همون کامپایلر، کامپایل می‌کنم.

./Compiler Compiler.cpp -o newCompiler

این به این معنا هست که برای کامپایل کردن سورس کد ورژن جدید کامپایلرمون می‌تونیم از ورژن های قبلی خودش استفاده کنیم.
زبان های دیگه ای هم مثل C++ ، Java و یا python هم این ویژگی رو دارند
جلوتر در مورد اینکه این ویژگی چه تاثیری داره صحبت می‌کنم اما حالا بیایید در مورد اینکه درب پشتی چطور به کد اضافه می‌شه صحبت کنیم.
زمانی که شما فایل Login.cpp رو کامپایل می‌کنید اتفاق های زیر داخل کامپایلر می‌افته:
1- فایل Login.cpp داخل یک فایل موقت به نام LoginWithBackdoor.cpp کپی می‌شود.
2- فایل LoginWithBackdoor.cpp به اون شکلی که می‌خوایم اصلاح میشه. مثلا یک کلمه عبور جدید به اون اضافه می‌شه.
3- فایل LoginWithBackdoor.cpp به جای فایل Login.cpp کامپایل میشه
4- در نهایت فایل LoginWithBackdoor.cpp پاک میشه
اگر به فایل EvilCompiler.cpp نگاه کنیم می‌تونید این مراحل رو ببینید

اما کسی از این کامپایلر استفاده نمی‌کنه چون با خوندن سورس کد ‌مربوط به EvilCompiler.cpp متوجه این مشکل می‌شه پس باید راهی پیدا کنیم که این مشکل رو هم حل کنه. برای حل این مشکل از self-hosting استفاده ‌می‌کنیم.

مخفی کردن ساخت درب پشتی در کامپایلر

فرض کنید کسی برای اطمینان از درست عمل کردن کامپایلر، سورس کد های کامپایلر را دانلود کند و بخواهد خودش آن را کامپایل و از ان استفاده کند.
در این حالت اگر سورس کد EvilCompiler.cpp رو به کاربر بدیم ، کاربر متوجه حمله ما میشه و اگر سورس کد Compiler.cpp رو به کاربر بدیم دیگه درب پشتی به وجود نمیاد

برای رفع این مشکل باید از خاصیت self-hosting استفاده کنیم.
کاربر زمانی که می‌خواهد سورس کد کامپایلر رو کامپایل کنه نیاز به یک کامپایلر داره. ما ( در نقش طراح حمله ) می‌تونیم سورس کد Compiler.cpp رو به عنوان اولین کامپایلر ساخته شده برای این زبان به کاربر نشون بدیم ولی فایل اجرایی EvilCompiler.cpp به عنوان اولین کامپایلر به کاربر داده بشه. در نتیجه هر بار که کاربر سورس کد Compiler.cpp رو با این فایل اجرایی کامپایل کنه، همون روش اضافه کردن درب پشتی تکرار می‌شه و درب پشتی به کامپایلر اضافه می‌شه.

عکس زیر کل این روش رو به خوبی نشون می‌ده :

استفاده از هش برای کشف درب پشتی

یکپارچه بودن اطلاعات یعنی کسی اطلاعات رو در حین انتقال تغییر نده و چیزی که توی مقصد تحویل می‌گیریم دقیقا همون چیزی باشه که در مبدا ارسال شده.
یکی از روش های مرسومی که این روز ها برای چک کردن بکپارچه بودن اطلاعات استفاده می‌شه هش هست. توی این روش فرسنتده‌ی اطلاعات، یک هش رو هم که هش اون اطلاعات هست در اختیار گیرنده قرار میده و با این روش گیرنده با هش کردن اطلاعات دریافتی و مقایسه کردن این دو تا هش با هم میتونه چک کنه آیا اطلاعات تغییر کردن یا نه.
مثلا وقتی که فایل ایزو اوبونتو رو دانلود می‌کنید هش اون هم همراهش به شما نشون داده می‌شه که می‌تونید با هشی که خودتون از فایل ایزو می‌گیرید مقایسه کنید.
اما باید حواسمون باشه که برنامه ای که هش رو برای ما محاسبه میکنه خودش ممکنه یک درب پشتی داشته باشه. اگر به نظرتون چنین چیزی خیلی عجیب، غیر ممکن و یا سخت هست کافیه به این فکر کنید که از gcc که معروف ترین کامپایلر برای زبان C هست برای کامپایل کردن خود gcc و sha256 استفاده میشه.

و اما درسی که ما می‌گیریم …

همانطور که دیدید حمله compiler backdoor attack حمله بسیار جالب و هوشمندانه ای محسوب میشه.
اما این حمله دو نکته مهم رو به ما یاد میده :
۱- به هیچ کدی که توسط شما نوشته و ایجاد نشده باشه نمیشه اعتماد کرد
2- هر چه به سمت لایه های پایین تر کامپیوتر ها و برنامه ها پیش میریم پیدا کردن باگ ها و درب های پشتی و .. سخت تر میشه

اگر توضیحات اضافه تری در مورد این حمله می‌خواید می‌تونید به منبع سر بزنید

در نام گذاری از واحد استفاده کنید !!

نوشتن کد تمیز یکی از مهمترین مهارت هایی هست که هر برنامه نویسی باید حتما به اون توجه کنه. کد تمیز اصطلاح عامی به حساب میاد و به جنبه های مختلفی توی کد نویسی اشاره داره . از ظاهر کد گرفته تا نام گذاری متغیر ها و نوشتن کامنت داخل کد و غیره.
این مهارت به مرور زمان و قدم به قدم تقویت میشه و به نظر من بهترین روش یادگرفتنش این هست که چند وقت یکبار یکی از تکنیک هاش رو یاد بگیرید و سعی کنید از اون لحظه به بعد اون تکنیک رو داخل کد هاتون استفاده کنید.

مشکل کجاست ؟
به کد های زیر که به زبان های پایتون، جاوا و هسکل نوشته شدند نگاه کنید.
همه این  کد ها یک کار رو انجام می‌دهند یعنی سیستم رو برای یک مدت زمانی به تعلیق در میارن یا به عبارت دیگه از نظر زمانی توی اجرای برنامه تاخیر ایجاد می‌کنند. ظاهر کد که خیلی خوب و سادس ولی مشکل کجاست ؟‌

time.sleep(300)
Thread.sleep(300)
threadDelay 300


سوال اصلی این هست که برنامه ما چقدر قراره به حالت تعلیق در بیاد ؟ با توجه به کد های بالا برنامه پایتون برای 5 دقیقه،  برنامه به زبان جاوا برای 0.3 ثانیه و برنامه هسکل برای 0.3 میلی ثانیه به حالت تعلیق در میاد.
چیزی که مشخصه این هست که برنامه های مختلف از  واحد های زمانی مختلف برای کارهاشون استفاده می‌کنند و وقتی کسی کد رو میخونه اگر از قبل اطلاعات نداشته باشه باید بره و مستندات رو بررسی کنه و این یعنی کد به خودی خود خوانا نیست

راه حل چیه؟

۱ – نام گذاری برای آرگومان ها
بعضی از زبان ها مثل پایتون یا کاتلین این امکان رو دارند که بتونیم آرگومان هایی که به یک تابع می‌فرستیم رو به شکل کلید مقدار ارسال کنیم یعنی نام اون آرگومان رو هم کنارش قرار بدیم در نتیجه وقتی می‌خوایم تابع رو فراخوانی کنیم می‌تونیم اسم آرگومان رو کنارش بذاریم که اسم آرگومان، واحد رو هم داخل خودش داره.
یعنی به جای کد زیر

def frobnicate(timeout: int) -> None:
    ...

frobnicate(300)


این کد رو بنویسیم

def frobnicate(*, timeout_seconds: int) -> None:
    # The * forces the caller to use named arguments
    # for all arguments after the *.
    ...

frobnicate(timeout_seconds=300)


برای زبان هایی مثل جاوا هم که چنین امکانی رو ندارند میشه به شکل زیر یک متغیر تعریف کرد و واحد رو داخل اون مشخص کرد

sleep_seconds = 300
time.sleep(sleep_seconds)

۲ – استفاده از نوع داده خاص
راه حل اول همیشه جوابگو نیست چون زبان هایی مثل جاوا این امکان رو ندارن که بشه آرگومان ها رو همراه با اسم ارسال کرد. اما می‌تونیم نوع داده های خودمون رو داشته باشیم. مثلا به حای اینکه ورودی تابع من یک عدد int باشه که نشون دهنده زمان هست، ورودی از نوع کلاسی با عنوان TimeUnit باشه که واحد های زمانی رو داخل خوش مشخص میکنه.
یعنی به جای کد زیر

def frobnicate(timeout: int) -> None:
    ...

frobnicate(300)


از این کد استفاده کنیم

def frobnicate(timeout: timedelta) -> None:
    ...

timeout = timedelta(seconds=300)
frobnicate(timeout)


اما مسئله به اینجا ختم نمیشه و این مشکل نه تنها توی برنامه نویسی بلکه جا های دیگه هم وجود داره مثلا فرض کنید که می‌خواهیم با یک api کار کنیم و اون api جواب زیر رو برای ما ارسال می‌کنه.

{
   "error_code": "E429",
   "error_message": "Rate limit exceeded",
   "retry_after": 100,
}



راه حل هایی که گفته شد برای این موارد هم کاربرد داره مثلا برای این مورد می‌تونیم جواب رو به این شکل برگردونیم.

{
   "error_code": "E429",
   "error_message": "Rate limit exceeded",
   "retry_after_seconds": 100,
}


یا مثلا داخل فایل های کانفیگ برنامه ها ممکنه به چنین مواردی بر بخورید

request_timeout = 10



که بهتر هست به یکی از شکل های زیر تغییر کنند

request_timeout = 10s
request_timeout_seconds = 10


در نهایت این رو یادمون باشه که کدی خوبه که آدم های دیگه هم بتونن اون رو بخونن و هر چی کد های ساده تری رو بنویسیم برنامه نویس بهتری هستیم. کد های پیچیده  نوشتن نه تنها خفن بودن آدم ها رو نشون نمیده بلکه باعث میشه کم کم از بازی حذف بشیم و کسی دوست نداشته باشه باهامون کار کنه یا داخل پروژه هامون مشارکت کنه
منبع