فاز 13 · درس 40

پیوند، بارگذاری و باینری‌های استریپ‌شده: سازوکار PLT، GOT و شناسایی توابع

نوع
ساختنی
زمان
حدود 160 دقیقه
زبان‌ها
Python, C, x86-64, arm64 assembly
پیش‌نیاز
فاز 13 · درس 32 (ساخت ELF از صفر)، درس 33 (دیس‌اسمبل و جریان کنترل)، درس 38 (قواعد فراخوانی)
نمایی از چندین رشته‌ی فلزی نوری در هم تنیده که در سه مدار مجزا به خانه‌ها و سوکت‌های منطقی طلایی متصل شده و تراشه‌ای با پدهای اتصال بدون برچسب را هدایت می‌کنند.
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. رفتار ویرایشگر پیوند با یک بازآرایی
  4. بخش پویا، PLT و GOT
  5. PLT0 و مدخل puts
  6. بخش‌هایی که در فایل وجود ندارند
  7. بخش‌های dynsym. و symtab. و دستور strip
  8. شیء COFF
  9. ایمیج PE: ایمپورت‌ها، اکسپورت‌ها و بازآرایی‌های پایه
  10. دیرکتوری ایمپورت
  11. دیرکتوری اکسپورت
  12. بازآرایی‌های پایه
  13. ایمیج Mach-O: فرمان‌های بارگذاری، استاب‌ها و جدول نماد غیرمستقیم
  14. بررسی دستی یک استاب
  15. فضای نام دوسطحی
  16. فیکس‌آپ‌های متوالی، بایت به بایت
  17. ساختاری که جایگزین شد
  18. ترای اکسپورت‌ها
  19. داده‌هایی که strip باقی می‌گذارد
  20. پیدا کردن توابع بدون نمادها
  21. LC_FUNCTION_STARTS
  22. اسکن پرولوگ
  23. مقاصد فراخوانی
  24. امضایی با ساختار FLIRT
  25. ابزارهای مختلف با همین ایده چه می‌کنند
  26. پیاده‌سازی از صفر
  27. استفاده از ابزارهای واقعی
  28. کارگاه تعاملی
  29. آزمایش A: محاسبه‌ی دستی PLT
  30. آزمایش B: یک فیکس‌آپ زنجیره‌ای
  31. آزمایش C: استریپ و پیوند مجدد
  32. آزمایش D: تداخل امضاها
  33. تمرین عملی
  34. خروجی این درس
  35. اعتبارسنجی
  36. حالت‌های شکست در محیط عملیاتی
  37. دستورالعمل اجرایی
  38. برای مطالعه‌ی بیشتر

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

این درس به کالبدشکافی فرآیند پیوند پویا در سه فرمت پرکاربرد 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 آن‌ها را می‌خواند و پروب کتابخانه‌ها نیز بررسی‌شان می‌کند. این دو فایل اجرا نشده‌اند و متن درس هر جا اهمیت داشته این نکته را یادآوری کرده است.

یک فراخوانی، سه لودرفراخوانی puts توسط main
One call to puts traced from the call site to the loader-written slot, and the function finders of a stripped buildMach-O: stub and chained fixup, stage 4 of 4, slot after the loader: dyld writes the address of _puts from libSystem here while it walks the chain, before main runs.محل فراخوانیمحل فراخوانی در main، مقدار 0x1000005a03d 00 00 94bl 0x100000694stubstub in __TEXT,__stubs, 0x100000694adrp x16, 0x100004000ldr x16, [x16, #0x38] -> slot 0x100004038br x16اسلات در فایل__DATA_CONST,__got slot, 0x100004038کلمه در فایل: 0x8010000000000007chained import 7: _puts (from libSystem)اسلات پس از لودرپس از اجرای dyldslot = address of puts in the shared libraryبیلد strip شدهstatic-arm64-O2.strippedشروع‌هاsub spstp fp⁦bl+⁩ورودیامضا_main 0x100000500یافت شدیافت شدجا ماندیافت شدنام‌گذاری شد_percent_change 0x1000005f4یافت شدجا ماندجا ماندیافت شدنام‌گذاری شد_grade 0x10000060cیافت شدجا ماندجا ماندیافت شدنام‌گذاری شد_total 0x100000648یافت شدجا ماندجا ماندیافت شدنام‌گذاری شد_grade_label 0x100000740یافت شدجا ماندجا ماندیافت شدنام‌گذاری شدشروع‌ها 5/5 sub sp 1/5 stp fp 0/5 ⁦bl+⁩ورودی 5/5 امضا 5/5
کادرهای توپر مراحلی هستند که قبلاً طی شده‌اند، کادر آبی‌رنگ مرحله‌ی فعلی است، و کادرهای خط‌چین مراحلی هستند که پیش رو قرار دارند. در جدول، خانه‌ی آبی یعنی آن روش شروع تابع را پیدا کرده یا نام‌گذاری کرده است، خانه‌ی خاکستری یعنی آن را از دست داده، و خانه‌ی کهربایی به معنای تطبیق امضا با نام اشتباه خواهد بود.

در درس 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 را با این ترتیب مطالعه کنید:

  1. کلاس ElfFile هدرها، بخش‌ها، هر دو جدول نمادها و بازآرایی‌ها را می‌خواند؛ و توابع dynamic_entries()، jump_slots()، plt_entries() و plt0() ایمیج پیوندخورده را پردازش می‌کنند.
  2. تابع plan_dynamic_link() جدول 4.9 و بخش 5.2 را روی رکوردهای بازآرایی فایل‌های شیء اعمال می‌کند.
  3. کلاس CoffObject هدر فایل، جدول بخش‌ها، نمادهای 18 بایتی با رکوردهای کمکی و بازآرایی‌های 10 بایتی را می‌خواند.
  4. کلاس PeImage استاب MS-DOS، امضای PE، هدر اختیاری، دیرکتوری‌های داده، بخش‌ها و دیرکتوری‌های ایمپورت، اکسپورت و بازآرایی پایه را می‌خواند، و متد iat_calls() بایت‌های ff 15 را رمزگشایی می‌کند.
  5. کلاس MachoFile فرمان‌های بارگذاری، سگمنت‌ها، رکوردهای nlist، جدول نماد غیرمستقیم، استاب‌ها، فیکس‌آپ‌های متوالی، ترای اکسپورت‌ها و بازآرایی‌های فایل شیء را تحلیل می‌کند.
  6. کلاس FunctionFinder چهار روش پیدا کردن شروع توابع را اجرا می‌کند و متد score() هر کدام را با داده‌ی واقعی می‌سنجد.
  7. توابع signatures_from_object() و match_signatures() امضاهایی با ساختار FLIRT را می‌سازند و تطبیق می‌دهند.
  8. توابع 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... روش‌های پیدا کردن توابع و امضاها را روی یک جفت بیلد دیگر از پروژه‌ی خودتان ارزیابی می‌کند.

استفاده از ابزارهای واقعی

این خواننده‌ها را با این ترتیب روی برنامه‌ای که خودتان ساخته‌اید اجرا کنید:

  1. هدرها را بخوانید و فرمت و نوع فایل را مشخص کنید: فایل شیء با قابلیت بازآرایی، فایل اجرایی، یا کتابخانه‌ی اشتراکی. خواننده فایل‌های ناشناخته را رد می‌کند و همین نپذیرفتن نوعی داده‌ی تشخیصی است.
  2. ایمپورت‌ها را به همراه ارائه‌دهندگان آن‌ها فهرست کنید: DT_NEEDED و .dynsym، یا دیرکتوری ایمپورت، یا جدول ایمپورت‌های زنجیره‌ای و LC_LOAD_DYLIB. هر فراخوانی به نامی در این فهرست، به بیرون از این ایمیج می‌رود.
  3. یک فراخوانی را به شکل دستی تا خانه‌ی مربوطه‌اش دنبال کنید. بایت‌های فراخوانی، بایت‌های استاب یا PLT، آدرس خانه، کلمه‌ی موجود در فایل و آنچه بارگذار در آن می‌نویسد را یادداشت کنید. اگر کلمه‌ی درون فایل شبیه داده‌ی بی‌معنی به نظر می‌رسد، پیش از هر قضاوتی آن را به عنوان یک فیکس‌آپ رمزگشایی کنید.
  4. جدول نماد را با اکسپورت‌ها و نقاط شروع توابع مقایسه کنید. نام‌هایی که بارگذار نیاز دارد بعد از strip هم باقی می‌مانند. نام‌هایی که فقط به درد دیباگر می‌خورند حذف می‌شوند. مشخص کنید هر نام حذف‌شده به کدام گروه تعلق داشته است.
  5. در یک بیلد استریپ‌شده، تابع‌یاب‌ها را اجرا کنید و تا زمانی که نسخه‌ی جفتِ نماددار را دارید، دقت آن‌ها را ارزیابی کنید. جدول‌هایی را که ویرایشگر پیوند نوشته است (مانند LC_FUNCTION_STARTS، .pdata یا .eh_frame) به الگوهای بایتی ترجیح دهید، و از مقاصد فراخوانی برای پر کردن شکاف‌ها استفاده کنید.
  6. از روی فایل‌های شیء خودتان امضا بسازید و آن‌ها را اعمال کنید. هر مورد تداخل و بایتی را که باعث تفکیک آن شده یادداشت نمایید. نامی که دو امضا مدعی آن شده‌اند تا زمان بررسی بایت انتهایی یا نام‌های ارجاع‌شده یک مجهول باقی می‌ماند.

همیشه متن خروجی ابزارها را همراه فایل‌ها نگه دارید. با تغییر زنجیره ابزار (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 در آن صفر است.

در دیرکتوری 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 تداخل را حل می‌کنند
اجرای ابزار روی بیلد ماشینی دیگر همان اعداد این متن درس همه‌ی خروجی‌ها را از روی فایل‌های آزمایشی ماشین خودتان دوباره ذخیره کنید

دستورالعمل اجرایی

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

برای مطالعه‌ی بیشتر

خودآزمایی

به سؤال‌ها جواب بدهید تا ببینید چقدر از درس در ذهنتان مانده است.

  1. پیش از درسیک فایل آبجکت بازآرایی‌پذیر (relocatable) روی هر فراخوانی رکورد R_X86_64_PLT32 دارد، از جمله فراخوانی توابعی که در آبجکت دیگری از همان عملیات لینک تعریف شده‌اند. این رکورد چه چیزی را درباره‌ی فایل اجرایی نهایی بیان می‌کند؟
  2. حین درسدر tiny-dyn-x86_64 اسلات ‎.got.plt برای puts در فایل حاوی 0x206 است، و puts@plt در 0x200 با ‎jmpq *slot(%rip)‎ شروع می‌شود. در اولین فراخوانی چه رخ می‌دهد؟
  3. حین درسکلمه در آدرس 0x100004038 در ledger_main مقدار 0x8010000000000007 را در فایل نشان می‌دهد. این چیست؟
  4. حین درسدر tiny-x86_64.exe مقدار IAT در آدرس مجازی نسبی (RVA) معادل 0x2040 شامل مقادیر 0x2058 و 0x2060 است که RVAهایی درون ‎.rdata هستند. چرا این‌ها آدرس‌های puts و exit نیستند؟
  5. حین درسپس از اجرای strip بدون هیچ آپشنی، دستور nm هیچ نمادی برای libledger.stripped.dylib چاپ نمی‌کند. یک فایل جدید main.c همچنان با موفقیت به آن لینک می‌شود و برنامه اجرا می‌شود. چرا؟
  6. پس از درسدر static-arm64-O2.stripped پویش به دنبال ‎stp x29, x30, [sp, #-0x10]!‎ تعداد 0 تابع از 5 تابع و پویش برای ‎sub sp, sp, #imm تعداد 1 تابع از 5 را می‌یابد. اما LC_FUNCTION_STARTS هر 5 از 5 را پیدا می‌کند. علت این تفاوت چیست؟
  7. پس از درسامضاهای به سبک FLIRT برای دو تابع grade و grade_label در سطح ‎-O2 دارای الگوی 32 بایتی یکسان و CRC16 یکسان روی چهار بایت هستند. موتور تطبیق چگونه این دو تابع را به درستی نام‌گذاری می‌کند؟

سؤالات متداول

چرا دستور strip جدول نمادها را پاک می‌کند اما برنامه همچنان به درستی اجرا می‌شود؟

دستور strip فقط جدول نمادهای معمولی (.symtab) و داده‌های دیباگ را حذف می‌کند که مصرف زمان اجرا ندارند. نمادها و متادیتاهایی که بارگذار پویا برای پیوند نیاز دارد در بخش‌های تخصیص‌یافته مانند .dynsym، دیرکتوری ایمپورت یا Exports Trie ذخیره شده و پس از استریپ دست‌نخورده باقی می‌مانند.

تفاوت فیکس‌آپ متوالی (Chained Fixup) در Mach-O با سازوکار کلاسیک چیست؟

در فیکس‌آپ متوالی، هر کلمه‌ی 64 بیتی در بخش دیتا خود شامل وضعیت بایند یا ریبیس، شماره نماد ایمپورت و آفست تا خانه‌ی فیکس‌آپ بعدی است. تمام اشاره‌گرها پیش از اجرای تابع main توسط dyld پر می‌شوند و جدول‌های بایندینگ تنبل و پرش‌های ترامپولین کنار گذاشته می‌شوند.

در یک باینری arm64 استریپ‌شده چگونه می‌توان شروع توابع را بدون جدول نماد پیدا کرد؟

بهترین روش خواندن ساختار تفاضلی LC_FUNCTION_STARTS است که ویرایشگر پیوند برای خطایابی ایجاد می‌کند و استریپ نمی‌شود. در نبود آن، استخراج مقاصد دستورات فراخوانی (دستور bl) و اسکن پرولوگ‌های فریم پشته به همراه ساخت امضای بایتی شبیه FLIRT به تفکیک توابع کمک می‌کنند.

ما رو توی گوگل بیشتر ببین

با افزودن نکست ادمین به «منابع ترجیحی» گوگل، درس‌ها و مطالب ما را در بخش اخبار برتر جستجو بیشتر می‌بینید.