作者:yueji0j1anke
首发于公号:剑客古月的安全屋
字数:4234
阅读时间: 30min
声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。合法渗透,本文章内容纯属虚构,如遇巧合,纯属意外
目录
-
前言
-
前置知识
-
测试demo
-
原理探索
-
深度利用ptrace
-
Frida-Seccomp
0x00 前言
最近遇到了一些在svc层开发的app,需要进行hook,无奈只好回来重新学ptrace
很多大厂会对open、read等函数通过svc重写实现系统调用,此时我们想要hook系统调用只能通过定位svc指令所在位置进行inline hook,整个过程很容易被检测到,有什么简单而实用的hook方式吗?
有的兄弟,有的!
文章参考:
https://bbs.kanxue.com/thread-277544.htm
0x01 前置知识
1.ptrace
什么是ptrace?
ptrace是linux提供的调试函数,gdb、ida等工具都是以此为基础去开发设计
ptrace允许一个进程监控和控制另一个进程的执行
#include <sys/ptrace.h>
longptrace(enum__ptrace_requestrequest, pid_tpid, void*addr, void*data);
一共有四个参数:
-
request
: 表示要执行的操作类型。//反调试会用到PT_DENY_ATTACH
,调试会用到PTRACE_ATTACH
-
pid
: 要操作的目标进程ID -
addr
: 要监控的目标内存地址 -
data
: 保存读取出或者要写入的数据详情请参看man手册https://man7.org/linux/man-pages/man2/ptrace.2.html
2.Seccomp
类似于我们前面讲的selinux,但这里Seccomp是一个内核安全模块,可以使进程限制可以进行的系统调用数量,提高进程的安全性。同时还提供了轻量级的进程隔离方式。
而其主要工作流程我们也需要了解->在进程中使用prctl()
系统调用来指定一个过滤规则集,该规则集定义了进程允许使用的系统调用的类型和参数
该规则详情如下
SECCOMP_RET_ALLOW:允许系统调用通过。
SECCOMP_RET_KILL_PROCESS:杀死整个进程,即结束进程。
SECCOMP_RET_TRAP:禁止并强制引发 SIGSYS 信号(与 SIGILL、SIGABRT 类似)。
SECCOMP_RET_TRACE:允许并将事件传递给跟踪器(tracee)。
SECCOMP_RET_KILL_THREAD:杀死线程,即终止当前线程。
SECCOMP_RET_KILL:与 SECCOMP_RET_KILL_THREAD 含义相同,只是别名。
SECCOMP_RET_ERRNO:返回一个 errno 错误码。
SECCOMP_RET_USER_NOTIF:通知用户空间的监听程序,即向用户态传递信息。
SECCOMP_RET_LOG:记录事件到内核日志中。
这里借助网上的demo给大家讲解一下
voidconfigure_seccomp()
{
// struct sock_filter filter[] = {
// BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, nr))),
// BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 0, 3),
// BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, args[2]))),
// BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, O_RDONLY, 0, 1),
// BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
// BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL)
// };
structsock_filterfilter[] = {
BPF_STMT(BPF_LD|BPF_W|BPF_ABS, (offsetof(structseccomp_data, nr))), //L1
//从 seccomp 数据中读取当前被调用的系统调用号
BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, __NR_openat, 0, 2),//L2
//比较上一步加载的系统调用号与 __NR_openat 是否相等
//相等的话,继续往下执行,跳转0步
//不相等,跳转2步,到L5执行,SECCOMP_RET_ALLOW允许执行
BPF_STMT(BPF_LD|BPF_W|BPF_ABS, (offsetof(structseccomp_data, args[2]))),//L3
//读取arg2
BPF_JUMP(BPF_JMP|BPF_JEQ|BPF_K, O_RDONLY, 0, 1),//L4
//arg2是不是O_RDONLY
//如果是,则SECCOMP_RET_ALLOW允许执行
//如果不是,则跳转1步,SECCOMP_RET_KILL进程终止
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_ALLOW),//L5
BPF_STMT(BPF_RET|BPF_K, SECCOMP_RET_KILL)//L6
};
//open()函数底层会调用__NR_openat,而不是
structsock_fprogprog= {
.len= (unsignedshort)(sizeof(filter) /sizeof(filter[0])),
.filter=filter,
};
printf("Configuring seccompn");
prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}
代码大致意思
1. 从seccomp数据中读取当前被调用的系统调用号
2. 检测系统调用号是否为__NR_openat(即系统对"openat"的调用)
3. 读取系统调用的第三个参数(args[2])
4. 该参数是否为O_RDONLY标识(只读模式)
5. 如果判断成功,允许执行openat,如果失败,则直接kill掉
这里有的demo大家可以自行尝试一下
https://github.com/callrsp/attachment/blob/master/svc-learn/2.seccomp_linux_amd64/demo.c
3.ptrace+Seccomp
可能看到这里有些师傅已经明白了。
ptrace 可以监控系统调用,Seccomp可以对系统调用进行规则过滤,那这不就相当于if else + frida的一个hook逻辑吗
比如说系统要读取一个文件,我们是否能实现讲他重定向到另一个文件呢
https://github.com/callrsp/attachment/blob/master/svc-learn/3.ptrace-seccomp-linux_amd64/demo.c
这个代码很好实现了相关逻辑
子进程启动后,通过ptrace(PTRACE_TRACEME,...)设置自身为被跟踪状态,然后使用prctl配置seccomp规则以便后续的系统调用监控。
使用kill(getpid(), SIGSTOP);实现暂停,等待父进程设置信号处理。
随后通过seccomp配置一个规则来监控openat系统调用。
当openat调用发生时,该进程会触发一个SECCOMP事件,并通知父进程。
子进程在受到监控情况下,执行打开、读取文件的系统调用。
ptrace工具允许父进程在捕捉到openat系统调用时,检查子进程试图访问的文件名,并用不同的文件路径替换它。
父进程使用waitpid()监控子进程状态变更。
当子进程触发SECCOMP_RET_TRACE时,ptrace允许父进程取得控制。
ptrace(PTRACE_GETREGS, ...)获取子进程寄存器状态,以取得当前系统调用编号。
使用read_filename_from_regs()和putdata_to_regs()来读取并修改子进程试图打开的文件名。
如果试图打开特定文件(如 /home/kali/tmp/flag.txt),则会重定向到另一个文件(如 /home/kali/tmp/hacker)。
看这里demo
0x02 测试demo
注意这里
需要两个根级目录
这里我直接用的框架去进行学习
首先配置好cmake
编译
随后进行测试
./Inject -p 13569 -so /data/local/tmp/libHook.so -symbols hello
google和小米手机都测试成功,兼容性还蛮不错的,值得学习
0x03 原理探索
核心其实就在于该函数
int inject_remote_process(pid_t pid, char *LibPath, char *FunctionName, char *FlagSELinux)
在该函数中,会帮忙处理selinux安全机制
参数
-
pid
: 被注入的目标进程ID。 -
LibPath
: 被注入的共享库路径。 -
FunctionName
: 注入的共享库中需要调用的函数名。 -
FlagSELinux
: 标识 SELinux 状态,以便在注入完成后恢复原状态。
步骤
附加到目标进程: 使用 ptrace_attach
附加到目标进程,便于控制和修改目标进程的执行。
-
获取和保存寄存器状态: 拿到当前的寄存器状态,并保存原始状态以便后续恢复。
-
查找函数地址: 获取目标进程中必要函数(
mmap
,dlopen
,dlsym
,dlclose
和dlerror
)的地址。 -
内存映射: 调用目标进程的
mmap
函数分配一块内存,用于存储共享库路径及其它所需数据。 -
写入共享库路径: 将输入的
LibPath
写入到目标进程的内存。 -
调用
dlopen
: 加载共享库并获得其返回地址(即模块加载地址)。 -
错误处理: 如果
dlopen
返回空指针,调用dlerror
获取错误信息。 -
调用
dlsym
: 查找并调用目标进程中的函数(如果给出了FunctionName
)。 -
恢复寄存器: 恢复原先保存的寄存器状态,这样目标进程可以继续正常执行。
-
分离进程: 使用
ptrace_detach
分离,停止对目标进程的控制。
除此之外
这里查找地址可以任意替换,比如dlopen,dlsym
0x04 利用ptrace hook内核方法打印目标进程
我们现在机子是mac,因此还是采用cmake去进行编译
其实我们学习框架,就是要打造出属于我们自己的ptrace框架,用顺手这样方便hook
这里给出我的hook demp
intmain(intargc, char*argv[])
{
if (argc!=2) {
fprintf(stderr, "Usage: %s <PID>n", argv[0]);
return1;
}
pid_tchild=atoi(argv[1]);
intiRet=-1;
if (ptrace_attach(child) !=0) {
returniRet;
}
longorig_rax, rax;
intstatus;
structuser_regs_structregs;
intsyscall=0;
char*str;
while (1) {
ptrace(PTRACE_SYSCALL, child, NULL, NULL);
wait(&status);
if(WIFEXITED(status)) {
break;
}
orig_rax=ptrace(PTRACE_PEEKUSER, child, 8*ORIG_RAX, NULL); // 保存的系统调用号,进行的是当前系统调用号的获取
printf("系统调用号 %ld -> 系统调用名 %s n", orig_rax, find_syscall_symbol(orig_rax));
printf("=====================================n");
// if (orig_rax == 0) {
//
// }
}
ptrace_detach(child);
}
编译好后,执行
可以直接hook住内核系统调用的方法名,下边更近一步,打印参数
在安卓平台写好配置文件编译即可
export ANDROID_NDK=/Users//Library/Android/sdk/ndk/27.0.12077973
export PATH=$PATH:$ANDROID_NDK/toolchains/llvm/prebuilt/darwin-x86_64/bin
$ANDROID_NDK/ndk-build NDK_PROJECT_PATH=. APP_BUILD_SCRIPT=./Android.mk NDK_APPLICATION_MK=./Application.mk
0x05 Frida-Seccomp
接下来,版本迭代到了我们的
https://github.com/Abbbbbi/Frida-Seccomp
如何快速配合我们的frida进行捕捉和修改
->写Seccomp规则
如果是目标调用, 那么返回 SECCOMP_RET_TRAP, 其它调用允许执行.
SECCOMP_RET_TRAP会触发一个 SIGSYS 信号(与 SIGILL、SIGABRT 类似)。同时拒绝系统调用.
原作者使用frida自带的Process.setExceptionHandler函数, 即可捕获异常SIGSYS, 并在自己写的回调中进行数据处理而且frida提供了cmodule模块~巧妙,巧妙!
在so加载时,直接hook之前流程
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter(args)
{
if (g_lpf_cm_install_seccomp_filter==null)
{
hook_init();//第一次加载so时,就初始化init()
}
}
})
Hook_init函数对应如下
//CModule已经初始化好了
functionhook_init()
{
//初始化,需要在主线程初始化且需要一个比较早的时机,frida脚本自己创建的一个C线程(没被安装seccomp规则)
//CModule层,函数获取
g_lpf_cm_thread_syscall_tvar=newNativeFunction(cm.pthread_syscall_create, "pointer", [])() //创建一个没有被seccomp过滤的线程
//woc,这里不仅仅获取了thread_syscall_t,而且还调用了pthread_syscall_create.此刻已有线程被创建
g_lpf_cm_findSoinfoByAddr=newNativeFunction(cm.findSoinfoByAddr, "pointer", ["pointer"])
g_lpg_cm_get_base=newNativeFunction(cm.get_base, "uint64", ["pointer"])
g_lpf_cm_get_size=newNativeFunction(cm.get_size, "size_t", ["pointer"])
g_lpf_cm_call_task=newNativeFunction(cm.call_task, "pointer", ["pointer", "pointer", "int"])
g_lpf_cm_install_seccomp_filter=newNativeFunction(cm.install_seccomp_filter, "int", ['uint32'])
g_lpf_cm_lock=newNativeFunction(cm.lock, "int", ["pointer"])
g_lpf_cm_unlock=newNativeFunction(cm.unlock, "int", ["pointer"])
// 异常处理 捕获seccomp异常 <-- SECCOMP_RET_TRAP
Process.setExceptionHandler(function (details)
{
constcurrent_off=details.context.pc-4; //指向当前异常发生的pc, 本来pc是指向异常的下一个位置
// 010000d4 是4字节
// 判断是否是seccomp导致的异常 读取opcode 010000d4 == svc 0
if (details.message=="system error"
&&details.type=="system"
&&utils_hex(ptr(current_off).readByteArray(4)) =="010000d4") //机器码 D4000001 `svc #0x0`
{
//进入SVC异常处理
// 上锁避免多线程问题
// g_lpf_cm_lock(g_lpf_cm_thread_syscall_tvar); //源代码感觉写得有问题
// 获取x8寄存器中的调用号
constnr_syscall_id=details.context.x8.toString(10);
letloginfo="n=================="
loginfo+=`nSVC[${syscall_enum_infos[nr_syscall_id][1]}|${nr_syscall_id}] ==> PC:${utils_addrToString(current_off)}Pid:${Process.id}Tid:${Process.getCurrentThreadId()}`
// 构造线程syscall调用参数
constargs=Memory.alloc(7*8)
args.writePointer(details.context.x8)
letargs_reg_arr= {}
for (letindex=0; index<6; index++)
{
//eval 动态执行js代码
eval(`args.add(8 * (index + 1)).writePointer(details.context.x${index})`)//分别获取当前寄存器参数 x0,x1,x2,x3,x4,x5,x6,写入开辟的c内存中
eval(`args_reg_arr["arg${index}"] = details.context.x${index}`) //同时写入js的变量中
}
// 获取手动堆栈信息
loginfo+="n"+utils_stacktrace(ptr(current_off), details.context.fp, details.context.sp).map(utils_addrToString).join('n')
// 打印传参
loginfo+="nargs = "+JSON.stringify(args_reg_arr)
// 调用线程syscall 赋值x0寄存器
details.context.x0=g_lpf_cm_call_task(g_lpf_cm_thread_syscall_tvar, args, 0);//传递线程函数? 寄存器参数,
loginfo+="nret = "+details.context.x0.toString()
// 打印信息
utils_call_thread_log(loginfo)
// 解锁
// g_lpf_cm_unlock(g_lpf_cm_thread_syscall_tvar)
returntrue;
}
returnfalse;
})
// openat的调用号
g_lpf_cm_install_seccomp_filter(Target_NR); //开启线程过滤
}
核心逻辑
-
通过C模块的NativeFunction进行初始化:
-
CModule
已经初始化,包括对pthread和其它C函数的包装。 -
pthread_syscall_create
用于创建一个不受Seccomp过滤的线程。这是为了能够在受Seccomp保护的程序中执行某些系统调用。初始化过程中,这些函数在CModule
中定义,Frida以其NativeFunction
进行包装。 -
捕获异常并处理Seccomp触发:
-
锁定执行线程。
-
读取当前指令和寄存器状态,包括x8中存储的系统调用号。
-
保存上下文和参数以便后续适配处理。
-
在同一个未被Seccomp限制的线程中执行系统调用。
-
打印和记录信息以便调试(如调用堆栈、参数等)。
-
解锁执行线程。
-
Process.setExceptionHandler()
用于定义一个异常处理器,可以捕获程序运行时的各种异常。 -
该处理器专用于捕获Seccomp返回的
SECCOMP_RET_TRAP
异常。 -
如果触发了Seccomp异常(通过判断
details.message
和机器码):
-
对特定系统调用进行Seccomp过滤:
-
通过
g_lpf_cm_install_seccomp_filter(Target_NR);
开启Seccomp过滤,也就是限制当前进程的系统调用,只允许过滤器表中允许的调用。
项目效果可以参照
https://github.com/Abbbbbi/Frida-Seccomp
这里我在回调函数增加了一些内容,就可以打印出收集的文件信息
市面上大多数的svc hook工具其实都只能实现对应的检测功能
后续将研究研究推出svc层重定向工具。
原文始发于微信公众号(剑客古月的安全屋):app攻防-SVC的终极奥义Ptrace+Seccomp
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论