为了做出腾讯游戏安全竞赛初赛的这道安卓题,开始学习 rwProcMem33 的使用来打硬件断点了

juice4fun 师傅做腾讯游戏安全竞赛初赛的安卓题的 writeup 时,使用了 rwProcMem33 来对安卓手机打下硬件断点分析反调试,我也对在安卓手机中打硬件断点的工具很感兴趣,所以就学习一下编译和使用的方法啦

要想使用 rwProcMem33, 编译环境 (即 AOSP 安卓内核源码环境) 的搭建过程是必不可少的,因为最终内核模块是运行在安卓手机的 linux 内核中,而非虚拟机的 linux 内核中,所以内核源码是有必要下载的

所以这篇文章不仅是硬件断点工具的编译和使用笔记,也是安卓内核的编译笔记,用来记录我在编译内核的过程中遇到的困难,以及如何克服的

为了更快的下载速度,可以选择配置代理,也可以手动切换下载源,只要不出现网络问题导致下载失败就行

docker 的使用完全是因为我的虚拟机 shell 环境崩溃,从而导致无法编译,如果对自己虚拟机的 shell 环境足够自信,不使用 docker 也是可以的

# 编译环境搭建

# 虚拟机配置

我使用的虚拟机为 Ubuntu22.04 , 虚拟机的参考配置如下

image-20230811162236189

# 为虚拟机配置代理

# 查看虚拟机代理地址

打开 clash for windows , 并打开 Allow LAN 的开关,随后点击 network interfaces

image-20230811175325211

请注意我的虚拟机使用的网络连接方式为 NAT 模式,所以需要关注 VMnet8 的地址,所以对于该虚拟机,代理地址为 192.168.27.1 , 端口就是 clashPort 选项所显示的端口

image-20230811175516413

# 启用虚拟机代理

依次点击如下选项进入代理配置

image-20230811180538737

输入代理地址保存即可

image-20230811180608202

# 配置 docker ubuntu 镜像

本人因为不小心运行了一个命令 source _setup_env.sh , 导致虚拟机的 shell 环境整个崩掉了, build.sh 也屡屡运行失败,看了眼 _setup_env.sh 我真是只能苦涩的笑…

image-20230816224032513

所以不得不用 docker 了,不过用下来发现竟然意外的好用

# 安装 docker

sudo apt install docker.io

# docker pull 代理配置

sudo mkdir -p /etc/systemd/system/docker.service.d
sudo gedit /etc/systemd/system/docker.service.d/proxy.conf

输入以下代理服务器内容

[Service]
Environment="HTTP_PROXY=http://192.168.27.1:7890/"
Environment="HTTPS_PROXY=http://192.168.27.1:7890/"
Environment="NO_PROXY=localhost,127.0.0.1"

# 刷新配置并重启 docker 服务

sudo systemctl daemon-reload
sudo systemctl restart docker

# docker 镜像代理配置

sudo mkdir -p ~/.docker/
sudo gedit ~/.docker/config.json

输入以下内容

{
 "proxies":
 {
   "default":
   {
     "httpProxy": "http://192.168.27.1:7890/",
     "httpsProxy": "http://192.168.27.1:7890/",
     "noProxy": "localhost,127.0.0.1"
   }
 }
}

# 下载 Ubuntu 镜像

docker pull ubuntu

# 运行 Ubuntu 镜像

docker run -it --net host --name Akernel ubuntu /bin/bash

# 安装 sudo,vim

apt-get update
apt-get install vim
apt-get install sudo

# 修改 apt-get 的软件源为阿里源

sudo cp /etc/apt/sources.list /etc/apt/sources.list_backup
sudo vim /etc/apt/sources.list

替换为如下内容

deb http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse
# deb http://mirrors.aliyun.com/ubuntu/ jammy-proposed main restricted universe multiverse
# deb-src http://mirrors.aliyun.com/ubuntu/ jammy-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ jammy-backports main restricted universe multiverse

随后将 apt-get 更新至最新版本

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install build-essential

# 安装必要的库

sudo apt-get 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 libssl-dev bc kmod cpio git curl

# 为 git 配置基本信息

git config --global user.email "xxx@gmail.com"
git config --global user.name "xxx"
git config --global http.proxy 192.168.27.1:7890

# 安装 repo

mkdir ~/bin
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo

# 修改 repo 的下载源为清华源,并添加 repo 至全局变量

# 打开全局变量配置文件

sudo vim ~/.bashrc

# 添加全局变量

在末尾添加这三行并保存

# repo
export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo/'
export PATH="~/bin:$PATH"

# 使配置文件生效

source ~/.bashrc

# 安装 python

如果使用 python --version 有打印 python 版本的话,那么这一步就不需要了,如果 docker 中没有安装 python , 在 docker 内使用如下命令安装

sudo apt-get install software-properties-common
add-apt-repository ppa:deadsnakes/ppa
sudo apt install python3.9
sudo ln -s /usr/bin/python3 /usr/bin/python

# 修改交换区大小

为了防止编译源码的过程中由于交换区不足而失败,所以我们需要去修改虚拟机的交换区的大小

# 如果提示 No such file or directory,那就直接进行下面设置交换区的操作
sudo swapoff /swapfile
sudo rm /swapfile
# 设置了 32g 交换区,防止编译失败,执行下列命令需要花费一段时间,如果执行命令后没有输出,请耐心等待命令执行完毕
sudo dd if=/dev/zero of=/swapfile bs=1GB count=32
sudo chmod 600 /swapfile
sudo mkswap -f /swapfile
sudo swapon /swapfile

# 下载内核源码

# 查看内核版本号

这里我的 pixel3 的内核版本为 Linux version 4.9.270-g862f51bac900-ab7613625

adb shell cat /proc/version
Linux version 4.9.270-g862f51bac900-ab7613625 (android-build@abfarm-east4-101) (Android (7284624, based on r416183b) clang version 12.0.5 (https://android.googlesource.com/toolchain/llvm-project c935d99d7cf2016289302412d708641d52d2f7ee)) #0 SMP PREEMPT Thu Aug 5 07:04:42 UTC 2021

# 查看内核源码分支

进入 building-kernels 中查看自己的源码分支,如图我的手机型号为 pixel3 , 并且内核版本为 4.9, 于是就知道内核源码的代号是 android-msm-crosshatch-4.9

image-20230811172340268

之后进入安卓内核源码列表中,搜索内核源码代号 android-msm-crosshatch-4.9

image-20230811232551456

我的手机是安卓 12, 所以我下载的内核版本为 android-msm-crosshatch-4.9-android12

这里的 qpr1qpr2 , 我们先看一下 QPR 的定义,简单来说就是 QPR 后面跟的数字越高,内核版本就越新

QPR, of course, is short for the Quarterly Platform Release, which Google first introduced with Android 12. These are not full system updates, but they bring a few select changes to the Pixels and other great high-end phones that opt to receive them.

注意一点就是例如你想要下载 android-msm-crosshatch-4.9-android10, 请先进入你想要下载的 AOSP 的地址,看看仓库中的 default.xml 文件,重点关注 <project path="build" name="kernel/build" , 如果 revision 的值为 main , 请千万不要下载,否则你就会发现下载下来之后根本无法 build!!, 这是由于 build 仓库和内核源码仓库不同步导致的

image-20230812144108245

当然你也可以选择进入 kernel/build 中找到适合你的 build file , 不过我还是建议能一键编译就一键编译,比如我下载的 android-msm-crosshatch-4.9-android12 , buildkernel source code 就是同步的 (惨痛的教训,头铁想要用安卓 9 的内核源码编译,结果根本无法 build… 最后还是妥协把手机刷成安卓 12 了)

# 下载内核源码

mkdir android-kernel && cd android-kernel
repo init -u https://android.googlesource.com/kernel/manifest -b android-msm-crosshatch-4.9-android12
repo sync -j4

# 切换 git 分支

我的手机的 Linux 内核版本为 Linux version 4.9.270-g862f51bac900-ab7613625 , g 后面跟的是 git 分支,所以切换的分支为 862f51bac900

cd private/msm-google
git checkout 862f51bac900

# 编译内核源码

# 解包 boot.img

首先下载 android-image-kitchen

然后将 boot.img 放在工具的根目录下,这里的 boot.img 就是网上下载的刷机包解压之后其中的 boot.img

image-20230811184714849

然后运行 unpackimg.bat , 运行之后的窗口请不要关闭,因为输出中有需要后续使用到的参数,当然也可以将输出的内容复制下来到 txt 中

Android Image Kitchen - UnpackImg Script
by osm0sis @ xda-developers
Supplied image: boot.img
Removing old work folders and files . . .
Setting up work folders . . .
Image type: AOSP
Signature with "AVBv2" type detected.
Splitting image to "split_img/" . . .
ANDROID! magic found at: 0
BOARD_KERNEL_CMDLINE console=ttyMSM0,115200n8 androidboot.console=ttyMSM0 printk.devkmsg=on msm_rtb.filter=0x237 ehci-hcd.park=3 service_locator.enable=1 cgroup.memory=nokmem lpm_levels.sleep_disabled=1 usbcore.autosuspend=7 loop.max_part=7 androidboot.boot_devices=soc/1d84000.ufshc androidboot.super_partition=system buildvariant=user
BOARD_KERNEL_BASE 0x00000000
BOARD_NAME
BOARD_PAGE_SIZE 4096
BOARD_HASH_TYPE sha1
BOARD_KERNEL_OFFSET 0x00008000
BOARD_RAMDISK_OFFSET 0x01000000
BOARD_SECOND_OFFSET 0x00000000
BOARD_TAGS_OFFSET 0x00000100
BOARD_OS_VERSION 12.0.0
BOARD_OS_PATCH_LEVEL 2021-10
BOARD_HEADER_VERSION 2
BOARD_HEADER_SIZE 1660
BOARD_DTB_SIZE 863100
BOARD_DTB_OFFSET 0x01f00000
Unpacking ramdisk to "ramdisk/" . . .
Compression used: gzip
56773 blocks
Done!
请按任意键继续. . .

unpackimg.bat 运行完毕后,我们进入 split_img/ , 然后解压其中的 boot.img-ramdisk.cpio.gz , 并将解压后的 boot.img-ramdisk.cpio 文件复制到内核源码根目录中

root@oacia-virtual-machine:/home/oacia/Desktop/# docker cp boot.img-ramdisk.cpio Akernel:/android-kernel/

# 下载 mkbootimg.py

我们还需要下载 mkbootimg.py, 并将其复制到放到内核源码根目录

docker cp mkbootimg.py Akernel:/android-kernel/

# 修改 build.sh

在内核源码根目录,进入 build/build.sh , 找到下方代码的位置

echo "========================================================"
echo " Files copied to ${DIST_DIR}"

并在这两行代码之前加上下列命令

if [ -f "${VENDOR_RAMDISK_BINARY}" ]; then
cp ${VENDOR_RAMDISK_BINARY} ${DIST_DIR}
fi

image-20230811192125650

# 下载 rwProcMem33

下载地址

https://github.com/abcz316/rwProcMem33

然后将解压后的文件夹复制到 docker 中内核源码目录下的 private/msm-google/drivers/

docker cp rwProcMem33 Akernel:/android-kernel/private/msm-google/drivers/

# 修改 rwProcMem33

# ver_control.h

MY_LINUX_VERSION_CODE 切换到对应的安卓内核版本,我们在下载内核源码阶段已经通过 cat /proc/version 知道了内核的版本号为 Linux version 4.9.270-g862f51bac900-ab7613625 , 所以在 private/msm-google/drivers/rwProcMem33/ver_control.hprivate/msm-google/drivers/rwProcMem33/hwBreakpointProcModule/hwBreakpointProc/ver_control.h 中我们也将内核切换到对应的 4.9 版本,选择 MY_LINUX_VERSION_CODE 的原则选这里出现的版本号中越接近自己手机内核版本的版本号

image-20230813120617329


Linux 4.11 前,Linux 内核把页表分为 4 级

  • 页全局目录 (Page Global Directory,PGD)
  • 页上层目录 (Page Upper Directory,PUD)
  • 页中间目录 (Page Middle Directory,PMD)
  • 直接页表 (Page Table,PT)

所以对于 Linux version 4.11 以下的内核版本,并不支持五级页表,选择 启用读取pagemap文件来计算物理内存的地址 ,同时注释掉 启用页表计算物理内存的地址 如图所示

image-20230903200605263

Linux version 4.11 版本把页表扩展到五级,在页全局目录和页上层目录之间增加了页四级目录 (Page 4th Directory,P4D)

所以对于 Linux version 4.11 及以上的内核版本,选择 启用页表计算物理内存的地址 ,同时注释掉 启用读取pagemap文件来计算物理内存的地址 如图所示

image-20230903202421912

# linux_kernel_api 文件夹

rwProcMem33/linux_kernel_api 文件夹中新建一个头文件 linux_kernel_api.h

image-20230913174004277

并写入如下内容

#ifndef LINUX_KERNEL_API_H_
#define LINUX_KERNEL_API_H_
#include "../ver_control.h"
#if MY_LINUX_VERSION_CODE < KERNEL_VERSION(5,8,0)
long probe_kernel_read(void* dst, const void* src, size_t size);
MY_STATIC long x_probe_kernel_read(void* bounce, const char* ptr, size_t sz) {
	return probe_kernel_read(bounce, ptr, sz);
}
#endif
#if MY_LINUX_VERSION_CODE >= KERNEL_VERSION(5,8,0)
long copy_from_kernel_nofault(void* dst, const void* src, size_t size);
MY_STATIC long x_probe_kernel_read(void* bounce, const char* ptr, size_t sz) {
	return copy_from_kernel_nofault(bounce, ptr, sz);
}
#endif
#endif /* LINUX_KERNEL_API_H_ */

# api_proxy.h

为了包含 linux_kernel_api.h 头文件,我们在 api_proxy.h 的前几行中加入 #include "linux_kernel_api/linux_kernel_api.h" 如图

image-20230913174430779

# 修改 drivers 的 Makefile

private/msm-google/drivers/Makefile 的开头加入下列命令

obj-y += rwProcMem33/hwBreakpointProcModule/hwBreakpointProc/
obj-y += rwProcMem33/

# 修改 msm-google 的 Makefile

在编译 rwProcMem33 内核模块时,由于内核编译时会将警告视为错误导致编译内核停止,所以我们要修改 Makefile 来忽视 warning

private/msm-google/Makefile 找到如下位置,在 -Wno-format-security 后加上一个 -w 参数

image-20230816235239736

# 开始编译

在安卓内核源码的根目录打开终端使用如下命令开始编译

命令的参数为使用 android-image-kitchen 解包 boot.img 之后,控制台所打印的参数

请务必替换为相对应的参数!!!

参数对应关系为

android-image-kitchen 解包参数名称 编译命令参数名称
BOARD_KERNEL_CMDLINE console=ttyMSM0,115200n8 androidboot.console=ttyMSM0 printk.devkmsg=on msm_rtb.filter=0x237 ehci-hcd.park=3 service_locator.enable=1 cgroup.memory=nokmem lpm_levels.sleep_disabled=1 usbcore.autosuspend=7 loop.max_part=7 androidboot.boot_devices=soc/1d84000.ufshc androidboot.super_partition=system buildvariant=user KERNEL_CMDLINE
BOARD_KERNEL_BASE 0x00000000 BASE_ADDRESS
BOARD_PAGE_SIZE 4096 PAGE_SIZE
BOARD_HEADER_VERSION 2 BOOT_IMAGE_HEADER_VERSION

编译命令中的 BUILD_CONFIG 为 AOSP 源码根目录的 build.config 的软连接所指向的配置文件

image-20230812145430979

所以最终的编译命令为

BUILD_CONFIG=private/msm-google/build.config.bluecross BUILD_BOOT_IMG=1 MKBOOTIMG_PATH=mkbootimg.py VENDOR_RAMDISK_BINARY=boot.img-ramdisk.cpio KERNEL_BINARY=Image.lz4 BOOT_IMAGE_HEADER_VERSION=2 KERNEL_CMDLINE="console=ttyMSM0,115200n8 androidboot.console=ttyMSM0 printk.devkmsg=on msm_rtb.filter=0x237 ehci-hcd.park=3 service_locator.enable=1 cgroup.memory=nokmem lpm_levels.sleep_disabled=1 usbcore.autosuspend=7 loop.max_part=7 androidboot.boot_devices=soc/1d84000.ufshc androidboot.super_partition=system buildvariant=user" BASE_ADDRESS=0x00000000 PAGE_SIZE=4096 build/build.sh

经过一段时间的等待,编译成功!

image-20230813121805073

生成的 boot.img 的路径为 /android-kernel/out/android-msm-pixel-4.9/dist/boot.img

# 使用 Magisk 修补 boot.img 实现 root

安装 Magisk 下载地址

adb install "D:\TOOLS\pixel3\Magisk-v26.1.apk"

将由内核源码编译出来的 boot.img 上传到手机上

adb push boot.img /sdcard/

然后在手机上打开 Magisk , 依次点击 安装->选择并修补一个文件->/sdcard/boot.img->开始

待修补完成后,将修补后的 boot.img 传到电脑上

adb pull /storage/emulated/0/Download/magisk_patched-xxxxx_xxxxx.img

# 刷入内核

adb reboot bootloader
fastboot flash boot magisk_patched-xxxxx_xxxxx.img
fastboot reboot

# 编译 HwBpClient 客户端

进入 rwProcMem33\hwBreakpointProcModule\testHwBpClient 文件夹,双击 testHwBpClient.vcxprojvisual studio 中打开

编译的程序位数应为 64 位

对于 Windows 平台编译的 HwBpClient , 并且需要在 testHwBpClientDlg.cpp 的这个位置进行修改,将这个位置的 llX 改为 I64X , %zu 改成 I64d , 否则将无法正确输入内容

image-20230817023155232

原因在于 C/C++ 输出 64 位数在 window 下和 linux 下是不一样的

linux

printf("%lld/n",a);
printf("%llu/n",a);
printf("%llx/n",a);

windows

printf("%I64d/n",a);
printf("%I64u/n",a);
printf("%I64x/n",a);

修改完成后如图所示

image-20230817023547904

接下来按下 ctrl+B 生成即可

image-20230816194207598

# 编译 HwBpServer 服务端

编译 HwBpServer 服务端需要 NDK, 并将 NDK 引入环境变量中

NDK 可以在 android studio 中进行安装,依次点击 File->Project Structure->SDK Location , 找到 Android NDK location , 点击 Download 即可开始安装,我这里使用的 ndk 的版本为 ndk25.2.9519653

如果没有安装 Android studio ,NDK 的安装方法可以在谷歌上找到

NDK 安装完成后,进入到 rwProcMem33\hwBreakpointProcModule\testHwBpServer\jni , 打开 cmd 运行命令

ndk-build

即可完成编译,编译后的文件在 rwProcMem33\hwBreakpointProcModule\testHwBpServer\libs 中,选择对应手机架构的 ELF, push 到手机中运行即可

# 获取手机的 IPv4 地址

将电脑和我们的测试手机连接同一个手机热点,然后在测试手机中打开 设置->WLAN 点击我们连接的热点的高级选项,来查看手机的 IP 地址

# 运行 HwBpServer 服务端

将编译出来的程序复制到手机中并运行

adb push rwProcMem33\hwBreakpointProcModule\testHwBpServer\libs\arm64-v8a\testHwBpServer.out /data/local/tmp
adb shell
su
cd /data/local/tmp
chmod 777 testHwBpServer.out
./testHwBpServer.out

查看打印出来的端口号 3170

image-20230816221255328

# 运行 HwBpClient 客户端

在电脑中运行编译完成的 HwBpClient 客户端,填入手机的 IP 地址以及由 testHwBpServer.out 打印出来的端口号

image-20230816221420002

点击连接即可开始打硬件断点

image-20230816221529230

# 查询进程 PID

ps -A

# 查询目标 so 的基址

可以使用命令来查询

cat /proc/[app的PID]/maps

也可以使用下面的 frida 脚本查询 so 的基址

function dump_so(so_name) {
    Java.perform(function () {
        var currentApplication = Java.use("android.app.ActivityThread").currentApplication();
        var dir = currentApplication.getApplicationContext().getFilesDir().getPath();
        var libso = Process.getModuleByName(so_name);
        console.log("[name]:", libso.name);
        console.log("[base]:", libso.base);
        console.log("[size]:", ptr(libso.size));
        console.log("[path]:", libso.path);
        }
    });
}
rpc.exports = {
    dump_so: dump_so
};

接下来就可以愉快的打硬件断点啦~

# 参考资料

  • [原创] 开源一个 Linux 内核里进程内存管理模块源码

  • 万字长文教你使用安卓内核驱动进行内存读写

  • 安卓内核驱动编译

  • AOSP Android 10 内核编译刷入 Pixel3

  • kernel 编译的哪些坑

  • eBPF on Android 之打补丁和编译内核修正版

  • Android10 内核编译笔记

更新于 阅读次数