769242464da960eff6d520fc09fb6b7d.png

This report was written for a job interview (spoiler alert: I didn't get it) and has been sitting in my notes for half a year. windows-sandbox-init was used to create this report.

Executive Summary

I confirmed the file is malicious. It functions as a downloader designed to fetch and execute a second-stage payload on the victim's machine. The binary is a 64-bit Windows executable compiled with Visual C++ and stands out because it links the libcurl library for its network operations. The malware attempts to evade automated analysis by sleeping for extended periods, effectively timing out sandboxes before it performs any malicious activity. Once active, it secures persistence by creating a scheduled task that runs with elevated privileges every time a user logs in. The network logic is robust, utilising a list of fallback URLs and even DNS over HTTPS to locate the C2 server if direct connections fail.

Classification and Attribution

We can classify this sample as a Downloader or Loader. It does not appear to contain the final payload itself but serves as the initial foothold to retrieve it from external servers. The specific use of libcurl and the custom obfuscation techniques, such as the stack-based string construction and the specific sleep loops, suggest it is likely a custom tool rather than a generic commodity malware found in mass campaigns. While I haven't linked it to a specific known threat actor based on these artifacts alone, the techniques imply a moderate level of sophistication intended to bypass standard perimeter defenses.

Prevention and Detection

Defenders can detect this threat by monitoring for specific host and network behaviors. The creation of a scheduled task requesting highest privileges is a significant indicator. On the network side, the traffic pattern involving a HEAD request followed by a hardcoded sleep interval and then a GET request provides a strong signature for intrusion detection systems. Blocking the identified C2 domains and restricting DNS over HTTPS traffic to unauthorized public resolvers like Google or Cloudflare would disrupt its communication channel. Additionally, endpoint protection systems should flag the creation of new executable files in protected directories like System32 by non-system processes.

Initial Assessment

File Inspection Using DIE

File Type and Imports

This file is a PE64 executable built by VC++, interestingly, it links libcurl.

2391820cba06ca993f7887b28972057d.png

Looking at the imported functions, libcurl is probably used for HTTP requests with a C2 server.

9d44304c84cf40d02942de02086f534c.png

It also imports some Windows service operation functions.

9fcba844d5b760015b41210b9e97234e.png

Strings

Some interesting strings can be found. They probably are used for error messages, and there is a file download error message implying that there might be code using libcurl to download files, typical C2 behaviour.

http:// is found but not any URLs, meaning that URLs are constructed during run time.

Some regex patterns are found, they might be used to parse C2 responses.

And JSON related strings indicates the existence of a JSON parser, it might parse JSON responses from the C2.

32235567c0b912bdf8301cbecbe0d654.png

More interesting strings are located, they appear to be used for runtime string construction, likely using some strfmt functions. And {}Windows\System32\backup_f64.exe is something that can't be ignored, the malware might use this path to save its downloaded file.

schtasks /create /tn "{}" /sc ONLOGON /tr "{}" /rl HIGHEST /f creates a scheduled task with highest privilieges.

754aa844a71ef3f79c6b79acd67a27d8.png

The final XML string essentially asks the user to give it elevated privileges, so the malware would run with administrator privileges. This allows the task creation to succeed.

a45a232e3fad1b9bb029501607cca9c3.png

Further Investigation

Looking at the disassembly in Ghidra, and considering the strings we saw, this binary is not packed, all functions are clearly visible.

The next step is to find the main function of this malware. Considering what we saw in DIE, it is very likely to have a main function that constructs the C2 URL, and requests it, saves it to disk, create scheduled task for persistence.

It does have a main function invoked by entry point, however, it has nothing to do with the C2 logic we assumed, instead, it seems to be a winmain function.

809b1685a3393531d88a719d760225ac.png

The main function calls some other functions. c2_main_persistence_tasksch has many of the interesting strings, and complex logic which calls more functions, and it's the first real function it calls. Let's make this our starting point.

Live Analysis

Anti-Debugging

To make my life easier, I rebased Ghidra using the memory map in x64dbg. Then I set a breakpoint at c2_main_persistence_tasksch.

We want to know what it does with schtasks /create /tn "{}" /sc ONLOGON /tr "{}" /rl HIGHEST /f, so we stop at the relevant function strfmt. We are going to inspect its return values so the breakpoint will be before its ret.

b20af09472a784b3282f7cd95f234e39.png

However, the process freezes. Looking at the call stack, we see why.

44aa3ea39823c7d1f92c39a22a7dbad7.png

Unsurprisingly, this malware tries to sleep to make it impossible to be analysed.

8b57059167c89e7ceba1ec461ed9f68c.png

anti_dbg_sleep ensures that the sleep is executed by trap the execution in its loop, it validates actual sleep time, if not met, it will enforce the sleep. This defeats many simple sandboxes that patch Sleep call.

void anti_dbg_sleep(longlong *psleep_interval)

{
  char cVar1;
  ulonglong uVar2;
  longlong time_now;
  longlong sleep_time;

  while( true ) {
    get_time(&time_now);
    sleep_time = *psleep_interval;
    if (sleep_time == time_now) {
      return;
    }
    cVar1 = -1;
    if (time_now <= sleep_time) {
      cVar1 = '\x01';
    }
    if (cVar1 < '\x01') break;
    sleep_time = sleep_time - time_now;
    if (sleep_time == 86400000000000) {
      Sleep(86400000);
    }
    else {
                    /* sleep time less than 24h */
      if (sleep_time < 86400000000000) {
        uVar2 = sleep_time / 1000000;
        if ((longlong)(uVar2 * 1000000) < sleep_time) {
          uVar2 = (ulonglong)((int)uVar2 + 1);
        }
        Sleep((DWORD)uVar2);
      }
      else {
        Sleep(86400000);
      }
    }
  }
  return;
}

Simply patching the function with ret solves this problem.

769242464da960eff6d520fc09fb6b7d.png

Scheduled Task Persistence

We can see the command string being constructed in strfmt, the final result is schtasks /create /tn \"downloader\" /sc ONLOGON /tr \"C:\\Users\\WDAGUtilityAccount\\Desktop\\downloader.exe\" /rl HIGHEST /f". So it adds itself as a task that runs at logon with admin.

4fe6327de5fe3437e5f7f23dd58b5e40.png

3d8f4aab5e9448fed1451dc9c1228332.png

6755ee159143c89429c6062e966a83b6.png

Checking existing malware process

It scans System32 directory for all file names, then tries to match with ^x\d{6}\.dat$ regex pattern.

24d6cc832ead9050f14d9bbba020266c.png

Then it skips the service query code.

7c02cd6bc7a77a31b62de736abb9afc7.png

The service query code essentially uses the matched file name as service name, and verify its status, if running, it will skip C2 logic and exit.

fc74db5a60f12e7a79eccd8d108dd3e1.png

This is highly likely a mechanism to prevent running duplicated instances of the malware.

C2 Breakdown

C2 Orchestrator

The malware skips to the next function, which contains all its major code and calls many more functions, we call it c2_orchestrator_loop.

It has many lines of code, and some useful strings such as {}Windows\\System32\\backup_f64.exe, {}Windows\\System32\\czero_log, it also invokes std::basic_ofstream<>::vftable to open the "log" file.

What about backup_f64.exe? We can see start \"\" \"{}\" in the code, before that it has to download the payload for it to be started. The function responsible for that is likely the complex one right before it.

9a0d093f210ac2ad3db04b5f68b3f141.png

For now, we assume network_orchestrator downloads a file and save it to \\Windows\\System32\\backup_f64.exe then runs it.

In the debugger we set breakpoints at each call to observe what's going on before each function call, hopefully figuring out their purposes.

C2 URLs

And we have an interesting one, it returns a URL that we haven't seen anywhere before, it must have been constructed by that function, we call it make_url.

fced9ab07d6647a1ba9286f12e296446.png

More interestingly, make_url has a string decrypt_data error: {}. This indicates that the URL data is encrypted then decrypted dynamically.

6667c802b98e89cc765b61aa54f1c45d.png

We don't care how it decrypts, since it already got us one URL, and there are more calls to it, let's note down all URLs.

http://unvdwx.com/un1/uhard.dat
https://raw.githubusercontent.com/rootunvbot/mydata/refs/heads/main/mainuhard.dat
https://github.com/unvdwl/dwl/raw/main/mainuhard.dat
http://rootunvdwl.com/un1/uhard.dat

4 URLs are found, this looks like a list of C2 URLs.

Before network_orchestrator, the list is accessible from the stack.

ab1433b32c04f7d9fc44d26d1ae9a078.png

network_orchestrator will loop through the list trying to connect to its C2. In fact, look at its function parameters as it gets executed for the first time.

92400aee5f2f87de4798eecf7f00df71.png

CURL HTTP Worker and C2 Protocol

network_orchestrator initialises libcurl handle, then one function uses the handle to do its stuff, we call it c2_http_worker.

b0f17729567836e09ff4e3a4326dc686.png

Let's run it through, and we get a string returned.

86a853f040b0d3fea7e827232d03e974.png

In FakeNet, it shows a HEAD request.

73d421aa7dd9aa740273b8f3df9acee4.png 8e5eba5bb4830914411f2fd56d615813.png

In Ghidra we see it tries to match some string values.

8363200d934037dd515a7f8fd798ed3b.png

Unsurprisingly, it's trying to validate its C2 server by checking the HTTP response headers.

It looks for application/octet-stream in returned HTTP headers, in this case not present.

Another line of code iVar4 = memcmp(pcVar14,"text/plain; charset=utf-8",0x19); looks for a text based response.

995097210c30dde5a0afdd7680cf2c5a.png

Trigger the C2 Logic to Run the Payload

Let's write a FakeNet rule to satisfy the malware and see what it will do with the C2 response.

def HandleHttp(req, method, post_data=None):
    req.send_response(200)
    req.send_header('Content-Type', 'application/octet-stream')
    req.end_headers()

    # Read a valid EXE and send it as the body.
    try:
        with open('defaultFiles/FakeNetMini.exe', 'rb') as f:
            req.wfile.write(f.read())
    except:
        # Fallback dummy binary (Will save but likely crash/do nothing)
        req.wfile.write(b"MZ" + b"\x90" * 1024)

This script tries to trigger its text based workflow, FakeNetMini.exe is supposed to be spawned if our hypothesis is correct.

As shown in the screenshots, the malware is tricked into downloading our dummy executable and executing it.

It validates Content-Type.

370469f3a339d06425c555b32ae6645e.png

And it tries to write the file to the pre-defined path C:\\Windows\\System32\\backup_f64.exe.

5b0e223e457563163821e1a1803bcdee.png

Constructs the path string.

88a36e02addbbd0bd3e780684100aeb0.png

And CreateProcessW, run the downloaded executable in System32 directory.

c1ace272f2bf64503bc13cb33f5f9635.png

The executable is executed.

1f59a2ebf1c9989dfc2bfcc333c05944.png

Finally it creates a file czero_log in the same System32 directory.

33fcd9009683b4df36984c9142b32fa8.png

Text-based C2 Response

From the test, this malware saves whatever it reads from C2 to backup_f64.exe and runs it, regardless of Content-Type.

def HandleHttp(req, method, post_data=None):
    req.send_response(200)
    req.send_header('Content-Type', 'text/plain; charset=utf-8')
    response_body = "Answer: cmd.exe"
    req.send_header('Content-Length', len(response_body))
    req.end_headers()
    req.wfile.write(response_body.encode('utf-8'))

After the malware finishes its lifecycle, it saves and runs this (text) file as an executable.

df8bce819d6e2f66babfc40c9aa8348c.png

Fallback Resilience

What if the malware can't reach its C2?

3a56ab672b35554aafbea8cc43c5ef61.png

As anti_dbg_sleep functions is called every time before anything important happens, we can just set a breakpoint there and observe the registers and stack.

We can observe all the hotspots in this malware by stopping at its supposedly anti-debugging function since it's so eager to prevent us from analysing whatever that's around that function.

And a DNS-over-HTTPS server address is constructed, it's not hard to guess its use.

fe3a293b27cd6ef5ad8a74bcc36b58db.png

Follow the call stack, the DoH function is found, we call it resolve_via_doh.

58637b4542dfa5e1916450d82d5c34e2.png

As we can observe from later on, it tries to resolve all C2 names using multiple DoH servers.

https://dns.google/resolve?name=
https://dns.nextdns.io/resolve?name=
https://doh.dns.sb/dns-query?name=

Notably, it also tries to resolve a backup name https://dns.google/resolve?name=rootunvdwl.com with the same DoH logic.

22c6afe22d823e45e8f7bea92f0fb08e.png


Comments

comments powered by Disqus