وقتی یک فایل باینری را به ابزارهای متداول مهندسی معکوس مثل Ghidra، radare2 یا angr میدهید، ممکن است با خروجیهای متناقضی روبهرو شوید؛ یکی آدرس تابع را متفاوت اعلام میکند، دیگری تعداد بلوکهای پایهای را کمتر نشان میدهد و سومی در مواجهه با یک جدول پرش ساختار کد را گم میکند. این تفاوتها اغلب ناشی از اشتباه ابزارها نیست، بلکه به تفاوت در معماری لودر، فرضیات دیساسمبل و مراحل تحلیل جریان کنترل برمیگردد.
درک معماری داخلی دیکامپایلرها و پایپلاین چندمرحلهای آنها به شما کمک میکند متوجه شوید دادههای خام چگونه به فرمهای میانی (IR) مانند P-code یا VEX و در نهایت به کد شبیه به C تبدیل میشوند. با این شناخت، به جای اعتماد کورکورانه به اعداد و آدرسهای گزارششده، دلیل هر اختلاف را در گراف جریان کنترل و منطق داخلی ابزارها پیدا میکنید.
پنج ابزار یک فایل آبجکت را میخوانند و چهار آدرس مختلف، سه شمارش متفاوت برای بلوکها و دو پاسخ متناقض برای یک ساختار switch تحویل میدهند؛ در این درس بستر آزمایشی (harness) لازم را میسازیم تا نشان دهیم واقعیت ماجرا چیست.
اهداف یادگیری
- ابزارهای اصلی این حوزه را روی یک خط زمانی تاریخدار از 1973 تا 2026 قرار دهید و نام خانوادهی هر یک را مشخص کنید.
- مراحل هشتگانهی یک دیکامپایلر و اصطلاحی را که Ghidra، Binary Ninja، Hex-Rays، radare2، rizin و angr برای هر مرحله به کار میبرند نام ببرید.
- یک دستورالعمل اسمبلی را به ESIL، RzIL، P-code و VEX تبدیل کنید (Lift) و توضیح دهید کدامیک فلگها را بیدرنگ (eager) مینویسند و کدامیک یک تانک (thunk) ذخیره میکنند.
- آدرسهای خروجی پنج ابزار را به یک آفست یکتا در بخش
.textتقلیل دهید و صحت این تقلیل را روی یک تابع اثبات کنید. - بستر مقایسهای بسازید که خروجیهای ثبتشدهی ابزارها را بخواند و میزان توافق روی نقطهی شروع دستورالعملها، بلوکها، یالها و شروع توابع را گزارش دهد.
- بر اساس شواهد ثبتشده، نشان دهید هر ابزار در کجا کدهای ساختگی سرهم میکند، کدهایی را جا میاندازد یا جلوی یک پرش غیرمستقیم متوقف میشود.
صورت مسئله
از یک ایجنت (Agent) میخواهید گزارش سه ابزار مهندسی معکوس را دربارهی تابع grade در فایل ledger-x86_64-O1.o خلاصه کند. ایجنت هر ابزار را اجرا میکند، سه لیستینگ را میخواند و گزارش میدهد که تابع در یک ابزار در آدرس 0x08000050، در دیگری در 0x400010 و در سومی در 0x100010 قرار دارد. در نتیجه نتیجهگیری میکند که این ابزارها روی محل تابع با هم اختلاف نظر دارند.
در واقع ابزارها کاملاً همنظرند. آبجکت از نوع بازآراییپذیر (relocatable) است و آدرس بارگذاری مشخصی ندارد؛ بنابراین هر ابزار آدرس مبنای فرضی خودش را انتخاب میکند. آدرسی را که هر ابزار به بخش .text اختصاص داده از آدرس تابع کم کنید؛ پاسخ در تمام آنها دقیقاً 0x10 است. ایجنت آدرسهای خام را مقایسه کرده بود، چون چیزی به آن تفهیم نکرده بود که این آدرس در هر ابزار چه معنایی دارد.
کمی بعد، همان ایجنت تابع grade را در بیلد -O0 میخواند و از چهار ابزار تعداد بلوکهای پایهایِ 3، 8، 8 و 9 را گزارش میکند. هر چهار عدد واقعی هستند: یک ابزار جلوی jmp rax متوقف شده؛ دو ابزار جدول پرش (jump table) را با دنبال کردن بازآراییهایش خواندهاند؛ و ابزار چهارم بلوک را در محل پرش شکسته تا یک برچسب (label) بگذارد. شمارش تعداد بلوکها بدون دانستن دلیل پشت آن، عددی بیمعناست.
این درس نقشهی راهی است که پیش از اعتماد به هر یک از این اعداد به آن نیاز دارید. در درسهای 32 تا 36 یک رمزگشا، یک لیفتر، یک رکورد شواهد و یک ابزار بازسازی ساختیم. در نیمهی دوم این مسیر، آنها را در کنار ابزارهای متداول دنیای واقعی قرار میدهیم. برای این کار به تاریخچهی این حوزه، دستهبندی خانوادهی ابزارها، پایپلاینی (Pipeline) که هر دیکامپایلر طی میکند و بستری برای مقایسهی خروجی ابزارها روی آبجکتهایی که خودتان کامپایل کردهاید، نیاز دارید.
پنجاه و سه سال تاریخ در گامهای زمانبندیشده
عمر این حوزه از بیشتر ابزارهایش بیشتر است. تکتک تاریخهای زیر منبع اولیهای در بخش «برای مطالعهی بیشتر» دارند.
از 1973 تا 1991: ابتدا دیباگرها، سپس دیساسمبلرها
نخستین پایاننامههای حوزهی دیکامپایل مربوط به سال 1973 هستند. هولندر در استنفورد و هوسل در پردو آنها را نوشتند و هر دو امروزه از طریق ارجاعاتشان شناخته میشوند. ابزارهای کاربردی آن دوران دیباگرها بودند. مارانزانو و بورن در سال 1977 آموزش adb را در آزمایشگاههای بل نوشتند و این دیباگر در سال 1979 همراه با UNIX V7 عرضه شد. ابزار DEBUG تیم پترسون همراه با یک دیساسمبلر 8086، در اوت 1981 با PC-DOS 1.00 منتشر شد. گنو دیباگر (GDB) ریچارد استالمن در سال 1986 وارد مرحلهی توزیع شد. SoftICE در سال 1987 یک دیباگر کرنل (Kernel) را به محیط DOS آورد.
سپس نوبت به دیساسمبلرهای دستهای (batch) رسید. ابزار Sourcer از شرکت V Communications در سال 1988، برنامههای DOS را در چند دور پردازش میکرد و خروجیِ تفکیکشده همراه با کامنت تولید میکرد. گنو binutils با اسنپشاتهای (Snapshot) بدون شماره در سال 1988 آغاز شد و objdump کپیرایت سال 1990 را بر پیشانی دارد. ایلپاک گویلفانوف نخستین خط کد IDA را در ژانویهی 1991 نوشت و IDA 0.1 را در 20 مه 1991 بهصورت نرمافزار اشتراکی (shareware) منتشر کرد.
از 1991 تا 2009: دیساسمبلرهای تعاملی و نخستین دیکامپایلرها
کریستینا سیفوئنتس رسالهی «تکنیکهای ترجمهی معکوس» را در ژوئیهی 1994 در QUT به پایان رساند. دیکامپایلر او، dcc، برنامههای i80286 محیط DOS را به C تبدیل میکرد؛ مقالهی مشترک او و گاف در سال 1995 مراحلی را پایهگذاری کرد که تمام دیکامپایلرهای بعدی به آن وفادار ماندند. در پی آن، REC در 1997 و Boomerang در 2002 بهعنوان نخستین دیکامپایلر متنباز تحت مجوز BSD عرضه شدند.
ابزار IDA در سال 1996 در شرکت DataRescue به محصولی تجاری تبدیل شد، در 1999 کیت توسعهی افزونه (SDK) دریافت کرد و در سال 2005 به Hex-Rays منتقل شد. دیکامپایلر Hex-Rays Decompiler 1.0 در 17 سپتامبر 2007 عرضه شد. در همان سال مایک ون امریک رسالهی «فرم انتساب تکایستا برای دیکامپایل» را ارائه داد؛ رسالهای که فرم انتساب تکایستا (Static Single Assignment یا SSA) را در کانون تحلیل جریان دادهی دیکامپایلرها نشاند. مقالهی هالوار فلیک با عنوان «مقایسهی ساختاری آبجکتهای اجرایی» (2004) جرقهی خانوادهی ابزارهای مقایسه (diffing) را زد و BinDiff در اوت 2004 روانهی بازار شد.
پروژهی radare در فوریهی 2006 بهعنوان یک ویرایشگر هگز فارنزیک با آفستهای 64 بیتی کلید خورد. پروژهی radare2، بازنویسی ماژولار آن تحت مجوز LGPL-3.0، نخستین بار در 7 مارس 2009 منتشر شد.
از 2009 تا 2019: فریمورکها و بازنماییهای واسط
ابزار LLDB در 8 ژوئن 2010 بهعنوان یک زیرپروژهی LLVM معرفی شد. BAP یا پلتفرم تحلیل باینری همراه با زبان واسط BIL در کنفرانس CAV 2011 ارائه شد. گوگل در 1 مارس 2011 شرکت zynamics (سازندهی BinDiff) را خرید. Hopper در اواخر 2011 وارد فروشگاه مک اپ استور شد. مت گادبولت در سال 2012 پروژهی Compiler Explorer را راه انداخت تا برنامهنویسان C++ بتوانند کد اسمبلی خروجی کامپایلر را ببینند.
نخستین خط کد angr در اوت 2013 نوشته شد. مقالهی Phoenix (شوارتز، لی، وو، براملی، USENIX Security 2013) بازیابی ساختار با حفظ معنا را با تنها 40 دستور goto در میان 8,676 تابع به نمایش گذاشت. مقالهی DREAM (یکدان و دیگران، NDSS 2015) دستورهای goto را با ساختاربندی مستقل از الگو حذف کرد. شرکت Vector 35 در 14 ژانویهی 2015 تأسیس شد و Binary Ninja در 23 ژوئیهی 2016 با زبان واسط Low Level IL و API پایتون عرضه شد. angr در 2015 بهصورت عمومی منتشر شد و در مسابقهی سایبری DARPA Cyber Grand Challenge در اوت 2016 مقام سوم را کسب کرد. BinDiff در مارس 2016 رایگان شد. Cutter 1.0 در دسامبر 2017 رابط کاربری Qt را به radare2 اضافه کرد. در سال 2018 گویلفانوف ارائهی «سازوکار درونی دیکامپایلر: مایکروکد» را در Recon ارائه کرد که نخستین تشریح عمومی از بازنمایی واسط (IR) در Hex-Rays بود.
از 2019 تا 2026: دیکامپایلرهای متنباز و چرخش مسیر پژوهشها
ابزار Ghidra 9.0 در 5 مارس 2019 توسط آژانس امنیت ملی آمریکا (NSA) در کنفرانس RSA تحت مجوز Apache-2.0 منتشر شد و سورس کامل آن یک ماه بعد روی GitHub قرار گرفت. نسخهی 8.19.2.4 ابزار angr در همان سال یک دیکامپایلر مقدماتی اضافه کرد. Binary Ninja 2.0 دیکامپایلر مبتنی بر High Level IL خود را در 20 مه 2020 عرضه کرد. Rizin در 8 دسامبر 2020 بهعنوان یک fork از radare2 متولد شد و تیم Cutter به آن مهاجرت کردند. نسخهی رایگان IDA Freeware 7.6 در مه 2021 یک دیکامپایلر ابری x64 اضافه کرد.
ابزار BinDiff 8 در 25 سپتامبر 2023 تحت Apache-2.0 متنباز شد. ساختاربند SAILR (باسک و دیگران، USENIX Security 2024) به ساختاربند پیشفرض angr تبدیل شد و استدلال کرد که فقط باید دستورهای goto اضافی حذف شوند، زیرا لینوکس 6.1 در کد منبع خود 3,754 دستور از این نوع دارد. Binary Ninja 4.0 در فوریهی 2024 یک نسخهی رایگان (Free) معرفی کرد و IDA 9.0 کتابخانهی idalib را برای استفاده بدون رابط گرافیکی ارائه داد. مقالات حوزهی دیکامپایلرهای مبتنی بر یادگیری ماشین از SLaDe (2023) آغاز شد، به LLM4Decompile (2024) و DecompileBench (2025) رسید و با دو مطالعه در 2026 در مورد خوانایی کد با کمک ایجنتها و دیکامپایلرهایی که بیشتر بازکامپایل میکنند و کمتر ساختار را حفظ میکنند، ادامه یافت.
نسخههایی که این درس اجرا کرده در 14 اکتبر 2026 بررسی شدهاند: Ghidra 12.1.4 (21 سپتامبر 2026)، radare2 6.2.4 (5 اکتبر 2026)، rizin 0.9.1 (29 ژوئن 2026) و angr 10.0.3 (10 اکتبر 2026). ابزارهای تجاری و غیرمتنباز روز عبارتاند از Binary Ninja 6.0 "Krypton" (3 سپتامبر 2026) و IDA 9.4 (13 ژوئیه 2026) که این درس آنها را مستقیماً اجرا نکرده است.
خانوادههای ابزارها
هر ابزار بسته به خروجیاش به یک خانواده تعلق دارد. این خانوادهها درون یک نرمافزار همپوشانی پیدا میکنند و بیشتر ابهامات دقیقاً از همین نقطه شروع میشود.
دیساسمبلرها
روش پیمایش خطی (linear sweep) بایتها را از ابتدای بخش کد تا انتهای آن بدون وقفه رمزگشایی (Decode) میکند. ابزارهای objdump -d گنو و llvm-objdump -d به این شکل کار میکنند و هیچکدام گراف جریان کنترل (CFG) نمیسازند. ابزار objdump گنو نمادها (symbols) را مرز دستورالعمل در نظر میگیرد و بین آنها را پیمایش میکند. نسخهی binutils 2.47 (26 ژوئیه 2026) تحت GPL-3.0-or-later است. نسخهی LLVM 23.1.3 تحت مجوز Apache-2.0 با استثنای LLVM منتشر شده است. بستهی Command Line Tools اپل llvm-objdump را با عنوان "Apple LLVM version 17.0.0" ارائه میدهد که دادههای این درس از روی همین نسخه ثبت شدهاند.
یک دیساسمبلر تعاملی، روش فرود بازگشتی (recursive descent) را از نقاط ورودی شناختهشده اجرا میکند، برای تشخیص آغاز توابع از روشهای اکتشافی (heuristics) کمک میگیرد و یک پایگاه داده نگه میدارد. IDA Pro (ماژولهای پردازنده، صفهای تحلیل خودکار، امضاهای FLIRT)، Ghidra (کلاس Disassembler به همراه تحلیلگرهای Auto Analysis)، Binary Ninja (فرود بازگشتی و سپس پیمایش خطی روی نواحی نامشخص)، Hopper، radare2 و rizin (دستورهای aa، aaa، af) و Cutter و iaito بهعنوان واسط گرافیکی، همگی در این خانواده قرار دارند. آندریسه و همکارانش (USENIX Security 2016) نه مورد از این ابزارها را روی باینریهای کامپایلشده سنجیدند. دقت تشخیص دستورالعملها نزدیک به 100 درصد بود، اما تشخیص نقطهی آغاز توابع بسیار چالشبرانگیز بود و نرخ منفی کاذب (false negative) آن به 20 درصد یا بیشتر میرسید.
دیکامپایلرها
دیکامپایلرهای تجاری: Hex-Rays Decompiler (برای دوازده معماری از x86 تا RISC-V)، سطح HLIL در Binary Ninja به همراه نمایش شبهکد C، و نمای شبهکد در Hopper. دیکامپایلرهای متنباز: دیکامپایلر Ghidra، یک موتور C++ تحت Apache-2.0 که در r2ghidra و rz-ghidra بهعنوان افزونه و در pypcode بهعنوان لیفتر استفاده میشود؛ دیکامپایلر angr بر پایهی AIL با ساختاربندی SAILR تحت مجوز BSD-2-Clause؛ پروژههای r2dec و jsdec در جاوااسکریپت؛ و نمونههای تاریخی Boomerang و dcc. ابزار radare2 یک دستور داخلی به نام pdc هم دارد: «خروجی شبهدیساسمبل با نحو شبیه به C». ابزار rizin دیکامپایلر داخلی ندارد؛ دستور pdc در rizin 0.9.1 وجود ندارد و اجرای pdg به افزونهی rz-ghidra وابسته است.
دیباگرها
ابزار GDB (1986) همراه با فرانتاندهای GEF و pwndbg، ابزار LLDB (2010)، WinDbg با قابلیت Time Travel Debugging (2017)، ابزار OllyDbg (توقف توسعه در 2014)، x64dbg (مجوز GPL-3.0، 2014)، دیباگر Ghidra با اتصالدهندههای (Connector) پایتون، دیباگر Binary Ninja و دیباگرهای داخلی radare2 و rizin. در درس 42 از دو مورد از آنها روی کدهای خودتان استفاده میکنید.
فریمورکهای تحلیل باینری
فریمورک angr (زبان واسط VEX، اجرای نمادین، تحلیلهای CFGFast و CFGEmulated، دیکامپایلر)، ابزار BAP (زبان واسط BIL، OCaml)، Dyninst، Jakstab و ByteWeight چارچوبهایی هستند که در مقالهی آندریسه نام برده شدهاند. ابزار Ghidra از طریق PyGhidra، ابزار Binary Ninja از طریق import binaryninja و IDA از طریق idalib بهصورت کتابخانهای قابل استفاده هستند.
کتابخانههای فرمت و موتورهای پردازش
کتابخانهی LIEF 1.0.0 (12 ژوئیه 2026، Apache-2.0) فرمتهای ELF، PE، Mach-O، COFF و فرمتهای اندروید را پارس میکند. کتابخانهی CLE مخفف "CLE Loads Everything"، لودر اختصاصی angr است. Capstone 5.0.9 (28 مه 2026، BSD-3-Clause) رمزگشایی (Decode) میکند؛ Keystone 0.9.2 (GPL-2.0) اسمبل میکند؛ Unicorn 2.1.4 (GPL-2.0) شبیهسازی اجرا انجام میدهد. radare2 6.2.4 شامل Capstone 5 است و rizin 0.9.1 کتابخانههای Capstone، Zydis و tree-sitter را در خود جای داده است.
ابزارهای مقایسه و نمایشگرهای سمت کامپایلر
ابزار BinDiff 8 (Apache-2.0) با افزونهی BinExport 12 برای IDA Pro، Binary Ninja و Ghidra؛ ابزار Diaphora 3.4.3 (11 اکتبر 2026، AGPL-3.0) بهعنوان افزونهی IDA؛ ابزارهای radiff2 و rz-diff. وبسایت Compiler Explorer (BSD-2-Clause) کد اسمبلی تولیدشده توسط کامپایلر را برای یک قطعه کد منبع نشان میدهد که دقیقاً برعکسِ مسیر تمام ابزارهای این لیست است. درس 44 به خانوادهی مقایسهگرها اختصاص دارد.
نسخهها، مجوزها و آنچه در این درس اجرا شد
| ابزار | نسخهی بررسیشده در 2026-10-14 | مجوز | ورود بدون محیط گرافیکی | اجرا در اینجا |
|---|---|---|---|---|
| Ghidra | 12.1.4 | Apache-2.0 | analyzeHeadless، pyghidra.start() |
بله، روی هر چهار آبجکت |
| radare2 | 6.2.4 | LGPL-3.0 | r2 -qc '<cmds>' file، rabin2 |
بله |
| rizin | 0.9.1 | LGPL-3.0 | rizin -qc '<cmds>' file، rz-bin |
بله |
| angr | 10.0.3 | BSD-2-Clause | angr.Project در پایتون |
بله |
| llvm-objdump | Apple LLVM 17.0.0 | Apache-2.0 با استثنای LLVM | llvm-objdump -d -r |
بله |
| Binary Ninja | 6.0 "Krypton" | تجاری؛ نسخهی Free برای استفادهی غیرتجاری | binaryninja.load() با لایسنس Commercial، Ultimate یا headless |
خیر، صرفاً بررسی مستندات |
| IDA Pro | 9.4 | اشتراک تجاری؛ نسخهی IDA Free برای x86 و x86-64 غیرتجاری | idat، ida -A -S، idalib |
خیر، صرفاً بررسی مستندات |
| Hopper | 6.6.0 | تجاری، دائمی | ثبتنشده در مستندات | خیر، صرفاً بررسی مستندات |
| Cutter | 2.5.0 | GPL-3.0 | واسط گرافیکی روی rizin | خیر |
نسخهی رایگان Binary Ninja فقط کدهای x86، x86-64، ARMv7 و AArch64 را دیکامپایل میکند و API ندارد. نسخهی رایگان IDA فاقد IDAPython، فاقد SDK و فاقد idalib است و دیکامپایلر آن در فضای ابری اجرا میشود. تمام نکات ذکرشده در مورد این دو ابزار بر اساس مستندات و گزارش تغییرات رسمی آنهاست و در نمودارها نیز با برچسب عدم اجرا مشخص شدهاند.
پایپلاین دیکامپایلر، مرحله به مرحله
سیفوئنتس در سال 1994 این مراحل را نامگذاری کرد: تحلیل نحو (syntax analysis)، تحلیل معنایی (semantic analysis)، تولید کد واسط، تولید گراف جریان کنترل، تحلیل جریان داده، تحلیل جریان کنترل و تولید کد نهایی. او آنها را به بخش فرانتاند وابسته به ماشین، بخش میانی مستقل از ماشین و زبان، و بخش بکاند وابسته به زبان تقسیم کرد. تمام دیکامپایلرهای بعدی، همین چارچوب را حفظ کردند و فقط نام اجزا را تغییر دادند. جدول زیر مراحل هشتگانهی این درس و نام معادل آن را در هر ابزار نشان میدهد. در تصویر تعاملی، ستونهای این جدول قابل انتخاب هستند.
| مرحله | Ghidra | Binary Ninja | Hex-Rays / IDA | radare2 | rizin | angr |
|---|---|---|---|---|---|---|
| لودر | ElfLoader، MachoLoader، PeLoader؛ تعیین زبان با .ldefs |
BinaryView، BinaryViewType |
ماژول لودر (LDSC) |
RBin (rabin2) |
RzBin (rz-bin) |
CLE (cle.Loader، main_object) |
| دیساسمبل | Disassembler؛ کامپایل .slaspec مربوط به SLEIGH به .sla؛ تحلیلگرهای "Disassemble Entry Points"، "Function Start Search" |
پلاگین معماری؛ فازهای Recursive Descent و سپس linear sweep | ماژول پردازنده (ana.cpp، emu.cpp، out.cpp)؛ صفهای تحلیل خودکار؛ FLIRT |
پلاگین RArch به RAnalOp؛ دستورهای aa، aaa، af |
پلاگین RzAnalysis؛ دستورهای aa، aaa، af |
بلوکهای Capstone؛ جستجوی آغاز تابع با CFGFast |
| بازیابی CFG | "Generate Basic Blocks and the CFG"؛ BlockGraph؛ "Decompiler Switch Analysis" |
Function، BasicBlock؛ فاز "Control Flow Graph (CFG) Analysis" |
mbl_graph_t، آماده در MMAT_LOCOPT |
afb، agf، aab |
afb، agf، aab (موتور Nucleus) |
CFGFast، CFGEmulated؛ kb.functions؛ IRSB |
| تبدیل به کد واسط (Lifting) | "Generate Raw P-code"؛ ورنودها (Varnodes) در فضاهای ram، register، const، unique |
Lifted IL، LLIL، Mapped MLIL، MLIL | مایکروکد (minsn_t، mop_t)؛ وضعیت MMAT_GENERATED |
ESIL (asm.esil، ae) |
RzIL (aoi، aez) |
ابتدا VEX (pyvex.lift، IRSB) سپس AIL |
| فرم SSA | Heritage، ActionHeritage؛ عملگرهای MULTIEQUAL، INDIRECT |
سطوح LLIL SSA، MLIL SSA، HLIL SSA؛ عملگر HLIL_VAR_PHI |
فرم صریحی با نام SSA ندارد؛ تحلیل جریان use-def در سطوح MMAT_GLBOPT1 تا 3 |
ندارد؛ ESIL برای شبیهسازی طراحی شده است | ندارد؛ RzIL برای شبیهسازی طراحی شده است | فازهای _stage_transform_to_ssa_level0 و level1؛ SeqNodeDephication |
| جریان داده و تایپها | حلقهی ActionDeadCode، ActionInferTypes و مجموعهی Rule؛ استخراج پروتوتایپ از .cspec |
انتشار مقادیر ثابت، متغیرها و تایپها در MLIL؛ مرحلهی analyzeReturns |
مرحلهی "Transform microcode"؛ سطوح MMAT_CALLS، MMAT_GLBOPT، MMAT_LVARS؛ اطلاعات TIL |
دستورهای afv، afc، aaft؛ شبیهسازی با ESIL |
دستورهای afv، afc، aft |
ابزارهای CallingConventionAnalysis، VariableRecoveryFast، Typehoon |
| ساختاربندی (Structuring) | ماژول CollapseStructure؛ بلوکهای BlockIf، BlockWhileDo، BlockSwitch، BlockGoto |
ساختارهای HLIL_IF، HLIL_WHILE، HLIL_SWITCH؛ درخت AST در root |
ساخت "ctree"؛ تحلیل ساختاری؛ نواحی goto | pdc؛ pdd (در r2dec)؛ pdg (در r2ghidra) |
pdg (در rz-ghidra)؛ jsdec |
ابزار RegionIdentifier؛ RecursiveStructurer همراه با SAILRStructurer |
| تولید کد نهایی | مرحلهی "Emit Final C Tokens"؛ ماژولهای PrintC، ActionNameVars، ActionSetCasts |
رندر کردن HLIL؛ زبانهای Pseudo C، Pseudo Rust، Pseudo Python | مراحل "Beautify ctree; Print ctree"؛ نوع cfunc_t |
متن خروجی pdc، pdd، pdg |
متن خروجی pdg |
ماژول CStructuredCodeGenerator؛ خروجی codegen.text |
لودر
لودر ساختار فایل را پارس میکند، بخشها (sections) را نگاشت میدهد و نمادها و بازآراییها را میخواند. در یک فایل آبجکتِ بازآراییپذیر هنوز هیچ بخشی آدرس نهایی ندارد، بنابراین لودر یک آدرس مبنا انتخاب میکند. radare2 و rizin فایل را در 0x08000000 نگاشت میدهند؛ در نتیجه بخش .text در 0x08000040 مینشیند، چون این بخش از آفستِ فایلی 0x40 شروع میشود. CLE بخش .text را در 0x400000 قرار میدهد. لودر ElfLoader در Ghidra آن را در 0x100000 بارگذاری میکند. ابزار llvm-objdump فقط آفستهای درون بخش را چاپ میکند و اصلاً آدرس پایهای در نظر نمیگیرد.
لودرها در اعمال بازآراییها نیز تفاوت دارند. Ghidra آنها را در زمان ایمپورت اعمال میکند و CLE هم آنها را پردازش میکند. radare2 هنگام باز کردن آبجکت اخطار میدهد که بازآراییها اعمال نشدهاند و تغییر تنظیم به bin.relocs.apply=true رفتار آن را عوض میکند. رفتار لودر در برخورد با بازآراییها تعیین میکند که مراحل بعدی چه دادههایی را میبینند؛ اثری که در جدول پرش کامپایل -O0 بهوضوح خواهید دید.
دیساسمبل
در درس 33 هر دو روش را پیادهسازی کردیم. تمام ابزارهای جدول بهجز llvm-objdump ابتدا فرود بازگشتی را اجرا میکنند و سپس جستجویی برای کشف آغاز توابع انجام میدهند. مستندات Binary Ninja مراحل را به این ترتیب نام میبرد: فرود بازگشتی، تحلیل اهداف فراخوانی، تحلیل CFG بهصورت «دیساسمبل خطیِ نواحی نامشخص» و تحلیل فراخوانیهای دمدستی (Tail Call). ابزار Ghidra آنالایزرهای خود را طبق اولویت اجرا میکند و آنالایزرهای "Disassemble Entry Points" و "Function Start Search" در آغاز کارند. ابزار IDA صفهایی با اولویتهای مشخص برای تحلیل خودکار دارد و امضاهای FLIRT را برای کدهای کتابخانهای استفاده میکند. تحلیل CFGFast در angr چهار منبع برای یافتن شروع توابع ذکر میکند: نمادها، اسکن پرولوگ، مقاصد فراخوانی و اسکن کامل بایتهای باقیمانده. angr دقیقاً در همین منبع چهارم است که روی پدینگها (حاشیهگذاری) توابع ساختگی ایجاد میکند.
بازیابی CFG
هر ابزار بلوکها را مشابه روش درس 33 میشکند، با یک تفاوت که آن را اندازه خواهید گرفت: angr بلوک را با رسیدن به دستور call پایان میدهد، چون بلوک IRSB در آن یک ورودی و چند خروجی دارد و هر فراخوانی یک خروج محسوب میشود. اما Ghidra، radare2 و rizin از روی دستور call عبور میکنند. در تابع grade_label با بهینهسازی -O0 که تابع grade را فراخوانی میکند، angr دو بلوک گزارش میدهد اما سه ابزار دیگر 1 بلوک شناسایی میکنند؛ درحالیکه دستورالعملها کاملاً یکسان هستند.
تبدیل به کد واسط (Lifting)
Binary Ninja تبدیل به کد واسط را اینگونه تعریف میکند: «ترجمه از یک زبان سطح پایینتر به زبان واسطی که سطحبالاتر محسوب میشود». بازنماییهای واسط در هدف طراحی با یکدیگر تفاوت دارند. P-code و VEX برای تحلیل و دیکامپایل ساخته شدهاند. ESIL و RzIL برای شبیهسازی طراحی شدهاند و مرحلهی SSA ندارند. مایکروکد Hex-Rays با کدی شبیه به RISC و دارای 72 آپکد شروع میشود و از سطوح تکاملی مختلف، از MMAT_GENERATED تا MMAT_LVARS عبور میکند. Binary Ninja کد را به Lifted IL برمیگرداند، سپس در سطح LLIL فلگها را در قالب شرطها ترکیب میکند و سطح MLIL آن «ثباتها و دسترسیهای حافظه را به متغیر تبدیل میکند». در بخش بعد، این چهار زبان واسط را روی یک دستورالعمل در کنار هم بررسی میکنیم.
فرم SSA
پایاننامهی ون امریک مزایای استفاده از SSA را برای دیکامپایلر برمیشمارد: انتشار عبارات، شناسایی پارامترها و مقادیر بازگشتی بدون نیاز به پیشفرضهای قرارداد فراخوانی (ABI)، تحلیل تغییرناپذیری متغیرها، تحلیل تایپها در قالب یک مسئلهی تنک جریان داده، و تحلیل پرشها و فراخوانیهای غیرمستقیم. Ghidra بخش ساخت فرم SSA خود را Heritage مینامد و دو عملگر جدید اضافه میکند که فقط در گراف وجود دارند: MULTIEQUAL برای تابع فی (phi) و INDIRECT برای اثرات جانبی. ابزار Binary Ninja در تمام سطوح IL فرم SSA را ارائه میدهد. ابزار angr دو مرحلهی SSA درون ماژول Clinic دارد. Hex-Rays فرم مجزایی با نام SSA ندارد و تحلیل use-def را در گامهای بهینهسازی سراسری انجام میدهد. ESIL و RzIL این مرحله را ندارند.
جریان داده و تایپها
حلقهی اصلی سادهسازی در Ghidra شامل "Eliminate Dead Code"، "Propagate Local Types" و "Perform Term Rewriting" است که تا رسیدن به حالت پایدار تکرار میشود و بازنویسی عبارات با مجموعهای از اشیای Rule انجام میگیرد. موتور Hex-Rays مایکروکد را تبدیل میکند، آرگومانهای توابع را در MMAT_CALLS تشخیص میدهد و متغیرهای محلی را در MMAT_LVARS تخصیص میدهد. ابزار angr فازهای CallingConventionAnalysis، VariableRecoveryFast و Typehoon (پیادهسازی سیستم استنتاج چندریختی نونان، لاگینوف و کوک) را اجرا میکند. درس 41 به این مرحله اختصاص دارد.
ساختاربندی
ماژول CollapseStructure در Ghidra زیرگرافهای منطبق بر یک الگو را پیدا میکند و آنها را در یک گره ادغام میکند؛ اگر در جایی متوقف شود، یالی را حذف کرده و آن را ساختارنیافته علامت میزند. Hex-Rays گراف CFG را به ساختارهای if، while، do و switch تجزیه میکند و باقیمانده را بهعنوان نواحی دارای goto رها میکند. ابزار angr نواحی تکورودی تکخروجی را شناسایی کرده و بهطور پیشفرض با SAILRStructurer و در صورت نیاز با PhoenixStructurer یا DreamStructurer آنها را ساختاربندی میکند. در درس 34 یک ساختاربند را بهصورت دستی پیاده کردیم.
تولید کد نهایی
ابزار Ghidra پس از فازهای ActionNameVars و ActionSetCasts توکنهای C را از طریق PrintC با ابزار مرتبساز اوپن (Oppen pretty-printer) تولید میکند. Hex-Rays درخت ctree را مرتب کرده و خروجی میدهد. ابزار Binary Ninja سطح HLIL را در قالبهای Pseudo C، Pseudo Rust یا Pseudo Python رندر میکند. ابزار angr متن را در codegen.text برمیگرداند. ابزار radare2 خروجی دستورهای pdc، pdd یا pdg را چاپ میکند.
یک دستورالعمل در چهار بازنمایی واسط
بایتهای 48 83 ec 10 یعنی subq $0x10, %rsp را در نظر بگیرید. طبق قواعد درس 33 آن را به شکل دستی رمزگشایی کنید:
- بایت
48یک پیشوند REX با بیتW = 1است؛ پس عملوند 64 بیتی است. - بایت
83آپکد محاسباتی گروه 1 با یک مقدار فوری (immediate) 8 بیتی است. فیلد reg در ModRM نوع عملیات را تعیین میکند. - بایت
ecبایت ModRM معادل11 101 100است. مقدارmod = 11حالت کار با ثبات است. مقدارreg = 101برابر با کد عملگر/5یعنیsubاست. مقدارr/m = 100ثبات%rspرا مشخص میکند. - بایت
10همان مقدار فوری یعنی عدد 16 است.
این چهار بایت به چهار لیفتر تحویل داده شد. دستورات دقیق و متن کامل در فایلهای تست ثبت شدهاند.
خروجی ESIL در radare2 6.2.4 یک رشتهی جداشده با کاما شامل 30 توکن است:
16,0x8000000000000000,-,!,16,rsp,-=,63,$o,^,of,:=,63,$s,sf,:=,$z,zf,:=,$p,pf,:=,64,$b,cf,:=,4,$b,af,:=
این مدل یک ماشین مبتنی بر پشته است. عبارت 16,rsp,-= تفریق را درجا انجام میدهد. سپس تکتک فلگها با یک عملگر داخلی مقداردهی میشوند: $o برای سرریز، $s برای علامت، $z برای صفر، $p برای توازن، و $b برای قرض گرفتن.
خروجی RzIL در rizin 0.9.1 یک S-expression با 14 عملگرِ set است:
(seq (set op1 (var rsp)) (set op2 (bv 64 0x10)) (set sub (- (var op1) (var op2))) (set rsp (var sub))
(set _result (var sub)) (set pf (! (lsb (let _val (cast 8 false (var _result)) ...))))
(set zf (is_zero (var _result))) (set sf (msb (var _result))) ... (set cf (|| ...)) (set of (|| ...)) (set af (|| ...)))
هر فلگ یک عبارت بولی روی بردار بیتهاست. فلگ توازن بهتنهایی یک عبارت let چهار سطحی تولید کرده است.
خروجی P-code از مشخصات SLEIGH به شناسه x86:LE:64:default از طریق pypcode 4.0.1، بعد از شناسه IMARK شامل 9 عملیات است:
CF = RSP < 0x10
OF = sborrow(RSP, 0x10)
RSP = RSP - 0x10
SF = RSP s< 0x0
ZF = RSP == 0x0
unique[58300:8] = RSP & 0xff
unique[58400:1] = popcount(unique[58300:8])
unique[58500:1] = unique[58400:1] & 0x1
PF = unique[58500:1] == 0x0
انتقالهای بین ثباتها با متغیرهای موقت در فضای unique. فلگها بلافاصله در محل دستور و بهصورت بیدرنگ محاسبه میشوند و فلگهای CF و OF پیش از آنکه تفریق مقدار RSP را بازنویسی کند، محاسبه میگردند.
خروجی VEX در pyvex 10.0.3 شامل 7 عبارت بعد از IMark است:
t2 = GET:I64(offset=48)
t0 = Sub64(t2,0x0000000000000010)
PUT(offset=144) = 0x0000000000000008
PUT(offset=152) = t2
PUT(offset=160) = 0x0000000000000010
PUT(offset=168) = 0x0000000000000000
PUT(offset=48) = t0
هیچ فلگی در این خروجی دیده نمیشود. VEX ثباتها را با آفست آنها در وضعیت ماشین مهمان مشخص میکند و آفست 48 همان rsp است. آفستهای 144 تا 168 مربوط به cc_op، cc_dep1، cc_dep2 و cc_ndep هستند. لیفتر کد عملیات 8 (که نشاندهندهی تفریق 64 بیتی است) و دو عملوند را ذخیره میکند. اگر انشعابی در ادامه نیاز به فلگ داشته باشد، مقدار آن را از این تانک محاسبه خواهد کرد. به همین دلیل در گزارش بستر ما، مقدار pcode.writesFlags برابر با true و vex.computesFlags برابر با false است، و VEX کوتاهترین خروجی را بین این چهار فرمت دارد.
اگر همین چهار لیفتر روی بایتهای 48 63 c7 مربوط به movslq %edi, %rax (نخستین دستور تابع grade در بهینهسازی -O1) اجرا شوند: 5 توکن ESIL (عبارت 32,edi,~,rax,=)، 1 عملگر set در RzIL، 1 عملیات P-code (عبارت RAX = sext(EDI)) و 4 عبارت در VEX تولید میکنند. هیچ لیفتری فلگی ثبت نمیکند، چون این دستور هیچ فلگی را تغییر نمیدهد. لیستینگ خود Ghidra برای این دستور که از طریق اسکریپت headless ثبت شده، همین عمل را بهصورت (register, 0x0, 8) INT_SEXT (register, 0x38, 4) چاپ میکند: آفست 0 ثبات RAX، آفست 0x38 ثبات RDI، و اندازهها به ترتیب 8 و 4 بایت هستند.
دستور 83 f9 04 معادل cmpl $0x4, %ecx که مربوط به بررسی محدوده در تابع grade است، 30 توکن در ESIL، 13 عملگر set در RzIL، 10 عملیات در P-code و 7 عبارت در VEX با مقدار cc_op برابر با 7 (تفریق 32 بیتی) تولید میکند. هیچ مقداری در %ecx نوشته نمیشود. دو اندازهی تفریق که در VEX خواهید دید 7 برای 32 بیت و 8 برای 64 بیت است، و مترجمی که میسازید این جدول را در خود خواهد داشت.
چهار آدرس پایه برای یک تابع
پیش از آنکه بستر ما بتواند چیزی را مقایسه کند، باید مشخص شود هر تابع کجاست. این محاسبه را یک بار بهصورت دستی برای تابع grade در ledger-x86_64-O1.o انجام دهید:
| ابزار | آدرس بخش .text |
آدرس تابع grade |
آفست محاسبهشده |
|---|---|---|---|
llvm-objdump |
0x0 |
0x10 |
0x10 - 0x0 = 0x10 |
| radare2، rizin | 0x08000040 |
0x08000050 |
0x08000050 - 0x08000040 = 0x10 |
| angr | 0x400000 |
0x400010 |
0x400010 - 0x400000 = 0x10 |
| Ghidra | 0x100000 |
0x100010 |
0x100010 - 0x100000 = 0x10 |
جدول نمادها نشان میدهد که grade در آفست .text + 0x10 با طول 46 بایت قرار دارد. پس از کسر آدرس مبنا، تمام ابزارها همنظر هستند. بستر تست آدرس مبنا را از خروجی ثبتشدهی هر ابزار میخواند: خروجی iSj در radare2 و rizin، ورودی بخش .text در خروجی angr، بلوک حافظهی .text در خروجی Ghidra، و مقدار صفر برای روش پیمایش خطی. تست test_every_tool_places_grade_at_offset_0x10_from_its_own_base درستی این مبنا را میسنجد.
نقاط اشتراک و افتراق ابزارها
بستر آزمایشی موجود در code/main.py خروجیها را میخواند، آدرسها را تقلیل میدهد و مقایسه میکند. در ادامه اعدادی که گزارش میکند آمده و تستها صحت تکتک آنها را میسنجند.
حالت توافق کامل: آبجکت ledger با -O1
چهار تابع، 56 دستور در پیمایش خطی (54 دستور در محدودهی نمادها و 2 دستور در پدینگ). تمام ابزارها نقاط شروع یکسانی را برای دستورات در هر تابع گزارش میکنند: 9، 14، 13 و 18 دستور. چهار ابزار سازندهی گراف، 3، 3، 5 و 4 بلوک و همچنین 2، 3، 5 و 4 یال را ثبت میکنند؛ مجموعهی یالها جفت به جفت میان radare2، rizin، angr و Ghidra کاملاً یکسان است. پنج بلوک و پنج یال مربوط به تابع total دقیقاً همان مواردی هستند که در درس 33 با دست رسم کردیم.
تنها اختلاف در بخش پدینگ است. فاصلهی 3 بایتی میان توابع (66 90 بعد از grade، و 90 بعد از total) توسط پیمایش خطی رمزگشایی میشود، چون دلیلی برای توقف ندارد؛ angr نیز آنها را رمزگشایی کرده و دو تابع ساختگی بدون نماد به نامهای sub_40003e و sub_40005f میسازد. radare2، rizin و Ghidra هیچ ادعایی روی این بایتها مطرح نمیکنند.
حالت برداری: آبجکت ledger با -O2
تابع total در هر چهار سازندهی گراف، شامل 41 دستور در 10 بلوک و 13 یال است. حلقهی SSE2 و حلقهی اسکالر باقیمانده از درس 33 در تمام ابزارها یکدست شکسته شدهاند. angr در اینجا هم دو تابع ساختگی در پدینگ اضافه میکند.
جدول پرش: تابع grade با -O0
در اینجا ابزارها از هم جدا میشوند و اصل درس در همین نقطه است.
| ابزار | تعداد بلوکها | تعداد یالها | دادهی ثبتشده از ابزار |
|---|---|---|---|
| radare2 | 3 | 2 | بلوک واقع در 0x62 خروجی ندارد؛ بایتهای 0x76 تا 0x9a اصلاً تحلیل نشدهاند؛ 8 نقطهی شروع دستور جا افتاده است |
| rizin | 8 | 11 | بلوک واقع در 0x62 شامل یک switch_op با 5 حالت و 4 مقصد مجزاست |
| angr | 8 | 7 | بلوکهای مربوط به هر حالت ایجاد شدهاند، اما انتقال در 0x62 به یک گرهی مقصد غیرقابلحل میرسد |
| Ghidra | 9 | 12 | برچسب switchD در 0x74 بلوک را میشکند؛ چهار مسیر COMPUTED_JUMP به آدرسهای 0x76، 0x7f، 0x88 و 0x91 وجود دارد |
هر چهار ابزار در مواجهه با دستورات، 33 دستورالعمل یکسان را رمزگشایی کردند. radare2 جلوی دستور jmp rax متوقف شد و 36 بایت را پردازشنشده رها کرد؛ بستر مقایسه این بازه را کد جاافتاده (missed code) مینامد. rizin و Ghidra چهار مقصد متمایز را پیدا کردند که ما در درس 33 از روی پنج بازآرایی در بخش .rela.rodata خوانده بودیم. ابزار angr بلوکهای مربوط به caseها را درون تابع grade فهرست کرده اما یالی از بلوک پرش به آنها وصل نکرده است؛ خروجی ثبتشده از آن نشان میدهد انتقال کنترل در 0x62 به یک گره مقصد غیرقابلحل ختم شده و دیکامپایلرش یک دستور goto محاسباتی چاپ کرده است. ابزار Ghidra به دلیل تحلیل سوییچ، یک برچسب روی jmp rax قرار داده و مدل بلوکبندی آن بلوک جدیدی در محل برچسب ایجاد میکند؛ بنابراین با وجود جریان کنترل یکسان، یک بلوک و یک یال بیشتر از rizin دارد.
گراف درس 33 دارای 8 بلوک و 11 یال بود و خروجی rizin دقیقاً با آن مطابقت دارد. نکتهی مهم این است که اعداد 9، 11، 7 و 2 نشاندهندهی تفاوت در حجم کد شناساییشده نیستند؛ بلکه چهار رفتار متفاوت در مواجهه با یک پرش غیرمستقیم و نحوهی قطعهبندی بلوکها را نشان میدهند.
داده در میان کد: تلهی sweep-trap
تابع lookup کلاً 24 بایت است که 4 بایت جدول داده شامل 31 c0 90 48 در آفست 0x10 آن قرار دارد. پیمایش خطی در این بازه 8 نقطهی شروع دستور گزارش میکند. بستر ما تفاوتهای آن با اکثریت ابزارها را فهرست میکند: شروعهای اضافی در 0x10، 0x12 و 0x13، و یک شروع ناموجود در 0x14. این همان دستور addq $0x1, %rax درس 33 است که یک بایت زودتر خوانده شده است. radare2، rizin و Ghidra تعداد 6 شروع، 2 بلوک و 1 یال گزارش میکنند و بازهی 0x10 تا 0x14 را بدون پردازش باقی میگذارند. angr برای تابع lookup همان 2 بلوک و 1 یال را تشخیص میدهد، اما روی بایتهای جدول یک تابع ساختگی به نام sub_400010 میسازد: 2 دستورالعمل در یک بلوک 4 بایتی که در 0x14 تمام میشود؛ یعنی درست جایی که بلوک دوم lookup آغاز میشود، و در نتیجه بایت REX در 0x13 به هیچ دستوری تعلق نمیگیرد. نتیجهگیری نهایی بستر ما برای این آبجکت این است: چون هیچ سازندهی گرافی بازهی 0x10 تا 0x14 را رمزگشایی نکرده، این بایتها درون تابع قرار دارند و ماهیت دادهای دارند، نه کد.
کدهای ساختگی و جاافتاده به تفکیک ابزار
در میان این چهار آبجکت، بستر آزمایشی 9 تابع ساختگی و بدون نماد را شناسایی میکند که همگی توسط angr و روی پدینگها یا جدول داده ایجاد شدهاند. یک بازه نیز توسط radare2 جا افتاده است که شامل 36 بایت مربوط به caseهای تابع grade با بهینهسازی -O0 است. همچنین 9 بازه توسط پیمایش خطی رمزگشایی شده که هیچ ابزار مبتنی بر گرافی آنها را کد در نظر نگرفته است: هشت فاصلهی پدینگ بهاضافهی جدول داده در فایل sweep-trap. هیچ تابعی در هیچ ابزاری جا نیفتاده است.
عملکرد سه دیکامپایلر روی grade
سه ابزار تستشده برای تابع grade با بهینهسازی -O1 خروجی شبیه به C تولید میکنند. کد اصلی شامل switch (score / 10) با پنج حالت است که به یک بررسی محدوده و یک جدول از مقادیر بازگشتی کامپایل شده است.
خروجی Ghidra 12.1.4:
uint grade(int param_1)
{
uint uVar1;
uint uVar2;
uVar2 = param_1 / 10 - 6;
uVar1 = -(param_1 >> 0x1f) ^ (uint)((ulong)((long)param_1 * 0x66666667) >> 0x3f);
if (uVar2 < 5) {
uVar1 = *(uint *)(&DAT_001000b4 + (ulong)uVar2 * 4);
}
return uVar1;
}
خروجی angr 10.0.3:
extern unsigned int g_4000b4[4];
unsigned int grade(int a0)
{
unsigned int v1; // ecx
unsigned int v2; // eax
v1 = (unsigned int)((long long)(a0 * 1717986919) >> 0x22) + (unsigned int)(a0 * 1717986919 >> 63) - 6;
v2 = 0;
if (v1 <= 4)
v2 = g_4000b4[v1];
return v2;
}
خروجی pdc در radare2 6.2.4:
void sym.grade (int64_t arg1) {
rax = edi
rax = rax * 0x66666667
rcx = rax
rcx >>= 0x22
rax >>>= 0x3f
ecx += eax
ecx += 0xfffffffa
eax = 0
v = ecx - 4
if (((unsigned) v) <= 0) {
eax = ecx
rcx = rip
eax = dword [rcx + rax*4]
}
return
}
این سه خروجی را در مقایسه با مراحل پایپلاین بررسی کنید: مرحلهی بازنویسی عبارات در Ghidra ضرب در 0x66666667 و شیفت به راستِ 34تایی را به ساختار param_1 / 10 برگردانده که اصطلاح معرفیشده در درس 34 است. angr همان بایتها را خوانده و محاسبات را به شکل دستنخوردهی a0 * 1717986919 >> 0x22 رها کرده است. خروجی pdc در radare2 صرفاً شبیهسازی ESIL با نحو شبهکد است: نام ثباتها، بدون متغیر، بدون تایپ، و بررسی محدوده نیز بهصورت 0 => پس از یک تفریق بدون علامت نوشته شده است. ابزار Ghidra بارگذاری جدول را با تایپ DAT_001000b4 مشخص کرده است؛ angr آرایهای به شکل g_4000b4[4] تعریف کرده که چهار عنصری است، درحالیکه کد اصلی پنج عنصر دارد. نه Ghidra و نه angr ساختار اصلی switch را بازسازی نکردهاند، چون در خروجی بهینهسازی -O1 دیگر ساختار switch در بایتها باقی نمانده است. اما در بهینهسازی -O0 که جدول پرش مستقیماً وجود دارد، Ghidra عبارت switch(param_1 / 10) را با caseهای 6 تا 10 و حالت default بازسازی کرده و angr یک goto محاسباتی چاپ کرده است.
ابزار rizin دیکامپایلر داخلی ندارد و دادههای ثبتشده نشان میدهند که دستور pdc در آن وجود ندارد. زبان واسط HLIL در Binary Ninja و درخت ctree در Hex-Rays دو خروجیای بودند که این درس امکان ثبت مستقیم آنها را نداشت.
پیادهسازی از صفر
اجرای دمو:
cd phases/13-tools-and-protocols/37-the-reverse-engineering-landscape
python3 code/main.py
خروجی کار یک گزارش JSON است. گزارش با مشخصات نسخهی ابزارهای ذخیرهشده و ابزارهای نصبشده روی سیستم شما (که ممکن است هیچکدام نصب نباشند) شروع میشود. سپس برای هر یک از چهار آبجکت موارد زیر را ارائه میدهد: دامنهی نمادها، فواصل پدینگ، آدرس مبنای هر ابزار، نقاط شروع توابع (یافتشده یا ساختگی)، و برای هر تابع وضعیت انطباق شروع دستورالعملها، تعداد بلوکها، تعداد یالها و یالهای ناموجود در هر ابزار. در پایان، آمار بایتها به تفکیک ابزار، یادداشتهای داخلی ابزارها، فهرستی از یافتهها به زبان ساده، و بخش IR شامل خروجی مترجم برای سه دستورالعمل در کنار مقادیر ثبتشده ارائه میشود.
ساختار مورد انتظار:
{
"captureTools": {"llvm-objdump": "Apple LLVM version 17.0.0", "radare2": "radare2 6.2.4 ...", "rizin": "rizin 0.9.1 ...", "angr": "10.0.3", "ghidra": "12.1.4"},
"localTools": {"radare2": "absent", "...": "..."},
"fixtures": {
"ledger-x86_64-O0.o": {
"bases": {"llvm-objdump": "0x0", "radare2": "0x8000040", "angr": "0x400000", "ghidra": "0x100000", "...": "..."},
"functions": {"grade": {"blocks": {"radare2": 3, "rizin": 8, "angr": 8, "ghidra": 9}, "edges": {"radare2": 2, "rizin": 11, "angr": 7, "ghidra": 12}, "...": "..."}},
"findings": ["radare2 never reaches bytes 0x76-0x9a inside grade that the other graph builders decode", "..."]
}
},
"ir": {"translator": [{"text": "subq $0x10, %rsp", "flags": {"pcodeLike": "eager", "vexLike": "thunk"}, "...": "..."}], "observed": {"...": "..."}}
}
کد code/main.py را به این ترتیب بخوانید:
- تابع
read_text_symbols()هدر بخشها و نمادهایSTT_FUNCمربوط به.textرا میخواند. جدول نمادها تنها دادهی واقعی (ground truth) این بستر است و متدTextSection.gaps()فواصل پدینگ را از روی آن استخراج میکند. - توابع
parse_objdump()،parse_r2_family()،parse_angr()وparse_ghidra()هر خروجی ثبتشده را به یک شیءToolViewتبدیل میکنند که شامل آدرس مبنا، دستورالعملها (بهصورت نگاشت آفست به طول) و برای هر تابع یکFunctionViewشامل بلوکها، یالها و متن دیکامپایلشده است. هر پارسر فقط واژگان اختصاصی همان ابزار را میشناسد و نه چیزی بیشتر. - توابع کمکی
majority()وranges()در تمام مقایسهها به کار میروند. - تابع
function_report()ابتدا نقاط شروع دستورات را با نظر اکثریت سازندگان گراف مقایسه میکند، سپس به سراغ تعداد بلوکها و مجموعهی یالها رفته و موارد ناموجود در هر ابزار را مشخص میکند. - تابع
byte_report()وضعیت تکتک بایتها را در دستههای کد درون نماد، پدینگ، نامشخص، ساختگی یا جاافتاده ثبت میکند و بایتهای درون نمادها را که هیچ ابزار گرافی رمزگشایی نکرده معرفی میکند. - تابع
findings()نتایج را به جملات توصیفی تبدیل میکند وcompare()گزارش کامل هر آبجکت را سرهم میکند. - توابع
decode_small()،to_pcode_like()وto_vex_like()بخش مترجم را تشکیل میدهند: سه نوع رمزگذاری در فرم ثبات، یکی با فلگهای بیدرنگ و دیگری در قالب تانک. - تابع
observed_ir()مقادیر مربوط به کدهای واسط ثبتشده را میسنجد وfigure_data()خط زمانی، جدول مراحل و شواهد نمایشی را آماده میکند.
سه دستور دیگر نیز تعبیه شده است: دستور python3 code/main.py compare FIXTURE.o گزارش یک آبجکت را چاپ میکند. دستور python3 code/main.py ir 4883ec10 دو فرم ترجمهشدهی یک دستور را نمایش میدهد. دستور python3 code/main.py figure-data دادههای نمودار تصویری را چاپ میکند.
دادههای آزمایشی یک بار توسط اسکریپت code/fixtures/capture_tools.py ذخیره شدهاند که تمام دستورات و نسخهها را در captures.json نگه میدارد. بستر و تستها هیچ ابزاری را اجرا نمیکنند. اگر ابزارها را روی سیستم دارید، با اجرای اسکریپت capture فایلها بهروزرسانی میشوند؛ برای اطلاع از محیط مورد نیاز به راهنمای موجود در پوشهی fixtures مراجعه کنید.
استفاده از ابزارهای واقعی
روی یک فایل آبجکت جدید، بستر آزمایشی را به این ترتیب به کار بگیرید:
- آبجکت را خودتان کامپایل کنید و دستور کامپایل را نگه دارید. پیش از باز کردن هر ابزاری، جدول نمادهای آن را بخوانید تا بدانید چند تابع وجود دارد و فواصل پدینگ در کجاست.
- هر ابزار را یک بار در حالت خط فرمان (headless) اجرا و خروجی را ذخیره کنید. خروجی خام و دستور دقیق اجرا را کنار فایل آبجکت نگه دارید. اختلافی که نتوان آن را دوباره بازخوانی کرد، یافتهی معتبری نیست.
- آدرس مبنا را بررسی کنید. یک تابع را در نظر بگیرید و مطمئن شوید همهی ابزارها پس از کسر آدرس مبنای
.text، آفست یکسانی برای آن نشان میدهند. اگر اینطور نبود، متوقف شوید و ببینید آن ابزار چه چیزی را لود کرده است. - اول از همه شروع دستورالعملها را بسنجید. اگر ابزارهای مبتنی بر گراف در این مرحله با هم اختلاف داشته باشند، اختلاف در مراحل بعدی ناشی از رمزگشایی است نه تحلیل.
- بلوکها و یالها را مقایسه کنید و پیش از قضاوت روی اعداد، یادداشتهای هر ابزار را بخوانید. وجود یک
switch_op، یکCOMPUTED_JUMPیا یک گره غیرقابلحل، دلیل بیشتر تفاوتها را روشن میکند. - بایتها را دستهبندی کنید. پدینگی که یک ابزار به آن کد نسبت میدهد، حاصل روشهای اکتشافی آن است. بایتهای درون نماد که نادیده گرفته شدهاند، نشانهی توقف در پیمایش کد هستند. بایتهای درون نماد که هیچ ابزاری سراغشان نرفته، داده هستند.
- ابزارهایی را که اجرا نکردهاید یادداشت کنید. توصیف مبتنی بر مستندات ارزش خود را دارد، اما جنس شواهد آن با اجرای مستقیم متفاوت است.
کارگاه تعاملی
آزمایش الف: یک تابع، چهار آدرس مبنا
از پوشهی code محیط پایتون را باز کنید:
cd phases/13-tools-and-protocols/37-the-reverse-engineering-landscape/code
python3 -q
کدهای زیر را اجرا کنید:
from main import *
views = load_views("ledger-x86_64-O1.o")
{tool: hex(view.base) for tool, view in views.items()}
{tool: hex(view.functions["grade"].start + view.base) for tool, view in views.items()}
پیش از اجرای خط دوم، چهار آدرس خام را یادداشت کنید و آنها را با جدول این درس تطبیق دهید. سپس به جای grade تابع grade_label را قرار دهید و جایگاه آن در Ghidra را بهصورت دستی محاسبه کنید.
آزمایش ب: بررسی جدول پرش از چهار زاویه
report = compare("ledger-x86_64-O0.o")
report["functions"]["grade"]["blocks"]
report["functions"]["grade"]["edgesMissingPerTool"]["radare2"]
report["notes"]["rizin"]
report["notes"]["ghidra"]
report["bytes"]["radare2"]["misses"]
یالهایی را که radare2 جا انداخته بشمارید و هر کدام را با خطی از لیستینگ -O0 در درس 33 تطبیق دهید. سپس یادداشت Ghidra را بخوانید و برچسب switchD را در دادههای ثبتشدهی Ghidra پیدا کنید.
آزمایش ج: فلگهای بیدرنگ در برابر ساختار تانک
translate("48 83 ec 10")["pcodeLike"]
translate("48 83 ec 10")["vexLike"]
translate("83 f9 04")["vexLike"]
{row["name"]: (row["pcode"]["ops"], row["vex"]["statements"]) for row in observed_ir()["instructions"]}
ثابت cc_op را در هر دو خروجی پیدا کنید و توضیح دهید چرا در یکی برابر با 8 و در دیگری برابر با 7 است. سپس دستور translate("48 01 c8") را اجرا کنید و پیام خطای TranslateError را بخوانید.
آزمایش د: جدول تله در پنج ابزار
report = compare("sweep-trap.o")
report["functions"]["lookup"]["instructionStarts"]
report["bytes"]["dataInsideSymbols"]
report["functionStarts"]["angr"]
بایتهای 31 c0 90 48 83 c0 01 را ابتدا به روش پیمایش خطی روی کاغذ رمزگشایی کنید، سپس به روش فرود بازگشتی بعد از دستور jmp بنویسید. این دو لیستینگ را با شروعهای اضافی و جاافتادهای که سیستم گزارش میدهد مقایسه کنید.
تمرین عملی
یک ابزار پنجم را ثبت و بستر ارزیابی را برای آن توسعه دهید.
الزامات:
- یکی از ابزارهایی را که در این درس ثبت نشده و روی سیستمتان قابل اجراست انتخاب کنید: ابزار GNU
objdump، ابزار LIEF با Capstone، یک خروجی دستی از نسخهی Binary Ninja Free، یا Ghidra با تنظیمات متفاوتِ آنالایزر. خروجی آن را برای هر چهار آبجکت ثبت کرده و دستور و نسخهی آن را درcaptures.jsonوارد کنید. - یک تابع پارسر بنویسید که این داده را به یک
ToolViewشامل آدرس مبنا، دستورالعملها، توابع، بلوکها و یالها تبدیل کند. - ابزار را به لیست
TOOLSو در صورت تولید گراف بهDESCENT_TOOLSاضافه کنید. دمو را اجرا کنید و تغییرات در نظر اکثریت را بررسی کنید. - تستی بنویسید که تعداد بلوکهای این ابزار را روی
ledger-x86_64-O1.oبسنجد و تست دیگری برای نحوهی برخورد آن با جدول داده در فایل trap اضافه کنید. - یک بند توصیفی به درس اضافه کنید که بیان کند این ابزار با چه عناوینی از مراحل لودر، دیساسمبلر و بازیابی CFG یاد میکند.
معیار موفقیت این است که اجرای python3 code/main.py ابزار جدید را در بخش captureTools نمایش دهد، برای همهی توابع فایل ledger-x86_64-O1.o مقدار allDescentToolsAgree همچنان true بماند، و گزارش مربوط به sweep-trap.o رفتار ابزار جدید با بایتهای 0x10 تا 0x14 را بهروشنی نشان دهد.
خروجی این درس
این درس شامل فایل outputs/skill-the-reverse-engineering-landscape.md است. از آن برای اجرای ابزارهای مختلف روی آبجکتی که خودتان ساختهاید استفاده کنید، آدرسها را به یک مبنا تقلیل دهید و در قالب گزارشهایی برای هر تابع نشان دهید در شروع دستورها، بلوکها و یالها کجا اتفاق نظر دارند و کدام بایتها ساختگی یا جاافتاده هستند. این فرآیند مستلزم ثبت دقیق دستورات و نسخهها، خواندن جدول نمادها از فایل و تفکیک ابزارهای اجراشده از ابزارهای توصیفشده بر اساس مستندات است.
ارزیابی و اعتبارسنجی
اجرای دمو و تستهای قطعی:
cd phases/13-tools-and-protocols/37-the-reverse-engineering-landscape
python3 code/main.py
python3 -m unittest discover -s code/tests -v
اعتبارسنجی باید موارد زیر را اثبات کند:
- پارسر ELF چهار نماد تابع در
ledger-x86_64-O1.oبا اندازههای 16، 46، 31 و 63 و دو فاصلهی پدینگ پیدا کند. - دادههای ثبتشده از پیمایش خطی برای چهار آبجکت به ترتیب شامل 86، 56، 84 و 11 دستور باشند.
- ابزارهای radare2 و rizin برای فایل
ledger-x86_64-O1.oدستورات، بلوکها و یالهای کاملاً یکسانی گزارش دهند و فقط radare2 خروجی متنیpdcداشته باشد. - ابزار angr دو تابع بدون نماد به نامهای
sub_40003eوsub_40005fبسازد. - بلوکهای Ghidra برای تابع
totalهمان پنج بلوک درس 33 باشند و انشعابهای فراخوانی جزو یالهای گراف به شمار نیایند. - همهی ابزارها پس از کسر مبنا، تابع
gradeرا در آفست0x10قرار دهند. - همهی ابزارها در تکتک توابع
ledger-x86_64-O1.oدر مورد شروع دستورالعملها اتفاق نظر کامل داشته باشند. - چهار ابزار مبتنی بر گراف به ترتیب 3، 3، 5 و 4 بلوک و 2، 3، 5 و 4 یال را با مجموعهی یالهای یکسان گزارش کنند.
- تابع
totalدر بهینهسازی-O2در تمام ابزارهای گرافی دارای 10 بلوک و 13 یال باشد. - تابع
gradeدر بهینهسازی-O0در ابزارهای radare2، rizin، angr و Ghidra به ترتیب 3، 8، 8 و 9 بلوک و 2، 11، 7 و 12 یال داشته باشد و دلایل آن در یادداشتهای هر ابزار موجود باشد. - تابع
grade_labelدر بهینهسازی-O0در تمام ابزارها یک بلوک باشد، بهجز angr که بلوک را در محل دستور فراخوانی پایان میدهد. - ابزار angr دقیقاً یک تابع ساختگی در هر فاصلهی پدینگ و یکی روی جدول داده ایجاد کند و هیچ ابزار دیگری تابع ساختگی یا جاافتاده نداشته باشد.
- پیمایش خطی شروعهای
0x10،0x12و0x13را اضافه کرده و0x14را جا بیندازد، و هیچ سازندهی گرافی بازهی0x10تا0x14را رمزگشایی نکند. - گزارشها بازهی جاافتاده در radare2 و توابع ساختگی در angr را نام ببرند.
- مانیفست (Manifest) برای هر 21 فایل ذخیرهشده نسخه و دستور اجرا را ثبت کرده باشد.
- سه ابزار تابع
gradeرا دیکامپایل کنند و فقط Ghidra الگوی محاسباتی تقسیم را سادهسازی کند. - مترجم در یک حالت فلگهای CF، OF، SF، ZF، PF را بیدرنگ بنویسد و در حالت دیگر یک تانک
cc_opبسازد؛ برایcmpثباتی را بازنویسی نکند و برایmovslqفلگی تغییر ندهد. - مترجم از پذیرش دسترسی به حافظه، آپکدهای متفرقه، بیتهای اضافی REX و طولهای اشتباه خودداری کند.
- مقادیر
cc_opمترجم برای اعداد 8 و 7 دقیقاً با مقادیر ثبتشده در VEX برابر باشند. - دادههای ثبتشده برای سه دستور به ترتیب 9، 1 و 10 عملیات P-code و 7، 4 و 7 عبارت VEX باشند.
- خط زمانی از 1973 تا 2026 به ترتیب مرتب باشد، پایپلاین دارای هشت مرحله و شش ابزار با اصطلاح دقیق در هر خانه باشد، و دو ابزار تجاری برچسب اجرا نشدن داشته باشند.
- تصویر تعاملی دقیقاً همان دادههای تولیدشده توسط بستر آزمایشی را نمایش دهد.
حالتهای شکست در محیط عملیاتی
| حالت شکست (Failure mode) | گزارش ضعیف چه میگوید | آنچه بستر ارزیابی باید اثبات کند |
|---|---|---|
| مقایسهی آدرسهای خام ابزارهای مختلف | «ابزارها بر سر محل تابع grade اختلاف دارند» | رسیدن به یک آفست یکتا پس از تفریق مبنای هر ابزار، سنجیدهشده روی یک تابع |
| تلقی تعداد بلوکها بهعنوان معیار حجم کد | «Ghidra بیشترین حجم کد را کشف کرده است» | انطباق کامل در نقاط شروع دستورالعملها، همراه با توضیح بلوک اضافه ناشی از برچسب |
| در نظر گرفتن یک ابزار بهعنوان مرجع مطلق | «خروجی objdump نشان میدهد بایتهای جدول دستورالعمل هستند» | ماهیت پیمایش خطی objdump؛ سه ابزار گرافی آن بایتها را دستنخورده رها کردهاند |
| در نظر گرفتن توابع پدینگ بهعنوان کشفیات جدید | «angr توانسته هفت تابع بیشتر پیدا کند» | همهی آنها در فواصل پدینگ یا روی دادهها قرار دارند و هیچکدام نماد معتبری ندارند |
| گزارش توقف در پیمایش بهعنوان کد مرده | «چهار بلوک مربوط به caseها غیرقابل دسترسی هستند» | 36 بایت کد که یک ابزار جا انداخته اما سه ابزار دیگر به آن رسیدهاند |
| مقایسهی تعداد یالها بدون خواندن یادداشتها | «rizin و Ghidra در تحلیل grade اختلاف دارند» | وجود مقاصد محاسباتی یکسان، همراه با شکستهشدن یک بلوک در محل برچسب |
| تلقی محاسبات دیکامپایلر بهعنوان منطق برنامه | «تابع مقدار ورودی را در عدد 1717986919 ضرب میکند» | این الگو همان شیوهی محاسباتی درس 34 است که یک ابزار آن را ساده کرده و دیگری رها کرده است |
| توصیف ابزار غیرمتنباز بهگونهای که گویی اجرا شده | «Binary Ninja تعداد پنج بلوک را گزارش میدهد» | نمودار و جدول باید بهروشنی نشان دهند که بررسی صرفاً بر اساس مستندات بوده است |
| ثبت خروجیها بدون ذکر دستورات اجرا | «ما radare2 را اجرا کردیم» | وجود مانیفست کامل شامل نسخه، دستور و تاریخ برای تکتک فایلها |
| تلقی کدهای واسط بدون فلگ بهعنوان تبدیل ناقص | «لیفتر VEX فلگها را حذف کرده است» | نگهداری وضعیت در تانکِ cc_op، cc_dep1، cc_dep2 و ارزیابی آن هنگام انشعاب |
دستورالعمل اجرایی
خروجی هر ابزار را یک بار همراه با نسخه و دستور دقیق آن ثبت کنید، تمام آدرسها را به یک مبنای یکتا تقلیل دهید، و نقاط شروع دستورالعملها را پیش از بلوکها و بلوکها را پیش از یالها بسنجید. وضعیت تکتک بایتها را مشخص کنید: کد واقعی، پدینگ، داده یا کد جاافتاده. پیش از قضاوت دربارهی تعداد اجزا، یادداشتهای داخلی ابزار را بخوانید و ابزارهایی را که صرفاً از روی مستندات بررسی کردهاید شفاف سازید.
برای مطالعهی بیشتر
- سیفوئنتس، «تکنیکهای ترجمهی معکوس»، رسالهی دکتری، QUT (1994)
- سیفوئنتس و گاف، «دیکامپایل برنامههای باینری»، Software: Practice and Experience (1995)
- ون امریک، «فرم انتساب تکایستا برای دیکامپایل»، رسالهی دکتری، دانشگاه کوئینزلند (2007)
- شوارتز، لی، وو، براملی، «دیکامپایل بومی x86 با تحلیل ساختاری حافظ معنا و ساختاربندی تکراری جریان کنترل» (USENIX Security 2013)
- یکدان و دیگران، «پایان گوتو» (NDSS 2015)
- باسک و دیگران، «درود بر SAILR! دیگر نیازی به رویاپردازی در C نیست» (USENIX Security 2024)
- آندریسه و دیگران، «تحلیل عمیق دیساسمبل باینریهای واقعی x86/x64» (USENIX Security 2016)
- فلیک، «مقایسهی ساختاری آبجکتهای اجرایی» (DIMVA 2004)
- مارانزانو و بورن، «راهنمای مقدماتی ADB»، آزمایشگاههای بل (1977)
- استالمن، سخنرانی KTH پیرامون آغاز توزیع GDB (30 اکتبر 1986)
- هگز-ریز، «IDA: گرامیداشت 30 سال نوآوری در تحلیل باینری»
- گویلفانوف، «سازوکار درونی دیکامپایلر: مایکروکد»، Recon بروکسل 2018 (اسلایدها)
- هگز-ریز، نمای مایکروکد و سطوح تکاملی آن
- هگز-ریز، یادداشتهای انتشار IDA 9.4
- هگز-ریز، نسخهی رایگان IDA Free
- انتشارهای Ghidra (نسخهی 12.1.4)
- مرجع P-code در Ghidra
- کد منبع دیکامپایلر Ghidra، جریان کاری اصلی و کلاسهای Action
- مستندات analyzeHeadless در Ghidra
- باینری نینجا، نمای کلی BNIL
- باینری نینجا، «تشخیص توابع در باینریها بدون وابستگی به معماری» (2017)
- باینری نینجا، پردازش دستهای و مجوزهای headless
- پست انتشار Binary Ninja 6.0 "Krypton"
- نسخهی رایگان Binary Ninja Free
- کتاب radare2، بخش ESIL
- کتاب radare2، بخش دیکامپایلرها
- انتشارهای radare2 (نسخهی 6.2.4)
- ریزین، یادداشتهای طراحی RzIL
- انتشارهای rizin (نسخهی 0.9.1)
- اعلامیهی معرفی Rizin (8 دسامبر 2020)
- انتشارهای Cutter (نسخهی 2.5.0)
- مستندات angr، بازیابی CFG
- مستندات angr، دیکامپایلر و فازهای تحلیلی آن
- مستندات angr، بازنمایی واسط (VEX)
- بستهی angr در PyPI (نسخهی 10.0.3)
- بستهی pypcode در PyPI
- راهنمای دستور llvm-objdump
- دفترچهی راهنمای GNU objdump
- مستندات LIEF
- انتشارهای موتور Capstone
- انتشارهای BinDiff (نسخهی 8 متنباز)
- پروژهی Diaphora
- صفحهی دانلود دیساسمبلر Hopper
- وبسایت Compiler Explorer
- معرفی LLDB بهعنوان زیرپروژهی LLVM (2010)
- دیکامپایلر Boomerang
- تان و دیگران، «LLM4Decompile» (EMNLP 2024)
- گائو و دیگران، «DecompileBench» (Findings of ACL 2025)
- لیو، راف، میسینسکی، «وقتی دیکامپایلرهای مبتنی بر LLM بیشتر بازکامپایل میکنند و کمتر ساختار را حفظ میکنند» (2026)
