في هذا الدرس سنتعلم طريقة الحكم على ملف PE بصحة البنية أو بعدمها – ليس ملف PE.

النظرية:

كيف يمكننا التأكد من أنّ ملفاً معطى هو ملف PE؟ الحقيقة أنّ الإجابة على هذا السؤال صعبة! و هي تعتمد على العمق الذي نود التأكد منه. يمكننا اختبار كل بنية معطيات معرفة في صيغة ملف PE, أو يمكننا فقط اختبار أكثر الأقسام أهميّة. ففي أغلب الأحيان تكون الحكمة في اختبار الكتل الهامة فقط, فإذا كانت صحيحة يمكننا اعتبار الكتل الأخرى كذلك, و سنقوم في هذا الدرس باعتماد الطريقة الثانية الموفّرة للوقت.

و البنية الأساسية التي سنقوم باختبارها هنا هي ترويسة PE بذاتها. و للقيام بذلك سنحتاج إلى معرفة بعض المعلومات عنها –برمجياً. و كما ذكرنا سابقاً بأنّ ترويسة PE هي عبارة عن سجل (بنية) تدعى IMAGE_NT_HEADERS, و هي معرّفة بالشكل التالي:

IMAGE_NT_HEADERS STRUCT

 ?   Signature dd

<>    FileHeader IMAGE_FILE_HEADER

<>    OptionalHeader IMAGE_OPTIONAL_HEADER32

IMAGE_NT_HEADERS ENDS

لنبدأ بتفصيل حقول هذا السجل:

  1. Signature عبارة عن قيمة dword (كلمة مضاعفة) و يحوي على القيم الست عشرية التالي: 50,45,00,00 و بلغة أكثر طبيعية كلغة البشر مثلاً تحوي على الكلمة “PE” ملحقة بمحرفين صفريين. و هذا الحقل هو توقيع ملف PE و سنستخدمه للتأكد من أنّ ملفاً معطى هو ملف PE صحيح.
  2. FileHeader عبارة عن بنية تحوي معلومات حول الشكل الفيزيائي (التركيبي) لملف PE كعدد الأقسام, بنية الآلة التي يعمل عليها البرنامج, …إلخ.
  3. OptionalHeader هي بنية تحوي معلومات عن الشكل المنطقي (التنظيمي) لملف PE. و للذكر أنّ اسم Optional ليس بمعنى اختياري فهذا السجل دائماً موجود.

الآن يبدو أنّ هدفنا واضح. إذا كانت قيمة توقيع PE في الحقل Signature التابع للبنية IMAGE_NT_HEADERS تحوي القيمة “PE” متبوعة بمحرفين صفريين, يمكننا اعتبار هذا الملف ملف PE صحيح. و كمعلومة فإنّ Microsoft قامت بتعريف ثابت باسم IMAGE_NT_SIGNATURE يمكننا استخدامه فوراً. وفيما يلي بعض قيم الثوابت المعرّفة:

IMAGE_DOS_SIGNATURE equ 5A4Dh, IMAGE_OS2_SIGNATURE equ 454Eh, IMAGE_OS2_SIGNATURE_LE equ 454Ch, IMAGE_VXD_SIGNATURE equ 454Ch, IMAGE_NT_SIGNATURE equ 4550h

و الآن, السؤال التالي هو: كيف يمكننا معرفة مكان ترويسة PE؟ الإجابة ببساطة: ترويسة DOS MZ تحوي على إزاحة الملف الخاصة بترويسة PE.

إنّ ترويسة DOS MZ معرفة بالشكل IMAGE_DOS_HEADER. و العضو e_lfanew من IMAGE_DOS_HEADER يحوي على إزاحة الملف الخاصة بترويسة PE.

أصبحت الخطوات الآن كالتالي:

  1. التأكد من أنّ الملف المعطى يملك ترويسة DOS MZ صحيحة, و ذلك عن طريق مقارنة أول كلمة من الملف بقيمة IMAGE_DOS_SIGNATURE.
  2. فإذا كان للمف ترويسة DOS صحيحة, سنستخدم القيمة في e_lfanew للعثور على ترويسة PE.
  3. بمقارنة أول كلمة من ترويسة PE مع القيمة IMAGE_NT_HEADER يمكننا اعتبار الملف صحيحاً في حال تطابق القيم.

مثال:

386.

model flat, stdcall.

Option casemap:none

include \masm32\include\windows.inc

include \masm32\include\kernel32.inc

include \masm32\include\comdlg32.inc

include \masm32\include\user32.inc

includelib \masm32\lib\kernel32.lib

includelib \masm32\lib\comdlg32.lib

SEH struct

PrevLink dd ? ; the address of the previous seh structure

CurrentHandler dd ? ; the address of the exception handler

SafeOffset dd ? ; The offset where it’s safe to continue execution

PrevEsp dd ? ; the old value in esp

PrevEbp dd ? ; the old value in ebp

SEH ends

data.

AppName db “PE tutorial no.2”,0

 <> Ofn OPENFILENAME

FilterString db “Executable Files (*.exe, *ddl)”,0,”*.exe;*.dll”,0

FileOpenError db “Cannot Open the file for reading”,0

FileOpenMappingError db “Cannot map the file into memory”,0

FileValidPE db “This file is a valid PE”,0

FileInValidPE db “This file is not a valid PE”,0

?data.

 (?) Buffer db 512 dup

 ? hFile dd

 ? hMapping dd

 ? pMapping dd

 ? validPE dd

code.

start proc

mov ofn.lStructSize, SIZEOF ofn

mov ofn.lpstrFilter, OFFSET FilterString

mov ofn.lpstrFile, OFFSET buffer

mov ofn.nMaxFile, 512

mov ofn.Flags, OFN_FILEMUSTEXIST or OFN_PATHMUSTEXIST or OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY

invoke GetOpenFileName, ADDR ofn

if eax==TRUE.

Invoke CreateFile, addr buffer, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL

if eax !=INVALID_HANDLE_VALUE.

mov hFile, eax

invoke CreateFileMapping, hFile, NULL, PAGE_READONLY, 0,0,0

if eax != NULL.

Mov hMapping, eax

Invoke MapViewOfFile, hMapping, FILE_MAP_READ, 0,0,0

if eax != NULL.

Mov pMapping, eax

Assume fs:nothing

[Push fs:[0

Pop seh.PrevLink

Mov seh.CurrentHandler, offset SEHHandler

Mov seh.SafeOffset, offset FinalExit

Lea, eax, seh

Mov fs:[0], eax

Mov seh.PrevEsp, esp

Mov seh.PrevEbp, ebp

Mov edi, pMapping

Assume edi:ptr IMAGE_DOS_HEADER

if [edi].e_magic == IMAGE_DOS_SIGNATURE.

Add edi, [edi].e_lfanew

Assume edi:ptr IMAGE_NT_HEADERS

if [edi].Signature == IMAGE_NT_SIGNATURE.

Mov ValidPE, TRUE

else.

Mov ValidPE, FALSE

endif.

else.

Mov ValidPE, FALSE

endif.

:FinalExit

if ValidPE == TRUE.

Invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION

else.

Invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION

endif.

Push seh.PrevLink

[Pop fs:[0

Invoke UnmapViewOfFile, pMapping

else.

Invoke MessageBox, 0, addr FileMappingError, addr AppName, MB_OK+MB_ICONERROR

endif.

Invoke CloseHandle, hMapping

else.

Invoke MessageBox, 0, addr FileOpenMappingError, addr AppName, MB_OK+MB_ICONERROR

endif.

Invoke CloseHandle, hFile

else.

Invoke MessageBox, 0, addr FileOpenError, addr AppName, MB_OK+MB_ICONERROR

endif.

endif.

Invoke ExitProcess, 0

start endp

SEHHandler proc C uses edx pExcept:DWORD, pFrame:DWORD, pContext:DWORD, pDispatch:DWORD

Mov edx, pFrame

Assume edx:ptr SEH

Mov eax, pContext

Assume eax:ptr CONTEXT

Push [edx].SafeOffset

Pop [eax].regEip

Push [edx].PrevEsp

Pop [eax].regEsp

Push [edx].PrevEbp

Pop {eax].regEbp

Mov ValidPE, FALSE

Mov eax, ExceptionContinueExecution

Ret

SEHHandler endp

End start

التحليل:

يقوم البرنامج بفتح ملف و يتأكد من وجود ترويسة DOS, فإن وجدت, يتفحص ترويسة PE, إن كانت صحيحة يقوم باعتبار الملف تطبيق PE. في المثال السابق, قمنا باستخدام معالجة الاستثثناءات البنيوية SEH
حتى نتجنب عملية تفحص كل الأخطاء الممكنة و ندع هذه العملية للنظام, و إن حصل أي خطأ سنعتبر أنّ الملف ليس ملف PE بكل بساطة.

يقوم برنامج المثال السابق بعرض نافذة فتح ملف حتى يتمكن المستخدم من اختيار الملف المراد فحصه, ثم يقوم بفتح الملف و تحميله إلى الذاكرة, وقبل القيام بعملية التأكد يقوم البرنامج بتسجيل SEH :

Assume fs:nothing

[Push fs:[0

Pop seh.PrevLink

Mov seh.CurrentHandler, offset SEHHandler

Mov seh.SafeOffset, offset FinalExit

Lea eax, seh

Mov fs:[0], eax

Mov seh.PrevEsp, esp

Mov seh.PrevEbp, ebp

نبدأ بوضع استخدام المسجل FS كـ nothing (لأن MASM يستخدم هذا المسجل من أجل الأخطاء ERROR). ثم نقوم بتخزين عنوان Handler
SEH السابق في سجلنا حتى يستخدمه Windows. نقوم بتخزين عنوان SEH Handler الخاص بنا, و هذا هو العنوان الذي يمكن متابعة التنفيذ منه بأمان في حال حدوث خطأ ما. و القيم الحالية ل ESP و EBP حتى يتمكن SEH Handler من استرجاع حالة المكدس الطبيعية قبل متابعة تنفيذ البرنامج.

Mov edi, pMapping

Assume edi:ptr IMAGE_DOS_HEADER

if [edi].e_magic==IMAGE_DOS_SIGNATURE.

بعد الانتهاء من اعداد SEH, نقوم بمتابعة عملية التحقق. نضع عنوان البايت الأول من الملف الهدف في المسجل edi, و هو عبارة عن أول بايت في ترويسة DOS. و من أجل سهولة المقارنة, سنقوم باخبار الـ compiler بأنه بستطيع اعتبار edi يقوم بالتأشير على بنية IMAGE_DOS_HEADER ( طبعاً هذه هي الحقيقة J ). سنقوم بعد ذلك بمقارنة أول كلمة WORD من ترويسة DOS بالعبارة “MZ” و التي هي عبارة عن ثابت معرف في الملف windows.inc يدعى IMAGE_DOS_SIGNATURE. إذا كانت عملية المقارنة صحيحة, نتابع إلى ترويسة PE, و إن لم تكن, نقوم بوضع القيمة FALSE في ValidPE, و هذا يعني بأن الملف غير صالح.

add edi, [edi].e_lfanew

assume edi:ptr IMAGE_NT_HEADERS

if [edi].Signature==IMAGE_NT_SIGNATURE.

mov ValidPE, TRUE

else.

mov ValidPE, FALSE

endif.

للوصول إلى ترويسة PE, سنحتاج إلى القيمة المخزنة في e_lfanew الموجودة في ترويسة DOS. هذا الحقل يحوي على الإزاحة offset الخاصة بترويسة PE بالنسبة لبداية الملف. و هكذا, بإضافة هذه القيمة إلى edi نحصل على أول بايت من ترويسة PE. إذا كان الملف فعلا ليس ملف PE, فالقيمة في e_lfanew ستكون غير صحيحة و باستخدامها سنحصل على مؤشر هائم Wild Pointer, و هنا مكمن حدوث الخطأ, ففي حال لم نسنخدم SEH, سيتوجب علينا مقارنة هذه القيمة بحجم الملف, و هذه هي النقطة السيئة.

إذا سار كل شيء على ما يرام, سنقوم بمقارنة أول كلمة WORD من ترويسة PE مع العبارة “PE” , و التي لها الاسم IMAGE_NT_SIGNATURE و الذي يمكننا استخدامه ان أردنا. إذا كانت نتيجة المقارنة صحيحة, يمكننا اعتبار الملف ملف PE صحيح.

إذا كانت القيمة في e_lfanew غير صحيحة, يمكن حدوث خطأ سيتولى عندها SEH التحكم. و ببساطة يقوم باستعادة مؤشر المكدس, مؤشر القاعدة, و يتابع التنفيذ عند الموقع الآمن عند العلامة FinalExit.

:FinalExit
if ValidPE==TRUE.
invoke MessageBox, 0, addr FileValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
else.
invoke MessageBox, 0, addr FileInValidPE, addr AppName, MB_OK+MB_ICONINFORMATION
endif.

الكود في الأعلى هو البساطة بعينها J يقوم بفحص القيمة المخزنة في ValidPE و يعرض رسالة للمستخدم.

push seh.PrevLink
[pop fs:[0

عندا ينتهي دور SEH سنقوم بفصله عن سلسلة SEH .

انتهى الدرس الثاني.

بانتظار الـ feedback منكم!

Ammar M. Zerouk

Advertisements