项目初衷

 正如标题看到的那样,本文所提到的技术均由于诸多原因未能实现其目的,所以您可以理解为这是一篇对于古早bypass技术的探究,亦或者是初入android内核小白的自我摸索之旅。

 最初的目的是想要修改android kernel 的方式实现bypass root检测、frida检测,但是由于技术陈旧亦或者对kernel不熟悉,导致了诸多问题,这个最后会在文末做一一汇总

  • kernel 3.* 修改TracerPid
  • android 11 修改ro.system.build参数
  • root检测常用手段
  • 内核编译
  • android系统编译

tracePid 修改

*注:此项修改只适用于kernel 3+,且已经属于弃用的绕过手段,此处也只做记录学习;实战环境中此方案无法绕过frida的检测

Linux文件

/proc/[pid]/wchan,显示进程sleep时,kernel当前运行的函数;调试状态下,wchan文件会显示ptrace_stop

/proc/[pid]/cmdline,文件存放进程的命令内容;使用GDB这类的调试器时,调试会fork一个子进程,然后执行ELF程序;此时,ELF文件就可以通过getppid()获取父进程pid,然后检测cmdline中是否存在gdb关键字,来判断程序的调试状态。

/proc/[pid]/stat,文件包含对应进程的状态信息,主要用于ps命令。此文件由kernel源文件kernel/msm/fs/proc/array.c定义。当进程处于调试状态时,在stat文件的第三个字段处,会显示小写t,表示Tracing stop.(适用于Linux 2.6.33之后,也就是Android 2.3.x之后的版本)

/proc/[pid]/statm,该文件包含对应进程的页面内存状态信息。

/proc/[pid]/status,文件是/proc/[pid]/stat和/proc/[pid]statm两文件的集合,更易阅读。status文件除了TracerPid字段外,第二行的state字段也能够用来判断进程是否处于被调试状态(Tracing stop)

反调试相关的文件除了/proc/[pid]目录,在/proc/[pid]/task/tid/目录下应该有线程的状态信息文件,同样可以用来做反调试检测

bypass原理

基本原则是修改array.c、base.c绕过检测TracerPid、State的反调试

kernel 差异

kernel 3.* 与 kernel 4.* 修改上存在明显的差异,下方列举了修改tracePid时,kernel不同版本的差异性

fs/proc/array.c

kernel 4.9 task_state 函数构造如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
static inline void task_state(struct seq_file *m, struct pid_namespace *ns,
struct pid *pid, struct task_struct *p)
{
struct user_namespace *user_ns = seq_user_ns(m);
struct group_info *group_info;
int g, umask;
struct task_struct *tracer;
const struct cred *cred;
pid_t ppid, tpid = 0, tgid, ngid;
unsigned int max_fds = 0;

rcu_read_lock();
ppid = pid_alive(p) ?
task_tgid_nr_ns(rcu_dereference(p->real_parent), ns) : 0;

tracer = ptrace_parent(p);
if (tracer)
tpid = task_pid_nr_ns(tracer, ns);

tgid = task_tgid_nr_ns(p, ns);
ngid = task_numa_group_id(p);
cred = get_task_cred(p);

umask = get_task_umask(p);
if (umask >= 0)
seq_printf(m, "Umask:\t%#04o\n", umask);

task_lock(p);
if (p->files)
max_fds = files_fdtable(p->files)->max_fds;
task_unlock(p);
rcu_read_unlock();

seq_printf(m, "State:\t%s", get_task_state(p));

seq_put_decimal_ull(m, "\nTgid:\t", tgid);
seq_put_decimal_ull(m, "\nNgid:\t", ngid);
seq_put_decimal_ull(m, "\nPid:\t", pid_nr_ns(pid, ns));
seq_put_decimal_ull(m, "\nPPid:\t", ppid);
seq_put_decimal_ull(m, "\nTracerPid:\t", tpid);
seq_put_decimal_ull(m, "\nUid:\t", from_kuid_munged(user_ns, cred->uid));
seq_put_decimal_ull(m, "\t", from_kuid_munged(user_ns, cred->euid));
seq_put_decimal_ull(m, "\t", from_kuid_munged(user_ns, cred->suid));
seq_put_decimal_ull(m, "\t", from_kuid_munged(user_ns, cred->fsuid));
seq_put_decimal_ull(m, "\nGid:\t", from_kgid_munged(user_ns, cred->gid));
seq_put_decimal_ull(m, "\t", from_kgid_munged(user_ns, cred->egid));
seq_put_decimal_ull(m, "\t", from_kgid_munged(user_ns, cred->sgid));
seq_put_decimal_ull(m, "\t", from_kgid_munged(user_ns, cred->fsgid));
seq_put_decimal_ull(m, "\nFDSize:\t", max_fds);

seq_puts(m, "\nGroups:\t");
group_info = cred->group_info;
for (g = 0; g < group_info->ngroups; g++)
seq_put_decimal_ull(m, g ? " " : "",
from_kgid_munged(user_ns, group_info->gid[g]));
put_cred(cred);
/* Trailing space shouldn't have been added in the first place. */
seq_putc(m, ' ');

#ifdef CONFIG_PID_NS
seq_puts(m, "\nNStgid:");
for (g = ns->level; g <= pid->level; g++)
seq_put_decimal_ull(m, "\t", task_tgid_nr_ns(p, pid->numbers[g].ns));
seq_puts(m, "\nNSpid:");
for (g = ns->level; g <= pid->level; g++)
seq_put_decimal_ull(m, "\t", task_pid_nr_ns(p, pid->numbers[g].ns));
seq_puts(m, "\nNSpgid:");
for (g = ns->level; g <= pid->level; g++)
seq_put_decimal_ull(m, "\t", task_pgrp_nr_ns(p, pid->numbers[g].ns));
seq_puts(m, "\nNSsid:");
for (g = ns->level; g <= pid->level; g++)
seq_put_decimal_ull(m, "\t", task_session_nr_ns(p, pid->numbers[g].ns));
#endif
seq_putc(m, '\n');
}

kernel 3.18 task_state 函数构造如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
static inline void task_state(struct seq_file *m, struct pid_namespace *ns,
struct pid *pid, struct task_struct *p)
{
struct user_namespace *user_ns = seq_user_ns(m);
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)
tpid = task_pid_nr_ns(tracer, ns);
}
cred = get_task_cred(p);
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",
get_task_state(p),
task_tgid_nr_ns(p, ns),
task_numa_group_id(p),
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));

task_lock(p);
if (p->files)
fdt = files_fdtable(p->files);
seq_printf(m,
"FDSize:\t%d\n"
"Groups:\t",
fdt ? fdt->max_fds : 0);
rcu_read_unlock();

group_info = cred->group_info;
task_unlock(p);

for (g = 0; g < group_info->ngroups; g++)
seq_printf(m, "%d ",
from_kgid_munged(user_ns, GROUP_AT(group_info, g)));
put_cred(cred);

seq_putc(m, '\n');
}

fs/proc/base.c 原始文件

kernel 4.9 proc_pid_wchan 函数构造如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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))
seq_printf(m, "%s", symname);
else
seq_putc(m, '0');

return 0;
}

kernel 3.18 proc_pid_wchan 函数构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 (lookup_symbol_name(wchan, symname) < 0)
if (!ptrace_may_access(task, PTRACE_MODE_READ))
return 0;
else
return seq_printf(m, "%lu", wchan);
else
return seq_printf(m, "%s", symname);
}

kernel 内核修改

base.c 文件

array.c

root 检测绕过

*注:本节所提到的技术均为参考应用root检测通杀篇所述技术,且最终未能成功实现

 android 手机root在渗透环境中是必不可少的一环,同样也是App攻防中的一个点;Magisk可以在Zygisk孵化阶段实现root的屏蔽,已达到在root环境下运行某些特殊App,但是该功能也会彻底的决断root场景下的渗透,类似于frida注入等

 虽然定制化的面具也能做到root检测的bypass,不过此阶段既然是研究android kernel,那么本文就从kernel层面去尝试绕过root检测

*本节适用于 kernel 4+,android 11

root检测手段

既然是绕过,那么就要了解一下市面上常见的root检测手段

特征目录扫描

如果看过MobSF的朋友,一定见过其对于root检测扫描的一条规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# MobSF 2.0

{
'desc': 'This App may have root detection capabilities.',
'type': 'string',
'string1': '.contains("test-keys")',
'string2': '/system/app/Superuser.apk',
'string3': 'isDeviceRooted()',
'string4': '/system/bin/failsafe/su',
'string5': '/system/sd/xbin/su',
'string6': '"/system/xbin/which", "su"',
'string7': 'RootTools.isAccessGiven()',
'level': 'good',
'match': 'string_or',
'input_case': 'exact',
'cvss': 0,
'cwe': '',
}

# MobSF 3.0

- id: android_detect_root
message: This App may have root detection capabilities.
input_case: exact
pattern:
- \.contains\("test-keys"\)
- \/system\/app\/Superuser.apk
- isDeviceRooted\(\)
- \/system\/bin\/failsafe\/su
- \/system\/bin\/su
- \/system\/xbin\/su
- \/sbin\/su
- \/system\/sd\/xbin\/su
- '"\/system\/xbin\/which", "su"'
- RootTools\.isAccessGiven\(\)
severity: good
type: RegexOr
metadata:
cvss: 0
cwe: ''
masvs: resilience-1
owasp-mobile: ''
ref: https://github.com/MobSF/owasp-mstg/blob/master/Document/0x05j-Testing-Resiliency-Against-Reverse-Engineering.md#testing-root-detection-mstg-resilience-1

上述的规则罗列出来的各种目录,其实是root的一些特征机制,类似于:magisk、SuperSU

而App在做运行环境检测时,一种方案就是扫描特定的目录,查询是否存在特征文件

root操作执行

 App检测root的另一种方案很简单粗暴,那就是自己去主动的root后才能做得操作,例如:

  1. 在/data、/system、/etc等目录下尝试新建文件
  2. 执行su、find、mount等命令

 当然这个只是一种检测方案,在当前的市场监管环境下,这么检测无疑是自杀行为,总不能在《隐私政策》中写:我们为了检测当前App运行环境是否可信,故而申请root权限;监管砍了捏紧了手里的大刀

所以该方案只是本节记录,生产环境中很难遇到这种情况

读取手机状态

 前文# Android kernel 入坑之旅我最后编译的userdebug模式,是一种自带root的系统模式,此种模式下,无需Magisk刷机,自带root功能(只有kernel是userdebug,才能在command命令行下使用adb root

 所以如果App检测root时,只是单一的检测Magisk、SuperSu等root工具的特征,那userdebug自身技能逃避一定的检测规则,故而有了App需要读取手机编译版本、调试状态来判断是否是root

 方法其实也很简单,通过读取/system/build.prop文件,获取其中的特定参数值,即可做到最简单的判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# Pixel 3XL 为例

crosshatch:/ # cat /system/build.prop

# begin common build properties
# autogenerated by build/make/tools/buildinfo_common.sh
ro.system.build.date=Tue Mar 30 21:41:10 UTC 2021
ro.system.build.date.utc=1617140470
ro.system.build.fingerprint=google/crosshatch/crosshatch:11/RQ2A.210505.002/7246365:user/release-keys
ro.system.build.id=RQ2A.210505.002
ro.system.build.tags=release-keys
ro.system.build.type=user
ro.system.build.version.incremental=7246365
ro.system.build.version.release=11
ro.system.build.version.release_or_codename=11
ro.system.build.version.sdk=30
ro.product.system.brand=google
ro.product.system.device=generic
ro.product.system.manufacturer=Google
ro.product.system.model=mainline
ro.product.system.name=mainline
# end common build properties
# begin build properties
# autogenerated by buildinfo.sh
ro.build.id=RQ2A.210505.002
ro.build.display.id=RQ2A.210505.002
ro.build.version.incremental=7246365
ro.build.version.sdk=30
ro.build.version.preview_sdk=0
ro.build.version.preview_sdk_fingerprint=REL
ro.build.version.codename=REL
ro.build.version.all_codenames=REL
ro.build.version.release=11
ro.build.version.release_or_codename=11
ro.build.version.security_patch=2021-05-05
ro.build.version.base_os=
ro.build.version.min_supported_target_sdk=23
ro.build.date=Tue Mar 30 21:41:10 UTC 2021
ro.build.date.utc=1617140470
ro.build.type=user
ro.build.user=android-build
ro.build.host=abfarm-00920
ro.build.tags=release-keys
ro.build.flavor=crosshatch-user
# ro.product.cpu.abi and ro.product.cpu.abi2 are obsolete,
# use ro.product.cpu.abilist instead.
ro.product.cpu.abi=arm64-v8a
ro.product.cpu.abilist=arm64-v8a,armeabi-v7a,armeabi
ro.product.cpu.abilist32=armeabi-v7a,armeabi
ro.product.cpu.abilist64=arm64-v8a
ro.product.locale=en-US
ro.wifi.channels=
# ro.build.product is obsolete; use ro.product.device
ro.build.product=crosshatch
# Do not try to parse description or thumbprint
ro.build.description=crosshatch-user 11 RQ2A.210505.002 7246365 release-keys
# end build properties

#
# ADDITIONAL_BUILD_PROPERTIES
#
ro.treble.enabled=true
net.bt.name=Android

crosshatch:/ #

上述查询结果中

ro.system.build.tags【手机编译版本】:release-keys

kernel 内核修改

既然大致知道了厂商检测root的手段,那么本节开始着手去针对性的修改内核来尝试绕过检测

change “su”

最简单的修改就是把su这个明显的特征去掉,

/system/extras/su/Android.mk中的LOCAL_MODULE值为自定义数据

system/core/libcutils/fs_config.cpp中原有的 /system/xbin/su 修改末尾su为刚刚LOCAL_MODULE的值

system/sepolicy/private/file_contexts中原有的 /system/xbin/su 修改末尾su为刚刚LOCAL_MODULE的值

change ro.build.tags

 以及编译脚本build/make/tools/buildinfo.sh 中 echo ro.build.tags 的取值为BUILD_VERSION_TAGS,而 BUILD_VERSION_TAGS 该参数的赋值是由 build/core/Makefile 文件负责

此时只需要修改 build/core/Makefile 该文件中的 BUILD_KEYS 值为 release-keys 即可(原值为:test-keys)

change userdebug

 截止到目前为止,已经修改了:su、ro.build.tags

 接下来需要修改的是内核编译时,userdebug的标识,此标识如果不修改,会导致ro.build.type、ro.build.display.id、ro.build.flavor、ro.build.description、ro.build.fingerprint标签中均带有userdebug,但是release版本对应的上述标签应为user

 同理,还是从编译脚本build/make/tools/buildinfo.sh 中 echo 各类标识参数来判断

ro.build.type

 ro.build.type 的取值为 TARGET_BUILD_TYPE,而在Makefile中,存在一个判断逻辑

1
$(hide) TARGET_BUILD_TYPE = "$TARGET_BUILD_VERIANT"

 此判断逻辑会导致原本在Makefile文件中修改的值,需要额外跳转到新的文件修改,所以直接修改该if逻辑,将跳转的值直接该为user

ro.build.flavor

 ro.build.flavor 的取值为 TARGET_BUILD_FLAVOR,在Makefile中,对该值有如下逻辑

1
TARGET_BUILD_FLAVOR := $(TARGET_PRODUCT)-$(TARGET_BUILD_VARIANT)

 咳咳,此处的“-”这个符号不是减号哈,而是一个字符串,得出这个结论的依据可以将目光移到“/system/build.prop”文件中的“ro.build.flavor=crosshatch-user”值,所以在Makefile文件中TARGET_BUILD_FLAVOR 的拼接逻辑只要改成 TARGET_BUILD_FLAVOR := $(TARGET_PRODUCT)-user 即可

残留项目

 目前存在俩个ro的残留未作修改:ro.build.description、ro.build.fingerprint

 ro.build.description 的取值为 PRIVATE_BUILD_DESC,Makefile中对应的逻辑如下

1
2
3
4
5
6
7
8
9
10
11
## line 118

# A human-readable string that descibes this build in detail.

build_desc := $(TARGET_PRODUCT)-$(TARGET_BUILD_VARIANT) $(PLATFORM_VERSION) $(BUILD_ID) $(BUILD_NUMBER) $(BUILD_VERSION_TAGS)

$(INSTALLED_BUILD_PROP_TARGET): PRIVATE_BUILD_DESC := $(build_desc)

## line 211

PRIVATE_BUILD_DESC = "($PRIVATE_BUILD_DESC)"

 ro.build.fingerprint 则略有不同,fingerprint指纹信息在android 9 之后官方已经将其移除,diff如下

 由于涉及到需要修改build_desc的值,而该值可能会涉及到系统文件的读取,所以目前不对这俩个参数最修改,看后续绕过root检测时,是否会受影响

刷机

内核编译

 上述修改已经完成,只需要编译android 系统以及内核即可,其实具体的编译可以查看本人之前的博客,不过为了方便,此处再走一遍

 kernel 的选择不清楚的,可以看官方文档:builds-kernel,该文章只限于谷歌系手机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# down kernel

# 使用国内源
export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo'

# repo 初始化内核
~/bin/repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/kernel/manifest -b android-msm-crosshatch-4.9-android11-qpr2

# repo 内核源码下载
~/bin/repo sync

# 编译工具下载
cd prebuilts

git clone https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/kernel/prebuilts/build-tools

mv build-tools kernel-build-tools

# 引入环境变量
export PATH=/home/kali/Desktop/compile/Pixel_3XL/kernel/prebuilts/kernel-build-tools/linux-x86/bin:$PATH

# 切换分支 (cat /proc/version 查看手机内核后,g字母后的数字为本机分支)
cd kernel/private/msm-google

git checkout 4291d86870f1

# 开始编译
./build/build.sh

# 编译大约20分钟左右,编译结果:Image.lz4
ls /home/kali/Desktop/compile/Pixel_3XL/kernel/out/android-msm-pixel-4.9/dist

内核编译完成,以及结果如下图所示

android 编译

 内核编译完成后,将编译结果复制到android源码中,对android进行编译即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 复制内核
cp /home/kali/Desktop/compile/Pixel_3XL/kernel/out/android-msm-pixel-4.9/dist/Image.lz4 device/google/crosshatch-kernel/Image.lz4

# 下载系统硬件库 - https://developers.google.com/android/drivers;下载之前,切记一定要看一下自己手机的硬件版本,上文中提到的,ro.build.display.id即可
tar -zxvf google_devices-crosshatch-rq3a.211001.001-3dd43cb6.tgz

mkdir devices

# 硬件库解压后是一个.sh的运行文件
mv extract-google_devices-crosshatch.sh devices

# 执行.sh文件后,会生成一个vendor的文件夹,将其复制到android aosp根目录即可
cp -rf vendor /home/kali/Desktop/compile/Pixel_3XL/android-11

# 生成编译环境
source build/envsetup.sh

# 选择需要编译的系统
lunch aosp_crosshatch-userdebug

# 开始编译
make -j8

 android系统正式开始编译之后,打开一部电影等着吧,目前我自己台式机 3600处理器,需要编译2 - 3个点,诚挚建议,千万别在凌晨编内核,别问,问就是泪

 编译成功如下图所示

报错汇总

no valid slot to boot

烧录android boot.img之后,无论是重启还是关机,都会无限制停留在bootloader模式,这种情况根本原因是编译android内核时,并没有把硬件库(vendor)导进去,你可以看一下自己的aosp根本目下是否存在vendor这个文件夹

如果没有,那么按照如下步骤操作即可

1
2
3
4
5
6
7
8
9
10
11
12
13
# 清空原来编译的结果
make clean

# 下载系统硬件库 - https://developers.google.com/android/drivers;下载之前,切记一定要看一下自己手机的硬件版本,上文中提到的,ro.build.display.id即可
tar -zxvf google_devices-crosshatch-rq3a.211001.001-3dd43cb6.tgz

mkdir devices

# 硬件库解压后是一个.sh的运行文件
mv extract-google_devices-crosshatch.sh devices

# 执行.sh文件后,会生成一个vendor的文件夹,将其复制到android aosp根目录即可
cp -rf vendor /home/kali/Desktop/compile/Pixel_3XL/android-11

当然,如果是跟着本文刷机这一章节操作的,就不会出现这个问题

总结

 本文其实写了俩个目标:frida 检测绕过、root 检测绕过

 作为安全对抗的主战场,无疑本文提到的俩种技术已经太过于陈旧,且由于技术的不纯熟,导致撰写本文时,博主自身的技术水准只能做到生硬的套用,无法理解内在的修改逻辑,所以有了如下的错误

1、root检测绕过时,修改build/core/Makefile后导致编译到末尾阶段 “# ninja failed with: exit status 1”

2、root检测绕过时,修改各文件su特征符号后,依旧是编译失败

有错才有进步,还是基础以及技术不够呀,与君共勉吧。

参考文档

Linux /proc目录下和ELF反调试有关的文件

应用root检测通杀篇