
Vulnerability Research
EnkiWhiteHat
2025. 7. 21.
Prologue
There was a FullChain challenge (RCE, LPE and SBX) in Codegate 2025 Final
.
While brainstorming ideas for Codegate challenges, we came up with the idea of creating RCE
, SBX
, and LPE
challenges and offering bonus points (FullChain
) to teams that solved all of them. That's how the FullChain
series came to be.
This series consisted of 4 problems in total: the 3 individual challenges and a FullChain bonus challenge that could be solved by simply chaining them together after solving all three (RCE
, LPE
, SBX
).
Before we begin, a shout-out to the challenge solvers 🙂
RCE: BunkyoWesterns (First Blood!), The Duck, Blue Water
SBX: None 😞
LPE: GYG (First Blood!), BunkyoWesterns, CyGoN, Blue Water
Also, shout-out to our challenge authors 🙂 !!
Hyungil Moon (@mhibio-ptw), Jongseong Kim (@nevul37) and Dongjun Kim (@smlijun)
Here is the full challenges and exploit codes [link]
Part 1: RCE

Challenge Overview
The goal of the challenge is to find vulnerabilities in the renderer process and develop an exploit code by analyzing the provided rce-sbx-138-0-7204-97.patch
file.
The patch file creates a new Blink module called minishell
in the renderer. It provides various shell functions and file writing and saving, and the file data is managed through Codegate File System (CFS)
, which is a browser API.
The available commands are:
They are similar to the basic shell commands. Commands such as exec
are not implemented, but there are several file operations.
When a file is opened, it is managed through file_descriptor_
in the form of a FileBuffer
class until Save
.
A user can invoke the minishell as follows:
Callable methods can be bound in the *idl
file.
In short, one user can have multiple shells and execute each command in one shell.
Vulnerability
We can see the main functionality in mini_shell.cc
.
However, the vulnerability is pretty simple compared to the file size. The following shows the FileBuffer
structure.
In here, we can see the fixed-size buffer. Let’s check the part which uses it.
There is a size check for the input data vector, but there is no any bound check for idx_
so an out-of-bounds (OOB) read/write
occurs.
Although the vulnerability is simple, we need to obtain arbitrary address read / write
primitives with this relative address read / write
, and finally achieve Arbitrary Code Execution
.
Exploit - AAR/W
Now, we have relative read / write
primitive of uint64_t
size. In fact, there is no difference in the method to achieve arbitrary address read / write
.
However, in order to access an arbitrary address, we must know the address of the current object. This is because we need to measure the distance to move to the target.
There are various ways to leak the address of a controllable object.
In this challenge, it is difficult to achieve address leakage with just a simple OOB read because there is no valid address area written anywhere in the heap area. Among them, we tried using brand new technique that can stably leak objects by utilizing the characteristics of Oilpan GC.
Oilpan GC
The Heap object of Oilpan GC has the following structure [link].
Oilpan GC uses a different allocation method than PartitionAlloc (PA), which is mark-and-sweep
and space
. Unlike PA, which uses slot-bucket
, Oilpan allocates space
for the heap and divides (i.e., allocates) the heap object as much as requested size from the space
when a request comes in.
In other words, without a fixed slot, it dynamically allocates multiple sizes in each space.
When they lose their reference and are GC reclaims them, they take the form of FreeList::Entry
.
When an object in the space
is freed, the HeapObject changes to a FreeList::Entry
, and additional next_
fields are created to point to the next freed object.
Leak Idea
The idea is as follows:
Loop the action below enough times to allocate new space
Spray shell object
Spray File in each shell
Trigger gc()
Read the
next_
of the header of the next adjacent chunk ofFileBuffer
in theN-th
Sprayed ObjectLeak
(N-1)th
Sprayed_object
Since each shell has only one File Buffer
, N shells are needed to spray N File Buffers
.
Considering the characteristics of the Oilpan GC described above, consider the following chunk situation.

Currently, there is only my object in space. When an object is dynamically divided(i.e., allocated) from space, gc() is executed and the small areas between each object will be treated as Free Entry
, forming a FreeList as shown above.
We can now read the chained Free Entry
by reading temp = sizeof(FileBuffer) + 0x8
from the Sprayed2 object, and leak the Sprayed 1 address through heap_leak = temp - sizeof(FileBuffer)
This allows us to leak the address of the object with only spray and out-of-bounds, regardless of how big the distance is between Sprayed 1 and Sprayed 2 whether there is a stable address.
Since we have the address of the Sprayed 1 object and the relative address read / write, we can perform arbitrary address read / write.
In the exploit, after sufficient spray, it triggers gc() and then leaks objects 90th to 89th.
Exploit - Arbitrary Code Execution
Now, we obtain the arbitrary address read / write primitives.
In a typical V8 engine, addrof
is used to obtain address of a Wasm RWX Page
. However, we only have OOB, and it seems difficult to create an addrof
primitive.
So what should we do?
Overwrite the vtable of HeapMojoRemote
to call 0x4141414141414141
?
The challenge says that it should be exploited on chrome.exe
running on Windows 11 24H2
. That is, in order to achieve arbitrary function calls in the challenge, CFG Bypass must be accompanied. Of course, considering the huge size of the code base, there may be many gadgets that can bypass CFG.
Also, Function::Invoker Chaining
, a well-known technique, can bypass CFG.
We wanted to find a more stable method, and after auditing the code, we found that there is a LazyInstance Getter
for WasmCodePointerObject
. We can leak Wasm RWX Page
by reading WasmCodePointerTable → entrypoint_
.
Let's overwrite RWX Page
with arbitrary shellcode and execute wasm exports function.
In the end, we can stably execute arbitrary shellcode while maintaining persistence. An interesting fact is that the bug of the SBX challenge can be triggered even in the Renderer. However, triggering the vulnerability requires a slight race condition in the SBX Challenge, we are unsure whether UAF Object can be reliably occupied in Blink.
Part 2 : SBX

Challenge Overview
This challenge was inspired by a real-world case. So, SBX
seems to be the most difficult challenge in the FullChain
series.
TL;DR
Triggering a vulnerability to create a
UAF
Occupying UAF Object through
Race
andSpray
Create arbitrary read / write / call primitives
The goal of the challenge is to find vulnerabilities in the browser process and develop sandbox escape exploit code by analyzing the provided rce-sbx-138-0-7204-97.patch file
.
This patch file creates a new Mojo Endpoint Impl called Codegate File System
in the browser.
CFS
implements DirectoryImpl
and FileImpl
. These are structured as a tree with the Root Directory as the root under the File System Manager
. Files can execute Read
/ Write
/ Edit
/ Close
operations, and Directories have operations such as Create
/ Delete
/ Change
, etc
.
Below is the cfs.mojom
file defining the mojom interface.
Vulnerability
The vulnerability in the ChangeItemLocation
function is simple in nature but complex in its exploitation. Inaccurate input validation for filename_src
/ filename_dst
causes smart pointer malfunctions, leading to UAF.
The ChangeItemLocation
function checks the source and destination through ValidateChangeLocation [1]
returns the destination_directory. It then removes the src item from the current directory [2]
, and performs AddItemInternal[3]
to the directory to move
Since it's still difficult to find vulnerabilities, let's look at the ValidateChangeLocation
function further.
From this, we can identify several things.
The src and dst files must always exist.
The src file cannot be the current or parent directory.
The dst location can be the parent or current directory.
If it's a filename, it checks if the file is a directory and returns a pointer.
It seems to perform all checks well, but one thing is missing.
There is no check for when src and dst items are the same.
In other words, in the following situation [1]
, the next call [2]
becomes a valid call.
To connect this to UAF, let's go back to ChangeItemLocation
.
Now that we can set src and dst files to be the same, at the moment just before [3]
executes, file_to_move
and destination_directory
will point the same object.
If we can release file_to_move
in this situation, we can make destination_directory
dangling and use it as UAF
.
To release file_to_move
, we need to look at the AddItemInternal
function.
At the destination_directory
, the ownership of file_to_move
is passed to AddItemInternal
with std::move()[4]
to allow destination_directory→item_list
to be bound with ownership.
However, if the new_item
that came in with ownership as an argument isn't bound anywhere and the function ends, the reference will be 0 and be released at the end of the function.
The AddItemInternal
function checks if a file with the same name already exists in the destination directory, and if so, returns false.
This is an appropriate action for the release scenario described above.
Trigger
Now, let's call ChangeItemLocation
again, considering the following situation:
file_to_move(src)
and destination_directory(dst)
are pointing to /root/dir1
.
AddItemInternal
is triggered and checks for duplicate filenames in ./root/dir1
.
Since there is a duplicate filename ./root/dir1/dir1
, the function will return false and finish.
At this point, file_to_move
loses its reference and is freed.
Since file_to_move
has been released, destination_directory
has also been freed.
Because AddItemInternal
returned false, PostTask
will be executed. It then passes the freed destination_directory
as an argument to RecoverItem
, causing UAF to occur when RecoverItem
is executed.
Heap Spray
The first step of the exploit is to occupy the freed object.
There are various heap spray techniques in real browser exploitation, but since this is a "CTF Challenge" , the primitive may exists that challengers could easily access.
CodegateFileImpl
is the heap spray primitive provided by the challenge.
CodegateFile→Write()
stores the incoming array data as std::vector<uint8_t>
.
This means we can create unlimited controllable Heap Objects of any desired size.
Let's try to occupy the UAF object using this.
Occupy
To occupy, we need to spray object before RecoverItem
, which is posted to ThreadRunner, is executed.
However, the journey to achieve this is a bit complicated.
Since the Challenge is a Release version, many Code Snippets are excluded, and the speed is extremely fast. This means that the race window between the end of
ChangeItemLocation
and the execution of the posted RecoverItem is extremely short.Finding an Object of exactly the same size that can be Sprayed from another thread? This can be very helpful when the Race Window is short, but finding a Sprayable Object in time can be difficult, and more effort may be required to achieve
Thread cache
bypass.
Fortunately, there are no restrictions on interface calls and the creation of Directories and Files, so let's try to occupy by satisfying the given conditions.
When a Mojo IPC Call
comes in, the IO Thread
receives it, and after processes like Impl identification and Input Validation, it PostTasks the appropriate function to each Impl ThreadRunner.
All Codegate***
IPC calls will be executed on the same thread, and it's impossible to overwrite UAF Object with CodegateFile
while ChangeItemLocation
is running.
So, let's try to buy some time between ChangeItemLocation
and RecoverItem
.

We have called the normally functioning ChangeItemLocation
function multiple times and then we called ChangeItemLocation, which can trigger UAF.
The above is the queue of the SequenceTaskRunner
. It shows the tasks posted so far waiting for execution.
This way, before the UAF Trigger
is executed, we can send additional Mojo IPC call for as long as ChangeItemLocation
runs.
If we push in CodegateFile::Write
N times during this time,

The running TaskRunner will take the form above, and if time passes and UAF
is triggered

It will look like the above. Now the Write Spray work begins, and if N was sufficient, we will eventually be able to occupy the Freed Object.

We can repeat this until successfully overwriting.
In the exploit, we can improve stability by comparing the file name, vector, etc., to check if it has been successfully overwritten.
Leak
After overwriting the UAF Object
, we need a controllable area and address to avoid crashes and control the flow. There are various techniques to achieve this, but in this challenge, we'll use std::vector<>
to allocate our objects at a Known Address
.
(With a few attempts, you will see that it is s impossible to receive a leak through Mojo Response.)
When std::vector<>
reaches full capacity, it releases the existing heap, allocates a new heap with increased capacity, and moves the existing data.
Using CodegateFile→Read()
, we can leak UAF Object->CodegateItem→itemname_
. Additionally, using CodegateFile→Edit(), we can manipulate UAF Object→vector{start, last, end}
The next step is to create & leak a Known Address of size 0x800, then occupy that area.
UAF object parents→Rename(uaf_object, "A" * 0x800)
The
std::string
will be allocated in a heap slot of size0x800
.This
0x800
size area will be used later for Fake Object, ROP, Temp Memory, etc.
UAF Object→Read()
to leakUAF Object→itemname_
.Modify the
UAF Object Vector
as follows:UAF Object→Vector→start
=heapleak
UAF Object→Vector→last
=heapleak
UAF Object→Vector→end
=heapleak + 0x800
Perform
UAF Object→CreateItem
0x800 / 8
times.The vector will point to
std::string
, and the 0x800 size will be filled.
Perform
UAF Object→CreateItem
one more time.Due to insufficient capacity, the existing area (
std::string
) is released, and the existing data is moved to the new vector space.
Perform
Spray(0x800, spray_cnt)
.We can now occupy the freed 0x800, and this address becomes the one leaked in steps 1 and 2.
In this way, we can achieve Known Address creation, leaking, and occupation, and now we can make Fake Objects
, ROP Chains
, etc. in that area.
Simply using rename() itself for Spray is not suitable because the JS → Mojo → Impl encoding conversion process doesn't properly recognize Null Characters and characters in the UTF-8
range, which is why we need to use the method above. The same goes for Leak
.
In exploit, we can use specific fields(here, std::string item_name_
) as success identifiers.
AAR/W Primitive
CodegateFileImpl
has the same structure as CodegateDirectoryImpl
, but the std::vector
type is uint8_t
.
By creating a Fake CodegateFileImpl
, adding it to the UAF Object
, and manipulating the m_first
, m_last
, and m_end
of the Fake CodegateFileImpl
, we can achieve arbitrary address read / write
.

Here is simple Exploit POC for AAR/W.
Arbitrary Call Primitive
By setting the UAF Object→directory_vtable
to a controllable heap object and manipulating only CodegateDirectory→ListItems
, we can achieve arbitrary function calls.
Like the Renderer, the Browser Process also has CFG Mitigation enabled
.
This time, we use the well-known technique of Didwrite → Function Invoker Chain
to achieve CFG Bypass and arbitrary function calls.
Since we've manipulated the vtable of the UAF Object
and achieved arbitrary function calls, rcx(this)
currently points to the UAF Object
.
The variable ptr[1]
will be the value of UAF Object + 0x10
, and [2]
allows us to make a new function call (ptr+8)
.
We maintain the RefCount
at ptr+0x0
as 1 (gadget constraint), and set ptr+0x8
with useful Gadget from Function Invoker
.
The gadget above looks good for our situation.
At the point when the Function Invoker is executed, rcx can be manipulated to an Arbitrary Address, so we can set it to our obtained Heap Leak, allowing us to configure the function and argv as desired.

Arbitrary Call achieved!
Part 3 LPE

The files provided in the challenge are minimal. Aside from the pre-distributed Windows 11 image, only the PoW (Proof-of-Work) code and the MemoryStorage.sys
file were given. We should analyze the MemoryStorage.sys
driver and use it to achieve privilege escalation on Windows.
Challenge Overview
MemoryStorage.sys
is a Windows Kernel Driver, developed using the legacy Windows Driver Model (WDM). When examining the driver's DriverEntry
function, it contains only two functional components: one function responsible for initializing the kernel stack cookie and another function that handles the main routine of the driver.
In the sub_14000173C
function, the driver creates a symbolic link named \\\\DosDevices\\\\MemoryStorage
and registers the device with the kernel. This symbolic link allows user-mode applications to communicate with the driver by sending requests.
Inside the conditional block, the driver registers its dispatch routines. The entries a1->MajorFunction[0]
and a1->MajorFunction[2]
correspond to the IRP_MJ_CREATE
and IRP_MJ_CLOSE
routines, which are called when a device handle is opened or closed. These routines are not essential for solving the challenge, so they are mentioned only for completeness.
The most important part is the assignment to a1->MajorFunction[14]
, which registers the sub_140001370
function as the handler for IRP_MJ_DEVICE_CONTROL
. This routine is responsible for handling various commands based on I/O Control Codes (IOCTLs), and it plays a central role in the vulnerability analysis.
Let’s now take a closer look at the sub_140001370
function. This function acts as a handler that processes commands based on four specific I/O Control Codes.
Before examining the behavior of each code, it is helpful to briefly review the structure of an IRP (I/O Request Packet), which is used when sending requests to a kernel driver. The IRP is a fundamental data structure in Windows used to represent I/O operations, and it carries information about the requested operation, the involved device, and associated buffers. Understanding how IRPs work is important for analyzing how the driver handles user-mode requests.
IRP Structure
An IRP (I/O Request Packet) is a kernel-level structure used to handle I/O requests between the operating system and device drivers. It follows a specific internal layout, and one of its important fields is CurrentStackLocation
, which points to an IO_STACK_LOCATION
structure. This structure helps each layer in the driver stack process the IRP appropriately.
Every I/O request is handled according to the information stored in the IRP and its associated IO_STACK_LOCATION
structure. These fields allow the driver to process each request in a way that fits its purpose.

For example, the sub_140001370
function processes requests based on the I/O Control Code. In this case, the MajorFunction
field in the IO_STACK_LOCATION
structure has the value 14, which corresponds to IRP_MJ_DEVICE_CONTROL
. When the IoControlCode
field matches a specific value, the driver executes the function that implements the behavior for that particular I/O request.

The values shown in the sub_140001370
function all come from fields within the IRP structure. Since the IRP contains many fields, it is not practical to explain all of them here. For a full reference, please refer to the official documentation at here.
For the purpose of solving this challenge, we will focus only on the Type3InputBuffer
field located inside IRP->CurrentStackLocation
. This field points to the user-provided input buffer, which the driver uses to receive data from user space.
Type3InputBuffer
is used as the input buffer when a request is sent using the Neither I/O method. This leads us to another important concept: what exactly is the Neither I/O method?
Neither I/O
According to the official documentation [link], the Neither I/O method does not provide a SystemBuffer
(a kernel-mode buffer) or an MDL (Memory Descriptor List) when accessing the input or output buffer. Instead, it uses the user-mode virtual address directly. This means that in I/O requests using this method, the data provided by the user remains in user space and is not copied into kernel memory.
In this context, the input buffer is handled through the Type3InputBuffer
field within the IO_STACK_LOCATION
structure, while the output buffer is accessed via the UserBuffer
field in the IRP
structure. These pointers reference memory located in user space, and it becomes the driver's responsibility to validate and safely use them.

Identifying whether an I/O Control Routine uses the Neither I/O method is straightforward. If the I/O Control Code, when divided by 4, leaves a remainder of 3, it indicates that the routine uses the Neither I/O method.
In simpler terms, if the last half-byte (nibble) of the I/O Control Code is one of the following values: 3, 7, B, or F, then the corresponding routine is using Neither I/O.
Keep Analyzing sub_140001370
Let’s now continue analyzing the sub_140001370
function. As explained earlier, we can determine that the I/O Control Codes 0x7101003
and 0x7101007
use the Neither I/O method. This means that within the routines handling these codes, the driver uses the Type3InputBuffer
field as the input buffer.
Since Neither I/O directly passes a user-mode virtual address to the driver, the buffer referenced by Type3InputBuffer
resides in user space, not in kernel memory. Therefore, any data access using this pointer happens in the user’s address space unless the driver explicitly validates or copies it into a safe region.
Root Cause Analysis: Stack-based buffer overflow
Let’s analyze the function sub_140001274
, which handles the I/O Control Code 0x7101003
. This function copies data from a user-supplied memory region into a local kernel buffer, named Dst
, by following five main steps:
The function first checks whether
Type3InputBuffer
is a valid user-mode pointer and whether its size is at least0x10
bytes.It then examines the first two bytes of
Type3InputBuffer
and checks whether the value is greater than0x40
and not equal to zero.The function reads an 8-byte value from the address at an 8-byte offset within
Type3InputBuffer
. This value, which represents another pointer, is stored in the local variablev4
.Using
ProbeForRead
, it validates whether the address stored inv4
is a readable user-mode address.If all the above checks pass, the function copies a number of bytes equal to the value read in step 2, from the memory pointed to by
v4
into the local variableDst
.
Through this process, the driver attempts to read and copy user-supplied data, but as we will see later, this logic can be exploited if the validations are incomplete or misused.
At first glance, it appears that all user-supplied addresses and values are properly validated, making the function seem secure. However, remember that Type3InputBuffer
points to user-mode memory.
The flaw in this code arises in step [2]. In this step, the function reads the first two bytes of Type3InputBuffer
and checks whether the value falls within a valid range. But in step [5], when calling memcpy
, it does not use the previously stored v3
value. Instead, it reads from Type3InputBuffer
again, causing a double fetch.
This allows the user to trigger a race condition and copy more than 0x40
bytes into the local variable Dst
.
However, in order to exploit this stack-based buffer overflow vulnerability, leaking a kernel address is essential. So where does the vulnerability that allows kernel address leakage exist?
Root Cause Analysis: Information Disclosure
Let’s analyze the function sub_14000113C
, which handles the I/O Control Code 0x7101007
. This routine also uses the Neither I/O method. The function can be broken down into the following four steps:
It first checks whether
Type3InputBuffer
is notNULL
and whether the input buffer length is at least 8 bytes. It then uses theProbeForRead
function to verify thatType3InputBuffer
is a valid user-mode address.It checks whether the first 2-byte value of
Type3InputBuffer
is non-zero and less than or equal to0x40
.It allocates a memory pool of size
0x10000
bytes.It copies the contents of the local variable
Dst
into the allocated pool, using the size specified in the first 2 bytes ofType3InputBuffer
.The allocated pool is then stored in the global array
qword_140003080
.
This function, like the previous one, is also vulnerable to a double fetch between steps [2] and [4]. Because of this, more than 0x40
bytes of the Dst
variable can be copied into the pool. The question is: where can this copied data be read from?
The answer lies in the function sub_1400014CC
, which handles the I/O Control Code 0x7101010
. This function, named LoggingMemoryInformationForInternalMemoryStorageDriver
, performs the following steps.
First, it creates a Section. Then, it maps 0x10000
bytes of memory to the Section. After that, it copies the contents from the global array qword_140003080
into the mapped Section memory. Finally, it unmaps the Section and closes its handle in Kernel Mode.
This raises the question: How the data inside the Section can be accessed if it is unmapped and closed immediately.
The answer is that on Windows, if a user-mode process opens the Section in shared mode beforehand, the kernel is unable to fully unmap and close the Section. By occupying the Section from user mode before the driver releases it, it becomes possible to access the data stored inside, including leaked kernel addresses.
For reference, some of the leaked kernel addresses are shown below. Since parts of both ntoskrnl
and the kernel driver's addresses have been leaked, there should be no issue in constructing a Stack ROP chain 👍
Exploit
Now that all necessary information has been gathered, we can exploit the stack-based buffer overflow using a ROP chain to obtain SYSTEM privileges. The ROP chain operates in the following sequence:
Before entering the ROP chain, a
cmd
process running with Medium Integrity is created. This process continuously attempts to readC:\\Windows\\System32\\flag.txt
using thecurl
command.Using the PID of the
cmd
process, the ROP chain calls thePsLookupProcessByProcessId
function to retrieve the EPROCESS address of thecmd
process.The same function is called again to obtain the EPROCESS address of the System process. (In Windows, the PID of the System process is always 4.)
The Token value of the
cmd
process is then overwritten with the Token value from the System process.
Once this chain completes, the cmd
process that was initially created will be running with SYSTEM privileges. However, the process is not yet complete. Because the ROP chain was executed in Kernel Context, control must be returned to User Context. Additionally, some time is needed for the cmd
process to read flag.txt
and send the content outward.
Although functions like KiKernelSysretExit
could be used to return to User Context, this particular exploit only required a brief delay to allow the data transmission. To achieve this, a simple \\xEB\\xFE
gadget was used at the end of the ROP chain, which causes an infinite self-jump, effectively stalling the kernel and giving the user-mode process enough time to complete its task.
Part 4: FullChain

RCE to SBX
The existing RCE-SBX chaining method was to change enable_mojo_js
to True
. However, a year ago, Chrome introduced a new Mitigation [link] to prevent exploitation.
Mitigation Detail
The existing method to enable mojo_js
was as follows:
Find the current
RenderFrameImpl
in the chrome binary,Overwrite the member variable
enable_mojo_js
ofRenderFrameImpl
However, the new mitigation does the following:
If ScriptContext is not finished, grant ReadOnly permission to
enable_mojo_js
area viaProtectMemory
.Mojo binding can be enabled only when ScriptContext is finished.
That is, it is impossible to overwrite mojo_js_binding
during Script execution (= during Exploitation ).
Now, there is a difficulty in chaining with the existing method.
Bypass
We can do endless things after achieving arbitrary code execution. For example, “making ReadOnly memory into ReadWrite memory”.
As mentioned earlier, whether enable_mojo_js
is enabled or not is now managed in ExecuteContext
.
The default flags applied to the new ExecuteContext are managed globally with ProtectedMemory
applied.
In other words, if the global enable_mojo_js
default flags managed by (ReadOnly)ProtectedMemory are set to true and a new Script Context is created(Reload), the context will be able to use mojo bindings.
We can use base::AutoWritableMemoryBase::SetMemoryReadWrite
to grant rw permission to protected memory, and base::AutoWritableMemoryBase::SetMemoryRead
to grant readonly permission to protected memory.
If we execute the shellcode that does this and then execute windows.reload, we can normally obtain a context where mojo_js_binding
is activated.
DEMO
Epilogue
Web browsers remain high-value targets consistently exploited in numerous in-the-wild attacks. Despite extensive security mitigations introduced by Chrome over the years, our full-chain exploit demonstrates that bypassing these protections remains feasible. Similarly, although Windows Control Flow Guard (CFG) provides robust protection mechanisms, sophisticated techniques exist to effectively circumvent these defenses.
Creating this CTF challenge series has been a rewarding experience, and we sincerely hope participants enjoyed tackling these challenges as much as we enjoyed developing them. Our goal was not only to provide engaging, technically intricate challenges but also to reflect real-world exploitation scenarios, showcasing the latest trends and bypass techniques employed in actual threat environments.
Moving forward, we are committed to continuing our exploration of emerging exploitation techniques and developing challenges aligned with the latest cybersecurity trends. Our research journey is ongoing, and we have no intention of slowing down. In future posts, we aim to delve deeper into analyzing genuine in-the-wild vulnerabilities, demonstrating real-world bug chaining techniques, and sharing our insights and methodologies with the broader security community.
Thanks to the all participants and readers for your interest and engagement. Stay tuned for more groundbreaking research and practical insights.