本文博客链接:http://blog.csdn.net/qq1084283172/article/details/57086486
0x1.手机设备环境
Model number: Nexus 5
OS Version: Android 4.4.4 KTU84P
Kernel Version: 3.4.0-gd59db4e
0x2.Android内核提取
查找Android设备的boot分区文件。高通芯片的设备可以通过下面的命令进行查找。
cd /home/androidcode/AndroidDevlop/modifyNexus5Boot
adb shell
ls -al /dev/block/platform/msm_sdcc.1/by-name
root权限下,用 dd 将其dump到Nexus 5手机的sdcard文件夹下,然后导出到文件 /home/androidcode/AndroidDevlop/modifyNexus5Boot 下:
adb shell
su
dd if=/dev/block/mmcblk0p19 of=/sdcard/boot.img
exit
exit
adb pull /sdcard/boot.img boot.img
使用abootimg工具对boot.img文件进行解包处理,解包之后得到的 zImage 文件即为Android的内核文件。
abootimg工具的github地址:https://github.com/ggrandou/abootimg
abootimg工具的直接安装命令:sudo apt-get install build-essential abootimg
abootimg -x boot.img
ls -al
将zImage文件拷贝一份作为文件名为 kernel.gz 的文件,并使用 WinHex 工具查找十六进制 1F 8B 08 00,找到之后把前面的数据部分全删掉,使kernel.gz文件变成标准的gzip压缩文件,这样子就可以使用
gunzip/gzip 命令进行解压内核文件了。
cp ./zImage ./kernel.gz
# 去掉解包的内核文件kernel.gz中前面的垃圾数据
gzip -d kernel_new.gz
ls -al
使用WinHex查找十六进制数据:
删除掉解包的内核文件kernel.gz中的前面的垃圾数据,然后重新保存修改后的 kernel.gz文件为 kernel_new.gz.
修改后的gzip格式的 kernel_new.gz 文件的解压得到kernel_new内核文件:
提示:关于gzip格式文件的解压,既可以使用 gzip 命令也可以使用 gunzip 命令,都一样。有关 gzip/gunzip 命令的参数使用说明,如下:
$ gzip -h
Usage: gzip [OPTION]... [FILE]...
Compress or uncompress FILEs (by default, compress FILES in-place).
Mandatory arguments to long options are mandatory for short options too.
-c, --stdout write on standard output, keep original files unchanged
-d, --decompress decompress
-f, --force force overwrite of output file and compress links
-h, --help give this help
-k, --keep keep (don't delete) input files
-l, --list list compressed file contents
-L, --license display software license
-n, --no-name do not save or restore the original name and time stamp
-N, --name save or restore the original name and time stamp
-q, --quiet suppress all warnings
-r, --recursive operate recursively on directories
-S, --suffix=SUF use suffix SUF on compressed files
-t, --test test compressed file integrity
-v, --verbose verbose mode
-V, --version display version number
-1, --fast compress faster
-9, --best compress better
--rsyncable Make rsync-friendly archive
With no FILE, or when FILE is -, read standard input.
Report bugs to <bug-gzip@gnu.org>.
关于gzip文件格式的说明和源码的解析可以参考 gzip文件格式解析及源代码分析,进行深入的研究和学习。
0x3.Android内核文件的逆向修改
将解压后的Android内核文件 kernel_new 拖入到IDA Pro 中进行分析,设置处理器类型为ARM Little-endian。
在 ROM start address 和Loading address 处填上
0xc0008000,然后点击 OK 。
IDA显示效果如下图所示,没有函数名,不方便定位代码,显示不友好需要添加Android内核的内核符号。
为了要获取Android内核中所有的内核符号信息,可以通过在root权限下,修改Andriod设备中的/proc/sys/kernel/kptr_restrict 的值来实现,去掉Android内核符号的信息屏蔽。
adb shell
su
# 查看默认值
cat /proc/sys/kernel/kptr_restrict
# 关闭内核符号屏蔽
echo 0 > /proc/sys/kernel/kptr_restrict
# 查看修改后的值
cat /proc/sys/kernel/kptr_restrict
cat /proc/kallsyms
关闭Android设备的内核符号的屏蔽以后,再次执行 cat /proc/kallsyms ,发现被隐藏的内核符号信息都显示出来了。
在root权限下,将Android设备中的内核符号信息dump出来,导出到 /home/androidcode/AndroidDevlop/modifyNexus5Boot/syms.txt文件中。因此,Android内核文件的内核符号信息都保存在syms.txt文件中了。
# cat /proc/kallsyms > /sdcard/syms.txt
# exit
$ exit
$ adb pull /sdcard/syms.txt syms.txt
我们已经将Androd内核文件中的内核符号信息都dump出来,下面将大有用武之地。因此,向IDA中导入之前提取出来的内核符号信息就可以看到对应的函数名称了。需要用到下面的Python脚本:
ksyms = open("C:UsersFly2016DesktopAndroid内核的提取和逆向syms.txt")
for line in ksyms:
addr = int(line[0:8],16)
name = line[11:]
idaapi.set_debug_name(addr,name)
MakeNameEx(addr,name,SN_NOWARN)
Message("%08X:%sn"%(addr,name))
在IDA的 File->Script Command中运行上述python脚本,之后就可以在IDA中成功添加内核符号信息使IDA显示出正确的系统调用的函数名称来。
Android内核中隐藏的系统函数调用的名称在IDA中显示出来了。
现在来聊一聊修改Android的内核文件绕过反调试,很多的Android加固都会通过查看当前进程的 /proc/pid/status 的状态信息,来进行判断当前进程是否被调试的依据。如果当前进程被调试器所调试,那么cat /proc/self/status 显示的状态如下图所示,比较常见的Android反调试也就是通过 TracerPid 的值在调试状态和非调试状态的不同且非调试状态该值为0而调试状态为非0,来判断是否被调试器所调试。
这里修改Android内核绕过反调试也就只是考虑 TracerPid 的值不同的这种情况,真真的也过掉这些检测的反调试还是需要从具体的Android加固的检测逻辑代码入手,没准现在有些Android加固还会检测State的值的不同呢!修改Android内核绕过Android加固的反调试,其实还是要依赖具体的开源的Android内核代码来进行对照着分析,否则根本不知道哪个地方是 /proc/pid/status 的值根据调试状态改变的代码位置,因此这里通过修改Android内核文件绕过反调试还是基于Android内核源码文件 /kernel/msm/fs/proc/array.c 中 的代码实现进行对照着修改的。
/kernel/msm/fs/proc/array.c文件中,检测调试修改TracerPid的值的Android内核源码:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */
"S (sleeping)", /* 1 */
"D (disk sleep)", /* 2 */
"T (stopped)", /* 4 */
"t (tracing stop)", /* 8 */
"Z (zombie)", /* 16 */
"X (dead)", /* 32 */
"x (dead)", /* 64 */
"K (wakekill)", /* 128 */
"W (waking)", /* 256 */
};
static inline const char *get_task_state(struct task_struct *tsk)
{
unsigned int state = (tsk->state & TASK_REPORT) | tsk->exit_state;
const char * const *p = &task_state_array[0];
BUILD_BUG_ON(1 + ilog2(TASK_STATE_MAX) != ARRAY_SIZE(task_state_array));
while (state) {
p++;
state >>= 1;
}
return *p;
}
static inline void task_state(struct seq_file *m, struct pid_namespace *ns,
struct pid *pid, struct task_struct *p)
{
struct group_info *group_info;
int g;
struct fdtable *fdt = NULL;
const struct cred *cred;
pid_t ppid, tpid;
rcu_read_lock();
ppid = pid_alive(p) ?
task_tgid_nr_ns(rcu_dereference(p->real_parent), ns) : 0;
tpid = 0;
if (pid_alive(p)) {
struct task_struct *tracer = ptrace_parent(p);
if (tracer)
// 逆向Android内核文件需要关注的地方
tpid = task_pid_nr_ns(tracer, ns);
}
cred = get_task_cred(p);
seq_printf(m,
"State: %s
"
"Tgid: %d
"
"Pid: %d
"
"PPid: %d
"
"TracerPid: %d
"
"Uid: %d %d %d %d
"
"Gid: %d %d %d %d
",
get_task_state(p),
task_tgid_nr_ns(p, ns),
pid_nr_ns(pid, ns),
ppid, tpid,
cred->uid, cred->euid, cred->suid, cred->fsuid,
cred->gid, cred->egid, cred->sgid, cred->fsgid);
task_lock(p);
if (p->files)
fdt = files_fdtable(p->files);
seq_printf(m,
"FDSize: %d
"
"Groups: ",
fdt ? fdt->max_fds : 0);
rcu_read_unlock();
group_info = cred->group_info;
task_unlock(p);
for (g = 0; g < min(group_info->ngroups, NGROUPS_SMALL); g++)
seq_printf(m, "%d ", GROUP_AT(group_info, g));
put_cred(cred);
seq_putc(m, '
');
}
通过对Android内核源码的研究知道了我们在Android内核文件中修改的地方,在IDA中通过字符串搜索 TracerPid 即查找上面提到的特征字符串组。
在IDA中通过对特征字符串的引用功能可以定位到我们需要关注的代码的位置。
通过IDA的F5功能分析Android内核根据检测调试状态修改TracerPid值的代码位置。
通过IDA具体细致的看下,我们需要关注的代码位置处的ARM汇编指令。
通过逆向分析代码的流程可以知道,只要 ROM:C02BA5C0 EC FE FF 0A BEQ Jmp_C02BA178 处改为直接跳转到地址C02BA178处执行,没有机会执行下面的代码既可以绕过反调试检测。通过IDA的二进制修改的功能,实现了ARM汇编代码的修改,修改后的代码如下图:
Android内核文件kernel_new在修改前后的代码的对比结果示意图:
0x4.将逆向修改的Android内核刷回Android设备
对修改后的Android内核文件 kernel_new 进行gzip的压缩处理得到压缩文件 kernel_new.gz 。
# -n, --no-name do not save or restore the original name and time stamp
# -f, --force force overwrite of output file and compress links
# -9, --best compress better
gzip -n -f -9 kernel_new
使用WinHex工具将kernel_new.gz文件的二进制数据覆盖到原来的zImage文件的 1F 8B 08 00 处的位置开始到结束的地方(新的kernel_new.gz文件必须比原kernel_new.gz文件小,并且回写回去时不能改变原zImage文件的大小及修改原zImage文件中后面的内容,否则会很麻烦),这时得到了zImage文件。
上面这句话,可能不太好理解,但是也很好理解,可以参考一下作者 lcweik 给出的理解的例子:
通过WinHex工具查看kernel_new.gz文件的大小为 0x6AB190,zImage文件中 1F 8B 08 00 处的位置起始偏移为0x48B4,因此在zImage文件中kernel_new.gz文件的起始位置偏移为0x48B4,结束位置偏移为0x6AFA43。使用WinHex工具先将zImage文件中0x48B4~0x6AFA43处的数据删除,然后将kernel_new.gz文件中的数据全部拷贝到0x48B4~0x6AFA43的范围中,即zImage文件中偏移0x48B3后面的位置开始覆盖。
使用abootimg打包工具,重新对解包的boot.img的文件进行打包处理。
abootimg --create myboot.img -f bootimg.cfg -k zImage -r initrd.img
将修改后重新打包的 myboot.img镜像 文件,更新到Android设备上。
adb reboot bootloader
fastboot flash boot myboot.img
0x5.手机刷成砖的还原
直接修改Android内核的二进制文件比较危险,很容易导致Android设备变砖的。如果不幸Android设备变砖了,只需要将前面的步骤中备份的原始boot.img镜像文件重新输入Android设备即可。
adb reboot bootloader
fastboot flash boot boot.img
0x6.逆向修改Android内核的总结。
这篇博文主要是参考:逆向修改手机内核,绕过反调试,原文的作者方法说的很详细,但是我的操作步骤有些地方和原作者的不同。
1.找目标代码和目标函数的方法不同,原作者通过关闭Android设备中内核符号屏蔽然后拿到关键函数 proc_pid_status_和proc_pid_status_(获取调试器进程的pid)的系统调用的地址,在IDA进行查找定位到需要逆向分析的关键代码的位置。
2.在修改二进制代码绕过反调试的方法上,我和原作者修改的地方稍有一处不同,原作者的修改如下图。
3.按照作者的操作步骤,修改Andorid内核成功绕过反调试耳朵检测,但是我按照自己改进后的操作,修改Android内核成功但是刷机重启直接变砖,哈哈。说实话,这么逆向修改Android内核绕过反调试只是提供一种思路吧,实际干活是吃力不讨好而且要真的绕过这种反调试的检测还需要修改其他的地方,而且其他的检测位置修改也不方便。这种open 情况下的反调试检测,其实手动patch内存过掉也是很简单的事情。
0x7.关于ARM汇编BL指令的计算
ARM汇编下BL类指令的修改以及偏移的计算具体可以参考:【求助】arm指令BL指令对应的机器码问题、ARM中跳转指令BL/BLX偏移值计算规则
,由于在前面的操作步骤中涉及到B类跳转指令的修改,特此提到一下。提醒两点:1.一定要善于利用IDA能够显示ARM指令机器码的特点,2.在内存中ARM指令的存放是按小尾端存放的。
参考资料:
逆向修改手机内核,绕过反调试
<主要参考>