[Wipeload Step 1.] One Day, phishing mail comes to me (EN)

Hello, this is OUYA77 from Hackyboiz.

It’s been a while since we last published a research post, hasn’t it? 😅

Behind the scenes, we’ve been busy preparing for a new season of Hackyboiz and taking some time to reorganize the team.

Hackyboiz was originally founded back in 2018, and over the years many of the members who helped run the team moved on for various reasons. Recently, ogu123 stepped into the role of team lead, and we’ve been making a number of changes and improvements together to prepare for the next chapter of the team.

To make things even more interesting, our X (Twitter) account got wiped out somehow (ㅠ.ㅠ), so we didn’t really have a place to share updates. That was pretty frustrating for us.

During this transition, we worked on an internal research project.

And the project’s name was…

image.png

The project was simple:

Build two Chrome full-chain exploits.

Of course, developing a true zero-day full chain was still a bit beyond the scope of this project. We all have our own day jobs, and none of us had much experience with that level of exploitation yet.

Instead, we decided to focus on chaining known 1-day vulnerabilities. As it turned out, it didn’t take nearly as long as we expected. 😄

We originally planned to spend four months on the project, from February through June. Surprisingly, we had already completed our goal by the middle of April. ☠️

Most of February and March were spent studying previous 1-day cases and reproducing existing exploits. Then, throughout April, we focused on building exploit chains and sharing our findings and techniques with one another.

Since we learned a lot along the way, we thought it would be worthwhile to document the entire process as a research series.

And when we say “series,” we mean it — this will be a long one. We’re planning (maybe) ten parts in total!

We hope you’ll enjoy following along with The Way of Primitives(Wipeload) research series. Stay tuned! 😄 (Wipeload stands for “Wipe Payload,” a project aimed at cultivating reliable exploit primitives)

Intro: Have You Heard of the Way?

Background

To briefly explain how this project started, it all began with an interest in V8 type confusion bugs. Last year, I spent quite a bit of time studying V8 RCE exploitation.

Previous research post:

https://hackyboiz.github.io/2025/07/01/OUYA77/Chrome_part1/kr/

Originally, I planned to include some discussion on V8 heap sandbox escapes as a follow-up, but things escalated a little and eventually turned into this project instead. 😅 So if you’ve been waiting for that topic, don’t worry—we’ll be covering it in this series as well.

As I learned more, a question naturally came to mind “Okay, but how does the chaining part actually work?”

Around the same time, I started feeling that recently AI systems have become surprisingly good at developing exploits. If that’s the direction things are heading, perhaps the more valuable skill is not simply finding vulnerabilities, but knowing how to weaponize them effectively.

With that in mind, rather than chasing new zero-days, I wanted to focus on building reliable exploit primitives. So I kidnapped g0ngjae, banda, and ji9umi (figuratively, of course) and convinced them to join a project whose goal was Build two Chrome full-chain exploits.

We originally had even more ambitious chaining ideas in mind and wanted to build something much fancier. Unfortunately, real life tends to get in the way.

So for now, we’ve decided to wrap up the project and document what we learned along the way.

Project Goals

Finding new zero-days was intentionally out of scope for this project. Our goal was to study well-known 1-day vulnerabilities and gain hands-on experience by reproducing and chaining them together.

A typical Chrome exploit chain consists of three major stages (though in some cases, two stages may be enough):

  1. Renderer Compromise
  2. Chrome Sandbox Escape
  3. Windows Local Privilege Escalation (LPE)

We’ll cover each of these stages in much greater detail throughout this series 😇

For the first chain, we combined CVE-2018-17463 (Renderer RCE), presented by Matteo Malvica in Chrome Browser Exploitation: From Zero to Heap Sandbox Escape at NDC Security 2025, with CVE-2019-5826 (Sandbox Escape), presented by Zhen Feng and Gengming Liu in The Most Secure Browser? Pwning Chrome from 2016 to 2019 at Black Hat 2019.

For the Windows privilege escalation stage, we originally planned to use any suitable LPE vulnerability we could get our hands on.

Then banda went ahead and found a zero-day.

Unfortunately, the vulnerability had already been patched by the time we finished our work, so we were unable to submit it through a vulnerability disclosure program. Still, we decided to use it as the final LPE stage in our exploit chain.

For the second chain, we chose a case study that completely blew my mind when I first encountered it…!

image.png

For the second chain, we decided to focus on the Chrome and Windows portions of Theori’s impressive VM-to-LPE-in-Host-OS full-chain research, specifically chaining CVE-2023-3079, CVE-2023-21674, and CVE-2023-29360, while leaving the VM escape component out of scope.

We’d also like to take this opportunity to thank the researchers at Theori for publishing such an excellent write-up.

https://theori.io/blog/chaining-n-days-to-compromise-all-part-1-chrome-renderer-rce

One of the motivations behind this series is that many of the questions I had while learning exploit chaining simply weren’t discussed in much detail online.

So instead of keeping the journey to ourselves, we’ll walk through the entire process—from studying the bugs to building working chains—and share the lessons we learned along the way.

With that said, Let’s dive in!

What Happens When You Click an HTML Attachment from a Phishing Email?

One evening, you’re wrapping up another successful day as a fund manager when a new email arrives in your inbox.

“I know what you did last night.”

A cryptic subject line. A single unsettling sentence.

You can’t think of anything unusual that happened the night before—in fact, you remember doing nothing more exciting than going to bed early.

But the message leaves you with an uncomfortable feeling.

And before long, curiosity gets the better of you.

image.png

Eventually, curiosity wins. You open the HTML attachment.

A loading screen flashes briefly across your monitor. Moments later, all you see is a blank white page. Nothing happens.

“Just another spam email,” you think.

You close the tab, finish up your daily reports, check a few remaining emails, and finally leave the office late that night. As far as you’re concerned, nothing happened. Or so you thought.

he next morning, the moment you arrive at work, something feels wrong.

Phones are ringing nonstop. People are rushing through the office. The expressions on your coworkers’ faces tell you that something has gone terribly wrong.

“Money is missing from our clients’ accounts!”

“We’re seeing massive international wire transfers!”

“According to the logs, the last login came from… your account.”

Your mind goes blank.

You didn’t do anything.

You didn’t install software.

You didn’t enter your credentials.

You didn’t approve any transactions.

All you did was open an HTML attachment from an email.

So what exactly happened when that HTML file was opened in Chrome?

Let’s take a deep dive and follow the attack chain step by step.

Chrome Full-chain Exploit Overview

Chrome remains one of the most widely used web browsers across both desktop and mobile platforms.

ref. [https://martech.zone/browser-market-share/](https://martech.zone/browser-market-share/)

ref. https://martech.zone/browser-market-share/

Because such a large portion of users rely on Chrome every day, vulnerabilities affecting the browser can have a significant impact. Chrome is also an extremely complex piece of software. Modern web pages require the browser to manage large amounts of memory and intricate object graphs while coordinating numerous components, including the rendering engine, JavaScript engine, and networking stack.

As a result, successfully exploiting a Chrome vulnerability can provide attackers with powerful primitives such as arbitrary memory access or even code execution within the browser process. Furthermore, Chrome interacts extensively with the underlying operating system through file access, graphics processing, networking, and other system services. Compromising the browser can therefore become the first step toward compromising the system itself.

In other words, Chrome is popular not only among users, but also among security researchers and attackers alike.

For many traditional desktop applications on Windows, a successful exploit may directly result in arbitrary code execution. Chrome, however, is designed with multiple layers of security in mind. Even after gaining control of a renderer process, an attacker is still confined by additional security boundaries such as the browser sandbox.

Because of these defenses, modern Chrome exploitation typically involves multiple stages rather than a single vulnerability. A full-chain exploit usually follows a progression similar to the one shown below.

image.png

Over the course of this series, we’ll break down each stage of the chain and examine it step by step.

Chain 1: CVE-2018-17463 + CVE-2019-5826 + Windows LPE 1-day

The following video demonstrates Chrome Full-Chain Exploit #1, developed as part of this project.

classic_chrome_chaining.mp4

By simply opening a single HTML file in Chrome, we were able to execute commands with SYSTEM privileges on the host machine.

image.png

This chain combines V8 RCE (CVE-2018-17463), Sandbox Escape (CVE-2019-5826), and a Windows LPE vulnerability developed by our team. The result is a complete exploit chain capable of obtaining SYSTEM privileges on Windows from nothing more than a single HTML file.

We’re admittedly quite proud of this one. 😄

Since portions of the chain are based on publicly available research, we’ll keep the introduction brief for now and save the technical details for the rest of the series. We’ll be walking through every stage of the chain in depth in the upcoming posts.

This exploit chain was developed and tested on Chrome 69.0.3497.100 running inside VMware on Windows 11.

CVE-2018-17463(V8 Type Confusion - Renderer RCE)

Previous Research Recap

As expected, the initial attacker-controlled input comes from HTML, CSS, and JavaScript running inside the renderer process.

While processing this input, a type confusion vulnerability is triggered. By exploiting the resulting object overlap, we can construct relative read/write primitives, commonly referred to as the addrOf and fakeObj primitives. One important detail to keep in mind is that V8 Heap Sandbox had not yet been introduced at the time of this vulnerability. 🫡

Because the bug allows us to overwrite a backing-store pointer, we can leverage two ArrayBuffer objects to build arbitrary address read/write (AAR/W) primitives.

With these primitives in hand, we can leak both the address of a WebAssembly instance object and its RWX jump-table pointer. However, the original addrOf primitive relied on corrupting overlapping property storage, which could inadvertently damage the target object and reduce exploit reliability.

To address this issue, we adopted a more stable approach. We added an out-of-line property to an ArrayBuffer object and configured that property to reference the target object. By reading the appropriate offset within the property store, we were able to leak object pointers without directly corrupting object fields, resulting in a significantly more reliable primitive.

Once a stable AAR/W primitive is available, the remaining steps are relatively straightforward. We create a WebAssembly instance, causing V8 to allocate an RWX jump table. After leaking the address of that jump table, we use our arbitrary-address write primitive to overwrite it with shellcode.

And then, all that’s left to do is invoke the function…

image.png

And sure enough, Calculator pops up. ㅎㅎ

Launching Calculator means we’ve successfully achieved remote code execution (RCE).

A Quick Note on Exploit Primitives

One thing I noticed while learning exploitation is that there isn’t a lot of material that clearly explains what a primitive actually is—or at least there wasn’t when I went looking for it.

My personal interpretation is:

A primitive is a capability that is:

  • Repeatable
  • Controllable
  • Usable as input for the next stage of an exploit

In a sense, you can think of a primitive as an exploit API.

For example, an addrOf primitive returns an object’s address. An AAR primitive reads memory from an arbitrary address. An AAW primitive writes data to an arbitrary address. Each primitive exposes a useful capability that can be composed with others to build a complete exploit chain.

Now, before anyone gets too excited, there’s an important caveat.

The demonstration above was performed with Chrome’s sandbox disabled. In a normal Chrome environment, achieving renderer RCE alone is not enough to spawn a process such as Calculator, because the renderer remains confined by Chrome’s sandbox. 🫠

So now that we’ve obtained RCE inside the renderer, what comes next?

Chrome_SBX.png

The answer is to break out of the prison that Chrome has carefully built around us.

image.png

Let’s move on to the next stage of the chain: Chrome Sandbox Escape.

CVE-2019-5826(UAF in IndexedDB - Sandbox Escape)

We’ll be covering Chrome’s sandbox architecture in much greater detail later in this series, so stay tuned. 😄

One point that’s worth clarifying up front is that V8 Heap Sandbox and the Chrome Sandbox are two completely different concepts. In simple terms, the V8 Heap Sandbox is a memory-isolation mechanism within the renderer process itself, whereas the Chrome Sandbox is a process-isolation mechanism that restricts the privileges of renderer and other untrusted processes. We’ll dive deeper into both topics in future posts.

Once renderer RCE has been achieved, techniques such as PEB walking (using gs:[0x60] to locate structures such as kernel32.dll and APIs like VirtualAlloc) can be used to bypass ASLR. For the remainder of this discussion, we’ll assume that ASLR has already been defeated.

Our next target is CVE-2019-5826, a vulnerability presented at Black Hat USA 2019. The bug affected Chrome’s IndexedDB implementation, where a use-after-free (UAF) condition could be exploited to escape the Chrome sandbox.

Just a taste for now. 😋 (Since this is only Step 1 of the series, we’ll skip the detailed PoC analysis.)

image.png

In Chrome, much of the IndexedDB implementation resides within the browser process. IndexedDB is a built-in NoSQL database that allows web applications to store large amounts of structured data on the client side. When a renderer process interacts with IndexedDB through its API, the actual requests are handled by the browser process.

The vulnerability arose from an interaction between the asynchronous request queue and Chrome’s forced-cleanup logic. Under certain circumstances, these two mechanisms could become out of sync, creating a situation where a database object could still be referenced after it had already been destroyed.

In other words, this was a classic lifetime-management bug that ultimately resulted in a use-after-free (UAF) condition, allowing attackers to interact with an object whose lifetime had already ended.

Step 1. UAF

IndexedDB maintains a queue called pending_requests to manage incoming database operations. Database objects are reference-counted, and their lifetime is ultimately determined by a ref_count-style mechanism.

Under the following sequence of operations, however, a dangling pointer can be created:

  • Open(“db1”, 1);
  • Open(“db1”, 2);
  • DeleteDatabase(“db1”, force_close=True);
  • AbortTransactionsForDatabase();

The root cause lies in the interaction between DeleteDatabase(force_close=true) and the request-processing logic.

During ForceClose(), Chrome iterates over every active IndexedDB connection stored in connections_ and closes them one by one. The interesting behavior occurs when the final connection is closed. As soon as the last connection is removed, the pending Open(v2) request is processed, which initiates a version-upgrade transaction. This operation creates a new upgrade connection and inserts it back into the connections_ array.

The problem is that the original close loop is already in the process of terminating. As a result, the newly inserted connection is never properly cleaned up.

Consequently, the following condition inside Close() no longer evaluates to true:

connections_.empty() && !active_request_

Since this condition fails, the call to factory_->ReleaseDatabase() is skipped. The database entry therefore remains registered inside database_map_, even though the object should eventually be released.

Later, when AbortTransactionsForDatabase() aborts the version-change transaction, the reference count of the corresponding IndexedDBDatabase object drops to zero and the object is destroyed. However, because the cleanup of database_map_ was skipped earlier, the map still contains a pointer to the freed object. In other words, database_map_ now holds a dangling pointer.

Once that stale pointer is accessed again, the exploit gains a classic Use-After-Free (UAF) condition.

Step 2. Spray

To take advantage of the dangling pointer, we perform a heap spray.

IndexedDB provides a function called createObjectStore(), which normally stores a key_path string. However, because we already have renderer RCE, we can bypass Blink’s type checks and send arbitrary bytes through this interface rather than a legitimate string.

This allows us to craft a 0x170-byte payload that mimics the layout of an IndexedDBDatabase object and spray it across the heap. In practice, it didn’t take much effort—after roughly 50 attempts, one of our sprayed objects reliably landed in the freed slot. 🤤

Step 3. Heap Addresss Leak

Once the spray succeeds, the freed 0x170-byte slot is reoccupied by our fake IndexedDBDatabase object.

The key structure layout is shown below:

Offset Field Value Role in Leak
+0x000 vptr 0 Prevent accidental vtable calls
+0x008 ref_count_ 0xFFFFFFFF00000030 Keep the object alive during the leak stage
+0x010 backing_store_ 0 Avoid crashes
+0x018 metadata_.name (string16) 0 (empty SSO string) Prevent crashes during metadata serialization
+0x050 metadata_.object_stores (map) 0 (empty map) Prevent iteration crashes
+0x060 identifier_ 0 Prevent string-copy crashes
+0x0D0 factory_ 0 Null
+0x128 connections_ 0 Empty state
+0x148 active_request_ 0x4142434445464748 Non-null marker used to skip request processing and identify successful sprays
+0x150 pending_requests_.buffer_ 0 Leak target; will later contain an allocated heap address
+0x158/160/168 capacity_ / begin_ / end_ 0 Empty deque
+0x170 processing_pending_requests_ 0

The most important field for this stage is the pending_requests_ deque located at offset +0x150. When constructing the spray payload, we intentionally initialize this deque as completely empty. At the same time, we place a non-null marker value (0x4142434445464748) into active_request_ at +0x148.

These two fields are all we need for the leak.

Next, we send another Open("db1") request through the dangling pointer. Eventually, execution reaches IndexedDBDatabase::AppendRequest(), which calls pending_requests_.push_back(...). At this point, two important things happen.

  1. First, because active_request_ is non-null, AppendRequest() immediately returns after queuing the request if (active_request_) return;. As a result, ProcessRequestQueue() and OpenRequest::Perform() never execute. This prevents Chrome from dereferencing our fake object and crashing. The request simply remains queued. More importantly, that queue operation triggers a heap allocation. Since the deque starts with buffer_ = NULL, capacity_ = 0, the first push_back() forces base::circular_deque to allocate a backing buffer. The address of that newly allocated heap buffer is immediately written into buffer_, which resides at offset +0x150 of our fake object.

    Although a single Open() call is enough to populate the field, we issue fifteen requests in total. As the deque grows, its internal storage expands multiple times. Each reallocation updates buffer_ to point to the newest backing store. After fifteen insertions, the deque ends up with a capacity of sixteen entries, producing a buffer of approximately 16 × 8 = 0x80 bytes. This specific size becomes useful in the next stage of the exploit.

  2. The remaining challenge is reading the pointer back. At first glance, this seems easy: simply read the sprayed key_path data and recover the contents of +0x150. However, there is a subtle issue. If we retrieve the data through the normal Commit()SuccessDatabase path, the pointer disappears.

    The reason is that Commit() serializes metadata into LevelDB, and the callback reconstructs fresh metadata objects from disk. The resulting key_path strings are therefore newly created copies rather than the original sprayed memory.

    To avoid this, we split the process into two phases.

    First, we commit the version-change transaction so that the sprayed buffers remain alive. Next, we issue an additional Open("db_reuse", version=0). This request returns metadata that is still resident in memory rather than reconstructed from disk. The associated key_path objects therefore continue to reference the original sprayed buffer—the same buffer that reoccupied the dangling-pointer slot.

    When the callback receives these object stores, it examines each key_path and checks whether +0x148 == 0x4142434445464748. If the marker matches, we know the spray successfully landed in the target slot.

    The callback then reads the eight bytes located at +0x150, verifies that they resemble a valid heap pointer, and stores the result as leaked_heap_addr_. At this point we have successfully leaked the address of the deque’s backing buffer allocated on the IndexedDB thread heap.

    Rather than blindly guessing heap locations, we now possess a concrete heap anchor that can be used to derive additional heap addresses in subsequent stages of the exploit.

In breif,

  • Open("db1", version=0) × 15
    • pending_requests_.push_back()
    • circular_deque automatically allocates a backing buffer
    • Heap pointer is written to spray + 0x150
  • Commit transaction and re-open db_reuse
    • Receive live metadata through SuccessDatabase
    • Read back key_path
    • Extract the pointer stored at +0x150
  • Heap leak achieved
    • leaked_heap_addr_ acquired

Step 4. AAF(Arbitrary Address Free) Primitive

As discussed earlier, pending_requests is implemented using a circular deque. Because of its design, when the deque needs to expand, it does not simply grow into adjacent memory. Instead, it allocates a new backing buffer, copies the existing contents, and frees the old buffer. This behavior becomes extremely useful when combined with our fake object.

After setting active_request_ to a non-null value, we configure pending_requests as follows:

pending_requests
- buffer_   = leaked_heap_addr_   <- address to be freed
- capacity_ = 1, begin_=end_ = 0  (empty, expansion-ready)

In this state, any operation that forces the deque to expand will eventually free the address stored in buffer_.

In other words, if we can fully control the contents of pending_requests, we gain the ability to free an arbitrary address. This gives us an Arbitrary Address Free (AAF) primitive.

To trigger it, we first reclaim the slot that was previously used during the heap leak stage. Calling DeleteDatabase("db_reuse", force_close=true) causes the corresponding key_path strings to be released, returning the 0x170-byte slot to the free list once again. Whereas Step 3 used this slot for leaking heap addresses, we now reuse it to construct an AAF payload.

We open a new upgrade transaction using Open("db_aaf") and perform another spray using CreateObjectStore(). Eventually, one of the sprayed string16 buffers reoccupies the freshly freed 0x170-byte slot. This time, however, the fake object’s pending_requests deque is intentionally crafted to appear empty while still requiring expansion.

When another Open("db1") request is issued through the dangling pointer, execution once again reaches AppendRequest() -> push_back(). Since the deque has no usable capacity, ExpandCapacityIfNecessary() is invoked. The deque allocates a new backing buffer and frees the previous one.

The crucial observation is that the previous buffer pointer is no longer a legitimate deque allocation—it is our chosen target address(buffer_ = leaked_heap_addr_).

As a result, the expansion logic ultimately performs free(leaked_heap_addr_)!

From the attacker’s perspective, an arbitrary address has now been freed.

Finally, because active_request_ is populated with a non-null value (for example, 0x101), AppendRequest() exits immediately after queuing the request and never proceeds to ProcessRequestQueue(). This prevents the fake request from being processed, avoids exceptions caused by dereferencing invalid objects, and allows the process to continue running normally. At this point, we have successfully constructed a stable AAF primitive: the ability to free a chosen address while keeping the browser process alive.

Step 5-6. Allocating a Fake Factory → Browser RCE

Once we gain the ability to free an arbitrary address, the game is almost over.

The next goal is to hijack control flow by overwriting a virtual function table. Inside Chromium’s browser process lives an object called IndexedDBFactory. This object is responsible for managing IndexedDB databases for each origin. Whenever a renderer invokes APIs such as Open() or DeleteDatabase(), the browser process eventually reaches virtual method calls on this Factory object. This makes the Factory an attractive target.

Using the AAF primitive, we reclaim the Factory object with attacker-controlled data and overwrite its vtable. When one of the corresponding virtual methods is invoked, execution is redirected into our ROP chain.

And just like that, a dangling pointer that originated in the renderer process eventually leads to the execution of WinExec() with the privileges of the browser process—outside the Chrome sandbox. Delicious. 냐미 🍇

image.png

Running the exploit currently takes around 40 seconds to a minute. That’s admittedly longer than we’d like, but the exploit performs multiple sprays and several stages of heap manipulation, so immediate execution isn’t always possible.

Personally, I’d love to spend more time improving the exploit’s reliability and performance someday… but that’s a project for future me. 😅

One additional challenge is that the available ROP space is fairly limited. In our proof-of-concept, we perform a stack pivot and invoke WinExec() to launch calc.exe. However, when chaining into the Windows LPE stage, there isn’t enough room to carry the entire payload directly.

Instead, we packaged the LPE stage into a separate executable (a.exe) and simply launched it through the browser-process ROP chain. It gets the job done, although there are probably cleaner approaches. If you know of a better technique, we’d love to hear about it. Feel free to reach out at hackyboizteam2@gmail.com. 🙇‍♂️

Windows LPE Payload(made by banda)

Hello, this is banda. I’ll be taking over for a moment.

Compared to the V8 exploit and sandbox escape stages, which heavily depended on Chrome internals such as object layouts, Mojo IPC, and browser-process object lifetimes, the kernel LPE stage felt somewhat more independent within the overall chain. Once execution had been achieved at Medium Integrity Level, the kernel stage could be developed and reasoned about largely on its own. (The team members who worked on them did some awesome work… 🐐)

While thinking about what kind of kernel LPE would fit well into the chain, I decided to explore a bug pattern that Hackyboiz had not previously covered in depth. The result was a Windows LPE payload based on an MDL abuse bug pattern.

In this post, I’ll briefly introduce the vulnerability pattern and the resulting exploit primitive using an example that has already been patched and is no longer exploitable. We’ll save the full analysis for the dedicated Kernel LPE article near the end of the Primitive Cultivation series. 😁

A Taste of MDL-Related Vulnerabilities: MDL Unregister Double-Free

At a high level, an MDL (Memory Descriptor List) is a kernel structure used to describe which physical pages back a particular virtual-address buffer.

The bug pattern I encountered was another example of a common theme in MDL vulnerabilities: lifetime management issues. A typical MDL lifecycle consists of allocation, page locking, mapping, unmapping, and finally deallocation. Problems can arise when a driver stores MDL objects in a slot table or queue and allows multiple requests to reference the same MDL pointer. Under those conditions, multiple threads may end up interacting with the same object simultaneously, creating opportunities for lifetime races.

[unregister flow]
input.va
  ↓
find matching slot
  ↓
slot = &a1[6 * idx]
  ↓
slot state clear
  ↓
if slot[16]:
    user_va = slot[15]
    mdl     = a1[2 * (3 * idx + 9)]
    
    MmUnmapLockedPages(user_va, mdl)
    IoFreeMdl(mdl)
else:
    mdl = slot[17]
    if mdl:
        IoFreeMdl(mdl)

Register/unregister designs are particularly prone to this class of issue because multiple threads may end up referencing the same MDL pointer. In the pattern I analyzed, the unregister routine looked up the mapped virtual address and MDL pointer that had previously been stored during registration. The sequence slot lookupslot clearMmUnmapLockedPages()IoFreeMdl() was performed without sufficient synchronization.

As a result, two threads could race through the unregister path and free the same MDL object twice, leading to a double-free condition.

Windows 22H2 kernel driver exploit primitive

After identifying the MDL bug pattern, the next step was turning it into a useful primitive on Windows 22H2.

There are multiple ways such a vulnerability could potentially be leveraged, but for the chain described here, the goal was to build a primitive that allowed a Medium Integrity Level process to interact with a vulnerable kernel driver and ultimately manipulate the current process’s token object.

The high-level process looked like this:

  • Leak the kernel address of the current process’s TOKEN object.
  • Identify the privilege field within that token.
  • Use the MDL vulnerability to obtain a writable user-mode mapping that references the target kernel memory.
  • Modify the token’s privilege bitmap through that mapping.
  • Enable privileges such as SeDebugPrivilege.
  • Access a SYSTEM-owned process and launch a SYSTEM-level shell.

This provided the final building block required for the full chain: a Windows LPE payload capable of elevating privileges after the browser compromise and sandbox escape stages had already succeeded.

When all of the vulnerabilities are chained together, a single malicious webpage or phishing attachment can ultimately lead to the compromise of a Windows system with SYSTEM privileges.

⛰️Introducing Wipeload Series

And with that, you’ve just finished reading the very first installment of our Wipeload series.

We’re currently working on many more articles, so while we may not have a subscribe button, a like button, or a notification bell, we’d greatly appreciate your continued interest and support. 😇

In Step 2, we’ll revisit type confusion vulnerabilities and discuss how traditional AAR/W primitives evolved into Caged AAR/W under modern V8 mitigations. Starting around 2020, V8 introduced the Heap Sandbox, making renderer exploitation significantly more challenging. Great news for defenders. Slightly less exciting for exploit developers. 🫠

After that, we’ll dive into the three-vulnerability chain featured in Theori’s research and use it as a foundation for exploring many of the core concepts behind modern Chrome exploitation. Along the way, we’ll cover not only the vulnerabilities themselves, but also the primitives, mitigation bypasses, browser internals, and exploitation techniques that connect everything together.

So, after reading this entire series, will you fully understand what a Chrome full-chain exploit is?

That’s certainly the goal. We’ll do our best to make it happen.

Thanks for joining us on the journey, and we hope you’ll stick around for the rest of the ride. 🏃‍♂️🏃‍♀️💨

Coming Up Next

image.png

Reference.

https://i.blackhat.com/USA-19/Wednesday/us-19-Feng-The-Most-Secure-Browser-Pwning-Chrome-From-2016-To-2019.pdf

https://blog.kiprey.io/2020/10/CVE-2019-5826/