روشان وقت ولادة جديدة التصور

تتناول هذه المقالة اعتراض وظائف API الرسومية باستخدام مثال DirectX 9 لـ x64 فيما يتعلق بلعبة Dota 2 .

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



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

1. مقدمة


تم التخطيط لهذه المقالة باعتبارها الأولى من سلسلة وتعطي فكرة عن كيفية استخدام API الرسومية لأغراضك الخاصة ، وتصف الوظيفة المطلوبة لفهم الجزء التالي. أخطط لتخصيص المقالة الثانية للبحث عن مؤشر إلى قائمة الكيانات في المصدر 2 (أيضًا باستخدام Dota 2 كمثال) واستخدامه بالاقتران مع Source2Gen لكتابة منطق "إضافي" (سيظهر شيء مثل هذا "خريطة الاختراق" (راجع الانتباه إلى اقتباسات ، ما هو على المحك يمكن أن ينظر إليه في الفيديو) ، أو أتمتة المقال الأول). تم التخطيط للمادة الثالثة في شكل كتابة برنامج تشغيل ، والتواصل معه (IOCTL) ، واستخدامه لتجاوز حماية VAC (شيء مشابه لهذا ).

2. لماذا أحتاجه


كنت بحاجة إلى استخدام واجهة برمجة التطبيقات الرسومية لتصحيح بصري للروبوت ، والذي كتبته لـ Dota 2 (المعلومات المرئية في الوقت الفعلي مريحة للغاية). أنا طالب دراسات عليا وأشارك في إعادة بناء الرؤوس ثلاثية الأبعاد والتحول باستخدام الصور وكاميرا العمق - الموضوع مثير جدًا للاهتمام ، ولكن ليس المفضل لدي. نظرًا لأنني كنت أفعل ذلك للعام الخامس (بدءًا من برنامج الماجستير) ، فهمت شيئًا واحدًا - نعم ، لقد درست هذا المجال جيدًا ، يمكنني بسهولة دراسة المقالات باستخدام الطرق والمناهج ، وتنفيذها. لكن هذا كل شيء ، أنا شخصياً لا يمكنني إلا تحسين الخوارزمية المستفادة التالية ، ومقارنتها بالخوارز التي تمت دراستها وتنفيذها بالفعل ، وتحديد ما إذا كنت ستستخدمها في مهمة معينة. إنها نهاية التحسين ، وليس من الممكن التوصل إلى شيء جديد ، وهو أمر مهم للغاية بالنسبة لمدرسة الدراسات العليا (حداثة الدراسة). بدأت أفكر - بينما يوجد وقت ، يمكنك العثور على موضوع جديد. تحتاج بالفعل إلى فهم الموضوع جيدًا (على المستوى الحالي) أو يمكنك سحبه بسرعة.

في موازاة ذلك ، عملت في لعبة dev ، وربما هذا هو الأكثر إثارة للاهتمام فيما يمكن للمبرمج القيام به (الرأي الشخصي) وكان مهتمًا جدًا بموضوع الروبوتات AI. في ذلك الوقت ، كان هناك موضوعان كنت أعرفهما جيدًا - ثم كنت أقوم بإنشاء شبكة تنقل ديناميكية (خادم عميل) وأدرس جزء الشبكة من أداة إطلاق النار الديناميكية. لم يكن الموضوع الذي يتضمن متصفحًا ديناميكيًا مناسبًا على الفور - لقد فعلت ذلك أثناء ساعات العمل ، وكان علي أن أطلب من الإدارة الحصول على إذن لاستخدامه في شهادتي ، إلى جانب أن موضوع الجدة كان مفتوحًا - لقد قمت أيضًا بدراسة وتطبيق النهج الحالية جيدًا حسب المقالة ، ولكن هذا لم يكن جديدا. الموضوع مع جزء الشبكة من مطلق النار الديناميكي (كنت أخطط لاستخدامه للتفاعل في الواقع الافتراضي) تعطل مرة أخرى حول حقيقة أنني كنت أفعل ذلك أثناء ساعات العمل ، وعن الجدة ، يمكنك قراءة سلسلة من المقالات من Pixonic ، حيث يقول المؤلف نفسه أن الموضوع هذا مثير للاهتمام ، حيث تم ابتكار الأساليب فقط منذ 30 عامًا ولم تتغير كثيرًا.

حول هذا الوقت ، أطلق OpenAI روبوتهم. هذا بالتأكيد ليس 5 في 5 ، لكنه كان رائع! لم أتمكن من طرح الأفكار لمحاولة صنع روبوت ، وقبل كل شيء بدأت أفكر في كيفية استخدامه كرسالة ، عن الجدة ، وكيفية تقديمها إلى قائد. مع الحداثة في هذا الصدد ، كان كل شيء أفضل بكثير - بالتأكيد كان من الممكن التوصل إلى شيء للموضوعين السابقين ، ولكن يبدو أن الروبوت جعلني أفكر وأتشبث وأطور وأبحث عن أفكار أقوى بكثير. لذلك ، قررت إنشاء روبوت واحد على واحد (معركة في المنتصف ، مثل OpenAI) ، وتقديمها للقائد ، وإخبار مدى روعتها ، وعدد المناهج المختلفة ، والرياضيات ، والأهم من ذلك ، المعركة الجديدة.

الشيء الأكثر أهمية الذي يحتاجه الروبوت في المرحلة الأولى هو معرفة البيئة الموجودة فيه - كنت أنوي أخذ حالة العالم من ذاكرة اللعبة وقضيت المرحلة الأولى في البحث عن مؤشر إلى قائمة الكيانات والتكامل مع بنات أفكار الصلاة Dog2 Source2Gen - هذا الشيء يولد هيكل محرك Source2 ، والذي يأخذ من الدوائر. إن الفكرة الرئيسية والشرط الأساسي لظهور المخططات هو تكرار الحالة بين العميل والخادم ، ولكن يبدو أن المطورين قد أحبوا الفكرة بالفعل وقاموا بتوزيعها على نطاق أوسع كثيرًا ، أنصحك بقراءة هنا .

كانت لدي تجربة هندسية معكوسة: لقد قمت بالغش في Silent Storm ، وصنعت مولدات رئيسية (كان الأكثر إثارة للاهتمام بالنسبة لـ Black & White) - ما يمكن قراءة keygen من DrMefistO هنا ، تنفيذ combo في Cabal Online (كل شيء معقد بسبب حقيقة أن Game Guard كانت محمية ، وحمايتها من ring0 (تحت برنامج التشغيل في وضع kernel) ، وإخفاء العملية (والتي على الأقل لا تجعل من السهل التسلل إليها) - يمكن قراءة المزيد من التفاصيل هنا ).
وفقًا لذلك ، حدثت تطورات في هذا المجال ، حيث وصل الروبوت إلى البيئة في الوقت المخطط له. إنه لأمر مدهش مقدار المعلومات التي ينسخها خادم المستودعات عبر الدلتا إلى العميل ، على سبيل المثال ، لدى العميل معلومات حول أي ناقلين عن بُعد ، الصحة وتغييراته بين الوكلاء (باستثناء روشان ، فهو لا يكرر) - ​​كل هذا في ضباب الحرب. على الرغم من أنني واجهت بعض الصعوبات ، إلا أن هذا ما سأتحدث عنه في المقالة التالية.
إذا كان لديك سؤال لماذا لم أستخدم Dota Bot Scripting ، فسوف أجيب مع مقتطف من الوثائق:
واجهة برمجة التطبيقات مقيدة بحيث لا يمكن للغش في البرامج النصية - لا يمكن الاستعلام عن الوحدات في FoW ، ولا يمكن إصدار الأوامر إلى الوحدات التي لا يتحكم البرنامج النصي فيها ، إلخ.
هذه السلسلة من المقالات موجهة للمبتدئين المهتمين بموضوع الهندسة العكسية.

3. لماذا أنا أكتب عن هذا


نتيجةً لذلك ، واجهت العديد من المشكلات في تنفيذ برنامج الروبوت من ml ، والذي قضيت وقتًا كافيًا لأدرك أنه قبل عامين من انتهاء التدريب لم أتمكن من تجاوز معرفتي وخبرتي في الموضوع الحالي. في Dota 2 لا ألعب من خلال إصدار Dota Auto Chess المخصص ، أقضي الآن وقت فراغي في دبلوم وعكس Apex Legend (هيكلها يشبه إلى حد كبير Dota 2 ، كما يبدو لي). وفقًا لذلك ، فإن الفائدة الوحيدة من العمل المنجز هي نشر مقالة فنية حول هذا الموضوع.

4. دوتا 2


أخطط لإظهار هذه المبادئ في لعبة حقيقية - Dota 2. تستخدم اللعبة مكافحة الغش Valve Anti Cheat . أنا حقًا أحب Valve كشركة: منتجات رائعة جدًا ، مدير ، موقف اللاعبين ، Steam ، Source Engine 2 ، ... VAC. تعمل VAC من وضع المستخدم (ring3) ، ولا تفحص كل شيء وغير ضارة مقارنةً بمضادات الغش الأخرى (كل ما تفعله esea (على وجه التحديد مكافحة الغش) يجعل كل الرغبة في استخدام هذه المنصة تختفي). أنا متأكد من أن VAC تقوم بعملها بطريقة متفرقة - فهي لا تراقب من وضع kernel ، ولا تحظر الأجهزة (فقط حساب) ، ولا تدخل علامات مائية في لقطات الشاشة - بفضل موقف Valve تجاه اللاعبين ، لا يقومون بتثبيت برنامج مكافحة فيروسات كامل لك Game Guard و BattlEye و Warden وغيرها ، لأنه تم اختراقها وتنفق موارد المعالج التي قد تأخذها اللعبة (حتى لو تم ذلك بشكل دوري) ، هناك إيجابيات كاذبة (خاصة للاعبين على أجهزة الكمبيوتر المحمولة). ليس هناك اختراق للجدار ، aimbot ، اختراق السرعة ، ESP في PUBG ، Apex ، Fortnite؟

في الواقع حول Dota 2. يتم تشغيل اللعبة على تردد 40 هرتز (25 مللي ثانية) ، يقوم العميل بتحريف حالة اللعبة ، ولا يتم استخدام التنبؤ بالإدخال - إذا كان لديك تأخر ، لعبة - من المهم ألا يتم تجميد اللعبة تمامًا. يقوم خادم ميكانيكا اللعبة بتبادل الرسائل المشفرة مع العميل عبر RUDP (UDP الموثوق) ، حيث يرسل العميل أساسًا المدخلات (إذا كنت تستضيف اللوبي ، يمكن إرسال الأوامر) ، يرسل الخادم نسخة طبق الأصل من عالم اللعبة وفرقها. يتم التنقل على شبكة ثلاثية الأبعاد ، ولكل خلية نوع من أنواع المباح الخاصة بها. تتم الحركة باستخدام الملاحة والفيزياء (استحالة المرور من شق الشاكر ، kogi clokverka ، إلخ).

حالة العالم مع جميع الكيانات موجودة في الذاكرة في أنقى صورها دون تشفير - يمكنك دراسة ذاكرة اللعبة باستخدام محرك الغش. لا ينطبق التعتيم على الأوتار والكود.

تتوفر DirectX9 و DirectX11 و Vulkan و OpenGL من واجهة برمجة التطبيقات الرسومية.


5. بيان المشكلة


في لعبة Dota 2 ، هناك "قديم" محايد ، يعطي قتله مكافأة جيدة: الخبرة ، والذهب ، والقدرة على التراجع عن التزاوج بين المهارات والأشياء ، ايجيس (الحياة الثانية) ، اسمه روشان. يمكن لـ Getting Aegis أن يقلب اللعبة بشكل أساسي أو يعطي ميزة أكبر للجانب الأقوى ، على التوالي ، يحاول اللاعبون أن يتذكروا / يسجلوا وقت وفاته من أجل التخطيط لموعد اللقاء ومهاجمته ، أو أن يكونوا قريبين لحمايته. يتم إخطار جميع اللاعبين العشرة بوفاة روشان ، بغض النظر عما إذا كان مخفياً في ضباب الحرب. يكون وقت الاستراحة ثماني دقائق إلزامية ، وبعدها قد يظهر روشان بشكل عشوائي في فترة ثلاث دقائق.

المهمة كالتالي : لتزويد اللاعب بمعلومات عن حالة روشان الحالية (على قيد الحياة ، ressurect_base-revives الوقت الأساسي ، ressurect_extra-revives وقت إضافي).


الشكل 1 - شروط الانتقال بين الدول والإجراءات أثناء الانتقال

بالنسبة للظروف التي مات فيها روشان ، اعرض وقت انتهاء الإقامة في هذه الحالة. يجب أن يتم الانتقال من الحالة المباشرة إلى ressurect_base بواسطة المشغل في الوضع اليدوي بواسطة الزر. في حالة اكتشاف / وفاة روشان في حالة ressurect_extra (على سبيل المثال ، تسلل فريق عدو سرًا إلى العرين وقتلوه) ، يتم أيضًا الانتقال إلى حالة alive / ressurect_base يدويًا باستخدام الزر. يجب أن تظهر حالة روشان (ووقت الانتهاء من كونها في حالة إحياء) في شكل نص ، ويجب توفير المدخلات اللازمة (قتل وانقطاع حالة ressurect_extra) مع زر.


الشكل 2 - عناصر واجهة - التسمية ، زر وقماش

هذه هي المهمة الوحيدة التي يمكن أن أتوصل إليها حتى لا أحتاج للعمل مع ذاكرة اللعبة وكان هناك بعض القيمة على الأقل بالنسبة للاعب - حتى لاشتقاق أي خصائص أولية ، مثل الصحة والمانا ومواقف الكيانات ، فأنت بحاجة إلى العثور عليها مسبقًا. ساعد محرك الغش في ذاكرة اللعبة ، والذي يحتاج إلى شرح إضافي لفترة طويلة إلى حد ما ، أو بمساعدة Source2Gen ، والتي ستتم مناقشتها في المقالة التالية. يفرض بيان المشكلة على اللاعب اتباع روشان ، ونقل الكثير من الإجراءات إليه ، وهو أمر غير مريح إلى حد ما - ولكن سيكون هناك شيء يمكن الاعتماد عليه في الجزء الثاني.

سنكتب injected.dll ، والذي سيحتوي على منطق الأعمال المستندة إلى MVC وتطبيقه في عملية Dota 2. سوف تستخدم مكتبة DLL الخاصة بنا silk_way.lib ، والتي ستحتوي على منطق الملاءمة لتغيير تدفق التنفيذ والمسجل وماسح الذاكرة وهياكل البيانات .

6. حاقن


إنشاء مشروع C ++ فارغة ، استدعاء NativeInjector. الكود الرئيسي هو في وظيفة الحقن.

void Inject(string & dllPath, string & processName) { DWORD processId = GetProcessIdentificator(processName); if (processId == NULL) throw invalid_argument("Process dont existed"); HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, processId); HMODULE hModule = GetModuleHandle("kernel32.dll"); FARPROC address = GetProcAddress(hModule, "LoadLibraryA"); int payloadSize = sizeof(char) * dllPath.length() + 1; LPVOID allocAddress = VirtualAllocEx( hProcess, NULL, payloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); SIZE_T written; bool writeResult = WriteProcessMemory(hProcess, allocAddress, dllPath.c_str(), payloadSize, & written); DWORD treadId; CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) address, allocAddress, 0, & treadId); CloseHandle(hProcess); } 

تحصل الوظيفة على مسار العملية واسمها ، وتبحث عن المعرف الخاص بها باسم العملية باستخدام GetProcessIdentificator.

وظيفة GetProcessIdentificator
 DWORD GetProcessIdentificator(string & processName) { PROCESSENTRY32 processEntry; processEntry.dwSize = sizeof(PROCESSENTRY32); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL); DWORD processId = NULL; if (Process32First(snapshot, & processEntry)) { while (Process32Next(snapshot, & processEntry)) { if (!_stricmp(processEntry.szExeFile, processName.c_str())) { processId = processEntry.th32ProcessID; break; } } } CloseHandle(snapshot); return processId; } 


باختصار ، يعمل GetProcessIdentificator من خلال جميع العمليات الجارية ويبحث عن عملية بالاسم المناسب.


الشكل 3 - الحالة الأولية للعملية

بعد ذلك ، التنفيذ المباشر للمكتبة عن طريق إنشاء دفق بعيد.

شرح مفصل لوظيفة الحقن
استنادًا إلى المعرف الذي تم العثور عليه ، يتم فتح العملية باستخدام وظيفة OpenProcess مع حقوق إنشاء سلسلة رسائل ، وتلقي معلومات العملية ، وقدرات الكتابة والقراءة. تقوم الدالة GetModuleHandle باسترداد الوحدة النمطية لمكتبة kernel32 ، ويتم ذلك للحصول على عنوان وظيفة LoadLibraryA الموجودة فيه بواسطة دالة GetProcAddress. الغرض من LoadLibrary هو تحميل injected.dll الخاص بنا في العملية المحددة. بمعنى ، نحن بحاجة إلى استدعاء LoadLibrary من العملية التي تهمنا ("Dota2.exe") ، لذلك ننشئ عن بعد موضوعًا جديدًا باستخدام CreateRemoteThread. كمؤشر إلى الوظيفة التي يبدأ منها مؤشر الترابط الجديد ، نقوم بتمرير عنوان دالة LoadLibraryA. إذا نظرت إلى توقيع الدالة LoadLibraryA ، فإنها تتطلب المسار إلى المكتبة المحملة كوسيطة - HMODULE LoadLibraryA (LPCSTR lpLibFileName). نقدم هذه الوسيطة كما يلي: CreateRemoteThread في المعلمات بعد أن يأخذ عنوان الدالة start مؤشرًا إلى المعلمات الخاصة به ، ونقوم بتكوين مؤشر إلى lpLibFileName عن طريق كتابة القيمة إلى ذاكرة العملية باستخدام دالة WriteProcessMemory (بعد تخصيص الذاكرة باستخدام VirtualAllocEx).


الشكل 4 - إنشاء تيار بعيد

تأكد من إغلاق معالج العملية في النهاية بوظيفة CloseHandle ، كما يمكنك تحرير الذاكرة المخصصة. حاقننا جاهز وينتظر منا أن نكتب منطق العمل في injected.dll مع مكتبة silk_way.lib.


الشكل 5 - استكمال تنفيذ المكتبة

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

7. طريق الحرير


لنبدأ في تطبيق silk_way.lib ، وهي مكتبة ثابتة تحتوي على هياكل بيانات ومسجل وماسح ضوئي للذاكرة وفخاخ. في الواقع ، أخذت جزءًا صغيرًا من عملي ، وهو شيء يمكن شرحه بأكثر بساطة ، وهو غير مرتبط بالباقي ، ولكنه في الوقت نفسه يحل المشكلة.

7.1. هياكل البيانات.


باختصار حول بنيات البيانات: Vector - القائمة الكلاسيكية ، وقت الإدراج والحذف O (N) ، البحث O (N) ، الذاكرة O (N) ؛ قائمة الانتظار - قائمة انتظار دائرية ، وقت الإدراج وحذف O (1) ، لا بحث ، ذاكرة O (N) ؛ RBTree - شجرة الأحمر والأسود ، وقت الإدراج والحذف O (logN) ، البحث O (logN) ، الذاكرة O (N). أفضّل التجزئة المستخدمة في تنفيذ القواميس في C # و Python ، الأشجار الحمراء السوداء التي تستخدمها مكتبة C ++ القياسية. والسبب في ذلك هو أنه من الصعب تنفيذ تجزئة بشكل صحيح أكثر من الشجرة (تقريبًا كل نصف عام أجده وأجرّب أنواعًا من التجزئات) ، وعادة ما تأخذ التجزئة المزيد من الذاكرة (على الرغم من أنها تعمل بشكل أسرع). يتم استخدام هذه الهياكل لإنشاء مجموعات سواء في منطق العمل أو في الفخاخ.

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

7.2. مسجل


نفذت الإخراج إلى وحدة التحكم والكتابة إلى ملف. التفاعل:

 class ILogger { protected: ILogger(const char * _path) { path = path; } public: virtual ~ILogger() {} virtual void Log(const char * format, ...) = 0; protected: const char * path; }; 

تنفيذ الإخراج إلى ملف:

 class MemoryLogger: public ILogger { public: MemoryLogger(const char * _path): ILogger(_path) { fopen_s( & fptr, _path, "w+"); } ~MemoryLogger() { fclose(fptr); } void Log(const char * format, ...) { char log[MAX_LOG_SIZE]; log[MAX_LOG_SIZE - 1] = 0; va_list args; va_start(args, format); vsprintf_s(log, MAX_LOG_SIZE, format, args); va_end(args); fprintf(fptr, log); } protected: FILE * fptr; }; 

تنفيذ الإخراج إلى وحدة التحكم هو نفسه. إذا أردنا استخدام التسجيل ، فمن الضروري تحديد واجهة ILogger * ، والإعلان عن المسجل الضروري ، واستدعاء وظيفة السجل بالتنسيق المطلوب ، على سبيل المثال:

 ILogger* logger = new MemoryLogger(filename); logger->Log("(%llu)%s: %d\n", GetCurrentThreadId(), "EnumerateThread result", result); 

7.3. الماسح الضوئي


يشارك الماسح الضوئي في حقيقة أنه يعرض قيمة الذاكرة التي يشير إليها المؤشر المنقول ويقارنها بالعينة الموجودة في الذاكرة. مقارنة وظيفية مع نمط سيتم النظر في وقت لاحق.

التفاعل:

 class IScanner { protected: IScanner() {} public: virtual ~IScanner() {} virtual void PrintMemory(const char * title, unsigned char * memPointer, int size) = 0; }; 

تطبيق ملف الرأس:

 class FileScanner : public IScanner { public: FileScanner(const char* _path) : IScanner() { fopen_s(&fptr, _path, "w+"); } ~FileScanner() { fclose(fptr); } void PrintMemory(const char* title, unsigned char* memPointer, int size); protected: FILE* fptr; }; 

تطبيق ملف المصدر:

 void FileScanner::PrintMemory(const char* title, unsigned char* memPointer, int size) { fprintf(fptr, "%s:\n", title); for (int i = 0; i < size; i++) fprintf(fptr, "%x ", (int)(*(memPointer + i))); fprintf(fptr, "\n", title); } 

لاستخدامها ، تحتاج إلى تحديد واجهة IScanner * ، والإعلان عن الماسح الضوئي المطلوب واستدعاء وظيفة PrintMemory ، حيث يمكنك تعيين العنوان والمؤشر والطول ، على سبيل المثال:

 IScanner* scan = new ConsoleScanner(); scan->PrintMemory("source orig", (unsigned char*)source, 30); 

7.4. الفخاخ


الجزء الأكثر إثارة للاهتمام من مكتبة silk_way.lib. تستخدم السنانير لتغيير تدفق تنفيذ البرنامج. إنشاء مشروع قابل للتنفيذ يسمى Sandbox.

فئة الجهاز ستكون دمية لدينا للتحقيق في تشغيل الفخاخ.
 class Unknown { protected: Unknown() {} public: ~Unknown() {} virtual HRESULT QueryInterface() = 0; virtual ULONG AddRef(void) = 0; virtual ULONG Release(void) = 0; }; class Device : public Unknown { public: Device() : Unknown() {} ~Device() {} virtual HRESULT QueryInterface() { return 0; } virtual ULONG AddRef(void) { return 0; } virtual ULONG Release(void) { return 0; } virtual int Present() { cout << "Present()" << " " << i << endl; return i; } virtual void EndScene(int j) { cout << "EndScene()" << " " << i << " " << j << endl; } void Dispose() { cout << "Dispose()" << " " << i << endl; } public: int i; }; 


يتم توريث فئة الجهاز من واجهة IUnknown ، ومهمتنا هي اعتراض استدعاء وظائف Present و EndScene لأي مثيل لجهاز ، واستدعاء الوظائف الأصلية في جهاز الاستقبال. نحن لا نعرف المكان في الكود حيث وأين يتم استدعاء هذه الوظائف ، في أي موضوع.

بالنظر إلى وظائف Present و EndScene ، نرى أنها ظاهرية. هناك حاجة إلى وظائف افتراضية لتجاوز سلوك الفئة الأصل. تعتبر الدالات الافتراضية ، بالإضافة إلى الدوال غير الافتراضية ، مؤشرًا على ذاكرة يتم فيها كتابة رموز الشفرة وقيم الوسيطة. نظرًا لاختلاف الوظائف الافتراضية بين الورثة والآباء ، فإن لديهم مؤشرات مختلفة (هذه وظائف مختلفة تمامًا) ويتم تخزينها في جدول الأساليب الافتراضية (VMT). يتم تخزين هذا الجدول في الذاكرة وهو مؤشر لمؤشر الفصل ، ونجده للجهاز:

 Device* device = new Device(); unsigned long long vmt = **(unsigned long long**)&device; 

يخزن VMT المؤشرات إلى وظائف افتراضية ، إذا كنا نريد أن نرث من الجهاز ، فإن الوريث سيحتوي على VMT. يخزن VMT مؤشرات الدالة بالتتابع بخطوة مساوية لحجم المؤشر (بالنسبة إلى x86 ، يكون 4 بايت ، بالنسبة إلى x64 هو 8) ، بما يتوافق مع الترتيب الذي تم تعريف الوظيفة به في الفصل. ابحث عن المؤشرات إلى الدالتين الحالية و EndScene ، والموجودة في المكانين الثالث والرابع:

 typedef int (*pPresent)(Device*); typedef void (*pEndScene)(Device*, int j); pPresent ptrPresent = nullptr; pEndScene ptrEndScene = nullptr; int main() { //declare Device and find pointer vmt ptrPresent = (pPresent)(*(unsigned long long*)(vmt + 8 * 3)); ptrEndScene = (pEndScene)(*(unsigned long long*)(vmt + 8 * 4)); } 

من المهم أيضًا أن يحتوي مؤشر أسلوب الفئة على الوسيطة الأولى كمرجع إلى مثيل الفئة. في C ++ ، C # ، يتم إخفاء هذا منا ، والمعلم يعرف عنه - في بيثون تتم الإشارة إلى النفس صراحة بواسطة المعلمة الأولى في طريقة الفصل. المزيد عن اتفاقية الاتصال هنا ، تحتاج إلى البحث عن هذا الاتصال.

ضع في اعتبارك التعليمات e9 ff 3a fd ff - هنا e9 هو شفرة تشغيل (مع رموز تعبير JMP) تخبر المعالج بتغيير المؤشر إلى التعليمات (EIP for x86 ، RIP لـ x64) ، انتقل من العنوان الحالي إلى FFFD3AFF (4294785791). تجدر الإشارة أيضًا إلى أن أرقام الذاكرة يتم تخزينها "بالعكس". وظائف لها مقدمة و epilogue ويتم تخزينها في قسم .code. دعونا نرى ما يتم تخزينه مع المؤشر إلى وظيفة الحاضر باستخدام الماسح الضوئي:

 IScanner* scan = new ConsoleScanner(); scan->PrintMemory("Present", (unsigned char*)ptrPresent, 30); 

في وحدة التحكم نرى:

 Present: 48 89 4c 24 8 48 83 ec 28 48 8d 15 40 4a 0 0 48 8b d 71 47 0 0 e8 64 10 0 0 48 8d 

لفهم مجموعة هذه الرموز ، يمكنك إلقاء نظرة على الجدول ، أو استخدام أدوات التفكيك المتاحة. سوف نأخذ مفكك تفكيك جاهز - hde (محرك قرصان متسلل). يمكنك أيضًا إلقاء نظرة على التشويش والتتويج للمقارنة. قم بتمرير مؤشر إلى إحدى الوظائف إلى أي أداة فك الشفرة وسوف يحدد ماهية الشفرات التي يستخدمها ، وقيم الوسائط ، وما إلى ذلك.

7.4.1 شفرة التشغيل


الآن نحن على استعداد للذهاب مباشرة إلى الفخاخ. سوف نلقي نظرة على Opcode Hook و Hardware Breakpoint. الفخاخ الأكثر شيوعًا التي أوصي بتنفيذها واستكشافها.

ربما يكون مصيدة Opcode Hook الأكثر استخدامًا وبسيطة (في مصائد قوائم المقالات ، يطلق عليه Byte patching) - لاحظ أنه يمكن التعرف عليه بسهولة عن طريق مكافحة الغش عند سوء الاستخدام (دون فهم كيفية عمل مضادات الغش ، دون معرفة ما هو مجال وقسم الذاكرة الذي يقوم بمسحها في اللحظة الحالية وأشياء أخرى الحظر لن تبطئ في الانتظار). عند استخدامه بمهارة ، هذا فخ كبير وسريع وسهل الفهم.
إذا كنت تقرأ مقالاً تقوم بتشغيل الكود عليه في نفس الوقت وكنت في وضع التصحيح ، فانتقل إلى الإصدار - هذا أمر مهم.

لذلك ، اسمحوا لي أن أذكرك ، نحن بحاجة إلى اعتراض تنفيذ وظائف Present و EndScene.
نحن نطبق اعتراض - وظائف حيث نريد نقل السيطرة:

 int PresentHook(Device* device) { cout << "PresentHook" << endl; return 1; } void EndSceneHook(Device* device, int j) { cout << "EndSceneHook" << " " << j << endl; } 

دعونا نفكر في التجريدات التي نحتاجها. نحتاج إلى واجهة ستمكننا من تعيين فخ وإزالته وتقديم معلومات عنه. يجب أن تحتوي معلومات حول الملاءمة على مؤشر للاعتراض على الوظيفة ، والمتلقي ووظائف نقطة الانطلاق (حقيقة أننا اعترضنا هذه الوظيفة لا تعني أنه لم تعد هناك حاجة إليها ، فنحن نريد أيضًا أن نكون قادرين على استخدامها - فإن نقطة الانطلاق ستساعد في استدعاء الوظيفة المعترضة الأصلية).

 #pragma pack(push, 1) struct HookRecord { HookRecord() { reservationLen = 0; sourceReservation = new void*[RESERV_SIZE](); } ~HookRecord() { reservationLen = 0; delete[] sourceReservation; } void* source; void* destination; void* pTrampoline; int reservationLen; void* sourceReservation; }; #pragma pack(pop) class IHook { protected: IHook() {} public: virtual ~IHook() {} virtual void SetExceptionHandler( PVECTORED_EXCEPTION_HANDLER pVecExcHandler) = 0; virtual int SetHook(void* source, void* destination) = 0; virtual int UnsetHook(void* source) = 0; virtual silk_data::Vector<HookRecord*>* GetInfo() = 0; virtual HookRecord* GetRecordBySource(void* source) = 0; }; 

توفر لنا واجهة IHook هذه القدرات. نريد أنه عندما يستدعي أي مثيل لفئة الجهاز الدالتين Present و EndScene (أي أن مؤشر RIP ينتقل إلى هذين العنوانين) ، يتم تنفيذ الدالتين PresentHook و EndSceneHook لدينا وفقًا لذلك.

تخيل بصريًا كيف توجد الوظيفة المعترضة والمستقبل واللوحة الرئيسية في الذاكرة (قسم الرمز.) في الوقت الذي يدخل فيه عنصر التحكم الوظيفة المعترضة:


الشكل 6 - الحالة الأولية للذاكرة ، يذهب التنفيذ إلى الوظيفة المعترضة

الآن نريد أن ينتقل RIP (السهم الأحمر) من المصدر إلى بداية الوجهة. كيف نفعل ذلك؟ كما ذكرنا أعلاه ، تحتوي الذاكرة المصدر على شفرة تشغيل سوف ينفذها المعالج عندما يصل التنفيذ إلى المصدر. في جوهر الأمر ، نحتاج إلى الانتقال من جزء إلى آخر ، وإعادة توجيه مؤشر RIP. كما قد تكون خمنت ، هناك شفرة تشغيل تسمح لك بنقل التحكم من العنوان الحالي إلى العنوان المرغوب فيه ، وهذا ما يسمى اختصارات JMP.

يمكنك الانتقال مباشرة إلى العنوان المطلوب ، أو بالنسبة إلى العنوان الحالي ، يمكن العثور على هذه القفزات في اللوحة - ff و e9 ، على التوالي. إنشاء هياكل لهذه التعليمات:

 #pragma pack(push, 1) // 32-bit relative jump. typedef struct { unsigned char opcode; unsigned int delta; } JMP_REL; // 64-bit absolute jump. typedef struct { unsigned char opcode1; unsigned char opcode2; unsigned int dummy; unsigned long long address; } JMP_ABS; #pragma pack(pop) 

تعليمات القفز النسبي أقصر ، ولكن هناك قيود - يقول int غير الموقعة أنه يمكنك القفز في حدود 4،294،967،295 ، وهو ما لا يكفي لـ x64.
وفقًا لذلك ، يمكن أن يتجاوز عنوان وظيفة الوجهة في جهاز الاستقبال الوجهة هذه القيمة بسهولة وأن يكون خارج علامة int غير الموقعة ، وهو أمر ممكن تمامًا لعملية x64 (لأن x86 كل شيء أبسط بكثير ويمكنك تقييد نفسك بهذه القفزة النسبية للغاية لتنفيذ ربط Opcode). تستغرق القفزة المباشرة 14 بايتًا ، للمقارنة ، القفزة النسبية هي 5 فقط (قمنا بتعبئة الهياكل ، مع الانتباه إلى # pragma pack (push ، 1)).

نحتاج إلى إعادة كتابة القيمة من المصدر إلى أحد إرشادات القفزات هذه.
قبل أن تحصل على وظيفة ، يجب أن تدرسها - أسهل طريقة للقيام بذلك هي مع مصحح أخطاء (سأريكم كيفية القيام بذلك باستخدام x64dbg لاحقًا) ، أو مع أداة فك تجميع. في الوقت الحاضر ، نخرج بالفعل 30 بايت من بدايتها ، تحتل التعليمات 48 89 4c 24 8 5 بايت.
دعنا ننفذ قفزة نسبية. أنا أحب هذا الخيار أكثر بسبب طول التعليمات. الفكرة هي: نحن نستبدل البايتات الخمسة الأولى من الوظيفة الأصلية ، مع الحفاظ على البايتات التي تم تغييرها ، واستبدالها بنقطة نسبية إلى عنوان التعليمات ، والتي تقع داخل int غير الموقعة.


الشكل 7 - يتم استبدال مصدر 5 بايت من وظيفة المصدر بواسطة قفزة نسبية

ما الذي يعطينا قفزة إلى الذاكرة المخصصة (المنطقة الأرجواني) ، كيف اقتربنا من نقل التحكم إلى الوجهة باستخدام هذا الإجراء؟ في الذاكرة المخصصة من قبلنا ، هناك قفزة مباشرة ، والتي سوف تنقل RIP إلى الوجهة.


الشكل 8 - تحويل RIP إلى وظيفة المستقبل

يبقى لمعرفة كيفية استدعاء وظيفة اشتعلت. نحتاج إلى تنفيذ التعليمات المحشورة وبدء التنفيذ من الجزء الذي لم يمسها المصدر. نمضي على النحو التالي - احفظ الإرشادات التالفة إلى بداية الترامبولين ، وتذكر عدد البايتات التي تعرضت للتلف والقفز مباشرة إلى المصدر + الفاسدين ، إلى الإرشادات "الصحية".

تنفيذ التعليمات المحفوظة تمحى بواسطة قفزة نسبية:


الشكل 9 - استخدام نقطة انطلاق لاستدعاء دالة تم اعتراضها

تنفيذ إضافي للتعليمات التي لم تؤثر على المزج:


الشكل 10 - استمرار تنفيذ تعليمات الوظيفة المعترضة

مدونة تنفيذ الفكرة المذكورة أعلاه
 int OpcodeHook::SetHook(void* source, void* destination) { auto record = new HookRecord(); record->source = source; record->destination = destination; info->PushBack(record); JMP_ABS pattern = {0xFF, 0x25, 0x00000000, // JMP[RIP + 6] empty 0x0000000000000000 }; // absolute address pattern.address = (ULONG_PTR)source; int currentLen = 0; int redLine = sizeof(JMP_REL); while (currentLen < redLine) { hde64s context; const void* pSource = (void*)((unsigned char*)source + currentLen); hde64_disasm(pSource, &context); memcpy((unsigned char*)record->sourceReservation + currentLen, pSource, context.len); record->reservationLen += context.len; currentLen += context.len; } int trampolineMemorySize = 2 * sizeof(JMP_ABS) + record->reservationLen; record->pTrampoline = AllocateMemory(source, trampolineMemorySize); pattern.address = (unsigned long long)(unsigned char*)source + record->reservationLen; memcpy((unsigned char*)record->pTrampoline, record->sourceReservation, record->reservationLen); int offset = record->reservationLen; memcpy((unsigned char*)record->pTrampoline + offset, &pattern, sizeof(JMP_ABS)); pattern.address = (ULONG_PTR)destination; ULONG_PTR relay = (ULONG_PTR)record->pTrampoline + sizeof(pattern) + record->reservationLen; memcpy((void*)relay, &pattern, sizeof(pattern)); DWORD oldProtect = 0; VirtualProtect(source, sizeof(JMP_REL), PAGE_EXECUTE_READWRITE, &oldProtect); JMP_REL* pJmpRelPattern = (JMP_REL*)source; pJmpRelPattern->opcode = 0xE9; pJmpRelPattern->delta = (unsigned int)((LPBYTE)relay - ((LPBYTE)source + sizeof(JMP_REL))); VirtualProtect(source, sizeof(JMP_REL), oldProtect, &oldProtect); return SUCCESS_CODE; } 


شرح وظيفة SetHook
يتم إنشاء سجل يخزّن معلومات حول الملاءمة ، وبعد ذلك يضاف السجل إلى المجموعة. يتم الزحف إلى التعليمات من بداية عنوان المصدر حتى يمكن إدخال تعليمة الانتقال النسبية بالكامل (5 بايت) ، ويتم نسخ التعليمات المحشورة إلى الحجز ، ويُذكر طولها.
هناك نقطة مهمة للغاية وهي أننا نحتاج إلى تخصيص ذاكرة للقفزة والترحيل ، حيث سنقوم بتخزين الإرشادات الخاصة بإعادة توجيه الدفق من المصدر إلى الوجهة ، ويجب أن يكون عنوان هذه الذاكرة ضمن الحدود التي يمكن للقفزة النسبية الانتقال إليها (غير موقعة) الباحث).

تطبق هذه الوظيفة الدالة AllocateMemory.

 void* OpcodeHook::AllocateMemory(void* origin, int size) { const unsigned int MEMORY_RANGE = 0x40000000; SYSTEM_INFO sysInfo; GetSystemInfo(&sysInfo); ULONG_PTR minAddr = (ULONG_PTR)sysInfo.lpMinimumApplicationAddress; ULONG_PTR maxAddr = (ULONG_PTR)sysInfo.lpMaximumApplicationAddress; ULONG_PTR castedOrigin = (ULONG_PTR)origin; ULONG_PTR minDesired = castedOrigin - MEMORY_RANGE; if (minDesired > minAddr && minDesired < castedOrigin) minAddr = minDesired; int test = sizeof(ULONG_PTR); ULONG_PTR maxDesired = castedOrigin + MEMORY_RANGE - size; if (maxDesired < maxAddr && maxDesired > castedOrigin) maxAddr = maxDesired; DWORD granularity = sysInfo.dwAllocationGranularity; ULONG_PTR freeMemory = 0; ULONG_PTR ptr = castedOrigin; while (ptr >= minAddr) { ptr = FindPrev(ptr, minAddr, granularity, size); if (ptr == 0) break; LPVOID pAlloc = VirtualAlloc((LPVOID)ptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (pAlloc != 0) return pAlloc; } while (ptr < maxAddr) { ptr = FindNext(ptr, maxAddr, granularity, size); if (ptr == 0) break; LPVOID pAlloc = VirtualAlloc((LPVOID)ptr, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (pAlloc != 0) return pAlloc; } return NULL; } 

الفكرة بسيطة - سننتقل من الذاكرة ، بدءًا من عنوان معين (في حالتنا ، مؤشر إلى المصدر) لأعلى ولأسفل حتى نعثر على حجم مناسب مجاني.

العودة إلى وظيفة SetHook. انسخ البايتات البالية من المصدر إلى الذاكرة المخصصة وأدخل على الفور قفزة مباشرة إلى المصدر + تالفة لمواصلة التنفيذ بتعليمات غير تالفة.

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

وضعنا فخًا ، لكنه يحتاج أيضًا إلى أن يكون قادرًا على التنظيف. الفكر - وليس البناء ، الفكرة بسيطة - سنقوم بإرجاع البايتات المتهالكة للمصدر ، وحذف السجل الخاص بالمصيدة من المجموعة ، وتحرير الذاكرة المخصصة:

 int OpcodeHook::UnsetHook(void* source) { auto record = GetRecordBySource(source); DWORD oldProtect = 0; VirtualProtect(source, sizeof(JMP_REL), PAGE_EXECUTE_READWRITE, &oldProtect); memcpy(source, record->sourceReservation, record->reservationLen); VirtualProtect(source, sizeof(JMP_REL), oldProtect, &oldProtect); info->Erase(record); FreeMemory(record); return SUCCESS_CODE; } 

اختبار العمل. قم بتغيير أجهزة الاستقبال الخاصة بنا على الفور حتى يتمكنوا من استدعاء الوظائف المعترضة باستخدام نقطة الانطلاق:

 int PresentHook(Device* device) { auto record = hook->GetRecordBySource(ptrPresent); pPresent pTrampoline = (pPresent)record->pTrampoline; auto result = pTrampoline(device); cout << "PresentHook" << endl; return result; } void EndSceneHook(Device* device, int j) { auto record = hook->GetRecordBySource(ptrEndScene); pEndScene pTrampoline = (pEndScene)record->pTrampoline; pTrampoline(device, 2); cout << "EndSceneHook" << " " << j << endl; } 

نحن نختبر ما إذا كنا قد فعلنا كل شيء بشكل صحيح ، وما إذا كانت الذاكرة تتدفق ، وما إذا كان يتم تنفيذ كل شيء بشكل صحيح.
 int main() { while (true) { Device* device = new Device(); device->i = 3; unsigned long long vmt = **(unsigned long long**)&device; ptrPresent = (pPresent)(*(unsigned long long*)(vmt + 8 * 3)); ptrEndScene = (pEndScene)(*(unsigned long long*)(vmt + 8 * 4)); IScanner* scan = new ConsoleScanner(); scan->PrintMemory("Present", (unsigned char*)ptrPresent, 30); hook = new OpcodeHook(); hook->SetHook(ptrPresent, &PresentHook); hook->SetHook(ptrEndScene, &EndSceneHook); device->Present(); device->EndScene(7); device->Present(); device->EndScene(7); device->i = 5; ptrPresent(device); ptrEndScene(device, 9); hook->UnsetHook(ptrPresent); hook->UnsetHook(ptrEndScene); ptrPresent(device); ptrEndScene(device, 7); delete hook; delete device; } } 


إنه يعمل.يمكنك أيضًا التحقق من x64dgb.

تذكر ، في البداية طلبت منك العمل في إصدار الإصدار؟ اذهب الآن إلى Debug وقم بتشغيل البرنامج. تعطل البرنامج ... يتم تشغيل المصيدة ، لكن محاولة استدعاء نقطة الانطلاق تثير استثناءً ، حيث يشير إلى أن العنوان الذي نسميه نقطة انطلاق ليس للتنفيذ على الإطلاق. ماذا افتقدنا؟ ما هي مشكلة بناء Debug؟ نبدأ وننظر إلى شفرة تشغيل الوظيفة الحالية:

 Present: e9 f4 36 0 0 e9 df 8d 0 0 e9 aa b0 0 0 e9 75 3e 0 0 e9 80 38 0 0 e9 da 81 0 0 

x64dbg .


11 — Debug

Debug , e9 f4 36 0. , main mainCRTStartup. , , , , source.

, , , , source trampoline, RIP .

, 99% . , . , , , . 100 ( , ), , , — , , .

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

تنفيذ تركيب الفخ مع مراعاة القفزة النسبية
 int OpcodeHook::SetHook(void* source, void* destination) { auto record = new HookRecord(); record->source = source; record->destination = destination; info->PushBack(record); JMP_ABS pattern = {0xFF, 0x25, 0x00000000, // JMP[RIP + 6] empty 0x0000000000000000 }; // address pattern.address = (ULONG_PTR)source; int currentLen = 0; bool isJmpOpcode = false; int redLine = sizeof(JMP_REL); while (currentLen < redLine && !isJmpOpcode) { hde64s context; const void* pSource = (void*)((unsigned char*)source + currentLen); hde64_disasm(pSource, &context); if (context.opcode == 0xE9) { ULONG_PTR ripPtr = (ULONG_PTR)pSource + context.len + (INT32)context.imm.imm32; pattern.address = ripPtr; isJmpOpcode = true; } memcpy((unsigned char*)record->sourceReservation + currentLen, pSource, context.len); record->reservationLen += context.len; currentLen += context.len; } int trampolineMemorySize = isJmpOpcode ? 2 * sizeof(JMP_ABS) : 2 * sizeof(JMP_ABS) + record->reservationLen; record->pTrampoline = AllocateMemory(source, trampolineMemorySize); if (!isJmpOpcode) { pattern.address = (unsigned long long)(unsigned char*)source + record->reservationLen; memcpy((unsigned char*)record->pTrampoline, record->sourceReservation, record->reservationLen); } int offset = isJmpOpcode ? 0 : record->reservationLen; memcpy((unsigned char*)record->pTrampoline + offset, &pattern, sizeof(JMP_ABS)); pattern.address = (ULONG_PTR)destination; ULONG_PTR relay = (ULONG_PTR)record->pTrampoline + sizeof(pattern) + record->reservationLen; memcpy((void*)relay, &pattern, sizeof(pattern)); DWORD oldProtect = 0; VirtualProtect(source, sizeof(JMP_REL), PAGE_EXECUTE_READWRITE, &oldProtect); JMP_REL* pJmpRelPattern = (JMP_REL*)source; pJmpRelPattern->opcode = 0xE9; pJmpRelPattern->delta = (unsigned int)((LPBYTE)relay - ((LPBYTE)source + sizeof(JMP_REL))); VirtualProtect(source, sizeof(JMP_REL), oldProtect, &oldProtect); return SUCCESS_CODE; } 


, e9, , (ULONG_PTR ripPtr = (ULONG_PTR)pSource + context.len + (INT32)context.imm.imm32), .

, , / , — . Hardware Breakpoint.

, , , — , Microsoft Detour. , - , , - : PolyHook , MinHook , EasyHook ( C#).

7.4.2. Hardware Breakpoint


Opcode Hook , . , Opcode Hook ( NtSetInformationThread), . Hardware Breakpoint , . , , VAC — . VAC ( , - ).
, , VAC DR /, - , . HWBP , - , , , DR0-DR7 .
HWBP . DR0-DR7 RIP , DR0-DR3, , , , — . , , , / , . BaseThreadInitThunk, 4 . , PageGuard.

, — ( Sandbox), Device Present EndScene, . — IHook, «» .

: «» DR0-DR3, , DR7 , EXCEPTION_SINGLE_STEP, . SEH , VEH – , .

:

 int HardwareBPHook::SetHook(void* source, void* destination, HANDLE* hThread, int* reg) { CONTEXT context; ZeroMemory(&context, sizeof(context)); context.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (!GetThreadContext(*hThread, &context)) return ERROR_GET_CONTEXT; *(&context.Dr0 + *reg) = (unsigned long long)source; context.Dr7 |= 1ULL << (2 * (*reg)); context.Dr7 |= HW_EXECUTE << ((*reg) * 4 + 16); context.Dr7 |= HW_LENGTH << ((*reg) * 4 + 18); if (!SetThreadContext(*hThread, &context)) return ERROR_SET_CONTEXT; return SUCCESS_CODE; } 

يتلقى هذا الرمز سياق سلسلة رسائل محددة ، عند الاستلام الناجح ، يدرج عنوان الوظيفة التي يتم اعتراضها في سجل مجاني ، ويقوم بتعيين سجل التحكم DR7. أخيرًا ، يتم تعيين السياق الذي تم تغييره.

, DR6 DR7, PageGuard Gray Hat Python: Python Programming for Hackers and Reverse Engineers. , DR7 / «» — - DR0-DR3 , DR7 , . DR7 , — , ( ).

DR7.

 int HardwareBPHook::UnsetHook(void* source, HANDLE* hThread) { CONTEXT context; ZeroMemory(&context, sizeof(context)); context.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (!GetThreadContext(*hThread, &context)) return ERROR_GET_CONTEXT; for (int i = 0; i < DEBUG_REG_COUNT; i++) { if ((unsigned long long)source == *(&context.Dr0 + i)) { info->GetItem(i)->source = 0; *(&context.Dr0 + i) = 0; context.Dr7 &= ~(1ULL << (2 * i)); context.Dr7 &= ~(3 << (i * 4 + 16)); context.Dr7 &= ~(3 << (i * 4 + 18)); break; } } if (!SetThreadContext(*hThread, &context)) return ERROR_SET_CONTEXT; return SUCCESS_CODE; } 

— , . .

.
 int HardwareBPHook::SetHook(void* source, void* destination) { THREADENTRY32 te32; HANDLE hThread = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (hThread == INVALID_HANDLE_VALUE) return ERROR_ENUM_THREAD_START; te32.dwSize = sizeof(THREADENTRY32); if (!Thread32First(hThread, &te32)) { CloseHandle(hThread); return ERROR_ENUM_THREAD_START; } DWORD dwOwnerPID = GetCurrentProcessId(); bool isRegDefined = false; int freeReg = -1; Freeze(); do { if (te32.th32OwnerProcessID == dwOwnerPID) { HANDLE openThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID); if (!isRegDefined) { CONTEXT context; ZeroMemory(&context, sizeof(context)); context.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (!GetThreadContext(openThread, &context)) return ERROR_GET_CONTEXT; freeReg = GetFreeReg(&context.Dr7); if (freeReg == -1) return ERROR_GET_FREE_REG; isRegDefined = true; } SetHook(source, destination, &openThread, &freeReg); CloseHandle(openThread); } } while (Thread32Next(hThread, &te32)); CloseHandle(hThread); Unfreeze(); auto record = info->GetItem(freeReg); record->source = source; record->destination = destination; record->pTrampoline = source; return SUCCESS_CODE; } 


. , . Freeze Unfreeze – , Opcode Hook — ( ), , .

 int IHook::Freeze() { THREADENTRY32 te32; HANDLE hThread = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (hThread == INVALID_HANDLE_VALUE) return ERROR_ENUM_THREAD_START; te32.dwSize = sizeof(THREADENTRY32); if (!Thread32First(hThread, &te32)) { CloseHandle(hThread); return ERROR_ENUM_THREAD_START; } DWORD dwOwnerPID = GetCurrentProcessId(); do { if (te32.th32OwnerProcessID == dwOwnerPID && te32.th32ThreadID != GetCurrentThreadId()) { HANDLE openThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID); if (openThread != NULL) { SuspendThread(openThread); CloseHandle(openThread); } } } while (Thread32Next(hThread, &te32)); CloseHandle(hThread); return SUCCESS_CODE; } int IHook::Unfreeze() { // equal { HANDLE openThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID); if (openThread != NULL) { ResumeThread(openThread); CloseHandle(openThread); } } // equal return 0; } 


.

VEH. AddVectoredExceptionHandler RemoveVectoredExceptionHandler .

 void HardwareBPHook::SetExceptionHandler(PVECTORED_EXCEPTION_HANDLER pVecExcHandler) { pException = AddVectoredExceptionHandler(1, pVecExcHandler); } ~HardwareBPHook() { info->Clear(); delete info; RemoveVectoredExceptionHandler(pException); } 

( EXCEPTION_SINGLE_STEP), , , RIP . , .

:

 LONG OnExceptionHandler( EXCEPTION_POINTERS* exceptionPointers) { if (exceptionPointers->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) return EXCEPTION_CONTINUE_EXECUTION; for (int i = 0; i < DEBUG_REG_COUNT; i++) { if (exceptionPointers->ContextRecord->Rip == (unsigned long long)hook->GetInfo()->GetItem(i)->source) { exceptionPointers->ContextRecord->Rip = (unsigned long long)hook->GetInfo()->GetItem(i)->destination; break; } } return EXCEPTION_CONTINUE_EXECUTION; } 

, , , OpcodeHook.
, — PresentHook , , . , «» , ( «» ) . — , . .

— , . .

 IDeferredCommands* hookCommands; int PresentHook(Device* device) { auto record = hook->GetRecordBySource(ptrPresent); pPresent pTrampoline = (pPresent)record->pTrampoline; auto result = pTrampoline(device); cout << "PresentHook" << endl; hookCommands->Run(); return result; } void EndSceneHook(Device* device, int j) { auto record = hook->GetRecordBySource(ptrEndScene); pEndScene pTrampoline = (pEndScene)record->pTrampoline; pTrampoline(device, 2); cout << "EndSceneHook" << " " << j << endl; hookCommands->Run(); } LONG OnExceptionHandler(EXCEPTION_POINTERS* exceptionPointers) { if (exceptionPointers->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) return EXCEPTION_CONTINUE_EXECUTION; for (int i = 0; i < DEBUG_REG_COUNT; i++) { if (exceptionPointers->ContextRecord->Rip == (unsigned long long)hook->GetInfo()->GetItem(i)->source) { exceptionPointers->ContextRecord->Dr7 &= ~(1ULL << (2 * i)); exceptionPointers->ContextRecord->Rip = (unsigned long long)hook->GetInfo()->GetItem(i)->destination; IDeferredCommand* cmd = new SetD7Command(hook, GetCurrentThreadId(), i); hookCommands->Enqueue(cmd); break; } } return EXCEPTION_CONTINUE_EXECUTION; } 

 namespace silk_way { class IDeferredCommand { protected: IDeferredCommand(silk_way::IHook* _hook) { hook = _hook; } public: virtual ~IDeferredCommand() { hook = nullptr; } virtual void Run() = 0; protected: silk_way::IHook* hook; }; class SetD7Command : public IDeferredCommand { public: SetD7Command(silk_way::IHook* _hook, unsigned long long _threadId, int _reg) : IDeferredCommand(_hook) { threadId = _threadId; reg = _reg; } void Run() { HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, threadId); if (hThread != NULL) { bool res = SetD7(&hThread); CloseHandle(hThread); } } private: bool SetD7(HANDLE* hThread) { CONTEXT context; ZeroMemory(&context, sizeof(context)); context.ContextFlags = CONTEXT_DEBUG_REGISTERS; if (!GetThreadContext(*hThread, &context)) return false; *(&context.Dr0 + reg) = (unsigned long long)hook->GetInfo()->GetItem(reg)->source; context.Dr7 |= 1ULL << (2 * reg); if (!SetThreadContext(*hThread, &context)) return false; return true; } private: unsigned long long threadId; int reg; }; class IDeferredCommands : public silk_data::Queue<IDeferredCommand*>, public IDeferredCommand { protected: IDeferredCommands() : Queue(), IDeferredCommand(nullptr) {} public: virtual ~IDeferredCommands() {} }; } 


«» .


12 —

, VEH , , -:


13 —

, , RIP :


14 —

, silk_way.lib . , OpcodeHook, VMT Hook, Forced Exception Hook (, «» ), HardwareBreakpoint PageGuard ( , ).

8.


يتم تقديم أساس المنطق في شكل MVC (نموذج عرض وحدة تحكم). ترث جميع الكيانات الأساسية من واجهة ISilkObject.

8.1. نموذج


ECS ( ). , , , ml ( (Dota 2 3D ) 2D ). , , , ECS ( , SkyForge, « » ), , , Source2Gen. . Schema, ( , ( , ) — xml/json).

:


15 —

:

 template <class S> SILK_OBJ(IModel) { ACCESSOR(IIdentity, Id) ACCESSOR(S, Schema) public: IModel(IIdentity * id, ISchema * schema) { Id = id; Schema = dynamic_cast<S*>(schema); components = new silk_data::RBTree<SILK_STRING*, IComponent>( new StringCompareStrategy()); } ~IModel() { delete Id; Schema = nullptr; components->Clear(); delete components; } template <class T> T* Get(SILK_STRING * key) { return (T*)components->Find(key); } private: silk_data::RBTree<SILK_STRING*, IComponent>* components; }; 

, .

 class IModelSchema : public BaseSchema { ACCESSOR(ModelContext, Context) public: IModelSchema(const char* type, const char* name, IContext* context) : BaseSchema(type, name) { Context = dynamic_cast<ModelContext*>(context); } ~IModelSchema() { Context = nullptr; } }; class ModelContext : public SilkContext { ACCESSOR(ILogger, Logger) ACCESSOR(IChrono, Clock) ACCESSOR(GigaFactory, Factory) ACCESSOR(IGameModel*, Model) public: ModelContext(SILK_GUID* guid, ILogger* logger, IChrono* clock, GigaFactory* factory, IGameModel** model) : SilkContext(guid) { Logger = logger; Clock = clock; Factory = factory; Model = model; } ~ModelContext() { Logger = nullptr; Clock = nullptr; Factory = nullptr; Model = nullptr; } }; 

 template <class T, class S> class IModelCollection : public silk_data::Vector<T*>, public IModel<S> { protected: IModelCollection(IIdentity* id, ISchema* schema) : Vector(), IModel(id, schema) { auto factory = Schema->GetContext()->GetFactory(); auto guid = Schema->GetContext()->GetGuid(); foreach (Schema->Length()) { auto itemSchema = Schema->GetItem(i); auto item = factory->Build<T>(itemSchema->GetType()->GetValue(), guid->Get(), itemSchema); PushBack(item); } } public: ~IModelCollection() { Clear(); } T* GetByName(const char* name) { foreach (Length()) if (GetItem(i)->GetSchema()->CheckName(name)) return GetItem(i); return nullptr; } }; 


,
 DEFINE_IMODEL(IRoshanStatusModel, IRoshanStatusSchema) { VIRTUAL_COMPONENT(IStatesModel, States) public: virtual void Resolve() = 0; protected: IRoshanStatusModel(IIdentity * id, ISchema * schema) : IModel(id, schema) {} }; DEFINE_MODEL(RoshanStatusModel, IRoshanStatusModel) { COMPONENT(IStatesModel, States) public : RoshanStatusModel(IIdentity * id, ISchema* schema) : IRoshanStatusModel( id, schema) { auto factory = Schema->GetContext()->GetFactory(); auto guid = Schema -> GetContext() -> GetGuid(); auto statesSchema = Schema -> GetStates(); States = factory->Build<IStatesModel>( statesSchema->GetType()->GetValue(), guid->Get(), statesSchema); } ~RoshanStatusModel() { delete States; } void Resolve() { auto currentStateSchema = States->GetCurrent()->GetSchema(); Schema->GetContext()->GetLogger()->Log("RESOLVE\n"); foreach (currentStateSchema->GetTransitions()->Length()) { auto transition = currentStateSchema->GetTransitions()->GetItem(i); if (transition->GetRequirement()->Check()) { transition->GetAction()->Make(); States->SetCurrent(States->GetByName( transition->GetTo()->GetValue())); break; } } } }; 


8.2. ,


, , . . Canvas, ViewCollection, Label Button, , , .


الشكل 16 - تمثيل تخطيطي للرأي


الشكل 17 - التمثيل التخطيطي لدولة المشاهدة

8.3.


. , typeid(T).raw_name(). , , Andrei Alexandrescu, Modern C++ Design: Generic Programming. :

 class SilkFactory { public: SilkFactory() { items = new silk_data::RBTree<SILK_STRING*, IImplementator>( new StringCompareStrategy()); } ~SilkFactory() { items->Clear(); delete items; } template <class... Args> ISILK_WAY_OBJECT* Build(const char* type, Args... args) { auto key = new SILK_STRING(type); auto impl = items->Find(key)->payload; return impl->Build(args...); } void Register(const char* type, IImplementator* impl) { auto key = new SILK_STRING(type); items->Insert(*items->MakeNode(key, impl)); } protected: silk_data::RBTree<SILK_STRING*, IImplementator>* items; }; class GigaFactory { public: GigaFactory() { items = new silk_data::RBTree<SILK_STRING*, SilkFactory>( new StringCompareStrategy()); } ~GigaFactory() { items->Clear(); delete items; } template <class T, class... Args> T* Build(const char* concreteType, Args... args) { auto key = new SILK_STRING(typeid(T).raw_name()); auto factory = items->Find(key)->payload; return (T*)factory->Build(concreteType, args...); } template <class T> void Register(SilkFactory* factory) { auto key = new SILK_STRING(typeid(T).raw_name()); items->Insert(*items->MakeNode(key, factory)); } protected: silk_data::RBTree<SILK_STRING*, SilkFactory>* items; }; 

قبل استخدام المصنع لبناء الكائنات ، تحتاج إلى التسجيل.
مثال على نموذج التسجيل
 void ModelRegistrator::Register( GigaFactory* factory) { auto requirement = new SilkFactory(); requirement->Register("true", new SchemaImplementator<TrueRequirement>); requirement->Register("false", new SchemaImplementator<FalseRequirement>); requirement->Register("roshan_killed", new SchemaImplementator<RoshanKilledRequirement>); requirement->Register("roshan_alive_manual", new SchemaImplementator<RoshanAliveManualRequirement>); requirement->Register("time", new SchemaImplementator<TimeRequirement>); requirement->Register("roshan_state", new SchemaImplementator<RoshanStateRequirement>); factory->Register<IRequirement>(requirement); auto action = new SilkFactory(); action->Register("action", new SchemaImplementator<EmptyAction>); action->Register("set_current_time", new SchemaImplementator<SetCurrentTimeAction>); factory->Register<IAction>(action); auto transition = new SilkFactory(); transition->Register("transition", new SchemaImplementator<TransitionSchema>); factory->Register<ITransitionSchema>(transition); auto transitions = new SilkFactory(); transitions->Register("transitions", new SchemaImplementator<TransitionsSchema>); factory->Register<ITransitionsSchema>(transitions); auto stateSchema = new SilkFactory(); stateSchema->Register("state", new SchemaImplementator<StateSchema>); factory->Register<IStateSchema>(stateSchema); auto statesSchema = new SilkFactory(); statesSchema->Register("states", new SchemaImplementator<StatesSchema>); factory->Register<IStatesSchema>(statesSchema); auto roshanStatusSchema = new SilkFactory(); roshanStatusSchema->Register("roshan_status", new SchemaImplementator<RoshanStatusSchema>); factory->Register<IRoshanStatusSchema>(roshanStatusSchema); auto triggerSchema = new SilkFactory(); triggerSchema->Register("trigger", new SchemaImplementator<TriggerSchema>); factory->Register<ITriggerSchema>(triggerSchema); auto triggersSchema = new SilkFactory(); triggersSchema->Register("triggers", new SchemaImplementator<TriggersSchema>); factory->Register<ITriggersSchema>(triggersSchema); auto resourceSchema = new SilkFactory(); resourceSchema->Register("resource", new SchemaImplementator<ResourceSchema>); factory->Register<IResourceSchema>(resourceSchema); auto resourcesSchema = new SilkFactory(); resourcesSchema->Register("resources", new SchemaImplementator<ResourcesSchema>); factory->Register<IResourcesSchema>(resourcesSchema); auto gameSchema = new SilkFactory(); gameSchema->Register("game", new SchemaImplementator<GameSchema>); factory->Register<IGameSchema>(gameSchema); auto gameModel = new SilkFactory(); gameModel->Register("game", new ConcreteImplementator<GameModel>); factory->Register<IGameModel>(gameModel); auto resources = new SilkFactory(); resources->Register("resources", new ConcreteImplementator<ResourceCollection>); factory->Register<IResourceCollection>(resources); auto resource = new SilkFactory(); resource->Register("resource", new ConcreteImplementator<Resource>); factory->Register<IResource>(resource); auto triggers = new SilkFactory(); triggers->Register("triggers", new ConcreteImplementator<TriggerCollection>); factory->Register<ITriggerCollection>(triggers); auto trigger = new SilkFactory(); trigger->Register("trigger", new ConcreteImplementator<Trigger>); factory->Register<ITrigger>(trigger); auto roshanStatus = new SilkFactory(); roshanStatus->Register("roshan_status", new ConcreteImplementator<RoshanStatusModel>); factory->Register<IRoshanStatusModel>(roshanStatus); auto states = new SilkFactory(); states->Register("states", new ConcreteImplementator<StatesModel>); factory->Register<IStatesModel>(states); auto state = new SilkFactory(); state->Register("state", new ConcreteImplementator<StateModel>); factory->Register<IStateModel>(state); } 


يمكن ملء المخطط بأي طريقة - يمكنك استخدام json ، يمكنك مباشرة في الكود.
الخيار لملء مخطط النماذج في json
 { "game": { "roshan_status": { "states": [ { "name": "alive", "transitions": [ { "from": "alive", "to": "ressurect_base", "requirement": { "typename": "roshan_killed", "action": { "typename": "set_current_time", "resource": "roshan_killed_ts" } } } ] }, { "name": "ressurect_base", "transitions": [ { "from": "ressurect_base", "to": "ressurect_extra", "requirement": { "typename": "time", "resource": "roshan_killed_ts", "offset": 480 }, "action": { "typename": "action" } } ] }, { "name": "ressurect_extra", "transitions": [ { "from": "ressurect_extra", "to": "alive", "requirement": { "typename": "time", "resource": "roshan_killed_ts", "offset": 660 }, "action": { "typename": "action" } }, { "from": "ressurect_extra", "to": "alive", "requirement": { "typename": "roshan_alive_manual" }, "action": { "typename": "action" } } ] } ] }, "triggers": { "roshan_killed": {}, "roshan_alive_manual": {} }, "resources": { "roshan_killed_ts": {} } } } 


خيار لملء مخطط لتقديم الكود
 void GameController::InitViewSchema(ICanvasSchema** schema) { *schema = factory->Build<ICanvasSchema>("canvas_d9", "canvas_d9", "canvas_d9", viewContext); IViewCollectionSchema* elements = factory->Build<IViewCollectionSchema>( "elements", "elements", "elements", viewContext); (*schema)->SetElements(elements); ILabelSchema* labelSchema = factory->Build<ILabelSchema>( "label_d9", "label_d9", "roshan_status_label", viewContext); labelSchema->SetRecLeft(new SILK_INT(30)); labelSchema->SetRecTop(new SILK_INT(100)); labelSchema->SetRecRight(new SILK_INT(230)); labelSchema->SetRecDown(new SILK_INT(250)); labelSchema->SetColorR(new SILK_FLOAT(1.0f)); labelSchema->SetColorG(new SILK_FLOAT(1.0f)); labelSchema->SetColorB(new SILK_FLOAT(1.0f)); labelSchema->SetColorA(new SILK_FLOAT(1.0f)); labelSchema->SetText(new SILK_STRING("Roshan status: alive\0")); elements->PushBack((IViewSchema*&)labelSchema); IButtonSchema* buttonSchema = factory->Build<IButtonSchema>( "button_d9", "button_d9", "roshan_kill_button", viewContext); ILabelSchema* buttonLabelSchema = factory->Build<ILabelSchema>( "label_d9", "label_d9", "button_text", viewContext); buttonLabelSchema->SetRecLeft(new SILK_INT(30)); buttonLabelSchema->SetRecTop(new SILK_INT(115)); buttonLabelSchema->SetRecRight(new SILK_INT(110)); buttonLabelSchema->SetRecDown(new SILK_INT(130)); buttonLabelSchema->SetColorR(new SILK_FLOAT(1.0f)); buttonLabelSchema->SetColorG(new SILK_FLOAT(0.0f)); buttonLabelSchema->SetColorB(new SILK_FLOAT(0.0f)); buttonLabelSchema->SetColorA(new SILK_FLOAT(1.0f)); buttonLabelSchema->SetText(new SILK_STRING("Kill Roshan\0")); buttonSchema->SetLabel(buttonLabelSchema); buttonSchema->SetBorderColorR(new SILK_INT(0)); buttonSchema->SetBorderColorG(new SILK_INT(0)); buttonSchema->SetBorderColorB(new SILK_INT(0)); buttonSchema->SetBorderColorA(new SILK_INT(70)); buttonSchema->SetFillColorR(new SILK_INT(255)); buttonSchema->SetFillColorG(new SILK_INT(119)); buttonSchema->SetFillColorB(new SILK_INT(0)); buttonSchema->SetFillColorA(new SILK_INT(150)); buttonSchema->SetPushColorR(new SILK_INT(0)); buttonSchema->SetPushColorG(new SILK_INT(0)); buttonSchema->SetPushColorB(new SILK_INT(0)); buttonSchema->SetPushColorA(new SILK_INT(70)); buttonSchema->SetBorder(new SILK_FLOAT(5)); elements->PushBack((IViewSchema*&)buttonSchema); } 


8.4. أحداث


يتعرف العرض على التغيير في النموذج من خلال الأحداث. يمكنك الحصول على ملاحظات في أساليب الفصل والوظائف العادية.

 #define VIRTUAL_EVENT(e) public: virtual IEvent* Get##e() = 0; #define EVENT(e) private: IEvent* e; public: IEvent* Get##e() { return e; } const int MAX_EVENT_CALLBACKS = 1024; class IEventArgs {}; class ICallback { public: virtual void Invoke(IEventArgs* args) = 0; }; template <class A> class Callback : public ICallback { typedef void (*f)(A*); public: Callback(f _pFunc) { ptr = _pFunc; } ~Callback() { delete ptr; } void Invoke(IEventArgs* args) { ptr((A*)args); } private: f ptr = nullptr; }; template <typename T, class A> class MemberCallback : public ICallback { typedef void (T::*f)(A*); public: MemberCallback(f _pFunc, T* _obj) { ptr = _pFunc; obj = _obj; } ~MemberCallback() { delete ptr; obj = nullptr; } void Invoke(IEventArgs* args) { (obj->*(ptr))((A*)args); } private: f ptr = nullptr; T* obj; }; class IEvent { public: virtual void Invoke(IEventArgs* args) = 0; virtual void Add(ICallback* callback) = 0; virtual bool Remove(ICallback* callback) = 0; virtual ~IEvent() {} }; 

إذا أراد كائن الإبلاغ عن الأحداث التي تحدث بداخله ، فأنت بحاجة إلى إضافة IEvent * لكل حدث. كائن آخر مهتم بالأحداث التي تحدث داخل هذا الكائن يجب أن ينشئ ICallback * ويمرره داخل IEvent * (الاشتراك في الحدث).
اشتراكات المثال التي تحدث في وحدة التحكم
 void Attach() { statesChangedCallback = new MemberCallback<GameController, IEventArgs>( &GameController::OnStatesChanged, this); Model->GetRoshanStatus()->GetStates()->GetCurrentChanged()->Add( statesChangedCallback); buttonClickedCallback = new MemberCallback<GameController, IEventArgs>( &GameController::OnKillRoshanClicked, this); killButton->GetClickedEvent()->Add(buttonClickedCallback); } 


مثال على الإعلان عن حدث داخل الفصل - مع كل نقرة على مدار الساعة (استدعاء طريقة التجزئة) ، يتم رفع حدث StruckEvent
 class IChrono { VIRTUAL_EVENT(Struck) public: virtual void Tick() = 0; virtual long long GetStamp() = 0; virtual long long GetDiffS(long long ts) = 0; }; class Chrono : public IChrono { EVENT(Struck) public: Chrono() { start = time(0); Struck = new Event(); } ~Chrono() { delete Struck; } void Tick() { auto cur = clock(); worked += cur - savepoint; bool isStriking = savepoint < cur; savepoint = cur; if (isStriking) Struck->Invoke(nullptr); } long long GetStamp() { return start * CLOCKS_PER_SEC + worked; } long long GetDiffS(long long ts) { return (GetStamp() - ts) / CLOCKS_PER_SEC; } private: long long worked = 0; time_t start; time_t savepoint; }; 


يتم تطبيق الأنواع البدائية الأساسية (SILK_INT و SILT_FLOAT و SILK_STRING ، ...) في Core.h.

9. دايركت 9


DirectX 9 API, Dota 2. Device , IUnknown . , , . .code, , . , OpenGL Vulkan , GetProcAddress(). DirectX 11 9, .

( ) , . . — .

IDirect3D9 Direct3DCreate9, CreateDevice. DirectX, . d3d9.h, Direct3DCreate9 GetProcAddress ( , NativeInjector LoadLibrary).


الشكل 18 - وصف CreateDevice في d3d9.h

قم بإنشاء مثيل IDirect3D9:
 typedef IDirect3D9* (WINAPI *SILK_Direct3DCreate9) (UINT SDKVersion); //IDirect3D9* pD3D = Direct3DCreate9(D3D_SDK_VERSION); SILK_Direct3DCreate9 Silk_Direct3DCreate9 = (SILK_Direct3DCreate9)GetProcAddress(GetModuleHandle("d3d9.dll"), "Direct3DCreate9"); IDirect3D9* pD3D = Silk_Direct3DCreate9(D3D_SDK_VERSION); 

باستخدام IDirect3D9 ، يمكننا إنشاء جهاز عن طريق استدعاء pD3D-> CreateDevice (...). للحصول على مؤشر إلى الوظائف الضرورية من VMT ، نحتاج إلى معرفة الإجراء الخاص بتحديد هذه الطرق. الشكل 19 - البحث في الفهرس عن طريقة CreateDevice للواجهة IDirect3D9 ، احصل على الفهرس السادس عشر. بالإضافة إلى CreateDevice ، نحتاج أيضًا إلى أساليب التحرير و GetAdapterDisplayMode.






ننفذ إنشاء الجهاز في التعليمات البرمجية
 typedef HRESULT(WINAPI *SILK_GetAdapterDisplayMode)(IDirect3D9* direct3D9, UINT Adapter, D3DDISPLAYMODE* pMode); typedef HRESULT(WINAPI *SILK_CreateDevice)(IDirect3D9* direct3D9, UINT Adapter, D3DDEVTYPE DeviceType, HWND hFocusWindow, DWORD BehaviorFlags, D3DPRESENT_PARAMETERS* pPresentationParameters, IDirect3DDevice9** ppReturnedDeviceInterface); typedef ULONG(WINAPI *SILK_Release)(IDirect3D9* direct3D9); const int RELEASE_INDEX = 2; const int GET_ADAPTER_DISPLAY_MODE_INDEX = 8; const int CREATE_DEVICE_INDEX = 16; BOOL CreateSearchDevice(IDirect3D9** d3d, IDirect3DDevice9** device) { if (!d3d || !device) return FALSE; *d3d = NULL; *device = NULL; //IDirect3D9* pD3D = Direct3DCreate9(D3D_SDK_VERSION); SILK_Direct3DCreate9 Silk_Direct3DCreate9 = (SILK_Direct3DCreate9)GetProcAddress(GetModuleHandle("d3d9.dll"), "Direct3DCreate9"); IDirect3D9* pD3D = Silk_Direct3DCreate9(D3D_SDK_VERSION); if (!pD3D) return FALSE; D3DDISPLAYMODE displayMode; int pointerSize = sizeof(unsigned long long); unsigned long long vmt = **(unsigned long long **)&pD3D; SILK_GetAdapterDisplayMode pGetAdapderDisplayMode = (SILK_GetAdapterDisplayMode)((*(unsigned long long *) (vmt + pointerSize * GET_ADAPTER_DISPLAY_MODE_INDEX))); pGetAdapderDisplayMode(pD3D, D3DADAPTER_DEFAULT, &displayMode); //pD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &displayMode); HWND hWindow = GetDesktopWindow(); D3DPRESENT_PARAMETERS pp; ZeroMemory(&pp, sizeof(pp)); pp.Windowed = TRUE; pp.hDeviceWindow = hWindow; pp.BackBufferCount = 0; pp.BackBufferWidth = 0; pp.BackBufferHeight = 0; pp.BackBufferFormat = displayMode.Format; pp.SwapEffect = D3DSWAPEFFECT_DISCARD; IDirect3DDevice9* pDevice = NULL; SILK_CreateDevice pCreateDevice = (SILK_CreateDevice) ((*(unsigned long long *)(vmt + pointerSize * CREATE_DEVICE_INDEX))); if(SUCCEEDED(pCreateDevice(pD3D, D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWindow, D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_DISABLE_DRIVER_MANAGEMENT, &pp, &pDevice))) { //if (SUCCEEDED(pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWindow, D3DCREATE_SOFTWARE_VERTEXPROCESSING | D3DCREATE_DISABLE_DRIVER_MANAGEMENT, &pp, &pDevice))) { if (pDevice != NULL) { *d3d = pD3D; *device = pDevice; } } BOOL result = (*d3d != NULL); if (result == FALSE) if (pD3D) { SILK_Release pRelease= (SILK_Release)((*(unsigned long long *)(vmt + pointerSize * RELEASE_INDEX))); pRelease(pD3D); //pD3D->Release(); } return result; } 


, DirectX 9, , , . : « DirectX 9 ?». Present . , front buffer (, , ( ) ), back buffer – , front buffer, swap chain – , (flipping) front back (DirectX 9 1 swap chain). , Present, BeginScene EndScene , back buffer.

( ): EndScene Present. IDirect3DDevice9


20 — IDirect3DDevice9

:

 typedef HRESULT(*VirtualOverloadPresent)(IDirect3DDevice9* pd3dDevice, CONST RECT* pSourceRect, CONST RECT* pDestRect, HWND hDestWindowOverride, CONST RGNDATA* pDirtyRegion); VirtualOverloadPresent oOverload = NULL; typedef HRESULT(*VirtualOverloadEndScene)(IDirect3DDevice9* pd3dDevice); VirtualOverloadEndScene oOverloadEndScene = NULL; const int PRESENT_INDEX = 17; const int END_SCENE_INDEX = 42; 

, HardwareBreakpoint , VAC ( Opcode Hook, ):

 silk_way::IDeferredCommands* deferredCommands; silk_way::IHook* hook; LONG OnExceptionHandler(EXCEPTION_POINTERS* exceptionPointers) { if (exceptionPointers->ExceptionRecord->ExceptionCode != EXCEPTION_SINGLE_STEP) return EXCEPTION_EXIT_UNWIND; for (int i = 0; i < silk_way::DEBUG_REG_COUNT; i++) { if (exceptionPointers->ContextRecord->Rip == (unsigned long long) hook->GetInfo()->GetItem(i)->source) { exceptionPointers->ContextRecord->Dr7 &= ~(1ULL << (2 * i)); exceptionPointers->ContextRecord->Rip = (unsigned long long) hook->GetInfo()->GetItem(i)->destination; silk_way::IDeferredCommand* cmd = new silk_way::SetD7Command(hook, GetCurrentThreadId(), i); deferredCommands->Enqueue(cmd); break; } } return EXCEPTION_CONTINUE_EXECUTION; } 

:

 BOOL HookDevice(IDirect3DDevice9* pDevice) { unsigned long long vmt = **(unsigned long long **)&pDevice; int pointerSize = sizeof(unsigned long long); VirtualOverloadPresent pointerPresent= (VirtualOverloadPresent) ((*(unsigned long long *)(vmt + pointerSize * PRESENT_INDEX))); VirtualOverloadEndScene pointerEndScene = (VirtualOverloadEndScene) ((*(unsigned long long *)(vmt + pointerSize * END_SCENE_INDEX))); oOverload = pointerPresent; oOverloadEndScene = pointerEndScene; deferredCommands = new silk_way::DeferredCommands(); //hook = new silk_way::HardwareBPHook(); hook = new silk_way::OpcodeHook(); hook->SetExceptionHandler(OnExceptionHandler); hook->SetHook(pointerPresent, &PresentHook); hook->SetHook(pointerEndScene, &EndSceneHook); return TRUE; } 

:

 HRESULT WINAPI PresentHook(IDirect3DDevice9* pd3dDevice, CONST RECT* pSourceRect, CONST RECT* pDestRect, HWND hDestWindowOverride, CONST RGNDATA* pDirtyRegion) { Capture(pd3dDevice); auto record = hook->GetRecordBySource(oOverload); VirtualOverloadPresent pTrampoline = (VirtualOverloadPresent) record->pTrampoline; auto result = pTrampoline(pd3dDevice, pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion); deferredCommands->Run(); return result; } HRESULT WINAPI EndSceneHook(IDirect3DDevice9* pd3dDevice) { if (controller == nullptr) { controller = new GameController(); controller->SetDevice(pd3dDevice); } controller->Update(); auto record = hook->GetRecordBySource(oOverloadEndScene); VirtualOverloadEndScene pTrampoline = (VirtualOverloadEndScene) record->pTrampoline; auto result = pTrampoline(pd3dDevice); deferredCommands->Run(); return result; } 

Present ( ) Capture
 VOID WINAPI Capture(IDirect3DDevice9* pd3dDevice) { IDirect3DSurface9 *renderTarget = NULL; IDirect3DSurface9 *destTarget = NULL; HRESULT res1 = pd3dDevice->GetRenderTarget(0, &renderTarget); D3DSURFACE_DESC descr; HRESULT res2 = renderTarget->GetDesc(&descr); HRESULT res3 = pd3dDevice->CreateOffscreenPlainSurface( descr.Width, descr.Height, /*D3DFMT_A8R8G8B8*/descr.Format, D3DPOOL_SYSTEMMEM, &destTarget, NULL); HRESULT res4 = pd3dDevice->GetRenderTargetData(renderTarget, destTarget); D3DLOCKED_RECT lockedRect; ZeroMemory(&lockedRect, sizeof(lockedRect)); if (destTarget == NULL) return; HRESULT res5 = destTarget->LockRect(&lockedRect, NULL, D3DLOCK_READONLY); HRESULT res7 = destTarget->UnlockRect(); HRESULT res6 = D3DXSaveSurfaceToFile(screenshootPath, D3DXIFF_BMP, destTarget, NULL, NULL); renderTarget->Release(); destTarget->Release(); } 


EndScene . , .

, DirectX 9. , — API. , , UI, — , .

, EndScene() — . EndScene , lockstep.

, .

DirectX SDK , , DirectX 9 DirectX 11. DirectX 11, - SDK, ( , ) , , DXUT, , — , FPS .


21 — DirectX SDK StateManager.exe

injected.dll Dota 2. , , «» — Opcode Hook (, ) . — Hardware Breakpoint , . Opcode Hook HWBP , ( 2- ), ( 3 ). , . , — .

( )

22 —


23 —

1x1.

24 —

, — . , , , - .

UI , DirectX 9 — , . API, , — , UI . , 2D.

10.


API , , , UI , . Valve Dota 2 API Javascript Lua . , , C++ ( C++, ). , — , , , . , .

DoIncludeScript, Lua Scripting API. , , C++, or_75 . , , .

لنبدأ. : DoIncludeScript, , . silk_way.lib. , , — . , .

, ( client.dll, ). tlHelp32 , GetModuleInfo.

GetModuleInfo
 int IScanner::GetModuleInfo(const char* name, MODULEENTRY32* entry) { HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE32 | TH32CS_SNAPMODULE, GetCurrentProcessId()); if (snapshot == INVALID_HANDLE_VALUE) return 1; entry->dwSize = sizeof(MODULEENTRY32); if (!Module32First(snapshot, entry)) { CloseHandle(snapshot); return 1; } do { if (!_stricmp(entry->szModule, name)) break; } while (Module32Next(snapshot, entry)); CloseHandle(snapshot); return 0; } 


, «??» — “j9 ?? ؟؟ ؟؟ ؟؟ 48 03 08 ?? f1 ff”.

, unsigned char , .

 unsigned char* IScanner::Parse(int& len, const char* strPattern, unsigned char* skipByteMask) { int strPatternLen = strlen(strPattern); unsigned char* pattern = new unsigned char[strPatternLen]; for (int i = 0; i < strPatternLen; i++) pattern[i] = 0; len = 0; for (int i = 0; i < strPatternLen; i += 2) { unsigned char code = 0; if (strPattern[i] == SKIP_SYMBOL) skipByteMask[len] = 1; else code = Parse(strPattern[i]) * 16 + Parse(strPattern[i + 1]); i++; pattern[len++] = code; } return pattern; } unsigned char IScanner::Parse(char byte) { // some magic values if (byte >= '0' && byte <= '9') return byte - '0'; else if (byte >= 'a' && byte <= 'f') return byte - 'a' + 10; else if (byte >= 'A' && byte <= 'F') return byte - 'A' + 10; return 0; } 

FindPattern, . , , VirtualQuery, — ( ), , PageGuard:

 void* pStart = moduleEntry.modBaseAddr; void* pFinish = moduleEntry.modBaseAddr + moduleEntry.modBaseSize; unsigned char* current = (unsigned char*)pStart; for (; current < pFinish && j < patternLen; current++) { if (!VirtualQuery((LPCVOID)current, &info, sizeof(info))) continue; unsigned long long protectMask = PAGE_READONLY | PAGE_READWRITE | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE | PAGE_EXECUTE_READ; if (info.State == MEM_COMMIT && info.Protect & protectMask && !(info.Protect & PAGE_GUARD)) { unsigned long long finish = (unsigned long long)pFinish < (unsigned long long)info.BaseAddress + info.RegionSize ? (unsigned long long)pFinish : (unsigned long long) info.BaseAddress + info.RegionSize; current = (unsigned char*)info.BaseAddress; unsigned char* rip = 0; for (unsigned long long k = (unsigned long long)info.BaseAddress; k < finish && j < patternLen; k++, current++) { if (skipByteMask[j] || pattern[j] == *current) { if (j == 0) rip = current; j++; } else { j = 0; if (pattern[0] == *current) { rip = current; j = 1; } } } if (j == patternLen) { current = rip; break; } } else current += sysInfo.dwPageSize; } 

, , . Steam (, x64dbg — IDA Pro ), dota2.exe ...\Steam\steamapps\common\dota 2 beta\game\bin\win64. , VAC Cheat Engine x64dbg, , . , ScyllaHide , NtCreateThreadEx, NtSetInformationThread . ., , .

( 10-15) Run (F9). , . (Search for->All Modules->String References), “DoIncludeScript”.


25 —

( CPU) . , client.dll, server.dll animationsystem.dll.

.


26 —

, DoIncludeScript — . , .


27 — DoIncludeScript

.


28 — DoIncludeScript

( , ).


29 — DoIncludeScript

27 DoIncludeScript. , , «??». : 40 57 48 81 EC ?? ؟؟ ؟؟ ؟؟ 48 83 3D ?? ؟؟ ؟؟ ؟؟ ؟؟ 48 8B F9 0F 84. 28, 27.

Lua silk_way.lua, «...\Steam\steamapps\common\dota 2 beta\game\dota\scripts\vscripts».

 print("SILK_WAY START") local first = Entities:First() while (first ~= nil) do local position = first:GetAbsOrigin() local strInfo = "[" .. "pos:" .. tostring(position.x) .. "," .. tostring(position.y) .. "," .. tostring(position.z) .. "]" DebugDrawText(position, strInfo, true, 300.0) first = Entities:Next(first) end print("SILK_WAY FINISH") --[[ListenToGameEvent("dota_roshan_kill",roshan_kill,nil)]] 

.

, 29.

 typedef bool(*fDoIncludeScript)(const char*, unsigned long long); 


.

 HRESULT WINAPI EndSceneHook(IDirect3DDevice9* pd3dDevice) { if (controller == nullptr) { controller = new GameController(); controller->SetDevice(pd3dDevice); fDoIncludeScript DoIncludeScript = (fDoIncludeScript) scanner->FindPattern("client.dll", "40 57 48 81 EC ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ?? 48 8B F9 0F 84"); DoIncludeScript("silk_way", 0); } //... } 

.


30 —

. Lua, , , , C++ ( ), ? ( DoIncludeScript), Source SDK Source2Gen. , . , , , , .

11.


, , . Dota 2 praydog , Cheat Engine , Valve . , . or75 DoIncludeScript .

- , , , , — , . .

, .

Source: https://habr.com/ru/post/ar446516/


All Articles