华为⼿机内核代码的编译及刷⼊教程【通过魔改华为
P9AndroidKernel对抗反调试机制】
0x00  写在前⾯
攻防对⽴。程序调试与反调试之间的对抗是⼀个永恒的主题。在安卓逆向⼯程实践中,通过修改和编译安卓内核源码来对抗反调试是⼀种常见的⽅法。但⽹上关于此类的资料⽐较少,且都是基于AOSP(即"Android 开放源代码项⽬",可以理解为原⽣安卓源码)进⾏修改,然后编译成⼆进制镜像再刷⼊Nexus 或者Pixel 等⾕歌亲⼉⼦⼿机。但因为⾕歌的亲⼉⼦在国内没有⾏货销售渠道,市场占有率更多的是国产⼿机,⽽修改国产⼿机系统内核的教程却很少,加之部分国产⼿机的安卓内核和主线 AOSP 存在些许差异,照搬原⽣安卓代码的修改⽅法⽆法在国产⼿机上实现某些功能,甚⾄⽆法编译成功。所以本⽂以某国产⼿机为例,通过研究其内核源码,对关键代码进⾏分析、修改,编译内核、打包成刷机镜像,对全过程予以展⽰。
0x01 常见反调试⼿段及对抗策略简介
在安卓程序的开发过程中,反调试的⼿段有很多种,简单列举若⼲:
(1) 检测特定进程或端⼝号。如 IDA Pro 在对安卓应⽤进⾏调试时,需要在⼿机端启动调试程序 android
_server ,该调试程序默认开启端⼝23946。⽬标程序若发现⼿机⾥有 android_server 进程或开启了端⼝23946,⽬标程序就⾃动退出,以达到反调试的⽬的。
(2)检测某些关键⽂件的状态。如⽬标程序在调试状态时,Linux内核会向部分系统⽂件内写⼊⼀些进程状态信息,包括但不限于向 “ /proc/⽬标程序pid/status ” 这⼀⽂件的 TracerPid 字段写⼊调试进程的 pid 。有部分程序会检查这些字段,⽐如⽬标程序发现对应的 TracerPid 不等于 0 ,则说明⾃⼰本⾝正在被别的程序调试,⽐如:
(Pid为19707的进程正在被Pid为24741的进程调试)
(3)检测软件断点。在对⽬标程序进⾏调试的过程中,难免会出现断点。有些程序会通过检测在调试状态下的软件断点(如读取ELF⽂件在内存中的某些地址是否存在断点指令)来判断⾃⼰是否正在被调试。
相应的,反调试的对抗策略也层出不穷。⽐如相针对以上第(2)种的反调试⼿段,在实战中存在有以下⼏种⽅案来对抗:
A.修改 Android 系统的 kernel 源码,对“进程状态”相关的函数源码进⾏修改,然后对内核源码进⾏重新编译并刷写到⼿机⾥以骗过反调试检测。
B.提取⼿机 boot.img ,⽤⼯具对 boot.img⽂件进⾏解包处理,解包之后得到 Android 的⼆进制内核⽂件。使⽤ IDA 对其进⾏逆向分析及修改某些位置,其实质也是修改内核“进程状态”相关函数,
C.hook 系统 fopen 函数,或者 hook ⽬标程序对 /proc/pid/status 等⽂件的读取等,使其返回错误的值以骗过反调试检测。
综合以上⽅案,不难看出,在内核层⾯进⾏修改⽆疑为⼀劳永逸的办法。关于修改内核源码,⽹上当前的资料都是基于原⽣安卓源码进⾏修改。前⾯我们也说过,照搬原⽣安卓的修改办法,往往并不能在国产⼿机上通过。本⽂便采取以上第 A 种⽅案,通过修改某⼿机的内核源码,并在Ubuntu 上进⾏交叉编译,然后打包成刷机镜像,刷⼊⼿机,对抗反调试。
0x02 源码获取及修改
不同于 AOSP ⼤⼤⽅⽅的开源,国产⼿机的开源代码却有点”遮遮掩掩“,不太好。(但是⼩⽶⼿机除外,⼩⽶的开源做的是越来越好了,在上公开了好多机型的代码。)⽽该⼿机的 kernel 源码就得在它的英⽂版⽹站上才能到(以某⼿机为例):其内核源码下载,这个地址实在是不太好。进⼊正题,我⼿头上的是 Android 7.0, EMUI 5.0 的系统,我们下载对应的 kernel 源码,然后解压到硬盘上,如图(本⽂的源码存放⽬录是 /home/lazarus/Huawei_Kernel/Code_Opensource ):
kernel ⽬录⾥是该⼿机的内核源码,这是整个⼿机系统的核⼼,它负责着内存管理、CPU和进程管理、⽂件系统、设备管理和驱动、⽹络通信,以及系统的初始化(引导)、系统调⽤等。经过分析研究以及查阅资料得知,我们要修改的源⽂件位于
/Code_Opensource/kernel/fs/proc ⽬录下,array.c 和 base.c 这两个⽂件,总共3处需要修改,如图:
接下来,我们⽤⽂本编辑器分别打开这两个⽂件,开始进⾏如下修改:
第1处,  /Code_Opensource/kernel/fs/proc/array.c  (115⾏):
具体操作如下:
static const char * const task_state_array[] = {
"R (running)",        /*  0 */
"S (sleeping)",        /*  1 */
"D (disk sleep)",    /*  2 */
"T (stopped)",        /*  4 */
"S (sleeping)",        /*  1 */  //第⼆步,再加上⼀⾏,保持数组⼤⼩不变
// "t (tracing stop)",    /*  8 */  //第⼀步,把这⼀⾏注释掉(或删掉)
"X (dead)",        /*  16 */
"Z (zombie)",        /*  32 */
};
这⼀处操作是修改Linux内核对进程状态的描述,主要是改掉"t (tracing stop)",这表⽰进程处于跟踪状态或者暂停状态,会写⼊进程状态的描述⽂件的。修改时要注意保持数组⼤⼩不变,因为后⾯的代码会检查这个数组⼤⼩,如果数组⼤⼩变动了,编译的时候会出错。
第2处,  /Code_Opensource/kernel/fs/proc/array.c  (163⾏):
具体操作如下:
tpid = 0;    //添加上这⼀⾏,将 tpid 重新赋值为 0
seq_printf(m,
"State:\t%s\n"
"Tgid:\t%d\n"
"Ngid:\t%d\n"
"Pid:\t%d\n"
"PPid:\t%d\n"
"TracerPid:\t%d\n"
"Uid:\t%d\t%d\t%d\t%d\n"
"Gid:\t%d\t%d\t%d\t%d\n"
"FDSize:\t%d\nGroups:\t",
get_task_state(p),
tgid, ngid, pid_nr_ns(pid, ns), ppid, tpid,
from_kuid_munged(user_ns, cred->uid),
from_kuid_munged(user_ns, cred->euid),
from_kuid_munged(user_ns, cred->suid),
from_kuid_munged(user_ns, cred->fsuid),
from_kgid_munged(user_ns, cred->gid),
from_kgid_munged(user_ns, cred->egid),
from_kgid_munged(user_ns, cred->sgid),
from_kgid_munged(user_ns, cred->fsgid),
max_fds);
这⼀处操作是对 tpid 进⾏重新赋值。tpid 是描述进程状态的⼀个变量,它关联着进程状态描述的TracerPid 的值,表⽰ ptrace 对应的进程 id ,可以理解为如果⽬标程序处于调试状态,tpid的值 == 调试程序的pid;如果⽬标程序未处于调试状态,则 tpid 的值 == 0 。
第3处,  /Code_Opensource/kernel/fs/proc/base.c  (243⾏):
具体操作如下:
static int proc_pid_wchan(struct seq_file *m, struct pid_namespace *ns,
struct pid *pid, struct task_struct *task)
{
unsigned long wchan;
char symname[KSYM_NAME_LEN];
wchan = get_wchan(task);
if (wchan && ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS)
&& !lookup_symbol_name(wchan, symname))
//在此处增加代码如下:
{
if (strstr(symname, "trace")) {
seq_printf(m, "%s", "sys_epoll_wait");
}
//增加到到这⾥为⽌。
seq_printf(m, "%s", symname);
}
else
seq_putc(m, '0');
return 0;
这⼀处操作是针对 proc_pid_wchan() 函数,它影响着  /proc/⽬标进程PID/wchan 这⼀⽂件,当进程处于调试状态下, wchan⽂件会显⽰ptrace_stop。
以上就是对两个⽂件的修改及简要讲解。注意:在修改代码时注意不要出现语法错误,以免编译的时候报错。修改完毕之后,我们进⼊下⼀章,也就是紧张刺激的交叉编译环节。
0x03 交叉编译环境配置及编译流程
建议使⽤ Liunx 系统编译,我⽤的是 Ubuntu 。在开始编译之前,我们当然要先对编译环境进⾏⼀番配置。下载的源代码中有个 “ ” 的⽂本⽂档,⾥⾯简要描述了编译要求,这⾥我们展开再详细讲⼀下。该⽂档是这么说的:
1. How to Build
- get Toolchain
From android git server, codesourcery and etc ..
- aarch64-linux-android-4.9
- edit Makefile
edit CROSS_COMPILE to right toolchain path(You downloaded).
Ex)  export PATH=$PATH:$(android platform directory you download)/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin
Ex)  export CROSS_COMPILE=aarch64-linux-android-
$ mkdir ../out
$ make ARCH=arm64 O=../out merge_hi3650_defconfig
$ make ARCH=arm64 O=../out -j8
2. Output files
- Kernel : out/arch/arm64/
- module : out/drivers/*/*.ko
3. How to Clean
$ make ARCH=arm64 distclean
$ rm -rf out
也就是说,第⼀步 : 我们要先获得交叉编译的⼯具链(该⼿机是 aarch64 架构):
aarch64-linux-android-4.9
这个可以从⽹上下载,⽐如 Google 官⽅地址(因众所周知的原因,访问该URL可能需要某种⼿段)
当然也可以从github等处到。(我⽤的是之前编译 AOSP 时的⼯具链,所以我就没有再下载。)
下载后解压到某个⽬录,⽐如我的在 /home/lazarus/aarch64-linux-android-4.9 这个⽬录:
第⼆步 : 把⼯具链的路径放到系统变量⾥⾯,好让我们的操作系统在编译的时候知道去哪⼉到⼯具链。打开终端,输⼊如下命令:
export PATH=$PATH:/home/lazarus/aarch64-linux-android-4.9/bin
(你需要将 /home/lazarus/aarch64-linux-android-4.9 换成你⾃⼰的⼯具链存放路径)
第三步 : 设置交叉编译参数,在刚才的终端⾥再输⼊
export CROSS_COMPILE=aarch64-linux-android-
(不知为何,有的时候在开始编译时 gcc会报错,这时把 CROSS_COMPILE 后⾯的参数设为了完整路径即:/home/lazarus/aarch64-linux-android-4.9/bin/aarch64-linux-android- 就好了 ……)
第4步:还记得我们下载的某⼿机kernel源码吗?我们在该终端内输⼊以下命令,来到kernel的源码⽬录:
cd /home/lazarus/Huawei_Kernel/Code_Opensource/kernel
第5步:按照 “ ”的说明,在kernel ⽬录的上⼀级新建⼀个⽬录(或称之为⽂件夹也⾏),这个⽬录将⽤来存放我们编译出来的内核⼆进制⽂件:
mkdir ../out
第6步:设置编译参数,将⽬标⽂件存放路径设为刚才的 out ⽬录,编译设置从 merge_hi3650_defconfig 中读取:
make ARCH=arm64 O=../out merge_hi3650_defconfig
第7步:开始编译。输⼊以下命令,准备起飞吧!
make ARCH=arm64 O=../out -j8
第1~7步的输⼊如下图所⽰:
经过⼀番等待,编译完成后,我们最会在 ~/Code_Opensource/out/arch/arm64/boot ⽬录中发现 这⼀⽂件,这个就是编译完成后的⼆进制内核⽂件——的压缩包。接下来我们需要做的,就是把这个内核放到⼿机系统⾥,让它跑起来就⾏了。不过……这个压缩包怎么写⼊⼿机系统⾥⾯呢?这是系统内核,可不是简单复制粘贴就能完事⼉的。我们且看下⼀章节。
国内源代码网站
0x04 将内核刷⼊⼿机
经常刷机的朋友们想必知道 fastboot 。在安卓⼿机中,fastboot 是⼀种⽐ recovery 更底层的刷机模式(俗称引导模式)。需要使⽤USB数据线连接⼿机,然后刷⼊相应的镜像⽂件。较为常见的镜像⼤多是 boot.img(内核/引导),recovery.img(恢复界⾯,⼤众喜爱的第三⽅recovery TWRP 就是此类镜像), system.img(这个⼀般⽐较⼤,⾥⾯是安卓系统。常见的第三⽅ROM就是通过修改它得来的)。
我们这次需要通过给⼿机刷⼊ boot.img 来更新⼿机内核。简单的说,boot.img 包含两部分,分别为 kernel 和 ramdisk 。⽽其中的 kernel 就包含我们刚才编译出来的内核⽂件。那么 boot.img 从哪⾥可以搞的到呢?第⼀种⽅法:如果你硬盘⾥存放的有这款⼿机的刷机包的话,可以通过解包等操作来获取⼿机的 boot.img 。不过这种⽅法显然略显苛刻,那既然 boot.img 是被刷⼊⼿机中的,可不可以直
接从⼿机中提取出来呢?答:可以(前提是⼿机已经 root ),这就是我们要讲的第⼆种⽅法,看操作:
(1)到 boot.img 的“藏⾝之处”
⼿机打开开发者模式,勾选允许USB调试,然后通过USB数据线接⼊电脑。在电脑端启动⼀个终端,输⼊如下命令:
adb shell
su
cd /dev/block/platform/hi_mci.0/by-name
ls -l boot
简要解释以下这段命令的意思:⾸先进⼊ ADB shell 并获得 su 权限(这也是需要⼿机已经 root 的原因),然后切换到
/dev/block/platform/hi_mci.0/by-name 这⼀⽬录。如果你在⼿机⾥⾯的⽂件管理器中打开这个⽬录,
会发现⾥⾯是⼀堆类似于Windows系统中的“快捷⽅式”⼀类的东西,其实这个在Linux系统中叫做“软链接”(或者叫“符号链接”),不同名字的软链接会指向它们真正的所在的mmcblk(块设备)。⽐如以上命令最后⼀句 ls -l boot 的意思就是显⽰ boot 分区所在的mmcblk。如下图,boot 分区存放在“mmcblk0p28”之中:
(2)将boot.img 提取到⼿机
到了 boot 分区的存放位置,我们⽤ Linux 的 dd 命令将其提取到⼿机的内部存储空间中:
dd if=/dev/block/mmcblk0p28 of=/sdcard/boot.img
简单解释下:dd 命令的⽤途是⽤指定⼤⼩的块拷贝⼀个⽂件,其中 “ if = ” 后⾯跟着的,是输⼊⽂件名,也就是我们上⼀步到的 boot 分区藏⾝之处,⽽“ of= ” 后⾯跟着的,是输出⽂件名,也就是我们想要的boot.img 。这样,我们就把⼿机的boot f分区内容提取到了⼿机的
/sdcard ⽬录中,你可以在⼿机的内部存储空间⾥到它。
然后再开启⼀个终端,将boot .img 从⼿机的/sdcard ⽬录中复制到电脑上:
adb pull  /sdcard/boot.img  boot.img
这个命令很简单,不⽤解释了吧?复制完成后,我们可以在电脑硬盘中到 boot.img,如图:
(3) 对 boot.img 进⾏修改,放⼊新内核
既然得到了 boot.img ,下⼀步就是把修改 boot.img 。我们需要先把 boot.img 解包,然后将新内核替换进去,再重新打包,然后刷⼊⼿机。这⾥我们需要⼀个⼯具,叫做 Android Image Kitchen (这⼀步你也可以在Windows上操作,但我⽤了Ubuntu,所以要下载这个⼯具的 Linux 版本,它也有Windows 版本你可以到,也可以⽹上搜索)。下载后解压到硬盘,同时为了⽅便操作,我们把刚才提取的 boot.img 也放
到 Android Image Kitchen 所在的⽬录中。然后再开⼀个终端,定位到该⽬录,执⾏ ./unpackimg.sh 进⾏解包,如下图:
解包完成后,⽬录下会多出两个⽂件夹。其中⼀个名叫 split_img ,我们要替换的 kernel 就在⾥⾯存放着。我们打开这个⽂件夹,会发现⼀个叫做 boot.img-zImage 的⽂件——这个就是我们要的东西了!还记得之前我们编译出来的新内核⽂件吗?我们把新内核⽂件重命名
为 boot.img-zImage ,复制到 split_img ⽂件夹,替换掉之前这个旧的内核⽂件。然后执⾏ ./repackimg.sh 对 boot 镜像进⾏重新打包,这样会⽣成⼀个新的 boot 镜像⽂件 “image-new.img” ,如下图:
到了这⼀步,⼯作基本上就接近尾声了。接下来,我们要是把这个新镜像刷⼊⼿机。
(4)通过fastboot刷⼊新内核
将该⼿机通过USB数据线连接电脑(记得开USB调试),在刚才的终端内执⾏以下命令进⼊fastboot:
adb reboot bootloader
⼿机会⾃动重启到 fastboot 界⾯。进⼊fastboot界⾯以后,在终端内执⾏以下命令,将 image-new.img 刷⼊⼿机的 boot 分区:
su
fastboot flash boot image-new.img
⼀切顺利的话,如下图所⽰:
此处要声明两个情况:
(1)我的 Ubuntu 的fastboot 需要在 su 权限下运⾏,也许有的⼈不需要 su 就可以。另外也可以⽤ Windows 系统也进⾏fastboot 。(2)如果出现 FAILED (remote: Command not allowed) 的错误信息,
很有可能是没有关闭“⼿机回”这⼀功能所致,需要在⼿机⾥⾯关闭⼿机回功能。可以参考。
刷⼊完毕后,对⼿机进⾏重启:
fastboot reboot
重启完毕后,你亲⼿编译的新内核就运⾏在你⼿机上了。看看新鲜出炉的内核版本:
0x05 真机测试
好了,⼤功告成。到了我们喜闻乐见的真机测试环节。我们将⼿机连接电脑,push IDA 的gdb调试器到⼿机的 /data/local/tmp ⽬录,启动
gdb调试器,开启端⼝转发,启动IDA Pro ……(具体操作⾃⾏查阅⽤IDA 调试 Android 的⽅法。)这⼀章节我⽤了 Windows 10 系统,安装的是 IDA Pro 7.0:
就以我⼿机上的 “yapplication” 这个程序来测试吧,记下它的进程PID 是 13819 ,我们附加上去开始调试……
此时,我们再开⼀个命令⾏窗⼝,进⼊⼿机 adb shell ,⽤如下命令查看PID 13819 的进程状态:
cat /proc/13819/status
在我们对内核修改之前,TracerPid 的值应该是 android_server 的 PID 。⽽现在,我们仔细观察它的 TracerPid 字段,是不是已经变成 0 了说明我们编译的内核已经正常运⾏,⽽且实现了我们想要的对抗反调试的功能。然后你就拥有了⼀个开启“⽆敌模式”的⼿机,某些(通过检测⾃⾝状态)带有反调试功能的程序在⾥⾯将⽆法察觉⾃⾝的状态,已然完全任你摆布(调试)了——⽤ IDA 附加上去,开始起飞吧!

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。