



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 ofFileBufferin theN-thSprayed ObjectLeak
(N-1)thSprayed_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
UAFOccupying UAF Object through
RaceandSprayCreate 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
ChangeItemLocationand 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 cachebypass.
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::stringwill be allocated in a heap slot of size0x800.This
0x800size area will be used later for Fake Object, ROP, Temp Memory, etc.
UAF Object→Read()to leakUAF Object→itemname_.Modify the
UAF Object Vectoras follows:UAF Object→Vector→start=heapleakUAF Object→Vector→last=heapleakUAF Object→Vector→end=heapleak + 0x800

Perform
UAF Object→CreateItem0x800 / 8times.The vector will point to
std::string, and the 0x800 size will be filled.

Perform
UAF Object→CreateItemone 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_endof 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

문제에서 제공된 파일은 필수적인 것만 들어있어 매우 미니멀합니다. 사전에 배포된 Windows 11 이미지 외에는 PoW (Proof-of-Work) 코드와 MemoryStorage.sys 파일만 주어졌습니다. 우리는 이 MemoryStorage.sys 드라이버를 분석하고 이를 이용해 Windows에서 권한 상승을 수행해야 합니다.
문제 개요
MemoryStorage.sys는 레거시 Windows 드라이버 모델(WDM)을 사용하여 개발된 Windows 커널 드라이버입니다. 이 드라이버의 DriverEntry 함수를 살펴보면, 커널 스택 쿠키를 초기화하는 기능과 드라이버의 메인 루틴을 처리하는 기능을 담당하는 단 두 개의 함수 구성 요소만 포함되어 있습니다.
sub_14000173C 함수에서 드라이버는 \DosDevices\MemoryStorage라는 이름의 기호 링크를 생성하고 드라이버 장치를 커널에 등록합니다. 이 기호 링크를 통해 사용자 모드 애플리케이션은 요청을 보내 드라이버와 통신할 수 있습니다.
조건문 블록 내부에서 드라이버는 디스패치 루틴을 등록합니다. a1->MajorFunction[0] 및 a1->MajorFunction[2] 항목은 디바이스 핸들이 열리거나 닫힐 때 호출되는 IRP_MJ_CREATE 및 IRP_MJ_CLOSE 루틴에 대응합니다. 이 루틴들은 문제를 해결하는 데 있어서 필수적인 부분은 아니므로 완결성을 위해서만 언급합니다.
가장 중요한 부분은 sub_140001370 함수를 IRP_MJ_DEVICE_CONTROL의 처리기로 등록하는 a1->MajorFunction[14]에 대한 할당입니다. 이 루틴은 I/O 제어 코드(IOCTL)를 기반으로 다양한 명령을 처리하는 역할을 담당하며, 취약점 분석에서 핵심적인 역할을 합니다.
이제 sub_140001370 함수를 더 자세히 살펴보겠습니다. 이 함수는 네 개의 특정 I/O 제어 코드를 바탕으로 명령을 처리하는 핸들러 역할을 합니다.
각 코드의 작동 방식을 살펴보기 전에, 커널 드라이버로 요청을 보낼 때 사용되는 IRP (I/O Request Packet) 구조를 가볍게 검토하는 것이 도움이 됩니다. IRP는 I/O 작업을 나타내기 위해 Windows에서 사용하는 본질적인 데이터 구조이며, 요청된 작업, 관련 장치 및 연결된 버퍼에 대한 정보를 전달합니다. IRP의 작동 방식을 이해하는 것은 드라이버가 사용자 모드 요청을 처리하는 방식을 분석하는 데 중요합니다.
IRP 구조
IRP (I/O Request Packet)는 운영체제와 장치 드라이버 간의 I/O 요청을 처리하는 데 사용되는 커널 수준의 구조체입니다. 이는 특정한 내부 레이아웃을 따르며, 중요한 필드 중 하나는 IO_STACK_LOCATION 구조체를 가리키는 CurrentStackLocation입니다. 이 구조체는 드라이버 스택의 각 계층이 IRP를 적절하게 처리할 수 있도록 돕습니다.
모든 I/O 요청은 IRP 및 이와 관련된 IO_STACK_LOCATION 구조체에 저장된 정보에 따라 처리됩니다. 이러한 필드들을 통해 드라이버는 각 요청을 용도에 맞게 처리할 수 있습니다.

예를 들어, sub_140001370 함수는 I/O 제어 코드를 기반으로 요청을 처리합니다. 이 경우 IO_STACK_LOCATION 구조체의 MajorFunction 필드 값은 14이며, 이는 IRP_MJ_DEVICE_CONTROL에 대응합니다. IoControlCode 필드가 특정 값과 일치하면 드라이버는 해당 I/O 요청에 대한 동작을 구현하는 함수를 실행합니다.

sub_140001370 함수에 표시된 값들은 모두 IRP 구조체 내의 필드에서 가져온 것입니다. IRP에는 많은 필드가 포함되어 있으므로 여기서 모두 설명하는 것은 실용적이지 않습니다. 전체 참고 자료는 여기의 공식 문서를 참조하시기 바랍니다.
이 문제를 해결하기 위해 우리는 IRP->CurrentStackLocation 내부에 위치한 Type3InputBuffer 필드에만 초점을 맞출 것입니다. 이 필드는 사용자가 제공한 입력 버퍼를 가리키며, 드라이버는 이를 사용하여 사용자 공간으로부터 데이터를 받습니다.
Type3InputBuffer는 Neither I/O 방식을 사용하여 요청을 보낼 때 입력 버퍼로 사용됩니다. 이는 또 다른 중요한 개념으로 이어집니다. Neither I/O 방식이란 정확히 무엇일까요?
Neither I/O
공식 문서 [링크]에 따르면, Neither I/O 방식은 입력 또는 출력 버퍼에 액세스할 때 SystemBuffer (커널 모드 버퍼)나 MDL (Memory Descriptor List)을 제공하지 않습니다. 대신 사용자 모드 가상 주소를 직접 사용합니다. 즉, 이 방식을 사용하는 I/O 요청에서 사용자가 제공한 데이터는 사용자 공간에 남아 있으며 커널 메모리로 복사되지 않습니다.
이러한 맥락에서 입력 버퍼는 IO_STACK_LOCATION 구조체 내의 Type3InputBuffer 필드를 통해 처리되는 반면, 출력 버퍼는 IRP 구조체 내의 UserBuffer 필드를 통해 액세스됩니다. 이 포인터들은 사용자 공간에 위치한 메모리를 참조하므로, 이를 검증하고 안전하게 사용하는 것은 드라이버의 책임이 됩니다.

I/O 제어 루틴이 Neither I/O 방식을 사용하는지 식별하는 방법은 간단합니다. I/O 제어 코드를 4로 나누었을 때 나머지가 3 주어지면 해당 루틴이 Neither I/O 방식을 사용함을 나타냅니다.
더 쉽게 말해, I/O 제어 코드의 마지막 반 바이트(니블)가 3, 7, B, 또는 F 중 하나이면 해당 루틴은 Neither I/O를 사용하는 것입니다.
sub_140001370 지속 분석
이제 sub_140001370 함수의 분석을 계속해 보겠습니다. 앞서 설명한 것처럼, I/O 제어 코드 0x7101003 및 0x7101007이 Neither I/O 방식을 사용함을 확인할 수 있습니다. 이는 이 코드들을 처리하는 루틴 내에서 드라이버가 Type3InputBuffer 필드를 입력 버퍼로 사용함을 의미합니다.
Neither I/O는 사용자 모드 가상 주소를 드라이버로 직접 전달하므로, Type3InputBuffer가 참조하는 버퍼는 커널 메모리가 아니라 사용자 공간에 존재합니다. 따라서 드라이버가 명시적으로 검증하거나 안전한 영역으로 복사하지 않는 한, 이 포인터를 사용한 모든 데이터 액세스는 사용자의 주소 공간 내에서 발생합니다.
근본 원인 분석: 스택 기반 버퍼 오버플로우
I/O 제어 코드 0x7101003을 처리하는 sub_140001274 함수를 분석해 보겠습니다. 이 함수는 다음 다섯 단계를 거쳐 사용자가 제공한 메모리 영역의 데이터를 Dst라는 이름의 로컬 커널 버퍼로 복사합니다.
함수는 먼저
Type3InputBuffer가 유효한 사용자 모드 포인터인지, 크기가 최소0x10바이트 이상인지 확인합니다.그런 다음
Type3InputBuffer의 처음 2바이트를 검사하여 해당 값이0x40이하이고 0이 아닌지를 확인합니다.함수는
Type3InputBuffer내의 8바이트 오프셋 주소에서 8바이트 값을 읽습니다. 또 다른 포인터를 나타내는 이 값은 로컬 변수v4에 저장됩니다.ProbeForRead를 사용하여v4에 저장된 주소가 읽기 가능한 사용자 모드 주소인지 검증합니다.위의 모든 검사가 통과하면 함수는
v4가 가리키는 메모리로부터 2단계에서 읽은 값만큼의 바이트를 로컬 변수Dst로 복사합니다.
이 과정을 통해 드라이버는 사용자가 제공한 데이터를 읽고 복사하려고 시도하지만, 나중에 살펴보겠지만 이 검증들이 불완전하거나 오용될 경우 해당 로직이 악용될 수 있습니다.
언뜻 보기에는 사용자가 제공한 모든 주소와 값이 올바르게 검증된 것처럼 보여 함수가 안전한 것처럼 보입니다. 그러나 Type3InputBuffer가 사용자 모드 메모리를 가리킨다는 점을 기억하십시오.
이 코드의 결함은 [2]단계에서 발생합니다. 이 단계에서 함수는 Type3InputBuffer의 처음 2바이트를 읽고 해당 값이 유효한 범위 내에 있는지 확인합니다. 하지만 [5]단계에서 memcpy를 호출할 때 이전에 저장된 v3 값을 사용하지 않습니다. 대신 Type3InputBuffer에서 다시 값을 읽어오기 때문에 double fetch(이중 페치)가 발생합니다.
이를 통해 사용자는 레이스 컨디션을 유발하여 로컬 변수 Dst에 0x40 바이트보다 더 많은 바이트를 복사하도록 할 수 있습니다.
하지만 이 스택 기반 버퍼 오버플로우 취약점을 익스플로잇하기 위해서는 커널 주소 유출이 필수적입니다. 그렇다면 커널 주소 유출을 허용하는 취약점은 어디에 존재할까요?
근본 원인 분석: 정보 공개 (Information Disclosure)
I/O 제어 코드 0x7101007을 처리하는 sub_14000113C 함수를 분석해 보겠습니다. 이 루틴 역시 Neither I/O 방식을 사용합니다. 이 함수는 다음 네 단계로 나눌 수 있습니다.
먼저
Type3InputBuffer가NULL이 아닌지, 입력 버퍼 길이가 최소 8바이트 이상인지 확인합니다. 그런 다음ProbeForRead함수를 사용하여Type3InputBuffer가 유효한 사용자 모드 주소인지 확인합니다.Type3InputBuffer의 최초 2바이트 값이 0이 아니고0x40이하인지 확인합니다.0x10000바이트 크기의 메모리 풀을 할당합니다.Type3InputBuffer의 첫 2바이트에 지정된 크기를 사용하여 로컬 변수Dst의 내용을 할당된 풀에 복사합니다.할당된 풀은 글로벌 배열
qword_140003080에 저장됩니다.
이 함수도 이전 함수와 마찬가지로 [2]단계와 [4]단계 사이에서 double fetch에 취약합니다. 이 때문에 Dst 변수의 0x40 바이트 이상의 데이터가 풀로 복사될 수 있습니다. 문제는 이 복사된 데이터를 어디서 읽을 수 있는가입니다.
그 해답은 I/O 제어 코드 0x7101010을 처리하는 sub_1400014CC 함수에 있습니다. LoggingMemoryInformationForInternalMemoryStorageDriver라는 이름의 이 함수는 다음 단계를 수행합니다.
먼저 섹션(Section)을 생성합니다. 그런 다음 0x10000 바이트의 메모리를 해당 섹션에 매핑합니다. 그 후 글로벌 배열 qword_140003080의 내용을 매핑된 섹션 메모리에 복사합니다. 마지막으로 섹션 매핑을 해제하고 커널 모드에서 해당 핸들을 닫습니다.
여기서 의문이 생깁니다. 섹션이 매핑 해제되고 즉시 닫힌다면 어떻게 내부의 데이터에 액세스할 수 있을까요?
답은 Windows에서 사용자 모드 프로세스가 미리 공유 모드로 Section을 열어두면 커널이 섹션을 완전히 매핑 해제하고 닫을 수 없다는 것입니다. 드라이버가 릴리스하기 전에 사용자 모드에서 해당 섹션을 점유함으로써, 유출된 커널 주소를 포함하여 내부에 저장된 데이터에 액세스하는 것이 가능해집니다.
참고로 유출된 일부 커널 주소는 아래와 같이 나타납니다. ntoskrnl과 커널 드라이버의 주소 일부가 유출되었으므로 스택 ROP 체인을 구성하는 데 있어 문제가 없을 것입니다 👍
익스플로잇 (Exploit)
이제 필요한 모든 정보가 수집되었으므로, SYSTEM 권한을 획득하기 위해 ROP 체인을 사용하여 스택 기반 버퍼 오버플로우를 익스플로잇할 수 있습니다. ROP 체인은 다음 순서로 작동합니다.
ROP 체인에 들어가기 전에, Medium 무결성 수준으로 실행되는
cmd프로세스가 생성됩니다. 이 프로세스는curl명령을 사용해C:\Windows\System32\flag.txt를 지속적으로 읽으려고 시도합니다.cmd프로세스의 PID를 사용하여, ROP 체인은PsLookupProcessByProcessId함수를 호출하여cmd프로세스의 EPROCESS 주소를 획득합니다.동일한 함수를 다시 호출하여 System 프로세스의 EPROCESS 주소를 얻습니다. (Windows에서 System 프로세스의 PID는 항상 4입니다.)
그 후
cmd프로세스의 Token 값을 System 프로세스의 Token 값으로 덮어씁니다.
이 체인이 완료되면 처음 생성되었던 cmd 프로세스는 SYSTEM 권한으로 실행되게 됩니다. 그러나 아직 과정이 끝난 것은 아닙니다. ROP 체인이 커널 컨텍스트에서 실행되었기 때문에 제어권을 유저 컨텍스트로 돌려놓아야 합니다. 또한, cmd 프로세스가 flag.txt를 읽어 그 내용을 외부로 전송하는 데 약간의 시간이 필요합니다.
유저 컨텍스트로 돌아가기 위해 KiKernelSysretExit 같은 함수를 사용할 수도 있지만, 이번 익스플로잇에서는 데이터 전송이 이루어질 수 있는 짧은 지연만 필요했습니다. 이를 달성하기 위해 ROP 체인의 끝에 단순한 \xEB\xFE 가젯을 사용하였는데, 이는 무한 셀프 점프를 유발하여 사실상 커널을 지
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
RenderFrameImplin the chrome binary,Overwrite the member variable
enable_mojo_jsofRenderFrameImpl
However, the new mitigation does the following:
If ScriptContext is not finished, grant ReadOnly permission to
enable_mojo_jsarea 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.

Popular Articles








