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 大大方方的开源,国产手机的开源代码却有点”遮遮掩掩“,不太好找。(但是 小米手机 除外,小米的开源做的是越来越好了,在 他们的Github上公开了好多机型的代码。)而该手机的 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 。在开始编译之前,我们当然要先对编译环境进行一番配置。下载的源代码中有个 “ README_Kernel.txt ” 的文本文档,里面简要描述了编译要求,这里我们展开再详细讲一下。该文档是这么说的:

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/boot/Image.gz
- module : out/drivers/*/*.ko
3. How to Clean
$ make ARCH=arm64 distclean
$ rm -rf out

也就是说,第一步 : 我们要先获得交叉编译的工具链(该手机是 aarch64 架构):

aarch64-linux-android-4.9

这个可以从网上下载,比如 Google 官方地址(因众所周知的原因,访问该URL可能需要某种手段)

https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/

当然也可以从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步:按照 “ README_Kernel.txt ”的说明,在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 目录中发现 Image.gz 这一文件,这个就是编译完成后的二进制内核文件——的压缩包。接下来我们需要做的,就是把这个内核放到手机系统里,让它跑起来就行了。不过……这个压缩包怎么写入手机系统里面呢?这是系统内核,可不是简单复制粘贴就能完事儿的。我们且看下一章节。

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) 的错误信息,很有可能是没有关闭“手机找回”这一功能所致,需要在手机里面关闭手机找回功能。可以参考 该帖子的2楼回帖 。

刷入完毕后,对手机进行重启:

fastboot reboot

重启完毕后,你亲手编译的新内核就运行在你手机上了。看看新鲜出炉的内核版本:

0x05 真机测试

好了,大功告成。到了我们喜闻乐见的真机测试环节。我们将手机连接电脑,push IDA 的gdb调试器到手机的 /data/local/tmp 目录,启动gdb调试器,开启端口转发,启动IDA Pro ……(具体操作自行查阅用IDA 调试 Android 的方法。)这一章节我用了 Windows 10 系统,安装的是 IDA Pro 7.0:

就以我手机上的 “com.example.root.myapplication” 这个程序来测试吧,记下它的 进程PID 是 13819 ,我们附加上去开始调试……

此时,我们再开一个命令行窗口,进入手机 adb shell ,用如下命令查看PID 13819 的进程状态:

cat /proc/13819/status

在我们对内核修改之前,TracerPid 的值应该是 android_server 的 PID 。而现在,我们仔细观察它的 TracerPid 字段,是不是已经变成 0 了 ?说明我们编译的内核已经正常运行,而且实现了我们想要的对抗反调试的功能。然后你就拥有了一个开启“无敌模式”的手机,某些(通过检测自身状态)带有反调试功能的程序在里面将无法察觉自身的状态,已然完全任你摆布(调试)了——用 IDA 附加上去,开始起飞吧!

【本文首发于 FreeBuf.COM ,在此做一备份记录。链接地址--> https://www.freebuf.com/articles/terminal/229624.html 】

华为手机内核代码的编译及刷入教程【通过魔改华为P9 Android Kernel 对抗反调试机制】的更多相关文章

  1. 从谷歌官网下载android 6.0源码、编译并刷入nexus 6p手机

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/fuchaosz/article/details/52473660 1 前言 经过一周的奋战,终于从谷 ...

  2. 自己编译Android(小米5)内核并刷入(一键自动编译打包)

    之前自己编译过Android系统,刷入手机.编译很简单,但坑比较大,主要是GFW埋的坑.. 编译android系统太大了,今天记下自己编译及刷入android内核的方法. 主要是看到第三方内核可以超频 ...

  3. 红米手机4A怎么样刷入开发版获得ROOT权限

    小米的手机或平板不同手机型号一般情况官方都提供两个不同系统,可分为稳定版和开发版,稳定版没有提供root权限管理,开发版中就支持了root权限,在很多工作的时候我们需要使用的一些功能强大的app,都需 ...

  4. 识别手机浏览器代码【C#和JS两种语言】

    C# 识别手机浏览器代码: public static bool MobileBrowserDetect() { bool bismobile = false; try { #region 包含and ...

  5. 解决华为手机无法输出Debug级别log的问题

    近期购入了新款的华为手机荣耀8,手感.性能.颜值都非常好.作为android开发工程师,自然会用到真机进行日常的调试.然而,这部手机并没有这么“听话“!反复尝试开启开发者选项中的设置项,依旧无法输出L ...

  6. KaliLinuxNetHunter教程刷入第三方Recovery与开始刷机

    KaliLinuxNetHunter教程刷入第三方Recovery与开始刷机 2.刷入第三方Recovery(即TWRP) TWRP(TeamWin Recovery Project)是一款XDA大神 ...

  7. 努比亚Z7 mini刷机教程_recovery卡刷机教程

    之前小编分享努比亚Z7 mini电话访问Root权限.recovery刷机教程. 所以对于朋友谁搞机整机的爱,左边是写第三方手机刷包.那么下面刷的家小编与您分享努比亚Z7 mini刷机教程手机. 一. ...

  8. 解决linux ubuntu不能识别华为手机的问题--升级内核

    敝人手中有一个华为mate8,但是debian, ubuntu及一系列衍生版均不能识别.只能识别出一个华为手机助手,但是无法使用华为的内置存贮. 在fedora上是可以完美使用的. 归根到底的原因,是 ...

  9. 【Android 系统开发】CyanogenMod 13.0 源码下载 编译 ROM 制作 ( 手机平台 : 小米4 | 编译平台 : Ubuntu 14.04 LTS 虚拟机)

                 分类: Android 系统开发(5)                                              作者同类文章X 版权声明:本文为博主原创文章 ...

  10. Android6.0源码下载编译刷入真机

    编译环境是Ubuntu12.04.手机nexus 5,编译安卓6.0.1源码并烧录到真机. 源码用的是科大的镜像:http://mirrors.ustc.edu.cn/aosp-monthly/,下载 ...

随机推荐

  1. jdbctemplate中的批量更新使用,BigDecimal与造型的联系和区别

    //jdbctemplate批量新增的使用MENU_ID_LIST是前端页面传递到后端控制层,再由控制层传到实现层的List //JdbcTemplate是spring jdbctemplate通过注 ...

  2. MyBatis支持的jdbcType类型

    BIT         FLOAT      CHAR           TIMESTAMP       OTHER       UNDEFINED TINYINT     REAL       V ...

  3. 让apache后端显示真实客户端IP

    公司是nginx做代理,后端的web服务用的是apache,然后我现在要分析日志,但是,我的apache日志上显示的是代理服务器的ip地址,不是客户的真实IP 所以这里我需要修改一下,让apache的 ...

  4. 每天一个 Linux 命令(6):rmdir 命令

    今天学习一下linux中命令: rmdir命令.rmdir是常用的命令,该命令的功能是删除空目录,一个目录被删除之前必须是空的.(注意,rm – r dir命令可代替rmdir,但是有很大危险性.)删 ...

  5. 比较实用的JavaScript库

    如果你操作过cookie的接口,那么你一定会感觉这东西的规范真的是太复杂了,根本记不住啊,其实你是对的,因为cookie的接口设计的是有问题的,也就是说设计的太底层了,根本不友好,那么来试试这个js库 ...

  6. android startActivityForResult的用法

    有时候我们需要把A activity提交数据给B  activity处理,然后把结果返回给A 这种方式在很多种情况需要用到,比如我应用的程序需要有拍照上传的功能. 一种解决方案是  我的应用程序 〉调 ...

  7. restful php

    http://bbs.phpchina.com/thread-228725-1-1.html http://www.cnblogs.com/artech/p/3506553.html http://w ...

  8. asp.net web form中 用attribute实现权限验证方式

    以前项目的代码比较陈旧,今天抽空优化了一下.作为记录. 以前每次请求一个方法都要验证是否登录 if xxx等  现在通过global文件中的改进 反射这个方法的属性是否需要权限 要的话先验证权限.以下 ...

  9. IOS UI 第十一篇: UITABLEVIEW

    DIY a tableviewcell :   - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *) ...

  10. 打开本页N秒后跳转页面

    在head标签里面 <meta http-equiv="refresh" content="4;url=" />