
उपयोगकर्ता मोड डिटेक्टर जो अप्रत्यक्ष syscalls को पकड़ता है। यह Hell's Hall, Tartarus' Gate, RecycledGate, और VEH syscalls और कई अन्य को फँसाता है।
कॉपीराइट (C) 2026 Adam Zypherion <[email protected]> — GPL-3.0 के अंतर्गत लाइसेंस प्राप्त।
इनडायरेक्ट सिस्कॉल्स सरल हो जाते हैं जब आप एक देख लेते हैं। लोडर ntdll में चलता है, Nt* प्रोलॉग से SSN पढ़ता है, आगे दो बाइट्स में 0F 05 ढूंढता है, स्वयं r10/rax/rdx/r8/r9 सेट करता है, और सीधे उन दो बाइट्स पर jmp करता है। सिस्कॉल ntdll के अंदर से होता है। kernel32 एक्सपोर्ट पर आपका हुक कभी छुआ नहीं जाता। ntdll एक्सपोर्ट पर आपका हुक कभी छुआ नहीं जाता। SYSCALL के समय [RSP] लोडर के RWX पेज पर वापस इंगित करता है, लेकिन बाहर से कॉल में कुछ भी असामान्य नहीं दिखता।
HellHall proc
mov r10, rcx
mov eax, dwSSN
jmp qword ptr [qAddr]
ret
HellHall endp
वेरिएंट के नाम हैं। Tartarus' Gate और RecycledGate एक स्टब के syscall निर्देश का उपयोग करते हैं लेकिन किसी दूसरे के SSN का, इसलिए भले ही आप स्टब का नाम लॉग करें जो आपका हुक रिपोर्ट करता है, वह झूठ है। VEH syscall जानबूझकर एक एक्सेस उल्लंघन ट्रिगर करता है और अपने स्वयं के VEH का उपयोग करके संदर्भ को फिर से लिखता है ताकि RIP ntdll के syscall निर्देश पर पहले से तैयार रजिस्टरों के साथ उतरे। Hell's Gate ntdll का बिल्कुल उपयोग नहीं करता। लोडर अपने स्वयं के RWX पेज में 0F 05 लिखता है और वहाँ से चलता है।
कर्नेल मोड (KM) का उत्तर एक ड्राइवर है, मैं वास्तव में समय के साथ इस पर काम कर सकता हूँ और एक प्रोजेक्ट जारी कर सकता हूँ लेकिन जल्द ही ऐसा नहीं होगा :D
PAGE_GUARD काम करता हुआ दिखता है। syscall बाइट्स रखने वाले पेज को PAGE_GUARD से चिह्नित करें, VEH में STATUS_GUARD_PAGE_VIOLATION को पकड़ें, निरीक्षण करें, एक निजी ट्रैम्पोलिन पर रीडायरेक्ट करें, ट्रैप फ्लैग सेट करें, एकल-चरण बाहर निकलें, पेज को वापस सशस्त्र करें। प्रति Nt कॉल एक ट्रैप, चाहे लोडर वहाँ कैसे भी पहुंचे। यह Hell's Gate को छोड़कर सब कुछ के खिलाफ काम करता है।
समस्या यह है कि OS सहयोग नहीं करता। PAGE_GUARD एक शॉट है, इसका क्या मतलब है? हर बार जब यह फायर करता है तो बिट साफ हो जाता है और आपको इसे वापस रखना होता है। आपका हैंडलर syscalls करता है (गार्ड को वापस रखने के लिए NtProtect, फिर से शुरू करने के लिए NtContinue) और उन syscalls में स्टब होते हैं और वे स्टब उस पेज पर होते हैं जिसे आपने अभी गार्ड किया था। मैंने इसका अधिकांश भाग एक अलग पेज पर बने निजी syscall स्टब्स के साथ हल किया जो हमारे स्वामित्व में था (RWX आवंटित करें, mov r10,rcx; mov eax,SSN; syscall; ret लिखें, RX लॉक करें, ntdll को कभी न छुएं), लेकिन यह नाजुक था। हर विंडोज माइनर वर्जन ने टाइमिंग बदल दी। नमूने द्वारा स्पॉन किया गया प्रत्येक थ्रेड इंटीग्रिटी वर्कर के खिलाफ एक और दौड़ था जो गार्ड का पुनर्निर्माण कर रहा था। एक ही मशीन पर दस रनों में डेमो सफलता लगभग 30 से 50 प्रतिशत थी।
आखिरकार मैंने विंडोज को यह समझाने की कोशिश करना बंद कर दिया कि PAGE_GUARD को मेरी इच्छानुसार व्यवहार करना चाहिए, और इसके बजाय बाइट को ओवरराइट करने की कोशिश की।
https://github.com/user-attachments/assets/0cb670fd-e51b-413e-bf00-08f9297888ed
syscall निर्देश पर बाइट 0F 05 है। पहली बाइट अकेली (0F) दो-बाइट ओपकोड के एक परिवार का उपसर्ग है जिसमें SYSCALL, CPUID और RDTSC शामिल हैं। अपने आप में यह निष्पादन योग्य नहीं है; CPU को दूसरे बाइट की आवश्यकता है। यदि आप पहली बाइट को 0xCC (INT3) से बदल देते हैं, तो जोड़ी CC 05 बन जाती है, जिसे CPU INT3 के रूप में डिकोड करता है जिसके बाद एक भटका हुआ बाइट होता है जिस तक वह कभी नहीं पहुँचता। कोई भी कोड पथ जो उस पते पर उतरता है, EXCEPTION_BREAKPOINT उठाता है।
यह पूरी तंत्र है। हमारा VEH ब्रेकपॉइंट को पकड़ता है, देखता है कि पता किस स्टब का है (हम init समय पर ntdll के एक्सपोर्ट को गणना करके मानचित्र बनाते हैं), जो कोई भी आया उस पर तीन जाँच चलाता है, और Context->Rip को एक निजी ट्रैम्पोलिन पर सेट करता है जो वास्तविक syscall करता है और लौटता है। बाइट CC ही रहता है। अगला कॉलर उसी तरह से इसे हिट करता है, इसलिए पेज प्रोटेक्शन को आगे-पीछे फ्लिप करने की कोई आवश्यकता नहीं है।
स्पष्ट होने के लिए: हाँ, यह बाइट ओवरराइट द्वारा हुकिंग है। यह सिर्फ उस बाइट पर नहीं है जिसे लोग आमतौर पर "ntdll हुक" से समझते हैं। क्लासिक EDR हुक स्टब के पहले बाइट्स को JMP <my_func> से ओवरराइट करता है:
ntdll!NtAllocateVirtualMemory:
E9 ?? ?? ?? ?? jmp my_hook ; overwrites "mov r10, rcx"
...
0F 05 syscall
C3 ret
यह ठीक वही है जो इनडायरेक्ट syscalls के आसपास चलता है। लोडर स्वयं SSN पढ़ता है और सीधे 0F 05 पर कूदता है, प्रोलॉग (और आपका jmp) कभी नहीं चलता, हुक कभी फायर नहीं करता। हम जो करते हैं वह syscall बाइट को ही ओवरराइट करना है:
ntdll!NtAllocateVirtualMemory:
4C 8B D1 mov r10, rcx ; अछूता
B8 18 00 00 00 mov eax, 18h ; अछूता
CC 05 int3 / 05 ; था 0F 05, हमने 0F के ऊपर CC लिखा
C3 ret
अब इससे कोई फर्क नहीं पड़ता कि आप उस पते पर कैसे पहुँचे। प्रोलॉग के माध्यम से, एक इनडायरेक्ट जंप के माध्यम से जो प्रोलॉग को छोड़ता है, एक VEH-syscall संदर्भ पुनर्लेखन के माध्यम से जो RIP को वहाँ गिराता है, जो भी हो। यदि CPU उस बाइट को निष्पादित करता है, तो यह ट्रैप करता है। क्लासिक हुक के समान तकनीक परिवार, अलग स्थान, पूरी तरह से अलग कवरेज।
ट्रैम्पोलिन इस तरह दिखता है:
F3 0F 1E FA endbr64
49 89 CA mov r10, rcx
B8 <stub SSN> mov eax, ssn
0F 05 syscall
C3 ret
तीन जाँच PAGE_GUARD संस्करण से अपरिवर्तित हैं क्योंकि वे सही तीन जाँच थीं; उन्हें बस एक विश्वसनीय स्थान पर रहने की आवश्यकता थी।
वापसी पता। [RSP] वह है जहाँ syscall वापस आ गया होता। एक वास्तविक कॉल के लिए यह ntdll, kernel32, kernelbase, या संबंधित रनटाइम DLL में से किसी एक के अंदर है। Hell's Hall के लिए यह लोडर के जिस भी RWX पेज से चल रहा है, उसके अंदर है। हमारे पास init पर उन नामों के लिए GetModuleHandle कॉल करके और उनके PE हेडर से .text रेंज पढ़कर बनाए गए विश्वसनीय रिटर्न लक्ष्यों की एक छोटी सूची है।
SSN। स्टब का प्रोलॉग syscall बाइट तक पहुँचने से पहले ही चल चुका होता है, इसलिए eax में वह मान होता है जो इसमें लोड किया गया था। यदि लोडर ने Tartarus स्वैप किया, तो वह मान उस SSN से मेल नहीं खाएगा जिसे हमने गणना के समय इसी स्टब से पढ़ा था। हम बेमेल को लॉग करते हैं, और ट्रैम्पोलिन अपने स्वयं के syscall से पहले सही SSN लिखता है, इसलिए जो कर्नेल फ़ंक्शन चलता है वह बाइट का है न कि वह जो लोडर चाहता था। तकनीक को उसी चरण में लॉग और बेअसर किया जाता है।
स्टैक वॉक। RtlVirtualUnwind वर्तमान संदर्भ से, पाँच फ्रेम ऊपर। प्रत्येक फ्रेम का RIP एक ज्ञात मॉड्यूल के अंदर होना चाहिए और उसकी एक RUNTIME_FUNCTION प्रविष्टि होनी चाहिए। शेलकोड और ROP गैजेट इसमें विफल होते हैं, भले ही [RSP] स्वयं विश्वसनीय दिखता हो, जिसे एक लोडर नकली बना सकता है (यह मोटे तौर पर अनुमान लगा सकता है कि इसका कॉलर मेमोरी में कहाँ होगा और वहाँ एक विश्वसनीय वापसी पता गढ़ सकता है)।
यदि कोई जाँच विफल होती है, तो हम एक लॉक-फ्री रिंग में एक छोटा स्ट्रक्ट पुश करते हैं और ड्रेन थ्रेड अगली बार जागने पर इसे प्रिंट करता है।
[!! hallwatch !!] indirect syscall (अविश्वसनीय कॉलर, इस स्टब के लिए गलत ssn)
syscall : NtAllocateVirtualMemory
syscall rip : 0x00007FF827660372
return addr : 0x00007FF7EED719D6
rax (ssn) : 0x0000000F (stub encodes 0x00000018)
thread : 26388
Hell's Gate वह जगह है जहाँ INT3 मदद करना बंद कर देता है। हमने कभी लोडर के RWX पेज को पैच नहीं किया क्योंकि हमें कभी पता नहीं चला कि यह अस्तित्व में है।
इसके बजाय हम जो करते हैं वह सबसे सरल स्कैनर है जो काम कर सकता है। हर 250 ms (मूल रूप से बहुत बार, CPU चक्र बर्बाद करता है लेकिन यह PoC के लिए है) इंटीग्रिटी वर्कर VirtualQuery के साथ पता स्थान में चलता है, किसी भी MEM_COMMIT क्षेत्र को देखता है जिसमें निष्पादन योग्य पेज प्रोटेक्शन है। यदि क्षेत्र लोड किए गए मॉड्यूल के अंदर है तो इसे छोड़ दिया जाता है। यदि यह हमारे स्वयं के ट्रैम्पोलिन पूल के अंदर है तो इसे छोड़ दिया जाता है। जो कुछ बचता है वह विदेशी निष्पादन योग्य मेमोरी है। हम इसमें 0F 05 बाइट जोड़ियों के लिए 64 KB तक स्कैन करते हैं, पते के अनुसार डुप्लिकेट हटाते हैं, प्रत्येक अद्वितीय हिट को एक बार लॉग करते हैं, मुझे पता है कि यह बेवकूफी है और आप CPU चक्र भी बर्बाद करते हैं लेकिन यह PoC है जो स्थिति को कम नहीं करना चाहिए लेकिन वर्तमान में करता है।
यह एक ताजा RWX आवंटन में Hell's Gate को पकड़ता है और यह शैडो ntdll (एक लोडर जो एक नए पते पर एक निजी प्रतिलिपि प्राप्त करने के लिए NtMapViewOfSection करता है) को पकड़ता है। शैडो मामला एक मजेदार है। मैपिंग हमारे मॉड्यूल स्नैपशॉट में नहीं है, इसलिए इसके syscall निर्देश विदेशी निष्पादन योग्य बाइट्स के रूप में दिखाई देते हैं, भले ही वे डिस्क पर पूरी तरह से वैध हस्ताक्षरित DLL से आए हों।
एक अन्य प्रकार: हमलावर अपने स्वयं के बाइनरी के .text में 0F 05 बेक करता है, ताकि syscall RWX पेज के बजाय लोड किए गए मॉड्यूल के अंदर रहे। विदेशी-RWX लोड किए गए मॉड्यूल को छोड़ देता है, इसलिए यह पहले फिसल जाता था। इसलिए हमारे पास एक और स्कैनर है जो प्रत्येक मॉड्यूल के निष्पादन योग्य बाइट्स में चलता है, ntdll, win32u और wow64cpu (तीन मॉड्यूल जिनमें वैध रूप से syscall निर्देश होते हैं) को छोड़ता है, और 0F 05 की रिपोर्ट केवल तभी करता है जब अगले तीन बाइट्स के भीतर C3 हो। ret जाँच ही इसे उपयोगी बनाती है। 0F 05 अपने आप में कंपाइलर आउटपुट में बड़े निर्देशों के भागों के रूप में हर समय दिखाई देता है लेकिन कभी भी C3 के समीप नहीं होता। वास्तविक syscall स्टब हमेशा होते हैं। लॉग:
[hallwatch] scan: module-text syscall stub at 0x00007FF7FFC9227C in HellsHall-Demo.exe -- Hell's Gate inside loaded module .text
इंटीग्रिटी वर्कर के दो अन्य छोटे काम हैं। यह प्रत्येक टिक पर प्रत्येक पैच किए गए स्टब पर बाइट पढ़ता है और यदि वह CC नहीं है तो CC वापस लिखता है। यह सबसे सरल संभव अनहुक है: नमूना मूल 0F को कहीं से पढ़ता है (एक ताजा ntdll मैपिंग, डिस्क पर PE, एक ज्ञात-अच्छा बाइट तालिका) और हमारे पैच के ऊपर वापस लिखता है। हम अगले टिक पर नोटिस करते हैं और फिर से पैच करते हैं।
हर पचासवें टिक (लगभग 12.5 सेकंड) पर वर्कर श्रृंखला के शीर्ष पर हमारे VEH को फिर से स्थापित करता है और पिछले हैंडल को हटा देता है। एक नमूना LdrpVectorHandlerList में चल सकता है और हमें अनरजिस्टर कर सकता है; वर्कर सीमित करता है कि वह अंतराल कितने समय तक खुला रहता है। बारह सेकंड एक लंबी विंडो है यदि आप पर हमला किया जा रहा है और एक छोटी है यदि आप एक सैंडबॉक्स में मैलवेयर चला रहे हैं। मैं शायद इसे छोटा कर दूंगा जब मैं इसे अधिक बार करने की लागत को माप लूंगा।
वर्कर थ्रेड एक TLS फ्लैग के साथ चलता है जिसे VEH जाँचता है। वर्कर के अपने syscalls (पैचिंग के लिए NtProtect, स्कैन के लिए NtQuery) किसी भी अन्य की तरह हमारे ब्रेकपॉइंट को ट्रिगर करते हैं, लेकिन फ्लैग VEH को डिटेक्शन लॉजिक को छोड़ने और चुपचाप ट्रैम्पोलिन के माध्यम से रीडायरेक्ट करने के लिए कहता है।
क्रिटिकल सेक्शन पहली बार जब कोई कॉल करता है तो स्वयं को इनिशियलाइज़ करता है, तीन-अवस्था कम्पेयर-एक्सचेंज (0 = अनिनिशियलाइज़्ड, 2 = व्यस्त, 1 = तैयार) के माध्यम से। यह बदसूरत है लेकिन यह DLL के अंदर एक स्थिर कंस्ट्रक्टर की आवश्यकता से बचता है, जो विंडोज पर लोडर लॉक आदि से संबंधित समस्याओं का एक अलग सेट है।
एक ABI बात जो उल्लेखनीय है। INT3 एक ट्रैप है, जिसका अर्थ है कि जब हमारा VEH कॉल किया जाता है तो Context->Rip अगले निर्देश की ओर इशारा करता है, ट्रैप की ओर नहीं। यदि हमने 0x7FF827660372 पर बाइट को पैच किया, तो Context->Rip VEH पर 0x7FF827660373 के रूप में पहुँचता है। Record->ExceptionAddress ट्रैप की ओर इशारा करता है, लेकिन हमें ट्रैम्पोलिन पर रीडायरेक्ट करने से पहले Context->Rip = ExceptionAddress रीसेट करना होगा, अन्यथा ट्रैम्पोलिन एक बाइट देर से शुरू होता है और syscall कुछ भी उपयोगी नहीं करता है, यह आपको बता रहा हूँ क्योंकि यह एक बग था जिसे खोजने में मुझे बहुत समय लगा।
चार एक्सपोर्ट हैं। IscInitialize डिटेक्टर को सशस्त्र करता है; DllMain इसे स्वचालित रूप से कॉल करता है लेकिन यदि आप रिटर्न वैल्यू चाहते हैं तो आप इसे होस्ट प्रक्रिया से कॉल कर सकते हैं। IscGetDetectionCount एक नीरस रूप से बढ़ने वाला काउंटर लौटाता है। IscShutdown इन-फ्लाइट हैंडलर्स की प्रतीक्षा करता है और 0F बाइट्स को पुनर्स्थापित करता है। IscFlush रिंग को सिंक्रोनस रूप से ड्रेन करता है, उपयोगी है यदि आप डिटेक्टर को एक सैंडबॉक्स में एम्बेड कर रहे हैं जिसे नमूने से बाहर निकलने से पहले घटनाओं की आवश्यकता है।
न्यूनतम एकीकरण LoadLibrary है। DllMain init को संभालता है और वहाँ से दोनों पृष्ठभूमि थ्रेड शुरू करता है।
Isc = इनडायरेक्ट Syscalls
चीजें जो हम अभी तक नहीं पकड़ते।
एक नमूना जो इंटीग्रिटी जाँच करता है।
एक नमूना जो उन स्टब्स का उपयोग करता है जिन्हें हमने पैच नहीं किया। वर्तमान अनुमति सूची में लगभग 40 नाम हैं जो आक्रामक मेमोरी, प्रक्रिया, थ्रेड, सेक्शन, टोकन और फ़ाइल प्रिमिटिव को कवर करते हैं। जब तक DllMain जल्दी समाप्त होता है, अधिक जोड़ना वृद्धिशील है। लोडर लॉक के अंदर से सभी 488 स्टब्स को पैच करना... बस एह...
एक नमूना जो कर्नेल विशेषाधिकारों के साथ चल रहा है। यूज़रमोड समस्या नहीं।
INT3 वह है जिसे हमने अंततः चुना, लेकिन ट्रैप तंत्र दिलचस्प हिस्सा नहीं है। यह PAGE_GUARD से बेहतर काम करने का कारण यह है कि ट्रैप को OS के साथ एक चालू वार्ता की आवश्यकता नहीं है। बाइट CC है। यह CC ही रहता है। OS की इस बारे में कोई राय नहीं है कि ntdll में कौन से बाइट्स रहते हैं।