23-10-2018, 06:31 PM
(آخر تعديل لهذه المشاركة : 11-05-2019, 05:49 AM بواسطة M!X0R.
تعديل السبب: تصحيح اتجاهات الاكواد
)
السلام عليكم ورحمة الله وبركاته .
درس اليوم عن كيفية كتابة debugger للتحكم في سير البرنامج و كيفية وضع نقط توقف Hardware Breakpoints .
كما نعلم فإن الـHardware Breakpoints - او سنرمز لها اختصاراً بـHWBP - أقصى عدد يمكن وضعه هو 4 ويتم التعامل معها عبر مسجلات التنقيح Debug Registers وهي 8 مسجلات طول كل منهم 32bit وظيفتهم كالتالي :-
Dr0-Dr1-Dr2-Dr3 : يمثل كل مسجل منهم العنوان الذي سنضع عليه نقطة التوقف .
Dr4-Dr5 : محجوزان ولا يمكن استعمالهم .
Dr6 : يسمى ايضا Debug Status Register و يمكننا من معرفة أي HWBP حدث .. سنتطرق له بالتفصيل لاحقاً .
Dr7 : يسمى بـDebug Control Register وهو الذي يتحكم بالـHWBP (الكل في الكل) ,, تركيبته كالتالي :
أول 8 بتات ( 0 -> 7 ) يمكن تقسيمهم الى اثنين اثنين بحيث كل اثنين يخصان احدى نقط التوقف ,, اذا تم وضع اول بت (L) فأن نقطة التوقف تكون local breakpoint أي يحدث الـDebug exception في الـProcess صاحب الـthread الذي غيرنا Dr7 له .
ثاني بت (G) لوصف نقطة التوقف على انها global breakpoint اي يحدث الـdebug exception اذا توافرت شروطه في جميع الـprocesses .
من R/W0 الى R/W3 :
لتحديد نوع نقطة التوقف ,, هل اذا تم القراءة من العنوان او الكتابة عليه او تنفيذه كتعليمة ,, يمكن وضع قيمته كالتالي (بالبت) :
00 : توقف عند تنفيذ التعليمة التي بالعنوان .
01 : توقف عند الكتابة على العنوان .
10 : لا تستعمل الا في ظروف خاصة (للهاردوير) .
11 : توقف عند القراءة او الكتابة من العنوان .
من LEN0 الى LEN3 :
لتحديد طول البيانات التي ستضع عليها نقطة التوقف ,, يمكن وضع قيمته كالتالي (بالبت) :
00 : لتحديد طول 1 بايت .
01 : لتحديد طول 2 بايت .
10 : تستخدم في بعض المعالجات لتحديد طول 8 بايت (اقرأ الـdocumentations الخاصة بإنتل او AMD لتفاصيل اكثر) .
11 : لتحديد طول 4 بايت .
-----------------------------------------
حسنا لقد تعرفنا بما فيه الكفاية على مسجلات التنقيح ...
الان ما الخطة التي سنعمل عليها ؟
كيف سنقوم بوضع HWBP عملياً على Process معين ؟
كيف سنعرف انه تم الوصول الى نقطة توقف معينة ؟
لتوضيح الفكرة بشكل عملي سنقوم بكتابة debugger صغير يقوم بعمل debug على برنامج crackme كمثال ووضع نقط توقف فيه وعمل اشياء اخرى .
لتنقيح برنامج معين يمكن فعل ذلك عن طريق انشاء العملية بالدالة CreateProcess ووضع بارامتر الاعلام flags == DEBUG_PROCESS ..
كفكرة عامة ما الذي يحدث عند وقوع خطأ في برنامج معين ليس تحت التنقيح ؟
يتم اظهار الـexception او كما يقال exception raise ويتم تسليم اول SEH handler التنفيذ (ليس بدقّة) .. لكن ما يحدث عند وجود البرنامج تحت التنقيح ان الـdebugger هو الذي يستلم الـexecution ونوع الـexception الذي حدث ويقرر اذا كان سيتعامل مع هذا الـexception ام يجعل البرنامج نفسه (debuggee) هو الذي يتعامل مع الـexception .. أرجو ان تكون هذه الفكرة واضحة !
دوال win32api توفر عدة دوال للتعامل مع التنقيح مثل WaitForDebugEvent - GetThreadContext - SetThreadContext - Read/WriteProcessMemory ..
طبعا نحن هنا لسنا بصدد التعرف على تلك الدوال .. يمكنك التطلع عليهم اكثر في مكتبة MSDN .
عند حدوث HWBP يتم رفع exception من النوع EXCEPTION_SINGLE_STEP الى المنقح اذا كان موجوداً .. اذا لابد علينا من عمل handling لهذا الـexception code ..
طيب سؤال اخر .. كيف نعرف اي HWBP من الاربعة هو الذي حدث ؟ لا توجد دوال api معينة لمعرفة ذلك !
صحيح .. نحن لم نتكلم عن Dr6 بالتفصيل بعد كما وعدنا وها قد حان الوقت .. تركيبة هذا المسجل بسيطة جداً كالتالي :
ما يهمنا معرفته هو أول 4 bits وهي تمثل بت لكل HWBP .. اذا تم وضع احدهم فهو يدل على ان الـHWBP المناظر له هو الذي حدث .
حسنا كيف نضع HWBP ؟؟
ستقول لي من مسجلات التنقيح Drx ؟! طيب ما انت عارف ان استخدامها privileged ولايمكن تغييرها من user-mode !!
سأقول لك لا .. أين ذهبت دالة SetThreadContext اذا ؟؟
hThread : مقبض handle للثريد الذي سنغير الـcontext له ..
lpContext : مؤشر الى structure به الـcontext الجديد ..
ما هو الـThread Context ؟؟
تعرفون ان نظام ويندوز يتمتع بخاصية الـMultithreading أي امكانية عمل اكثر من thread في نفس الـprocess .. لكل thread له مسجلاته الخاصة و المكدس و اشياء اخرى .
يوجد في قلب النظام ما يسمى بالـKernel Dispatcher وهو المسؤول عن تنظيم الـthreads او ThreadScheduling .
عند عمل swap-out لـthread لابد من حفظ كل ما يتعلق به (مسجلات - مكدس - storage areas .. الخ) في مكان آمن حتى اذا تم عمل swap-in يتم استرجاع البيانات من هذا المكان مرة اخرى والعكس .. هذا المكان او الذاكرة هي ما نقصده بالـThread Context او Context مباشرة .
تركيب الـCONTEXT strcuture هي كالتالي :
كما ترون توجد جميع مسجلات التنقيح داخل الـstrcuture ...
اذا ما سنفعله هو كالتالي :
1- جلب الـcontext عن طريق الدالة GetThreadContext .
2- تعديل مسجل Dr7 ووضع البتات الخاصة بطول الـHWBP و نوعها .
3- وضع عنوان الـHWBP في احد المسجلات الاربعة Dr0 -> Dr3 .
4- ارجاع الـcontext المعدلة عن طريق الدالة SetThreadContext .
هذا كود مبسط لشرح الكلام النظري السابق :
ولحذف HWBP نفعل مثل ما سبق لكن نصفّر الـbit الخاص بالـlocal والمسجل الخاص بالعنوان له :
لتعامل أسهل مع مسجل Dr7 قمنا بعمل structure لتسهيل الامر بدلا من انجاز كل شئ بالـshifting والـbit level :
مثال عملي :
سنقوم بعمل مثال loader على crackme بسيط للأخ dj-siba نقتنص فيه السيريال الصحيح + نتخطى رسالة badboy المزعجة .. التحدي اسمه "حاول ان تكسرني - تمرين رقم واحد" .. موجود بالمرفقات .
بتحليل سريع للتحدي اليكم المفيد :
ما سنفعله هو كالتالي .. وضع نقطة توقف للـexecution عند العنوان 0x00403FF4 وبذلك نضمن وجود السيريال الصحيح في العنوان الذي يشير اليه المسجل edx .
وضع نقطة توقف اخرى عند عنوان القفزة التي تؤدي الى اضهار رسالة عدم التسجيل 0x00403FFA .
في اول HWBP نقوم بجلب مسجل edx عن طريق GetThreadContext ثم نقوم بقراءة العنوان الذي يشير اليه المسجل من الـprocess عن طريق الدالة ReadProcessMemory ونقوم باظهار السيريال الصحيح .
في ثاني HWBP نقوم بجلب مسجل الاعلام EFLAGS و نقوم بوضع العلم ZF وذلك كي تتم القفزة المشروطة و نتخطى الرسالة المزعجة .
مسجل EFLAGS أيضا يمكن التعامل معه عن طريق الـbit level لكن قمت بعمل structure لتسهيل الأمر :
حسنا اترككم مع الكود في المرفقات .. أي شئ مبهم لا تتردد بالسؤال عنه .. أيضا اذا كان هناك خطأ في النظري او الكود ارجو ابلاغي .
المراجع :
Intel® 64 and IA-32 Architectures , System Programming Guide, Part 2
Toggle hardware data/read/execute breakpoints programmatically
صاحب الموضوع الأصلي: GamingMasteR
درس اليوم عن كيفية كتابة debugger للتحكم في سير البرنامج و كيفية وضع نقط توقف Hardware Breakpoints .
كما نعلم فإن الـHardware Breakpoints - او سنرمز لها اختصاراً بـHWBP - أقصى عدد يمكن وضعه هو 4 ويتم التعامل معها عبر مسجلات التنقيح Debug Registers وهي 8 مسجلات طول كل منهم 32bit وظيفتهم كالتالي :-
Dr0-Dr1-Dr2-Dr3 : يمثل كل مسجل منهم العنوان الذي سنضع عليه نقطة التوقف .
Dr4-Dr5 : محجوزان ولا يمكن استعمالهم .
Dr6 : يسمى ايضا Debug Status Register و يمكننا من معرفة أي HWBP حدث .. سنتطرق له بالتفصيل لاحقاً .
Dr7 : يسمى بـDebug Control Register وهو الذي يتحكم بالـHWBP (الكل في الكل) ,, تركيبته كالتالي :
أول 8 بتات ( 0 -> 7 ) يمكن تقسيمهم الى اثنين اثنين بحيث كل اثنين يخصان احدى نقط التوقف ,, اذا تم وضع اول بت (L) فأن نقطة التوقف تكون local breakpoint أي يحدث الـDebug exception في الـProcess صاحب الـthread الذي غيرنا Dr7 له .
ثاني بت (G) لوصف نقطة التوقف على انها global breakpoint اي يحدث الـdebug exception اذا توافرت شروطه في جميع الـprocesses .
من R/W0 الى R/W3 :
لتحديد نوع نقطة التوقف ,, هل اذا تم القراءة من العنوان او الكتابة عليه او تنفيذه كتعليمة ,, يمكن وضع قيمته كالتالي (بالبت) :
00 : توقف عند تنفيذ التعليمة التي بالعنوان .
01 : توقف عند الكتابة على العنوان .
10 : لا تستعمل الا في ظروف خاصة (للهاردوير) .
11 : توقف عند القراءة او الكتابة من العنوان .
من LEN0 الى LEN3 :
لتحديد طول البيانات التي ستضع عليها نقطة التوقف ,, يمكن وضع قيمته كالتالي (بالبت) :
00 : لتحديد طول 1 بايت .
01 : لتحديد طول 2 بايت .
10 : تستخدم في بعض المعالجات لتحديد طول 8 بايت (اقرأ الـdocumentations الخاصة بإنتل او AMD لتفاصيل اكثر) .
11 : لتحديد طول 4 بايت .
-----------------------------------------
حسنا لقد تعرفنا بما فيه الكفاية على مسجلات التنقيح ...
الان ما الخطة التي سنعمل عليها ؟
كيف سنقوم بوضع HWBP عملياً على Process معين ؟
كيف سنعرف انه تم الوصول الى نقطة توقف معينة ؟
لتوضيح الفكرة بشكل عملي سنقوم بكتابة debugger صغير يقوم بعمل debug على برنامج crackme كمثال ووضع نقط توقف فيه وعمل اشياء اخرى .
لتنقيح برنامج معين يمكن فعل ذلك عن طريق انشاء العملية بالدالة CreateProcess ووضع بارامتر الاعلام flags == DEBUG_PROCESS ..
كفكرة عامة ما الذي يحدث عند وقوع خطأ في برنامج معين ليس تحت التنقيح ؟
يتم اظهار الـexception او كما يقال exception raise ويتم تسليم اول SEH handler التنفيذ (ليس بدقّة) .. لكن ما يحدث عند وجود البرنامج تحت التنقيح ان الـdebugger هو الذي يستلم الـexecution ونوع الـexception الذي حدث ويقرر اذا كان سيتعامل مع هذا الـexception ام يجعل البرنامج نفسه (debuggee) هو الذي يتعامل مع الـexception .. أرجو ان تكون هذه الفكرة واضحة !
دوال win32api توفر عدة دوال للتعامل مع التنقيح مثل WaitForDebugEvent - GetThreadContext - SetThreadContext - Read/WriteProcessMemory ..
طبعا نحن هنا لسنا بصدد التعرف على تلك الدوال .. يمكنك التطلع عليهم اكثر في مكتبة MSDN .
عند حدوث HWBP يتم رفع exception من النوع EXCEPTION_SINGLE_STEP الى المنقح اذا كان موجوداً .. اذا لابد علينا من عمل handling لهذا الـexception code ..
طيب سؤال اخر .. كيف نعرف اي HWBP من الاربعة هو الذي حدث ؟ لا توجد دوال api معينة لمعرفة ذلك !
صحيح .. نحن لم نتكلم عن Dr6 بالتفصيل بعد كما وعدنا وها قد حان الوقت .. تركيبة هذا المسجل بسيطة جداً كالتالي :
ما يهمنا معرفته هو أول 4 bits وهي تمثل بت لكل HWBP .. اذا تم وضع احدهم فهو يدل على ان الـHWBP المناظر له هو الذي حدث .
حسنا كيف نضع HWBP ؟؟
ستقول لي من مسجلات التنقيح Drx ؟! طيب ما انت عارف ان استخدامها privileged ولايمكن تغييرها من user-mode !!
سأقول لك لا .. أين ذهبت دالة SetThreadContext اذا ؟؟
BOOL WINAPI SetThreadContext(
__in HANDLE hThread,
__in const CONTEXT* lpContext
);
hThread : مقبض handle للثريد الذي سنغير الـcontext له ..
lpContext : مؤشر الى structure به الـcontext الجديد ..
ما هو الـThread Context ؟؟
تعرفون ان نظام ويندوز يتمتع بخاصية الـMultithreading أي امكانية عمل اكثر من thread في نفس الـprocess .. لكل thread له مسجلاته الخاصة و المكدس و اشياء اخرى .
يوجد في قلب النظام ما يسمى بالـKernel Dispatcher وهو المسؤول عن تنظيم الـthreads او ThreadScheduling .
عند عمل swap-out لـthread لابد من حفظ كل ما يتعلق به (مسجلات - مكدس - storage areas .. الخ) في مكان آمن حتى اذا تم عمل swap-in يتم استرجاع البيانات من هذا المكان مرة اخرى والعكس .. هذا المكان او الذاكرة هي ما نقصده بالـThread Context او Context مباشرة .
تركيب الـCONTEXT strcuture هي كالتالي :
struct {
DWORD ContextFlags;
//
// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
// set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
// included in CONTEXT_FULL.
//
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
//
FLOATING_SAVE_AREA FloatSave;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_SEGMENTS.
//
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_INTEGER.
//
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_CONTROL.
//
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;
//
// This section is specified/returned if the ContextFlags word
// contains the flag CONTEXT_EXTENDED_REGISTERS.
// The format and contexts are processor specific
//
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
كما ترون توجد جميع مسجلات التنقيح داخل الـstrcuture ...
اذا ما سنفعله هو كالتالي :
1- جلب الـcontext عن طريق الدالة GetThreadContext .
2- تعديل مسجل Dr7 ووضع البتات الخاصة بطول الـHWBP و نوعها .
3- وضع عنوان الـHWBP في احد المسجلات الاربعة Dr0 -> Dr3 .
4- ارجاع الـcontext المعدلة عن طريق الدالة SetThreadContext .
هذا كود مبسط لشرح الكلام النظري السابق :
bool SetHWBP(HANDLE hThread, unsigned int linearAddress, int type, int length, int count)
{
CONTEXT context = {CONTEXT_ALL|CONTEXT_DEBUG_REGISTERS};
DR7 dr7;
if (GetThreadContext(hThread, &context))
{
dr7 = *(DR7*)&context.Dr7;
switch (count)
{
case 0:
{
context.Dr0 = linearAddress;
dr7.HWBP0_MODE = HWBP_LOCAL;
dr7.HWBP0_LENGTH = length;
dr7.HWBP0_ACCESS = type;
break;
};
case 1:
{
context.Dr1 = linearAddress;
dr7.HWBP1_MODE = HWBP_LOCAL;
dr7.HWBP1_LENGTH = length;
dr7.HWBP1_ACCESS = type;
break;
};
case 2:
{
context.Dr2 = linearAddress;
dr7.HWBP2_MODE = HWBP_LOCAL;
dr7.HWBP2_LENGTH = length;
dr7.HWBP2_ACCESS = type;
break;
};
case 3:
{
context.Dr3 = linearAddress;
dr7.HWBP3_MODE = HWBP_LOCAL;
dr7.HWBP3_LENGTH = length;
dr7.HWBP3_ACCESS = type;
break;
};
default:
return false;
};
context.Dr7 = *(PDWORD)&dr7;
return SetThreadContext(hThread, &context);
};
return false;
};
ولحذف HWBP نفعل مثل ما سبق لكن نصفّر الـbit الخاص بالـlocal والمسجل الخاص بالعنوان له :
bool RemoveHWBP(HANDLE hThread, int count)
{
CONTEXT context = {CONTEXT_ALL|CONTEXT_DEBUG_REGISTERS};
DR7 dr7;
if (GetThreadContext(hThread, &context))
{
dr7 = *(DR7*)&context.Dr7;
switch (count)
{
case 0:
{
context.Dr0 = 0;
dr7.HWBP0_MODE = 0;
break;
};
case 1:
{
context.Dr1 = 0;
dr7.HWBP1_MODE = 0;
break;
};
case 2:
{
context.Dr2 = 0;
dr7.HWBP2_MODE = 0;
break;
};
case 3:
{
context.Dr3 = 0;
dr7.HWBP3_MODE = 0;
break;
};
default:
return false;
};
context.Dr7 = *(PDWORD)&dr7;
return SetThreadContext(hThread, &context);
};
return false;
};
لتعامل أسهل مع مسجل Dr7 قمنا بعمل structure لتسهيل الامر بدلا من انجاز كل شئ بالـshifting والـbit level :
struct {
ULONG HWBP0_MODE :2;
ULONG HWBP1_MODE :2;
ULONG HWBP2_MODE :2;
ULONG HWBP3_MODE :2;
ULONG LE :1;
ULONG GE :1;
ULONG __unused :6;
ULONG HWBP0_ACCESS :2;
ULONG HWBP0_LENGTH :2;
ULONG HWBP1_ACCESS :2;
ULONG HWBP1_LENGTH :2;
ULONG HWBP2_ACCESS :2;
ULONG HWBP2_LENGTH :2;
ULONG HWBP3_ACCESS :2;
ULONG HWBP3_LENGTH :2;
}DR7;
مثال عملي :
سنقوم بعمل مثال loader على crackme بسيط للأخ dj-siba نقتنص فيه السيريال الصحيح + نتخطى رسالة badboy المزعجة .. التحدي اسمه "حاول ان تكسرني - تمرين رقم واحد" .. موجود بالمرفقات .
بتحليل سريع للتحدي اليكم المفيد :
00403FE9 CALL 00403E60
00403FEE MOV EDX,dword ptr [ebp - 208] ---------------------> good serial
00403FF4 POP EAX
00403FF5 CALL 00402F58
00403FFA JE SHORT 00404011 ---------------------> magic jump
00403FFC PUSH 0
00403FFE PUSH 00404050
00404003 PUSH 004040DC
00404008 PUSH 0
0040400A CALL <JMP.&user32.MessageBoxA> ---------------------> badboy msg
0040400F JMP SHORT 00404024
00404011 PUSH 0
00404013 PUSH 00404050
00404018 PUSH 00404100
0040401D PUSH 0
0040401F CALL <JMP.&user32.MessageBoxA> ---------------------> goodboy msg
00404024 XOR EAX,EAX
ما سنفعله هو كالتالي .. وضع نقطة توقف للـexecution عند العنوان 0x00403FF4 وبذلك نضمن وجود السيريال الصحيح في العنوان الذي يشير اليه المسجل edx .
وضع نقطة توقف اخرى عند عنوان القفزة التي تؤدي الى اضهار رسالة عدم التسجيل 0x00403FFA .
في اول HWBP نقوم بجلب مسجل edx عن طريق GetThreadContext ثم نقوم بقراءة العنوان الذي يشير اليه المسجل من الـprocess عن طريق الدالة ReadProcessMemory ونقوم باظهار السيريال الصحيح .
في ثاني HWBP نقوم بجلب مسجل الاعلام EFLAGS و نقوم بوضع العلم ZF وذلك كي تتم القفزة المشروطة و نتخطى الرسالة المزعجة .
مسجل EFLAGS أيضا يمكن التعامل معه عن طريق الـbit level لكن قمت بعمل structure لتسهيل الأمر :
struct {
ULONG CF :1;
ULONG __unused0 :1;
ULONG PF :1;
ULONG __unused1 :1;
ULONG AF :1;
ULONG __unused2 :1;
ULONG ZF :1;
ULONG SF :1;
ULONG TF :1;
ULONG IF :1;
ULONG DF :1;
ULONG OF :1;
ULONG IOPL :2;
ULONG __unused3 :1;
ULONG RF :1;
ULONG VM :1;
ULONG AC :1;
ULONG VIF :1;
ULONG VIP :1;
ULONG ID :1;
ULONG __unused4 :10;
}EFLAGS;
حسنا اترككم مع الكود في المرفقات .. أي شئ مبهم لا تتردد بالسؤال عنه .. أيضا اذا كان هناك خطأ في النظري او الكود ارجو ابلاغي .
المراجع :
Intel® 64 and IA-32 Architectures , System Programming Guide, Part 2
Toggle hardware data/read/execute breakpoints programmatically
GamingMaster aka Sadistic-X
Arab Team 4 Reverse Engineering
Arab Team 4 Reverse Engineering
صاحب الموضوع الأصلي: GamingMasteR