<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Proctor Ninja</title>
    <link>https://proctor.ninja/</link>
    <description>Oxylibrium&#39;s blog about exam spyware analysis.</description>
    <pubDate>Sat, 11 Apr 2026 16:08:20 -0400</pubDate>
    <item>
      <title>Proctorio&#39;s facial recognition is racist.</title>
      <link>https://proctor.ninja/proctorios-facial-recognition-is-racist</link>
      <description>&lt;![CDATA[Also, it kind of sucks in general.&#xA;&#xA;!--more--&#xA;&#xA;It&#39;s much more likely to not recognize Black people, for example&#xA;&#xA;QED.&#xA;&#xA;Wait. That&#39;s it?&#xA;&#xA;Failures in the real world of their facial recognition algorithm are widely known, even if Mike is in denial (EDIT: see page 21, paragraph 2-3 of the letter to the senate, &#34;fewer than five&#34; issues due to race). See @Procteario \[1\] \[2\] for more; or for generic failures, even this image a friend sent me.&#xA;&#xA;Now I have the data to prove it.&#xA;&#xA;I talked about how I got my hands on their facial recognition data in my previous post - with my hands on their &#39;prized algorithm&#39;, I decided to take it on a test drive.&#xA;&#xA;I set out to look for a representative dataset, and I found FairFace - a facial recognition dataset balanced for race, gender and age. I found their GitHub, which included a link to the dataset. I downloaded their \[Padding=1.25\] set, and ran the facial recognition algorithm against the 10954 faces stored in val.&#xA;&#xA;Proctorio includes all four OpenCV models - and runs all four of them at once too, despite that being... quite inefficient. But, I gave them the benefit of the doubt: I count an image as a &#34;pass&#34; in the above graph if any one of the models thought it found anything like a face at all; though often times it just doesn&#39;t work that way.&#xA;&#xA;It could not find the Black woman&#39;s face.&#xA;It could not find the face in the foreground.&#xA;It thinks the ear is a face.&#xA;It thinks the badge on a hat is a face?&#xA;&#xA;Several &#34;failures&#34; were also embarassingly obvious:&#xA;It doesn&#39;t think there was a face at all. There is.&#xA;&#xA;Note that all images featuring faces were taken from the FairFace dataset by Kimmo Kärkkäinen and Jungseock Joo. Original images licensed under the CC-BY 4.0 license.&#xA;&#xA;EDIT: Added raw data.&#xA;&#xA;| Race              | Pass | Fail | Pass % | Relative % from avg  |&#xA;|-------------------|------|------|--------|---------------------:|&#xA;| Black             |  669 |  887 |  42.99 |               -28.72 |&#xA;| Middle Eastern    |  718 |  491 |  59.39 |                -1.53 |&#xA;| White             | 1258 |  827 |  60.34 |                 0.05 |&#xA;| East Asian        |  971 |  579 |  62.65 |                 3.88 |&#xA;| Southeast Asian   |  917 |  498 |  64.81 |                 7.46 |&#xA;| Indian            |  984 |  532 |  64.91 |                 7.63 |&#xA;| Latino / Hispanic | 1089 |  534 |   67.1 |                11.26 |&#xA;| Total             | 6606 | 4348 |  60.31 |                      |&#xA;&#xA;Audits, audits...&#xA;&#xA;I earlier had the experience of looking through Proctorio&#39;s &#34;zero knowledge encryption&#34; claims, and their audit from a &#39;White Oak Security&#39;, which I found... less than promising.&#xA;&#xA;Now, I have the privilege of looking through their facial recognition suite, the centerpiece of their &#34;unbiased, unblinking&#34; service (quote from home page), supposedly undergoing an audit by a &#39;BABL AI&#39;, which has been supposedly happening for months now.&#xA;&#xA;As an outsider to their company, I managed to pull their facial recognition outside of their application and run it against an open-source dataset in a weekend.&#xA;&#xA;With this track record of audits that take too long and lead to nowhere, I&#39;m starting to question not just Proctorio, but the questionable people they contract with to rubber-stamp their broken products.&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Also, it kind of sucks in general.</p>



<p><img src="https://i.imgur.com/sjuwg8t.png" alt="It&#39;s much more likely to not recognize Black people, for example"></p>

<p>QED.</p>

<h2 id="wait-that-s-it">Wait. That&#39;s it?</h2>

<p>Failures in the real world of their facial recognition algorithm are widely known, even if Mike is in denial (EDIT: see page 21, paragraph 2-3 of the <a href="https://epic.org/privacy/dccppa/online-test-proctoring/Proctorio-senate-response-010721.pdf" rel="nofollow">letter to the senate</a>, “fewer than five” issues due to race). See <a href="https://twitter.com/Procteario" rel="nofollow">@Procteario</a> <a href="https://twitter.com/sharr__o/status/1364376521519992832" rel="nofollow">[1]</a> <a href="https://twitter.com/cham_omot/status/1364376131516854275" rel="nofollow">[2]</a> for more; or for generic failures, even this image a friend <a href="https://twitter.com/Oxylibrium/status/1370365889187483661" rel="nofollow">sent me</a>.</p>

<p>Now I have the data to prove it.</p>

<p>I talked about how I got my hands on their facial recognition data in my <a href="https://proctor.ninja/the-duality-of-obfuscation-feat" rel="nofollow">previous post</a> – with my hands on their &#39;prized algorithm&#39;, I decided to take it on a test drive.</p>

<p>I set out to look for a representative dataset, and I found <a href="https://arxiv.org/abs/1908.04913" rel="nofollow">FairFace</a> – a facial recognition dataset balanced for race, gender and age. I found their <a href="https://github.com/joojs/fairface" rel="nofollow">GitHub</a>, which included a link to the dataset. I downloaded their <a href="https://drive.google.com/file/d/1g7qNOZz9wC7OfOhcPqH1EZ5bk1UFGmlL/view" rel="nofollow">[Padding=1.25]</a> set, and ran the facial recognition algorithm against the 10954 faces stored in <code>val</code>.</p>

<p>Proctorio includes all four OpenCV models – and runs all four of them at once too, despite that being... quite inefficient. But, I gave them the benefit of the doubt: I count an image as a “pass” in the above graph if any one of the models thought it found anything like a face at all; though often times it just doesn&#39;t work that way.</p>

<p><img src="https://i.imgur.com/Ku7NkEG.png" alt="It could not find the Black woman&#39;s face.">
<img src="https://imgur.com/wIpY0yO.png" alt="It could not find the face in the foreground.">
<img src="https://imgur.com/iMANz8M.png" alt="It thinks the ear is a face.">
<img src="https://imgur.com/Z1m3fOC.png"></p>

<p>Several “failures” were also embarassingly obvious:
<img src="https://i.imgur.com/5XumUHo.png" alt="It doesn&#39;t think there was a face at all. There is."></p>

<p>Note that all images featuring faces were taken from the FairFace dataset by Kimmo Kärkkäinen and Jungseock Joo. Original images licensed under the CC-BY 4.0 license.</p>

<p>EDIT: Added raw data.</p>

<table>
<thead>
<tr>
<th>Race</th>
<th>Pass</th>
<th>Fail</th>
<th>Pass %</th>
<th align="right">Relative % from avg</th>
</tr>
</thead>

<tbody>
<tr>
<td>Black</td>
<td>669</td>
<td>887</td>
<td>42.99</td>
<td align="right">-28.72</td>
</tr>

<tr>
<td>Middle Eastern</td>
<td>718</td>
<td>491</td>
<td>59.39</td>
<td align="right">-1.53</td>
</tr>

<tr>
<td>White</td>
<td>1258</td>
<td>827</td>
<td>60.34</td>
<td align="right">0.05</td>
</tr>

<tr>
<td>East Asian</td>
<td>971</td>
<td>579</td>
<td>62.65</td>
<td align="right">3.88</td>
</tr>

<tr>
<td>Southeast Asian</td>
<td>917</td>
<td>498</td>
<td>64.81</td>
<td align="right">7.46</td>
</tr>

<tr>
<td>Indian</td>
<td>984</td>
<td>532</td>
<td>64.91</td>
<td align="right">7.63</td>
</tr>

<tr>
<td>Latino / Hispanic</td>
<td>1089</td>
<td>534</td>
<td>67.1</td>
<td align="right">11.26</td>
</tr>

<tr>
<td>Total</td>
<td>6606</td>
<td>4348</td>
<td>60.31</td>
<td align="right"></td>
</tr>
</tbody>
</table>

<h2 id="audits-audits">Audits, audits...</h2>

<p>I earlier had the experience of looking through Proctorio&#39;s “zero knowledge encryption” claims, and their audit from a &#39;White Oak Security&#39;, which I found... <a href="https://proctor.ninja/wave-rake-proctorio" rel="nofollow">less than promising</a>.</p>

<p>Now, I have the privilege of looking through their facial recognition suite, the centerpiece of their “unbiased, unblinking” service (quote from home page), supposedly undergoing an audit by a &#39;BABL AI&#39;, which has been supposedly happening for months now.</p>

<p>As an outsider to their company, I managed to pull their facial recognition outside of their application and run it against an open-source dataset in a weekend.</p>

<p>With this track record of audits that take too long and lead to nowhere, I&#39;m starting to question not just Proctorio, but the questionable people they contract with to rubber-stamp their broken products.</p>
]]></content:encoded>
      <guid>https://proctor.ninja/proctorios-facial-recognition-is-racist</guid>
      <pubDate>Thu, 18 Mar 2021 19:28:08 +0000</pubDate>
    </item>
    <item>
      <title>The duality of obfuscation: feat. Proctorio</title>
      <link>https://proctor.ninja/the-duality-of-obfuscation-feat</link>
      <description>&lt;![CDATA[Proctorio obfuscates code and hides behavior. Do Google and Microsoft care?&#xA;&#xA;!--more--&#xA;&#xA;Recently, Google made the headlines (or, at least the front page of Hacker News) for removing an extension that included lodash, citing a portion of the script as evidence of &#34;obfuscation&#34;.&#xA;&#xA;Screenshot of Google report&#xA;&#xA;Which prompted me to think back to one of the first posts I made to this blog - about Proctorio, specifically. TL;DR extension store obfuscation policies are not as equally enforced as they appear to be.&#xA;&#xA;Obfuscation galore&#xA;&#xA;This analysis was performed on a version of the extension obtained from the Chrome Web Store on the 18th of March.&#xA;&#xA;To start out, let&#39;s look at something simple, a random sample of names for .js files in  their extension:&#xA;&#xA;[oxy@toolbox proctorio20210318]$ find . -name .js | shuf -n 10&#xA;./assets/h7iY.js&#xA;./assets/touch/af20a691735f66590005df7e2b2d6691.js&#xA;./assets/G787.js&#xA;./assets/pipes/b86493d2ae255a02bb7ea61e7fb77c10.js&#xA;./assets/k7Wq.js&#xA;./assets/drops/c5e5541622a147e4b5b279d034d44975.js&#xA;./assets/touch/bc3a0da96bfbf227d678f23d5c1c8379.js&#xA;./assets/touch/ec518e9a786dbecff863386ee44bb8d2.js&#xA;./assets/elbows/d9b75e781f5d4ef9a11c68875d99ea8d.js&#xA;./assets/touch/8c8f73ef24833fd3a8dd6befc832a060.js&#xA;&#xA;&#34;Readable&#34; code was Google&#39;s standard, yes? Well... I&#39;ll let the readability of the above names speak for themselves. (No, the names do not match checksums.)&#xA;&#xA;Silly renames: Wonder what ./assets/J5HG.js is? There&#39;s a variable called dhs in there... its really SJCL. Oh, there&#39;s also a cia and a kgb if you look hard enough; real mature.&#xA;&#xA;JScrambler!&#xA;function a000() {}&#xA;a000.o6 = &#34;classic-learn-iframe&#34;;&#xA;a000 is a standard artifact of using JScrambler, and so are the locale strings being Axxx. It looks like they&#39;ve turned off a reasonable amount of JScrambler&#39;s obfuscation options since I&#39;ve last written about them.&#xA;&#xA;Now, JScrambler&#39;s product page should tell you its an obfuscation tool, so...&#xA;&#xA;What&#39;s in the .7zs?&#xA;&#xA;Proctorio, in addition to shipping a lot of questionably &#34;readable&#34;/unreadable JS, also ships some .7z files in assets/packs, which are... actually not 7zip files.&#xA;&#xA;[oxy@toolbox packs]$ ls&#xA;FmuG8K.7z  nWZk8V.7z  rXqE9b.7z  sVAazF.7z&#xA;[oxy@toolbox packs]$ file FmuG8K.7z &#xA;FmuG8K.7z: data&#xA;[oxy@toolbox packs]$ 7za x FmuG8K.7z &#xA;~[snip]~&#xA;Extracting archive: FmuG8K.7z&#xA;ERROR: FmuG8K.7z&#xA;FmuG8K.7z&#xA;Open ERROR: Can not open the file as [7z] archive&#xA;~[snip]~&#xA;[oxy@toolbox packs]$ binwalk FmuG8K.7z &#xA;&#xA;DECIMAL       HEXADECIMAL     DESCRIPTION&#xA;--------------------------------------------------------------------------------&#xA;&#xA;If you look for references to these file names, they don&#39;t show up anywhere in the JS, but they also ship a PNaCl binary! I wonder if anything shows up there...&#xA;&#xA;Nope, nothing in strings proctorio.pexe, and I have better things to do than to wire up LLVM to work with PNaCl files, so... moving along&#xA;&#xA;Scripts from a CDN!&#xA;&#xA;A friend was running a packet capture tool while taking a Proctorio practice test, and this showed up in the logs: https://cdn.proctorauth.com/assets/payload-3.4.7.2.js&#xA;&#xA;They texted me &#34;Hey, there&#39;s some JS, wanna look at it?&#34;.&#xA;&#xA;Policy reminder: &#34;Developers must not obfuscate code or conceal functionality of their extension. This also applies to any external code or resource fetched by the extension package.&#34;&#xA;&#xA;Decided I&#39;d take a peek, and at the top of the file, lo and behold!&#xA;&#xA;a000.G26=&#39;function&#39;;&#xA;function a000(){}&#xA;&#xA;It&#39;s my old friend JScrambler again. Nice to see you :)&#xA;&#xA;I wondered if they used JScrambler in the near no-op mode they&#39;ve reduced it to on their in-extension JavaScript, but I scrolled to the end of the file and...&#xA;&#xA;function B4xx(){return&#34;lro%1Ce%1C%1BjI:$;%3CPn)5;%0E%5CF-e)%1EtK2%07~8%18C&amp;l~.%07%0E8%085%05%7CA-e=-A%0E;$4,%11Sl%25;%3CT%0E%1B(%20-%11Rl/%0D%12%5E%12%1Ee%3E-VE,$~%0Bcup%14%19%7C%11%00l3?;EE&amp;2?lGO)%25~=%5BN-&#39;3&amp;PNl.4$ZK,e2-%5CM%205~%1API%3C%17?+AE:e)=WY%3C33&amp;R%0E;5;:AY%1F(.%20%11Y!;?lFC2$~.GE%25%022)Gi&#39;%25?l%18l558%11&amp;%25?.%5CD-%25~&#39;%5BF&#39;%20%3ElE%07&#39;4.e%11M-5%18=%5CF,%084.ZX%25%20.!ZDl.-%5B%0E8.)%3CxO;2;/P%0E,$.-V%5E%0546%3C%5Cy+%206-%11g)5~+C%5E%0B.6&#39;G%0E%0E%203$PNh55hYE)%25zlZZ-/~;ck);%1ClYE)%25~+TG%3E(%3ElRO%3C%046-XO&amp;5%181%7CNlnu+QDf1(&#39;V%5E&#39;3;=ABf%225%25%1AK;2?%3CF%05ln~;AK%3C4)lTN,%04,-%5B%5E%04()%3CPD-3~+C%5E%0B.6&#39;G%0E:$;,Ly%3C%20.-%11%0A;5;%3C@Yra~:PY8.4;P%0E%1A$9%3CcO+55:%11F-/=%3C%5D%0E%25$);TM-e-!Q%5E%20e?:GE:e;,Qo%3E$4%3CyC;5?&amp;PXl%2258L~&#39;e4%1FoAp%17~%0Bzf%07%13%05%1Arh%09s%1D%1Atsl%20(:TS4%3C.PXl%077=r%12%03e%19)FI)%25?%0BYK;23.%5CO:e%0C!QO&#39;%02;8A:$~:PY8.4;P~11?lsK!-?,%15%5E&#39;a6&#39;TNhe=-Ao$$7-%5B%5E%0A8%13,%11Z=22lVB)3%19&#39;QO%095~-%5BI&#39;%25?lGr9%04c%11l%254%1Dp~&#34;;}&#xA;&#xA;String obfuscation! Here&#39;s a prettified version of one of the functions in it, with no deobfuscation done:&#xA;&#xA;  function f05() {&#xA;    var i05 = [arguments];&#xA;    i05[8] = new XMLHttpRequest();&#xA;    i058](e644.B7(40), e644.B7(18) + i050, !![]);&#xA;    i058] = e644.v7(30);&#xA;    e644e644.v7(44);&#xA;    i058] = function () {&#xA;      var j05 = [arguments];&#xA;      e644e644.B7(44);&#xA;      if (i058] === 4) {&#xA;        if (i058] === 200) {&#xA;          j051] = new TextDecoder()[e644.v7(53)[e644.B7(56)]);&#xA;          j05[8] = e644.v7(45);&#xA;          j05[4] = e644.B7(39);&#xA;          for (j057] = 0; j05[7] &lt; j05[1]; j05[7]++)&#xA;            j05[4] += String[d05.B7(4)](&#xA;              j051](j05[7]) ^&#xA;                j058](j057] % j05[8])&#xA;            );&#xA;          j056] = new Uint8Array(new TextEncoder()[e644.v7(37)));&#xA;          cve644.B7(41), i050, j05[6], true, false, false);&#xA;        }&#xA;        (1, i050)();&#xA;      }&#xA;    };&#xA;    i058]();&#xA;  }&#xA;&#xA;There&#39;s many of our JScrambler passes in here:&#xA;&#xA;String Concealing&#xA;Variable Masking&#xA;Boolean to Anything&#xA;&#xA;There&#39;s also other transformations elsewhere in the file, such as probably a few Opaque Predicates, but... life&#39;s too short for listing obfuscator passes that were trivial to defeat!&#xA;&#xA;I&#39;m pretty sure that if you&#39;ve read this far, you probably wanted to know what&#39;s behind that mess of characters - and may I present, in about an hour&#39;s time:&#xA;&#xA;  function fetchxorencasset(filepath, callback) {&#xA;    var xhr = new XMLHttpRequest();&#xA;    xhr.open(&#34;GET&#34;, &#34;//cdn.proctorauth.com/assets/&#34; + filepath, true);&#xA;    xhr.responseType = &#34;arraybuffer&#34;;&#xA;    xhr.onload = function () {&#xA;      if (xhr.readyState === 4) {&#xA;        if (xhr.status === 200) {&#xA;          var xorenc = new TextDecoder().decode(xhr.response);&#xA;          var xorkey = &#34;pIoMIke&#34;;&#xA;          var xordec = &#34;&#34;;&#xA;          for (var i = 0; i &lt; xorenc.length; i++)&#xA;            xordec += String.fromCharCode(xorenc.charCodeAt(i) ^ xorkey.charCodeAt(i % xorkey.length));&#xA;          var result = new Uint8Array(new TextEncoder().encode(xordec));&#xA;          cv.FScreateDataFile(&#34;/&#34;, filepath, result, true, false, false);&#xA;        }&#xA;        callback();&#xA;      }&#xA;    };&#xA;    xhr.send();&#xA;  }&#xA;&#xA;XOR encryption, my favorite kind of obfuscation! /s&#xA;Oh, and it has Mike&#39;s name on it too, how cute.&#xA;&#xA;So now that I knew this much, I set out to find out what it fetched, and turns out...&#xA;&#xA;fetchxorencasset(&#34;sVAazF&#34;, function () {&#xA;  true;&#xA;  cvcclassifier2.load(&#34;sVAazF&#34;);&#xA;  fetchxorencasset(&#34;FmuG8K&#34;, function () {&#xA;    cvcclassifier3.load(&#34;FmuG8K&#34;);&#xA;    true;&#xA;    fetchxorencasset(&#34;rXqE9b&#34;, function () {&#xA;      cvcclassifier4.load(&#34;rXqE9b&#34;);&#xA;      fetchxorencasset(&#34;nWZk8V&#34;, function () {&#xA;&#xA;Do these names look familiar? (Spoiler: look for the .7z files.)&#xA;&#xA;Yes, they do. In fact, they&#39;re the exact same files that Proctorio ships in their extension! Armed with a key, XOR-decrypting it leads to...&#xA;&#xA;OpenCV XML files.&#xA;&#xA;Peering under the hood&#xA;&#xA;Once I had the XML files, I decided to take a look at them. Hooked them up into OpenCV for Python, and...&#xA;&#xA;I got the exact same results as the corresponding stock OpenCV model. For every single test image.&#xA;&#xA;Impressive. I somehow expected this from this company, but... it&#39;s nice to know that this is really how it works.&#xA;&#xA;TL;DR&#xA;&#xA;Okay, so you gave me a lot of snippets of code, what about them?&#xA;&#xA;Proctorio&#39;s extension is questionably readable at best, and also fetches clearly obfuscated web resources when in use. In addition, the extension ships with &#34;concealed&#34; XML files obfuscated with a XOR key and mislabeled as a .7z. Not sure why they did it, but they did.&#xA;&#xA;I&#39;ve reported this via the Report abuse button on the Chrome Web Store, but it appears all reports get sent to /dev/null. I&#39;ve also flagged this exact same thing with Microsoft&#39;s extension store, which has similar policies, but have not heard back from them in a week and a half.&#xA;&#xA;I&#39;d love to see Google and Microsoft show up and actually enforce things.]]&gt;</description>
      <content:encoded><![CDATA[<p>Proctorio obfuscates code and hides behavior. Do Google and Microsoft care?</p>



<p>Recently, Google made the headlines (or, at least the front page of Hacker News) for <a href="https://roadtoramen.com/Day-435-Google-Took-Down-My-Chrome-Extension-for-Using-Lodash-a3096c51321f42e0a04c77e1a25f484a" rel="nofollow">removing an extension</a> that included lodash, citing a portion of the script as evidence of “obfuscation”.</p>

<p><img src="https://i.imgur.com/0LfQJNS.png" alt="Screenshot of Google report"></p>

<p>Which prompted me to think back to one of the first posts I made to this blog – about Proctorio, specifically. TL;DR extension store obfuscation policies are not as equally enforced as they appear to be.</p>

<h2 id="obfuscation-galore">Obfuscation galore</h2>

<p><em>This analysis was performed on a version of the extension obtained from the Chrome Web Store on the 18th of March.</em></p>

<p>To start out, let&#39;s look at something simple, a random sample of names for .js files in  their extension:</p>

<pre><code>[oxy@toolbox proctorio_20210318]$ find . -name *.js | shuf -n 10
./assets/h7iY.js
./assets/touch/af20a691735f66590005df7e2b2d6691.js
./assets/G787.js
./assets/pipes/b86493d2ae255a02bb7ea61e7fb77c10.js
./assets/k7Wq.js
./assets/drops/c5e5541622a147e4b5b279d034d44975.js
./assets/touch/bc3a0da96bfbf227d678f23d5c1c8379.js
./assets/touch/ec518e9a786dbecff863386ee44bb8d2.js
./assets/elbows/d9b75e781f5d4ef9a11c68875d99ea8d.js
./assets/touch/8c8f73ef24833fd3a8dd6befc832a060.js
</code></pre>

<p>“Readable” code was Google&#39;s standard, yes? Well... I&#39;ll let the readability of the above names speak for themselves. (No, the names do not match checksums.)</p>

<p>Silly renames: Wonder what <code>./assets/J5HG.js</code> is? There&#39;s a variable called <code>dhs</code> in there... its really SJCL. Oh, there&#39;s also a <code>cia</code> and a <code>kgb</code> if you look hard enough; real mature.</p>

<p>JScrambler!</p>

<pre><code class="language-js">function a000() {}
a000.o6 = &#34;classic-learn-iframe&#34;;
</code></pre>

<p>a000 is a standard artifact of using JScrambler, and so are the locale strings being Axxx. It looks like they&#39;ve turned off a reasonable amount of JScrambler&#39;s obfuscation options since I&#39;ve last written about them.</p>

<p>Now, JScrambler&#39;s product page should tell you its an obfuscation tool, so...</p>

<h2 id="what-s-in-the-7z-s">What&#39;s in the <code>.7z</code>s?</h2>

<p>Proctorio, in addition to shipping a lot of questionably “readable”/unreadable JS, also ships some <code>.7z</code> files in <code>assets/packs</code>, which are... actually not 7zip files.</p>

<pre><code>[oxy@toolbox packs]$ ls
FmuG8K.7z  nWZk8V.7z  rXqE9b.7z  sVAazF.7z
[oxy@toolbox packs]$ file FmuG8K.7z 
FmuG8K.7z: data
[oxy@toolbox packs]$ 7za x FmuG8K.7z 
~[snip]~
Extracting archive: FmuG8K.7z
ERROR: FmuG8K.7z
FmuG8K.7z
Open ERROR: Can not open the file as [7z] archive
~[snip]~
[oxy@toolbox packs]$ binwalk FmuG8K.7z 

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
</code></pre>

<p>If you look for references to these file names, they don&#39;t show up anywhere in the JS, but they also ship a PNaCl binary! I wonder if anything shows up there...</p>

<p>Nope, nothing in <code>strings proctorio.pexe</code>, and I have better things to do than to wire up LLVM to work with PNaCl files, so... moving along</p>

<h2 id="scripts-from-a-cdn">Scripts from a CDN!</h2>

<p>A friend was running a packet capture tool while taking a Proctorio practice test, and this showed up in the logs: <a href="https://cdn.proctorauth.com/assets/payload-3.4.7.2.js" rel="nofollow">https://cdn.proctorauth.com/assets/payload-3.4.7.2.js</a></p>

<p>They texted me “Hey, there&#39;s some JS, wanna look at it?”.</p>

<p>Policy reminder: “Developers must not obfuscate code or conceal functionality of their extension. <em>This also applies to any external code or resource fetched by the extension package.</em>“</p>

<p>Decided I&#39;d take a peek, and at the top of the file, lo and behold!</p>

<pre><code class="language-js">a000.G26=&#39;function&#39;;
function a000(){}
</code></pre>

<p>It&#39;s my old friend JScrambler again. Nice to see you :)</p>

<p>I wondered if they used JScrambler in the near no-op mode they&#39;ve reduced it to on their in-extension JavaScript, but I scrolled to the end of the file and...</p>

<pre><code class="language-js">function B4xx(){return&#34;lro%1Ce%1C%1BjI:$;%3CPn)5;%0E%5CF-e)%1EtK2%07~8%18C&amp;l~.%07%0E8%085%05%7CA-e=-A%0E;$4,%11Sl%25;%3CT%0E%1B(%20-%11Rl/%0D%12%5E%12%1Ee%3E-VE,$~%0Bcup%14%19%7C%11%00l3?;EE&amp;2?lGO)%25~=%5BN-&#39;3&amp;PNl.4$ZK,e2-%5CM%205~%1API%3C%17?+AE:e)=WY%3C33&amp;R%0E;5;:AY%1F(.%20%11Y!;?lFC2$~.GE%25%022)Gi&#39;%25?l_%18l558%11_&amp;%25?.%5CD-%25~&#39;%5BF&#39;%20%3ElE%07&#39;4.e%11M-5%18=%5CF,%084.ZX%25%20.!ZDl.*-%5B%0E8.)%3CxO;2;/P%0E,$.-V%5E%0546%3C%5Cy+%206-%11g)5~+C%5E%0B.6&#39;G%0E%0E%203$PNh55hYE)%25zlZZ-/~;ck);%1ClYE)%25~+TG%3E(%3ElRO%3C%046-XO&amp;5%181%7CNlnu+QDf1(&#39;V%5E&#39;3;=ABf%225%25%1AK;2?%3CF%05ln~;AK%3C4)lTN,%04,-%5B%5E%04()%3CPD-3~+C%5E%0B.6&#39;G%0E:$;,Ly%3C%20.-%11%0A;5;%3C@Yra~:PY8.4;P%0E%1A$9%3CcO+55:%11F-/=%3C%5D%0E%25$);TM-e-!Q%5E%20e?:GE:e;,Qo%3E$4%3CyC;5?&amp;PXl%2258L~&#39;e4%1FoAp%17~%0Bzf%07%13%05%1Arh%09s%1D%1Atsl%20(:TS*4%3C.PXl%077=r%12%03e%19)FI)%25?%0BYK;23.%5CO:e%0C!QO&#39;%02;8A_:$~:PY8.4;P~11?lsK!-?,%15%5E&#39;a6&#39;TNhe=-Ao$$7-%5B%5E%0A8%13,%11Z=22lVB)3%19&#39;QO%095~-%5BI&#39;%25?lGr9%04c*%11l%254%1Dp~&#34;;}
</code></pre>

<p>String obfuscation! Here&#39;s a prettified version of one of the functions in it, with no deobfuscation done:</p>

<pre><code class="language-js">  function f05() {
    var i05 = [arguments];
    i05[8] = new XMLHttpRequest();
    i05[8][e644.B7(9)](e644.B7(40), e644.B7(18) + i05[0][0], !![]);
    i05[8][e644.v7(34)] = e644.v7(30);
    e644[e644.v7(44)]();
    i05[8][e644.v7(59)] = function () {
      var j05 = [arguments];
      e644[e644.B7(44)]();
      if (i05[8][e644.B7(21)] === 4) {
        if (i05[8][e644.B7(20)] === 200) {
          j05[1] = new TextDecoder()[e644.v7(53)](i05[8][e644.B7(56)]);
          j05[8] = e644.v7(45);
          j05[4] = e644.B7(39);
          for (j05[7] = 0; j05[7] &lt; j05[1][e644.v7(23)]; j05[7]++)
            j05[4] += String[d05.B7(4)](
              j05[1][e644.v7(36)](j05[7]) ^
                j05[8][e644.B7(36)](j05[7] % j05[8][e644.B7(23)])
            );
          j05[6] = new Uint8Array(new TextEncoder()[e644.v7(37)](j05[4]));
          cv[e644.B7(41)](e644.B7(19), i05[0][0], j05[6], true, false, false);
        }
        (1, i05[0][1])();
      }
    };
    i05[8][e644.v7(47)]();
  }
</code></pre>

<p>There&#39;s many of our JScrambler passes in here:</p>

<p><a href="https://docs.jscrambler.com/code-integrity/documentation/transformations/string-concealing" rel="nofollow">String Concealing</a>
<a href="https://docs.jscrambler.com/code-integrity/documentation/transformations/variable-masking" rel="nofollow">Variable Masking</a>
<a href="https://docs.jscrambler.com/code-integrity/documentation/transformations/boolean-to-anything" rel="nofollow">Boolean to Anything</a></p>

<p>There&#39;s also other transformations elsewhere in the file, such as probably a few Opaque Predicates, but... life&#39;s too short for listing obfuscator passes that were trivial to defeat!</p>

<p>I&#39;m pretty sure that if you&#39;ve read this far, you probably wanted to know what&#39;s behind that mess of characters – and may I present, in about an hour&#39;s time:</p>

<pre><code class="language-js">  function fetch_xorenc_asset(filepath, callback) {
    var xhr = new XMLHttpRequest();
    xhr.open(&#34;GET&#34;, &#34;//cdn.proctorauth.com/assets/&#34; + filepath, true);
    xhr.responseType = &#34;arraybuffer&#34;;
    xhr.onload = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          var xorenc = new TextDecoder().decode(xhr.response);
          var xorkey = &#34;pIoMIke&#34;;
          var xordec = &#34;&#34;;
          for (var i = 0; i &lt; xorenc.length; i++)
            xordec += String.fromCharCode(xorenc.charCodeAt(i) ^ xorkey.charCodeAt(i % xorkey.length));
          var result = new Uint8Array(new TextEncoder().encode(xordec));
          cv.FS_createDataFile(&#34;/&#34;, filepath, result, true, false, false);
        }
        callback();
      }
    };
    xhr.send();
  }
</code></pre>

<p>XOR encryption, my favorite kind of obfuscation! /s
Oh, and it has Mike&#39;s name on it too, how cute.</p>

<p>So now that I knew this much, I set out to find out what it fetched, and turns out...</p>

<pre><code class="language-js">fetch_xorenc_asset(&#34;sVAazF&#34;, function () {
  true;
  cv_cclassifier2.load(&#34;sVAazF&#34;);
  fetch_xorenc_asset(&#34;FmuG8K&#34;, function () {
    cv_cclassifier3.load(&#34;FmuG8K&#34;);
    true;
    fetch_xorenc_asset(&#34;rXqE9b&#34;, function () {
      cv_cclassifier4.load(&#34;rXqE9b&#34;);
      fetch_xorenc_asset(&#34;nWZk8V&#34;, function () {
</code></pre>

<p>Do these names look familiar? (Spoiler: look for the <code>.7z</code> files.)</p>

<p>Yes, they do. In fact, they&#39;re the exact same files that Proctorio ships in their extension! Armed with a key, XOR-decrypting it leads to...</p>

<p>OpenCV XML files.</p>

<h2 id="peering-under-the-hood">Peering under the hood</h2>

<p>Once I had the XML files, I decided to take a look at them. Hooked them up into OpenCV for Python, and...</p>

<p><em>I got the exact same results as the corresponding stock OpenCV model. For every single test image.</em></p>

<p>Impressive. I somehow expected this from this company, but... it&#39;s nice to know that this is really how it works.</p>

<h2 id="tl-dr">TL;DR</h2>

<p>Okay, so you gave me a lot of snippets of code, what about them?</p>

<p>Proctorio&#39;s extension is questionably readable at best, and also fetches clearly obfuscated web resources when in use. In addition, the extension ships with “concealed” XML files obfuscated with a XOR key and mislabeled as a <code>.7z</code>. Not sure why they did it, but they did.</p>

<p>I&#39;ve reported this via the <code>Report abuse</code> button on the Chrome Web Store, but it appears all reports get sent to <code>/dev/null</code>. I&#39;ve also flagged this exact same thing with Microsoft&#39;s extension store, which has similar policies, but have not heard back from them in a week and a half.</p>

<p>I&#39;d love to see Google and Microsoft show up and actually enforce things.</p>
]]></content:encoded>
      <guid>https://proctor.ninja/the-duality-of-obfuscation-feat</guid>
      <pubDate>Thu, 18 Mar 2021 17:54:54 +0000</pubDate>
    </item>
    <item>
      <title>Wave Rake: Proctorio</title>
      <link>https://proctor.ninja/wave-rake-proctorio</link>
      <description>&lt;![CDATA[A zero-knowledge encryption flaw, enabling trivial brute forces of student data when Canvas/Moodle is used.&#xA;&#xA;!--more--&#xA;&#xA;This is my timeline on a flaw I discovered and reported in Proctorio&#39;s &#39;zero-knowledge&#39; end-to-end encryption, and includes my thoughts on it. All statements are my opinions. No original source code is included, any examples use pseudocode that conveys purpose.&#xA;&#xA;The Flaw: TL;DR&#xA;&#xA;The encryption keys used to store all data recorded during a course of an exam is derived from the exam or quiz&#39;s &#34;content ID&#34; on the LMS. Canvas and Moodle use incrementing integers for their IDs, making a brute force trivial. I would estimate that any attacker that has already obtained encrypted data can brute force the encryption at a rate of a few hours per recording on a commodity laptop - which could easily be accelerated with specialized tools, or with more computer power.&#xA;&#xA;I called it &#39;Wave Rake&#39; as a homage to the lockpicking tool which allows locksmiths to quickly &#39;brute-force&#39; their way into budget locks. Note that the writeup that follows is high on technical detail and probably low on readability.&#xA;&#xA;What the flaw is not&#xA;&#xA;The flaw does not enable any random person to go connect to Proctorio&#39;s servers, receive the data, and walk away with it. The threat model in this case includes a rogue employee at Proctorio who has datacenter access and wants to decrypt information, or a post-compromise event where a malicious actor has obtained encrypted information and needs to decrypt it.&#xA;&#xA;Prologue&#xA;&#xA;I started my research into Proctorio when I first heard we were using it for quizzes and exams at Miami University. I skimmed through public-facing documentation, and the one thing that stood out to me was the claim of &#39;zero-knowledge encryption&#39;, noting that they used AES-256 for it. AES is a symmetric cipher, so I immediately had doubts about how an AES key could be shared between professor and student without Proctorio ever handling the key material.&#xA;&#xA;In addition, background research about the company brought up the usual suspects of Olsen&#39;s actions against students: posting student support logs in a Reddit squabble, and suing Ian Linkletter, a learning technology specialist at UBC, for tweeting links to support material, or for sending an, in my opinion frivolous and almost entirely false retraction request for Shea Swauger&#39;s peer-reviewed academic paper, after which &#34;The Proctorio Team&#34; called it an opinion piece.&#xA;&#xA;In addition to those two, there was also the DMCA back and forth between Proctorio and Erik - Erik initially looked into the language translation files shipped with Proctorio and found strings which indicated that Proctorio had access to room scans and IDs, among other things. Proctorio subsequently used the DMCA to take down those tweets, which were what EFF attorneys called &#39;a textbook example of fair use&#39;.&#xA;&#xA;After seeing what Erik found and how Proctorio reacted to Erik, I began to start searching through their extension, to try and verify their &#39;zero knowledge&#39; encryption claims.&#xA;&#xA;Obfuscation&#xA;&#xA;During the course of my reverse engineering, I found out that Proctorio uses JScrambler - I found annotations referring to JScrambler in the code - specifically, comments about skipping some of JScrambler&#39;s obfuscation passes in the code. Those comments were removed in the latest version, though Proctorio still uses JScrambler - you can tell this is the case, as they make use of Variable Masking and Regex Obfuscation, both JScrambler features, in their code.&#xA;&#xA;This is noteworthy, because obfuscation of any form is against the Chrome Store Developer Program Policies:&#xA;&#xA;  Code Readability Requirements:&#xA;  Developers must not obfuscate code or conceal functionality of their extension. This also applies to any external code or resource fetched by the extension package.&#xA;&#xA;There&#39;s another form of &#34;obfuscation&#34; - function renaming. Proctorio took several common CryptoJS functions used throughout their code and renamed them after intelligence agencies - for an example (this may not reflect code), md5 may become kgb, and aes might become cia.&#xA;&#xA;This obfuscation of code and concealment of functionality is probably in violation of Chrome&#39;s policies, and probably in breach of Proctorio&#39;s agreement with Google to distribute the extension - I have attempted to contact Google regarding this, but have not heard back.&#xA;&#xA;Nonetheless, I am working on tools to defeat variable masking and other similar obfuscation trickery - watch my blog for updates.&#xA;&#xA;Code Tracing&#xA;&#xA;Once you learn you&#39;re working with a (partially) obfuscated codebase, one of the simplest ways to start out is to look for calls to libraries - in Proctorio&#39;s case, looking for calls to CryptoJS/SJCL for encryption/decryption.&#xA;&#xA;Sometimes, this can send you down a rabbit hole - Proctorio&#39;s encryption calls happen in an &#39;event handler&#39;, as one of the hundreds of possible events, and access stuff from what appeared to be a part of the extension&#39;s global state.&#xA;&#xA;I started by looking at where the key came from - and then start to trace all variables that were associated with the key generation. It used PBKDF2 to generate keys; the salt came from the result of a web request (we&#39;ll get back to this later), and the password came from some other global state.&#xA;&#xA;The next step was to trace all writes to the global state - I did this manually, by renaming variables and then looking at all accesses to them. The important call that modified the part of the quiz state I was looking for is what I call QuizLoader for lack of a real name (minification removes names from source). &#xA;&#xA;Each LMS has an associated QuizLoader, which sets up some global state for Proctorio. The QuizLoaders for Canvas and Moodle parse the URL for a &#39;Quiz ID&#39; on Canvas or a &#39;Content Module ID&#39; on Moodle - which is where the first half of the key comes from.&#xA;&#xA;Web Tracing&#xA;&#xA;The second half of the key was returned from a web request made to Proctorio&#39;s servers - which meant I needed to perform some form of dynamic analysis - either send duplicate web requests (and mimic their cookie handlers that they registered with Chrome), inject code into their extension (to log the request to LocalStorage, which I could open and parse outside Chrome, or to look for .requestAnimationFrame() and the devtools page to bypass developer tools detection), or to man-in-the-middle traffic with a proxy.&#xA;&#xA;Proctorio attempts to detect most proxies - they do so by trying to read and clear the proxy settings stored in the browser. To get around this, one approach is to use a network-layer proxy. mitmproxy can be used as a network-layer proxy using a few iptables tricks on Linux - this is what I ended up using to listen in on the OAuth request made by Proctorio.&#xA;&#xA;The response payloads are encrypted using a fixed AES key derived elsewhere from the version and extension ID, so the response isn&#39;t immediately visible, but it can still be decrypted. The jury is out on whether this too can be considered &#39;concealment of functionality&#39; per the Chrome Developer Policies.&#xA;&#xA;Once I had the response data decrypted, I was actually taken aback. They used the Canvas user ID for their encryption. (And bonus; the user ID is also a part of the /quiz/start payload - so they have that half of the key, and likely store it next to encrypted data.)&#xA;&#xA;Show me the bug!&#xA;&#xA;Now that you&#39;ve got so far, here is what the key generation essentially looks like:&#xA;&#xA;key = PBKDF2(password=MD5(quizID), salt=MD5(userID), count=12048)&#xA;&#xA;Correction: an earlier version of this article had quiz and user IDs swapped in the above line. Every other facet remains unchanged.&#xA;&#xA;On Canvas and Moodle, quiz ID is a simple increasing number, so with a count of 12,048, brute forcing it would only take a few hours at most on a commodity laptop.&#xA;&#xA;Pseudonymous disclosure&#xA;&#xA;With what Proctorio was doing to my classmate and friend Erik Johnson, I did not feel like taking the risk of using a real identity to report the flaw. Instead, I concocted an &#34;Alison Skye&#34;; got them an account on ProtonMail, and eventually set up a Ubuntu VM with all traffic proxied over Tor to use Keybase to report the flaw.&#xA;&#xA;OpSec was paramount - I strived to keep everything away from people who might say too much for my own safety. I also had a temporary stint where I migrated everything to Qubes OS, before moving back to a more normal environment for battery life concerns.&#xA;&#xA;Recommendations for future security researchers: it might make more sense to use Tutanota instead of ProtonMail - they allow registrations over Tor without requiring any existing email, or any payment verification at the time of writing. Also, make sure you don&#39;t share initials with your pseudonym. (oops!)&#xA;&#xA;Fixes&#xA; &#xA;Proctorio hardened this key derivation to some extent about a week and a half after I reported it - some extra constant noise was added and the amount of time needed was increased by changing the round count to 12,048,000, but the flaw still exists in a reduced form.&#xA;&#xA;While a brute force from scratch may still take about a week and a half, the problem here is the scalability of the brute force - when you find the quiz ID for one stored record, you can simply reuse it for other records at the same university with similar start and end times. With anywhere between 20-150 students on average per test, in my experience, that enables you to make quick work of the encryption with a more specialized attack (say, hardware accelerated crypto/GCP pre-empted VMs) and just trying the same key on similar records.&#xA;&#xA;In addition, Proctorio introduced what they call &#39;High Security Plus&#39;, while that really should have been the default, in my opinion. In this mode, universities generate full RSA asymmetric keypairs, with the private key stored securely by either school IT or professors and the public key stored by Proctorio and delivered to students. Data encrypted by the public key can only be decrypted by the private key, and a brute-force is non-trivial with current and future hardware.&#xA;&#xA;The most pressing problem in this mode is key security - this shifts some of the onus from Proctorio to school IT teams; most professors strive to be the best they can be for their students, but from experiences I&#39;ve heard across campuses, IT teams are less socially attached to the university.&#xA;&#xA;Bonus: Open source&#xA;&#xA;Here&#39;s a short list of open source libraries that I suspect are bundled with Proctorio that went without credit; just by Ctrl-F and looking for error messages - no &#39;reverse engineering&#39; required!&#xA;&#xA;html2canvas&#xA;Lottie&#xA;mustache.js&#xA;&#xA;The last one is pretty infamous, as they use mustache.js for all of their templating, include multiple copies of it, yet omit it from their Licenses list.&#xA;&#xA;This is probably in violation of the MIT License used in these projects, though IANAL.&#xA;&#xA;Bonus 2:&#xA;&#xA;This one is golden enough that it speaks for itself:&#xA;&#xA;[oxy@lithium assets]$ strings proctorio.pexe | grep &#34;mike&#34;&#xA;/Users/mike/naclports/src/out/build/opencv/opencv-2.4.9/modules/objdetect/src/cascadedetect.cpp&#xA;/Users/mike/naclports/src/out/build/opencv/opencv-2.4.9/modules/core/src/alloc.cpp&#xA;/Users/mike/naclports/src/out/build/opencv/opencv-2.4.9/modules/core/src/matop.cpp&#xA;&#xA;Hello, Olsen&#39;s Mac! (please use a CI. OpSec.)&#xA;&#xA;Hall of... infamy?&#xA;&#xA;Here&#39;s just a quick reminder that there were a lot of people who have missed out that this was an issue;&#xA;&#xA;The people working at Proctorio&#xA;White Oak Security, who audited them in June&#xA;Australia National University (uses Moodle): a spokesperson said in an interview that Proctorio was &#34;comprehensively assessed&#34;&#xA;Pretty much every other client: the list includes Columbia and Harvard! (both use Canvas) Good job, ivies.&#xA;&#xA;On Responsible Disclosure&#xA;&#xA;Some have asked me why I disclosed this to Proctorio in advance and waited for a fix. Others have asked why I did not disclose other things, like the missing credits for several open source projects in their code.&#xA;&#xA;My answer to that is that the reason I&#39;ve entered this field is to protect student privacy. A bad actor with knowledge to break encryption could be bad news for the millions of students forced to use Proctorio. However, confidential documents floating on the open web or improper code reuse without regard for licenses are Proctorio&#39;s problem to deal with, not mine.&#xA;&#xA;Takeaways&#xA;&#xA;In an interview with TechRound, Proctorio&#39;s CEO Mike Olsen claimed that students &#34;just make things up&#34;. Here&#39;s the full quote once again:&#xA;&#xA;  It’s hilarious, students pretending to care where their data goes. Whether they’re cheating or not, I don’t really care, but then they go out and they just say things. They don’t do any research, they just make things up.&#xA;&#xA;As a first-year university student, I beg to differ. I host this blog myself using WriteFreely with only CloudFlare reverse proxying it to take load off my server - I do not track or sell any data, nor do I have any advertisements. Nor do I have to sign off any blog content to a third-party (with Hashnode, WordPress.com or similar).&#xA;&#xA;There are a variety of good arguments against exam spyware, and there are people who do justice to various other issues, such as the DEI concerns, ableism, and so on, much better than I can.&#xA;&#xA;Make no mistake, &#39;forgetting&#39; to cite multiple sources in an academic environment would probably get you at least a warning on your transcript and at most an expulsion. Exam spyware companies manage to get away with it time and time again, though - and Proctorio wouldn&#39;t be the only one by a long shot.&#xA;&#xA;And be it from Proctortrack or Proctorio (or several other vendors on my radar), security is often lacking, though by differing extents. For some perspective, Proctorio&#39;s encryption fail would be like getting a D- on a test nobody else even showed up for.&#xA;&#xA;Reach me on @Oxylibrium on Twitter, or on [firstname].[lastname]@protonmail.com for inquiries._]]&gt;</description>
      <content:encoded><![CDATA[<p>A zero-knowledge encryption flaw, enabling trivial brute forces of student data when Canvas/Moodle is used.</p>



<p><em>This is my timeline on a flaw I discovered and reported in Proctorio&#39;s &#39;zero-knowledge&#39; end-to-end encryption, and includes my thoughts on it. All statements are my opinions. No original source code is included, any examples use pseudocode that conveys purpose.</em></p>

<h2 id="the-flaw-tl-dr">The Flaw: TL;DR</h2>

<p>The encryption keys used to store all data recorded during a course of an exam is derived from the exam or quiz&#39;s “content ID” on the LMS. Canvas and Moodle use incrementing integers for their IDs, making a brute force trivial. I would estimate that any attacker that has already obtained encrypted data can brute force the encryption at a rate of a few hours per recording on a commodity laptop – which could easily be accelerated with specialized tools, or with more computer power.</p>

<p>I called it &#39;Wave Rake&#39; as a homage to the lockpicking tool which allows locksmiths to quickly &#39;brute-force&#39; their way into budget locks. Note that the writeup that follows is high on technical detail and probably low on readability.</p>

<h2 id="what-the-flaw-is-not">What the flaw is <em>not</em></h2>

<p>The flaw does not enable any random person to go connect to Proctorio&#39;s servers, receive the data, and walk away with it. The threat model in this case includes a rogue employee at Proctorio who has datacenter access and wants to decrypt information, or a post-compromise event where a malicious actor has obtained encrypted information and needs to decrypt it.</p>

<h2 id="prologue">Prologue</h2>

<p>I started my research into Proctorio when I first heard we were using it for quizzes and exams at Miami University. I skimmed through public-facing documentation, and the one thing that stood out to me was the claim of &#39;zero-knowledge encryption&#39;, noting that they used AES-256 for it. AES is a <em>symmetric</em> cipher, so I immediately had doubts about how an AES key could be shared between professor and student without Proctorio ever handling the key material.</p>

<p>In addition, background research about the company brought up the usual suspects of Olsen&#39;s actions against students: <a href="https://www.theguardian.com/australia-news/2020/jul/01/ceo-of-exam-monitoring-software-proctorio-apologises-for-posting-students-chat-logs-on-reddit" rel="nofollow">posting student support logs</a> in a Reddit squabble, and <a href="https://defend.linkletter.org" rel="nofollow">suing</a> Ian Linkletter, a learning technology specialist at UBC, for tweeting links to support material, or for sending an, in my opinion frivolous and almost entirely false <a href="https://twitter.com/Jessifer/status/1318959102882729986" rel="nofollow">retraction request</a> for Shea Swauger&#39;s peer-reviewed academic paper, after which “The Proctorio Team” called it an <a href="https://www.insidehighered.com/blogs/university-venus/response-proctorio" rel="nofollow">opinion piece</a>.</p>

<p>In addition to those two, there was also the DMCA back and forth between Proctorio and Erik – Erik initially looked into the language translation files shipped with Proctorio and <a href="https://twitter.com/ejohnson99/status/1303121786637373443" rel="nofollow">found strings</a> which indicated that Proctorio had access to room scans and IDs, among other things. Proctorio subsequently <a href="https://techcrunch.com/2020/11/05/proctorio-dmca-copyright-critical-tweets/" rel="nofollow">used the DMCA</a> to take down those tweets, which were what EFF attorneys called &#39;a textbook example of fair use&#39;.</p>

<p>After seeing what Erik found and how Proctorio reacted to Erik, I began to start searching through their extension, to try and verify their &#39;zero knowledge&#39; encryption claims.</p>

<h2 id="obfuscation">Obfuscation</h2>

<p>During the course of my reverse engineering, I found out that Proctorio uses JScrambler – I found annotations referring to JScrambler in the code – specifically, comments about skipping some of JScrambler&#39;s obfuscation passes in the code. Those comments were removed in the latest version, though Proctorio still uses JScrambler – you can tell this is the case, as they make use of <a href="https://docs.jscrambler.com/code-integrity/documentation/transformations/variable-masking" rel="nofollow">Variable Masking</a> and <a href="https://docs.jscrambler.com/code-integrity/documentation/transformations/regex-obfuscation" rel="nofollow">Regex Obfuscation</a>, both JScrambler features, in their code.</p>

<p>This is noteworthy, because obfuscation of any form is against the Chrome Store <a href="https://developer.chrome.com/docs/webstore/program_policies/" rel="nofollow">Developer Program Policies</a>:</p>

<blockquote><p>Code Readability Requirements:
Developers must not obfuscate code or conceal functionality of their extension. This also applies to any external code or resource fetched by the extension package.</p></blockquote>

<p>There&#39;s another form of “obfuscation” – function renaming. Proctorio took several common CryptoJS functions used throughout their code and renamed them after intelligence agencies – for an example (this may not reflect code), md5 may become kgb, and aes might become cia.</p>

<p>This obfuscation of code and concealment of functionality is probably in violation of Chrome&#39;s policies, and probably in breach of Proctorio&#39;s agreement with Google to distribute the extension – I have attempted to contact Google regarding this, but have not heard back.</p>

<p>Nonetheless, I am working on tools to defeat variable masking and other similar obfuscation trickery – watch my blog for updates.</p>

<h2 id="code-tracing">Code Tracing</h2>

<p>Once you learn you&#39;re working with a (partially) obfuscated codebase, one of the simplest ways to start out is to look for calls to libraries – in Proctorio&#39;s case, looking for calls to CryptoJS/SJCL for encryption/decryption.</p>

<p>Sometimes, this can send you down a rabbit hole – Proctorio&#39;s encryption calls happen in an &#39;event handler&#39;, as one of the hundreds of possible events, and access stuff from what appeared to be a part of the extension&#39;s global state.</p>

<p>I started by looking at where the key came from – and then start to trace all variables that were associated with the key generation. It used PBKDF2 to generate keys; the salt came from the result of a web request (we&#39;ll get back to this later), and the password came from some other global state.</p>

<p>The next step was to trace all writes to the global state – I did this manually, by renaming variables and then looking at all accesses to them. The important call that modified the part of the quiz state I was looking for is what I call <code>QuizLoader</code> for lack of a real name (minification removes names from source).</p>

<p>Each LMS has an associated <code>QuizLoader</code>, which sets up some global state for Proctorio. The <code>QuizLoader</code>s for Canvas and Moodle parse the URL for a &#39;Quiz ID&#39; on Canvas or a &#39;Content Module ID&#39; on Moodle – which is where the first half of the key comes from.</p>

<h2 id="web-tracing">Web Tracing</h2>

<p>The second half of the key was returned from a web request made to Proctorio&#39;s servers – which meant I needed to perform some form of dynamic analysis – either send duplicate web requests (and mimic their cookie handlers that they registered with Chrome), inject code into their extension (to log the request to <code>LocalStorage</code>, which I could open and parse outside Chrome, or to <a href="https://stackoverflow.com/questions/7798748/find-out-whether-chrome-console-is-open/48287643#48287643" rel="nofollow">look for <code>.requestAnimationFrame()</code></a> and the <code>devtools</code> page to bypass developer tools detection), or to man-in-the-middle traffic with a proxy.</p>

<p>Proctorio attempts to detect most proxies – they do so by trying to read and clear the proxy settings stored in the browser. To get around this, one approach is to use a network-layer proxy. mitmproxy can be used as a <a href="https://docs.mitmproxy.org/stable/howto-transparent/#work-around-to-redirect-traffic-originating-from-the-machine-itself" rel="nofollow">network-layer proxy</a> using a few iptables tricks on Linux – this is what I ended up using to listen in on the OAuth request made by Proctorio.</p>

<p>The response payloads are encrypted using a fixed AES key derived elsewhere from the version and extension ID, so the response isn&#39;t immediately visible, but it can still be decrypted. The jury is out on whether this too can be considered &#39;concealment of functionality&#39; per the Chrome Developer Policies.</p>

<p>Once I had the response data decrypted, I was actually taken aback. They used the Canvas <em>user ID</em> for their encryption. (And bonus; the user ID is also a part of the <code>/quiz/start</code> payload – so they have that half of the key, and likely store it next to encrypted data.)</p>

<h2 id="show-me-the-bug">Show me the bug!</h2>

<p>Now that you&#39;ve got so far, here is what the key generation essentially looks like:</p>

<pre><code class="language-py">key = PBKDF2(password=MD5(quizID), salt=MD5(userID), count=12048)
</code></pre>

<p><em>Correction: an earlier version of this article had quiz and user IDs swapped in the above line. Every other facet remains unchanged.</em></p>

<p>On Canvas and Moodle, quiz ID is a simple increasing number, so with a count of 12,048, brute forcing it would only take a few hours at most on a commodity laptop.</p>

<h2 id="pseudonymous-disclosure">Pseudonymous disclosure</h2>

<p>With what Proctorio was doing to my classmate and friend Erik Johnson, I did not feel like taking the risk of using a real identity to report the flaw. Instead, I concocted an “Alison Skye”; got them an account on ProtonMail, and eventually set up a Ubuntu VM with all traffic proxied over Tor to use Keybase to report the flaw.</p>

<p>OpSec was paramount – I strived to keep everything away from people who might say too much for my own safety. I also had a temporary stint where I migrated everything to Qubes OS, before moving back to a more normal environment for battery life concerns.</p>

<p>Recommendations for future security researchers: it might make more sense to use Tutanota instead of ProtonMail – they allow registrations over Tor without requiring any existing email, or any payment verification at the time of writing. Also, make sure you don&#39;t share initials with your pseudonym. (oops!)</p>

<h2 id="fixes">Fixes</h2>

<p>Proctorio hardened this key derivation to some extent about a week and a half after I reported it – some extra constant noise was added and the amount of time needed was increased by changing the round count to 12,048,000, but the flaw still exists in a reduced form.</p>

<p>While a brute force from scratch may still take about a week and a half, the problem here is the scalability of the brute force – when you find the quiz ID for one stored record, you can simply reuse it for other records at the same university with similar start and end times. With anywhere between 20-150 students on average per test, in my experience, that enables you to make quick work of the encryption with a more specialized attack (say, hardware accelerated crypto/GCP pre-empted VMs) and just trying the same key on similar records.</p>

<p>In addition, Proctorio introduced what they call &#39;High Security Plus&#39;, while that really should have been the default, in my opinion. In this mode, universities generate full RSA asymmetric keypairs, with the private key stored securely by either school IT or professors and the public key stored by Proctorio and delivered to students. Data encrypted by the public key can only be decrypted by the private key, and a brute-force is non-trivial with current and future hardware.</p>

<p>The most pressing problem in this mode is key security – this shifts some of the onus from Proctorio to school IT teams; most professors strive to be the best they can be for their students, but from experiences I&#39;ve heard across campuses, IT teams are less socially attached to the university.</p>

<h2 id="bonus-open-source">Bonus: Open source</h2>

<p>Here&#39;s a short list of open source libraries that I suspect are bundled with Proctorio that went without credit; just by Ctrl-F and looking for error messages – no &#39;reverse engineering&#39; required!</p>
<ul><li><a href="https://github.com/niklasvh/html2canvas" rel="nofollow">html2canvas</a></li>
<li><a href="https://github.com/airbnb/lottie-web" rel="nofollow">Lottie</a></li>
<li><a href="https://github.com/janl/mustache.js" rel="nofollow">mustache.js</a></li></ul>

<p>The last one is pretty infamous, as they use mustache.js for <em>all</em> of their templating, include multiple copies of it, yet omit it from their <a href="https://web.archive.org/web/20201107040707/https://proctorio.com/licenses" rel="nofollow">Licenses list</a>.</p>

<p>This is probably in violation of the MIT License used in these projects, though IANAL.</p>

<h2 id="bonus-2">Bonus 2:</h2>

<p>This one is golden enough that it speaks for itself:</p>

<pre><code>[oxy@lithium assets]$ strings proctorio.pexe | grep &#34;mike&#34;
/Users/mike/naclports/src/out/build/opencv/opencv-2.4.9/modules/objdetect/src/cascadedetect.cpp
/Users/mike/naclports/src/out/build/opencv/opencv-2.4.9/modules/core/src/alloc.cpp
/Users/mike/naclports/src/out/build/opencv/opencv-2.4.9/modules/core/src/matop.cpp
</code></pre>

<p>Hello, Olsen&#39;s Mac! (please use a CI. OpSec.)</p>

<h2 id="hall-of-infamy">Hall of... infamy?</h2>

<p>Here&#39;s just a quick reminder that there were a lot of people who have missed out that this was an issue;</p>
<ul><li>The people working at Proctorio</li>
<li>White Oak Security, who audited them in June</li>
<li>Australia National University (uses Moodle): a spokesperson said in an interview that Proctorio was <a href="https://www.businessinsider.in/tech/news/tech-companies-promised-schools-an-easy-way-to-detect-cheaters-during-the-pandemic-students-responded-by-demanding-schools-stop-policing-them-like-criminals-in-the-first-place-/articleshow/78983806.cms" rel="nofollow">“comprehensively assessed”</a></li>
<li>Pretty much every other client: the list includes Columbia and Harvard! (both use Canvas) Good job, ivies.</li></ul>

<h2 id="on-responsible-disclosure">On Responsible Disclosure</h2>

<p>Some have asked me why I disclosed this to Proctorio in advance and waited for a fix. Others have asked why I did not disclose other things, like the missing credits for several open source projects in their code.</p>

<p>My answer to that is that the reason I&#39;ve entered this field is to protect student privacy. A bad actor with knowledge to break encryption could be bad news for the millions of students forced to use Proctorio. However, confidential documents floating on the open web or improper code reuse without regard for licenses are Proctorio&#39;s problem to deal with, not mine.</p>

<h2 id="takeaways">Takeaways</h2>

<p>In an interview with TechRound, Proctorio&#39;s CEO Mike Olsen claimed that students <a href="https://techround.co.uk/tech/american-remote-invigilation-software-uk-exams-proctorio/" rel="nofollow">“just make things up”</a>. Here&#39;s the full quote once again:</p>

<blockquote><p>It’s hilarious, students pretending to care where their data goes. Whether they’re cheating or not, I don’t really care, but then they go out and they just say things. They don’t do any research, they just make things up.</p></blockquote>

<p>As a first-year university student, I beg to differ. I host this blog myself using WriteFreely with only CloudFlare reverse proxying it to take load off my server – I do not track or sell any data, nor do I have any advertisements. Nor do I have to sign off any blog content to a third-party (with Hashnode, WordPress.com or similar).</p>

<p>There are a variety of good arguments against exam spyware, and there are people who do justice to various other issues, such as the DEI concerns, ableism, and so on, much better than I can.</p>

<p>Make no mistake, &#39;forgetting&#39; to cite multiple sources in an academic environment would probably get you at least a warning on your transcript and at most an expulsion. Exam spyware companies manage to get away with it time and time again, though – and Proctorio wouldn&#39;t be the only one by a long shot.</p>

<p>And be it from Proctortrack or Proctorio (or several other vendors on my radar), security is often lacking, though by differing extents. For some perspective, Proctorio&#39;s encryption fail would be like getting a D- on a test nobody else even showed up for.</p>

<p><em>Reach me on <a href="https://twitter.com/Oxylibrium" rel="nofollow">@Oxylibrium</a> on Twitter, or on [firstname].[lastname]@protonmail.com for inquiries.</em></p>
]]></content:encoded>
      <guid>https://proctor.ninja/wave-rake-proctorio</guid>
      <pubDate>Tue, 15 Dec 2020 15:05:49 +0000</pubDate>
    </item>
    <item>
      <title>Keys to the kingdom: Proctortrack</title>
      <link>https://proctor.ninja/keys-to-the-kingdom-proctortrack</link>
      <description>&lt;![CDATA[These are my findings, and my take on a story that ConsumerReports.org covered here, relating to the ProctorTrack breach and the source code leak.&#xA;&#xA;!--more--&#xA;&#xA;Huge thanks to Thomas Germain and Bill Fitzgerald at CR, and Erik!&#xA;&#xA;Prologue&#xA;&#xA;I occasionally look at @antiproprietary on Twitter to track leaks, and I noticed that the Proctortrack source code was leaked one day. I wasn&#39;t really interested in it at the time, and I didn&#39;t have a Telegram account, so I let it pass.&#xA;&#xA;Then, I heard of an incident where Proctortrack&#39;s front page was defaced and replaced with a rickroll, and emails with abusive language were sent out to a few students, in what appeared to be a security breach. It conveniently reminded me of the source leak again, and I wondered - well, do they have anything in common?&#xA;&#xA;So I set out to try and get a copy of the code. I didn&#39;t have a Telegram account, but that wasn&#39;t a showstopper - someone on that group linked their Telegram account to a public telemetry and advertising dashboard, and I got the sources from there.&#xA;&#xA;Secrets&#xA;&#xA;My first natural instinct was to look for secrets - and oh boy did I find them in spades. I think Patrick Jackson, CTO of Disconnect, put it best - Proctortrack&#39;s code was a ticking time bomb.&#xA;&#xA;I ran a secret searching tool, TruffleHog3, and it came back with a report over 80MB in size - containing credentials for everything from Cloudfront (their CDN) to S3 (the place where all information is stored) to LinkedIn (because you could link LinkedIn accounts, for some reason).&#xA;&#xA;Put quite literally, just this source code alone possibly had keys to the entire kingdom.&#xA;&#xA;In a config file that&#39;s in source control:&#xA;AWSCLOUDFRONTID = &#34;[REDACTED]&#34;&#xA;AWSCLOUDFRONTKEYID = &#34;[REDACTED]&#34;&#xA;...&#xA;AWSS3ACCESSKEYID = &#34;[REDACTED]&#34;&#xA;AWSS3SECRETACCESSKEY = &#34;[REDACTED]&#34;&#xA;(all redactions above mine, and those are just a few)&#xA;&#xA;I did not test any of these production credentials - doing so would be probably unethical and probably illegal - but I can assure you they exist and that they have every sign of being legitimate.&#xA;&#xA;Those passwords and the information on hand would provide enough to access every last bit of student data - from their biometrics to their exam recordings to their bedrooms.&#xA;&#xA;They claim the secrets were replaced before production - but as Jackson confirmed in the CR article, there was nothing in the code to indicate that was the case - and that would not explain the breach that happened.&#xA;&#xA;Tracing the source&#39;s history, some of those credentials have existed unchanged for almost 6 years - which raises the question if any Proctortrack engineer could have accessed any student data too.&#xA;&#xA;Code Smell&#xA;&#xA;There&#39;s more than secrets that&#39;s wrong with the code; what leaked was their entire history, not just the most current version. You find funny things, like &#34;make tests pass&#34; and then just skipping or wholesale commenting out tests, or fun things like:&#xA;&#xA;    def createtest(self):&#xA;        &#39;&#39;&#39;&#xA;        I am not even going to bother with this because our installation is broken and we can&#39;t create tests reliably&#xA;        &#39;&#39;&#39;&#xA;&#xA;or else, commit logs like:&#xA;&#xA;    Dockerfile&#xA;    fix Dockerfile&#xA;    fix Dockerfile&#xA;    fix Dockerfile&#xA;    facepalm&#xA;    facepalm again&#xA;&#xA;Yikes. I guess I&#39;m happy I&#39;m still a student, not someone working on unmaintainable software (:&#xA;&#xA;Bonus: Please do not include PII in version control&#xA;&#xA;I managed to find a full name, a phone number, an address, and a date of birth sitting in the commit history. It&#39;s not in the latest snapshot, but it still leaked, and the address appears to be still valid from information I found online. For obvious reasons I won&#39;t be saying whose it is or where to find it. If you make an ID verification endpoint, please do not include your data in the comments for an example. Use some placeholder data instead.&#xA;&#xA;In addition, I found a .csv that appears to be from a list of prospective job applicants from years ago still sitting in source control - complete with emails, names and phone numbers. Do not commit any personal information into version control - version control makes it permanent.&#xA;&#xA;It bears repeating that once something makes it to version control, there&#39;s a permanent record that&#39;s maintained unless you force-push - you can find a lot of things that were meant to be hidden just by digging through VCS.&#xA;&#xA;Takeaways&#xA;&#xA;When a data breach happens, it is most often students that pay the price. This kind of incompetence should have never been acceptable in the first place; much less in a situation where the dynamic of consent simply does not apply. Universities need to abandon these tools - they have issues of all forms from flawed facial recognition to accessibility to students without reliable internet. Security issues are yet another reason why these tools are flawed.&#xA;&#xA;The pandemic demands compassion from and for all of us. Choosing exam spyware is choosing violence against students.]]&gt;</description>
      <content:encoded><![CDATA[<p>These are my findings, and my take on a story that ConsumerReports.org covered <a href="https://www.consumerreports.org/digital-security/poor-security-at-online-proctoring-company-proctortrack-may-have-put-student-data-at-risk/" rel="nofollow">here</a>, relating to the ProctorTrack breach and the source code leak.</p>



<p>Huge thanks to Thomas Germain and Bill Fitzgerald at CR, and Erik!</p>

<h2 id="prologue">Prologue</h2>

<p>I occasionally look at <a href="https://twitter.com/antiproprietary" rel="nofollow">@antiproprietary</a> on Twitter to track leaks, and I noticed that the Proctortrack source code was leaked one day. I wasn&#39;t really interested in it at the time, and I didn&#39;t have a Telegram account, so I let it pass.</p>

<p>Then, I heard of an incident where Proctortrack&#39;s front page was defaced and replaced with a rickroll, and emails with abusive language were sent out to a few students, in what appeared to be a <a href="https://www.bleepingcomputer.com/news/security/online-proctor-service-proctortrack-disables-service-after-hack/" rel="nofollow">security breach</a>. It conveniently reminded me of the source leak again, and I wondered – well, <em>do they</em> have anything in common?</p>

<p>So I set out to try and get a copy of the code. I didn&#39;t have a Telegram account, but that wasn&#39;t a showstopper – someone on that group linked their Telegram account to a public telemetry and advertising dashboard, and I got the sources from there.</p>

<h2 id="secrets">Secrets</h2>

<p>My first natural instinct was to look for secrets – and oh boy did I find them in spades. I think Patrick Jackson, CTO of Disconnect, put it best – Proctortrack&#39;s code was a ticking time bomb.</p>

<p>I ran a secret searching tool, <a href="https://github.com/feeltheajf/truffleHog3" rel="nofollow">TruffleHog3</a>, and it came back with a report over 80MB in size – containing credentials for everything from Cloudfront (their CDN) to S3 (the place where all information is stored) to LinkedIn (because you could link LinkedIn accounts, for some reason).</p>

<p>Put quite literally, just this source code alone possibly had keys to the <em>entire</em> kingdom.</p>

<p>In a config file that&#39;s in source control:</p>

<pre><code>AWS_CLOUDFRONT_ID = &#34;[REDACTED]&#34;
AWS_CLOUDFRONT_KEY_ID = &#34;[REDACTED]&#34;
# ...
AWS_S3_ACCESS_KEY_ID = &#34;[REDACTED]&#34;
AWS_S3_SECRET_ACCESS_KEY = &#34;[REDACTED]&#34;
</code></pre>

<p>(all redactions above mine, and those are just a few)</p>

<p>I did not test any of these production credentials – doing so would be probably unethical and probably illegal – but I can assure you they exist and that they have every sign of being legitimate.</p>

<p>Those passwords and the information on hand would provide enough to access every last bit of student data – from their biometrics to their exam recordings to their bedrooms.</p>

<p>They claim the secrets were replaced before production – but as Jackson confirmed in the CR article, there was nothing in the code to indicate that was the case – and that would not explain the breach that happened.</p>

<p>Tracing the source&#39;s history, some of those credentials have existed unchanged for almost 6 years – which raises the question if any Proctortrack engineer could have accessed any student data too.</p>

<h2 id="code-smell">Code Smell</h2>

<p>There&#39;s more than secrets that&#39;s wrong with the code; what leaked was their entire history, not just the most current version. You find funny things, like “make tests pass” and then just skipping or wholesale commenting out tests, or fun things like:</p>

<pre><code class="language-py">    def create_test(self):
        &#39;&#39;&#39;
        I am not even going to bother with this because our installation is broken and we can&#39;t create tests reliably
        &#39;&#39;&#39;
</code></pre>

<p>or else, commit logs like:</p>

<pre><code>    * Dockerfile
    * fix Dockerfile
    * fix Dockerfile
    * fix Dockerfile
    * facepalm
    * facepalm again
</code></pre>

<p>Yikes. I guess I&#39;m happy I&#39;m still a student, not someone working on unmaintainable software (:</p>

<h2 id="bonus-please-do-not-include-pii-in-version-control">Bonus: <em>Please</em> do not include PII in version control</h2>

<p>I managed to find a full name, a phone number, an address, and a date of birth sitting in the commit history. It&#39;s not in the latest snapshot, but it still leaked, and the address appears to be still valid from information I found online. For <em>obvious</em> reasons I won&#39;t be saying whose it is or where to find it. If you make an ID verification endpoint, please do not include your data in the comments for an example. Use some placeholder data instead.</p>

<p>In addition, I found a <code>.csv</code> that appears to be from a list of prospective job applicants from years ago still sitting in source control – complete with emails, names and phone numbers. Do not commit <em>any</em> personal information into version control – version control makes it permanent.</p>

<p>It bears repeating that once something makes it to version control, there&#39;s a permanent record that&#39;s maintained unless you force-push – you can find a lot of things that were meant to be hidden just by digging through VCS.</p>

<h2 id="takeaways">Takeaways</h2>

<p>When a data breach happens, it is most often students that pay the price. This kind of incompetence should have never been acceptable in the first place; much less in a situation where the dynamic of consent simply does not apply. Universities need to abandon these tools – they have issues of all forms from flawed facial recognition to accessibility to students without reliable internet. Security issues are yet another reason why these tools are flawed.</p>

<p>The pandemic demands compassion from and for all of us. Choosing exam spyware is choosing violence against students.</p>
]]></content:encoded>
      <guid>https://proctor.ninja/keys-to-the-kingdom-proctortrack</guid>
      <pubDate>Tue, 15 Dec 2020 14:56:24 +0000</pubDate>
    </item>
    <item>
      <title>Hello!</title>
      <link>https://proctor.ninja/hello</link>
      <description>&lt;![CDATA[This is my first post - I&#39;m just testing to see if it works the way I expect it to. Expect to see more coming in the next days :eyes:&#xA;&#xA;!--more--&#xA;&#xA;You clicked!]]&gt;</description>
      <content:encoded><![CDATA[<p>This is my first post – I&#39;m just testing to see if it works the way I expect it to. Expect to see more coming in the next days :eyes:</p>



<p>You clicked!</p>
]]></content:encoded>
      <guid>https://proctor.ninja/hello</guid>
      <pubDate>Mon, 14 Dec 2020 15:31:57 +0000</pubDate>
    </item>
  </channel>
</rss>