├── _config.yml ├── accessibility-service-configuration-change.md ├── analyse-battery-stats.md ├── analyse-libdexhelper.md ├── analyse-livin.md ├── android-fps-counter.md ├── android-init.md ├── android-ptrace.md ├── annoying-vibrate.md ├── avd.md ├── build-magisk-on-windows.md ├── clipboard-whitelist.md ├── commited-secrets-in-git.md ├── core-patch-extend.md ├── cuttlefish-on-wsl.md ├── dark-mode-and-debug-layout.md ├── debug-app-process.md ├── development-settings.md ├── dex2oat.md ├── frida-java-bridge.md ├── gradle-source-dependency.md ├── hackergame-2022-writeups.md ├── icalingua-homework.md ├── im-not-obscured.md ├── index.md ├── jadx-smali-debugger.md ├── keep-accessibility-service-alive.md ├── key-attestation.md ├── ksu-new-su.md ├── ksu-overlay-fs.md ├── learn-opengl.md ├── learn-seccomp-bpf.md ├── learn-systrace.md ├── linker-log.md ├── linker.md ├── load-library-in-app-process.md ├── lspass.md ├── lsplant.md ├── lsposed-native-hook.md ├── maru.md ├── mi-kernel.md ├── minimal-linux.md ├── miui_perms.md ├── mpv.md ├── new-idea-detect-zygisk.md ├── overflow-menu.md ├── parse-dumpsys-with-protobuf.md ├── perspective-transform.md ├── proc-start-monitor.md ├── ptrace.md ├── pwn-tv.md ├── raspberry-pi.md ├── res └── images │ ├── 20220717_01.png │ ├── 20220717_02.png │ ├── 20220717_03.png │ ├── 20220717_04.png │ ├── 20220717_05.png │ ├── 20220717_06.png │ ├── 20220717_07.png │ ├── 20220717_08.png │ ├── 20220717_09.png │ ├── 20220717_10.png │ ├── 20220717_11.png │ ├── 20220717_12.png │ ├── 20220718_01.png │ ├── 20220718_02.png │ ├── 20220718_03.png │ ├── 20220718_04.png │ ├── 20220718_05.png │ ├── 20220719_01.png │ ├── 20220719_02.png │ ├── 20220719_03.png │ ├── 20220719_04.png │ ├── 20220720_01.png │ ├── 20220720_02.png │ ├── 20220907_01.png │ ├── 20220907_02.png │ ├── 20220907_03.png │ ├── 20220907_04.png │ ├── 20220917_01.png │ ├── 20220917_02.png │ ├── 20220917_03.png │ ├── 20220917_04.png │ ├── 20220917_05.png │ ├── 20220917_06.png │ ├── 20220917_07.png │ ├── 20220921_01.png │ ├── 20220921_02.png │ ├── 20220921_03.png │ ├── 20220921_04.png │ ├── 20220922_01.png │ ├── 20220923_01.png │ ├── 20220923_02.png │ ├── 20220924_01.png │ ├── 20221001_01.png │ ├── 20221001_02.png │ ├── 20221003_01.png │ ├── 20221003_02.png │ ├── 20221003_03.png │ ├── 20221003_04.png │ ├── 20221004_01.png │ ├── 20221006_01.png │ ├── 20221006_02.png │ ├── 20221006_03.png │ ├── 20221006_04.png │ ├── 20221006_05.png │ ├── 20221006_06.png │ ├── 20221020_01.png │ ├── 20221020_02.png │ ├── 20221020_03.png │ ├── 20221020_04.png │ ├── 20221020_05.png │ ├── 20221020_06.png │ ├── 20221022_01.png │ ├── 20221022_02.png │ ├── 20221022_03.png │ ├── 20221022_04.png │ ├── 20221022_05.png │ ├── 20221023_01.png │ ├── 20221023_02.png │ ├── 20221023_03.png │ ├── 20221023_04.png │ ├── 20221023_05.png │ ├── 20221105_01.png │ ├── 20221105_02.png │ ├── 20221106_01.png │ ├── 20221107_01.png │ ├── 20221107_02.png │ ├── 20221107_03.png │ ├── 20221107_04.png │ ├── 20221122_01.png │ ├── 20221122_02.png │ ├── 20221122_03.png │ ├── 20221122_04.png │ ├── 20221122_05.png │ ├── 20221122_06.png │ ├── 20230114_01.png │ ├── 20230126_01.png │ ├── 20230214_01.png │ ├── 20230214_02.png │ ├── 20230215_01.png │ ├── 20230215_02.png │ ├── 20230217_01.png │ ├── 20230217_02.png │ ├── 20230222_01.png │ ├── 20230222_02.png │ ├── 20230222_03.png │ ├── 20230307_01.png │ ├── 20230307_02.png │ ├── 20230307_03.png │ ├── 20230309_01.png │ ├── 20230309_02.png │ ├── 20230313_01.png │ ├── 20230314_01.png │ ├── 20230314_02.png │ ├── 20230314_03.png │ ├── 20230314_04.png │ ├── 20230317_01.png │ ├── 20230319_01.png │ ├── 20230319_02.png │ ├── 20230402_01.png │ ├── 20230402_02.png │ ├── 20230402_03.png │ ├── 20230402_04.png │ ├── 20230510_01.png │ ├── 20230510_02.png │ ├── 20230510_03.png │ ├── 20230510_04.png │ └── 20230510_05.png ├── ro-tieba.md ├── sockets-on-android.md ├── strange-configuration-changed-broadcast-causes-lagging.md ├── strange-problem-causes-saladict-broken.md ├── system-properties.md ├── wsl-gui-and-qq.md ├── xposed-stetho.md ├── xweb-debugging.md ├── zygisk-analyse.md └── zygisk-new-start-mode.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /analyse-battery-stats.md: -------------------------------------------------------------------------------- 1 | # 分析手机的电池使用情况 2 | 3 | 感觉手上这台 K30 5G 的耗电越来越快了,记得一次晚上睡觉前充满了电,醒来(6 小时后)发现竟然掉了 7% 。使用系统自带的电量分析,发现主要是 QQ 这个大毒瘤害的。 4 | 5 | 没记错的话,MIUI 专门为 QQ 和微信开了后门,看似「智能管控」实则「无限制」。不过最令我气愤的还是这个 QQ 明明耗电如此过分,消息还常常收不到。于是尝试了 [TSBattery](https://github.com/fankes/TSBattery) 这个专门针对 QQ 耗电优化的 xposed 模块,效果显著——一晚上只耗 2% 的电了,虽然和某为的电量管控尚有差距,起码是个很大的进步。 6 | 7 | 不过这也激起了我对这个模块的好奇心——究竟如何分析特定 App 的耗电原因? 8 | 9 | 其实我也很好奇那些我「允许自启、省电策略无限制」还总是被系统警告「耗电过多」的那些 App 到底耗了多少电,然而 MIUI 的电量统计看起来不想让我们知道这些——只要 app 设置成「无管控」,就不显示电量消耗信息——我寻思不管控就不耗电了? 10 | 11 | 因此决定自己探索一下。 12 | 13 | ## 基础知识 14 | 15 | 首先了解一下电量统计的基础知识:一个 App 的耗电,主要取决于它使用硬件的多少,包括 CPU、Wifi、蓝牙、传感器、相机、闪光灯等。系统统计耗电的时候,以 Uid 为单位。 16 | 17 | > 可以参考这篇文章:[Android耗电统计算法 - Gityuan博客 | 袁辉辉的技术博客](http://gityuan.com/2016/01/10/power_rank/) 18 | 19 | 导致 App 耗电的因素,从 TSBattery 的简介来看,对于 QQ 主要是获取 WakeLock 和通过循环占用 CPU 。 20 | 21 | ``` 22 | Q.这个模块是做什么的? 23 | A.此模块的诞生来源于国内厂商毒瘤 APP 强行霸占后台耗电,QQ 在 8.6.0 版本以后也只是接入了 HMS 推送,但是可笑的是开发组却并没有删除之前疯狂耗电的接收消息方法,于是这个模块就因此而诞生了。 24 | Q.原理是什么? 25 | A.模块有两套工作方式,一种是针对 QQ、TIM Hook 掉系统自身的电源锁“WakeLock”使其不能影响系统休眠,这样子在锁屏的时候 QQ、TIM 就可以进入睡眠状态。第二种就是针对 QQ、TIM 删除其自身的无用耗电疯狂循环检测后台强行保活服务。 26 | Q.如何使用? 27 | A.目前模块支持 LSPosed、EdXposed 以及太极(无极)框架,在太极和 LSPosed 的作用域中,只需勾选 QQ、TIM、微信即可,模块可以做到即插即用,激活后无需重启手机,重启 QQ、TIM 或微信就可以了。 28 | Q.激活后一定可以非常省电吗? 29 | A.并不,模块只能减少 QQ、TIM、微信的耗电,但是请务必记住这一点,省电只是一个理论上的东西,实际水平由你使用的系统和硬件决定,如果你在前台疯狂使用 QQ、TIM,那么照样会耗电,模块只能保证后台运行和锁屏时毒瘤不会消耗过多的无用的电量,仅此而已。 30 | ``` 31 | 32 | ## 获取电量统计数据 33 | 34 | 检查服务列表,发现一些我们感兴趣的服务: 35 | 36 | ```sh 37 | # service list | grep battery 38 | 28 battery: [] 39 | 29 batteryproperties: [android.os.IBatteryPropertiesRegistrar] 40 | 30 batterystats: [com.android.internal.app.IBatteryStats] 41 | ``` 42 | 43 | 显然,`batterystats` 就是我们需要的「电池使用统计」。直接 `dumpsys batterystats` 打印所有的统计,加上包名可以打印部分的。 44 | 45 | …… 46 | 47 | [使用 Batterystats 和 Battery Historian 分析电池用量  |  Android 开发者  |  Android Developers](https://developer.android.com/topic/performance/power/setup-battery-historian?hl=zh-cn) -------------------------------------------------------------------------------- /analyse-libdexhelper.md: -------------------------------------------------------------------------------- 1 | # 梆梆加固分析 2 | 3 | 最近学车,用了驾校推荐的一款「驾考宝典」App ,虽然在流氓方面出乎了我的意料,没有强制申请写存储权限,也不强制注册登录即可使用,算是改变了我对国产 App 的一贯印象,也许是多亏了工信部的整治。然而用着不舒服的地方仍然有许多:广告还是太多,尤其是 pause 久之后 resume 必然会有 splash 广告突脸,想要通过 hook 解决一下,但是过程并不顺利。下面把我探索的过程记录一下。 4 | 5 | ## 认识敌人 6 | 7 | 包名:`com.handsgo.jiakao.android` 8 | 加固方式:梆梆加固企业版(来自 MT 管理器) 9 | 10 | ## 第一次尝试:注入 11 | 12 | 首先尝试 frida 注入。我们的目标首先是 splash ,因此最好在 activity 启动前注入,于是这就需要拿出我们的 TheWorld Xposed 模块,让它在 handleLoadPackage 暂停,等待注入,这时候不会 ANR 或被 AMS 强制杀掉。 13 | 14 | > 在 LSPosed 设置作用域的过程中出现了一些小插曲:首先,作用域怎么都找不到这个 app ,但是取消勾选 LSP 默认隐藏的「游戏」分类就找到了,原来是这玩意在 Manifest 的 application 声明了 `isGame="true"` 。解决了作用域之后,打开 App 发现仍然显示了它的 splash (停下其他应用都是白屏),我以为模块没生效,但看 log 明明有注入,仔细一看发现确实停下了,不知道这个 splash 图片是怎么显示的,明明任何代码都没加载。 15 | 16 | 拿出我们的 frida-helper ,注入,设置 hook ,然后继续执行—— 17 | 18 | ```js 19 | Attaching... 20 | loaded 21 | This app is waiting for us, use `cont()` to continue 22 | [Remote::PID::8841]-> t=trace(android.view.WindowManagerGlobal.addView) 23 | Trace 1 overload methods! 24 | hook void android.view.WindowManagerGlobal#addView (android.view.View, android.view.ViewGroup$LayoutParams, android.view.Display, android.view.Window, int) success 25 | [ 26 | "" 27 | ] 28 | [Remote::PID::8841]-> cont() 29 | [Remote::PID::8841]-> Process terminated 30 | ``` 31 | 32 | ……果不其然,进程直接寄了,看来这位不好对付。 33 | 34 | 观察 log ,发现进程退出原因是 SIGSEGV : 35 | 36 | ``` 37 | 07-12 18:56:22.231 1870 5529 I ActivityManager: Process com.handsgo.jiakao.android (pid 8841) has died: fg TOP 38 | 07-12 18:56:22.231 1870 2229 I Process : PerfMonitor : current process killing process group. PID: 8841 39 | 07-12 18:56:22.231 756 756 I Zygote : Process 8841 exited due to signal 11 (Segmentation fault) 40 | ``` 41 | 42 | 按理来说 sigsegv 应该会触发 crash_dump ,但是 tombstone 、frida 和日志都没有(可能是 signal handler 被替换?)。 43 | 44 | 上 strace 再 frida ,结果仍然是 sigsegv : 45 | 46 | ``` 47 | # strace -e kill -f -p 17840 48 | strace: Process 17840 attached with 16 threads 49 | [pid 17933] +++ exited with 0 +++ 50 | strace: Process 18050 attached 51 | strace: Process 18051 attached 52 | strace: Process 18052 attached 53 | strace: Process 18053 attached 54 | strace: Process 18054 attached 55 | strace: Process 18056 attached 56 | strace: Process 18057 attached 57 | [pid 18052] --- SIGSEGV {si_signo=SIGSEGV, si_code=SEGV_MAPERR, si_addr=0x7a0} --- 58 | [pid 18052] --- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=NULL} --- 59 | [pid 18056] +++ killed by SIGSEGV +++ 60 | [pid 18053] +++ killed by SIGSEGV +++ 61 | [pid 18050] +++ killed by SIGSEGV +++ 62 | ``` 63 | 64 | 尝试不停下直接 frida 注入已经运行的进程,同样闪退。 65 | 66 | 如果单纯只是 xposed 注入,看起来不会崩溃(不过我没有设置 hook) 67 | 68 | 为了确认这个崩溃不是 frida 搞坏了什么,而是进程故意为之,我用 GDB 附加: 69 | 70 | ``` 71 | (gdb) continue 72 | Continuing. 73 | [LWP 28364 exited] 74 | BFD: warning: target:/data/app/~~Xr8RoLKyKG8j_kcnB8BCdg==/com.handsgo.jiakao.android-AuzfW4rSqgeN89DhIyqPSA==/lib/arm64/libDexHelper.so has a corrupt string table index - ignoring 75 | [New LWP 28492] 76 | [New LWP 28493] 77 | [New LWP 28494] 78 | [LWP 28493 exited] 79 | 80 | Thread 17 ".jiakao.android" received signal SIGSEGV, Segmentation fault. 81 | [Switching to LWP 28492] 82 | 0x00000000000007b0 in ?? () 83 | (gdb) info register 84 | x0 0x7b0 1968 85 | x1 0x7229c0b000 490326765568 86 | x2 0x1068 4200 87 | x3 0x1d 29 88 | x4 0x67 103 89 | x5 0x721d038a19 490113042969 90 | x6 0x67 103 91 | x7 0x73757461 1937077345 92 | x8 0xb6a287f1 3064104945 93 | x9 0x7b0 1968 94 | x10 0x1400 5120 95 | x11 0x7200000000 489626271744 96 | x12 0x0 0 97 | x13 0x721d037eeb 490113040107 98 | x14 0x72aded0be2 492544265186 99 | x15 0x2 2 100 | x16 0x72adecc9b8 492544248248 101 | x17 0x72adec103c 492544200764 102 | x18 0x71b3180000 488335998976 103 | x19 0x721d038a18 490113042968 104 | x20 0x721d040798 490113075096 105 | x21 0x15 21 106 | x22 0x7229c0b000 490326765568 107 | x23 0x721d038a68 490113043048 108 | x24 0x721d038990 490113042832 109 | x25 0x721672d000 490002894848 110 | x26 0x721d038b68 490113043304 111 | x27 0x721d038a69 490113043049 112 | x28 0x721d038a40 490113043008 113 | x29 0x721d038890 490113042576 114 | x30 0x0 0 115 | sp 0x0 0x0 116 | pc 0x7b0 0x7b0 117 | cpsr 0x60000000 [ EL=0 C Z ] 118 | fpsr 0x11 17 119 | fpcr 0x0 0 120 | (gdb) 121 | (gdb) start 122 | start starti 123 | (gdb) bt 124 | #0 0x00000000000007b0 in ?? () 125 | #1 0x0000000000000000 in ?? () 126 | Backtrace stopped: previous frame identical to this frame (corrupt stack?) 127 | ``` 128 | 129 | 看上去在这个线程做了一些奇怪的操作,导致 pc 和 sp 寄存器被置了一个奇怪的值和 0 ,这种情况下根本无法栈回溯。 130 | 131 | ## 第二次尝试:静态分析 132 | 133 | 先用 readelf 看一看 program header : 134 | 135 | ``` 136 | $ readelf -l libDexHelper.so 137 | 138 | Elf file type is DYN (Shared object file) 139 | Entry point 0x15730 140 | There are 5 program headers, starting at offset 64 141 | 142 | Program Headers: 143 | Type Offset VirtAddr PhysAddr 144 | FileSiz MemSiz Flags Align 145 | LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 146 | 0x000000000012c728 0x000000000012c728 R E 0x10000 147 | LOAD 0x000000000012d1a8 0x000000000013d1a8 0x000000000013d1a8 148 | 0x000000000000a0e0 0x000000000001f4f0 RW 0x10000 149 | DYNAMIC 0x0000000000138ae0 0x000000000013e0e8 0x000000000013e0e8 150 | 0x0000000000000240 0x0000000000000240 RW 0x8 151 | readelf: Error: no .dynamic section in the dynamic segment 152 | GNU_EH_FRAME 0x000000000011d11c 0x000000000011d11c 0x000000000011d11c 153 | 0x0000000000001bdc 0x0000000000001bdc R 0x4 154 | LOAD 0x0000000000138078 0x000000000015d078 0x000000000015d078 155 | 0x0000000000000a54 0x0000000000000a54 R E 0x1000 156 | 157 | Section to Segment mapping: 158 | Segment Sections... 159 | 00 ^�ELF^B^A^A 160 | 01 161 | 02 162 | 03 163 | 04 164 | ``` 165 | 166 | 很奇怪,报错 `no .dynamic section in the dynamic segment` ,看不到 section 的名字。 167 | 168 | 再看看 section header : 169 | 170 | ``` 171 | $ readelf -S libDexHelper.so 172 | There are 4 section headers, starting at offset 0x138ca0: 173 | 174 | Section Headers: 175 | [Nr] Name Type Address Offset 176 | Size EntSize Flags Link Info Align 177 | [ 0] ^�ELF^B^A^A NULL 0000000000000000 00000000 178 | 0000000000000000 0000000000000000 0 0 0 179 | [ 1] ^�ELF^B^A^A NOBITS 0000000000000000 00000000 180 | 0000000000138da0 0000000000000000 0 0 0 181 | [ 2] ^�ELF^B^A^A STRTAB 00000000000088c0 000088c0 182 | 0000000000006fc1 0000000000000000 A 0 0 1 183 | [ 3] ^�ELF^B^A^A DYNAMIC 0000000000138ae0 00138ae0 184 | 0000000000000240 0000000000000010 WA 2 0 8 185 | Key to Flags: 186 | W (write), A (alloc), X (execute), M (merge), S (strings), I (info), 187 | L (link order), O (extra OS processing required), G (group), T (TLS), 188 | C (compressed), x (unknown), o (OS specific), E (exclude), 189 | D (mbind), p (processor specific) 190 | readelf: Error: no .dynamic section in the dynamic segment 191 | ``` 192 | 193 | 虽然能读出 4 个节,但是名字都异常,像是名字的偏移指向了文件的头部。 194 | 195 | 我们知道,Android linker 解析 ELF 实际上只需要知道 segment ,不需要 section 的信息,因此将其完全抹除也是没有问题的。 196 | 197 | [Android Linker学习笔记 - 路人甲](https://wooyun.js.org/drops/Android%20Linker%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.html) 198 | 199 | Linker 认为的 dynamic section 实际上就是 dynamic segment ,从程序头即可读出,但是我们的 readelf 并不认识,所以动态符号也无法读取,看得出来这个加固的坑还是不少的。 200 | 201 | 既然如此,那就上更高级的分析工具,Ghidra ! 202 | 203 | ![](res/images/20220717_01.png) 204 | 205 | ghidra 看起来能分析出 elf 的符号,但是竟然反编译不出来。 206 | 207 | 一开始我以为 ghidra 没有正确分析出符号的位置,尝试自己分析。 208 | 209 | 先看一看 ELF Header : 210 | 211 | ![](res/images/20220717_02.png) 212 | 213 | 可以发现,section header 的字符串表已经被破坏,偏移为 1 ,难怪找不到节名。 214 | 215 | 于是我尝试在 section 中找 dynamic 段。 216 | 217 | ![](res/images/20220717_03.png) 218 | 219 | 可以看到一个叫 SHT_DYNAMIC 的段,ghidra 已经贴心地给我们标好了位置,跳过去看一看: 220 | 221 | ![](res/images/20220717_04.png) 222 | 223 | 但是 ghidra 没有认为它是 dynamic 段格式,所以我自作聪明地帮它选了: 224 | 225 | ![](res/images/20220717_05.png) 226 | 227 | 结果发现这个「dynamic section」竟然只有 7 个 DT_NEEDED,一个 SONAME ,剩下的全是 DT_NULL ! 228 | 229 | ![](res/images/20220717_06.png) 230 | 231 | 假如动态段真的只有这些内容,那不太可能运行代码,我尝试在这些 DT_NEEDED 中寻找突破口,但发现这些都是系统库,并没有引入 apk 中其他 so 作为依赖: 232 | 233 | ![](res/images/20220717_07.png) 234 | 235 | 既然 dynamic 段没有东西,那 ghidra 到底怎么分析出这些符号的呢?我又看到程序头中 dynamic 段的信息: 236 | 237 | ![](res/images/20220717_08.png) 238 | 239 | 可以发现,dynamic 段位于文件偏移 `0x138ae0` 的位置,加载到地址 `0x13e0e8` 上。 240 | 241 | 回顾 section header 的信息:dynamic 节位于 `0x138ae0` ,加载地址是 `0x138ae0` 。 242 | 243 | 看上去文件偏移是一致的,但是地址却不同。 244 | 245 | 于是我跟着 ghidra 跳到 dynamic 段的位置,发现这个段才是真正的动态段,包含了我们需要的符号信息: 246 | 247 | ![](res/images/20220717_09.png) 248 | 249 | 显然,这里的地址和上面是不同的。由于 ghidra 不会显示文件偏移,我们只好在其他二进制编辑器中比较它们的区别: 250 | 251 | 假「动态节」的文件偏移 `0x138ae0` : 252 | 253 | ![](res/images/20220717_10.png) 254 | 255 | 真「动态段」的文件偏移 `0x12e0e8`: 256 | 257 | ![](res/images/20220717_11.png) 258 | 259 | 看上去,section header 和 program header 都引导我们到了一个假的动态段,那么真正的动态段的文件偏移是怎么来的呢? 260 | 261 | 分析 linker 源码发现,实际上只有 LOAD 段的文件偏移是有用的,而寻找 DYNAMIC 段实际上用的是 vaddr 的地址,在已经 load 的段中寻找(bias + vaddr)。也就是说,**DYNAMIC 段的文件 offset 可以不正确,只要 vaddr 正确即可**。 262 | 263 | ``` 264 | Type Offset VirtAddr PhysAddr 265 | FileSiz MemSiz Flags Align 266 | LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000 267 | 0x000000000012c728 0x000000000012c728 R E 0x10000 268 | LOAD[DYNAMIC] 0x000000000012d1a8 0x000000000013d1a8 0x000000000013d1a8 269 | 0x000000000000a0e0 0x000000000001f4f0 RW 0x10000 270 | DYNAMIC 0x0000000000138ae0 0x000000000013e0e8 0x000000000013e0e8 271 | 0x0000000000000240 0x0000000000000240 RW 0x8 272 | GNU_EH_FRAME 0x000000000011d11c 0x000000000011d11c 0x000000000011d11c 273 | 0x0000000000001bdc 0x0000000000001bdc R 0x4 274 | LOAD 0x0000000000138078 0x000000000015d078 0x000000000015d078 275 | 0x0000000000000a54 0x0000000000000a54 R E 0x1000 276 | ``` 277 | 278 | DYNAMIC 所在的 vaddr 是 `0x13e0e8` ,位于第二个 load 段,它的 vaddr 范围是 `0x13d1a8 ~ 0x15c698` ,对应文件偏移 `0x12d1a8 ~ 0x137288`。DYNAMIC 和该 load 段的的文件偏移之差与 vaddr 之差应该相等,所以 DYNAMIC 的文件偏移就是 `0x12d1a8 + (0x13e0e8 - 0x13d1a8) = 0x12e0e8` 279 | 280 | 这么一通分析下来,发现 ghidra 的分析是没错的,至少它找对了 dynamic 段。 281 | 282 | 但是为什么 `JNI_OnLoad` 的反编译失败呢?显然是加固做了代码保护,如某种加密。 283 | 284 | 我们看一看 `DT_INIT` 指向的函数,这里是程序的入口,并没有被保护: 285 | 286 | ![](res/images/20220717_12.png) 287 | 288 | ## 终极尝试: 289 | -------------------------------------------------------------------------------- /android-fps-counter.md: -------------------------------------------------------------------------------- 1 | # 动手做一个 Android FPS 监视器 2 | 3 | ## 前言 4 | 5 | 最近在研究一个开源 App 的源码,看到一条 issue 提到在某个界面静止的情况下,Surface 持续刷新,FPS 很高,导致耗电更快。我就想解决这个问题,但首先应该自己验证一下,这个界面是不是真的静态时 FPS 高,但我对如何获取 FPS 一无所知,手上也没有合适的工具,于是决定自己钻研一番。 6 | 7 | ## 认识 FPS 8 | 9 | FPS (每秒传输帧数, Frames Per Second),也叫帧率。在 Android 中, 10 | 11 | 刷新率 12 | 13 | [VSYNC  |  Android 开源项目  |  Android Open Source Project](https://source.android.com/devices/graphics/implement-vsync?hl=zh-cn) 14 | [“终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!_胡飞洋的博客-CSDN博客](https://blog.csdn.net/hfy8971613/article/details/108041504) 15 | [通俗易懂的Android屏幕刷新机制 - 掘金](https://juejin.cn/post/6976787089703010341) 16 | [Android-屏幕刷新机制 - 简书](https://www.jianshu.com/p/6958d3b11b6a) 17 | 18 | 19 | 20 | ……先坑着 21 | 22 | [vtools/FpsUtils.kt at master · helloklf/vtools](https://github.com/helloklf/vtools/blob/master/app/src/main/java/com/omarea/library/shell/FpsUtils.kt) 23 | 24 | ``` 25 | # 内核级 fps 数据 26 | /sys/class/drm/sde-crtc-0/measured_fps 27 | /sys/class/graphics/fb0/measured_fps 28 | # 在 /sys 下寻找 fps 29 | find /sys -name measured_fps 2>/dev/null 30 | find /sys -name fps 2>/dev/null 31 | # 利用 SurfaceFlinger 后门代码获取 PageFlipCount 32 | service call SurfaceFlinger 1013 33 | # 也可以通过 dumpsys SurfaceFlinger 获取上面的数据(不能 proto ) 34 | # Android 9: grep '+ DisplayDevice' 找 flips= 35 | # Android 11: grep 'Composition RenderSurface State' 找 flips 36 | ``` -------------------------------------------------------------------------------- /android-init.md: -------------------------------------------------------------------------------- 1 | # init 2 | 3 | 已经很久没有研究过 init 了,发现对一些细节不太了解,复习一下。 4 | 5 | ## rc 语法 6 | 7 | https://android.googlesource.com/platform/system/core/+/main/init/README.md 8 | 9 | https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/README.md;drc=5c4217cf6eb0686fcab73cd55d216859549b6250 10 | 11 | 要点 12 | 13 | 1. `on ...` 下面是一系列 command ,顺序执行(?) 14 | 2. `service ...` 定义服务,服务可以有 class ,`start` 和 `class_start` 启动一个或一类服务。 15 | 3. triggers 排队执行,可以在 command 中触发某个 trigger ,即添加到队列 16 | 4. `exec` command 阻塞等待被执行的程序退出 17 | 18 | ## rc 解析 19 | 20 | [LoadBootScripts](https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/init.cpp;l=337;drc=e274a5ef062925c35b042726f9dff8c0fd6cabd6) 21 | 22 | actions 的顺序和 rc 文件顺序的关系(TODO) 23 | 24 | ## boot 执行 25 | 26 | init 里面执行事件循环之前已经预先 queue 了一系列 trigger ,在 hw/init.rc 则会触发更多 trigger 27 | 28 | [init.cpp](https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/init.cpp;l=1088;drc=55ef3d6104032dcaa773e486405cb9f8978fd5ce) 29 | 30 | [hw/init.rc](https://cs.android.com/android/platform/superproject/main/+/main:system/core/rootdir/init.rc;l=542;drc=b5ce7aa444f7abfd62649c329f76c2b9e728cf92) 31 | 32 | [nonencrypted](https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/builtins.cpp;l=581;drc=f06e218e826039bf5113de5a773803fccfcac561) 33 | 34 | 关键流程 35 | 36 | ``` 37 | (init) 38 | trigger early-init 39 | trigger init 40 | trigger late-init 41 | loop { 42 | on early-init 43 | on init 44 | on late-init 45 | trigger late-fs 46 | trigger post-fs-data 47 | on late-fs # 由厂商实现,例如 /vendor/etc/init/hw/init.target.rc 48 | mount_all --late 49 | (do_mount_all -> queue_fs_event) 50 | trigger nonencrypt 51 | on post-fs-data 52 | on nonencrypt 53 | class_start main 54 | } 55 | ``` 56 | 57 | post-fs-data 之后 /data 已经被挂载 58 | 59 | start main 就会启动 zygote 和一系列服务 60 | 61 | ## 主流 root 框架对 init 的注入 62 | 63 | post-fs-data.sh 在 post-fs-data trigger 执行,并且是阻塞的 64 | 65 | service.sh 在 nonencrypted trigger 执行,按理来说此时 zygote 已经启动 66 | 67 | https://topjohnwu.github.io/Magisk/guides.html#boot-scripts 68 | 69 | ### magisk 70 | 71 | https://github.com/topjohnwu/Magisk/blob/cf43c562185cb359063ee36bfd879310b43dc949/native/src/init/rootdir.cpp#L16 72 | 73 | 选择的是 hw/init.rc 的尾部 74 | 75 | ``` 76 | on post-fs-data 77 | start logd 78 | exec %2$s 0 0 -- %1$s/magisk --post-fs-data 79 | 80 | on property:vold.decrypt=trigger_restart_framework 81 | exec %2$s 0 0 -- %1$s/magisk --service 82 | 83 | on nonencrypted 84 | exec %2$s 0 0 -- %1$s/magisk --service 85 | 86 | on property:sys.boot_completed=1 87 | exec %2$s 0 0 -- %1$s/magisk --boot-complete 88 | 89 | on property:init.svc.zygote=restarting 90 | exec %2$s 0 0 -- %1$s/magisk --zygote-restart 91 | 92 | on property:init.svc.zygote=stopped 93 | exec %2$s 0 0 -- %1$s/magisk --zygote-restart 94 | ``` 95 | 96 | ### ksu 97 | 98 | 注入到 /system/etc/init/atrace.rc 的头部,这么做的原因也许是 kprobes 注入到文件尾部太麻烦,于是选择了 a 开头的文件的头部,确保跟在 init.rc 后。 99 | 100 | https://github.com/tiann/KernelSU/blob/ce892bc439272a0ec41498a5b46df143ca11f68b/kernel/ksud.c#L21 101 | 102 | ``` 103 | static const char KERNEL_SU_RC[] = 104 | "\n" 105 | 106 | "on post-fs-data\n" 107 | " start logd\n" 108 | // We should wait for the post-fs-data finish 109 | " exec u:r:su:s0 root -- " KSUD_PATH " post-fs-data\n" 110 | "\n" 111 | 112 | "on nonencrypted\n" 113 | " exec u:r:su:s0 root -- " KSUD_PATH " services\n" 114 | "\n" 115 | 116 | "on property:vold.decrypt=trigger_restart_framework\n" 117 | " exec u:r:su:s0 root -- " KSUD_PATH " services\n" 118 | "\n" 119 | 120 | "on property:sys.boot_completed=1\n" 121 | " exec u:r:su:s0 root -- " KSUD_PATH " boot-completed\n" 122 | "\n" 123 | 124 | "\n"; 125 | ``` 126 | -------------------------------------------------------------------------------- /annoying-vibrate.md: -------------------------------------------------------------------------------- 1 | # 烦人的振动 2 | 3 | Telegram 相比国内一众 App ,振动反馈十分充足,然而也给我带来了不少尴尬——因为深夜在宿舍刷 TG ,不小心碰到个地方都会有个振动反馈,并且我的手机振动又很「大声」,总觉得会给其他人带来不好的影响——起码我是这么认为的。 4 | 5 | 但是当我试图关闭这些振动反馈,却发现没那么简单: 6 | 7 | 首先,应用内的「振动」开关似乎全都关上了: 8 | 9 | ![](res/images/20220917_01.png) 10 | 11 | 然后,「通知」的「振动权限」也关掉了——虽然这个是属于通知的;并且 Android 或 MIUI 似乎都没有应用的「振动权限」一说,起码设置没有。 12 | 13 | ![](res/images/20220917_02.png) 14 | 15 | 最后,我长期晚上开着「勿扰」模式,但是也没法阻止这个振动。 16 | 17 | 就拿最常用的例子,发生振动的源头是左划消息回复,以及投票的选项。 18 | 19 | ![](res/images/20220917_03.png) 20 | 21 | 前者是的振动时间较段,后者则较长,且幅度也更大。这两个振动都无法关掉。 22 | 23 | 尝试寻找系统服务里面有没有 vibrator : 24 | 25 | ``` 26 | # service list|grep vib 27 | 16 android.hardware.vibrator.IVibrator/default: [android.hardware.vibrator.IVibrator] 28 | 66 external_vibrator_service: [android.os.IExternalVibratorService] 29 | 207 vibrator: [android.os.IVibratorService] 30 | ``` 31 | 32 | 最后一个看上去是向应用层提供的,实现了 ShellCommand ,但是没什么对我有用的命令: 33 | 34 | ``` 35 | # cmd vibrator 36 | Vibrator commands: 37 | help 38 | Prints this help text. 39 | 40 | vibrate duration [description] 41 | Vibrates for duration milliseconds; ignored when device is on 42 | DND (Do Not Disturb) mode; touch feedback strength user setting 43 | will be used to scale amplitude. 44 | waveform [-d description] [-r index] [-a] duration [amplitude] ... 45 | Vibrates for durations and amplitudes in list; ignored when 46 | device is on DND (Do Not Disturb) mode; touch feedback strength 47 | user setting will be used to scale amplitude. 48 | If -r is provided, the waveform loops back to the specified 49 | index (e.g. 0 loops from the beginning) 50 | If -a is provided, the command accepts duration-amplitude pairs; 51 | otherwise, it accepts durations only and alternates off/on 52 | Duration is in milliseconds; amplitude is a scale of 1-255. 53 | prebaked [-b] effect-id [description] 54 | Vibrates with prebaked effect; ignored when device is on DND 55 | (Do Not Disturb) mode; touch feedback strength user setting 56 | will be used to scale amplitude. 57 | If -b is provided, the prebaked fallback effect will be played if 58 | the device doesn't support the given effect-id. 59 | capabilities 60 | Prints capabilities of this device. 61 | cancel 62 | Cancels any active vibration 63 | Common Options: 64 | -f - Force. Ignore Do Not Disturb setting. 65 | ``` 66 | 67 | 振动虽然有个 `android.permission.VIBRATE` 的权限,但是这个应该类似于网络权限,无法 pm revoke 掉。 68 | 69 | ``` 70 | # pm revoke org.telegram.messenger android.permission.VIBRATE 71 | 72 | Exception occurred while executing 'revoke': 73 | java.lang.SecurityException: Permission android.permission.VIBRATE requested by org.telegram.messenger is not a changeable permission type 74 | ``` 75 | 76 | 思路转移到另一个「管理权限」的东西: AppOps 。在 appops 中也有 vibrate 权限! 77 | 78 | ``` 79 | # appops get org.telegram.messenger VIBRATE 80 | VIBRATE: allow; time=+9h57m39s866ms ago; duration=+121ms 81 | # appops set org.telegram.messenger VIBRATE deny 82 | ``` 83 | 84 | 设置之后,左划消息不再产生振动了,然而投票选项的长振动仍然存在。 85 | 86 | 观察 `dumpsys vibrator` ,发现这个振动的来源并非 telegram ,而是 Android ! 87 | 88 | ``` 89 | # 左划消息的振动 90 | , mAddedTime: 2022-09-17 00:51:59, effect: Waveform{mTimings=[80, 25, 15], mAmplitudes=[15, 0, 255], mRepeat=-1}, usageHint: 18, uid: 10256, opPkg: org.telegram.messenger, foreground: true 91 | # 投票选项的振动 92 | , mAddedTime: 2022-09-17 10:47:37, effect: OneShot{mDuration=40, mAmplitude=-1}, usageHint: 18, uid: 1000, opPkg: android, foreground: false 93 | ``` 94 | 95 | 对此,解决方法是 appops 禁用 android 的 VIBRATE 权限,不过这么一来或许某些必要的振动也无法听到了。 96 | 97 | 但是这个振动到底来自哪里呢?我们拿出祖传的 frida helper hook 一下 VibratorService 。 98 | 99 | 这个服务位于 system_server ,不过随着版本变迁改了几次名。 100 | 101 | ```js 102 | // aidl 103 | android.os.IVibratorService -> android.os.IVibratorManagerService 104 | // 系统服务 105 | com.android.server.VibratorService -> com.android.server.vibrator.VibratorManagerService 106 | ``` 107 | 108 | Android 11 用的是左边的。 109 | 110 | ```js 111 | VS=use('com.android.server.VibratorService') 112 | t=trace(VS.vibrate) 113 | ``` 114 | 115 | 得到调用栈: 116 | 117 | ```js 118 | com.android.server.VibratorService.vibrate(Native Method) 119 | android.os.SystemVibrator.vibrate(SystemVibrator.java:245) 120 | android.os.Vibrator.vibrate(Vibrator.java:397) 121 | android.os.Vibrator.vibrate(Vibrator.java:306) 122 | android.os.Vibrator.vibrate(Vibrator.java:286) 123 | miui.util.VibrateUtils.vibrate(VibrateUtils.java:87) 124 | miui.util.HapticFeedbackUtil.performHapticFeedback(HapticFeedbackUtil.java:357) 125 | miui.util.HapticFeedbackUtil.performHapticFeedback(HapticFeedbackUtil.java:322) 126 | miui.util.HapticFeedbackUtil.performHapticFeedback(HapticFeedbackUtil.java:295) 127 | com.android.server.policy.BaseMiuiPhoneWindowManager.performHapticFeedback(BaseMiuiPhoneWindowManager.java:1945) 128 | com.android.server.wm.Session.performHapticFeedback(Session.java:270) 129 | android.view.IWindowSession$Stub.onTransact(IWindowSession.java:1050) 130 | com.android.server.wm.Session.onTransact(Session.java:141) 131 | android.os.Binder.execTransactInternal(Binder.java:1154) 132 | android.os.Binder.execTransact(Binder.java:1123) 133 | ``` 134 | 135 | 这是来自 `IWindowSession#performHapticFeedback` ,虽然调用栈有 MIUI ,不过是原生的功能。 136 | 137 | 所谓「Haptic Feedback」,就是触摸反馈,这是 View 的一个属性。 138 | 139 | [android - How to enable haptic feedback on button view - Stack Overflow](https://stackoverflow.com/questions/2228151/how-to-enable-haptic-feedback-on-button-view/13152567#13152567) 140 | 141 | 相关的属性和方法: 142 | 143 | ``` 144 | android.view.View: 145 | android:hapticFeedbackEnabled 146 | setHapticFeedbackEnabled(boolean) 147 | performHapticFeedback 148 | ``` 149 | 150 | 至于这个东西到底怎么关闭,我也很迷惑,因为「声音与振动」里面的「点按振动」就是关闭的,但是 TG 还能振。不过稍微看了一下 View 的源码,发现这个设置好像也可以被忽略。目前屏蔽振动的方法是写一个定时任务用 appops 禁用和启用 android 的 VIBRATE 权限。 151 | -------------------------------------------------------------------------------- /avd.md: -------------------------------------------------------------------------------- 1 | # AVD 记录 2 | 3 | ## llkd 4 | 5 | 2022.08.16 6 | 7 | 最近在用 avd (Android 11 x86-64)上安装 lsposed 调试 xposed 模块,但是总是遇到 lspd 被杀的情况: 8 | 9 | ```log 10 | 2022-08-16 22:07:58.753 2501-2646/system_process E/LSPosed Bridge: service is dead 11 | ``` 12 | 13 | 进一步查明原因,在 logcat 中过滤 lspd 的 pid : 14 | 15 | ```log 16 | 08-16 13:57:36.990 2501 3238 I ActivityManager: Force stopping com.android.shell appid=2000 user=0: from pid 2366 17 | 08-16 13:57:36.990 2501 3238 I ActivityManager: Killing 3831:com.android.shell/2000 (adj 935): stop com.android.shell due to from pid 2366 18 | 08-16 14:07:58.728 515 515 W livelock: Z 600.000s 2366->2497->2497 [idmap2] [kill] 19 | 08-16 14:07:58.728 515 515 I livelock: Killing 'lspd' (2366) to check forward scheduling progress in Z state for '[idmap2]' (2497) 20 | ``` 21 | 22 | 发现被一个叫做 livelock 的东西给杀了。 23 | 24 | [Android Live-LocK 守护程序 (llkd)](https://source.android.com/devices/architecture/kernel/llkd#coverage) 25 | 26 | 根据文档所说,有一个设置免杀(黑名单)的属性 `ro.llk.blacklist.process` ,于是尝试 `resetprop ro.llk.blacklist.process lspd` 27 | 28 | 观察到 livelock 的日志开头有输出一些 ro 属性,怀疑是启动时加载一次,因此重启一下(首先确认服务名为 `llkd-1`): 29 | 30 | ```sh 31 | generic_x86_64:/data/local/tmp # getprop | grep llkd 32 | [init.svc.llkd-1]: [running] 33 | [init.svc_debug_pid.llkd-1]: [515] 34 | generic_x86_64:/data/local/tmp # stop llkd-1 35 | generic_x86_64:/data/local/tmp # start llkd-1 36 | generic_x86_64:/data/local/tmp # logcat -s livelock:* 37 | --------- beginning of kernel 38 | --------- beginning of system 39 | --------- beginning of main 40 | --------- beginning of crash 41 | 08-16 14:07:58.728 515 515 W livelock: Z 600.000s 2366->2497->2497 [idmap2] [kill] 42 | 08-16 14:07:58.728 515 515 I livelock: Killing 'lspd' (2366) to check forward scheduling progress in Z state for '[idmap2]' (2497) 43 | 08-16 14:17:50.948 6087 6087 I livelock: started 44 | 08-16 14:17:50.949 6087 6087 W livelock: [khungtaskd] not configurable 45 | 08-16 14:17:50.950 6087 6087 I livelock: ro.config.low_ram=false 46 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.sysrq_t=true 47 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.enable=true 48 | 08-16 14:17:50.950 6087 6087 I livelock: ro.khungtask.enable=true 49 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.mlockall=true 50 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.killtest=true 51 | 08-16 14:17:50.950 6087 6087 I livelock: ro.khungtask.timeout=720s 52 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.timeout_ms=600.000s 53 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.D.timeout_ms=600.000s 54 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.Z.timeout_ms=600.000s 55 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.stack.timeout_ms=600.000s 56 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.check_ms=120.000s 57 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.stack=wait_on_page_bit_killable,bit_wait_io,__get_user_pages,cma_alloc 58 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.ignorelist.process.stack=llkd,lmkd.llkd,apexd,ueventd,keystore,init 59 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.ignorelist.process=[watchdog/3],[watchdog/2],[watchdog/1],[watchdogd/0],2,6087,[watchdogd],llkd,lmkd,[khungtaskd],[kthreadd],init,watchdogd,1,0 60 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.ignorelist.parent=[kthreadd],2,0,adbd&[setsid] 61 | 08-16 14:17:50.950 6087 6087 I livelock: ro.llk.ignorelist.uid= 62 | ``` 63 | 64 | 发现属性并没有发生任何变化,而且属性的名字 `ro.llk.ignorelist.process` 似乎也与文档描述不符,最后,在 props 根本找不到文档所说的默认值: 65 | 66 | ```sh 67 | 130|generic_x86_64:/data/local/tmp # getprop ro.llk.ignorelist.process 68 | 69 | ``` 70 | 71 | 既然没法配置黑名单,干脆直接 `stop llkd-1` 好了。 72 | 73 | ## AVD 安装 Magisk 74 | 75 | 在 Android 11 的 AVD 上安装 Magisk 一直以来令人头疼,因为这个版本具有奇怪的「双层」 ramdisk 镜像,而 Magisk 的 `avd_patch.sh` 似乎并没有处理这一特性。 76 | 77 | 此前一直使用 [MagiskOnEmulator](https://github.com/shakalaca/MagiskOnEmulator) 和官方发布的 magisk 来修补 ramdisk ,它确实可以正确处理 Android 11 的 ramdisk 。但随着版本更新,到了 Magisk 24 就会出现一些诡异问题,比如在 x86-64 的 avd 上它只会复制 x64 的 so ,导致 32 位 zygisk 无法启动,尝试修复未果。 78 | 79 | 既然这样,不如试试另一种方式 —— [`avd_magisk.sh`](https://github.com/topjohnwu/Magisk/blob/c2f96975cef433a67dc036fdd6ea3736f69d6763/scripts/avd_magisk.sh),也就是利用 avd 本身具有的 root 权限模拟 magisk 运行。考虑到我的需求只是用 zygisk 、lsposed 调试模块,其实完全没必要修补 ramdisk 。 80 | 81 | 正确的用法应该是用 magisk 的构建脚本从头构建一遍,然后用 `build.py emulator` 的,不过我不想那么麻烦,直接用官方编译好的 apk 凑合用吧。 82 | 83 | 首先在一个目录准备好 `scripts/avd_magisk.sh` 和 magisk 的 apk (这里用 debug 版 `app-debug.apk`),然后写一个脚本(此处在 windows 下是 `avd.bat`) 84 | 85 | ```bat 86 | adb push busybox app-debug.apk avd_magisk.sh /data/local/tmp 87 | adb shell sh /data/local/tmp/avd_magisk.sh 88 | ``` 89 | 90 | 打开 avd ,执行 `avd.bat` 。完成后 avd 会软重启,并且直接启动 magisk 的 post-fs-data 和 service 。此时就可以愉快地用 magisk 了。如果要开启 zygisk ,应该在 app 打开开关后,再次执行 `avd.bat` 。安装模块后的重启也应该执行这个脚本。如果只是需要 zygote 重启的话,也可以在 adb shell 中 `stop;start` 。 91 | 92 | 用这样的方法调试了几次模块,总的来说还是非常方便的,也没有像 patch 方法要考虑 magisk 升级的麻烦。 93 | 94 | ## 冷启动 95 | 96 | 调试的时候经常搞坏系统。比如: 97 | 98 | 1. 之前调试 sui 的时候把 adbd 杀了,却没有被 init 自动拉起 99 | 2. magisk 模块导致 zygote bootloop ,**stop 之后在这个阶段安装模块**会导致诡异的情况——执行任何程序都会提示 not found (包括 sh),至今没找明白原因。 100 | 101 | 这种情况下就需要彻底关闭系统,然而 AVD 的 UI 甚至没有强制重启的选项,并且由于 AVD 有快照功能,直接关闭的话,下次启动又恢复上次坏掉的系统。 102 | 103 | 虽然确实有冷启动的方法,不过我总是记不住,现在记在下面: 104 | 105 | 启动的时候添加这个参数: 106 | 107 | ``` 108 | -no-snapshot perform a full boot and do not auto-save, but qemu vmload and vmsave operate on snapstorage 109 | ``` 110 | 111 | ## 删除快照 112 | 113 | 上面的方法是临时不从快照启动,而下次启动实际上还是从快照启动的。 114 | 115 | 虽然似乎可以禁用快照,不过这样就慢了,我们只是想在快照坏掉的时候把它删掉。 116 | 117 | 每个 avd 的快照位于: 118 | 119 | `~\.android\avd\$AVD_NAME\snapshots\$SNAPSHOT_NAME` 120 | 121 | 快照都有名字,默认为 `default_boot` ,直接将整个快照删掉,下次就会重新启动了。 122 | -------------------------------------------------------------------------------- /clipboard-whitelist.md: -------------------------------------------------------------------------------- 1 | # 读 Riru-ClipboardWhitelist 源码 2 | 3 | ## ClipboardWhiteList 4 | 5 | ### 原理 6 | 7 | https://github.com/Kr328/Riru-ClipboardWhitelist/blob/main/module/src/main/java/com/github/kr328/clipboard/ClipboardProxy.java#L1 8 | 9 | 将白名单中的 app 进行的 IClipboard 的 binder 调用来源(包括包名和 uid)换成默认输入法的。 10 | 11 | 方法还是传统的 Binder Proxy ,记得以前是用 Java proxy 实现的,似乎近期改换了实现。 12 | 13 | 比较有意思的是 uid 的替换,因为 uid 不通过调用参数传递。在 `Binder.restoreCallingIdentity` 中做手脚,替换 uid ,使得系统通过 `Binder.getCallingUid` 读取到的就是我们想让它读到的 uid 。 14 | 15 | ClipboardService 中在 `clipboardAccessAllowed` 方法处理的剪贴板访问权限 16 | 17 | 对于读剪贴板操作来说,`clipboardAccessAllowed` 可以通过。对于监听器来说,系统会记录调用者的包名,在剪贴板变更,分发回调的时候再次调用 `clipboardAccessAllowed` ,因此需要修改记录的包名。 18 | 19 | 疑问:为什么不直接用 `android` 或者 `com.android.systemui` 20 | 21 | ## ZygoteLoader 22 | 23 | ### customize.sh 24 | 25 | ZygoteLoader 的 gradle plugin 收集插件自身和用户定义的 sh ,按照文件名顺序将不同阶段任务的 sh 整合到最终生成一个的 `customize.sh` 中依次调用,用户可以自行扩展。 26 | 27 | ### 数据目录 28 | 29 | https://github.com/Kr328/ZygoteLoader/blob/16d550b499fc273191f63987ff0971efc797646c/runtime/src/main/assets/customize.d/40-initialize-data-directory.sh#L24 30 | 31 | 数据目录在 sh 里面初始化,随机生成名字写到 `/data/system/zloader-xxxxxx` ,并记录到 `/data/adb/zloader/data-root-directory` 里面(可能是用于共享) 32 | 33 | 还会写到模块的 module.prop 。ZygoteLoader 会读取 module.prop 上报到 java 层,便于修改配置。 34 | -------------------------------------------------------------------------------- /commited-secrets-in-git.md: -------------------------------------------------------------------------------- 1 | # 记年轻人的第一次 git 误提交 2 | 3 | ## 一切的开始 4 | 5 | 最近在写 android app 大作业的时候用到了 git 管理项目,今天给它加上了 release 的 signingConfigs ,方便在 gradle task 中自动签名。 6 | 7 | 我的 app 签名基本用的都是同一个 keystore 文件,里面有密码。写的时候直接从老项目复制了一个 keystore 过来。 8 | 9 | 然而我没注意创建文件的时候 keystore.properties 已经被 AS 自动 add 到缓存里面了。测试完成之后,直接提交,才发现我的 keystore 文件已经出现在了 git 的历史里面。 10 | 11 | 于是我赶紧把这个提交在 AS 的 git ui 里面给 undo 了,然后在 gitignore 补上 keystore ,重新提交——这样看起来好像就完事了。 12 | 13 | 泄露密码的危机这样就解决了吗?我想起以前看 git book 的时候有[这么一句话](https://git-scm.com/book/zh/v2/Git-%E5%9F%BA%E7%A1%80-%E6%92%A4%E6%B6%88%E6%93%8D%E4%BD%9C): 14 | 15 | > 记住,在 Git 中任何 **已提交** 的东西几乎总是可以恢复的。 16 | 17 | 因此事情或许没那么简单,不能这么草率地就 push 到 github 上了。 18 | 19 | ## 探究 20 | 21 | [Git - 维护与数据恢复](https://git-scm.com/book/zh/v2/Git-%E5%86%85%E9%83%A8%E5%8E%9F%E7%90%86-%E7%BB%B4%E6%8A%A4%E4%B8%8E%E6%95%B0%E6%8D%AE%E6%81%A2%E5%A4%8D#_data_recovery) 22 | 23 | 回顾之前的操作,我们需要找到误提交那次的 hash ,由于它已经不在 master 分支上,因此找起来就麻烦了。 24 | 25 | 好在 AS 有一个 console ,里面记录了 git 命令的调用流程。 26 | 27 | ![](res/images/20230402_01.png) 28 | 29 | `398b6bc` 就是把 keystore 提交上去的那个 commit 。 30 | 31 | 使用 `git reflog` 查看,它果然还在: 32 | 33 | ![](res/images/20230402_02.png) 34 | 35 | 把它 checkout 出来看看: 36 | 37 | `git branch wtf 398b6bc` 38 | 39 | ![](res/images/20230402_03.png) 40 | 41 | 在这个分支可以看到被提交上去的 keystore 和其中的密码。 42 | 43 | 观察到 reflogs 不仅包含了我们 undo 的那次提交,甚至还有以前 amend 的提交。那么这些 ref 到底会不会被推送到 remote 呢? 44 | 45 | 于是我从 github 上又 clone 了一份仓库下来,观察它的 reflog ,发现只有一条: 46 | 47 | ![](res/images/20230402_04.png) 48 | 49 | 尝试 checkout 一个 amend 的提交,发现是不能的: 50 | 51 | ``` 52 | # 在原来工作的本地仓库 git reflog 53 | 073aec9 HEAD@{14}: commit (amend): generator: support share qrcode 54 | 8fe6c00 HEAD@{15}: commit: generator: support share qrcode 55 | 56 | # 在新 clone 的本地仓库 57 | D:\Documents\tmp\Scanner>git branch aa 8fe6c00 58 | fatal: Not a valid object name: '8fe6c00'. 59 | 60 | D:\Documents\tmp\Scanner>git branch aa 073aec9 61 | ``` 62 | 63 | 看起来本地的 ref 并没有都推送到 remote 。 64 | 65 | 实际上,reflog 看到的是 local 仓库的 ref 记录。 66 | 67 | [Git - git-reflog Documentation](https://git-scm.com/docs/git-reflog) 68 | 69 | AS 中 undo commit 用的是 `reset --soft` ,那么这样推送到远程是否安全呢?查了几个回答,得到的答案都是肯定的: 70 | 71 | [Can `git reset --soft` be used to undo secrets from a commit before pushing to a remote (Github)? - Stack Overflow](https://stackoverflow.com/questions/66129120/can-git-reset-soft-be-used-to-undo-secrets-from-a-commit-before-pushing-to-a) 72 | 73 | 从上面的实验来看,那些 amend 的提交仅存在于本地,从远程再拉回来是不存在的。这样总算可以大胆地 push 了。 74 | 75 | ## 后记 76 | 77 | 在仓库目录里面放 keystore 还是有些危险,一不小心就会出现这样的差错,好在 undo 还是有效的。 78 | 79 | 仔细想想,如果在 gradle 里面从 home 目录读取 keystore 也许会更好。 80 | -------------------------------------------------------------------------------- /core-patch-extend.md: -------------------------------------------------------------------------------- 1 | # 核心破解 2 | 3 | 核心破解([CorePatch](https://github.com/LSPosed/CorePatch))是一个针对 PMS 进行修改的 Xposed 模块,它具有以下功能: 4 | 5 | 1. 允许缺少签名、签名错误的包安装到系统中; 6 | 2. 允许降级安装; 7 | 3. 允许与系统中存在的包不同签名的新 apk 直接覆盖安装。(「禁用 APK 签名验证」) 8 | 9 | 但是它并不能解决所有的问题,至今核心破解还存在两大难题: 10 | 11 | ## 两大难题 12 | 13 | ### 自定义权限 14 | 15 | 如果一个包声明了自定义权限,即使用了核心破解的禁用签名验证,仍然无法安装,并且有以下报错: 16 | 17 | > 这个权限的保护等级不需要是「signature」的 18 | 19 | ``` 20 | Failure [INSTALL_FAILED_DUPLICATE_PERMISSION: Package fivecc.tools.signdemo attempting to redeclare permission fivecc.tools.MY_PERMISSION already owned by fivecc.tools.signdemo] 21 | ``` 22 | 23 | 据说 3.8 是可用的,而 4.2 反而不可用。 24 | 25 | 是由于这个提交: 26 | 27 | [Don't hook checkCapability() for permission usecase · LSPosed/CorePatch@65a3ee5](https://github.com/LSPosed/CorePatch/commit/65a3ee5245d42b0ac2348a32b074f298f359e295) 28 | 29 | ```java 30 | hookAllMethods("android.content.pm.PackageParser", loadPackageParam.classLoader, "checkCapability", new XC_MethodHook() { 31 | @Override 32 | protected void beforeHookedMethod(MethodHookParam param) { 33 | // Don't handle PERMISSION (grant SIGNATURE permissions to pkgs with this cert) 34 | // Or applications will have all privileged permissions 35 | // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/content/pm/PackageParser.java;l=5947?q=CertCapabilities 36 | if ((Integer) param.args[1] != 4) { 37 | param.setResult(true); 38 | } 39 | } 40 | }); 41 | ``` 42 | 43 | 根据注释,是为了防止安装任意 apk 都被给予系统权限;但是这也导致了具有自定义权限的 app 在签名不同时无法通过核心破解覆盖安装。 44 | 45 | 现在包含了 androidx core 的 app 都会带一个 DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION 权限,因此核心破解对很多新 app 可能都失效了。 46 | 47 | 这个权限应该是为了向下移植 Android 13 的 registerReceiver 的 `RECEIVER_(NOT_)EXPORTED` flags 。 48 | 49 | [功能和 API 概览  |  Android 开发者  |  Android Developers](https://developer.android.google.cn/about/versions/13/features?hl=zh-cn#runtime-receivers) 50 | 51 | 实际上,安装一个不同签名的 app ,重新定义其中自己的权限,对用户来说不应该是不安全的(既然用户都决定用核心破解了),因此这个还是有必要修复的。 52 | 53 | ### SharedUser 54 | 55 | ``` 56 | Failure [INSTALL_FAILED_SHARED_USER_INCOMPATIBLE: Package fivecc.tools.signdemo has a signing lineage that diverges from the lineage of the sharedUserId] 57 | ``` 58 | 59 | 虽然 shared user 已经[废弃](https://developer.android.com/guide/topics/manifest/manifest-element?hl=zh-cn),但是还是有一些旧的 app 需要这个 feature (知名的如 Termux)。这个问题导致 termux 难以在不同签名的版本之间迁移(毕竟要把整个 termux 的用户目录搬走,移动到新的版本还是很麻烦的,有可能遇到各种问题)。 60 | 61 | ## 如何解决? 62 | 63 | 未完待续 64 | 65 | 70 | -------------------------------------------------------------------------------- /dark-mode-and-debug-layout.md: -------------------------------------------------------------------------------- 1 | > 下面研究两个同样会影响所有 ui 绘制的设置,以及如何用简单的 shell 命令修改。 2 | 3 | # 深色模式 4 | 5 | 比较开启深色模式前后的 system settings ,发现变化的值(MIUI 12.5 上): 6 | 7 | ``` 8 | # settings list system > d1 (d2) 9 | # diff d1 d2 10 | 73c73 11 | < dark_mode_enable=0 12 | --- 13 | > dark_mode_enable=1 14 | 208c208 15 | < smart_dark_enable=0 16 | --- 17 | > smart_dark_enable=1 18 | ``` 19 | 20 | 直接 `settings put` 并不能切换深色模式,看上去系统并不会监视值的变化,需要我们手动触发。 21 | 22 | 搜索设置相关源码(其实是反编译,直接搜上面的 key 找不到),发现是通过 UiModeManager 设置的,这是一个系统服务 (`uimode`)。 23 | 24 | ``` 25 | frameworks/base/core/java/android/app/UiModeManager.java 26 | frameworks/base/core/java/android/app/IUiModeManager.aidl 27 | ``` 28 | 29 | 它实现了 ShellCommand ,可以直接通过 shell 命令控制深色模式。 30 | 31 | ``` 32 | # service list | grep uimode 33 | 198 uimode: [android.app.IUiModeManager] 34 | # cmd uimode 35 | UiModeManager service (uimode) commands: 36 | help 37 | Print this help text. 38 | night [yes|no|auto|custom] 39 | Set or read night mode. 40 | time [start|end] 41 | Set custom start/end schedule time (night mode must be set to custom to apply). 42 | 43 | # cmd uimode night yes 44 | Night mode: yes 45 | ``` 46 | 47 | # 显示布局边界 48 | 49 | 这个设置存在了 sysprops 里面,同样 diff : 50 | 51 | ``` 52 | # getprop > d1 (d2) 53 | # diff d1 d2 54 | 62c62 55 | < [debug.layout]: [false] 56 | --- 57 | > [debug.layout]: [true] 58 | ``` 59 | 60 | 直接 setprop ,也不会立即影响已有的 view ,只对新创建的 view 有效。 61 | 62 | 搜索 `debug_layout` ,在设置中: 63 | 64 | ``` 65 | packages/apps/Settings/src/com/android/settings/development/ShowLayoutBoundsPreferenceController.java 66 | ``` 67 | 68 | > 也可以找一找它的 Tile 69 | 70 | ```java 71 | @Override 72 | public boolean onPreferenceChange(Preference preference, Object newValue) { 73 | final boolean isEnabled = (Boolean) newValue; 74 | DisplayProperties.debug_layout(isEnabled); 75 | SystemPropPoker.getInstance().poke(); 76 | return true; 77 | } 78 | ``` 79 | 80 | DisplayProperties 似乎是生成的类,反编译看到仅仅是调用 native 代码设置了 prop ,因此关键在下面的 `SystemPropPoker` 。 81 | 82 | Poker 会创建一个 AsyncTask ,check 所有系统服务,并向它们发送一个 transact ,code 为 `IBinder.SYSPROPS_TRANSACTION` 83 | 84 | ```java 85 | // frameworks/base/packages/SettingsLib/src/com/android/settingslib/development/SystemPropPoker.java 86 | @Override 87 | protected Void doInBackground(Void... params) { 88 | String[] services = listServices(); 89 | if (services == null) { 90 | Log.e(TAG, "There are no services, how odd"); 91 | return null; 92 | } 93 | for (String service : services) { 94 | IBinder obj = checkService(service); 95 | if (obj != null) { 96 | Parcel data = Parcel.obtain(); 97 | try { 98 | obj.transact(IBinder.SYSPROPS_TRANSACTION, data, null, 0); 99 | } catch (RemoteException e) { 100 | // Ignore 101 | } catch (Exception e) { 102 | Log.i(TAG, "Someone wrote a bad service '" + service 103 | + "' that doesn't like to be poked", e); 104 | } 105 | data.recycle(); 106 | } 107 | } 108 | return null; 109 | } 110 | ``` 111 | 112 | 这个 code 值为 `1599295570` ,看起来是专门用于通知系统属性改变的。 113 | 114 | ```java 115 | // frameworks/base/core/java/android/os/IBinder.java 116 | /** @hide */ 117 | @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 118 | int SYSPROPS_TRANSACTION = ('_'<<24)|('S'<<16)|('P'<<8)|'R'; 119 | ``` 120 | 121 | 尝试修改 `debug.layout` 为 true 后给 `window`, `SurfaceFlinger`, `activity` 发这个 transact ,发现只有 `activity` 会提醒到 view 绘制边界。 122 | 123 | 于是利用 shell 命令开关「显示布局边界」如下: 124 | 125 | ``` 126 | # 开启 127 | setprop debug.layout true; service call activity 1599295570 128 | # 关闭 129 | setprop debug.layout false; service call activity 1599295570 130 | ``` 131 | 132 | > 在 ViewRootImpl 中的 `loadSystemProperties` 负责处理这个属性的变化。在 WindowManagerGlobal 中通过 SystemProperties.addChangeCallback 设置了监听器。 133 | 134 | > 参考:[“显示布局边界”的原理 | 姜康的技术博客](https://www.jiangkang.tech/2021/01/19/android/xian-shi-bu-ju-bian-jie-de-yuan-li/#toc-heading-2) 135 | 136 | # 附录:props 的更新回调机制 137 | 138 | 在 native 层 Binder 的 onTransact 中处理了 SYSPROPS_TRANSACTION : 139 | 140 | ```cpp 141 | // frameworks/native/libs/binder/Binder.cpp 142 | status_t BBinder::onTransact( 143 | uint32_t code, const Parcel& data, Parcel* reply, uint32_t /*flags*/) 144 | { 145 | switch (code) { 146 | // ... 147 | case SYSPROPS_TRANSACTION: { 148 | report_sysprop_change(); 149 | return NO_ERROR; 150 | } 151 | 152 | default: 153 | return UNKNOWN_TRANSACTION; 154 | } 155 | } 156 | ``` 157 | 158 | 而 AMS 中也有一个 onTransact : 159 | 160 | ```java 161 | // frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java 162 | @Override 163 | public boolean onTransact(int code, Parcel data, Parcel reply, int flags) 164 | throws RemoteException { 165 | if (code == SYSPROPS_TRANSACTION) { 166 | // We need to tell all apps about the system property change. 167 | ArrayList procs = new ArrayList(); 168 | synchronized (mProcLock) { 169 | final ArrayMap> pmap = 170 | mProcessList.getProcessNamesLOSP().getMap(); 171 | final int numOfNames = pmap.size(); 172 | for (int ip = 0; ip < numOfNames; ip++) { 173 | SparseArray apps = pmap.valueAt(ip); 174 | final int numOfApps = apps.size(); 175 | for (int ia = 0; ia < numOfApps; ia++) { 176 | ProcessRecord app = apps.valueAt(ia); 177 | final IApplicationThread thread = app.getThread(); 178 | if (thread != null) { 179 | procs.add(thread.asBinder()); 180 | } 181 | } 182 | } 183 | } 184 | 185 | int N = procs.size(); 186 | for (int i=0; i 这里使用了 SurfaceFlinger backdoor ,需要 root/system uid 才能调用,shell 一般是不行的。 28 | > [`frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp`](https://android.googlesource.com/platform/frameworks/native/+/213454462ca60ec8af20f69a797bbef19712b85b/services/surfaceflinger/SurfaceFlinger.cpp#5980) 29 | 30 | ## 时间悬浮窗(MIUI) 31 | 32 | 在 system settings 中,key 为 miui_time_floating_window 33 | 34 | > 这种原生没有的情况下使用 settings diff 就比较好找。 35 | 36 | ``` 37 | settings put system miui_time_floating_window 0|1 38 | ``` 39 | 40 | ## 显示触摸 41 | 42 | `packages/apps/Settings/src/com/android/settings/development/ShowTapsPreferenceController.java` 43 | 44 | key: show_touches 45 | 46 | > AOSP 的 strings.xml 竟然没有翻译…… 47 | 48 | ``` 49 | settings put system show_touches 0|1 50 | ``` 51 | 52 | ## 指针位置 53 | 54 | `packages/apps/Settings/src/com/android/settings/development/PointerLocationPreferenceController.java` 55 | 56 | pointer_location 57 | 58 | > 同上,也没有翻译 59 | 60 | `settings put system pointer_location 0|1` 61 | 62 | ## 显示布局边界 63 | 64 | [详情](dark-mode-and-debug-layout.md) 65 | 66 | `packages/apps/Settings/src/com/android/settings/development/ShowLayoutBoundsPreferenceController.java` 67 | 68 | ``` 69 | # 开启 70 | setprop debug.layout true; service call activity 1599295570 71 | # 关闭 72 | setprop debug.layout false; service call activity 1599295570 73 | ``` 74 | -------------------------------------------------------------------------------- /frida-java-bridge.md: -------------------------------------------------------------------------------- 1 | # Frida Java Bridge 2 | 3 | https://github.com/frida/frida-java-bridge 4 | 5 | ## implementation 6 | 7 | https://github.com/frida/frida-java-bridge/blob/58030ace413a9104b8bf67f7396b22bf5d889e43/lib/class-factory.js#L1676 8 | 9 | https://github.com/frida/frida-java-bridge/blob/58030ace413a9104b8bf67f7396b22bf5d889e43/lib/android.js#L3347 10 | 11 | `ArtMethodMangler.replace` 12 | 13 | 复制原 ArtMethod 得到 replacement 方法,是一个 native 方法,实现放在 jniCode 中。 14 | 15 | ```js 16 | patchArtMethod(replacementMethodId, { 17 | jniCode: impl, 18 | accessFlags: ((originalFlags & ~(kAccCriticalNative | kAccFastNative | kAccNterpEntryPointFastPathFlag)) | kAccNative | kAccCompileDontBother) >>> 0, 19 | quickCode: api.artClassLinker.quickGenericJniTrampoline, 20 | interpreterCode: api.artInterpreterToCompiledCodeBridge 21 | }, vm); 22 | ``` 23 | 24 | 原方法强制走解释器 25 | 26 | ```js 27 | // Remove kAccFastInterpreterToInterpreterInvoke and kAccSkipAccessChecks to disable use_fast_path 28 | // in interpreter_common.h 29 | let hookedMethodRemovedFlags = kAccFastInterpreterToInterpreterInvoke | kAccSingleImplementation | kAccNterpEntryPointFastPathFlag; 30 | if ((originalFlags & kAccNative) === 0) { 31 | hookedMethodRemovedFlags |= kAccSkipAccessChecks; 32 | } 33 | 34 | patchArtMethod(hookedMethodId, { 35 | accessFlags: ((originalFlags & ~(hookedMethodRemovedFlags)) | kAccCompileDontBother) >>> 0 36 | }, vm); 37 | 38 | const quickCode = this.originalMethod.quickCode; 39 | 40 | // Replace Nterp quick entrypoints with art_quick_to_interpreter_bridge to force stepping out 41 | // of ART's next-generation interpreter and use the quick stub instead. 42 | if (artNterpEntryPoint !== undefined && quickCode.equals(artNterpEntryPoint)) { 43 | patchArtMethod(hookedMethodId, { 44 | quickCode: api.artQuickToInterpreterBridge 45 | }, vm); 46 | } 47 | ``` 48 | 49 | ### kAccFastInterpreterToInterpreterInvoke 50 | 51 | 强制走 switch 实现而不是 mterp ,这样才能进入 DoCall 52 | 53 | A11: 54 | 55 | http://aospxref.com/android-11.0.0_r21/xref/art/runtime/art_method.h#319 56 | 57 | ```cpp 58 | bool UseFastInterpreterToInterpreterInvoke() const { 59 | // The bit is applicable only if the method is not intrinsic. 60 | constexpr uint32_t mask = kAccFastInterpreterToInterpreterInvoke | kAccIntrinsic; 61 | return (GetAccessFlags() & mask) == kAccFastInterpreterToInterpreterInvoke; 62 | } 63 | ``` 64 | 65 | http://aospxref.com/android-11.0.0_r21/xref/art/runtime/interpreter/interpreter_common.h?fi=DoInvoke#319 66 | 67 | ```cpp 68 | static ALWAYS_INLINE bool DoInvoke(Thread* self, 69 | ShadowFrame& shadow_frame, 70 | const Instruction* inst, 71 | uint16_t inst_data, 72 | JValue* result) 73 | use_fast_path = called_method->UseFastInterpreterToInterpreterInvoke(); 74 | 75 | if (use_fast_path) { 76 | // mterp 77 | } 78 | 79 | return DoCall(called_method, self, shadow_frame, inst, inst_data, 80 | result); 81 | ``` 82 | 83 | ## art controller 84 | 85 | https://github.com/frida/frida-java-bridge/blob/58030ace413a9104b8bf67f7396b22bf5d889e43/lib/android.js#L1841 86 | 87 | hook `art::interpreter::DoCall` 88 | 89 | ```js 90 | function instrumentArtMethodInvocationFromInterpreter () { 91 | const apiLevel = getAndroidApiLevel(); 92 | 93 | let artInterpreterDoCallExportRegex; 94 | if (apiLevel <= 22) { 95 | artInterpreterDoCallExportRegex = /^_ZN3art11interpreter6DoCallILb[0-1]ELb[0-1]EEEbPNS_6mirror9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE$/; 96 | } else { 97 | artInterpreterDoCallExportRegex = /^_ZN3art11interpreter6DoCallILb[0-1]ELb[0-1]EEEbPNS_9ArtMethodEPNS_6ThreadERNS_11ShadowFrameEPKNS_11InstructionEtPNS_6JValueE$/; 98 | } 99 | 100 | for (const exp of Module.enumerateExports('libart.so').filter(exp => artInterpreterDoCallExportRegex.test(exp.name))) { 101 | Interceptor.attach(exp.address, artController.hooks.Interpreter.doCall); 102 | } 103 | } 104 | ``` 105 | 106 | https://github.com/frida/frida-java-bridge/blob/58030ace413a9104b8bf67f7396b22bf5d889e43/lib/android.js#L1706C1-L1716C2 107 | 108 | 替换第一个参数为被替换的方法 109 | 110 | ```c 111 | void 112 | on_interpreter_do_call (GumInvocationContext * ic) 113 | { 114 | gpointer method, replacement_method; 115 | 116 | method = gum_invocation_context_get_nth_argument (ic, 0); 117 | 118 | replacement_method = get_replacement_method (method); 119 | if (replacement_method != NULL) 120 | gum_invocation_context_replace_nth_argument (ic, 0, replacement_method); 121 | } 122 | ``` 123 | 124 | ... 125 | 126 | ``` 127 | #22 pc 000000000013f7d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: 734ef1b8ef156af69af806e34ced7fac) 128 | #23 pc 0000000000a2d58c /data/app/~~sZ208PWnIVkknU3lQtjBAQ==/io.github.a13e300.demo-SSntgDgXpiAKakwGSEo49A==/oat/arm64/base.vdex (io.github.a13e300.demo.SyncCallActivity.onCreate) 129 | #24 pc 0000000000306dc0 /apex/com.android.art/lib64/libart.so (art::interpreter::Execute(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame&, art::JValue, bool, bool) (.llvm.11487796752256266877)+532) (BuildId: 734ef1b8ef156af69af806e34ced7fac) 130 | #25 pc 000000000030eca8 /apex/com.android.art/lib64/libart.so (art::interpreter::ArtInterpreterToInterpreterBridge(art::Thread*, art::CodeItemDataAccessor const&, art::ShadowFrame*, art::JValue*)+200) (BuildId: 734ef1b8ef156af69af806e34ced7fac) 131 | #26 pc 000000000030f6a0 /apex/com.android.art/lib64/libart.so (bool art::interpreter::DoCall(art::ArtMethod*, art::Thread*, art::ShadowFrame&, art::Instruction const*, unsigned short, art::JValue*)+968) (BuildId: 734ef1b8ef156af69af806e34ced7fac) 132 | #27 pc 00000000001489d0 /apex/com.android.art/lib64/libart.so (void art::interpreter::ExecuteSwitchImplCpp(art::interpreter::SwitchImplContext*)+28080) (BuildId: 734ef1b8ef156af69af806e34ced7fac) 133 | #28 pc 000000000013f7d8 /apex/com.android.art/lib64/libart.so (ExecuteSwitchImplAsm+8) (BuildId: 734ef1b8ef156af69af806e34ced7fac) 134 | #29 pc 00000000001b012c /system/framework/framework.jar (android.app.Activity.performCreate) 135 | ``` 136 | 137 | 138 | -------------------------------------------------------------------------------- /gradle-source-dependency.md: -------------------------------------------------------------------------------- 1 | # 令人困惑的 Gradle 源码依赖 2 | 3 | 最近想试用一下 LSPosed 的新 API ,clone example 下来发现无法 sync ,提示 `Unknown build name 'api'` ,但是可以正常 build 。找同学测试了一下,也是一样的结果。 4 | 5 | https://github.com/libxposed/example 6 | 7 | 发现这是由于 libxposed example 使用了 gradle 的 source dependencies 导致的,删除相关代码后可以 sync 成功,但是必须用其他的方法引入依赖。 8 | 9 | https://blog.gradle.org/introducing-source-dependencies 10 | 11 | 类似的还有 LSPosed ,自从下面这个提交开始: 12 | 13 | https://github.com/LSPosed/LSPosed/commit/03d2cea093db056d56dbbea2ee13831e3dc5253a 14 | 15 | source dependencies 在 Android Studio 稳定版 (E) 会出错,想着升级肯定就没问题了,于是升级到 canary (G) 后,虽然能 sync 成功,但 IDE 的依赖解析几乎完全坏掉,每个模块依赖外部的类都无法显示。 16 | 17 | 当然可以自己主动删掉 source dependencies ,改用 maven local ,但是这样是否有些不方便了?看到 LSPosed 的开发者这两个星期还在不断提交,怎么看都像是能正常使用 IDE 的。 18 | 19 | 在 Discussion 看到类似问题,也去下面提问,却在群里收到西大师的一个问号。 20 | 21 | https://github.com/orgs/LSPosed/discussions/2444 22 | 23 | 我以为是自己无知,问了一个如此愚蠢的问题,一时间不知道如何回应。但是在群里翻了一下,从那个提交到现在也没有讨论过相关问题,想来大家都能正常工作。也许其他开发者本地就是改用 maven local ;或者干脆就不使用 IDE 开发——这我是做不到的。 24 | 25 | 后来我反复测试,确认确实是 IDE 的问题。 26 | 27 | 在 jetbrains 的 issue tracker 看到这个问题在四年前就有人反馈,至今似乎也没解决。 28 | 29 | https://youtrack.jetbrains.com/issue/IDEA-208388/New-Gradle-source-dependencies-integration 30 | 31 | 我想新 API 的 example 既然是要给开发者使用的,起码要能正常在 IDE 中工作。如果 source dependencies 真的还没有被 IDE 所集成,还是应该改掉的。 32 | -------------------------------------------------------------------------------- /icalingua-homework.md: -------------------------------------------------------------------------------- 1 | # 使用 Icalingua 打开群作业 2 | 3 | [Icalingua-plus-plus/Icalingua-plus-plus: A client for QQ and more.](https://github.com/Icalingua-plus-plus/Icalingua-plus-plus) 4 | 5 | 菜单中可以打开群公告,而且是打开了网页,但是未提供群作业(本质上也是网页) 6 | 7 | Icalingua 没有提供公开的 API 打开 qq url ,直接任意打开链接(或者 devtools 中 window.open)会用本地浏览器打开。 8 | 9 | 菜单的逻辑似乎不在 web 中,仅凭内置 devtools 似乎无法打开 icalingua 的浏览器窗口,而其打开的扩展窗口都没法直接访问 devtools 。 10 | 11 | 干脆 clone 源码找一找 12 | 13 | ```js 14 | // icalingua\src\main\ipc\menuManager.ts 15 | menu.append( 16 | new MenuItem({ 17 | label: '群公告', 18 | async click() { 19 | const size = screen.getPrimaryDisplay().size 20 | const win = newIcalinguaWindow({ 21 | height: size.height - 200, 22 | width: 500, 23 | autoHideMenuBar: true, 24 | title: '群公告', 25 | }) 26 | const cookies = await getCookies('qun.qq.com') 27 | for (const i in cookies) { 28 | await win.webContents.session.cookies.set({ 29 | url: 'https://web.qun.qq.com', 30 | name: i, 31 | value: cookies[i], 32 | }) 33 | } 34 | await win.loadURL('https://web.qun.qq.com/mannounce/index.html#gc=' + -room.roomId) 35 | }, 36 | }), 37 | ) 38 | ``` 39 | 40 | 可以看到主要是两个步骤:获取 cookies ,打开浏览器窗口加载 url 。 41 | 42 | 由于编译起来太麻烦,因此我们直接修改现成的 icalingua 。 43 | 44 | 在命令行打开 icalingua 可以进入 repl ,但 electron 在 Windows 上不支持 repl (只能输出不能输入),所以开一个 node.js 调试 45 | 46 | [Debugging the Main Process | Electron](https://www.electronjs.org/docs/latest/tutorial/debugging-main-process#--inspectport) 47 | 48 | 启动 `Icalingua++.exe --inspect=5858` 49 | 50 | Chrome devtools 附加 Node.js 调试(在 `chrome://inspect`) 51 | 52 | ![](res/images/20221020_01.png) 53 | 54 | 我们要获得 newIcalinguaWindow 和 getCookies 函数,找了一下 require ,发现并非像官方所说不能用,但是也 require 不到 icalingua 的模块。 55 | 56 | 干脆直接找到相关源码,然后断点调试,在菜单点击一下群公告就可以拿到两个函数了。 57 | 58 | icalingua 的所有代码都被编译到 `dist/electron/main.js` 里面了。 59 | 60 | ![](res/images/20221020_02.png) 61 | 62 | 断点停下来的时候 console 输入: 63 | 64 | ```js 65 | _A=A; 66 | _b=b; 67 | ``` 68 | 69 | 模仿上面的逻辑: 70 | 71 | ```js 72 | mwin=_b.newIcalinguaWindow({ 73 | height: 300, 74 | width: 500, 75 | autoHideMenuBar: true, 76 | title: 'aaa', 77 | }) 78 | cookies = await _A.getCookies('qun.qq.com') 79 | for (const i in cookies) { 80 | await mwin.webContents.session.cookies.set({ 81 | url: 'https://qun.qq.com', 82 | name: i, 83 | value: cookies[i], 84 | }) 85 | } 86 | await mwin.loadURL('https://qun.qq.com/homework/features/detail.html?_wv=1027&_bid=2146#web=1&src=6&hw_id=xxx&puin=xxx&hw_type=0&need_feedback=1&gid=xxx&group_code=xxx&group_id=xxx&open_web=1') 87 | ``` 88 | 89 | ![](res/images/20221020_03.png) 90 | 91 | 可以打开群作业了,但是点击下面的「去完成」会显示「请在手机完成」的 alert 92 | 93 | `mwin.openDevTools()` 打开 devtools 。 94 | 95 | 在 alert 下断点,发现是通过 UA 判断的,需要 startsWith `QQ/` 96 | 97 | 由于 newIcalinguaWindow 内部使用了 electron.BrowserWindow (`icalingua\src\utils\IcalinguaWindow.ts`),我们可以在 loadURL 的时候定义 UA : 98 | 99 | [javascript - How to set electron UserAgent - Stack Overflow](https://stackoverflow.com/questions/35672602/how-to-set-electron-useragent) 100 | 101 | ```js 102 | await mwin.loadURL('https://qun.qq.com/homework/features/detail.html?_wv=1027&_bid=2146#web=1&src=6&hw_id=xxx&puin=xxx&hw_type=0&need_feedback=1&gid=xxx&group_code=xxx&group_id=xxx&open_web=1', { userAgent: "QQ/114514" }) 103 | ``` 104 | 105 | 这样还是没法打开,进一步得知其会调用 QQ 的扩展 API `window.external.openWebWindow` ,那我们就实现一个: 106 | 107 | ```js 108 | window.external.openWebWindow=(d)=>{console.log(d);location.href=JSON.parse(d).url;} 109 | ``` 110 | 111 | ![](res/images/20221020_04.png) 112 | 113 | 这样就可以进入提交页面了 114 | 115 | 另外我们还需要群作业的主页面,远程调试 qq 的内置浏览器得知 url 为: 116 | 117 | ``` 118 | https://qun.qq.com/homework/features/v2/index.html?_wv=1027&_bid=3089&gc=${group_id} 119 | ``` 120 | 121 | 用自带的 UA 访问也会有问题,会找不到参数并疯狂请求(真不懂 QQ 这帮前端的水平……);以下是我设备的 UA ,可以正常打开手机版的群作业: 122 | 123 | ``` 124 | Mozilla/5.0 (Linux; Android 11; Redmi K30 5G Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.72 MQQBrowser/6.2 TBS/046209 Mobile Safari/537.36 V1_AND_SQ_8.8.50_2324_YYB_D A_8085000 QQ/8.8.50.6735 NetType/WIFI WebP/0.3.0 Pixel/1080 StatusBarHeight/96 SimpleUISwitch/0 QQTheme/1000 InMagicWin/0 StudyMode/0 CurrentMode/0 CurrentFontScale/1.0 125 | ``` 126 | 127 | 精简成下面的:`Android V1_AND_SQ_8` (如果把 8 删除就是电脑版了) 128 | 129 | 如果把含 Android 的部分删除,可以打开电脑版页面: 130 | 131 | ``` 132 | AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/89.0.4389.72 MQQBrowser/6.2 TBS/046209 Mobile Safari/537.36 V1_AND_SQ_8.8.50_2324_YYB_D A_8085000 QQ/8.8.50.6735 NetType/WIFI WebP/0.3.0 Pixel/1080 StatusBarHeight/96 SimpleUISwitch/0 QQTheme/1000 InMagicWin/0 StudyMode/0 CurrentMode/0 CurrentFontScale/1.0 133 | ``` 134 | 135 | 看上去 `V1_AND_SQ_` 是关键,如果删掉就会导致打不开,而如果整个 UA 只有这个字段也是能打开的。 136 | 137 | 上面 url 的参数似乎只有 gc (群 id)有用,bid 似乎是随机的,但删除也不影响正常打开。 138 | 139 | 另外无论是 pc 还是手机页面,都没法在里面操作,看来私有 API 太多了! 140 | 141 | 代码搜索 detail.html ,发现手机版是从 `mqq.ui.openUrl` 打开的。我们添加一下: 142 | 143 | `mqq.ui.openUrl=function(u) {location.href = u.url;}` 144 | 145 | 可以打开,但是这样出现的页面根本没法提交(提交按钮是放在内置浏览器 ui 上的,可能有别的 api) 146 | 147 | 它打开的 url 148 | 149 | 未完成: `//qun.qq.com/homework/features/answer.html?_wv=1027#group_id=xxxxx&hw_id=xxxxx&hw_type=0` 150 | 151 | 已完成:`//qun.qq.com/homework/features/v2/feedback.html?_wv=16778243&_bid=3089&gc=xxxxx&hw_id=xxxxx&isAdmin=0&uin=xxxxx&course_name=xxxxx&originalRole=335` 152 | 153 | 154 | 再看 PC 端的,它会重定向到 `https://qun.qq.com/homework/p/features/index.html#/detail?` ,实际上是只要 UA 不含 `_SQ_` 就会重定向。 155 | 156 | 这个页面似乎参数不对,然后会疯狂请求一个 api ,不管怎么改参数都不对 157 | 158 | …… 159 | 160 | 经过反复尝试,发现作业列表实际上是 `https://qun.qq.com/homework/p/features#?gid=${group_id}` ,上面那个对应的是特定作业的页面。 161 | 162 | 这个页面上的内容就可以点开了。 163 | 164 | 因此最终思路就是: 165 | 166 | ```js 167 | await mwin.loadURL('https://qun.qq.com/homework/p/features#?gid=xxxxx', {userAgent:"QQ/"}) 168 | // 注入 js 到 qun.qq.com 域名 169 | window.external.openWebWindow=(d)=>{console.log(d);location.href=JSON.parse(d).url;} 170 | ``` 171 | 172 | ## 开发 icalingua 173 | 174 | npm install -g pnpm 175 | 176 | 在 icalingua 仓库目录下: 177 | 178 | ```sh 179 | pnpm install 180 | cd icalingua 181 | # 启动 182 | npm run dev 183 | ``` 184 | 185 | dev 启动会开启 inspector ,默认端口也是 5858 186 | 187 | ![](res/images/20221020_05.png) 188 | 189 | 看上去 dev 模式下的 userData 和实际打包的不一样。 190 | 191 | icalingua 的数据目录在 `%APPDATA%\icalingua` 192 | 193 | dev 模式的数据目录在 `%APPDATA%\Electron` 194 | 195 | 可以通过 `require('electron').app.getPath("userData")` 获取。 196 | 197 | 直接复制安装的 icalingua 数据的 config.yml 到 dev 的数据,可以登录(密码直接写在里面) 198 | 199 | > 但是调试热更新需要反复重启,然后 qq 就发了个登录提醒……有点害怕,可能要弄一个 [bridge](https://github.com/Icalingua-plus-plus/Icalingua-plus-plus/tree/development/icalingua-bridge-oicq) 保持登录,确保 app 反复重启也没事 200 | 201 | 作业文件下载:`window.external.addDownLoadTask` 202 | 203 | ![](res/images/20221020_06.png) 204 | 205 | 由于没有提供完整 url ,因此暂时不知道如何下载…… 206 | 207 | ## 2022.10.26 208 | 209 | 发现这个作业的文件下载和群文件是一样的,url 类似 `/102/${uuid}` ,得到 uuid 和群号后可以直接用 icalingua 的 api 下载。 210 | 211 | 不过群号没有直接从 addDownLoadTask 传入,因此我们解析 url 获得群号。 212 | 213 | 另外在菜单加了前进和后退,方便在群作业的两个页面中切换。 214 | 215 | 再提几个 BrowserWindow 相关的发现 216 | 217 | [BrowserWindow | Electron](https://www.electronjs.org/docs/latest/api/browser-window) 218 | 219 | `autoHideMenuBar` 控制菜单显隐,实际上隐藏的情况下按 alt 就会出现。 220 | 221 | `webPreferences.preload` 加载外部脚本,`webPreferences.contextIsolation` 允许使用 electron api (似乎还要开了这个选项覆写 window 的对象里面的函数才能被调用,否则会被过滤掉)。 222 | 223 | Menu 似乎对所有 browserWindow 实例都是一样的,不过 click 有个回调可以获取实际点击的 browserWindow 224 | 225 | [Class: MenuItem | Electron](https://www.electronjs.org/docs/latest/api/menu-item#menuitemclick) 226 | 227 | ## 后记 228 | 229 | 上面的代码不久后我就 [PR](https://github.com/Icalingua-plus-plus/Icalingua-plus-plus/pull/359) 了,原仓库维护者爽快地[接受](https://github.com/Icalingua-plus-plus/Icalingua-plus-plus/commit/0d38ab6d171abbd02c4d5595f7d73f723121e9d3)了(同时那个恶臭数字 UA 也引来不少人吐槽);尽管我的 pr 引入了一些乱七八糟的修改(比如增加了前进后退的菜单),但维护者仍然耐心地帮我改掉了,并换了更好的实现,甚至把我没实现的「下载统计数据」「查看图片」都做出来了,在这里谢谢 Icalingua 的维护者了。 230 | 231 | # 群公告 232 | 233 | ## 确认收到 234 | 235 | 目前 icalingua 没法确认收到,但似乎只要注入 cookie 是 qun.qq.com 而非 web.qun.qq.com 就能收到了。 236 | 237 | ## 发送公告 238 | 239 | CDP 找到 Android 发送公告的页面 240 | 241 | `https://web.qun.qq.com/mannounce/edit.html?&_wv=5127#gc={}&role=2` 242 | 243 | 发送按钮放在了 QQ 内置浏览器的 UI 上,需要实现 api `mqq.ui.setTitleButtons` 244 | 245 | # TODO 246 | 247 | 1. 打开群作业后过一段时间(一天以上?)有时候无法加载;发现是添加的 session cookie 出现了重复的 key ,暂时不知道什么原因。 248 | 2. 之前发了一个「修复群公告无法确认公告」的 commit ,结果发现还是有问题(尴尬) 249 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | # home -------------------------------------------------------------------------------- /jadx-smali-debugger.md: -------------------------------------------------------------------------------- 1 | # jadx 的 smali debugger 研究 2 | 3 | 已知两个 smali debugger ,包括 [jadx](https://github.com/skylot/jadx) 和 [smalidea](https://github.com/JesusFreke/smalidea) 。 4 | 5 | 前者是知名 java 反编译器,目前仍在更新;后者是 smali 作者维护的 IDEA 插件,已经一年多未更新。 6 | 7 | 于是决定研究 jadx 。 8 | 9 | ## 构建与调试 jadx 10 | 11 | jadx 使用 gradle 构建。 12 | 13 | 使用 jadx-gui:run 命令可以运行和调试 jadx-gui 。 14 | 15 | ![](res/images/20221105_02.png) 16 | 17 | 显示调试日志:run 的 configuration 添加 `--args='--log-level debug'` 18 | 19 | ![](res/images/20221106_01.png) 20 | 21 | ## smali 22 | 23 | [Dalvik 可执行指令格式  |  Android 开源项目  |  Android Open Source Project](https://source.android.com/docs/core/runtime/instruction-formats) 24 | 25 | smali 指令的长度以字(16 bit)为单位(?) 26 | 27 | ## JDWP 协议 28 | 29 | JDWP 即 Java Debugging Wire Protocol ,用于 java 调试的协议。Android 实现了 jdwp 协议。 30 | 31 | JDWP 命令分为数个集合(command set),每个集合下包含命令(command),集合和命令使用数字 id 指明。 32 | 33 | 一个命令包 Command Packet 的组成: 34 | 35 | ``` 36 | Header 37 | length (4 bytes) 38 | id (4 bytes) 39 | flags (1 byte) 40 | command set (1 byte) 41 | command (1 byte) 42 | data (Variable) 43 | ``` 44 | 45 | 一个回应包 Reply Packet 的组成: 46 | 47 | ``` 48 | Header 49 | length (4 bytes) 50 | id (4 bytes) 51 | flags (1 byte) 52 | error code (2 bytes) 53 | data (Variable) 54 | ``` 55 | 56 | 协议相关内容的参考文档: 57 | 58 | [Java(tm) Debug Wire Protocol](https://docs.oracle.com/en/java/javase/15/docs/specs/jdwp/jdwp-protocol.html#JDWP_VirtualMachine_AllClassesWithGeneric) 59 | 60 | [Java Debug Wire Protocol](https://docs.oracle.com/en/java/javase/15/docs/specs/jdwp/jdwp-spec.html) 61 | 62 | jdwp 由 jvmti 实现: 63 | 64 | [JVM(TM) Tool Interface 15.0.0](https://docs.oracle.com/en/java/javase/15/docs/specs/jvmti.html#GetBytecodes) 65 | 66 | ART 中的 jvmti 和 jdwp 源码: 67 | 68 | ``` 69 | art/openjdkjvmti/ 70 | external/oj-libjdwp/ 71 | ``` 72 | 73 | [Android Runtime (ART)  |  Android Open Source Project](https://source.android.com/docs/core/architecture/modular-system/art?hl=en) 74 | 75 | jadx 使用[JDWP](https://github.com/skylot/jdwp)实现协议的编解码。 76 | 77 | 整个项目就一个 java 文件: 78 | 79 | [jdwp/JDWP.java at master · skylot/jdwp](https://github.com/skylot/jdwp/blob/master/src/main/java/io/github/skylot/jdwp/JDWP.java) 80 | 81 | ## 核心源码 82 | 83 | 1. Debugger 的实现 84 | 85 | jadx-gui/src/main/java/jadx/gui/device/debugger/SmaliDebugger.java 86 | 87 | 2. DebugController 88 | 89 | jadx-gui/src/main/java/jadx/gui/device/debugger/DebugController.java 90 | 91 | 3. 断点管理 92 | 93 | jadx-gui/src/main/java/jadx/gui/device/debugger/BreakpointManager.java 94 | 95 | ## 断点 96 | 97 | JDWP 中,[EventRequest](https://docs.oracle.com/en/java/javase/15/docs/specs/jdwp/jdwp-protocol.html#JDWP_EventRequest) 命令可以设置一些条件使得 jvm 挂起(suspend),即暂停 vm 供我们调试。 98 | 99 | 支持的事件类型 [EventKind](https://docs.oracle.com/en/java/javase/15/docs/specs/jdwp/jdwp-protocol.html#JDWP_EventKind) 有很多,包括: 100 | 101 | 1. 单步(SINGLE_STEP) 102 | 2. 断点(BREAKPOINT) 103 | 3. 异常(EXCEPTION) 104 | 4. 方法入口/退出(METHOD_ENTRY/EXIT) 105 | 5. field 访问(FIELD_ACCESS/MODIFICATION) 106 | 6. 类加载相关(CLASS_PREPARE/LOAD/UNLOAD) 107 | 7. 线程相关(THREAD_START/END/DEATH) 108 | 8. 其他 109 | 110 | 为了指定断点的位置,JDWP 使用 [`location`](https://docs.oracle.com/en/java/javase/15/docs/specs/jdwp/jdwp-spec.html#detailed-command-information) 类型描述。 111 | 112 | > An executable location. The location is identified by one byte type tag followed by a a classID followed by a methodID followed by an unsigned eight-byte index, which identifies the location within the method. See below for details on the location index. The type tag is necessary to identify whether location's classID identifies a class or an interface. Almost all locations are within classes, but it is possible to have executable code in the static initializer of an interface. 113 | 114 | location 包含了 classID, methodID, 以及一个代码偏移。 115 | 116 | 代码偏移指的是 smali 指令的字位置,例如下面代码的偏移: 117 | 118 | ![](res/images/20221105_01.png) 119 | 120 | 第一条指令位于位置 0 ,字长 3 ,因此第二条指令位于位置 3 ,字长 2,第三条位于位置 3+2=5 …… 121 | 122 | jvmti 只会处理「已加载的类」,可以用 EventRequest 设置类加载的监听; [VirtualMachine](https://docs.oracle.com/en/java/javase/15/docs/specs/jdwp/jdwp-protocol.html#JDWP_VirtualMachine) 命令集的 AllClasses 和 AllClassesWithGeneric 命令可以获取所有已加载的类。ClassesBySignature 可以根据类签名获取已加载的类(如果未加载则不会取得;如果多个类加载器有相同的类签名会返回多个) 123 | 124 | 类使用 referenceTypeID 表示(可以表示 class, interface, array),可以用[ReferenceType 命令集](https://docs.oracle.com/en/java/javase/15/docs/specs/jdwp/jdwp-protocol.html#JDWP_ReferenceType)进行操作。 125 | 126 | 127 | -------------------------------------------------------------------------------- /keep-accessibility-service-alive.md: -------------------------------------------------------------------------------- 1 | # 无障碍服务保活探究 2 | 3 | ## 启动无障碍 4 | 5 | 无障碍服务的启动 API 并不在 IAccessibilityManagerService ,而是在安全设置(`content://settings/secure`)里面! 6 | 7 | 以下安全设置的值即为启用的无障碍服务列表,是 `:` 分隔的平坦化组件名 (`packageName/componentName`)。 8 | 9 | ``` 10 | enabled_accessibility_services 11 | ``` 12 | 13 | 读写安全设置需要 `WRITE_SECURE_SETTINGS` 权限,这个权限是 `development` 的,所以默认第三方 app 即使声明也不获得权限,系统设置也不会有启用的开关,但可以通过 `pm grant` 授予。 14 | 15 | > 类似的还有 `READ_LOGS` ,也被利用滥用于在 Android 10 以上版本监听剪贴板的黑魔法。 16 | 17 | 无障碍服务是否启用完全由该值决定,调用 disableSelf 禁用自身在内部也是更新这个值。 18 | 19 | > 「设置」中更新无障碍服务的相关源码在此:`frameworks/base/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java` ,竟然不在「Settings」包而是在「SettingsLib」,找起来着实费了一番功夫。 20 | 21 | ## 自动拉起 22 | 23 | 理论上来说,原生的无障碍服务在启用后是可以在进程异常结束后被自动拉起的,看起来类似于 TileService 、通知使用权或桌面。只要服务列表中存在,系统总会将其自动拉起,只有 force-stop 才会将其关闭(此时无障碍服务列表中该服务会被自动移除)。 24 | 无障碍服务由 SystemServer 通过 bindService 启动,并通过服务的重启机制自动重启(`BIND_AUTO_CREATE`),所以开发者应该不用处理崩溃后恢复的情况,系统会自动完成。 25 | 26 | 不过一些(大部分?)定制系统的默认行为阻止了应用的自启,可重启的服务显然属于「自启」的范围,因此需要手动设置允许自启,此外省电策略也是影响服务自启的关键问题(因为被省电策略停止的应用相当于被 force-stop ,服务会被自动移除)。 27 | 28 | > 尽管无障碍服务自动拉起阻碍重重,但通知使用权和 TileService 的拉起就往往不受限制,看起来「无障碍」服务的「障碍」更大一些……不过自启机制更多被利用滥用于满足某些应用的非正常保活需求,所以限制也并非是坏事,感觉 Android 原生更应该加入相关的权限控制。 29 | > 其实比起限制自启,更为恐怖的还是某些应用在系统具有的「免杀白名单」……此处就不点名了。 30 | 31 | 此前研究的时候,一直以为进程崩溃后服务不从列表中移除是 bug (实际上一直以为进程死亡就导致无障碍服务被移除),还尝试了反复将自己的服务从无障碍服务列表移除再添加来「解决问题」,最后却导致了更严重的问题,比如发现服务被反复 bind ,或者虽然 bind 了(也就是 onServiceConnected 了)却无法添加无障碍窗口(导致崩溃)。现在看来其实都是自启被限制了的锅。 32 | 33 | > 不过能出现这种 bug ,就算我有 99% 的问题,难道你 AccessibilityManagerService 就没有 1% 的责任?←_← 34 | 35 | ## 附录:Service 的 Restart 机制 36 | 37 | Service 的运行有两种:启动(startService) 和绑定(bindService),它们是被分别对待的。也就是说,服务可以同时处于被启动和被绑定两种状态,且互不影响(?)。而服务的 Restart (重启)机制自然也是分开处理的。 38 | 39 | 被绑定的服务自启与 bindService 时的 flag `BIND_AUTO_CREATE` 有关。这样的服务就会被系统自动拉起,直到显式调用 unbind 为止。在 `dumpsys activity services` 中,如果服务的某个绑定者具有这个 flag ,会显示一个 `CREATE` 。这个重启发生在服务所在进程异常退出的时候(也就是非系统主动杀死,具体来说就是 `IApplicationThread` binderDead 的时候)。当然,系统主动重启服务会等待一定时间,次数也是有限制的,且等待时间会随着次数的增加而增加。 40 | 41 | > 实际上在 Manifest 声明的 persistence (持久)服务可以无限制地被系统地主动重启,当然这个就是属于系统 App 的特权了。 42 | 43 | ``` 44 | * ServiceRecord{64da221 u0 com.fooview.android.fooview/.fvprocess.FooAccessibilityService} 45 | intent={cmp=com.fooview.android.fooview/.fvprocess.FooAccessibilityService} 46 | packageName=com.fooview.android.fooview 47 | processName=com.fooview.android.fooview:fv 48 | permission=android.permission.BIND_ACCESSIBILITY_SERVICE 49 | baseDir=/data/app/com.fooview.android.fooview-UobdMJWrMIOstRT8Xauzqw==/base.apk 50 | dataDir=/data/user/0/com.fooview.android.fooview 51 | app=ProcessRecord{c1586f 5534:com.fooview.android.fooview:fv/u0a211} 52 | hasBindingWhitelistingBgActivityStarts=true 53 | allowWhileInUsePermissionInFgs=true 54 | startForegroundCount=0 55 | recentCallingPackage=android 56 | createTime=-14h26m19s327ms startingBgTimeout=-- 57 | lastActivity=-3h32m4s228ms restartTime=-3h32m4s228ms createdFromFg=true 58 | callerPackage=android 59 | Bindings: 60 | * IntentBindRecord{cd2556f CREATE}: 61 | intent={cmp=com.fooview.android.fooview/.fvprocess.FooAccessibilityService} 62 | binder=android.os.BinderProxy@e70627c 63 | requested=true received=true hasBound=true doRebind=false 64 | * Client AppBindRecord{bc3e705 ProcessRecord{f8c2255 1489:system/1000}} 65 | Per-process Connections: 66 | ConnectionRecord{f246f4e u0 CR FGSA CAPS com.fooview.android.fooview/.fvprocess.FooAccessibilityService:@b622a49} 67 | All Connections: 68 | ConnectionRecord{f246f4e u0 CR FGSA CAPS com.fooview.android.fooview/.fvprocess.FooAccessibilityService:@b622a49} 69 | ``` 70 | 71 | 无障碍服务绑定: 72 | 73 | ``` 74 | frameworks/base/services/accessibility/java/com/android/server/accessibility/AccessibilityServiceConnection.java 75 | bindLocked 76 | ``` 77 | 78 | 服务重启: 79 | 80 | ``` 81 | frameworks/base/services/core/java/com/android/server/am/ActiveServices.java 82 | killServicesLocked 83 | -> scheduleServiceRestartLocked 84 | ``` 85 | 86 | 重启日志: 87 | 88 | ``` 89 | 06-28 15:06:17.132 1489 2429 W ActivityManager: Scheduling restart of crashed service five.ec1cff.assistant/.AssistantService in 1000ms for connection 90 | ``` 91 | 92 | 有关 启动服务 的重启可参考 → [Android Service重启恢复(Service进程重启)原理解析 - 简书](https://www.jianshu.com/p/7a659ca38510) 93 | 94 | > 简单来说就是 onStartCommand 的返回值决定了是否重启,返回 `START_STICKY` 就可以自动重启。 -------------------------------------------------------------------------------- /ksu-new-su.md: -------------------------------------------------------------------------------- 1 | # 增强 KernelSU 的 su 2 | 3 | ## 新想法 4 | 5 | 众所周知,KSU 的 su 实际上是重定向到系统的 sh ,因此缺少了一些基本功能,比如切换 uid 、切换全局挂载命名空间等。 6 | 7 | 这样导致某些以前重度依赖 MagiskSU 的程序到了 ksu 上工作不正常。 8 | 9 | 因此我想了一个新的实现: 10 | 11 | 1. 仿照 MagiskSU 写一个 su (ksu 的 su 也只有基本功能),放在 `/data/adb/ksu/bin/su` ,execveat 重定向 `/system/bin/su` 到 `/data/adb/ksu/bin/su` 即可。由于调用 /system/bin/su 后 ksu 已经给予权限,因此可以访问这个文件。 12 | 2. ksud install 的时候将安装 su 。 13 | 14 | https://github.com/5ec1cff/KernelSU 15 | 16 | ## 更进一步 17 | 18 | 看了一下 ksu 的 PR ,发现曾经有个解压 ksud 到 /dev 中的临时目录的提案。 19 | 20 | [kernel: extract ksud to /dev by Ylarod · Pull Request #130 · tiann/KernelSU](https://github.com/tiann/KernelSU/pull/130) 21 | 22 | PR 在内核中创建 /dev 下的临时目录,并解压内核中 embed 的 ksud 。看起来可能是为了解决低版本 /data/adb/ksud 由于 suid 问题无法被 init 执行的问题。 23 | 24 | ``` 25 | normally ksud will be started by the init process (initrc is injected to it by ksu from kernel) on boot and it handles module updates, mountings and post-fs-data or service scripts execution 26 | 27 | ksud requires some privileges to run ofc, so it must change its selinux domain from init to su(ksu domain) 28 | 29 | but ksud is located under /data which has the nosuid mount flag, domain transitions are restricted by selinux in such environment (only bounded transitions are allowed but it does not meet our needs), this results in the ksud not starting at all on boot and therefore the modules cannot work 30 | 31 | bcs we didn't find a proper way to fix unbounded domain transitions under nnp/nosuid environment on kernels earlier than 4.14, u should patch ksu or kernel for now to fix the module support 32 | ``` 33 | 34 | > https://t.me/KernelSU_group/3249/40767 35 | 36 | 这个 PR 虽然暂时没有被合并,但还是给了我一些启发。 37 | 38 | 在我的方法中,su 的 execve 被重定向到 /data/adb ,但是 stat 和 faccessat 仍然重定向到 sh ,因为如果不这样,没有权限的进程无法访问 /data/adb 。 39 | 40 | 如果把 su 放在临时目录里面,execve, faccessat, stat 都重定向到这里,可以确保在 su 不存在的情况下不会被错误执行。 41 | 42 | 因此新的想法更改如下: 43 | 44 | 1. ksud install 的时候释放 su 45 | 2. ksud 在系统启动的某个阶段把 su 放到临时目录,权限设为 system_file 46 | 3. 临时目录的名字由内核产生,可以由内核或者 ksud 创建,但是用户空间应该有一个 API (如 prctl)拿到临时目录。 47 | 4. 由于临时目录中的 su 已经可执行,因此不需要 sucompat 在 execve 的时候主动提权了。 48 | 49 | 当前进度: 50 | 51 | 1. ~~new su~~ 52 | 2. ~~ksud embed su~~ 53 | 3. ~~内核重定向 su execve~~ 54 | 4. ~~实现临时目录~~ 55 | 56 | ![](res/images/20230317_01.png) 57 | 58 | ![](res/images/20230319_01.png) 59 | 60 | ![](res/images/20230319_02.png) 61 | -------------------------------------------------------------------------------- /learn-seccomp-bpf.md: -------------------------------------------------------------------------------- 1 | # bpf 和 seccomp 2 | 3 | 最近看了[空大师的博客](https://nullptr.icu/),发现了一篇[讲解 seccomp 拦截系统调用的文章](https://nullptr.icu/index.php/archives/62/) 4 | 5 | [[原创]分享一个Android通用svc跟踪以及hook方案——Frida-Seccomp-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com](https://bbs.pediy.com/thread-271815.htm#msg_header_h1_2) 6 | 7 | [seccomp沙箱机制 & 2019ByteCTF VIP · pollux's Dairy](http://pollux.cc/2019/09/22/seccomp%E6%B2%99%E7%AE%B1%E6%9C%BA%E5%88%B6%20&%202019ByteCTF%20VIP/#0x00-seccomp%E6%B2%99%E7%AE%B1%E6%9C%BA%E5%88%B6) 8 | 9 | ## 什么是 bpf 10 | 11 | [bpf(4)](https://www.freebsd.org/cgi/man.cgi?query=bpf&sektion=4&manpath=FreeBSD+4.7-RELEASE) 12 | 13 | 过滤器程序是一个指令数组 14 | 15 | ``` 16 | A filter program is an array of instructions, with all branches forwardly 17 | directed, terminated by a return instruction. Each instruction performs 18 | some action on the pseudo-machine state, which consists of an accumula- 19 | tor, index register, scratch memory store, and implicit program counter. 20 | 21 | The following structure defines the instruction format: 22 | ``` 23 | 24 | ## 指令格式 25 | 26 | 一个 bpf 指令的结构体: 27 | 28 | ``` 29 | struct bpf_insn { 30 | u_short code; 31 | u_char jt; 32 | u_char jf; 33 | u_long k; 34 | }; 35 | ``` 36 | 37 | BPF 指令的格式:操作码(code)+参数(ulong k),jt 和 jf 为判断值为真/假后跳转的指令相对当前指令的位置(偏移) 38 | 39 | ``` 40 | The k field is used in different ways by different instructions, and the 41 | jt and jf fields are used as offsets by the branch instructions. The op- 42 | codes are encoded in a semi-hierarchical fashion. There are eight 43 | classes of instructions: BPF_LD, BPF_LDX, BPF_ST, BPF_STX, BPF_ALU, 44 | BPF_JMP, BPF_RET, and BPF_MISC. Various other mode and operator bits are 45 | or'd into the class to give the actual instructions. The classes and 46 | modes are defined in . 47 | 48 | Below are the semantics for each defined bpf instruction. We use the 49 | convention that A is the accumulator, X is the index register, P[] packet 50 | data, and M[] scratch memory store. P[i:n] gives the data at byte offset 51 | "i" in the packet, interpreted as a word (n=4), unsigned halfword (n=2), 52 | or unsigned byte (n=1). M[i] gives the i'th word in the scratch memory 53 | store, which is only addressed in word units. The memory store is in- 54 | dexed from 0 to BPF_MEMWORDS - 1. k, jt, and jf are the corresponding 55 | fields in the instruction definition. "len" refers to the length of the 56 | packet. 57 | ``` 58 | 59 | ### 寄存器 60 | 61 | A: 累加器 (4字节) 62 | X: 索引寄存器(index) (4字节) 63 | P[]: 包,P[i:n] P 从 i 位置开始的 n 个字节(1, 2, 4) 64 | M[]: 暂存内存(?) 以 word (4字节)单位访问 M[i] 第 i 字节 65 | 暂存内存(scratch memory): [Scratchpad memory - Wikipedia](https://en.wikipedia.org/wiki/Scratchpad_memory) 66 | 67 | pc: 程序寄存器(不可访问) 68 | 69 | ## 指令一览 70 | 71 | ### LD: 修改累加器 A 72 | W, H, B: 4, 2, 1 (字,半字,字节) 73 | ABS: 取绝对位置 (k) 74 | IND: 取相对 X 寄存器的位置 (X+k) 75 | LEN: P 的长度 76 | IMM: 直接赋值 k 77 | MEM: 取 M[k] 78 | 79 | ``` 80 | BPF_LD These instructions copy a value into the accumulator. The type 81 | of the source operand is specified by an "addressing mode" and 82 | can be a constant (BPF_IMM), packet data at a fixed offset 83 | (BPF_ABS), packet data at a variable offset (BPF_IND), the 84 | packet length (BPF_LEN), or a word in the scratch memory store 85 | (BPF_MEM). For BPF_IND and BPF_ABS, the data size must be 86 | specified as a word (BPF_W), halfword (BPF_H), or byte (BPF_B). 87 | The semantics of all the recognized BPF_LD instructions follow. 88 | 89 | BPF_LD+BPF_W+BPF_ABS A <- P[k:4] 90 | BPF_LD+BPF_H+BPF_ABS A <- P[k:2] 91 | BPF_LD+BPF_B+BPF_ABS A <- P[k:1] 92 | BPF_LD+BPF_W+BPF_IND A <- P[X+k:4] 93 | BPF_LD+BPF_H+BPF_IND A <- P[X+k:2] 94 | BPF_LD+BPF_B+BPF_IND A <- P[X+k:1] 95 | BPF_LD+BPF_W+BPF_LEN A <- len 96 | BPF_LD+BPF_IMM A <- k 97 | BPF_LD+BPF_MEM A <- M[k] 98 | ``` 99 | 100 | ### LDX: 修改索引寄存器 X 101 | 只有 W (4字节访问) 102 | IMM: 直接赋值 103 | MEM: M[k] 104 | LEN: P 的长度 105 | MSH: 4*(P[k:1] & 0xf) (用于获取 ip 头长度) 106 | 107 | ``` 108 | BPF_LDX These instructions load a value into the index register. Note 109 | that the addressing modes are more restrictive than those of 110 | the accumulator loads, but they include BPF_MSH, a hack for ef- 111 | ficiently loading the IP header length. 112 | 113 | BPF_LDX+BPF_W+BPF_IMM X <- k 114 | BPF_LDX+BPF_W+BPF_MEM X <- M[k] 115 | BPF_LDX+BPF_W+BPF_LEN X <- len 116 | BPF_LDX+BPF_B+BPF_MSH X <- 4*(P[k:1]&0xf) 117 | ``` 118 | 119 | ### ST: 把累加器 A 存储到暂存内存的第 k 字节 120 | 121 | ``` 122 | BPF_ST This instruction stores the accumulator into the scratch mem- 123 | ory. We do not need an addressing mode since there is only one 124 | possibility for the destination. 125 | 126 | BPF_ST M[k] <- A 127 | ``` 128 | 129 | ### STX: 把索引寄存器 X 存储到暂存内存的第 k 字节 130 | 131 | ``` 132 | BPF_STX This instruction stores the index register in the scratch mem- 133 | ory store. 134 | 135 | BPF_STX M[k] <- X 136 | ``` 137 | 138 | ### ALU: 运算单元 139 | 140 | 进行一系列二元运算,结果保存到 A 累加器,且 A 是运算的第一个元。 141 | 142 | 运算的第二个元: 143 | 144 | K: 使用操作数 145 | X: 使用 X 寄存器 146 | 147 | 支持 ADD, SUB, MUL, DIV, AND, OR, LSH, RSH ,即加减乘除且或左移右移 148 | 149 | 此外还有取反(单元运算) 150 | 151 | ``` 152 | BPF_ALU The alu instructions perform operations between the accumulator 153 | and index register or constant, and store the result back in 154 | the accumulator. For binary operations, a source mode is re- 155 | quired (BPF_K or BPF_X). 156 | 157 | BPF_ALU+BPF_ADD+BPF_K A <- A + k 158 | BPF_ALU+BPF_SUB+BPF_K A <- A - k 159 | BPF_ALU+BPF_MUL+BPF_K A <- A * k 160 | BPF_ALU+BPF_DIV+BPF_K A <- A / k 161 | BPF_ALU+BPF_AND+BPF_K A <- A & k 162 | BPF_ALU+BPF_OR+BPF_K A <- A | k 163 | BPF_ALU+BPF_LSH+BPF_K A <- A << k 164 | BPF_ALU+BPF_RSH+BPF_K A <- A >> k 165 | BPF_ALU+BPF_ADD+BPF_X A <- A + X 166 | BPF_ALU+BPF_SUB+BPF_X A <- A - X 167 | BPF_ALU+BPF_MUL+BPF_X A <- A * X 168 | BPF_ALU+BPF_DIV+BPF_X A <- A / X 169 | BPF_ALU+BPF_AND+BPF_X A <- A & X 170 | BPF_ALU+BPF_OR+BPF_X A <- A | X 171 | BPF_ALU+BPF_LSH+BPF_X A <- A << X 172 | BPF_ALU+BPF_RSH+BPF_X A <- A >> X 173 | BPF_ALU+BPF_NEG A <- -A 174 | ``` 175 | 176 | ### JMP:跳转(操作 pc 寄存器) 177 | 178 | JA: pc 直接加操作数 k (允许跳转 32 位长) 179 | 180 | GT, GE, EQ, SET :(无符号比较)大于,大于等于,等于,且;结果为 true (非零)就 + jt ,否则 + jf (只能跳转 8 位长) 181 | 182 | ``` 183 | BPF_JMP The jump instructions alter flow of control. Conditional jumps 184 | compare the accumulator against a constant (BPF_K) or the index 185 | register (BPF_X). If the result is true (or non-zero), the 186 | true branch is taken, otherwise the false branch is taken. 187 | Jump offsets are encoded in 8 bits so the longest jump is 256 188 | instructions. However, the jump always (BPF_JA) opcode uses 189 | the 32 bit k field as the offset, allowing arbitrarily distant 190 | destinations. All conditionals use unsigned comparison conven- 191 | tions. 192 | 193 | BPF_JMP+BPF_JA pc += k 194 | BPF_JMP+BPF_JGT+BPF_K pc += (A > k) ? jt : jf 195 | BPF_JMP+BPF_JGE+BPF_K pc += (A >= k) ? jt : jf 196 | BPF_JMP+BPF_JEQ+BPF_K pc += (A == k) ? jt : jf 197 | BPF_JMP+BPF_JSET+BPF_K pc += (A & k) ? jt : jf 198 | BPF_JMP+BPF_JGT+BPF_X pc += (A > X) ? jt : jf 199 | BPF_JMP+BPF_JGE+BPF_X pc += (A >= X) ? jt : jf 200 | BPF_JMP+BPF_JEQ+BPF_X pc += (A == X) ? jt : jf 201 | BPF_JMP+BPF_JSET+BPF_X pc += (A & X) ? jt : jf 202 | ``` 203 | 204 | ### RET: 终止程序,返回寄存器 A 或操作数 k 205 | 206 | ``` 207 | BPF_RET The return instructions terminate the filter program and spec- 208 | ify the amount of packet to accept (i.e., they return the trun- 209 | cation amount). A return value of zero indicates that the 210 | packet should be ignored. The return value is either a con- 211 | stant (BPF_K) or the accumulator (BPF_A). 212 | 213 | BPF_RET+BPF_A accept A bytes 214 | BPF_RET+BPF_K accept k bytes 215 | ``` 216 | 217 | ### MISC: 杂项 218 | 219 | 目前可以把一个寄存器的值复制到另一个(A 到 X 或 X 到 A) 220 | 221 | BPF_MISC The miscellaneous category was created for anything that 222 | doesn't fit into the above classes, and for any new instruc- 223 | tions that might need to be added. Currently, these are the 224 | register transfer instructions that copy the index register to 225 | the accumulator or vice versa. 226 | 227 | BPF_MISC+BPF_TAX X <- A 228 | BPF_MISC+BPF_TXA A <- X 229 | 230 | ## 初始化指令的宏 231 | 232 | ``` 233 | The bpf interface provides the following macros to facilitate array ini- 234 | tializers: BPF_STMT(opcode, operand) and BPF_JUMP(opcode, operand, 235 | true_offset, false_offset). 236 | ``` 237 | 238 | # 理解空大师的 bpf 239 | 240 | 先看看 32 位部分: 241 | 242 | ```cpp 243 | // 跳板的地址 244 | auto trampoline = (uintptr_t) Trampoline; 245 | struct sock_filter filter[] = { 246 | // 读取 packet ——此处是 seccomp_data 结构体——的 nr 字段,也就是系统调用号 247 | BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)), 248 | #if defined(__i386__) || defined(__arm__) 249 | // 如果是,则继续前进,否则跳过两个指令 250 | BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, nr, 0, 2), 251 | // 读 instruction_pointer ,也就是产生系统调用的指令地址 252 | BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, instruction_pointer)), 253 | // 如果是跳板代码,则放行,否则 TRAP 进入信号处理器 254 | BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, trampoline, 0, 1), 255 | // 允许执行(从跳板执行的 syscall 会走到这里) 256 | BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW), 257 | // trap (从非跳板处执行的 syscall 会走到这里) 258 | BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_TRAP) 259 | }; 260 | ``` 261 | 262 | # Android 系统的 seccomp 263 | 264 | Zygote 在 SpecializeCommon 中给新进程设置了一系列 seccomp filter 。 265 | 266 | ```cpp 267 | // frameworks/base/core/jni/com_android_internal_os_Zygote.cpp 268 | static void SetUpSeccompFilter(uid_t uid, bool is_child_zygote) { 269 | if (!gIsSecurityEnforced) { 270 | ALOGI("seccomp disabled by setenforce 0"); 271 | return; 272 | } 273 | 274 | // Apply system or app filter based on uid. 275 | if (uid >= AID_APP_START) { 276 | if (is_child_zygote) { 277 | set_app_zygote_seccomp_filter(); 278 | } else { 279 | set_app_seccomp_filter(); 280 | } 281 | } else { 282 | set_system_seccomp_filter(); 283 | } 284 | } 285 | ``` 286 | 287 | seccomp filter 通过 `bionic/libc/tools/genseccomp.py` 生成。 288 | 289 | bionic 对 syscall 设置了一套白名单和一套黑名单,在 `bionic/libc/SECCOMP_BLOCKLIST_COMMON.TXT` 的注释写了: 290 | 291 | ``` 292 | # This file is used to populate seccomp's allowlist policy in combination with SYSCALLS.TXT. 293 | # Note that the resultant policy is applied only to zygote spawned processes. 294 | # 295 | # The final seccomp allowlist is SYSCALLS.TXT - SECCOMP_BLOCKLIST.TXT + SECCOMP_ALLOWLIST.TXT 296 | # Any entry in the blocklist must be in the syscalls file and not be in the allowlist file 297 | # 298 | # This file is processed by a python script named genseccomp.py. 299 | ``` 300 | -------------------------------------------------------------------------------- /learn-systrace.md: -------------------------------------------------------------------------------- 1 | # Android 系统跟踪实战 2 | 3 | 最近特别关心 app 的性能问题——并非自己开发的(还没到这个地步),而是手机上那些臃肿的国产 app 。虽然上一篇文章的结尾表明「一切分析皆是徒劳」,仅凭个人的力量是无法阻止这些屎山越长越大、越跑越慢的。不过我还是好奇它们究竟为何这么慢,到底慢在哪里,哪怕无法解决,也算是增长了自己的认识。 4 | 5 | ## 如何跟踪 6 | 7 | 对于我们要跟踪的目标,我们并没有它们的源码,不知道它们的行为,并且也「不可调试」它们。不过它们终究运行在 Android framework 上,要分析到底哪里慢,可以先系统关键方法插桩分析,如 `ActivityThread` 等。通过 hook 各个组件的生命周期分发方法,标记它们的起始和结束时间,通过每个生命周期的方法耗时来粗略分析。并且我们有 Xposed 等注入工具,可以直接注入到进程去分析。 8 | 9 | 思路看上去很好,但是想要凭空造一个这样的轮子却是困难的——首先要自己一个个找到生命周期的分发的方法,逐个 hook ,然后找到一种方法收集数据,最后还要将数据可视化,方便我们的分析……总之,我那微弱的产能是造不出这个轮子的。 10 | 11 | 不过我们不必自己造轮子,Android 发展那么多年怎么能没有性能分析工具呢?来看看 weishu 大神曾经写过的文章: 12 | 13 | [手把手教你使用Systrace(一) - 知乎](https://zhuanlan.zhihu.com/p/27331842) 14 | 15 | ## systrace? 16 | 17 | 我们了解到 Android 系统有 `systrace` 这个工具进行性能分析,并且 framework 提供了 `android.os.Trace` 用于在程序的执行流程中给我们感兴趣的过程做标记 (label) 。 `Trace.beginSection` 标记开始, `Trace.endSection` 标记结束。实际上,Android framework 自身就大量使用 Trace ,在 ActivityThread 等类的关键方法都有跟踪标记。 18 | 19 | 不过这玩意的使用前提是进程是 debuggable 的——没关系,用 xposed 强开就好了。在 ~~LSP 应用市场~~ 模块仓库中就有一个为 app 强制开启 debug 的模块:XAppDebug 20 | 21 | [Palatis/XAppDebug: toggle app debuggable](https://github.com/Palatis/XAppDebug) 22 | 23 | 粗略看了一下源码,原理就是注入 ss , hook `PMS` 的一些方法,使得 `ApplicationInfo` 对特定 app 返回 `debuggable=true` 的字段,这允许 app 进程的 ActivityThread 在 bindApplication 的时候初始化各种 debug 功能(其中就包括 Trace);还有 `Process.start` 中也注入了允许调试的 flags ,以便 app 进程能够连接到 adbd ,让 ddmHandle 生效。 24 | 25 | 不过这玩意把进程的 niceName 当作 packageName ,着实让我汗颜(参数明明就有 packageName——虽然不是 zygote 必需的参数)。并且使用文件系统结构存储配置(也就是文件名起名为目标包名,然后以文件是否存在作为开关……),还通过 SELinuxHelper 在 system server 访问 app 的配置,总感觉实现得特别不优雅……起码用个 XSP 罢! 26 | 27 | 总之,按照自己的需求修改了一下模块后,安装启用模块,重启手机,果然能够选择 app 开启调试了,AS 的 log 出现了原本 undebuggable app 的选项! 28 | 29 | ![](res/images/20220907_01.png) 30 | 31 | 既然这样,那 systrace 就可以派上用场了吧?然而…… 32 | 33 | ![](res/images/20220907_02.png) 34 | 35 | 没想到这个远古老物居然需要 python 2.7 …… 36 | 37 | 在 Windows 上配置 Python 3 和 2.7 共存似乎很麻烦,我不想冒这个险。 38 | 39 | > 此时我又想骂自己:怎么主力操作系统还不换 Linux ?非要坚守破烂 Windows 吗?就因为 OEM 预装花了几个破钱?这些钱能给你带来好的体验吗? 40 | 41 | 且慢!systrace 看上去也早就不被 Google 所重视了。查看官方文档,发现官方目前推崇一个更强的 trace 分析工具:Perfetto 42 | 43 | ## 初步探索 Perfetto 44 | 45 | [系统跟踪概览  |  Android 开发者  |  Android Developers](https://developer.android.com/topic/performance/tracing?hl=zh-cn) 46 | 47 | [Perfetto](https://ui.perfetto.dev/#!/)是一个 web 工具,浏览器直接访问即可(需要挂梯)。使用方法也很简单,把采集到的数据在这个页面打开即可。 48 | 49 | > 再次感受到浏览器的强大,这玩意竟然能在浏览器上面跑。(难怪 google 都不开发桌面产品了,什么都放浏览器上) 50 | 51 | ![](res/images/20220907_03.png) 52 | 53 | 并且,Android 9 开始内置了「系统跟踪」App ,提供了图形化界面的配置和方便的跟踪开关(可以显示为磁贴)。需要跟踪,直接点击磁贴,会产生一条通知;跟踪结束,点击通知即可分享跟踪结果。 54 | 55 | 尝试着跟踪了一下 bilibili 的启动——好家伙,跟踪文件大小高达 60M ! 56 | 57 | 打开一看,似乎记录了系统中的不少进程,当然只有 debuggable 的才会显示很密集的跟踪记录。 58 | 59 | 简单分析一下,发现启动这整个 app 花了 7s , bindApplication 竟然长达 4s 。其中打开 apk 的过程大概 1.5s ,还有 3s 不知道在干什么。 60 | 61 | ![](res/images/20220907_04.png) 62 | 63 | > 这里我打开了漫游模块,所以结果可能不是很准确,因为仔细观察,发现有一些过程涉及到漫游的加载。不过曾经试过关掉模块启动 bilibili ,还是一样的慢。 64 | 65 | 随便开了一个 debuggable 的 app 对比,启动过程不足 1s ,由此可见国产屑 app 到底有多臃肿。 66 | 67 | 继续研究了一下,发现捕获的记录似乎不怎么全面,比如 Provider 等的初始化似乎没有跟踪(有待考察源码,也许是持续时间太短了没看见?)。bilibili 的启动会拉起几个进程,不过大部分都是主进程启动完成后才启动的,也看不出其中的关系。此外,这个工具偶尔会卡 bug ,导入文件而半天显示不出可视化结果,需要刷新页面。 68 | 69 | ## 尾声 70 | 71 | 这次算是体验了一把性能分析,虽然收获甚微,并未揭开「为何国产 app 启动如此之慢」的谜底,不过我想总有一天会研究明白,并且使用这些工具的经验对将来的开发也是有帮助的。 72 | -------------------------------------------------------------------------------- /linker-log.md: -------------------------------------------------------------------------------- 1 | # 从日志学习 Android Linker 2 | 3 | 我最近在研究 Linker ,在源码中不断跳转,感觉毫无头绪,而附加调试器过于困难我不会,实际上也对整体的理解没有太大的帮助。头昏脑胀中灵光一闪:要是有它的日志就方便多了!没错,要理解一个程序的运作机制,日志是最好的导师,那么,Linker 有没有调试日志呢? 4 | 5 | 然而正常情况下 logcat 并不包含 linker 的详细日志,只是偶尔有错误日志出现,这样对理解 linker 的工作没有太大帮助。 6 | 7 | 于是乎求助万能的 google ,搜到[一篇文章](https://blog.csdn.net/chiefhsing/article/details/116757358)介绍了 linker 日志的两个开关:系统属性和环境变量。 8 | 9 | ## `debug.ld.` 属性 10 | 11 | 有两个属性,分别是对全局生效的 `debug.ld.all` 和对指定包名生效的 `debug.ld.app.${packageName}`。 12 | 13 | 这个属性指定了 log 的 flags ,开启对应日志类型的输出,取值可以为 `dlopen`, `dlsym` 或者 `dlerror` ;可以取多个值,用逗号 `,` 分隔。(dlclose 包含在 dlopen 中) 14 | 15 | 如全局输出 dlopen 和 dlerror 的日志,可以 `setprop debug.ld.all dlopen,dlerror` 16 | 17 | ![](res/images/20220921_01.png) 18 | 19 | 观察日志,发现确实出现了大量以 `linker` 为 tag 的日志,主要是与 lib 的加载有关,被加载的 lib 本身和它的依赖项都很清楚地显示在日志中。 20 | 21 | 搜索源码,发现相关逻辑位于 `bionic/linker/linker_logger.cpp` : 22 | 23 | ```cpp 24 | void LinkerLogger::ResetState() { 25 | // The most likely scenario app is not debuggable and 26 | // is running on a user build, in which case logging is disabled. 27 | if (prctl(PR_GET_DUMPABLE, 0, 0, 0, 0) == 0) { 28 | return; 29 | } 30 | 31 | flags_ = 0; 32 | 33 | // For logging, check the flag applied to all processes first. 34 | static CachedProperty debug_ld_all("debug.ld.all"); 35 | flags_ |= ParseProperty(debug_ld_all.Get()); 36 | 37 | // Safeguard against a NULL g_argv. Ignore processes started without argv (http://b/33276926). 38 | if (g_argv == nullptr || g_argv[0] == nullptr) { 39 | return; 40 | } 41 | 42 | // Otherwise check the app-specific property too. 43 | // We can't easily cache the property here because argv[0] changes. 44 | char debug_ld_app[PROP_VALUE_MAX] = {}; 45 | GetAppSpecificProperty(debug_ld_app); 46 | flags_ |= ParseProperty(debug_ld_app); 47 | } 48 | ``` 49 | 50 | 看起来能否显示日志还取决于是否 dumpable ,而 user 构建的 release app 的进程都是 undumpable 的,因此不能希望这个属性对不可调试的 app 有效。 51 | 52 | 此外,所谓的包名其实是进程名前缀,获取进程名在第一个 `:` 之前的内容作为前缀,取得相应的 `debug.ld.app.$prefix` ,设置 log flags 。 53 | 54 | ```cpp 55 | static void GetAppSpecificProperty(char* buffer) { 56 | // Get process basename. 57 | const char* process_name_start = basename(g_argv[0]); 58 | 59 | // Remove ':' and everything after it. This is the naming convention for 60 | // services: https://developer.android.com/guide/components/services.html 61 | const char* process_name_end = strchr(process_name_start, ':'); 62 | 63 | std::string process_name = (process_name_end != nullptr) ? 64 | std::string(process_name_start, (process_name_end - process_name_start)) : 65 | std::string(process_name_start); 66 | 67 | std::string property_name = std::string("debug.ld.app.") + process_name; 68 | __system_property_get(property_name.c_str(), buffer); 69 | } 70 | ``` 71 | 72 | ![](res/images/20220921_02.png) 73 | 74 | `LinkerLogger::ResetState` 除了初始化之外,似乎会在每次 dlsym 或 dlopen 的时候调用 75 | 76 | ## `LD_DEBUG` 环境变量 77 | 78 | 这个环境变量在 linker_main 处理(`bionic/linker/linker_main.cpp`): 79 | 80 | ```cpp 81 | static ElfW(Addr) linker_main(KernelArgumentBlock& args, const char* exe_to_load) { 82 | // ... 83 | // Get a few environment variables. 84 | const char* LD_DEBUG = getenv("LD_DEBUG"); 85 | if (LD_DEBUG != nullptr) { 86 | g_ld_debug_verbosity = atoi(LD_DEBUG); 87 | } 88 | // ... 89 | } 90 | ``` 91 | 92 | 在 `bionic/linker/linker_debug.h` 定义了这个环境变量的取值: 93 | 94 | ```cpp 95 | #define LINKER_VERBOSITY_PRINT (-1) 96 | #define LINKER_VERBOSITY_INFO 0 97 | #define LINKER_VERBOSITY_TRACE 1 98 | #define LINKER_VERBOSITY_DEBUG 2 99 | 100 | __LIBC_HIDDEN__ extern int g_ld_debug_verbosity; 101 | 102 | #define _PRINTVF(v, x...) \ 103 | do { \ 104 | if (g_ld_debug_verbosity > (v)) linker_log((v), x); \ 105 | } while (0) 106 | ``` 107 | 108 | 可见,只要 `LD_DEBUG` 的数大于 `LINKER_VERBOSITY_` 的值,那么这个等级的日志就会输出。而设置为 `3` 就是所有等级都可以输出(**这样会导致程序运行极度缓慢**)。 109 | 110 | 看上去这个环境变量所指示的日志范围或许比上面那个属性更广,不过修改新进程的 env 并不容易,起码没有 root 是做不到的。有 root 的情况下,我们可以修改 init.environ.rc 来注入 env,当然也可以用 zygisk 那样劫持启动的方式来注入 env。 111 | 112 | 当然,如果只是简单地了解一下 linker 的启动流程,不涉及特定进程的分析的话,直接开一个 shell 为新进程设置环境变量即可。 113 | 114 | 下面我们看一看 ls 的 linker INFO 等级的日志: 115 | 116 | ```log 117 | emulator64_x86_64:/data/local/tmp # LD_DEBUG=1 ls & 118 | [1] 10403 119 | emulator64_x86_64:/data/local/tmp # Shamiko-v0.5.2-120-release.zip app-debug.apk busybox magisk32 magiskboot magiskpolicy util_functions.sh 120 | ap avd_magisk.sh magisk.apk magisk64 magiskinit stack.sh zyg 121 | logcat --pid 10403 122 | --------- beginning of main 123 | 09-21 19:24:10.792 10403 10403 W linker : [ Android dynamic linker (64-bit) ] 124 | 09-21 19:24:10.793 10403 10403 W linker : [ Linking executable "/system/bin/toybox" ] 125 | 09-21 19:24:10.793 10403 10403 W linker : [ Linking "[vdso]" ] 126 | 09-21 19:24:10.793 10403 10403 W linker : [ Reading linker config "/linkerconfig/ld.config.txt" ] 127 | 09-21 19:24:10.793 10403 10403 W linker : [ Using config section "system" ] 128 | 09-21 19:24:10.794 10403 10403 W linker : [ Linking "/system/bin/toybox" ] 129 | 09-21 19:24:10.794 10403 10403 W linker : [ Linking "/system/lib64/libcrypto.so" ] 130 | 09-21 19:24:10.794 10403 10403 W linker : [ Linking "/system/lib64/libz.so" ] 131 | 09-21 19:24:10.795 10403 10403 W linker : [ Linking "/system/lib64/liblog.so" ] 132 | 09-21 19:24:10.795 10403 10403 W linker : [ Linking "/system/lib64/libprocessgroup.so" ] 133 | 09-21 19:24:10.795 10403 10403 W linker : [ Linking "/system/lib64/libselinux.so" ] 134 | 09-21 19:24:10.795 10403 10403 W linker : [ Linking "/apex/com.android.runtime/lib64/bionic/libc.so" ] 135 | 09-21 19:24:10.795 10403 10403 W linker : [ Linking "/apex/com.android.runtime/lib64/bionic/libm.so" ] 136 | 09-21 19:24:10.795 10403 10403 W linker : [ Linking "/apex/com.android.runtime/lib64/bionic/libdl.so" ] 137 | 09-21 19:24:10.795 10403 10403 W linker : [ Linking "/system/lib64/libc++.so" ] 138 | 09-21 19:24:10.795 10403 10403 W linker : [ Linking "/system/lib64/libbase.so" ] 139 | 09-21 19:24:10.795 10403 10403 W linker : [ Linking "/system/lib64/libcgrouprc.so" ] 140 | 09-21 19:24:10.795 10403 10403 W linker : [ Linking "/system/lib64/libpcre2.so" ] 141 | 09-21 19:24:10.795 10403 10403 W linker : [ Linking "/system/lib64/libpackagelistparser.so" ] 142 | 09-21 19:24:10.796 10403 10403 W linker : [ Linking "/system/lib64/libnetd_client.so" ] 143 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x63f23b918000 + 0x80000 ] 144 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7ffff517c000 + 0x1000 linux-vdso.so.1 ] 145 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b4242039000 + 0x18b000 libcrypto.so ] 146 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b4241e53000 + 0x1c000 libz.so ] 147 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b42427e5000 + 0x12000 liblog.so ] 148 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b424268a000 + 0x5b000 libprocessgroup.so ] 149 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b424281b000 + 0x1d000 libselinux.so ] 150 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b4240380000 + 0x633000 libc.so ] 151 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b4241eea000 + 0x4b000 libm.so ] 152 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b4242658000 + 0x5000 libdl.so ] 153 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b4241f45000 + 0xb8000 libc++.so ] 154 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b4242729000 + 0x42000 libbase.so ] 155 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b4241eaf000 + 0x6000 libcgrouprc.so ] 156 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b4240310000 + 0x5e000 libpcre2.so ] 157 | 09-21 19:24:10.797 10403 10403 W linker : [ CFI add 0x7b42427b8000 + 0x4000 libpackagelistparser.so ] 158 | 09-21 19:24:10.798 10403 10403 W linker : [ CFI add 0x7b4030272000 + 0xb000 libnetd_client.so: 0x7b4030277000 ] 159 | 09-21 19:24:10.799 10403 10403 W linker : [ Jumping to _start (0x63f23b947280)... ] 160 | ``` 161 | 162 | 比较短小,不过已经能让我们清楚地看到 linker 初始化,加载程序,最后跳转到入口执行的过程了。 163 | 164 | 再来看看 LD_DEBUG=2 的,此时已经很卡了,等了几秒才出结果,logcat 直接吐出了大量日志,这里节选了最后一小段: 165 | 166 | ```log 167 | 09-21 19:33:52.106 10411 10411 I linker : [ DT_INIT_ARRAY[0] == 0x7b929d26ec70 ] 168 | 09-21 19:33:52.106 10411 10411 I linker : [ Calling c-tor function @ 0x7b929d26ec70 for '/system/lib64/libprocessgroup.so' ] 169 | 09-21 19:33:52.106 10411 10411 I linker : [ Done calling c-tor function @ 0x7b929d26ec70 for '/system/lib64/libprocessgroup.so' ] 170 | 09-21 19:33:52.106 10411 10411 I linker : [ Done calling DT_INIT_ARRAY for '/system/lib64/libprocessgroup.so' ] 171 | 09-21 19:33:52.106 10411 10411 I linker : [ Calling DT_INIT_ARRAY (size 1) @ 0x7b929d0b2be8 for '/system/lib64/libselinux.so' ] 172 | 09-21 19:33:52.106 10411 10411 I linker : [ DT_INIT_ARRAY[0] == 0x7b929d0ab4f0 ] 173 | 09-21 19:33:52.106 10411 10411 I linker : [ Calling c-tor function @ 0x7b929d0ab4f0 for '/system/lib64/libselinux.so' ] 174 | 09-21 19:33:52.106 10411 10411 I linker : [ Done calling c-tor function @ 0x7b929d0ab4f0 for '/system/lib64/libselinux.so' ] 175 | 09-21 19:33:52.106 10411 10411 I linker : [ Done calling DT_INIT_ARRAY for '/system/lib64/libselinux.so' ] 176 | 09-21 19:33:52.106 10411 10411 I linker : [ Calling DT_INIT_ARRAY (size 2) @ 0x5ae6be612290 for '/system/bin/toybox' ] 177 | 09-21 19:33:52.106 10411 10411 I linker : [ DT_INIT_ARRAY[0] == 0xffffffffffffffff ] 178 | 09-21 19:33:52.106 10411 10411 I linker : [ DT_INIT_ARRAY[1] == 0x0 ] 179 | 09-21 19:33:52.106 10411 10411 I linker : [ Done calling DT_INIT_ARRAY for '/system/bin/toybox' ] 180 | 09-21 19:33:52.106 10411 10411 I linker : [ Ready to execute "/system/bin/toybox" @ 0x5ae6be5cd280 ] 181 | 09-21 19:33:52.106 10411 10411 W linker : [ Jumping to _start (0x5ae6be5cd280)... ] 182 | ``` 183 | 184 | 可以看到日志中甚至包含了调用 init array 的记录(以及地址)。从这里可以知道,程序最先被执行的代码是位于 DT_INIT_ARRAY 的函数,而后才是 `_start` 。 185 | 186 | > [Android安全–linker加载so流程,在.init下断点](http://www.alonemonkey.com/linker-load-so.html) 187 | 188 | 顺带一提,完整的日志有 11M : 189 | 190 | ![](res/images/20220921_03.png) 191 | 192 | 其中多半是…… 193 | 194 | ![](res/images/20220921_04.png) 195 | 196 | 由此可见,其实 LD_DEBUG=1 足够了。 -------------------------------------------------------------------------------- /linker.md: -------------------------------------------------------------------------------- 1 | # linker 2 | 3 | [linker log](linker-log.md) 4 | 5 | 为什么 dl_iterator_phdr 可以遍历到 linker (第一个就是),而 dladdr 不能根据 linker 的地址取得 linker 的 Dlinfo ? 6 | 7 | bionic/linker/linker.cpp 8 | 9 | 两者同样遍历 solist 链表: 10 | 11 | ```cpp 12 | // Here, we only have to provide a callback to iterate across all the 13 | // loaded libraries. gcc_eh does the rest. 14 | int do_dl_iterate_phdr(int (*cb)(dl_phdr_info* info, size_t size, void* data), void* data) { 15 | int rv = 0; 16 | for (soinfo* si = solist_get_head(); si != nullptr; si = si->next) { 17 | dl_phdr_info dl_info; 18 | dl_info.dlpi_addr = si->link_map_head.l_addr; 19 | dl_info.dlpi_name = si->link_map_head.l_name; 20 | ``` 21 | 22 | dl_iterator_phdr 的 dlpi_addr 取自 soinfo.link_map_head.l_addr 23 | 24 | ```cpp 25 | int do_dladdr(const void* addr, Dl_info* info) { 26 | // Determine if this address can be found in any library currently mapped. 27 | soinfo* si = find_containing_library(addr); 28 | if (si == nullptr) { 29 | return 0; 30 | } 31 | 32 | memset(info, 0, sizeof(Dl_info)); 33 | 34 | info->dli_fname = si->get_realpath(); 35 | // Address at which the shared object is loaded. 36 | info->dli_fbase = reinterpret_cast(si->base); 37 | 38 | // Determine if any symbol in the library contains the specified address. 39 | ElfW(Sym)* sym = si->find_symbol_by_address(addr); 40 | if (sym != nullptr) { 41 | info->dli_sname = si->get_string(sym->st_name); 42 | info->dli_saddr = reinterpret_cast(si->resolve_symbol_address(sym)); 43 | } 44 | 45 | return 1; 46 | } 47 | ``` 48 | 49 | dladdr 判断地址是否位于 so 的某个 loadable segment 里面,用的是 soinfo.base 和 soinfo.size 50 | 51 | find_containing_library: 52 | 53 | ```cpp 54 | soinfo* find_containing_library(const void* p) { 55 | // Addresses within a library may be tagged if they point to globals. Untag 56 | // them so that the bounds check succeeds. 57 | ElfW(Addr) address = reinterpret_cast(untag_address(p)); 58 | for (soinfo* si = solist_get_head(); si != nullptr; si = si->next) { 59 | if (address < si->base || address - si->base >= si->size) { 60 | continue; 61 | } 62 | ElfW(Addr) vaddr = address - si->load_bias; 63 | for (size_t i = 0; i != si->phnum; ++i) { 64 | const ElfW(Phdr)* phdr = &si->phdr[i]; 65 | if (phdr->p_type != PT_LOAD) { 66 | continue; 67 | } 68 | if (vaddr >= phdr->p_vaddr && vaddr < phdr->p_vaddr + phdr->p_memsz) { 69 | return si; 70 | } 71 | } 72 | } 73 | return nullptr; 74 | } 75 | ``` 76 | 77 | 而 linker_main 初始化自己的 soinfo 的时候似乎没有初始化这个字段,因此无法通过 find_containing_library 得到 linker 自身(不知道是不是有意而为的)。 78 | 79 | ```cpp 80 | // bionic/linker/linker_main.cpp __linker_init_post_relocation 81 | // bionic/linker/dlfcn.cpp get_libdl_info 82 | ``` 83 | 84 | 同样的道理,__loader_dlopen 的 caller_addr 传 __loader_dlopen 自己的地址(也就是 linker 的地址)也不能让获取到的 ns 变成 linker 所属的 ns ,而是使用了 anonymous ns ,默认是 global ns ,不过在 java 进程会设置成 classloader ns 。 85 | -------------------------------------------------------------------------------- /load-library-in-app-process.md: -------------------------------------------------------------------------------- 1 | # 在 app process 中使用 jni 2 | 3 | ## 加载 so 4 | 5 | 相比 zygote 启动的应用,app_process 启动虽然可以直接加载 apk ,但是并不会设置 nativeLibraryPath ,无法直接从 apk 加载 so ,因此我们 hack 一下: 6 | 7 | ```kotlin 8 | class JNI { 9 | companion object { 10 | init { 11 | JNI::class.java.classLoader!!.getField("pathList")!!.let { pathList -> 12 | val firstElement = (pathList.getField("dexElements") as Array<*>)[0]!! 13 | val path = firstElement.getField("path") as File 14 | val abi = if (Process.is64Bit()) Build.SUPPORTED_64_BIT_ABIS[0] else Build.SUPPORTED_32_BIT_ABIS[0] 15 | pathList.invokeMethod("addNativePath", 16 | arrayOf(Collection::class.java), 17 | arrayOf(listOf("${path.absolutePath}!/lib/$abi")) 18 | ) 19 | } 20 | System.loadLibrary("ash") 21 | } 22 | } 23 | 24 | external fun hello(): String 25 | } 26 | ``` 27 | 28 | 这里假定 ClassLoader 是 PathClassLoader 。 29 | 30 | ## 在 signal handler 与 jvm 交互 31 | 32 | 研究这个 jni 其实主要目的是在我的 shell 程序中处理 Ctrl+C ,在 linux 下自然是要处理 SIGINT 信号。 33 | 34 | ```cpp 35 | static JavaVM* gVM = nullptr; 36 | static jobject gInstance = nullptr; 37 | static void handler(int sig) { 38 | JNIEnv *env; 39 | LOGD("received signal %d at thread %d", sig, gettid()); 40 | int e = gVM->GetEnv((void**) &env, JNI_VERSION_1_6); 41 | if (e == JNI_OK) { 42 | jmethodID callback = env->GetMethodID(env->GetObjectClass(gInstance), "onInterrupted", "()V"); 43 | env->CallVoidMethod(gInstance, callback); 44 | } else { 45 | LOGE("failed to getEnv %d", e); 46 | } 47 | } 48 | 49 | extern "C" 50 | JNIEXPORT void JNICALL 51 | Java_five_ec1cff_ash_JNI_nativeInitSignalHandler(JNIEnv *env, jobject thiz) { 52 | env->GetJavaVM(&gVM); 53 | gInstance = env->NewGlobalRef(thiz); 54 | signal(SIGINT, handler); 55 | } 56 | ``` 57 | 58 | ```kotlin 59 | external fun nativeInitSignalHandler() 60 | 61 | fun onInterrupted() { 62 | // Thread.currentThread().interrupt() 63 | println("test") 64 | } 65 | ``` 66 | 67 | 代码看起来没有任何问题,然而实际运行起来按 Ctrl + C 后总是报错 StackOverflowError (如下): 68 | 69 | ```java 70 | ^Cjava.lang.StackOverflowError: stack size 8192KB 71 | at java.lang.Thread.sleep(Native Method) 72 | at java.lang.Thread.sleep(Thread.java:442) 73 | at java.lang.Thread.sleep(Thread.java:358) 74 | at five.ec1cff.ash.Main$run$15.onCommand(Main.kt:158) 75 | at five.ec1cff.ash.ArgParser.runHandler(ArgParser.java:80) 76 | at five.ec1cff.ash.Main.run(Main.kt:181) 77 | at five.ec1cff.ash.Main$Companion.main$lambda-1(Main.kt:190) 78 | at five.ec1cff.ash.Main$Companion.$r8$lambda$_3oe5AW9liX-gULKkqL_PYYbzDA(Unknown Source:0) 79 | at five.ec1cff.ash.Main$Companion$$ExternalSyntheticLambda0.run(Unknown Source:2) 80 | at five.ec1cff.ash.SystemHelperKt.runOnActivityThread(SystemHelper.kt:46) 81 | at five.ec1cff.ash.SystemHelperKt.runOnActivityThread$default(SystemHelper.kt:19) 82 | at five.ec1cff.ash.Main$Companion.main(Main.kt:190) 83 | at five.ec1cff.ash.Main.main(Unknown Source:2) 84 | at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method) 85 | at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:463) 86 | ``` 87 | 88 | 搜索 `jni signal` 查到这么一条 issue : 89 | 90 | [ART prevents any Java calls from JNI during native signal handling [37035211] - Visible to Public - Issue Tracker](https://issuetracker.google.com/issues/37035211) 91 | 92 | 看上去 ART 虚拟机不支持在 signal handler 中和 jvm 交互,原因似乎是 ART 使用了 sigaltstack (其实我没懂)。官方也表示不会修复。看起来在 app process 中处理信号相当麻烦了。(难怪没有实现 `sun.misc.SignalHandler` ) -------------------------------------------------------------------------------- /lsplant.md: -------------------------------------------------------------------------------- 1 | # LSPlant 2 | 3 | https://github.com/LSPosed/LSPlant 4 | 5 | ## 基本原理 6 | 7 | 修改 ArtMethod 的 entry_point_from_quick_compiled_code_ ,这里原先是 dex2oat 优化后的本机代码,现在指向我们控制的代码。同时通过各种手段使调用方法时走本机代码 。 8 | 9 | 为此,hook 一个 method 需要另外两个 method ,一个作为 hook 的 stub ;另一个 method 用于备份被 hook 的方法。LSPlant 使用 DexBuilder 生成包含这两个方法的辅助类。 10 | 11 | ## ArtMethod 调用约定 12 | 13 | 我们可以从反射执行看 oat 后 ArtMethod 的本机代码是如何调用的。 14 | 15 | [ArtMethod::Invoke](https://cs.android.com/android/platform/superproject/+/master:art/runtime/art_method.cc;l=365;drc=473c5a01699e82723c936bfd47ceac9abee70e09) 16 | 17 | 根据方法是否为 static ,执行一段汇编编写的代码 art_quick_invoke_stub 或 art_quick_invoke_static_stub ,其代码位于: 18 | 19 | `art/runtime/arch/{arch}/quick_entrypoints_{arch}.S` 20 | 21 | [art/runtime/arch/x86_64/quick_entrypoints_x86_64.S](https://cs.android.com/android/platform/superproject/+/master:art/runtime/arch/x86_64/quick_entrypoints_x86_64.S;l=415;drc=630c507467f637a5c9221db1558cd679f494fb6a) 22 | 23 | ## Trampoline 24 | 25 | 根据调用约定,进入 art method 第一个参数应该是 ArtMethod 指针,而 hook 方法替换了 entry_point_from_quick_compiled_code_ ,但 caller 调用时仍然传入了原 ArtMethod 的指针,我们需要把它替换成自己的 ArtMethod 指针。因此 entry_point_from_quick_compiled_code_ 指向一段跳板代码完成这个工作,即把第一个参数换成自己的 ArtMethod ,并从 26 | 27 | ```cpp 28 | auto [trampoline, entry_point_offset, art_method_offset] = GetTrampoline(); 29 | ``` 30 | 31 | trampoline 表示跳板代码内容 32 | 33 | entry_point_offset 表示 ArtMethod 的 entry_point_from_quick_compiled_code_ 偏移在指令中的位置 (按位),当 entry_point_from_quick_compiled_code_ 的偏移计算好后,会根据 entry_point_offset 修正 trampoline 指令。 34 | 35 | art_method_offset 表示 ArtMethod 地址在指令中的位置(按字节)。LSPlant 为每个方法生成一个跳板代码,生成时在该偏移写入 hook 的 ArtMethod 指针。 36 | 37 | 下面具体分析一些 arch 的 code : 38 | 39 | ### x86-64 40 | 41 | ```cpp 42 | if constexpr (kArch == Arch::kX86_64) { 43 | return std::make_tuple("\x48\xbf\x78\x56\x34\x12\x78\x56\x34\x12\xff\x77\x00\xc3"_uarr, 44 | // NOLINTNEXTLINE 45 | uint8_t{96u}, uintptr_t{2u}); 46 | } 47 | ``` 48 | 49 | ``` 50 | 0x0000000000000000: 48 BF 78 56 34 12 78 56 34 12 movabs rdi, 0x1234567812345678 # ArtMethod 地址置于 rdi 中 51 | 0x000000000000000a: FF 77 xx push qword ptr [rdi + xx] # 取 hook ArtMethod 的 entry_point_from_quick_compiled_code_ 放到栈上 52 | 0x000000000000000d: C3 ret # 跳转到 hook 的 entry_point_from_quick_compiled_code_ 53 | ``` 54 | 55 | ### arm64 56 | 57 | ```cpp 58 | if constexpr (kArch == Arch::kArm64) { 59 | return std::make_tuple( 60 | "\x60\x00\x00\x58\x10\x00\x40\xf8\x00\x02\x1f\xd6\x78\x56\x34\x12\x78\x56\x34\x12"_uarr, 61 | // NOLINTNEXTLINE 62 | uint8_t{44u}, uintptr_t{12u}); 63 | } 64 | ``` 65 | 66 | ``` 67 | 0x0000000000000000: 60 00 00 58 ldr x0, #0xc # 读相对第一条指令 0xc 偏移的位置的内存,即 hook 的 ArtMethod 地址到第一个参数 (x0) 68 | 0x0000000000000004: 10 x0 4x F8 ldur x16, [x0] # 取 entry_point_from_quick_compiled_code_ 69 | 0x0000000000000008: 00 02 1F D6 br x16 # 跳转到 hook 70 | 0x000000000000000c: 78 56 34 12 and w24, w19, #0xfffff003 # ArtMethod 地址 71 | 0x0000000000000010: 78 56 34 12 and w24, w19, #0xfffff003 72 | ``` 73 | 74 | ### arm 75 | 76 | ```cpp 77 | if constexpr (kArch == Arch::kArm) { 78 | return std::make_tuple("\x00\x00\x9f\xe5\x00\xf0\x90\xe5\x78\x56\x34\x12"_uarr, 79 | // NOLINTNEXTLINE 80 | uint8_t{32u}, uintptr_t{8u}); 81 | } 82 | ``` 83 | 84 | 指令是 arm 模式而非 thumb (因此地址也是偶数) 85 | 86 | ``` 87 | 0: e59f0000 ldr r0, [pc] # 加载 pc+8 到第一个参数,即 hook ArtMethod 地址 88 | 4: e590f0xx ldr pc, [r0, #xx] # hook entry_point_from_quick_compiled_code_ 送 pc 直接跳转 89 | 8: 12345678 # hook ArtMethod 地址 90 | ``` 91 | 92 | [PC+8](https://stackoverflow.com/questions/24091566/why-does-the-arm-pc-register-point-to-the-instruction-after-the-next-one-to-be-e) 93 | 94 | ### x86 95 | 96 | ```cpp 97 | if constexpr (kArch == Arch::kX86) { 98 | return std::make_tuple("\xb8\x78\x56\x34\x12\xff\x70\x00\xc3"_uarr, 99 | // NOLINTNEXTLINE 100 | uint8_t{56u}, uintptr_t{1u}); 101 | } 102 | ``` 103 | 104 | ``` 105 | 0: b8 78 56 34 12 movl $0x12345678, %eax # imm = 0x12345678 106 | 5: ff 70 xx pushl (%eax + xx) 107 | 8: c3 retl 108 | ``` 109 | 110 | ## 疑难杂症 111 | 112 | ### 设置备份为 private 113 | 114 | https://github.com/LSPosed/LSPlant/blob/ab5830a0207a76cc2abc82e6d4f15f2053f51523/lsplant/src/main/jni/lsplant.cc#L530 115 | 116 | ```cpp 117 | if (!backup->IsStatic()) backup->SetPrivate(); 118 | ``` 119 | 120 | 和方法的解析有关,一般非 static 非 private 则认为是虚函数,会在虚表上解析,导致无法正确解析到备份方法的地址 121 | 122 | 可以参考 [ART深度探索开篇:从Method Hook谈起 | Weishu's Notes](https://weishu.me/2017/03/20/dive-into-art-hello-world/) 123 | 124 | > 在调用的时候,如果不是static的方法,会去查找这个方法的真正实现;我们直接把原方法做了备份之后,去调用备份的那个方法,如果此方法是public的,则会查找到原来的那个函数,于是就无限循环了;我们只需要阻止这个过程,查看 FindVirtualMethodForVirtualOrInterface 这个方法的实现就知道,只要方法是 invoke-direct 进行调用的,就会直接返回原方法,这些方法包括:构造函数,private的方法( 见 https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html) 因此,我们手动把这个备份的方法属性修改为private即可解决这个问题。 125 | 126 | ## 参考 127 | 128 | [YAHFA--ART环境下的Hook框架 - 记事本](http://rk700.github.io/2017/03/30/YAHFA-introduction/) 129 | 130 | [在Android N上对Java方法做hook遇到的坑 - 记事本](http://rk700.github.io/2017/06/30/hook-on-android-n/) 131 | -------------------------------------------------------------------------------- /lsposed-native-hook.md: -------------------------------------------------------------------------------- 1 | # LSP Native Hook 踩坑 2 | 3 | 准备用 LSP 的 native hook 实现之前提到的 [`ImNotObscured`](im-not-obscured.md) ,不过遇到了不少坑,记录一下。 4 | 5 | [Native Hook · LSPosed/LSPosed Wiki](https://github.com/LSPosed/LSPosed/wiki/Native-Hook) 6 | 7 | ## sepolicy 8 | 9 | 看上去 LSP 没有对在系统服务里面加载 so 做特殊处理,因此系统没法加载模块的 so 10 | 11 | ``` 12 | avc: denied { execute } for path="/data/app/~~VLQwcy9J5DQF_BVy2ONN7w==/fivecc.tools.im_not_obscured-dRL-Ki7aYUu5JDh-h2Poeg==/base.apk" dev="dm-5" ino=41029 scontext=u:r:system_server:s0 tcontext=u:object_r:apk_data_file:s0 tclass=file permissive=0 13 | ``` 14 | 15 | 解决方法:临时加一个规则上去 16 | 17 | ``` 18 | echo "allow system_server apk_data_file file execute" > add_pol 19 | magiskpolicy --apply add_pol --live 20 | ``` 21 | 22 | ## dlopen 23 | 24 | 看上去模块的 namespace 也没法 dlopen 某些系统库,于是去 LSP 抄了一份 elf_utils 。 25 | 26 | ``` 27 | 12-11 12:41:28.398 31376 31376 D linker : dlopen(name="libinput.so", flags=0x0, extinfo=(null), caller="/data/app/~~VLQwcy9J5DQF_BVy2ONN7w==/fivecc.tools.im_not_obscured-dRL-Ki7aYUu5JDh-h2Poeg==/base.apk!/lib/x86_64/libim_not_obscured.so", caller_ns=classloader-namespace@0x7f2269859b10, targetSdkVersion=10000) ... 28 | 12-11 12:41:28.398 31376 31376 D linker : find_libraries(ns=classloader-namespace): task=libinput.so, is_dt_needed=0 29 | 12-11 12:41:28.398 31376 31376 D linker : load_library(ns=classloader-namespace, task=libinput.so, flags=0x0, search_linked_namespaces=1): calling open_library with realpath= 30 | 12-11 12:41:28.398 31376 31376 D linker : load_library(ns=classloader-namespace, task=libinput.so, flags=0x0, realpath=/system/lib64/libinput.so, search_linked_namespaces=1) 31 | 12-11 12:41:28.398 31376 31376 D linker : load_library(ns=classloader-namespace, task=libinput.so): Adding DT_NEEDED task: libbase.so 32 | 12-11 12:41:28.398 31376 31376 D linker : load_library(ns=classloader-namespace, task=libinput.so): Adding DT_NEEDED task: liblog.so 33 | 12-11 12:41:28.398 31376 31376 D linker : load_library(ns=classloader-namespace, task=libinput.so): Adding DT_NEEDED task: libcutils.so 34 | 12-11 12:41:28.398 31376 31376 D linker : load_library(ns=classloader-namespace, task=libinput.so): Adding DT_NEEDED task: libutils.so 35 | 12-11 12:41:28.398 31376 31376 D linker : load_library(ns=classloader-namespace, task=libinput.so): Adding DT_NEEDED task: libbinder.so 36 | 12-11 12:41:28.398 31376 31376 D linker : load_library(ns=classloader-namespace, task=libinput.so): Adding DT_NEEDED task: libui.so 37 | 12-11 12:41:28.398 31376 31376 D linker : load_library(ns=classloader-namespace, task=libinput.so): Adding DT_NEEDED task: libc++.so 38 | -- 39 | 12-11 12:41:28.407 31376 31376 D linker : find_libraries(ns=classloader-namespace): task=libc.so, is_dt_needed=1 40 | 12-11 12:41:28.407 31376 31376 D linker : find_library_internal(ns=classloader-namespace, task=libc.so): Already loaded (by soname): /apex/com.android.runtime/lib64/bionic/libc.so 41 | 12-11 12:41:28.408 31376 31376 D linker : find_libraries(ns=classloader-namespace): task=libm.so, is_dt_needed=1 42 | 12-11 12:41:28.408 31376 31376 D linker : find_library_internal(ns=classloader-namespace, task=libm.so): Already loaded (by soname): /apex/com.android.runtime/lib64/bionic/libm.so 43 | 12-11 12:41:28.408 31376 31376 D linker : find_libraries(ns=classloader-namespace): task=libdl.so, is_dt_needed=1 44 | 12-11 12:41:28.408 31376 31376 D linker : find_library_internal(ns=classloader-namespace, task=libdl.so): Already loaded (by soname): /apex/com.android.runtime/lib64/bionic/libdl.so 45 | 12-11 12:41:28.408 31376 31376 D linker : find_libraries(ns=classloader-namespace): task=libdl_android.so, is_dt_needed=1 46 | 12-11 12:41:28.408 31376 31376 D linker : load_library(ns=classloader-namespace, task=libdl_android.so, flags=0x0, search_linked_namespaces=1): calling open_library with realpath= 47 | 12-11 12:41:28.408 31376 31376 D linker : load_library(ns=classloader-namespace, task=libdl_android.so, flags=0x0, realpath=/apex/com.android.runtime/lib64/bionic/libdl_android.so, search_linked_namespaces=1) 48 | 12-11 12:41:28.408 31376 31376 D linker : library "libdl_android.so" needed or dlopened by "/system/lib64/libvndksupport.so" is not accessible for the namespace "classloader-namespace" 49 | 12-11 12:41:28.408 31376 31376 E linker : library "libdl_android.so" ("/apex/com.android.runtime/lib64/bionic/libdl_android.so") needed or dlopened by "/system/lib64/libvndksupport.so" is not accessible for the namespace: [name="classloader-namespace", ld_library_paths="", default_library_paths="/data/app/~~VLQwcy9J5DQF_BVy2ONN7w==/fivecc.tools.im_not_obscured-dRL-Ki7aYUu5JDh-h2Poeg==/base.apk!/lib/x86_64:/system/lib64:/system_ext/lib64", permitted_paths="/data:/mnt/expand"] 50 | 12-11 12:41:28.409 31376 31376 D linker : find_library_internal(ns=classloader-namespace, task=libdl_android.so): Trying 4 linked namespaces 51 | 12-11 12:41:28.409 31376 31376 D linker : find_library_in_linked_namespace(ns=(default), task=libdl_android.so): Not accessible (soname=libdl_android.so) 52 | 12-11 12:41:28.409 31376 31376 D linker : find_library_in_linked_namespace(ns=com_android_art, task=libdl_android.so): Not accessible (soname=libdl_android.so) 53 | 12-11 12:41:28.409 31376 31376 D linker : find_library_in_linked_namespace(ns=com_android_neuralnetworks, task=libdl_android.so): Not accessible (soname=libdl_android.so) 54 | 12-11 12:41:28.409 31376 31376 D linker : find_library_in_linked_namespace(ns=com_android_os_statsd, task=libdl_android.so): Not accessible (soname=libdl_android.so) 55 | 12-11 12:41:28.409 31376 31376 D linker : ... dlclose(realpath="/system/lib64/libinput.so"@0x7f2269931910) ... load group root is "/system/lib64/libinput.so"@0x7f2269931910 56 | 12-11 12:41:28.409 31376 31376 D linker : ... dlclose: calling destructors for "/system/lib64/libinput.so"@0x7f2269931910 ... 57 | 12-11 12:41:28.409 31376 31376 D linker : ... dlclose: calling destructors for "/system/lib64/libinput.so"@0x7f2269931910 ... done 58 | 12-11 12:41:28.409 31376 31376 D linker : ... dlclose: calling destructors for "/system/lib64/libbinder.so"@0x7f2269932250 ... 59 | 12-11 12:41:28.409 31376 31376 D linker : ... dlclose: calling destructors for "/system/lib64/libbinder.so"@0x7f2269932250 ... done 60 | ``` 61 | 62 | ```cpp 63 | extern "C" [[gnu::visibility("default")]] [[gnu::used]] 64 | NativeOnModuleLoaded native_init(const NativeAPIEntries *entries) { 65 | hook_func = entries->hook_func; 66 | SandHook::ElfImg elf("libinput.so"); 67 | auto target = elf.getSymbAddress("_ZN7android14InputPublisher18publishMotionEventEjiiiiNSt3__15arrayIhLm32EEEiiiiiiNS_20MotionClassificationEfffffffflljPKNS_17PointerPropertiesEPKNS_13PointerCoordsE"); 68 | LOGD("function=%p", target); 69 | auto r = hook_func(target, (void *) PublishMotionEventFunc_API30_new, 70 | (void **) &PublishMotionEventFunc_API30_old); 71 | LOGD("hook result=%d", r); 72 | return on_library_loaded; 73 | } 74 | ``` 75 | -------------------------------------------------------------------------------- /minimal-linux.md: -------------------------------------------------------------------------------- 1 | # 在 QEMU 上体验 Linux 最小系统 2 | 3 | ## 构建 4 | 5 | [Building the Minimal Rootfs Using Busybox – Embedded learning](https://embeddedstudy.home.blog/2019/01/23/building-the-minimal-rootfs-using-busybox/) 6 | 7 | [利用busybox构造linux rootfs - 知乎](https://zhuanlan.zhihu.com/p/409425657) 8 | 9 | [使用busybox制作完成的最小rootfs - 王东力 - 博客园](https://www.cnblogs.com/ethandlwang/p/14789266.html) 10 | 11 | Busybox rootfs img 12 | 13 | [使用busybox制作根文件系统,并使用qemu加载_科技ing的博客-CSDN博客](https://blog.csdn.net/WMX843230304WMX/article/details/102869468) 14 | 15 | [RootFS and Network of Qemu](http://zhiyisun.github.io/2015/04/10/rootfs-and-network-qemu.html) 16 | 17 | ## 获取最小系统 18 | 19 | 因为暂时不想研究构建,所以找了一个现成的用: 20 | 21 | [系统调用和 UNIX Shell](https://jyywiki.cn/OS/2022/slides/13.slides#/1/1) 22 | 23 | 这是南京大学的操作系统课程(强烈推荐!)课件,PPT 第四页有一个 linux-minimal.zip 下载。 24 | 25 | https://box.nju.edu.cn/f/3f67e092e1ba441187d9/?dl=1 26 | 27 | 这个 zip 包含了一个内核(vmlinuz),一个 initramfs (只有一个 busybox 和一个 sh 写成的 init),以及一个 Makefile 28 | 29 | ```mk 30 | .PHONY: initramfs run clean 31 | 32 | $(shell mkdir -p build) 33 | 34 | initramfs: 35 | @cd initramfs && find . -print0 | cpio --null -ov --format=newc | gzip -9 \ 36 | > ../build/initramfs.cpio.gz 37 | 38 | run: 39 | @qemu-system-x86_64 \ 40 | -nographic \ 41 | -serial mon:stdio \ 42 | -m 128 \ 43 | -kernel vmlinuz \ 44 | -initrd build/initramfs.cpio.gz \ 45 | -append "console=ttyS0 quiet acpi=off" 46 | 47 | clean: 48 | @rm -rf build 49 | ``` 50 | 51 | 下面在 WSL 里面运行: 52 | 53 | 首先安装 qemu 54 | 55 | ``` 56 | apt install qemu-system 57 | ``` 58 | 59 | ``` 60 | # 构建 initramfs 61 | make 62 | # 运行 63 | make run 64 | ``` 65 | 66 | 进入系统,发现甚至连 proc 都没有,还要自己 mount ;调用命令也必须要通过 busybox : 67 | 68 | ![](res/images/20221107_01.png) 69 | 70 | ```sh 71 | / # busybox mount proc /proc -t proc 72 | / # busybox mount 73 | rootfs on / type rootfs (rw,size=46668k,nr_inodes=11667) 74 | proc on /proc type proc (rw,relatime) 75 | ``` 76 | 77 | ![](res/images/20221107_02.png) 78 | 79 | 直接退出终端会导致 kernel panic ,需要使用 `Ctrl+A X` 退出 qemu 80 | 81 | ![](res/images/20221107_03.png) 82 | 83 | sh 想创建一个后台任务,结果居然打不开 /dev/null ,也要自己创建: 84 | 85 | ![](res/images/20221107_04.png) 86 | 87 | [linux - How to create /dev/null? - Unix & Linux Stack Exchange](https://unix.stackexchange.com/questions/27279/how-to-create-dev-null) 88 | 89 | [null(4) - Linux manual page](https://man7.org/linux/man-pages/man4/zero.4.html) 90 | 91 | ``` 92 | / # busybox mknod /dev/null c 1 3 93 | / # busybox ls 94 | bin dev init proc root sys 95 | / # busybox ls /dev 96 | console null 97 | / # busybox ls /dev -l 98 | total 0 99 | crw------- 1 0 0 5, 1 Nov 7 12:25 console 100 | crw-r--r-- 1 0 0 1, 3 Nov 7 12:25 null 101 | / # busybox chmod 666 /dev/null 102 | ``` 103 | 104 | # initrd & initramfs 105 | 106 | [Linux内核Ramdisk(initrd)机制【转】 - sky-heaven - 博客园](https://www.cnblogs.com/sky-heaven/p/13856545.html) 107 | 108 | [](https://www.kernel.org/doc/Documentation/filesystems/ramfs-rootfs-initramfs.txt) 109 | 110 | # 其他 111 | 112 | [在qemu环境中用gdb调试Linux内核 - wipan - 博客园](https://www.cnblogs.com/wipan/p/9264979.html) 113 | 114 | [Initramfs 原理和实践 - wipan - 博客园](https://www.cnblogs.com/wipan/p/9269505.html) 115 | 116 | [根文件系统挂载过程分析 - 知乎](https://zhuanlan.zhihu.com/p/347110313) 117 | 118 | [linux创建init进程的3种实现方式原理分析【转】 - sky-heaven - 博客园](https://www.cnblogs.com/sky-heaven/p/16116574.html) 119 | -------------------------------------------------------------------------------- /miui_perms.md: -------------------------------------------------------------------------------- 1 | # MIUI 权限 2 | 3 | ## 「自启动」权限 4 | 5 | 自启动看起来包括了服务重启、开机广播,还有 wakepath (应用唤起 activity)等。 6 | 7 | 包含自启动设置的 Activity :`com/miui/appmanager/ApplicationsDetailsActivity` ,属于 `com.miui.securitycenter` 包(也就是应用详情)。 8 | 9 | `com/miui/permission/PermissionManager` 包含了一系列权限控制的方法,以及常量(见[附录 1](#constants)) 10 | 11 | 权限控制由另一个包 `com.lbe.security.miui` 的 ContentProvider 提供 12 | 13 | ```xml 14 | 19 | ``` 20 | 21 | 有一个自定义权限,这个权限是 signatureOrSystem 的,而且 shell 没有权限,只能 root 访问了。 22 | 23 | ## 设置 App 权限 24 | 25 | 参照 PermissionManager 的 setApplicationPermissions 方法写了一个设置权限的方法: 26 | 27 | ```kotlin 28 | private const val AUTHORITY = "com.lbe.security.miui.permmgr" 29 | 30 | fun setAppPermission(packageName: String, permId: Long, action: Int, flags: Int=0) { 31 | // Os.seteuid(1000) 32 | useContentProvider(AUTHORITY) { 33 | println(it.callCompat(AUTHORITY, "6", null, Bundle().apply { 34 | putLong("extra_permission", permId) 35 | putInt("extra_action", action) 36 | putStringArray("extra_package", arrayOf(packageName)) 37 | putInt("extra_flags", flags) 38 | println(this) 39 | })) 40 | } 41 | } 42 | ``` 43 | 44 | > 其中,call 的方法是 `6` (并未在常量给出,具体作用可以反编译 `com.lbe.security.miui` 查看。 45 | > `extra_permission` 是一个类似于 flags 的东西(与内部存储方式有关,权限存储在 long 的每个位上),值为 `PERM_` 前缀的常量。`16384` 为自启动。 46 | > `extra_action` 为操作,`1` 禁止,`3` 允许。值为 `ACTION_` 前缀的常量。 47 | > `extra_package` 是包的列表(就是这个导致没法用 content 命令写😅) 48 | > `extra_flags` 默认为 0 。值为 `FLAG_` 前缀的常量。观察到有一个 flag 与杀死进程有关,而关闭自启时应用会被杀死(实在不懂这个意图),但 flags 仍为 0 。根据反编译发现是关闭的时候默认总是杀死应用(😅😅😅) 49 | 50 | ## 查询 App 权限 51 | 52 | 查询权限的方法被混淆,参考了下面这个方法: 53 | 54 | ``` 55 | com/miui/permcenter/j a(Landroid/content/Context;JLjava/lang/String;)Lcom/miui/permcenter/d; 56 | ``` 57 | 58 | 可以用 content query 查询: 59 | 60 | ``` 61 | content query --uri content://com.lbe.security.miui.permmgr/active --where 'pkgName="five.ec1cff.assistant"' 62 | ``` 63 | 64 | 得到的结果如下: 65 | 66 | ``` 67 | Row: 0 _id=506 68 | pkgName=five.ec1cff.assistant 69 | installTime=1656347084014 70 | uninstallTime=0 71 | present=1 72 | pruneAfterDelete=0 73 | lastConfigured=0 74 | lastUpdated=1656332731269 75 | permMask=6309578099143000064 76 | suggestAccept=1441168485102157824 77 | suggestPrompt=283203338271 78 | suggestReject=112607600083747328 79 | forcedBits=0 80 | permDesc=BLOB 81 | userAccept=103079231488 82 | userPrompt=0 83 | userReject=0 84 | suggestBlock=0 85 | suggestForeground=4755801756259057664 86 | userForeground=0 87 | ``` 88 | 89 | 我们编写的设置权限方法会影响 `userAccept` 和 `userReject` (应该就是权限的 bitset) 90 | 91 | ## bug 92 | 93 | 如果应用详情的自启动显示为「允许」是在这个 ui 设置的,那么用上面的 call 方法设置为「禁止」之后并不会反映在 ui 中,也就是仍然显示为「允许」(甚至强杀重启都不会改变),但是 query 的结果明明和手动关闭一致。但如果是我们用 call 方法设置过的「允许」,就不会有上述问题。至于 call 了之后是不是真的起作用了,也有待进一步研究。 94 | 95 | ## 附录 1 PermissionManager 的常量 96 | 97 | 98 | 99 | ```java 100 | public static final int ACTION_ACCEPT = 3; 101 | public static final int ACTION_BLOCK = 4; 102 | public static final int ACTION_DEFAULT = 0; 103 | public static final int ACTION_FOREGROUND = 6; 104 | public static final int ACTION_NONBLOCK = 5; 105 | public static final int ACTION_PROMPT = 2; 106 | public static final int ACTION_REJECT = 1; 107 | public static final int ACTION_VIRTUAL = 7; 108 | public static final int FLAG_GRANT_ONTTIME = 4; 109 | public static final int FLAG_GRANT_THREESEC = 8; 110 | public static final int FLAG_KILL_PROCESS = 2; 111 | public static final int GET_APP_COUNT = 1; 112 | public static final int GROUP_CHARGES = 1; 113 | public static final int GROUP_MEDIA = 4; 114 | public static final int GROUP_PRIVACY = 2; 115 | public static final int GROUP_SENSITIVE_PRIVACY = 16; 116 | public static final int GROUP_SETTINGS = 8; 117 | public static final long PERM_ID_ACCESS_XIAOMI_ACCOUNT = 4294967296L; 118 | public static final long PERM_ID_ACTIVITY_RECOGNITION = 137438953472L; 119 | public static final long PERM_ID_ADD_VOICEMAIL = 281474976710656L; 120 | public static final long PERM_ID_AUDIO_RECORDER = 131072; 121 | public static final long PERM_ID_AUTOSTART = 16384; 122 | public static final long PERM_ID_BACKGROUND_LOCATION = 2305843009213693952L; 123 | public static final long PERM_ID_BACKGROUND_START_ACTIVITY = 72057594037927936L; 124 | public static final long PERM_ID_BLUR_LOCATION = 8589934592L; 125 | public static final long PERM_ID_BODY_SENSORS = 70368744177664L; 126 | public static final long PERM_ID_BOOT_COMPLETED = 0x08000000; 127 | public static final long PERM_ID_BT_CONNECTIVITY = 4194304; 128 | public static final long PERM_ID_CALENDAR = 0x01000000; 129 | public static final long PERM_ID_CALLLOG = 16; 130 | public static final long PERM_ID_CALLMONITOR = 2048; 131 | public static final long PERM_ID_CALLPHONE = 2; 132 | public static final long PERM_ID_CALLSTATE = 1024; 133 | public static final long PERM_ID_CLIPBOARD = 4611686018427387904L; 134 | public static final long PERM_ID_CONTACT = 8; 135 | public static final long PERM_ID_DEAMON_NOTIFICATION = 1152921504606846976L; 136 | public static final long PERM_ID_DISABLE_KEYGUARD = 8388608; 137 | public static final long PERM_ID_EXTERNAL_STORAGE = 35184372088832L; 138 | public static final long PERM_ID_GALLERY_RESTRICTION = 68719476736L; 139 | public static final long PERM_ID_GET_ACCOUNTS = 140737488355328L; 140 | public static final long PERM_ID_GET_INSTALLED_APPS = 144115188075855872L; 141 | public static final long PERM_ID_GET_TASKS = 18014398509481984L; 142 | public static final long PERM_ID_INSTALL_PACKAGE = 65536; 143 | public static final long PERM_ID_INSTALL_SHORTCUT = 4503599627370496L; 144 | public static final long PERM_ID_LOCATION = 32; 145 | public static final long PERM_ID_MEDIA_VOLUME = 549755813888L; 146 | public static final long PERM_ID_MMSDB = 262144; 147 | public static final long PERM_ID_MOBILE_CONNECTIVITY = 1048576; 148 | public static final long PERM_ID_NETDEFAULT = 128; 149 | public static final long PERM_ID_NETWIFI = 256; 150 | public static final long PERM_ID_NFC = 2251799813685248L; 151 | public static final long PERM_ID_NOTIFICATION = 32768; 152 | public static final long PERM_ID_PHONEINFO = 64; 153 | public static final long PERM_ID_PROCESS_OUTGOING_CALLS = 1125899906842624L; 154 | public static final long PERM_ID_READCALLLOG = 0x40000000; 155 | public static final long PERM_ID_READCONTACT = 2147483648L; 156 | public static final long PERM_ID_READMMS = 0x20000000; 157 | public static final long PERM_ID_READSMS = 0x10000000; 158 | public static final long PERM_ID_READ_CLIPBOARD = 274877906944L; 159 | public static final long PERM_ID_READ_NOTIFICATION_SMS = 9007199254740992L; 160 | public static final long PERM_ID_REAL_READ_CALENDAR = 4398046511104L; 161 | public static final long PERM_ID_REAL_READ_CALL_LOG = 8796093022208L; 162 | public static final long PERM_ID_REAL_READ_CONTACTS = 2199023255552L; 163 | public static final long PERM_ID_REAL_READ_PHONE_STATE = 17592186044416L; 164 | public static final long PERM_ID_REAL_READ_SMS = 1099511627776L; 165 | public static final long PERM_ID_ROOT = 512; 166 | public static final long PERM_ID_SENDMMS = 524288; 167 | public static final long PERM_ID_SENDSMS = 1; 168 | public static final long PERM_ID_SERVICE_FOREGROUND = 288230376151711744L; 169 | public static final long PERM_ID_SETTINGS = 8192; 170 | public static final long PERM_ID_SHOW_WHEN_LOCKED = 36028797018963968L; 171 | public static final long PERM_ID_SMSDB = 4; 172 | public static final long PERM_ID_SOCIALITY_RESTRICTION = 34359738368L; 173 | public static final long PERM_ID_SYSTEMALERT = 0x02000000; 174 | public static final long PERM_ID_UDEVICEID = 576460752303423488L; 175 | public static final long PERM_ID_USE_SIP = 562949953421312L; 176 | public static final long PERM_ID_VIDEO_RECORDER = 4096; 177 | public static final long PERM_ID_WAKELOCK = 0x04000000; 178 | public static final long PERM_ID_WIFI_CONNECTIVITY = 2097152; 179 | ``` -------------------------------------------------------------------------------- /mpv.md: -------------------------------------------------------------------------------- 1 | # mpv 调教日记 2 | 3 | [mpv](https://mpv.io) 是一款自由的媒体播放器。 4 | 5 | 自从某一天打开 potplayer 后发现桌面右下角不知哪来的韩文广告后,我就对它敬而远之了,实际上 potplayer 的恶名也早有耳闻,于是转向了自由开源的 mpv 。然而虽然开源自由,mpv 却并非那么容易上手,需要经过一定的调教,才能改造成自己喜欢的模样。 6 | 7 | ## 配置文件 8 | 9 | 在非 Windows 上,默认配置目录位于 `~/mpv` ,而在 Windows 上则是 `%AppData%` 。一开始以为是 `%UserProfile%` ,这样有点反直觉。 10 | 11 | 这个目录下,`mpv.conf` 包含了播放器的默认配置,实际上就是默认命令行选项,去掉了前面的 `--` 。 12 | 13 | ## 窗口默认大小 14 | 15 | ``` 16 | autofit=1024 17 | ``` 18 | 19 | ## 控制台 20 | 21 | 按反引号(`)即可打开控制台 22 | 23 | ## 字幕加载 24 | 25 | 简单的: 26 | 27 | ``` 28 | # 精确匹配名字 29 | # sub-auto=exact 30 | # 加载同目录下所有字幕 31 | sub-auto=all 32 | ``` 33 | 34 | 但是这样不能加载次字幕(secondary subtitles)。其实 potplayer 也不能,每次都要手动添加。 35 | 36 | 某些字幕组的外挂字幕会分成两部分(次字幕的名字包含 `Annotations`)。 37 | 38 | mpv 的*简陋* UI 可以切换字幕,然而不能选择次字幕。 39 | 40 | 使用 mpv 的脚本功能,勉强实现了这一需求。 41 | 42 | 脚本放在配置目录的 `scripts` 子目录下。 43 | 44 | ```js 45 | mp.add_hook("on_preloaded", 0, function() { 46 | var p = mp.get_property("path"); 47 | mp.msg.info(p); 48 | var file_name = mp.utils.split_path(p)[1]; 49 | var name = file_name.match(/(.*)\.(.*?)/)[1]; 50 | 51 | var n = mp.get_property_number("track-list/count"); 52 | var sub = null, sub2 = null; 53 | for (var i = 0; i < n; i++) { 54 | var track_id = mp.get_property_number("track-list/" + i + "/id"); 55 | var track_type = mp.get_property("track-list/" + i + "/type"); 56 | var track_filename = mp.get_property("track-list/" + i + "/external-filename"); 57 | if (track_type == 'sub') { 58 | // secondary sub contains 'Annotation' 59 | if (track_filename.match(/Annotation/i)) { 60 | if (sub2 == null) { 61 | sub2 = track_id; 62 | } 63 | } else { 64 | // we prefer the one whose name is similar to video file's name 65 | if (sub == null || track_filename.indexOf(name) != -1) { 66 | sub = track_id; 67 | } 68 | } 69 | } 70 | } 71 | mp.msg.info("sub=" + sub); 72 | mp.msg.info("sub2=" + sub2); 73 | if (sub !== null) { 74 | mp.set_property("options/sid", sub.toString()); 75 | } 76 | if (sub2 !== null) { 77 | mp.set_property("options/secondary-sid", sub2.toString()); 78 | } 79 | }) 80 | ``` 81 | 82 | mpv 的事件处理不能感知新文件的添加,因此无法实现拖放字幕后处理的情况,因此需要选项 `sub-auto=all` 在文件打开的时候加载所有字幕,在 `on_preloaded` hook 点处理,使用 `set_property` 选择字幕和次字幕。 83 | -------------------------------------------------------------------------------- /overflow-menu.md: -------------------------------------------------------------------------------- 1 | # androidx 溢出菜单分析 2 | 3 | 溢出菜单按钮:`OverflowMenuButton` 4 | 5 | `appcompat/appcompat/src/main/java/androidx/appcompat/widget/ActionMenuPresenter.java` 6 | 7 | 溢出菜单: 8 | 9 | `appcompat/appcompat/src/main/java/androidx/appcompat/widget/MenuPopupWindow.java` 10 | 11 | ``` 12 | android:background="?selectableItemBackground" 13 | android:clickable="true" 14 | android:focusable="true" 15 | ``` 16 | -------------------------------------------------------------------------------- /parse-dumpsys-with-protobuf.md: -------------------------------------------------------------------------------- 1 | # 处理 dumpsys 的 protobuf dump 2 | 3 | 不知道从哪个版本的 Android 开始,dumpsys 支持 protobuf 输出了,这比起解析 dumpsys 的一大串字符串自然方便不少,也更加具有兼容性、稳定性,便于制作各种系统分析工具。 4 | 5 | 不过实际使用起来……着实费了一番功夫。 6 | 7 | ## 准备 proto 文件 8 | 9 | 10 | 获取这些 proto 自然是从 AOSP 源码仓库,大部分的 proto 可以在下面三个目录找到: 11 | 12 | ``` 13 | platform/frameworks/proto_logging 14 | platform/frameworks/base/core/proto 15 | platform/frameworks/base/proto 16 | ``` 17 | 18 | SurfaceFlinger : 19 | 20 | ``` 21 | platform/frameworks/native/services/surfaceflinger/layerproto 22 | ``` 23 | 24 | 由于这些 proto 并没有单独的 git 模块,因此 clone 下来很不方便,所以用了朋友写的爬虫来下载。 25 | 26 | ``` 27 | platform/frameworks/proto_logging/+/refs/heads/master 28 | platform/frameworks/base/+/refs/heads/master/core/proto 29 | platform/frameworks/base/+/refs/heads/master/proto 30 | ``` 31 | 32 | 实际上上面三个目录也并非包含了所有的依赖,而且并不是所有的 proto 都用得上,因此我用脚本处理了需要的依赖。每个支持 proto dump 的服务一般会有一个主 proto 文件,只要找到这个文件所需的依赖即可(参见最后的脚本)。 33 | 34 | ## 获取并处理 proto dump 35 | 36 | 一般来说只要在 dumpsys 参数加上 `--proto` 即可得到 protobuf 格式的 dump 。 37 | 38 | 服务对应的 proto 一般在 `frameworks/base/core/proto/android/server/xxxServiceDumpProto.proto` (可以看一看 `base/core/proto/README.md`) 39 | 40 | ```sh 41 | # wm 42 | dumpsys window --proto > s.buf 43 | # 没有数据类型 44 | cat s.buf | protoc --decode_raw 45 | # 有数据类型 46 | cat s.buf|protoc --decode=com.android.server.wm.WindowManagerServiceDumpProto --proto_path=protos protos/frameworks/base/core/proto/android/server/windowmanagerservice.proto > dump.txt 47 | # SF 48 | dumpsys SurfaceFlinger --proto 49 | cat d.proto.txt | protoc --decode=android.surfaceflinger.LayersProto --proto_path=proto proto/android/surfaceflinger/layers.proto > decoded.txt 50 | ``` 51 | 52 | ## 向 Android Studio 项目引入 protobuf 53 | 54 | 最终目的当然是让我们的程序能够处理这些 proto 数据。在这里我们使用 google 的 [protobuf gradle 插件](https://github.com/google/protobuf-gradle-plugin) 55 | 56 | build.gradle.kts: 57 | 58 | > 配置 protobuf 的代码参考了 [哔哩漫游](https://github.com/yujincheng08/BiliRoaming) 59 | 60 | ```kotlin 61 | // 项目级 62 | buildscript { 63 | dependencies { 64 | // ... 65 | classpath("com.google.protobuf:protobuf-gradle-plugin:0.8.19") 66 | } 67 | } 68 | 69 | // 模块级 70 | plugins { 71 | // ... 72 | id("com.google.protobuf") 73 | } 74 | 75 | // https://github.com/google/protobuf-gradle-plugin/issues/540#issuecomment-1001053066 76 | fun com.android.build.api.dsl.AndroidSourceSet.proto(action: SourceDirectorySet.() -> Unit) { 77 | (this as? ExtensionAware) 78 | ?.extensions 79 | ?.getByName("proto") 80 | ?.let { it as? SourceDirectorySet } 81 | ?.apply(action) 82 | } 83 | 84 | android { 85 | // ... 86 | 87 | sourceSets { 88 | named("main") { 89 | proto { 90 | srcDir("src/main/proto") 91 | include("**/*.proto") 92 | } 93 | } 94 | } 95 | } 96 | 97 | dependencies { 98 | // ... 99 | implementation("com.google.protobuf:protobuf-kotlin-lite:3.20.1") 100 | compileOnly("com.google.protobuf:protoc:3.20.1") 101 | } 102 | 103 | protobuf { 104 | protoc { 105 | artifact = "com.google.protobuf:protoc:3.19.4" 106 | } 107 | 108 | generatedFilesBaseDir = "$projectDir/src/generated" 109 | 110 | generateProtoTasks { 111 | all().forEach { task -> 112 | task.builtins { 113 | id("java") { 114 | option("lite") 115 | } 116 | id("kotlin") { 117 | option("lite") 118 | } 119 | } 120 | } 121 | } 122 | } 123 | ``` 124 | 125 | 配置好后,将我们的 proto 文件放在 `src/main/proto` 下,直接 Run generate source gradle tasks 即可在 `src/generated` 下生成 proto 的对应代码。(此处目录为相对模块的目录) 126 | 127 | ## 踩坑 128 | 129 | ### 预构建的 protoc 不包含 include 130 | 131 | 一些 proto 文件 import 了 `include/google/protobuf/descriptor.proto` ,然而通过 gradle 自动下载的 protoc 竟然没有这些 include 。好在之前下载了压缩包版的 protoc 包含了这些 include ,暂且放在 `src/main/proto` 下了。 132 | 133 | ### proto 类与 android 类重名 134 | 135 | 处理好后能够生成代码了,但是和项目一起编译就报错,发现一些 proto 类的类名竟然和 android SDK 的类重名,如 `android.content.pm.PackageInfo` 。 136 | 137 | 观察这些类,发现基本是个空壳,都是对应 proto 文件名生成的类,本来应该把 proto 中的数据类放在这个类里的,但是由于存在 `option java_multiple_files = true;` 这个选项,这些类被分开放到单独的文件去了,但是剩下的这个空壳似乎没法清除掉。 138 | 139 | 按照官方的说法,这些 proto 不应该改名,但是这样在编译时就会和 SDK 冲突,不知道 AOSP 是如何编译的(似乎没有用标准的 protoc ,而是定制的编译器)。此外,运行时的 frameworks 也包含了一些 proto 类,就算过了编译这一关,也会在运行时链接发生冲突。总之,想在 android 上使用 proto ,必须修改类名。 140 | 141 | 经过一番考察,决定用脚本修补这些 proto 文件,用 `option java_package=` 指定新的包名,加上一个后缀 `.proto_` ,具体做法可以参见最后附的脚本。 142 | 143 | ## 优雅地 dumpsys 144 | 145 | 下面给一个简单的例子,不需要创建进程 `dumpsys` 即可获取 proto ,对应 `dumpsys activity --proto activities` : 146 | 147 | ```kotlin 148 | val iam = ServiceManager.getService("activity") 149 | // 创建管道,write 端发送给系统服务,read 端读取 150 | val pipes = ParcelFileDescriptor.createPipe() 151 | val r = pipes[0]!! 152 | val w = pipes[1] 153 | // dump 似乎是阻塞的,考虑使用线程,不过正常情况一般会很快返回 154 | iam.dump(w.fileDescriptor, arrayOf("--proto", "activities")) 155 | w.close() // 必须关闭,否则由于 write 端未关闭,read 端仍然阻塞 156 | val ins = AssetFileDescriptor(r, 0L, AssetFileDescriptor.UNKNOWN_LENGTH) 157 | .createInputStream() 158 | val proto = ActivityManagerServiceDumpActivitiesProto.parseFrom(ins) 159 | // 这个结构比较复杂,就不进一步处理了,直接 toString 可以输出结构化的数据 160 | println(proto) 161 | ``` 162 | 163 | ## 处理脚本 164 | 165 | 下面给出用于寻找依赖和修补 proto 的脚本: 166 | 167 | ```py 168 | import os 169 | import re 170 | 171 | os.chdir("protos") 172 | 173 | target = r"frameworks\base\core\proto\android\server\activitymanagerservice.proto".replace("\\", "/") 174 | q = set() 175 | deps = set() 176 | 177 | def read_import(path): 178 | if not os.path.exists(path): 179 | print(f"warn: import {path} does not exists!") 180 | return 181 | print(f"reading import for {path}") 182 | with open(path, 'r', encoding='utf-8') as f: 183 | ls = f.readlines() 184 | for l in ls: 185 | r = re.match(r'import "(.*)";', l) 186 | if r is not None: 187 | q.add(r.group(1)) 188 | 189 | q.add(target) 190 | while len(q) > 0: 191 | c = q.pop() 192 | read_import(c) 193 | deps.add(c) 194 | 195 | for d in deps: 196 | newPath = os.path.join('..', 'ams', *os.path.split(d)) 197 | os.makedirs(os.path.dirname(newPath), exist_ok=True) 198 | with open(d, 'r', encoding='utf-8') as f: 199 | ls = f.readlines() 200 | pn, pi = None, 0 201 | jp = False 202 | for i, l in enumerate(ls): 203 | r = re.match(r'option java_package\s*=\s*"(.*)";', l) 204 | if r is not None: 205 | jp = True 206 | pn, pi = r.group(1), i 207 | break 208 | r = re.match(r'package (.*);', l) 209 | if r is not None: 210 | pn, pi = r.group(1), i 211 | if pn.startswith('com.android') or pn.startswith("android"): 212 | newLine = f'option java_package = "{pn}.protos_";\n' 213 | if jp: 214 | ls[i] = newLine 215 | else: 216 | ls.insert(pi + 1, newLine) 217 | with open(newPath, 'w', encoding='utf-8') as ff: 218 | ff.writelines(ls) 219 | ``` 220 | 221 | ## 参考 222 | 223 | [[译]Protobuf 语法指南](https://colobu.com/2015/01/07/Protobuf-language-guide/#%E9%80%89%E9%A1%B9%EF%BC%88Options%EF%BC%89) 224 | [CookBook/Protobuf基础教程.md at master · Byron4j/CookBook](https://github.com/Byron4j/CookBook/blob/master/Protobuf/ProtobufTutorial/Protobuf%E5%9F%BA%E7%A1%80%E6%95%99%E7%A8%8B.md) 225 | [Language Guide (proto3)  |  Protocol Buffers  |  Google Developers](https://developers.google.com/protocol-buffers/docs/proto3) 226 | [Java Generated Code  |  Protocol Buffers  |  Google Developers](https://developers.google.com/protocol-buffers/docs/reference/java-generated#package) 227 | [protocol buffers - Do I need to download all .proto files in Android framework source codes in order to parse proto message of adb dumpsys? - Stack Overflow](https://stackoverflow.com/questions/68608099/do-i-need-to-download-all-proto-files-in-android-framework-source-codes-in-orde) 228 | 229 | ## 思考 230 | 231 | 1. 应该寻找一些使用了 protobuf 处理 dumpsys 的项目作为参考。 232 | 2. 有没有可能利用 frameworks 中存在的 proto 类?(不过里面甚至没有反序列化的方法) 233 | -------------------------------------------------------------------------------- /perspective-transform.md: -------------------------------------------------------------------------------- 1 | # 投影变换 2 | 3 | ## 空间中的点中心投影到平面 4 | 5 | 考虑空间坐标系的原点 $O$ 和不过原点的一个屏,这个屏上也有一个平面坐标系,基向量为 $\vec{i},\vec{j}$ ,原点的坐标为 $\vec{o}$ 。它们都是这个空间坐标系下的三维向量。 6 | 7 | 连接空间任意点 $\vec{p}=(x,y,z)$ 与原点,和平面有一个交点,那么这个交点就是该空间点关于原点 $O$ 在屏上的投影点。假设投影点在屏上坐标系的坐标为 $(x_1,y_1)$ ,可以写出如下关系: 8 | 9 | $$ 10 | k(\vec{o}+x_1 \vec{i}+y_1 \vec{j})=\vec{p} 11 | $$ 12 | 13 | 其中 $k$ 为某个实数。 14 | 15 | 这个式子很好理解:空间中的多个点可以对应同一个投影点(只要位于同一条投影线,即经过 $O$ 的直线上),而投影点的坐标可由 $x_1, y_1$ 表示: $\vec{o}+x_1 \vec{i}+y_1 \vec{j}$ 。显然对某个 $\vec{p}$ (如果不与屏平行),存在唯一的 $x_1, y_1$ 使得投影点和 $O$ 的连线与其共线,只相差比例 $k$ 。 16 | 17 | 我们将上式改写成矩阵的形式: 18 | 19 | $$ 20 | \left( 21 | \begin{matrix} 22 | \vec{i} & \vec{j} & \vec{o} 23 | \end{matrix} 24 | \right) 25 | \cdot k \cdot 26 | \left( 27 | \begin{matrix} 28 | x_1 \\ 29 | y_1 \\ 30 | 1 31 | \end{matrix} 32 | \right) = 33 | \left( 34 | \begin{matrix} 35 | x \\ 36 | y \\ 37 | z 38 | \end{matrix} 39 | \right) 40 | $$ 41 | 42 | 可以发现,中间的坐标就是齐次坐标的形式。 43 | 44 | 注意到 $\vec{i},\vec{j},\vec{o}$ 显然不会共面,因此构成了一个可逆矩阵。 45 | 46 | 记: 47 | 48 | $$ 49 | M = 50 | \left( 51 | \begin{matrix} 52 | \vec{i} & \vec{j} & \vec{o} 53 | \end{matrix} 54 | \right) 55 | $$ 56 | 57 | 则已知 $\vec{p}$ 的情况下,我们就很容易求出投影点在屏上坐标系的坐标了: 58 | 59 | $$ 60 | \vec{p'} = 61 | \left( 62 | \begin{matrix} 63 | x_1k \\ 64 | y_1k \\ 65 | k 66 | \end{matrix} 67 | \right) = M^{-1}\vec{p} 68 | $$ 69 | 70 | 求出右边后,将所得向量同除以第三个分量,前两个分量就是 $x_1, y_1$ 。当然,我们也可以用中间的三维的**齐次坐标**来表示这个二维的向量。在这种表示下,由于 $k$ 不固定,因此一个二维的点有无数种表示方法,它们之间只相差一个倍数。 71 | 72 | ## 两个投影面之间的坐标变换 73 | 74 | 利用上面的公式,我们就可以将三维空间中的点到某一个平面的坐标系上的齐次坐标映射用一个可逆矩阵表示。而一个点(非投影中心)投影到两个平面的坐标系上,这两个坐标系之间的转换关系同样是一个可逆矩阵。 75 | 76 | $$ 77 | k_1M_1\vec{p_1'}=k_2M_2\vec{p_2'}=\vec{p} 78 | $$ 79 | 80 | -------------------------------------------------------------------------------- /proc-start-monitor.md: -------------------------------------------------------------------------------- 1 | # 无侵入式的进程启动监视 2 | 3 | 监视 Android 进程的启动,如果不侵入系统服务,往往需要借助 log ,SR 在非增强模式就是通过 logcat 监视进程启动的。但假如直接解析 logcat 程序的输出,对于不同系统版本,log 的格式也不一致,似乎很难做到兼容。 4 | 5 | 就在今天(2022.07.20),LSPosed 开发者之一 vvb2060 在其个人频道[发布](https://t.me/vvb2060Channel/743)了一个 github [gist](https://gist.github.com/vvb2060/a3d40084cd9273b65a15f8a351b4eb0e) ,提供了一种监视进程启动的实现,该方法不需要注入系统,也不需要创建 logcat 进程,完全使用 liblog 的 API 。 6 | 7 | ## 源码 8 | 9 | ```cpp 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | using namespace std; 17 | 18 | extern "C" { 19 | 20 | struct logger_entry { 21 | uint16_t len; /* length of the payload */ 22 | uint16_t hdr_size; /* sizeof(struct logger_entry) */ 23 | int32_t pid; /* generating process's pid */ 24 | uint32_t tid; /* generating process's tid */ 25 | uint32_t sec; /* seconds since Epoch */ 26 | uint32_t nsec; /* nanoseconds */ 27 | uint32_t lid; /* log id of the payload, bottom 4 bits currently */ 28 | uint32_t uid; /* generating process's uid */ 29 | }; 30 | 31 | #define LOGGER_ENTRY_MAX_LEN (5 * 1024) 32 | struct log_msg { 33 | union [[gnu::aligned(4)]] { 34 | unsigned char buf[LOGGER_ENTRY_MAX_LEN + 1]; 35 | struct logger_entry entry; 36 | }; 37 | }; 38 | 39 | [[gnu::weak]] struct logger_list *android_logger_list_alloc(int mode, unsigned int tail, pid_t pid); 40 | [[gnu::weak]] void android_logger_list_free(struct logger_list *list); 41 | [[gnu::weak]] int android_logger_list_read(struct logger_list *list, struct log_msg *log_msg); 42 | [[gnu::weak]] struct logger *android_logger_open(struct logger_list *list, log_id_t id); 43 | 44 | typedef struct [[gnu::packed]] { 45 | int32_t tag; // Little Endian Order 46 | } android_event_header_t; 47 | 48 | typedef struct [[gnu::packed]] { 49 | int8_t type; // EVENT_TYPE_INT 50 | int32_t data; // Little Endian Order 51 | } android_event_int_t; 52 | 53 | typedef struct [[gnu::packed]] { 54 | int8_t type; // EVENT_TYPE_STRING; 55 | int32_t length; // Little Endian Order 56 | char data[]; 57 | } android_event_string_t; 58 | 59 | typedef struct [[gnu::packed]] { 60 | int8_t type; // EVENT_TYPE_LIST 61 | int8_t element_count; 62 | } android_event_list_t; 63 | 64 | // 30014 am_proc_start (User|1|5),(PID|1|5),(UID|1|5),(Process Name|3),(Type|3),(Component|3) 65 | typedef struct [[gnu::packed]] { 66 | android_event_header_t tag; 67 | android_event_list_t list; 68 | android_event_int_t user; 69 | android_event_int_t pid; 70 | android_event_int_t uid; 71 | android_event_string_t process_name; 72 | // android_event_string_t type; 73 | // android_event_string_t component; 74 | } android_event_am_proc_start; 75 | 76 | } 77 | 78 | void ProcessBuffer(struct logger_entry *buf) { 79 | auto *eventData = reinterpret_cast(buf) + buf->hdr_size; 80 | auto *event_header = reinterpret_cast(eventData); 81 | if (event_header->tag != 30014) return; 82 | auto *am_proc_start = reinterpret_cast(eventData); 83 | printf("user=%" PRId32" pid=%" PRId32" uid=%" PRId32" proc=%.*s\n", 84 | am_proc_start->user.data, am_proc_start->pid.data, am_proc_start->uid.data, 85 | am_proc_start->process_name.length, am_proc_start->process_name.data); 86 | } 87 | 88 | [[noreturn]] void Run() { 89 | while (true) { 90 | bool first; 91 | __system_property_set("persist.log.tag", ""); 92 | 93 | unique_ptr logger_list{ 94 | android_logger_list_alloc(0, 1, 0), &android_logger_list_free}; 95 | auto *logger = android_logger_open(logger_list.get(), LOG_ID_EVENTS); 96 | if (logger != nullptr) [[likely]] { 97 | first = true; 98 | } else { 99 | continue; 100 | } 101 | 102 | struct log_msg msg{}; 103 | while (true) { 104 | if (android_logger_list_read(logger_list.get(), &msg) <= 0) [[unlikely]] { 105 | break; 106 | } 107 | if (first) [[unlikely]] { 108 | first = false; 109 | continue; 110 | } 111 | 112 | ProcessBuffer(&msg.entry); 113 | } 114 | 115 | sleep(1); 116 | } 117 | } 118 | 119 | int main(int argc, char *argv[]) { 120 | Run(); 121 | } 122 | ``` 123 | 124 | ## 使用 125 | 126 | 我直接在 Termux 上用 g++ 编译了,注意要链接系统的 log 库,否则无法找到符号。 127 | 128 | ```sh 129 | wget https://gist.github.com/vvb2060/a3d40084cd9273b65a15f8a351b4eb0e/raw/3e712a7e0d9e0bf5a5b87564aa7aa1b42bf70994/am_ 130 | proc_start.cpp 131 | g++ am_proc_start.cpp -o amp -L/system/lib64/ -llog 132 | ``` 133 | 134 | 在 root 下直接启动即可监视进程(当然 shell 下也可以): 135 | 136 | ![](res/images/20220720_01.png) 137 | 138 | 经过测试,这个程序在我的 Android 9 和 Android 11 上都可以运行。 139 | 140 | ## 分析 141 | 142 | ### logtags 143 | 144 | cs 搜索 `am_proc_start` 找到这么一个 [logtags 文件](https://android.googlesource.com/platform/frameworks/base/+/407d531735419cb7139dde78e65102ae949618f4/services/core/java/com/android/server/am/EventLogTags.logtags): 145 | 146 | `frameworks/base/services/core/java/com/android/server/am/EventLogTags.logtags` 147 | 148 | ``` 149 | # See system/core/logcat/event.logtags for a description of the format of this file. 150 | 151 | # ... 152 | 153 | # Do not change these names without updating the checkin_events setting in 154 | # google3/googledata/wireless/android/provisioning/gservices.config !! 155 | # 156 | # Application Not Responding 157 | 30008 am_anr (User|1|5),(pid|1|5),(Package Name|3),(Flags|1|5),(reason|3) 158 | 159 | # Application process bound to work 160 | 30010 am_proc_bound (User|1|5),(PID|1|5),(Process Name|3) 161 | # Application process died 162 | 30011 am_proc_died (User|1|5),(PID|1|5),(Process Name|3),(OomAdj|1|5),(ProcState|1|5) 163 | 164 | # Application process has been started 165 | 30014 am_proc_start (User|1|5),(PID|1|5),(UID|1|5),(Process Name|3),(Type|3),(Component|3) 166 | # An application process has been marked as bad 167 | 30015 am_proc_bad (User|1|5),(UID|1|5),(Process Name|3) 168 | # An application process that was bad is now marked as good 169 | 30016 am_proc_good (User|1|5),(UID|1|5),(Process Name|3) 170 | # Reporting to applications that memory is low 171 | 30017 am_low_memory (Num Processes|1|1) 172 | 173 | # Kill a process to reclaim memory. 174 | 30023 am_kill (User|1|5),(PID|1|5),(Process Name|3),(OomAdj|1|5),(Reason|3) 175 | ``` 176 | 177 | 每一行都定义了一些日志类型的 id 和对应的结构,不过不知道是什么意思。 178 | 179 | 文件顶上的注释的路径现在找不到了,不过在下面这个[同名的文件](https://android.googlesource.com/platform/system/logging/+/234f8d965769a4cf211b002b1bda19baaa9062f3/liblog/event.logtags)解释了这些结构: 180 | 181 | `system/logging/liblog/event.logtags` 182 | 183 | ``` 184 | # The entries in this file map a sparse set of log tag numbers to tag names. 185 | # This is installed on the device, in /system/etc, and parsed by logcat. 186 | # 187 | # Tag numbers are decimal integers, from 0 to 2^31. (Let's leave the 188 | # negative values alone for now.) 189 | # 190 | # Tag names are one or more ASCII letters and numbers or underscores, i.e. 191 | # "[A-Z][a-z][0-9]_". Do not include spaces or punctuation (the former 192 | # impacts log readability, the latter makes regex searches more annoying). 193 | # 194 | # Tag numbers and names are separated by whitespace. Blank lines and lines 195 | # starting with '#' are ignored. 196 | # 197 | # Optionally, after the tag names can be put a description for the value(s) 198 | # of the tag. Description are in the format 199 | # (|data type[|data unit]) 200 | # Multiple values are separated by commas. 201 | # 202 | # The data type is a number from the following values: 203 | # 1: int 204 | # 2: long 205 | # 3: string 206 | # 4: list 207 | # 208 | # The data unit is a number taken from the following list: 209 | # 1: Number of objects 210 | # 2: Number of bytes 211 | # 3: Number of milliseconds 212 | # 4: Number of allocations 213 | # 5: Id 214 | # 6: Percent 215 | # s: Number of seconds (monotonic time) 216 | # Default value for data of type int/long is 2 (bytes). 217 | ``` 218 | 219 | 看上去对每个 `()` 里面的内容,第一个参数是名字,第二个参数是类型,第三个参数没看出来什么用。我们只要第一个参数和第二个参数就能写出对应的结构体: 220 | 221 | am_proc_start 的描述: 222 | 223 | ``` 224 | 30014 am_proc_start (User|1|5),(PID|1|5),(UID|1|5),(Process Name|3),(Type|3),(Component|3) 225 | ``` 226 | 227 | 对应的结构体: 228 | 229 | ```cpp 230 | typedef struct [[gnu::packed]] { 231 | android_event_header_t tag; 232 | android_event_list_t list; 233 | android_event_int_t user; 234 | android_event_int_t pid; 235 | android_event_int_t uid; 236 | android_event_string_t process_name; 237 | // android_event_string_t type; 238 | // android_event_string_t component; 239 | } android_event_am_proc_start; 240 | ``` 241 | 242 | 实际上这些 logtags 最终都会放到 `/system/bin/event-log-tags` 。 243 | 244 | ### 日志缓冲区 245 | 246 | Android 的日志系统实际上分为多个缓冲区,上面这些 event 日志都会被写入一个缓冲区 `events` (系统服务中的 `EventLog` 就是负责写入这里的),在 logcat 的帮助中有: 247 | 248 | ``` 249 | -b, --buffer= Request alternate ring buffer(s): 250 | main system radio events crash default all 251 | Additionally, 'kernel' for userdebug and eng builds, and 252 | 'security' for Device Owner installations. 253 | Multiple -b parameters or comma separated list of buffers are 254 | allowed. Buffers are interleaved. 255 | Default -b main,system,crash,kernel. 256 | ``` 257 | 258 | 由于默认不包含 `events` ,因此平时看不到这些日志。 259 | 260 | logcat 也可以查看 events buffer 中的内容: 261 | 262 | ```sh 263 | logcat -b events 264 | # 查看特定 tags 265 | logcat -b events -s am_proc_start:* am_proc_died:* 266 | ``` 267 | 268 | ![](res/images/20220720_02.png) 269 | 270 | ### 使用 liblog 的非公开 API 271 | 272 | 系统提供的 api 并不包含读取日志,[头文件](https://android.googlesource.com/platform/system/logging/+/234f8d965769a4cf211b002b1bda19baaa9062f3/liblog/include/android/log.h)路径如下: 273 | 274 | `system/logging/liblog/include/android/log.h` 275 | 276 | 因此上面的程序利用了 weak 符号声明这些不在头文件中的函数和结构体,运行时链接到 liblog 就可以使用。 277 | 278 | ```cpp 279 | [[gnu::weak]] struct logger_list *android_logger_list_alloc(int mode, unsigned int tail, pid_t pid); 280 | [[gnu::weak]] void android_logger_list_free(struct logger_list *list); 281 | [[gnu::weak]] int android_logger_list_read(struct logger_list *list, struct log_msg *log_msg); 282 | [[gnu::weak]] struct logger *android_logger_open(struct logger_list *list, log_id_t id); 283 | ``` 284 | 285 | 这些函数和结构体 `logger_entry`, `log_msg` 的声明在[这里](https://android.googlesource.com/platform/system/logging/+/234f8d965769a4cf211b002b1bda19baaa9062f3/liblog/include/log/log_read.h):`system/logging/liblog/include/log/log_read.h` 286 | 287 | `android_event_` 开头的结构体声明在[这里](https://android.googlesource.com/platform/system/logging/+/234f8d965769a4cf211b002b1bda19baaa9062f3/liblog/include/private/android_logger.h):`system/logging/liblog/include/private/android_logger.h` -------------------------------------------------------------------------------- /raspberry-pi.md: -------------------------------------------------------------------------------- 1 | # 树莓派 2 | 3 | 从老朋友那以只有邮费的价格淘到了一台 2.5 手的树莓派,至于为何是 2.5 手,因为第二任主人自称从第一任主人那里买来后几乎没有动过。 4 | 5 | 现在轮到我做它的第 2.5 任主人了,不知道它将来会经历怎么样的命运…… 6 | 7 | ## 第一课:开机和查看配置 8 | 9 | Type-C 电源 10 | 11 | ssh 默认用户名和密码:`pi@raspberry` 12 | 13 | …… 14 | 15 | ## 第二课:关机 16 | 17 | [power supply - How do I turn off my Raspberry Pi? - Raspberry Pi Stack Exchange](https://raspberrypi.stackexchange.com/questions/381/how-do-i-turn-off-my-raspberry-pi) 18 | 19 | > 1. Execute the command: 20 | > `sudo shutdown -h now` 21 | > 2. Wait until the LEDs stop blinking on the Raspberry Pi. 22 | > 3. Wait an additional five seconds for good measure (optional). 23 | > 4. Switch off the powerstrip that the Raspberry Pi power supply is plugged into. 24 | 25 | 简单来说,直接拔电会对 SD 卡有影响。正确的关机方法:执行 `sudo shutdown -h now` ,然后等待闪烁的灯熄灭(红灯和黄灯都会熄灭),此时就可以拔电了。 26 | 27 | ## 2022.12.2 28 | 29 | ### 刷入新系统 30 | 31 | 考虑到上一任留下的系统不知道会有什么暗坑,以及它还是 32 位的,也不是 latest 的,决定更新系统。 32 | 33 | 查了一些资料,CPU `BCM2711` 是 armv8 ,支持 64 位。 34 | 35 | [Raspberry Pi 4 Model B specifications – Raspberry Pi](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/specifications/) 36 | 37 | 考察了一些资料,可供我选择的系统有官方的 Raspberry PI OS 、Ubuntu MATE 、Ubuntu Server 等。 38 | 39 | [用于各种用途的最佳树莓派操作系统 | Linux 中国 - 知乎](https://zhuanlan.zhihu.com/p/141068779) 40 | 41 | 考虑到这玩意目前只是用来跑一些脚本,甚至不需要图形界面,于是选择 Raspberry PI OS Lite 。 42 | 43 | 去官方下载 64 位版本,基于 Debian 11 和 Linux 5.15 内核,发布日期 September 22nd 2022。 44 | 45 | `2022-09-22-raspios-bullseye-arm64-lite.img` 46 | 47 | [Operating system images – Raspberry Pi](https://www.raspberrypi.com/software/operating-systems/#raspberry-pi-os-32-bit) 48 | 49 | [Raspberry Pi OS – Raspberry Pi](https://www.raspberrypi.com/software/) 50 | 51 | 下载好后用官方的工具刷入 SD 卡即可。这个工具甚至还能自动配置 ssh 公钥登录(看上去是从 `~/.ssh/id_rsa.pub` 提取的)和与 PC 相同的 WIFI 配置(它用什么黑科技看到的密码……),我最后选择用 ssh 公钥登录、网线连接。 52 | 53 | 登录用户名默认是 `pi` ,可以用 `sudo su` 访问 root (无需密码,可能因为我没设置?)。 54 | 55 | ### 配置用户 56 | 57 | 在用了一段时间,安装了一些东西之后,我才注意到每次 ssh 都有下面的提示: 58 | 59 | ``` 60 | Please note that SSH may not work until a valid user has been set up. 61 | 62 | See http://rptl.io/newuser for details. 63 | ``` 64 | 65 | [An update to Raspberry Pi OS Bullseye - Raspberry Pi](https://www.raspberrypi.com/news/raspberry-pi-bullseye-update-april-2022/) 66 | 67 | 根据文章所说,现在的 rasp os 引入了新的安全措施,要求首次登录必须设置新用户名和密码,对于 headless ,应该用刷写工具创建新用户。然而我只配置了 ssh 登录,并没有配置用户名和密码。 68 | 69 | 文章还指出对于已经安装的用户,可以用 `rename-user` 命令来重命名。但是这个命令需要重启,并且启动后似乎不能远程操作。 70 | 71 | 看了一下那个脚本 `/usr/bin/rename-user` ,会创建一个向导 (wizard) 用户,下次启动会自动启动用户配置向导。 72 | 73 | 另外里面也有写入 sshd banner 的逻辑(不是很懂为什么写在这里): 74 | 75 | ```sh 76 | cat <<- EOF > /etc/ssh/sshd_config.d/rename_user.conf 77 | Banner /usr/share/userconf-pi/sshd_banner 78 | EOF 79 | ``` 80 | 81 | 看起来现在这个状况要配置新用户挺麻烦的,不知道有什么未知后果,反正这个 `pi` 用户也不是不能用。 82 | 83 | 至于那个 banner ,干脆修改 `/etc/ssh/sshd_config.d/rename_user.conf` 把它注释掉,然后 `systemctl --quiet reload ssh` 84 | 85 | ## 配置时区 86 | 87 | 挂了一天脚本,发现 crontab 没有正确执行,原来是时区没设置 88 | 89 | [TimeZoneChanges - Debian Wiki](https://wiki.debian.org/TimeZoneChanges#Check_Configured_Timezone) 90 | 91 | debian 使用 `dpkg-reconfigure tzdata` 配置时区,选择「chongqing」(不知道为什么没有 Beijing ,但是有 Shanghai, Chongqing, Hong Kong) 92 | -------------------------------------------------------------------------------- /res/images/20220717_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_01.png -------------------------------------------------------------------------------- /res/images/20220717_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_02.png -------------------------------------------------------------------------------- /res/images/20220717_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_03.png -------------------------------------------------------------------------------- /res/images/20220717_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_04.png -------------------------------------------------------------------------------- /res/images/20220717_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_05.png -------------------------------------------------------------------------------- /res/images/20220717_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_06.png -------------------------------------------------------------------------------- /res/images/20220717_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_07.png -------------------------------------------------------------------------------- /res/images/20220717_08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_08.png -------------------------------------------------------------------------------- /res/images/20220717_09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_09.png -------------------------------------------------------------------------------- /res/images/20220717_10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_10.png -------------------------------------------------------------------------------- /res/images/20220717_11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_11.png -------------------------------------------------------------------------------- /res/images/20220717_12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220717_12.png -------------------------------------------------------------------------------- /res/images/20220718_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220718_01.png -------------------------------------------------------------------------------- /res/images/20220718_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220718_02.png -------------------------------------------------------------------------------- /res/images/20220718_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220718_03.png -------------------------------------------------------------------------------- /res/images/20220718_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220718_04.png -------------------------------------------------------------------------------- /res/images/20220718_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220718_05.png -------------------------------------------------------------------------------- /res/images/20220719_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220719_01.png -------------------------------------------------------------------------------- /res/images/20220719_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220719_02.png -------------------------------------------------------------------------------- /res/images/20220719_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220719_03.png -------------------------------------------------------------------------------- /res/images/20220719_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220719_04.png -------------------------------------------------------------------------------- /res/images/20220720_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220720_01.png -------------------------------------------------------------------------------- /res/images/20220720_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220720_02.png -------------------------------------------------------------------------------- /res/images/20220907_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220907_01.png -------------------------------------------------------------------------------- /res/images/20220907_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220907_02.png -------------------------------------------------------------------------------- /res/images/20220907_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220907_03.png -------------------------------------------------------------------------------- /res/images/20220907_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220907_04.png -------------------------------------------------------------------------------- /res/images/20220917_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220917_01.png -------------------------------------------------------------------------------- /res/images/20220917_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220917_02.png -------------------------------------------------------------------------------- /res/images/20220917_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220917_03.png -------------------------------------------------------------------------------- /res/images/20220917_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220917_04.png -------------------------------------------------------------------------------- /res/images/20220917_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220917_05.png -------------------------------------------------------------------------------- /res/images/20220917_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220917_06.png -------------------------------------------------------------------------------- /res/images/20220917_07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220917_07.png -------------------------------------------------------------------------------- /res/images/20220921_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220921_01.png -------------------------------------------------------------------------------- /res/images/20220921_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220921_02.png -------------------------------------------------------------------------------- /res/images/20220921_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220921_03.png -------------------------------------------------------------------------------- /res/images/20220921_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220921_04.png -------------------------------------------------------------------------------- /res/images/20220922_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220922_01.png -------------------------------------------------------------------------------- /res/images/20220923_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220923_01.png -------------------------------------------------------------------------------- /res/images/20220923_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220923_02.png -------------------------------------------------------------------------------- /res/images/20220924_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20220924_01.png -------------------------------------------------------------------------------- /res/images/20221001_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221001_01.png -------------------------------------------------------------------------------- /res/images/20221001_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221001_02.png -------------------------------------------------------------------------------- /res/images/20221003_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221003_01.png -------------------------------------------------------------------------------- /res/images/20221003_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221003_02.png -------------------------------------------------------------------------------- /res/images/20221003_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221003_03.png -------------------------------------------------------------------------------- /res/images/20221003_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221003_04.png -------------------------------------------------------------------------------- /res/images/20221004_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221004_01.png -------------------------------------------------------------------------------- /res/images/20221006_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221006_01.png -------------------------------------------------------------------------------- /res/images/20221006_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221006_02.png -------------------------------------------------------------------------------- /res/images/20221006_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221006_03.png -------------------------------------------------------------------------------- /res/images/20221006_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221006_04.png -------------------------------------------------------------------------------- /res/images/20221006_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221006_05.png -------------------------------------------------------------------------------- /res/images/20221006_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221006_06.png -------------------------------------------------------------------------------- /res/images/20221020_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221020_01.png -------------------------------------------------------------------------------- /res/images/20221020_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221020_02.png -------------------------------------------------------------------------------- /res/images/20221020_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221020_03.png -------------------------------------------------------------------------------- /res/images/20221020_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221020_04.png -------------------------------------------------------------------------------- /res/images/20221020_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221020_05.png -------------------------------------------------------------------------------- /res/images/20221020_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221020_06.png -------------------------------------------------------------------------------- /res/images/20221022_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221022_01.png -------------------------------------------------------------------------------- /res/images/20221022_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221022_02.png -------------------------------------------------------------------------------- /res/images/20221022_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221022_03.png -------------------------------------------------------------------------------- /res/images/20221022_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221022_04.png -------------------------------------------------------------------------------- /res/images/20221022_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221022_05.png -------------------------------------------------------------------------------- /res/images/20221023_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221023_01.png -------------------------------------------------------------------------------- /res/images/20221023_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221023_02.png -------------------------------------------------------------------------------- /res/images/20221023_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221023_03.png -------------------------------------------------------------------------------- /res/images/20221023_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221023_04.png -------------------------------------------------------------------------------- /res/images/20221023_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221023_05.png -------------------------------------------------------------------------------- /res/images/20221105_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221105_01.png -------------------------------------------------------------------------------- /res/images/20221105_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221105_02.png -------------------------------------------------------------------------------- /res/images/20221106_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221106_01.png -------------------------------------------------------------------------------- /res/images/20221107_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221107_01.png -------------------------------------------------------------------------------- /res/images/20221107_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221107_02.png -------------------------------------------------------------------------------- /res/images/20221107_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221107_03.png -------------------------------------------------------------------------------- /res/images/20221107_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221107_04.png -------------------------------------------------------------------------------- /res/images/20221122_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221122_01.png -------------------------------------------------------------------------------- /res/images/20221122_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221122_02.png -------------------------------------------------------------------------------- /res/images/20221122_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221122_03.png -------------------------------------------------------------------------------- /res/images/20221122_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221122_04.png -------------------------------------------------------------------------------- /res/images/20221122_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221122_05.png -------------------------------------------------------------------------------- /res/images/20221122_06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20221122_06.png -------------------------------------------------------------------------------- /res/images/20230114_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230114_01.png -------------------------------------------------------------------------------- /res/images/20230126_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230126_01.png -------------------------------------------------------------------------------- /res/images/20230214_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230214_01.png -------------------------------------------------------------------------------- /res/images/20230214_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230214_02.png -------------------------------------------------------------------------------- /res/images/20230215_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230215_01.png -------------------------------------------------------------------------------- /res/images/20230215_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230215_02.png -------------------------------------------------------------------------------- /res/images/20230217_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230217_01.png -------------------------------------------------------------------------------- /res/images/20230217_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230217_02.png -------------------------------------------------------------------------------- /res/images/20230222_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230222_01.png -------------------------------------------------------------------------------- /res/images/20230222_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230222_02.png -------------------------------------------------------------------------------- /res/images/20230222_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230222_03.png -------------------------------------------------------------------------------- /res/images/20230307_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230307_01.png -------------------------------------------------------------------------------- /res/images/20230307_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230307_02.png -------------------------------------------------------------------------------- /res/images/20230307_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230307_03.png -------------------------------------------------------------------------------- /res/images/20230309_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230309_01.png -------------------------------------------------------------------------------- /res/images/20230309_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230309_02.png -------------------------------------------------------------------------------- /res/images/20230313_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230313_01.png -------------------------------------------------------------------------------- /res/images/20230314_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230314_01.png -------------------------------------------------------------------------------- /res/images/20230314_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230314_02.png -------------------------------------------------------------------------------- /res/images/20230314_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230314_03.png -------------------------------------------------------------------------------- /res/images/20230314_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230314_04.png -------------------------------------------------------------------------------- /res/images/20230317_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230317_01.png -------------------------------------------------------------------------------- /res/images/20230319_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230319_01.png -------------------------------------------------------------------------------- /res/images/20230319_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230319_02.png -------------------------------------------------------------------------------- /res/images/20230402_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230402_01.png -------------------------------------------------------------------------------- /res/images/20230402_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230402_02.png -------------------------------------------------------------------------------- /res/images/20230402_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230402_03.png -------------------------------------------------------------------------------- /res/images/20230402_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230402_04.png -------------------------------------------------------------------------------- /res/images/20230510_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230510_01.png -------------------------------------------------------------------------------- /res/images/20230510_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230510_02.png -------------------------------------------------------------------------------- /res/images/20230510_03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230510_03.png -------------------------------------------------------------------------------- /res/images/20230510_04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230510_04.png -------------------------------------------------------------------------------- /res/images/20230510_05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/5ec1cff/my-notes/8fde03d5031265a8c90df30ed823cc057e1014b2/res/images/20230510_05.png -------------------------------------------------------------------------------- /ro-tieba.md: -------------------------------------------------------------------------------- 1 | # RoTieba 开发之路 2 | 3 | 由于无法忍受 [TiebaLite](https://github.com/HuanCheng65/TiebaLite) 旧 UI 的拉跨,以及新 UI 使用 Jetpack Compose 导致开发及使用的性能骤降,在 TiebaLite 上维护个人分支已经难以满足需求,所以我决定按照自己的需求做一款全新贴吧 App —— 4 | 5 | RoTieba ,即 Readonly Tieba ,只读贴吧(名称暂定) 6 | 7 | RoTieba 是一款贴吧阅读器。由于个人并没有在贴吧互动的需求,且第三方回帖容易被封禁,因此计划只做阅读器,不支持发帖、回帖及其他互动操作,此外还会加入下载帖子的功能。 8 | 9 | ## 0x01 移植 aiotieba 10 | 11 | [aiotieba](https://github.com/Starry-OvO/aiotieba) 是一个开源的贴吧 client ,使用 Python 编写。由于其使用了较新的来自贴吧 App 的 API ,因此具有很好的学习价值。 12 | 13 | ### 取得 proto 14 | 15 | aiotieba 所用的 API 基本上都是 protobuf 格式的,相比贴吧 Lite 使用的 json API ,protobuf 开发起来相对方便。 16 | 17 | 有两种渠道获得 API 的 proto ,可以从 aiotieba 源码的 api 目录下的 _protobuf 目录获取,也可以从下面的仓库获取,里面包含了直接从 app dump 的原始 protobuf 定义文件。 18 | 19 | https://github.com/n0099/tbclient.protobuf 20 | 21 | 经过考虑,决定还是使用 aiotieba 自带的 protobuf ,因为相对精简,去掉了不必要的业务数据的定义,比如广告。而且我们的请求逻辑也来自 aiotieba ,因此直接使用 aiotieba 的 proto ,可以确保一致性。 22 | 23 | 不过 aiotieba 的 proto 没有声明 package ,类与类的关系是扁平的,好在每个文件的第一行注释就是类名,因此我们可以处理一下,给 proto 加上 `option java_package` ,再加入我们的项目。 24 | 25 | [Java Generated Code Guide | Protocol Buffers Documentation](https://protobuf.dev/reference/java/java-generated/) 26 | 27 | ### 配置 protobuf 依赖 28 | 29 | 直接参考 [BiliRoaming](https://github.com/yujincheng08/BiliRoaming/blob/a2d5a267e1b12a9f5019c897c2f550722a5a6dc5/app/build.gradle.kts) 。 30 | 31 | protobuf gradle plugin 有个坑点,`0.9.2` 版本一直有问题,会导致 sync 失败,不过 `0.9.3` 已经修复了。 32 | 33 | 即使项目是纯 kotlin 项目,似乎也要生成 java 的 protobuf 类,因为生成的 kotlin 类依赖于 java 的。 34 | 35 | ### 第一个请求 36 | 37 | 把 aiotieba 的 get_posts 复刻到 kotlin 上: 38 | 39 | ```kt 40 | fun getPosts() = runCatching { 41 | val client = OkHttpClient() 42 | val body = PbPageReqIdl.newBuilder() 43 | .setData(PbPageReqIdl.DataReq.newBuilder() 44 | .setCommon(CommonReqOuterClass.CommonReq.newBuilder() 45 | .setClientType(2) 46 | .setClientVersion(MAIN_VERSION) 47 | ) 48 | .setTid(8223016861) 49 | .setPn(1) 50 | .setRn(30) // post count 51 | .setSort(0) 52 | .setOnlyThreadAuthor(1) 53 | .setWithComments(0) 54 | .setIsFold(0) 55 | ).build() 56 | val multipart = MultipartBody.Builder().setType(MultipartBody.FORM) 57 | .addFormDataPart("data", "file", body.toByteArray().toRequestBody()) 58 | .build() 59 | val req = Request.Builder() 60 | .url("https://$APP_BASE_HOST/c/f/pb/page?cmd=302001") 61 | .post(multipart) 62 | .build() 63 | val resp = client.newCall(req).execute().let { 64 | PbPageResIdl.parseFrom(it.body!!.byteStream()) 65 | } 66 | if (resp.errorOrNull?.errorno != 0) { 67 | Log.e(TAG, "error occurred: ${resp.error.errorno} ${resp.error.errmsg}") 68 | } else { 69 | Log.i(TAG, "thread title: ${resp.data.thread.title}") 70 | } 71 | }.onFailure { 72 | Log.e(TAG, "failed to request:", it) 73 | } 74 | ``` 75 | 76 | 仅仅只是请求一个固定帖子 https://tieba.baidu.com/p/8223016861 77 | 78 | 然而解析请求的时候出现了问题,没法反序列化: 79 | 80 | ``` 81 | E failed to request: 82 | com.google.protobuf.InvalidProtocolBufferException: Protocol message end-group tag did not match expected tag. 83 | at com.google.protobuf.UnknownFieldSchema.mergeOneFieldFrom(UnknownFieldSchema.java:103) 84 | ``` 85 | 86 | 尝试用 App Inspector 的 Network Inspector 抓包分析。但发现这玩意虽然自带 OkHttp 支持,然而过于拉跨,二进制内容根本没法查看: 87 | 88 | ![](res/images/20230510_02.png) 89 | 90 | ### 抓包分析 91 | 92 | 使用 [mitmproxy](https://mitmproxy.org/) 93 | 94 | mitmproxy 有 web ui ,输入 `mitmweb` 即可开启,当然不能和已有的 mitmproxy 实例同时开启(默认监听端口是相同的) 95 | 96 | 用 aiotieba 写一个简单的请求,并设置代理。 97 | 98 | ```py 99 | import asyncio 100 | import aiotieba 101 | from yarl import URL 102 | 103 | 104 | async def main(): 105 | async with aiotieba.Client(proxy=(URL("http://localhost:8080"), None)) as client: 106 | ps = await client.get_posts(8223016861) 107 | print(ps) 108 | 109 | if __name__ == '__main__': 110 | import platform 111 | 112 | if platform.system() == 'Windows': 113 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) 114 | asyncio.run(main()) 115 | ``` 116 | 117 | 看上去 aiotieba 内部自动把 ssl 校验关掉了,或者说 mitmproxy 安装时已经自带了系统证书,https 竟然能直接抓 118 | 119 | ![](res/images/20230510_01.png) 120 | 121 | 同样的套路放在 Android 上: 122 | 123 | ```kt 124 | val client = OkHttpClient.Builder() 125 | .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("192.168.1.144", 8080))) 126 | .build() 127 | ``` 128 | 129 | 这样显然会失败: 130 | 131 | ``` 132 | [16:31:04.924][192.168.1.143:41724] server connect tiebac.baidu.com:443 (198.18.1.219:443) 133 | [16:31:05.023][192.168.1.143:41724] Client TLS handshake failed. The client does not trust the proxy's certificate for tiebac.baidu.com (OpenSSL Error([('SSL routines', '', 'sslv3 alert certificate unknown')])) 134 | [16:31:05.025][192.168.1.143:41724] client disconnect 135 | ``` 136 | 137 | ``` 138 | failed to request: 139 | javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found. 140 | at com.android.org.conscrypt.SSLUtils.toSSLHandshakeException(SSLUtils.java:362) 141 | at com.android.org.conscrypt.ConscryptEngine.convertException(ConscryptEngine.java:1134) 142 | at com.android.org.conscrypt.ConscryptEngine.readPlaintextData(ConscryptEngine.java:1089) 143 | ``` 144 | 145 | 在 stackoverflow 找到一个好方法,可以让 OkHttpClient 信任所有证书,这样就能抓到包了:https://stackoverflow.com/a/59322754 146 | 147 | ```kt 148 | fun OkHttpClient.Builder.ignoreAllSSLErrors(): OkHttpClient.Builder { 149 | val naiveTrustManager = object : X509TrustManager { 150 | override fun getAcceptedIssuers(): Array = arrayOf() 151 | override fun checkClientTrusted(certs: Array, authType: String) = Unit 152 | override fun checkServerTrusted(certs: Array, authType: String) = Unit 153 | } 154 | 155 | val insecureSocketFactory = SSLContext.getInstance("TLSv1.2").apply { 156 | val trustAllCerts = arrayOf(naiveTrustManager) 157 | init(null, trustAllCerts, SecureRandom()) 158 | }.socketFactory 159 | 160 | sslSocketFactory(insecureSocketFactory, naiveTrustManager) 161 | hostnameVerifier { _, _ -> true } 162 | return this 163 | } 164 | val client = OkHttpClient.Builder() 165 | .proxy(Proxy(Proxy.Type.HTTP, InetSocketAddress("192.168.1.144", 8080))) 166 | .ignoreAllSSLErrors() 167 | .build() 168 | ``` 169 | 170 | 来看一看问题,抓到的 response 居然是个 json: 171 | 172 | ```json 173 | { 174 | "logid":"2027643993","error_code":110001,"ctime":"0","server_time":1,"error_msg":"未知错误","time":1683707627 175 | } 176 | ``` 177 | 178 | 比较一下两个请求,左边是 aiotieba ,右边是 okhttp : 179 | 180 | ![](res/images/20230510_03.png) 181 | 182 | 看来少了个 header ,应该和服务端接受的数据类型有关,我们需要显式指定为 `protobuf` 。 183 | 184 | 加上去就 ok 了,这个不仔细看源码真的看不到: 185 | 186 | https://github.com/Starry-OvO/aiotieba/blob/ed8867f6ac73b523389dd1dcbdd4b5f62a16ff81/aiotieba/core/http.py 187 | 188 | 再次执行,这次可以得到正确的标题: 189 | 190 | ```kt 191 | .header("x_bd_data_type", "protobuf") 192 | ``` 193 | 194 | ![](res/images/20230510_05.png) 195 | 196 | 除此之外,请求返回的错误也能被正确汇报(下面请求了一个 tid=1 的帖子),说明基本上没问题了: 197 | 198 | ![](res/images/20230510_04.png) 199 | 200 | 201 | ## …… 202 | 203 | ### Json API 204 | 205 | 实际上 aiotieba (或者说贴吧)仍然在使用很多 json api ,这些 API 也有不少坑,比如说要签名。 206 | 207 | aiotieba 的签名实现竟然放在了 native 代码,实际上签名只需要简单的 md5 ,python 完全可以胜任,不知道作者怎么想的。 208 | 209 | 签名的具体过程:md5 依次更新每对 key 和 value 的 key , `=`, value 的 utf-8 字节,最后加上 `tiebaclient!!!` ,取 digest 。 210 | 211 | 似乎参数的顺序也有讲究,必须要按照字母升序排列。 212 | 213 | ## …… 214 | 215 | ### 表情 216 | 217 | Tieba Lite 的表情是内置在资源文件的 218 | 219 | 从网页端找到的表情: 220 | 221 | image_emoticon -> image_emoticon1 222 | 223 | 支持 https (PC web):https://tb2.bdstatic.com/tb/editor/images/client/image_emoticon25.png 224 | 225 | 不支持 https (手机触屏 web):http://static.tieba.baidu.com/tb/editor/images/client/image_emoticon25.png 226 | 227 | 但是这些表情都是旧版的,如果想要新版就麻烦了 228 | 229 | EmojiAll 收录了一些,不过没有 emoji 名字的对应 230 | 231 | https://www.emojiall.com/zh-hans/platform-baidu 232 | 233 | 最后发现表情其实是存在贴吧的 apk 里面的,因此可以直接提取 234 | 235 | 不过代码和资源有混淆,想要自动定位难度比较大 236 | -------------------------------------------------------------------------------- /sockets-on-android.md: -------------------------------------------------------------------------------- 1 | # sockets 2 | 3 | ## INET 4 | 5 | 正常的 linux 是允许任何用户访问网络的 6 | 7 | https://t.me/real5ec1cff/15 8 | 9 | https://t.me/real5ec1cff/16 10 | 11 | 曾经是 CONFIG_ANDROID_PARANOID_NETWORK 这个内核选项,如果有 AID_INET 则允许 12 | 13 | 这个源码找起来比较费劲,因为 mainline 内核是没有的 14 | 15 | https://android.googlesource.com/kernel/msm/+/2038692b9acf7ae3054a35e879039ff062316bf0/net/ipv4/af_inet.c#265 16 | 17 | ## ICMP 18 | 19 | 正常的 linux ping : 20 | 21 | ``` 22 | getcap /usr/bin/ping 23 | /usr/bin/ping cap_net_raw=ep 24 | ``` 25 | 26 | ``` 27 | socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) = -1 EACCES (Permission denied) 28 | socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) = -1 EPERM (Operation not permitted) 29 | socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6) = -1 EACCES (Permission denied) 30 | socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6) = -1 EPERM (Operation not permitted) 31 | ``` 32 | 33 | root: 34 | 35 | ``` 36 | socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) = -1 EACCES (Permission denied) 37 | socket(AF_INET, SOCK_RAW, IPPROTO_ICMP) = 3 38 | socket(AF_INET6, SOCK_DGRAM, IPPROTO_ICMPV6) = -1 EACCES (Permission denied) 39 | socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6) = 4 40 | ``` 41 | 42 | Android: 43 | 44 | ``` 45 | getcap /system/bin/ping 46 | # 无 47 | ``` 48 | 49 | ``` 50 | socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP) = 3 51 | ``` 52 | 53 | 这里用的是 SOCK_DGRAM + IPPROTO_ICMP 54 | 55 | 这实际上是内核的免特权 icmp 特性 56 | 57 | https://stackoverflow.com/a/20105379 58 | 59 | https://lkml.org/lkml/2011/5/18/305 60 | 61 | 正常 linux 默认不开启: 62 | 63 | ``` 64 | $ sysctl net.ipv4.ping_group_range 65 | net.ipv4.ping_group_range = 1 0 66 | ``` 67 | 68 | 1 0 表示 `1 页面是这个: 12 | > [Modules: Packages | Node.js v19.1.0 Documentation](https://nodejs.org/api/packages.html) 13 | 14 | 然而在那个页面刷新了多次,扩展仍然不正常工作,而切去其他页面工作都是正常的。点开扩展程序面板一看,竟然显示「不支持」。 15 | 16 | ![](res/images/20221122_01.png) 17 | 18 | 我就纳闷了,同样是 HTML 怎么你 nodejs.org 就不支持了呢?~~难道这是谁要刻意阻拦我这个英语渣渣学习 node 吗?~~ 19 | 20 | 于是打开 console 一看,果不其然有大问题: 21 | 22 | ![](res/images/20221122_02.png) 23 | 24 | ## 分析 25 | 26 | 出事的脚本是这一个: 27 | 28 | `chrome-extension://cdonnmffkdaoajfknoeeecmchibpmkmg/assets/browser-polyfill.min.js` 29 | 30 | 这应该是 webextension-polyfill ,在 saladict 的 package.json 也有它的 dependency : 31 | 32 | [mozilla/webextension-polyfill: A lightweight polyfill library for Promise-based WebExtension APIs in Chrome](https://github.com/mozilla/webextension-polyfill) 33 | 34 | 发生问题的代码,似乎是处理 js 模块加载的部分。 35 | 36 | ```js 37 | (function(a, b) { 38 | if ("function" == typeof define && define.amd) 39 | define("webextension-polyfill", ["module"], b); 40 | else if ("undefined" != typeof exports) 41 | b(module); 42 | else { 43 | var c = { 44 | exports: {} 45 | }; 46 | b(c), 47 | a.browser = c.exports 48 | } 49 | } 50 | ``` 51 | 52 | 第一部分判断全局有没有 define ,这是 [amdjs](https://github.com/amdjs/amdjs-api/wiki/AMD) 模块的定义。 53 | 54 | > [也有说是 requirejs 的](https://stackoverflow.com/questions/16950560/what-is-define-function-in-javascript) 55 | 56 | 接下来是 `exports` ,这应该是 CommonJS 模块,其中有我们熟知的 `require` 和 `module.exports` 。 57 | 58 | > [Modules: CommonJS modules | Node.js v19.1.0 Documentation](https://nodejs.org/api/modules.html#exports) 59 | 60 | 最后是直接把模块导出到 global ,应该是浏览器正常应该走的分支。 61 | 62 | 然而现在在 `b(module)` 那报了个错,提示 `module` undefined 。 63 | 64 | 那么我们看看 exports 是什么: 65 | 66 | ![](res/images/20221122_03.png) 67 | 68 | 竟然指向了一个 id 是 exports 的 DOM …… 这难道是什么奇怪的 feature 吗? 69 | 70 | 实际上 window 对象上似乎没定义这个 property ,作为对比,location 就是 window 上的 property 。 71 | 72 | ![](res/images/20221122_04.png) 73 | 74 | 于是去搜索了一下,发现这个确实是 HTML 的规范「[named access on the window object](https://html.spec.whatwg.org/multipage/nav-history-apis.html#named-access-on-the-window-object)」,由 whatwg.org 制定,应该是很权威的。 75 | 76 | > https://stackoverflow.com/a/11691401 77 | 78 | 我也随手写了一个 html 验证了想法,这样确实会破坏 saladict 的功能: 79 | 80 | ![](res/images/20221122_05.png) 81 | 82 | ![](res/images/20221122_06.png) 83 | 84 | 看上去就是这段处理模块的脚本的不对了,因为它除了判断 `exports` 是否为 object 外,没有做其他的判断。 85 | 86 | 在 UMD (Universal Module Definition) 的模板中是这么写的: 87 | 88 | [umd/returnExports.js at 36fd1135ba44e758c7371e7af72295acdebce010 · umdjs/umd](https://github.com/umdjs/umd/blob/36fd1135ba44e758c7371e7af72295acdebce010/templates/returnExports.js) 89 | 90 | 91 | ```js 92 | (function (root, factory) { 93 | if (typeof define === 'function' && define.amd) { 94 | // AMD. Register as an anonymous module. 95 | define(['b'], factory); 96 | } else if (typeof module === 'object' && module.exports) { 97 | // Node. Does not work with strict CommonJS, but 98 | // only CommonJS-like environments that support module.exports, 99 | // like Node. 100 | module.exports = factory(require('b')); 101 | } else { 102 | // Browser globals (root is window) 103 | root.returnExports = factory(root.b); 104 | } 105 | }(typeof self !== 'undefined' ? self : this, function (b) { 106 | // Use b in some fashion. 107 | 108 | // Just return a value to define the module export. 109 | // This example returns an object, but the module 110 | // can return a function as the exported value. 111 | return {}; 112 | })); 113 | ``` 114 | 115 | 这里是先判断 module 是否为 object ,然后再看 module 有没有 exports ,这样起码更加可靠一些。 116 | 117 | 但是我找不到这段问题代码到底出自哪里,webextension-polyfill 里面没有相关代码,因此可能是打包工具处理成这样的。saladict 似乎使用 webpack ,但是唯独这一个文件和其他打包的文件不太一样。 118 | 119 | ## To be continued 120 | 121 | 122 | -------------------------------------------------------------------------------- /system-properties.md: -------------------------------------------------------------------------------- 1 | # 系统属性 2 | 3 | [Android Property 实现解析与黑魔法 - 残页的小博客](https://blog.canyie.top/2022/04/09/property-implementation-and-isolation/) 4 | 5 | ## bionic 6 | 7 | ### 属性系统概述 8 | 9 | [bionic/libc/bionic/system_property_api.cpp](https://cs.android.com/android/platform/superproject/+/master:bionic/libc/bionic/system_property_api.cpp;drc=b481a2e743102efc6b18cc586aa979a70e575d64) 10 | 11 | [bionic/libc/include/sys/_system_properties.h](https://cs.android.com/android/platform/superproject/+/master:bionic/libc/include/sys/_system_properties.h;l=44;drc=a505b2d37a4ed106925c480b8c1c3ffc442d6ec5) 12 | 13 | 属性系统实际上就是一个整个系统共享的键值对存储器,通过共享文件内存映射实现。 14 | 15 | 存放属性的文件位于 `/dev/__properties__` ,这下面的多个文件,实际上是将属性按照前缀划分为不同的 SELinux 上下文,存放在对应的文件中。 16 | 17 | 划分的规则可以在 `/{system,vendor,...}/etc/selinux/{plat,...}_property_contexts` 等文件找到。 18 | 19 | init 进程负责初始化属性存储区域,且是系统中唯一能够修改属性的进程(当然,只要某个进程能 rw 映射存放属性的文件,也是可以修改的),其他进程通过属性服务(一个名为 property_service 的 socket)修改属性。其他进程会以 ro 形式共享映射属性存储,用于读取。 20 | 21 | ### 初始化 22 | 23 | 一般进程: 24 | 25 | [bionic/libc/bionic/libc_init_common.cpp __libc_init_common](https://cs.android.com/android/platform/superproject/+/master:bionic/libc/bionic/libc_init_common.cpp;l=171;drc=2557f73c05f256db3ffa9ac9892b13e226b6ea4c) 26 | 27 | `__system_properties_init` 28 | 29 | 只能映射 ro 的属性区域共享内存 30 | 31 | init: 32 | 33 | [system/core/init/property_service.cpp PropertyInit](https://cs.android.com/android/platform/superproject/+/master:system/core/init/property_service.cpp;l=1368;drc=da5323e2d6be16470b7ce2be118d41a497c7d9a6) 34 | 35 | `__system_property_area_init` 36 | 37 | 创建属性区域,并映射 rw 共享内存 38 | 39 | ### 结构 40 | 41 | bionic/libc/system_properties/include/system_properties/prop_area.h 42 | bionic/libc/system_properties/prop_area.cpp 43 | 44 | prop_area 结构体对应了整个存储区域的结构。大小 128K ,前 32*4 字节包含已分配字节、用于同步的原子 serial、魔数和版本 45 | 46 | ```cpp 47 | struct prop_trie_node { 48 | uint32_t namelen; 49 | atomic_uint_least32_t prop; // 指向 prop_info 结构体 50 | atomic_uint_least32_t left; // 二叉树左子 51 | atomic_uint_least32_t right; // 二叉树右子 52 | atomic_uint_least32_t children; // 下一层级的二叉树 53 | char name[0]; 54 | }; 55 | ``` 56 | 57 | prop_trie_node(prop_bt) 是二叉搜索树。属性名按 `.` 分割为数个层级,每个层级都由二叉搜索树用于按名字索引。prop_area 数据区的第一个对象(偏移 0)就是根结点,其 children 是第一层级的根树。可以看到,每个结点还要记录自己的名字(在最末尾)。 58 | 59 | 这一点在源码的注释已经说得比较清楚了,总的来说,这就是一个二叉搜索树 + 字典树的混合体。 60 | 61 | ```cpp 62 | // Properties are stored in a hybrid trie/binary tree structure. 63 | // Each property's name is delimited at '.' characters, and the tokens are put 64 | // into a trie structure. Siblings at each level of the trie are stored in a 65 | // binary tree. For instance, "ro.secure"="1" could be stored as follows: 66 | // 67 | // +-----+ children +----+ children +--------+ 68 | // | |-------------->| ro |-------------->| secure | 69 | // +-----+ +----+ +--------+ 70 | // / \ / | 71 | // left / \ right left / | prop +===========+ 72 | // v v v +-------->| ro.secure | 73 | // +-----+ +-----+ +-----+ +-----------+ 74 | // | net | | sys | | com | | 1 | 75 | // +-----+ +-----+ +-----+ +===========+ 76 | 77 | // Represents a node in the trie. 78 | ``` 79 | 80 | bionic/libc/system_properties/include/system_properties/prop_info.h 81 | 82 | prop_info 就是属性对象,记录了属性的值。这个值可以是最长 92 字节的短值,也可以是更长的长值。长值会作为独立的对象进行分配。 83 | 84 | ```cpp 85 | struct prop_info { 86 | atomic_uint_least32_t serial; 87 | union { 88 | char value[PROP_VALUE_MAX]; 89 | struct { 90 | char error_message[kLongLegacyErrorBufferSize]; 91 | uint32_t offset; 92 | } long_property; 93 | }; 94 | char name[0]; 95 | }; 96 | 97 | // bionic/libc/include/sys/system_properties.h 98 | #define PROP_VALUE_MAX 92 99 | ``` 100 | 101 | ### 对象分配 102 | 103 | 用于分配 prop_trie_node 、 prop_info 和 long value 。所有对象都在内存区域上依次分配。 104 | 105 | ```cpp 106 | // bionic/libc/system_properties/prop_area.cpp 107 | void* prop_area::allocate_obj(const size_t size, uint_least32_t* const off) { 108 | const size_t aligned = __BIONIC_ALIGN(size, sizeof(uint_least32_t)); 109 | if (bytes_used_ + aligned > pa_data_size_) { 110 | return nullptr; 111 | } 112 | 113 | *off = bytes_used_; 114 | bytes_used_ += aligned; 115 | return data_ + *off; 116 | } 117 | ``` 118 | 119 | prop_area 没有实现值的释放,因为 prop_info 是一个固定大小的结构体,只要已经分配过 prop 的结点,今后改写都在这个结点进行。而 long value 是 ro 属性专属的特性(众所周知 ro 不需要修改,且 bionic 也没有实现修改 long value 属性)。 120 | 121 | ## Magisk resetprop 122 | 123 | Magisk resetprop 并没有调用系统实现,而是根据 [bionic](https://cs.android.com/android/platform/superproject/+/master:bionic/libc/system_properties/) 自行实现了 property 的操作。 124 | 125 | https://github.com/topjohnwu/Magisk/blob/350d0d600cd8f24d82c83876d79e74ade5c9db64/native/src/core/resetprop/resetprop.cpp#L254 126 | 127 | 在这里, `__system_property_xxx` 不是系统 API ,而是自己实现的,原始的系统 API 通过 dlsym 获得。 128 | 129 | 就在今年五月的一个[提交](https://github.com/topjohnwu/Magisk/commit/f36b21bae55d13317b126ddf1489719870739801)后,该模块从 magisk 独立出来 130 | 131 | https://github.com/topjohnwu/system_properties 132 | 133 | 实际上大部分源码都照搬 bionic ,属性的初始化也是从 `__system_properties_init` -> `SystemProperties::Init` ,即一般进程的调用,映射的是 ro 属性区域。 134 | 135 | 不同之处在于给 `prop_area::map_fd_ro` 加了一个 rw 映射的实现,并且这个 rw 是可选的,如果不能 rw 才映射 ro 。 136 | 137 | https://github.com/topjohnwu/system_properties/blob/3b4b3f0c64bb83f955acb10fe1f2716a91c335dd/prop_area.cpp#L107 138 | 139 | 此外该库还实现了属性的 delete 140 | 141 | https://github.com/topjohnwu/system_properties/blob/3b4b3f0c64bb83f955acb10fe1f2716a91c335dd/prop_area.cpp#L433 142 | 143 | ```cpp 144 | bool prop_area::remove(const char *name, bool prune) { 145 | prop_bt *node = traverse_trie(root_node(), name, false); 146 | if (!node) return false; 147 | 148 | uint_least32_t prop_offset = get_offset(&node->prop); 149 | if (prop_offset == 0) return false; 150 | 151 | prop_info *prop = to_prop_info(&node->prop); 152 | 153 | // Detach the property from trie ASAP 154 | set_offset(&node->prop, 0u); 155 | 156 | // Then wipe out the property from memory 157 | if (prop->is_long()) { 158 | char *value = const_cast(prop->long_value()); 159 | memset(value, 0, strlen(value)); 160 | } 161 | memset(prop->name, 0, strlen(prop->name)); 162 | memset(prop, 0, sizeof(*prop)); 163 | 164 | if (prune) { 165 | prune_trie(root_node()); 166 | } 167 | 168 | return true; 169 | } 170 | ``` 171 | 172 | 方法是将 prop info 及 long value 填 0 ,并从 prop_bt 删除 prop ,还可能执行 prune ,会把没有 prop info 的树结点也全部填 0 。 173 | 174 | 为了实现修改 ro 属性,需要删除原先的 ro 属性,再用 add 重新分配结点(因为 update 不允许增加 long value) 175 | 176 | ## 权限检查 177 | 178 | 曾经尝试提权某 tv os ,由于其 selinux permissive ,利用 magica 拿到受限的 root ,尝试修改属性的 ro.debuggable 进一步提权,但是没有文件系统的 cap ,因此直接修改属性文件权限为 777 ,这样确实可以直接写文件了,但是 resetprop 报错初始化失败,启动其他新程序也无法正确初始化属性,将权限恢复后才正常,可能原因来自于 prop_area map_fd_ro 的一段检查: 179 | 180 | https://cs.android.com/android/platform/superproject/+/master:bionic/libc/system_properties/prop_area.cpp;l=113;drc=c37aa7ad3c9767257dfcfd978a2527dc63fb57cd 181 | 182 | ```cpp 183 | prop_area* prop_area::map_fd_ro(const int fd) { 184 | struct stat fd_stat; 185 | if (fstat(fd, &fd_stat) < 0) { 186 | return nullptr; 187 | } 188 | 189 | if ((fd_stat.st_uid != 0) || (fd_stat.st_gid != 0) || 190 | ((fd_stat.st_mode & (S_IWGRP | S_IWOTH)) != 0) || 191 | (fd_stat.st_size < static_cast(sizeof(prop_area)))) { 192 | return nullptr; 193 | } 194 | ``` 195 | -------------------------------------------------------------------------------- /wsl-gui-and-qq.md: -------------------------------------------------------------------------------- 1 | # WSL GUI 与 QQ 2 | 3 | 前段时间我的 ILPP 登不上了,因此又回归了电脑无 QQ 的时代。 4 | 5 | 由于自己所在的学校的原因,对 QQ 的依赖程度还是很高的,如果只靠手机,难以满足各种日常需求。 6 | 7 | 但是我又不想装那个带驱动的肮脏 PC QQ ,而新的 NT QQ Windows 版本还要过些日子才公测——当然,哪怕出了 Windows 版我也不太想装,毕竟 Windows 上想限制 QQ 的行为还是比较困难的。 8 | 9 | 曾经尝试过开一个 Windows 虚拟机的方法使用 QQ ,但是 Windows 虚拟机占用资源过多,很不方便。 10 | 11 | 既然 WSL 已经支持 GUI 了,而在 Linux 上有十万甚至九万种方法管住进程,不如就在 WSL 上跑 QQ 好了。 12 | 13 | ## GUI 14 | 15 | [Run Linux GUI apps with WSL | Microsoft Learn](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gui-apps) 16 | 17 | ``` 18 | wsl --update 19 | ``` 20 | 21 | 似乎只要更新到 latest ,就可以安装任意 gui 程序了。 22 | 23 | 上面还说要安装对应显卡的驱动,以便硬件加速,不过我没装。 24 | 25 | 安装了一个 gedit 试了一下,可以运行。 26 | 27 | ## 体验 28 | 29 | 安装了 GUI 程序后,在系统的开始菜单生成了 GUI 程序的入口,当然也可以在终端启动。 30 | 31 | ![](res/images/20230314_03.png) 32 | 33 | 不支持输入法穿透,但是可以剪切板穿透。 34 | 35 | ## 字体 36 | 37 | 安装后没有中文字体,下面有一个很有意思的方法: 38 | 39 | [WSL2 安装中文字体_wsl安装字体_Abyss0729的博客-CSDN博客](https://blog.csdn.net/oZuoZuoZuoShi/article/details/118977701) 40 | 41 | ``` 42 | sudo ln -s /mnt/c/Windows/Fonts /usr/share/fonts 43 | fc-cache -fv 44 | ``` 45 | 46 | ## QQ 47 | 48 | 这里直接用好友 Mufanc 写的 QWrap 49 | 50 | [Mufanc/QWrapper: 为 Linux QQ 提供基本的存储隔离,同时集成一些其它小功能](https://github.com/Mufanc/QWrapper) 51 | 52 | 启动和登录还是没问题的 53 | 54 | ![](res/images/20230314_04.png) 55 | 56 | 使用作者原来写的 wrap 无法保存文件,但是可以保存图片。 57 | 58 | 于是修改了一下。 59 | 60 | ``` 61 | #!/usr/bin/sh 62 | if [ "$1" = "--wrap" ] ; then 63 | exec bwrap --unshare-all --share-net \ 64 | --dev-bind / / \ 65 | --proc /proc \ 66 | --bind "$HOME/QQHome" "$HOME" \ 67 | --bind "/mnt/f/Downloads/LinuxQQ" "$HOME/Downloads" \ 68 | --bind "$HOME/.config/QQ" "$HOME/.config/QQ" \ 69 | --setenv LD_PRELOAD /opt/QQ/__patch__/libhook.so \ 70 | --chdir "$HOME" \ 71 | /opt/QQ/qq 72 | else 73 | /opt/QQ/__patch__/daemon "$0" 74 | fi 75 | ``` 76 | 77 | 现在 home 绑定挂载了 home/QQHome ,而 Downloads 则绑定到 Windows 的 Downloads 。 78 | 79 | > 本来想 home 挂载 tmpfs ,但这样会根本无法写 tmpfs : `cannot create directory ‘Downloads’: Value too large for defined data type` ,原因未知。 80 | 81 | 这样下载的问题就解决了。 82 | 83 | ## 总结 84 | 85 | 现在总算是能在 Windows 上相对干净地使用 QQ 了,满足了我的洁癖。 86 | 87 | 新 QQ 虽然发消息不太方便(输入法问题),但起码可以接收消息,可以保存文件,~~已经满足了基本需求~~,平时也不必常开,需要用的时候打开就 OK (当然我目前还是必须要手机确认登录) 88 | 89 | 补充:连群作业都不支持,基本需求还是没满足,鉴定为 *NT* QQ。 90 | 91 | ## TODO: vsock x11 92 | 93 | [WSL2 GUI切换网络后保活(vsock) - 简书](https://www.jianshu.com/p/0aa58436b230) 94 | 95 | ## TODO: 输入法 96 | 97 | …… 98 | -------------------------------------------------------------------------------- /xposed-stetho.md: -------------------------------------------------------------------------------- 1 | # 使用 Xposed 注入 Stetho 2 | 3 | [facebook/stetho: Stetho is a debug bridge for Android applications, enabling the powerful Chrome Developer Tools and much more.](https://github.com/facebook/stetho) 4 | 5 | Stetho 是 facebook 开源的一个借助 Chrome Devtools Protol 协议调试 Android app 的库,思路可谓清奇。虽然两年前就已经停止更新,不过也可以利用来调试。 6 | 7 | [Chrome DevTools Protocol - latest (tip-of-tree)](https://chromedevtools.github.io/devtools-protocol/tot/) 8 | 9 | 前人已经做了利用 Xposed 注入 stetho 到应用的模块:https://gitlab.com/derSchabi/Stethox 10 | 11 | 体验了一把,就 View 调试方面来说,感觉完爆 AS 的 Layout Inspect ;我更期待的还是 js 反射 java ,这不就像 frida 一样么,感觉这么一组合起来,我之前写的半成品 frida 脚本 libview 也可以丢掉了。可惜原来的模块并没有实现,不知道为什么。 12 | 13 | 自己动手,丰衣足食,我们自己添加 stetho-rhino 依赖进去不就好了? 14 | 15 | …… 16 | 17 | 但是就算加入了 rhino 的依赖,Stetho 的 console 仍然无法执行 js 。 18 | 19 | 于是使用 CDP Monitor 发送 raw command: 20 | 21 | ```json 22 | {"command":"Runtime.evaluate","parameters":{"expression": "1"}} 23 | ``` 24 | 25 | 结果: 26 | 27 | ```json 28 | { 29 | "exceptionDetails": { 30 | "text": "java.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference" 31 | }, 32 | "result": { 33 | "className": "What??", 34 | "description": "NullPointerException", 35 | "objectId": "5", 36 | "type": "object" 37 | }, 38 | "wasThrown": true 39 | } 40 | ``` 41 | 42 | 上调试器去找,发现问题在这里: 43 | 44 | https://github.com/facebook/stetho/blob/7c4dc8de4deb28c012b199ef52b9b5c7ad626793/stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/Runtime.java 45 | 46 | ```java 47 | public EvaluateResponse evaluate(RuntimeReplFactory replFactory, JSONObject params) { 48 | EvaluateRequest request = mObjectMapper.convertValue(params, EvaluateRequest.class); 49 | 50 | try { 51 | if (!request.objectGroup.equals("console")) { 52 | return buildExceptionResponse("Not supported by FAB"); 53 | } 54 | 55 | RuntimeRepl repl = getRepl(replFactory); 56 | Object result = repl.evaluate(request.expression); 57 | return buildNormalResponse(result); 58 | } catch (Throwable t) { 59 | return buildExceptionResponse(t); 60 | } 61 | } 62 | ``` 63 | 64 | 原来是我传参数没有加 `objectGroup` ,他这里要求必须是 `console` 65 | 66 | 重发一遍,得到了正确结果。 67 | 68 | ```json 69 | {"command":"Runtime.evaluate","parameters":{"expression": "1", "objectGroup": "console"}} 70 | ``` 71 | 72 | ```json 73 | { 74 | "result": { 75 | "type": "number", 76 | "value": 1 77 | }, 78 | "wasThrown": false 79 | } 80 | ``` 81 | 82 | 看起来 rhino 没有问题,协议的 evaluate 也没问题。 83 | 84 | 但是直接在 console 输入命令也无法触发 evaluate 的断点,说明是 devtools 本身的问题,或者是 stetho 没有实现协议的某些部分 85 | 86 | https://github.com/ChromeDevTools/devtools-frontend/blob/80586f9a4612cac80102a0be1a0e152fd168e1e2/front_end/panels/console/ConsolePrompt.ts#L278 87 | 88 | ```ts 89 | private appendCommand(text: string, useCommandLineAPI: boolean): void { 90 | const currentExecutionContext = UI.Context.Context.instance().flavor(SDK.RuntimeModel.ExecutionContext); 91 | if (currentExecutionContext) { 92 | const executionContext = currentExecutionContext; 93 | const consoleModel = executionContext.target().model(SDK.ConsoleModel.ConsoleModel); 94 | if (consoleModel) { 95 | const message = consoleModel.addCommandMessage(executionContext, text); 96 | const expression = ObjectUI.JavaScriptREPL.JavaScriptREPL.wrapObjectLiteral(text); 97 | void this.evaluateCommandInConsole(executionContext, message, expression, useCommandLineAPI); 98 | if (ConsolePanel.instance().isShowing()) { 99 | Host.userMetrics.actionTaken(Host.UserMetrics.Action.CommandEvaluatedInConsolePanel); 100 | } 101 | } 102 | } 103 | } 104 | ``` 105 | 106 | 需要一个 currentExecutionContext ,用调试器一看,果然是空的。 107 | 108 | 这应该和 `Runtime.ExecutionContextDescription` Event 有关,需要 backend 发送一个事件。 109 | 110 | https://github.com/facebook/stetho/blob/7c4dc8de4deb28c012b199ef52b9b5c7ad626793/stetho/src/main/java/com/facebook/stetho/inspector/protocol/module/Page.java 111 | 112 | stheto 中的 Page.enable 发送了该事件。 113 | 114 | ```java 115 | @ChromeDevtoolsMethod 116 | public void enable(JsonRpcPeer peer, JSONObject params) { 117 | notifyExecutionContexts(peer); 118 | sendWelcomeMessage(peer); 119 | } 120 | 121 | private void notifyExecutionContexts(JsonRpcPeer peer) { 122 | ExecutionContextDescription context = new ExecutionContextDescription(); 123 | context.frameId = "1"; 124 | context.id = 1; 125 | ExecutionContextCreatedParams params = new ExecutionContextCreatedParams(); 126 | params.context = context; 127 | peer.invokeMethod("Runtime.executionContextCreated", params, null /* callback */); 128 | } 129 | ``` 130 | 131 | 重新打开 devtools ,观察 Protocol Monitor ,frontend 确实调用了 `Page.enable` ,而 stetho 也回应了 `Runtime.executionContextCreated` ,但是 console 的 context 仍无法选择。 132 | 133 | ![](res/images/20230309_01.png) 134 | 135 | 其实我很怀疑这个逻辑的正确性,因为 Runtime 也有一个 enable 方法,应该在这里产生事件才对,而 Stetho 并没有实现它。 136 | 137 | 移到 Runtime 里面,实现 enable ,然后编译成 aar 直接加入模块 138 | 139 | ```js 140 | host.js:1 TypeError: Cannot read properties of undefined (reading 'toString') TypeError: Cannot read properties of undefined (reading 'toString') 141 | at Jt.fromString (common.js:1:53563) 142 | at kr.setLabelInternal (sdk.js:1:350735) 143 | at new kr (sdk.js:1:348382) 144 | at br.executionContextCreated (sdk.js:1:341250) 145 | at Ir.executionContextCreated (sdk.js:1:347528) 146 | at g.dispatch (protocol_client.js:1:7345) 147 | at z.dispatch (protocol_client.js:1:8336) 148 | at d.onMessage (protocol_client.js:1:4292) 149 | at Lr.dispatchMessage (sdk.js:1:356274) 150 | at At.dispatchEventToListeners (common.js:1:48112) 151 | ``` 152 | 153 | ```js 154 | setLabelInternal(t) { 155 | if (t) 156 | return void (this.#pa = t); 157 | if (this.name) 158 | return void (this.#pa = this.name); 159 | const n = e.ParsedURL.ParsedURL.fromString(this.origin); 160 | this.#pa = n ? n.lastPathComponentWithFragment() : "" 161 | } 162 | ``` 163 | 164 | 看起来要读一个 origin 属性,对比 devtools 自己的: 165 | 166 | ```json 167 | { 168 | "context": { 169 | "id": 1, 170 | "origin": "devtools://devtools", 171 | "name": "", 172 | "uniqueId": "-8461912759144154757.-4584591039569730994", 173 | "auxData": { 174 | "isDefault": true, 175 | "type": "default", 176 | "frameId": "DB80B16589F6F2FD463D4997E5AB8D32" 177 | } 178 | } 179 | } 180 | ``` 181 | 182 | 于是再修改,这次总算可以正确执行 js 了。 183 | 184 | 执行官方的 toast example 也可以通过。 185 | 186 | ```js 187 | importPackage(android.widget); 188 | importPackage(android.os); 189 | var handler = new Handler(Looper.getMainLooper()); 190 | handler.post(function() { Toast.makeText(context, "Hello from JavaScript", Toast.LENGTH_LONG).show() }); 191 | ``` 192 | 193 | ![](res/images/20230309_02.png) 194 | 195 | 196 | -------------------------------------------------------------------------------- /xweb-debugging.md: -------------------------------------------------------------------------------- 1 | # XWeb 调试 2 | 3 | 前段时间升级了微信,现在想要提取青年大学习的 cookie ,发现以前 8.0.7 开启调试的方法行不通了。 4 | 5 | 以前是强制开启 x5 内核,然后在 `debugx5.qq.com` 开启调试。据说现在换成了 XWeb 内核,方法又不一样了。 6 | 7 | WebviewPP 的 hookXWeb 方法似乎行不通,不过稍微逆向了一下,发现其实不用 Xposed 也可以。 8 | 9 | 在任意聊天栏输入地址 `http://debugxweb.qq.com/?inspector=true` ,打开后如果跳转到微信首页说明成功。 10 | 11 | 另外,如果开启了 WebviewPP 反而会失效,原因不明。 12 | -------------------------------------------------------------------------------- /zygisk-analyse.md: -------------------------------------------------------------------------------- 1 | # 分析 Zygisk 2 | 3 | 半年多前在 gist 写了一篇分析 zygisk 源码的文章,时过境迁,Zygisk 的源码也不断更新,因此旧的文章可能不再适用了。刚好最近研究使用 native bridge 重写 zygisk 加载,也了解了一下新的变化,于是重新写一篇分析。 4 | 5 | ## pre fork 6 | 7 | Zygisk 执行了一个「预 fork」的操作,具体来说就是调用 forkAndSpecialize 的时候主动 fork ,并记录 fork pid ,等到真正的 fork 要执行的时候再把这个 pid 传过去。这个操作有利于和 USAP 机制保持统一性,以及最重要的,确保模块能够不进入 zygote 主进程而干预 Specialize pre 的逻辑。 8 | 9 | 由于 USAP 的存在,fork 的时候不一定就会 specialize ,因此只有当 HookContext 存在且记录的 pid 有效的时候才执行 pre fork ,否则执行原逻辑。 10 | 11 | ```cpp 12 | // Skip actual fork and return cached result if applicable 13 | DCL_HOOK_FUNC(int, fork) { 14 | return (g_ctx && g_ctx->pid >= 0) ? g_ctx->pid : old_fork(); 15 | } 16 | ``` 17 | 18 | ## fd sanitize 19 | 20 | Zygote 在某个版本引入了 fd 检查机制,确保设计上不存在把 zygote 的 fd 泄露给 app 进程的情况。 21 | 22 | 这个检查发生在任何 fork 之前。 23 | 24 | ```cpp 25 | // frameworks/base/core/jni/com_android_internal_os_Zygote.cpp 26 | // Utility routine to fork a process from the zygote. 27 | NO_STACK_PROTECTOR 28 | pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server, 29 | const std::vector& fds_to_close, 30 | const std::vector& fds_to_ignore, 31 | bool is_priority_fork, 32 | bool purge) { 33 | SetSignalHandlers(); 34 | 35 | // Curry a failure function. 36 | auto fail_fn = std::bind(zygote::ZygoteFailure, env, 37 | is_system_server ? "system_server" : "zygote", 38 | nullptr, _1); 39 | 40 | // Temporarily block SIGCHLD during forks. The SIGCHLD handler might 41 | // log, which would result in the logging FDs we close being reopened. 42 | // This would cause failures because the FDs are not allowlisted. 43 | // 44 | // Note that the zygote process is single threaded at this point. 45 | BlockSignal(SIGCHLD, fail_fn); 46 | 47 | // Close any logging related FDs before we start evaluating the list of 48 | // file descriptors. 49 | __android_log_close(); 50 | AStatsSocket_close(); 51 | 52 | // If this is the first fork for this zygote, create the open FD table, 53 | // verifying that files are of supported type and allowlisted. Otherwise (not 54 | // the first fork), check that the open files have not changed. Newly open 55 | // files are not expected, and will be disallowed in the future. Currently 56 | // they are allowed if they pass the same checks as in the 57 | // FileDescriptorTable::Create() above. 58 | if (gOpenFdTable == nullptr) { 59 | gOpenFdTable = FileDescriptorTable::Create(fds_to_ignore, fail_fn); 60 | } else { 61 | gOpenFdTable->Restat(fds_to_ignore, fail_fn); 62 | } 63 | 64 | android_fdsan_error_level fdsan_error_level = android_fdsan_get_error_level(); 65 | 66 | ``` 67 | 68 | ## Zygisk hook 和 zygote 69 | 70 | 除了 jni hook ,zygisk 还设置了一系列 libc 的 hook ,用于更精细地控制模块的加载和卸载。 71 | 72 | ```cpp 73 | XHOOK_REGISTER(ANDROID_RUNTIME, fork); 74 | XHOOK_REGISTER(ANDROID_RUNTIME, unshare); 75 | // XHOOK_REGISTER(ANDROID_RUNTIME, jniRegisterNativeMethods); 76 | XHOOK_REGISTER(ANDROID_RUNTIME, selinux_android_setcontext); 77 | XHOOK_REGISTER_SYM(ANDROID_RUNTIME, "__android_log_close", android_log_close); 78 | ``` 79 | 80 | 在 SpecializeCommon 中,执行顺序: 81 | 82 | ``` 83 | unshare(可选) 84 | mount 85 | setresgid 86 | setresuid (此时 cap 会消失) 87 | __android_log_close 88 | selinux_android_setcontext 89 | ``` 90 | 91 | 1. unshare 92 | 93 | unshare 是分离挂载命名空间。在 Android 11 以前,这是个可选的操作,只有需要 sdcard 的 app 才会 unshare 。Android 11 及之后的版本都是强制执行。 94 | 95 | Zygote 本身启动的时候就会 unshare ,并 remount `/` 为 `MS_REC|MS_SLAVE` ,确保在 specialize 中的 mount 操作不会影响全局挂载命名空间。 96 | 97 | Zygisk 在这个阶段记录,确认 app 进程是否 unshare mnt ns ,如果 unshare 了则可以对处于 denylist 的 app umount magisk 模块(但是不会强制不可 unshare 的进行 umount)。 98 | 99 | > TODO: 此处是我修改的代码,应该改成原来的 100 | 101 | ```cpp 102 | // Unmount stuffs in the process's private mount namespace 103 | DCL_HOOK_FUNC(int, unshare, int flags) { 104 | int res = old_unshare(flags); 105 | if (g_ctx && (flags & CLONE_NEWNS) != 0 && res == 0 && 106 | // For some unknown reason, unmounting app_process in SysUI can break. 107 | // This is reproducible on the official AVD running API 26 and 27. 108 | // Simply avoid doing any unmounts for SysUI to avoid potential issues. 109 | g_ctx->process && g_ctx->process != "com.android.systemui"sv) { 110 | if (g_ctx->flags[DO_REVERT_UNMOUNT]) { 111 | revert_unmount(); 112 | } 113 | // Restore errno back to 0 114 | errno = 0; 115 | } 116 | return res; 117 | } 118 | ``` 119 | 120 | 121 | 122 | --------------------------------------------------------------------------------