In this 3-part blog post we will take a deep-dive into Trend Micro Antivirus for macOS. In our normal blogs we often detail a single security issue. This time we dig deeper and look at multiple attack vectors against a single product:
In part 2 we described how a local attacker can exploit an issue Trend Micro Antivirus and get code execution as root. In this post we will investigate possible attacks against a kernel extensions. Trend Micro installs multiple kernel extensions, also called kexts. We will take a look at one of them, called
Most of the bugs in this blog post are quite low-level. To understand them better it is useful to take first look at
KERedirect from a high level.
The main responsibility of this kext is to block access to malicious websites. It intercepts network connections in the kernel and filters them looking for malicious traffic. We can see the kext in action by visting a malicious webiste. At the time of writing the domain
harpa.site is considered malicious. The domain was published in a report by Trend Micro. If a user tries to visit the page it will be blocked:
[email protected] ~ % curl harpa.site <html><h1>blocked by TrendMicro<h1></html>
The following diagram shows an overview of the kext's architecture:
We start analyzing the diagram at (1), the point where a user visits a website. The first step is the data reaching
NKE. This is the
Network Kernel Extension provided by Apple to kexts. It will callback to
At this point the kext will collect the outgoing request and forward it to
libTmProxy.so. This is a core component in filtering traffic. This daemon does all the parsing of HTTP requests, extracting headers and deciding if the request is malicious. All this code runs in userland. Once a decision has been made it will inform
KERedirect.kext which will enforce it and either block the connection or not.
It's important to note the kext does "as little as possible". Almost all logic is done in userland. I think this is a good approach. It reduces the code that has to run with kernel privileges.
I was initially interested in how the extension deals with large HTTP requests. It is expensive to keep large buffers in kernel memory. So at one point it has to decide to forward it to userland. How is this decision make and what are the limits?
Collecting requests in the kext is done by
The buffer containing the HTTP request is stored in an
mbuf chain. The code iterates over this chain and stores the data. At line 260 there is a check to make sure the size stays below 0x7FF+1 bytes. If the request is larger it is ignored. The reasononing is that the matching buffer in userland is only 0x800 bytes. To prevent any overflows all larger requests are just dropped.
Although this prevents overflows between these components it also makes for an easy bypass: just send a large request. The following example shows that visiting the website
hxxp://harpa.site is normally blocked. However, if an attacker supplies a large HTTP request it will bypass the protection:
# this request is blocked [email protected] ~ % curl harpa.site <html><h1>blocked by TrendMicro<h1></html> # this request is not [email protected] ~ % curl -vv -H "Header: "$(python -c 'print "A"*0x800') harpa.site * Trying 18.104.22.168... * TCP_NODELAY set * Connected to harpa.site (22.214.171.124) port 80 (#0) > GET / HTTP/1.1 > Host: harpa.site > User-Agent: curl/7.64.1 > Accept: */* > Header: AAAA[..]AAAA > < HTTP/1.1 401 Unauthorized [..] <title>401 Unauthorized</title> # The "401" error message is the response from the actual server
This bypass was assigned CVE-2020-25777.
While reversing the kext I noticed there are many log messages in the code. These were probably used during development and are left behind for debugging. The log messages are not enabled by default as the loglevel defaults to '4'. This seems to correspond with
LOG_ERROR. It disables most logging except for errors.
However, there is one log message that is printed in level 4 that also prints a kernel pointer. Pseudo-code from KERedirect's function
Debug messages from the kernel are readable for all users on macOS. This is different from eg Linux, where only privileged users can view kernel logs. Any user can trigger the vulnerable function while monitoring the kernel logs. The log message is printed when visiting a blocked website, so we can use the same request as before.
[email protected] ~ % log stream --predicate "sender == 'KERedirect'" & Filtering the log data using "sender == "KERedirect"" # visit blocked website [email protected] ~ % curl --silent harpa.site | grep TITLE <TITLE>Trend Micro</TITLE> # kernel pointers leak: Timestamp Thread Type Activity PID TTL [..](KERedirect) [KERedirect] ****node->data=0xffffff8184a11a00, so=0xffffff803d498520
Leaking kernel pointers is a security issue as it can help with calculating the kernel slide (kernel ASLR) and make further exploitation possible. This issue was assigned CVE-2020-27015.
While the last bug leaked kernel pointers we didn't have much control over it. A better bug is in the function
handle_set_result(). This function has a verbose debug message that prints information about a 'node':
The caller of the function has control over
data at line 57. From this buffer the argument
tl_track is taken and at line 73 this argument is used as a pointer. An attacker can supply any kernel pointer he wants and leak 4 bytes of memory (interpreted as a
To exploit the issue the
debug_level must first be set to a high value to enable the debug prints. Doing so requires root privileges. But as we have shown in part 2 of this series any user can get those. The kernel extension exposes as sysctl to update it's debug level:
This bug gives allows root users to read arbitraty kernel memory. But what address to read? Dereferencing an invalid pointer will crash the kernel. On MacOS all data in the kernel is “slid” by a random offset determined as boot (kernel ASLR). One way to get an initial address is to take another look at the log message printed by
KERedirect. With the highest
debug_level enabled there are many that log kernel pointers:
The attached exploit uses a different strategy. It inserts itself as a new version of
libTmProxy.so by registering its own callback. When
KERedirect now calls back to userland it will call the exploit instead of the original
libTmProxy.so. This gives a useful primitive as the callback contains a kernel pointer to something called the
peddingList. By walking elements from this list the exploit can determine the base address of the kext, and after that the base address of the macOS kernel itself.
Sample output of the exploit
[email protected] ~ % sudo ./tm_read_mem Connecting Control ID: 12 Starting logwatcher Waiting 2s for the logwatcher to settle net.tm.redirect.debug: 4 4 -> 9 9 Starting request handler Waiting for a record tl_track: 0xffffff803ad30500 Waiting 3s for timeout Got peddlelist: 0xffffff7fa0dffa40 kext_base: 0xffffff7fa0df0000 kernel_slide: 0x1e000000 kernel_base: 0xffffff801e200000
This issue was assigned CVE-2020-25778.
For the last bug we have to take a look at the function
url_rating_data_out(). This function prepares the error page that will be returned to the user. It does so by fetching the page from memory and updating the error message on this page. The error message can be the current domain, URL, or other information. This is to present the user with a specific error message such as
Access to the website: *http://malicious.com* is blocked.
The problem is that the length of the page is calculated twice. The first time it's used to allocate a buffer. The second time it's used to determine how much data to copy into this buffer. The code assumes the content of the page hasn't changed between these two calls. But this assumption is not always true.
The following code shows the pseudo-code for allocating the buffer in
url_rating_data_out(). It calculates reponseLength as
strlen(blockpage) + strlen(urlStr) + 0xB and allocates a buffer of that size. It then calls the function
replace_cat() the content of the blockpage is updated with
urlStr. This updates the general error message with a specific one:
Access to the website: *http://evil.malicous* is blocked. The bug happens on the last line inside
strlen() is called AGAIN to calculate how many bytes to copy. If the contents of the blockpage (
page_ptr) has changed since it causes a problem.
For reference, the disassembly of the relevant function call:
strlen() is at 0x5B6B and the call to
memcpy() at 0x5B8F.
To trigger the issue someone must visit a blocked website and at the same time the contents of the blockpage must be updated. Visiting a malicious website is easy but I'm unaware of the official way to update the blockpage. I therefore implemented a custom client to talk to the kernel extension directly. Talking to the extension requires root, but as we have shown in part 2 any user can get root privileges.
Looking at the code to update the block page in
handle_set_ud_block_page() shows possible ways the bug can manifest:
Depending on when the bug hits this will either trigger:
The core issue of this bug is that the code to update the blockpage is not protected by the
BlockPageLock mutex. The attached exploit is only a proof-of-concept that triggers the issue and crash the kernel. Sample runs:
[email protected] ~ % sudo ./tm_racer Connecting Control ID: 12 Connected Starting race thread Going to trigger the bug Trigger: 0 Trigger: 1 Trigger: 2 Trigger: 3 Trigger: 4 Trigger: 5 Trigger: 6 Trigger: 7 Trigger: 8 ... hangs ...
Some of the exceptions when running the exploit with lldb attached as a kernel debugger show the different crash types:
This issue was assigned CVE-2020-27014. Sample exploit
All issues in this series were reported to the Zero Day Initiative. They helped us validate the issues and worked with Trend Micro to get them fixed.
|ZDI-20-1242||CVE-2020-25777||TMKA-09947||Web threat protection bypass|
|ZDI-20-1286||CVE-2020-27015||TMKA-09975||Kernel pointer leak|
|ZDI-20-1241||CVE-2020-25778||TMKA-09948||Kernel memory read|
|ZDI-20-1236||CVE-2020-25776||TMKA-09924||Privilege escalation to root (part 2)|
|ZDI-20-1243||CVE-2020-27013||TMKA-09950||Improper access control (part 1)|