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 KERedirect
.
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 KERedirect.kext
.
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 KERedirect
's url_rating_data_out()
:
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 109.68.213.102...
* TCP_NODELAY set
* Connected to harpa.site (109.68.213.102) 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 url_rating_data_in()
:
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.
Proof-of-concept:
[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 src_port
and dst_port
).
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:
sysctl net.tm.redirect.debug=9
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()
.
Inside of 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 replace_cat()
when 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:
The second 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 KERedirect
's 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 | CVE | Trend Micro | Description |
---|---|---|---|
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-1285 | CVE-2020-27014 | TMKA-09974 | Kernel TOCTOU |
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) |