فازینگ هیبریدی (Hybrid Fuzzing) و اجرای نمادین پویا (Dynamic Symbolic Execution) به بخش حیاتی چرخه حیات توسعه نرمافزار امن تبدیل شدهاند. در حال حاضر، نسبت کدهایی که برای معماریهای ARM و RISC-V توسعه داده میشوند، به طور مداوم در حال افزایش است و وظیفه تحلیل مؤثر آنها را در اولویت قرار میدهد. این مقاله با توسعه روشهای اجرای نمادین پویا و فازینگ هیبریدی برای معماریهای مدرن RISC – «Baikal-M (ARM/AArch64) و RISC-V 64» میپردازد. رویکردهای توسعهیافته بر اساس مدلسازی معانی نمادین دستورالعملهای ماشین، در ابزار Sydr در چارچوب Sydr-Fuzz ادغام شدهاند و هدف آنها افزایش کارایی فازینگ هیبریدی است. نتایج کلیدی شامل الگوریتمهایی برای پردازش شاخههای غیرمستقیم با تعیین دقیق آدرسهای هدف و پشتیبانی از مجموعه دستورالعملهای صحیح RISC-V در چارچوب نمادین متنباز Triton است که پایه و اساس آمادهای را برای ایجاد ابزارهای تحلیل پویا در اختیار جامعه قرار میدهد.
کلمات کلیدی: تحلیل پویا؛ اجرای نمادین پویا؛ فازینگ؛ فازینگ هیبریدی؛ ابزار دقیق؛ تحلیل کد دودویی
1.مقدمه
نرمافزار بهطور عمیق در زندگی مدرن ادغام شده است؛ از زیرساختهای حیاتی گرفته تا ابزارهای شخصی. با این حال، این فراگیری با ریسک همراه است، زیرا در فرایند توسعه، بروز نواقص نرمافزاری بهصورت خطاها و آسیبپذیریها اجتنابناپذیر است. از این رو، چرخه حیات توسعه نرمافزار امن به عنوان یک استاندارد برای شناسایی و رفع این نواقص بهکار گرفته میشود؛ چرخهای که شامل انواع مختلفی از تحلیل برنامههاست [1]. بر این اساس، یکی از مسائل مهم و روز، توسعه ابزارهای خودکار و عمومی است که بتوانند بهطور مؤثر خطاها را در کد منبع شناسایی کنند.
ویژگی متمایز تحلیل پویا اجرای برنامه مورد بررسی است که به واسطه آن تعداد گزارشهای خطای کاذب کاهش مییابد. روش کلاسیک در این حوزه در حال حاضر، فازینگ با بازخورد مبتنی بر پوشش (فازینگ «جعبهخاکستری» یا Gray-Box) است [2–3]. در این نوع تحلیل، متریک پوششِ حاصل از اجرای مکرر برنامه با ورودیهای متنوع نقشی کلیدی ایفا میکند. تولید ورودیهای جدید از طریق اعمال جهشهای تصادفی بر دادههای ورودی اجراهای قبلی انجام میشود و این ورودیها بر اساس میزان افزایش پوشش اولویتبندی میشوند.
علاوه بر این، فازینگ «جعبهسفید یا White-Box» نیز وجود دارد [4] که با نام اجرای نمادین پویا شناخته میشود. برخلاف دریافت صرف متریک پوشش، این رویکرد اطلاعات دقیقتری از ساختار درونی برنامه استخراج میکند تا بر پایه محاسبات تحلیلی، ورودیهای جدید تولید شوند. مهمترین این اطلاعات، شرایط منطقی انشعابها هستند که به دادههای ورودی اولیه علامتگذاریشده وابستهاند. از این شرایط، یک مدل انتزاعی (نمادین) موسوم به پیششرط مسیر ساخته میشود که وابستگی جریان کنترل به ورودیها را منعکس میکند. ورودیهای جدید بهگونهای انتخاب میشوند که در اجرای بعدی، مسیر انشعاب تغییر کند.
هرچه در جریان تحلیل، اطلاعات بیشتری از محتوای کد استخراج شود، امکان شناسایی دقیقتر خطا فراهم میشود و در عین حال، ویژگیهای بیشتری از کد باید در هنگام طراحی تحلیلگر برای ابزارگذاری صحیح در نظر گرفته شود. این ویژگیها میتواند شامل زبان برنامهنویسی در ابزارگذاری کد منبع یا معماری پردازنده در تحلیل کد باینری باشد. اجرای نمادین در مقایسه با فازینگ معمولی به زمان بیشتری برای هر اجرا نیاز دارد. با وجود این، در صورت همکاری همزمان ابزارها و تبادل بهموقع ورودیها میان آنها، میتوان کارایی کلی تحلیل را افزایش داد [5]. این رویکرد با عنوان فازینگ هیبریدی شناخته میشود.
2. ایجاد مدل انتزاعی برنامه در اجرای نمادین پویا
در این پژوهش، کاربرد فازینگ هیبریدی برای تحلیل کد باینری معماریهای Baikal-M و RISC-V مورد بررسی قرار میگیرد. همانگونه که قابل مشاهده است، اجرای نمادین پویا رویکردی نسبتاً پایینسطح محسوب میشود. یکی از جنبههای مهم در این حوزه، قابلیت حمل (portability) است؛ به این معنا که امکان تحلیل برنامههایی که برای پلتفرمهای سختافزاری مختلف توسعه یافتهاند فراهم باشد. ابزارهای موجود از نظر نیاز به در دسترس بودن کد منبع و نیز از نظر انتخاب نوع نمایش (representation) که تبدیلات نمادین دادههای ورودی بر اساس آن تعریف میشوند، با یکدیگر تفاوت دارند. افزون بر این، شیوههای مختلفی برای بهرهگیری از اجرای مشخص برنامه در فرایند تحلیل وجود دارد.
در صورت در دسترس بودن کد منبع، امکان کامپایل برنامه به یک نمایش میانیِ عمومی وجود دارد؛ نمایشی که در آن، اجزای وابسته به معماری با استفاده از یک کامپایلرِ بهطور خاص اصلاحشده، از فرایند تحلیل کنار گذاشته میشوند. رویکردی مشابه را میتوان مستقیماً برای کد باینری نیز بهکار برد، هرچند این کار به دلیل از دست رفتن بخشی از اطلاعات در مرحلهٔ کامپایل، پیچیدهتر و پرزحمتتر است. با این حال، دسترسی به کد منبع برنامه همواره امکانپذیر نیست؛ برای مثال، هنگام استفاده از کتابخانههای پویای شخص ثالث.
علاوه بر این، یکپارچهسازی تحلیلگری که به کد منبع نیاز دارد، مستلزم اعمال تغییراتی یا مستقیماً در خود کد برنامه است یا در پشته (Stack) فناوری مورد استفاده، از جمله کامپایلر و سامانههای ساخت نرمافزار، که این امر محدودیتهایی را از نظر سهولت استفاده و عمومیت ابزار به همراه دارد. ازاینرو، در چارچوب اجرای نمادین پویا، الزام به وجود کد منبع امری زائد به نظر میرسد؛ بنابراین، در ادامه تمرکز اصلی بر روشهای ابزارگذاریای خواهد بود که در سطح کد باینری قابل اعمال هستند.
رایجترین رویکرد برای اجرای نمادین پویا، به اصطلاح اجرای نمادین عینی (concrete symbolic execution) است، که در آن اجرای مستقیم برنامه مورد مطالعه با اجرای نمادین ترکیب میشود تا یک مدل انتزاعی دقیقتر از اجرای برنامه به دست آید. اولین ابزاری که این رویکرد را برای کد دودویی پیادهسازی کرد، SAGE [4] بود، که در آن ابتدا اجرای عینی برنامه برای ثبت رد اجرا و سپس تبدیل آن به محدودیتهای نمادین انجام میشود.
در ابزارهای مدرن اجرای نمادین عینی، این دو مرحله بهوسیله ابزارگذاری پویای کد با یکدیگر ادغام شدهاند؛ بهطوریکه ابزارگذاری، زمینه جاری اجرای مشخص برنامه (از جمله دستورالعملهای در حال اجرا، مقادیر حافظه، ثباتها (Register) و پرچمها (Flag)) را برای اجرای نمادین فراهم میکند. برای مثال، ابزارهای QSYM [6] و Sydr [7] از چارچوبهای ابزارگذاری پویای باینری استفاده کرده و اجرای نمادین را در سطح دستورالعملهای منفرد کد باینری انجام میدهند. در این ابزارها، پشتیبانی از هر پلتفرم جدید مستلزم افزودن نمایش نمادین متناظر با مجموعه دستورالعملهای معماری مورد نظر است.
در سطح دستورالعملهای ماشین، اما بدون اجرای واقعی برنامه، اجرای نمادین در ابزار TritonDSE [8] انجام میشود؛ این ابزار با شبیهسازی کد باینری، اجرای گامبهگام آن را فراهم میسازد. در عین حال، اجرای واقعی میتواند برای اعتبارسنجی یا جمعآوری پوشش مورد استفاده قرار گیرد؛ مشابه آنچه در ماشین نمادین مجازی KLEE [9] دیده میشود که دستورالعملهای بایتکد LLVM را شبیهسازی میکند. مزیت ابزارگذاری در سطح نمایش میانی همچنین در SymQEMU [10] بهکار گرفته شده است؛ جایی که کد باینری پیش از اجرا با استفاده از QEMU به عملیات TCG ترجمه شده و سپس به کد اجرایی بازگردانده میشود.
ابزارگذاری نمادین در سطح این نمایش انجام میشود که امکان انتزاع ابزار تحلیل از جزئیات پیادهسازی پلتفرمهای سختافزاری مختلف را فراهم میسازد. این پژوهش در زمینه بهکارگیری تحلیل نمادین کد باینری معماریهای Baikal-M (ARM/AArch64) و RISC-V در چارچوب فازینگ هیبریدی، بر پایه مفسر نمادین پویای Sydr انجام شده است که در مؤسسه تحقیقات سیستمهای برنامهنویسی آکادمی علوم روسیه (ИСП РАН) توسعه یافته است. ابزار Sydr اجرای مشخص–نمادین کد باینری معماریهای x86/x86-64 را با استفاده از ابزارگذار پویای باینری DynamoRIO و کتابخانه اجرای نمادین Triton پیادهسازی میکند.
۳. اجرای نمادین کد باینری معماری BaikalM (ARM/AArch64)
در روش پیشنهادی برای ابزار Sydr، مدلسازی زمینه اجرای نمادین (symbolic execution context modeling) برای دستورالعملهای صحیح (Integer)، معماری ARM/AArch64 انجام میشود. پردازش عملیات ممیز شناور در جریان اجرای نمادین برنامه منجر به تولید معادلات نمادین نسبتاً پیچیدهای میگردد که حلکننده SMT (SMT solver) معمولاً قادر به یافتن پاسخ آنها در بازه زمانی تعیینشده نیست. از این رو، در روش ارائهشده برای معماری AArch64، اجرای نمادین بر پایه مؤلفههای زیر انجام میشود:
- مجموعه ثباتهای عمومی که شامل ثباتهای x0–x30 با اندازه ۶۴ بیت و زیرثباتهای متناظر w0–w30 با اندازه ۳۲ بیت است؛
- مجموعه ثباتهای ویژه که شامل ثبات اشارهگر دستور فعلی (PC)، ثبات اشارهگر پشته (SP یا stack pointer register) ، ثبات فلگها (flag register) و همچنین ثباتهای صفر xzr و wzr میباشد؛
- معناشناسی عملیاتی دستورالعملها با عملوندهای صحیح (Integer)؛
- مکانیزمهای محاسبه عبارات آدرسدهی؛
- مکانیزمهای انتقال کنترل و قراردادهای فراخوانی (Calling Conventions)
بهروزرسانی زمینه نمادین در طول پردازش جریان دستورالعملها که تحت کنترل DynamoRIO اجرا میشوند، انجام میگیرد. پس از دریافت دستورالعمل جاری همراه با مقادیر مشخص از ابزارگذار، فرآیند پردازش نمای نمادین، کد عملیاتی آن را دیاسمبل کرده و مجموعهای از بررسیها را برای تشخیص وجود عملوندهای نمادین و نیاز به بهروزرسانی پشته نمادین فراخوانیها انجام میدهد.
برای معماری AArch64، دستورالعملهای فراخوانی تابع عبارتند از BL و BLR، و برای بازگشت از تابع از دستور RET استفاده میشود؛ یعنی دستور BR x30 که کنترل را به دستوری منتقل میکند که آدرس آن در ثبات x30 (ثبات لینک رویه – Procedure Link Register) نگهداری میشود. ردیابی این دستورالعملها برای بهروز نگهداشتن مدل نمادین حافظه پشته ضروری است. در غیر این صورت، در عملیات بعدی خواندن یا ذخیره دادههای نمادین در حافظه پشته، مفسر با ناسازگاری مواجه شده و مقدار واقعی دادهها را جایگزین میکند؛ به این معنا که برچسب نمادین از دست خواهد رفت.
برای معماری ARM/AArch64، دستورالعملهای دسترسی به حافظه (Load/Store) به یک گروه جداگانه اختصاص داده میشوند. اندازه بخش حافظهای که هر دستورالعمل میتواند به آن دسترسی داشته باشد توسط معناشناسی مشخص میشود و میتواند ۱، ۲، ۴ یا ۸ بایت باشد.
در پیادهسازی روش پیشنهادی، در رابط انتزاعی MemoryAccess کتابخانهٔ Triton، خطایی که مقدار حافظه را با اندازهای برابر با اندازهٔ ثبات برای نگهداری موقت آن (۴ یا ۸ بایت) اختصاص میداد، اصلاح شد.
علاوه بر این، در مکانیزم آدرسدهی با اندیسگذاری در ARM/AArch64، فرم غیرمعمولی برای اعمال ضریب مقیاسدهی به ثبات اندیس وجود دارد. معمولاً این ضریب یک عدد ثابت است، برای مثال ۸ در عبارت: « Addr = Reg1 + Reg2 * 8». در معماری AArch64، به جای این ضریب ثابت، امکان استفاده از عملیات شیفت به چپ بیتها (LSL) فراهم شده است که میتواند با عملیات گسترش ثبات اندیس ترکیب شود (به شکل ۱ مراجعه شود).
عملیات شیفت، در واقع معادل ضرب در توانهای ۲ از ۰ تا ۳ است و امکان آدرسدهی بلوکهای حافظه با اندازه ۱ تا ۸ بایت را فراهم میکند. این عملیات همچنین میتواند در آدرسدهی پرشهای غیرمستقیم جدولمحور [11] نقش داشته باشد، از جمله در زمینه یک عملیات حسابی (مانند دستور جمع در آخرین خط شکل ۱) که پس از بارگذاری مقدار از حافظه انجام میشود.
روش پیشنهادی چنین تبدیلات حسابی بین دستورالعمل خواندن از حافظه و انتقال کنترل را با استفاده از الگوریتم ردیابی ثباتها (Slicing) شناسایی میکند و بر اساس آن، اصلاح عبارت آدرسدهی با در نظر گرفتن امکان شیفت بیتها انجام میگیرد. خود عبارت آدرسدهی در پرشهای با چند مسیر احتمالی، بعداً در پیششرط مسیر (Path Predicate) اضافه میشود، زمانی که دستورالعمل مرتبط با انتقال کنترل ظاهر گردد.
علائم و اختصارات در تصویر بالا:
=: – عملیات انتساب مقدار
ext – عملیات گسترش اندازه
Mem[addr] – مقدار حافظه در آدرس addr
در هنگام ساخت پیششرطهای نمادین ایمنی [12] که امکان وقوع خطای سرریز عدد صحیح (Integer Overflow) را بررسی میکنند، هم سرریزهای علامتدار و هم بدون علامت برای دستورالعملهای جمع، تفریق، ضرب و همچنین شیفتهای بیت در نظر گرفته میشوند. برای این منظور، ابتدا عبارت AST دستورالعمل باز میشود و زیرعبارات مربوط به عملوندهای دستور استخراج میگردد.
برای مثال، عبارت دستورالعمل تفریق با بیت Carry شامل دو عملیات جمع بر روی بردارهای بیتی است، که در آن مقدار عملوند مبدأ به صورت منفی شده و به شکل یک گره اضافی در درخت نمایش داده میشود. از آنجا که دستورالعمل عملاً شامل دو عملیات حسابی است که امکان سرریز را دارند، دو پیششرط ایمنی (safety predicates) ساخته خواهد شد.
۳.۱ روش شناسایی انتقالهای (پرش) غیرمستقیم در کد باینری
شاخههای با چندین مسیر ممکن (به شکل switch) مستلزم وجود چند گزینه برای مقصد انتقال کنترل (control transfer) هستند. هنگام استفاده از بهینهسازیهای کامپایلر، چنین پرشهایی معمولاً با سازماندهی گزینههای مقصد در قالب جدول بلوکهای پایه متوالی در کد اجرایی انجام میشوند که هر یک متناظر با یک مسیر شاخه است. آدرسهای مقصد در این جدول بهصورت متوالی در حافظه ذخیره میشوند و انتخاب سلول مناسب حافظه از طریق افزودن آفست (offset) به آدرس پایه جدول اشارهگرهای کد اجرایی انجام میشود.
در شکل ۲، بلوک پایه که در آن خواندن آدرس مقصد پرش از حافظه و انتقال کنترل صورت میگیرد، با رنگ روشن مشخص شده است. در این مثال، انتقال کنترل با فراخوانی تابع و از طریق دستور BLR x8 انجام میشود. همانطور که مشخص است، برای محاسبه آدرس پایه جدول، از سه مقدار ثابت مختلف (0x420000، 0x28 و 0x180) و همچنین مقدار ثبات x8 استفاده میشود. این مقدار پیش از ورود به بلوک پایه از طریق دستور LDRB از آدرس 0x4006c0 در حافظه خوانده شده است.
با توجه به اینکه هشت ثبات اول x0–x7 در AArch64 برای نگهداری آرگومانها مطابق قرارداد فراخوانی استفاده میشوند، با نگاه به دستورالعمل قبلی میتوان حدس زد که ثبات x8 دارای یک مقدار احتمالی نمادین است که اشارهگری به آدرسی است که به عنوان آرگومان به تابع main ارسال میشود. از این رو، بسته به مقدار این آرگومان، یکی از توابع func0–func5 فراخوانی خواهد شد.
شناسایی چنین جدولهایی در روش پیشنهادی بر اساس اکتشافات (heuristic) انجام میشود، که شامل حضور مؤلفههای زیر در بلوک پایه است:
- آخرین دستورالعمل استفاده شده، یک دستورالعمل انتقال غیرشرطی (unconditional) بر اساس مقدار ثبات است.
- ثبات مربوطه دارای مقداری است که از حافظه خوانده شده یا مشتق شده از آن مقدار است؛
- بلوک پایه شامل یکی از دستورالعملها برای بارگذاری یک مقدار آدرس ثابت ADR/ADRP است.
این ساختار میتواند پیچیدهتر شود اگر به جای آدرس مقصد پرش، نیاز باشد که آفست نسبت به ابتدای جدول در کد اجرایی از حافظه خوانده شود. بنابراین، آدرس در جدول بلوکهای پایه اجرایی نیز به دو بخش تقسیم میشود: آدرس پایه ابتدای جدول و آفست.
بهنوبه خود، نگهداری مقادیر آفستها مشابه با جدول جداگانهای در حافظه انجام میشود، که عملیات خواندن آن با دستور load و بر اساس آدرس پایه قابل اندیسگذاری صورت میگیرد. اندازه بلوکهای حافظه برای ذخیره آفستها بین ۱ تا ۸ بایت است.
از آنجایی که محدودیت اندازه مقدار آفست برای کدگذاری شماره دستورالعمل یا رکورد متناظر با بلوک پایه وجود دارد، هنگام افزودن مقدار آفست به عبارت آدرسدهی، این مقدار در اندازه مناسب ضرب میشود.
همانطور که پیشتر ذکر شد، در معماری AArch64 برای این محاسبات، از مکانیزم داخلی دستورالعملهای حسابی به شکل عملیات شیفت بیت بر روی مقدار ثابت استفاده میشود.
در شکل ۳، یک بلوک پایه نمایش داده شده است که انتقال کنترل بر اساس محاسبه آدرس مقصد با استفاده از آفست خوانده شده از جدول آفستها انجام میشود. دو دستورالعمل اول شامل ثابتهایی برای تعیین آدرس پایه جدول آفستها هستند و دستورالعمل سوم آدرس آغاز جدول در کد منبع را بارگذاری میکند.
به دلیل اینکه وجود جدول آفستها موجب میشود دو آدرس پایه به جای یک آدرس وجود داشته باشد، میتوان این نوع سازماندهی جدول را از حالت قبلی (شکل ۲) تشخیص داد، که این تمایز از طریق دستورالعمل دوم بارگذاری آدرس قابل مشاهده است.
برای ساخت یک گزاره مسیر (path predicate) که بتواند تمام گزینه های موجود را در نظر بگیرد، ضروری است نه تنها روشی برای تشخیص چنین ساختارهایی، بلکه برای تعیین اندازه جدول نیز پیاده سازی شود. روش پیشنهادی برای تعیین مرزهای جدول پرش مبتنی بر جستجوی دستورالعمل مقایسه CMP در بلوک پایه منطقی قبلی است. برای این منظور، از مکانیسمی برای ذخیره بلوک های پایه اجرا شده استفاده میشود. با بازیابی یک مقدار ثابت برای دستورالعمل مقایسه، سلولهای هدف حافظه خواندن تعیین میشوند. برای محتویات آنها، آدرس های پرشهای ممکن محاسبه و تأیید میشوند که متعلق به نواحی کد اجرایی هستند..
4. اجرای نمادین کد دودویی معماری RISC-V
معماری پردازنده باز RISC-V اخیرا ً پدیدار شده است. ویژگی های این استاندارد معماری، مجوز رایگان، ماژولار بودن و مجموعه دستورالعمل های مینیمالیستی است که آن را برای طیف وسیعی از برنامه ها قابل اجرا میکند.
4.1 طراحی معناشناسی نمادین برای دستورالعملهای عدد صحیح RISC-V
روش پیشنهادی بر اساس چارچوب Triton [13] پیاده سازی شده است. این روش قادر به ساخت عبارات نمادینAST بر حسب بردارهای بیتی است که منعکسکننده معانی عملیاتی دستورالعملهای RISC-V هستند که اجرای نمادین برای آنها انجام میشود. روشهای تابعی مربوطه و عبارات نمادین تولید شده توسط آنها نیز در این مقاله به عنوان “معناشناسی نمادین (symbolic semantics) ” نامیده میشوند. مجموعه دستورالعمل های معماریRISC-V دارای ساختار ماژولار از پسوندهای قابل اتصال است که با حروف لاتین نشان داده میشوند.
پسوند I پایه شامل دستورالعمل هایی برای انتقال کنترل، دسترسی به حافظه و عملیات حسابی به جز دستورالعمل های ضرب و تقسیم متعلق به پسوند M است. مجموعه پایه همچنین شامل دستورالعمل هایی برای سازماندهی وقفه ها و موانع حافظه (دستورالعمل های حصار) و همچنین دستورالعمل هایی برای دسترسی به ثبات سرویس CSR است، اما این گروه های دستورالعمل در روش پیشنهادی گنجانده نشدهاند. بخش قابل توجهی از مجموعه دستورالعمل های ۶۴ بیتی که توسط این روش اجرای میشوند، با نسخه ۳۲ بیتی معماری همپوشانی دارند و تنها در اندازه عملوندهای رجیستر و آدرس های مورد استفاده متفاوت هستند.
به همین دلیل، این روش هر دو مجموعه توسعه ماژولار RV64IMC و RV32IMC را پوشش میدهد، که در آن حرف C نشان دهنده مجموعهای از دستورالعملهای فشرده است. برای نمایش معماری انتخاب شده با استفاده از زیرساخت رابط انتزاعی Triton، مشخصات مربوطه اضافه شد که امکان تعامل با دیساسمبلر (disassembler) خارجی Capstone و مدلسازی وضعیت نمادین برنامه را فراهم میکند. پس از ایجاد مشخصات و شناسه ها، روش پیشنهادی روشهای رابطهای riscv64Cpu و riscv32Cpu را پیاده سازی کرد که اجزای وابسته به معماری را بر اساس کلاس های جهانی انتزاعی به عنوان مثال، triton::arch::Register مدیریت میکنند.
این رابطها امکان بازیابی و به روزرسانی عبارات نمادین و مقادیر خاص ثباتها و نواحی حافظه، دسترسی به ثبات های سرویس (sp،pc) را فراهم میکنند و همچنین شامل توابعی برای تبدیل زمینه دستورالعمل های جدا شده از طریق Capstone به کلاس انتزاعی Triton به نام triton::arch::Instruction هستند. در مرحله ساخت معناشناسی نمادین، روش عملکردی مربوطه با استفاده از شناسه دستورالعمل انتخاب میشود. ابتدا، عملوندهای دستورالعمل را استخراج کرده و عبارات بردار بیتی زمینه اجرای نمادین را برای آنها به دست میآورد. چنین عبارتی میتواند یا یک درخت انتزاعی از پیش ساخته شده یا یک بردار بیتی مربوط به مقدار یک ثابت برای اندازه عملوند معین باشد.
در مرحله بعد، برای عبارات به دست آمده، مطابق با معناشناسی عملیاتی دستورالعمل، یک عبارت جدید ساخته میشود که شامل آنها به عنوان زیردرخت است. عبارت ساخته شده با عملوند هدف مرتبط است، پس از آن مقدار نمادین شمارنده دستورالعمل افزایش مییابد، مگر اینکه معناشناسی دستورالعمل طور دیگری ارائه کرده باشد. روش پیشنهادی برای معماریRISC-V، تشکیل عبارات معنایی بر حسب بردارهای بیتی برای۶۲ دستورالعمل با اندازه استاندارد،۱۹تغییر اضافی آنها به شکل شبه دستورالعملها و ۳۳ دستورالعمل کوتاه شده را فراهم میکند.
شبهدستورالعملها، خوانشهای جایگزین از دستورالعملهای استاندارد با استفاده از ترکیبهای خاصی از عملوندها هستند. اغلب اوقات، ثبات x0 که همیشه صفر است، به عنوان چنین عملوندی عمل میکند. ثبات x1 و عملوندهای ثابت نیز استفاده میشوند. یک دستورالعمل میتواند بسته به انتخاب عملوندها، چندین نوع مختلف در قالب شبهدستورالعمل داشته باشد. به عنوان مثال، دستورالعمل انتقال کنترل JALR rd،rs،offset دارای سه نوع شبه دستورالعمل است که برای هر کدام مقدار آفست عملوند عددی برابر با صفر است، در حالی که rd لزوما مقدارx0 یاx1 را میگیرد. ترکیب JALR x0, x1, 0 منجر به یک دستورالعمل بازگشتی از یک فراخوانی RET میشود.
با توجه به ویژگی های Capstone، فیلدهایی که از پیش معنای عملوندها را تعیین میکنند، در طول جداسازی پر نمیشوند. به عنوان مثال، کلاس انتزاعی که نشان دهنده شبه دستورالعمل RET است، هیچ عملوندی ندارد. از منظر ساخت معانی نمادین، این بدان معناست که قبل از به دست آوردن عبارات بردار بیتی، لازم است مشخص شود که آیا دستورالعمل اجرای شده یک دستورالعمل استاندارد است یا یک شبه دستورالعمل. برای این منظور، روش پیشنهادی از تجزیه بر اساس تعداد عملوندها و شرح متنی دستورالعمل استفاده میکند.
دستورالعمل های خلاصه شده مجموعه ای از عملیات اساسی را به شکل انتقال کنترل، دسترسی به حافظه و عملیات حسابی ساده تشکیل میدهند. تفاوت اصلی آنها این است که به جای ۳۲ بیت،۱۶بیت برای رمزگذاری آنها اختصاص داده شده است که باید هنگام به روزرسانی مقدار نمادین شمارنده دستورالعمل در نظر گرفته شود.
علاوه بر این، به دلیل عدم ترجمه صحیح برای دستورالعملهای دسترسی به حافظه در طول توسعه روش پیشنهادی، ایجاد عملوند کلاس MemoryAccess برای چنین دستورالعملهایی در روش تابعی مربوطه انجام میشود تا با استفاده از عملوندهای ثبات، معانی نمادین ایجاد شود.
۴.۲ اجرای نمادین پویا از کد دودویی معماری RISC-V 64
روش توسعه یافته اجرای نمادین پویا مبتنی بر روش ساخت معانی نمادین دستورالعمل های RISC-V 64 است که در بخش قبلی توضیح داده شد. برای مدلسازی بافت نمادین معماری RISC-V 64 با استفاده از روش پیشنهادی برای کد دودویی، از اجزای معماری زیر استفاده شده است:
- مجموعهای از ثباتهای عمومی ۶۴ بیتی x0-x31، شامل ثباتهای x1 (رجیستر بازگشتی) و x2 (sp) برای اهداف خدماتی؛
- اشارهگر شمارنده دستورالعمل (pc یا pointer counter)؛
- معانی عملیاتی دستورالعملها با عملوندهای صحیح؛
- مکانیسمهای انتقال کنترل، قراردادهای فراخوانی و حالتهای آدرسدهی.
برخالف AArch64، دستورالعمل های معماری RISC-V دارای ویژگی مختصر بودن عملیات انجام شده هستند. در عین حال، شباهت را میتوان در تمایز بین دستورالعمل های دسترسی به حافظه و عملیات حسابی مشاهده کرد. RISC-V از ثبات های فلگ (Flag) استفاده نمیکند. با این حال، پرش های شرطی همچنان میتوانند برای مجموعهای از دستورالعملهای انتقال کنترل مربوطه پس از مقایسه عملوندهای آنها اجرا شوند. علاوه بر این، برای مدلسازی صحیح جریان کنترل و پردازش صحیح حافظه نمادین پشته، تشخیص شبه دستورالعمل ها برای دستورالعملهای JAL و JALR ضروری است.
مهمترین تفاوت برای شاخههای غیرمستقیم به دلیل وجود محدودیت فنی در عملکرد ابزار ابزار دقیق DynamoRIO برای معماری RISC-V مشاهده میشود. برای AArch64، مشکل تشخیص پرشهای غیرمستقیم جدول در سطح تحلیل بلوک پایه حل میشود، که قبل از پردازش دستورالعمل های موجود در آن، به طور کامل توسط ابزار دقیق بارگذاری میشود. با این حال، در طول کار بر روی با استفاده از روش اجرای کد دودویی RISC-V 64، مشخص شد که وقتی یک بلوک پایه بزرگتر از 5 دستورالعمل ظاهر میشود، ابزار دقیق آن را به چندین بلوک تقسیم میکند.
به دلیل ماهیت مینیمالیستی مجموعه دستورالعمل های فشرده این معماری، بلوک های پایه به طور متوسطب زرگتر از، به عنوان مثال،AArch64 و به ویژه x86 هستند. بنابراین، بلوک های پایه حاوی پرشهای جدول ضمنی را میتوان به دو یا سه قسمت تقسیم کرد که هر کدام بیش از پنج دستورالعمل ندارند. گزاره مربوط به معکوس کردن پرشهای جدول در حین پردازش دستورالعمل بارگذاری داده (load) ایجاد میشود، اما در حین اجرای دستورالعمل انتقال کنترل به گزاره مسیر کلی اضافه میشود. اگر این دو دستورالعمل مرتبط در حین تقسیم در بلوک های مختلفی قرار گیرند، آدرس پرش از قبل ناشناخته است. بنابراین، به جای آن یک آدرس ساختگی نوشته میشود – یعنی آدرس دستورالعمل بارگذاری داده هدف، که یک واحد افزایش مییابد. این آدرس فرد است، بنابراین به دلیل همترازی نمیتواند با هیچ آدرس دستورالعمل دیگری که نیازی به اضافه کردن گزاره ندارد، حتی اگر یک دستورالعمل خالصه شده باشد، مطابقت داشته باشد. یک دستورالعمل پرش از بخش دیگری از بلوک پایه میتواند از آن برای تعیین گزاره مورد نیاز استفاده کند.
توضیح مفصلی در مورد نحوه کار جداول پرش، شامل خواندن آدرسهای هدف کد منبع و آفستهای نسبی از حافظه، در بخش مشابهی برای AArch64 ارائه شده است. برای معماری RISC-V 64، وجود یک جدول پرش در بلوک پایه با سه جزء زیر نشان داده شده است. الگویی از سه دستورالعمل، که میتوانند در ترتیبهای مختلف قرار گیرند:
- auipc – یک عملیات بارگذاری مقدار آدرس، یک عملیات جابجایی بیت (به عنوان مثال slli) و یکی از دستورالعملهای جمع؛
- یک دستورالعمل خواندن حافظه، که بعد از هر سه دستورالعمل تشکیل دهنده الگو قرار دارد؛
- یک دستورالعمل انتقال کنترل برای یک مقدار ثبات
در شکل های 4a-4c انواع بلوک های پایه حاوی شاخههای جدول را نشان میدهند که بر اساس اجزای مورد نیاز، مشمول پارتیشن های مختلفی خواهند بود. بر اساس مشاهدات اکتشافی، میتوان نتیجه گرفت که سه دستورالعملی که الگو را تشکیل میدهند، در بلوک اول قرار میگیرند که ممکن است شامل دستورالعمل هدف برای خواندن از حافظه نیز باشد. با توجه به امکان تقسیم بلوک پایه به سه بخش، که هر کدام فقط شامل یکی از اجزای مشخص کننده وجود یک انشعاب غیرمستقیم هستند، وضعیت مرحله جستجوی انشعاب جدول ممکن است هنگام ظاهر شدن یک بلوک جدید از دستورالعملها متفاوت باشد.
یک ناحیه حافظه اختصاص داده شده ویژه برای ذخیرهسازی آن استفاده میشود که با ظاهر شدن اجزا، بررسی و پر میشود. در طول اسکن اولیه، ابتدا غیرشرطی بودن آخرین دستورالعمل بلوک تأیید میشود. سپس، از انتها، جستجو برای آخرین دستورالعمل بارگذاری داده در بلوک انجام میشود. هنگامی که این دستورالعمل ظاهر میشود، از وضعیت ذخیرهسازی مربوط به وجود الگو در بلوک قبلی استفاده میشود. برای اسکن اولیه، که در آن بلوک بازرسی شده فعلی در واقع شروع بلوک پایه است، وضعیت وجود الگو بدیهی است که نمیتواند وجود داشته باشد. بنابراین، جستجوی بیشتر برای ترکیبی از سه دستورالعمل مورد نیاز است، که حتی اگر هیچ دستورالعمل خواندنی در بلوک فعلی وجود نداشته باشد، رخ میدهد. وقتی این الگو شناسایی میشود، مکانآخرین دستورالعمل موجود در آن برای مقایسه احتمالی با آدرس دستورالعمل خواندن یافت شده ذخیره میشود. این کار برای حذف مواردی که یک دستورالعمل دسترسی به حافظه خارج از هدف در همان بلوک پارتیشن بندی وجود دارد، مانند شکل4a و شکل4c، ضروری است.
اگر هر دو مؤلفه باموفقیت پیدا شوند، اما آخرین دستورالعمل بر اساس مقدار رجیستر کنترل را منتقل نمیکند (و بر این اساس، اصلا دستورالعمل انتقال کنترل نیست)، آنگاه وضعیت مربوطه، همراه آدرس ساختگی هدف دستورالعمل خواندن در حافظه ذخیره میشود.
هنگام اسکن قسمت بعدی بلوک پایه، دسترسی به حافظه که قبل از جستجوی دستورالعمل خواندن رخ میدهد، آدرس اضافه کردن گزاره را برمیگرداند. اگر در طول اسکن اولیه فقط یک الگو پیدا شده باشد، این اطلاعات نیز ذخیره میشوند. برای تقسیم به سه بخش، وضعیت ذخیره سازی دو بار به روزرسانی میشود. هنگامی که آدرس ساختگی مورد نظر توسط هدف دستورالعمل انتقال کنترل دریافت میشود، گزاره با آدرس صحیح در اجرا کننده نمادین دوباره ایجاد میشود.
اندازه جدول پرش برای RISC-V 64 با انتخاب بزرگترین آرگومان مقایسه شده از دستورالعمل اجرای شرطی که جریان کنترل را به بلوک حاوی الگو هدایت کرده است، تعیین میشود. اهداف انتقال کنترل احتمالی شناسایی شده نیز به عنوان کد اجرایی تأیید میشوند.
۵. ارزیابی تجربی روشهای پیشنهادی اجرای نمادین پویا
۵.۱ Baikal-M (ARM/AArch64)
برای آزمایش روش اجرای نمادین پویای پیشنهادی، مجموعههای آزمایشی بر اساس معیارهای مصنوعی و برنامههای کاربردی Baikal-M (AArch64) در دنیای واقعی توسعه داده شدند. این روش با استفاده از ابزار Sydr پیادهسازی شد. در میان آنالوگهای متنباز مدرن که از معماری ARM/AArch64 پشتیبانی میکنند، مفسر پویای SymQEMU از نظر اصل عملکرد و مجموعه تکنیکهای تحلیل نمادین پشتیبانیشده، نزدیکترین به این معماری در نظر گرفته میشود. برای ارزیابی روش توسعهیافته، مقایسه تجربی این دو مفسر نمادین انجام شد.
این مقایسه با استفاده از مجموعهای از برنامههای متنباز که روی پردازنده Baikal-M اجرا میشوند، انجام شد. برای هر برنامه محدودیت زمانی 20 دقیقهای در نظر گرفته شد. نتایج مقایسه در جدول 1 ارائه شده است.
نتایج هر ابزار شامل تعداد تستهای تولید شده با موفقیت (که به عنوان “فایلهای ورودی” تعیین شدهاند) و همچنین تعداد خطوط پوشش منحصر به فرد به دست آمده برای کل مجموعه تست (ستون “منحصر به فرد”) به دست آمده از هر دو ابزار است. درصد معیار پوشش برای دادههای تست یک ابزار جداگانه نسبت به کل پوشش به دست آمده محاسبه میشود. همانطور که مشاهده میشود، برای اکثریت قریب به اتفاق پروژهها (به استثنای libxml2)، روش اجرای نمادین پیشنهادی بهترین نتایج را نشان داده است.
این روش همچنین با موفقیت اثربخشی خود را در زمینه آزمایش فازینگ هیبریدی نشان داده است. نتایج به صورت نمودارهای معیار پوشش در شکل 5 ارائه شدهاند. این مقایسه با استفاده از اجرای جفتی مفسر نمادین پویای Sydr و یک ابزار فازینگ با بازخورد پوشش (که در نمودارها با رنگ سبز مشخص شده است) انجام شد. این جفت با یک فازینگ یکسان که در دو رشته (با رنگ آبی مشخص شده است) اجرا میشد، رقابت کردند.
مقایسه تجربی برای فازینگ هیبریدی با استفاده از چارچوب ارزیابی ابزار فازینگ FuzzBench انجام شد. محدودیت زمانی برای آزمایش فازینگ 23 ساعت بود. بر اساس نتایج دو ابزار فازینگ مختلف، libFuzzer و ++AFL (نمودارهایی که در شکل به ترتیب به ستونهای چپ و راست اشاره دارند)، استفاده از فازینگ هیبریدی به جای مقیاسبندی منجر به افزایش معیار پوشش به دست آمده برای بیش از نیمی از برنامههای مورد تجزیه و تحلیل شد. چندین اشکال شناسایی و رفع شده در رابطهای چارچوب نمادین Triton برای معماری ARM/AArch64 نیز به عنوان عواملی در ارزیابی عملکرد روش پیشنهادی در نظر گرفته شدند.
۵.۲ RISC-V 64
برای ارزیابی عملکرد روش اجرای نمادین پویای پیشنهادی که در ابزار Sydr پیادهسازی شده است، معیارهای ترکیبی و یک مجموعه آزمایشی بر اساس برنامههای واقعی RISC-V 64 ایجاد شد. برای این روش، مقایسه تجربی Sydr با مفسر پویای SymQEMU نیز صورت پذیرفت.
نتایج مقایسه در جدول ۲ ارائه شده است. این مقایسه با استفاده از مجموعهای از برنامههای متنباز در حال اجرا در یک ماشین مجازی انجام شد، بنابراین محدودیت زمانی برای هر برنامه به ۶۰ دقیقه افزایش یافت. روش مقایسه و نمادهای مربوط به خطوط پوشش منحصر به فرد و درصد پوشش برای یک ابزار منفرد نسبت به کل خطوط به دست آمده، همانند بخش قبلی است. جدول ۲ نشان میدهد که Sydr برای دو سوم برنامههای تحلیل شده، پوشش بهتری را به دست میآورد، در حالی که در موارد باقی مانده، نتایج برابری را برای این معیار نشان میدهد.
اجرای مقایسهای برای فازینگ هیبریدی با استفاده از چارچوب FuzzBench به دلیل استفاده از ماشین مجازی انجام نشد. برای این معماری، یک مقایسه دستی بین فازینگ هیبریدی جفت Sydr و libFuzzer و همچنین دو فرآیند موازی libFuzzer انجام شد. جدول 3 میانگین مقادیر پوشش خط به دست آمده برای هر پروژه را در پنج اجرا که هر کدام به مدت 24 ساعت اجرا شدهاند، نشان میدهد. همانطور که مشاهده میشود، فازینگ هیبریدی برای سه پروژه از چهار پروژه به معیارهای پوشش بالاتری دست یافته است.
6. نتیجه گیری
در این مقاله، روشهای توسعه یافته برای اجرای نمادین پویای کدهای باینری Baikal-M (AArch64) و RISCV64 ارائه دادیم. این روشها مبتنی بر ساخت عبارات نمادین برای معانی عملیاتی دستورالعملهای ماشین هستند و در زمینه فازینگ هیبریدی قابل اجرا میباشند. این روشها در ابزار Sydr پیادهسازی شدهاند که بخشی از چارچوب تحلیل پویای Sydr-Fuzz است [14]. این روشها امکان تشخیص پرشهای غیرمستقیم جدول و تعیین آدرسهای هدف مربوطه در کد اجرایی را فراهم میکنند. علاوه بر این، روشی برای اجرای نمادین مجموعه دستورالعملهای صحیح معماری RISC-V، شامل دستورالعملهای کوتاهشده و شبهدستورالعملها، توسعه داده شد. این روش در چارچوب اجرای نمادین متنباز Triton پیادهسازی شده است و میتواند توسط جامعه توسعهدهندگان برای ایجاد ابزارهای جدید تحلیل پویا مورد استفاده قرار گیرد.
7. منابع
[1]. ГОСТ Р 58412-2019: Защита информации. Разработка безопасного программного обеспечения. Угрозы безопасности информации при разработке программного обеспечения. –– Национальный стандарт РФ, 2019.
[2]. Serebryany, K. Continuous Fuzzing with libFuzzer and AddressSanitizer [Текст] / Kosta Serebryany // 2016 IEEE Cybersecurity Development (SecDev) / IEEE. 2016, с. 157.
[3]. Fioraldi, A. AFL++: Combining Incremental Steps of Fuzzing Research [Текст] / A. Fioraldi, D. Maier, H. Eißfeldt, M. Heuse // 14th USENIX Workshop on Offensive Technologies (WOOT 20). 2020, с. 10.
[4]. Molnar, D. Automated whitebox fuzz testing [Текст] / D. Molnar, P. Godefroid, M. Levin // Network and Distributed System Security Symposium, NDSS. 2008, с. 416-426.
[5]. FuzzBench (Google). DSE+Fuzzing Experiment Report. 2021.[Электронный ресурс]. –– URL:https://www.fuzzbench.com/reports/experimental/2021-07-03-symbolic/index.html (доступ 23.09.2025).
[6]. Yun I. QSYM: A practical concolic execution engine tailored for hybrid fuzzing [Текст] / I. Yun [и др.]// 27th USENIX Security Symposium (USENIX Security 18). 2018, с. 745-761.
[7]. Vishnyakov, A. Sydr: Cutting edge dynamic symbolic execution [Текст] / A. Vishnyakov [и др.] // 2020 Ivannikov ISPRAS Open Conference (ISPRAS). IEEE. 2020, с. 46-54.
[8]. David, R. From source code to crash test-cases through software testing automation [Текст] / Robin David, Jonathan Salwan, Justin Bourroux // CESAR 2021: Automation in Cybersecurity. 2021.
[9]. Cadar C. Klee: unassisted and automatic generation of high-coverage tests for complex systems programs. [Текст] / C. Cadar, D. Dunbar, D. R. Engler [и др.] // OSDI. Т. 8. 2008, с. 209-224.
[10]. Poeplau, S. SymQEMU: Compilation-based symbolic execution for binaries. [Текст] / S. Poeplau, A. Francillon // NDSS. 2021.
[11]. Kutz D.Towards Symbolic Pointers Reasoning in Dynamic Symbolic Execution [Текст] / D. Kuts // 2021 Ivannikov Memorial Workshop (IVMEM). IEEE. 2021, с. 42-49.
[12]. Vishnyakov A. Symbolic Security Predicates: Hunt Program Weaknesses [Текст] / A. Vishnyakov [и др.]// 2021 Ivannikov Ispras Open Conference (ISPRAS). IEEE. 2021, с. 76-85.
[13]. Saudel, F. Triton: A Dynamic Symbolic Execution Framework [Текст] / Florent Saudel, Jonathan Salwan// Symposium sur la s ́ ecurit ́ e des technologies de l’information et des communications. SSTIC. 2015, с. 31-54.
[14]. Vishnyakov, A. Sydr-Fuzz: Continuous Hybrid Fuzzing and Dynamic Analysis for Security Development Lifecycle [Текст]/ A. Vishnyakov, D. Kuts, V. Logunova, D. Parygina, E. Kobrin, G. Savidov, A. Fedotov// 2022 Ivannikov ISPRAS Open Conference (ISPRAS). IEEE, 2022, с. 111-123.