Android Kernel Patch

Android逆向中,我们分析app时常常遇到如下问题:

  1. APP获取了哪些设备信息
  2. APP怎么检测调试器
  3. APP怎么检测Frida
  4. APP怎么检测root环境

同时在检测到该行为,并定位到具体代码位置后,我们如何去修改设备信息、隐藏调试器、隐藏Frida又是一个问题。

比如传统方法检测调试器是检查 /proc/pid/status 文件,调试器附加进程后,TracerPid字段的值是调试器的 PID ,要想绕过该反调试,需要解决如下问题:

  1. 如何定位代码
  2. 如何修改绕过,修改里面的 PID

定位代码可以依靠逆向工程或者Frida hook等,但是遇到混淆、SVC等会遇到问题,且针对性不强,不够通用,如果用 frida hook libc的函数,遇到 syscall 时就无法使用了。

因此,解决问题的关键是如何定位系统调用位置并修改系统调用逻辑。

Android Linux Kernel 定制

这就涉及到 Android Linux Kernel 定制方案,具体如下:

监测 修改 动态加载/卸载 需要内核源码/头文件/重新编译内核 开发难度
内核源码定制
ko内核模块
eBPF(监测) 是(有些也不用)
eBPF(修改) 是(默认不能修改返回值)
Kernel Patch
Binary Patch

基于 eBPF 开发的开源工具

https://github.com/SeeFlowerX/stackplz

stackplz是一款基于eBPF的堆栈追踪工具,目前仅适用于Android平台

特性:

  • 支持arm64 syscall trace,可以打印参数(包括详细的结构体信息)、调用栈、寄存器
  • 支持对64位用户态动态库进行uprobe hook,可以打印参数、调用栈、寄存器
  • 支持硬件断点功能,可以打印调用栈、寄存器,并且提供了frida rpc调用
  • 支持进程号、线程号、线程名的黑白名单过滤
  • 支持追踪fork产生的进程

要求:

  • root权限,系统内核版本5.10+(可执行uname -r查看)
  • 对于4.1x的内核,内核开启了CONFIG_HAVE_HW_BREAKPOINT,硬件断点功能同样可以使用

不仅仅是真机,这些环境下也可以使用:

  • arm开发板刷安卓镜像
  • arm开发板/云服务器 + Docker + ReDroid
  • Apple M系列设备 + 安卓官方arm64模拟器
  • 有root权限,内核版本5.10+的云真机也可以

eBPF非常适合做监测类任务,但是不适合做修改类任务:

  1. 很难修改寄存器或参数的值
  2. 很难修改返回值(CONFIG_BPF_KPROBE_OVERRIDE 配置未开)
  3. 对用户态内存只能修改可写内存

Android Linux Kernel 定制工作流

由于eBPF适合监测任务,因此考虑

  1. 用 eBPF(stackplz)进行监测
  2. 分析修改定制逻辑
  3. 编写 APatch kpm 模块 hook 内核

APatch

https://github.com/bmax121/APatch

  1. 一种新的 root 方案,基于 Patch 内核实现
  2. 提供内核模块机制,支持动态加载、动态卸载、符号管理等
  3. 提供 syscall hook, inline hook 接口,方便定制修改内核
  4. 核心是 Kernel Patch

安装 Kernel Patch

Linux Kernel 基础知识

  • android源码在线搜索:Android Code Search
  • 系统调用
  • kpm内核模块需要调用和hook某些内核函数
  • 使用目标函数的符号名来找到其函数地址:kallsyms_lookup_name,位于 ##include <linux/kallsyms.h>
  • 使用 zcat /proc/config.gz | grep -w CONFIG_KALLSYMS 查看设备是否有内核符号导出

    QQ_1739279267926.png

  • 获取所有导出符号 cat proc/kallsyms

  • 查找某个符号是否导出 cat proc/kallsyms | grep proc_task_name

    QQ_1739279400762.png

内核镜像逆向

为什么需要逆向Linux Kernel:

  1. 不一定找到对应的源码
  2. 设备内核可能内联了某些函数没法 hook
  3. 源码/反编译对照

步骤(设备是 Pixel 6 pro):

  1. 提取 boot.img:查看 boot 挂载点 ls /dev/block/bootdevice/by-name -l | grep boot,由前面的分析我们知道Android会把厂商部分和系统部分分开,我们关注 boot_a, boot_b 即可,可以看出分别被挂载在 sda13, sda21 两个块设备中。

    QQ_1739279767093.png

  2. 提取镜像文件:使用 dd if=/dev/block/sda13 of=/sdcard/Download/boot_a.img 提取 boot_a.img ,再用相同的方法提取 boot_b.img

    QQ_1739280912502.png

  3. 解压镜像文件提取 kernel:用 magiskboot工具解包内核镜像文件 https://github.com/svoboda18/magiskboot:`magiskboot unpack boot_a.img,解压得到kernel文件和ramdisk.cpio文件,我们重点关注kernel` 二进制文件

    QQ_1739282212151.png

  4. 重建符号表:用https://github.com/marin-m/vmlinux-to-elf把我们得到的镜像文件转化成ELF文件(Linux环境)`./vmlinux-to-elf kernel kernel_a.elf`,并得到带符号的内核文件

    QQ_1739289681893.png

  5. IDA载入:把 kernel_a.elf 拖入IDA,等待许久,内核文件符号表已全部恢复

    QQ_1739289859498.png

    QQ_1739289905518.png

如果是模拟器只需要三步:

  1. 直接在宿主文件系统里面找到 kernel文件(该文件是gz压缩后的)
  2. 添加 gz后缀并解压
  3. vmlinux-to-elf处理得到 elf文件

APatch修补

方法一:APatch UI 一键修补

方法二:magiskboot unpack boot.img → kptools修补 → magiskbook repack boot.img

然后执行fastboot flash boot boot.img

Kernel Patch 开发

开发环境

Ubuntu22.04+ clangd + bear + arm-gnu-toolchain

arm64交叉编译工具链下载地址:Arm GNU Toolchain Downloads – Arm Developer 找到对应平台的即可,我这里使用的是 AArch64 bare-metal target (aarch64-none-elf)

编译时临时添加环境变量 export TARGET_COMPILE=/home/ep/Desktop/arm-gnu-toolchain-14.2/bin/aarch64-none-elf-即可。

第一次编译使用 bear -- make -B 生成 compile_commands.json 文件辅助 clangd即可(可以把makefile里面的TARGET_COMPILE去掉先用gcc编译然后再用交叉编译。

之后可以直接执行 make push 推送到手机设备。

入口函数解析

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
static long eps1l0h_hide_init(const char *args, const char *event, void *__user reserved)
{
pr_info("eps1l0h-hide init, event: %s, args: %s\n", event, args);
pr_info("kernelpatch version: %x\n", kpver);


pr_info("eps1l0h-hide install\n");
return 0;
}

static long eps1l0h_hide_control0(const char *args, char *__user out_msg, int outlen)
{
pr_info("eps1l0h-hide control0, args: %s\n", args);
char echo[64] = "echo: ";
strncat(echo, args, 48);
compat_copy_to_user(out_msg, echo, sizeof(echo));
return 0;
}

static long eps1l0h_hide_control1(void *a1, void *a2, void *a3)
{
pr_info("eps1l0h-hide control1, a1: %llx, a2: %llx, a3: %llx\n", a1, a2, a3);
return 0;
}

static long eps1l0h_hide_exit(void *__user reserved)
{
pr_info("eps1l0h-hide exit\n");
return 0;
}

KPM_INIT(eps1l0h_hide_init); // 装载回调
KPM_CTL0(eps1l0h_hide_control0); // 控制0回调
KPM_CTL1(eps1l0h_hide_control1); // 控制1回调
KPM_EXIT(eps1l0h_hide_exit); // 卸载回调

比较重要的就是 eps1l0h_hide_init 装载回调,用来在里面添加hook代码,在 eps1l0h_hide_exit 卸载回调里面清除回调并释放内存等。控制 0/1 回调涉及到用户控制,先不管。

用 Kernel Patch hook 系统调用

kpm 安装syscall hook

安装

1
2
3
4
5
6
7
hook_err_t fp_hook_syscalln(int nr, int narg, void *before, void *after, void *udata)
/*
nr:系统调用号
narg:参数个数
before:系统调用执行前回调
after:系统调用执行后回调
*/

卸载

1
2
3
4
5
6
hook_err_t fp_unhook_syscall(int nr, void *before, void *after)
/*
nr:系统调用号
before:系统调用执行前回调
after:系统调用执行后回调
*/

hook openat

image.png

  • libc/java 提供的文件操作最终都会走 openat 系统调用打开某个文件,比如检测 status / maps 文件,通常都是走这个系统调用
  • 其系统调用号为 56,有四个参数,我们关注第二个参数 *filename

开发流程:

  1. 在src目录下添加 svc_hook.c, svc_hook.h 文件,在makefile文件中添加 BASE_SRC += ./src/svc_hook.c
  2. 定义 svc_hook_openat_install, svc_hook_openat_uninstall ,在 main.c 中调用,负责模块的安装和卸载,安装用到之前提到的fp_hook_syscalln, fp_unhook_syscall,逻辑如下:

    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
    void svc_hook_example_install(void)
    {
    pr_info("eps1l0h-hide: svc_hook_example_install\n");
    /*
    __task_pid_nr_ns是一个内核函数,用来获取task信息,即pid,tgid等
    其被定义成函数指针
    enum pid_type
    {
    PIDTYPE_PID,
    PIDTYPE_TGID,
    PIDTYPE_PGID,
    PIDTYPE_SID,
    PIDTYPE_MAX,
    };
    struct pid_namespace;
    pid_t (*__task_pid_nr_ns)(struct task_struct *task, enum pid_type type, struct pid_namespace *ns) = 0;
    初始化的时候根据符号来获取其地址
    */
    __task_pid_nr_ns = (void *)kallsyms_lookup_name("__task_pid_nr_ns");
    if(!__task_pid_nr_ns) {
    pr_warn("eps1l0h-hide: __task_pid_nr_ns not found\n");
    }

    // fp_hook_syscalln 安装模块,__NR_openat是openat的系统调用号宏定义,其有4个参数,before_openat和after_openat是hook函数,NULL是数据
    hook_err_t err = fp_hook_syscalln(__NR_openat, 4, before_openat, after_openat, NULL);

    // hook_openat_status 表示是否正在执行hook逻辑
    // 其定义 int hook_openat_status = 0
    if (err) {
    pr_err("eps1l0h-hide: hook openat error: %d\n", err);
    } else {
    hook_openat_status = 1;
    pr_info("eps1l0h-hide: hook openat success\n");
    }
    }

    void svc_hook_example_uninstall(void)
    {
    pr_info("eps1l0h-hide: svc_hook_example_uninstall\n");

    // 根据 hook_openat_status 执行 fp_unhook_syscall
    if (hook_openat_status) {
    fp_unhook_syscall(__NR_openat, before_openat, after_openat);
    hook_openat_status = 0;
    pr_info("eps1l0h-hide: unhook openat success\n");
    }
    }
  3. 下面开始写 before_openat, after_openat 两个回调函数,重点是 before_openat

    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

    // hook_fargs4_t是一个结构体,表示有4个参数,由于是内核态读用户态参数,需要用syscall_argn读取,字符串用compat_strncpy_from_user进一步读取
    void before_openat(hook_fargs4_t *args, void *udata) {
    pr_info("eps1l0h-hide: before_openat\n");
    // 重点是读取openat的第一个参数filename,注意这里是用户态的指针,要加上__user进行区分
    const char __user *filename = (const char __user *)syscall_argn(args, 1);
    // int fd = (int)syscall_arng(args, 0);

    if(!filename) return;
    // 进一步读取用户态指针的内容存入buf
    char buf[1024];
    compat_strncpy_from_user(buf, filename, sizeof(buf));

    // 获取当前进程,current是get_current()的宏定义
    struct task_struct *task = current;
    pid_t pid = -1, tgid = -1;
    if (__task_pid_nr_ns) {
    pid = __task_pid_nr_ns(task, PIDTYPE_PID, 0); // 获取pid
    tgid = __task_pid_nr_ns(task, PIDTYPE_TGID, 0); // 获取tgid
    }

    // 如果字符串是backdoor-skip就修改返回值和skip_origin
    if(strstr(buf, "backdoor-skip")) {
    pr_info("eps1l0h-hide: before_openat, filename: %s, pid: %d, tgid: %d, skip\n", buf, pid, tgid);
    args->ret = -1;
    args->skip_origin = 1;
    } else {
    pr_info("eps1l0h-hide: before_openat, filename: %s, pid: %d, tgid: %d\n", buf, pid, tgid);
    }
    }

测试步骤:

  1. 添加环境变量 export TARGET_COMPILE=/home/ep/Desktop/arm-gnu-toolchain-14.2/bin/aarch64-none-elf-,运行 make push 推送到手机 sdcard

    QQ_1740148649827.png

  2. 打开 Apatch,加载写好的kpm模块,运行 adb logcat | grep eps1l0h-hide 查看是否安装成功

    QQ_1740148688478.png

  3. 但是我们发现这些日志都是Apatch的用户态日志,由于我们在kpm模块中打印的都是内核日志,所以无法使用logcat获取,需要获取root权限后使用 cat /proc/kmsg | grep eps1l0h-hide 。这时候就会发现输出了一万行日志,都是我们hook的信息。

    代码中使用的 pr_info内核级打印函数(底层基于 printk),其输出直接写入 内核环形缓冲区,而 logcat 仅捕获用户空间(Android应用层)的日志。

    QQ_1740148841767.png

  4. 进入 data/local/tmp ,尝试创建 backdoor-skip 文件,我们对该文件进行了处理,如果匹配到该文件名就直接跳过 openat 的调用,尝试发现无法创建该文件,而其他名字的文件都可以创建

    QQ_1740149045475.png

    QQ_1740149059978.png

用 Kernel Patch 隐藏调试器特征

常见的 debugger check 方法

  1. /proc/pid/statusTracerPid字段,该字段的生成相关代码如下:

    QQ_1740502897674.png

    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

    struct seq_file { // 关注该结构体的前四个字段,seq_file是一个很重要的字符串缓冲区
    char *buf; // 指向内存
    size_t size; // 表示最大内存空间
    size_t from;
    size_t count; // 表示长度
    size_t pad_until;
    loff_t index;
    loff_t read_pos;
    struct mutex lock;
    const struct seq_operations *op;
    int poll_event;
    const struct file *file;
    void *private;
    };

    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);
    ...

    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);

    ...

    seq_put_decimal_ull(m, "\nTracerPid:\t", tpid); // 该字段就是TracerPid

    ...
    }

    int proc_pid_status(struct seq_file *m, struct pid_namespace *ns,
    struct pid *pid, struct task_struct *task)
    {
    struct mm_struct *mm = get_task_mm(task);

    seq_puts(m, "Name:\t");
    proc_task_name(m, task, true);
    seq_putc(m, '\n');

    task_state(m, ns, pid, task);
    ...
    }
  2. /proc/pid/statusState 字段,get_task_statetask_state_array获取静态字符串,相关代码如下,重点关注 t (tracing stop)

    QQ_1740502897674.png

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    static const char * const task_state_array[] = {

    /* states in TASK_REPORT: */
    "R (running)", /* 0x00 */
    "S (sleeping)", /* 0x01 */
    "D (disk sleep)", /* 0x02 */
    "T (stopped)", /* 0x04 */
    "t (tracing stop)", /* 0x08 */
    "X (dead)", /* 0x10 */
    "Z (zombie)", /* 0x20 */
    "P (parked)", /* 0x40 */

    /* states beyond TASK_REPORT: */
    "I (idle)", /* 0x80 */
    };

    seq_puts(m, "State:\t");
    seq_puts(m, get_task_state(p));
  3. proc/pod/wchanwchan文件内容是内核中进程休眠位置对应的符号名称,比如等待调试器就是ptrace_stop,正常情况是字符"0" ,或者就是调试器的符号名称,具体如下:

    QQ_1740502775930.png

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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];

    if (!ptrace_may_access(task, PTRACE_MODE_READ_FSCREDS))
    goto print0;

    wchan = get_wchan(task);
    if (wchan && !lookup_symbol_name(wchan, symname)) {
    seq_puts(m, symname);
    return 0;
    }

    print0:
    seq_putc(m, '0');
    return 0;
    }`
  4. proc/pid/statps -A,箭头所指的字段正常是R,挂上调试器就是t ,该字段通过do_task_stat 获得,关键代码如下,state字段就在 (proc_task_name) 字段后面

    QQ_1740503054606.png

    1
    2
    3
    4
    5
    6
    7
    8
    static int do_task_stat(struct seq_file *m, struct pid_namespace *ns, struct pid *pid, struct task_struct *task, int whole) {
    ...
    seq_puts(m, " (");
    proc_task_name(m, task, false);
    seq_puts(m, ") ");
    seq_putc(m, state);
    ...
    }

hook seq_put_decimal_ull 函数过 TracerPid 检测

task_state 函数中,系统通过 seq_put_decimal_ull 来获取,该函数是和内核导出函数,可以通过符号来获取其运行时地址并对其进行 hook

QQ_1740392570950.png

其函数定义为

1
void seq_put_decimal_ull(struct seq_file *m, const char *delimiter, unsigned long long num)

task_state 中的调用为

1
seq_put_decimal_ull(m, "\nTracerPid:\t", tpid);

因此只需要先检测第二个参数是否为 TracerPid ,然后再修改其第三个参数,也就是 tpid 的值即可。

获取函数地址可以用 kallsyms_lookup_name 函数,再用 hook_warp3seq_put_decimal_ull修改第三个参数即可。

但是加载完模块以后发现根本就没调用,这就需要结合之前提取的内核文件的逆向结果来进行分析了,可以看出在我们这个镜像里 tsak_state 函数是没有执行before_seq_put_decimal_ull的,而是直接调用了before_seq_put_decimal_ull_width,那么我们直接对before_seq_put_decimal_ull_width进行hook即可。

QQ_1740551351552.png

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
// 比较第二个参数,同时为了避免没有附加调试器的情况需要判断 fargs->arg2 != 0
// 由于是内核导出函数,所以参数也是内核态,直接调用即可
void before_seq_put_decimal_ull_width(hook_fargs4_t *fargs, void *udata) {
if (strcmp((char *)fargs->arg1, "\nTracerPid:\t") == 0) {
pr_info("eps1l0h-hide: before_seq_put_decimal_ull_width, TracerPid: %d\t\n", fargs->arg2);
fargs->arg2 = 0;
}
}

void *seq_put_decimal_ull = NULL;

void debugger_hide_install(void) {
pr_info("eps1l0h-hide: debugger_hide_install\n");
// 获取目标函数地址
seq_put_decimal_ull_width= (void *)kallsyms_lookup_name("seq_put_decimal_ull_width");

if (!seq_put_decimal_ull_width) {
pr_warn("eps1l0h-hide: seq_put_decimal_ull_widthnot found\n");
} else {
hook_err_t err = hook_wrap4(seq_put_decimal_ull_width, before_seq_put_decimal_ull, NULL, NULL);
if (err) {
pr_err("eps1l0h-hide: hook seq_put_decimal_ull_widthfailed\n");
seq_put_decimal_ull_width= NULL;
}
}
}

void debugger_hide_uninstall(void) {
if (seq_put_decimal_ull_width) {
unhook(seq_put_decimal_ull_width);
}
}

随意打开一个app,并挂上任意一个调试器,使用 cat /proc/pid/status 查看其 TracerPid ,并关注调试信息,发现调试信息中原本的 TracerPid 字段为 15498,但是查询出来变成了 0,说明hook成功。

QQ_1740484871424.png

QQ_1740484883407.png

QQ_1740484904173.png

hook seq_puts 函数过 State 检测

根据前面的分析, State字段是获取如下,那么只要 hook 了seq_puts 函数,查看是否是"t (tracing stop)", /* 0x08 */,然后将其修改为"S (sleeping)", /* 0x01 */即可。

1
2
seq_puts(m, "State:\\t");
seq_puts(m, get_task_state(p));

具体实现如下:

1
2
3
4
5
6
7
void before_seq_puts(hook_fargs2_t *fargs, void *udata) {
pr_info("eps1l0h-hide: before_seq_puts, %s\n", (const char*)fargs->arg1);
if(strcmp((const char*)fargs->arg1, "t (tracing stop)") == 0) {
pr_info("eps1l0h-hide: before_seq_puts, t (tracing stop)\n");
strcpy((char *)fargs->arg1, "S (sleeping)");
}
}

但是由于在测试机提取的内核文件中获取该字段的方法被内联了,所以暂时还无法进行 hook。

hook proc_pid_wchan 函数过 wchan 检测

proc_pid_wchan函数不是很好hook,因为 ptrace_may_access 如果随便hook会把系统搞崩,而且输入的参数也不好控制,注意到函数定义,其最后把结果存在了 struct seq_file *m 里面,这个seq_file 之前有提到过,在这里

1
2
3
4
5
6
static int proc_pid_wchan(struct seq_file *m, struct pid_namespace *ns, struct pid *pid, struct task_struct *task) {
...
seq_puts(m, symname);
...
seq_putc(m, '0');
}

seq_puts 函数最后会调用 seq_write 函数把数据写到 struct seq_file *m 里,可以看到写入的名称有多长,count变量就是多大。

1
2
3
4
5
6
7
8
9
10
int seq_write(struct seq_file *seq, const void *data, size_t len)
{
if (seq->count + len < seq->size) {
memcpy(seq->buf + seq->count, data, len);
seq->count += len;
return 0;
}
seq_set_overflow(seq);
return -1;
}

那么我们可以直接在函数执行后修改 struct seq_file *m 中的值,如果监测到 m->bufptrace_stop,就直接改为 "0"

1
2
3
4
5
6
7
8
9
10
void after_proc_pid_wchan(hook_fargs4_t *fargs, void *udata) {
struct seq_file *m = (struct seq_file *)fargs->arg0;
if (m && m->buf) {
if(strcmp((char *)m->buf, "ptrace_stop") == 0) {
pr_info("eps1l0h-hide: after_proc_pid_wchan, ptrace_stop\n");
strcpy(m->buf, "0");
m->count = 1;
}
}
}

挂上调试器测试,执行 cat /proc/pid/wchan,返回 "0",同时弹出日志,说明hook成功。

QQ_1740552328908.png

QQ_1740552355106.png

hook do_task_stat 函数过 stat 检测

由于该函数中是 seq_putc 获得的字段,只用了一个字符,不像 "t (tracing stop)"这么有区分度,所以入参判断需要注意一下,考虑到其前面必然跟着 ") ",可以基于此来进行 hook。

1
2
3
4
5
6
7
8
9
10
11
12
void after_do_task_stat(hook_fargs5_t *fargs, void *udata) {
struct seq_file *m = (struct seq_file *)fargs->arg0;
if (m && m->buf) {
for(size_t i = 0; i + 2 < m->count; i++) {
if(m->buf[i] == ')' && m->buf[i + 1] == ' ' && m->buf[i + 2] == 't') {
pr_info("eps1l0h-hide: after_do_task_stat, t\n");
m->buf[i + 2] = 'R';
break;
}
}
}
}

执行 cat /proc/pid/stat ,发现调试器标志被改成了 R,说明hook成功。

QQ_1740552517384.png

QQ_1740552450667.png

用 kernel patch 隐藏 frida 特征

  1. maps 文件特征 & 内存特征

    /proc/pid/maps 文件内容描述了进程的内存布局信息,frida会注入一共frida-agent模块,因此在maps里面能找到对应的内存映射信息。

    QQ_1740651131511.png

    检测代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    char line[512];
    FILE *fp;
    fp = fopen("/proc/self/maps", "r");
    if (fp) {
    while (fgets(line, 512, fp)) {
    if(strstr(line, "frida")) {
    // 检测到maps内存中有frida特征
    }
    }
    fclose(fp);
    }
  2. 线程名特征,使用如下命令检测,特征为gmain, gum-js-loop, gdbus, pool-frida, linjector,由 proc_pid_status 中的 proc_task_name 函数获得,但是该函数有些内核文件没有导出,可以进一步hook proc_task_name 里面的 __get_task_comm,该函数可以获取线程/进程名,导出符号

    1
    adb shell 'for taskdir in /proc/11198/task/*; do cat "$taskdir/status" | grep "Name:"; done'

    QQ_1740991763068.png

QQ_1740992158926.png

  1. D-BUS端口特征

    frida 注入后在目标进程监听 27042 端口,这个端口只有 adbd 进程连接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    for(i = 0;i <= 65535; i++) {
    sock = Socket(AF INET, SOCK_STREAM, 0);
    sa.sin_port = htons(i);
    if(connect(sock, (struct sockaddr* )&sa, sizeof sa) != -1) {
    __android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "FRIDA DETECTION [1]: 0pen Port: %d", i);
    memset(res, 0, 7);
    //Frida Server 使用 D-Bus 协议进行通信。正常服务在收到 AUTH 命令后应返回 REJECT(表示需要认证)。
    send(sock,"\x00"1NULL);
    send(sock,"AUTH\r\n"6,NULL);
    usleep(100);
    if(ret=recv(sock,res,6,MSG_DONTWAIT)!=-1){
    // 如果响应是 REJECT,则认为该端口运行了 Frida Server。
    if(strcmp(res,"REJECT")==){
    /* Frida server detected.Do something. */
    }
    }
    }
    close(sock);
    }

可以用 Envcheck来检测frida特征:

QQ_1740988639218.png

hook show_map_vma 函数修改 maps 文件过 maps 检测和内存特征检测

看一下源码,该函数作用是输出maps文件中的一段内存区域(一行)

get_vma_name 获取内存name

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
show_map_vma(struct seq_file *m, struct vm_area_struct *vma)
{
const struct path *path;
const char *name_fmt, *name;
vm_flags_t flags = vma->vm_flags;
unsigned long ino = 0;
unsigned long long pgoff = 0;
unsigned long start, end;
dev_t dev = 0;

if (vma->vm_file) {
const struct inode *inode = file_user_inode(vma->vm_file);

dev = inode->i_sb->s_dev;
ino = inode->i_ino;
pgoff = ((loff_t)vma->vm_pgoff) << PAGE_SHIFT;
}

start = vma->vm_start;
end = vma->vm_end;
show_vma_header_prefix(m, start, end, flags, pgoff, dev, ino);

get_vma_name(vma, &path, &name, &name_fmt);
if (path) {
seq_pad(m, ' ');
seq_path(m, path, "\n");
} else if (name_fmt) {
seq_pad(m, ' ');
seq_printf(m, name_fmt, name);
} else if (name) {
seq_pad(m, ' ');
seq_puts(m, name);
}
seq_putc(m, '\n');
}

可以在函数执行前记录下seq_file→count,执行后再根据当前内存段是否是frida相关的进行删除,只要恢复之前保存的count值就可以删除。

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
bool __attribute__((optimize("O0")))is_hidden_module(struct seq_file *m) {
const char * block_str[] = {
"frida-agent",
};

for (int i = 0; i < sizeof(block_str) / sizeof(block_str[0]); i++) {
if (strstr(m->buf, block_str[i])) { // 查看buf中是否有frida-agent符号
return true;
}
}
return false;
}

void before_show_map_vma(hook_fargs2_t *fargs, void *udata) {
struct seq_file *m = (struct seq_file *)fargs->arg0;
fargs->local.data0 = 0; // 临时变量,存储 count 值
if (m && m->buf) {
fargs->local.data0 = m->count;
}
}

void after_show_map_vma(hook_fargs2_t *fargs, void *udata) {
struct seq_file *m = (struct seq_file *)fargs->arg0;
if (m && m->buf) {
if (fargs->local.data0 && is_hidden_module(m)) { // 检查frida-agent内存特征
pr_info("eps1l0h-hide: after_show_map_vma, name: %s\n", (char *)m->buf);
m->count = fargs->local.data0; // 如果检查到就把这一行删掉
}
}
}

可以用 Envcheck app来对 hook 效果进行检测,挂上 frida 后,Maps记录检查被绕过

QQ_1740988484105.png

hook __get_task_comm 函数过线程检测

_get_task_comm 函数定义如下

1
2
3
4
5
6
7
8
9
 char *__get_task_comm(char *buf, size_t buf_size, struct task_struct *tsk)
{
task_lock(tsk);
- strncpy(buf, tsk->comm, buf_size);
+ /* Always NUL terminated and zero-padded */
+ strscpy_pad(buf, tsk->comm, buf_size);
task_unlock(tsk);
return buf;
}

buf中的就是name,可以after_hook把buf值替换成一些自定义字符。

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
bool __attribute__((optimize("O0")))is_hidden_comm(char * buf) {
const char * ban_names[] = {
"gum-js-loop",
"pool-frida",
"linjector",
"gmain",
"gdbus",
};

for (int i = 0; i < sizeof(ban_names) / sizeof(ban_names[0]); i++) {
if (strstr(buf, ban_names[i])) {
return true;
}
}

return false;
}

// 注意这,一定要加上__attribute__((optimize("O0"))),否则下面那部分会被优化成memset导致加载失败
void __attribute__((optimize("O0")))after_get_task_comm(hook_fargs3_t *fargs, void *udata) {
char *buf = (char *)fargs->arg0;
size_t buf_size = (size_t)fargs->arg1;
if (buf && buf_size && is_hidden_comm(buf)) {
pr_info("eps1l0h-hide: after_get_task_comm, name: %s\n", buf);
for (int i = 0; i < buf_size; i++) {
buf[i] = ' ';
}
}
}

再次检测一下,发现线程检测已经被过掉了

QQ_1740994157769.png

QQ_1740994253805.png

hook connect 过端口检测

既然知道是27042端口,直接对connect 系统调用进行 hook,当port是27042时,直接跳过该函数即可,其函数原型如下:

1
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 

可以对connect系统调用进行hook,如果检测到端口为27042,就直接跳过函数执行。

image.png