هنگام تحلیل یک فایل اجرایی با ابزارهای دیساسمبلر، بارها پیش میآید که فراخوانی یک تابع خارجی به آدرسی عجیب با بایتهایی نامفهوم ختم میشود، یا پس از استریپ کردن باینری، هیچ نامی از توابع به جز نام فایل باقی نمیماند. تحلیلگری که نداند بارگذار سیستمعامل پیش از اجرای اولین دستور چه تغییراتی روی فایل اعمال میکند، این دادهها را به اشتباه اشارهگر مخدوش یا کدهای نامعتبر فرض میکند.
این درس به کالبدشکافی فرآیند پیوند پویا در سه فرمت پرکاربرد ELF، PE32+ و Mach-O میپردازد. در این آموزش با دنبال کردن ساختار جدولهای PLT، GOT، ساختار دیرکتوری ایمپورت و زنجیرهی فیکسآپها (Chained Fixups)، یاد میگیرید که کامپایلر و ویرایشگر پیوند چگونه فراخوانیهای برونبرنامهای را آماده میکنند و بارگذار چطور آنها را پیش از اجرا به توابع مقصد گره میزند.
در بخش پایانی، مسئلهی از دست رفتن نمادها بررسی میشود: چه اطلاعاتی بعد از اجرای فرمان strip در فایل زنده میماند و چگونه میتوان با استفاده از جدولهای داخلی مانند LC_FUNCTION_STARTS، بررسی پرولوگها، تحلیل مقاصد فراخوانی و ساخت امضاهایی مشابه سیستم FLIRT، توابع یک باینری استریپشده را بازشناسی و نامگذاری کرد.
یک برنامهی پیوندخورده توابعی را فراخوانی میکند که درون خودش وجود ندارند. در این درس، جدولهایی را که ویرایشگر پیوند (Link editor) برای این فراخوانیها در سه فرمت مختلف نوشته است میخوانیم، یک فراخوانی را تا خانهای که بارگذار (Loader) پر میکند دنبال میکنیم، و توابع یک بیلد را پس از پاک شدن نامهایشان پیدا میکنیم.
اهداف یادگیری
- با تکیه بر یک رکورد بازآرایی (Relocation record) و جدولهای نماد تشخیص دهید ویرایشگر پیوند از آن چه میسازد: یک فاصلهی ثابت، یک مدخل PLT به همراه یک خانه در GOT، یا یک بازآرایی پویا.
- بخش پویا (Dynamic section)، جدول پیوند رویهها (PLT) و جدول آفست سراسری (GOT) یک فایل اجرایی ELF مستقل از موقعیت را بخوانید و با فرمولهای psABI یک فراخوانی را در طول آنها دنبال کنید.
- یک فایل شیء COFF (با هدر فایل 20 بایتی، مدخلهای بخش 40 بایتی، نمادهای 18 بایتی و بازآراییهای 10 بایتی) و دیرکتوریهای ایمپورت، اکسپورت و بازآرایی پایه (Base relocation) یک ایمیج PE32+ را بخوانید.
- یک زنجیرهی فیکسآپ متوالی (Chained fixup chain) در Mach-O را کلمه به کلمه رمزگشایی (Decode) کنید و تکتک کلمات را با
dyld_infoتطبیق دهید. - مشخص کنید دستور
stripدر هر فرمت چه چیزهایی را حذف میکند و چه دادههایی باقی میمانند، و برنامهی استریپشده را اجرا کنید تا نشان دهید هنوز کار میکند. - نقاط شروع توابع را در یک بیلد استریپشدهی arm64 به کمک
LC_FUNCTION_STARTS، اسکن پرولوگ و مقاصد فراخوانی بازیابی کنید، هر روش را در برابر جدول نماد نسخهی جفت استریپنشده امتیازدهی کنید، و نام توابع را با امضایی شبیه FLIRT به دست آورید.
صورت مسئله
شما main.c را در برابر libledger.dylib پیوند دادهاید و از یک ایجنت پرسیدهاید که تابع main در آدرس 0x1000005a0 چه تابعی را فراخوانی میکند. ایجنت دستورالعمل را دیساسمبل کرده و به bl 0x100000694 رسیده است. آن آدرس دوازده بایت دارد: adrp x16، ldr x16, [x16, #0x38] و br x16. ایجنت دستور بارگذاری را تا 0x100004038 دنبال کرده، هشت بایت 00 00 00 00 00 00 10 80 را خوانده، و گزارش داده که فراخوانی از طریق یک اشارهگر مخدوش به فضای کرنل انجام میشود.
سپس نسخهی ایستای همان برنامه را استریپ کرده و پرسیدهاید چند تابع دارد. ایجنت nm را اجرا کرده، فقط یک نام یعنی __mh_execute_header را دیده و پاسخ داده: یک تابع.
هر دو پاسخ، فایل را طوری خواندهاند که انگار همان پردازهی در حال اجراست. کلمهی موجود در 0x100004038 یک فیکسآپ زنجیرهای است. بیت 63 میگوید بایند (Bind) شود، دوازده بیت مشخص میکنند فیکسآپ بعدی کجاست، و بیتهای پایین تعیین میکنند کدام ایمپورت باید بایند شود. ابزار dyld پیش از اجرای main، این کلمه را با آدرس _puts جایگزین میکند. فایل استریپشده جدول نمادهایش را از دست داده، اما LC_FUNCTION_STARTS را نگه داشته است؛ فرمانی که تمام پنج نقطهی شروع توابع را تنها در نه بایت فهرست میکند.
در این درس، پنج خواننده با کتابخانهی استاندارد Python میسازیم. یک خوانندهی COFF برای اشیای خودمان و یک خوانندهی PE32+ برای دیرکتوریهای ایمپورت، اکسپورت و بازآرایی پایه. یک خوانندهی Mach-O برای فرمانهای بارگذاری (Load commands)، استابها، جدول نماد غیرمستقیم، زنجیرهی فیکسآپ متوالی و ترای اکسپورتها (Exports trie)، که کلمه به کلمه با dyld_info بررسی میشود. یک خوانندهی ELF برای بخش پویا، PLT و GOT، به همراه یک برنامهریز (Planner) که مشخص میکند پیوند PIE اشیای ما چه خروجیای میسازد. یک تابعیاب برای بیلد استریپشده با چهار روش و امتیازی برای هر کدام. و یک امضاساز با ساختار FLIRT، به همراه بایتهای وایلدکارتشده برای بازآرایی و یک CRC16 از بخش انتهایی.
تنها فرمت Mach-O روی این ماشین پیوند میخورد. فایلهای اجرایی arm64 و dylib در اینجا پیوند خورده، استریپ شده و اجرا شدهاند. اشیای ELF و COFF با clang -target کامپایل شدهاند و چون هیچ ویرایشگر پیوند ELF یا PE روی سیستم نصب نیست، دو ایمیج پیوندخورده در این فرمتها بایت به بایت بر اساس مشخصات فنی توسط دو اسکریپت پایتون در پوشهی fixtures نوشته شدهاند. ابزار llvm-objdump آنها را میخواند و پروب کتابخانهها نیز بررسیشان میکند. این دو فایل اجرا نشدهاند و متن درس هر جا اهمیت داشته این نکته را یادآوری کرده است.
رفتار ویرایشگر پیوند با یک بازآرایی
در درس 32 رکوردهای بازآرایی را خواندیم و در درس 33 فرمول S + A - P را روی یک دستور lea پیاده کردیم. هر رکورد شامل یک مکان P، یک نوع، یک نماد S و یک متمم (Addend) به نام A است. جدول 4.9 از x86-64 psABI برای هر نوع یک فرمول ارائه میدهد؛ با این متغیرها: L برای مکان مدخل PLT نماد، G برای آفست نماد در GOT، نماد GOT برای آدرس GOT، و B برای آدرس پایهی بارگذاری ایمیج. اشیای ما در سه حالت کلی جای میگیرند.
ویرایشگر پیوند مقدار نهایی را داخل فیلد مینویسد و رکورد حذف میشود. رکورد R_X86_64_PC32 برای نمادی که در ورودیها تعریف شده از این دسته است. فاصلهی بین دو بایت از یک ایمیج، صرفنظر از آدرس بارگذاری، هرگز تغییر نمیکند.
ویرایشگر پیوند یک مدخل PLT و یک خانهی GOT میسازد. رکورد R_X86_64_PLT32 در برابر یک تابع تعریفنشده از این نوع است. فراخوانی مقدار L + A - P (یعنی فاصلهی تا مدخل PLT) را دریافت میکند. مدخل PLT از طریق خانهی GOT پرش میکند. یک رکورد R_X86_64_JUMP_SLOT در بخش .rela.plt به پیونددهندهی پویا میگوید که آن خانه را پر کند.
ویرایشگر پیوند بازآرایی را برای بارگذار نگه میدارد. R_X86_64_64 یک آدرس مطلق هشت بایتی است و یک فایل اجرایی مستقل از موقعیت تا زمان بارگذاری آدرس خود را نمیداند. این رکورد به R_X86_64_RELATIVE با فرمول B + A در بخش .rela.dyn تبدیل میشود.
فایل main-x86_64-O1.o شامل 18 رکورد است: 14 رکورد در .text، 2 رکورد در .data.rel.ro و 2 رکورد در .rodata. تابع plan_dynamic_link() در code/main.py این سه قاعده را همراه با ledger-x86_64-O1.o روی آنها اعمال میکند:
| رکورد | تعداد | تصمیم | دلیل |
|---|---|---|---|
R_X86_64_PC32 به .rodata، .L.str، fns، جدول در .rodata |
16 | درجا (in place) | نماد در ورودیها تعریف شده است |
R_X86_64_PLT32 به percent_change، grade، total، grade_label |
4 | مستقیم، بدون PLT | در ledger-x86_64-O1.o تعریف شده، پس ویرایشگر پیوند PLT32 را به PC32 تقلیل میدهد |
R_X86_64_PLT32 دو بار به puts و دو بار به printf |
4 | مدخل PLT و خانهی GOT | تعریفنشده، ارائهشده توسط یک کتابخانهی اشتراکی |
R_X86_64_64 به grade در fns[] |
2 | R_X86_64_RELATIVE |
فیلدی مطلق در یک PIE |
دو ایمپورت یک بخش .plt به اندازهی 48 بایت (شامل PLT0 به اضافهی دو مدخل 16 بایتی)، یک بخش .got.plt به اندازهی 40 بایت (سه خانهی رزروشده به اضافهی دو خانه) و یک بخش .rela.plt به اندازهی 48 بایت میسازند. دو رکورد نسبی هم یک .rela.dyn به اندازهی 48 بایت تولید میکنند. این اعداد حاصل اعمال استاندارد روی رکوردهای ما هستند. فایل دستساز tiny-dyn-x86_64 بخش PLT این طرح را برای همین دو ایمپورت پیاده میکند و اندازههای آن دقیقاً با طرح همخوانی دارد.
کامپایلر از زمان binutils 2.31 (تاریخ 2018-02-13، H.J. Lu) و LLVM 7.0.0 (D43383، تاریخ 2018-02-20) روی همهی فراخوانیها، چه تعریفشده و چه تعریفنشده، PLT32 مینویسد. تصمیم نهایی با ویرایشگر پیوند است. بنابراین وجود PLT32 در یک فایل شیء فقط به معنای «یک فراخوانی» است و نشان نمیدهد که حتماً مدخل PLT ساخته خواهد شد. دو نوع دیگر نیز در سایر بیلدها دیده میشوند: R_X86_64_GOTPCRELX و R_X86_64_REX_GOTPCRELX با فرمول (G + GOT + A - P) درخواست یک خانهی GOT بدون PLT دارند؛ خروجیای که -fno-plt تولید میکند و ویرایشگر پیوند در صورت محلی بودن نماد میتواند آن را به یک lea یا call مستقیم ساده کند (پیوست B در psABI). رکوردهای R_X86_64_32 و R_X86_64_32S نمیتوانند آدرس را در یک PIE نگه دارند و پیوند با شکست مواجه میشود. هیچکدام از این دو نوع در دو فایل شیء ما وجود ندارند.
بخش پویا، PLT و GOT
فصل پنجم gABI میگوید فایل اجرایی حاضر در پیوند پویا باید یک هدر برنامهی PT_INTERP داشته باشد که قبل از هر سگمنت قابل بارگذاری بیاید، و یک هدر PT_DYNAMIC که بخش .dynamic را پوشش دهد. این بخش آرایهای از جفتهای Elf64_Dyn (یک تگ علامتدار و یک مقدار) است که با DT_NULL خاتمه مییابد.
فایل tiny-dyn-x86_64 حجم 5,184 بایت دارد که توسط build_tiny_dyn_elf.py بر اساس gABI و psABI نوشته شده است. بخش پویای آن دوازده جفت در آدرس 0x1000 دارد:
| تگ | شماره | مقدار | مفهوم |
|---|---|---|---|
DT_NEEDED |
1 | libc.so.6 |
آفست در جدول رشتهها، 0xb، کتابخانهای که باید بارگذاری شود |
DT_HASH |
4 | 0x1a0 |
جدول درهمسازی SysV |
DT_STRTAB |
5 | 0x188 |
بخش .dynstr |
DT_SYMTAB |
6 | 0x140 |
بخش .dynsym، هر مدخل 24 بایت |
DT_STRSZ |
10 | 0x15 |
21 بایت برای رشتهها |
DT_SYMENT |
11 | 0x18 |
24 |
DT_PLTGOT |
3 | 0x10c0 |
بخش .got.plt |
DT_PLTRELSZ |
2 | 0x30 |
دو رکورد 24 بایتی |
DT_PLTREL |
20 | 7 | رکوردها از نوع DT_RELA هستند |
DT_JMPREL |
23 | 0x1b8 |
بخش .rela.plt |
DT_FLAGS_1 |
0x6ffffffb |
0x08000000 |
تگ DF_1_PIE گنو |
DT_NULL |
0 | 0 | پایان |
مستندات gABI علت جداسازی DT_JMPREL را اینگونه توضیح میدهد: این تگ «آدرس رکوردهای بازآرایی مربوط به جدول پیوند رویهها را نگه میدارد. تفکیک این رکوردها به پیونددهندهی پویا اجازه میدهد در صورت فعال بودن بایندینگ تنبل (Lazy binding)، در زمان راهاندازی پردازه از آنها چشمپوشی کند.» وجود DF_BIND_NOW در DT_FLAGS، یا DF_1_NOW در DT_FLAGS_1، یا متغیر محیطی LD_BIND_NOW، بایندینگ تنبل را غیرفعال کرده و پیونددهندهی پویا آنها را همان ابتدای کار پردازش میکند.
PLT0 و مدخل puts
بخش 5.2 از psABI دو ساختار موجود در شکل 5.2 را ارائه میدهد. بایتهای فایل ما به این صورت است:
1f0: ff 35 d2 0e 00 00 pushq 0xed2(%rip) # 0x10c8 = GOT+8
1f6: ff 25 d4 0e 00 00 jmpq *0xed4(%rip) # 0x10d0 = GOT+16
1fc: 0f 1f 40 00 nopl (%rax)
200: ff 25 d2 0e 00 00 jmpq *0xed2(%rip) # 0x10d8 = GOT+24, the slot for puts
206: 68 00 00 00 00 pushq $0x0 # index 0 into DT_JMPREL
20b: e9 e0 ff ff ff jmp 0x1f0 # PLT0
محاسبات را یک بار با هم مرور کنیم. جابهجایی وابسته به RIP نسبت به دستور بعدی سنجیده میشود: 0x1f6 + 0xed2 = 0x10c8 که همان .got.plt + 8 است. 0x1fc + 0xed4 = 0x10d0 که .got.plt + 16 است. 0x206 + 0xed2 = 0x10d8 که خانهی چهارم را نشان میدهد. 0x210 - 0x20 = 0x1f0 که به PLT0 برمیگردد. هر مدخل 16 بایت است، بنابراین مدخل مربوط به exit در آدرس 0x210 شروع میشود، مقدار $0x1 را push میکند و آدرس 0x10e0 را میخواند.
جدول GOT سه خانه را رزرو میکند. طبق بخش 5.2 مستندات psABI: خانهی صفر آدرس _DYNAMIC را نگه میدارد و «خانههای یک و دو در جدول آفست سراسری نیز رزرو شدهاند». محتوای .got.plt ما به صورت 0x1000, 0, 0, 0x206, 0x216 است. وقتی glibc فایل را بارگذاری میکند، خانهی یک را با struct link_map خود و خانهی دو را با _dl_runtime_resolve پر میکند. خانهی سه در فایل مقدار 0x206 را دارد که همان دستور pushq درون puts@plt است. به بیان psABI، این خانه «در ابتدا ... حاوی آدرس دستور pushq بعدی است».
بنابراین اولین فراخوانی call 0x200 را اجرا میکند، دستور jmpq *0x10d8 به pushq $0 میپرد، دستور jmp 0x1f0 مقدار GOT+8 را push کرده و از طریق GOT+16 به داخل ریزالور میپرد؛ ریزالور رکورد 0 از DT_JMPREL یعنی R_X86_64_JUMP_SLOT puts را میخواند، puts را در libc.so.6 جستجو میکند، آدرس آن را در 0x10d8 مینویسد و به آنجا میپرد. فراخوانی دوم دستور call و سپس jmpq *0x10d8 را اجرا میکند و مستقیم به مقصد میرسد. درپر (Drepper) مینویسد: «سربار تمام فراخوانیها به جز فراخوانی اول، "تنها" یک پرش غیرمستقیم است». پاورقی psABI دربارهی %r11 دلیل انتخاب رجیستر در درس 38 را روشن میکند: این یک رجیستر موقت (Scratch register) است تا «کد درون PLT نیازی به ذخیره و بازیابی (Spill) هیچ رجیستری نداشته باشد».
محل فراخوانی در .text شامل بایتهای e8 d4 ff ff ff در آدرس 0x227 است، و 0x22c - 0x2c = 0x200. ابزار llvm-objdump برای آن عبارت callq 0x200 <puts@plt> را چاپ کرده و نام مدخل را از روی رکورد .rela.plt استخراج میکند؛ تابعی مثل plt_entries() نیز دقیقاً همین نامگذاری را انجام میدهد.
بخشهایی که در فایل وجود ندارند
این فایل فاقد هدر PT_GNU_RELRO است. یاکوب یلینک (Jakub Jelinek) قابلیت RELRO را در تاریخ 2004-01-06 در فهرست ایمیل binutils پیشنهاد داد: بخشهایی را که فقط در زمان بازآرایی نوشته میشوند به ابتدای سگمنت قابلنوشتن منتقل کنید تا پیونددهندهی پویا بتواند بعد از اتمام کار، با mprotect آنها را فقطخواندنی کند. اسکریپت پیشفرض GNU ld بخشهای .init_array، .fini_array، .data.rel.ro، .dynamic و .got را داخل این ناحیه میگذارد، و از .got.plt فقط سه خانهی رزروشده (24 بایت) را نگه میدارد، چون سایر خانهها به شکل تنبل نوشته میشوند. این حالت RELRO جزئی (Partial RELRO) است. گزینهی -z now پرچم DF_BIND_NOW را فعال کرده و کل .got.plt را به این محدوده منتقل میکند که به آن RELRO کامل (Full RELRO) میگویند، زیرا بعد از راهاندازی هیچ دادهای نوشته نخواهد شد. تابع _dl_protect_relro در glibc دستور mprotect(start, end - start, PROT_READ) را روی بازهی تراز شده با صفحه صدا میزند. گزینهی -fno-plt از سمت کامپایلر یک گام فراتر میرود: فراخوانیها مستقیماً با jmpq *name@GOTPCREL(%rip) انجام میشوند، بایندینگ تنبل غیرممکن میشود و راهنمای GCC اشاره میکند که «تمام نمادهای خارجی در زمان بارگذاری حل میشوند».
فایل فاقد بخشهای نسخهبندی (Version sections) نیز هست. یک پیوند واقعی در برابر libc.so.6 بخش .gnu.version (یک Elfxx_Half به ازای هر مدخل در .dynsym) و بخش .gnu.version_r را اضافه میکند؛ که شامل یک Verneed برای هر کتابخانه و رکوردهای Vernaux برای نام هر نسخه است. عبارت printf@GLIBC_2.2.5 یعنی: مدخل printf در .gnu.version یک ایندکس دارد، رکورد Vernaux که فیلد vna_other آن با این ایندکس برابر است نام vna_name برابر "GLIBC_2.2.5" دارد، که زیرمجموعهی Verneed با فایل vn_file برابر "libc.so.6" قرار گرفته است. ایندکس 0 محلی و 1 سراسری است، و بیت 15 (VERSYM_HIDDEN یا 0x8000) نسخه را از دید ویرایشگر پیوند پنهان میکند.
جدول درهمسازی از نوع DT_HASH مربوط به SysV با یک باکت و زنجیرهای سه مدخلی با حجم 24 بایت است. پیونددهندهی پویا برای جستجوی نمادها در فایل به DT_HASH یا DT_GNU_HASH نیاز دارد. این فایل هیچ نمادی اکسپورت نمیکند و در اینجا هم اجرا نشده، بنابراین پذیرش جدول تکباکتی توسط glibc بررسی تجربی نشده است.
بخشهای dynsym. و symtab. و دستور strip
استاندارد gABI بخشهای .dynsym و .dynstr را بخشهای تخصیصیافته (Allocated sections) و بخشهای .symtab و .strtab را تخصیصنیافته تعریف میکند. پیونددهندهی پویا فقط جفت اول را میخواند و کاری با دومی ندارد. درپر میگوید: «جدول نمادهای معمولی در زمان اجرا استفاده نمیشود، بنابراین نیازی به استریپ کردن باینری نیست و حذف آن تأثیری روی کارایی ندارد».
دستور strip --strip-all در گنو بخشهای .symtab، .strtab، .debug_* و .comment را حذف میکند. تمام بخشهای تخصیصیافته باقی میمانند: .interp، .dynsym، .dynstr، .gnu.hash، *.gnu.version، .rela.dyn، .rela.plt، .plt، .text، .rodata، .eh_frame، .init_array، .dynamic، .got، .got.plt، .data، .bss و یادداشتها (Notes). هدرهای بخشها باقی میمانند مگر اینکه گزینهی --strip-section-headers (از binutils 2.41) اضافه شود یا llvm-strip --strip-sections اجرا گردد. گزینهی --strip-debug نمادها را حفظ میکند. گزینهی --only-keep-debug فایلی از دادههای دیباگ میسازد که --add-gnu-debuglink با ایجاد یک CRC32 در .gnu_debuglink به آن ارجاع میدهد، و فلگ ld --build-id بخش .note.gnu.build-id را ایجاد میکند تا دیباگر بتواند این دو فایل را به هم تطبیق دهد.
شیء COFF
دستور clang -target x86_64-pc-windows-msvc -O1 -fno-asynchronous-unwind-tables -c دو فایل main-x86_64.obj و ledger-x86_64.obj را تولید کرد. مشخصات فرمت PE ساختار این فایل شیء را تعریف میکند: یک هدر فایل 20 بایتی، بدون هدر اختیاری (Optional header)، یک جدول بخشها با مدخلهای 40 بایتی، یک جدول نمادها با رکوردهای 18 بایتی و رکوردهای بازآرایی 10 بایتی برای هر بخش.
کلاس CoffObject در code/main.py هر چهار بخش را میخواند. هدر فایل نشان میدهد: معماری 0x8664، تعداد 10 بخش، 37 رکورد نماد و مقدار SizeOfOptionalHeader برابر صفر است. جدول بخشها نامهای .text، .data، .bss، شش بخش به نام .rdata و یک بخش به نام /16 را برمیگرداند. نامی که با / آغاز میشود، یک آفست دهدهی در جدول رشتههاست؛ جدولی که با طول چهار بایتی خودش شروع میشود؛ نام /16 به .llvm_addrsig ترجمه میشود که همان نام چاپشده توسط llvm-objdump -h است.
این 37 رکورد شامل رکوردهای کمکی (Auxiliary records) هم هستند. نماد یک بخش (کلاس ذخیرهسازی 3) یک رکورد کمکی دارد که طول بخش، تعداد بازآراییها، چکسام و نوع انتخاب COMDAT را در خود نگه میدارد. پنج مورد از بخشهای .rdata نوع انتخاب 2 یعنی «any» را دارند و هر کدام یک رشتهی متنی تحت نامی با تغییر نامگذاری (Name mangling) اماسویسی دارند: ??_C@_04JDDJEAHN@even?$AA@ همان رشتهی "even" است. ویرایشگر پیوند یک کپی از هر کدام نگه میدارد. آخرین رکورد .file به همراه رکورد کمکی "main.c" است. ابزار llvm-objdump -t اینها را به شکل [ 0]، AUX scnlen ...، [ 2] و به همین ترتیب با رد کردن ایندکسهای کمکی چاپ میکند، و تست ما جفتهای ایندکس و نام را با این متن مقایسه مینماید.
بازآراییها: 14 مورد IMAGE_REL_AMD64_REL32 در .text و 4 مورد IMAGE_REL_AMD64_ADDR64 در .rdata. رکورد REL32 به معنای «آدرس نسبی 32 بیتی از بایت بعد از بازآرایی» است. این همان R_X86_64_PC32 است که مقدار 4- درون خود آن لحاظ شده؛ رکورد ELF متمم 4- را درون فیلد addend میآورد زیرا چنین فیلدی دارد، در حالی که رکورد COFF فاقد این فیلد است. فایل ledger-x86_64.obj سه رکورد REL32 در آفستهای 0x36، 0x84 و 0x93 دارد و فایل ledger-x86_64-O1.o در درس 32 نیز سه رکورد R_X86_64_PC32 در همان سه آفست دارد. کد کاملاً یکی است و فرمتها فقط در محل ذخیرهی عدد چهار با هم تفاوت دارند. رکورد ADDR64 یک آدرس مطلق هشت بایتی، یعنی جدول labels مربوط به اشارهگرهای رشته است که در ایمیج نهایی به یک بازآرایی پایه تبدیل میشود، درست مانند تبدیل R_X86_64_64 به R_X86_64_RELATIVE.
ELF Elf64_Rela |
بازآرایی COFF | relocation_info در Mach-O |
|
|---|---|---|---|
| اندازهی رکورد | 24 بایت | 10 بایت | 8 بایت |
| فیلدها | آفست، نوع و نماد در r_info، متمم |
آفست، ایندکس نماد، نوع | آدرس، سپس یک کلمهی 32 بیتی با ساختار symbolnum:24, pcrel:1, length:2, extern:1, type:4 |
| متمم (Addend) | صریح | ندارد، درون خود فیلد قرار دارد | ندارد، یا یک رکورد ARM64_RELOC_ADDEND قبل از رکورد اصلی |
فراخوانی به puts |
R_X86_64_PLT32 puts-0x4 |
IMAGE_REL_AMD64_REL32 puts |
ARM64_RELOC_BRANCH26 _puts |
ایمیج PE: ایمپورتها، اکسپورتها و بازآراییهای پایه
فایل tiny-x86_64.exe با حجم 2,560 بایت توسط build_tiny_pe.py ایجاد شده است. این فایل همان وظیفهی فایل ELF کوچک را دارد: فراخوانی puts و exit از msvcrt.dll. همچنین یک نام را اکسپورت کرده و یک بازآرایی پایه دارد، بنابراین هر سه دیرکتوری مورد نیاز این درس در آن حاضرند. کتابخانهی pefile نسخهی 2024.8.26 و ابزار llvm-objdump -p هر دو آن را میخوانند و خروجی هر دو به عنوان fixture ذخیره شده است.
هدرها را بررسی میکنیم. استاب MS-DOS با MZ شروع شده و در آفست 0x3c فیلد e_lfanew برابر با 0x40 را نگه میدارد. در آفست 0x40 امضای PE\0\0 دیده میشود. سپس هدر فایل COFF قرار دارد: ماشین 0x8664، 4 بخش، مقدار SizeOfOptionalHeader برابر 240 و ویژگیهای 0x22 (ایمیج اجرایی، پشتیبانی از آدرسهای بزرگ). هدر اختیاری با فیلد Magic مقدار 0x20b آغاز میشود که به معنای PE32+ است. مقدار AddressOfEntryPoint برابر 0x1000، ImageBase برابر 0x140000000، SectionAlignment برابر 0x1000، FileAlignment برابر 0x200، SizeOfImage برابر 0x5000 و Subsystem برابر 3 (کنسول) است. فیلد DllCharacteristics با مقدار 0x8160 به موارد HIGH_ENTROPY_VA 0x0020، DYNAMIC_BASE 0x0040، NX_COMPAT 0x0100 و TERMINAL_SERVER_AWARE 0x8000 ترجمه میشود. در ادامه 16 دیرکتوری داده متشکل از RVA و اندازه میآیند. چهار مورد مقداردهی شدهاند: دیرکتوری اکسپورت در 0x2080 با اندازهی 0x4d، ایمپورت در 0x2000 با اندازهی 0x28، بازآرایی پایه در 0x4000 با اندازهی 0xc و IAT در 0x2040 با اندازهی 0x18.
آدرسها در یک ایمیج از نوع RVA هستند؛ آفستهایی نسبت به پایهی بارگذاری. جدول بخشها آنها را به آفستهای فایلی نگاشت میکند: بخش .text با RVA برابر 0x1000 در آفست فایلی 0x200، بخش .rdata با RVA برابر 0x2000 در آفست فایلی 0x400، بخش .data با RVA برابر 0x3000 در آفست فایلی 0x600 و بخش .reloc با RVA برابر 0x4000 در آفست فایلی 0x800. متد PeImage.rva_to_offset() همان جدول است.
دیرکتوری ایمپورت
در RVA برابر 0x2000 آرایهای از توصیفکنندههای ایمپورت (Import descriptors) 20 بایتی قرار دارد که با یک توصیفکنندهی صفر پایان مییابد. فایل ما یک مورد دارد: جدول جستجوی ایمپورت (ILT) در 0x2028، برچسب زمان 0، زنجیرهی ارجاع (Forwarder chain) صفر، نام در 0x2068 و جدول آدرس ایمپورت (IAT) در 0x2040. مقدار RVA مربوط به نام به "msvcrt.dll" اشاره دارد. جدول جستجو شامل مدخلهای 64 بیتی است که به صفر ختم میشوند. اگر بیت 63 یک باشد، ایمپورت با شمارهی ترتیبی (Ordinal) انجام میشود و این شماره در بیتهای 15 تا 0 قرار دارد. اگر بیت 63 صفر باشد، بیتهای 30 تا 0 همان RVA مربوط به مدخل hint/name هستند: یک فیلد 16 بیتی hint به عنوان «ایندکسی در جدول اشارهگر نامهای اکسپورت»، و در ادامه نام اسکی. در فایل ما این مقادیر 0x2058 (با hint برابر صفر و نام "puts") و 0x2060 (با hint برابر صفر و نام "exit") هستند.
مشخصات استاندارد میگوید محتوای IAT «تا قبل از بایند شدن ایمیج، دقیقاً با محتوای جدول جستجوی ایمپورت یکسان است». جدول IAT ما در 0x2040 همان دو RVA را دارد و تست بررسی میکند که 24 بایت موجود در 0x2028 با 24 بایت 0x2040 برابر باشند. «در حین بایندینگ، مدخلهای جدول آدرس ایمپورت با آدرسهای 32 بیتی (در PE32) یا 64 بیتی (در PE32+) نمادهای ایمپورتشده بازنویسی میشوند». بارگذار ویندوز این کار را قبل از اجرای نقطهی ورود انجام میدهد. هیچ بایندینگ تنبل یا استابی وجود ندارد. کد مستقیماً به خانهی حافظه دسترسی پیدا میکند:
100b: ff 15 2f 10 00 00 call qword ptr [rip + 0x102f] # 0x1011 + 0x102f = 0x2040, the IAT slot of puts
1013: ff 15 2f 10 00 00 call qword ptr [rip + 0x102f] # 0x1019 + 0x102f = 0x2048, the IAT slot of exit
دستور FF /2 همان فراخوانی غیرمستقیم نزدیک (Indirect near call) است. این دو دستور بایتهای کاملاً یکسانی دارند، زیرا هر کدام هشت بایت بعد از دیگری قرار گرفته و خانهای را میخواند که آن هم دقیقاً هشت بایت بعد از خانهی قبلی است. متد iat_calls() بخش کد را برای بایتهای ff 15 اسکن میکند، عدد شش و مقدار جابهجایی را میافزاید و نام خانه را از دیرکتوری ایمپورت استخراج میکند.
دیرکتوری اکسپورت
در RVA برابر 0x2080 جدول دیرکتوری اکسپورت 40 بایتی قرار دارد: پرچمها، برچسب زمان، نسخه، RVA مربوط به نام که به "tiny-x86_64.exe" اشاره میکند، پایهی شمارهی ترتیبی 1، تعداد 1 مدخل در جدول آدرسها و 1 اشارهگر نام. در ادامه سه جدول میآیند: جدول آدرس اکسپورت در 0x20a8 حاوی یک RVA برابر 0x1000 است. جدول اشارهگر نام اکسپورت در 0x20ac یک RVA دارد که به رشتهی "tiny_entry" اشاره میکند. جدول ترتیبی اکسپورت در 0x20b0 یک ایندکس 16 بیتی بدون بایاس با مقدار 0 دارد. نام k به مدخل آدرس ordinals[k] نگاشت میشود و شماره ترتیبی اکسپورتشده برابر است با ordinals[k] + ordinal base. مشخصات فنی قاعدهای دارد که ما استفاده نمیکنیم: اگر مدخل آدرس به داخل خود بخش اکسپورت اشاره کند، یک رشتهی ارجاعدهنده مانند MYDLL.expfunc است، و جدول اشارهگر نامها «به ترتیب الفبایی مرتب شده تا امکان جستجوی دودویی فراهم باشد».
بازآراییهای پایه
بخش .reloc از چندین بلوک تشکیل شده است. هر بلوک با RVA صفحه و اندازهی بلوک آغاز میشود، سپس مدخلهای 16 بیتی میآیند که نوع در چهار بیت بالا و آفست در صفحه در دوازده بیت پایین قرار دارد. تنها بلوک ما: صفحهی 0x3000، اندازه 12، مدخل 0xa000، نوع 10 که معادل IMAGE_REL_BASED_DIR64 است، آفست صفر، و یک مدخل پدینگ از نوع صفر یعنی IMAGE_REL_BASED_ABSOLUTE. هشت بایت موجود در RVA برابر 0x3000 مقدار 0x1400020d0 را دارند؛ یعنی پایهی ایمیج به علاوهی RVA رشتهی "linked by hand". اگر بارگذار ایمیج را در جای دیگری قرار دهد، اختلاف آدرس را به این فیلد اضافه میکند. طبق مشخصات استاندارد: «فایلهای ایمیج شامل بازآراییهای COFF نیستند... یک ایمیج، اطلاعات بازآرایی را به صورت بازآراییهای پایه در بخش .reloc نگه میدارد (مگر اینکه ویژگی IMAGE_FILE_RELOCS_STRIPPED را داشته باشد)». گزینهی /DYNAMICBASE در ویرایشگر link.exe امروزی به صورت پیشفرض فعال است، بنابراین فایلهای ایمیج بخش .reloc و ویژگی DYNAMIC_BASE را همراه خود دارند.
ایمیج PE جدول نمادی برای استریپ شدن ندارد. مقدار PointerToSymbolTable در هدر فایل «باید برای یک ایمیج صفر باشد زیرا اطلاعات دیباگ COFF منسوخ شدهاند»، و نامها درون یک فایل PDB جداگانه ذخیره میشوند. آنچه در هر ایمیج باقی میماند دیرکتوری ایمپورت، دیرکتوری اکسپورت، و برای کدهای 64 بیتی بخش .pdata است که مدخلهای RUNTIME_FUNCTION در آن، نقطه شروع و پایان هر تابعی را که پشته تخصیص میدهد یا تابع دیگری را صدا میزند مشخص میکنند. توابع برگ (Leaf functions) در آن وجود ندارند.
ایمیج Mach-O: فرمانهای بارگذاری، استابها و جدول نماد غیرمستقیم
فایل ledger_main فایل اجرایی پیوندخوردهی arm64 است. دستور otool -h تعداد 19 فرمان بارگذاری و پرچمهای 0x00200085 را چاپ میکند. مجموع مقادیر MH_NOUNDEFS 0x1، MH_DYLDLINK 0x4، MH_TWOLEVEL 0x80 و MH_PIE 0x200000 برابر این عدد است. این 19 فرمان عبارتند از چهار LC_SEGMENT_64 (شامل __PAGEZERO، __TEXT، __DATA_CONST، __LINKEDIT)، دو LC_LOAD_DYLIB (ابتدا @rpath/libledger.dylib و سپس /usr/lib/libSystem.B.dylib) و یک فرمان از هر یک از موارد LC_DYLD_CHAINED_FIXUPS، LC_DYLD_EXPORTS_TRIE، LC_SYMTAB، LC_DYSYMTAB، LC_LOAD_DYLINKER، LC_UUID، LC_BUILD_VERSION، LC_SOURCE_VERSION، LC_MAIN، LC_RPATH، LC_FUNCTION_STARTS، LC_DATA_IN_CODE و LC_CODE_SIGNATURE. متد MachoFile.command_names() دقیقاً همین فهرست را با همان ترتیب otool -l برمیگرداند.
سگمنت __TEXT در 0x100000000 آغاز میشود. بخش __text به طول 256 بایت در آدرس 0x100000558 فقط شامل تابع main است، زیرا چهار تابع کتابخانهی ledger درون dylib قرار دارند. بخش __stubs طول 84 بایت در 0x100000658 دارد که هفت استاب دوازده بایتی است. سگمنت __DATA_CONST در 0x100004000 با بخش __got شروع میشود که 64 بایت است؛ هشت خانهی اشارهگر از نوع بخش S_NON_LAZY_SYMBOL_POINTERS و بخش __const با 32 بایت که جدولهای fns[] و names[] در main.c هستند.
بررسی دستی یک استاب
استاب مربوط به _puts در آدرس 0x100000694 قرار دارد:
30 00 00 90 adrp x16, 0x100004000
10 1e 40 f9 ldr x16, [x16, #0x38]
00 02 1f d6 br x16
دستور 0x90000030 همان adrp است. رجیستر در پنج بیت پایین، یعنی 16 قرار دارد. فیلد immlo بیتهای 30 تا 29 با مقدار 0 است. فیلد immhi بیتهای 23 تا 5 با مقدار 1 است. مقدار برابر است با immhi << 2 | immlo = 4، در واحدهای صفحهی 4,096 بایتی؛ بنابراین صفحهی هدف برابر با صفحهی خود دستور (0x100000000) به اضافهی 0x4000 خواهد بود. دستور 0xf9401e10 یک ldr با آفست 12 بیتی بدون علامت با مقیاس 8 است: بیتهای 21 تا 10 عدد 7 هستند که ضرب در 8 برابر 0x38 میشود، پس عمل خواندن از 0x100004038 صورت میگیرد. دستور 0xd61f0200 همان br x16 است. استاب arm64 در سورسکد ld64 دقیقاً همین StubAtom سه دستوری است.
اینکه کدام نام به کدام خانه تعلق دارد از جدول نماد غیرمستقیم مشخص میشود. فرمان LC_DYSYMTAB به 15 ایندکس در جدول نماد اشاره میکند. یک بخش stubs در فیلد reserved1 مشخص میکند توالی ایندکسهایش از کجا آغاز میشود و در reserved2 اندازهی هر استاب را میآورد (12). یک بخش اشارهگر از reserved1 و اندازه هشت بایت به ازای هر خانه استفاده میکند. بخش __stubs از ایندکس 0 و __got از ایندکس 7 شروع میشوند؛ در نتیجه استاب 5 به مدخل 10 جدول نماد یعنی _puts نگاشت میشود و خانهی 7 از __got نیز به همین مدخل میرسد. دستور otool -Iv هر دو توالی را چاپ میکند و تابع indirect_table() تمام ردیفها را بازتولید مینماید.
فضای نام دوسطحی
دستور nm -m عبارت (undefined) external _puts (from libSystem) و (undefined) external _grade (from libledger) را نمایش میدهد. نام کتابخانه از خود رکورد نماد میآید. فایل nlist.h ماکروی GET_LIBRARY_ORDINAL(n_desc) را به عنوان بیتهای 15 تا 8 از n_desc تعریف میکند: مقدار 1 برای اولین LC_LOAD_DYLIB، مقدار 2 برای دومی، 0xfe برای جستجوی پویا و 0xff برای خود فایل اجرایی. راهنمای ld(1): «با فعال بودن two_levelnamespace- (که حالت پیشفرض است)، پیونددهنده نمادها را فقط درون dylibهای مشخصشده در خط فرمان جستجو کرده و ثبت میکند که هر نماد در کدام dylib پیدا شده است». متد nm_lines() دوازده رکورد را بر اساس n_type، n_sect، n_desc و n_value بازسازی میکند و تست، خروجی آن را خط به خط با خروجی ثبتشده از nm -m میسنجد. اگر برنامه را با -Wl,-flat_namespace دوباره پیوند دهید و otool -h را اجرا کنید، میبینید بیت MH_TWOLEVEL حذف شده است.
فیکسآپهای متوالی، بایت به بایت
فرمان LC_DYLD_CHAINED_FIXUPS از دستهی linkedit_data_command است: dataoff 0x8000 و datasize 208. این 208 بایت مطابق ساختار fixup-chains.h هستند.
هدر آن هفت فیلد 32 بیتی دارد: fixups_version 0، starts_offset 32، imports_offset 80، symbols_offset 112، imports_count 8، imports_format 1 (معادل DYLD_CHAINED_IMPORT) و symbols_format 0 (فشردهنشده). دستور llvm-objdump --macho --chained-fixups این هفت مقدار را چاپ میکند و تست ما آنها را تطبیق میدهد.
در آفست 32 ساختار dyld_chained_starts_in_image قرار دارد: seg_count 4 و چهار آفست، یکی به ازای هر سگمنت، و صفر برای سگمنتهای بدون فیکسآپ. تنها __DATA_CONST فیکسآپ دارد. ساختار dyld_chained_starts_in_segment آن شامل این موارد است: size 24، page_size 0x4000، pointer_format 6 (معادل DYLD_CHAINED_PTR_64_OFFSET، «مقصد آفست vm است»)، segment_offset 0x4000، max_valid_pointer 0، page_count 1، و یک page_start با مقدار صفر. مقدار DYLD_CHAINED_PTR_START_NONE یعنی 0xffff به معنای صفحهای بدون فیکسآپ خواهد بود.
زنجیره در آفست فایلی 0x4000 + 0 شروع میشود. هر کلمهی 64 بیتی دو مفهوم را در خود دارد: چه چیزی باید در آنجا نوشته شود، و کلمهی بعدی در کجا قرار دارد.
dyld_chained_ptr_64_bind ordinal:24 addend:8 reserved:19 next:12 bind:1 = 1
dyld_chained_ptr_64_rebase target:36 high8:8 reserved:7 next:12 bind:1 = 0
اولین کلمه را میخوانیم: 0x8010000000000000. بیت 63 یک است، یعنی bind. مقدار next در بیتهای 62 تا 51 برابر 0x002 در واحدهای 4 بایتی است، بنابراین فیکسآپ بعدی 8 بایت جلوتر است. شماره ترتیبی 0 است. کلمهی دوازدهم را در 0x4058 میخوانیم: 0x00000000000006db. بیت 63 صفر است، پس یک تغییر مبنا (Rebase) داریم. مقدار next برابر صفر است، یعنی پایان زنجیره. مقصد 0x6db یک آفست vm است که با احتساب پایهی ایمیج 0x100000000 به آدرس 0x1000006db یعنی رشتهی "odd" در __cstring تبدیل میشود. کلمهی قبلی یعنی 0x00100000000006d6، عنصر names[0] را به "even" تغییر مبنا میدهد.
پیمایش زنجیره دوازده کلمه را میبیند: هشت بایند در __got با شمارههای ترتیبی 0 تا 7، دو بایند در __const با شماره ترتیبی 0 (هر دو عنصر fns[] به _grade اشاره میکنند) و دو تغییر مبنا برای names[]. متد chained_fixups() آنها را با آفستهای فایلی و کلمات خام برمیگرداند، و تست این دوازده جفت (offset, raw) را با خروجی dyld_info -fixup_chain_details مقایسه میکند؛ دستوری که برای اولین مورد عبارت raw: 0x8010000000000000 bind: (next: 002, bindOrdinal: 0x000000) را چاپ میکند.
جدول ایمپورتها در آفست 80 دارای هشت مدخل 32 بیتی است: lib_ordinal:8، weak_import:1 و name_offset:23، که نامها در آفست 112 ذخیره شدهاند. شمارههای 0 تا 3 نمادهای _grade، _grade_label، _percent_change و _total از کتابخانهی 1 هستند و شمارههای 4 تا 7 نمادهای ___stack_chk_fail، ___stack_chk_guard، _printf و _puts از کتابخانهی 2. متد fixup_rows() این دو جدول را ترکیب کرده و خطوطی مانند خروجی dyld_info -fixups تولید میکند؛ مثل __DATA_CONST __got 0x100004038 bind libSystem/_puts، و تست ما تمام دوازده سطر را تطبیق میدهد.
بنابراین آن «اشارهگر مخدوش» در 0x100004038 همان ایمپورت 7 است. ابزار dyld این زنجیره را یک بار هنگام بارگذاری پیمایش میکند و آدرس _puts را درون آن خانه مینویسد. ارائهی اپل در WWDC22 این موضوع را اینگونه بیان کرد: «در محل قرارگیری اشارهگر 64 بیتی در DATA، برخی از بیتها حاوی آفستِ محل فیکسآپ بعدی هستند. همچنین بیتی در آن گنجانده شده که نشان میدهد فیکسآپ از نوع bind است یا rebase». این فرمت از Xcode 13 به عنوان گزینهی پیشفرض برای تارگتهای macOS 12 یا iOS 15 انتخاب شد و Xcode 13.3 آن را به macOS 11 و iOS 13.4 نیز تعمیم داد. در این ساختار هیچ بایندینگ تنبلی وجود ندارد. تمام خانهها قبل از اجرای main پر میشوند.
ساختاری که جایگزین شد
فایل ledger_main_nochain همان پیوند است که با فلگ -Wl,-no_fixup_chains پیوند خورده است. این فایل به جای فرمان قبلی، فرمان LC_DYLD_INFO_ONLY را به همراه چهار جدول بایتکد دارد: تغییر مبنا 8 بایت، بایند 72، بایند تنبل 120 و اکسپورت 64 بایت. همچنین سه بخش دارد که در فایل زنجیرهای وجود ندارند: بخش __TEXT,__stub_helper (108 بایت)، یک __got با حجم تنها 16 بایت شامل ___stack_chk_guard و dyld_stub_binder، و بخش __DATA,__la_symbol_ptr (56 بایت، هفت خانه). دستور dyld_info -fixups هفت سطر lazy-bind و هفت تغییر مبنا را نمایش میدهد که هر خانهی تنبل را به ترامپولین (Trampoline) اختصاصیاش در __stub_helper متصل میکنند. ترامپولین یک ایندکس را push کرده و به dyld_stub_binder میپرد تا نام را حل کرده و خانه را پر کند. این دقیقاً معادل PLT و GOT بخش قبل در قالب ساختارهای Mach-O است: __la_symbol_ptr همان .got.plt است، __stub_helper نقش PLT0 و pushq $index را دارد، و dyld_stub_binder معادل _dl_runtime_resolve عمل میکند. فایل code/main.py آفستها و بخشهای این فایل را گزارش میدهد اما اپکدهای بایند را رمزگشایی نمیکند؛ این کار در تمرینهای عملی خواسته شده است.
ترای اکسپورتها
فرمان LC_DYLD_EXPORTS_TRIE به یک درخت پیشوندی (Prefix tree) اشاره دارد. هر گره شامل یک فیلد uleb128 برای اندازهی پایانه است و در صورت غیرصفر بودن، پرچمهای uleb128 و آفست uleb128 میآیند؛ سپس یک بایت برای تعداد فرزندان و برای هر فرزند، یک یال رشتهای و یک آفست uleb128 برای گره مقصد قرار دارد. متد exports_trie() این ساختار را پیمایش میکند. روی libledger.dylib خروجیهای _percent_change 0x410، _grade 0x428، _total 0x464 و _grade_label 0x498 را میدهد که همان چهار آفست چاپشده با dyld_info -exports هستند. روی فایل اجرایی نیز نامهای __mh_execute_header، _fns و _main را بازمیگرداند.
دادههایی که strip باقی میگذارد
صفحهی راهنمای strip(1) در ابزارهای اپل میگوید اجرای آن بدون هیچ سوییچی روی یک فایل اجرایی پیوندخوردهی پویا شبیه به -u -r عمل میکند: نمادهای تعریفنشده و نمادهایی که ارجاع پویای آنها وجود دارد حفظ میشوند. گزینهی -x نمادهای محلی را حذف میکند. گزینهی -N تمام جدول نماد و جدول رشته را پاک میکند. «برای کتابخانههای اشتراکی پویا، بیشترین سطح استریپ معمولاً -x است»، زیرا «در فرآیند پیوند پویا فقط از نمادهای سراسری استفاده میشود».
این نتایج روی دادههای آزمایشی ما دیده شد: دستور strip ledger_main رکوردهای LC_SYMTAB را از 12 به 10 عدد کاهش داد و نمادهای _main، _fns و _names را حذف کرد. هشت نماد ایمپورتِ تعریفنشده باقی ماندند چون dyld برای بایند کردن به نام آنها نیاز دارد، و __mh_execute_header هم چون وضعیت REFERENCED_DYNAMICALLY داشت دستنخورده ماند. رکورد دهم یک نماد stab از نوع N_OPT به نام radr://5614542 است؛ ابزارهای اپل این نماد را مینویسند تا جدول نماد هیچوقت کاملاً خالی نماند و دستور nm هم آن را چاپ نمیکند. برنامهی استریپشده دقیقاً همان سه سطر قبلی را چاپ کرده و با کد 0 خارج میشود. فرمان LC_FUNCTION_STARTS هنوز یک نقطهی شروع یعنی main را در خود دارد.
اجرای strip libledger.dylib بدون سوییچ، از حالت معمول -x در man page هم فراتر رفت: LC_SYMTAB به 0 رکورد رسید و nm عبارت "no symbols" را چاپ کرد. ترای اکسپورتها همچنان هر چهار تابع را با همان آفستها فهرست میکند، زیرا بایندینگ dyld از طریق همین ترای انجام میشود و به LC_SYMTAB نیازی ندارد. فرمان LC_FUNCTION_STARTS نیز همچنان چهار نقطهی شروع را نشان میدهد. آزمایش کارگاهی C یک فایل جدید main.c را در برابر این dylib استریپشده پیوند میدهد تا نشان دهد ویرایشگر پیوند هم دادههایش را از همین ترای میگیرد.
| داده | ELF بعد از --strip-all |
ایمیج PE | Mach-O بعد از strip |
|---|---|---|---|
| جدول نمادهای ایستا | حذف میشود | در ایمیج وجود ندارد | نامهای محلی و تعریفشده حذف میشوند، ایمپورتهای تعریفنشده باقی میمانند |
| نامهای مورد نیاز بارگذار برای بایند | .dynsym و .dynstr باقی میمانند |
دیرکتوری ایمپورت باقی میماند | ایمپورتها در LC_SYMTAB و جدول ایمپورتهای زنجیرهای باقی میمانند |
| نامهای اکسپورتشده | .dynsym باقی میماند |
دیرکتوری اکسپورت باقی میماند | ترای اکسپورتها باقی میماند |
| نقاط شروع توابع | رکوردهای FDE در .eh_frame، مگر بدون جدولهای unwind ساخته شده باشد |
.pdata برای توابع غیر برگ |
LC_FUNCTION_STARTS باقی میماند |
| اطلاعات دیباگ | بخشهای debug_* حذف میشوند |
در فایل PDB، در خود ایمیج نیست | در فایل dSYM، در خود ایمیج نیست |
| هدرهای بخشها یا فرمانهای بارگذاری | باقی میمانند مگر با --strip-section-headers |
باقی میمانند | باقی میمانند |
پیدا کردن توابع بدون نمادها
فایل static-arm64-O2 حاصل پیوند main-arm64-O2.o و ledger-arm64-O2.o در قالب یک فایل اجرایی تک با پنج تابع در __text است. خروجی nm -n روی فایل استریپنشده دادهی واقعی ماست: _main در 0x100000500 (244 بایت)، _percent_change در 0x1000005f4 (24 بایت)، _grade در 0x10000060c (60 بایت)، _total در 0x100000648 (248 بایت) و _grade_label در 0x100000740 (72 بایت). فایل static-arm64-O2.stripped هیچ نماد N_SECT داخل بخش __text ندارد. کلاس FunctionFinder چهار روش را روی آن اجرا میکند و متد score() هر فهرست را با دادهی واقعی میسنجد.
LC_FUNCTION_STARTS
این فرمان به نه بایت در سگمنت __LINKEDIT اشاره میکند: 80 0a f4 01 18 3c f8 01 00. ساختار آن شامل مقادیر تفاضلی (Delta) در قالب uleb128 است که اولین مقدار نسبت به آدرس __TEXT محاسبه میشود. مقدار 80 0a معادل 0x500 است، بنابراین اولین شروع در 0x100000500 است. مقدار f4 01 معادل 0xf4 است، پس آدرس بعدی 0x1000005f4 خواهد بود. مقدار 18 آدرس 0x10000060c را میدهد، 3c به 0x100000648 میرسد، f8 01 آدرس 0x100000740 را به دست میدهد و 00 پایان فهرست را مشخص میکند. پنج تابع از پنج تابع، بدون هیچ نتیجهی مثبت کاذب (False positive)، هم در -O0 و هم در -O2. ویرایشگر پیوند این جدول را برای ابزار unwind و سیستم گزارش کرش مینویسد و strip کاری به آن ندارد. قیدرا (Ghidra) این اطلاعات را در آنالایزر "Mach-O Function Starts" میخواند، ابزار dyld_info -function_starts آن را چاپ میکند و LIEF آن را با فیلد function_starts در دسترس قرار میدهد.
اسکن پرولوگ
الگوهای شروع AARCH64 در نرمافزار Ghidra با بایتهای fd 7b bf a9 شروع میشوند که معادل دستور stp x29, x30, [sp, #-0x10]! است. در فایلهای ما این الگو در هر دو سطح بهینهسازی 0 از 5 را پیدا میکند. کامپایلر clang ابتدا sub sp, sp, #N را درج کرده و سپس stp x29, x30, [sp, #M] را با یک آفست مثبت اعمال میکند؛ بنابراین فرم پیشایندکس (Pre-index) اصلاً تولید نمیشود. یک ماسک بیتی برای sub sp, sp, #imm به صورت (word & 0xff0003ff) == 0xd10003ff در حالت -O0 که همهی توابع فریم دارند 5 از 5 را پیدا میکند، اما در حالت -O2 نتیجه 1 از 5 است؛ زیرا در این حالت توابع percent_change، grade، total و grade_label همگی توابع برگ بدون فریم هستند. برای مثال percent_change با دستور cbz w0, ... شروع میشود. اسکن پرولوگ تنها الگوهایی را پیدا میکند که کامپایلر تصمیم گرفته تولید کند، و درس 38 نشان داد که کامپایلر چقدر مواقع ترجیح میدهد هیچ کدی در پرولوگ نگذارد.
هیچکدام از این دو اسکن در اینجا مثبت کاذب تولید نکردند زیرا برنامهی ما کوچک است. الگوهای یکبایتی، مانند بایت 55 در x86-64 برای push rbp، ممکن است وسط دستورات دیگر هم دیده شوند. به همین دلیل فایلهای الگوی Ghidra برای x86-64 یک الگوی پسین (مانند 55 48 89 e5) را با یک الگوی پیشین (مانند ret یا nop که باید پایان تابع قبلی باشند) جفت میکنند.
مقاصد فراخوانی
هر دستور bl در __text یک نقطه شروع تابع را مشخص میکند، زیرا کامپایلر در کد C هیچگاه دستوری تولید نمیکند که با bl به میانهی یک تابع دیگر بپرد. این دستور با کدگذاری 0x94000000 به همراه یک آفست کلمهای 26 بیتی علامتدار ثبت میشود. دستور bl در 0x1000005a0 به صورت 3d 00 00 94 خوانده میشود: عدد 0x3d برابر 61 است که ضرب در 4 برابر 0xf4 میشود، پس مقصد 0x100000694 خواهد بود. در ledger_main این آدرس مربوط به استاب _puts است. در بیلد ایستا، تابع call_targets() مقاصد درون بخش __text را جمعآوری کرده، آنهایی را که به __stubs ختم میشوند کنار میگذارد، و مدخل LC_MAIN را هم اضافه میکند (چون هیچ دستوری خود main را فراخوانی نمیکند). نتیجه: پنج از پنج در هر دو سطح بهینهسازی. ابزار Nucleus (اثر Andriesse, Slowinska, Bos در 2017) شناسایی توابع خود را برای کدهای کامپایلر روی همین ایده پیاده کرده است، و آندریسه و همکارانش در سال 2016 نشان دادند که تعیین مرز توابع در دیساسمبل کردن دشوارترین بخش است؛ حتی سختتر از مشخص کردن مرز دستورالعملها.
امضایی با ساختار FLIRT
شرکت Hex-Rays ساختار FLIRT را در مستنداتش چنین شرح میدهد: یک الگو شامل «32 بایت اول یک تابع است که تمام بایتهای متغیر در آن علامتگذاری شدهاند»، بایتهای متغیر از فیکسآپها و مراجع خارجی میآیند، الگوها در یک ساختار درختی ذخیره میشوند، و فراتر از الگو «ما مقدار CRC16 بایتها را از موقعیت 33 تا اولین بایت متغیر محاسبه میکنیم» و آن طول را ذخیره میسازیم. دو تابع با الگو و CRC یکسان، به کمک بایت پایانی در محلی که با هم تفاوت دارند و سپس با نامهایی که ارجاع میدهند از هم تفکیک میشوند. تداخلی که با هیچ روشی برطرف نشود «توسط هوش طبیعی حل خواهد شد». توابع کوتاهتر از چهار بایت بدون ارجاع خارجی کنار گذاشته میشوند و «فایل امضا به جز نام توابع، حاوی هیچ بایتی از کتابخانههای اصلی نیست».
متد signatures_from_object() این ساختار را بر اساس ledger-arm64-O2.o و main-arm64-O2.o میسازد؛ درست همانطور که ابزار FLAIR آن را از فایلهای شیء یک کتابخانه استخراج میکند. هر رکورد relocation_info یک کلمهی چهار بایتی را پوشش میدهد و آن بایتها متغیر در نظر گرفته میشوند. ساختار یک سطر .pat که در پارسر rizin مستند شده شامل الگو، طول بخش محاسبهی CRC، مقدار CRC16، طول کل تابع، عبارت 0000: و نام تابع، و در نهایت ^offset name به ازای هر نام ارجاعشده است:
A00000342800004B890C8052087D091B000DC01AC0035FD6 00 0000 0018 :0000 _percent_change
E8CC8C52C8CCAC72087C289B09FD7FD308FD62930801090B081900511F110071 04 9AAF 003C :0000 _grade ^0000 l_switch.table.grade ^0001 l_switch.table.grade
E8CC8C52C8CCAC72087C289B09FD7FD308FD62930801090B081900511F110071 04 9AAF 0048 :0000 _grade_label ^0000 l_switch.table.grade_label ^0001 l_switch.table.grade_label ^0002 _labels ^0003 _labels
تابع _percent_change اندازهی 24 بایت دارد، بنابراین کل تابع الگوی آن است و طول CRC صفر خواهد بود. تابع _grade اندازهی 60 بایت دارد و اولین بازآرایی آن در بایت 36 است، پس بخش CRC شامل چهار بایت میشود. الگوریتم CRC16 همان الگوریتمی است که پارسرهای عمومی فایلهای .pat استفاده میکنند: چندجملهای 0x8408، مقدار اولیهی 0xffff، متممگیری، و سپس تعویض جای دو بایت. مقدار آن برای رشتهی متنی "123456789" برابر 0x6e90 است و تست ما این مقدار را چک میکند.
تابع match_signatures() تکتک امضاها را در هر یک از نقاط شروع توابع در بیلد استریپشده بررسی میکند؛ ابتدا الگو و سپس CRC. در حالت -O0 هر پنج تابع نام خود را میگیرند و هیچ تداخلی رخ نمیدهد. در حالت -O2 دو خط بالا با هم تداخل پیدا میکنند. تابع grade_label تابع grade را درونخطی (Inline) کرده است، در نتیجه هر دو تابع در 36 بایت اول و چهار بایت CRC مشترک هستند. هر دو امضا در آدرسهای 0x10000060c و 0x100000740 تطبیق داده میشوند. بایتهای انتهایی تداخل را حل میکنند: در بایت 44 تابع _grade مقدار 20 و تابع _grade_label مقدار 28 دارد که به دستورات ldr w0, [x9, w8, uxtw #2] در برابر ldr x8, [x9, w8, uxtw #3] مربوط میشود، ضمن اینکه طول آنها نیز 60 در برابر 72 است. گزارش خروجی هر دو تداخل و بایتی را که باعث تفکیک شد ثبت میکند. نتیجه: پنج از پنج در هر دو سطح به درستی نامگذاری شدند.
تست test_object_bytes_equal_linked_bytes_outside_relocated_words صحت فرضپایه را میسنجد: تمام بایتهای غیرمتغیر هر تابع در فایل شیء، با بایتهای موجود در همان آفست از فایل اجرایی پیوندخورده برابر هستند؛ یعنی ویرایشگر پیوند تنها کلماتی را تغییر داده که بازآرایی داشتهاند. تابع _total اصلاً هیچ بازآرایی یا بایت متغیری ندارد.
ابزارهای مختلف با همین ایده چه میکنند
نرمافزار IDA Pro 9.4 امضاهای FLIRT را برای هر پردازنده ارائه داده و ابزار sigmake را برای ساخت آنها در نظر گرفته است؛ نسخهی IDA Free فقط x86 و x86-64 را پوشش میدهد و کاربرد غیرتجاری دارد. سامانهی Lumina سروری است که متادیتای تابع را با هش md5 آن نگهداری میکند و توابع زیر 32 بایت را نادیده میگیرد. نرمافزار Ghidra 12.1.4 (تحت مجوز Apache-2.0) سازوکار Function ID دارد: یک هش کامل روی نماد دستورها (Mnemonics) و حالتهای آدرسدهی که «در برابر تغییرات ناشی از پیوند مقاوم است» و یک هش خاص که عملوندهای ثابت را اضافه میکند و بر اساس هر دستور منطبق امتیاز میگیرد؛ در این روش توابع والد و فرزند نقش حل تداخل را دارند. پایگاههای دادهی آمادهی آن فقط کتابخانههای Visual Studio را پوشش میدهند و برای بقیهی باینریها باید خودتان آن را پر کنید. نرمافزار Binary Ninja 6.0 Krypton (تجاری، با یک نسخهی رایگان) از الگوریتم WARP (تحت مجوز Apache-2.0) بهره میبرد: شناسه تابع یک UUIDv5 روی شناسههای بلوکهای پایه است که هر کدام با یک UUIDv5 روی بایتهای بلوک پس از صفر کردن «همهی دستورالعملهای دارای بازآرایی» و حذف دستورات NOP ساخته میشوند؛ توابع فراخواندهشده، فراخواننده و توابع مجاور هم به عنوان قید در نظر گرفته میشوند. نرمافزار BinDiff 8 (تحت مجوز Apache-2.0) توابع دو باینری را با ترتیبی مشخص از مراحل با هم تطبیق میدهد؛ از تطبیق هش گرفته تا اندیسهای MD گراف جریان کنترل و تطبیق توالی فراخوانی، و میزان شباهت را از یالها، بلوکها، دستورالعملها و اندیس MD گراف به دست میآورد. ابزار Diaphora 3.4.3 (تحت مجوز AGPL-3.0، مخصوص IDA) ضرب اعداد اول کوچک را هم اضافه کرده است؛ جایی که هر نماد دستور به یک عدد اول نگاشت میشود و مقدار نهایی تابع حاصلضرب آنهاست که اثر انگشتی بدون وابستگی به ترتیب برای یک چندمجموعه میسازد. ابزار radare2 6.2.4 (تحت مجوز LGPL-3.0) بخش zignatures (دستورات zg، z/ و zb) دارد و فایلهای .sig مربوط به FLIRT را با zfs میخواند. ابزار rizin 0.9.1 (تحت مجوز LGPL-3.0) از روی یک باینری باز با دستور Fc فایلهای .pat یا .sig میسازد و با Fs آنها را اعمال میکند.
نرمافزارهای IDA و Binary Ninja متنبسته هستند و توضیحات آنها از روی مستنداتشان آورده شده است. ابزارهای radare2 و rizin روی این ماشین نصب نبودهاند و دستورات آنها از کتابها و سورسکدهایشان نقل شده است. ابزارهایی که روی این فایلها اجرا شدند عبارتند از clang، ld، llvm-objdump، otool، nm، strip و dyld_info از Command Line Tools، و کتابخانههای LIEF 1.0.0، pyelftools 0.33 و pefile 2024.8.26 به کمک اسکریپت بررسی. کتابخانهی LIEF تمام فرمتهای حاضر را خواند؛ از جمله فایلهای شیء COFF را که از نسخهی 0.17.0 پشتیبانی میکند، و ده بایند و دو تغییر مبنا را روی ledger_main گزارش کرد که دقیقاً همان دوازده فیکسآپ است. ابزار pyelftools فقط فایلهای ELF را خواند. کتابخانهی pefile فایل main-x86_64.obj را با خطای PEFormatError: 'DOS Header magic not found.' رد کرد اما ایمیج کوچک PE را به درستی خواند.
پیادهسازی از صفر
اجرای دمو:
cd phases/13-tools-and-protocols/40-linking-loading-and-stripped-binaries
python3 code/main.py
خروجی یک گزارش JSON با پنج بخش است. بخش elf شامل تعداد بازآراییهای دو فایل شیء، طرح پیوند، و بخش پویا، PLT و GOT در فایل tiny-dyn-x86_64 است. بخش coff بخشها، بازآراییها، توابع و نامهای تعریفنشدهی دو فایل شیء را در بر دارد. بخش pe هدرها، دیرکتوریها، ایمپورتها، اکسپورتها، بازآراییهای پایه و فراخوانیهای IAT در tiny-x86_64.exe را نشان میدهد. بخش macho فایل ledger_main را به همراه استابها، فیکسآپهای متوالی، سطرهای فیکسآپ، اکسپورتها، ساختار قدیمی ledger_main_nochain، فایل dylib و دو گزارش استریپ در بر دارد. بخش identification نیز دادهی واقعی، چهار روش ارزیابی به همراه امتیازات، و امضاها همراه با تداخلهایشان را در هر دو حالت -O0 و -O2 نمایش میدهد.
ساختار مورد انتظار:
{
"elf": {"linkPlan": {"pltSymbols": ["puts", "printf"], "sizes": {".plt": 48, ".got.plt": 40, ".rela.plt": 48, ".rela.dyn": 48}, "...": "..."}, "tiny-dyn-x86_64": {"...": "..."}},
"coff": {"main-x86_64.obj": {"...": "..."}},
"pe": {"imports": [{"dll": "msvcrt.dll", "iat": "0x2040", "...": "..."}], "...": "..."},
"macho": {"ledger_main": {"flags": "0x200085", "fixupRows": ["__DATA_CONST __got 0x100004000 bind libledger/_grade", "..."], "...": "..."}, "strip": {"...": "..."}},
"identification": {"O2": {"methods": {"functionStarts": {"recall": "5/5"}, "prologueSub": {"recall": "1/5"}, "...": "..."}, "correctNames": 5}}
}
فایل code/main.py را با این ترتیب مطالعه کنید:
- کلاس
ElfFileهدرها، بخشها، هر دو جدول نمادها و بازآراییها را میخواند؛ و توابعdynamic_entries()،jump_slots()،plt_entries()وplt0()ایمیج پیوندخورده را پردازش میکنند. - تابع
plan_dynamic_link()جدول 4.9 و بخش 5.2 را روی رکوردهای بازآرایی فایلهای شیء اعمال میکند. - کلاس
CoffObjectهدر فایل، جدول بخشها، نمادهای 18 بایتی با رکوردهای کمکی و بازآراییهای 10 بایتی را میخواند. - کلاس
PeImageاستاب MS-DOS، امضای PE، هدر اختیاری، دیرکتوریهای داده، بخشها و دیرکتوریهای ایمپورت، اکسپورت و بازآرایی پایه را میخواند، و متدiat_calls()بایتهایff 15را رمزگشایی میکند. - کلاس
MachoFileفرمانهای بارگذاری، سگمنتها، رکوردهایnlist، جدول نماد غیرمستقیم، استابها، فیکسآپهای متوالی، ترای اکسپورتها و بازآراییهای فایل شیء را تحلیل میکند. - کلاس
FunctionFinderچهار روش پیدا کردن شروع توابع را اجرا میکند و متدscore()هر کدام را با دادهی واقعی میسنجد. - توابع
signatures_from_object()وmatch_signatures()امضاهایی با ساختار FLIRT را میسازند و تطبیق میدهند. - توابع
parse_*خروجیهای متنی ذخیرهشده از ابزارها را میخوانند تا تستها بتوانند مقایسه را انجام دهند.
فرمانهای بیشتر: فرمانهای python3 code/main.py elf FILE، coff FILE، pe FILE و macho FILE گزارش مربوطه را چاپ میکنند. فرمان python3 code/main.py plan OBJECT... طرح پیوند را برای هر مجموعهای از فایلهای شیء ELF نشان میدهد. فرمان python3 code/main.py functions STRIPPED UNSTRIPPED OBJECT... روشهای پیدا کردن توابع و امضاها را روی یک جفت بیلد دیگر از پروژهی خودتان ارزیابی میکند.
استفاده از ابزارهای واقعی
این خوانندهها را با این ترتیب روی برنامهای که خودتان ساختهاید اجرا کنید:
- هدرها را بخوانید و فرمت و نوع فایل را مشخص کنید: فایل شیء با قابلیت بازآرایی، فایل اجرایی، یا کتابخانهی اشتراکی. خواننده فایلهای ناشناخته را رد میکند و همین نپذیرفتن نوعی دادهی تشخیصی است.
- ایمپورتها را به همراه ارائهدهندگان آنها فهرست کنید:
DT_NEEDEDو.dynsym، یا دیرکتوری ایمپورت، یا جدول ایمپورتهای زنجیرهای وLC_LOAD_DYLIB. هر فراخوانی به نامی در این فهرست، به بیرون از این ایمیج میرود. - یک فراخوانی را به شکل دستی تا خانهی مربوطهاش دنبال کنید. بایتهای فراخوانی، بایتهای استاب یا PLT، آدرس خانه، کلمهی موجود در فایل و آنچه بارگذار در آن مینویسد را یادداشت کنید. اگر کلمهی درون فایل شبیه دادهی بیمعنی به نظر میرسد، پیش از هر قضاوتی آن را به عنوان یک فیکسآپ رمزگشایی کنید.
- جدول نماد را با اکسپورتها و نقاط شروع توابع مقایسه کنید. نامهایی که بارگذار نیاز دارد بعد از
stripهم باقی میمانند. نامهایی که فقط به درد دیباگر میخورند حذف میشوند. مشخص کنید هر نام حذفشده به کدام گروه تعلق داشته است. - در یک بیلد استریپشده، تابعیابها را اجرا کنید و تا زمانی که نسخهی جفتِ نماددار را دارید، دقت آنها را ارزیابی کنید. جدولهایی را که ویرایشگر پیوند نوشته است (مانند
LC_FUNCTION_STARTS،.pdataیا.eh_frame) به الگوهای بایتی ترجیح دهید، و از مقاصد فراخوانی برای پر کردن شکافها استفاده کنید. - از روی فایلهای شیء خودتان امضا بسازید و آنها را اعمال کنید. هر مورد تداخل و بایتی را که باعث تفکیک آن شده یادداشت نمایید. نامی که دو امضا مدعی آن شدهاند تا زمان بررسی بایت انتهایی یا نامهای ارجاعشده یک مجهول باقی میماند.
همیشه متن خروجی ابزارها را همراه فایلها نگه دارید. با تغییر زنجیره ابزار (Toolchain)، این خروجیها را دوباره ذخیره کنید، چرا که تغییر خروجی به معنای تغییر دادههای آزمایشی است.
کارگاه تعاملی
آزمایش A: محاسبهی دستی PLT
از دیرکتوری code مفسر پایتون را باز کنید:
cd phases/13-tools-and-protocols/40-linking-loading-and-stripped-binaries/code
python3 -q
اجرا کنید:
from main import *
elf = load_elf("tiny-dyn-x86_64")
[(e.tag_name, e.text) for e in elf.dynamic_entries()]
elf.plt0()
[(e.symbol, hex(e.address), hex(e.slot), hex(e.slot_initial)) for e in elf.plt_entries()]
[hex(w) for w in elf.got_plt_words()]
پیش از اجرای هر فراخوانی، پاسخ را از روی بایتهای ذکرشده در متن حساب کنید. سپس متغیر IMPORTS را در build_tiny_dyn_elf.py به سه نام تغییر دهید، دوباره فایل را بسازید و قبل از خواندن، اندازهی جدید بخشهای .plt، .got.plt و .rela.plt را پیشبینی کنید.
آزمایش B: یک فیکسآپ زنجیرهای
exe = load_macho("ledger_main")
stub = next(s for s in exe.stubs() if s.symbol == "_puts")
stub.lines()
raw = int.from_bytes(exe.read(stub.slot, 8), "little")
hex(raw), raw >> 63, (raw >> 51) & 0xfff, raw & 0xffffff
[(i.index, i.name, i.library) for i in exe.chained_fixups()["imports"]]
کلمهی بایند را ابتدا روی کاغذ رمزگشایی کنید: بیت بایند، فیلد next در واحدهای چهار بایتی، و شماره ترتیبی. سپس کلمهای را که هشت بایت جلوتر است بخوانید و مطمئن شوید که فیکسآپ بعدی است. در نهایت آخرین کلمهی زنجیره در 0x100004058 را بخوانید و توضیح دهید چرا فیلد next در آن صفر است.
آزمایش C: استریپ و پیوند مجدد
در دیرکتوری fixtures، یک برنامهی جدید را در برابر dylib کاملاً استریپشده پیوند دهید:
cd phases/13-tools-and-protocols/40-linking-loading-and-stripped-binaries/code/fixtures
clang -target arm64-apple-macos -O1 main.c -L. -lledger.stripped -Wl,-rpath,@executable_path -o /tmp/relinked
nm /tmp/relinked | grep ' U '
dyld_info -exports libledger.stripped.dylib
عملیات پیوند با کتابخانهای که LC_SYMTAB آن خالی است موفق میشود، چرا که ویرایشگر پیوند ترای اکسپورتها را میخواند. سپس دستور python3 ../main.py macho libledger.stripped.dylib را اجرا کرده و مقادیر symtab، exports و functionStarts را با هم مقایسه کنید. بعد از آن، بیلد ایستا را با فلگ -N استریپ کنید و ببینید dyld_info -function_starts هنوز چه مواردی را چاپ میکند.
آزمایش D: تداخل امضاها
from main import *
stripped = load_macho("static-arm64-O2.stripped")
sigs = signatures_from_object(load_macho("ledger-arm64-O2.o"))
[s.pat_line() for s in sigs]
starts = FunctionFinder(stripped).function_starts()
match_signatures(sigs, stripped, starts)
دو رکورد تداخل را بررسی کنید. سپس امضاها را از روی ledger-arm64-O0.o بسازید، آنها را در برابر static-arm64-O0.stripped تطبیق دهید، و با تکیه بر سطرهای .pat توضیح دهید چرا در سطح -O0 هیچ تداخلی رخ نمیدهد. سپس مقدار PATTERN_LENGTH را به 16 کاهش دهید و بشمارید چند تابع دچار تداخل میشوند.
تمرین عملی
جدولهای بایندینگ قدیمی را رمزگشایی کنید و تابعیاب PE را اضافه نمایید.
نیازمندیها:
- جریان اپکدهای rebase، bind و lazy bind در فرمان
LC_DYLD_INFO_ONLYاز فایلledger_main_nochainرا به صورت سطرهایی شامل سگمنت، بخش، آدرس، نوع و مقصد رمزگشایی کنید و کاری کنید که خروجی آن با 21 سطر متن ذخیرهشده ازdyld_info -fixupsکاملاً مطابقت داشته باشد. - یک هدر برنامهی
PT_GNU_RELROو یک تگDT_FLAGSبا پرچمDF_BIND_NOWبه اسکریپتbuild_tiny_dyn_elf.pyاضافه کنید، بخش.got.pltرا به داخل محدودهی پوشش داده شده منتقل کنید، و کاری کنید کهllvm-objdump -pاین هدر جدید را نمایش دهد. در README بخش fixtures توضیح دهید این تغییر کدام بخش از RELRO کامل را بازسازی میکند و کدام بخش را تنها glibc میتواند انجام دهد. - یک خوانندهی
.pdataبرای ایمیج PE32+ و یک تابعیاب بنویسید که از آدرسهای شروعRUNTIME_FUNCTIONاستفاده کند. یک فایل شیء با دستورclang -target x86_64-pc-windows-msvc -cبدون گزینهی-fno-asynchronous-unwind-tablesبسازید، بازآراییهای.pdataآن را بخوانید، و مشخص کنید کدام توابع آن توابع برگ هستند که در این جدول غایبند. - برای هر خواننده تستی بنویسید که خروجی آن را با متن ابزارهای رسمی مقایسه کند.
موفقیت یعنی اجرای دستور python3 code/main.py macho ledger_main_nochain دقیقاً همان 21 سطر فیکسآپی را نشان دهد که dyld_info چاپ میکند، فایل کوچک ELF هدر PT_GNU_RELRO را داشته باشد که توسط llvm-objdump دیده شود، و تابعیاب مبتنی بر .pdata توابعی را که میتواند نام ببرد و توابع برگی را که در جدول نیستند فهرست کند.
خروجی این درس
این درس شامل فایل outputs/skill-linking-loading-and-stripped-binaries.md است. از آن استفاده کنید تا برنامهای را که ساختهاید به فهرستی از ایمپورتها همراه با ارائهدهندگان، یک فراخوانی ردگیریشده تا خانهی پرشونده توسط بارگذار، تحلیلی دقیق از موارد حذفشده و باقیمانده پس از strip، و بازیابی امتیازدهیشدهی نقاط شروع و نام توابع در یک بیلد استریپشده تبدیل کنید. این کار به نسخهی جفت استریپنشده به عنوان دادهی واقعی (Ground truth)، خروجی ذخیرهشده از ابزارها برای هر ادعا، و ثبت دقیق تکتک تداخلها نیاز دارد.
اعتبارسنجی
اجرای دمو و مجموعهی آزمونهای قطعی:
cd phases/13-tools-and-protocols/40-linking-loading-and-stripped-binaries
python3 code/main.py
python3 code/tests/test_main.py
مراحل اعتبارسنجی باید موارد زیر را اثبات کنند:
- بخش پویای
tiny-dyn-x86_64تگ به تگ باllvm-objdump -pتطابق دارد، وDT_FLAGS_1پرچمDF_1_PIEرا داراست. - دو رکورد
R_X86_64_JUMP_SLOTبا خروجیllvm-objdump -Rمطابقت دارند و نامهایputsوexitرا برمیگردانند. - بخش PLT0 مقدار GOT+8 را push کرده و از طریق GOT+16 پرش میکند، هر مدخل خانهی خود را میخواند و ایندکسش را push میکند، و هر خانه حاوی آدرس دستور
pushqخودش است. - بخشها و هدرهای برنامهی فایل کوچک ELF با
llvm-objdump -h -pتطابق دارند. - رکوردهای بازآرایی دو فایل شیء ELF با
llvm-objdump -rهماهنگ هستند. - طرح پیوند، دو نماد PLT، چهار فراخوانی مستقیم، دو بازآرایی نسبی و اندازههای ایمیج کوچک را به درستی محاسبه میکند.
- جدول بخشهای COFF، تعداد 37 رکورد نماد همراه با رکوردهای کمکی، و بازآراییها با
llvm-objdump -h -t -rهمخوانی دارند، و/16به.llvm_addrsigترجمه میشود. - سه رکورد
REL32فرمت COFF درledgerدقیقاً در همان آفستهای سه رکوردPC32در ELF قرار دارند. - هدرهای PE، فیلد
DllCharacteristicsو دیرکتوریهای داده باllvm-objdump -pتطبیق دارند. - دیرکتوری ایمپورت، مدخلهای hint/name و IAT با
llvm-objdumpوpefileتطابق دارند، و IAT پیش از بایند شدن با جدول جستجو برابر است. - دیرکتوری اکسپورت و بازآرایی پایهی
DIR64به درستی رمزگشایی میشوند، و اشارهگر بازآراییشده نام رشتهی "linked by hand" را نشان میدهد. - دستور
ff 15در RVA برابر0x100bبه خانهی IAT تابعputsارجاع میدهد. - پرچمهای Mach-O، تعداد 19 نام فرمان بارگذاری و دو کتابخانه با خروجی
otoolبرابر هستند. - هفت استاب موجود به هشت خانهی
__gotرمزگشایی میشوند و جدول نماد غیرمستقیم باotool -Ivتطابق دارد. - هدر فیکسآپهای متوالی با
llvm-objdump --macho --chained-fixupsهماهنگ است. - پیمایش زنجیره، تمام دوازده کلمهی خام
dyld_info -fixup_chain_detailsرا بازتولید میکند. - سطرهای فیکسآپ و جدول ایمپورتها با
dyld_info -fixupsو-importsمطابقت دارند. - ترای اکسپورتها با خروجی
dyld_info -exportsبرای کتابخانهی dylib و نسخهی استریپشدهی آن تطابق دارد. - متد
nm_lines()برای فایل اجرایی و نسخهی استریپشده باnm -mیکسان است. - دستور
stripنامهای_main،_fnsو_namesرا حذف کرده، نه نام و یک نماد stab به صورتradr://5614542را نگه داشته، و برنامهی استریپشده همان خروجی را چاپ میکند. - ساختار قدیمی دارای
LC_DYLD_INFO_ONLY، هفت اشارهگر تنبل، و بازآراییها به داخل__stub_helperاست. - بازآراییهای شیء arm64 با
llvm-objdump -rهماهنگ هستند. - فرمان
LC_FUNCTION_STARTSهر پنج تابع را در هر دو سطح پیدا میکند و باnmوdyld_info -function_startsبرابر است. - اسکن دستور
sub spپنج از پنج را در-O0و یک از پنج را در-O2پیدا میکند، و اسکن فریمstpهیچ موردی نمییابد. - مقاصد
blبه اضافهی مدخلLC_MAINهر پنج تابع را بازمییابند. - امضاها هر پنج تابع را در هر دو سطح نامگذاری میکنند و تداخل
gradeرا که با بایت 44 حل میشود ثبت مینمایند. - مقدار CRC16 رشتهی "123456789" برابر
0x6e90است و هشت بایت زیر جفت دستور بارگذاری از GOT درmainاز نوع متغیر هستند. - تمام بایتهای غیرمتغیر شیء با بایتهای پیوندخورده یکسان هستند.
- دمو و دادههای شکل، آدرسهای یکسانی گزارش میدهند و ماژول سایت همین دادهها را در قالب ES5 جاسازی میکند.
حالتهای شکست در محیط عملیاتی
| نوع شکست | گزارش ابزار ضعیف | آنچه تحلیل باید ثابت کند |
|---|---|---|
یک bl یا call به استاب میپرد |
«یک تابع بینام را در 0x694 فراخوانی میکند» | بایتهای استاب، خانهای که لود میکند، و ایمپورتی که آن خانه بایند میکند |
| خواندن کلمهی GOT از داخل فایل | «اشارهگر مخدوش 0x8010000000000007» | بیت 63 نشانهی bind است، next برابر 2 است، شماره ترتیبی 7 به _puts از کتابخانهی 2 اشاره دارد |
وجود PLT32 در فایل شیء |
«تمام فراخوانیها از PLT عبور میکنند» | نمادهای تعریفشده به یک فاصلهی مستقیم تقلیل مییابند و فقط نمادهای تعریفنشده مدخل میگیرند |
| خواندن RELRO جزئی به جای کامل | «بخش GOT فقطخواندنی است» | خانههای .got.plt بعد از سه خانهی رزروشده، بدون -z now خارج از PT_GNU_RELRO قرار میگیرند |
مقایسهی REL32 در COFF با PC32 در ELF |
«بیلد ویندوز بازآراییهای متفاوتی دارد» | آفستها یکی هستند؛ مقدار 4- در متمم ELF و در فیلد بازآرایی COFF ثبت شده است |
| خواندن IAT قبل از بارگذاری | «ایمپورتها به رشتهها اشاره میکنند» | جدول IAT تا قبل از بایندینگ بارگذار با جدول جستجو برابر است و رشتهها مدخلهای hint/name هستند |
| یک کتابخانهی dylib استریپشده | «فاقد نماد، چیزی اکسپورت نشده» | ترای اکسپورتها و LC_FUNCTION_STARTS تمام توابع را در خود دارند |
اجرای nm روی فایل اجرایی استریپشده |
«فقط یک تابع» | فرمان LC_FUNCTION_STARTS پنج نقطه شروع را تنها در نه بایت فهرست میکند |
اسکن پرولوگ در سطح -O2 |
«فقط یک تابع» | چهار تابع برگ فاقد فریم هستند؛ مقاصد فراخوانی و جدول شروع توابع آنها را مییابند |
| دو تابع در 36 بایت مشترک هستند | «تابع grade_label همان grade است» | بایت 44 در بخش انتهایی و طولهای 60 و 72 تداخل را حل میکنند |
| اجرای ابزار روی بیلد ماشینی دیگر | همان اعداد این متن درس | همهی خروجیها را از روی فایلهای آزمایشی ماشین خودتان دوباره ذخیره کنید |
دستورالعمل اجرایی
هر فراخوانی خارجی را از نقطهی فراخوانی تا خانهای که توسط بارگذار پر میشود تعقیب کنید، و پیش از آنکه محتوای آن خانه را یک آدرس بنامید، آن کلمه را مطابق تعریف ساختار فرمت فایل رمزگشایی کنید. عمل استریپ را تنها پس از ثبت دادههای مورد نیاز بارگذار انجام دهید. نقاط شروع توابع را قبل از اسکن بایتها از جدولهایی که ویرایشگر پیوند نوشته استخراج کنید، دقت هر تابعیاب را در برابر نسخهی جفت دارای نماد بسنجید، و هر تداخل در امضاها را همراه با بایتی که باعث رفع تداخل شده است ثبت کنید.
برای مطالعهی بیشتر
- فصل پنجم System V gABI: Program Loading and Dynamic Linking
- مستند System V Application Binary Interface برای معماری AMD64: بخش 5.2 و جدول 4.9
- مقاله درپر، "How To Write Shared Libraries"، نسخه 4.1.2
- مقاله درپر، "ELF Symbol Versioning"
- مطلب یلینک، "Little hardening DSOs/executables against exploits" در لیست binutils به تاریخ 2004-01-06
- مطلب تیلور، "Linker relro"، سال 2008
- کامیت bd7ab16b در binutils با موضوع PLT32 for calls به تاریخ 2018-02-13 و تغییر LLVM D43383
- مستندات GNU Binutils برای strip و ابزار llvm-objcopy
- مستندات مایکروسافت، PE Format
- مرجع دستور CALL برای عملوند FF /2
- فایلهای هدر اپل، mach-o/loader.h و mach-o/nlist.h
- سورس dyld در اپل، mach-o/fixup-chains.h
- سورس ld64 در اپل، stub_arm64.hpp
- یادداشتهای انتشار Xcode 13 درباره فیکسآپهای زنجیرهای و ویدئوی WWDC22 بخش 110362 با عنوان "Link fast: Improve build and launch times"
- صفحات راهنمای strip(1)، دستور ld(1) و دستور dyld(1)
- مستندات Hex-Rays، فناوری IDA F.L.I.R.T. به شکل عمیق و معرفی Lumina
- راهنمای Ghidra Function ID و الگوهای شروع توابع در AARCH64
- پروژه WARP در Vector 35 و راهنمای WARP در Binary Ninja
- راهنمای BinDiff و سورس BinDiff
- پروژهی Diaphora
- سورس pat.c در rizin برای پارس فایلهای pat.، سورس flirt.c در radare2 و کتاب radare2: Zignatures
- مقاله آندریسه و دیگران، "An In-Depth Analysis of Disassembly on Full-Scale x86/x64 Binaries" (کنفرانس USENIX Security 2016) و مقاله Andriesse, Slowinska, Bos با عنوان "Compiler-Agnostic Function Detection in Binaries" (کنفرانس EuroS&P 2017)
- مقاله بائو و دیگران، ByteWeight (کنفرانس USENIX Security 2014) و مقاله Shin, Song, Moazzezi با عنوان "Recognizing Functions in Binaries with Neural Networks" (کنفرانس USENIX Security 2015)
- لیست تغییرات LIEF، کتابخانه pyelftools و کتابخانه pefile
