2020-11-16 11:00 | Cees Elzinga

Trend Micro Antivirus - Part 3

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:

Part 3: Attacks against the kernel

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:

Trend Micro KERedirect flow

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.

Vulnerability 1: Web threat protection bypass

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():

Trend Micro KERedirect 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.

Vulnerability 2: kernel infoleak

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():

Trend Micro KERedirect 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.

Vulnerability 3: kernel memery read

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':

Trend Micro KERedirect handle_set_result

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:

Trend Micro KERedirect pointer log messages

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.

Trend Micro KERedirect flow

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

Full exploit

This issue was assigned CVE-2020-25778.

Vulnerability 4: kernel double fetch

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().

Trend Micro KERedirect url rating data out source

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.

Trend Micro KERedirect replace_cat

For reference, the disassembly of the relevant function call:

Trend Micro KERedirect replace_cat disassembly

The second strlen() is at 0x5B6B and the call to memcpy() at 0x5B8F.

Exploitation

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:

Trend Micro KERedirect set user defined blockpage

Depending on when the bug hits this will either trigger:

  1. Accessing free()-ed memory
  2. Null-pointer dereference
  3. Heap overflow (assuming new page is larger)

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:

Trend Micro KERedirect sample crashes

This issue was assigned CVE-2020-27014. Sample exploit

Fixes

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.

ZDICVETrend MicroDescription
ZDI-20-1242CVE-2020-25777TMKA-09947Web threat protection bypass
ZDI-20-1286CVE-2020-27015TMKA-09975Kernel pointer leak
ZDI-20-1241CVE-2020-25778TMKA-09948Kernel memory read
ZDI-20-1285CVE-2020-27014TMKA-09974Kernel TOCTOU
ZDI-20-1236CVE-2020-25776TMKA-09924Privilege escalation to root (part 2)
ZDI-20-1243CVE-2020-27013TMKA-09950Improper access control (part 1)

Contact us

+45 2054 4448

[email protected]

Vester Farimagsgade 41, 1606 Copenhagen V

Consulting | Training | Blog

© 2020 Danish Cyber Defence A/S · Vester Farimagsgade 41 · 1606 Copenhagen V · CVR 38871064