安卓内核编译和kernelsu的开发环境搭建
解决实际的报错的位置.
calleng@wd:/media/calleng/pm9a1/px6_backup202506/vmlinux-to-elf$
git log --since="2024-01-01" --until="2024-12-31" --oneline
da14e78 Merge pull request #64 from maringuu/master
c385e09 fix: Use raw strings for regex expressions
git show -s --format=%ci da14e78
2024-07-20 21:00:42 +0200
git show -s --format=%ci c385e09
2024-07-18 11:01:53 +0200
calleng@wd:/media/calleng/pm9a1/px6_backup202506/vmlinux-to-elf$ git checkout da14e78
注意:正在切换到 'da14e78'。
您正处于分离头指针状态。您可以查看、做试验性的修改及提交,并且您可以在切换
回一个分支时,丢弃在此状态下所做的提交而不对分支造成影响。
如果您想要通过创建分支来保留在此状态下所做的提交,您可以通过在 switch 命令
中添加参数 -c 来实现(现在或稍后)。例如:
git switch -c <新分支名>
或者撤销此操作:
git switch -
通过将配置变量 advice.detachedHead 设置为 false 来关闭此建议
HEAD 目前位于 da14e78 Merge pull request #64 from maringuu/master
calleng@wd:/media/calleng/pm9a1/px6_backup202506/vmlinux-to-elf$ git log -1
commit da14e789596d493f305688e221e9e34ebf63cbb8 (HEAD)
Merge: fa5c930 c385e09
Author: Marin <25437947+marin-m@users.noreply.github.com>
Date: Sat Jul 20 21:00:42 2024 +0200
Merge pull request #64 from maringuu/master
fix: Use raw strings for regex expressions
然后进行修补 得到 elf 文件的 符号列表
calleng@wd:/media/calleng/pm9a1/px6_backup202506/firmware/px4_factory/flame-tp1a.221005.002/output_dir$ ls ../../../../vmlinux-to-elf/
.git/ .gitignore kallsyms-finder@ LICENSE pics/ README.md setup.py* vmlinux-to-elf@ vmlinux_to_elf/
calleng@wd:/media/calleng/pm9a1/px6_backup202506/firmware/px4_factory/flame-tp1a.221005.002/output_dir$ f1613
(f1613) calleng@wd:/media/calleng/pm9a1/px6_backup202506/firmware/px4_factory/flame-tp1a.221005.002/output_dir$ ../../../../vmlinux-to-elf/vmlinux-to-elf kernel-4.14.276-android13-pixel4 kernel-4.14.276-android13-pixel4.elf
[+] Version string: Linux version 4.14.276-gecab2e0c9918-ab8931408 (android-build@abfarm-2004-0286) (Android (7284624, based on r416183b) clang version 12.0.5 (https://android.googlesource.com/toolchain/llvm-project c935d99d7cf2016289302412d708641d52d2f7ee), LLD 12.0.5 (/buildbot/src/android/llvm-toolchain/out/llvm-project/lld c935d99d7cf2016289302412d708641d52d2f7ee)) #1 SMP PREEMPT Wed Aug 10 20:40:15 UTC 2022
[+] Guessed architecture: aarch64 successfully in 7.27 seconds
[+] Found relocations table at file offset 0x262cec0 (count=161276)
[+] Found kernel text candidate: 0xffffff8008080000
[+] Successfully applied 161276 relocations.
[+] Found kallsyms_token_table at file offset 0x01f00900
[+] Found kallsyms_token_index at file offset 0x01f00d00
[+] Found kallsyms_markers at file offset 0x01eff900
[+] Found kallsyms_names at file offset 0x01d32100
[+] Found kallsyms_num_syms at file offset 0x01d32000
[i] Negative offsets overall: 0 %
[i] Null addresses overall: 0 %
[+] Found kallsyms_offsets at file offset 0x01cb2000
[+] Successfully wrote the new ELF kernel to kernel-4.14.276-android13-pixel4.elf
如何和课程的进行对比, 从这里的一些位置可以知道,
kernel-ranchu.elf 是 6.1 的 px6的模拟器的内核文件.
./vmlinux-to-elf/vmlinux-to-elf pixel/kernel pixel/kernel_a.elf 就是 px4的 物理机的 内核.
修改补丁的具体的位置.

通过图上看到, kernelsu 作为内核中的一个子模块一起编译的.
在 android-kernel 目录下, 执行补丁.
echo ‘[+] GKI_ROOT: /data/android-kernel ‘
现在最重要的问题, 就是, ksu 规模庞大, 先 apatch 适应. 作为 内核hook轻量级绕过.
hook_err_t err = fp_hook_syscalln(__NR_openat, 4, before_openat, after_openat, NULL );
if (err) {
pr_err("panda-hide: hook openat error : %d\n", err),
} else {
hook_openat_status = 1;
pr_info("panda-hide: hook openat success \n");
}
// fp_hook_syscalln(__NR_openat, 4, before_openat, after_openat, NULL );
// 对于 syscall 的hook 需要我们提供调用号
// nr , 系统调用号,
// narg: 参数个数.
// before , 系统调用前执行的回调
// after, 执行后的回调.
void before_openat(hook_fargs4_t *args , void *udata){ 多参数, 用户数据, 会直接传递到这里来.
// hook_fargs4_t ,表示 4个 参数
// 解析参数 使用 syscall_argn(args, 0 ) 读取出来.
// 字符串 用 compat_strcpy_from_user 进一步读取.
// args->skip_origin = 1 ; 跳过原调用. ==================> 跳过原始函数的执行, 相当于,阻止它的调用.! 在 ebpf 中 是很难实现的!
// strstr 位于< linux/string.h> 头文件(谨慎使用 c 库函数, 除非能在 kp 头文件中找到 )
// 调用内核函数_task_pid_nr_ns 获取 pid, tgid 信息.
//
// Frida 一般不一定使用 openat 打开.
// 重定向的做法, 一般来说是非常 low 的.
// hook openat 一般是让他不打开文件,
// 当他直接 打开 某些 文件时候,我们给他 block掉. 还有其他的一些检测的函数.
//
如果 failed , 常见的失败,怎么查找原因, 因为常见的符号 , 没有被导入进来 .
adb shell logcat | grep KP, ========> 来显示日志.
如果 没有使用到 memset , 这是编译优化引入的 builtin函数, 直接 KPM 拖到 IDA 中去看一看. 看看 memset 在哪里出现的? 明明没有使用 memset 但是, 编译器, 自动添加了, memset 这个, 特别在数组初始化的时候,
在 log 日志中, dmesg -w | grep KP , 可以阅读到,
添加 __attribute__((optimize(“o0”))) 就这样, 搞告诉编译器,不要优化 我们的这个函数了. 把 优化等级 改为 o0, 欧零 级别.!! 也可以把全局的 MakeFile 修改为 o0. 欧零.
看这个 KP 的日志, 这个目的, 就是告诉大家有没有其他的 error.
读取用户态字符串
oriole:/ # cat /proc/kallsyms | grep __arch_copy_from_user
0000000000000000 T __arch_copy_from_user
0000000000000000 r __ksymtab___arch_copy_from_user
0000000000000000 r __kstrtab___arch_copy_from_user
怎么去读取用户态的字符串的, 拷贝的到内核态里面. 如果需要读取一片固定的内存区域.
使用 , 字符串 , 使用 compat_strncpy_from_user , 这个在内核头文件里面是有导出的. 我们在用户态 时候, 需要去读取 用户态 addr_in 的结构体, 是 16个字节, 里面包含了 ip 和端口号, 这个使用,可以使用, __arch_copy_from_user
可以把 struct sockaddr_in 这个结构体, 给他读出来, 然后,我们再去解析这个端口.
调试器反检测
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"); // 输出的 第一个就是 name . 后面讲道 frida 检测的这个地方也会用到.
proc_task_name(m, task, true);
seq_putc(m, '\n');
task_state(m, ns, pid, task);
if (mm) {
task_mem(m, mm);
task_core_dumping(m, mm);
task_thp_status(m, mm);
mmput(mm);
}
task_sig(m, task);
task_cap(m, task);
task_seccomp(m, task);
task_cpus_allowed(m, task);
cpuset_task_status_allowed(m, task);
task_context_switch_counts(m, task);
return 0;
}
// /media/calleng/pm9a1/p6kernel/private/gs-google/fs/proc/array.c
Name 字段是由 proc_task_name 函数生成, 这个涉及 frida 检测.
TracePid 在 task_state 生成.
往下找, 就能找到 , TracePid 生成的地方.
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 = -1;
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) // /media/calleng/pm9a1/p6kernel/private/gs-google/fs/proc/array.c
tpid = task_pid_nr_ns(tracer, ns); // 这是调试器的 PID ---> tpid 就是在这里进行赋值.
tgid = task_tgid_nr_ns(p, ns);
ngid = task_numa_group_id(p);
cred = get_task_cred(p);
task_lock(p);
if (p->fs)
umask = p->fs->umask;
if (p->files)
max_fds = files_fdtable(p->files)->max_fds;
task_unlock(p);
rcu_read_unlock();
if (umask >= 0)
seq_printf(m, "Umask:\t%#04o\n", umask);
seq_puts(m, "State:\t"); // 检测 这个字段!! /proc/pid/status State 字段
seq_puts(m, 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); // 这里的就是 TracerPid 生成的地方.
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, ' ');
}
struct seq_file 缓冲器,
类似java里面的 一个 StringBuilder 的字符串 缓冲区, 不断的 构造数据, 然后,从缓冲区把数据取出来.
* buf 指向数据地址
count 指向数据尾部.
struct seq_operations;
// /media/calleng/pm9a1/p6kernel/private/gs-google/include/linux/seq_file.h
struct seq_file { // linux 中非常常见的缓冲区, 结构体是这样.
char *buf; // 用到的字段 1 ---> * buf 指向数据地址
size_t size; // 用到的字段 2 --> count 指向数据尾部.
size_t from; // 用到的字段 3
size_t count; // 用到的字段 4
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;
};
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
等下定义只要, 这个4个连续的字段, 因为在最前面. 因为它是连续的.
很多的虚拟的文件, 比如, maps 的内存映射, status 的文件 映射都使用 seq_file 缓冲区生成.
/proc/pid/status tracerpid 字段
/proc/pid/status State 字段
/proc/pid/wchan 文件
/proc/pid/stat 文件. 这个标志 ps -A 也可以查看.
比如这些缓冲区有一些的操作函数.比如.,
seq_puts
seq_put_decimal_ull // pull 一个 十进制的数
seq_putc //
或者还有格式字符串的操作都有.
通过 cat /proc/self/staus 可以看到自己的结构体定义.
130|oriole:/ # cat /proc/self/status
Name: cat // 这里第一个就是 Name
Umask: 0022
State: R (running)
Tgid: 13637
Ngid: 0
Pid: 13637
PPid: 7442
TracerPid: 2237
Uid: 0 0 0 0
Gid: 0 0 0 0
FDSize: 64
Groups:
VmPeak: 10829656 kB
VmSize: 10829504 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 3496 kB
VmRSS: 3496 kB
RssAnon: 668 kB
RssFile: 2592 kB
RssShmem: 236 kB
VmData: 8112 kB
VmStk: 136 kB
VmExe: 304 kB
VmLib: 3732 kB
VmPTE: 112 kB
VmSwap: 0 kB
CoreDumping: 0
THP_enabled: 1
Threads: 1
SigQ: 0/27701
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000080000000
SigIgn: 0000002000000000
SigCgt: 0000004c400084f8
CapInh: 000001ffffffffff
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 000001ffffffffff
NoNewPrivs: 0
Seccomp: 0
Seccomp_filters: 0
Speculation_Store_Bypass: thread vulnerable
Cpus_allowed: ff
Cpus_allowed_list: 0-7
Mems_allowed: 1
Mems_allowed_list: 0
voluntary_ctxt_switches: 0
nonvoluntary_ctxt_switches: 5
这个结构体我们会在后面,多次接触到.
我们 HOOK的时候, 只会使用4个字段
struct seq_file {
char *buf; // 指向了一片内存, 是存放数据的地方.
size_t size; // size 这片内存区域最大是多大?
size_t from;
size_t count; // 指向了数据的尾部. 有多少个字节.比如原来 buffer 是空的, put seq =c ,
// 我放一个字符进去, count就变成1 , 以此类推. count 表示, 我们buffer里面 有多少有效的数据.
// 我想要从 buffer 中 删除一些数据时候, 就可以从 cunt 来进行删除.
};
// 为了方便使用, 只需要前 4个字段. 将这个连续4个字段,定义出来,就可以用了.
// 只要不使用4个 之外字段, 都是可以的.
当我们想要修改一些数据的时候, 只需要,修改一下它的 count 来进行删除.
在 int proc_pid_status ( ) 下面可以找到, task_state 调用.
这里面会发现, 会放置一个 seq_put_decimal_ull () 十进制 无符号数进去.
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); // 这里的就是 TracerPid 生成的地方. \t 就是制表符, 把 tpid 给放进去.
这个数的名字 叫做 TracerPid.
目的: TracerPid 的 值永远改为 0.
如果hook, task_state() , 他里面生成了很多数据.
所以我们 hook, seq_put_decimal_ull( ) 这个函数.
同时,这是一个导出函数, 同时, 它是有个导出符号的,就是导出函数. 那么我们可以,定位它的运行时地址. 并 hook 上去.
oriole:/ # cat /proc/kallsyms | grep seq_put_decimal_ull
0000000000000000 T seq_put_decimal_ull_width
0000000000000000 T seq_put_decimal_ull
oriole:/ #
这里可以 hook 上 导出函数, 确定地址.
我们看到了源码之后, 设备上的内核, 是否也是这个情况?
确定一下,设备上的内核的, 导出到 IDA 中进行观看. proc_pid_status 这个导出函数,可以找到.< === 可读性很高,是 6.1的内核, 在 5.10的生产分支里面, 符号丢失 严重.
通过ida 反汇编 px6 的 boot_a 的槽位的 导出的 kernel 的 elf. 文件的符号. 确实是这样的.
__break(0x5512u);
seq_puts(a1, task_state_array[v22]);
seq_put_decimal_ull(a1, aTgid_1, v12);
seq_put_decimal_ull(a1, aNgid, 0LL);
v23 = pid_nr_ns(a3, a2);
seq_put_decimal_ull(a1, aPid_0, v23);
seq_put_decimal_ull(a1, aPpid, v10);
seq_put_decimal_ull(a1, aTracerpid, v11); < ========== 这个地方. hook ,虽然被 inline 了.
但是, seq_put_decimail_ull 这个函数已经被 inline,了.
__int64 __fastcall seq_put_decimal_ull(__int64 a1, __int64 a2, __int64 a3)
{
return seq_put_decimal_ull_width(a1, a2, a3, 0LL);
}
它的这里 定义的 是 3个参数.
hook思路, 直接读取 这个 3个参数的 seq_put_decimal_ull() 函数的的” 字符串[在第二参数位置]”, 看看是不是 包含 TracerPid的 这个字符串,如果是,那么 , 直接 第三个 参数修改为 0.
我们想要 这段代码执行的时候,
seq_put_decimal_ull(m, “\nTracerPid:\t”, tpid) 在
tpid 传递 进去的是 0.
在下面实现的,时候, 首先函数地址找到. 通过, kallsyms_lookup_name 来找到相对应的地址 .
找到后, 我们去hook _wrap3() –> 3 是参数的意思.
我们只需要, before hook 就可以了, 因为我们需要改它的参数. 改 参数时候, 我们就要在 before的时候去修改.
在 before 回调里面, 我们判断第一个参数,
void before_seq_put_decimal_ull(hook_fargs3_t *args, void *udata)
hook_fargs3_t 这个表示的是 3个参数. 读取,第1个字符串的指针,然后, 逐个, strcmp 函数, 进行比较, 如果是 TracerPid:\t” 这个东西 == 0 , 等于0 表示 是的.
&& args->arg2 != 0 , 并且传递进来的 tpid 不为 0 .
直接 hook 是一个内核函数, 我们syscall的字符串是 用户态传递进来的, 我们在内核态hook 不能直接读取用户态的 字符串, 我们, 直接使用, 一个内核态的函数. 可以直接. 去使用 字符串的地址.
strcmp 有个 定义, 如果 相等的话, 返回是 0, 不等 ,返回 1.
通过一些实践能力环节.===> 撒花, .—->
get_task_state的检测源码
static inline const char *get_task_state(struct task_struct *tsk)
{
BUILD_BUG_ON(1 + ilog2(TASK_REPORT_MAX) != ARRAY_SIZE(task_state_array));
return task_state_array[task_state_index(tsk)]; // 被它处理.
}
然后就是调试器的等待状态.
/*
* 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[] = {
/* states in TASK_REPORT: */
"R (running)", /* 0x00 */
"S (sleeping)", /* 0x01 */ // 可以修改为这个 sleep!
"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 */
};
大家搞逆向的, 就要多读源码!!
很多时候, 源码都读不懂, 还逆什么呢? 对把.
所以多多看一下源码,提升 代码 审计能力.
看了源码, 到 IDA 中, 反汇编, 实际 核对一下, 看看是否存在. 看看是否有这样的结构体存在.
看看这里.要么 改为 running, seleep 这种.
v17 = raw_spin_unlock(a4 + 2336);
_rcu_read_unlock(v17);
if ( (v15 & 0x80000000) == 0 )
seq_printf(a1, "Umask:\t%#04o\n", v15);
seq_puts(a1, aState); // State 在这里.使用了, seq_puts() , 本身作为参数,传递进去.
v18 = *(_DWORD *)(a4 + 48);
v19 = ((unsigned __int8)*(_DWORD *)(a4 + 1476) | (unsigned __int8)v18) & 0x7F;
if ( v18 == 1026 )
v19 = 128;
if ( v18 == 4096 )
v20 = 2;
else
v20 = v19;
v21 = 32 - __clz(v20);
if ( v20 )
v22 = v21;
else
v22 = 0;
if ( v22 >= 9 )
__break(0x5512u);
seq_puts(a1, task_state_array[v22]); // 这里也有一个 task_state_array , 同样调用 seq_puts!
seq_put_decimal_ull(a1, aTgid_1, v12);
seq_put_decimal_ull(a1, aNgid, 0LL);
v23 = pid_nr_ns(a3, a2);
seq_put_decimal_ull(a1, aPid_0, v23);
seq_put_decimal_ull(a1, aPpid, v10);
seq_put_decimal_ull(a1, aTracerpid, v11);
直接, hook seq_puts , 检测它的第二个参数,
这里反编译后的伪代码, 显示 都是 a1, 所以, seq_puts(a1, aState); 和 seq_puts(a1, task_state_array[v22]);
所以,
简化版本的源代码对比
static inline void task_state(struct seq_file *m, struct pid_namespace *ns,
struct pid *pid, struct task_struct *p)
{
.................. 省略 很多
if (umask >= 0)
seq_printf(m, "Umask:\t%#04o\n", umask);
seq_puts(m, "State:\t"); // 检测 这个字段!! /proc/pid/status State 字段
seq_puts(m, get_task_state(p)); // 这里的, get_task_state 从 task_state_array[] 获取静态字符串返回
// 调用 seq_puts 写入!
..................... 省略很多.
}
他们 都是在一起.
我们hook , seq_puts( ) 直接 before_hook 就可以了.
如果它要 puts 这样的一个字符串, 被我们检测到. 我们就把他们改为 runing 状态的字符串.
比如
void before_seq_puts(hook_fargs2_t *args, void *udata) {
if ( strcmp((const char *) args->arg1, "t (tracing stop)") == 0) {
pr_info("panda-debugger-hide: seq_puts:t\n");
args->arg1 = (uint64_t)"R (running)"; // 如果确实达到目标,直接在这里进行修改!
}
}
有个小问题, 这里引用, 执行完毕, 并没有被释放 , 可能导致 crash. 当我们字符串,卸载之后, 引用并没有消失.
这样就容易出现一些内核的crash. 不稳定的一些情况.
根据自己的内核实际考察一下.
测试环节, —-> 调试器 IDA 直接挂载, 直接返回, sleeping 的状态.
找一个ie幸运的进程,直接挂上一个调试器,
# ps -A |grep 5412
# 通过 IDA pro 挂载一个 进程, 5539. com.example.checkfrida. [choose process to attach to ]
# 它的进程的名字 是 5539.
# cat /proc/5539/?????????????? status 缺失什么? 5539.? status
# cat /proc/self/status
# 我们看到 , State: S(sleeping )
Name: mple.checkfirda // 这里第一个就是 Name
Umask: 0077
State: S (sleeping) <----------- 原来的, t (tracing stop)",变成了sleeping
Tgid: 13637
Ngid: 0
Pid: 13637
PPid: 7442
# 通过 dmesg -w | grep panda-hide 的输出.我们得到了, seq_puts: tracing stop 的输出 !
kpatch 修改很快,只要找到了修改点.
wchan 位置
wchan 文件内容是 内核中,进程休眠位置 对应的符号名称 ,比如 等待调试器就是 ptrace_stop, 正常情况下是 0.
#ifdef CONFIG_KALLSYMS
/*
* Provides a wchan file via kallsyms in a proper one-value-per-file format.
* Returns the resolved symbol. If that fails, simply return the address.
*/
static int proc_pid_wchan(struct seq_file *m, struct pid_namespace *ns, // 从定义看, 并没有找到合适的 hook 点.
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'); // 如果没有找到任何的函数,符号, 那么它返回的是 0
// . 是字符 0. 字符串,的最大长度,就是 1. 如果是正常的,他是不是只有 1个字符.
// 整个缓冲区里面, 只有这么一个字符?
return 0;
}
#endif /* CONFIG_KALLSYMS */
# 就是在 ptrace 的 stop 里面, 休息, 睡觉.
// 他里面其实也是 , ,struct seq_file ,
通过附加一个 phone上去, 就出来了.
oriole:/ # ps -ef | grep phone
radio 2317 854 0 06:58:04 ? 00:00:04 com.android.phone
root 15708 14753 23 16:02:12 pts/0 00:00:00 grep phone
oriole:/ # cat /proc/2317/wchan
ptrace_stop # 这里并没有换行符.
oriole:/ #
这就是 被 oriole forward tcp:23946 tcp:23946 附加了的调试器. ida dbgsrv,
所显现的状态.
struct seq_file {
char *buf; // 指向了一片内存, 是存放数据的地方. <===== 缓冲区它只有一个字符!
size_t size;
size_t from;
size_t count; // 指向了数据的尾部. 有多少个字节.
};
// buffer 也还是指向缓冲区的, count 也是等于 1 .
// hook 也是 after hook , 函数修改, 就是修改执行后的.
// 我们也是从 sq_file的 角度去修改.
我们只改, seq_file 的输出结果, 如果 , 改的话, 我们直接将他的缓冲区的数据改了.
所以我们 hook 的目标,就是 after hook了.
方法1 , before hook: 向 seq_file 写入 ‘0’, 字符, count +1
方法2, after hook : 覆盖 seq_file 数据, 写入 ‘0’, 把 count 改成 1.
为了代码的稳定性判断一下, if(m && m->buf) {,
m 不为0 , buffer , 也不为 0. 再继续往后,做, 否则,容易崩溃的.
我们把不想要的全部都改成0 ,了, 要不就是 1 把. ==========> 这里不可以,否则, 这个功能就废掉了.
把 ptrace_stop 等于这个值的 情况下, if() 在这个条件下, 我们把就 第[0]个字符,换为 0. 把 第[1]位的字符换位 \0 结尾. 然后 count 改为 1.
最后 pr_onfo 输出一下. 就是 after hook.
make 后, push
加载 模块, success 后,
oriole:/ # ps -ef | grep phone
radio 2317 854 0 06:58:04 ? 00:00:04 com.android.phone
root 15708 14753 23 16:02:12 pts/0 00:00:00 grep phone
oriole:/ # cat /proc/2317/wchan
0 # 这里并没有换行符.
oriole:/ # 被hook的 phone 的值, 直接变成 0 了.
# 进程之前是 ptrace_stop .
do_task_stat
最后一个,就是 do_task_stat 文件.
seq_putc 放入 1 字节
不能 hook seq_putc
不能特异性区分.
// /media/calleng/p6kernel/out/mixed/device-kernel/staging/lib/modules/5.10.198-android13-4-dirty/source/fs/proc/array.c
static int do_task_stat(struct seq_file *m, struct pid_namespace *ns,
struct pid *pid, struct task_struct *task, int whole)
{
// **************** 省略
state = *get_task_state(task);
// **************** 省略
seq_put_decimal_ull(m, "", pid_nr_ns(pid, ns));
seq_puts(m, " (");
proc_task_name(m, task, false);
seq_puts(m, ") ");
seq_putc(m, state);
// ****************** 省略
return 0;
}
// 怎么做.
// 检测 seq_file 内容
// after hook do_task_stat 检测有括号 + 空格后 的 1 字节是否 为 t.
static inline const char *get_task_state(struct task_struct *tsk)
{
BUILD_BUG_ON(1 + ilog2(TASK_REPORT_MAX) != ARRAY_SIZE(task_state_array));
return task_state_array[task_state_index(tsk)];
}
oriole:/ # ps -ef | grep phone
radio 2317 854 0 06:58:04 ? 00:00:04 com.android.phone
root 15708 14753 23 16:02:12 pts/0 00:00:00 grep phone
# 2317 就是电话进程.
oriole:/ # cat /proc/2317/stat
2317 (m.android.phone) t 854 854 0 0 -1 1077936448 18564 0 173 0 311 148 0 0 20 0 51 0 897 17719033856 39022 18446744073709551615 432752218112 432752222224 548713562224 0 0 0 4612 1 1073775864 0 0 0 17 0 0 0 5 0 0 432752231592 432752231616 433092550656 548713565899 548713565998 548713565998 548713570270 0
oriole:/ #
# phone) t 这里的 t 代表的,就是 进程正在被调试.
正常就是 r , running , 就是进程正在被调试.
使用 ps -A 命令也可以看到这一行.
oriole:/ # ps -A | grep phone
radio 2317 854 17303744 156088 ptrace_stop 0 t com.android.phone
oriole:/ #
也是可以看到这一行被调试!
Frida 检测
哪些 检测手段
maps 文件特征
内存特征
线程名特征
D-Bus 端口特征
maps 文件和内存
/proc/pid/maps 文件内容描述了进程的内存布局信息
frida 会注入一个 frida-agent 模块, 因此, 在 maps 里面能够找到对应的内存映射信息.
这里面能够找到一个 maps 文件.
能够找到内存的映射信息.
内核定位 : show_map_vma
show_map_vma 输出 maps 文件中 一段内存区域(一行)
static void
show_map_vma(struct seq_file *m, struct vm_area_struct *vma)
{
// ****************
start = vma->vm_start;
end = vma->vm_end;
// 输出内存区间范围,标志等
show_vma_header_prefix(m, start, end, flags, pgoff, dev, ino);
// 输出
/*
* Print the dentry name for named mappings, and a
* special [heap] marker for the heap:
*/
if (file) {
seq_pad(m, ' ');
seq_file_path(m, file, "\n"); // 输出路径相关
goto done;
}
if (vma->vm_ops && vma->vm_ops->name) {
name = vma->vm_ops->name(vma);
if (name)
goto done;
}
name = arch_vma_name(vma); // 获取内存区间 name
if (!name) {
struct anon_vma_name *anon_name;
if (!mm) {
name = "[vdso]";
goto done;
}
if (vma->vm_start <= mm->brk &&
vma->vm_end >= mm->start_brk) {
name = "[heap]";
goto done;
}
if (is_stack(vma)) {
name = "[stack]";
goto done;
}
anon_name = anon_vma_name(vma);
if (anon_name) {
seq_pad(m, ' ');
seq_printf(m, "[anon:%s]", anon_name->name);
}
}
done:
if (name) {
seq_pad(m, ' ');
seq_puts(m, name);
}
seq_putc(m, '\n');
}
show_map_vma 是导出符号.
after hook 从 seq_file 找 关键字, frida .
问题: 视频演示, 应该6.1 的内核, 但是 5.10 源代码, 并没有 , get_vma_name() 这个函数.
要隐藏 Frida 的话, 它的 name 和 trace , 不能确定是哪一个, 但是它的name 和 trace, 肯定包含, Frida Agent 输出的字符串.
内存特征, 比如 frida:rpc, FridaScriptEngine, 以及一些不可见的 byte 特侦, 都在 frida-agent 段里面.
oriole:/ # cat /proc/6522/maps | grep frida
7391e2a000-7392822000 r--p 00000000 00:01 6423 /memfd:frida-agent-64.so (deleted)
7392823000-7393542000 r-xp 009f8000 00:01 6423 /memfd:frida-agent-64.so (deleted)
7393542000-7393612000 r--p 01716000 00:01 6423 /memfd:frida-agent-64.so (deleted)
7393613000-739362f000 rw-p 017e6000 00:01 6423 /memfd:frida-agent-64.so (deleted)
oriole:/ #
# 常规的 f1613.的版本 , 在 oriole 版本下的输出 的so.
内核层面修改隐藏 maps 文件, 隐藏 frida 相关 的内存, 通过是绕过 maps 检测 和 内存特征检测.
字符串, 大致在, struct seq_file *m 这一块 输出的.
我们不需要这一行, 在 show_map_vma ( ) 这一行, 进入 之前的 这一行数据, 整个输出,我们不要了.
做法, 在 after hook 从 seq_file 找到 关键字, frida , 找到了, 我们就不要了.
如果找到了 , 这个关键的, frida-agent 的这个 show_map_vma (() 的调用 , 它作用就是输出 frida 所占用的内存.
我们不想输出, 但是 after hook , 它的已经被输出到 seq_file 里面了.
用到前面的方法, 把 seq_file 的 count, 改了, 改到, 进入到 show_map_vma( ) 这个进入的时候, 有多大?
退出的时候, 也给他改成多大 !
效果: 从 show_map_vma( ) 函数, 进入到 结束, 输出, 都被删除掉了.
头疼的地方, -> after hook 中 如何从 seq_file 删除 当前 show_map_vma( ) 输出的全部数据, 而不影响其他调用数据?
巧妙的地方 -> , before hook 记录 seq_file 中的 count输出多少字节 , 在 after hook 中 设置 count 为记录值, 效果上等价于 删除.
for taskdir in /proc/22495/task/*; do
tid=$(basename "$taskdir")
name=$(grep 'Name:' "$taskdir/status" 2>/dev/null | awk '{print $2}')
[ -n "$name" ] && echo "TID: $tid, Name: $name"
done
# 进阶版本:输出 TID 与线程名
TID: 22495, Name: yimian.envcheck
TID: 22504, Name: Signal
TID: 22505, Name: perfetto_hprof_
TID: 22506, Name: Jit
TID: 22507, Name: HeapTaskDaemon
TID: 22508, Name: ReferenceQueueD
TID: 22509, Name: FinalizerDaemon
TID: 22510, Name: FinalizerWatchd
TID: 22517, Name: binder:22495_1
TID: 22518, Name: binder:22495_2
TID: 22520, Name: Profile
TID: 22522, Name: RenderThread
TID: 22524, Name: mali-mem-purge
TID: 22525, Name: mali-utility-wo
TID: 22526, Name: mali-utility-wo
TID: 22527, Name: mali-utility-wo
TID: 22528, Name: mali-utility-wo
TID: 22529, Name: mali-utility-wo
TID: 22530, Name: mali-utility-wo
TID: 22531, Name: mali-utility-wo
TID: 22532, Name: mali-utility-wo
TID: 22533, Name: mali-cmar-backe
TID: 22538, Name: hwuiTask0
TID: 22539, Name: hwuiTask1
TID: 22540, Name: SurfaceSyncGrou
TID: 22542, Name: binder:22495_3
TID: 22544, Name: binder:22495_3
TID: 22635, Name: binder:22495_4
TID: 22656, Name: yimian.envcheck
TID: 22657, Name: gmain
TID: 22659, Name: gdbus
TID: 22660, Name: Thread-2
oriole:/ #
今天回头看, 昨天的 带着 加上一个 width ,
才想起, 给 制表符加上一个宽度…..
结合
int proc_pid_status(struct seq_file *m, struct pid_namespace *ns,
struct pid *pid, struct task_struct *task)
这个函数就是 /proc//status(和 /proc//task//status) 的生成者。
就知道了, 为什么要加上一个循环. 所有 线程下的 Name .
PID 的, TracerPid, TID 下的, TracerPid, 原来都有. 内核层的hook 这些拦截不让出现.
线程名特征
_get_task_comm ,
获取进程/线程名.
导出符号.
