به نام خدا

در این پست قصد دارم توضیحاتی در مورد فرمت ELF خدمتتون ارائه کنم.

این فرمت برای فایل های اجرایی و همچنین فایل های لینک پذیر مانند لایبراری های so استفاده میشود. این فرمت دارای هدر های متنوع و گوناگونی است. به کمک دستور readelf میتوان مشخصات یک فایل elf را بدست آورد. 

در فایل elf دو نوع وجود دارد Segment و Secction

به داده های موجود روی فایل section میگویند و به ساختاری که در حافظه ی ram ایجاد میشود segment گفته میشود.

سکشن ها شامل موارد زیر است که میتوانند توسط برنامه نویس بیشتر هم شوند.

.text

.data

.bss

.symtab

برای مشاهده ی کل شکسن هدر های برنامه از دستور readelf -S استفاده میشود.

یکی از سکشن های مهم symbol table نام دارد که به صورت .symtab نمایش داده میشود. برای مشاهده ی تمام symbol ها از دستور readelf -s استفاده میشود.

نام تمام متغیر های سراسری و توابع و فایل ها و سکشن ها توسط این دستور نمایش داده میشود (داخل این سکشن وجود دارد)

همچنین نام فانکشن هایی که قرار است بعدا به برنامه لینک شوند با تگ UND به معنی تعریف نشده مشخص شده است.

برای بررسی کد اسمبلی یک فایل اجرایی میتوان از دستور objdump -d استفاده کرد.

اگر در کد اسمبلی به دستور call توجه کنید خواهید دید که آدرس توابعی که باید به آن ها پرش کنیم با صفر مشخص شده است (حتی توابع معمولی داخل فایل به شرطی که با gcc -c کامپایل شده باشند).

سوال اینجاست که این آدرس ها چه زمانی و توسط چه کسی باید اصلاح شود؟پاسخ آن است که لینکر موقع لینک شدن (بعد از کامپایل) آدرس ها را جایگذاری میکند اما لینکر از کجا متوجه میشود که چه بخش هایی از کد باید با چه مقادیری تغییر کند؟

پاسخ اسن است که کامپایلر یک سکشن به نام Relocation Section ایجاد کرده است که در آن تمام آفست هایی از کد برنامه که باید تغییر کنند را مشخص کرده همچنین مقادیری که باید قرار بگیرند را نیز مشخص کرده است.برای مشاهده ی این سکشن از readelf -r استفاده کنید.

 

لینکر هنگامی که فایل اجرایی میسازد آن را با /usr/lib/Scrt1.o پیوند میزند این فایل شامل تابع _stat است که اولین تابعی است که اجرا خواهد شد و تابع ما را صدا میزند.

 

اما فرض کنید که توابعی که ما فراخوانی میکنیم جزو توابعی است که داخل دیگر لایبراری ها مثل libc.so یا هر فایل لایبراری دیگر قرار دارد. در این موارد آدرس تابع اصلی چه زمانی جایگذاری میشود؟

 

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

برای جلوگیری از اینکار به مفاهیم PLT , GOT روی اورده اند در این روش برنامه موقع فراخوانی تابع (مثلا printf) یک رپر (که لینکر یا کامپایلر) میسازد را صدا میزنند با این نام printf@plt که این تابع سه دستور اسمبلی دارد. به صورت زیر 

 

10001 jmp *0x2fca(%rip) 
10002 push %0x00 
10003 jmp 0x400020

 

نکته ای که باید به آن توجه کنید این است که همانطور که اشاره کردیم یک جدول دیگر به نام GOT وجود دارد که صرفا یک سری آدرس را نگهداری میکند. دستور اول در دستورات بالا وظیفه ی خواندن اولین ردیف از جدول GOT را دارد اولین ردیف جدول GOT عملا آدرس printf است لذا با اولین دستور ما مستقیما به کد printf در لابراری libc جامپ میکنیم و پس از اتمام کار تابع printf ریترن میشود و به دستور اصلی برمیگردد (دیگر اینجا برنمیگردد چون ما jump کرده بودیم نه call).

اما پس کاربرد دو دستور بعدی چیست.

نکته اینجاست که برای افزایش سرعت اجرای برنامه. به جای اینکه dynamic linker از ابتدا تمام لینک ها را ایجاد کند و آدرس ها را اصلاح کند و جدول GOT را بسازد آن را به تعویق می‌اندازد. با اولین فراخوانی تابع مثلا printf فایل ld.so اجرا میشود و آدرس درست تابع printf را که در libc قرار دارد درون جدول GOT قرار میدهد. اما چه کسی ld.so را فراخوانی میکند؟

میدانیم موقع شروع برنامه و قبل از اجرای برنامه لینکر اجرا شده و یک سری مقدار دهی ها انجام داده و جدول GOT را ساخته اما مقدار اولیه ی جدول GOT همانطور که گفتیم مقادیر اصلی توابع نبوده پس چه چیز بوده؟

مقادیر اولیه جدول GOT دقیقا آدرس مطلق دستور بعد از دستور jmp است. در تکه کد بالا اگر آدرس دستور اول در آدرس 10001 باشد و دستور بعدی در آدرس 10002 باشد در جدول GOT برای تابع printf (که یک ایندکس از جدول GOT است) مقدار 10002 است که باعث میشود دستور اول به دستور دوم جامپ کند (چرا چون هنوز مقدار درست آدرس تابع printf را نداریم چون مشخص کردن آدرس آن را به تعویق انداختیم).

دستور push مشخص میکند که در جدول GOT ما ایندکس صفر را میخواهیم مشخص کنیم و بعد به یک روتین دیگه میپرد که در آن روتین هم ld.so فراخوانی میشود بعد از اینکه ld.so فراخوانی شد ld.so آدرس printf را پیدا میکند و درون جدول GOT قرار میدهد و آن را نیز اجرا میکند.

دفعه ی بعد که تابع printf فراخوانی شود در سطر اول جدول GOT آدرس صحیح printf قرار دارد که باعث میشود با یک jump مستقیم به printf بپریم.

 

----

 

یک entry به نام PT_INTERP در فایل ELF وجود دارد که مشخص میکند این برنامه باید توسط که مفسری اجرا شود برای برنامه هایی که لینک به خارج دارند این مقدار با PATH مربوط به ld-linux.so مقدار دهی میشود عملا ld اجرا خواهد شد و کار های مربوط به لینک را انجام میدهد (مثل ایجاد جدول GOT) و سپس کنترل را به دست برنامه ی اصلی میسپارد.

 

به کمک دستور readelf -l میتوان مقدار INTERP را مشاهده نمود همچنین آدرس این استرینگ در همان جا قابل مشاهده است که میتوان با دستوری مثل dd آن آدرس خاص را تغییر داد.

echo -en "/lib32/" | dd of=./n.o seek=$((0x318)) bs=1  conv=notrunc

 

همچنین این مقدار توسط دستور فایل قابل مشاهده است.

 

هنگام کامپایل با استفاده از -pie و -no-pie میتوان مشخص نمود که فایل اجرایی از چه نوعی باشد Possion Independed Executable باشد یا خیر.

اگر از نوع PIE باشد برنامه این قابلیت را دارد که در هر مکانی از حافظه اجرا شود زیر اتمام آدرس دهی ها کاملا نسبی است اما در حالت no-pie ممکن است کامپایلر هنگام کامپایل از آدرس دهی مطلق استفاده کرده باشد که با جا به جای کردن برنامه درون رم باعث بهم ریختن برنامه میشود.

همچنین موقغ لینک کردن و ایجاد فایل اجرایی مقدار p_vaddr برای هر سگمنت دارای مقدار غیر صفر میباشد که معمولا 0x4000 است. اگر برنامه no-pie کامپایل شود حتما در همان آدرس مشخص شده درون رم قرار میگرد که توسط p_vaddr مشخص شده.

اما اگر در حالت pie کامپایل شود مقدار p_vaddr برابر صفر خواهد بود که به داینامیک لینکر اعلام میکند میتواند در هر کجای حافظه اجرا شود.

به کمک دستور readelf -l میتوانیم program header ها را ببینیم و در آنجا LOAD مشخص میکند هر سگمنت باید کجا لودو شود و در انتهای خروجی این دستور نیز مشخص شده که داخل هر سگمنت چه سکشن هایی است.

 

سکشن .dynamic شامل اطلاعاتی برای داینامیک لینکر است مثلا اطلاعاتی شامل اینکه برنامه برای اجرا به چه so هایی نیاز دارد (برای مشاهده این سکشن از readelf -d استفاده کنید)

 

تکنیک ASLR یک ویژگی کرنل لینوکس است که باعث میشود سگمنت های مختلف برنامه در جاهای مختلف ram به صورت رندوم قرار بگیرند تا اینگونه جلوی اقدامات مخرب هکر ها گرفته شود.

برای غیر فعال کردن آن میتوان از دستور زیر استفاده کرد.

 

echo 0 >  /proc/sys/kernel/randomize_va_space

 

این ویژگی صرفا روی فایل های PIE تاثیر گذار است.

 

 

 

منابع:

 

https://jasoncc.github.io/gnu_gcc_glibc/gnu-ifunc.html
https://www.networkworld.com/article/966844/what-does-aslr-do-for-linux.html
https://github.com/rizinorg/cutter/tree/dev/src
https://ir0nstone.gitbook.io/notes/types/stack/aslr/plt_and_got