Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
packer-tutorial — درس تعليمي حول كيفية كتابة أداة packer لـ Windows! | Kitploit
أدوات/GitHubGitHub/frank2/packer-tutorial
الهندسة العكسيةتحليل البرمجيات الخبيثةتحليل الملفات الثنائيةالتعلم والتعليم
GitHubfrank2/packer-tutorial

packer-tutorial

درس تعليمي حول كيفية كتابة أداة packer لـ Windows!

عرض المستودع
31932منذ 2 سنواتتمت المراجعة من قبل Kitploit

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

PACKERS

جدول المحتويات

  1. ما هو المُعبئ؟: مقدمة إلى الغرض من المُعبئات ودليل عبر ما هي ضروريات تطوير المُعبئ.
  2. المتطلبات الأساسية: الأدوات اللازمة للعمل مع هذا البرنامج التعليمي.
  3. تجربة سريعة: عرض توضيحي لما ستبنيه باستخدام هذا البرنامج التعليمي.
  4. رسم مشروع CMake الخاص بنا: درس مصغّر عن CMake حول إنشاء نظام بناء يلبي الاحتياجات المعقدة نوعًا ما للمُعبئ.
  5. تعبئة الملفات الثنائية داخل الستب الخاص بك: درس حول كيفية كتابة جزء المُعبئ من مزيج المُعبئ/الستب، بالإضافة إلى مقدمة إلى تنسيق ملفات ويندوز القابلة للتنفيذ.
    1. إدارة الموارد
    2. تحليل ملف PE
    3. التلاعب بملف PE
  6. محاكاة المُحمّل: درس حول كيفية بناء برنامج ستب تنفيذي بسيط لفك ضغط وتحميل برنامج تنفيذي مستهدف، بالإضافة إلى مقدمة إلى معالجة أكثر تقدمًا لملفات ويندوز القابلة للتنفيذ.
    1. قراءة ملف PE الخاص بنا من الذاكرة
    2. تحميل ملف PE الخاص بنا للتنفيذ
    3. حل استيرادات API
    4. حل العناوين
    5. نقل التنفيذ
  7. تمارين إضافية: بعض التمارين لتوسيع معرفتك بتطوير المُعبئات.

هل يبدو الأمر قراءة شاقة؟ جرّب نسخة العرض التقديمي، الذي يلخّص هذا المستند. فيديو يوتيوب قريبًا!

ما هو المُعبئ؟

المُعبئ هو برنامج يقوم بفك ضغط وتشغيل برنامج آخر داخل مساحة العناوين الخاصة به (أو أحيانًا، مساحة عناوين عملية أخرى). يُعرف أحيانًا بكونه الوسيلة التي تهاجم بيئات التحليل، مثل المصححات والصناديق الرملية الافتراضية. يُستخدم أساسًا لأغراض قليلة:

  • الضغط: تُستخدم المُعبئات عادةً لضغط كود برنامج معين. هذا أحد استخداماتها المشروعة القليلة. انظر UPX كمثال على مُعبئ ضاغط.
  • التعتيم: تُستخدم المُعبئات أيضًا عند محاولة تعتيم برنامج أو حمايته من الهندسة العكسية. انظر مُعبئ packman من Riot Games كمثال على مُعبئ مضاد للهندسة العكسية.
  • المراوغة: تستخدم البرمجيات الخبيثة غالبًا مجموعة متنوعة من المُعبئات من أجل مراوغة برامج مكافحة الفيروسات وحتى EDR. انظر هذا التحليل لمُعبئ SmokeLoader كمثال على مُعبئ مراوغ.

أساسيًا، يمر المُعبئ بعدد قليل من الخطوات الأساسية:

  1. مرحلة الضغط: هنا يتم ضغط البرنامج التنفيذي الأصلي أو تعتيمه أو كلاهما، في ملف ثنائي جديد.
  2. مرحلة فك الضغط: هنا يقوم الملف التنفيذي المُعبأ بفك ضغط أو إزالة التعتيم عن ملفه التنفيذي الأصلي لتحميله.
  3. مرحلة التحميل: هنا يحاكي المُعبئ مجموعة من الخطوات المشابهة لمُحمّل الملفات التنفيذية في النظام المضيف. بسبب تعقيد الملفات التنفيذية، تعد هذه الخطوة الأكثر تعقيدًا، اعتمادًا على مستوى العمق الذي ترغب في محاكاته.
  4. مرحلة التنفيذ: هنا ينتقل الكود من ملف الستب التنفيذي المضيف إلى الكود المحمّل حديثًا (أي غير المُعبأ).

وبالمثل، يتكوّن المُعبئ من بضعة أجزاء فقط:

  • المُعبئ: هذا الجزء مسؤول عن إعداد ما يسمى الملف التنفيذي الستب ومنحه البيانات اللازمة لفك ضغط ملف ثنائي معين. هذا هو في النهاية مرحلة الضغط.
  • الستب: هذا الملف الثنائي هو جزء الكود المسؤول عن فك ضغط وتحميل وتنفيذ الملف الثنائي الأصلي. كما ترى، يقوم ملف الستب التنفيذي بمعظم العمل الشاق للمُعبئ.

قد يكون بناء مُعبئ أمرًا معقدًا، لأن الستب يحتاج إلى البناء واستيراده بطريقة ما داخل الملف التنفيذي للمُعبئ. بالنسبة لويندوز، فإن تعلم نظام البناء الخاص بـ Visual Studio بما يتجاوز الترجمة البسيطة قد يكون مهمة مرهقة. لحسن الحظ، يوفر CMake نظام بناء بسيطًا ومتعدد المنصات يدعم Visual Studio، وهو قابل للتخصيص بدرجة كبيرة!

يهدف هذا البرنامج التعليمي إلى تعليم ما يلي:

  • كيفية تقسيم ودمج الجزأين الرئيسيين للمُعبئ في بيئة بناء متماسكة وقابلة للاختبار.
  • كيفية التعامل مع ملف ويندوز تنفيذي لإضافة بيانات إضافية قابلة للتحميل.
  • كيفية التنقل داخل ملف ويندوز تنفيذي لاسترجاع وتحميل بيانات عشوائية.
  • كيفية محاكاة أجزاء من مُحمّل ويندوز لتحميل وتنفيذ ملف ويندوز تنفيذي.

إذا كنت معتادًا بالفعل على C++ و CMake، فلا تتردد في الانتقال مباشرة إلى قسم التعبئة. وإلا، تابع القراءة!

المتطلبات الأساسية

  • معرفة بـ C++: هذا البرنامج التعليمي غير مفيد نوعًا ما إذا كنت لا تعرف C++، لأننا سنقوم بالكثير من الحسابات على المؤشرات.
  • Visual Studio: يحتوي Visual Studio على مترجم C++ لويندوز كامل الميزات. تم اختبار هذا المشروع مع Visual Studio 2019، ولكن الإصدارات الأحدث يجب أن تكون جيدة.
  • CMake: CMake هو نظام البناء الذي نستخدمه للمساعدة في تهيئة عمليات البناء الخاصة بنا لمترجم Visual Studio.

تجربة سريعة

أولًا، دعنا نثبت بطريقة ما أن نظام البناء هذا يعمل وينشئ ملفًا تنفيذيًا مُعبأ بشكل صحيح. بمجرد تثبيت CMake و Visual Studio، انتقل إلى الدليل الجذر لهذا المستودع باستخدام الطرفية التي تفضلها ونفّذ ما يلي:``` $ mkdir build $ cd build $ cmake ../

root@kitploit:~
سيؤدي هذا إلى إنشاء ملفات المشروع اللازمة لبناء كود البرنامج التعليمي الخاص بـ Packer. ثم قم بتشغيل:```
$ cmake --build ./ --config Release

سيؤدي هذا إلى بناء مشروع packer في وضع Release. ثم يمكنك تشغيل الاختبار التالي:``` $ ctest -C Release ./

root@kitploit:~
إذا سار كل شيء على ما يرام، يجب أن ينجح test\_pack و test_unpack. إذا أردت رؤية نتائج التعبئة بنفسك، يجب أن ترى هذا:```
$ ./packed.exe
I'm just a little guy!

لنتحدث عن جميع الأجزاء المتحركة التي جعلت هذا الأمر يتحقق.

رسم الخطوط العريضة لمشروع CMake الخاص بنا

إذًا نحن نعرف ما هي المكوّنات الرئيسية لأداة التغليف (packer)، لكن كيف يمكننا اختبار هذه الأداة فورًا كجزء من دورة التطوير؟ سنحتاج إلى إضافة ملف تنفيذي ثالث إلى مشروعنا العام لاختبار عملية التغليف وفك التغليف لملفنا التنفيذي بشكل صحيح. لذا نحتاج بشكل عام إلى ثلاثة مشاريع:

  • أداة التغليف (packer)
  • الوصلة (stub)
  • الملف التنفيذي التجريبي المراد تغليفه (dummy executable)

سنحتاج أيضًا إلى مكتبة ضغط لضمان أن الملف التنفيذي يُضغط داخل الملف التنفيذي للوصلة. ستكون zlib مناسبة تمامًا لهذا الغرض.

يجب أن يبدو هيكل مشروعنا، في البداية، على النحو التالي:``` packer/ +---+ CMakeLists.txt + dummy/ | | | +---+ CMakeLists.txt | + src/ | | | +---+ main.cpp | + stub/ | | | +---+ CMakeLists.txt | + src/ | | | +---+ main.cpp | + src/ | | | +---+ main.cpp | + zlib-1.2.13/ | +---+ CMakeLists.txt + ...

root@kitploit:~
يمكن أن يبدو ملف main.cpp لدينا في كل مجلد ببساطة على هذا النحو في الوقت الحالي:```cpp
#include <iostream>

int main(int argc, char *argv[]) {
    std::cout << "I'm just a little guy!" << std::endl;
    
    return 0;
}

بالفعل، يجب أن نثبت أن لدينا سلسلة تبعيات بسيطة للتعامل معها، والتي سيقوم CMake بحلّها بشكل جيد لنا مع بعض الإعدادات:

  • packer و stub يعتمدان على zlib
  • packer يعتمد على stub
  • dummy يعتمد على packer (لأنه يحتاج إلى أن يتم تعبئته بواسطة packer)

لنبدأ بالمشروع الجذري root project، أي packer، من أجل تجهيز CMake الخاص بنا.

عادةً ما يتطلب CMake حدًا أدنى من الإصدار للتعامل معه، لأنه موجود منذ فترة طويلة ويدعم الاستخدام طويل الأمد للإصدارات السابقة. بعد ذلك، يمكننا تعريف مشروع packer كمشروع C++.```cmake

target a cmake version, you can target a lower version if you like

cmake_minimum_required(VERSION 3.24)

declare our packer as a C++ project (since zlib is a C project and the compilation

detection might get confused)

project(packer CXX)

root@kitploit:~
نريد أيضًا تعريف الحزمة الخاصة بنا كـ "MultiThreaded" (أي /MT) بدلاً من "MultiThreadedDLL" (أي /MD) حتى لا نضطر للقلق بشأن تبعيات مكتبات الارتباط الديناميكي (DLL) في وقت التشغيل.```cmake
# this line will mark our packer as MultiThreaded, instead of MultiThreadedDLL
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")

العبارة الموجودة بين قوسين زاويتين في عبارة تعيين المتغير تُسمى تعبير مولد وتساعدنا في حل بيانات وقت الإعداد حيثما نحتاج إليها، وستراها كثيراً في هذا الملف. ما يفعله هذا التعبير المولد هو إصدار السلسلة Debug عندما يكتشف أن الإعداد هو Debug، ولا يصدر شيئاً في الحالات الأخرى. وهذا يوفر مكتبة وقت تشغيل باسم MultiThreadedDebug عند تحديد ملف تعريف ترجمة Debug، وMultiThreaded عند تحديد ملف تعريف ترجمة غير تصحيحي مثل Release. راجع تعبيرات المولد الشرطية لفهم جيد لما يحدث هنا. لمزيد من المعلومات حول المتغير CMAKE_MSVC_RUNTIME_LIBRARY، راجع توثيق CMake.

يتيح لك CMake تنظيم الكود المصدري الخاص بك في تسلسلات هرمية تماماً كما يفعل Visual Studio تلقائياً عند إنشاء مشاريع باستخدام واجهة المستخدم الخاصة به، كما يبحث بشكل متكرر في مجلداتك عن أسماء الملفات المطابقة. في ملف الإعداد الخاص بنا، قمنا بتعيين بحث متكرر عام على ملفات الرؤوس (.hpp) والكود (.cpp) وسكربتات الموارد (.rc). في مثالنا، نحتاج فعلياً إلى main.cpp فقط، لكن معرفة ذلك مفيدة للمشاريع الأكبر.```cmake

this will collect header, source and resource files into convenient variables

file(GLOB_RECURSE SRC_FILES ${PROJECT_SOURCE_DIR}/src/.cpp) file(GLOB_RECURSE HDR_FILES ${PROJECT_SOURCE_DIR}/src/.hpp) file(GLOB_RECURSE RC_FILES ${PROJECT_SOURCE_DIR}/src/*.rc)

this will give you source groups in the resulting Visual Studio project

source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Header Files" FILES ${HDR_FILES}) source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Source Files" FILES ${SRC_FILES}) source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Resource Files" FILES ${RC_FILES})

root@kitploit:~
هنا يجب أن أوضح أن CMake بشكل أدق يُوصف بأنه **نظام make**. حيث يقوم بإنشاء "نظام make" المناسب للبيئة المحددة بناءً على المترجم المعطى. على Linux، سيكون هذا ملف makefile للمترجمات المكتشفة (أو المقدمة). أما بالنسبة لنا على Windows، فإنه ينتج مشروع Visual Studio متوافقًا مع إصدار Visual Studio الخاص بنا، مما يعني أنه بمجرد إنشاء نظام make الخاص بـ MSVC يمكنك فقط استخدام Visual Studio لكل شيء إذا رغبت في ذلك! في النهاية، أنت تستخدم CMake لتهيئة Visual Studio. إذن ما تفعله هنا هو أساسًا إنشاء أشجار الملفات الخاصة بمشروعك في Visual Studio التي تقوم الواجهة الرسومية (GUI) بإنشائها لك عند إنشاء مشروع.

بعد ذلك، نحتاج إلى إضافة مشاريعنا التابعة:```cmake
# this will add zlib as a build target
add_subdirectory(${PROJECT_SOURCE_DIR}/zlib-1.2.13)

# this will add our stub project
add_subdirectory(${PROJECT_SOURCE_DIR}/stub)

# this will add our test dummy project
add_subdirectory(${PROJECT_SOURCE_DIR}/dummy)

شيء ذكرته سابقًا هو أن مشروع الحزمة (packer) يعتمد على مشروع الهيكل (stub). بطريقةٍ ما، يحتاج الحزمة إلى الاحتفاظ بالهيكل ومعالجته للوصول في النهاية إلى ملفنا التنفيذي المعبأ. يمكننا استخدام ملفات موارد ويندوز لدمج ملفنا التنفيذي الهيكلي داخل ثنائي الحزمة لدينا، بغض النظر عن تكوين البناء! بالإضافة إلى ذلك، يمكننا استخدام CMake لإنشاء تلك الملفات لنا بحيث تكون مراجعنا للملفات التنفيذية المبنية سليمة داخل مشروع CMake! لننشئ ملفات الموارد الخاصة بنا الآن حتى نتمكن من تضمينها في مشروعنا:```cmake

this will make sure our stub data will be included in the resources of our packer

despite where it may reside in cmake's build system

file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/$/stub.hpp" CONTENT "#pragma once\n#define IDB_STUB 1000\n") file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/$/stub.rc" CONTENT "#include <winresrc.h>\n#include "stub.hpp"\nIDB_STUB STUB "$<TARGET_FILE:stub>"\n")

root@kitploit:~
`CMAKE_CURRENT_BINARY_DIR` هو السلسلة التي تحتوي على دليل البناء الحالي لديك. يقوم Visual Studio بوضع الملفات الثنائية في مجلدات بناءً على تهيئتها، لذلك نستخدم عبارة المولّد `$<CONFIG>` للحصول على التهيئة الحالية للبناء. كما نستخدم عبارة المولّد `$<TARGET_FILE:stub>` لإخراج اسم الملف القابل للتنفيذ للملف الثنائي الوتد بعد تجميعه. عندما نضمّن هذه الملفات في مشروعنا—ملف RC المولَّد والملف الترويسي المولَّد—يمكننا دمج ملفنا الثنائي الوتد بنجاح في مشروع packer.

بعد ذلك، نُعلن عن ملف تنفيذي لمشروع packer باستخدام الملفات التي تم جمعها سابقًا:```cmake
# this will create our packer executable
add_executable(packer ${HDR_FILES} ${SRC_FILES})
target_sources(packer PRIVATE ${RC_FILES} "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/stub.rc")

ثم نربط مكتبة zlib المستوردة للرابط:```cmake

this will link zlib to our packer

target_link_libraries(packer zlibstatic)

root@kitploit:~
يمكنك بدلاً من ذلك ربط `zlib` فقط إذا كنت تريد حقًا DLL الخاص بـ zlib.

نظرًا لامتلاكنا ملفات مولّدة (وكذلك zlib كجزء من خطوة البناء الخاصة به)، فنحن بحاجة إلى تضمين الدلائل الديناميكية في ملفات الترويسة للمشروع. وبسبب اعتماد على كون المشاريع في جذر مشروع في CMake، نضيف أيضًا دلائل تضمين zlib للملف الثنائي الكعبري الخاص بنا:```cmake
# zlib, as part of its build step, drops a config header in the build directory.
# we do this too, so make sure to include everything for the build!
target_include_directories(packer PUBLIC
  "${PROJECT_SOURCE_DIR}/src"
  "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>"
  "${PROJECT_SOURCE_DIR}/zlib-1.2.13"
  "${CMAKE_CURRENT_BINARY_DIR}/zlib-1.2.13"
)

# also set the includes for the stub from here.
# we can't set this in the stub CMake file because CMake requires includes to be in the same
# directory as the build target. for this file, our build target is packer, so this sets
# up includes relative to the packer executable.
target_include_directories(stub PUBLIC
  "${PROJECT_SOURCE_DIR}/zlib-1.2.13"
  "${CMAKE_CURRENT_BINARY_DIR}/zlib-1.2.13"
)

أخيرًا، نُسوّي الأمور في سلسلة التبعيات لدينا بإعلام CMake بحالة التبعيات: نُعلّم الـ stub كتبعية للـ packer، والـ packer كتبعية للـ dummy.```cmake

this will add our stub as a dependency and our dummy as being dependent on the packer.

add_dependencies(packer stub) add_dependencies(dummy packer)

root@kitploit:~
من المفترض أن تكون ملفات CMake الخاصة بـ [الـ stub](https://github.com/frank2/packer-tutorial/blob/main/stub/CMakeLists.txt) و [الـ dummy](https://github.com/frank2/packer-tutorial/blob/main/dummy/CMakeLists.txt) سهلة الفهم إلى حد كبير الآن!

الأمر الرائع هو أن CMake يمكنه إدارة الاختبارات نيابةً عنا! وفي هذه المرحلة، أصبح توليد الاختبارات لأداة packer لدينا عملية سلسة: يمكننا ببساطة إصدار أمر، وإذا كان رمز الخروج هو 0، ينجح الاختبار. لأجل البساطة، لنفترض أننا نُغلف ملفًا ثنائيًا بتمريره كوسيطة أولى للملف التنفيذي. نريد شيئًا مثل هذا:```
$ packer.exe dummy.exe

لعمل ذلك باستخدام CMake بسيط جدًا:```cmake

enable testing to verify our packer works

enable_testing() add_test(NAME test_pack COMMAND "$<TARGET_FILE:packer>" "$<TARGET_FILE:dummy>")

root@kitploit:~
أخيرًا، اختبار ما إذا كان البرنامج يفك التغليف أبسط من ذلك: فقط قم بتشغيل المخرجات! لن يفاجئك كم الأخطاء التي ستواجهها والتي تتسبب في تعطل البرنامج بمجرد تشغيله عندما تحاول محاكاة المُحمِّل! لنقل مرة أخرى، من باب التبسيط، أن الملف الثنائي يجب أن يُخرج إلى "packed.exe" بالنسبة لملف ثنائي مغلّف. في هذه الحالة، كل ما عليك فعله هو:```cmake
add_test(NAME test_unpack
  COMMAND "packed.exe")

سيفشل هذا بسلاسة إذا لم يُنتج الباكَر الخاص بك الملف الثنائي. لسوء الحظ، فإن CMake الخاص بـ Visual Studio يتجاهل متغير ADDITIONAL_CLEAN_FILES، لذا سيتعين عليك تنظيف جميع الملفات المُنشأة يدويًا في نظام البناء الخاص بك، بما في ذلك الملفات المُنشأة stub.rc وstub.hpp المذكورة أعلاه.

تهانينا! تم إنجاز الكثير من العمل لبناء واختبار الباكَر الخاص بك بنجاح. والآن بعد أن أنجزنا المهام الأساسية، أصبح بإمكان الباكَر الخاص بنا القيام بما يلي:

  • تجميع جميع التبعيات بالترتيب الصحيح
  • بناء الملف الثنائي للـ stub وحقنه داخل الملف الثنائي للباكَر
  • اختبار عملية التعبئة/فك التعبئة تلقائيًا

الآن يمكننا الانتقال إلى الجزء الممتع!

تعبئة الملفات الثنائية داخل الـ stub الخاص بك

إذًا نجحنا في إعداد المترجم الخاص بنا لتجميع الملف التنفيذي للـ stub كمورد داخل الملف الثنائي للباكَر، ولكن كيف يمكننا وضع ملف ثنائي في حالة معبأة داخل الـ stub؟ لا يمكننا إضافته كمورد لأننا لا يمكننا أن نتوقع من المستخدم النهائي للباكَر أن يستخدم مترجمًا فقط؛ فمن المفترض أن يكون الملف التنفيذي للباكَر حلًا قائمًا بذاته.

إحدى التقنيات التي استخدمتها كثيرًا (وإن كانت قد تكون واضحة للمحللين) هي إضافة قسم جديد إلى الملف الثنائي للـ stub ليتم تحميله لاحقًا في وقت التشغيل. وسيكون هذا بمثابة دورة مكثّفة في صيغة PE. لكن أولًا، كيف يمكننا استخراج البيانات من الموارد؟

إدارة الموارد

هناك ثلاث خطوات أساسية للحصول على بيانات الموارد من ملف ثنائي في وقت التشغيل:

  • العثور على المورد
  • تحميل المورد
  • قفل المورد (وهو ما يحصل على وحدات البايت الخاصة بالمورد)

توضح الدالة التالية كيفية البحث عن مورد من ملف ثنائي معيّن والحصول عليه:```cpp std::vectorstd::uint8_t load_resource(LPCSTR name, LPCSTR type) { auto resource = FindResourceA(nullptr, name, type);

if (resource == nullptr) { std::cerr << "Error: couldn't find resource." << std::endl; ExitProcess(6); }

auto rsrc_size = SizeofResource(GetModuleHandleA(nullptr), resource); auto handle = LoadResource(nullptr, resource);

if (handle == nullptr) { std::cerr << "Error: couldn't load resource." << std::endl; ExitProcess(7); }

auto byte_buffer = reinterpret_cast<std::uint8_t *>(LockResource(handle));

return std::vectorstd::uint8_t(&byte_buffer[0], &byte_buffer[rsrc_size]); }

root@kitploit:~
مع بياناتنا في متجه سهل التشكيل، يمكننا الآن تحليل صورة الـ stub وإضافة بيانات جديدة إليها.

### تحليل ملف PE

يُقسَّم الملف التنفيذي لنظام Windows، في أبسط مكوناته، إلى جزأين: *الترويسات* و*بيانات الأقسام*. تحتوي الترويسات على الكثير من البيانات الوصفية المهمة لعملية التحميل، وبيانات الأقسام هي مجرد ذلك — بيانات، قد تكون كودًا قابلًا للتنفيذ (أي قسم `.text`) أو بيانات عشوائية (أي قسم `.data`). تبدأ بداية كل ملف تنفيذي لنظام Windows ببنية `IMAGE_DOS_HEADER`:```c
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

في حين يبدو أن هذا الهيكل يحتوي على الكثير من التفاصيل، يمكنك على الأرجح أن تدرك من الاسم أن هذه الترويسة هي من بقايا إصدارات ويندوز و MS-DOS القديمة. هنا، لا نهتم إلا بقيمتين في هذه الترويسة: e_magic و e_lfanew. e_magic هي ببساطة قيمة الترويسة السحرية في أعلى الصورة، أي "MZ" في بداية الملف. e_lfanew هو الإزاحة إلى ترويسات NT من بداية الملف، وهي ترويسات PE التي تحتوي على قدر أكبر بكثير من معلومات البيانات الوصفية حول الملف التنفيذي. يمكننا، على سبيل المثال، بناء مدقق PE بسيط لأداة التعبئة الخاصة بنا كما يلي:```cpp void validate_target(const std::vectorstd::uint8_t &target) { auto dos_header = reinterpret_cast<const IMAGE_DOS_HEADER *>(target.data());

// IMAGE_DOS_SIGNATURE is 0x5A4D (for "MZ") if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) { std::cerr << "Error: target image has no valid DOS header." << std::endl; ExitProcess(3); }

auto nt_header = reinterpret_cast<const IMAGE_NT_HEADERS *>(target.data() + dos_header->e_lfanew);

// IMAGE_NT_SIGNATURE is 0x4550 (for "PE") if (nt_header->Signature != IMAGE_NT_SIGNATURE) { std::cerr << "Error: target image has no valid NT header." << std::endl; ExitProcess(4); }

// IMAGE_NT_OPTIONAL_HDR64_MAGIC is 0x020B if (nt_header->OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) { std::cerr << "Error: only 64-bit executables are supported for this example!" << std::endl; ExitProcess(5); } }

root@kitploit:~
`IMAGE_NT_HEADERS` هو هيكل كبير إلى حد ما بشكل عام، لذا لن أوثّق كل شيء هنا، لكن يمكنك العثور على كل ما تحتاج معرفته عنه في [وثائق مايكروسوفت للترويسة](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers64). سنحتاج فقط إلى عدد قليل من أعضاء الهيكل من هذه الترويسات على أي حال. في الوقت الحالي، ينبغي علينا ضغط الملف الثنائي المستهدف لإضافته إلى بيانات stub الخاصة بنا.

استخدام دالتي `compress` و `decompress` من zlib أمر مباشر للغاية. إذا كنت تريد حقًا التوسع في العمل مع zlib، أقترح العمل مع دالتي `deflate`/`inflate`، اللتين تتيحان لك التعامل مع تدفقات الضغط على شكل أجزاء في كل مرة. راجع قسم "الدوال المتقدمة" في [دليل zlib](https://www.zlib.net/manual.html). ومع ذلك، في هذا المثال، ستكفي `compress` و `decompress`.

للبدء، نستحصل على قيمة حجم باستخدام دالة `compressBound` من zlib بناءً على حجم الملف الثنائي المستهدف. تتوافق قيمة الحجم هذه مع الحد الأقصى المطلوب لاحتواء تدفق بيانات مضغوطة بالنظر إلى حجم البيانات. يمكننا بعد ذلك استخدام هذه القيمة لتخصيص متجه (vector) لاحتواء البيانات المضغوطة. تُرجع دالة `compress` في النهاية الحجم الفعلي للمخزن المؤقت المضغوط، ويمكننا عندئذٍ تغيير حجم المتجه لدينا إلى الحجم المناسب.```cpp
// get the maximum size of a compressed buffer of the target binary's size.
uLong packed_max = compressBound(target.size());
uLong packed_real = packed_max;

// allocate a vector with that size
std::vector<std::uint8_t> packed(packed_max);
   
if (compress(packed.data(), &packed_real, target.data(), target.size()) != Z_OK)
{
   std::cerr << "Error: zlib failed to compress the buffer." << std::endl;
   ExitProcess(8);
}

// resize the buffer to the real compressed size
packed.resize(packed_real);

معالجة ملف PE

لنأخذ لحظة للحديث عن محاذاة البيانات. يُعتبر دفق بيانات معين محاذىً إذا كان عنوانه أو حجمه قابلاً للقسمة على حد محاذاة معين. على سبيل المثال، في ملفات PE، تُحاذى أقسام البيانات على القرص عادةً إلى حد 0x400، بينما تُحاذى في الذاكرة إلى حد 0x1000. يمكننا تحديد ما إذا كانت قيمة معينة محاذاةً بإجراء عملية مودولو على القيمة بالنسبة إلى الحد (أي أن value % alignment == 0). يمكن محاذاة ملفات PE بشكل عشوائي إلى قيم أخرى، وهذه القيمة موجودة داخل محمّل PE ومهمة له بشكل عام. محاذاة قيمة معينة وحد معين هي عملية بسيطة نسبيًا:```cpp template T align(T value, T alignment) { auto result = value + ((value % alignment == 0) ? 0 : alignment - (value % alignment)); return result; }

root@kitploit:~
---

هذه الدالة تقوم أساسًا بحشو قيمة غير متحاذاة مع الباقي اللازم لتصبح متحاذاة بشكل صحيح إلى حد معين.

لإضافة بيانات عشوائية بشكل صحيح إلى ملف PE الخاص بنا، يجب أن نكون حريصين على *محاذاة الملف* بشكل خاص-- يمكننا حساب القيم المتحاذاة الصحيحة لملف PE عندما يكون في الذاكرة لاحقًا، لكن في الوقت الحالي عند إضافة بياناتنا نحتاج إلى محاذاة ملفنا إلى حد محاذاة الملف. في الكود التالي، نحصل على الرؤوس، ثم حدود محاذاة الملف ومحاذاة الأقسام، ثم نتابع لمحاذاة بيانات الـ stub إلى حد الملف وإلحاق القسم المعبأ الجديد.

---```cpp
// next, load the stub and get some initial information
std::vector<std::uint8_t> stub_data = load_resource(MAKEINTRESOURCE(IDB_STUB), "STUB");
auto dos_header = reinterpret_cast<IMAGE_DOS_HEADER *>(stub_data.data());
auto e_lfanew = dos_header->e_lfanew;

// get the nt header and get the alignment information
auto nt_header = reinterpret_cast<IMAGE_NT_HEADERS64 *>(stub_data.data() + e_lfanew);
auto file_alignment = nt_header->OptionalHeader.FileAlignment;
auto section_alignment = nt_header->OptionalHeader.SectionAlignment;

// align the buffer to the file boundary if it isn't already
if (stub_data.size() % file_alignment != 0)
   stub_data.resize(align<std::size_t>(stub_data.size(), file_alignment));
      
// save the offset to our new section for later for our new PE section
auto raw_offset = static_cast<std::uint32_t>(stub_data.size());

// encode the size of our unpacked data into the stub data
auto unpacked_size = target.size();
stub_data.insert(stub_data.end(),
                 reinterpret_cast<std::uint8_t *>(&unpacked_size),
                 reinterpret_cast<std::uint8_t *>(&unpacked_size)+sizeof(std::size_t));

// add our compressed data.
stub_data.insert(stub_data.end(), packed.begin(), packed.end());

الآن، ربما أضفنا البيانات لقسمنا وفقًا لحدود أقسام الملف، لكن ملف stub التنفيذي ما زال غير مدرك لوجود هذا القسم في ملف PE. نحتاج ليس فقط إلى تحليل جدول الأقسام لملف PE، بل أيضًا إلى إضافة إدخال جديد يشير إلى القسم الخاص بنا. لهذا السبب يوجد المتغير raw_offset.

أولاً، يمكننا زيادة عدد الأقسام بسهولة عن طريق تحديث NumberOfSections. عادةً، تكون البيانات بعد آخر جدول أقسام مملوءة بأصفار، لذا يمكننا الكتابة فوق البيانات المصفّرة بإضافة القسم الجديد بسهولة.```cpp // increment the number of sections in the file header auto section_index = nt_header->FileHeader.NumberOfSections; ++nt_header->FileHeader.NumberOfSections;

root@kitploit:~
بعد ذلك، نحتاج إلى الحصول على مؤشر إلى جدول المقاطع نفسه. وعلى الرغم من أنه يتبع مباشرةً رأس NT الاختياري من الناحية الفنية، إلا أن حجم الرأس الاختياري يتم تحديده فعليًا بقيمة `SizeOfOptionalHeader` في رأس ملف NT. لذا، للوصول إلى هناك، نحتاج إلى حساب مؤشر من أعلى بنية `OptionalHeader` إلى الإزاحة المقدمة من قيمة `SizeOfOptionalHeader`.```cpp
// acquire a pointer to the section table
auto size_of_header = nt_header->FileHeader.SizeOfOptionalHeader;
auto section_table = reinterpret_cast<IMAGE_SECTION_HEADER *>(
   reinterpret_cast<std::uint8_t *>(&nt_header->OptionalHeader)+size_of_header
);

أخيرًا، نحن مستعدون لبدء إضافة بيانات تعريف القسم الخاص بنا. هذا هو رأس القسم الخاص بـ PE:```c typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // IMAGE_SIZEOF_SHORT_NAME is 8 union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

root@kitploit:~
المتغيرات المحددة التي تهمنا في ترويسة القسم الجديد هي `Name`, `VirtualSize`, `VirtualAddress`, `SizeOfRawData`, `PointerToRawData` و `Characteristics`. عند هذه النقطة، ينبغي أن تدرك أن السبب وراء حاجتك إلى معرفة نوعين من المحاذاة-- محاذاة الملف ومحاذاة الذاكرة-- هو وجود حالتين مختلفتين للذاكرة لملف PE تنفيذي معين: شكله على *القرص*، وشكله في *الذاكرة*، كنتيجة لعملية التحميل. من الممكن تكوين ملف PE معين بحيث يكون له نفس تخطيط الذاكرة سواء تم تحميله أم لا، لكنه ليس تكوينًا شائعًا.

المتغير `Name` هو التسمية ذات الثمانية بايتات التي يمكنك إعطاؤها لقسمك الجديد. اخترت `.packed`، لأنها سلسلة ASCII من سبعة بايتات وتناسب تمامًا داخل المخزن المؤقت.

`VirtualAddress` يشير إلى إزاحة قسم معين في الذاكرة. ويُعرف أيضًا باسم "العنوان الافتراضي النسبي"، أو RVA. `VirtualSize` يشير إلى حجم القسم في الذاكرة. (جدير بالذكر أن MSVC يعيّن هذه القيمة على أنها قيمة الحجم غير المُحاذى للقسم، لذلك نتبع هذه الاتفاقية في قسمنا أيضًا.) `PointerToRawData` يشير إلى إزاحة قسم معين على القرص، و`SizeOfRawData` يشير إلى حجم القسم على القرص.

`Characteristics` معقد ويمكن أن يشير إلى كون القسم قابلاً للقراءة أو الكتابة أو التنفيذ، بالإضافة إلى دلالات أخرى، انظر قسم الخصائص في [توثيق `IMAGE_SECTION_HEADER`](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_section_header). في الوقت الحالي، ينبغي أن تعلم أن كل ما نحتاجه هو أن يكون القسم قابلاً للقراءة ومُعلَّمًا على أنه يحتوي على بيانات مُهيأة.

مع وضع كل هذا في الاعتبار، يمكننا الآن إنشاء قسمنا الجديد!```cpp
// get a pointer to our new section and the previous section
auto section = &section_table[section_index];
auto prev_section = &section_table[section_index-1];

// calculate the memory offset, memory size and raw aligned size of our packed section
auto virtual_offset = align(prev_section->VirtualAddress + prev_section->Misc.VirtualSize, section_alignment);
auto virtual_size = section_size;
auto raw_size = align<DWORD>(section_size, file_alignment);

// assign the section metadata
std::memcpy(section->Name, ".packed", 8);
section->Misc.VirtualSize = virtual_size;
section->VirtualAddress = virtual_offset;
section->SizeOfRawData = raw_size;
section->PointerToRawData = raw_offset;

// mark our section as initialized, readable data.
section->Characteristics = IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_INITIALIZED_DATA;

هذا يطرح الآن مشكلة صغيرة، رغم ذلك: حجم الصورة تغيّر. قد تظن أنها لن تكون مشكلة، لكن في الترويسة الاختيارية لترويسة NT يوجد متغير يُسمى SizeOfImage يحدد مقدار المساحة التي يحتاجها المُحمِّل لتخصيصها لملفنا التنفيذي. وهذا إصلاح بسيط، رغم ذلك: الحجم الذي نحتاجه لصورتنا هو حجم القسم الأخير بعد محاذاته إلى حدّ محاذاة الأقسام.```cpp // calculate the new size of the image. nt_header->OptionalHeader.SizeOfImage = align(virtual_offset + virtual_size, section_alignment);

root@kitploit:~
وهذا كل شيء! لقد نجحنا في إضافة ملفنا الثنائي المضغوط كقسم جديد ليتمكن الـ stub من فك ضغطه وتحميله في النهاية. الآن يمكننا ببساطة حفظ صورة الـ stub المعدّلة على القرص.```cpp
std::ofstream fp("packed.exe", std::ios::binary);
   
if (!fp.is_open()) {
   std::cerr << "Error: couldn't open packed binary for writing." << std::endl;
   ExitProcess(9);
}
   
fp.write(reinterpret_cast<const char *>(stub_data.data()), stub_data.size());
fp.close();

تهانينا! لقد أنجزنا حتى الآن ما يلي:

  • قمنا بتجميع وحقن واسترجاع ملفنا الثنائي (stub binary) من دليل الموارد الخاص بالحزمة (packer) الخاصة بنا في وقت التشغيل
  • قمنا بتحليل رؤوس الملفات القابلة للتنفيذ لكل من ملفنا الثنائي (stub binary) والملف الثنائي الهدف لاستخراج المعلومات الأساسية
  • قمنا بتعديل ملف قابل للتنفيذ لتوسيع بيانات أقسامه بحيث تحتوي على ملفنا القابل للتنفيذ المضغوط

نحن في منتصف الطريق من كتابة الحزمة (packer) الخاصة بنا! الآن يمكننا الانتقال إلى ما يمكن القول إنه أصعب جزء في العملية: تطوير ملف الـ stub الثنائي.

محاكاة المُحمِّل

في حين أن التفاصيل الدقيقة لكتابة كعب فك الحزم (unpack stub) قد تكون معقدة داخليًا، إلا أنها في الأساس تختصر في بضع خطوات فقط:

  • استرجاع بيانات الصورة: الحصول على تمثيل الملف الثنائي الهدف وفك ضغطه (وإزالة التشويش عنه اختياريًا) لمعالجته لاحقًا.
  • تحميل الصورة: وهي الأكثر تعقيدًا بفارق كبير؛ هنا تتم محاكاة الـ Loader وتجهيز الصورة الهدف للتنفيذ.
  • استدعاء نقطة الدخول للصورة: يُشار إليها عادةً باسم "original entry point," أو OEP، وهي النقطة التي تنتقل من خلالها من مرحلة التحميل إلى مرحلة التنفيذ للملف الثنائي المضغوط الخاص بك.

هذه العملية بسيطة للغاية، فإجراءنا الرئيسي لا يتجاوز سوى عدد قليل من الدوال:```cpp int main(int argc, char *argv[]) { // first, decompress the image from our added section auto image = get_image();

// next, prepare the image to be a virtual image
auto loaded_image = load_image(image);

// resolve the imports from the executable load_imports(loaded_image);

// relocate the executable relocate(loaded_image);

// get the headers from our loaded image auto nt_headers = get_nt_headers(loaded_image);

// acquire and call the entrypoint auto entrypoint = loaded_image + nt_headers->OptionalHeader.AddressOfEntryPoint; reinterpret_cast<void(*)()>(entrypoint)();

return 0; }

root@kitploit:~
### قراءة ملف PE الخاص بنا من الذاكرة

أولاً، نحتاج بطريقة ما إلى الحصول على البيانات من القسم الذي أنشأه المُعبئ الخاص بنا من الثنائي قيد التشغيل. هل من الممكن الحصول على رؤوس الثنائي قيد التشغيل في وقت التشغيل؟ نعم، بالتأكيد! [`GetModuleHandleA`](https://learn.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandlea)، مع وسيطة فارغة، يعيد في النهاية مؤشرًا إلى رؤوس PE الخاصة بالثنائي قيد التشغيل! لذا في وقت التشغيل، يمكنك الوصول بسهولة إلى الصورة كما هي موجودة في الذاكرة. وهذا هو ما يجعل إضافة قسم جديد إلى ثنائينا أمرًا جذابًا للغاية: يمكننا بسهولة تامة تحليل القسم المستهدف من الثنائي.

مع ما تعلمناه عن تحليل جدول الأقسام، ينبغي أن يكون هذا الجزء من الكود سهل الفهم:```cpp
// find our packed section
auto base = reinterpret_cast<const std::uint8_t *>(GetModuleHandleA(NULL));
auto nt_header = get_nt_headers(base);
auto section_table = reinterpret_cast<const IMAGE_SECTION_HEADER *>(
   reinterpret_cast<const std::uint8_t *>(&nt_header->OptionalHeader)+nt_header->FileHeader.SizeOfOptionalHeader
);
const IMAGE_SECTION_HEADER *packed_section = nullptr;

for (std::uint16_t i=0; i<nt_header->FileHeader.NumberOfSections; ++i)
{
   if (std::memcmp(section_table[i].Name, ".packed", 8) == 0)
   {
      packed_section = &section_table[i];
      break;
   }
}

if (packed_section == nullptr) {
   std::cerr << "Error: couldn't find packed section in binary." << std::endl;
   ExitProcess(1);
}

بعد ذلك، نحتاج إلى فك ضغط بيانات الـ stub الخاصة بنا من الثنائي. توصي zlib بتضمين حجم الحمولة الأصلية المفكوكة بطريقة يمكن لروتين فك الضغط الوصول إليها، ولهذا قمنا بتضمين حجم الثنائي المفكوك في ترويسة بياناتنا المضغوطة. لذا، نحصل على مؤشر إلى بياناتنا المفكوكة، وننشئ مخزنًا جديدًا يمكنه استيعاب البيانات المفكوكة، ثم ننتقل إلى استدعاء دالة فك الضغط الخاصة بـ zlib.```cpp // decompress our packed image auto section_start = base + packed_section->VirtualAddress; auto section_end = section_start + packed_section->Misc.VirtualSize; auto unpacked_size = *reinterpret_cast<const std::size_t *>(section_start); auto packed_data = section_start + sizeof(std::size_t); auto packed_size = packed_section->Misc.VirtualSize - sizeof(std::size_t);

auto decompressed = std::vectorstd::uint8_t(unpacked_size); uLong decompressed_size = static_cast(unpacked_size);

if (uncompress(decompressed.data(), &decompressed_size, packed_data, packed_size) != Z_OK) { std::cerr << "Error: couldn't decompress image data." << std::endl; ExitProcess(2); }

return decompressed;

root@kitploit:~
كما ترى، كانت `get_image` دالة بسيطة نسبيًا في نهاية المطاف. لقد حصلنا على ثنائي الهدف مستخرجًا من القسم المُلحق، لذا نحتاج الآن إلى تحميله.

### تحميل PE الخاص بنا للتنفيذ

يقوم محمّل الملفات التنفيذية في Windows بالكثير من الأمور المختلفة في الخلفية، ويدعم العديد من إعدادات الملفات التنفيذية المختلفة. من المرجح أن تواجه مجموعة متنوعة من الأخطاء مع أداة التغليف (packer) التي ينتجها هذا البرنامج التعليمي إذا استكشفت أكثر من الثنائي النموذجي، لأن الإعداد الذي سنبنيه اليوم هو تقنيًا حد أدنى للغاية. ولكن من أجل تأسيس ذلك الحد الأدنى المطلق للتنفيذ، نحتاج إلى القيام بالأشياء التالية لتحميل ملف Windows تنفيذي حديث:

* تخصيص الصورة التي تحمل تمثيل الذاكرة لصورة الملف التنفيذي
* تعيين أقسام صورة الملف التنفيذي لدينا، بما في ذلك الرؤوس، إلى تلك الصورة المخصصة
* حل الاستيرادات (imports) في وقت التشغيل إلى المكتبات الأخرى التي يحتاجها ثنائينا
* إعادة تعيين صورة الثنائي بحيث تشير مجموعة متنوعة من العناوين في صورتنا إلى حيث يفترض أن تشير

لنبدأ مع دالة `load_image`. في البداية، نحتاج إلى الحصول على جدول الأقسام الخاص بنا من الثنائي المعبأ. سيتم في النهاية تعيينه إلى صورتنا المخصصة حديثًا. بالنسبة للمخزن المؤقت التنفيذي الصحيح، يكون التخصيص بسيطًا جدًا -- خذ قيمة `SizeOfImage` من رؤوس الأقسام الخاصة بنا وأنشئ مخزنًا مؤقتًا جديدًا عبر [`VirtualAlloc`](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc) ينشئ صورة قابلة للقراءة والكتابة والتنفيذ لنا لنقوم بتعيينها إليه.```cpp
// get the original image section table
auto nt_header = get_nt_headers(image.data());
auto section_table = reinterpret_cast<const IMAGE_SECTION_HEADER *>(
   reinterpret_cast<const std::uint8_t *>(&nt_header->OptionalHeader)+nt_header->FileHeader.SizeOfOptionalHeader
);

// create a new VirtualAlloc'd buffer with read, write and execute privileges
// that will fit our image
auto image_size = nt_header->OptionalHeader.SizeOfImage;
auto base = reinterpret_cast<std::uint8_t *>(VirtualAlloc(nullptr,
                                                          image_size,
                                                          MEM_COMMIT | MEM_RESERVE,
                                                          PAGE_EXECUTE_READWRITE));

if (base == nullptr) {
   std::cerr << "Error: VirtualAlloc failed: Windows error " << GetLastError() << std::endl;
   ExitProcess(3);
}

مع تخصيص المخزن المؤقت، ما نحتاج إلى فعله هو نسخ الترويسات والأقسام إليه. يجب أن يتوافق هذا مع متغير SectionAlignment السابق. لحسن الحظ، فإن الطريقة التي حضّرنا بها قسمنا-- والطريقة التي حُضّرت بها الأقسام الأخرى-- هي أنها محاذاة بالفعل إلى حد SectionAlignment. ويكفي القول إن النتيجة النهائية هي حساب مؤشرات بسيط: انسخ صورتنا المستهدفة عند إزاحة PointerToRawData، إلى صورتنا المحمّلة عند إزاحة VirtualAddress.

بشكل اختياري، يمكنك نسخ ترويسات PE إلى أعلى الصورة. إن الاحتفاظ بالترويسات الأصلية يسهّل عملية التطوير، لكن الرغبة في إزالتها خطوة جيدة نحو بناء حزّام (packer) معادٍ للتحليل. نحن ننسخ الترويسات هنا من باب سهولة الاستخدام.```cpp // copy the headers to our new virtually allocated image std::memcpy(base, image.data(), nt_header->OptionalHeader.SizeOfHeaders);

// copy our sections to their given addresses in the virtual image for (std::uint16_t i=0; i<nt_header->FileHeader.NumberOfSections; ++i) if (section_table[i].SizeOfRawData > 0) std::memcpy(base+section_table[i].VirtualAddress, image.data()+section_table[i].PointerToRawData, section_table[i].SizeOfRawData);

return base;

root@kitploit:~
إذن انتهى الجزء السهل من عملية التحميل: لقد استرجعنا ملفنا الثنائي من الذاكرة، وفككنا تغليفه، وأعدنا تعيين مقاطعه إلى منطقة ذاكرة قابلة للتنفيذ. ومع تحضير الصورة، يمكننا الخوض في تفاصيل عملية التحميل الدقيقة.

### تحليل استيرادات API

داخل الترويسات الاختيارية يوجد ما يُسمى *دليل البيانات*. يحتوي هذا الدليل على الكثير من المعلومات المختلفة حول الملف التنفيذي، مثل الرموز المُصدَّرة من الصورة والموارد مثل الأيقونات والصور النقطية. سنقوم في هذا البرنامج التعليمي بتحليل دليلين من أدلة البيانات: **دليل الاستيراد** و**دليل إعادة التوطين**. كل دليل مُرمَّز بشكل ثابت، ويمكن العثور على فهارسها [في توثيق الترويسة الاختيارية](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32) (مرر لأسفل إلى وصف `DataDirectory`). يكون دليل البيانات موجودًا إذا كانت قيمة `VirtualAddress` الخاصة به غير صفرية.```c
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

نسترجع المؤشرات إلى دلائل البيانات عبر تحويل عنوان RVA المتوفر. على سبيل المثال، هكذا تحصل في النهاية على جدول الاستيراد من دليل بيانات الاستيراد:```cpp // get the import table directory entry auto nt_header = get_nt_headers(image); auto directory_entry = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];

// if there are no imports, that's fine-- return because there's nothing to do. if (directory_entry.VirtualAddress == 0) { return; }

// get a pointer to the import descriptor array auto import_table = reinterpret_cast<IMAGE_IMPORT_DESCRIPTOR *>(image + directory_entry.VirtualAddress);

root@kitploit:~
لحل استيرادات API، يقوم المُحمِّل بتحليل هذا الدليل، ثم يشرع في تحميل المكتبات الضرورية والحصول على دوالها المستوردة. ولحسن الحظ، فإن هذا الدليل سهل التحليل نسبيًا.

يبدأ ببنية واصف الاستيراد:```c
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    DWORD   TimeDateStamp;                  // 0 if not bound,
                                            // -1 if bound, and real date\time stamp
                                            //     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                                            // O.W. date/time stamp of DLL bound to (Old BIND)

    DWORD   ForwarderChain;                 // -1 if no forwarders
    DWORD   Name;
    DWORD   FirstThunk;                     // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

ما يهمنا أكثر هو متغيران اسماهما مربكان إلى حد ما: OriginalFirstThunk وFirstThunk. يحتوي OriginalFirstThunk على معلومات حول الواردات التي يرغب بها هذا الملف القابل للتنفيذ فيما يتعلق بـ DLL المعطى بواسطة RVA الخاصة بـ Name. ولزيادة الإرباك، فإن FirstThunk يفعل الشيء نفسه. ما الذي يفرّق بينهما؟ يحتوي FirstThunk على الواردات بعد حلها. يتم هذا الحل بواسطة دالة GetProcAddress سيئة السمعة.

ثنكاتنا (thunks) هي هياكل بيانات إضافية يجب التعامل معها:```c typedef struct _IMAGE_THUNK_DATA64 { union { ULONGLONG ForwarderString; // PBYTE ULONGLONG Function; // PDWORD ULONGLONG Ordinal; ULONGLONG AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA64;

root@kitploit:~
هيكل البيانات هذا يغطي thunks الاستيراد وكذلك التصدير، لذلك يمكن تجاهل `ForwarderString`. يمكن أن يكون thunk الاستيراد إما `Ordinal` أو RVA إلى بنية أخرى، `IMAGE_IMPORT_BY_NAME`. *ordinal* هو ببساطة إزاحة في جدول التصدير الخاص بـ DLL معين. بنية `IMAGE_IMPORT_BY_NAME` تبدو كالتالي:```c
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;
    CHAR   Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

هذا مثال على بنية متغيرة الطول. تستفيد من غياب فحص الحدود في الوصول إلى المصفوفات لإنشاء بنى يمكن أن تكون متغيرة الحجم. Name، في هذه الحالة، يُتوقع أن يكون سلسلة C منتهية بالصفر.

لأن البيانات الثنائية لا تحتوي على أنواع، يتم التمييز بين الترتيبي و الاستيراد من خلال البت الأكثر أهمية في إدخال الـ thunk. يقع الترتيبي في النصف السفلي من العدد الصحيح في كل من تطبيقات 32-bit و 64-bit.

يعمل جدول الاستيراد لدينا مثل سلسلة C-- حيث يكون إدخاله الأخير هو OriginalFirstThunk منتهيًا بالصفر للإشارة إلى نهاية الاستيرادات المحتملة. تعمل بيانات الـ thunk الخاصة بنا بنفس الطريقة، حيث تنتهي بإدخال صفري في مصفوفة الـ thunk.

لتجميع كل شيء معًا، هذه هي الطريقة التي نتعامل بها مع تحليل جدول الاستيراد في شبه الكود:``` for every import descriptor: load the dll parse the original and first thunk

root@kitploit:~
for every thunk:
    if ordinal bit set:
        import by ordinal
    else:
        import by name
        
    store import in first thunk
root@kitploit:~
من المثير للاهتمام أنه على الرغم من أن `GetProcAddress` مُعرَّفة بنوع C-string كوسيطة للدالة، فإن Windows يتوقع منك الاستيراد بالترتيب العددي بمجرد تحويل قيمة الترتيب إلى C-string. يا للغرابة.

مع هذه التوضيحات في الاعتبار، يجب أن تكون حلقة while هذه واضحة الآن:```cpp
// when we reach an OriginalFirstThunk value that is zero, that marks the end of our array.
// typically all values in the import descriptor are zero, but we do this
// to be shorter about it.
while (import_table->OriginalFirstThunk != 0)
{
   // get a string pointer to the DLL to load.
   auto dll_name = reinterpret_cast<char *>(image + import_table->Name);

   // load the DLL with our import.
   auto dll_import = LoadLibraryA(dll_name);

   if (dll_import == nullptr) {
      std::cerr << "Error: failed to load DLL from import table: " << dll_name << std::endl;
      ExitProcess(4);
   }

   // load the array which contains our import entries
   auto lookup_table = reinterpret_cast<IMAGE_THUNK_DATA64 *>(image + import_table->OriginalFirstThunk);

   // load the array which will contain our resolved imports
   auto address_table = reinterpret_cast<IMAGE_THUNK_DATA64 *>(image + import_table->FirstThunk);

   // an import can be one of two things: an "import by name," or an "import ordinal," which is
   // an index into the export table of a given DLL.
   while (lookup_table->u1.AddressOfData != 0)
   {
      FARPROC function = nullptr;
      auto lookup_address = lookup_table->u1.AddressOfData;

      // if the top-most bit is set, this is a function ordinal.
      // otherwise, it's an import by name.
      if (lookup_address & IMAGE_ORDINAL_FLAG64 != 0)
      {
         // get the function ordinal by masking the lower 32-bits of the lookup address.
         function = GetProcAddress(dll_import,
                                   reinterpret_cast<LPSTR>(lookup_address & 0xFFFFFFFF));

         if (function == nullptr) {
            std::cerr << "Error: failed ordinal lookup for " << dll_name << ": " << (lookup_address & 0xFFFFFFFF) << std::endl;
            ExitProcess(5);
         }
      }
      else {
         // in an import by name, the lookup address is an offset to
         // an IMAGE_IMPORT_BY_NAME structure, which contains our function name
         // to import
         auto import_name = reinterpret_cast<IMAGE_IMPORT_BY_NAME *>(image + lookup_address);
         function = GetProcAddress(dll_import, import_name->Name);

         if (function == nullptr) {
            std::cerr << "Error: failed named lookup: " << dll_name << "!" << import_name->Name << std::endl;
            ExitProcess(6);
         }
      }

      // store either the ordinal function or named function
      // in our address table.
      address_table->u1.Function = reinterpret_cast<std::uint64_t>(function);

      // advance to the next entries in the address table and lookup table
      ++lookup_table;
      ++address_table;
   }

   // advance to the next entry in our import table
   ++import_table;
}

مع حل الاستيرادات، يمكننا الآن الانتقال إلى الجزء الأخير: التعامل مع دليل إعادة التوطين!

تحليل العناوين

دليل البيانات التالي الذي يجب التعامل معه هو ما يُسمى دليل إعادة التوطين. هذا الدليل مسؤول عن ترجمة العناوين المطلقة داخل الكود إلى قيمها الأساسية الجديدة. هذه العملية تنفّذ بشكل أساسي شيئًا قد تكون على دراية به يُسمى عشوائية تخطيط مساحة العنوان، ولكنها مسؤولة أيضًا عن ضمان عدم تصادم مساحات عناوين DLL مع بعضها البعض.

أولاً، نحتاج إلى التأكد من أن ملفنا الثنائي قادر فعلاً على نقل القواعد الأساسية للعناوين. في بعض الأحيان، خاصةً بالنسبة للملفات الثنائية الأقدم، لا يكون هذا الخيار مفعّلاً. ضمن خصائص ملف Windows التنفيذي المعين توجد خصائصه، والتي يُشار إليها بشكل مربك باسم DllCharacteristics في الترويسة الاختيارية. نحن معنيون بخاصية "القاعدة الديناميكية". هناك طريقة خاصة لفك حزم الملفات الثنائية غير المدعومة بـ ASLR، والتي لن نغطيها، لذا فإنه من الخطأ إذا كان ملفنا الثنائي لا يدعمها. (من الأفضل اعتبار هذا خطأ في أداة التعبئة الخاصة بك، وليس في الـ stub الخاص بك، ولكن من أجل سهولة تدفق شرح ترويسات PE، تم وضعه هنا بدلاً من ذلك.)```cpp // first, check if we can even relocate the image. if the dynamic base flag isn't set, // then this image probably isn't prepared for relocating. auto nt_header = get_nt_headers(image);

if (nt_header->OptionalHeader.DllCharacteristics & IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE == 0) { std::cerr << "Error: image cannot be relocated." << std::endl; ExitProcess(7); }

// once we know we can relocate the image, make sure a relocation directory is present auto directory_entry = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

if (directory_entry.VirtualAddress == 0) { std::cerr << "Error: image can be relocated, but contains no relocation directory." << std::endl; ExitProcess(8); }

root@kitploit:~
بعد ذلك، نحتاج إلى حساب *فرق العناوين*. وهو ببساطة الفرق بين متغير `ImageBase` الخاص بالصورة وعنوان القاعدة للصورة الافتراضية. يُستخدم لضبط قيم العناوين المرمّزة في الثنائي بسرعة.```cpp
// calculate the difference between the image base in the compiled image
// and the current virtually allocated image. this will be added to our
// relocations later.
std::uintptr_t delta = reinterpret_cast<std::uintptr_t>(image) - nt_header->OptionalHeader.ImageBase;

الآن نحن مستعدون لمعالجة جدول إعادة التوطين.```c typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION;

root@kitploit:~
لاحظ مصفوفة `TypeOffset` المعلّقة، فهي ذات صلة فعلاً هنا! يتكوّن جدول إعادة التوطين من كتل من الإزاحات التي تحتوي على العناوين المراد تعديلها، ويتم تحديدها بواسطة RVA الخاص بـ `VirtualAddress`. تحتوي مصفوفة `TypeOffset` على قيم كلمات مشفّرة تتضمن نوع إعادة التوطين بالإضافة إلى الإزاحة من `VirtualAddress` المراد تعديلها. فيما يتعلق بنوع إعادة التوطين، بالنسبة للملفات الثنائية 64-bit، لا يهمّنا سوى نوع واحد من أنواع إعادة التوطين. لسوء الحظ، هذا الهيكل ليس في الواقع بنية متغيّرة (variadic struct)، لذا يتعين علينا القيام ببعض حسابات المؤشرات للحصول على مصفوفة `TypeOffset`.

كما ذُكر، تحتوي مصفوفة `TypeOffset` على كلمات مشفّرة. البتات الأربع العليا (القناع `0xF000`) تحتوي على نوع إعادة التوطين، والذي يمكن العثور عليه في قسم ["أنواع إعادة التوطين الأساسية"](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format) من توثيق تنسيق PE. أما البتات الاثنتا عشرة السفلى (القناع `0x0FFF`) فتحتوي على الإزاحة من وسيط `VirtualAddress` المراد تعديلها.

شرح ذلك مهمة شاقة وينتهي به الأمر إلى كونه مربكًا جدًا في النهاية. الحصول على مؤشر إلى عنوان لإعادة توطينه يبدو كالتالي:```cpp
auto ptr = reinterpret_cast<std::uintptr_t *>(image + relocation_table->VirtualAddress + offset);

وتعديل هذا العنوان يبدو هكذا:```cpp *ptr += delta;

root@kitploit:~
لذلك بقدر ما هو معقّد شرح دليل إعادة التوطين (relocation directory)، فإن العمليات في الواقع بسيطة جدًا لفهمها في الكود.

الانتقال إلى الكتلة التالية من بيانات إعادة التوطين يصبح سهلاً بفضل متغير `SizeOfBlock`. تحتوي هذه الكتلة على حجم الترويسة (header) *بالإضافة إلى* حجم مصفوفة `TypeOffset`. إذا كانت `TypeOffset` بدلاً من ذلك مصفوفة ذات حجم ثابت، لكنا ببساطة انتقلنا إلى الإدخال التالي لإعادة التوطين بإضافة استدعاء `sizeof` لترويسة إعادة التوطين.

مع شرح كل هذا، يجب أن تكون قادرًا على فهم كود إعادة التوطين هذا:```cpp
// get the relocation table.
auto relocation_table = reinterpret_cast<IMAGE_BASE_RELOCATION *>(image + directory_entry.VirtualAddress);

// when the virtual address for our relocation header is null,
// we've reached the end of the relocation table.
while (relocation_table->VirtualAddress != 0)
{
   // since the SizeOfBlock value also contains the size of the relocation table header,
   // we can calculate the size of the relocation array by subtracting the size of
   // the header from the SizeOfBlock value and dividing it by its base type: a 16-bit integer.
   std::size_t relocations = (relocation_table->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(std::uint16_t);

   // additionally, the relocation array for this table entry is directly after
   // the relocation header
   auto relocation_data = reinterpret_cast<std::uint16_t *>(&relocation_table[1]);

   for (std::size_t i=0; i<relocations; ++i)
   {
      // a relocation is an encoded 16-bit value:
      //   * the upper 4 bits are its relocation type
      //     (https://learn.microsoft.com/en-us/windows/win32/debug/pe-format see "base relocation types")
      //   * the lower 12 bits contain the offset into the relocation entry's address base into the image
      //
      auto relocation = relocation_data[i];
      std::uint16_t type = relocation >> 12;
      std::uint16_t offset = relocation & 0xFFF;
      auto ptr = reinterpret_cast<std::uintptr_t *>(image + relocation_table->VirtualAddress + offset);

      // there are typically only two types of relocations for a 64-bit binary:
      //   * IMAGE_REL_BASED_DIR64: a 64-bit delta calculation
      //   * IMAGE_REL_BASED_ABSOLUTE: a no-op
      //
      if (type == IMAGE_REL_BASED_DIR64)
         *ptr += delta;
   }

   // the next relocation entry is at SizeOfBlock bytes after the current entry
   relocation_table = reinterpret_cast<IMAGE_BASE_RELOCATION *>(
      reinterpret_cast<std::uint8_t *>(relocation_table) + relocation_table->SizeOfBlock
   );
}

تهانينا! صورتنا الآن جاهزة للتنفيذ! لقد أنجزنا الكثير حتى الآن:

  • فككنا ملفنا الثنائي من جدول الأقسام الخاص بنا في وقت التشغيل
  • قمنا بتعيين ملفنا الثنائي على منطقة ذاكرة قابلة للتنفيذ
  • قمنا بحل الواردات اللازمة لتنفيذ ملفنا الثنائي
  • قمنا بإعادة توجيه العناوين في الصورة بحيث تشير إلى قاعدة صورتنا الجديدة

الآن نحن مستعدون لتشغيل ملفنا الثنائي المُفكك!

نقل التنفيذ

لنعد الآن إلى حلقتنا الرئيسية:```cpp int main(int argc, char *argv[]) { // first, decompress the image from our added section auto image = get_image();

// next, prepare the image to be a virtual image
auto loaded_image = load_image(image);

// resolve the imports from the executable load_imports(loaded_image);

// relocate the executable relocate(loaded_image);

// get the headers from our loaded image auto nt_headers = get_nt_headers(loaded_image);

// acquire and call the entrypoint auto entrypoint = loaded_image + nt_headers->OptionalHeader.AddressOfEntryPoint; reinterpret_cast<void(*)()>(entrypoint)();

return 0; }

root@kitploit:~
كما ترى، نقل التنفيذ بسيط للغاية، على الرغم من أنه يتطلب معرفة بـ [مؤشرات الدوال](https://en.wikipedia.org/wiki/Function_pointer). هذه هي الأجزاء ذات الصلة لدينا:```cpp
// acquire and call the entrypoint
auto entrypoint = loaded_image + nt_headers->OptionalHeader.AddressOfEntryPoint;
reinterpret_cast<void(*)()>(entrypoint)();

AddressOfEntryPoint هو، كما يمكنك أن تخمّن، RVA في نقطة دخول الكود لصورتنا المحمّلة. على الرغم مما قد تعرفه عن نقاط الدخول main، فإن نقطة الدخول الخام لأي ثنائي معيّن غير محددة النوع -- إن مترجم C++ الخاص بك هو المسؤول أساسًا عن إعداد بيئة الكود لتغذية الوسائط إلى دوال main المتوقعة لديك، سواء كانت main أو WinMain.

في الأساس، هذه هي طريقة تعريف مؤشر دالة:```c return_type (*variable_name)(int arg1, int arg2, ...)

root@kitploit:~
وبالتالي، فإن مؤشر الدالة الخاص بنا لاستدعاء نقطة الدخول-- دون وجود أي نوع مرتبط بها-- يبدو كما يلي:```c
void (*entrypoint)()

كعملية cast، يتلخص الأمر في هذا:```c void(*)()

root@kitploit:~
عند تجميع كل شيء معًا، يمكننا ببساطة تحويل نقطة الدخول إلى مؤشر دالة واستدعائها في نفس السطر، كما يلي:```cpp
reinterpret_cast<void(*)()>(entrypoint)();

مع تحميل كل شيء بشكل صحيح، يجب أن ترى برنامجك المعبأ يعمل. في حالتنا، نظرًا لأننا قمنا بتعبئة ملفنا التنفيذي التجريبي، فإنه يكتفي بإخراج رسالة:``` $ ./packed.exe I'm just a little guy!

root@kitploit:~
تهانينا! لقد أنجزت! لقد كتبت للتو أداة packer لنظام Windows!

## تمارين إضافية

* **هاجم المحلل**: تعلّم كيفية تنفيذ [بعض تقنيات مكافحة التنقيح](https://anti-reversing.com/Downloads/Anti-Reversing/The_Ultimate_Anti-Reversing_Reference.pdf) وقوِّ packer الخاص بك.
* **وسّع دعمك**: تعلّم كيفية تنفيذ ملف ثنائي غير قابل لإعادة التوطين، أو تعلّم تنفيذ أدلة إضافية مثل دليل التخزين المحلي لمؤشرات الترابط (`IMAGE_TLS_DIRECTORY`) ودليل الموارد (`IMAGE_RESOURCE_DIRECTORY`). وبما أن التوثيق نادر على هذا المستوى، يمكنك الاطلاع على تطبيقاتي في [exe-rs](https://github.com/exe-rs).
* **عقّد الـ stub الخاص بك**: حاول معرفة الكيفية التي سيفك بها المحلل حزم ملفك الثنائي، وامنع سهولة فك الحزم، مثل [مسح الترويسات](https://github.com/frank2/packer-tutorial/blob/main/stub/src/main.cpp#L84).
تنزيل الأداة