June 5, 2018

F-Secure Anti-Virus: Remote Code Execution via Solid RAR Unpacking

As I briefly mentioned in my last two posts about the 7-Zip bugs CVE-2017-17969, CVE-2018-5996, and CVE-2018-10115, the products of at least one antivirus vendor were affected by those bugs. Now that all patches have been rolled out, I can finally make the vendor’s name public: It is F-Secure with all of its Windows-based endpoint protection products (including consumer products such as F-Secure Anti-Virus as well as corporate products such as F-Secure Server Security).

Even though F-Secure products are directly affected by the mentioned 7-Zip bugs, exploitation is substantially more difficult than it was in 7-Zip (before version 18.05), because F-Secure properly deploys ASLR. In this post, I am presenting an extension to my previous 7-Zip exploit of CVE-2018-10115 that achieves Remote Code Execution on F-Secure products.

Introduction

With my previous 7-Zip exploit I demonstrated how we can use 7-Zip’s methods for RAR header processing to massage the heap. This was not completely trivial, but after that, we were basically done. Since 7-Zip 18.01 came without ASLR, a completely static ROP chain was enough to obtain code execution.

With F-Secure deploying ASLR, such a static ROP chain cannot work anymore, and an additional idea is required. In particular, we need to compute the ROP chain dynamically. In a scriptable environment, this is usually quite easy: Simply leak a pointer to derive the base address of some module, and then just add this base address to the prepared ROP chain.

Since the bug we try to exploit resides within RAR extraction code, a promising idea could be to use the RarVM as a scripting environment to compute the ROP chain. I am quite confident that this would work, if only the RarVM were actually available. Unfortunately, it is not: Even though 7-Zip’s RAR implementation supports the RarVM, it is disabled by default at compile time, and F-Secure did not enable it either.

While it is almost certain the F-Secure engine contains some attacker-controllable scripting engine (outside of the 7-Zip module), it seemed difficult to exploit something like this in a reliable manner. Moreover, my goal was to find an ASLR bypass that works independently of any F-Secure features. Ideally, the new exploit would also work for 7-Zip (with ASLR), as well as any other software that makes use of 7-Zip as a library.

In the following, I will briefly recap the most important aspects of the exploited bug. Then, we will see how to bypass ASLR in order to achieve code execution.

The Bug

The bug I am exploiting is explained in detail in my previous blog post. In essence, it is an uninitialized memory usage that allows us to control a large part of a RAR decoder’s state. In particular, we are going to use the Rar1 decoder. The method NCompress::NRar1::CDecoder::LongLZ1 contains the following code:

if (AvrPlcB > 0x28ff) { distancePlace = DecodeNum(PosHf2); }
else if (AvrPlcB > 0x6ff) { distancePlace = DecodeNum(PosHf1); }
else { distancePlace = DecodeNum(PosHf0); }
// some code omitted
for (;;) {
  dist = ChSetB[distancePlace & 0xff];
  newDistancePlace = NToPlB[dist++ & 0xff]++;
  if (!(dist & 0xff)) { CorrHuff(ChSetB,NToPlB); }
  else { break; }
}

ChSetB[distancePlace] = ChSetB[newDistancePlace];
ChSetB[newDistancePlace] = dist;

This is very useful, because the uint32_t arrays ChSetB and NtoPlB are fully attacker controlled (since they are not initialized if we trigger this bug). Hence, newDistancePlace is an attacker-controlled uint32_t, and so is dist (with the restriction that the least significant byte cannot be 0xff). Moreover, distancePlace is determined by the input stream, so it is attacker-controlled as well.

So this gives us a pretty good read-write primitive. Note, however, that it has a few restrictions. In particular, the executed operation is basically a swap. We can use the primitive to do the following:

  • We can read arbitrary uint32_t values from 4-byte aligned 32-bit offsets starting from &ChSetB[0] into the ChSetB array. If we do this, we always overwrite the value we just read (since it is a swap).
  • We can write uint32_t values from the ChSetB array to arbitrary 4-byte aligned 32-bit offsets starting from &ChSetB[0]. Those values can be either constants, or values that we have read before into the ChSetB array. In any case, the least significant byte must not be 0xff. Furthermore, since we are swapping values, a written value is always destroyed (within the ChSetB array) and cannot be written a second time.

Lastly, note that the way the index newDistancePlace is determined restricts us further. First, we cannot do too many of such read/write operations, since the array NToPlB has only 256 elements. Second, if we are writing a value that is unknown in advance (say, a part of an address subject to ASLR), we might not know exactly what dist & 0xff is, so we need to fill (possibly many) different entries in the NToPlB with the desired index.

It is clear that this basic read-write primitive for itself is not enough to bypass ASLR. An additional idea is required.

Exploitation Strategy

We make use of roughly the same exploitation strategy as in the 7-Zip exploit:

  1. Place a Rar3 decoder object in constant distance after the Rar1 decoder containing the read-write primitive.
  2. Use the Rar3 decoder to extract the payload into the _window buffer.
  3. Use the read-write primitive to swap the Rar3 decoder’s vtable pointer with the _window pointer.

Recall that in the 7-Zip exploit, the payload we extracted in step 2 contained a stack pivot, the (static) ROP chain, and the shellcode. Obviously, such a static ROP chain cannot work in an environment with full ASLR. So how do we dynamically extract a valid ROP chain into the buffer without knowing any address in advance?

Bypassing ASLR

We are in a non-scriptable environment, but we still want to correct our ROP chain by a randomized offset. Specifically, we would like to add 64-bit integers.

Well, we might not need a full 64-bit addition. The ability to adjust an address by overwriting the least significant bytes of it could suffice. Note, however, that this does not work in general. Consider &f being a randomized address to some function. If the address was a completely uniform random 64-bit value, and we would just overwrite the least significant byte, then we would not know by how much we changed the address. However, the idea works if we know nothing about the address, except for the d least significant bytes. In this case, we can safely overwrite the d least significant bytes, and we will always know by how much we changed the address. Luckily2, Windows loads every module at a (randomized) 64K aligned address. This means that the two least significant bytes of any code address will be constant.

Why is this idea useful in our case? As you might know, RAR is strongly based on Lempel–Ziv compression algorithms. In these algorithms, the coder builds a dynamic dictionary, which contains sequences of bytes that occurred earlier in the compressed stream. If a byte sequence is repeating itself, then it can be encoded efficiently as a reference to the corresponding entry in the dictionary.

In RAR, the concept of a dynamic dictionary occurs in a generalized form. In fact, on an abstract level, the decoder executes in every step one of the following two operations:

  1. PutByte(bytevalue), or
  2. CopyBlock(distance,num)

The operation CopyBlock copies num bytes, starting from distance bytes before the current position of the window buffer. This gives rise to the following idea:

  1. Use the read-write primitive to write a function pointer to the end of our Rar3 window buffer. This function pointer is the 8-byte address &7z.dll+c for some (known) constant c.
  2. The base address &7z.dll is strongly randomized, but it is always 64K aligned. Hence, we can make use of the idea explained at the beginning of this section: First, we write two arbitrary bytes of our choice (using two invocations of PutByte(b)). Then, we copy (by using a CopyBlock(d,n) operation) the six most significant bytes of the function pointer &7z.dll+c from the end of the window buffer. Together, they form eight bytes, a valid address, pointing to executable code.

Note that we are copying from the end of the window buffer. It turns out that this works in general, because the source index (currentpos - 1) - distance is computed modulo the size of the window. However, the 7-Zip implementation actually checks whether we copy from a distance greater than the current position and aborts if this is the case. Fortunately, it is possible to bypass this check by corrupting a member variable of the Rar3 decoder with the read-write primitive. It is left as an (easy) exercise for the interested reader to figure out which variable this is and why this works.

ROP

The technique outlined in the previous section allows us to write a ROP chain that consists of addresses within a single 64K region of code. Does this suffice? Let’s see. We try to write the following ROP chain:

// pivot stack: xchg rax, rsp;
exec_buffer = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec_buffer, rsp+shellcode_offset, 0x1000);
jmp exec_buffer;

The crucial step of the chain is to call VirtualAlloc. All occurrences of jmp cs:VirtualAlloc I could find within F-Secure’s 7z.dll where at offsets of the form +0xd****. Unfortunately, I could not find an easy way to retrieve a pointer of this form within (or near) the Rar decoder objects. Instead, I could find a pointer of the form +0xc****, and used the following technique to turn it into a pointer of the form +0xd****:

  1. Use the read-write primitive to swap the largest available pointer of the form +0xc**** into the member variable LCount of the Rar1 decoder.
  2. Let the Rar1 decoder process a carefully crafted item, such that the member variable LCount is incremented (with a stepsize of one) until it has the form +0xd****.
  3. Use the read-write primitive to swap the member variable LCount into the end of the Rar3 decoder’s window buffer (see previous section).

As it turns out, the largest available pointer of the form +0xc**** is roughly +0xcd000, so we only need to increase it by 0x3000.

Being able to address a full 64K code region containing a jump to VirtualAlloc, I hoped that a ROP chain of the above form would be easy to achieve. Unfortunately, I simply could not do it, so I copied a second pointer to the window buffer. Two regions of 64K code, so 128K in total, were enough to obtain the desired ROP chain. It is still far from being nice, though. For example, this is what the stack pivot looks like:

0xd335c # push rax; cmp eax, 0x8b480002; or byte ptr [r8 - 0x77], cl; cmp bh, bh; adc byte ptr [r8 - 0x75], cl; pop rsp; and al, 0x48; mov rbp, qword ptr [rsp + 0x50]; mov rsi, qword ptr [rsp + 0x58]; add rsp, 0x30; pop rdi; ret;

Another example is how we set the register R9 to PAGE_EXECUTE_READWRITE (0x40) before calling VirtualAlloc:

# r9 := r9 >> 9
0xd6e75, # pop rcx; sbb al, 0x5f; ret;
0x9, # value popped into rcx
0xcdb4d, # shr r9d, cl; mov ecx, r10d; shl edi, cl; lea eax, dword ptr [rdi - 1]; mov rdi, qword ptr [rsp + 0x18]; and eax, r9d; or eax, esi; mov rsi, qword ptr [rsp + 0x10]; ret; 

This works, because R9 always has the value 0x8000 when we enter the ROP chain.

Wrapping up

We have seen a sketch of the basic exploitation idea. When actually implementing it, one has to overcome quite a few additional obstacles that are omitted to avoid boring you too much. Roughly speaking, the basic implementation steps are as follows:

  1. Use (roughly) the same heap massaging technique as in the 7-Zip exploit.
  2. Implement a basic Rar1 encoder to create a Rar1 item that controls the read-write primitive in the desired way.
  3. Implement a basic Rar3 encoder to create a Rar3 item that writes the ROP chain as well as the shellcode into the window buffer.

Finally, all items (even of different Rar versions) can be merged into a single archive, which leads to code execution when it is extracted.

Minimizing Required User Interaction

Virtually all antivirus products come with a so-called file system minifilter, which intercepts every file system access and triggers the engine to run background scans. F-Secure’s products do this as well. However, such automatic background scans do not extract compressed files. This means that it is not enough to send a victim a malicious RAR archive via e-mail. If one did this, it would be necessary for the victim to trigger a scan manually.

Obviously, this is still extremely bad, since the very purpose of antivirus software is to scan untrusted files. Yet, we can do better. It turns out that F-Secure’s products intercept HTTP traffic and automatically scan files received over HTTP if they are at most 5MB in size. This automatic scan includes (by default) the extraction of compressed files. Hence, we can deliver our victim a web page that automatically downloads the exploit file. In order to do this silently (preventing the user even from noticing that a download is triggered), we can issue an asynchronous HTTP request as follows:

<script>
  var xhr = new XMLHttpRequest(); 
  xhr.open('GET', '/exploit.rar', true); 
  xhr.responseType = 'blob';
  xhr.send(null);
</script>

UPDATE (2018-06-06): As @matalaz has pointed out, we do not even need JavaScript to trigger the desired GET request. It is sufficient to embed a simple img tag:

<img src='/exploit.rar'>

I have just tested this, and it works indeed.

Demo

The following demo video briefly presents the exploit running on a freshly installed and fully updated Windows 10 RS4 64-bit (Build 17134.81) with F-Secure Anti-Virus (also fully updated, but 7z.dll has been replaced with the unpatched version, which has been extracted from an F-Secure installation on April 15, 2018).

As you can see, the engine (fshoster64.exe) runs as NT AUTHORITY\SYSTEM, and the exploit causes it to start notepad.exe (also as NT AUTHORITY\SYSTEM).

Maybe you are asking yourself now why the shellcode starts notepad.exe instead of the good old calc.exe. Well, I tried to open calc.exe as NT AUTHORITY\SYSTEM, but it did not work. This has nothing to do with the exploit or the shellcode itself. It seems that it just does not work anymore with the new UWP calculator (it also fails to start when using psexec64.exe -i -s). If you have any clue why this is the case, please shoot me an e-mail.

Conclusion

We have seen how an uninitialized memory usage bug can be exploited for arbitrary remote code execution as NT AUTHORITY\SYSTEM with minimal user interaction.

Apart from discussing the bugs and possible solutions with F-Secure, I have proposed three mitigation measures to harden their products:

  1. Sandbox the engine and make sure most of the code does not run under such high privileges.
  2. Stop snooping into HTTP traffic. This feature is useless anyway. It literally does not provide any security benefit whatsoever, since evading it requires the attacker only to switch from HTTP to HTTPS (F-Secure does not snoop into HTTPS traffic – thank God!). Hence, this feature only increases the attack surface of their products.
  3. Enable modern Windows exploitation mitigations such as CFG and ACG.

Finally, I want to remark that the presented exploitation technique is independent of any F-Secure features. It works on any product that uses the 7-Zip library to extract compressed RAR files, even if ASLR and DEP are enabled. For example, it is likely that Malwarebytes was affected3 as well.

Do you have any comments, feedback, doubts, or complaints? I would love to hear them. You can find my e-mail address on the about page.

Alternatively, you are invited to join the discussion on HackerNews or on /r/netsec.

Timeline of Disclosure

  • 2018-03-06 - Discovery of the bug in 7-Zip and F-Secure products (no reliably crashing PoC for F-Secure yet).
  • 2018-03-06 - Report to 7-Zip developer Igor Pavlov.
  • 2018-03-11 - Report to F-Secure (with reliably crashing PoC).
  • 2018-04-14 - MITRE assigned CVE-2018-10115 to the bug (for 7-Zip).
  • 2018-04-15 - Additional report to F-Secure that this was a highly critical vulnerability, and that I had a working code execution exploit for 7-Zip (only an ALSR bypass missing to attack F-Secure products). Proposed a detailed patch to F-Secure, and strongly recommended to roll out a fix without waiting for the upcoming 7-Zip update.
  • 2018-04-30 - 7-Zip 18.05 released, fixing CVE-2018-10115.
  • 2018-05-22 - F-Secure fix release via automatic update channel.
  • 2018-05-23 - Additional report to F-Secure with a full PoC for Remote Code Execution on various F-Secure products.
  • 2018-06-01 - Release of F-Secure advisory.
  • 2018-06-28 - Bug bounty paid.

Thanks & Acknowledgements

I want to thank the F-Secure team for fixing the bug. In addition, I want to thank Kiran Krishnappa for providing me with regular status updates.


  1. See CPP/7zip/Compress/Rar1Decoder.cpp. ↩︎

  2. As always, it depends on which side of the fence you are standing on. ↩︎

  3. https://forums.malwarebytes.com/topic/228610-vulnerability-in-7zip-remote-code-execution-and-malwarebytes/. ↩︎

© 2023 | about