فاز 13 · درس 38

شیوه‌های فراخوانی و فریم‌های پشته: System V، ویندوز x64 و AArch64

نوع
ساختنی
زمان
حدود 140 دقیقه
زبان‌ها
Python, C, x86-64, AArch64 assembly
پیش‌نیاز
فاز 13 · 32 (ساخت ELF از صفر)، فاز 13 · 33 (دیس‌اسمبلی و جریان کنترل)
نمایی از چندین ستون عمودی شیشه‌ای با اندازه‌ها و برچسب‌های متغیر روی پلتفرمی چندلایه که ساختار بخش‌های حافظه و فریم پشته را به تصویر می‌کشد.
فهرست مطالب
  1. اهداف یادگیری
  2. صورت مسئله
  3. تشخیص کتابچه‌ی قواعد از روی یک هدر
  4. استاندارد System V: دو توالی مستقل ثبات و یک ناحیه‌ی سرخ
  5. مایکروسافت x64: موقعیت‌ها، فضای Shadow Store و نبود Red Zone
  6. معماری AAPCS64 و تغییرات اعمال‌شده توسط Apple
  7. بازخوانی و رمزگشایی دستی یک پرولوگ، در سه حالت
  8. نگارش دوگانه‌ی اطلاعات توسط کامپایلر
  9. نام‌گذاری ابزارهای مختلف مهندسی معکوس برای این مفاهیم
  10. پیاده‌سازی از صفر
  11. استفاده از ابزارهای واقعی
  12. آزمایشگاه تعاملی
  13. بخش اول: یک دستور، سه اثر مختلف
  14. بخش دوم: ساختار فریم پشته در تابع keeps
  15. بخش سوم: شناسایی الگوی خواندن قبل از نوشتن در تابع mixed
  16. بخش چهارم: تأیید از طریق داده‌های Unwinder کامپایلر
  17. تمرین عملی
  18. خروجی این درس
  19. راستی‌آزمایی
  20. حالت‌های شکست در محیط واقعی
  21. قاعده‌ی عملیاتی
  22. برای مطالعه‌ی بیشتر

در مهندسی معکوس و تحلیل باینری، دیس‌اسمبلی کد تنها نشان می‌دهد که یک تابع کدام ثبات‌ها را دستکاری می‌کند؛ اما فهمیدن نقش دقیق هر ثبات نیازمند شناخت قرارداد فراخوانی (Calling Convention) متناظر است. بدون در نظر گرفتن این قرارداد که توسط هدر فایل تعیین می‌شود، حتی ابزارهای شناخته‌شده‌ای مثل radare2 یا rizin ممکن است تعداد آرگومان‌ها را اشتباه استخراج کنند یا ثبات‌های به‌کاررفته در آدرس‌دهی را نادیده بگیرند.

در این درس یاد می‌گیرید چگونه بر پایه‌ی هدر فایل‌های ELF، COFF و Mach-O استاندارد حاکم را تشخیص دهید، تفاوت‌های کلیدی میان قراردادهای System V، ویندوز x64 و AAPCS64 را بازشناسی کنید و چیدمان پشته را به شکل بایت‌به‌بایت با داده‌های بازگشایی پشته (Unwind Data) خود کامپایلر بسنجید تا تحلیلی مستقل و مطمئن از خروجی ابزارهای خودکار داشته باشید.

دیس‌اسمبلی فقط نشان می‌دهد که یک تابع کدام ثبات‌ها را می‌خواند. این تنها ABI است که مشخص می‌کند آن ثبات‌ها چه معنایی دارند، و نوع ABI نیز با هدر آبجکت‌فایل تعیین می‌شود؛ بنابراین در این درس، ابتدا هدر را می‌خوانیم و سپس هر ثبات را با قاعده‌ای که آن را در آنجا نشانده تطبیق می‌دهیم.

اهداف یادگیری

  • تشخیص شیوه‌ی فراخوانی (Calling convention) از روی هدر آبجکت‌فایل: ELF x86-64 همان System V psABI است، COFF AMD64 همان Microsoft x64، ساختار ELF AArch64 همان AAPCS64، و Mach-O arm64 گونه‌ی اختصاصی Apple است.
  • رمزگشایی (Decode) بایت‌به‌بایت پرولوگ (Prologue) یک تابع برای هر ABI و قرار دادن تک‌تک ثبات‌های ذخیره‌شده، آدرس بازگشت و اشاره‌گر فریم (Frame pointer) در آفست متناظرشان نسبت به اشاره‌گر پشته‌ی ورودی (Entry stack pointer).
  • استنتاج ثبات‌های پارامتر تابع بر اساس ثبات‌هایی که قبل از نوشتن خوانده می‌شوند، و استنتاج اسلات‌های آرگومان پشته بر اساس خواندن‌های حافظه در بالای اشاره‌گر پشته‌ی ورودی.
  • تمایز قائل شدن بین توالی‌های مستقل ثبات‌ها در System V و اسلات‌های موضعی در Microsoft x64، و تفکیک هر دوی آن‌ها از شمارنده‌های NGRN و NSRN در AAPCS64.
  • اعتبارسنجی هر استنتاج در برابر امضای کد منبع، قواعد مشخص‌شده در استانداردها، و داده‌های بازگشایی پشته (Unwind data) خود کامپایلر در بخش‌های .pdata، .xdata و .eh_frame.
  • مقایسه‌ی نحوه‌ی نام‌گذاری و استخراج این حقایق در ابزارهای Ghidra، IDA، Binary Ninja، radare2، rizin و angr، و ثبت مواردی که خروجی آن‌ها با کد منبع مغایرت دارد.

صورت مسئله

فرض کنید فایل convention.c را برای Linux x86-64 با فلگ -O2 کامپایل کرده‌اید و از ابزار radare2 نسخه‌ی 6.2.4 آرگومان‌های تابع eight_args را می‌خواهید. این ابزار هفت آرگومان گزارش می‌دهد: rdi، rsi، rdx، r8، r9، rsp+0x8 و rsp+0x10؛ در حالی که سورس‌کد C هشت پارامتر دارد. در بایت‌های کد ماشین، %rcx دقیقاً یک بار در دستور leaq (%rdx,%rcx), %rax خوانده می‌شود، اما سازوکار بازیابی متغیر این ابزار، ثباتی را که فقط درون یک آدرس‌دهی ظاهر شده، به حساب نیاورده است.

سپس نسخه‌ی ویندوزی همان فایل را در rizin نسخه‌ی 0.9.1 باز می‌کنید. این ابزار شیوه‌ی فراخوانی را amd64 تشخیص داده و ثبات‌های r8، rcx، rdx و سه اسلات پشته پیدا می‌کند. از آنجا که فایل خروجی COFF است، شیوه‌ی فراخوانی در واقع Microsoft x64 است، چهارمین آرگومان ثباتی r9 است و باید چهار اسلات پشته وجود داشته باشد. دو ابزار مختلف، در تحلیلی از یک سورس‌کد یکسان، ثبات‌های متفاوتی را نادیده گرفته‌اند.

یک لیستینگ (Listing) صرفاً توالی‌ای از دستورالعمل‌هاست. در مقابل، شیوه‌ی فراخوانی قراردادی است که بیرون از این لیستینگ معنا پیدا می‌کند: این‌که چه ثبات‌هایی حامل آرگومان‌ها هستند، تابع فراخوانده‌شده (Callee) موظف به حفظ کدام ثبات‌هاست، آدرس بازگشت و آرگومان‌های پشته در کجا می‌نشینند، و تابع فراخوان (Caller) چه فضایی را باید بالای اشاره‌گر پشته خالی بگذارد. چهار سند مرجع، این قراردادهای مهم را تعریف می‌کنند: بخش‌های 3.2.1 تا 3.2.3 از System V AMD64 psABI (نسخه‌ی 1.0، تاریخ ساخت 12 مارس 2025)؛ مستندات شیوه‌ی فراخوانی، استفاده از پشته، و صفحات پرولوگ و اپیلوگ Microsoft x64 (با تاریخ‌های 2018-12-17 تا 2025-03-19)؛ بخش‌های 6.1 تا 6.9 از مستند Arm AAPCS64 ویرایش 2025Q4 (تاریخ 23 ژانویه 2026)؛ و یادداشت‌های پلتفرم arm64 شرکت Apple. در این درس این قواعد را می‌خوانیم، آن‌ها را روی یک فایل C که به سه روش کامپایل شده پیاده می‌کنیم، و آنالایزری می‌سازیم که یک لیستینگ خام را به فهرستی دقیق و ارزیابی‌شده از محل پارامترها، ثبات‌های ذخیره‌شده و آفست‌های فریم تبدیل کند.

سه فریم استکیک تابع C، سه ABI مختلف
Stack frame of eight_args under three ABIs at -O2eight_args at -O2. SysV: 6 register arguments, 2 stack arguments, 0 frame bytes. Win64: 4 register arguments, 4 stack arguments, 32-byte shadow space, 0 frame bytes. AArch64: 8 register arguments, 0 frame bytes.System V AMD640 بایت فریم، بدون اشاره‌گر فریمsp هنگام ورود+16آرگومان h (فریم فراخواننده)+8آرگومان g (فریم فراخواننده)+0آدرس بازگشتثبات‌ها: a در rdi، b در rsi، c در rdx، d در rcx، e در r8، f در r9نتیجه در rax5 از 5 بررسی مطابق با قاعده هستندMicrosoft x640 بایت فریم، بدون اشاره‌گر فریمsp هنگام ورود+64آرگومان h (فریم فراخواننده)+56آرگومان g (فریم فراخواننده)+48آرگومان f (فریم فراخواننده)+40آرگومان e (فریم فراخواننده)+32اسلات سایه برای r9+24اسلات سایه برای r8+16اسلات سایه برای rdx+8اسلات سایه برای rcx+0آدرس بازگشتثبات‌ها: a در rcx، b در rdx، c در r8، d در r9نتیجه در rax6 از 6 بررسی مطابق با قاعده هستندAArch64 AAPCS640 بایت فریم، بدون اشاره‌گر فریمsp هنگام ورودثبات‌ها: a در x0، b در x1، c در x2، d در x3، e در x4، f در x5، g در x6، h در x7نتیجه در x05 از 5 بررسی مطابق با قاعده هستند
خط سیاه نشان‌دهنده اشاره‌گر استک هنگام ورود است. در x86-64 آدرس بازگشت دقیقاً بالای آن قرار دارد. خانه‌های بالای خط به فراخواننده (caller) تعلق دارند: آرگومان‌های استک و در Microsoft x64، فضای سایه 32 بایتی. خانه‌های زیر خط فریم تابع فراخوانده‌شده (callee) هستند: ثبات‌های ذخیره‌شده، رکورد فریم در AArch64، ثبات‌های آرگومان ریخته‌شده در استک در سطح -O0، و کادر خط‌چین کهربایی همان منطقه قرمز System V است در صورتی که تابع از آن استفاده کند.

تشخیص کتابچه‌ی قواعد از روی یک هدر

فایل convention.c شامل دوازده تابع است: leaf_add، eight_args، mixed، by_value، big_ret، sum_varargs، keeps و پنج تابع فراخوان برای توابع extern با ساختارهای مشابه. در ساختارها (Structs) از نوع long long استفاده شده است، زیرا long در پلتفرم MSVC برابر 4 بایت و در سایر پلتفرم‌ها 8 بایت است؛ تغییری در اندازه‌ی struct به معنای تغییر در شیوه‌ی فراخوانی خواهد بود. کامپایلر Apple clang نسخه‌ی 17.0.0 این سورس را هفت بار کامپایل کرده است: برای تارگت‌های -target x86_64-unknown-linux-gnu، x86_64-pc-windows-msvc و aarch64-unknown-linux-gnu در دو سطح بهینه‌سازی -O0 و -O2، و برای arm64-apple-macos با فلگ -O2. از آنجا که هیچ هدر سیستم‌عاملی برای لینوکس یا ویندوز روی سیستم نصب نبوده، فایل تمام نیازهایش را خودش تعریف کرده و صرفاً هدرهای استاندارد خود clang یعنی stdarg.h و stddef.h را می‌آورد.

تابع describe_object() در فایل code/main.py عدد جادویی (Magic number) چهار بایتی و یک فیلد ماشین را خوانده و نام قرارداد را تعیین می‌کند:

هدر فیلد مقدار کتابچه‌ی قواعد
7f 45 4c 46 ELF e_machine در آفست 0x12 62 System V AMD64 psABI
64 86 COFF Machine در آفست 0 0x8664 Microsoft x64
7f 45 4c 46 ELF e_machine 183 AAPCS64
cf fa ed fe Mach-O 64 cputype در آفست 4 0x0100000c AAPCS64 با تفاوت‌های Apple

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

قاعده System V psABI Microsoft x64 AAPCS64
آرگومان‌های عدد صحیح %rdi %rsi %rdx %rcx %r8 %r9، بر اساس اولین ثبات آزاد RCX RDX R8 R9، بر اساس موقعیت x0 تا x7، بر اساس NGRN
آرگومان‌های ممیز شناور %xmm0 تا %xmm7، بر اساس اولین ثبات آزاد XMM0 تا XMM3، بر اساس موقعیت v0 تا v7، بر اساس NSRN
نتیجه‌ی عدد صحیح %rax، سپس %rdx RAX x0، سپس x1
نتیجه‌ی ممیز شناور %xmm0، سپس %xmm1 XMM0 v0
نتیجه‌ی بزرگ‌تر از گنجایش ثبات‌ها آدرس با %rdi ارسال می‌شود «گویی اولین آرگومان است»، و در %rax برمی‌گردد اشاره‌گر در RCX، آرگومان‌ها یک شیفت به راست می‌خورند، بازگشت در RAX آدرس در x8، ثبات‌های x0 تا x7 دست‌نخورده می‌مانند
ذخیره‌شده توسط فراخوانده‌شده (Callee-saved) %rbp %rbx %r12 تا %r15 RBX RBP RDI RSI RSP R12 تا R15، و XMM6 تا XMM15 r19 تا r29، SP، و 64 بیت پایینی v8 تا v15
اولین آرگومان پشته در بدو ورود 8(%rsp) 40(%rsp)، بالای فضای 32 بایتی Shadow store 0(sp)
پایین‌تر از اشاره‌گر پشته ناحیه‌ی سرخ (Red zone) 128 بایتی «فرار (Volatile) تلقی می‌شود» بدون Red zone (در Apple: 128 بایت)
تراز پشته هنگام فراخوانی (%rsp + 8) در بدو ورود مضربی از 16 است خارج از پرولوگ و اپیلوگ تراز 16 بایتی دارد SP mod 16 = 0
نشانگر توابع با تعداد متغیر آرگومان (Variadic) ثبات %al حد بالای تعداد ثبات‌های برداری استفاده‌شده را نگه می‌دارد ممیزهای شناور در ثبات هم‌ارز عدد صحیح تکثیر می‌شوند هیچ نشانگری در ثبات‌ها نیست؛ Apple از اسلات‌های پشته‌ی 8 بایتی استفاده می‌کند

استاندارد System V: دو توالی مستقل ثبات و یک ناحیه‌ی سرخ

بخش 3.2.3 از psABI هر قطعه‌ی 8 بایتی (Eightbyte) از هر آرگومان را در دسته‌های INTEGER، SSE یا MEMORY طبقه‌بندی کرده و سپس ثبات‌ها را از دو فهرست کاملاً مستقل تخصیص می‌دهد. دسته‌ی INTEGER «اولین ثبات در دسترس از توالی %rdi، %rsi، %rdx، %rcx، %r8 و %r9» را می‌گیرد. دسته‌ی SSE ثبات‌های %xmm0 تا %xmm7 را مصرف می‌کند. «اگر برای هر یک از بخش‌های هشت‌بایتی یک آرگومان ثبات آزادی وجود نداشته باشد، کل آن آرگومان روی پشته ارسال می‌شود»، و آرگومان‌های حافظه «با ترتیب معکوس (از راست به چپ) روی پشته push می‌شوند».

تابع eight_args(a, b, c, d, e, f, g, h) در حالت -O2 تنها 27 بایت است:

      10: 48 01 fe                     	addq	%rdi, %rsi
      13: 48 8d 04 0a                  	leaq	(%rdx,%rcx), %rax
      17: 48 01 f0                     	addq	%rsi, %rax
      1a: 4c 01 c0                     	addq	%r8, %rax
      1d: 4c 01 c8                     	addq	%r9, %rax
      20: 48 03 44 24 08               	addq	0x8(%rsp), %rax
      25: 48 03 44 24 10               	addq	0x10(%rsp), %rax
      2a: c3                           	retq

شش ثبات، سپس دو بار خواندن حافظه در 8(%rsp) و 16(%rsp). شکل 3.3 از مستندات psABI بخش «memory argument eightbyte 0» را در 16(%rbp) تعریف می‌کند (زمانی که %rbp پایه‌ی فریم است)، که این مقدار در بدو ورود و قبل از هر گونه push معادل 8(%rsp) است. بنابراین g در آفست 8 و h در آفست 16 قرار دارد.

تابع mixed(int i, double d, long long l, float f) رفتار این دو فهرست مستقل را نشان می‌دهد. اعداد صحیح در %edi و %rsi (اولین و دومین ثبات INTEGER) می‌نشینند. مقادیر double و float به %xmm0 و %xmm1 (اولین و دومین ثبات SSE) اختصاص می‌یابند. اولین دستور cvtsi2sd %edi, %xmm2 است که در آن ثبات %xmm2 ثباتی موقت (Scratch register) به شمار می‌رود که تابع قبل از خواندن، ابتدا روی آن می‌نویسد. اگر آنالایزری مقصد دستور cvtsi2sd را به عنوان خواندن در نظر بگیرد، به اشتباه یک آرگومان ممیز شناور سوم ابداع می‌کند. این دقیقاً همان خطایی است که radare2 مرتکب شد و تابع mixed را به عنوان (double arg1, double arg2, double arg3) در ثبات‌های xmm0، xmm1 و xmm2 گزارش کرد.

تابع by_value(struct pair p) یک ساختار 16 بایتی را به شکل دو هشت‌بایتی INTEGER در ثبات‌های %rdi و %rsi ارسال کرده و به همان صورت نیز برمی‌گرداند: leaq 0x1(%rdi), %rax; leaq -0x1(%rsi), %rdx; retq. تابع big_ret(long long seed) یک داده‌ی 32 بایتی برمی‌گرداند که در دسته‌ی MEMORY قرار می‌گیرد؛ طبق psABI «فراخوان فضایی را برای مقدار بازگشتی اختصاص داده و آدرس این حافظه را در %rdi ارسال می‌کند، گویی اولین آرگومان تابع است». آرگومان seed به ثبات %rsi منتقل می‌شود و تابع با دستور movq %rdi, %rax آغاز به کار می‌کند، زیرا «در زمان بازگشت، %rax حاوی همان آدرسی خواهد بود که از سوی فراخوان در %rdi ارسال شده بود».

ناحیه‌ی سرخ (Red zone) مشخصه‌ی کلیدی دیگر System V است. به نقل از بخش 3.2.2 استاندارد psABI: «فضای 128 بایتی فراتر از مکانی که %rsp به آن اشاره دارد، رزرو تلقی شده و نباید توسط هندلرهای سیگنال یا وقفه (Interrupt) دستکاری شود.» در سطح -O0، تابع eight_args با pushq %rbp; movq %rsp, %rbp شروع شده و سپس شش عملیات ذخیره‌سازی روی -0x8(%rbp) تا -0x30(%rbp) بدون هیچ دستور subq انجام می‌دهد. این ذخیره‌سازی‌ها 48 بایت زیر اشاره‌گر پشته می‌نشینند. خروجی بهینه‌سازی‌شده‌ی -O2 برای تابع sum_varargs با دستور subq $0x58, %rsp مقدار 88 بایت حافظه می‌گیرد و سپس ناحیه‌ی ذخیره‌سازی ثبات‌ها را از -0x58(%rsp) تا -0x80(%rsp) می‌نویسد: دقیقاً 128 بایت زیر اشاره‌گر پشته، یعنی تمام ظرفیت Red zone. اگر همین فایل را با فلگ -mno-red-zone بسازید، تابع leaf_add دستورهای pushq %rax و addq $0x8, %rsp را اضافه می‌کند تا 8 بایت متغیرهای محلی خود را بالای اشاره‌گر پشته نگهداری کند.

فراخوانی‌های با تعداد متغیر آرگومان یک ثبات دیگر را هم درگیر می‌کنند. «از %al به عنوان یک آرگومان پنهان برای مشخص کردن تعداد ثبات‌های برداری استفاده‌شده استفاده می‌شود» که یک حد بالا از 0 تا 8 است. تابع call_varargs در حالت -O2 آن را با xorl %eax, %eax و در حالت -O0 با movb $0x0, %al مقداردهی می‌کند. تابع sum_varargs ابتدا این ثبات را بررسی می‌کند: دستور testb %al, %al; je باعث می‌شود در صورت عدم استفاده از ثبات‌های برداری، از ذخیره‌سازی هشت مرحله‌ای movaps %xmmN صرف‌نظر شود.

مایکروسافت x64: موقعیت‌ها، فضای Shadow Store و نبود Red Zone

قواعد مایکروسافت موضعی هستند. «آرگومان‌های عدد صحیح در ثبات‌های RCX، RDX، R8 و R9 منتقل می‌شوند. آرگومان‌های ممیز شناور در ثبات‌های XMM0L، XMM1L، XMM2L و XMM3L ارسال می‌گردند.» همچنین «هر آرگومان ممیز شناور و دقت مضاعف (Double-precision) در میان چهار پارامتر نخست، بسته به موقعیت خود، در یکی از ثبات‌های XMM0 تا XMM3 قرار می‌گیرد.» همان تابع mixed در اینجا آرگومان‌هایش را به ترتیب در %ecx، %xmm1، %r8 و %xmm3 دریافت می‌کند: اسلات 0 عدد صحیح است، پس RCX؛ اسلات 1 نوع double است، پس XMM1؛ اسلات 3 نوع float است، پس XMM3. در این میان XMM0 و XMM2 دست‌نخورده باقی می‌مانند.

علاوه بر این، فراخوان باید فضای لازم را رزرو کند. «فراخوان همواره باید فضای کافی برای ذخیره‌ی چهار پارامتر ثباتی تخصیص دهد، حتی اگر تابع فراخوانده‌شده این تعداد پارامتر نداشته باشد.» در مستندات به این بخش Shadow store یا Home addresses گفته می‌شود و مالکیت این چهار اسلات «عملاً در اختیار تابع فراخوانده‌شده است». در زمان ورود، آدرس 0(%rsp) محل قرارگیری آدرس بازگشت، 8(%rsp) تا 39(%rsp) مربوط به این چهار اسلات، و اولین آرگومان واقعی پشته در 40(%rsp) است. تابع eight_args در حالت -O2 مقادیر 0x28(%rsp)، 0x30(%rsp)، 0x38(%rsp) و 0x40(%rsp) را می‌خواند: یعنی آفست‌های 40، 48، 56 و 64.

محاسبات مربوط به پشته را می‌توان در سمت فراخوان دید. تابع call8 در حالت -O2 ابتدا دستور subq $0x48, %rsp را اجرا کرده و سپس مقادیر 5، 6، 7 و 8 را در بازه‌ی 0x20(%rsp) تا 0x38(%rsp) ذخیره می‌کند و پس از آن دستورهایی مثل movl $1, %ecx می‌آیند. این 72 بایت متشکل از 32 بایت برای Shadow store، 32 بایت برای چهار آرگومان پشته، و 8 بایت جهت حفظ تراز 16 بایتی آدرس بازگشت است. تابع call_varargs حتی با این‌که فقط آرگومان‌های ثباتی دارد، باز هم دستور subq $0x28, %rsp را اجرا می‌کند: 32 بایت برای Shadow store و 8 بایت برای تراز. تابع keeps سه ثبات غیرفرار (Nonvolatile) را ذخیره می‌کند (24 بایت) و سپس subq $0x20, %rsp را اجرا می‌کند تا تابع ext فضای Shadow store خود به اندازه‌ی 32 بایت را در زیر آدرس بازگشت ببیند.

ساختارها قاعده‌ی خاص خود را دارند. «هر آرگومانی که در 8 بایت جا نشود، یا اندازه‌ی آن 1، 2، 4 یا 8 بایت نباشد، باید با ارجاع (By reference) ارسال شود» و همچنین «آرگومان‌های 16 بایتی با ارجاع ارسال می‌شوند.» بنابراین تابع by_value اشاره‌گری به یک کپی از ساختار را در ثبات %rdx دریافت می‌کند که پس از اشاره‌گر مخفی نتیجه در %rcx قرار می‌گیرد: «فراخوان باید برای مقدار بازگشتی حافظه تخصیص دهد و اشاره‌گری به آن را به عنوان اولین آرگومان ارسال کند. سپس باقی آرگومان‌ها یک واحد به سمت راست شیفت پیدا می‌کنند.» تابع call_pair این کپی را در 0x20(%rsp) با دستور movaps می‌سازد، آدرس آن را با leaq 0x20(%rsp), %rdx و بافر نتیجه را با leaq 0x38(%rsp), %rcx می‌فرستد، و مقدار فیلد .b را از 0x40(%rsp) می‌خواند.

در این معماری Red zone وجود ندارد. «هر بخش از حافظه که فراتر از آدرس جاری RSP باشد فرار محسوب می‌شود: سیستم‌عامل یا یک دیباگر ممکن است در طول نشست اشکال‌زدایی کاربر یا در زمان اجرای هندلر وقفه، روی این حافظه بازنویسی انجام دهند.» تابع leaf_add در حالت -O0 به 8 بایت متغیر محلی نیاز دارد و این فضا را با pushq %rax در ورودی و popq %rcx پیش از retq به دست می‌آورد؛ یعنی دو دستور تک‌بایتی به جای ترکیب subq و addq. مستندات استفاده از پشته توابع برگی مجاز به این کار را این‌گونه تعریف می‌کند: «یک تابع برگ تابعی است که به ورودی در جدول توابع نیاز ندارد. این تابع حق ایجاد تغییر در ثبات‌های غیرفرار، از جمله RSP را ندارد».

توابع Variadic ثبات‌هایشان را در فضای خانگی ذخیره می‌کنند. «در توابع با آرگومان متغیر یا بدون پیش‌نمونه (Unprototyped)، هر مقدار ممیز شناور باید در ثبات هم‌ارز چندمنظوره نیز تکثیر شود.» تابع sum_varargs در وضعیت -O2 پس از یک دستور pushq %rax، ثبات‌های %rdx، %r8 و %r9 را در آفست‌های 0x18(%rsp)، 0x20(%rsp) و 0x28(%rsp) ذخیره می‌کند، که در واقع همان آفست‌های ورودی 16، 24 و 32 (فضای ذخیره‌ی پارامترهای ثباتی دوم، سوم و چهارم) هستند. ساختار va_list در آن با دستور leaq 0x18(%rsp), %rax یعنی موقعیت ذخیره‌ی %rdx (اولین آرگومان بی‌نام) مقداردهی می‌شود.

معماری AAPCS64 و تغییرات اعمال‌شده توسط Apple

استاندارد AAPCS64 در بخش 6.8.2 فهرستی از قواعد را بر پایه‌ی دو شمارنده مدیریت می‌کند. قاعده‌ی C.9: آرگومان‌های با مقدار صحیح «در کم‌ارزش‌ترین بیت‌های x[NGRN] کپی می‌شوند» و سپس مقدار شمارنده‌ی NGRN یکی افزایش می‌یابد. قاعده‌ی C.1 همین فرآیند را برای آرگومان‌های ممیز شناور به کمک NSRN انجام می‌دهد. بر اساس قاعده‌ی C.12، داده‌های مرکب با حداکثر اندازه‌ی 8 - NGRN کلمه‌ی مضاعف (Double-word) «در ثبات‌های عمومی متوالی کپی می‌شوند». طبق C.13، به محض این‌که آرگومانی به پشته سرریز کند، مقدار «NGRN برابر با 8 تنظیم می‌شود» تا پس از آن هیچ داده‌ای نتواند به ثبات‌ها برگردد. به همین ترتیب، eight_args تمام ثبات‌های x0 تا x7 را مصرف کرده و هرگز به پشته کاری ندارد. تابع mixed از ثبات‌های w0، d0، x1 و s1 استفاده می‌کند. تابع by_value هم در ورودی و هم در خروجی ثبات‌های x0 و x1 را به کار می‌گیرد.

بخش 6.9 تعیین می‌کند نتیجه‌ای که شرایط انتقال در ثبات را داشته باشد «در همان ثبات‌ها بازگردانده می‌شود»، در غیر این صورت «آدرس بلوک حافظه باید به عنوان یک آرگومان اضافی در ثبات x8 به تابع داده شود». تابع big_ret دستورهای str x0, [x8]، stur q0, [x8, #0x8] و str x9, [x8, #0x18] را اجرا کرده و اصلاً چیزی در x0 نمی‌نویسد. تابع call_big دستور mov x8, sp را تنظیم می‌کند و مقدار seed درون x0 باقی می‌ماند، چون در ادامه‌ی بخش 6.9 آمده است که «هیچ الزامی بر عهده‌ی تابع فراخوانده‌شده نیست که مقدار موجود در x8 را دست‌نخورده نگه دارد» و x8 در دسته‌ی ثبات‌های اصلی آرگومان قرار نمی‌گیرد.

بخش 6.1.1: «فراخوانی یک تابع موظف است محتوای ثبات‌های r19 تا r29 و همچنین SP را حفظ کند.» طبق 6.4.5.2، در رابط‌های عمومی همواره باید SP mod 16 = 0 باشد. پشته از نوع نزولی کامل (Full-descending) است و «هیچ نخی (Thread) مجاز به دسترسی خواندن یا نوشتن در ناحیه‌ی غیرفعال» زیر پشته نیست؛ به همین علت تابع leaf_add در پلتفرم لینوکس و حالت -O0 برای اختصاص 8 بایت متغیر محلی دستور sub sp, sp, #0x10 را اجرا می‌کند، در حالی که System V در این شرایط از Red zone بهره می‌برد.

بخش 6.4.6 رکورد فریم (Frame record) را به این شکل تعریف می‌کند: «یک رکورد فریم متشکل از دو مقدار 64 بیتی روی پشته... کلمه‌ی مضاعف با آدرس پایین‌تر به رکورد فریم قبلی اشاره کرده و کلمه‌ی مضاعف با آدرس بالاتر شامل مقداری است که در زمان ورود به تابع جاری، در ثبات LR قرار داشته است.» و در ادامه می‌آورد: «محل قرارگیری رکورد فریم در داخل یک فریم پشته الزام خاصی ندارد.» کامپایلر clang هر دو حالت قرارگیری را در همین فایل نشان می‌دهد. تابع keeps با کامپایلر لینوکس و فلگ -O2 این رکورد را در پایین‌ترین بخش قرار می‌دهد: stp x29, x30, [sp, #-0x30]!; mov x29, sp. همین تابع در سطح -O0 آن را در بالاترین بخش فریم می‌گذارد: sub sp, sp, #0x20; stp x29, x30, [sp, #0x10]; add x29, sp, #0x10.

یادداشت‌های فنی پلتفرم arm64 شرکت Apple، قواعد ثبات‌ها و تراز پشته را حفظ می‌کند اما ساختار فریم، Red zone و توابع با پارامتر متغیر را تغییر می‌دهد. «ثبات اشاره‌گر فریم (x29) باید همواره به یک رکورد فریم معتبر اشاره داشته باشد. برخی توابع مانند توابع برگ یا فراخوانی‌های انتهایی (Tail calls) مجازند از ایجاد ورودی در این لیست صرف‌نظر کنند.» نسخه‌ی Apple تابع keeps ابتدا ثبات‌های x22, x21, x20, x19 را ذخیره می‌کند و سپس رکورد فریم را در بالا می‌گذارد: stp x29, x30, [sp, #0x20]; add x29, sp, #0x20. همچنین «پلتفرم‌ها ثبات x18 را برای مصارف داخلی رزرو کرده‌اند؛ از این ثبات استفاده نکنید.» درباره‌ی حافظه می‌گوید: «ناحیه‌ی سرخ ARM64 شامل 128 بایت بلافاصله زیر اشاره‌گر پشته است»، با این حال clang در تابع برگ -O0 همچنان دستور sub sp, sp, #0x10 را صادر کرد؛ بنابراین کامپایلر به طور پیش‌فرض آن را مصرف نمی‌کند. پارامترهای متغیر به «تعداد مناسبی از اسلات‌های پشته‌ی 8 بایتی» اختصاص داده می‌شوند و «نوع داده‌ی va_list نام مستعاری برای *char است». تابع call_varargs در سیستم‌عامل اپل مقادیر 10، 20 و 30 را در [sp]، [sp, #8] و [sp, #0x10] ذخیره کرده و فقط w0 را پاس می‌دهد؛ اما نسخه‌ی لینوکس مقادیر را در w1، w2 و w3 قرار داده و با یک پرش بهینه‌ی b extva کار را تمام می‌کند. تابع sum_varargs اپل، آدرس پشته‌ی فراخوان را به عنوان va_list خود با دستور add x8, sp, #0x10 می‌خواند؛ در حالی که همین تابع در لینوکس، ثبات‌های x1 تا x7 و q0 تا q7 را در یک فریم 224 بایتی ذخیره می‌کند و فیلدهای ساختار __gr_offs را روی 56- و __vr_offs را روی 128- با یک جفت دستور mov x8, #-0x38; movk x8, #0xff80, lsl #32 مقداردهی اولیه می‌نماید.

سیستم نمادگذاری نیز متفاوت است. فرمت Mach-O پیشوند _ را به ابتدای تمام نمادهای C اضافه می‌کند، بنابراین در لیستینگ آن نام _keeps دیده می‌شود و اولین تابع با برچسب خصوصی ltmp0 ظاهر می‌گردد. آنالایزر به کمک جدول نمادها این برچسب‌ها را به نام اصلی بازمی‌گرداند.

بازخوانی و رمزگشایی دستی یک پرولوگ، در سه حالت

تابع keeps(int n) در یک حلقه تابع ext(i) را فراخوانی می‌کند و مقادیر n، i و acc را در طول فراخوانی زنده نگه می‌دارد؛ بنابراین طبق تمام ABIها باید ثبات‌ها ذخیره شوند. بایت‌های اولیه‌ی خروجی کامپایل با بهینه‌سازی -O2 را برای هر هدف بررسی می‌کنیم.

حالت System V در 4 بایت:

  1. مقدار 55 معادل دستور push r64 است که شماره‌ی ثبات در سه بیت پایینی آن قرار دارد: 50 + 5 یعنی %rbp. عمق پشته 8 بایت می‌شود و %rbp در آفست ورودی 8- ذخیره می‌گردد.
  2. مقدار 41 56: پیشوند 41 بایت REX است که بیت B آن روشن است، و 56 معادل 50 + 6 است. ترکیب REX.B شماره‌ی 6 را به 14 گسترش می‌دهد: یعنی pushq %r14 در آفست 16-.
  3. مقدار 53 همان 50 + 3 است: یعنی pushq %rbx در آفست 24-.

سه دستور push در مجموع 24 بایت به پشته اضافه می‌کنند. آدرس بازگشت نیز 8 بایت دارد که مجموعاً 32 بایت (مضرب 16) می‌شود؛ بنابراین فراخوانی callq ext در داخل حلقه بدون نیاز به دستور subq کاملاً تراز است. چون دستوری مثل movq %rsp, %rbp وجود ندارد، این تابع فاقد اشاره‌گر فریم است. بدنه از %rbp به عنوان یک ثبات Callee-saved جهت نگهداری متغیر acc استفاده می‌کند (xorl %ebp, %ebp و addl %eax, %ebp). اپیلوگ معکوس دقیق همین است: 5b 41 5e 5d c3.

حالت Microsoft x64 در 7 بایت:

  1. بایت 56 معادل pushq %rsi در آفست 8- است. بایت 57 معادل pushq %rdi در آفست 16- و 53 معادل pushq %rbx در آفست 24- است. هر سه ثبات در این ABI غیرفرار هستند، در حالی که RDI و RSI در System V ثبات‌های آرگومان بودند.
  2. توالی 48 83 ec 20: پیشوند REX.W، آپ‌کد 83 /5 با عملوند فوری 8 بیتی، و بایت ModRM با مقدار ec (معادل 11 101 100): یعنی subq $0x20, %rsp. عمق پشته به 56 می‌رسد.

مجموع 56 بایت به‌علاوه‌ی 8 بایت آدرس بازگشت برابر 64 می‌شود که تراز است. این 32 بایت همان Shadow store الزامی برای ext است. مستندات اپیلوگ فقط ساختارهایی مثل «یک دستور add RSP,constant یا lea RSP,constant[FPReg]، به همراه توالی صفر یا چند دستور pop ثبات‌های 8 بایتی و یک دستور return یا jmp» را مجاز می‌داند، و بایت‌های کد در اینجا عبارتند از: 48 83 c4 20 5b 5f 5e c3.

معماری AArch64 در 16 بایت به صورت چهار کلمه‌ی Little-endian:

  1. دستور a9bd7bfd یک stp با پیش‌ایندکس (Pre-index) است. بیت‌های 31:30 با مقدار 10 ثبات‌های 64 بیتی را انتخاب می‌کنند، بیت‌های 25:23 با مقدار 011 حالت Pre-index را مشخص می‌کنند، و بیت 22 با مقدار 0 نشان‌دهنده‌ی Store است. مقدار imm7 در بیت‌های 21:15 برابر 1111010 (معادل 6-) است که در 8 ضرب شده و 48- را می‌سازد. فیلد Rt2 در بیت‌های 14:10 عدد 30، فیلد Rn در بیت‌های 9:5 عدد 31 (همان sp) و فیلد Rt در بیت‌های 4:0 عدد 29 است. خروجی stp x29, x30, [sp, #-0x30]! می‌شود: اشاره‌گر پشته 48 واحد پایین می‌رود، x29 در آفست 48- و x30 در 40- می‌نشیند.
  2. دستور f9000bf5 یک str با آفست بدون علامت 12 بیتی ضرب‌در 8 است: مقدار imm12 در بیت‌های 21:10 برابر 2، فیلد Rn همان sp، و Rt برابر 21 است. خروجی str x21, [sp, #0x10] با آفست ورودی 32- خواهد بود.
  3. دستور 910003fd یک add با مقدار فوری است: مقدار imm12 برابر 0، ثبات Rn همان sp، و Rd برابر 29 است. خروجی mov x29, sp می‌شود: اشاره‌گر فریم دقیقاً به رکورد ایجادشده در آفست 48- اشاره می‌کند.
  4. دستور a9024ff4 یک stp با آفست علامت‌دار است: مقدار imm7 برابر 4 است که مقیاس آن به 32 می‌رسد. ثبات Rt2 برابر 19 و Rt برابر 20 است. حاصل stp x20, x19, [sp, #0x20] است: ثبات x20 در آفست 16- و x19 در آفست 8- می‌نشیند.

اندازه‌ی فریم 48 بایت است: x29 در آفست 0، x30 در آفست 8، x21 در 16، 8 بایت پدینگ (حاشیه‌گذاری) در 24، x20 در 32، و x19 در 40. همان‌طور که بخش 6.4.6 الزام می‌کند، کلمه‌ی آدرس‌پایین‌تر رکورد به رکورد قبلی اشاره دارد و کلمه‌ی بعدی حاوی ثبات LR است. در این میان، دستور cmp w0, #0x1 بین اولین و دومین عملیات ذخیره‌سازی جا گرفته است؛ اسکژولر این دستور را به درون پرولوگ منتقل کرده و آنالایزر توانایی چشم‌پوشی از مقایسه‌ها یا دستورهای حسابی بین عملیات‌های پشته را دارد.

نگارش دوگانه‌ی اطلاعات توسط کامپایلر

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

در ویندوز، این داده‌های موازی در سکشن‌های .pdata و .xdata ذخیره می‌شوند. اگر فایل کامپایل‌شده با -O2 را بدون فلگ -fno-asynchronous-unwind-tables بسازید، خروجی دستور llvm-objdump -h وجود سکشن .xdata با حجم 0x34 بایت و .pdata با حجم 0x48 بایت را نشان می‌دهد. مقدار 0x48 معادل 72 است که نشان‌دهنده‌ی شش ورودی RUNTIME_FUNCTION به حجم 12 بایت است که هر کدام سه جابه‌جایی (Relocation) از نوع IMAGE_REL_AMD64_ADDR32NB دارند. این شش تابع عبارتند از: sum_varargs، keeps، call8، call_varargs، call_pair و call_big. شش تابع دیگر در دسته‌ی توابع برگ قرار می‌گیرند؛ مستندات مدیریت خطای ویندوز می‌گوید: «اگر جستجو به ورودی در جدول توابع نرسد، کد به عنوان تابعی برگ در نظر گرفته می‌شود و RSP مستقیماً آدرس بازگشت را در بر دارد.»

بایت‌های سکشن .xdata برای تابع keeps به صورت 01 07 04 00 07 32 03 30 02 70 01 60 ذخیره شده است. ساختار UNWIND_INFO با یک فیلد 3 بیتی Version و 5 بیتی Flags شروع می‌شود: نسخه‌ی 1، بدون فلگ. فیلد SizeOfProlog برابر 7، فیلد CountOfCodes برابر 4، و FrameRegister به همراه FrameOffset برابر 0 هستند. هر ساختار UNWIND_CODE شامل یک آفست در پرولوگ به همراه یک بایت مشخص‌کننده‌ی آپ‌کد و اطلاعات جانبی است. توالی 07 32: در آفست 7، عملیات 2 همان UWOP_ALLOC_SMALL است، و اطلاعات 3 به معنی تخصیص 3 * 8 + 8 = 32 بایت است. توالی 03 30: عملیات 0 همان UWOP_PUSH_NONVOL است و ثبات 3 به rbx اشاره دارد. توالی 02 70: به ثبات rdi، و 01 60: به ثبات rsi اشاره می‌کند. کدهای عملیات به ترتیب معکوسِ اجرا ذخیره می‌شوند و دقیقاً همان سه عمل push و همان تخصیص 32 بایتی موجود در توالی بایت‌های 56 57 53 48 83 ec 20 را بازتاب می‌دهند. ورودی تابع sum_varargs به صورت 01 01 01 00 01 02 00 00 است: پرولوگی تک‌بایتی با یک کد، یعنی UWOP_ALLOC_SMALL به میزان 8 بایت. از دید آنوایندر، دستور pushq %rax یک عمل تخصیص فضا در نظر گرفته می‌شود؛ زیرا ثبات %rax فرار است و چیزی در آن ذخیره نمی‌شود.

در لینوکس، این اطلاعات موازی درون بخش .eh_frame ثبت می‌شود که ساختار آن در psABI 4.2.4 تعریف شده و با فرمت DWARF 5 بخش 6.4 انکود می‌گردد. اجرای دستور llvm-dwarfdump --eh-frame روی فایل کامپایل‌شده، ساختار یک ورودی FDE به ازای هر تابع را چاپ کرده و جابه‌جایی‌های R_X86_64_PC32 را بازمی‌گشاید؛ از این رو بازه‌ها معتبر هستند. خروجی برای keeps:

  DW_CFA_def_cfa_offset: +16
  DW_CFA_def_cfa_offset: +24
  DW_CFA_def_cfa_offset: +32
  DW_CFA_offset: RBX -32
  DW_CFA_offset: R14 -24
  DW_CFA_offset: RBP -16

مقدار CFA در واقع همان مقدار اشاره‌گر پشته پیش از فراخوانی است (یعنی 8 بایت بالاتر از پشته در لحظه‌ی ورود). بنابراین RBP -16 معادل آفست ورودی 8-، و R14 -24 معادل 16-، و RBX -32 معادل 24- است. عمیق‌ترین آفست CFA که 32 است، نشان‌دهنده‌ی فریم 24 بایتی به‌علاوه‌ی 8 بایت آدرس بازگشت است. ساختار FDE در AArch64 برای تابع keeps مقدار DW_CFA_def_cfa: W29 +48 را به همراه مقادیر W29 -48، W30 -40، W21 -32، W20 -16 و W19 -8 نشان می‌دهد. در آنجا خودِ CFA همان اشاره‌گر پشته در بدو ورود است؛ بنابراین آفست‌ها بدون نیاز به تبدیل، منطبق بر آفست‌های فریم هستند، و مقدار CFA تا زمانی که رکورد فریم زنده باشد بر اساس x29 تعریف می‌شود. تابع eh_frame_cross_check() این مقایسه را روی تمامی 12 ورودی FDE هر دو آبجکت‌فایل اجرا می‌کند. یک مورد نیاز به دقت داشت: تابع call8 در استاندارد System V دو آرگومان را در میانه‌ی اجرای خود روی پشته push می‌کند؛ از این رو عمیق‌ترین آفست CFA آن 32 است در حالی که فریم پرولوگ آن 8 است، و اعتبارسنجیْ عمیق‌ترین نقطه‌ای را که پشته در طول بدنه لمس می‌کند (24) با مقدار 8 جمع کرده و تطبیق می‌دهد.

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

تمام ابزارهای این حوزه ساختار داده‌ای اختصاصی برای نگهداری شیوه‌ی فراخوانی دارند و از یک مرحله‌ی تحلیلی (Pass) برای تخصیص آن به هر تابع استفاده می‌کنند. نام‌ها و خطاهای هر ابزار با دیگری تفاوت دارد. ابزارهای تجاری بر اساس مستنداتشان بررسی شده‌اند و ابزارهای متن‌باز روی نمونه‌های کامپایل‌شده اجرا شده و خروجی‌هایشان در کنار فایل‌ها ثبت گردیده است.

نرم‌افزار Ghidra 12.1.4 (تحت لایسنس Apache-2.0): قواعد در فایل‌های مشخصات کامپایلر با پسوند .cspec در کنار تعاریف زبان SLEIGH قرار دارند. فایل x86-64-gcc.cspec یک پیش‌الگو تحت عنوان <default_proto> به نام __stdcall تعریف می‌کند که بر اساس مستندات آن «برگرفته از ضمیمه‌ی معماری پردازنده‌ی System V Application Binary Interface AMD64 نگارش آوریل 2016 است». این فایل لیست‌های <pentry> را برای ثبات‌های XMM0_Qa تا XMM7_Qa و ثبات‌های RDI, RSI, RDX, RCX, R8, R9 مشخص کرده، ورودی پشته را در آفست 8 قرار داده، از قاعده‌ی join_dual_class برای انواع داده تا 16 بایت استفاده می‌کند، مقادیر بازگشتی را در XMM0_Qa, XMM1_Qa, RAX, RDX می‌نشاند و برای بقیه‌ی موارد حالت hidden_return را به کار می‌برد. فایل x86-64-win.cspec قرارداد __fastcall را با چهار تگ <group> تعریف می‌کند که XMM0_Qa را با RCX، و XMM1_Qa را با RDX و الی آخر جفت می‌کند؛ این همان رویکرد موضعی است. این فایل ورودی پشته را در آفست 40 در نظر گرفته، برای ساختارهای 3، 5، 6 یا 7 بایتی از convert_to_ptr با کامنت «تنها ساختارهای 8، 16، 32 یا 64 بیتی امکان قرارگیری در ثبات را دارند» استفاده می‌کند، ثبات‌های RBX RBP RDI RSI RSP R12 تا R15 و XMM6 تا XMM15 را در بخش unaffected می‌آورد، و بازه‌ی localrange از آفست 8 تا 39 را به عنوان Shadow store علامت می‌زند. فایل AARCH64.cspec نیز پروتکل __cdecl را مشخص می‌کند که اولین ورودی آن x8 با مشخصه‌ی storage="hiddenret" است، سپس q0 تا q7 و x0 تا x7 می‌آیند؛ انواع بزرگ‌تر از 17 بایت به اشاره‌گر تبدیل می‌شوند (convert_to_ptr) و ثبات x8 در لیست killedbycall با این توضیح قرار دارد: «ثبات حامل مکان خروجی غیرمستقیم که در لیست pentry منعکس نشده است». فاز پایانی دکامپایلر "Establish Function's Prototype" نام دارد؛ بخش‌های آنالیز نیز با عناوین "Call Convention ID" و "Decompiler Parameter ID" شناخته می‌شوند و گزینه‌ی "Prototype Evaluation" در تنظیمات، مدل پیش‌فرض را تعیین می‌کند.

نرم‌افزارهای IDA Pro و IDA Free (محصول شرکت Hex-Rays، تجاری؛ نگارش کنونی 9.4، بیلد 9.4.260714): امضای تابع در قالب کدهای C و به وسیله‌ی دستور SetType ثبت می‌شود و طبق مستندات «IDA از اطلاعات نوع داده استفاده می‌کند تا در لیستینگ دیس‌اسمبلی، آرگومان‌های تابع را کامنت‌گذاری کند». آرایش‌هایی که با هیچ قرارداد استانداردی منطبق نباشند با نام‌گذاری اختصاصی IDA مشخص می‌شوند: مثلاً تعریف int __usercall func@<ebx>(int x, int y@<esi>) خروجی را در EBX و متغیر y را در ESI تعیین می‌کند، و ساختار @<^12.4> برای ارجاع به اسلات پشته بر اساس آفست و اندازه به کار می‌رود. کلمه‌ی کلیدی __userpurge همین کارکرد را با پاک‌سازی پشته توسط فراخوانده‌شده ترکیب می‌کند. در همان بخش، شناسه‌های __spoils برای لیست ثبات‌های تخریب‌شده، و __hidden، __return_ptr و __struct_ptr برای نشان دادن آرگومان‌های ضمنی (که در این درس دستی استخراج می‌شوند) مطرح شده‌اند. قابلیت "Decompile as call" به کاربر اجازه می‌دهد برای یک دستورالعمل خاص، پروتوتایپ __usercall تعیین کند. نسخه‌ی IDA Free تنها از x86 و x86-64 پشتیبانی می‌کند.

نرم‌افزار Binary Ninja 6.0 با نام Krypton (محصول Vector 35، تجاری، به همراه نسخه‌ی Free با امکان دکامپایل x86، x86_64، ARMv7 و AArch64): در این پلتفرم، ساختار Platform «شامل تمامی اطلاعات محیط اجرای باینری، به ویژه شیوه‌های فراخوانی، سیستم‌عامل و معماری است» و مواردی چون linux-x86_64، windows-x86_64، linux-aarch64 و mac-aarch64 را در بر می‌گیرد که هر کدام متغیر default_calling_convention مشخصی دارند. یک شیء CallingConvention حاوی لیست‌های int_arg_regs، float_arg_regs، callee_saved_regs، caller_saved_regs، int_return_reg، high_int_return_reg، float_return_reg و دو فلگ است که تفاوت‌های مطرح‌شده در این درس را مستقیماً نشان می‌دهند: فلگ arg_regs_share_index که «اگر True باشد، آرگومان Nام بدون توجه به نوع داده، اسلات Nام را از هر دو لیست عدد صحیح و ممیز شناور اشغال می‌کند»، و فلگ stack_reserved_for_arg_regs که مشخص‌کننده‌ی «فضای shadow/home مورد استفاده در شیوه‌ی فراخوانی Windows x64» است. شناسه‌های eligible_for_heuristics، required_arg_regs و required_clobbered_regs موتور تخمین اکتشافی این ابزار را هدایت می‌کنند. انواع پلتفرم‌ها در قالب فایل‌های هدر C در مسیر types/platform/ قرار دارند.

نرم‌افزار radare2 6.2.4 (تحت لایسنس LGPL-3.0، تست‌شده در این درس): دستور afcl لیست قراردادهای شناخته‌شده توسط معماری جاری را ارائه می‌دهد: مواردی همچون amd64 amd64syscall dlang golang golang_abi0 ms p9 reg swift برای x86-64 و گزینه‌های arm64 dlang golang p9 reg swift برای معماری AArch64. دستور afc شیوه‌ی فراخوانی تابع فعلی را نمایش می‌دهد، دستور afcr نگاشت‌هایی مثل cc.amd64.arg0: rdi تا arg5: r9 و مقدار بازگشتی ret0: rax را چاپ می‌کند، و پروفایل‌ها در قالب فایل‌های SDB با الگوی نام‌گذاری cc-ARCH-BITS.sdb ذخیره شده‌اند. نرم‌افزار radare2 به درستی گزینه‌ی amd64 را برای باینری ELF و ms را برای فایل COFF انتخاب کرد. دستور afv آرگومان‌ها و متغیرها را برمی‌شمرد، و afs امضای تابع را می‌سازد. در فایل بهینه‌شده‌ی -O2 لینوکس، دستور afv هفت آرگومان برای تابع eight_args پیدا کرد و ثبات rcx را جا انداخت؛ در فایل ویندوزی نیز فقط rcx, rdx, r8 و چهار اسلات پشته را تشخیص داد و r9 را از قلم انداخت. در هر دو مورد، ثبات‌های جاافتاده تنها درون ساختار دستور lea حضور داشتند. در خصوص تابع mixed، در لینوکس مقدار double arg1 @ xmm0, arg2 @ xmm1, arg3 @ xmm2 را نشان داد و در ویندوز هیچ آرگومانی تشخیص نداد. روی AArch64 آرگومان‌های x0 تا x7 را برای eight_args شناخت، ولی برای big_ret آرگومان‌های x0 و double arg2 @ d1 را خروجی داد در حالی که تابع عملاً x8 را خوانده و ثابتی را در q1 لود می‌کند. تمام خروجی‌های afs با نوع void شروع می‌شوند زیرا نوع خروجی استنتاج نمی‌شود. در حالت -O0 که همه‌ی آرگومان‌ها روی فریم ریخته می‌شوند، afv هر هشت آرگومان eight_args را به درستی همراه با rbp+0x10 و rbp+0x18 برای جفت روی پشته تشخیص داد.

نرم‌افزار rizin 0.9.1 (تحت لایسنس LGPL-3.0، تست‌شده در این درس) و Cutter 2.5.0: دستور afcl گزینه‌های amd64 amd64syscall ms reg swift را برمی‌گرداند. دستور afc برای فایل COFF مقدار amd64 را نشان داد؛ بنابراین با این‌که پروفایل مایکروسافت در ابزار تعریف شده بود، اما از فرمت فایل استنتاج نشد. دستور afi برای eight_args روی لینوکس هفت آرگومان (باز هم بدون rcx) نشان داد، و روی ویندوز آرگومان‌های r8, rcx, rdx به همراه سه اسلات پشته (بدون r9 و بدون اسلات موجود در 0x40) را خروجی داد. دستوری به نام afv وجود ندارد و معادل آن afvl است. نرم‌افزار Cutter رابط گرافیکی کاربری بر روی rizin است.

نرم‌افزار angr 10.0.3 (تحت لایسنس BSD-2، تست‌شده در این درس): اجرای CFGFast و به دنبال آن CompleteCallingConventions(recover_variables=True) ویژگی function.calling_convention را بر مبنای فرمت فایل به کلاس‌های SimCCSystemVAMD64، SimCCMicrosoftAMD64 یا SimCCAArch64 نگاشت می‌کند و مقدار function.prototype را با یک امضا مقداردهی می‌نماید. این ابزار هر هشت آرگومان تابع eight_args را در هر سه هدف به درستی پیدا کرد، اشاره‌گر پنهان big_ret را در x86-64 به شکل (BOT*, long long) -> BOT* شناخت، ارسال ارجاعی ساختار در تابع by_value ویندوز را به صورت (BOT*, BOT*) -> BOT* درآورد، و تابع variadic با نام sum_varargs را در System V به شکل (long long, ...) بازیابی کرد. با این حال خطاهای آن با radare2 متفاوت بود: تابع mixed در System V و AArch64 به شکل (long long, long long, double, float) استخراج شد؛ یعنی ثبات‌های صحیح با ترتیبی اشتباه ثبت شدند زیرا خروجی لیست اعداد صحیح را پیش از لیست اعشاری چاپ می‌کند. تابع big_ret در معماری AArch64 به عنوان (long long) -> double تشخیص داده شد که نشان می‌دهد قاعده‌ی x8 نادیده گرفته شده و ذخیره‌سازی‌های برداری به عنوان خروجی فرض شده‌اند. در نهایت تابع sum_varargs روی پلتفرم AArch64 با 16 پارامتر استخراج شد که کل فضای ذخیره‌سازی ثبات‌ها را پوشش داده بود.

سایر ابزارها: نرم‌افزار Hopper 6.6.0 یک دیس‌اسمبلر و دکامپایلر تجاری برای macOS و Linux است؛ عملکرد آن روی شیوه‌های فراخوانی در این درس پیاده یا مستند نشده است. ابزارهای دیباگر مثل lldb 1703 و gdb ساختار پشته‌ی در حال اجرا را از طریق دستورهای frame info و register read نمایش می‌دهند که نمای پویای همین آفست‌ها در درس 42 است. وب‌سایت Compiler Explorer هر سه لیستینگ دیس‌اسمبلی را در کنار سورس‌کد اصلی نشان می‌دهد، که سریع‌ترین روش جهت اعتبارسنجی یک قاعده پیش از بررسی بایت‌های آن است.

مفهوم Ghidra IDA Binary Ninja radare2 / rizin angr این درس
شیء شیوه‌ی فراخوانی prototype در فایل .cspec کلمه‌ی کلیدی calling convention در نوع تابع CallingConvention بر روی پلتفرم نام در afc، فایل cc-ARCH-BITS.sdb زیرکلاس SimCC expected_locations(abi, name)
مقدار پیش‌فرض هر تارگت <default_proto> در هر cspec تنظیمات کامپایلر Platform.default_calling_convention متغیرهای afc= و anal.cc بر اساس فرمت باینری describe_object()
ثبات‌های آرگومان لیست <pentry>، تگ <group> برای موضعی @<reg> در __usercall ثبات‌های int_arg_regs، float_arg_regs، arg_regs_share_index cc.NAME.argN لیست‌های ARG_REGS و FP_ARG_REGS INT_ARG_REGS, FLOAT_ARG_REGS
فضای Shadow store اسلات پشته‌ی 8 تا 39 در localrange ضمنی stack_reserved_for_arg_regs cc.ms.argn=stack STACKARG_SP_DIFF STACK_ARG_BASE[WIN64] = 40
اشاره‌گر مخفی نتیجه مقادیر hidden_return و storage="hiddenret" عبارات __return_ptr و __hidden خروجی بزرگ‌تر از گنجایش int_return_reg مدل نشده است اولین آرگومان با نام *BOT HIDDEN_RETURN_REG
ثبات‌های Callee-saved تگ <unaffected> لیست __spoils عکس آن را دارد فیلد callee_saved_regs پروفایل آرگومان CALLEE_CLEANUP و معماری CALLEE_SAVED
تخصیص شیوه به تابع آنالایزرهای "Call Convention ID" و "Establish Function's Prototype" کاربر یا کتابخانه‌ی تایپ‌ها تشخیص اکتشافی با required_arg_regs دستورهای afc، afva و aaft مرحله‌ی CallingConventionAnalysis تحلیل خواندن قبل از نوشتن به همراه کتابچه‌ی قواعد

پیاده‌سازی از صفر

اجرای برنامه‌ی دمو:

cd phases/13-tools-and-protocols/38-calling-conventions-and-stack-frames
python3 code/main.py

خروجی برنامه یک گزارش ساختاریافته به فرمت JSON است. این گزارش به ازای تک‌تک آن هفت آبجکت‌فایل، فرمت، معماری و ABI را تعیین می‌کند؛ و به ازای هر یک از دوازده تابع، بایت‌های پرولوگ، ثبات‌های ذخیره‌شده به همراه آفست‌های ورودی، اندازه‌ی فریم پشته، اشاره‌گر فریم، ثبات‌های پارامتر، آفست‌های آرگومان‌های روی پشته، مقادیر Spilled (ذخیره‌شده در پشته)، ثبات برگشتی یا اشاره‌گر مخفی، عمق پشته در زمان فراخوانی‌ها، و ثبات‌ها و مقادیر ذخیره‌شده‌ی خروجی در توابع فراخوان را ارائه می‌دهد. در پایان هر تابع، چک‌های اعتبارسنجی آن آورده شده است: متن قاعده، مقدار مورد انتظار بر اساس امضای سورس‌کد، مقدار مشاهده‌شده از روی بایت‌های ماشین، و وضعیت تطابق آن‌ها. انتهای گزارش شامل دو بررسی متقابل بر اساس داده‌های Unwind و اطلاعات استخراج‌شده از سایر ابزارهاست. در جریان تحلیل این 84 تابع، مجموعاً 489 بررسی تطبیقی انجام می‌شود که تمام 489 بررسی نتیجه‌ی مثبت می‌دهند.

ساختار مورد انتظار:

{
  "objects": {
    "convention-x86_64-windows-O2": {
      "format": "COFF", "machine": "AMD64", "abi": "Microsoft x64", "checks": "73/73 agree",
      "functions": {
        "keeps": {
          "prologue": ["56  pushq %rsi", "57  pushq %rdi", "53  pushq %rbx", "48 83 ec 20  subq $0x20, %rsp"],
          "frameBytes": 56,
          "savedRegisters": [{"register": "rsi", "entryOffset": -8}, {"register": "rdi", "entryOffset": -16}, {"register": "rbx", "entryOffset": -24}],
          "parameterRegisters": ["rcx"],
          "returnRegisters": ["rax"],
          "calls": [{"callee": "ext", "depth": 56}],
          "checks": [{"rule": "Microsoft x64: the caller must always allocate ...", "expected": "at least 32 bytes ...", "observed": [56], "agree": true}, "..."]
        }
      }
    }
  },
  "sehCrossCheck": ["..."],
  "ehFrameCrossCheck": {"...": "..."}
}

برای درک بهتر، کدهای فایل code/main.py را به این ترتیب بررسی کنید:

  1. تابع describe_object() هدر باینری‌های ELF، COFF یا Mach-O و جدول نمادها را پردازش کرده و نام ABI و فهرست توابع را استخراج می‌کند.
  2. تابع parse_listing() متن حاصل از دستور llvm-objdump -d -r را بر اساس توابع تفکیک کرده، مقاصد Relocation را برای پرش‌ها و فراخوانی‌ها نگه می‌دارد، و کامنت‌ها را بر اساس ساختار نحوی معتبر حذف می‌کند: کاراکتر # در سینتکس AT&T، و کاراکترهای // و ; در AArch64 (چرا که در آنجا # نشانگر عملوند فوری است).
  3. توابع effect_x86() و effect_arm() هر دستور را به مجموعه‌ای از خواندن‌ها، خواندن‌های آدرس، نوشتن‌ها، بارگذاری‌ها (Loads)، ذخیره‌سازی‌ها (Stores) و تغییرات اعمال‌شده بر اشاره‌گر پشته تبدیل می‌کنند. در سینتکس AT&T آخرین عملوند مقصد است؛ در AArch64 اولین عملوند مقصد به حساب می‌آید، مگر در دستورهای ذخیره‌سازی، مقایسه‌ها و branchها. دستور lea فقط آدرس را محاسبه و می‌خواند و هیچ لودی از حافظه ندارد. مقصد دستوراتی نظیر mov، cvtsi2sd، setcc، و دستور صفر کردن یعنی xor r, r صرفاً یک عمل نوشتن است. یک فراخوانی (Call) تمام ثبات‌های فرار را بازنویسی می‌کند. دستور Store با پیش‌ایندکس اشاره‌گر sp را پیش از دسترسی، و Load با پس‌ایندکس (Post-index) آن را بعد از دسترسی جابه‌جا می‌کند.
  4. تابع analyze_function() بدنه‌ی تابع را یک بار تا رسیدن به اولین ret یا پرش انتهایی پیمایش می‌کند. این تابع عمق نفوذ اشاره‌گر پشته را نسبت به وضعیت ورود ردیابی می‌کند؛ به طوری که عملوند حافظه‌ای مثل disp(%rsp) در آفست ورودی disp - depth و عبارت disp(%rbp) در آفست frame_pointer + disp تفسیر می‌شود. همچنین ثبات‌های آرگومان را که پیش از مقداردهی خوانده می‌شوند، دسترسی‌های پشته از آفست مربوط به اولین آرگومان به بعد، ذخیره‌سازی ثبات‌های ورودی در فریم پشته، دسترسی‌های پایین‌تر از اشاره‌گر پشته، و عمق پشته در زمان هر فراخوانی را ثبت می‌کند.
  5. تابع _return_registers() اگر آخرین دسترسی به یک ثبات قبل از خروج تابع از نوع نوشتن باشد، آن را به عنوان ثبات خروجی در نظر می‌گیرد. اگر از ثبات به عنوان عملوند داده‌ای خوانده شود رد صلاحیت می‌گردد، اما اگر به عنوان پایه‌ی یک آدرس خوانده شود همچنان معتبر است؛ این همان قاعده‌ای است که ثبات rax را در تابع big_ret در حالت Microsoft x64 حفظ می‌کند، جایی که بعد از movq %rcx, %rax عملیات‌های نوشتن متوالی از طریق (%rax) داریم.
  6. تابع expected_locations() قواعد سه کتابچه را روی امضای C اعمال می‌کند: دو لیست مستقل System V همراه با کلاس‌های هشت‌بایتی، اسلات‌های موضعی مایکروسافت با ساختارهای ارجاعی، و شمارنده‌های استاندارد AAPCS64 همراه با ثبات x8 برای خروجی‌های بزرگ‌تر از 16 بایت.
  7. تابع run_checks() مقادیر واقعی را با انتظارات تطبیق داده و قواعد ساختار فریم را می‌سنجد: ثبات‌های غیرفراری که دستکاری شده‌اند باید حتماً ذخیره شده باشند، شرط (rsp + 8) % 16 == 0 یا sp % 16 == 0 در زمان هر فراخوانی برقرار باشد، وجود 32 بایت فضای Shadow در هر فراخوانی مایکروسافت رعایت گردد، هیچ دسترسی به پایین پشته در مایکروسافت و AAPCS64 و حداکثر 128 بایت دسترسی در System V ثبت نشود، و ساختار اپیلوگ قرینه‌ی دقیق پرولوگ بوده و پشته در انتها به مکان اولیه‌اش بازگردد.
  8. توابع seh_cross_check() و eh_frame_cross_check() داده‌های Unwind موجود در فایل‌های بدون فلگ -fno-asynchronous-unwind-tables را تحلیل کرده و آن‌ها را با نتایج حاصل از پردازش پرولوگ مقایسه می‌کنند.

سه دستور کمکی دیگر نیز در برنامه تعریف شده است: دستور python3 code/main.py analyze convention-x86_64-windows-O2 keeps جزئیات یک تابع مشخص را چاپ می‌کند. دستور python3 code/main.py object FILE خروجی تطبیق هدر را نمایش می‌دهد. دستور python3 code/main.py seh نیز جزئیات ساختار UNWIND_INFO را برای تمام توابع واجد شرایط رمزگشایی می‌کند.

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

روی یک فایل باینری جدید که کامپایل کرده‌اید، مراحل زیر را با آنالایزر اجرا کنید:

  1. دستور python3 code/main.py object FILE.o را اجرا کرده و نام ABI گزارش‌شده را یادداشت کنید. اگر نوع هدر با فرمان کامپایل شما تناقض دارد کار را متوقف کنید؛ یکی از آن‌ها اشتباه است.
  2. با اجرای llvm-objdump -d -r FILE.o فایل لیستینگ را کنار آبجکت‌فایل ذخیره کرده و تحلیل را شروع کنید. پیش از هر چیز بایت‌های پرولوگ و ثبات‌های ذخیره‌شده را بخوانید، و اندازه‌ی فریم را با عملیات‌های push و دستور sub مقایسه کنید.
  3. ثبات‌های پارامتر و آفست‌های پشته را با امضای تابع در سورس‌کد بسنجید. اگر ثباتی وجود دارد که امضا آن را توجیه نمی‌کند، یا با یک اشاره‌گر مخفی روبه‌رو هستید، یا با نشانگر پارامتر متغیر، و یا با یک ثبات موقت که تحلیل‌گر آن را به اشتباه آرگومان فرض کرده است. مورد آخر از نقص جدول اثرات دستورات (Effect table) ناشی می‌شود که باید آن را با قاعده‌ای عمومی برای آن دستور رفع کنید.
  4. مغایرت‌های احتمالی در گزارش ارزیابی را بخوانید. از آنجا که هر ارزیابی به قاعده‌ی مشخصی ارجاع می‌دهد، گام نخست بررسی درستی قاعده، سپس بازبینی بایت‌های کد، و در نهایت تصحیح آنالایزر است.
  5. اگر فایل باینری دارای اطلاعات Unwind است، تطبیق متقابل را اجرا کنید. انطباق این دو منبع مستقل، محکم‌ترین شواهد (Evidence) درستی را به دست می‌دهد.
  6. اگر ابزار دیگری نصب دارید، خروجی آن را با دستور مربوطه استخراج کرده و نتایج را بسنجید. اگر نتیجه یکسان بود، سه منبع تأیید دارید؛ اگر متفاوت بود، نقطه و ثبات خاصی را پیدا کرده‌اید که نیازمند موشکافی است، و این موضوع پایه‌ی ادامه‌ی تحلیل در درس 35 خواهد بود.

آزمایشگاه تعاملی

بخش اول: یک دستور، سه اثر مختلف

از مسیر دایرکتوری code مفسر Python را باز کنید:

cd phases/13-tools-and-protocols/38-calling-conventions-and-stack-frames/code
python3 -q

دستورهای زیر را اجرا کنید:

from main import *
effect(Instruction(0, "48 8d 04 0a", "leaq", "(%rdx,%rcx), %rax"), SYSV)
effect(Instruction(0, "f2 0f 2a d7", "cvtsi2sd", "%edi, %xmm2"), SYSV)
effect(Instruction(0, "a9bd7bfd", "stp", "x29, x30, [sp, #-0x30]!"), AAPCS64)

پیش از اجرای هر خط، ثبات‌های خوانده‌شده، ثبات‌های به کار رفته در آدرس‌دهی، ثبات‌های نوشته‌شده و میزان تغییر پشته را پیش‌بینی و یادداشت کنید. سپس ساختار [sp, #-0x30]! را با دستور ldp به شکل [sp], #0x30 تغییر دهید و مشاهده کنید که چگونه میزان جابه‌جایی پشته از پارامتر sp_delta به post_sp_delta منتقل می‌شود.

بخش دوم: ساختار فریم پشته در تابع keeps

info, listing = load_fixture("convention-aarch64-linux-O2")
keeps = analyze_function("keeps", listing["keeps"], info.abi)
keeps.saved
keeps.frame_pointer
[c.to_dict() for c in frame_cells(keeps)]

آفست‌های به دست آمده را با نتایج تحلیل دستی در بخش‌های قبل تطبیق دهید. سپس باینری convention-arm64-macos-O2 را بارگذاری کرده و همان سه خط را فراخوانی کنید؛ خواهید دید که رکورد فریم به آفست 16- جابه‌جا شده و چهار ثبات در زیر آن قرار گرفته‌اند. در ادامه فایل convention-x86_64-linux-O2-fp را لود کرده و مقدار saved را برای keeps ببینید: اشاره‌گر فریم بازگشته است، و هم‌زمان یک دستور pushq %rax اضافه شده که تنها وظیفه‌اش رساندن (rsp + 8) به مضربی از 16 در زمان فراخوانی است.

بخش سوم: شناسایی الگوی خواندن قبل از نوشتن در تابع mixed

info, listing = load_fixture("convention-x86_64-linux-O2")
mixed = analyze_function("mixed", listing["mixed"], info.abi)
mixed.param_registers
mixed.return_registers
expected_locations(SYSV, "mixed")["parameters"]
expected_locations(WIN64, "mixed")["parameters"]

ترتیب ثبات‌ها در System V برابر با rdi, xmm0, rsi, xmm1 و در مایکروسافت rcx, xmm1, r8, xmm3 است. حالا فایل ویندوزی را لود کرده و سه خط اول را مجدداً اجرا کنید. سپس خروجی ثبت‌شده از radare2 را برای لینوکس باز کرده و بخش [afv mixed] را پیدا کنید: این ابزار سه متغیر از نوع double یافته است. دستوری را که باعث شده تا ثبات سوم به اشتباه آرگومان تلقی شود پیدا کنید.

بخش چهارم: تأیید از طریق داده‌های Unwinder کامپایلر

for row in seh_cross_check():
    print(row["function"], row["sizeOfProlog"], row["unwindPushes"], row["unwindAllocBytes"], row["agree"])
for row in eh_frame_cross_check("convention-x86_64-linux-O2-eh"):
    print(row["function"], row["cfiSaved"], row["cfiMaxOffset"], row["deepestStack"], row["agree"])

دستور pushq %rax از تابع sum_varargs را در جدول SEH پیدا کنید و توضیح دهید چرا کد Unwind آن را به عنوان یک عمل تخصیص فضا (Allocation) ثبت کرده است. سپس تابع call8 را در جدول .eh_frame بررسی کرده و دلیل وجود عدد 32 را شرح دهید.

تمرین عملی

یک تارگت چهارم و یک کتابچه‌ی قواعد چهارم اضافه کنید.

نیازمندی‌ها:

  • فایل convention.c را با دستور -target x86_64-apple-macos -O2 کامپایل کرده و لیستینگ دیس‌اسمبلی آن را استخراج کنید. تابع describe_object() را طوری توسعه دهید که مقدار cputype با کد 0x01000007 در Mach-O را بخواند و آن را با در نظر گرفتن پیشوند نماد _ به استاندارد System V psABI (که اپل در سامانه‌های x86-64 از آن تبعیت می‌کند) نگاشت کند.
  • تطابق تمام توابع را با ارزیابی‌های System V بسنجید. یک تفاوت ساختاری در پرولوگ‌ها را پیدا و ثبت کنید: سیستم‌عامل Darwin در تمامی سطوح بهینه‌سازی (Optimization)، اشاره‌گر فریم را حفظ می‌کند.
  • نسخه‌ای از باینری لینوکس x86-64 را همراه با فلگ -fstack-protector-strong کامپایل کنید. اسکن پرولوگ را طوری گسترش دهید که عملیات بارگذاری Canary از %fs:0x28 و ذخیره‌ی آن در زیر متغیرهای محلی را به عنوان یک خانه‌ی canary بشناسد، و دستور مقایسه پیش از ret را به عنوان چک محافظتی اپیلوگ ثبت نماید.
  • یک تست بنویسید که اگر ذخیره‌سازی مقدار Canary به اشتباه به عنوان یک آرگومان Spilled تلقی شد، با خطا مواجه شود.
  • طرح فریم کدهای محافظت‌شده با قناری را پیش از اجرای آنالایزر، به شکل دستی رسم کنید و تصویر یا دیاگرام آن را کنار کدهای تست قرار دهید.

معیار موفقیت این است که اجرای python3 code/main.py آبجکت جدید را گزارش دهد، تمام چک‌ها با موفقیت پاس شوند، و سلول‌های فریمِ توابع محافظت‌شده، مقدار canary را دقیقاً در میان متغیرهای محلی و ثبات‌های ذخیره‌شده نشان دهند.

خروجی این درس

این درس سند راهنمای outputs/skill-calling-conventions-and-stack-frames.md را به شما تحویل می‌دهد. با استفاده از آن می‌توانید یک فایل کامپایل‌شده، لیستینگ و امضاهای C آن را دریافت کنید و محل پارامترها، چیدمان فریم، ثبات مقدار بازگشتی، و انطباق هر استنتاج با قواعد مرجع و اطلاعات Unwind را به دست آورید. این ابزار ابتدا نوع ABI را از روی هدر الزامی می‌داند، تلقی کردن مقصد دستورهایی نظیر cvtsi2sd یا xor r, r به عنوان خواندن را ممنوع می‌کند، و با مشاهده‌ی اولین عدم تطابق، گزارش خطا تولید می‌نماید.

راستی‌آزمایی

برنامه‌ی دمو و تست‌های قطعی را اجرا کنید:

cd phases/13-tools-and-protocols/38-calling-conventions-and-stack-frames
python3 code/main.py
python3 -m unittest discover -s code/tests -v

اعتبارسنجی باید موارد زیر را اثبات کند:

  • هدرهای فرمت‌های ELF، COFF و Mach-O در هفت فایل، به درستی به System V، Microsoft x64، AAPCS64 و Apple arm64 اشاره دارند.
  • جدول نمادها در تمام فایل‌ها دوازده تابع یکسان را فهرست می‌کند، و پیشوند _ در Mach-O و برچسب ltmp0 به درستی رفع ابهام شده‌اند.
  • پارسر لیستینگ، نام تابع ext را به عنوان مقصد فراخوانی حفظ می‌کند و مقادیر فوری AArch64 مانند #-0x30 را دست‌نخورده باقی می‌گذارد.
  • دستور leaq (%rdx,%rcx), %rax دو ثبات را به عنوان آدرس می‌خواند و هیچ داده‌ای لود نمی‌کند؛ دستورهای xorl %eax, %eax و cvtsi2sd صرفاً عملیات نوشتن هستند؛ و دستور stp پیش‌ایندکس مقدار sp را پیش از ذخیره و ldp پس‌ایندکس آن را پس از خواندن تغییر می‌دهد.
  • تابع keeps در سطح -O2 روی System V بایت‌های 55 41 56 53 را با یک فریم 24 بایتی بدون اشاره‌گر فریم دارد؛ در Microsoft x64 توالی 56 57 53 48 83 ec 20 با فریم 56 بایتی؛ و در AArch64 با دستور a9bd7bfd شروع شده و رکورد فریم را در آفست 48- قرار می‌دهد.
  • نسخه‌ی اپل، رکورد فریم را در آفست 16- و بالاتر از چهار ثبات ذخیره‌شده قرار می‌دهد.
  • تمام توابع در سطح بهینه‌سازی -O0 روی لینوکس دارای اشاره‌گر فریم هستند ولی در -O2 فاقد آنند، و فلگ -fno-omit-frame-pointer اشاره‌گر فریم را همراه با یک عمل push برای تراز پشته بازمی‌گرداند.
  • تابع برگ در سطح -O0 از 8 بایت Red zone در System V، دستور pushq %rax در Microsoft x64، و sub sp, sp, #0x10 در AAPCS64 استفاده می‌کند؛ همچنین فلگ -mno-red-zone استفاده از Red zone را کاملاً متوقف می‌سازد.
  • ساختار تمام اپیلوگ‌ها قرینه‌ی دقیق پرولوگ‌هایشان است و اشاره‌گر پشته به وضعیت اولیه‌اش بازمی‌گردد.
  • تابع eight_args در هر دو سطح بهینه‌سازی، شش ثبات به همراه اسلات‌های 8 و 16، یا چهار ثبات به همراه اسلات‌های 40 تا 64، یا ثبات‌های x0 تا x7 را می‌خواند.
  • تابع mixed با ثبات‌های rdi, xmm0, rsi, xmm1 در System V و با ثبات‌های rcx, xmm1, r8, xmm3 در Microsoft x64 نگاشت می‌شود و خروجی هر دوی آن‌ها در xmm0 قرار دارد.
  • ساختار 16 بایتی در System V و AAPCS64 درون دو ثبات جابه‌جا می‌شود و در Microsoft x64 به صورت ارجاعی در پشت یک اشاره‌گر پنهان قرار می‌گیرد.
  • ساختار 32 بایتی خروجی خود را از طریق ثبات‌های rdi، rcx یا x8 برمی‌گرداند.
  • تابع با تعداد متغیر آرگومان در System V ثبات %al را چک می‌کند، در مایکروسافت ثبات‌های rdx, r8, r9 را در آفست‌های 16، 24 و 32 می‌نشاند، در لینوکس ثبات‌های x1 تا x7 و q0 تا q7 را ذخیره می‌کند، و در اپل اشاره‌گر پشته‌ی فراخوان را مستقیماً می‌خواند.
  • توابع فراخوان ثبات‌های مورد انتظار را مقداردهی می‌کنند، مقادیر 7 و 8 را در آفست‌های 0 و 8 یا مقادیر 5 تا 8 را در آفست‌های 32 تا 56 پشته می‌ریزند، مقدار %al را برای فراخوانی توابع متغیر صفر می‌کنند، و آدرس بافر خروجی را در rdi، rcx یا x8 قرار می‌دهند.
  • تمام 489 مورد چک با قواعد استاندارد همخوانی کامل دارند.
  • شش ورودی ساختار UNWIND_INFO دقیقاً با نتایج تحلیل پرولوگ تطابق داشته و توابع برگ فاقد ورودی هستند.
  • 24 ورودی ساختار FDE به طور کامل با آفست‌های ثبات‌های ذخیره‌شده و عمیق‌ترین نقطه‌ی پشته مطابقت دارند.
  • داده‌های ابزارها ثبت می‌کنند که radare2 مقادیر amd64 و ms را دارد، rizin برچسب amd64 را برای COFF زده است، ثبات rcx جا افتاده است، سه متغیر double تشخیص داده شده، و پروتوتایپ‌های ابزار angr ثبت شده‌اند.
  • داده‌های نمودار و جدول که درون ماژول وب‌سایت تعبیه شده‌اند، دقیقاً با خروجی آنالایزر یکسان هستند.

حالت‌های شکست در محیط واقعی

حالت شکست خطایی که ابزار ضعیف نشان می‌دهد حقیقتی که آنالایزر باید اثبات کند
پردازش باینری COFF با قواعد System V شناسایی rdi و rsi به عنوان آرگومان‌هایی که تابع اصلاً آن‌ها را نخوانده است هدر عدد 0x8664 دارد؛ شیوه‌ی فراخوانی Microsoft x64 است
خوانده شدن ثبات تنها در ساختار دستور lea گزارش هفت آرگومان برای تابعی که هشت آرگومان دارد ثبات‌های Base و Index نقش ثبات‌های خوانده‌شده را دارند
فرض عملوند مقصد در cvtsi2sd به عنوان خواندن ابداع یک آرگومان اعشاری سوم در ثبات xmm2 اولین عمل روی xmm2 نوشتن است؛ mixed تنها دو آرگومان SSE دارد
موضعی فرض کردن اسلات‌های پشته به جای توالی معرفی xmm0 به عنوان آرگومان نوع double در ویندوز اسلات شماره 1 ثبات XMM1 است و اسلات 3 ثبات XMM3
اشاره‌گر فریم فرض کردن دستور push rbp گزارش وجود Frame pointer در تابعی با فلگ -O2 که از rbp به عنوان انباره استفاده کرده فقط در صورتی که پس از push دستور mov %rsp, %rbp باشد فریم داریم
اندازه‌گیری Red zone نسبت به پشته در بدو ورود ادعای وجود 216 بایت «پایین‌تر از اشاره‌گر پشته» فضا 128 بایت زیر موقعیت فعلی پشته است، آن هم بعد از یک sub به اندازه‌ی 88 بایت
متغیر محلی در نظر گرفتن فضای Shadow store فرض کردن متغیرهای پشته در بازه‌ی 8 تا 32 آرگومان‌های پشته در استاندارد مایکروسافت از آفست 40 شروع می‌شوند
دستور lea را یک عمل بارگذاری داده دانستن ثبت آرگومان روی پشته در آفست 0x60(%rsp) در تابع variadic دستور lea صرفاً آدرس overflow_arg_area را محاسبه می‌کند؛ لودی ندارد
آرگومان عادی دانستن اشاره‌گر مخفی بازگشتی نمایش امضا به صورت big_ret(void *, long long) اشاره‌گر مبنای نوشتن است و در rax کپی می‌شود، یا در x8 می‌ماند
مقایسه‌ی عمیق‌ترین نقطه‌ی CFA با فریم پرولوگ عدم تطابق در تابعی که آرگومان‌های خروجی را در میانه push می‌کند باید آفست را با عمیق‌ترین نقطه‌ای که پشته در طول بدنه لمس می‌کند مقایسه کرد
حدس زدن شیوه‌ی فراخوانی صرفاً بر مبنای معماری پردازنده برچسب amd64 زدن به یک باینری با فرمت COFF ابتدا فرمت فایل کتابچه‌ی قواعد را معین می‌کند، سپس معماریْ ثبات‌ها را
اعتماد قطعی به امضای استخراج‌شده توسط یک ابزار ثبت سه آرگومان double یا شانزده پارامتر variadic امضای سورس اصلی یا تحلیل ثانویه‌ی Unwind باید آن را تصدیق کند

قاعده‌ی عملیاتی

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

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

خودآزمایی

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

  1. پیش از درستابع C یکسان double mixed(int i, double d, long long l, float f) برای لینوکس x86-64 و ویندوز x64 کامپایل شده است. کدام ثبات‌ها این چهار آرگومان را نگه می‌دارند؟
  2. حین درسدر سطح بهینه‌سازی -O2، بیلد Microsoft x64 تابع eight_args آرگومان پنجم خود را در 0x28(%rsp) بدون هیچ پرولوگی می‌خواند. مقدار 0x28 از کجا می‌آید؟
  3. حین درستابع keeps در سطح -O2 روی System V با pushq %rbp; pushq %r14; pushq %rbx شروع می‌شود و سپس بدون subq تابع ext را صدا می‌زند. چرا استک در محل فراخوانی هم‌تراز است؟
  4. حین درسپرولوگ keeps در AArch64 عبارت است از: stp x29, x30, [sp, #-0x30]!; str x21, [sp, #0x10]; mov x29, sp; stp x20, x19, [sp, #0x20]. رکورد فریم کجا قرار دارد؟
  5. حین درسدر سطح -O0 بیلد System V تابع eight_args شش ثبات آرگومان را بدون دستور subq در -0x8(%rbp) تا -0x30(%rbp) ذخیره می‌کند. چه چیزی این ذخیره‌سازی را مجاز می‌سازد؟
  6. پس از درستابع struct big big_ret(long long seed) یک ساختار 32 بایتی را برمی‌گرداند. هر ABI چگونه آن را برمی‌گرداند؟
  7. پس از درسابزار radare2 6.2.4 هفت آرگومان را برای eight_args در سطح -O2 روی System V لیست می‌کند: rdi، rsi، rdx، r8، r9 و دو اسلات استک. کدام آرگومان جا افتاده و چرا؟
  8. پس از درسمدخل .xdata برای keeps در فایل آبجکت ویندوز عبارت است از: 01 07 04 00 07 32 03 30 02 70 01 60. مفهوم آن چیست؟

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

چرا ابزارهای دیس‌اسمبلی مانند radare2 گاهی تعداد آرگومان‌های تابع را کمتر یا اشتباه تشخیص می‌دهند؟

این ابزارها گاهی ثبات‌هایی را که صرفاً در محاسبات آدرس‌دهی (مانند دستور lea) خوانده می‌شوند به حساب نمی‌آورند، یا مقاصد عملیات‌هایی مثل cvtsi2sd را به اشتباه یک خواندن داده‌ای در نظر می‌گیرند و آرگومان جدید ابداع می‌کنند.

تفاوت اصلی قرارداد ارسال آرگومان در System V AMD64 و Microsoft x64 چیست؟

در System V ثبات‌های اعداد صحیح و ممیز شناور از دو توالی مستقل تخصیص می‌یابند و 128 بایت Red Zone وجود دارد؛ اما در مایکروسافت x64 اسلات‌ها بر اساس موقعیت ترتیبی آرگومان به ثبات‌ها اختصاص داده شده، Red Zone وجود ندارد و 32 بایت فضای Shadow Store الزامی است.

داده‌های Unwind در پلتفرم‌های ویندوز و لینوکس چه نقشی در بررسی صحت پرولوگ دارند؟

کامپایلر اطلاعات فریم را برای مدیریت استثناها در سکشن‌های .pdata/.xdata (در ویندوز) و .eh_frame (در لینوکس) دو بار ثبت می‌کند؛ بنابراین تطبیق چیدمان فریم با این جداول، معیاری قطعی برای صحت تحلیل پرولوگ پشته است.

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

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