
ExportHider: توليد جدول التصدير أثناء وقت التشغيل لإخفاء الدوال المُصدَّرة من ملف DLL.
يُنشئ ExportHider قالب DLL بلغة C++ يحتوي على كود stub يسمح لك بإخفاء الدوال المُصدّرة من دليل التصدير (Export Directory) لملف DLL على نظام الملفات. بعد وضع تعريفات الدوال وتجميع الملف، لن ترى دوال التصدير المخفية من خلال عارضي ملفات PE مثل CFF Explorer. ومع ذلك، نظرًا لأن كود stub في القالب يُعيد إنشاء دليل التصدير أثناء وقت التشغيل، فإن استدعاءات GetProcAddress الشرعية ستنفذ بنجاح. تعمل هذه الطريقة فقط مع حالات تحميل DLL الديناميكي أو حالات محمل DLL المخصص.
عادةً، عندما تريد تعريف دالة مُصدّرة في ملفات DLL (بلغة C أو C++)، تضع ببساطة الكلمة المفتاحية __declspec(dllexport) قبل اسم الدالة، أو تنشئ ملف .def. بعد التجميع، يُنشئ المترجم جدولًا محددًا يسمى دليل التصدير لحفظ المعلومات المتعلقة بالدوال المُصدّرة. يمكن رؤية هيكل دليل التصدير أدناه:
عندما تريد عملية استخدام دالة من ملف DLL، يقوم محمل Windows ببساطة بتحليل هذا الهيكل واستيراد الدوال المطلوبة باستخدام المصفوفات AddressOfFunctions و AddressOfNames و AddressOfNameOrdinals.
يشرح منشور مدونة ferreirasc الإجراء المحدد لاستيراد دالة بالتفصيل، ولكن باختصار، بالنسبة لدالة مستوردة بالاسم، يقوم المحمل بالتكرار على مصفوفة AddressOfNames (قيم هذه المصفوفة هي مجرد قيم RVA) ويبحث عن الاسم المحدد. بمجرد أن يجد المحمل تطابقًا في الموضع “i”، سيشير إلى الفهرس ith من مصفوفة AddressOfNameOrdinals ويحصل على الترتيب (ordinal) المرتبط بهذه الدالة. بعد الحصول على الترتيب، سيشير المحمل إلى AddressOfFunctions في موضع قيمة الترتيب للحصول أخيرًا على RVA المرتبط بالدالة المستوردة.
النقطة الحاسمة هنا هي أن جميع عمليات البحث والوصول هذه التي يقوم بها محمل Windows، عند استدعاء LoadLibrary، تتم بعد تعيين ملف DLL إلى مساحة عنوان العملية. أثناء تعيين DLL، يُكتب ملف DLL بأكمله، بما في ذلك رؤوس PE الخاصة به، إلى الذاكرة، ويقوم المحمل بتحليل الرؤوس في الذاكرة للوصول إلى دليل التصدير. هذا يعني أنه إذا كان DLL نفسه قادرًا على الكتابة فوق رؤوس PE الخاصة به في الذاكرة لتغيير عنوان دليل التصدير (ببساطة الفهرس 0 من DataDirectory) بعد الارتباط بالعملية، يمكن للمحمل البحث عن دوال لاستيرادها في دليل تصدير عشوائي بدلاً من دليل التصدير الذي أضافه المترجم.
__ _ _ _
/__\_ ___ __ ___ _ __| |_ /\ /(_) __| | ___ _ __
/_\ \ \/ / '_ \ / _ \| '__| __|/ /_/ / |/ _` |/ _ \ '__|
//__ > <| |_) | (_) | | | |_/ __ /| | (_| | __/ |
\__/ /_/\_\ .__/ \___/|_| \__\/ /_/ |_|\__,_|\___|_|
|_|
by @R0h1rr1m
Usage of C:\Users\Public\DLLDemo\ExportHider.exe:
-h | --help Show the help message.
-i | --input <Input Path> Input path for the list of function names to be hidden. (Mandatory)
-o | --output <Output Path> Output path for the DLL template. (Mandatory)
-n | --name <DLL Name> Name of the DLL for the Export Directory. (Mandatory)
-c | --count <Number of Other Functions> Number of other exported functions that won't be hidden.
فيما يتعلق بمعامل -i | --input <Input Path>، تحتاج إلى تحديد مسار لملف الإدخال الذي يخزن أسماء الدوال المراد إخفاؤها سطرًا سطرًا. سيكون محتوى ملف إدخال مثال كالتالي:
TestFunction1
TestFunction2
TestFunction3
فيما يتعلق بمعامل -c | --count، إذا كنت لا ترغب في إخفاء جميع دوال التصدير الخاصة بك (أي أن هناك بعض الدوال المُصدّرة باستخدام __declspec(dllexport) أو ملف .def، وتظهر في دليل التصدير لملف DLL على نظام الملفات عبر عارضي ملفات PE)، فقط حدد عددها باستخدام هذا المعامل لأن الأداة تحتاج إلى هذه المعلومات عند إجراء حسابات الذاكرة.
إذا كنت ترغب في اللعب بهذه التقنية، هناك نقطتان مثيرتان للاهتمام واجهتهما أثناء تطوير المشروع. قد تحتاج إلى معرفتهما قبل تغيير المشروع:
GetProcAddress باسم. بسبب ذلك، يجب ترتيب أسماء جميع الدوال المُصدّرة، بما في ذلك المخفية، في مصفوفة AddressOfNames. وإلا، فإن الدالة GetProcAddress تُرجع NULL. لذلك، استخدمت خوارزمية Bubble Sort لترتيب أعضاء هذه المصفوفة.AddressOfNames ومصفوفة AddressOfFunctions ومصفوفة DataDirectory وبعض الحقول الأخرى قيمًا في عناوين افتراضية نسبية (RVAs). بالإضافة إلى ذلك، فهي تحتفظ بقيم RVA هذه في حقول بحجم DWORD. عندما تقوم بتخصيص منطقة ذاكرة لدليل تصدير عشوائي جديد باستخدام دوال تخصيص الذاكرة الديناميكية مثل VirtualAlloc أو HeapAlloc، ستكون العناوين المقدمة بعيدة عن منطقة تعيين DLL، وقيم RVA لا تناسب حقول بحجم DWORD، وتواجه تجاوزًا صحيحًا. ولهذا السبب استخدمت متغيرات عامة من نوع مصفوفة بايت في قالب DLL لتلبية متطلبات الذاكرة.كان هدفي الأول أثناء إنشاء هذا المشروع هو إنشاء DLL يفتقد إلى الصادرات (أو يفتقر تمامًا إلى جدول تصدير) ولكن لا يزال يتم تحميله بنجاح بواسطة العملية المنشأة حديثًا. اعتقدت أن هذا السلوك قد يوفر ساحة لعب جديدة لحمولات التحميل الجانبي لـ DLL. ومع ذلك، لم أتمكن من العثور على أي دالة أو طريقة تجعل كود إصلاح دليل التصدير يعمل قبل أن يتحقق محمل Windows من دوال DLL المُصدّرة.
بشكل أكثر تقنية، لاحظت التدفق واستدعاء الدالة التاليين لكل DLL في أقسام الكود المتعلقة بمحمل Windows في NTDLL (جُمعت من ملاحظاتي القديمة؛ قد يكون هناك بعض الأخطاء لأنني لست مهندس عكسي خبير):
1. LdrpMapDll - This is where the DLL is mapped to the Process Address Space. All DLLs that satisfy the DLL name condition
are directly put into the memory, no precheck control for the filesystem version.
2. LdrpSnapModule - This is where the Windows Loader starts to resolve imports. For each import descriptor, it parses the PE
structure, checks the export table, binary searching for the imported function, calculating RVA of that,
and writes its address to the corresponding caller's process' Import Address Table entry during this function.
3. LdrpDoPostSnapWork - If step 2 succeeds for each imported function, memory protections, TLS initialization, CFG enablement
are done in this function.
4. LdrpInitializeNode - If step 3 succeeds, there are module linking functions in this step.
5. LdrpCallTlsInitializers - This is where the TLS callbacks are called before the DllMain function.
6. LdrpCallInitRoutine - This is where the DLLMain itself is called for the first time for the imported DLL. In the original
solution, this function is too late to fix the export table.
عند تشغيل ملف تنفيذي يستورد DLL، إذا لم يتمكن محمل Windows من العثور على اسم الدالة المطلوبة في جدول التصدير لـ DLL، فإنه يوقف التنفيذ ولا ينفذ الدوال التي يتم تنفيذها بعد LdrpSnapModule.