Reverse engineering Cordova apps

Cordova allows you to develop cross-platform mobile apps using web technologies. Traditional way of decompiling Android apps using tools like jadx wouldn’t give you much useful information when dealing with Cordova apps, since most of the logic and interface is implemented in HTML and Javascript.

In theory web apps should be much easier to reverse engineer because there’s technically no “compiling" process, more information is preserved from the original source code. Unless the developer uses an obfuscater, the original program symbols (function names, variable names, etc) are preserved in the final app package. And, because they’re just web technologies, we should be able to just debug them using modern browsers’ amazing debugging tools.

In practice, we need a way to extract the web app part out before we can do anything with it. This doesn’t seem to be a common operation so google doesn’t give you a straight answer, but now I’ve figured it out and find it quite easy.

Note that I have no experience in Android app development, nor Cordova, so there might be more straightforward ways to do things that I don’t know. But so far these methods have served my needs well.


Using apktool or jadx we can easily extract the web part of the source code. Cordova apps usually place them under resources/assets/www.

For the app I’m working on, simply opening index.html in a normal browser will lead to it running infinite loop in Javascript.

Running in Browser

To make the app run under normal browser, I took idea from Cordova’s app development tutorial.

First create an empty app project:

cordova create hello com.example.hello HelloWorld
cd hello

Add browser and Android platform. Some plugins will fail to install if android platform wasn’t added:

cordova platform add browser
cordova platform add android

Copy the extracted source files from the app over:

cp -r ~/apk/targetapp/resources/assets/www .

Run in browser:

cordova run browser

This should allow the app to at least start initializing, though if the app uses any Cordova plugins that we haven’t installed yet there will be an error and the initialization would fail. Next step would be to install missing plugins.

Add Missing Plugins

In this step, we need to look at the error messages from the browser console to see what plugins are missing. Below is my example, but each app will use different plugins so the process will vary.

Error: Uncaught ReferenceError: device is not defined

Quick Googling gave me this answer:

cordova plugin add cordova-plugin-device

Add Missing Plugins – a better way

I noticed www/plugins/ contains all plugin files, and the directory names are conveniently the name of the plugin. So I can just use xargs to install all of them:

ls www/plugins/ | xargs cordova plugin add 

This way all plugins included in the app are installed.


If an app fails to load in browser, check browser console for error messages. This section notes all error messages I’ve encountered and the solutions.

“Could not find cordova.js script tag. Plugin loading may fail."

I noticed in index.html there was this line:

<script type="text/javascript" src="cordova.93551f4e1f7af4ca1581.js"></script>

And in www/ both and cordova.js exist.

So I simply changed the src in index.html to cordova.js and the app loaded successfully.

Analyzing API Usage

One important part of security auditing apps is to see what system APIs it accesses, under what condition, and what it does with the obtained data.

In Cordova, most system API access require plugins. The app logic in Javascript would call respective plugins’ Javascript API, and in turn call the Java part of the plugin, then from there call system Java APIs.

So to answer the above questions, here’s the high level process that I usually take:

  1. Use jadx to look for Java code that calls interesting system APIs
  2. Note down the Java package name containing the code block of interest, for example: org.apache.cordova.file.FileUtils or com.rohithvaranasi.callnumber.CFCallNumber
  3. Google for these plugins’ documentation on their Javascript API. Using the above plugins as examples, I found:
    1. cordova-plugin-file :
    2. CordovaCallNumberPlugin:
  4. Read their documentation and look for the global object name encapsulating the plugin’s functions, or the function names of interest. For example, CordovaCallNumberPlugin offers only one function here: window.plugins.CallNumber.callNumber
  5. Go back to the app’s Javascript source code, grep for use of CallNumber.callNumber


If the app wants to send any HTTP request to external servers (other than localhost, most likely due to the app calling API servers), in the browser it will fail from CORS requirements.

I didn’t investigate to fix this because I didn’t need to inspect the server response. But this should be fixable by tweaking the CORS header sent by cordova run browser .

Instagram, Youtube 的擋廣告前端網站 Bibliogram 和 Invidious

Invidious / Youtube 是一個開源的 Youtube 前端,簡單來說就是幫 Youtube 包一個新界面,然後行為追蹤和廣告全部都可以一起擋掉,網頁完全靜態不需要 Javascript。不過 2020/9/1 開始官方的站台就停止維護了。

其實知道這個專案很久了但以前不瞭解這個到底有什麼意義,因為要擋廣告的話就裝個瀏覽器插件就好了啊。不過後來才想到有些平臺上面是不能擋廣告的,像是 iOS 。雖然我相信 App Store 裡面大概也有幫你擋 Youtube 廣告的 app 啦,不過如果他幫你擋掉廣告然後 app 又免費的話肯定是要靠其他方法賺錢,如果用這類 app 的話只是從讓 Youtube 賺你錢變成讓其他公司賺你錢而已。 的另一個問題是影片最高解析度似乎只能到 720p ,雖然 Github 上面開發者的說法是可以選擇登入之後選影片品質 “dash" ,不過我不想登入。

然後在另一個連結的 issue 裡面有人貼了他開發的 Youtube 前端 CloudTube,實測可以 1080p 。所以以後我應該就用這個了。

Hacker News 的討論串裡面還有其他相關的專案。

Bibliogram / Instagram

跟隨同樣邏輯,其他各大內容網站也有類似的前端,CloudTube 的同一個作者 cadence 做了一個 Instagram 的前端 bibliogram

Bibliogram works without browser JavaScript, has no ads or tracking, and doesn’t urge you to sign up.

對於像我一個沒有 Instagram 帳號但偶爾還是會看 Instagram 的內容的人來說,不登入的狀況下看 Instagram 體驗超差,圖片不能點開,點開會叫你註冊,使用者的貼文串往下捲一捲就會跳出註冊框強迫註冊不然不能繼續捲。前者有個解法就是按右鍵然後選在新分頁開啟連結,不過後者就一定要開開發者檢閱器了,很麻煩。


為了要讓使用這些替代網站方便一些,可以設定瀏覽器自動重導向,Firefox 已經有一個 Privacy Redirect 支援 Nitter (Twitter) , Invidious (Youtube), Bibliogram (Instagram), OpenStreetMap (Google Maps),還會自動隨機選擇不同站台,這樣就不會單一站台流量太大被 Instagram 封鎖。

不過 Bibliogram 似乎被 Instagram 封鎖的情況還是很常見,我經常遇到。

Xpra: GUI 界的 screen/tmux


大部分 Linux/BSD 伺服器和桌面環境都有內建 X Forwarding 功能,一個指令 ssh -X 就可以在 headless(沒有 X display)的伺服器上執行 X 圖形化程式,然後把視窗畫面包在本地的視窗管理器裡面顯示,看起來跟在本地執行沒兩樣。剪貼簿也不用額外設定可以直接使用。

不過, X forwarding 有一個很大的缺陷: ssh 連線斷掉的時候在遠端執行的應用程式會直接中止。

Xpra 就是專門為了解決這個問題而生的,他的用途就像是給純文字界面應用程式使用的 screen 和 tmux 一樣,只是 xpra 是給圖形應用程式。

和 tmux 一樣, xpra 會在伺服器端建立 “session" ,xpra 客戶端可以 attach / detach 一個 session,一個 session 後面是一個完整的 X server 在執行,可以跑多個圖形應用程式,然後和 X forwarding 一樣,把每一個視窗都在客戶端繪製,並且和客戶端的桌面環境整合。

即使 xpra 客戶端突然斷線,應用程式仍然在遠端伺服器繼續運行。當然也可以從一個客戶端 detach ,然後在另一個客戶端 attach。


Xpra 官方建議不要用發行版的套件,建議直接加他們的套件源。另外根據坊間說法 Xpra 跨版本的相容性很差,所以要儘可能維持伺服器和客戶端是相近的版本。


xpra start ssh://SERVERUSERNAME@SERVERHOSTNAME/ --start-child=xterm

就可以開一個 Xpra session ,執行 xterm ,然後自動 attach



後面那個 1 是 session 編號,通常都是 1 開始,在 xpra start 完成之後他的訊息會寫 session 編號。如果伺服器上面只有一個 session 的話上面那個指令可以不加 session 編號。

如果真的不知道 session 編號的話可以到伺服器上面執行:

xpra list

就會列出所有 session ,如果有當掉的 session 他也會自動清除。


我通常都是先開一個 xterm 然後在 xterm 裡面開個 tmux ,再執行圖形應用程式。有時候重新 attach 之後在 xterm 裡面的 tmux 開新的 tmux window ,新的 window 會吃不到 $DISPLAY 環境變數,這個時候很簡單, detach tmux session 之後再 attach ,然後再開新的 tmux window 就正常了。


不確定是不是我個人環境的問題, Xpra 伺服器很容易當掉,當掉之後就沒辦法 attach 。不過通常即使 Xpra 伺服器當掉, Xpra 伺服器啟動的 X server 仍然還在。

解法很簡單,先 ssh 進伺服器下 xpra list 清掉當掉的 session ,然後再執行下面這個指令,就可以開新的 Xpra 伺服器,但是使用已經存在的 X server instance :

xpra start --use-display :1

接下來在客戶端正常 attach 就可以了。

在 Linux 桌面上擠出更多記憶體的各種方法、對 SWAP 常見的誤解

平時有需求平行跑一些很吃記憶體的圖形應用程式,再加上現在流行 Electron app,在 Linux 桌面上我便經常遇到界面卡頓。因此花時間研究擠出更多記憶體的方法。


講到記憶體不夠當然會馬上想到 SWAP ,但對現代的電腦和應用程式,專業觀點似乎大多認為要避免使用 SWAP 。因為 SWAP 太慢,當主記憶體吃滿,系統完全卡住的時候,才開始把資料搬到 SWAP 通常也已經來不及挽救沒有回應的系統了(例如這篇這篇)。我的經驗是在這個狀況下大部分還是只能強制重開機,只有少數狀況下系統可以在幾分鐘之內回到有反應可操作的狀態。

不過這也牽涉到兩個參數:vm.swapiness 和 oom killer 的設定 。大部分 Linux 發行版預設的 swapiness 是 60,對於桌面應用來說似乎不夠。然後預設的 OOM killer 動作時間通常都太晚,砍掉程序的選擇感覺也很奇怪(實際上砍掉的程序和使用者覺得應該要被砍掉的程序不一樣)。這部分對於桌面為主的應用應該有和伺服器應用不同的調校。

另一方面, SWAP 是非常古老的設計,當時大多數應用程式的使用方式是在 shell 的前景背景切換,也沒有像現在有這麼多應用程式是基於 GC 類型的 runtime (Javascript, Java, etc)。SWAP 的設計並不適合 GC 類型的 runtime ,因為跑在 process 裡面的 virtual machine 並不知道底層的記憶體到底是主記憶體還是 SWAP(因為這個理論上應該是作業系統要去管理的),理想上我們希望在作業系統即將把某個區塊 swap out 的時候觸發 GC ,但實際上 process 不會知道這件事情。


ZRAM 的原理是在主記憶體中割一塊空間出來,把它變成一個壓縮的 block device ,然後在上面建 SWAP 。主要適用於沒有建置 SWAP 空間的系統。


ZSWAP 是基於現有 SWAP 的改良,因此運作的前提是必須有建置 SWAP 空間。ZSWAP 的原理是去 hook kernel 裡面把 memory page swap out 的流程(這個 hook 叫做 FRONTSWAP),攔截要 swap out 的 page,然後嘗試壓縮這個 page ,如果可以壓縮,再使用 ZSWAP 特有的 page allocater ,zbud 或是 zsmalloc ,去 allocate 主記憶體上面的 physical page ,再把壓縮後的 page 放進去。以 zbud 為例,壓縮比例是固定的,2 個 virtual page 會塞進一個 physical page。如果這個 page 壓縮效果不佳,就會循正常的流程 swap out to disk。

zbud 、zsmalloc 和 kernel 裡面其他的 page allocater 的基本運作原理是一樣的,因此不像 ZRAM 是 allocate 一個固定大小的空間來放壓縮後的 page ,而是動態地配置和釋放記憶體。 ZSWAP 所使用的最大主記憶體比例是可以調整的,預設是 20% 。當這個壓縮的記憶體空間快要滿的時候,或是主記憶體耗竭的時候, ZSWAP 就會開始根據 LRU (Least Recently Used) 規則(尋找最久沒有使用的分頁)把已壓縮的 page 解壓縮後 swap out to disk 。

Mac OS X

Mac OS X 應該是主流作業系統裡面最早預設啟用記憶體壓縮的,從 10.9 版就預設啟用。在我的另一臺 Mac 筆電上即使在低可用記憶體的狀況下使用者界面仍然很流暢,我很好奇他們是怎麼做的,就做了研究,它的運作方式如下(這張投影片講得簡單明瞭):

根據研究論文:In lieu of swap: Analyzing compressed RAM in Mac OS X and Linux – ScienceDirect
投影片: pres-in_lieu_of_swap_-_analyzing_compressed_ram_in_mac_os_x_and_linux.pdf

所以看來, Mac OS X 的記憶體壓縮運作方式比較接近 ZSWAP。

KSM – Kernel Same Page Merging

要擠出更多記憶體空間,除了壓縮,另一個方法就是把資料一樣的分頁 (page) 合併只存一份,這就是 KSM 主要的想法。

目前在 mainline kernel 裡面的 KSM 設計似乎主要是給 KVM 虛擬機器使用,因為同時開很多臺一樣作業系統的 VM 顯然會有很多記憶體內容長得一樣,所以設計 KSM。

然而目前 mainline 的 KSM 功能只會去合併由應用程式特別標示「可以合併」的分頁 ,應用程式需要透過 madvise()系統呼叫將 page 標示成 MADV_MERGABLE ,KSM 才會去掃描它的內容。快速查了一下,目前除了 KVM 之外似乎沒有應用程式有使用這個功能標示。

問了一下朋友,他提到 UKSM “Ultra Kernel Samepage Merging" ,是一個第三方 kernel 模組,不像 mainline KSM 有 MADV_MERGABLE 的限制,UKSM 會主動去合併所有程序的記憶體分頁,不需要程序主動標示。

另外還有 PKSM ,說是 UKSM 的改良版,看來已經很久沒有更新了。不過現在 UKSM 的 repo 雖然看起來沒有新功能的開發,但經常會對新版的 Linux kernel rebase 他們的 code ,讓新版的也能使用。

除了上面的各種 KSM ,也有人嘗試直接提 patch 到 mainline ,像這位想讓 KSM 可以不受限於有標示的分頁。不過後來有位 Jann Horn (不確定他是不是 kernel 開發者)指出 KSM 有嚴重的安全性問題,如果讓 KSM 可以合併所有分頁的話那資安問題會變更嚴重,然後就沒有後續討論了。(不太確定為啥,如果 Jann 不是 kernel 開發者的話,那這個 patchset 就沒有被明確拒絕啊,原來 kernel 開發者都可以這樣收 patch 當作沒看到的哦?)(雖然 kernel 開發者沒有義務要回覆送進來的 patch ,但就這樣不明確拒絕人家,竟然是旗艦開源專案 Linux 會發生的事情?我孤陋寡聞了。)

後記一:關於 SWAP 的迷思

這篇快寫完的時候我發現一篇 Facebook 工程師寫的文章在講大家對 SWAP 的迷思。覺得清晰了不少,在這邊我直接翻譯他列的幾個重點,全文非常值得一讀:

  1. SWAP 是運作良好的系統當中合理且重要的元件,沒有它就難以達成良好的記憶體管理。
  2. 大致上來說 swap 並不應該被拿來當作「緊急記憶體」使用, swap 的目的是讓記憶體分頁的釋放更為公平和有效率。而且實際上,如果把 swap 當成緊急記憶體使用的話一般是有害的。
  3. 停用 swap 並不會解決可用記憶體稀缺的狀況下磁碟 IO 過慢而造成的問題(可用記憶體稀缺的時候會造成分頁不斷被逐出主記憶體然後又被載入,也就是 contention);停用 swap 只會使得磁碟頻寬從「被匿名分頁傳輸吃滿」變成「被快取分頁傳輸吃滿」。
    這樣可能比較沒效率,因為這樣我們在主記憶體當中可以取回的分頁的範圍就變小了(有 swap 的話我們可以選擇從主記憶體逐出匿名分頁和快取分頁,沒有 swap 的話我們就只能選擇逐出快取分頁)。更糟的是,因為沒有 swap ,永遠只能選擇逐出快取分頁,這可能反過來成為記憶體稀缺,然後 contention 的主因。
    (這邊對「快取分頁」的定義是「在其他儲存空間上有資料副本」,也就是 “backed by other storage" ;「匿名分頁」的定義是「在其他儲存空間上沒有資料副本,記憶體當中的是唯一一份」。因此,逐出匿名分頁需要先把資料複製到 SWAP,逐出快取分頁只要把這筆快取的參照刪除就可以了。)
  4. swap 模組在 kernel 4.0 以前有很多問題,例如會過度主動地 swap out 分頁,這造成很多人對 swap 的負面印象。 4.0 以後狀況改善很多。
  5. 使用 SSD 的狀況下,swap out (把分頁從主記憶體搬到 swap)匿名分頁,以及,重新取得快取分頁的內容,這兩件事情基本上效能和延遲差不多。使用機械磁碟的狀況下,swap 讀取因為是隨機存取所以會比較慢,所以把 vm.swappiness 調低是合理的。
  6. 停用 swap 並不會防止記憶體幾乎用完 (OOM) 的時候系統完全卡住,雖然,的確,如果有 swap 的話卡住的時間可能會比較長。無論 OOM killer 何時、在有或是沒有 swap 的狀況下開始動作,結果都一樣:系統會進入一個無法預測的狀態,有 swap 或是沒有 swap 都一樣。

原文另外有連結到一個 Facebook 的專案: Pressure Stall Information。 PSI 已經內建於 Linux kernel 4.20+ ,主要功能是提供一個簡單的針對系統資源的壓力測量。不過這部分我還沒有深入研究可以怎麼利用,看來 Facebook 他們是搭配 cgroup2 和 oomd 去分配資源。

後記二:ZSWAP 一個月使用心得

調查過以上這麼多解決方案之後,最後決定啟用 ZSWAP。有人寫了一個管理 ZSWAP / ZRAM 等相關功能的管理工具 systemd-swap ,為了方便管理,我使用這個。但後來發現 Ubuntu 上面裝起來不會動,有個看起來很像的問題回報,開發者說已經解決了但我還是不能用,所以我就乾脆直接手動跑了:

/usr/bin/systemd-swap start


  • swappiness 調整為 100:根據後記一提到的那篇文章,swappiness 其實是一個成本的比值,比較匿名分頁和快取分頁逐出再載入的成本。 swappiness 越高表示匿名分頁逐出再載入的成本和快取分頁相近,越低則表示匿名分頁逐出再載入的成本高於快取分頁。因此通常 SSD 的系統可以直接設定 100 (成本相等),機械硬碟的系統就要設低一點。
  • zswap_max_pool_percent 調整為 30 (預設是 25):主記憶體有百分之多少可以被拿來存壓縮的分頁。我有 16GB 的記憶體,想說開大一點看他可以用到多少。


  • 平常如果沒有注意記憶體用量,經常會不小心開一個很吃記憶體的應用程式,主記憶體不夠用,這時候如果沒有 ZSWAP,圖形界面就會直接卡住沒辦法操作,現在有 ZSWAP,圖形界面就只會頓頓的而已,勉強還可以操作。
  • 偶爾檢查記憶體用量,發現 ZSWAP 的壓縮倍率通常都在 2~2.5
    • zswap 的相關資訊都在 /sys/kernel/debug/zswap
    • pool_total_size 是 zswap 在主記憶體當中壓縮後實際佔用的大小
    • stored_pages 是在 zswap 壓縮區當中已存的分頁數量,Linux 預設每個分頁是 4096 bytes
    • 計算壓縮率:
# cd /sys/kernel/debug/zswap
# perl -E "say $(cat stored_pages) * 4096 / $(cat pool_total_size)"


後記三: Xpra

為了要在 Linux 桌面上並行執行更多應用程式,除了本文原本的主題,設法擠出記憶體之外,我還嘗試了另一個方法,就是直接把應用程式丟到遠端伺服器跑,然後透過網路回傳畫面。 Xpra 可以讓你方便做到這件事情,之後我再寫另一篇文章記錄。


WWDAACC20 全球開發者反蘋果審查大會

在開發者社群關注蘋果的 WWDC 同時,人權團體將在 6/22 舉行全球開發者反蘋果審查大會 Worldwide Developers Against Apple Censorship Conference ,邀請圖博(西藏)、東突厥斯坦(新疆)、香港等地的人權團體和國際研究機構、開源軟體開發者,一起來探討蘋果在中國、香港等地以「遵循當地法規」為由,協助執政當局實行言論審查和破壞言論自由的政策。



  1. 蘋果正以「遵從當地法規」為藉口協助極權政府侵害人權。
  2. 蘋果在企業社會責任的人權議題上透明度極差。

或許是因為蘋果一向把公司資訊控制的很好,內部資訊無論是政策還是產品都很少外流,宣傳手腕也比 Facebook 等公司高明,所以除了「蘋果協助極權政府限制人權」這個大方向、長久以來的批評之外,在大方向下面的小新聞比 Google, Facebook 等公司少很多。 Facebook 三不五時就會爆出醜聞,Google 三不五時就會有員工出來遊行抗議,蘋果在人權方面受到輿論的壓力是比 Facebook, Google 小很多的。


像是 Facebook 這公司,如果被輿論批評的很嚴重的話,八成會出個新聞稿說 XXX 是我們一向重視的理念,我們的社群守則也已經涵蓋這方面,我們會增加社群守則執行人員的培訓以確保更好地處理這些議題巴拉巴拉。雖然很爛但至少有回應,要蘋果出來回應的輿論嚴重程度得要到很高很高才有可能的。

其實蘋果侵犯人權的程度說不定比 Google Facebook 高很多,因為蘋果的不透明度高很多,沒人知道也沒人爆料實際上發生了什麼事,也就沒什麼人關心,當作一切都好,真的是很成功的公關策略。

如果簡單地去看蘋果對人權議題的回應度,已經比 Google Facebook 糟很多。聯合國商業與人權中心的企業回應比率評比,蘋果只有 31% ,Facebook 有 68%,Google 有 71%






相關 可以比較不同國家 app store 的搜尋結果,可以看到某些應用程式在特定國家看不到。

我的 Android 10 模改、工具、設定

最近成功把手機從 LineageOS 15.1 無痛升級到 17.1 (沒刪 /data),決定記錄一下過程、在新系統上面使用的工具和安裝方法,一方面避免自己以後需要重做這個流程,另一方面也當作分享和推廣其他人使用 LineageOS。

背景 – LineageOS 15.1

應該是從我買了這隻 Galaxy S7 我就在用 LineageOS 15.1 了,當時似乎已經有 LineageOS 16 ,但因為 Xposed 支援停在 Android 8.1 ,所以我就決定先不升 LineageOS 16。

我之前有在 COSCUP 分享過《加強 Android 隱私的工具和技巧》,裡面有提到,對我來說我覺得 XPrivacyLua 是我不能不用的模組,然後他需要 Xposed ,所以為了他我就一直留在 LineageOS 15.1。

後來我那隻手機型號的 UNOFFICIAL LineageOS 開發者轉移重心到 LOS 16 ,不再更新他的 LOS 15.1 build ,所以為了拿到 Android Security Bulletin,後期我只好自己 build ,還好其實沒很難。

後來知道有厲害的中國開發者做了 EdXposed ,配合 Magisk 和 Riru-core 就可以繼續在 Android 8 以上提供 Xposed 原本提供的系統功能。

最近 LineageOS 17.1 出了,我也找到時間,打算先來測試 LOS 17.1 + EdXposed + XPrivacyLua 的穩定性,如果夠穩定的話就可以升級啦。

Build ROM

Galaxy S7 的 LOS UNOFFICIAL 開發者還蠻認真,更新的很勤勞,幾乎每個月都會出 LOS 17 新的 build 。不過我想說既然我都可以自己 build ,何必用別人編譯的東西。

自己 build 也可以順便確認一下這個開發者寫的 code 的品質怎麼樣,我之前就發現我另一臺手機的 LOS UNOFFICIAL 開發者不是很認真,去他的 repo 看, commit message 根本亂寫,然後還不太會用 git revert ,看他經常在 revert 自己的 revert ,然後有的 revert 還不是真的 revert ,是手動把 code 加回來……

他不會用 git 就算了,我嘗試下載他的原始碼來編譯,居然還編不起來。我跑去聊天群組裡面問他,他就修了修 push 了幾個新 commit,最後我終於 build 起來。但是神奇的事情又發生了,我刷他的 build 就可以開機,我自己的 build 就開不了機 (bootloop) ,嘗試 debug 了許久沒有成果最後逼不得已還是只能用他的 build ,感覺他東西沒完整開源。(不過感覺應該是沒有惡意啦,可能只是不會用 git)(不過後來又發現他只是個法國高中生,不會用 git 就可以 port Android ROM 也是蠻厲害)……另一隻手機的故事扯遠了。

不同裝置 build LOS 的流程幾乎完全一樣,不過對於 UNOFFICIAL 裝置需要的額外動作就是要先抓 local_manifests 的 XML 放到 .repo/local_manifests/roomservice.xml ,然後才繼續 repo init, repo sync,繼續照官方文件的流程操作。

官方文件沒提到的是 LineageOS 17.1 build 需要什麼版本的 OpenJDK,我也沒查到資料,我就隨便猜,先用 OpenJDK 1.9 ,最後 build 成功,證實的確是要用 1.9 。

另一件事情是我原本希望 LOS 15.1 和 17.1 可以共用一個 source tree ,想說既然是 git 應該可以單純 checkout 不同的 branch 成 working copy 就可以,但後來查到資料是說不行,只好分開(找不到資料來源了)。

另外,完成了第一次 repo sync 之後,想要再更新 source tree (用 repo sync)的時候可能會遇到問題,這時候就直接強迫他覆蓋就好: repo sync --force-sync

自己 build 完之後建議用自己的 key 來 sign 這個 build ,官方文件有說明

安裝 ROM 和 OpenGApps

安裝之前當然記得先做個 Nandroid backup 。

安裝就是很普通的進 recovery 按 install zip ,安裝完 ROM 之後再裝 OpenGApps。

我原本有點擔心,我沒有清除 /data ,不知道這樣直接升級可不可以(而且我還是一次跳兩個大版本),怕有些應用程式會不相容。


安裝 Magisk, Riru-core, EdXposed

就直接依照 EdXposed 的說明,依序安裝 Magisk, Riru-core 和 EdXposed 就可以了。

測試 XprivacyLua

根據過去在 LOS 15.1 的經驗,有不少應用程式在受到 XprivacyLua 的限制之後會沒辦法打開。這似乎不太像是應用程式呼叫 API 拿到假資料的時候崩潰,因為我曾經遇過好幾次的神祕狀況是,原本一個受到 XprivacyLua 限制可以正常開啟的應用程式突然打不開(打開馬上當掉),然後我解除了一些 XprivacyLua 限制之後他就可以打開了,實際上看 XprivacyLua 的限制紀錄裡面也沒有顯示應用程式會使用那個打開的限制。並且在 logcat 裡面會有一些看起來跟 ART 有關的 native error (會產生 tombstone 的那種)。我猜測大概是因為 Xposed 在 ART 的 hook 沒有很穩定吧。

所以根據上面的經驗,升級之後我得先測試一下我的應用程式受到 XprivacyLua 限制的時候的穩定性。

測試結果,沒想到, LOS 17.1 + EdXposed + XprivacyLua 的組合,居然比在 LOS 15.1 之下更穩定!

有不少在 LOS 15.1 經常 crash 的應用程式,在 17.1 下,完全不會 crash。

而且,為了穩定性,我在 15.1 的 XprivacyLua 設定裡面經常必須關閉一些限制;在 17.1 ,我把這些限制都重新打開,應用程式竟然還是可以穩定運行!

所以簡單來說在 LOS 17.1 的 XprivacyLua 真的可以想限制什麼 API 就限制什麼,開到爽都不會當機。

不過!有件事情變糟,就是雖然當機的機率降低,但應用程式崩潰的影響似乎變大。在 15.1 應用程式因為 XprivacyLua 限制而當機的時候只會影響單一應用程式(強制關閉),但在 17.1 因為 XprivacyLua 當機的時候經常會直接讓整個系統 reset 重啟。但這原因我也不是很清楚, logcat 看不出所以然。

但總之整體來說我覺得比 LOS 15.1 穩定,所以決定繼續使用 17.1 。

LOS 17.1 設定微調

一開始我注意到電池消耗的速度似乎變快了,不過裝了 BetterBatteryStats 實測了一下耗電速度,又覺得還好,螢幕開著持續使用的狀況下大概 10 分鐘消耗 4% ,螢幕鎖定之後幾乎是完全不耗電,偶爾拿出來使用的話可以輕鬆撐過一整天(12 小時)。

Android 10 換了一個應用程式切換器,從原本應用程式會在畫面上垂直堆疊切換,現在變成一次只顯示一個應用程式,還要水平切換,真的很難用。原本的只要看到對的標題點下去就切換了,新的要滑一下才會顯示上下一個應用程式。網路上查了一下,換回垂直切換器目前似乎沒有解法,只有非原生 launcher 的用戶想要使用原生的切換器,但 Google 不開放權限所以有人做了模組

原本在 Android 8 使用的 GCam 版本到 10 也變得不太穩定,試了幾個之後我覺得 Mod8.3b_IDan_v4.7_test2_fixlos17.apk 這個版本最好。安裝之後要打開設定頁,然後重開 app ,設定才會套用,才能開始拍照。

Websocket 可以拿來掃本地連接埠,順便抱怨 Web{*} 垃圾

今天 Hacker News 上面很紅的一篇是有人發現 eBay 會用 websocket 來掃使用者的 local port:

然後就有人發了另一篇測試 websocket 掃本地連接埠的技術探討和抱怨 Web{*} 這些垃圾,像是 WebVR/WebGL/WebAudio/WebBluetooth/WebUSB 這些東東:


有幾個常見支持 Web* 垃圾的論點是:

一、如果不把這些東西標準化,會變成各家廠商自己使用互不相容的標準,降低網頁技術的可攜性。降低可攜性之後就會產生以前 IE 壟斷時代的各種問題,像是開發者要多做很多工,因此放棄支援比較少人使用的瀏覽器之類的。

不過這裡更深層的問題應該是什麼東西要納入 W3C 的標準,直覺的想法應該是要平衡開發者的需求和使用者的福祉,開發者通常希望網頁技術越多功能越好,這樣他們就可以作出更多功能的網頁應用程式來吸引使用者;使用者則通常是弱勢的一方,對 Web 不一定有什麼願景,只是等開發者作出東西來給他們用,喜歡就用不喜歡就不用,但這種狀況到後來通常會變成使用者不得不用(例如因為網頁版「功能完整」切換到網頁版而停用原生桌面應用程式),所以其實還是蠻單方控制的生態系。

然後 W3C 和 Mozilla 這類理論上應該要平衡雙方、多幫使用者謀求福祉的組織,似乎也只是很簡單地覺得 Web 功能越多對使用者越好,這想法嚴格來說也沒有錯,大概就是和我對於「使用者福祉」的想法不太一樣。

粗略而言,我覺得使用者應該要更主動出力去型塑他們理想的 Web ,跟民主政治很類似,而這需要一個願景作為出發點,但當然大多數使用者是不會有什麼願景的。所以在協助使用者辨明願景這方面 Mozilla 倒是做了很多功夫。

二、Web 應該儘可能實作並且預設啟用更多功能,這樣可以持續強化 Web 作為應用程式和內容的發佈管道的通用和無所不在的特性(我想說的是 “universal"),相對於封閉和不跨平臺的應用程式和內容發佈管道,例如 Play Store 和 iTunes Store。

這和我的願景就不太一樣,我覺得應該是要設法教育使用者讓他們有能力自己去決定要啟用哪一些功能(同時間接表態「我想要透過 Web 使用哪一些應用程式」、「哪些應用程式我不想看到」,這也是對 Web 的願景了),所以大部分的功能(WebGL 之類的)應該是要預設關閉,然後在充分告知的情況下由使用者自己啟用。

但這樣的話對於開發者來說 Web 作為一個發佈管道的吸引力就降低很多,因為他們想要塞給使用者的功能(和夾帶的賺錢機會)使用者不一定買單。


Web{*} 這些標準在瀏覽器裡面預設啟用,我覺得並沒有充分考量使用者的福祉。有能力的使用者是可以用我前一篇講到的方式停用一些功能,但這並沒有解決大環境下使用者福祉被忽略的問題。

Migrate Android ROM build signing keys without losing /data (UNOFFICIAL to a different UNOFFICIAL key)

I did encounter some problem in the process and dug in a little bit to learn about how the system works, this is a note from my learning.


LineageOS documents the steps you need to take when you migrate from one build signed with unofficial/testkey to official build, or vice versa. But What to do if I’m migrating from UNOFFICIAL KEY A to another UNOFFICIAL KEY B?

By looking at I realized that it’s just doing some search and replace in /data/system/packages.xml . So the answer to my own problem is simple, just change the script so that it replaces KEY A with KEY B.

But… I didn’t succeed in the first time, so I dug in the files and code to figure out why. It turns out I just pushed the wrong to the device… but I still decided to note down my learning.


This is basically the database of PackageManager. On boot PackageManager will check the signatures in existing APK files against this database, if they don’t match, data and permission will not be granted. (Also refer to source code at )


Not only each apk is signed, system users also have associated certificates. See

Decoding certificates

I wrote a script in order to decode the encoded certificate strings in packages.xml: . It takes packages.xml file path as argument and print out information of the certificates that the packages.xml contains.

Other useful information

Updating Linux iwlwifi firmware

The Intel Corporation Wireless 8265 / 8275 Wifi adapter on my Dell Latitude 7490 was disconnecting once every few minutes. And after a few reconnections the interface simply disappeared from ifconfig. This is too annoying so I decided to trace down the issue.

This is a note by me, not knowing much about the kernel, debugging the issue.

Firmware bug

Searching for kernel log messages quickly lead me to this kernel bug: . The bug was resolved in 2019 June. So the next thing would be to check if my distribution includes the fix. According to this stackexchange answer, I will be able to see which firmware (ucode) file was loaded, and the firmware version in dmesg, but for me it only showed:

kernel: [525967.408047] iwlwifi 0000:02:00.0: Loaded firmware version: 36.9f0a2d68.0

Luckily in a duplicated bug report the developer said that 36.9f0a2d68.0 is not the latest version.

So I had to update the firmware, but how?

Updating iwlwifi firmware — look for loaded ucode file

I wasn’t able to figure out which ucode file was loaded above, but I found another bug report by the same person: , so it seems that for my adapter, it either uses iwlwifi-8000C-36.ucode or iwlwifi-8265-36.ucode. This is narrow enough and I should be able to just update these 2 files.

Updating iwlwifi firmware — actually updating

On Ubuntu the 2 ucode files are included in linux-firmware package. After googling I couldn’t find any canonical ways to update it. Non-canonical ways would be to either download, build, and install linux-firmware from the upstream master branch, or to install a package from newer version of Ubuntu. Both of these methods will update unrelated firmware in the process, which seemed dangerous (the updated firmwares might not work with my non-mainline Ubuntu kernel version).

More googling says that I should be able to manually download the ucode files, copy it to /lib/firmware and it will load.

And turns out it does. After reboot I can see new firmware is loaded:

[   38.969834] iwlwifi 0000:02:00.0: loaded firmware version 36.77d01142.0 op_mode iwlmvm

Side note – TDLS

This bug is triggered upon a TDLS connection. According to Wikipedia it is “a seamless way to stream media and other data faster between devices already on the same Wi-Fi network." Devices using it communicate directly with one another, without involving the wireless network’s router.

So I searched for “TDLS" in syslog:

Apr  8 00:47:25 X wpa_supplicant[1262]: TDLS: Creating peer entry for xx:xx:xx:xx:xx:xx                                                                                   
Apr  8 00:47:25 X wpa_supplicant[1262]: TDLS: Dialog Token in TPK M1 1                                                                                                    
Apr  8 00:47:25 X kernel: [525967.407778] iwlwifi 0000:02:00.0: Microcode SW error detected.  Restarting 0x82000000.                                                      
Apr  8 00:47:25 X kernel: [525967.408032] iwlwifi 0000:02:00.0: Start IWL Error Log Dump:                                                                                 

Apparently, upon receiving a TDLS connection setup request, iwlwifi resetted. So TDLS is definitely the issue here.

Looking for the MAC address, it is my other Samsung Android device. The reason it was setting up TDLS is probably because I configured the Android to use Burp proxy on my computer.

TDLS seemed quite interesting, and you can set it up manually:

What my broken Synology NAS taught me about mdadm , LVM and testdisk

The fail

I was trying to set up SSL client certificate authentication on nginx. In the config nginx needs to read the client certificate CA from custom file location. Then I found out nginx on Synology DSM wouldn’t start because of AppArmor. I tried to modify the apparmor profile of usr.bin.nginx but can only find cache of the profile under /etc/apparmor.d/cache. So I had to completely disable apparmor for nginx, by removing the cache file.

Then it’s some weeks of trial and error to get it working on nginx. In the end I couldn’t get it to work. (I forgot why.)

The problem came when I tried to enable apparmor for nginx by moving back the profile cache and reloading apparmor:

sudo synoservice --restart apparmor

I lost communication with it after this command. It does not respond to HTTP or SSH or any requests anymore. I unplugged it from power and plugged back, it couldn’t boot. The blinking blue light of death.

Disk structure


Below are some operations that I did, not in order, I only want to document how to achieve each individual task.

Mounting the data partition and system partition:

On first mount of the system partition, it is readonly, I used the following command to make it readwrite:

sudo mdadm --readwrite /dev/md2

Since the data partition lives on a LVM LV inside a MD array, here is the correct procedure to remove the disk:

  1. umount the filesystem
  2. Deactivate the volume group: sudo vgchange -an vg1000 , if you have more than one VG named vg1000, vgchange will ask you to specify uuid, like this: vgchange -ay vg1000 --select vg_uuid=22aBQe-iiUm-XJEd-cjvF-S9qV-U26U-WpsG3p
  3. Use sudo dmsetup table to check the VG is actually disabled. (If disabled it won’t show up.)
  4. Stop DSM system and data MD arrays: sudo mdadm --stop /dev/md126 , sudo mdadm --stop /dev/md127
  5. Tell the kernel to remove the device: echo 1 > /sys/block/sde/device/delete (issue this as root) (reference)
  6. You should hear the disk power down, wait for it to power down completely, then you can safely unplug the disk.

View MD status:

cat /proc/mdstat

Repairing DSM (failed)

First after mounting the system partition, I took a backup using tar .

Then I slide in another disk to the NAS, ask it to install a new operating system on the disk. Then copied the fresh DSM system partition content into the old system partition. This didn’t work, it still wouldn’t boot.

I also tried the official HDD migration method. Didn’t work.

Then I tried powering on the NAS without a disk, and insert the original disk. Hoping it would detect it as an “migrated" data disk. It asked me if I want to install DSM on the disk, I thought, well, let’s see if it can install the system in existing system partition, and, usually, before any formatting is done, there is a warning. – This was a mistake, I clicked install and it immediately started to format the disk without warning. I immediately unplugged the power, hoping to save the data from being overwritten.


Now I got a disk with broken partition table. I had to recover files from it.

Directly asking testdisk to work on the whole disk device: testdisk /dev/sdb . It did find the MD and LV header, but did not find the ext4 partition inside the LV. I tried waiting on testdisk longer to do “deep search" to see if it finds the ext4 superblock. It didn’t. (I imagined the ext4 superblock should be near the LV header, but seems that it is not.)

Fortunately, I soon find that, the LV is still detected by my system. Not sure if it’s the original LV or the new one created. But I asked testdisk to work on the LV: testdisk /dev/vg1000/lv . And this time it quickly found the ext4 filesystem and the directory structure. The directory structure and content are still preserved.

With this I can start dumping the files using testdisk.


Don’t buy a product that you don’t know how it works. (And have bad documentation on how it works.)

If you ever buy such product, don’t use it in “unsupported" use cases, or try to customize it too much. (but sometimes how much is too much can be hard to tell)

If I need a NAS, buy Node 304 chassis, put ASRock J5005-ITX motherboard in it, and install FreeNAS.