ویرگول
ورودثبت نام
سید عمید قائم مقامی
سید عمید قائم مقامیبرنامه نویسی سیستم ویندوز و مهندسی معکوس و علاقه مند به آموزش.
سید عمید قائم مقامی
سید عمید قائم مقامی
خواندن ۸ دقیقه·۳ ساعت پیش

مهندسی معکوس(Manually unpacking a UPX):

باینری پک‌شده چیست؟

قبل از اینکه بفهمیم باینری پک‌شده چیست، باید بدانیم پَکر (Packer) چیست. پکر نوعی نرم‌افزار است که هم توسط نویسندگان بدافزار و هم توسط افرادی که می‌خواهند مالکیت فکری کد خود را محافظت کنند استفاده می‌شود.
پکر با مبهم‌سازی (Obfuscation) کد و فشرده‌سازی باینری کار می‌کند.

معمولاً پِی‌لود (Payload) داخل باینری یا رمزگذاری یا رمزنگاری می‌شود و در زمان اجرا (Run Time) دیکد یا دیکریپت شده و سپس اجرا می‌شود.
فرآیند پَک کردن معمولاً مراحل ساده‌شده زیر را دنبال می‌کند:

  1. پکر، محتوای باینری یا پِی‌لود را فشرده می‌کند.

  2. سپس پکر یک Unpacking Stub اضافه می‌کند؛ کدی که هنگام اجرا فراخوانی می‌شود تا باینری را آن‌پک کرده و اجرای برنامه را به OEP (Original Entry Point) بازگرداند.

  3. بعد پکر Entry Point باینری را طوری تغییر می‌دهد که به محل Unpacking Stub اشاره کند.

  4. در نهایت، پکر یک باینری پک‌شده تولید می‌کند.


Unpacking Stub (بخش بازگشایی‌کننده)

وقتی یک فایل دودویی (Binary) اجرا می‌شود، سیستم‌عامل ابتدا Unpacking Stub را بارگذاری می‌کند؛ این بخش وظیفه دارد برنامهٔ اصلی را از حالت فشرده خارج کند و سپس آن را بارگذاری نماید.

کد OEP (Original Entry Point) یا همان «نقطه ورود اصلی»، به مکانی اشاره می‌کند که Unpacking Stub در آن ذخیره شده است. یعنی وقتی OEP اجرا می‌شود، پردازنده به ناحیهٔ دیگری از کد می‌پرد که قرار است فایل اصلی را از حالت فشرده خارج کند و سپس اجرای آن را ادامه دهد.


آن‌پک کردن دستی (Manual Unpacking)

در آن‌پک کردن دستی، ما باینری پک‌شده را اجرا می‌کنیم تا خود Unpacking Stub آن را برای ما آن‌پک کند. سپس فرآیند درحال‌اجرا را dump کرده و هدر PE را دستی اصلاح می‌کنیم. مراحل کلی عبارت‌اند از:

  1. تشخیص OEP؛ یعنی اولین دستور قبل از این‌که برنامه پک شود. باید دستور یا جهشی را پیدا کنیم که از داخل باینری پک‌شده به OEP می‌رود.

  2. اجرای برنامه تا زمانی که به OEP برسیم؛ در این مرحله برنامه خودش را در حافظه آن‌پک کرده و اجرای آن در OEP متوقف می‌شود. این‌جا دقیقاً قبل از اجرای کد آن‌پک‌شده قرار می‌گیریم.

  3. سپس می‌توانیم پروسهٔ آن‌پک‌شده در حافظه را dump کرده و آن را روی دیسک ذخیره کنیم.

  4. در نهایت، باید IAT (Import Address Table) فایل dump‌ شده را اصلاح کنیم تا importها دوباره به‌ درستی resolve شوند.

چگونه تشخیص دهیم که یک فایل دودویی (Binary) پک شده است؟

هنگام انجام ارزیابی اولیه، برخی نشانه‌ها وجود دارند که می‌توانند نشان دهند فایل مورد نظر پک شده است.

  1. آنتروپی بالای فایل (High Entropy)
    اولین نشانه، وجود آنتروپی کلی بالا در فایل است. آنتروپی معیاری برای سنجش تصادفی بودن داده‌ها است و هرچه آنتروپی بالاتر باشد، داده‌ها تصادفی‌تر بوده و معمولاً به این معناست که کدگذاری یا رمزگذاری شده‌اند.
    یک قاعدهٔ کلی این است که اگر آنتروپی فایل 6.5 یا بالاتر باشد، این می‌تواند نشان دهد که نمونه احتمالاً پک شده است.

  2. امضای فایل (Signature)
    نشانهٔ دیگری که به راحتی قابل تشخیص است، امضای فایل است. به عنوان مثال، اگر فایل دارای امضای UPX باشد، این می‌تواند نشان دهد که فایل با UPX پک شده است.

با نگاه کردن به نقطهٔ ورود (Entry Point) نمونه، می‌توان دید که این نقطه به بخش (Section) با نام UPX1 در آدرس 0x00017B30 اشاره می‌کند.

معمولاً نقطهٔ ورود یک فایل دودویی، به بخش text اشاره دارد که کد برنامه در آن ذخیره شده است. اما در این نمونه، نقطهٔ ورود به Unpacking Stub اشاره می‌کند که ابتدا فایل را از حالت فشرده خارج می‌کند و سپس کد آنپک‌شده را اجرا می‌کند.

با نگاه کردن به سکشن‌ها، می‌بینم که یک سکشن دیگر به نام UPX0 نیز وجود دارد. نکتهٔ جالب این است که این سکشن اندازهٔ خام (Raw Size) برابر ۰ بایت دارد، اما اندازهٔ مجازی (Virtual Size) آن 49152 بایت است، که کاملاً غیرمعمول است.

همچنین این سکشن دارای ویژگی‌های قابل‌اجرا (Executable)، قابل‌نوشتن (Writeable)، خودتغییر‌دهنده (Self‑modifying) و مجازی‌سازی‌شده (Virtualized) است؛ که نشان می‌دهد قرار است داده‌هایی در این بخش نوشته شود.

سکشن UPX1 (که نقطهٔ ورود برنامه نیز در آن است) فایل را آنپک می‌کند و کدِ آنپک‌شده را در سکشن UPX0 می‌نویسد و سپس اجرای برنامه را به UPX0 منتقل می‌کند.


روش اول: آنپک کردن با استفاده از دستورهای pushad و popad

در روش اول برای آنپک‌کردن دستی این باینری، باید به دنبال یک دستور pushad بگردیم و اجازه دهیم خود باینری عملیات آنپک را انجام دهد.

ابتدا لازم بود آدرس حافظهٔ سکشن UPX0 را پیدا کنم؛ این همان بخشی است که استابِ بازگشایی (unpacking stub) کد را در آن آنپک می‌کند.
این آدرس 00401000 است.

گام بعدی این است که دستور pushad را پیدا کنیم. این دستور در واقع OEP (Original Entry Point) باینری است و برای ریختن همهٔ رجیسترهای ۳۲ بیتی روی استک استفاده می‌شود.

این کار انجام می‌شود تا وقتی که unpacking stub باینری را آنپک کرد، بتواند با استفاده از دستور popad دوباره این رجیسترها را بازیابی کرده و به همین مکان بازگردد. سپس من کلید F7 را زدم تا این دستور اجرا شود.

بعد از اجرای این دستور، به پنجرهٔ رجیسترها (Registers Window) رفتم و روی مقدار رجیستر ESP (که برابر 0019FF54 بود) راست‌کلیک کردم و آن را در Dump دنبال کردم. این مقدار Stack Pointer است و به آیتم بعدی روی استک اشاره می‌کند.

در پنجرهٔ Dump، روی آدرسی که در رجیستر ESP بود (0019FF54) راست‌کلیک کردم و یک Breakpoint سخت‌افزاری (Hardware Breakpoint) برای زمانی که DWORD دسترسی پیدا می‌کند، تنظیم کردم.

این آدرس همان محلی است که بلافاصله پس از اجرای دستور popad و بازگردانی مقادیر رجیسترها، مورد دسترسی قرار می‌گیرد.

این اتفاق زمانی رخ می‌دهد که unpacking stub عملیات آنپک کردن باینری را تمام کرده و اجرای برنامه به OEP منتقل شده تا کد آنپک‌شده اجرا شود.

در این مرحله، من یک Breakpoint روی آدرس حافظهٔ بعدی که قرار است پس از آنپک شدن باینری دسترسی پیدا کند، قرار داده‌ام.

در این نقطه می‌توانم کلید F9 را فشار دهم و اجرای برنامه را دقیقا قبل از کد آنپک‌شده متوقف کنم.

بعد از فشار دادن F9، من به یک Breakpoint روی دستور بعد از popad رسیدم. از این نقطه به بعد، باینری خودش را آنپک کرده و کد آنپک‌شده را در سکشن UPX0 نوشته است.

گام بعدی این است که به دنبال Tail Jump بگردیم، که در واقع یک دستور Jump است که به آدرسی در محدودهٔ سکشن UPX0 می‌رود.

من به یاد دارم که آدرس حافظهٔ سکشن UPX0 برابر 00401000 بود و چند دستور پایین‌تر از دستور popad، یک Jump وجود دارد که به آدرس حافظه 401C50 می‌رود؛ این آدرس در محدودهٔ سکشن UPX0 قرار دارد.

این بدان معناست که این دستور Jump ما را به کد آنپک‌شده خواهد برد. در این نقطه، من یک Breakpoint روی این دستور قرار دادم.

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

روش دوم: آنپک کردن با استفاده از Tail Jump

روش دوم برای آنپک کردن دستی این باینری، به‌طور مستقیم به دنبال یک Tail Jump بگردیم.

اولین گام در این روش، پیدا کردن دستور pushad است که همان نقطهٔ ورود (Entry Point) این برنامه می‌باشد.

سپس، همانند قبل، بررسی کردم که آدرس حافظهٔ سکشن UPX0 کجاست که برابر 00401000 بود.

این همان مکانی است که کد آنپک‌شده در آن نوشته خواهد شد.

سپس با استفاده از CTRL+F، دستور popad را پیدا کردم.

این دستور برای بازگرداندن مقادیر رجیسترها پس از آنپک شدن باینری و بازگشت به OEP استفاده می‌شود.

درست زیر دستور popad، با استفاده از همان روشی که قبلاً به کار بردم، توانستم Tail Jump را پیدا کنم که به آدرس حافظه 401C50 در محدودهٔ سکشن UPX0 می‌پرد.

مشابه قبل، من یک Breakpoint روی این دستور قرار دادم و حالا کلید F7 را زدم تا به داخل این دستور Jump قدم بگذارم.

اکنون ما در کد آنپک‌شده قرار داریم و می‌توانیم فرآیند Dump کردن را شروع کنیم.


استفاده از Scylla برای Dump کردن فرآیند آنپک‌شده

در x64dbg، یک Plugin به نام Scylla وجود دارد و من از آن برای Dump کردن فرآیند آنپک‌شده استفاده کردم.

وقتی این پلاگین را باز می‌کنید، باید صفحه‌ای مشابه تصویر زیر مشاهده کنید.

گام اول برای Dump کردن فرآیند آنپک‌شده: پیدا کردن و بازسازی جدول ایمپورت (Import Table)

این کار به ما اجازه می‌دهد تمام ایمپورت‌ها، آدرس‌های آن‌ها و اصلاح آدرس‌های مجازی (Virtual Addresses) را شناسایی کنیم.

برای جستجوی جدول IAT ابتدا روی دکمه IAT Autosearch کلیک کنید.

پس از اینکه جدول IAT پیدا شد، من روی Get Imports کلیک کردم تا ایمپورت‌هایی که در باینری استفاده شده‌اند را شناسایی کنم.

در تصویر زیر می‌توانیم لیستی از کتابخانه‌هایی که ایمپورت شده‌اند را ببینیم و درون آن‌ها، توابع ایمپورت‌شده قرار دارند.

حالا که جدول IAT پیدا شد و ایمپورت‌ها شناسایی شدند، من روی دکمه Dump کلیک کردم تا باینری آنپک‌شده را به فایل Dump کنم.

در این مرحله، اگر تلاش کنیم باینری را اجرا کنیم، یک خطا رخ می‌دهد زیرا آدرس‌های حافظه (Virtual Addresses) با آدرس‌های روی دیسک (Raw Addresses) متفاوت هستند.

برای رفع این مشکل، من روی دکمه Fix Dump کلیک کردم تا ناسازگاری این آدرس‌ها اصلاح شود.

پس از کلیک روی دکمه Fix Dump، Scylla یک باینری جدید با پسوند _SCY ایجاد می‌کند که شامل تمام آدرس‌های اصلاح‌شده و جدول IAT به‌روز شده است.

تحلیل باینری آنپک‌شده

حالا که باینری آنپک شده، هنگام تحلیل آن در PeStudio مشاهده کردم که آنتروپی باینری به ۵.۶ کاهش یافته است که نسبت به قبل به‌طور قابل توجهی پایین‌تر است.

این نشان می‌دهد که باینری دیگر پک نشده.
علاوه بر این، امضای UPX نیز دیگر وجود ندارد.

با نگاه کردن به نقطهٔ ورود (Entry Point) باینری، اکنون مشاهده می‌کنیم که به آدرس موجود در ناحیهٔ UPX0 تغییر یافته است.

این به این دلیل است که unpacking stub توانسته است باینری را در این مکان آنپک کند، جایی که OEP قرار دارد.

با نگاهی سریع به سکشن‌ها، مشاهده می‌کنیم که UPX0 دیگر اندازهٔ خام (Raw Size) برابر صفر ندارد و در واقع اندازهٔ خام و مجازی (Virtual Size) بسیار نزدیک به هم هستند.

تفاوت قابل توجه دیگری نیز، افزوده شدن سکشن .SCY است که توسط Scylla هنگام Dump کردن باینری از x64dbg ایجاد شده است.

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

اما پس از آنپک کردن، تمام ایمپورت‌ها در PeStudio فهرست شده‌اند و مجموعاً ۹۰ مورد ثبت شده است.

Telegram: @CaKeegan
Gmail : amidgm2020@gmail.com

windows
۰
۰
سید عمید قائم مقامی
سید عمید قائم مقامی
برنامه نویسی سیستم ویندوز و مهندسی معکوس و علاقه مند به آموزش.
شاید از این پست‌ها خوشتان بیاید