فقط یه فرد کنجکاو درمورد مباحث آنالیز باینری و مهندسی معکوس
Binary Analysis - PART One
اکثر نرمافزارهای نوشته شده برای کامپیوتر به زبان های سطح بالایی مانند سی و سیپلاسپلاس نوشته شده اند. با این حال کامپیوتر ها نمیتوانند آن را به طور مستقیم اجرا کنند.پیش از اجرای برنامه باید ابتدا آن را به زبان قابل فهم ماشین که همان باینری است تبدیل کنید.اما برنامه کامپایل شده دقیقا همان برنامه ای است که به زبان سطح بالا نوشته شده؟این دقیقا همان چیزی است که نمیدانیم.
این رشته پست مربوط به تحلیل و بررسی فایل های باینری است.در این پست که بخش اول این رشته پست به حساب میرود، قرار است تا ساختار کلی فایل های باینری برای شما شرح داده شود.
فرایند کامپایل فایل سی
همانطور که گفته شد ، فایلهای باینری از طریق ترجمه سورس کد قابل خواندن برای انسان(زبان هایی نظیر سی) به زبان ماشین توسط پردازنده اجرا میشوند.مراحل دخیل در کامپایل کد سی در شکل زیر نمایش داده شده است.
کامپایل کد سی شامل چهار مرحله است،که یکی از این مراحل کامپایل نام دارد،درست شبیه نام فرایند اصلی تبدیل سورس فایل به فایل باینری.در کامپایلر های مدرن اغلب ،برخی یا تمام این فاز ها (پیشپردازندش،کامپایل،اسمبل و لینکر) با هم ادغام میشوند. اما برای اهداف آموزشی این ها را جداگانه پوشش خواهم داد.
فاز پیشپردازش
فرایند کامپایل با یک یا تعدادی از فایل های مبدا که میخواهید کامپایل شوند شروع میشود.سورس کد های سی شامل ماکروها(define#) و دستورعمل include# هستند. از دستور عمل include# برای وارد کردن هدرفایلها داخل سورس کد استفاده میشود.بنابراین تنها چیزی که باقی میماند ما هستیم و کد سی خالص که آماده کامپایل است :)
من برای درک بهتر مثالی را برای شما کامپایل میکنم.دراین مثال از کامپایلر gcc استفاده میکنم که به طور پیشفرض در اکثر توزیع های لینوکسی وجود دارد.خروجی سایر کامپایلر ها مانند cc و Visual Studio ممکن است مشابه باشد.
#include <stdio.h>
#define FORMAT_STRING "%s"
#define MESSAGE "Hello,World!\n"
int main(){
printf(FORMAT_STRING,MESSAGE);
return 0;
}
کد بالا را به این شکل کامپایل میکنیم:gcc -E -P file.c .یک لحظه صبر کنید!چرا از فلگ های E و P استفاده شده است؟برایتان خواهم گفت.درواقع نحوه کامپایل gcc به شکل دیفالت بدون نمایش جزئیات است.با استفاده از فلگ E به کامپایلر میگوییم تا پس از پیشپردازش متوقف شود.و در فلگ P میگوییم تا اطلاعات دیباگینگ(خطایابی) را حذف کند تا خروجی کمی تمیزتر باشد.در نهایت خروجی به شکل زیر است:
typedef long unsigned int size_t;
typedef __builtin_va_list __gnuc_va_list;
typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;
typedef signed char __int8_t;
typedef unsigned char __uint8_t;
typedef signed short int __int16_t;
typedef unsigned short int __uint16_t;
/*....*/
extern char *ctermid (char *__s) __attribute__ ((__nothrow__ , __leaf__));
extern void flockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
extern int ftrylockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__)) ;
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
int main(){
printf("%s","Hello,World!\n");
return 0;
}
همانطور که مشاهده میکنید ، هدر stdio.h با تمامی متغییرهای گلوبال ، موارد تعریف شده(define#) و … داخل سورس فایل سی کپی شده شده است. در واقع ما با استفاده از include# هدر فایل stdio را وارد برنامه خود میکنیم.
فاز کامپایل
پس از فاز پیشپردازش سورس کد آماده است تا کامپایل شود.در این مرحله کامپایلر کد تولید شده در قسمت قبل را میگیرد و آن را به زبان اسمبلی ترجمه میکند.مسئلهای که اینجا وجود دارد این است که چرا کامپایلر کد را مستقیما به زبان ماشین ترجمه نمیکند؟!
دلیل اینکار پیچیده و طاقتفرسا بودن تبدیل کد های یک زبان سطح بالا به زبان ماشین است.بنابراین منطقی است که آن را به نزدیکترین زبان موجود به زبان ماشین ترجمه کنیم.نکته قابل توجه در زبان اسمبلی این است که سمبل ها و دستورات موجود در آن معادل دقیق کدهای قابل فهم در CPU است.
همانطور که گفته شد gcc به صورت پیشفرض تمامی مراحل را ادغام میکند بنابراین برای تبدیل فایل سی به اسمبلی از فلگ S استفاده میکنیم . همچنین این نکته قابل ذکر است که کامپایلر gcc ، کد های سی را به سینتکس AT&T تبدیل میکند.جهت تغییر آن به سینتکس intel میتوانیم از فلگ masm استفاده کنیم:
gcc -S -masm=intel file.c
پس از آن یک فایل با فرمت s. برای ما ساخته میشود که حاوی کدهای اسمبلی برنامه است.
.file "test.c"
.intel_syntax noprefix
.text
.section .rodata
.LC0:
.string "Hello,World!"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
lea rdi, .LC0[rip]
call puts@PLT
mov eax, 0
pop rbp
.cfi_def_cfa 7, 8 ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 10.2.0"
.section .note.GNU-stack,"",@progbits
اکنون وارد جزئیات کد اسمبلی نمیشویم اما در پست های آینده آن را بررسی خواهیم کرد.
فاز اسمبلی
در فاز اسمبلی ما بلاخره میتوانیم کد را تبدیل به زمان ماشین کنیم.در ادامه کامپایلر آن فایل تولید شده به زبان اسمبلی را به عنوان ورودی گرفته و درخروجی یک آبجکت فایل ایجاد میکند.در واقع آبجکت فایل ها حاوی کد های ماشین جهت اجرا هستند .اما صبر کنید هنوز یک مرحله برای ساخته شدن فایل اجرایی وجود دارد.پیش از بررسی آن بیاید تا فایل آبجکت را بررسی کنیم.برای ایجاد فایل آبجکت از طریق کامپایلر gcc از فلگ c استفاده میکنیم.پس اجرای این دستور یک فایل با فرمت o. برای شما ساخته میشود. با استفاده از ابزار file(این ابزار را در آینده مورد بررسی قرار خواهیم داد) میتوانیم به اطلاعات ساختار این فایل دسترسی داشته باشیم:
خروجی به شکل زیر است :
test.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
اما اینها به چه معناست؟بخش اول خروجی(ELF 64-bit) فرمت اجرایی فایل را نمایش میدهد .بخش X86-64 نمایش میدهد این فایل قابل اجرا بر روی کدام پردازنده با چه معماری است.و بخش relocatable به این معناست که این فایل یک Object File است نه یک Executable File .
فاز لینکر
همانطور که از نام این فاز مشخص است وظیفه آن متصل کردن آبجکت فایل ها نهایتا یک فایل اجرایی تکی را ایجاد میکند.جهت حفظ حوصله شما در خواندن این پست ، من درمورد این بخش چیزی نمینویسم.اما پیشنهاد میدهم تا جدا از این مطلب درمورد آنها تحقیق کنید.در نهایت فایل سی را به شکل دیفالت کامپایل(بدون هیچ فلگی) کامپایل میکنیم تا تمامی مراحل به شکل خودکار انجام شود:
همانطور که مشاهده میکنید، فایل اجرا شده همچنین بخش relocatable به executable تبدیل شده که نشان میدهد فایل قابل اجراست و یک آبجکت فایل نیست.
امیدوارم که از این مطلب لذت برده باشید.در پست های آینده به بررسی بیشتر کدهای باینری و همچنین خروجی ابزار file در فایل اجرایی خواهم پرداخت.
مطلبی دیگر از این انتشارات
7 نکته ای که قطعا درباره گوگل نمی دانستید
مطلبی دیگر از این انتشارات
مشکلات package*.json
مطلبی دیگر از این انتشارات
دستور grep در لینوکس