The curious case of Realtek and LSASS

TL;DR for general readers: RtkAuduservice64.exe might trigger your AV or EDR because of some weird quirks in the implementation of this process. It’s most likely a false positive. Don’t freak out immediately.

TL;DR for blue teams: RtkAuduservice64.exe is reading lsass.exe memory “by accident”. This makes it the ideal hiding spot for an attacker to dump creds from memory and blend in. 😱

TL;DR for red teams: RtkAuduservice64.exe is reading lsass.exe memory “by accident”. Inject yourself into this process, dump memory and you won’t stand out for reading lsass memory. 😈

Introduction

I was working on building some new hunts in Microsoft Defender ATP (MDATP). After spending a whole day writing KQL, I was almost done with it. I spent the day building new hunts to detect malicious code executions and wanted to fine-tune the last rule of the day to get rid of the false positives as much as possible. This rule was meant to detect malicious DLL injections (will publish the rule once it’s ready to share). I was going through the ‘*ApiCall’ actions in ‘DeviceEvents’ when I noticed that RtkAuduservice64.exe was calling ‘OpenProcess and ‘ReadProcessMemory’ on lsass.exe. To top it off, it’s doing OpenProcess with ‘PROCESS_ALL_ACCESS’. A quick Google revealed that this process is the “Realtek HD Audio Universal Service” and this is confirmed by the description in the file and the signature on the binary.

Wait…why is a sound driver service thingy reading LSASS 135 times in 7 days??

Results in ATP of the last 7 days for the OpenProcessApiCall event.

On Google I only found random tech support messages where people we’re asking if RtkAuduservice64.exe is malware and why Norton AV is alerting on RtkAuduservice64.exe.

By now I’m freaked out — I reached out to Twitter to see if someone has seen this before and knows what’s going on. Nothing…

Plotting the graph for the last 30 days, makes it worse…

My friend & colleague, Olaf Hartong joins the hunt and manages to confirm that in an totally unrelated and representative environment, the same thing is happening. By now, there is only one rabbit hole to go down in, IDA & WinDbg.

I could fairly quickly confirm with a reasonable degree of certainty that the reads to LSASS were in fact coming from the signed binary and were not injected. After another day of reversing in IDA, WinDbg & Ghidra and with the help of friend & colleague @gijs_h, we managed to crack the puzzle.

The binary is iterating over all the processes in a thread, trying to find certain processes and modules. It’s specifically looking for VOIP executables such as Teams.exe, Skype.exe, Lync.exe, Zoom.exe, etc. For each process found, it enumerates all DLLs loaded and if it finds a certain DLL, it will try to read the command line arguments of that process by reading its PEB.

At this point, the puzzle is only partially solved. The ‘OpenProcess can be traced back to the functionality of enumerating all loaded modules. You can only enumerate these if you open another process. However, the ReadProcessMemory is still a mystery. My understanding of the assembly was that the ReadProcessMemory was only triggered if the loaded module list contained a certain DLL name. Why would LSASS have what Realtek is looking for and hence sometimes trigger the ReadProcessMemory??

In the end, we figured out that it was an unintended side-effect of enumerating all the loaded modules. The call to K32EnumProcessModules triggers a ReadProcessMemory under the hood — which obviously makes sense once you realize that.

The next part of this blog will cover the the detailed technical deep dive, for those interested.


Technical deep dive

My biggest concern when I started with this ghost hunt, was that someone has injected into RtkAudUService64.exe and is dumping lsass from that process to hide their track. Professional deformation, I know…

To disprove this hypothesis somewhat, I need to find the calls to OpenProcess and ReadProcessMemory in the binary. If I find the code causing this, I can conclude with reasonable degree of certainty that it’s by design as the binary is signed by Realtek and Microsoft.

Looking at the imports, we see that the function is called twice, both in the same function:

If you want to re-use the addresses in the analysis below, my base address is set at 0x140000000.

Looking at this function, we notice that the error strings towards the end of the function reveal this functions name being “CWin32AppMgr::GetAppCommandline”.

This function first does a call to NtQueryInformationProcess, the OUT parameters of this function is in R8 (ProcessInformation) and the top of the stack (ReturnLength). Remember this is a x64 binary, so the x64 calling convention applies.

We can see that function call to NtQueryInformationProcess looks approximately like this:

NtQueryInformationProcess(LSASS_HANDLE*, ProcessBasicInformation, (PROCESS_BASIC_INFORMATION)var_740, 0x30, lpNumberOfBytesRead)

*rbp stores the second argument to GetAppCommandline, the handle to LSASS in our case.

Then the code checks if the pointer to PebBaseAddress is not null, and when it’s not, it does this:

ReadProcessMemory(LSASS_HANDLE, var_740.PebBaseAddress, &Buffer, 0x2C8, &NumberOfBytesRead);

Next, the function checks if this function was successful (i.e. non-zero return value).

If the result of the first ReadProcessMemory was successful, Buffer now contains a PEB struct. Defining this struct in IDA, shows that it takes the ProcessParameters+4 member of the PEB struct. I double checked my struct definition against the PEB definition at least 5 times, and can’t figure out why an offset of +4 . But this doesn’t seem to matter too much for the bigger picture.

Next it clears a 0x400 byte memory region called Dst and calls ReadProcessMemory again, as follows:

ReadProcessMemory(LSASS_HANDLE, *(Buffer.ProcessParameters+0x04)+0x78, Dst, 0x400, &NumberOfBytesRead);

So the ProcessParameters member is of type PRTL_USER_PROCESS_PARAMETERS. Looking at the definition, we see that offset 0x78 is the start of Buffer of the UNICODE_STRING CommandLine.

Looking at the next blocks, we see

Dst now has a unicode “string”, as the loop shows that it iterates over Dst with 2 bytes at a time until it hits a zero byte. Makes sense if the function name is GetAppCommandLine.

Now we know this does what it’s supposed to, let’s backtrack a bit. There is only one place where the function we just analyzed is being called. It’s at 0x1400775F0.

The first part of the function. Last part overlaps a bit with next screenshot for readability.

Here we see the following WinAPI calls:

  • K32EnumProcesses,
  • OpenProcess (which triggered the OpenProcess on LSASS)
  • K32EnumProcessModules
  • K32GetModuleBaseNameW
  • wcsicmp

So it first lists all running processes, for each process it lists all the loaded modules (i.e. DLLs), if there are 8 or more modules loaded, compares the first loaded module with r12 and if it’s a match, it calls GetAppCommandLine which then triggers ReadProcessMemory. I’m still not sure why it only looks at the first module, only if there are more than 8 or more modules loaded. I would expect a loop to iterate over all the modules, but well…it doesn’t really matter for our purpose.

This is what the decompiled code looks like in Ghidra.

But even then, the data wasn’t really matching up. The number of bytes read from LSASS reported by ATP was off by a magnitude as what I’ve expected from the GetAppCommandLine. As we saw earlier, the GetAppCommandLine reads 0x2C8 and 0x400 bytes, while ATP shows values between 98kb and 2400kb.

Finally, Gijs realized that this ReadProcessMemory which we were seeing wasn’t triggered directly from GetAppCommandLine function, but was triggered indirectly by K32EnumProcessModules…We confirmed this hypothesis.

The final difference which we needed to explain is the difference in the number of times OpenProcess was called compared to ReadProcessMemory. Both should appear an equal number of times according to our analysis so far, but the data shows differently.

This final difference can be explained by other calls in the binary to the OpenProcess function. OpenProcess is way more widely used, but given the requested access rights according to ATP, the only other candidate in the code is the call at address 0x14007771a.


Final thoughts

If you’re in a blue team and it’s up to you to defend your network, this is kind of an annoying situation. Strictly speaking, your AV should prevent that passwords are grabbed from LSASS memory. But aside from a number of special cases, this isn’t happening broadly. Hence, defenders have to rely on detection and hunting to catch malicious practices. This design flaw in the Realtek service allows an attacker to “hide in plain sight”.

We’ve reached out to Realtek to report this design flaw, and they handled it very professionally.

Thanks to @pelekhd for providing the contact details of Realtek Audio. Also thanks to the Realtek team who was very responsive and eager to get the issue resolved. They confirmed the hypothesis we had and we discussed and agreed on a better solution. Turns out, this new solution is accidently also 15 times faster :-).

31 July 2020 — First contact and reported this design flaw.
31 July 2020 — Confirmation of Realtek. Promised to look into it and respond back. 
10 August 2020 — In-depth discussion on potential solutions, pros and cons.
11 August 2020 — Test version privately released to me to confirm issue is resolved. 
28 August 2020 — Updated driver released including fix for this issue (v9013). Agreed to wait 4 weeks prior to releasing this article.

Knowledge center

Other articles

FalconHound, attack path management for blue teams

FalconHound, attack path management for blue teams

[dsm_breadcrumbs show_home_icon="off" separator_icon="K||divi||400" admin_label="Supreme Breadcrumbs" _builder_version="4.18.0" _module_preset="default" items_font="||||||||" items_text_color="rgba(255,255,255,0.6)" custom_css_main_element="color:...

Together. Secure. Today.

Stay in the loop and sign up to our newsletter

FalconForce realizes ambitions by working closely with its customers in a methodical manner, improving their security in the digital domain.

Energieweg 3
3542 DZ Utrecht
The Netherlands

FalconForce B.V.
[email protected]
(+31) 85 044 93 34

KVK 76682307
BTW NL860745314B01