前言

eBPF on Android之bcc编译与体验末尾提到可以修改内核,打补丁以获取更好的eBPF体验

本文记录了给Pixel 4XL内核打补丁,添加KPROBES支持,编译内核并刷入的过程

libssl.soSSL_writeSSL_read进行hook,对用户态进程实现无感抓包

环境

  • Pixel 4XL
  • coral-sq3a.220605.009.a1 最新版系统,Android 12
  • 4.14.261-g84ae96329b23-ab8487884
  • Ubuntu 20.04

如果想动手操作,请先看完文章再动手,不要直接复制粘贴运行,有些问题可以提前处理

记录

官方推荐的编译系统较新的是ubuntu 18.04,我这里选择的是20.04

准备环境和编译

那么先安装必要的软件等

sudo apt install git-core gnupg flex bison build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 libncurses5 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig

创建一个工作目录,后面的文件都会放这下面

mkdir /home/kali/Desktop/p4xl
export WORK_DIR=/home/kali/Desktop/p4xl
cd ${WORK_DIR}

先创建放代码的文件夹,然后同步内核源代码

根据官方的说明,我这里选择Pixel 4XL对应的分支android-msm-coral-4.14-android12L

为了能对应上手机的内核版本,这里没有加上--depth=1,后面需要checkout

mkdir kernel && cd kernel
repo init -u https://android.googlesource.com/kernel/manifest -b android-msm-coral-4.14-android12L
repo sync

最新版的内核编译脚本会使用自带的编译工具,基本上不需要自己设置

如果你同步的内核源代码没有编译工具,需要自己手动同步一下,如下

cd prebuilts
git clone https://android.googlesource.com/kernel/prebuilts/build-tools
mv build-tools kernel-build-tools
export PATH=${WORK_DIR}/kernel/prebuilts/kernel-build-tools/linux-x86/bin:$PATH

这里的prebuilts是前面同步完内核源码出现的文件夹~

查看下手机的内核版本信息

adb shell uname -r

得到的是4.14.261-g84ae96329b23-ab8487884,其中g后面的84ae96329b23就是内核的short commit id

如果要看内核编译时间等详细信息,用下面的命令

adb shell uname -a

进入内核源码目录,切换到与当前手机内核一致的分支

cd private/msm-google
git checkout 84ae96329b23

现在切回到repo同步时创建的kernel目录,通过ls -al可以看到几个软链接,但是都是红的

这是因为指向的是不存在的文件,手动删掉;并且这里bonito不是Pixel 4XL的代码,所以肯定也不符合我们的要求

[email protected]:~/Desktop/p4xl/kernel$ ls -al /home/kali/Desktop/p4xl/kernel/build.config
lrwxrwxrwx 1 kali kali 38  6月 25 17:48 /home/kali/Desktop/p4xl/kernel/build.config -> private/msm-google/build.config.bonito

然后创建我们自己的软链接

ln -s private/msm-google/build.config.floral build.config

现在的文件夹结构

.
├── build
│   ├── abi
│   ├── build_abi.sh
│   ├── build.config -> ../build.config
│   ├── build.config.net_test
│   ├── build.sh
│   ├── build_test.sh
│   ├── build-tools
│   ├── envsetup.sh
│   ├── gki
│   ├── hermetic
│   ├── LICENSE
│   ├── METADATA
│   ├── MODULE_LICENSE_APACHE2
│   ├── multi-switcher.sh
│   ├── NOTICE -> LICENSE
│   ├── _setup_env.sh
│   ├── static_analysis
│   └── upstream
├── build.config -> private/msm-google/build.config.floral
├── prebuilts
│   └── gcc
├── prebuilts-master
│   ├── clang
│   └── misc
└── private
    ├── msm-google
    └── msm-google-modules

现在可以运行build/build.sh命令进行编译了

编译之后在out/android-msm-pixel-4.14/private/msm-google/arch/arm64/boot生成了下面这些文件

-rw-rw-r--  1 kali kali  3397238 Jun 30 07:21 dtbo.img
-rw-rw-r--  1 kali kali      781 Jun 30 07:21 .dtbo.img.cmd
drwxrwxr-x 28 kali kali     4096 Jun 30 07:21 dts
-rw-rw-r--  1 kali kali 50219024 Jun 30 07:35 Image
-rw-rw-r--  1 kali kali      146 Jun 30 07:35 .Image.cmd
-rw-rw-r--  1 kali kali 24198300 Jun 30 07:35 Image.lz4
-rw-rw-r--  1 kali kali      144 Jun 30 07:35 .Image.lz4.cmd
-rw-rw-r--  1 kali kali 25246584 Jun 30 07:35 Image.lz4-dtb
-rw-rw-r--  1 kali kali      248 Jun 30 07:35 .Image.lz4-dtb.cmd

.cmd结尾的文件是文本文件,其实就是表明对应的文件是怎么来的

.Image.lz4-dtb.cmd为例,其内容如下

cmd_arch/arm64/boot/Image.lz4-dtb := (cat arch/arm64/boot/Image.lz4 arch/arm64/boot/dts/google/qcom-base/sm8150-v2.dtb arch/arm64/boot/dts/google/qcom-base/sm8150.dtb > arch/arm64/boot/Image.lz4-dtb) || (rm -f arch/arm64/boot/Image.lz4-dtb; false)

可以看到Image.lz4-dtb是几个文件合并得到的,现在看到的Image.lz4-dtb已经是合并好之后的文件了

这里需要的是Image.lz4-dtb,那有的地方说是Image.lz4,怎么判断呢

我是这样判断的,首先解包手机当前的boot.img,这里我使用的是Android-Image-Kitchen-3.8

解压Android-Image-Kitchen-3.8.zip,把手机原始的boot.img复制到解压的目录

然后将boot.img拖放到unpackimg.bat脚本上,然后就会解包

得到ramdisksplit_img文件夹,然后进入split_img文件夹,有一个boot.img-kernel文件,用16进制工具打开

可以看到末尾有不少可读的内容,而编译生成的Image.lz4文件末尾没有这些,而Image.lz4-dtb

2022-07-02T04:57:27.png

所以这里要用Image.lz4-dtb替换boot.img-kernel文件

替换之后双击运行unpackimg.bat,会自动重新打包生成image-new.img

现在就可以重新刷入这个新内核了

adb reboot bootloader
fastboot flash boot image-new.img
fastboot reboot

如果不确定内核一定没问题,可以临时刷入,这样再次重启的时候会使用之前的内核

fastboot boot image-new.img

注意,请提前配好adb,这样万一屏幕失灵还可以通过scrcpy操作,当然直接刷回刷机包的boot.img一般是可以恢复的

遇到的问题

Terminal崩溃

最开始尝试的时候,在LTO vmlinux.o的时候Terminal总是会在某个时候崩掉,完全找不到有用的信息

后续尝试了很多次,以为是swap不够,于是增加了swap

sudo dd if=/dev/zero of=/swapfile bs=1k count=16384000
sudo mkswap /swapfile
sudo swapon /swapfile

如果提示文件繁忙,先关闭

sudo swapoff /swapfile

但仍然崩溃

最后是把虚拟机从原来的处理器数量 12 + 每个处理器的内核数量 1变更为处理器数量 6 + 每个处理器的内核数量 2才能编译成功

另外还有问题试试把虚拟化打开?(虽然这好像没有什么关系...)

当然swap还是要加的,内存我给了28G,外加16G的swap,最后根据观察,打开能吃完内存,然后用掉4-5G左右的swap

如果条件有限,要么把swap设置大点,要么关闭LTO,也就是去掉CONFIG_LTO

刷机卡谷歌LOGO

当时就纳闷了,这啥也没动啊,怎么回事,怎么就卡谷歌LOGO了呢

后面分析了下编译的脚本,发现build.config.floral配置里面还会先过几个配置

2022-07-02T05:06:52.png

挨个查看,发现build.config.common里面的DEFCONFIG配置的是sunfish_defconfig

2022-07-02T05:09:26.png

难怪不开机,这里应该改成floral_defconfig

这个对应的文件是private/msm-google/arch/arm64/configs/floral_defconfig

或者在build.config.floral里面设置DEFCONFIG=floral_defconfig,这样可以覆盖掉前面设置的变量

修改之后再次编译,可以正常开机,触摸和WIFI等功能一切正常

添加内核编译项

通过zcat /proc/config.gz | grep PROBE查看内核配置

可以发现没有配置CONFIG_KPROBES

# CONFIG_KPROBES is not set

在之前的文章中提到,如果要有更良好的bcc使用体验,那得有这个才行

修改build.config.floral配置如下

KERNEL_DIR=private/msm-google
. ${ROOT_DIR}/${KERNEL_DIR}/build.config.floral.common.clang
POST_DEFCONFIG_CMDS="check_defconfig && update_my_config"
UNSTRIPPED_MODULES="
wlan.ko
"

function update_my_config() {
    ${KERNEL_DIR}/scripts/config --file ${OUT_DIR}/.config \
        -e CONFIG_IKHEADERS \
        -e CONFIG_HAVE_KPROBES \
        -e CONFIG_KPROBES \
        -e CONFIG_KPROBE_EVENT
    (cd ${OUT_DIR} && \
     make ${CC_LD_ARG} O=${OUT_DIR} olddefconfig)
}

2022-07-02T05:20:23.png

这里添加了CONFIG_IKHEADERSKPROBES的支持,CONFIG_IKHEADERS是为了方便bcc可以取到头文件信息,不然得自己手动整理内核源代码,还得放到rootfs编译环境下去

注意这里千万不要直接复制过去使用

请手动修改你的配置文件


补充小姿势

如果要给指定选项设定一个值,那么应该用--set-val,如果要编译为模块那么应该用-m

这些信息在下面这个文件中可以看到

  • private/msm-google/scripts/config
commands:
    --enable|-e option   Enable option
    --disable|-d option  Disable option
    --module|-m option   Turn option into a module
    --set-str option string
                         Set option to "string"
    --set-val option value
                         Set option to value
    --undefine|-u option Undefine option
    --state|-s option    Print state of option (n,y,m,undef)

    --enable-after|-E beforeopt option
                             Enable option directly after other option
    --disable-after|-D beforeopt option
                             Disable option directly after other option
    --module-after|-M beforeopt option
                             Turn option into module directly after other option

    commands can be repeated multiple times

options:
    --file config-file   .config file to change (default .config)
    --keep-case|-k       Keep next symbols' case (dont' upper-case it)

再次执行build/build.sh编译内核,重打包boot.img步骤和前面的一样

刷入手机后开机,可以看到现在的内核配置如下,有CONFIG_KPROBESCONFIG_IKHEADERS了(PS:CONFIG_UPROBES默认开着的,所以前面没有提)

coral:/ $ zcat /proc/config.gz | grep PROBE
CONFIG_ARCH_SUPPORTS_UPROBES=y
CONFIG_GENERIC_IRQ_PROBE=y
CONFIG_KPROBES=y
CONFIG_UPROBES=y
CONFIG_HAVE_KPROBES=y
CONFIG_HAVE_KRETPROBES=y
# CONFIG_NET_TCPPROBE is not set
# CONFIG_BUILTINS_ASYNC_PROBE is not set
# CONFIG_TEST_ASYNC_DRIVER_PROBE is not set
CONFIG_GENERIC_CPU_AUTOPROBE=y
CONFIG_TIMER_PROBE=y
CONFIG_KPROBE_EVENTS=y
CONFIG_UPROBE_EVENTS=y
CONFIG_PROBE_EVENTS=y
# CONFIG_KPROBES_SANITY_TEST is not set
coral:/ $ zcat /proc/config.gz | grep IKH
CONFIG_IKHEADERS=y

添加bpf_probe_read_user

根据bcc的issue

要添加读取用户空间数据的功能,可以参考下面这个补丁

这个看着不清晰,可以看下面这个链接

这里没有几行,加上对git打补丁不太熟,就手动找到对应的代码位置,添加了代码

  • private/msm-google/include/uapi/linux/bpf.h

2022-07-02T05:28:51.png

  • private/msm-google/kernel/trace/bpf_trace.c

2022-07-02T05:29:38.png

2022-07-02T05:29:54.png

  • private/msm-google/tools/include/uapi/linux/bpf.h

2022-07-02T05:30:22.png

注意原patch中有两处FN(probe_read_user),,但是我截图没有,这是因为我这个版本的源代码已经有这个了

2022-07-02T05:33:35.png

主要是缺少BPF_CALL_3(bpf_probe_read_user, void *, dst, u32, size, const void *, unsafe_ptr)函数

所以请打补丁的时候仔细确定,看好了再修改,否则编译会出问题,提示重定义什么的

再次编译内核,刷入手机

查看下内核的符号信息

cat /proc/kallsyms | grep probe_read

2022-07-02T06:26:01.png

完整体验sslsniff.py

根据issue的信息,完成内核的patch后,bcc脚本中bpf_probe_read_user还是失败了

这是因为bcc的代码中,找不到bpf_probe_read_kernel符号,然后直接回退到bpf_probe_read了,而不是bpf_probe_read_user,所以实际上还是通过读内核的函数去读取用户态数据

issue中提到了一些方案,比如修改内核源码的bpf_probe_read,如果probe_kernel_read不成功就调用probe_user_read去读

73984-ew9moz24rma.png

不过我觉得还是改bcc的代码方便点,我这里简单把check_bpf_probe_read_kernel返回bpf_probe_read改为bpf_probe_read_user

然后重新编译bcc并安装即可

  • src/cc/frontends/clang/b_frontend_action.cc

2022-07-02T06:16:31.png

但这样的话就只能读用户态数据了,更好的兼容方案最好还是分析下bcc的逻辑才行,这里是临时操作

如果你改了这个,不要忘记了

又看了下代码,修改DoMiscWorkAround里面的定义最合适,如下图

2022-07-02T06:33:35.png

也就是把#define bpf_probe_read_user bpf_probe_read改为#define bpf_probe_read_user bpf_probe_read_user即可


前面刷的内核是没有root权限的,所以记得用magisk给新编译出来的内核patch,再刷到手机上

如果之前准备过了bcc编译和使用环境,那么只是重新刷入了新的内核是不需要再准备一遍的,因为环境还在的

脚本是bcc目录下的tools/sslsniff.py

这个脚本需要做出两处修改

probe_SSL_do_handshake_enter函数中添加u32 uid = bpf_get_current_uid_gid();

2022-07-02T05:39:38.png

SSL_exit中将buf_copy_size改为固定值

添加一句日志打印

bpf_trace_printk("[bpf_probe_read_user] ret:%d len:%d\\n", ret, len);

2022-07-02T05:41:26.png

先找一个APP,看下pid,然后执行下面的命令

python sslsniff.py -p 17507 --no-openssl --no-gnutls --no-nss --hexdump -x --extra-lib openssl:/apex/com.android.conscrypt/lib64/libssl.so

APP滑动下,触发https请求,现在就能得到带有数据的输出了

2022-07-02T05:44:21.png

这说明bpf_probe_read_user的补丁确实有效,现在可以实现用户态数据的读取了

2022-07-02T05:46:51.png

查看trace_pipe,可以看到bpf_probe_read_user返回值为0而不是-14,确实是读取正常的

echo 1 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace_pipe

关闭trace记录

echo 0 > /sys/kernel/debug/tracing/tracing_on

bcc自带的脚本很多依赖于KRETPROBES,实际上只有KPROBES也能做不少事情了

如果无法开启KRETPROBES那就只能再自己写脚本来获取数据了

我测试添加下面的特性,但是编译出来的内核刷入后,触摸和WIFI都会失效,尝试了很久还是没有解决,最终还是放弃折腾了

-e CONFIG_KRETPROBES \
-e CONFIG_HAVE_KRETPROBES \
-d CONFIG_SHADOW_CALL_STACK \
-e CONFIG_ROP_PROTECTION_NONE \

以上就是整个过程,现在能实现对用户态的程序进行hook,并读取用户态数据了~~

结合bpf_probe_write_user则可以对用户态数据进行修改了

后面将结合对sslsniff.py的理解,尝试写一个hook任意so与修改数据的demo

这是他人已有的示例

参考