Golang for infosec — Building an EDR #Part3 — Registry and Startup folder persistence mechanisms hunting.
--
When we talk about detecting malware on a compromised system, it is most often a question of looking at the most representative behaviours of a malicious binary. All you have to focus on is usually based on tactics, techniques and procedures (TTPs).
TTPs — an acronym that has become THE Cyber Threat Intelligence’s buzzword. Without getting into the debate about whether or not it is worth focusing on this, it is an aspect that I find interesting in a EDR both for performance and efficiency reasons.
This is why I will try to focus the upcoming articles on behaviours, mainly based on the most common techniques used by malwares.
Persistence — A starting point for scanning guidance
In the previous parts, I mentioned how it was possible to extract processes’s memory, use memory dumps and scan them with YARA. If you ever want to go back over these parts, it’s this way:
Analysing process memory and executables is interesting, but searching in the entire file system is both inefficient and very resource-intensive. A common feature for malwares is the ability to persist over time. Sometimes launched when the computer is started, when a user logon or when this user performs a given action, the malware generally has the ability to restart itself automatically.
There are many mechanisms of persistence. In order to prioritise the detection capabilities of my EDR, I chose to first focus on the most commonly observed techniques — registry keys, scheduled tasks, start-up folder links and Windows services.
After having listed these persistence sources, the idea will be, in a first step, to scan the associated executables or commands. I will deliberately not deal with the part related to scanning because we have already talked about it in the previous article. For the full implementation, have a look at my project on Github.
Registry keys
Windows registry is one of the most widely used way of ensuring persistence. This technique can be done either as a simple user or after an increase in privileges. Golang offers some possibilities to simplify queries to the Windows registry through the golang.org/x/sys/windows/registry module.
This allows us to search for persistence on registry keys quite easily:
// EnumRegistryPeristence get all the potential registry values used for persistencefunc EnumRegistryPeristence() (values []RegistryValue, errors []error) {
keys := []registry.Key{registry.USERS, registry.LOCAL_MACHINE}
subkeys := []string{`SOFTWARE\Microsoft\Windows\CurrentVersion\Run`, `SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce`} for _, k := range keys {
for _, s := range subkeys {
v, err := EnumRegHivePersistence(k, s)
if err != nil {
errMsg := fmt.Errorf(“%s\\%s — %s”, GetRegistryHiveNameFromConst(k), s, err) errors = append(errors, errMsg)
} for _, value := range v {
values = append(values, value)
} }
}
return values, errors
}
What have we done? First, it is ecessary to iterate on keys located in both HKEY_LOCAL_MACHINE and HKEY_USERS hives. What difference does it make? It is only a matter of scope. The values in HKEY_LOCAL_MACHINE will apply to all users but will require the malware to have high privileges (administrators) to ensure persistence in this hive. Keys in HKEY_USERS have scope only for the user who created them. Most malicious samples will ensure persistence with user privileges because, even if limited to user scope, it doesn’t require to be an admin to do this.
For each hive, we will focus on two main paths:
- SOFTWARE\Microsoft\Windows\CurrentVersion\Run
- SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
The main difficulty lies in the HKEY_USERS hive. This hive is splitted in order to contains keys organised from Windows SIDs. Some of theses paths will not be relevant. How to manage this?
// EnumRegHivePersistence parse the specified key and subkey and return all string key/value in a []RegistryValuefunc EnumRegHivePersistence(key registry.Key, subkey string) (values []RegistryValue, err error) {
k, err := registry.OpenKey(key, "", registry.READ)
if err != nil {
return nil, err
}
defer k.Close() subkeys, err := k.ReadSubKeyNames(0)
if err != nil {
return nil, err
} if key == registry.USERS {
for _, s := range subkeys {
if len(s) > 10 && !strings.HasSuffix(s, "_Classes") {
v, err := enumRegSubKey(key, s+`\`+subkey)
if err != nil {
return nil, err
} for _, item := range v {
values = append(values, item)
}
}
}
} else {
v, err := enumRegSubKey(key, subkey)
if err != nil {
return nil, err
} for _, item := range v {
values = append(values, item) }
}
return values, nil
}
The first part of this code is here to avoid listing well-known security identifier. Then, for the same SID we will exclude the key that has “_Classes” suffix (that is a user scoped HKEY_CLASSES_ROOT content).
Finally, as we are now able to iterate throw all theses registry key, we now have to list Run and RunOnce values like this
// enumRegSubKey format subkey values in []RegistryValuefunc enumRegSubKey(key registry.Key, subkey string) (values []RegistryValue, err error) { var (
sk registry.Key
sv []string
) sk, err = registry.OpenKey(key, subkey, registry.READ)
if err != nil {
return nil, err
} sv, err = sk.ReadValueNames(0)
if err != nil {
return nil, err
} for _, item := range sv {
v, _, _ := sk.GetStringValue(item)
if len(v) > 0 {
var result = RegistryValue{key: key, subKey: subkey, valueName: item, value: v}
values = append(values, result)
}
} return values, nil
}
With this job done, we now have all the desired registry values. All that remains to be done is to scan the associated files.
Start-up folders
Again, this is one of the historical persistence mechanisms on Microsoft Windows. Although this feature has changed over time, it is still widely used for both legitimate and illegitimate purposes. In this article, i will only focused on what’s currently true in the latest Windows version:
How does it work? The attacker generally write a *.lnk file in one of the following directories in order to make it lauched:
- %SYSTEMDRIVE%\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp
- %APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup
These two folders have a similar role as the registry keys previously mentioned above with a user or entire computer scope. The first folder is used on every user logon and the second one belongs to one unique user.
Apart from searching through each file, what is the main problem? lnk is a binary file format much more complex than it appears to be. If you’re interested on how it works, Microsoft has described it here.
Hopefully, we don’t need to do all from scratch and github.com/parsiya/golnk is a really nice library to help you. In a few lines, here’s an implementation that will do the job:
// ListStartMenuFolders return a []string of all available StartMenu foldersfunc ListStartMenuFolders(verbose bool) (startMenu []string, err error) {
var usersDir []string
startMenu = append(startMenu, os.Getenv("SystemDrive")+`\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp`) usersDir, err = RetrivesFilesFromUserPath(os.Getenv("SystemDrive")+`\Users`, false, nil, false, verbose) if err != nil {
return startMenu, err
} for _, uDir := range usersDir {
startMenu = append(startMenu, uDir+`\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup`)
} return startMenu, err
}// ListStartMenuLnkPersistence check for every *.lnk in Windows StartMenu folders and extract every executable path in those linksfunc ListStartMenuLnkPersistence(verbose bool) (exePath []string, errors []error) {
startMenuFolders, err := ListStartMenuFolders(verbose)
if err != nil {
errors = append(errors, err)
} for _, path := range startMenuFolders {
files, err := RetrivesFilesFromUserPath(path, true, []string{".lnk"}, false, verbose)
if err != nil {
errors = append(errors, fmt.Errorf("%s - %s", path, err.Error()))
} for _, p := range files {
lnk, lnkErr := golnk.File(p)
if lnkErr != nil {
errors = append(errors, fmt.Errorf("%s - Lnk parse error", p))
continue
}
exePath = append(exePath, lnk.LinkInfo.LocalBasePath)
}
}
return exePath, errors
}
Handling complex paths
As you maybe noticed, registry and startup folders persistence will generally refers to a specific path that represents you malware executable location. However, it doesn’t means that a persistence mechanism has to directly point to a malware. Instead, the malware could be launched through a specific cmd / powershell command, or called by a legitimate Windows binary, or called using special paths… That’s why it will be necessary to parse complex paths and dissect each part. To be precise, our program shoud be able to interpret the following paths among others:
- “C:\Path\Example\Binary.exe” -attribute
- “%windir%\system32\Binary.exe”
- “%windir%\system32\cmd.exe” -c “run c:\Path\To\Binary.exe”
- “rundll.exe c:\Path\To\file.dll,LockWorkStation”
- …
Here is how i try to solve this. It’s not perfect but it works in most cases:
// FormatPathFromComplexString search for file/directory path and remove environments variables, quotes and extra parametersfunc FormatPathFromComplexString(command string) (paths []string) {
var buffer []string
// quoted path
if strings.Contains(command, `"`) || strings.Contains(command, `'`) {
re := regexp.MustCompile(`[\'\"](.+)[\'\"]`)
matches := re.FindStringSubmatch(command) for i := range matches {
if i != 0 {
buffer = append(buffer, matches[i])
}
}
} else {
for _, i := range strings.Split(strings.Replace(command, ",", "", -1), " ") {
buffer = append(buffer, i)
}
} for _, item := range buffer {
// environment variables
if strings.Contains(command, `%`) {
re := regexp.MustCompile(`%(\w+)%`)
res := re.FindStringSubmatch(item)
for i := range res {
item = strings.Replace(item, "%"+res[i]+"%", os.Getenv(res[i]), -1)
}
} // check if file exists
if _, err := os.Stat(item); !os.IsNotExist(err) {
paths = append(paths, item)
}
} return paths
}
With this, you will be able to split everything that is a valid file path and also the literal string (in order to also handle powershell / cmd inline execution)
That’s all for today, i hope you enjoyed this article. In the next part, we will cover other way of persistence like task scheduler and windows services.