目前绝大部分app都会频繁的使用syscall去获取设备指纹和做一些反调试,使用常规方式过反调试已经非常困难了,使用内存搜索svc指令已经不能满足需求了,开始学习了一下通过ptrace/ptrace配合seccomp来解决svc反调试难定位难绕过等问题。
Linux 2.6.12中的导入了第一个版本的seccomp,通过向/proc/PID/seccomp接口中写入“1”来启动通过滤器只支持几个函数。
1
|
read(),write(),_exit(),sigreturn()
|
使用其他系统调用就会收到信号(SIGKILL)退出。测试代码如下:
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
|
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
void configure_seccomp() {
printf(
"Configuring seccomp\n"
);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);
}
int
main(
int
argc, char
*
argv[]) {
int
infd, outfd;
if
(argc <
3
) {
printf(
"Usage:\n\t%s <input path> <output_path>\n"
, argv[
0
]);
return
-
1
;
}
printf(
"Starting test seccomp Y/N?"
);
char c
=
getchar();
if
(c
=
=
'y'
|| c
=
=
'Y'
) configure_seccomp();
printf(
"Opening '%s' for reading\n"
, argv[
1
]);
if
((infd
=
open
(argv[
1
], O_RDONLY)) >
0
) {
ssize_t read_bytes;
char
buffer
[
1024
];
printf(
"Opening '%s' for writing\n"
, argv[
2
]);
if
((outfd
=
open
(argv[
2
], O_WRONLY | O_CREAT,
0644
)) >
0
) {
while
((read_bytes
=
read(infd, &
buffer
,
1024
)) >
0
)
write(outfd, &
buffer
, (ssize_t)read_bytes);
}
close(infd);
close(outfd);
}
printf(
"End!\n"
);
return
0
;
}
|
可以看到执行到22行就结束了没执行到 Eed.
Seccomp-BPF(Berkeley Packet Filter)是Linux内核中的一种安全机制,用于限制进程对系统调用的访问权限。它主要用于防止恶意软件对系统的攻击,提高系统的安全性。
Seccomp-BPF使用BPF(Berkeley Packet Filter)技术来实现系统调用过滤,可以使用BPF程序指定哪些系统调用可以被进程访问,哪些不能。BPF程序由一组BPF指令组成,可以在系统调用执行之前对其进行检查,以决定是否允许执行该系统调用。
Seccomp-BPF提供了两种模式:白名单模式和黑名单模式。白名单模式允许所有系统调用,除非明确指定不允许的系统调用。黑名单模式禁止所有系统调用,除非明确指定允许的系统调用。这两种模式的选择取决于您的实际需求。
Seccomp-BPF提供了一个钩子函数,在系统调用执行之前会进入到这个函数,对系统调用进行检查,如果BPF程序允许执行该系统调用,则进程可以继续执行,否则会抛出一个异常。
1
2
3
4
5
6
7
8
9
10
11
|
简单指令集
小型指令集
所有的命令大小相一致
实现过程简单、快速
只有分支向前指令
程序是有向无环图(DAGs),没有循环
易于验证程序的有效性
/
安全性
简单的指令集⇒可以验证操作码和参数
可以检测死代码
程序必须以 Return 结束
BPF过滤器程序仅限于
4096
条指令
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
Conditional JMP(条件判断跳转)
当匹配条件为真,跳转到true指定位置
当 匹配条件为假,跳转到false指定位置
跳转偏移量最大
255
JMP(直接跳转)
跳转目标是指令偏移量
跳转 偏移量最大
255
Load(数据读取)
读取程序参数
读取指定的
16
位内存地址
Store(数据存储)
保存数据到指定的
16
位内存地址中
支持的运算
+
-
*
/
& | ^ >> << !
返回值
SECCOMP_RET_ALLOW
-
允许继续使用系统调用
SECCOMP_RET_KILL
-
终止系统调用
SECCOMP_RET_ERRNO
-
返回设置的errno值
SECCOMP_RET_TRACE
-
通知附加的ptrace(如果存在)
SECCOMP_RET_TRAP
-
往进程发送 SIGSYS信号
最多只能有
4096
条命令
不能出现循环
|
Seccomp-BPF程序 接收以下结构作为输入参数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
/
*
*
*
struct seccomp_data
-
the
format
the BPF program executes over.
*
@nr: the system call number
*
@arch: indicates system call convention as an AUDIT_ARCH_
*
value
*
as defined
in
<linux
/
audit.h>.
*
@instruction_pointer: at the time of the system call.
*
@args: up to
6
system call arguments always stored as
64
-
bit values
*
regardless of the architecture.
*
/
struct seccomp_data {
int
nr;
__u32 arch;
__u64 instruction_pointer;
__u64 args[
6
];
};
|
在这种情况下,seccomp-BPF 程序将允许使用 O_RDONLY 参数打开第一个调用 , 但是在使用 O_WRONLY | O_CREAT 参数调用 open 时终止程序。
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
|
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stddef.h>
#include <sys/prctl.h>
#include <linux/seccomp.h>
#include <linux/filter.h>
#include <linux/unistd.h>
void configure_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_write,
0
,
1
),
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_open,
0
,
3
),
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, (offsetof(struct seccomp_data, args[
1
]))),
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)
};
struct sock_fprog prog
=
{
.
len
=
(unsigned short)(sizeof(
filter
)
/
sizeof (
filter
[
0
])),
.
filter
=
filter
,
};
printf(
"Configuring seccomp\n"
);
prctl(PR_SET_NO_NEW_PRIVS,
1
,
0
,
0
,
0
);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);
}
int
main(
int
argc, char
*
argv[]) {
int
infd, outfd;
ssize_t read_bytes;
char
buffer
[
1024
];
if
(argc <
3
) {
printf(
"Usage:\n\tdup_file <input path> <output_path>\n"
);
return
-
1
;
}
printf(
"Ducplicating file '%s' to '%s'\n"
, argv[
1
], argv[
2
]);
configure_seccomp();
/
/
配置seccomp
printf(
"Opening '%s' for reading\n"
, argv[
1
]);
if
((infd
=
open
(argv[
1
], O_RDONLY)) >
0
) {
printf(
"Opening '%s' for writing\n"
, argv[
2
]);
if
((outfd
=
open
(argv[
2
], O_WRONLY | O_CREAT,
0644
)) >
0
) {
while
((read_bytes
=
read(infd, &
buffer
,
1024
)) >
0
)
write(outfd, &
buffer
, (ssize_t)read_bytes);
}
}
close(infd);
close(outfd);
return
0
;
}
|
将getpid()的实现改为mkdir()的实现。主要是通过ptrace函数来跟踪子进程,获取其寄存器中的信息,然后根据需求替换对应的系统调用。
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
76
77
|
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/user.h>
#include <sys/signal.h>
#include <sys/wait.h>
#include <sys/ptrace.h>
#include <sys/fcntl.h>
#include <syscall.h>
void die (const char
*
msg)
{
perror(msg);
exit(errno);
}
void attack()
{
int
rc;
syscall(SYS_getpid, SYS_mkdir,
"dir"
,
0777
);
}
int
main()
{
int
pid;
struct user_regs_struct regs;
switch( (pid
=
fork()) ) {
case
-
1
: die(
"Failed fork"
);
case
0
:
ptrace(PTRACE_TRACEME,
0
, NULL, NULL);
kill(getpid(), SIGSTOP);
attack();
return
0
;
}
waitpid(pid,
0
,
0
);
while
(
1
) {
int
st;
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
if
(waitpid(pid, &st, __WALL)
=
=
-
1
) {
break
;
}
if
(!(WIFSTOPPED(st) && WSTOPSIG(st)
=
=
SIGTRAP)) {
break
;
}
ptrace(PTRACE_GETREGS, pid, NULL, ®s);
printf(
"orig_rax = %lld\n"
, regs.orig_rax);
if
(regs.rax !
=
-
ENOSYS) {
continue
;
}
if
(regs.orig_rax
=
=
SYS_getpid) {
regs.orig_rax
=
regs.rdi;
regs.rdi
=
regs.rsi;
regs.rsi
=
regs.rdx;
regs.rdx
=
regs.r10;
regs.r10
=
regs.r8;
regs.r8
=
regs.r9;
regs.r9
=
0
;
ptrace(PTRACE_SETREGS, pid, NULL, ®s);
}
}
return
0
;
}
|
看一下main函数这里设置了跟踪openat系统调用子进程请求父进程附加 父进程开启ptrace+seccomp。
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
|
int
main()
{
pid_t pid;
int
status;
if
((pid
=
fork())
=
=
0
) {
/
*
目前是跟踪
open
系统调用
*
/
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
,
1
),
BPF_STMT(BPF_RET
+
BPF_K, SECCOMP_RET_TRACE),
BPF_STMT(BPF_RET
+
BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog
=
{
.
filter
=
filter
,
.
len
=
(unsigned short) (sizeof(
filter
)
/
sizeof(
filter
[
0
])),
};
/
/
告诉父进程允许子进程跟踪
ptrace(PTRACE_TRACEME,
0
,
0
,
0
);
/
*
避免需要 CAP_SYS_ADMIN
*
/
if
(prctl(PR_SET_NO_NEW_PRIVS,
1
,
0
,
0
,
0
)
=
=
-
1
) {
perror(
"prctl(PR_SET_NO_NEW_PRIVS)"
);
return
1
;
}
if
(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)
=
=
-
1
) {
perror(
"when setting seccomp filter"
);
return
1
;
}
kill(getpid(), SIGSTOP);
ssize_t count;
char buf[
256
];
int
fd;
fd
=
syscall(__NR_openat,fd,
"/data/local/tmp/tuzi.txt"
, O_RDONLY);
syscall(__NR_openat,fd,
"/data/local/tmp/asdss.txt"
, O_RDONLY);
syscall(__NR_openat,fd,
"/data/local/tmp/asda.txt"
, O_RDONLY);
syscall(__NR_openat,fd,
"/data/local/tmp/TsdsaWO.txt"
, O_RDONLY);
syscall(__NR_openat,fd,
"/data/local/tmp/sadas.txt"
, O_RDONLY);
syscall(__NR_openat,fd,
"/data/local/tmp/sad.txt"
, O_RDONLY);
syscall(__NR_openat,fd,
"/data/local/tmp/asda.txt"
, O_RDONLY);
/
/
printf(
"fd : %d \n"
,fd);
if
(fd
=
=
-
1
) {
perror(
"open"
);
return
1
;
}
while
((count
=
syscall(__NR_read, fd, buf, sizeof(buf))) >
0
) {
syscall(__NR_write, STDOUT_FILENO, buf, count);
}
syscall(__NR_close, fd);
}
else
{
waitpid(pid, &status,
0
);
/
/
尝试开启ptrace
+
seccomp
ptrace(PTRACE_SETOPTIONS, pid,
0
, PTRACE_O_TRACESECCOMP);
process_signals(pid);
return
0
;
}
}
|
下面来解释一下bpf结构,BPF 被定义为一种虚拟机 (VM),它具有一个数据寄存器或累加器、一个索引寄存器和一个隐式程序计数器 (PC)。它的“汇编”指令被定义为具有以下格式的结构:
1
2
3
4
5
6
|
struct sock_filter {
u_short code;
u_char jt;
u_char jf;
u_long k;
};
|
有累加器,跳转等待码(操作码),jt和jf是跳转指令中使用的程序计数器的增量,而k是一个辅助值,其用法取决于代码编号。
BPFs有一个可寻址空间,其中的数据在网络情况下是一个数据包数据报,对于seccomp有一下结构:
1
2
3
4
5
6
7
|
struct seccomp_data {
int
nr;
/
*
System call number
*
/
__u32 arch;
/
*
AUDIT_ARCH_
*
value
(see <linux
/
audit.h>)
*
/
__u64 instruction_pointer;
/
*
CPU instruction pointer
*
/
__u64 args[
6
];
/
*
Up to
6
system call arguments
*
/
};
|
所以bpfs在seccomp中做的是对这些数据进行操作并返回一个值告诉内核下一步做什么,比如:
1
2
|
允许进程执行调用(SECCOMP_RET_ALLOW)
终止(SECCOMP_RET_KILL)
|
详细见文档:
现在我们可以根据系统调用号和参数进行过滤,bpf过滤器被定义为一个sock_filter结构,其中每条都是一个bpf指令。
1
2
3
4
|
BPF_STMT(BPF_LD
+
BPF_W
+
BPF_ABS, offsetof(struct seccomp_data, nr)),
BPF_JUMP(BPF_JMP
+
BPF_JEQ
+
BPF_K, __NR_openat,
0
,
1
),
BPF_STMT(BPF_RET
+
BPF_K, SECCOMP_RET_TRACE),
BPF_STMT(BPF_RET
+
BPF_K, SECCOMP_RET_ALLOW),
|
BPF_STMT和BPF_JUMP是两个填充sock_filter结构的简单红。他在参数上有所不同。其中包括BPF_JUMP中的跳跃偏移量。在两种情况下。第一个参数都是操作码,作为助记符帮助:例如,第一个参数是使用绝对寻址(BPF_ABS) 将一个字 (BPF_W) 加载到累加器 (BPF_LD) 中。
第一条指令是要求VM将呼叫号码加载nr到累加器。第二条将与openat的系统调用号进行比较。如果他们相等(pc + o),则要求vm不修改计数器。因此运行第三条指令,否则跳转到PC+1,这是第四条指令(当执行到这条指令时,pc已经指向第三条指令)。因此如果这是一个开放的系统调用,我们将返回SECCOMP_RET_TRACE,这会调用跟踪器否则返回SECCOMP_RET_ALLOW,这将会让没有被跟踪的系统调用直接执行。
然后是第一次调用 prctl 设置PR_SET_NO_NEW_RPIVS,这会阻止子进程拥有比父进程更多的权限。他使用PR_SET_SECCOMP选择设置seccomp过滤器,不是root用户也可以使用,之后使用openat系统调用进行打开文件等操作。
父进程我设置了PTRACE_O_TRACESECCOMP 选项,当过滤器返回 SECCOMP_RET_TRACE 并将事件信号发送给跟踪器时,跟踪器将停止。此函数的另一个变化是我们不再需要设置 PTRACE_O_TRACESYSGOOD,因为我们被 seccomp 中断,而不是因为系统调用。
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
|
static void process_signals(pid_t child)
{
char file_to_redirect[
256
]
=
"/data/local/tmp/tuzi1.txt"
;
char file_to_avoid[
256
]
=
"/data/local/tmp/tuzi.txt"
;
int
status;
while
(
1
) {
char orig_file[PATH_MAX];
struct user_pt_regs regs;
struct iovec io;
io.iov_base
=
®s;
io.iov_len
=
sizeof(regs);
ptrace(PTRACE_CONT, child,
0
,
0
);
waitpid(child, &status,
0
);
ptrace(PTRACE_GETREGSET, child, (void
*
)NT_PRSTATUS, &io);
if
(status >>
8
=
=
(SIGTRAP | (PTRACE_EVENT_SECCOMP <<
8
)) ){
switch (regs.regs[
8
])
{
case __NR_openat:
read_file(child, orig_file,regs);
if
(strcmp(file_to_avoid, orig_file)
=
=
0
){
putdata(child,regs.regs[
1
],file_to_redirect,strlen(file_to_avoid)
+
1
);
}
}
if
(WIFEXITED(status)){
break
;
}
}
}
|
这里就很简单了获取到svc的信号后读取x8寄存器判断是否为openat的系统调用号,这里只对file_to_avoid进行了替换,看一下最终效果:
可以看到不仅只对openat进行了监控也成功的将了第一次打开的文件
/data/local/tmp/tuzi.txt修改为了/data/local/tmp/tuzi1.txt。
完结撒花!
更多【seccomp-bpf+ptrace实现修改系统调用原理(附demo)】相关视频教程:www.yxfzedu.com